commit 90687b56a41e8d10e5c8a936069d16374a929e3e Author: VirtuBrick <139835327+VirtuBrick@users.noreply.github.com> Date: Thu Feb 22 12:15:30 2024 -0500 Initial commit diff --git a/Client2014/AppSettings.xml b/Client2014/AppSettings.xml new file mode 100644 index 0000000..f51afc2 --- /dev/null +++ b/Client2014/AppSettings.xml @@ -0,0 +1,5 @@ + + + content + http://www.syntax.eco + diff --git a/Client2014/Log.dll b/Client2014/Log.dll new file mode 100644 index 0000000..ec697f0 Binary files /dev/null and b/Client2014/Log.dll differ diff --git a/Client2014/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest b/Client2014/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest new file mode 100644 index 0000000..41623b1 --- /dev/null +++ b/Client2014/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest @@ -0,0 +1,13 @@ + + + + + + + diff --git a/Client2014/Microsoft.VC90.CRT/msvcm90.dll b/Client2014/Microsoft.VC90.CRT/msvcm90.dll new file mode 100644 index 0000000..b9cb123 Binary files /dev/null and b/Client2014/Microsoft.VC90.CRT/msvcm90.dll differ diff --git a/Client2014/Microsoft.VC90.CRT/msvcp90.dll b/Client2014/Microsoft.VC90.CRT/msvcp90.dll new file mode 100644 index 0000000..6b07c75 Binary files /dev/null and b/Client2014/Microsoft.VC90.CRT/msvcp90.dll differ diff --git a/Client2014/Microsoft.VC90.CRT/msvcr90.dll b/Client2014/Microsoft.VC90.CRT/msvcr90.dll new file mode 100644 index 0000000..a68249a Binary files /dev/null and b/Client2014/Microsoft.VC90.CRT/msvcr90.dll differ diff --git a/Client2014/Microsoft.VC90.MFC/Microsoft.VC90.MFC.manifest b/Client2014/Microsoft.VC90.MFC/Microsoft.VC90.MFC.manifest new file mode 100644 index 0000000..0184745 --- /dev/null +++ b/Client2014/Microsoft.VC90.MFC/Microsoft.VC90.MFC.manifest @@ -0,0 +1,13 @@ + + + + + + + diff --git a/Client2014/Microsoft.VC90.MFC/mfc90.dll b/Client2014/Microsoft.VC90.MFC/mfc90.dll new file mode 100644 index 0000000..31fb2b3 Binary files /dev/null and b/Client2014/Microsoft.VC90.MFC/mfc90.dll differ diff --git a/Client2014/Microsoft.VC90.MFC/mfcm90.dll b/Client2014/Microsoft.VC90.MFC/mfcm90.dll new file mode 100644 index 0000000..55b19e3 Binary files /dev/null and b/Client2014/Microsoft.VC90.MFC/mfcm90.dll differ diff --git a/Client2014/Microsoft.VC90.OPENMP/Microsoft.VC90.OpenMP.manifest b/Client2014/Microsoft.VC90.OPENMP/Microsoft.VC90.OpenMP.manifest new file mode 100644 index 0000000..213b1d7 --- /dev/null +++ b/Client2014/Microsoft.VC90.OPENMP/Microsoft.VC90.OpenMP.manifest @@ -0,0 +1,13 @@ + + + + + + + diff --git a/Client2014/Microsoft.VC90.OPENMP/vcomp90.dll b/Client2014/Microsoft.VC90.OPENMP/vcomp90.dll new file mode 100644 index 0000000..cf7c805 Binary files /dev/null and b/Client2014/Microsoft.VC90.OPENMP/vcomp90.dll differ diff --git a/Client2014/NPRobloxProxy.dll b/Client2014/NPRobloxProxy.dll new file mode 100644 index 0000000..811eb6c Binary files /dev/null and b/Client2014/NPRobloxProxy.dll differ diff --git a/Client2014/PlatformContent/pc/textures/aluminum/diffuse.dds b/Client2014/PlatformContent/pc/textures/aluminum/diffuse.dds new file mode 100644 index 0000000..fee64f0 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/aluminum/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/aluminum/normal.dds b/Client2014/PlatformContent/pc/textures/aluminum/normal.dds new file mode 100644 index 0000000..30614cd Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/aluminum/normal.dds differ diff --git a/Client2014/PlatformContent/pc/textures/aluminum/normaldetail.dds b/Client2014/PlatformContent/pc/textures/aluminum/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/aluminum/normaldetail.dds differ diff --git a/Client2014/PlatformContent/pc/textures/aluminum/specular.dds b/Client2014/PlatformContent/pc/textures/aluminum/specular.dds new file mode 100644 index 0000000..0473951 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/aluminum/specular.dds differ diff --git a/Client2014/PlatformContent/pc/textures/brick/diffuse.dds b/Client2014/PlatformContent/pc/textures/brick/diffuse.dds new file mode 100644 index 0000000..7115e54 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/brick/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/brick/normal.dds b/Client2014/PlatformContent/pc/textures/brick/normal.dds new file mode 100644 index 0000000..f42c2e6 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/brick/normal.dds differ diff --git a/Client2014/PlatformContent/pc/textures/brick/normaldetail.dds b/Client2014/PlatformContent/pc/textures/brick/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/brick/normaldetail.dds differ diff --git a/Client2014/PlatformContent/pc/textures/brick/specular.dds b/Client2014/PlatformContent/pc/textures/brick/specular.dds new file mode 100644 index 0000000..b346190 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/brick/specular.dds differ diff --git a/Client2014/PlatformContent/pc/textures/concrete/diffuse.dds b/Client2014/PlatformContent/pc/textures/concrete/diffuse.dds new file mode 100644 index 0000000..2dd4e21 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/concrete/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/concrete/normal.dds b/Client2014/PlatformContent/pc/textures/concrete/normal.dds new file mode 100644 index 0000000..72149f9 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/concrete/normal.dds differ diff --git a/Client2014/PlatformContent/pc/textures/concrete/normaldetail.dds b/Client2014/PlatformContent/pc/textures/concrete/normaldetail.dds new file mode 100644 index 0000000..03b0daa Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/concrete/normaldetail.dds differ diff --git a/Client2014/PlatformContent/pc/textures/concrete/specular.dds b/Client2014/PlatformContent/pc/textures/concrete/specular.dds new file mode 100644 index 0000000..6b22d02 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/concrete/specular.dds differ diff --git a/Client2014/PlatformContent/pc/textures/diamondplate/diffuse.dds b/Client2014/PlatformContent/pc/textures/diamondplate/diffuse.dds new file mode 100644 index 0000000..2184b3f Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/diamondplate/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/diamondplate/normal.dds b/Client2014/PlatformContent/pc/textures/diamondplate/normal.dds new file mode 100644 index 0000000..234aa72 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/diamondplate/normal.dds differ diff --git a/Client2014/PlatformContent/pc/textures/diamondplate/normaldetail.dds b/Client2014/PlatformContent/pc/textures/diamondplate/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/diamondplate/normaldetail.dds differ diff --git a/Client2014/PlatformContent/pc/textures/diamondplate/specular.dds b/Client2014/PlatformContent/pc/textures/diamondplate/specular.dds new file mode 100644 index 0000000..8646e2d Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/diamondplate/specular.dds differ diff --git a/Client2014/PlatformContent/pc/textures/fabric/diffuse.dds b/Client2014/PlatformContent/pc/textures/fabric/diffuse.dds new file mode 100644 index 0000000..101e832 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/fabric/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/fabric/normal.dds b/Client2014/PlatformContent/pc/textures/fabric/normal.dds new file mode 100644 index 0000000..ffe776c Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/fabric/normal.dds differ diff --git a/Client2014/PlatformContent/pc/textures/fabric/normaldetail.dds b/Client2014/PlatformContent/pc/textures/fabric/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/fabric/normaldetail.dds differ diff --git a/Client2014/PlatformContent/pc/textures/fabric/specular.dds b/Client2014/PlatformContent/pc/textures/fabric/specular.dds new file mode 100644 index 0000000..20eb572 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/fabric/specular.dds differ diff --git a/Client2014/PlatformContent/pc/textures/granite/diffuse.dds b/Client2014/PlatformContent/pc/textures/granite/diffuse.dds new file mode 100644 index 0000000..5ed1b7a Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/granite/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/granite/normal.dds b/Client2014/PlatformContent/pc/textures/granite/normal.dds new file mode 100644 index 0000000..d3b818f Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/granite/normal.dds differ diff --git a/Client2014/PlatformContent/pc/textures/granite/normaldetail.dds b/Client2014/PlatformContent/pc/textures/granite/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/granite/normaldetail.dds differ diff --git a/Client2014/PlatformContent/pc/textures/granite/specular.dds b/Client2014/PlatformContent/pc/textures/granite/specular.dds new file mode 100644 index 0000000..cef4a01 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/granite/specular.dds differ diff --git a/Client2014/PlatformContent/pc/textures/grass/diffuse.dds b/Client2014/PlatformContent/pc/textures/grass/diffuse.dds new file mode 100644 index 0000000..2907e38 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/grass/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/grass/normal.dds b/Client2014/PlatformContent/pc/textures/grass/normal.dds new file mode 100644 index 0000000..81720c5 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/grass/normal.dds differ diff --git a/Client2014/PlatformContent/pc/textures/grass/normaldetail.dds b/Client2014/PlatformContent/pc/textures/grass/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/grass/normaldetail.dds differ diff --git a/Client2014/PlatformContent/pc/textures/grass/specular.dds b/Client2014/PlatformContent/pc/textures/grass/specular.dds new file mode 100644 index 0000000..183463a Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/grass/specular.dds differ diff --git a/Client2014/PlatformContent/pc/textures/ice/diffuse.dds b/Client2014/PlatformContent/pc/textures/ice/diffuse.dds new file mode 100644 index 0000000..fee64f0 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/ice/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/ice/normal.dds b/Client2014/PlatformContent/pc/textures/ice/normal.dds new file mode 100644 index 0000000..87c01dd Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/ice/normal.dds differ diff --git a/Client2014/PlatformContent/pc/textures/ice/normaldetail.dds b/Client2014/PlatformContent/pc/textures/ice/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/ice/normaldetail.dds differ diff --git a/Client2014/PlatformContent/pc/textures/ice/specular.dds b/Client2014/PlatformContent/pc/textures/ice/specular.dds new file mode 100644 index 0000000..6d5c917 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/ice/specular.dds differ diff --git a/Client2014/PlatformContent/pc/textures/marble/diffuse.dds b/Client2014/PlatformContent/pc/textures/marble/diffuse.dds new file mode 100644 index 0000000..894dcc2 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/marble/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/marble/normal.dds b/Client2014/PlatformContent/pc/textures/marble/normal.dds new file mode 100644 index 0000000..cba80e1 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/marble/normal.dds differ diff --git a/Client2014/PlatformContent/pc/textures/marble/normaldetail.dds b/Client2014/PlatformContent/pc/textures/marble/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/marble/normaldetail.dds differ diff --git a/Client2014/PlatformContent/pc/textures/marble/specular.dds b/Client2014/PlatformContent/pc/textures/marble/specular.dds new file mode 100644 index 0000000..085525a Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/marble/specular.dds differ diff --git a/Client2014/PlatformContent/pc/textures/pebble/diffuse.dds b/Client2014/PlatformContent/pc/textures/pebble/diffuse.dds new file mode 100644 index 0000000..c5b849f Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/pebble/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/pebble/normal.dds b/Client2014/PlatformContent/pc/textures/pebble/normal.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/pebble/normal.dds differ diff --git a/Client2014/PlatformContent/pc/textures/pebble/normaldetail.dds b/Client2014/PlatformContent/pc/textures/pebble/normaldetail.dds new file mode 100644 index 0000000..03b0daa Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/pebble/normaldetail.dds differ diff --git a/Client2014/PlatformContent/pc/textures/pebble/specular.dds b/Client2014/PlatformContent/pc/textures/pebble/specular.dds new file mode 100644 index 0000000..6b22d02 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/pebble/specular.dds differ diff --git a/Client2014/PlatformContent/pc/textures/plastic/diffuse.dds b/Client2014/PlatformContent/pc/textures/plastic/diffuse.dds new file mode 100644 index 0000000..f9a20d9 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/plastic/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/plastic/normal.dds b/Client2014/PlatformContent/pc/textures/plastic/normal.dds new file mode 100644 index 0000000..ba5c24f Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/plastic/normal.dds differ diff --git a/Client2014/PlatformContent/pc/textures/plastic/normaldetail.dds b/Client2014/PlatformContent/pc/textures/plastic/normaldetail.dds new file mode 100644 index 0000000..79db22a Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/plastic/normaldetail.dds differ diff --git a/Client2014/PlatformContent/pc/textures/reflection.dds b/Client2014/PlatformContent/pc/textures/reflection.dds new file mode 100644 index 0000000..b5bff8e Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/reflection.dds differ diff --git a/Client2014/PlatformContent/pc/textures/rust/diffuse.dds b/Client2014/PlatformContent/pc/textures/rust/diffuse.dds new file mode 100644 index 0000000..5c387f0 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/rust/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/rust/normal.dds b/Client2014/PlatformContent/pc/textures/rust/normal.dds new file mode 100644 index 0000000..4a20383 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/rust/normal.dds differ diff --git a/Client2014/PlatformContent/pc/textures/rust/normaldetail.dds b/Client2014/PlatformContent/pc/textures/rust/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/rust/normaldetail.dds differ diff --git a/Client2014/PlatformContent/pc/textures/rust/specular.dds b/Client2014/PlatformContent/pc/textures/rust/specular.dds new file mode 100644 index 0000000..f148de9 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/rust/specular.dds differ diff --git a/Client2014/PlatformContent/pc/textures/sand/diffuse.dds b/Client2014/PlatformContent/pc/textures/sand/diffuse.dds new file mode 100644 index 0000000..20e8b6d Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/sand/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/sand/normal.dds b/Client2014/PlatformContent/pc/textures/sand/normal.dds new file mode 100644 index 0000000..fb5b3b4 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/sand/normal.dds differ diff --git a/Client2014/PlatformContent/pc/textures/sand/normaldetail.dds b/Client2014/PlatformContent/pc/textures/sand/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/sand/normaldetail.dds differ diff --git a/Client2014/PlatformContent/pc/textures/sand/specular.dds b/Client2014/PlatformContent/pc/textures/sand/specular.dds new file mode 100644 index 0000000..6efec1b Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/sand/specular.dds differ diff --git a/Client2014/PlatformContent/pc/textures/sky/sky512_bk.tex b/Client2014/PlatformContent/pc/textures/sky/sky512_bk.tex new file mode 100644 index 0000000..fcdd232 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/sky/sky512_bk.tex differ diff --git a/Client2014/PlatformContent/pc/textures/sky/sky512_dn.tex b/Client2014/PlatformContent/pc/textures/sky/sky512_dn.tex new file mode 100644 index 0000000..d63e52d Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/sky/sky512_dn.tex differ diff --git a/Client2014/PlatformContent/pc/textures/sky/sky512_ft.tex b/Client2014/PlatformContent/pc/textures/sky/sky512_ft.tex new file mode 100644 index 0000000..215b51b Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/sky/sky512_ft.tex differ diff --git a/Client2014/PlatformContent/pc/textures/sky/sky512_lf.tex b/Client2014/PlatformContent/pc/textures/sky/sky512_lf.tex new file mode 100644 index 0000000..a096acf Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/sky/sky512_lf.tex differ diff --git a/Client2014/PlatformContent/pc/textures/sky/sky512_rt.tex b/Client2014/PlatformContent/pc/textures/sky/sky512_rt.tex new file mode 100644 index 0000000..6c0aaf1 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/sky/sky512_rt.tex differ diff --git a/Client2014/PlatformContent/pc/textures/sky/sky512_up.tex b/Client2014/PlatformContent/pc/textures/sky/sky512_up.tex new file mode 100644 index 0000000..a973654 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/sky/sky512_up.tex differ diff --git a/Client2014/PlatformContent/pc/textures/slate/diffuse.dds b/Client2014/PlatformContent/pc/textures/slate/diffuse.dds new file mode 100644 index 0000000..3ccb456 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/slate/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/slate/normal.dds b/Client2014/PlatformContent/pc/textures/slate/normal.dds new file mode 100644 index 0000000..dc94035 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/slate/normal.dds differ diff --git a/Client2014/PlatformContent/pc/textures/slate/normaldetail.dds b/Client2014/PlatformContent/pc/textures/slate/normaldetail.dds new file mode 100644 index 0000000..e8faa22 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/slate/normaldetail.dds differ diff --git a/Client2014/PlatformContent/pc/textures/slate/specular.dds b/Client2014/PlatformContent/pc/textures/slate/specular.dds new file mode 100644 index 0000000..0a44f39 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/slate/specular.dds differ diff --git a/Client2014/PlatformContent/pc/textures/studs.dds b/Client2014/PlatformContent/pc/textures/studs.dds new file mode 100644 index 0000000..525b972 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/studs.dds differ diff --git a/Client2014/PlatformContent/pc/textures/terrain/diffuse.dds b/Client2014/PlatformContent/pc/textures/terrain/diffuse.dds new file mode 100644 index 0000000..ba03a1d Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/terrain/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/terrain/diffusefar.dds b/Client2014/PlatformContent/pc/textures/terrain/diffusefar.dds new file mode 100644 index 0000000..d8a7981 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/terrain/diffusefar.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0001.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0001.dds new file mode 100644 index 0000000..2dec827 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0001.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0002.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0002.dds new file mode 100644 index 0000000..82f8b8e Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0002.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0003.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0003.dds new file mode 100644 index 0000000..aa094de Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0003.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0004.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0004.dds new file mode 100644 index 0000000..0723462 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0004.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0005.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0005.dds new file mode 100644 index 0000000..661dc6e Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0005.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0006.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0006.dds new file mode 100644 index 0000000..d2ea2db Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0006.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0007.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0007.dds new file mode 100644 index 0000000..8087f89 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0007.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0008.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0008.dds new file mode 100644 index 0000000..4b38726 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0008.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0009.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0009.dds new file mode 100644 index 0000000..cfe56b2 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0009.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0010.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0010.dds new file mode 100644 index 0000000..97a1a36 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0010.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0011.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0011.dds new file mode 100644 index 0000000..148697a Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0011.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0012.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0012.dds new file mode 100644 index 0000000..c86b5cf Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0012.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0013.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0013.dds new file mode 100644 index 0000000..6feda1c Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0013.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0014.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0014.dds new file mode 100644 index 0000000..ddc1586 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0014.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0015.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0015.dds new file mode 100644 index 0000000..4c34f67 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0015.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0016.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0016.dds new file mode 100644 index 0000000..e87c2d6 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0016.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0017.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0017.dds new file mode 100644 index 0000000..c6ad2ec Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0017.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0018.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0018.dds new file mode 100644 index 0000000..0c60ff4 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0018.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0019.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0019.dds new file mode 100644 index 0000000..8a9848c Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0019.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0020.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0020.dds new file mode 100644 index 0000000..b5e8d79 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0020.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0021.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0021.dds new file mode 100644 index 0000000..750ad8a Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0021.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0022.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0022.dds new file mode 100644 index 0000000..7473007 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0022.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0023.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0023.dds new file mode 100644 index 0000000..6acff51 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0023.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0024.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0024.dds new file mode 100644 index 0000000..95d5fe5 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0024.dds differ diff --git a/Client2014/PlatformContent/pc/textures/water/a/RealWave_0025.dds b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0025.dds new file mode 100644 index 0000000..f74a8ec Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/water/a/RealWave_0025.dds differ diff --git a/Client2014/PlatformContent/pc/textures/wood/diffuse.dds b/Client2014/PlatformContent/pc/textures/wood/diffuse.dds new file mode 100644 index 0000000..db2c2fb Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/wood/diffuse.dds differ diff --git a/Client2014/PlatformContent/pc/textures/wood/normal.dds b/Client2014/PlatformContent/pc/textures/wood/normal.dds new file mode 100644 index 0000000..e92f07a Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/wood/normal.dds differ diff --git a/Client2014/PlatformContent/pc/textures/wood/normaldetail.dds b/Client2014/PlatformContent/pc/textures/wood/normaldetail.dds new file mode 100644 index 0000000..2d9f2bc Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/wood/normaldetail.dds differ diff --git a/Client2014/PlatformContent/pc/textures/wood/specular.dds b/Client2014/PlatformContent/pc/textures/wood/specular.dds new file mode 100644 index 0000000..9929b34 Binary files /dev/null and b/Client2014/PlatformContent/pc/textures/wood/specular.dds differ diff --git a/Client2014/ReflectionMetadata.xml b/Client2014/ReflectionMetadata.xml new file mode 100644 index 0000000..6410516 --- /dev/null +++ b/Client2014/ReflectionMetadata.xml @@ -0,0 +1,3752 @@ + + + + + + BindableFunction + Allow functions defined in one script to be called by another script + 4 + 66 + + + + + + Invoke + Causes the function assigned to OnInvoke to be called. Arguments passed to this function get passed to OnInvoke function. + + + + + + + OnInvoke + Should be defined as a function. This function is called when Invoke() is called. Number of arguments is variable. + + + + + + + BindableEvent + Allow events defined in one script to be subscribed to by another script + + 5 + 67 + + + + + Fire + Used to make the custom event fire (see Event for more info). Arguments can be variable length. + + + + + + + Event + This event fires when the Fire() method is used. Receives the variable length arguments from Fire(). + + + + + + + TouchTransmitter + Used by networking and replication code to transmit touch events - no other purpose + + false + 3 + 37 + + + + + ForceField + Prevents joint breakage from explosions, and stops Humanoids from taking damage + 3 + 37 + + + + + PluginManager + + + + + + TeleportService + Allows players to seamlessly leave a game and join another + + + + + StudioTool + + + + + + Plugin + + + + + + PluginMouse + + + + + + Glue + + + + + CollectionService + + + + + JointsService + + + + + BadgeService + + + + + LogService + + + + + AssetService + A service used to set and get information about assets stored on the Roblox website. + + + + + RevertAsset + Reverts a given place id to the version number provided. Returns true if successful on reverting, false otherwise. + + + + + SetPlacePermissions + Sets the permissions for a placeID to the place accessType. An optional table (inviteList) can be included that will set the accessType for only the player names provided. The table should be set up as an array of usernames (strings). + + + + + GetPlacePermissions + Given a placeID, this function will return a table with the permissions of the place. Useful for determining what kind of permissions a particular user may have for a place. + + + + + GetAssetVersions + Given a placeID, this function will return a table with the version info of the place. An optional arg of page number can be used to page through all revisions (a single page may hold about 50 revisions). + + + + + GetCreatorAssetID + Given a creationID, this function will return the asset that created the creationID. If no other asset created the given creationID, 0 is returned. + + + + + + + HttpService + 101 + + + + + + HttpEnabled + true + Enabling http requests from scripts + + + + + + + InsertService + A service used to insert objects stored on the website into the game. + + + + + GetCollection + Returns a table for the assets stored in the category. A category is an setId from www.roblox.com that links to a set. <a href="http://wiki.roblox.com/index.php/GetCollection_(Method)" target="_blank">More info on table format</a>. <a href="http://wiki.roblox.com/index.php/Sets" target="_blank">More info on sets</a> + + + + + Insert + Inserts the Instance into the workspace. It is recommended to use Instance.Parent = game.Workspace instead, as this can cause issues currently. + + + + + ApproveAssetId + true + Deprecated + + + + + ApproveAssetVersionId + true + Deprecated + + + + + + + + GetBaseSets + Returns a table containing a list of the various setIds that are ROBLOX approved. <a href="http://wiki.roblox.com/index.php/Sets" target="_blank">More info on sets</a> + + + + + GetUserSets + Returns a table containing a list of the various setIds that correspond to argument 'userId'. <a href="http://wiki.roblox.com/index.php/Sets" target="_blank">More info on sets</a> + + + + + GetBaseCategories + true + Deprecated. Use GetBaseSets() instead. + + + + + GetUserCategories + true + Deprecated. Use GetUserSets() instead. + + + + + LoadAsset + Returns a Model containing the Instance that resides at AssetId on the web. This call will also yield the script until the model is returned. Script execution can still continue, however, if you use a <a href="http://wiki.roblox.com/index.php/Function_Dump/Coroutine_Manipulationcoroutine" target="_blank">coroutine</a>. + + + + + LoadAssetVersion + Similar to LoadAsset, but instead an AssetVersionId is passed in, which refers to a particular version of the asset which is not neccessarily the latest version. + + + + + + + Hat + 3 + 45 + + + + + LocalBackpack + + + + + LocalBackpackItem + + + + + MotorFeature + + + + + Mouse + Used to receive input from the user. Actually tracks mouse events and keyboard events. + + + + + Hit + The CoordinateFrame of where the Mouse ray is currently hitting a 3D object in the Workspace. If the mouse is not over any 3D objects in the Workspace, this property is nil. + + + + + Icon + The current Texture of the Mouse Icon. Stored as a string, for more information on how to format the string <a href="http://wiki.roblox.com/index.php/Content" target="_blank">go here</a> + + + + + Origin + The CoordinateFrame of where the Mouse is when the mouse is not clicking. + + + + + Origin + The CoordinateFrame of where the Mouse is when the mouse is not clicking. This CoordinateFrame will be very close to the Camera.CoordinateFrame. + + + + + Target + The Part the mouse is currently over. If the mouse is not currently over any object (on the skybox, for example) this property is nil. + + + + + TargetFilter + A Part or Model that the Mouse will ignore when trying to find the Target, TargetSurface and Hit. + + + + + TargetSurface + The NormalId (Top, Left, Down, etc.) of the face of the part the Mouse is currently over. + + + + + UnitRay + The Unit Ray from where the mouse is (Origin) to the current Mouse.Target. + + + + + ViewSizeX + The viewport's (game window) width in pixels. + + + + + ViewSizeY + The viewport's (game window) height in pixels. + + + + + X + The absolute pixel position of the Mouse along the x-axis of the viewport (game window). Values start at 0 on the left hand side of the screen and increase to the right. + + + + + Y + The absolute pixel position of the Mouse along the y-axis of the viewport (game window). Values start at 0 on the stop of the screen and increase to the bottom. + + + + + + + Button1Down + Fired when the first button (usually the left, but could be another) on the mouse is depressed. + + + + + Button1Up + Fired when the first button (usually the left, but could be another) on the mouse is release. + + + + + Button2Down + This event is currently non-operational. + + + + + Button2Up + This event is currently non-operational. + + + + + Idle + Fired constantly when the mouse is not firing any other event (i.e. the mouse isn't moving, nor any buttons being pressed or depressed). + + + + + KeyDown + Fired when a user presses a key on the keyboard. Argument is a string representation of the key. If the key has no string representation (such as space), the string passed in is the keycode for that character. Keycodes are currently in ASCII. + + + + + KeyUp + Fired when a user releases a key on the keyboard. Argument is a string representation of the key. If the key has no string representation (such as space), the string passed in is the keycode for that character. Keycodes are currently in ASCII. + + + + + Move + Fired when the mouse X or Y member changes. + + + + + WheelBackward + This event is currently non-operational. + + + + + WheelForward + This event is currently non-operational. + + + + + + + ProfilingItem + + + + + ChangeHistoryService + + + + + RotateP + + + + + RotateV + + + + + ScriptContext + + + + + Selection + + + + + VelocityMotor + + + + + Weld + 20 + 34 + + + + + TaskScheduler + + + + + SetThreadShare + true + Deprecated + + + + + + + StatsItem + + + + + Snap + + + + + FileMesh + + + + + + ClickDetector + Raises mouse events for parent object + 3 + 41 + + + + + MaxActivationDistance + The maximum distance a Player's character can be from the ClickDetector's parent Part that will allow the Player's mouse to fire events on this object. + + + + + + + MouseClick + Fired when a player clicks on the parent Part of ClickDetector. The argument provided is always of type Player. + + + + + MouseHoverEnter + Fired when a player's mouse enters on the parent Part of ClickDetector. The argument provided is always of type Player. + + + + + MouseHoverLeave + Fired when a player's mouse leaves the parent Part of ClickDetector. The argument provided is always of type Player. + + + + + + + + Clothing + 0 + + + + + + Smoke + Makes the parent part or model object emit smoke + 3 + 59 + + + + + Sparkles + Makes the parent part or model object fantastic + 3 + 42 + + + + + Explosion + 3 + 36 + Creates an Explosion! This can be used as a purely graphical effect, or can be made to damage objects. + + + + + BlastPressure + How much force this Explosion exerts on objects within it's BlastRadius. Setting this to 0 creates a purely graphical effect. A larger number will cause Parts to fly away at higher velocities. + + + + + BlastRadius + How big the Explosion is. This is a circle starting from the center of the Explosion's Position, the larger this property the larger the circle of destruction. + + + + + Position + Where the Explosion occurs in absolute world coordinates. + + + + + ExplosionType + Defines the behavior of the Explosion. <a href="http://wiki.roblox.com/index.php/ExplosionType_(Enum)" target="_blank">More info</a> + + + + + + + Fire + Makes the parent part or model object emit fire + 3 + 61 + + + + + Color + The color of the base of the fire. See SecondaryColor for more. + + + + + Heat + How hot the fire appears to be. The flame moves quicker the higher this value is set. + + + + + SecondaryColor + The color the fire interpolates to from Color. The longer a particle exists in the fire, the close to this color it becomes. + + + + + Size + How large the fire appears to be. + + + + + + + Seat + 3 + 35 + + + + + Platform + + Equivalent to a seat, except that the character stands up rather than sits down. + 3 + 35 + + + + + SkateboardPlatform + + 3 + 35 + + + + + VehicleSeat + Automatically finds and powers hinge joints in an assembly. Ignores motors. + 3 + 35 + + + + + Tool + 3 + 17 + + + + + Flag + 3 + 38 + + + + + CanBeDropped + If someone is carrying this flag, this bool determines whether or not they can drop it and run. + + + + + TeamColor + The Team this flag is for. Corresponds with the TeamColors in the Teams service. + + + + + + + FlagStand + 3 + 39 + + + + + BackpackItem + 0 + + + + + Decal + 4 + 7 + Descibes a texture that is placed on one of the sides of the Part it is parented to. + + + + + Face + Describes the face of the Part the decal will be applied to. <a href="http://wiki.roblox.com/index.php/NormalId_(Enum)" target="_blank">More info</a> + + + + + Shiny + How much light will appear to reflect off of the decal. + + + + + Specular + How light will react to the surface of the decal. + + + + + Transparency + How visible the decal is. 1 is completely invisible, while 0 is completely opaque + + + + + + + JointInstance + 20 + 34 + + + + + Message + 11 + 33 + StarterGui + + + + + Hint + true + 11 + 33 + + + + + IntValue + 3 + 4 + Stores a int value in it's Value member. Useful to share int information across multiple scripts. + + + + + RayValue + 3 + 4 + Stores a Ray value in it's Value member. Useful to share Ray information across multiple scripts. + + + + + IntConstrainedValue + 3 + 4 + Stores an int value in it's Value member. Value is clamped to be in range of Min and MaxValue. Useful to share int information across multiple scripts. + + + + MaxValue + The maximum we allow this Value to be set. If Value is set higher than this, it automatically gets adjusted to MaxValue + + + + + MinValue + The minimum we allow this Value to be set. If Value is set lower than this, it automatically gets adjusted to MinValue + + + + + + DoubleConstrainedValue + 3 + 4 + Stores a double value in it's Value member. Value is clamped to be in range of Min and MaxValue. Useful to share double information across multiple scripts. + + + + + MaxValue + The maximum we allow this Value to be set. If Value is set higher than this, it automatically gets adjusted to MaxValue + + + + + MinValue + The minimum we allow this Value to be set. If Value is set lower than this, it automatically gets adjusted to MinValue + + + + + + + BoolValue + 3 + 4 + Stores a boolean value in it's Value member. Useful to share boolean information across multiple scripts. + + + + + CustomEvent + 3 + 4 + + + + + CustomEventReceiver + 3 + 4 + + + + + TextureTrail + 3 + 4 + + + + + FloorWire + 3 + 4 + Renders a thin cylinder than can be adorned with textures that 'flow' from one object to the next. Has basic pathing abilities and attempts to to not intersect anything. <a href="http://wiki.roblox.com/index.php/FloorWire_Guide" target="_blank">More info</a> + + + + + CycleOffset + Controls how the decals are positioned along the wire. <a href="http://wiki.roblox.com/index.php/CycleOffset_(Property)" target="_blank">More info</a> + + + + + From + The object the FloorWire 'emits' from + + + + + StudsBetweenTextures + The space between two textures on the wire. Note: studs are relative depending on how far the camera is from the FloorWire. + + + + + Texture + The image we use to render the textures that flow from beginning to end of the FloorWire. + + + + + TextureSize + The size in studs of the Texture we use to flow from one object to the next. + + + + + To + The object the FloorWire 'emits' to + + + + + Velocity + The rate of travel that the textures flow along the wire. + + + + + WireRadius + How thick the wire is. + + + + + + + NumberValue + 3 + 4 + + + + + StringValue + 3 + 4 + + + + + Vector3Value + 3 + 4 + + + + + CFrameValue + 3 + 4 + Stores a CFrame value in it's Value member. Useful to share CFrame information across multiple scripts. + + + + + Color3Value + 3 + 4 + Stores a Color3 value in it's Value member. Useful to share Color3 information across multiple scripts. + + + + + BrickColorValue + 3 + 4 + Stores a BrickColor value in it's Value member. Useful to share BrickColor information across multiple scripts. + + + + + ObjectValue + 3 + 4 + + + + + SpecialMesh + 3 + 8 + + + + + BlockMesh + 3 + 8 + + + + + + CylinderMesh + 3 + 8 + + + + + + BevelMesh + false + true + + + + + DataModelMesh + false + + + + + + Texture + 4 + 10 + + + + + Sound + 1 + 11 + + + + + play + true + Deprecated. Use Play() instead + + + + + + + PlayOnRemove + The sound will play when it is removed from the Workspace. Looped sounds don't play + + + + + + + StockSound + false + -1 + + + + + SoundService + 50 + 31 + + + + + + AmbientReverb + + The ambient sound environment. May not work when using hardware sound + + + + + DopplerScale + + The doppler scale is a general scaling factor for how much the pitch varies due to doppler shifting in 3D sound. Doppler is the pitch bending effect when a sound comes towards the listener or moves away from it, much like the effect you hear when a train goes past you with its horn sounding. With dopplerscale you can exaggerate or diminish the effect. + + + + + DistanceFactor + + the relative distance factor, compared to 1.0 meters. + + + + + RolloffScale + + Setting this value makes the sound drop off faster or slower. The higher the value, the faster volume will attenuate, and conversely the lower the value, the slower it will attenuate. For example a rolloff factor of 1 will simulate the real world, where as a value of 2 will make sounds attenuate 2 times quicker. + + + + + + + Backpack + 3 + 20 + + + + + StarterPack + 3 + 20 + + + + + + StarterGear + 3 + 20 + + + + + + StarterGui + 3 + 46 + + + + + + SetCoreGuiEnabled + Will stop/begin certain core gui elements being rendered. See CoreGuiType for core guis that can be modified. + + + + + GetCoreGuiEnabled + Returns a boolean describing whether a CoreGuiType is currently being rendered. + + + + + + + + CoreGui + false + 46 + + + + + + ContextActionService + A service used to bind input to various lua functions. + + + + + + BindActionToInputTypes + Binds 'functionToBind' to fire when any 'inputTypes' happen. InputTypes can be variable in number and type. Types can be Enum.KeyCode, single strings corresponding to keys, or Enum.UserInputType. 'actionName' is a key used by many other ContextActionService functions to query state. 'createTouchButton' if true will create a button on screen on touch devices. This button will fire 'functionToBind'. + + + + + SetTitle + If 'actionName' key contains a bound action, then 'title' is set as the title of the touch button. Does nothing if a touch button was not created. No guarantees are made whether title will be set when button is manipulated. + + + + + SetDescription + If 'actionName' key contains a bound action, then 'description' is set as the description of the bound action. This description will appear for users in a listing of current actions availables. + + + + + SetImage + If 'actionName' key contains a bound action, then 'image' is set as the image of the touch button. Does nothing if a touch button was not created. No guarantees are made whether image will be set when button is manipulated. + + + + + SetPosition + If 'actionName' key contains a bound action, then 'position' is set as the position of the touch button. Does nothing if a touch button was not created. No guarantees are made whether position will be set when button is manipulated. + + + + + UnbindAction + If 'actionName' key contains a bound action, removes function from being called by all input that it was bound by (if function was also bound by a different action name as well, those bound input are still active). Will also remove any touch button created (if button was manipulated manually there is no guarantee it will be cleaned up). + + + + + UnbindAllActions + Removes all functions bound. No actionNames will remain. All touch buttons will be removed. If button was manipulated manually there is no guarantee it will be cleaned up. + + + + + GetBoundActionInfo + Returns a table with info regarding the function bound with 'actionName'. Table has the keys 'title' (current title that was set with SetTitle) 'image' (image set with SetImage) 'description' (description set with SetDescription) 'inputTypes' (tuple containing all input bound for this 'actionName') 'createTouchButton' (whether or not we created a touch button for this 'actionName'). + + + + + GetAllBoundActionInfo + Returns a table with all bound action info. Each entry is a key with 'actionName' and value being the same table you would get from ContextActionService:GetBoundActionInfo('actionName'). + + + + + + + + GetButton + If 'actionName' key contains a bound action, then this will return the touch button (if was created). Returns nil if a touch button was not created. No guarantees are made whether button will be retrievable when button is manipulated. + + + + + + + + PointsService + A service used to query and award points for Roblox users using the universal point system. + + + + + + PointsAwarded + Fired when points are successfully awarded 'userId'. Also returns the updated balance of points for usedId in universe via 'userBalanceInUniverse', total points via 'userTotalBalance', and the amount points that were awarded via 'pointsAwarded'. This event fires on the server and also all clients in the game that awarded the points. + + + + + + + + AwardPoints + Will attempt to award the 'amount' points to 'userId', returns 'userId' awarded to, the number of points awarded, the new point total the user has in the game, and the total number of points the user now has. Will also fire PointsService.PointsAwarded. Works with server scripts ONLY. + + + + + GetPointBalance + Returns the overall balance of points that player with userId has (the sum of all points across all games). Works with server scripts ONLY. + + + + + GetGamePointBalance + Returns the balance of points that player with userId has in the current game (all placeID points combined within the game). Works with server scripts ONLY. + + + + + GetAwardablePoints + Returns the number of points the current universe can award to players. Works with server scripts ONLY. + + + + + + + + MarketplaceService + 46 + + + + + PromptPurchase + Will prompt 'player' to purchase the item associated with 'assetId'. 'equipIfPurchased' is an optional argument that will give the item to the player immediately if they buy it (only applies to gear). 'currencyType' is also optional and will attempt to prompt the user with a specified currency if the product can be purchased with this currency, otherwise we use the default currency of the product. + + + + + + + + GetProductInfo + Takes one argument "assetId" which should be a number of an asset on www.roblox.com. Returns a table containing the product information (if this process fails, returns an empty table). + + + + + PlayerOwnsAsset + Checks to see if 'Player' owns the product associated with 'assetId'. Returns true if the player owns it, false otherwise. This call will produce a warning if called on a guest player. + + + + + + + ProcessReceipt + Callback that is executed for pending Developer Product receipts. + + If this function does not return Enum.ProductPurchaseDecision.PurchaseGranted, then you will not be granted the money for the purchase! + + The callback will be invoked with a table, containing the following informational fields: + PlayerId - the id of the player making the purchase. + PlaceIdWherePurchased - the specific place where the purchase was made. + PurchaseId - a unique identifier for the purchase, should be used to prevent granting an item multiple times for one purchase. + ProductId - the id of the purchased product. + CurrencyType - the type of currency used (Tix, Robux). + CurrencySpent - the amount of currency spent on the product for this purchase. + + + + + + + + PromptPurchaseFinished + Fired when a 'player' dismisses a purchase dialog for 'assetId'. If the player purchased the item 'isPurchased' will be true, otherwise it will be false. This call will produce a warning if called on a guest player. + + + + + + + + UserInputService + + + + + TouchEnabled + Returns true if the local device accepts touch input, false otherwise. + + + + + KeyboardEnabled + Returns true if the local device accepts keyboard input, false otherwise. + + + + + MouseEnabled + Returns true if the local device accepts mouse input, false otherwise. + + + + + + + + TouchTap + Fired when a user taps their finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the tap gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchPinch + Fired when a user pinches their fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the pinch gesture. 'scale' is a float that indicates the difference from the beginning of the pinch gesture. 'velocity' is a float indicating how quickly the pinch gesture is happening. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchSwipe + Fired when a user swipes their fingers on a TouchEnabled device. 'swipeDirection' is an Enum.SwipeDirection, indicating the direction the user swiped. 'numberOfTouches' is an int that indicates how many touches were involved with the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchLongPress + Fired when a user holds at least one finger for a short amount of time on the same screen position on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchRotate + Fired when a user rotates two fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'rotation' is a float indicating how much the rotation has gone from the start of the gesture. 'velocity' is a float that indicates how quickly the gesture is being performed. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchPan + Fired when a user drags at least one finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'totalTranslation' is a Vector2, indicating how far the pan gesture has gone from its starting point. 'velocity' is a Vector2 that indicates how quickly the gesture is being performed in each dimension. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + + TouchStarted + Fired when a user places their finger on a TouchEnabled device. 'touch' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchMoved + Fired when a user moves their finger on a TouchEnabled device. 'touch' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchEnded + Fired when a user moves their finger on a TouchEnabled device. 'touch' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + + InputBegan + Fired when a user begins interacting via a Human-Computer Interface device (Mouse button down, touch begin, keyboard button down, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + InputChanged + Fired when a user changes interacting via a Human-Computer Interface device (Mouse move, touch move, mouse wheel, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + InputEnded + Fired when a user stops interacting via a Human-Computer Interface device (Mouse button up, touch end, keyboard button up, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + + + + + Sky + 0 + 28 + + + + + Motor + + 0 + + + + + Humanoid + 3 + 9 + + + + + + MoveTo + Attempts to move the Humanoid and it's associated character to 'part'. 'location' is used as an offset from part's origin. + + + + + Jump + + + + + Sit + + + + + TakeDamage + Decreases health by the amount. Use this instead of changing health directly to make sure weapons are filtered for things such as ForceField(s). + + + + + UnequipTools + + Takes any active gear/tools that the Humanoid is using and puts them into the backpack. This function only works on Humanoids with a corresponding Player. + + + + + EquipTool + + Takes a specified tool and equips it to the Humanoid's Character. Tool argument should be of type 'Tool'. + + + + + + + NameOcclusion + + Sets how to display other humanoid names to this humanoid's player. <a href="http://wiki.roblox.com/index.php/NameOcclusion_(Enum)" target="_blank">More info</a> + + + Health + How many hit points the Humanoid has. When this number reaches 0 or goes below 0, the Humanoid's character falls apart and will respawn. + + + MaxHealth + The maximum number of hit points a Humanoid's health can reach. If the Humanoid's health is set over this amount, the health gets set to this value. + + + TargetPoint + The location that the Humanoid is trying to walk to. + + + + + + + BodyColors + + 0 + + + + + Shirt + 0 + 43 + + + + + Pants + 0 + 44 + + + + + ShirtGraphic + 0 + 40 + + + + + Skin + + 0 + + + + + DebugSettings + false + 0 + + + + + FaceInstance + false + + + + + GameSettings + false + 0 + + + + + GlobalSettings + false + 0 + + + + + Item + false + 0 + + + + + NetworkPeer + false + + + + + NetworkSettings + false + 0 + + + + + PVInstance + false + + + + + CoordinateFrame + true + Deprecated. Use CFrame instead + + + + + + + RenderSettings + false + 0 + + + + + RootInstance + false + + + + + ServiceProvider + false + + + + + service + true + Use GetService() instead + + + + + + + ProfilingItem + false + + + + + NetworkMarker + false + + + + + + Hopper + true + Use StarterPack instead + 0 + + + + + + Instance + false + + + + + + Archivable + Determines whether or not an Instance can be saved when the game closes/attempts to save the game. Note: this only applies to games that use Data Persistence, or Personal Build Servers. + + + + + ClassName + The string name of this Instance's most derived class. + + + + + Parent + The Instance that is directly above this Instance in the tree. + + + + + + + + + GetDebugId + false + This function is for internal testing. Don't use in production code + + + + + Clone + Returns a copy of this Object and all its children. The copy's Parent is nil + + + + + clone + true + Use Clone() instead + + + + + isA + true + Use IsA() instead + + + + + IsA + Returns a boolean if this Instance is of type 'className' or a is a subclass of type 'className'. If 'className' is not a valid class type in ROBLOX, this function will always return false. <a href="http://wiki.roblox.com/index.php/IsA" target="_blank">More info</a> + + + + + FindFirstChild + Returns the first child of this Instance that matches the first argument 'name'. The second argument 'recursive' is an optional boolean (defaults to false) that will force the call to traverse down thru all of this Instance's descendants until it finds an object with a name that matches the 'name' argument. The function will return nil if no Instance is found. + + + + + GetFullName + Returns a string that shows the path from the root node (DataModel) to this Instance. This string does not include the root node (DataModel). + + + + + children + true + Use GetChildren() instead + + + + + getChildren + true + Use GetChildren() instead + + + + + GetChildren + Returns a read-only table of this Object's children + + + + + Remove + Deprecated. Use ClearAllChildren() to get rid of all child objects, or Destroy() to invalidate this object and its descendants + true + + + + + remove + true + Use Remove() instead + + + + + ClearAllChildren + Removes all children (but not this object) from the workspace. + + + + + Destroy + Removes object and all of its children from the workspace. Disconnects object and all children from open connections. Object and children may not be usable after calling Destroy. + + + + + findFirstChild + true + Use FindFirstChild() instead + + + + + + + + AncestryChanged + Fired when any of this object's ancestors change. First argument 'child' is the object whose parent changed. Second argument 'parent' is the first argument's new parent. + + + + + DescendantAdded + Fired after an Instance is parented to this object, or any of this object's descendants. The 'descendant' argument is the Instance that is being added. + + + + + DescendantRemoving + Fired after an Instance is unparented from this object, or any of this object's descendants. The 'descendant' argument is the Instance that is being added. + + + + + Changed + Fired after a property changes value. The property argument is the name of the property + + + + + + + + BodyGyro + Attempts to maintain a fixed orientation of its parent Part + 14 + 14 + + + + + + maxTorque + The maximum torque that will be exerted on the Part + + + + + D + The dampening factor applied to this force + + + + + P + The power continually applied to this force + + + + + cframe + the cframe that this force is trying to orient its parent Part to. Note: this force only uses the rotation of the cframe, not the position. + + + + + + + BodyPosition + 14 + 14 + + + + + + maxForce + The maximum force that will be exerted on the Part + + + + + D + The dampening factor applied to this force + + + + + P + The power factor continually applied to this force + + + + + position + The Vector3 that this force is trying to position its parent Part to. + + + + + + + RocketPropulsion + + 14 + 14 + A propulsion system that mimics a rocket + + + + + BodyVelocity + 14 + 14 + + + + + maxForce + The maximum force that will be exerted on the Part in each axis + + + + + P + The amount of power we add to the system. The higher the power, the quicker the force will achieve its goal. + + + + + velocity + The velocity this system tries to achieve. How quickly the system reaches this velocity (if ever) is defined by P. + + + + + + + BodyAngularVelocity + 14 + 14 + + + + maxTorque + The maximum torque that will be exerted on the Part in each axis + + + P + The amount of power we add to the system. The higher the power, the quicker the force will achieve its goal. + + + angularVelocity + The rotational velocity this system tries to achieve. How quickly the system reaches this velocity is defined by P. + + + + + + BodyForce + 14 + 14 + When parented to a physical part, BodyForce will continually exert a force upon its parent object. + + + + force + The continual force exerted on an object, defined in each axis. + + + + + + BodyThrust + 14 + 14 + + + + + + force + The power continually applied to this force + + + + + location + The Vector3 location of where to apply the force to. + + + + + + + Hole + + 0 + + + + + Feature + 0 + + + + + + Teams + This Service-level object is the container for all Team objects in a level. A map that supports team games must have a Teams service. <a href="http://wiki.roblox.com/index.php/Team" target="_blank">More info</a> + 14 + 23 + + + + + + Team + The Team class is used to represent a faction in a team game. The only valid location for a Team object is under the Teams service. <a href="http://wiki.roblox.com/index.php/Team" target="_blank">More info</a> + 1 + 24 + + + + + SpawnLocation + 3 + 25 + + + + + NetworkClient + 3 + 16 + + + + + NetworkServer + 3 + 15 + + + + + + Script + 3 + 6 + + + + + + LinkedScript + + This property is under development. Do not use + + + + + + + + LocalScript + 4 + 18 + A script that runs on clients, NOT servers. LocalScripts can only run when parented under the PlayerGui currently. + + + + + + NetworkReplicator + 3 + 29 + + + + + + Model + 10 + 2 + A construct used to group Parts and other objects together, also allows manipulation of multiple objects. + + + + + BreakJoints + Breaks all surface joints contained within + + + + + GetModelCFrame + Returns a CFrame that has position of the centroid of all Parts in the Model. The rotation matrix is either the rotation matrix of the user-defined PrimaryPart, or if not specified then a part in the Model chosen by the engine. + + + + + GetModelSize + Returns a Vector3 that is union of the extents of all Parts in the model. + + + + + MakeJoints + Creates the appropriate SurfaceJoints between all touching Parts contrained within the model. Technically, this function calls MakeJoints() on all Parts inside the model. + + + + + MoveTo + Moves the centroid of the Model to the specified location, respecting all relative distances between parts in the model. + + + + + ResetOrientationToIdentity + Rotates all parts in the model to the orientation that was set using SetIdentityOrientation(). If this function has never been called, rotation is reset to GetModelCFrame()'s rotation. + + + + + SetIdentityOrientation + Takes the current rotation matrix of the model and stores it as the model's identity matrix. The rotation is applied when ResetOrientationToIdentity() is called. + + + + + TranslateBy + Similar to MoveTo(), except instead of moving to an explicit location, we use the model's current CFrame location and offset it. + + + + + GetPrimaryPartCFrame + Returns the cframe of the Model.PrimaryPart. If PrimaryPart is nil, then this function will throw an error. + + + + + SetPrimaryPartCFrame + Sets the cframe of the Model.PrimaryPart. If PrimaryPart is nil, then this function will throw an error. This also sets the cframe of all descendant Parts relative to the cframe change to PrimaryPart. + + + + + makeJoints + Use MakeJoints() instead + true + + + + + move + true + Use MoveTo() instead + + + + + + + PrimaryPart + A Part that serves as a reference for the Model's CFrame. Used in conjunction with GetModelPrimaryPartCFrame and SetModelPrimaryPartCFrame. Use this to rotate/translate all Parts relative to the PrimaryPart. + + + + + + + + Status + 10 + 2 + + + + + move + true + Use MoveTo() instead + + + + + + + + DataModel + + + + + + + + Workspace + + + + + workspace + true + Deprecated. Use Workspace + + + + + ShowMouse + true + Deprecated. Use Workspace.IsMouseCursorVisible + + + + + IsLoaded + Returns true if the game has finished loading, false otherwise. Check this before listening to the Loaded signal to ensure a script knows when a game finishes loading. + + + + + + + + Loaded + Fires when the game finishes loading. Use this to know when to remove your custom loading gui. It is best to check IsLoaded() before connecting to this event, as the game may load before the event is connected to. + + + + + + + + get + true + Use GetObjects() instead + + + + + SetPlaceID + true + Use SetPlaceId() instead + + + + + SetCreatorID + true + Use SetCreatorId() instead + + + + + + + + HopperBin + 24 + 22 + + + + + + Camera + 5 + 5 + + + + + CameraSubject + Where the Camera's focus is. Any rotation of the camera will be about this subject. + + + + + CameraType + Defines how the camera will behave. <a href="http://wiki.roblox.com/index.php/CameraType_(Enum)" target="_blank">More info</a> + + + + + CoordinateFrame + The current position and rotation of the Camera. For most CameraTypes, the rotation is set such that the CoordinateFrame lookVector is pointing at the Focus. + + + + + FieldOfView + The current angle, or width, of what the camera can see. Current acceptable values are from 20 degrees to 80. + + + + + Focus + The current CoordinateFrame that the camera is looking at. Note: it is not always guaranteed that the camera is always looking here. + + + + + + + GetRoll + Returns the camera's current roll. Roll is defined in radians, and is stored as the delta from the camera's y axis default normal vector. + + + + + SetRoll + Sets the camera's current roll. Roll is defined in radians, and is stored as the delta from the camera's y axis default normal vector. + + + + + + + + Players + 2 + 21 + + + + + CharacterAutoLoads + true + Set to true, when a player joins a game, they get a character automatically, as well as when they die. When set to false, characters do not auto load and will only load in using Player:LoadCharacter(). + + + + + + + players + true + Use GetPlayers() instead + + + + + + + + ReplicatedStorage + 3 + 70 + A container whose contents are replicated to all clients and the server. + + + + + + ReplicatedFirst + 3 + 70 + A container whose contents are replicated to all clients (but not back to the server) first before anything else. Useful for creating loading guis, tutorials, etc. + + + + + RemoveRobloxLoadingScreen + Removes the default Roblox loading screen from view. Call this when you are ready to either show your own loading gui, or when the game is ready to play. + + + + + + + + ServerStorage + 3 + 69 + A container whose contents are only on the server. + + + + + + ServerScriptService + 3 + 71 + A container whose contents should be scripts. Scripts that are added to the container are run on the server. + + + + + + Lighting + 3 + 13 + Responsible for all lighting aspects of the world (affects how things are rendered). + + + + + GetMinutesAfterMidnight + The number of minutes that the current time is past midnight. If currently at midnight, returns 0. Will return decimal values if not at an exact minute. + + + + + GetMoonDirection + Returns the lookVector (Vector3) of the moon. If this lookVector was used in a CFrame, the Part would face the moon. + + + + + GetMoonPhase + Currently always returns 0.75. MoonPhase cannot be edited. + + + + + GetSunDirection + Returns the lookVector (Vector3) of the sun. If this lookVector was used in a CFrame, the Part would face the moon. + + + + + SetMinutesAfterMidnight + Sets the time to be a certain number of minutes after midnight. This works with integer and decimal values. + + + + + + + Ambient + The hue of the global lighting. Changing this changes the color tint of all objects in the Workspace. + + + + + Brightness + How much global light each Part in the Workspace receives. Standard range is 0 to 1 (0 being little light), but can be increased all the way to 5 (colors start to be appear very different at this value). + + + + + ColorShift_Bottom + The hue of global lighting on the bottom surfaces of an object. + + + + + ColorShift_Top + The hue of global lighting on the top surfaces of an object. + + + + + FogColor + A Color3 value that changes the hue of distance fog. + + + + + FogEnd + The distance at which fog completely blocks your vision. This distance is relative to the camera position. Units are in studs + + + + + FogStart + The distance at which the fog gradient begins. This distance is relative to the camera position. Units are in studs. + + + + + GeographicLatitude + The latitude position the level is placed at. This affects sun position. <a href="http://wiki.roblox.com/index.php/GeographicLatitude_(Property)" target="_blank">More info</a> + + + + + GlobalShadows + Flag enabling shadows from sun and moon in the place + + + + + OutdoorAmbient + Effective ambient value for outdoors, effectively shadow color outdoors (requires GlobalShadows enabled) + + + + + Outlines + Flag enabling or disabling outlines on parts and terrain + + + + + ShadowColor + Color the shadows appear as. Shadows are drawn mostly for characters, but depending on the lighting will also show for Parts in the Workspace. Rendering settings can also affect if shadows are drawn. + + + + + TimeOfDay + A string that represent the current time of day. Time is in 24-hour clock format "XX::YY:ZZ", where X is hour, Y is minute, and Z is seconds. + + + + + + + LightingChanged + Fired whenever a property of Lighting is changed, or a skybox is added or removed. Skyboxes are of type 'Sky' and should be parented directly to lighting. + + + + + + + + TestService + 100 + 68 + + + + + + DebuggerManager + + + + + + + + ScriptDebugger + + + + + + + + DebuggerBreakpoint + + + + + + + + DebuggerWatch + + + + + + + + Debris + 10 + 30 + A service that provides utility in cleaning up objects + + + + + addItem + true + Use AddItem() instead + + + + + AddItem + Adds an Instance into the debris service that will later be destroyed. Second argument 'lifetime' is optional and specifies how long (in seconds) to wait before destroying the item. If no time is specified then the item added will automatically be destroyed in 10 seconds. + + + + + + + MaxItems + true + Deprecated. No replacement + + + + + + + + Accoutrement + 0 + 32 + + + + + + Player + 1 + 12 + + + + + + CharacterAppearance + false + + + + + CameraMode + An enum that describes how a Player's camera is allowed to behave. <a href="http://wiki.roblox.com/index.php/CameraMode_(Enum)" target="_blank">More info</a>. + + + + + DataReady + Read-only. If true, this Player's persistent data can be loaded, false otherwise. <a href="http://wiki.roblox.com/index.php/ROBLOX_Scripting_How_To:_Data_Persistence" target="_blank">Info on Data Persistence</a>. + + + + + + + + LoadCharacter + true + Loads in a new character for this player. This will replace the player's current character, if they have one. This should be used in conjunction with Players.CharacterAutoLoads to control spawning of characters. This function only works from a server-side script (NOT a LocalScript). + + + + + playerFromCharacter + true + Use GetPlayerFromCharacter() instead + + + + + SetUnder13 + true + + + + + + + + WaitForDataReady + true + Yields until the persistent data for this Player is ready to be loaded. <a href="http://wiki.roblox.com/index.php/ROBLOX_Scripting_How_To:_Data_Persistence" target="_blank">Info on Data Persistence</a>. + + + + + GetWebPersonalServerRank + true + + + + + + + + + Idled + Fired periodically after the user has been AFK for a while. Currently this event is only fired for the *local* Player. "time" is the time in seconds that the user has been idle. + + + + + + + + Workspace + 1 + 19 + + + + + FindPartsInRegion3 + Returns parts in the area defined by the Region3, up to specified maxCount or 100, whichever is less + + + + + FindPartsInRegion3WithIgnoreList + Returns parts in the area defined by the Region3, up to specified maxCount or 100, whichever is less + + + + + FindPartOnRay + Return type is (BasePart, Vector3) if the ray hits. If it misses it will return (nil, PointAtEndOfRay) + + + + + FindPartOnRayWithIgnoreList + Return type is (BasePart, Vector3) if the ray hits. If it misses it will return (nil, PointAtEndOfRay) + + + + + + + + BasePart + A structural class, not creatable + -1 + false + + + + + + Color + true + Deprecated. Use BrickColor instead + + + + + CFrame + Contains information regarding the Part's position and a matrix that defines the Part's rotation. Can read/write. <a href="http://wiki.roblox.com/index.php/Cframe" target="_blank">More info</a> + + + + + CanCollide + Determines whether physical interactions with other Parts are respected. If true, will collide and react with physics to other Parts. If false, other parts will pass thru instead of colliding + + + + + Anchored + Determines whether or not physics acts upon the Part. If true, part stays 'Anchored' in space, not moving regardless of any collision/forces acting upon it. If false, physics works normally on the part. + + + + + Elasticity + A float value ranging from 0.0f to 1.0f. Sets how much the Part will rebound against another. a value of 1 is like a superball, and 0 is like a lead block. + + + + + Friction + A float value ranging from 0.0f to 1.0f. Sets how much the Part will be able to slide. a value of 1 is no sliding, and 0 is no friction, so infinite sliding. + + + + + Locked + Determines whether building tools (in-game and studio) can manipulate this Part. If true, no editing allowed. If false, editing is allowed. + + + + + Material + Specifies the look and feel the Part should have. Note: this does not define the color the Part is, see BrickColor for that. <a href="http://wiki.roblox.com/index.php/Material_(Enum)" target="_blank">More info</a> + + + + + Reflectance + Specifies how shiny the Part is. A value of 1 is completely reflective (chrome), while a value of 0 is no reflectance (concrete wall) + + + + + ResizeIncrement + Sets the value for the smallest change in size allowable by the Resize(NormalId, int) function. + + + + + ResizeableFaces + Sets the value for the faces allowed to be resized by the Resize(NormalId, int) function. + + + + + Transparency + Sets how visible an object is. A value of 1 makes the object invisible, while a value of 0 makes the object opaque. + + + + + Velocity + How fast the Part is traveling in studs/second. This property is NOT recommended to be modified directly, unless there is good reason. Otherwise, try using a BodyForce to move a Part. + + + + + + + + makeJoints + Use MakeJoints() instead + true + + + + + MakeJoints + Creates the appropriate SurfaceJoints with all parts that are touching this Instance (including internal joints in the Instance, as in a Model). This uses the SurfaceTypes defined on the surfaces of parts to create the appropriate welds. <a href="http://wiki.roblox.com/index.php/MakeJoints_(Method)" target="_blank">More info</a> + + + + + BreakJoints + Destroys SurfaceJoints with all parts that are touching this Instance (including internal joints in the Instance, as in a Model). + + + + + GetMass + Returns a number that is the mass of this Instance. Mass of a Part is immutable, and is changed only by the size of the Part. + + + + + Resize + Resizes a Part in the direction of the face defined by 'NormalId', by the amount specified by 'deltaAmount'. If the operation will expand the part to intersect another Instance, the part will not resize at all. Return true if the call is successful, false otherwise. + + + + + getMass + Use GetMass() instead + true + + + + + + + OutfitChanged + true + + + + + LocalSimulationTouched + true + Deprecated. Use Touched instead + + + + + StoppedTouching + + Deprecated. Use TouchEnded instead + + + + + TouchEnded + Fired when the part stops touching another part + + + + + + + Part + A plastic building block - the fundamental component of ROBLOX + 11 + 1 + + + + + TrussPart + An extendable building truss + 12 + 1 + + + + + WedgePart + A Wedge Part + 12 + 1 + + + + + PrismPart + A Prism Part + false + 12 + 1 + + + + + PyramidPart + A Pyramid Part + false + 12 + 1 + + + + + ParallelRampPart + A ParallelRamp Part + false + 12 + 1 + + + + + RightAngleRampPart + A RightAngleRamp Part + false + 12 + 1 + + + + + CornerWedgePart + A CornerWedge Part + 12 + 1 + + + + + PlayerGui + A container instance that syncs data between a single player and the server. ScreenGui objects that are placed in this container will be shown to the Player parent only + 13 + 46 + + + + + GuiMain + Deprecated, please use ScreenGui + true + 14 + 47 + + + + + + ScreenGui + The core GUI object on which tools are built. Add Frames/Labels/Buttons to this object to have them rendered as a 2D overlay + 14 + 47 + StarterGui + + + + + FunctionalTest + Deprecated. Use TestService instead + true + 1 + + + + + BillboardGui + A GUI that adorns an object in the 3D world. Add Frames/Labels/Buttons to this object to have them rendered while attached to a 3D object + 14 + 64 + StarterGui + + + + + + Adornee + The Object the billboard gui uses as its base to render from. Currently, the only way to set this property is thru a script, and must exist in the workspace. This will only render if the object assigned derives from BasePart. + + + + + AbsolutePosition + A read-only Vector2 value that is the GuiObject's current position (x,y) in pixel space, from the top left corner of the GuiObject. + + + + + AbsoluteSize + A read-only Vector2 value that is the GuiObject's current size (width, height) in pixel space. + + + + + Active + If true, this GuiObject can fire mouse events and will pass them to any GuiObjects layered underneath, while false will do neither. + + + + + AlwaysOnTop + If true, billboard gui does not get occluded by 3D objects, but always renders on the screen. + + + + + Enabled + If true, billboard gui will render, otherwise rendering will be skipped. + + + + + ExtentsOffset + A Vector3 (x,y,z) defined in studs that will offset the gui from the extents of the 3d object it is rendering from. + + + + + PlayerToHideFrom + Specifies a Player that the BillboardGui will not render to. + + + + + StudsOffset + A Vector3 (x,y,z) defined in studs that will offset the gui from the centroid of the 3d object it is rendering from + + + + + SizeOffset + A Vector2 (x,y) defined in studs that will offset the gui size from it's current size. + + + + + Size + A UDim2 value describing the size of the BillboardGui. More information on UDim2 is available <a href="http://wiki.roblox.com/index.php/UDim2" target="_blank">here</a>. Relative values are defined as one-to-one with studs. + + + + + + + + SurfaceGui + tbd + 14 + 64 + StarterGui + + + + + + Adornee + The Object the billboard gui uses as its base to render from. Currently, the only way to set this property is thru a script, and must exist in the workspace. This will only render if the object assigned derives from BasePart. + + + + + Active + If true, this GuiObject can fire mouse events and will pass them to any GuiObjects layered underneath, while false will do neither. + + + + + Enabled + If true, billboard gui will render, otherwise rendering will be skipped. + + + + + + + + + + GuiBase2d + false + + + + + + AbsolutePosition + A read-only Vector2 value that is the GuiObject's current position (x,y) in pixel space, from the top left corner of the GuiObject. + + + + + AbsoluteSize + A read-only Vector2 value that is the GuiObject's current size (width, height) in pixel space. + + + + + + + + InputObject + An object that describes a particular user input, such as mouse movement, touches, keyboard, and more. + + + + + UserInputType + An enum that describes what kind of input this object is describing (mousebutton, touch, etc.). See Enum.UserInputType for more info. + + + + + UserInputState + An enum that describes what state of a particular input (touch began, touch moved, touch ended, etc.). See Enum.UserInputState for more info. + + + + + Position + A Vector3 value that describes a positional value of this input. For mouse and touch input, this is the screen position of the mouse/touch, described in the x and y components. For mouse wheel input, the z component describes whether the wheel was moved forward or backward. + + + + + KeyCode + An enum that describes what kind of input is being pressed. For types of input like Keyboard, this describes what key was pressed. For input like mousebutton, this provides no additional information. + + + + + + + + GuiObject + false + + + + + + TweenPosition + Smoothly moves a GuiObject from its current position to 'endPosition'. The only required argument is 'endPosition'. <a href="http://wiki.roblox.com/index.php/TweenPosition_(Method)" target="_blank">More info</a> + + + + + TweenSize + Smoothly translates a GuiObject's current size to 'endSize'. The only required argument is 'endSize'. <a href="http://wiki.roblox.com/index.php/TweenSize_(Method)" target="_blank">More info</a> + + + + + TweenSizeAndPosition + Smoothly translates a GuiObject's current size to 'endSize', and also smoothly translates the GuiObject's current position to 'endPosition'. The only required arguments are 'endSize' and 'endPosition'. <a href="http://wiki.roblox.com/index.php/TweenSizeAndPosition_(Method)" target="_blank">More info</a> + + + + + + + + Active + If true, this GuiObject can fire mouse events and will pass them to any GuiObjects layered underneath, while false will do neither. + + + + + BackgroundColor3 + A Color3 value that specifies the background color for the GuiObject. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + BackgroundTransparency + A number value that specifies how transparent the background of the GuiObject is. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + BorderColor3 + A Color3 value that specifies the color of the outline of the GuiObject. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + BorderSizePixel + A number value that specifies the thickness (in pixels) of the outline of the GuiObject. Currently this value can only be set to either 0 or 1, any other number has no effect. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + ClipsDescendants + If set to true, any descendants of this GuiObject will only render if contained within it's borders. If set to false, all descendants will render regardless of position. + + + + + Draggable + If true, allows a GuiObject to be dragged by the user's mouse. The events 'DragBegin' and 'DragStopped' are fired when the appropriate action happens, and only will fire on Draggable=true GuiObjects. + + + + + Size + A UDim2 value describing the size of the GuiObject on screen in both absolute and relative coordinates. More information on UDim2 is available <a href="http://wiki.roblox.com/index.php/UDim2" target="_blank">here</a>. + + + + + Position + A UDim2 value describing the position of the top-left corner of the GuiObject on screen. More information on UDim2 is available <a href="http://wiki.roblox.com/index.php/UDim2" target="_blank">here</a>. + + + + + SizeConstraint + The direction(s) that an object can be resized in. <a href="http://wiki.roblox.com/index.php/SizeConstraint_(Enum)" target="_blank">More info</a>. + + + + + ZIndex + Describes the ordering in which overlapping GuiObjects will be drawn. A value of 1 is drawn first, while higher values are drawn in ascending order (each value draws over the last). + + + + + BackgroundColor + true + Deprecated. Use BackgroundColor3 instead + + + + + BorderColor + true + Deprecated. Use BorderColor3 instead + + + + + + + + DragBegin + Fired when a GuiObject with Draggable set to true starts to be dragged. 'InitialPosition' is a UDim2 value of the position of the GuiObject before any drag operation began. + + + + + DragStopped + Always fired after a DragBegin event, DragStopped is fired when the user releases the mouse button causing a drag operation on the GuiObject. Arguments 'x', and 'y' specify the top-left absolute position of the GuiObject when the event is fired. + + + + + MouseEnter + Fired when the mouse enters a GuiObject, as long as the GuiObject is active (see active property for more detail). Arguments 'x', and 'y' specify the absolute pixel position of the mouse. + + + + + MouseLeave + Fired when the mouse leaves a GuiObject, as long as the GuiObject is active (see active property for more detail). Arguments 'x', and 'y' specify the absolute pixel position of the mouse. + + + + + MouseMoved + Fired when the mouse is inside a GuiObject and moves, as long as the GuiObject is active (see active property for more detail). Arguments 'x', and 'y' specify the absolute pixel position of the mouse. + + + + + + TouchTap + Fired when a user taps their finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the tap gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchPinch + Fired when a user pinches their fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the pinch gesture. 'scale' is a float that indicates the difference from the beginning of the pinch gesture. 'velocity' is a float indicating how quickly the pinch gesture is happening. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. + + + + + TouchSwipe + Fired when a user swipes their fingers on a TouchEnabled device. 'swipeDirection' is an Enum.SwipeDirection, indicating the direction the user swiped. 'numberOfTouches' is an int that indicates how many touches were involved with the gesture. This event only fires locally. + + + + + TouchLongPress + Fired when a user holds at least one finger for a short amount of time on the same screen position on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. + + + + + TouchRotate + Fired when a user rotates two fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'rotation' is a float indicating how much the rotation has gone from the start of the gesture. 'velocity' is a float that indicates how quickly the gesture is being performed. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. + + + + + TouchPan + Fired when a user drags at least one finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'totalTranslation' is a Vector2, indicating how far the pan gesture has gone from its starting point. 'velocity' is a Vector2 that indicates how quickly the gesture is being performed in each dimension. 'state' indicates the Enum.UserInputState of the gesture. + + + + + + InputBegan + Fired when a user begins interacting via a Human-Computer Interface device (Mouse button down, touch begin, keyboard button down, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. + + + + + InputChanged + Fired when a user changes interacting via a Human-Computer Interface device (Mouse move, touch move, mouse wheel, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. + + + + + InputEnded + Fired when a user stops interacting via a Human-Computer Interface device (Mouse button up, touch end, keyboard button up, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. + + + + + + + + + NotificationBox + false + + + + + NotificationObject + false + + + + + Frame + A container object used to layout other GUI objects + 15 + 48 + StarterGui + + + + + Style + Determines how a frame will look. Uses Enum.FrameStyle. <a href="http://wiki.roblox.com/index.php/Framestyle" target="_blank">More info</a> + + + + + + + ScrollingFrame + A container object used to layout other GUI objects, and allows for scrolling. + 15 + 48 + StarterGui + + + + + ScrollingEnabled + Determines whether or not scrolling is allowed on this frame. If turned off, no scroll bars will be rendered. + + + + + CanvasSize + Determines the size of the area that is scrollable. The UDim2 is calculated using the parent gui's size, similar to the regular Size property on gui objects. + + + + + CanvasPosition + The absolute position the scroll frame is in respect to the canvas size. The minimum this can be set to is (0,0), while the max is the absolute canvas size - AbsoluteWindowSize. + + + + + AbsoluteWindowSize + The size in pixels of the frame, without the scrollbars. + + + + + ScrollBarThickness + How thick the scroll bar appears. This applies to both the horizontal and vertical scroll bars. Can be set to 0 for no bars render. + + + + + TopImage + The "Up" image on the vertical scrollbar. Size of this is always ScrollBarThickness by ScrollBarThickness. This is also used as the "left" image on the horizontal scroll bar. + + + + + MidImage + The "Middle" image on the vertical scrollbar. Size of this can vary in the y direction, but is always set at ScrollBarThickness in x direction. This is also used as the "mid" image on the horizontal scroll bar. + + + + + BottomImage + The "Down" image on the vertical scrollbar. Size of this is always ScrollBarThickness by ScrollBarThickness. This is also used as the "right" image on the horizontal scroll bar. + + + + + + + ImageLabel + A GUI object containing an Image + 18 + 49 + StarterGui + + + + + Image + Specifies the id of the texture to display. <a href="http://wiki.roblox.com/index.php/Image" target="_blank">More info</a> + + + + + + + TextLabel + A GUI object containing text + 19 + 50 + StarterGui + + + + + TextColor + true + Deprecated. Use TextColor3 instead + + + + + + + TextButton + A GUI button containing text + 17 + 51 + StarterGui + + + + + TextColor + true + Deprecated. Use TextColor3 instead + + + + + + + TextBox + A text entry box + 17 + 51 + StarterGui + + + + + TextColor + true + Deprecated. Use TextColor3 instead + + + + + + + GuiButton + A GUI button containing an Image + false + 16 + 52 + + + + + AutoButtonColor + Determines whether a button changes color automatically when reacting to mouse events. + + + + + Modal + Allows the mouse to be free in first person mode. If a button with this property set to true is visible, the mouse is 'free' in first person mode. + + + + + Style + Determines how a button will look, including mouse event states. Uses Enum.ButtonStyle. <a href="http://wiki.roblox.com/index.php/ButtonStyle_(Enum)" target="_blank">More info</a> + + + + + + + MouseButton1Click + Fired when the mouse is over the button, and the mouse down and up events fire without the mouse leaving the button. + + + + + MouseButton1Down + Fired when the mouse button is pushed down on a button. + + + + + MouseButton1Up + Fired when the mouse button is released on a button. + + + + + MouseButton2Click + This function currently does not work :( + + + + + MouseButton2Down + This function currently does not work :( + + + + + MouseButton2Up + This function currently does not work :( + + + + + + + + ImageButton + A GUI button containing an Image + 16 + 52 + StarterGui + + + + + Image + Specifies the asset id of the texture to display. <a href="http://wiki.roblox.com/index.php/Image" target="_blank">More info</a> + + + + + + + Handles + A 3D GUI object to represent draggable handles + + 19 + 53 + + + + + ArcHandles + A 3D GUI object to represent draggable arc handles + + 20 + 56 + + + + + SelectionBox + A 3D GUI object to represent the visible selection around an object + 21 + 54 + + + + + SurfaceSelection + A 3D GUI object to represent the visible selection around a face of an object + 21 + 55 + + + + + Configuration + An object that can be placed under parts to hold Value objects that represent that part's configuration + 22 + 58 + + + + + SelectionPartLasso + A visual line drawn representation between two part objects + 22 + 57 + + + + + SelectionPointLasso + A visual line drawn representation between two positions + 22 + 57 + + + + + PartPairLasso + A visual line drawn representation between two parts. + 22 + 57 + + + + + Pose + The pose of a joint relative to it's parent part in a keyframe + 22 + 60 + + + + + Keyframe + One keyframe of an animation + 22 + 60 + + + + + Animation + Represents a linked animation object, containing keyframes and poses. + 22 + 60 + + + + + AnimationTrack + Returned by a call to LoadAnimation. Controls the playback of an animation on a Humanoid. + 22 + 60 + + + + + Stopped + true + This event is never raised + + + + + + + AnimationController + Allows animations to be played on joints of the parent object. + 22 + 60 + + + + + CharacterMesh + Modifies the appearance of a body part. + 22 + 60 + + + + + Dialog + An object used to make dialog trees to converse with players + 22 + 62 + + + + + DialogChoice + An object used to make dialog trees to converse with players + 22 + 63 + + + + + Terrain + Object representing a high performance bounded grid of static 4x4 parts + true + 0 + 65 + + + + + GetCell + Returns CellMaterial, CellBlock, CellOrientation + + + + + GetWaterCell + + Returns hasAnyWater, WaterForce, WaterDirection + + + + + SetWaterCell + + + + + + + + PointLight + Makes the parent part emit light in a spherical shape + 3 + 13 + + + + + SpotLight + Makes the parent part emit light in a conical shape + 3 + 13 + + + + + RemoteFunction + Allow functions defined in one script to be called by another script across client/server boundary + 4 + 66 + + + + + RemoteEvent + Allow events defined in one script to be subscribed to by another script across client/server boundary + 5 + 67 + + + + + TerrainRegion + Object representing a snapshot of the region of terrain + true + 0 + 65 + + + + + ModuleScript + A script fragment. Only runs when another script uses require() on it. + 5 + 66 + + + + diff --git a/Client2014/RobloxProxy.dll b/Client2014/RobloxProxy.dll new file mode 100644 index 0000000..c382d6d Binary files /dev/null and b/Client2014/RobloxProxy.dll differ diff --git a/Client2014/SyntaxPlayerBeta.exe b/Client2014/SyntaxPlayerBeta.exe new file mode 100644 index 0000000..754f400 Binary files /dev/null and b/Client2014/SyntaxPlayerBeta.exe differ diff --git a/Client2014/VMProtectSDK32.dll b/Client2014/VMProtectSDK32.dll new file mode 100644 index 0000000..ac30c03 Binary files /dev/null and b/Client2014/VMProtectSDK32.dll differ diff --git a/Client2014/boost.dll b/Client2014/boost.dll new file mode 100644 index 0000000..6f48268 Binary files /dev/null and b/Client2014/boost.dll differ diff --git a/Client2014/content/fonts/Arial.font b/Client2014/content/fonts/Arial.font new file mode 100644 index 0000000..f0209da Binary files /dev/null and b/Client2014/content/fonts/Arial.font differ diff --git a/Client2014/content/fonts/ArialBold.font b/Client2014/content/fonts/ArialBold.font new file mode 100644 index 0000000..dab0c94 Binary files /dev/null and b/Client2014/content/fonts/ArialBold.font differ diff --git a/Client2014/content/fonts/CompositExtraSlot0.mesh b/Client2014/content/fonts/CompositExtraSlot0.mesh new file mode 100644 index 0000000..87b9d85 Binary files /dev/null and b/Client2014/content/fonts/CompositExtraSlot0.mesh differ diff --git a/Client2014/content/fonts/CompositExtraSlot1.mesh b/Client2014/content/fonts/CompositExtraSlot1.mesh new file mode 100644 index 0000000..6874c5e Binary files /dev/null and b/Client2014/content/fonts/CompositExtraSlot1.mesh differ diff --git a/Client2014/content/fonts/CompositExtraSlot2.mesh b/Client2014/content/fonts/CompositExtraSlot2.mesh new file mode 100644 index 0000000..423f198 Binary files /dev/null and b/Client2014/content/fonts/CompositExtraSlot2.mesh differ diff --git a/Client2014/content/fonts/CompositExtraSlot3.mesh b/Client2014/content/fonts/CompositExtraSlot3.mesh new file mode 100644 index 0000000..de05ced Binary files /dev/null and b/Client2014/content/fonts/CompositExtraSlot3.mesh differ diff --git a/Client2014/content/fonts/CompositExtraSlot4.mesh b/Client2014/content/fonts/CompositExtraSlot4.mesh new file mode 100644 index 0000000..ff9cb62 Binary files /dev/null and b/Client2014/content/fonts/CompositExtraSlot4.mesh differ diff --git a/Client2014/content/fonts/CompositFullAtlasBaseTexture.mesh b/Client2014/content/fonts/CompositFullAtlasBaseTexture.mesh new file mode 100644 index 0000000..b7e6595 Binary files /dev/null and b/Client2014/content/fonts/CompositFullAtlasBaseTexture.mesh differ diff --git a/Client2014/content/fonts/CompositFullAtlasOverlayTexture.mesh b/Client2014/content/fonts/CompositFullAtlasOverlayTexture.mesh new file mode 100644 index 0000000..eda1938 Binary files /dev/null and b/Client2014/content/fonts/CompositFullAtlasOverlayTexture.mesh differ diff --git a/Client2014/content/fonts/CompositLeftArmBase.mesh b/Client2014/content/fonts/CompositLeftArmBase.mesh new file mode 100644 index 0000000..5bcc4ae Binary files /dev/null and b/Client2014/content/fonts/CompositLeftArmBase.mesh differ diff --git a/Client2014/content/fonts/CompositLeftLegBase.mesh b/Client2014/content/fonts/CompositLeftLegBase.mesh new file mode 100644 index 0000000..f4712ce Binary files /dev/null and b/Client2014/content/fonts/CompositLeftLegBase.mesh differ diff --git a/Client2014/content/fonts/CompositPantsTemplate.mesh b/Client2014/content/fonts/CompositPantsTemplate.mesh new file mode 100644 index 0000000..756ee03 Binary files /dev/null and b/Client2014/content/fonts/CompositPantsTemplate.mesh differ diff --git a/Client2014/content/fonts/CompositRightArmBase.mesh b/Client2014/content/fonts/CompositRightArmBase.mesh new file mode 100644 index 0000000..02f2721 Binary files /dev/null and b/Client2014/content/fonts/CompositRightArmBase.mesh differ diff --git a/Client2014/content/fonts/CompositRightLegBase.mesh b/Client2014/content/fonts/CompositRightLegBase.mesh new file mode 100644 index 0000000..b287939 Binary files /dev/null and b/Client2014/content/fonts/CompositRightLegBase.mesh differ diff --git a/Client2014/content/fonts/CompositShirtTemplate.mesh b/Client2014/content/fonts/CompositShirtTemplate.mesh new file mode 100644 index 0000000..75487e1 Binary files /dev/null and b/Client2014/content/fonts/CompositShirtTemplate.mesh differ diff --git a/Client2014/content/fonts/CompositTShirt.mesh b/Client2014/content/fonts/CompositTShirt.mesh new file mode 100644 index 0000000..b39b8ac Binary files /dev/null and b/Client2014/content/fonts/CompositTShirt.mesh differ diff --git a/Client2014/content/fonts/CompositTorsoBase.mesh b/Client2014/content/fonts/CompositTorsoBase.mesh new file mode 100644 index 0000000..0388bde Binary files /dev/null and b/Client2014/content/fonts/CompositTorsoBase.mesh differ diff --git a/Client2014/content/fonts/LoadingScript.lua b/Client2014/content/fonts/LoadingScript.lua new file mode 100644 index 0000000..22969c6 --- /dev/null +++ b/Client2014/content/fonts/LoadingScript.lua @@ -0,0 +1,177 @@ +--rbxsig%YL1mzy/MHYBZIy/YPyTW5a+2IGIvi1rHoU0jlxv53VXzZYPw+HeXMzjSBg7YzXUS9p1Ft5aLlmul1YmvXypBGIcMKU/prdq4y9s9W3RG0/77IvYwMjZgKkTMhLyU8qvz70OKNqvdcCklqNC/fPHV35VvgqQk3we3CLShSXmIyMk=% +--rbxassetid%158948138% +-- Creates the generic "ROBLOX" loading screen on startup +-- Written by Ben Tkacheff, 2014 + +local frame +local forceRemovalTime = 5 +local destroyed = false + +Game:GetService("ContentProvider"):Preload("rbxasset://textures/roblox-logo.png") + +-- get control functions set up immediately +function removeLoadingScreen() + if frame then frame:Destroy() end + if script then script:Destroy() end + destroyed = true +end + +function startForceLoadingDoneTimer() + wait(forceRemovalTime) + removeLoadingScreen() +end + +function gameIsLoaded() + if Game.ReplicatedFirst:IsDefaultLoadingGuiRemoved() then + removeLoadingScreen() + else + startForceLoadingDoneTimer() + end +end + +function makeDefaultLoadingScreen() + if not settings():GetFFlag("NewLoadingScreen") then return end + if destroyed then return end + + frame = Instance.new("Frame") + frame.ZIndex = 10 + frame.Active = true + frame.Size = UDim2.new(1,0,1,0) + frame.BackgroundColor3 = Color3.new(48/255,90/255,206/255) + + local robloxLogo = Instance.new("ImageLabel") + robloxLogo.BackgroundTransparency = 1 + robloxLogo.ZIndex = 10 + robloxLogo.Image = "rbxasset://textures/roblox-logo.png" + robloxLogo.Size = UDim2.new(0,1031,0,265) + robloxLogo.Position = UDim2.new(0.5,-515,0.5,-132) + robloxLogo.Name = "RobloxLogo" + robloxLogo.Parent = frame + + local poweredByText = Instance.new("TextLabel") + poweredByText.Font = Enum.Font.SourceSansBold + poweredByText.FontSize = Enum.FontSize.Size24 + poweredByText.TextWrap = true + poweredByText.TextColor3 = Color3.new(1,1,1) + poweredByText.BackgroundTransparency = 1 + poweredByText.ZIndex = 10 + poweredByText.Text = "This Game Powered By" + poweredByText.TextXAlignment = Enum.TextXAlignment.Left + poweredByText.Size = UDim2.new(1,0,0,40) + poweredByText.Position = UDim2.new(0,0,0,-50) + poweredByText.Name = "PoweredByText" + poweredByText.Parent = robloxLogo + + local exitButton = Instance.new("ImageButton") + exitButton.ZIndex = 10 + exitButton.BackgroundTransparency = 1 + exitButton.Image = "rbxasset://textures/ui/CloseButton.png" + exitButton.Size = UDim2.new(0,22,0,22) + exitButton.Position = UDim2.new(1,-23,0,1) + exitButton.Name = "ExitButton" + exitButton:SetVerb("Exit") + + UserSettings().GameSettings.FullscreenChanged:connect(function ( isFullScreen ) + if isFullScreen then + exitButton.Parent = frame + else + exitButton.Parent = nil + end + end) + if UserSettings().GameSettings:InFullScreen()then + exitButton.Parent = frame + end + + -- put something visible up asap + frame.Parent = Game.CoreGui.RobloxGui + + local instanceText = Instance.new("TextLabel") + instanceText.Font = Enum.Font.SourceSansBold + instanceText.FontSize = Enum.FontSize.Size18 + instanceText.TextWrap = true + instanceText.TextColor3 = Color3.new(1,1,1) + instanceText.BackgroundTransparency = 1 + instanceText.ZIndex = 10 + instanceText.Text = "" + instanceText.Size = UDim2.new(1,0,0,40) + instanceText.Position = UDim2.new(0,0,1,-60) + instanceText.Name = "InstanceText" + instanceText.Parent = frame + + local loadingText = Instance.new("TextLabel") + loadingText.Font = Enum.Font.SourceSansBold + loadingText.FontSize = Enum.FontSize.Size36 + loadingText.TextWrap = true + loadingText.TextColor3 = Color3.new(1,1,1) + loadingText.BackgroundTransparency = 1 + loadingText.ZIndex = 10 + loadingText.Text = "Loading" + loadingText.Size = UDim2.new(1,0,0,40) + loadingText.Position = UDim2.new(0,0,1,20) + loadingText.Name = "LoadingText" + loadingText.Parent = robloxLogo + + local howManyDots = 0 + local lastUpdateTime = tick() + local minUpdateTime = 0.3 + local aspectRatio = 1031/265 + + function ResolutionChanged( prop ) + if prop == "AbsoluteSize" then + local size = Game.CoreGui.RobloxGui.AbsoluteSize + if size.X >= 1031 then + robloxLogo.Size = UDim2.new(0,1031,0,265) + robloxLogo.Position = UDim2.new(0.5,-515,0.5,-132) + else + local sizeReducer = -0.05 + while size.X < robloxLogo.AbsoluteSize.X do + + robloxLogo.Size = UDim2.new(sizeReducer,1031,0,265) + local newY = robloxLogo.AbsoluteSize.X * 265/1031 + robloxLogo.Size = UDim2.new(sizeReducer,1031,0,newY) + robloxLogo.Position = UDim2.new(0.5 - (sizeReducer/2),-515,0.5,-132) + + sizeReducer = sizeReducer - 0.1 + end + end + end + end + + ResolutionChanged("AbsoluteSize") + Game.CoreGui.RobloxGui.Changed:connect(ResolutionChanged) + + Game:GetService("RunService").RenderStepped:connect(function() + instanceText.Text = Game:GetMessage() + + if tick() - lastUpdateTime >= minUpdateTime then + howManyDots = howManyDots + 1 + if howManyDots > 5 then + howManyDots = 0 + end + + loadingText.Text = "Loading" + for i = 1, howManyDots do + loadingText.Text = loadingText.Text .. "." + end + lastUpdateTime = tick() + end + end) +end + +makeDefaultLoadingScreen() + +Game.ReplicatedFirst.RemoveDefaultLoadingGuiSignal:connect(function() + removeLoadingScreen() +end) +if Game.ReplicatedFirst:IsDefaultLoadingGuiRemoved() then + removeLoadingScreen() + return +end + +Game.Loaded:connect(function() + gameIsLoaded() +end) + +if Game:IsLoaded() then + gameIsLoaded() +end \ No newline at end of file diff --git a/Client2014/content/fonts/Rocket.rbxm b/Client2014/content/fonts/Rocket.rbxm new file mode 100644 index 0000000..72beea5 --- /dev/null +++ b/Client2014/content/fonts/Rocket.rbxm @@ -0,0 +1,102 @@ + + null + nil + + + false + -0.5 + 0.5 + 3 + 0 + -0.5 + 0.5 + 3 + 0 + 23 + + -0.5 + 0.5 + 0 + -1.1920929e-007 + 1.00000012 + 0 + 1.00000012 + -1.1920929e-007 + 0 + 0 + 0 + -1.00000024 + + true + true + 0 + true + true + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 3 + 0 + -0.5 + 0.5 + 3 + 0 + false + Rocket + 0 + -0.5 + 0.5 + 3 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 1 + + 1 + 1 + 4 + + + + + true + Swoosh + 0 + false + rbxasset://sounds\Rocket whoosh 01.wav + 0.699999988 + + + + + false + Explosion + 0 + true + rbxasset://sounds\collide.wav + 1 + + + + + Script + r = game:service("RunService") shaft = script.Parent position = Vector3.new(0,0,0) function fly() direction = shaft.CFrame.lookVector position = position + direction error = position - shaft.Position shaft.Velocity = 7*error end function blow() swoosh:Stop() explosion = Instance.new("Explosion") explosion.Position = shaft.Position explosion.Parent = game.Workspace connection:disconnect() shaft:remove() end t, s = r.Stepped:wait() swoosh = script.Parent.Swoosh swoosh:Play() position = shaft.Position d = t + 10.0 - s connection = shaft.Touched:connect(blow) while t < d do fly() t = r.Stepped:wait() end script.Parent.Explosion.PlayOnRemove = false swoosh:Stop() shaft:remove() + + + + \ No newline at end of file diff --git a/Client2014/content/fonts/SlingshotPellet.rbxm b/Client2014/content/fonts/SlingshotPellet.rbxm new file mode 100644 index 0000000..d753b0a --- /dev/null +++ b/Client2014/content/fonts/SlingshotPellet.rbxm @@ -0,0 +1,82 @@ + + null + nil + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 194 + + 0 + 6.4000001 + -8 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + true + 0 + true + true + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + false + Pellet + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + + 2 + 2 + 2 + + + + + Script + pellet = script.Parent damage = 8 function onTouched(hit) humanoid = hit.Parent:findFirstChild("Humanoid") if humanoid~=nil then humanoid.Health = humanoid.Health - damage connection:disconnect() else damage = damage / 2 if damage < 0.1 then connection:disconnect() end end end connection = pellet.Touched:connect(onTouched) r = game:service("RunService") t, s = r.Stepped:wait() d = t + 1.0 - s while t < d do t = r.Stepped:wait() end pellet.Parent = nil + + + + \ No newline at end of file diff --git a/Client2014/content/fonts/SourceSans.font b/Client2014/content/fonts/SourceSans.font new file mode 100644 index 0000000..ea67ffc Binary files /dev/null and b/Client2014/content/fonts/SourceSans.font differ diff --git a/Client2014/content/fonts/SourceSansBold.font b/Client2014/content/fonts/SourceSansBold.font new file mode 100644 index 0000000..1749dab Binary files /dev/null and b/Client2014/content/fonts/SourceSansBold.font differ diff --git a/Client2014/content/fonts/character.rbxm b/Client2014/content/fonts/character.rbxm new file mode 100644 index 0000000..7bf4b8a --- /dev/null +++ b/Client2014/content/fonts/character.rbxm @@ -0,0 +1,527 @@ + + null + nil + + + 7 + true + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + erik.cassel + RBX1 + true + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 24 + + 0 + 4.5 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + true + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 2 + 1 + 1 + + + + + + 0 + Mesh + + 1.25 + 1.25 + 1.25 + + + + 1 + 1 + 1 + + true + + + + + 5 + face + 20 + 0 + rbxasset://textures/face.png + true + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 23 + + 0 + 3 + 25.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + true + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 2 + 0 + true + Torso + 0 + 0 + 0 + 2 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 2 + 2 + 1 + + + + + 5 + roblox + 20 + 0 + + + + true + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 24 + + 1.5 + 3 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Left Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 24 + + -1.5 + 3 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Right Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 119 + + 0.5 + 1 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Left Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 119 + + -0.5 + 1 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Right Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + 100 + false + 100 + Humanoid + false + + 0 + 0 + 0 + + + 0 + 0 + 0 + + 0 + null + + 0 + 0 + 0 + + true + + + + \ No newline at end of file diff --git a/Client2014/content/fonts/character3.rbxm b/Client2014/content/fonts/character3.rbxm new file mode 100644 index 0000000..e39f9d6 --- /dev/null +++ b/Client2014/content/fonts/character3.rbxm @@ -0,0 +1,599 @@ + + null + nil + + + 7 + true + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + erik.cassel + RBX1 + true + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 24 + + 0 + 4.5 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + true + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 2 + 1 + 1 + + + + + + 0 + Mesh + + 1.25 + 1.25 + 1.25 + + + + 1 + 1 + 1 + + true + + + + + 5 + face + 20 + 0 + rbxasset://textures/face.png + true + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 23 + + 0 + 3 + 25.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + true + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 2 + 0 + true + Torso + 0 + 0 + 0 + 2 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 2 + 2 + 1 + + + + + 5 + roblox + 20 + 0 + + + + true + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 24 + + 1.5 + 3 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Left Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 24 + + -1.5 + 3 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Right Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 119 + + 0.5 + 1 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Left Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 119 + + -0.5 + 1 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Right Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + 100 + false + 100 + Humanoid + false + + 0 + 0 + 0 + + + 0 + 0 + 0 + + 0 + null + + 0 + 0 + 0 + + true + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 23 + + 0 + 3 + 25.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 0 + 0 + true + HumanoidRootPart + 0 + 0 + 0 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 1 + + 0 + 0 + 0 + + true + 1 + + 2 + 2 + 1 + + + + + \ No newline at end of file diff --git a/Client2014/content/fonts/comics.fnt b/Client2014/content/fonts/comics.fnt new file mode 100644 index 0000000..6b71c7e Binary files /dev/null and b/Client2014/content/fonts/comics.fnt differ diff --git a/Client2014/content/fonts/diogenes.fnt b/Client2014/content/fonts/diogenes.fnt new file mode 100644 index 0000000..c43fded --- /dev/null +++ b/Client2014/content/fonts/diogenes.fnt @@ -0,0 +1,356 @@ +a 6> +4=:90 +4;499, +4; & +4&=:90 +4&=:90& +4&&=:90 +4&&=:90& +4&&><&&0' +704&!<49 +7 !!3 6>0' +7 !!=4<' +7 !!&0- +7 !!&06& +7 !!&0>& +6=<;> +6<'690?0'> +69 +6:6>{ +6:6>& +6:6>& 6> +6:6>& 6>01 +6:6>& 6>0' +6:6>& 6><;2 +6:6>& 6>& +6:> +6:;1:8 +6:;1:8& +6::!0' +6 8 +6 880' +6 88<;2 +6 8& +6 8&=:! +6 8&=:! +6 ;<9<;2 & +6 ;<99<;2 & +6 ;;<9<;2 & +6 ;! +6 ;!9<6> +6 ;!9<6>0' +6 ;!9<6><;2 +6 ;!& +6,70'3 6 +6,70'3 6> +6,70'3 6>01 +6,70'3 6>0' +6,70'3 6>0'& +6,70'3 6><;2 +1<6> +1<6>& +1<>0 +1<91: +1<91: +1<91:& +1<91:& +1<%&=0 +0?46 94!0 +0?46 94!01 +0?46 94!0& +0?46 94!<;2 +0?46 94!<;2& +0?46 94!<:; +30' +3<;2 +342 +3420! +3422 +34220! +3422<;2 +3422 +36><;2 +3094!<: +3094!<: +30994!<: +3<;20'74;2 +3<;20'3 6> +3<;20'3 6>01 +3<;20'3 6>0' +3<;20'3 6>0'& +3<;20'3 6><;2 +3<;20'3 6>& +3<&!3 6> +3<&!3 6>01 +3<&!3 6>0' +3<&!3 6>0'& +3<&!3 6><;2 +3<&!3 6><;2& +3<&!3 6>& +3:6><;2 +3 6> +3 6>t +3 6>0 +3 6>01 +3 6>0; +3 6>0' +3 6>0'& +3 6><; +3 6><;2 +3 6><;2& +3 6>>>>>>>>>>>>>>>>> +3 6>80 +3 6>& +3 6>!4'1 +3 6>,: +3 > +3 >0' +3 ><; +3 ><;2 +3 >> +3 >& +3 '7 '20' +24;274;2 +24;274;201 +24;274;2& +24, +24,&0- +24/:;24& +24/:;20'& +2:;41& +2::> +=4'16:'0&0- +=4'1:; +=:8: +=::>0' +=:';<0&! +=:';, +=:!&0- += 8% +?46>4&& +?46><;2:33 +?46>:33 +?46>x:33 +?4% +?0'>x:33 +?0" +?<&8 +?<>0 +>>> +>:6> +>:;1 8 +>:;1 8& +> 8 +> 880' +> 88<;2 +> 8& +> ;<9<;2 & +90&7<4; +90&7: +9 &!<;2 +80'10 +8:90&! +8:!=43 6> +8:!=43 6> +8:!=43 6>4 +8:!=43 6>4 +8:!=43 6>4& +8:!=43 6>4& +8:!=43 6>4/ +8:!=43 6>4/ +8:!=43 6>01 +8:!=43 6>01 +8:!=43 6>0' +8:!=43 6>0' +8:!=43 6>0'& +8:!=43 6>0'& +8:!=43 6><; +8:!=43 6><; +8:!=43 6><;2 +8:!=43 6><;2 +8:!=43 6><;2& +8:!=43 6><;2& +8:!=43 6>& +8:!=43 6>& +8:!=0'3 6> +8:!=0'3 6> +8:!=0'3 6>01 +8:!=0'3 6>0' +8:!=0'3 6>0' +8:!=0'3 6>0'& +8:!=0'3 6>0'& +8:!=0'3 6><; +8:!=0'3 6><; +8:!=0'3 6><;2 +8:!=0'3 6><;2 +8:!=0'3 6><;2& +8:!=0'3 6><;2& +8:!=0'3 6>& +8:!=0'3 6>& +;4/< +;<20' +;<224 +;<224& +;<220' +;<220' +;<220'& +;<220'& +:'24&<8 +:'24&<8& +:'24&8 +:'24&8& +%06>0' +%0;<& +%=:;0&0- +%= >01 +%= ><;2 +%= >>01 +%= >><;2 +%= > +%= >& +%= $ +%<8% +%<&&:33 +%:'; +%:';: +%:';:2'4%=, +%:';:& +%'<6> +%'<6>& +%':&! +& 6> +!'48% +!"4! +#42<;4 +#<7'4!:' +"4;2 +"4;> +"4;>0' +"0!746> +"=:'0 +":% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Client2014/content/fonts/fonts.dds b/Client2014/content/fonts/fonts.dds new file mode 100644 index 0000000..add0f07 Binary files /dev/null and b/Client2014/content/fonts/fonts.dds differ diff --git a/Client2014/content/fonts/head.mesh b/Client2014/content/fonts/head.mesh new file mode 100644 index 0000000..26db950 Binary files /dev/null and b/Client2014/content/fonts/head.mesh differ diff --git a/Client2014/content/fonts/headA.mesh b/Client2014/content/fonts/headA.mesh new file mode 100644 index 0000000..76b2e72 Binary files /dev/null and b/Client2014/content/fonts/headA.mesh differ diff --git a/Client2014/content/fonts/headB.mesh b/Client2014/content/fonts/headB.mesh new file mode 100644 index 0000000..7705a05 Binary files /dev/null and b/Client2014/content/fonts/headB.mesh differ diff --git a/Client2014/content/fonts/headC.mesh b/Client2014/content/fonts/headC.mesh new file mode 100644 index 0000000..4672c59 Binary files /dev/null and b/Client2014/content/fonts/headC.mesh differ diff --git a/Client2014/content/fonts/headD.mesh b/Client2014/content/fonts/headD.mesh new file mode 100644 index 0000000..ff6cd6c Binary files /dev/null and b/Client2014/content/fonts/headD.mesh differ diff --git a/Client2014/content/fonts/headE.mesh b/Client2014/content/fonts/headE.mesh new file mode 100644 index 0000000..d722d29 Binary files /dev/null and b/Client2014/content/fonts/headE.mesh differ diff --git a/Client2014/content/fonts/headF.mesh b/Client2014/content/fonts/headF.mesh new file mode 100644 index 0000000..7c0b311 Binary files /dev/null and b/Client2014/content/fonts/headF.mesh differ diff --git a/Client2014/content/fonts/headG.mesh b/Client2014/content/fonts/headG.mesh new file mode 100644 index 0000000..38db235 Binary files /dev/null and b/Client2014/content/fonts/headG.mesh differ diff --git a/Client2014/content/fonts/headH.mesh b/Client2014/content/fonts/headH.mesh new file mode 100644 index 0000000..fe7365b Binary files /dev/null and b/Client2014/content/fonts/headH.mesh differ diff --git a/Client2014/content/fonts/headI.mesh b/Client2014/content/fonts/headI.mesh new file mode 100644 index 0000000..8ff7ba1 Binary files /dev/null and b/Client2014/content/fonts/headI.mesh differ diff --git a/Client2014/content/fonts/headJ.mesh b/Client2014/content/fonts/headJ.mesh new file mode 100644 index 0000000..ff76d44 Binary files /dev/null and b/Client2014/content/fonts/headJ.mesh differ diff --git a/Client2014/content/fonts/headK.mesh b/Client2014/content/fonts/headK.mesh new file mode 100644 index 0000000..b08bffc Binary files /dev/null and b/Client2014/content/fonts/headK.mesh differ diff --git a/Client2014/content/fonts/headL.mesh b/Client2014/content/fonts/headL.mesh new file mode 100644 index 0000000..645531b Binary files /dev/null and b/Client2014/content/fonts/headL.mesh differ diff --git a/Client2014/content/fonts/headM.mesh b/Client2014/content/fonts/headM.mesh new file mode 100644 index 0000000..ba144e8 Binary files /dev/null and b/Client2014/content/fonts/headM.mesh differ diff --git a/Client2014/content/fonts/headN.mesh b/Client2014/content/fonts/headN.mesh new file mode 100644 index 0000000..6fc4620 Binary files /dev/null and b/Client2014/content/fonts/headN.mesh differ diff --git a/Client2014/content/fonts/headO.mesh b/Client2014/content/fonts/headO.mesh new file mode 100644 index 0000000..a41f85f Binary files /dev/null and b/Client2014/content/fonts/headO.mesh differ diff --git a/Client2014/content/fonts/headP.mesh b/Client2014/content/fonts/headP.mesh new file mode 100644 index 0000000..cdb1647 Binary files /dev/null and b/Client2014/content/fonts/headP.mesh differ diff --git a/Client2014/content/fonts/humanoidAnimate.rbxm b/Client2014/content/fonts/humanoidAnimate.rbxm new file mode 100644 index 0000000..7a59cb9 --- /dev/null +++ b/Client2014/content/fonts/humanoidAnimate.rbxm @@ -0,0 +1,296 @@ + + null + nil + + + false + + Animate + -- Now with exciting TeamColors HACK! + +function waitForChild(parent, childName) + local child = parent:findFirstChild(childName) + if child then return child end + while true do + child = parent.ChildAdded:wait() + if child.Name==childName then return child end + end +end + +-- TEAM COLORS + + +function onTeamChanged(player) + + wait(1) + + local char = player.Character + if char == nil then return end + + if player.Neutral then + -- Replacing the current BodyColor object will force a reset + local old = char:findFirstChild("Body Colors") + if not old then return end + old:clone().Parent = char + old.Parent = nil + else + local head = char:findFirstChild("Head") + local torso = char:findFirstChild("Torso") + local left_arm = char:findFirstChild("Left Arm") + local right_arm = char:findFirstChild("Right Arm") + local left_leg = char:findFirstChild("Left Leg") + local right_leg = char:findFirstChild("Right Leg") + + if head then head.BrickColor = BrickColor.new(24) end + if torso then torso.BrickColor = player.TeamColor end + if left_arm then left_arm.BrickColor = BrickColor.new(26) end + if right_arm then right_arm.BrickColor = BrickColor.new(26) end + if left_leg then left_leg.BrickColor = BrickColor.new(26) end + if right_leg then right_leg.BrickColor = BrickColor.new(26) end + end +end + +function onPlayerPropChanged(property, player) + if property == "Character" then + onTeamChanged(player) + end + if property== "TeamColor" or property == "Neutral" then + onTeamChanged(player) + end +end + + +local cPlayer = game.Players:GetPlayerFromCharacter(script.Parent) +cPlayer.Changed:connect(function(property) onPlayerPropChanged(property, cPlayer) end ) +onTeamChanged(cPlayer) + + +-- ANIMATION + +-- declarations + +local Figure = script.Parent +local Torso = waitForChild(Figure, "Torso") +local RightShoulder = waitForChild(Torso, "Right Shoulder") +local LeftShoulder = waitForChild(Torso, "Left Shoulder") +local RightHip = waitForChild(Torso, "Right Hip") +local LeftHip = waitForChild(Torso, "Left Hip") +local Neck = waitForChild(Torso, "Neck") +local Humanoid = waitForChild(Figure, "Humanoid") +local pose = "Standing" + +local toolAnim = "None" +local toolAnimTime = 0 + +-- functions + +function onRunning(speed) + if speed>0 then + pose = "Running" + else + pose = "Standing" + end +end + +function onDied() + pose = "Dead" +end + +function onJumping() + pose = "Jumping" +end + +function onClimbing() + pose = "Climbing" +end + +function onGettingUp() + pose = "GettingUp" +end + +function onFreeFall() + pose = "FreeFall" +end + +function onFallingDown() + pose = "FallingDown" +end + +function onSeated() + pose = "Seated" +end + +function onPlatformStanding() + pose = "PlatformStanding" +end + +function moveJump() + RightShoulder.MaxVelocity = 0.5 + LeftShoulder.MaxVelocity = 0.5 + RightShoulder:SetDesiredAngle(3.14) + LeftShoulder:SetDesiredAngle(-3.14) + RightHip:SetDesiredAngle(0) + LeftHip:SetDesiredAngle(0) +end + + +-- same as jump for now + +function moveFreeFall() + RightShoulder.MaxVelocity = 0.5 + LeftShoulder.MaxVelocity = 0.5 + RightShoulder:SetDesiredAngle(3.14) + LeftShoulder:SetDesiredAngle(-3.14) + RightHip:SetDesiredAngle(0) + LeftHip:SetDesiredAngle(0) +end + +function moveSit() + RightShoulder.MaxVelocity = 0.15 + LeftShoulder.MaxVelocity = 0.15 + RightShoulder:SetDesiredAngle(3.14 /2) + LeftShoulder:SetDesiredAngle(-3.14 /2) + RightHip:SetDesiredAngle(3.14 /2) + LeftHip:SetDesiredAngle(-3.14 /2) +end + +function getTool() + for _, kid in ipairs(Figure:GetChildren()) do + if kid.className == "Tool" then return kid end + end + return nil +end + +function getToolAnim(tool) + for _, c in ipairs(tool:GetChildren()) do + if c.Name == "toolanim" and c.className == "StringValue" then + return c + end + end + return nil +end + +function animateTool() + + if (toolAnim == "None") then + RightShoulder:SetDesiredAngle(1.57) + return + end + + if (toolAnim == "Slash") then + RightShoulder.MaxVelocity = 0.5 + RightShoulder:SetDesiredAngle(0) + return + end + + if (toolAnim == "Lunge") then + RightShoulder.MaxVelocity = 0.5 + LeftShoulder.MaxVelocity = 0.5 + RightHip.MaxVelocity = 0.5 + LeftHip.MaxVelocity = 0.5 + RightShoulder:SetDesiredAngle(1.57) + LeftShoulder:SetDesiredAngle(1.0) + RightHip:SetDesiredAngle(1.57) + LeftHip:SetDesiredAngle(1.0) + return + end +end + +function move(time) + local amplitude + local frequency + + if (pose == "Jumping") then + moveJump() + return + end + + if (pose == "FreeFall") then + moveFreeFall() + return + end + + if (pose == "Seated") then + moveSit() + return + end + + local climbFudge = 0 + + if (pose == "Running") then + RightShoulder.MaxVelocity = 0.15 + LeftShoulder.MaxVelocity = 0.15 + amplitude = 1 + frequency = 9 + elseif (pose == "Climbing") then + RightShoulder.MaxVelocity = 0.5 + LeftShoulder.MaxVelocity = 0.5 + amplitude = 1 + frequency = 9 + climbFudge = 3.14 + else + amplitude = 0.1 + frequency = 1 + end + + desiredAngle = amplitude * math.sin(time*frequency) + + RightShoulder:SetDesiredAngle(desiredAngle + climbFudge) + LeftShoulder:SetDesiredAngle(desiredAngle - climbFudge) + RightHip:SetDesiredAngle(-desiredAngle) + LeftHip:SetDesiredAngle(-desiredAngle) + + + local tool = getTool() + + if tool then + + animStringValueObject = getToolAnim(tool) + + if animStringValueObject then + toolAnim = animStringValueObject.Value + -- message recieved, delete StringValue + animStringValueObject.Parent = nil + toolAnimTime = time + .3 + end + + if time > toolAnimTime then + toolAnimTime = 0 + toolAnim = "None" + end + + animateTool() + + + else + toolAnim = "None" + toolAnimTime = 0 + end +end + + +-- connect events + +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Jumping:connect(onJumping) +Humanoid.Climbing:connect(onClimbing) +Humanoid.GettingUp:connect(onGettingUp) +Humanoid.FreeFalling:connect(onFreeFall) +Humanoid.FallingDown:connect(onFallingDown) +Humanoid.Seated:connect(onSeated) +Humanoid.PlatformStanding:connect(onPlatformStanding) + +-- main program + +local runService = game:service("RunService"); + +while Figure.Parent~=nil do + local _, time = wait(0.1) + move(time) +end + + true + + + \ No newline at end of file diff --git a/Client2014/content/fonts/humanoidAnimateLocal.rbxm b/Client2014/content/fonts/humanoidAnimateLocal.rbxm new file mode 100644 index 0000000..5c18211 --- /dev/null +++ b/Client2014/content/fonts/humanoidAnimateLocal.rbxm @@ -0,0 +1,336 @@ + + null + nil + + + false + + Animate + + + function waitForChild(parent, childName) + local child = parent:findFirstChild(childName) + if child then return child end + while true do + child = parent.ChildAdded:wait() + if child.Name==childName then return child end + end +end + +-- ANIMATION + +-- declarations + +local Figure = script.Parent +local Torso = waitForChild(Figure, "Torso") +local RightShoulder = waitForChild(Torso, "Right Shoulder") +local LeftShoulder = waitForChild(Torso, "Left Shoulder") +local RightHip = waitForChild(Torso, "Right Hip") +local LeftHip = waitForChild(Torso, "Left Hip") +local Neck = waitForChild(Torso, "Neck") +local Humanoid = waitForChild(Figure, "Humanoid") +local pose = "Standing" + +local toolAnim = "None" +local toolAnimTime = 0 + +local jumpMaxLimbVelocity = 0.75 + +-- functions + +function onRunning(speed) + if speed>0 then + pose = "Running" + else + pose = "Standing" + end +end + +function onDied() + pose = "Dead" +end + +function onJumping() + pose = "Jumping" +end + +function onClimbing() + pose = "Climbing" +end + +function onGettingUp() + pose = "GettingUp" +end + +function onFreeFall() + pose = "FreeFall" +end + +function onFallingDown() + pose = "FallingDown" +end + +function onSeated() + pose = "Seated" +end + +function onPlatformStanding() + pose = "PlatformStanding" +end + +function onSwimming(speed) + if speed>0 then + pose = "Running" + else + pose = "Standing" + end +end + +function moveJump() + RightShoulder.MaxVelocity = jumpMaxLimbVelocity + LeftShoulder.MaxVelocity = jumpMaxLimbVelocity + RightShoulder:SetDesiredAngle(3.14) + LeftShoulder:SetDesiredAngle(-3.14) + RightHip:SetDesiredAngle(0) + LeftHip:SetDesiredAngle(0) +end + + +-- same as jump for now + +function moveFreeFall() + RightShoulder.MaxVelocity = jumpMaxLimbVelocity + LeftShoulder.MaxVelocity = jumpMaxLimbVelocity + RightShoulder:SetDesiredAngle(3.14) + LeftShoulder:SetDesiredAngle(-3.14) + RightHip:SetDesiredAngle(0) + LeftHip:SetDesiredAngle(0) +end + +function moveSit() + RightShoulder.MaxVelocity = 0.15 + LeftShoulder.MaxVelocity = 0.15 + RightShoulder:SetDesiredAngle(3.14 /2) + LeftShoulder:SetDesiredAngle(-3.14 /2) + RightHip:SetDesiredAngle(3.14 /2) + LeftHip:SetDesiredAngle(-3.14 /2) +end + +function getTool() + for _, kid in ipairs(Figure:GetChildren()) do + if kid.className == "Tool" then return kid end + end + return nil +end + +function getToolAnim(tool) + for _, c in ipairs(tool:GetChildren()) do + if c.Name == "toolanim" and c.className == "StringValue" then + return c + end + end + return nil +end + +function animateTool() + + if (toolAnim == "None") then + RightShoulder:SetDesiredAngle(1.57) + return + end + + if (toolAnim == "Slash") then + RightShoulder.MaxVelocity = 0.5 + RightShoulder:SetDesiredAngle(0) + return + end + + if (toolAnim == "Lunge") then + RightShoulder.MaxVelocity = 0.5 + LeftShoulder.MaxVelocity = 0.5 + RightHip.MaxVelocity = 0.5 + LeftHip.MaxVelocity = 0.5 + RightShoulder:SetDesiredAngle(1.57) + LeftShoulder:SetDesiredAngle(1.0) + RightHip:SetDesiredAngle(1.57) + LeftHip:SetDesiredAngle(1.0) + return + end +end + +function move(time) + local amplitude + local frequency + + if (pose == "Jumping") then + moveJump() + return + end + + if (pose == "FreeFall") then + moveFreeFall() + return + end + + if (pose == "Seated") then + moveSit() + return + end + + local climbFudge = 0 + + if (pose == "Running") then + if (RightShoulder.CurrentAngle > 1.5 or RightShoulder.CurrentAngle < -1.5) then + RightShoulder.MaxVelocity = jumpMaxLimbVelocity + else + RightShoulder.MaxVelocity = 0.15 + end + if (LeftShoulder.CurrentAngle > 1.5 or LeftShoulder.CurrentAngle < -1.5) then + LeftShoulder.MaxVelocity = jumpMaxLimbVelocity + else + LeftShoulder.MaxVelocity = 0.15 + end + amplitude = 1 + frequency = 9 + elseif (pose == "Climbing") then + RightShoulder.MaxVelocity = 0.5 + LeftShoulder.MaxVelocity = 0.5 + amplitude = 1 + frequency = 9 + climbFudge = 3.14 + else + amplitude = 0.1 + frequency = 1 + end + + desiredAngle = amplitude * math.sin(time*frequency) + + RightShoulder:SetDesiredAngle(desiredAngle + climbFudge) + LeftShoulder:SetDesiredAngle(desiredAngle - climbFudge) + RightHip:SetDesiredAngle(-desiredAngle) + LeftHip:SetDesiredAngle(-desiredAngle) + + + local tool = getTool() + + if tool then + + animStringValueObject = getToolAnim(tool) + + if animStringValueObject then + toolAnim = animStringValueObject.Value + -- message recieved, delete StringValue + animStringValueObject.Parent = nil + toolAnimTime = time + .3 + end + + if time > toolAnimTime then + toolAnimTime = 0 + toolAnim = "None" + end + + animateTool() + + + else + toolAnim = "None" + toolAnimTime = 0 + end +end + + +-- connect events + +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Jumping:connect(onJumping) +Humanoid.Climbing:connect(onClimbing) +Humanoid.GettingUp:connect(onGettingUp) +Humanoid.FreeFalling:connect(onFreeFall) +Humanoid.FallingDown:connect(onFallingDown) +Humanoid.Seated:connect(onSeated) +Humanoid.PlatformStanding:connect(onPlatformStanding) +Humanoid.Swimming:connect(onSwimming) +-- main program + +local runService = game:service("RunService"); + +while Figure.Parent~=nil do + local _, time = wait(0.1) + move(time) +end + + true + + + + + false + + + + RobloxTeam + + -- Now with exciting TeamColors HACK! + + function waitForChild(parent, childName) + local child = parent:findFirstChild(childName) + if child then return child end + while true do + child = parent.ChildAdded:wait() + if child.Name==childName then return child end + end + end + + -- TEAM COLORS + + + function onTeamChanged(player) + + wait(1) + + local char = player.Character + if char == nil then return end + + if player.Neutral then + -- Replacing the current BodyColor object will force a reset + local old = char:findFirstChild("Body Colors") + if not old then return end + old:clone().Parent = char + old.Parent = nil + else + local head = char:findFirstChild("Head") + local torso = char:findFirstChild("Torso") + local left_arm = char:findFirstChild("Left Arm") + local right_arm = char:findFirstChild("Right Arm") + local left_leg = char:findFirstChild("Left Leg") + local right_leg = char:findFirstChild("Right Leg") + + if head then head.BrickColor = BrickColor.new(24) end + if torso then torso.BrickColor = player.TeamColor end + if left_arm then left_arm.BrickColor = BrickColor.new(26) end + if right_arm then right_arm.BrickColor = BrickColor.new(26) end + if left_leg then left_leg.BrickColor = BrickColor.new(26) end + if right_leg then right_leg.BrickColor = BrickColor.new(26) end + end + end + + function onPlayerPropChanged(property, player) + if property == "Character" then + onTeamChanged(player) + end + if property== "TeamColor" or property == "Neutral" then + onTeamChanged(player) + end + end + + + local cPlayer = game.Players:GetPlayerFromCharacter(script.Parent) + cPlayer.Changed:connect(function(property) onPlayerPropChanged(property, cPlayer) end ) + onTeamChanged(cPlayer) + + + true + + + \ No newline at end of file diff --git a/Client2014/content/fonts/humanoidAnimateLocalKeyframe.rbxm b/Client2014/content/fonts/humanoidAnimateLocalKeyframe.rbxm new file mode 100644 index 0000000..981e3cb --- /dev/null +++ b/Client2014/content/fonts/humanoidAnimateLocalKeyframe.rbxm @@ -0,0 +1,606 @@ + + null + nil + + + false + + Animate + function waitForChild(parent, childName) + local child = parent:findFirstChild(childName) + if child then return child end + while true do + child = parent.ChildAdded:wait() + if child.Name==childName then return child end + end +end + +local Figure = script.Parent +local Torso = waitForChild(Figure, "Torso") +local RightShoulder = waitForChild(Torso, "Right Shoulder") +local LeftShoulder = waitForChild(Torso, "Left Shoulder") +local RightHip = waitForChild(Torso, "Right Hip") +local LeftHip = waitForChild(Torso, "Left Hip") +local Neck = waitForChild(Torso, "Neck") +local Humanoid = waitForChild(Figure, "Humanoid") +local pose = "Standing" + +local currentAnim = "" +local currentAnimTrack = nil +local currentAnimKeyframeHandler = nil +local currentAnimSpeed = 1.0 +local oldAnimTrack = nil +local animTable = {} +local animNames = { + idle = { + { id = "http://www.roblox.com/asset/?id=125750544", weight = 9 }, + { id = "http://www.roblox.com/asset/?id=125750618", weight = 1 } + }, + walk = { + { id = "http://www.roblox.com/asset/?id=125749145", weight = 10 } + }, + run = { + { id = "run.xml", weight = 10 } + }, + jump = { + { id = "http://www.roblox.com/asset/?id=125750702", weight = 10 } + }, + fall = { + { id = "http://www.roblox.com/asset/?id=125750759", weight = 10 } + }, + climb = { + { id = "http://www.roblox.com/asset/?id=125750800", weight = 10 } + }, + toolnone = { + { id = "http://www.roblox.com/asset/?id=125750867", weight = 10 } + }, + toolslash = { + { id = "http://www.roblox.com/asset/?id=129967390", weight = 10 } +-- { id = "slash.xml", weight = 10 } + }, + toollunge = { + { id = "http://www.roblox.com/asset/?id=129967478", weight = 10 } + }, + wave = { + { id = "http://www.roblox.com/asset/?id=128777973", weight = 10 } + }, + point = { + { id = "http://www.roblox.com/asset/?id=128853357", weight = 10 } + }, + dance = { + { id = "http://www.roblox.com/asset/?id=130018893", weight = 10 }, + { id = "http://www.roblox.com/asset/?id=132546839", weight = 10 }, + { id = "http://www.roblox.com/asset/?id=132546884", weight = 10 } + }, + laugh = { + { id = "http://www.roblox.com/asset/?id=129423131", weight = 10 } + }, + cheer = { + { id = "http://www.roblox.com/asset/?id=129423030", weight = 10 } + }, +} + +-- Existance in this list signifies that it is an emote, the value indicates if it is a looping emote +local emoteNames = { wave = false, point = false, dance = true, laugh = false, cheer = false} + +math.randomseed(tick()) + +-- Setup animation objects +for name, fileList in pairs(animNames) do + animTable[name] = {} + animTable[name].count = 0 + animTable[name].totalWeight = 0 + + -- check for config values + local config = script:FindFirstChild(name) + if (config ~= nil) then +-- print("Loading anims " .. name) + local idx = 1 + for _, childPart in pairs(config:GetChildren()) do + animTable[name][idx] = {} + animTable[name][idx].anim = childPart + local weightObject = childPart:FindFirstChild("Weight") + if (weightObject == nil) then + animTable[name][idx].weight = 1 + else + animTable[name][idx].weight = weightObject.Value + end + animTable[name].count = animTable[name].count + 1 + animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight +-- print(name .. " [" .. idx .. "] " .. animTable[name][idx].anim.AnimationId .. " (" .. animTable[name][idx].weight .. ")") + idx = idx + 1 + end + end + + -- fallback to defaults + if (animTable[name].count <= 0) then + for idx, anim in pairs(fileList) do + animTable[name][idx] = {} + animTable[name][idx].anim = Instance.new("Animation") + animTable[name][idx].anim.Name = name + animTable[name][idx].anim.AnimationId = anim.id + animTable[name][idx].weight = anim.weight + animTable[name].count = animTable[name].count + 1 + animTable[name].totalWeight = animTable[name].totalWeight + anim.weight +-- print(name .. " [" .. idx .. "] " .. anim.id .. " (" .. anim.weight .. ")") + end + end +end + +-- ANIMATION + +-- declarations +local toolAnim = "None" +local toolAnimTime = 0 + +local jumpAnimTime = 0 +local jumpAnimDuration = 0.175 + +local toolTransitionTime = 0.1 +local fallTransitionTime = 0.2 +local jumpMaxLimbVelocity = 0.75 + +-- functions + +function stopAllAnimations() + local oldAnim = currentAnim + + -- return to idle if finishing an emote + if (emoteNames[oldAnim] ~= nil and emoteNames[oldAnim] == false) then + oldAnim = "idle" + end + + currentAnim = "" + if (currentAnimKeyframeHandler ~= nil) then + currentAnimKeyframeHandler:disconnect() + end + + if (oldAnimTrack ~= nil) then + oldAnimTrack:Stop() + oldAnimTrack:Destroy() + oldAnimTrack = nil + end + if (currentAnimTrack ~= nil) then + currentAnimTrack:Stop() + currentAnimTrack:Destroy() + currentAnimTrack = nil + end + return oldAnim +end + +function setAnimationSpeed(speed) + if speed ~= currentAnimSpeed then + currentAnimSpeed = speed + currentAnimTrack:AdjustSpeed(currentAnimSpeed) + end +end + +function keyFrameReachedFunc(frameName) + if (frameName == "End") then +-- print("Keyframe : ".. frameName) + local repeatAnim = stopAllAnimations() + local animSpeed = currentAnimSpeed + playAnimation(repeatAnim, 0.0, Humanoid) + setAnimationSpeed(animSpeed) + end +end + +-- Preload animations +function playAnimation(animName, transitionTime, humanoid) + if (animName ~= currentAnim) then + + if (oldAnimTrack ~= nil) then + oldAnimTrack:Stop() + oldAnimTrack:Destroy() + end + + currentAnimSpeed = 1.0 + local roll = math.random(1, animTable[animName].totalWeight) + local origRoll = roll + local idx = 1 + while (roll > animTable[animName][idx].weight) do + roll = roll - animTable[animName][idx].weight + idx = idx + 1 + end +-- print(animName .. " " .. idx .. " [" .. origRoll .. "]") + local anim = animTable[animName][idx].anim + + -- load it to the humanoid; get AnimationTrack + oldAnimTrack = currentAnimTrack + currentAnimTrack = humanoid:LoadAnimation(anim) + + -- play the animation + currentAnimTrack:Play(transitionTime) + currentAnim = animName + + -- set up keyframe name triggers + if (currentAnimKeyframeHandler ~= nil) then + currentAnimKeyframeHandler:disconnect() + end + currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:connect(keyFrameReachedFunc) + end +end + +------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------- + +local toolAnimName = "" +local toolOldAnimTrack = nil +local toolAnimTrack = nil +local currentToolAnimKeyframeHandler = nil + +function toolKeyFrameReachedFunc(frameName) + if (frameName == "End") then +-- print("Keyframe : ".. frameName) + local repeatAnim = stopToolAnimations() + playToolAnimation(repeatAnim, 0.0, Humanoid) + end +end + + +function playToolAnimation(animName, transitionTime, humanoid) + if (animName ~= toolAnimName) then + + if (toolAnimTrack ~= nil) then + toolAnimTrack:Stop() + toolAnimTrack:Destroy() + transitionTime = 0 + end + + local roll = math.random(1, animTable[animName].totalWeight) + local origRoll = roll + local idx = 1 + while (roll > animTable[animName][idx].weight) do + roll = roll - animTable[animName][idx].weight + idx = idx + 1 + end +-- print(animName .. " * " .. idx .. " [" .. origRoll .. "]") + local anim = animTable[animName][idx].anim + + -- load it to the humanoid; get AnimationTrack + toolOldAnimTrack = toolAnimTrack + toolAnimTrack = humanoid:LoadAnimation(anim) + + -- play the animation + toolAnimTrack:Play(transitionTime) + toolAnimName = animName + + currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:connect(toolKeyFrameReachedFunc) + end +end + +function stopToolAnimations() + local oldAnim = toolAnimName + + if (currentToolAnimKeyframeHandler ~= nil) then + currentToolAnimKeyframeHandler:disconnect() + end + + toolAnimName = "" + if (toolAnimTrack ~= nil) then + toolAnimTrack:Stop() + toolAnimTrack:Destroy() + toolAnimTrack = nil + end + + + return oldAnim +end + +------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------- + + +function onRunning(speed) + if speed>0 then + playAnimation("walk", 0.1, Humanoid) + pose = "Running" + else + playAnimation("idle", 0.1, Humanoid) + pose = "Standing" + end +end + +function onDied() + pose = "Dead" +end + +function onJumping() + playAnimation("jump", 0.1, Humanoid) + jumpAnimTime = jumpAnimDuration + pose = "Jumping" +end + +function onClimbing(speed) + playAnimation("climb", 0.1, Humanoid) + setAnimationSpeed(speed / 12.0) + pose = "Climbing" +end + +function onGettingUp() + pose = "GettingUp" +end + +function onFreeFall() + if (jumpAnimTime <= 0) then + playAnimation("fall", fallTransitionTime, Humanoid) + end + pose = "FreeFall" +end + +function onFallingDown() + pose = "FallingDown" +end + +function onSeated() + pose = "Seated" +end + +function onPlatformStanding() + pose = "PlatformStanding" +end + +function onSwimming(speed) + if speed>0 then + pose = "Running" + else + pose = "Standing" + end +end + +function getTool() + for _, kid in ipairs(Figure:GetChildren()) do + if kid.className == "Tool" then return kid end + end + return nil +end + +function getToolAnim(tool) + for _, c in ipairs(tool:GetChildren()) do + if c.Name == "toolanim" and c.className == "StringValue" then + return c + end + end + return nil +end + +function animateTool() + + if (toolAnim == "None") then + playToolAnimation("toolnone", toolTransitionTime, Humanoid) + return + end + + if (toolAnim == "Slash") then + playToolAnimation("toolslash", 0, Humanoid) + return + end + + if (toolAnim == "Lunge") then + playToolAnimation("toollunge", 0, Humanoid) + return + end +end + +function moveSit() + RightShoulder.MaxVelocity = 0.15 + LeftShoulder.MaxVelocity = 0.15 + RightShoulder:SetDesiredAngle(3.14 /2) + LeftShoulder:SetDesiredAngle(-3.14 /2) + RightHip:SetDesiredAngle(3.14 /2) + LeftHip:SetDesiredAngle(-3.14 /2) +end + +local lastTick = 0 + +function move(time) + local amplitude = 1 + local frequency = 1 + local deltaTime = time - lastTick + lastTick = time + + local climbFudge = 0 + local setAngles = false + + if (jumpAnimTime > 0) then + jumpAnimTime = jumpAnimTime - deltaTime + end + + if (pose == "FreeFall" and jumpAnimTime <= 0) then + playAnimation("fall", fallTransitionTime, Humanoid) + elseif (pose == "Seated") then + stopAllAnimations() + moveSit() + return + elseif (pose == "Running") then + playAnimation("walk", 0.1, Humanoid) + elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then +-- print("Wha " .. pose) + amplitude = 0.1 + frequency = 1 + setAngles = true + end + + if (setAngles) then + desiredAngle = amplitude * math.sin(time * frequency) + + RightShoulder:SetDesiredAngle(desiredAngle + climbFudge) + LeftShoulder:SetDesiredAngle(desiredAngle - climbFudge) + RightHip:SetDesiredAngle(-desiredAngle) + LeftHip:SetDesiredAngle(-desiredAngle) + end + + -- Tool Animation handling + local tool = getTool() + if tool then + + animStringValueObject = getToolAnim(tool) + + if animStringValueObject then + toolAnim = animStringValueObject.Value + -- message recieved, delete StringValue + animStringValueObject.Parent = nil + toolAnimTime = time + .3 + end + + if time > toolAnimTime then + toolAnimTime = 0 + toolAnim = "None" + end + + animateTool() + else + stopToolAnimations() + toolAnim = "None" + toolAnimTime = 0 + end +end + +-- connect events +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Jumping:connect(onJumping) +Humanoid.Climbing:connect(onClimbing) +Humanoid.GettingUp:connect(onGettingUp) +Humanoid.FreeFalling:connect(onFreeFall) +Humanoid.FallingDown:connect(onFallingDown) +Humanoid.Seated:connect(onSeated) +Humanoid.PlatformStanding:connect(onPlatformStanding) +Humanoid.Swimming:connect(onSwimming) + +-- setup emote chat hook +Game.Players.LocalPlayer.Chatted:connect(function(msg) + local emote = "" + if (string.sub(msg, 1, 3) == "/e ") then + emote = string.sub(msg, 4) + elseif (string.sub(msg, 1, 7) == "/emote ") then + emote = string.sub(msg, 8) + end + + if (pose == "Standing" and emoteNames[emote] ~= nil) then + playAnimation(emote, 0.1, Humanoid) + end +-- print("===> " .. string.sub(msg, 1, 3) .. "(" .. emote .. ")") +end) + + +-- main program + +local runService = game:service("RunService"); + +-- initialize to idle +playAnimation("idle", 0.1, Humanoid) +pose = "Standing" + +while Figure.Parent~=nil do + local _, time = wait(0.1) + move(time) +end + + + + + + + idle + + + + + http://www.roblox.com/asset/?id=125750544 + Animation1 + + + + Weight + 9 + + + + + + http://www.roblox.com/asset/?id=125750618 + Animation2 + + + + Weight + 1 + + + + + + + walk + + + + + http://www.roblox.com/asset/?id=125749145 + WalkAnim + + + + + + run + + + + + http://www.roblox.com/asset/?id=125749145 + RunAnim + + + + + + jump + + + + + http://www.roblox.com/asset/?id=125750702 + JumpAnim + + + + + + climb + + + + + http://www.roblox.com/asset/?id=125750800 + ClimbAnim + + + + + + toolnone + + + + + http://www.roblox.com/asset/?id=125750867 + ToolNoneAnim + + + + + + fall + + + + + http://www.roblox.com/asset/?id=125750759 + FallAnim + + + + + \ No newline at end of file diff --git a/Client2014/content/fonts/humanoidExtra.rbxm b/Client2014/content/fonts/humanoidExtra.rbxm new file mode 100644 index 0000000..ea08e27 --- /dev/null +++ b/Client2014/content/fonts/humanoidExtra.rbxm @@ -0,0 +1,11 @@ + + null + nil + + + false + Script + while script.Parent.Head==nil do wait(0.05) end function newSound(id) local sound = Instance.new("Sound") sound.SoundId = id sound.Parent = script.Parent.Head return sound end sDied = newSound("rbxasset://sounds/uuhhh.wav") sFallingDown = newSound("rbxasset://sounds/splat.wav") sFreeFalling = newSound("rbxasset://sounds/swoosh.wav") sGettingUp = newSound("rbxasset://sounds/hit.wav") sJumping = newSound("rbxasset://sounds/button.wav") sRunning = newSound("rbxasset://sounds/bfsl-minifigfoots1.mp3") sRunning.Looped = true function onDied() sDied:play() end function onState(state, sound) if state then sound:play() else sound:pause() end end function onRunning(speed) if speed>0 then sRunning:play() else sRunning:pause() end end while script.Parent.Humanoid==nil do wait(0.05) end h = script.Parent.Humanoid h.Died:connect(onDied) h.Running:connect(onRunning) h.Jumping:connect(function(state) onState(state, sJumping) end) h.GettingUp:connect(function(state) onState(state, sGettingUp) end) h.FreeFalling:connect(function(state) onState(state, sFreeFalling) end) h.FallingDown:connect(function(state) onState(state, sFallingDown) end) -- regeneration while true do local s = wait(1) local health=h.Health if health>0 and health<h.MaxHealth then health = health + 0.01*s*h.MaxHealth if health*1.05 < h.MaxHealth then h.Health = health else h.Health = h.MaxHealth end end end + + + \ No newline at end of file diff --git a/Client2014/content/fonts/humanoidHealth.rbxm b/Client2014/content/fonts/humanoidHealth.rbxm new file mode 100644 index 0000000..12cdbbb --- /dev/null +++ b/Client2014/content/fonts/humanoidHealth.rbxm @@ -0,0 +1,191 @@ + + null + nil + + + false + + HealthScript v2.0 + local humanoid = script.Parent.Humanoid + +if (humanoid == nil) then + print("ERROR: no humanoid found in 'HealthScript v2.0'") +end + + +function CreateGUI() + local p = game.Players:GetPlayerFromCharacter(humanoid.Parent) + print("Health for Player: " .. p.Name) + script.HealthGUI.Parent = p.PlayerGui +end + +function UpdateGUI(health) + local pgui = game.Players:GetPlayerFromCharacter(humanoid.Parent).PlayerGui + local tray = pgui.HealthGUI.Tray + + tray.HealthBar.Size = UDim2.new(0.2, 0, 0.8 * (health / humanoid.MaxHealth), 0) + tray.HealthBar.Position = UDim2.new(0.4, 0, 0.8 * (1- (health / humanoid.MaxHealth)) , 0) + +end + + +function HealthChanged(health) + UpdateGUI(health) +end + + +CreateGUI() +humanoid.HealthChanged:connect(HealthChanged) +humanoid.Died:connect(function() HealthChanged(0) end) + true + + + + HealthGUI + true + + + + false + 4285215356 + 1 + 4279970357 + 1 + Tray + + 0.949999988 + 0 + 0.380000025 + 0 + + + 0.0450000018 + 0 + 0.340000004 + 0 + + 0 + true + 1 + true + + + + false + 4294967295 + 1 + 4279970357 + 1 + http://www.roblox.com/asset/?id=18441769 + ImageLabel + + 0 + 0 + 0.800000012 + 3 + + + 1 + 0 + 0.25 + 0 + + 1 + true + 1 + true + + + + + false + 4286892054 + 0 + 4278190080 + 0 + HealthBar + + 0.420000017 + 0 + 0 + 0 + + + 0.159999996 + 0 + 0.800000012 + 0 + + 0 + true + 2 + true + + + + + false + 4289733411 + 0 + 4278190080 + 0 + HealthBarBacking + + 0.419999987 + 0 + 0 + 0 + + + 0.159999996 + 0 + 0.800000012 + 0 + + 0 + true + 1 + true + + + + + + + + false + + Health + function waitForChild(parent, childName) + local child = parent:findFirstChild(childName) + if child then return child end + while true do + child = parent.ChildAdded:wait() + if child.Name==childName then return child end + end + end + + -- declarations + + local Figure = script.Parent + local Head = waitForChild(Figure, "Head") + local Humanoid = waitForChild(Figure, "Humanoid") + + -- regeneration + while true do + local s = wait(1) + local health = Humanoid.Health + if health > 0 and health < Humanoid.MaxHealth then + health = health + 0.01 * s * Humanoid.MaxHealth + if health * 1.05 < Humanoid.MaxHealth then + Humanoid.Health = health + else + Humanoid.Health = Humanoid.MaxHealth + end + end + end + + true + + + \ No newline at end of file diff --git a/Client2014/content/fonts/humanoidSound.rbxm b/Client2014/content/fonts/humanoidSound.rbxm new file mode 100644 index 0000000..0523e7b --- /dev/null +++ b/Client2014/content/fonts/humanoidSound.rbxm @@ -0,0 +1,76 @@ + + null + nil + + + false + + Sound + -- util + +function waitForChild(parent, childName) + local child = parent:findFirstChild(childName) + if child then return child end + while true do + child = parent.ChildAdded:wait() + if child.Name==childName then return child end + end +end + +function newSound(id) + local sound = Instance.new("Sound") + sound.SoundId = id + sound.archivable = false + sound.Parent = script.Parent.Head + return sound +end + +-- declarations + +local sDied = newSound("rbxasset://sounds/uuhhh.wav") +local sFallingDown = newSound("rbxasset://sounds/splat.wav") +local sFreeFalling = newSound("rbxasset://sounds/swoosh.wav") +local sGettingUp = newSound("rbxasset://sounds/hit.wav") +local sJumping = newSound("rbxasset://sounds/button.wav") +local sRunning = newSound("rbxasset://sounds/bfsl-minifigfoots1.mp3") +sRunning.Looped = true + +local Figure = script.Parent +local Head = waitForChild(Figure, "Head") +local Humanoid = waitForChild(Figure, "Humanoid") + +-- functions + +function onDied() + sDied:Play() +end + +function onState(state, sound) + if state then + sound:Play() + else + sound:Pause() + end +end + +function onRunning(speed) + if speed>0 then + sRunning:Play() + else + sRunning:Pause() + end +end + +-- connect up + +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Jumping:connect(function(state) onState(state, sJumping) end) +Humanoid.GettingUp:connect(function(state) onState(state, sGettingUp) end) +Humanoid.FreeFalling:connect(function(state) onState(state, sFreeFalling) end) +Humanoid.FallingDown:connect(function(state) onState(state, sFallingDown) end) + + true + + + \ No newline at end of file diff --git a/Client2014/content/fonts/humanoidSound2.rbxm b/Client2014/content/fonts/humanoidSound2.rbxm new file mode 100644 index 0000000..01591b3 --- /dev/null +++ b/Client2014/content/fonts/humanoidSound2.rbxm @@ -0,0 +1,43 @@ + + null + nil + + + false + + Sound + -- util + +function waitForChild(parent, childName) + local child = parent:findFirstChild(childName) + if child then return child end + while true do + child = parent.ChildAdded:wait() + if child.Name==childName then return child end + end +end + +function newSound(id, name) + local sound = Instance.new("Sound") + sound.SoundId = id + sound.archivable = false + sound.Parent = script.Parent.Head + sound.Name = name + return sound +end + +-- declarations +local Figure = script.Parent +local Head = waitForChild(Figure, "Head") +local Humanoid = waitForChild(Figure, "Humanoid") + +local sDied = newSound("rbxasset://sounds/uuhhh.wav", "DiedSound") +local sFallingDown = newSound("rbxasset://sounds/splat.wav", "FallingDownSound") +local sFreeFalling = newSound("rbxasset://sounds/swoosh.wav", "FreeFallingSound") +local sGettingUp = newSound("rbxasset://sounds/hit.wav", "GettingUpSound") +local sJumping = newSound("rbxasset://sounds/button.wav", "JumpingSound") +local sRunning = newSound("rbxasset://sounds/bfsl-minifigfoots1.mp3", "RunningSound") +sRunning.Looped = true + + + \ No newline at end of file diff --git a/Client2014/content/fonts/humanoidStatic.rbxm b/Client2014/content/fonts/humanoidStatic.rbxm new file mode 100644 index 0000000..1c9fcb7 --- /dev/null +++ b/Client2014/content/fonts/humanoidStatic.rbxm @@ -0,0 +1,12 @@ + + null + nil + + + false + Static + local Figure = script.Parent local Torso = Figure:findFirstChild("Torso") Torso:makeJoints() + true + + + \ No newline at end of file diff --git a/Client2014/content/fonts/leftarm.mesh b/Client2014/content/fonts/leftarm.mesh new file mode 100644 index 0000000..6e8bb63 Binary files /dev/null and b/Client2014/content/fonts/leftarm.mesh differ diff --git a/Client2014/content/fonts/leftleg.mesh b/Client2014/content/fonts/leftleg.mesh new file mode 100644 index 0000000..aba3a29 Binary files /dev/null and b/Client2014/content/fonts/leftleg.mesh differ diff --git a/Client2014/content/fonts/rightarm.mesh b/Client2014/content/fonts/rightarm.mesh new file mode 100644 index 0000000..14a52a4 Binary files /dev/null and b/Client2014/content/fonts/rightarm.mesh differ diff --git a/Client2014/content/fonts/rightleg.mesh b/Client2014/content/fonts/rightleg.mesh new file mode 100644 index 0000000..dab065d Binary files /dev/null and b/Client2014/content/fonts/rightleg.mesh differ diff --git a/Client2014/content/fonts/safechat.xml b/Client2014/content/fonts/safechat.xml new file mode 100644 index 0000000..199e3b2 --- /dev/null +++ b/Client2014/content/fonts/safechat.xml @@ -0,0 +1,737 @@ + + null + nil + Use the Chat menu to talk to me. + I can only see menu chats. + + Hello + + Hi + Hi there! + Hi everyone + + + Howdy + Howdy partner! + + + Greetings + Greetings everyone + Greetings Robloxians! + Seasons greetings! + + + Welcome + Welcome to my place + Welcome to our base + Welcome to my barbeque + + Hey there! + + What's up? + How are you doing? + How's it going? + What's new? + + + Good day + Good morning + Good afternoon + Good evening + Good night + + + Silly + Waaaaaaaz up?! + Hullo! + Behold greatness, mortals! + Pardon me, is this Sparta? + THIS IS SPARTAAAA! + + + Happy Holidays! + Happy New Year! + Happy Valentine's Day! + Beware the Ides of March! + Happy St. Patrick's Day! + Happy Easter! + Happy Earth Day! + Happy 4th of July! + Happy Thanksgiving! + Happy Halloween! + Happy Hanukkah! + Merry Christmas! + Happy Halloween! + Happy Earth Day! + Happy May Day! + Happy Towel Day! + Happy ROBLOX Day! + Happy LOL Day! + + + + Goodbye + + Good Night + Sweet dreams + Go to sleep! + Lights out! + Bedtime + Going to bed now + + + Later + See ya later + Later gator! + See you tomorrow + + + Bye + Hasta la bye bye! + + I'll be right back + I have to go + + Farewell + Take care + Have a nice day + Goodluck! + Ta-ta for now! + + + Peace + Peace out! + Peace dudes! + Rest in pieces! + + + Silly + To the batcave! + Over and out! + Happy trails! + I've got to book it! + Tootles! + Smell you later! + GG! + My house is on fire! gtg. + + + + Friend + Wanna be friends? + + Follow me + Come to my place! + Come to my base! + Follow me, team! + Follow me + + + Your place is cool + Your place is fun + Your place is awesome + Your place looks good + This place is awesome! + + + Thank you + Thanks for playing + Thanks for visiting + Thanks for everything + No, thank you + Thanx + + + No problem + Don't worry + That's ok + np + + + You are ... + You are great! + You are good! + You are cool! + You are funny! + You are silly! + You are awesome! + You are doing something I don't like, please stop + + + I like ... + I like your name + I like your shirt + I like your place + I like your style + I like you + I like items + I like money + + + Sorry + My bad! + I'm sorry + Whoops! + Please forgive me. + I forgive you. + I didn't mean to do that. + Sorry, I'll stop now. + + + + Questions + + Who? + Who wants to be my friend? + Who wants to be on my team? + Who made this brilliant game? + LOLWHO? + + + What? + What is your favorite animal? + What is your favorite game? + What is your favorite movie? + What is your favorite TV show? + What is your favorite music? + What are your hobbies? + LOLWUT? + + + When? + When are you online? + When is the new version coming out? + When can we play again? + When will your place be done? + + + Where? + Where do you want to go? + Where are you going? + Where am I?! + Where did you go? + + + How? + How are you today? + How did you make this cool place? + LOLHOW? + + + Can I... + Can I have a tour? + Can I be on your team? + Can I be your friend? + Can I try something? + Can I have that please? + Can I have that back please? + Can I have borrow your hat? + Can I have borrow your gear? + + + + Answers + + You need help? + Check out the news section + Check out the help section + Read the wiki! + All the answers are in the wiki! + I will help you with this. + + + Some people ... + Me + Not me + You + All of us + Everyone but you + Builderman! + Telamon! + My team + My group + Mom + Dad + Sister + Brother + Cousin + Grandparent + Friend + + + Time ... + In the morning + In the afternoon + At night + Tomorrow + This week + This month + Sometime + Sometimes + Whenever you want + Never + After this + In 10 minutes + In a couple hours + In a couple days + + + Animals + + Cats + Lion + Tiger + Leopard + Cheetah + + + Dogs + Wolves + Beagle + Collie + Dalmatian + Poodle + Spaniel + Shepherd + Terrier + Retriever + + + Horses + Ponies + Stallions + Pwnyz + + + Reptiles + Dinosaurs + Lizards + Snakes + Turtles! + + Hamster + Monkey + Bears + + Fish + Goldfish + Sharks + Sea Bass + Halibut + Tropical Fish + + + Birds + Eagles + Penguins + Parakeets + Owls + Hawks + Pidgeons + + Elephants + + Mythical Beasts + Dragons + Unicorns + Sea Serpents + Sphinx + Cyclops + Minotaurs + Goblins + Honest Politicians + Ghosts + Scylla and Charybdis + + + + Games + + Roblox + BrickBattle + Community Building + Roblox Minigames + Contest Place + + Action + Puzzle + Strategy + Racing + RPG + Obstacle Course + Tycoon + + Board games + Chess + Checkers + Settlers of Catan + Tigris and Euphrates + El Grande + Stratego + Carcassonne + + + + Sports + Hockey + Soccer + Football + Baseball + Basketball + Volleyball + Tennis + Sports team practice + + Watersports + Surfing + Swimming + Water Polo + + + Winter sports + Skiing + Snowboarding + Sledding + Skating + + + Adventure + Rock climbing + Hiking + Fishing + Horseback riding + + + Wacky + Foosball + Calvinball + Croquet + Cricket + Dodgeball + Squash + Trampoline + + + + Movies/TV + Science Fiction + + Animated + Anime + + Comedy + Romantic + Action + Fantasy + + + Music + Country + Jazz + Rap + Hip-hop + Techno + Classical + Pop + Rock + + + Hobbies + + Computers + Building computers + Videogames + Coding + Hacking + + + The Internet + lol. teh internets! + Watching vids + + Dance + Gymnastics + + Martial Arts + Karate + Judo + Taikwon Do + Wushu + Street fighting + + Listening to music + + Music lessons + Playing in my band + Playing piano + Playing guitar + Playing violin + Playing drums + Playing a weird instrument + + Arts and crafts + + + Location + + USA + + West + Alaska + Arizona + California + Colorado + Hawaii + Idaho + Montana + Nevada + New Mexico + Oregon + Utah + Washington + Wyoming + + + Midwest + Illinois + Indiana + Iowa + Kansas + Michigan + Minnesota + Missouri + Nebraska + North Dakota + Ohio + South Dakota + Wisconsin + + + Northeast + Connecticut + Delaware + Maine + Maryland + Massachusetts + New Hampshire + New Jersey + New York + Pennsylvania + Rhode Island + Vermont + + + South + Alabama + Arkansas + Florida + Georgia + Kentucky + Louisiana + Mississippi + North Carolina + Oklahoma + South Carolina + Tennessee + Texas + Virginia + West Virginia + + + + Canada + Alberta + British Columbia + Manitoba + New Brunswick + Newfoundland + Northwest Territories + Nova Scotia + Nunavut + Ontario + Prince Edward Island + Quebec + Saskatchewan + Yukon + + Mexico + Central America + + Europe + + Great Britain + England + Scotland + Wales + Northern Ireland + + France + Germany + Spain + Italy + Poland + Switzerland + Greece + Romania + Netherlands + + + Asia + China + India + Japan + Korea + Russia + Vietnam + + + South America + Argentina + Brazil + + + Africa + Eygpt + Swaziland + + Australia + Middle East + Antarctica + New Zealand + + + Age + Rugrat + Kid + Tween + Teen + Twenties + Old + Ancient + Mesozoic + I don't want to say my age. Don't ask. + + + Mood + Good + Great! + Not bad + Sad + Hyper + Chill + Happy + Kind of mad + + Boy + Girl + I don't want to say boy or girl. Don't ask. + + + Game + Let's build + Let's battle + Nice one! + So far so good! + Lucky shot! + Oh man! + I challenge you to a fight! + Help me with this + Let's go to your game + Can you show me how do to that? + Backflip! + Frontflip! + Dance! + I'm on your side! + + Game Commands + regen + reset + go + fix + respawn + + + + Silly + Muahahahaha! + all your base are belong to me! + GET OFF MAH LAWN + TEH EPIK DUCK IS COMING!!! + ROFL + + 1337 + i r teh pwnz0r! + w00t! + z0mg h4x! + ub3rR0xXorzage! + + + + Yes + Absolutely! + Rock on! + Totally! + Juice! + Yay! + Yesh + + + No + Ummm. No. + ... + Stop! + Go away! + Don't do that + Stop breaking the rules + I don't want to + + + Ok + Well... ok + Sure + + + Uncertain + Maybe + I don't know + idk + I can't decide + Hmm... + + + + :-) + :-( + :D + :-O + lol + =D + D= + XD + ;D + ;) + O_O + =) + @_@ + >_< + T_T + ^_^ + <(0_0<) <(0_0)> (>0_0)> KIRBY DANCE + )'; + :3 + + + Ratings + Rate it! + I give it a 1 out of 10 + I give it a 2 out of 10 + I give it a 3 out of 10 + I give it a 4 out of 10 + I give it a 5 out of 10 + I give it a 6 out of 10 + I give it a 7 out of 10 + I give it a 8 out of 10 + I give it a 9 out of 10 + I give it a 10 out of 10! + + diff --git a/Client2014/content/fonts/torso.mesh b/Client2014/content/fonts/torso.mesh new file mode 100644 index 0000000..43d58d1 Binary files /dev/null and b/Client2014/content/fonts/torso.mesh differ diff --git a/Client2014/content/music/bass.wav b/Client2014/content/music/bass.wav new file mode 100644 index 0000000..5f4b791 Binary files /dev/null and b/Client2014/content/music/bass.wav differ diff --git a/Client2014/content/music/ufofly.wav b/Client2014/content/music/ufofly.wav new file mode 100644 index 0000000..359710b Binary files /dev/null and b/Client2014/content/music/ufofly.wav differ diff --git a/Client2014/content/particles/explosion.particle b/Client2014/content/particles/explosion.particle new file mode 100644 index 0000000..a7c5e1f --- /dev/null +++ b/Client2014/content/particles/explosion.particle @@ -0,0 +1,192 @@ +///////////////////////////////////////////////////////// +// The fast rising center plume of the explosion. +// Grows and rotates. +///////////////////////////////////////////////////////// +particle_system explosion/explosionPlume +{ + quota 40 + material explosion/explosionMatl + particle_width 6 + particle_height 6 + cull_each false + renderer billboard + billboard_type point + sorted false + local_space false + iteration_interval 0 + nonvisible_update_timeout 0 + billboard_type point + billboard_origin center + billboard_rotation_type vertex + common_up_vector 0 1 0 + point_rendering false + accurate_facing false + + emitter Ellipsoid + { + angle 16 + colour 1.0 0.4 0.2 1.0 + colour_range_start 1.0 0.4 0.2 1.0 + colour_range_end 0.7 0.2 0.1 0.6 + direction 0 1 0 + emission_rate 100 + position 0 6 0 + velocity 25 + velocity_min 25 + velocity_max 38 + time_to_live 1.5 + time_to_live_min 1.5 + time_to_live_max 1.5 + duration 0.2 + duration_min 0.2 + duration_max 0.2 + repeat_delay 10000 + repeat_delay_min 10000 + repeat_delay_max 10000 + width 3 + height 3 + depth 3 + } + + affector ColourFader + { + red -0.9 + green -0.5 + blue -0.3 + alpha -1.0 + } + affector Scaler + { + rate 13 + } + affector Rotator + { + rotation_speed_range_start 100 + rotation_speed_range_end 200 + rotation_range_start 100 + rotation_range_end 300 + } +} + + +///////////////////////////////////////////////////////// +//The slow moving base of the explosion. +///////////////////////////////////////////////////////// +particle_system explosion/explosionBase +{ + quota 40 + material explosion/explosionMatl + particle_width 20 + particle_height 20 + cull_each false + renderer billboard + billboard_type point + sorted false + local_space false + iteration_interval 0 + nonvisible_update_timeout 0 + billboard_type point + billboard_origin center + billboard_rotation_type vertex + common_up_vector 0 1 0 + point_rendering false + accurate_facing false + + emitter Ellipsoid + { + angle 100 + colour 1.0 0.4 0.2 1.0 + colour_range_start 1.0 0.4 0.2 1.0 + colour_range_end 0.7 0.2 0.1 0.6 + direction 0 1 0 + emission_rate 80 + position 0 0 0 + velocity 11 + velocity_min 11 + velocity_max 16 + time_to_live 1.5 + time_to_live_min 1.5 + time_to_live_max 1.5 + duration 0.2 + duration_min 0.2 + duration_max 0.2 + repeat_delay 10000 + repeat_delay_min 10000 + repeat_delay_max 10000 + width 15 + height 15 + depth 15 + } + + affector ColourFader + { + red -0.9 + green -0.5 + blue -0.3 + alpha -1.0 + } + affector Scaler + { + rate 5 + } +} + + +///////////////////////////////////////////////////////// +// The fast flying sparks of the explosion. +///////////////////////////////////////////////////////// +particle_system explosion/explosionSparks +{ + quota 120 + material explosion/explosparkMatl + particle_width 3 + particle_height 3 + cull_each false + renderer billboard + billboard_type point + sorted false + local_space false + iteration_interval 0 + nonvisible_update_timeout 0 + billboard_type point + billboard_origin center + billboard_rotation_type vertex + common_up_vector 0 1 0 + point_rendering false + accurate_facing false + + emitter Point + { + angle 120 + colour 1.0 0.6 0.4 1.0 + colour_range_start 1.0 0.6 0.4 1.0 + colour_range_end 1.0 0.6 0.4 1.0 + direction 0 1 0 + emission_rate 900 + position 0 0 0 + velocity 30 + velocity_min 30 + velocity_max 60 + time_to_live 1.2 + time_to_live_min 1.2 + time_to_live_max 1.2 + duration 0.2 + duration_min 0.1 + duration_max 0.1 + repeat_delay 10000 + repeat_delay_min 10000 + repeat_delay_max 10000 + } + + affector ColourFader + { + red -0.7 + green -0.6 + blue -0.4 + alpha -0.7 + } + affector Scaler + { + rate -3 + } +} diff --git a/Client2014/content/particles/fire.particle b/Client2014/content/particles/fire.particle new file mode 100644 index 0000000..442fe8e --- /dev/null +++ b/Client2014/content/particles/fire.particle @@ -0,0 +1,59 @@ +///////////////////////////////////////////////////////// +// Fire +///////////////////////////////////////////////////////// + +// height and width modified by RbxParticleFactory for user input size +particle_system FireTemplate +{ + material fireMat1 + particle_width 5 + particle_height 5 + cull_each false + quota 40 + renderer billboard + billboard_type point + point_rendering false + accurate_facing false + sorted false + local_space false + iteration_interval 0 + nonvisible_update_timeout 0 + + // emission rate is modified by RbxParticleManager for throttling + // of particle systems + emitter Point + { + colour_range_start 240 240 240 + colour_range_end 240 240 240 + angle 18 + emission_rate 35 + time_to_live_min 1 + time_to_live_max 1 + direction 0 1 0 + velocity_min 2.33 + velocity_max 7.0 + } + affector Rotator + { + rotation_range_start 0 + rotation_range_end 365 + rotation_speed_range_start 0 + rotation_speed_range_end 100 + } + // modified in RbxParticleFactory for user input size + affector Scaler + { + rate -5.0 + } + // modified in RbxParticleFactory for user input colors + affector ColourInterpolator + { + time0 0 + colour0 240 240 240 1 + + time1 1 + colour1 240 240 240 1 + + } + +} diff --git a/Client2014/content/particles/forceFieldBeam.particle b/Client2014/content/particles/forceFieldBeam.particle new file mode 100644 index 0000000..22914a9 --- /dev/null +++ b/Client2014/content/particles/forceFieldBeam.particle @@ -0,0 +1,55 @@ +///////////////////////////////////////////////////////// +// Beam +///////////////////////////////////////////////////////// +particle_system forceField/beam +{ + quota 40 + material PE/lensflare + particle_width 0.5 + particle_height 2 + cull_each false + billboard_type oriented_common + common_direction 0 1 0 + + emitter Ring + { + colour 1 1 1 0 + angle 0 + direction 0 1 0 + emission_rate 20 + position 0 -2 0 + velocity_min 2 + velocity_max 5 + time_to_live 2 + duration 0 + duration_min 0 + duration_max 0 + repeat_delay 0 + repeat_delay_min 0 + repeat_delay_max 0 + width 5 + height 5 + depth 1 + inner_width 0.8 + inner_height 0.8 + } + + //affector LinearForce +// { + // force_vector 0 -1 0 + // force_application add + //} + + affector ColourFader2 + { + red1 0 + green1 0 + blue1 0 + alpha1 1 + red2 0 + green2 0 + blue2 0 + alpha2 -1 + state_change 1 + } +} diff --git a/Client2014/content/particles/forceFieldRadial.particle b/Client2014/content/particles/forceFieldRadial.particle new file mode 100644 index 0000000..807f93d --- /dev/null +++ b/Client2014/content/particles/forceFieldRadial.particle @@ -0,0 +1,51 @@ +///////////////////////////////////////////////////////// +// Radial +///////////////////////////////////////////////////////// +particle_system forceField/radial +{ + material PE/lensflare + particle_width 5 + particle_height 5 + cull_each false + quota 6 + renderer billboard + billboard_type point + point_rendering false + accurate_facing false + sorted false + local_space false + iteration_interval 0 + nonvisible_update_timeout 0 + + // emission rate is modified by RbxParticleManager for throttling + // of particle systems + emitter Point + { + angle 18 + emission_rate 2 + time_to_live_min 2 + time_to_live_max 2 + direction 0 1 0 + velocity_min 0 + velocity_max 0.1 + } + affector Rotator + { + rotation_range_start 0 + rotation_range_end 365 + rotation_speed_range_start 4 + rotation_speed_range_end 100 + } + // modified in RbxParticleFactory for user input size + affector Scaler + { + rate 6.0 + } + affector ColourFader + { + red 0 + green 0 + blue 0 + alpha -0.5 + } +} diff --git a/Client2014/content/particles/smoke.particle b/Client2014/content/particles/smoke.particle new file mode 100644 index 0000000..79c6c16 --- /dev/null +++ b/Client2014/content/particles/smoke.particle @@ -0,0 +1,57 @@ +particle_system SmokeTemplate +{ + quota 70 + material PE/smoke + particle_width 1 + particle_height 1 + cull_each false + renderer billboard + billboard_type point + + emitter Box + { + angle 180 + colour 0.8 0.8 0.8 0.3 + colour_range_start 0.8 0.8 0.8 0.3 + colour_range_end 0.8 0.8 0.8 0.3 + direction 0 1 0 + emission_rate 6 + position 0 0 0 + velocity 0.1 + velocity_min 0 + velocity_max 0.1 + time_to_live 4 + time_to_live_min 4 + time_to_live_max 6 + duration 0 + duration_min 0 + duration_max 0 + repeat_delay 0 + repeat_delay_min 0 + repeat_delay_max 0 + width 1 + height 1 + depth 1 + } + + affector ColourFader + { + red -0.11 + green -0.11 + blue -0.11 + alpha -0.1 + } + + affector Scaler + { + rate 1.5 + } + + affector Rotator + { + rotation_speed_range_start 0 + rotation_speed_range_end 0 + rotation_range_start 0 + rotation_range_end 0 + } +} diff --git a/Client2014/content/particles/sparkles.particle b/Client2014/content/particles/sparkles.particle new file mode 100644 index 0000000..60102d0 --- /dev/null +++ b/Client2014/content/particles/sparkles.particle @@ -0,0 +1,49 @@ +///////////////////////////////////////////////////////// +// Sparkles +///////////////////////////////////////////////////////// +particle_system SparklesTemplate +{ + quota 40 + material sparkle/sparkleMatl + particle_width 0.8 + particle_height 1 + cull_each false + renderer billboard + sorted false + local_space false + iteration_interval 0 + nonvisible_update_timeout 0 + billboard_type point + billboard_origin center + billboard_rotation_type vertex + common_up_vector 0 1 0 + point_rendering false + accurate_facing false + + emitter Point + { + angle 180 + direction 0 -1 0 + emission_rate 35 + position 0 0 0 + velocity_min 4 + velocity_max 8 + duration 0.0 + time_to_live 1.1 + //repeat_delay 2.0 + } + affector Rotator + { + rotation_speed_range_end 360 + rotation_range_start 0 + rotation_range_end 360 + } + affector ColourFader + { + red 0 + green 0 + blue 0 + alpha -1 + } +} + diff --git a/Client2014/content/sky/lensflare.jpg b/Client2014/content/sky/lensflare.jpg new file mode 100644 index 0000000..c53f2ec Binary files /dev/null and b/Client2014/content/sky/lensflare.jpg differ diff --git a/Client2014/content/sky/moon-alpha.jpg b/Client2014/content/sky/moon-alpha.jpg new file mode 100644 index 0000000..5e193d5 Binary files /dev/null and b/Client2014/content/sky/moon-alpha.jpg differ diff --git a/Client2014/content/sky/moon.jpg b/Client2014/content/sky/moon.jpg new file mode 100644 index 0000000..247b6cd Binary files /dev/null and b/Client2014/content/sky/moon.jpg differ diff --git a/Client2014/content/sky/null_plainsky512_bk.jpg b/Client2014/content/sky/null_plainsky512_bk.jpg new file mode 100644 index 0000000..a01c9ef Binary files /dev/null and b/Client2014/content/sky/null_plainsky512_bk.jpg differ diff --git a/Client2014/content/sky/null_plainsky512_dn.jpg b/Client2014/content/sky/null_plainsky512_dn.jpg new file mode 100644 index 0000000..75f16c1 Binary files /dev/null and b/Client2014/content/sky/null_plainsky512_dn.jpg differ diff --git a/Client2014/content/sky/null_plainsky512_ft.jpg b/Client2014/content/sky/null_plainsky512_ft.jpg new file mode 100644 index 0000000..253cb31 Binary files /dev/null and b/Client2014/content/sky/null_plainsky512_ft.jpg differ diff --git a/Client2014/content/sky/null_plainsky512_lf.jpg b/Client2014/content/sky/null_plainsky512_lf.jpg new file mode 100644 index 0000000..aa3626b Binary files /dev/null and b/Client2014/content/sky/null_plainsky512_lf.jpg differ diff --git a/Client2014/content/sky/null_plainsky512_rt.jpg b/Client2014/content/sky/null_plainsky512_rt.jpg new file mode 100644 index 0000000..45b5071 Binary files /dev/null and b/Client2014/content/sky/null_plainsky512_rt.jpg differ diff --git a/Client2014/content/sky/null_plainsky512_up.jpg b/Client2014/content/sky/null_plainsky512_up.jpg new file mode 100644 index 0000000..49940da Binary files /dev/null and b/Client2014/content/sky/null_plainsky512_up.jpg differ diff --git a/Client2014/content/sky/skyspheremap.jpg b/Client2014/content/sky/skyspheremap.jpg new file mode 100644 index 0000000..947a7ee Binary files /dev/null and b/Client2014/content/sky/skyspheremap.jpg differ diff --git a/Client2014/content/sky/sun-rays.jpg b/Client2014/content/sky/sun-rays.jpg new file mode 100644 index 0000000..a76a4d5 Binary files /dev/null and b/Client2014/content/sky/sun-rays.jpg differ diff --git a/Client2014/content/sky/sun.jpg b/Client2014/content/sky/sun.jpg new file mode 100644 index 0000000..12b3829 Binary files /dev/null and b/Client2014/content/sky/sun.jpg differ diff --git a/Client2014/content/sounds/Kerplunk.mp3 b/Client2014/content/sounds/Kerplunk.mp3 new file mode 100644 index 0000000..1211b02 Binary files /dev/null and b/Client2014/content/sounds/Kerplunk.mp3 differ diff --git a/Client2014/content/sounds/Kid saying Ouch.mp3 b/Client2014/content/sounds/Kid saying Ouch.mp3 new file mode 100644 index 0000000..b3f87e0 Binary files /dev/null and b/Client2014/content/sounds/Kid saying Ouch.mp3 differ diff --git a/Client2014/content/sounds/Rubber band sling shot.mp3 b/Client2014/content/sounds/Rubber band sling shot.mp3 new file mode 100644 index 0000000..430f96c Binary files /dev/null and b/Client2014/content/sounds/Rubber band sling shot.mp3 differ diff --git a/Client2014/content/sounds/Rubber band.mp3 b/Client2014/content/sounds/Rubber band.mp3 new file mode 100644 index 0000000..f815a17 Binary files /dev/null and b/Client2014/content/sounds/Rubber band.mp3 differ diff --git a/Client2014/content/sounds/SWITCH3.mp3 b/Client2014/content/sounds/SWITCH3.mp3 new file mode 100644 index 0000000..0132c84 Binary files /dev/null and b/Client2014/content/sounds/SWITCH3.mp3 differ diff --git a/Client2014/content/sounds/bass.mp3 b/Client2014/content/sounds/bass.mp3 new file mode 100644 index 0000000..ffd6286 Binary files /dev/null and b/Client2014/content/sounds/bass.mp3 differ diff --git a/Client2014/content/sounds/bfsl-minifigfoots1.mp3 b/Client2014/content/sounds/bfsl-minifigfoots1.mp3 new file mode 100644 index 0000000..ca2e697 Binary files /dev/null and b/Client2014/content/sounds/bfsl-minifigfoots1.mp3 differ diff --git a/Client2014/content/sounds/bfsl-minifigfoots2.mp3 b/Client2014/content/sounds/bfsl-minifigfoots2.mp3 new file mode 100644 index 0000000..113ad8f Binary files /dev/null and b/Client2014/content/sounds/bfsl-minifigfoots2.mp3 differ diff --git a/Client2014/content/sounds/button.mp3 b/Client2014/content/sounds/button.mp3 new file mode 100644 index 0000000..1136b2b Binary files /dev/null and b/Client2014/content/sounds/button.mp3 differ diff --git a/Client2014/content/sounds/clickfast.mp3 b/Client2014/content/sounds/clickfast.mp3 new file mode 100644 index 0000000..12bfbff Binary files /dev/null and b/Client2014/content/sounds/clickfast.mp3 differ diff --git a/Client2014/content/sounds/collide.mp3 b/Client2014/content/sounds/collide.mp3 new file mode 100644 index 0000000..8f69cf0 Binary files /dev/null and b/Client2014/content/sounds/collide.mp3 differ diff --git a/Client2014/content/sounds/electronicpingshort.mp3 b/Client2014/content/sounds/electronicpingshort.mp3 new file mode 100644 index 0000000..bfb3eb0 Binary files /dev/null and b/Client2014/content/sounds/electronicpingshort.mp3 differ diff --git a/Client2014/content/sounds/flashbulb.mp3 b/Client2014/content/sounds/flashbulb.mp3 new file mode 100644 index 0000000..fdcf7f6 Binary files /dev/null and b/Client2014/content/sounds/flashbulb.mp3 differ diff --git a/Client2014/content/sounds/grass.mp3 b/Client2014/content/sounds/grass.mp3 new file mode 100644 index 0000000..e79fb5b Binary files /dev/null and b/Client2014/content/sounds/grass.mp3 differ diff --git a/Client2014/content/sounds/grass2.mp3 b/Client2014/content/sounds/grass2.mp3 new file mode 100644 index 0000000..1a1ad7a Binary files /dev/null and b/Client2014/content/sounds/grass2.mp3 differ diff --git a/Client2014/content/sounds/grass3.mp3 b/Client2014/content/sounds/grass3.mp3 new file mode 100644 index 0000000..131602a Binary files /dev/null and b/Client2014/content/sounds/grass3.mp3 differ diff --git a/Client2014/content/sounds/grassstone.mp3 b/Client2014/content/sounds/grassstone.mp3 new file mode 100644 index 0000000..4e9397c Binary files /dev/null and b/Client2014/content/sounds/grassstone.mp3 differ diff --git a/Client2014/content/sounds/grassstone2.mp3 b/Client2014/content/sounds/grassstone2.mp3 new file mode 100644 index 0000000..0a5f1c1 Binary files /dev/null and b/Client2014/content/sounds/grassstone2.mp3 differ diff --git a/Client2014/content/sounds/grassstone3.mp3 b/Client2014/content/sounds/grassstone3.mp3 new file mode 100644 index 0000000..51df635 Binary files /dev/null and b/Client2014/content/sounds/grassstone3.mp3 differ diff --git a/Client2014/content/sounds/hit.mp3 b/Client2014/content/sounds/hit.mp3 new file mode 100644 index 0000000..a299c1a Binary files /dev/null and b/Client2014/content/sounds/hit.mp3 differ diff --git a/Client2014/content/sounds/ice.mp3 b/Client2014/content/sounds/ice.mp3 new file mode 100644 index 0000000..db528cc Binary files /dev/null and b/Client2014/content/sounds/ice.mp3 differ diff --git a/Client2014/content/sounds/ice2.mp3 b/Client2014/content/sounds/ice2.mp3 new file mode 100644 index 0000000..dc4cb7b Binary files /dev/null and b/Client2014/content/sounds/ice2.mp3 differ diff --git a/Client2014/content/sounds/ice3.mp3 b/Client2014/content/sounds/ice3.mp3 new file mode 100644 index 0000000..7f3e5b1 Binary files /dev/null and b/Client2014/content/sounds/ice3.mp3 differ diff --git a/Client2014/content/sounds/icegrass.mp3 b/Client2014/content/sounds/icegrass.mp3 new file mode 100644 index 0000000..2561aff Binary files /dev/null and b/Client2014/content/sounds/icegrass.mp3 differ diff --git a/Client2014/content/sounds/icegrass2.mp3 b/Client2014/content/sounds/icegrass2.mp3 new file mode 100644 index 0000000..db6f6d2 Binary files /dev/null and b/Client2014/content/sounds/icegrass2.mp3 differ diff --git a/Client2014/content/sounds/icegrass3.mp3 b/Client2014/content/sounds/icegrass3.mp3 new file mode 100644 index 0000000..b72c2db Binary files /dev/null and b/Client2014/content/sounds/icegrass3.mp3 differ diff --git a/Client2014/content/sounds/icemetal.mp3 b/Client2014/content/sounds/icemetal.mp3 new file mode 100644 index 0000000..5c9d2d4 Binary files /dev/null and b/Client2014/content/sounds/icemetal.mp3 differ diff --git a/Client2014/content/sounds/icemetal2.mp3 b/Client2014/content/sounds/icemetal2.mp3 new file mode 100644 index 0000000..60f9e4d Binary files /dev/null and b/Client2014/content/sounds/icemetal2.mp3 differ diff --git a/Client2014/content/sounds/icemetal3.mp3 b/Client2014/content/sounds/icemetal3.mp3 new file mode 100644 index 0000000..2ee4312 Binary files /dev/null and b/Client2014/content/sounds/icemetal3.mp3 differ diff --git a/Client2014/content/sounds/icestone.mp3 b/Client2014/content/sounds/icestone.mp3 new file mode 100644 index 0000000..37b99bb Binary files /dev/null and b/Client2014/content/sounds/icestone.mp3 differ diff --git a/Client2014/content/sounds/icestone2.mp3 b/Client2014/content/sounds/icestone2.mp3 new file mode 100644 index 0000000..1f5e264 Binary files /dev/null and b/Client2014/content/sounds/icestone2.mp3 differ diff --git a/Client2014/content/sounds/icestone3.mp3 b/Client2014/content/sounds/icestone3.mp3 new file mode 100644 index 0000000..e51ce3d Binary files /dev/null and b/Client2014/content/sounds/icestone3.mp3 differ diff --git a/Client2014/content/sounds/metal.mp3 b/Client2014/content/sounds/metal.mp3 new file mode 100644 index 0000000..32473b1 Binary files /dev/null and b/Client2014/content/sounds/metal.mp3 differ diff --git a/Client2014/content/sounds/metal2.mp3 b/Client2014/content/sounds/metal2.mp3 new file mode 100644 index 0000000..023273f Binary files /dev/null and b/Client2014/content/sounds/metal2.mp3 differ diff --git a/Client2014/content/sounds/metal3.mp3 b/Client2014/content/sounds/metal3.mp3 new file mode 100644 index 0000000..e407be0 Binary files /dev/null and b/Client2014/content/sounds/metal3.mp3 differ diff --git a/Client2014/content/sounds/metalgrass.mp3 b/Client2014/content/sounds/metalgrass.mp3 new file mode 100644 index 0000000..46fdef4 Binary files /dev/null and b/Client2014/content/sounds/metalgrass.mp3 differ diff --git a/Client2014/content/sounds/metalgrass2.mp3 b/Client2014/content/sounds/metalgrass2.mp3 new file mode 100644 index 0000000..3a7dad2 Binary files /dev/null and b/Client2014/content/sounds/metalgrass2.mp3 differ diff --git a/Client2014/content/sounds/metalgrass3.mp3 b/Client2014/content/sounds/metalgrass3.mp3 new file mode 100644 index 0000000..e8172ad Binary files /dev/null and b/Client2014/content/sounds/metalgrass3.mp3 differ diff --git a/Client2014/content/sounds/metalstone.mp3 b/Client2014/content/sounds/metalstone.mp3 new file mode 100644 index 0000000..6910e46 Binary files /dev/null and b/Client2014/content/sounds/metalstone.mp3 differ diff --git a/Client2014/content/sounds/metalstone2.mp3 b/Client2014/content/sounds/metalstone2.mp3 new file mode 100644 index 0000000..0dd924e Binary files /dev/null and b/Client2014/content/sounds/metalstone2.mp3 differ diff --git a/Client2014/content/sounds/metalstone3.mp3 b/Client2014/content/sounds/metalstone3.mp3 new file mode 100644 index 0000000..37e1198 Binary files /dev/null and b/Client2014/content/sounds/metalstone3.mp3 differ diff --git a/Client2014/content/sounds/plasticgrass.mp3 b/Client2014/content/sounds/plasticgrass.mp3 new file mode 100644 index 0000000..579d4ab Binary files /dev/null and b/Client2014/content/sounds/plasticgrass.mp3 differ diff --git a/Client2014/content/sounds/plasticgrass2.mp3 b/Client2014/content/sounds/plasticgrass2.mp3 new file mode 100644 index 0000000..f77843a Binary files /dev/null and b/Client2014/content/sounds/plasticgrass2.mp3 differ diff --git a/Client2014/content/sounds/plasticgrass3.mp3 b/Client2014/content/sounds/plasticgrass3.mp3 new file mode 100644 index 0000000..9274ff0 Binary files /dev/null and b/Client2014/content/sounds/plasticgrass3.mp3 differ diff --git a/Client2014/content/sounds/plasticice.mp3 b/Client2014/content/sounds/plasticice.mp3 new file mode 100644 index 0000000..668ba91 Binary files /dev/null and b/Client2014/content/sounds/plasticice.mp3 differ diff --git a/Client2014/content/sounds/plasticice2.mp3 b/Client2014/content/sounds/plasticice2.mp3 new file mode 100644 index 0000000..041e4ca Binary files /dev/null and b/Client2014/content/sounds/plasticice2.mp3 differ diff --git a/Client2014/content/sounds/plasticice3.mp3 b/Client2014/content/sounds/plasticice3.mp3 new file mode 100644 index 0000000..180b476 Binary files /dev/null and b/Client2014/content/sounds/plasticice3.mp3 differ diff --git a/Client2014/content/sounds/plasticmetal.mp3 b/Client2014/content/sounds/plasticmetal.mp3 new file mode 100644 index 0000000..1eadf0c Binary files /dev/null and b/Client2014/content/sounds/plasticmetal.mp3 differ diff --git a/Client2014/content/sounds/plasticmetal2.mp3 b/Client2014/content/sounds/plasticmetal2.mp3 new file mode 100644 index 0000000..6d4119a Binary files /dev/null and b/Client2014/content/sounds/plasticmetal2.mp3 differ diff --git a/Client2014/content/sounds/plasticmetal3.mp3 b/Client2014/content/sounds/plasticmetal3.mp3 new file mode 100644 index 0000000..c777ee4 Binary files /dev/null and b/Client2014/content/sounds/plasticmetal3.mp3 differ diff --git a/Client2014/content/sounds/plasticplastic.mp3 b/Client2014/content/sounds/plasticplastic.mp3 new file mode 100644 index 0000000..7283013 Binary files /dev/null and b/Client2014/content/sounds/plasticplastic.mp3 differ diff --git a/Client2014/content/sounds/plasticplastic2.mp3 b/Client2014/content/sounds/plasticplastic2.mp3 new file mode 100644 index 0000000..f9710c7 Binary files /dev/null and b/Client2014/content/sounds/plasticplastic2.mp3 differ diff --git a/Client2014/content/sounds/plasticplastic3.mp3 b/Client2014/content/sounds/plasticplastic3.mp3 new file mode 100644 index 0000000..c7cd1ca Binary files /dev/null and b/Client2014/content/sounds/plasticplastic3.mp3 differ diff --git a/Client2014/content/sounds/plasticstone.mp3 b/Client2014/content/sounds/plasticstone.mp3 new file mode 100644 index 0000000..d33e553 Binary files /dev/null and b/Client2014/content/sounds/plasticstone.mp3 differ diff --git a/Client2014/content/sounds/plasticstone2.mp3 b/Client2014/content/sounds/plasticstone2.mp3 new file mode 100644 index 0000000..cd33d00 Binary files /dev/null and b/Client2014/content/sounds/plasticstone2.mp3 differ diff --git a/Client2014/content/sounds/plasticstone3.mp3 b/Client2014/content/sounds/plasticstone3.mp3 new file mode 100644 index 0000000..5732f38 Binary files /dev/null and b/Client2014/content/sounds/plasticstone3.mp3 differ diff --git a/Client2014/content/sounds/snap.mp3 b/Client2014/content/sounds/snap.mp3 new file mode 100644 index 0000000..8dd8d5a Binary files /dev/null and b/Client2014/content/sounds/snap.mp3 differ diff --git a/Client2014/content/sounds/splat.mp3 b/Client2014/content/sounds/splat.mp3 new file mode 100644 index 0000000..934a634 Binary files /dev/null and b/Client2014/content/sounds/splat.mp3 differ diff --git a/Client2014/content/sounds/stone.mp3 b/Client2014/content/sounds/stone.mp3 new file mode 100644 index 0000000..e9dd548 Binary files /dev/null and b/Client2014/content/sounds/stone.mp3 differ diff --git a/Client2014/content/sounds/stone2.mp3 b/Client2014/content/sounds/stone2.mp3 new file mode 100644 index 0000000..f582a22 Binary files /dev/null and b/Client2014/content/sounds/stone2.mp3 differ diff --git a/Client2014/content/sounds/stone3.mp3 b/Client2014/content/sounds/stone3.mp3 new file mode 100644 index 0000000..b8bfa31 Binary files /dev/null and b/Client2014/content/sounds/stone3.mp3 differ diff --git a/Client2014/content/sounds/switch.mp3 b/Client2014/content/sounds/switch.mp3 new file mode 100644 index 0000000..69e9442 Binary files /dev/null and b/Client2014/content/sounds/switch.mp3 differ diff --git a/Client2014/content/sounds/swoosh.mp3 b/Client2014/content/sounds/swoosh.mp3 new file mode 100644 index 0000000..99014d0 Binary files /dev/null and b/Client2014/content/sounds/swoosh.mp3 differ diff --git a/Client2014/content/sounds/swordlunge.mp3 b/Client2014/content/sounds/swordlunge.mp3 new file mode 100644 index 0000000..c5b017a Binary files /dev/null and b/Client2014/content/sounds/swordlunge.mp3 differ diff --git a/Client2014/content/sounds/swordslash.mp3 b/Client2014/content/sounds/swordslash.mp3 new file mode 100644 index 0000000..cf5b902 Binary files /dev/null and b/Client2014/content/sounds/swordslash.mp3 differ diff --git a/Client2014/content/sounds/unsheath.mp3 b/Client2014/content/sounds/unsheath.mp3 new file mode 100644 index 0000000..6913873 Binary files /dev/null and b/Client2014/content/sounds/unsheath.mp3 differ diff --git a/Client2014/content/sounds/uuhhh.mp3 b/Client2014/content/sounds/uuhhh.mp3 new file mode 100644 index 0000000..81b3565 Binary files /dev/null and b/Client2014/content/sounds/uuhhh.mp3 differ diff --git a/Client2014/content/sounds/victory.mp3 b/Client2014/content/sounds/victory.mp3 new file mode 100644 index 0000000..793b1ba Binary files /dev/null and b/Client2014/content/sounds/victory.mp3 differ diff --git a/Client2014/content/sounds/woodgrass.mp3 b/Client2014/content/sounds/woodgrass.mp3 new file mode 100644 index 0000000..f70237c Binary files /dev/null and b/Client2014/content/sounds/woodgrass.mp3 differ diff --git a/Client2014/content/sounds/woodgrass2.mp3 b/Client2014/content/sounds/woodgrass2.mp3 new file mode 100644 index 0000000..0d12b62 Binary files /dev/null and b/Client2014/content/sounds/woodgrass2.mp3 differ diff --git a/Client2014/content/sounds/woodgrass3.mp3 b/Client2014/content/sounds/woodgrass3.mp3 new file mode 100644 index 0000000..f234835 Binary files /dev/null and b/Client2014/content/sounds/woodgrass3.mp3 differ diff --git a/Client2014/content/sounds/woodice.mp3 b/Client2014/content/sounds/woodice.mp3 new file mode 100644 index 0000000..5604dcf Binary files /dev/null and b/Client2014/content/sounds/woodice.mp3 differ diff --git a/Client2014/content/sounds/woodice2.mp3 b/Client2014/content/sounds/woodice2.mp3 new file mode 100644 index 0000000..dc3a586 Binary files /dev/null and b/Client2014/content/sounds/woodice2.mp3 differ diff --git a/Client2014/content/sounds/woodice3.mp3 b/Client2014/content/sounds/woodice3.mp3 new file mode 100644 index 0000000..300dea9 Binary files /dev/null and b/Client2014/content/sounds/woodice3.mp3 differ diff --git a/Client2014/content/sounds/woodmetal.mp3 b/Client2014/content/sounds/woodmetal.mp3 new file mode 100644 index 0000000..666db63 Binary files /dev/null and b/Client2014/content/sounds/woodmetal.mp3 differ diff --git a/Client2014/content/sounds/woodmetal2.mp3 b/Client2014/content/sounds/woodmetal2.mp3 new file mode 100644 index 0000000..60e87dc Binary files /dev/null and b/Client2014/content/sounds/woodmetal2.mp3 differ diff --git a/Client2014/content/sounds/woodmetal3.mp3 b/Client2014/content/sounds/woodmetal3.mp3 new file mode 100644 index 0000000..29ec64a Binary files /dev/null and b/Client2014/content/sounds/woodmetal3.mp3 differ diff --git a/Client2014/content/sounds/woodplastic.mp3 b/Client2014/content/sounds/woodplastic.mp3 new file mode 100644 index 0000000..d909b8a Binary files /dev/null and b/Client2014/content/sounds/woodplastic.mp3 differ diff --git a/Client2014/content/sounds/woodplastic2.mp3 b/Client2014/content/sounds/woodplastic2.mp3 new file mode 100644 index 0000000..c4e9b19 Binary files /dev/null and b/Client2014/content/sounds/woodplastic2.mp3 differ diff --git a/Client2014/content/sounds/woodplastic3.mp3 b/Client2014/content/sounds/woodplastic3.mp3 new file mode 100644 index 0000000..4398179 Binary files /dev/null and b/Client2014/content/sounds/woodplastic3.mp3 differ diff --git a/Client2014/content/sounds/woodstone.mp3 b/Client2014/content/sounds/woodstone.mp3 new file mode 100644 index 0000000..3f7c071 Binary files /dev/null and b/Client2014/content/sounds/woodstone.mp3 differ diff --git a/Client2014/content/sounds/woodstone2.mp3 b/Client2014/content/sounds/woodstone2.mp3 new file mode 100644 index 0000000..1b4505a Binary files /dev/null and b/Client2014/content/sounds/woodstone2.mp3 differ diff --git a/Client2014/content/sounds/woodstone3.mp3 b/Client2014/content/sounds/woodstone3.mp3 new file mode 100644 index 0000000..6e04dec Binary files /dev/null and b/Client2014/content/sounds/woodstone3.mp3 differ diff --git a/Client2014/content/sounds/woodwood.mp3 b/Client2014/content/sounds/woodwood.mp3 new file mode 100644 index 0000000..8279e0c Binary files /dev/null and b/Client2014/content/sounds/woodwood.mp3 differ diff --git a/Client2014/content/sounds/woodwood2.mp3 b/Client2014/content/sounds/woodwood2.mp3 new file mode 100644 index 0000000..ad29d7f Binary files /dev/null and b/Client2014/content/sounds/woodwood2.mp3 differ diff --git a/Client2014/content/sounds/woodwood3.mp3 b/Client2014/content/sounds/woodwood3.mp3 new file mode 100644 index 0000000..e8bf04f Binary files /dev/null and b/Client2014/content/sounds/woodwood3.mp3 differ diff --git a/Client2014/content/textures/AnchorCursor.png b/Client2014/content/textures/AnchorCursor.png new file mode 100644 index 0000000..5c4cac7 Binary files /dev/null and b/Client2014/content/textures/AnchorCursor.png differ diff --git a/Client2014/content/textures/ArrowCursor.png b/Client2014/content/textures/ArrowCursor.png new file mode 100644 index 0000000..0f839d0 Binary files /dev/null and b/Client2014/content/textures/ArrowCursor.png differ diff --git a/Client2014/content/textures/ArrowCursorDecalDrag.png b/Client2014/content/textures/ArrowCursorDecalDrag.png new file mode 100644 index 0000000..0f839d0 Binary files /dev/null and b/Client2014/content/textures/ArrowCursorDecalDrag.png differ diff --git a/Client2014/content/textures/ArrowFarCursor.png b/Client2014/content/textures/ArrowFarCursor.png new file mode 100644 index 0000000..c3ce391 Binary files /dev/null and b/Client2014/content/textures/ArrowFarCursor.png differ diff --git a/Client2014/content/textures/BWGradient.png b/Client2014/content/textures/BWGradient.png new file mode 100644 index 0000000..0a5edb5 Binary files /dev/null and b/Client2014/content/textures/BWGradient.png differ diff --git a/Client2014/content/textures/BackgroundImage.png b/Client2014/content/textures/BackgroundImage.png new file mode 100644 index 0000000..9f76324 Binary files /dev/null and b/Client2014/content/textures/BackgroundImage.png differ diff --git a/Client2014/content/textures/Blank.png b/Client2014/content/textures/Blank.png new file mode 100644 index 0000000..d8860f6 Binary files /dev/null and b/Client2014/content/textures/Blank.png differ diff --git a/Client2014/content/textures/CameraTiltDown.png b/Client2014/content/textures/CameraTiltDown.png new file mode 100644 index 0000000..398f1f6 Binary files /dev/null and b/Client2014/content/textures/CameraTiltDown.png differ diff --git a/Client2014/content/textures/CameraTiltDown_dn.png b/Client2014/content/textures/CameraTiltDown_dn.png new file mode 100644 index 0000000..3f078ce Binary files /dev/null and b/Client2014/content/textures/CameraTiltDown_dn.png differ diff --git a/Client2014/content/textures/CameraTiltDown_ds.png b/Client2014/content/textures/CameraTiltDown_ds.png new file mode 100644 index 0000000..bb4f78c Binary files /dev/null and b/Client2014/content/textures/CameraTiltDown_ds.png differ diff --git a/Client2014/content/textures/CameraTiltDown_ovr.png b/Client2014/content/textures/CameraTiltDown_ovr.png new file mode 100644 index 0000000..3f078ce Binary files /dev/null and b/Client2014/content/textures/CameraTiltDown_ovr.png differ diff --git a/Client2014/content/textures/CameraTiltUp.png b/Client2014/content/textures/CameraTiltUp.png new file mode 100644 index 0000000..a6c1645 Binary files /dev/null and b/Client2014/content/textures/CameraTiltUp.png differ diff --git a/Client2014/content/textures/CameraTiltUp_dn.png b/Client2014/content/textures/CameraTiltUp_dn.png new file mode 100644 index 0000000..9e6a9d6 Binary files /dev/null and b/Client2014/content/textures/CameraTiltUp_dn.png differ diff --git a/Client2014/content/textures/CameraTiltUp_ds.png b/Client2014/content/textures/CameraTiltUp_ds.png new file mode 100644 index 0000000..dd22842 Binary files /dev/null and b/Client2014/content/textures/CameraTiltUp_ds.png differ diff --git a/Client2014/content/textures/CameraTiltUp_ovr.png b/Client2014/content/textures/CameraTiltUp_ovr.png new file mode 100644 index 0000000..9e6a9d6 Binary files /dev/null and b/Client2014/content/textures/CameraTiltUp_ovr.png differ diff --git a/Client2014/content/textures/CameraZoomIn.png b/Client2014/content/textures/CameraZoomIn.png new file mode 100644 index 0000000..5825340 Binary files /dev/null and b/Client2014/content/textures/CameraZoomIn.png differ diff --git a/Client2014/content/textures/CameraZoomIn_dn.png b/Client2014/content/textures/CameraZoomIn_dn.png new file mode 100644 index 0000000..dfb4626 Binary files /dev/null and b/Client2014/content/textures/CameraZoomIn_dn.png differ diff --git a/Client2014/content/textures/CameraZoomIn_ds.png b/Client2014/content/textures/CameraZoomIn_ds.png new file mode 100644 index 0000000..eac143e Binary files /dev/null and b/Client2014/content/textures/CameraZoomIn_ds.png differ diff --git a/Client2014/content/textures/CameraZoomIn_ovr.png b/Client2014/content/textures/CameraZoomIn_ovr.png new file mode 100644 index 0000000..dfb4626 Binary files /dev/null and b/Client2014/content/textures/CameraZoomIn_ovr.png differ diff --git a/Client2014/content/textures/CameraZoomOut.png b/Client2014/content/textures/CameraZoomOut.png new file mode 100644 index 0000000..0c3e470 Binary files /dev/null and b/Client2014/content/textures/CameraZoomOut.png differ diff --git a/Client2014/content/textures/CameraZoomOut_dn.png b/Client2014/content/textures/CameraZoomOut_dn.png new file mode 100644 index 0000000..0bb2995 Binary files /dev/null and b/Client2014/content/textures/CameraZoomOut_dn.png differ diff --git a/Client2014/content/textures/CameraZoomOut_ds.png b/Client2014/content/textures/CameraZoomOut_ds.png new file mode 100644 index 0000000..ce63a1c Binary files /dev/null and b/Client2014/content/textures/CameraZoomOut_ds.png differ diff --git a/Client2014/content/textures/CameraZoomOut_ovr.png b/Client2014/content/textures/CameraZoomOut_ovr.png new file mode 100644 index 0000000..0bb2995 Binary files /dev/null and b/Client2014/content/textures/CameraZoomOut_ovr.png differ diff --git a/Client2014/content/textures/Chat.png b/Client2014/content/textures/Chat.png new file mode 100644 index 0000000..4fd0c0d Binary files /dev/null and b/Client2014/content/textures/Chat.png differ diff --git a/Client2014/content/textures/Chat_dn.png b/Client2014/content/textures/Chat_dn.png new file mode 100644 index 0000000..4e8604e Binary files /dev/null and b/Client2014/content/textures/Chat_dn.png differ diff --git a/Client2014/content/textures/Chat_ds.png b/Client2014/content/textures/Chat_ds.png new file mode 100644 index 0000000..97e075a Binary files /dev/null and b/Client2014/content/textures/Chat_ds.png differ diff --git a/Client2014/content/textures/Chat_ovr.png b/Client2014/content/textures/Chat_ovr.png new file mode 100644 index 0000000..be854e8 Binary files /dev/null and b/Client2014/content/textures/Chat_ovr.png differ diff --git a/Client2014/content/textures/Clone.png b/Client2014/content/textures/Clone.png new file mode 100644 index 0000000..31915a9 Binary files /dev/null and b/Client2014/content/textures/Clone.png differ diff --git a/Client2014/content/textures/CloneCursor.png b/Client2014/content/textures/CloneCursor.png new file mode 100644 index 0000000..afe32c0 Binary files /dev/null and b/Client2014/content/textures/CloneCursor.png differ diff --git a/Client2014/content/textures/CloneDownCursor.png b/Client2014/content/textures/CloneDownCursor.png new file mode 100644 index 0000000..b33240d Binary files /dev/null and b/Client2014/content/textures/CloneDownCursor.png differ diff --git a/Client2014/content/textures/CloneOverCursor.png b/Client2014/content/textures/CloneOverCursor.png new file mode 100644 index 0000000..5c97ee1 Binary files /dev/null and b/Client2014/content/textures/CloneOverCursor.png differ diff --git a/Client2014/content/textures/DragCursor.png b/Client2014/content/textures/DragCursor.png new file mode 100644 index 0000000..a8f1894 Binary files /dev/null and b/Client2014/content/textures/DragCursor.png differ diff --git a/Client2014/content/textures/DropperCursor.png b/Client2014/content/textures/DropperCursor.png new file mode 100644 index 0000000..d98bcb6 Binary files /dev/null and b/Client2014/content/textures/DropperCursor.png differ diff --git a/Client2014/content/textures/Exit.png b/Client2014/content/textures/Exit.png new file mode 100644 index 0000000..523d2c5 Binary files /dev/null and b/Client2014/content/textures/Exit.png differ diff --git a/Client2014/content/textures/Exit_dn.png b/Client2014/content/textures/Exit_dn.png new file mode 100644 index 0000000..b2359c5 Binary files /dev/null and b/Client2014/content/textures/Exit_dn.png differ diff --git a/Client2014/content/textures/Exit_ovr.png b/Client2014/content/textures/Exit_ovr.png new file mode 100644 index 0000000..b2359c5 Binary files /dev/null and b/Client2014/content/textures/Exit_ovr.png differ diff --git a/Client2014/content/textures/FillCursor.png b/Client2014/content/textures/FillCursor.png new file mode 100644 index 0000000..752cebc Binary files /dev/null and b/Client2014/content/textures/FillCursor.png differ diff --git a/Client2014/content/textures/FlagCursor.png b/Client2014/content/textures/FlagCursor.png new file mode 100644 index 0000000..59c3798 Binary files /dev/null and b/Client2014/content/textures/FlagCursor.png differ diff --git a/Client2014/content/textures/FlatCursor.png b/Client2014/content/textures/FlatCursor.png new file mode 100644 index 0000000..feba33a Binary files /dev/null and b/Client2014/content/textures/FlatCursor.png differ diff --git a/Client2014/content/textures/GameTool.png b/Client2014/content/textures/GameTool.png new file mode 100644 index 0000000..86bee9f Binary files /dev/null and b/Client2014/content/textures/GameTool.png differ diff --git a/Client2014/content/textures/GrabCursor.png b/Client2014/content/textures/GrabCursor.png new file mode 100644 index 0000000..d411bae Binary files /dev/null and b/Client2014/content/textures/GrabCursor.png differ diff --git a/Client2014/content/textures/GrabRotateCursor.png b/Client2014/content/textures/GrabRotateCursor.png new file mode 100644 index 0000000..0cd0c13 Binary files /dev/null and b/Client2014/content/textures/GrabRotateCursor.png differ diff --git a/Client2014/content/textures/Grass_Texture.jpg b/Client2014/content/textures/Grass_Texture.jpg new file mode 100644 index 0000000..cd273dc Binary files /dev/null and b/Client2014/content/textures/Grass_Texture.jpg differ diff --git a/Client2014/content/textures/GunCursor.png b/Client2014/content/textures/GunCursor.png new file mode 100644 index 0000000..d1b6afc Binary files /dev/null and b/Client2014/content/textures/GunCursor.png differ diff --git a/Client2014/content/textures/GunWaitCursor.png b/Client2014/content/textures/GunWaitCursor.png new file mode 100644 index 0000000..0c812be Binary files /dev/null and b/Client2014/content/textures/GunWaitCursor.png differ diff --git a/Client2014/content/textures/HammerCursor.png b/Client2014/content/textures/HammerCursor.png new file mode 100644 index 0000000..ad2b622 Binary files /dev/null and b/Client2014/content/textures/HammerCursor.png differ diff --git a/Client2014/content/textures/HammerDownCursor.png b/Client2014/content/textures/HammerDownCursor.png new file mode 100644 index 0000000..cdbcfe2 Binary files /dev/null and b/Client2014/content/textures/HammerDownCursor.png differ diff --git a/Client2014/content/textures/HammerOverCursor.png b/Client2014/content/textures/HammerOverCursor.png new file mode 100644 index 0000000..4182d69 Binary files /dev/null and b/Client2014/content/textures/HammerOverCursor.png differ diff --git a/Client2014/content/textures/HingeCursor.png b/Client2014/content/textures/HingeCursor.png new file mode 100644 index 0000000..e507cd3 Binary files /dev/null and b/Client2014/content/textures/HingeCursor.png differ diff --git a/Client2014/content/textures/LockCursor.png b/Client2014/content/textures/LockCursor.png new file mode 100644 index 0000000..4c4c6b2 Binary files /dev/null and b/Client2014/content/textures/LockCursor.png differ diff --git a/Client2014/content/textures/MaterialCursor.png b/Client2014/content/textures/MaterialCursor.png new file mode 100644 index 0000000..f43fa99 Binary files /dev/null and b/Client2014/content/textures/MaterialCursor.png differ diff --git a/Client2014/content/textures/MissingCursor.png b/Client2014/content/textures/MissingCursor.png new file mode 100644 index 0000000..4145456 Binary files /dev/null and b/Client2014/content/textures/MissingCursor.png differ diff --git a/Client2014/content/textures/MotorCursor.png b/Client2014/content/textures/MotorCursor.png new file mode 100644 index 0000000..c363ab6 Binary files /dev/null and b/Client2014/content/textures/MotorCursor.png differ diff --git a/Client2014/content/textures/MouseLockedCursor.png b/Client2014/content/textures/MouseLockedCursor.png new file mode 100644 index 0000000..c3b44db Binary files /dev/null and b/Client2014/content/textures/MouseLockedCursor.png differ diff --git a/Client2014/content/textures/ResizeCursor.png b/Client2014/content/textures/ResizeCursor.png new file mode 100644 index 0000000..e9e06f5 Binary files /dev/null and b/Client2014/content/textures/ResizeCursor.png differ diff --git a/Client2014/content/textures/RustGradient.png b/Client2014/content/textures/RustGradient.png new file mode 100644 index 0000000..0180253 Binary files /dev/null and b/Client2014/content/textures/RustGradient.png differ diff --git a/Client2014/content/textures/Smoke.png b/Client2014/content/textures/Smoke.png new file mode 100644 index 0000000..b86df5d Binary files /dev/null and b/Client2014/content/textures/Smoke.png differ diff --git a/Client2014/content/textures/SpawnCursor.png b/Client2014/content/textures/SpawnCursor.png new file mode 100644 index 0000000..37fb08e Binary files /dev/null and b/Client2014/content/textures/SpawnCursor.png differ diff --git a/Client2014/content/textures/SpawnLocation.png b/Client2014/content/textures/SpawnLocation.png new file mode 100644 index 0000000..049c2d3 Binary files /dev/null and b/Client2014/content/textures/SpawnLocation.png differ diff --git a/Client2014/content/textures/SurfacesDefault.png b/Client2014/content/textures/SurfacesDefault.png new file mode 100644 index 0000000..53b1505 Binary files /dev/null and b/Client2014/content/textures/SurfacesDefault.png differ diff --git a/Client2014/content/textures/UnAnchorCursor.png b/Client2014/content/textures/UnAnchorCursor.png new file mode 100644 index 0000000..fa4e9cb Binary files /dev/null and b/Client2014/content/textures/UnAnchorCursor.png differ diff --git a/Client2014/content/textures/UnlockCursor.png b/Client2014/content/textures/UnlockCursor.png new file mode 100644 index 0000000..052a07a Binary files /dev/null and b/Client2014/content/textures/UnlockCursor.png differ diff --git a/Client2014/content/textures/WeldCursor.png b/Client2014/content/textures/WeldCursor.png new file mode 100644 index 0000000..c2a4292 Binary files /dev/null and b/Client2014/content/textures/WeldCursor.png differ diff --git a/Client2014/content/textures/advClosed-hand-no-weld.png b/Client2014/content/textures/advClosed-hand-no-weld.png new file mode 100644 index 0000000..beb2fd4 Binary files /dev/null and b/Client2014/content/textures/advClosed-hand-no-weld.png differ diff --git a/Client2014/content/textures/advClosed-hand-weld.png b/Client2014/content/textures/advClosed-hand-weld.png new file mode 100644 index 0000000..b79d71b Binary files /dev/null and b/Client2014/content/textures/advClosed-hand-weld.png differ diff --git a/Client2014/content/textures/advClosed-hand.png b/Client2014/content/textures/advClosed-hand.png new file mode 100644 index 0000000..38fcea4 Binary files /dev/null and b/Client2014/content/textures/advClosed-hand.png differ diff --git a/Client2014/content/textures/advCursor-default.png b/Client2014/content/textures/advCursor-default.png new file mode 100644 index 0000000..58c115b Binary files /dev/null and b/Client2014/content/textures/advCursor-default.png differ diff --git a/Client2014/content/textures/advCursor-openedHand.png b/Client2014/content/textures/advCursor-openedHand.png new file mode 100644 index 0000000..c972e29 Binary files /dev/null and b/Client2014/content/textures/advCursor-openedHand.png differ diff --git a/Client2014/content/textures/advCursor-white.png b/Client2014/content/textures/advCursor-white.png new file mode 100644 index 0000000..8a68e4f Binary files /dev/null and b/Client2014/content/textures/advCursor-white.png differ diff --git a/Client2014/content/textures/advancedMove.png b/Client2014/content/textures/advancedMove.png new file mode 100644 index 0000000..da15757 Binary files /dev/null and b/Client2014/content/textures/advancedMove.png differ diff --git a/Client2014/content/textures/advancedMoveResize.png b/Client2014/content/textures/advancedMoveResize.png new file mode 100644 index 0000000..8b1264a Binary files /dev/null and b/Client2014/content/textures/advancedMoveResize.png differ diff --git a/Client2014/content/textures/advancedMove_joint.png b/Client2014/content/textures/advancedMove_joint.png new file mode 100644 index 0000000..17079e0 Binary files /dev/null and b/Client2014/content/textures/advancedMove_joint.png differ diff --git a/Client2014/content/textures/advancedMove_keysOnly.png b/Client2014/content/textures/advancedMove_keysOnly.png new file mode 100644 index 0000000..36eb233 Binary files /dev/null and b/Client2014/content/textures/advancedMove_keysOnly.png differ diff --git a/Client2014/content/textures/advancedMove_noJoint.png b/Client2014/content/textures/advancedMove_noJoint.png new file mode 100644 index 0000000..62752c4 Binary files /dev/null and b/Client2014/content/textures/advancedMove_noJoint.png differ diff --git a/Client2014/content/textures/blackBkg_round.png b/Client2014/content/textures/blackBkg_round.png new file mode 100644 index 0000000..7070a7c Binary files /dev/null and b/Client2014/content/textures/blackBkg_round.png differ diff --git a/Client2014/content/textures/blackBkg_square.png b/Client2014/content/textures/blackBkg_square.png new file mode 100644 index 0000000..dc97acf Binary files /dev/null and b/Client2014/content/textures/blackBkg_square.png differ diff --git a/Client2014/content/textures/chatBubble_botBlue_bkg.png b/Client2014/content/textures/chatBubble_botBlue_bkg.png new file mode 100644 index 0000000..9f0b008 Binary files /dev/null and b/Client2014/content/textures/chatBubble_botBlue_bkg.png differ diff --git a/Client2014/content/textures/chatBubble_botBlue_notify_bkg.png b/Client2014/content/textures/chatBubble_botBlue_notify_bkg.png new file mode 100644 index 0000000..0b03982 Binary files /dev/null and b/Client2014/content/textures/chatBubble_botBlue_notify_bkg.png differ diff --git a/Client2014/content/textures/chatBubble_botBlue_tail.png b/Client2014/content/textures/chatBubble_botBlue_tail.png new file mode 100644 index 0000000..316bc9d Binary files /dev/null and b/Client2014/content/textures/chatBubble_botBlue_tail.png differ diff --git a/Client2014/content/textures/chatBubble_botBlue_tailRight.png b/Client2014/content/textures/chatBubble_botBlue_tailRight.png new file mode 100644 index 0000000..da047a8 Binary files /dev/null and b/Client2014/content/textures/chatBubble_botBlue_tailRight.png differ diff --git a/Client2014/content/textures/chatBubble_botGreen_bkg.png b/Client2014/content/textures/chatBubble_botGreen_bkg.png new file mode 100644 index 0000000..e158c85 Binary files /dev/null and b/Client2014/content/textures/chatBubble_botGreen_bkg.png differ diff --git a/Client2014/content/textures/chatBubble_botGreen_notify_bkg.png b/Client2014/content/textures/chatBubble_botGreen_notify_bkg.png new file mode 100644 index 0000000..d0a0c47 Binary files /dev/null and b/Client2014/content/textures/chatBubble_botGreen_notify_bkg.png differ diff --git a/Client2014/content/textures/chatBubble_botGreen_tail.png b/Client2014/content/textures/chatBubble_botGreen_tail.png new file mode 100644 index 0000000..01e6a00 Binary files /dev/null and b/Client2014/content/textures/chatBubble_botGreen_tail.png differ diff --git a/Client2014/content/textures/chatBubble_botGreen_tailRight.png b/Client2014/content/textures/chatBubble_botGreen_tailRight.png new file mode 100644 index 0000000..8cd6293 Binary files /dev/null and b/Client2014/content/textures/chatBubble_botGreen_tailRight.png differ diff --git a/Client2014/content/textures/chatBubble_botRed_bkg.png b/Client2014/content/textures/chatBubble_botRed_bkg.png new file mode 100644 index 0000000..66e411f Binary files /dev/null and b/Client2014/content/textures/chatBubble_botRed_bkg.png differ diff --git a/Client2014/content/textures/chatBubble_botRed_notify_bkg.png b/Client2014/content/textures/chatBubble_botRed_notify_bkg.png new file mode 100644 index 0000000..e7376eb Binary files /dev/null and b/Client2014/content/textures/chatBubble_botRed_notify_bkg.png differ diff --git a/Client2014/content/textures/chatBubble_botRed_tail.png b/Client2014/content/textures/chatBubble_botRed_tail.png new file mode 100644 index 0000000..b29c2af Binary files /dev/null and b/Client2014/content/textures/chatBubble_botRed_tail.png differ diff --git a/Client2014/content/textures/chatBubble_botRed_tailRight.png b/Client2014/content/textures/chatBubble_botRed_tailRight.png new file mode 100644 index 0000000..1e39788 Binary files /dev/null and b/Client2014/content/textures/chatBubble_botRed_tailRight.png differ diff --git a/Client2014/content/textures/chatBubble_bot_notifyGray_dotDotDot.png b/Client2014/content/textures/chatBubble_bot_notifyGray_dotDotDot.png new file mode 100644 index 0000000..f5b017d Binary files /dev/null and b/Client2014/content/textures/chatBubble_bot_notifyGray_dotDotDot.png differ diff --git a/Client2014/content/textures/chatBubble_bot_notify_bang.png b/Client2014/content/textures/chatBubble_bot_notify_bang.png new file mode 100644 index 0000000..1ccef6c Binary files /dev/null and b/Client2014/content/textures/chatBubble_bot_notify_bang.png differ diff --git a/Client2014/content/textures/chatBubble_bot_notify_dotDotDot.png b/Client2014/content/textures/chatBubble_bot_notify_dotDotDot.png new file mode 100644 index 0000000..4318458 Binary files /dev/null and b/Client2014/content/textures/chatBubble_bot_notify_dotDotDot.png differ diff --git a/Client2014/content/textures/chatBubble_bot_notify_money.png b/Client2014/content/textures/chatBubble_bot_notify_money.png new file mode 100644 index 0000000..54f58c5 Binary files /dev/null and b/Client2014/content/textures/chatBubble_bot_notify_money.png differ diff --git a/Client2014/content/textures/chatBubble_bot_notify_question.png b/Client2014/content/textures/chatBubble_bot_notify_question.png new file mode 100644 index 0000000..0a91c4f Binary files /dev/null and b/Client2014/content/textures/chatBubble_bot_notify_question.png differ diff --git a/Client2014/content/textures/chatBubble_white_bkg.png b/Client2014/content/textures/chatBubble_white_bkg.png new file mode 100644 index 0000000..7e6648c Binary files /dev/null and b/Client2014/content/textures/chatBubble_white_bkg.png differ diff --git a/Client2014/content/textures/chatBubble_white_notify_bkg.png b/Client2014/content/textures/chatBubble_white_notify_bkg.png new file mode 100644 index 0000000..828e362 Binary files /dev/null and b/Client2014/content/textures/chatBubble_white_notify_bkg.png differ diff --git a/Client2014/content/textures/chatBubble_white_tail.png b/Client2014/content/textures/chatBubble_white_tail.png new file mode 100644 index 0000000..77d1bd4 Binary files /dev/null and b/Client2014/content/textures/chatBubble_white_tail.png differ diff --git a/Client2014/content/textures/dirt.jpg b/Client2014/content/textures/dirt.jpg new file mode 100644 index 0000000..05f5c94 Binary files /dev/null and b/Client2014/content/textures/dirt.jpg differ diff --git a/Client2014/content/textures/explosion.png b/Client2014/content/textures/explosion.png new file mode 100644 index 0000000..5124cb2 Binary files /dev/null and b/Client2014/content/textures/explosion.png differ diff --git a/Client2014/content/textures/face.png b/Client2014/content/textures/face.png new file mode 100644 index 0000000..5555669 Binary files /dev/null and b/Client2014/content/textures/face.png differ diff --git a/Client2014/content/textures/fire_0.png b/Client2014/content/textures/fire_0.png new file mode 100644 index 0000000..bbada0f Binary files /dev/null and b/Client2014/content/textures/fire_0.png differ diff --git a/Client2014/content/textures/glow.png b/Client2014/content/textures/glow.png new file mode 100644 index 0000000..db4a99e Binary files /dev/null and b/Client2014/content/textures/glow.png differ diff --git a/Client2014/content/textures/lua.png b/Client2014/content/textures/lua.png new file mode 100644 index 0000000..2791c43 Binary files /dev/null and b/Client2014/content/textures/lua.png differ diff --git a/Client2014/content/textures/roblox-logo.png b/Client2014/content/textures/roblox-logo.png new file mode 100644 index 0000000..cf5d0c2 Binary files /dev/null and b/Client2014/content/textures/roblox-logo.png differ diff --git a/Client2014/content/textures/script.png b/Client2014/content/textures/script.png new file mode 100644 index 0000000..0f9ed4d Binary files /dev/null and b/Client2014/content/textures/script.png differ diff --git a/Client2014/content/textures/spark.png b/Client2014/content/textures/spark.png new file mode 100644 index 0000000..486f47c Binary files /dev/null and b/Client2014/content/textures/spark.png differ diff --git a/Client2014/content/textures/sparkle.png b/Client2014/content/textures/sparkle.png new file mode 100644 index 0000000..5fa5e2a Binary files /dev/null and b/Client2014/content/textures/sparkle.png differ diff --git a/Client2014/content/textures/ui/CloneButton.png b/Client2014/content/textures/ui/CloneButton.png new file mode 100644 index 0000000..7e24cc6 Binary files /dev/null and b/Client2014/content/textures/ui/CloneButton.png differ diff --git a/Client2014/content/textures/ui/CloneButton_dn.png b/Client2014/content/textures/ui/CloneButton_dn.png new file mode 100644 index 0000000..adbfe60 Binary files /dev/null and b/Client2014/content/textures/ui/CloneButton_dn.png differ diff --git a/Client2014/content/textures/ui/CloseButton.png b/Client2014/content/textures/ui/CloseButton.png new file mode 100644 index 0000000..59d62c0 Binary files /dev/null and b/Client2014/content/textures/ui/CloseButton.png differ diff --git a/Client2014/content/textures/ui/CloseButton_dn.png b/Client2014/content/textures/ui/CloseButton_dn.png new file mode 100644 index 0000000..2a92663 Binary files /dev/null and b/Client2014/content/textures/ui/CloseButton_dn.png differ diff --git a/Client2014/content/textures/ui/Concrete.png b/Client2014/content/textures/ui/Concrete.png new file mode 100644 index 0000000..79ee458 Binary files /dev/null and b/Client2014/content/textures/ui/Concrete.png differ diff --git a/Client2014/content/textures/ui/CorrodedMetal.png b/Client2014/content/textures/ui/CorrodedMetal.png new file mode 100644 index 0000000..76483c7 Binary files /dev/null and b/Client2014/content/textures/ui/CorrodedMetal.png differ diff --git a/Client2014/content/textures/ui/DPadSheet.png b/Client2014/content/textures/ui/DPadSheet.png new file mode 100644 index 0000000..dbef643 Binary files /dev/null and b/Client2014/content/textures/ui/DPadSheet.png differ diff --git a/Client2014/content/textures/ui/DeleteButton.png b/Client2014/content/textures/ui/DeleteButton.png new file mode 100644 index 0000000..7f5757c Binary files /dev/null and b/Client2014/content/textures/ui/DeleteButton.png differ diff --git a/Client2014/content/textures/ui/DeleteButton_dn.png b/Client2014/content/textures/ui/DeleteButton_dn.png new file mode 100644 index 0000000..184a615 Binary files /dev/null and b/Client2014/content/textures/ui/DeleteButton_dn.png differ diff --git a/Client2014/content/textures/ui/DiamondPlate.png b/Client2014/content/textures/ui/DiamondPlate.png new file mode 100644 index 0000000..ad9d4d0 Binary files /dev/null and b/Client2014/content/textures/ui/DiamondPlate.png differ diff --git a/Client2014/content/textures/ui/Foil.png b/Client2014/content/textures/ui/Foil.png new file mode 100644 index 0000000..7cf51fc Binary files /dev/null and b/Client2014/content/textures/ui/Foil.png differ diff --git a/Client2014/content/textures/ui/Gear.png b/Client2014/content/textures/ui/Gear.png new file mode 100644 index 0000000..de60a6a Binary files /dev/null and b/Client2014/content/textures/ui/Gear.png differ diff --git a/Client2014/content/textures/ui/Gear_dn.png b/Client2014/content/textures/ui/Gear_dn.png new file mode 100644 index 0000000..53c8366 Binary files /dev/null and b/Client2014/content/textures/ui/Gear_dn.png differ diff --git a/Client2014/content/textures/ui/Glue.png b/Client2014/content/textures/ui/Glue.png new file mode 100644 index 0000000..3ceed9c Binary files /dev/null and b/Client2014/content/textures/ui/Glue.png differ diff --git a/Client2014/content/textures/ui/Grass.png b/Client2014/content/textures/ui/Grass.png new file mode 100644 index 0000000..fd390c4 Binary files /dev/null and b/Client2014/content/textures/ui/Grass.png differ diff --git a/Client2014/content/textures/ui/GroupMoveButton.png b/Client2014/content/textures/ui/GroupMoveButton.png new file mode 100644 index 0000000..61a48b4 Binary files /dev/null and b/Client2014/content/textures/ui/GroupMoveButton.png differ diff --git a/Client2014/content/textures/ui/GroupMoveButton_dn.png b/Client2014/content/textures/ui/GroupMoveButton_dn.png new file mode 100644 index 0000000..7500d53 Binary files /dev/null and b/Client2014/content/textures/ui/GroupMoveButton_dn.png differ diff --git a/Client2014/content/textures/ui/Hinge.png b/Client2014/content/textures/ui/Hinge.png new file mode 100644 index 0000000..c2ab9c9 Binary files /dev/null and b/Client2014/content/textures/ui/Hinge.png differ diff --git a/Client2014/content/textures/ui/Ice.png b/Client2014/content/textures/ui/Ice.png new file mode 100644 index 0000000..e9d0a32 Binary files /dev/null and b/Client2014/content/textures/ui/Ice.png differ diff --git a/Client2014/content/textures/ui/Inlets.png b/Client2014/content/textures/ui/Inlets.png new file mode 100644 index 0000000..cac89e3 Binary files /dev/null and b/Client2014/content/textures/ui/Inlets.png differ diff --git a/Client2014/content/textures/ui/InsertButton.png b/Client2014/content/textures/ui/InsertButton.png new file mode 100644 index 0000000..4058f36 Binary files /dev/null and b/Client2014/content/textures/ui/InsertButton.png differ diff --git a/Client2014/content/textures/ui/InsertButton_dn.png b/Client2014/content/textures/ui/InsertButton_dn.png new file mode 100644 index 0000000..03e72b2 Binary files /dev/null and b/Client2014/content/textures/ui/InsertButton_dn.png differ diff --git a/Client2014/content/textures/ui/MaterialButton.png b/Client2014/content/textures/ui/MaterialButton.png new file mode 100644 index 0000000..4191aab Binary files /dev/null and b/Client2014/content/textures/ui/MaterialButton.png differ diff --git a/Client2014/content/textures/ui/MaterialButton_dn.png b/Client2014/content/textures/ui/MaterialButton_dn.png new file mode 100644 index 0000000..d61978c Binary files /dev/null and b/Client2014/content/textures/ui/MaterialButton_dn.png differ diff --git a/Client2014/content/textures/ui/MaterialMenu.png b/Client2014/content/textures/ui/MaterialMenu.png new file mode 100644 index 0000000..c4a1c73 Binary files /dev/null and b/Client2014/content/textures/ui/MaterialMenu.png differ diff --git a/Client2014/content/textures/ui/Motor.png b/Client2014/content/textures/ui/Motor.png new file mode 100644 index 0000000..e9b63eb Binary files /dev/null and b/Client2014/content/textures/ui/Motor.png differ diff --git a/Client2014/content/textures/ui/PaintButton.png b/Client2014/content/textures/ui/PaintButton.png new file mode 100644 index 0000000..03b99a7 Binary files /dev/null and b/Client2014/content/textures/ui/PaintButton.png differ diff --git a/Client2014/content/textures/ui/PaintButton_dn.png b/Client2014/content/textures/ui/PaintButton_dn.png new file mode 100644 index 0000000..af4c64d Binary files /dev/null and b/Client2014/content/textures/ui/PaintButton_dn.png differ diff --git a/Client2014/content/textures/ui/PartMoveButton.png b/Client2014/content/textures/ui/PartMoveButton.png new file mode 100644 index 0000000..a8f8fce Binary files /dev/null and b/Client2014/content/textures/ui/PartMoveButton.png differ diff --git a/Client2014/content/textures/ui/PartMoveButton_dn.png b/Client2014/content/textures/ui/PartMoveButton_dn.png new file mode 100644 index 0000000..3c866db Binary files /dev/null and b/Client2014/content/textures/ui/PartMoveButton_dn.png differ diff --git a/Client2014/content/textures/ui/Plastic.png b/Client2014/content/textures/ui/Plastic.png new file mode 100644 index 0000000..8f8bbf3 Binary files /dev/null and b/Client2014/content/textures/ui/Plastic.png differ diff --git a/Client2014/content/textures/ui/PlayerListFriendRequestReceivedIcon.png b/Client2014/content/textures/ui/PlayerListFriendRequestReceivedIcon.png new file mode 100644 index 0000000..ab292da Binary files /dev/null and b/Client2014/content/textures/ui/PlayerListFriendRequestReceivedIcon.png differ diff --git a/Client2014/content/textures/ui/PlayerListFriendRequestSentIcon.png b/Client2014/content/textures/ui/PlayerListFriendRequestSentIcon.png new file mode 100644 index 0000000..e10634b Binary files /dev/null and b/Client2014/content/textures/ui/PlayerListFriendRequestSentIcon.png differ diff --git a/Client2014/content/textures/ui/PlayerlistFriendIcon.png b/Client2014/content/textures/ui/PlayerlistFriendIcon.png new file mode 100644 index 0000000..21619a5 Binary files /dev/null and b/Client2014/content/textures/ui/PlayerlistFriendIcon.png differ diff --git a/Client2014/content/textures/ui/PropertyButton.png b/Client2014/content/textures/ui/PropertyButton.png new file mode 100644 index 0000000..254e00d Binary files /dev/null and b/Client2014/content/textures/ui/PropertyButton.png differ diff --git a/Client2014/content/textures/ui/PropertyButton_dn.png b/Client2014/content/textures/ui/PropertyButton_dn.png new file mode 100644 index 0000000..c95ec8d Binary files /dev/null and b/Client2014/content/textures/ui/PropertyButton_dn.png differ diff --git a/Client2014/content/textures/ui/RecordStop.png b/Client2014/content/textures/ui/RecordStop.png new file mode 100644 index 0000000..6b86baf Binary files /dev/null and b/Client2014/content/textures/ui/RecordStop.png differ diff --git a/Client2014/content/textures/ui/ResetIcon.png b/Client2014/content/textures/ui/ResetIcon.png new file mode 100644 index 0000000..b9558d4 Binary files /dev/null and b/Client2014/content/textures/ui/ResetIcon.png differ diff --git a/Client2014/content/textures/ui/ScaleButton.png b/Client2014/content/textures/ui/ScaleButton.png new file mode 100644 index 0000000..05ab151 Binary files /dev/null and b/Client2014/content/textures/ui/ScaleButton.png differ diff --git a/Client2014/content/textures/ui/ScaleButton_dn.png b/Client2014/content/textures/ui/ScaleButton_dn.png new file mode 100644 index 0000000..c5010fa Binary files /dev/null and b/Client2014/content/textures/ui/ScaleButton_dn.png differ diff --git a/Client2014/content/textures/ui/SearchIcon.png b/Client2014/content/textures/ui/SearchIcon.png new file mode 100644 index 0000000..d72741c Binary files /dev/null and b/Client2014/content/textures/ui/SearchIcon.png differ diff --git a/Client2014/content/textures/ui/SettingsButton.png b/Client2014/content/textures/ui/SettingsButton.png new file mode 100644 index 0000000..196cf23 Binary files /dev/null and b/Client2014/content/textures/ui/SettingsButton.png differ diff --git a/Client2014/content/textures/ui/SettingsButton_dn.png b/Client2014/content/textures/ui/SettingsButton_dn.png new file mode 100644 index 0000000..e6f4799 Binary files /dev/null and b/Client2014/content/textures/ui/SettingsButton_dn.png differ diff --git a/Client2014/content/textures/ui/SettingsButton_ds.png b/Client2014/content/textures/ui/SettingsButton_ds.png new file mode 100644 index 0000000..2aca460 Binary files /dev/null and b/Client2014/content/textures/ui/SettingsButton_ds.png differ diff --git a/Client2014/content/textures/ui/SettingsButton_ovr.png b/Client2014/content/textures/ui/SettingsButton_ovr.png new file mode 100644 index 0000000..e6f4799 Binary files /dev/null and b/Client2014/content/textures/ui/SettingsButton_ovr.png differ diff --git a/Client2014/content/textures/ui/Slate.png b/Client2014/content/textures/ui/Slate.png new file mode 100644 index 0000000..4f90fb8 Binary files /dev/null and b/Client2014/content/textures/ui/Slate.png differ diff --git a/Client2014/content/textures/ui/Slider.png b/Client2014/content/textures/ui/Slider.png new file mode 100644 index 0000000..63c4dea Binary files /dev/null and b/Client2014/content/textures/ui/Slider.png differ diff --git a/Client2014/content/textures/ui/Slider_dn.png b/Client2014/content/textures/ui/Slider_dn.png new file mode 100644 index 0000000..d98cb85 Binary files /dev/null and b/Client2014/content/textures/ui/Slider_dn.png differ diff --git a/Client2014/content/textures/ui/Slider_sel.png b/Client2014/content/textures/ui/Slider_sel.png new file mode 100644 index 0000000..d98cb85 Binary files /dev/null and b/Client2014/content/textures/ui/Slider_sel.png differ diff --git a/Client2014/content/textures/ui/Smooth.png b/Client2014/content/textures/ui/Smooth.png new file mode 100644 index 0000000..e4d6c4d Binary files /dev/null and b/Client2014/content/textures/ui/Smooth.png differ diff --git a/Client2014/content/textures/ui/StampToolButton.png b/Client2014/content/textures/ui/StampToolButton.png new file mode 100644 index 0000000..34e7994 Binary files /dev/null and b/Client2014/content/textures/ui/StampToolButton.png differ diff --git a/Client2014/content/textures/ui/StampToolButton_dn.png b/Client2014/content/textures/ui/StampToolButton_dn.png new file mode 100644 index 0000000..affebf4 Binary files /dev/null and b/Client2014/content/textures/ui/StampToolButton_dn.png differ diff --git a/Client2014/content/textures/ui/Studs.png b/Client2014/content/textures/ui/Studs.png new file mode 100644 index 0000000..194a02b Binary files /dev/null and b/Client2014/content/textures/ui/Studs.png differ diff --git a/Client2014/content/textures/ui/SurfaceButton.png b/Client2014/content/textures/ui/SurfaceButton.png new file mode 100644 index 0000000..78c90f0 Binary files /dev/null and b/Client2014/content/textures/ui/SurfaceButton.png differ diff --git a/Client2014/content/textures/ui/SurfaceButton_dn.png b/Client2014/content/textures/ui/SurfaceButton_dn.png new file mode 100644 index 0000000..850e099 Binary files /dev/null and b/Client2014/content/textures/ui/SurfaceButton_dn.png differ diff --git a/Client2014/content/textures/ui/SurfaceMenu.png b/Client2014/content/textures/ui/SurfaceMenu.png new file mode 100644 index 0000000..1582479 Binary files /dev/null and b/Client2014/content/textures/ui/SurfaceMenu.png differ diff --git a/Client2014/content/textures/ui/TinyBcIcon.png b/Client2014/content/textures/ui/TinyBcIcon.png new file mode 100644 index 0000000..1e2c7ef Binary files /dev/null and b/Client2014/content/textures/ui/TinyBcIcon.png differ diff --git a/Client2014/content/textures/ui/TinyObcIcon.png b/Client2014/content/textures/ui/TinyObcIcon.png new file mode 100644 index 0000000..f12104d Binary files /dev/null and b/Client2014/content/textures/ui/TinyObcIcon.png differ diff --git a/Client2014/content/textures/ui/TinyTbcIcon.png b/Client2014/content/textures/ui/TinyTbcIcon.png new file mode 100644 index 0000000..d5c37fe Binary files /dev/null and b/Client2014/content/textures/ui/TinyTbcIcon.png differ diff --git a/Client2014/content/textures/ui/ToggleFullScreen_ds.png b/Client2014/content/textures/ui/ToggleFullScreen_ds.png new file mode 100644 index 0000000..07d7de0 Binary files /dev/null and b/Client2014/content/textures/ui/ToggleFullScreen_ds.png differ diff --git a/Client2014/content/textures/ui/ToolButton.png b/Client2014/content/textures/ui/ToolButton.png new file mode 100644 index 0000000..ca9a395 Binary files /dev/null and b/Client2014/content/textures/ui/ToolButton.png differ diff --git a/Client2014/content/textures/ui/ToolButton_dn.png b/Client2014/content/textures/ui/ToolButton_dn.png new file mode 100644 index 0000000..e53c826 Binary files /dev/null and b/Client2014/content/textures/ui/ToolButton_dn.png differ diff --git a/Client2014/content/textures/ui/ToolButton_ds.png b/Client2014/content/textures/ui/ToolButton_ds.png new file mode 100644 index 0000000..016ae9c Binary files /dev/null and b/Client2014/content/textures/ui/ToolButton_ds.png differ diff --git a/Client2014/content/textures/ui/TouchControlsSheet.png b/Client2014/content/textures/ui/TouchControlsSheet.png new file mode 100644 index 0000000..d3b740e Binary files /dev/null and b/Client2014/content/textures/ui/TouchControlsSheet.png differ diff --git a/Client2014/content/textures/ui/Universal.png b/Client2014/content/textures/ui/Universal.png new file mode 100644 index 0000000..508fac8 Binary files /dev/null and b/Client2014/content/textures/ui/Universal.png differ diff --git a/Client2014/content/textures/ui/Weld.png b/Client2014/content/textures/ui/Weld.png new file mode 100644 index 0000000..d45375b Binary files /dev/null and b/Client2014/content/textures/ui/Weld.png differ diff --git a/Client2014/content/textures/ui/Wood.png b/Client2014/content/textures/ui/Wood.png new file mode 100644 index 0000000..93898de Binary files /dev/null and b/Client2014/content/textures/ui/Wood.png differ diff --git a/Client2014/content/textures/ui/btn_grey.png b/Client2014/content/textures/ui/btn_grey.png new file mode 100644 index 0000000..5e46696 Binary files /dev/null and b/Client2014/content/textures/ui/btn_grey.png differ diff --git a/Client2014/content/textures/ui/btn_greyTransp.png b/Client2014/content/textures/ui/btn_greyTransp.png new file mode 100644 index 0000000..df5989b Binary files /dev/null and b/Client2014/content/textures/ui/btn_greyTransp.png differ diff --git a/Client2014/content/textures/ui/btn_red.png b/Client2014/content/textures/ui/btn_red.png new file mode 100644 index 0000000..17666e6 Binary files /dev/null and b/Client2014/content/textures/ui/btn_red.png differ diff --git a/Client2014/content/textures/ui/btn_redGlow.png b/Client2014/content/textures/ui/btn_redGlow.png new file mode 100644 index 0000000..5cf7081 Binary files /dev/null and b/Client2014/content/textures/ui/btn_redGlow.png differ diff --git a/Client2014/content/textures/ui/btn_white.png b/Client2014/content/textures/ui/btn_white.png new file mode 100644 index 0000000..d2a1cf2 Binary files /dev/null and b/Client2014/content/textures/ui/btn_white.png differ diff --git a/Client2014/content/textures/ui/mouseLock_off.png b/Client2014/content/textures/ui/mouseLock_off.png new file mode 100644 index 0000000..e170d2c Binary files /dev/null and b/Client2014/content/textures/ui/mouseLock_off.png differ diff --git a/Client2014/content/textures/ui/mouseLock_off_ds.png b/Client2014/content/textures/ui/mouseLock_off_ds.png new file mode 100644 index 0000000..85b9a10 Binary files /dev/null and b/Client2014/content/textures/ui/mouseLock_off_ds.png differ diff --git a/Client2014/content/textures/ui/mouseLock_off_ovr.png b/Client2014/content/textures/ui/mouseLock_off_ovr.png new file mode 100644 index 0000000..6c06e1e Binary files /dev/null and b/Client2014/content/textures/ui/mouseLock_off_ovr.png differ diff --git a/Client2014/content/textures/ui/mouseLock_on.png b/Client2014/content/textures/ui/mouseLock_on.png new file mode 100644 index 0000000..caa9d52 Binary files /dev/null and b/Client2014/content/textures/ui/mouseLock_on.png differ diff --git a/Client2014/content/textures/ui/mouseLock_on_ds.png b/Client2014/content/textures/ui/mouseLock_on_ds.png new file mode 100644 index 0000000..0fa160e Binary files /dev/null and b/Client2014/content/textures/ui/mouseLock_on_ds.png differ diff --git a/Client2014/content/textures/ui/mouseLock_on_ovr.png b/Client2014/content/textures/ui/mouseLock_on_ovr.png new file mode 100644 index 0000000..653535f Binary files /dev/null and b/Client2014/content/textures/ui/mouseLock_on_ovr.png differ diff --git a/Client2014/content/textures/ui/scrollbar.png b/Client2014/content/textures/ui/scrollbar.png new file mode 100644 index 0000000..9d7a4ff Binary files /dev/null and b/Client2014/content/textures/ui/scrollbar.png differ diff --git a/Client2014/content/textures/ui/scrollbuttonDown.png b/Client2014/content/textures/ui/scrollbuttonDown.png new file mode 100644 index 0000000..a3e6539 Binary files /dev/null and b/Client2014/content/textures/ui/scrollbuttonDown.png differ diff --git a/Client2014/content/textures/ui/scrollbuttonDown_dn.png b/Client2014/content/textures/ui/scrollbuttonDown_dn.png new file mode 100644 index 0000000..f7002ce Binary files /dev/null and b/Client2014/content/textures/ui/scrollbuttonDown_dn.png differ diff --git a/Client2014/content/textures/ui/scrollbuttonDown_ds.png b/Client2014/content/textures/ui/scrollbuttonDown_ds.png new file mode 100644 index 0000000..eac4eb9 Binary files /dev/null and b/Client2014/content/textures/ui/scrollbuttonDown_ds.png differ diff --git a/Client2014/content/textures/ui/scrollbuttonDown_ovr.png b/Client2014/content/textures/ui/scrollbuttonDown_ovr.png new file mode 100644 index 0000000..f7002ce Binary files /dev/null and b/Client2014/content/textures/ui/scrollbuttonDown_ovr.png differ diff --git a/Client2014/content/textures/ui/scrollbuttonUp.png b/Client2014/content/textures/ui/scrollbuttonUp.png new file mode 100644 index 0000000..5754f55 Binary files /dev/null and b/Client2014/content/textures/ui/scrollbuttonUp.png differ diff --git a/Client2014/content/textures/ui/scrollbuttonUp_dn.png b/Client2014/content/textures/ui/scrollbuttonUp_dn.png new file mode 100644 index 0000000..f12d690 Binary files /dev/null and b/Client2014/content/textures/ui/scrollbuttonUp_dn.png differ diff --git a/Client2014/content/textures/ui/scrollbuttonUp_ds.png b/Client2014/content/textures/ui/scrollbuttonUp_ds.png new file mode 100644 index 0000000..1e1b9db Binary files /dev/null and b/Client2014/content/textures/ui/scrollbuttonUp_ds.png differ diff --git a/Client2014/content/textures/ui/scrollbuttonUp_ovr.png b/Client2014/content/textures/ui/scrollbuttonUp_ovr.png new file mode 100644 index 0000000..f12d690 Binary files /dev/null and b/Client2014/content/textures/ui/scrollbuttonUp_ovr.png differ diff --git a/Client2014/content/textures/water_Subsurface.dds b/Client2014/content/textures/water_Subsurface.dds new file mode 100644 index 0000000..0435f14 Binary files /dev/null and b/Client2014/content/textures/water_Subsurface.dds differ diff --git a/Client2014/content/textures/water_Wave.dds b/Client2014/content/textures/water_Wave.dds new file mode 100644 index 0000000..fc2f7a0 Binary files /dev/null and b/Client2014/content/textures/water_Wave.dds differ diff --git a/Client2014/content/textures/wrench.png b/Client2014/content/textures/wrench.png new file mode 100644 index 0000000..5c8213f Binary files /dev/null and b/Client2014/content/textures/wrench.png differ diff --git a/Client2014/fmodex.dll b/Client2014/fmodex.dll new file mode 100644 index 0000000..d91f410 Binary files /dev/null and b/Client2014/fmodex.dll differ diff --git a/Client2014/msvcp110.dll b/Client2014/msvcp110.dll new file mode 100644 index 0000000..93fab56 Binary files /dev/null and b/Client2014/msvcp110.dll differ diff --git a/Client2014/msvcr110.dll b/Client2014/msvcr110.dll new file mode 100644 index 0000000..1ce960d Binary files /dev/null and b/Client2014/msvcr110.dll differ diff --git a/Client2014/shaders/shaders.json b/Client2014/shaders/shaders.json new file mode 100644 index 0000000..83eda67 --- /dev/null +++ b/Client2014/shaders/shaders.json @@ -0,0 +1,105 @@ +[ + { "name": "AdornVS", "source": "adorn.hlsl", "target": "vs_2_0", "entrypoint": "AdornVS" }, + { "name": "AdornSelfLitVS", "source": "adorn.hlsl", "target": "vs_2_0", "entrypoint": "AdornSelfLitVS" }, + { "name": "AdornSelfLitHighlightVS", "source": "adorn.hlsl", "target": "vs_2_0", "entrypoint": "AdornSelfLitHighlightVS" }, + { "name": "AdornLightingVS", "source": "adorn.hlsl", "target": "vs_2_0", "entrypoint": "AdornVS", "defines": "PIN_LIGHTING" }, + { "name": "AdornFS", "source": "adorn.hlsl", "target": "ps_2_0", "entrypoint": "AdornPS" }, + { "name": "DefaultStaticVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS" }, + { "name": "DefaultStaticHQVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_HQ" }, + { "name": "DefaultSkinnedVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_SKINNED" }, + { "name": "DefaultSkinnedHQVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_SKINNED PIN_HQ" }, + { "name": "DefaultStaticDebugVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_DEBUG" }, + { "name": "DefaultSkinnedDebugVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_SKINNED PIN_DEBUG" }, + { "name": "DefaultFS", "source": "default.hlsl", "target": "ps_2_0", "entrypoint": "DefaultPS" }, + { "name": "DefaultHQFS", "source": "default.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultHQGBufferFS", "source": "default.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultStaticReflectionVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_REFLECTION" }, + { "name": "DefaultSkinnedReflectionVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_SKINNED PIN_REFLECTION" }, + { "name": "DefaultStaticSurfaceHQVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_SURFACE PIN_HQ" }, + { "name": "DefaultSkinnedSurfaceHQVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_SKINNED PIN_SURFACE PIN_HQ" }, + { "name": "DefaultPlasticFS", "source": "plastic.hlsl", "target": "ps_2_0", "entrypoint": "DefaultPS" }, + { "name": "DefaultPlasticHQFS", "source": "plastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultPlasticHQGBufferFS", "source": "plastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultPlasticReflectionFS", "source": "plastic.hlsl", "target": "ps_2_0", "entrypoint": "DefaultPS", "defines": "PIN_REFLECTION" }, + { "name": "DefaultPlasticReflectionHQFS", "source": "plastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_REFLECTION PIN_HQ" }, + { "name": "DefaultPlasticReflectionHQGBufferFS", "source": "plastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_REFLECTION PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultSmoothPlasticFS", "source": "smoothplastic.hlsl", "target": "ps_2_0", "entrypoint": "DefaultPS" }, + { "name": "DefaultSmoothPlasticHQFS", "source": "smoothplastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultSmoothPlasticHQGBufferFS", "source": "smoothplastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultSmoothPlasticReflectionFS", "source": "smoothplastic.hlsl", "target": "ps_2_0", "entrypoint": "DefaultPS", "defines": "PIN_REFLECTION" }, + { "name": "DefaultSmoothPlasticReflectionHQFS", "source": "smoothplastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_REFLECTION PIN_HQ" }, + { "name": "DefaultSmoothPlasticReflectionHQGBufferFS", "source": "smoothplastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_REFLECTION PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultWoodHQFS", "source": "wood.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultWoodHQGBufferFS", "source": "wood.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultMarbleHQFS", "source": "marble.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultMarbleHQGBufferFS", "source": "marble.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultSlateHQFS", "source": "slate.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultSlateHQGBufferFS", "source": "slate.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultGraniteHQFS", "source": "granite.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultGraniteHQGBufferFS", "source": "granite.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultConcreteHQFS", "source": "concrete.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultConcreteHQGBufferFS", "source": "concrete.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultPebbleHQFS", "source": "pebble.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultPebbleHQGBufferFS", "source": "pebble.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultBrickHQFS", "source": "brick.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultBrickHQGBufferFS", "source": "brick.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultRustHQFS", "source": "rust.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultRustHQGBufferFS", "source": "rust.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultDiamondplateHQFS", "source": "diamondplate.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultDiamondplateHQGBufferFS", "source": "diamondplate.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultAluminumHQFS", "source": "aluminum.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultAluminumHQGBufferFS", "source": "aluminum.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultGrassHQFS", "source": "grass.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultGrassHQGBufferFS", "source": "grass.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultSandHQFS", "source": "sand.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultSandHQGBufferFS", "source": "sand.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultFabricHQFS", "source": "fabric.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultFabricHQGBufferFS", "source": "fabric.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultIceHQFS", "source": "ice.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultIceHQGBufferFS", "source": "ice.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "MegaClusterVS", "source": "megacluster.hlsl", "target": "vs_2_0", "entrypoint": "MegaClusterVS" }, + { "name": "MegaClusterHQVS", "source": "megacluster.hlsl", "target": "vs_2_0", "entrypoint": "MegaClusterVS", "defines": "PIN_HQ" }, + { "name": "MegaClusterFS", "source": "megacluster.hlsl", "target": "ps_2_0", "entrypoint": "MegaClusterPS" }, + { "name": "MegaClusterHQFS", "source": "megacluster.hlsl", "target": "ps_2_a", "entrypoint": "MegaClusterPS", "defines": "PIN_HQ" }, + { "name": "MegaClusterHQGBufferFS", "source": "megacluster.hlsl", "target": "ps_2_a", "entrypoint": "MegaClusterPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "ParticleVS", "source": "particle.hlsl", "target": "vs_1_1", "entrypoint": "vs" }, + { "name": "ParticleAddFS", "source": "particle.hlsl", "target": "ps_2_0", "entrypoint": "psAdd" }, + { "name": "ParticleMulFS", "source": "particle.hlsl", "target": "ps_2_0", "entrypoint": "psMul" }, + { "name": "ShadowExtrudeVS", "source": "shadowextrude.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS" }, + { "name": "ShadowExtrudeFS", "source": "shadowextrude.hlsl", "target": "ps_2_0", "entrypoint": "DefaultPS" }, + { "name": "ShadowQuadVS", "source": "shadowquad.hlsl", "target": "vs_1_1", "entrypoint": "QuadVS" }, + { "name": "ShadowQuadFS", "source": "shadowquad.hlsl", "target": "ps_2_0", "entrypoint": "QuadPS" }, + { "name": "SkyVS", "source": "sky.hlsl", "target": "vs_1_1", "entrypoint": "SkyVS" }, + { "name": "SkyFS", "source": "sky.hlsl", "target": "ps_2_0", "entrypoint": "SkyPS" }, + { "name": "TexCompVS", "source": "texcomp.hlsl", "target": "vs_1_1", "entrypoint": "TexCompVS" }, + { "name": "TexCompFS", "source": "texcomp.hlsl", "target": "ps_2_0", "entrypoint": "TexCompPS" }, + { "name": "TexCompPMAFS", "source": "texcomp.hlsl", "target": "ps_2_0", "entrypoint": "TexCompPMAPS" }, + { "name": "UIVS", "source": "ui.hlsl", "target": "vs_1_1", "entrypoint": "UIVS" }, + { "name": "UIFogVS", "source": "ui.hlsl", "target": "vs_1_1", "entrypoint": "UIVS", "defines": "PIN_FOG" }, + { "name": "UIFS", "source": "ui.hlsl", "target": "ps_2_0", "entrypoint": "UIPS" }, + { "name": "UIFogFS", "source": "ui.hlsl", "target": "ps_2_0", "entrypoint": "UIPS", "defines": "PIN_FOG" }, + { "name": "WaterVS", "source": "water.hlsl", "target": "vs_2_0", "entrypoint": "water_vs" }, + { "name": "WaterHQVS", "source": "water.hlsl", "target": "vs_2_0", "entrypoint": "water_vs", "defines": "PIN_HQ" }, + { "name": "WaterFS", "source": "water.hlsl", "target": "ps_2_0", "entrypoint": "water_ps" }, + { "name": "WaterHQFS", "source": "water.hlsl", "target": "ps_2_a", "entrypoint": "water_ps", "defines": "PIN_HQ" }, + + { "name": "OLD_WaterVS", "source": "water_r3.hlsl", "target": "vs_2_0", "entrypoint": "water_vs" }, + { "name": "OLD_WaterHQVS", "source": "water_r3.hlsl", "target": "vs_2_0", "entrypoint": "water_vs", "defines": "PIN_HQ" }, + { "name": "OLD_WaterFS", "source": "water_r3.hlsl", "target": "ps_2_0", "entrypoint": "water_ps" }, + { "name": "OLD_WaterHQFS", "source": "water_r3.hlsl", "target": "ps_2_a", "entrypoint": "water_ps", "defines": "PIN_HQ" }, + + + { "name": "SSAOVS", "source": "ssao.hlsl", "target": "vs_1_1", "entrypoint": "ssao_vs", "exclude": "glsles" }, + { "name": "SSAOFS", "source": "ssao.hlsl", "target": "ps_2_a", "entrypoint": "ssao_ps", "exclude": "glsles" }, + { "name": "SSAODepthDownVS", "source": "ssao.hlsl", "target": "vs_1_1", "entrypoint": "ssaoDepthDown_vs", "exclude": "glsles" }, + { "name": "SSAODepthDownFS", "source": "ssao.hlsl", "target": "ps_2_a", "entrypoint": "ssaoDepthDown_ps", "exclude": "glsles" }, + { "name": "SSAOBlurXVS", "source": "ssao.hlsl", "target": "vs_1_1", "entrypoint": "ssaoBlurX_vs", "exclude": "glsles" }, + { "name": "SSAOBlurXFS", "source": "ssao.hlsl", "target": "ps_2_a", "entrypoint": "ssaoBlurX_ps", "exclude": "glsles" }, + { "name": "SSAOBlurYVS", "source": "ssao.hlsl", "target": "vs_1_1", "entrypoint": "ssaoBlurY_vs", "exclude": "glsles" }, + { "name": "SSAOBlurYFS", "source": "ssao.hlsl", "target": "ps_2_a", "entrypoint": "ssaoBlurY_ps", "exclude": "glsles" }, + { "name": "SSAOCompositVS", "source": "ssao.hlsl", "target": "vs_1_1", "entrypoint": "ssaoComposit_vs", "exclude": "glsles" }, + { "name": "SSAOCompositBlankFS", "source": "ssao.hlsl", "target": "ps_2_a", "entrypoint": "ssaoCompositBlank_ps", "exclude": "glsles" }, + { "name": "SSAOCompositFS", "source": "ssao.hlsl", "target": "ps_2_a", "entrypoint": "ssaoComposit_ps", "exclude": "glsles" }, + { "name": "MSAACompositVS", "source": "msaa.hlsl", "target": "vs_1_1", "entrypoint": "msaaComposit_vs" }, + { "name": "MSAACompositFS", "source": "msaa.hlsl", "target": "ps_2_0", "entrypoint": "msaaComposit_ps" } +] diff --git a/Client2014/shaders/shaders_d3d9.pack b/Client2014/shaders/shaders_d3d9.pack new file mode 100644 index 0000000..d1136e2 Binary files /dev/null and b/Client2014/shaders/shaders_d3d9.pack differ diff --git a/Client2014/shaders/shaders_glsl.pack b/Client2014/shaders/shaders_glsl.pack new file mode 100644 index 0000000..7592a73 Binary files /dev/null and b/Client2014/shaders/shaders_glsl.pack differ diff --git a/Client2014/shaders/shaders_glsles.pack b/Client2014/shaders/shaders_glsles.pack new file mode 100644 index 0000000..f98429f Binary files /dev/null and b/Client2014/shaders/shaders_glsles.pack differ diff --git a/Client2014/shaders/source/adorn.hlsl b/Client2014/shaders/source/adorn.hlsl new file mode 100644 index 0000000..a574f3c --- /dev/null +++ b/Client2014/shaders/source/adorn.hlsl @@ -0,0 +1,88 @@ +#include "globals.h" + +struct Appdata +{ + float4 Position : POSITION; + float2 Uv : TEXCOORD0; + float3 Normal : NORMAL0; +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; + + float2 Uv : TEXCOORD0; + float4 Color : COLOR0; + + float FogFactor : TEXCOORD1; +}; + +uniform float4x4 WorldMatrix; + +uniform float4 Color; + +VertexOutput AdornSelfLitVSGeneric(Appdata IN, float ambient) +{ + VertexOutput OUT = (VertexOutput)0; + + float4 position = mul(WorldMatrix, IN.Position); + float3 normal = normalize(mul((float3x3)WorldMatrix, IN.Normal)); + + float3 light = normalize(G.CameraPosition - position.xyz); + float ndotl = saturate(dot(normal, light)); + + float lighting = ambient + (1 - ambient) * ndotl; + float specular = pow(ndotl, 64.0); + + OUT.HPosition = mul(G.ViewProjection, mul(WorldMatrix, IN.Position)); + OUT.Uv = IN.Uv; + OUT.Color = float4(Color.rgb * lighting + specular, Color.a); + + OUT.FogFactor = (G.FogParams.z - OUT.HPosition.w) * G.FogParams.w; + + return OUT; +} + +VertexOutput AdornSelfLitVS(Appdata IN) +{ + return AdornSelfLitVSGeneric(IN, 0.5f); +} + +VertexOutput AdornSelfLitHighlightVS(Appdata IN) +{ + return AdornSelfLitVSGeneric(IN, 0.75f); +} + +VertexOutput AdornVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + float4 position = mul(WorldMatrix, IN.Position); + +#ifdef PIN_LIGHTING + float3 normal = normalize(mul((float3x3)WorldMatrix, IN.Normal)); + float ndotl = dot(normal, -G.Lamp0Dir); + float3 lighting = G.AmbientColor + saturate(ndotl) * G.Lamp0Color + saturate(-ndotl) * G.Lamp1Color; +#else + float3 lighting = 1; +#endif + + OUT.HPosition = mul(G.ViewProjection, position); + OUT.Uv = IN.Uv; + OUT.Color = float4(Color.rgb * lighting, Color.a); + + OUT.FogFactor = (G.FogParams.z - OUT.HPosition.w) * G.FogParams.w; + + return OUT; +} + +sampler2D DiffuseMap: register(s0); + +float4 AdornPS(VertexOutput IN): COLOR0 +{ + float4 result = tex2D(DiffuseMap, IN.Uv) * IN.Color; + + result.rgb = lerp(G.FogColor, result.rgb, saturate(IN.FogFactor)); + + return result; +} diff --git a/Client2014/shaders/source/aluminum.hlsl b/Client2014/shaders/source/aluminum.hlsl new file mode 100644 index 0000000..f327503 --- /dev/null +++ b/Client2014/shaders/source/aluminum.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 1 +#define CFG_GLOSS_SCALE 256 +#define CFG_REFLECTION_SCALE 0.6 + +#define CFG_NORMAL_SHADOW_SCALE 0 + +#define CFG_SPECULAR_LOD 0.94 +#define CFG_GLOSS_LOD 240 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0.25 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0.75 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#define CFG_OPT_DIFFUSE_CONST + +#include "material.hlsl" diff --git a/Client2014/shaders/source/brick.hlsl b/Client2014/shaders/source/brick.hlsl new file mode 100644 index 0000000..eb7c8db --- /dev/null +++ b/Client2014/shaders/source/brick.hlsl @@ -0,0 +1,21 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 1.3 +#define CFG_GLOSS_SCALE 64 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.1 + +#define CFG_SPECULAR_LOD 0.26 +#define CFG_GLOSS_LOD 26 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#include "material.hlsl" diff --git a/Client2014/shaders/source/common.h b/Client2014/shaders/source/common.h new file mode 100644 index 0000000..c598916 --- /dev/null +++ b/Client2014/shaders/source/common.h @@ -0,0 +1,140 @@ +#include "globals.h" + +// GLSLES has limited number of vertex shader registers so we have to use less bones +#ifdef GLSLES +#define MAX_BONE_COUNT 32 +#else +#define MAX_BONE_COUNT 72 +#endif + +// PowerVR saturate() is compiled to min/max pair +// These are cross-platform specialized saturates that are free on PC and only cost 1 cycle on PowerVR +#ifdef GLSLES +float saturate0(float v) { return max(v, 0); } +float saturate1(float v) { return min(v, 1); } +#else +float saturate0(float v) { return saturate(v); } +float saturate1(float v) { return saturate(v); } +#endif + +#define GBUFFER_MAX_DEPTH 500.0f + +float4 gbufferPack(float depth, float3 diffuse, float3 specular, float fog) +{ + depth = saturate(depth / GBUFFER_MAX_DEPTH); + + const float3 bitSh = float3(255*255, 255, 1); + const float3 lumVec = float3(0.299, 0.587, 0.114); + + float2 comp; + comp = depth*float2(255,255*256); + comp = frac(comp); + comp = float2(depth,comp.x*256/255) - float2(comp.x, comp.y)/255; + + float4 result; + + result.r = lerp(1, dot(specular, lumVec), saturate(3 * fog)); + result.g = lerp(0, dot(diffuse, lumVec), saturate(3 * fog)); + result.ba = comp.yx; + + return result; +} + +float3 getPosInLightSpace(float3 posIn) +{ + float3 lightToWorld = posIn - G.BlobShadowData0.xyz; + return float3(dot(G.Lamp0Right, lightToWorld), dot(G.Lamp0Up, lightToWorld), dot(G.Lamp0Dir, lightToWorld)); +} + +float getSingleBlobShadowOrigin(float3 lightSpacePos, float4 blobData) +{ + float distSq = dot(lightSpacePos.xy, lightSpacePos.xy); + + // OH MY GOD! a BRANCH? Why? Because this produces a better assembly over other solution + float projDistScaled = lightSpacePos.z * 0.04; + if (lightSpacePos.z < 0) + projDistScaled = lightSpacePos.z * -0.3; + + return min(1, distSq * G.OutlineBrightness_ShadowInfo.z + projDistScaled + blobData.a); +} + +float getSingleBlobShadow(float3 lightSpacePos, float4 blobData) +{ + return getSingleBlobShadowOrigin(lightSpacePos - blobData.xyz, blobData); +} + +float getBlobShadow(float3 lightSpacePos) +{ + #ifdef PIN_HQ + float shadow = min(getSingleBlobShadowOrigin(lightSpacePos, G.BlobShadowData0), getSingleBlobShadow(lightSpacePos, G.BlobShadowData1)); + shadow = min(getSingleBlobShadow(lightSpacePos, G.BlobShadowData2), shadow); + shadow = min(getSingleBlobShadow(lightSpacePos, G.BlobShadowData3), shadow); + return shadow; + #else + return getSingleBlobShadowOrigin(lightSpacePos, G.BlobShadowData0); + #endif +} + +float3 lgridOffset(float3 v, float3 n) +{ + // cells are 4 studs in size + // offset in normal direction to prevent self-occlusion + // the offset has to be 1.5 cells in order to fully eliminate the influence of the source cell with trilinear filtering + // (i.e. 1 cell is enough for point filtering, but is not enough for trilinear filtering) + return v + n * (1.5f * 4.f); +} + +float3 lgridPrepareSample(float3 c) +{ + // yxz swizzle is necessary for GLSLES sampling to work efficiently + // (having .y as the first component allows to do the LUT lookup as a non-dependent texture fetch) + return c.yxz * G.LightConfig0.xyz + G.LightConfig1.xyz; +} + +#ifdef GLSLES +#define LGRID_SAMPLER sampler2D + +float4 lgridSample(LGRID_SAMPLER t, sampler2D lut, float3 data) +{ + float4 offsets = tex2D(lut, data.xy); + + // texture is 64 pixels high + // let's compute slice lerp coeff + float slicef = frac(data.x * 64); + + // texture has 64 slices with 8x8 atlas setup + float2 base = saturate(data.yz) * 0.125; + + float4 s0 = tex2D(t, base + offsets.xy); + float4 s1 = tex2D(t, base + offsets.zw); + + return lerp(s0, s1, slicef); +} +#else +#define LGRID_SAMPLER sampler3D + +float4 lgridSample(LGRID_SAMPLER t, sampler2D lut, float3 data) +{ + float3 edge = step(G.LightConfig3.xyz, abs(data - G.LightConfig2.xyz)); + float edgef = saturate1(dot(edge, 1)); + + // replace data with 0 on edges to minimize texture cache misses + float4 light = tex3D(t, data.yzx - data.yzx * edgef); + + return lerp(light, G.LightBorder, edgef); +} +#endif + +#ifdef GLSLES +float3 nmapUnpack(float4 value) +{ + return value.rgb * 2 - 1; +} +#else +float3 nmapUnpack(float4 value) +{ + float2 xy = value.ag * 2 - 1; + + return float3(xy, sqrt(saturate(1 + dot(-xy, xy)))); +} +#endif diff --git a/Client2014/shaders/source/concrete.hlsl b/Client2014/shaders/source/concrete.hlsl new file mode 100644 index 0000000..e068502 --- /dev/null +++ b/Client2014/shaders/source/concrete.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 1.3 +#define CFG_GLOSS_SCALE 128 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0 + +#define CFG_SPECULAR_LOD 0.07 +#define CFG_GLOSS_LOD 22 + +#define CFG_NORMAL_DETAIL_TILING 10 +#define CFG_NORMAL_DETAIL_SCALE 1 + +#define CFG_FAR_TILING 0.25 +#define CFG_FAR_DIFFUSE_CUTOFF 0.75 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#define CFG_OPT_NORMAL_CONST + +#include "material.hlsl" diff --git a/Client2014/shaders/source/default.hlsl b/Client2014/shaders/source/default.hlsl new file mode 100644 index 0000000..11f4cec --- /dev/null +++ b/Client2014/shaders/source/default.hlsl @@ -0,0 +1,267 @@ +#include "common.h" + +struct Appdata +{ + float4 Position : POSITION; + float3 Normal : NORMAL; + float2 Uv : TEXCOORD0; + float2 UvStuds : TEXCOORD1; + + float4 Color : COLOR0; + + // int4 produces better D3D asm, float4 produces better GLSL code +#ifdef GLSL + float4 Extra : COLOR1; +#else + int4 Extra : COLOR1; +#endif + +#ifdef PIN_SURFACE + float3 Tangent : TEXCOORD2; +#endif + float4 EdgeDistances : TEXCOORD3; +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; + float4 Uv_EdgeDistance1 : TEXCOORD0; + float4 UvStuds_EdgeDistance2 : TEXCOORD1; + + float4 Color : COLOR0; + float4 LightPosition_Fog : TEXCOORD2; + + #if defined(PIN_HQ) || defined(PIN_REFLECTION) + float4 View_DepthMulFadeout : TEXCOORD3; + float4 Normal_SpecPower : TEXCOORD4; + #endif + + #ifdef PIN_SURFACE + float3 Tangent : TEXCOORD5; + #else + float4 Diffuse_Specular : COLOR1; + #endif + + float4 PosLightSpace_Reflectance: TEXCOORD6; +}; + +#ifdef PIN_SKINNED +uniform float4 WorldMatrixArray[MAX_BONE_COUNT * 3]; +#endif + +#ifdef PIN_DEBUG +uniform float4 DebugColor; +#endif + +VertexOutput DefaultVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + // Transform position and normal to world space +#ifdef PIN_SKINNED + int boneIndex = IN.Extra.r; + + float4 worldRow0 = WorldMatrixArray[boneIndex * 3 + 0]; + float4 worldRow1 = WorldMatrixArray[boneIndex * 3 + 1]; + float4 worldRow2 = WorldMatrixArray[boneIndex * 3 + 2]; + + float3 posWorld = float3(dot(worldRow0, IN.Position), dot(worldRow1, IN.Position), dot(worldRow2, IN.Position)); + float3 normalWorld = float3(dot(worldRow0.xyz, IN.Normal), dot(worldRow1.xyz, IN.Normal), dot(worldRow2.xyz, IN.Normal)); +#else + float3 posWorld = IN.Position.xyz; + float3 normalWorld = IN.Normal; +#endif + + // Decode diffuse/specular parameters; encoding depends on the skinned flag due to vertex declaration differences +#if defined(PIN_DEBUG) + float4 color = DebugColor; +#else + float4 color = IN.Color; +#endif + + float specularIntensity = IN.Extra.g / 255.f; + float specularPower = IN.Extra.b; + + float ndotl = dot(normalWorld, -G.Lamp0Dir); + +#ifdef PIN_HQ + // We'll calculate specular in pixel shader + float2 lt = float2(saturate(ndotl), (ndotl > 0)); +#else + // Using lit here improves performance on software vertex shader implementations + float2 lt = lit(ndotl, dot(normalize(-G.Lamp0Dir + normalize(G.CameraPosition - posWorld.xyz)), normalWorld), specularPower).yz; +#endif + + OUT.HPosition = mul(G.ViewProjection, float4(posWorld, 1)); + + OUT.Uv_EdgeDistance1.xy = IN.Uv; + OUT.UvStuds_EdgeDistance2.xy = IN.UvStuds; + + OUT.Color = color; + OUT.LightPosition_Fog = float4(lgridPrepareSample(lgridOffset(posWorld, normalWorld)), (G.FogParams.z - OUT.HPosition.w) * G.FogParams.w); + + #if defined(PIN_HQ) || defined(PIN_REFLECTION) + OUT.View_DepthMulFadeout = float4(G.CameraPosition - posWorld, OUT.HPosition.w * G.FadeDistance.y); + float4 edgeDistances = IN.EdgeDistances*G.FadeDistance.z + 0.5 * OUT.View_DepthMulFadeout.w; + OUT.Uv_EdgeDistance1.zw = edgeDistances.xy; + OUT.UvStuds_EdgeDistance2.zw = edgeDistances.zw; + OUT.Normal_SpecPower = float4(normalWorld, specularPower); + OUT.PosLightSpace_Reflectance.w = IN.Extra.a / 255.f; + #endif + + #ifdef PIN_SURFACE + #ifdef PIN_SKINNED + float3 tangent = float3(dot(worldRow0.xyz, IN.Tangent), dot(worldRow1.xyz, IN.Tangent), dot(worldRow2.xyz, IN.Tangent)); + #else + float3 tangent = IN.Tangent; + #endif + + OUT.Tangent = tangent; + #else + float3 diffuse = lt.x * G.Lamp0Color + max(-ndotl, 0) * G.Lamp1Color; + + OUT.Diffuse_Specular = float4(diffuse, lt.y * specularIntensity); + #endif + + OUT.PosLightSpace_Reflectance.xyz = getPosInLightSpace(posWorld); + + return OUT; +} + +#ifdef PIN_SURFACE +struct SurfaceInput +{ + float4 Color; + float2 Uv; + float2 UvStuds; + +#ifdef PIN_REFLECTION + float Reflectance; +#endif +}; + +struct Surface +{ + float3 albedo; + float3 normal; + float specular; + float gloss; + float reflectance; +}; + +Surface surfaceShader(SurfaceInput IN, float fade); + +Surface surfaceShaderExec(VertexOutput IN) +{ + SurfaceInput SIN; + SIN.Color = IN.Color; + SIN.Uv = IN.Uv_EdgeDistance1.xy; + SIN.UvStuds = IN.UvStuds_EdgeDistance2.xy; + + #ifdef PIN_REFLECTION + SIN.Reflectance = IN.PosLightSpace_Reflectance.w; + #endif + + float fade = saturate0(1 - IN.View_DepthMulFadeout.w); + + return surfaceShader(SIN, fade); +} +#endif + +sampler2D StudsMap: register(s0); +LGRID_SAMPLER LightMap: register(s1); +sampler2D LightMapLookup: register(s2); + +sampler2D DiffuseMap: register(s3); +sampler2D NormalMap: register(s4); +samplerCUBE EnvironmentMap: register(s5); + +sampler2D SpecularMap: register(s6); +sampler2D NormalDetailMap: register(s7); + +void DefaultPS(VertexOutput IN, +#ifdef PIN_GBUFFER + out float4 oColor1: COLOR1, +#endif + out float4 oColor0: COLOR0) +{ + // Compute albedo term +#ifdef PIN_SURFACE + Surface surface = surfaceShaderExec(IN); + + float4 albedo = float4(surface.albedo, IN.Color.a); + + float3 bitangent = cross(IN.Normal_SpecPower.xyz, IN.Tangent.xyz); + float3 normal = normalize(surface.normal.x * IN.Tangent.xyz + surface.normal.y * bitangent + surface.normal.z * IN.Normal_SpecPower.xyz); + + float ndotl = dot(normal, -G.Lamp0Dir); + + float3 diffuseIntensity = saturate0(ndotl) * G.Lamp0Color + max(-ndotl, 0) * G.Lamp1Color; + float specularIntensity = step(0, ndotl) * surface.specular; + float specularPower = surface.gloss; + + float reflectance = surface.reflectance; +#else + #ifdef PIN_PLASTIC + float4 studs = tex2D(StudsMap, IN.UvStuds_EdgeDistance2.xy); + float4 albedo = float4(IN.Color.rgb * 2 * studs.rgb, IN.Color.a); + #else + float4 albedo = tex2D(DiffuseMap, IN.Uv_EdgeDistance1.xy) * IN.Color; + #endif + + #ifdef PIN_HQ + float3 normal = normalize(IN.Normal_SpecPower.xyz); + float specularPower = IN.Normal_SpecPower.w; + #elif defined(PIN_REFLECTION) + float3 normal = IN.Normal_SpecPower.xyz; + #endif + + float3 diffuseIntensity = IN.Diffuse_Specular.xyz; + float specularIntensity = IN.Diffuse_Specular.w; + + #ifdef PIN_REFLECTION + float reflectance = IN.PosLightSpace_Reflectance.w; + #endif + +#endif + + float4 light = lgridSample(LightMap, LightMapLookup, IN.LightPosition_Fog.xyz); + + // Compute reflection term +#if defined(PIN_SURFACE) || defined(PIN_REFLECTION) + float3 reflection = texCUBE(EnvironmentMap, reflect(-IN.View_DepthMulFadeout.xyz, normal)).rgb; + + albedo.rgb = lerp(albedo.rgb, reflection.rgb, reflectance); +#endif + + float shadow = getBlobShadow(IN.PosLightSpace_Reflectance.xyz) * light.a; + + // Compute diffuse term + float3 diffuse = (G.AmbientColor + diffuseIntensity * shadow + light.rgb) * albedo.rgb; + + // Compute specular term +#ifdef PIN_HQ + float3 specular = G.Lamp0Color * (specularIntensity * shadow * (float)(half)pow(saturate(dot(normal, normalize(-G.Lamp0Dir + normalize(IN.View_DepthMulFadeout.xyz)))), specularPower)); +#else + float3 specular = G.Lamp0Color * (specularIntensity * shadow); +#endif + + // Combine + oColor0.rgb = diffuse.rgb + specular.rgb; + oColor0.a = albedo.a; + +#ifdef PIN_HQ + float outlineFade = saturate1(IN.View_DepthMulFadeout.w * G.OutlineBrightness_ShadowInfo.x + G.OutlineBrightness_ShadowInfo.y); + float2 minIntermediate = min(IN.Uv_EdgeDistance1.wz, IN.UvStuds_EdgeDistance2.wz); + float minEdgesPlus = min(minIntermediate.x, minIntermediate.y) / IN.View_DepthMulFadeout.w; + oColor0.rgb *= saturate1(outlineFade *(1.5 - minEdgesPlus) + minEdgesPlus); +#endif + + float fogAlpha = saturate(IN.LightPosition_Fog.w); + + oColor0.rgb = lerp(G.FogColor, oColor0.rgb, fogAlpha); + +#ifdef PIN_GBUFFER + oColor1 = gbufferPack(IN.View_DepthMulFadeout.w*G.FadeDistance.x, diffuse.rgb, specular.rgb, fogAlpha); +#endif +} diff --git a/Client2014/shaders/source/diamondplate.hlsl b/Client2014/shaders/source/diamondplate.hlsl new file mode 100644 index 0000000..6b0c8f8 --- /dev/null +++ b/Client2014/shaders/source/diamondplate.hlsl @@ -0,0 +1,21 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 2.7 +#define CFG_GLOSS_SCALE 256 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.5 + +#define CFG_SPECULAR_LOD 0.9 +#define CFG_GLOSS_LOD 160 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#include "material.hlsl" diff --git a/Client2014/shaders/source/fabric.hlsl b/Client2014/shaders/source/fabric.hlsl new file mode 100644 index 0000000..3dafd80 --- /dev/null +++ b/Client2014/shaders/source/fabric.hlsl @@ -0,0 +1,21 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 0.2 +#define CFG_GLOSS_SCALE 128 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.2 + +#define CFG_SPECULAR_LOD 0.03 +#define CFG_GLOSS_LOD 16 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#include "material.hlsl" diff --git a/Client2014/shaders/source/globals.h b/Client2014/shaders/source/globals.h new file mode 100644 index 0000000..2387b0b --- /dev/null +++ b/Client2014/shaders/source/globals.h @@ -0,0 +1,34 @@ +struct Globals +{ + float4x4 ViewProjection; + + float4 ViewRight; + float4 ViewUp; + float3 CameraPosition; + + float3 AmbientColor; + float3 Lamp0Color; + float3 Lamp0Dir; + float3 Lamp0Right; + float3 Lamp0Up; + float3 Lamp1Color; + + float3 FogColor; + float4 FogParams; + + float4 LightBorder; + float4 LightConfig0; + float4 LightConfig1; + float4 LightConfig2; + float4 LightConfig3; + + float3 FadeDistance; + float4 OutlineBrightness_ShadowInfo; + + float4 BlobShadowData0; + float4 BlobShadowData1; + float4 BlobShadowData2; + float4 BlobShadowData3; +}; + +uniform Globals G: register(c0); diff --git a/Client2014/shaders/source/granite.hlsl b/Client2014/shaders/source/granite.hlsl new file mode 100644 index 0000000..77c4aed --- /dev/null +++ b/Client2014/shaders/source/granite.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 0.5 +#define CFG_GLOSS_SCALE 128 +#define CFG_REFLECTION_SCALE 0.2 + +#define CFG_NORMAL_SHADOW_SCALE 0.1 + +#define CFG_SPECULAR_LOD 0.19 +#define CFG_GLOSS_LOD 24 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0.25 +#define CFG_FAR_DIFFUSE_CUTOFF 0.75 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#define CFG_OPT_NORMAL_CONST + +#include "material.hlsl" diff --git a/Client2014/shaders/source/grass.hlsl b/Client2014/shaders/source/grass.hlsl new file mode 100644 index 0000000..177cdb4 --- /dev/null +++ b/Client2014/shaders/source/grass.hlsl @@ -0,0 +1,21 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 1 +#define CFG_GLOSS_SCALE 256 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.5 + +#define CFG_SPECULAR_LOD 0.17 +#define CFG_GLOSS_LOD 18 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#include "material.hlsl" diff --git a/Client2014/shaders/source/ice.hlsl b/Client2014/shaders/source/ice.hlsl new file mode 100644 index 0000000..493698a --- /dev/null +++ b/Client2014/shaders/source/ice.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1.0 +#define CFG_SPECULAR_SCALE 1.2 +#define CFG_GLOSS_SCALE 256 +#define CFG_REFLECTION_SCALE 0.3 + +#define CFG_NORMAL_SHADOW_SCALE 0 + +#define CFG_SPECULAR_LOD 1 +#define CFG_GLOSS_LOD 190 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0.25 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0.75 + +#define CFG_OPT_DIFFUSE_CONST + +#include "material.hlsl" diff --git a/Client2014/shaders/source/marble.hlsl b/Client2014/shaders/source/marble.hlsl new file mode 100644 index 0000000..4447ca8 --- /dev/null +++ b/Client2014/shaders/source/marble.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 1.0 +#define CFG_GLOSS_SCALE 128 +#define CFG_REFLECTION_SCALE 0.2 + +#define CFG_NORMAL_SHADOW_SCALE 0.1 + +#define CFG_SPECULAR_LOD 0.7 +#define CFG_GLOSS_LOD 54 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#define CFG_OPT_NORMAL_CONST + +#include "material.hlsl" diff --git a/Client2014/shaders/source/material.hlsl b/Client2014/shaders/source/material.hlsl new file mode 100644 index 0000000..cce8c53 --- /dev/null +++ b/Client2014/shaders/source/material.hlsl @@ -0,0 +1,74 @@ +#define PIN_SURFACE +#include "default.hlsl" + +float4 sampleFar(sampler2D s, float2 uv, float fade, float cutoff) +{ +#ifdef GLSLES + return tex2D(s, uv); +#else + if (cutoff == 0) + return tex2D(s, uv); + else + { + float cscale = 1 / (1 - cutoff); + + return lerp(tex2D(s, uv * (CFG_FAR_TILING)), tex2D(s, uv), saturate0(fade * cscale - cutoff * cscale)); + } +#endif +} + +Surface surfaceShader(SurfaceInput IN, float fade) +{ + float2 uv = IN.Uv * (CFG_TEXTURE_TILING); + +#ifdef CFG_OPT_DIFFUSE_CONST + float4 diffuse = 1; +#else + float4 diffuse = sampleFar(DiffuseMap, uv, fade, CFG_FAR_DIFFUSE_CUTOFF); + + diffuse.rgb = lerp(float3(1, 1, 1), diffuse.rgb * (CFG_DIFFUSE_SCALE), fade); +#endif + +#ifdef CFG_OPT_NORMAL_CONST + float3 normal = float3(0, 0, 1); +#else + float3 normal = nmapUnpack(sampleFar(NormalMap, uv, fade, CFG_FAR_NORMAL_CUTOFF)); +#endif + +#ifndef GLSLES + float3 normalDetail = nmapUnpack(tex2D(NormalDetailMap, uv * (CFG_NORMAL_DETAIL_TILING))); + + normal.xy += normalDetail.xy * (CFG_NORMAL_DETAIL_SCALE); +#endif + + normal.xy *= fade; + + float shadowFactor = 1 + normal.x * (CFG_NORMAL_SHADOW_SCALE); + +#ifdef CFG_OPT_BLEND_COLOR + float3 albedo = lerp(float3(1, 1, 1), IN.Color.rgb, lerp(1, diffuse.a, fade)) * diffuse.rgb * shadowFactor; +#else + float3 albedo = IN.Color.rgb * diffuse.rgb * shadowFactor; +#endif + +#ifndef GLSLES + float4 studs = tex2D(StudsMap, IN.UvStuds); + + albedo *= studs.rgb * 2; +#endif + + float2 specular = sampleFar(SpecularMap, uv, fade, CFG_FAR_SPECULAR_CUTOFF).rg; + + // make sure glossiness is never 0 to avoid fp specials + float2 specbase = specular * float2(CFG_SPECULAR_SCALE, CFG_GLOSS_SCALE) + float2(0, 0.01); + float2 specfade = lerp(float2(CFG_SPECULAR_LOD, CFG_GLOSS_LOD), specbase, fade); + + Surface surface = (Surface)0; + surface.albedo = albedo; + surface.normal = normal; + surface.specular = specfade.r; + surface.gloss = specfade.g; + surface.reflectance = specular.g * fade * (CFG_REFLECTION_SCALE); + + return surface; +} diff --git a/Client2014/shaders/source/megacluster.hlsl b/Client2014/shaders/source/megacluster.hlsl new file mode 100644 index 0000000..e6bfb8a --- /dev/null +++ b/Client2014/shaders/source/megacluster.hlsl @@ -0,0 +1,150 @@ +#include "common.h" + +struct Appdata +{ + float4 Position : POSITION; + float3 Normal : NORMAL; + float4 Uv : TEXCOORD0; +#ifdef PIN_HQ + float4 EdgeDistances: TEXCOORD1; + float3 Tangent : TEXCOORD2; +#endif +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; + + float4 UvHigh_EdgeDistance1 : TEXCOORD0; + float4 UvLow_EdgeDistance2 : TEXCOORD1; + + float4 LightPosition_Fog : TEXCOORD2; + +#ifdef PIN_HQ + float4 View_Depth : TEXCOORD3; + float4 Normal_Blend : TEXCOORD4; + float3 Tangent : TEXCOORD5; +#else + float4 Diffuse_Blend : COLOR0; +#endif + + float3 PosLightSpace : TEXCOORD7; +}; + +uniform float4x4 WorldMatrix; + +VertexOutput MegaClusterVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + // Decode vertex data + IN.Normal = (IN.Normal - 127) / 127; + IN.Uv /= 2048; + + // Transform position and normal to world space + // Note: world matrix does not contain rotation/scale for static geometry so we can avoid transforming normal + float3 posWorld = mul(WorldMatrix, IN.Position).xyz; + float3 normalWorld = IN.Normal; + + OUT.HPosition = mul(G.ViewProjection, float4(posWorld, 1)); + + float blend = OUT.HPosition.w / 200; + + OUT.LightPosition_Fog = float4(lgridPrepareSample(lgridOffset(posWorld, normalWorld)), (G.FogParams.z - OUT.HPosition.w) * G.FogParams.w); + + OUT.UvHigh_EdgeDistance1.xy = IN.Uv.xy; + OUT.UvLow_EdgeDistance2.xy = IN.Uv.zw; + +#ifdef PIN_HQ + OUT.View_Depth = float4(posWorld, OUT.HPosition.w * G.FadeDistance.y); + float4 edgeDistances = IN.EdgeDistances*G.FadeDistance.z + 0.5 * OUT.View_Depth.w; + + OUT.UvHigh_EdgeDistance1.zw = edgeDistances.xy; + OUT.UvLow_EdgeDistance2.zw = edgeDistances.zw; + + OUT.View_Depth.xyz = G.CameraPosition - posWorld; + OUT.Normal_Blend = float4(IN.Normal, blend); + // decode tangent + OUT.Tangent = (IN.Tangent - 127) / 127; +#else + // IF LQ shading is performed in VS + float ndotl = dot(normalWorld, -G.Lamp0Dir); + float3 diffuse = saturate(ndotl) * G.Lamp0Color + max(-ndotl, 0) * G.Lamp1Color; + + OUT.Diffuse_Blend = float4(diffuse, blend); +#endif + + OUT.PosLightSpace = getPosInLightSpace(posWorld); + + return OUT; +} + +sampler2D DiffuseHighMap: register(s0); +sampler2D DiffuseLowMap: register(s1); +sampler2D NormalMap: register(s2); +sampler2D SpecularMap: register(s3); +LGRID_SAMPLER LightMap: register(s4); +sampler2D LightMapLookup: register(s5); + +void MegaClusterPS(VertexOutput IN, +#ifdef PIN_GBUFFER + out float4 oColor1: COLOR1, +#endif + out float4 oColor0: COLOR0) +{ + float4 high = tex2D(DiffuseHighMap, IN.UvHigh_EdgeDistance1.xy); + float4 low = tex2D(DiffuseLowMap, IN.UvLow_EdgeDistance2.xy); + + float4 light = lgridSample(LightMap, LightMapLookup, IN.LightPosition_Fog.xyz); + float shadow = getBlobShadow(IN.PosLightSpace) * light.a; + +#ifdef PIN_HQ + float3 albedo = lerp(high.rgb, low.rgb, saturate1(IN.Normal_Blend.a)); + + // sample normal map and specular map + float4 normalMapSample = tex2D(NormalMap, IN.UvHigh_EdgeDistance1.xy); + float4 specularMapSample = tex2D(SpecularMap, IN.UvHigh_EdgeDistance1.xy); + + // compute bitangent and world space normal + float3 bitangent = cross(IN.Normal_Blend.xyz, IN.Tangent.xyz); + float3 nmap = nmapUnpack(normalMapSample); + float3 normal = normalize(nmap.x * IN.Tangent.xyz + nmap.y * bitangent + nmap.z * IN.Normal_Blend.xyz); + + float ndotl = dot(normal, -G.Lamp0Dir); + float3 diffuseIntensity = saturate0(ndotl) * G.Lamp0Color + max(-ndotl, 0) * G.Lamp1Color; + float specularIntensity = step(0, ndotl) * specularMapSample.r; + float specularPower = specularMapSample.g * 255 + 0.01; + + // Compute diffuse and specular and combine them + float3 diffuse = (G.AmbientColor + diffuseIntensity * shadow + light.rgb) * albedo.rgb; + float3 specular = G.Lamp0Color * (specularIntensity * shadow * (float)(half)pow(saturate(dot(normal, normalize(-G.Lamp0Dir + normalize(IN.View_Depth.xyz)))), specularPower)); + oColor0.rgb = diffuse + specular; + + // apply outlines + float outlineFade = saturate1(IN.View_Depth.w * G.OutlineBrightness_ShadowInfo.x + G.OutlineBrightness_ShadowInfo.y); + float2 minIntermediate = min(IN.UvHigh_EdgeDistance1.wz, IN.UvLow_EdgeDistance2.wz); + float minEdgesPlus = min(minIntermediate.x, minIntermediate.y) / IN.View_Depth.w; + oColor0.rgb *= saturate1(outlineFade * (1.5 - minEdgesPlus) + minEdgesPlus); + + oColor0.a = 1; + +#else + float3 albedo = lerp(high.rgb, low.rgb, saturate1(IN.Diffuse_Blend.a)); + + // Compute diffuse term + float3 diffuse = (G.AmbientColor + IN.Diffuse_Blend.rgb * shadow + light.rgb) * albedo.rgb; + + // Combine + oColor0.rgb = diffuse; + oColor0.a = 1; + +#endif + + float fogAlpha = saturate(IN.LightPosition_Fog.w); + + oColor0.rgb = lerp(G.FogColor, oColor0.rgb, fogAlpha); + +#ifdef PIN_GBUFFER + oColor1 = gbufferPack(IN.View_Depth.w*G.FadeDistance.x, diffuse.rgb, 0, fogAlpha); +#endif +} diff --git a/Client2014/shaders/source/msaa.hlsl b/Client2014/shaders/source/msaa.hlsl new file mode 100644 index 0000000..3cdf77d --- /dev/null +++ b/Client2014/shaders/source/msaa.hlsl @@ -0,0 +1,40 @@ +#include "common.h" + +struct Appdata +{ + float4 p : POSITION; + float2 uv : TEXCOORD0; +}; + +struct VertexOutput +{ + float4 p : POSITION; + float2 uv : TEXCOORD0; +}; + + +float4 convertPosition(float4 p, float scale) +{ + return p; +} + +float2 convertUv(float4 p) +{ + return p.xy * 0.5 + 0.5; +} + +VertexOutput msaaComposit_vs(Appdata IN) +{ + float2 uv = convertUv(IN.p); + + VertexOutput OUT; + OUT.p = convertPosition(IN.p, 1); + OUT.uv = uv; + + return OUT; +} + +float4 msaaComposit_ps(float2 uv : TEXCOORD0, uniform sampler2D colorMap: register(s0)): COLOR0 +{ + return tex2D(colorMap, uv); +} diff --git a/Client2014/shaders/source/particle.hlsl b/Client2014/shaders/source/particle.hlsl new file mode 100644 index 0000000..b38cadd --- /dev/null +++ b/Client2014/shaders/source/particle.hlsl @@ -0,0 +1,84 @@ +#include "globals.h" + +sampler2D tex : register(s0); + +//#define + +uniform float4 colorBias; +uniform float4 throttleFactor; // .x = alpha cutoff, .y = alpha boost (clamp) + +struct VS_INPUT +{ + float4 pos : POSITION; + float4 scaleRotLife : TEXCOORD0; // transform matrix + float2 disp : TEXCOORD1; // .xy = corner, either (0,0), (1,0), (0,1), or (1,1) + float4 color0: COLOR0; + float4 color1: COLOR1; +}; + +struct VS_OUTPUT +{ + float4 pos : POSITION; + float3 uvFog : TEXCOORD0; + float4 color : COLOR0; +}; + +float4 rotScale( float4 scaleRotLife ) +{ + float cr = cos( scaleRotLife.z ); + float sr = sin( scaleRotLife.z ); + + float4 r; + r.x = cr * scaleRotLife.x; + r.y = -sr * scaleRotLife.x; + r.z = sr * scaleRotLife.y; + r.w = cr * scaleRotLife.y; + + return r; +} + + +VS_OUTPUT vs( VS_INPUT input ) +{ + VS_OUTPUT o; + + float4 pos = float4( input.pos.xyz, 1 ); + float2 disp = input.disp.xy * 2 - 1; // -1..1 + + input.scaleRotLife *= float4( 1/256.0f, 1/256.0f, 2 * 3.1415926f / 32767, 1 / 32767.0f ); + + float4 rs = rotScale( input.scaleRotLife ); + + pos += G.ViewRight * dot( disp, rs.xy ); + pos += G.ViewUp * dot( disp, rs.zw ); + o.pos = mul( G.ViewProjection, pos ); + + o.uvFog.xy = input.disp.xy; + o.uvFog.y = 1 - o.uvFog.y; + o.uvFog.z = (G.FogParams.z - o.pos.w) * G.FogParams.w; + float t = max( 0, min(1, input.scaleRotLife.w ) ); + o.color = lerp( input.color1, input.color0, t ); + + // alpha channel magic for particle throttling + float2 cmp = o.color.aa < throttleFactor.xy; + o.pos.xyz += cmp.xxx * (1e10f).xxx; // move the particle off-screen + o.color.a = lerp( o.color.a, throttleFactor.y, cmp.y ); // if below threshold, alpha = threshold + + return o; +} + +float4 psAdd( VS_OUTPUT input ) : COLOR0 // #0 +{ + float4 color = tex2D( tex, input.uvFog.xy ); + float4 result = float4( input.color.rgb + color.rgb + colorBias.rgb, color.a * input.color.a ); + result.rgb = lerp( G.FogColor.rgb, result.rgb, saturate( input.uvFog.zzz ) ); + return result; +} + +float4 psMul( VS_OUTPUT input ) : COLOR0 // #1 +{ + float4 color = tex2D( tex, input.uvFog.xy ); + float4 result = input.color * color; + result.rgb = lerp( G.FogColor.rgb, result.rgb, saturate( input.uvFog.zzz ) ); + return result; +} diff --git a/Client2014/shaders/source/pebble.hlsl b/Client2014/shaders/source/pebble.hlsl new file mode 100644 index 0000000..9e35fe9 --- /dev/null +++ b/Client2014/shaders/source/pebble.hlsl @@ -0,0 +1,21 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 2.5 +#define CFG_GLOSS_SCALE 128 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0 + +#define CFG_SPECULAR_LOD 0.15 +#define CFG_GLOSS_LOD 22 + +#define CFG_NORMAL_DETAIL_TILING 6 +#define CFG_NORMAL_DETAIL_SCALE 1.5 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#include "material.hlsl" diff --git a/Client2014/shaders/source/plastic.hlsl b/Client2014/shaders/source/plastic.hlsl new file mode 100644 index 0000000..8320b1d --- /dev/null +++ b/Client2014/shaders/source/plastic.hlsl @@ -0,0 +1,46 @@ +#if defined(PIN_HQ) +#define PIN_SURFACE +#include "default.hlsl" + +#define CFG_TEXTURE_TILING 1 + +#define CFG_BUMP_INTENSITY 0.5 + +#define CFG_SPECULAR 0.4 +#define CFG_GLOSS 9 + +#define CFG_NORMAL_SHADOW_SCALE 0.1 + +Surface surfaceShader(SurfaceInput IN, float fade) +{ + float4 studs = tex2D(DiffuseMap, IN.UvStuds); + float3 normal = nmapUnpack(tex2D(NormalMap, IN.UvStuds)); + + float3 noise = nmapUnpack(tex2D(NormalDetailMap, IN.Uv * (CFG_TEXTURE_TILING))); + + float noiseScale = saturate0(IN.Color.a * 2 * (CFG_BUMP_INTENSITY) - 1 * (CFG_BUMP_INTENSITY)); + +#ifdef PIN_REFLECTION + noiseScale *= saturate(1 - 2 * IN.Reflectance); +#endif + + normal.xy += noise.xy * noiseScale; + + normal.xy *= fade; + + Surface surface = (Surface)0; + surface.albedo = IN.Color.rgb * studs.rgb * 2; + surface.normal = normal; + surface.specular = (CFG_SPECULAR); + surface.gloss = (CFG_GLOSS); + +#ifdef PIN_REFLECTION + surface.reflectance = IN.Reflectance; +#endif + + return surface; +} +#else +#define PIN_PLASTIC +#include "default.hlsl" +#endif diff --git a/Client2014/shaders/source/rust.hlsl b/Client2014/shaders/source/rust.hlsl new file mode 100644 index 0000000..4a9a736 --- /dev/null +++ b/Client2014/shaders/source/rust.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 1 +#define CFG_GLOSS_SCALE 256 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.5 + +#define CFG_SPECULAR_LOD 0.35 +#define CFG_GLOSS_LOD 103 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0.5 +#define CFG_FAR_DIFFUSE_CUTOFF 0.75 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#define CFG_OPT_BLEND_COLOR + +#include "material.hlsl" diff --git a/Client2014/shaders/source/sand.hlsl b/Client2014/shaders/source/sand.hlsl new file mode 100644 index 0000000..f64f6c1 --- /dev/null +++ b/Client2014/shaders/source/sand.hlsl @@ -0,0 +1,21 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 0.4 +#define CFG_GLOSS_SCALE 32 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0 + +#define CFG_SPECULAR_LOD 0.07 +#define CFG_GLOSS_LOD 6 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#include "material.hlsl" diff --git a/Client2014/shaders/source/shadowextrude.hlsl b/Client2014/shaders/source/shadowextrude.hlsl new file mode 100644 index 0000000..ea26762 --- /dev/null +++ b/Client2014/shaders/source/shadowextrude.hlsl @@ -0,0 +1,41 @@ +#include "common.h" + +struct Appdata +{ + float4 Position : POSITION; + + int4 BoneIndices : COLOR0; +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; +}; + +uniform float ShadowExtrusionDistance; + +uniform float4 WorldMatrixArray[MAX_BONE_COUNT * 3]; + +VertexOutput DefaultVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + int boneIndex = IN.BoneIndices.r; + + float4 worldRow0 = WorldMatrixArray[boneIndex * 3 + 0]; + float4 worldRow1 = WorldMatrixArray[boneIndex * 3 + 1]; + float4 worldRow2 = WorldMatrixArray[boneIndex * 3 + 2]; + + float3 posWorld = float3(dot(worldRow0, IN.Position), dot(worldRow1, IN.Position), dot(worldRow2, IN.Position)); + + float3 extrusion = G.Lamp0Dir * (ShadowExtrusionDistance * IN.BoneIndices.g); + + OUT.HPosition = mul(G.ViewProjection, float4(posWorld + extrusion, 1)); + + return OUT; +} + +float4 DefaultPS(): COLOR0 +{ + return float4(0, 0, 0, 1); +} diff --git a/Client2014/shaders/source/shadowquad.hlsl b/Client2014/shaders/source/shadowquad.hlsl new file mode 100644 index 0000000..30709ca --- /dev/null +++ b/Client2014/shaders/source/shadowquad.hlsl @@ -0,0 +1,28 @@ +#include "globals.h" + +struct Appdata +{ + float4 Position : POSITION; +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; +}; + +uniform float4 Color; + +VertexOutput QuadVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + OUT.HPosition = IN.Position; + OUT.HPosition.z = 0.5; // force set depth to avoid depth clipping + + return OUT; +} + +float4 QuadPS(): COLOR0 +{ + return Color; +} diff --git a/Client2014/shaders/source/sky.hlsl b/Client2014/shaders/source/sky.hlsl new file mode 100644 index 0000000..088427f --- /dev/null +++ b/Client2014/shaders/source/sky.hlsl @@ -0,0 +1,50 @@ +#include "globals.h" + +struct Appdata +{ + float4 Position : POSITION; + float2 Uv : TEXCOORD0; + float4 Color : COLOR0; +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; + float PSize : PSIZE; + + float2 Uv : TEXCOORD0; + float4 Color : COLOR0; +}; + +uniform float4x4 WorldMatrix; + +uniform float4 Color; +uniform float4 Color2; + +VertexOutput SkyVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + float4 wpos = mul(WorldMatrix, IN.Position); + + OUT.HPosition = mul(G.ViewProjection, wpos); + + // snap to far plane to prevent scene-sky intersections + // small offset is needed to prevent 0/0 division in case w=0, which causes rasterization issues + OUT.HPosition.z = OUT.HPosition.w - 1.f / 16; + + OUT.PSize = 2.0; // star size + + OUT.Uv = IN.Uv; + OUT.Color = IN.Color * lerp(Color2,Color,wpos.y/1700); + //OUT.Color = IN.Color * Color; + + return OUT; +} + +sampler2D DiffuseMap: register(s0); + +float4 SkyPS(VertexOutput IN): COLOR0 +{ + return tex2D(DiffuseMap, IN.Uv) * IN.Color; +} diff --git a/Client2014/shaders/source/slate.hlsl b/Client2014/shaders/source/slate.hlsl new file mode 100644 index 0000000..cd135ba --- /dev/null +++ b/Client2014/shaders/source/slate.hlsl @@ -0,0 +1,21 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 0.9 +#define CFG_GLOSS_SCALE 128 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.5 + +#define CFG_SPECULAR_LOD 0.14 +#define CFG_GLOSS_LOD 20 + +#define CFG_NORMAL_DETAIL_TILING 5 +#define CFG_NORMAL_DETAIL_SCALE 1 + +#define CFG_FAR_TILING 0.25 +#define CFG_FAR_DIFFUSE_CUTOFF 0.75 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#include "material.hlsl" diff --git a/Client2014/shaders/source/smoothplastic.hlsl b/Client2014/shaders/source/smoothplastic.hlsl new file mode 100644 index 0000000..fb0aa90 --- /dev/null +++ b/Client2014/shaders/source/smoothplastic.hlsl @@ -0,0 +1,2 @@ +#define PIN_PLASTIC +#include "default.hlsl" diff --git a/Client2014/shaders/source/ssao.hlsl b/Client2014/shaders/source/ssao.hlsl new file mode 100644 index 0000000..e782e1a --- /dev/null +++ b/Client2014/shaders/source/ssao.hlsl @@ -0,0 +1,447 @@ +#include "common.h" + +// tweakables +#define SSAO_NUM_PAIRS 8 +#define SSAO_SPHERE_RAD 2.0f // world-space +#define SSAO_MIN_PIXEL_RANGE 10.0f +#define SSAO_MAX_PIXEL_RANGE 100.0f +#define BLUR_DEPTH_DELTA 0.4f + +#define COMPOSITE_DEPTH_DELTA 0.02f +#define COMPOSITE_DEPTH_DELTA2 0.4f + +struct Appdata +{ + float4 p : POSITION; + float2 uv : TEXCOORD0; +}; + +struct VertexOutput +{ + float4 p : POSITION; + float2 uv : TEXCOORD0; +}; + + +// .xy = gbuffer width/height, .zw = inverse gbuffer width/height +uniform float4 TextureSize; + +#ifdef GLSL +float4 convertPosition(float4 p, float scale) +{ + return p; +} + +float2 convertUv(float4 p) +{ + return p.xy * 0.5 + 0.5; +} +#else +float4 convertPosition(float4 p, float scale) +{ + // half-pixel offset + return p + float4(-TextureSize.z, TextureSize.w, 0, 0) * scale; +} + +float2 convertUv(float4 p) +{ + return p.xy * float2(0.5, -0.5) + 0.5; +} +#endif + +VertexOutput ssao_vs(Appdata IN) +{ + float2 uv = convertUv(IN.p); + + VertexOutput OUT; + OUT.p = convertPosition(IN.p, 1); + OUT.uv = uv; + + return OUT; +} + +// used for depth downsampling pass +struct VertexOutput_4uv +{ + float4 p : POSITION; + float2 uv : TEXCOORD0; + float4 uv12 : TEXCOORD1; + float4 uv34 : TEXCOORD2; +}; + +VertexOutput_4uv ssaoDepthDown_vs(Appdata IN) +{ + float2 uv = convertUv(IN.p); + + VertexOutput_4uv OUT; + OUT.p = convertPosition(IN.p, 2); + OUT.uv = uv; + + float2 uvOffset = TextureSize.zw * 0.5f; + + OUT.uv12.xy = uv + uvOffset * float2(-1, -1); + OUT.uv12.zw = uv + uvOffset * float2(+1, -1); + OUT.uv34.xy = uv + uvOffset * float2(-1, +1); + OUT.uv34.zw = uv + uvOffset * float2(+1, +1); + + return OUT; +} + +struct VertexOutput_8uv +{ + float4 p : POSITION; + float2 uv : TEXCOORD0; + float4 uv12 : TEXCOORD1; + float4 uv34 : TEXCOORD2; + float4 uv56 : TEXCOORD3; + float4 uv78 : TEXCOORD4; +}; + +// used for ssao blurring passes +VertexOutput_8uv ssaoBlur_vs(Appdata IN, float2 uvOffset) +{ + float2 uv = convertUv(IN.p); + + VertexOutput_8uv OUT; + OUT.p = convertPosition(IN.p, 2); + OUT.uv = uv; + + OUT.uv12.xy = uv + 1 * uvOffset; + OUT.uv12.zw = uv + 2 * uvOffset; + OUT.uv34.xy = uv + 3 * uvOffset; + OUT.uv34.zw = uv + 4 * uvOffset; + + OUT.uv56.xy = uv - 1 * uvOffset; + OUT.uv56.zw = uv - 2 * uvOffset; + OUT.uv78.xy = uv - 3 * uvOffset; + OUT.uv78.zw = uv - 4 * uvOffset; + + return OUT; +} + +VertexOutput_8uv ssaoBlurX_vs(Appdata IN) +{ + return ssaoBlur_vs(IN, float2(TextureSize.z * 2, 0)); +} + +VertexOutput_8uv ssaoBlurY_vs(Appdata IN) +{ + return ssaoBlur_vs(IN, float2(0, TextureSize.w * 2)); +} + +float unpackDepth( sampler2D s, float2 uv ) +{ + float4 geomTex = tex2D(s, uv); + float d = geomTex.z * (1.0f/256.0f) + geomTex.w; + return d; +} + +float getDepth( sampler2D s, float2 uv ) +{ + return (float)tex2D(s,uv).r; +} + +#define NUM_PAIRS SSAO_NUM_PAIRS +#define RANGE 60.0/1024.0 + +#define pi 3.14159265359 +#define RAD(X) ( (X) * (pi/180) ) + +float2 GetRotatedSample(float i) +{ + return (i+1) / (NUM_PAIRS+2) * float2(cos( RAD(45) + i / NUM_PAIRS * 2 * pi ), sin( RAD(45) + i / NUM_PAIRS * 2 * pi ) ); +} + +#define NUM_SAMPLES NUM_PAIRS*2+1 + +float4 ssao_ps( + float2 uv: TEXCOORD0, + uniform sampler2D depthBuffer: register(s0), + uniform sampler2D randMap: register(s1)): COLOR0 +{ + float2 mapSize = TextureSize.xy / 2; + + float baseDepth = getDepth( depthBuffer, uv ); + + float4 noiseTex = tex2D(randMap, uv*mapSize/4) * 2 - 1; + + float2x2 rotation = + { + { noiseTex.y, noiseTex.x }, + { -noiseTex.x, noiseTex.y } + }; + + float2 OFFSETS1[NUM_PAIRS] = + { + GetRotatedSample(0), + GetRotatedSample(1), + GetRotatedSample(2), + GetRotatedSample(3), + GetRotatedSample(4), + GetRotatedSample(5), +#if NUM_PAIRS > 6 + GetRotatedSample(6), + GetRotatedSample(7), +#if NUM_PAIRS > 8 + GetRotatedSample(8), + GetRotatedSample(9), + GetRotatedSample(10), + GetRotatedSample(11), +#endif +#endif + }; + + float occ = 1; + + float sphereRadiusZB = (float) ( 2.0f / GBUFFER_MAX_DEPTH ); + +#define MINPIXEL SSAO_MIN_PIXEL_RANGE +#define MAXPIXEL SSAO_MAX_PIXEL_RANGE + + float radiusTex = (float)clamp( 0.5*sphereRadiusZB / baseDepth, MINPIXEL / mapSize.x, MAXPIXEL / mapSize.y); + + float numSamples = 2; + + for(int i = 0; i < NUM_PAIRS; i++) + { + float2 offset1 = mul(rotation, OFFSETS1[i]); + + float2 offseted1 = uv + offset1 * radiusTex; + float2 offseted2 = uv - offset1 * radiusTex; + + float2 offsetDepth; + offsetDepth.x = getDepth( depthBuffer, offseted1 ); + offsetDepth.y = getDepth( depthBuffer, offseted2 ); + + float2 diff = offsetDepth - baseDepth.xx; + + float normalizedOffsetLen = (float)(i+1)/(NUM_PAIRS+2); + + float segmentDiff = (float) ( 1.5f*sphereRadiusZB*sqrt(1-normalizedOffsetLen*normalizedOffsetLen) ); + + float2 normalizedDiff = (diff / segmentDiff) + 0.5; + + float minDiff = min(normalizedDiff.x, normalizedDiff.y); + + // At 0, full sample + // At -1, zero sample, zero weight + + float sampleadd = (float) saturate(1+minDiff); + + float a = (float)(saturate(normalizedDiff.x) + saturate(normalizedDiff.y))*sampleadd; + occ += a; + numSamples += 2 * sampleadd; + } + + occ = occ / numSamples; + + float finalocc = (float)saturate(occ*2); + + if(baseDepth - (1.0f-1/256.0f) > 0) + finalocc += 1; + + return float4(finalocc, finalocc, finalocc, 1); +} + +// this function estimates depth discrepancy tolerance for the blur filter +float depthTolerance( float baseDepth, float sphereRadiusZB ) +{ + float ramp = 80; // tweak + return ( clamp( sphereRadiusZB * (baseDepth * ramp) , 0.1f * sphereRadiusZB, 40*sphereRadiusZB ) ); +} + +float ssaoBlur( + float2 uv, + + float4 uv12, + float4 uv34, + float4 uv56, + float4 uv78, + + sampler2D map, + sampler2D depthBuffer + ) +{ + float sphereRadiusZB = BLUR_DEPTH_DELTA / GBUFFER_MAX_DEPTH; + float4 i = { 1, 2, 3, 4 }; + float4 iw = 4-i; + float4 denom = 1; + + + float4 sum = tex2D(map, uv).rrrr * denom; + + float baseDepth = getDepth( depthBuffer, uv ); + + float4 newDepth, delta, ssample, coef; + + newDepth.x = getDepth( depthBuffer, uv12.xy ); + newDepth.y = getDepth( depthBuffer, uv12.zw ); + newDepth.z = getDepth( depthBuffer, uv34.xy ); + newDepth.w = getDepth( depthBuffer, uv34.zw ); + + delta = (newDepth - baseDepth.xxxx); + coef = iw * ( abs(delta) < depthTolerance( baseDepth, sphereRadiusZB ).xxxx ); + + + ssample.x = tex2D( map, uv12.xy ).r; + ssample.y = tex2D( map, uv12.zw ).r; + ssample.z = tex2D( map, uv34.xy ).r; + ssample.w = tex2D( map, uv34.zw ).r; + + sum += ssample * coef; + denom += coef; + + //////////////////////////////////////// + + newDepth.x = getDepth( depthBuffer, uv56.xy ); + newDepth.y = getDepth( depthBuffer, uv56.zw ); + newDepth.z = getDepth( depthBuffer, uv78.xy ); + newDepth.w = getDepth( depthBuffer, uv78.zw ); + + delta = newDepth - baseDepth.xxxx; + coef = iw * ( abs(delta) < depthTolerance( baseDepth, sphereRadiusZB ).xxxx ); + + ssample.x = tex2D( map, uv56.xy ).r; + ssample.y = tex2D( map, uv56.zw ).r; + ssample.z = tex2D( map, uv78.xy ).r; + ssample.w = tex2D( map, uv78.zw ).r; + + sum += ssample * coef; + denom += coef; + + return dot( sum, float4(1,1,1,1) ) / dot( denom, float4(1,1,1,1) ); +} + + +float4 ssaoBlurX_ps( + float2 uv : TEXCOORD0, + float4 uv12 : TEXCOORD1, + float4 uv34 : TEXCOORD2, + float4 uv56 : TEXCOORD3, + float4 uv78 : TEXCOORD4, + + uniform sampler2D map : register(s0), uniform sampler2D depthBuffer : register(s1) ): COLOR0 +{ + //return tex2D( map, uv ); + float ssaoTerm = ssaoBlur( uv, uv12, uv34, uv56, uv78, map, depthBuffer); + + return float4(ssaoTerm.xxx, 1); +} + +#define SPECULAR_WEIGHT 3 + + +float4 ssaoBlurY_ps( + float2 uv : TEXCOORD0, + float4 uv12 : TEXCOORD1, + float4 uv34 : TEXCOORD2, + float4 uv56 : TEXCOORD3, + float4 uv78 : TEXCOORD4, + + uniform sampler2D map : register(s0), uniform sampler2D depthBuffer : register(s1), uniform sampler2D geomMap : register(s2) ): COLOR0 +{ + float ssaoTerm = ssaoBlur(uv, uv12, uv34, uv56, uv78, map, depthBuffer); + + float4 geom = tex2D(geomMap, uv); + + float specular = geom.x; + float diffuse = geom.y; + + // Making specular kill SSAO faster, so it doesn't get capped by 1 + return (SPECULAR_WEIGHT*specular + diffuse * ssaoTerm) / (SPECULAR_WEIGHT*specular + diffuse + 0.001); +} + + + +float4 ssaoDepthDown_ps( + float2 uv : TEXCOORD0, + float4 uv12 : TEXCOORD1, + float4 uv34 : TEXCOORD2, + + uniform sampler2D depthBuffer : register(s0) +) : COLOR0 +{ + + float4 d; + d.x = unpackDepth( depthBuffer, uv12.xy ); + d.y = unpackDepth( depthBuffer, uv12.zw ); + d.z = unpackDepth( depthBuffer, uv34.xy ); + d.w = unpackDepth( depthBuffer, uv34.zw ); + + float2 tmp = min( d.xy, d.zw ); + return min( tmp.x, tmp.y ).x; +} + +VertexOutput_4uv ssaoComposit_vs(Appdata IN) +{ + float2 uv = convertUv(IN.p); + + VertexOutput_4uv OUT; + OUT.p = convertPosition(IN.p, 1); + OUT.uv = uv; + + float2 uvOffset = TextureSize.zw * 2; + + OUT.uv12.xy = uv + float2(uvOffset.x, 0); + OUT.uv12.zw = uv - float2(uvOffset.x, 0); + OUT.uv34.xy = uv + float2(0, uvOffset.y); + OUT.uv34.zw = uv - float2(0, uvOffset.y); + + return OUT; +} + +float4 ssaoCompositBlank_ps(float2 uv : TEXCOORD0, uniform sampler2D colorMap: register(s0)): COLOR0 +{ + return tex2D(colorMap, uv); +} + +#define CMP_LESS(X,Y) ( (X) < (Y) ) + +float4 ssaoComposit_ps( + float2 uv : TEXCOORD0, + float4 uv12 : TEXCOORD1, + float4 uv34 : TEXCOORD2, + + uniform sampler2D colorMap : register(s0), + uniform sampler2D map : register(s1), + uniform sampler2D gbuffer : register(s2), + uniform sampler2D depthBuffer: register(s3) + ): COLOR0 +{ + //return float4(1,0,0,1); + float depth_range = COMPOSITE_DEPTH_DELTA / GBUFFER_MAX_DEPTH; + float depth_range2 = COMPOSITE_DEPTH_DELTA2 / GBUFFER_MAX_DEPTH; + + // we're here + float baseDepth = unpackDepth( gbuffer, uv ); + float ssaoTerm = 1.0f; + + float depth = getDepth( depthBuffer, uv ); + float diff = abs( depth - baseDepth ); + ssaoTerm = tex2D( map, uv ).x; + + float chk1 = CMP_LESS( depth_range, diff ); // can we trust the base depth? 0 - yes, 1 - no + float4 ssaoTermNew = 0, chk2, depth2, diff2; + + depth2.x = getDepth( depthBuffer, uv12.xy ); + depth2.y = getDepth( depthBuffer, uv12.zw ); + depth2.z = getDepth( depthBuffer, uv34.xy ); + depth2.w = getDepth( depthBuffer, uv34.zw ); + + ssaoTermNew.x = tex2D( map, uv12.xy ).x; + ssaoTermNew.y = tex2D( map, uv12.zw ).x; + ssaoTermNew.z = tex2D( map, uv34.xy ).x; + ssaoTermNew.w = tex2D( map, uv34.zw ).x; + + diff2 = abs( depth2 - baseDepth.xxxx ); + chk2 = CMP_LESS( diff2, depth_range2.xxxx ); + + ssaoTermNew *= chk2; + float den = dot( chk2, 1 ); // + 1e-5f; - TODO: add this if we encounter glitches; // + ssaoTermNew.x = dot( ssaoTermNew, 1 ) / den; + + // the final decision: pick the base sample or its estimate, if base depth in unauthorative + ssaoTerm = saturate(den*chk1) ? ssaoTermNew.x : ssaoTerm; + + return tex2D(colorMap, uv) * ssaoTerm; +} diff --git a/Client2014/shaders/source/texcomp.hlsl b/Client2014/shaders/source/texcomp.hlsl new file mode 100644 index 0000000..a333e2c --- /dev/null +++ b/Client2014/shaders/source/texcomp.hlsl @@ -0,0 +1,39 @@ +#include "globals.h" + +struct Appdata +{ + float4 Position : POSITION; + float2 Uv : TEXCOORD0; +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; + float2 Uv : TEXCOORD0; +}; + +VertexOutput TexCompVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + OUT.HPosition = mul(G.ViewProjection, IN.Position); + OUT.Uv = IN.Uv; + + return OUT; +} + +sampler2D DiffuseMap: register(s0); + +uniform float4 Color; + +float4 TexCompPS(VertexOutput IN): COLOR0 +{ + return tex2Dbias(DiffuseMap, float4(IN.Uv, 0, -10)) * Color; +} + +float4 TexCompPMAPS(VertexOutput IN): COLOR0 +{ + float4 tex = tex2Dbias(DiffuseMap, float4(IN.Uv, 0, -10)); + + return float4(tex.rgb * tex.a * Color.rgb, tex.a * Color.a); +} diff --git a/Client2014/shaders/source/ui.hlsl b/Client2014/shaders/source/ui.hlsl new file mode 100644 index 0000000..ea3281f --- /dev/null +++ b/Client2014/shaders/source/ui.hlsl @@ -0,0 +1,50 @@ +#include "globals.h" + +struct Appdata +{ + float4 Position : POSITION; + float2 Uv : TEXCOORD0; + float4 Color : COLOR0; +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; + + float2 Uv : TEXCOORD0; + float4 Color : COLOR0; + +#if defined(PIN_FOG) + float FogFactor : TEXCOORD1; +#endif +}; + +VertexOutput UIVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + OUT.HPosition = mul(G.ViewProjection, IN.Position); + + OUT.Uv = IN.Uv; + OUT.Color = IN.Color; + +#if defined(PIN_FOG) + OUT.FogFactor = (G.FogParams.z - OUT.HPosition.w) * G.FogParams.w; +#endif + + return OUT; +} + +sampler2D DiffuseMap: register(s0); + +float4 UIPS(VertexOutput IN): COLOR0 +{ + float4 base = tex2D(DiffuseMap, IN.Uv); + float4 result = IN.Color * base; + +#if defined(PIN_FOG) + result.rgb = lerp(G.FogColor, result.rgb, saturate(IN.FogFactor)); +#endif + + return result; +} diff --git a/Client2014/shaders/source/water.hlsl b/Client2014/shaders/source/water.hlsl new file mode 100644 index 0000000..c596b04 --- /dev/null +++ b/Client2014/shaders/source/water.hlsl @@ -0,0 +1,220 @@ + +// +// Water shader. +// Big, fat and ugly. +// +// All (most) things considered, I have converged to this particular way of rendering water: +// +// Vertex waves +// No transparency. Solid color for deep water. +// Fresnel law, reflects environment. +// Phong speculars. +// Ripples via animated normal map. Adjustable intensity, speed and scale. Affect reflection and speculars. + +#include "common.h" + +uniform float4x4 WorldMatrix; + +uniform float4 nmAnimLerp; // ratio between normal map frames +uniform float4 waveParams; // .x = frequency .y = phase .z = height +uniform float4 WaterColor; // deep water color + +#ifdef PIN_HQ +# define WATER_LOD 1 +#else +# define WATER_LOD 2 +#endif + +#define LODBIAS (-1) + +float fadeFactor( float3 wspos ) +{ + return saturate( -0.4f + 1.4f*length( G.CameraPosition.xyz - wspos.xyz ) * G.FadeDistance.y ); +} + +float wave( float4 wspos ) +{ + float x = sin( ( wspos.z - wspos.x - waveParams.y ) * waveParams.x ); + float z = sin( ( wspos.z + wspos.x + waveParams.y ) * waveParams.x ); + float p = (x + z) * waveParams.z; + return p - p * fadeFactor( wspos.xyz ); +} + + + +// perturbs the water mesh and vertex normals +void makeWaves( inout float4 wspos, inout float3 wsnrm ) +{ +#if WATER_LOD == 0 + float gridSize = 4.0f; + + float4 wspos1 = wspos; + float4 wspos2 = wspos; + + wspos1.x += gridSize; + wspos2.z += gridSize; + + wspos.y += wave(wspos) ; + wspos1.y += wave(wspos1); + wspos2.y += wave(wspos2); + + wsnrm = normalize( cross( wspos2.xyz - wspos.xyz, wspos1.xyz - wspos.xyz ) ); +#elif WATER_LOD == 1 + wspos.y += wave( wspos ); +#else /* do n0thing */ +#endif +} + +struct V2P +{ + float4 pos : POSITION; + float4 tc0Fog : TEXCOORD0; + float4 wspos : TEXCOORD1; + float3 wsnrm : TEXCOORD2; + float3 light : TEXCOORD3; + float3 fade : TEXCOORD4; +}; + +V2P water_vs( + float4 pos : POSITION, + float3 nrm : NORMAL +) +{ + V2P o; + + // Decode vertex data + nrm = (nrm - 127) / 127; + + nrm = normalize(nrm); + + float4 wspos = mul( WorldMatrix, pos ); + float3 wsnrm = nrm; + + wspos.y -= 2*waveParams.z; + + makeWaves( /*INOUT*/ wspos, /*INOUT*/ wsnrm ); + + o.wspos = wspos; + o.wsnrm = wsnrm; + + if( nrm.y < 0.01f ) o.wsnrm = nrm; + + // box mapping + //float3x2 m = { wspos.xz, wspos.xy, wspos.yz }; + //float2 tcselect = mul( abs( nrm.yzx ), m ); + + float2 tcselect; + float3 wspostc = float3( wspos.x, -wspos.y, wspos.z ); + + tcselect.x = dot( abs( nrm.yxz ), wspostc.xzx ); + tcselect.y = dot( abs( nrm.yxz ), wspostc.zyy ); + + o.pos = mul( G.ViewProjection, wspos ); + o.tc0Fog.xy = tcselect * .05f; + o.tc0Fog.z = saturate( (G.FogParams.z - o.pos.w) * G.FogParams.w ); + o.tc0Fog.w = LODBIAS; + + o.light = lgridPrepareSample(lgridOffset(wspos.xyz, wsnrm.xyz)); + + o.fade.x = fadeFactor( wspos.xyz ); + o.fade.y = (1-o.fade.x) * saturate( dot( wsnrm, -G.Lamp0Dir ) ) * 100; + o.fade.z = 1 - 0.9*saturate1( exp( -0.005 * length( G.CameraPosition.xyz - wspos.xyz ) ) ); + + return o; +} + +////////////////////////////////////////////////////////////////////////////// + +sampler2D NormalMap1 : register(s0); +sampler2D NormalMap2 : register(s1); +samplerCUBE EnvMap : register(s2); +LGRID_SAMPLER LightMap : register(s3); +sampler2D LightMapLookup: register(s4); + + + +float3 pixelNormal( float4 tc0 ) +{ + float4 nm1 = tex2Dbias( NormalMap1, tc0 ); +#if WATER_LOD <= 1 + float4 nm2 = tex2Dbias( NormalMap2, tc0 ); + float3 normal = lerp( nm1, nm2, nmAnimLerp.xxxx ).agb; +#else + float3 normal = nm1.agb; +#endif + //normal = nm2; + normal.xy = 2*normal.xy - 1; + normal.z = sqrt( 1.001 - saturate1( dot( normal.xy, normal.xy ) ) ); + return normal; +} + +// Fresnel approximation. N1 and N2 are refractive indices. +// for above water, use n1 = 1, n2 = 1.3, for underwater use n1 = 1.3, n2 = 1 +float fresnel( float3 N, float3 V, float n1, float n2, float p, float fade ) +{ +#if WATER_LOD == 0 + float r0 = (n1-n2)/(n1+n2); + r0 *= r0; + return r0 + (1-r0) * pow( 1 - abs( dot( normalize(N), V ) ), p ); +#else + return 0.1 + saturate( - 1.9 * abs( dot( N, V ) ) + 0.8); // HAXX! + //return 1 - 2 * abs( dot( N, V ) ); +#endif +} + +float4 envColor( float3 N, float3 V, float fade ) +{ + float3 dir = reflect( V, N ); + return texCUBE(EnvMap, dir) * 0.91f; +} + +float4 deepWaterColor(float4 light) +{ + //float4 tint = 5*float4( 0.1f, 0.1f, 0.13f, 1); + float4 tint = 0.8f*float4( 118, 143, 153, 255 ) / 255; + return (light + texCUBEbias( EnvMap, float4( 0,1,0, 10.0f) )) * tint; +} + + +////////////////////////////////////////// +////////////////////////////////////////// + + + +float4 water_ps( V2P v ) : COLOR0 +{ + + float4 WaterColorTest = 0.5 * float4( 26, 169, 185, 0 ) / 255; + float4 FogColorTest = 0.8 * float4( 35, 107, 130, 0 ) / 255; + + float3 N2 = v.wsnrm; + float3 N1 = pixelNormal( v.tc0Fog ).xzy; + float3 N3 = 0.5*(N2 + N1); + + N3 = lerp( N3, N2, v.fade.z ); + + float3 L = /*normalize*/(-G.Lamp0Dir.xyz); + float3 E = normalize( G.CameraPosition.xyz - v.wspos.xyz ); + + float4 light = lgridSample(LightMap, LightMapLookup, v.light.xyz); + + float fre = fresnel( N3, E, 1.0f, 1.3f, 5, v.fade.x ); + float3 diffuse = deepWaterColor(light).rgb; + float3 env = envColor( N3, -E, v.fade.x ).rgb; + + float3 R = reflect( -L, N1 ); + +#if WATER_LOD <= 1 + float specular = pow( saturate0( dot( R, E ) ), 1600 ) * L.y * 100; // baseline +# ifndef GLSLES + specular = 0.65 * saturate1( specular * saturate0( light.a - 0.4f ) ); +# endif +#else + float specular = 0; +#endif + + float3 result = lerp( diffuse, env, fre ) + specular.xxx; + result = lerp( G.FogColor.rgb, result, v.tc0Fog.z ); + + return float4( result, 1 ); +} diff --git a/Client2014/shaders/source/water_r3.hlsl b/Client2014/shaders/source/water_r3.hlsl new file mode 100644 index 0000000..a3bea01 --- /dev/null +++ b/Client2014/shaders/source/water_r3.hlsl @@ -0,0 +1,208 @@ + +// +// Old new water saved here for possible damage control. +// To be removed in a couple of weeks. +// +// - Max +// + +#include "common.h" + +uniform float4x4 WorldMatrix; + +uniform float4 nmAnimLerp; // ratio between normal map frames +uniform float4 waveParams; // .x = frequency .y = phase .z = height +uniform float4 WaterColor; // deep water color + +#ifdef PIN_HQ +# define WATER_LOD 1 +#else +# define WATER_LOD 2 +#endif + +//#undef WATER_LOD +//#define WATER_LOD 0 + +float fadeFactor( float3 wspos ) +{ + return saturate( -0.4f + 1.4f*length( G.CameraPosition.xyz - wspos.xyz ) * G.FadeDistance.y ); +} + +float wave( float4 wspos ) +{ + float x = sin( ( wspos.z - wspos.x - waveParams.y ) * waveParams.x ); + float z = sin( ( wspos.z + wspos.x + waveParams.y ) * waveParams.x ); + float p = (x + z) * waveParams.z; + return p - p * fadeFactor( wspos.xyz ); +} + +// Fresnel approximation. N1 and N2 are refractive indices. +// for above water, use n1 = 1, n2 = 1.3, for underwater use n1 = 1.3, n2 = 1 +// TODO: use mul/bias hack on ipad +float fresnel( float3 N, float3 V, float n1, float n2, float p, float fade ) +{ +#if WATER_LOD == 0 + float r0 = (n1-n2)/(n1+n2); + r0 *= r0; + return r0 + (1-r0) * pow( 1 - abs( dot( N, V ) ), p ); +#else + return saturate( - 2.5 * abs( dot( N, V ) ) + 0.78 ); // HAXX! + //return 1 - 2 * abs( dot( N, V ) ); +#endif +} + +// perturbs the water mesh and vertex normals +// TODO: remove costly normal computations on ipad +void makeWaves( inout float4 wspos, inout float3 wsnrm ) +{ +#if WATER_LOD == 0 + float gridSize = 4.0f; + + float4 wspos1 = wspos; + float4 wspos2 = wspos; + + wspos1.x += gridSize; + wspos2.z += gridSize; + + wspos.y += wave(wspos) ; + wspos1.y += wave(wspos1); + wspos2.y += wave(wspos2); + + wsnrm = normalize( cross( wspos2.xyz - wspos.xyz, wspos1.xyz - wspos.xyz ) ); +#elif WATER_LOD == 1 + wspos.y += wave( wspos ); +#else /* do n0thing */ +#endif +} + +struct V2P +{ + float4 pos : POSITION; + float3 tc0Fog : TEXCOORD0; + float4 wspos : TEXCOORD1; + float3 wsnrm : TEXCOORD2; + float3 light : TEXCOORD3; + float2 fade : TEXCOORD4; +}; + +V2P water_vs( + float4 pos : POSITION, + float3 nrm : NORMAL +) +{ + V2P o; + + // Decode vertex data + nrm = (nrm - 127) / 127; + + nrm = normalize(nrm); + + float4 wspos = mul( WorldMatrix, pos ); + float3 wsnrm = nrm; + + wspos.y -= 2*waveParams.z; + + makeWaves( /*INOUT*/ wspos, /*INOUT*/ wsnrm ); + + o.wspos = wspos; + o.wsnrm = wsnrm; + + if( nrm.y < 0.01f ) o.wsnrm = nrm; + + // box mapping + //float3x2 m = { wspos.xz, wspos.xy, wspos.yz }; + //float2 tcselect = mul( abs( nrm.yzx ), m ); + + float2 tcselect; + float3 wspostc = float3( wspos.x, -wspos.y, wspos.z ); + + tcselect.x = dot( abs( nrm.yxz ), wspostc.xzx ); + tcselect.y = dot( abs( nrm.yxz ), wspostc.zyy ); + + o.pos = mul( G.ViewProjection, wspos ); + o.tc0Fog.xy = tcselect * .05f; + o.tc0Fog.z = saturate( (G.FogParams.z - o.pos.w) * G.FogParams.w ); + + o.light = lgridPrepareSample(lgridOffset(wspos.xyz, wsnrm.xyz)); + + o.fade.x = fadeFactor( wspos.xyz ); + o.fade.y = (1-o.fade.x) * saturate( dot( wsnrm, -G.Lamp0Dir ) ) * 100; + + return o; +} + +////////////////////////////////////////////////////////////////////////////// + +sampler2D NormalMap1 : register(s0); +sampler2D NormalMap2 : register(s1); +samplerCUBE EnvMap : register(s2); +LGRID_SAMPLER LightMap : register(s3); +sampler2D LightMapLookup: register(s4); + +float3 pixelNormal( float2 tc0 ) +{ + float4 nm1 = tex2D( NormalMap1, tc0 ); +#if WATER_LOD <= 1 + float4 nm2 = tex2D( NormalMap2, tc0 ); + float3 normal = lerp( nm1, nm2, nmAnimLerp.xxxx ).agb; +#else + float3 normal = nm1.agb; +#endif + //normal = nm2; + normal.xy = 2*normal.xy - 1; + normal.z = sqrt( 1.001 - saturate1( dot( normal.xy, normal.xy ) ) ); + return normal; +} + + +float4 envColor( float3 N, float3 V, float fade ) +{ + float4 solidColor = float4( 0.65f, 0.85f, 0.93f, 1 )*.95f; +#if WATER_LOD > 1 + return solidColor; +#endif + + float3 dir = reflect( V, N ); + return lerp( texCUBE( EnvMap, V ), solidColor, fade ); + + //return float3( 0.8f, 0.8f, 0.93f )*.91f; +} + +////////////////////////////////////////// +////////////////////////////////////////// + + + +float4 water_ps( V2P v ) : COLOR0 +{ + float4 WaterColorTest = 0.5 * float4( 26, 169, 185, 0 ) / 255; + float4 FogColorTest = 0.8 * float4( 35, 107, 130, 0 ) / 255; + + float3 N2 = v.wsnrm; + float3 N1 = pixelNormal( v.tc0Fog.xy ).xzy; + float3 N3 = 0.5f * (N2 + N1); + + float3 L = /*normalize*/(-G.Lamp0Dir.xyz); + float3 E = normalize( G.CameraPosition.xyz - v.wspos.xyz ); + + float4 light = lgridSample(LightMap, LightMapLookup, v.light.xyz); + + float3 ambient = ( light.rgb + G.AmbientColor.rgb ); + float fre = fresnel( N3, E, 1.0f, 1.3f, 8, v.fade.x ); + float3 diffuse = WaterColor.rgb + N1.y*0.02f; + + float3 env = envColor( N3, E, v.fade.x ).rgb; + + float3 R = reflect( -L, N1 ); + +#if WATER_LOD <= 1 + float3 specular = pow( saturate( dot( R, E ) ), 900 ) * saturate( light.a - 0.4f ) * v.fade.y; // * (L.y * 100); +#else + float3 specular = 0; +#endif + + float3 result = lerp( diffuse, env, fre ) * ( G.Lamp0Color.rgb * light.a + ambient ) + specular; + result = lerp( G.FogColor.rgb, result, v.tc0Fog.z ); + + return float4( result, 1 ); +} diff --git a/Client2014/shaders/source/wood.hlsl b/Client2014/shaders/source/wood.hlsl new file mode 100644 index 0000000..2e7f1e2 --- /dev/null +++ b/Client2014/shaders/source/wood.hlsl @@ -0,0 +1,21 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 2 +#define CFG_GLOSS_SCALE 256 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.3 + +#define CFG_SPECULAR_LOD 0.25 +#define CFG_GLOSS_LOD 32 + +#define CFG_NORMAL_DETAIL_TILING 7 +#define CFG_NORMAL_DETAIL_SCALE 0.6 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#include "material.hlsl" diff --git a/Client2014/tbb.dll b/Client2014/tbb.dll new file mode 100644 index 0000000..919da8b Binary files /dev/null and b/Client2014/tbb.dll differ diff --git a/Client2014/tbb_debug.dll b/Client2014/tbb_debug.dll new file mode 100644 index 0000000..c368a7e Binary files /dev/null and b/Client2014/tbb_debug.dll differ diff --git a/Client2016/AppSettings.xml b/Client2016/AppSettings.xml new file mode 100644 index 0000000..52b2249 --- /dev/null +++ b/Client2016/AppSettings.xml @@ -0,0 +1,7 @@ + + + https://www.syntax.eco + content + 0 + 0 + diff --git a/Client2016/COPYRIGHT.txt b/Client2016/COPYRIGHT.txt new file mode 100644 index 0000000..82ff85c --- /dev/null +++ b/Client2016/COPYRIGHT.txt @@ -0,0 +1,31 @@ +COPYRIGHT.txt + +This product uses third party open source libraries. Their copyright notices are reproduced below. + +/================================================================================= +Copyright (c) 2007-2016. The YARA Authors. All Rights Reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Client2016/PlatformContent/pc/terrain/diffuse.dds b/Client2016/PlatformContent/pc/terrain/diffuse.dds new file mode 100644 index 0000000..a279877 Binary files /dev/null and b/Client2016/PlatformContent/pc/terrain/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/terrain/materials.json b/Client2016/PlatformContent/pc/terrain/materials.json new file mode 100644 index 0000000..6ce791d --- /dev/null +++ b/Client2016/PlatformContent/pc/terrain/materials.json @@ -0,0 +1,118 @@ +{ + "platform": "pc", + "atlas": + { + "pc": { + "width": 4096, + "height": 4096, + "tileSize": 512, + "tileCount": 6, + + "borderSize": 85 + }, + "ios": { + "width": 2048, + "height": 2048, + "tileSize": 304, + "tileCount": 5, + + "borderSize": 85 + }, + "android": { + "width": 2048, + "height": 2048, + "tileSize": 304, + "tileCount": 5, + + "borderSize": 85 + } + }, + "materials": + [ + { + "name": "Air" + }, + { + "name": "Water", + "water": 0.5 + }, + { + "name": "Grass", + "texture_top": { "tiling": 0.25, "detiling": 0.25 }, + "texture_side": { "tiling": 0.2, "detiling": 0.2 }, + "texture_bottom": { "tiling": 0.45, "detiling": 0.45 } + }, + { + "name": "Slate", + "shift": 0.1, + "texture": { "tiling": 0.55, "detiling": 0.55 } + }, + { + "name": "Concrete", + "quantize": 0.5, + "type": "hard", + "texture_top": { "tiling": 0.5, "detiling": 0.8 }, + "texture_side": { "tiling": 0.4, "detiling": 0.4 } + }, + { + "name": "Brick", + "cubify": 1, + "type": "hard", + "mapping": "cube", + "texture": { "tiling": 0.80, "detiling": 0.20 } + }, + { + "name": "Sand", + "texture_top": { "tiling": 0.25, "detiling": 0.25 }, + "texture_side": { "tiling": 0.25, "detiling": 0.25 } + }, + { + "name": "WoodPlanks", + "cubify": 1.0, + "type": "hard", + "mapping": "cube", + "texture": { "tiling": 0.8, "detiling": 0.5 } + }, + { + "name": "Rock", + "shift": 0.3, + "type": "hardsoft", + "texture": { "tiling": 0.55, "detiling": 0.55 } + }, + { + "name": "Glacier", + "texture_top": { "tiling": 0.23, "detiling": 0.23 }, + "texture_side": { "tiling": 0.2, "detiling": 0.9}, + "texture_bottom": { "tiling": 0.23, "detiling": 0.6 } + }, + { + "name": "Snow", + "texture": { "tiling": 0.3, "detiling": 0.3 } + }, + { + "name": "Sandstone", + "texture_top": { "tiling": 0.5, "detiling": 0.7 }, + "texture_side": { "tiling": 0.3, "detiling": 0.5003}, + "texture_bottom": { "tiling": 0.45, "detiling": 0.45 } + }, + { + "name": "Mud", + "texture": { "tiling": 0.3, "detiling": 0.1 } + }, + { + "name": "Basalt", + "shift": 0.2, + "type": "hardsoft", + "texture": { "tiling": 0.55, "detiling": 0.55 } + }, + { + "name": "Ground", + "texture": { "tiling": 0.3, "detiling": 0.3 } + }, + { + "name": "CrackedLava", + "shift": 0.1, + "texture": { "tiling": 0.28, "detiling": 0.3 } + } + ] +} diff --git a/Client2016/PlatformContent/pc/terrain/normal.dds b/Client2016/PlatformContent/pc/terrain/normal.dds new file mode 100644 index 0000000..8bd4aa3 Binary files /dev/null and b/Client2016/PlatformContent/pc/terrain/normal.dds differ diff --git a/Client2016/PlatformContent/pc/terrain/specular.dds b/Client2016/PlatformContent/pc/terrain/specular.dds new file mode 100644 index 0000000..8b2c531 Binary files /dev/null and b/Client2016/PlatformContent/pc/terrain/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/aluminum/diffuse.dds b/Client2016/PlatformContent/pc/textures/aluminum/diffuse.dds new file mode 100644 index 0000000..fee64f0 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/aluminum/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/aluminum/normal.dds b/Client2016/PlatformContent/pc/textures/aluminum/normal.dds new file mode 100644 index 0000000..466fcb4 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/aluminum/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/aluminum/normaldetail.dds b/Client2016/PlatformContent/pc/textures/aluminum/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/aluminum/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/aluminum/specular.dds b/Client2016/PlatformContent/pc/textures/aluminum/specular.dds new file mode 100644 index 0000000..1cd4a27 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/aluminum/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/brick/diffuse.dds b/Client2016/PlatformContent/pc/textures/brick/diffuse.dds new file mode 100644 index 0000000..a9f10fa Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/brick/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/brick/normal.dds b/Client2016/PlatformContent/pc/textures/brick/normal.dds new file mode 100644 index 0000000..1131f06 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/brick/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/brick/normaldetail.dds b/Client2016/PlatformContent/pc/textures/brick/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/brick/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/brick/specular.dds b/Client2016/PlatformContent/pc/textures/brick/specular.dds new file mode 100644 index 0000000..58faad2 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/brick/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/cobblestone/diffuse.dds b/Client2016/PlatformContent/pc/textures/cobblestone/diffuse.dds new file mode 100644 index 0000000..1ff6add Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/cobblestone/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/cobblestone/normal.dds b/Client2016/PlatformContent/pc/textures/cobblestone/normal.dds new file mode 100644 index 0000000..87e3a3f Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/cobblestone/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/cobblestone/normaldetail.dds b/Client2016/PlatformContent/pc/textures/cobblestone/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/cobblestone/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/cobblestone/specular.dds b/Client2016/PlatformContent/pc/textures/cobblestone/specular.dds new file mode 100644 index 0000000..423e0b4 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/cobblestone/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/concrete/diffuse.dds b/Client2016/PlatformContent/pc/textures/concrete/diffuse.dds new file mode 100644 index 0000000..2174765 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/concrete/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/concrete/normal.dds b/Client2016/PlatformContent/pc/textures/concrete/normal.dds new file mode 100644 index 0000000..dfd9bb8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/concrete/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/concrete/normaldetail.dds b/Client2016/PlatformContent/pc/textures/concrete/normaldetail.dds new file mode 100644 index 0000000..5c11a43 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/concrete/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/concrete/specular.dds b/Client2016/PlatformContent/pc/textures/concrete/specular.dds new file mode 100644 index 0000000..bfeb384 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/concrete/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/diamondplate/diffuse.dds b/Client2016/PlatformContent/pc/textures/diamondplate/diffuse.dds new file mode 100644 index 0000000..e4f5f3a Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/diamondplate/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/diamondplate/normal.dds b/Client2016/PlatformContent/pc/textures/diamondplate/normal.dds new file mode 100644 index 0000000..88ece5f Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/diamondplate/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/diamondplate/normaldetail.dds b/Client2016/PlatformContent/pc/textures/diamondplate/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/diamondplate/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/diamondplate/specular.dds b/Client2016/PlatformContent/pc/textures/diamondplate/specular.dds new file mode 100644 index 0000000..9154362 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/diamondplate/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/fabric/diffuse.dds b/Client2016/PlatformContent/pc/textures/fabric/diffuse.dds new file mode 100644 index 0000000..a0efd50 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/fabric/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/fabric/normal.dds b/Client2016/PlatformContent/pc/textures/fabric/normal.dds new file mode 100644 index 0000000..4025a92 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/fabric/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/fabric/normaldetail.dds b/Client2016/PlatformContent/pc/textures/fabric/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/fabric/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/fabric/specular.dds b/Client2016/PlatformContent/pc/textures/fabric/specular.dds new file mode 100644 index 0000000..e3c973d Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/fabric/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/granite/diffuse.dds b/Client2016/PlatformContent/pc/textures/granite/diffuse.dds new file mode 100644 index 0000000..bd67946 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/granite/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/granite/normal.dds b/Client2016/PlatformContent/pc/textures/granite/normal.dds new file mode 100644 index 0000000..1a168d9 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/granite/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/granite/normaldetail.dds b/Client2016/PlatformContent/pc/textures/granite/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/granite/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/granite/specular.dds b/Client2016/PlatformContent/pc/textures/granite/specular.dds new file mode 100644 index 0000000..aba04d5 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/granite/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/grass/diffuse.dds b/Client2016/PlatformContent/pc/textures/grass/diffuse.dds new file mode 100644 index 0000000..c648fe4 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/grass/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/grass/normal.dds b/Client2016/PlatformContent/pc/textures/grass/normal.dds new file mode 100644 index 0000000..829c341 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/grass/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/grass/normaldetail.dds b/Client2016/PlatformContent/pc/textures/grass/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/grass/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/grass/specular.dds b/Client2016/PlatformContent/pc/textures/grass/specular.dds new file mode 100644 index 0000000..e47ed61 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/grass/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/ice/diffuse.dds b/Client2016/PlatformContent/pc/textures/ice/diffuse.dds new file mode 100644 index 0000000..fee64f0 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/ice/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/ice/normal.dds b/Client2016/PlatformContent/pc/textures/ice/normal.dds new file mode 100644 index 0000000..8752407 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/ice/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/ice/normaldetail.dds b/Client2016/PlatformContent/pc/textures/ice/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/ice/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/ice/specular.dds b/Client2016/PlatformContent/pc/textures/ice/specular.dds new file mode 100644 index 0000000..4689e14 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/ice/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/marble/diffuse.dds b/Client2016/PlatformContent/pc/textures/marble/diffuse.dds new file mode 100644 index 0000000..028e920 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/marble/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/marble/normal.dds b/Client2016/PlatformContent/pc/textures/marble/normal.dds new file mode 100644 index 0000000..ba3641b Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/marble/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/marble/normaldetail.dds b/Client2016/PlatformContent/pc/textures/marble/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/marble/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/marble/specular.dds b/Client2016/PlatformContent/pc/textures/marble/specular.dds new file mode 100644 index 0000000..d2a1c7e Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/marble/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/metal/diffuse.dds b/Client2016/PlatformContent/pc/textures/metal/diffuse.dds new file mode 100644 index 0000000..a1f7fd2 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/metal/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/metal/normal.dds b/Client2016/PlatformContent/pc/textures/metal/normal.dds new file mode 100644 index 0000000..b7d6a83 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/metal/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/metal/normaldetail.dds b/Client2016/PlatformContent/pc/textures/metal/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/metal/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/metal/specular.dds b/Client2016/PlatformContent/pc/textures/metal/specular.dds new file mode 100644 index 0000000..98a9f60 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/metal/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/pebble/diffuse.dds b/Client2016/PlatformContent/pc/textures/pebble/diffuse.dds new file mode 100644 index 0000000..48b56cc Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/pebble/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/pebble/normal.dds b/Client2016/PlatformContent/pc/textures/pebble/normal.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/pebble/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/pebble/normaldetail.dds b/Client2016/PlatformContent/pc/textures/pebble/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/pebble/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/pebble/specular.dds b/Client2016/PlatformContent/pc/textures/pebble/specular.dds new file mode 100644 index 0000000..bfeb384 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/pebble/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/plastic/diffuse.dds b/Client2016/PlatformContent/pc/textures/plastic/diffuse.dds new file mode 100644 index 0000000..c33c9e5 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/plastic/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/plastic/normal.dds b/Client2016/PlatformContent/pc/textures/plastic/normal.dds new file mode 100644 index 0000000..7804947 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/plastic/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/plastic/normaldetail.dds b/Client2016/PlatformContent/pc/textures/plastic/normaldetail.dds new file mode 100644 index 0000000..b29ac42 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/plastic/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/rust/diffuse.dds b/Client2016/PlatformContent/pc/textures/rust/diffuse.dds new file mode 100644 index 0000000..abd195a Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/rust/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/rust/normal.dds b/Client2016/PlatformContent/pc/textures/rust/normal.dds new file mode 100644 index 0000000..7d15959 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/rust/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/rust/normaldetail.dds b/Client2016/PlatformContent/pc/textures/rust/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/rust/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/rust/specular.dds b/Client2016/PlatformContent/pc/textures/rust/specular.dds new file mode 100644 index 0000000..9c3133e Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/rust/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/sand/diffuse.dds b/Client2016/PlatformContent/pc/textures/sand/diffuse.dds new file mode 100644 index 0000000..5b91413 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/sand/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/sand/normal.dds b/Client2016/PlatformContent/pc/textures/sand/normal.dds new file mode 100644 index 0000000..c8a2c84 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/sand/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/sand/normaldetail.dds b/Client2016/PlatformContent/pc/textures/sand/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/sand/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/sand/specular.dds b/Client2016/PlatformContent/pc/textures/sand/specular.dds new file mode 100644 index 0000000..6efec1b Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/sand/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/sky/sky512_bk.tex b/Client2016/PlatformContent/pc/textures/sky/sky512_bk.tex new file mode 100644 index 0000000..fcdd232 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/sky/sky512_bk.tex differ diff --git a/Client2016/PlatformContent/pc/textures/sky/sky512_dn.tex b/Client2016/PlatformContent/pc/textures/sky/sky512_dn.tex new file mode 100644 index 0000000..d63e52d Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/sky/sky512_dn.tex differ diff --git a/Client2016/PlatformContent/pc/textures/sky/sky512_ft.tex b/Client2016/PlatformContent/pc/textures/sky/sky512_ft.tex new file mode 100644 index 0000000..215b51b Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/sky/sky512_ft.tex differ diff --git a/Client2016/PlatformContent/pc/textures/sky/sky512_lf.tex b/Client2016/PlatformContent/pc/textures/sky/sky512_lf.tex new file mode 100644 index 0000000..a096acf Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/sky/sky512_lf.tex differ diff --git a/Client2016/PlatformContent/pc/textures/sky/sky512_rt.tex b/Client2016/PlatformContent/pc/textures/sky/sky512_rt.tex new file mode 100644 index 0000000..6c0aaf1 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/sky/sky512_rt.tex differ diff --git a/Client2016/PlatformContent/pc/textures/sky/sky512_up.tex b/Client2016/PlatformContent/pc/textures/sky/sky512_up.tex new file mode 100644 index 0000000..a973654 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/sky/sky512_up.tex differ diff --git a/Client2016/PlatformContent/pc/textures/slate/diffuse.dds b/Client2016/PlatformContent/pc/textures/slate/diffuse.dds new file mode 100644 index 0000000..ff7ca6d Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/slate/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/slate/normal.dds b/Client2016/PlatformContent/pc/textures/slate/normal.dds new file mode 100644 index 0000000..6f38346 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/slate/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/slate/normaldetail.dds b/Client2016/PlatformContent/pc/textures/slate/normaldetail.dds new file mode 100644 index 0000000..7e6b0ca Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/slate/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/slate/specular.dds b/Client2016/PlatformContent/pc/textures/slate/specular.dds new file mode 100644 index 0000000..271b6c1 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/slate/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/studs.dds b/Client2016/PlatformContent/pc/textures/studs.dds new file mode 100644 index 0000000..8f11167 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/studs.dds differ diff --git a/Client2016/PlatformContent/pc/textures/terrain/diffuse.dds b/Client2016/PlatformContent/pc/textures/terrain/diffuse.dds new file mode 100644 index 0000000..26b370d Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/terrain/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/terrain/diffusefar.dds b/Client2016/PlatformContent/pc/textures/terrain/diffusefar.dds new file mode 100644 index 0000000..657e10b Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/terrain/diffusefar.dds differ diff --git a/Client2016/PlatformContent/pc/textures/terrain/normal.dds b/Client2016/PlatformContent/pc/textures/terrain/normal.dds new file mode 100644 index 0000000..a64ba4a Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/terrain/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/terrain/specular.dds b/Client2016/PlatformContent/pc/textures/terrain/specular.dds new file mode 100644 index 0000000..82bc31f Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/terrain/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/wangIndex.dds b/Client2016/PlatformContent/pc/textures/wangIndex.dds new file mode 100644 index 0000000..e613ff1 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/wangIndex.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_01.dds b/Client2016/PlatformContent/pc/textures/water/normal_01.dds new file mode 100644 index 0000000..d3ba9cc Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_01.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_02.dds b/Client2016/PlatformContent/pc/textures/water/normal_02.dds new file mode 100644 index 0000000..012bf30 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_02.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_03.dds b/Client2016/PlatformContent/pc/textures/water/normal_03.dds new file mode 100644 index 0000000..343de72 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_03.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_04.dds b/Client2016/PlatformContent/pc/textures/water/normal_04.dds new file mode 100644 index 0000000..19a6276 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_04.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_05.dds b/Client2016/PlatformContent/pc/textures/water/normal_05.dds new file mode 100644 index 0000000..721320d Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_05.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_06.dds b/Client2016/PlatformContent/pc/textures/water/normal_06.dds new file mode 100644 index 0000000..1e13691 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_06.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_07.dds b/Client2016/PlatformContent/pc/textures/water/normal_07.dds new file mode 100644 index 0000000..5edd661 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_07.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_08.dds b/Client2016/PlatformContent/pc/textures/water/normal_08.dds new file mode 100644 index 0000000..14693e8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_08.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_09.dds b/Client2016/PlatformContent/pc/textures/water/normal_09.dds new file mode 100644 index 0000000..5b161c7 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_09.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_10.dds b/Client2016/PlatformContent/pc/textures/water/normal_10.dds new file mode 100644 index 0000000..353590e Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_10.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_11.dds b/Client2016/PlatformContent/pc/textures/water/normal_11.dds new file mode 100644 index 0000000..8e2c7a0 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_11.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_12.dds b/Client2016/PlatformContent/pc/textures/water/normal_12.dds new file mode 100644 index 0000000..5babefd Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_12.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_13.dds b/Client2016/PlatformContent/pc/textures/water/normal_13.dds new file mode 100644 index 0000000..fdc82da Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_13.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_14.dds b/Client2016/PlatformContent/pc/textures/water/normal_14.dds new file mode 100644 index 0000000..7ca5c06 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_14.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_15.dds b/Client2016/PlatformContent/pc/textures/water/normal_15.dds new file mode 100644 index 0000000..601ebdc Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_15.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_16.dds b/Client2016/PlatformContent/pc/textures/water/normal_16.dds new file mode 100644 index 0000000..ae10be5 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_16.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_17.dds b/Client2016/PlatformContent/pc/textures/water/normal_17.dds new file mode 100644 index 0000000..86667b2 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_17.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_18.dds b/Client2016/PlatformContent/pc/textures/water/normal_18.dds new file mode 100644 index 0000000..3cd7cfb Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_18.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_19.dds b/Client2016/PlatformContent/pc/textures/water/normal_19.dds new file mode 100644 index 0000000..01db6d4 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_19.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_20.dds b/Client2016/PlatformContent/pc/textures/water/normal_20.dds new file mode 100644 index 0000000..0fcdfa8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_20.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_21.dds b/Client2016/PlatformContent/pc/textures/water/normal_21.dds new file mode 100644 index 0000000..2aa6e82 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_21.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_22.dds b/Client2016/PlatformContent/pc/textures/water/normal_22.dds new file mode 100644 index 0000000..3e91016 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_22.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_23.dds b/Client2016/PlatformContent/pc/textures/water/normal_23.dds new file mode 100644 index 0000000..91e13c6 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_23.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_24.dds b/Client2016/PlatformContent/pc/textures/water/normal_24.dds new file mode 100644 index 0000000..87bbbdf Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_24.dds differ diff --git a/Client2016/PlatformContent/pc/textures/water/normal_25.dds b/Client2016/PlatformContent/pc/textures/water/normal_25.dds new file mode 100644 index 0000000..a0a4b49 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/water/normal_25.dds differ diff --git a/Client2016/PlatformContent/pc/textures/wood/diffuse.dds b/Client2016/PlatformContent/pc/textures/wood/diffuse.dds new file mode 100644 index 0000000..941b70f Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/wood/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/wood/normal.dds b/Client2016/PlatformContent/pc/textures/wood/normal.dds new file mode 100644 index 0000000..aa3190c Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/wood/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/wood/normaldetail.dds b/Client2016/PlatformContent/pc/textures/wood/normaldetail.dds new file mode 100644 index 0000000..5630e27 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/wood/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/wood/specular.dds b/Client2016/PlatformContent/pc/textures/wood/specular.dds new file mode 100644 index 0000000..f5a8470 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/wood/specular.dds differ diff --git a/Client2016/PlatformContent/pc/textures/woodplanks/diffuse.dds b/Client2016/PlatformContent/pc/textures/woodplanks/diffuse.dds new file mode 100644 index 0000000..df819fd Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/woodplanks/diffuse.dds differ diff --git a/Client2016/PlatformContent/pc/textures/woodplanks/normal.dds b/Client2016/PlatformContent/pc/textures/woodplanks/normal.dds new file mode 100644 index 0000000..5f4dcf9 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/woodplanks/normal.dds differ diff --git a/Client2016/PlatformContent/pc/textures/woodplanks/normaldetail.dds b/Client2016/PlatformContent/pc/textures/woodplanks/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/woodplanks/normaldetail.dds differ diff --git a/Client2016/PlatformContent/pc/textures/woodplanks/specular.dds b/Client2016/PlatformContent/pc/textures/woodplanks/specular.dds new file mode 100644 index 0000000..6a1371d Binary files /dev/null and b/Client2016/PlatformContent/pc/textures/woodplanks/specular.dds differ diff --git a/Client2016/ReflectionMetadata.xml b/Client2016/ReflectionMetadata.xml new file mode 100644 index 0000000..feea260 --- /dev/null +++ b/Client2016/ReflectionMetadata.xml @@ -0,0 +1,4241 @@ + + + + + + BindableFunction + Allow functions defined in one script to be called by another script + 4 + 66 + + + + + + Invoke + Causes the function assigned to OnInvoke to be called. Arguments passed to this function get passed to OnInvoke function. + + + + + + + OnInvoke + Should be defined as a function. This function is called when Invoke() is called. Number of arguments is variable. + + + + + + + BindableEvent + Allow events defined in one script to be subscribed to by another script + + 5 + 67 + + + + + Fire + Used to make the custom event fire (see Event for more info). Arguments can be variable length. + + + + + + + Event + This event fires when the Fire() method is used. Receives the variable length arguments from Fire(). + + + + + + + TouchTransmitter + Used by networking and replication code to transmit touch events - no other purpose + + false + 3 + 37 + + + + + ForceField + Prevents joint breakage from explosions, and stops Humanoids from taking damage + 3 + 37 + + + + + PluginManager + + + + + + TeleportService + Allows players to seamlessly leave a game and join another + + + + CustomizedTeleportUI + true + Deprecated + + + + + + StudioTool + + + + + + Plugin + + + + + + PluginMouse + + + + + + Glue + + + + + CollectionService + + + + + JointsService + + + + + BadgeService + + + + + LogService + + + + + AssetService + A service used to set and get information about assets stored on the Roblox website. + + + + + RevertAsset + Reverts a given place id to the version number provided. Returns true if successful on reverting, false otherwise. + + + + + SetPlacePermissions + Sets the permissions for a placeID to the place accessType. An optional table (inviteList) can be included that will set the accessType for only the player names provided. The table should be set up as an array of usernames (strings). + + + + + GetPlacePermissions + Given a placeID, this function will return a table with the permissions of the place. Useful for determining what kind of permissions a particular user may have for a place. + + + + + GetAssetVersions + Given a placeID, this function will return a table with the version info of the place. An optional arg of page number can be used to page through all revisions (a single page may hold about 50 revisions). + + + + + GetCreatorAssetID + Given a creationID, this function will return the asset that created the creationID. If no other asset created the given creationID, 0 is returned. + + + + + + + HttpService + 101 + + + + + + HttpEnabled + true + Enabling http requests from scripts + + + + + + + InsertService + A service used to insert objects stored on the website into the game. + + + + + GetCollection + Returns a table for the assets stored in the category. A category is an setId from www.roblox.com that links to a set. <a href="http://wiki.roblox.com/index.php?title=API:Class/InsertService/GetCollection" target="_blank">More info on table format</a>. <a href="http://wiki.roblox.com/index.php/Sets" target="_blank">More info on sets</a> + + + + + Insert + Inserts the Instance into the workspace. It is recommended to use Instance.Parent = game.Workspace instead, as this can cause issues currently. + + + + + ApproveAssetId + true + Deprecated + + + + + ApproveAssetVersionId + true + Deprecated + + + + + + + + GetBaseSets + Returns a table containing a list of the various setIds that are ROBLOX approved. <a href="http://wiki.roblox.com/index.php/Sets" target="_blank">More info on sets</a> + + + + + GetUserSets + Returns a table containing a list of the various setIds that correspond to argument 'userId'. <a href="http://wiki.roblox.com/index.php/Sets" target="_blank">More info on sets</a> + + + + + GetBaseCategories + true + Deprecated. Use GetBaseSets() instead. + + + + + GetUserCategories + true + Deprecated. Use GetUserSets() instead. + + + + + LoadAsset + Returns a Model containing the Instance that resides at AssetId on the web. This call will also yield the script until the model is returned. Script execution can still continue, however, if you use a <a href="http://wiki.roblox.com/index.php?title=Coroutine" target="_blank">coroutine</a>. + + + + + LoadAssetVersion + Similar to LoadAsset, but instead an AssetVersionId is passed in, which refers to a particular version of the asset which is not neccessarily the latest version. + + + + + + + Hat + 3 + 45 + true + + + + + Accessory + 3 + 32 + + + + + LocalBackpack + + + + + LocalBackpackItem + + + + + MotorFeature + true + + + + + Mouse + Used to receive input from the user. Actually tracks mouse events and keyboard events. + + + + + Hit + The CoordinateFrame of where the Mouse ray is currently hitting a 3D object in the Workspace. If the mouse is not over any 3D objects in the Workspace, this property is nil. + + + + + Icon + The current Texture of the Mouse Icon. Stored as a string, for more information on how to format the string <a href="http://wiki.roblox.com/index.php/Content" target="_blank">go here</a> + + + + + Origin + The CoordinateFrame of where the Mouse is when the mouse is not clicking. + + + + + Origin + The CoordinateFrame of where the Mouse is when the mouse is not clicking. This CoordinateFrame will be very close to the Camera.CoordinateFrame. + + + + + Target + The Part the mouse is currently over. If the mouse is not currently over any object (on the skybox, for example) this property is nil. + + + + + TargetFilter + A Part or Model that the Mouse will ignore when trying to find the Target, TargetSurface and Hit. + + + + + TargetSurface + The NormalId (Top, Left, Down, etc.) of the face of the part the Mouse is currently over. + + + + + UnitRay + The Unit Ray from where the mouse is (Origin) to the current Mouse.Target. + + + + + ViewSizeX + The viewport's (game window) width in pixels. + + + + + ViewSizeY + The viewport's (game window) height in pixels. + + + + + X + The absolute pixel position of the Mouse along the x-axis of the viewport (game window). Values start at 0 on the left hand side of the screen and increase to the right. + + + + + Y + The absolute pixel position of the Mouse along the y-axis of the viewport (game window). Values start at 0 on the stop of the screen and increase to the bottom. + + + + + + + Button1Down + Fired when the first button (usually the left, but could be another) on the mouse is depressed. + + + + + Button1Up + Fired when the first button (usually the left, but could be another) on the mouse is release. + + + + + Button2Down + This event is currently non-operational. + + + + + Button2Up + This event is currently non-operational. + + + + + Idle + Fired constantly when the mouse is not firing any other event (i.e. the mouse isn't moving, nor any buttons being pressed or depressed). + + + + + KeyDown + Fired when a user presses a key on the keyboard. Argument is a string representation of the key. If the key has no string representation (such as space), the string passed in is the keycode for that character. Keycodes are currently in ASCII. + + + + + KeyUp + Fired when a user releases a key on the keyboard. Argument is a string representation of the key. If the key has no string representation (such as space), the string passed in is the keycode for that character. Keycodes are currently in ASCII. + + + + + Move + Fired when the mouse X or Y member changes. + + + + + WheelBackward + This event is currently non-operational. + + + + + WheelForward + This event is currently non-operational. + + + + + + + ProfilingItem + + + + + ChangeHistoryService + + + + + RotateP + + + + + RotateV + + + + + ScriptContext + + + + + Selection + + + + + VelocityMotor + + + + + Weld + 20 + 34 + + + + + TaskScheduler + + + + + SetThreadShare + true + Deprecated + + + + + + + StatsItem + + + + + Snap + + + + + FileMesh + + + + + + ClickDetector + Raises mouse events for parent object + 3 + 41 + + + + + MaxActivationDistance + The maximum distance a Player's character can be from the ClickDetector's parent Part that will allow the Player's mouse to fire events on this object. + + + + + + + MouseClick + Fired when a player clicks on the parent Part of ClickDetector. The argument provided is always of type Player. + + + + + MouseHoverEnter + Fired when a player's mouse enters on the parent Part of ClickDetector. The argument provided is always of type Player. + + + + + MouseHoverLeave + Fired when a player's mouse leaves the parent Part of ClickDetector. The argument provided is always of type Player. + + + + + + + + Clothing + 0 + + + + + + Smoke + Makes the parent part or model object emit smoke + 3 + 59 + + + + + + ParticleEmitter + A generic particle system. + 3 + 80 + + + + + LightEmission + 0 + 1 + + + + + + + + Sparkles + Makes the parent part or model object fantastic + 3 + 42 + + + + + Explosion + 3 + 36 + Creates an Explosion! This can be used as a purely graphical effect, or can be made to damage objects. + + + + + BlastPressure + How much force this Explosion exerts on objects within it's BlastRadius. Setting this to 0 creates a purely graphical effect. A larger number will cause Parts to fly away at higher velocities. + + + + + BlastRadius + How big the Explosion is. This is a circle starting from the center of the Explosion's Position, the larger this property the larger the circle of destruction. + + + + + Position + Where the Explosion occurs in absolute world coordinates. + + + + + ExplosionType + Defines the behavior of the Explosion. <a href="http://wiki.roblox.com/index.php/ExplosionType" target="_blank">More info</a> + + + + + + + Fire + Makes the parent part or model object emit fire + 3 + 61 + + + + + Color + The color of the base of the fire. See SecondaryColor for more. + + + + + Heat + How hot the fire appears to be. The flame moves quicker the higher this value is set. + + + + + SecondaryColor + The color the fire interpolates to from Color. The longer a particle exists in the fire, the close to this color it becomes. + + + + + Size + How large the fire appears to be. + + + + + + + Seat + 3 + 35 + + + + + Platform + + Equivalent to a seat, except that the character stands up rather than sits down. + 3 + 35 + + + + + SkateboardPlatform + true + 3 + 35 + + + + + VehicleSeat + Automatically finds and powers hinge joints in an assembly. Ignores motors. + 3 + 35 + + + + + Tool + 3 + 17 + + + + + Flag + true + 3 + 38 + + + + + CanBeDropped + If someone is carrying this flag, this bool determines whether or not they can drop it and run. + + + + + TeamColor + The Team this flag is for. Corresponds with the TeamColors in the Teams service. + + + + + + + FlagStand + true + 3 + 39 + + + + + BackpackItem + 0 + + + + + Decal + 4 + 7 + Descibes a texture that is placed on one of the sides of the Part it is parented to. + + + + + Face + Describes the face of the Part the decal will be applied to. <a href="http://wiki.roblox.com/index.php/NormalId" target="_blank">More info</a> + + + + + Shiny + How much light will appear to reflect off of the decal. + + + + + Specular + How light will react to the surface of the decal. + + + + + Transparency + How visible the decal is. 1 is completely invisible, while 0 is completely opaque + 0 + 1 + + + + + + + JointInstance + 20 + 34 + + + + + Message + 11 + 33 + true + StarterGui + + + + + Hint + true + 11 + 33 + + + + + IntValue + 3 + 4 + Stores a int value in it's Value member. Useful to share int information across multiple scripts. + + + + + RayValue + 3 + 4 + Stores a Ray value in it's Value member. Useful to share Ray information across multiple scripts. + + + + + IntConstrainedValue + 3 + 4 + Stores an int value in it's Value member. Value is clamped to be in range of Min and MaxValue. Useful to share int information across multiple scripts. + + + + MaxValue + The maximum we allow this Value to be set. If Value is set higher than this, it automatically gets adjusted to MaxValue + + + + + MinValue + The minimum we allow this Value to be set. If Value is set lower than this, it automatically gets adjusted to MinValue + + + + + + DoubleConstrainedValue + 3 + 4 + Stores a double value in it's Value member. Value is clamped to be in range of Min and MaxValue. Useful to share double information across multiple scripts. + + + + + MaxValue + The maximum we allow this Value to be set. If Value is set higher than this, it automatically gets adjusted to MaxValue + + + + + MinValue + The minimum we allow this Value to be set. If Value is set lower than this, it automatically gets adjusted to MinValue + + + + + + + BoolValue + 3 + 4 + Stores a boolean value in it's Value member. Useful to share boolean information across multiple scripts. + + + + + CustomEvent + 3 + true + 4 + + + + + CustomEventReceiver + 3 + true + 4 + + + + + TextureTrail + true + 3 + 4 + + + + + FloorWire + true + 3 + 4 + Renders a thin cylinder than can be adorned with textures that 'flow' from one object to the next. Has basic pathing abilities and attempts to to not intersect anything. <a href="http://wiki.roblox.com/index.php/FloorWire_Guide" target="_blank">More info</a> + + + + + CycleOffset + Controls how the decals are positioned along the wire. <a href="http://wiki.roblox.com/index.php/CycleOffset" target="_blank">More info</a> + + + + + From + The object the FloorWire 'emits' from + + + + + StudsBetweenTextures + The space between two textures on the wire. Note: studs are relative depending on how far the camera is from the FloorWire. + + + + + Texture + The image we use to render the textures that flow from beginning to end of the FloorWire. + + + + + TextureSize + The size in studs of the Texture we use to flow from one object to the next. + + + + + To + The object the FloorWire 'emits' to + + + + + Velocity + The rate of travel that the textures flow along the wire. + + + + + WireRadius + How thick the wire is. + + + + + + + NumberValue + 3 + 4 + + + + + StringValue + 3 + 4 + + + + + Vector3Value + 3 + 4 + + + + + CFrameValue + 3 + 4 + Stores a CFrame value in it's Value member. Useful to share CFrame information across multiple scripts. + + + + + Color3Value + 3 + 4 + Stores a Color3 value in it's Value member. Useful to share Color3 information across multiple scripts. + + + + + BrickColorValue + 3 + 4 + Stores a BrickColor value in it's Value member. Useful to share BrickColor information across multiple scripts. + + + + + ObjectValue + 3 + 4 + + + + + SpecialMesh + 3 + 8 + + + + + BlockMesh + 3 + 8 + + + + + + CylinderMesh + 3 + 8 + + + + + + BevelMesh + false + true + + + + + DataModelMesh + false + + + + + + Texture + 4 + 10 + + + + + Sound + 1 + 11 + + + + + play + true + Deprecated. Use Play() instead + + + + + + + PlayOnRemove + The sound will play when it is removed from the Workspace. Looped sounds don't play + + + + + + + StockSound + false + -1 + + + + + SoundService + 50 + 31 + + + + + + AmbientReverb + + The ambient sound environment. May not work when using hardware sound + + + + + DopplerScale + + The doppler scale is a general scaling factor for how much the pitch varies due to doppler shifting in 3D sound. Doppler is the pitch bending effect when a sound comes towards the listener or moves away from it, much like the effect you hear when a train goes past you with its horn sounding. With dopplerscale you can exaggerate or diminish the effect. + + + + + DistanceFactor + + the relative distance factor, compared to 1.0 meters. + + + + + RolloffScale + + Setting this value makes the sound drop off faster or slower. The higher the value, the faster volume will attenuate, and conversely the lower the value, the slower it will attenuate. For example a rolloff factor of 1 will simulate the real world, where as a value of 2 will make sounds attenuate 2 times quicker. + + + + + + + Backpack + 3 + 20 + + + + + StarterPack + 3 + 20 + + + + + StarterPlayer + 3 + 79 + + + + + StarterGear + 3 + 20 + + + + + + StarterGui + 3 + 46 + + + + + + SetCoreGuiEnabled + Will stop/begin certain core gui elements being rendered. See CoreGuiType for core guis that can be modified. + + + + + GetCoreGuiEnabled + Returns a boolean describing whether a CoreGuiType is currently being rendered. + + + + + + + + CoreGui + false + 46 + + + + + + ContextActionService + A service used to bind input to various lua functions. + + + + + + BindAction + Binds 'functionToBind' to fire when any 'inputTypes' happen. InputTypes can be variable in number and type. Types can be Enum.KeyCode, single character strings corresponding to keys, or Enum.UserInputType. 'actionName' is a key used by many other ContextActionService functions to query state. 'createTouchButton' if true will create a button on screen on touch devices. This button will fire 'functionToBind' with three arguments: first argument is the actionName, second argument is the UserInputState of the input, and the third is the InputObject that fired this function. + + + + + SetTitle + If 'actionName' key contains a bound action, then 'title' is set as the title of the touch button. Does nothing if a touch button was not created. No guarantees are made whether title will be set when button is manipulated. + + + + + SetDescription + If 'actionName' key contains a bound action, then 'description' is set as the description of the bound action. This description will appear for users in a listing of current actions availables. + + + + + SetImage + If 'actionName' key contains a bound action, then 'image' is set as the image of the touch button. Does nothing if a touch button was not created. No guarantees are made whether image will be set when button is manipulated. + + + + + SetPosition + If 'actionName' key contains a bound action, then 'position' is set as the position of the touch button. Does nothing if a touch button was not created. No guarantees are made whether position will be set when button is manipulated. + + + + + UnbindAction + If 'actionName' key contains a bound action, removes function from being called by all input that it was bound by (if function was also bound by a different action name as well, those bound input are still active). Will also remove any touch button created (if button was manipulated manually there is no guarantee it will be cleaned up). + + + + + UnbindAllActions + Removes all functions bound. No actionNames will remain. All touch buttons will be removed. If button was manipulated manually there is no guarantee it will be cleaned up. + + + + + GetBoundActionInfo + Returns a table with info regarding the function bound with 'actionName'. Table has the keys 'title' (current title that was set with SetTitle) 'image' (image set with SetImage) 'description' (description set with SetDescription) 'inputTypes' (tuple containing all input bound for this 'actionName') 'createTouchButton' (whether or not we created a touch button for this 'actionName'). + + + + + GetAllBoundActionInfo + Returns a table with all bound action info. Each entry is a key with 'actionName' and value being the same table you would get from ContextActionService:GetBoundActionInfo('actionName'). + + + + + + + + GetButton + If 'actionName' key contains a bound action, then this will return the touch button (if was created). Returns nil if a touch button was not created. No guarantees are made whether button will be retrievable when button is manipulated. + + + + + + + + PointsService + A service used to query and award points for Roblox users using the universal point system. + + + + + + PointsAwarded + Fired when points are successfully awarded 'userId'. Also returns the updated balance of points for usedId in universe via 'userBalanceInUniverse', total points via 'userTotalBalance', and the amount points that were awarded via 'pointsAwarded'. This event fires on the server and also all clients in the game that awarded the points. + + + + + + + + AwardPoints + Will attempt to award the 'amount' points to 'userId', returns 'userId' awarded to, the number of points awarded, the new point total the user has in the game, and the total number of points the user now has. Will also fire PointsService.PointsAwarded. Works with server scripts ONLY. + + + + + GetPointBalance + Returns the overall balance of points that player with userId has (the sum of all points across all games). Works with server scripts ONLY. + + + + + GetGamePointBalance + Returns the balance of points that player with userId has in the current game (all placeID points combined within the game). Works with server scripts ONLY. + + + + + GetAwardablePoints + Returns the number of points the current universe can award to players. Works with server scripts ONLY. + + + + + + + + Chat + + + + + FilterStringForPlayerAsync + Will return an appropriately filtered string for the player passed in. + + + + + + + + MarketplaceService + 46 + + + + + + PromptPurchase + Will prompt 'player' to purchase the item associated with 'assetId'. 'equipIfPurchased' is an optional argument that will give the item to the player immediately if they buy it (only applies to gear). 'currencyType' is also optional and will attempt to prompt the user with a specified currency if the product can be purchased with this currency, otherwise we use the default currency of the product. + + + + + + + + GetProductInfo + Takes one argument "assetId" which should be a number of an asset on www.roblox.com. Returns a table containing the product information (if this process fails, returns an empty table). + + + + + PlayerOwnsAsset + Checks to see if 'Player' owns the product associated with 'assetId'. Returns true if the player owns it, false otherwise. This call will produce a warning if called on a guest player. + + + + + + + ProcessReceipt + Callback that is executed for pending Developer Product receipts. + + If this function does not return Enum.ProductPurchaseDecision.PurchaseGranted, then you will not be granted the money for the purchase! + + The callback will be invoked with a table, containing the following informational fields: + PlayerId - the id of the player making the purchase. + PlaceIdWherePurchased - the specific place where the purchase was made. + PurchaseId - a unique identifier for the purchase, should be used to prevent granting an item multiple times for one purchase. + ProductId - the id of the purchased product. + CurrencyType - the type of currency used (Tix, Robux). + CurrencySpent - the amount of currency spent on the product for this purchase. + + + + + + + + PromptPurchaseFinished + Fired when a 'player' dismisses a purchase dialog for 'assetId'. If the player purchased the item 'isPurchased' will be true, otherwise it will be false. This call will produce a warning if called on a guest player. + + + + + + + + UserInputService + + + + + TouchEnabled + Returns true if the local device accepts touch input, false otherwise. + + + + + KeyboardEnabled + Returns true if the local device accepts keyboard input, false otherwise. + + + + + MouseEnabled + Returns true if the local device accepts mouse input, false otherwise. + + + + + AccelerometerEnabled + Returns true if the local device has an accelerometer, false otherwise. + + + + + GyroscopeEnabled + Returns true if the local device has an gyroscope, false otherwise. + + + + + + + + TouchTap + Fired when a user taps their finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the tap gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchPinch + Fired when a user pinches their fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the pinch gesture. 'scale' is a float that indicates the difference from the beginning of the pinch gesture. 'velocity' is a float indicating how quickly the pinch gesture is happening. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchSwipe + Fired when a user swipes their fingers on a TouchEnabled device. 'swipeDirection' is an Enum.SwipeDirection, indicating the direction the user swiped. 'numberOfTouches' is an int that indicates how many touches were involved with the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchLongPress + Fired when a user holds at least one finger for a short amount of time on the same screen position on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchRotate + Fired when a user rotates two fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'rotation' is a float indicating how much the rotation has gone from the start of the gesture. 'velocity' is a float that indicates how quickly the gesture is being performed. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchPan + Fired when a user drags at least one finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'totalTranslation' is a Vector2, indicating how far the pan gesture has gone from its starting point. 'velocity' is a Vector2 that indicates how quickly the gesture is being performed in each dimension. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + + TouchStarted + Fired when a user places their finger on a TouchEnabled device. 'touch' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchMoved + Fired when a user moves their finger on a TouchEnabled device. 'touch' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchEnded + Fired when a user moves their finger on a TouchEnabled device. 'touch' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + + InputBegan + Fired when a user begins interacting via a Human-Computer Interface device (Mouse button down, touch begin, keyboard button down, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + InputChanged + Fired when a user changes interacting via a Human-Computer Interface device (Mouse move, touch move, mouse wheel, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + InputEnded + Fired when a user stops interacting via a Human-Computer Interface device (Mouse button up, touch end, keyboard button up, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + + TextBoxFocused + Fired when a user clicks/taps on a textbox to begin text entry. Argument is the textbox that was put in focus. This also fires if a textbox forces focus on the user. This event only fires locally. + + + + + TextBoxFocusReleased + Fired when a user stops text entry into a textbox (usually by pressing return or clicking/tapping somewhere else on the screen). Argument is the textbox that was taken out of focus. This event only fires locally. + + + + + DeviceAccelerationChanged + Fired when a user moves a device that has an accelerometer. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. This event only fires locally. + + + + + DeviceGravityChanged + Fired when the force of gravity changes on a device that has an accelerometer. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. This event only fires locally. + + + + + DeviceRotationChanged + Fired when a user rotates a device that has an gyroscope. This is fired with an InputObject, which has type Enum.InputType.Gyroscope, and position that shows total rotation in each local device axis. The delta property describes the amount of rotation that last happened. A second argument of Vector4 is the device's current quaternion rotation in reference to it's default reference frame. This event only fires locally. + + + + + + + + GetDeviceAcceleration + Returns an InputObject that describes the device's current acceleration. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. The delta property describes the amount of rotation that last happened. This event only fires locally. + + + + + GetDeviceGravity + Returns an InputObject that describes the device's current gravity vector. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. The delta property describes the amount of rotation that last happened. This event only fires locally. + + + + + GetDeviceRotation + Returns an InputObject and a Vector4 that describes the device's current rotation vector. This is fired with an InputObject, which has type Enum.InputType.Gyroscope, and position that shows total rotation in each local device axis. The delta property describes the amount of rotation that last happened. The Vector4 is the device's current quaternion rotation in reference to it's default reference frame. This event only fires locally. + + + + + + + + Sky + 0 + 28 + + + + + Motor + + 0 + + + + + Humanoid + 3 + 9 + + + + + + MoveTo + Attempts to move the Humanoid and it's associated character to 'part'. 'location' is used as an offset from part's origin. + + + + + Jump + + + + + Sit + + + + + TakeDamage + Decreases health by the amount. Use this instead of changing health directly to make sure weapons are filtered for things such as ForceField(s). + + + + + UnequipTools + + Takes any active gear/tools that the Humanoid is using and puts them into the backpack. This function only works on Humanoids with a corresponding Player. + + + + + EquipTool + + Takes a specified tool and equips it to the Humanoid's Character. Tool argument should be of type 'Tool'. + + + + + + + NameOcclusion + + Sets how to display other humanoid names to this humanoid's player. <a href="http://wiki.roblox.com/index.php/NameOcclusion" target="_blank">More info</a> + + + Health + How many hit points the Humanoid has. When this number reaches 0 or goes below 0, the Humanoid's character falls apart and will respawn. + + + MaxHealth + The maximum number of hit points a Humanoid's health can reach. If the Humanoid's health is set over this amount, the health gets set to this value. + + + TargetPoint + The location that the Humanoid is trying to walk to. + + + + + + + BodyColors + + 0 + + + + + Shirt + 0 + 43 + + + + + Pants + 0 + 44 + + + + + ShirtGraphic + 0 + 40 + + + + + Skin + true + 0 + + + + + DebugSettings + false + 0 + + + + + FaceInstance + false + + + + + GameSettings + false + 0 + + + + + GlobalSettings + false + 0 + + + + + Item + false + 0 + + + + + NetworkPeer + false + + + + + NetworkSettings + false + 0 + + + + + PVInstance + false + + + + + CoordinateFrame + true + Deprecated. Use CFrame instead + + + + + + + RenderSettings + false + 0 + + + + + RootInstance + false + + + + + ServiceProvider + false + + + + + service + true + Use GetService() instead + + + + + + + ProfilingItem + false + + + + + NetworkMarker + false + + + + + + Hopper + true + Use StarterPack instead + 0 + + + + + + Instance + false + + + + + + Archivable + Determines whether or not an Instance can be saved when the game closes/attempts to save the game. Note: this only applies to games that use Data Persistence, or Personal Build Servers. + + + + + ClassName + The string name of this Instance's most derived class. + + + + + Parent + The Instance that is directly above this Instance in the tree. + + + + + + + + + GetDebugId + false + This function is for internal testing. Don't use in production code + + + + + Clone + Returns a copy of this Object and all its children. The copy's Parent is nil + + + + + clone + true + Use Clone() instead + + + + + isA + true + Use IsA() instead + + + + + IsA + Returns a boolean if this Instance is of type 'className' or a is a subclass of type 'className'. If 'className' is not a valid class type in ROBLOX, this function will always return false. <a href="http://wiki.roblox.com/index.php/IsA" target="_blank">More info</a> + + + + + FindFirstChild + Returns the first child of this Instance that matches the first argument 'name'. The second argument 'recursive' is an optional boolean (defaults to false) that will force the call to traverse down thru all of this Instance's descendants until it finds an object with a name that matches the 'name' argument. The function will return nil if no Instance is found. + + + + + GetFullName + Returns a string that shows the path from the root node (DataModel) to this Instance. This string does not include the root node (DataModel). + + + + + children + true + Use GetChildren() instead + + + + + getChildren + true + Use GetChildren() instead + + + + + GetChildren + Returns a read-only table of this Object's children + + + + + GetDescendants + Returns a ready-only table of this Object's descendants + + + + + Remove + Deprecated. Use ClearAllChildren() to get rid of all child objects, or Destroy() to invalidate this object and its descendants + true + + + + + remove + true + Use Remove() instead + + + + + ClearAllChildren + Removes all children (but not this object) from the workspace. + + + + + Destroy + Removes object and all of its children from the workspace. Disconnects object and all children from open connections. Object and children may not be usable after calling Destroy. + + + + + findFirstChild + true + Use FindFirstChild() instead + + + + + + + + AncestryChanged + Fired when any of this object's ancestors change. First argument 'child' is the object whose parent changed. Second argument 'parent' is the first argument's new parent. + + + + + DescendantAdded + Fired after an Instance is parented to this object, or any of this object's descendants. The 'descendant' argument is the Instance that is being added. + + + + + DescendantRemoving + Fired after an Instance is unparented from this object, or any of this object's descendants. The 'descendant' argument is the Instance that is being added. + + + + + Changed + Fired after a property changes value. The property argument is the name of the property + + + + + + + + BodyGyro + Attempts to maintain a fixed orientation of its parent Part + 14 + 14 + + + + + + MaxTorque + The maximum torque that will be exerted on the Part + + + + + maxTorque + true + Use MaxTorque instead + + + + + D + The dampening factor applied to this force + + + + + P + The power continually applied to this force + + + + + CFrame + The cframe that this force is trying to orient its parent Part to. Note: this force only uses the rotation of the cframe, not the position. + + + + + cframe + true + Use CFrame instead + + + + + + + BodyPosition + 14 + 14 + + + + + + MaxForce + The maximum force that will be exerted on the Part + + + + + maxForce + true + Use MaxForce instead + + + + + D + The dampening factor applied to this force + + + + + P + The power factor continually applied to this force + + + + + Position + The Vector3 that this force is trying to position its parent Part to. + + + + + position + true + Use position instead + + + + + + + RocketPropulsion + + 14 + 14 + A propulsion system that mimics a rocket + + + + + BodyVelocity + 14 + 14 + + + + + MaxForce + The maximum force that will be exerted on the Part in each axis + + + + + maxForce + true + Use MaxForce instead + + + + + P + The amount of power we add to the system. The higher the power, the quicker the force will achieve its goal. + + + + + Velocity + The velocity this system tries to achieve. How quickly the system reaches this velocity (if ever) is defined by P. + + + + + velocity + true + Use Velocity instead + + + + + + + BodyAngularVelocity + 14 + 14 + + + + MaxTorque + The maximum torque that will be exerted on the Part in each axis + + + maxTorque + true + Use MaxTorque instead + + + P + The amount of power we add to the system. The higher the power, the quicker the force will achieve its goal. + + + AngularVelocity + The rotational velocity this system tries to achieve. How quickly the system reaches this velocity is defined by P. + + + angularVelocity + true + Use AngularVelocity instead + + + + + + BodyForce + 14 + 14 + When parented to a physical part, BodyForce will continually exert a force upon its parent object. + + + + Force + The continual force exerted on an object, defined in each axis. + + + force + true + Use Force instead + + + + + + BodyThrust + 14 + 14 + + + + + + Force + The power continually applied to this force + + + + + force + true + Use Force instead + + + + + Location + The Vector3 location of where to apply the force to. + + + + + location + true + Use Location instead + + + + + + + Hole + true + 0 + + + + + Feature + 0 + + + + + + Teams + This Service-level object is the container for all Team objects in a level. A map that supports team games must have a Teams service. <a href="http://wiki.roblox.com/index.php/Team" target="_blank">More info</a> + 14 + 23 + + + + + + Team + The Team class is used to represent a faction in a team game. The only valid location for a Team object is under the Teams service. <a href="http://wiki.roblox.com/index.php/Team" target="_blank">More info</a> + 1 + 24 + + + + + SpawnLocation + 3 + 25 + + + + + NetworkClient + 3 + 16 + + + + + NetworkServer + 3 + 15 + + + + + + Script + 3 + 6 + + + + + + LinkedScript + + This property is under development. Do not use + + + + + + + + LocalScript + 4 + 18 + A script that runs on clients, NOT servers. LocalScripts can only run when parented under the PlayerGui currently. + + + + + + NetworkReplicator + 3 + 29 + + + + + + Model + 10 + 2 + A construct used to group Parts and other objects together, also allows manipulation of multiple objects. + + + + + BreakJoints + Breaks all surface joints contained within + + + + + GetModelCFrame + Returns a CFrame that has position of the centroid of all Parts in the Model. The rotation matrix is either the rotation matrix of the user-defined PrimaryPart, or if not specified then a part in the Model chosen by the engine. + + + + + GetModelSize + Returns a Vector3 that is union of the extents of all Parts in the model. + + + + + MakeJoints + Creates the appropriate SurfaceJoints between all touching Parts contrained within the model. Technically, this function calls MakeJoints() on all Parts inside the model. + + + + + MoveTo + Moves the centroid of the Model to the specified location, respecting all relative distances between parts in the model. + + + + + ResetOrientationToIdentity + Rotates all parts in the model to the orientation that was set using SetIdentityOrientation(). If this function has never been called, rotation is reset to GetModelCFrame()'s rotation. + + + + + SetIdentityOrientation + Takes the current rotation matrix of the model and stores it as the model's identity matrix. The rotation is applied when ResetOrientationToIdentity() is called. + + + + + TranslateBy + Similar to MoveTo(), except instead of moving to an explicit location, we use the model's current CFrame location and offset it. + + + + + GetPrimaryPartCFrame + Returns the cframe of the Model.PrimaryPart. If PrimaryPart is nil, then this function will throw an error. + + + + + SetPrimaryPartCFrame + Sets the cframe of the Model.PrimaryPart. If PrimaryPart is nil, then this function will throw an error. This also sets the cframe of all descendant Parts relative to the cframe change to PrimaryPart. + + + + + makeJoints + Use MakeJoints() instead + true + + + + + move + true + Use MoveTo() instead + + + + + + + PrimaryPart + A Part that serves as a reference for the Model's CFrame. Used in conjunction with GetModelPrimaryPartCFrame and SetModelPrimaryPartCFrame. Use this to rotate/translate all Parts relative to the PrimaryPart. + + + + + + + + Status + true + 10 + 2 + + + + + move + true + Use MoveTo() instead + + + + + + + + DataModel + + + + + + + + Workspace + + + + + workspace + true + Deprecated. Use Workspace + + + + + ShowMouse + true + Deprecated. Use Workspace.IsMouseCursorVisible + + + + + IsLoaded + Returns true if the game has finished loading, false otherwise. Check this before listening to the Loaded signal to ensure a script knows when a game finishes loading. + + + + + + + + Loaded + Fires when the game finishes loading. Use this to know when to remove your custom loading gui. It is best to check IsLoaded() before connecting to this event, as the game may load before the event is connected to. + + + + + + + + get + true + Use GetObjects() instead + + + + + SetPlaceID + true + Use SetPlaceId() instead + + + + + SetCreatorID + true + Use SetCreatorId() instead + + + + + + + + HopperBin + true + 24 + 22 + + + + + + Camera + 5 + 5 + + + + + CameraSubject + Where the Camera's focus is. Any rotation of the camera will be about this subject. + + + + + CameraType + Defines how the camera will behave. <a href="http://wiki.roblox.com/index.php/CameraType" target="_blank">More info</a> + + + + + CoordinateFrame + true + The current position and rotation of the Camera. For most CameraTypes, the rotation is set such that the CoordinateFrame lookVector is pointing at the Focus. + + + + + CFrame + The current position and rotation of the Camera. For most CameraTypes, the rotation is set such that the CoordinateFrame lookVector is pointing at the Focus. + + + + + FieldOfView + The current angle, or width, of what the camera can see. Current acceptable values are from 20 degrees to 80. + + + + + Focus + The current CoordinateFrame that the camera is looking at. Note: it is not always guaranteed that the camera is always looking here. + + + + + ViewportSize + Holds the x,y screen resolution of the viewport the camera is presenting (note: this can differ from the AbsoluteSize property of a full screen gui). + + + + + + + GetRoll + Returns the camera's current roll. Roll is defined in radians, and is stored as the delta from the camera's y axis default normal vector. + + + + + WorldToScreenPoint + Takes a 3D position in the world and projects it onto x,y coordinates of screen space. Returns two values, first is a Vector3 that has x,y position and z position which is distance from camera (negative if behind camera, positive if in front). Second return value is a boolean indicating if the first argument is an on-screen coordinate. + + + + + ScreenPointToRay + Takes a 2D screen position and produces a Ray object to be used for 3D raycasting. Input is x,y screen coordinates, and a (optional, defaults to 0) z position which sets how far in the camera look vector to start the ray origin. + + + + + ViewportPointToRay + Same as ScreenPointToRay, except no GUI offsets are taken into account. Useful for things like casting a ray from the middle of the Camera.ViewportSize + + + + + WorldToViewportPoint + Same as WorldToScreenPoint, except no GUI offsets are taken into account. + + + + + SetRoll + Sets the camera's current roll. Roll is defined in radians, and is stored as the delta from the camera's y axis default normal vector. + + + + + + + + Players + 2 + 21 + + + + + CharacterAutoLoads + true + Set to true, when a player joins a game, they get a character automatically, as well as when they die. When set to false, characters do not auto load and will only load in using Player:LoadCharacter(). + + + + + + + players + true + Use GetPlayers() instead + + + + + + + + ReplicatedStorage + 3 + 70 + A container whose contents are replicated to all clients and the server. + + + + + + RobloxReplicatedStorage + false + + + + + + ReplicatedFirst + 3 + 70 + A container whose contents are replicated to all clients (but not back to the server) first before anything else. Useful for creating loading guis, tutorials, etc. + + + + + RemoveRobloxLoadingScreen + Removes the default Roblox loading screen from view. Call this when you are ready to either show your own loading gui, or when the game is ready to play. + + + + + + + + ServerStorage + 3 + 69 + A container whose contents are only on the server. + + + + + + ServerScriptService + 3 + 71 + A container whose contents should be scripts. Scripts that are added to the container are run on the server. + + + + + + Lighting + 3 + 13 + Responsible for all lighting aspects of the world (affects how things are rendered). + + + + + GetMinutesAfterMidnight + The number of minutes that the current time is past midnight. If currently at midnight, returns 0. Will return decimal values if not at an exact minute. + + + + + GetMoonDirection + Returns the lookVector (Vector3) of the moon. If this lookVector was used in a CFrame, the Part would face the moon. + + + + + GetMoonPhase + Currently always returns 0.75. MoonPhase cannot be edited. + + + + + GetSunDirection + Returns the lookVector (Vector3) of the sun. If this lookVector was used in a CFrame, the Part would face the moon. + + + + + SetMinutesAfterMidnight + Sets the time to be a certain number of minutes after midnight. This works with integer and decimal values. + + + + + + + Ambient + The hue of the global lighting. Changing this changes the color tint of all objects in the Workspace. + + + + + Brightness + How much global light each Part in the Workspace receives. Standard range is 0 to 1 (0 being little light), but can be increased all the way to 5 (colors start to be appear very different at this value). + + + + + ColorShift_Bottom + The hue of global lighting on the bottom surfaces of an object. + + + + + ColorShift_Top + The hue of global lighting on the top surfaces of an object. + + + + + FogColor + A Color3 value that changes the hue of distance fog. + + + + + FogEnd + The distance at which fog completely blocks your vision. This distance is relative to the camera position. Units are in studs + + + + + FogStart + The distance at which the fog gradient begins. This distance is relative to the camera position. Units are in studs. + + + + + GeographicLatitude + The latitude position the level is placed at. This affects sun position. <a href="http://wiki.roblox.com/index.php/GeographicLatitude" target="_blank">More info</a> + + + + + GlobalShadows + Flag enabling shadows from sun and moon in the place + + + + + OutdoorAmbient + Effective ambient value for outdoors, effectively shadow color outdoors (requires GlobalShadows enabled) + + + + + Outlines + Flag enabling or disabling outlines on parts and terrain + + + + + ShadowColor + Color the shadows appear as. Shadows are drawn mostly for characters, but depending on the lighting will also show for Parts in the Workspace. Rendering settings can also affect if shadows are drawn. + + + + + TimeOfDay + A string that represent the current time of day. Time is in 24-hour clock format "XX::YY:ZZ", where X is hour, Y is minute, and Z is seconds. + + + + + + + LightingChanged + Fired whenever a property of Lighting is changed, or a skybox is added or removed. Skyboxes are of type 'Sky' and should be parented directly to lighting. + + + + + + + + TestService + 100 + 68 + + + + + + DebuggerManager + + + + + + + + ScriptDebugger + + + + + + + + DebuggerBreakpoint + + + + + + + + DebuggerWatch + + + + + + + + Debris + -1 + 30 + A service that provides utility in cleaning up objects + + + + + addItem + true + Use AddItem() instead + + + + + AddItem + Adds an Instance into the debris service that will later be destroyed. Second argument 'lifetime' is optional and specifies how long (in seconds) to wait before destroying the item. If no time is specified then the item added will automatically be destroyed in 10 seconds. + + + + + + + MaxItems + true + Deprecated. No replacement + + + + + + + + Accoutrement + 0 + 32 + + + + + + Player + 1 + 12 + + + + + + CharacterAppearance + false + + + + + CameraMode + An enum that describes how a Player's camera is allowed to behave. <a href="http://wiki.roblox.com/index.php/CameraMode" target="_blank">More info</a>. + + + + + DataReady + Read-only. If true, this Player's persistent data can be loaded, false otherwise. <a href="http://wiki.roblox.com/index.php/ROBLOX_Scripting_How_To:_Data_Persistence" target="_blank">Info on Data Persistence</a>. + + + + + + + + LoadCharacter + true + Loads in a new character for this player. This will replace the player's current character, if they have one. This should be used in conjunction with Players.CharacterAutoLoads to control spawning of characters. This function only works from a server-side script (NOT a LocalScript). + + + + + playerFromCharacter + true + Use GetPlayerFromCharacter() instead + + + + + SetUnder13 + true + + + + + + + + WaitForDataReady + true + Yields until the persistent data for this Player is ready to be loaded. <a href="http://wiki.roblox.com/index.php/ROBLOX_Scripting_How_To:_Data_Persistence" target="_blank">Info on Data Persistence</a>. + + + + + GetWebPersonalServerRank + true + + + + + + + + + Idled + Fired periodically after the user has been AFK for a while. Currently this event is only fired for the *local* Player. "time" is the time in seconds that the user has been idle. + + + + + + + + Workspace + 1 + 19 + + + + + FindPartsInRegion3 + Returns parts in the area defined by the Region3, up to specified maxCount or 100, whichever is less + + + + + FindPartsInRegion3WithIgnoreList + Returns parts in the area defined by the Region3, up to specified maxCount or 100, whichever is less + + + + + FindPartOnRay + Return type is (BasePart, Vector3) if the ray hits. If it misses it will return (nil, PointAtEndOfRay) + + + + + FindPartOnRayWithIgnoreList + Return type is (BasePart, Vector3) if the ray hits. If it misses it will return (nil, PointAtEndOfRay) + + + + + + + PGSPhysicsSolverEnabled + Boolean used to enable the experimental physics solver + + + + + FallenPartsDestroyHeight + Sets the height at which falling characters and parts are destroyed. This property is not scriptable and can only be set in Studio + + + + + + + + BasePart + A structural class, not creatable + -1 + false + + + + + + Color + true + Deprecated. Use BrickColor instead + + + + + CFrame + Contains information regarding the Part's position and a matrix that defines the Part's rotation. Can read/write. <a href="http://wiki.roblox.com/index.php/Cframe" target="_blank">More info</a> + + + + + CanCollide + Determines whether physical interactions with other Parts are respected. If true, will collide and react with physics to other Parts. If false, other parts will pass thru instead of colliding + + + + + Anchored + Determines whether or not physics acts upon the Part. If true, part stays 'Anchored' in space, not moving regardless of any collision/forces acting upon it. If false, physics works normally on the part. + + + + + Elasticity + A float value ranging from 0.0f to 1.0f. Sets how much the Part will rebound against another. a value of 1 is like a superball, and 0 is like a lead block. + 0 + 1 + + + + + Friction + A float value ranging from 0.0f to 1.0f. Sets how much the Part will be able to slide. a value of 1 is no sliding, and 0 is no friction, so infinite sliding. + 0 + 2 + + + + + Locked + Determines whether building tools (in-game and studio) can manipulate this Part. If true, no editing allowed. If false, editing is allowed. + + + + + Material + Specifies the look and feel the Part should have. Note: this does not define the color the Part is, see BrickColor for that. <a href="http://wiki.roblox.com/index.php/Material" target="_blank">More info</a> + + + + + Reflectance + Specifies how shiny the Part is. A value of 1 is completely reflective (chrome), while a value of 0 is no reflectance (concrete wall) + 0 + 1 + + + + + ResizeIncrement + Sets the value for the smallest change in size allowable by the Resize(NormalId, int) function. + + + + + ResizeableFaces + Sets the value for the faces allowed to be resized by the Resize(NormalId, int) function. + + + + + Transparency + Sets how visible an object is. A value of 1 makes the object invisible, while a value of 0 makes the object opaque. + 0 + 1 + + + + + Velocity + How fast the Part is traveling in studs/second. This property is NOT recommended to be modified directly, unless there is good reason. Otherwise, try using a BodyForce to move a Part. + + + + + + + + makeJoints + Use MakeJoints() instead + true + + + + + MakeJoints + Creates the appropriate SurfaceJoints with all parts that are touching this Instance (including internal joints in the Instance, as in a Model). This uses the SurfaceTypes defined on the surfaces of parts to create the appropriate welds. <a href="http://wiki.roblox.com/index.php/MakeJoints" target="_blank">More info</a> + + + + + BreakJoints + Destroys SurfaceJoints with all parts that are touching this Instance (including internal joints in the Instance, as in a Model). + + + + + GetMass + Returns a number that is the mass of this Instance. Mass of a Part is immutable, and is changed only by the size of the Part. + + + + + Resize + Resizes a Part in the direction of the face defined by 'NormalId', by the amount specified by 'deltaAmount'. If the operation will expand the part to intersect another Instance, the part will not resize at all. Return true if the call is successful, false otherwise. + + + + + getMass + Use GetMass() instead + true + + + + + + + OutfitChanged + true + + + + + LocalSimulationTouched + true + Deprecated. Use Touched instead + + + + + StoppedTouching + + Deprecated. Use TouchEnded instead + + + + + TouchEnded + Fired when the part stops touching another part + + + + + + + Part + A plastic building block - the fundamental component of ROBLOX + 11 + 1 + + + + + TrussPart + An extendable building truss + 12 + 1 + + + + + WedgePart + A Wedge Part + 12 + 1 + + + + + PrismPart + A Prism Part + false + true + 12 + 1 + + + + + PyramidPart + A Pyramid Part + false + true + 12 + 1 + + + + + ParallelRampPart + A ParallelRamp Part + false + true + 12 + 1 + + + + + RightAngleRampPart + A RightAngleRamp Part + false + true + 12 + 1 + + + + + CornerWedgePart + A CornerWedge Part + 12 + 1 + + + + + PlayerGui + A container instance that syncs data between a single player and the server. ScreenGui objects that are placed in this container will be shown to the Player parent only + 13 + 46 + + + + + SelectionImageObject + Overrides the default selection adornment (used for gamepads). For best results, this should point to a GuiObject. + + + + + + + PlayerScripts + A container instance that contains LocalScripts. LocalScript objects that are placed in this container will be exectue only when a Player is the parent. + 13 + 78 + + + + + StarterPlayerScripts + A container instance that contains LocalScripts. LocalScript objects that are placed in this container will be copied to new Players on startup. + 13 + 78 + + + + + StarterCharacterScripts + A container instance that contains LocalScripts. LocalScript objects that are placed in this container will be copied to new characters on startup. + 13 + 78 + + + + + + GuiMain + Deprecated, please use ScreenGui + true + 14 + 47 + + + + + + ScreenGui + The core GUI object on which tools are built. Add Frames/Labels/Buttons to this object to have them rendered as a 2D overlay + 14 + 47 + StarterGui + + + + + FunctionalTest + Deprecated. Use TestService instead + true + 1 + + + + + BillboardGui + A GUI that adorns an object in the 3D world. Add Frames/Labels/Buttons to this object to have them rendered while attached to a 3D object + 14 + 64 + StarterGui + + + + + + Adornee + The Object the billboard gui uses as its base to render from. Currently, the only way to set this property is thru a script, and must exist in the workspace. This will only render if the object assigned derives from BasePart. + + + + + AbsolutePosition + A read-only Vector2 value that is the GuiObject's current position (x,y) in pixel space, from the top left corner of the GuiObject. + + + + + AbsoluteSize + A read-only Vector2 value that is the GuiObject's current size (width, height) in pixel space. + + + + + Active + If true, this GuiObject can fire mouse events and will pass them to any GuiObjects layered underneath, while false will do neither. + + + + + AlwaysOnTop + If true, billboard gui does not get occluded by 3D objects, but always renders on the screen. + + + + + Enabled + If true, billboard gui will render, otherwise rendering will be skipped. + + + + + ExtentsOffset + A Vector3 (x,y,z) defined in studs that will offset the gui from the extents of the 3d object it is rendering from. + + + + + PlayerToHideFrom + Specifies a Player that the BillboardGui will not render to. + + + + + StudsOffset + A Vector3 (x,y,z) defined in studs that will offset the gui from the centroid of the 3d object it is rendering from + + + + + SizeOffset + A Vector2 (x,y) defined in studs that will offset the gui size from it's current size. + + + + + Size + A UDim2 value describing the size of the BillboardGui. More information on UDim2 is available <a href="http://wiki.roblox.com/index.php/UDim2" target="_blank">here</a>. Relative values are defined as one-to-one with studs. + + + + + + + + SurfaceGui + tbd + 14 + 64 + StarterGui + + + + + + Adornee + The Object the billboard gui uses as its base to render from. Currently, the only way to set this property is thru a script, and must exist in the workspace. This will only render if the object assigned derives from BasePart. + + + + + Active + If true, this GuiObject can fire mouse events and will pass them to any GuiObjects layered underneath, while false will do neither. + + + + + Enabled + If true, billboard gui will render, otherwise rendering will be skipped. + + + + + + + + + + GuiBase2d + false + + + + + + AbsolutePosition + A read-only Vector2 value that is the GuiObject's current position (x,y) in pixel space, from the top left corner of the GuiObject. + + + + + AbsoluteSize + A read-only Vector2 value that is the GuiObject's current size (width, height) in pixel space. + + + + + + + + InputObject + An object that describes a particular user input, such as mouse movement, touches, keyboard, and more. + + + + + UserInputType + An enum that describes what kind of input this object is describing (mousebutton, touch, etc.). See Enum.UserInputType for more info. + + + + + UserInputState + An enum that describes what state of a particular input (touch began, touch moved, touch ended, etc.). See Enum.UserInputState for more info. + + + + + Position + A Vector3 value that describes a positional value of this input. For mouse and touch input, this is the screen position of the mouse/touch, described in the x and y components. For mouse wheel input, the z component describes whether the wheel was moved forward or backward. + + + + + KeyCode + An enum that describes what kind of input is being pressed. For types of input like Keyboard, this describes what key was pressed. For input like mousebutton, this provides no additional information. + + + + + + + + GuiObject + false + + + + + + TweenPosition + Smoothly moves a GuiObject from its current position to 'endPosition'. The only required argument is 'endPosition'. <a href="http://wiki.roblox.com/index.php/TweenPosition" target="_blank">More info</a> + + + + + TweenSize + Smoothly translates a GuiObject's current size to 'endSize'. The only required argument is 'endSize'. <a href="http://wiki.roblox.com/index.php/TweenSize" target="_blank">More info</a> + + + + + TweenSizeAndPosition + Smoothly translates a GuiObject's current size to 'endSize', and also smoothly translates the GuiObject's current position to 'endPosition'. The only required arguments are 'endSize' and 'endPosition'. <a href="http://wiki.roblox.com/index.php/TweenSizeAndPosition" target="_blank">More info</a> + + + + + + + + Active + If true, this GuiObject can fire mouse events and will pass them to any GuiObjects layered underneath, while false will do neither. + + + + + BackgroundColor3 + A Color3 value that specifies the background color for the GuiObject. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + BackgroundTransparency + A number value that specifies how transparent the background of the GuiObject is. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + 0 + 1 + + + + + BorderColor3 + A Color3 value that specifies the color of the outline of the GuiObject. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + BorderSizePixel + A number value that specifies the thickness (in pixels) of the outline of the GuiObject. Currently this value can only be set to either 0 or 1, any other number has no effect. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + ClipsDescendants + If set to true, any descendants of this GuiObject will only render if contained within it's borders. If set to false, all descendants will render regardless of position. + + + + + Draggable + If true, allows a GuiObject to be dragged by the user's mouse. The events 'DragBegin' and 'DragStopped' are fired when the appropriate action happens, and only will fire on Draggable=true GuiObjects. + + + + + Size + A UDim2 value describing the size of the GuiObject on screen in both absolute and relative coordinates. More information on UDim2 is available <a href="http://wiki.roblox.com/index.php/UDim2" target="_blank">here</a>. + + + + + Position + A UDim2 value describing the position of the top-left corner of the GuiObject on screen. More information on UDim2 is available <a href="http://wiki.roblox.com/index.php/UDim2" target="_blank">here</a>. + + + + + SizeConstraint + The direction(s) that an object can be resized in. <a href="http://wiki.roblox.com/index.php/SizeConstraint" target="_blank">More info</a>. + + + + + ZIndex + Describes the ordering in which overlapping GuiObjects will be drawn. A value of 1 is drawn first, while higher values are drawn in ascending order (each value draws over the last). + + + + + BackgroundColor + true + Deprecated. Use BackgroundColor3 instead + + + + + BorderColor + true + Deprecated. Use BorderColor3 instead + + + + + SelectionImageObject + Overrides the default selection adornment (used for gamepads). For best results, this should point to a GuiObject. + + + + + + + + DragBegin + Fired when a GuiObject with Draggable set to true starts to be dragged. 'InitialPosition' is a UDim2 value of the position of the GuiObject before any drag operation began. + + + + + DragStopped + Always fired after a DragBegin event, DragStopped is fired when the user releases the mouse button causing a drag operation on the GuiObject. Arguments 'x', and 'y' specify the top-left absolute position of the GuiObject when the event is fired. + + + + + MouseEnter + Fired when the mouse enters a GuiObject, as long as the GuiObject is active (see active property for more detail). Arguments 'x', and 'y' specify the absolute pixel position of the mouse. + + + + + MouseLeave + Fired when the mouse leaves a GuiObject, as long as the GuiObject is active (see active property for more detail). Arguments 'x', and 'y' specify the absolute pixel position of the mouse. + + + + + MouseMoved + Fired when the mouse is inside a GuiObject and moves, as long as the GuiObject is active (see active property for more detail). Arguments 'x', and 'y' specify the absolute pixel position of the mouse. + + + + + + TouchTap + Fired when a user taps their finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the tap gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchPinch + Fired when a user pinches their fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the pinch gesture. 'scale' is a float that indicates the difference from the beginning of the pinch gesture. 'velocity' is a float indicating how quickly the pinch gesture is happening. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. + + + + + TouchSwipe + Fired when a user swipes their fingers on a TouchEnabled device. 'swipeDirection' is an Enum.SwipeDirection, indicating the direction the user swiped. 'numberOfTouches' is an int that indicates how many touches were involved with the gesture. This event only fires locally. + + + + + TouchLongPress + Fired when a user holds at least one finger for a short amount of time on the same screen position on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. + + + + + TouchRotate + Fired when a user rotates two fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'rotation' is a float indicating how much the rotation has gone from the start of the gesture. 'velocity' is a float that indicates how quickly the gesture is being performed. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. + + + + + TouchPan + Fired when a user drags at least one finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'totalTranslation' is a Vector2, indicating how far the pan gesture has gone from its starting point. 'velocity' is a Vector2 that indicates how quickly the gesture is being performed in each dimension. 'state' indicates the Enum.UserInputState of the gesture. + + + + + + InputBegan + Fired when a user begins interacting via a Human-Computer Interface device (Mouse button down, touch begin, keyboard button down, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. + + + + + InputChanged + Fired when a user changes interacting via a Human-Computer Interface device (Mouse move, touch move, mouse wheel, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. + + + + + InputEnded + Fired when a user stops interacting via a Human-Computer Interface device (Mouse button up, touch end, keyboard button up, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. + + + + + + + + + Frame + A container object used to layout other GUI objects + 15 + 48 + StarterGui + + + + + Style + Determines how a frame will look. Uses Enum.FrameStyle. <a href="http://wiki.roblox.com/index.php?title=API:Enum/FrameStyle" target="_blank">More info</a> + + + + + + + ScrollingFrame + A container object used to layout other GUI objects, and allows for scrolling. + 15 + 48 + StarterGui + + + + + ScrollingEnabled + Determines whether or not scrolling is allowed on this frame. If turned off, no scroll bars will be rendered. + + + + + CanvasSize + Determines the size of the area that is scrollable. The UDim2 is calculated using the parent gui's size, similar to the regular Size property on gui objects. + + + + + CanvasPosition + The absolute position the scroll frame is in respect to the canvas size. The minimum this can be set to is (0,0), while the max is the absolute canvas size - AbsoluteWindowSize. + + + + + AbsoluteWindowSize + The size in pixels of the frame, without the scrollbars. + + + + + ScrollBarThickness + How thick the scroll bar appears. This applies to both the horizontal and vertical scroll bars. Can be set to 0 for no bars render. + + + + + TopImage + The "Up" image on the vertical scrollbar. Size of this is always ScrollBarThickness by ScrollBarThickness. This is also used as the "left" image on the horizontal scroll bar. + + + + + MidImage + The "Middle" image on the vertical scrollbar. Size of this can vary in the y direction, but is always set at ScrollBarThickness in x direction. This is also used as the "mid" image on the horizontal scroll bar. + + + + + BottomImage + The "Down" image on the vertical scrollbar. Size of this is always ScrollBarThickness by ScrollBarThickness. This is also used as the "right" image on the horizontal scroll bar. + + + + + + + ImageLabel + A GUI object containing an Image + 18 + 49 + StarterGui + + + + + Image + Specifies the id of the texture to display. <a href="http://wiki.roblox.com/index.php?title=API:Class/ImageLabel/Image" target="_blank">More info</a> + + + + + ScaleType + Specifies how an image should be displayed. See ScaleType for more info. + + + + + SliceCenter + If ScaleType is set to Slice, this Rect is used to specify the central part of the image. Everything outside of this is considered to be the border. + + + + + + + TextLabel + A GUI object containing text + 19 + 50 + StarterGui + + + + + TextColor + true + Deprecated. Use TextColor3 instead + + + + + + + TextButton + A GUI button containing text + 17 + 51 + StarterGui + + + + + TextColor + true + Deprecated. Use TextColor3 instead + + + + + + + TextBox + A text entry box + 17 + 51 + StarterGui + + + + + TextColor + true + Deprecated. Use TextColor3 instead + + + + + + + GuiButton + A GUI button containing an Image + false + 16 + 52 + + + + + AutoButtonColor + Determines whether a button changes color automatically when reacting to mouse events. + + + + + Modal + Allows the mouse to be free in first person mode. If a button with this property set to true is visible, the mouse is 'free' in first person mode. + + + + + Style + Determines how a button will look, including mouse event states. Uses Enum.ButtonStyle. <a href="http://wiki.roblox.com/index.php?title=API:Class/GuiButton/Style" target="_blank">More info</a> + + + + + + + MouseButton1Click + Fired when the mouse is over the button, and the mouse down and up events fire without the mouse leaving the button. + + + + + MouseButton1Down + Fired when the mouse button is pushed down on a button. + + + + + MouseButton1Up + Fired when the mouse button is released on a button. + + + + + MouseButton2Click + This function currently does not work :( + + + + + MouseButton2Down + This function currently does not work :( + + + + + MouseButton2Up + This function currently does not work :( + + + + + + + + ImageButton + A GUI button containing an Image + 16 + 52 + StarterGui + + + + + Image + Specifies the asset id of the texture to display. <a href="http://wiki.roblox.com/index.php?title=API:Class/ImageButton/Image" target="_blank">More info</a> + + + + + ScaleType + Specifies how an image should be displayed. See ScaleType for more info. + + + + + SliceCenter + If ScaleType is set to Slice, this Rect is used to specify the central part of the image. Everything outside of this is considered to be the border. + + + + + + + Handles + A 3D GUI object to represent draggable handles + + 19 + 53 + + + + + ArcHandles + A 3D GUI object to represent draggable arc handles + + 20 + 56 + + + + + SelectionBox + A 3D GUI object to represent the visible selection around an object + 21 + 54 + + + + + SelectionSphere + A 3D GUI object to represent the visible selection around an object + 21 + 54 + + + + + SurfaceSelection + A 3D GUI object to represent the visible selection around a face of an object + 21 + 55 + + + + + Configuration + An object that can be placed under parts to hold Value objects that represent that part's configuration + 22 + 58 + + + + + Folder + An object that can be created to hold and organize objects + 22 + 77 + + + + + SelectionPartLasso + true + A visual line drawn representation between two part objects + 22 + 57 + + + + + SelectionPointLasso + true + A visual line drawn representation between two positions + 22 + 57 + + + + + PartPairLasso + A visual line drawn representation between two parts. + 22 + 57 + + + + + Pose + The pose of a joint relative to it's parent part in a keyframe + 22 + 60 + + + + + Keyframe + One keyframe of an animation + 22 + 60 + + + + + Animation + Represents a linked animation object, containing keyframes and poses. + 22 + 60 + + + + + AnimationTrack + Returned by a call to LoadAnimation. Controls the playback of an animation on a Humanoid. + 22 + 60 + + + + + Stopped + true + This event is never raised + + + + + + + AnimationController + Allows animations to be played on joints of the parent object. + 22 + 60 + + + + + CharacterMesh + Modifies the appearance of a body part. + 22 + 60 + + + + + Dialog + An object used to make dialog trees to converse with players + 22 + 62 + + + + + DialogChoice + An object used to make dialog trees to converse with players + 22 + 63 + + + + + MeshPart + Parts + true + true + 105 + 73 + Model + + + + + UnionOperation + A UnionOperation is a union of multiple parts + true + false + 0 + 73 + + + + + UsePartColor + Override the colors of the mesh with the part color. + + + + + + + NegateOperation + A NegateOperation can be used to create holes in other parts + true + false + 0 + 72 + + + + + UsePartColor + Override the colors of the mesh with the part color. + + + + + + + Terrain + Object representing a high performance bounded grid of static 4x4 parts + true + 0 + 65 + + + + + GetCell + Returns CellMaterial, CellBlock, CellOrientation + + + + + GetWaterCell + + Returns hasAnyWater, WaterForce, WaterDirection + + + + + SetWaterCell + + + + + + + + PointLight + Makes the parent part emit light in a spherical shape + 3 + 13 + + + + + SpotLight + Makes the parent part emit light in a conical shape + 3 + 13 + + + + + SurfaceLight + Makes the parent part emit light in a frustum shape from rectangle defined by part + 3 + 13 + + + + + RemoteFunction + Allow functions defined in one script to be called by another script across client/server boundary + 4 + 74 + + + + + RemoteEvent + Allow events defined in one script to be subscribed to by another script across client/server boundary + 5 + 75 + + + + + TerrainRegion + Object representing a snapshot of the region of terrain + true + 0 + 65 + + + + + ModuleScript + A script fragment. Only runs when another script uses require() on it. + 5 + 76 + + + + + + + + Material + + + + Air + false + + + + + Water + false + + + + + Rock + false + + + + + Glacier + false + + + + + Snow + false + + + + + Sandstone + false + + + + + Mud + false + + + + + Basalt + false + + + + + Ground + false + + + + + CrackedLava + false + + + + + + Status + true + + + + Poison + true + + + + + Confusion + true + + + + + diff --git a/Client2016/SDL2.dll b/Client2016/SDL2.dll new file mode 100644 index 0000000..b118189 Binary files /dev/null and b/Client2016/SDL2.dll differ diff --git a/Client2016/SyntaxPlayerBeta.exe b/Client2016/SyntaxPlayerBeta.exe new file mode 100644 index 0000000..a8cd985 Binary files /dev/null and b/Client2016/SyntaxPlayerBeta.exe differ diff --git a/Client2016/VMProtectSDK64.dll b/Client2016/VMProtectSDK64.dll new file mode 100644 index 0000000..4f2a936 Binary files /dev/null and b/Client2016/VMProtectSDK64.dll differ diff --git a/Client2016/content/fonts/Arial.font b/Client2016/content/fonts/Arial.font new file mode 100644 index 0000000..e223b3d Binary files /dev/null and b/Client2016/content/fonts/Arial.font differ diff --git a/Client2016/content/fonts/ArialBold.font b/Client2016/content/fonts/ArialBold.font new file mode 100644 index 0000000..eda0976 Binary files /dev/null and b/Client2016/content/fonts/ArialBold.font differ diff --git a/Client2016/content/fonts/CompositExtraSlot0.mesh b/Client2016/content/fonts/CompositExtraSlot0.mesh new file mode 100644 index 0000000..87b9d85 Binary files /dev/null and b/Client2016/content/fonts/CompositExtraSlot0.mesh differ diff --git a/Client2016/content/fonts/CompositExtraSlot1.mesh b/Client2016/content/fonts/CompositExtraSlot1.mesh new file mode 100644 index 0000000..6874c5e Binary files /dev/null and b/Client2016/content/fonts/CompositExtraSlot1.mesh differ diff --git a/Client2016/content/fonts/CompositExtraSlot2.mesh b/Client2016/content/fonts/CompositExtraSlot2.mesh new file mode 100644 index 0000000..423f198 Binary files /dev/null and b/Client2016/content/fonts/CompositExtraSlot2.mesh differ diff --git a/Client2016/content/fonts/CompositExtraSlot3.mesh b/Client2016/content/fonts/CompositExtraSlot3.mesh new file mode 100644 index 0000000..de05ced Binary files /dev/null and b/Client2016/content/fonts/CompositExtraSlot3.mesh differ diff --git a/Client2016/content/fonts/CompositExtraSlot4.mesh b/Client2016/content/fonts/CompositExtraSlot4.mesh new file mode 100644 index 0000000..ff9cb62 Binary files /dev/null and b/Client2016/content/fonts/CompositExtraSlot4.mesh differ diff --git a/Client2016/content/fonts/CompositFullAtlasBaseTexture.mesh b/Client2016/content/fonts/CompositFullAtlasBaseTexture.mesh new file mode 100644 index 0000000..b7e6595 Binary files /dev/null and b/Client2016/content/fonts/CompositFullAtlasBaseTexture.mesh differ diff --git a/Client2016/content/fonts/CompositFullAtlasOverlayTexture.mesh b/Client2016/content/fonts/CompositFullAtlasOverlayTexture.mesh new file mode 100644 index 0000000..eda1938 Binary files /dev/null and b/Client2016/content/fonts/CompositFullAtlasOverlayTexture.mesh differ diff --git a/Client2016/content/fonts/CompositLeftArmBase.mesh b/Client2016/content/fonts/CompositLeftArmBase.mesh new file mode 100644 index 0000000..5bcc4ae Binary files /dev/null and b/Client2016/content/fonts/CompositLeftArmBase.mesh differ diff --git a/Client2016/content/fonts/CompositLeftLegBase.mesh b/Client2016/content/fonts/CompositLeftLegBase.mesh new file mode 100644 index 0000000..f4712ce Binary files /dev/null and b/Client2016/content/fonts/CompositLeftLegBase.mesh differ diff --git a/Client2016/content/fonts/CompositPantsTemplate.mesh b/Client2016/content/fonts/CompositPantsTemplate.mesh new file mode 100644 index 0000000..756ee03 Binary files /dev/null and b/Client2016/content/fonts/CompositPantsTemplate.mesh differ diff --git a/Client2016/content/fonts/CompositRightArmBase.mesh b/Client2016/content/fonts/CompositRightArmBase.mesh new file mode 100644 index 0000000..02f2721 Binary files /dev/null and b/Client2016/content/fonts/CompositRightArmBase.mesh differ diff --git a/Client2016/content/fonts/CompositRightLegBase.mesh b/Client2016/content/fonts/CompositRightLegBase.mesh new file mode 100644 index 0000000..b287939 Binary files /dev/null and b/Client2016/content/fonts/CompositRightLegBase.mesh differ diff --git a/Client2016/content/fonts/CompositShirtTemplate.mesh b/Client2016/content/fonts/CompositShirtTemplate.mesh new file mode 100644 index 0000000..75487e1 Binary files /dev/null and b/Client2016/content/fonts/CompositShirtTemplate.mesh differ diff --git a/Client2016/content/fonts/CompositTShirt.mesh b/Client2016/content/fonts/CompositTShirt.mesh new file mode 100644 index 0000000..b39b8ac Binary files /dev/null and b/Client2016/content/fonts/CompositTShirt.mesh differ diff --git a/Client2016/content/fonts/CompositTorsoBase.mesh b/Client2016/content/fonts/CompositTorsoBase.mesh new file mode 100644 index 0000000..0388bde Binary files /dev/null and b/Client2016/content/fonts/CompositTorsoBase.mesh differ diff --git a/Client2016/content/fonts/LoadingScript.lua b/Client2016/content/fonts/LoadingScript.lua new file mode 100644 index 0000000..2e03d7f --- /dev/null +++ b/Client2016/content/fonts/LoadingScript.lua @@ -0,0 +1,815 @@ +--rbxsig%hm7SbDaf0GHizpUYxb6qQAHuUU7LfsWX8bgPnc64hv1MnSJFX+Komih+acv/5vqLxpzxHNtvBCnuAwN6l63fYukZ0Yat7mDBHBx0w0CPTaWSCs2P5WDx/W7P+L8AkqpaowXKgCKST9VBErgwqTlUySsi0r9+HfExO+cl3h2lXig=% +--rbxassetid%158948138% +-- Creates the generic "ROBLOX" loading screen on startup +-- Written by ArceusInator & Ben Tkacheff, 2014 +-- + +-- Constants +local PLACEID = Game.PlaceId + +local MPS = Game:GetService 'MarketplaceService' +local CP = Game:GetService 'ContentProvider' + +local COLORS = { + BLACK = Color3.new(0, 0, 0), + DARK = Color3.new(35/255, 35/255, 38/255), + DARKMED = Color3.new(61/255, 61/255, 67/255), + DARKMED2 = Color3.new(75/255, 76/255, 85/255), + MED = Color3.new(118/255, 118/255, 129/255), + LIGHTMED = Color3.new(190/255, 192/255, 212/255), + LIGHT = Color3.new(217/255, 218/255, 231/255), + ERROR = Color3.new(253/255,68/255,72/255) +} + +local IMAGES = { + BACKGROUND_THUMBNAIL_VIGNETTE = 'rbxasset://textures/loading/loadingvignette.png', + ROBLOX_LOGO_256 = 'rbxasset://textures/loading/robloxlogo.png', + GAME_THUMBNAIL = 'http://www.roblox.com/Thumbs/Asset.ashx?format=png&width=420&height=230&assetId=', + GAME_BACKGROUND = 'rbxasset://textures/loading/loadingTexture.png' +} + +local VALID_TEXT_SIZES = { + 12, + 14, + 18, + 24, + 36, + 48 +} + +-- +-- Variables +local GameAssetInfo -- loaded by InfoProvider:LoadAssets() +local currScreenGui = nil +local renderSteppedConnection = nil + +-- +-- Utility functions +local create = function(className, defaultParent) + return function(propertyList) + local object = Instance.new(className) + + for index, value in next, propertyList do + if type(index) == 'string' then + object[index] = value + else + if type(value) == 'function' then + value(object) + elseif type(value) == 'userdata' then + value.Parent = object + end + end + end + + if object.Parent == nil then + object.Parent = defaultParent + end + + return object + end +end + +-- +-- Create objects +local MainGui = {} +local InfoProvider = {} + + +function InfoProvider:GetGameName() + if GameAssetInfo ~= nil then + return GameAssetInfo.Name + else + return '' + end +end + +function InfoProvider:GetCreatorName() + if GameAssetInfo ~= nil then + return GameAssetInfo.Creator.Name + else + return '' + end +end + +function InfoProvider:LoadAssets() + Spawn(function() + if PLACEID <= 0 then + while Game.PlaceId <= 0 do + wait() + end + PLACEID = Game.PlaceId + end + + IMAGES.GAME_THUMBNAIL = IMAGES.GAME_THUMBNAIL .. tostring(PLACEID) + + -- load game asset info + coroutine.resume(coroutine.create(function() GameAssetInfo = MPS:GetProductInfo(PLACEID) end)) + + while not currScreenGui do + wait() + end + currScreenGui.ThumbnailContainer.Thumbnail.Image = IMAGES.GAME_THUMBNAIL + + -- load images + for imageName, imageContent in next, IMAGES do + CP:Preload(imageContent) + end + + end) +end + +-- +-- Declare member functions +function MainGui:GenerateMain() + local screenGui = create 'ScreenGui' { + Name = 'RobloxLoadingGui' + } + + -- + -- create descendant frames + + local mainBackgroundContainer = create 'Frame' { + Name = 'MainBackgroundContainer', + BackgroundColor3 = COLORS.DARK, + Size = UDim2.new(1, 0, 1, 0), + Active = true, + + create 'Frame' { + Name = 'TopBar', + BackgroundColor3 = COLORS.DARKMED, + BorderColor3 = COLORS.MED, + BorderSizePixel = 3, + Position = UDim2.new(0, -220, 0, -205), + Rotation = -10, + Size = UDim2.new(0, 1000, 0, 220), + ZIndex = 5, + + create 'ImageLabel' { + Name = 'RobloxLogo', + BackgroundTransparency = 1, + Image = IMAGES.ROBLOX_LOGO_256, + Position = UDim2.new(0, 214, 1, -80), + Rotation = 2, + Size = UDim2.new(0, 128, 0, 128), + ZIndex = 6, + + create 'TextLabel' { + Name = 'PoweredBy', + BackgroundTransparency = 1, + Position = UDim2.new(0.5, -60, 0, 30), + Size = UDim2.new(0, 80, 0, 18), + Font = Enum.Font.SourceSans, + FontSize = Enum.FontSize.Size18, + TextColor3 = Color3.new(1,1,1), + Text = "Powered By", + ZIndex = 6 + } + } + }, + + create 'ImageButton' { + Name = 'CloseButton', + Image = 'rbxasset://textures/ui/CloseButton.png', + BackgroundTransparency = 1, + Position = UDim2.new(1, -27, 0, 5), + Size = UDim2.new(0, 22, 0, 22), + Active = true, + ZIndex = 10 + }, + + create 'Frame' { + Name = 'ErrorFrame', + BackgroundColor3 = COLORS.ERROR, + BorderSizePixel = 0, + Position = UDim2.new(0.25,0,0,0), + Size = UDim2.new(0.5, 0, 0, 80), + ZIndex = 5, + Visible = false, + + create 'TextLabel' { + Name = "ErrorText", + BackgroundTransparency = 1, + ZIndex = 6, + Position = UDim2.new(0,5,0,5), + Size = UDim2.new(1,-10,1,-10), + Font = Enum.Font.SourceSans, + FontSize = Enum.FontSize.Size18, + Text = "", + TextColor3 = Color3.new(1,1,1), + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + TextWrap = true + } + }, + + create 'Frame' { + Name = 'BottomBar', + BorderSizePixel = 0, + BackgroundTransparency = 1, + Position = UDim2.new(0, 0, 1, -150), + Size = UDim2.new(1, 0, 0, 300), + ZIndex = 5, + + create 'Frame' { + Name = 'BottomBarActual', + BackgroundColor3 = COLORS.DARKMED, + BorderColor3 = COLORS.MED, + BorderSizePixel = 3, + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 5, + + create 'Frame' { + Name = 'TextContainer', + BackgroundTransparency = 1, + Position = UDim2.new(0, -5, 0, 5), + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 8, + + create 'TextLabel' { + Name = 'CreatorName', + BackgroundTransparency = 1, + Position = UDim2.new(0, 0, 0, 70), + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 9, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size48, + Text = InfoProvider:GetCreatorName(), + TextColor3 = COLORS.LIGHT, + TextStrokeColor3 = COLORS.DARKMED2, + TextStrokeTransparency = 0, + TextXAlignment = Enum.TextXAlignment.Right, + TextYAlignment = Enum.TextYAlignment.Top + }, + + create 'TextLabel' { + Name = 'GameName', + BackgroundTransparency = 1, + Position = UDim2.new(0, 0, 0, 30), + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 9, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size48, + Text = InfoProvider:GetGameName(), + TextColor3 = COLORS.LIGHT, + TextStrokeColor3 = COLORS.DARKMED2, + TextStrokeTransparency = 0, + TextXAlignment = Enum.TextXAlignment.Right, + TextYAlignment = Enum.TextYAlignment.Top + }, + + create 'TextLabel' { + Name = 'CreatorNamePrefix', + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 9, + Font = Enum.Font.SourceSans, + FontSize = Enum.FontSize.Size48, + Text = 'By', + TextColor3 = COLORS.LIGHTMED, + TextStrokeColor3 = COLORS.DARKMED2, + TextStrokeTransparency = 0, + TextXAlignment = Enum.TextXAlignment.Right, + TextYAlignment = Enum.TextYAlignment.Top + }, + + create 'TextLabel' { + Name = 'OnYourWay', + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 9, + Font = Enum.Font.SourceSans, + FontSize = Enum.FontSize.Size36, + Text = 'You\'re on your way to', + TextColor3 = COLORS.LIGHTMED, + TextStrokeColor3 = COLORS.DARKMED2, + TextStrokeTransparency = 0, + TextXAlignment = Enum.TextXAlignment.Right, + TextYAlignment = Enum.TextYAlignment.Top + } + } + } + }, + + create 'ImageLabel' { + Name = 'BackgroundThumbnailVignette', + BackgroundTransparency = 1, + Image = IMAGES.BACKGROUND_THUMBNAIL_VIGNETTE, + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 3 + }, + + create 'ImageLabel' { + Name = 'BackgroundThumbnail', + BackgroundTransparency = 1, + Image = IMAGES.GAME_BACKGROUND, + Size = UDim2.new(1.5, 0, 1.5, 0), + Position = UDim2.new(-0.5,0,0,0), + ZIndex = 2 + }, + + Parent = screenGui + } + + local thumbnailContainer = create 'Frame' { + Name = 'ThumbnailContainer', + BackgroundColor3 = COLORS.BLACK, + BorderColor3 = COLORS.MED, + BorderSizePixel = 4, + Position = UDim2.new(0.5, -210, 0.5, -115), + Size = UDim2.new(0, 420, 0, 230), + ZIndex = 8, + + create 'ImageLabel' { + Name = 'Thumbnail', + BorderColor3 = COLORS.DARKMED2, + BorderSizePixel = 3, + Image = "", + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 8 + }, + + create 'Frame' { + Name = 'LoadingInfoContainer', + BorderColor3 = COLORS.MED, + BackgroundColor3 = COLORS.DARKMED, + BorderSizePixel = 2, + Position = UDim2.new(0,20,1,0), + Size = UDim2.new(1,-40,0,40), + ZIndex = 7, + + create 'TextLabel' { + Name = 'InstancesLabel', + BackgroundTransparency = 1, + Position = UDim2.new(0,0,0,5), + Size = UDim2.new(0.25, 0, 1, -10), + ZIndex = 9, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size14, + Text = 'Instances', + TextColor3 = COLORS.LIGHTMED, + TextStrokeColor3 = COLORS.DARKMED2, + TextStrokeTransparency = 0, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Top + }, + create 'TextLabel' { + Name = 'InstancesValue', + BackgroundTransparency = 1, + Position = UDim2.new(0,0,0,5), + Size = UDim2.new(0.25, 0, 1, -10), + ZIndex = 9, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size14, + Text = '0', + TextColor3 = COLORS.LIGHTMED, + TextStrokeColor3 = COLORS.DARKMED2, + TextStrokeTransparency = 0, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Bottom + }, + + + + + create 'TextLabel' { + Name = 'VoxelsLabel', + BackgroundTransparency = 1, + Position = UDim2.new(0.75,0,0,5), + Size = UDim2.new(0.25, 0, 1, -10), + ZIndex = 9, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size14, + Text = 'Voxels', + TextColor3 = COLORS.LIGHTMED, + TextStrokeColor3 = COLORS.DARKMED2, + TextStrokeTransparency = 0, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Top + }, + create 'TextLabel' { + Name = 'VoxelsValue', + BackgroundTransparency = 1, + Position = UDim2.new(0.75,0,0,5), + Size = UDim2.new(0.25, 0, 1, -10), + ZIndex = 9, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size14, + Text = '0', + TextColor3 = COLORS.LIGHTMED, + TextStrokeColor3 = COLORS.DARKMED2, + TextStrokeTransparency = 0, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Bottom + }, + + + + create 'TextLabel' { + Name = 'ConnectorsLabel', + BackgroundTransparency = 1, + Position = UDim2.new(0.5,0,0,5), + Size = UDim2.new(0.25, 0, 1, -10), + ZIndex = 9, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size14, + Text = 'Connectors', + TextColor3 = COLORS.LIGHTMED, + TextStrokeColor3 = COLORS.DARKMED2, + TextStrokeTransparency = 0, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Top + }, + create 'TextLabel' { + Name = 'ConnectorsValue', + BackgroundTransparency = 1, + Position = UDim2.new(0.5,0,0,5), + Size = UDim2.new(0.25, 0, 1, -10), + ZIndex = 9, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size14, + Text = '0', + TextColor3 = COLORS.LIGHTMED, + TextStrokeColor3 = COLORS.DARKMED2, + TextStrokeTransparency = 0, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Bottom + }, + + + + create 'TextLabel' { + Name = 'BricksLabel', + BackgroundTransparency = 1, + Position = UDim2.new(0.25,0,0,5), + Size = UDim2.new(0.25, 0, 1, -10), + ZIndex = 9, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size14, + Text = 'Bricks', + TextColor3 = COLORS.LIGHTMED, + TextStrokeColor3 = COLORS.DARKMED2, + TextStrokeTransparency = 0, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Top + }, + create 'TextLabel' { + Name = 'BricksValue', + BackgroundTransparency = 1, + Position = UDim2.new(0.25,0,0,5), + Size = UDim2.new(0.25, 0, 1, -10), + ZIndex = 9, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size14, + Text = '0', + TextColor3 = COLORS.LIGHTMED, + TextStrokeColor3 = COLORS.DARKMED2, + TextStrokeTransparency = 0, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Bottom + }, + }, + + Parent = screenGui + } + + -- + -- recalculate everything + while not Game:GetService("CoreGui") do + wait() + end + screenGui.Parent = Game.CoreGui + + MainGui:RecalculateSizes(screenGui) + + -- + -- return generated gui + return screenGui +end + +function MainGui:RecalculateTextSize(screenGui) + local screenSize = screenGui.AbsoluteSize + + local textSizeScale = math.min(screenSize.y/800, 1) + local closestValidSizePrevIndex = math.floor(textSizeScale*#VALID_TEXT_SIZES)-1 + local closestValidSizePrevIndex2 = closestValidSizePrevIndex-1 + local closestValidTextSize + local closestValidTextSizePrev + -- next can't take a 0 because it's a total wuss + if closestValidSizePrevIndex > 0 then + _, closestValidTextSize = next(VALID_TEXT_SIZES, closestValidSizePrevIndex) + if closestValidSizePrevIndex2 > 0 then + _, closestValidTextSizePrev = next(VALID_TEXT_SIZES, closestValidSizePrevIndex2) + else + _, closestValidTextSizePrev = next(VALID_TEXT_SIZES) -- not doing t[1] because this looks cleaner + end + else + _, closestValidTextSize = next(VALID_TEXT_SIZES) + _, closestValidTextSizePrev = next(VALID_TEXT_SIZES) + end + local textSizeEnum = Enum.FontSize['Size'..closestValidTextSize] + local textSizePrevEnum = Enum.FontSize['Size'..closestValidTextSizePrev] + + if not screenGui:FindFirstChild("MainBackgroundContainer") then return end + + local TextContainer = screenGui.MainBackgroundContainer.BottomBar.BottomBarActual.TextContainer + local currentYBumpDistance = 0 + TextContainer.OnYourWay.FontSize = textSizePrevEnum + currentYBumpDistance = currentYBumpDistance + closestValidTextSizePrev*(40/48) + TextContainer.GameName.Position = UDim2.new(0, 0, 0, currentYBumpDistance) + TextContainer.GameName.FontSize = textSizeEnum + currentYBumpDistance = currentYBumpDistance + closestValidTextSize*(40/48) + TextContainer.CreatorName.Position = UDim2.new(0, 0, 0, currentYBumpDistance) + TextContainer.CreatorName.FontSize = textSizeEnum + local currentXBumpDistance = -(TextContainer.CreatorName.TextBounds.X+5) + TextContainer.CreatorNamePrefix.Position = UDim2.new(0, currentXBumpDistance, 0, currentYBumpDistance) + TextContainer.CreatorNamePrefix.FontSize = textSizeEnum + + -- recalculate bottom bar size + local sizeScale = closestValidTextSize/48 + screenGui.MainBackgroundContainer.BottomBar.Size = UDim2.new(1, 0, 0, 300 * sizeScale) + screenGui.MainBackgroundContainer.BottomBar.Position = UDim2.new(0, 0, 1, -150 * sizeScale) + screenGui.MainBackgroundContainer.BottomBar.BottomBarActual.Position = UDim2.new(0, -130 * sizeScale,0,0) +end + +function MainGui:RecalculateSizes(screenGui) + local screenSize = screenGui.AbsoluteSize + + -- recalculate thumbnail size + local thumbnailSizeScale = math.min(math.max(screenSize.y/630, 50/230), 1) + local thumbnailSize = UDim2.new(0, thumbnailSizeScale*420, 0, thumbnailSizeScale*230) + local thumbnailPosition = UDim2.new(0.5, -thumbnailSizeScale*420/2, 0.5, -20 - thumbnailSizeScale*230/2 ) + screenGui.ThumbnailContainer.Size = thumbnailSize + screenGui.ThumbnailContainer.Position = thumbnailPosition + + screenGui.ThumbnailContainer.LoadingInfoContainer.Visible = (screenSize.Y > 500) + + -- update names + + -- if we don't have a name yet, keep trying! + if InfoProvider:GetCreatorName() == '' or InfoProvider:GetGameName() == '' then + Spawn(function() + + while InfoProvider and InfoProvider:GetCreatorName() == '' or InfoProvider:GetGameName() == '' do + wait() + end + + if screenGui and screenGui:FindFirstChild("MainBackgroundContainer") then + screenGui.MainBackgroundContainer.BottomBar.BottomBarActual.TextContainer.CreatorName.Text = InfoProvider:GetCreatorName() + screenGui.MainBackgroundContainer.BottomBar.BottomBarActual.TextContainer.GameName.Text = InfoProvider:GetGameName() + end + + MainGui:RecalculateTextSize(screenGui) + end) + else + screenGui.MainBackgroundContainer.BottomBar.BottomBarActual.TextContainer.CreatorName.Text = InfoProvider:GetCreatorName() + screenGui.MainBackgroundContainer.BottomBar.BottomBarActual.TextContainer.GameName.Text = InfoProvider:GetGameName() + end + + MainGui:RecalculateTextSize(screenGui) +end + +function MainGui:Show() + currScreenGui = MainGui:GenerateMain() + currScreenGui.MainBackgroundContainer.Visible = true + currScreenGui.ThumbnailContainer.Visible = true + + currScreenGui.Changed:connect(function(prop) + if prop == "AbsoluteSize" then + MainGui:RecalculateSizes(currScreenGui) + end + end) +end + + + +--------------------------------------------------------- +-- Main Script (show something now + setup connections) + +-- start loading assets asap +InfoProvider:LoadAssets() + +MainGui:Show() + +local guiService = Game:GetService("GuiService") +local instanceCount = 0 +local voxelCount = 0 +local brickCount = 0 +local connectorCount = 0 +local setVerb = true + +renderSteppedConnection = Game:GetService("RunService").RenderStepped:connect(function() + + instanceCount = guiService:GetInstanceCount() + voxelCount = guiService:GetVoxelCount() + brickCount = guiService:GetBrickCount() + connectorCount = guiService:GetConnectorCount() + + if not currScreenGui then return end + + if setVerb then + currScreenGui.MainBackgroundContainer.CloseButton:SetVerb("Exit") + setVerb = false + end + + currScreenGui.ThumbnailContainer.LoadingInfoContainer.InstancesValue.Text = tostring(instanceCount) + currScreenGui.ThumbnailContainer.LoadingInfoContainer.BricksValue.Text = tostring(brickCount) + currScreenGui.ThumbnailContainer.LoadingInfoContainer.ConnectorsValue.Text = tostring(connectorCount) + + if voxelCount <= 0 then + currScreenGui.ThumbnailContainer.LoadingInfoContainer.VoxelsValue.Text = "0" + else + currScreenGui.ThumbnailContainer.LoadingInfoContainer.VoxelsValue.Text = tostring(voxelCount) .." million" + end +end) + +guiService.ErrorMessageChanged:connect(function() + if guiService:GetErrorMessage() ~= '' then + currScreenGui.MainBackgroundContainer.ErrorFrame.ErrorText.Text = guiService:GetErrorMessage() + currScreenGui.MainBackgroundContainer.ErrorFrame.Visible = true + else + currScreenGui.MainBackgroundContainer.ErrorFrame.Visible = false + end +end) + +if guiService:GetErrorMessage() ~= '' then + currScreenGui.MainBackgroundContainer.ErrorFrame.ErrorText.Text = guiService:GetErrorMessage() + currScreenGui.MainBackgroundContainer.ErrorFrame.Visible = true +end + + +local forceRemovalTime = 5 +local destroyed = false + +function removeLoadingScreen() + if renderSteppedConnection then + renderSteppedConnection:disconnect() + end + + if currScreenGui then + currScreenGui:Destroy() + currScreenGui = nil + end + + if script then script:Destroy() end + destroyed = true +end + + +function startForceLoadingDoneTimer() + wait(forceRemovalTime) + removeLoadingScreen() +end + +function gameIsLoaded() + if Game.ReplicatedFirst:IsDefaultLoadingGuiRemoved() then + removeLoadingScreen() + else + startForceLoadingDoneTimer() + end +end + +Game.ReplicatedFirst.RemoveDefaultLoadingGuiSignal:connect(function() + removeLoadingScreen() +end) + +if Game.ReplicatedFirst:IsDefaultLoadingGuiRemoved() then + removeLoadingScreen() + return +end + +Game.Loaded:connect(function() + gameIsLoaded() +end) + +if Game:IsLoaded() then + gameIsLoaded() +end + +-------------------------------------------------------------------------- +-- +-- Animation (make the stuff we are showing look cool) + +local blockSize = 10 +local blockColor = Color3.new(33/255,66/255,209/255) + +local yPosScale = 0 +local yPosOffset = -blockSize * 3.5 + +local tweenStyle = Enum.EasingStyle.Sine +local tweenVelocity = 1500 +local tweenTime = (currScreenGui.AbsoluteSize.X/2)/tweenVelocity + +function createBlock() + local initBlock = Instance.new("Frame") + initBlock.ZIndex = 5 + initBlock.Size = UDim2.new(0,blockSize,0,blockSize) + initBlock.BackgroundColor3 = COLORS.DARK + initBlock.BorderSizePixel = 0 + initBlock.Position = UDim2.new(0,-blockSize,yPosScale,yPosOffset) + initBlock.Parent = currScreenGui.MainBackgroundContainer.BottomBar + + return initBlock +end + + +local blocks = {} +for i = 1,6 do + blocks[i] = createBlock() +end + +function getYOffset(newSize) + return yPosOffset - (newSize/3) +end + +function rightScreenExit() + wait(tweenTime * 3) + if not currScreenGui then return end + + local regSize = blocks[6].Size + local regPos = blocks[6].Position + + blocks[6].Size = blocks[1].Size + blocks[6].Position = blocks[1].Position + + blocks[1].Size = regSize + blocks[1].Position = regPos + + wait() + + for i = 1,6 do + local delayTime = tweenTime * (i - 1) * 0.5 + Delay(delayTime, function() + if not currScreenGui then return end + + local blockIndex = i + local blockSizeMultiplier = 4 - (i * 0.5) + + blocks[blockIndex]:TweenPosition(UDim2.new(1,0,yPosScale,yPosOffset), + Enum.EasingDirection.Out,tweenStyle, + tweenTime,true) + if i == 6 then + blocks[6]:TweenSizeAndPosition(UDim2.new(0,blockSize,0,blockSize), + UDim2.new(1,0,yPosScale,yPosOffset), + Enum.EasingDirection.InOut,tweenStyle, + tweenTime,true) + + wait(tweenTime * 1.1) + leftScreenEntrance() + else + local newSize = blockSize * blockSizeMultiplier + blocks[6]:TweenSizeAndPosition(UDim2.new(0,newSize,0,newSize), + UDim2.new(0.5,-newSize/2,yPosScale,getYOffset(newSize)), + Enum.EasingDirection.InOut,tweenStyle, + tweenTime * 0.75,true) + end + end) + end +end + +function leftScreenEntrance() + if not currScreenGui then return end + + for i = 1,6 do + blocks[i].Size = UDim2.new(0,blockSize,0,blockSize) + blocks[i].Position = UDim2.new(0,-blockSize,yPosScale,yPosOffset) + end + + blocks[1]:TweenPosition(UDim2.new(0.5,-blockSize/2,yPosScale,yPosOffset),Enum.EasingDirection.Out,tweenStyle,tweenTime,true,function() + for i = 1, 6 do + local delayTime = tweenTime * (i - 1) * 0.5 + + Delay(delayTime, function() + if not currScreenGui then return end + + local blockIndex = i + local blockSizeMultiplier = 1 + (i * 0.5) + + blocks[blockIndex]:TweenPosition(UDim2.new(0.5,-blockSize/2,yPosScale,yPosOffset), + Enum.EasingDirection.Out,tweenStyle, + tweenTime,true) + + local newSize = blockSize * blockSizeMultiplier + blocks[1]:TweenSizeAndPosition(UDim2.new(0,newSize,0,newSize), + UDim2.new(0.5,-newSize/2,yPosScale,getYOffset(newSize)), + Enum.EasingDirection.InOut,tweenStyle, + tweenTime * 0.75,true) + + + if i == 4 then + rightScreenExit() + end + end) + end + end) +end + +function startLoadingAnimation() + currScreenGui.MainBackgroundContainer.BackgroundThumbnail:TweenPosition(UDim2.new(0,0,0,0),Enum.EasingDirection.InOut,Enum.EasingStyle.Linear,20,true) + leftScreenEntrance() +end + + +---------------------------------- +-- Animation Begin +startLoadingAnimation() \ No newline at end of file diff --git a/Client2016/content/fonts/Rocket.rbxm b/Client2016/content/fonts/Rocket.rbxm new file mode 100644 index 0000000..72beea5 --- /dev/null +++ b/Client2016/content/fonts/Rocket.rbxm @@ -0,0 +1,102 @@ + + null + nil + + + false + -0.5 + 0.5 + 3 + 0 + -0.5 + 0.5 + 3 + 0 + 23 + + -0.5 + 0.5 + 0 + -1.1920929e-007 + 1.00000012 + 0 + 1.00000012 + -1.1920929e-007 + 0 + 0 + 0 + -1.00000024 + + true + true + 0 + true + true + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 3 + 0 + -0.5 + 0.5 + 3 + 0 + false + Rocket + 0 + -0.5 + 0.5 + 3 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 1 + + 1 + 1 + 4 + + + + + true + Swoosh + 0 + false + rbxasset://sounds\Rocket whoosh 01.wav + 0.699999988 + + + + + false + Explosion + 0 + true + rbxasset://sounds\collide.wav + 1 + + + + + Script + r = game:service("RunService") shaft = script.Parent position = Vector3.new(0,0,0) function fly() direction = shaft.CFrame.lookVector position = position + direction error = position - shaft.Position shaft.Velocity = 7*error end function blow() swoosh:Stop() explosion = Instance.new("Explosion") explosion.Position = shaft.Position explosion.Parent = game.Workspace connection:disconnect() shaft:remove() end t, s = r.Stepped:wait() swoosh = script.Parent.Swoosh swoosh:Play() position = shaft.Position d = t + 10.0 - s connection = shaft.Touched:connect(blow) while t < d do fly() t = r.Stepped:wait() end script.Parent.Explosion.PlayOnRemove = false swoosh:Stop() shaft:remove() + + + + \ No newline at end of file diff --git a/Client2016/content/fonts/SlingshotPellet.rbxm b/Client2016/content/fonts/SlingshotPellet.rbxm new file mode 100644 index 0000000..d753b0a --- /dev/null +++ b/Client2016/content/fonts/SlingshotPellet.rbxm @@ -0,0 +1,82 @@ + + null + nil + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 194 + + 0 + 6.4000001 + -8 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + true + 0 + true + true + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + false + Pellet + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + + 2 + 2 + 2 + + + + + Script + pellet = script.Parent damage = 8 function onTouched(hit) humanoid = hit.Parent:findFirstChild("Humanoid") if humanoid~=nil then humanoid.Health = humanoid.Health - damage connection:disconnect() else damage = damage / 2 if damage < 0.1 then connection:disconnect() end end end connection = pellet.Touched:connect(onTouched) r = game:service("RunService") t, s = r.Stepped:wait() d = t + 1.0 - s while t < d do t = r.Stepped:wait() end pellet.Parent = nil + + + + \ No newline at end of file diff --git a/Client2016/content/fonts/SourceSans.font b/Client2016/content/fonts/SourceSans.font new file mode 100644 index 0000000..6b7a4f3 Binary files /dev/null and b/Client2016/content/fonts/SourceSans.font differ diff --git a/Client2016/content/fonts/SourceSansBold.font b/Client2016/content/fonts/SourceSansBold.font new file mode 100644 index 0000000..f3520e0 Binary files /dev/null and b/Client2016/content/fonts/SourceSansBold.font differ diff --git a/Client2016/content/fonts/SourceSansItalic.font b/Client2016/content/fonts/SourceSansItalic.font new file mode 100644 index 0000000..6c49189 Binary files /dev/null and b/Client2016/content/fonts/SourceSansItalic.font differ diff --git a/Client2016/content/fonts/SourceSansLight.font b/Client2016/content/fonts/SourceSansLight.font new file mode 100644 index 0000000..5bb3ac8 Binary files /dev/null and b/Client2016/content/fonts/SourceSansLight.font differ diff --git a/Client2016/content/fonts/SourceSansPro-Bold.ttf b/Client2016/content/fonts/SourceSansPro-Bold.ttf new file mode 100644 index 0000000..be46652 Binary files /dev/null and b/Client2016/content/fonts/SourceSansPro-Bold.ttf differ diff --git a/Client2016/content/fonts/SourceSansPro-It.ttf b/Client2016/content/fonts/SourceSansPro-It.ttf new file mode 100644 index 0000000..c689cd2 Binary files /dev/null and b/Client2016/content/fonts/SourceSansPro-It.ttf differ diff --git a/Client2016/content/fonts/SourceSansPro-Light.ttf b/Client2016/content/fonts/SourceSansPro-Light.ttf new file mode 100644 index 0000000..dcff4e9 Binary files /dev/null and b/Client2016/content/fonts/SourceSansPro-Light.ttf differ diff --git a/Client2016/content/fonts/SourceSansPro-Regular.ttf b/Client2016/content/fonts/SourceSansPro-Regular.ttf new file mode 100644 index 0000000..a011dff Binary files /dev/null and b/Client2016/content/fonts/SourceSansPro-Regular.ttf differ diff --git a/Client2016/content/fonts/arial.ttf b/Client2016/content/fonts/arial.ttf new file mode 100644 index 0000000..4ba94d1 Binary files /dev/null and b/Client2016/content/fonts/arial.ttf differ diff --git a/Client2016/content/fonts/arialbd.ttf b/Client2016/content/fonts/arialbd.ttf new file mode 100644 index 0000000..2ca4b28 Binary files /dev/null and b/Client2016/content/fonts/arialbd.ttf differ diff --git a/Client2016/content/fonts/character.rbxm b/Client2016/content/fonts/character.rbxm new file mode 100644 index 0000000..7bf4b8a --- /dev/null +++ b/Client2016/content/fonts/character.rbxm @@ -0,0 +1,527 @@ + + null + nil + + + 7 + true + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + erik.cassel + RBX1 + true + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 24 + + 0 + 4.5 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + true + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 2 + 1 + 1 + + + + + + 0 + Mesh + + 1.25 + 1.25 + 1.25 + + + + 1 + 1 + 1 + + true + + + + + 5 + face + 20 + 0 + rbxasset://textures/face.png + true + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 23 + + 0 + 3 + 25.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + true + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 2 + 0 + true + Torso + 0 + 0 + 0 + 2 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 2 + 2 + 1 + + + + + 5 + roblox + 20 + 0 + + + + true + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 24 + + 1.5 + 3 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Left Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 24 + + -1.5 + 3 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Right Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 119 + + 0.5 + 1 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Left Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 119 + + -0.5 + 1 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Right Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + 100 + false + 100 + Humanoid + false + + 0 + 0 + 0 + + + 0 + 0 + 0 + + 0 + null + + 0 + 0 + 0 + + true + + + + \ No newline at end of file diff --git a/Client2016/content/fonts/character3.rbxm b/Client2016/content/fonts/character3.rbxm new file mode 100644 index 0000000..e39f9d6 --- /dev/null +++ b/Client2016/content/fonts/character3.rbxm @@ -0,0 +1,599 @@ + + null + nil + + + 7 + true + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + erik.cassel + RBX1 + true + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 24 + + 0 + 4.5 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + true + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 2 + 1 + 1 + + + + + + 0 + Mesh + + 1.25 + 1.25 + 1.25 + + + + 1 + 1 + 1 + + true + + + + + 5 + face + 20 + 0 + rbxasset://textures/face.png + true + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 23 + + 0 + 3 + 25.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + true + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 2 + 0 + true + Torso + 0 + 0 + 0 + 2 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 2 + 2 + 1 + + + + + 5 + roblox + 20 + 0 + + + + true + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 24 + + 1.5 + 3 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Left Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 24 + + -1.5 + 3 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Right Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 119 + + 0.5 + 1 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Left Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 119 + + -0.5 + 1 + 25.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + Right Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + true + 1 + + 1 + 2 + 1 + + + + + + 100 + false + 100 + Humanoid + false + + 0 + 0 + 0 + + + 0 + 0 + 0 + + 0 + null + + 0 + 0 + 0 + + true + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 23 + + 0 + 3 + 25.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + false + 0 + true + false + 0.5 + 0 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 0 + 0 + true + HumanoidRootPart + 0 + 0 + 0 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 1 + + 0 + 0 + 0 + + true + 1 + + 2 + 2 + 1 + + + + + \ No newline at end of file diff --git a/Client2016/content/fonts/characterCameraScript.rbxmx b/Client2016/content/fonts/characterCameraScript.rbxmx new file mode 100644 index 0000000..ebed40f --- /dev/null +++ b/Client2016/content/fonts/characterCameraScript.rbxmx @@ -0,0 +1,4264 @@ + + null + nil + + + false + + CameraScript + + + + + + ClickToMove + y then + return x + else + return y + end + end + end + Utility.ViewSizeX = ViewSizeX + + local function ViewSizeY() + local camera = workspace.CurrentCamera + local x = camera and camera.ViewportSize.X or 0 + local y = camera and camera.ViewportSize.Y or 0 + if y == 0 then + return 768 + else + if x > y then + return y + else + return x + end + end + end + Utility.ViewSizeY = ViewSizeY + + local function AspectRatio() + return ViewSizeX() / ViewSizeY() + end + Utility.AspectRatio = AspectRatio + + local function FindChacterAncestor(part) + if part then + local humanoid = part:FindFirstChild("Humanoid") + if humanoid then + return part, humanoid + else + return FindChacterAncestor(part.Parent) + end + end + end + Utility.FindChacterAncestor = FindChacterAncestor + + + local function GetUnitRay(x, y, viewWidth, viewHeight, camera) + return camera:ScreenPointToRay(x, y) + end + Utility.GetUnitRay = GetUnitRay + + local RayCastIgnoreList = workspace.FindPartOnRayWithIgnoreList + local function Raycast(ray, ignoreNonCollidable, ignoreList) + local ignoreList = ignoreList or {} + local hitPart, hitPos = RayCastIgnoreList(workspace, ray, ignoreList) + if hitPart then + if ignoreNonCollidable and hitPart.CanCollide == false then + table.insert(ignoreList, hitPart) + return Raycast(ray, ignoreNonCollidable, ignoreList) + end + return hitPart, hitPos + end + return nil, nil + end + Utility.Raycast = Raycast + + + Utility.Round = function(num, roundToNearest) + roundToNearest = roundToNearest or 1 + return math.floor((num + roundToNearest/2) / roundToNearest) * roundToNearest + end + + local function AveragePoints(positions) + local avgPos = Vector2.new(0,0) + if #positions > 0 then + for i = 1, #positions do + avgPos = avgPos + positions[i] + end + avgPos = avgPos / #positions + end + return avgPos + end + Utility.AveragePoints = AveragePoints + + local function FuzzyEquals(numa, numb) + return numa + 0.1 > numb and numa - 0.1 < numb + end + Utility.FuzzyEquals = FuzzyEquals + + local LastInput = 0 + UIS.InputBegan:connect(function(inputObject, wasSunk) + if not wasSunk then + if inputObject.UserInputType == Enum.UserInputType.Touch or + inputObject.UserInputType == Enum.UserInputType.MouseButton1 or + inputObject.UserInputType == Enum.UserInputType.MouseButton2 then + LastInput = tick() + end + end + end) + Utility.GetLastInput = function() + return LastInput + end +end + +local humanoidCache = {} +local function findPlayerHumanoid(player) + local character = player and player.Character + if character then + local resultHumanoid = humanoidCache[player] + if resultHumanoid and resultHumanoid.Parent == character then + return resultHumanoid + else + humanoidCache[player] = nil -- Bust Old Cache + for _, child in pairs(character:GetChildren()) do + if child:IsA('Humanoid') then + humanoidCache[player] = child + return child + end + end + end + end +end + +local function CFrameInterpolator(c0, c1) -- (CFrame from, CFrame to) -> (float theta, (float fraction -> CFrame between)) + local fromAxisAngle = CFrame.fromAxisAngle + local components = CFrame.new().components + local inverse = CFrame.new().inverse + local v3 = Vector3.new + local acos = math.acos + local sqrt = math.sqrt + local invroot2 = 1 / math.sqrt(2) + -- The expanded matrix + local _, _, _, xx, yx, zx, + xy, yy, zy, + xz, yz, zz = components(inverse(c0)*c1) + -- The cos-theta of the axisAngles from + local cosTheta = (xx + yy + zz - 1)/2 + -- Rotation axis + local rotationAxis = v3(yz-zy, zx-xz, xy-yx) + -- The position to tween through + local positionDelta = (c1.p - c0.p) + -- Theta + local theta; + -- Catch degenerate cases + if cosTheta >= 0.999 then + -- Case same rotation, just return an interpolator over the positions + return 0, function(t) + return c0 + positionDelta*t + end + elseif cosTheta <= -0.999 then + -- Case exactly opposite rotations, disambiguate + theta = math.pi + xx = (xx + 1) / 2 + yy = (yy + 1) / 2 + zz = (zz + 1) / 2 + if xx > yy and xx > zz then + if xx < 0.001 then + rotationAxis = v3(0, invroot2, invroot2) + else + local x = sqrt(xx) + xy = (xy + yx) / 4 + xz = (xz + zx) / 4 + rotationAxis = v3(x, xy/x, xz/x) + end + elseif yy > zz then + if yy < 0.001 then + rotationAxis = v3(invroot2, 0, invroot2) + else + local y = sqrt(yy) + xy = (xy + yx) / 4 + yz = (yz + zy) / 4 + rotationAxis = v3(xy/y, y, yz/y) + end + else + if zz < 0.001 then + rotationAxis = v3(invroot2, invroot2, 0) + else + local z = sqrt(zz) + xz = (xz + zx) / 4 + yz = (yz + zy) / 4 + rotationAxis = v3(xz/z, yz/z, z) + end + end + else + -- Normal case, get theta from cosTheta + theta = acos(cosTheta) + end + -- Return the interpolator + return theta, function(t) + return c0*fromAxisAngle(rotationAxis, theta*t) + positionDelta*t + end +end +--------------------------------------------------------- + +local Signal = Utility.Signal +local Create = Utility.Create + +--------------------------CHARACTER CONTROL------------------------------- +local function CreateController() + local this = {} + + this.TorsoLookPoint = nil + + function this:SetTorsoLookPoint(point) + local humanoid = findPlayerHumanoid(Player) + if humanoid then + humanoid.AutoRotate = false + end + this.TorsoLookPoint = point + self:UpdateTorso() + delay(2, + function() + -- this isnt technically correct for detecting if this is the last issue to the setTorso function + if this.TorsoLookPoint == point then + this.TorsoLookPoint = nil + if humanoid then + humanoid.AutoRotate = true + end + end + end) + end + + function this:UpdateTorso(point) + if this.TorsoLookPoint then + point = this.TorsoLookPoint + else + return + end + + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + if torso then + local lookVec = (point - torso.CFrame.p).unit + local squashedLookVec = Vector3.new(lookVec.X, 0, lookVec.Z).unit + torso.CFrame = CFrame.new(torso.CFrame.p, torso.CFrame.p + squashedLookVec) + end + end + + return this +end + +local CharacterControl = CreateController() +----------------------------------------------------------------------- + +--------------------------PC AUTO JUMPER------------------------------- + +local function GetCharacter() + return Player and Player.Character +end + +local function GetTorso() + local humanoid = findPlayerHumanoid(Player) + return humanoid and humanoid.Torso +end + +local function IsPartAHumanoid(part) + return part and part.Parent and (part.Parent:FindFirstChild('Humanoid') ~= nil) +end + +local function doAutoJump() + local character = GetCharacter() + if (character == nil) then + return; + end + + local humanoid = findPlayerHumanoid(Player) + if (humanoid == nil) then + return; + end + + local rayLength = 1.5; + -- This is how high a ROBLOXian jumps from the mid point of his torso + local jumpHeight = 7.0; + + local torso = GetTorso() + if (torso == nil) then + return; + end + + local torsoCFrame = torso.CFrame; + local torsoLookVector = torsoCFrame.lookVector; + local torsoPos = torsoCFrame.p; + + local torsoRay = Ray.new(torsoPos + Vector3.new(0, -torso.Size.Y/2, 0), torsoLookVector * rayLength); + local jumpRay = Ray.new(torsoPos + Vector3.new(0, jumpHeight - torso.Size.Y, 0), torsoLookVector * rayLength); + + local hitPart, _ = RayCastIgnoreList(workspace, torsoRay, {character}, false) + local jumpHitPart, _ = RayCastIgnoreList(workspace, jumpRay, {character}, false) + + if (hitPart and jumpHitPart == nil and hitPart.CanCollide == true) then + -- NOTE: this follow line is not in the C++ impl, but an improvement in Click to Move + if not IsPartAHumanoid(hitPart) then + humanoid.Jump = true; + end + end +end + +local NO_JUMP_STATES = +{ + [Enum.HumanoidStateType.FallingDown] = false; + [Enum.HumanoidStateType.Flying] = false; + [Enum.HumanoidStateType.Freefall] = false; + [Enum.HumanoidStateType.GettingUp] = false; + [Enum.HumanoidStateType.Ragdoll] = false; + [Enum.HumanoidStateType.Running] = false; + [Enum.HumanoidStateType.Seated] = false; + [Enum.HumanoidStateType.Swimming] = false; + + -- Special case to detect if we are on a ladder + [Enum.HumanoidStateType.Climbing] = false; +} + +local function enableAutoJump() + local humanoid = findPlayerHumanoid(Player) + local currentState = humanoid and humanoid:GetState() + if currentState then + return NO_JUMP_STATES[currentState] == nil + end + return false +end + +local function getAutoJump() + return true +end + +local function vec3IsZero(vec3) + return vec3.magnitude < 0.05 +end + +-- NOTE: This function is radically different from the engine's implementation +local function calcDesiredWalkVelocity() + -- TEMP + return Vector3.new(1,1,1) +end + +local function preStepSimulatorSide(dt) + if getAutoJump() and enableAutoJump() then + local desiredWalkVelocity = calcDesiredWalkVelocity(); + if (not vec3IsZero(desiredWalkVelocity)) then + doAutoJump(); + end + end +end + +local function AutoJumper() + local this = {} + local running = false + local runRoutine = nil + + function this:Run() + running = true + local thisRoutine = nil + thisRoutine = coroutine.create(function() + while running and thisRoutine == runRoutine do + this:Step() + wait() + end + end) + runRoutine = thisRoutine + coroutine.resume(thisRoutine) + end + + function this:Stop() + running = false + end + + function this:Step() + preStepSimulatorSide() + end + + return this +end + +----------------------------------------------------------------------------- + +-----------------------------------PATHER-------------------------------------- + +local function CreateDestinationIndicator(pos) + local destinationGlobe = Create'Part' + { + Name = 'PathGlobe'; + TopSurface = 'Smooth'; + BottomSurface = 'Smooth'; + Shape = 'Ball'; + CanCollide = false; + Size = Vector3.new(2,2,2); + BrickColor = BrickColor.new('Institutional white'); + Transparency = 0; + Anchored = true; + CFrame = CFrame.new(pos); + } + return destinationGlobe +end + +local function Pather(character, point) + local this = {} + + this.Cancelled = false + this.Started = false + + this.Finished = Signal.Create() + this.PathFailed = Signal.Create() + this.PathStarted = Signal.Create() + + this.PathComputed = false + + function this:YieldUntilPointReached(character, point, timeout) + timeout = timeout or 10000000 + + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + local start = tick() + local lastMoveTo = start + while torso and tick() - start < timeout and this.Cancelled == false do + local diffVector = (point - torso.CFrame.p) + local xzMagnitude = (diffVector * Vector3.new(1,0,1)).magnitude + if xzMagnitude < 6 then + -- Jump if the path is telling is to go upwards + if diffVector.Y >= 2.2 then + humanoid.Jump = true + end + end + -- The hard-coded number 2 here is from the engine's MoveTo implementation + if xzMagnitude < 2 then + return true + end + -- Keep on issuing the move command because it will automatically quit every so often. + if tick() - lastMoveTo > 1.5 then + humanoid:MoveTo(point) + lastMoveTo = tick() + end + CharacterControl:UpdateTorso(point) + wait() + end + return false + end + + function this:Cancel() + this.Cancelled = true + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + if humanoid and torso then + humanoid:MoveTo(torso.CFrame.p) + end + end + + function this:CheckOcclusion(point1, point2, character, torsoRadius) + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + if torsoRadius == nil then + torsoRadius = torso and Vector3.new(torso.Size.X/2,0,torso.Size.Z/2) or Vector3.new(1,0,1) + end + + local diffVector = point2 - point1 + local directionVector = diffVector.unit + + local rightVector = Vector3.new(0,1,0):Cross(directionVector) * torsoRadius + + local rightPart, _ = Utility.Raycast(Ray.new(point1 + rightVector, diffVector + rightVector), true, {character}) + local hitPart, _ = Utility.Raycast(Ray.new(point1, diffVector), true, {character}) + local leftPart, _ = Utility.Raycast(Ray.new(point1 - rightVector, diffVector - rightVector), true, {character}) + + if rightPart or hitPart or leftPart then + return false + end + + -- Make sure we have somewhere to stand on + local midPt = (point2 + point1) / 2 + local studsBetweenSamples = 2 + for i = 1, math.floor(diffVector.magnitude/studsBetweenSamples) do + local downPart, _ = Utility.Raycast(Ray.new(point1 + directionVector * i * studsBetweenSamples, Vector3.new(0,-7,0)), true, {character}) + if not downPart then + return false + end + end + + return true + end + + function this:SmoothPoints(pathToSmooth) + local result = {} + + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + for i = 1, #pathToSmooth do + table.insert(result, pathToSmooth[i]) + end + + -- Backwards for safe-deletion + for i = #result - 1, 1, -1 do + if i + 1 <= #result then + + local nextPoint = result[i+1] + local thisPoint = result[i] + + local lastPoint = result[i-1] + if lastPoint == nil then + lastPoint = torso and Vector3.new(torso.CFrame.p.X, thisPoint.Y, torso.CFrame.p.Z) + end + + if lastPoint and Utility.FuzzyEquals(thisPoint.Y, lastPoint.Y) and Utility.FuzzyEquals(thisPoint.Y, nextPoint.Y) then + if this:CheckOcclusion(lastPoint, nextPoint, character) then + table.remove(result, i) + -- Move i back one to recursively-smooth + i = i + 1 + end + end + end + end + + return result + end + + function this:CheckNeighboringCells(character) + local pathablePoints = {} + local humanoid = findPlayerHumanoid(Player) + local torso = character and humanoid and humanoid.Torso + if torso then + local torsoCFrame = torso.CFrame + local torsoPos = torsoCFrame.p + -- Minus and plus 2 is so we can get it into the cell-corner space and then translate it back into cell-center space + local roundedPos = Vector3.new(Utility.Round(torsoPos.X-2,4)+2, Utility.Round(torsoPos.Y-2,4)+2, Utility.Round(torsoPos.Z-2,4)+2) + local neighboringCells = {} + for x = -4, 4, 8 do + for z = -4, 4, 8 do + table.insert(neighboringCells, roundedPos + Vector3.new(x,0,z)) + end + end + for _, testPoint in pairs(neighboringCells) do + local pathable = this:CheckOcclusion(roundedPos, testPoint, character, Vector3.new(0,0,0)) + if pathable then + table.insert(pathablePoints, testPoint) + end + end + end + return pathablePoints + end + + function this:ComputeDirectPath() + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + if torso then + local startPt = torso.CFrame.p + local finishPt = point + if (finishPt - startPt).magnitude < 150 then + -- move back the destination by 2 studs or otherwise the pather will collide with the object we are trying to reach + finishPt = finishPt - (finishPt - startPt).unit * 2 + if this:CheckOcclusion(startPt, finishPt, character, Vector3.new(0,0,0)) then + local pathResult = {} + pathResult.Status = Enum.PathStatus.Success + function pathResult:GetPointCoordinates() + return {finishPt} + end + return pathResult + end + end + end + end + + local function AllAxisInThreshhold(targetPt, otherPt, threshold) + return math.abs(targetPt.X - otherPt.X) <= threshold and + math.abs(targetPt.Y - otherPt.Y) <= threshold and + math.abs(targetPt.Z - otherPt.Z) <= threshold + end + + function this:ComputePath() + local smoothed = false + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + if torso then + if this.PathComputed then return end + this.PathComputed = true + -- Will yield the script since it is an Async script (start, finish, maxDistance) + -- Try to use the smooth function, but it may not exist yet :( + local success = pcall(function() + -- 3 is height from torso cframe to ground + this.pathResult = PathfindingService:ComputeSmoothPathAsync(torso.CFrame.p - Vector3.new(0,3,0), point, 400) + smoothed = true + end) + if not success then + -- 3 is height from torso cframe to ground + this.pathResult = PathfindingService:ComputeRawPathAsync(torso.CFrame.p - Vector3.new(0,3,0), point, 400) + smoothed = false + end + this.pointList = this.pathResult and this.pathResult:GetPointCoordinates() + local pathFound = false + if this.pathResult.Status == Enum.PathStatus.FailFinishNotEmpty then + -- Lets try again with a slightly set back start point; it is ok to do this again so the FailFinishNotEmpty uses little computation + local diffVector = point - workspace.CurrentCamera.CoordinateFrame.p + if diffVector.magnitude > 2 then + local setBackPoint = point - (diffVector).unit * 2.1 + local success = pcall(function() + this.pathResult = PathfindingService:ComputeSmoothPathAsync(torso.CFrame.p, setBackPoint, 400) + smoothed = true + end) + if not success then + this.pathResult = PathfindingService:ComputeRawPathAsync(torso.CFrame.p, setBackPoint, 400) + smoothed = false + end + this.pointList = this.pathResult and this.pathResult:GetPointCoordinates() + pathFound = true + end + end + if this.pathResult.Status == Enum.PathStatus.ClosestNoPath and #this.pointList >= 1 and pathFound == false then + local otherPt = this.pointList[#this.pointList] + if AllAxisInThreshhold(point, otherPt, 4) and (torso.CFrame.p - point).magnitude > (otherPt - point).magnitude then + local pathResult = {} + pathResult.Status = Enum.PathStatus.Success + function pathResult:GetPointCoordinates() + return {this.pointList} + end + this.pathResult = pathResult + pathFound = true + end + end + if (this.pathResult.Status == Enum.PathStatus.FailStartNotEmpty or this.pathResult.Status == Enum.PathStatus.ClosestNoPath) and pathFound == false then + local pathablePoints = this:CheckNeighboringCells(character) + for _, otherStart in pairs(pathablePoints) do + local pathResult; + local success = pcall(function() + pathResult = PathfindingService:ComputeSmoothPathAsync(otherStart, point, 400) + smoothed = true + end) + if not success then + pathResult = PathfindingService:ComputeRawPathAsync(otherStart, point, 400) + smoothed = false + end + if pathResult and pathResult.Status == Enum.PathStatus.Success then + this.pathResult = pathResult + if this.pathResult then + this.pointList = this.pathResult:GetPointCoordinates() + table.insert(this.pointList, 1, otherStart) + end + break + end + end + end + if DirectPathEnabled then + if this.pathResult.Status ~= Enum.PathStatus.Success then + local directPathResult = this:ComputeDirectPath() + if directPathResult and directPathResult.Status == Enum.PathStatus.Success then + this.pathResult = directPathResult + this.pointList = directPathResult:GetPointCoordinates() + end + end + end + end + return smoothed + end + + function this:IsValidPath() + this:ComputePath() + local pathStatus = this.pathResult.Status + return pathStatus == Enum.PathStatus.Success + end + + function this:GetPathStatus() + this:ComputePath() + return this.pathResult.Status + end + + function this:Start() + if CurrentSeatPart then + return + end + spawn(function() + local humanoid = findPlayerHumanoid(Player) + --humanoid.AutoRotate = false + local torso = humanoid and humanoid.Torso + if torso then + if this.Started then return end + this.Started = true + -- Will yield the script since it is an Async function script (start, finish, maxDistance) + local smoothed = this:ComputePath() + if this:IsValidPath() then + this.PathStarted:fire() + -- smooth out zig-zaggy paths + local smoothPath = smoothed and this.pointList or this:SmoothPoints(this.pointList) + for i, point in pairs(smoothPath) do + if humanoid then + if this.Cancelled then + return + end + + local wayPoint = nil + if SHOW_PATH then + wayPoint = CreateDestinationIndicator(point) + wayPoint.BrickColor = BrickColor.new("New Yeller") + wayPoint.Parent = workspace + print(wayPoint.CFrame.p) + end + + humanoid:MoveTo(point) + + local distance = ((torso.CFrame.p - point) * Vector3.new(1,0,1)).magnitude + local approxTime = 10 + if math.abs(humanoid.WalkSpeed) > 0 then + approxTime = distance / math.abs(humanoid.WalkSpeed) + end + + local yielding = true + + if i == 1 then + --local rotatedCFrame = CameraModule:LookAtPreserveHeight(point) + if CameraModule then + local rotatedCFrame = CameraModule:LookAtPreserveHeight(smoothPath[#smoothPath]) + local finishedSignal, duration = CameraModule:TweenCameraLook(rotatedCFrame) + end + --CharacterControl:SetTorsoLookPoint(point) + end + ---[[ + if (humanoid.Torso.CFrame.p - point).magnitude > 9 then + spawn(function() + while yielding and this.Cancelled == false do + if CameraModule then + local look = CameraModule:GetCameraLook() + local squashedLook = (look * Vector3.new(1,0,1)).unit + local direction = ((point - CameraModule.cframe.p) * Vector3.new(1,0,1)).unit + + local theta = math.deg(math.acos(squashedLook:Dot(direction))) + + if tick() - Utility.GetLastInput() > 2 and theta > (workspace.CurrentCamera.FieldOfView / 2) then + local rotatedCFrame = CameraModule:LookAtPreserveHeight(point) + local finishedSignal, duration = CameraModule:TweenCameraLook(rotatedCFrame) + --return + end + end + wait(0.1) + end + end) + end + --]] + local didReach = this:YieldUntilPointReached(character, point, approxTime * 3 + 1) + + yielding = false + + if SHOW_PATH then + wayPoint:Destroy() + end + + if not didReach then + this.PathFailed:fire() + return + end + end + end + + this.Finished:fire() + return + end + end + this.PathFailed:fire() + end) + end + + return this +end + +------------------------------------------------------------------------- + +local function FlashRed(object) + local origColor = object.BrickColor + local redColor = BrickColor.new("Really red") + local start = tick() + local duration = 4 + spawn(function() + while object and tick() - start < duration do + object.BrickColor = origColor + wait(0.13) + if object then + object.BrickColor = redColor + end + wait(0.13) + end + end) +end + +--local joystickWidth = 250 +--local joystickHeight = 250 +local function IsInBottomLeft(pt) + local joystickHeight = math.min(Utility.ViewSizeY() * 0.33, 250) + local joystickWidth = joystickHeight + return pt.X <= joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight +end + +local function IsInBottomRight(pt) + local joystickHeight = math.min(Utility.ViewSizeY() * 0.33, 250) + local joystickWidth = joystickHeight + return pt.X >= Utility.ViewSizeX() - joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight +end + +local function CheckAlive(character) + local humanoid = findPlayerHumanoid(Player) + return humanoid ~= nil and humanoid.Health > 0 +end + +local function GetEquippedTool(character) + if character ~= nil then + for _, child in pairs(character:GetChildren()) do + if child:IsA('Tool') then + return child + end + end + end +end + +local function ExploreWithRayCast(currentPoint, originDirection) + local TestDistance = 40 + local TestVectors = {} + do + local forwardVector = originDirection; + for i = 0, 15 do + table.insert(TestVectors, CFrame.Angles(0, math.pi / 8 * i, 0) * forwardVector) + end + end + + local testResults = {} + -- Heuristic should be something along the lines of distance and closeness to the traveling direction + local function ExploreHeuristic() + for _, testData in pairs(testResults) do + local walkDirection = -1 * originDirection + local directionCoeff = (walkDirection:Dot(testData['Vector']) + 1) / 2 + local distanceCoeff = testData['Distance'] / TestDistance + testData["Value"] = directionCoeff * distanceCoeff + end + end + + for i, vec in pairs(TestVectors) do + local hitPart, hitPos = Utility.Raycast(Ray.new(currentPoint, vec * TestDistance), true, {Player.Character}) + if hitPos then + table.insert(testResults, {Vector = vec; Distance = (hitPos - currentPoint).magnitude}) + else + table.insert(testResults, {Vector = vec; Distance = TestDistance}) + end + end + + ExploreHeuristic() + + table.sort(testResults, function(a,b) return a["Value"] > b["Value"] end) + + return testResults +end + +local TapId = 1 +local ExistingPather = nil +local ExistingIndicator = nil +local PathCompleteListener = nil +local PathFailedListener = nil + +local function CleanupPath() + DrivingTo = nil + if ExistingPather then + ExistingPather:Cancel() + end + if PathCompleteListener then + PathCompleteListener:disconnect() + PathCompleteListener = nil + end + if PathFailedListener then + PathFailedListener:disconnect() + PathFailedListener = nil + end + if ExistingIndicator then + DebrisService:AddItem(ExistingIndicator, 0) + ExistingIndicator = nil + end +end + +local function getExtentsSize(Parts) + local maxX = Parts[1].Position.X + local maxY = Parts[1].Position.Y + local maxZ = Parts[1].Position.Z + local minX = Parts[1].Position.X + local minY = Parts[1].Position.Y + local minZ = Parts[1].Position.Z + for i = 2, #Parts do + maxX = math.max(maxX, Parts[i].Position.X) + maxY = math.max(maxY, Parts[i].Position.Y) + maxZ = math.max(maxZ, Parts[i].Position.Z) + minX = math.min(minX, Parts[i].Position.X) + minY = math.min(minY, Parts[i].Position.Y) + minZ = math.min(minZ, Parts[i].Position.Z) + end + return Region3.new(Vector3.new(minX, minY, minZ), Vector3.new(maxX, maxY, maxZ)) +end + +local function inExtents(Extents, Position) + if Position.X < (Extents.CFrame.p.X - Extents.Size.X/2) or Position.X > (Extents.CFrame.p.X + Extents.Size.X/2) then + return false + end + if Position.Z < (Extents.CFrame.p.Z - Extents.Size.Z/2) or Position.Z > (Extents.CFrame.p.Z + Extents.Size.Z/2) then + return false + end + --ignoring Y for now + return true +end + +local AutoJumperInstance = nil +local ShootCount = 0 +local FailCount = 0 +local function OnTap(tapPositions, goToPoint) + -- Good to remember if this is the latest tap event + TapId = TapId + 1 + local thisTapId = TapId + + + local camera = workspace.CurrentCamera + local character = Player.Character + + + if not CheckAlive(character) then return end + + -- This is a path tap position + if #tapPositions == 1 or goToPoint then + if camera then + local unitRay = Utility.GetUnitRay(tapPositions[1].x, tapPositions[1].y, MyMouse.ViewSizeX, MyMouse.ViewSizeY, camera) + local ray = Ray.new(unitRay.Origin, unitRay.Direction*400) + local hitPart, hitPt = Utility.Raycast(ray, true, {character}) + + local hitChar, hitHumanoid = Utility.FindChacterAncestor(hitPart) + local torso = character and character:FindFirstChild("Humanoid") and character:FindFirstChild("Humanoid").Torso + local startPos = torso.CFrame.p + if goToPoint then + hitPt = goToPoint + hitChar = nil + end + if hitChar and hitHumanoid and hitHumanoid.Torso and (hitHumanoid.Torso.CFrame.p - torso.CFrame.p).magnitude < 7 then + CleanupPath() + + local myHumanoid = findPlayerHumanoid(Player) + if myHumanoid then + myHumanoid:MoveTo(hitPt) + end + + ShootCount = ShootCount + 1 + local thisShoot = ShootCount + -- Do shooot + local currentWeapon = GetEquippedTool(character) + if currentWeapon then + currentWeapon:Activate() + LastFired = tick() + end + elseif hitPt and character and not CurrentSeatPart then + local thisPather = Pather(character, hitPt) + if thisPather:IsValidPath() then + FailCount = 0 + -- TODO: Remove when bug in engine is fixed + Player:Move(Vector3.new(1, 0, 0)) + Player:Move(Vector3.new(0, 0, 0)) + thisPather:Start() + if BindableEvent_OnFailStateChanged then + BindableEvent_OnFailStateChanged:Fire(false) + end + CleanupPath() + + local destinationGlobe = CreateDestinationIndicator(hitPt) + destinationGlobe.Parent = camera + + ExistingPather = thisPather + ExistingIndicator = destinationGlobe + + if AutoJumperInstance then + AutoJumperInstance:Run() + end + + PathCompleteListener = thisPather.Finished:connect(function() + if AutoJumperInstance then + AutoJumperInstance:Stop() + end + if destinationGlobe then + if ExistingIndicator == destinationGlobe then + ExistingIndicator = nil + end + DebrisService:AddItem(destinationGlobe, 0) + destinationGlobe = nil + end + if hitChar then + local humanoid = findPlayerHumanoid(Player) + ShootCount = ShootCount + 1 + local thisShoot = ShootCount + -- Do shoot + local currentWeapon = GetEquippedTool(character) + if currentWeapon then + currentWeapon:Activate() + LastFired = tick() + end + if humanoid then + humanoid:MoveTo(hitPt) + end + end + local finishPos = torso and torso.CFrame.p --hitPt + if finishPos and startPos and tick() - Utility.GetLastInput() > 2 then + local exploreResults = ExploreWithRayCast(finishPos, ((startPos - finishPos) * Vector3.new(1,0,1)).unit) + -- Check for Nans etc.. + if exploreResults[1] and exploreResults[1]["Vector"] and exploreResults[1]["Vector"].magnitude >= 0.5 and exploreResults[1]["Distance"] > 3 then + if CameraModule then + local rotatedCFrame = CameraModule:LookAtPreserveHeight(finishPos + exploreResults[1]["Vector"] * exploreResults[1]["Distance"]) + local finishedSignal, duration = CameraModule:TweenCameraLook(rotatedCFrame) + end + end + end + end) + PathFailedListener = thisPather.PathFailed:connect(function() + if AutoJumperInstance then + AutoJumperInstance:Stop() + end + if destinationGlobe then + FlashRed(destinationGlobe) + DebrisService:AddItem(destinationGlobe, 3) + end + end) + else + if hitPt then + -- Feedback here for when we don't have a good path + local failedGlobe = CreateDestinationIndicator(hitPt) + FlashRed(failedGlobe) + DebrisService:AddItem(failedGlobe, 1) + failedGlobe.Parent = camera + if ExistingIndicator == nil then + FailCount = FailCount + 1 + if FailCount >= 3 then + if BindableEvent_OnFailStateChanged then + BindableEvent_OnFailStateChanged:Fire(true) + end + CleanupPath() + end + end + end + end + elseif hitPt and character and CurrentSeatPart then + local destinationGlobe = CreateDestinationIndicator(hitPt) + destinationGlobe.Parent = camera + ExistingIndicator = destinationGlobe + DrivingTo = hitPt + local ConnectedParts = CurrentSeatPart:GetConnectedParts(true) + + while wait() do + if CurrentSeatPart and ExistingIndicator == destinationGlobe then + local ExtentsSize = getExtentsSize(ConnectedParts) + if inExtents(ExtentsSize, destinationGlobe.Position) then + DebrisService:AddItem(destinationGlobe, 0) + destinationGlobe = nil + DrivingTo = nil + break + end + else + DebrisService:AddItem(destinationGlobe, 0) + if CurrentSeatPart == nil and destinationGlobe == ExistingIndicator then + DrivingTo = nil + OnTap(tapPositions, hitPt) + end + destinationGlobe = nil + break + end + end + + else + -- no hit pt + end + end + elseif #tapPositions >= 2 then + if camera then + ShootCount = ShootCount + 1 + local thisShoot = ShootCount + -- Do shoot + local avgPoint = Utility.AveragePoints(tapPositions) + local unitRay = Utility.GetUnitRay(avgPoint.x, avgPoint.y, MyMouse.ViewSizeX, MyMouse.ViewSizeY, camera) + local currentWeapon = GetEquippedTool(character) + if currentWeapon then + currentWeapon:Activate() + LastFired = tick() + end + end + end +end + + +local function CreateClickToMoveModule() + local this = {} + + local LastStateChange = 0 + local LastState = Enum.HumanoidStateType.Running + local FingerTouches = {} + local NumUnsunkTouches = 0 + -- PC simulation + local mouse1Down = tick() + local mouse1DownPos = Vector2.new() + local mouse2Down = tick() + local mouse2DownPos = Vector2.new() + local mouse2Up = tick() + + local movementKeys = { + [Enum.KeyCode.W] = true; + [Enum.KeyCode.A] = true; + [Enum.KeyCode.S] = true; + [Enum.KeyCode.D] = true; + [Enum.KeyCode.Up] = true; + [Enum.KeyCode.Down] = true; + } + + local TapConn = nil + local InputBeganConn = nil + local InputChangedConn = nil + local InputEndedConn = nil + local HumanoidDiedConn = nil + local CharacterChildAddedConn = nil + local OnCharacterAddedConn = nil + local CharacterChildRemovedConn = nil + local RenderSteppedConn = nil + local HumanoidSeatedConn = nil + + local function disconnectEvent(event) + if event then + event:disconnect() + end + end + + local function DisconnectEvents() + disconnectEvent(TapConn) + disconnectEvent(InputBeganConn) + disconnectEvent(InputChangedConn) + disconnectEvent(InputEndedConn) + disconnectEvent(HumanoidDiedConn) + disconnectEvent(CharacterChildAddedConn) + disconnectEvent(OnCharacterAddedConn) + disconnectEvent(RenderSteppedConn) + disconnectEvent(CharacterChildRemovedConn) + pcall(function() RunService:UnbindFromRenderStep("ClickToMoveRenderUpdate") end) + disconnectEvent(HumanoidSeatedConn) + end + + + + local function IsFinite(num) + return num == num and num ~= 1/0 and num ~= -1/0 + end + + local function findAngleBetweenXZVectors(vec2, vec1) + return math.atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z) + end + + -- Setup the camera + CameraModule = ClassicCameraModule() + + do + -- Extend The Camera Module Class + function CameraModule:LookAtPreserveHeight(newLookAtPt) + local camera = workspace.CurrentCamera + + local focus = camera.Focus.p + + local cameraCFrame = CameraModule.cframe + local mag = Vector3.new(cameraCFrame.lookVector.x, 0, cameraCFrame.lookVector.z).magnitude + local newLook = (Vector3.new(newLookAtPt.x, focus.y, newLookAtPt.z) - focus).unit * mag + local flippedLook = newLook + Vector3.new(0, cameraCFrame.lookVector.y, 0) + + local distance = (focus - cameraCFrame.p).magnitude + + local newCamPos = focus - flippedLook.unit * distance + return CFrame.new(newCamPos, newCamPos + flippedLook) + end + + function CameraModule:TweenCameraLook(desiredCFrame, speed) + local e = 2.718281828459 + local function SCurve(t) + return 1/(1 + e^(-t*1.5)) + end + local function easeOutSine(t, b, c, d) + if t >= d then return b + c end + return c * math.sin(t/d * (math.pi/2)) + b; + end + + local theta, interper = CFrameInterpolator(CFrame.new(Vector3.new(), self:GetCameraLook()), desiredCFrame - desiredCFrame.p) + theta = Utility.Clamp(0, math.pi, theta) + local duration = 0.65 * SCurve(theta - math.pi/4) + 0.15 + if speed then + duration = theta / speed + end + local start = tick() + local finish = start + duration + + self.UpdateTweenFunction = function() + local currTime = tick() - start + local alpha = Utility.Clamp(0, 1, easeOutSine(currTime, 0, 1, duration)) + local newCFrame = interper(alpha) + local y = findAngleBetweenXZVectors(newCFrame.lookVector, self:GetCameraLook()) + if IsFinite(y) and math.abs(y) > 0.0001 then + self.RotateInput = self.RotateInput + Vector2.new(y, 0) + end + return (currTime >= finish or alpha >= 1) + end + end + end + --- Done Extending + + + local function OnTouchBegan(input, processed) + if FingerTouches[input] == nil and not processed then + NumUnsunkTouches = NumUnsunkTouches + 1 + end + FingerTouches[input] = processed + end + + local function OnTouchChanged(input, processed) + if FingerTouches[input] == nil then + FingerTouches[input] = processed + if not processed then + NumUnsunkTouches = NumUnsunkTouches + 1 + end + end + end + + local function OnTouchEnded(input, processed) + --print("Touch tap fake:" , processed) + --if not processed then + -- OnTap({input.Position}) + --end + if FingerTouches[input] ~= nil and FingerTouches[input] == false then + NumUnsunkTouches = NumUnsunkTouches - 1 + end + FingerTouches[input] = nil + end + + + local function OnCharacterAdded(character) + DisconnectEvents() + + InputBeganConn = UIS.InputBegan:connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchBegan(input, processed) + + + -- Give back controls when they tap both sticks + local wasInBottomLeft = IsInBottomLeft(input.Position) + local wasInBottomRight = IsInBottomRight(input.Position) + if wasInBottomRight or wasInBottomLeft then + for otherInput, _ in pairs(FingerTouches) do + if otherInput ~= input then + local otherInputInLeft = IsInBottomLeft(otherInput.Position) + local otherInputInRight = IsInBottomRight(otherInput.Position) + if otherInput.UserInputState ~= Enum.UserInputState.End and ((wasInBottomLeft and otherInputInRight) or (wasInBottomRight and otherInputInLeft)) then + if BindableEvent_OnFailStateChanged then + BindableEvent_OnFailStateChanged:Fire(true) + end + return + end + end + end + end + end + + -- Cancel path when you use the keyboard controls. + if processed == false and input.UserInputType == Enum.UserInputType.Keyboard and movementKeys[input.KeyCode] then + CleanupPath() + end + if input.UserInputType == Enum.UserInputType.MouseButton1 then + mouse1Down = tick() + mouse1DownPos = input.Position + end + if input.UserInputType == Enum.UserInputType.MouseButton2 then + mouse2Down = tick() + mouse2DownPos = input.Position + end + end) + + InputChangedConn = UIS.InputChanged:connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchChanged(input, processed) + end + end) + + InputEndedConn = UIS.InputEnded:connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchEnded(input, processed) + end + + if input.UserInputType == Enum.UserInputType.MouseButton2 then + mouse2Up = tick() + local currPos = input.Position + if mouse2Up - mouse2Down < 0.25 and (currPos - mouse2DownPos).magnitude < 5 then + local positions = {currPos} + OnTap(positions) + end + end + end) + + TapConn = UIS.TouchTap:connect(function(touchPositions, processed) + if not processed then + OnTap(touchPositions) + end + end) + + if not UIS.TouchEnabled then -- PC + if AutoJumperInstance then + AutoJumperInstance:Stop() + AutoJumperInstance = nil + end + AutoJumperInstance = AutoJumper() + end + + local function getThrottleAndSteer(object, point) + local lookVector = (point - object.Position) + lookVector = Vector3.new(lookVector.X, 0, lookVector.Z).unit + local objectVector = Vector3.new(object.CFrame.lookVector.X, 0, object.CFrame.lookVector.Z).unit + local dirVector = lookVector - objectVector + local mag = dirVector.magnitude + local degrees = math.deg(math.acos(lookVector:Dot(objectVector))) + local side = (object.CFrame:pointToObjectSpace(point).X > 0) + local throttle = 0 + if mag < 0.25 then + throttle = 1 + end + if mag > 1.8 then + throttle = -1 + end + local distance = CurrentSeatPart.Position - DrivingTo + local velocity = CurrentSeatPart.Velocity + if velocity.magnitude*1.5 > distance.magnitude then + if velocity.magnitude*0.5 > distance.magnitude then + throttle = -throttle + else + throttle = 0 + end + end + local steer = 0 + if degrees > 5 and degrees < 175 then + if side then + steer = 1 + else + steer = -1 + end + end + local rotatingAt = math.deg(CurrentSeatPart.RotVelocity.magnitude) + local degreesAway = math.max(math.min(degrees, 180 - degrees), 10) + if (CurrentSeatPart.RotVelocity.X < 0)== (steer < 0) then + if rotatingAt*1.5 > degreesAway then + if rotatingAt*0.5 > degreesAway then + steer = -steer + else + steer = 0 + end + end + end + return throttle, steer + end + + local function Update() + if CameraModule then + if CameraModule.UserPanningTheCamera then + CameraModule.UpdateTweenFunction = nil + else + if CameraModule.UpdateTweenFunction then + local done = CameraModule.UpdateTweenFunction() + if done then + CameraModule.UpdateTweenFunction = nil + end + end + end + CameraModule:Update() + end + if CurrentSeatPart then + if DrivingTo then + local throttle, steer = getThrottleAndSteer(CurrentSeatPart, DrivingTo) + CurrentSeatPart.Throttle = throttle + CurrentSeatPart.Steer = steer + end + end + end + + local success = pcall(function() RunService:BindToRenderStep("ClickToMoveRenderUpdate",Enum.RenderPriority.Camera.Value - 1,Update) end) + if not success then + if RenderSteppedConn then + RenderSteppedConn:disconnect() + end + RenderSteppedConn = RunService.RenderStepped:connect(Update) + end + + local WasAutoJumper = false + local WasAutoJumpMobile = false + local function onSeated(child, active, currentSeatPart) + if active then + if BindableEvent_EnableTouchJump then + BindableEvent_EnableTouchJump:Fire(true) + end + if currentSeatPart.ClassName == "VehicleSeat" then + CurrentSeatPart = currentSeatPart + if AutoJumperInstance then + AutoJumperInstance:Stop() + AutoJumperInstance = nil + WasAutoJumper = true + else + WasAutoJumper = false + end + if child.AutoJumpEnabled then + WasAutoJumpMobile = true + child.AutoJumpEnabled = false + end + end + else + CurrentSeatPart = nil + if BindableEvent_EnableTouchJump then + BindableEvent_EnableTouchJump:Fire(false) + end + if WasAutoJumper then + AutoJumperInstance = AutoJumper() + WasAutoJumper = false + end + if WasAutoJumpMobile then + child.AutoJumpEnabled = true + WasAutoJumpMobile = false + end + end + end + + local function OnCharacterChildAdded(child) + if UIS.TouchEnabled then + if child:IsA('Tool') then + child.ManualActivationOnly = true + end + end + if child:IsA('Humanoid') then + disconnectEvent(HumanoidDiedConn) + HumanoidDiedConn = child.Died:connect(function() + DebrisService:AddItem(ExistingIndicator, 1) + if AutoJumperInstance then + AutoJumperInstance:Stop() + AutoJumperInstance = nil + end + end) + local WasAutoJumper = false + local WasAutoJumpMobile = false + HumanoidSeatedConn = child.Seated:connect(function(active, seat) onSeated(child, active, seat) end) + if child.SeatPart then + onSeated(child, true, child.SeatPart) + end + end + end + + CharacterChildAddedConn = character.ChildAdded:connect(function(child) + OnCharacterChildAdded(child) + end) + CharacterChildRemovedConn = character.ChildRemoved:connect(function(child) + if UIS.TouchEnabled then + if child:IsA('Tool') then + child.ManualActivationOnly = false + end + end + end) + for _, child in pairs(character:GetChildren()) do + OnCharacterChildAdded(child) + end + end + + local Running = false + + function this:Stop() + if Running then + DisconnectEvents() + CleanupPath() + if AutoJumperInstance then + AutoJumperInstance:Stop() + AutoJumperInstance = nil + end + if CameraModule then + CameraModule.UpdateTweenFunction = nil + CameraModule:SetEnabled(false) + end + -- Restore tool activation on shutdown + if UIS.TouchEnabled then + local character = Player.Character + if character then + for _, child in pairs(character:GetChildren()) do + if child:IsA('Tool') then + child.ManualActivationOnly = false + end + end + end + end + DrivingTo = nil + Running = false + end + end + + function this:Start() + if not Running then + if Player.Character then -- retro-listen + OnCharacterAdded(Player.Character) + end + OnCharacterAddedConn = Player.CharacterAdded:connect(OnCharacterAdded) + if CameraModule then + CameraModule:SetEnabled(true) + end + Running = true + end + end + + return this +end + +return CreateClickToMoveModule +]]> + + + + + Invisicam + originalFade then + hit.LocalTransparencyModifier = math.max(originalFade, currentFade - FADE_RATE) + else + SavedHits[hit] = nil + end + end + end +end + +function Invisicam:SetMode(newMode) + AssertTypes(newMode, 'number') + for modeName, modeNum in pairs(MODE) do + if modeNum == newMode then + Mode = newMode + return + end + end + error("Invalid mode number") +end + +function Invisicam:SetCustomBehavior(func) + AssertTypes(func, 'function') + Behaviors[MODE.CUSTOM] = func +end + +-- Want to turn off Invisicam? Be sure to call this after. +function Invisicam:Cleanup() + for hit, originalFade in pairs(SavedHits) do + hit.LocalTransparencyModifier = originalFade + end +end + +--------------------- +--| Running Logic |-- +--------------------- + +-- Connect to the current and all future cameras +workspace.Changed:connect(OnWorkspaceChanged) +OnWorkspaceChanged('CurrentCamera') + +Player.CharacterAdded:connect(OnCharacterAdded) +if Player.Character then + OnCharacterAdded(Player.Character) +end + +Invisicam:SetMode(STARTING_MODE) + +Behaviors[MODE.CUSTOM] = function() end -- (Does nothing until SetCustomBehavior) +Behaviors[MODE.LIMBS] = LimbBehavior +Behaviors[MODE.MOVEMENT] = MoveBehavior +Behaviors[MODE.CORNERS] = CornerBehavior +Behaviors[MODE.CIRCLE1] = CircleBehavior +Behaviors[MODE.CIRCLE2] = CircleBehavior +Behaviors[MODE.LIMBMOVE] = LimbMoveBehavior + +return Invisicam +]]> + + + + + + PopperCam + 0.95 or hitPart.CanCollide == false) then + table.insert(ignoreList, hitPart) + else + return hitPart, hitPoint + end + until false +end + +local function ScreenToWorld(screenPoint, screenSize, pushDepth) + local cameraFOV, cameraCFrame = Camera.FieldOfView, Camera.CoordinateFrame + local imagePlaneDepth = screenSize.y / (2 * math.tan(math.rad(cameraFOV) / 2)) + local direction = Vector3.new(screenPoint.x - (screenSize.x / 2), (screenSize.y / 2) - screenPoint.y, -imagePlaneDepth) + local worldDirection = (cameraCFrame:vectorToWorldSpace(direction)).Unit + local theta = math.acos(math.min(1, worldDirection:Dot(cameraCFrame.lookVector))) + local fixedPushDepth = pushDepth / math.sin((math.pi / 2) - theta) + return cameraCFrame.p + worldDirection * fixedPushDepth +end + +local function OnCameraChanged(property) + if property == 'CameraSubject' then + VehicleParts = {} + + local newSubject = Camera.CameraSubject + if newSubject then + -- Determine if we should be popping at all + PopperEnabled = false + for _, subjectType in pairs(VALID_SUBJECTS) do + if newSubject:IsA(subjectType) then + PopperEnabled = true + break + end + end + + -- Get all parts of the vehicle the player is controlling + if newSubject:IsA('VehicleSeat') then + VehicleParts = newSubject:GetConnectedParts(true) + end + end + end +end + +local function OnCharacterAdded(player, character) + PlayerCharacters[player] = character +end + +local function OnPlayersChildAdded(child) + if child:IsA('Player') then + child.CharacterAdded:connect(function(character) + OnCharacterAdded(child, character) + end) + if child.Character then + OnCharacterAdded(child, child.Character) + end + end +end + +local function OnPlayersChildRemoved(child) + if child:IsA('Player') then + PlayerCharacters[child] = nil + end +end + +local function OnWorkspaceChanged(property) + if property == 'CurrentCamera' then + local newCamera = workspace.CurrentCamera + if newCamera then + Camera = newCamera + + if CameraChangeConn then + CameraChangeConn:disconnect() + end + CameraChangeConn = Camera.Changed:connect(OnCameraChanged) + OnCameraChanged('CameraSubject') + end + end +end + +------------------------- +--| Exposed Functions |-- +------------------------- + +function PopperCam:Update() + if PopperEnabled then + -- First, prep some intermediate vars + local focusPoint = Camera.Focus.p + local cameraCFrame = Camera.CoordinateFrame + local cameraFrontPoint = cameraCFrame.p + (cameraCFrame.lookVector * NEAR_CLIP_PLANE_OFFSET) + local screenSize = Camera.ViewportSize + local ignoreList = {} + for _, character in pairs(PlayerCharacters) do + table.insert(ignoreList, character) + end + for _, basePart in pairs(VehicleParts) do + table.insert(ignoreList, basePart) + end + + -- Cast rays at the near clip plane, from corresponding points near the focus point, + -- and find the direct line that is the most cut off + local largest = 0 + for _, screenScale in pairs(CAST_SCREEN_SCALES) do + local clipWorldPoint = ScreenToWorld(screenSize * screenScale, screenSize, NEAR_CLIP_PLANE_OFFSET) + local rayStartPoint = focusPoint + (clipWorldPoint - cameraFrontPoint) + local _, hitPoint = PiercingCast(rayStartPoint, clipWorldPoint, ignoreList) + local cutoffAmount = (hitPoint - clipWorldPoint).Magnitude + if cutoffAmount > largest then + largest = cutoffAmount + end + end + + -- Then check if the player zoomed since the last frame, + -- and if so, reset our pop history so we stop tweening + local zoomLevel = (cameraCFrame.p - focusPoint).Magnitude + if math.abs(zoomLevel - LastZoomLevel) > 0.001 then + LastPopAmount = 0 + end + + -- Finally, zoom the camera in (pop) by that most-cut-off amount, or the last pop amount if that's more + local popAmount = math.max(largest, LastPopAmount) + if popAmount > 0 then + Camera.CoordinateFrame = cameraCFrame + (cameraCFrame.lookVector * popAmount) + LastPopAmount = math.max(0, popAmount - POP_RESTORE_RATE) -- Shrink it for the next frame + end + + LastZoomLevel = zoomLevel + end +end + +-------------------- +--| Script Logic |-- +-------------------- + +-- Connect to the current and all future cameras +workspace.Changed:connect(OnWorkspaceChanged) +OnWorkspaceChanged('CurrentCamera') + +-- Connect to all Players so we can ignore their Characters +PlayersService.ChildRemoved:connect(OnPlayersChildRemoved) +PlayersService.ChildAdded:connect(OnPlayersChildAdded) +for _, player in pairs(PlayersService:GetPlayers()) do + OnPlayersChildAdded(player) +end + +return PopperCam +]]> + + + + + + RootCamera + = 0 then + return (k*t) / (k - t + 1) + end + return -((lowerK*-t) / (lowerK + t + 1)) + end + + -- DEADZONE + local DEADZONE = 0.1 + local function toSCurveSpace(t) + return (1 + DEADZONE) * (2*math.abs(t) - 1) - DEADZONE + end + + local function fromSCurveSpace(t) + return t/2 + 0.5 + end + + local function gamepadLinearToCurve(thumbstickPosition) + local function onAxis(axisValue) + local sign = 1 + if axisValue < 0 then + sign = -1 + end + local point = fromSCurveSpace(SCurveTranform(toSCurveSpace(math.abs(axisValue)))) + point = point * sign + return clamp(-1,1,point) + end + return Vector2.new(onAxis(thumbstickPosition.x), onAxis(thumbstickPosition.y)) + end + + function this:UpdateGamepad() + local gamepadPan = this.GamepadPanningCamera + if gamepadPan then + gamepadPan = gamepadLinearToCurve(gamepadPan) + local currentTime = tick() + if gamepadPan.X ~= 0 or gamepadPan.Y ~= 0 then + this.userPanningTheCamera = true + elseif gamepadPan == Vector2.new(0,0) then + lastThumbstickRotate = nil + if lastThumbstickPos == Vector2.new(0,0) then + currentSpeed = 0 + end + end + + local finalConstant = 0 + + if lastThumbstickRotate then + local elapsedTime = (currentTime - lastThumbstickRotate) * 10 + currentSpeed = currentSpeed + (maxSpeed * ((elapsedTime*elapsedTime)/numOfSeconds)) + + if currentSpeed > maxSpeed then currentSpeed = maxSpeed end + + if lastVelocity then + local velocity = (gamepadPan - lastThumbstickPos)/(currentTime - lastThumbstickRotate) + local velocityDeltaMag = (velocity - lastVelocity).magnitude + + if velocityDeltaMag > 12 then + currentSpeed = currentSpeed * (20/velocityDeltaMag) + if currentSpeed > maxSpeed then currentSpeed = maxSpeed end + end + end + + finalConstant = thumbstickSensitivity * currentSpeed + lastVelocity = (gamepadPan - lastThumbstickPos)/(currentTime - lastThumbstickRotate) + end + + lastThumbstickPos = gamepadPan + lastThumbstickRotate = currentTime + + return Vector2.new( gamepadPan.X * finalConstant, gamepadPan.Y * finalConstant * ySensitivity) + end + + return Vector2.new(0,0) + end + + local InputBeganConn, InputChangedConn, InputEndedConn, ShiftLockToggleConn, GamepadConnectedConn, GamepadDisconnectedConn = nil, nil, nil, nil, nil, nil + + function this:DisconnectInputEvents() + if InputBeganConn then + InputBeganConn:disconnect() + InputBeganConn = nil + end + if InputChangedConn then + InputChangedConn:disconnect() + InputChangedConn = nil + end + if InputEndedConn then + InputEndedConn:disconnect() + InputEndedConn = nil + end + if ShiftLockToggleConn then + ShiftLockToggleConn:disconnect() + ShiftLockToggleConn = nil + end + if GamepadConnectedConn then + GamepadConnectedConn:disconnect() + GamepadConnectedConn = nil + end + if GamepadDisconnectedConn then + GamepadDisconnectedConn:disconnect() + GamepadDisconnectedConn = nil + end + this.TurningLeft = false + this.TurningRight = false + this.LastCameraTransform = nil + self.LastSubjectCFrame = nil + this.UserPanningTheCamera = false + this.RotateInput = Vector2.new() + this.GamepadPanningCamera = Vector2.new(0,0) + + -- Reset input states + startPos = nil + lastPos = nil + panBeginLook = nil + isRightMouseDown = false + isMiddleMouseDown = false + + fingerTouches = {} + NumUnsunkTouches = 0 + + StartingDiff = nil + pinchBeginZoom = nil + + -- Unlock mouse for example if right mouse button was being held down + if UserInputService.MouseBehavior ~= Enum.MouseBehavior.LockCenter then + UserInputService.MouseBehavior = Enum.MouseBehavior.Default + end + end + + function this:ConnectInputEvents() + InputBeganConn = UserInputService.InputBegan:connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchBegan(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseButton2 then + OnMouse2Down(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseButton3 then + OnMouse3Down(input, processed) + end + -- Keyboard + if input.UserInputType == Enum.UserInputType.Keyboard then + OnKeyDown(input, processed) + end + end) + + InputChangedConn = UserInputService.InputChanged:connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchChanged(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseMovement then + OnMouseMoved(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseWheel then + OnMouseWheel(input, processed) + end + end) + + InputEndedConn = UserInputService.InputEnded:connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchEnded(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseButton2 then + OnMouse2Up(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseButton3 then + OnMouse3Up(input, processed) + end + -- Keyboard + if input.UserInputType == Enum.UserInputType.Keyboard then + OnKeyUp(input, processed) + end + end) + + ShiftLockToggleConn = ShiftLockController.OnShiftLockToggled.Event:connect(function() + this:UpdateMouseBehavior() + end) + + this.RotateInput = Vector2.new() + + local activateGamepad = nil + local function assignActivateGamepad() + local connectedGamepads = UserInputService:GetConnectedGamepads() + if #connectedGamepads > 0 then + for i = 1, #connectedGamepads do + if activateGamepad == nil then + activateGamepad = connectedGamepads[i] + elseif connectedGamepads[i].Value < activateGamepad.Value then + activateGamepad = connectedGamepads[i] + end + end + end + + if activateGamepad == nil then -- nothing is connected, at least set up for gamepad1 + activateGamepad = Enum.UserInputType.Gamepad1 + end + end + + GamepadConnectedConn = UserInputService.GamepadDisconnected:connect(function(gamepadEnum) + if activateGamepad ~= gamepadEnum then return end + activateGamepad = nil + assignActivateGamepad() + end) + + GamepadDisconnectedConn = UserInputService.GamepadConnected:connect(function(gamepadEnum) + if activateGamepad == nil then + assignActivateGamepad() + end + end) + + local getGamepadPan = function(name, state, input) + if input.UserInputType == activateGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then + + if state == Enum.UserInputState.Cancel then + this.GamepadPanningCamera = Vector2.new(0,0) + return + end + + local inputVector = Vector2.new(input.Position.X, -input.Position.Y) + if inputVector.magnitude > THUMBSTICK_DEADZONE then + this.GamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y) + else + this.GamepadPanningCamera = Vector2.new(0,0) + end + end + end + + local doGamepadZoom = function(name, state, input) + if input.UserInputType == activateGamepad and input.KeyCode == Enum.KeyCode.ButtonR3 and state == Enum.UserInputState.Begin then + if this.ZoomEnabled then + if this.currentZoom > 0.5 then + this:ZoomCamera(0) + else + this:ZoomCamera(10) + end + end + end + end + + game.ContextActionService:BindAction("RootCamGamepadPan", getGamepadPan, false, Enum.KeyCode.Thumbstick2) + game.ContextActionService:BindAction("RootCamGamepadZoom", doGamepadZoom, false, Enum.KeyCode.ButtonR3) + + assignActivateGamepad() + + -- set mouse behavior + self:UpdateMouseBehavior() + end + + function this:SetEnabled(newState) + if newState ~= self.Enabled then + self.Enabled = newState + if self.Enabled then + self:ConnectInputEvents() + self.cframe = workspace.CurrentCamera.CoordinateFrame + else + self:DisconnectInputEvents() + end + end + end + + local function OnPlayerAdded(player) + player.Changed:connect(function(prop) + if this.Enabled then + if prop == "CameraMode" or prop == "CameraMaxZoomDistance" or prop == "CameraMinZoomDistance" then + this:ZoomCameraFixedBy(0) + end + end + end) + + local function OnCharacterAdded(newCharacter) + this:ZoomCamera(12.5) + local humanoid = findPlayerHumanoid(player) + local start = tick() + while tick() - start < 0.3 and (humanoid == nil or humanoid.Torso == nil) do + wait() + humanoid = findPlayerHumanoid(player) + end + local function setLookBehindCharacter() + if humanoid and humanoid.Torso and player.Character == newCharacter then + local newDesiredLook = (humanoid.Torso.CFrame.lookVector - Vector3.new(0,0.23,0)).unit + local horizontalShift = findAngleBetweenXZVectors(newDesiredLook, this:GetCameraLook()) + local vertShift = math.asin(this:GetCameraLook().y) - math.asin(newDesiredLook.y) + if not IsFinite(horizontalShift) then + horizontalShift = 0 + end + if not IsFinite(vertShift) then + vertShift = 0 + end + this.RotateInput = Vector2.new(horizontalShift, vertShift) + + -- reset old camera info so follow cam doesn't rotate us + this.LastCameraTransform = nil + end + end + wait() + setLookBehindCharacter() + end + + player.CharacterAdded:connect(function(character) + if this.Enabled or SetCameraOnSpawn then + OnCharacterAdded(character) + SetCameraOnSpawn = false + end + end) + if player.Character then + spawn(function() OnCharacterAdded(player.Character) end) + end + end + if PlayersService.LocalPlayer then + OnPlayerAdded(PlayersService.LocalPlayer) + end + PlayersService.ChildAdded:connect(function(child) + if child and PlayersService.LocalPlayer == child then + OnPlayerAdded(PlayersService.LocalPlayer) + end + end) + + return this +end + +return CreateCamera +]]> + + + + + AttachCamera + 1 then + module:ResetCameraLook() + self.LastCameraTransform = nil + end + + local subjectPosition = self:GetSubjectPosition() + if subjectPosition and player and camera then + local zoom = self:GetCameraZoom() + if zoom <= 0 then + zoom = 0.1 + end + + + local humanoid = self:GetHumanoid() + if lastUpdate and humanoid and humanoid.Torso then + + -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from + local delta = math.min(0.1, now - lastUpdate) + local gamepadRotation = self:UpdateGamepad() + self.RotateInput = self.RotateInput + (gamepadRotation * delta) + + local forwardVector = humanoid.Torso.CFrame.lookVector + + local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook()) + if IsFinite(y) then + -- Preserve vertical rotation from user input + self.RotateInput = Vector2.new(y, self.RotateInput.Y) + end + end + + local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput) + self.RotateInput = Vector2.new() + + + camera.Focus = CFrame.new(subjectPosition) + camera.CoordinateFrame = CFrame.new(camera.Focus.p - (zoom * newLookVector), camera.Focus.p) + self.LastCameraTransform = camera.CoordinateFrame + end + lastUpdate = now + end + + return module +end + +return CreateAttachCamera +]]> + + + + + + ClassicCamera + 1 then + module:ResetCameraLook() + self.LastCameraTransform = nil + end + + if lastUpdate then + -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from + local delta = math.min(0.1, now - lastUpdate) + local angle = 0 + if not (isInVehicle or isOnASkateboard) then + angle = angle + (self.TurningLeft and -120 or 0) + angle = angle + (self.TurningRight and 120 or 0) + end + + local gamepadRotation = self:UpdateGamepad() + if gamepadRotation ~= Vector2.new(0,0) then + userPanningTheCamera = true + self.RotateInput = self.RotateInput + (gamepadRotation * delta) + end + + if angle ~= 0 then + userPanningTheCamera = true + self.RotateInput = self.RotateInput + Vector2.new(math.rad(angle * delta), 0) + end + end + + -- Reset tween speed if user is panning + if userPanningTheCamera then + tweenSpeed = 0 + module.LastUserPanCamera = tick() + end + + local userRecentlyPannedCamera = now - module.LastUserPanCamera < timeBeforeAutoRotate + local subjectPosition = self:GetSubjectPosition() + + if subjectPosition and player and camera then + local zoom = self:GetCameraZoom() + if zoom < 0.5 then + zoom = 0.5 + end + + if self:GetShiftLock() and not self:IsInFirstPerson() then + -- We need to use the right vector of the camera after rotation, not before + local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput) + local offset = ((newLookVector * XZ_VECTOR):Cross(UP_VECTOR).unit * 1.75) + + if IsFiniteVector3(offset) then + subjectPosition = subjectPosition + offset + end + else + if self.LastCameraTransform and not userPanningTheCamera then + local isInFirstPerson = self:IsInFirstPerson() + if (isInVehicle or isOnASkateboard) and lastUpdate and humanoid and humanoid.Torso then + if isInFirstPerson then + if self.LastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then + local y = -findAngleBetweenXZVectors(self.LastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector) + if IsFinite(y) then + self.RotateInput = self.RotateInput + Vector2.new(y, 0) + end + tweenSpeed = 0 + end + elseif not userRecentlyPannedCamera then + local forwardVector = humanoid.Torso.CFrame.lookVector + if isOnASkateboard then + forwardVector = cameraSubject.CFrame.lookVector + end + local timeDelta = (now - lastUpdate) + + tweenSpeed = clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta) + + local percent = clamp(0, 1, tweenSpeed * timeDelta) + if self:IsInFirstPerson() then + percent = 1 + end + + local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook()) + if IsFinite(y) and math.abs(y) > 0.0001 then + self.RotateInput = self.RotateInput + Vector2.new(y * percent, 0) + end + end + end + end + end + + local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput) + self.RotateInput = Vector2.new() + + camera.Focus = CFrame.new(subjectPosition) + camera.CoordinateFrame = CFrame.new(camera.Focus.p - (zoom * newLookVector), camera.Focus.p) + Vector3.new(0, self:GetCameraHeight(), 0) + + self.LastCameraTransform = camera.CoordinateFrame + if isInVehicle or isOnASkateboard and cameraSubject:IsA('BasePart') then + self.LastSubjectCFrame = cameraSubject.CFrame + else + self.LastSubjectCFrame = nil + end + end + + lastUpdate = now + end + + return module +end + +return CreateClassicCamera]]> + + + + + + FixedCamera + 1 then + module:ResetCameraLook() + self.LastCameraTransform = nil + end + + if lastUpdate then + -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from + local delta = math.min(0.1, now - lastUpdate) + local gamepadRotation = self:UpdateGamepad() + self.RotateInput = self.RotateInput + (gamepadRotation * delta) + end + + local subjectPosition = self:GetSubjectPosition() + if subjectPosition and player and camera then + local zoom = self:GetCameraZoom() + if zoom <= 0 then + zoom = 0.1 + end + local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput) + self.RotateInput = Vector2.new() + + camera.CoordinateFrame = CFrame.new(camera.Focus.p - (zoom * newLookVector), camera.Focus.p) + self.LastCameraTransform = camera.CoordinateFrame + end + lastUpdate = now + end + + return module +end + +return CreateFixedCamera +]]> + + + + + + FollowCamera + 1 then + module:ResetCameraLook() + self.LastCameraTransform = nil + end + + if lastUpdate then + -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from + local delta = math.min(0.1, now - lastUpdate) + local angle = 0 + -- NOTE: Traditional follow camera does not rotate with arrow keys + if not (isInVehicle or isOnASkateboard) then + angle = angle + (self.TurningLeft and -120 or 0) + angle = angle + (self.TurningRight and 120 or 0) + end + + local gamepadRotation = self:UpdateGamepad() + if gamepadRotation ~= Vector2.new(0,0) then + userPanningTheCamera = true + self.RotateInput = self.RotateInput + (gamepadRotation * delta) + end + + if angle ~= 0 then + userPanningTheCamera = true + self.RotateInput = self.RotateInput + Vector2.new(math.rad(angle * delta), 0) + end + end + + -- Reset tween speed if user is panning + if userPanningTheCamera then + tweenSpeed = 0 + module.LastUserPanCamera = tick() + end + + local userRecentlyPannedCamera = now - module.LastUserPanCamera < timeBeforeAutoRotate + + local subjectPosition = self:GetSubjectPosition() + if subjectPosition and player and camera then + local zoom = self:GetCameraZoom() + if zoom < 0.5 then + zoom = 0.5 + end + + if self:GetShiftLock() and not self:IsInFirstPerson() then + local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput) + local offset = ((newLookVector * XZ_VECTOR):Cross(UP_VECTOR).unit * 1.75) + if IsFiniteVector3(offset) then + subjectPosition = subjectPosition + offset + end + else + if self.LastCameraTransform and not userPanningTheCamera then + local isInFirstPerson = self:IsInFirstPerson() + if (isClimbing or isInVehicle or isOnASkateboard) and lastUpdate and humanoid and humanoid.Torso then + if isInFirstPerson then + if self.LastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then + local y = -findAngleBetweenXZVectors(self.LastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector) + if IsFinite(y) then + self.RotateInput = self.RotateInput + Vector2.new(y, 0) + end + tweenSpeed = 0 + end + elseif not userRecentlyPannedCamera then + local forwardVector = humanoid.Torso.CFrame.lookVector + if isOnASkateboard then + forwardVector = cameraSubject.CFrame.lookVector + end + local timeDelta = (now - lastUpdate) + + tweenSpeed = clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta) + + local percent = clamp(0, 1, tweenSpeed * timeDelta) + if not isClimbing and self:IsInFirstPerson() then + percent = 1 + end + local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook()) + -- Check for NaN + if IsFinite(y) and math.abs(y) > 0.0001 then + self.RotateInput = self.RotateInput + Vector2.new(y * percent, 0) + end + end + elseif not (isInFirstPerson or userRecentlyPannedCamera) and (HasVRAPI and not UserInputService.VREnabled) then + local lastVec = -(self.LastCameraTransform.p - subjectPosition) + local y = findAngleBetweenXZVectors(lastVec, self:GetCameraLook()) + -- Check for NaNs + if IsFinite(y) and math.abs(y) > 0.0001 then + self.RotateInput = self.RotateInput + Vector2.new(y, 0) + end + end + end + end + local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput) + self.RotateInput = Vector2.new() + + camera.Focus = CFrame.new(subjectPosition) + camera.CoordinateFrame = CFrame.new(camera.Focus.p - (zoom * newLookVector), camera.Focus.p) + Vector3.new(0, self:GetCameraHeight(), 0) + + self.LastCameraTransform = camera.CoordinateFrame + if isInVehicle or isOnASkateboard and cameraSubject:IsA('BasePart') then + self.LastSubjectCFrame = cameraSubject.CFrame + else + self.LastSubjectCFrame = nil + end + end + + lastUpdate = now + end + + return module +end + +return CreateFollowCamera +]]> + + + + + + ScriptableCamera + + + + + + + TrackCamera + 1 then + module:ResetCameraLook() + self.LastCameraTransform = nil + end + + if lastUpdate then + -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from + local delta = math.min(0.1, now - lastUpdate) + local gamepadRotation = self:UpdateGamepad() + if gamepadRotation ~= Vector2.new(0,0) then + userPanningTheCamera = true + self.RotateInput = self.RotateInput + (gamepadRotation * delta) + end + end + + local subjectPosition = self:GetSubjectPosition() + if subjectPosition and player and camera then + local zoom = self:GetCameraZoom() + if zoom <= 0 then + zoom = 0.1 + end + local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput) + self.RotateInput = Vector2.new() + + camera.Focus = CFrame.new(subjectPosition) + camera.CoordinateFrame = CFrame.new(camera.Focus.p - (zoom * newLookVector), camera.Focus.p) + self.LastCameraTransform = camera.CoordinateFrame + end + lastUpdate = now + end + + return module +end + +return CreateTrackCamera +]]> + + + + + + WatchCamera + 1 then + module:ResetCameraLook() + self.LastCameraTransform = nil + self.LastZoom = nil + end + + + local subjectPosition = self:GetSubjectPosition() + if subjectPosition and player and camera then + local cameraLook = nil + + if self.LastCameraTransform then + local humanoid = self:GetHumanoid() + if humanoid and humanoid.Torso then + -- TODO: let the paging buttons move the camera but not the mouse/touch + -- currently neither do + local diffVector = subjectPosition - self.LastCameraTransform.p + cameraLook = diffVector.unit + + if self.LastZoom and self.LastZoom == self:GetCameraZoom() then + -- Don't clobber the zoom if they zoomed the camera + local zoom = diffVector.magnitude + self:ZoomCamera(zoom) + end + end + end + + local zoom = self:GetCameraZoom() + if zoom <= 0 then + zoom = 0.1 + end + + local newLookVector = self:RotateVector(cameraLook or self:GetCameraLook(), self.RotateInput) + self.RotateInput = Vector2.new() + local newFocus = CFrame.new(subjectPosition) + local newCamCFrame = CFrame.new(newFocus.p - (zoom * newLookVector), newFocus.p) + + camera.Focus = newFocus + camera.CoordinateFrame = newCamCFrame + self.LastCameraTransform = newCamCFrame + self.LastZoom = zoom + end + lastUpdate = now + end + + return module +end + +return CreateWatchCamera +]]> + + + + + + + ShiftLockController + + + + + + + TransparencyController + + + + + \ No newline at end of file diff --git a/Client2016/content/fonts/characterControlScript.rbxmx b/Client2016/content/fonts/characterControlScript.rbxmx new file mode 100644 index 0000000..f9a2a5e --- /dev/null +++ b/Client2016/content/fonts/characterControlScript.rbxmx @@ -0,0 +1,2064 @@ + + null + nil + + + false + + ControlScript + PlayerScripts. + + // Required Modules: + ClickToMove + DPad + KeyboardMovement + Thumbpad + Thumbstick + TouchJump + MasterControl + VehicleController +--]] + +local canUseNewControlScript = pcall(function() game:GetService('UserInputService'):GetLastInputType() end) +local success, value = pcall(function() return UserSettings():IsUserFeatureEnabled("UserUseNewControlScript") end) +local shouldUseNewControlScript = success and value + +if not canUseNewControlScript or not shouldUseNewControlScript then + +--[[ Services ]]-- +local ContextActionService = game:GetService('ContextActionService') +local Players = game:GetService('Players') +local UserInputService = game:GetService('UserInputService') +-- Settings and GameSettings are read only +local Settings = UserSettings() +local GameSettings = Settings.GameSettings + +-- Issue with play solo? (F6) +while not UserInputService.KeyboardEnabled and not UserInputService.TouchEnabled and not UserInputService.GamepadEnabled do + wait() +end + +--[[ Script Variables ]]-- +while not Players.LocalPlayer do + wait() +end +local LocalPlayer = Players.LocalPlayer +local PlayerGui = LocalPlayer:WaitForChild('PlayerGui') +local IsTouchDevice = UserInputService.TouchEnabled +local IsKeyboardDevice = UserInputService.KeyboardEnabled +local UserMovementMode = IsTouchDevice and GameSettings.TouchMovementMode or GameSettings.ComputerMovementMode +local DevMovementMode = IsTouchDevice and LocalPlayer.DevTouchMovementMode or LocalPlayer.DevComputerMovementMode +local IsUserChoice = (IsTouchDevice and DevMovementMode == Enum.DevTouchMovementMode.UserChoice) or + DevMovementMode == Enum.DevComputerMovementMode.UserChoice +local TouchGui = nil +local TouchControlFrame = nil +local TouchJumpModule = nil +local IsModalEnabled = UserInputService.ModalEnabled +local BindableEvent_OnFailStateChanged = nil +local BindableEvent_EnableTouchJump = nil +local isJumpEnabled = false + + +--[[ Modules ]]-- +local CurrentControlModule = nil +local ClickToMoveTouchControls = nil +local ControlModules = {} + +local MasterControl = require(script:WaitForChild('MasterControl')) + +if IsTouchDevice then + ControlModules.Thumbstick = require(script.MasterControl:WaitForChild('Thumbstick')) + ControlModules.Thumbpad = require(script.MasterControl:WaitForChild('Thumbpad')) + ControlModules.DPad = require(script.MasterControl:WaitForChild('DPad')) + ControlModules.Default = ControlModules.Thumbstick + TouchJumpModule = require(script.MasterControl:WaitForChild('TouchJump')) + BindableEvent_OnFailStateChanged = script.Parent:WaitForChild('OnClickToMoveFailStateChange') + BindableEvent_EnableTouchJump = script.Parent:WaitForChild('EnableTouchJump') +elseif IsKeyboardDevice then + ControlModules.Keyboard = require(script.MasterControl:WaitForChild('KeyboardMovement')) +end +ControlModules.Gamepad = require(script.MasterControl:WaitForChild('Gamepad')) +local success, value = pcall(function() return UserSettings():IsUserFeatureEnabled("UserUseLuaVehicleController") end) +if success and value then + local VehicleController = require(script.MasterControl:WaitForChild('VehicleController')) -- Not used, but needs to be required +end + + +--[[ Initialization/Setup ]]-- +local function createTouchGuiContainer() + if TouchGui then TouchGui:Destroy() end + + -- Container for all touch device guis + TouchGui = Instance.new('ScreenGui') + TouchGui.Name = "TouchGui" + TouchGui.Parent = PlayerGui + + TouchControlFrame = Instance.new('Frame') + TouchControlFrame.Name = "TouchControlFrame" + TouchControlFrame.Size = UDim2.new(1, 0, 1, 0) + TouchControlFrame.BackgroundTransparency = 1 + TouchControlFrame.Parent = TouchGui + + ControlModules.Thumbstick:Create(TouchControlFrame) + ControlModules.DPad:Create(TouchControlFrame) + ControlModules.Thumbpad:Create(TouchControlFrame) + TouchJumpModule:Create(TouchControlFrame) +end + +--[[ Local Functions ]]-- +local function setJumpModule(isEnabled) + if not isEnabled then + TouchJumpModule:Disable() + elseif CurrentControlModule == ControlModules.Thumbpad or CurrentControlModule == ControlModules.Thumbstick or + CurrentControlModule == ControlModules.Default then + -- + TouchJumpModule:Enable() + end +end + +local function setClickToMove() + if DevMovementMode == Enum.DevTouchMovementMode.ClickToMove or DevMovementMode == Enum.DevComputerMovementMode.ClickToMove or + UserMovementMode == Enum.ComputerMovementMode.ClickToMove or UserMovementMode == Enum.TouchMovementMode.ClickToMove then + -- + if IsTouchDevice then + ClickToMoveTouchControls = CurrentControlModule or ControlModules.Default + end + else + if IsTouchDevice and ClickToMoveTouchControls then + ClickToMoveTouchControls:Disable() + ClickToMoveTouchControls = nil + end + end +end + +--[[ Controls State Management ]]-- +local onControlsChanged = nil +if IsTouchDevice then + createTouchGuiContainer() + onControlsChanged = function() + local newModuleToEnable = nil + if not IsUserChoice then + if DevMovementMode == Enum.DevTouchMovementMode.Thumbstick then + newModuleToEnable = ControlModules.Thumbstick + isJumpEnabled = true + elseif DevMovementMode == Enum.DevTouchMovementMode.Thumbpad then + newModuleToEnable = ControlModules.Thumbpad + isJumpEnabled = true + elseif DevMovementMode == Enum.DevTouchMovementMode.DPad then + newModuleToEnable = ControlModules.DPad + isJumpEnabled = false + elseif DevMovementMode == Enum.DevTouchMovementMode.ClickToMove then + -- Managed by CameraScript + newModuleToEnable = nil + elseif DevMovementMode == Enum.DevTouchMovementMode.Scriptable then + newModuleToEnable = nil + end + else + if UserMovementMode == Enum.TouchMovementMode.Default or UserMovementMode == Enum.TouchMovementMode.Thumbstick then + newModuleToEnable = ControlModules.Thumbstick + isJumpEnabled = true + elseif UserMovementMode == Enum.TouchMovementMode.Thumbpad then + newModuleToEnable = ControlModules.Thumbpad + isJumpEnabled = true + elseif UserMovementMode == Enum.TouchMovementMode.DPad then + newModuleToEnable = ControlModules.DPad + isJumpEnabled = false + elseif UserMovementMode == Enum.TouchMovementMode.ClickToMove then + -- Managed by CameraScript + newModuleToEnable = nil + end + end + setClickToMove() + if newModuleToEnable ~= CurrentControlModule then + if CurrentControlModule then + CurrentControlModule:Disable() + end + setJumpModule(isJumpEnabled) + CurrentControlModule = newModuleToEnable + if CurrentControlModule and not IsModalEnabled then + CurrentControlModule:Enable() + if isJumpEnabled then TouchJumpModule:Enable() end + end + end + end +elseif UserInputService.KeyboardEnabled then + onControlsChanged = function() + -- NOTE: Click to move still uses keyboard. Leaving cases in case this ever changes. + local newModuleToEnable = nil + if not IsUserChoice then + if DevMovementMode == Enum.DevComputerMovementMode.KeyboardMouse then + newModuleToEnable = ControlModules.Keyboard + elseif DevMovementMode == Enum.DevComputerMovementMode.ClickToMove then + -- Managed by CameraScript + newModuleToEnable = ControlModules.Keyboard + end + else + if UserMovementMode == Enum.ComputerMovementMode.KeyboardMouse or UserMovementMode == Enum.ComputerMovementMode.Default then + newModuleToEnable = ControlModules.Keyboard + elseif UserMovementMode == Enum.ComputerMovementMode.ClickToMove then + -- Managed by CameraScript + newModuleToEnable = ControlModules.Keyboard + end + end + if newModuleToEnable ~= CurrentControlModule then + if CurrentControlModule then + CurrentControlModule:Disable() + end + CurrentControlModule = newModuleToEnable + if CurrentControlModule then + CurrentControlModule:Enable() + end + end + end +elseif UserInputService.GamepadEnabled then + onControlsChanged = function() + -- nil for now, probably needs some stuff later + end +end + +--[[ Settings Changed Connections ]]-- +LocalPlayer.Changed:connect(function(property) + if IsTouchDevice and property == 'DevTouchMovementMode' then + DevMovementMode = LocalPlayer.DevTouchMovementMode + IsUserChoice = DevMovementMode == Enum.DevTouchMovementMode.UserChoice + if IsUserChoice then + UserMovementMode = GameSettings.TouchMovementMode + end + onControlsChanged() + elseif not IsTouchDevice and property == 'DevComputerMovementMode' then + DevMovementMode = LocalPlayer.DevComputerMovementMode + IsUserChoice = DevMovementMode == Enum.DevComputerMovementMode.UserChoice + if IsUserChoice then + UserMovementMode = GameSettings.ComputerMovementMode + end + onControlsChanged() + end +end) + +GameSettings.Changed:connect(function(property) + if not IsUserChoice then return end + if property == 'TouchMovementMode' or property == 'ComputerMovementMode' then + UserMovementMode = GameSettings[property] + onControlsChanged() + end +end) + +--[[ Touch Events ]]-- +if IsTouchDevice then + -- On touch devices we need to recreate the guis on character load. + LocalPlayer.CharacterAdded:connect(function(character) + createTouchGuiContainer() + if CurrentControlModule then + CurrentControlModule:Disable() + CurrentControlModule = nil + end + onControlsChanged() + end) + + UserInputService.Changed:connect(function(property) + if property == 'ModalEnabled' then + IsModalEnabled = UserInputService.ModalEnabled + setJumpModule(not UserInputService.ModalEnabled) + if UserInputService.ModalEnabled then + if CurrentControlModule then + CurrentControlModule:Disable() + end + else + if CurrentControlModule then + CurrentControlModule:Enable() + end + end + end + end) + + BindableEvent_OnFailStateChanged.Event:connect(function(isOn) + if ClickToMoveTouchControls then + if isOn then + ClickToMoveTouchControls:Enable() + else + ClickToMoveTouchControls:Disable() + end + if ClickToMoveTouchControls == ControlModules.Thumbpad or ClickToMoveTouchControls == ControlModules.Thumbstick or + ClickToMoveTouchControls == ControlModules.Default then + -- + if isOn then + TouchJumpModule:Enable() + else + TouchJumpModule:Disable() + end + end + end + end) + BindableEvent_EnableTouchJump.Event:connect(function(enable) + if enable then + TouchJumpModule:Enable() + else + TouchJumpModule:Disable() + end + end) +end + +MasterControl:Init() +onControlsChanged() + +-- why do I need a wait here?!?!?!? Sometimes touch controls don't disappear without this +wait() +if UserInputService.GamepadEnabled then + if CurrentControlModule and IsTouchDevice then + CurrentControlModule:Disable() + end + if isJumpEnabled and IsTouchDevice then TouchJumpModule:Disable() end + + ControlModules.Gamepad:Enable() +end + +UserInputService.GamepadDisconnected:connect(function(gamepadEnum) + if UserInputService.GamepadEnabled then return end + + if CurrentControlModule and IsTouchDevice then + CurrentControlModule:Enable() + end + if isJumpEnabled and IsTouchDevice then TouchJumpModule:Enable() end + + ControlModules.Gamepad:Disable() +end) + +UserInputService.GamepadConnected:connect(function(gamepadEnum) + if gamepadEnum ~= Enum.UserInputType.Gamepad1 then return end + + if CurrentControlModule and IsTouchDevice then + CurrentControlModule:Disable() + end + if isJumpEnabled and IsTouchDevice then TouchJumpModule:Disable() end + + ControlModules.Gamepad:Enable() +end) + + + + +-- new version of ControlScript below +else + + + + + + + +--[[ Services ]]-- +local ContextActionService = game:GetService('ContextActionService') +local Players = game:GetService('Players') +local UserInputService = game:GetService('UserInputService') +-- Settings and GameSettings are read only +local Settings = UserSettings() +local GameSettings = Settings.GameSettings + +-- Issue with play solo? (F6) +while not UserInputService.KeyboardEnabled and not UserInputService.TouchEnabled and not UserInputService.GamepadEnabled do + wait() +end + +--[[ Script Variables ]]-- +while not Players.LocalPlayer do + wait() +end + +local lastInputType = nil +local LocalPlayer = Players.LocalPlayer +local PlayerGui = LocalPlayer:WaitForChild('PlayerGui') +local IsTouchDevice = UserInputService.TouchEnabled +local UserMovementMode = IsTouchDevice and GameSettings.TouchMovementMode or GameSettings.ComputerMovementMode +local DevMovementMode = IsTouchDevice and LocalPlayer.DevTouchMovementMode or LocalPlayer.DevComputerMovementMode +local IsUserChoice = (IsTouchDevice and DevMovementMode == Enum.DevTouchMovementMode.UserChoice) or (DevMovementMode == Enum.DevComputerMovementMode.UserChoice) +local TouchGui = nil +local TouchControlFrame = nil +local IsModalEnabled = UserInputService.ModalEnabled +local BindableEvent_OnFailStateChanged = nil +local isJumpEnabled = false + +local ControlState = {} +ControlState.Current = nil +function ControlState:SwitchTo(newControl) + if ControlState.Current == newControl then return end + + if ControlState.Current then + ControlState.Current:Disable() + end + + ControlState.Current = newControl + + if ControlState.Current then + ControlState.Current:Enable() + end +end + +--[[ Modules ]]-- +local ClickToMoveTouchControls = nil +local ControlModules = {} + +local MasterControl = require(script:WaitForChild('MasterControl')) + +local ThumbstickModule = require(script.MasterControl:WaitForChild('Thumbstick')) +local ThumbpadModule = require(script.MasterControl:WaitForChild('Thumbpad')) +local DPadModule = require(script.MasterControl:WaitForChild('DPad')) +local DefaultModule = ControlModules.Thumbstick +local TouchJumpModule = require(script.MasterControl:WaitForChild('TouchJump')) + +local keyboardModule = require(script.MasterControl:WaitForChild('KeyboardMovement')) +ControlModules.Gamepad = require(script.MasterControl:WaitForChild('Gamepad')) + +function getTouchModule() + local module = nil + if not IsUserChoice then + if DevMovementMode == Enum.DevTouchMovementMode.Thumbstick then + module = ThumbstickModule + isJumpEnabled = true + elseif DevMovementMode == Enum.DevTouchMovementMode.Thumbpad then + module = ThumbpadModule + isJumpEnabled = true + elseif DevMovementMode == Enum.DevTouchMovementMode.DPad then + module = DPadModule + isJumpEnabled = false + elseif DevMovementMode == Enum.DevTouchMovementMode.ClickToMove then + -- Managed by CameraScript + module = nil + elseif DevMovementMode == Enum.DevTouchMovementMode.Scriptable then + module = nil + end + else + if UserMovementMode == Enum.TouchMovementMode.Default or UserMovementMode == Enum.TouchMovementMode.Thumbstick then + module = ThumbstickModule + isJumpEnabled = true + elseif UserMovementMode == Enum.TouchMovementMode.Thumbpad then + module = ThumbpadModule + isJumpEnabled = true + elseif UserMovementMode == Enum.TouchMovementMode.DPad then + module = DPadModule + isJumpEnabled = false + elseif UserMovementMode == Enum.TouchMovementMode.ClickToMove then + -- Managed by CameraScript + module = nil + end + end + + return module +end + +function setJumpModule(isEnabled) + if not isEnabled then + TouchJumpModule:Disable() + elseif ControlState.Current == ControlModules.Touch then + TouchJumpModule:Enable() + end +end + +function setClickToMove() + if DevMovementMode == Enum.DevTouchMovementMode.ClickToMove or DevMovementMode == Enum.DevComputerMovementMode.ClickToMove or + UserMovementMode == Enum.ComputerMovementMode.ClickToMove or UserMovementMode == Enum.TouchMovementMode.ClickToMove then + -- + if lastInputType == Enum.UserInputType.Touch then + ClickToMoveTouchControls = ControlState.Current + end + elseif ClickToMoveTouchControls then + ClickToMoveTouchControls:Disable() + ClickToMoveTouchControls = nil + end +end + +ControlModules.Touch = {} +ControlModules.Touch.Current = nil +ControlModules.Touch.LocalPlayerChangedCon = nil +ControlModules.Touch.GameSettingsChangedCon = nil + +function ControlModules.Touch:RefreshControlStyle() + ControlModules.Touch.Current:Disable() + setJumpModule(false) + TouchJumpModule:Disable() + + ControlModules.Touch:Enable() +end +function ControlModules.Touch:DisconnectEvents() + if ControlModules.Touch.LocalPlayerChangedCon then + ControlModules.Touch.LocalPlayerChangedCon:disconnect() + ControlModules.Touch.LocalPlayerChangedCon = nil + end + if ControlModules.Touch.GameSettingsChangedCon then + ControlModules.Touch.GameSettingsChangedCon:disconnect() + ControlModules.Touch.GameSettingsChangedCon = nil + end +end +function ControlModules.Touch:Enable() + DevMovementMode = LocalPlayer.DevTouchMovementMode + IsUserChoice = DevMovementMode == Enum.DevTouchMovementMode.UserChoice + if IsUserChoice then + UserMovementMode = GameSettings.TouchMovementMode + end + + local newModuleToEnable = getTouchModule() + if newModuleToEnable then + setClickToMove() + setJumpModule(isJumpEnabled) + + newModuleToEnable:Enable() + ControlModules.Touch.Current = newModuleToEnable + + ControlModules.Touch:DisconnectEvents() + ControlModules.Touch.LocalPlayerChangedCon = LocalPlayer.Changed:connect(function(property) + if property == 'DevTouchMovementMode' then + ControlModules.Touch:RefreshControlStyle() + end + end) + + ControlModules.Touch.GameSettingsChangedCon = GameSettings.Changed:connect(function(property) + if property == 'TouchMovementMode' then + ControlModules.Touch:RefreshControlStyle() + end + end) + + if isJumpEnabled then TouchJumpModule:Enable() end + end +end +function ControlModules.Touch:Disable() + ControlModules.Touch:DisconnectEvents() + + local newModuleToDisable = getTouchModule() + + if newModuleToDisable == ThumbstickModule or + newModuleToDisable == DPadModule or + newModuleToDisable == ThumbpadModule then + newModuleToDisable:Disable() + setJumpModule(false) + TouchJumpModule:Disable() + end +end + +local function getKeyboardModule() + -- NOTE: Click to move still uses keyboard. Leaving cases in case this ever changes. + local whichModule = nil + if not IsUserChoice then + if DevMovementMode == Enum.DevComputerMovementMode.KeyboardMouse then + whichModule = keyboardModule + elseif DevMovementMode == Enum.DevComputerMovementMode.ClickToMove then + -- Managed by CameraScript + whichModule = keyboardModule + end + else + if UserMovementMode == Enum.ComputerMovementMode.KeyboardMouse or UserMovementMode == Enum.ComputerMovementMode.Default then + whichModule = keyboardModule + elseif UserMovementMode == Enum.ComputerMovementMode.ClickToMove then + -- Managed by CameraScript + whichModule = keyboardModule + end + end + + return whichModule +end + +ControlModules.Keyboard = {} +function ControlModules.Keyboard:Enable() + DevMovementMode = LocalPlayer.DevComputerMovementMode + IsUserChoice = DevMovementMode == Enum.DevComputerMovementMode.UserChoice + if IsUserChoice then + UserMovementMode = GameSettings.ComputerMovementMode + end + + local newModuleToEnable = getKeyboardModule() + if newModuleToEnable then + if newModuleToEnable then + newModuleToEnable:Enable() + end + end +end +function ControlModules.Keyboard:Disable() + local newModuleToDisable = getKeyboardModule() + if newModuleToDisable then + newModuleToDisable:Disable() + end +end + +if IsTouchDevice then + BindableEvent_OnFailStateChanged = script.Parent:WaitForChild('OnClickToMoveFailStateChange') +end + +-- not used, but needs to be required +local VehicleController = require(script.MasterControl:WaitForChild('VehicleController')) + + +--[[ Initialization/Setup ]]-- +local function createTouchGuiContainer() + if TouchGui then TouchGui:Destroy() end + + -- Container for all touch device guis + TouchGui = Instance.new('ScreenGui') + TouchGui.Name = "TouchGui" + TouchGui.Parent = PlayerGui + + TouchControlFrame = Instance.new('Frame') + TouchControlFrame.Name = "TouchControlFrame" + TouchControlFrame.Size = UDim2.new(1, 0, 1, 0) + TouchControlFrame.BackgroundTransparency = 1 + TouchControlFrame.Parent = TouchGui + + ThumbstickModule:Create(TouchControlFrame) + DPadModule:Create(TouchControlFrame) + ThumbpadModule:Create(TouchControlFrame) + TouchJumpModule:Create(TouchControlFrame) +end + +--[[ Settings Changed Connections ]]-- +LocalPlayer.Changed:connect(function(property) + if lastInputType == Enum.UserInputType.Touch and property == 'DevTouchMovementMode' then + ControlState:SwitchTo(ControlModules.Touch) + elseif UserInputService.KeyboardEnabled and property == 'DevComputerMovementMode' then + ControlState:SwitchTo(ControlModules.Keyboard) + end +end) + +GameSettings.Changed:connect(function(property) + if not IsUserChoice then return end + if property == 'TouchMovementMode' or property == 'ComputerMovementMode' then + UserMovementMode = GameSettings[property] + if property == 'TouchMovementMode' then + ControlState:SwitchTo(ControlModules.Touch) + elseif property == 'ComputerMovementMode' then + ControlState:SwitchTo(ControlModules.Keyboard) + end + end +end) + +--[[ Touch Events ]]-- +-- On touch devices we need to recreate the guis on character load. +local lastControlState = nil +LocalPlayer.CharacterAdded:connect(function(character) + if UserInputService.TouchEnabled then + createTouchGuiContainer() + end + + if ControlState.Current == nil then + ControlState:SwitchTo(lastControlState) + end +end) + +LocalPlayer.CharacterRemoving:connect(function(character) + lastControlState = ControlState.Current + ControlState:SwitchTo(nil) +end) + +UserInputService.Changed:connect(function(property) + if property == 'ModalEnabled' then + IsModalEnabled = UserInputService.ModalEnabled + + if lastInputType == Enum.UserInputType.Touch then + if ControlState.Current == ControlModules.Touch and IsModalEnabled then + ControlState:SwitchTo(nil) + elseif ControlState.Current == nil and not IsModalEnabled then + ControlState:SwitchTo(ControlModules.Touch) + end + end + end +end) + +if BindableEvent_OnFailStateChanged then + BindableEvent_OnFailStateChanged.Event:connect(function(isOn) + if lastInputType == Enum.UserInputType.Touch and ClickToMoveTouchControls then + if isOn then + ControlState:SwitchTo(ClickToMoveTouchControls) + else + ControlState:SwitchTo(nil) + end + end + end) +end + +local switchToInputType = function(newLastInputType) + lastInputType = newLastInputType + + if lastInputType == Enum.UserInputType.Touch then + ControlState:SwitchTo(ControlModules.Touch) + elseif lastInputType == Enum.UserInputType.Keyboard or + lastInputType == Enum.UserInputType.MouseButton1 or + lastInputType == Enum.UserInputType.MouseButton2 or + lastInputType == Enum.UserInputType.MouseButton3 or + lastInputType == Enum.UserInputType.MouseWheel or + lastInputType == Enum.UserInputType.MouseMovement then + ControlState:SwitchTo(ControlModules.Keyboard) + elseif lastInputType == Enum.UserInputType.Gamepad1 or + lastInputType == Enum.UserInputType.Gamepad2 or + lastInputType == Enum.UserInputType.Gamepad3 or + lastInputType == Enum.UserInputType.Gamepad4 then + ControlState:SwitchTo(ControlModules.Gamepad) + end +end + +if IsTouchDevice then + createTouchGuiContainer() +end + +MasterControl:Init() + +UserInputService.GamepadDisconnected:connect(function(gamepadEnum) + local connectedGamepads = UserInputService:GetConnectedGamepads() + if #connectedGamepads > 0 then return end + + if UserInputService.KeyboardEnabled then + ControlState:SwitchTo(ControlModules.Keyboard) + elseif IsTouchDevice then + ControlState:SwitchTo(ControlModules.Touch) + end +end) + +UserInputService.GamepadConnected:connect(function(gamepadEnum) + ControlState:SwitchTo(ControlModules.Gamepad) +end) + +switchToInputType(UserInputService:GetLastInputType()) +UserInputService.LastInputTypeChanged:connect(switchToInputType) + +end -- end of control script flag switch]]> + + + + + MasterControl + + + + + DPad + jumpRadius then + local angle = ATAN2(direction.y, direction.x) + local octant = (FLOOR(8 * angle / (2 * PI) + 8.5)%8) + 1 + movementVector = COMPASS_DIR[octant] + end + + if not flBtn.Visible and movementVector == COMPASS_DIR[7] then + flBtn.Visible = true + frBtn.Visible = true + end + end + + DPadFrame.InputBegan:connect(function(inputObject) + if TouchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch then + return + end + + MasterControl:AddToPlayerMovement(-movementVector) + + TouchObject = inputObject + normalizeDirection(TouchObject.Position) + + MasterControl:AddToPlayerMovement(movementVector) + end) + + DPadFrame.InputChanged:connect(function(inputObject) + if inputObject == TouchObject then + MasterControl:AddToPlayerMovement(-movementVector) + normalizeDirection(TouchObject.Position) + MasterControl:AddToPlayerMovement(movementVector) + MasterControl:SetIsJumping(false) + end + end) + + OnInputEnded = function() + TouchObject = nil + flBtn.Visible = false + frBtn.Visible = false + + MasterControl:AddToPlayerMovement(-movementVector) + movementVector = Vector3.new(0, 0, 0) + end + + DPadFrame.InputEnded:connect(function(inputObject) + if inputObject == TouchObject then + OnInputEnded() + end + end) + + DPadFrame.Parent = parentFrame +end + +return DPad +]]> + + + + + + Gamepad + 0 then + for i = 1, #connectedGamepads do + if activateGamepad == nil then + activateGamepad = connectedGamepads[i] + elseif connectedGamepads[i].Value < activateGamepad.Value then + activateGamepad = connectedGamepads[i] + end + end + end + + if activateGamepad == nil then -- nothing is connected, at least set up for gamepad1 + activateGamepad = Enum.UserInputType.Gamepad1 + end +end + +--[[ Public API ]]-- +function Gamepad:Enable() + local forwardValue = 0 + local backwardValue = 0 + local leftValue = 0 + local rightValue = 0 + + local moveFunc = LocalPlayer.Move + local gamepadSupports = UserInputService.GamepadSupports + + local controlCharacterGamepad = function(actionName, inputState, inputObject) + if activateGamepad ~= inputObject.UserInputType then return end + if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end + + if inputState == Enum.UserInputState.Cancel then + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + return + end + + if inputObject.Position.magnitude > thumbstickDeadzone then + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y) + MasterControl:AddToPlayerMovement(currentMoveVector) + else + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + end + end + + local jumpCharacterGamepad = function(actionName, inputState, inputObject) + if activateGamepad ~= inputObject.UserInputType then return end + if inputObject.KeyCode ~= Enum.KeyCode.ButtonA then return end + + if inputState == Enum.UserInputState.Cancel then + MasterControl:SetIsJumping(false) + return + end + + MasterControl:SetIsJumping(inputObject.UserInputState == Enum.UserInputState.Begin) + end + + local doDpadMoveUpdate = function(userInputType) + if not gamepadSupports(UserInputService, userInputType, Enum.KeyCode.Thumbstick1) then + if LocalPlayer and LocalPlayer.Character then + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(leftValue + rightValue,0,forwardValue + backwardValue) + MasterControl:AddToPlayerMovement(currentMoveVector) + end + end + end + + local moveForwardFunc = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + forwardValue = -1 + elseif inputState == Enum.UserInputState.Begin or inputState == Enum.UserInputState.Cancel then + forwardValue = 0 + end + + doDpadMoveUpdate(inputObject.UserInputType) + end + + local moveBackwardFunc = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + backwardValue = 1 + elseif inputState == Enum.UserInputState.Begin or inputState == Enum.UserInputState.Cancel then + backwardValue = 0 + end + + doDpadMoveUpdate(inputObject.UserInputType) + end + + local moveLeftFunc = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + leftValue = -1 + elseif inputState == Enum.UserInputState.Begin or inputState == Enum.UserInputState.Cancel then + leftValue = 0 + end + + doDpadMoveUpdate(inputObject.UserInputType) + end + + local moveRightFunc = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + rightValue = 1 + elseif inputState == Enum.UserInputState.Begin or inputState == Enum.UserInputState.Cancel then + rightValue = 0 + end + + doDpadMoveUpdate(inputObject.UserInputType) + end + + local function setActivateGamepad() + if activateGamepad then + ContextActionService:UnbindActivate(activateGamepad, Enum.KeyCode.ButtonR2) + end + assignActivateGamepad() + if activateGamepad then + ContextActionService:BindActivate(activateGamepad, Enum.KeyCode.ButtonR2) + end + end + + ContextActionService:BindAction("JumpButton",jumpCharacterGamepad, false, Enum.KeyCode.ButtonA) + ContextActionService:BindAction("MoveThumbstick",controlCharacterGamepad, false, Enum.KeyCode.Thumbstick1) + + ContextActionService:BindAction("forwardDpad", moveForwardFunc, false, Enum.KeyCode.DPadUp) + ContextActionService:BindAction("backwardDpad", moveBackwardFunc, false, Enum.KeyCode.DPadDown) + ContextActionService:BindAction("leftDpad", moveLeftFunc, false, Enum.KeyCode.DPadLeft) + ContextActionService:BindAction("rightDpad", moveRightFunc, false, Enum.KeyCode.DPadRight) + + setActivateGamepad() + + gamepadConnectedCon = UserInputService.GamepadDisconnected:connect(function(gamepadEnum) + if activateGamepad ~= gamepadEnum then return end + + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + + activateGamepad = nil + setActivateGamepad() + end) + + gamepadDisconnectedCon = UserInputService.GamepadConnected:connect(function(gamepadEnum) + if activateGamepad == nil then + setActivateGamepad() + end + end) +end + +function Gamepad:Disable() + + ContextActionService:UnbindAction("forwardDpad") + ContextActionService:UnbindAction("backwardDpad") + ContextActionService:UnbindAction("leftDpad") + ContextActionService:UnbindAction("rightDpad") + + ContextActionService:UnbindAction("MoveThumbstick") + ContextActionService:UnbindAction("JumpButton") + ContextActionService:UnbindActivate(activateGamepad, Enum.KeyCode.ButtonR2) + + if gamepadConnectedCon then gamepadConnectedCon:disconnect() end + if gamepadDisconnectedCon then gamepadDisconnectedCon:disconnect() end + + activateGamepad = nil + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + MasterControl:SetIsJumping(false) +end + +return Gamepad +]]> + + + + + + KeyboardMovement + + + + + + Thumbpad + 0.5 then -- UP + if not isUp then + isUp, isDown = true, false + doTween(uArrow, lgArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset, 0, smImgOffset - smArrowSize.Y.Offset * 1.5)) + doTween(dArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 1, lgImgOffset)) + end + elseif forwardDot < -0.5 then -- DOWN + if not isDown then + isDown, isUp = true, false + doTween(dArrow, lgArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset, 1, lgImgOffset + smArrowSize.Y.Offset/2)) + doTween(uArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 0, smImgOffset)) + end + else + isUp, isDown = false, false + doTween(dArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 1, lgImgOffset)) + doTween(uArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 0, smImgOffset)) + end + + if rightDot > 0.5 then + if not isRight then + isRight, isLeft = true, false + doTween(rArrow, lgArrowSize, UDim2.new(1, lgImgOffset + smArrowSize.X.Offset/2, 0.5, -smArrowSize.Y.Offset)) + doTween(lArrow, smArrowSize, UDim2.new(0, smImgOffset, 0.5, -smArrowSize.Y.Offset/2)) + end + elseif rightDot < -0.5 then + if not isLeft then + isLeft, isRight = true, false + doTween(lArrow, lgArrowSize, UDim2.new(0, smImgOffset - smArrowSize.X.Offset * 1.5, 0.5, -smArrowSize.Y.Offset)) + doTween(rArrow, smArrowSize, UDim2.new(1, lgImgOffset, 0.5, -smArrowSize.Y.Offset/2)) + end + else + isRight, isLeft = false, false + doTween(lArrow, smArrowSize, UDim2.new(0, smImgOffset, 0.5, -smArrowSize.Y.Offset/2)) + doTween(rArrow, smArrowSize, UDim2.new(1, lgImgOffset, 0.5, -smArrowSize.Y.Offset/2)) + end + end + + --input connections + ThumbpadFrame.InputBegan:connect(function(inputObject) + if TouchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch then + return + end + + ThumbpadFrame.Position = UDim2.new(0, inputObject.Position.x - ThumbpadFrame.AbsoluteSize.x/2, 0, inputObject.Position.y - ThumbpadFrame.Size.Y.Offset/2) + padOrigin = Vector2.new(ThumbpadFrame.AbsolutePosition.x + ThumbpadFrame.AbsoluteSize.x/2, + ThumbpadFrame.AbsolutePosition.y + ThumbpadFrame.AbsoluteSize.y/2) + doMove(inputObject.Position) + TouchObject = inputObject + end) + + OnTouchChangedCn = UserInputService.TouchMoved:connect(function(inputObject, isProcessed) + if inputObject == TouchObject then + doMove(TouchObject.Position) + end + end) + + OnInputEnded = function() + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + MasterControl:SetIsJumping(false) + + ThumbpadFrame.Position = position + TouchObject = nil + isUp, isDown, isLeft, isRight = false, false, false, false + doTween(dArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 1, lgImgOffset)) + doTween(uArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 0, smImgOffset)) + doTween(lArrow, smArrowSize, UDim2.new(0, smImgOffset, 0.5, -smArrowSize.Y.Offset/2)) + doTween(rArrow, smArrowSize, UDim2.new(1, lgImgOffset, 0.5, -smArrowSize.Y.Offset/2)) + end + + OnTouchEndedCn = UserInputService.TouchEnded:connect(function(inputObject) + if inputObject == TouchObject then + OnInputEnded() + end + end) + + ThumbpadFrame.Parent = parentFrame +end + +return Thumbpad +]]> + + + + + + Thumbstick + maxLength then + local offset = relativePosition.unit * maxLength + ThumbstickFrame.Position = UDim2.new( + 0, pos.x - ThumbstickFrame.AbsoluteSize.x/2 - offset.x, + 0, pos.y - ThumbstickFrame.AbsoluteSize.y/2 - offset.y) + else + length = math.min(length, maxLength) + relativePosition = relativePosition.unit * length + end + StickImage.Position = UDim2.new(0, relativePosition.x + StickImage.AbsoluteSize.x/2, 0, relativePosition.y + StickImage.AbsoluteSize.y/2) + end + + -- input connections + ThumbstickFrame.InputBegan:connect(function(inputObject) + if MoveTouchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch then + return + end + + MoveTouchObject = inputObject + ThumbstickFrame.Position = UDim2.new(0, inputObject.Position.x - ThumbstickFrame.Size.X.Offset/2, 0, inputObject.Position.y - ThumbstickFrame.Size.Y.Offset/2) + centerPosition = Vector2.new(ThumbstickFrame.AbsolutePosition.x + ThumbstickFrame.AbsoluteSize.x/2, + ThumbstickFrame.AbsolutePosition.y + ThumbstickFrame.AbsoluteSize.y/2) + local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y) + moveStick(inputObject.Position) + end) + + OnTouchMovedCn = UserInputService.TouchMoved:connect(function(inputObject, isProcessed) + if inputObject == MoveTouchObject then + centerPosition = Vector2.new(ThumbstickFrame.AbsolutePosition.x + ThumbstickFrame.AbsoluteSize.x/2, + ThumbstickFrame.AbsolutePosition.y + ThumbstickFrame.AbsoluteSize.y/2) + local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y) + doMove(direction) + moveStick(inputObject.Position) + end + end) + + OnTouchEnded = function() + ThumbstickFrame.Position = position + StickImage.Position = UDim2.new(0, ThumbstickFrame.Size.X.Offset/2 - thumbstickSize/4, 0, ThumbstickFrame.Size.Y.Offset/2 - thumbstickSize/4) + MoveTouchObject = nil + + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + MasterControl:SetIsJumping(false) + end + + OnTouchEndedCn = UserInputService.TouchEnded:connect(function(inputObject, isProcessed) + if inputObject == MoveTouchObject then + OnTouchEnded() + end + end) + + ThumbstickFrame.Parent = parentFrame +end + +return Thumbstick +]]> + + + + + TouchJump + + + + + + + VehicleController + 0.5 then + return 1 + elseif value < -0.5 then + return -1 + end + return 0 +end + +local function onRenderStepped() + if CurrentVehicleSeat then + local moveValue = MasterControl:GetMoveVector() + if game:GetService("UserInputService"):GetGamepadConnected(Enum.UserInputType.Gamepad1) and onlyTriggersForThrottle and useTriggersForThrottle then + CurrentVehicleSeat.Throttle = -CurrentThrottle + else + CurrentVehicleSeat.Throttle = getClosestFittingValue(-moveValue.z) + end + CurrentVehicleSeat.Steer = getClosestFittingValue(moveValue.x) + end +end + +local function onSeated(active, currentSeatPart) + if active then + if currentSeatPart and currentSeatPart:IsA('VehicleSeat') then + CurrentVehicleSeat = currentSeatPart + if useTriggersForThrottle then + ContextActionService:BindAction("throttleAccel", onThrottleAccel, false, Enum.KeyCode.ButtonR2) + ContextActionService:BindAction("throttleDeccel", onThrottleDeccel, false, Enum.KeyCode.ButtonL2) + end + ContextActionService:BindAction("arrowSteerRight", onSteerRight, false, Enum.KeyCode.Right) + ContextActionService:BindAction("arrowSteerLeft", onSteerLeft, false, Enum.KeyCode.Left) + local success = pcall(function() RunService:BindToRenderStep("VehicleControlStep", Enum.RenderPriority.Input.Value, onRenderStepped) end) + + if not success then + if RenderSteppedCn then return end + RenderSteppedCn = RunService.RenderStepped:connect(onRenderStepped) + end + end + else + CurrentVehicleSeat = nil + if useTriggersForThrottle then + ContextActionService:UnbindAction("throttleAccel") + ContextActionService:UnbindAction("throttleDeccel") + end + ContextActionService:UnbindAction("arrowSteerRight") + ContextActionService:UnbindAction("arrowSteerLeft") + MasterControl:AddToPlayerMovement(Vector3.new(-CurrentSteer, 0, -CurrentThrottle)) + CurrentThrottle = 0 + CurrentSteer = 0 + local success = pcall(function() RunService:UnbindFromRenderStep("VehicleControlStep") end) + if not success and RenderSteppedCn then + RenderSteppedCn:disconnect() + RenderSteppedCn = nil + end + end +end + +local function CharacterAdded(character) + local humanoid = getHumanoid() + while not humanoid do + wait() + humanoid = getHumanoid() + end + -- + if HumanoidSeatedCn then + HumanoidSeatedCn:disconnect() + HumanoidSeatedCn = nil + end + HumanoidSeatedCn = humanoid.Seated:connect(onSeated) +end + +if LocalPlayer.Character then + CharacterAdded(LocalPlayer.Character) +end +LocalPlayer.CharacterAdded:connect(CharacterAdded) + +return VehicleController +]]> + + + + + \ No newline at end of file diff --git a/Client2016/content/fonts/characterR15.rbxm b/Client2016/content/fonts/characterR15.rbxm new file mode 100644 index 0000000..6b9045b Binary files /dev/null and b/Client2016/content/fonts/characterR15.rbxm differ diff --git a/Client2016/content/fonts/comics.fnt b/Client2016/content/fonts/comics.fnt new file mode 100644 index 0000000..6b71c7e Binary files /dev/null and b/Client2016/content/fonts/comics.fnt differ diff --git a/Client2016/content/fonts/diogenes.fnt b/Client2016/content/fonts/diogenes.fnt new file mode 100644 index 0000000..e69de29 diff --git a/Client2016/content/fonts/fonts.dds b/Client2016/content/fonts/fonts.dds new file mode 100644 index 0000000..30e4474 Binary files /dev/null and b/Client2016/content/fonts/fonts.dds differ diff --git a/Client2016/content/fonts/gamecontrollerdb.txt b/Client2016/content/fonts/gamecontrollerdb.txt new file mode 100644 index 0000000..dd49836 --- /dev/null +++ b/Client2016/content/fonts/gamecontrollerdb.txt @@ -0,0 +1,89 @@ +# Windows - DINPUT +8f0e1200000000000000504944564944,Acme,platform:Windows,x:b2,a:b0,b:b1,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +341a3608000000000000504944564944,Afterglow PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, +ffff0000000000000000504944564944,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, +6d0416c2000000000000504944564944,Generic DirectInput Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, +6d0419c2000000000000504944564944,Logitech F710 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, +88880803000000000000504944564944,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b9,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b0,y:b3,platform:Windows, +4c056802000000000000504944564944,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Windows, +25090500000000000000504944564944,PS3 DualShock,a:b2,b:b1,back:b9,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b0,y:b3,platform:Windows, +4c05c405000000000000504944564944,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, +6d0418c2000000000000504944564944,Logitech RumblePad 2 USB,platform:Windows,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, +36280100000000000000504944564944,OUYA Controller,platform:Windows,a:b0,b:b3,y:b2,x:b1,start:b14,guide:b15,leftstick:b6,rightstick:b7,leftshoulder:b4,rightshoulder:b5,dpup:b8,dpleft:b10,dpdown:b9,dpright:b11,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:b12,righttrigger:b13, +4f0400b3000000000000504944564944,Thrustmaster Firestorm Dual Power,a:b0,b:b2,y:b3,x:b1,start:b10,guide:b8,back:b9,leftstick:b11,rightstick:b12,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Windows, +00f00300000000000000504944564944,RetroUSB.com RetroPad,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Windows, +00f0f100000000000000504944564944,RetroUSB.com Super RetroPort,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Windows, +28040140000000000000504944564944,GamePad Pro USB,platform:Windows,a:b1,b:b2,x:b0,y:b3,back:b8,start:b9,leftshoulder:b4,rightshoulder:b5,leftx:a0,lefty:a1,lefttrigger:b6,righttrigger:b7, +ff113133000000000000504944564944,SVEN X-PAD,platform:Windows,a:b2,b:b3,y:b1,x:b0,start:b5,back:b4,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a4,lefttrigger:b8,righttrigger:b9, +8f0e0300000000000000504944564944,Piranha xtreme,platform:Windows,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +8f0e0d31000000000000504944564944,Multilaser JS071 USB,platform:Windows,a:b1,b:b2,y:b3,x:b0,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7, +10080300000000000000504944564944,PS2 USB,platform:Windows,a:b2,b:b1,y:b0,x:b3,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a4,righty:a2,lefttrigger:b4,righttrigger:b5, +79000600000000000000504944564944,G-Shark GS-GP702,a:b2,b:b1,x:b3,y:b0,back:b8,start:b9,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a4,lefttrigger:b6,righttrigger:b7,platform:Windows, +4b12014d000000000000504944564944,NYKO AIRFLO,a:b0,b:b1,x:b2,y:b3,back:b8,guide:b10,start:b9,leftstick:a0,rightstick:a2,leftshoulder:a3,rightshoulder:b5,dpup:h0.1,dpdown:h0.0,dpleft:h0.8,dpright:h0.2,leftx:h0.6,lefty:h0.12,rightx:h0.9,righty:h0.4,lefttrigger:b6,righttrigger:b7,platform:Windows, +d6206dca000000000000504944564944,PowerA Pro Ex,a:b1,b:b2,x:b0,y:b3,back:b8,guide:b12,start:b9,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.0,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7,platform:Windows, +a3060cff000000000000504944564944,Saitek P2500,a:b2,b:b3,y:b1,x:b0,start:b4,guide:b10,back:b5,leftstick:b8,rightstick:b9,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,platform:Windows, +8f0e0300000000000000504944564944,Trust GTX 28,a:b2,b:b1,y:b0,x:b3,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7,platform:Windows, +4f0415b3000000000000504944564944,Thrustmaster Dual Analog 3.2,platform:Windows,x:b1,a:b0,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, + +# OS X +0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X, +6d0400000000000016c2000000000000,Logitech F310 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, +6d0400000000000018c2000000000000,Logitech F510 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, +6d040000000000001fc2000000000000,Logitech F710 Gamepad (XInput),a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, +6d0400000000000019c2000000000000,Logitech Wireless Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, +4c050000000000006802000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Mac OS X, +4c05000000000000c405000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,Platform:Mac OS X, +5e040000000000008e02000000000000,X360 Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, +891600000000000000fd000000000000,Razer Onza Tournament,a:b0,b:b1,y:b3,x:b2,start:b8,guide:b10,back:b9,leftstick:b6,rightstick:b7,leftshoulder:b4,rightshoulder:b5,dpup:b11,dpleft:b13,dpdown:b12,dpright:b14,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Mac OS X, +4f0400000000000000b3000000000000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,y:b3,x:b1,start:b10,guide:b8,back:b9,leftstick:b11,rightstick:,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Mac OS X, +8f0e0000000000000300000000000000,Piranha xtreme,platform:Mac OS X,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +0d0f0000000000004d00000000000000,HORI Gem Pad 3,platform:Mac OS X,a:b1,b:b2,y:b3,x:b0,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7, +79000000000000000600000000000000,G-Shark GP-702,a:b2,b:b1,x:b3,y:b0,back:b8,start:b9,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:b6,righttrigger:b7,platform:Mac OS X, +4f0400000000000015b3000000000000,Thrustmaster Dual Analog 3.2,platform:Mac OS X,x:b1,a:b0,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, + +# Linux +0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, +03000000ba2200002010000001010000,Jess Technology USB Game Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux, +030000006d04000019c2000010010000,Logitech Cordless RumblePad 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, +030000006d0400001dc2000014400000,Logitech F310 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000006d0400001ec2000020200000,Logitech F510 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000006d04000019c2000011010000,Logitech F710 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, +030000006d0400001fc2000005030000,Logitech F710 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000004c0500006802000011010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux, +030000004c050000c405000011010000,Sony DualShock 4,a:b1,b:b2,y:b3,x:b0,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a5,lefttrigger:b6,righttrigger:b7,platform:Linux, +03000000de280000ff11000001000000,Valve Streaming Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e0400008e02000014010000,X360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e0400008e02000010010000,X360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e0400001907000000010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +03000000100800000100000010010000,Twin USB PS2 Adapter,a:b2,b:b1,y:b0,x:b3,start:b9,guide:,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a2,lefttrigger:b4,righttrigger:b5,platform:Linux, +03000000a306000023f6000011010000,Saitek Cyborg V.1 Game Pad,a:b1,b:b2,y:b3,x:b0,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a4,lefttrigger:b6,righttrigger:b7,platform:Linux, +030000004f04000020b3000010010000,Thrustmaster 2 in 1 DT,a:b0,b:b2,y:b3,x:b1,start:b9,guide:,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Linux, +030000004f04000023b3000000010000,Thrustmaster Dual Trigger 3-in-1,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a5, +030000008f0e00000300000010010000,GreenAsia Inc. USB Joystick ,platform:Linux,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +030000008f0e00001200000010010000,GreenAsia Inc. USB Joystick ,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +030000005e0400009102000007010000,X360 Wireless Controller,a:b0,b:b1,y:b3,x:b2,start:b7,guide:b8,back:b6,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:b13,dpleft:b11,dpdown:b14,dpright:b12,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux, +030000006d04000016c2000010010000,Logitech Logitech Dual Action,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, +03000000260900008888000000010000,GameCube {WiseGroup USB box},a:b0,b:b2,y:b3,x:b1,start:b7,leftshoulder:,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,rightstick:,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,platform:Linux, +030000006d04000011c2000010010000,Logitech WingMan Cordless RumblePad,a:b0,b:b1,y:b4,x:b3,start:b8,guide:b5,back:b2,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:b9,righttrigger:b10,platform:Linux, +030000006d04000018c2000010010000,Logitech Logitech RumblePad 2 USB,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, +05000000d6200000ad0d000001000000,Moga Pro,platform:Linux,a:b0,b:b1,y:b3,x:b2,start:b6,leftstick:b7,rightstick:b8,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a5,righttrigger:a4, +030000004f04000009d0000000010000,Thrustmaster Run N Drive Wireless PS3,platform:Linux,a:b1,b:b2,x:b0,y:b3,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7, +030000004f04000008d0000000010000,Thrustmaster Run N Drive Wireless,platform:Linux,a:b1,b:b2,x:b0,y:b3,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a5,lefttrigger:b6,righttrigger:b7, +0300000000f000000300000000010000,RetroUSB.com RetroPad,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Linux, +0300000000f00000f100000000010000,RetroUSB.com Super RetroPort,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Linux, +030000006f0e00001f01000000010000,Generic X-Box pad,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b6,guide:b8,start:b7,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,leftstick:b9,rightstick:b10,leftx:a0,lefty:a1,rightx:a3,righty:a4, +03000000280400000140000000010000,Gravis GamePad Pro USB ,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftx:a0,lefty:a1, +030000005e0400008902000021010000,Microsoft X-Box pad v2 (US),platform:Linux,x:b3,a:b0,b:b1,y:b4,back:b6,start:b7,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b5,lefttrigger:a2,rightshoulder:b2,righttrigger:a5,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a3,righty:a4, +030000006f0e00001e01000011010000,Rock Candy Gamepad for PS3,platform:Linux,a:b1,b:b2,x:b0,y:b3,back:b8,start:b9,guide:b12,leftshoulder:b4,rightshoulder:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2, +03000000250900000500000000010000,Sony PS2 pad with SmartJoy adapter,platform:Linux,a:b2,b:b1,y:b0,x:b3,start:b8,back:b9,leftstick:b10,rightstick:b11,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b4,righttrigger:b5, +030000008916000000fd000024010000,Razer Onza Tournament,a:b0,b:b1,y:b3,x:b2,start:b7,guide:b8,back:b6,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:b13,dpleft:b11,dpdown:b14,dpright:b12,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux, +030000004f04000000b3000010010000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,y:b3,x:b1,start:b10,guide:b8,back:b9,leftstick:b11,rightstick:b12,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Linux, +03000000ad1b000001f5000033050000,Hori Pad EX Turbo 2,a:b0,b:b1,y:b3,x:b2,start:b7,guide:b8,back:b6,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux, +050000004c050000c405000000010000,PS4 Controller (Bluetooth),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, +060000004c0500006802000000010000,PS3 Controller (Bluetooth),a:b14,b:b13,y:b12,x:b15,start:b3,guide:b16,back:b0,leftstick:b1,rightstick:b2,leftshoulder:b10,rightshoulder:b11,dpup:b4,dpleft:b7,dpdown:b6,dpright:b5,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b8,righttrigger:b9,platform:Linux, +03000000790000000600000010010000,DragonRise Inc. Generic USB Joystick ,platform:Linux,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a4, +03000000666600000488000000010000,Super Joy Box 5 Pro,platform:Linux,a:b2,b:b1,x:b3,y:b0,back:b9,start:b8,leftshoulder:b6,rightshoulder:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b4,righttrigger:b5,dpup:b12,dpleft:b15,dpdown:b14,dpright:b13, +05000000362800000100000002010000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,platform:Linux,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2, +05000000362800000100000003010000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,platform:Linux,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2, +030000008916000001fd000024010000,Razer Onza Classic Edition,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b6,guide:b8,start:b7,dpleft:b11,dpdown:b14,dpright:b12,dpup:b13,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,leftstick:b9,rightstick:b10,leftx:a0,lefty:a1,rightx:a3,righty:a4, +030000005e040000d102000001010000,Microsoft X-Box One pad,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b6,guide:b8,start:b7,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,leftstick:b9,rightstick:b10,leftx:a0,lefty:a1,rightx:a3,righty:a4, \ No newline at end of file diff --git a/Client2016/content/fonts/head.mesh b/Client2016/content/fonts/head.mesh new file mode 100644 index 0000000..26db950 Binary files /dev/null and b/Client2016/content/fonts/head.mesh differ diff --git a/Client2016/content/fonts/headA.mesh b/Client2016/content/fonts/headA.mesh new file mode 100644 index 0000000..76b2e72 Binary files /dev/null and b/Client2016/content/fonts/headA.mesh differ diff --git a/Client2016/content/fonts/headB.mesh b/Client2016/content/fonts/headB.mesh new file mode 100644 index 0000000..7705a05 Binary files /dev/null and b/Client2016/content/fonts/headB.mesh differ diff --git a/Client2016/content/fonts/headC.mesh b/Client2016/content/fonts/headC.mesh new file mode 100644 index 0000000..4672c59 Binary files /dev/null and b/Client2016/content/fonts/headC.mesh differ diff --git a/Client2016/content/fonts/headD.mesh b/Client2016/content/fonts/headD.mesh new file mode 100644 index 0000000..ff6cd6c Binary files /dev/null and b/Client2016/content/fonts/headD.mesh differ diff --git a/Client2016/content/fonts/headE.mesh b/Client2016/content/fonts/headE.mesh new file mode 100644 index 0000000..d722d29 Binary files /dev/null and b/Client2016/content/fonts/headE.mesh differ diff --git a/Client2016/content/fonts/headF.mesh b/Client2016/content/fonts/headF.mesh new file mode 100644 index 0000000..7c0b311 Binary files /dev/null and b/Client2016/content/fonts/headF.mesh differ diff --git a/Client2016/content/fonts/headG.mesh b/Client2016/content/fonts/headG.mesh new file mode 100644 index 0000000..38db235 Binary files /dev/null and b/Client2016/content/fonts/headG.mesh differ diff --git a/Client2016/content/fonts/headH.mesh b/Client2016/content/fonts/headH.mesh new file mode 100644 index 0000000..fe7365b Binary files /dev/null and b/Client2016/content/fonts/headH.mesh differ diff --git a/Client2016/content/fonts/headI.mesh b/Client2016/content/fonts/headI.mesh new file mode 100644 index 0000000..8ff7ba1 Binary files /dev/null and b/Client2016/content/fonts/headI.mesh differ diff --git a/Client2016/content/fonts/headJ.mesh b/Client2016/content/fonts/headJ.mesh new file mode 100644 index 0000000..ff76d44 Binary files /dev/null and b/Client2016/content/fonts/headJ.mesh differ diff --git a/Client2016/content/fonts/headK.mesh b/Client2016/content/fonts/headK.mesh new file mode 100644 index 0000000..b08bffc Binary files /dev/null and b/Client2016/content/fonts/headK.mesh differ diff --git a/Client2016/content/fonts/headL.mesh b/Client2016/content/fonts/headL.mesh new file mode 100644 index 0000000..645531b Binary files /dev/null and b/Client2016/content/fonts/headL.mesh differ diff --git a/Client2016/content/fonts/headM.mesh b/Client2016/content/fonts/headM.mesh new file mode 100644 index 0000000..ba144e8 Binary files /dev/null and b/Client2016/content/fonts/headM.mesh differ diff --git a/Client2016/content/fonts/headN.mesh b/Client2016/content/fonts/headN.mesh new file mode 100644 index 0000000..6fc4620 Binary files /dev/null and b/Client2016/content/fonts/headN.mesh differ diff --git a/Client2016/content/fonts/headO.mesh b/Client2016/content/fonts/headO.mesh new file mode 100644 index 0000000..a41f85f Binary files /dev/null and b/Client2016/content/fonts/headO.mesh differ diff --git a/Client2016/content/fonts/headP.mesh b/Client2016/content/fonts/headP.mesh new file mode 100644 index 0000000..cdb1647 Binary files /dev/null and b/Client2016/content/fonts/headP.mesh differ diff --git a/Client2016/content/fonts/humanoidAnimate.rbxm b/Client2016/content/fonts/humanoidAnimate.rbxm new file mode 100644 index 0000000..7a59cb9 --- /dev/null +++ b/Client2016/content/fonts/humanoidAnimate.rbxm @@ -0,0 +1,296 @@ + + null + nil + + + false + + Animate + -- Now with exciting TeamColors HACK! + +function waitForChild(parent, childName) + local child = parent:findFirstChild(childName) + if child then return child end + while true do + child = parent.ChildAdded:wait() + if child.Name==childName then return child end + end +end + +-- TEAM COLORS + + +function onTeamChanged(player) + + wait(1) + + local char = player.Character + if char == nil then return end + + if player.Neutral then + -- Replacing the current BodyColor object will force a reset + local old = char:findFirstChild("Body Colors") + if not old then return end + old:clone().Parent = char + old.Parent = nil + else + local head = char:findFirstChild("Head") + local torso = char:findFirstChild("Torso") + local left_arm = char:findFirstChild("Left Arm") + local right_arm = char:findFirstChild("Right Arm") + local left_leg = char:findFirstChild("Left Leg") + local right_leg = char:findFirstChild("Right Leg") + + if head then head.BrickColor = BrickColor.new(24) end + if torso then torso.BrickColor = player.TeamColor end + if left_arm then left_arm.BrickColor = BrickColor.new(26) end + if right_arm then right_arm.BrickColor = BrickColor.new(26) end + if left_leg then left_leg.BrickColor = BrickColor.new(26) end + if right_leg then right_leg.BrickColor = BrickColor.new(26) end + end +end + +function onPlayerPropChanged(property, player) + if property == "Character" then + onTeamChanged(player) + end + if property== "TeamColor" or property == "Neutral" then + onTeamChanged(player) + end +end + + +local cPlayer = game.Players:GetPlayerFromCharacter(script.Parent) +cPlayer.Changed:connect(function(property) onPlayerPropChanged(property, cPlayer) end ) +onTeamChanged(cPlayer) + + +-- ANIMATION + +-- declarations + +local Figure = script.Parent +local Torso = waitForChild(Figure, "Torso") +local RightShoulder = waitForChild(Torso, "Right Shoulder") +local LeftShoulder = waitForChild(Torso, "Left Shoulder") +local RightHip = waitForChild(Torso, "Right Hip") +local LeftHip = waitForChild(Torso, "Left Hip") +local Neck = waitForChild(Torso, "Neck") +local Humanoid = waitForChild(Figure, "Humanoid") +local pose = "Standing" + +local toolAnim = "None" +local toolAnimTime = 0 + +-- functions + +function onRunning(speed) + if speed>0 then + pose = "Running" + else + pose = "Standing" + end +end + +function onDied() + pose = "Dead" +end + +function onJumping() + pose = "Jumping" +end + +function onClimbing() + pose = "Climbing" +end + +function onGettingUp() + pose = "GettingUp" +end + +function onFreeFall() + pose = "FreeFall" +end + +function onFallingDown() + pose = "FallingDown" +end + +function onSeated() + pose = "Seated" +end + +function onPlatformStanding() + pose = "PlatformStanding" +end + +function moveJump() + RightShoulder.MaxVelocity = 0.5 + LeftShoulder.MaxVelocity = 0.5 + RightShoulder:SetDesiredAngle(3.14) + LeftShoulder:SetDesiredAngle(-3.14) + RightHip:SetDesiredAngle(0) + LeftHip:SetDesiredAngle(0) +end + + +-- same as jump for now + +function moveFreeFall() + RightShoulder.MaxVelocity = 0.5 + LeftShoulder.MaxVelocity = 0.5 + RightShoulder:SetDesiredAngle(3.14) + LeftShoulder:SetDesiredAngle(-3.14) + RightHip:SetDesiredAngle(0) + LeftHip:SetDesiredAngle(0) +end + +function moveSit() + RightShoulder.MaxVelocity = 0.15 + LeftShoulder.MaxVelocity = 0.15 + RightShoulder:SetDesiredAngle(3.14 /2) + LeftShoulder:SetDesiredAngle(-3.14 /2) + RightHip:SetDesiredAngle(3.14 /2) + LeftHip:SetDesiredAngle(-3.14 /2) +end + +function getTool() + for _, kid in ipairs(Figure:GetChildren()) do + if kid.className == "Tool" then return kid end + end + return nil +end + +function getToolAnim(tool) + for _, c in ipairs(tool:GetChildren()) do + if c.Name == "toolanim" and c.className == "StringValue" then + return c + end + end + return nil +end + +function animateTool() + + if (toolAnim == "None") then + RightShoulder:SetDesiredAngle(1.57) + return + end + + if (toolAnim == "Slash") then + RightShoulder.MaxVelocity = 0.5 + RightShoulder:SetDesiredAngle(0) + return + end + + if (toolAnim == "Lunge") then + RightShoulder.MaxVelocity = 0.5 + LeftShoulder.MaxVelocity = 0.5 + RightHip.MaxVelocity = 0.5 + LeftHip.MaxVelocity = 0.5 + RightShoulder:SetDesiredAngle(1.57) + LeftShoulder:SetDesiredAngle(1.0) + RightHip:SetDesiredAngle(1.57) + LeftHip:SetDesiredAngle(1.0) + return + end +end + +function move(time) + local amplitude + local frequency + + if (pose == "Jumping") then + moveJump() + return + end + + if (pose == "FreeFall") then + moveFreeFall() + return + end + + if (pose == "Seated") then + moveSit() + return + end + + local climbFudge = 0 + + if (pose == "Running") then + RightShoulder.MaxVelocity = 0.15 + LeftShoulder.MaxVelocity = 0.15 + amplitude = 1 + frequency = 9 + elseif (pose == "Climbing") then + RightShoulder.MaxVelocity = 0.5 + LeftShoulder.MaxVelocity = 0.5 + amplitude = 1 + frequency = 9 + climbFudge = 3.14 + else + amplitude = 0.1 + frequency = 1 + end + + desiredAngle = amplitude * math.sin(time*frequency) + + RightShoulder:SetDesiredAngle(desiredAngle + climbFudge) + LeftShoulder:SetDesiredAngle(desiredAngle - climbFudge) + RightHip:SetDesiredAngle(-desiredAngle) + LeftHip:SetDesiredAngle(-desiredAngle) + + + local tool = getTool() + + if tool then + + animStringValueObject = getToolAnim(tool) + + if animStringValueObject then + toolAnim = animStringValueObject.Value + -- message recieved, delete StringValue + animStringValueObject.Parent = nil + toolAnimTime = time + .3 + end + + if time > toolAnimTime then + toolAnimTime = 0 + toolAnim = "None" + end + + animateTool() + + + else + toolAnim = "None" + toolAnimTime = 0 + end +end + + +-- connect events + +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Jumping:connect(onJumping) +Humanoid.Climbing:connect(onClimbing) +Humanoid.GettingUp:connect(onGettingUp) +Humanoid.FreeFalling:connect(onFreeFall) +Humanoid.FallingDown:connect(onFallingDown) +Humanoid.Seated:connect(onSeated) +Humanoid.PlatformStanding:connect(onPlatformStanding) + +-- main program + +local runService = game:service("RunService"); + +while Figure.Parent~=nil do + local _, time = wait(0.1) + move(time) +end + + true + + + \ No newline at end of file diff --git a/Client2016/content/fonts/humanoidAnimateLocal.rbxm b/Client2016/content/fonts/humanoidAnimateLocal.rbxm new file mode 100644 index 0000000..5c18211 --- /dev/null +++ b/Client2016/content/fonts/humanoidAnimateLocal.rbxm @@ -0,0 +1,336 @@ + + null + nil + + + false + + Animate + + + function waitForChild(parent, childName) + local child = parent:findFirstChild(childName) + if child then return child end + while true do + child = parent.ChildAdded:wait() + if child.Name==childName then return child end + end +end + +-- ANIMATION + +-- declarations + +local Figure = script.Parent +local Torso = waitForChild(Figure, "Torso") +local RightShoulder = waitForChild(Torso, "Right Shoulder") +local LeftShoulder = waitForChild(Torso, "Left Shoulder") +local RightHip = waitForChild(Torso, "Right Hip") +local LeftHip = waitForChild(Torso, "Left Hip") +local Neck = waitForChild(Torso, "Neck") +local Humanoid = waitForChild(Figure, "Humanoid") +local pose = "Standing" + +local toolAnim = "None" +local toolAnimTime = 0 + +local jumpMaxLimbVelocity = 0.75 + +-- functions + +function onRunning(speed) + if speed>0 then + pose = "Running" + else + pose = "Standing" + end +end + +function onDied() + pose = "Dead" +end + +function onJumping() + pose = "Jumping" +end + +function onClimbing() + pose = "Climbing" +end + +function onGettingUp() + pose = "GettingUp" +end + +function onFreeFall() + pose = "FreeFall" +end + +function onFallingDown() + pose = "FallingDown" +end + +function onSeated() + pose = "Seated" +end + +function onPlatformStanding() + pose = "PlatformStanding" +end + +function onSwimming(speed) + if speed>0 then + pose = "Running" + else + pose = "Standing" + end +end + +function moveJump() + RightShoulder.MaxVelocity = jumpMaxLimbVelocity + LeftShoulder.MaxVelocity = jumpMaxLimbVelocity + RightShoulder:SetDesiredAngle(3.14) + LeftShoulder:SetDesiredAngle(-3.14) + RightHip:SetDesiredAngle(0) + LeftHip:SetDesiredAngle(0) +end + + +-- same as jump for now + +function moveFreeFall() + RightShoulder.MaxVelocity = jumpMaxLimbVelocity + LeftShoulder.MaxVelocity = jumpMaxLimbVelocity + RightShoulder:SetDesiredAngle(3.14) + LeftShoulder:SetDesiredAngle(-3.14) + RightHip:SetDesiredAngle(0) + LeftHip:SetDesiredAngle(0) +end + +function moveSit() + RightShoulder.MaxVelocity = 0.15 + LeftShoulder.MaxVelocity = 0.15 + RightShoulder:SetDesiredAngle(3.14 /2) + LeftShoulder:SetDesiredAngle(-3.14 /2) + RightHip:SetDesiredAngle(3.14 /2) + LeftHip:SetDesiredAngle(-3.14 /2) +end + +function getTool() + for _, kid in ipairs(Figure:GetChildren()) do + if kid.className == "Tool" then return kid end + end + return nil +end + +function getToolAnim(tool) + for _, c in ipairs(tool:GetChildren()) do + if c.Name == "toolanim" and c.className == "StringValue" then + return c + end + end + return nil +end + +function animateTool() + + if (toolAnim == "None") then + RightShoulder:SetDesiredAngle(1.57) + return + end + + if (toolAnim == "Slash") then + RightShoulder.MaxVelocity = 0.5 + RightShoulder:SetDesiredAngle(0) + return + end + + if (toolAnim == "Lunge") then + RightShoulder.MaxVelocity = 0.5 + LeftShoulder.MaxVelocity = 0.5 + RightHip.MaxVelocity = 0.5 + LeftHip.MaxVelocity = 0.5 + RightShoulder:SetDesiredAngle(1.57) + LeftShoulder:SetDesiredAngle(1.0) + RightHip:SetDesiredAngle(1.57) + LeftHip:SetDesiredAngle(1.0) + return + end +end + +function move(time) + local amplitude + local frequency + + if (pose == "Jumping") then + moveJump() + return + end + + if (pose == "FreeFall") then + moveFreeFall() + return + end + + if (pose == "Seated") then + moveSit() + return + end + + local climbFudge = 0 + + if (pose == "Running") then + if (RightShoulder.CurrentAngle > 1.5 or RightShoulder.CurrentAngle < -1.5) then + RightShoulder.MaxVelocity = jumpMaxLimbVelocity + else + RightShoulder.MaxVelocity = 0.15 + end + if (LeftShoulder.CurrentAngle > 1.5 or LeftShoulder.CurrentAngle < -1.5) then + LeftShoulder.MaxVelocity = jumpMaxLimbVelocity + else + LeftShoulder.MaxVelocity = 0.15 + end + amplitude = 1 + frequency = 9 + elseif (pose == "Climbing") then + RightShoulder.MaxVelocity = 0.5 + LeftShoulder.MaxVelocity = 0.5 + amplitude = 1 + frequency = 9 + climbFudge = 3.14 + else + amplitude = 0.1 + frequency = 1 + end + + desiredAngle = amplitude * math.sin(time*frequency) + + RightShoulder:SetDesiredAngle(desiredAngle + climbFudge) + LeftShoulder:SetDesiredAngle(desiredAngle - climbFudge) + RightHip:SetDesiredAngle(-desiredAngle) + LeftHip:SetDesiredAngle(-desiredAngle) + + + local tool = getTool() + + if tool then + + animStringValueObject = getToolAnim(tool) + + if animStringValueObject then + toolAnim = animStringValueObject.Value + -- message recieved, delete StringValue + animStringValueObject.Parent = nil + toolAnimTime = time + .3 + end + + if time > toolAnimTime then + toolAnimTime = 0 + toolAnim = "None" + end + + animateTool() + + + else + toolAnim = "None" + toolAnimTime = 0 + end +end + + +-- connect events + +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Jumping:connect(onJumping) +Humanoid.Climbing:connect(onClimbing) +Humanoid.GettingUp:connect(onGettingUp) +Humanoid.FreeFalling:connect(onFreeFall) +Humanoid.FallingDown:connect(onFallingDown) +Humanoid.Seated:connect(onSeated) +Humanoid.PlatformStanding:connect(onPlatformStanding) +Humanoid.Swimming:connect(onSwimming) +-- main program + +local runService = game:service("RunService"); + +while Figure.Parent~=nil do + local _, time = wait(0.1) + move(time) +end + + true + + + + + false + + + + RobloxTeam + + -- Now with exciting TeamColors HACK! + + function waitForChild(parent, childName) + local child = parent:findFirstChild(childName) + if child then return child end + while true do + child = parent.ChildAdded:wait() + if child.Name==childName then return child end + end + end + + -- TEAM COLORS + + + function onTeamChanged(player) + + wait(1) + + local char = player.Character + if char == nil then return end + + if player.Neutral then + -- Replacing the current BodyColor object will force a reset + local old = char:findFirstChild("Body Colors") + if not old then return end + old:clone().Parent = char + old.Parent = nil + else + local head = char:findFirstChild("Head") + local torso = char:findFirstChild("Torso") + local left_arm = char:findFirstChild("Left Arm") + local right_arm = char:findFirstChild("Right Arm") + local left_leg = char:findFirstChild("Left Leg") + local right_leg = char:findFirstChild("Right Leg") + + if head then head.BrickColor = BrickColor.new(24) end + if torso then torso.BrickColor = player.TeamColor end + if left_arm then left_arm.BrickColor = BrickColor.new(26) end + if right_arm then right_arm.BrickColor = BrickColor.new(26) end + if left_leg then left_leg.BrickColor = BrickColor.new(26) end + if right_leg then right_leg.BrickColor = BrickColor.new(26) end + end + end + + function onPlayerPropChanged(property, player) + if property == "Character" then + onTeamChanged(player) + end + if property== "TeamColor" or property == "Neutral" then + onTeamChanged(player) + end + end + + + local cPlayer = game.Players:GetPlayerFromCharacter(script.Parent) + cPlayer.Changed:connect(function(property) onPlayerPropChanged(property, cPlayer) end ) + onTeamChanged(cPlayer) + + + true + + + \ No newline at end of file diff --git a/Client2016/content/fonts/humanoidAnimateLocalKeyframe.rbxm b/Client2016/content/fonts/humanoidAnimateLocalKeyframe.rbxm new file mode 100644 index 0000000..fc12d22 --- /dev/null +++ b/Client2016/content/fonts/humanoidAnimateLocalKeyframe.rbxm @@ -0,0 +1,649 @@ + + null + nil + + + false + + Animate + function waitForChild(parent, childName) + local child = parent:findFirstChild(childName) + if child then return child end + while true do + child = parent.ChildAdded:wait() + if child.Name==childName then return child end + end +end + +local Figure = script.Parent +local Torso = waitForChild(Figure, "Torso") +local RightShoulder = waitForChild(Torso, "Right Shoulder") +local LeftShoulder = waitForChild(Torso, "Left Shoulder") +local RightHip = waitForChild(Torso, "Right Hip") +local LeftHip = waitForChild(Torso, "Left Hip") +local Neck = waitForChild(Torso, "Neck") +local Humanoid = waitForChild(Figure, "Humanoid") +local pose = "Standing" + +local currentAnim = "" +local currentAnimTrack = nil +local currentAnimKeyframeHandler = nil +local currentAnimSpeed = 1.0 +local animTable = {} +local animNames = { + idle = { + { id = "http://www.roblox.com/asset/?id=125750544", weight = 9 }, + { id = "http://www.roblox.com/asset/?id=125750618", weight = 1 } + }, + walk = { + { id = "http://www.roblox.com/asset/?id=125749145", weight = 10 } + }, + run = { + { id = "run.xml", weight = 10 } + }, + jump = { + { id = "http://www.roblox.com/asset/?id=125750702", weight = 10 } + }, + fall = { + { id = "http://www.roblox.com/asset/?id=125750759", weight = 10 } + }, + climb = { + { id = "http://www.roblox.com/asset/?id=125750800", weight = 10 } + }, + sit = { + { id = "http://www.roblox.com/asset/?id=178130996", weight = 10 } + }, + toolnone = { + { id = "http://www.roblox.com/asset/?id=125750867", weight = 10 } + }, + toolslash = { + { id = "http://www.roblox.com/asset/?id=129967390", weight = 10 } +-- { id = "slash.xml", weight = 10 } + }, + toollunge = { + { id = "http://www.roblox.com/asset/?id=129967478", weight = 10 } + }, + wave = { + { id = "http://www.roblox.com/asset/?id=128777973", weight = 10 } + }, + point = { + { id = "http://www.roblox.com/asset/?id=128853357", weight = 10 } + }, + dance = { + { id = "http://www.roblox.com/asset/?id=130018893", weight = 10 }, + { id = "http://www.roblox.com/asset/?id=132546839", weight = 10 }, + { id = "http://www.roblox.com/asset/?id=132546884", weight = 10 } + }, + dance2 = { + { id = "http://www.roblox.com/asset/?id=160934142", weight = 10 }, + { id = "http://www.roblox.com/asset/?id=160934298", weight = 10 }, + { id = "http://www.roblox.com/asset/?id=160934376", weight = 10 } + }, + dance3 = { + { id = "http://www.roblox.com/asset/?id=160934458", weight = 10 }, + { id = "http://www.roblox.com/asset/?id=160934530", weight = 10 }, + { id = "http://www.roblox.com/asset/?id=160934593", weight = 10 } + }, + laugh = { + { id = "http://www.roblox.com/asset/?id=129423131", weight = 10 } + }, + cheer = { + { id = "http://www.roblox.com/asset/?id=129423030", weight = 10 } + }, +} + +-- Existance in this list signifies that it is an emote, the value indicates if it is a looping emote +local emoteNames = { wave = false, point = false, dance = true, dance2 = true, dance3 = true, laugh = false, cheer = false} + +math.randomseed(tick()) + +function configureAnimationSet(name, fileList) + if (animTable[name] ~= nil) then + for _, connection in pairs(animTable[name].connections) do + connection:disconnect() + end + end + animTable[name] = {} + animTable[name].count = 0 + animTable[name].totalWeight = 0 + animTable[name].connections = {} + + -- check for config values + local config = script:FindFirstChild(name) + if (config ~= nil) then +-- print("Loading anims " .. name) + table.insert(animTable[name].connections, config.ChildAdded:connect(function(child) configureAnimationSet(name, fileList) end)) + table.insert(animTable[name].connections, config.ChildRemoved:connect(function(child) configureAnimationSet(name, fileList) end)) + local idx = 1 + for _, childPart in pairs(config:GetChildren()) do + if (childPart:IsA("Animation")) then + table.insert(animTable[name].connections, childPart.Changed:connect(function(property) configureAnimationSet(name, fileList) end)) + animTable[name][idx] = {} + animTable[name][idx].anim = childPart + local weightObject = childPart:FindFirstChild("Weight") + if (weightObject == nil) then + animTable[name][idx].weight = 1 + else + animTable[name][idx].weight = weightObject.Value + end + animTable[name].count = animTable[name].count + 1 + animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight + -- print(name .. " [" .. idx .. "] " .. animTable[name][idx].anim.AnimationId .. " (" .. animTable[name][idx].weight .. ")") + idx = idx + 1 + end + end + end + + -- fallback to defaults + if (animTable[name].count <= 0) then + for idx, anim in pairs(fileList) do + animTable[name][idx] = {} + animTable[name][idx].anim = Instance.new("Animation") + animTable[name][idx].anim.Name = name + animTable[name][idx].anim.AnimationId = anim.id + animTable[name][idx].weight = anim.weight + animTable[name].count = animTable[name].count + 1 + animTable[name].totalWeight = animTable[name].totalWeight + anim.weight +-- print(name .. " [" .. idx .. "] " .. anim.id .. " (" .. anim.weight .. ")") + end + end +end + +-- Setup animation objects +function scriptChildModified(child) + local fileList = animNames[child.Name] + if (fileList ~= nil) then + configureAnimationSet(child.Name, fileList) + end +end + +script.ChildAdded:connect(scriptChildModified) +script.ChildRemoved:connect(scriptChildModified) + + +for name, fileList in pairs(animNames) do + configureAnimationSet(name, fileList) +end + +-- ANIMATION + +-- declarations +local toolAnim = "None" +local toolAnimTime = 0 + +local jumpAnimTime = 0 +local jumpAnimDuration = 0.3 + +local toolTransitionTime = 0.1 +local fallTransitionTime = 0.3 +local jumpMaxLimbVelocity = 0.75 + +-- functions + +function stopAllAnimations() + local oldAnim = currentAnim + + -- return to idle if finishing an emote + if (emoteNames[oldAnim] ~= nil and emoteNames[oldAnim] == false) then + oldAnim = "idle" + end + + currentAnim = "" + if (currentAnimKeyframeHandler ~= nil) then + currentAnimKeyframeHandler:disconnect() + end + + if (currentAnimTrack ~= nil) then + currentAnimTrack:Stop() + currentAnimTrack:Destroy() + currentAnimTrack = nil + end + return oldAnim +end + +function setAnimationSpeed(speed) + if speed ~= currentAnimSpeed then + currentAnimSpeed = speed + currentAnimTrack:AdjustSpeed(currentAnimSpeed) + end +end + +function keyFrameReachedFunc(frameName) + if (frameName == "End") then +-- print("Keyframe : ".. frameName) + local repeatAnim = stopAllAnimations() + local animSpeed = currentAnimSpeed + playAnimation(repeatAnim, 0.0, Humanoid) + setAnimationSpeed(animSpeed) + end +end + +-- Preload animations +function playAnimation(animName, transitionTime, humanoid) + local idleFromEmote = (animName == "idle" and emoteNames[currentAnim] ~= nil) + if (animName ~= currentAnim and not idleFromEmote) then + + if (currentAnimTrack ~= nil) then + currentAnimTrack:Stop(transitionTime) + currentAnimTrack:Destroy() + end + + currentAnimSpeed = 1.0 + local roll = math.random(1, animTable[animName].totalWeight) + local origRoll = roll + local idx = 1 + while (roll > animTable[animName][idx].weight) do + roll = roll - animTable[animName][idx].weight + idx = idx + 1 + end +-- print(animName .. " " .. idx .. " [" .. origRoll .. "]") + local anim = animTable[animName][idx].anim + + -- load it to the humanoid; get AnimationTrack + currentAnimTrack = humanoid:LoadAnimation(anim) + + -- play the animation + currentAnimTrack:Play(transitionTime) + currentAnim = animName + + -- set up keyframe name triggers + if (currentAnimKeyframeHandler ~= nil) then + currentAnimKeyframeHandler:disconnect() + end + currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:connect(keyFrameReachedFunc) + end +end + +------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------- + +local toolAnimName = "" +local toolAnimTrack = nil +local currentToolAnimKeyframeHandler = nil + +function toolKeyFrameReachedFunc(frameName) + if (frameName == "End") then +-- print("Keyframe : ".. frameName) + local repeatAnim = stopToolAnimations() + playToolAnimation(repeatAnim, 0.0, Humanoid) + end +end + + +function playToolAnimation(animName, transitionTime, humanoid) + if (animName ~= toolAnimName) then + + if (toolAnimTrack ~= nil) then + toolAnimTrack:Stop() + toolAnimTrack:Destroy() + transitionTime = 0 + end + + local roll = math.random(1, animTable[animName].totalWeight) + local origRoll = roll + local idx = 1 + while (roll > animTable[animName][idx].weight) do + roll = roll - animTable[animName][idx].weight + idx = idx + 1 + end +-- print(animName .. " * " .. idx .. " [" .. origRoll .. "]") + local anim = animTable[animName][idx].anim + + -- load it to the humanoid; get AnimationTrack + toolAnimTrack = humanoid:LoadAnimation(anim) + + -- play the animation + toolAnimTrack:Play(transitionTime) + toolAnimName = animName + + currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:connect(toolKeyFrameReachedFunc) + end +end + +function stopToolAnimations() + local oldAnim = toolAnimName + + if (currentToolAnimKeyframeHandler ~= nil) then + currentToolAnimKeyframeHandler:disconnect() + end + + toolAnimName = "" + if (toolAnimTrack ~= nil) then + toolAnimTrack:Stop() + toolAnimTrack:Destroy() + toolAnimTrack = nil + end + + + return oldAnim +end + +------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------- + + +function onRunning(speed) + if speed>0.5 then + playAnimation("walk", 0.1, Humanoid) + pose = "Running" + else + playAnimation("idle", 0.1, Humanoid) + pose = "Standing" + end +end + +function onDied() + pose = "Dead" +end + +function onJumping() + playAnimation("jump", 0.1, Humanoid) + jumpAnimTime = jumpAnimDuration + pose = "Jumping" +end + +function onClimbing(speed) + playAnimation("climb", 0.1, Humanoid) + setAnimationSpeed(speed / 12.0) + pose = "Climbing" +end + +function onGettingUp() + pose = "GettingUp" +end + +function onFreeFall() + if (jumpAnimTime <= 0) then + playAnimation("fall", fallTransitionTime, Humanoid) + end + pose = "FreeFall" +end + +function onFallingDown() + pose = "FallingDown" +end + +function onSeated() + pose = "Seated" +end + +function onPlatformStanding() + pose = "PlatformStanding" +end + +function onSwimming(speed) + if speed>0 then + pose = "Running" + else + pose = "Standing" + end +end + +function getTool() + for _, kid in ipairs(Figure:GetChildren()) do + if kid.className == "Tool" then return kid end + end + return nil +end + +function getToolAnim(tool) + for _, c in ipairs(tool:GetChildren()) do + if c.Name == "toolanim" and c.className == "StringValue" then + return c + end + end + return nil +end + +function animateTool() + + if (toolAnim == "None") then + playToolAnimation("toolnone", toolTransitionTime, Humanoid) + return + end + + if (toolAnim == "Slash") then + playToolAnimation("toolslash", 0, Humanoid) + return + end + + if (toolAnim == "Lunge") then + playToolAnimation("toollunge", 0, Humanoid) + return + end +end + +function moveSit() + RightShoulder.MaxVelocity = 0.15 + LeftShoulder.MaxVelocity = 0.15 + RightShoulder:SetDesiredAngle(3.14 /2) + LeftShoulder:SetDesiredAngle(-3.14 /2) + RightHip:SetDesiredAngle(3.14 /2) + LeftHip:SetDesiredAngle(-3.14 /2) +end + +local lastTick = 0 + +function move(time) + local amplitude = 1 + local frequency = 1 + local deltaTime = time - lastTick + lastTick = time + + local climbFudge = 0 + local setAngles = false + + if (jumpAnimTime > 0) then + jumpAnimTime = jumpAnimTime - deltaTime + end + + if (pose == "FreeFall" and jumpAnimTime <= 0) then + playAnimation("fall", fallTransitionTime, Humanoid) + elseif (pose == "Seated") then + playAnimation("sit", 0.5, Humanoid) + return + elseif (pose == "Running") then + playAnimation("walk", 0.1, Humanoid) + elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then +-- print("Wha " .. pose) + stopAllAnimations() + amplitude = 0.1 + frequency = 1 + setAngles = true + end + + if (setAngles) then + desiredAngle = amplitude * math.sin(time * frequency) + + RightShoulder:SetDesiredAngle(desiredAngle + climbFudge) + LeftShoulder:SetDesiredAngle(desiredAngle - climbFudge) + RightHip:SetDesiredAngle(-desiredAngle) + LeftHip:SetDesiredAngle(-desiredAngle) + end + + -- Tool Animation handling + local tool = getTool() + if tool then + + animStringValueObject = getToolAnim(tool) + + if animStringValueObject then + toolAnim = animStringValueObject.Value + -- message recieved, delete StringValue + animStringValueObject.Parent = nil + toolAnimTime = time + .3 + end + + if time > toolAnimTime then + toolAnimTime = 0 + toolAnim = "None" + end + + animateTool() + else + stopToolAnimations() + toolAnim = "None" + toolAnimTime = 0 + end +end + +-- connect events +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Jumping:connect(onJumping) +Humanoid.Climbing:connect(onClimbing) +Humanoid.GettingUp:connect(onGettingUp) +Humanoid.FreeFalling:connect(onFreeFall) +Humanoid.FallingDown:connect(onFallingDown) +Humanoid.Seated:connect(onSeated) +Humanoid.PlatformStanding:connect(onPlatformStanding) +Humanoid.Swimming:connect(onSwimming) + +-- setup emote chat hook +Game.Players.LocalPlayer.Chatted:connect(function(msg) + local emote = "" + if (string.sub(msg, 1, 3) == "/e ") then + emote = string.sub(msg, 4) + elseif (string.sub(msg, 1, 7) == "/emote ") then + emote = string.sub(msg, 8) + end + + if (pose == "Standing" and emoteNames[emote] ~= nil) then + playAnimation(emote, 0.1, Humanoid) + end +-- print("===> " .. string.sub(msg, 1, 3) .. "(" .. emote .. ")") +end) + + +-- main program + +local runService = game:service("RunService"); + +-- initialize to idle +playAnimation("idle", 0.1, Humanoid) +pose = "Standing" + +while Figure.Parent~=nil do + local _, time = wait(0.1) + move(time) +end + + + + + + + idle + + + + + http://www.roblox.com/asset/?id=125750544 + Animation1 + + + + Weight + 9 + + + + + + http://www.roblox.com/asset/?id=125750618 + Animation2 + + + + Weight + 1 + + + + + + + walk + + + + + http://www.roblox.com/asset/?id=125749145 + WalkAnim + + + + + + run + + + + + http://www.roblox.com/asset/?id=125749145 + RunAnim + + + + + + jump + + + + + http://www.roblox.com/asset/?id=125750702 + JumpAnim + + + + + + climb + + + + + http://www.roblox.com/asset/?id=125750800 + ClimbAnim + + + + + + toolnone + + + + + http://www.roblox.com/asset/?id=125750867 + ToolNoneAnim + + + + + + fall + + + + + http://www.roblox.com/asset/?id=125750759 + FallAnim + + + + + + sit + + + + + http://www.roblox.com/asset/?id=178130996 + SitAnim + + + + + \ No newline at end of file diff --git a/Client2016/content/fonts/humanoidAnimateLocalKeyframe2.rbxm b/Client2016/content/fonts/humanoidAnimateLocalKeyframe2.rbxm new file mode 100644 index 0000000..7753644 --- /dev/null +++ b/Client2016/content/fonts/humanoidAnimateLocalKeyframe2.rbxm @@ -0,0 +1,669 @@ + + null + nil + + + false + + Animate + animTable[animName][idx].weight) do + roll = roll - animTable[animName][idx].weight + idx = idx + 1 + end +-- print(animName .. " " .. idx .. " [" .. origRoll .. "]") + local anim = animTable[animName][idx].anim + + -- switch animation + if (anim ~= currentAnimInstance) then + + if (currentAnimTrack ~= nil) then + currentAnimTrack:Stop(transitionTime) + currentAnimTrack:Destroy() + end + + currentAnimSpeed = 1.0 + + -- load it to the humanoid; get AnimationTrack + currentAnimTrack = humanoid:LoadAnimation(anim) + + -- play the animation + currentAnimTrack:Play(transitionTime) + currentAnim = animName + currentAnimInstance = anim + + -- set up keyframe name triggers + if (currentAnimKeyframeHandler ~= nil) then + currentAnimKeyframeHandler:disconnect() + end + currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:connect(keyFrameReachedFunc) + + end + +end + +------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------- + +local toolAnimName = "" +local toolAnimTrack = nil +local toolAnimInstance = nil +local currentToolAnimKeyframeHandler = nil + +function toolKeyFrameReachedFunc(frameName) + if (frameName == "End") then +-- print("Keyframe : ".. frameName) + playToolAnimation(toolAnimName, 0.0, Humanoid) + end +end + + +function playToolAnimation(animName, transitionTime, humanoid) + + local roll = math.random(1, animTable[animName].totalWeight) + local origRoll = roll + local idx = 1 + while (roll > animTable[animName][idx].weight) do + roll = roll - animTable[animName][idx].weight + idx = idx + 1 + end +-- print(animName .. " * " .. idx .. " [" .. origRoll .. "]") + local anim = animTable[animName][idx].anim + + if (toolAnimInstance ~= anim) then + + if (toolAnimTrack ~= nil) then + toolAnimTrack:Stop() + toolAnimTrack:Destroy() + transitionTime = 0 + end + + -- load it to the humanoid; get AnimationTrack + toolAnimTrack = humanoid:LoadAnimation(anim) + + -- play the animation + toolAnimTrack:Play(transitionTime) + toolAnimName = animName + toolAnimInstance = anim + + currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:connect(toolKeyFrameReachedFunc) + end +end + +function stopToolAnimations() + local oldAnim = toolAnimName + + if (currentToolAnimKeyframeHandler ~= nil) then + currentToolAnimKeyframeHandler:disconnect() + end + + toolAnimName = "" + toolAnimInstance = nil + if (toolAnimTrack ~= nil) then + toolAnimTrack:Stop() + toolAnimTrack:Destroy() + toolAnimTrack = nil + end + + + return oldAnim +end + +------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------- + + +function onRunning(speed) + if speed>0.01 then + playAnimation("walk", 0.1, Humanoid) + if currentAnimInstance and currentAnimInstance.AnimationId == "http://www.roblox.com/asset/?id=180426354" then + setAnimationSpeed(speed / 14.5) + end + pose = "Running" + else + playAnimation("idle", 0.1, Humanoid) + pose = "Standing" + end +end + +function onDied() + pose = "Dead" +end + +function onJumping() + playAnimation("jump", 0.1, Humanoid) + jumpAnimTime = jumpAnimDuration + pose = "Jumping" +end + +function onClimbing(speed) + playAnimation("climb", 0.1, Humanoid) + setAnimationSpeed(speed / 12.0) + pose = "Climbing" +end + +function onGettingUp() + pose = "GettingUp" +end + +function onFreeFall() + if (jumpAnimTime <= 0) then + playAnimation("fall", fallTransitionTime, Humanoid) + end + pose = "FreeFall" +end + +function onFallingDown() + pose = "FallingDown" +end + +function onSeated() + pose = "Seated" +end + +function onPlatformStanding() + pose = "PlatformStanding" +end + +function onSwimming(speed) + if speed>0 then + pose = "Running" + else + pose = "Standing" + end +end + +function getTool() + for _, kid in ipairs(Figure:GetChildren()) do + if kid.className == "Tool" then return kid end + end + return nil +end + +function getToolAnim(tool) + for _, c in ipairs(tool:GetChildren()) do + if c.Name == "toolanim" and c.className == "StringValue" then + return c + end + end + return nil +end + +function animateTool() + + if (toolAnim == "None") then + playToolAnimation("toolnone", toolTransitionTime, Humanoid) + return + end + + if (toolAnim == "Slash") then + playToolAnimation("toolslash", 0, Humanoid) + return + end + + if (toolAnim == "Lunge") then + playToolAnimation("toollunge", 0, Humanoid) + return + end +end + +function moveSit() + RightShoulder.MaxVelocity = 0.15 + LeftShoulder.MaxVelocity = 0.15 + RightShoulder:SetDesiredAngle(3.14 /2) + LeftShoulder:SetDesiredAngle(-3.14 /2) + RightHip:SetDesiredAngle(3.14 /2) + LeftHip:SetDesiredAngle(-3.14 /2) +end + +local lastTick = 0 + +function move(time) + local amplitude = 1 + local frequency = 1 + local deltaTime = time - lastTick + lastTick = time + + local climbFudge = 0 + local setAngles = false + + if (jumpAnimTime > 0) then + jumpAnimTime = jumpAnimTime - deltaTime + end + + if (pose == "FreeFall" and jumpAnimTime <= 0) then + playAnimation("fall", fallTransitionTime, Humanoid) + elseif (pose == "Seated") then + playAnimation("sit", 0.5, Humanoid) + return + elseif (pose == "Running") then + playAnimation("walk", 0.1, Humanoid) + elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then +-- print("Wha " .. pose) + stopAllAnimations() + amplitude = 0.1 + frequency = 1 + setAngles = true + end + + if (setAngles) then + desiredAngle = amplitude * math.sin(time * frequency) + + RightShoulder:SetDesiredAngle(desiredAngle + climbFudge) + LeftShoulder:SetDesiredAngle(desiredAngle - climbFudge) + RightHip:SetDesiredAngle(-desiredAngle) + LeftHip:SetDesiredAngle(-desiredAngle) + end + + -- Tool Animation handling + local tool = getTool() + if tool and tool:FindFirstChild("Handle") then + + animStringValueObject = getToolAnim(tool) + + if animStringValueObject then + toolAnim = animStringValueObject.Value + -- message recieved, delete StringValue + animStringValueObject.Parent = nil + toolAnimTime = time + .3 + end + + if time > toolAnimTime then + toolAnimTime = 0 + toolAnim = "None" + end + + animateTool() + else + stopToolAnimations() + toolAnim = "None" + toolAnimInstance = nil + toolAnimTime = 0 + end +end + +-- connect events +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Jumping:connect(onJumping) +Humanoid.Climbing:connect(onClimbing) +Humanoid.GettingUp:connect(onGettingUp) +Humanoid.FreeFalling:connect(onFreeFall) +Humanoid.FallingDown:connect(onFallingDown) +Humanoid.Seated:connect(onSeated) +Humanoid.PlatformStanding:connect(onPlatformStanding) +Humanoid.Swimming:connect(onSwimming) + +-- setup emote chat hook +game.Players.LocalPlayer.Chatted:connect(function(msg) + local emote = "" + if msg == "/e dance" then + emote = dances[math.random(1, #dances)] + elseif (string.sub(msg, 1, 3) == "/e ") then + emote = string.sub(msg, 4) + elseif (string.sub(msg, 1, 7) == "/emote ") then + emote = string.sub(msg, 8) + end + + if (pose == "Standing" and emoteNames[emote] ~= nil) then + playAnimation(emote, 0.1, Humanoid) + end + +end) + + +-- main program + +local runService = game:service("RunService"); + +-- initialize to idle +playAnimation("idle", 0.1, Humanoid) +pose = "Standing" + +while Figure.Parent~=nil do + local _, time = wait(0.1) + move(time) +end + + +]]> + + + + idle + + + + + http://www.roblox.com/asset/?id=180435571 + Animation1 + + + + Weight + 9 + + + + + + http://www.roblox.com/asset/?id=180435792 + Animation2 + + + + Weight + 1 + + + + + + + walk + + + + + http://www.roblox.com/asset/?id=180426354 + WalkAnim + + + + + + run + + + + + http://www.roblox.com/asset/?id=180426354 + RunAnim + + + + + + jump + + + + + http://www.roblox.com/asset/?id=125750702 + JumpAnim + + + + + + climb + + + + + http://www.roblox.com/asset/?id=180436334 + ClimbAnim + + + + + + toolnone + + + + + http://www.roblox.com/asset/?id=182393478 + ToolNoneAnim + + + + + + fall + + + + + http://www.roblox.com/asset/?id=180436148 + FallAnim + + + + + + sit + + + + + http://www.roblox.com/asset/?id=178130996 + SitAnim + + + + + \ No newline at end of file diff --git a/Client2016/content/fonts/humanoidAnimateR15.rbxm b/Client2016/content/fonts/humanoidAnimateR15.rbxm new file mode 100644 index 0000000..ebae470 Binary files /dev/null and b/Client2016/content/fonts/humanoidAnimateR15.rbxm differ diff --git a/Client2016/content/fonts/humanoidExtra.rbxm b/Client2016/content/fonts/humanoidExtra.rbxm new file mode 100644 index 0000000..ea08e27 --- /dev/null +++ b/Client2016/content/fonts/humanoidExtra.rbxm @@ -0,0 +1,11 @@ + + null + nil + + + false + Script + while script.Parent.Head==nil do wait(0.05) end function newSound(id) local sound = Instance.new("Sound") sound.SoundId = id sound.Parent = script.Parent.Head return sound end sDied = newSound("rbxasset://sounds/uuhhh.wav") sFallingDown = newSound("rbxasset://sounds/splat.wav") sFreeFalling = newSound("rbxasset://sounds/swoosh.wav") sGettingUp = newSound("rbxasset://sounds/hit.wav") sJumping = newSound("rbxasset://sounds/button.wav") sRunning = newSound("rbxasset://sounds/bfsl-minifigfoots1.mp3") sRunning.Looped = true function onDied() sDied:play() end function onState(state, sound) if state then sound:play() else sound:pause() end end function onRunning(speed) if speed>0 then sRunning:play() else sRunning:pause() end end while script.Parent.Humanoid==nil do wait(0.05) end h = script.Parent.Humanoid h.Died:connect(onDied) h.Running:connect(onRunning) h.Jumping:connect(function(state) onState(state, sJumping) end) h.GettingUp:connect(function(state) onState(state, sGettingUp) end) h.FreeFalling:connect(function(state) onState(state, sFreeFalling) end) h.FallingDown:connect(function(state) onState(state, sFallingDown) end) -- regeneration while true do local s = wait(1) local health=h.Health if health>0 and health<h.MaxHealth then health = health + 0.01*s*h.MaxHealth if health*1.05 < h.MaxHealth then h.Health = health else h.Health = h.MaxHealth end end end + + + \ No newline at end of file diff --git a/Client2016/content/fonts/humanoidSound.rbxm b/Client2016/content/fonts/humanoidSound.rbxm new file mode 100644 index 0000000..8968b16 --- /dev/null +++ b/Client2016/content/fonts/humanoidSound.rbxm @@ -0,0 +1,75 @@ + + null + nil + + + false + + Sound + -- util + +function waitForChild(parent, childName) + local child = parent:findFirstChild(childName) + if child then return child end + while true do + child = parent.ChildAdded:wait() + if child.Name==childName then return child end + end +end + +function newSound(id) + local sound = Instance.new("Sound") + sound.SoundId = id + sound.archivable = false + sound.Parent = script.Parent.Head + return sound +end + +-- declarations + +local sDied = newSound("rbxasset://sounds/uuhhh.wav") +local sFallingDown = newSound("rbxasset://sounds/splat.wav") +local sFreeFalling = newSound("rbxasset://sounds/swoosh.wav") +local sGettingUp = newSound("rbxasset://sounds/hit.wav") +local sJumping = newSound("rbxasset://sounds/button.wav") +local sRunning = newSound("rbxasset://sounds/bfsl-minifigfoots1.mp3") +sRunning.Looped = true + +local Figure = script.Parent +local Head = waitForChild(Figure, "Head") +local Humanoid = waitForChild(Figure, "Humanoid") + +-- functions + +function onDied() + sDied:Play() +end + +function onState(state, sound) + if state then + sound:Play() + else + sound:Pause() + end +end + +function onRunning(speed) + if speed>0.01 then + sRunning:Play() + else + sRunning:Pause() + end +end + +-- connect up + +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Jumping:connect(function(state) onState(state, sJumping) end) +Humanoid.GettingUp:connect(function(state) onState(state, sGettingUp) end) +Humanoid.FreeFalling:connect(function(state) onState(state, sFreeFalling) end) +Humanoid.FallingDown:connect(function(state) onState(state, sFallingDown) end) + + + + \ No newline at end of file diff --git a/Client2016/content/fonts/humanoidSound2.rbxm b/Client2016/content/fonts/humanoidSound2.rbxm new file mode 100644 index 0000000..01591b3 --- /dev/null +++ b/Client2016/content/fonts/humanoidSound2.rbxm @@ -0,0 +1,43 @@ + + null + nil + + + false + + Sound + -- util + +function waitForChild(parent, childName) + local child = parent:findFirstChild(childName) + if child then return child end + while true do + child = parent.ChildAdded:wait() + if child.Name==childName then return child end + end +end + +function newSound(id, name) + local sound = Instance.new("Sound") + sound.SoundId = id + sound.archivable = false + sound.Parent = script.Parent.Head + sound.Name = name + return sound +end + +-- declarations +local Figure = script.Parent +local Head = waitForChild(Figure, "Head") +local Humanoid = waitForChild(Figure, "Humanoid") + +local sDied = newSound("rbxasset://sounds/uuhhh.wav", "DiedSound") +local sFallingDown = newSound("rbxasset://sounds/splat.wav", "FallingDownSound") +local sFreeFalling = newSound("rbxasset://sounds/swoosh.wav", "FreeFallingSound") +local sGettingUp = newSound("rbxasset://sounds/hit.wav", "GettingUpSound") +local sJumping = newSound("rbxasset://sounds/button.wav", "JumpingSound") +local sRunning = newSound("rbxasset://sounds/bfsl-minifigfoots1.mp3", "RunningSound") +sRunning.Looped = true + + + \ No newline at end of file diff --git a/Client2016/content/fonts/humanoidSoundNew.rbxm b/Client2016/content/fonts/humanoidSoundNew.rbxm new file mode 100644 index 0000000..abb6eec --- /dev/null +++ b/Client2016/content/fonts/humanoidSoundNew.rbxm @@ -0,0 +1,160 @@ + + null + nil + + + false + + Sound + 0.1) then + local vol = math.min(1.0, math.max(0.0, (fallSpeed - 50) / 110)) + sLanding.Volume = vol + sLanding:Play() + fallSpeed = 0 + end + if speed>0.5 then + sRunning:Play() + sRunning.Pitch = speed / 8.0 + else + sRunning:Stop() + end + prevState = "Run" +end + +function onSwimming(speed) + if (prevState ~= "Swim" and speed > 0.1) then + local volume = math.min(1.0, speed / 350) + sSplash.Volume = volume + sSplash:Play() + prevState = "Swim" + end + sClimbing:Stop() + sRunning:Stop() + sSwimming.Pitch = 1.6 + sSwimming:Play() +end + +function onClimbing(speed) + sRunning:Stop() + sSwimming:Stop() + if speed>0.01 then + sClimbing:Play() + sClimbing.Pitch = speed / 5.5 + else + sClimbing:Stop() + end + prevState = "Climb" +end +-- connect up + +function stopLoopedSounds() + sRunning:Stop() + sClimbing:Stop() + sSwimming:Stop() +end + +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Swimming:connect(onSwimming) +Humanoid.Climbing:connect(onClimbing) +Humanoid.Jumping:connect(function(state) onStateNoStop(state, sJumping) prevState = "Jump" end) +Humanoid.GettingUp:connect(function(state) stopLoopedSounds() onStateNoStop(state, sGettingUp) prevState = "GetUp" end) +Humanoid.FreeFalling:connect(function(state) stopLoopedSounds() onStateFall(state, sFreeFalling) prevState = "FreeFall" end) +Humanoid.FallingDown:connect(function(state) stopLoopedSounds() end) +Humanoid.StateChanged:connect(function(old, new) + if not (new.Name == "Dead" or + new.Name == "Running" or + new.Name == "RunningNoPhysics" or + new.Name == "Swimming" or + new.Name == "Jumping" or + new.Name == "GettingUp" or + new.Name == "Freefall" or + new.Name == "FallingDown") then + stopLoopedSounds() + end +end) +]]> + + + \ No newline at end of file diff --git a/Client2016/content/fonts/humanoidSoundNewLocal.rbxmx b/Client2016/content/fonts/humanoidSoundNewLocal.rbxmx new file mode 100644 index 0000000..8100552 --- /dev/null +++ b/Client2016/content/fonts/humanoidSoundNewLocal.rbxmx @@ -0,0 +1,317 @@ + + null + nil + + + false + + Sound + 0.1) then + local vol = math.min(1.0, math.max(0.0, (fallSpeed - 50) / 110)) + sLanding.Volume = vol + sLanding:Play() + fallSpeed = 0 + end + if speed>0.5 then + sRunning:Play() + sRunning.Pitch = speed / 8.0 + else + sRunning:Stop() + end + prevState = "Run" +end + +function onSwimming(speed) + if (prevState ~= "Swim" and speed > 0.1) then + local volume = math.min(1.0, speed / 350) + sSplash.Volume = volume + sSplash:Play() + prevState = "Swim" + end + sClimbing:Stop() + sRunning:Stop() + sSwimming.Pitch = 1.6 + sSwimming:Play() +end + +function onClimbing(speed) + sRunning:Stop() + sSwimming:Stop() + if speed>0.01 then + sClimbing:Play() + sClimbing.Pitch = speed / 5.5 + else + sClimbing:Stop() + end + prevState = "Climb" +end +-- connect up + +function stopLoopedSounds() + sRunning:Stop() + sClimbing:Stop() + sSwimming:Stop() +end + +if hasPlayer == nil then + Humanoid.Died:connect(onDied) + Humanoid.Running:connect(onRunning) + Humanoid.Swimming:connect(onSwimming) + Humanoid.Climbing:connect(onClimbing) + Humanoid.Jumping:connect(function(state) onStateNoStop(state, sJumping) prevState = "Jump" end) + Humanoid.GettingUp:connect(function(state) stopLoopedSounds() onStateNoStop(state, sGettingUp) prevState = "GetUp" end) + Humanoid.FreeFalling:connect(function(state) stopLoopedSounds() onStateFall(state, sFreeFalling) prevState = "FreeFall" end) + Humanoid.FallingDown:connect(function(state) stopLoopedSounds() end) + Humanoid.StateChanged:connect(function(old, new) + if not (new.Name == "Dead" or + new.Name == "Running" or + new.Name == "RunningNoPhysics" or + new.Name == "Swimming" or + new.Name == "Jumping" or + new.Name == "GettingUp" or + new.Name == "Freefall" or + new.Name == "FallingDown") then + stopLoopedSounds() + end + end) +end +]]> + + + + false + + LocalSound + 0.1) then + local vol = math.min(1.0, math.max(0.0, (fallSpeed - 50) / 110)) + sLanding.Volume = vol + sLanding:Play() + fallSpeed = 0 + end + if speed>0.5 then + sRunning:Play() + sRunning.Pitch = speed / 8.0 + else + sRunning:Stop() + end + prevState = "Run" +end + +function onSwimming(speed) + if (prevState ~= "Swim" and speed > 0.1) then + local volume = math.min(1.0, speed / 350) + sSplash.Volume = volume + sSplash:Play() + prevState = "Swim" + end + sClimbing:Stop() + sRunning:Stop() + sSwimming.Pitch = 1.6 + sSwimming:Play() +end + +function onClimbing(speed) + sRunning:Stop() + sSwimming:Stop() + if speed>0.01 then + sClimbing:Play() + sClimbing.Pitch = speed / 5.5 + else + sClimbing:Stop() + end + prevState = "Climb" +end +-- connect up + +function stopLoopedSounds() + sRunning:Stop() + sClimbing:Stop() + sSwimming:Stop() +end + +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Swimming:connect(onSwimming) +Humanoid.Climbing:connect(onClimbing) +Humanoid.Jumping:connect(function(state) onStateNoStop(state, sJumping) prevState = "Jump" end) +Humanoid.GettingUp:connect(function(state) stopLoopedSounds() onStateNoStop(state, sGettingUp) prevState = "GetUp" end) +Humanoid.FreeFalling:connect(function(state) stopLoopedSounds() onStateFall(state, sFreeFalling) prevState = "FreeFall" end) +Humanoid.FallingDown:connect(function(state) stopLoopedSounds() end) +Humanoid.StateChanged:connect(function(old, new) + if not (new.Name == "Dead" or + new.Name == "Running" or + new.Name == "RunningNoPhysics" or + new.Name == "Swimming" or + new.Name == "Jumping" or + new.Name == "GettingUp" or + new.Name == "Freefall" or + new.Name == "FallingDown") then + stopLoopedSounds() + end +end) + +]]> + + + + \ No newline at end of file diff --git a/Client2016/content/fonts/humanoidStatic.rbxm b/Client2016/content/fonts/humanoidStatic.rbxm new file mode 100644 index 0000000..1c9fcb7 --- /dev/null +++ b/Client2016/content/fonts/humanoidStatic.rbxm @@ -0,0 +1,12 @@ + + null + nil + + + false + Static + local Figure = script.Parent local Torso = Figure:findFirstChild("Torso") Torso:makeJoints() + true + + + \ No newline at end of file diff --git a/Client2016/content/fonts/leftarm.mesh b/Client2016/content/fonts/leftarm.mesh new file mode 100644 index 0000000..6e8bb63 Binary files /dev/null and b/Client2016/content/fonts/leftarm.mesh differ diff --git a/Client2016/content/fonts/leftleg.mesh b/Client2016/content/fonts/leftleg.mesh new file mode 100644 index 0000000..aba3a29 Binary files /dev/null and b/Client2016/content/fonts/leftleg.mesh differ diff --git a/Client2016/content/fonts/rightarm.mesh b/Client2016/content/fonts/rightarm.mesh new file mode 100644 index 0000000..14a52a4 Binary files /dev/null and b/Client2016/content/fonts/rightarm.mesh differ diff --git a/Client2016/content/fonts/rightleg.mesh b/Client2016/content/fonts/rightleg.mesh new file mode 100644 index 0000000..dab065d Binary files /dev/null and b/Client2016/content/fonts/rightleg.mesh differ diff --git a/Client2016/content/fonts/safechat.xml b/Client2016/content/fonts/safechat.xml new file mode 100644 index 0000000..199e3b2 --- /dev/null +++ b/Client2016/content/fonts/safechat.xml @@ -0,0 +1,737 @@ + + null + nil + Use the Chat menu to talk to me. + I can only see menu chats. + + Hello + + Hi + Hi there! + Hi everyone + + + Howdy + Howdy partner! + + + Greetings + Greetings everyone + Greetings Robloxians! + Seasons greetings! + + + Welcome + Welcome to my place + Welcome to our base + Welcome to my barbeque + + Hey there! + + What's up? + How are you doing? + How's it going? + What's new? + + + Good day + Good morning + Good afternoon + Good evening + Good night + + + Silly + Waaaaaaaz up?! + Hullo! + Behold greatness, mortals! + Pardon me, is this Sparta? + THIS IS SPARTAAAA! + + + Happy Holidays! + Happy New Year! + Happy Valentine's Day! + Beware the Ides of March! + Happy St. Patrick's Day! + Happy Easter! + Happy Earth Day! + Happy 4th of July! + Happy Thanksgiving! + Happy Halloween! + Happy Hanukkah! + Merry Christmas! + Happy Halloween! + Happy Earth Day! + Happy May Day! + Happy Towel Day! + Happy ROBLOX Day! + Happy LOL Day! + + + + Goodbye + + Good Night + Sweet dreams + Go to sleep! + Lights out! + Bedtime + Going to bed now + + + Later + See ya later + Later gator! + See you tomorrow + + + Bye + Hasta la bye bye! + + I'll be right back + I have to go + + Farewell + Take care + Have a nice day + Goodluck! + Ta-ta for now! + + + Peace + Peace out! + Peace dudes! + Rest in pieces! + + + Silly + To the batcave! + Over and out! + Happy trails! + I've got to book it! + Tootles! + Smell you later! + GG! + My house is on fire! gtg. + + + + Friend + Wanna be friends? + + Follow me + Come to my place! + Come to my base! + Follow me, team! + Follow me + + + Your place is cool + Your place is fun + Your place is awesome + Your place looks good + This place is awesome! + + + Thank you + Thanks for playing + Thanks for visiting + Thanks for everything + No, thank you + Thanx + + + No problem + Don't worry + That's ok + np + + + You are ... + You are great! + You are good! + You are cool! + You are funny! + You are silly! + You are awesome! + You are doing something I don't like, please stop + + + I like ... + I like your name + I like your shirt + I like your place + I like your style + I like you + I like items + I like money + + + Sorry + My bad! + I'm sorry + Whoops! + Please forgive me. + I forgive you. + I didn't mean to do that. + Sorry, I'll stop now. + + + + Questions + + Who? + Who wants to be my friend? + Who wants to be on my team? + Who made this brilliant game? + LOLWHO? + + + What? + What is your favorite animal? + What is your favorite game? + What is your favorite movie? + What is your favorite TV show? + What is your favorite music? + What are your hobbies? + LOLWUT? + + + When? + When are you online? + When is the new version coming out? + When can we play again? + When will your place be done? + + + Where? + Where do you want to go? + Where are you going? + Where am I?! + Where did you go? + + + How? + How are you today? + How did you make this cool place? + LOLHOW? + + + Can I... + Can I have a tour? + Can I be on your team? + Can I be your friend? + Can I try something? + Can I have that please? + Can I have that back please? + Can I have borrow your hat? + Can I have borrow your gear? + + + + Answers + + You need help? + Check out the news section + Check out the help section + Read the wiki! + All the answers are in the wiki! + I will help you with this. + + + Some people ... + Me + Not me + You + All of us + Everyone but you + Builderman! + Telamon! + My team + My group + Mom + Dad + Sister + Brother + Cousin + Grandparent + Friend + + + Time ... + In the morning + In the afternoon + At night + Tomorrow + This week + This month + Sometime + Sometimes + Whenever you want + Never + After this + In 10 minutes + In a couple hours + In a couple days + + + Animals + + Cats + Lion + Tiger + Leopard + Cheetah + + + Dogs + Wolves + Beagle + Collie + Dalmatian + Poodle + Spaniel + Shepherd + Terrier + Retriever + + + Horses + Ponies + Stallions + Pwnyz + + + Reptiles + Dinosaurs + Lizards + Snakes + Turtles! + + Hamster + Monkey + Bears + + Fish + Goldfish + Sharks + Sea Bass + Halibut + Tropical Fish + + + Birds + Eagles + Penguins + Parakeets + Owls + Hawks + Pidgeons + + Elephants + + Mythical Beasts + Dragons + Unicorns + Sea Serpents + Sphinx + Cyclops + Minotaurs + Goblins + Honest Politicians + Ghosts + Scylla and Charybdis + + + + Games + + Roblox + BrickBattle + Community Building + Roblox Minigames + Contest Place + + Action + Puzzle + Strategy + Racing + RPG + Obstacle Course + Tycoon + + Board games + Chess + Checkers + Settlers of Catan + Tigris and Euphrates + El Grande + Stratego + Carcassonne + + + + Sports + Hockey + Soccer + Football + Baseball + Basketball + Volleyball + Tennis + Sports team practice + + Watersports + Surfing + Swimming + Water Polo + + + Winter sports + Skiing + Snowboarding + Sledding + Skating + + + Adventure + Rock climbing + Hiking + Fishing + Horseback riding + + + Wacky + Foosball + Calvinball + Croquet + Cricket + Dodgeball + Squash + Trampoline + + + + Movies/TV + Science Fiction + + Animated + Anime + + Comedy + Romantic + Action + Fantasy + + + Music + Country + Jazz + Rap + Hip-hop + Techno + Classical + Pop + Rock + + + Hobbies + + Computers + Building computers + Videogames + Coding + Hacking + + + The Internet + lol. teh internets! + Watching vids + + Dance + Gymnastics + + Martial Arts + Karate + Judo + Taikwon Do + Wushu + Street fighting + + Listening to music + + Music lessons + Playing in my band + Playing piano + Playing guitar + Playing violin + Playing drums + Playing a weird instrument + + Arts and crafts + + + Location + + USA + + West + Alaska + Arizona + California + Colorado + Hawaii + Idaho + Montana + Nevada + New Mexico + Oregon + Utah + Washington + Wyoming + + + Midwest + Illinois + Indiana + Iowa + Kansas + Michigan + Minnesota + Missouri + Nebraska + North Dakota + Ohio + South Dakota + Wisconsin + + + Northeast + Connecticut + Delaware + Maine + Maryland + Massachusetts + New Hampshire + New Jersey + New York + Pennsylvania + Rhode Island + Vermont + + + South + Alabama + Arkansas + Florida + Georgia + Kentucky + Louisiana + Mississippi + North Carolina + Oklahoma + South Carolina + Tennessee + Texas + Virginia + West Virginia + + + + Canada + Alberta + British Columbia + Manitoba + New Brunswick + Newfoundland + Northwest Territories + Nova Scotia + Nunavut + Ontario + Prince Edward Island + Quebec + Saskatchewan + Yukon + + Mexico + Central America + + Europe + + Great Britain + England + Scotland + Wales + Northern Ireland + + France + Germany + Spain + Italy + Poland + Switzerland + Greece + Romania + Netherlands + + + Asia + China + India + Japan + Korea + Russia + Vietnam + + + South America + Argentina + Brazil + + + Africa + Eygpt + Swaziland + + Australia + Middle East + Antarctica + New Zealand + + + Age + Rugrat + Kid + Tween + Teen + Twenties + Old + Ancient + Mesozoic + I don't want to say my age. Don't ask. + + + Mood + Good + Great! + Not bad + Sad + Hyper + Chill + Happy + Kind of mad + + Boy + Girl + I don't want to say boy or girl. Don't ask. + + + Game + Let's build + Let's battle + Nice one! + So far so good! + Lucky shot! + Oh man! + I challenge you to a fight! + Help me with this + Let's go to your game + Can you show me how do to that? + Backflip! + Frontflip! + Dance! + I'm on your side! + + Game Commands + regen + reset + go + fix + respawn + + + + Silly + Muahahahaha! + all your base are belong to me! + GET OFF MAH LAWN + TEH EPIK DUCK IS COMING!!! + ROFL + + 1337 + i r teh pwnz0r! + w00t! + z0mg h4x! + ub3rR0xXorzage! + + + + Yes + Absolutely! + Rock on! + Totally! + Juice! + Yay! + Yesh + + + No + Ummm. No. + ... + Stop! + Go away! + Don't do that + Stop breaking the rules + I don't want to + + + Ok + Well... ok + Sure + + + Uncertain + Maybe + I don't know + idk + I can't decide + Hmm... + + + + :-) + :-( + :D + :-O + lol + =D + D= + XD + ;D + ;) + O_O + =) + @_@ + >_< + T_T + ^_^ + <(0_0<) <(0_0)> (>0_0)> KIRBY DANCE + )'; + :3 + + + Ratings + Rate it! + I give it a 1 out of 10 + I give it a 2 out of 10 + I give it a 3 out of 10 + I give it a 4 out of 10 + I give it a 5 out of 10 + I give it a 6 out of 10 + I give it a 7 out of 10 + I give it a 8 out of 10 + I give it a 9 out of 10 + I give it a 10 out of 10! + + diff --git a/Client2016/content/fonts/torso.mesh b/Client2016/content/fonts/torso.mesh new file mode 100644 index 0000000..43d58d1 Binary files /dev/null and b/Client2016/content/fonts/torso.mesh differ diff --git a/Client2016/content/music/bass.wav b/Client2016/content/music/bass.wav new file mode 100644 index 0000000..5f4b791 Binary files /dev/null and b/Client2016/content/music/bass.wav differ diff --git a/Client2016/content/music/ufofly.wav b/Client2016/content/music/ufofly.wav new file mode 100644 index 0000000..359710b Binary files /dev/null and b/Client2016/content/music/ufofly.wav differ diff --git a/Client2016/content/particles/explosion.particle b/Client2016/content/particles/explosion.particle new file mode 100644 index 0000000..a7c5e1f --- /dev/null +++ b/Client2016/content/particles/explosion.particle @@ -0,0 +1,192 @@ +///////////////////////////////////////////////////////// +// The fast rising center plume of the explosion. +// Grows and rotates. +///////////////////////////////////////////////////////// +particle_system explosion/explosionPlume +{ + quota 40 + material explosion/explosionMatl + particle_width 6 + particle_height 6 + cull_each false + renderer billboard + billboard_type point + sorted false + local_space false + iteration_interval 0 + nonvisible_update_timeout 0 + billboard_type point + billboard_origin center + billboard_rotation_type vertex + common_up_vector 0 1 0 + point_rendering false + accurate_facing false + + emitter Ellipsoid + { + angle 16 + colour 1.0 0.4 0.2 1.0 + colour_range_start 1.0 0.4 0.2 1.0 + colour_range_end 0.7 0.2 0.1 0.6 + direction 0 1 0 + emission_rate 100 + position 0 6 0 + velocity 25 + velocity_min 25 + velocity_max 38 + time_to_live 1.5 + time_to_live_min 1.5 + time_to_live_max 1.5 + duration 0.2 + duration_min 0.2 + duration_max 0.2 + repeat_delay 10000 + repeat_delay_min 10000 + repeat_delay_max 10000 + width 3 + height 3 + depth 3 + } + + affector ColourFader + { + red -0.9 + green -0.5 + blue -0.3 + alpha -1.0 + } + affector Scaler + { + rate 13 + } + affector Rotator + { + rotation_speed_range_start 100 + rotation_speed_range_end 200 + rotation_range_start 100 + rotation_range_end 300 + } +} + + +///////////////////////////////////////////////////////// +//The slow moving base of the explosion. +///////////////////////////////////////////////////////// +particle_system explosion/explosionBase +{ + quota 40 + material explosion/explosionMatl + particle_width 20 + particle_height 20 + cull_each false + renderer billboard + billboard_type point + sorted false + local_space false + iteration_interval 0 + nonvisible_update_timeout 0 + billboard_type point + billboard_origin center + billboard_rotation_type vertex + common_up_vector 0 1 0 + point_rendering false + accurate_facing false + + emitter Ellipsoid + { + angle 100 + colour 1.0 0.4 0.2 1.0 + colour_range_start 1.0 0.4 0.2 1.0 + colour_range_end 0.7 0.2 0.1 0.6 + direction 0 1 0 + emission_rate 80 + position 0 0 0 + velocity 11 + velocity_min 11 + velocity_max 16 + time_to_live 1.5 + time_to_live_min 1.5 + time_to_live_max 1.5 + duration 0.2 + duration_min 0.2 + duration_max 0.2 + repeat_delay 10000 + repeat_delay_min 10000 + repeat_delay_max 10000 + width 15 + height 15 + depth 15 + } + + affector ColourFader + { + red -0.9 + green -0.5 + blue -0.3 + alpha -1.0 + } + affector Scaler + { + rate 5 + } +} + + +///////////////////////////////////////////////////////// +// The fast flying sparks of the explosion. +///////////////////////////////////////////////////////// +particle_system explosion/explosionSparks +{ + quota 120 + material explosion/explosparkMatl + particle_width 3 + particle_height 3 + cull_each false + renderer billboard + billboard_type point + sorted false + local_space false + iteration_interval 0 + nonvisible_update_timeout 0 + billboard_type point + billboard_origin center + billboard_rotation_type vertex + common_up_vector 0 1 0 + point_rendering false + accurate_facing false + + emitter Point + { + angle 120 + colour 1.0 0.6 0.4 1.0 + colour_range_start 1.0 0.6 0.4 1.0 + colour_range_end 1.0 0.6 0.4 1.0 + direction 0 1 0 + emission_rate 900 + position 0 0 0 + velocity 30 + velocity_min 30 + velocity_max 60 + time_to_live 1.2 + time_to_live_min 1.2 + time_to_live_max 1.2 + duration 0.2 + duration_min 0.1 + duration_max 0.1 + repeat_delay 10000 + repeat_delay_min 10000 + repeat_delay_max 10000 + } + + affector ColourFader + { + red -0.7 + green -0.6 + blue -0.4 + alpha -0.7 + } + affector Scaler + { + rate -3 + } +} diff --git a/Client2016/content/particles/fire.particle b/Client2016/content/particles/fire.particle new file mode 100644 index 0000000..442fe8e --- /dev/null +++ b/Client2016/content/particles/fire.particle @@ -0,0 +1,59 @@ +///////////////////////////////////////////////////////// +// Fire +///////////////////////////////////////////////////////// + +// height and width modified by RbxParticleFactory for user input size +particle_system FireTemplate +{ + material fireMat1 + particle_width 5 + particle_height 5 + cull_each false + quota 40 + renderer billboard + billboard_type point + point_rendering false + accurate_facing false + sorted false + local_space false + iteration_interval 0 + nonvisible_update_timeout 0 + + // emission rate is modified by RbxParticleManager for throttling + // of particle systems + emitter Point + { + colour_range_start 240 240 240 + colour_range_end 240 240 240 + angle 18 + emission_rate 35 + time_to_live_min 1 + time_to_live_max 1 + direction 0 1 0 + velocity_min 2.33 + velocity_max 7.0 + } + affector Rotator + { + rotation_range_start 0 + rotation_range_end 365 + rotation_speed_range_start 0 + rotation_speed_range_end 100 + } + // modified in RbxParticleFactory for user input size + affector Scaler + { + rate -5.0 + } + // modified in RbxParticleFactory for user input colors + affector ColourInterpolator + { + time0 0 + colour0 240 240 240 1 + + time1 1 + colour1 240 240 240 1 + + } + +} diff --git a/Client2016/content/particles/forceFieldBeam.particle b/Client2016/content/particles/forceFieldBeam.particle new file mode 100644 index 0000000..22914a9 --- /dev/null +++ b/Client2016/content/particles/forceFieldBeam.particle @@ -0,0 +1,55 @@ +///////////////////////////////////////////////////////// +// Beam +///////////////////////////////////////////////////////// +particle_system forceField/beam +{ + quota 40 + material PE/lensflare + particle_width 0.5 + particle_height 2 + cull_each false + billboard_type oriented_common + common_direction 0 1 0 + + emitter Ring + { + colour 1 1 1 0 + angle 0 + direction 0 1 0 + emission_rate 20 + position 0 -2 0 + velocity_min 2 + velocity_max 5 + time_to_live 2 + duration 0 + duration_min 0 + duration_max 0 + repeat_delay 0 + repeat_delay_min 0 + repeat_delay_max 0 + width 5 + height 5 + depth 1 + inner_width 0.8 + inner_height 0.8 + } + + //affector LinearForce +// { + // force_vector 0 -1 0 + // force_application add + //} + + affector ColourFader2 + { + red1 0 + green1 0 + blue1 0 + alpha1 1 + red2 0 + green2 0 + blue2 0 + alpha2 -1 + state_change 1 + } +} diff --git a/Client2016/content/particles/forceFieldRadial.particle b/Client2016/content/particles/forceFieldRadial.particle new file mode 100644 index 0000000..807f93d --- /dev/null +++ b/Client2016/content/particles/forceFieldRadial.particle @@ -0,0 +1,51 @@ +///////////////////////////////////////////////////////// +// Radial +///////////////////////////////////////////////////////// +particle_system forceField/radial +{ + material PE/lensflare + particle_width 5 + particle_height 5 + cull_each false + quota 6 + renderer billboard + billboard_type point + point_rendering false + accurate_facing false + sorted false + local_space false + iteration_interval 0 + nonvisible_update_timeout 0 + + // emission rate is modified by RbxParticleManager for throttling + // of particle systems + emitter Point + { + angle 18 + emission_rate 2 + time_to_live_min 2 + time_to_live_max 2 + direction 0 1 0 + velocity_min 0 + velocity_max 0.1 + } + affector Rotator + { + rotation_range_start 0 + rotation_range_end 365 + rotation_speed_range_start 4 + rotation_speed_range_end 100 + } + // modified in RbxParticleFactory for user input size + affector Scaler + { + rate 6.0 + } + affector ColourFader + { + red 0 + green 0 + blue 0 + alpha -0.5 + } +} diff --git a/Client2016/content/particles/smoke.particle b/Client2016/content/particles/smoke.particle new file mode 100644 index 0000000..79c6c16 --- /dev/null +++ b/Client2016/content/particles/smoke.particle @@ -0,0 +1,57 @@ +particle_system SmokeTemplate +{ + quota 70 + material PE/smoke + particle_width 1 + particle_height 1 + cull_each false + renderer billboard + billboard_type point + + emitter Box + { + angle 180 + colour 0.8 0.8 0.8 0.3 + colour_range_start 0.8 0.8 0.8 0.3 + colour_range_end 0.8 0.8 0.8 0.3 + direction 0 1 0 + emission_rate 6 + position 0 0 0 + velocity 0.1 + velocity_min 0 + velocity_max 0.1 + time_to_live 4 + time_to_live_min 4 + time_to_live_max 6 + duration 0 + duration_min 0 + duration_max 0 + repeat_delay 0 + repeat_delay_min 0 + repeat_delay_max 0 + width 1 + height 1 + depth 1 + } + + affector ColourFader + { + red -0.11 + green -0.11 + blue -0.11 + alpha -0.1 + } + + affector Scaler + { + rate 1.5 + } + + affector Rotator + { + rotation_speed_range_start 0 + rotation_speed_range_end 0 + rotation_range_start 0 + rotation_range_end 0 + } +} diff --git a/Client2016/content/particles/sparkles.particle b/Client2016/content/particles/sparkles.particle new file mode 100644 index 0000000..60102d0 --- /dev/null +++ b/Client2016/content/particles/sparkles.particle @@ -0,0 +1,49 @@ +///////////////////////////////////////////////////////// +// Sparkles +///////////////////////////////////////////////////////// +particle_system SparklesTemplate +{ + quota 40 + material sparkle/sparkleMatl + particle_width 0.8 + particle_height 1 + cull_each false + renderer billboard + sorted false + local_space false + iteration_interval 0 + nonvisible_update_timeout 0 + billboard_type point + billboard_origin center + billboard_rotation_type vertex + common_up_vector 0 1 0 + point_rendering false + accurate_facing false + + emitter Point + { + angle 180 + direction 0 -1 0 + emission_rate 35 + position 0 0 0 + velocity_min 4 + velocity_max 8 + duration 0.0 + time_to_live 1.1 + //repeat_delay 2.0 + } + affector Rotator + { + rotation_speed_range_end 360 + rotation_range_start 0 + rotation_range_end 360 + } + affector ColourFader + { + red 0 + green 0 + blue 0 + alpha -1 + } +} + diff --git a/Client2016/content/sky/lensflare.jpg b/Client2016/content/sky/lensflare.jpg new file mode 100644 index 0000000..c53f2ec Binary files /dev/null and b/Client2016/content/sky/lensflare.jpg differ diff --git a/Client2016/content/sky/moon-alpha.jpg b/Client2016/content/sky/moon-alpha.jpg new file mode 100644 index 0000000..5e193d5 Binary files /dev/null and b/Client2016/content/sky/moon-alpha.jpg differ diff --git a/Client2016/content/sky/moon.jpg b/Client2016/content/sky/moon.jpg new file mode 100644 index 0000000..247b6cd Binary files /dev/null and b/Client2016/content/sky/moon.jpg differ diff --git a/Client2016/content/sky/null_plainsky512_bk.jpg b/Client2016/content/sky/null_plainsky512_bk.jpg new file mode 100644 index 0000000..a01c9ef Binary files /dev/null and b/Client2016/content/sky/null_plainsky512_bk.jpg differ diff --git a/Client2016/content/sky/null_plainsky512_dn.jpg b/Client2016/content/sky/null_plainsky512_dn.jpg new file mode 100644 index 0000000..75f16c1 Binary files /dev/null and b/Client2016/content/sky/null_plainsky512_dn.jpg differ diff --git a/Client2016/content/sky/null_plainsky512_ft.jpg b/Client2016/content/sky/null_plainsky512_ft.jpg new file mode 100644 index 0000000..253cb31 Binary files /dev/null and b/Client2016/content/sky/null_plainsky512_ft.jpg differ diff --git a/Client2016/content/sky/null_plainsky512_lf.jpg b/Client2016/content/sky/null_plainsky512_lf.jpg new file mode 100644 index 0000000..aa3626b Binary files /dev/null and b/Client2016/content/sky/null_plainsky512_lf.jpg differ diff --git a/Client2016/content/sky/null_plainsky512_rt.jpg b/Client2016/content/sky/null_plainsky512_rt.jpg new file mode 100644 index 0000000..45b5071 Binary files /dev/null and b/Client2016/content/sky/null_plainsky512_rt.jpg differ diff --git a/Client2016/content/sky/null_plainsky512_up.jpg b/Client2016/content/sky/null_plainsky512_up.jpg new file mode 100644 index 0000000..49940da Binary files /dev/null and b/Client2016/content/sky/null_plainsky512_up.jpg differ diff --git a/Client2016/content/sky/skyspheremap.jpg b/Client2016/content/sky/skyspheremap.jpg new file mode 100644 index 0000000..947a7ee Binary files /dev/null and b/Client2016/content/sky/skyspheremap.jpg differ diff --git a/Client2016/content/sky/sun-rays.jpg b/Client2016/content/sky/sun-rays.jpg new file mode 100644 index 0000000..a76a4d5 Binary files /dev/null and b/Client2016/content/sky/sun-rays.jpg differ diff --git a/Client2016/content/sky/sun.jpg b/Client2016/content/sky/sun.jpg new file mode 100644 index 0000000..12b3829 Binary files /dev/null and b/Client2016/content/sky/sun.jpg differ diff --git a/Client2016/content/sounds/Kerplunk.mp3 b/Client2016/content/sounds/Kerplunk.mp3 new file mode 100644 index 0000000..1211b02 Binary files /dev/null and b/Client2016/content/sounds/Kerplunk.mp3 differ diff --git a/Client2016/content/sounds/Kid saying Ouch.mp3 b/Client2016/content/sounds/Kid saying Ouch.mp3 new file mode 100644 index 0000000..b3f87e0 Binary files /dev/null and b/Client2016/content/sounds/Kid saying Ouch.mp3 differ diff --git a/Client2016/content/sounds/Rubber band sling shot.mp3 b/Client2016/content/sounds/Rubber band sling shot.mp3 new file mode 100644 index 0000000..430f96c Binary files /dev/null and b/Client2016/content/sounds/Rubber band sling shot.mp3 differ diff --git a/Client2016/content/sounds/Rubber band.mp3 b/Client2016/content/sounds/Rubber band.mp3 new file mode 100644 index 0000000..f815a17 Binary files /dev/null and b/Client2016/content/sounds/Rubber band.mp3 differ diff --git a/Client2016/content/sounds/SWITCH3.mp3 b/Client2016/content/sounds/SWITCH3.mp3 new file mode 100644 index 0000000..0132c84 Binary files /dev/null and b/Client2016/content/sounds/SWITCH3.mp3 differ diff --git a/Client2016/content/sounds/action_falling.mp3 b/Client2016/content/sounds/action_falling.mp3 new file mode 100644 index 0000000..6408717 Binary files /dev/null and b/Client2016/content/sounds/action_falling.mp3 differ diff --git a/Client2016/content/sounds/action_footsteps_plastic.mp3 b/Client2016/content/sounds/action_footsteps_plastic.mp3 new file mode 100644 index 0000000..6804420 Binary files /dev/null and b/Client2016/content/sounds/action_footsteps_plastic.mp3 differ diff --git a/Client2016/content/sounds/action_get_up.mp3 b/Client2016/content/sounds/action_get_up.mp3 new file mode 100644 index 0000000..ababd77 Binary files /dev/null and b/Client2016/content/sounds/action_get_up.mp3 differ diff --git a/Client2016/content/sounds/action_jump.mp3 b/Client2016/content/sounds/action_jump.mp3 new file mode 100644 index 0000000..7159074 Binary files /dev/null and b/Client2016/content/sounds/action_jump.mp3 differ diff --git a/Client2016/content/sounds/action_jump_land.mp3 b/Client2016/content/sounds/action_jump_land.mp3 new file mode 100644 index 0000000..3d68ce1 Binary files /dev/null and b/Client2016/content/sounds/action_jump_land.mp3 differ diff --git a/Client2016/content/sounds/action_swim.mp3 b/Client2016/content/sounds/action_swim.mp3 new file mode 100644 index 0000000..95dd2d5 Binary files /dev/null and b/Client2016/content/sounds/action_swim.mp3 differ diff --git a/Client2016/content/sounds/bass.mp3 b/Client2016/content/sounds/bass.mp3 new file mode 100644 index 0000000..ffd6286 Binary files /dev/null and b/Client2016/content/sounds/bass.mp3 differ diff --git a/Client2016/content/sounds/bfsl-minifigfoots1.mp3 b/Client2016/content/sounds/bfsl-minifigfoots1.mp3 new file mode 100644 index 0000000..ca2e697 Binary files /dev/null and b/Client2016/content/sounds/bfsl-minifigfoots1.mp3 differ diff --git a/Client2016/content/sounds/bfsl-minifigfoots2.mp3 b/Client2016/content/sounds/bfsl-minifigfoots2.mp3 new file mode 100644 index 0000000..113ad8f Binary files /dev/null and b/Client2016/content/sounds/bfsl-minifigfoots2.mp3 differ diff --git a/Client2016/content/sounds/button.mp3 b/Client2016/content/sounds/button.mp3 new file mode 100644 index 0000000..1136b2b Binary files /dev/null and b/Client2016/content/sounds/button.mp3 differ diff --git a/Client2016/content/sounds/clickfast.mp3 b/Client2016/content/sounds/clickfast.mp3 new file mode 100644 index 0000000..12bfbff Binary files /dev/null and b/Client2016/content/sounds/clickfast.mp3 differ diff --git a/Client2016/content/sounds/collide.mp3 b/Client2016/content/sounds/collide.mp3 new file mode 100644 index 0000000..8f69cf0 Binary files /dev/null and b/Client2016/content/sounds/collide.mp3 differ diff --git a/Client2016/content/sounds/electronicpingshort.mp3 b/Client2016/content/sounds/electronicpingshort.mp3 new file mode 100644 index 0000000..bfb3eb0 Binary files /dev/null and b/Client2016/content/sounds/electronicpingshort.mp3 differ diff --git a/Client2016/content/sounds/flashbulb.mp3 b/Client2016/content/sounds/flashbulb.mp3 new file mode 100644 index 0000000..fdcf7f6 Binary files /dev/null and b/Client2016/content/sounds/flashbulb.mp3 differ diff --git a/Client2016/content/sounds/grass.mp3 b/Client2016/content/sounds/grass.mp3 new file mode 100644 index 0000000..e79fb5b Binary files /dev/null and b/Client2016/content/sounds/grass.mp3 differ diff --git a/Client2016/content/sounds/grass2.mp3 b/Client2016/content/sounds/grass2.mp3 new file mode 100644 index 0000000..1a1ad7a Binary files /dev/null and b/Client2016/content/sounds/grass2.mp3 differ diff --git a/Client2016/content/sounds/grass3.mp3 b/Client2016/content/sounds/grass3.mp3 new file mode 100644 index 0000000..131602a Binary files /dev/null and b/Client2016/content/sounds/grass3.mp3 differ diff --git a/Client2016/content/sounds/grassstone.mp3 b/Client2016/content/sounds/grassstone.mp3 new file mode 100644 index 0000000..4e9397c Binary files /dev/null and b/Client2016/content/sounds/grassstone.mp3 differ diff --git a/Client2016/content/sounds/grassstone2.mp3 b/Client2016/content/sounds/grassstone2.mp3 new file mode 100644 index 0000000..0a5f1c1 Binary files /dev/null and b/Client2016/content/sounds/grassstone2.mp3 differ diff --git a/Client2016/content/sounds/grassstone3.mp3 b/Client2016/content/sounds/grassstone3.mp3 new file mode 100644 index 0000000..51df635 Binary files /dev/null and b/Client2016/content/sounds/grassstone3.mp3 differ diff --git a/Client2016/content/sounds/hit.mp3 b/Client2016/content/sounds/hit.mp3 new file mode 100644 index 0000000..a299c1a Binary files /dev/null and b/Client2016/content/sounds/hit.mp3 differ diff --git a/Client2016/content/sounds/ice.mp3 b/Client2016/content/sounds/ice.mp3 new file mode 100644 index 0000000..db528cc Binary files /dev/null and b/Client2016/content/sounds/ice.mp3 differ diff --git a/Client2016/content/sounds/ice2.mp3 b/Client2016/content/sounds/ice2.mp3 new file mode 100644 index 0000000..dc4cb7b Binary files /dev/null and b/Client2016/content/sounds/ice2.mp3 differ diff --git a/Client2016/content/sounds/ice3.mp3 b/Client2016/content/sounds/ice3.mp3 new file mode 100644 index 0000000..7f3e5b1 Binary files /dev/null and b/Client2016/content/sounds/ice3.mp3 differ diff --git a/Client2016/content/sounds/icegrass.mp3 b/Client2016/content/sounds/icegrass.mp3 new file mode 100644 index 0000000..2561aff Binary files /dev/null and b/Client2016/content/sounds/icegrass.mp3 differ diff --git a/Client2016/content/sounds/icegrass2.mp3 b/Client2016/content/sounds/icegrass2.mp3 new file mode 100644 index 0000000..db6f6d2 Binary files /dev/null and b/Client2016/content/sounds/icegrass2.mp3 differ diff --git a/Client2016/content/sounds/icegrass3.mp3 b/Client2016/content/sounds/icegrass3.mp3 new file mode 100644 index 0000000..b72c2db Binary files /dev/null and b/Client2016/content/sounds/icegrass3.mp3 differ diff --git a/Client2016/content/sounds/icemetal.mp3 b/Client2016/content/sounds/icemetal.mp3 new file mode 100644 index 0000000..5c9d2d4 Binary files /dev/null and b/Client2016/content/sounds/icemetal.mp3 differ diff --git a/Client2016/content/sounds/icemetal2.mp3 b/Client2016/content/sounds/icemetal2.mp3 new file mode 100644 index 0000000..60f9e4d Binary files /dev/null and b/Client2016/content/sounds/icemetal2.mp3 differ diff --git a/Client2016/content/sounds/icemetal3.mp3 b/Client2016/content/sounds/icemetal3.mp3 new file mode 100644 index 0000000..2ee4312 Binary files /dev/null and b/Client2016/content/sounds/icemetal3.mp3 differ diff --git a/Client2016/content/sounds/icestone.mp3 b/Client2016/content/sounds/icestone.mp3 new file mode 100644 index 0000000..37b99bb Binary files /dev/null and b/Client2016/content/sounds/icestone.mp3 differ diff --git a/Client2016/content/sounds/icestone2.mp3 b/Client2016/content/sounds/icestone2.mp3 new file mode 100644 index 0000000..1f5e264 Binary files /dev/null and b/Client2016/content/sounds/icestone2.mp3 differ diff --git a/Client2016/content/sounds/icestone3.mp3 b/Client2016/content/sounds/icestone3.mp3 new file mode 100644 index 0000000..e51ce3d Binary files /dev/null and b/Client2016/content/sounds/icestone3.mp3 differ diff --git a/Client2016/content/sounds/impact_bodyfall.mp3 b/Client2016/content/sounds/impact_bodyfall.mp3 new file mode 100644 index 0000000..d7bf3ae Binary files /dev/null and b/Client2016/content/sounds/impact_bodyfall.mp3 differ diff --git a/Client2016/content/sounds/impact_explosion_01.mp3 b/Client2016/content/sounds/impact_explosion_01.mp3 new file mode 100644 index 0000000..3e03015 Binary files /dev/null and b/Client2016/content/sounds/impact_explosion_01.mp3 differ diff --git a/Client2016/content/sounds/impact_explosion_02.mp3 b/Client2016/content/sounds/impact_explosion_02.mp3 new file mode 100644 index 0000000..df33b3b Binary files /dev/null and b/Client2016/content/sounds/impact_explosion_02.mp3 differ diff --git a/Client2016/content/sounds/impact_explosion_03.mp3 b/Client2016/content/sounds/impact_explosion_03.mp3 new file mode 100644 index 0000000..4501c83 Binary files /dev/null and b/Client2016/content/sounds/impact_explosion_03.mp3 differ diff --git a/Client2016/content/sounds/impact_water.mp3 b/Client2016/content/sounds/impact_water.mp3 new file mode 100644 index 0000000..741862b Binary files /dev/null and b/Client2016/content/sounds/impact_water.mp3 differ diff --git a/Client2016/content/sounds/metal.mp3 b/Client2016/content/sounds/metal.mp3 new file mode 100644 index 0000000..32473b1 Binary files /dev/null and b/Client2016/content/sounds/metal.mp3 differ diff --git a/Client2016/content/sounds/metal2.mp3 b/Client2016/content/sounds/metal2.mp3 new file mode 100644 index 0000000..023273f Binary files /dev/null and b/Client2016/content/sounds/metal2.mp3 differ diff --git a/Client2016/content/sounds/metal3.mp3 b/Client2016/content/sounds/metal3.mp3 new file mode 100644 index 0000000..e407be0 Binary files /dev/null and b/Client2016/content/sounds/metal3.mp3 differ diff --git a/Client2016/content/sounds/metalgrass.mp3 b/Client2016/content/sounds/metalgrass.mp3 new file mode 100644 index 0000000..46fdef4 Binary files /dev/null and b/Client2016/content/sounds/metalgrass.mp3 differ diff --git a/Client2016/content/sounds/metalgrass2.mp3 b/Client2016/content/sounds/metalgrass2.mp3 new file mode 100644 index 0000000..3a7dad2 Binary files /dev/null and b/Client2016/content/sounds/metalgrass2.mp3 differ diff --git a/Client2016/content/sounds/metalgrass3.mp3 b/Client2016/content/sounds/metalgrass3.mp3 new file mode 100644 index 0000000..e8172ad Binary files /dev/null and b/Client2016/content/sounds/metalgrass3.mp3 differ diff --git a/Client2016/content/sounds/metalstone.mp3 b/Client2016/content/sounds/metalstone.mp3 new file mode 100644 index 0000000..6910e46 Binary files /dev/null and b/Client2016/content/sounds/metalstone.mp3 differ diff --git a/Client2016/content/sounds/metalstone2.mp3 b/Client2016/content/sounds/metalstone2.mp3 new file mode 100644 index 0000000..0dd924e Binary files /dev/null and b/Client2016/content/sounds/metalstone2.mp3 differ diff --git a/Client2016/content/sounds/metalstone3.mp3 b/Client2016/content/sounds/metalstone3.mp3 new file mode 100644 index 0000000..37e1198 Binary files /dev/null and b/Client2016/content/sounds/metalstone3.mp3 differ diff --git a/Client2016/content/sounds/plasticgrass.mp3 b/Client2016/content/sounds/plasticgrass.mp3 new file mode 100644 index 0000000..579d4ab Binary files /dev/null and b/Client2016/content/sounds/plasticgrass.mp3 differ diff --git a/Client2016/content/sounds/plasticgrass2.mp3 b/Client2016/content/sounds/plasticgrass2.mp3 new file mode 100644 index 0000000..f77843a Binary files /dev/null and b/Client2016/content/sounds/plasticgrass2.mp3 differ diff --git a/Client2016/content/sounds/plasticgrass3.mp3 b/Client2016/content/sounds/plasticgrass3.mp3 new file mode 100644 index 0000000..9274ff0 Binary files /dev/null and b/Client2016/content/sounds/plasticgrass3.mp3 differ diff --git a/Client2016/content/sounds/plasticice.mp3 b/Client2016/content/sounds/plasticice.mp3 new file mode 100644 index 0000000..668ba91 Binary files /dev/null and b/Client2016/content/sounds/plasticice.mp3 differ diff --git a/Client2016/content/sounds/plasticice2.mp3 b/Client2016/content/sounds/plasticice2.mp3 new file mode 100644 index 0000000..041e4ca Binary files /dev/null and b/Client2016/content/sounds/plasticice2.mp3 differ diff --git a/Client2016/content/sounds/plasticice3.mp3 b/Client2016/content/sounds/plasticice3.mp3 new file mode 100644 index 0000000..180b476 Binary files /dev/null and b/Client2016/content/sounds/plasticice3.mp3 differ diff --git a/Client2016/content/sounds/plasticmetal.mp3 b/Client2016/content/sounds/plasticmetal.mp3 new file mode 100644 index 0000000..1eadf0c Binary files /dev/null and b/Client2016/content/sounds/plasticmetal.mp3 differ diff --git a/Client2016/content/sounds/plasticmetal2.mp3 b/Client2016/content/sounds/plasticmetal2.mp3 new file mode 100644 index 0000000..6d4119a Binary files /dev/null and b/Client2016/content/sounds/plasticmetal2.mp3 differ diff --git a/Client2016/content/sounds/plasticmetal3.mp3 b/Client2016/content/sounds/plasticmetal3.mp3 new file mode 100644 index 0000000..c777ee4 Binary files /dev/null and b/Client2016/content/sounds/plasticmetal3.mp3 differ diff --git a/Client2016/content/sounds/plasticplastic.mp3 b/Client2016/content/sounds/plasticplastic.mp3 new file mode 100644 index 0000000..7283013 Binary files /dev/null and b/Client2016/content/sounds/plasticplastic.mp3 differ diff --git a/Client2016/content/sounds/plasticplastic2.mp3 b/Client2016/content/sounds/plasticplastic2.mp3 new file mode 100644 index 0000000..f9710c7 Binary files /dev/null and b/Client2016/content/sounds/plasticplastic2.mp3 differ diff --git a/Client2016/content/sounds/plasticplastic3.mp3 b/Client2016/content/sounds/plasticplastic3.mp3 new file mode 100644 index 0000000..c7cd1ca Binary files /dev/null and b/Client2016/content/sounds/plasticplastic3.mp3 differ diff --git a/Client2016/content/sounds/plasticstone.mp3 b/Client2016/content/sounds/plasticstone.mp3 new file mode 100644 index 0000000..d33e553 Binary files /dev/null and b/Client2016/content/sounds/plasticstone.mp3 differ diff --git a/Client2016/content/sounds/plasticstone2.mp3 b/Client2016/content/sounds/plasticstone2.mp3 new file mode 100644 index 0000000..cd33d00 Binary files /dev/null and b/Client2016/content/sounds/plasticstone2.mp3 differ diff --git a/Client2016/content/sounds/plasticstone3.mp3 b/Client2016/content/sounds/plasticstone3.mp3 new file mode 100644 index 0000000..5732f38 Binary files /dev/null and b/Client2016/content/sounds/plasticstone3.mp3 differ diff --git a/Client2016/content/sounds/snap.mp3 b/Client2016/content/sounds/snap.mp3 new file mode 100644 index 0000000..8dd8d5a Binary files /dev/null and b/Client2016/content/sounds/snap.mp3 differ diff --git a/Client2016/content/sounds/splat.mp3 b/Client2016/content/sounds/splat.mp3 new file mode 100644 index 0000000..934a634 Binary files /dev/null and b/Client2016/content/sounds/splat.mp3 differ diff --git a/Client2016/content/sounds/stone.mp3 b/Client2016/content/sounds/stone.mp3 new file mode 100644 index 0000000..e9dd548 Binary files /dev/null and b/Client2016/content/sounds/stone.mp3 differ diff --git a/Client2016/content/sounds/stone2.mp3 b/Client2016/content/sounds/stone2.mp3 new file mode 100644 index 0000000..f582a22 Binary files /dev/null and b/Client2016/content/sounds/stone2.mp3 differ diff --git a/Client2016/content/sounds/stone3.mp3 b/Client2016/content/sounds/stone3.mp3 new file mode 100644 index 0000000..b8bfa31 Binary files /dev/null and b/Client2016/content/sounds/stone3.mp3 differ diff --git a/Client2016/content/sounds/switch.mp3 b/Client2016/content/sounds/switch.mp3 new file mode 100644 index 0000000..69e9442 Binary files /dev/null and b/Client2016/content/sounds/switch.mp3 differ diff --git a/Client2016/content/sounds/swoosh.mp3 b/Client2016/content/sounds/swoosh.mp3 new file mode 100644 index 0000000..99014d0 Binary files /dev/null and b/Client2016/content/sounds/swoosh.mp3 differ diff --git a/Client2016/content/sounds/swordlunge.mp3 b/Client2016/content/sounds/swordlunge.mp3 new file mode 100644 index 0000000..c5b017a Binary files /dev/null and b/Client2016/content/sounds/swordlunge.mp3 differ diff --git a/Client2016/content/sounds/swordslash.mp3 b/Client2016/content/sounds/swordslash.mp3 new file mode 100644 index 0000000..cf5b902 Binary files /dev/null and b/Client2016/content/sounds/swordslash.mp3 differ diff --git a/Client2016/content/sounds/unsheath.mp3 b/Client2016/content/sounds/unsheath.mp3 new file mode 100644 index 0000000..6913873 Binary files /dev/null and b/Client2016/content/sounds/unsheath.mp3 differ diff --git a/Client2016/content/sounds/uuhhh.mp3 b/Client2016/content/sounds/uuhhh.mp3 new file mode 100644 index 0000000..81b3565 Binary files /dev/null and b/Client2016/content/sounds/uuhhh.mp3 differ diff --git a/Client2016/content/sounds/victory.mp3 b/Client2016/content/sounds/victory.mp3 new file mode 100644 index 0000000..793b1ba Binary files /dev/null and b/Client2016/content/sounds/victory.mp3 differ diff --git a/Client2016/content/sounds/woodgrass.mp3 b/Client2016/content/sounds/woodgrass.mp3 new file mode 100644 index 0000000..f70237c Binary files /dev/null and b/Client2016/content/sounds/woodgrass.mp3 differ diff --git a/Client2016/content/sounds/woodgrass2.mp3 b/Client2016/content/sounds/woodgrass2.mp3 new file mode 100644 index 0000000..0d12b62 Binary files /dev/null and b/Client2016/content/sounds/woodgrass2.mp3 differ diff --git a/Client2016/content/sounds/woodgrass3.mp3 b/Client2016/content/sounds/woodgrass3.mp3 new file mode 100644 index 0000000..f234835 Binary files /dev/null and b/Client2016/content/sounds/woodgrass3.mp3 differ diff --git a/Client2016/content/sounds/woodice.mp3 b/Client2016/content/sounds/woodice.mp3 new file mode 100644 index 0000000..5604dcf Binary files /dev/null and b/Client2016/content/sounds/woodice.mp3 differ diff --git a/Client2016/content/sounds/woodice2.mp3 b/Client2016/content/sounds/woodice2.mp3 new file mode 100644 index 0000000..dc3a586 Binary files /dev/null and b/Client2016/content/sounds/woodice2.mp3 differ diff --git a/Client2016/content/sounds/woodice3.mp3 b/Client2016/content/sounds/woodice3.mp3 new file mode 100644 index 0000000..300dea9 Binary files /dev/null and b/Client2016/content/sounds/woodice3.mp3 differ diff --git a/Client2016/content/sounds/woodmetal.mp3 b/Client2016/content/sounds/woodmetal.mp3 new file mode 100644 index 0000000..666db63 Binary files /dev/null and b/Client2016/content/sounds/woodmetal.mp3 differ diff --git a/Client2016/content/sounds/woodmetal2.mp3 b/Client2016/content/sounds/woodmetal2.mp3 new file mode 100644 index 0000000..60e87dc Binary files /dev/null and b/Client2016/content/sounds/woodmetal2.mp3 differ diff --git a/Client2016/content/sounds/woodmetal3.mp3 b/Client2016/content/sounds/woodmetal3.mp3 new file mode 100644 index 0000000..29ec64a Binary files /dev/null and b/Client2016/content/sounds/woodmetal3.mp3 differ diff --git a/Client2016/content/sounds/woodplastic.mp3 b/Client2016/content/sounds/woodplastic.mp3 new file mode 100644 index 0000000..d909b8a Binary files /dev/null and b/Client2016/content/sounds/woodplastic.mp3 differ diff --git a/Client2016/content/sounds/woodplastic2.mp3 b/Client2016/content/sounds/woodplastic2.mp3 new file mode 100644 index 0000000..c4e9b19 Binary files /dev/null and b/Client2016/content/sounds/woodplastic2.mp3 differ diff --git a/Client2016/content/sounds/woodplastic3.mp3 b/Client2016/content/sounds/woodplastic3.mp3 new file mode 100644 index 0000000..4398179 Binary files /dev/null and b/Client2016/content/sounds/woodplastic3.mp3 differ diff --git a/Client2016/content/sounds/woodstone.mp3 b/Client2016/content/sounds/woodstone.mp3 new file mode 100644 index 0000000..3f7c071 Binary files /dev/null and b/Client2016/content/sounds/woodstone.mp3 differ diff --git a/Client2016/content/sounds/woodstone2.mp3 b/Client2016/content/sounds/woodstone2.mp3 new file mode 100644 index 0000000..1b4505a Binary files /dev/null and b/Client2016/content/sounds/woodstone2.mp3 differ diff --git a/Client2016/content/sounds/woodstone3.mp3 b/Client2016/content/sounds/woodstone3.mp3 new file mode 100644 index 0000000..6e04dec Binary files /dev/null and b/Client2016/content/sounds/woodstone3.mp3 differ diff --git a/Client2016/content/sounds/woodwood.mp3 b/Client2016/content/sounds/woodwood.mp3 new file mode 100644 index 0000000..8279e0c Binary files /dev/null and b/Client2016/content/sounds/woodwood.mp3 differ diff --git a/Client2016/content/sounds/woodwood2.mp3 b/Client2016/content/sounds/woodwood2.mp3 new file mode 100644 index 0000000..ad29d7f Binary files /dev/null and b/Client2016/content/sounds/woodwood2.mp3 differ diff --git a/Client2016/content/sounds/woodwood3.mp3 b/Client2016/content/sounds/woodwood3.mp3 new file mode 100644 index 0000000..e8bf04f Binary files /dev/null and b/Client2016/content/sounds/woodwood3.mp3 differ diff --git a/Client2016/content/textures/AnchorCursor.png b/Client2016/content/textures/AnchorCursor.png new file mode 100644 index 0000000..5c4cac7 Binary files /dev/null and b/Client2016/content/textures/AnchorCursor.png differ diff --git a/Client2016/content/textures/ArrowCursor.png b/Client2016/content/textures/ArrowCursor.png new file mode 100644 index 0000000..0f839d0 Binary files /dev/null and b/Client2016/content/textures/ArrowCursor.png differ diff --git a/Client2016/content/textures/ArrowCursorDecalDrag.png b/Client2016/content/textures/ArrowCursorDecalDrag.png new file mode 100644 index 0000000..0f839d0 Binary files /dev/null and b/Client2016/content/textures/ArrowCursorDecalDrag.png differ diff --git a/Client2016/content/textures/ArrowFarCursor.png b/Client2016/content/textures/ArrowFarCursor.png new file mode 100644 index 0000000..c3ce391 Binary files /dev/null and b/Client2016/content/textures/ArrowFarCursor.png differ diff --git a/Client2016/content/textures/BWGradient.png b/Client2016/content/textures/BWGradient.png new file mode 100644 index 0000000..0a5edb5 Binary files /dev/null and b/Client2016/content/textures/BWGradient.png differ diff --git a/Client2016/content/textures/BackgroundImage.png b/Client2016/content/textures/BackgroundImage.png new file mode 100644 index 0000000..9f76324 Binary files /dev/null and b/Client2016/content/textures/BackgroundImage.png differ diff --git a/Client2016/content/textures/Blank.png b/Client2016/content/textures/Blank.png new file mode 100644 index 0000000..d8860f6 Binary files /dev/null and b/Client2016/content/textures/Blank.png differ diff --git a/Client2016/content/textures/Clone.png b/Client2016/content/textures/Clone.png new file mode 100644 index 0000000..31915a9 Binary files /dev/null and b/Client2016/content/textures/Clone.png differ diff --git a/Client2016/content/textures/CloneCursor.png b/Client2016/content/textures/CloneCursor.png new file mode 100644 index 0000000..afe32c0 Binary files /dev/null and b/Client2016/content/textures/CloneCursor.png differ diff --git a/Client2016/content/textures/CloneDownCursor.png b/Client2016/content/textures/CloneDownCursor.png new file mode 100644 index 0000000..b33240d Binary files /dev/null and b/Client2016/content/textures/CloneDownCursor.png differ diff --git a/Client2016/content/textures/CloneOverCursor.png b/Client2016/content/textures/CloneOverCursor.png new file mode 100644 index 0000000..5c97ee1 Binary files /dev/null and b/Client2016/content/textures/CloneOverCursor.png differ diff --git a/Client2016/content/textures/Cursors/Gamepad/Pointer.png b/Client2016/content/textures/Cursors/Gamepad/Pointer.png new file mode 100644 index 0000000..d14627c Binary files /dev/null and b/Client2016/content/textures/Cursors/Gamepad/Pointer.png differ diff --git a/Client2016/content/textures/Cursors/Gamepad/Pointer@2x.png b/Client2016/content/textures/Cursors/Gamepad/Pointer@2x.png new file mode 100644 index 0000000..cd8c978 Binary files /dev/null and b/Client2016/content/textures/Cursors/Gamepad/Pointer@2x.png differ diff --git a/Client2016/content/textures/Cursors/Gamepad/PointerOver.png b/Client2016/content/textures/Cursors/Gamepad/PointerOver.png new file mode 100644 index 0000000..61fee5d Binary files /dev/null and b/Client2016/content/textures/Cursors/Gamepad/PointerOver.png differ diff --git a/Client2016/content/textures/Cursors/Gamepad/PointerOver@2x.png b/Client2016/content/textures/Cursors/Gamepad/PointerOver@2x.png new file mode 100644 index 0000000..54f37cc Binary files /dev/null and b/Client2016/content/textures/Cursors/Gamepad/PointerOver@2x.png differ diff --git a/Client2016/content/textures/DialogHelp.png b/Client2016/content/textures/DialogHelp.png new file mode 100644 index 0000000..073e3b7 Binary files /dev/null and b/Client2016/content/textures/DialogHelp.png differ diff --git a/Client2016/content/textures/DialogQuest.png b/Client2016/content/textures/DialogQuest.png new file mode 100644 index 0000000..0eb475c Binary files /dev/null and b/Client2016/content/textures/DialogQuest.png differ diff --git a/Client2016/content/textures/DialogShop.png b/Client2016/content/textures/DialogShop.png new file mode 100644 index 0000000..5e5d503 Binary files /dev/null and b/Client2016/content/textures/DialogShop.png differ diff --git a/Client2016/content/textures/DragCursor.png b/Client2016/content/textures/DragCursor.png new file mode 100644 index 0000000..a8f1894 Binary files /dev/null and b/Client2016/content/textures/DragCursor.png differ diff --git a/Client2016/content/textures/DropperCursor.png b/Client2016/content/textures/DropperCursor.png new file mode 100644 index 0000000..d98bcb6 Binary files /dev/null and b/Client2016/content/textures/DropperCursor.png differ diff --git a/Client2016/content/textures/Exit.png b/Client2016/content/textures/Exit.png new file mode 100644 index 0000000..523d2c5 Binary files /dev/null and b/Client2016/content/textures/Exit.png differ diff --git a/Client2016/content/textures/Exit_dn.png b/Client2016/content/textures/Exit_dn.png new file mode 100644 index 0000000..b2359c5 Binary files /dev/null and b/Client2016/content/textures/Exit_dn.png differ diff --git a/Client2016/content/textures/Exit_ovr.png b/Client2016/content/textures/Exit_ovr.png new file mode 100644 index 0000000..b2359c5 Binary files /dev/null and b/Client2016/content/textures/Exit_ovr.png differ diff --git a/Client2016/content/textures/FillCursor.png b/Client2016/content/textures/FillCursor.png new file mode 100644 index 0000000..752cebc Binary files /dev/null and b/Client2016/content/textures/FillCursor.png differ diff --git a/Client2016/content/textures/FlagCursor.png b/Client2016/content/textures/FlagCursor.png new file mode 100644 index 0000000..59c3798 Binary files /dev/null and b/Client2016/content/textures/FlagCursor.png differ diff --git a/Client2016/content/textures/FlatCursor.png b/Client2016/content/textures/FlatCursor.png new file mode 100644 index 0000000..feba33a Binary files /dev/null and b/Client2016/content/textures/FlatCursor.png differ diff --git a/Client2016/content/textures/GrabCursor.png b/Client2016/content/textures/GrabCursor.png new file mode 100644 index 0000000..d411bae Binary files /dev/null and b/Client2016/content/textures/GrabCursor.png differ diff --git a/Client2016/content/textures/GrabRotateCursor.png b/Client2016/content/textures/GrabRotateCursor.png new file mode 100644 index 0000000..0cd0c13 Binary files /dev/null and b/Client2016/content/textures/GrabRotateCursor.png differ diff --git a/Client2016/content/textures/Grass_Texture.jpg b/Client2016/content/textures/Grass_Texture.jpg new file mode 100644 index 0000000..cd273dc Binary files /dev/null and b/Client2016/content/textures/Grass_Texture.jpg differ diff --git a/Client2016/content/textures/GunCursor.png b/Client2016/content/textures/GunCursor.png new file mode 100644 index 0000000..d1b6afc Binary files /dev/null and b/Client2016/content/textures/GunCursor.png differ diff --git a/Client2016/content/textures/GunWaitCursor.png b/Client2016/content/textures/GunWaitCursor.png new file mode 100644 index 0000000..0c812be Binary files /dev/null and b/Client2016/content/textures/GunWaitCursor.png differ diff --git a/Client2016/content/textures/HammerCursor.png b/Client2016/content/textures/HammerCursor.png new file mode 100644 index 0000000..ad2b622 Binary files /dev/null and b/Client2016/content/textures/HammerCursor.png differ diff --git a/Client2016/content/textures/HammerDownCursor.png b/Client2016/content/textures/HammerDownCursor.png new file mode 100644 index 0000000..cdbcfe2 Binary files /dev/null and b/Client2016/content/textures/HammerDownCursor.png differ diff --git a/Client2016/content/textures/HammerOverCursor.png b/Client2016/content/textures/HammerOverCursor.png new file mode 100644 index 0000000..4182d69 Binary files /dev/null and b/Client2016/content/textures/HammerOverCursor.png differ diff --git a/Client2016/content/textures/HingeCursor.png b/Client2016/content/textures/HingeCursor.png new file mode 100644 index 0000000..e507cd3 Binary files /dev/null and b/Client2016/content/textures/HingeCursor.png differ diff --git a/Client2016/content/textures/LockCursor.png b/Client2016/content/textures/LockCursor.png new file mode 100644 index 0000000..4c4c6b2 Binary files /dev/null and b/Client2016/content/textures/LockCursor.png differ diff --git a/Client2016/content/textures/MotorCursor.png b/Client2016/content/textures/MotorCursor.png new file mode 100644 index 0000000..c363ab6 Binary files /dev/null and b/Client2016/content/textures/MotorCursor.png differ diff --git a/Client2016/content/textures/MouseLockedCursor.png b/Client2016/content/textures/MouseLockedCursor.png new file mode 100644 index 0000000..c3b44db Binary files /dev/null and b/Client2016/content/textures/MouseLockedCursor.png differ diff --git a/Client2016/content/textures/ResizeCursor.png b/Client2016/content/textures/ResizeCursor.png new file mode 100644 index 0000000..e9e06f5 Binary files /dev/null and b/Client2016/content/textures/ResizeCursor.png differ diff --git a/Client2016/content/textures/Roblox-loading-glow.png b/Client2016/content/textures/Roblox-loading-glow.png new file mode 100644 index 0000000..bc757b1 Binary files /dev/null and b/Client2016/content/textures/Roblox-loading-glow.png differ diff --git a/Client2016/content/textures/Roblox-loading-glow@2x.png b/Client2016/content/textures/Roblox-loading-glow@2x.png new file mode 100644 index 0000000..a1107c4 Binary files /dev/null and b/Client2016/content/textures/Roblox-loading-glow@2x.png differ diff --git a/Client2016/content/textures/Roblox-loading.png b/Client2016/content/textures/Roblox-loading.png new file mode 100644 index 0000000..c4d13ae Binary files /dev/null and b/Client2016/content/textures/Roblox-loading.png differ diff --git a/Client2016/content/textures/Roblox-loading@2x.png b/Client2016/content/textures/Roblox-loading@2x.png new file mode 100644 index 0000000..0530a1a Binary files /dev/null and b/Client2016/content/textures/Roblox-loading@2x.png differ diff --git a/Client2016/content/textures/Roblox.bmp b/Client2016/content/textures/Roblox.bmp new file mode 100644 index 0000000..362aa34 Binary files /dev/null and b/Client2016/content/textures/Roblox.bmp differ diff --git a/Client2016/content/textures/RustGradient.png b/Client2016/content/textures/RustGradient.png new file mode 100644 index 0000000..0180253 Binary files /dev/null and b/Client2016/content/textures/RustGradient.png differ diff --git a/Client2016/content/textures/Smoke.png b/Client2016/content/textures/Smoke.png new file mode 100644 index 0000000..b86df5d Binary files /dev/null and b/Client2016/content/textures/Smoke.png differ diff --git a/Client2016/content/textures/SpawnCursor.png b/Client2016/content/textures/SpawnCursor.png new file mode 100644 index 0000000..37fb08e Binary files /dev/null and b/Client2016/content/textures/SpawnCursor.png differ diff --git a/Client2016/content/textures/SpawnLocation.png b/Client2016/content/textures/SpawnLocation.png new file mode 100644 index 0000000..049c2d3 Binary files /dev/null and b/Client2016/content/textures/SpawnLocation.png differ diff --git a/Client2016/content/textures/SurfacesDefault.png b/Client2016/content/textures/SurfacesDefault.png new file mode 100644 index 0000000..53b1505 Binary files /dev/null and b/Client2016/content/textures/SurfacesDefault.png differ diff --git a/Client2016/content/textures/UnAnchorCursor.png b/Client2016/content/textures/UnAnchorCursor.png new file mode 100644 index 0000000..fa4e9cb Binary files /dev/null and b/Client2016/content/textures/UnAnchorCursor.png differ diff --git a/Client2016/content/textures/UnlockCursor.png b/Client2016/content/textures/UnlockCursor.png new file mode 100644 index 0000000..052a07a Binary files /dev/null and b/Client2016/content/textures/UnlockCursor.png differ diff --git a/Client2016/content/textures/WeldCursor.png b/Client2016/content/textures/WeldCursor.png new file mode 100644 index 0000000..c2a4292 Binary files /dev/null and b/Client2016/content/textures/WeldCursor.png differ diff --git a/Client2016/content/textures/WhiteCircle.png b/Client2016/content/textures/WhiteCircle.png new file mode 100644 index 0000000..78f4075 Binary files /dev/null and b/Client2016/content/textures/WhiteCircle.png differ diff --git a/Client2016/content/textures/advClosed-hand-no-weld.png b/Client2016/content/textures/advClosed-hand-no-weld.png new file mode 100644 index 0000000..beb2fd4 Binary files /dev/null and b/Client2016/content/textures/advClosed-hand-no-weld.png differ diff --git a/Client2016/content/textures/advClosed-hand-weld.png b/Client2016/content/textures/advClosed-hand-weld.png new file mode 100644 index 0000000..b79d71b Binary files /dev/null and b/Client2016/content/textures/advClosed-hand-weld.png differ diff --git a/Client2016/content/textures/advClosed-hand.png b/Client2016/content/textures/advClosed-hand.png new file mode 100644 index 0000000..38fcea4 Binary files /dev/null and b/Client2016/content/textures/advClosed-hand.png differ diff --git a/Client2016/content/textures/advCursor-default.png b/Client2016/content/textures/advCursor-default.png new file mode 100644 index 0000000..58c115b Binary files /dev/null and b/Client2016/content/textures/advCursor-default.png differ diff --git a/Client2016/content/textures/advCursor-openedHand.png b/Client2016/content/textures/advCursor-openedHand.png new file mode 100644 index 0000000..c972e29 Binary files /dev/null and b/Client2016/content/textures/advCursor-openedHand.png differ diff --git a/Client2016/content/textures/advCursor-white.png b/Client2016/content/textures/advCursor-white.png new file mode 100644 index 0000000..8a68e4f Binary files /dev/null and b/Client2016/content/textures/advCursor-white.png differ diff --git a/Client2016/content/textures/advancedMove.png b/Client2016/content/textures/advancedMove.png new file mode 100644 index 0000000..da15757 Binary files /dev/null and b/Client2016/content/textures/advancedMove.png differ diff --git a/Client2016/content/textures/advancedMoveResize.png b/Client2016/content/textures/advancedMoveResize.png new file mode 100644 index 0000000..8b1264a Binary files /dev/null and b/Client2016/content/textures/advancedMoveResize.png differ diff --git a/Client2016/content/textures/advancedMove_joint.png b/Client2016/content/textures/advancedMove_joint.png new file mode 100644 index 0000000..17079e0 Binary files /dev/null and b/Client2016/content/textures/advancedMove_joint.png differ diff --git a/Client2016/content/textures/advancedMove_keysOnly.png b/Client2016/content/textures/advancedMove_keysOnly.png new file mode 100644 index 0000000..36eb233 Binary files /dev/null and b/Client2016/content/textures/advancedMove_keysOnly.png differ diff --git a/Client2016/content/textures/advancedMove_noJoint.png b/Client2016/content/textures/advancedMove_noJoint.png new file mode 100644 index 0000000..62752c4 Binary files /dev/null and b/Client2016/content/textures/advancedMove_noJoint.png differ diff --git a/Client2016/content/textures/blackBkg_round.png b/Client2016/content/textures/blackBkg_round.png new file mode 100644 index 0000000..7070a7c Binary files /dev/null and b/Client2016/content/textures/blackBkg_round.png differ diff --git a/Client2016/content/textures/blackBkg_square.png b/Client2016/content/textures/blackBkg_square.png new file mode 100644 index 0000000..dc97acf Binary files /dev/null and b/Client2016/content/textures/blackBkg_square.png differ diff --git a/Client2016/content/textures/chatBubble_botBlue_bkg.png b/Client2016/content/textures/chatBubble_botBlue_bkg.png new file mode 100644 index 0000000..9f0b008 Binary files /dev/null and b/Client2016/content/textures/chatBubble_botBlue_bkg.png differ diff --git a/Client2016/content/textures/chatBubble_botBlue_notify_bkg.png b/Client2016/content/textures/chatBubble_botBlue_notify_bkg.png new file mode 100644 index 0000000..0b03982 Binary files /dev/null and b/Client2016/content/textures/chatBubble_botBlue_notify_bkg.png differ diff --git a/Client2016/content/textures/chatBubble_botBlue_tail.png b/Client2016/content/textures/chatBubble_botBlue_tail.png new file mode 100644 index 0000000..316bc9d Binary files /dev/null and b/Client2016/content/textures/chatBubble_botBlue_tail.png differ diff --git a/Client2016/content/textures/chatBubble_botBlue_tailRight.png b/Client2016/content/textures/chatBubble_botBlue_tailRight.png new file mode 100644 index 0000000..da047a8 Binary files /dev/null and b/Client2016/content/textures/chatBubble_botBlue_tailRight.png differ diff --git a/Client2016/content/textures/chatBubble_botGreen_bkg.png b/Client2016/content/textures/chatBubble_botGreen_bkg.png new file mode 100644 index 0000000..e158c85 Binary files /dev/null and b/Client2016/content/textures/chatBubble_botGreen_bkg.png differ diff --git a/Client2016/content/textures/chatBubble_botGreen_notify_bkg.png b/Client2016/content/textures/chatBubble_botGreen_notify_bkg.png new file mode 100644 index 0000000..d0a0c47 Binary files /dev/null and b/Client2016/content/textures/chatBubble_botGreen_notify_bkg.png differ diff --git a/Client2016/content/textures/chatBubble_botGreen_tail.png b/Client2016/content/textures/chatBubble_botGreen_tail.png new file mode 100644 index 0000000..01e6a00 Binary files /dev/null and b/Client2016/content/textures/chatBubble_botGreen_tail.png differ diff --git a/Client2016/content/textures/chatBubble_botGreen_tailRight.png b/Client2016/content/textures/chatBubble_botGreen_tailRight.png new file mode 100644 index 0000000..8cd6293 Binary files /dev/null and b/Client2016/content/textures/chatBubble_botGreen_tailRight.png differ diff --git a/Client2016/content/textures/chatBubble_botRed_bkg.png b/Client2016/content/textures/chatBubble_botRed_bkg.png new file mode 100644 index 0000000..66e411f Binary files /dev/null and b/Client2016/content/textures/chatBubble_botRed_bkg.png differ diff --git a/Client2016/content/textures/chatBubble_botRed_notify_bkg.png b/Client2016/content/textures/chatBubble_botRed_notify_bkg.png new file mode 100644 index 0000000..e7376eb Binary files /dev/null and b/Client2016/content/textures/chatBubble_botRed_notify_bkg.png differ diff --git a/Client2016/content/textures/chatBubble_botRed_tail.png b/Client2016/content/textures/chatBubble_botRed_tail.png new file mode 100644 index 0000000..b29c2af Binary files /dev/null and b/Client2016/content/textures/chatBubble_botRed_tail.png differ diff --git a/Client2016/content/textures/chatBubble_botRed_tailRight.png b/Client2016/content/textures/chatBubble_botRed_tailRight.png new file mode 100644 index 0000000..1e39788 Binary files /dev/null and b/Client2016/content/textures/chatBubble_botRed_tailRight.png differ diff --git a/Client2016/content/textures/chatBubble_bot_notifyGray_dotDotDot.png b/Client2016/content/textures/chatBubble_bot_notifyGray_dotDotDot.png new file mode 100644 index 0000000..f5b017d Binary files /dev/null and b/Client2016/content/textures/chatBubble_bot_notifyGray_dotDotDot.png differ diff --git a/Client2016/content/textures/chatBubble_bot_notify_bang.png b/Client2016/content/textures/chatBubble_bot_notify_bang.png new file mode 100644 index 0000000..1ccef6c Binary files /dev/null and b/Client2016/content/textures/chatBubble_bot_notify_bang.png differ diff --git a/Client2016/content/textures/chatBubble_bot_notify_dotDotDot.png b/Client2016/content/textures/chatBubble_bot_notify_dotDotDot.png new file mode 100644 index 0000000..4318458 Binary files /dev/null and b/Client2016/content/textures/chatBubble_bot_notify_dotDotDot.png differ diff --git a/Client2016/content/textures/chatBubble_bot_notify_money.png b/Client2016/content/textures/chatBubble_bot_notify_money.png new file mode 100644 index 0000000..54f58c5 Binary files /dev/null and b/Client2016/content/textures/chatBubble_bot_notify_money.png differ diff --git a/Client2016/content/textures/chatBubble_bot_notify_question.png b/Client2016/content/textures/chatBubble_bot_notify_question.png new file mode 100644 index 0000000..0a91c4f Binary files /dev/null and b/Client2016/content/textures/chatBubble_bot_notify_question.png differ diff --git a/Client2016/content/textures/chatBubble_white_bkg.png b/Client2016/content/textures/chatBubble_white_bkg.png new file mode 100644 index 0000000..7e6648c Binary files /dev/null and b/Client2016/content/textures/chatBubble_white_bkg.png differ diff --git a/Client2016/content/textures/chatBubble_white_notify_bkg.png b/Client2016/content/textures/chatBubble_white_notify_bkg.png new file mode 100644 index 0000000..828e362 Binary files /dev/null and b/Client2016/content/textures/chatBubble_white_notify_bkg.png differ diff --git a/Client2016/content/textures/chatBubble_white_tail.png b/Client2016/content/textures/chatBubble_white_tail.png new file mode 100644 index 0000000..77d1bd4 Binary files /dev/null and b/Client2016/content/textures/chatBubble_white_tail.png differ diff --git a/Client2016/content/textures/dirt.jpg b/Client2016/content/textures/dirt.jpg new file mode 100644 index 0000000..05f5c94 Binary files /dev/null and b/Client2016/content/textures/dirt.jpg differ diff --git a/Client2016/content/textures/explosion.png b/Client2016/content/textures/explosion.png new file mode 100644 index 0000000..5124cb2 Binary files /dev/null and b/Client2016/content/textures/explosion.png differ diff --git a/Client2016/content/textures/face.png b/Client2016/content/textures/face.png new file mode 100644 index 0000000..e57e342 Binary files /dev/null and b/Client2016/content/textures/face.png differ diff --git a/Client2016/content/textures/fire_0.png b/Client2016/content/textures/fire_0.png new file mode 100644 index 0000000..bbada0f Binary files /dev/null and b/Client2016/content/textures/fire_0.png differ diff --git a/Client2016/content/textures/glow.png b/Client2016/content/textures/glow.png new file mode 100644 index 0000000..db4a99e Binary files /dev/null and b/Client2016/content/textures/glow.png differ diff --git a/Client2016/content/textures/gradient.png b/Client2016/content/textures/gradient.png new file mode 100644 index 0000000..4278521 Binary files /dev/null and b/Client2016/content/textures/gradient.png differ diff --git a/Client2016/content/textures/loading/cancelButton.png b/Client2016/content/textures/loading/cancelButton.png new file mode 100644 index 0000000..4020631 Binary files /dev/null and b/Client2016/content/textures/loading/cancelButton.png differ diff --git a/Client2016/content/textures/loading/darkLoadingTexture.png b/Client2016/content/textures/loading/darkLoadingTexture.png new file mode 100644 index 0000000..1cae38f Binary files /dev/null and b/Client2016/content/textures/loading/darkLoadingTexture.png differ diff --git a/Client2016/content/textures/loading/loadingCircle.png b/Client2016/content/textures/loading/loadingCircle.png new file mode 100644 index 0000000..aa1c650 Binary files /dev/null and b/Client2016/content/textures/loading/loadingCircle.png differ diff --git a/Client2016/content/textures/loading/loadingTexture.png b/Client2016/content/textures/loading/loadingTexture.png new file mode 100644 index 0000000..ba9b60d Binary files /dev/null and b/Client2016/content/textures/loading/loadingTexture.png differ diff --git a/Client2016/content/textures/loading/loadingvignette.png b/Client2016/content/textures/loading/loadingvignette.png new file mode 100644 index 0000000..1398ef7 Binary files /dev/null and b/Client2016/content/textures/loading/loadingvignette.png differ diff --git a/Client2016/content/textures/loading/robloxlogo.png b/Client2016/content/textures/loading/robloxlogo.png new file mode 100644 index 0000000..180233d Binary files /dev/null and b/Client2016/content/textures/loading/robloxlogo.png differ diff --git a/Client2016/content/textures/lua.png b/Client2016/content/textures/lua.png new file mode 100644 index 0000000..2791c43 Binary files /dev/null and b/Client2016/content/textures/lua.png differ diff --git a/Client2016/content/textures/particles/common_alpha.dds b/Client2016/content/textures/particles/common_alpha.dds new file mode 100644 index 0000000..2cc9c9c Binary files /dev/null and b/Client2016/content/textures/particles/common_alpha.dds differ diff --git a/Client2016/content/textures/particles/explosion01_core_alpha.png b/Client2016/content/textures/particles/explosion01_core_alpha.png new file mode 100644 index 0000000..2dbf12f Binary files /dev/null and b/Client2016/content/textures/particles/explosion01_core_alpha.png differ diff --git a/Client2016/content/textures/particles/explosion01_core_main.dds b/Client2016/content/textures/particles/explosion01_core_main.dds new file mode 100644 index 0000000..44fd908 Binary files /dev/null and b/Client2016/content/textures/particles/explosion01_core_main.dds differ diff --git a/Client2016/content/textures/particles/explosion01_implosion_color.png b/Client2016/content/textures/particles/explosion01_implosion_color.png new file mode 100644 index 0000000..385bed7 Binary files /dev/null and b/Client2016/content/textures/particles/explosion01_implosion_color.png differ diff --git a/Client2016/content/textures/particles/explosion01_implosion_main.dds b/Client2016/content/textures/particles/explosion01_implosion_main.dds new file mode 100644 index 0000000..a5bf1b5 Binary files /dev/null and b/Client2016/content/textures/particles/explosion01_implosion_main.dds differ diff --git a/Client2016/content/textures/particles/explosion01_shockwave_main.dds b/Client2016/content/textures/particles/explosion01_shockwave_main.dds new file mode 100644 index 0000000..7a30ca6 Binary files /dev/null and b/Client2016/content/textures/particles/explosion01_shockwave_main.dds differ diff --git a/Client2016/content/textures/particles/explosion01_smoke_alpha.dds b/Client2016/content/textures/particles/explosion01_smoke_alpha.dds new file mode 100644 index 0000000..99807c7 Binary files /dev/null and b/Client2016/content/textures/particles/explosion01_smoke_alpha.dds differ diff --git a/Client2016/content/textures/particles/explosion01_smoke_color_new.dds b/Client2016/content/textures/particles/explosion01_smoke_color_new.dds new file mode 100644 index 0000000..fd4df8f Binary files /dev/null and b/Client2016/content/textures/particles/explosion01_smoke_color_new.dds differ diff --git a/Client2016/content/textures/particles/explosion01_smoke_main.dds b/Client2016/content/textures/particles/explosion01_smoke_main.dds new file mode 100644 index 0000000..c37f99a Binary files /dev/null and b/Client2016/content/textures/particles/explosion01_smoke_main.dds differ diff --git a/Client2016/content/textures/particles/explosion_alpha.dds b/Client2016/content/textures/particles/explosion_alpha.dds new file mode 100644 index 0000000..add095f Binary files /dev/null and b/Client2016/content/textures/particles/explosion_alpha.dds differ diff --git a/Client2016/content/textures/particles/explosion_color.dds b/Client2016/content/textures/particles/explosion_color.dds new file mode 100644 index 0000000..f69e2cc Binary files /dev/null and b/Client2016/content/textures/particles/explosion_color.dds differ diff --git a/Client2016/content/textures/particles/fire_alpha.dds b/Client2016/content/textures/particles/fire_alpha.dds new file mode 100644 index 0000000..812a506 Binary files /dev/null and b/Client2016/content/textures/particles/fire_alpha.dds differ diff --git a/Client2016/content/textures/particles/fire_color.dds b/Client2016/content/textures/particles/fire_color.dds new file mode 100644 index 0000000..5d569ad Binary files /dev/null and b/Client2016/content/textures/particles/fire_color.dds differ diff --git a/Client2016/content/textures/particles/fire_main.dds b/Client2016/content/textures/particles/fire_main.dds new file mode 100644 index 0000000..bd398ae Binary files /dev/null and b/Client2016/content/textures/particles/fire_main.dds differ diff --git a/Client2016/content/textures/particles/fire_sparks_color.dds b/Client2016/content/textures/particles/fire_sparks_color.dds new file mode 100644 index 0000000..8876db1 Binary files /dev/null and b/Client2016/content/textures/particles/fire_sparks_color.dds differ diff --git a/Client2016/content/textures/particles/fire_sparks_main.dds b/Client2016/content/textures/particles/fire_sparks_main.dds new file mode 100644 index 0000000..4468e5a Binary files /dev/null and b/Client2016/content/textures/particles/fire_sparks_main.dds differ diff --git a/Client2016/content/textures/particles/forcefield_alpha.dds b/Client2016/content/textures/particles/forcefield_alpha.dds new file mode 100644 index 0000000..03fdac0 Binary files /dev/null and b/Client2016/content/textures/particles/forcefield_alpha.dds differ diff --git a/Client2016/content/textures/particles/forcefield_glow_alpha.dds b/Client2016/content/textures/particles/forcefield_glow_alpha.dds new file mode 100644 index 0000000..326310f Binary files /dev/null and b/Client2016/content/textures/particles/forcefield_glow_alpha.dds differ diff --git a/Client2016/content/textures/particles/forcefield_glow_color.dds b/Client2016/content/textures/particles/forcefield_glow_color.dds new file mode 100644 index 0000000..39dfb8e Binary files /dev/null and b/Client2016/content/textures/particles/forcefield_glow_color.dds differ diff --git a/Client2016/content/textures/particles/forcefield_glow_main.dds b/Client2016/content/textures/particles/forcefield_glow_main.dds new file mode 100644 index 0000000..d1a6472 Binary files /dev/null and b/Client2016/content/textures/particles/forcefield_glow_main.dds differ diff --git a/Client2016/content/textures/particles/forcefield_vortex_color.dds b/Client2016/content/textures/particles/forcefield_vortex_color.dds new file mode 100644 index 0000000..fe33e22 Binary files /dev/null and b/Client2016/content/textures/particles/forcefield_vortex_color.dds differ diff --git a/Client2016/content/textures/particles/forcefield_vortex_main.dds b/Client2016/content/textures/particles/forcefield_vortex_main.dds new file mode 100644 index 0000000..d67cf49 Binary files /dev/null and b/Client2016/content/textures/particles/forcefield_vortex_main.dds differ diff --git a/Client2016/content/textures/particles/legacy_fire_alpha_color.dds b/Client2016/content/textures/particles/legacy_fire_alpha_color.dds new file mode 100644 index 0000000..da0fb05 Binary files /dev/null and b/Client2016/content/textures/particles/legacy_fire_alpha_color.dds differ diff --git a/Client2016/content/textures/particles/smoke_color.dds b/Client2016/content/textures/particles/smoke_color.dds new file mode 100644 index 0000000..a4d0a4f Binary files /dev/null and b/Client2016/content/textures/particles/smoke_color.dds differ diff --git a/Client2016/content/textures/particles/smoke_main.dds b/Client2016/content/textures/particles/smoke_main.dds new file mode 100644 index 0000000..1aaef8d Binary files /dev/null and b/Client2016/content/textures/particles/smoke_main.dds differ diff --git a/Client2016/content/textures/particles/sparkles_color.dds b/Client2016/content/textures/particles/sparkles_color.dds new file mode 100644 index 0000000..4fd1f85 Binary files /dev/null and b/Client2016/content/textures/particles/sparkles_color.dds differ diff --git a/Client2016/content/textures/particles/sparkles_main.dds b/Client2016/content/textures/particles/sparkles_main.dds new file mode 100644 index 0000000..d1875fd Binary files /dev/null and b/Client2016/content/textures/particles/sparkles_main.dds differ diff --git a/Client2016/content/textures/roblox-logo.png b/Client2016/content/textures/roblox-logo.png new file mode 100644 index 0000000..cf5d0c2 Binary files /dev/null and b/Client2016/content/textures/roblox-logo.png differ diff --git a/Client2016/content/textures/rotationArrow.png b/Client2016/content/textures/rotationArrow.png new file mode 100644 index 0000000..427aa20 Binary files /dev/null and b/Client2016/content/textures/rotationArrow.png differ diff --git a/Client2016/content/textures/script.png b/Client2016/content/textures/script.png new file mode 100644 index 0000000..0f9ed4d Binary files /dev/null and b/Client2016/content/textures/script.png differ diff --git a/Client2016/content/textures/shadowmask.png b/Client2016/content/textures/shadowmask.png new file mode 100644 index 0000000..47ed550 Binary files /dev/null and b/Client2016/content/textures/shadowmask.png differ diff --git a/Client2016/content/textures/spark.png b/Client2016/content/textures/spark.png new file mode 100644 index 0000000..486f47c Binary files /dev/null and b/Client2016/content/textures/spark.png differ diff --git a/Client2016/content/textures/sparkle.png b/Client2016/content/textures/sparkle.png new file mode 100644 index 0000000..5fa5e2a Binary files /dev/null and b/Client2016/content/textures/sparkle.png differ diff --git a/Client2016/content/textures/transformFiveDegrees.png b/Client2016/content/textures/transformFiveDegrees.png new file mode 100644 index 0000000..542315f Binary files /dev/null and b/Client2016/content/textures/transformFiveDegrees.png differ diff --git a/Client2016/content/textures/transformNinetyDegrees.png b/Client2016/content/textures/transformNinetyDegrees.png new file mode 100644 index 0000000..8f6f8fe Binary files /dev/null and b/Client2016/content/textures/transformNinetyDegrees.png differ diff --git a/Client2016/content/textures/transformOneDegree.png b/Client2016/content/textures/transformOneDegree.png new file mode 100644 index 0000000..aa93f1e Binary files /dev/null and b/Client2016/content/textures/transformOneDegree.png differ diff --git a/Client2016/content/textures/transformTwentyTwoDegrees.png b/Client2016/content/textures/transformTwentyTwoDegrees.png new file mode 100644 index 0000000..cf9e6a1 Binary files /dev/null and b/Client2016/content/textures/transformTwentyTwoDegrees.png differ diff --git a/Client2016/content/textures/ui/Backpack/Backpack.png b/Client2016/content/textures/ui/Backpack/Backpack.png new file mode 100644 index 0000000..3aab561 Binary files /dev/null and b/Client2016/content/textures/ui/Backpack/Backpack.png differ diff --git a/Client2016/content/textures/ui/Backpack/Backpack@2x.png b/Client2016/content/textures/ui/Backpack/Backpack@2x.png new file mode 100644 index 0000000..c13f00b Binary files /dev/null and b/Client2016/content/textures/ui/Backpack/Backpack@2x.png differ diff --git a/Client2016/content/textures/ui/Backpack/Backpack_Down.png b/Client2016/content/textures/ui/Backpack/Backpack_Down.png new file mode 100644 index 0000000..c2f0fdd Binary files /dev/null and b/Client2016/content/textures/ui/Backpack/Backpack_Down.png differ diff --git a/Client2016/content/textures/ui/Backpack/Backpack_Down@2x.png b/Client2016/content/textures/ui/Backpack/Backpack_Down@2x.png new file mode 100644 index 0000000..56e9c4b Binary files /dev/null and b/Client2016/content/textures/ui/Backpack/Backpack_Down@2x.png differ diff --git a/Client2016/content/textures/ui/Backpack_Close.png b/Client2016/content/textures/ui/Backpack_Close.png new file mode 100644 index 0000000..ed2442b Binary files /dev/null and b/Client2016/content/textures/ui/Backpack_Close.png differ diff --git a/Client2016/content/textures/ui/Backpack_Close@2x.png b/Client2016/content/textures/ui/Backpack_Close@2x.png new file mode 100644 index 0000000..73098da Binary files /dev/null and b/Client2016/content/textures/ui/Backpack_Close@2x.png differ diff --git a/Client2016/content/textures/ui/Backpack_Open.png b/Client2016/content/textures/ui/Backpack_Open.png new file mode 100644 index 0000000..d6ddd0b Binary files /dev/null and b/Client2016/content/textures/ui/Backpack_Open.png differ diff --git a/Client2016/content/textures/ui/Backpack_Open@2x.png b/Client2016/content/textures/ui/Backpack_Open@2x.png new file mode 100644 index 0000000..d3a58cd Binary files /dev/null and b/Client2016/content/textures/ui/Backpack_Open@2x.png differ diff --git a/Client2016/content/textures/ui/ButtonLeft.png b/Client2016/content/textures/ui/ButtonLeft.png new file mode 100644 index 0000000..3ccafc8 Binary files /dev/null and b/Client2016/content/textures/ui/ButtonLeft.png differ diff --git a/Client2016/content/textures/ui/ButtonLeftDown.png b/Client2016/content/textures/ui/ButtonLeftDown.png new file mode 100644 index 0000000..a17d93c Binary files /dev/null and b/Client2016/content/textures/ui/ButtonLeftDown.png differ diff --git a/Client2016/content/textures/ui/ButtonRight.png b/Client2016/content/textures/ui/ButtonRight.png new file mode 100644 index 0000000..e6f3e3e Binary files /dev/null and b/Client2016/content/textures/ui/ButtonRight.png differ diff --git a/Client2016/content/textures/ui/ButtonRightDown.png b/Client2016/content/textures/ui/ButtonRightDown.png new file mode 100644 index 0000000..70f1bc7 Binary files /dev/null and b/Client2016/content/textures/ui/ButtonRightDown.png differ diff --git a/Client2016/content/textures/ui/Chat/Chat.png b/Client2016/content/textures/ui/Chat/Chat.png new file mode 100644 index 0000000..4a81845 Binary files /dev/null and b/Client2016/content/textures/ui/Chat/Chat.png differ diff --git a/Client2016/content/textures/ui/Chat/Chat@2x.png b/Client2016/content/textures/ui/Chat/Chat@2x.png new file mode 100644 index 0000000..52cdff1 Binary files /dev/null and b/Client2016/content/textures/ui/Chat/Chat@2x.png differ diff --git a/Client2016/content/textures/ui/Chat/ChatDown.png b/Client2016/content/textures/ui/Chat/ChatDown.png new file mode 100644 index 0000000..a8e354f Binary files /dev/null and b/Client2016/content/textures/ui/Chat/ChatDown.png differ diff --git a/Client2016/content/textures/ui/Chat/ChatDown@2x.png b/Client2016/content/textures/ui/Chat/ChatDown@2x.png new file mode 100644 index 0000000..1bc47fd Binary files /dev/null and b/Client2016/content/textures/ui/Chat/ChatDown@2x.png differ diff --git a/Client2016/content/textures/ui/Chat/MessageCounter.png b/Client2016/content/textures/ui/Chat/MessageCounter.png new file mode 100644 index 0000000..ad062c4 Binary files /dev/null and b/Client2016/content/textures/ui/Chat/MessageCounter.png differ diff --git a/Client2016/content/textures/ui/Chat/MessageCounter@2x.png b/Client2016/content/textures/ui/Chat/MessageCounter@2x.png new file mode 100644 index 0000000..d978e2e Binary files /dev/null and b/Client2016/content/textures/ui/Chat/MessageCounter@2x.png differ diff --git a/Client2016/content/textures/ui/Chat/ToggleChat.png b/Client2016/content/textures/ui/Chat/ToggleChat.png new file mode 100644 index 0000000..49a4eb1 Binary files /dev/null and b/Client2016/content/textures/ui/Chat/ToggleChat.png differ diff --git a/Client2016/content/textures/ui/Chat/ToggleChat@2x.png b/Client2016/content/textures/ui/Chat/ToggleChat@2x.png new file mode 100644 index 0000000..20bdca8 Binary files /dev/null and b/Client2016/content/textures/ui/Chat/ToggleChat@2x.png differ diff --git a/Client2016/content/textures/ui/Chat/ToggleChatDown.png b/Client2016/content/textures/ui/Chat/ToggleChatDown.png new file mode 100644 index 0000000..16c5acb Binary files /dev/null and b/Client2016/content/textures/ui/Chat/ToggleChatDown.png differ diff --git a/Client2016/content/textures/ui/Chat/ToggleChatDown@2x.png b/Client2016/content/textures/ui/Chat/ToggleChatDown@2x.png new file mode 100644 index 0000000..9f07a8d Binary files /dev/null and b/Client2016/content/textures/ui/Chat/ToggleChatDown@2x.png differ diff --git a/Client2016/content/textures/ui/CloneButton.png b/Client2016/content/textures/ui/CloneButton.png new file mode 100644 index 0000000..7e24cc6 Binary files /dev/null and b/Client2016/content/textures/ui/CloneButton.png differ diff --git a/Client2016/content/textures/ui/CloneButton_dn.png b/Client2016/content/textures/ui/CloneButton_dn.png new file mode 100644 index 0000000..adbfe60 Binary files /dev/null and b/Client2016/content/textures/ui/CloneButton_dn.png differ diff --git a/Client2016/content/textures/ui/CloseButton.png b/Client2016/content/textures/ui/CloseButton.png new file mode 100644 index 0000000..59d62c0 Binary files /dev/null and b/Client2016/content/textures/ui/CloseButton.png differ diff --git a/Client2016/content/textures/ui/CloseButton_dn.png b/Client2016/content/textures/ui/CloseButton_dn.png new file mode 100644 index 0000000..2a92663 Binary files /dev/null and b/Client2016/content/textures/ui/CloseButton_dn.png differ diff --git a/Client2016/content/textures/ui/Concrete.png b/Client2016/content/textures/ui/Concrete.png new file mode 100644 index 0000000..79ee458 Binary files /dev/null and b/Client2016/content/textures/ui/Concrete.png differ diff --git a/Client2016/content/textures/ui/CorrodedMetal.png b/Client2016/content/textures/ui/CorrodedMetal.png new file mode 100644 index 0000000..76483c7 Binary files /dev/null and b/Client2016/content/textures/ui/CorrodedMetal.png differ diff --git a/Client2016/content/textures/ui/DPadSheet.png b/Client2016/content/textures/ui/DPadSheet.png new file mode 100644 index 0000000..dbef643 Binary files /dev/null and b/Client2016/content/textures/ui/DPadSheet.png differ diff --git a/Client2016/content/textures/ui/DeleteButton.png b/Client2016/content/textures/ui/DeleteButton.png new file mode 100644 index 0000000..7f5757c Binary files /dev/null and b/Client2016/content/textures/ui/DeleteButton.png differ diff --git a/Client2016/content/textures/ui/DeleteButton_dn.png b/Client2016/content/textures/ui/DeleteButton_dn.png new file mode 100644 index 0000000..184a615 Binary files /dev/null and b/Client2016/content/textures/ui/DeleteButton_dn.png differ diff --git a/Client2016/content/textures/ui/DiamondPlate.png b/Client2016/content/textures/ui/DiamondPlate.png new file mode 100644 index 0000000..ad9d4d0 Binary files /dev/null and b/Client2016/content/textures/ui/DiamondPlate.png differ diff --git a/Client2016/content/textures/ui/ErrorIcon.png b/Client2016/content/textures/ui/ErrorIcon.png new file mode 100644 index 0000000..e69c486 Binary files /dev/null and b/Client2016/content/textures/ui/ErrorIcon.png differ diff --git a/Client2016/content/textures/ui/ErrorIconSmall.png b/Client2016/content/textures/ui/ErrorIconSmall.png new file mode 100644 index 0000000..16a49a7 Binary files /dev/null and b/Client2016/content/textures/ui/ErrorIconSmall.png differ diff --git a/Client2016/content/textures/ui/Foil.png b/Client2016/content/textures/ui/Foil.png new file mode 100644 index 0000000..7cf51fc Binary files /dev/null and b/Client2016/content/textures/ui/Foil.png differ diff --git a/Client2016/content/textures/ui/Gear.png b/Client2016/content/textures/ui/Gear.png new file mode 100644 index 0000000..de60a6a Binary files /dev/null and b/Client2016/content/textures/ui/Gear.png differ diff --git a/Client2016/content/textures/ui/Gear_dn.png b/Client2016/content/textures/ui/Gear_dn.png new file mode 100644 index 0000000..53c8366 Binary files /dev/null and b/Client2016/content/textures/ui/Gear_dn.png differ diff --git a/Client2016/content/textures/ui/Glue.png b/Client2016/content/textures/ui/Glue.png new file mode 100644 index 0000000..3ceed9c Binary files /dev/null and b/Client2016/content/textures/ui/Glue.png differ diff --git a/Client2016/content/textures/ui/Grass.png b/Client2016/content/textures/ui/Grass.png new file mode 100644 index 0000000..fd390c4 Binary files /dev/null and b/Client2016/content/textures/ui/Grass.png differ diff --git a/Client2016/content/textures/ui/GroupMoveButton.png b/Client2016/content/textures/ui/GroupMoveButton.png new file mode 100644 index 0000000..61a48b4 Binary files /dev/null and b/Client2016/content/textures/ui/GroupMoveButton.png differ diff --git a/Client2016/content/textures/ui/GroupMoveButton_dn.png b/Client2016/content/textures/ui/GroupMoveButton_dn.png new file mode 100644 index 0000000..7500d53 Binary files /dev/null and b/Client2016/content/textures/ui/GroupMoveButton_dn.png differ diff --git a/Client2016/content/textures/ui/Health-BKG-Center.png b/Client2016/content/textures/ui/Health-BKG-Center.png new file mode 100644 index 0000000..eb0f3a4 Binary files /dev/null and b/Client2016/content/textures/ui/Health-BKG-Center.png differ diff --git a/Client2016/content/textures/ui/Health-BKG-Center@2x.png b/Client2016/content/textures/ui/Health-BKG-Center@2x.png new file mode 100644 index 0000000..e875086 Binary files /dev/null and b/Client2016/content/textures/ui/Health-BKG-Center@2x.png differ diff --git a/Client2016/content/textures/ui/Health-BKG-Left-Cap.png b/Client2016/content/textures/ui/Health-BKG-Left-Cap.png new file mode 100644 index 0000000..4437d29 Binary files /dev/null and b/Client2016/content/textures/ui/Health-BKG-Left-Cap.png differ diff --git a/Client2016/content/textures/ui/Health-BKG-Left-Cap@2x.png b/Client2016/content/textures/ui/Health-BKG-Left-Cap@2x.png new file mode 100644 index 0000000..1f7c652 Binary files /dev/null and b/Client2016/content/textures/ui/Health-BKG-Left-Cap@2x.png differ diff --git a/Client2016/content/textures/ui/Health-BKG-Right-Cap.png b/Client2016/content/textures/ui/Health-BKG-Right-Cap.png new file mode 100644 index 0000000..12db34b Binary files /dev/null and b/Client2016/content/textures/ui/Health-BKG-Right-Cap.png differ diff --git a/Client2016/content/textures/ui/Health-BKG-Right-Cap@2x.png b/Client2016/content/textures/ui/Health-BKG-Right-Cap@2x.png new file mode 100644 index 0000000..49a0e50 Binary files /dev/null and b/Client2016/content/textures/ui/Health-BKG-Right-Cap@2x.png differ diff --git a/Client2016/content/textures/ui/Hinge.png b/Client2016/content/textures/ui/Hinge.png new file mode 100644 index 0000000..c2ab9c9 Binary files /dev/null and b/Client2016/content/textures/ui/Hinge.png differ diff --git a/Client2016/content/textures/ui/Ice.png b/Client2016/content/textures/ui/Ice.png new file mode 100644 index 0000000..e9d0a32 Binary files /dev/null and b/Client2016/content/textures/ui/Ice.png differ diff --git a/Client2016/content/textures/ui/Inlets.png b/Client2016/content/textures/ui/Inlets.png new file mode 100644 index 0000000..cac89e3 Binary files /dev/null and b/Client2016/content/textures/ui/Inlets.png differ diff --git a/Client2016/content/textures/ui/InsertButton.png b/Client2016/content/textures/ui/InsertButton.png new file mode 100644 index 0000000..4058f36 Binary files /dev/null and b/Client2016/content/textures/ui/InsertButton.png differ diff --git a/Client2016/content/textures/ui/InsertButton_dn.png b/Client2016/content/textures/ui/InsertButton_dn.png new file mode 100644 index 0000000..03e72b2 Binary files /dev/null and b/Client2016/content/textures/ui/InsertButton_dn.png differ diff --git a/Client2016/content/textures/ui/LoadingBKG.png b/Client2016/content/textures/ui/LoadingBKG.png new file mode 100644 index 0000000..212468c Binary files /dev/null and b/Client2016/content/textures/ui/LoadingBKG.png differ diff --git a/Client2016/content/textures/ui/MaterialButton.png b/Client2016/content/textures/ui/MaterialButton.png new file mode 100644 index 0000000..4191aab Binary files /dev/null and b/Client2016/content/textures/ui/MaterialButton.png differ diff --git a/Client2016/content/textures/ui/MaterialButton_dn.png b/Client2016/content/textures/ui/MaterialButton_dn.png new file mode 100644 index 0000000..d61978c Binary files /dev/null and b/Client2016/content/textures/ui/MaterialButton_dn.png differ diff --git a/Client2016/content/textures/ui/MaterialMenu.png b/Client2016/content/textures/ui/MaterialMenu.png new file mode 100644 index 0000000..c4a1c73 Binary files /dev/null and b/Client2016/content/textures/ui/MaterialMenu.png differ diff --git a/Client2016/content/textures/ui/Menu/Hamburger.png b/Client2016/content/textures/ui/Menu/Hamburger.png new file mode 100644 index 0000000..29520b8 Binary files /dev/null and b/Client2016/content/textures/ui/Menu/Hamburger.png differ diff --git a/Client2016/content/textures/ui/Menu/Hamburger@2x.png b/Client2016/content/textures/ui/Menu/Hamburger@2x.png new file mode 100644 index 0000000..e17c15f Binary files /dev/null and b/Client2016/content/textures/ui/Menu/Hamburger@2x.png differ diff --git a/Client2016/content/textures/ui/Menu/HamburgerDown.png b/Client2016/content/textures/ui/Menu/HamburgerDown.png new file mode 100644 index 0000000..6d4fe9c Binary files /dev/null and b/Client2016/content/textures/ui/Menu/HamburgerDown.png differ diff --git a/Client2016/content/textures/ui/Menu/HamburgerDown@2x.png b/Client2016/content/textures/ui/Menu/HamburgerDown@2x.png new file mode 100644 index 0000000..b788db9 Binary files /dev/null and b/Client2016/content/textures/ui/Menu/HamburgerDown@2x.png differ diff --git a/Client2016/content/textures/ui/Modal.png b/Client2016/content/textures/ui/Modal.png new file mode 100644 index 0000000..ff98cf8 Binary files /dev/null and b/Client2016/content/textures/ui/Modal.png differ diff --git a/Client2016/content/textures/ui/Motor.png b/Client2016/content/textures/ui/Motor.png new file mode 100644 index 0000000..e9b63eb Binary files /dev/null and b/Client2016/content/textures/ui/Motor.png differ diff --git a/Client2016/content/textures/ui/PaintButton.png b/Client2016/content/textures/ui/PaintButton.png new file mode 100644 index 0000000..03b99a7 Binary files /dev/null and b/Client2016/content/textures/ui/PaintButton.png differ diff --git a/Client2016/content/textures/ui/PaintButton_dn.png b/Client2016/content/textures/ui/PaintButton_dn.png new file mode 100644 index 0000000..af4c64d Binary files /dev/null and b/Client2016/content/textures/ui/PaintButton_dn.png differ diff --git a/Client2016/content/textures/ui/PartMoveButton.png b/Client2016/content/textures/ui/PartMoveButton.png new file mode 100644 index 0000000..a8f8fce Binary files /dev/null and b/Client2016/content/textures/ui/PartMoveButton.png differ diff --git a/Client2016/content/textures/ui/PartMoveButton_dn.png b/Client2016/content/textures/ui/PartMoveButton_dn.png new file mode 100644 index 0000000..3c866db Binary files /dev/null and b/Client2016/content/textures/ui/PartMoveButton_dn.png differ diff --git a/Client2016/content/textures/ui/Plastic.png b/Client2016/content/textures/ui/Plastic.png new file mode 100644 index 0000000..8f8bbf3 Binary files /dev/null and b/Client2016/content/textures/ui/Plastic.png differ diff --git a/Client2016/content/textures/ui/PlayerList/BlockedIcon.png b/Client2016/content/textures/ui/PlayerList/BlockedIcon.png new file mode 100644 index 0000000..6505f14 Binary files /dev/null and b/Client2016/content/textures/ui/PlayerList/BlockedIcon.png differ diff --git a/Client2016/content/textures/ui/PlayerList/CharacterImageBackground.png b/Client2016/content/textures/ui/PlayerList/CharacterImageBackground.png new file mode 100644 index 0000000..624bc78 Binary files /dev/null and b/Client2016/content/textures/ui/PlayerList/CharacterImageBackground.png differ diff --git a/Client2016/content/textures/ui/PlayerList/TileShadowMissingTop.png b/Client2016/content/textures/ui/PlayerList/TileShadowMissingTop.png new file mode 100644 index 0000000..64b90fc Binary files /dev/null and b/Client2016/content/textures/ui/PlayerList/TileShadowMissingTop.png differ diff --git a/Client2016/content/textures/ui/PlayerListFriendRequestReceivedIcon.png b/Client2016/content/textures/ui/PlayerListFriendRequestReceivedIcon.png new file mode 100644 index 0000000..ab292da Binary files /dev/null and b/Client2016/content/textures/ui/PlayerListFriendRequestReceivedIcon.png differ diff --git a/Client2016/content/textures/ui/PlayerListFriendRequestSentIcon.png b/Client2016/content/textures/ui/PlayerListFriendRequestSentIcon.png new file mode 100644 index 0000000..e10634b Binary files /dev/null and b/Client2016/content/textures/ui/PlayerListFriendRequestSentIcon.png differ diff --git a/Client2016/content/textures/ui/PlayerlistFriendIcon.png b/Client2016/content/textures/ui/PlayerlistFriendIcon.png new file mode 100644 index 0000000..21619a5 Binary files /dev/null and b/Client2016/content/textures/ui/PlayerlistFriendIcon.png differ diff --git a/Client2016/content/textures/ui/PropertyButton.png b/Client2016/content/textures/ui/PropertyButton.png new file mode 100644 index 0000000..254e00d Binary files /dev/null and b/Client2016/content/textures/ui/PropertyButton.png differ diff --git a/Client2016/content/textures/ui/PropertyButton_dn.png b/Client2016/content/textures/ui/PropertyButton_dn.png new file mode 100644 index 0000000..c95ec8d Binary files /dev/null and b/Client2016/content/textures/ui/PropertyButton_dn.png differ diff --git a/Client2016/content/textures/ui/RecordDown.png b/Client2016/content/textures/ui/RecordDown.png new file mode 100644 index 0000000..0e815a2 Binary files /dev/null and b/Client2016/content/textures/ui/RecordDown.png differ diff --git a/Client2016/content/textures/ui/RecordStop.png b/Client2016/content/textures/ui/RecordStop.png new file mode 100644 index 0000000..6b86baf Binary files /dev/null and b/Client2016/content/textures/ui/RecordStop.png differ diff --git a/Client2016/content/textures/ui/ResetIcon.png b/Client2016/content/textures/ui/ResetIcon.png new file mode 100644 index 0000000..b9558d4 Binary files /dev/null and b/Client2016/content/textures/ui/ResetIcon.png differ diff --git a/Client2016/content/textures/ui/RobuxIcon.png b/Client2016/content/textures/ui/RobuxIcon.png new file mode 100644 index 0000000..ce39cb5 Binary files /dev/null and b/Client2016/content/textures/ui/RobuxIcon.png differ diff --git a/Client2016/content/textures/ui/ScaleButton.png b/Client2016/content/textures/ui/ScaleButton.png new file mode 100644 index 0000000..05ab151 Binary files /dev/null and b/Client2016/content/textures/ui/ScaleButton.png differ diff --git a/Client2016/content/textures/ui/ScaleButton_dn.png b/Client2016/content/textures/ui/ScaleButton_dn.png new file mode 100644 index 0000000..c5010fa Binary files /dev/null and b/Client2016/content/textures/ui/ScaleButton_dn.png differ diff --git a/Client2016/content/textures/ui/Scroll/scroll-bottom.png b/Client2016/content/textures/ui/Scroll/scroll-bottom.png new file mode 100644 index 0000000..74b6f8e Binary files /dev/null and b/Client2016/content/textures/ui/Scroll/scroll-bottom.png differ diff --git a/Client2016/content/textures/ui/Scroll/scroll-bottom@2x.png b/Client2016/content/textures/ui/Scroll/scroll-bottom@2x.png new file mode 100644 index 0000000..351081f Binary files /dev/null and b/Client2016/content/textures/ui/Scroll/scroll-bottom@2x.png differ diff --git a/Client2016/content/textures/ui/Scroll/scroll-middle.png b/Client2016/content/textures/ui/Scroll/scroll-middle.png new file mode 100644 index 0000000..0702f1e Binary files /dev/null and b/Client2016/content/textures/ui/Scroll/scroll-middle.png differ diff --git a/Client2016/content/textures/ui/Scroll/scroll-middle@2x.png b/Client2016/content/textures/ui/Scroll/scroll-middle@2x.png new file mode 100644 index 0000000..86ffc6f Binary files /dev/null and b/Client2016/content/textures/ui/Scroll/scroll-middle@2x.png differ diff --git a/Client2016/content/textures/ui/Scroll/scroll-top.png b/Client2016/content/textures/ui/Scroll/scroll-top.png new file mode 100644 index 0000000..dd426d1 Binary files /dev/null and b/Client2016/content/textures/ui/Scroll/scroll-top.png differ diff --git a/Client2016/content/textures/ui/Scroll/scroll-top@2x.png b/Client2016/content/textures/ui/Scroll/scroll-top@2x.png new file mode 100644 index 0000000..29f9243 Binary files /dev/null and b/Client2016/content/textures/ui/Scroll/scroll-top@2x.png differ diff --git a/Client2016/content/textures/ui/SearchIcon.png b/Client2016/content/textures/ui/SearchIcon.png new file mode 100644 index 0000000..d72741c Binary files /dev/null and b/Client2016/content/textures/ui/SearchIcon.png differ diff --git a/Client2016/content/textures/ui/SelectionBox.png b/Client2016/content/textures/ui/SelectionBox.png new file mode 100644 index 0000000..ab76dee Binary files /dev/null and b/Client2016/content/textures/ui/SelectionBox.png differ diff --git a/Client2016/content/textures/ui/SelectionBox@2x.png b/Client2016/content/textures/ui/SelectionBox@2x.png new file mode 100644 index 0000000..54621f5 Binary files /dev/null and b/Client2016/content/textures/ui/SelectionBox@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/DropDown/DropDown.png b/Client2016/content/textures/ui/Settings/DropDown/DropDown.png new file mode 100644 index 0000000..6088030 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/DropDown/DropDown.png differ diff --git a/Client2016/content/textures/ui/Settings/DropDown/DropDown@2x.png b/Client2016/content/textures/ui/Settings/DropDown/DropDown@2x.png new file mode 100644 index 0000000..0d82f2b Binary files /dev/null and b/Client2016/content/textures/ui/Settings/DropDown/DropDown@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/AButtonDark.png b/Client2016/content/textures/ui/Settings/Help/AButtonDark.png new file mode 100644 index 0000000..5b761d0 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/AButtonDark.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/AButtonDark@2x.png b/Client2016/content/textures/ui/Settings/Help/AButtonDark@2x.png new file mode 100644 index 0000000..b913a96 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/AButtonDark@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/AButtonLight.png b/Client2016/content/textures/ui/Settings/Help/AButtonLight.png new file mode 100644 index 0000000..344af4c Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/AButtonLight.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/AButtonLight@2x.png b/Client2016/content/textures/ui/Settings/Help/AButtonLight@2x.png new file mode 100644 index 0000000..436be8e Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/AButtonLight@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/AButtonLightSmall.png b/Client2016/content/textures/ui/Settings/Help/AButtonLightSmall.png new file mode 100644 index 0000000..5f95aa4 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/AButtonLightSmall.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/BButtonDark.png b/Client2016/content/textures/ui/Settings/Help/BButtonDark.png new file mode 100644 index 0000000..f00a6db Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/BButtonDark.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/BButtonDark@2x.png b/Client2016/content/textures/ui/Settings/Help/BButtonDark@2x.png new file mode 100644 index 0000000..22d6007 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/BButtonDark@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/BButtonLight.png b/Client2016/content/textures/ui/Settings/Help/BButtonLight.png new file mode 100644 index 0000000..6314785 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/BButtonLight.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/BButtonLight@2x.png b/Client2016/content/textures/ui/Settings/Help/BButtonLight@2x.png new file mode 100644 index 0000000..46737cf Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/BButtonLight@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/EscapeIcon.png b/Client2016/content/textures/ui/Settings/Help/EscapeIcon.png new file mode 100644 index 0000000..e49636e Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/EscapeIcon.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/GenericController.png b/Client2016/content/textures/ui/Settings/Help/GenericController.png new file mode 100644 index 0000000..c92f1e1 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/GenericController.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/GenericController@2x.png b/Client2016/content/textures/ui/Settings/Help/GenericController@2x.png new file mode 100644 index 0000000..3b82e31 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/GenericController@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/LeaveIcon.png b/Client2016/content/textures/ui/Settings/Help/LeaveIcon.png new file mode 100644 index 0000000..cbe8d88 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/LeaveIcon.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/ResetIcon.png b/Client2016/content/textures/ui/Settings/Help/ResetIcon.png new file mode 100644 index 0000000..64fedc8 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/ResetIcon.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/RotateCameraGesture.png b/Client2016/content/textures/ui/Settings/Help/RotateCameraGesture.png new file mode 100644 index 0000000..53fd54e Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/RotateCameraGesture.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/UseToolGesture.png b/Client2016/content/textures/ui/Settings/Help/UseToolGesture.png new file mode 100644 index 0000000..1e4124b Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/UseToolGesture.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/XButtonDark.png b/Client2016/content/textures/ui/Settings/Help/XButtonDark.png new file mode 100644 index 0000000..83e45cd Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/XButtonDark.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/XButtonDark@2x.png b/Client2016/content/textures/ui/Settings/Help/XButtonDark@2x.png new file mode 100644 index 0000000..cd15ff2 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/XButtonDark@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/XButtonLight.png b/Client2016/content/textures/ui/Settings/Help/XButtonLight.png new file mode 100644 index 0000000..fd294c8 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/XButtonLight.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/XButtonLight@2x.png b/Client2016/content/textures/ui/Settings/Help/XButtonLight@2x.png new file mode 100644 index 0000000..efffae4 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/XButtonLight@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/XboxController.png b/Client2016/content/textures/ui/Settings/Help/XboxController.png new file mode 100644 index 0000000..ae478c4 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/XboxController.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/YButtonDark.png b/Client2016/content/textures/ui/Settings/Help/YButtonDark.png new file mode 100644 index 0000000..46d4284 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/YButtonDark.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/YButtonDark@2x.png b/Client2016/content/textures/ui/Settings/Help/YButtonDark@2x.png new file mode 100644 index 0000000..81aaf13 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/YButtonDark@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/YButtonLight.png b/Client2016/content/textures/ui/Settings/Help/YButtonLight.png new file mode 100644 index 0000000..b8130fb Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/YButtonLight.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/YButtonLight@2x.png b/Client2016/content/textures/ui/Settings/Help/YButtonLight@2x.png new file mode 100644 index 0000000..b19c89e Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/YButtonLight@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Help/ZoomGesture.png b/Client2016/content/textures/ui/Settings/Help/ZoomGesture.png new file mode 100644 index 0000000..23a8f62 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Help/ZoomGesture.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuBackground.png b/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuBackground.png new file mode 100644 index 0000000..825c316 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuBackground.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuButton.png b/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuButton.png new file mode 100644 index 0000000..ca1fb48 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuButton.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuButton@2x.png b/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuButton@2x.png new file mode 100644 index 0000000..1406ca9 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuButton@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png b/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png new file mode 100644 index 0000000..298a952 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected@2x.png b/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected@2x.png new file mode 100644 index 0000000..4e25b24 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuSelection.png b/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuSelection.png new file mode 100644 index 0000000..0e79bfe Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuSelection.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuSelection@2x.png b/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuSelection@2x.png new file mode 100644 index 0000000..da1a3a3 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarAssets/MenuSelection@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab.png b/Client2016/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab.png new file mode 100644 index 0000000..81df477 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab@2x.png b/Client2016/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab@2x.png new file mode 100644 index 0000000..8fa963f Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarIcons/HelpTab.png b/Client2016/content/textures/ui/Settings/MenuBarIcons/HelpTab.png new file mode 100644 index 0000000..2658eea Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarIcons/HelpTab.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarIcons/HelpTab@2x.png b/Client2016/content/textures/ui/Settings/MenuBarIcons/HelpTab@2x.png new file mode 100644 index 0000000..2910461 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarIcons/HelpTab@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarIcons/HomeTab.png b/Client2016/content/textures/ui/Settings/MenuBarIcons/HomeTab.png new file mode 100644 index 0000000..f05200f Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarIcons/HomeTab.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarIcons/HomeTab@2x.png b/Client2016/content/textures/ui/Settings/MenuBarIcons/HomeTab@2x.png new file mode 100644 index 0000000..7fe7075 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarIcons/HomeTab@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon.png b/Client2016/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon.png new file mode 100644 index 0000000..9405151 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon@2x.png b/Client2016/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon@2x.png new file mode 100644 index 0000000..1e32cfd Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarIcons/RecordTab.png b/Client2016/content/textures/ui/Settings/MenuBarIcons/RecordTab.png new file mode 100644 index 0000000..4020a78 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarIcons/RecordTab.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarIcons/RecordTab@2x.png b/Client2016/content/textures/ui/Settings/MenuBarIcons/RecordTab@2x.png new file mode 100644 index 0000000..63a7411 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarIcons/RecordTab@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab.png b/Client2016/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab.png new file mode 100644 index 0000000..bddb28b Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab.png differ diff --git a/Client2016/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab@2x.png b/Client2016/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab@2x.png new file mode 100644 index 0000000..abb6674 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/Alert.png b/Client2016/content/textures/ui/Settings/Radial/Alert.png new file mode 100644 index 0000000..4165e5c Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/Alert.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/Alert@2x.png b/Client2016/content/textures/ui/Settings/Radial/Alert@2x.png new file mode 100644 index 0000000..c4fd032 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/Alert@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/Backpack.png b/Client2016/content/textures/ui/Settings/Radial/Backpack.png new file mode 100644 index 0000000..fe7ed08 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/Backpack.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/Backpack@2x.png b/Client2016/content/textures/ui/Settings/Radial/Backpack@2x.png new file mode 100644 index 0000000..70aa912 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/Backpack@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/Bottom.png b/Client2016/content/textures/ui/Settings/Radial/Bottom.png new file mode 100644 index 0000000..deb9a5d Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/Bottom.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/BottomLeft.png b/Client2016/content/textures/ui/Settings/Radial/BottomLeft.png new file mode 100644 index 0000000..9606a32 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/BottomLeft.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/BottomLeftSelected.png b/Client2016/content/textures/ui/Settings/Radial/BottomLeftSelected.png new file mode 100644 index 0000000..c695162 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/BottomLeftSelected.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/BottomRight.png b/Client2016/content/textures/ui/Settings/Radial/BottomRight.png new file mode 100644 index 0000000..470101a Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/BottomRight.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/BottomRightSelected.png b/Client2016/content/textures/ui/Settings/Radial/BottomRightSelected.png new file mode 100644 index 0000000..9752011 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/BottomRightSelected.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/BottomSelected.png b/Client2016/content/textures/ui/Settings/Radial/BottomSelected.png new file mode 100644 index 0000000..8503642 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/BottomSelected.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/Chat.png b/Client2016/content/textures/ui/Settings/Radial/Chat.png new file mode 100644 index 0000000..de382f9 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/Chat.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/Chat@2x.png b/Client2016/content/textures/ui/Settings/Radial/Chat@2x.png new file mode 100644 index 0000000..7d2f835 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/Chat@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/EmptyBottom.png b/Client2016/content/textures/ui/Settings/Radial/EmptyBottom.png new file mode 100644 index 0000000..65bfc6a Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/EmptyBottom.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/EmptyBottomLeft.png b/Client2016/content/textures/ui/Settings/Radial/EmptyBottomLeft.png new file mode 100644 index 0000000..6944d53 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/EmptyBottomLeft.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/EmptyBottomRight.png b/Client2016/content/textures/ui/Settings/Radial/EmptyBottomRight.png new file mode 100644 index 0000000..6ec4168 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/EmptyBottomRight.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/EmptyTop.png b/Client2016/content/textures/ui/Settings/Radial/EmptyTop.png new file mode 100644 index 0000000..bd0bdda Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/EmptyTop.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/EmptyTopLeft.png b/Client2016/content/textures/ui/Settings/Radial/EmptyTopLeft.png new file mode 100644 index 0000000..fcb0312 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/EmptyTopLeft.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/EmptyTopRight.png b/Client2016/content/textures/ui/Settings/Radial/EmptyTopRight.png new file mode 100644 index 0000000..5b17865 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/EmptyTopRight.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/Leave.png b/Client2016/content/textures/ui/Settings/Radial/Leave.png new file mode 100644 index 0000000..90ee842 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/Leave.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/Leave@2x.png b/Client2016/content/textures/ui/Settings/Radial/Leave@2x.png new file mode 100644 index 0000000..e9d11ad Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/Leave@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/Menu.png b/Client2016/content/textures/ui/Settings/Radial/Menu.png new file mode 100644 index 0000000..c414691 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/Menu.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/Menu@2x.png b/Client2016/content/textures/ui/Settings/Radial/Menu@2x.png new file mode 100644 index 0000000..5c65bd0 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/Menu@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/PlayerList.png b/Client2016/content/textures/ui/Settings/Radial/PlayerList.png new file mode 100644 index 0000000..b8da356 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/PlayerList.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/PlayerList@2x.png b/Client2016/content/textures/ui/Settings/Radial/PlayerList@2x.png new file mode 100644 index 0000000..3ca6d5f Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/PlayerList@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/RadialLabel.png b/Client2016/content/textures/ui/Settings/Radial/RadialLabel.png new file mode 100644 index 0000000..420552f Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/RadialLabel.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/RadialLabel@2x.png b/Client2016/content/textures/ui/Settings/Radial/RadialLabel@2x.png new file mode 100644 index 0000000..a75f44d Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/RadialLabel@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/Top.png b/Client2016/content/textures/ui/Settings/Radial/Top.png new file mode 100644 index 0000000..e3cc6eb Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/Top.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/TopLeft.png b/Client2016/content/textures/ui/Settings/Radial/TopLeft.png new file mode 100644 index 0000000..b58730f Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/TopLeft.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/TopLeftSelected.png b/Client2016/content/textures/ui/Settings/Radial/TopLeftSelected.png new file mode 100644 index 0000000..03e85ff Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/TopLeftSelected.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/TopRight.png b/Client2016/content/textures/ui/Settings/Radial/TopRight.png new file mode 100644 index 0000000..120dcbf Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/TopRight.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/TopRightSelected.png b/Client2016/content/textures/ui/Settings/Radial/TopRightSelected.png new file mode 100644 index 0000000..2b447f0 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/TopRightSelected.png differ diff --git a/Client2016/content/textures/ui/Settings/Radial/TopSelected.png b/Client2016/content/textures/ui/Settings/Radial/TopSelected.png new file mode 100644 index 0000000..6e94eed Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Radial/TopSelected.png differ diff --git a/Client2016/content/textures/ui/Settings/Slider/BarLeft.png b/Client2016/content/textures/ui/Settings/Slider/BarLeft.png new file mode 100644 index 0000000..7c77f60 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Slider/BarLeft.png differ diff --git a/Client2016/content/textures/ui/Settings/Slider/BarLeft@2x.png b/Client2016/content/textures/ui/Settings/Slider/BarLeft@2x.png new file mode 100644 index 0000000..f116856 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Slider/BarLeft@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Slider/BarRight.png b/Client2016/content/textures/ui/Settings/Slider/BarRight.png new file mode 100644 index 0000000..52256d6 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Slider/BarRight.png differ diff --git a/Client2016/content/textures/ui/Settings/Slider/BarRight@2x.png b/Client2016/content/textures/ui/Settings/Slider/BarRight@2x.png new file mode 100644 index 0000000..41bfe69 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Slider/BarRight@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Slider/Left.png b/Client2016/content/textures/ui/Settings/Slider/Left.png new file mode 100644 index 0000000..a742728 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Slider/Left.png differ diff --git a/Client2016/content/textures/ui/Settings/Slider/Left@2x.png b/Client2016/content/textures/ui/Settings/Slider/Left@2x.png new file mode 100644 index 0000000..582699d Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Slider/Left@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Slider/Right.png b/Client2016/content/textures/ui/Settings/Slider/Right.png new file mode 100644 index 0000000..0d47c0a Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Slider/Right.png differ diff --git a/Client2016/content/textures/ui/Settings/Slider/Right@2x.png b/Client2016/content/textures/ui/Settings/Slider/Right@2x.png new file mode 100644 index 0000000..fb86118 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Slider/Right@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Slider/SelectedBarLeft.png b/Client2016/content/textures/ui/Settings/Slider/SelectedBarLeft.png new file mode 100644 index 0000000..42cbddf Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Slider/SelectedBarLeft.png differ diff --git a/Client2016/content/textures/ui/Settings/Slider/SelectedBarLeft@2x.png b/Client2016/content/textures/ui/Settings/Slider/SelectedBarLeft@2x.png new file mode 100644 index 0000000..8c97cda Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Slider/SelectedBarLeft@2x.png differ diff --git a/Client2016/content/textures/ui/Settings/Slider/SelectedBarRight.png b/Client2016/content/textures/ui/Settings/Slider/SelectedBarRight.png new file mode 100644 index 0000000..c1b3f78 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Slider/SelectedBarRight.png differ diff --git a/Client2016/content/textures/ui/Settings/Slider/SelectedBarRight@2x.png b/Client2016/content/textures/ui/Settings/Slider/SelectedBarRight@2x.png new file mode 100644 index 0000000..56010f0 Binary files /dev/null and b/Client2016/content/textures/ui/Settings/Slider/SelectedBarRight@2x.png differ diff --git a/Client2016/content/textures/ui/SettingsButton.png b/Client2016/content/textures/ui/SettingsButton.png new file mode 100644 index 0000000..196cf23 Binary files /dev/null and b/Client2016/content/textures/ui/SettingsButton.png differ diff --git a/Client2016/content/textures/ui/SettingsButton_dn.png b/Client2016/content/textures/ui/SettingsButton_dn.png new file mode 100644 index 0000000..e6f4799 Binary files /dev/null and b/Client2016/content/textures/ui/SettingsButton_dn.png differ diff --git a/Client2016/content/textures/ui/SettingsButton_ds.png b/Client2016/content/textures/ui/SettingsButton_ds.png new file mode 100644 index 0000000..2aca460 Binary files /dev/null and b/Client2016/content/textures/ui/SettingsButton_ds.png differ diff --git a/Client2016/content/textures/ui/SettingsButton_ovr.png b/Client2016/content/textures/ui/SettingsButton_ovr.png new file mode 100644 index 0000000..e6f4799 Binary files /dev/null and b/Client2016/content/textures/ui/SettingsButton_ovr.png differ diff --git a/Client2016/content/textures/ui/SingleButton.png b/Client2016/content/textures/ui/SingleButton.png new file mode 100644 index 0000000..6bf0fa0 Binary files /dev/null and b/Client2016/content/textures/ui/SingleButton.png differ diff --git a/Client2016/content/textures/ui/SingleButtonDown.png b/Client2016/content/textures/ui/SingleButtonDown.png new file mode 100644 index 0000000..b72335b Binary files /dev/null and b/Client2016/content/textures/ui/SingleButtonDown.png differ diff --git a/Client2016/content/textures/ui/Slate.png b/Client2016/content/textures/ui/Slate.png new file mode 100644 index 0000000..4f90fb8 Binary files /dev/null and b/Client2016/content/textures/ui/Slate.png differ diff --git a/Client2016/content/textures/ui/Slider-BKG-Center.png b/Client2016/content/textures/ui/Slider-BKG-Center.png new file mode 100644 index 0000000..34f9c13 Binary files /dev/null and b/Client2016/content/textures/ui/Slider-BKG-Center.png differ diff --git a/Client2016/content/textures/ui/Slider-BKG-Center@2x.png b/Client2016/content/textures/ui/Slider-BKG-Center@2x.png new file mode 100644 index 0000000..4c4e650 Binary files /dev/null and b/Client2016/content/textures/ui/Slider-BKG-Center@2x.png differ diff --git a/Client2016/content/textures/ui/Slider-BKG-Left-Cap.png b/Client2016/content/textures/ui/Slider-BKG-Left-Cap.png new file mode 100644 index 0000000..ae0ba4e Binary files /dev/null and b/Client2016/content/textures/ui/Slider-BKG-Left-Cap.png differ diff --git a/Client2016/content/textures/ui/Slider-BKG-Left-Cap@2x.png b/Client2016/content/textures/ui/Slider-BKG-Left-Cap@2x.png new file mode 100644 index 0000000..e97552f Binary files /dev/null and b/Client2016/content/textures/ui/Slider-BKG-Left-Cap@2x.png differ diff --git a/Client2016/content/textures/ui/Slider-BKG-Right-Cap.png b/Client2016/content/textures/ui/Slider-BKG-Right-Cap.png new file mode 100644 index 0000000..19ef545 Binary files /dev/null and b/Client2016/content/textures/ui/Slider-BKG-Right-Cap.png differ diff --git a/Client2016/content/textures/ui/Slider-BKG-Right-Cap@2x.png b/Client2016/content/textures/ui/Slider-BKG-Right-Cap@2x.png new file mode 100644 index 0000000..614b0f4 Binary files /dev/null and b/Client2016/content/textures/ui/Slider-BKG-Right-Cap@2x.png differ diff --git a/Client2016/content/textures/ui/Slider-Fill-Center.png b/Client2016/content/textures/ui/Slider-Fill-Center.png new file mode 100644 index 0000000..6f45d7a Binary files /dev/null and b/Client2016/content/textures/ui/Slider-Fill-Center.png differ diff --git a/Client2016/content/textures/ui/Slider-Fill-Center@2x.png b/Client2016/content/textures/ui/Slider-Fill-Center@2x.png new file mode 100644 index 0000000..9bf9420 Binary files /dev/null and b/Client2016/content/textures/ui/Slider-Fill-Center@2x.png differ diff --git a/Client2016/content/textures/ui/Slider-Fill-Left-Cap.png b/Client2016/content/textures/ui/Slider-Fill-Left-Cap.png new file mode 100644 index 0000000..1c1ccdc Binary files /dev/null and b/Client2016/content/textures/ui/Slider-Fill-Left-Cap.png differ diff --git a/Client2016/content/textures/ui/Slider-Fill-Left-Cap@2x.png b/Client2016/content/textures/ui/Slider-Fill-Left-Cap@2x.png new file mode 100644 index 0000000..cfd7689 Binary files /dev/null and b/Client2016/content/textures/ui/Slider-Fill-Left-Cap@2x.png differ diff --git a/Client2016/content/textures/ui/Slider-Fill-Right-Cap.png b/Client2016/content/textures/ui/Slider-Fill-Right-Cap.png new file mode 100644 index 0000000..7e74df5 Binary files /dev/null and b/Client2016/content/textures/ui/Slider-Fill-Right-Cap.png differ diff --git a/Client2016/content/textures/ui/Slider-Fill-Right-Cap@2x.png b/Client2016/content/textures/ui/Slider-Fill-Right-Cap@2x.png new file mode 100644 index 0000000..31b7fc1 Binary files /dev/null and b/Client2016/content/textures/ui/Slider-Fill-Right-Cap@2x.png differ diff --git a/Client2016/content/textures/ui/Slider.png b/Client2016/content/textures/ui/Slider.png new file mode 100644 index 0000000..63c4dea Binary files /dev/null and b/Client2016/content/textures/ui/Slider.png differ diff --git a/Client2016/content/textures/ui/Slider_dn.png b/Client2016/content/textures/ui/Slider_dn.png new file mode 100644 index 0000000..d98cb85 Binary files /dev/null and b/Client2016/content/textures/ui/Slider_dn.png differ diff --git a/Client2016/content/textures/ui/Slider_sel.png b/Client2016/content/textures/ui/Slider_sel.png new file mode 100644 index 0000000..d98cb85 Binary files /dev/null and b/Client2016/content/textures/ui/Slider_sel.png differ diff --git a/Client2016/content/textures/ui/Smooth.png b/Client2016/content/textures/ui/Smooth.png new file mode 100644 index 0000000..e4d6c4d Binary files /dev/null and b/Client2016/content/textures/ui/Smooth.png differ diff --git a/Client2016/content/textures/ui/StampToolButton.png b/Client2016/content/textures/ui/StampToolButton.png new file mode 100644 index 0000000..34e7994 Binary files /dev/null and b/Client2016/content/textures/ui/StampToolButton.png differ diff --git a/Client2016/content/textures/ui/StampToolButton_dn.png b/Client2016/content/textures/ui/StampToolButton_dn.png new file mode 100644 index 0000000..affebf4 Binary files /dev/null and b/Client2016/content/textures/ui/StampToolButton_dn.png differ diff --git a/Client2016/content/textures/ui/Studs.png b/Client2016/content/textures/ui/Studs.png new file mode 100644 index 0000000..194a02b Binary files /dev/null and b/Client2016/content/textures/ui/Studs.png differ diff --git a/Client2016/content/textures/ui/SurfaceButton.png b/Client2016/content/textures/ui/SurfaceButton.png new file mode 100644 index 0000000..78c90f0 Binary files /dev/null and b/Client2016/content/textures/ui/SurfaceButton.png differ diff --git a/Client2016/content/textures/ui/SurfaceButton_dn.png b/Client2016/content/textures/ui/SurfaceButton_dn.png new file mode 100644 index 0000000..850e099 Binary files /dev/null and b/Client2016/content/textures/ui/SurfaceButton_dn.png differ diff --git a/Client2016/content/textures/ui/SurfaceMenu.png b/Client2016/content/textures/ui/SurfaceMenu.png new file mode 100644 index 0000000..1582479 Binary files /dev/null and b/Client2016/content/textures/ui/SurfaceMenu.png differ diff --git a/Client2016/content/textures/ui/TinyBcIcon.png b/Client2016/content/textures/ui/TinyBcIcon.png new file mode 100644 index 0000000..1e2c7ef Binary files /dev/null and b/Client2016/content/textures/ui/TinyBcIcon.png differ diff --git a/Client2016/content/textures/ui/TinyObcIcon.png b/Client2016/content/textures/ui/TinyObcIcon.png new file mode 100644 index 0000000..f12104d Binary files /dev/null and b/Client2016/content/textures/ui/TinyObcIcon.png differ diff --git a/Client2016/content/textures/ui/TinyTbcIcon.png b/Client2016/content/textures/ui/TinyTbcIcon.png new file mode 100644 index 0000000..d5c37fe Binary files /dev/null and b/Client2016/content/textures/ui/TinyTbcIcon.png differ diff --git a/Client2016/content/textures/ui/TixIcon.png b/Client2016/content/textures/ui/TixIcon.png new file mode 100644 index 0000000..852d976 Binary files /dev/null and b/Client2016/content/textures/ui/TixIcon.png differ diff --git a/Client2016/content/textures/ui/ToggleFullScreen_ds.png b/Client2016/content/textures/ui/ToggleFullScreen_ds.png new file mode 100644 index 0000000..07d7de0 Binary files /dev/null and b/Client2016/content/textures/ui/ToggleFullScreen_ds.png differ diff --git a/Client2016/content/textures/ui/ToolButton.png b/Client2016/content/textures/ui/ToolButton.png new file mode 100644 index 0000000..ca9a395 Binary files /dev/null and b/Client2016/content/textures/ui/ToolButton.png differ diff --git a/Client2016/content/textures/ui/ToolButton_dn.png b/Client2016/content/textures/ui/ToolButton_dn.png new file mode 100644 index 0000000..e53c826 Binary files /dev/null and b/Client2016/content/textures/ui/ToolButton_dn.png differ diff --git a/Client2016/content/textures/ui/ToolButton_ds.png b/Client2016/content/textures/ui/ToolButton_ds.png new file mode 100644 index 0000000..016ae9c Binary files /dev/null and b/Client2016/content/textures/ui/ToolButton_ds.png differ diff --git a/Client2016/content/textures/ui/TopBar/dropshadow.png b/Client2016/content/textures/ui/TopBar/dropshadow.png new file mode 100644 index 0000000..8147254 Binary files /dev/null and b/Client2016/content/textures/ui/TopBar/dropshadow.png differ diff --git a/Client2016/content/textures/ui/TopBar/dropshadow@2x.png b/Client2016/content/textures/ui/TopBar/dropshadow@2x.png new file mode 100644 index 0000000..5b97175 Binary files /dev/null and b/Client2016/content/textures/ui/TopBar/dropshadow@2x.png differ diff --git a/Client2016/content/textures/ui/TouchControlsSheet.png b/Client2016/content/textures/ui/TouchControlsSheet.png new file mode 100644 index 0000000..d3b740e Binary files /dev/null and b/Client2016/content/textures/ui/TouchControlsSheet.png differ diff --git a/Client2016/content/textures/ui/Universal.png b/Client2016/content/textures/ui/Universal.png new file mode 100644 index 0000000..508fac8 Binary files /dev/null and b/Client2016/content/textures/ui/Universal.png differ diff --git a/Client2016/content/textures/ui/Vehicle/SpeedBar.png b/Client2016/content/textures/ui/Vehicle/SpeedBar.png new file mode 100644 index 0000000..0bd0fb6 Binary files /dev/null and b/Client2016/content/textures/ui/Vehicle/SpeedBar.png differ diff --git a/Client2016/content/textures/ui/Vehicle/SpeedBar@2x.png b/Client2016/content/textures/ui/Vehicle/SpeedBar@2x.png new file mode 100644 index 0000000..403883b Binary files /dev/null and b/Client2016/content/textures/ui/Vehicle/SpeedBar@2x.png differ diff --git a/Client2016/content/textures/ui/Vehicle/SpeedBarBKG.png b/Client2016/content/textures/ui/Vehicle/SpeedBarBKG.png new file mode 100644 index 0000000..67cf7dd Binary files /dev/null and b/Client2016/content/textures/ui/Vehicle/SpeedBarBKG.png differ diff --git a/Client2016/content/textures/ui/Vehicle/SpeedBarBKG@2x.png b/Client2016/content/textures/ui/Vehicle/SpeedBarBKG@2x.png new file mode 100644 index 0000000..93f3e60 Binary files /dev/null and b/Client2016/content/textures/ui/Vehicle/SpeedBarBKG@2x.png differ diff --git a/Client2016/content/textures/ui/Vehicle/SpeedBarEmpty.png b/Client2016/content/textures/ui/Vehicle/SpeedBarEmpty.png new file mode 100644 index 0000000..23cef24 Binary files /dev/null and b/Client2016/content/textures/ui/Vehicle/SpeedBarEmpty.png differ diff --git a/Client2016/content/textures/ui/Vehicle/SpeedBarEmpty@2x.png b/Client2016/content/textures/ui/Vehicle/SpeedBarEmpty@2x.png new file mode 100644 index 0000000..fe7f6d6 Binary files /dev/null and b/Client2016/content/textures/ui/Vehicle/SpeedBarEmpty@2x.png differ diff --git a/Client2016/content/textures/ui/Weld.png b/Client2016/content/textures/ui/Weld.png new file mode 100644 index 0000000..d45375b Binary files /dev/null and b/Client2016/content/textures/ui/Weld.png differ diff --git a/Client2016/content/textures/ui/Wood.png b/Client2016/content/textures/ui/Wood.png new file mode 100644 index 0000000..93898de Binary files /dev/null and b/Client2016/content/textures/ui/Wood.png differ diff --git a/Client2016/content/textures/ui/btn_grey.png b/Client2016/content/textures/ui/btn_grey.png new file mode 100644 index 0000000..5e46696 Binary files /dev/null and b/Client2016/content/textures/ui/btn_grey.png differ diff --git a/Client2016/content/textures/ui/btn_greyTransp.png b/Client2016/content/textures/ui/btn_greyTransp.png new file mode 100644 index 0000000..df5989b Binary files /dev/null and b/Client2016/content/textures/ui/btn_greyTransp.png differ diff --git a/Client2016/content/textures/ui/btn_newBlue.png b/Client2016/content/textures/ui/btn_newBlue.png new file mode 100644 index 0000000..5a08d4b Binary files /dev/null and b/Client2016/content/textures/ui/btn_newBlue.png differ diff --git a/Client2016/content/textures/ui/btn_newBlue@2x.png b/Client2016/content/textures/ui/btn_newBlue@2x.png new file mode 100644 index 0000000..d20d595 Binary files /dev/null and b/Client2016/content/textures/ui/btn_newBlue@2x.png differ diff --git a/Client2016/content/textures/ui/btn_newBlueGlow.png b/Client2016/content/textures/ui/btn_newBlueGlow.png new file mode 100644 index 0000000..9376645 Binary files /dev/null and b/Client2016/content/textures/ui/btn_newBlueGlow.png differ diff --git a/Client2016/content/textures/ui/btn_newBlueGlow@2x.png b/Client2016/content/textures/ui/btn_newBlueGlow@2x.png new file mode 100644 index 0000000..b559d35 Binary files /dev/null and b/Client2016/content/textures/ui/btn_newBlueGlow@2x.png differ diff --git a/Client2016/content/textures/ui/btn_newGrey.png b/Client2016/content/textures/ui/btn_newGrey.png new file mode 100644 index 0000000..3868d8c Binary files /dev/null and b/Client2016/content/textures/ui/btn_newGrey.png differ diff --git a/Client2016/content/textures/ui/btn_newGrey@2x.png b/Client2016/content/textures/ui/btn_newGrey@2x.png new file mode 100644 index 0000000..967c988 Binary files /dev/null and b/Client2016/content/textures/ui/btn_newGrey@2x.png differ diff --git a/Client2016/content/textures/ui/btn_newGreyGlow.png b/Client2016/content/textures/ui/btn_newGreyGlow.png new file mode 100644 index 0000000..6782b53 Binary files /dev/null and b/Client2016/content/textures/ui/btn_newGreyGlow.png differ diff --git a/Client2016/content/textures/ui/btn_newGreyGlow@2x.png b/Client2016/content/textures/ui/btn_newGreyGlow@2x.png new file mode 100644 index 0000000..aba488a Binary files /dev/null and b/Client2016/content/textures/ui/btn_newGreyGlow@2x.png differ diff --git a/Client2016/content/textures/ui/btn_newWhite.png b/Client2016/content/textures/ui/btn_newWhite.png new file mode 100644 index 0000000..bd5efaf Binary files /dev/null and b/Client2016/content/textures/ui/btn_newWhite.png differ diff --git a/Client2016/content/textures/ui/btn_newWhite@2x.png b/Client2016/content/textures/ui/btn_newWhite@2x.png new file mode 100644 index 0000000..35c6494 Binary files /dev/null and b/Client2016/content/textures/ui/btn_newWhite@2x.png differ diff --git a/Client2016/content/textures/ui/btn_newWhiteGlow.png b/Client2016/content/textures/ui/btn_newWhiteGlow.png new file mode 100644 index 0000000..857274f Binary files /dev/null and b/Client2016/content/textures/ui/btn_newWhiteGlow.png differ diff --git a/Client2016/content/textures/ui/btn_newWhiteGlow@2x.png b/Client2016/content/textures/ui/btn_newWhiteGlow@2x.png new file mode 100644 index 0000000..f0dbb9d Binary files /dev/null and b/Client2016/content/textures/ui/btn_newWhiteGlow@2x.png differ diff --git a/Client2016/content/textures/ui/btn_red.png b/Client2016/content/textures/ui/btn_red.png new file mode 100644 index 0000000..17666e6 Binary files /dev/null and b/Client2016/content/textures/ui/btn_red.png differ diff --git a/Client2016/content/textures/ui/btn_redGlow.png b/Client2016/content/textures/ui/btn_redGlow.png new file mode 100644 index 0000000..5cf7081 Binary files /dev/null and b/Client2016/content/textures/ui/btn_redGlow.png differ diff --git a/Client2016/content/textures/ui/btn_white.png b/Client2016/content/textures/ui/btn_white.png new file mode 100644 index 0000000..d2a1cf2 Binary files /dev/null and b/Client2016/content/textures/ui/btn_white.png differ diff --git a/Client2016/content/textures/ui/chatBubble_blue_notify_bkg.png b/Client2016/content/textures/ui/chatBubble_blue_notify_bkg.png new file mode 100644 index 0000000..75871f9 Binary files /dev/null and b/Client2016/content/textures/ui/chatBubble_blue_notify_bkg.png differ diff --git a/Client2016/content/textures/ui/chatBubble_green_notify_bkg.png b/Client2016/content/textures/ui/chatBubble_green_notify_bkg.png new file mode 100644 index 0000000..5dd7ce5 Binary files /dev/null and b/Client2016/content/textures/ui/chatBubble_green_notify_bkg.png differ diff --git a/Client2016/content/textures/ui/chatBubble_red_notify_bkg.png b/Client2016/content/textures/ui/chatBubble_red_notify_bkg.png new file mode 100644 index 0000000..92ffa3b Binary files /dev/null and b/Client2016/content/textures/ui/chatBubble_red_notify_bkg.png differ diff --git a/Client2016/content/textures/ui/chatBubble_white_notify_bkg.png b/Client2016/content/textures/ui/chatBubble_white_notify_bkg.png new file mode 100644 index 0000000..45158ce Binary files /dev/null and b/Client2016/content/textures/ui/chatBubble_white_notify_bkg.png differ diff --git a/Client2016/content/textures/ui/chat_teamButton.png b/Client2016/content/textures/ui/chat_teamButton.png new file mode 100644 index 0000000..ad390aa Binary files /dev/null and b/Client2016/content/textures/ui/chat_teamButton.png differ diff --git a/Client2016/content/textures/ui/chat_teamButton@2x.png b/Client2016/content/textures/ui/chat_teamButton@2x.png new file mode 100644 index 0000000..b38e73f Binary files /dev/null and b/Client2016/content/textures/ui/chat_teamButton@2x.png differ diff --git a/Client2016/content/textures/ui/dialog_blue.png b/Client2016/content/textures/ui/dialog_blue.png new file mode 100644 index 0000000..734129b Binary files /dev/null and b/Client2016/content/textures/ui/dialog_blue.png differ diff --git a/Client2016/content/textures/ui/dialog_blue@2x.png b/Client2016/content/textures/ui/dialog_blue@2x.png new file mode 100644 index 0000000..7d17ee8 Binary files /dev/null and b/Client2016/content/textures/ui/dialog_blue@2x.png differ diff --git a/Client2016/content/textures/ui/dialog_ellipses.png b/Client2016/content/textures/ui/dialog_ellipses.png new file mode 100644 index 0000000..845c727 Binary files /dev/null and b/Client2016/content/textures/ui/dialog_ellipses.png differ diff --git a/Client2016/content/textures/ui/dialog_green.png b/Client2016/content/textures/ui/dialog_green.png new file mode 100644 index 0000000..b3b655c Binary files /dev/null and b/Client2016/content/textures/ui/dialog_green.png differ diff --git a/Client2016/content/textures/ui/dialog_green@2x.png b/Client2016/content/textures/ui/dialog_green@2x.png new file mode 100644 index 0000000..2daaacf Binary files /dev/null and b/Client2016/content/textures/ui/dialog_green@2x.png differ diff --git a/Client2016/content/textures/ui/dialog_purpose_help.png b/Client2016/content/textures/ui/dialog_purpose_help.png new file mode 100644 index 0000000..04c7c71 Binary files /dev/null and b/Client2016/content/textures/ui/dialog_purpose_help.png differ diff --git a/Client2016/content/textures/ui/dialog_purpose_quest.png b/Client2016/content/textures/ui/dialog_purpose_quest.png new file mode 100644 index 0000000..1fb351d Binary files /dev/null and b/Client2016/content/textures/ui/dialog_purpose_quest.png differ diff --git a/Client2016/content/textures/ui/dialog_purpose_shop.png b/Client2016/content/textures/ui/dialog_purpose_shop.png new file mode 100644 index 0000000..5e14018 Binary files /dev/null and b/Client2016/content/textures/ui/dialog_purpose_shop.png differ diff --git a/Client2016/content/textures/ui/dialog_red.png b/Client2016/content/textures/ui/dialog_red.png new file mode 100644 index 0000000..0cfce4d Binary files /dev/null and b/Client2016/content/textures/ui/dialog_red.png differ diff --git a/Client2016/content/textures/ui/dialog_red@2x.png b/Client2016/content/textures/ui/dialog_red@2x.png new file mode 100644 index 0000000..7c63527 Binary files /dev/null and b/Client2016/content/textures/ui/dialog_red@2x.png differ diff --git a/Client2016/content/textures/ui/dialog_tail.png b/Client2016/content/textures/ui/dialog_tail.png new file mode 100644 index 0000000..34e7d89 Binary files /dev/null and b/Client2016/content/textures/ui/dialog_tail.png differ diff --git a/Client2016/content/textures/ui/dialog_tail@2x.png b/Client2016/content/textures/ui/dialog_tail@2x.png new file mode 100644 index 0000000..461718b Binary files /dev/null and b/Client2016/content/textures/ui/dialog_tail@2x.png differ diff --git a/Client2016/content/textures/ui/dialog_white.png b/Client2016/content/textures/ui/dialog_white.png new file mode 100644 index 0000000..92cc922 Binary files /dev/null and b/Client2016/content/textures/ui/dialog_white.png differ diff --git a/Client2016/content/textures/ui/dialog_white@2x.png b/Client2016/content/textures/ui/dialog_white@2x.png new file mode 100644 index 0000000..3935a05 Binary files /dev/null and b/Client2016/content/textures/ui/dialog_white@2x.png differ diff --git a/Client2016/content/textures/ui/dropdown_arrow.png b/Client2016/content/textures/ui/dropdown_arrow.png new file mode 100644 index 0000000..e428280 Binary files /dev/null and b/Client2016/content/textures/ui/dropdown_arrow.png differ diff --git a/Client2016/content/textures/ui/dropdown_arrow@2x.png b/Client2016/content/textures/ui/dropdown_arrow@2x.png new file mode 100644 index 0000000..fec2bf9 Binary files /dev/null and b/Client2016/content/textures/ui/dropdown_arrow@2x.png differ diff --git a/Client2016/content/textures/ui/expandPlayerList.png b/Client2016/content/textures/ui/expandPlayerList.png new file mode 100644 index 0000000..beff417 Binary files /dev/null and b/Client2016/content/textures/ui/expandPlayerList.png differ diff --git a/Client2016/content/textures/ui/expandPlayerList@2x.png b/Client2016/content/textures/ui/expandPlayerList@2x.png new file mode 100644 index 0000000..6766d2a Binary files /dev/null and b/Client2016/content/textures/ui/expandPlayerList@2x.png differ diff --git a/Client2016/content/textures/ui/homeButton.png b/Client2016/content/textures/ui/homeButton.png new file mode 100644 index 0000000..0ad7744 Binary files /dev/null and b/Client2016/content/textures/ui/homeButton.png differ diff --git a/Client2016/content/textures/ui/homeButton@2x.png b/Client2016/content/textures/ui/homeButton@2x.png new file mode 100644 index 0000000..b0b5a0f Binary files /dev/null and b/Client2016/content/textures/ui/homeButton@2x.png differ diff --git a/Client2016/content/textures/ui/icon_BC-16.png b/Client2016/content/textures/ui/icon_BC-16.png new file mode 100644 index 0000000..57f0ce1 Binary files /dev/null and b/Client2016/content/textures/ui/icon_BC-16.png differ diff --git a/Client2016/content/textures/ui/icon_OBC-16.png b/Client2016/content/textures/ui/icon_OBC-16.png new file mode 100644 index 0000000..3d9b177 Binary files /dev/null and b/Client2016/content/textures/ui/icon_OBC-16.png differ diff --git a/Client2016/content/textures/ui/icon_TBC-16.png b/Client2016/content/textures/ui/icon_TBC-16.png new file mode 100644 index 0000000..ea803fe Binary files /dev/null and b/Client2016/content/textures/ui/icon_TBC-16.png differ diff --git a/Client2016/content/textures/ui/icon_follower-16.png b/Client2016/content/textures/ui/icon_follower-16.png new file mode 100644 index 0000000..f5c057a Binary files /dev/null and b/Client2016/content/textures/ui/icon_follower-16.png differ diff --git a/Client2016/content/textures/ui/icon_following-16.png b/Client2016/content/textures/ui/icon_following-16.png new file mode 100644 index 0000000..3810904 Binary files /dev/null and b/Client2016/content/textures/ui/icon_following-16.png differ diff --git a/Client2016/content/textures/ui/icon_friendrequestrecieved-16.png b/Client2016/content/textures/ui/icon_friendrequestrecieved-16.png new file mode 100644 index 0000000..2e45d38 Binary files /dev/null and b/Client2016/content/textures/ui/icon_friendrequestrecieved-16.png differ diff --git a/Client2016/content/textures/ui/icon_friendrequestsent_16.png b/Client2016/content/textures/ui/icon_friendrequestsent_16.png new file mode 100644 index 0000000..fa7c6bd Binary files /dev/null and b/Client2016/content/textures/ui/icon_friendrequestsent_16.png differ diff --git a/Client2016/content/textures/ui/icon_friends_16.png b/Client2016/content/textures/ui/icon_friends_16.png new file mode 100644 index 0000000..649067e Binary files /dev/null and b/Client2016/content/textures/ui/icon_friends_16.png differ diff --git a/Client2016/content/textures/ui/icon_mutualfollowing-16.png b/Client2016/content/textures/ui/icon_mutualfollowing-16.png new file mode 100644 index 0000000..e08ba52 Binary files /dev/null and b/Client2016/content/textures/ui/icon_mutualfollowing-16.png differ diff --git a/Client2016/content/textures/ui/icon_placeowner.png b/Client2016/content/textures/ui/icon_placeowner.png new file mode 100644 index 0000000..1feb020 Binary files /dev/null and b/Client2016/content/textures/ui/icon_placeowner.png differ diff --git a/Client2016/content/textures/ui/mouseLock_off.png b/Client2016/content/textures/ui/mouseLock_off.png new file mode 100644 index 0000000..27b1272 Binary files /dev/null and b/Client2016/content/textures/ui/mouseLock_off.png differ diff --git a/Client2016/content/textures/ui/mouseLock_off@2x.png b/Client2016/content/textures/ui/mouseLock_off@2x.png new file mode 100644 index 0000000..4fdd594 Binary files /dev/null and b/Client2016/content/textures/ui/mouseLock_off@2x.png differ diff --git a/Client2016/content/textures/ui/mouseLock_on.png b/Client2016/content/textures/ui/mouseLock_on.png new file mode 100644 index 0000000..250c42a Binary files /dev/null and b/Client2016/content/textures/ui/mouseLock_on.png differ diff --git a/Client2016/content/textures/ui/mouseLock_on@2x.png b/Client2016/content/textures/ui/mouseLock_on@2x.png new file mode 100644 index 0000000..9614f3e Binary files /dev/null and b/Client2016/content/textures/ui/mouseLock_on@2x.png differ diff --git a/Client2016/content/textures/ui/newBkg_square.png b/Client2016/content/textures/ui/newBkg_square.png new file mode 100644 index 0000000..923826c Binary files /dev/null and b/Client2016/content/textures/ui/newBkg_square.png differ diff --git a/Client2016/content/textures/ui/newBkg_square@2x.png b/Client2016/content/textures/ui/newBkg_square@2x.png new file mode 100644 index 0000000..ab0e64c Binary files /dev/null and b/Client2016/content/textures/ui/newBkg_square@2x.png differ diff --git a/Client2016/content/textures/ui/scroll-bottom.png b/Client2016/content/textures/ui/scroll-bottom.png new file mode 100644 index 0000000..2db02a8 Binary files /dev/null and b/Client2016/content/textures/ui/scroll-bottom.png differ diff --git a/Client2016/content/textures/ui/scroll-bottom@2x.png b/Client2016/content/textures/ui/scroll-bottom@2x.png new file mode 100644 index 0000000..713ade5 Binary files /dev/null and b/Client2016/content/textures/ui/scroll-bottom@2x.png differ diff --git a/Client2016/content/textures/ui/scroll-middle.png b/Client2016/content/textures/ui/scroll-middle.png new file mode 100644 index 0000000..7ef7495 Binary files /dev/null and b/Client2016/content/textures/ui/scroll-middle.png differ diff --git a/Client2016/content/textures/ui/scroll-middle@2x.png b/Client2016/content/textures/ui/scroll-middle@2x.png new file mode 100644 index 0000000..9162507 Binary files /dev/null and b/Client2016/content/textures/ui/scroll-middle@2x.png differ diff --git a/Client2016/content/textures/ui/scroll-top.png b/Client2016/content/textures/ui/scroll-top.png new file mode 100644 index 0000000..2bcfd00 Binary files /dev/null and b/Client2016/content/textures/ui/scroll-top.png differ diff --git a/Client2016/content/textures/ui/scroll-top@2x.png b/Client2016/content/textures/ui/scroll-top@2x.png new file mode 100644 index 0000000..50e5389 Binary files /dev/null and b/Client2016/content/textures/ui/scroll-top@2x.png differ diff --git a/Client2016/content/textures/ui/scrollbar.png b/Client2016/content/textures/ui/scrollbar.png new file mode 100644 index 0000000..9d7a4ff Binary files /dev/null and b/Client2016/content/textures/ui/scrollbar.png differ diff --git a/Client2016/content/textures/ui/scrollbuttonDown.png b/Client2016/content/textures/ui/scrollbuttonDown.png new file mode 100644 index 0000000..a3e6539 Binary files /dev/null and b/Client2016/content/textures/ui/scrollbuttonDown.png differ diff --git a/Client2016/content/textures/ui/scrollbuttonDown_dn.png b/Client2016/content/textures/ui/scrollbuttonDown_dn.png new file mode 100644 index 0000000..f7002ce Binary files /dev/null and b/Client2016/content/textures/ui/scrollbuttonDown_dn.png differ diff --git a/Client2016/content/textures/ui/scrollbuttonDown_ds.png b/Client2016/content/textures/ui/scrollbuttonDown_ds.png new file mode 100644 index 0000000..eac4eb9 Binary files /dev/null and b/Client2016/content/textures/ui/scrollbuttonDown_ds.png differ diff --git a/Client2016/content/textures/ui/scrollbuttonDown_ovr.png b/Client2016/content/textures/ui/scrollbuttonDown_ovr.png new file mode 100644 index 0000000..f7002ce Binary files /dev/null and b/Client2016/content/textures/ui/scrollbuttonDown_ovr.png differ diff --git a/Client2016/content/textures/ui/scrollbuttonUp.png b/Client2016/content/textures/ui/scrollbuttonUp.png new file mode 100644 index 0000000..5754f55 Binary files /dev/null and b/Client2016/content/textures/ui/scrollbuttonUp.png differ diff --git a/Client2016/content/textures/ui/scrollbuttonUp_dn.png b/Client2016/content/textures/ui/scrollbuttonUp_dn.png new file mode 100644 index 0000000..f12d690 Binary files /dev/null and b/Client2016/content/textures/ui/scrollbuttonUp_dn.png differ diff --git a/Client2016/content/textures/ui/scrollbuttonUp_ds.png b/Client2016/content/textures/ui/scrollbuttonUp_ds.png new file mode 100644 index 0000000..1e1b9db Binary files /dev/null and b/Client2016/content/textures/ui/scrollbuttonUp_ds.png differ diff --git a/Client2016/content/textures/ui/scrollbuttonUp_ovr.png b/Client2016/content/textures/ui/scrollbuttonUp_ovr.png new file mode 100644 index 0000000..f12d690 Binary files /dev/null and b/Client2016/content/textures/ui/scrollbuttonUp_ovr.png differ diff --git a/Client2016/content/textures/ui/slider_new_tab.png b/Client2016/content/textures/ui/slider_new_tab.png new file mode 100644 index 0000000..8ecb85a Binary files /dev/null and b/Client2016/content/textures/ui/slider_new_tab.png differ diff --git a/Client2016/content/textures/ui/slider_new_tab@2x.png b/Client2016/content/textures/ui/slider_new_tab@2x.png new file mode 100644 index 0000000..674c6ff Binary files /dev/null and b/Client2016/content/textures/ui/slider_new_tab@2x.png differ diff --git a/Client2016/content/textures/water_Subsurface.dds b/Client2016/content/textures/water_Subsurface.dds new file mode 100644 index 0000000..0435f14 Binary files /dev/null and b/Client2016/content/textures/water_Subsurface.dds differ diff --git a/Client2016/content/textures/water_Wave.dds b/Client2016/content/textures/water_Wave.dds new file mode 100644 index 0000000..fc2f7a0 Binary files /dev/null and b/Client2016/content/textures/water_Wave.dds differ diff --git a/Client2016/content/textures/wrench.png b/Client2016/content/textures/wrench.png new file mode 100644 index 0000000..5c8213f Binary files /dev/null and b/Client2016/content/textures/wrench.png differ diff --git a/Client2016/d3dcompiler_47.dll b/Client2016/d3dcompiler_47.dll new file mode 100644 index 0000000..e5bf5cf Binary files /dev/null and b/Client2016/d3dcompiler_47.dll differ diff --git a/Client2016/d3dx9_35.dll b/Client2016/d3dx9_35.dll new file mode 100644 index 0000000..48e487b Binary files /dev/null and b/Client2016/d3dx9_35.dll differ diff --git a/Client2016/fmod64.dll b/Client2016/fmod64.dll new file mode 100644 index 0000000..ee3f58f Binary files /dev/null and b/Client2016/fmod64.dll differ diff --git a/Client2016/openvr_api.dll b/Client2016/openvr_api.dll new file mode 100644 index 0000000..8494ad3 Binary files /dev/null and b/Client2016/openvr_api.dll differ diff --git a/Client2016/shaders/shaders.json b/Client2016/shaders/shaders.json new file mode 100644 index 0000000..874ac86 --- /dev/null +++ b/Client2016/shaders/shaders.json @@ -0,0 +1,152 @@ +[ + { "name": "AdornAALineVS", "source": "adorn.hlsl", "target": "vs_2_0", "entrypoint": "AdornAALineVS"}, + { "name": "AdornAALineFS", "source": "adorn.hlsl", "target": "ps_2_0", "entrypoint": "AdornAALinePS" }, + { "name": "AdornVS", "source": "adorn.hlsl", "target": "vs_2_0", "entrypoint": "AdornVS" }, + { "name": "AdornSelfLitVS", "source": "adorn.hlsl", "target": "vs_2_0", "entrypoint": "AdornSelfLitVS" }, + { "name": "AdornSelfLitHighlightVS", "source": "adorn.hlsl", "target": "vs_2_0", "entrypoint": "AdornSelfLitHighlightVS" }, + { "name": "AdornLightingVS", "source": "adorn.hlsl", "target": "vs_2_0", "entrypoint": "AdornVS", "defines": "PIN_LIGHTING" }, + { "name": "AdornFS", "source": "adorn.hlsl", "target": "ps_2_0", "entrypoint": "AdornPS" }, + { "name": "AdornOutlineVS", "source": "adorn.hlsl", "target": "vs_2_0", "entrypoint": "AdornOutlineVS"}, + { "name": "AdornOutlineFS", "source": "adorn.hlsl", "target": "ps_2_0", "entrypoint": "AdornOutlinePS" }, + { "name": "DefaultStaticVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS" }, + { "name": "DefaultStaticHQVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_HQ" }, + { "name": "DefaultSkinnedVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_SKINNED" }, + { "name": "DefaultSkinnedHQVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_SKINNED PIN_HQ" }, + { "name": "DefaultStaticDebugVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_DEBUG" }, + { "name": "DefaultSkinnedDebugVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_SKINNED PIN_DEBUG" }, + { "name": "DefaultFS", "source": "default.hlsl", "target": "ps_2_0", "entrypoint": "DefaultPS" }, + { "name": "DefaultHQFS", "source": "default.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultHQGBufferFS", "source": "default.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultStaticReflectionVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_REFLECTION" }, + { "name": "DefaultSkinnedReflectionVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_SKINNED PIN_REFLECTION" }, + { "name": "DefaultStaticSurfaceHQVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_SURFACE PIN_HQ" }, + { "name": "DefaultSkinnedSurfaceHQVS", "source": "default.hlsl", "target": "vs_2_0", "entrypoint": "DefaultVS", "defines": "PIN_SKINNED PIN_SURFACE PIN_HQ" }, + + { "name": "LowQMaterialFS", "source": "plastic.hlsl", "target": "ps_2_0", "entrypoint": "DefaultPS", "defines": "PIN_LOWQMAT" }, + { "name": "LowQMaterialWangFS", "source": "plastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_LOWQMAT PIN_WANG" }, + { "name": "LowQMaterialWangFallbackFS", "source": "plastic.hlsl", "target": "ps_2_0", "entrypoint": "DefaultPS", "defines": "PIN_LOWQMAT PIN_WANG_FALLBACK" }, + + { "name": "DefaultShadowStaticVS", "source": "shadow.hlsl", "target": "vs_2_0", "entrypoint": "ShadowVS" }, + { "name": "DefaultShadowSkinnedVS", "source": "shadow.hlsl", "target": "vs_2_0", "entrypoint": "ShadowVS", "defines": "PIN_SKINNED" }, + { "name": "DefaultShadowFS", "source": "shadow.hlsl", "target": "ps_2_0", "entrypoint": "ShadowPS" }, + + { "name": "DefaultPlasticFS", "source": "plastic.hlsl", "target": "ps_2_0", "entrypoint": "DefaultPS" }, + { "name": "DefaultNeonFS", "source": "neon.hlsl", "target": "ps_2_0", "entrypoint": "DefaultPS" }, + { "name": "DefaultPlasticHQFS", "source": "plastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultPlasticHQGBufferFS", "source": "plastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultPlasticReflectionFS", "source": "plastic.hlsl", "target": "ps_2_0", "entrypoint": "DefaultPS", "defines": "PIN_REFLECTION" }, + { "name": "DefaultPlasticReflectionHQFS", "source": "plastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_REFLECTION PIN_HQ" }, + { "name": "DefaultPlasticReflectionHQGBufferFS", "source": "plastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_REFLECTION PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultSmoothPlasticFS", "source": "smoothplastic.hlsl", "target": "ps_2_0", "entrypoint": "DefaultPS" }, + { "name": "DefaultSmoothPlasticHQFS", "source": "smoothplastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultSmoothPlasticHQGBufferFS", "source": "smoothplastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultSmoothPlasticReflectionFS", "source": "smoothplastic.hlsl", "target": "ps_2_0", "entrypoint": "DefaultPS", "defines": "PIN_REFLECTION" }, + { "name": "DefaultSmoothPlasticReflectionHQFS", "source": "smoothplastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_REFLECTION PIN_HQ" }, + { "name": "DefaultSmoothPlasticReflectionHQGBufferFS", "source": "smoothplastic.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_REFLECTION PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultWoodHQFS", "source": "wood.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultWoodHQGBufferFS", "source": "wood.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultMarbleHQFS", "source": "marble.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultMarbleHQGBufferFS", "source": "marble.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultSlateHQFS", "source": "slate.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultSlateHQGBufferFS", "source": "slate.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultGraniteHQFS", "source": "granite.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultGraniteHQGBufferFS", "source": "granite.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultConcreteHQFS", "source": "concrete.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultConcreteHQGBufferFS", "source": "concrete.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultPebbleHQFS", "source": "pebble.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultPebbleHQGBufferFS", "source": "pebble.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultBrickHQFS", "source": "brick.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultBrickHQGBufferFS", "source": "brick.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultRustHQFS", "source": "rust.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultRustHQGBufferFS", "source": "rust.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultDiamondplateHQFS", "source": "diamondplate.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultDiamondplateHQGBufferFS", "source": "diamondplate.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultAluminumHQFS", "source": "aluminum.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultAluminumHQGBufferFS", "source": "aluminum.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultGrassHQFS", "source": "grass.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultGrassHQGBufferFS", "source": "grass.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultSandHQFS", "source": "sand.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultSandHQGBufferFS", "source": "sand.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultFabricHQFS", "source": "fabric.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultFabricHQGBufferFS", "source": "fabric.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultIceHQFS", "source": "ice.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultIceHQGBufferFS", "source": "ice.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultCobblestoneHQFS", "source": "cobblestone.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultCobblestoneHQGBufferFS", "source": "cobblestone.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultMetalHQFS", "source": "metal.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultMetalHQGBufferFS", "source": "metal.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultWoodPlanksHQFS", "source": "woodplanks.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultWoodPlanksHQGBufferFS", "source": "woodplanks.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + { "name": "DefaultNeonHQFS", "source": "neon.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ" }, + { "name": "DefaultNeonHQGBufferFS", "source": "neon.hlsl", "target": "ps_2_a", "entrypoint": "DefaultPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + + { "name": "MegaClusterVS", "source": "megacluster.hlsl", "target": "vs_2_0", "entrypoint": "MegaClusterVS" }, + { "name": "MegaClusterHQVS", "source": "megacluster.hlsl", "target": "vs_2_0", "entrypoint": "MegaClusterVS", "defines": "PIN_HQ" }, + { "name": "MegaClusterFS", "source": "megacluster.hlsl", "target": "ps_2_0", "entrypoint": "MegaClusterPS" }, + { "name": "MegaClusterHQFS", "source": "megacluster.hlsl", "target": "ps_2_a", "entrypoint": "MegaClusterPS", "defines": "PIN_HQ" }, + { "name": "MegaClusterHQGBufferFS", "source": "megacluster.hlsl", "target": "ps_2_a", "entrypoint": "MegaClusterPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + + { "name": "SmoothClusterVS", "source": "smoothcluster.hlsl", "target": "vs_2_0", "entrypoint": "TerrainVS" }, + { "name": "SmoothClusterHQVS", "source": "smoothcluster.hlsl", "target": "vs_3_0", "entrypoint": "TerrainVS", "defines": "PIN_HQ" }, + { "name": "SmoothClusterFS", "source": "smoothcluster.hlsl", "target": "ps_2_0", "entrypoint": "TerrainPS" }, + { "name": "SmoothClusterHQFS", "source": "smoothcluster.hlsl", "target": "ps_3_0", "entrypoint": "TerrainPS", "defines": "PIN_HQ" }, + { "name": "SmoothClusterHQGBufferFS", "source": "smoothcluster.hlsl", "target": "ps_3_0", "entrypoint": "TerrainPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + + { "name": "ParticleVS", "source": "particle.hlsl", "target": "vs_1_1", "entrypoint": "vs" }, + { "name": "ParticleAddFS", "source": "particle.hlsl", "target": "ps_2_0", "entrypoint": "psAdd" }, + { "name": "ParticleModulateFS", "source": "particle.hlsl", "target": "ps_2_0", "entrypoint": "psModulate" }, + { "name": "ParticleCrazyFS", "source": "particle.hlsl", "target": "ps_2_0", "entrypoint": "psCrazy" }, + { "name": "ParticleCrazySparklesFS", "source": "particle.hlsl", "target": "ps_2_0", "entrypoint": "psCrazySparkles" }, + + { "name": "ParticleCustomVS", "source": "particle.hlsl", "target": "vs_1_1", "entrypoint": "vsCustom" }, + { "name": "ParticleCustomFS", "source": "particle.hlsl", "target": "ps_2_0", "entrypoint": "psCustom" }, + + { "name": "SkyVS", "source": "sky.hlsl", "target": "vs_1_1", "entrypoint": "SkyVS" }, + { "name": "SkyFS", "source": "sky.hlsl", "target": "ps_2_0", "entrypoint": "SkyPS" }, + { "name": "TexCompVS", "source": "texcomp.hlsl", "target": "vs_1_1", "entrypoint": "TexCompVS" }, + { "name": "TexCompFS", "source": "texcomp.hlsl", "target": "ps_2_0", "entrypoint": "TexCompPS" }, + { "name": "TexCompPMAFS", "source": "texcomp.hlsl", "target": "ps_2_0", "entrypoint": "TexCompPMAPS" }, + { "name": "UIVS", "source": "ui.hlsl", "target": "vs_1_1", "entrypoint": "UIVS" }, + { "name": "UIFogVS", "source": "ui.hlsl", "target": "vs_1_1", "entrypoint": "UIVS", "defines": "PIN_FOG" }, + { "name": "UIFS", "source": "ui.hlsl", "target": "ps_2_0", "entrypoint": "UIPS" }, + { "name": "UIFogFS", "source": "ui.hlsl", "target": "ps_2_0", "entrypoint": "UIPS", "defines": "PIN_FOG" }, + + { "name": "WaterVS", "source": "water.hlsl", "target": "vs_2_0", "entrypoint": "water_vs" }, + { "name": "WaterHQVS", "source": "water.hlsl", "target": "vs_2_0", "entrypoint": "water_vs", "defines": "PIN_HQ" }, + { "name": "WaterFS", "source": "water.hlsl", "target": "ps_2_0", "entrypoint": "water_ps" }, + { "name": "WaterHQFS", "source": "water.hlsl", "target": "ps_2_a", "entrypoint": "water_ps", "defines": "PIN_HQ" }, + + { "name": "SmoothWaterVS", "source": "smoothwater.hlsl", "target": "vs_2_0", "entrypoint": "WaterVS" }, + { "name": "SmoothWaterHQVS", "source": "smoothwater.hlsl", "target": "vs_3_0", "entrypoint": "WaterVS", "defines": "PIN_HQ" }, + { "name": "SmoothWaterFS", "source": "smoothwater.hlsl", "target": "ps_2_0", "entrypoint": "WaterPS" }, + { "name": "SmoothWaterHQFS", "source": "smoothwater.hlsl", "target": "ps_3_0", "entrypoint": "WaterPS", "defines": "PIN_HQ" }, + { "name": "SmoothWaterHQGBufferFS", "source": "smoothwater.hlsl", "target": "ps_3_0", "entrypoint": "WaterPS", "defines": "PIN_HQ PIN_GBUFFER", "exclude": "glsles" }, + + { "name": "SSAOFS", "source": "ssao.hlsl", "target": "ps_2_a", "entrypoint": "ssao_ps", "exclude": "glsles" }, + { "name": "SSAODepthDownVS", "source": "ssao.hlsl", "target": "vs_1_1", "entrypoint": "ssaoDepthDown_vs", "exclude": "glsles" }, + { "name": "SSAODepthDownFS", "source": "ssao.hlsl", "target": "ps_2_a", "entrypoint": "ssaoDepthDown_ps", "exclude": "glsles" }, + { "name": "SSAOBlurXVS", "source": "ssao.hlsl", "target": "vs_1_1", "entrypoint": "ssaoBlurX_vs", "exclude": "glsles" }, + { "name": "SSAOBlurXFS", "source": "ssao.hlsl", "target": "ps_2_a", "entrypoint": "ssaoBlurX_ps", "exclude": "glsles" }, + { "name": "SSAOBlurYVS", "source": "ssao.hlsl", "target": "vs_1_1", "entrypoint": "ssaoBlurY_vs", "exclude": "glsles" }, + { "name": "SSAOBlurYFS", "source": "ssao.hlsl", "target": "ps_2_a", "entrypoint": "ssaoBlurY_ps", "exclude": "glsles" }, + { "name": "SSAOCompositVS", "source": "ssao.hlsl", "target": "vs_1_1", "entrypoint": "ssaoComposit_vs", "exclude": "glsles" }, + { "name": "SSAOCompositFS", "source": "ssao.hlsl", "target": "ps_2_a", "entrypoint": "ssaoComposit_ps", "exclude": "glsles" }, + + { "name": "PassThroughVS", "source": "screenspace.hlsl", "target": "vs_1_1", "entrypoint": "passThrough_vs"}, + { "name": "PassThroughFS", "source": "screenspace.hlsl", "target": "ps_2_0", "entrypoint": "passThrough_ps"}, + { "name": "GlowApplyFS", "source": "screenspace.hlsl", "target": "ps_2_a", "entrypoint": "glowApply_ps", "exclude": "glsles" }, + { "name": "DownSample4x4VS", "source": "screenspace.hlsl", "target": "vs_1_1", "entrypoint": "downsample4x4_vs", "exclude": "glsles" }, + { "name": "DownSample4x4GlowFS", "source": "screenspace.hlsl", "target": "ps_2_0", "entrypoint": "downSample4x4Glow_ps", "exclude": "glsles" }, + { "name": "ShadowBlurFS", "source": "screenspace.hlsl", "target": "ps_2_0", "entrypoint": "ShadowBlurPS" }, + + { "name": "Blur3FS", "source": "screenspace.hlsl", "target": "ps_2_a", "entrypoint": "blur3_ps", "exclude": "glsles" }, + { "name": "Blur5FS", "source": "screenspace.hlsl", "target": "ps_2_a", "entrypoint": "blur5_ps", "exclude": "glsles" }, + { "name": "Blur7FS", "source": "screenspace.hlsl", "target": "ps_2_a", "entrypoint": "blur7_ps", "exclude": "glsles" }, + { "name": "ImageProcessFS", "source": "screenspace.hlsl", "target": "ps_2_0", "entrypoint": "imageProcess_ps", "exclude": "glsles" }, + + { "name": "GBufferResolveVS", "source" : "gbuffer.hlsl", "target" : "vs_2_0", "entrypoint" : "gbufferVS" }, + { "name": "GBufferResolveFS", "source" : "gbuffer.hlsl", "target" : "ps_2_0", "entrypoint" : "gbufferPS" }, + + { "name": "ProfilerVS", "source": "profiler.hlsl", "target": "vs_1_1", "entrypoint": "ProfilerVS" }, + { "name": "ProfilerFS", "source": "profiler.hlsl", "target": "ps_2_0", "entrypoint": "ProfilerPS" } +] diff --git a/Client2016/shaders/shaders_d3d11.pack b/Client2016/shaders/shaders_d3d11.pack new file mode 100644 index 0000000..e94eeaa Binary files /dev/null and b/Client2016/shaders/shaders_d3d11.pack differ diff --git a/Client2016/shaders/shaders_d3d11_durango.pack b/Client2016/shaders/shaders_d3d11_durango.pack new file mode 100644 index 0000000..52d5392 Binary files /dev/null and b/Client2016/shaders/shaders_d3d11_durango.pack differ diff --git a/Client2016/shaders/shaders_d3d9.pack b/Client2016/shaders/shaders_d3d9.pack new file mode 100644 index 0000000..21eafb1 Binary files /dev/null and b/Client2016/shaders/shaders_d3d9.pack differ diff --git a/Client2016/shaders/shaders_glsl.pack b/Client2016/shaders/shaders_glsl.pack new file mode 100644 index 0000000..66b2a99 Binary files /dev/null and b/Client2016/shaders/shaders_glsl.pack differ diff --git a/Client2016/shaders/shaders_glsl3.pack b/Client2016/shaders/shaders_glsl3.pack new file mode 100644 index 0000000..cab9fea Binary files /dev/null and b/Client2016/shaders/shaders_glsl3.pack differ diff --git a/Client2016/shaders/shaders_glsles.pack b/Client2016/shaders/shaders_glsles.pack new file mode 100644 index 0000000..940b565 Binary files /dev/null and b/Client2016/shaders/shaders_glsles.pack differ diff --git a/Client2016/shaders/shaders_glsles3.pack b/Client2016/shaders/shaders_glsles3.pack new file mode 100644 index 0000000..9f5f176 Binary files /dev/null and b/Client2016/shaders/shaders_glsles3.pack differ diff --git a/Client2016/shaders/source/adorn.hlsl b/Client2016/shaders/source/adorn.hlsl new file mode 100644 index 0000000..991e248 --- /dev/null +++ b/Client2016/shaders/source/adorn.hlsl @@ -0,0 +1,249 @@ +#include "common.h" + +struct Appdata +{ + float4 Position : POSITION; + float2 Uv : TEXCOORD0; + float3 Normal : NORMAL0; +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; + + float2 Uv : TEXCOORD0; + float4 Color : COLOR0; + + float FogFactor : TEXCOORD1; +}; + +struct AALineVertexOutput +{ + float4 HPosition : POSITION; + + float4 Position : TEXCOORD1; + float4 Color : COLOR0; + + float FogFactor : COLOR1; + float4 Start : TEXCOORD2; + float4 End : TEXCOORD3; +}; + +struct OutlineVertexOutput +{ + float4 HPosition : POSITION; + + float4 Color : COLOR0; + float4 Position : TEXCOORD0; + + float4 CenterRadius : TEXCOORD1; +}; + +WORLD_MATRIX(WorldMatrix); + +uniform float4 Color; +// pixel info is for AA line +// x -> Fov * 0.5f / screenSize.y; +// y -> ScreenWidth +// z -> ScreenWidth / ScreenHeight +// w -> Line thickness +uniform float4 PixelInfo; + + +VertexOutput AdornSelfLitVSGeneric(Appdata IN, float ambient) +{ + VertexOutput OUT = (VertexOutput)0; + + float4 position = mul(WorldMatrix, IN.Position); + float3 normal = normalize(mul((float3x3)WorldMatrix, IN.Normal)); + + float3 light = normalize(G(CameraPosition).xyz - position.xyz); + float ndotl = saturate(dot(normal, light)); + + float lighting = ambient + (1 - ambient) * ndotl; + float specular = pow(ndotl, 64.0); + + OUT.HPosition = mul(G(ViewProjection), mul(WorldMatrix, IN.Position)); + OUT.Uv = IN.Uv; + OUT.Color = float4(Color.rgb * lighting + specular, Color.a); + + OUT.FogFactor = (G(FogParams).z - OUT.HPosition.w) * G(FogParams).w; + + return OUT; +} + +VertexOutput AdornSelfLitVS(Appdata IN) +{ + return AdornSelfLitVSGeneric(IN, 0.5f); +} + +VertexOutput AdornSelfLitHighlightVS(Appdata IN) +{ + return AdornSelfLitVSGeneric(IN, 0.75f); +} + +VertexOutput AdornVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + float4 position = mul(WorldMatrix, IN.Position); + +#ifdef PIN_LIGHTING + float3 normal = normalize(mul((float3x3)WorldMatrix, IN.Normal)); + float ndotl = dot(normal, -G(Lamp0Dir)); + float3 lighting = G(AmbientColor) + saturate(ndotl) * G(Lamp0Color) + saturate(-ndotl) * G(Lamp1Color); +#else + float3 lighting = 1; +#endif + + OUT.HPosition = mul(G(ViewProjection), position); + OUT.Uv = IN.Uv; + OUT.Color = float4(Color.rgb * lighting, Color.a); + + OUT.FogFactor = (G(FogParams).z - OUT.HPosition.w) * G(FogParams).w; + + return OUT; +} + +TEX_DECLARE2D(DiffuseMap, 0); + +float4 AdornPS(VertexOutput IN): COLOR0 +{ + float4 result = tex2D(DiffuseMap, IN.Uv) * IN.Color; + + result.rgb = lerp(G(FogColor), result.rgb, saturate(IN.FogFactor)); + + return result; +} + +AALineVertexOutput AdornAALineVS(Appdata IN) +{ + AALineVertexOutput OUT = (AALineVertexOutput)0; + + float4 position = mul(WorldMatrix, IN.Position); + float3 normal = normalize(mul((float3x3)WorldMatrix, IN.Normal)); + + // line start and end position in world space + float4 startPosW = mul(WorldMatrix, float4(1, 0, 0, 1)); + float4 endPosW = mul(WorldMatrix, float4(-1, 0, 0, 1)); + + // Compute view-space w + float w = dot(G(ViewProjection)[3], float4(position.xyz, 1.0f)); + + // radius in pixels + constant because line has to be little bit bigget to perform anti aliasing + float radius = PixelInfo.w + 2; + + // scale the way that line has same size on screen + if (length(position - startPosW) < length(position - endPosW)) + { + float w = dot(G(ViewProjection)[3], float4(startPosW.xyz, 1.0f)); + float pixel_radius = radius * w * PixelInfo.x; + position.xyz = startPosW.xyz + normal * pixel_radius; + } + else + { + float w = dot(G(ViewProjection)[3], float4(endPosW.xyz, 1.0f)); + float pixel_radius = radius * w * PixelInfo.x; + position.xyz = endPosW.xyz + normal * pixel_radius; + } + + // output for PS + OUT.HPosition = mul(G(ViewProjection), position); + OUT.Position = OUT.HPosition; + OUT.Start = mul(G(ViewProjection), startPosW); + OUT.End = mul(G(ViewProjection), endPosW); + OUT.FogFactor = (G(FogParams).z - OUT.HPosition.w) * G(FogParams).w; + + // screen ratio + OUT.Position.y *= PixelInfo.z; + OUT.Start.y *= PixelInfo.z; + OUT.End.y *= PixelInfo.z; + + return OUT; +} + +float4 AdornAALinePS(AALineVertexOutput IN): COLOR0 +{ + IN.Position /= IN.Position.w ; + IN.Start /= IN.Start.w; + IN.End /= IN.End.w; + + float4 result = 1; + + float2 lineDir = normalize(IN.End.xy - IN.Start.xy); + float2 fragToPoint = IN.Position.xy - IN.Start.xy; + + // tips of the line are not Anti-Aliesed, they are just cut + // discard as soon as we can + float startDist = dot(lineDir, fragToPoint); + float endDist = dot(lineDir, -IN.Position.xy + IN.End.xy); + + if (startDist < 0) + discard; + + if (endDist < 0) + discard; + + float2 perpLineDir = float2(lineDir.y, -lineDir.x); + + float dist = abs(dot(perpLineDir, fragToPoint)); + + // high point serves to compute the function which is described bellow. + float highPoint = 1 + (PixelInfo.w - 1) * 0.5; + + // this is function that has this shape /¯¯¯\, it is symetric, centered around 0 on X axis + // slope parts are +- 45 degree and are 1px thick. Area of the shape sums to line thickness in pixels + // funtion for 1px would be /\, func for 2px is /¯\ and so on... + result.a = saturate(highPoint - (dist * 0.5 * PixelInfo.y)); + + result *= Color; + + // convert to sRGB, its not perfect for non-black backgrounds, but its the best we can get + result.a = pow( saturate(1 - result.a), 1/2.2); + result.a = 1 - result.a; + + result.rgb = lerp(G(FogColor), result.rgb, saturate(IN.FogFactor)); + return result; + +} + +OutlineVertexOutput AdornOutlineVS(Appdata IN) +{ + OutlineVertexOutput OUT = (OutlineVertexOutput)0; + + float4 position = mul(WorldMatrix, IN.Position); + + OUT.HPosition = mul(G(ViewProjection), position); + + OUT.Color = Color; + OUT.Position = position; + + OUT.CenterRadius = float4(mul(WorldMatrix, float4(0, 0, 0, 1)).xyz, length(mul(WorldMatrix, float4(1, 0, 0, 0)))); + + return OUT; +} + +float4 AdornOutlinePS(OutlineVertexOutput IN): COLOR0 +{ + float3 rayO = IN.Position.xyz - IN.CenterRadius.xyz; + float3 rayD = normalize(IN.Position.xyz - G(CameraPosition)); + + // magnitude(rayO + t * rayD) = radius + // t^2 + bt + c = radius + float thickness = 1; + + float r0 = IN.CenterRadius.w; + float r1 = max(0, IN.CenterRadius.w - thickness); + + float b = 2 * dot(rayO, rayD); + float c0 = dot(rayO, rayO) - r0 * r0; + float c1 = dot(rayO, rayO) - r1 * r1; + + if (b * b < 4 * c0) + discard; + + if (b * b > 4 * c1) + discard; + + return IN.Color; +} diff --git a/Client2016/shaders/source/aluminum.hlsl b/Client2016/shaders/source/aluminum.hlsl new file mode 100644 index 0000000..f327503 --- /dev/null +++ b/Client2016/shaders/source/aluminum.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 1 +#define CFG_GLOSS_SCALE 256 +#define CFG_REFLECTION_SCALE 0.6 + +#define CFG_NORMAL_SHADOW_SCALE 0 + +#define CFG_SPECULAR_LOD 0.94 +#define CFG_GLOSS_LOD 240 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0.25 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0.75 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#define CFG_OPT_DIFFUSE_CONST + +#include "material.hlsl" diff --git a/Client2016/shaders/source/brick.hlsl b/Client2016/shaders/source/brick.hlsl new file mode 100644 index 0000000..4b9c574 --- /dev/null +++ b/Client2016/shaders/source/brick.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 1.3 +#define CFG_GLOSS_SCALE 64 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.1 + +#define CFG_SPECULAR_LOD 0.13 +#define CFG_GLOSS_LOD 44 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#define CFG_OPT_BLEND_COLOR + +#include "material.hlsl" diff --git a/Client2016/shaders/source/cobblestone.hlsl b/Client2016/shaders/source/cobblestone.hlsl new file mode 100644 index 0000000..560e18d --- /dev/null +++ b/Client2016/shaders/source/cobblestone.hlsl @@ -0,0 +1,22 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 3 +#define CFG_GLOSS_SCALE 256 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.3 + +#define CFG_SPECULAR_LOD 0.21 +#define CFG_GLOSS_LOD 22 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_OPT_BLEND_COLOR + +#define CFG_WANG_TILES +#define CFG_WANG_TILES_SCALE 1 + + +#include "material.hlsl" diff --git a/Client2016/shaders/source/common.h b/Client2016/shaders/source/common.h new file mode 100644 index 0000000..2fd3e6d --- /dev/null +++ b/Client2016/shaders/source/common.h @@ -0,0 +1,252 @@ +#include "globals.h" + +// GLSLES has limited number of vertex shader registers so we have to use less bones +#if defined(GLSLES) && !defined(GL3) +#define MAX_BONE_COUNT 32 +#else +#define MAX_BONE_COUNT 72 +#endif + +// PowerVR saturate() is compiled to min/max pair +// These are cross-platform specialized saturates that are free on PC and only cost 1 cycle on PowerVR +#ifdef GLSLES +float saturate0(float v) { return max(v, 0); } +float saturate1(float v) { return min(v, 1); } +#define WANG_SUBSET_SCALE 2 +#else +float saturate0(float v) { return saturate(v); } +float saturate1(float v) { return saturate(v); } +#define WANG_SUBSET_SCALE 1 +#endif + +#define GBUFFER_MAX_DEPTH 500.0f + + +#ifndef DX11 + #define TEX_DECLARE2D(name, reg) sampler2D name: register(s##reg) + #define TEX_DECLARE3D(name, reg) sampler3D name: register(s##reg) + #define TEX_DECLARECUBE(name, reg) samplerCUBE name: register(s##reg) + + #define TEXTURE(name) name + #define TEXTURE_IN_2D(name) sampler2D name + #define TEXTURE_IN_3D(name) sampler3D name + #define TEXTURE_IN_CUBE(name) samplerCUBE name + + #define WORLD_MATRIX(name) uniform float4x4 name; + #define WORLD_MATRIX_ARRAY(name, count) uniform float4 name [ count ]; + + #ifdef GLSL + #define ATTR_INT4 float4 + #define ATTR_INT3 float3 + #define ATTR_INT2 float2 + #define ATTR_INT float + #else + #define ATTR_INT4 int4 + #define ATTR_INT3 int3 + #define ATTR_INT2 int2 + #define ATTR_INT int + #endif +#else + #define TEX_DECLARE2D(name, reg) SamplerState name##Sampler: register(s##reg); Texture2D name##Texture: register(t##reg) + #define TEX_DECLARE3D(name, reg) SamplerState name##Sampler: register(s##reg); Texture3D name##Texture: register(t##reg) + #define TEX_DECLARECUBE(name, reg) SamplerState name##Sampler: register(s##reg); TextureCube name##Texture: register(t##reg) + + #define tex2D(tex, uv) tex##Texture.Sample(tex##Sampler, uv) + #define tex3D(tex, uv) tex##Texture.Sample(tex##Sampler, uv) + #define texCUBE(tex, uv) tex##Texture.Sample(tex##Sampler, uv) + #define tex2Dgrad(tex, uv, DDX, DDY) tex##Texture.SampleGrad(tex##Sampler, uv, DDX, DDY) + #define tex2Dbias(tex, uv) tex##Texture.SampleBias(tex##Sampler, uv.xy, uv.w) + #define texCUBEbias(tex, uv) tex##Texture.SampleBias(tex##Sampler, uv.xyz, uv.w) + + #define TEXTURE(name) name##Sampler, name##Texture + #define TEXTURE_IN_2D(name) SamplerState name##Sampler, Texture2D name##Texture + #define TEXTURE_IN_3D(name) SamplerState name##Sampler, Texture3D name##Texture + #define TEXTURE_IN_CUBE(name) SamplerState name##Sampler, TextureCube name##Texture + + #define WORLD_MATRIX(name) cbuffer WorldMatrixCB : register( b1 ) { float4x4 name; } + #define WORLD_MATRIX_ARRAY(name, count) cbuffer WorldMatrixCB : register( b1 ) { float4 name[ count ]; } + + #define ATTR_INT4 int4 + #define ATTR_INT3 int3 + #define ATTR_INT2 int2 + #define ATTR_INT int +#endif + +#if defined(GLSLES) || defined(PIN_WANG_FALLBACK) + #define TEXTURE_WANG(name) 0 + void getWang(float unused, float2 uv, float tiling, out float2 wangUv, out float4 wangUVDerivatives) + { + wangUv = uv * WANG_SUBSET_SCALE; + wangUVDerivatives = float4(0,0,0,0); // not used in this mode + } + float4 sampleWang(TEXTURE_IN_2D(s), float2 uv, float4 wangUVDerivatives) + { + return tex2D(s,uv); + } +#else + #define TEXTURE_WANG(name) TEXTURE(name) + void getWang(TEXTURE_IN_2D(s), float2 uv, float tiling, out float2 wangUv, out float4 wangUVDerivatives) + { + #ifndef WIN_MOBILE + float idxTexSize = 128; + #else + float idxTexSize = 32; + #endif + + float2 wangBase = uv * tiling * 4; + + #if defined(DX11) && !defined(WIN_MOBILE) + // compensate the precision problem of Point Sampling on some cards. (We do it just at DX11 for performance reasons) + float2 wangUV = (floor(wangBase) + 0.5) / idxTexSize; + #else + float2 wangUV = wangBase / idxTexSize; + #endif + + #if defined(DX11) || defined(GL3) + float2 wang = tex2D(s, wangUV).rg; + #else + float2 wang = tex2D(s, wangUV).ba; + #endif + + wangUVDerivatives = float4(ddx(wangBase*0.25), ddy(wangBase*0.25)); + + wang *= 255.0/256.0; + wangUv = wang + frac(wangBase)*0.25; + } + float4 sampleWang(TEXTURE_IN_2D(s), float2 uv, float4 derivates) + { + return tex2Dgrad(s, uv, derivates.xy, derivates.zw); + } +#endif + +float4 gbufferPack(float depth, float3 diffuse, float3 specular, float fog) +{ + depth = saturate(depth / GBUFFER_MAX_DEPTH); + + const float3 bitSh = float3(255*255, 255, 1); + const float3 lumVec = float3(0.299, 0.587, 0.114); + + float2 comp; + comp = depth*float2(255,255*256); + comp = frac(comp); + comp = float2(depth,comp.x*256/255) - float2(comp.x, comp.y)/255; + + float4 result; + + result.r = lerp(1, dot(specular, lumVec), saturate(3 * fog)); + result.g = lerp(0, dot(diffuse, lumVec), saturate(3 * fog)); + result.ba = comp.yx; + + return result; +} + +float3 lgridOffset(float3 v, float3 n) +{ + // cells are 4 studs in size + // offset in normal direction to prevent self-occlusion + // the offset has to be 1.5 cells in order to fully eliminate the influence of the source cell with trilinear filtering + // (i.e. 1 cell is enough for point filtering, but is not enough for trilinear filtering) + return v + n * (1.5f * 4.f); +} + +float3 lgridPrepareSample(float3 c) +{ + // yxz swizzle is necessary for GLSLES sampling to work efficiently + // (having .y as the first component allows to do the LUT lookup as a non-dependent texture fetch) + return c.yxz * G(LightConfig0).xyz + G(LightConfig1).xyz; +} + +#if defined(GLSLES) && !defined(GL3) +#define LGRID_SAMPLER(name, register) TEX_DECLARE2D(name, register) + +float4 lgridSample(TEXTURE_IN_2D(t), TEXTURE_IN_2D(lut), float3 data) +{ + float4 offsets = tex2D(lut, data.xy); + + // texture is 64 pixels high + // let's compute slice lerp coeff + float slicef = frac(data.x * 64); + + // texture has 64 slices with 8x8 atlas setup + float2 base = saturate(data.yz) * 0.125; + + float4 s0 = tex2D(t, base + offsets.xy); + float4 s1 = tex2D(t, base + offsets.zw); + + return lerp(s0, s1, slicef); +} +#else +#define LGRID_SAMPLER(name, register) TEX_DECLARE3D(name, register) + +float4 lgridSample(TEXTURE_IN_3D(t), TEXTURE_IN_2D(lut), float3 data) +{ + float3 edge = step(G(LightConfig3).xyz, abs(data - G(LightConfig2).xyz)); + float edgef = saturate1(dot(edge, 1)); + + // replace data with 0 on edges to minimize texture cache misses + float4 light = tex3D(t, data.yzx - data.yzx * edgef); + + return lerp(light, G(LightBorder), edgef); +} +#endif + +#ifdef GLSLES +float3 nmapUnpack(float4 value) +{ + return value.rgb * 2 - 1; +} +#else +float3 nmapUnpack(float4 value) +{ + float2 xy = value.ag * 2 - 1; + + return float3(xy, sqrt(saturate(1 + dot(-xy, xy)))); +} +#endif + +float3 terrainNormal(float4 tnp0, float4 tnp1, float4 tnp2, float3 w, float3 normal, float3 tsel) +{ + // Inspired by "Voxel-Based Terrain for Real-Time Virtual Simulations" [Lengyel2010] 5.5.2 + float3 tangentTop = float3(normal.y, -normal.x, 0); + float3 tangentSide = float3(normal.z, 0, -normal.x); + + float3 bitangentTop = float3(0, -normal.z, normal.y); + float3 bitangentSide = float3(0, -1, 0); + + // Blend pre-unpack to save cycles + float3 tn = nmapUnpack(tnp0 * w.x + tnp1 * w.y + tnp2 * w.z); + + // We blend all tangent frames together as a faster approximation to the correct world normal blend + float tselw = dot(tsel, w); + + float3 tangent = lerp(tangentSide, tangentTop, tselw); + float3 bitangent = lerp(bitangentSide, bitangentTop, tselw); + + return normalize(tangent * tn.x + bitangent * tn.y + normal * tn.z); +} + +float3 shadowPrepareSample(float3 p) +{ + float4 c = float4(p, 1); + + return float3(dot(G(ShadowMatrix0), c), dot(G(ShadowMatrix1), c), dot(G(ShadowMatrix2), c)); +} + +float shadowDepth(float3 lpos) +{ + return lpos.z; +} + +float shadowStep(float d, float z) +{ + // saturate returns 1 for z in [0.1..0.9]; it fades to 0 as z approaches 0 or 1 + return step(d, z) * saturate(9 - 20 * abs(z - 0.5)); +} + +float shadowSample(TEXTURE_IN_2D(map), float3 lpos, float lightShadow) +{ + float2 smDepth = tex2D(map, lpos.xy).rg; + float smShadow = shadowStep(smDepth.x, shadowDepth(lpos)); + + return (1 - smShadow * smDepth.y * G(OutlineBrightness_ShadowInfo).w) * lightShadow; +} diff --git a/Client2016/shaders/source/concrete.hlsl b/Client2016/shaders/source/concrete.hlsl new file mode 100644 index 0000000..e068502 --- /dev/null +++ b/Client2016/shaders/source/concrete.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 1.3 +#define CFG_GLOSS_SCALE 128 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0 + +#define CFG_SPECULAR_LOD 0.07 +#define CFG_GLOSS_LOD 22 + +#define CFG_NORMAL_DETAIL_TILING 10 +#define CFG_NORMAL_DETAIL_SCALE 1 + +#define CFG_FAR_TILING 0.25 +#define CFG_FAR_DIFFUSE_CUTOFF 0.75 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#define CFG_OPT_NORMAL_CONST + +#include "material.hlsl" diff --git a/Client2016/shaders/source/default.hlsl b/Client2016/shaders/source/default.hlsl new file mode 100644 index 0000000..60058df --- /dev/null +++ b/Client2016/shaders/source/default.hlsl @@ -0,0 +1,329 @@ +#include "common.h" + +#define LQMAT_FADE_FACTOR (1.0f/300.0f) + +struct Appdata +{ + float4 Position : POSITION; + float3 Normal : NORMAL; + float2 Uv : TEXCOORD0; + float2 UvStuds : TEXCOORD1; + + float4 Color : COLOR0; + ATTR_INT4 Extra : COLOR1; + +#ifdef PIN_SURFACE + float3 Tangent : TEXCOORD2; +#endif + float4 EdgeDistances : TEXCOORD3; +}; + +struct VertexOutput +{ + float4 Uv_EdgeDistance1 : TEXCOORD0; + float4 UvStuds_EdgeDistance2 : TEXCOORD1; + + float4 Color : COLOR0; + float4 LightPosition_Fog : TEXCOORD2; + + float4 View_Depth : TEXCOORD3; + #if defined(PIN_HQ) || defined(PIN_REFLECTION) + float4 Normal_SpecPower : TEXCOORD4; + #endif + + #ifdef PIN_SURFACE + float3 Tangent : TEXCOORD5; + #else + float4 Diffuse_Specular : COLOR1; + #endif + + float4 PosLightSpace_Reflectance: TEXCOORD6; +}; + +#ifdef PIN_SKINNED + WORLD_MATRIX_ARRAY(WorldMatrixArray, MAX_BONE_COUNT * 3); +#endif + +#ifdef PIN_DEBUG +uniform float4 DebugColor; +#endif + +VertexOutput DefaultVS(Appdata IN, out float4 HPosition: POSITION) +{ + VertexOutput OUT = (VertexOutput)0; + + // Transform position and normal to world space +#ifdef PIN_SKINNED + int boneIndex = IN.Extra.r; + + float4 worldRow0 = WorldMatrixArray[boneIndex * 3 + 0]; + float4 worldRow1 = WorldMatrixArray[boneIndex * 3 + 1]; + float4 worldRow2 = WorldMatrixArray[boneIndex * 3 + 2]; + + float3 posWorld = float3(dot(worldRow0, IN.Position), dot(worldRow1, IN.Position), dot(worldRow2, IN.Position)); + float3 normalWorld = float3(dot(worldRow0.xyz, IN.Normal), dot(worldRow1.xyz, IN.Normal), dot(worldRow2.xyz, IN.Normal)); +#else + float3 posWorld = IN.Position.xyz; + float3 normalWorld = IN.Normal; +#endif + + // Decode diffuse/specular parameters; encoding depends on the skinned flag due to vertex declaration differences +#if defined(PIN_DEBUG) + float4 color = DebugColor; +#else + float4 color = IN.Color; +#endif + + float specularIntensity = IN.Extra.g / 255.f; + float specularPower = IN.Extra.b; + + float ndotl = dot(normalWorld, -G(Lamp0Dir)); + +#ifdef PIN_HQ + // We'll calculate specular in pixel shader + float2 lt = float2(saturate(ndotl), (ndotl > 0)); +#else + // Using lit here improves performance on software vertex shader implementations + float2 lt = lit(ndotl, dot(normalize(-G(Lamp0Dir) + normalize(G(CameraPosition).xyz - posWorld.xyz)), normalWorld), specularPower).yz; +#endif + + HPosition = mul(G(ViewProjection), float4(posWorld, 1)); + + OUT.Uv_EdgeDistance1.xy = IN.Uv; + OUT.UvStuds_EdgeDistance2.xy = IN.UvStuds; + + OUT.Color = color; + OUT.LightPosition_Fog = float4(lgridPrepareSample(lgridOffset(posWorld, normalWorld)), (G(FogParams).z - HPosition.w) * G(FogParams).w); + + OUT.View_Depth = float4(G(CameraPosition).xyz - posWorld, HPosition.w); + + #if defined(PIN_HQ) || defined(PIN_REFLECTION) + float4 edgeDistances = IN.EdgeDistances*G(FadeDistance_GlowFactor).z + 0.5 * OUT.View_Depth.w * G(FadeDistance_GlowFactor).y; + + OUT.Uv_EdgeDistance1.zw = edgeDistances.xy; + OUT.UvStuds_EdgeDistance2.zw = edgeDistances.zw; + OUT.Normal_SpecPower = float4(normalWorld, specularPower); + OUT.PosLightSpace_Reflectance.w = IN.Extra.a / 255.f; + #endif + + #ifdef PIN_SURFACE + #ifdef PIN_SKINNED + float3 tangent = float3(dot(worldRow0.xyz, IN.Tangent), dot(worldRow1.xyz, IN.Tangent), dot(worldRow2.xyz, IN.Tangent)); + #else + float3 tangent = IN.Tangent; + #endif + + OUT.Tangent = tangent; + #else + float3 diffuse = lt.x * G(Lamp0Color) + max(-ndotl, 0) * G(Lamp1Color); + + OUT.Diffuse_Specular = float4(diffuse, lt.y * specularIntensity); + #endif + + OUT.PosLightSpace_Reflectance.xyz = shadowPrepareSample(posWorld); + + return OUT; +} + +#ifdef PIN_SURFACE +struct SurfaceInput +{ + float4 Color; + float2 Uv; + float2 UvStuds; + +#ifdef PIN_REFLECTION + float Reflectance; +#endif +}; + +struct Surface +{ + float3 albedo; + float3 normal; + float specular; + float gloss; + float reflectance; +}; + +Surface surfaceShader(SurfaceInput IN, float2 fade); + +Surface surfaceShaderExec(VertexOutput IN) +{ + SurfaceInput SIN; + SIN.Color = IN.Color; + SIN.Uv = IN.Uv_EdgeDistance1.xy; + SIN.UvStuds = IN.UvStuds_EdgeDistance2.xy; + + #ifdef PIN_REFLECTION + SIN.Reflectance = IN.PosLightSpace_Reflectance.w; + #endif + + float2 fade; + fade.x = saturate0(1 - IN.View_Depth.w * LQMAT_FADE_FACTOR ); + fade.y = saturate0(1 - IN.View_Depth.w * G(FadeDistance_GlowFactor).y ); + + return surfaceShader(SIN, fade); +} +#endif + +TEX_DECLARE2D(StudsMap, 0); + +LGRID_SAMPLER(LightMap, 1); +TEX_DECLARE2D(LightMapLookup, 2); + +TEX_DECLARE2D(ShadowMap, 3); + +TEX_DECLARECUBE(EnvironmentMap, 4); + +TEX_DECLARE2D(DiffuseMap, 5); +TEX_DECLARE2D(NormalMap, 6); +TEX_DECLARE2D(SpecularMap, 7); + +#ifndef GLSLES +TEX_DECLARE2D(NormalDetailMap, 8); +#endif + +uniform float4 LqmatFarTilingFactor; // material tiling factor for low-quality shader, must be the same as CFG_FAR_TILING + + +float4 sampleFar1(TEXTURE_IN_2D(s), float2 uv, float fade, float cutoff) +{ +#ifdef GLSLES + return tex2D(s, uv); +#else + if (cutoff == 0) + return tex2D(s, uv); + else + { + float cscale = 1 / (1 - cutoff); + + return lerp(tex2D(s, uv * (LqmatFarTilingFactor.xy) ), tex2D(s, uv ), saturate0(fade * cscale - cutoff * cscale)); + } +#endif +} + +#if defined(PIN_WANG) || defined(PIN_WANG_FALLBACK) +float4 sampleWangSimple(TEXTURE_IN_2D(s), float2 uv) +{ + float2 wangUv; + float4 wangUVDerivatives; + getWang(TEXTURE_WANG(NormalDetailMap), uv, 1, wangUv, wangUVDerivatives); + return sampleWang(TEXTURE(s), wangUv, wangUVDerivatives); +} +#endif + +void DefaultPS(VertexOutput IN, +#ifdef PIN_GBUFFER + out float4 oColor1: COLOR1, +#endif + out float4 oColor0: COLOR0) +{ + // Compute albedo term +#ifdef PIN_SURFACE + Surface surface = surfaceShaderExec(IN); + + float4 albedo = float4(surface.albedo, IN.Color.a); + + float3 bitangent = cross(IN.Normal_SpecPower.xyz, IN.Tangent.xyz); + float3 normal = normalize(surface.normal.x * IN.Tangent.xyz + surface.normal.y * bitangent + surface.normal.z * IN.Normal_SpecPower.xyz); + + float ndotl = dot(normal, -G(Lamp0Dir)); + + float3 diffuseIntensity = saturate0(ndotl) * G(Lamp0Color) + max(-ndotl, 0) * G(Lamp1Color); + float specularIntensity = step(0, ndotl) * surface.specular; + float specularPower = surface.gloss; + + float reflectance = surface.reflectance; +#elif PIN_LOWQMAT + +#ifndef CFG_FAR_DIFFUSE_CUTOFF +#define CFG_FAR_DIFFUSE_CUTOFF (0.6f) +#endif + +#if defined(PIN_WANG) || defined(PIN_WANG_FALLBACK) + float4 albedo = sampleWangSimple(TEXTURE(DiffuseMap), IN.Uv_EdgeDistance1.xy); +#else + float fade = saturate0(1 - IN.View_Depth.w * LQMAT_FADE_FACTOR); + float4 albedo = sampleFar1(TEXTURE(DiffuseMap), IN.Uv_EdgeDistance1.xy, fade, CFG_FAR_DIFFUSE_CUTOFF); +#endif + + albedo.rgb = lerp(float3(1, 1, 1), IN.Color.rgb, albedo.a ) * albedo.rgb; + albedo.a = IN.Color.a; + + float3 diffuseIntensity = IN.Diffuse_Specular.xyz; + float specularIntensity = IN.Diffuse_Specular.w; + float reflectance = 0; + +#else + #ifdef PIN_PLASTIC + float4 studs = tex2D(StudsMap, IN.UvStuds_EdgeDistance2.xy); + float4 albedo = float4(IN.Color.rgb * (studs.r * 2), IN.Color.a); + #else + float4 albedo = tex2D(DiffuseMap, IN.Uv_EdgeDistance1.xy) * IN.Color; + #endif + + #ifdef PIN_HQ + float3 normal = normalize(IN.Normal_SpecPower.xyz); + float specularPower = IN.Normal_SpecPower.w; + #elif defined(PIN_REFLECTION) + float3 normal = IN.Normal_SpecPower.xyz; + #endif + + float3 diffuseIntensity = IN.Diffuse_Specular.xyz; + float specularIntensity = IN.Diffuse_Specular.w; + + #ifdef PIN_REFLECTION + float reflectance = IN.PosLightSpace_Reflectance.w; + #endif + +#endif + + float4 light = lgridSample(TEXTURE(LightMap), TEXTURE(LightMapLookup), IN.LightPosition_Fog.xyz); + float shadow = shadowSample(TEXTURE(ShadowMap), IN.PosLightSpace_Reflectance.xyz, light.a); + + // Compute reflection term +#if defined(PIN_SURFACE) || defined(PIN_REFLECTION) + float3 reflection = texCUBE(EnvironmentMap, reflect(-IN.View_Depth.xyz, normal)).rgb; + + albedo.rgb = lerp(albedo.rgb, reflection.rgb, reflectance); +#endif + + // Compute diffuse term + float3 diffuse = (G(AmbientColor) + diffuseIntensity * shadow + light.rgb) * albedo.rgb; + + // Compute specular term +#ifdef PIN_HQ + float3 specular = G(Lamp0Color) * (specularIntensity * shadow * (float)(half)pow(saturate(dot(normal, normalize(-G(Lamp0Dir) + normalize(IN.View_Depth.xyz)))), specularPower)); +#else + float3 specular = G(Lamp0Color) * (specularIntensity * shadow); +#endif + + // Combine + oColor0.rgb = diffuse.rgb + specular.rgb; + oColor0.a = albedo.a; + +#ifdef PIN_HQ + float ViewDepthMul = IN.View_Depth.w * G(FadeDistance_GlowFactor).y; + float outlineFade = saturate1( ViewDepthMul * G(OutlineBrightness_ShadowInfo).x + G(OutlineBrightness_ShadowInfo).y); + float2 minIntermediate = min(IN.Uv_EdgeDistance1.wz, IN.UvStuds_EdgeDistance2.wz); + float minEdgesPlus = min(minIntermediate.x, minIntermediate.y) / ViewDepthMul; + oColor0.rgb *= saturate1(outlineFade *(1.5 - minEdgesPlus) + minEdgesPlus); +#endif + + float fogAlpha = saturate(IN.LightPosition_Fog.w); + +#ifdef PIN_NEON + oColor0.rgb = IN.Color.rgb * G(FadeDistance_GlowFactor).w; + oColor0.a = 1 - fogAlpha * IN.Color.a; + diffuse.rgb = 0; + specular.rgb = 0; +#endif + + oColor0.rgb = lerp(G(FogColor), oColor0.rgb, fogAlpha); + +#ifdef PIN_GBUFFER + oColor1 = gbufferPack(IN.View_Depth.w, diffuse.rgb, specular.rgb, fogAlpha); +#endif + +} diff --git a/Client2016/shaders/source/diamondplate.hlsl b/Client2016/shaders/source/diamondplate.hlsl new file mode 100644 index 0000000..6b0c8f8 --- /dev/null +++ b/Client2016/shaders/source/diamondplate.hlsl @@ -0,0 +1,21 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 2.7 +#define CFG_GLOSS_SCALE 256 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.5 + +#define CFG_SPECULAR_LOD 0.9 +#define CFG_GLOSS_LOD 160 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#include "material.hlsl" diff --git a/Client2016/shaders/source/fabric.hlsl b/Client2016/shaders/source/fabric.hlsl new file mode 100644 index 0000000..86a06e0 --- /dev/null +++ b/Client2016/shaders/source/fabric.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 0.2 +#define CFG_GLOSS_SCALE 128 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.2 + +#define CFG_SPECULAR_LOD 0.03 +#define CFG_GLOSS_LOD 16 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#define CFG_OPT_BLEND_COLOR + +#include "material.hlsl" diff --git a/Client2016/shaders/source/gbuffer.hlsl b/Client2016/shaders/source/gbuffer.hlsl new file mode 100644 index 0000000..b3b710c --- /dev/null +++ b/Client2016/shaders/source/gbuffer.hlsl @@ -0,0 +1,52 @@ +#include "common.h" + +// .xy = gbuffer width/height, .zw = inverse gbuffer width/height +uniform float4 TextureSize; + +TEX_DECLARE2D(tex, 0); + +struct v2f +{ + float4 pos : POSITION; + float2 uv : TEXCOORD0; +}; + +#if defined(GLSL) || defined(DX11) +float4 convertPosition(float4 p) +{ + return p; +} +#else +float4 convertPosition(float4 p) +{ + // half-pixel offset + return p + float4(-TextureSize.z, TextureSize.w, 0, 0); +} +#endif + +#if defined(GLSL) +float2 convertUv(float4 p) +{ + return p.xy * 0.5 + 0.5; +} +#else +float2 convertUv(float4 p) +{ + return p.xy * float2(0.5, -0.5) + 0.5; +} +#endif + + +v2f gbufferVS( in float4 pos : POSITION ) +{ + v2f o; + o.pos = convertPosition(pos); + o.uv = convertUv(pos); + return o; +} + + +float4 gbufferPS( v2f i ) : COLOR0 +{ + return tex2D( tex, i.uv ); +} diff --git a/Client2016/shaders/source/globals.h b/Client2016/shaders/source/globals.h new file mode 100644 index 0000000..e4f3621 --- /dev/null +++ b/Client2016/shaders/source/globals.h @@ -0,0 +1,44 @@ +#ifndef GLSL +struct Globals +{ +#endif + float4x4 ViewProjection; + + float4 ViewRight; + float4 ViewUp; + float4 ViewDir; + float3 CameraPosition; + + float3 AmbientColor; + float3 Lamp0Color; + float3 Lamp0Dir; + float3 Lamp1Color; + + float3 FogColor; + float4 FogParams; + + float4 LightBorder; + float4 LightConfig0; + float4 LightConfig1; + float4 LightConfig2; + float4 LightConfig3; + + float4 FadeDistance_GlowFactor; + float4 OutlineBrightness_ShadowInfo; + + float4 ShadowMatrix0; + float4 ShadowMatrix1; + float4 ShadowMatrix2; +#ifndef GLSL +}; + +#ifdef DX11 +cbuffer Globals: register( b0 ) { Globals _G; }; +#else +uniform Globals _G: register(c0); +#endif + +#define G(x) _G.x +#else +#define G(x) x +#endif diff --git a/Client2016/shaders/source/granite.hlsl b/Client2016/shaders/source/granite.hlsl new file mode 100644 index 0000000..39541a7 --- /dev/null +++ b/Client2016/shaders/source/granite.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 0.5 +#define CFG_GLOSS_SCALE 128 +#define CFG_REFLECTION_SCALE 0.2 + +#define CFG_NORMAL_SHADOW_SCALE 0.1 + +#define CFG_SPECULAR_LOD 0.19 +#define CFG_GLOSS_LOD 24 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0.25 +#define CFG_FAR_DIFFUSE_CUTOFF 0.6 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#define CFG_OPT_NORMAL_CONST + +#include "material.hlsl" diff --git a/Client2016/shaders/source/grass.hlsl b/Client2016/shaders/source/grass.hlsl new file mode 100644 index 0000000..7b4480a --- /dev/null +++ b/Client2016/shaders/source/grass.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 1 +#define CFG_GLOSS_SCALE 256 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.5 + +#define CFG_SPECULAR_LOD 0.17 +#define CFG_GLOSS_LOD 18 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0.25 +#define CFG_FAR_DIFFUSE_CUTOFF 0.6 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#define CFG_OPT_BLEND_COLOR + +#include "material.hlsl" diff --git a/Client2016/shaders/source/ice.hlsl b/Client2016/shaders/source/ice.hlsl new file mode 100644 index 0000000..493698a --- /dev/null +++ b/Client2016/shaders/source/ice.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1.0 +#define CFG_SPECULAR_SCALE 1.2 +#define CFG_GLOSS_SCALE 256 +#define CFG_REFLECTION_SCALE 0.3 + +#define CFG_NORMAL_SHADOW_SCALE 0 + +#define CFG_SPECULAR_LOD 1 +#define CFG_GLOSS_LOD 190 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0.25 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0.75 + +#define CFG_OPT_DIFFUSE_CONST + +#include "material.hlsl" diff --git a/Client2016/shaders/source/marble.hlsl b/Client2016/shaders/source/marble.hlsl new file mode 100644 index 0000000..4447ca8 --- /dev/null +++ b/Client2016/shaders/source/marble.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 1.0 +#define CFG_GLOSS_SCALE 128 +#define CFG_REFLECTION_SCALE 0.2 + +#define CFG_NORMAL_SHADOW_SCALE 0.1 + +#define CFG_SPECULAR_LOD 0.7 +#define CFG_GLOSS_LOD 54 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#define CFG_OPT_NORMAL_CONST + +#include "material.hlsl" diff --git a/Client2016/shaders/source/material.hlsl b/Client2016/shaders/source/material.hlsl new file mode 100644 index 0000000..17dc3ed --- /dev/null +++ b/Client2016/shaders/source/material.hlsl @@ -0,0 +1,100 @@ +#define PIN_SURFACE +#include "default.hlsl" + +#ifndef CFG_WANG_TILES +float4 sampleFar(TEXTURE_IN_2D(s), float2 uv, float fade, float cutoff) +{ +#ifdef GLSLES + return tex2D(s, uv); +#else + if (cutoff == 0) + return tex2D(s, uv); + else + { + float cscale = 1 / (1 - cutoff); + + return lerp(tex2D(s, uv * (CFG_FAR_TILING) ), tex2D(s, uv ), saturate0(fade * cscale - cutoff * cscale)); + } +#endif +} +#endif + +Surface surfaceShader(SurfaceInput IN, float2 fade2) +{ +#ifdef CFG_WANG_TILES + float2 wangUv; + float4 wangUVDerivatives; + getWang(TEXTURE_WANG(NormalDetailMap), IN.Uv, CFG_TEXTURE_TILING, wangUv, wangUVDerivatives); +#endif + + float2 uv = IN.Uv * (CFG_TEXTURE_TILING); + + float fadeDiffuse = fade2.x; + float fade = fade2.y; + +#ifdef CFG_OPT_DIFFUSE_CONST + float4 diffuse = 1; +#else + + #ifdef CFG_WANG_TILES + float4 diffuse = sampleWang(TEXTURE(DiffuseMap), wangUv, wangUVDerivatives); + #else + float4 diffuse = sampleFar(TEXTURE(DiffuseMap), uv, fadeDiffuse, CFG_FAR_DIFFUSE_CUTOFF); + #endif + + diffuse.rgba = diffuse.rgba * (CFG_DIFFUSE_SCALE); + +#endif + +#ifdef CFG_OPT_NORMAL_CONST + float3 normal = float3(0, 0, 1); +#else + #ifdef CFG_WANG_TILES + float3 normal = nmapUnpack(sampleWang(TEXTURE(NormalMap), wangUv, wangUVDerivatives)); + #else + float3 normal = nmapUnpack(sampleFar(TEXTURE(NormalMap), uv, fade, CFG_FAR_NORMAL_CUTOFF)); + #endif +#endif + +#ifndef GLSLES + #ifndef CFG_WANG_TILES // normal detail unavailable when running wang tiles + float3 normalDetail = nmapUnpack(tex2D(NormalDetailMap, uv * (CFG_NORMAL_DETAIL_TILING))); + normal.xy += normalDetail.xy * (CFG_NORMAL_DETAIL_SCALE); + #endif +#endif + + normal.xy *= fade; + + float shadowFactor = 1 + normal.x * (CFG_NORMAL_SHADOW_SCALE); + +#ifdef CFG_OPT_BLEND_COLOR + float3 albedo = lerp(float3(1, 1, 1), IN.Color.rgb, diffuse.a) * diffuse.rgb * shadowFactor; +#else + float3 albedo = IN.Color.rgb * diffuse.rgb * shadowFactor; +#endif + +#ifndef GLSLES + float4 studs = tex2D(StudsMap, IN.UvStuds); + + albedo *= studs.r * 2; +#endif + +#ifdef CFG_WANG_TILES + float2 specular = sampleWang(TEXTURE(SpecularMap), wangUv, wangUVDerivatives).rg; +#else + float2 specular = sampleFar(TEXTURE(SpecularMap), uv, fade, CFG_FAR_SPECULAR_CUTOFF).rg; +#endif + + // make sure glossiness is never 0 to avoid fp specials + float2 specbase = specular * float2(CFG_SPECULAR_SCALE, CFG_GLOSS_SCALE) + float2(0, 0.01); + float2 specfade = lerp(float2(CFG_SPECULAR_LOD, CFG_GLOSS_LOD), specbase, fade); + + Surface surface = (Surface)0; + surface.albedo = albedo; + surface.normal = normal; + surface.specular = specfade.r; + surface.gloss = specfade.g; + surface.reflectance = specular.g * fade * (CFG_REFLECTION_SCALE); + + return surface; +} diff --git a/Client2016/shaders/source/megacluster.hlsl b/Client2016/shaders/source/megacluster.hlsl new file mode 100644 index 0000000..7b7d369 --- /dev/null +++ b/Client2016/shaders/source/megacluster.hlsl @@ -0,0 +1,151 @@ +#include "common.h" + +struct Appdata +{ + ATTR_INT4 Position : POSITION; + ATTR_INT3 Normal : NORMAL; + ATTR_INT4 Uv : TEXCOORD0; +#ifdef PIN_HQ + ATTR_INT4 EdgeDistances : TEXCOORD1; + ATTR_INT3 Tangent : TEXCOORD2; +#endif +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; + + float4 UvHigh_EdgeDistance1 : TEXCOORD0; + float4 UvLow_EdgeDistance2 : TEXCOORD1; + + float4 LightPosition_Fog : TEXCOORD2; + +#ifdef PIN_HQ + float4 View_Depth : TEXCOORD3; + float4 Normal_Blend : TEXCOORD4; + float3 Tangent : TEXCOORD5; +#else + float4 Diffuse_Blend : COLOR0; +#endif + + float3 PosLightSpace : TEXCOORD7; +}; + +WORLD_MATRIX(WorldMatrix); + +VertexOutput MegaClusterVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + // Decode vertex data + float3 Normal = (IN.Normal - 127.0) / 127.0; + float4 UV = IN.Uv / 2048.0; + + // Transform position and normal to world space + // Note: world matrix does not contain rotation/scale for static geometry so we can avoid transforming normal + float3 posWorld = mul(WorldMatrix, IN.Position).xyz; + float3 normalWorld = Normal; + + OUT.HPosition = mul(G(ViewProjection), float4(posWorld, 1)); + + float blend = OUT.HPosition.w / 200; + + OUT.LightPosition_Fog = float4(lgridPrepareSample(lgridOffset(posWorld, normalWorld)), (G(FogParams).z - OUT.HPosition.w) * G(FogParams).w); + + OUT.UvHigh_EdgeDistance1.xy = UV.xy; + OUT.UvLow_EdgeDistance2.xy = UV.zw; + +#ifdef PIN_HQ + OUT.View_Depth = float4(posWorld, OUT.HPosition.w * G(FadeDistance_GlowFactor).y); + float4 edgeDistances = IN.EdgeDistances*G(FadeDistance_GlowFactor).z + 0.5 * OUT.View_Depth.w; + + OUT.UvHigh_EdgeDistance1.zw = edgeDistances.xy; + OUT.UvLow_EdgeDistance2.zw = edgeDistances.zw; + + OUT.View_Depth.xyz = G(CameraPosition).xyz - posWorld; + OUT.Normal_Blend = float4(Normal, blend); + // decode tangent + OUT.Tangent = (IN.Tangent - 127.0) / 127.0; +#else + // IF LQ shading is performed in VS + float ndotl = dot(normalWorld, -G(Lamp0Dir)); + float3 diffuse = saturate(ndotl) * G(Lamp0Color) + max(-ndotl, 0) * G(Lamp1Color); + + OUT.Diffuse_Blend = float4(diffuse, blend); +#endif + + OUT.PosLightSpace = shadowPrepareSample(posWorld); + + return OUT; +} + +TEX_DECLARE2D(DiffuseHighMap, 0); +TEX_DECLARE2D(DiffuseLowMap, 1); +TEX_DECLARE2D(NormalMap, 2); +TEX_DECLARE2D(SpecularMap, 3); +LGRID_SAMPLER(LightMap, 4); +TEX_DECLARE2D(LightMapLookup, 5); +TEX_DECLARE2D(ShadowMap, 6); + +void MegaClusterPS(VertexOutput IN, +#ifdef PIN_GBUFFER + out float4 oColor1: COLOR1, +#endif + out float4 oColor0: COLOR0) +{ + float4 high = tex2D(DiffuseHighMap, IN.UvHigh_EdgeDistance1.xy); + float4 low = tex2D(DiffuseLowMap, IN.UvLow_EdgeDistance2.xy); + + float4 light = lgridSample(TEXTURE(LightMap), TEXTURE(LightMapLookup), IN.LightPosition_Fog.xyz); + float shadow = shadowSample(TEXTURE(ShadowMap), IN.PosLightSpace, light.a); + +#ifdef PIN_HQ + float3 albedo = lerp(high.rgb, low.rgb, saturate1(IN.Normal_Blend.a)); + + // sample normal map and specular map + float4 normalMapSample = tex2D(NormalMap, IN.UvHigh_EdgeDistance1.xy); + float4 specularMapSample = tex2D(SpecularMap, IN.UvHigh_EdgeDistance1.xy); + + // compute bitangent and world space normal + float3 bitangent = cross(IN.Normal_Blend.xyz, IN.Tangent.xyz); + float3 nmap = nmapUnpack(normalMapSample); + float3 normal = normalize(nmap.x * IN.Tangent.xyz + nmap.y * bitangent + nmap.z * IN.Normal_Blend.xyz); + + float ndotl = dot(normal, -G(Lamp0Dir)); + float3 diffuseIntensity = saturate0(ndotl) * G(Lamp0Color) + max(-ndotl, 0) * G(Lamp1Color); + float specularIntensity = step(0, ndotl) * specularMapSample.r; + float specularPower = specularMapSample.g * 255 + 0.01; + + // Compute diffuse and specular and combine them + float3 diffuse = (G(AmbientColor) + diffuseIntensity * shadow + light.rgb) * albedo.rgb; + float3 specular = G(Lamp0Color) * (specularIntensity * shadow * (float)(half)pow(saturate(dot(normal, normalize(-G(Lamp0Dir) + normalize(IN.View_Depth.xyz)))), specularPower)); + oColor0.rgb = diffuse + specular; + + // apply outlines + float outlineFade = saturate1(IN.View_Depth.w * G(OutlineBrightness_ShadowInfo).x + G(OutlineBrightness_ShadowInfo).y); + float2 minIntermediate = min(IN.UvHigh_EdgeDistance1.wz, IN.UvLow_EdgeDistance2.wz); + float minEdgesPlus = min(minIntermediate.x, minIntermediate.y) / IN.View_Depth.w; + oColor0.rgb *= saturate1(outlineFade * (1.5 - minEdgesPlus) + minEdgesPlus); + + oColor0.a = 1; + +#else + float3 albedo = lerp(high.rgb, low.rgb, saturate1(IN.Diffuse_Blend.a)); + + // Compute diffuse term + float3 diffuse = (G(AmbientColor) + IN.Diffuse_Blend.rgb * shadow + light.rgb) * albedo.rgb; + + // Combine + oColor0.rgb = diffuse; + oColor0.a = 1; + +#endif + + float fogAlpha = saturate(IN.LightPosition_Fog.w); + + oColor0.rgb = lerp(G(FogColor), oColor0.rgb, fogAlpha); + +#ifdef PIN_GBUFFER + oColor1 = gbufferPack(IN.View_Depth.w*G(FadeDistance_GlowFactor).x, diffuse.rgb, 0, fogAlpha); +#endif +} diff --git a/Client2016/shaders/source/metal.hlsl b/Client2016/shaders/source/metal.hlsl new file mode 100644 index 0000000..ad92d37 --- /dev/null +++ b/Client2016/shaders/source/metal.hlsl @@ -0,0 +1,21 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 2 +#define CFG_GLOSS_SCALE 256 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.3 + +#define CFG_SPECULAR_LOD 0.8 +#define CFG_GLOSS_LOD 120 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0.25 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0.75 + +#include "material.hlsl" diff --git a/Client2016/shaders/source/neon.hlsl b/Client2016/shaders/source/neon.hlsl new file mode 100644 index 0000000..60c7186 --- /dev/null +++ b/Client2016/shaders/source/neon.hlsl @@ -0,0 +1,3 @@ +#define PIN_PLASTIC +#define PIN_NEON +#include "default.hlsl" diff --git a/Client2016/shaders/source/particle.hlsl b/Client2016/shaders/source/particle.hlsl new file mode 100644 index 0000000..367ef8f --- /dev/null +++ b/Client2016/shaders/source/particle.hlsl @@ -0,0 +1,240 @@ +#include "common.h" + +TEX_DECLARE2D(tex, 0); +TEX_DECLARE2D(cstrip, 1); +TEX_DECLARE2D(astrip, 2); + +uniform float4 throttleFactor; // .x = alpha cutoff, .y = alpha boost (clamp), .w - additive/alpha ratio for Crazy shaders +uniform float4 modulateColor; +uniform float4 zOffset; + +struct VS_INPUT +{ + float4 pos : POSITION; + ATTR_INT4 scaleRotLife : TEXCOORD0; // transform matrix + ATTR_INT2 disp : TEXCOORD1; // .xy = corner, either (0,0), (1,0), (0,1), or (1,1) + ATTR_INT2 cline: TEXCOORD2; // .x = color line [0...32767] +}; + +struct VS_OUTPUT +{ + float4 pos : POSITION; + float3 uvFog : TEXCOORD0; + float2 colorLookup : TEXCOORD1; +}; + +float4 rotScale( float4 scaleRotLife ) +{ + float cr = cos( scaleRotLife.z ); + float sr = sin( scaleRotLife.z ); + + float4 r; + r.x = cr * scaleRotLife.x; + r.y = -sr * scaleRotLife.x; + r.z = sr * scaleRotLife.y; + r.w = cr * scaleRotLife.y; + + return r; +} + +float4 mulq( float4 a, float4 b ) +{ + float3 i = cross( a.xyz, b.xyz ) + a.w * b.xyz + b.w * a.xyz; + float r = a.w * b.w - dot( a.xyz, b.xyz ); + return float4( i, r ); +} + +float4 conj( float4 a ) { return float4( -a.xyz, a.w ); } + +float4 rotate( float4 v, float4 q ) +{ + return mulq( mulq( q, v ), conj( q ) ); +} + +float4 axis_angle( float3 axis, float angle ) +{ + return float4( sin(angle/2) * axis, cos(angle/2) ); +} + +VS_OUTPUT vs( VS_INPUT input ) +{ + VS_OUTPUT o; + + float4 pos = float4( input.pos.xyz, 1 ); + float2 disp = input.disp.xy * 2 - 1; // -1..1 + + float4 scaleRotLifeFlt = (float4)input.scaleRotLife * float4( 1.0f/256.0f, 1.0f/256.0f, 2.0 * 3.1415926f / 32767.0f, 1.0f / 32767.0f ); + scaleRotLifeFlt.xy += 127.0f; + + float4 rs = rotScale( scaleRotLifeFlt ); + + pos += G(ViewRight) * dot( disp, rs.xy ); + pos += G(ViewUp) * dot( disp, rs.zw ); + + float4 pos2 = pos + G(ViewDir)*zOffset.x; // Z-offset position in world space + + o.pos = mul( G(ViewProjection), pos ); + + o.uvFog.xy = input.disp.xy; + o.uvFog.y = 1 - o.uvFog.y; + o.uvFog.z = (G(FogParams).z - o.pos.w) * G(FogParams).w; + + o.colorLookup.x = 1 - max( 0, min(1, scaleRotLifeFlt.w ) ); + o.colorLookup.y = (float)input.cline.x * (1.0 / 32767.0f); + + + pos2 = mul( G(ViewProjection), pos2 ); // Z-offset position in clip space + o.pos.z = pos2.z * o.pos.w/pos2.w; // Only need z + + + return o; +} + + +float4 psAdd( VS_OUTPUT input ) : COLOR0 // #0 +{ + float4 texcolor = tex2D( tex, input.uvFog.xy ); + float4 vcolor = tex2D( cstrip, input.colorLookup.xy ); + vcolor.a = tex2D( astrip, input.colorLookup.xy ).r; + + float4 result; + + result.rgb = (texcolor.rgb + vcolor.rgb) * modulateColor.rgb; + result.a = texcolor.a * vcolor.a; + result.rgb *= result.a; + + result.rgb = lerp( 0.0f.xxx, result.rgb, saturate( input.uvFog.zzz ) ); + return result; +} + +float4 psModulate( VS_OUTPUT input ) : COLOR0 // #1 +{ + + float4 texcolor = tex2D( tex, input.uvFog.xy ); + float4 vcolor = tex2D( cstrip, input.colorLookup.xy ) * modulateColor; + vcolor.a = tex2D( astrip, input.colorLookup.xy ).r * modulateColor.a; + + float4 result; + + result.rgb = texcolor.rgb * vcolor.rgb; + result.a = texcolor.a * vcolor.a; + + result.rgb = lerp( G(FogColor).rgb, result.rgb, saturate( input.uvFog.zzz ) ); + return result; +} + + +// - this shader is crazy +// - used instead of additive particles to help see bright particles (e.g. fire) on top of extremely bright backgrounds +// - requires ONE | INVSRCALPHA blend mode, useless otherwise +// - does not use color strip texture +// - outputs a blend between additive blend and alpha blend in fragment alpha +// - ratio multiplier is in throttleFactor.w +float4 psCrazy( VS_OUTPUT input ) : COLOR0 +{ + float4 texcolor = tex2D( tex, input.uvFog.xy ); + float4 vcolor = float4(1,0,0,0); //tex2D( cstrip, input.colorLookup.xy ); // not actually used + vcolor.a = tex2D( astrip, input.colorLookup.xy ).r; + float blendRatio = throttleFactor.w; // yeah yeah + + float4 result; + + result.rgb = (texcolor.rgb ) * modulateColor.rgb * vcolor.a * texcolor.a; + result.a = blendRatio * texcolor.a * vcolor.a; + + result = lerp( 0.0f.xxxx, result, saturate( input.uvFog.zzzz ) ); + return result; +} + +float4 psCrazySparkles( VS_OUTPUT input ) : COLOR0 +{ + float4 texcolor = tex2D( tex, input.uvFog.xy ); + float4 vcolor = tex2D( cstrip, input.colorLookup.xy ); + vcolor.a = tex2D( astrip, input.colorLookup.xy ).r; + float blendRatio = throttleFactor.w; + + float4 result; + + if( texcolor.a < 0.5f ) + { + result.rgb = vcolor.rgb * modulateColor.rgb * (2 * texcolor.a); + } + else + { + result.rgb = lerp( vcolor.rgb * modulateColor.rgb, texcolor.rgb, 2*texcolor.a-1 ); + } + + //vcolor.a *= modulateColor.a; + result.rgb *= vcolor.a; + result.a = blendRatio * texcolor.a * vcolor.a; + + result = lerp( 0.0f.xxxx, result, saturate( input.uvFog.zzzz ) ); + return result; +} + + +/////////////////////////////////////////////////////////////////////////////////// + +struct VS_INPUT2 +{ + float4 pos : POSITION; + ATTR_INT4 scaleRotLife : TEXCOORD0; // transform matrix + ATTR_INT2 disp : TEXCOORD1; // .xy = corner, either (0,0), (1,0), (0,1), or (1,1) + ATTR_INT2 cline: TEXCOORD2; // .x = color line [0...32767] + ATTR_INT4 color: TEXCOORD3; // .xyzw +}; + +struct VS_OUTPUT2 +{ + float4 pos : POSITION; + float3 uvFog : TEXCOORD0; + float4 color : TEXCOORD1; +}; + +VS_OUTPUT2 vsCustom( VS_INPUT2 input ) +{ + VS_OUTPUT2 o; + + float4 pos = input.pos; + float2 disp = input.disp.xy * 2 - 1; // -1..1 + + float4 scaleRotLifeFlt = (float4)input.scaleRotLife * float4( 1.0/256.0f, 1.0/256.0f, 2.0 * 3.1415926f / 32767.0, 1.0 / 32767.0f ); + scaleRotLifeFlt.xy += 127.0f; + + float4 rs = rotScale( scaleRotLifeFlt ); + + pos += G(ViewRight) * dot( disp, rs.xy ); + pos += G(ViewUp) * dot( disp, rs.zw ); + + float4 pos2 = pos + G(ViewDir)*zOffset.x; // Z-offset position in world space + + o.pos = mul( G(ViewProjection), pos ); + + o.uvFog.xy = input.disp.xy; + o.uvFog.y = 1 - o.uvFog.y; + o.uvFog.z = (G(FogParams).z - o.pos.w) * G(FogParams).w; + + o.color = input.color * (1/255.0f); + + pos2 = mul( G(ViewProjection), pos2 ); // Z-offset position in clip space + o.pos.z = pos2.z * o.pos.w/pos2.w; // Only need z + + + return o; +} + +float4 psCustom( VS_OUTPUT2 input ) : COLOR0 // #1 +{ + float4 texcolor = tex2D( tex, input.uvFog.xy ); + float4 vcolor = input.color; + + float blendRatio = throttleFactor.w; // yeah yeah + + float4 result; + + result.rgb = texcolor.rgb * vcolor.rgb * vcolor.a * texcolor.a; + result.a = blendRatio * texcolor.a * vcolor.a; + + result = lerp( 0.0f.xxxx, result, saturate( input.uvFog.zzzz ) ); + return result; +} diff --git a/Client2016/shaders/source/pebble.hlsl b/Client2016/shaders/source/pebble.hlsl new file mode 100644 index 0000000..9e35fe9 --- /dev/null +++ b/Client2016/shaders/source/pebble.hlsl @@ -0,0 +1,21 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 2.5 +#define CFG_GLOSS_SCALE 128 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0 + +#define CFG_SPECULAR_LOD 0.15 +#define CFG_GLOSS_LOD 22 + +#define CFG_NORMAL_DETAIL_TILING 6 +#define CFG_NORMAL_DETAIL_SCALE 1.5 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#include "material.hlsl" diff --git a/Client2016/shaders/source/plastic.hlsl b/Client2016/shaders/source/plastic.hlsl new file mode 100644 index 0000000..905a540 --- /dev/null +++ b/Client2016/shaders/source/plastic.hlsl @@ -0,0 +1,52 @@ +#if defined(PIN_HQ) +#define PIN_SURFACE +#include "default.hlsl" + +#define CFG_TEXTURE_TILING 1 + +#define CFG_BUMP_INTENSITY 0.5 + +#define CFG_SPECULAR 0.4 +#define CFG_GLOSS 9 + +#define CFG_NORMAL_SHADOW_SCALE 0.1 + +Surface surfaceShader(SurfaceInput IN, float2 fade2) +{ + float fade = fade2.y; + + float4 studs = tex2D(DiffuseMap, IN.UvStuds); + float3 normal = nmapUnpack(tex2D(NormalMap, IN.UvStuds)); + +#ifdef GLSLES + float3 noise = float3(0, 0, 1); +#else + float3 noise = nmapUnpack(tex2D(NormalDetailMap, IN.Uv * (CFG_TEXTURE_TILING))); +#endif + + float noiseScale = saturate0(IN.Color.a * 2 * (CFG_BUMP_INTENSITY) - 1 * (CFG_BUMP_INTENSITY)); + +#ifdef PIN_REFLECTION + noiseScale *= saturate(1 - 2 * IN.Reflectance); +#endif + + normal.xy += noise.xy * noiseScale; + + normal.xy *= fade; + + Surface surface = (Surface)0; + surface.albedo = IN.Color.rgb * (studs.r * 2); + surface.normal = normal; + surface.specular = (CFG_SPECULAR); + surface.gloss = (CFG_GLOSS); + +#ifdef PIN_REFLECTION + surface.reflectance = IN.Reflectance; +#endif + + return surface; +} +#else +#define PIN_PLASTIC +#include "default.hlsl" +#endif diff --git a/Client2016/shaders/source/profiler.hlsl b/Client2016/shaders/source/profiler.hlsl new file mode 100644 index 0000000..ec6001f --- /dev/null +++ b/Client2016/shaders/source/profiler.hlsl @@ -0,0 +1,40 @@ +#include "common.h" + + + +struct Appdata +{ + float4 Position : POSITION; + float2 Uv : TEXCOORD0; + float4 Color : COLOR0; +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; + float2 Uv : TEXCOORD0; + float4 Color : COLOR0; +}; + +VertexOutput ProfilerVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + OUT.HPosition = mul(G(ViewProjection), IN.Position); + OUT.HPosition.y = -OUT.HPosition.y; + + OUT.Uv = IN.Uv; + OUT.Color = IN.Color; + + return OUT; +} + +TEX_DECLARE2D(DiffuseMap, 0); + +float4 ProfilerPS(VertexOutput IN): COLOR0 +{ + float4 c0 = tex2D(DiffuseMap, IN.Uv); + float4 c1 = tex2D(DiffuseMap, IN.Uv + float2(0, 1.f / 9.f)); + + return c0.a < 0.5 ? float4(0, 0, 0, c1.a) : c0 * IN.Color; +} diff --git a/Client2016/shaders/source/rust.hlsl b/Client2016/shaders/source/rust.hlsl new file mode 100644 index 0000000..9be4823 --- /dev/null +++ b/Client2016/shaders/source/rust.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 1 +#define CFG_GLOSS_SCALE 256 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.5 + +#define CFG_SPECULAR_LOD 0.35 +#define CFG_GLOSS_LOD 103 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0.5 +#define CFG_FAR_DIFFUSE_CUTOFF 0.6 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#define CFG_OPT_BLEND_COLOR + +#include "material.hlsl" diff --git a/Client2016/shaders/source/sand.hlsl b/Client2016/shaders/source/sand.hlsl new file mode 100644 index 0000000..f64f6c1 --- /dev/null +++ b/Client2016/shaders/source/sand.hlsl @@ -0,0 +1,21 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 0.4 +#define CFG_GLOSS_SCALE 32 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0 + +#define CFG_SPECULAR_LOD 0.07 +#define CFG_GLOSS_LOD 6 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#include "material.hlsl" diff --git a/Client2016/shaders/source/screenspace.hlsl b/Client2016/shaders/source/screenspace.hlsl new file mode 100644 index 0000000..825c8ba --- /dev/null +++ b/Client2016/shaders/source/screenspace.hlsl @@ -0,0 +1,211 @@ +#include "common.h" + +TEX_DECLARE2D(Texture, 0); +TEX_DECLARE2D(Mask, 1); + +// .xy = gbuffer width/height, .zw = inverse gbuffer width/height +uniform float4 TextureSize; +uniform float4 Params1; +uniform float4 Params2; + +#if defined(GLSL) || defined(DX11) +float4 convertPosition(float4 p, float scale) +{ + return p; +} +#else +float4 convertPosition(float4 p, float scale) +{ + // half-pixel offset + return p + float4(-TextureSize.z, TextureSize.w, 0, 0) * scale; +} +#endif + +#ifndef GLSL +float2 convertUv(float4 p) +{ + return p.xy * float2(0.5, -0.5) + 0.5; +} +#else +float2 convertUv(float4 p) +{ + return p.xy * 0.5 + 0.5; +} +#endif + + +// simple pass through structure +struct VertexOutput +{ + float4 p : POSITION; + float2 uv : TEXCOORD0; +}; + +// position and tex coord + 4 additional tex coords +struct VertexOutput_4uv +{ + float4 p : POSITION; + float2 uv : TEXCOORD0; + float4 uv12 : TEXCOORD1; + float4 uv34 : TEXCOORD2; +}; + +// position and tex coord + 8 additional tex coords +struct VertexOutput_8uv +{ + float4 p : POSITION; + float2 uv : TEXCOORD0; + float4 uv12 : TEXCOORD1; + float4 uv34 : TEXCOORD2; + float4 uv56 : TEXCOORD3; + float4 uv78 : TEXCOORD4; +}; + +VertexOutput passThrough_vs(float4 p: POSITION) +{ + VertexOutput OUT; + OUT.p = convertPosition(p, 1); + OUT.uv = convertUv(p); + + return OUT; +} + +float4 passThrough_ps( VertexOutput IN ) : COLOR0 +{ + return tex2D(Texture, IN.uv); +} + +VertexOutput_4uv downsample4x4_vs(float4 p: POSITION) +{ + float2 uv = convertUv(p); + + VertexOutput_4uv OUT; + OUT.p = convertPosition(p, 1); + OUT.uv = uv; + + float2 uvOffset = TextureSize.zw * 0.25f; + + OUT.uv12.xy = uv + uvOffset * float2(-1, -1); + OUT.uv12.zw = uv + uvOffset * float2(+1, -1); + OUT.uv34.xy = uv + uvOffset * float2(-1, +1); + OUT.uv34.zw = uv + uvOffset * float2(+1, +1); + + return OUT; +} + +float4 imageProcess_ps( VertexOutput IN ) : COLOR0 +{ + float3 color = tex2D(Texture, IN.uv).rgb; + + float4 tintColor = float4(Params2.xyz,1); + //float4 tintColor = float4(18.0 / 255.0, 58.0 / 255.0, 80.0 / 255.0, 1); + float contrast = Params1.y; + float brightness = Params1.x; + float grayscaleLvl = Params1.z; + + color = contrast*(color - 0.5) + 0.5 + brightness; + float grayscale = (color.r + color.g + color.g) / 3.0; + + return lerp(float4(color.rgb,1), float4(grayscale.xxx,1), grayscaleLvl) * tintColor; +} + +float4 gauss(float samples, float2 uv) +{ + float2 step = Params1.xy; + float sigma = Params1.z; + + float sigmaN1 = 1 / sqrt(2 * 3.1415926 * sigma * sigma); + float sigmaN2 = 1 / (2 * sigma * sigma); + + // First sample is in the center and accounts for our pixel + float4 result = tex2D(Texture, uv) * sigmaN1; + float weight = sigmaN1; + + // Every loop iteration computes impact of 4 pixels + // Each sample computes impact of 2 neighbor pixels, starting with the next one to the right + // Note that we sample exactly in between pixels to leverage bilinear filtering + for (int i = 0; i < samples; ++i) + { + float ix = 2 * i + 1.5; + float iw = 2 * exp(-ix * ix * sigmaN2) * sigmaN1; + + result += (tex2D(Texture, uv + step * ix) + tex2D(Texture, uv - step * ix)) * iw; + weight += 2 * iw; + } + + // Since the above is an approximation of the integral with step functions, normalization compensates for the error + return (result / weight); +} + + + +float4 blur3_ps(VertexOutput IN): COLOR0 +{ + return gauss(3, IN.uv); +} + +float4 blur5_ps(VertexOutput IN): COLOR0 +{ + return gauss(5, IN.uv); +} + +float4 blur7_ps(VertexOutput IN): COLOR0 +{ + return gauss(7, IN.uv); +} + +float4 glowApply_ps( VertexOutput IN ) : COLOR0 +{ + float4 color = tex2D(Texture, IN.uv); + return float4(color.rgb * Params1.x, color.a); +} + +// this is specific glow downsample +float4 downSample4x4Glow_ps( VertexOutput_4uv IN ) : COLOR0 +{ + float4 avgColor = tex2D( Texture, IN.uv12.xy ); + avgColor += tex2D( Texture, IN.uv12.zw ); + avgColor += tex2D( Texture, IN.uv34.xy ); + avgColor += tex2D( Texture, IN.uv34.zw ); + + avgColor *= 0.25; + return float4(avgColor.rgb, 1) * (1-avgColor.a); +} + +float4 ShadowBlurPS(VertexOutput IN): COLOR0 +{ +#ifdef GLSLES + int N = 1; + float sigma = 0.5; +#else + int N = 3; + float sigma = 1.5; +#endif + + float2 step = Params1.xy; + + float sigmaN1 = 1 / sqrt(2 * 3.1415926 * sigma * sigma); + float sigmaN2 = 1 / (2 * sigma * sigma); + + float depth = 1; + float color = 0; + float weight = 0; + + for (int i = -N; i <= N; ++i) + { + float ix = i; + float iw = exp(-ix * ix * sigmaN2) * sigmaN1; + + float4 data = tex2D(Texture, IN.uv + step * ix); + + depth = min(depth, data.x); + color += data.y * iw; + weight += iw; + } + + float mask = tex2D(Mask, IN.uv).r; + + // Since the above is an approximation of the integral with step functions, normalization compensates for the error + return float4(depth, color * mask * (1 / weight), 0, 0); +} + diff --git a/Client2016/shaders/source/shadow.hlsl b/Client2016/shaders/source/shadow.hlsl new file mode 100644 index 0000000..f691f02 --- /dev/null +++ b/Client2016/shaders/source/shadow.hlsl @@ -0,0 +1,50 @@ +#include "common.h" + +struct Appdata +{ + float4 Position : POSITION; + + ATTR_INT4 Extra : COLOR1; +}; + +struct VertexOutput +{ + float4 HPosition: POSITION; + + float3 PosLightSpace: TEXCOORD0; +}; + +#ifdef PIN_SKINNED + WORLD_MATRIX_ARRAY(WorldMatrixArray, MAX_BONE_COUNT * 3); +#endif + +VertexOutput ShadowVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + // Transform position to world space +#ifdef PIN_SKINNED + int boneIndex = IN.Extra.r; + + float4 worldRow0 = WorldMatrixArray[boneIndex * 3 + 0]; + float4 worldRow1 = WorldMatrixArray[boneIndex * 3 + 1]; + float4 worldRow2 = WorldMatrixArray[boneIndex * 3 + 2]; + + float3 posWorld = float3(dot(worldRow0, IN.Position), dot(worldRow1, IN.Position), dot(worldRow2, IN.Position)); +#else + float3 posWorld = IN.Position.xyz; +#endif + + OUT.HPosition = mul(G(ViewProjection), float4(posWorld, 1)); + + OUT.PosLightSpace = shadowPrepareSample(posWorld); + + return OUT; +} + +float4 ShadowPS(VertexOutput IN): COLOR0 +{ + float depth = shadowDepth(IN.PosLightSpace); + + return float4(depth, 1, 0, 0); +} diff --git a/Client2016/shaders/source/sky.hlsl b/Client2016/shaders/source/sky.hlsl new file mode 100644 index 0000000..05b9708 --- /dev/null +++ b/Client2016/shaders/source/sky.hlsl @@ -0,0 +1,53 @@ +#include "common.h" + +struct Appdata +{ + float4 Position : POSITION; + float2 Uv : TEXCOORD0; + float4 Color : COLOR0; +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; + float PSize : PSIZE; + + float2 Uv : TEXCOORD0; + float4 Color : COLOR0; +}; + +WORLD_MATRIX(WorldMatrix); + +uniform float4 Color; +uniform float4 Color2; + +VertexOutput SkyVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + float4 wpos = mul(WorldMatrix, IN.Position); + + OUT.HPosition = mul(G(ViewProjection), wpos); + +#ifndef GLSLES + // snap to far plane to prevent scene-sky intersections + // small offset is needed to prevent 0/0 division in case w=0, which causes rasterization issues + // some mobile chips (hello, Vivante!) don't like it + OUT.HPosition.z = OUT.HPosition.w - 1.f / 16; +#endif + + OUT.PSize = 2.0; // star size + + OUT.Uv = IN.Uv; + OUT.Color = IN.Color * lerp(Color2,Color,wpos.y/1700); + //OUT.Color = IN.Color * Color; + + return OUT; +} + +TEX_DECLARE2D(DiffuseMap, 0); + +float4 SkyPS(VertexOutput IN): COLOR0 +{ + return tex2D(DiffuseMap, IN.Uv) * IN.Color; +} diff --git a/Client2016/shaders/source/slate.hlsl b/Client2016/shaders/source/slate.hlsl new file mode 100644 index 0000000..cd135ba --- /dev/null +++ b/Client2016/shaders/source/slate.hlsl @@ -0,0 +1,21 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 0.9 +#define CFG_GLOSS_SCALE 128 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.5 + +#define CFG_SPECULAR_LOD 0.14 +#define CFG_GLOSS_LOD 20 + +#define CFG_NORMAL_DETAIL_TILING 5 +#define CFG_NORMAL_DETAIL_SCALE 1 + +#define CFG_FAR_TILING 0.25 +#define CFG_FAR_DIFFUSE_CUTOFF 0.75 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#include "material.hlsl" diff --git a/Client2016/shaders/source/smoothcluster.hlsl b/Client2016/shaders/source/smoothcluster.hlsl new file mode 100644 index 0000000..6929d5d --- /dev/null +++ b/Client2016/shaders/source/smoothcluster.hlsl @@ -0,0 +1,174 @@ +#include "common.h" + +struct Appdata +{ + ATTR_INT4 Position : POSITION; + + ATTR_INT4 Normal : NORMAL; + + ATTR_INT4 Material0 : TEXCOORD0; + ATTR_INT4 Material1 : TEXCOORD1; +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; + + float3 Weights: COLOR0; + + float4 Uv0: TEXCOORD0; + float4 Uv1: TEXCOORD1; + float4 Uv2: TEXCOORD2; + + float4 LightPosition_Fog : TEXCOORD3; + float3 PosLightSpace : TEXCOORD4; + +#ifdef PIN_HQ + float3 Normal: TEXCOORD5; + float4 View_Depth: TEXCOORD6; + + float3 Tangents: COLOR1; +#else + float3 Diffuse: COLOR1; +#endif +}; + +WORLD_MATRIX_ARRAY(WorldMatrixArray, 72); + +uniform float4 LayerScale; + +float4 getUV(float3 position, ATTR_INT material, ATTR_INT projection, float seed) +{ + float3 u = WorldMatrixArray[1 + int(projection)].xyz; + float3 v = WorldMatrixArray[19 + int(projection)].xyz; + + float4 m = WorldMatrixArray[37 + int(material)]; + + float2 uv = float2(dot(position, u), dot(position, v)) * m.x + m.y * float2(seed, floor(seed * 2.6651441f)); + + return float4(uv, m.zw); +} + +VertexOutput TerrainVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + float3 posWorld = IN.Position.xyz * WorldMatrixArray[0].w + WorldMatrixArray[0].xyz; + float3 normalWorld = IN.Normal.xyz * (1.0 / 127.0) - 1.0; + + OUT.HPosition = mul(G(ViewProjection), float4(posWorld, 1)); + + OUT.LightPosition_Fog = float4(lgridPrepareSample(lgridOffset(posWorld, normalWorld)), (G(FogParams).z - OUT.HPosition.w) * G(FogParams).w); + + OUT.PosLightSpace = shadowPrepareSample(posWorld); + + OUT.Uv0 = getUV(posWorld, IN.Material0.x, IN.Material1.x, IN.Normal.w); + OUT.Uv1 = getUV(posWorld, IN.Material0.y, IN.Material1.y, IN.Material0.w); + OUT.Uv2 = getUV(posWorld, IN.Material0.z, IN.Material1.z, IN.Material1.w); + +#if defined(GLSLES) && !defined(GL3) // iPad2 workaround + OUT.Weights = abs(IN.Position.www - float3(0, 1, 2)) < 0.1; +#else + OUT.Weights = IN.Position.www == float3(0, 1, 2); +#endif + +#ifdef PIN_HQ + OUT.Normal = normalWorld; + OUT.View_Depth = float4(G(CameraPosition) - posWorld, OUT.HPosition.w); + OUT.Tangents = float3(IN.Material1.xyz) > 7.5; // side vs top +#else + float ndotl = dot(normalWorld, -G(Lamp0Dir)); + float3 diffuse = max(ndotl, 0) * G(Lamp0Color) + max(-ndotl, 0) * G(Lamp1Color); + + OUT.Diffuse = diffuse; +#endif + + return OUT; +} + +TEX_DECLARE2D(AlbedoMap, 0); +TEX_DECLARE2D(NormalMap, 1); +TEX_DECLARE2D(SpecularMap, 2); +TEX_DECLARECUBE(EnvMap, 3); +LGRID_SAMPLER(LightMap, 4); +TEX_DECLARE2D(LightMapLookup, 5); +TEX_DECLARE2D(ShadowMap, 6); + +float4 sampleMap(TEXTURE_IN_2D(s), float4 uv) +{ +#ifdef PIN_HQ + float2 uvs = uv.xy * LayerScale.xy; + + return tex2Dgrad(s, frac(uv.xy) * LayerScale.xy + uv.zw, ddx(uvs), ddy(uvs)); +#else + return tex2D(s, frac(uv.xy) * LayerScale.xy + uv.zw); +#endif +} + +float4 sampleBlend(TEXTURE_IN_2D(s), float4 uv0, float4 uv1, float4 uv2, float3 w) +{ + return + sampleMap(TEXTURE(s), uv0) * w.x + + sampleMap(TEXTURE(s), uv1) * w.y + + sampleMap(TEXTURE(s), uv2) * w.z; +} + +float3 sampleNormal(TEXTURE_IN_2D(s), float4 uv0, float4 uv1, float4 uv2, float3 w, float3 normal, float3 tsel) +{ + return terrainNormal(sampleMap(TEXTURE(s), uv0), sampleMap(TEXTURE(s), uv1), sampleMap(TEXTURE(s), uv2), w, normal, tsel); +} + +void TerrainPS(VertexOutput IN, +#ifdef PIN_GBUFFER + out float4 oColor1: COLOR1, +#endif + out float4 oColor0: COLOR0) +{ + float4 light = lgridSample(TEXTURE(LightMap), TEXTURE(LightMapLookup), IN.LightPosition_Fog.xyz); + float shadow = shadowSample(TEXTURE(ShadowMap), IN.PosLightSpace, light.a); + + float3 w = IN.Weights.xyz; + + float4 albedo = sampleBlend(TEXTURE(AlbedoMap), IN.Uv0, IN.Uv1, IN.Uv2, w); + +#ifdef PIN_HQ + float fade = saturate0(1 - IN.View_Depth.w * G(FadeDistance_GlowFactor).y); + +#ifndef PIN_GBUFFER + float3 normal = IN.Normal; +#else + float3 normal = sampleNormal(TEXTURE(NormalMap), IN.Uv0, IN.Uv1, IN.Uv2, w, IN.Normal, IN.Tangents); +#endif + + float4 params = sampleBlend(TEXTURE(SpecularMap), IN.Uv0, IN.Uv1, IN.Uv2, w); + + float ndotl = dot(normal, -G(Lamp0Dir)); + + // Compute diffuse term + float3 diffuse = (G(AmbientColor) + (saturate(ndotl) * G(Lamp0Color) + max(-ndotl, 0) * G(Lamp1Color)) * shadow + light.rgb + params.b * 2) * albedo.rgb; + + // Compute specular term + float specularIntensity = step(0, ndotl) * params.r * fade; + float specularPower = params.g * 128 + 0.01; + + float3 specular = G(Lamp0Color) * (specularIntensity * shadow * (float)(half)pow(saturate(dot(normal, normalize(-G(Lamp0Dir) + normalize(IN.View_Depth.xyz)))), specularPower)); +#else + // Compute diffuse term + float3 diffuse = (G(AmbientColor) + IN.Diffuse * shadow + light.rgb) * albedo.rgb; + + // Compute specular term + float3 specular = 0; +#endif + + // Combine + oColor0.rgb = diffuse + specular; + oColor0.a = 1; + + float fogAlpha = saturate(IN.LightPosition_Fog.w); + + oColor0.rgb = lerp(G(FogColor), oColor0.rgb, fogAlpha); + +#ifdef PIN_GBUFFER + oColor1 = gbufferPack(IN.View_Depth.w, diffuse.rgb, specular.rgb, fogAlpha); +#endif +} diff --git a/Client2016/shaders/source/smoothplastic.hlsl b/Client2016/shaders/source/smoothplastic.hlsl new file mode 100644 index 0000000..fb0aa90 --- /dev/null +++ b/Client2016/shaders/source/smoothplastic.hlsl @@ -0,0 +1,2 @@ +#define PIN_PLASTIC +#include "default.hlsl" diff --git a/Client2016/shaders/source/smoothwater.hlsl b/Client2016/shaders/source/smoothwater.hlsl new file mode 100644 index 0000000..4bee008 --- /dev/null +++ b/Client2016/shaders/source/smoothwater.hlsl @@ -0,0 +1,310 @@ +#include "common.h" + +// Tunables +#define CFG_TEXTURE_TILING 0.2 +#define CFG_TEXTURE_DETILING 0.1 + +#define CFG_SPECULAR 2 +#define CFG_GLOSS 900 + +#define CFG_NORMAL_STRENGTH 0.25 + +#define CFG_REFRACTION_STRENGTH 0.05 + +#define CFG_FRESNEL_OFFSET 0.3 + +#define CFG_SSR_STEPS 8 +#define CFG_SSR_START_DISTANCE 1 +#define CFG_SSR_STEP_CLAMP 0.2 +#define CFG_SSR_DEPTH_CUTOFF 10 + +// Shader code +struct Appdata +{ + ATTR_INT4 Position : POSITION; + + ATTR_INT4 Normal : NORMAL; + + ATTR_INT4 Material0 : TEXCOORD0; + ATTR_INT4 Material1 : TEXCOORD1; +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; + + float4 Weights_Wave: COLOR0; + float3 Tangents: COLOR1; + + float2 Uv0: TEXCOORD0; + float2 Uv1: TEXCOORD1; + float2 Uv2: TEXCOORD2; + + float4 LightPosition_Fog : TEXCOORD3; + + float3 Normal: TEXCOORD4; + float4 View_Depth: TEXCOORD5; + +#ifdef PIN_HQ + float4 PositionScreen: TEXCOORD6; +#endif +}; + +WORLD_MATRIX_ARRAY(WorldMatrixArray, 72); + +uniform float4 WaveParams; // .x = frequency .y = phase .z = height .w = lerp +uniform float4 WaterColor; // deep water color +uniform float4 WaterParams; // .x = refraction depth scale, .y = refraction depth offset + +float3 displacePosition(float3 position, float waveFactor) +{ + float x = sin((position.z - position.x) * WaveParams.x - WaveParams.y); + float z = sin((position.z + position.x) * WaveParams.x + WaveParams.y); + float p = (x + z) * WaveParams.z; + + float3 result = position; + + result.y += p * waveFactor; + + return result; +} + +float4 clipToScreen(float4 pos) +{ +#ifdef GLSL + pos.xy = pos.xy * 0.5 + 0.5 * pos.w; +#else + pos.xy = pos.xy * float2(0.5, -0.5) + 0.5 * pos.w; +#endif + return pos; +} + +float2 getUV(float3 position, ATTR_INT projection, float seed) +{ + float3 u = WorldMatrixArray[1 + int(projection)].xyz; + float3 v = WorldMatrixArray[19 + int(projection)].xyz; + + float2 uv = float2(dot(position, u), dot(position, v)) * (0.25 * CFG_TEXTURE_TILING) + CFG_TEXTURE_DETILING * float2(seed, floor(seed * 2.6651441f)); + + return uv; +} + +VertexOutput WaterVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + float3 posWorld = IN.Position.xyz * WorldMatrixArray[0].w + WorldMatrixArray[0].xyz; + float3 normalWorld = IN.Normal.xyz * (1.0 / 127.0) - 1.0; + +#if defined(GLSLES) && !defined(GL3) // iPad2 workaround + float3 weights = abs(IN.Position.www - float3(0, 1, 2)) < 0.1; +#else + float3 weights = IN.Position.www == float3(0, 1, 2); +#endif + + float waveFactor = dot(weights, IN.Material0.xyz) * (1.0 / 255.0); + +#ifdef PIN_HQ + float fade = saturate0(1 - dot(posWorld - G(CameraPosition), -G(ViewDir).xyz) * G(FadeDistance_GlowFactor).y); + + posWorld = displacePosition(posWorld, waveFactor * fade); +#endif + + OUT.HPosition = mul(G(ViewProjection), float4(posWorld, 1)); + + OUT.LightPosition_Fog = float4(lgridPrepareSample(lgridOffset(posWorld, normalWorld)), (G(FogParams).z - OUT.HPosition.w) * G(FogParams).w); + + OUT.Uv0 = getUV(posWorld, IN.Material1.x, IN.Normal.w); + OUT.Uv1 = getUV(posWorld, IN.Material1.y, IN.Material0.w); + OUT.Uv2 = getUV(posWorld, IN.Material1.z, IN.Material1.w); + + OUT.Weights_Wave.xyz = weights; + OUT.Weights_Wave.w = waveFactor; + + OUT.Normal = normalWorld; + OUT.View_Depth = float4(G(CameraPosition) - posWorld, OUT.HPosition.w); + OUT.Tangents = float3(IN.Material1.xyz) > 7.5; // side vs top + +#ifdef PIN_HQ + OUT.PositionScreen = clipToScreen(OUT.HPosition); +#endif + + return OUT; +} + +TEX_DECLARE2D(NormalMap1, 0); +TEX_DECLARE2D(NormalMap2, 1); +TEX_DECLARECUBE(EnvMap, 2); +LGRID_SAMPLER(LightMap, 3); +TEX_DECLARE2D(LightMapLookup, 4); +TEX_DECLARE2D(GBufferColor, 5); +TEX_DECLARE2D(GBufferDepth, 6); + +float fresnel(float ndotv) +{ + return saturate(0.78 - 2.5 * abs(ndotv)) + CFG_FRESNEL_OFFSET; +} + +float4 sampleMix(float2 uv) +{ +#ifdef PIN_HQ + return lerp(tex2D(NormalMap1, uv), tex2D(NormalMap2, uv), WaveParams.w); +#else + return tex2D(NormalMap1, uv); +#endif +} + +float3 sampleNormal(float2 uv0, float2 uv1, float2 uv2, float3 w, float3 normal, float3 tsel) +{ + return terrainNormal(sampleMix(uv0), sampleMix(uv1), sampleMix(uv2), w, normal, tsel); +} + +float3 sampleNormalSimple(float2 uv0, float2 uv1, float2 uv2, float3 w) +{ + float4 data = sampleMix(uv0) * w.x + sampleMix(uv1) * w.y + sampleMix(uv2) * w.z; + + return nmapUnpack(data).xzy; +} + +float unpackDepth(float2 uv) +{ + float4 geomTex = tex2D(GBufferDepth, uv); + float d = geomTex.z * (1.0f/256.0f) + geomTex.w; + return d * GBUFFER_MAX_DEPTH; +} + +float3 getRefractedColor(float4 cpos, float3 N, float3 waterColor) +{ + float2 refruv0 = cpos.xy / cpos.w; + float2 refruv1 = refruv0 + N.xz * CFG_REFRACTION_STRENGTH; + + float4 refr0 = tex2D(GBufferColor, refruv0); + refr0.w = unpackDepth(refruv0); + + float4 refr1 = tex2D(GBufferColor, refruv1); + refr1.w = unpackDepth(refruv1); + + float4 result = lerp(refr0, refr1, saturate(refr1.w - cpos.w)); + + // Estimate water absorption by a scaled depth difference + float depthfade = saturate((result.w - cpos.w) * WaterParams.x + WaterParams.y); + + // Since GBuffer depth is clamped we tone the refraction down after half of the range for a smooth fadeout + float gbuffade = saturate(cpos.w * (2.f / GBUFFER_MAX_DEPTH) - 1); + + float fade = saturate(depthfade + gbuffade); + + return lerp(result.rgb, waterColor, fade); +} + +float3 getReflectedColor(float4 cpos, float3 wpos, float3 R) +{ + float3 result = 0; + float inside = 0; + + float distance = CFG_SSR_START_DISTANCE; + float diff = 0; + float diffclamp = cpos.w * CFG_SSR_STEP_CLAMP; + + float4 Pproj = cpos; + float4 Rproj = clipToScreen(mul(G(ViewProjection), float4(R, 0))); + +#ifndef GLSL + [unroll] +#endif + for (int i = 0; i < CFG_SSR_STEPS; ++i) + { + distance += clamp(diff, -diffclamp, diffclamp); + + float4 cposi = Pproj + Rproj * distance; + float2 uv = cposi.xy / cposi.w; + float depth = unpackDepth(uv); + + diff = depth - cposi.w; + } + + float4 cposi = Pproj + Rproj * distance; + float2 uv = cposi.xy / cposi.w; + + // Ray hit has to be inside the screen bounds + float ufade = abs(uv.x - 0.5) < 0.5; + float vfade = abs(uv.y - 0.5) < 0.5; + + // Fade reflections out with distance; use max(ray hit, original depth) to discard hits that are too far + // 4 - depth * 4 would give us fade out from 0.75 to 1; 3.9 makes sure reflections go to 0 slightly before GBUFFER_MAX_DEPTH + float wfade = saturate((4 - 0.1) - max(cpos.w, cposi.w) * (4.f / GBUFFER_MAX_DEPTH)); + + // Ray hit has to be reasonably close to where we started + float dfade = abs(diff) < CFG_SSR_DEPTH_CUTOFF; + + // Avoid back-projection + float Vfade = Rproj.w > 0; + + float fade = ufade * vfade * wfade * dfade * Vfade; + + return lerp(texCUBE(EnvMap, R).rgb, tex2D(GBufferColor, uv).rgb, fade); +} + +float4 WaterPS(VertexOutput IN): COLOR0 +{ + float4 light = lgridSample(TEXTURE(LightMap), TEXTURE(LightMapLookup), IN.LightPosition_Fog.xyz); + float shadow = light.a; + + float3 w = IN.Weights_Wave.xyz; + + // Use simplified normal reconstruction for LQ mobile (assumes flat water surface) +#if defined(GLSLES) && !defined(PIN_HQ) + float3 normal = sampleNormalSimple(IN.Uv0, IN.Uv1, IN.Uv2, w); +#else + float3 normal = sampleNormal(IN.Uv0, IN.Uv1, IN.Uv2, w, IN.Normal, IN.Tangents); +#endif + + // Flatten the normal for Fresnel and for reflections to make them less chaotic + float3 flatNormal = lerp(IN.Normal, normal, CFG_NORMAL_STRENGTH); + + float3 waterColor = WaterColor.rgb; + +#ifdef PIN_HQ + float fade = saturate0(1 - IN.View_Depth.w * G(FadeDistance_GlowFactor).y); + + float3 view = normalize(IN.View_Depth.xyz); + + float fre = fresnel(dot(flatNormal, view)) * IN.Weights_Wave.w; + + float3 position = G(CameraPosition) - IN.View_Depth.xyz; + +#ifdef PIN_GBUFFER + float3 refr = getRefractedColor(IN.PositionScreen, normal, waterColor); + float3 refl = getReflectedColor(IN.PositionScreen, position, reflect(-view, flatNormal)); +#else + float3 refr = waterColor; + float3 refl = texCUBE(EnvMap, reflect(-view, flatNormal)).rgb; +#endif + + float specularIntensity = CFG_SPECULAR * fade; + float specularPower = CFG_GLOSS; + + float3 specular = G(Lamp0Color) * (specularIntensity * shadow * (float)(half)pow(saturate(dot(normal, normalize(-G(Lamp0Dir) + view))), specularPower)); +#else + float3 view = normalize(IN.View_Depth.xyz); + + float fre = fresnel(dot(flatNormal, view)); + + float3 refr = waterColor; + + float3 refl = texCUBE(EnvMap, reflect(-IN.View_Depth.xyz, flatNormal)).rgb; + + float3 specular = 0; +#endif + + // Combine + float4 result; + result.rgb = lerp(refr, refl, fre) * (G(AmbientColor).rgb + G(Lamp0Color).rgb * shadow + light.rgb) + specular; + result.a = 1; + + float fogAlpha = saturate(IN.LightPosition_Fog.w); + + result.rgb = lerp(G(FogColor), result.rgb, fogAlpha); + + return result; +} diff --git a/Client2016/shaders/source/ssao.hlsl b/Client2016/shaders/source/ssao.hlsl new file mode 100644 index 0000000..fcdf566 --- /dev/null +++ b/Client2016/shaders/source/ssao.hlsl @@ -0,0 +1,349 @@ +#include "screenspace.hlsl" + +// tweakables +#define SSAO_NUM_PAIRS 8 +#define SSAO_SPHERE_RAD 2.0f // world-space +#define SSAO_MIN_PIXEL_RANGE 10.0f +#define SSAO_MAX_PIXEL_RANGE 100.0f +#define BLUR_DEPTH_DELTA 0.4f + +#define COMPOSITE_DEPTH_DELTA 0.02f +#define COMPOSITE_DEPTH_DELTA2 0.4f + +TEX_DECLARE2D(depthBuffer, 0); +TEX_DECLARE2D(randMap, 1); +TEX_DECLARE2D(map, 2); +TEX_DECLARE2D(geomMap, 3); +TEX_DECLARE2D(colorMap, 4); + +VertexOutput_4uv ssaoDepthDown_vs(float4 p: POSITION) +{ + float2 uv = convertUv(p); + + VertexOutput_4uv OUT; + OUT.p = convertPosition(p, 2); + OUT.uv = uv; + + float2 uvOffset = TextureSize.zw * 0.25f; + + OUT.uv12.xy = uv + uvOffset * float2(-1, -1); + OUT.uv12.zw = uv + uvOffset * float2(+1, -1); + OUT.uv34.xy = uv + uvOffset * float2(-1, +1); + OUT.uv34.zw = uv + uvOffset * float2(+1, +1); + + return OUT; +} + +// used for ssao blurring passes +VertexOutput_8uv ssaoBlur_vs(float4 position, float2 uvOffset) +{ + float2 uv = convertUv(position); + + VertexOutput_8uv OUT; + OUT.p = convertPosition(position, 2); + OUT.uv = uv; + + OUT.uv12.xy = uv + 1 * uvOffset; + OUT.uv12.zw = uv + 2 * uvOffset; + OUT.uv34.xy = uv + 3 * uvOffset; + OUT.uv34.zw = uv + 4 * uvOffset; + + OUT.uv56.xy = uv - 1 * uvOffset; + OUT.uv56.zw = uv - 2 * uvOffset; + OUT.uv78.xy = uv - 3 * uvOffset; + OUT.uv78.zw = uv - 4 * uvOffset; + + return OUT; +} + +VertexOutput_8uv ssaoBlurX_vs(float4 p: POSITION) +{ + return ssaoBlur_vs(p, float2(TextureSize.z * 1, 0)); +} + +VertexOutput_8uv ssaoBlurY_vs(float4 p: POSITION) +{ + return ssaoBlur_vs(p, float2(0, TextureSize.w * 1)); +} + +float unpackDepth( TEXTURE_IN_2D(s), float2 uv ) +{ + float4 geomTex = tex2D(s, uv); + float d = geomTex.z * (1.0f/256.0f) + geomTex.w; + return d; +} + +float getDepth( TEXTURE_IN_2D(s), float2 uv ) +{ + return (float)tex2D(s,uv).r; +} + +#define NUM_PAIRS SSAO_NUM_PAIRS +#define RANGE 60.0/1024.0 + +#define pi 3.14159265359 +#define RAD(X) ( (X) * (pi/180) ) + +float2 GetRotatedSample(float i) +{ + return (i+1) / (NUM_PAIRS+2) * float2(cos( RAD(45) + i / NUM_PAIRS * 2 * pi ), sin( RAD(45) + i / NUM_PAIRS * 2 * pi ) ); +} + +#define NUM_SAMPLES NUM_PAIRS*2+1 + +float4 ssao_ps( + VertexOutput IN): COLOR0 +{ + float2 mapSize = TextureSize.xy; + + float baseDepth = getDepth( TEXTURE(depthBuffer), IN.uv ); + + float4 noiseTex = tex2D(randMap, IN.uv*mapSize/4) * 2 - 1; + + float2x2 rotation = + { + { noiseTex.y, noiseTex.x }, + { -noiseTex.x, noiseTex.y } + }; + + float2 OFFSETS1[NUM_PAIRS] = + { + GetRotatedSample(0), + GetRotatedSample(1), + GetRotatedSample(2), + GetRotatedSample(3), + GetRotatedSample(4), + GetRotatedSample(5), +#if NUM_PAIRS > 6 + GetRotatedSample(6), + GetRotatedSample(7), +#if NUM_PAIRS > 8 + GetRotatedSample(8), + GetRotatedSample(9), + GetRotatedSample(10), + GetRotatedSample(11), +#endif +#endif + }; + + float occ = 1; + + float sphereRadiusZB = (float) ( 2.0f / GBUFFER_MAX_DEPTH ); + +#define MINPIXEL SSAO_MIN_PIXEL_RANGE +#define MAXPIXEL SSAO_MAX_PIXEL_RANGE + + float radiusTex = (float)clamp( 0.5*sphereRadiusZB / baseDepth, MINPIXEL / mapSize.x, MAXPIXEL / mapSize.y); + + float numSamples = 2; + + for(int i = 0; i < NUM_PAIRS; i++) + { + float2 offset1 = mul(rotation, OFFSETS1[i]); + + float2 offseted1 = IN.uv + offset1 * radiusTex; + float2 offseted2 = IN.uv - offset1 * radiusTex; + + float2 offsetDepth; + offsetDepth.x = getDepth( TEXTURE(depthBuffer), offseted1 ); + offsetDepth.y = getDepth( TEXTURE(depthBuffer), offseted2 ); + + float2 diff = offsetDepth - baseDepth.xx; + + float normalizedOffsetLen = (float)(i+1)/(NUM_PAIRS+2); + + float segmentDiff = (float) ( 1.5f*sphereRadiusZB*sqrt(1-normalizedOffsetLen*normalizedOffsetLen) ); + + float2 normalizedDiff = (diff / segmentDiff) + 0.5; + + float minDiff = min(normalizedDiff.x, normalizedDiff.y); + + // At 0, full sample + // At -1, zero sample, zero weight + + float sampleadd = (float) saturate(1+minDiff); + + float a = (float)(saturate(normalizedDiff.x) + saturate(normalizedDiff.y))*sampleadd; + occ += a; + numSamples += 2 * sampleadd; + } + + occ = occ / numSamples; + + float finalocc = (float)saturate(occ*2); + + if(baseDepth - (1.0f-1/256.0f) > 0) + finalocc += 1; + + return float4(finalocc, finalocc, finalocc, 1); +} + +// this function estimates depth discrepancy tolerance for the blur filter +float depthTolerance( float baseDepth, float sphereRadiusZB ) +{ + float ramp = 80; // tweak + return ( clamp( sphereRadiusZB * (baseDepth * ramp) , 0.1f * sphereRadiusZB, 40*sphereRadiusZB ) ); +} + +float ssaoBlur( + float2 uv, + + float4 uv12, + float4 uv34, + float4 uv56, + float4 uv78, + + TEXTURE_IN_2D(map), + TEXTURE_IN_2D(depthBuffer) + ) +{ + float sphereRadiusZB = BLUR_DEPTH_DELTA / GBUFFER_MAX_DEPTH; + float4 i = { 1, 2, 3, 4 }; + float4 iw = 4-i; + float4 denom = 1; + + + float4 sum = tex2D(map, uv).rrrr * denom; + + float baseDepth = getDepth( TEXTURE(depthBuffer), uv ); + + float4 newDepth, delta, ssample, coef; + + newDepth.x = getDepth( TEXTURE(depthBuffer), uv12.xy ); + newDepth.y = getDepth( TEXTURE(depthBuffer), uv12.zw ); + newDepth.z = getDepth( TEXTURE(depthBuffer), uv34.xy ); + newDepth.w = getDepth( TEXTURE(depthBuffer), uv34.zw ); + + delta = (newDepth - baseDepth.xxxx); + coef = iw * ( abs(delta) < depthTolerance( baseDepth, sphereRadiusZB ).xxxx ); + + + ssample.x = tex2D( map, uv12.xy ).r; + ssample.y = tex2D( map, uv12.zw ).r; + ssample.z = tex2D( map, uv34.xy ).r; + ssample.w = tex2D( map, uv34.zw ).r; + + sum += ssample * coef; + denom += coef; + + //////////////////////////////////////// + + newDepth.x = getDepth( TEXTURE(depthBuffer), uv56.xy ); + newDepth.y = getDepth( TEXTURE(depthBuffer), uv56.zw ); + newDepth.z = getDepth( TEXTURE(depthBuffer), uv78.xy ); + newDepth.w = getDepth( TEXTURE(depthBuffer), uv78.zw ); + + delta = newDepth - baseDepth.xxxx; + coef = iw * ( abs(delta) < depthTolerance( baseDepth, sphereRadiusZB ).xxxx ); + + ssample.x = tex2D( map, uv56.xy ).r; + ssample.y = tex2D( map, uv56.zw ).r; + ssample.z = tex2D( map, uv78.xy ).r; + ssample.w = tex2D( map, uv78.zw ).r; + + sum += ssample * coef; + denom += coef; + + return dot( sum, float4(1,1,1,1) ) / dot( denom, float4(1,1,1,1) ); +} + + +float4 ssaoBlurX_ps(VertexOutput_8uv IN): COLOR0 +{ + float ssaoTerm = ssaoBlur( IN.uv, IN.uv12, IN.uv34, IN.uv56, IN.uv78, TEXTURE(map), TEXTURE(depthBuffer)); + + return float4(ssaoTerm.xxx, 1); +} + +#define SPECULAR_WEIGHT 3 + + +float4 ssaoBlurY_ps(VertexOutput_8uv IN): COLOR0 +{ + float ssaoTerm = ssaoBlur( IN.uv, IN.uv12, IN.uv34, IN.uv56, IN.uv78, TEXTURE(map), TEXTURE(depthBuffer)); + + float4 geom = tex2D(geomMap, IN.uv); + + float specular = geom.x; + float diffuse = geom.y + 0.001; + + // Making specular kill SSAO faster, so it doesn't get capped by 1 + return (SPECULAR_WEIGHT*specular + diffuse * ssaoTerm) / (SPECULAR_WEIGHT*specular + diffuse); +} + + + +float4 ssaoDepthDown_ps( VertexOutput_4uv IN ) : COLOR0 +{ + + float4 d; + d.x = unpackDepth( TEXTURE(depthBuffer), IN.uv12.xy ); + d.y = unpackDepth( TEXTURE(depthBuffer), IN.uv12.zw ); + d.z = unpackDepth( TEXTURE(depthBuffer), IN.uv34.xy ); + d.w = unpackDepth( TEXTURE(depthBuffer), IN.uv34.zw ); + + float2 tmp = min( d.xy, d.zw ); + return min( tmp.x, tmp.y ).x; +} + +VertexOutput_4uv ssaoComposit_vs(float4 p: POSITION) +{ + float2 uv = convertUv(p); + + VertexOutput_4uv OUT; + OUT.p = convertPosition(p, 1); + OUT.uv = uv; + + float2 uvOffset = TextureSize.zw * 2; + + OUT.uv12.xy = uv + float2(uvOffset.x, 0); + OUT.uv12.zw = uv - float2(uvOffset.x, 0); + OUT.uv34.xy = uv + float2(0, uvOffset.y); + OUT.uv34.zw = uv - float2(0, uvOffset.y); + + return OUT; +} + +#define CMP_LESS(X,Y) ( (X) < (Y) ) + +float4 ssaoComposit_ps(VertexOutput_4uv IN): COLOR0 +{ + //return float4(1,0,0,0.5); + float depth_range = COMPOSITE_DEPTH_DELTA / GBUFFER_MAX_DEPTH; + float depth_range2 = COMPOSITE_DEPTH_DELTA2 / GBUFFER_MAX_DEPTH; + + // we're here + float baseDepth = unpackDepth( TEXTURE(geomMap), IN.uv ); + float ssaoTerm = 1.0f; + + float depth = getDepth( TEXTURE(depthBuffer), IN.uv ); + float diff = abs( depth - baseDepth ); + ssaoTerm = tex2D( map, IN.uv ).x; + + float chk1 = CMP_LESS( depth_range, diff ); // can we trust the base depth? 0 - yes, 1 - no + float4 ssaoTermNew = 0, chk2, depth2, diff2; + + depth2.x = getDepth( TEXTURE(depthBuffer), IN.uv12.xy ); + depth2.y = getDepth( TEXTURE(depthBuffer), IN.uv12.zw ); + depth2.z = getDepth( TEXTURE(depthBuffer), IN.uv34.xy ); + depth2.w = getDepth( TEXTURE(depthBuffer), IN.uv34.zw ); + + ssaoTermNew.x = tex2D( map, IN.uv12.xy ).x; + ssaoTermNew.y = tex2D( map, IN.uv12.zw ).x; + ssaoTermNew.z = tex2D( map, IN.uv34.xy ).x; + ssaoTermNew.w = tex2D( map, IN.uv34.zw ).x; + + diff2 = abs( depth2 - baseDepth.xxxx ); + chk2 = CMP_LESS( diff2, depth_range2.xxxx ); + + ssaoTermNew *= chk2; + float den = dot( chk2, 1 ); // + 1e-5f; - TODO: add this if we encounter glitches; // + ssaoTermNew.x = dot( ssaoTermNew, 1 ) / den; + + // the final decision: pick the base sample or its estimate, if base depth in unauthorative + ssaoTerm = saturate(den*chk1) ? ssaoTermNew.x : ssaoTerm; + + //return float4(ssaoTermNew.rgb,1); + float4 colorMapSample = tex2D(colorMap, IN.uv); + return float4(colorMapSample.rgb * ssaoTerm, colorMapSample.a); +} diff --git a/Client2016/shaders/source/texcomp.hlsl b/Client2016/shaders/source/texcomp.hlsl new file mode 100644 index 0000000..79674ba --- /dev/null +++ b/Client2016/shaders/source/texcomp.hlsl @@ -0,0 +1,39 @@ +#include "common.h" + +struct Appdata +{ + float4 Position : POSITION; + float2 Uv : TEXCOORD0; +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; + float2 Uv : TEXCOORD0; +}; + +VertexOutput TexCompVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + OUT.HPosition = mul(G(ViewProjection), IN.Position); + OUT.Uv = IN.Uv; + + return OUT; +} + +TEX_DECLARE2D(DiffuseMap, 0); + +uniform float4 Color; + +float4 TexCompPS(VertexOutput IN): COLOR0 +{ + return tex2Dbias(DiffuseMap, float4(IN.Uv, 0, -10)) * Color; +} + +float4 TexCompPMAPS(VertexOutput IN): COLOR0 +{ + float4 tex = tex2Dbias(DiffuseMap, float4(IN.Uv, 0, -10)); + + return float4(tex.rgb * tex.a * Color.rgb, tex.a * Color.a); +} diff --git a/Client2016/shaders/source/ui.hlsl b/Client2016/shaders/source/ui.hlsl new file mode 100644 index 0000000..6688cc7 --- /dev/null +++ b/Client2016/shaders/source/ui.hlsl @@ -0,0 +1,58 @@ +#include "common.h" + +struct Appdata +{ + float4 Position : POSITION; + float2 Uv : TEXCOORD0; + float4 Color : COLOR0; +}; + +struct VertexOutput +{ + float4 HPosition : POSITION; + + float2 Uv : TEXCOORD0; + float4 Color : COLOR0; + +#if defined(PIN_FOG) + float FogFactor : TEXCOORD1; +#endif +}; + +uniform float4 UIParams; // x = luminance sampling on/off, w = z offset +TEX_DECLARE2D(DiffuseMap, 0); + +VertexOutput UIVS(Appdata IN) +{ + VertexOutput OUT = (VertexOutput)0; + + OUT.HPosition = mul(G(ViewProjection), IN.Position); + OUT.HPosition.z -= UIParams.w; // against z-fighting + + OUT.Uv = IN.Uv; + OUT.Color = IN.Color; + +#if defined(PIN_FOG) + OUT.FogFactor = (G(FogParams).z - OUT.HPosition.w) * G(FogParams).w; +#endif + + return OUT; +} + +float4 UIPS(VertexOutput IN): COLOR0 +{ + float4 base; + + if (UIParams.x > 0.5) + base = float4(1, 1, 1,tex2D(DiffuseMap, IN.Uv).r); + else + base = tex2D(DiffuseMap, IN.Uv); + + float4 result = IN.Color * base; + +#if defined(PIN_FOG) + result.rgb = lerp(G(FogColor), result.rgb, saturate(IN.FogFactor)); +#endif + + return result; +} diff --git a/Client2016/shaders/source/water.hlsl b/Client2016/shaders/source/water.hlsl new file mode 100644 index 0000000..6c328c3 --- /dev/null +++ b/Client2016/shaders/source/water.hlsl @@ -0,0 +1,215 @@ + +// +// Water shader. +// Big, fat and ugly. +// +// All (most) things considered, I have converged to this particular way of rendering water: +// +// Vertex waves +// No transparency. Solid color for deep water. +// Fresnel law, reflects environment. +// Phong speculars. +// Ripples via animated normal map. Adjustable intensity, speed and scale. Affect reflection and speculars. + +#include "common.h" + +WORLD_MATRIX(WorldMatrix); + +uniform float4 nmAnimLerp; // ratio between normal map frames +uniform float4 waveParams; // .x = frequency .y = phase .z = height +uniform float4 WaterColor; // deep water color + +#ifdef PIN_HQ +# define WATER_LOD 1 +#else +# define WATER_LOD 2 +#endif + +#define LODBIAS (-1) + +float fadeFactor( float3 wspos ) +{ + return saturate( -0.4f + 1.4f*length( G(CameraPosition) - wspos.xyz ) * G(FadeDistance_GlowFactor).y ); +} + +float wave( float4 wspos ) +{ + float x = sin( ( wspos.z - wspos.x - waveParams.y ) * waveParams.x ); + float z = sin( ( wspos.z + wspos.x + waveParams.y ) * waveParams.x ); + float p = (x + z) * waveParams.z; + return p - p * fadeFactor( wspos.xyz ); +} + + + +// perturbs the water mesh and vertex normals +void makeWaves( inout float4 wspos, inout float3 wsnrm ) +{ +#if WATER_LOD == 0 + float gridSize = 4.0f; + + float4 wspos1 = wspos; + float4 wspos2 = wspos; + + wspos1.x += gridSize; + wspos2.z += gridSize; + + wspos.y += wave(wspos) ; + wspos1.y += wave(wspos1); + wspos2.y += wave(wspos2); + + wsnrm = normalize( cross( wspos2.xyz - wspos.xyz, wspos1.xyz - wspos.xyz ) ); +#elif WATER_LOD == 1 + wspos.y += wave( wspos ); +#else /* do n0thing */ +#endif +} + +struct V2P +{ + float4 pos : POSITION; + float4 tc0Fog : TEXCOORD0; + float4 wspos : TEXCOORD1; + float3 wsnrm : TEXCOORD2; + float3 light : TEXCOORD3; + float3 fade : TEXCOORD4; +}; + +V2P water_vs( + ATTR_INT4 pos : POSITION, + ATTR_INT3 nrm : NORMAL +) +{ + V2P o; + + // Decode vertex data + float3 normal = (nrm - 127.0) / 127.0; + + normal = normalize(normal); + + float4 wspos = mul( WorldMatrix, pos ); + float3 wsnrm = normal; + + wspos.y -= 2*waveParams.z; + + makeWaves( /*INOUT*/ wspos, /*INOUT*/ wsnrm ); + + o.wspos = wspos; + o.wsnrm = wsnrm; + + if( normal.y < 0.01f ) o.wsnrm = normal; + + // box mapping + //float3x2 m = { wspos.xz, wspos.xy, wspos.yz }; + //float2 tcselect = mul( abs( nrm.yzx ), m ); + + float2 tcselect; + float3 wspostc = float3( wspos.x, -wspos.y, wspos.z ); + + tcselect.x = dot( abs( normal.yxz ), wspostc.xzx ); + tcselect.y = dot( abs( normal.yxz ), wspostc.zyy ); + + o.pos = mul( G(ViewProjection), wspos ); + o.tc0Fog.xy = tcselect * .05f; + o.tc0Fog.z = saturate( (G(FogParams).z - o.pos.w) * G(FogParams).w ); + o.tc0Fog.w = LODBIAS; + + o.light = lgridPrepareSample(lgridOffset(wspos.xyz, wsnrm.xyz)); + + o.fade.x = fadeFactor( wspos.xyz ); + o.fade.y = (1-o.fade.x) * saturate( dot( wsnrm, -G(Lamp0Dir) ) ) * 100; + o.fade.z = 1 - 0.9*saturate1( exp( -0.005 * length( G(CameraPosition) - wspos.xyz ) ) ); + + return o; +} + +////////////////////////////////////////////////////////////////////////////// + +TEX_DECLARE2D(NormalMap1, 0); +TEX_DECLARE2D(NormalMap2, 1); +TEX_DECLARECUBE(EnvMap, 2); +LGRID_SAMPLER(LightMap, 3); +TEX_DECLARE2D(LightMapLookup, 4); + +float3 pixelNormal( float4 tc0 ) +{ + float4 nm1 = tex2Dbias( NormalMap1, tc0 ); +#if WATER_LOD <= 1 + float4 nm2 = tex2Dbias( NormalMap2, tc0 ); + float4 nm3 = lerp( nm1, nm2, nmAnimLerp.xxxx ); +#else + float4 nm3 = nm1; +#endif + return nmapUnpack( nm3 ); +} + +// Fresnel approximation. N1 and N2 are refractive indices. +// for above water, use n1 = 1, n2 = 1.3, for underwater use n1 = 1.3, n2 = 1 +float fresnel( float3 N, float3 V, float n1, float n2, float p, float fade ) +{ +#if WATER_LOD == 0 + float r0 = (n1-n2)/(n1+n2); + r0 *= r0; + return r0 + (1-r0) * pow( 1 - abs( dot( normalize(N), V ) ), p ); +#else + return 0.1 + saturate( - 1.9 * abs( dot( N, V ) ) + 0.8); // HAXX! + //return 1 - 2 * abs( dot( N, V ) ); +#endif +} + +float4 envColor( float3 N, float3 V, float fade ) +{ + float3 dir = reflect( V, N ); + return texCUBE(EnvMap, dir) * 0.91f; +} + +float4 deepWaterColor(float4 light) +{ + //float4 tint = 5*float4( 0.1f, 0.1f, 0.13f, 1); + float4 tint = 0.8f*float4( 118, 143, 153, 255 ) / 255; + return (light + texCUBEbias( EnvMap, float4( 0,1,0, 10.0f) )) * tint; +} + + +////////////////////////////////////////// +////////////////////////////////////////// + + + +float4 water_ps( V2P v ) : COLOR0 +{ + + float4 WaterColorTest = 0.5 * float4( 26, 169, 185, 0 ) / 255; + float4 FogColorTest = 0.8 * float4( 35, 107, 130, 0 ) / 255; + + float3 N2 = v.wsnrm; + float3 N1 = pixelNormal( v.tc0Fog ).xzy; + float3 N3 = 0.5*(N2 + N1); + + N3 = lerp( N3, N2, v.fade.z ); + + float3 L = /*normalize*/(-G(Lamp0Dir).xyz); + float3 E = normalize( G(CameraPosition) - v.wspos.xyz ); + + float4 light = lgridSample(TEXTURE(LightMap), TEXTURE(LightMapLookup), v.light.xyz); + + float fre = fresnel( N3, E, 1.0f, 1.3f, 5, v.fade.x ); + float3 diffuse = deepWaterColor(light).rgb; + float3 env = envColor( N3, -E, v.fade.x ).rgb; + + float3 R = reflect( -L, N1 ); + +#if WATER_LOD <= 1 + float specular = pow( saturate0( dot( R, E ) ), 1600 ) * L.y * 100; // baseline +# ifndef GLSLES + specular = 0.65 * saturate1( specular * saturate0( light.a - 0.4f ) ); +# endif +#else + float specular = 0; +#endif + + float3 result = lerp( diffuse, env, fre ) + specular.xxx; + result = lerp( G(FogColor).rgb, result, v.tc0Fog.z ); + + return float4( result, 1 ); +} diff --git a/Client2016/shaders/source/wood.hlsl b/Client2016/shaders/source/wood.hlsl new file mode 100644 index 0000000..2e7f1e2 --- /dev/null +++ b/Client2016/shaders/source/wood.hlsl @@ -0,0 +1,21 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 2 +#define CFG_GLOSS_SCALE 256 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.3 + +#define CFG_SPECULAR_LOD 0.25 +#define CFG_GLOSS_LOD 32 + +#define CFG_NORMAL_DETAIL_TILING 7 +#define CFG_NORMAL_DETAIL_SCALE 0.6 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#include "material.hlsl" diff --git a/Client2016/shaders/source/woodplanks.hlsl b/Client2016/shaders/source/woodplanks.hlsl new file mode 100644 index 0000000..685f947 --- /dev/null +++ b/Client2016/shaders/source/woodplanks.hlsl @@ -0,0 +1,23 @@ +#define CFG_TEXTURE_TILING 1 + +#define CFG_DIFFUSE_SCALE 1 +#define CFG_SPECULAR_SCALE 2 +#define CFG_GLOSS_SCALE 256 +#define CFG_REFLECTION_SCALE 0 + +#define CFG_NORMAL_SHADOW_SCALE 0.3 + +#define CFG_SPECULAR_LOD 0.28 +#define CFG_GLOSS_LOD 53 + +#define CFG_NORMAL_DETAIL_TILING 0 +#define CFG_NORMAL_DETAIL_SCALE 0 + +#define CFG_FAR_TILING 0 +#define CFG_FAR_DIFFUSE_CUTOFF 0 +#define CFG_FAR_NORMAL_CUTOFF 0 +#define CFG_FAR_SPECULAR_CUTOFF 0 + +#define CFG_OPT_BLEND_COLOR + +#include "material.hlsl" diff --git a/Client2016/zlib1.dll b/Client2016/zlib1.dll new file mode 100644 index 0000000..0c0e87c Binary files /dev/null and b/Client2016/zlib1.dll differ diff --git a/Client2018/AppSettings.xml b/Client2018/AppSettings.xml new file mode 100644 index 0000000..59cc39e --- /dev/null +++ b/Client2018/AppSettings.xml @@ -0,0 +1,5 @@ + + + content + http://www.syntax.eco + \ No newline at end of file diff --git a/Client2018/ReflectionMetadata.xml b/Client2018/ReflectionMetadata.xml new file mode 100644 index 0000000..8600cf6 --- /dev/null +++ b/Client2018/ReflectionMetadata.xml @@ -0,0 +1,7215 @@ + + + + + + BindableFunction + Scripting + Allow functions defined in one script to be called by another script + 40 + 66 + + + + + + Invoke + Causes the function assigned to OnInvoke to be called. Arguments passed to this function get passed to OnInvoke function. + + + + + + + OnInvoke + Should be defined as a function. This function is called when Invoke() is called. Number of arguments is variable. + + + + + + + BindableEvent + Scripting + Allow events defined in one script to be subscribed to by another script + + 50 + 67 + + + + + Fire + Used to make the custom event fire (see Event for more info). Arguments can be variable length. + + + + + + + Event + This event fires when the Fire() method is used. Receives the variable length arguments from Fire(). + + + + + + + TouchTransmitter + Used by networking and replication code to transmit touch events - no other purpose + + false + 30 + 37 + + + + + ForceField + Avatar + Prevents joint breakage from explosions, and stops Humanoids from taking damage + 30 + 37 + Model + Model + + + + + PluginManager + + + + + + TeleportService + Allows players to seamlessly leave a game and join another + + + + CustomizedTeleportUI + true + Deprecated + + + + + + + Plugin + + + + + + PluginMouse + + + + + + Glue + BasePart + BasePart + + + + + CollectionService + A service which provides collections of instances based on tags assigned to them. + + + + + ItemAdded + true + Deprecated. Use GetInstanceAddedSignal instead. + + + + + ItemRemoved + true + Deprecated. Use GetInstancedRemovedSignal instead. + + + + + + + GetCollection + true + Deprecated. Use GetTagged instead. + + + + + GetTagged + Returns an array of all of the instances in the data model which have the given tag. + + + + + AddTag + Adds a tag to an instance. + + + + + RemoveTag + Removes a tag to an instance. + + + + + GetTags + Returns a list of all the collections that an instance belongs to. + + + + + HasTag + Returns whether the given instance has the given tag. + + + + + GetInstanceAddedSignal + Returns a signal that fires when the given tag either has a new instance with that tag added to the data model or that tag is assigned to an instance within the data model. + + + + + GetInstanceRemovedSignal + Returns a signal that fires when the given tag either has an instance with that tag removed from the data model or that tag is removed from an instance within the data model. + + + + + + + JointsService + + + + + RunService + + + + + BadgeService + + + + + LogService + + + + + AssetService + A service used to set and get information about assets stored on the Roblox website. + + + + + RevertAsset + Reverts a given place id to the version number provided. Returns true if successful on reverting, false otherwise. + + + + + SetPlacePermissions + Sets the permissions for a placeID to the place accessType. An optional table (inviteList) can be included that will set the accessType for only the player names provided. The table should be set up as an array of usernames (strings). + + + + + GetPlacePermissions + Given a placeID, this function will return a table with the permissions of the place. Useful for determining what kind of permissions a particular user may have for a place. + + + + + GetAssetVersions + Given a placeID, this function will return a table with the version info of the place. An optional arg of page number can be used to page through all revisions (a single page may hold about 50 revisions). + + + + + GetCreatorAssetID + Given a creationID, this function will return the asset that created the creationID. If no other asset created the given creationID, 0 is returned. + + + + + + + HttpService + + + + + HttpEnabled + true + Enabling http requests from scripts + + + + + + + GetAsync + Server + + + + + PostAsync + Server + + + + + + + InsertService + A service used to insert objects stored on the website into the game. + + + + + AllowClientInsertModels + true + Can be set in non-filtering-enabled places to allow LoadAsset to be used in LocalScripts. + + + + + AllowInsertFreeModels + false + true + -1 + Allows free models to be inserted into place. + + + + + + + GetCollection + Returns a table for the assets stored in the category. A category is an setId from www.roblox.com that links to a set. <a href="http://wiki.roblox.com/index.php?title=API:Class/InsertService/GetCollection" target="_blank">More info on table format</a>. <a href="http://wiki.roblox.com/index.php/Sets" target="_blank">More info on sets</a> + + + + + Insert + Inserts the Instance into the workspace. It is recommended to use Instance.Parent = game.Workspace instead, as this can cause issues currently. + + + + + ApproveAssetId + true + Deprecated + + + + + ApproveAssetVersionId + true + Deprecated + + + + + + + + GetBaseSets + Returns a table containing a list of the various setIds that are ROBLOX approved. <a href="http://wiki.roblox.com/index.php/Sets" target="_blank">More info on sets</a> + + + + + GetUserSets + Returns a table containing a list of the various setIds that correspond to argument 'userId'. <a href="http://wiki.roblox.com/index.php/Sets" target="_blank">More info on sets</a> + + + + + GetBaseCategories + true + Deprecated. Use GetBaseSets() instead. + + + + + GetUserCategories + true + Deprecated. Use GetUserSets() instead. + + + + + LoadAsset + Returns a Model containing the Instance that resides at AssetId on the web. This call will also yield the script until the model is returned. Script execution can still continue, however, if you use a <a href="http://wiki.roblox.com/index.php?title=Coroutine" target="_blank">coroutine</a>. + + + + + LoadAssetVersion + Similar to LoadAsset, but instead an AssetVersionId is passed in, which refers to a particular version of the asset which is not neccessarily the latest version. + + + + + + + Hat + Avatar + 30 + 45 + true + + + + + Accessory + Avatar + 30 + 32 + Model + Model + + + + + LocalBackpack + + + + + LocalBackpackItem + + + + + MotorFeature + true + + + + + Attachment + Constraints + 30 + 81 + PVInstance + PVInstance + + + + + + Rotation + + + + + WorldRotation + true + Deprecated. Use WorldOrientation instead + + + + + Orientation + Euler angles applied in YXZ order + + + + + WorldOrientation + Euler angles applied in YXZ order + + + + + + + + Constraint + Physics + 30 + 86 + BasePart + BasePart + + + + + Enabled + Toggles whether or not this constraint is enabled. Disabled constraints will not render in game. + + + + + Color + The color of the in-game visual. + + + + + Visible + Toggles the in-game visual associated with this constraint. + + + + + + + + BallSocketConstraint + Constraints + 30 + 86 + BasePart + BasePart + + + + + LimitsEnabled + Enables the angular limit between the axis of Attachment0 and the axis of Attachment1. + + + + + UpperAngle + Maximum angle between the two main axes. Value in [0, 180]. + + + + + Restitution + Restitution of the limit, or how elastic it is. Value in [0, 1]. + + + + + TwistLimitsEnabled + Enables the angular limits around the main axis of Attachment1. + + + + + TwistUpperAngle + Upper angular limit around the axis of Attachment1. Value in [-180, 180]. + + + + + TwistLowerAngle + Lower angular limit around the axis of Attachment1. Value in [-180, 180]. + + + + + Radius + Radius of the in-game visual. Value in [0, inf). + + + + + + + + RopeConstraint + Constraints + 30 + 89 + BasePart + BasePart + + + + + Length + The length of the rope or the maximum distance between the two attachments. Value in [0, inf). + + + + + Restitution + Restitution of the rope, or how elastic it is. Value in [0, 1]. + + + + + CurrentDistance + Current distance between the two attachments. Value in [0, inf). + + + + + Thickness + The thickness of the in-game visual (diameter). Value in [0, inf). + + + + + + + + RodConstraint + Constraints + 30 + 90 + BasePart + BasePart + + + + + Length + The length of the rod or the distance to be maintained between the two attachments. Value in [0, inf). + + + + + CurrentDistance + Current distance between the two attachments. Value in [0, inf). + + + + + Thickness + The thickness of the in-game visual (diameter). Value in [0, inf). + + + + + + + + SpringConstraint + Constraints + 30 + 91 + BasePart + BasePart + + + + + LimitsEnabled + Enables limits on the length of the spring. + + + + + Stiffness + The stiffness parameter of the spring. Force is scaled based on distance from the free length. The units of this property are force / distance. Value in [0, inf). + + + + + Damping + The damping parameter of the spring. The force is scaled with respect to relative velocity. The units of this property are force / velocity. Value in [0, inf). + + + + + FreeLength + The distance (in studs) between the two attachments at which the spring exerts no stiffness force. Value in [0, inf). + + + + + MaxForce + The maximum force that the spring can apply. Useful to prevent instabilities. The units are mass * studs / seconds^2. Value in [0, inf). + + + + + MaxLength + Maximum spring length, or the maxium distance between the two attachments. Value in [0, inf). + + + + + MinLength + Minimum spring length, or the minimum distance between the two attachments. Value in [0, inf). + + + + + Radius + The radius of the in-game spring coil visual. Value in [0, inf). + + + + + Thickness + The thickness of the spring wire (diameter) in the in-game visual. Value in [0, inf). + + + + + Coils + The number of coils in the in-game visual. Value in [0, 8]. + + + + + CurrentLength + Current distance between the two attachments. Value in [0, inf). + + + + + + + + WeldConstraint + Constraints + 30 + 94 + PVInstance + PVInstance + + + + + + HingeConstraint + Constraints + 30 + 87 + BasePart + BasePart + + + + + ActuatorType + Type of the rotational actuator: None, Motor, or Servo. + + + + + LimitsEnabled + Enables the angular limits on rotations around the main axis of Attachment0. + + + + + UpperAngle + Upper limit for the angle from the SecondaryAxis of Attachment0 to the SecondaryAxis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + LowerAngle + Lower limit for the angle from the SecondaryAxis of Attachment0 to the SecondaryAxis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + AngularRestitution + Restitution of the two limits, or how elastic they are. Value in [0,1]. + + + + + AngularVelocity + The target angular velocity of the motor in radians per second around the rotation axis. Value in [0, inf). + + + + + MotorMaxTorque + The maximum torque the motor can apply to achieve the target angular velocity. Value in [0, inf). + + + + + MotorMaxAcceleration + The maximum angular acceleration of the motor in radians per second square. Value in [0, inf). + + + + + AngularSpeed + Target angular speed. This value is unsigned as the servo will always move toward its target. Value in [0, inf). + + + + + ServoMaxTorque + Maximum torque the servo motor can apply. Value in [0, inf). + + + + + TargetAngle + Target angle for the SecondaryAxis of Attachment1 from the SecondaryAxis of Attachment0 around the rotation axis. Value in [-180, 180]. + + + + + CurrentAngle + Signed angle between the SecondaryAxis of Attchement0 and the SecondaryAxis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + Radius + Radius of the in-game visual. Value in [0, inf). + + + + + + + + SlidingBallConstraint + Constraints + 30 + 88 + BasePart + BasePart + + + + + ActuatorType + Type of linear actuator (along the axis of the slider): None, Motor, or Servo. + + + + + LimitsEnabled + Enables the limits on the linear motion along the axis of the slider. + + + + + LowerLimit + Lower limit for the position of Attachment1 with respect to Attachment0 along the slider axis. Value in (-inf, inf). + + + + + UpperLimit + Upper limit for the position of Attachment1 with respect to Attachment0 along the slider axis. Value in (-inf, inf). + + + + + Restitution + Restitution of the two limits, or how elastic they are. Value in [0, 1]. + + + + + Velocity + The target linear velocity of the motor in studs per second along the slider axis. Value in (-inf, inf). + + + + + MotorMaxForce + The maximum force the motor can apply to achieve the target velocity. Units are mass * studs / seconds^2. Value in [0, inf). + + + + + MotorMaxAcceleration + The maximum acceleration of the motor in studs per second squared. Value in [0, inf). + + + + + Speed + Target speed in studs per second. This value is unsigned as the servo will always move toward its target. Value in [0, inf). + + + + + ServoMaxForce + Maximum force the servo motor can apply. Units are mass * studs / seconds^2. Value in [0, inf). + + + + + TargetPosition + Target position of Attachment1 with respect to Attachment0 along the slider axis. Value in (-inf, inf). + + + + + CurrentPosition + Current position of Attachment1 with respect to Attachment0 along the slider axis. Value in (-inf, inf). + + + + + Size + Size of the in-game visual associated with this constraint. Value in [0, inf). + + + + + + + + PrismaticConstraint + Constraints + 30 + 88 + BasePart + BasePart + + + + + + CylindricalConstraint + Constraints + 30 + 95 + BasePart + BasePart + + + + + InclinationAngle + Direction of the rotation axis as an angle from the x-axis in the xy-plane of Attachment0. Value in [-180, 180]. + + + + + AngularActuatorType + Type of angular actuator: None, Motor, or Servo. + + + + + AngularLimitsEnabled + Enables the angular limits around the rotation axis. + + + + + UpperAngle + Upper limit for the angle (in degrees) between the reference axis and the SecondaryAxis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + LowerAngle + Lower limit for the angle (in degrees) between the reference axis and the SecondaryAxis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + AngularRestitution + Restitution of the two limits, or how elastic they are. Value in [0, 1]. + + + + + AngularVelocity + The target angular velocity of the motor in radians per second around the rotation axis. Value in [0, inf). + + + + + MotorMaxTorque + The maximum torque the motor can apply to achieve the target angular velocity. The units are mass * studs^2 / second^2. Value in [0, inf). + + + + + MotorMaxAngularAcceleration + The maximum angular acceleration of the motor in radians per second squared. Value in [0, inf). + + + + + AngularSpeed + Target angular speed. This value is unsigned as the servo will always move toward its target. In radians per second. Value in [0, inf). + + + + + ServoMaxTorque + Maximum torque the servo motor can apply. The units are mass * studs^2 / second^2. Value in [0, inf). + + + + + TargetAngle + Target angle (in degrees) between the reference axis and the secondary axis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + CurrentAngle + Signed angle (in degrees) between the reference axis and the secondary axis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + WorldRotationAxis + The unit vector direction of the rotation axis in world coordinates. + + + + + RotationAxisVisible + Enable the visibility of the rotation axis. + + + + + + + + AlignOrientation + Constraints + 30 + 82 + BasePart + BasePart + + + + + AlignPosition + Constraints + 30 + 82 + BasePart + BasePart + + + + + VectorForce + Constraints + 30 + 82 + Model + BasePart,Model + + + + + LineForce + Constraints + 30 + 82 + BasePart + BasePart + + + + + Torque + Constraints + 30 + 82 + BasePart + BasePart + + + + + Mouse + Used to receive input from the user. Actually tracks mouse events and keyboard events. + + + + + Hit + The CoordinateFrame of where the Mouse ray is currently hitting a 3D object in the Workspace. If the mouse is not over any 3D objects in the Workspace, this property is nil. + + + + + Icon + The current Texture of the Mouse Icon. Stored as a string, for more information on how to format the string <a href="http://wiki.roblox.com/index.php/Content" target="_blank">go here</a> + + + + + Origin + The CoordinateFrame of where the Mouse is when the mouse is not clicking. + + + + + Origin + The CoordinateFrame of where the Mouse is when the mouse is not clicking. This CoordinateFrame will be very close to the Camera.CoordinateFrame. + + + + + Target + The Part the mouse is currently over. If the mouse is not currently over any object (on the skybox, for example) this property is nil. + + + + + TargetFilter + A Part or Model that the Mouse will ignore when trying to find the Target, TargetSurface and Hit. + + + + + TargetSurface + The NormalId (Top, Left, Down, etc.) of the face of the part the Mouse is currently over. + + + + + UnitRay + The Unit Ray from where the mouse is (Origin) to the current Mouse.Target. + + + + + ViewSizeX + The viewport's (game window) width in pixels. + + + + + ViewSizeY + The viewport's (game window) height in pixels. + + + + + X + The absolute pixel position of the Mouse along the x-axis of the viewport (game window). Values start at 0 on the left hand side of the screen and increase to the right. + + + + + Y + The absolute pixel position of the Mouse along the y-axis of the viewport (game window). Values start at 0 on the top of the screen and increase to the bottom. + + + + + + + Button1Down + Fired when the first button (usually the left, but could be another) on the mouse is depressed. + + + + + Button1Up + Fired when the first button (usually the left, but could be another) on the mouse is release. + + + + + Button2Down + This event is currently non-operational. + + + + + Button2Up + This event is currently non-operational. + + + + + Idle + Fired constantly when the mouse is not firing any other event (i.e. the mouse isn't moving, nor any buttons being pressed or depressed). + + + + + KeyDown + Fired when a user presses a key on the keyboard. Argument is a string representation of the key. If the key has no string representation (such as space), the string passed in is the keycode for that character. Keycodes are currently in ASCII. + + + + + KeyUp + Fired when a user releases a key on the keyboard. Argument is a string representation of the key. If the key has no string representation (such as space), the string passed in is the keycode for that character. Keycodes are currently in ASCII. + + + + + Move + Fired when the mouse X or Y member changes. + + + + + WheelBackward + This event is currently non-operational. + + + + + WheelForward + This event is currently non-operational. + + + + + + + ProfilingItem + + + + + ChangeHistoryService + + + + + RotateP + BasePart + BasePart + + + + + RotateV + BasePart + BasePart + + + + + ScriptContext + + + + + Selection + + + + + VelocityMotor + BasePart + BasePart + + + + + Weld + 200 + 34 + BasePart + BasePart + + + + + TaskScheduler + false + + + + + SetThreadShare + true + Deprecated + + + + + + + StatsItem + + + + + Snap + 200 + 34 + BasePart + BasePart + + + + + FileMesh + BasePart + BasePart + + + + + ClickDetector + 3D Interfaces + Raises mouse events for parent object + 30 + 41 + BasePart + PVInstance + + + + + MaxActivationDistance + The maximum distance a Player's character can be from the ClickDetector's parent Part that will allow the Player's mouse to fire events on this object. + + + + + + + MouseClick + Fired when a player clicks on the parent Part of ClickDetector. The argument provided is always of type Player. + + + + + MouseHoverEnter + Fired when a player's mouse enters on the parent Part of ClickDetector. The argument provided is always of type Player. + + + + + MouseHoverLeave + Fired when a player's mouse leaves the parent Part of ClickDetector. The argument provided is always of type Player. + + + + + + + + Clothing + 20 + + + + + + Smoke + Effects + Makes the parent part or model object emit smoke + 30 + 59 + BasePart + BasePart + + + + + + Trail + Effects + Makes two attachments emit trail when moving + 30 + 93 + Model + BasePart,Model + + + + + LightEmission + 0 + 1 + + + + + + LightInfluence + 0 + 1 + + + + + + ZOffset + -1 + 1 + + + + + + Lifetime + 0 + 20 + + + + + + TextureLength + 0 + 5 + 40 + + + + + + MinLength + 0 + 1 + + + + + + + + + Beam + Effects + Makes beam between two attachments + 30 + 96 + BasePart + BasePart,Model + + + + + LightEmission + 0 + 1 + + + + + + LightInfluence + 0 + 1 + + + + + + TextureSpeed + -1 + 1 + + + + + + TextureLength + 0 + 5 + 40 + + + + + + CurveSize0 + -10 + 10 + + + + + + CurveSize1 + -10 + 10 + + + + + + ZOffset + -1 + 1 + + + + + + + + ParticleEmitter + Effects + A generic particle system. + 30 + 80 + BasePart + BasePart,Attachment + + + + + LightEmission + 0 + 1 + + + + + LightInfluence + Specifies the amount of influence lighting has on the particle emmitter. A value of 0 is unlit, 1 is fully lit. Fractional values blend from unlit to lit. + 0 + 1 + + + + + + Drag + 0 + 5 + + + + + VelocityInheritance + 0 + 1 + + + + + Rate + 0 + 100 + 100 + + + + + + Rotation + -180 + 180 + 72 + + + + + RotSpeed + -360 + 360 + 72 + + + + + Speed + 0 + 100 + 100 + + + + + Lifetime + 0 + 5 + + + + + + + + Sparkles + Effects + Makes the parent part or model object fantastic + 30 + 42 + BasePart + BasePart + + + + + Explosion + Effects + 30 + 36 + Creates an Explosion! This can be used as a purely graphical effect, or can be made to damage objects. + BasePart + Basepart,Model + + + + + BlastPressure + How much force this Explosion exerts on objects within it's BlastRadius. Setting this to 0 creates a purely graphical effect. A larger number will cause Parts to fly away at higher velocities. + + + + + BlastRadius + How big the Explosion is. This is a circle starting from the center of the Explosion's Position, the larger this property the larger the circle of destruction. + + + + + Position + Where the Explosion occurs in absolute world coordinates. + + + + + ExplosionType + Defines the behavior of the Explosion. <a href="http://wiki.roblox.com/index.php/ExplosionType" target="_blank">More info</a> + + + + + + + Fire + Effects + Makes the parent part or model object emit fire + 30 + 61 + BasePart + BasePart + + + + + Color + The color of the base of the fire. See SecondaryColor for more. + + + + + Heat + How hot the fire appears to be. The flame moves quicker the higher this value is set. + + + + + SecondaryColor + The color the fire interpolates to from Color. The longer a particle exists in the fire, the close to this color it becomes. + + + + + Size + How large the fire appears to be. + + + + + + + Seat + Interaction + 30 + 35 + + + + + Platform + + Equivalent to a seat, except that the character stands up rather than sits down. + 30 + 35 + + + + + SkateboardPlatform + true + 30 + 35 + + + + + VehicleSeat + Interaction + Automatically finds and powers hinge joints in an assembly. Ignores motors. + 30 + 35 + Model + Model + + + + + Tool + Interaction + 30 + 17 + StarterPack + StarterPack,Backpack + + + + + Flag + true + 30 + 38 + + + + + CanBeDropped + If someone is carrying this flag, this bool determines whether or not they can drop it and run. + + + + + TeamColor + The Team this flag is for. Corresponds with the TeamColors in the Teams service. + + + + + + + FlagStand + true + 30 + 39 + + + + + BackpackItem + 20 + + + + + Decal + 3D Interfaces + 40 + 7 + Descibes a texture that is placed on one of the sides of the Part it is parented to. + BasePart + BasePart + + + + + Face + Describes the face of the Part the decal will be applied to. <a href="http://wiki.roblox.com/index.php/NormalId" target="_blank">More info</a> + + + + + Shiny + How much light will appear to reflect off of the decal. + + + + + Specular + How light will react to the surface of the decal. + + + + + Transparency + How visible the decal is. 1 is completely invisible, while 0 is completely opaque + 0 + 1 + + + + + + + JointInstance + 200 + 34 + + + + + Message + 110 + 33 + true + StarterGui + StarterGui + + + + + Hint + true + 110 + 33 + + + + + IntValue + Values + 30 + 4 + Stores a int value in it's Value member. Useful to share int information across multiple scripts. + + + + + RayValue + Values + 30 + 4 + Stores a Ray value in it's Value member. Useful to share Ray information across multiple scripts. + + + + + IntConstrainedValue + true + Values + 30 + 4 + Stores an int value in it's Value member. Value is clamped to be in range of Min and MaxValue. Useful to share int information across multiple scripts. + + + + MaxValue + The maximum we allow this Value to be set. If Value is set higher than this, it automatically gets adjusted to MaxValue + + + + + MinValue + The minimum we allow this Value to be set. If Value is set lower than this, it automatically gets adjusted to MinValue + + + + + + DoubleConstrainedValue + true + Values + 30 + 4 + Stores a double value in it's Value member. Value is clamped to be in range of Min and MaxValue. Useful to share double information across multiple scripts. + + + + + MaxValue + The maximum we allow this Value to be set. If Value is set higher than this, it automatically gets adjusted to MaxValue + + + + + MinValue + The minimum we allow this Value to be set. If Value is set lower than this, it automatically gets adjusted to MinValue + + + + + + + BoolValue + Values + 30 + 4 + Stores a boolean value in it's Value member. Useful to share boolean information across multiple scripts. + + + + + CustomEvent + 30 + true + 4 + + + + + CustomEventReceiver + 30 + true + 4 + + + + + FloorWire + true + 30 + 4 + Renders a thin cylinder than can be adorned with textures that 'flow' from one object to the next. Has basic pathing abilities and attempts to to not intersect anything. <a href="http://wiki.roblox.com/index.php/FloorWire_Guide" target="_blank">More info</a> + + + + + CycleOffset + Controls how the decals are positioned along the wire. <a href="http://wiki.roblox.com/index.php/CycleOffset" target="_blank">More info</a> + + + + + From + The object the FloorWire 'emits' from + + + + + StudsBetweenTextures + The space between two textures on the wire. Note: studs are relative depending on how far the camera is from the FloorWire. + + + + + Texture + The image we use to render the textures that flow from beginning to end of the FloorWire. + + + + + TextureSize + The size in studs of the Texture we use to flow from one object to the next. + + + + + To + The object the FloorWire 'emits' to + + + + + Velocity + The rate of travel that the textures flow along the wire. + + + + + WireRadius + How thick the wire is. + + + + + + + NumberValue + Values + 30 + 4 + + + + + StringValue + Values + 30 + 4 + + + + + Vector3Value + Values + 30 + 4 + + + + + CFrameValue + Values + 30 + 4 + Stores a CFrame value in it's Value member. Useful to share CFrame information across multiple scripts. + + + + + Color3Value + Values + 30 + 4 + Stores a Color3 value in it's Value member. Useful to share Color3 information across multiple scripts. + + + + + BrickColorValue + Values + 30 + 4 + Stores a BrickColor value in it's Value member. Useful to share BrickColor information across multiple scripts. + + + + + ValueBase + Values + 30 + 4 + The base class to all Value Objects. + + + + + ObjectValue + Values + 30 + 4 + + + + + SpecialMesh + Meshes + 30 + 8 + BasePart + BasePart + + + + + BlockMesh + Meshes + 30 + 8 + BasePart + BasePart + + + + + CylinderMesh + Meshes + 30 + 8 + BasePart + BasePart + true + + + + + BevelMesh + Meshes + false + true + + + + + DataModelMesh + false + + + + + + Texture + 3D Interfaces + 40 + 10 + BasePart + BasePart + + + + + Sound + Sounds + 10 + 11 + SoundGroup,SoundService + + + + + play + true + Deprecated. Use Play() instead + + + + + + + PlayOnRemove + The sound will play when it is removed from the Workspace. Looped sounds don't play + + + + + + + + EchoSoundEffect + An echo audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + Sound,SoundGroup + + + + + Delay + 0.1 + 5 + 100 + + + + + Feedback + 0 + 1 + 100 + + + + + DryLevel + -80 + 10 + 100 + + + + + WetLevel + -80 + 100 + 100 + + + + + + + + FlangeSoundEffect + A Flanging audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + Sound,SoundGroup + + + + + Mix + 0 + 1 + 100 + + + + + Depth + 0.01 + 1 + 100 + + + + + Rate + 0 + 20 + 100 + + + + + + + + DistortionSoundEffect + A Distortion audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + Sound,SoundGroup + + + + + Level + 0 + 1 + 100 + + + + + + + + PitchShiftSoundEffect + A Pitch Shifting audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + Sound,SoundGroup + + + + + Octave + 0.5 + 2 + 100 + + + + + + + + ChorusSoundEffect + A Chorus audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + Sound,SoundGroup + + + + + Mix + 0 + 1 + 100 + + + + + Rate + 0 + 20 + 100 + + + + + Depth + 0 + 1 + 100 + + + + + + + + TremoloSoundEffect + A Tremolo audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + Sound,SoundGroup + + + + + Frequency + 0.1 + 20 + 100 + + + + + Depth + 0 + 1 + 100 + + + + + Duty + 0 + 1 + 100 + + + + + + + + ReverbSoundEffect + A Reverb audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + Sound,SoundGroup + + + + + DecayTime + 0.1 + 20 + 100 + + + + + Diffusion + 0 + 1 + 100 + + + + + Density + 0 + 1 + 100 + + + + + DryLevel + -80 + 20 + 100 + + + + + WetLevel + -80 + 20 + 100 + + + + + + + + EqualizerSoundEffect + An Three-band Equalizer audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + Sound,SoundGroup + + + + + LowGain + -80 + 10 + 100 + + + + + MidGain + -80 + 10 + 100 + + + + + HighGain + -80 + 10 + 100 + + + + + + + + CompressorSoundEffect + A Compressor audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + Sound,SoundGroup + + + + + Threshold + -80 + 0 + 100 + + + + + Attack + 0.001 + 1 + 100 + + + + + Release + 0.001 + 5 + 100 + + + + + Ratio + 1 + 50 + 100 + + + + + GainMakeup + 0 + 30 + 100 + + + + + + + + SoundGroup + Sounds + 20 + 85 + SoundService + SoundService + + + + + + + + + StockSound + false + -1 + + + + + SoundService + 500 + 31 + + + + + + AmbientReverb + + The ambient sound environment. May not work when using hardware sound + + + + + DopplerScale + + The doppler scale is a general scaling factor for how much the pitch varies due to doppler shifting in 3D sound. Doppler is the pitch bending effect when a sound comes towards the listener or moves away from it, much like the effect you hear when a train goes past you with its horn sounding. With dopplerscale you can exaggerate or diminish the effect. + + + + + DistanceFactor + + the relative distance factor, compared to 1.0 meters. + + + + + RolloffScale + + Setting this value makes the sound drop off faster or slower. The higher the value, the faster volume will attenuate, and conversely the lower the value, the slower it will attenuate. For example a rolloff factor of 1 will simulate the real world, where as a value of 2 will make sounds attenuate 2 times quicker. + + + + + + + Backpack + 30 + 20 + false + Player + + + + + StarterPack + 30 + 20 + + + + + StarterPlayer + 30 + 79 + + + + + StarterGear + 30 + 20 + false + + + + + + CoreGui + 30 + 46 + + + + + + + PluginGuiService + 30 + 46 + + + + + + + Studio + + + + + Show Plugin GUI Service in Explorer + + + + + + + + UIGridStyleLayout + GUI + false + GuiBase2d + GuiObject,GuiBase2d + + + + + + SetCustomSortFunction + When SortOrder is set to Custom, this lua function is used to determine the ordering of elements. Function should take two arguments (each will be an Instance child to compare), and return true if a comes before b, otherwise return false. In other words, use this function the same way you would use a table.sort function. The sorting should be deterministic, otherwise sort will fail and fall back to name order. + true + + + + + ApplyLayout + Forces a relayout of all elements. Useful when sort is set to Custom. + + + + + + + + SortOrder + Determines how we decide which element to place next. Can be Name or Custom. If using Custom, make sure SetCustomSortFunction was called with an appropriate sort function. + + + + + FillDirection + Determines which direction to fill the grid. Can be Horizontal or Vertical. + + + + + HorizontalAlignment + Determines how grid is placed within it's parent's container in the x direction. Can be Left, Center, or Right. + + + + + VerticalAlignment + Determines how grid is placed within it's parent's container in the y direction. Can be Top, Center, or Bottom. + + + + + + + + UIListLayout + 30 + 26 + GUI + Sets the position of UI elements in a list. You can use a UIListLayout by parenting it to a GuiObject. The UIListLayout will then apply itself to all of its GuiObject siblings. + GuiBase2d + GuiObject,GuiBase2d + + + + + + Padding + Determines the amount of free space between each element. Can be set either using scale (Percentage of parent's size in the current direction) or offset (a static spacing value, similar to pixel size). + + + + + + + + UIGridLayout + 30 + 26 + GUI + Sets the position of UI elements in a 2D grid (this can be modified to 1D grid for list layout). This will also set the elements to a particular size, although this can be overridden with particular constraints on elements. You can use a UIGridLayout by parenting it to a GuiObject. The UIGridLayout will then apply itself to all of its GuiObject siblings. + GuiBase2d + GuiObject,GuiBase2d + + + + + + CellSize + Denotes what size each element should be. Can be overridden by elements using constraints on individual elements. + + + + + CellPadding + How much space between elements there should be. + + + + + FillDirectionMaxCells + Determines how many cells over in the FillDirection we go before starting a new row or column. Set to 0 for max cell count. Will be clamped if this is set higher than the parent container allows room for. + + + + + AbsoluteSize + Returns the current size of the grid. If more elements are added, this can increase. If elements are removed this can decrease. + + + + + StartCorner + Which corner we start laying the elements out from. Can be TopLeft, TopRight, BottomLeft, BottomRight. + + + + + + + + + UIPageLayout + 30 + 26 + GUI + Creates a paged viewing window, like the home screen of a mobile device. You can use a UIPageLayout by parenting it to a GuiObject. The UIPageLayout will then apply itself to all of its GuiObject siblings. + GuiBase2d + GuiObject,GuiBase2d + + + + + + CurrentPage + The page that is either currently being displayed or is the target of the current animation. + + + + + + Circular + Whether or not the page layout wraps around at the ends. + + + + + + Padding + Determines the amount that pages are separated from each other by. Can be set either using scale (Percentage of parent's size in the current direction) or offset (a static spacing value, similar to pixel size). + + + + + + Animated + Whether or not to animate transitions between pages. + + + + + + EasingStyle + The easing style to use when performing an animation. + + + + + + EasingDirection + The easing direction to use when performing an animation. + + + + + + TweenTime + The length of the animation. + + + + + + + + Next + Sets CurrentPage to the page after the current page and animates to it, or does nothing if there isn't a next page. + + + + + Previous + Sets CurrentPage to the page after the current page and animates to it, or does nothing if there isn't a next page. + + + + + JumpTo + If the instance is in the layout, then it sets CurrentPage to it and animtes to it. If circular layout is set, it will take the shortest path. + + + + + JumpToIndex + If the index is >= 0 and less than the size of the layout, acts like JumpTo. If it's out of bounds and circular is set, it will animate the full distance between the in-bounds index of CurrentPage and the new index. + + + + + + + + PageEnter + Fires when a page comes into view, and is going to be rendered. + + + + + PageLeave + Fires when a page leaves view, and will not be rendered. + + + + + Stopped + Fires when an animation to CurrentPage is completed without being cancelled, and the view stops scrolling. + + + + + + + + UITableLayout + 30 + 26 + GUI + Provides a layout of rows and columns that are sized based on the cells in them. + GuiBase2d + GuiObject,GuiBase2d + + + + + + Padding + The amount of padding to insert in between the cells of the table. + + + + + + FillEmptySpaceRows + Whether the table should expand to fill the available space of its container, row-wise. + + + + + + FillEmptySpaceColumns + Whether the table should expand to fill the available space of its container, column-wise. + + + + + + MajorAxis + Whether the direct siblings are considered the rows or the columns. The children of the direct siblings are the columns or rows, respectively. + + + + + + + + UISizeConstraint + 30 + 26 + GUI + Ensures a GuiObject does not become smaller or larger than the min and max size. If an element with a constraint is under the control of a layout, the constraint takes precedence in determining the element’s size, but not position. You can use a Constraint by parenting it to the element you wish to constrain. + GuiBase2d + GuiObject,GuiBase2d + + + + + + MinSize + The smallest size the GuiObject is allowed to be. + + + + + MaxSize + The biggest size the GuiObject is allowed to be. + + + + + + + + UITextSizeConstraint + 30 + 26 + GUI + Ensures a GuiObject with text does not allow the font size to become larger or smaller than min and max text sizes. If an element with a constraint is under the control of a layout, the constraint takes precedence in determining the element’s size, but not position. You can use a Constraint by parenting it to the element you wish to constrain. + GuiBase2d + GuiObject,GuiBase2d + + + + + + MinTextSize + The smallest size the font is allowed to be. + + + + + MaxTextSize + The biggest size the font is allowed to be. + + + + + + + + UIAspectRatioConstraint + 30 + 26 + GUI + Ensures a GuiObject will always have a particular aspect ratio. If an element with a constraint is under the control of a layout, the constraint takes precedence in determining the element’s size, but not position. You can use a Constraint by parenting it to the element you wish to constrain. + GuiObject,GuiBase2d + + + + + + AspectRatio + The aspect ratio to maintain. This is the width/height. Only positive numbers allowed. + + + + + AspectType + Describes how the aspect ratio will determine its size. Options are FitWithinMaxSize, ScaleWithParentSize. FitWithinMaxSize will make the element the maximum size it can be within the current possible AbsoluteSize of the element while maintaining the AspectRatio. ScaleWithParentSize will make the element the closest to the parent element’s maximum size while maintaining aspect ratio. + + + + + DominantAxis + Describes which axis to use when determining the new size of the element, while keeping respect to the aspect ratio. + + + + + + + + UIScale + 30 + 26 + GUI + Uniformly scales a GUI object and all its children. + GuiBase2d + GuiObject,GuiBase2d + + + + + + Scale + The scale factor to apply. + + + + + + + + UIPadding + 30 + 26 + GUI + Insets the children of the GuiObject this is parented to, by the specified padding. + GuiBase2d + GuiObject,GuiBase2d + + + + + + PaddingLeft + The padding to apply on the left side relative to the parent's normal size. + + + + + PaddingRight + The padding to apply on the right side relative to the parent's normal size. + + + + + PaddingTop + The padding to apply on the top side relative to the parent's normal size. + + + + + PaddingBottom + The padding to apply on the bottom side relative to the parent's normal size. + + + + + + + + TweenBase + false + + + + + + PlaybackState + The current state of how the tween is animating. Possible values are Begin, Playing, Paused, Completed and Cancelled. This property is modified by using functions such as Tween:Play(), Tween:Pause(), and Tween:Cancel(). Read-only. + + + + + + + + Play + Starts or resumes (if Tween.PlaybackState is Paused) the tween animation. If current PlaybackState is Cancelled, this property will reset the tween to the beginning properties and play the animations from the beginning. + + + + + Pause + Temporarily stops the tween animation. Animation can be resumed by calling Play(). + + + + + Cancel + Stops the tween animation. Animation can be restarted by calling Play(). Animation will start from the beginning values. + + + + + + + + Completed + Fires when the tween either reaches PlaybackState Completed or Cancelled. PlaybackState of one of these types is passed as the first arg to the function listening to this event. + + + + + + + + + Tween + An object linked to an instance that animates properties on the instance over a specified period of time. Useful for easily moving UI objects around, rotating objects, etc. without having to write a lot of code. To create a new tween, please use TweenService:Create. + + + + + + Instance + The object this tween is operating on. Read-only. + + + + + + TweenInfo + Specifies how the tween animates. Read-only. + + + + + + + + + TweenService + Service responsible for creating tweens on instances. + + + + + + + Create + Creates a Tween object bound to a particular Instance. The first arg is the Instance to tween. The second arg is a TweenInfo struct, which specifies how a tween should behave. The third arg is a table, which should specify the properties to tween as keys, with the end value specified as values to the keys. + + + + + + + + + + StarterGui + 30 + 46 + + + + + + SetCoreGuiEnabled + Will stop/begin certain core gui elements being rendered. See CoreGuiType for core guis that can be modified. + + + + + GetCoreGuiEnabled + Returns a boolean describing whether a CoreGuiType is currently being rendered. + + + + + + + + GuiService + The GuiService is a special service, which currently allows developers to control what GuiObject is currently being selected by the Gamepad Gui navigator, and allows clients to check if Roblox's main menu is currently open. This service has a lot of hidden members, which are mainly used internally by Roblox's CoreScripts. + + + + + + GetGuiInset + Returns a Tuple containing two Vector2 values representing the offset of user GUIs in pixels from the top right corner of the screen and the bottom right corner of the screen respectively. + + + + + + + + ContextActionService + A service used to bind input to various lua functions. + + + + + + BindAction + Binds 'functionToBind' to fire when any 'inputTypes' happen. InputTypes can be variable in number and type. Types can be Enum.KeyCode, single character strings corresponding to keys, or Enum.UserInputType. 'actionName' is a key used by many other ContextActionService functions to query state. 'createTouchButton' if true will create a button on screen on touch devices. This button will fire 'functionToBind' with three arguments: first argument is the actionName, second argument is the UserInputState of the input, and the third is the InputObject that fired this function. If 'functionToBind' yields or returns nil or Enum.ContextActionResult.Sink, the input will be sunk. If it returns Enum.ContextActionResult.Pass, the next bound action in the stack will be invoked. + + + + + SetTitle + If 'actionName' key contains a bound action, then 'title' is set as the title of the touch button. Does nothing if a touch button was not created. No guarantees are made whether title will be set when button is manipulated. + + + + + SetDescription + If 'actionName' key contains a bound action, then 'description' is set as the description of the bound action. This description will appear for users in a listing of current actions availables. + + + + + SetImage + If 'actionName' key contains a bound action, then 'image' is set as the image of the touch button. Does nothing if a touch button was not created. No guarantees are made whether image will be set when button is manipulated. + + + + + SetPosition + If 'actionName' key contains a bound action, then 'position' is set as the position of the touch button. Does nothing if a touch button was not created. No guarantees are made whether position will be set when button is manipulated. + + + + + UnbindAction + If 'actionName' key contains a bound action, removes function from being called by all input that it was bound by (if function was also bound by a different action name as well, those bound input are still active). Will also remove any touch button created (if button was manipulated manually there is no guarantee it will be cleaned up). + + + + + UnbindAllActions + Removes all functions bound. No actionNames will remain. All touch buttons will be removed. If button was manipulated manually there is no guarantee it will be cleaned up. + + + + + GetBoundActionInfo + Returns a table with info regarding the function bound with 'actionName'. Table has the keys 'title' (current title that was set with SetTitle) 'image' (image set with SetImage) 'description' (description set with SetDescription) 'inputTypes' (tuple containing all input bound for this 'actionName') 'createTouchButton' (whether or not we created a touch button for this 'actionName'). + + + + + GetAllBoundActionInfo + Returns a table with all bound action info. Each entry is a key with 'actionName' and value being the same table you would get from ContextActionService:GetBoundActionInfo('actionName'). + + + + + + + + GetButton + If 'actionName' key contains a bound action, then this will return the touch button (if was created). Returns nil if a touch button was not created. No guarantees are made whether button will be retrievable when button is manipulated. + + + + + + + + PointsService + A service used to query and award points for Roblox users using the universal point system. + true + + + + + + PointsAwarded + Fired when points are successfully awarded 'userId'. Also returns the updated balance of points for usedId in universe via 'userBalanceInUniverse', total points via 'userTotalBalance', and the amount points that were awarded via 'pointsAwarded'. This event fires on the server and also all clients in the game that awarded the points. + + + + + + + + AwardPoints + Will attempt to award the 'amount' points to 'userId', returns 'userId' awarded to, the number of points awarded, the new point total the user has in the game, and the total number of points the user now has. Will also fire PointsService.PointsAwarded. Works with server scripts ONLY. + + + + + GetPointBalance + Returns the overall balance of points that player with userId has (the sum of all points across all games). Works with server scripts ONLY. + + + + + GetGamePointBalance + Returns the balance of points that player with userId has in the current game (all placeID points combined within the game). Works with server scripts ONLY. + + + + + GetAwardablePoints + Returns the number of points the current universe can award to players. Works with server scripts ONLY. + + + + + + + + Chat + 510 + 33 + + + + + + + + + + ChatService + 510 + 33 + + + + + + + + LocalizationTable + 30 + 97 + Localization + LocalizationService + A database of strings used in the game and their translations. + + + + + + LocalizationService + 530 + 92 + + + + PreferredLanguage + Gets the system's preferred language (A Language enum). + + + + + GetLocaleId + Gets the system's LocaleId (Ex: "en-US"). + + + + + + + MarketplaceService + 46 + + + + + + PromptPurchase + Will prompt 'player' to purchase the item associated with 'assetId'. 'equipIfPurchased' is an optional argument that will give the item to the player immediately if they buy it (only applies to gear). 'currencyType' is also optional and will attempt to prompt the user with a specified currency if the product can be purchased with this currency, otherwise we use the default currency of the product. + + + + + + + + GetProductInfo + Takes one argument "assetId" which should be a number of an asset on www.roblox.com. Returns a table containing the product information (if this process fails, returns an empty table). + + + + + PlayerOwnsAsset + Checks to see if 'Player' owns the product associated with 'assetId'. Returns true if the player owns it, false otherwise. This call will produce a warning if called on a guest player. + + + + + + + ProcessReceipt + Callback that is executed for pending Developer Product receipts. + + If this function does not return Enum.ProductPurchaseDecision.PurchaseGranted, then you will not be granted the money for the purchase! + + The callback will be invoked with a table, containing the following informational fields: + PlayerId - the id of the player making the purchase. + PlaceIdWherePurchased - the specific place where the purchase was made. + PurchaseId - a unique identifier for the purchase, should be used to prevent granting an item multiple times for one purchase. + ProductId - the id of the purchased product. + CurrencyType - the type of currency used (Tix, Robux). + CurrencySpent - the amount of currency spent on the product for this purchase. + + + + + + + + PromptPurchaseFinished + Fired when a 'player' dismisses a purchase dialog for 'assetId'. If the player purchased the item 'isPurchased' will be true, otherwise it will be false. This call will produce a warning if called on a guest player. + + + + + + + + UserInputService + + + + + TouchEnabled + Returns true if the local device accepts touch input, false otherwise. + + + + + KeyboardEnabled + Returns true if the local device accepts keyboard input, false otherwise. + + + + + MouseEnabled + Returns true if the local device accepts mouse input, false otherwise. + + + + + AccelerometerEnabled + Returns true if the local device has an accelerometer, false otherwise. + + + + + GyroscopeEnabled + Returns true if the local device has an gyroscope, false otherwise. + + + + + + + + TouchTap + Fired when a user taps their finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the tap gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchPinch + Fired when a user pinches their fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the pinch gesture. 'scale' is a float that indicates the difference from the beginning of the pinch gesture. 'velocity' is a float indicating how quickly the pinch gesture is happening. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchSwipe + Fired when a user swipes their fingers on a TouchEnabled device. 'swipeDirection' is an Enum.SwipeDirection, indicating the direction the user swiped. 'numberOfTouches' is an int that indicates how many touches were involved with the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchLongPress + Fired when a user holds at least one finger for a short amount of time on the same screen position on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchRotate + Fired when a user rotates two fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'rotation' is a float indicating how much the rotation has gone from the start of the gesture. 'velocity' is a float that indicates how quickly the gesture is being performed. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchPan + Fired when a user drags at least one finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'totalTranslation' is a Vector2, indicating how far the pan gesture has gone from its starting point. 'velocity' is a Vector2 that indicates how quickly the gesture is being performed in each dimension. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + + TouchStarted + Fired when a user places their finger on a TouchEnabled device. 'touch' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchMoved + Fired when a user moves their finger on a TouchEnabled device. 'touch' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchEnded + Fired when a user moves their finger on a TouchEnabled device. 'touch' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + + InputBegan + Fired when a user begins interacting via a Human-Computer Interface device (Mouse button down, touch begin, keyboard button down, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + InputChanged + Fired when a user changes interacting via a Human-Computer Interface device (Mouse move, touch move, mouse wheel, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + InputEnded + Fired when a user stops interacting via a Human-Computer Interface device (Mouse button up, touch end, keyboard button up, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + + TextBoxFocused + Fired when a user clicks/taps on a textbox to begin text entry. Argument is the textbox that was put in focus. This also fires if a textbox forces focus on the user. This event only fires locally. + + + + + TextBoxFocusReleased + Fired when a user stops text entry into a textbox (usually by pressing return or clicking/tapping somewhere else on the screen). Argument is the textbox that was taken out of focus. This event only fires locally. + + + + + DeviceAccelerationChanged + Fired when a user moves a device that has an accelerometer. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. This event only fires locally. + + + + + DeviceGravityChanged + Fired when the force of gravity changes on a device that has an accelerometer. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. This event only fires locally. + + + + + DeviceRotationChanged + Fired when a user rotates a device that has an gyroscope. This is fired with an InputObject, which has type Enum.InputType.Gyroscope, and position that shows total rotation in each local device axis. The delta property describes the amount of rotation that last happened. A second argument of Vector4 is the device's current quaternion rotation in reference to it's default reference frame. This event only fires locally. + + + + + + + + GetDeviceAcceleration + Returns an InputObject that describes the device's current acceleration. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. The delta property describes the amount of rotation that last happened. This event only fires locally. + + + + + GetDeviceGravity + Returns an InputObject that describes the device's current gravity vector. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. The delta property describes the amount of rotation that last happened. This event only fires locally. + + + + + GetDeviceRotation + Returns an InputObject and a Vector4 that describes the device's current rotation vector. This is fired with an InputObject, which has type Enum.InputType.Gyroscope, and position that shows total rotation in each local device axis. The delta property describes the amount of rotation that last happened. The Vector4 is the device's current quaternion rotation in reference to it's default reference frame. This event only fires locally. + + + + + + + + Sky + 5 + 28 + Lighting + Lighting + + + + + ColorCorrectionEffect + Post Processing Effects + 20 + 83 + Lighting + + + + + Brightness + -1 + 1 + + + + + Contrast + -1 + 1 + + + + + Saturation + -1 + 1 + + + + + + + BloomEffect + Post Processing Effects + 20 + 83 + Lighting + + + + + Intensity + 0 + 1 + + + + + Threshold + 0.8 + 4 + 1000 + + + + + Size + 0 + 56 + 56 + + + + + + + + BlurEffect + Post Processing Effects + 20 + 83 + Lighting + + + + + Size + 0 + 56 + 56 + + + + + + + + SunRaysEffect + Post Processing Effects + 20 + 83 + Lighting + + + + + Intensity + 0 + 1 + + + + + Spread + 0 + 1 + + + + + + + + Motor + 20 + false + BasePart + BasePart + + + + + Humanoid + Avatar + 30 + 9 + Model + Model + + + + + + MoveTo + Attempts to move the Humanoid and it's associated character to 'part'. 'location' is used as an offset from part's origin. + + + + + Jump + + + + + Sit + + + + + TakeDamage + Decreases health by the amount. Use this instead of changing health directly to make sure weapons are filtered for things such as ForceField(s). + + + + + UnequipTools + + Takes any active gear/tools that the Humanoid is using and puts them into the backpack. This function only works on Humanoids with a corresponding Player. + + + + + EquipTool + + Takes a specified tool and equips it to the Humanoid's Character. Tool argument should be of type 'Tool'. + + + + + ReplaceBodyPartR15 + Replaces the desired bodypart on the Humanoid's Character using a specified Enum.BodyPartR15 and BasePart. Returns a success boolean. + + + + + GetBodyPartR15 + Returns a Enum.BodyPartR15 given a body part in the Humanoid's Character. + + + + + + + NameOcclusion + + Sets how to display other humanoid names to this humanoid's player. <a href="http://wiki.roblox.com/index.php/NameOcclusion" target="_blank">More info</a> + + + Health + How many hit points the Humanoid has. When this number reaches 0 or goes below 0, the Humanoid's character falls apart and will respawn. + + + MaxHealth + The maximum number of hit points a Humanoid's health can reach. If the Humanoid's health is set over this amount, the health gets set to this value. + + + TargetPoint + The location that the Humanoid is trying to walk to. + + + Torso + Humanoid.RootPart will be the preferred way of getting a character's humanoid root part. + true + + + LeftLeg + In R6 this property get the player's left leg. In R15 this gets nothing. + true + + + RightLeg + In R6 this property get the player's right leg. In R15 this gets nothing. + true + + + + + + + BodyColors + Avatar + 20 + Model + Model + + + + + Shirt + Avatar + 20 + 43 + Model + Model + + + + + Pants + Avatar + 20 + 44 + Model + Model + + + + + ShirtGraphic + Avatar + 20 + 40 + Model + Model + + + + + Skin + true + 20 + + + + + DebugSettings + false + 20 + + + + + FaceInstance + false + + + + + GameSettings + false + 20 + + + + + GlobalSettings + false + 20 + + + + + Item + false + 20 + + + + + NetworkPeer + false + + + + + NetworkSettings + false + 20 + + + + + PVInstance + false + + + + + CoordinateFrame + true + Deprecated. Use CFrame instead + + + + + + + PackageLink + 1 + 98 + false + + + + + Status + Current status of the Package + true + + + + + + + RenderSettings + false + 20 + + + + + RootInstance + false + + + + + ServiceProvider + false + + + + + service + true + Use GetService() instead + + + + + GetService + Instance:isService:0 + + + + + FindService + Instance:isService:0 + + + + + + + ProfilingItem + false + + + + + NetworkMarker + false + + + + + + Hopper + true + Use StarterPack instead + 20 + + + + + + Instance + false + + + + + + Archivable + Determines whether or not an Instance can be saved when the game closes/attempts to save the game. Note: this only applies to games that use Data Persistence, or SavePlaceAsync. + + + + + ClassName + The string name of this Instance's most derived class. + + + + + Parent + The Instance that is directly above this Instance in the tree. + + + + + + + + + GetDebugId + false + This function is for internal testing. Don't use in production code + + + + + Clone + Returns a copy of this Object and all its children. The copy's Parent is nil + + + + + clone + true + Use Clone() instead + + + + + isA + true + Use IsA() instead + + + + + IsA + Returns a boolean if this Instance is of type 'className' or a is a subclass of type 'className'. If 'className' is not a valid class type in ROBLOX, this function will always return false. <a href="http://wiki.roblox.com/index.php/IsA" target="_blank">More info</a> + Instance:Any:0 + + + + + FindFirstChild + Returns the first child of this Instance that matches the first argument 'name'. The second argument 'recursive' is an optional boolean (defaults to false) that will force the call to traverse down thru all of this Instance's descendants until it finds an object with a name that matches the 'name' argument. The function will return nil if no Instance is found. + + + + + FindFirstChildOfClass + Returns the first child of this Instance that with a ClassName equal to 'className'. The function will return nil if no Instance is found. + Instance:isScriptCreatable:0 + + + + + FindFirstChildWhichIsA + Returns the first child of this Instance that :IsA(className). The second argument 'recursive' is an optional boolean (defaults to false) that will force the call to traverse down thru all of this Instance's descendants until it finds an object with a name that matches the 'className' argument. The function will return nil if no Instance is found. + Instance:Any:0 + + + + + FindFirstAncestor + Returns the first ancestor of this Instance that matches the first argument 'name'. The function will return nil if no Instance is found. + + + + + FindFirstAncestorOfClass + Returns the first ancestor of this Instance with a ClassName equal to 'className'. The function will return nil if no Instance is found. + Instance:isScriptCreatable:0 + + + + + FindFirstAncestorWhichIsA + Returns the first ancestor of this Instance that :IsA(className). The function will return nil if no Instance is found. + Instance:Any:0 + + + + + GetFullName + Returns a string that shows the path from the root node (DataModel) to this Instance. This string does not include the root node (DataModel). + + + + + children + true + Use GetChildren() instead + + + + + getChildren + true + Use GetChildren() instead + + + + + GetChildren + Returns a read-only table of this Object's children + + + + + GetDescendants + Returns an array containing all of the descendants of the instance. Returns in preorder traversal, or in other words, where the parents come before their children, depth first. + + + + + Remove + Deprecated. Use ClearAllChildren() to get rid of all child objects, or Destroy() to invalidate this object and its descendants + true + + + + + remove + true + Use Remove() instead + + + + + ClearAllChildren + Removes all children (but not this object) from the workspace. + + + + + Destroy + Removes object and all of its children from the workspace. Disconnects object and all children from open connections. Object and children may not be usable after calling Destroy. + + + + + findFirstChild + true + Use FindFirstChild() instead + + + + + + + + AncestryChanged + Fired when any of this object's ancestors change. First argument 'child' is the object whose parent changed. Second argument 'parent' is the first argument's new parent. + + + + + DescendantAdded + Fired after an Instance is parented to this object, or any of this object's descendants. The 'descendant' argument is the Instance that is being added. + + + + + DescendantRemoving + Fired after an Instance is unparented from this object, or any of this object's descendants. The 'descendant' argument is the Instance that is being added. + + + + + Changed + Fired after a property changes value. The property argument is the name of the property + + + + + + + + BodyGyro + Legacy Body Movers + Attempts to maintain a fixed orientation of its parent Part + 140 + 14 + BasePart + BasePart + + + + + + MaxTorque + The maximum torque that will be exerted on the Part + + + + + maxTorque + true + Use MaxTorque instead + + + + + D + The dampening factor applied to this force + + + + + P + The power continually applied to this force + + + + + CFrame + The cframe that this force is trying to orient its parent Part to. Note: this force only uses the rotation of the cframe, not the position. + + + + + cframe + true + Use CFrame instead + + + + + + + BodyPosition + Legacy Body Movers + 140 + 14 + BasePart + BasePart + + + + + + MaxForce + The maximum force that will be exerted on the Part + + + + + maxForce + true + Use MaxForce instead + + + + + D + The dampening factor applied to this force + + + + + P + The power factor continually applied to this force + + + + + Position + The Vector3 that this force is trying to position its parent Part to. + + + + + position + true + Use position instead + + + + + + + RocketPropulsion + Legacy Body Movers + 140 + 14 + A propulsion system that mimics a rocket + BasePart + BasePart + + + + + BodyVelocity + Legacy Body Movers + 140 + 14 + BasePart + BasePart + + + + + MaxForce + The maximum force that will be exerted on the Part in each axis + + + + + maxForce + true + Use MaxForce instead + + + + + P + The amount of power we add to the system. The higher the power, the quicker the force will achieve its goal. + + + + + Velocity + The velocity this system tries to achieve. How quickly the system reaches this velocity (if ever) is defined by P. + + + + + velocity + true + Use Velocity instead + + + + + + + BodyAngularVelocity + Legacy Body Movers + 140 + 14 + BasePart + BasePart + + + + MaxTorque + The maximum torque that will be exerted on the Part in each axis + + + maxTorque + true + Use MaxTorque instead + + + P + The amount of power we add to the system. The higher the power, the quicker the force will achieve its goal. + + + AngularVelocity + The rotational velocity this system tries to achieve. How quickly the system reaches this velocity is defined by P. + + + angularVelocity + true + Use AngularVelocity instead + + + + + + BodyForce + Legacy Body Movers + 140 + 14 + When parented to a physical part, BodyForce will continually exert a force upon its parent object. + BasePart + BasePart + + + + Force + The continual force exerted on an object, defined in each axis. + + + force + true + Use Force instead + + + + + + BodyThrust + Legacy Body Movers + 140 + 14 + BasePart + BasePart + + + + + + Force + The power continually applied to this force + + + + + force + true + Use Force instead + + + + + Location + The Vector3 location of where to apply the force to. + + + + + location + true + Use Location instead + + + + + + + Hole + true + 20 + + + + + Feature + 20 + + + + + + Teams + This Service-level object is the container for all Team objects in a level. A map that supports team games must have a Teams service. <a href="http://wiki.roblox.com/index.php/Team" target="_blank">More info</a> + 140 + 23 + Teams + Teams + + + + + GetPlayers + Returns a read-only table of players which are on this team. + + + + + + + Team + Interaction + The Team class is used to represent a faction in a team game. The only valid location for a Team object is under the Teams service. <a href="http://wiki.roblox.com/index.php/Team" target="_blank">More info</a> + 10 + 24 + Teams + Teams + + + + + SpawnLocation + Interaction + 30 + 25 + + + + + NetworkClient + false + 30 + 16 + + + + + NetworkServer + false + 30 + 15 + + + + + LuaSourceContainer + false + StarterPlayerScripts,StarterCharacterScripts,ServerScriptService + + + + + CurrentEditor + The name of the player who is currently editing the script in Team Create. + true + + + + + + + Script + Scripting + 30 + 6 + ServerStorage,ServerScriptService + + + + + + LinkedScript + + This property is under development. Do not use + + + + + + + + LocalScript + Scripting + 40 + 18 + ReplicatedFirst,ReplicatedStorage,StarterCharacterScripts,StarterPlayerScripts + A script that runs on clients, NOT servers. LocalScripts can only run when parented under one of the following: + 1) A player's Backpack. + 2) A player's Character model. + 3) A player's PlayerGui. + 4) A player's PlayerScripts. + 5) The ReplicatedFirst service. + + + + + + + RenderingTest + Scripting + 40 + dummy summary + 5 + + + + + + NetworkReplicator + 30 + 29 + + + + + + Model + 100 + 2 + A construct used to group Parts and other objects together, also allows manipulation of multiple objects. + PVInstance + PVInstance + + + + + BreakJoints + Breaks all surface joints contained within + + + + + GetModelCFrame + Returns a CFrame that has position of the centroid of all Parts in the Model. The rotation matrix is either the rotation matrix of the user-defined PrimaryPart, or if not specified then a part in the Model chosen by the engine. + + + + + GetModelSize + Returns a Vector3 that is union of the extents of all Parts in the model. + + + + + MakeJoints + Creates the appropriate SurfaceJoints between all touching Parts contrained within the model. Technically, this function calls MakeJoints() on all Parts inside the model. + + + + + MoveTo + Moves the centroid of the Model to the specified location, respecting all relative distances between parts in the model. + + + + + ResetOrientationToIdentity + Rotates all parts in the model to the orientation that was set using SetIdentityOrientation(). If this function has never been called, rotation is reset to GetModelCFrame()'s rotation. + + + + + SetIdentityOrientation + Takes the current rotation matrix of the model and stores it as the model's identity matrix. The rotation is applied when ResetOrientationToIdentity() is called. + + + + + TranslateBy + Similar to MoveTo(), except instead of moving to an explicit location, we use the model's current CFrame location and offset it. + + + + + GetPrimaryPartCFrame + Returns the cframe of the Model.PrimaryPart. If PrimaryPart is nil, then this function will throw an error. + + + + + SetPrimaryPartCFrame + Sets the cframe of the Model.PrimaryPart. If PrimaryPart is nil, then this function will throw an error. This also sets the cframe of all descendant Parts relative to the cframe change to PrimaryPart. + + + + + makeJoints + Use MakeJoints() instead + true + + + + + move + true + Use MoveTo() instead + + + + + + + PrimaryPart + A Part that serves as a reference for the Model's CFrame. Used in conjunction with GetModelPrimaryPartCFrame and SetModelPrimaryPartCFrame. Use this to rotate/translate all Parts relative to the PrimaryPart. + + + + + + + + Status + true + 100 + 2 + + + + + move + true + Use MoveTo() instead + + + + + + + + DataModel + The root of ROBLOX's parent-child hierarchy (commonly known as game after the global variable used to access it) + + + + + + OnClose + true + Deprecated. Use DataModel.BindToClose + + + + + + + + + + Workspace + + + + + workspace + true + Deprecated. Use Workspace + + + + + ShowMouse + true + Deprecated. Use Workspace.IsMouseCursorVisible + + + + + IsLoaded + Returns true if the game has finished loading, false otherwise. Check this before listening to the Loaded signal to ensure a script knows when a game finishes loading. + + + + + + + + Loaded + Fires when the game finishes loading. Use this to know when to remove your custom loading gui. It is best to check IsLoaded() before connecting to this event, as the game may load before the event is connected to. + + + + + + + + SetPlaceID + true + Use SetPlaceId() instead + + + + + SetCreatorID + true + Use SetCreatorId() instead + + + + + + + + DataStoreService + Responsible for storing data across multiple user created places + -1 + + + + + + GetDataStore + Returns a data store with the given name and scope + + + + + GetGlobalDataStore + Returns the default data store + + + + + GetOrderedDataStore + Returns an ordered data store with the given name and scope + + + + + + + + GlobalDataStore + Exposes functions for saving and loading data for the DataStoreService + -1 + + + + + + OnUpdate + Sets callback as a function to be executed any time the value associated with key is changed. It is important to disconnect the connection when the subscription to the key is no longer needed. + + + + + + + + GetAsync + Returns the value of the entry in the DataStore with the given key + + + + + IncrementAsync + Increments the value of a particular key amd returns the incremented value + + + + + SetAsync + Sets the value of the key. This overwrites any existing data stored in the key + + + + + UpdateAsync + Retrieves the value of the key from the website, and updates it with a new value. The callback until the value fetched matches the value on the web. Returning nil means it will not save. + + + + + + + + OrderedDataStore + A type of DataStore where values must be positive integers. This makes OrderedDataStore suitable for leaderboard related scripting where you are required to order large amounts of data efficiently. + -1 + + + + + + GetSortedAsync + Returns a DataStorePages object. The length of each page is determined by pageSize, and the order is determined by isAscending. minValue and maxValue are optional parameters which will filter the result. + + + + + + + + HopperBin + true + 240 + 22 + + + + + + Camera + 5 + 5 + Model + Model + + + + + CameraSubject + Where the Camera's focus is. Any rotation of the camera will be about this subject. + + + + + CameraType + Defines how the camera will behave. <a href="http://wiki.roblox.com/index.php/CameraType" target="_blank">More info</a> + + + + + CoordinateFrame + true + The current position and rotation of the Camera. For most CameraTypes, the rotation is set such that the CoordinateFrame lookVector is pointing at the Focus. + + + + + CFrame + The current position and rotation of the Camera. For most CameraTypes, the rotation is set such that the CoordinateFrame lookVector is pointing at the Focus. + + + + + FieldOfView + The current angle, or width, of what the camera can see. Current acceptable values are from 20 degrees to 80. + + + + + Focus + The current CoordinateFrame that the camera is looking at. Note: it is not always guaranteed that the camera is always looking here. + + + + + ViewportSize + Holds the x,y screen resolution of the viewport the camera is presenting (note: this can differ from the AbsoluteSize property of a full screen gui). + + + + + NearPlaneZ + The negative z-offset of the view frustum's near clipping plane. + + + + + + + GetRoll + Returns the camera's current roll. Roll is defined in radians, and is stored as the delta from the camera's y axis default normal vector. + + + + + WorldToScreenPoint + Takes a 3D position in the world and projects it onto x,y coordinates of screen space. Returns two values, first is a Vector3 that has x,y position and z position which is distance from camera (negative if behind camera, positive if in front). Second return value is a boolean indicating if the first argument is an on-screen coordinate. + + + + + ScreenPointToRay + Takes a 2D screen position and produces a Ray object to be used for 3D raycasting. Input is x,y screen coordinates, and a (optional, defaults to 0) z position which sets how far in the camera look vector to start the ray origin. + + + + + ViewportPointToRay + Same as ScreenPointToRay, except no GUI offsets are taken into account. Useful for things like casting a ray from the middle of the Camera.ViewportSize + + + + + WorldToViewportPoint + Same as WorldToScreenPoint, except no GUI offsets are taken into account. + + + + + SetRoll + Sets the camera's current roll. Roll is defined in radians, and is stored as the delta from the camera's y axis default normal vector. + + + + + + + + Players + 20 + 21 + + + + + CharacterAutoLoads + true + Set to true, when a player joins a game, they get a character automatically, as well as when they die. When set to false, characters do not auto load and will only load in using Player:LoadCharacter(). + + + + + + + players + true + Use GetPlayers() instead + + + + + + + + ReplicatedStorage + 30 + 70 + A container whose contents are replicated to all clients and the server. + + + + + + RobloxReplicatedStorage + false + + + + + + ReplicatedFirst + 30 + 70 + A container whose contents are replicated to all clients (but not back to the server) first before anything else. Useful for creating loading guis, tutorials, etc. + + + + + RemoveRobloxLoadingScreen + Removes the default Roblox loading screen from view. Call this when you are ready to either show your own loading gui, or when the game is ready to play. + + + + + + + + ServerStorage + 30 + 69 + A container whose contents are only on the server. + + + + + + ServerScriptService + 30 + 71 + A container whose contents should be scripts. Scripts that are added to the container are run on the server. + + + + + + StudioService + -1 + A service for interfacing with the current studio state from Lua. + + + + + + Lighting + 30 + 13 + Responsible for all lighting aspects of the world (affects how things are rendered). + + + + + GetMinutesAfterMidnight + The number of minutes that the current time is past midnight. If currently at midnight, returns 0. Will return decimal values if not at an exact minute. + + + + + GetMoonDirection + Returns the lookVector (Vector3) of the moon. If this lookVector was used in a CFrame, the Part would face the moon. + + + + + GetMoonPhase + Currently always returns 0.75. MoonPhase cannot be edited. + + + + + GetSunDirection + Returns the lookVector (Vector3) of the sun. If this lookVector was used in a CFrame, the Part would face the sun. + + + + + SetMinutesAfterMidnight + Sets the time to be a certain number of minutes after midnight. This works with integer and decimal values. + + + + + + + Ambient + The hue of the global lighting. Changing this changes the color tint of all objects in the Workspace. + + + + + Brightness + How much global light each Part in the Workspace receives. Standard range is 0 to 1 (0 being little light), but can be increased all the way to 5 (colors start to be appear very different at this value). + 0 + 2 + + + + + ExposureCompensation + Exposure compensation amount. Applies a bias to the exposure level prior to the tonemap step. +1 indicates twice as much exposure and -1 means half as much exposure. + -3 + 3 + 600 + + + + + ColorShift_Bottom + The hue of global lighting on the bottom surfaces of an object. + + + + + ColorShift_Top + The hue of global lighting on the top surfaces of an object. + + + + + FogColor + A Color3 value that changes the hue of distance fog. + + + + + FogEnd + The distance at which fog completely blocks your vision. This distance is relative to the camera position. Units are in studs + + + + + FogStart + The distance at which the fog gradient begins. This distance is relative to the camera position. Units are in studs. + + + + + GeographicLatitude + The latitude position the level is placed at. This affects sun position. <a href="http://wiki.roblox.com/index.php/GeographicLatitude" target="_blank">More info</a> + 0 + 360 + 360 + + + + + GlobalShadows + Flag enabling shadows from sun and moon in the place + + + + + OutdoorAmbient + Effective ambient value for outdoors, effectively shadow color outdoors (requires GlobalShadows enabled) + + + + + Outlines + Flag enabling or disabling outlines on parts and terrain + + + + + ShadowColor + Color the shadows appear as. Shadows are drawn mostly for characters, but depending on the lighting will also show for Parts in the Workspace. Rendering settings can also affect if shadows are drawn. + + + + + TimeOfDay + A string that represent the current time of day. Time is in 24-hour clock format "XX::YY:ZZ", where X is hour, Y is minute, and Z is seconds. + + + + + ClockTime + 0 + 24 + 240 + + + + + + + LightingChanged + Fired whenever a property of Lighting is changed, or a skybox is added or removed. Skyboxes are of type 'Sky' and should be parented directly to lighting. + + + + + + + + TestService + 1000 + 68 + + + + + + DebuggerManager + + + + + + + + ScriptDebugger + + + + + + + + DebuggerBreakpoint + + + + + + + + DebuggerWatch + + + + + + + + Debris + -1 + 30 + A service that provides utility in cleaning up objects + + + + + addItem + true + Use AddItem() instead + + + + + AddItem + Adds an Instance into the debris service that will later be destroyed. Second argument 'lifetime' is optional and specifies how long (in seconds) to wait before destroying the item. If no time is specified then the item added will automatically be destroyed in 10 seconds. + + + + + + + MaxItems + true + Deprecated. No replacement + + + + + + + + Accoutrement + 20 + 32 + false + + + + + + Player + false + 10 + 12 + + + + + + CharacterAppearance + false + Model + Model + + + + + CameraMode + An enum that describes how a Player's camera is allowed to behave. <a href="http://wiki.roblox.com/index.php/CameraMode" target="_blank">More info</a>. + + + + + DataReady + true + Read-only. If true, this Player's persistent data can be loaded, false otherwise. <a href="http://wiki.roblox.com/index.php/ROBLOX_Scripting_How_To:_Data_Persistence" target="_blank">Info on Data Persistence</a>. + + + + + DataComplexity + true + + + + + + + + LoadCharacter + true + Loads in a new character for this player. This will replace the player's current character, if they have one. This should be used in conjunction with Players.CharacterAutoLoads to control spawning of characters. This function only works from a server-side script (NOT a LocalScript). + + + + + LoadData + true + + + + + SaveData + true + + + + + SaveBoolean + true + + + + + SaveInstance + true + + + + + SaveString + true + + + + + LoadBoolean + true + + + + + LoadNumber + true + + + + + LoadString + true + + + + + LoadInstance + true + + + + + SaveNumber + true + + + + + playerFromCharacter + true + Use GetPlayerFromCharacter() instead + + + + + SetUnder13 + true + + + + + + + + WaitForDataReady + true + true + Yields until the persistent data for this Player is ready to be loaded. <a href="http://wiki.roblox.com/index.php/ROBLOX_Scripting_How_To:_Data_Persistence" target="_blank">Info on Data Persistence</a>. + + + + + + + + + Idled + Fired periodically after the user has been AFK for a while. Currently this event is only fired for the *local* Player. "time" is the time in seconds that the user has been idle. + + + + + + + + Workspace + 5 + 19 + + + + + FindPartsInRegion3 + Returns parts in the area defined by the Region3, up to specified maxCount or 100, whichever is less + + + + + FindPartsInRegion3WithIgnoreList + Returns parts in the area defined by the Region3, up to specified maxCount or 100, whichever is less + + + + + FindPartOnRay + Return type is (BasePart, Vector3) if the ray hits. If it misses it will return (nil, PointAtEndOfRay) + + + + + FindPartOnRayWithIgnoreList + Return type is (BasePart, Vector3) if the ray hits. If it misses it will return (nil, PointAtEndOfRay) + + + + + + + PGSPhysicsSolverEnabled + Boolean used to enable the new physics solver + + + + + FallenPartsDestroyHeight + Sets the height at which falling characters and parts are destroyed. This property is not scriptable and can only be set in Studio + + + + + + + + BasePart + A structural class, not creatable + 3 + false + + + + + + Color + Color3 of the part. + + + + + CFrame + Contains information regarding the Part's position and a matrix that defines the Part's rotation. Can read/write. <a href="http://wiki.roblox.com/index.php/Cframe" target="_blank">More info</a> + + + + + CanCollide + Determines whether physical interactions with other Parts are respected. If true, will collide and react with physics to other Parts. If false, other parts will pass thru instead of colliding + + + + + Anchored + Determines whether or not physics acts upon the Part. If true, part stays 'Anchored' in space, not moving regardless of any collision/forces acting upon it. If false, physics works normally on the part. + + + + + Elasticity + A float value ranging from 0.0f to 1.0f. Sets how much the Part will rebound against another. a value of 1 is like a superball, and 0 is like a lead block. + 0 + 1 + + + + + Friction + A float value ranging from 0.0f to 1.0f. Sets how much the Part will be able to slide. a value of 1 is no sliding, and 0 is no friction, so infinite sliding. + 0 + 2 + + + + + Locked + Determines whether building tools (in-game and studio) can manipulate this Part. If true, no editing allowed. If false, editing is allowed. + + + + + Material + Specifies the look and feel the Part should have. Note: this does not define the color the Part is, see BrickColor for that. <a href="http://wiki.roblox.com/index.php/Material" target="_blank">More info</a> + + + + + Reflectance + Specifies how shiny the Part is. A value of 1 is completely reflective (chrome), while a value of 0 is no reflectance (concrete wall) + 0 + 1 + + + + + ResizeIncrement + Sets the value for the smallest change in size allowable by the Resize(NormalId, int) function. + + + + + ResizeableFaces + Sets the value for the faces allowed to be resized by the Resize(NormalId, int) function. + + + + + Transparency + Sets how visible an object is. A value of 1 makes the object invisible, while a value of 0 makes the object opaque. + 0 + 1 + + + + + Velocity + How fast the Part is traveling in studs/second. This property is NOT recommended to be modified directly, unless there is good reason. Otherwise, try using a BodyForce to move a Part. + + + + + PositionLocal + Position relative to parent part, or global space if there is no parent. + + + + + OrientationLocal + Orientation relative to parent part, or global space if there is no parent. + + + + + Orientation + Rotation around X, Y, and Z axis. Rotations applied in YXZ order. + + + + + Rotation + + + + + CenterOfMass + + + + + + + + makeJoints + Use MakeJoints() instead + true + + + + + MakeJoints + Creates the appropriate SurfaceJoints with all parts that are touching this Instance (including internal joints in the Instance, as in a Model). This uses the SurfaceTypes defined on the surfaces of parts to create the appropriate welds. <a href="http://wiki.roblox.com/index.php/MakeJoints" target="_blank">More info</a> + + + + + BreakJoints + Destroys SurfaceJoints with all parts that are touching this Instance (including internal joints in the Instance, as in a Model). + + + + + GetMass + Returns a number that is the mass of this Instance. Mass of a Part is immutable, and is changed only by the size of the Part. + + + + + Resize + Resizes a Part in the direction of the face defined by 'NormalId', by the amount specified by 'deltaAmount'. If the operation will expand the part to intersect another Instance, the part will not resize at all. Return true if the call is successful, false otherwise. + + + + + getMass + Use GetMass() instead + true + + + + + + + OutfitChanged + true + + + + + LocalSimulationTouched + true + Deprecated. Use Touched instead + + + + + StoppedTouching + + Deprecated. Use TouchEnded instead + + + + + TouchEnded + Fired when the part stops touching another part + + + + + + + Part + Parts + A plastic building block - the fundamental component of ROBLOX + 110 + 1 + Workspace + Workspace + + + + + TrussPart + Parts + An extendable building truss + 120 + 1 + Model + Model + + + + + WedgePart + Parts + A Wedge Part + 120 + 1 + Model + Model + + + + + PrismPart + A Prism Part + false + true + 120 + 1 + + + + + PyramidPart + A Pyramid Part + false + true + 120 + 1 + + + + + ParallelRampPart + A ParallelRamp Part + false + true + 120 + 1 + + + + + RightAngleRampPart + A RightAngleRamp Part + false + true + 120 + 1 + + + + + CornerWedgePart + Parts + A CornerWedge Part + 120 + 1 + Workspace + Workspace,Model + + + + + PlayerGui + A container instance that syncs data between a single player and the server. ScreenGui objects that are placed in this container will be shown to the Player parent only + 130 + 46 + + + + + SelectionImageObject + Overrides the default selection adornment (used for gamepads). For best results, this should point to a GuiObject. + + + + + + + PlayerScripts + A container instance that contains LocalScripts. LocalScript objects that are placed in this container will be exectue only when a Player is the parent. + 130 + 78 + + + + + StarterPlayerScripts + A container instance that contains LocalScripts. LocalScript objects that are placed in this container will be copied to new Players on startup. + 130 + 78 + false + + + + + StarterCharacterScripts + A container instance that contains LocalScripts. LocalScript objects that are placed in this container will be copied to new characters on startup. + 130 + 78 + false + + + + + + GuiMain + Deprecated, please use ScreenGui + true + 140 + 47 + + + + + + LayerCollector + The base class of ScreenGui, BillboardGui, and SurfaceGui. + false + + + + Enabled + Whether or not this should be displayed. + + + ZIndexBehavior + Controls the behavior of the ZIndex property for descendants of this object. It can be set to Global (Default) or Sibling. + + + + + + + ScreenGui + GUI + The core GUI object on which tools are built. Add Frames/Labels/Buttons to this object to have them rendered as a 2D overlay + 140 + 47 + BasePlayerGui + BasePlayerGui + + + + + FunctionalTest + Deprecated. Use TestService instead + true + 10 + + + + + BillboardGui + GUI + A GUI that adorns an object in the 3D world. Add Frames/Labels/Buttons to this object to have them rendered while attached to a 3D object + 140 + 64 + GuiBase2d + GuiBase2d + + + + + + Adornee + The Object the billboard gui uses as its base to render from. Currently, the only way to set this property is thru a script, and must exist in the workspace. This will only render if the object assigned derives from BasePart. + + + + + AbsolutePosition + A read-only Vector2 value that is the GuiObject's current position (x,y) in pixel space, from the top left corner of the GuiObject. + + + + + AbsoluteSize + A read-only Vector2 value that is the GuiObject's current size (width, height) in pixel space. + + + + + Active + If true, this GuiObject can fire mouse events and will pass them to any GuiObjects layered underneath, while false will do neither. + + + + + AlwaysOnTop + If true, billboard gui does not get occluded by 3D objects, but always renders on the screen. + + + + + Enabled + If true, billboard gui will render, otherwise rendering will be skipped. + + + + + ExtentsOffset + A Vector3 (x,y,z) defined in studs that will offset the gui from the extents of the 3d object it is rendering from. + + + + + PlayerToHideFrom + Specifies a Player that the BillboardGui will not render to. + + + + + StudsOffset + A Vector3 (x,y,z) defined in studs that will offset the gui from the centroid of the 3d object it is rendering from + + + + + SizeOffset + A Vector2 (x,y) defined in studs that will offset the gui size from it's current size. + + + + + Size + A UDim2 value describing the size of the BillboardGui. More information on UDim2 is available <a href="http://wiki.roblox.com/index.php/UDim2" target="_blank">here</a>. Relative values are defined as one-to-one with studs. + + + + + LightInfluence + Specifies the amount of influence lighting has on the billboard gui. A value of 0 is unlit, 1 is fully lit. Fractional values blend from unlit to lit. + 0 + 1 + + + + + + + + SurfaceGui + GUI + Renders its contained GuiObjects flat against the face of a part. + 140 + 64 + GuiBase2d + GuiBase2d + + + + + + Adornee + The Object the surface gui uses as its base to render from. Currently, the only way to set this property is thru a script, and must exist in the workspace. This will only render if the object assigned derives from BasePart. + + + + + Active + If true, this GuiObject can fire mouse events and will pass them to any GuiObjects layered underneath, while false will do neither. + + + + + Enabled + If true, surface gui will render, otherwise rendering will be skipped. + + + + + LightInfluence + Specifies the amount of influence lighting has on the surface gui. A value of 0 is unlit, 1 is fully lit. Fractional values blend from unlit to lit. + 0 + 1 + + + + + + + + + + GuiBase2d + false + LayerCollector,GuiBase2d + + + + + + AbsolutePosition + A read-only Vector2 value that is the GuiObject's current position (x,y) in pixel space, from the top left corner of the GuiObject. + + + + + AbsoluteSize + A read-only Vector2 value that is the GuiObject's current size (width, height) in pixel space. + + + + + + + + InputObject + An object that describes a particular user input, such as mouse movement, touches, keyboard, and more. + + + + + UserInputType + An enum that describes what kind of input this object is describing (mousebutton, touch, etc.). See Enum.UserInputType for more info. + + + + + UserInputState + An enum that describes what state of a particular input (touch began, touch moved, touch ended, etc.). See Enum.UserInputState for more info. + + + + + Position + A Vector3 value that describes a positional value of this input. For mouse and touch input, this is the screen position of the mouse/touch, described in the x and y components. For mouse wheel input, the z component describes whether the wheel was moved forward or backward. + + + + + KeyCode + An enum that describes what kind of input is being pressed. For types of input like Keyboard, this describes what key was pressed. For input like mousebutton, this provides no additional information. + + + + + + + + GuiObject + false + + + + + + TweenPosition + Smoothly moves a GuiObject from its current position to 'endPosition'. The only required argument is 'endPosition'. <a href="http://wiki.roblox.com/index.php/TweenPosition" target="_blank">More info</a> + + + + + TweenSize + Smoothly translates a GuiObject's current size to 'endSize'. The only required argument is 'endSize'. <a href="http://wiki.roblox.com/index.php/TweenSize" target="_blank">More info</a> + + + + + TweenSizeAndPosition + Smoothly translates a GuiObject's current size to 'endSize', and also smoothly translates the GuiObject's current position to 'endPosition'. The only required arguments are 'endSize' and 'endPosition'. <a href="http://wiki.roblox.com/index.php/TweenSizeAndPosition" target="_blank">More info</a> + + + + + + + + Active + If true, this GuiObject can fire mouse events and will pass them to any GuiObjects layered underneath, while false will do neither. + + + + + BackgroundColor3 + A Color3 value that specifies the background color for the GuiObject. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + BackgroundTransparency + A number value that specifies how transparent the background of the GuiObject is. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + 0 + 1 + + + + + BorderColor3 + A Color3 value that specifies the color of the outline of the GuiObject. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + BorderSizePixel + A number value that specifies the thickness (in pixels) of the outline of the GuiObject. Currently this value can only be set to either 0 or 1, any other number has no effect. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + ClipsDescendants + If set to true, any descendants of this GuiObject will only render if contained within it's borders. If set to false, all descendants will render regardless of position. + + + + + Draggable + true + If true, allows a GuiObject to be dragged by the user's mouse. The events 'DragBegin' and 'DragStopped' are fired when the appropriate action happens, and only will fire on Draggable=true GuiObjects. + + + + + Size + A UDim2 value describing the size of the GuiObject on screen in both absolute and relative coordinates. More information on UDim2 is available <a href="http://wiki.roblox.com/index.php/UDim2" target="_blank">here</a>. + + + + + Position + A UDim2 value describing the position of the top-left corner of the GuiObject on screen. More information on UDim2 is available <a href="http://wiki.roblox.com/index.php/UDim2" target="_blank">here</a>. + + + + + SizeConstraint + The direction(s) that an object can be resized in. <a href="http://wiki.roblox.com/index.php/SizeConstraint" target="_blank">More info</a>. + + + + + ZIndex + Describes the ordering in which overlapping GuiObjects will be drawn. A value of 1 is drawn first, while higher values are drawn in ascending order (each value draws over the last). + + + + + BackgroundColor + true + Deprecated. Use BackgroundColor3 instead + + + + + BorderColor + true + Deprecated. Use BorderColor3 instead + + + + + SelectionImageObject + Overrides the default selection adornment (used for gamepads). For best results, this should point to a GuiObject. + + + + + + + + DragBegin + true + Fired when a GuiObject with Draggable set to true starts to be dragged. 'InitialPosition' is a UDim2 value of the position of the GuiObject before any drag operation began. + + + + + DragStopped + true + Always fired after a DragBegin event, DragStopped is fired when the user releases the mouse button causing a drag operation on the GuiObject. Arguments 'x', and 'y' specify the top-left absolute position of the GuiObject when the event is fired. + + + + + MouseEnter + Fired when the mouse enters a GuiObject, as long as the GuiObject is active (see active property for more detail). Arguments 'x', and 'y' specify the absolute pixel position of the mouse. + + + + + MouseLeave + Fired when the mouse leaves a GuiObject, as long as the GuiObject is active (see active property for more detail). Arguments 'x', and 'y' specify the absolute pixel position of the mouse. + + + + + MouseMoved + Fired when the mouse is inside a GuiObject and moves, as long as the GuiObject is active (see active property for more detail). Arguments 'x', and 'y' specify the absolute pixel position of the mouse. + + + + + + TouchTap + Fired when a user taps their finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the tap gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchPinch + Fired when a user pinches their fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the pinch gesture. 'scale' is a float that indicates the difference from the beginning of the pinch gesture. 'velocity' is a float indicating how quickly the pinch gesture is happening. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. + + + + + TouchSwipe + Fired when a user swipes their fingers on a TouchEnabled device. 'swipeDirection' is an Enum.SwipeDirection, indicating the direction the user swiped. 'numberOfTouches' is an int that indicates how many touches were involved with the gesture. This event only fires locally. + + + + + TouchLongPress + Fired when a user holds at least one finger for a short amount of time on the same screen position on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. + + + + + TouchRotate + Fired when a user rotates two fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'rotation' is a float indicating how much the rotation has gone from the start of the gesture. 'velocity' is a float that indicates how quickly the gesture is being performed. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. + + + + + TouchPan + Fired when a user drags at least one finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'totalTranslation' is a Vector2, indicating how far the pan gesture has gone from its starting point. 'velocity' is a Vector2 that indicates how quickly the gesture is being performed in each dimension. 'state' indicates the Enum.UserInputState of the gesture. + + + + + + InputBegan + Fired when a user begins interacting via a Human-Computer Interface device (Mouse button down, touch begin, keyboard button down, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. + + + + + InputChanged + Fired when a user changes interacting via a Human-Computer Interface device (Mouse move, touch move, mouse wheel, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. + + + + + InputEnded + Fired when a user stops interacting via a Human-Computer Interface device (Mouse button up, touch end, keyboard button up, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. + + + + + + + + + Frame + GUI + A container object used to layout other GUI objects + 150 + 48 + GuiBase2d + GuiBase2d + + + + + Style + Determines how a frame will look. Uses Enum.FrameStyle. <a href="http://wiki.roblox.com/index.php?title=API:Enum/FrameStyle" target="_blank">More info</a> + + + + + + + ScrollingFrame + GUI + A container object used to layout other GUI objects, and allows for scrolling. + 150 + 48 + GuiBase2d + GuiBase2d + + + + + ScrollingEnabled + Determines whether or not scrolling is allowed on this frame. If turned off, no scroll bars will be rendered. + + + + + CanvasSize + Determines the size of the area that is scrollable. The UDim2 is calculated using the parent gui's size, similar to the regular Size property on gui objects. + + + + + CanvasPosition + The absolute position the scroll frame is in respect to the canvas size. The minimum this can be set to is (0,0), while the max is the absolute canvas size - AbsoluteWindowSize. + + + + + AbsoluteWindowSize + The size in pixels of the frame, without the scrollbars. + + + + + ScrollBarThickness + How thick the scroll bar appears. This applies to both the horizontal and vertical scroll bars. Can be set to 0 for no bars render. + + + + + TopImage + The "Up" image on the vertical scrollbar. Size of this is always ScrollBarThickness by ScrollBarThickness. This is also used as the "left" image on the horizontal scroll bar. + + + + + MidImage + The "Middle" image on the vertical scrollbar. Size of this can vary in the y direction, but is always set at ScrollBarThickness in x direction. This is also used as the "mid" image on the horizontal scroll bar. + + + + + BottomImage + The "Down" image on the vertical scrollbar. Size of this is always ScrollBarThickness by ScrollBarThickness. This is also used as the "right" image on the horizontal scroll bar. + + + + + + + ImageLabel + GUI + A GUI object containing an Image + 180 + 49 + GuiBase2d + GuiBase2d + + + + + Image + Specifies the id of the texture to display. <a href="http://wiki.roblox.com/index.php?title=API:Class/ImageLabel/Image" target="_blank">More info</a> + + + + + ScaleType + Specifies how an image should be displayed. See ScaleType for more info. + + + + + SliceCenter + If ScaleType is set to Slice, this Rect is used to specify the central part of the image. Everything outside of this is considered to be the border. + + + + + TileSize + If ScaleType is set to Tile, this sets the size of the tile. + + + + + + + TextLabel + GUI + A GUI object containing text + 190 + 50 + GuiBase2d + GuiBase2d + + + + + TextColor + true + Deprecated. Use TextColor3 instead + + + + + + + TextButton + GUI + A GUI button containing text + 170 + 51 + GuiBase2d + GuiBase2d + + + + + TextColor + true + Deprecated. Use TextColor3 instead + + + + + + + TextBox + GUI + A text entry box + 170 + 51 + GuiBase2d + GuiBase2d + + + + + TextColor + true + Deprecated. Use TextColor3 instead + + + + + + + GuiButton + GUI + A GUI button containing an Image + false + 160 + 52 + + + + + AutoButtonColor + Determines whether a button changes color automatically when reacting to mouse events. + + + + + Modal + Allows the mouse to be free in first person mode. If a button with this property set to true is visible, the mouse is 'free' in first person mode. + + + + + Style + Determines how a button will look, including mouse event states. Uses Enum.ButtonStyle. <a href="http://wiki.roblox.com/index.php?title=API:Class/GuiButton/Style" target="_blank">More info</a> + + + + + + + MouseButton1Click + Fired when the mouse is over the button, and the mouse down and up events fire without the mouse leaving the button. + + + + + MouseButton1Down + Fired when the mouse button is pushed down on a button. + + + + + MouseButton1Up + Fired when the mouse button is released on a button. + + + + + MouseButton2Click + This function currently does not work :( + + + + + MouseButton2Down + This function currently does not work :( + + + + + MouseButton2Up + This function currently does not work :( + + + + + + + ViewportFrame + GUI + A GUI that can show 3D objects + 30 + 52 + GuiBase2d + GuiBase2d + + + + + CurrentCamera + Current Camera of children objects + + + + + + + ImageButton + GUI + A GUI button containing an Image + 160 + 52 + GuiBase2d + GuiBase2d + + + + + Image + Specifies the asset id of the texture to display. <a href="http://wiki.roblox.com/index.php?title=API:Class/ImageButton/Image" target="_blank">More info</a> + + + + + ScaleType + Specifies how an image should be displayed. See ScaleType for more info. + + + + + SliceCenter + If ScaleType is set to Slice, this Rect is used to specify the central part of the image. Everything outside of this is considered to be the border. + + + + + TileSize + If ScaleType is set to Tile, this sets the size of the tile. + + + + + + + Handles + Adornments + A 3D GUI object to represent draggable handles + + 190 + 53 + + + + + ArcHandles + Adornments + A 3D GUI object to represent draggable arc handles + + 200 + 56 + + + + + SelectionBox + Adornments + A 3D GUI object to represent the visible selection around an object + 210 + 54 + + + + + SelectionSphere + Adornments + A 3D GUI object to represent the visible selection around an object + 210 + 54 + + + + + SurfaceSelection + Adornments + A 3D GUI object to represent the visible selection around a face of an object + 210 + 55 + + + + + Configuration + An object that can be placed under parts to hold Value objects that represent that part's configuration + 220 + 58 + + + + + Folder + An object that can be created to hold and organize objects + 10 + 77 + Instance + + + + + SelectionPartLasso + true + A visual line drawn representation between two part objects + 220 + 57 + + + + + SelectionPointLasso + true + A visual line drawn representation between two positions + 220 + 57 + + + + + PartPairLasso + A visual line drawn representation between two parts. + 220 + 57 + + + + + Pose + The pose of a joint relative to it's parent part in a keyframe + 220 + 60 + false + + + + + Keyframe + One keyframe of an animation + 220 + 60 + false + + + + + Animation + Animations + Represents a linked animation object, containing keyframes and poses. + 220 + 60 + + + + + AnimationTrack + Returned by a call to LoadAnimation. Controls the playback of an animation on a Humanoid. + 220 + 60 + + + + + AnimationController + Animations + Allows animations to be played on joints of the parent object. + 220 + 60 + + + + + CharacterMesh + Meshes + Modifies the appearance of a body part. + 220 + 60 + Model + Model + + + + + Dialog + 3D Interfaces + An object used to make dialog trees to converse with players + 220 + 62 + + + + + ConversationDistance + The maximum distance that the player's character can be from the dialog's parent in order to use the dialog. + + + + + GoodbyeChoiceActive + Indicates whether or not an extra choice is available for the player to exit the dialog tree at this node. + + + + + GoodbyeDialog + The prompt text for an extra choice that allows the player to exit the dialog tree at this node. + + + + + InUse + Indicates whether or not the dialog is currently being used by one or more players. + + + + + InitialPrompt + The chat message that is displayed to the player when they first activate the dialog. + + + + + Purpose + Describes the purpose of the dialog, which is used to display a relevant icon on the dialog's activation button. + + + + + Tone + Describes the tone of the dialog, which is used to display a relevant color in the dialog interface. + + + + + BehaviorType + Indicates how the dialog may be used by players. Use Enum.DialogBehaviorType.SinglePlayer if only one player should interact with the dialog at a time, otherwise use Enum.DialogBehaviorType.MultiplePlayers. + + + + + + + GetCurrentPlayers + Returns an array of the players currently conversing with this dialog. + + + + + + + DialogChoice + 3D Interfaces + An object used to make dialog trees to converse with players + 220 + 63 + + + + + UnionOperation + A UnionOperation is a union of multiple parts + true + false + 20 + 73 + + + + + UsePartColor + Override the colors of the mesh with the part color. + + + + + + + NegateOperation + A NegateOperation can be used to create holes in other parts + true + false + 20 + 72 + + + + + UsePartColor + Override the colors of the mesh with the part color. + + + + + + + MeshPart + Parts + A MeshPart is a physically simulatable mesh + true + true + 20 + 73 + Model + Model + + + + + Terrain + Object representing a high performance bounded grid of static 4x4 parts + true + false + 5 + 65 + + + + + WaterTransparency + 0 + 1 + + + + + WaterWaveSize + 0 + 1 + + + + + WaterWaveSpeed + 0 + 100 + + + + + WaterReflectance + 0 + 1 + + + + + + + GetCell + Returns CellMaterial, CellBlock, CellOrientation + + + + + GetWaterCell + + Returns hasAnyWater, WaterForce, WaterDirection + + + + + SetWaterCell + + + + + + + + Light + Lights + Parent of all light objects + 30 + 13 + PVInstance + Basepart,Attachment,PVInstance + + + + + Brightness + 0 + 10 + 2000 + + + + + + + PointLight + Lights + Makes the parent part emit light in a spherical shape + 30 + 13 + PVInstance + PVInstance + + + + + Range + 0 + 60 + + + + + + + SpotLight + Lights + Makes the parent part emit light in a conical shape + 30 + 13 + PVInstance + PVInstance + + + + + Range + 0 + 60 + + + + + Angle + 0 + 180 + + + + + + + SurfaceLight + Lights + Makes the parent part emit light in a frustum shape from rectangle defined by part + 30 + 13 + PVInstance + PVInstance + + + + + Range + 0 + 60 + + + + + Brightness + 0 + 10 + + + + + Angle + 0 + 180 + + + + + + + RemoteFunction + Scripting + Allow functions defined in one script to be called by another script across client/server boundary + 40 + 74 + + + + + InvokeClient + Server + + + + + InvokeServer + Client + + + + + + + OnClientInvoke + Client + + + + + OnServerInvoke + Server + + + + + + + RemoteEvent + Scripting + Allow events defined in one script to be subscribed to by another script across client/server boundary + 50 + 75 + + + + + FireAllClients + Server + + + + + FireClient + Server + + + + + FireServer + Client + + + + + + + OnClientEvent + Client + + + + + OnServerEvent + Server + + + + + + + TerrainRegion + Object representing a snapshot of the region of terrain + true + 20 + 65 + false + + + + + ModuleScript + Scripting + A script fragment. Only runs when another script uses require() on it. + 50 + 76 + Instance + + + + + + + + ContextActionResult + + + + Sink + If 'functionToBind' from ContextActionService:BindAction() returns Enum.ContextActionResult.Sink, the input event will stop at that function and no other bound actions under it will be invoked. This is the default behavior if 'functionToBind' does not return anything or yields in any way. + + + + + Pass + If 'functionToBind' from ContextActionService:BindAction() returns Enum.ContextActionResult.Pass, the input event is considered to have not been handled by 'functionToBind' and will continue being passed to actions bound to the same input type. + + + + + + Material + + + + Air + false + + + + + Water + false + + + + + Rock + false + + + + + Glacier + false + + + + + Snow + false + + + + + Sandstone + false + + + + + Mud + false + + + + + Basalt + false + + + + + Ground + false + + + + + CrackedLava + false + + + + + Asphalt + false + + + + + LeafyGrass + false + + + + + Salt + false + + + + + Limestone + false + + + + + Pavement + false + + + + + + Status + true + + + + Poison + true + + + + + Confusion + true + + + + + + SaveFilter + true + + + + + PrivilegeType + true + + + + + Genre + true + + + + + GearGenreSetting + true + + + + + SortOrder + The ordering to use for sorting an array of GuiObjects. + + + + Name + Sort by alphabetical ordering of the Name property. + + + + + LayoutOrder + Sort using the less than operator on the LayoutOrder property of GuiObject. + + + + + Custom + true + + + + + + ZIndexBehavior + Controls the behavior of the ZIndex property. + + + + Global + The ZIndex property will override the default value computed from the depth in the hierarchy. + + + + + Sibling + The ZIndex property will control the order that the GuiObject will be rendered relative to its siblings. + + + + + + ScaleType + Controls how an image is displayed. + + + + Stretch + Force the image to fill the available space. + + + + + Slice + Use the SliceCenter property to stretch the middle of the image but maintain crisp borders. + + + + + Tile + Tile the image using the TileSize property. + + + + + Fit + Size the image to the largest size that will fit in the available space while maintaining aspect ratio. + + + + + Crop + Fill the available space, maintaining aspect ratio by cropping the edges if necessary. + + + + + diff --git a/Client2018/SyntaxPlayerBeta.exe b/Client2018/SyntaxPlayerBeta.exe new file mode 100644 index 0000000..9fe5c37 Binary files /dev/null and b/Client2018/SyntaxPlayerBeta.exe differ diff --git a/Client2018/content/LuaPackages/.luacheckrc b/Client2018/content/LuaPackages/.luacheckrc new file mode 100644 index 0000000..5f5cf25 --- /dev/null +++ b/Client2018/content/LuaPackages/.luacheckrc @@ -0,0 +1,41 @@ +stds.roblox = { + globals = { + "game" + }, + read_globals = { + -- Roblox globals + "script", + + -- Extra functions + "tick", "warn", "spawn", + "wait", "settings", "typeof", "delay", + + -- Types + "Vector2", "Vector3", + "Color3", + "UDim", "UDim2", + "Rect", + "CFrame", + "Enum", + "Instance", + } +} + +stds.testez = { + read_globals = { + "describe", + "it", "itFOCUS", "itSKIP", + "FOCUS", "SKIP", "HACK_NO_XPCALL", + "expect", + } +} + +ignore = { + "212", -- unused arguments +} + +std = "lua51+roblox" + +files["**/*.spec.lua"] = { + std = "+testez", +} \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/Common/Action.lua b/Client2018/content/LuaPackages/AppTempCommon/Common/Action.lua new file mode 100644 index 0000000..d3d1ff4 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/Common/Action.lua @@ -0,0 +1,68 @@ +--[[ + A helper function to define a Rodux action creator with an associated name. + + Normally when creating a Rodux action, you can just create a function: + + return function(value) + return { + type = "MyAction", + value = value, + } + end + + And then when you check for it in your reducer, you either use a constant, + or type out the string name: + + if action.type == "MyAction" then + -- change some state + end + + Typos here are a remarkably common bug. We also have the issue that there's + no link between reducers and the actions that they respond to! + + `Action` (this helper) provides a utility that makes this a bit cleaner. + + Instead, define your Rodux action like this: + + return Action("MyAction", function(value) + return { + value = value, + } + end) + + We no longer need to add the `type` field manually. + + Additionally, the returned action creator now has a 'name' property that can + be checked by your reducer: + + local MyAction = require(Reducers.MyAction) + + ... + + if action.type == MyAction.name then + -- change some state! + end + + Now we have a clear link between our reducers and the actions they use, and + if we ever typo a name, we'll get a warning in LuaCheck as well as an error + at runtime! +]] + +return function(name, fn) + assert(type(name) == "string", "A name must be provided to create an Action") + assert(type(fn) == "function", "A function must be provided to create an Action") + + return setmetatable({ + name = name, + }, { + __call = function(self, ...) + local result = fn(...) + + assert(type(result) == "table", "An action must return a table") + + result.type = name + + return result + end + }) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/Common/Action.spec.lua b/Client2018/content/LuaPackages/AppTempCommon/Common/Action.spec.lua new file mode 100644 index 0000000..799c2b3 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/Common/Action.spec.lua @@ -0,0 +1,85 @@ +return function() + local Action = require(script.Parent.Action) + + it("should return a table", function() + local action = Action("foo", function() + return {} + end) + + expect(action).to.be.a("table") + end) + + it("should set the name of the action", function() + local action = Action("foo", function() + return {} + end) + + expect(action.name).to.equal("foo") + end) + + it("should be able to be called as a function", function() + local action = Action("foo", function() + return {} + end) + + expect(action).never.to.throw() + end) + + it("should return a table when called as a function", function() + local action = Action("foo", function() + return {} + end) + + expect(action()).to.be.a("table") + end) + + it("should set the type of the action", function() + local action = Action("foo", function() + return {} + end) + + expect(action().type).to.equal("foo") + end) + + it("should set values", function() + local action = Action("foo", function(value) + return { + value = value + } + end) + + expect(action(100).value).to.equal(100) + end) + + it("should throw when passed a function", function() + local action = Action("foo", function() + return function() end + end) + + expect(action).to.throw() + end) + + it("should throw with a invalid name", function() + expect(function() + Action(nil, function() + return {} + end) + end).to.throw() + + expect(function() + Action(100, function() + return {} + end) + end).to.throw() + end) + + it("should throw when passed a invalid function", function() + expect(function() + Action("foo", nil) + end).to.throw() + + expect(function() + Action("foo", {}) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/Common/Functional.lua b/Client2018/content/LuaPackages/AppTempCommon/Common/Functional.lua new file mode 100644 index 0000000..a40c8ad --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/Common/Functional.lua @@ -0,0 +1,131 @@ +--[[ + Provides an implementation of functional programming primitives. +]] + +local Functional = {} + +--[[ + Create a copy of a list with only values for which `callback` returns true +]] +function Functional.Filter(list, callback) + local new = {} + + for key = 1, #list do + local value = list[key] + if callback(value, key) then + table.insert(new, value) + end + end + + return new +end + +--[[ + Create a copy of a list where each value is transformed by `callback` +]] +function Functional.Map(list, callback) + local new = {} + + for key = 1, #list do + new[key] = callback(list[key], key) + end + + return new +end + +--[[ + Identical to Map, except that the result will be reversed. +]] +function Functional.MapReverse(list, callback) + local new = {} + + for key = #list, 1, -1 do + new[key] = callback(list[key], key) + end + + return new +end + +--[[ + Create a copy of a list doing a combination filter and map. + + If callback returns nil for any item, it is considered filtered from the + list. Any other value is considered the result of the 'map' operation. +]] +function Functional.FilterMap(list, callback) + local new = {} + + for key = 1, #list do + local value = list[key] + local result = callback(value, key) + + if result ~= nil then + table.insert(new, result) + end + end + + return new +end + +--[[ + Performs a left-fold of the list with the given initial value and callback. +]] +function Functional.Fold(list, initial, callback) + local accum = initial + + for key = 1, #list do + accum = callback(accum, list[key], key) + end + + return accum +end + +--[[ + Performs a fold over the entries in the given dictionary. +]] +function Functional.FoldDictionary(dictionary, initial, callback) + local accum = initial + + for key, value in pairs(dictionary) do + accum = callback(accum, key, value) + end + + return accum +end + +--[[ + Returns a list that contains at most `count` values from the given list. +]] +function Functional.Take(list, count, startingIndex) + startingIndex = startingIndex or 1 + + local maxIndex = count + (startingIndex - 1) + if maxIndex > #list then + maxIndex = #list + end + + local new = {} + + for i = startingIndex, maxIndex do + local value = list[i] + local newIndex = i - (startingIndex - 1) + new[newIndex] = value + end + + return new +end + +--[[ + If the list contains the sought-after element, return its index, or nil otherwise. +]] +function Functional.Find(list, value) + for index, element in ipairs(list) do + if element == value then + return index + end + end + + return nil +end + +return Functional \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/Common/Functional.spec.lua b/Client2018/content/LuaPackages/AppTempCommon/Common/Functional.spec.lua new file mode 100644 index 0000000..4ea87a4 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/Common/Functional.spec.lua @@ -0,0 +1,218 @@ +return function() + local Functional = require(script.Parent.Functional) + + local function identity(...) + return ... + end + + local function add(a, b) + return a + b + end + + describe("Filter", function() + it("should copy lists correctly", function() + local listA = {1, 2, 3} + local listB = Functional.Filter(listA, function() + return true + end) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the filter predicate", function() + local listA = {1, 2, 3, 4, 5} + local listB = Functional.Filter(listA, function(value, key) + expect(value).to.equal(key) + + return value % 2 == 0 + end) + + expect(listB[1]).to.equal(2) + expect(listB[2]).to.equal(4) + end) + end) + + describe("Map", function() + it("should copy lists correctly using the identity function", function() + local listA = {1, 2, 3} + local listB = Functional.Map(listA, identity) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the map predicate", function() + local listA = {1, 2, 3} + local listB = Functional.Map(listA, function(value, key) + expect(value).to.equal(key) + + return value * 2 + end) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i] * 2) + end + end) + end) + + describe("MapReverse", function() + it("should copy lists correctly using the identity function", function() + local listA = {1, 2, 3} + local listB = Functional.MapReverse(listA, identity) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the map predicate", function() + local listA = {1, 2, 3} + local listB = Functional.MapReverse(listA, function(value, key) + expect(value).to.equal(key) + + return value * 2 + end) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i] * 2) + end + end) + + it("should iterate backwards", function() + local list = {1, 2, 3} + local nextKey = 3 + + Functional.MapReverse(list, function(value, key) + expect(value).to.equal(nextKey) + expect(key).to.equal(nextKey) + + nextKey = nextKey - 1 + end) + + expect(nextKey).to.equal(0) + end) + end) + + describe("FilterMap", function() + it("should copy truthy lists using the identity function", function() + local listA = {1, 2, 3} + local listB = Functional.FilterMap(listA, identity) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the filter-map predicate", function() + local listA = {1, 2, 3, 4, 5} + + -- Create a list containing only the odd numbers, and double those numbers + local listB = Functional.FilterMap(listA, function(value, key) + expect(value).to.equal(key) + + if value % 2 == 0 then + return nil + end + + return value * 2 + end) + + expect(listB[1]).to.equal(2) + expect(listB[2]).to.equal(6) + expect(listB[3]).to.equal(10) + end) + end) + + describe("Fold", function() + it("should left-fold lists", function() + local list = {1, 2, 3, 4, 5} + + local sum = Functional.Fold(list, 0, add) + + expect(sum).to.equal(15) + end) + end) + + describe("Take", function() + it("should take values from a list", function() + local a = {1, 2, 3} + local b = Functional.Take(a, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + end) + + it("should not take past the end of a list", function() + local a = {1, 2, 3} + local b = Functional.Take(a, 4) + + expect(#b).to.equal(3) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + end) + + it("should copy all values when taking past the end of a list", function() + local a = {1, 2, 3} + local b = Functional.Take(a, 4) + + expect(#b).to.equal(#a) + expect(a[1]).to.equal(b[1]) + expect(a[2]).to.equal(b[2]) + expect(a[3]).to.equal(b[3]) + end) + + it("should take values from a starting index when provided", function() + local a = {1, 2, 3, 4} + local b = Functional.Take(a, 2, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(2) + expect(b[2]).to.equal(3) + end) + + it("should not take past the end of a list when the starting index is provided", function() + local a = {1, 2, 3, 4} + local b = Functional.Take(a, 3, 3) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(3) + expect(b[2]).to.equal(4) + end) + end) + + describe("Find", function() + it("should return index of matched item", function() + local a = {"foo", "bar", "garply"} + local b = Functional.Find(a, "bar") + + expect(b).to.equal(2) + end) + + it("should find the first example in the case of duplicates", function() + local a = {"foo", "bar", "garply", "bar"} + local b = Functional.Find(a, "bar") + + expect(b).to.equal(2) + end) + + it("should return nil if item is not found", function() + local a = {"foo", "bar", "garply"} + local b = Functional.Find(a, "fleebledegoop") + + expect(b).to.equal(nil) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/Common/Immutable.lua b/Client2018/content/LuaPackages/AppTempCommon/Common/Immutable.lua new file mode 100644 index 0000000..a73d203 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/Common/Immutable.lua @@ -0,0 +1,141 @@ +--[[ + Provides functions for manipulating immutable data structures. +]] + +local Immutable = {} + +--[[ + Merges dictionary-like tables together. +]] +function Immutable.JoinDictionaries(...) + local result = {} + + for i = 1, select("#", ...) do + local dictionary = select(i, ...) + for key, value in pairs(dictionary) do + result[key] = value + end + end + + return result +end + +--[[ + Joins any number of lists together into a new list +]] +function Immutable.JoinLists(...) + local new = {} + + for listKey = 1, select("#", ...) do + local list = select(listKey, ...) + local len = #new + + for itemKey = 1, #list do + new[len + itemKey] = list[itemKey] + end + end + + return new +end + +--[[ + Creates a new copy of the dictionary and sets a value inside it. +]] +function Immutable.Set(dictionary, key, value) + local new = {} + + for key, value in pairs(dictionary) do + new[key] = value + end + + new[key] = value + + return new +end + +--[[ + Creates a new copy of the list with the given elements appended to it. +]] +function Immutable.Append(list, ...) + local new = {} + local len = #list + + for key = 1, len do + new[key] = list[key] + end + + for i = 1, select("#", ...) do + new[len + i] = select(i, ...) + end + + return new +end + +--[[ + Remove elements from a dictionary +]] +function Immutable.RemoveFromDictionary(dictionary, ...) + local result = {} + + for key, value in pairs(dictionary) do + local found = false + for listKey = 1, select("#", ...) do + if key == select(listKey, ...) then + found = true + break + end + end + if not found then + result[key] = value + end + end + + return result +end + +--[[ + Remove the given key from the list. +]] +function Immutable.RemoveFromList(list, removeIndex) + local new = {} + + for i = 1, #list do + if i ~= removeIndex then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Remove the range from the list starting from the index. +]] +function Immutable.RemoveRangeFromList(list, index, count) + local new = {} + + for i = 1, #list do + if i < index or i >= index + count then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Creates a new list that has no occurrences of the given value. +]] +function Immutable.RemoveValueFromList(list, removeValue) + local new = {} + + for i = 1, #list do + if list[i] ~= removeValue then + table.insert(new, list[i]) + end + end + + return new +end + +return Immutable diff --git a/Client2018/content/LuaPackages/AppTempCommon/Common/Immutable.spec.lua b/Client2018/content/LuaPackages/AppTempCommon/Common/Immutable.spec.lua new file mode 100644 index 0000000..c9adc5f --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/Common/Immutable.spec.lua @@ -0,0 +1,284 @@ +return function() + local Immutable = require(script.Parent.Immutable) + + describe("JoinDictionaries", function() + it("should preserve immutability", function() + local a = {} + local b = {} + + local c = Immutable.JoinDictionaries(a, b) + + expect(c).never.to.equal(a) + expect(c).never.to.equal(b) + end) + + it("should treat list-like values like dictionary values", function() + local a = { + [1] = 1, + [2] = 2, + [3] = 3 + } + + local b = { + [1] = 11, + [2] = 22 + } + + local c = Immutable.JoinDictionaries(a, b) + + expect(c[1]).to.equal(b[1]) + expect(c[2]).to.equal(b[2]) + expect(c[3]).to.equal(a[3]) + end) + + it("should merge dictionary values correctly", function() + local a = { + hello = "world", + foo = "bar" + } + + local b = { + foo = "baz", + tux = "penguin" + } + + local c = Immutable.JoinDictionaries(a, b) + + expect(c.hello).to.equal(a.hello) + expect(c.foo).to.equal(b.foo) + expect(c.tux).to.equal(b.tux) + end) + + it("should merge multiple dictionaries", function() + local a = { + foo = "yes" + } + + local b = { + bar = "yup" + } + + local c = { + baz = "sure" + } + + local d = Immutable.JoinDictionaries(a, b, c) + + expect(d.foo).to.equal(a.foo) + expect(d.bar).to.equal(b.bar) + expect(d.baz).to.equal(c.baz) + end) + end) + + describe("JoinLists", function() + it("should preserve immutability", function() + local a = {} + local b = {} + + local c = Immutable.JoinLists(a, b) + + expect(c).never.to.equal(a) + expect(c).never.to.equal(b) + end) + + it("should treat list-like values correctly", function() + local a = {1, 2, 3} + local b = {4, 5, 6} + + local c = Immutable.JoinLists(a, b) + + expect(#c).to.equal(6) + + for i = 1, #c do + expect(c[i]).to.equal(i) + end + end) + + it("should merge multiple lists", function() + local a = {1, 2} + local b = {3, 4} + local c = {5, 6} + + local d = Immutable.JoinLists(a, b, c) + + expect(#d).to.equal(6) + + for i = 1, #d do + expect(d[i]).to.equal(i) + end + end) + end) + + describe("Set", function() + it("should preserve immutability", function() + local a = {} + + local b = Immutable.Set(a, "foo", "bar") + + expect(b).never.to.equal(a) + end) + + it("should treat numeric keys normally", function() + local a = {1, 2, 3} + + local b = Immutable.Set(a, 2, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(4) + expect(b[3]).to.equal(3) + end) + + it("should overwrite dictionary-like keys", function() + local a = { + foo = "bar", + baz = "qux" + } + + local b = Immutable.Set(a, "foo", "hello there") + + expect(b.foo).to.equal("hello there") + expect(b.baz).to.equal(a.baz) + end) + end) + + describe("Append", function() + it("should preserve immutability", function() + local a = {} + + local b = Immutable.Append(a, "another happy landing") + + expect(b).never.to.equal(a) + end) + + it("should append values", function() + local a = {1, 2, 3} + local b = Immutable.Append(a, 4, 5) + + expect(#b).to.equal(5) + + for i = 1, #b do + expect(b[i]).to.equal(i) + end + end) + end) + + describe("RemoveFromDictionary", function() + it("should preserve immutability", function() + local a = { foo = "bar" } + + local b = Immutable.RemoveFromDictionary(a, "foo") + + expect(b).to.never.equal(a) + end) + + it("should remove fields from the dictionary", function() + local a = { + foo = "bar", + baz = "qux", + boof = "garply", + } + + local b = Immutable.RemoveFromDictionary(a, "foo", "boof") + + expect(b.foo).to.never.be.ok() + expect(b.baz).to.equal("qux") + expect(b.boof).to.never.be.ok() + end) + end) + + describe("RemoveFromList", function() + it("should preserve immutability", function() + local a = {1, 2, 3} + local b = Immutable.RemoveFromList(a, 2) + + expect(b).never.to.equal(a) + end) + + it("should remove elements from the list", function() + local a = {1, 2, 3} + local b = Immutable.RemoveFromList(a, 2) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + end) + + describe("RemoveRangeFromList", function() + it("should preserve immutability", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 1) + + expect(b).never.to.equal(a) + end) + + it("should remove elements properly from the list", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 1) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + + it("should remove elements properly from the list", function() + local a = {1, 2, 3, 4, 5, 6} + local b = Immutable.RemoveRangeFromList(a, 1, 4) + + expect(b[1]).to.equal(5) + expect(b[2]).to.equal(6) + expect(b[3]).never.to.be.ok() + end) + + it("should remove elements properly from the list", function() + local a = {1, 2, 3, 4, 5, 6} + local b = Immutable.RemoveRangeFromList(a, 2, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(6) + expect(b[3]).never.to.be.ok() + end) + + it("should remove elements properly from the list", function() + local a = {1, 2, 3, 4, 5, 6, 7} + local b = Immutable.RemoveRangeFromList(a, 4, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + expect(b[4]).never.to.be.ok() + end) + + it("should not remove any elements when count is 0 or less", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 0) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + + local c = Immutable.RemoveRangeFromList(a, 2, -1) + expect(c[1]).to.equal(1) + expect(c[2]).to.equal(2) + expect(c[3]).to.equal(3) + end) + end) + + describe("RemoveValueFromList", function() + it("should preserve immutability", function() + local a = {1, 1, 1} + local b = Immutable.RemoveValueFromList(a, 1) + + expect(b).never.to.equal(a) + end) + + it("should remove all elements from the list", function() + local a = {1, 2, 2, 3} + local b = Immutable.RemoveValueFromList(a, 2) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/Common/Text.lua b/Client2018/content/LuaPackages/AppTempCommon/Common/Text.lua new file mode 100644 index 0000000..acd3a4b --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/Common/Text.lua @@ -0,0 +1,123 @@ +local LuaUseUtf8TextTruncation = settings():GetFFlag("LuaUseUtf8TextTruncation") +local TextMeasureTemporaryPatch = settings():GetFFlag("TextMeasureTemporaryPatch") + +local TextService = game:GetService("TextService") + +local Text = {} + +-- FYI: Any number greater than 2^30 will make TextService:GetTextSize give invalid results +local MAX_BOUND = 10000 + +-- TODO(CLIPLAYEREX-1633): We can remove this padding patch after fixing TextService:GetTextSize sizing bug +Text._TEMP_PATCHED_PADDING = Vector2.new(0, 0) + +if TextMeasureTemporaryPatch then + Text._TEMP_PATCHED_PADDING = Vector2.new(2, 2) +end + +-- Wrapper function for GetTextSize +function Text.GetTextBounds(text, font, fontSize, bounds) + return TextService:GetTextSize(text, fontSize, font, bounds) + Text._TEMP_PATCHED_PADDING +end + +function Text.GetTextWidth(text, font, fontSize) + return Text.GetTextBounds(text, font, fontSize, Vector2.new(MAX_BOUND, MAX_BOUND)).X +end + +function Text.GetTextHeight(text, font, fontSize, widthCap) + return Text.GetTextBounds(text, font, fontSize, Vector2.new(widthCap, MAX_BOUND)).Y +end + +-- TODO(CLIPLAYEREX-391): Kill these truncate functions once we have official support for text truncation +function Text.Truncate(text, font, fontSize, widthInPixels, overflowMarker) + overflowMarker = overflowMarker or "" + + if Text.GetTextWidth(text, font, fontSize) > widthInPixels then + if LuaUseUtf8TextTruncation then + -- A binary search may be more efficient + local lastText = "" + for _, stopIndex in utf8.graphemes(text) do + local newText = string.sub(text, 1, stopIndex) .. overflowMarker + if Text.GetTextWidth(newText, font, fontSize) > widthInPixels then + return lastText + end + lastText = newText + end + else + for len = #text, 1, -1 do + local newText = string.sub(text, 1, len) .. overflowMarker + if Text.GetTextWidth(newText, font, fontSize) <= widthInPixels then + return newText + end + end + end + else -- No truncation needed + return text + end + + return "" +end + +function Text.TruncateTextLabel(textLabel, overflowMarker) + textLabel.Text = Text.Truncate(textLabel.Text, textLabel.Font, + textLabel.TextSize, textLabel.AbsoluteSize.X, overflowMarker) +end + +-- Remove whitespace from the beginning and end of the string +function Text.Trim(str) + if type(str) ~= "string" then + error(string.format("Text.Trim called on non-string type %s.", type(str)), 2) + end + return (str:gsub("^%s*(.-)%s*$", "%1")) +end + +-- Remove whitespace from the end of the string +function Text.RightTrim(str) + if type(str) ~= "string" then + error(string.format("Text.RightTrim called on non-string type %s.", type(str)), 2) + end + return (str:gsub("%s+$", "")) +end + +-- Remove whitespace from the beginning of the string +function Text.LeftTrim(str) + if type(str) ~= "string" then + error(string.format("Text.LeftTrim called on non-string type %s.", type(str)), 2) + end + return (str:gsub("^%s+", "")) +end + +-- Replace multiple whitespace with one; remove leading and trailing whitespace +function Text.SpaceNormalize(str) + if type(str) ~= "string" then + error(string.format("Text.SpaceNormalize called on non-string type %s.", type(str)), 2) + end + return (str:gsub("%s+", " "):gsub("^%s+" , ""):gsub("%s+$" , "")) +end + +-- Splits a string by the provided pattern into a table. The pattern is interpreted as plain text. +function Text.Split(str, pattern) + if type(str) ~= "string" then + error(string.format("Text.Split called on non-string type %s.", type(str)), 2) + elseif type(pattern) ~= "string" then + error(string.format("Text.Split called with a pattern that is non-string type %s.", type(pattern)), 2) + elseif pattern == "" then + error("Text.Split called with an empty pattern.", 2) + end + + local result = {} + local currentPosition = 1 + + while true do + local patternStart, patternEnd = string.find(str, pattern, currentPosition, true) + if not patternStart or not patternEnd then break end + table.insert(result, string.sub(str, currentPosition, patternStart - 1)) + currentPosition = patternEnd + 1 + end + + table.insert(result, string.sub(str, currentPosition, string.len(str))) + + return result +end + +return Text \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/Common/Text.spec.lua b/Client2018/content/LuaPackages/AppTempCommon/Common/Text.spec.lua new file mode 100644 index 0000000..4fbd73a --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/Common/Text.spec.lua @@ -0,0 +1,410 @@ +return function() + local Text = require(script.Parent.Text) + + describe("GetTextBounds", function() + it("should return a bounds of padding width and font-size height when the string is empty", function() + local bounds = Text.GetTextBounds("", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000)) + expect(bounds.X).to.equal(Text._TEMP_PATCHED_PADDING.x) + expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y) + end) + it("should return the height and width of a string as one line with large bounds", function() + local bounds = Text.GetTextBounds("One Two Three", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000)) + expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y) + end) + + it("should return the height of the string as multiple lines with short bounds", function() + local bounds = Text.GetTextBounds("One Two Three Four", Enum.Font.SourceSans, 18, Vector2.new(32, 1000)) + expect(bounds.Y > 18).to.equal(true) + end) + end) + + describe("GetTextHeight", function() + it("should return height equal to font size when string is empty", function() + local height = Text.GetTextHeight("", Enum.Font.SourceSans, 18, 0) + expect(height).to.equal(18 + Text._TEMP_PATCHED_PADDING.y) + end) + end) + + describe("GetTextWidth", function() + it("should return width equal to 1 when string is empty", function() + local width = Text.GetTextWidth("", Enum.Font.SourceSans, 18, 18) + expect(width).to.equal(Text._TEMP_PATCHED_PADDING.x) + end) + end) + + describe("Truncate", function() + it("should return empty string", function() + local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 0, "...") + expect(emptyQuery).to.be.a("string") + expect(emptyQuery).to.equal("") + end) + + it("should return empty string for not empty box", function() + local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 50, "...") + expect(emptyQuery).to.be.a("string") + expect(emptyQuery).to.equal("") + end) + + it("should truncate with ...", function() + local reallyLongQuery = Text.Truncate( + "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100, "...") + expect(reallyLongQuery).to.equal("One Two Thre...") + end) + + it("should truncate without a ...", function() + local reallyLongQueryNoOverflowMarker = Text.Truncate( + "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100) + expect(reallyLongQueryNoOverflowMarker).to.equal("One Two Three ") + end) + + it("should not truncate", function() + local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, 100) + expect(shouldFitQuery).to.equal("One Two") + end) + + it("should not truncate, off by one check", function() + local oneCharQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 100) + expect(oneCharQuery).to.equal("O") + end) + + it("should truncate, off by one check", function() + local oneCharNoRoomQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 0) + expect(oneCharNoRoomQuery).to.equal("") + end) + + it("should perform a negative width check", function() + local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, -100, "...") + expect(shouldFitQuery).to.equal("") + end) + + it("should truncate long graphemes properly", function() + -- 11-byte rainbow flag grapheme + -- Flag, zero-space-joiner, rainbow + local rainbowFlag = utf8.char(127987) .. utf8.char(8205) .. utf8.char(127752) + local oneFlagWithinLimit = Text.Truncate( + rainbowFlag, Enum.Font.SourceSans, 18, 100, "...") + expect(oneFlagWithinLimit).to.equal(rainbowFlag) + + local twoRainbowFlags = rainbowFlag .. rainbowFlag + local twoFlagsAreFine = Text.Truncate( + twoRainbowFlags, Enum.Font.SourceSans, 18, 100, "...") + expect(twoFlagsAreFine).to.equal(twoRainbowFlags) + + local fourRainbowFlags = twoRainbowFlags .. twoRainbowFlags + local fourFlagsIsTooLong = Text.Truncate( + fourRainbowFlags, Enum.Font.SourceSans, 18, 100, "...") + expect(fourFlagsIsTooLong).to.equal(twoRainbowFlags .. "...") + end) + end) + + describe("TruncateTextLabel", function() + it("should use text label attributes to truncate text", function() + local screenGui = Instance.new("ScreenGui") + local textLabel = Instance.new("TextLabel") + textLabel.Size = UDim2.new(0, 100, 0, 32) + textLabel.Text = "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve" + textLabel.Font = Enum.Font.SourceSans + textLabel.TextSize = 18 + textLabel.Parent = screenGui + Text.TruncateTextLabel(textLabel) + + expect(textLabel.Text).to.equal("One Two Three ") + end) + end) + + + describe("TrimString", function() + it("Should trim the string properly", function() + local trimmedInput = Text.Trim("") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly", function() + local trimmedInput = Text.Trim(" ") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly", function() + local trimmedInput = Text.Trim("ab") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly", function() + local trimmedInput = Text.Trim(" ab ") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly", function() + local trimmedInput = Text.Trim(" a b ") + local expected = "a b" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly", function() + local trimmedInput = Text.Trim("\r\n\t\f a\r\n\t\f ") + local expected = "a" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string with unicode characters properly", function() + local trimmedInput = Text.Trim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly", function() + local trimmedInput = Text.Trim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly", function() + local trimmedInput = Text.Trim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n") + local expected = "😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + end) + + + describe("RightTrimString", function() + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim("") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim(" ") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim("ab") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim(" ab ") + local expected = " ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim(" a b ") + local expected = " a b" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim("\r\n\t\f a\r\n\t\f ") + local expected = "\r\n\t\f a" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string with unicode characters properly", function() + local trimmedInput = Text.RightTrim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ") + local expected = " 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n") + local expected = "\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + end) + + + describe("LeftTrimString", function() + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim("") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim(" ") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim("ab") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim(" ab ") + local expected = "ab " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim(" a b ") + local expected = " a b " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim("\r\n\t\f a\r\n\t\f ") + local expected = "a\r\n\t\f " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string with unicode characters properly", function() + local trimmedInput = Text.LeftTrim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n") + local expected = "😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n" + expect(trimmedInput).to.equal(expected) + end) + end) + + + describe("SpaceNormalize", function() + it("should remove multiple spaces between words", function() + local a = "This is not a normal sentence." + + expect(Text.SpaceNormalize(a)).to.equal("This is not a normal sentence.") + end) + + it("should remove leading and trailing whitespace", function() + local a = " SpaceTabSpaceTab " + + expect(Text.SpaceNormalize(a)).to.equal("SpaceTabSpaceTab") + end) + + it("should not change a string with no whitespace", function() + local a = "There'sNo%Whit.e\\space--InThis." + + expect(Text.SpaceNormalize(a)).to.equal(a) + end) + + it("should remove all whitespace in a string that is nothing but whitespace", function() + local a = " " + + expect(Text.SpaceNormalize(a)).to.equal("") + end) + + it("should handle the case where the string is empty", function() + local a = "" + + expect(Text.SpaceNormalize(a)).to.equal(a) + end) + + it("should throw an error if called an a non-string type", function() + local a = { first = 1, second = 2 } + + expect(function() + Text.SpaceNormalize(a) + end).to.throw() + end) + end) + + + describe("Split", function() + local function tableEquals(tb1, tb2) + local tables = { tb1, tb2 } + + for _,tb in ipairs(tables) do + for key in pairs(tb) do + if tb1[key] ~= tb2[key] then + return false + end + end + end + + return true + end + + it("should return the correct table for your standard use case", function() + local a = "this,is,comma,separated" + local pattern = "," + local expectedResult = { + [1] = "this", + [2] = "is", + [3] = "comma", + [4] = "separated", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should not remove whitespace", function() + local a = " SpaceTab , , Space" + local pattern = "," + local expectedResult = { + [1] = " SpaceTab ", + [2] = " ", + [3] = " Space", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should treat regular expressions as plain text", function() + local a = "Notyour^%s+normalstring.Thisisasecondsentence." + local b = "." + local c = "^%s+" + local d = "%A" + + local expectedB = { + [1] = "Notyour^%s+normalstring", + [2] = "Thisisasecondsentence", + [3] = "", + } + local expectedC = { + [1] = "Notyour", + [2] = "normalstring.Thisisasecondsentence." + } + local expectedD = { + [1] = "Notyour^%s+normalstring.Thisisasecondsentence." + } + + expect(tableEquals(Text.Split(a, b), expectedB)).to.equal(true) + expect(tableEquals(Text.Split(a, c), expectedC)).to.equal(true) + expect(tableEquals(Text.Split(a, d), expectedD)).to.equal(true) + end) + + it("should work when pattern is not in string", function() + local a = "The pattern you are looking for does not exist." + local pattern = "," + local expectedResult = { + [1] = "The pattern you are looking for does not exist.", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should work when called on an empty string", function() + local a = "" + local pattern = "," + local expectedResult = { + [1] = "", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should throw an error if called on an empty pattern", function() + local a = "The pattern definitely doesn't exist here." + local pattern = "" + + expect(function() + Text.Split(a, pattern) + end).to.throw() + end) + + it("should throw an error if called an a non-string type", function() + local a = { first = 1, second = 2 } + local b = "an actual string" + + expect(function() + Text.Split(a, b) + end).to.throw() + + expect(function() + Text.Split(b, a) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/Common/memoize.lua b/Client2018/content/LuaPackages/AppTempCommon/Common/memoize.lua new file mode 100644 index 0000000..261dcd0 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/Common/memoize.lua @@ -0,0 +1,68 @@ +--[[ + memoize creates a function as a wrapper that caches the last outputs of a function. + This is useful if you know that the function should return the same output every + time it is run with the same inputs. The function should only return an output, and + not have any side effects. These side effects are not cached. + + Without memoize's caching, even though the function ouputs the same values, the + memory locations of the values are different; tables made in the function, even if + they have the same values, won't be the same tables. + + memoize only caches the last set of inputs and ouputs. This means that it is only + helpful when the function is likely to be called with the same inputs multiple + times in a row. This is the case with most Roact use cases. + + Note that memoize only does a ** shallow check on table inputs ** . This means + that if the same table is input but the elements of the table are different then + it will be assumed that the table has not changed. + + In addition to all the previous warnings, memoize strips trailing nils. This means + that if foo is a memoized function and we call foo(), then foo(nil) will return a + cached value. This is opposed to how print handles input. print() only outputs a + new line, but print(nil) outputs "nil". This is because varargs can detect the + number of arguments passed in. So, be careful when using memoize with varargs. + Trailing nils will be stripped. + + The wrapper can take any number of inputs and give any number of outputs. + Leading and interspersed nils are handled gracefully. Trailing nils on the input + are stripped. +]] +local function captureSize(...) + return {...}, select("#", ...) +end + +local function memoize(func) + assert(type(func) == "function", "memoize requires a function to memoize") + + local lastArgs + local lastNumArgs + local lastOutput + local lastNumOutput + + return function(...) + local numArgs = select("#", ...) + + while numArgs > 0 and select(numArgs, ...) == nil do + numArgs = numArgs - 1 + end + + if numArgs ~= lastNumArgs then + lastArgs = {...} + lastNumArgs = numArgs + lastOutput, lastNumOutput = captureSize(func(...)) + return unpack(lastOutput, 1, lastNumOutput) + end + + for i = 1, lastNumArgs do + if select(i, ...) ~= lastArgs[i] then + lastArgs = {...} + lastOutput, lastNumOutput = captureSize(func(...)) + break + end + end + + return unpack(lastOutput, 1, lastNumOutput) + end +end + +return memoize \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/Common/memoize.spec.lua b/Client2018/content/LuaPackages/AppTempCommon/Common/memoize.spec.lua new file mode 100644 index 0000000..5ae2b6e --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/Common/memoize.spec.lua @@ -0,0 +1,213 @@ +return function() + local memoize = require(script.Parent.memoize) + + describe("memoize", function() + it("should handle arity 0", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + expect(identity()).to.equal(nil) + expect(identity(nil)).to.equal(nil) + expect(identity(nil, nil)).to.equal(nil) + expect(callCount).to.equal(1) + end) + + it("should handle arity 1", function() + local callCount = 0 + local identity = memoize(function(a) + callCount = callCount + 1 + return a + end) + + expect(identity(5)).to.equal(5) + expect(identity(5)).to.equal(5) + expect(callCount).to.equal(1) + + expect(identity(6)).to.equal(6) + expect(callCount).to.equal(2) + + expect(identity(5)).to.equal(5) + expect(callCount).to.equal(3) + end) + + it("should handle arity 2", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + expect(callCount).to.equal(1) + + a, b = identity(6, 5) + expect(a).to.equal(6) + expect(b).to.equal(5) + + expect(callCount).to.equal(2) + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + expect(callCount).to.equal(3) + end) + + it("should handle mixed arity", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + expect(callCount).to.equal(1) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b = identity() + expect(a).to.equal(nil) + expect(b).to.equal(nil) + + a, b = identity() + expect(a).to.equal(nil) + expect(b).to.equal(nil) + + expect(callCount).to.equal(3) + end) + + it("should handle trailing nils", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(5, nil) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + expect(callCount).to.equal(1) + + a, b = identity(7) + expect(a).to.equal(7) + expect(b).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + expect(callCount).to.equal(3) + end) + + it("should handle leading nils", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(nil, 7) + expect(a).to.equal(nil) + expect(b).to.equal(7) + + a, b = identity(nil, 7) + expect(a).to.equal(nil) + expect(b).to.equal(7) + + expect(callCount).to.equal(1) + + a, b = identity(7) + expect(a).to.equal(7) + expect(b).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b = identity(nil, 7) + expect(a).to.equal(nil) + expect(b).to.equal(7) + + expect(callCount).to.equal(3) + end) + + it("should handle interspersed nils", function() + local callCount = 0 + local identity = memoize(function(a, b, c, d) + callCount = callCount + 1 + return a, b, c, d + end) + + local a, b, c, d + + a, b, c, d = identity(7, nil, 7, nil) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(7) + expect(d).to.equal(nil) + + -- Trailing nils can affect how interspersed nils are handled + a, b, c, d = identity(7, nil, 7) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(7) + expect(d).to.equal(nil) + + expect(callCount).to.equal(1) + + a, b, c, d = identity(7, nil, nil, nil) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(nil) + expect(d).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b, c, d = identity(7, nil, 7, nil) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(7) + expect(d).to.equal(nil) + + expect(callCount).to.equal(3) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/AddUser.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/AddUser.lua new file mode 100644 index 0000000..7ac8be2 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/AddUser.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(user) + return { + user = user + } +end) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/AddUsers.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/AddUsers.lua new file mode 100644 index 0000000..fad0bcb --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/AddUsers.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(users) + return { + users = users + } +end) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/RemoveUser.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/RemoveUser.lua new file mode 100644 index 0000000..b407b15 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/RemoveUser.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId) + return { + userId = userId, + } +end) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetDeviceOrientation.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetDeviceOrientation.lua new file mode 100644 index 0000000..6cfa228 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetDeviceOrientation.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(deviceOrientation) + return { + deviceOrientation = deviceOrientation, + } +end) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetFriendCount.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetFriendCount.lua new file mode 100644 index 0000000..39aa112 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetFriendCount.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") +local Common = CorePackages.AppTempCommon.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(count) + return { + count = count, + } +end) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserIsFriend.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserIsFriend.lua new file mode 100644 index 0000000..690a05e --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserIsFriend.lua @@ -0,0 +1,9 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId, isFriend) + return { + userId = userId, + isFriend = isFriend, + } +end) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserMembershipType.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserMembershipType.lua new file mode 100644 index 0000000..1c08dbe --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserMembershipType.lua @@ -0,0 +1,11 @@ +local CorePackages = game:GetService("CorePackages") +local Common = CorePackages.AppTempCommon.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(userId, membershipType) + return { + userId = userId, + membershipType = membershipType, + } +end) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserPresence.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserPresence.lua new file mode 100644 index 0000000..b25a118 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserPresence.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId, presence, lastLocation) + return { + userId = tostring(userId), + presence = presence, + lastLocation = lastLocation, + } +end) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserThumbnail.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserThumbnail.lua new file mode 100644 index 0000000..508cf70 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserThumbnail.lua @@ -0,0 +1,13 @@ +local CorePackages = game:GetService("CorePackages") +local Common = CorePackages.AppTempCommon.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(userId, image, thumbnailType, thumbnailSize) + return { + userId = userId, + image = image, + thumbnailType = thumbnailType, + thumbnailSize = thumbnailSize, + } +end) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatSendMessage.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatSendMessage.lua new file mode 100644 index 0000000..1642bb5 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatSendMessage.lua @@ -0,0 +1,15 @@ +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +return function(requestImpl, conversationId, messageText) + local payload = HttpService:JSONEncode({ + conversationId = conversationId, + message = messageText, + }) + + local url = string.format("%s/send-message", Url.CHAT_URL) + + return requestImpl(url, "POST", { postBody = payload }) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatStartOneToOneConversation.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatStartOneToOneConversation.lua new file mode 100644 index 0000000..4f7a75f --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatStartOneToOneConversation.lua @@ -0,0 +1,14 @@ +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +return function(requestImpl, userId, clientId) + local payload = HttpService:JSONEncode({ + participantuserId = userId + }) + + local url = string.format("%s/start-one-to-one-conversation", Url.CHAT_URL) + + return requestImpl(url, "POST", { postBody = payload }) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GetPlaceInfos.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GetPlaceInfos.lua new file mode 100644 index 0000000..883bc34 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GetPlaceInfos.lua @@ -0,0 +1,17 @@ +local CorePackages = game:GetService("CorePackages") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +return function(requestImpl, placeIds) + local argTable = { + placeIds = placeIds, + } + + -- construct the url + local args = Url:makeQueryString(argTable) + local url = string.format("%s/v1/games/multiget-place-details?%s", + Url.GAME_URL, args + ) + + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriendCount.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriendCount.lua new file mode 100644 index 0000000..b4fa8c3 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriendCount.lua @@ -0,0 +1,30 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +--[[ + This endpoint returns a promise that resolves to: + + [ + { + "success:" true, + "count": "0" + }, + ] +]]-- + +-- requestImpl - (function>(url, requestMethod, options)) +return function(requestImpl) + + local argTable = { + userId = Players.LocalPlayer.UserId, + } + + local args = Url:makeQueryString(argTable) + local url = string.format("%s/user/get-friendship-count?%s", + Url.API_URL, tostring(Players.LocalPlayer.UserId), args + ) + + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriends.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriends.lua new file mode 100644 index 0000000..79b8653 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriends.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +return function(requestImpl, userId) + local url = string.format("%s/users/%s/friends", + Url.FRIEND_URL, userId + ) + + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetPresence.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetPresence.lua new file mode 100644 index 0000000..b054e6e --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetPresence.lua @@ -0,0 +1,25 @@ +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +-- Endpoint documented here: +-- https://presence.roblox.com/docs + +return function(requestImpl, userIds) + local userIdsToNumber = {} + for _, id in pairs(userIds) do + local idToNumber = tonumber(id) + if idToNumber then + table.insert(userIdsToNumber, idToNumber) + end + end + + local payload = HttpService:JSONEncode({ + userIds = userIdsToNumber, + }) + + local url = string.format("%s/presence/users", Url.PRESENCE_URL) + + return requestImpl(url, "POST", { postBody = payload }) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetThumbnail.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetThumbnail.lua new file mode 100644 index 0000000..ac79bd6 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetThumbnail.lua @@ -0,0 +1,46 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) + +local THUMBNAIL_TYPE_BY_NAME = { + AvatarThumbnail = Enum.ThumbnailType.AvatarThumbnail, + HeadShot = Enum.ThumbnailType.HeadShot, +} + +local THUMBNAIL_SIZE_BY_NAME = { + Size48x48 = Enum.ThumbnailSize.Size48x48, + Size100x100 = Enum.ThumbnailSize.Size100x100, + Size150x150 = Enum.ThumbnailSize.Size150x150, +} + +return function(userId, thumbnailType, thumbnailSize) + return Promise.new(function(resolve, reject) + --Async methods will yield the thread + spawn(function() + local result = {success = false} + local success, message = pcall(function() + local image, isFinal = Players:GetUserThumbnailAsync( + userId, THUMBNAIL_TYPE_BY_NAME[thumbnailType], THUMBNAIL_SIZE_BY_NAME[thumbnailSize] + ) + + result = { + success = true, + id = userId, + thumbnailType = thumbnailType, + thumbnailSize = thumbnailSize, + + image = isFinal and image or nil, + isFinal = isFinal, + } + end) + + if success then + resolve(result) + else + result.message = message + reject(result) + end + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Url.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Url.lua new file mode 100644 index 0000000..4201c06 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Url.lua @@ -0,0 +1,110 @@ +--[[ + Url Constructor + + Provides a single location for base urls. + +]]-- +local ContentProvider = game:GetService("ContentProvider") + +-- helper functions +local function parseBaseUrlInformation() + -- get the current base url from the current configuration + local baseUrl = ContentProvider.BaseUrl + + -- keep a copy of the base url (https://www.roblox.com/) + -- append a trailing slash if there isn't one + if baseUrl:sub(#baseUrl) ~= "/" then + baseUrl = baseUrl .. "/" + end + + -- parse out scheme (http, https) + local _, schemeEnd = baseUrl:find("://") + + -- parse out the prefix (www, kyle, ying, etc.) + local prefixIndex, prefixEnd = baseUrl:find("%.", schemeEnd + 1) + local basePrefix = baseUrl:sub(schemeEnd + 1, prefixIndex - 1) + + -- parse out the domain (roblox.com/, sitetest1.robloxlabs.com/, etc.) + local baseDomain = baseUrl:sub(prefixEnd + 1) + + return baseUrl, basePrefix, baseDomain +end +local function preventTableModification(aTable, key, value) + error("Attempt to modify read-only table") +end +local function createReadOnlyTable(aTable) + return setmetatable({}, { + __index = aTable, + __newindex = preventTableModification, + __metatable = false + }); +end + + +-- url construction building blocks +local _baseUrl, _basePrefix, _baseDomain = parseBaseUrlInformation() + +-- construct urls once +local _baseApiUrl = string.format("https://api.%s", _baseDomain) +local _baseAuthUrl = string.format("https://auth.%s", _baseDomain) +local _baseChatUrl = string.format("https://chat.%sv2", _baseDomain) +local _baseFriendUrl = string.format("https://friends.%sv1", _baseDomain) +local _baseGameAssetUrl = string.format("https://assetgame.%s", _baseDomain) +local _baseGamesUrl = string.format("https://games.%s", _baseDomain) +local _baseNotificationUrl = string.format("https://notifications.%s", _baseDomain) +local _basePresenceUrl = string.format("https://presence.%sv1", _baseDomain) +local _baseRealtimeUrl = string.format("https://realtime.%s", _baseDomain) +local _baseWebUrl = string.format("https://web.%s", _baseDomain) + +-- public api +local Url = { + DOMAIN = _baseDomain, + PREFIX = _basePrefix, + BASE_URL = _baseUrl, + API_URL = _baseApiUrl, + AUTH_URL = _baseAuthUrl, + GAME_URL = _baseGamesUrl, + GAME_ASSET_URL = _baseGameAssetUrl, + CHAT_URL = _baseChatUrl, + FRIEND_URL = _baseFriendUrl, + PRESENCE_URL = _basePresenceUrl, + NOTIFICATION_URL = _baseNotificationUrl, + REALTIME_URL = _baseRealtimeUrl, + WEB_URL = _baseWebUrl +} + +function Url:getUserProfileUrl(userId) + return string.format("%susers/%s/profile", self.BASE_URL, userId) +end + +function Url:isVanitySite() + return self.PREFIX ~= "www" +end + +-- data - (table) a table of key/value pairs to format +function Url:makeQueryString(data) + --NOTE - This function can be used to create a query string of parameters + -- at the end of url query, or create a application/form-url-encoded post body string + local params = {} + + -- NOTE - Arrays are handled, but generally data is expected to be flat. + for key, value in pairs(data) do + if value ~= nil then --for optional params + if type(value) == "table" then + for i = 1, #value do + table.insert(params, key .. "=" .. value[i]) + end + else + table.insert(params, key .. "=" .. tostring(value)) + end + end + end + + return table.concat(params, "&") +end + + +-- prevent anyone from modifying this table: +Url = createReadOnlyTable(Url) + +return Url \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Url.spec.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Url.spec.lua new file mode 100644 index 0000000..62b8fab --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Http/Url.spec.lua @@ -0,0 +1,12 @@ +return function() + + local ContentProvider = game:GetService("ContentProvider") + local Url = require(script.Parent.Url) + + it("The base url has not been changed for debugging", function() + local baseUrl = ContentProvider.BaseUrl + + expect(baseUrl).to.equal(Url.BASE_URL) + end) + +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/MockId.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/MockId.lua new file mode 100644 index 0000000..456cea0 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/MockId.lua @@ -0,0 +1,10 @@ +--[[ + A function to return a fake ID, used for testing +]] + +local lastId = 0 + +return function() + lastId = lastId + 1 + return ("MOCK-%d"):format(lastId) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Models/ThumbnailRequest.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Models/ThumbnailRequest.lua new file mode 100644 index 0000000..6df561f --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Models/ThumbnailRequest.lua @@ -0,0 +1,18 @@ +local ThumbnailRequest = {} + +function ThumbnailRequest.new() + local self = {} + + return self +end + +function ThumbnailRequest.fromData(thumbnailType, thumbnailSize) + local self = ThumbnailRequest.new() + + self.thumbnailType = thumbnailType + self.thumbnailSize = thumbnailSize + + return self +end + +return ThumbnailRequest \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Models/User.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Models/User.lua new file mode 100644 index 0000000..98bdc6b --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Models/User.lua @@ -0,0 +1,84 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId) + +local FFlagFixUsersReducerDataLoss = settings():GetFFlag("FixUsersReducerDataLoss") + +local User = {} + +User.PresenceType = { + OFFLINE = "OFFLINE", + ONLINE = "ONLINE", + IN_GAME = "IN_GAME", + IN_STUDIO = "IN_STUDIO", +} + +function User.new() + local self = {} + + return self +end + +function User.mock() + local self = User.new() + + self.id = MockId() + + self.isFetching = false + self.isFriend = false + self.lastLocation = nil + self.name = "USER NAME" + self.placeId = nil + self.presence = User.PresenceType.OFFLINE + self.membership = nil + if not FFlagFixUsersReducerDataLoss then + self.thumbnails = {} + end + + return self +end + +function User.fromData(id, name, isFriend) + local self = User.new() + + self.id = tostring(id) + + self.isFetching = false + self.isFriend = isFriend + self.lastLocation = nil + self.name = name + self.placeId = nil + if FFlagFixUsersReducerDataLoss then + self.presence = (self.id == tostring(Players.LocalPlayer.UserId)) and User.PresenceType.ONLINE or nil + else + self.presence = (self.id == tostring(Players.LocalPlayer.UserId)) and User.PresenceType.ONLINE or + User.PresenceType.OFFLINE + self.thumbnails = {} + end + + return self +end + +function User.userPresenceToText(localization, user) + local presence = user.presence + local lastLocation = user.lastLocation + + if not presence then + return '' + end + + if presence == User.PresenceType.OFFLINE then + return localization:Format("Common.Presence.Label.Offline") + elseif presence == User.PresenceType.ONLINE then + return localization:Format("Common.Presence.Label.Online") + elseif (presence == User.PresenceType.IN_GAME) or (presence == User.PresenceType.IN_STUDIO) then + if lastLocation ~= nil then + return lastLocation + else + return localization:Format("Common.Presence.Label.Online") + end + end +end + +return User \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Promise.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Promise.lua new file mode 100644 index 0000000..465e93c --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Promise.lua @@ -0,0 +1,337 @@ +--[[ + An implementation of Promises similar to Promise/A+. +]] +local TU = require(script.Parent.TableUtilities) + +local PROMISE_DEBUG = true + +-- If promise debugging is on, use a version of pcall that warns on failure. +-- This is useful for finding errors that happen within Promise itself. +local wpcall +if PROMISE_DEBUG then + wpcall = function(f, ...) + local result = { pcall(f, ...) } + + if not result[1] then + warn(result[2]) + end + + return unpack(result) + end +else + wpcall = pcall +end + +--[[ + Creates a function that invokes a callback with correct error handling and + resolution mechanisms. +]] +local function createAdvancer(callback, resolve, reject) + return function(...) + local result = { wpcall(callback, ...) } + local ok = table.remove(result, 1) + + if ok then + resolve(unpack(result)) + else + reject(unpack(result)) + end + end +end + +local function isEmpty(t) + return next(t) == nil +end + +local Promise = {} +Promise.__index = Promise + +Promise.Status = { + Started = "Started", + Resolved = "Resolved", + Rejected = "Rejected", +} + +--[[ + Constructs a new Promise with the given initializing callback. + + This is generally only called when directly wrapping a non-promise API into + a promise-based version. + + The callback will receive 'resolve' and 'reject' methods, used to start + invoking the promise chain. + + For example: + + local function get(url) + return Promise.new(function(resolve, reject) + spawn(function() + resolve(HttpService:GetAsync(url)) + end) + end) + end + + get("https://google.com") + :andThen(function(stuff) + print("Got some stuff!", stuff) + end) +]] +function Promise.new(callback) + local promise = { + -- Used to locate where a promise was created + _source = debug.traceback(), + + -- A tag to identify us as a promise + _type = "Promise", + + _status = Promise.Status.Started, + + -- A table containing a list of all results, whether success or failure. + -- Only valid if _status is set to something besides Started + _value = nil, + + -- Queues representing functions we should invoke when we update! + _queuedResolve = {}, + _queuedReject = {}, + } + + setmetatable(promise, Promise) + + local function resolve(...) + promise:_resolve(...) + end + + local function reject(...) + promise:_reject(...) + end + + local ok, err = wpcall(callback, resolve, reject) + + if not ok and promise._status == Promise.Status.Started then + reject(err) + end + + return promise +end + +--[[ + Create a promise that represents the immediately resolved value. +]] +function Promise.resolve(value) + return Promise.new(function(resolve) + resolve(value) + end) +end + +--[[ + Create a promise that represents the immediately rejected value. +]] +function Promise.reject(value) + return Promise.new(function(_, reject) + reject(value) + end) +end + +--[[ + Returns a new promise that: + * is resolved when all input promises resolve + * is rejected if ANY input promises reject +]] +function Promise.all(...) + local promises = {...} + + -- check if we've been given a list of promises, not just a variable number of promises + if type(promises[1]) == "table" and promises[1]._type ~= "Promise" then + -- we've been given a table of promises already + promises = promises[1] + end + + return Promise.new(function(resolve, reject) + local isResolved = false + local results = {} + local totalCompleted = 0 + local function promiseCompleted(index, result) + if isResolved then + return + end + + results[index] = result + totalCompleted = totalCompleted + 1 + + if totalCompleted == #promises then + resolve(results) + isResolved = true + end + end + + for index, promise in ipairs(promises) do + -- if a promise isn't resolved yet, add listeners for when it does + if promise._status == Promise.Status.Started then + promise:andThen(function(result) + promiseCompleted(index, result) + end):catch(function(reason) + isResolved = true + reject(reason) + end) + + -- if a promise is already resolved, move on + elseif promise._status == Promise.Status.Resolved then + promiseCompleted(index, promise._value) + + -- if a promise is rejected, reject the whole chain + else --if promise._status == Promise.Status.Rejected then + isResolved = true + reject(promise._value) + end + end + end) +end + +--[[ + Is the given object a Promise instance? +]] +function Promise.is(object) + if type(object) ~= "table" then + return false + end + + return object._type == "Promise" +end + +--[[ + Creates a new promise that receives the result of this promise. + + The given callbacks are invoked depending on that result. +]] +function Promise:andThen(successHandler, failureHandler) + -- Create a new promise to follow this part of the chain + return Promise.new(function(resolve, reject) + -- Our default callbacks just pass values onto the next promise. + -- This lets success and failure cascade correctly! + + local successCallback = resolve + if successHandler then + successCallback = createAdvancer(successHandler, resolve, reject) + end + + local failureCallback = reject + if failureHandler then + failureCallback = createAdvancer(failureHandler, resolve, reject) + end + + if self._status == Promise.Status.Started then + -- If we haven't resolved yet, put ourselves into the queue + table.insert(self._queuedResolve, successCallback) + table.insert(self._queuedReject, failureCallback) + elseif self._status == Promise.Status.Resolved then + -- This promise has already resolved! Trigger success immediately. + successCallback(unpack(self._value)) + elseif self._status == Promise.Status.Rejected then + -- This promise died a terrible death! Trigger failure immediately. + failureCallback(unpack(self._value)) + end + end) +end + +--[[ + Used to catch any errors that may have occurred in the promise. +]] +function Promise:catch(failureCallback) + return self:andThen(nil, failureCallback) +end + +--[[ + Yield until the promise is completed. + + This matches the execution model of normal Roblox functions. +]] +function Promise:await() + if self._status == Promise.Status.Started then + local result + local bindable = Instance.new("BindableEvent") + + self:andThen(function(...) + result = {...} + bindable:Fire(true) + end, function(...) + result = {...} + bindable:Fire(false) + end) + + local ok = bindable.Event:Wait() + bindable:Destroy() + + if not ok then + error(tostring(result[1]), 2) + end + + return unpack(result) + elseif self._status == Promise.Status.Resolved then + return unpack(self._value) + elseif self._status == Promise.Status.Rejected then + error(tostring(self._value[1]), 2) + end +end + +function Promise:_resolve(...) + if self._status ~= Promise.Status.Started then + return + end + + -- If the resolved value was a Promise, we chain onto it! + if Promise.is((...)) then + -- Without this warning, arguments sometimes mysteriously disappear + if select("#", ...) > 1 then + local message = ("When returning a Promise from andThen, extra arguments are discarded! See:\n\n%s"):format( + self._source + ) + warn(message) + end + + (...):andThen(function(...) + self:_resolve(...) + end, function(...) + self:_reject(...) + end) + + return + end + + self._status = Promise.Status.Resolved + self._value = {...} + + -- We assume that these callbacks will not throw errors. + for _, callback in ipairs(self._queuedResolve) do + callback(...) + end +end + +function Promise:_reject(...) + if self._status ~= Promise.Status.Started then + return + end + + self._status = Promise.Status.Rejected + self._value = {...} + + -- If there are any rejection handlers, call those! + if not isEmpty(self._queuedReject) then + -- We assume that these callbacks will not throw errors. + for _, callback in ipairs(self._queuedReject) do + callback(...) + end + else + -- At this point, no one was able to observe the error. + -- An error handler might still be attached if the error occurred + -- synchronously. We'll wait one tick, and if there are still no + -- observers, then we should put a message in the console. + + local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format( + TU.RecursiveToString((...)), + self._source + ) + warn(message) + end +end + +return Promise diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Promise.spec.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Promise.spec.lua new file mode 100644 index 0000000..2193fcb --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Promise.spec.lua @@ -0,0 +1,262 @@ +return function() + local Promise = require(script.Parent.Promise) + + describe("Promise.new", function() + it("should instantiate with a callback", function() + local promise = Promise.new(function() end) + + expect(promise).to.be.ok() + end) + + it("should invoke the given callback with resolve and reject", function() + local callCount = 0 + local resolveArg + local rejectArg + + local promise = Promise.new(function(resolve, reject) + callCount = callCount + 1 + resolveArg = resolve + rejectArg = reject + end) + + expect(promise).to.be.ok() + + expect(callCount).to.equal(1) + expect(resolveArg).to.be.a("function") + expect(rejectArg).to.be.a("function") + expect(promise._status).to.equal(Promise.Status.Started) + end) + + it("should resolve promises on resolve()", function() + local callCount = 0 + + local promise = Promise.new(function(resolve) + callCount = callCount + 1 + resolve() + end) + + expect(promise).to.be.ok() + expect(callCount).to.equal(1) + expect(promise._status).to.equal(Promise.Status.Resolved) + end) + + it("should reject promises on reject()", function() + local callCount = 0 + + local promise = Promise.new(function(resolve, reject) + callCount = callCount + 1 + reject() + end) + + expect(promise).to.be.ok() + expect(callCount).to.equal(1) + expect(promise._status).to.equal(Promise.Status.Rejected) + end) + + it("should reject on error in callback", function() + local callCount = 0 + + local promise = Promise.new(function() + callCount = callCount + 1 + error("hahah") + end) + + expect(promise).to.be.ok() + expect(callCount).to.equal(1) + expect(promise._status).to.equal(Promise.Status.Rejected) + expect(promise._value[1]:find("hahah")).to.be.ok() + end) + end) + + describe("Promise.resolve", function() + it("should immediately resolve with a value", function() + local promise = Promise.resolve(5) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Resolved) + expect(promise._value[1]).to.equal(5) + end) + + it("should chain onto passed promises", function() + local promise = Promise.resolve(Promise.new(function(_, reject) + reject(7) + end)) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Rejected) + expect(promise._value[1]).to.equal(7) + end) + end) + + describe("Promise.reject", function() + it("should immediately reject with a value", function() + local promise = Promise.reject(6) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Rejected) + expect(promise._value[1]).to.equal(6) + end) + + it("should pass a promise as-is as an error", function() + local innerPromise = Promise.new(function(resolve) + resolve(6) + end) + + local promise = Promise.reject(innerPromise) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Rejected) + expect(promise._value[1]).to.equal(innerPromise) + end) + end) + + describe("Promise:andThen", function() + it("should chain onto resolved promises", function() + local args + local argsLength + local callCount = 0 + local badCallCount = 0 + + local promise = Promise.resolve(5) + + local chained = promise + :andThen(function(...) + args = {...} + argsLength = select("#", ...) + callCount = callCount + 1 + end, function() + badCallCount = badCallCount + 1 + end) + + expect(badCallCount).to.equal(0) + + expect(callCount).to.equal(1) + expect(argsLength).to.equal(1) + expect(args[1]).to.equal(5) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Resolved) + expect(promise._value[1]).to.equal(5) + + expect(chained).to.be.ok() + expect(chained).never.to.equal(promise) + expect(chained._status).to.equal(Promise.Status.Resolved) + expect(#chained._value).to.equal(0) + end) + + it("should chain onto rejected promises", function() + local args + local argsLength + local callCount = 0 + local badCallCount = 0 + + local promise = Promise.reject(5) + + local chained = promise + :andThen(function(...) + badCallCount = badCallCount + 1 + end, function(...) + args = {...} + argsLength = select("#", ...) + callCount = callCount + 1 + end) + + expect(badCallCount).to.equal(0) + + expect(callCount).to.equal(1) + expect(argsLength).to.equal(1) + expect(args[1]).to.equal(5) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Rejected) + expect(promise._value[1]).to.equal(5) + + expect(chained).to.be.ok() + expect(chained).never.to.equal(promise) + expect(chained._status).to.equal(Promise.Status.Resolved) + expect(#chained._value).to.equal(0) + end) + + it("should chain onto asynchronously resolved promises", function() + local args + local argsLength + local callCount = 0 + local badCallCount = 0 + + local startResolution + local promise = Promise.new(function(resolve) + startResolution = resolve + end) + + local chained = promise + :andThen(function(...) + args = {...} + argsLength = select("#", ...) + callCount = callCount + 1 + end, function() + badCallCount = badCallCount + 1 + end) + + expect(callCount).to.equal(0) + expect(badCallCount).to.equal(0) + + startResolution(6) + + expect(badCallCount).to.equal(0) + + expect(callCount).to.equal(1) + expect(argsLength).to.equal(1) + expect(args[1]).to.equal(6) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Resolved) + expect(promise._value[1]).to.equal(6) + + expect(chained).to.be.ok() + expect(chained).never.to.equal(promise) + expect(chained._status).to.equal(Promise.Status.Resolved) + expect(#chained._value).to.equal(0) + end) + + it("should chain onto asynchronously rejected promises", function() + local args + local argsLength + local callCount = 0 + local badCallCount = 0 + + local startResolution + local promise = Promise.new(function(_, reject) + startResolution = reject + end) + + local chained = promise + :andThen(function() + badCallCount = badCallCount + 1 + end, function(...) + args = {...} + argsLength = select("#", ...) + callCount = callCount + 1 + end) + + expect(callCount).to.equal(0) + expect(badCallCount).to.equal(0) + + startResolution(6) + + expect(badCallCount).to.equal(0) + + expect(callCount).to.equal(1) + expect(argsLength).to.equal(1) + expect(args[1]).to.equal(6) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Rejected) + expect(promise._value[1]).to.equal(6) + + expect(chained).to.be.ok() + expect(chained).never.to.equal(promise) + expect(chained._status).to.equal(Promise.Status.Resolved) + expect(#chained._value).to.equal(0) + end) + end) +end diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.lua new file mode 100644 index 0000000..9374895 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.lua @@ -0,0 +1,113 @@ +local CorePackages = game:GetService("CorePackages") + +local Immutable = require(CorePackages.AppTempCommon.Common.Immutable) + +local AddUser = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUser) +local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers) +local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence) +local RemoveUser = require(CorePackages.AppTempCommon.LuaApp.Actions.RemoveUser) +local SetUserIsFriend = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserIsFriend) +local SetUserMembershipType = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserMembershipType) +local SetUserPresence = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserPresence) +local SetUserThumbnail = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserThumbnail) + +local FFlagFixUsersReducerDataLoss = settings():GetFFlag("FixUsersReducerDataLoss") + +return function(state, action) + state = state or {} + + if action.type == AddUser.name then + local user = action.user + state = Immutable.Set(state, user.id, user) + elseif action.type == AddUsers.name then + if FFlagFixUsersReducerDataLoss then + local addedUsers = action.users + local usersUpdate = {} + for userId, addedUser in pairs(addedUsers) do + local existingUser = state[userId] + if existingUser then + usersUpdate[userId] = Immutable.JoinDictionaries(existingUser, addedUser) + else + usersUpdate[userId] = addedUser + end + end + + state = Immutable.JoinDictionaries(state, usersUpdate) + else + local users = action.users + state = Immutable.JoinDictionaries(state, users) + end + + elseif action.type == SetUserIsFriend.name then + local user = state[action.userId] + if user then + local newUser = Immutable.Set(user, "isFriend", action.isFriend) + state = Immutable.Set(state, user.id, newUser) + else + warn("Setting isFriend on user", action.userId, "who doesn't exist yet") + end + elseif action.type == SetUserPresence.name then + local user = state[action.userId] + if user then + local newUser = Immutable.JoinDictionaries(user, { + presence = action.presence, + lastLocation = action.lastLocation, + }) + state = Immutable.Set(state, user.id, newUser) + else + warn("Setting presence on user", action.userId, "who doesn't exist yet") + end + elseif action.type == ReceivedUserPresence.name then + local user = state[action.userId] + if user then + state = Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(user, { + presence = action.presence, + lastLocation = action.lastLocation, + placeId = action.placeId, + }), + }) + end + elseif action.type == SetUserThumbnail.name then + local user = state[action.userId] + if user then + if FFlagFixUsersReducerDataLoss then + local thumbnails = user.thumbnails or {} + state = Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(user, { + thumbnails = Immutable.JoinDictionaries(thumbnails, { + [action.thumbnailType] = Immutable.JoinDictionaries(thumbnails[action.thumbnailType] or {}, { + [action.thumbnailSize] = action.image, + }), + }), + }), + }) + else + state = Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(user, { + thumbnails = Immutable.JoinDictionaries(user.thumbnails, { + [action.thumbnailType] = Immutable.JoinDictionaries(user.thumbnails[action.thumbnailType] or {}, { + [action.thumbnailSize] = action.image, + }), + }), + }), + }) + end + end + elseif action.type == SetUserMembershipType.name then + local user = state[action.userId] + if user then + state = Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(user, { + membership = action.membershipType, + }), + }) + end + elseif action.type == RemoveUser.name then + if state[action.userId] then + state = Immutable.RemoveFromDictionary(state, action.userId) + end + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.spec.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.spec.lua new file mode 100644 index 0000000..f3969bc --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.spec.lua @@ -0,0 +1,107 @@ +return function() + local CorePackages = game:GetService("CorePackages") + + local Constants = require(CorePackages.AppTempCommon.LuaChat.Constants) + local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId) + local User = require(CorePackages.AppTempCommon.LuaApp.Models.User) + local Users = require(CorePackages.AppTempCommon.LuaApp.Reducers.Users) + + local AddUser = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUser) + local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence) + local SetUserIsFriend = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserIsFriend) + local SetUserMembershipType = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserMembershipType) + local SetUserPresence = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserPresence) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = Users(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("AddUser", function() + it("should add a user to the store", function() + local user = User.mock() + local state = {} + + state = Users(state, AddUser(user)) + + expect(state[user.id]).to.equal(user) + end) + end) + + describe("SetUserIsFriend", function() + it("should set isFriend on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + expect(state[user.id].isFriend).to.equal(false) + + state = Users(state, SetUserIsFriend(user.id, true)) + expect(state[user.id].isFriend).to.equal(true) + + state = Users(state, SetUserIsFriend(user.id, false)) + expect(state[user.id].isFriend).to.equal(false) + end) + end) + + describe("SetUserPresence", function() + it("should set presence on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + expect(state[user.id].presence).to.equal(User.PresenceType.OFFLINE) + + state = Users(state, SetUserPresence(user.id, User.PresenceType.ONLINE)) + expect(state[user.id].presence).to.equal(User.PresenceType.ONLINE) + + state = Users(state, SetUserPresence(user.id, User.PresenceType.IN_GAME)) + expect(state[user.id].presence).to.equal(User.PresenceType.IN_GAME) + + state = Users(state, SetUserPresence(user.id, User.PresenceType.IN_STUDIO)) + expect(state[user.id].presence).to.equal(User.PresenceType.IN_STUDIO) + end) + end) + + describe("ReceivedUserPresence", function() + it("should set presence on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + local existingPresence = user.presence + local newPresence = Constants.PresenceType.ONLINE + local lastLocation = MockId() + local newPlaceId = MockId() + + state = Users(state, ReceivedUserPresence(user.id, newPresence, lastLocation, newPlaceId)) + + expect(user.presence).to.equal(existingPresence) + expect(state[user.id].presence).to.equal(newPresence) + expect(state[user.id].lastLocation).to.equal(lastLocation) + expect(state[user.id].placeId).to.equal(newPlaceId) + end) + end) + + describe("SetUserMembershipType", function() + it("should set membership on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + local existingMembership = user.membership + local newMembership = Enum.MembershipType.BuildersClub + + state = Users(state, SetUserMembershipType(user.id, newMembership)) + + expect(user.membership).to.equal(existingMembership) + expect(state[user.id].membership).to.equal(newMembership) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/TableUtilities.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/TableUtilities.lua new file mode 100644 index 0000000..635536b --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/TableUtilities.lua @@ -0,0 +1,173 @@ +--[[ + Provides functions for comparing and printing lua tables. +]] + +local TableUtilities = {} +local defaultIgnore = {} + +--[[ + Takes two tables A and B, returns if they have the same key-value pairs + Except ignored keys +]] +function TableUtilities.ShallowEqual(A, B, ignore) + if not A or not B then + return false + elseif A == B then + return true + end + + if not ignore then + ignore = defaultIgnore + end + + for key, value in pairs(A) do + if B[key] ~= value and not ignore[key] then + return false + end + end + for key, value in pairs(B) do + if A[key] ~= value and not ignore[key] then + return false + end + end + + return true +end + +--[[ + Takes two tables A, B and a key, returns if two tables have the same value at key +]] +function TableUtilities.EqualKey(A, B, key) + if A and B and key and key ~= "" and A[key] and B[key] and A[key] == B[key] then + return true + end + return false +end + +--[[ + Takes two tables A and B, returns a new table with elements of A + which are either not keys in B or have a different value in B +]] +function TableUtilities.TableDifference(A, B) + local new = {} + + for key, value in pairs(A) do + if B[key] ~= A[key] then + new[key] = value + end + end + + return new +end + + +--[[ + Takes a list and returns a table whose + keys are elements of the list and whose + values are all true +]] +local function membershipTable(list) + local result = {} + for i = 1, #list do + result[list[i]] = true + end + return result +end + + +--[[ + Takes a table and returns a list of keys in that table +]] +local function listOfKeys(t) + local result = {} + for key,_ in pairs(t) do + table.insert(result, key) + end + return result +end + + +--[[ + Takes two lists A and B, returns a new list of elements of A + which are not in B +]] +function TableUtilities.ListDifference(A, B) + return listOfKeys(TableUtilities.TableDifference(membershipTable(A), membershipTable(B))) +end + + +--[[ + For debugging. Returns false if the given table has any of the following: + - a key that is neither a number or a string + - a mix of number and string keys + - number keys which are not exactly 1..#t +]] +function TableUtilities.CheckListConsistency(t) + local containsNumberKey = false + local containsStringKey = false + local numberConsistency = true + + local index = 1 + for x, _ in pairs(t) do + if type(x) == 'string' then + containsStringKey = true + elseif type(x) == 'number' then + if index ~= x then + numberConsistency = false + end + containsNumberKey = true + else + return false + end + + if containsStringKey and containsNumberKey then + return false + end + + index = index + 1 + end + + if containsNumberKey then + return numberConsistency + end + + return true +end + + +--[[ + For debugging, serializes the given table to a reasonable string that might even interpret as lua. +]] +function TableUtilities.RecursiveToString(t, indent) + indent = indent or '' + + if type(t) == 'table' then + local result = "" + if not TableUtilities.CheckListConsistency(t) then + result = result .. "-- WARNING: this table fails the list consistency test\n" + end + result = result .. "{\n" + for k,v in pairs(t) do + if type(k) == 'string' then + result = result + .. " " + .. indent + .. tostring(k) + .. " = " + .. TableUtilities.RecursiveToString(v, " "..indent) + ..";\n" + end + if type(k) == 'number' then + result = result .. " " .. indent .. TableUtilities.RecursiveToString(v, " "..indent)..",\n" + end + end + result = result .. indent .. "}" + return result + else + return tostring(t) + end +end + + +return TableUtilities + diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/TableUtilities.spec.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/TableUtilities.spec.lua new file mode 100644 index 0000000..692757c --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/TableUtilities.spec.lua @@ -0,0 +1,140 @@ +return function() + local TableUtilities = require(script.Parent.TableUtilities) + + it("should return whether tables are equal to each other", function() + local tableA = nil + local tableB = nil + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = nil + tableB = {} + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = {} + tableB = nil + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = {} + tableB = {} + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(true) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(true) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value2", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value1", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value2", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + key2 = "value2", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + end) + + it("should return whether tables are equal to each other at key", function() + local tableA = nil + local tableB = nil + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = nil + tableB = {} + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = {} + tableB = nil + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = {} + tableB = {} + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(true) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value2", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value1", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value2", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + key2 = "value2", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(true) + expect(TableUtilities.EqualKey(tableA, tableB, "key2")).to.equal(false) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/TestHelpers/MockRequest.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/TestHelpers/MockRequest.lua new file mode 100644 index 0000000..db07e59 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/TestHelpers/MockRequest.lua @@ -0,0 +1,36 @@ +--[[ + A hub for faking networking responses for tests +]] + +local CorePackages = game:GetService("CorePackages") +local AppTempCommon = CorePackages.AppTempCommon + +local Promise = require(AppTempCommon.LuaApp.Promise) +local HttpError = require(AppTempCommon.LuaApp.Http.HttpError) +local HttpResponse = require(AppTempCommon.LuaApp.Http.HttpResponse) +local StatusCodes = require(AppTempCommon.LuaApp.Http.StatusCodes) + + +local MockRequest = {} + +-- responseBody : (string) +function MockRequest.simpleSuccessRequest(responseBody) + assert(responseBody ~= nil, "Expected responseBody not to be nil") + + -- create a simple network handler that only needs a response body specified + return function(url, requestMethod, options) + return Promise.resolve(HttpResponse.new(url, responseBody, 0, StatusCodes.OK)) + end +end + +-- errMsg : (HttpError.Kind) +function MockRequest.simpleFailRequest(errKind) + assert(errKind ~= nil, "Expected errKind not to be nil") + + -- create a simple network handler that only needs an error kind specified + return function(url, requestMethod, options) + return Promise.reject(HttpError.new(url, errKind, "Fake request failed")) + end +end + +return MockRequest \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/TestHelpers/MockRequest.spec.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/TestHelpers/MockRequest.spec.lua new file mode 100644 index 0000000..66711b7 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/TestHelpers/MockRequest.spec.lua @@ -0,0 +1,50 @@ +return function() + local MockRequest = require(script.Parent.MockRequest) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local HttpError = require(Modules.LuaApp.Http.HttpError) + + describe("simpleSuccessRequest", function() + it("should return a function", function() + local request = MockRequest.simpleSuccessRequest("this is a test") + + expect(type(request)).to.equal("function") + end) + + it("should return the provided response as the resolution to a promise", function() + local testBodyMatches = false + + local testBody = "this is a test" + local request = MockRequest.simpleSuccessRequest(testBody) + + local httpPromise = request("testUrl", "GET") + httpPromise:andThen(function(httpResponse) + testBodyMatches = httpResponse.responseBody == testBody + end) + expect(testBodyMatches).to.equal(true) + end) + end) + + + describe("simpleFailRequest", function() + it("should return a function", function() + local request = MockRequest.simpleFailRequest(HttpError.Kind.Unknown) + + expect(type(request)).to.equal("function") + end) + + it("should return the provided error code as the rejection to a promise", function() + local testErrMatches = false + + local testErrKind = HttpError.Kind.Unknown + local request = MockRequest.simpleFailRequest(testErrKind) + + local httpPromise = request("testUrl", "GET") + httpPromise:catch(function(httpError) + testErrMatches = httpError.kind == testErrKind + end) + + expect(testErrMatches).to.equal(true) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchPlaceInfos.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchPlaceInfos.lua new file mode 100644 index 0000000..d80ae99 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchPlaceInfos.lua @@ -0,0 +1,24 @@ +local CorePackages = game:GetService("CorePackages") + +local Functional = require(CorePackages.AppTempCommon.Common.Functional) +local GetPlaceInfos = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.GetPlaceInfos) + +-- LuaChat +local PlaceInfoModel = require(CorePackages.AppTempCommon.LuaChat.Models.PlaceInfoModel) +local ReceivedMultiplePlaceInfos = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedMultiplePlaceInfos) + +return function(networkImpl, placeIds) + return function(store) + return GetPlaceInfos(networkImpl, placeIds):andThen(function(result) + local data = result.responseBody + + local placeInfos = Functional.Map(data, function(placeInfoData) + return PlaceInfoModel.fromWeb(placeInfoData) + end) + + store:dispatch(ReceivedMultiplePlaceInfos(placeInfos)) + + return placeInfos + end) + end +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersData.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersData.lua new file mode 100644 index 0000000..36b9d05 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersData.lua @@ -0,0 +1,17 @@ +local CorePackages = game:GetService("CorePackages") + +local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) +local ApiFetchUsersPresences = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchUsersPresences) +local ApiFetchUsersThumbnail = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchUsersThumbnail) + +--this thunk will fill out users list with thumbnail and presence info +return function(networkImpl, userIds, thumbnailRequest) + return function(store) + local fetchedPromises = {} + + table.insert(fetchedPromises, store:dispatch(ApiFetchUsersPresences(networkImpl, userIds))) + table.insert(fetchedPromises, store:dispatch(ApiFetchUsersThumbnail(networkImpl, userIds, thumbnailRequest))) + + return Promise.all(fetchedPromises) + end +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriendCount.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriendCount.lua new file mode 100644 index 0000000..2301055 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriendCount.lua @@ -0,0 +1,21 @@ +local CorePackages = game:GetService("CorePackages") + +local Actions = CorePackages.AppTempCommon.LuaApp.Actions +local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests + +local UsersGetFriendCount = require(Requests.UsersGetFriendCount) +local SetFriendCount = require(Actions.SetFriendCount) + +return function(networkImpl) + return function(store) + return UsersGetFriendCount(networkImpl):andThen(function(result) + local data = result.responseBody + + if data.success and data.count then + store:dispatch(SetFriendCount(data.count)) + end + + return data.count + end) + end +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriends.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriends.lua new file mode 100644 index 0000000..1420205 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriends.lua @@ -0,0 +1,34 @@ +local CorePackages = game:GetService("CorePackages") +local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests + +local ApiFetchUsersData = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchUsersData) +local ApiFetchUsersFriendCount = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchUsersFriendCount) +local UsersGetFriends = require(Requests.UsersGetFriends) + +local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers) +local UserModel = require(CorePackages.AppTempCommon.LuaApp.Models.User) + +return function(requestImpl, userId, thumbnailRequests) + return function(store) + return store:dispatch(ApiFetchUsersFriendCount(requestImpl)):andThen(function() + return UsersGetFriends(requestImpl, userId):andThen(function(response) + local responseBody = response.responseBody + + local userIds = {} + local newUsers = {} + for _, userData in pairs(responseBody.data) do + local id = tostring(userData.id) + local newUser = UserModel.fromData(id, userData.name, true) + + table.insert(userIds, id) + newUsers[newUser.id] = newUser + end + store:dispatch(AddUsers(newUsers)) + + return userIds + end):andThen(function(userIds) + store:dispatch(ApiFetchUsersData(requestImpl, userIds, thumbnailRequests)) + end) + end) + end +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersPresences.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersPresences.lua new file mode 100644 index 0000000..3e50932 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersPresences.lua @@ -0,0 +1,29 @@ +local CorePackages = game:GetService("CorePackages") + +local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence) +local User = require(CorePackages.AppTempCommon.LuaApp.Models.User) +local UsersGetPresence = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.UsersGetPresence) + +local webPresenceMap = { + [0] = User.PresenceType.OFFLINE, + [1] = User.PresenceType.ONLINE, + [2] = User.PresenceType.IN_GAME, + [3] = User.PresenceType.IN_STUDIO +} + +return function(networkImpl, userIds) + return function(store) + return UsersGetPresence(networkImpl, userIds):andThen(function(result) + local responseBody = result.responseBody + + for _, presenceModel in pairs(responseBody.userPresences) do + store:dispatch(ReceivedUserPresence( + tostring(presenceModel.userId), + webPresenceMap[presenceModel.userPresenceType], + presenceModel.lastLocation, + presenceModel.placeId + )) + end + end) + end +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersThumbnail.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersThumbnail.lua new file mode 100644 index 0000000..0743c60 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersThumbnail.lua @@ -0,0 +1,42 @@ +local CorePackages = game:GetService("CorePackages") + +local Actions = CorePackages.AppTempCommon.LuaApp.Actions +local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests + +local UsersGetThumbnail = require(Requests.UsersGetThumbnail) +local SetUserThumbnail = require(Actions.SetUserThumbnail) +local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) + +local function fetchThumbnailsBatch(networkImpl, userIds, thumbnailRequest) + local fetchedPromises = {} + + for _, userId in pairs(userIds) do + table.insert(fetchedPromises, + UsersGetThumbnail(userId, thumbnailRequest.thumbnailType, thumbnailRequest.thumbnailSize) + ) + end + + return Promise.all(fetchedPromises) +end + +return function(networkImpl, userIds, thumbnailRequests) + return function(store) + return Promise.new(function() + -- We currently cannot batch request user avatar thumbnails, + -- so each thumbnailRequest has to be processed individually. + + local fetchedPromises = {} + for _, thumbnailRequest in pairs(thumbnailRequests) do + table.insert(fetchedPromises, + fetchThumbnailsBatch(networkImpl, userIds, thumbnailRequest):andThen(function(result) + for _, data in pairs(result) do + store:dispatch(SetUserThumbnail(data.id, data.image, data.thumbnailType, data.thumbnailSize)) + end + end) + ) + end + + return Promise.all(fetchedPromises) + end) + end +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiSendGameInvite.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiSendGameInvite.lua new file mode 100644 index 0000000..4f0fe01 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiSendGameInvite.lua @@ -0,0 +1,42 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local AppTempCommon = CorePackages.AppTempCommon +local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests + +local ChatSendMessage = require(Requests.ChatSendMessage) +local ChatStartOneToOneConversation = require(Requests.ChatStartOneToOneConversation) +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +local trimCharacterFromEndString = require(AppTempCommon.Temp.trimCharacterFromEndString) + +local INVITE_MESSAGE = "Come join me in %s %s/games/%s" + +return function(networkImpl, userId, placeInfo) + local clientId = Players.LocalPlayer.UserId + + local trimmedUrl = trimCharacterFromEndString(Url.BASE_URL, "/") + -- Construct the invite message based on place info + local messageText = string.format(INVITE_MESSAGE, + placeInfo.name, trimmedUrl, placeInfo.universeRootPlaceId + ) + + return function(store) + return ChatStartOneToOneConversation(networkImpl, userId, clientId):andThen(function(result) + local conversation = result.responseBody.conversation + + return ChatSendMessage(networkImpl, conversation.id, messageText):andThen(function(result) + local data = result.responseBody + local wasModerated = data.resultType ~= "Success" + if wasModerated then + warn("Game invite was moderated") + end + return { + conversationId = conversation.id, + placeId = placeInfo.universeRootPlaceId, + wasModerated = wasModerated + } + end) + end) + end +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/memoize.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/memoize.lua new file mode 100644 index 0000000..261dcd0 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/memoize.lua @@ -0,0 +1,68 @@ +--[[ + memoize creates a function as a wrapper that caches the last outputs of a function. + This is useful if you know that the function should return the same output every + time it is run with the same inputs. The function should only return an output, and + not have any side effects. These side effects are not cached. + + Without memoize's caching, even though the function ouputs the same values, the + memory locations of the values are different; tables made in the function, even if + they have the same values, won't be the same tables. + + memoize only caches the last set of inputs and ouputs. This means that it is only + helpful when the function is likely to be called with the same inputs multiple + times in a row. This is the case with most Roact use cases. + + Note that memoize only does a ** shallow check on table inputs ** . This means + that if the same table is input but the elements of the table are different then + it will be assumed that the table has not changed. + + In addition to all the previous warnings, memoize strips trailing nils. This means + that if foo is a memoized function and we call foo(), then foo(nil) will return a + cached value. This is opposed to how print handles input. print() only outputs a + new line, but print(nil) outputs "nil". This is because varargs can detect the + number of arguments passed in. So, be careful when using memoize with varargs. + Trailing nils will be stripped. + + The wrapper can take any number of inputs and give any number of outputs. + Leading and interspersed nils are handled gracefully. Trailing nils on the input + are stripped. +]] +local function captureSize(...) + return {...}, select("#", ...) +end + +local function memoize(func) + assert(type(func) == "function", "memoize requires a function to memoize") + + local lastArgs + local lastNumArgs + local lastOutput + local lastNumOutput + + return function(...) + local numArgs = select("#", ...) + + while numArgs > 0 and select(numArgs, ...) == nil do + numArgs = numArgs - 1 + end + + if numArgs ~= lastNumArgs then + lastArgs = {...} + lastNumArgs = numArgs + lastOutput, lastNumOutput = captureSize(func(...)) + return unpack(lastOutput, 1, lastNumOutput) + end + + for i = 1, lastNumArgs do + if select(i, ...) ~= lastArgs[i] then + lastArgs = {...} + lastOutput, lastNumOutput = captureSize(func(...)) + break + end + end + + return unpack(lastOutput, 1, lastNumOutput) + end +end + +return memoize \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaApp/memoize.spec.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/memoize.spec.lua new file mode 100644 index 0000000..5ae2b6e --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaApp/memoize.spec.lua @@ -0,0 +1,213 @@ +return function() + local memoize = require(script.Parent.memoize) + + describe("memoize", function() + it("should handle arity 0", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + expect(identity()).to.equal(nil) + expect(identity(nil)).to.equal(nil) + expect(identity(nil, nil)).to.equal(nil) + expect(callCount).to.equal(1) + end) + + it("should handle arity 1", function() + local callCount = 0 + local identity = memoize(function(a) + callCount = callCount + 1 + return a + end) + + expect(identity(5)).to.equal(5) + expect(identity(5)).to.equal(5) + expect(callCount).to.equal(1) + + expect(identity(6)).to.equal(6) + expect(callCount).to.equal(2) + + expect(identity(5)).to.equal(5) + expect(callCount).to.equal(3) + end) + + it("should handle arity 2", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + expect(callCount).to.equal(1) + + a, b = identity(6, 5) + expect(a).to.equal(6) + expect(b).to.equal(5) + + expect(callCount).to.equal(2) + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + expect(callCount).to.equal(3) + end) + + it("should handle mixed arity", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + expect(callCount).to.equal(1) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b = identity() + expect(a).to.equal(nil) + expect(b).to.equal(nil) + + a, b = identity() + expect(a).to.equal(nil) + expect(b).to.equal(nil) + + expect(callCount).to.equal(3) + end) + + it("should handle trailing nils", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(5, nil) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + expect(callCount).to.equal(1) + + a, b = identity(7) + expect(a).to.equal(7) + expect(b).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + expect(callCount).to.equal(3) + end) + + it("should handle leading nils", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(nil, 7) + expect(a).to.equal(nil) + expect(b).to.equal(7) + + a, b = identity(nil, 7) + expect(a).to.equal(nil) + expect(b).to.equal(7) + + expect(callCount).to.equal(1) + + a, b = identity(7) + expect(a).to.equal(7) + expect(b).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b = identity(nil, 7) + expect(a).to.equal(nil) + expect(b).to.equal(7) + + expect(callCount).to.equal(3) + end) + + it("should handle interspersed nils", function() + local callCount = 0 + local identity = memoize(function(a, b, c, d) + callCount = callCount + 1 + return a, b, c, d + end) + + local a, b, c, d + + a, b, c, d = identity(7, nil, 7, nil) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(7) + expect(d).to.equal(nil) + + -- Trailing nils can affect how interspersed nils are handled + a, b, c, d = identity(7, nil, 7) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(7) + expect(d).to.equal(nil) + + expect(callCount).to.equal(1) + + a, b, c, d = identity(7, nil, nil, nil) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(nil) + expect(d).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b, c, d = identity(7, nil, 7, nil) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(7) + expect(d).to.equal(nil) + + expect(callCount).to.equal(3) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedConversation.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedConversation.lua new file mode 100644 index 0000000..4d63e47 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedConversation.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CorePackages").AppTempCommon +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(convo) + return { + conversation = convo, + } +end) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedMultiplePlaceInfos.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedMultiplePlaceInfos.lua new file mode 100644 index 0000000..2bb1bb1 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedMultiplePlaceInfos.lua @@ -0,0 +1,10 @@ +local Modules = game:GetService("CorePackages").AppTempCommon + +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(placeInfos) + return { + placeInfos = placeInfos, + } +end) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedUserPresence.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedUserPresence.lua new file mode 100644 index 0000000..42da515 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedUserPresence.lua @@ -0,0 +1,12 @@ +local Modules = game:GetService("CorePackages").AppTempCommon + +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(userId, presence, lastLocation, placeId) + return { + userId = userId, + presence = presence, + lastLocation = lastLocation, + placeId = placeId, + } +end) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Models/PlaceInfoModel.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Models/PlaceInfoModel.lua new file mode 100644 index 0000000..103305a --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Models/PlaceInfoModel.lua @@ -0,0 +1,39 @@ +local CorePackages = game:GetService("CorePackages") + +local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId) + +local PlaceInfoModel = {} + +function PlaceInfoModel.new() + local self = {} + + return self +end + +function PlaceInfoModel.mock() + local self = PlaceInfoModel.new() + + self.builder = "builder" + self.builderId = MockId() + self.description = "description" + self.imageToken = MockId() + self.isPlayable = true + self.name = "name" + self.placeId = MockId() + self.price = 0 + self.reasonProhibited = nil + self.universeId = MockId() + self.universeRootPlaceId = MockId() + self.url = "url" + + return self +end + +function PlaceInfoModel.fromWeb(data) + local self = data or {} + self.placeId = tostring(self.placeId) + + return self +end + +return PlaceInfoModel \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.lua new file mode 100644 index 0000000..4a9ab03 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.lua @@ -0,0 +1,22 @@ +local CorePackages = game:GetService("CorePackages") + +local Common = CorePackages.AppTempCommon.Common +local LuaChat = CorePackages.AppTempCommon.LuaChat + +local ReceivedMultiplePlaceInfos = require(LuaChat.Actions.ReceivedMultiplePlaceInfos) + +local Immutable = require(Common.Immutable) + +return function(state, action) + state = state or {} + if action.type == ReceivedMultiplePlaceInfos.name then + + local newInfos = {} + for _, placeInfo in ipairs(action.placeInfos) do + newInfos[placeInfo.placeId] = placeInfo + end + + state = Immutable.JoinDictionaries(state, newInfos) + end + return state +end diff --git a/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.spec.lua b/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.spec.lua new file mode 100644 index 0000000..075534e --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.spec.lua @@ -0,0 +1,36 @@ +return function() + local CoreGui = game:GetService("CoreGui") + local Modules = CoreGui.RobloxGui.Modules + local LuaApp = Modules.LuaApp + local LuaChat = Modules.LuaChat + + local MockId = require(LuaApp.MockId) + local ReceivedMultiplePlaceInfos = require(LuaChat.Actions.ReceivedMultiplePlaceInfos) + + local PlaceInfosReducer = require(script.Parent.PlaceInfos) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = PlaceInfosReducer(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("ReceivedMultiplePlaceInfos", function() + it("should add place info to the store", function() + local state = PlaceInfosReducer(nil, {}) + + local placeId = MockId() + local returnedPlaceInfo = ReceivedMultiplePlaceInfos({ + { + placeId = placeId, + imageToken = "image-token", + }, + }) + + state = PlaceInfosReducer(state, returnedPlaceInfo) + + expect(state[placeId]).to.equal(returnedPlaceInfo.placeInfos[1]) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/Temp/EventStream.lua b/Client2018/content/LuaPackages/AppTempCommon/Temp/EventStream.lua new file mode 100644 index 0000000..05cb39b --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/Temp/EventStream.lua @@ -0,0 +1,79 @@ +local AnalyticsService = game:GetService("AnalyticsService") +local RunService = game:GetService("RunService") +local UserInputService = game:GetService("UserInputService") + +local SETTINGS_HUB_INVITE_RELEASE_STREAM_TIME = tonumber(settings():GetFVariable("SettingsHubInviteReleaseStreamTime")) + or math.huge + +local function getPlatformTarget() + local platformTarget = "unknownLua" + local platformEnum = Enum.Platform.None + + -- the call to GetPlatform is wrapped in a pcall() because the Testing Service + -- executes the scripts in the wrong authorization level + pcall(function() + platformEnum = UserInputService:GetPlatform() + end) + + -- bucket the platform based on consumer platform + local isDesktopClient = (platformEnum == Enum.Platform.Windows) or (platformEnum == Enum.Platform.OSX) + + local isMobileClient = (platformEnum == Enum.Platform.IOS) or (platformEnum == Enum.Platform.Android) + isMobileClient = isMobileClient or (platformEnum == Enum.Platform.UWP) + + local isConsole = (platformEnum == Enum.Platform.XBox360) or (platformEnum == Enum.Platform.XBoxOne) + isConsole = isConsole or (platformEnum == Enum.Platform.PS3) or (platformEnum == Enum.Platform.PS4) + isConsole = isConsole or (platformEnum == Enum.Platform.WiiU) + + -- assign a target based on the form factor + if isDesktopClient then + platformTarget = "client" + elseif isMobileClient then + platformTarget = "mobile" + elseif isConsole then + platformTarget = "console" + else + -- if we don't have a name for the form factor, report it here so that we can eventually track it down + platformTarget = platformTarget .. tostring(platformEnum) + end + + return platformTarget +end + +local EventStream = {} +EventStream.__index = EventStream + +function EventStream.new(overridePlatformTarget, overrideAnalyticsImpl) + local self = {} + setmetatable(self, EventStream) + + self._analyticsImpl = overrideAnalyticsImpl or AnalyticsService + self._platformTarget = overridePlatformTarget or getPlatformTarget() + + return self +end + +function EventStream:setRBXEventStream(eventContext, eventName, additionalArgs) + additionalArgs = additionalArgs or {} + -- this function sends reports to the server in batches, not real-time + self._analyticsImpl:SetRBXEventStream(self._platformTarget, eventContext, eventName, additionalArgs) + + if not self.timerSteppedConnection then + local lastGameTime = time() + self.timerSteppedConnection = RunService.Stepped:Connect(function(gameTime) + if gameTime - lastGameTime > SETTINGS_HUB_INVITE_RELEASE_STREAM_TIME then + self:releaseRBXEventStream() + end + end) + end +end + +function EventStream:releaseRBXEventStream() + self._analyticsImpl:ReleaseRBXEventStream(self._platformTarget) + if self.timerSteppedConnection then + self.timerSteppedConnection:Disconnect() + self.timerSteppedConnection = nil + end +end + +return EventStream \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/Temp/httpRequest.lua b/Client2018/content/LuaPackages/AppTempCommon/Temp/httpRequest.lua new file mode 100644 index 0000000..42b855d --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/Temp/httpRequest.lua @@ -0,0 +1,90 @@ +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local LuaApp = CorePackages.AppTempCommon.LuaApp + +local Promise = require(LuaApp.Promise) + +local DEFAULT_THROTTLING_PRIORITY = Enum.ThrottlingPriority.Extreme +local DEFAULT_POST_ASYNC_CONTENT_TYPE = Enum.HttpContentType.ApplicationJson + +-- httpRequest : (table, optional) an object that implements the same http functions as the data model +return function(httpImpl) + + local function doHttpPost(url, options) + assert(options.postBody, "Expected a postBody to be specified with this request") + assert(type(options.postBody) == "string", "Expected postBody to be a string") + + if not options.contentType then + options.contentType = DEFAULT_POST_ASYNC_CONTENT_TYPE + end + + if not options.throttlingPriority then + options.throttlingPriority = DEFAULT_THROTTLING_PRIORITY + end + + return function() + return httpImpl:PostAsyncFullUrl( + url, + options.postBody, + options.throttlingPriority, + options.contentType + ) + end + end + + local function doHttpGet(url) + return function() + return httpImpl:GetAsyncFullUrl(url, DEFAULT_THROTTLING_PRIORITY) + end + end + + -- return the request function + -- url : (string) + -- requestMethod : (string) "GET", "POST" + -- args : (table, optional) + -- options.throttlingPriority : (Enum.ThrottlingPriority, optional) + -- options.contentType : (Enum.HttpContentType, optional) + -- options.postBody : (string, optional ("POST" only)) + -- RETURNS : (promise) + return function(url, requestMethod, options) + assert(type(url) == "string", "Expected url to be a string") + assert(type(requestMethod) == "string", "Expected requestMethod to be a string") + assert(not options or type(options) == "table", "Expected options to be a table") + requestMethod = string.upper(requestMethod) + + local httpFunction + if requestMethod == "POST" then + httpFunction = doHttpPost(url, options) + elseif requestMethod == "GET" then + httpFunction = doHttpGet(url) + else + error(string.format("Unsupported requestMethod : %s", requestMethod or "nil")) + end + + return Promise.new(function(resolve, reject) + if httpFunction then + spawn(function() + local success, response = pcall(httpFunction) + + if success then + local jsonSuccess, decodedJson = pcall(function() + return HttpService:JSONDecode(response) + end) + if jsonSuccess then + resolve({ + responseBody = decodedJson, + }) + else + reject(decodedJson) + end + else + reject(response) + end + end) + else + reject() + end + end) + end +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/Temp/httpRequest.spec.lua b/Client2018/content/LuaPackages/AppTempCommon/Temp/httpRequest.spec.lua new file mode 100644 index 0000000..fed3d32 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/Temp/httpRequest.spec.lua @@ -0,0 +1,61 @@ +return function() + local httpRequest = require(script.Parent.httpRequest) + + local function createTestRequestFunc(testResponse) + local requestService = {} + function requestService:GetAsyncFullUrl() + return testResponse + end + function requestService:PostAsyncFullUrl() + return testResponse + end + + return httpRequest(requestService) + end + + it("should return a function", function() + expect(httpRequest()).to.be.ok() + expect(type(httpRequest())).to.equal("function") + end) + + it("should validate its inputs", function() + local testRequest = createTestRequestFunc() + local function testParams(url, requestMethod, args) + return function() + testRequest(url, requestMethod, args) + end + end + + local validUrl = "friends.roblox.com" + local validMethod = "GET" + local validArgs = {} + + -- url checks + expect(testParams(nil, validMethod, validArgs)).to.throw() + expect(testParams(123, validMethod, validArgs)).to.throw() + expect(testParams({}, validMethod, validArgs)).to.throw() + expect(testParams(true, validMethod, validArgs)).to.throw() + expect(testParams(function() end, validMethod, validArgs)).to.throw() + + -- request method checks + expect(testParams(validUrl, nil, validArgs)).to.throw() + expect(testParams(validUrl, 123, validArgs)).to.throw() + expect(testParams(validUrl, {}, validArgs)).to.throw() + expect(testParams(validUrl, true, validArgs)).to.throw() + expect(testParams(validUrl, function() end, validArgs)).to.throw() + + -- args checks + expect(testParams(validUrl, validMethod, 123)).to.throw() + expect(testParams(validUrl, validMethod, "Test")).to.throw() + expect(testParams(validUrl, validMethod, true)).to.throw() + expect(testParams(validUrl, validMethod, function() end)).to.throw() + end) + + it("should throw an error if the requestMethod isn't supported", function() + local testRequest = createTestRequestFunc("foo") + + expect(function() + testRequest("testUrl", "GIVEANDTAKE") + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.lua b/Client2018/content/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.lua new file mode 100644 index 0000000..3a0d69f --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.lua @@ -0,0 +1,16 @@ +return function(targetString, blacklistedCharacter) + local charactersArray = {} + local indexArray = {} + for index, byte in utf8.codes(targetString) do + local graphemeCharacter = utf8.char(byte) + table.insert(charactersArray, 1, graphemeCharacter) + table.insert(indexArray, 1, index) + end + for index, graphemeCharacter in ipairs(charactersArray) do + if graphemeCharacter ~= blacklistedCharacter then + return targetString:sub(1, indexArray[index]) + end + end + + return "" +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.spec.lua b/Client2018/content/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.spec.lua new file mode 100644 index 0000000..a08d217 --- /dev/null +++ b/Client2018/content/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.spec.lua @@ -0,0 +1,71 @@ +return function() + local trimCharacterFromEndString = require(script.Parent.trimCharacterFromEndString) + + describe("single byte characters", function() + it("should not trim a string if it does not end with passed character", function() + local passedString = "testing" + local passedCharacter = "/" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(passedString) + end) + + it("should trim a string if it ends with a single instance of the passed character", function() + local passedString = "testing/" + local passedCharacter = "/" + local expectedString = "testing" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + + it("should trim a string if it ends with multiple instances of the passed character", function() + local passedString = "testing///" + local passedCharacter = "/" + local expectedString = "testing" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + + it("should do nothing if the passed character is empty", function() + local passedString = "hunter2" + local passedCharacter = "" + local expectedString = "hunter2" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + end) + + describe("multiple byte characters", function() + it("should not trim a string if it does not end with passed character", function() + local passedString = "testing" + local passedCharacter = "🐶" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(passedString) + end) + + it("should trim a string if it ends with a single instance of the passed character", function() + local passedString = "testing🐶" + local passedCharacter = "🐶" + local expectedString = "testing" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + + it("should trim a string if it ends with multiple instances of the passed character", function() + local passedString = "testing🐶🐶🐶" + local passedCharacter = "🐶" + local expectedString = "testing" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + end) + + describe("a string with all blacklisted characters", function() + it("should return a empty string", function() + local passedString = "pppppppppppp" + local passedCharacter = "p" + local expectedString = "" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/Roact.lua b/Client2018/content/LuaPackages/Roact.lua new file mode 100644 index 0000000..5b48405 --- /dev/null +++ b/Client2018/content/LuaPackages/Roact.lua @@ -0,0 +1,7 @@ +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.RoactImpl) + +return require(CorePackages.RoactImpl) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/Change.lua b/Client2018/content/LuaPackages/RoactImpl/Change.lua new file mode 100644 index 0000000..b7d259c --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/Change.lua @@ -0,0 +1,36 @@ +--[[ + Change is used to generate special prop keys that can be used to connect to + GetPropertyChangedSignal. + + Generally, Change is indexed by a Roblox property name: + + Roact.createElement("TextBox", { + [Roact.Change.Text] = function(rbx) + print("The TextBox", rbx, "changed text to", rbx.Text) + end, + }) +]] + +local Change = {} + +local changeMetatable = { + __tostring = function(self) + return ("ChangeListener(%s)"):format(self.name) + end +} + +setmetatable(Change, { + __index = function(self, propertyName) + local changeListener = { + type = Change, + name = propertyName + } + + setmetatable(changeListener, changeMetatable) + Change[propertyName] = changeListener + + return changeListener + end, +}) + +return Change \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/Change.spec.lua b/Client2018/content/LuaPackages/RoactImpl/Change.spec.lua new file mode 100644 index 0000000..154539a --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/Change.spec.lua @@ -0,0 +1,15 @@ +return function() + local Change = require(script.Parent.Change) + + it("should yield change listener objects when indexed", function() + expect(Change.Text).to.be.ok() + expect(Change.Selected).to.be.ok() + end) + + it("should yield the same object when indexed again", function() + local a = Change.Text + local b = Change.Text + + expect(a).to.equal(b) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/Component.lua b/Client2018/content/LuaPackages/RoactImpl/Component.lua new file mode 100644 index 0000000..ca28cd8 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/Component.lua @@ -0,0 +1,435 @@ +--[[ + The base implementation of a stateful component in Roact. + + Stateful components handle most of their own mounting and reconciliation + process. Many of the private methods here are invoked by the reconciler. + + Stateful components expose a handful of lifecycle events: + - didMount + - willUnmount + - willUpdate + - didUpdate + - (static) getDerivedStateFromProps + + These lifecycle events line up with their semantics in React, and more + information (and a diagram) is available in the Roact documentation. +]] + +local Reconciler = require(script.Parent.Reconciler) +local Core = require(script.Parent.Core) +local GlobalConfig = require(script.Parent.GlobalConfig) +local Instrumentation = require(script.Parent.Instrumentation) + +local invalidSetStateMessages = require(script.Parent.invalidSetStateMessages) + +local Component = {} + +-- Locally cache tick so we can minimize impact of calling it for instrumentation +local tick = tick + +Component.__index = Component + +--[[ + Merge any number of dictionaries into a new dictionary, overwriting keys. + + If a value of `Core.None` is encountered, the key will be removed instead. + This is necessary because Lua doesn't differentiate between a key being + missing and a key being set to nil. +]] +local function merge(...) + local result = {} + + for i = 1, select("#", ...) do + local entry = select(i, ...) + + for key, value in pairs(entry) do + if value == Core.None then + result[key] = nil + else + result[key] = value + end + end + end + + return result +end + +--[[ + Create a new stateful component. + + Not intended to be a general OO implementation, this function only intends + to let users extend Component and PureComponent. + + Instead of using inheritance, use composition and props to extend + components. +]] +function Component:extend(name) + assert(type(name) == "string", "A name must be provided to create a Roact Component") + + local class = {} + + for key, value in pairs(self) do + -- We don't want users using 'extend' to create component inheritance + -- see https://reactjs.org/docs/composition-vs-inheritance.html + if key ~= "extend" then + class[key] = value + end + end + + class.__index = class + + setmetatable(class, { + __tostring = function(self) + return name + end + }) + + function class._new(props, context) + local self = {} + + -- When set to a value, setState will fail, using the given reason to + -- create a detailed error message. + -- You can see a list of reasons in invalidSetStateMessages. + self._setStateBlockedReason = nil + + if class.defaultProps == nil then + self.props = props + else + self.props = merge(class.defaultProps, props) + end + + self._context = {} + + -- Shallow copy all context values from our parent element. + if context then + for key, value in pairs(context) do + self._context[key] = value + end + end + + setmetatable(self, class) + + -- Call the user-provided initializer, where state and _props are set. + if class.init then + self._setStateBlockedReason = "init" + class.init(self, props) + self._setStateBlockedReason = nil + end + + -- The user constructer might not set state, so we can. + if not self.state then + self.state = {} + end + + if class.getDerivedStateFromProps then + local partialState = class.getDerivedStateFromProps(props, self.state) + + if partialState then + self.state = merge(self.state, partialState) + end + end + + return self + end + + return class +end + +--[[ + render is intended to describe what a UI should look like at the current + point in time. + + The default implementation throws an error, since forgetting to define + render is usually a mistake. + + The simplest implementation for render is: + + function MyComponent:render() + return nil + end + + You should explicitly return nil from functions in Lua to avoid edge cases + related to none versus nil. +]] +function Component:render() + local message = ( + "The component %q is missing the 'render' method.\n" .. + "render must be defined when creating a Roact component!" + ):format( + tostring(getmetatable(self)) + ) + + error(message, 0) +end + +--[[ + Used to tell Roact whether this component *might* need to be re-rendered + given a new set of props and state. + + This method is an escape hatch for when the Roact element creation and + reconciliation algorithms are not fast enough for specific cases. Poorly + written shouldUpdate methods *will* cause hard-to-trace bugs. + + If you're thinking of writing a shouldUpdate function, consider using + PureComponent instead, which provides a good implementation given that your + data is immutable. + + This function must be faster than the render method in order to be a + performance improvement. +]] +function Component:shouldUpdate(newProps, newState) + return true +end + +--[[ + Applies new state to the component. + + partialState may be one of two things: + - A table, which will be merged onto the current state. + - A function, returning a table to merge onto the current state. + + The table variant generally looks like: + + self:setState({ + foo = "bar", + }) + + The function variant generally looks like: + + self:setState(function(prevState, props) + return { + foo = prevState.count + 1, + }) + end) + + The function variant may also return nil in the callback, which allows Roact + to cancel updating state and abort the render. + + Future versions of Roact will potentially batch or delay state merging, so + any state updates that depend on the current state should use the function + variant. +]] +function Component:setState(partialState) + -- If setState was disabled, we should check for a detailed message and + -- display it. + if self._setStateBlockedReason ~= nil then + local messageSource = invalidSetStateMessages[self._setStateBlockedReason] + + if messageSource == nil then + messageSource = invalidSetStateMessages["default"] + end + + -- We assume that each message has a formatting placeholder for a component name. + local formattedMessage = string.format(messageSource, tostring(getmetatable(self))) + + error(formattedMessage, 2) + end + + -- If the partial state is a function, invoke it to get the actual partial state. + if type(partialState) == "function" then + partialState = partialState(self.state, self.props) + + -- If partialState is nil, abort the render. + if partialState == nil then + return + end + end + + local newState = merge(self.state, partialState) + self:_update(nil, newState) +end + +--[[ + Returns the current stack trace for this component, or nil if the + elementTracing configuration flag is set to false. +]] +function Component:getElementTraceback() + return self._handle._element.source +end + +--[[ + Notifies the component that new props and state are available. This function + is invoked by the reconciler. + + If shouldUpdate returns true, this method will trigger a re-render and + reconciliation step. +]] +function Component:_update(newProps, newState) + self._setStateBlockedReason = "shouldUpdate" + + local doUpdate + if GlobalConfig.getValue("componentInstrumentation") then + local startTime = tick() + + doUpdate = self:shouldUpdate(newProps or self.props, newState or self.state) + + local elapsed = tick() - startTime + Instrumentation.logShouldUpdate(self._handle, doUpdate, elapsed) + else + doUpdate = self:shouldUpdate(newProps or self.props, newState or self.state) + end + + self._setStateBlockedReason = nil + + if doUpdate then + self:_forceUpdate(newProps, newState) + end +end + +--[[ + Forces the component to re-render itself and its children. + + This is essentially the inner portion of _update. + + newProps and newState are optional. +]] +function Component:_forceUpdate(newProps, newState) + -- Compute new derived state. + -- Get the class - getDerivedStateFromProps is static. + local class = getmetatable(self) + + -- If newProps are passed, compute derived state and default props + if newProps then + if class.getDerivedStateFromProps then + local derivedState = class.getDerivedStateFromProps(newProps, newState or self.state) + + -- getDerivedStateFromProps can return nil if no changes are necessary. + if derivedState ~= nil then + newState = merge(newState or self.state, derivedState) + end + end + + if class.defaultProps then + -- We only allocate another prop table if there are props that are + -- falling back to their default. + local replacementProps + + for key in pairs(class.defaultProps) do + if newProps[key] == nil then + replacementProps = merge(class.defaultProps, newProps) + break + end + end + + if replacementProps then + newProps = replacementProps + end + end + end + + if self.willUpdate then + self._setStateBlockedReason = "willUpdate" + self:willUpdate(newProps or self.props, newState or self.state) + self._setStateBlockedReason = nil + end + + local oldProps = self.props + local oldState = self.state + + if newProps then + self.props = newProps + end + + if newState then + self.state = newState + end + + self._setStateBlockedReason = "render" + + local newChildElement + if GlobalConfig.getValue("componentInstrumentation") then + local startTime = tick() + + newChildElement = self:render() + + local elapsed = tick() - startTime + Instrumentation.logRenderTime(self._handle, elapsed) + else + newChildElement = self:render() + end + + self._setStateBlockedReason = nil + + self._setStateBlockedReason = "reconcile" + if self._handle._child ~= nil then + -- We returned an element during our last render, update it. + self._handle._child = Reconciler._reconcileInternal( + self._handle._child, + newChildElement + ) + elseif newChildElement then + -- We returned nil during our last render, construct a new child. + self._handle._child = Reconciler._mountInternal( + newChildElement, + self._handle._parent, + self._handle._key, + self._context + ) + end + self._setStateBlockedReason = nil + + if self.didUpdate then + self:didUpdate(oldProps, oldState) + end +end + +--[[ + Initializes the component instance and attaches it to the given + instance handle, created by Reconciler._mount. +]] +function Component:_mount(handle) + self._handle = handle + + self._setStateBlockedReason = "render" + + local virtualTree + if GlobalConfig.getValue("componentInstrumentation") then + local startTime = tick() + + virtualTree = self:render() + + local elapsed = tick() - startTime + Instrumentation.logRenderTime(self._handle, elapsed) + else + virtualTree = self:render() + end + + self._setStateBlockedReason = nil + + if virtualTree then + self._setStateBlockedReason = "reconcile" + handle._child = Reconciler._mountInternal( + virtualTree, + handle._parent, + handle._key, + self._context + ) + self._setStateBlockedReason = nil + end + + if self.didMount then + self:didMount() + end +end + +--[[ + Destructs the component and invokes all necessary lifecycle methods. +]] +function Component:_unmount() + local handle = self._handle + + if self.willUnmount then + self._setStateBlockedReason = "willUnmount" + self:willUnmount() + self._setStateBlockedReason = nil + end + + -- Stateful components can return nil from render() + if handle._child then + Reconciler.unmount(handle._child) + end + + self._handle = nil +end + +return Component diff --git a/Client2018/content/LuaPackages/RoactImpl/Component.spec.lua b/Client2018/content/LuaPackages/RoactImpl/Component.spec.lua new file mode 100644 index 0000000..d7f7bb1 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/Component.spec.lua @@ -0,0 +1,615 @@ +return function() + local Core = require(script.Parent.Core) + local createElement = require(script.Parent.createElement) + local Reconciler = require(script.Parent.Reconciler) + local GlobalConfig = require(script.Parent.GlobalConfig) + + local Component = require(script.Parent.Component) + + it("should be extendable", function() + local MyComponent = Component:extend("The Senate") + + expect(MyComponent).to.be.ok() + expect(MyComponent._new).to.be.ok() + end) + + it("should prevent extending a user component", function() + local MyComponent = Component:extend("Sheev") + + expect(function() + MyComponent:extend("Frank") + end).to.throw() + end) + + it("should use a given name", function() + local MyComponent = Component:extend("FooBar") + + local name = tostring(MyComponent) + + expect(name).to.be.a("string") + expect(name:find("FooBar")).to.be.ok() + end) + + it("should throw on render with a useful message by default", function() + local MyComponent = Component:extend("Foo") + + local instance = MyComponent._new({}) + + expect(instance).to.be.ok() + + local ok, err = pcall(function() + instance:render() + end) + + expect(ok).to.equal(false) + expect(err:find("Foo")).to.be.ok() + end) + + it("should pass props to the initializer", function() + local MyComponent = Component:extend("Wazo") + + local callCount = 0 + local testProps = {} + + function MyComponent:init(props) + expect(props).to.equal(testProps) + callCount = callCount + 1 + end + + MyComponent._new(testProps) + + expect(callCount).to.equal(1) + end) + + it("should fire didMount and willUnmount when reified", function() + local MyComponent = Component:extend("MyComponent") + local mounts = 0 + local unmounts = 0 + + function MyComponent:render() + return nil + end + + function MyComponent:didMount() + mounts = mounts + 1 + end + + function MyComponent:willUnmount() + unmounts = unmounts + 1 + end + + expect(mounts).to.equal(0) + expect(unmounts).to.equal(0) + + local instance = Reconciler.mount(createElement(MyComponent)) + + expect(mounts).to.equal(1) + expect(unmounts).to.equal(0) + + Reconciler.unmount(instance) + + expect(mounts).to.equal(1) + expect(unmounts).to.equal(1) + end) + + it("should provide the proper arguments to willUpdate and didUpdate", function() + local willUpdateCount = 0 + local didUpdateCount = 0 + local prevProps + local prevState + local nextProps + local nextState + local setValue + + local Child = Component:extend("PureChild") + + function Child:willUpdate(newProps, newState) + nextProps = assert(newProps) + nextState = assert(newState) + prevProps = assert(self.props) + prevState = assert(self.state) + willUpdateCount = willUpdateCount + 1 + end + + function Child:didUpdate(oldProps, oldState) + assert(oldProps) + assert(oldState) + expect(prevProps.value).to.equal(oldProps.value) + expect(prevState.value).to.equal(oldState.value) + expect(nextProps.value).to.equal(self.props.value) + expect(nextState.value).to.equal(self.state.value) + didUpdateCount = didUpdateCount + 1 + end + + function Child:render() + return nil + end + + local Container = Component:extend("Container") + + function Container:init() + self.state = { + value = 0, + } + end + + function Container:didMount() + setValue = function(value) + self:setState({ + value = value, + }) + end + end + + function Container:willUnmount() + setValue = nil + end + + function Container:render() + return createElement(Child, { + value = self.state.value, + }) + end + + local element = createElement(Container) + local instance = Reconciler.mount(element) + + expect(willUpdateCount).to.equal(0) + expect(didUpdateCount).to.equal(0) + + setValue(1) + + expect(willUpdateCount).to.equal(1) + expect(didUpdateCount).to.equal(1) + + setValue(1) + + expect(willUpdateCount).to.equal(2) + expect(didUpdateCount).to.equal(2) + + setValue(2) + + expect(willUpdateCount).to.equal(3) + expect(didUpdateCount).to.equal(3) + + setValue(1) + + expect(willUpdateCount).to.equal(4) + expect(didUpdateCount).to.equal(4) + + Reconciler.unmount(instance) + end) + + it("should call getDerivedStateFromProps appropriately", function() + local TestComponent = Component:extend("TestComponent") + local getStateCallback + + function TestComponent.getDerivedStateFromProps(newProps, oldState) + return { + visible = newProps.visible + } + end + + function TestComponent:init(props) + self.state = { + visible = false + } + + getStateCallback = function() + return self.state + end + end + + function TestComponent:render() end + + local handle = Reconciler.mount(createElement(TestComponent, { + visible = true + })) + + local state = getStateCallback() + expect(state.visible).to.equal(true) + + handle = Reconciler.reconcile(handle, createElement(TestComponent, { + visible = 123 + })) + + state = getStateCallback() + expect(state.visible).to.equal(123) + + Reconciler.unmount(handle) + end) + + it("should pull values from defaultProps where appropriate", function() + local lastProps + local TestComponent = Component:extend("TestComponent") + + TestComponent.defaultProps = { + foo = "hello", + bar = "world", + } + + function TestComponent:render() + lastProps = self.props + return nil + end + + local handle = Reconciler.mount(createElement(TestComponent)) + + expect(lastProps).to.be.a("table") + expect(lastProps.foo).to.equal("hello") + expect(lastProps.bar).to.equal("world") + + Reconciler.unmount(handle) + + lastProps = nil + handle = Reconciler.mount(createElement(TestComponent, { + foo = 5, + })) + + expect(lastProps).to.be.a("table") + expect(lastProps.foo).to.equal(5) + expect(lastProps.bar).to.equal("world") + + Reconciler.unmount(handle) + + lastProps = nil + handle = Reconciler.mount(createElement(TestComponent, { + bar = false, + })) + + expect(lastProps).to.be.a("table") + expect(lastProps.foo).to.equal("hello") + expect(lastProps.bar).to.equal(false) + + Reconciler.unmount(handle) + end) + + it("should fall back to defaultProps correctly after an update", function() + local lastProps + local TestComponent = Component:extend("TestComponent") + + TestComponent.defaultProps = { + foo = "hello", + bar = "world", + } + + function TestComponent:render() + lastProps = self.props + return nil + end + + local handle = Reconciler.mount(createElement(TestComponent, { + foo = "hey" + })) + + expect(lastProps).to.be.a("table") + expect(lastProps.foo).to.equal("hey") + expect(lastProps.bar).to.equal("world") + + handle = Reconciler.reconcile(handle, createElement(TestComponent)) + + expect(lastProps).to.be.a("table") + expect(lastProps.foo).to.equal("hello") + expect(lastProps.bar).to.equal("world") + + Reconciler.unmount(handle) + end) + + describe("setState", function() + it("should throw when called in init", function() + local InitComponent = Component:extend("InitComponent") + + function InitComponent:init() + self:setState({ + a = 1 + }) + end + + function InitComponent:render() + return nil + end + + local initElement = createElement(InitComponent) + + expect(function() + Reconciler.mount(initElement) + end).to.throw() + end) + + it("should throw when called in render", function() + local RenderComponent = Component:extend("RenderComponent") + + function RenderComponent:render() + self:setState({ + a = 1 + }) + end + + local renderElement = createElement(RenderComponent) + + expect(function() + Reconciler.mount(renderElement) + end).to.throw() + end) + + it("should throw when called in shouldUpdate", function() + local TestComponent = Component:extend("TestComponent") + + local triggerTest + + function TestComponent:init() + triggerTest = function() + self:setState({ + a = 1 + }) + end + end + + function TestComponent:render() + return nil + end + + function TestComponent:shouldUpdate() + self:setState({ + a = 1 + }) + end + + local testElement = createElement(TestComponent) + + expect(function() + Reconciler.mount(testElement) + triggerTest() + end).to.throw() + end) + + it("should throw when called in willUpdate", function() + local TestComponent = Component:extend("TestComponent") + local forceUpdate + + function TestComponent:init() + forceUpdate = function() + self:_forceUpdate() + end + end + + function TestComponent:render() + return nil + end + + function TestComponent:willUpdate() + self:setState({ + a = 1 + }) + end + + local testElement = createElement(TestComponent) + + expect(function() + Reconciler.mount(testElement) + forceUpdate() + end).to.throw() + end) + + it("should throw when called in willUnmount", function() + local TestComponent = Component:extend("TestComponent") + + function TestComponent:render() + return nil + end + + function TestComponent:willUnmount() + self:setState({ + a = 1 + }) + end + + local element = createElement(TestComponent) + local instance = Reconciler.mount(element) + + expect(function() + Reconciler.unmount(instance) + end).to.throw() + end) + + it("should remove values from state when the value is Core.None", function() + local TestComponent = Component:extend("TestComponent") + local setStateCallback, getStateCallback + + function TestComponent:init() + setStateCallback = function(newState) + self:setState(newState) + end + + getStateCallback = function() + return self.state + end + + self.state = { + value = 0 + } + end + + function TestComponent:render() + return nil + end + + local element = createElement(TestComponent) + local instance = Reconciler.mount(element) + + expect(getStateCallback().value).to.equal(0) + + setStateCallback({ + value = Core.None + }) + + expect(getStateCallback().value).to.equal(nil) + + Reconciler.unmount(instance) + end) + + it("should invoke functions to compute a partial state", function() + local TestComponent = Component:extend("TestComponent") + local setStateCallback, getStateCallback, getPropsCallback + + function TestComponent:init() + setStateCallback = function(newState) + self:setState(newState) + end + + getStateCallback = function() + return self.state + end + + getPropsCallback = function() + return self.props + end + + self.state = { + value = 0 + } + end + + function TestComponent:render() + return nil + end + + local element = createElement(TestComponent) + local instance = Reconciler.mount(element) + + expect(getStateCallback().value).to.equal(0) + + setStateCallback(function(state, props) + expect(state).to.equal(getStateCallback()) + expect(props).to.equal(getPropsCallback()) + + return { + value = state.value + 1 + } + end) + + expect(getStateCallback().value).to.equal(1) + + Reconciler.unmount(instance) + end) + + it("should cancel rendering if the function returns nil", function() + local TestComponent = Component:extend("TestComponent") + local setStateCallback + local renderCount = 0 + + function TestComponent:init() + setStateCallback = function(newState) + self:setState(newState) + end + + self.state = { + value = 0 + } + end + + function TestComponent:render() + renderCount = renderCount + 1 + return nil + end + + local element = createElement(TestComponent) + local instance = Reconciler.mount(element) + expect(renderCount).to.equal(1) + + setStateCallback(function(state, props) + return nil + end) + + expect(renderCount).to.equal(1) + + Reconciler.unmount(instance) + end) + + it("should not call getDerivedStateFromProps on setState", function() + local TestComponent = Component:extend("TestComponent") + local setStateCallback + local getDerivedStateFromPropsCount = 0 + + function TestComponent:init() + setStateCallback = function(newState) + self:setState(newState) + end + + self.state = { + value = 0 + } + end + + function TestComponent:render() + return nil + end + + function TestComponent.getDerivedStateFromProps(nextProps, lastState) + getDerivedStateFromPropsCount = getDerivedStateFromPropsCount + 1 + end + + local element = createElement(TestComponent, { + someProp = 1, + }) + + local instance = Reconciler.mount(element) + expect(getDerivedStateFromPropsCount).to.equal(1) + + setStateCallback({ + value = 1, + }) + expect(getDerivedStateFromPropsCount).to.equal(1) + + + Reconciler.unmount(instance) + end) + end) + + describe("getElementTraceback", function() + it("should return stack traces", function() + local stackTraceCallback = nil + + GlobalConfig.set({ + elementTracing = true + }) + + local TestComponent = Component:extend("TestComponent") + + function TestComponent:init() + stackTraceCallback = function() + return self:getElementTraceback() + end + end + + function TestComponent:render() + return createElement("StringValue") + end + + local handle = Reconciler.mount(createElement(TestComponent)) + expect(stackTraceCallback()).to.be.ok() + Reconciler.unmount(handle) + GlobalConfig.reset() + end) + + it("should return nil when elementTracing is off", function() + local stackTraceCallback = nil + + local TestComponent = Component:extend("TestComponent") + + function TestComponent:init() + stackTraceCallback = function() + return self:getElementTraceback() + end + end + + function TestComponent:render() + return createElement("StringValue") + end + + local handle = Reconciler.mount(createElement(TestComponent)) + expect(stackTraceCallback()).to.never.be.ok() + Reconciler.unmount(handle) + end) + end) +end diff --git a/Client2018/content/LuaPackages/RoactImpl/Config.lua b/Client2018/content/LuaPackages/RoactImpl/Config.lua new file mode 100644 index 0000000..ced1fc6 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/Config.lua @@ -0,0 +1,146 @@ +--[[ + Exposes an interface to set global configuration values for Roact. + + Configuration can only occur once, and should only be done by an application + using Roact, not a library. + + Any keys that aren't recognized will cause errors. Configuration is only + intended for configuring Roact itself, not extensions or libraries. + + Configuration is expected to be set immediately after loading Roact. Setting + configuration values after an application starts may produce unpredictable + behavior. +]] + +-- Every valid configuration value should be non-nil in this table. +local defaultConfig = { + -- Enables storage of `debug.traceback()` values on elements for debugging. + ["elementTracing"] = false, + -- Enables instrumentation of shouldUpdate and render methods for Roact components + ["componentInstrumentation"] = false, +} + +-- Build a list of valid configuration values up for debug messages. +local defaultConfigKeys = {} +for key in pairs(defaultConfig) do + table.insert(defaultConfigKeys, key) +end + +--[[ + Merges two tables together into a new table. +]] +local function join(a, b) + local new = {} + + for key, value in pairs(a) do + new[key] = value + end + + for key, value in pairs(b) do + new[key] = value + end + + return new +end + +local Config = {} + +function Config.new() + local self = {} + + -- Once configuration has been set, we record a traceback. + -- That way, if the user mistakenly calls `set` twice, we can point to the + -- first place it was called. + self._lastConfigTraceback = nil + + self._currentConfig = defaultConfig + + -- We manually bind these methods here so that the Config's methods can be + -- used without passing in self, since they eventually get exposed on the + -- root Roact object. + self.set = function(...) + return Config.set(self, ...) + end + + self.getValue = function(...) + return Config.getValue(self, ...) + end + + self.reset = function(...) + return Config.reset(self, ...) + end + + return self +end + +function Config.set(self, configValues) + if self._lastConfigTraceback then + local message = ( + "Global configuration can only be set once. Configuration was already set at:%s" + ):format( + self._lastConfigTraceback + ) + + error(message, 3) + end + + -- We use 3 as our traceback and error level because all of the methods are + -- manually bound to 'self', which creates an additional stack frame we want + -- to skip through. + self._lastConfigTraceback = debug.traceback("", 3) + + -- Validate values without changing any configuration. + -- We only want to apply this configuration if it's valid! + for key, value in pairs(configValues) do + if defaultConfig[key] == nil then + local message = ( + "Invalid global configuration key %q (type %s). Valid configuration keys are: %s" + ):format( + tostring(key), + typeof(key), + table.concat(defaultConfigKeys, ", ") + ) + + error(message, 3) + end + + -- Right now, all configuration values must be boolean. + if typeof(value) ~= "boolean" then + local message = ( + "Invalid value %q (type %s) for global configuration key %q. Valid values are: true, false" + ):format( + tostring(value), + typeof(value), + tostring(key) + ) + + error(message, 3) + end + end + + -- Assign all of the (validated) configuration values in one go. + self._currentConfig = join(self._currentConfig, configValues) +end + +function Config.getValue(self, key) + if defaultConfig[key] == nil then + local message = ( + "Invalid global configuration key %q (type %s). Valid configuration keys are: %s" + ):format( + tostring(key), + typeof(key), + table.concat(defaultConfigKeys, ", ") + ) + + error(message, 3) + end + + return self._currentConfig[key] +end + +function Config.reset(self) + self._lastConfigTraceback = nil + self._currentConfig = defaultConfig +end + +return Config \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/Config.spec.lua b/Client2018/content/LuaPackages/RoactImpl/Config.spec.lua new file mode 100644 index 0000000..cec7076 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/Config.spec.lua @@ -0,0 +1,86 @@ +return function() + local Config = require(script.Parent.Config) + + it("should accept valid configuration", function() + local config = Config.new() + + expect(config.getValue("elementTracing")).to.equal(false) + + config.set({ + elementTracing = true, + }) + + expect(config.getValue("elementTracing")).to.equal(true) + end) + + it("should reject invalid configuration keys", function() + local config = Config.new() + + local badKey = "garblegoop" + + local ok, err = pcall(function() + config.set({ + [badKey] = true, + }) + end) + + expect(ok).to.equal(false) + + -- The error should mention our bad key somewhere. + expect(err:find(badKey)).to.be.ok() + end) + + it("should reject invalid configuration values", function() + local config = Config.new() + + local goodKey = "elementTracing" + local badValue = "Hello there!" + + local ok, err = pcall(function() + config.set({ + [goodKey] = badValue, + }) + end) + + expect(ok).to.equal(false) + + -- The error should mention both our key and value + expect(err:find(goodKey)).to.be.ok() + expect(err:find(badValue)).to.be.ok() + end) + + it("should prevent setting configuration more than once", function() + local config = Config.new() + + -- We're going to use the name of this function to see if the traceback + -- was correct. + local function setEmptyConfig() + config.set({}) + end + + setEmptyConfig() + + local ok, err = pcall(setEmptyConfig) + + expect(ok).to.equal(false) + + -- The error should mention the stack trace with the original set call. + expect(err:find("setEmptyConfig")).to.be.ok() + end) + + it("should reset to default values after invoking reset()", function() + local config = Config.new() + + expect(config.getValue("elementTracing")).to.equal(false) + + config.set({ + elementTracing = true, + }) + + expect(config.getValue("elementTracing")).to.equal(true) + + config.reset() + + expect(config.getValue("elementTracing")).to.equal(false) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/Core.lua b/Client2018/content/LuaPackages/RoactImpl/Core.lua new file mode 100644 index 0000000..76d9658 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/Core.lua @@ -0,0 +1,24 @@ +--[[ + Provides a set of markers used for annotating data in Roact. +]] + +local Symbol = require(script.Parent.Symbol) + +local Core = {} + +-- Marker used to specify children of a node. +Core.Children = Symbol.named("Children") + +-- Marker used to specify a callback to receive the underlying Roblox object. +Core.Ref = Symbol.named("Ref") + +-- Marker used to specify that a component is a Roact Portal. +Core.Portal = Symbol.named("Portal") + +-- Marker used to specify that the value is nothing, because nil cannot be stored in tables. +Core.None = Symbol.named("None") + +-- Marker used to specify that the table it is present within is a component. +Core.Element = Symbol.named("Element") + +return Core \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/Event.lua b/Client2018/content/LuaPackages/RoactImpl/Event.lua new file mode 100644 index 0000000..499e56f --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/Event.lua @@ -0,0 +1,39 @@ +--[[ + Index into 'Event' to get a prop key for attaching to an event on a + Roblox Instance. + + Example: + + Roact.createElement("TextButton", { + Text = "Hello, world!", + + [Roact.Event.MouseButton1Click] = function(rbx) + print("Clicked", rbx) + end + }) +]] + +local Event = {} + +local eventMetatable = { + __tostring = function(self) + return ("Event(%s)"):format(self.name) + end +} + +setmetatable(Event, { + __index = function(self, eventName) + local event = { + type = Event, + name = eventName + } + + setmetatable(event, eventMetatable) + + Event[eventName] = event + + return event + end +}) + +return Event \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/Event.spec.lua b/Client2018/content/LuaPackages/RoactImpl/Event.spec.lua new file mode 100644 index 0000000..6c1283b --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/Event.spec.lua @@ -0,0 +1,15 @@ +return function() + local Event = require(script.Parent.Event) + + it("should yield event objects when indexed", function() + expect(Event.MouseButton1Click).to.be.ok() + expect(Event.Touched).to.be.ok() + end) + + it("should yield the same object when indexed again", function() + local a = Event.MouseButton1Click + local b = Event.MouseButton1Click + + expect(a).to.equal(b) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/GlobalConfig.lua b/Client2018/content/LuaPackages/RoactImpl/GlobalConfig.lua new file mode 100644 index 0000000..3219835 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/GlobalConfig.lua @@ -0,0 +1,7 @@ +--[[ + Exposes a single instance of a configuration as Roact's GlobalConfig. +]] + +local Config = require(script.Parent.Config) + +return Config.new() \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/GlobalConfig.spec.lua b/Client2018/content/LuaPackages/RoactImpl/GlobalConfig.spec.lua new file mode 100644 index 0000000..5974c79 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/GlobalConfig.spec.lua @@ -0,0 +1,10 @@ +return function() + local GlobalConfig = require(script.Parent.GlobalConfig) + + it("should have the correct methods", function() + expect(GlobalConfig).to.be.ok() + expect(GlobalConfig.set).to.be.ok() + expect(GlobalConfig.getValue).to.be.ok() + expect(GlobalConfig.reset).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/Instrumentation.lua b/Client2018/content/LuaPackages/RoactImpl/Instrumentation.lua new file mode 100644 index 0000000..771860d --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/Instrumentation.lua @@ -0,0 +1,104 @@ +--[[ + An optional instrumentation layer that the reconciler calls into to record + various events. + + Tracks a number of stats, including: + Recorded stats: + - Render count by component + - Update request count by component + - Actual update count by component + - shouldUpdate returned true count by component + - Time taken to run shouldUpdate + - Time taken to render by component + Derivable stats (for profiling manually or with a future tool): + - Average render time by component + - Percent of total render time by component + - Percent of time shouldUpdate returns true + - Average shouldUpdate time by component + - Percent of total shouldUpdate time by component +]] + +local Instrumentation = {} + +local componentStats = {} + +--[[ + Determines name of component from the given instance handle and returns a + stat object from the componentStats table, generating a new one if needed +]] +local function getStatEntry(handle) + local name + if handle and handle._element and handle._element.component then + name = tostring(handle._element.component) + else + warn("Component name not valid for " .. tostring(handle._key)) + return nil + end + local entry = componentStats[name] + if not entry then + entry = { + -- update requests + updateReqCount = 0, + -- actual updates + didUpdateCount = 0, + -- time spent in shouldUpdate + shouldUpdateTime = 0, + -- number of renders + renderCount = 0, + -- total render time spent + renderTime = 0, + } + componentStats[name] = entry + end + + return entry +end + +--[[ + Logs the time taken and resulting value of a Component's shouldUpdate function +]] +function Instrumentation.logShouldUpdate(handle, updateNeeded, shouldUpdateTime) + -- Grab or create associated entry in stats table + local statEntry = getStatEntry(handle) + if statEntry then + -- Increment the total number of times update was invoked + statEntry.updateReqCount = statEntry.updateReqCount + 1 + + -- Increment (when applicable) total number of times shouldUpdate returned true + statEntry.didUpdateCount = statEntry.didUpdateCount + (updateNeeded and 1 or 0) + + -- Add time spent checking if an update is needed (in millis) to total time + statEntry.shouldUpdateTime = statEntry.shouldUpdateTime + shouldUpdateTime * 1000 + end +end + +--[[ + Logs the time taken value of a Component's render function +]] +function Instrumentation.logRenderTime(handle, renderTime) + -- Grab or create associated entry in stats table + local statEntry = getStatEntry(handle) + if statEntry then + -- Increment total render count + statEntry.renderCount = statEntry.renderCount + 1 + + -- Add render time (in millis) to total rendering time + statEntry.renderTime = statEntry.renderTime + renderTime * 1000 + end +end + +--[[ + Clears all the stats collected thus far. Useful for testing and for profiling in the future +]] +function Instrumentation.clearCollectedStats() + componentStats = {} +end + +--[[ + Returns all the stats collected thus far. Useful for testing and for profiling in the future +]] +function Instrumentation.getCollectedStats() + return componentStats +end + +return Instrumentation \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/Instrumentation.spec.lua b/Client2018/content/LuaPackages/RoactImpl/Instrumentation.spec.lua new file mode 100644 index 0000000..a3af4f7 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/Instrumentation.spec.lua @@ -0,0 +1,98 @@ +return function() + local Component = require(script.Parent.PureComponent) + local GlobalConfig = require(script.Parent.GlobalConfig) + local Reconciler = require(script.Parent.Reconciler) + local createElement = require(script.Parent.createElement) + + local Instrumentation = require(script.Parent.Instrumentation) + + it("should count and time renders when enabled", function() + GlobalConfig.set({ + ["componentInstrumentation"] = true, + }) + local triggerUpdate + + local TestComponent = Component:extend("TestComponent") + function TestComponent:init() + self.state = { + value = 0 + } + end + + function TestComponent:render() + return nil + end + + function TestComponent:didMount() + triggerUpdate = function() + self:setState({ + value = self.state.value + 1 + }) + end + end + + local instance = Reconciler.mount(createElement(TestComponent)) + + local stats = Instrumentation.getCollectedStats() + expect(stats.TestComponent).to.be.ok() + expect(stats.TestComponent.renderCount).to.equal(1) + + triggerUpdate() + expect(stats.TestComponent.renderCount).to.equal(2) + + Reconciler.unmount(instance) + Instrumentation.clearCollectedStats() + GlobalConfig.reset() + end) + + it("should count and time shouldUpdate calls when enabled", function() + GlobalConfig.set({ + ["componentInstrumentation"] = true, + }) + local triggerUpdate + local willDoUpdate = false + + local TestComponent = Component:extend("TestComponent") + + function TestComponent:init() + self.state = { + value = 0, + } + end + + function TestComponent:shouldUpdate() + return willDoUpdate + end + + function TestComponent:didMount() + triggerUpdate = function() + self:setState({ + value = self.state.value + 1, + }) + end + end + + function TestComponent:render() end + + local instance = Reconciler.mount(createElement(TestComponent)) + + local stats = Instrumentation.getCollectedStats() + + willDoUpdate = true + triggerUpdate() + + expect(stats.TestComponent).to.be.ok() + expect(stats.TestComponent.updateReqCount).to.equal(1) + expect(stats.TestComponent.didUpdateCount).to.equal(1) + + willDoUpdate = false + triggerUpdate() + + expect(stats.TestComponent.updateReqCount).to.equal(2) + expect(stats.TestComponent.didUpdateCount).to.equal(1) + + Reconciler.unmount(instance) + Instrumentation.clearCollectedStats() + GlobalConfig.reset() + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/PureComponent.lua b/Client2018/content/LuaPackages/RoactImpl/PureComponent.lua new file mode 100644 index 0000000..0283298 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/PureComponent.lua @@ -0,0 +1,41 @@ +--[[ + A version of Component with a `shouldUpdate` method that forces the + resulting component to be pure. +]] + +local Component = require(script.Parent.Component) + +local PureComponent = Component:extend("PureComponent") + +-- When extend()ing a component, you don't get an extend method. +-- This is to promote composition over inheritance. +-- PureComponent is an exception to this rule. +PureComponent.extend = Component.extend + +function PureComponent:shouldUpdate(newProps, newState) + -- In a vast majority of cases, if state updated, something has updated. + -- We don't bother checking in this case. + if newState ~= self.state then + return true + end + + if newProps == self.props then + return false + end + + for key, value in pairs(newProps) do + if self.props[key] ~= value then + return true + end + end + + for key, value in pairs(self.props) do + if newProps[key] ~= value then + return true + end + end + + return false +end + +return PureComponent \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/PureComponent.spec.lua b/Client2018/content/LuaPackages/RoactImpl/PureComponent.spec.lua new file mode 100644 index 0000000..b0732c7 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/PureComponent.spec.lua @@ -0,0 +1,71 @@ +return function() + local createElement = require(script.Parent.createElement) + local Reconciler = require(script.Parent.Reconciler) + + local PureComponent = require(script.Parent.PureComponent) + + it("should be extendable", function() + local MyComponent = PureComponent:extend("MyComponent") + + expect(MyComponent).to.be.ok() + end) + + it("should skip updates for shallow-equal props", function() + local updateCount = 0 + local setValue + + local PureChild = PureComponent:extend("PureChild") + + function PureChild:willUpdate(newProps, newState) + updateCount = updateCount + 1 + end + + function PureChild:render() + end + + local PureContainer = PureComponent:extend("PureContainer") + + function PureContainer:init() + self.state = { + value = 0, + } + end + + function PureContainer:didMount() + setValue = function(value) + self:setState({ + value = value, + }) + end + end + + function PureContainer:render() + return createElement(PureChild, { + value = self.state.value, + }) + end + + local element = createElement(PureContainer) + local instance = Reconciler.mount(element) + + expect(updateCount).to.equal(0) + + setValue(1) + + expect(updateCount).to.equal(1) + + setValue(1) + + expect(updateCount).to.equal(1) + + setValue(2) + + expect(updateCount).to.equal(2) + + setValue(1) + + expect(updateCount).to.equal(3) + + Reconciler.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/Reconciler.lua b/Client2018/content/LuaPackages/RoactImpl/Reconciler.lua new file mode 100644 index 0000000..66e50ad --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/Reconciler.lua @@ -0,0 +1,543 @@ +--[[ + The reconciler uses the virtual DOM generated by components to create a real + tree of Roblox instances. + + The reonciler has three basic operations: + * mount (previously reify) + * reconcile + * unmount (previously teardown) + + Mounting is the process of creating new components. This is first + triggered when the user calls `Roact.mount` on an element. This is where the + structure of the component tree is built, later used and modified by the + reconciliation and unmounting steps. + + Reconciliation accepts an existing concrete instance tree (created by mount) + along with a new element that describes the desired tree. The reconciler + will do the minimum amount of work required to update tree's components to + match the new element, sometimes invoking mount to create new branches. + + Unmounting destructs for the tree. It will crawl through the tree, + destroying nodes from the bottom up. + + Much of the reconciler's work is done by Component, which is the base for + all stateful components in Roact. Components can trigger reconciliation (and + implicitly, unmounting) via state updates that come with their own caveats. +]] + +local Core = require(script.Parent.Core) +local Event = require(script.Parent.Event) +local Change = require(script.Parent.Change) +local getDefaultPropertyValue = require(script.Parent.getDefaultPropertyValue) +local SingleEventManager = require(script.Parent.SingleEventManager) +local Symbol = require(script.Parent.Symbol) + +local isInstanceHandle = Symbol.named("isInstanceHandle") + +local DEFAULT_SOURCE = "\n\t\n" + +local function isPortal(element) + if type(element) ~= "table" then + return false + end + + return element.component == Core.Portal +end + +--[[ + Sets the value of a reference to a new rendered object. + Correctly handles both function-style and object-style refs. +]] +local function applyRef(ref, newRbx) + if ref == nil then + return + end + + if type(ref) == "table" then + ref.current = newRbx + else + ref(newRbx) + end +end + +local Reconciler = {} + +Reconciler._singleEventManager = SingleEventManager.new() + +--[[ + Is this element backed by a Roblox instance directly? +]] +local function isPrimitiveElement(element) + if type(element) ~= "table" then + return false + end + + return type(element.component) == "string" +end + +--[[ + Is this element defined by a pure function? +]] +local function isFunctionalElement(element) + if type(element) ~= "table" then + return false + end + + return type(element.component) == "function" +end + +--[[ + Is this element defined by a component class? +]] +local function isStatefulElement(element) + if type(element) ~= "table" then + return false + end + + return type(element.component) == "table" +end + +--[[ + Destroy the given Roact instance, all of its descendants, and associated + Roblox instances owned by the components. +]] +function Reconciler.unmount(instanceHandle) + local element = instanceHandle._element + + if isPrimitiveElement(element) then + -- We're destroying a Roblox Instance-based object + + -- Kill refs before we make changes, since any mutations past this point + -- aren't relevant to components. + applyRef(element.props[Core.Ref], nil) + + for _, child in pairs(instanceHandle._children) do + Reconciler.unmount(child) + end + + -- Necessary to make sure SingleEventManager doesn't leak references + Reconciler._singleEventManager:disconnectAll(instanceHandle._rbx) + + instanceHandle._rbx:Destroy() + elseif isFunctionalElement(element) then + -- Functional components can return nil + if instanceHandle._child then + Reconciler.unmount(instanceHandle._child) + end + elseif isStatefulElement(element) then + instanceHandle._instance:_unmount() + elseif isPortal(element) then + for _, child in pairs(instanceHandle._children) do + Reconciler.unmount(child) + end + else + error(("Cannot unmount invalid Roact instance %q"):format(tostring(element))) + end +end + +--[[ + Public interface to reifier. Hides parameters used when recursing down the + component tree. +]] +function Reconciler.mount(element, parent, key) + return Reconciler._mountInternal(element, parent, key) +end + +--[[ + Instantiates components to represent the given element. + + Parameters: + - `element`: The element to mount. + - `parent`: The Roblox object to contain the contained instances + - `key`: The Name to give the Roblox instance that gets created + - `context`: Used to pass Roact context values down the tree + + The structure created by this method is important to the functionality of + the reconciliation methods; they depend on this structure being well-formed. +]] +function Reconciler._mountInternal(element, parent, key, context) + if isPrimitiveElement(element) then + -- Primitive elements are backed directly by Roblox Instances. + + local rbx = Instance.new(element.component) + + -- Update Roblox properties + for key, value in pairs(element.props) do + Reconciler._setRbxProp(rbx, key, value, element) + end + + -- Create children! + local children = {} + + if element.props[Core.Children] then + for key, childElement in pairs(element.props[Core.Children]) do + local childInstance = Reconciler._mountInternal(childElement, rbx, key, context) + + children[key] = childInstance + end + end + + -- This name can be passed through multiple components. + -- Elements with the same key will be treated as the same + -- element between reconciles; the old element will be + -- reconciled to the new element with the same key. + if key then + rbx.Name = key + end + + rbx.Parent = parent + + -- Attach ref values, since the instance is initialized now. + applyRef(element.props[Core.Ref], rbx) + + return { + [isInstanceHandle] = true, + _key = key, + _parent = parent, + _element = element, + _context = context, + _children = children, + _rbx = rbx, + } + elseif isFunctionalElement(element) then + -- Functional elements contain 0 or 1 children. + + local instanceHandle = { + [isInstanceHandle] = true, + _key = key, + _parent = parent, + _element = element, + _context = context, + } + + local vdom = element.component(element.props) + if vdom then + instanceHandle._child = Reconciler._mountInternal(vdom, parent, key, context) + end + + return instanceHandle + elseif isStatefulElement(element) then + -- Stateful elements have 0 or 1 children, and also have a backing + -- instance that can keep state. + + -- We separate the instance's implementation from our handle to it. + local instanceHandle = { + [isInstanceHandle] = true, + _key = key, + _parent = parent, + _element = element, + _child = nil, + } + + local instance = element.component._new(element.props, context) + + instanceHandle._instance = instance + instance:_mount(instanceHandle) + + return instanceHandle + elseif isPortal(element) then + -- Portal elements have one or more children. + + local target = element.props.target + if not target then + error(("Cannot mount Portal without specifying a target."):format(tostring(element))) + elseif typeof(target) ~= "Instance" then + error(("Cannot mount Portal with target of type %q."):format(typeof(target))) + end + + -- Create children! + local children = {} + + if element.props[Core.Children] then + for key, childElement in pairs(element.props[Core.Children]) do + local childInstance = Reconciler._mountInternal(childElement, target, key, context) + + children[key] = childInstance + end + end + + return { + [isInstanceHandle] = true, + _key = key, + _parent = parent, + _element = element, + _context = context, + _children = children, + _rbx = target, + } + elseif typeof(element) == "boolean" then + -- Ignore booleans of either value + -- See https://github.com/Roblox/roact/issues/14 + return nil + end + + error(("Cannot mount invalid Roact element %q"):format(tostring(element))) +end + +--[[ + A public interface around _reconcileInternal +]] +function Reconciler.reconcile(instanceHandle, newElement) + if instanceHandle == nil or not instanceHandle[isInstanceHandle] then + local message = ( + "Bad argument #1 to Reconciler.reconcile, expected component instance handle, found %s" + ):format( + typeof(instanceHandle) + ) + + error(message, 2) + end + + return Reconciler._reconcileInternal(instanceHandle, newElement) +end + +--[[ + Applies the state given by newElement to an existing Roact instance. + + reconcile will return the instance that should be used. This instance can + be different than the one that was passed in. +]] +function Reconciler._reconcileInternal(instanceHandle, newElement) + local oldElement = instanceHandle._element + + -- Instance was deleted! + if not newElement then + Reconciler.unmount(instanceHandle) + + return nil + end + + -- If the element changes type, we assume its subtree will be substantially + -- different. This lets us skip comparisons of a large swath of nodes. + if oldElement.component ~= newElement.component then + local parent = instanceHandle._parent + local key = instanceHandle._key + + local context + if isStatefulElement(oldElement) then + context = instanceHandle._instance._context + else + context = instanceHandle._context + end + + Reconciler.unmount(instanceHandle) + + local newInstance = Reconciler._mountInternal(newElement, parent, key, context) + + return newInstance + end + + if isPrimitiveElement(newElement) then + -- Roblox Instance change + + local oldRef = oldElement.props[Core.Ref] + local newRef = newElement.props[Core.Ref] + + -- Change the ref in one pass before applying any changes. + -- Roact doesn't provide any guarantees with regards to the sequencing + -- between refs and other changes in the commit phase. + if newRef ~= oldRef then + applyRef(oldRef, nil) + applyRef(newRef, instanceHandle._rbx) + end + + -- Update properties and children of the Roblox object. + Reconciler._reconcilePrimitiveProps(oldElement, newElement, instanceHandle._rbx) + Reconciler._reconcilePrimitiveChildren(instanceHandle, newElement) + + instanceHandle._element = newElement + + return instanceHandle + elseif isFunctionalElement(newElement) then + instanceHandle._element = newElement + + local rendered = newElement.component(newElement.props) + local newChild + + if instanceHandle._child then + -- Transition from tree to tree, even if 'rendered' is nil + newChild = Reconciler._reconcileInternal(instanceHandle._child, rendered) + elseif rendered then + -- Transition from nil to new tree + newChild = Reconciler._mountInternal( + rendered, + instanceHandle._parent, + instanceHandle._key, + instanceHandle._context + ) + end + + instanceHandle._child = newChild + + return instanceHandle + elseif isStatefulElement(newElement) then + instanceHandle._element = newElement + + -- Stateful elements can take care of themselves. + instanceHandle._instance:_update(newElement.props) + + return instanceHandle + elseif isPortal(newElement) then + if instanceHandle._rbx ~= newElement.props.target then + local parent = instanceHandle._parent + local key = instanceHandle._key + local context = instanceHandle._context + + Reconciler.unmount(instanceHandle) + + local newInstance = Reconciler._mountInternal(newElement, parent, key, context) + + return newInstance + end + + Reconciler._reconcilePrimitiveChildren(instanceHandle, newElement) + + instanceHandle._element = newElement + + return instanceHandle + end + + error(("Cannot reconcile to match invalid Roact element %q"):format(tostring(newElement))) +end + +--[[ + Reconciles the children of an existing Roact instance and the given element. +]] +function Reconciler._reconcilePrimitiveChildren(instance, newElement) + local elementChildren = newElement.props[Core.Children] + + -- Reconcile existing children that were changed or removed + for key, childInstance in pairs(instance._children) do + local childElement = elementChildren and elementChildren[key] + + childInstance = Reconciler._reconcileInternal(childInstance, childElement) + + instance._children[key] = childInstance + end + + -- Create children that were just added! + if elementChildren then + for key, childElement in pairs(elementChildren) do + -- Update if we didn't hit the child in the previous loop + if not instance._children[key] then + local childInstance = Reconciler._mountInternal(childElement, instance._rbx, key, instance._context) + instance._children[key] = childInstance + end + end + end +end + +--[[ + Reconciles the properties between two primitive Roact elements and applies + the differences to the given Roblox object. +]] +function Reconciler._reconcilePrimitiveProps(fromElement, toElement, rbx) + local seenProps = {} + + -- Set properties that were set with fromElement + for key, oldValue in pairs(fromElement.props) do + seenProps[key] = true + + local newValue = toElement.props[key] + + -- Assume any property that can be set to nil has a default value of nil + if newValue == nil then + local _, value = getDefaultPropertyValue(rbx.ClassName, key) + + -- We don't care if getDefaultPropertyValue fails, because + -- _setRbxProp will catch the error below. + newValue = value + end + + -- Roblox does this check for normal values, but we have special + -- properties like events that warrant this. + if oldValue ~= newValue then + Reconciler._setRbxProp(rbx, key, newValue, toElement) + end + end + + -- Set properties that are new in toElement + for key, newValue in pairs(toElement.props) do + if not seenProps[key] then + seenProps[key] = true + + local oldValue = fromElement.props[key] + + if oldValue ~= newValue then + Reconciler._setRbxProp(rbx, key, newValue, toElement) + end + end + end +end + +--[[ + Used in _setRbxProp to avoid creating a new closure for every property set. +]] +local function set(rbx, key, value) + rbx[key] = value +end + +--[[ + Sets a property on a Roblox object, following Roact's rules for special + case properties. + + This function can throw a couple different errors. In the future, calls to + _setRbxProp should be wrapped in a pcall to give better errors to the user. + + For that to be useful, we'll need to attach a 'source' property on every + element, created using debug.traceback(), that points to where the element + was created. +]] +function Reconciler._setRbxProp(rbx, key, value, element) + if type(key) == "string" then + -- Regular property + + local success, err = pcall(set, rbx, key, value) + + if not success then + local source = element.source or DEFAULT_SOURCE + + local message = ("Failed to set property %s on primitive instance of class %s\n%s\n%s"):format( + key, + rbx.ClassName, + err, + source + ) + + error(message, 0) + end + elseif type(key) == "table" then + -- Special property with extra data attached. + + if key.type == Event then + Reconciler._singleEventManager:connect(rbx, key.name, value) + elseif key.type == Change then + Reconciler._singleEventManager:connectProperty(rbx, key.name, value) + else + local source = element.source or DEFAULT_SOURCE + + -- luacheck: ignore 6 + local message = ("Failed to set special property on primitive instance of class %s\nInvalid special property type %q\n%s"):format( + rbx.ClassName, + tostring(key.type), + source + ) + + error(message, 0) + end + elseif type(key) ~= "userdata" then + -- Userdata values are special markers, usually created by Symbol + -- They have no data attached other than being unique keys + + local source = element.source or DEFAULT_SOURCE + + local message = ("Properties with a key type of %q are not supported\n%s"):format( + type(key), + source + ) + + error(message, 0) + end +end + +return Reconciler diff --git a/Client2018/content/LuaPackages/RoactImpl/Reconciler.spec.lua b/Client2018/content/LuaPackages/RoactImpl/Reconciler.spec.lua new file mode 100644 index 0000000..4ba75a9 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/Reconciler.spec.lua @@ -0,0 +1,88 @@ +return function() + local Core = require(script.Parent.Core) + local createRef = require(script.Parent.createRef) + local createElement = require(script.Parent.createElement) + + local Reconciler = require(script.Parent.Reconciler) + + it("should mount booleans as nil", function() + local booleanReified = Reconciler.mount(false) + expect(booleanReified).to.never.be.ok() + end) + + it("should handle object references properly", function() + local objectRef = createRef() + local element = createElement("StringValue", { + [Core.Ref] = objectRef, + }) + + local handle = Reconciler.mount(element) + expect(objectRef.current).to.be.ok() + Reconciler.unmount(handle) + expect(objectRef.current).to.never.be.ok() + end) + + it("should handle function references properly", function() + local currentRbx + + local function ref(rbx) + currentRbx = rbx + end + + local element = createElement("StringValue", { + [Core.Ref] = ref, + }) + + local handle = Reconciler.mount(element) + expect(currentRbx).to.be.ok() + Reconciler.unmount(handle) + expect(currentRbx).to.never.be.ok() + end) + + it("should handle changing function references", function() + local aValue, bValue + + local function aRef(rbx) + aValue = rbx + end + + local function bRef(rbx) + bValue = rbx + end + + local element = createElement("StringValue", { + [Core.Ref] = aRef, + }) + + local handle = Reconciler.mount(element, game, "Test123") + expect(aValue).to.be.ok() + expect(bValue).to.never.be.ok() + handle = Reconciler.reconcile(handle, createElement("StringValue", { + [Core.Ref] = bRef, + })) + expect(aValue).to.never.be.ok() + expect(bValue).to.be.ok() + Reconciler.unmount(handle) + expect(bValue).to.never.be.ok() + end) + + it("should handle changing object references", function() + local aRef = createRef() + local bRef = createRef() + + local element = createElement("StringValue", { + [Core.Ref] = aRef, + }) + + local handle = Reconciler.mount(element, game, "Test123") + expect(aRef.current).to.be.ok() + expect(bRef.current).to.never.be.ok() + handle = Reconciler.reconcile(handle, createElement("StringValue", { + [Core.Ref] = bRef, + })) + expect(aRef.current).to.never.be.ok() + expect(bRef.current).to.be.ok() + Reconciler.unmount(handle) + expect(bRef.current).to.never.be.ok() + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/ReconcilerCompat.lua b/Client2018/content/LuaPackages/RoactImpl/ReconcilerCompat.lua new file mode 100644 index 0000000..50149b8 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/ReconcilerCompat.lua @@ -0,0 +1,50 @@ +--[[ + Contains deprecated methods from Reconciler. Broken out so that removing + this shim is easy -- just delete this file and remove it from init. +]] + +local Reconciler = require(script.Parent.Reconciler) + +local warnedLocations = {} + +local reifyMessage = [[ +Roact.reify has been renamed to Roact.mount and will be removed in a future release. +Check the call to Roact.reify at: +]] + +local teardownMessage = [[ +Roact.teardown has been renamed to Roact.unmount and will be removed in a future release. +Check the call to Roact.teardown at: +]] + +local ReconcilerCompat = {} + +--[[ + Exposed as a method so that test cases can override `warn`. +]] +ReconcilerCompat._warn = warn + +local function warnOnce(message) + local trace = debug.traceback(message, 3) + if warnedLocations[trace] then + return + end + + warnedLocations[trace] = true + + ReconcilerCompat._warn(trace) +end + +function ReconcilerCompat.reify(...) + warnOnce(reifyMessage) + + return Reconciler.mount(...) +end + +function ReconcilerCompat.teardown(...) + warnOnce(teardownMessage) + + return Reconciler.unmount(...) +end + +return ReconcilerCompat \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/ReconcilerCompat.spec.lua b/Client2018/content/LuaPackages/RoactImpl/ReconcilerCompat.spec.lua new file mode 100644 index 0000000..4e3e3a1 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/ReconcilerCompat.spec.lua @@ -0,0 +1,61 @@ +return function() + local ReconcilerCompat = require(script.Parent.ReconcilerCompat) + local Reconciler = require(script.Parent.Reconciler) + local createElement = require(script.Parent.createElement) + + it("reify should only warn once per call site", function() + local callCount = 0 + local lastMessage + ReconcilerCompat._warn = function(message) + callCount = callCount + 1 + lastMessage = message + end + + -- We're using a loop so that we get the same stack trace and only one + -- warning hopefully. + for _ = 1, 2 do + local handle = ReconcilerCompat.reify(createElement("StringValue")) + Reconciler.unmount(handle) + end + + expect(callCount).to.equal(1) + expect(lastMessage:find("ReconcilerCompat.spec")).to.be.ok() + + -- This is a different call site, which should trigger another warning. + local handle = ReconcilerCompat.reify(createElement("StringValue")) + Reconciler.unmount(handle) + + expect(callCount).to.equal(2) + expect(lastMessage:find("ReconcilerCompat.spec")).to.be.ok() + + ReconcilerCompat._warn = warn + end) + + it("teardown should only warn once per call site", function() + local callCount = 0 + local lastMessage + ReconcilerCompat._warn = function(message) + callCount = callCount + 1 + lastMessage = message + end + + -- We're using a loop so that we get the same stack trace and only one + -- warning hopefully. + for _ = 1, 2 do + local handle = Reconciler.mount(createElement("StringValue")) + ReconcilerCompat.teardown(handle) + end + + expect(callCount).to.equal(1) + expect(lastMessage:find("ReconcilerCompat.spec")).to.be.ok() + + -- This is a different call site, which should trigger another warning. + local handle = Reconciler.mount(createElement("StringValue")) + ReconcilerCompat.teardown(handle) + + expect(callCount).to.equal(2) + expect(lastMessage:find("ReconcilerCompat.spec")).to.be.ok() + + ReconcilerCompat._warn = warn + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/SingleEventManager.lua b/Client2018/content/LuaPackages/RoactImpl/SingleEventManager.lua new file mode 100644 index 0000000..208e885 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/SingleEventManager.lua @@ -0,0 +1,145 @@ +--[[ + An interface to have one event listener at a time on an event. + + One listener can be registered per SingleEventManager/Instance/Event triple. + + For example: + + myManager:connect(myPart, "Touched", touchedListener) + myManager:connect(myPart, "Touched", otherTouchedListener) + + If myPart is touched, only `otherTouchedListener` will fire, because the + first listener was disconnected during the second connect call. + + The hooks provided by SingleEventManager pass the associated Roblox object + as the first parameter to the callback. This differs from normal + Roblox events. +]] + +local SingleEventManager = {} + +SingleEventManager.__index = SingleEventManager + +local function createHook(rbx, key, method) + local hook = { + method = method, + connection = rbx[key]:Connect(function(...) + method(rbx, ...) + end) + } + + return hook +end + +local function createChangeHook(rbx, key, method) + local hook = { + method = method, + connection = rbx:GetPropertyChangedSignal(key):Connect(function(...) + method(rbx, ...) + end) + } + + return hook +end + +local function formatChangeKey(key) + return ("!PropertyChangeEvent:%s"):format(key) +end + +function SingleEventManager.new() + local self = {} + + self._hookCache = {} + + setmetatable(self, SingleEventManager) + + return self +end + +function SingleEventManager:connect(rbx, key, method) + local rbxHooks = self._hookCache[rbx] + + if rbxHooks then + local existingHook = rbxHooks[key] + + if existingHook then + if existingHook.method == method then + return + end + + existingHook.connection:Disconnect() + end + + rbxHooks[key] = createHook(rbx, key, method) + else + rbxHooks = {} + rbxHooks[key] = createHook(rbx, key, method) + + self._hookCache[rbx] = rbxHooks + end +end + +function SingleEventManager:connectProperty(rbx, key, method) + local rbxHooks = self._hookCache[rbx] + local formattedKey = formatChangeKey(key) + + if rbxHooks then + local existingHook = rbxHooks[formattedKey] + + if existingHook then + if existingHook.method == method then + return + end + + existingHook.connection:Disconnect() + end + + rbxHooks[formattedKey] = createChangeHook(rbx, key, method) + else + rbxHooks = {} + rbxHooks[formattedKey] = createChangeHook(rbx, key, method) + + self._hookCache[rbx] = rbxHooks + end +end + +function SingleEventManager:disconnect(rbx, key) + local rbxHooks = self._hookCache[rbx] + + if not rbxHooks then + return + end + + local existingHook = rbxHooks[key] + + if not existingHook then + return + end + + existingHook.connection:Disconnect() + rbxHooks[key] = nil + + if next(rbxHooks) == nil then + self._hookCache[rbx] = nil + end +end + +function SingleEventManager:disconnectProperty(rbx, key) + self:disconnect(rbx, formatChangeKey(key)) +end + +function SingleEventManager:disconnectAll(rbx) + local rbxHooks = self._hookCache[rbx] + + if not rbxHooks then + return + end + + for _, hook in pairs(rbxHooks) do + hook.connection:Disconnect() + end + + self._hookCache[rbx] = nil +end + +return SingleEventManager diff --git a/Client2018/content/LuaPackages/RoactImpl/SingleEventManager.spec.lua b/Client2018/content/LuaPackages/RoactImpl/SingleEventManager.spec.lua new file mode 100644 index 0000000..9b84991 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/SingleEventManager.spec.lua @@ -0,0 +1,275 @@ +return function() + local SingleEventManager = require(script.Parent.SingleEventManager) + + describe("new", function() + it("should create a SingleEventManager", function() + local manager = SingleEventManager.new() + + expect(manager).to.be.ok() + end) + end) + + describe("connect", function() + it("should connect to events on an object", function() + local target = Instance.new("BindableEvent") + local manager = SingleEventManager.new() + + local callCount = 0 + + manager:connect(target, "Event", function(rbx, arg) + expect(rbx).to.equal(target) + expect(arg).to.equal("foo") + callCount = callCount + 1 + end) + + target:Fire("foo") + + expect(callCount).to.equal(1) + + target:Fire("foo") + + expect(callCount).to.equal(2) + end) + + it("should only connect one handler at a time", function() + local target = Instance.new("BindableEvent") + local manager = SingleEventManager.new() + + local callCountA = 0 + local callCountB = 0 + + manager:connect(target, "Event", function(rbx) + expect(rbx).to.equal(target) + callCountA = callCountA + 1 + end) + + manager:connect(target, "Event", function(rbx) + expect(rbx).to.equal(target) + callCountB = callCountB + 1 + end) + + target:Fire("foo") + + expect(callCountA).to.equal(0) + expect(callCountB).to.equal(1) + end) + + it("shouldn't conflate different event handlers", function() + local target = Instance.new("BindableEvent") + local manager = SingleEventManager.new() + + local callCountEvent = 0 + local callCountChanged = 0 + + manager:connect(target, "Event", function(rbx) + expect(rbx).to.equal(target) + callCountEvent = callCountEvent + 1 + end) + + manager:connect(target, "Changed", function(rbx) + expect(rbx).to.equal(target) + callCountChanged = callCountChanged + 1 + end) + + target:Fire() + + expect(callCountEvent).to.equal(1) + expect(callCountChanged).to.equal(0) + + target.Name = "unlimited power!" + + expect(callCountEvent).to.equal(1) + expect(callCountChanged).to.equal(1) + end) + end) + + describe("connectProperty", function() + it("should connect to property changes", function() + local target = Instance.new("BindableEvent") + local manager = SingleEventManager.new() + + local changeCount = 0 + + manager:connectProperty(target, "Name", function(rbx) + changeCount = changeCount + 1 + end) + + target.Name = "hi" + expect(changeCount).to.equal(1) + end) + + it("should disconnect the existing connection if present", function() + local target = Instance.new("IntValue") + local manager = SingleEventManager.new() + + local changeCountA = 0 + local changeCountB = 0 + + manager:connectProperty(target, "Name", function(rbx) + changeCountA = changeCountA + 1 + end) + + manager:connectProperty(target, "Name", function(rbx) + changeCountB = changeCountB + 1 + end) + + target.Name = "hi" + expect(changeCountA).to.equal(0) + expect(changeCountB).to.equal(1) + end) + + it("should only connect to the property specified", function() + local target = Instance.new("IntValue") + local manager = SingleEventManager.new() + + local changeCount = 0 + + manager:connectProperty(target, "Name", function(rbx) + changeCount = changeCount + 1 + end) + + target.Name = "hi" + target.Value = 0 + expect(changeCount).to.equal(1) + end) + end) + + describe("disconnect", function() + it("should disconnect handlers on an object", function() + local target = Instance.new("BindableEvent") + local manager = SingleEventManager.new() + + local callCount = 0 + + manager:connect(target, "Event", function(rbx) + expect(rbx).to.equal(target) + callCount = callCount + 1 + end) + + target:Fire() + + expect(callCount).to.equal(1) + + manager:disconnect(target, "Event") + + target:Fire() + + expect(callCount).to.equal(1) + end) + + it("should not disconnect unrelated connections", function() + local target = Instance.new("BindableEvent") + local manager = SingleEventManager.new() + + local callCountEvent = 0 + local callCountChanged = 0 + + manager:connect(target, "Event", function(rbx) + expect(rbx).to.equal(target) + callCountEvent = callCountEvent + 1 + end) + + manager:connect(target, "Changed", function(rbx) + expect(rbx).to.equal(target) + callCountChanged = callCountChanged + 1 + end) + + target:Fire() + target.Name = "bar" + + expect(callCountEvent).to.equal(1) + expect(callCountChanged).to.equal(1) + + manager:disconnect(target, "Event") + + target:Fire() + target.Name = "foo" + + expect(callCountEvent).to.equal(1) + expect(callCountChanged).to.equal(2) + end) + + it("should succeed with no events attached", function() + local manager = SingleEventManager.new() + local target = Instance.new("StringValue") + + manager:disconnect(target, "Event") + end) + end) + + describe("disconnectProperty", function() + it("should disconnect property change handlers on an object", function() + local target = Instance.new("IntValue") + local manager = SingleEventManager.new() + + local changeCount = 0 + + manager:connectProperty(target, "Name", function(rbx) + changeCount = changeCount + 1 + end) + + target.Name = "hi" + expect(changeCount).to.equal(1) + + manager:disconnectProperty(target, "Name") + target.Name = "test" + expect(changeCount).to.equal(1) + end) + + it("should succeed even if no handler is attached", function() + local target = Instance.new("IntValue") + local manager = SingleEventManager.new() + + manager:disconnectProperty(target, "Name") + end) + end) + + describe("disconnectAll", function() + it("should disconnect all listeners on an object", function() + local target = Instance.new("BindableEvent") + local manager = SingleEventManager.new() + + local callCountEvent = 0 + local callCountChanged = 0 + local changeCount = 0 + + manager:connect(target, "Event", function(rbx) + expect(rbx).to.equal(target) + callCountEvent = callCountEvent + 1 + end) + + manager:connect(target, "Changed", function(rbx) + expect(rbx).to.equal(target) + callCountChanged = callCountChanged + 1 + end) + + manager:connectProperty(target, "Name", function(rbx) + expect(rbx).to.equal(target) + changeCount = changeCount + 1 + end) + + target:Fire() + target.Name = "bar" + + expect(callCountEvent).to.equal(1) + expect(callCountChanged).to.equal(1) + expect(changeCount).to.equal(1) + + manager:disconnectAll(target) + + target:Fire() + target.Name = "foo" + + expect(callCountEvent).to.equal(1) + expect(callCountChanged).to.equal(1) + expect(changeCount).to.equal(1) + end) + + it("should succeed with no events attached", function() + local target = Instance.new("StringValue") + local manager = SingleEventManager.new() + + manager:disconnectAll(target) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/Symbol.lua b/Client2018/content/LuaPackages/RoactImpl/Symbol.lua new file mode 100644 index 0000000..d9e26d9 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/Symbol.lua @@ -0,0 +1,44 @@ +--[[ + A 'Symbol' is an opaque marker type. + + Symbols have the type 'userdata', but when printed to the console, the name + of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +--[[ + Create an unnamed Symbol. Usually, you should create a named Symbol using + Symbol.named(name) +]] +function Symbol.unnamed() + local self = newproxy(true) + + getmetatable(self).__tostring = function() + return "Unnamed Symbol" + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/Symbol.spec.lua b/Client2018/content/LuaPackages/RoactImpl/Symbol.spec.lua new file mode 100644 index 0000000..cde9be0 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/Symbol.spec.lua @@ -0,0 +1,45 @@ +return function() + local Symbol = require(script.Parent.Symbol) + + describe("named", function() + it("should give an opaque object", function() + local symbol = Symbol.named("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = Symbol.named("foo") + + expect(tostring(symbol):find("foo")).to.be.ok() + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.named("abc") + local symbolB = Symbol.named("abc") + + expect(symbolA).never.to.equal(symbolB) + end) + end) + + describe("unnamed", function() + it("should give an opaque object", function() + local symbol = Symbol.unnamed() + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to some string", function() + local symbol = Symbol.unnamed() + + expect(tostring(symbol)).to.be.a("string") + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.unnamed() + local symbolB = Symbol.unnamed() + + expect(symbolA).never.to.equal(symbolB) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/createElement.lua b/Client2018/content/LuaPackages/RoactImpl/createElement.lua new file mode 100644 index 0000000..48a56cb --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/createElement.lua @@ -0,0 +1,37 @@ +local Core = require(script.Parent.Core) +local GlobalConfig = require(script.Parent.GlobalConfig) + +--[[ + Creates a new Roact element of the given type. + + Does not create any concrete objects. +]] +local function createElement(elementType, props, children) + if elementType == nil then + error(("Expected elementType as an argument to createElement!"), 2) + end + + props = props or {} + + if children then + if props[Core.Children] ~= nil then + warn("props[Children] was defined but was overridden by third parameter to createElement!") + end + + props[Core.Children] = children + end + + local element = { + type = Core.Element, + component = elementType, + props = props, + } + + if GlobalConfig.getValue("elementTracing") then + element.source = ("\n%s\n"):format(debug.traceback()) + end + + return element +end + +return createElement \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/createElement.spec.lua b/Client2018/content/LuaPackages/RoactImpl/createElement.spec.lua new file mode 100644 index 0000000..82f4210 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/createElement.spec.lua @@ -0,0 +1,22 @@ +return function() + local createElement = require(script.Parent.createElement) + + it("should create new primitive elements", function() + local element = createElement("Frame") + + expect(element).to.be.ok() + end) + + it("should create new functional elements", function() + local element = createElement(function() + end) + + expect(element).to.be.ok() + end) + + it("should create new stateful components", function() + local element = createElement({}) + + expect(element).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/createRef.lua b/Client2018/content/LuaPackages/RoactImpl/createRef.lua new file mode 100644 index 0000000..bb3ae0b --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/createRef.lua @@ -0,0 +1,20 @@ +--[[ + Provides an API for acquiring a reference to a reified object. This + API is designed to mimic React 16.3's createRef API. + + See: + * https://reactjs.org/docs/refs-and-the-dom.html + * https://reactjs.org/blog/2018/03/29/react-v-16-3.html#createref-api +]] + +local refMetatable = { + __tostring = function(self) + return ("RoactReference(%s)"):format(tostring(self.current)) + end, +} + +return function() + return setmetatable({ + current = nil, + }, refMetatable) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/createRef.spec.lua b/Client2018/content/LuaPackages/RoactImpl/createRef.spec.lua new file mode 100644 index 0000000..e16115f --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/createRef.spec.lua @@ -0,0 +1,15 @@ +return function() + local createRef = require(script.Parent.createRef) + + it("should create refs", function() + expect(createRef()).to.be.ok() + end) + + it("should support tostring on refs", function() + local ref = createRef() + expect(tostring(ref)).to.equal("RoactReference(nil)") + + ref.current = "foo" + expect(tostring(ref)).to.equal("RoactReference(foo)") + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/getDefaultPropertyValue.lua b/Client2018/content/LuaPackages/RoactImpl/getDefaultPropertyValue.lua new file mode 100644 index 0000000..cb64976 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/getDefaultPropertyValue.lua @@ -0,0 +1,54 @@ +--[[ + Attempts to get the default value of a given property on a Roblox instance. + + This is used by the reconciler in cases where a prop was previously set on a + primitive component, but is no longer present in a component's new props. + + Eventually, Roblox might provide a nicer API to query the default property + of an object without constructing an instance of it. +]] + +local Symbol = require(script.Parent.Symbol) + +local Nil = Symbol.named("Nil") +local _cachedPropertyValues = {} + +local function getDefaultPropertyValue(className, propertyName) + local classCache = _cachedPropertyValues[className] + + if classCache then + local propValue = classCache[propertyName] + + -- We have to use a marker here, because Lua doesn't distinguish + -- between 'nil' and 'not in a table' + if propValue == Nil then + return true, nil + end + + if propValue ~= nil then + return true, propValue + end + else + classCache = {} + _cachedPropertyValues[className] = classCache + end + + local created = Instance.new(className) + local ok, defaultValue = pcall(function() + return created[propertyName] + end) + + created:Destroy() + + if ok then + if defaultValue == nil then + classCache[propertyName] = Nil + else + classCache[propertyName] = defaultValue + end + end + + return ok, defaultValue +end + +return getDefaultPropertyValue \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/getDefaultPropertyValue.spec.lua b/Client2018/content/LuaPackages/RoactImpl/getDefaultPropertyValue.spec.lua new file mode 100644 index 0000000..23df7d3 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/getDefaultPropertyValue.spec.lua @@ -0,0 +1,33 @@ +return function() + local getDefaultPropertyValue = require(script.Parent.getDefaultPropertyValue) + + it("should get default name string values", function() + local _, defaultName = getDefaultPropertyValue("StringValue", "Name") + + expect(defaultName).to.equal("Value") + end) + + it("should get default empty string values", function() + local _, defaultValue = getDefaultPropertyValue("StringValue", "Value") + + expect(defaultValue).to.equal("") + end) + + it("should get default number values", function() + local _, defaultValue = getDefaultPropertyValue("IntValue", "Value") + + expect(defaultValue).to.equal(0) + end) + + it("should get nil default values", function() + local _, defaultValue = getDefaultPropertyValue("ObjectValue", "Value") + + expect(defaultValue).to.equal(nil) + end) + + it("should get bool default values", function() + local _, defaultValue = getDefaultPropertyValue("BoolValue", "Value") + + expect(defaultValue).to.equal(false) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/init.lua b/Client2018/content/LuaPackages/RoactImpl/init.lua new file mode 100644 index 0000000..cfb0f94 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/init.lua @@ -0,0 +1,66 @@ +--[[ + Packages up the internals of Roact and exposes a public API for it. +]] + +local Change = require(script.Change) +local Component = require(script.Component) +local Core = require(script.Core) +local createElement = require(script.createElement) +local createRef = require(script.createRef) +local Event = require(script.Event) +local GlobalConfig = require(script.GlobalConfig) +local Instrumentation = require(script.Instrumentation) +local oneChild = require(script.oneChild) +local PureComponent = require(script.PureComponent) +local Reconciler = require(script.Reconciler) +local ReconcilerCompat = require(script.ReconcilerCompat) + +--[[ + A utility to copy one module into another, erroring if there are + overlapping keys. + + Any keys that begin with an underscore are considered private. +]] +local function apply(target, source) + for key, value in pairs(source) do + if target[key] ~= nil then + error(("Roact: key %q was overridden!"):format(key), 2) + end + + -- Don't add internal values + if not key:find("^_") then + target[key] = value + end + end +end + +local Roact = {} + +apply(Roact, Core) +apply(Roact, Reconciler) +apply(Roact, ReconcilerCompat) + +apply(Roact, { + Change = Change, + Component = Component, + createElement = createElement, + createRef = createRef, + Event = Event, + oneChild = oneChild, + PureComponent = PureComponent, +}) + +apply(Roact, { + setGlobalConfig = GlobalConfig.set, + getGlobalConfigValue = GlobalConfig.getValue, +}) + +apply(Roact, { + -- APIs that may change in the future + UNSTABLE = { + getCollectedStats = Instrumentation.getCollectedStats, + clearCollectedStats = Instrumentation.clearCollectedStats, + } +}) + +return Roact \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/init.spec.lua b/Client2018/content/LuaPackages/RoactImpl/init.spec.lua new file mode 100644 index 0000000..480dd15 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/init.spec.lua @@ -0,0 +1,495 @@ +return function() + local Roact = require(script.Parent) + + it("should load with all public APIs", function() + local publicApi = { + createElement = "function", + createRef = "function", + mount = "function", + unmount = "function", + reconcile = "function", + oneChild = "function", + setGlobalConfig = "function", + getGlobalConfigValue = "function", + + -- These functions are deprecated and will throw warnings soon! + reify = "function", + teardown = "function", + + Component = true, + PureComponent = true, + Portal = true, + Children = true, + Event = true, + Change = true, + Ref = true, + None = true, + Element = true, + UNSTABLE = true, + } + + expect(Roact).to.be.ok() + + for key, valueType in pairs(publicApi) do + local success + if typeof(valueType) == "string" then + success = typeof(Roact[key]) == valueType + else + success = Roact[key] ~= nil + end + + if not success then + local existence = typeof(valueType) == "boolean" and "present" or "of type " .. valueType + local message = ( + "Expected public API member %q to be %s, but instead it was of type %s" + ):format(tostring(key), existence, typeof(Roact[key])) + + error(message) + end + end + + for key in pairs(Roact) do + if publicApi[key] == nil then + local message = ( + "Found unknown public API key %q!" + ):format(tostring(key)) + + error(message) + end + end + end) + + describe("Props", function() + it("should be passed to primitive components", function() + local container = Instance.new("IntValue") + + local element = Roact.createElement("StringValue", { + Value = "foo", + }) + + Roact.mount(element, container, "TestStringValue") + + local rbx = container:FindFirstChild("TestStringValue") + + expect(rbx).to.be.ok() + expect(rbx.Value).to.equal("foo") + end) + + it("should be passed to functional components", function() + local testProp = {} + + local callCount = 0 + + local function TestComponent(props) + expect(props.testProp).to.equal(testProp) + callCount = callCount + 1 + end + + local element = Roact.createElement(TestComponent, { + testProp = testProp, + }) + + Roact.mount(element) + + -- The only guarantee is that the function will be invoked at least once + expect(callCount > 0).to.equal(true) + end) + + it("should be passed to stateful components", function() + local testProp = {} + + local callCount = 0 + + local TestComponent = Roact.Component:extend("TestComponent") + + function TestComponent:init(props) + expect(props.testProp).to.equal(testProp) + callCount = callCount + 1 + end + + function TestComponent:render() + end + + local element = Roact.createElement(TestComponent, { + testProp = testProp, + }) + + Roact.mount(element) + + expect(callCount).to.equal(1) + end) + end) + + describe("State", function() + it("should trigger a re-render of child components", function() + local renderCount = 0 + local listener = nil + + local TestChild = Roact.Component:extend("TestChild") + + function TestChild:render() + renderCount = renderCount + 1 + return nil + end + + local TestParent = Roact.Component:extend("TestParent") + + function TestParent:init(props) + self.state = { + value = 0, + } + end + + function TestParent:didMount() + listener = function() + self:setState({ + value = self.state.value + 1, + }) + end + end + + function TestParent:render() + return Roact.createElement(TestChild, { + value = self.state.value, + }) + end + + local element = Roact.createElement(TestParent) + Roact.mount(element) + + expect(renderCount >= 1).to.equal(true) + expect(listener).to.be.a("function") + + listener() + + expect(renderCount >= 2).to.equal(true) + end) + end) + + describe("Context", function() + it("should be passed to children through primitive and functional components", function() + local testValue = {} + + local callCount = 0 + + local ContextConsumer = Roact.Component:extend("ContextConsumer") + + function ContextConsumer:init(props) + expect(self._context.testValue).to.equal(testValue) + + callCount = callCount + 1 + end + + function ContextConsumer:render() + return + end + + local function ContextBarrier(props) + return Roact.createElement(ContextConsumer) + end + + local ContextProvider = Roact.Component:extend("ContextProvider") + + function ContextProvider:init(props) + self._context.testValue = props.testValue + end + + function ContextProvider:render() + return Roact.createElement("Frame", {}, { + Child = Roact.createElement(ContextBarrier), + }) + end + + local element = Roact.createElement(ContextProvider, { + testValue = testValue, + }) + + Roact.mount(element) + + expect(callCount).to.equal(1) + end) + end) + + describe("Ref", function() + it("should call back with a Roblox object after properties and children", function() + local callCount = 0 + + local function ref(rbx) + expect(rbx).to.be.ok() + expect(rbx.ClassName).to.equal("StringValue") + expect(rbx.Value).to.equal("Hey!") + expect(rbx.Name).to.equal("RefTest") + expect(#rbx:GetChildren()).to.equal(1) + + callCount = callCount + 1 + end + + local element = Roact.createElement("StringValue", { + Value = "Hey!", + [Roact.Ref] = ref, + }, { + TestChild = Roact.createElement("StringValue"), + }) + + Roact.mount(element, nil, "RefTest") + + expect(callCount).to.equal(1) + end) + + it("should pass nil to refs for tearing down", function() + local callCount = 0 + local currentRef + + local function ref(rbx) + currentRef = rbx + callCount = callCount + 1 + end + + local element = Roact.createElement("StringValue", { + [Roact.Ref] = ref, + }) + + local instance = Roact.mount(element, nil, "RefTest") + + expect(callCount).to.equal(1) + expect(currentRef).to.be.ok() + expect(currentRef.Name).to.equal("RefTest") + + Roact.unmount(instance) + + expect(callCount).to.equal(2) + expect(currentRef).to.equal(nil) + end) + + it("should tear down refs when switched out of the tree", function() + local updateMethod + local refCount = 0 + local currentRef + + local function ref(rbx) + currentRef = rbx + refCount = refCount + 1 + end + + local function RefWrapper() + return Roact.createElement("StringValue", { + Value = "ooba ooba", + [Roact.Ref] = ref, + }) + end + + local Root = Roact.Component:extend("RefTestRoot") + + function Root:init() + updateMethod = function(show) + self:setState({ + show = show, + }) + end + end + + function Root:render() + if self.state.show then + return Roact.createElement(RefWrapper) + end + end + + local element = Roact.createElement(Root) + Roact.mount(element) + + expect(refCount).to.equal(0) + expect(currentRef).to.equal(nil) + + updateMethod(true) + + expect(refCount).to.equal(1) + expect(currentRef.Value).to.equal("ooba ooba") + + updateMethod(false) + + expect(refCount).to.equal(2) + expect(currentRef).to.equal(nil) + end) + end) + + describe("Portal", function() + it("should place all children as children of the target Roblox instance", function() + local target = Instance.new("Folder") + + local function FunctionalComponent(props) + local intValue = props.value + + return Roact.createElement("IntValue", { + Value = intValue, + }) + end + + local portal = Roact.createElement(Roact.Portal, { + target = target + }, { + folderOne = Roact.createElement("Folder"), + folderTwo = Roact.createElement("Folder"), + intValueOne = Roact.createElement(FunctionalComponent, { + value = 42, + }), + }) + Roact.mount(portal) + + expect(target:FindFirstChild("folderOne")).to.be.ok() + expect(target:FindFirstChild("folderTwo")).to.be.ok() + expect(target:FindFirstChild("intValueOne")).to.be.ok() + expect(target:FindFirstChild("intValueOne").Value).to.equal(42) + end) + + it("should error if the target is nil", function() + local portal = Roact.createElement(Roact.Portal, {}, { + folderOne = Roact.createElement("Folder"), + folderTwo = Roact.createElement("Folder"), + }) + + expect(function() + Roact.mount(portal) + end).to.throw() + end) + + it("should error if the target is not a Roblox instance", function() + local portal = Roact.createElement(Roact.Portal, { + target = "NotARobloxInstance", + }, { + folderOne = Roact.createElement("Folder"), + folderTwo = Roact.createElement("Folder"), + }) + + expect(function() + Roact.mount(portal) + end).to.throw() + end) + + it("should update if parent changes the target", function() + local targetOne = Instance.new("Folder") + local targetTwo = Instance.new("Folder") + local countWillUnmount = 0 + local changeState + + local TestUnmountComponent = Roact.Component:extend("TestUnmountComponent") + + function TestUnmountComponent:render() + return nil + end + + function TestUnmountComponent:willUnmount() + countWillUnmount = countWillUnmount + 1 + end + + local PortalContainer = Roact.Component:extend("PortalContainer") + + function PortalContainer:init() + self.state = { + target = targetOne, + } + end + + function PortalContainer:render() + return Roact.createElement(Roact.Portal, { + target = self.state.target, + }, { + folderOne = Roact.createElement("Folder"), + folderTwo = Roact.createElement("Folder"), + testUnmount = Roact.createElement(TestUnmountComponent), + }) + end + + function PortalContainer:didMount() + expect(self.state.target:FindFirstChild("folderOne")).to.be.ok() + expect(self.state.target:FindFirstChild("folderTwo")).to.be.ok() + + changeState = function(newState) + self:setState(newState) + end + end + + Roact.mount(Roact.createElement(PortalContainer)) + + expect(targetOne:FindFirstChild("folderOne")).to.be.ok() + expect(targetOne:FindFirstChild("folderTwo")).to.be.ok() + + changeState({ + target = targetTwo, + }) + + expect(countWillUnmount).to.equal(1) + + expect(targetOne:FindFirstChild("folderOne")).never.to.be.ok() + expect(targetOne:FindFirstChild("folderTwo")).never.to.be.ok() + expect(targetTwo:FindFirstChild("folderOne")).to.be.ok() + expect(targetTwo:FindFirstChild("folderTwo")).to.be.ok() + end) + + it("should update Roblox instance properties when relevant parent props are changed", function() + local target = Instance.new("Folder") + local changeState + + local PortalContainer = Roact.Component:extend("PortalContainer") + + function PortalContainer:init() + self.state = { + value = "initialStringValue", + } + end + + function PortalContainer:render() + return Roact.createElement(Roact.Portal, { + target = target, + }, { + TestStringValue = Roact.createElement("StringValue", { + Value = self.state.value, + }) + }) + end + + function PortalContainer:didMount() + changeState = function(newState) + self:setState(newState) + end + end + + Roact.mount(Roact.createElement(PortalContainer)) + + expect(target:FindFirstChild("TestStringValue")).to.be.ok() + expect(target:FindFirstChild("TestStringValue").Value).to.equal("initialStringValue") + + changeState({ + value = "newStringValue", + }) + + expect(target:FindFirstChild("TestStringValue")).to.be.ok() + expect(target:FindFirstChild("TestStringValue").Value).to.equal("newStringValue") + end) + + it("should properly teardown the Portal", function() + local target = Instance.new("Folder") + + local portal = Roact.createElement(Roact.Portal, { + target = target + }, { + folderOne = Roact.createElement("Folder"), + folderTwo = Roact.createElement("Folder"), + }) + local instance = Roact.mount(portal) + + local folderThree = Instance.new("Folder") + folderThree.Name = "folderThree" + folderThree.Parent = target + + expect(target:FindFirstChild("folderOne")).to.be.ok() + expect(target:FindFirstChild("folderTwo")).to.be.ok() + expect(target:FindFirstChild("folderThree")).to.be.ok() + + Roact.unmount(instance) + + expect(target:FindFirstChild("folderOne")).never.to.be.ok() + expect(target:FindFirstChild("folderTwo")).never.to.be.ok() + expect(target:FindFirstChild("folderThree")).to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/invalidSetStateMessages.lua b/Client2018/content/LuaPackages/RoactImpl/invalidSetStateMessages.lua new file mode 100644 index 0000000..83f41e3 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/invalidSetStateMessages.lua @@ -0,0 +1,65 @@ +--[[ + These messages are used by Component to help users diagnose when they're + calling setState in inappropriate places. + + The indentation may seem odd, but it's necessary to avoid introducing extra + whitespace into the error messages themselves. +]] + +local invalidSetStateMessages = {} + +invalidSetStateMessages["willUpdate"] = [[ +setState cannot be used in the willUpdate lifecycle method. +Consider using the didUpdate method instead, or using getDerivedStateFromProps. + +Check the definition of willUpdate in the component %q.]] + +invalidSetStateMessages["willUnmount"] = [[ +setState cannot be used in the willUnmount lifecycle method. +A component that is being unmounted cannot be updated! + +Check the definition of willUnmount in the component %q.]] + +invalidSetStateMessages["shouldUpdate"] = [[ +setState cannot be used in the shouldUpdate lifecycle method. +shouldUpdate must be a pure function that only depends on props and state. + +Check the definition of shouldUpdate in the component %q.]] + +invalidSetStateMessages["init"] = [[ +setState cannot be used in the init method. +During init, the component hasn't initialized yet, and isn't ready to render. + +Instead, set the `state` value directly: + + self.state = { + value = "foo" + } + +Check the definition of init in the component %q.]] + +invalidSetStateMessages["render"] = [[ +setState cannot be used in the render method. +render must be a pure function that only depends on props and state. + +Check the definition of render in the component %q.]] + +invalidSetStateMessages["reconcile"] = [[ +setState cannot be called while a component is being reified or reconciled. +This is the step where Roact constructs Roblox instances, and starting another +render here would introduce bugs. + +Check the component %q to see if setState is being called by: +* a child Ref +* a child Changed event +* a child's render method]] + +invalidSetStateMessages["default"] = [[ +setState can not be used in the current situation, but Roact couldn't find a +message to display. + +This is a bug in Roact. +It was triggered by the component %q. +]] + +return invalidSetStateMessages \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/oneChild.lua b/Client2018/content/LuaPackages/RoactImpl/oneChild.lua new file mode 100644 index 0000000..f3fcb9e --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/oneChild.lua @@ -0,0 +1,28 @@ +--[[ + Utility to retrieve one child out the children passed to a component. + + If passed nil or an empty table, will return nil. + + Throws an error if passed more than one child, but can be passed zero. +]] +local function oneChild(children) + if not children then + return + end + + local key, child = next(children) + + if not child then + return + end + + local after = next(children, key) + + if after then + error("Expected at most child, had more than one child.", 2) + end + + return child +end + +return oneChild \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactImpl/oneChild.spec.lua b/Client2018/content/LuaPackages/RoactImpl/oneChild.spec.lua new file mode 100644 index 0000000..6540ce2 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactImpl/oneChild.spec.lua @@ -0,0 +1,35 @@ +return function() + local createElement = require(script.Parent.createElement) + + local oneChild = require(script.Parent.oneChild) + + it("should get zero children from a table", function() + local children = {} + + expect(oneChild(children)).to.equal(nil) + end) + + it("should get exactly one child", function() + local child = createElement("Frame") + local children = { + foo = child, + } + + expect(oneChild(children)).to.equal(child) + end) + + it("should error with more than one child", function() + local children = { + a = createElement("Frame"), + b = createElement("Frame"), + } + + expect(function() + oneChild(children) + end).to.throw() + end) + + it("should handle being passed nil", function() + expect(oneChild(nil)).to.equal(nil) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactRodux.lua b/Client2018/content/LuaPackages/RoactRodux.lua new file mode 100644 index 0000000..afa6fa1 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactRodux.lua @@ -0,0 +1,7 @@ +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.RoactRoduxImpl) + +return require(CorePackages.RoactRoduxImpl) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactRoduxImpl/StoreProvider.lua b/Client2018/content/LuaPackages/RoactRoduxImpl/StoreProvider.lua new file mode 100644 index 0000000..a226935 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactRoduxImpl/StoreProvider.lua @@ -0,0 +1,21 @@ +local Roact = require(script.Parent.Parent.Roact) + +local storeKey = require(script.Parent.storeKey) + +local StoreProvider = Roact.Component:extend("StoreProvider") + +function StoreProvider:init(props) + local store = props.store + + if store == nil then + error("Error initializing StoreProvider. Expected a `store` prop to be a Rodux store.") + end + + self._context[storeKey] = store +end + +function StoreProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +return StoreProvider \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactRoduxImpl/StoreProvider.spec.lua b/Client2018/content/LuaPackages/RoactRoduxImpl/StoreProvider.spec.lua new file mode 100644 index 0000000..e7236f4 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactRoduxImpl/StoreProvider.spec.lua @@ -0,0 +1,30 @@ +return function() + local StoreProvider = require(script.Parent.StoreProvider) + + local Roact = require(script.Parent.Parent.Roact) + local Rodux = require(script.Parent.Parent.Rodux) + + it("should be instantiable as a component", function() + local store = Rodux.Store.new(function() + return 0 + end) + local element = Roact.createElement(StoreProvider, { + store = store + }) + + expect(element).to.be.ok() + + local handle = Roact.mount(element, nil, "StoreProvider-test") + + Roact.unmount(handle) + store:destruct() + end) + + it("should expect a 'store' prop", function() + local element = Roact.createElement(StoreProvider) + + expect(function() + Roact.mount(element) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactRoduxImpl/Symbol.lua b/Client2018/content/LuaPackages/RoactRoduxImpl/Symbol.lua new file mode 100644 index 0000000..8b6adaf --- /dev/null +++ b/Client2018/content/LuaPackages/RoactRoduxImpl/Symbol.lua @@ -0,0 +1,43 @@ +--[[ + A 'Symbol' is an opaque marker type that can be used to signify unique + statuses. Symbols have the type 'userdata', but when printed to the console, + the name of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +--[[ + Create an unnamed Symbol. Usually, you should create a named Symbol using + Symbol.named(name) +]] +function Symbol.unnamed() + local self = newproxy(true) + + getmetatable(self).__tostring = function() + return "Unnamed Symbol" + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactRoduxImpl/Symbol.spec.lua b/Client2018/content/LuaPackages/RoactRoduxImpl/Symbol.spec.lua new file mode 100644 index 0000000..cde9be0 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactRoduxImpl/Symbol.spec.lua @@ -0,0 +1,45 @@ +return function() + local Symbol = require(script.Parent.Symbol) + + describe("named", function() + it("should give an opaque object", function() + local symbol = Symbol.named("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = Symbol.named("foo") + + expect(tostring(symbol):find("foo")).to.be.ok() + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.named("abc") + local symbolB = Symbol.named("abc") + + expect(symbolA).never.to.equal(symbolB) + end) + end) + + describe("unnamed", function() + it("should give an opaque object", function() + local symbol = Symbol.unnamed() + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to some string", function() + local symbol = Symbol.unnamed() + + expect(tostring(symbol)).to.be.a("string") + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.unnamed() + local symbolB = Symbol.unnamed() + + expect(symbolA).never.to.equal(symbolB) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactRoduxImpl/connect.lua b/Client2018/content/LuaPackages/RoactRoduxImpl/connect.lua new file mode 100644 index 0000000..805a7ce --- /dev/null +++ b/Client2018/content/LuaPackages/RoactRoduxImpl/connect.lua @@ -0,0 +1,113 @@ +local Roact = require(script.Parent.Parent.Roact) + +local storeKey = require(script.Parent.storeKey) + +local function shallowEqual(a, b) + for key, value in pairs(a) do + if b[key] ~= value then + return false + end + end + + for key, value in pairs(b) do + if a[key] ~= value then + return false + end + end + + return true +end + +--[[ + Joins two tables together into a new table +]] +local function join(a, b) + local result = {} + + for key, value in pairs(a) do + result[key] = value + end + + for key, value in pairs(b) do + result[key] = value + end + + return result +end + +-- A version of 'error' that outputs over multiple lines +local function errorLines(...) + error(table.concat({...}, "\n")) +end + +local function connect(mapStoreToProps) + local rootTrace = debug.traceback() + + local mapConnect = function(store, props) + local result = mapStoreToProps(store, props) + + if type(result) ~= "table" then + errorLines( + "mapStoreToProps must return a table! Check the function passed into 'connect' at:", + rootTrace + ) + end + + return result + end + + return function(component) + if component == nil then + error("Expected component to be passed to connection, got nil.") + end + + local name = ("Connection(%s)"):format( + tostring(component) + ) + local Connection = Roact.Component:extend(name) + + function Connection:init(props) + local store = self._context[storeKey] + + if not store then + errorLines( + "Cannot initialize Roact-Rodux component without being a descendent of StoreProvider!", + ("Tried to wrap component %q"):format(tostring(component)), + "Make sure there is a StoreProvider above this component in the tree." + ) + end + + self.store = store + + self.state = { + storeProps = mapConnect(store, props), + } + end + + function Connection:didMount() + self.eventHandle = self.store.changed:connect(function(state) + local storeProps = mapConnect(self.store, self.props) + + if not shallowEqual(self.state.storeProps, storeProps) then + self:setState({ + storeProps = storeProps + }) + end + end) + end + + function Connection:willUnmount() + self.eventHandle:disconnect() + end + + function Connection:render() + local props = join(self.props, self.state.storeProps) + + return Roact.createElement(component, props) + end + + return Connection + end +end + +return connect \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactRoduxImpl/connect.spec.lua b/Client2018/content/LuaPackages/RoactRoduxImpl/connect.spec.lua new file mode 100644 index 0000000..eae3b12 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactRoduxImpl/connect.spec.lua @@ -0,0 +1,116 @@ +return function() + local connect = require(script.Parent.connect) + + local StoreProvider = require(script.Parent.StoreProvider) + + local Roact = require(script.Parent.Parent.Roact) + local Rodux = require(script.Parent.Parent.Rodux) + + local function incrementReducer(state, action) + state = state or 0 + + if action.type == "increment" then + return state + 1 + end + + return state + end + + it("should throw if not passed a component", function() + local selector = function(store) + return {} + end + + expect(function() + connect(selector)(nil) + end).to.throw() + end) + + it("should successfully connect when mounted under a StoreProvider", function() + local store = Rodux.Store.new(incrementReducer) + + local function SomeComponent(props) + return nil + end + + local ConnectedSomeComponent = connect(function(store) + return {} + end)(SomeComponent) + + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + Child = Roact.createElement(ConnectedSomeComponent), + }) + + local handle = Roact.mount(tree) + + expect(handle).to.be.ok() + end) + + it("should fail to mount without a StoreProvider", function() + local function SomeComponent(props) + return nil + end + + local ConnectedSomeComponent = connect(function(store) + return {} + end)(SomeComponent) + + local tree = Roact.createElement(ConnectedSomeComponent) + + expect(function() + Roact.mount(tree) + end).to.throw() + end) + + it("should trigger renders on store changes only with shallow differences", function() + local callCount = 0 + + local store = Rodux.Store.new(incrementReducer) + + local function SomeComponent(props) + callCount = callCount + 1 + + return nil + end + + local ConnectedSomeComponent = connect(function(store) + return { + value = store:getState() + } + end)(SomeComponent) + + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + Child = Roact.createElement(ConnectedSomeComponent), + }) + + Roact.mount(tree) + + -- Our component should render initially + expect(store:getState()).to.equal(0) + expect(callCount).to.equal(1) + + store:dispatch({ + type = "increment", + }) + + store:flush() + + -- Our component should re-render, state is different. + expect(store:getState()).to.equal(1) + expect(callCount).to.equal(2) + + store:dispatch({ + type = "SOME_UNHANDLED_ACTION", + }) + + store:flush() + + -- Our component should not re-render, state is the same! + expect(store:getState()).to.equal(1) + expect(callCount).to.equal(2) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactRoduxImpl/connect2.lua b/Client2018/content/LuaPackages/RoactRoduxImpl/connect2.lua new file mode 100644 index 0000000..530062f --- /dev/null +++ b/Client2018/content/LuaPackages/RoactRoduxImpl/connect2.lua @@ -0,0 +1,183 @@ +local Roact = require(script.Parent.Parent.Roact) +local storeKey = require(script.Parent.storeKey) +local shallowEqual = require(script.Parent.shallowEqual) +local join = require(script.Parent.join) + +--[[ + Formats a multi-line message with printf-style placeholders. +]] +local function formatMessage(lines, parameters) + return table.concat(lines, "\n"):format(unpack(parameters or {})) +end + +local function noop() + return nil +end + +--[[ + The stateUpdater accepts props when they update and computes the + complete set of props that should be passed to the wrapped component. + + Each connected component will have a stateUpdater created for it. + + stateUpdater is put into the component's state in order for + getDerivedStateFromProps to be able to access it. It is not mutated. +]] +local function makeStateUpdater(store) + return function(nextProps, prevState, mappedStoreState) + -- The caller can optionally provide mappedStoreState if it needed that + -- value beforehand. Doing so is purely an optimization. + if mappedStoreState == nil then + mappedStoreState = prevState.mapStateToProps(store:getState(), nextProps) + end + + local propsForChild = join(nextProps, mappedStoreState, prevState.mappedStoreDispatch) + + return { + mappedStoreState = mappedStoreState, + propsForChild = propsForChild, + } + end +end + +--[[ + mapStateToProps: + (storeState, props) -> partialProps + OR + () -> (storeState, props) -> partialProps + mapDispatchToProps: (dispatch) -> partialProps +]] +local function connect(mapStateToPropsOrThunk, mapDispatchToProps) + local connectTrace = debug.traceback() + + if mapStateToPropsOrThunk ~= nil then + assert(typeof(mapStateToPropsOrThunk) == "function", "mapStateToProps must be a function or nil!") + else + mapStateToPropsOrThunk = noop + end + + if mapDispatchToProps ~= nil then + assert(typeof(mapDispatchToProps) == "function", "mapDispatchToProps must be a function or nil!") + else + mapDispatchToProps = noop + end + + return function(innerComponent) + if innerComponent == nil then + local message = formatMessage({ + "connect returns a function that must be passed a component.", + "Check the connection at:", + "%s", + }, { + connectTrace, + }) + + error(message, 2) + end + + local componentName = ("RoduxConnection(%s)"):format(tostring(innerComponent)) + + local Connection = Roact.Component:extend(componentName) + + function Connection.getDerivedStateFromProps(nextProps, prevState) + return prevState.stateUpdater(nextProps, prevState) + end + + function Connection:init() + self.store = self._context[storeKey] + + if self.store == nil then + local message = formatMessage({ + "Cannot initialize Roact-Rodux connection without being a descendent of StoreProvider!", + "Tried to wrap component %q", + "Make sure there is a StoreProvider above this component in the tree.", + }, { + tostring(innerComponent), + }) + + error(message) + end + + local storeState = self.store:getState() + + local mapStateToProps = mapStateToPropsOrThunk + local mappedStoreState = mapStateToProps(storeState, self.props) + + -- mapStateToPropsOrThunk can return a function instead of a state + -- value. In this variant, we keep that value as mapStateToProps + -- instead of the original mapStateToProps. This matches react-redux + -- and enables connectors to keep instance-level state. + if typeof(mappedStoreState) == "function" then + mapStateToProps = mappedStoreState + mappedStoreState = mapStateToProps(storeState, self.props) + end + + if mappedStoreState ~= nil and typeof(mappedStoreState) ~= "table" then + local message = formatMessage({ + "mapStateToProps must either return a table, or return another function that returns a table.", + "Instead, it returned %q, which is of type %s.", + }, { + tostring(mappedStoreState), + typeof(mappedStoreState), + }) + + error(message) + end + + local mappedStoreDispatch = mapDispatchToProps(function(...) + return self.store:dispatch(...) + end) + + local stateUpdater = makeStateUpdater(self.store) + + self.state = { + -- Combines props, mappedStoreDispatch, and the result of + -- mapStateToProps into propsForChild. Stored in state so that + -- getDerivedStateFromProps can access it. + stateUpdater = stateUpdater, + + -- Used by the store changed connection and stateUpdater to + -- construct propsForChild. + mapStateToProps = mapStateToProps, + + -- Used by stateUpdater to construct propsForChild. + mappedStoreDispatch = mappedStoreDispatch, + + -- Passed directly into the component that Connection is + -- wrapping. + propsForChild = nil, + } + + self.state.propsForChild = stateUpdater(self.props, self.state, mappedStoreState) + end + + function Connection:didMount() + self.storeChangedConnection = self.store.changed:connect(function(storeState) + self:setState(function(prevState, props) + local mappedStoreState = prevState.mapStateToProps(storeState, props) + + -- We run this check here so that we only check shallow + -- equality with the result of mapStateToProps, and not the + -- other props that could be passed through the connector. + if shallowEqual(mappedStoreState, prevState.mappedStoreState) then + return nil + end + + return prevState.stateUpdater(props, prevState, mappedStoreState) + end) + end) + end + + function Connection:willUnmount() + self.storeChangedConnection:disconnect() + end + + function Connection:render() + return Roact.createElement(innerComponent, self.state.propsForChild) + end + + return Connection + end +end + +return connect \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactRoduxImpl/connect2.spec.lua b/Client2018/content/LuaPackages/RoactRoduxImpl/connect2.spec.lua new file mode 100644 index 0000000..2531648 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactRoduxImpl/connect2.spec.lua @@ -0,0 +1,251 @@ +return function() + local connect2 = require(script.Parent.connect2) + + local StoreProvider = require(script.Parent.StoreProvider) + + local Roact = require(script.Parent.Parent.Roact) + local Rodux = require(script.Parent.Parent.Rodux) + + local function noop() + return nil + end + + local function NoopComponent() + return nil + end + + local function countReducer(state, action) + state = state or 0 + + if action.type == "increment" then + return state + 1 + end + + return state + end + + local reducer = Rodux.combineReducers({ + count = countReducer, + }) + + describe("Argument validation", function() + it("should accept no arguments", function() + connect2() + end) + + it("should accept one function", function() + connect2(noop) + end) + + it("should accept two functions", function() + connect2(noop, noop) + end) + + it("should accept only the second function", function() + connect2(nil, function() end) + end) + + it("should throw if not passed a component", function() + local selector = function(store) + return {} + end + + expect(function() + connect2(selector)(nil) + end).to.throw() + end) + end) + + it("should throw if not mounted under a StoreProvider", function() + local ConnectedSomeComponent = connect2()(NoopComponent) + + expect(function() + Roact.mount(Roact.createElement(ConnectedSomeComponent)) + end).to.throw() + end) + + it("should accept a higher-order function mapStateToProps", function() + local function mapStateToProps() + return function(state) + return { + count = state.count, + } + end + end + + local ConnectedSomeComponent = connect2(mapStateToProps)(NoopComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent), + }) + + local handle = Roact.mount(tree) + + Roact.unmount(handle) + end) + + it("should not accept a higher-order mapStateToProps that returns a non-table value", function() + local function mapStateToProps() + return function(state) + return "nope" + end + end + + local ConnectedSomeComponent = connect2(mapStateToProps)(NoopComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent), + }) + + expect(function() + Roact.mount(tree) + end).to.throw() + end) + + it("should not accept a mapStateToProps that returns a non-table value", function() + local function mapStateToProps() + return "nah" + end + + local ConnectedSomeComponent = connect2(mapStateToProps)(NoopComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent), + }) + + expect(function() + Roact.mount(tree) + end).to.throw() + end) + + it("should abort renders when mapStateToProps returns the same data", function() + local function mapStateToProps(state) + return { + count = state.count, + } + end + + local renderCount = 0 + local function SomeComponent(props) + renderCount = renderCount + 1 + end + + local ConnectedSomeComponent = connect2(mapStateToProps)(SomeComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent), + }) + + local handle = Roact.mount(tree) + + expect(renderCount).to.equal(1) + + store:dispatch({ type = "an unknown action" }) + store:flush() + + expect(renderCount).to.equal(1) + + store:dispatch({ type = "increment" }) + store:flush() + + expect(renderCount).to.equal(2) + + Roact.unmount(handle) + end) + + it("should only call mapDispatchToProps once and never re-render if no mapStateToProps was passed", function() + local dispatchCount = 0 + local mapDispatchToProps = function(dispatch) + dispatchCount = dispatchCount + 1 + + return { + increment = function() + return dispatch({ type = "increment" }) + end, + } + end + + local renderCount = 0 + local function SomeComponent(props) + renderCount = renderCount + 1 + end + + local ConnectedSomeComponent = connect2(nil, mapDispatchToProps)(SomeComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent), + }) + + local handle = Roact.mount(tree) + + expect(dispatchCount).to.equal(1) + expect(renderCount).to.equal(1) + + store:dispatch({ type = "an unknown action" }) + store:flush() + + expect(dispatchCount).to.equal(1) + expect(renderCount).to.equal(1) + + store:dispatch({ type = "increment" }) + store:flush() + + expect(dispatchCount).to.equal(1) + expect(renderCount).to.equal(1) + + Roact.unmount(handle) + end) + + it("should return result values from the dispatch passed to mapDispatchToProps", function() + local function reducer() + return 0 + end + + local function fiveThunk() + return 5 + end + + local dispatch + local function SomeComponent(props) + dispatch = props.dispatch + end + + local function mapDispatchToProps(dispatch) + return { + dispatch = dispatch + } + end + + local ConnectedSomeComponent = connect2(nil, mapDispatchToProps)(SomeComponent) + + -- We'll use the thunk middleware, as it should always return its result + local store = Rodux.Store.new(reducer, nil, { Rodux.thunkMiddleware }) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent) + }) + + local handle = Roact.mount(tree) + + expect(dispatch).to.be.a("function") + expect(dispatch(fiveThunk)).to.equal(5) + + Roact.unmount(handle) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactRoduxImpl/init.lua b/Client2018/content/LuaPackages/RoactRoduxImpl/init.lua new file mode 100644 index 0000000..8c740dc --- /dev/null +++ b/Client2018/content/LuaPackages/RoactRoduxImpl/init.lua @@ -0,0 +1,9 @@ +local StoreProvider = require(script.StoreProvider) +local connect = require(script.connect) +local connect2 = require(script.connect2) + +return { + StoreProvider = StoreProvider, + connect = connect, + UNSTABLE_connect2 = connect2, +} \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactRoduxImpl/join.lua b/Client2018/content/LuaPackages/RoactRoduxImpl/join.lua new file mode 100644 index 0000000..0e6b195 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactRoduxImpl/join.lua @@ -0,0 +1,17 @@ +local function join(...) + local result = {} + + for i = 1, select("#", ...) do + local source = select(i, ...) + + if source ~= nil then + for key, value in pairs(source) do + result[key] = value + end + end + end + + return result +end + +return join \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactRoduxImpl/shallowEqual.lua b/Client2018/content/LuaPackages/RoactRoduxImpl/shallowEqual.lua new file mode 100644 index 0000000..8e9b68a --- /dev/null +++ b/Client2018/content/LuaPackages/RoactRoduxImpl/shallowEqual.lua @@ -0,0 +1,23 @@ +local function shallowEqual(a, b) + if a == nil then + return b == nil + elseif b == nil then + return a == nil + end + + for key, value in pairs(a) do + if value ~= b[key] then + return false + end + end + + for key, value in pairs(b) do + if value ~= a[key] then + return false + end + end + + return true +end + +return shallowEqual \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactRoduxImpl/shallowEqual.spec.lua b/Client2018/content/LuaPackages/RoactRoduxImpl/shallowEqual.spec.lua new file mode 100644 index 0000000..fd0f9a2 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactRoduxImpl/shallowEqual.spec.lua @@ -0,0 +1,45 @@ +return function() + local shallowEqual = require(script.Parent.shallowEqual) + + it("should compare dictionaries", function() + local a = { + a = "a", + b = {}, + c = 6, + } + + local b = { + b = a.b, + c = a.c, + a = a.a, + } + + local c = { + b = {}, + a = a.a, + c = a.c, + } + + local d = { + a = a.a, + b = a.b, + c = a.c, + d = "hello", + } + + expect(shallowEqual(a, a)).to.equal(true) + expect(shallowEqual(a, b)).to.equal(true) + expect(shallowEqual(a, c)).to.equal(false) + expect(shallowEqual(b, c)).to.equal(false) + expect(shallowEqual(a, d)).to.equal(false) + expect(shallowEqual(b, d)).to.equal(false) + end) + + it("should handle nil for either argument", function() + local a = {} + + expect(shallowEqual(nil, nil)).to.equal(true) + expect(shallowEqual(a, nil)).to.equal(false) + expect(shallowEqual(nil, a)).to.equal(false) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoactRoduxImpl/storeKey.lua b/Client2018/content/LuaPackages/RoactRoduxImpl/storeKey.lua new file mode 100644 index 0000000..bb77a74 --- /dev/null +++ b/Client2018/content/LuaPackages/RoactRoduxImpl/storeKey.lua @@ -0,0 +1,3 @@ +local Symbol = require(script.Parent.Symbol) + +return Symbol.named("RoduxStore") \ No newline at end of file diff --git a/Client2018/content/LuaPackages/Rodux.lua b/Client2018/content/LuaPackages/Rodux.lua new file mode 100644 index 0000000..7828db4 --- /dev/null +++ b/Client2018/content/LuaPackages/Rodux.lua @@ -0,0 +1,7 @@ +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.RoduxImpl) + +return require(CorePackages.RoduxImpl) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoduxImpl/NoYield.lua b/Client2018/content/LuaPackages/RoduxImpl/NoYield.lua new file mode 100644 index 0000000..f9519f1 --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/NoYield.lua @@ -0,0 +1,29 @@ +--[[ + Calls a function and throws an error if it attempts to yield. + + Pass any number of arguments to the function after the callback. + + This function supports multiple return; all results returned from the + given function will be returned. +]] + +local function resultHandler(co, ok, ...) + if not ok then + local message = (...) + error(debug.traceback(co, message), 2) + end + + if coroutine.status(co) ~= "dead" then + error(debug.traceback(co, "Attempted to yield inside changed event!"), 2) + end + + return ... +end + +local function NoYield(callback, ...) + local co = coroutine.create(callback) + + return resultHandler(co, coroutine.resume(co, ...)) +end + +return NoYield \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoduxImpl/NoYield.spec.lua b/Client2018/content/LuaPackages/RoduxImpl/NoYield.spec.lua new file mode 100644 index 0000000..2034ff9 --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/NoYield.spec.lua @@ -0,0 +1,56 @@ +return function() + local NoYield = require(script.Parent.NoYield) + + it("should call functions normally", function() + local callCount = 0 + + local function test(a, b) + expect(a).to.equal(5) + expect(b).to.equal(6) + + callCount = callCount + 1 + + return 11, "hello" + end + + local a, b = NoYield(test, 5, 6) + + expect(a).to.equal(11) + expect(b).to.equal("hello") + end) + + it("should throw on yield", function() + local preCount = 0 + local postCount = 0 + + local function testMethod() + preCount = preCount + 1 + wait() + postCount = postCount + 1 + end + + local ok, err = pcall(NoYield, testMethod) + + expect(preCount).to.equal(1) + expect(postCount).to.equal(0) + + expect(ok).to.equal(false) + expect(err:find("wait")).to.be.ok() + expect(err:find("NoYield.spec")).to.be.ok() + end) + + it("should propagate error messages", function() + local count = 0 + + local function test() + count = count + 1 + error("foo") + end + + local ok, err = pcall(NoYield, test) + + expect(ok).to.equal(false) + expect(err:find("foo")).to.be.ok() + expect(err:find("NoYield.spec")).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoduxImpl/Signal.lua b/Client2018/content/LuaPackages/RoduxImpl/Signal.lua new file mode 100644 index 0000000..dc4d041 --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/Signal.lua @@ -0,0 +1,75 @@ +--[[ + A limited, simple implementation of a Signal. + + Handlers are fired in order, and (dis)connections are properly handled when + executing an event. +]] + +local function immutableAppend(list, ...) + local new = {} + local len = #list + + for key = 1, len do + new[key] = list[key] + end + + for i = 1, select("#", ...) do + new[len + i] = select(i, ...) + end + + return new +end + +local function immutableRemoveValue(list, removeValue) + local new = {} + + for i = 1, #list do + if list[i] ~= removeValue then + table.insert(new, list[i]) + end + end + + return new +end + +local Signal = {} + +Signal.__index = Signal + +function Signal.new() + local self = { + _listeners = {} + } + + setmetatable(self, Signal) + + return self +end + +function Signal:connect(callback) + local listener = { + callback = callback, + disconnected = false, + } + + self._listeners = immutableAppend(self._listeners, listener) + + local function disconnect() + listener.disconnected = true + self._listeners = immutableRemoveValue(self._listeners, listener) + end + + return { + disconnect = disconnect + } +end + +function Signal:fire(...) + for _, listener in ipairs(self._listeners) do + if not listener.disconnected then + listener.callback(...) + end + end +end + +return Signal \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoduxImpl/Signal.spec.lua b/Client2018/content/LuaPackages/RoduxImpl/Signal.spec.lua new file mode 100644 index 0000000..f00f947 --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/Signal.spec.lua @@ -0,0 +1,114 @@ +return function() + local Signal = require(script.Parent.Signal) + + it("should construct from nothing", function() + local signal = Signal.new() + + expect(signal).to.be.ok() + end) + + it("should fire connected callbacks", function() + local callCount = 0 + local value1 = "Hello World" + local value2 = 7 + + local callback = function(arg1, arg2) + expect(arg1).to.equal(value1) + expect(arg2).to.equal(value2) + callCount = callCount + 1 + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + + connection:disconnect() + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + end) + + it("should disconnect handlers", function() + local callback = function() + error("Callback was called after disconnect!") + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + connection:disconnect() + + signal:fire() + end) + + it("should fire handlers in order", function() + local signal = Signal.new() + local x = 0 + local y = 0 + + local callback1 = function() + expect(x).to.equal(0) + expect(y).to.equal(0) + x = x + 1 + end + + local callback2 = function() + expect(x).to.equal(1) + expect(y).to.equal(0) + y = y + 1 + end + + signal:connect(callback1) + signal:connect(callback2) + signal:fire() + + expect(x).to.equal(1) + expect(y).to.equal(1) + end) + + it("should continue firing despite mid-event disconnection", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionA + connectionA = signal:connect(function() + connectionA:disconnect() + countA = countA + 1 + end) + + signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(1) + end) + + it("should skip listeners that were disconnected during event evaluation", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionB + + signal:connect(function() + countA = countA + 1 + connectionB:disconnect() + end) + + connectionB = signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(0) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoduxImpl/Store.lua b/Client2018/content/LuaPackages/RoduxImpl/Store.lua new file mode 100644 index 0000000..90aa02f --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/Store.lua @@ -0,0 +1,131 @@ +local RunService = game:GetService("RunService") + +local Signal = require(script.Parent.Signal) +local NoYield = require(script.Parent.NoYield) + +local Store = {} + +-- This value is exposed as a private value so that the test code can stay in +-- sync with what event we listen to for dispatching the Changed event. +-- It may not be Heartbeat in the future. +Store._flushEvent = RunService.Heartbeat + +Store.__index = Store + +--[[ + Create a new Store whose state is transformed by the given reducer function. + + Each time an action is dispatched to the store, the new state of the store + is given by: + + state = reducer(state, action) + + Reducers do not mutate the state object, so the original state is still + valid. +]] +function Store.new(reducer, initialState, middlewares) + assert(typeof(reducer) == "function", "Bad argument #1 to Store.new, expected function.") + assert(middlewares == nil or typeof(middlewares) == "table", "Bad argument #3 to Store.new, expected nil or table.") + + local self = {} + + self._reducer = reducer + self._state = reducer(initialState, { + type = "@@INIT", + }) + self._lastState = self._state + + self._mutatedSinceFlush = false + self._connections = {} + + self.changed = Signal.new() + + setmetatable(self, Store) + + local connection = self._flushEvent:Connect(function() + self:flush() + end) + table.insert(self._connections, connection) + + if middlewares then + local unboundDispatch = self.dispatch + local dispatch = function(...) + return unboundDispatch(self, ...) + end + + for i = #middlewares, 1, -1 do + local middleware = middlewares[i] + dispatch = middleware(dispatch, self) + end + + self.dispatch = function(self, ...) + return dispatch(...) + end + end + + return self +end + +--[[ + Get the current state of the Store. Do not mutate this! +]] +function Store:getState() + return self._state +end + +--[[ + Dispatch an action to the store. This allows the store's reducer to mutate + the state of the application by creating a new copy of the state. + + Listeners on the changed event of the store are notified when the state + changes, but not necessarily on every Dispatch. +]] +function Store:dispatch(action) + if typeof(action) == "table" then + if action.type == nil then + error("action does not have a type field", 2) + end + + self._state = self._reducer(self._state, action) + self._mutatedSinceFlush = true + else + error(("actions of type %q are not permitted"):format(typeof(action)), 2) + end +end + +--[[ + Marks the store as deleted, disconnecting any outstanding connections. +]] +function Store:destruct() + for _, connection in ipairs(self._connections) do + connection:Disconnect() + end + + self._connections = nil +end + +--[[ + Flush all pending actions since the last change event was dispatched. +]] +function Store:flush() + if not self._mutatedSinceFlush then + return + end + + self._mutatedSinceFlush = false + + -- On self.changed:fire(), further actions may be immediately dispatched, in + -- which case self._lastState will be set to the most recent self._state, + -- unless we cache this value first + local state = self._state + + -- If a changed listener yields, *very* surprising bugs can ensue. + -- Because of that, changed listeners cannot yield. + NoYield(function() + self.changed:fire(state, self._lastState) + end) + + self._lastState = state +end + +return Store diff --git a/Client2018/content/LuaPackages/RoduxImpl/Store.spec.lua b/Client2018/content/LuaPackages/RoduxImpl/Store.spec.lua new file mode 100644 index 0000000..5e7a5fd --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/Store.spec.lua @@ -0,0 +1,342 @@ +return function() + local Store = require(script.Parent.Store) + + describe("new", function() + it("should instantiate with a reducer", function() + local store = Store.new(function(state, action) + return "hello, world" + end) + + expect(store).to.be.ok() + expect(store:getState()).to.equal("hello, world") + + store:destruct() + end) + + it("should instantiate with a reducer and an initial state", function() + local store = Store.new(function(state, action) + return state + end, "initial state") + + expect(store).to.be.ok() + expect(store:getState()).to.equal("initial state") + + store:destruct() + end) + + it("should instantiate with a reducer, initial state, and middlewares", function() + local store = Store.new(function(state, action) + return state + end, "initial state", {}) + + expect(store).to.be.ok() + expect(store:getState()).to.equal("initial state") + + store:destruct() + end) + + it("should modify the dispatch method when middlewares are passed", function() + local middlewareInstantiateCount = 0 + local middlewareInvokeCount = 0 + local passedDispatch + local passedStore + local passedAction + + local function reducer(state, action) + if action.type == "test" then + return "test state" + end + + return state + end + + local function testMiddleware(nextDispatch, store) + middlewareInstantiateCount = middlewareInstantiateCount + 1 + passedDispatch = nextDispatch + passedStore = store + + return function(action) + middlewareInvokeCount = middlewareInvokeCount + 1 + passedAction = action + + nextDispatch(action) + end + end + + local store = Store.new(reducer, "initial state", { testMiddleware }) + + expect(middlewareInstantiateCount).to.equal(1) + expect(middlewareInvokeCount).to.equal(0) + expect(passedDispatch).to.be.a("function") + expect(passedStore).to.equal(store) + + store:dispatch({ + type = "test", + }) + + expect(middlewareInstantiateCount).to.equal(1) + expect(middlewareInvokeCount).to.equal(1) + expect(passedAction.type).to.equal("test") + + store:flush() + + expect(store:getState()).to.equal("test state") + + store:destruct() + end) + + it("should execute middleware left-to-right", function() + local events = {} + + local function reducer(state) + return state + end + + local function middlewareA(nextDispatch, store) + table.insert(events, "instantiate a") + return function(action) + table.insert(events, "execute a") + return nextDispatch(action) + end + end + + local function middlewareB(nextDispatch, store) + table.insert(events, "instantiate b") + return function(action) + table.insert(events, "execute b") + return nextDispatch(action) + end + end + + local store = Store.new(reducer, 5, { middlewareA, middlewareB }) + + expect(#events).to.equal(2) + expect(events[1]).to.equal("instantiate b") + expect(events[2]).to.equal("instantiate a") + + store:dispatch({ + type = "test", + }) + + expect(#events).to.equal(4) + expect(events[3]).to.equal("execute a") + expect(events[4]).to.equal("execute b") + end) + + it("should send an initial action with a 'type' field", function() + local lastAction + local callCount = 0 + + local store = Store.new(function(state, action) + lastAction = action + callCount = callCount + 1 + + return state + end) + + expect(callCount).to.equal(1) + expect(lastAction).to.be.a("table") + expect(lastAction.type).to.be.ok() + + store:destruct() + end) + end) + + describe("getState", function() + it("should get the current state", function() + local store = Store.new(function(state, action) + return "foo" + end) + + local state = store:getState() + + expect(state).to.equal("foo") + + store:destruct() + end) + end) + + describe("dispatch", function() + it("should be sent through the reducer", function() + local store = Store.new(function(state, action) + state = state or "foo" + + if action.type == "act" then + return "bar" + end + + return state + end) + + expect(store).to.be.ok() + expect(store:getState()).to.equal("foo") + + store:dispatch({ + type = "act", + }) + + store:flush() + + expect(store:getState()).to.equal("bar") + + store:destruct() + end) + + it("should trigger the changed event after a flush", function() + local store = Store.new(function(state, action) + state = state or 0 + + if action.type == "increment" then + return state + 1 + end + + return state + end) + + local callCount = 0 + + store.changed:connect(function(state, oldState) + expect(oldState).to.equal(0) + expect(state).to.equal(1) + + callCount = callCount + 1 + end) + + store:dispatch({ + type = "increment", + }) + + store:flush() + + expect(callCount).to.equal(1) + + store:destruct() + end) + + it("should handle actions dispatched within the changed event", function() + local store = Store.new(function(state, action) + state = state or { + value = 0, + } + + if action.type == "increment" then + return { + value = state.value + 1, + } + elseif action.type == "decrement" then + return { + value = state.value - 1, + } + end + + return state + end) + + local changeCount = 0 + + store.changed:connect(function(state, oldState) + expect(state).never.to.equal(oldState) + + if state.value > 0 then + store:dispatch({ + type = "decrement", + }) + end + + changeCount = changeCount + 1 + end) + + store:dispatch({ + type = "increment", + }) + store:flush() + store:flush() + + expect(changeCount).to.equal(2) + + store:destruct() + end) + + it("should prevent yielding from changed handler", function() + local preCount = 0 + local postCount = 0 + + local store = Store.new(function(state, action) + state = state or 0 + return state + 1 + end) + + store.changed:connect(function(state, oldState) + preCount = preCount + 1 + wait() + postCount = postCount + 1 + end) + + store:dispatch({ + type = "increment", + }) + + expect(function() + store:flush() + end).to.throw() + + expect(preCount).to.equal(1) + expect(postCount).to.equal(0) + + store:destruct() + end) + + it("should throw if an action is dispatched without a type field", function() + local store = Store.new(function(state, action) + return state + end) + + expect(function() + store:dispatch({}) + end).to.throw() + + store:destruct() + end) + + it("should throw if the action is not a function or table", function() + local store = Store.new(function(state, action) + return state + end) + + expect(function() + store:dispatch(1) + end).to.throw() + + store:destruct() + end) + end) + + describe("flush", function() + it("should not fire a changed event if there were no dispatches", function() + local store = Store.new(function() + end) + + local count = 0 + store.changed:connect(function() + count = count + 1 + end) + + store:flush() + + expect(count).to.equal(0) + + store:dispatch({ + type = "increment", + }) + store:flush() + + expect(count).to.equal(1) + + store:flush() + + expect(count).to.equal(1) + + store:destruct() + end) + end) +end diff --git a/Client2018/content/LuaPackages/RoduxImpl/combineReducers.lua b/Client2018/content/LuaPackages/RoduxImpl/combineReducers.lua new file mode 100644 index 0000000..f3023c4 --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/combineReducers.lua @@ -0,0 +1,22 @@ +--[[ + Create a composite reducer from a map of keys and sub-reducers. +]] +local function combineReducers(map) + return function(state, action) + -- If state is nil, substitute it with a blank table. + if state == nil then + state = {} + end + + local newState = {} + + for key, reducer in pairs(map) do + -- Each reducer gets its own state, not the entire state table + newState[key] = reducer(state[key], action) + end + + return newState + end +end + +return combineReducers diff --git a/Client2018/content/LuaPackages/RoduxImpl/combineReducers.spec.lua b/Client2018/content/LuaPackages/RoduxImpl/combineReducers.spec.lua new file mode 100644 index 0000000..a3a85af --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/combineReducers.spec.lua @@ -0,0 +1,52 @@ +return function() + local combineReducers = require(script.Parent.combineReducers) + + it("should invoke each sub-reducer for every action", function() + local aCount = 0 + local bCount = 0 + + local reducer = combineReducers({ + a = function(state, action) + aCount = aCount + 1 + end, + b = function(state, action) + bCount = bCount + 1 + end, + }) + + -- Mock reducer invocation + reducer({}, {}) + expect(aCount).to.equal(1) + expect(bCount).to.equal(1) + end) + + it("should assign each sub-reducer's value to the new state", function() + local reducer = combineReducers({ + a = function(state, action) + return (state or 0) + 1 + end, + b = function(state, action) + return (state or 0) + 3 + end, + }) + + local newState = reducer({}, {}) + expect(newState.a).to.equal(1) + expect(newState.b).to.equal(3) + end) + + it("should not throw when state is nil", function() + local reducer = combineReducers({ + a = function(state, action) + return (state or 0) + 1 + end, + b = function(state, action) + return (state or 0) + 3 + end, + }) + + expect(function() + reducer(nil, {}) + end).to.never.throw() + end) +end diff --git a/Client2018/content/LuaPackages/RoduxImpl/createReducer.lua b/Client2018/content/LuaPackages/RoduxImpl/createReducer.lua new file mode 100644 index 0000000..ffb2683 --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/createReducer.lua @@ -0,0 +1,15 @@ +return function(initialState, handlers) + return function(state, action) + if state == nil then + return initialState + end + + local handler = handlers[action.type] + + if handler then + return handler(state, action) + end + + return state + end +end diff --git a/Client2018/content/LuaPackages/RoduxImpl/createReducer.spec.lua b/Client2018/content/LuaPackages/RoduxImpl/createReducer.spec.lua new file mode 100644 index 0000000..7d5ff23 --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/createReducer.spec.lua @@ -0,0 +1,79 @@ +return function() + local createReducer = require(script.Parent.createReducer) + + it("should handle actions", function() + local reducer = createReducer({ + a = 0, + b = 0, + }, { + a = function(state, action) + return { + a = state.a + 1, + b = state.b, + } + end, + b = function(state, action) + return { + a = state.a, + b = state.b + 2, + } + end, + }) + + local newState = reducer({ + a = 0, + b = 0, + }, { + type = "a", + }) + + expect(newState.a).to.equal(1) + + newState = reducer(newState, { + type = "b", + }) + + expect(newState.b).to.equal(2) + end) + + it("should return the initial state if the state is nil", function() + local reducer = createReducer({ + a = 0, + b = 0, + -- We don't care about the actions here + }, {}) + + local newState = reducer(nil, {}) + expect(newState).to.be.ok() + expect(newState.a).to.equal(0) + expect(newState.b).to.equal(0) + end) + + it("should return the same state if the action is not handled", function() + local initialState = { + a = 0, + b = 0, + } + + local reducer = createReducer(initialState, { + a = function(state, action) + return { + a = state.a + 1, + b = state.b, + } + end, + b = function(state, action) + return { + a = state.a, + b = state.b + 2, + } + end, + }) + + local newState = reducer(initialState, { + type = "c", + }) + + expect(newState).to.equal(initialState) + end) +end diff --git a/Client2018/content/LuaPackages/RoduxImpl/init.lua b/Client2018/content/LuaPackages/RoduxImpl/init.lua new file mode 100644 index 0000000..acef1df --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/init.lua @@ -0,0 +1,13 @@ +local Store = require(script.Store) +local createReducer = require(script.createReducer) +local combineReducers = require(script.combineReducers) +local loggerMiddleware = require(script.loggerMiddleware) +local thunkMiddleware = require(script.thunkMiddleware) + +return { + Store = Store, + createReducer = createReducer, + combineReducers = combineReducers, + loggerMiddleware = loggerMiddleware.middleware, + thunkMiddleware = thunkMiddleware, +} diff --git a/Client2018/content/LuaPackages/RoduxImpl/init.spec.lua b/Client2018/content/LuaPackages/RoduxImpl/init.spec.lua new file mode 100644 index 0000000..14a24ef --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/init.spec.lua @@ -0,0 +1,9 @@ +return function() + describe("Rodux", function() + it("should load", function() + local Rodux = require(script.Parent) + + expect(Rodux.Store).to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/LuaPackages/RoduxImpl/loggerMiddleware.lua b/Client2018/content/LuaPackages/RoduxImpl/loggerMiddleware.lua new file mode 100644 index 0000000..9bc922b --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/loggerMiddleware.lua @@ -0,0 +1,55 @@ +local indent = " " + +local function prettyPrint(value, indentLevel) + indentLevel = indentLevel or 0 + local output = {} + + if typeof(value) == "table" then + table.insert(output, "{\n") + + for key, value in pairs(value) do + table.insert(output, indent:rep(indentLevel + 1)) + table.insert(output, tostring(key)) + table.insert(output, " = ") + + table.insert(output, prettyPrint(value, indentLevel + 1)) + table.insert(output, "\n") + end + + table.insert(output, indent:rep(indentLevel)) + table.insert(output, "}") + elseif typeof(value) == "string" then + table.insert(output, string.format("%q", value)) + table.insert(output, " (string)") + else + table.insert(output, tostring(value)) + table.insert(output, " (") + table.insert(output, typeof(value)) + table.insert(output, ")") + end + + return table.concat(output, "") +end + +-- We want to be able to override outputFunction in tests, so the shape of this +-- module is kind of unconventional. +-- +-- We fix it this weird shape in init.lua. +local loggerMiddleware = { + outputFunction = print, +} + +function loggerMiddleware.middleware(nextDispatch, store) + return function(action) + local result = nextDispatch(action) + + loggerMiddleware.outputFunction(("Action dispatched: %s\nState changed to: %s"):format( + prettyPrint(action), + prettyPrint(store:getState()) + )) + + return result + end +end + +return loggerMiddleware diff --git a/Client2018/content/LuaPackages/RoduxImpl/loggerMiddleware.spec.lua b/Client2018/content/LuaPackages/RoduxImpl/loggerMiddleware.spec.lua new file mode 100644 index 0000000..2ec3ea3 --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/loggerMiddleware.spec.lua @@ -0,0 +1,39 @@ +return function() + local Store = require(script.Parent.Store) + local loggerMiddleware = require(script.Parent.loggerMiddleware) + + it("should print whenever an action is dispatched", function() + local outputCount = 0 + local outputMessage + + local function reducer(state, action) + return state + end + + local store = Store.new(reducer, { + fooValue = 12345, + barValue = { + bazValue = "hiBaz", + }, + }, { loggerMiddleware.middleware }) + + loggerMiddleware.outputFunction = function(message) + outputCount = outputCount + 1 + outputMessage = message + end + + store:dispatch({ + type = "testActionType", + }) + + expect(outputCount).to.equal(1) + expect(outputMessage:find("testActionType")).to.be.ok() + expect(outputMessage:find("fooValue")).to.be.ok() + expect(outputMessage:find("12345")).to.be.ok() + expect(outputMessage:find("barValue")).to.be.ok() + expect(outputMessage:find("bazValue")).to.be.ok() + expect(outputMessage:find("hiBaz")).to.be.ok() + + loggerMiddleware.outputFunction = print + end) +end diff --git a/Client2018/content/LuaPackages/RoduxImpl/thunkMiddleware.lua b/Client2018/content/LuaPackages/RoduxImpl/thunkMiddleware.lua new file mode 100644 index 0000000..08c676b --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/thunkMiddleware.lua @@ -0,0 +1,17 @@ +--[[ + A middleware that allows for functions to be dispatched. + Functions will receive a single argument, the store itself. + This middleware consumes the function; middleware further down the chain + will not receive it. +]] +local function thunkMiddleware(nextDispatch, store) + return function(action) + if typeof(action) == "function" then + return action(store) + else + return nextDispatch(action) + end + end +end + +return thunkMiddleware diff --git a/Client2018/content/LuaPackages/RoduxImpl/thunkMiddleware.spec.lua b/Client2018/content/LuaPackages/RoduxImpl/thunkMiddleware.spec.lua new file mode 100644 index 0000000..8f717e4 --- /dev/null +++ b/Client2018/content/LuaPackages/RoduxImpl/thunkMiddleware.spec.lua @@ -0,0 +1,58 @@ +return function() + local Store = require(script.Parent.Store) + local thunkMiddleware = require(script.Parent.thunkMiddleware) + + it("should dispatch thunks", function() + local function reducer(state, action) + return state + end + + local store = Store.new(reducer, {}, { thunkMiddleware }) + local thunkCount = 0 + + local function thunk(store) + thunkCount = thunkCount + 1 + end + + store:dispatch(thunk) + + expect(thunkCount).to.equal(1) + end) + + it("should allow normal actions to pass through", function() + local reducerCount = 0 + + local function reducer(state, action) + reducerCount = reducerCount + 1 + return state + end + + local store = Store.new(reducer, {}, { thunkMiddleware }) + + store:dispatch({ + type = "test", + }) + + -- Reducer will be invoked twice: + -- Once when creating the store (@@INIT action) + -- Once when the test action is dispatched + expect(reducerCount).to.equal(2) + end) + + it("should return the value from the thunk", function() + local function reducer(state, action) + return state + end + + local store = Store.new(reducer, {}, { thunkMiddleware }) + local thunkValue = "test" + + local function thunk(store) + return thunkValue + end + + local result = store:dispatch(thunk) + + expect(result).to.equal(thunkValue) + end) +end diff --git a/Client2018/content/LuaPackages/TestEZ.lua b/Client2018/content/LuaPackages/TestEZ.lua new file mode 100644 index 0000000..d2e566f --- /dev/null +++ b/Client2018/content/LuaPackages/TestEZ.lua @@ -0,0 +1,7 @@ +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.TestEZImpl) + +return require(CorePackages.TestEZImpl) \ No newline at end of file diff --git a/Client2018/content/LuaPackages/TestEZImpl/Expectation.lua b/Client2018/content/LuaPackages/TestEZImpl/Expectation.lua new file mode 100644 index 0000000..3909709 --- /dev/null +++ b/Client2018/content/LuaPackages/TestEZImpl/Expectation.lua @@ -0,0 +1,220 @@ +--[[ + Allows creation of expectation statements designed for behavior-driven + testing (BDD). See Chai (JS) or RSpec (Ruby) for examples of other BDD + frameworks. + + The Expectation class is exposed to tests as a function called `expect`: + + expect(5).to.equal(5) + expect(foo()).to.be.ok() + + Expectations can be negated using .never: + + expect(true).never.to.equal(false) + + Expectations throw errors when their conditions are not met. +]] + +local Expectation = {} + +--[[ + These keys don't do anything except make expectations read more cleanly +]] +local SELF_KEYS = { + to = true, + be = true, + been = true, + have = true, + was = true, + at = true, +} + +--[[ + These keys invert the condition expressed by the Expectation. +]] +local NEGATION_KEYS = { + never = true, +} + +--[[ + Extension of Lua's 'assert' that lets you specify an error level. +]] +local function assertLevel(condition, message, level) + message = message or "Assertion failed!" + level = level or 1 + + if not condition then + error(message, level + 1) + end +end + +--[[ + Returns a version of the given method that can be called with either . or : +]] +local function bindSelf(self, method) + return function(firstArg, ...) + if (firstArg == self) then + return method(self, ...) + else + return method(self, firstArg, ...) + end + end +end + +local function formatMessage(result, trueMessage, falseMessage) + if result then + return trueMessage + else + return falseMessage + end +end + +--[[ + Create a new expectation +]] +function Expectation.new(value) + local self = { + value = value, + successCondition = true, + condition = false + } + + setmetatable(self, Expectation) + + self.a = bindSelf(self, self.a) + self.an = self.a + self.ok = bindSelf(self, self.ok) + self.equal = bindSelf(self, self.equal) + self.throw = bindSelf(self, self.throw) + self.called = bindSelf(self, self.called) + + return self +end + +function Expectation.__index(self, key) + -- Keys that don't do anything except improve readability + if SELF_KEYS[key] then + return self + end + + -- Invert your assertion + if NEGATION_KEYS[key] then + local newExpectation = Expectation.new(self.value) + newExpectation.successCondition = not self.successCondition + + return newExpectation + end + + -- Fall back to methods provided by Expectation + return Expectation[key] +end + +--[[ + Called by expectation terminators to reset modifiers in a statement. + + This makes chains like: + + expect(5) + .never.to.equal(6) + .to.equal(5) + + Work as expected. +]] +function Expectation:_resetModifiers() + self.successCondition = true +end + +--[[ + Assert that the expectation value is the given type. + + expect(5).to.be.a("number") +]] +function Expectation:a(typeName) + local result = (type(self.value) == typeName) == self.successCondition + + local message = formatMessage(self.successCondition, + ("Expected value of type %q, got value %q of type %s"):format( + typeName, + tostring(self.value), + type(self.value) + ), + ("Expected value not of type %q, got value %q of type %s"):format( + typeName, + tostring(self.value), + type(self.value) + ) + ) + + assertLevel(result, message, 3) + self:_resetModifiers() + + return self +end + +--[[ + Assert that our expectation value is truthy +]] +function Expectation:ok() + local result = (self.value ~= nil) == self.successCondition + + local message = formatMessage(self.successCondition, + ("Expected value %q to be non-nil"):format( + tostring(self.value) + ), + ("Expected value %q to be nil"):format( + tostring(self.value) + ) + ) + + assertLevel(result, message, 3) + self:_resetModifiers() + + return self +end + +--[[ + Assert that our expectation value is equal to another value +]] +function Expectation:equal(otherValue) + local result = (self.value == otherValue) == self.successCondition + + local message = formatMessage(self.successCondition, + ("Expected value %q (%s), got %q (%s) instead"):format( + tostring(otherValue), + type(otherValue), + tostring(self.value), + type(self.value) + ), + ("Expected anything but value %q (%s)"):format( + tostring(otherValue), + type(otherValue) + ) + ) + + assertLevel(result, message, 3) + self:_resetModifiers() + + return self +end + +--[[ + Assert that our functoid expectation value throws an error when called +]] +function Expectation:throw() + local ok, err = pcall(self.value) + local result = ok ~= self.successCondition + + local message = formatMessage(self.successCondition, + ("Expected function to succeed, but it threw an error: %s"):format( + tostring(err) + ), + "Expected function to throw an error, but it did not." + ) + + assertLevel(result, message, 3) + self:_resetModifiers() + + return self +end + +return Expectation \ No newline at end of file diff --git a/Client2018/content/LuaPackages/TestEZImpl/Reporters/TextReporter.lua b/Client2018/content/LuaPackages/TestEZImpl/Reporters/TextReporter.lua new file mode 100644 index 0000000..0521c2d --- /dev/null +++ b/Client2018/content/LuaPackages/TestEZImpl/Reporters/TextReporter.lua @@ -0,0 +1,100 @@ +--[[ + The TextReporter uses the results from a completed test to output text to + standard output and TestService. +]] + +local TestService = game:GetService("TestService") + +local TestEnum = require(script.Parent.Parent.TestEnum) + +local INDENT = (" "):rep(3) +local STATUS_SYMBOLS = { + [TestEnum.TestStatus.Success] = "+", + [TestEnum.TestStatus.Failure] = "-", + [TestEnum.TestStatus.Skipped] = "~" +} +local UNKNOWN_STATUS_SYMBOL = "?" + +local TextReporter = {} + +local function reportNode(node, buffer, level) + buffer = buffer or {} + level = level or 0 + + if node.status == TestEnum.TestStatus.Skipped then + return buffer + end + + local line + + if node.status then + local symbol = STATUS_SYMBOLS[node.status] or UNKNOWN_STATUS_SYMBOL + + line = ("%s[%s] %s"):format( + INDENT:rep(level), + symbol, + node.planNode.phrase + ) + else + line = ("%s%s"):format( + INDENT:rep(level), + node.planNode.phrase + ) + end + + table.insert(buffer, line) + + for _, child in ipairs(node.children) do + reportNode(child, buffer, level + 1) + end + + return buffer +end + +local function reportRoot(node) + local buffer = {} + + for _, child in ipairs(node.children) do + reportNode(child, buffer, 0) + end + + return buffer +end + +local function report(root) + local buffer = reportRoot(root) + + return table.concat(buffer, "\n") +end + +function TextReporter.report(results) + local resultBuffer = { + "Test results:", + report(results), + ("%d passed, %d failed, %d skipped"):format( + results.successCount, + results.failureCount, + results.skippedCount + ) + } + + print(table.concat(resultBuffer, "\n")) + + if results.failureCount > 0 then + print(("%d test nodes reported failures."):format(results.failureCount)) + end + + if #results.errors > 0 then + print("Errors reported by tests:") + print("") + + for _, message in ipairs(results.errors) do + TestService:Error(message) + + -- Insert a blank line after each error + print("") + end + end +end + +return TextReporter \ No newline at end of file diff --git a/Client2018/content/LuaPackages/TestEZImpl/Stack.lua b/Client2018/content/LuaPackages/TestEZImpl/Stack.lua new file mode 100644 index 0000000..09c2f9c --- /dev/null +++ b/Client2018/content/LuaPackages/TestEZImpl/Stack.lua @@ -0,0 +1,38 @@ +local Stack = {} +Stack.__index = Stack + +function Stack.new() + local self = {} + setmetatable(self, Stack) + self.data = {} + return self +end + +function Stack:size() + return #self.data +end + +function Stack:push(obj) + self.data[self:size()+1] = obj + return self +end + +function Stack:pop() + local result = self:getBack() + table.remove(self.data,self:size()) + return result +end + +function Stack:getBack() + if self:size() == 0 then error("stack is empty") end + local result = self.data[self:size()] + return result +end + +function Stack:setBack(obj) + if self:size() == 0 then error("stack is empty") end + self.data[self:size()] = obj + return self +end + +return Stack \ No newline at end of file diff --git a/Client2018/content/LuaPackages/TestEZImpl/TestBootstrap.lua b/Client2018/content/LuaPackages/TestEZImpl/TestBootstrap.lua new file mode 100644 index 0000000..a413915 --- /dev/null +++ b/Client2018/content/LuaPackages/TestEZImpl/TestBootstrap.lua @@ -0,0 +1,114 @@ +--[[ + Provides an interface to quickly run and report tests from a given object. +]] + +local TestPlanner = require(script.Parent.TestPlanner) +local TestRunner = require(script.Parent.TestRunner) +local TextReporter = require(script.Parent.Reporters.TextReporter) + +local TestBootstrap = {} + +local function stripSpecSuffix(name) + return (name:gsub("%.spec$", "")) +end + +local function getPath(module, root) + root = root or game + + local path = {} + local last = module + + while last ~= nil and last ~= root do + table.insert(path, stripSpecSuffix(last.Name)) + last = last.Parent + end + + return path +end + +--[[ + Find all the ModuleScripts in this tree that are tests. +]] +function TestBootstrap:getModules(root, modules, current) + modules = modules or {} + current = current or root + + for _, child in ipairs(current:GetChildren()) do + if child:IsA("ModuleScript") and child.Name:match("%.spec$") then + local method = require(child) + local path = getPath(child, root) + + table.insert(modules, { + method = method, + path = path + }) + else + self:getModules(root, modules, child) + end + end + + table.sort(modules, function(a, b) + return a.path[#a.path]:lower() < b.path[#b.path]:lower() + end) + + return modules +end + +--[[ + Runs all test and reports the results using the given test reporter. + + If no reporter is specified, a reasonable default is provided. + + This function demonstrates the expected workflow with this testing system: + 1. Locate test modules + 2. Generate test plan + 3. Run test plan + 4. Report test results + + This means we could hypothetically present a GUI to the developer that shows + the test plan before we execute it, allowing them to toggle specific tests + before they're run, but after they've been identified! +]] +function TestBootstrap:run(root, reporter, showTimingInfo, noXpcallByDefault) + noXpcallByDefault = noXpcallByDefault or false + if not root then + error("You must provide a root object to search for tests in!", 2) + end + + reporter = reporter or TextReporter + + local startTime = tick() + + local modules + if type(root) == "function" then + modules = {{method = root, path = {}}} + else + modules = self:getModules(root) + end + + local afterModules = tick() + + local plan = TestPlanner.createPlan(modules, noXpcallByDefault) + local afterPlan = tick() + + local results = TestRunner.runPlan(plan) + local afterRun = tick() + + reporter.report(results) + local afterReport = tick() + + if showTimingInfo then + local timing = { + ("Took %f seconds to locate test modules"):format(afterModules - startTime), + ("Took %f seconds to create test plan"):format(afterPlan - afterModules), + ("Took %f seconds to run tests"):format(afterRun - afterPlan), + ("Took %f seconds to report tests"):format(afterReport - afterRun), + } + + print(table.concat(timing, "\n")) + end + + return results +end + +return TestBootstrap \ No newline at end of file diff --git a/Client2018/content/LuaPackages/TestEZImpl/TestEnum.lua b/Client2018/content/LuaPackages/TestEZImpl/TestEnum.lua new file mode 100644 index 0000000..68fd276 --- /dev/null +++ b/Client2018/content/LuaPackages/TestEZImpl/TestEnum.lua @@ -0,0 +1,25 @@ +--[[ + Constants used throughout the testing framework. +]] + +local TestEnum = {} + +TestEnum.TestStatus = { + Success = "Success", + Failure = "Failure", + Skipped = "Skipped" +} + +TestEnum.NodeType = { + Try = "Try", + Describe = "Describe", + It = "It" +} + +TestEnum.NodeModifier = { + None = "None", + Skip = "Skip", + Focus = "Focus" +} + +return TestEnum \ No newline at end of file diff --git a/Client2018/content/LuaPackages/TestEZImpl/TestPlan.lua b/Client2018/content/LuaPackages/TestEZImpl/TestPlan.lua new file mode 100644 index 0000000..237a07c --- /dev/null +++ b/Client2018/content/LuaPackages/TestEZImpl/TestPlan.lua @@ -0,0 +1,102 @@ +--[[ + Represents a tree of tests that have been loaded but not necessarily + executed yet. + + TestPlan objects are produced by TestPlanner and TestPlanBuilder. +]] + +local TestEnum = require(script.Parent.TestEnum) + +local TestPlan = {} + +TestPlan.__index = TestPlan + +--[[ + Create a new, empty TestPlan. +]] +function TestPlan.new() + local self = { + children = {} + } + + setmetatable(self, TestPlan) + + return self +end + +--[[ + Calls the given callback on all nodes in the tree, traversed depth-first. +]] +function TestPlan:visitAllNodes(callback, root) + root = root or self + + for _, child in ipairs(root.children) do + callback(child) + + self:visitAllNodes(callback, child) + end +end + +--[[ + Creates a new node that would be suitable to insert into the TestPlan. +]] +function TestPlan.createNode(phrase, nodeType, nodeModifier) + nodeModifier = nodeModifier or TestEnum.NodeModifier.None + + local node = { + phrase = phrase, + type = nodeType, + modifier = nodeModifier, + children = {}, + callback = nil + } + + return node +end + +--[[ + Visualizes the test plan in a simple format, suitable for debugging the test + plan's structure. +]] +function TestPlan:visualize(root, level) + root = root or self + level = level or 0 + + local buffer = {} + + for _, child in ipairs(root.children) do + if child.type == TestEnum.NodeType.It then + table.insert(buffer, (" "):rep(3 * level) .. child.phrase) + else + table.insert(buffer, (" "):rep(3 * level) .. child.phrase) + end + + if #child.children > 0 then + local text = self:visualize(child, level + 1) + table.insert(buffer, text) + end + end + + return table.concat(buffer, "\n") +end + +--[[ + Gets a list of all nodes in the tree for which the given callback returns + true. +]] +function TestPlan:findNodes(callback, results, node) + node = node or self + results = results or {} + + for _, childNode in ipairs(node.children) do + if callback(childNode) then + table.insert(results, childNode) + end + + self:findNodes(callback, results, childNode) + end + + return results +end + +return TestPlan \ No newline at end of file diff --git a/Client2018/content/LuaPackages/TestEZImpl/TestPlanBuilder.lua b/Client2018/content/LuaPackages/TestEZImpl/TestPlanBuilder.lua new file mode 100644 index 0000000..015f09c --- /dev/null +++ b/Client2018/content/LuaPackages/TestEZImpl/TestPlanBuilder.lua @@ -0,0 +1,87 @@ +--[[ + Represents the ephermal state used for building a TestPlan from some other + representation. + + TestPlanBuilder keeps track of a stack of nodes that represents the current + position in the hierarchy, allowing the consumer to move up and down the + tree as new nodes are discovered. +]] + +local TestPlan = require(script.Parent.TestPlan) + +local TestPlanBuilder = {} + +TestPlanBuilder.__index = TestPlanBuilder + +--[[ + Create a new TestPlanBuilder, used for creating a TestPlan. +]] +function TestPlanBuilder.new() + local self = { + plan = TestPlan.new(), + nodeStack = {}, + noXpcallByDefault = false, + } + + setmetatable(self, TestPlanBuilder) + + return self +end + +--[[ + Verify that the TestPlanBuilder's state is valid and get a TestPlan from it. +]] +function TestPlanBuilder:finalize() + if #self.nodeStack ~= 0 then + error("Cannot finalize a TestPlan with nodes still on the stack!", 2) + end + + return self.plan +end + +--[[ + Grab the current node being worked on by the TestPlanBuilder. +]] +function TestPlanBuilder:getCurrentNode() + return self.nodeStack[#self.nodeStack] or self.plan +end + +--[[ + Creates and pushes a node onto the navigation stack. +]] +function TestPlanBuilder:pushNode(phrase, nodeType, nodeModifier) + local lastNode = self.nodeStack[#self.nodeStack] or self.plan + + -- Find an existing node with this phrase to use + local useNode + for _, child in ipairs(lastNode.children) do + if child.phrase == phrase then + useNode = child + break + end + end + + -- Didn't find one, create a new node + if not useNode then + useNode = TestPlan.createNode(phrase, nodeType, nodeModifier) + useNode.parent = lastNode + + table.insert(lastNode.children, useNode) + end + + table.insert(self.nodeStack, useNode) + + useNode.HACK_NO_XPCALL = self.noXpcallByDefault + + return useNode +end + +--[[ + Pops a node off of the node navigation stack. +]] +function TestPlanBuilder:popNode() + assert(#self.nodeStack > 0, "Tried to pop from an empty node stack!") + return table.remove(self.nodeStack, #self.nodeStack) +end + +return TestPlanBuilder \ No newline at end of file diff --git a/Client2018/content/LuaPackages/TestEZImpl/TestPlanner.lua b/Client2018/content/LuaPackages/TestEZImpl/TestPlanner.lua new file mode 100644 index 0000000..7c58bb5 --- /dev/null +++ b/Client2018/content/LuaPackages/TestEZImpl/TestPlanner.lua @@ -0,0 +1,168 @@ +--[[ + Turns a series of specification functions into a test plan. + + Uses a TestPlanBuilder to keep track of the state of the tree being built. +]] + +local TestEnum = require(script.Parent.TestEnum) +local TestPlanBuilder = require(script.Parent.TestPlanBuilder) + +local TestPlanner = {} + +local function buildPlan(builder, module, env) + local currentEnv = getfenv(module.method) + + for key, value in pairs(env) do + currentEnv[key] = value + end + + local nodeCount = #module.path + + -- Dive into auto-named nodes for this module + for i = nodeCount, 1, -1 do + local name = module.path[i] + builder:pushNode(name, TestEnum.NodeType.Describe) + end + + local ok, err = xpcall(module.method, function(err) + return err .. "\n" .. debug.traceback() + end) + + -- This is an error outside of any describe/it blocks. + -- We attach it to the node we generate automatically per-file. + if not ok then + local node = builder:getCurrentNode() + node.loadError = err + end + + -- Back out of auto-named nodes + for _ = 1, nodeCount do + builder:popNode() + end +end + +--[[ + Create a new environment with functions for defining the test plan structure + using the given TestPlanBuilder. + + These functions illustrate the advantage of the stack-style tree navigation + as state doesn't need to be passed around between functions or explicitly + global. +]] +function TestPlanner.createEnvironment(builder) + local env = {} + + function env.describe(phrase, callback) + local node = builder:pushNode(phrase, TestEnum.NodeType.Describe) + + local ok, err = pcall(callback) + + -- loadError on a TestPlan node is an automatic failure + if not ok then + node.loadError = err + end + + builder:popNode() + end + + function env.try(phrase, callback) + local node = builder:pushNode(phrase, TestEnum.NodeType.Try) + + local ok, err = pcall(callback) + + -- loadError on a TestPlan node is an automatic failure + if not ok then + node.loadError = err + end + + builder:popNode() + end + + function env.it(phrase, callback) + local node = builder:pushNode(phrase, TestEnum.NodeType.It) + + node.callback = callback + + builder:popNode() + end + + function env.itFOCUS(phrase, callback) + local node = builder:pushNode(phrase, TestEnum.NodeType.It, TestEnum.NodeModifier.Focus) + + node.callback = callback + + builder:popNode() + end + + function env.itSKIP(phrase, callback) + local node = builder:pushNode(phrase, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip) + + node.callback = callback + + builder:popNode() + end + + function env.FOCUS() + local currentNode = builder:getCurrentNode() + + currentNode.modifier = TestEnum.NodeModifier.Focus + end + + function env.SKIP() + local currentNode = builder:getCurrentNode() + + currentNode.modifier = TestEnum.NodeModifier.Skip + end + + --[[ + These method is intended to disable the use of xpcall when running + nodes contained in the same node that this function is called in. + This is because xpcall breaks badly if the method passed yields. + + This function is intended to be hideous and seldom called. + + Once xpcall is able to yield, this function is obsolete. + ]] + function env.HACK_NO_XPCALL() + local currentNode = builder:getCurrentNode() + + currentNode.HACK_NO_XPCALL = true + end + + env.step = env.it + + function env.include(...) + local args = {...} + local method, path + if #args == 1 then + method = args[1] + path = {} + elseif #args == 2 then + method = args[2] + path = {args[1]} + end + buildPlan(builder, {path = path, method = method}, env) + end + + return env +end + +--[[ + Create a new TestPlan from a list of specification functions. + + These functions should call a combination of `describe` and `it` (and their + variants), which will be turned into a test plan to be executed. +]] +function TestPlanner.createPlan(specFunctions, noXpcallByDefault) + local builder = TestPlanBuilder.new() + builder.noXpcallByDefault = noXpcallByDefault + local env = TestPlanner.createEnvironment(builder) + + for _, module in ipairs(specFunctions) do + buildPlan(builder, module, env) + end + + return builder:finalize() +end + +return TestPlanner \ No newline at end of file diff --git a/Client2018/content/LuaPackages/TestEZImpl/TestResults.lua b/Client2018/content/LuaPackages/TestEZImpl/TestResults.lua new file mode 100644 index 0000000..ab8924c --- /dev/null +++ b/Client2018/content/LuaPackages/TestEZImpl/TestResults.lua @@ -0,0 +1,112 @@ +--[[ + Represents a tree of test results. + + Each node in the tree corresponds directly to a node in a corresponding + TestPlan, accessible via the 'planNode' field. + + TestResults objects are produced by TestRunner using TestSession as state. +]] + +local TestEnum = require(script.Parent.TestEnum) + +local STATUS_SYMBOLS = { + [TestEnum.TestStatus.Success] = "+", + [TestEnum.TestStatus.Failure] = "-", + [TestEnum.TestStatus.Skipped] = "~" +} + +local TestResults = {} + +TestResults.__index = TestResults + +--[[ + Create a new TestResults tree that's linked to the given TestPlan. +]] +function TestResults.new(plan) + local self = { + successCount = 0, + failureCount = 0, + skippedCount = 0, + planNode = plan, + children = {}, + errors = {} + } + + setmetatable(self, TestResults) + + return self +end + +--[[ + Create a new result node that can be inserted into a TestResult tree. +]] +function TestResults.createNode(planNode) + local node = { + planNode = planNode, + children = {}, + errors = {}, + status = nil + } + + return node +end + +--[[ + Visit all test result nodes, depth-first. +]] +function TestResults:visitAllNodes(callback, root) + root = root or self + + for _, child in ipairs(root.children) do + callback(child) + + self:visitAllNodes(callback, child) + end +end + +--[[ + Creates a debug visualization of the test results. +]] +function TestResults:visualize(root, level) + root = root or self + level = level or 0 + + local buffer = {} + + for _, child in ipairs(root.children) do + if child.planNode.type == TestEnum.NodeType.It then + local symbol = STATUS_SYMBOLS[child.status] or "?" + local str = ("%s[%s] %s"):format( + (" "):rep(3 * level), + symbol, + child.planNode.phrase + ) + + if #child.messages > 0 then + str = str .. "\n " .. (" "):rep(3 * level) .. table.concat(child.messages, "\n " .. (" "):rep(3 * level)) + end + + table.insert(buffer, str) + else + local str = ("%s%s"):format( + (" "):rep(3 * level), + child.planNode.phrase + ) + + if child.status then + str = str .. (" (%s)"):format(child.status) + end + + table.insert(buffer, str) + + if #child.children > 0 then + local text = self:visualize(child, level + 1) + table.insert(buffer, text) + end + end + end + + return table.concat(buffer, "\n") +end + +return TestResults \ No newline at end of file diff --git a/Client2018/content/LuaPackages/TestEZImpl/TestRunner.lua b/Client2018/content/LuaPackages/TestEZImpl/TestRunner.lua new file mode 100644 index 0000000..8f7b806 --- /dev/null +++ b/Client2018/content/LuaPackages/TestEZImpl/TestRunner.lua @@ -0,0 +1,152 @@ +--[[ + Contains the logic to run a test plan and gather test results from it. + + TestRunner accepts a TestPlan object, executes the planned tests, and + produces a TestResults object. While the tests are running, the system's + state is contained inside a TestSession object. +]] + +local Expectation = require(script.Parent.Expectation) +local TestEnum = require(script.Parent.TestEnum) +local TestSession = require(script.Parent.TestSession) +local Stack = require(script.Parent.Stack) + +local RUNNING_GLOBAL = "__TESTEZ_RUNNING_TEST__" + +local TestRunner = { + environment = {} +} + +function TestRunner.environment.expect(...) + return Expectation.new(...) +end + +--[[ + Runs the given TestPlan and returns a TestResults object representing the + results of the run. +]] +function TestRunner.runPlan(plan) + local session = TestSession.new(plan) + local tryStack = Stack.new() + + local exclusiveNodes = plan:findNodes(function(node) + return node.modifier == TestEnum.NodeModifier.Focus + end) + + session.hasFocusNodes = #exclusiveNodes > 0 + + TestRunner.runPlanNode(session, plan, tryStack) + + return session:finalize() +end + +--[[ + Run the given test plan node and its descendants, using the given test + session to store all of the results. +]] +function TestRunner.runPlanNode(session, planNode, tryStack, noXpcall) + for _, childPlanNode in ipairs(planNode.children) do + local childResultNode = session:pushNode(childPlanNode) + + if childPlanNode.type == TestEnum.NodeType.It then + if session:shouldSkip() then + childResultNode.status = TestEnum.TestStatus.Skipped + else + if tryStack:size() > 0 and tryStack:getBack().isOk == false then + childResultNode.status = TestEnum.TestStatus.Failure + table.insert(childResultNode.errors, + string.format("%q failed without trying, because test case %q failed", + childPlanNode.phrase, tryStack:getBack().failedNode.phrase)) + else + -- Errors can be set either via `error` propagating upwards or + -- by a test calling fail([message]). + local success = true + local errorMessage + + local testEnvironment = getfenv(childPlanNode.callback) + + for key, value in pairs(TestRunner.environment) do + testEnvironment[key] = value + end + + testEnvironment.fail = function(message) + if message == nil then + message = "fail() was called." + end + + success = false + errorMessage = message .. "\n" .. debug.traceback() + end + + -- We prefer xpcall, but yielding doesn't work from xpcall. + -- As a workaround, you can mark nodes as "not xpcallable" + local call = noXpcall and pcall or xpcall + + -- Any code can check RUNNING_GLOBAL to fork behavior based on + -- whether a test is running. We use this to avoid accessing + -- protected APIs; it's a workaround that will go away someday. + _G[RUNNING_GLOBAL] = true + + local nodeSuccess, nodeResult = call(childPlanNode.callback, function(message) + return message .. "\n" .. debug.traceback() + end) + + _G[RUNNING_GLOBAL] = nil + + -- If a node threw an error, we prefer to use that message over + -- one created by fail() if it was set. + if not nodeSuccess then + success = false + errorMessage = nodeResult + end + + if success then + childResultNode.status = TestEnum.TestStatus.Success + else + childResultNode.status = TestEnum.TestStatus.Failure + table.insert(childResultNode.errors, errorMessage) + end + end + end + elseif childPlanNode.type == TestEnum.NodeType.Describe or childPlanNode.type == TestEnum.NodeType.Try then + if childPlanNode.type == TestEnum.NodeType.Try then tryStack:push({isOk = true, failedNode = nil}) end + TestRunner.runPlanNode(session, childPlanNode, tryStack, childPlanNode.HACK_NO_XPCALL) + if childPlanNode.type == TestEnum.NodeType.Try then tryStack:pop() end + + local status = TestEnum.TestStatus.Success + + -- Did we have an error trying build a test plan? + if childPlanNode.loadError then + status = TestEnum.TestStatus.Failure + + local message = "Error during planning: " .. childPlanNode.loadError + + table.insert(childResultNode.errors, message) + else + local skipped = true + + -- If all children were skipped, then we were skipped + -- If any child failed, then we failed! + for _, child in ipairs(childResultNode.children) do + if child.status ~= TestEnum.TestStatus.Skipped then + skipped = false + + if child.status == TestEnum.TestStatus.Failure then + status = TestEnum.TestStatus.Failure + end + end + end + + if skipped then + status = TestEnum.TestStatus.Skipped + end + end + + childResultNode.status = status + end + + session:popNode() + end +end + +return TestRunner \ No newline at end of file diff --git a/Client2018/content/LuaPackages/TestEZImpl/TestSession.lua b/Client2018/content/LuaPackages/TestEZImpl/TestSession.lua new file mode 100644 index 0000000..203cc08 --- /dev/null +++ b/Client2018/content/LuaPackages/TestEZImpl/TestSession.lua @@ -0,0 +1,149 @@ +--[[ + Represents the state relevant while executing a test plan. + + Used by TestRunner to produce a TestResults object. + + Uses the same tree building structure as TestPlanBuilder; TestSession keeps + track of a stack of nodes that represent the current path through the tree. +]] + +local TestEnum = require(script.Parent.TestEnum) +local TestResults = require(script.Parent.TestResults) + +local TestSession = {} + +TestSession.__index = TestSession + +--[[ + Create a TestSession related to the given TestPlan. + + The resulting TestResults object will be linked to this TestPlan. +]] +function TestSession.new(plan) + local self = { + results = TestResults.new(plan), + nodeStack = {}, + hasFocusNodes = false + } + + setmetatable(self, TestSession) + + return self +end + +--[[ + Calculate success, failure, and skipped test counts in the tree at the + current point in the execution. +]] +function TestSession:calculateTotals() + local results = self.results + + results.successCount = 0 + results.failureCount = 0 + results.skippedCount = 0 + + results:visitAllNodes(function(node) + local status = node.status + local nodeType = node.planNode.type + + if nodeType == TestEnum.NodeType.It then + if status == TestEnum.TestStatus.Success then + results.successCount = results.successCount + 1 + elseif status == TestEnum.TestStatus.Failure then + results.failureCount = results.failureCount + 1 + elseif status == TestEnum.TestStatus.Skipped then + results.skippedCount = results.skippedCount + 1 + end + end + end) +end + +--[[ + Gathers all of the errors reported by tests and puts them at the top level + of the TestResults object. +]] +function TestSession:gatherErrors() + local results = self.results + + results.errors = {} + + results:visitAllNodes(function(node) + if #node.errors > 0 then + for _, message in ipairs(node.errors) do + table.insert(results.errors, message) + end + end + end) +end + +--[[ + Calculates test totals, verifies the tree is valid, and returns results. +]] +function TestSession:finalize() + if #self.nodeStack ~= 0 then + error("Cannot finalize TestResults with nodes still on the stack!", 2) + end + + self:calculateTotals() + self:gatherErrors() + + return self.results +end + +--[[ + Create a new test result node and push it onto the navigation stack. +]] +function TestSession:pushNode(planNode) + local node = TestResults.createNode(planNode) + + local lastNode = self.nodeStack[#self.nodeStack] or self.results + + table.insert(lastNode.children, node) + table.insert(self.nodeStack, node) + + return node +end + +--[[ + Pops a node off of the navigation stack. +]] +function TestSession:popNode() + assert(#self.nodeStack > 0, "Tried to pop from an empty node stack!") + return table.remove(self.nodeStack, #self.nodeStack) +end + +--[[ + Tells whether the current test we're in should be skipped. +]] +function TestSession:shouldSkip() + -- If our test tree had any exclusive tests, then normal tests are skipped! + if self.hasFocusNodes then + for i = #self.nodeStack, 1, -1 do + local node = self.nodeStack[i] + + -- Skipped tests are still skipped + if node.planNode.modifier == TestEnum.NodeModifier.Skip then + return true + end + + -- Focused tests are the only ones that aren't skipped + if node.planNode.modifier == TestEnum.NodeModifier.Focus then + return false + end + end + + return true + else + for i = #self.nodeStack, 1, -1 do + local node = self.nodeStack[i] + + if node.planNode.modifier == TestEnum.NodeModifier.Skip then + return true + end + end + end + + return false +end + +return TestSession \ No newline at end of file diff --git a/Client2018/content/LuaPackages/TestEZImpl/init.lua b/Client2018/content/LuaPackages/TestEZImpl/init.lua new file mode 100644 index 0000000..b208d80 --- /dev/null +++ b/Client2018/content/LuaPackages/TestEZImpl/init.lua @@ -0,0 +1,38 @@ +local Expectation = require(script.Expectation) +local TestBootstrap = require(script.TestBootstrap) +local TestEnum = require(script.TestEnum) +local TestPlan = require(script.TestPlan) +local TestPlanBuilder = require(script.TestPlanBuilder) +local TestPlanner = require(script.TestPlanner) +local TestResults = require(script.TestResults) +local TestRunner = require(script.TestRunner) +local TestSession = require(script.TestSession) +local TextReporter = require(script.Reporters.TextReporter) + +local function run(testRoot, callback) + local modules = TestBootstrap:getModules(testRoot) + local plan = TestPlanner.createPlan(modules) + local results = TestRunner.runPlan(plan) + + callback(results) +end + +local TestEZ = { + run = run, + + Expectation = Expectation, + TestBootstrap = TestBootstrap, + TestEnum = TestEnum, + TestPlan = TestPlan, + TestPlanBuilder = TestPlanBuilder, + TestPlanner = TestPlanner, + TestResults = TestResults, + TestRunner = TestRunner, + TestSession = TestSession, + + Reporters = { + TextReporter = TextReporter, + }, +} + +return TestEZ \ No newline at end of file diff --git a/Client2018/content/LuaPackages/initify.lua b/Client2018/content/LuaPackages/initify.lua new file mode 100644 index 0000000..f6334a1 --- /dev/null +++ b/Client2018/content/LuaPackages/initify.lua @@ -0,0 +1,40 @@ +--[[ + Restructures a tree of ModuleScript objects to emulate the behavior of stock + Lua and Rojo's `init.lua` mechanism, which essentially lets you load folders + as modules. + + A file structure like this: + + foo (directory) + `-- bar (directory) + `-- init.lua (file) + + Is turned into: + + foo (Folder) + `-- bar (ModuleScript) +]] + +local function initify(rbx) + local init = rbx:FindFirstChild("init") + + if init then + init.Name = rbx.Name + init.Parent = rbx.Parent + + for _, child in ipairs(rbx:GetChildren()) do + child.Parent = init + end + + rbx:Destroy() + rbx = init + end + + for _, child in ipairs(rbx:GetChildren()) do + initify(child) + end + + return rbx +end + +return initify \ No newline at end of file diff --git a/Client2018/content/avatar/character.rbxm b/Client2018/content/avatar/character.rbxm new file mode 100644 index 0000000..248da30 --- /dev/null +++ b/Client2018/content/avatar/character.rbxm @@ -0,0 +1,952 @@ + + null + nil + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + erik.cassel + RBX1 + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 194 + + 0 + 19.5 + 22.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 1 + 1 + + + + + 2 + 2 + + 0 + Mesh + + 0 + 0 + 0 + + + 1.25 + 1.25 + 1.25 + + + + 1 + 1 + 1 + + + + + + 5 + face + rbxasset://textures/face.png + 0 + + + + + + 0 + 0.600000024 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + HairAttachment + + + + + + 0 + 0.600000024 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + HatAttachment + + + + + + 0 + 0 + -0.600000024 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + FaceFrontAttachment + + + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + FaceCenterAttachment + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 194 + + 0 + 18 + 22.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 2 + 0 + true + 256 + Torso + 0 + 0 + 0 + 2 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 2 + 1 + + + + + 5 + roblox + + 0 + + + + + + 0 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckAttachment + + + + + + 0 + 0 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyFrontAttachment + + + + + + 0 + 0 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyBackAttachment + + + + + + -1 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftCollarAttachment + + + + + + 1 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightCollarAttachment + + + + + + 0 + -1 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistFrontAttachment + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistCenterAttachment + + + + + + 0 + -1 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistBackAttachment + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 194 + + 1.5 + 18 + 22.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Left Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 1.0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderAttachment + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftGripAttachment + + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 194 + + -1.5 + 18 + 22.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Right Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 1.0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderAttachment + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightGripAttachment + + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 194 + + 0.5 + 16 + 22.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Left Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftFootAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 194 + + -0.5 + 16 + 22.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Right Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightFootAttachment + false + + + + + + 0 + 100 + 100 + 0 + 50 + 100 + 89 + Humanoid + 100 + 2 + 16 + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 194 + + 0 + 18 + 22.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 0 + 0 + true + 256 + HumanoidRootPart + 0 + 0 + 0 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 1 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 2 + 1 + + + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootAttachment + false + + + + + \ No newline at end of file diff --git a/Client2018/content/avatar/characterR15.rbxm b/Client2018/content/avatar/characterR15.rbxm new file mode 100644 index 0000000..0dc757a --- /dev/null +++ b/Client2018/content/avatar/characterR15.rbxm @@ -0,0 +1,2332 @@ + + null + nil + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + Player + RBX9909D4D409004F18956C91B88A6A32A3 + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 311 + + -8.03512478 + 2.3499999 + -12.6754742 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + HumanoidRootPart + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 1 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + 1 + 1 + + 2 + 2 + 1 + + + + + + -0 + -0 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootRigAttachment + false + + + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -9.2211628 + 2.14999962 + -11.7571526 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 0.299999982 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532219986 + LeftHand + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999762 + 0.299999982 + 0.999999881 + + + + + + 0.000478863716 + 0.149999991 + 5.96046448e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftWristRigAttachment + false + + + + + + -1.1920929e-007 + -0.149999633 + -1.46306121e-007 + 1 + 0 + -0 + 0 + 6.12323426e-017 + 1 + 0 + -1 + 6.12323426e-017 + + LeftGripAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -9.2211628 + 2.84999967 + -11.7571526 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 1.20000029 + 1 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532219991 + LeftLowerArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999762 + 1.20000029 + 1 + + + + + + 0.000478506088 + 0.25000003 + 7.64462551e-020 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftElbowRigAttachment + false + + + + + + 0.000478506088 + -0.549999952 + 7.64462551e-020 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftWristRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -9.22116375 + 3.29999995 + -11.7571526 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 1.40000033 + 0.99999994 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532219996 + LeftUpperArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999762 + 1.40000033 + 0.99999994 + + + + + + 0.250109196 + 0.449999809 + 8.94069672e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderRigAttachment + false + + + + + + 0.000479102135 + -0.200000167 + 8.94069672e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftElbowRigAttachment + false + + + + + + 2.38418579e-007 + 0.700000286 + -2.70968314e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -6.84908628 + 2.14999962 + -13.5937948 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999881 + 0.299999982 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532219997 + RightHand + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999881 + 0.299999982 + 0.999999881 + + + + + + 3.57627869e-007 + 0.149999991 + 5.96046448e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightWristRigAttachment + false + + + + + + 0 + -0.149999633 + -1.46306121e-007 + 1 + 0 + -0 + 0 + 6.12323426e-017 + 1 + 0 + -1 + 6.12323426e-017 + + RightGripAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -6.84908628 + 2.84999967 + -13.5937948 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 1.20000029 + 1 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532219999 + RightLowerArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999762 + 1.20000029 + 1 + + + + + + 1.1920929e-007 + 0.25000003 + 7.64462551e-020 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightElbowRigAttachment + false + + + + + + 1.1920929e-007 + -0.549999952 + -6.86244753e-018 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightWristRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -6.84908581 + 3.29999995 + -13.5937958 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999642 + 1.40000033 + 0.99999994 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220004 + RightUpperArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999642 + 1.40000033 + 0.99999994 + + + + + + -0.250020266 + 0.449999809 + 8.94069672e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderRigAttachment + false + + + + + + -5.96046448e-007 + -0.200000167 + 8.94069672e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightElbowRigAttachment + false + + + + + + -9.53674316e-007 + 0.700000286 + -2.70968314e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.03512478 + 3.19999981 + -12.6754742 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + true + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 2 + 1.60000014 + 1.00000036 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220007 + UpperTorso + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 2 + 1.60000014 + 1.00000036 + + + + + + -5.96046448e-008 + -0.450000018 + 1.1920929e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistRigAttachment + false + + + + + + -5.96046448e-008 + 0.799999952 + 1.1920929e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckRigAttachment + false + + + + + + -1.24989128 + 0.549999952 + 1.1920929e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderRigAttachment + false + + + + + + 1.24998045 + 0.549999952 + 1.1920929e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderRigAttachment + false + + + + + + -5.96046448e-008 + -0.200000048 + -0.499999881 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyFrontAttachment + false + + + + + + -5.96046448e-008 + -0.200000048 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyBackAttachment + false + + + + + + -0.999999881 + 0.800000191 + -7.27397378e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftCollarAttachment + false + + + + + + 0.99999994 + 0.799999952 + 4.61295997e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightCollarAttachment + false + + + + + + 0.0 + 0.8 + 0.0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.43047047 + 0.150000095 + -12.3693666 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 1 + 0.300000191 + 1 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220012 + LeftFoot + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 1 + 0.300000191 + 1 + + + + + + 0 + 0.05 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftAnkleRigAttachment + false + + + + + + 0 + -0.15 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftFootAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.43047047 + 0.950000286 + -12.3693666 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.99999994 + 1.50000036 + 1.00000012 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220017 + LeftLowerLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.99999994 + 1.50000036 + 1.00000012 + + + + + + -0 + 0.249999642 + -1.78813934e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftKneeRigAttachment + false + + + + + + -1.78813934e-007 + -0.749997616 + 6.29340548e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftAnkleRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.43047047 + 1.49999988 + -12.3693666 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 1.00000036 + 1.49999976 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220018 + LeftUpperLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 1.00000036 + 1.49999976 + 0.999999881 + + + + + + 5.96046448e-008 + 0.5 + -1.63912773e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftHipRigAttachment + false + + + + + + 5.96046448e-008 + -0.299999952 + -1.63912773e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftKneeRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -7.63977861 + 0.150000095 + -12.9815807 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.99999994 + 0.300000191 + 1 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220020 + RightFoot + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.99999994 + 0.300000191 + 1 + + + + + + 0 + 0.05 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightAnkleRigAttachment + false + + + + + + 0 + -0.15 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightFootAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -7.63977861 + 0.950000286 + -12.9815807 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.99999994 + 1.50000036 + 1.00000012 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220027 + RightLowerLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.99999994 + 1.50000036 + 1.00000012 + + + + + + -0 + 0.249999642 + 4.35260044e-005 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightKneeRigAttachment + false + + + + + + -0 + -0.750000477 + 9.82746205e-005 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightAnkleRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -7.63977861 + 1.49999988 + -12.9815807 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 1.00000048 + 1.49999976 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220031 + RightUpperLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 1.00000048 + 1.49999976 + 0.999999881 + + + + + + -0 + 0.5 + -1.04308128e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightHipRigAttachment + false + + + + + + -0 + -0.299999952 + 4.36005103e-005 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightKneeRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.03512478 + 2.19999981 + -12.6754742 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + true + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 1.99999976 + 0.399999976 + 1.00000012 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220036 + LowerTorso + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 1.99999976 + 0.399999976 + 1.00000012 + + + + + + -1.1920929e-007 + 0.150000036 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootRigAttachment + false + + + + + + -1.1920929e-007 + 0.550000072 + 7.64462551e-020 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistRigAttachment + false + + + + + + -0.500000119 + -0.199999958 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftHipRigAttachment + false + + + + + + 0.499999881 + -0.199999958 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightHipRigAttachment + false + + + + + + 0.0 + -0.2 + 0.0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistCenterAttachment + false + + + + + + 0.0 + -0.2 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistFrontAttachment + false + + + + + + 0.0 + -0.2 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistBackAttachment + false + + + + + + 0 + 100 + 100 + 1.35000002 + 50 + 100 + 89 + Humanoid + 100 + 2 + 1 + 16 + + + + Animator + + + + + BodyWidthScale + 1 + + + + + BodyHeightScale + 1 + + + + + BodyDepthScale + 1 + + + + + HeadScale + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.03495789 + 4.5 + -12.6752586 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + 1 + 1 + + 2 + 1 + 1 + + + + + 2 + 2 + + 0 + Mesh + + 0 + 0 + 0 + + + 1.25 + 1.25 + 1.25 + + + + 1 + 1 + 1 + + + + + + + 3.93568822e-009 + 0 + -0.000272244215 + 1 + 7.87137555e-009 + 3.02998127e-015 + -7.87137555e-009 + 1 + -4.1444258e-016 + -3.02998127e-015 + 4.14442554e-016 + 1 + + FaceCenterAttachment + false + + + + + + 3.93568866e-009 + 0 + -0.600272298 + 1 + 7.87137555e-009 + 3.02998127e-015 + -7.87137555e-009 + 1 + -4.1444258e-016 + -3.02998127e-015 + 4.14442554e-016 + 1 + + FaceFrontAttachment + false + + + + + + 8.65851391e-009 + 0.599999905 + -0.000272244215 + 1 + 7.87137555e-009 + 3.02998127e-015 + -7.87137555e-009 + 1 + -4.1444258e-016 + -3.02998127e-015 + 4.14442554e-016 + 1 + + HairAttachment + false + + + + + + 8.65851391e-009 + 0.599999905 + -0.000272244215 + 1 + 7.87137555e-009 + 3.02998127e-015 + -7.87137555e-009 + 1 + -4.1444258e-016 + -3.02998127e-015 + 4.14442554e-016 + 1 + + HatAttachment + false + + + + + + -0 + -0.500000119 + -0.000272244215 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckRigAttachment + false + + + + + 5 + face + rbxasset://textures/face.png + 0 + + + + + \ No newline at end of file diff --git a/Client2018/content/avatar/characterR15V2.rbxm b/Client2018/content/avatar/characterR15V2.rbxm new file mode 100644 index 0000000..b02f520 Binary files /dev/null and b/Client2018/content/avatar/characterR15V2.rbxm differ diff --git a/Client2018/content/avatar/characterR15V3.rbxm b/Client2018/content/avatar/characterR15V3.rbxm new file mode 100644 index 0000000..9168b74 Binary files /dev/null and b/Client2018/content/avatar/characterR15V3.rbxm differ diff --git a/Client2018/content/avatar/compositing/CompositExtraSlot0.mesh b/Client2018/content/avatar/compositing/CompositExtraSlot0.mesh new file mode 100644 index 0000000..87b9d85 Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositExtraSlot0.mesh differ diff --git a/Client2018/content/avatar/compositing/CompositExtraSlot1.mesh b/Client2018/content/avatar/compositing/CompositExtraSlot1.mesh new file mode 100644 index 0000000..6874c5e Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositExtraSlot1.mesh differ diff --git a/Client2018/content/avatar/compositing/CompositExtraSlot2.mesh b/Client2018/content/avatar/compositing/CompositExtraSlot2.mesh new file mode 100644 index 0000000..423f198 Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositExtraSlot2.mesh differ diff --git a/Client2018/content/avatar/compositing/CompositExtraSlot3.mesh b/Client2018/content/avatar/compositing/CompositExtraSlot3.mesh new file mode 100644 index 0000000..de05ced Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositExtraSlot3.mesh differ diff --git a/Client2018/content/avatar/compositing/CompositExtraSlot4.mesh b/Client2018/content/avatar/compositing/CompositExtraSlot4.mesh new file mode 100644 index 0000000..ff9cb62 Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositExtraSlot4.mesh differ diff --git a/Client2018/content/avatar/compositing/CompositFullAtlasBaseTexture.mesh b/Client2018/content/avatar/compositing/CompositFullAtlasBaseTexture.mesh new file mode 100644 index 0000000..b7e6595 Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositFullAtlasBaseTexture.mesh differ diff --git a/Client2018/content/avatar/compositing/CompositFullAtlasOverlayTexture.mesh b/Client2018/content/avatar/compositing/CompositFullAtlasOverlayTexture.mesh new file mode 100644 index 0000000..eda1938 Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositFullAtlasOverlayTexture.mesh differ diff --git a/Client2018/content/avatar/compositing/CompositLeftArmBase.mesh b/Client2018/content/avatar/compositing/CompositLeftArmBase.mesh new file mode 100644 index 0000000..5bcc4ae Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositLeftArmBase.mesh differ diff --git a/Client2018/content/avatar/compositing/CompositLeftLegBase.mesh b/Client2018/content/avatar/compositing/CompositLeftLegBase.mesh new file mode 100644 index 0000000..f4712ce Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositLeftLegBase.mesh differ diff --git a/Client2018/content/avatar/compositing/CompositPantsTemplate.mesh b/Client2018/content/avatar/compositing/CompositPantsTemplate.mesh new file mode 100644 index 0000000..756ee03 Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositPantsTemplate.mesh differ diff --git a/Client2018/content/avatar/compositing/CompositQuad.mesh b/Client2018/content/avatar/compositing/CompositQuad.mesh new file mode 100644 index 0000000..192abf2 Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositQuad.mesh differ diff --git a/Client2018/content/avatar/compositing/CompositRightArmBase.mesh b/Client2018/content/avatar/compositing/CompositRightArmBase.mesh new file mode 100644 index 0000000..02f2721 Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositRightArmBase.mesh differ diff --git a/Client2018/content/avatar/compositing/CompositRightLegBase.mesh b/Client2018/content/avatar/compositing/CompositRightLegBase.mesh new file mode 100644 index 0000000..b287939 Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositRightLegBase.mesh differ diff --git a/Client2018/content/avatar/compositing/CompositShirtTemplate.mesh b/Client2018/content/avatar/compositing/CompositShirtTemplate.mesh new file mode 100644 index 0000000..75487e1 Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositShirtTemplate.mesh differ diff --git a/Client2018/content/avatar/compositing/CompositTShirt.mesh b/Client2018/content/avatar/compositing/CompositTShirt.mesh new file mode 100644 index 0000000..b39b8ac Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositTShirt.mesh differ diff --git a/Client2018/content/avatar/compositing/CompositTorsoBase.mesh b/Client2018/content/avatar/compositing/CompositTorsoBase.mesh new file mode 100644 index 0000000..0388bde Binary files /dev/null and b/Client2018/content/avatar/compositing/CompositTorsoBase.mesh differ diff --git a/Client2018/content/avatar/compositing/R15CompositLeftArmBase.mesh b/Client2018/content/avatar/compositing/R15CompositLeftArmBase.mesh new file mode 100644 index 0000000..c262d08 Binary files /dev/null and b/Client2018/content/avatar/compositing/R15CompositLeftArmBase.mesh differ diff --git a/Client2018/content/avatar/compositing/R15CompositRightArmBase.mesh b/Client2018/content/avatar/compositing/R15CompositRightArmBase.mesh new file mode 100644 index 0000000..c745b94 Binary files /dev/null and b/Client2018/content/avatar/compositing/R15CompositRightArmBase.mesh differ diff --git a/Client2018/content/avatar/compositing/R15CompositTorsoBase.mesh b/Client2018/content/avatar/compositing/R15CompositTorsoBase.mesh new file mode 100644 index 0000000..341d8b6 Binary files /dev/null and b/Client2018/content/avatar/compositing/R15CompositTorsoBase.mesh differ diff --git a/Client2018/content/avatar/heads/head.mesh b/Client2018/content/avatar/heads/head.mesh new file mode 100644 index 0000000..26db950 Binary files /dev/null and b/Client2018/content/avatar/heads/head.mesh differ diff --git a/Client2018/content/avatar/heads/headA.mesh b/Client2018/content/avatar/heads/headA.mesh new file mode 100644 index 0000000..76b2e72 Binary files /dev/null and b/Client2018/content/avatar/heads/headA.mesh differ diff --git a/Client2018/content/avatar/heads/headB.mesh b/Client2018/content/avatar/heads/headB.mesh new file mode 100644 index 0000000..7705a05 Binary files /dev/null and b/Client2018/content/avatar/heads/headB.mesh differ diff --git a/Client2018/content/avatar/heads/headC.mesh b/Client2018/content/avatar/heads/headC.mesh new file mode 100644 index 0000000..4672c59 Binary files /dev/null and b/Client2018/content/avatar/heads/headC.mesh differ diff --git a/Client2018/content/avatar/heads/headD.mesh b/Client2018/content/avatar/heads/headD.mesh new file mode 100644 index 0000000..ff6cd6c Binary files /dev/null and b/Client2018/content/avatar/heads/headD.mesh differ diff --git a/Client2018/content/avatar/heads/headE.mesh b/Client2018/content/avatar/heads/headE.mesh new file mode 100644 index 0000000..d722d29 Binary files /dev/null and b/Client2018/content/avatar/heads/headE.mesh differ diff --git a/Client2018/content/avatar/heads/headF.mesh b/Client2018/content/avatar/heads/headF.mesh new file mode 100644 index 0000000..7c0b311 Binary files /dev/null and b/Client2018/content/avatar/heads/headF.mesh differ diff --git a/Client2018/content/avatar/heads/headG.mesh b/Client2018/content/avatar/heads/headG.mesh new file mode 100644 index 0000000..38db235 Binary files /dev/null and b/Client2018/content/avatar/heads/headG.mesh differ diff --git a/Client2018/content/avatar/heads/headH.mesh b/Client2018/content/avatar/heads/headH.mesh new file mode 100644 index 0000000..fe7365b Binary files /dev/null and b/Client2018/content/avatar/heads/headH.mesh differ diff --git a/Client2018/content/avatar/heads/headI.mesh b/Client2018/content/avatar/heads/headI.mesh new file mode 100644 index 0000000..8ff7ba1 Binary files /dev/null and b/Client2018/content/avatar/heads/headI.mesh differ diff --git a/Client2018/content/avatar/heads/headJ.mesh b/Client2018/content/avatar/heads/headJ.mesh new file mode 100644 index 0000000..ff76d44 Binary files /dev/null and b/Client2018/content/avatar/heads/headJ.mesh differ diff --git a/Client2018/content/avatar/heads/headK.mesh b/Client2018/content/avatar/heads/headK.mesh new file mode 100644 index 0000000..b08bffc Binary files /dev/null and b/Client2018/content/avatar/heads/headK.mesh differ diff --git a/Client2018/content/avatar/heads/headL.mesh b/Client2018/content/avatar/heads/headL.mesh new file mode 100644 index 0000000..645531b Binary files /dev/null and b/Client2018/content/avatar/heads/headL.mesh differ diff --git a/Client2018/content/avatar/heads/headM.mesh b/Client2018/content/avatar/heads/headM.mesh new file mode 100644 index 0000000..ba144e8 Binary files /dev/null and b/Client2018/content/avatar/heads/headM.mesh differ diff --git a/Client2018/content/avatar/heads/headN.mesh b/Client2018/content/avatar/heads/headN.mesh new file mode 100644 index 0000000..6fc4620 Binary files /dev/null and b/Client2018/content/avatar/heads/headN.mesh differ diff --git a/Client2018/content/avatar/heads/headO.mesh b/Client2018/content/avatar/heads/headO.mesh new file mode 100644 index 0000000..a41f85f Binary files /dev/null and b/Client2018/content/avatar/heads/headO.mesh differ diff --git a/Client2018/content/avatar/heads/headP.mesh b/Client2018/content/avatar/heads/headP.mesh new file mode 100644 index 0000000..cdb1647 Binary files /dev/null and b/Client2018/content/avatar/heads/headP.mesh differ diff --git a/Client2018/content/avatar/meshes/leftarm.mesh b/Client2018/content/avatar/meshes/leftarm.mesh new file mode 100644 index 0000000..6e8bb63 Binary files /dev/null and b/Client2018/content/avatar/meshes/leftarm.mesh differ diff --git a/Client2018/content/avatar/meshes/leftleg.mesh b/Client2018/content/avatar/meshes/leftleg.mesh new file mode 100644 index 0000000..aba3a29 Binary files /dev/null and b/Client2018/content/avatar/meshes/leftleg.mesh differ diff --git a/Client2018/content/avatar/meshes/rightarm.mesh b/Client2018/content/avatar/meshes/rightarm.mesh new file mode 100644 index 0000000..14a52a4 Binary files /dev/null and b/Client2018/content/avatar/meshes/rightarm.mesh differ diff --git a/Client2018/content/avatar/meshes/rightleg.mesh b/Client2018/content/avatar/meshes/rightleg.mesh new file mode 100644 index 0000000..dab065d Binary files /dev/null and b/Client2018/content/avatar/meshes/rightleg.mesh differ diff --git a/Client2018/content/avatar/meshes/torso.mesh b/Client2018/content/avatar/meshes/torso.mesh new file mode 100644 index 0000000..43d58d1 Binary files /dev/null and b/Client2018/content/avatar/meshes/torso.mesh differ diff --git a/Client2018/content/avatar/scripts/characterSound.rbxmx b/Client2018/content/avatar/scripts/characterSound.rbxmx new file mode 100644 index 0000000..792a4cf --- /dev/null +++ b/Client2018/content/avatar/scripts/characterSound.rbxmx @@ -0,0 +1,366 @@ + + null + nil + + + false + + Sound + + + + + + false + + LocalSound + + 0.5 then + Util.Resume(sound) + setSoundInPlayingLoopedSounds(sound) + else + stopPlayingLoopedSoundsExcept() + end + end; + + [Enum.HumanoidStateType.Swimming] = function() + if activeState ~= Enum.HumanoidStateType.Swimming and Util.VerticalSpeed(Head) > 0.1 then + local splashSound = Sounds[SFX.Splash] + splashSound.Volume = Util.Clamp( + Util.YForLineGivenXAndTwoPts( + Util.VerticalSpeed(Head), + 100, 0.28, + 350, 1), + 0,1) + Util.Play(splashSound) + end + + do + local sound = Sounds[SFX.Swimming] + stopPlayingLoopedSoundsExcept(sound) + Util.Resume(sound) + setSoundInPlayingLoopedSounds(sound) + end + end; + + [Enum.HumanoidStateType.Climbing] = function() + local sound = Sounds[SFX.Climbing] + if Util.VerticalSpeed(Head) > 0.1 then + Util.Resume(sound) + stopPlayingLoopedSoundsExcept(sound) + else + stopPlayingLoopedSoundsExcept() + end + setSoundInPlayingLoopedSounds(sound) + end; + + [Enum.HumanoidStateType.Jumping] = function() + if activeState == Enum.HumanoidStateType.Jumping then + return + end + stopPlayingLoopedSoundsExcept() + local sound = Sounds[SFX.Jumping] + Util.Play(sound) + end; + + [Enum.HumanoidStateType.GettingUp] = function() + stopPlayingLoopedSoundsExcept() + local sound = Sounds[SFX.GettingUp] + Util.Play(sound) + end; + + [Enum.HumanoidStateType.Freefall] = function() + if activeState == Enum.HumanoidStateType.Freefall then + return + end + local sound = Sounds[SFX.FreeFalling] + sound.Volume = 0 + stopPlayingLoopedSoundsExcept() + end; + + [Enum.HumanoidStateType.FallingDown] = function() + stopPlayingLoopedSoundsExcept() + end; + + [Enum.HumanoidStateType.Landed] = function() + stopPlayingLoopedSoundsExcept() + if Util.VerticalSpeed(Head) > 75 then + local landingSound = Sounds[SFX.Landing] + landingSound.Volume = Util.Clamp( + Util.YForLineGivenXAndTwoPts( + Util.VerticalSpeed(Head), + 50, 0, + 100, 1), + 0,1) + Util.Play(landingSound) + end + end; + + [Enum.HumanoidStateType.Seated] = function() + stopPlayingLoopedSoundsExcept() + end; + } + + -- Handle state event fired or OnChange fired + function stateUpdated(state) + if stateUpdateHandler[state] ~= nil then + stateUpdateHandler[state]() + end + activeState = state + end + + Humanoid.Died:connect( function() stateUpdated(Enum.HumanoidStateType.Dead) end) + Humanoid.Running:connect( function() stateUpdated(Enum.HumanoidStateType.Running) end) + Humanoid.Swimming:connect( function() stateUpdated(Enum.HumanoidStateType.Swimming) end) + Humanoid.Climbing:connect( function() stateUpdated(Enum.HumanoidStateType.Climbing) end) + Humanoid.Jumping:connect( function() stateUpdated(Enum.HumanoidStateType.Jumping) end) + Humanoid.GettingUp:connect( function() stateUpdated(Enum.HumanoidStateType.GettingUp) end) + Humanoid.FreeFalling:connect( function() stateUpdated(Enum.HumanoidStateType.Freefall) end) + Humanoid.FallingDown:connect( function() stateUpdated(Enum.HumanoidStateType.FallingDown) end) + + -- required for proper handling of Landed event + Humanoid.StateChanged:connect(function(old, new) + stateUpdated(new) + end) + + + function onUpdate(stepDeltaSeconds, tickSpeedSeconds) + local stepScale = stepDeltaSeconds / tickSpeedSeconds + do + local sound = Sounds[SFX.FreeFalling] + if activeState == Enum.HumanoidStateType.Freefall then + if Head.Velocity.Y < 0 and Util.VerticalSpeed(Head) > 75 then + Util.Resume(sound) + + --Volume takes 1.1 seconds to go from volume 0 to 1 + local ANIMATION_LENGTH_SECONDS = 1.1 + + local normalizedIncrement = tickSpeedSeconds / ANIMATION_LENGTH_SECONDS + sound.Volume = Util.Clamp(sound.Volume + normalizedIncrement * stepScale, 0, 1) + else + sound.Volume = 0 + end + else + Util.Pause(sound) + end + end + + do + local sound = Sounds[SFX.Running] + if activeState == Enum.HumanoidStateType.Running then + if Util.HorizontalSpeed(Head) < 0.5 then + Util.Pause(sound) + end + end + end + end + + local lastTick = tick() + local TICK_SPEED_SECONDS = 0.25 + while true do + onUpdate(tick() - lastTick,TICK_SPEED_SECONDS) + lastTick = tick() + wait(TICK_SPEED_SECONDS) + end +end +]]> + + + + \ No newline at end of file diff --git a/Client2018/content/avatar/scripts/humanoidAnimateLocalKeyframe.rbxm b/Client2018/content/avatar/scripts/humanoidAnimateLocalKeyframe.rbxm new file mode 100644 index 0000000..ad71482 --- /dev/null +++ b/Client2018/content/avatar/scripts/humanoidAnimateLocalKeyframe.rbxm @@ -0,0 +1,664 @@ + + null + nil + + + false + + Animate + animTable[animName][idx].weight) do + roll = roll - animTable[animName][idx].weight + idx = idx + 1 + end +-- print(animName .. " " .. idx .. " [" .. origRoll .. "]") + local anim = animTable[animName][idx].anim + + -- switch animation + if (anim ~= currentAnimInstance) then + + if (currentAnimTrack ~= nil) then + currentAnimTrack:Stop(transitionTime) + currentAnimTrack:Destroy() + end + + currentAnimSpeed = 1.0 + + -- load it to the humanoid; get AnimationTrack + currentAnimTrack = humanoid:LoadAnimation(anim) + currentAnimTrack.Priority = Enum.AnimationPriority.Core + + -- play the animation + currentAnimTrack:Play(transitionTime) + currentAnim = animName + currentAnimInstance = anim + + -- set up keyframe name triggers + if (currentAnimKeyframeHandler ~= nil) then + currentAnimKeyframeHandler:disconnect() + end + currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:connect(keyFrameReachedFunc) + + end + +end + +------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------- + +local toolAnimName = "" +local toolAnimTrack = nil +local toolAnimInstance = nil +local currentToolAnimKeyframeHandler = nil + +function toolKeyFrameReachedFunc(frameName) + if (frameName == "End") then +-- print("Keyframe : ".. frameName) + playToolAnimation(toolAnimName, 0.0, Humanoid) + end +end + + +function playToolAnimation(animName, transitionTime, humanoid, priority) + + local roll = math.random(1, animTable[animName].totalWeight) + local origRoll = roll + local idx = 1 + while (roll > animTable[animName][idx].weight) do + roll = roll - animTable[animName][idx].weight + idx = idx + 1 + end +-- print(animName .. " * " .. idx .. " [" .. origRoll .. "]") + local anim = animTable[animName][idx].anim + + if (toolAnimInstance ~= anim) then + + if (toolAnimTrack ~= nil) then + toolAnimTrack:Stop() + toolAnimTrack:Destroy() + transitionTime = 0 + end + + -- load it to the humanoid; get AnimationTrack + toolAnimTrack = humanoid:LoadAnimation(anim) + if priority then + toolAnimTrack.Priority = priority + end + + -- play the animation + toolAnimTrack:Play(transitionTime) + toolAnimName = animName + toolAnimInstance = anim + + currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:connect(toolKeyFrameReachedFunc) + end +end + +function stopToolAnimations() + local oldAnim = toolAnimName + + if (currentToolAnimKeyframeHandler ~= nil) then + currentToolAnimKeyframeHandler:disconnect() + end + + toolAnimName = "" + toolAnimInstance = nil + if (toolAnimTrack ~= nil) then + toolAnimTrack:Stop() + toolAnimTrack:Destroy() + toolAnimTrack = nil + end + + + return oldAnim +end + +------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------- + + +function onRunning(speed) + if speed > 0.01 then + playAnimation("walk", 0.1, Humanoid) + if currentAnimInstance and currentAnimInstance.AnimationId == "http://www.roblox.com/asset/?id=180426354" then + setAnimationSpeed(speed / 14.5) + end + pose = "Running" + else + if emoteNames[currentAnim] == nil then + playAnimation("idle", 0.1, Humanoid) + pose = "Standing" + end + end +end + +function onDied() + pose = "Dead" +end + +function onJumping() + playAnimation("jump", 0.1, Humanoid) + jumpAnimTime = jumpAnimDuration + pose = "Jumping" +end + +function onClimbing(speed) + playAnimation("climb", 0.1, Humanoid) + setAnimationSpeed(speed / 12.0) + pose = "Climbing" +end + +function onGettingUp() + pose = "GettingUp" +end + +function onFreeFall() + if (jumpAnimTime <= 0) then + playAnimation("fall", fallTransitionTime, Humanoid) + end + pose = "FreeFall" +end + +function onFallingDown() + pose = "FallingDown" +end + +function onSeated() + pose = "Seated" +end + +function onPlatformStanding() + pose = "PlatformStanding" +end + +function onSwimming(speed) + if speed > 0 then + pose = "Running" + else + pose = "Standing" + end +end + +function getTool() + for _, kid in ipairs(Figure:GetChildren()) do + if kid.className == "Tool" then return kid end + end + return nil +end + +function getToolAnim(tool) + for _, c in ipairs(tool:GetChildren()) do + if c.Name == "toolanim" and c.className == "StringValue" then + return c + end + end + return nil +end + +function animateTool() + + if (toolAnim == "None") then + playToolAnimation("toolnone", toolTransitionTime, Humanoid, Enum.AnimationPriority.Idle) + return + end + + if (toolAnim == "Slash") then + playToolAnimation("toolslash", 0, Humanoid, Enum.AnimationPriority.Action) + return + end + + if (toolAnim == "Lunge") then + playToolAnimation("toollunge", 0, Humanoid, Enum.AnimationPriority.Action) + return + end +end + +function moveSit() + RightShoulder.MaxVelocity = 0.15 + LeftShoulder.MaxVelocity = 0.15 + RightShoulder:SetDesiredAngle(3.14 /2) + LeftShoulder:SetDesiredAngle(-3.14 /2) + RightHip:SetDesiredAngle(3.14 /2) + LeftHip:SetDesiredAngle(-3.14 /2) +end + +local lastTick = 0 + +function move(time) + local amplitude = 1 + local frequency = 1 + local deltaTime = time - lastTick + lastTick = time + + local climbFudge = 0 + local setAngles = false + + if (jumpAnimTime > 0) then + jumpAnimTime = jumpAnimTime - deltaTime + end + + if (pose == "FreeFall" and jumpAnimTime <= 0) then + playAnimation("fall", fallTransitionTime, Humanoid) + elseif (pose == "Seated") then + playAnimation("sit", 0.5, Humanoid) + return + elseif (pose == "Running") then + playAnimation("walk", 0.1, Humanoid) + elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then +-- print("Wha " .. pose) + stopAllAnimations() + amplitude = 0.1 + frequency = 1 + setAngles = true + end + + if (setAngles) then + local desiredAngle = amplitude * math.sin(time * frequency) + + RightShoulder:SetDesiredAngle(desiredAngle + climbFudge) + LeftShoulder:SetDesiredAngle(desiredAngle - climbFudge) + RightHip:SetDesiredAngle(-desiredAngle) + LeftHip:SetDesiredAngle(-desiredAngle) + end + + -- Tool Animation handling + local tool = getTool() + if tool and tool:FindFirstChild("Handle") then + + local animStringValueObject = getToolAnim(tool) + + if animStringValueObject then + toolAnim = animStringValueObject.Value + -- message recieved, delete StringValue + animStringValueObject.Parent = nil + toolAnimTime = time + .3 + end + + if time > toolAnimTime then + toolAnimTime = 0 + toolAnim = "None" + end + + animateTool() + else + stopToolAnimations() + toolAnim = "None" + toolAnimInstance = nil + toolAnimTime = 0 + end +end + +-- connect events +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Jumping:connect(onJumping) +Humanoid.Climbing:connect(onClimbing) +Humanoid.GettingUp:connect(onGettingUp) +Humanoid.FreeFalling:connect(onFreeFall) +Humanoid.FallingDown:connect(onFallingDown) +Humanoid.Seated:connect(onSeated) +Humanoid.PlatformStanding:connect(onPlatformStanding) +Humanoid.Swimming:connect(onSwimming) + +-- setup emote chat hook +game:GetService("Players").LocalPlayer.Chatted:connect(function(msg) + local emote = "" + if msg == "/e dance" then + emote = dances[math.random(1, #dances)] + elseif (string.sub(msg, 1, 3) == "/e ") then + emote = string.sub(msg, 4) + elseif (string.sub(msg, 1, 7) == "/emote ") then + emote = string.sub(msg, 8) + end + + if (pose == "Standing" and emoteNames[emote] ~= nil) then + playAnimation(emote, 0.1, Humanoid) + end + +end) + + +-- main program + +-- initialize to idle +playAnimation("idle", 0.1, Humanoid) +pose = "Standing" + +while Figure.Parent ~= nil do + local _, time = wait(0.1) + move(time) +end + + +]]> + + + + idle + + + + + http://www.roblox.com/asset/?id=180435571 + Animation1 + + + + Weight + 9 + + + + + + http://www.roblox.com/asset/?id=180435792 + Animation2 + + + + Weight + 1 + + + + + + + walk + + + + + http://www.roblox.com/asset/?id=180426354 + WalkAnim + + + + + + run + + + + + http://www.roblox.com/asset/?id=180426354 + RunAnim + + + + + + jump + + + + + http://www.roblox.com/asset/?id=125750702 + JumpAnim + + + + + + climb + + + + + http://www.roblox.com/asset/?id=180436334 + ClimbAnim + + + + + + toolnone + + + + + http://www.roblox.com/asset/?id=182393478 + ToolNoneAnim + + + + + + fall + + + + + http://www.roblox.com/asset/?id=180436148 + FallAnim + + + + + + sit + + + + + http://www.roblox.com/asset/?id=178130996 + SitAnim + + + + + \ No newline at end of file diff --git a/Client2018/content/avatar/scripts/humanoidAnimateR15.rbxm b/Client2018/content/avatar/scripts/humanoidAnimateR15.rbxm new file mode 100644 index 0000000..bfb4da2 --- /dev/null +++ b/Client2018/content/avatar/scripts/humanoidAnimateR15.rbxm @@ -0,0 +1,1032 @@ + + null + nil + + + false + + Animate + {0C2E6BF6-AFE6-4BAA-9650-0D7405850A49} + animTable[animName][idx].weight) do + roll = roll - animTable[animName][idx].weight + idx = idx + 1 + end + return idx +end + +function playAnimation(animName, transitionTime, humanoid) + local idx = rollAnimation(animName) + local anim = animTable[animName][idx].anim + + -- switch animation + if (anim ~= currentAnimInstance) then + + if (currentAnimTrack ~= nil) then + currentAnimTrack:Stop(transitionTime) + currentAnimTrack:Destroy() + end + + if (runAnimTrack ~= nil) then + runAnimTrack:Stop(transitionTime) + runAnimTrack:Destroy() + if userNoUpdateOnLoop == true then + runAnimTrack = nil + end + end + + currentAnimSpeed = 1.0 + + -- load it to the humanoid; get AnimationTrack + currentAnimTrack = humanoid:LoadAnimation(anim) + currentAnimTrack.Priority = Enum.AnimationPriority.Core + + -- play the animation + currentAnimTrack:Play(transitionTime) + currentAnim = animName + currentAnimInstance = anim + + -- set up keyframe name triggers + if (currentAnimKeyframeHandler ~= nil) then + currentAnimKeyframeHandler:disconnect() + end + currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:connect(keyFrameReachedFunc) + + -- check to see if we need to blend a walk/run animation + if animName == "walk" then + local runAnimName = "run" + local runIdx = rollAnimation(runAnimName) + + runAnimTrack = humanoid:LoadAnimation(animTable[runAnimName][runIdx].anim) + runAnimTrack.Priority = Enum.AnimationPriority.Core + runAnimTrack:Play(transitionTime) + + if (runAnimKeyframeHandler ~= nil) then + runAnimKeyframeHandler:disconnect() + end + runAnimKeyframeHandler = runAnimTrack.KeyframeReached:connect(keyFrameReachedFunc) + end + end + +end + +------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------- + +local toolAnimName = "" +local toolAnimTrack = nil +local toolAnimInstance = nil +local currentToolAnimKeyframeHandler = nil + +function toolKeyFrameReachedFunc(frameName) + if (frameName == "End") then + playToolAnimation(toolAnimName, 0.0, Humanoid) + end +end + + +function playToolAnimation(animName, transitionTime, humanoid, priority) + local idx = rollAnimation(animName) + local anim = animTable[animName][idx].anim + + if (toolAnimInstance ~= anim) then + + if (toolAnimTrack ~= nil) then + toolAnimTrack:Stop() + toolAnimTrack:Destroy() + transitionTime = 0 + end + + -- load it to the humanoid; get AnimationTrack + toolAnimTrack = humanoid:LoadAnimation(anim) + if priority then + toolAnimTrack.Priority = priority + end + + -- play the animation + toolAnimTrack:Play(transitionTime) + toolAnimName = animName + toolAnimInstance = anim + + currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:connect(toolKeyFrameReachedFunc) + end +end + +function stopToolAnimations() + local oldAnim = toolAnimName + + if (currentToolAnimKeyframeHandler ~= nil) then + currentToolAnimKeyframeHandler:disconnect() + end + + toolAnimName = "" + toolAnimInstance = nil + if (toolAnimTrack ~= nil) then + toolAnimTrack:Stop() + toolAnimTrack:Destroy() + toolAnimTrack = nil + end + + return oldAnim +end + +------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------- +-- STATE CHANGE HANDLERS + +function onRunning(speed) + if speed > 0.5 then + local scale = 16.0 + playAnimation("walk", 0.2, Humanoid) + setAnimationSpeed(speed / scale) + pose = "Running" + else + if emoteNames[currentAnim] == nil then + playAnimation("idle", 0.2, Humanoid) + pose = "Standing" + end + end +end + +function onDied() + pose = "Dead" +end + +function onJumping() + playAnimation("jump", 0.1, Humanoid) + jumpAnimTime = jumpAnimDuration + pose = "Jumping" +end + +function onClimbing(speed) + local scale = 5.0 + playAnimation("climb", 0.1, Humanoid) + setAnimationSpeed(speed / scale) + pose = "Climbing" +end + +function onGettingUp() + pose = "GettingUp" +end + +function onFreeFall() + if (jumpAnimTime <= 0) then + playAnimation("fall", fallTransitionTime, Humanoid) + end + pose = "FreeFall" +end + +function onFallingDown() + pose = "FallingDown" +end + +function onSeated() + pose = "Seated" +end + +function onPlatformStanding() + pose = "PlatformStanding" +end + +------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------- + +function onSwimming(speed) + if speed > 1.00 then + local scale = 10.0 + playAnimation("swim", 0.4, Humanoid) + setAnimationSpeed(speed / scale) + pose = "Swimming" + else + playAnimation("swimidle", 0.4, Humanoid) + pose = "Standing" + end +end + +function animateTool() + if (toolAnim == "None") then + playToolAnimation("toolnone", toolTransitionTime, Humanoid, Enum.AnimationPriority.Idle) + return + end + + if (toolAnim == "Slash") then + playToolAnimation("toolslash", 0, Humanoid, Enum.AnimationPriority.Action) + return + end + + if (toolAnim == "Lunge") then + playToolAnimation("toollunge", 0, Humanoid, Enum.AnimationPriority.Action) + return + end +end + +function getToolAnim(tool) + for _, c in ipairs(tool:GetChildren()) do + if c.Name == "toolanim" and c.className == "StringValue" then + return c + end + end + return nil +end + +local lastTick = 0 + +function stepAnimate(currentTime) + local amplitude = 1 + local frequency = 1 + local deltaTime = currentTime - lastTick + lastTick = currentTime + + local climbFudge = 0 + local setAngles = false + + if (jumpAnimTime > 0) then + jumpAnimTime = jumpAnimTime - deltaTime + end + + if (pose == "FreeFall" and jumpAnimTime <= 0) then + playAnimation("fall", fallTransitionTime, Humanoid) + elseif (pose == "Seated") then + playAnimation("sit", 0.5, Humanoid) + return + elseif (pose == "Running") then + playAnimation("walk", 0.2, Humanoid) + elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then + stopAllAnimations() + amplitude = 0.1 + frequency = 1 + setAngles = true + end + + -- Tool Animation handling + local tool = Character:FindFirstChildOfClass("Tool") + if tool and tool:FindFirstChild("Handle") then + local animStringValueObject = getToolAnim(tool) + + if animStringValueObject then + toolAnim = animStringValueObject.Value + -- message recieved, delete StringValue + animStringValueObject.Parent = nil + toolAnimTime = currentTime + .3 + end + + if currentTime > toolAnimTime then + toolAnimTime = 0 + toolAnim = "None" + end + + animateTool() + else + stopToolAnimations() + toolAnim = "None" + toolAnimInstance = nil + toolAnimTime = 0 + end +end + +-- connect events +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Jumping:connect(onJumping) +Humanoid.Climbing:connect(onClimbing) +Humanoid.GettingUp:connect(onGettingUp) +Humanoid.FreeFalling:connect(onFreeFall) +Humanoid.FallingDown:connect(onFallingDown) +Humanoid.Seated:connect(onSeated) +Humanoid.PlatformStanding:connect(onPlatformStanding) +Humanoid.Swimming:connect(onSwimming) + +-- setup emote chat hook +game:GetService("Players").LocalPlayer.Chatted:connect(function(msg) + local emote = "" + if (string.sub(msg, 1, 3) == "/e ") then + emote = string.sub(msg, 4) + elseif (string.sub(msg, 1, 7) == "/emote ") then + emote = string.sub(msg, 8) + end + + if (pose == "Standing" and emoteNames[emote] ~= nil) then + playAnimation(emote, 0.1, Humanoid) + end +end) + + + +-- initialize to idle +playAnimation("idle", 0.1, Humanoid) +pose = "Standing" + +-- loop to handle timed state transitions and tool animations +while Character.Parent ~= nil do + local _, currentGameTime = wait(0.1) + stepAnimate(currentGameTime) +end + +]]> + + + + + cheer + + + + + + http://www.roblox.com/asset/?id=507770677 + CheerAnim + + + + + + + climb + + + + + + http://www.roblox.com/asset/?id=507765644 + ClimbAnim + + + + + + + dance + + + + + + http://www.roblox.com/asset/?id=507771019 + Animation1 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507771955 + Animation2 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507772104 + Animation3 + + + + + Weight + + 10 + + + + + + + dance2 + + + + + + http://www.roblox.com/asset/?id=507776043 + Animation1 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507776720 + Animation2 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507776879 + Animation3 + + + + + Weight + + 10 + + + + + + + dance3 + + + + + + http://www.roblox.com/asset/?id=507777268 + Animation1 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507777451 + Animation2 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507777623 + Animation3 + + + + + Weight + + 10 + + + + + + + fall + + + + + + http://www.roblox.com/asset/?id=507767968 + FallAnim + + + + + + + idle + + + + + + http://www.roblox.com/asset/?id=507766388 + Animation1 + + + + + Weight + + 9 + + + + + + http://www.roblox.com/asset/?id=507766666 + Animation2 + + + + + Weight + + 1 + + + + + + + jump + + + + + + http://www.roblox.com/asset/?id=507765000 + JumpAnim + + + + + + + laugh + + + + + + http://www.roblox.com/asset/?id=507770818 + LaughAnim + + + + + + + point + + + + + + http://www.roblox.com/asset/?id=507770453 + PointAnim + + + + + + + run + + + + + + http://www.roblox.com/asset/?id=507767714 + RunAnim + + + + + + + sit + + + + + + http://www.roblox.com/asset/?id=507768133 + SitAnim + + + + + + + swim + + + + + + http://www.roblox.com/asset/?id=507784897 + Swim + + + + + + + swimidle + + + + + + http://www.roblox.com/asset/?id=481825862 + SwimIdle + + + + + + + toollunge + + + + + + http://www.roblox.com/asset/?id=522638767 + ToolLungeAnim + + + + + + + toolnone + + + + + + http://www.roblox.com/asset/?id=507768375 + ToolNoneAnim + + + + + + + toolslash + + + + + + http://www.roblox.com/asset/?id=522635514 + ToolSlashAnim + + + + + + + walk + + + + + + http://www.roblox.com/asset/?id=540798782 + WalkAnim + + + + + + + wave + + + + + + http://www.roblox.com/asset/?id=507770239 + WaveAnim + + + + + + \ No newline at end of file diff --git a/Client2018/content/avatar/scripts/humanoidAnimateR15Scaled.rbxm b/Client2018/content/avatar/scripts/humanoidAnimateR15Scaled.rbxm new file mode 100644 index 0000000..8dcd53b Binary files /dev/null and b/Client2018/content/avatar/scripts/humanoidAnimateR15Scaled.rbxm differ diff --git a/Client2018/content/avatar/scripts/humanoidHealthRegenScript.rbxmx b/Client2018/content/avatar/scripts/humanoidHealthRegenScript.rbxmx new file mode 100644 index 0000000..ece49f1 --- /dev/null +++ b/Client2018/content/avatar/scripts/humanoidHealthRegenScript.rbxmx @@ -0,0 +1,32 @@ + + null + nil + + + false + + Health + {EC3A881D-5F49-4644-A69D-FB60F2E59FF2} + + + + \ No newline at end of file diff --git a/Client2018/content/fonts/AccanthisADFStd-Regular.otf b/Client2018/content/fonts/AccanthisADFStd-Regular.otf new file mode 100644 index 0000000..b3d6f03 Binary files /dev/null and b/Client2018/content/fonts/AccanthisADFStd-Regular.otf differ diff --git a/Client2018/content/fonts/Balthazar-Regular.ttf b/Client2018/content/fonts/Balthazar-Regular.ttf new file mode 100644 index 0000000..fff2ff7 Binary files /dev/null and b/Client2018/content/fonts/Balthazar-Regular.ttf differ diff --git a/Client2018/content/fonts/ComicNeue-Angular-Bold.ttf b/Client2018/content/fonts/ComicNeue-Angular-Bold.ttf new file mode 100644 index 0000000..d70a258 Binary files /dev/null and b/Client2018/content/fonts/ComicNeue-Angular-Bold.ttf differ diff --git a/Client2018/content/fonts/Guru-Regular.otf b/Client2018/content/fonts/Guru-Regular.otf new file mode 100644 index 0000000..20d7398 Binary files /dev/null and b/Client2018/content/fonts/Guru-Regular.otf differ diff --git a/Client2018/content/fonts/HWYGOTH.ttf b/Client2018/content/fonts/HWYGOTH.ttf new file mode 100644 index 0000000..20ac3e2 Binary files /dev/null and b/Client2018/content/fonts/HWYGOTH.ttf differ diff --git a/Client2018/content/fonts/Inconsolata-Regular.ttf b/Client2018/content/fonts/Inconsolata-Regular.ttf new file mode 100644 index 0000000..bbc9647 Binary files /dev/null and b/Client2018/content/fonts/Inconsolata-Regular.ttf differ diff --git a/Client2018/content/fonts/PressStart2P-Regular.ttf b/Client2018/content/fonts/PressStart2P-Regular.ttf new file mode 100644 index 0000000..e659e95 Binary files /dev/null and b/Client2018/content/fonts/PressStart2P-Regular.ttf differ diff --git a/Client2018/content/fonts/RomanAntique.otf b/Client2018/content/fonts/RomanAntique.otf new file mode 100644 index 0000000..dac6f3d Binary files /dev/null and b/Client2018/content/fonts/RomanAntique.otf differ diff --git a/Client2018/content/fonts/SourceSansPro-Bold.ttf b/Client2018/content/fonts/SourceSansPro-Bold.ttf new file mode 100644 index 0000000..be46652 Binary files /dev/null and b/Client2018/content/fonts/SourceSansPro-Bold.ttf differ diff --git a/Client2018/content/fonts/SourceSansPro-It.ttf b/Client2018/content/fonts/SourceSansPro-It.ttf new file mode 100644 index 0000000..c689cd2 Binary files /dev/null and b/Client2018/content/fonts/SourceSansPro-It.ttf differ diff --git a/Client2018/content/fonts/SourceSansPro-Light.ttf b/Client2018/content/fonts/SourceSansPro-Light.ttf new file mode 100644 index 0000000..5094f90 Binary files /dev/null and b/Client2018/content/fonts/SourceSansPro-Light.ttf differ diff --git a/Client2018/content/fonts/SourceSansPro-Regular.ttf b/Client2018/content/fonts/SourceSansPro-Regular.ttf new file mode 100644 index 0000000..24962c7 Binary files /dev/null and b/Client2018/content/fonts/SourceSansPro-Regular.ttf differ diff --git a/Client2018/content/fonts/SourceSansPro-Semibold.ttf b/Client2018/content/fonts/SourceSansPro-Semibold.ttf new file mode 100644 index 0000000..96be817 Binary files /dev/null and b/Client2018/content/fonts/SourceSansPro-Semibold.ttf differ diff --git a/Client2018/content/fonts/arial.ttf b/Client2018/content/fonts/arial.ttf new file mode 100644 index 0000000..729da61 Binary files /dev/null and b/Client2018/content/fonts/arial.ttf differ diff --git a/Client2018/content/fonts/arialbd.ttf b/Client2018/content/fonts/arialbd.ttf new file mode 100644 index 0000000..aac564c Binary files /dev/null and b/Client2018/content/fonts/arialbd.ttf differ diff --git a/Client2018/content/fonts/gamecontrollerdb.txt b/Client2018/content/fonts/gamecontrollerdb.txt new file mode 100644 index 0000000..dd49836 --- /dev/null +++ b/Client2018/content/fonts/gamecontrollerdb.txt @@ -0,0 +1,89 @@ +# Windows - DINPUT +8f0e1200000000000000504944564944,Acme,platform:Windows,x:b2,a:b0,b:b1,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +341a3608000000000000504944564944,Afterglow PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, +ffff0000000000000000504944564944,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, +6d0416c2000000000000504944564944,Generic DirectInput Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, +6d0419c2000000000000504944564944,Logitech F710 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, +88880803000000000000504944564944,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b9,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b0,y:b3,platform:Windows, +4c056802000000000000504944564944,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Windows, +25090500000000000000504944564944,PS3 DualShock,a:b2,b:b1,back:b9,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b0,y:b3,platform:Windows, +4c05c405000000000000504944564944,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, +6d0418c2000000000000504944564944,Logitech RumblePad 2 USB,platform:Windows,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, +36280100000000000000504944564944,OUYA Controller,platform:Windows,a:b0,b:b3,y:b2,x:b1,start:b14,guide:b15,leftstick:b6,rightstick:b7,leftshoulder:b4,rightshoulder:b5,dpup:b8,dpleft:b10,dpdown:b9,dpright:b11,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:b12,righttrigger:b13, +4f0400b3000000000000504944564944,Thrustmaster Firestorm Dual Power,a:b0,b:b2,y:b3,x:b1,start:b10,guide:b8,back:b9,leftstick:b11,rightstick:b12,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Windows, +00f00300000000000000504944564944,RetroUSB.com RetroPad,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Windows, +00f0f100000000000000504944564944,RetroUSB.com Super RetroPort,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Windows, +28040140000000000000504944564944,GamePad Pro USB,platform:Windows,a:b1,b:b2,x:b0,y:b3,back:b8,start:b9,leftshoulder:b4,rightshoulder:b5,leftx:a0,lefty:a1,lefttrigger:b6,righttrigger:b7, +ff113133000000000000504944564944,SVEN X-PAD,platform:Windows,a:b2,b:b3,y:b1,x:b0,start:b5,back:b4,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a4,lefttrigger:b8,righttrigger:b9, +8f0e0300000000000000504944564944,Piranha xtreme,platform:Windows,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +8f0e0d31000000000000504944564944,Multilaser JS071 USB,platform:Windows,a:b1,b:b2,y:b3,x:b0,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7, +10080300000000000000504944564944,PS2 USB,platform:Windows,a:b2,b:b1,y:b0,x:b3,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a4,righty:a2,lefttrigger:b4,righttrigger:b5, +79000600000000000000504944564944,G-Shark GS-GP702,a:b2,b:b1,x:b3,y:b0,back:b8,start:b9,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a4,lefttrigger:b6,righttrigger:b7,platform:Windows, +4b12014d000000000000504944564944,NYKO AIRFLO,a:b0,b:b1,x:b2,y:b3,back:b8,guide:b10,start:b9,leftstick:a0,rightstick:a2,leftshoulder:a3,rightshoulder:b5,dpup:h0.1,dpdown:h0.0,dpleft:h0.8,dpright:h0.2,leftx:h0.6,lefty:h0.12,rightx:h0.9,righty:h0.4,lefttrigger:b6,righttrigger:b7,platform:Windows, +d6206dca000000000000504944564944,PowerA Pro Ex,a:b1,b:b2,x:b0,y:b3,back:b8,guide:b12,start:b9,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.0,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7,platform:Windows, +a3060cff000000000000504944564944,Saitek P2500,a:b2,b:b3,y:b1,x:b0,start:b4,guide:b10,back:b5,leftstick:b8,rightstick:b9,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,platform:Windows, +8f0e0300000000000000504944564944,Trust GTX 28,a:b2,b:b1,y:b0,x:b3,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7,platform:Windows, +4f0415b3000000000000504944564944,Thrustmaster Dual Analog 3.2,platform:Windows,x:b1,a:b0,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, + +# OS X +0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X, +6d0400000000000016c2000000000000,Logitech F310 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, +6d0400000000000018c2000000000000,Logitech F510 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, +6d040000000000001fc2000000000000,Logitech F710 Gamepad (XInput),a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, +6d0400000000000019c2000000000000,Logitech Wireless Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, +4c050000000000006802000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Mac OS X, +4c05000000000000c405000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,Platform:Mac OS X, +5e040000000000008e02000000000000,X360 Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, +891600000000000000fd000000000000,Razer Onza Tournament,a:b0,b:b1,y:b3,x:b2,start:b8,guide:b10,back:b9,leftstick:b6,rightstick:b7,leftshoulder:b4,rightshoulder:b5,dpup:b11,dpleft:b13,dpdown:b12,dpright:b14,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Mac OS X, +4f0400000000000000b3000000000000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,y:b3,x:b1,start:b10,guide:b8,back:b9,leftstick:b11,rightstick:,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Mac OS X, +8f0e0000000000000300000000000000,Piranha xtreme,platform:Mac OS X,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +0d0f0000000000004d00000000000000,HORI Gem Pad 3,platform:Mac OS X,a:b1,b:b2,y:b3,x:b0,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7, +79000000000000000600000000000000,G-Shark GP-702,a:b2,b:b1,x:b3,y:b0,back:b8,start:b9,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:b6,righttrigger:b7,platform:Mac OS X, +4f0400000000000015b3000000000000,Thrustmaster Dual Analog 3.2,platform:Mac OS X,x:b1,a:b0,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, + +# Linux +0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, +03000000ba2200002010000001010000,Jess Technology USB Game Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux, +030000006d04000019c2000010010000,Logitech Cordless RumblePad 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, +030000006d0400001dc2000014400000,Logitech F310 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000006d0400001ec2000020200000,Logitech F510 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000006d04000019c2000011010000,Logitech F710 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, +030000006d0400001fc2000005030000,Logitech F710 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000004c0500006802000011010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux, +030000004c050000c405000011010000,Sony DualShock 4,a:b1,b:b2,y:b3,x:b0,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a5,lefttrigger:b6,righttrigger:b7,platform:Linux, +03000000de280000ff11000001000000,Valve Streaming Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e0400008e02000014010000,X360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e0400008e02000010010000,X360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e0400001907000000010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +03000000100800000100000010010000,Twin USB PS2 Adapter,a:b2,b:b1,y:b0,x:b3,start:b9,guide:,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a2,lefttrigger:b4,righttrigger:b5,platform:Linux, +03000000a306000023f6000011010000,Saitek Cyborg V.1 Game Pad,a:b1,b:b2,y:b3,x:b0,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a4,lefttrigger:b6,righttrigger:b7,platform:Linux, +030000004f04000020b3000010010000,Thrustmaster 2 in 1 DT,a:b0,b:b2,y:b3,x:b1,start:b9,guide:,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Linux, +030000004f04000023b3000000010000,Thrustmaster Dual Trigger 3-in-1,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a5, +030000008f0e00000300000010010000,GreenAsia Inc. USB Joystick ,platform:Linux,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +030000008f0e00001200000010010000,GreenAsia Inc. USB Joystick ,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +030000005e0400009102000007010000,X360 Wireless Controller,a:b0,b:b1,y:b3,x:b2,start:b7,guide:b8,back:b6,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:b13,dpleft:b11,dpdown:b14,dpright:b12,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux, +030000006d04000016c2000010010000,Logitech Logitech Dual Action,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, +03000000260900008888000000010000,GameCube {WiseGroup USB box},a:b0,b:b2,y:b3,x:b1,start:b7,leftshoulder:,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,rightstick:,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,platform:Linux, +030000006d04000011c2000010010000,Logitech WingMan Cordless RumblePad,a:b0,b:b1,y:b4,x:b3,start:b8,guide:b5,back:b2,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:b9,righttrigger:b10,platform:Linux, +030000006d04000018c2000010010000,Logitech Logitech RumblePad 2 USB,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, +05000000d6200000ad0d000001000000,Moga Pro,platform:Linux,a:b0,b:b1,y:b3,x:b2,start:b6,leftstick:b7,rightstick:b8,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a5,righttrigger:a4, +030000004f04000009d0000000010000,Thrustmaster Run N Drive Wireless PS3,platform:Linux,a:b1,b:b2,x:b0,y:b3,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7, +030000004f04000008d0000000010000,Thrustmaster Run N Drive Wireless,platform:Linux,a:b1,b:b2,x:b0,y:b3,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a5,lefttrigger:b6,righttrigger:b7, +0300000000f000000300000000010000,RetroUSB.com RetroPad,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Linux, +0300000000f00000f100000000010000,RetroUSB.com Super RetroPort,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Linux, +030000006f0e00001f01000000010000,Generic X-Box pad,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b6,guide:b8,start:b7,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,leftstick:b9,rightstick:b10,leftx:a0,lefty:a1,rightx:a3,righty:a4, +03000000280400000140000000010000,Gravis GamePad Pro USB ,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftx:a0,lefty:a1, +030000005e0400008902000021010000,Microsoft X-Box pad v2 (US),platform:Linux,x:b3,a:b0,b:b1,y:b4,back:b6,start:b7,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b5,lefttrigger:a2,rightshoulder:b2,righttrigger:a5,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a3,righty:a4, +030000006f0e00001e01000011010000,Rock Candy Gamepad for PS3,platform:Linux,a:b1,b:b2,x:b0,y:b3,back:b8,start:b9,guide:b12,leftshoulder:b4,rightshoulder:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2, +03000000250900000500000000010000,Sony PS2 pad with SmartJoy adapter,platform:Linux,a:b2,b:b1,y:b0,x:b3,start:b8,back:b9,leftstick:b10,rightstick:b11,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b4,righttrigger:b5, +030000008916000000fd000024010000,Razer Onza Tournament,a:b0,b:b1,y:b3,x:b2,start:b7,guide:b8,back:b6,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:b13,dpleft:b11,dpdown:b14,dpright:b12,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux, +030000004f04000000b3000010010000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,y:b3,x:b1,start:b10,guide:b8,back:b9,leftstick:b11,rightstick:b12,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Linux, +03000000ad1b000001f5000033050000,Hori Pad EX Turbo 2,a:b0,b:b1,y:b3,x:b2,start:b7,guide:b8,back:b6,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux, +050000004c050000c405000000010000,PS4 Controller (Bluetooth),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, +060000004c0500006802000000010000,PS3 Controller (Bluetooth),a:b14,b:b13,y:b12,x:b15,start:b3,guide:b16,back:b0,leftstick:b1,rightstick:b2,leftshoulder:b10,rightshoulder:b11,dpup:b4,dpleft:b7,dpdown:b6,dpright:b5,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b8,righttrigger:b9,platform:Linux, +03000000790000000600000010010000,DragonRise Inc. Generic USB Joystick ,platform:Linux,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a4, +03000000666600000488000000010000,Super Joy Box 5 Pro,platform:Linux,a:b2,b:b1,x:b3,y:b0,back:b9,start:b8,leftshoulder:b6,rightshoulder:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b4,righttrigger:b5,dpup:b12,dpleft:b15,dpdown:b14,dpright:b13, +05000000362800000100000002010000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,platform:Linux,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2, +05000000362800000100000003010000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,platform:Linux,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2, +030000008916000001fd000024010000,Razer Onza Classic Edition,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b6,guide:b8,start:b7,dpleft:b11,dpdown:b14,dpright:b12,dpup:b13,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,leftstick:b9,rightstick:b10,leftx:a0,lefty:a1,rightx:a3,righty:a4, +030000005e040000d102000001010000,Microsoft X-Box One pad,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b6,guide:b8,start:b7,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,leftstick:b9,rightstick:b10,leftx:a0,lefty:a1,rightx:a3,righty:a4, \ No newline at end of file diff --git a/Client2018/content/fonts/humanoidSoundNewLocal.rbxmx b/Client2018/content/fonts/humanoidSoundNewLocal.rbxmx new file mode 100644 index 0000000..d0fb982 --- /dev/null +++ b/Client2018/content/fonts/humanoidSoundNewLocal.rbxmx @@ -0,0 +1,191 @@ + + null + nil + + + false + + Sound + + + + + false + + LocalSound + 0.1) then + local vol = math.min(1.0, math.max(0.0, (fallSpeed - 50) / 110)) + sLanding.Volume = vol + sLanding:Play() + fallSpeed = 0 + end + if speed>0.5 then + sRunning:Resume() + sRunning.Pitch = speed / 8.0 + else + sRunning:Pause() + end + prevState = "Run" +end + +function onSwimming(speed) + if (prevState ~= "Swim" and speed > 0.1) then + local volume = math.min(1.0, speed / 350) + sSplash.Volume = volume + sSplash:Play() + prevState = "Swim" + end + sClimbing:Stop() + sRunning:Stop() + sSwimming.Pitch = 1.6 + sSwimming:Resume() +end + +function onClimbing(speed) + sRunning:Stop() + sSwimming:Stop() + if speed>0.01 then + sClimbing:Resume() + sClimbing.Pitch = speed / 5.5 + else + sClimbing:Pause() + end + prevState = "Climb" +end +-- connect up + +function stopLoopedSounds() + sRunning:Stop() + sClimbing:Stop() + sSwimming:Stop() +end + +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Swimming:connect(onSwimming) +Humanoid.Climbing:connect(onClimbing) +Humanoid.Jumping:connect(function(state) onStateNoStop(state, sJumping) prevState = "Jump" end) +Humanoid.GettingUp:connect(function(state) stopLoopedSounds() onStateNoStop(state, sGettingUp) prevState = "GetUp" end) +Humanoid.FreeFalling:connect(function(state) stopLoopedSounds() onStateFall(state, sFreeFalling) prevState = "FreeFall" end) +Humanoid.FallingDown:connect(function(state) stopLoopedSounds() end) +Humanoid.StateChanged:connect(function(old, new) + if not (new.Name == "Dead" or + new.Name == "Running" or + new.Name == "RunningNoPhysics" or + new.Name == "Swimming" or + new.Name == "Jumping" or + new.Name == "GettingUp" or + new.Name == "Freefall" or + new.Name == "FallingDown") then + stopLoopedSounds() + end +end) + +]]> + + + + \ No newline at end of file diff --git a/Client2018/content/fonts/zekton_rg.ttf b/Client2018/content/fonts/zekton_rg.ttf new file mode 100644 index 0000000..8ad2dd1 Binary files /dev/null and b/Client2018/content/fonts/zekton_rg.ttf differ diff --git a/Client2018/content/internal/AppShell/.luacheckrc b/Client2018/content/internal/AppShell/.luacheckrc new file mode 100644 index 0000000..6470d75 --- /dev/null +++ b/Client2018/content/internal/AppShell/.luacheckrc @@ -0,0 +1,48 @@ +stds.roblox = { + globals = { + "game" + }, + read_globals = { + -- Roblox globals + "script", + "utf8", + + -- Extra functions + "tick", "warn", "spawn", + "wait", "settings", "typeof", + "UserSettings", + + -- Types + "Vector2", "Vector3", + "Color3", + "UDim", "UDim2", + "Rect", + "CFrame", + "Enum", + "Instance", + "TweenInfo", + } +} + +stds.testez = { + read_globals = { + "describe", + "it", "itFOCUS", "itSKIP", + "FOCUS", "SKIP", "HACK_NO_XPCALL", + "expect", + } +} + +ignore = { + "212", -- unused arguments + "421", -- shadowing local variable + "422", -- shadowing argument + "431", -- shadowing upvalue + "432", -- shadowing upvalue argument +} + +std = "lua51+roblox" + +files["**/*.spec.lua"] = { + std = "+testez", +} diff --git a/Client2018/content/internal/AppShell/Modules/Shell/AccountLinkingView.lua b/Client2018/content/internal/AppShell/Modules/Shell/AccountLinkingView.lua new file mode 100644 index 0000000..137df7e --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/AccountLinkingView.lua @@ -0,0 +1,285 @@ +local XboxUseUnlinkCallback = settings():GetFFlag("XboxUseUnlinkCallback") + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local ContextActionService = game:GetService('ContextActionService') + +local AccountManager = require(ShellModules:FindFirstChild('AccountManager')) +local AssetManager = require(ShellModules:FindFirstChild('AssetManager')) +local Errors = require(ShellModules:FindFirstChild('Errors')) +local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader')) +local UnlinkAccountOverlay = require(ShellModules:FindFirstChild('UnlinkAccountOverlay')) +local XboxAppState = require(ShellModules:FindFirstChild('AppState')) + +local function createAccountLinkingView() + local this = {} + + local gamerTag = XboxAppState.store:getState().XboxUser.gamertag + local robloxName = XboxAppState.store:getState().RobloxUser.robloxName + local rbxuid = XboxAppState.store:getState().RobloxUser.rbxuid + local linkedAsPhrase = string.format(Strings:LocalizedString('LinkedAsPhrase'), gamerTag, robloxName) + + local dummySelection = Utility.Create'Frame' + { + BackgroundTransparency = 1; + } + + local Container = Utility.Create'Frame' + { + Name = "Container"; + Position = UDim2.new(0, 0, 0, 0); + Size = UDim2.new(0, 765, 0, 630); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Selectable = true; + SelectionImageObject = dummySelection; + } + + Utility.Create'ImageLabel' + { + Name = "GamerPic"; + Position = UDim2.new(0, 40, 0, 25); + Size = UDim2.new(0, 300, 0, 300); + BackgroundTransparency = 0; + BorderSizePixel = 0; + Image = 'rbxapp://xbox/localgamerpic'; + Parent = Container; + } + + Utility.Create'ImageLabel' + { + Name = "AccountLinkIcon"; + Position = UDim2.new(0, 354, 0, 166); + Size = UDim2.new(0, 58, 0, 20); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Shell/Icons/AccountLinkIcon.png"; + Parent = Container; + } + + local ProfileImage = Utility.Create'ImageLabel' + { + Name = "ProfileImage"; + Position = UDim2.new(0, 425, 0, 25); + Size = UDim2.new(0, 300, 0, 300); + BackgroundTransparency = 0; + BackgroundColor3 = GlobalSettings.CharacterBackgroundColor; + BorderSizePixel = 0; + Parent = Container; + } + + if rbxuid then + spawn(function() + local thumbnailSize = ThumbnailLoader.AvatarSizes.Size352x352 + local thumbLoader = ThumbnailLoader:LoadAvatarThumbnailAsync(ProfileImage, rbxuid, + Enum.ThumbnailType.AvatarThumbnail, Enum.ThumbnailSize.Size352x352, true) + thumbLoader:LoadAsync() + ProfileImage.ImageRectSize = Vector2.new(thumbnailSize.X, (1) * thumbnailSize.X) + end) + end + + Utility.Create'TextLabel' + { + Name = 'ProfileLabel'; + Position = UDim2.new(0, 575, 0, 335); + + Text = robloxName or ''; + TextXAlignment = 'Center'; + TextYAlignment = 'Top'; + + BackgroundColor3 = Color3.new(1,0,0); + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.SubHeaderSize; + BackgroundTransparency = 1; + Parent = Container; + } + + Utility.Create'TextLabel' + { + Name = 'GamerLabel'; + Text = gamerTag or ''; + TextXAlignment = 'Center'; + TextYAlignment = 'Top'; + + Position = UDim2.new(0, 190, 0, 335); + + BackgroundColor3 = Color3.new(1,0,0); + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.SubHeaderSize; + BackgroundTransparency = 1; + Parent = Container; + } + + Utility.Create'TextLabel' + { + Name = "LinkedAsText"; + Position = UDim2.new(0, 40, 0, 395); + Size = UDim2.new(0, 686, 0, 120); + Text = linkedAsPhrase; + TextXAlignment = 'Center'; + TextYAlignment = 'Top'; + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + TextColor3 = GlobalSettings.GreyTextColor; + FontSize = GlobalSettings.SubHeaderSize; + TextWrapped = true; + Parent = Container; + } + + local UnlinkButton = Utility.Create'ImageButton' + { + Name = "UnlinkButton"; + Position = UDim2.new(0, 220, 0, 520); + Size = UDim2.new(0, 320, 0, 80); + BackgroundTransparency = 1; + ImageColor3 = GlobalSettings.GreySelectedButtonColor; + Image = GlobalSettings.RoundCornerButtonImage; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28)); + ZIndex = 2; + Parent = Container; + SoundManager:CreateSound('MoveSelection'); + AssetManager.CreateShadow(1) + } + + local DefaultButtonColor = GlobalSettings.GreyButtonColor + local SelectedButtonColor = GlobalSettings.GreySelectedButtonColor + local DefaultButtonTextColor = GlobalSettings.WhiteTextColor + local SelectedButtonTextColor = GlobalSettings.TextSelectedColor + + local UnlinkText = Utility.Create'TextLabel' + { + Name = "UnlinkText"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = Color3.new(0,0,0); + Text = Strings:LocalizedString("UnlinkGamerTagWord"); + ZIndex = 2; + Parent = UnlinkButton; + } + Utility.ResizeButtonWithText(UnlinkButton, UnlinkText, GlobalSettings.TextHorizontalPadding) + + Container.SelectionGained:connect(function() + Utility.SetSelectedCoreObject(UnlinkButton) + end) + + UnlinkButton.SelectionGained:connect(function() + UnlinkButton.ImageColor3 = SelectedButtonColor + UnlinkText.TextColor3 = SelectedButtonTextColor + end) + UnlinkButton.SelectionLost:connect(function() + UnlinkButton.ImageColor3 = DefaultButtonColor + UnlinkText.TextColor3 = DefaultButtonTextColor + end) + + local ModalOverlay = Utility.Create'Frame' + { + Name = "ModalOverlay"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = GlobalSettings.ModalBackgroundTransparency; + BackgroundColor3 = GlobalSettings.ModalBackgroundColor; + BorderSizePixel = 0; + ZIndex = 4; + } + + local isUnlinking = false + local function unlinkAccountAsync() + if isUnlinking then return end + isUnlinking = true + local unlinkResult = nil + local loader = LoadingWidget( + { Parent = Container }, { + function() + unlinkResult = AccountManager:UnlinkAccountAsync() + end + }) + + -- set up full screen loader + ModalOverlay.Parent = GuiRoot + ContextActionService:BindCoreAction("BlockB", function() end, false, Enum.KeyCode.ButtonB) + UnlinkButton.SelectionImageObject = dummySelection + UnlinkButton.ImageColor3 = GlobalSettings.GreyButtonColor + UnlinkText.TextColor3 = GlobalSettings.WhiteTextColor + + -- call loader + loader:AwaitFinished() + + -- clean up + -- NOTE: Unlink success will fire the ThirdPartyUserService ActiveUserSignedOut event. + -- This event will fire and listeners will run before the loader is finished. The below + -- code needs to run in case of errors, but on success will not interfere with the reauth + -- logic in AppHome.lua + loader:Cleanup() + UnlinkButton.SelectionImageObject = nil + UnlinkButton.ImageColor3 = GlobalSettings.GreySelectedButtonColor + UnlinkText.TextColor3 = GlobalSettings.TextSelectedColor + ContextActionService:UnbindCoreAction("BlockB") + ModalOverlay.Parent = nil + + if unlinkResult ~= AccountManager.AuthResults.Success then + local err = unlinkResult and Errors.Authentication[unlinkResult] or Errors.Default + ScreenManager:OpenScreen(ErrorOverlay(err), false) + end + isUnlinking = false + end + + function this:Focus() + if XboxUseUnlinkCallback then + return + end + + EventHub:addEventListener(EventHub.Notifications["UnlinkAccountConfirmation"], "unlinkAccount", + function() + unlinkAccountAsync() + end + ) + end + + function this:RemoveFocus() + if XboxUseUnlinkCallback then + return + end + + EventHub:removeEventListener(EventHub.Notifications["UnlinkAccountConfirmation"], "unlinkAccount") + end + + UnlinkButton.MouseButton1Click:connect(function() + if isUnlinking then return end + SoundManager:Play('ButtonPress') + local confirmTitleAndMsg = { Title = Strings:LocalizedString("UnlinkTitle"), + Msg = Strings:LocalizedString("UnlinkPhrase") } + + if XboxUseUnlinkCallback then + ScreenManager:OpenScreen(UnlinkAccountOverlay(confirmTitleAndMsg, unlinkAccountAsync), false) + else + ScreenManager:OpenScreen(UnlinkAccountOverlay(confirmTitleAndMsg), false) + end + end) + + --[[ Public API ]]-- + function this:SetParent(newParent) + Container.Parent = newParent + end + + function this:GetUnlinkButton() + return UnlinkButton + end + + return this +end + +return createAccountLinkingView diff --git a/Client2018/content/internal/AppShell/Modules/Shell/AccountManager.lua b/Client2018/content/internal/AppShell/Modules/Shell/AccountManager.lua new file mode 100644 index 0000000..a3bb8da --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/AccountManager.lua @@ -0,0 +1,204 @@ +--[[ + // AccountManager.lua + + // Handles all account related functions +]] +local IsNewUsernameCheckEnabled = settings():GetFFlag("XboxUseNewValidUsernameCheck2") + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) +local UserInputService = game:GetService('UserInputService') +local AnalyticsService = game:GetService("AnalyticsService") + +local Http = require(ShellModules:FindFirstChild('Http')) + +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local AccountManager = {} + +AccountManager.AuthResults = { + Error = -1; + Success = 0; + InProgress = 1; + AccountUnlinked = 2; + MissingGamePad = 3; + NoUserDetected = 4; + HttpErrorDetected = 5; + SignUpDisabled = 6; + Flooded = 7; + LeaseLocked = 8; + AccountLinkingDisabled = 9; + InvalidRobloxUser = 10; + RobloxUserAlreadyLinked = 11; + XboxUserAlreadyLinked = 12; + IllgealChildAccountLinking = 13; + InvalidPassword = 14; + UsernamePasswordNotSet = 15; + UsernameAlreadyTaken = 16; +} + +AccountManager.InvalidUsernameReasons = { + Valid = "Valid"; + InvalidUsername = "Invalid Username"; + AlreadyTaken = "Already Taken"; + InvalidCharactersUsed = "Invalid Characters Used"; + UsernameCannotContainSpaces = "Username Cannot Contain Spaces"; +} + +--[[ Authentication ]]-- +local function authenticateStudio() + return AccountManager.AuthResults.Success +end + +--[[ Signup/Login ]]-- +function AccountManager:LoginAsync() + if UserSettings().GameSettings:InStudioMode() or UserInputService:GetPlatform() == Enum.Platform.Windows then + return authenticateStudio() + end + + local success, result = pcall(function() + return PlatformService:BeginPlatformLogin() + end) + -- catch pcall failure, something went wrong with API call + if not success then + return self.AuthResults.Error + end + + return result +end + +function AccountManager:SignupAsync(username, password) + local success, result = pcall(function() + return PlatformService:BeginPlatformSignup(username, password) + end) + + -- catch pcall failure, something went wrong with the API call + if not success then + result = self.AuthResults.Error + end + + if result == self.AuthResults.Success then + AnalyticsService:ReportCounter("Xbox_SignUp_New_Account_Success") + AnalyticsService:ReportCounter("Xbox_SignUp_Success") + end + + return result +end + +--[[ Account Linking ]]-- +-- called at sign in +function AccountManager:LinkAccountAsync(accountName, password) + local success, result = pcall(function() + -- PlatformService may not exist on studio platform + return PlatformService:BeginAccountLink(accountName, password) + end) + if not success then + Utility.DebugLog("AccountManager:LinkAccountAsync() failed because", result) + result = AccountManager.AuthResults.Error + end + + if result == self.AuthResults.Success then + AnalyticsService:ReportCounter("Xbox_SignUp_Account_Link_Success") + AnalyticsService:ReportCounter("Xbox_SignUp_Success") + end + + return result +end + +-- used when setting credentials for a generated account +function AccountManager:SetRobloxCredentialsAsync(accountName, password) + local success, result = pcall(function() + -- PlatformService may not exist on studio platform + return PlatformService:BeginSetRobloxCredentials(accountName, password) + end) + if not success then + Utility.DebugLog("AccountManager:SetRobloxCredentialsAsync() failed because", result) + result = AccountManager.AuthResults.Error + end + + return result +end + +-- called when user has roblox credentials +function AccountManager:UnlinkAccountAsync() + local success, result = pcall(function() + -- PlatformService may not exist on studio platform + return PlatformService:BeginUnlinkAccount() + end) + if not success then + Utility.DebugLog("AccountManager:UnlinkAccountAsync() failed because", result) + result = AccountManager.AuthResults.Error + end + + return result +end + +function AccountManager:HasLinkedAccountAsync() + if UserSettings().GameSettings:InStudioMode() or UserInputService:GetPlatform() == Enum.Platform.Windows then + return AccountManager.AuthResults.Success + end + + local success, result = pcall(function() + -- PlatformService may not exist on studio platform + return PlatformService:BeginHasLinkedAccount() + end) + if not success then + Utility.DebugLog("AccountManager:HasLinkedAccountAsync() failed because", result) + result = AccountManager.AuthResults.Error + end + + return result +end + +function AccountManager:HasRobloxCredentialsAsync() + if UserSettings().GameSettings:InStudioMode() or UserInputService:GetPlatform() == Enum.Platform.Windows then + return AccountManager.AuthResults.Success + end + + local success, result = pcall(function() + -- PlatformService may not exist on studio platform + return PlatformService:BeginHasRobloxCredentials() + end) + if not success then + Utility.DebugLog("AccountManager:HasRobloxCredentialsAsync() failed because", result) + result = AccountManager.AuthResults.Error + end + + return result +end + +function AccountManager:IsValidUsernameAsync(username) + local result = Http.IsValidUsername(username) + if not result then + -- return false + return nil + end + + -- there are two endpoints being used based on flag, they return different casing, check for both + -- old returns IsValid and ErrorMessage + -- new returns isValid and errorMessage + if IsNewUsernameCheckEnabled then + return result["isValid"], result["errorMessage"] + else + return result["IsValid"], result["ErrorMessage"] + end + + return nil +end + +function AccountManager:IsValidPasswordAsync(username, password) + local result = Http.IsValidPassword(username, password) + if not result then + -- return false + return nil + end + + return result["IsValid"], result["ErrorMessage"] +end + +return AccountManager diff --git a/Client2018/content/internal/AppShell/Modules/Shell/AccountScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/AccountScreen.lua new file mode 100644 index 0000000..b730a9f --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/AccountScreen.lua @@ -0,0 +1,84 @@ +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local AccountLinkingView = require(ShellModules:FindFirstChild('AccountLinkingView')) +local GameplaySettingsView = require(ShellModules:FindFirstChild('GameplaySettingsView')) + +local function createAccountScreen(errorCode) + local this = BaseScreen() + + this:SetTitle(Strings:LocalizedString("AccountSettingsTitle")) + + local AccountLinkingViewContainer = Utility.Create'Frame' + { + Name = "AccountLinkingViewContainer"; + Position = UDim2.new(0, 75, 0, 275); + Size = UDim2.new(0, 765, 0, 630); + BorderSizePixel = 0; + BackgroundTransparency = 1; + Parent = this.Container + } + + local accountLinkingView = AccountLinkingView() + accountLinkingView:SetParent(AccountLinkingViewContainer) + + local ScreenDivide = Utility.Create'Frame' + { + Name = "ScreenDivide"; + Size = UDim2.new(0, 2, 0, 615); + Position = UDim2.new(0, 840, 0, 275); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.PageDivideColor; + Parent = this.Container; + } + + local gameplaySettingsViewContainer = Utility.Create'Frame' + { + Name = "GameplaySettingsViewContainer"; + Position = UDim2.new(0, 840, 0, 275); + Size = UDim2.new(0, 765, 0, 630); + BorderSizePixel = 0; + BackgroundTransparency = 1; + Parent = this.Container; + } + + local gameplaySettingsView = GameplaySettingsView(errorCode) + gameplaySettingsView:SetParent(gameplaySettingsViewContainer) + + --[[ Public API ]]-- + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('UnlinkAccountScreenId')} + end + + -- Override + function this:GetDefaultSelectionObject() + return accountLinkingView:GetUnlinkButton() + end + + -- Override + local baseFocus = this.Focus + function this:Focus() + baseFocus(self) + accountLinkingView:Focus() + gameplaySettingsView:Focus() + end + + -- Override + local baseRemoveFocus = this.RemoveFocus + function this:RemoveFocus() + baseRemoveFocus(self) + accountLinkingView:RemoveFocus() + end + + return this +end + +return createAccountScreen diff --git a/Client2018/content/internal/AppShell/Modules/Shell/AchievementManager.lua b/Client2018/content/internal/AppShell/Modules/Shell/AchievementManager.lua new file mode 100644 index 0000000..24d0643 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/AchievementManager.lua @@ -0,0 +1,388 @@ +-- Written by Kip Turner, Copyright Roblox 2015 + +-- Achievement Manager +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) +local ThirdPartyUserService = nil +pcall(function() ThirdPartyUserService = game:GetService('ThirdPartyUserService') end) + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local Http = require(ShellModules:FindFirstChild('Http')) +local UserData = require(ShellModules:FindFirstChild('UserData')) +local PlatformInterface = require(ShellModules:FindFirstChild('PlatformInterface')) +local Alerts = require(ShellModules:FindFirstChild('Alerts')) +local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local SortsData = require(ShellModules:FindFirstChild('SortsData')) +local XboxAppState = require(ShellModules:FindFirstChild('AppState')) + + +--[[ ACHIEVEMENT NAMES --]] + -- "Award10DayRoll" + -- "Award20DayRoll" + -- "Award3DayRoll" + -- "AwardDeepDiver" + -- "AwardFoursCompany" + -- "AwardOneNameManyFaces" + -- "AwardPollster" + -- "AwardSampler" + -- "AwardStrengthInNumbers" + -- "AwardWorldTraveler" + -- "AwardYouDidIt" + -- "GameProgress" + -- "MultiplayerRoundEnd" + -- "MultiplayerRoundStart" + -- "PlayerSessionEnd" + -- "PlayerSessionPause" + -- "PlayerSessionResume" + -- "PlayerSessionStart" + -- "Test_XPresses" +--[[ END OF ACHIEVEMENT NAMES --]] + +local VIEW_GAMETYPE_ENUM = +{ + AppShell = 0; + Game = 1; +} + +local GAMES_FOR_YOU_DID_IT = 1 +local GAMES_FOR_AWARD_SAMPLER = 5 + +local DAYS_FOR_3DAYROLL = 3 +local DAYS_FOR_10DAYROLL = 10 +local DAYS_FOR_20DAYROLL = 20 + +local GAMES_RATED_FOR_POLLSTER = 5 + +local PLAY_SECONDS_FOR_DEEP_DIVER = 60 * 60 + +local NUMBER_OF_FRIENDS_REQUIRED_FOR_FOURS_COMPANY = 3 + + +local SECONDS_BETWEEN_FOURS_COMPANY_CHECKS = 30 + + +local AchievementManager = {} + +local CurrentView = VIEW_GAMETYPE_ENUM['AppShell'] + +local partyUpdateConn = nil + +AchievementManager.AchivementId = { + Scout = "2"; + Explorer = "3"; + Trailblazer = "4"; + Pollster = "5"; + Marathon = "6"; + OneNameManyFaces = "7"; + ThreeDayRoll = "8"; + TenDayRoll = "9"; + TwentyDayRoll = "10"; + StrengthInNumbers = "11"; + FoursCompany = "12"; +} +-- Map ID to trigger name so we can do a look up in SessionAchievementState +local AchievementIdToTrigger = { + ["2"] = "AwardYouDidIt"; + ["3"] = "AwardSampler"; + ["4"] = "AwardWorldTraveler"; + ["5"] = "AwardPollster"; + ["6"] = "AwardDeepDiver"; + ["7"] = "AwardOneNameManyFaces"; + ["8"] = "Award3DayRoll"; + ["9"] = "Award10DayRoll"; + ["10"] = "Award20DayRoll"; + ["11"] = "AwardStrengthInNumbers"; + ["12"] = "AwardFoursCompany"; +} + +local SessionAchievementState = {} + +local function GetTotalNumberOfGamesOnXbox() + -- TODO: is there a programmatic way of figuring this out? + return 15 +end + +local function FilterInGameFriends(onlineFriends, playersInGame) + local result = {} + + if onlineFriends and playersInGame then + -- Create reverse lookup for speed + local playersInGameReverseLookup = {} + for _, playerInGame in pairs(playersInGame) do + -- TODO: Figure out what the actual lookup + if playerInGame['robloxuid'] then + playersInGameReverseLookup[playerInGame['robloxuid']] = true + end + end + + for _, friend in pairs(onlineFriends) do + if playersInGameReverseLookup[friend['robloxuid']] then + table.insert(result, friend) + end + end + end + + return result +end + + +local function OnPlayedGamesChanged() + EventHub:dispatchEvent(EventHub.Notifications["PlayedGamesChanged"]) + spawn(function() + local myUserId = XboxAppState.store:getState().RobloxUser.rbxuid + if myUserId then + local recentCollection + -- TODO: is this the right way of getting num of played games? + local recentlyPage1 + recentCollection = SortsData:GetUserRecent() + recentlyPage1 = recentCollection and recentCollection:GetSortAsync(0, GetTotalNumberOfGamesOnXbox()) + local gamesPlayed = recentlyPage1 and #recentlyPage1 or 0 + + Utility.DebugLog("You have played:" , gamesPlayed , "games" ) + if gamesPlayed >= GAMES_FOR_YOU_DID_IT then + AchievementManager:SendAchievementEventAsync("AwardYouDidIt") + end + + local hasExplorerAchievement = AchievementManager:HasAchievementAsync(AchievementManager.AchivementId.Explorer) + if gamesPlayed >= GAMES_FOR_AWARD_SAMPLER then + AchievementManager:SendAchievementEventAsync("AwardSampler") + + -- if we didn't have it before then let's unlock UGC + if not hasExplorerAchievement then + EventHub:dispatchEvent(EventHub.Notifications["UnlockedUGC"], true) + else + EventHub:dispatchEvent(EventHub.Notifications["UnlockedUGC"], false) + end + else + --if the user has the ExplorerAchievement but hasn't played 5 games + --(may happen when user links a new Roblox account to the Xbox account which already has the ExplorerAchievement) + if hasExplorerAchievement then + EventHub:dispatchEvent(EventHub.Notifications["UnlockedUGC"], false) + end + end + if gamesPlayed >= GetTotalNumberOfGamesOnXbox() then + AchievementManager:SendAchievementEventAsync("AwardWorldTraveler") + end + end + end) +end + +local function OnJoinedGame() + spawn(function() + Utility.DebugLog("OnJoinGame: Fours Company check") + + if PlatformService then + local lastCheck = 0 + while CurrentView == VIEW_GAMETYPE_ENUM['Game'] do + local now = tick() + if now - lastCheck > SECONDS_BETWEEN_FOURS_COMPANY_CHECKS then + + local friendsData = require(ShellModules:FindFirstChild('FriendsData')) + local onlineFriends = friendsData.GetOnlineFriendsAsync() + + -- TODO: add actually API + local inGamePlayers = PlatformService:GetInGamePlayers() + if inGamePlayers and onlineFriends then + local inGameFriends = FilterInGameFriends(onlineFriends, inGamePlayers) + + if #inGameFriends >= NUMBER_OF_FRIENDS_REQUIRED_FOR_FOURS_COMPANY then + AchievementManager:SendAchievementEventAsync("AwardFoursCompany") + return + end + + end + lastCheck = now + end + wait(1) + end + end + end) + spawn(function() + local startTime = tick() + while tick() - startTime < PLAY_SECONDS_FOR_DEEP_DIVER do + if CurrentView ~= VIEW_GAMETYPE_ENUM['Game'] then + return + end + wait(1) + end + if CurrentView == VIEW_GAMETYPE_ENUM['Game'] then + AchievementManager:SendAchievementEventAsync("AwardDeepDiver") + end + end) +end + +local function CheckStrengthInNumbers() + Utility.DebugLog("PartyTitlePresenceChanged: AwardStrengthInNumbers check") + local partyMembers = PlatformInterface:GetPartyMembersAsync() + if partyMembers then + if PlatformInterface:IsInAParty(partyMembers) then + AchievementManager:SendAchievementEventAsync("AwardStrengthInNumbers") + if partyUpdateConn then + partyUpdateConn:disconnect() + partyUpdateConn = nil + end + end + end +end + +function AchievementManager:SendAchievementEventAsync(achievementName) + -- Always set session state to awarded, we don't want to block session related achievement progress (UGC for example) + -- see: https://forums.xboxlive.com/articles/56661/achievements-and-when-they-arent-unlocking-1.html + -- there is an issue with xbox live granting achievements + SessionAchievementState[achievementName] = true + + Utility.DebugLog("Achievement Manager - Awarding achievement:" , achievementName) + local achievementStatus = nil + local success, msg = pcall(function() + -- NOTE: Yielding function + if not UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows then + achievementStatus = PlatformService:BeginAwardAchievement(achievementName) + end + end) + if not success then + -- NOTE: very likely this function ever throws an error but returns error codes + Utility.DebugLog("Achievement Manager - Unable to award achievement:" , achievementName , "for reason:" , msg) + end + + Utility.DebugLog("Achievement Manager - Achievement:" , achievementName , "event status:" , achievementStatus) +end + +EventHub:addEventListener(EventHub.Notifications["UnlockedUGC"], "ShowUnlockedUGCOverlay", + function(ShowAlert) + if ShowAlert == true then + ScreenManager:OpenScreen(ErrorOverlay(Alerts.UnlockedUGC), false) + end + end) + +EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "AchievementManager", + function() + spawn(function() + local myUserId = XboxAppState.store:getState().RobloxUser.rbxuid + local function stillLoggedIn() + local newUserId = XboxAppState.store:getState().RobloxUser.rbxuid + return newUserId ~= nil and myUserId == newUserId + end + + if myUserId ~= nil then + local loggedInResult = Http.GetConsecutiveDaysLoggedInAsync() + local daysLoggedIn = loggedInResult and loggedInResult['count'] + if daysLoggedIn then + if daysLoggedIn >= DAYS_FOR_3DAYROLL and stillLoggedIn() then + AchievementManager:SendAchievementEventAsync("Award3DayRoll") + end + if daysLoggedIn >= DAYS_FOR_10DAYROLL and stillLoggedIn() then + AchievementManager:SendAchievementEventAsync("Award10DayRoll") + end + if daysLoggedIn >= DAYS_FOR_20DAYROLL and stillLoggedIn() then + AchievementManager:SendAchievementEventAsync("Award20DayRoll") + end + end + --Check if using new "StrengthInNumbers" achievement implementation + if PlatformService then + local hasStrengthInNumbersAchievement = AchievementManager:HasAchievementAsync(AchievementManager.AchivementId.StrengthInNumbers) + if not hasStrengthInNumbersAchievement then + --Haven't got "StrengthInNumbers" achievement + partyUpdateConn = PlatformService.PartyTitlePresenceChanged:connect(CheckStrengthInNumbers) + CheckStrengthInNumbers() + end + end + OnPlayedGamesChanged() + end + end) + end) + +EventHub:addEventListener(EventHub.Notifications["DonnedDifferentPackage"], "AchievementManager", + function(assetId) + AchievementManager:SendAchievementEventAsync("AwardOneNameManyFaces") + end) + +EventHub:addEventListener(EventHub.Notifications["AvatarEquipSuccess"], "AchievementManager", + function(assetId) + AchievementManager:SendAchievementEventAsync("AwardOneNameManyFaces") + end) + +EventHub:addEventListener(EventHub.Notifications["VotedOnPlace"], "AchievementManager", + function() + spawn(function() + local voteCount = UserData:GetVoteCount() + Utility.DebugLog("Vote Check: with vote count" , voteCount) + if voteCount >= GAMES_RATED_FOR_POLLSTER then + AchievementManager:SendAchievementEventAsync("AwardPollster") + end + end) + end) + +if PlatformService then + PlatformService.ViewChanged:connect(function(newView) + Utility.DebugLog("ViewChanged:" , newView) + CurrentView = newView + if newView == VIEW_GAMETYPE_ENUM['AppShell'] then + Utility.DebugLog("New view is appshell") + OnPlayedGamesChanged() + elseif newView == VIEW_GAMETYPE_ENUM['Game'] then + Utility.DebugLog("New view is game") + OnJoinedGame() + end + end) +end + +local UserChangedCount = 0 +if ThirdPartyUserService then + ThirdPartyUserService.ActiveUserSignedOut:connect(function() + UserChangedCount = UserChangedCount + 1 + SessionAchievementState = {} + end) +end + +function AchievementManager:HasAchievementAsync(achievementId) + local startCount = UserChangedCount + if UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows then + SessionAchievementState[AchievementIdToTrigger[achievementId]] = true + return true + end + + if SessionAchievementState[AchievementIdToTrigger[achievementId]] == true then + return true + end + + local success, result = pcall(function() + return PlatformService:BeginHasAchievement(achievementId) + end) + if not success then + return false + end + + --if the result is true, we use it to update SessionAchievementState + if startCount == UserChangedCount then + if result == true then + SessionAchievementState[AchievementIdToTrigger[achievementId]] = result + end + end + + --[[ + If the result is false or the API fails, we need to use our session state as backup. There is an edge case where locally we + grant the achievement, but xbox does not update/grant the achievement for some period of time (like xbox services being down). + We want to avoid ever locking the user from UGC if they have in fact unlocked it. + Two other cases remain. During the retro check, if xbox services are down, but we have in fact unlocked UGC because we've played + 5 games, we still unlock. The user will get the notification again however. + If the user unlocks UGC then unlinks their account and creates a new Roblox account, and if xbox services are down, since we + check games played, UGC will be locked for them. It will remain locked until they play 5 games on the new account, or until + xbox services come back up and we can correctly get the state of the achievement. Dan says this is both of these are OK. + ]] + return result or SessionAchievementState[AchievementIdToTrigger[achievementId]] +end + +function AchievementManager:AllGamesUnlocked() + local achievementId = AchievementManager.AchivementId.Explorer + return SessionAchievementState[AchievementIdToTrigger[achievementId]] +end + +return AchievementManager diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/AddError.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/AddError.lua new file mode 100644 index 0000000..f551af2 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/AddError.lua @@ -0,0 +1,14 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) + +return Action("AddError", function(error, timestamp) + error = error or {} + return { + error = { + Title = error.Title, + Msg = error.Msg, + Code = error.Code, + timestamp = timestamp + } + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/AddError.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/AddError.spec.lua new file mode 100644 index 0000000..ee6b6dd --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/AddError.spec.lua @@ -0,0 +1,39 @@ +return function() + it("should return a table", function() + local action = require(script.Parent.AddError) + + expect(action).to.be.a("table") + end) + + it("should return a table when called as a function", function() + local action = require(script.Parent.AddError)() + + expect(action).to.be.a("table") + end) + + it("should return a table with an error with same Title, Msg, Code set as the passed in error and aslo has the error timestamp appended", function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local ShellModules = Modules:FindFirstChild("Shell") + local Errors = require(ShellModules:FindFirstChild('Errors')) + local DefaultError = Errors.Default + local action = require(script.Parent.AddError)(DefaultError, tick()) + local error = action.error + expect(error).to.be.a("table") + expect(error.Title).to.equal(DefaultError.Title) + expect(error.Msg).to.equal(DefaultError.Msg) + expect(error.Code).to.equal(DefaultError.Code) + expect(error.timestamp).to.be.a("number") + end) + + it("should set the name", function() + local action = require(script.Parent.AddError) + + expect(action.name).to.equal("AddError") + end) + + it("should set the type", function() + local action = require(script.Parent.AddError)() + + expect(action.type).to.equal("AddError") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/DeleteError.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/DeleteError.lua new file mode 100644 index 0000000..753f7bc --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/DeleteError.lua @@ -0,0 +1,13 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) + +return Action("DeleteError", function(error) + error = error or {} + return { + error = { + Title = error.Title, + Msg = error.Msg, + Code = error.Code + } + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/DeleteError.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/DeleteError.spec.lua new file mode 100644 index 0000000..e96d4ff --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/DeleteError.spec.lua @@ -0,0 +1,38 @@ +return function() + it("should return a table", function() + local action = require(script.Parent.DeleteError) + + expect(action).to.be.a("table") + end) + + it("should return a table when called as a function", function() + local action = require(script.Parent.DeleteError)() + + expect(action).to.be.a("table") + end) + + it("should return a table with an error with same Title, Msg, Code set as the passed in error", function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local ShellModules = Modules:FindFirstChild("Shell") + local Errors = require(ShellModules:FindFirstChild('Errors')) + local DefaultError = Errors.Default + local action = require(script.Parent.DeleteError)(DefaultError) + local error = action.error + expect(error).to.be.a("table") + expect(error.Title).to.equal(DefaultError.Title) + expect(error.Msg).to.equal(DefaultError.Msg) + expect(error.Code).to.equal(DefaultError.Code) + end) + + it("should set the name", function() + local action = require(script.Parent.DeleteError) + + expect(action.name).to.equal("DeleteError") + end) + + it("should set the type", function() + local action = require(script.Parent.DeleteError)() + + expect(action.type).to.equal("DeleteError") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/FetchPrivilegeSettings.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/FetchPrivilegeSettings.lua new file mode 100644 index 0000000..1cf7383 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/FetchPrivilegeSettings.lua @@ -0,0 +1,6 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) + +return Action("FetchPrivilegeSettings", function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/FetchPrivilegeSettings.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/FetchPrivilegeSettings.spec.lua new file mode 100644 index 0000000..9dcd46a --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/FetchPrivilegeSettings.spec.lua @@ -0,0 +1,25 @@ +return function() + it("should return a table", function() + local action = require(script.Parent.FetchPrivilegeSettings) + + expect(action).to.be.a("table") + end) + + it("should return a table when called as a function", function() + local action = require(script.Parent.FetchPrivilegeSettings)() + + expect(action).to.be.a("table") + end) + + it("should set the name", function() + local action = require(script.Parent.FetchPrivilegeSettings) + + expect(action.name).to.equal("FetchPrivilegeSettings") + end) + + it("should set the type", function() + local action = require(script.Parent.FetchPrivilegeSettings)() + + expect(action.type).to.equal("FetchPrivilegeSettings") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/FetchUserThumbnail.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/FetchUserThumbnail.lua new file mode 100644 index 0000000..946af67 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/FetchUserThumbnail.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) + +return Action("FetchUserThumbnail", function(thumbnailInfo) + thumbnailInfo = thumbnailInfo or {} + return { + rbxuid = thumbnailInfo.rbxuid, + thumbnailType = thumbnailInfo.thumbnailType, + thumbnailSize = thumbnailInfo.thumbnailSize + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/FetchUserThumbnail.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/FetchUserThumbnail.spec.lua new file mode 100644 index 0000000..5c1945d --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/FetchUserThumbnail.spec.lua @@ -0,0 +1,39 @@ +return function() + it("should return a table", function() + local action = require(script.Parent.FetchUserThumbnail) + + expect(action).to.be.a("table") + end) + + it("should return a table when called as a function", function() + local action = require(script.Parent.FetchUserThumbnail)() + + expect(action).to.be.a("table") + end) + + it("should set the name", function() + local action = require(script.Parent.FetchUserThumbnail) + + expect(action.name).to.equal("FetchUserThumbnail") + end) + + it("should set the rbxuid, thumbnailType and thumbnailSize", function() + local action = require(script.Parent.FetchUserThumbnail)( + { + rbxuid = 12345, + thumbnailType = Enum.ThumbnailType.HeadShot, + thumbnailSize = Enum.ThumbnailSize.Size180x180 + }) + + + expect(action.rbxuid).to.equal(12345) + expect(action.thumbnailType).to.equal(Enum.ThumbnailType.HeadShot) + expect(action.thumbnailSize).to.equal(Enum.ThumbnailSize.Size180x180) + end) + + it("should set the type", function() + local action = require(script.Parent.FetchUserThumbnail)() + + expect(action.type).to.equal("FetchUserThumbnail") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/GetCrossPlayEnabledFailed.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/GetCrossPlayEnabledFailed.lua new file mode 100644 index 0000000..969ab97 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/GetCrossPlayEnabledFailed.lua @@ -0,0 +1,6 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) + +return Action("GetCrossPlayEnabledFailed", function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/GetCrossPlayEnabledFailed.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/GetCrossPlayEnabledFailed.spec.lua new file mode 100644 index 0000000..13a9e6a --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/GetCrossPlayEnabledFailed.spec.lua @@ -0,0 +1,25 @@ +return function() + it("should return a table", function() + local action = require(script.Parent.GetCrossPlayEnabledFailed) + + expect(action).to.be.a("table") + end) + + it("should return a table when called as a function", function() + local action = require(script.Parent.GetCrossPlayEnabledFailed)() + + expect(action).to.be.a("table") + end) + + it("should set the name", function() + local action = require(script.Parent.GetCrossPlayEnabledFailed) + + expect(action.name).to.equal("GetCrossPlayEnabledFailed") + end) + + it("should set the type", function() + local action = require(script.Parent.GetCrossPlayEnabledFailed)() + + expect(action.type).to.equal("GetCrossPlayEnabledFailed") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/InsertScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/InsertScreen.lua new file mode 100644 index 0000000..e62091f --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/InsertScreen.lua @@ -0,0 +1,8 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) + +return Action("InsertScreen", function(item) + return { + item = item, + } +end) diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/InsertScreen.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/InsertScreen.spec.lua new file mode 100644 index 0000000..b475c45 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/InsertScreen.spec.lua @@ -0,0 +1,44 @@ +return function() + describe("require", function() + it("should create without errors", function() + require(script.Parent.InsertScreen) + end) + + it("should set the name", function() + local action = require(script.Parent.InsertScreen) + + expect(action.name).to.equal("InsertScreen") + end) + end) + + describe("call", function() + it("should return a table when called as a function", function() + local action = require(script.Parent.InsertScreen) + + action = action({}) + expect(action).to.be.a("table") + end) + + it("should set the type", function() + local action = require(script.Parent.InsertScreen) + + action = action({}) + expect(action.type).to.equal("InsertScreen") + end) + + it("should set the item", function() + local action = require(script.Parent.InsertScreen) + + local item = "foo" + action = action(item) + expect(action.item).to.equal("foo") + end) + + it("should set the type and name to be equal", function() + local action = require(script.Parent.InsertScreen) + + local actionItem = action({}) + expect(actionItem.type).to.equal(action.name) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/PostCrossPlayEnabledFailed.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/PostCrossPlayEnabledFailed.lua new file mode 100644 index 0000000..a7cd4d1 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/PostCrossPlayEnabledFailed.lua @@ -0,0 +1,6 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) + +return Action("PostCrossPlayEnabledFailed", function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/PostCrossPlayEnabledFailed.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/PostCrossPlayEnabledFailed.spec.lua new file mode 100644 index 0000000..77789e2 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/PostCrossPlayEnabledFailed.spec.lua @@ -0,0 +1,25 @@ +return function() + it("should return a table", function() + local action = require(script.Parent.PostCrossPlayEnabledFailed) + + expect(action).to.be.a("table") + end) + + it("should return a table when called as a function", function() + local action = require(script.Parent.PostCrossPlayEnabledFailed)() + + expect(action).to.be.a("table") + end) + + it("should set the name", function() + local action = require(script.Parent.PostCrossPlayEnabledFailed) + + expect(action.name).to.equal("PostCrossPlayEnabledFailed") + end) + + it("should set the type", function() + local action = require(script.Parent.PostCrossPlayEnabledFailed)() + + expect(action.type).to.equal("PostCrossPlayEnabledFailed") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/RemoveScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/RemoveScreen.lua new file mode 100644 index 0000000..77b1963 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/RemoveScreen.lua @@ -0,0 +1,8 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) + +return Action("RemoveScreen", function(item) + return { + item = item, + } +end) diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/RemoveScreen.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/RemoveScreen.spec.lua new file mode 100644 index 0000000..e14509b --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/RemoveScreen.spec.lua @@ -0,0 +1,44 @@ +return function() + describe("require", function() + it("should create without errors", function() + require(script.Parent.RemoveScreen) + end) + + it("should set the name", function() + local action = require(script.Parent.RemoveScreen) + + expect(action.name).to.equal("RemoveScreen") + end) + end) + + describe("call", function() + it("should return a table when called as a function", function() + local action = require(script.Parent.RemoveScreen) + + action = action({}) + expect(action).to.be.a("table") + end) + + it("should set the type", function() + local action = require(script.Parent.RemoveScreen) + + action = action({}) + expect(action.type).to.equal("RemoveScreen") + end) + + it("should set the item", function() + local action = require(script.Parent.RemoveScreen) + + local item = "foo" + action = action(item) + expect(action.item).to.equal("foo") + end) + + it("should set the type and name to be equal", function() + local action = require(script.Parent.RemoveScreen) + + local actionItem = action({}) + expect(actionItem.type).to.equal(action.name) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/RequestCrossPlayEnabled.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/RequestCrossPlayEnabled.lua new file mode 100644 index 0000000..37687dc --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/RequestCrossPlayEnabled.lua @@ -0,0 +1,6 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) + +return Action("RequestCrossPlayEnabled", function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/RequestCrossPlayEnabled.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/RequestCrossPlayEnabled.spec.lua new file mode 100644 index 0000000..2a545e8 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/RequestCrossPlayEnabled.spec.lua @@ -0,0 +1,25 @@ +return function() + it("should return a table", function() + local action = require(script.Parent.RequestCrossPlayEnabled) + + expect(action).to.be.a("table") + end) + + it("should return a table when called as a function", function() + local action = require(script.Parent.RequestCrossPlayEnabled)() + + expect(action).to.be.a("table") + end) + + it("should set the name", function() + local action = require(script.Parent.RequestCrossPlayEnabled) + + expect(action.name).to.equal("RequestCrossPlayEnabled") + end) + + it("should set the type", function() + local action = require(script.Parent.RequestCrossPlayEnabled)() + + expect(action.type).to.equal("RequestCrossPlayEnabled") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/ResetUserThumbnails.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/ResetUserThumbnails.lua new file mode 100644 index 0000000..7376685 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/ResetUserThumbnails.lua @@ -0,0 +1,6 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) + +return Action("ResetUserThumbnails", function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/ResetUserThumbnails.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/ResetUserThumbnails.spec.lua new file mode 100644 index 0000000..312077b --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/ResetUserThumbnails.spec.lua @@ -0,0 +1,25 @@ +return function() + it("should return a table", function() + local action = require(script.Parent.ResetUserThumbnails) + + expect(action).to.be.a("table") + end) + + it("should return a table when called as a function", function() + local action = require(script.Parent.ResetUserThumbnails)() + + expect(action).to.be.a("table") + end) + + it("should set the name", function() + local action = require(script.Parent.ResetUserThumbnails) + + expect(action.name).to.equal("ResetUserThumbnails") + end) + + it("should set the type", function() + local action = require(script.Parent.ResetUserThumbnails)() + + expect(action.type).to.equal("ResetUserThumbnails") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetCrossPlayEnabled.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetCrossPlayEnabled.lua new file mode 100644 index 0000000..52b96ec --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetCrossPlayEnabled.lua @@ -0,0 +1,9 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) + +return Action("SetCrossPlayEnabled", function(enabled, timestamp) + return { + enabled = enabled, + timestamp = timestamp + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetCrossPlayEnabled.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetCrossPlayEnabled.spec.lua new file mode 100644 index 0000000..01beb93 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetCrossPlayEnabled.spec.lua @@ -0,0 +1,32 @@ +return function() + it("should return a table", function() + local action = require(script.Parent.SetCrossPlayEnabled) + + expect(action).to.be.a("table") + end) + + it("should return a table when called as a function", function() + local action = require(script.Parent.SetCrossPlayEnabled)() + + expect(action).to.be.a("table") + end) + + it("should set the name", function() + local action = require(script.Parent.SetCrossPlayEnabled) + + expect(action.name).to.equal("SetCrossPlayEnabled") + end) + + it("should set the enabled value and timestamp", function() + local action = require(script.Parent.SetCrossPlayEnabled)(true, 10) + + expect(action.enabled).to.equal(true) + expect(action.timestamp).to.equal(10) + end) + + it("should set the type", function() + local action = require(script.Parent.SetCrossPlayEnabled)() + + expect(action.type).to.equal("SetCrossPlayEnabled") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetFriendsData.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetFriendsData.lua new file mode 100644 index 0000000..ac2d023 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetFriendsData.lua @@ -0,0 +1,22 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) +--[[ + // friendsData is table + // Table keys: + // [index number] - table + // xuid - number + // robloxName - string + // placeId - number + // robloxStatus - string + // robloxuid - number + // lastLocation - string + // gamertag - string + // xboxStatus - string + // friendsSource - string +]] + +return Action("SetFriendsData", function(friendsData) + return { + data = friendsData + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetFriendsData.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetFriendsData.spec.lua new file mode 100644 index 0000000..b4269cf --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetFriendsData.spec.lua @@ -0,0 +1,39 @@ +return function() + it("should return a table", function() + local action = require(script.Parent.SetFriendsData) + + expect(action).to.be.a("table") + end) + + it("should return a table without data when passed nil", function() + local action = require(script.Parent.SetFriendsData)() + + expect(action).to.be.a("table") + expect(action.data).to.equal(nil) + end) + + it("should set the name", function() + local action = require(script.Parent.SetFriendsData) + + expect(action.name).to.equal("SetFriendsData") + end) + + it("should set the elements at the first depth", function() + local action = require(script.Parent.SetFriendsData)( { {}, {} } ) + + expect(action.data[2]).to.be.a("table") + end) + + it("should set the elements at the second depth", function() + local action = require(script.Parent.SetFriendsData)( { { a="A", b="B" } } ) + + expect(action.data[1]).to.be.a("table") + expect(action.data[1].a).to.equal("A") + end) + + it("should set the type", function() + local action = require(script.Parent.SetFriendsData)() + + expect(action.type).to.equal("SetFriendsData") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetPrivilegeSettings.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetPrivilegeSettings.lua new file mode 100644 index 0000000..aff4bc5 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetPrivilegeSettings.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) + +return Action("SetPrivilegeSettings", function(privilegeSettings) + privilegeSettings = privilegeSettings or {} + return + { + Multiplayer = privilegeSettings.Multiplayer, + SharedContent = privilegeSettings.SharedContent, + timestamp = privilegeSettings.timestamp + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetPrivilegeSettings.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetPrivilegeSettings.spec.lua new file mode 100644 index 0000000..268cf49 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetPrivilegeSettings.spec.lua @@ -0,0 +1,33 @@ +return function() + it("should return a table", function() + local action = require(script.Parent.SetPrivilegeSettings) + + expect(action).to.be.a("table") + end) + + it("should return a table when called as a function", function() + local action = require(script.Parent.SetPrivilegeSettings)() + + expect(action).to.be.a("table") + end) + + it("should set the name", function() + local action = require(script.Parent.SetPrivilegeSettings) + + expect(action.name).to.equal("SetPrivilegeSettings") + end) + + it("should set the privilege settings and timestamp", function() + local action = require(script.Parent.SetPrivilegeSettings)({Multiplayer = {}, SharedContent = {}, timestamp = 10}) + + expect(action.Multiplayer).to.be.a("table") + expect(action.SharedContent).to.be.a("table") + expect(action.timestamp).to.equal(10) + end) + + it("should set the type", function() + local action = require(script.Parent.SetPrivilegeSettings)() + + expect(action.type).to.equal("SetPrivilegeSettings") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetRenderedFriendsData.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetRenderedFriendsData.lua new file mode 100644 index 0000000..a42430f --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetRenderedFriendsData.lua @@ -0,0 +1,22 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) +--[[ + // friendsData is table + // Table keys: + // [index number] - table + // xuid - number + // robloxName - string + // placeId - number + // robloxStatus - string + // robloxuid - number + // lastLocation - string + // gamertag - string + // xboxStatus - string + // friendsSource - string +]] + +return Action("SetRenderedFriendsData", function(friendsData) + return { + data = friendsData + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetRenderedFriendsData.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetRenderedFriendsData.spec.lua new file mode 100644 index 0000000..344fe01 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetRenderedFriendsData.spec.lua @@ -0,0 +1,39 @@ +return function() + it("should return a table", function() + local action = require(script.Parent.SetRenderedFriendsData) + + expect(action).to.be.a("table") + end) + + it("should return a table without data when passed nil", function() + local action = require(script.Parent.SetRenderedFriendsData)() + + expect(action).to.be.a("table") + expect(action.data).to.equal(nil) + end) + + it("should set the name", function() + local action = require(script.Parent.SetRenderedFriendsData) + + expect(action.name).to.equal("SetRenderedFriendsData") + end) + + it("should set the elements at the first depth", function() + local action = require(script.Parent.SetRenderedFriendsData)( { {}, {} } ) + + expect(action.data[2]).to.be.a("table") + end) + + it("should set the elements at the second depth", function() + local action = require(script.Parent.SetRenderedFriendsData)( { { a="A", b="B" } } ) + + expect(action.data[1]).to.be.a("table") + expect(action.data[1].a).to.equal("A") + end) + + it("should set the type", function() + local action = require(script.Parent.SetRenderedFriendsData)() + + expect(action.type).to.equal("SetRenderedFriendsData") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetRobloxUser.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetRobloxUser.lua new file mode 100644 index 0000000..2757a01 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetRobloxUser.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) + +return Action("SetRobloxUser", function(userInfo) + userInfo = userInfo or {} + return { + robloxName = userInfo.robloxName, + rbxuid = userInfo.rbxuid, + under13 = userInfo.under13 + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetRobloxUser.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetRobloxUser.spec.lua new file mode 100644 index 0000000..ec1361c --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetRobloxUser.spec.lua @@ -0,0 +1,44 @@ +return function() + it("should return a table", function() + local action = require(script.Parent.SetRobloxUser) + + expect(action).to.be.a("table") + end) + + it("should return a table when called as a function", function() + local action = require(script.Parent.SetRobloxUser)({robloxName="TestRobloxName", rbxuid=12345, under13 = true}) + + expect(action).to.be.a("table") + end) + + it("should set the name", function() + local action = require(script.Parent.SetRobloxUser) + + expect(action.name).to.equal("SetRobloxUser") + end) + + it("should set the robloxName, rbxuid, and under13 values", function() + local action = require(script.Parent.SetRobloxUser)({robloxName="TestRobloxName", rbxuid=12345, under13 = true}) + + expect(action.robloxName).to.be.a("string") + expect(action.robloxName).to.equal("TestRobloxName") + expect(action.rbxuid).to.be.a("number") + expect(action.rbxuid).to.equal(12345) + expect(action.under13).to.be.a("boolean") + expect(action.under13).to.equal(true) + end) + + it("should set the robloxName, rbxuid, and under13 values to nil when passed an empty table", function() + local action = require(script.Parent.SetRobloxUser)({}) + + expect(action.robloxName).to.equal(nil) + expect(action.rbxuid).to.equal(nil) + expect(action.under13).to.equal(nil) + end) + + it("should set the type", function() + local action = require(script.Parent.SetRobloxUser)({robloxName="TestRobloxName", rbxuid=12345, under13 = true}) + + expect(action.type).to.equal("SetRobloxUser") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetUserThumbnail.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetUserThumbnail.lua new file mode 100644 index 0000000..57a56df --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetUserThumbnail.lua @@ -0,0 +1,15 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) + +return Action("SetUserThumbnail", function(thumbnailInfo) + thumbnailInfo = thumbnailInfo or {} + return { + success = thumbnailInfo.success, + rbxuid = thumbnailInfo.rbxuid, + imageUrl = thumbnailInfo.imageUrl, + thumbnailType = thumbnailInfo.thumbnailType, + thumbnailSize = thumbnailInfo.thumbnailSize, + isFinal = thumbnailInfo.isFinal, + timestamp = thumbnailInfo.timestamp + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetXboxUser.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetXboxUser.lua new file mode 100644 index 0000000..6fe3f44 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetXboxUser.lua @@ -0,0 +1,10 @@ +local CoreGui = game:GetService("CoreGui") +local Action = require(CoreGui.RobloxGui.Modules.Common.Action) + +return Action("SetXboxUser", function(userInfo) + userInfo = userInfo or {} + return { + gamertag = userInfo.gamertag, + xuid = userInfo.xuid + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetXboxUser.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetXboxUser.spec.lua new file mode 100644 index 0000000..1788a4b --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetXboxUser.spec.lua @@ -0,0 +1,41 @@ +return function() + it("should return a table", function() + local action = require(script.Parent.SetXboxUser) + + expect(action).to.be.a("table") + end) + + it("should return a table when called as a function", function() + local action = require(script.Parent.SetXboxUser)({gamertag="TestGamerTag", xuid=12345}) + + expect(action).to.be.a("table") + end) + + it("should set the name", function() + local action = require(script.Parent.SetXboxUser) + + expect(action.name).to.equal("SetXboxUser") + end) + + it("should set the gamertag and xuid values", function() + local action = require(script.Parent.SetXboxUser)({gamertag="TestGamerTag", xuid=12345}) + + expect(action.gamertag).to.be.a("string") + expect(action.gamertag).to.equal("TestGamerTag") + expect(action.xuid).to.be.a("number") + expect(action.xuid).to.equal(12345) + end) + + it("should set gamertag and xuid to nil if passed an empty table", function() + local action = require(script.Parent.SetXboxUser)({}) + + expect(action.gamertag).to.equal(nil) + expect(action.xuid).to.equal(nil) + end) + + it("should set the type", function() + local action = require(script.Parent.SetXboxUser)({gamertag="TestGamerTag", xuid=12345}) + + expect(action.type).to.equal("SetXboxUser") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetuserThumbnail.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetuserThumbnail.spec.lua new file mode 100644 index 0000000..fd3f504 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Actions/SetuserThumbnail.spec.lua @@ -0,0 +1,65 @@ +return function() + it("should return a table", function() + local action = require(script.Parent.SetUserThumbnail) + + expect(action).to.be.a("table") + end) + + it("should return a table when called as a function", function() + local action = require(script.Parent.SetUserThumbnail)( + { + success = true, + rbxuid = 12345, + imageUrl = "x", + thumbnailType = Enum.ThumbnailType.HeadShot, + thumbnailSize = Enum.ThumbnailSize.Size180x180, + isFinal = true, + timestamp = 10 + }) + + expect(action).to.be.a("table") + end) + + it("should set the name", function() + local action = require(script.Parent.SetUserThumbnail) + + expect(action.name).to.equal("SetUserThumbnail") + end) + + it("should set the success, rbxuid, imageUrl, thumbnailType, thumbnailSize and isFinal values", function() + local action = require(script.Parent.SetUserThumbnail)( + { + rbxuid = 12345, + thumbnailType = Enum.ThumbnailType.HeadShot, + thumbnailSize = Enum.ThumbnailSize.Size180x180, + success = true, + imageUrl = "x", + isFinal = true, + timestamp = 10 + }) + + expect(action.success).to.equal(true) + expect(action.rbxuid).to.equal(12345) + expect(action.imageUrl).to.equal("x") + expect(action.thumbnailType).to.equal(Enum.ThumbnailType.HeadShot) + expect(action.thumbnailSize).to.equal(Enum.ThumbnailSize.Size180x180) + expect(action.isFinal).to.equal(true) + expect(action.timestamp).to.equal(10) + end) + + it("should set the success, rbxuid, imageUrl, thumbnailType, thumbnailSize and isFinal to nil if passed an empty table", function() + local action = require(script.Parent.SetUserThumbnail)({}) + + expect(action.success).never.to.be.ok() + expect(action.rbxuid).never.to.be.ok() + expect(action.imageUrl).never.to.be.ok() + expect(action.thumbnailType).never.to.be.ok() + expect(action.thumbnailSize).never.to.be.ok() + expect(action.isFinal).never.to.be.ok() + end) + + it("should set the type", function() + local action = require(script.Parent.SetUserThumbnail)({}) + expect(action.type).to.equal("SetUserThumbnail") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Alerts.lua b/Client2018/content/internal/AppShell/Modules/Shell/Alerts.lua new file mode 100644 index 0000000..759eab8 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Alerts.lua @@ -0,0 +1,58 @@ +--[[ + // Alerts.lua + + // Global alert codes, each alert has an unique Id +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) + +local Alerts = +{ + Default = { Title = Strings:LocalizedString("AlertOccurredTitle"), Msg = Strings:LocalizedString("DefaultAlertPhrase"), Id = 0 }; + + UnlockedUGC = { Title = Strings:LocalizedString("UnlockedUGCTitle"), Msg = Strings:LocalizedString("UnlockedUGCPhrase"), Id = 101}; + + LostConnection = + { + Controller = { Title = Strings:LocalizedString("ControllerLostConnectionTitle"), Msg = Strings:LocalizedString("ControllerLostConnectionPhrase"), Id = 201 }; + ActiveUser = { Title = Strings:LocalizedString("ActiveUserLostConnectionTitle"), Msg = Strings:LocalizedString("ActiveUserLostConnectionPhrase"), Id = 202 }; + }; + + PlayMyPlaceMoreGames = { Title = Strings:LocalizedString('PlayMyPlaceMoreGamesTitle'), Msg = Strings:LocalizedString('PlayMyPlaceMoreGamesPhrase'), Id = 301 }; + + CrossPlatformPlayWelcome = { Title = Strings:LocalizedString("CPPWelcomeTitle"), Msg = Strings:LocalizedString("CPPWelcomePhrase"), Id = 401}; + + Reauthentication = + { + -- index mapped to int error code from c++, you must index into this with a string + [0] = { Title = Strings:LocalizedString("AlertOccurredTitle"), Msg = Strings:LocalizedString("ReauthUnknownPhrase"), Id = 1001 }; + [1] = { Title = Strings:LocalizedString("ReauthSignedOutTitle"), Msg = Strings:LocalizedString("ReauthSignedOutPhrase"), Id = 1002 }; + [2] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1003 }; + [3] = { Title = Strings:LocalizedString("ReauthSignedOutTitle"), Msg = Strings:LocalizedString("ReauthInvalidSessionPhrase"), Id = 1004 }; + [4] = { Title = Strings:LocalizedString("ReauthUnlinkTitle"), Msg = Strings:LocalizedString("ReauthUnlinkPhrase"), Id = 1005 }; + [5] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1006 }; + [6] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1007 }; + [7] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1008 }; + }; + + SignOut = + { + -- index mapped to int error code from c++, you must index into this with a string + [0] = { Title = Strings:LocalizedString("AlertOccurredTitle"), Msg = Strings:LocalizedString("ReauthUnknownPhrase"), Id = 1001 }; + [1] = { Title = Strings:LocalizedString("ReauthSignedOutTitle"), Msg = Strings:LocalizedString("ReauthSignedOutPhrase"), Id = 1002 }; + [2] = { Title = Strings:LocalizedString("ReauthSignedOutTitle"), Msg = Strings:LocalizedString("ReauthInvalidSessionPhrase"), Id = 1003 }; + [3] = { Title = Strings:LocalizedString("ReauthUnlinkTitle"), Msg = Strings:LocalizedString("ReauthUnlinkPhrase"), Id = 1004 }; + [4] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1005 }; + [5] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1006 }; + [6] = nil; + }; + + + PlatformLink = { Title = Strings:LocalizedString('PlatformLinkInfoTitle'), Msg = Strings:LocalizedString('PlatformLinkInfoMessage'), Id = 1201}; +} + +return Alerts diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Analytics.lua b/Client2018/content/internal/AppShell/Modules/Shell/Analytics.lua new file mode 100644 index 0000000..357d840 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Analytics.lua @@ -0,0 +1,173 @@ +--[[ + // Analytics.lua + + // Fetches analytics data for console platform +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild('Utility')) + +--[[ Services ]]-- +local AnalyticsService = nil +pcall(function() AnalyticsService = game:GetService('AnalyticsService') end) +local UserInputService = game:GetService('UserInputService') + +local Analytics = {} + +--[[ Helper Functions ]]-- +local function setRBXEvent(eventName, additionalArgs) + local target, eventContext = nil, nil + local success, result = pcall(function() + if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then + target = "console" + eventContext = "XboxOne" + eventName = eventName or "" + additionalArgs = additionalArgs or {} + AnalyticsService:SetRBXEvent(target, eventContext, eventName, additionalArgs) + end + end) + + if not success then + Utility.DebugLog("setRBXEvent() failed because", result, "Input: target:", target, " eventContext:", eventContext, " eventName:", eventName) + end + + return success +end + +local function setRBXEventStream(eventName, additionalArgs) + local target, eventContext = nil, nil + local success, result = pcall(function() + if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then + target = "console" + eventContext = "XboxOne" + eventName = eventName or "" + additionalArgs = additionalArgs or {} + AnalyticsService:SetRBXEventStream(target, eventContext, eventName, additionalArgs) + end + end) + + if not success then + Utility.DebugLog("setRBXEventStream() failed because", result, "Input: target:", target, " eventContext:", eventContext, " eventName:", eventName) + end + + return success +end + +local function releaseRBXEventStream(eventName) + local target = nil + local success, result = pcall(function() + if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then + target = "console" + AnalyticsService:ReleaseRBXEventStream(target) + end + end) + + if not success then + Utility.DebugLog("releaseRBXEventStream() failed because", result, "Input: target:", target) + end + + return success +end + +local function updateHeartbeatObject(additionalArgs) + local success, result = pcall(function() + AnalyticsService:UpdateHeartbeatObject(additionalArgs) + end) + + if not success then + Utility.DebugLog("UpdateHeartbeatObject() failed because ", result, "Input: args:", additionalArgs) + end + + return success +end + +local function reportCounter(counterName, amount) + local success, result = pcall(function() + if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then + counterName = counterName or "" + counterName = "Xbox-"..tostring(counterName) + amount = amount or 1 + AnalyticsService:ReportCounter(counterName, amount) + end + end) + + if not success then + Utility.DebugLog("reportCounter() failed because", result, "Input: counterName:", counterName, "amount:", amount) + end + + return success +end + +--[[ Public API ]]-- +function Analytics.SetRBXEvent(eventName, additionalArgs) + setRBXEvent(eventName, additionalArgs) +end + +-- Non-real time +function Analytics.SetRBXEventStream(eventName, additionalArgs) + setRBXEventStream(eventName, additionalArgs) +end + +function Analytics.UpdateHeartbeatObject(additionalArgs) + updateHeartbeatObject(additionalArgs) +end + +-- Real time +function Analytics.ReportCounter(counterName, amount) + reportCounter(counterName, amount) +end + + +local WidgetNames = { + --Widget Name + ["WidgetId"] = "WidgetName"; + ["AppHubId"] = "AppHub"; + ["AvatarEditorScreenId"] = "AvatarEditorScreen"; + ["AvatarPaneId"] = "AvatarPane"; + ["AvatarTileId"] = "AvatarTile"; + ["BadgeOverlayId"] = "BadgeOverlay"; + ["BaseCarouselScreenId"] = "BaseCarouselScreen"; + ["BaseOverlayId"] = "BaseOverlay"; + ["BaseTileId"] = "BaseTile"; + ["ConfirmPromptId"] = "ConfirmPrompt"; + ["DisableCrossplayOverlayId"] = "DisableCrossplayOverlay"; + ["EnableCrossplayOverlayId"] = "EnableCrossplayOverlay"; + ["EngagementScreenId"] = "EngagementScreen"; + ["ErrorOverlayId"] = "ErrorOverlay"; + ["GameDetailId"] = "GameDetail"; + ["GameGenreScreenId"] = "GameGenreScreen"; + ["GamesPaneId"] = "GamesPane"; + ["GameSearchScreenId"] = "GameSearchScreen"; + ["HomePaneId"] = "HomePane"; + ["ImageOverlayId"] = "ImageOverlay"; + ["LinkAccountScreenId"] = "LinkAccountScreen"; --AKA "Sign In Screen" + ["NoActionOverlayId"] = "NoActionOverlay"; + ["OutfitTileId"] = "OutfitTile"; + ["OverscanScreenId"] = "OverscanScreen"; + ["PurchasePackagePromptId"] = "PurchasePackagePrompt"; + ["ReportOverlayId"] = "ReportOverlay"; + ["RobuxBalanceOverlayId"] = "RobuxBalanceOverlay"; + ["SetAccountCredentialsScreenId"] = "SetAccountCredentialsScreen"; --AKA "Sign Up Screen" and "Microwave Screen" (User sees it as two different screens) + ["SettingsScreenId"] = "SettingsScreen"; + ["SideBarId"] = "SideBar"; + ["SignInScreenId"] = "SignInScreen"; --AKA "Choice Screen" + ["SocialPaneId"] = "SocialPane"; + ["StorePaneId"] = "StorePane"; + ["TabDockId"] = "TabDock"; + ["UnlinkAccountOverlayId"] = "UnlinkAccountOverlay"; + ["UnlinkAccountScreenId"] = "UnlinkAccountScreen"; +} + +function Analytics.WidgetNames(stringKey) + local result = WidgetNames and WidgetNames[stringKey] + if not result then + Utility.DebugLog("Analytics.WidgetNames: Could not find widget name for:" , stringKey) + result = stringKey + end + return result +end + +return Analytics diff --git a/Client2018/content/internal/AppShell/Modules/Shell/AppContainer.lua b/Client2018/content/internal/AppShell/Modules/Shell/AppContainer.lua new file mode 100644 index 0000000..ca2ee8e --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/AppContainer.lua @@ -0,0 +1,66 @@ +-- Creates UI for Action/Title safe container as per Microsofts reccomendations + +local CoreGui = game:GetService('CoreGui') +local RobloxGui = CoreGui.RobloxGui +local ShellModules = script.Parent + +local Utility = require(ShellModules.Utility) + +local ACTION_SAFE_INSET = UDim2.new((128 / 1920) * 0.5, 0, (64 / 1080) * 0.5, 0) +local TITLE_SAFE_INSET = UDim2.new((72 / 1792) * 0.5, 0, (16 / 1080) * 0.5, 0) + +local AppContainer = {} + +AppContainer.Root = Utility.Create'Frame' +{ + Name = "AppContainerRoot"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + BorderSizePixel = 0; + BackgroundColor3 = Color3.new(0, 0, 0); + ClipsDescendants = true; + Parent = RobloxGui; +} + +AppContainer.AspectRatioProtector = Utility.Create'Frame' +{ + Name = 'AspectRatioProtector'; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0,0,0,0); + BackgroundTransparency = 1; + Parent = AppContainer.Root; +} + +AppContainer.ActionSafeContainer = Utility.Create'Frame' +{ + Name = "ActionSafeContainer"; + Size = UDim2.new(1, 0, 1, 0) - (ACTION_SAFE_INSET + ACTION_SAFE_INSET); + Position = UDim2.new(0,0,0,0) + ACTION_SAFE_INSET; + BackgroundTransparency = 1; + Parent = AppContainer.AspectRatioProtector; +} + +AppContainer.TitleSafeContainer = Utility.Create'Frame' +{ + Name = "TitleSafeContainer"; + Size = UDim2.new(1, 0, 1, 0) - (TITLE_SAFE_INSET + TITLE_SAFE_INSET); + Position = UDim2.new(0, 0, 0, 0) + TITLE_SAFE_INSET; + BackgroundTransparency = 1; + Parent = AppContainer.ActionSafeContainer; +} + +local function OnAbsoluteSizeChanged() + local newSize = Utility.CalculateFit(AppContainer.Root, Vector2.new(16,9)) + if newSize ~= AppContainer.AspectRatioProtector.Size then + AppContainer.AspectRatioProtector.Size = newSize + AppContainer.AspectRatioProtector.AnchorPoint = Vector2.new(0.5, 0.5) + AppContainer.AspectRatioProtector.Position = UDim2.new(0.5, 0, 0.5, 0) + end +end + +AppContainer.Root:GetPropertyChangedSignal('AbsoluteSize'):connect(function() + OnAbsoluteSizeChanged() +end) +OnAbsoluteSizeChanged() + +return AppContainer diff --git a/Client2018/content/internal/AppShell/Modules/Shell/AppHome.lua b/Client2018/content/internal/AppShell/Modules/Shell/AppHome.lua new file mode 100644 index 0000000..edca250 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/AppHome.lua @@ -0,0 +1,198 @@ +-- Written by Kip Turner, Copyright Roblox 2015 + +-- App's Main +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:FindFirstChild("RobloxGui") +local Modules = RobloxGui:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +-- TODO: Will use for re-auth when finished +local UserInputService = game:GetService('UserInputService') +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) +local ThirdPartyUserService = nil +pcall(function() ThirdPartyUserService = game:GetService('ThirdPartyUserService') end) +local Players = game:GetService("Players") + +-- Start up background scene before anything else +require(ShellModules.BackgroundSceneManager) + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local AppHubModule = require(ShellModules:FindFirstChild('AppHub')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local Errors = require(ShellModules:FindFirstChild('Errors')) +local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local GameGenreScreen = require(ShellModules:FindFirstChild('GameGenreScreen')) +local EngagementScreenModule = require(ShellModules:FindFirstChild('EngagementScreen')) +local BadgeScreenModule = require(ShellModules:FindFirstChild('BadgeScreen')) +local AccountScreen = require(ShellModules:FindFirstChild('AccountScreen')) +local UserData = require(ShellModules:FindFirstChild('UserData')) +local ControllerStateManager = require(ShellModules:FindFirstChild('ControllerStateManager')) +local Alerts = require(ShellModules:FindFirstChild('Alerts')) + +local SiteInfoWidget = require(ShellModules:FindFirstChild('SiteInfoWidget')) + +local GameDetailModule = require(ShellModules:FindFirstChild('GameDetailScreen')) + +-- Initialize AppState and in turn initialize the Store +local AppState = require(ShellModules.AppState) + +local AppContainer = require(ShellModules.AppContainer) +local TitleSafeContainer = AppContainer.TitleSafeContainer + +local EngagementScreen = EngagementScreenModule() +EngagementScreen:SetParent(TitleSafeContainer) + +-- Site Info View +SiteInfoWidget.new() + +-- Initialzie Account Age View +require(ShellModules.Components.AccountAgeStatus).new(AppState.store, TitleSafeContainer) + +local function returnToEngagementScreen() + if ScreenManager:ContainsScreen(EngagementScreen) then + while ScreenManager:GetTopScreen() ~= EngagementScreen do + ScreenManager:CloseCurrent() + end + else + while ScreenManager:GetTopScreen() do + ScreenManager:CloseCurrent() + end + ScreenManager:OpenScreen(EngagementScreen) + end +end + +local AppHub = nil +local function onAuthenticationSuccess(isNewLinkedAccount) + -- Set UserData + UserData:Initialize() + + local SetRobloxUser = require(ShellModules.Actions.SetRobloxUser) + AppState.store:dispatch(SetRobloxUser( { + robloxName = Players.LocalPlayer.Name, + rbxuid = Players.LocalPlayer.UserId, + under13 = Players.LocalPlayer:GetUnder13(), + } )) + + -- Unwind Screens if needed - this will be needed once we put in account linking + returnToEngagementScreen() + + AppHub = AppHubModule() + AppHub:SetParent(TitleSafeContainer) + + EventHub:addEventListener(EventHub.Notifications["OpenGameDetail"], "gameDetail", + function(placeId) + local gameDetail = GameDetailModule(placeId) + gameDetail:SetParent(TitleSafeContainer); + ScreenManager:OpenScreen(gameDetail); + end); + EventHub:addEventListener(EventHub.Notifications["OpenGameGenre"], "gameGenre", + function(sortName, gameCollection) + local gameGenre = GameGenreScreen(sortName, gameCollection) + gameGenre:SetParent(TitleSafeContainer); + ScreenManager:OpenScreen(gameGenre); + end); + EventHub:addEventListener(EventHub.Notifications["OpenBadgeScreen"], "gameBadges", + function(badgeData, previousScreenName) + local badgeScreen = BadgeScreenModule(badgeData, previousScreenName) + badgeScreen:SetParent(TitleSafeContainer); + ScreenManager:OpenScreen(badgeScreen); + end) + EventHub:addEventListener(EventHub.Notifications["OpenSettingsScreen"], "settingsScreen", + function(settingsScreen) + settingsScreen:SetParent(TitleSafeContainer); + ScreenManager:OpenScreen(settingsScreen); + end) + + EventHub:addEventListener(EventHub.Notifications["OpenAvatarEditorScreen"], "avatarEditorScreen", + function(screen) + screen:SetParent(AppContainer.Root); + ScreenManager:OpenScreen(screen); + end) + + EventHub:addEventListener(EventHub.Notifications["OpenAccountSettingsScreen"], "accountSettingsScreen", + function(errorCode) + local accountScreen = AccountScreen(errorCode) + accountScreen:SetParent(TitleSafeContainer); + ScreenManager:OpenScreen(accountScreen); + end) + + Utility.DebugLog("User and Event initialization finished. Opening AppHub") + ScreenManager:OpenScreen(AppHub); + + if PlatformService and not PlatformService:SeenCPPWelcomeMsg() then + ScreenManager:OpenScreen(ErrorOverlay(Alerts.CrossPlatformPlayWelcome), false) + end + + -- Comment out for now since this has never been called in current flow + -- show info popup to users on newly linked accounts +-- if isNewLinkedAccount == true then +-- ScreenManager:OpenScreen(ErrorOverlay(Alerts.PlatformLink), false) +-- end +end + +local function onReAuthentication(reauthenticationReason) + Utility.DebugLog("Beging Reauth, cleaning things up") + + local SetRobloxUser = require(ShellModules.Actions.SetRobloxUser) + AppState.store:dispatch(SetRobloxUser()) + + local SetXboxUser = require(ShellModules.Actions.SetXboxUser) + AppState.store:dispatch(SetXboxUser()) + + -- unwind ScreenManager + returnToEngagementScreen() + + UserData:Reset() + AppHub = nil + EventHub:removeEventListener(EventHub.Notifications["OpenGameDetail"], "gameDetail") + EventHub:removeEventListener(EventHub.Notifications["OpenGameGenre"], "gameGenre") + EventHub:removeEventListener(EventHub.Notifications["OpenBadgeScreen"], "gameBadges") + EventHub:removeEventListener(EventHub.Notifications["OpenSettingsScreen"], "settingsScreen") + EventHub:removeEventListener(EventHub.Notifications["OpenAvatarEditorScreen"], "avatarEditorScreen") + EventHub:removeEventListener(EventHub.Notifications["OpenAccountSettingsScreen"], "accountSettingsScreen") + + Utility.DebugLog("Reauth complete. Return to engagement screen.") + + -- show reason overlay + local alert = Alerts.SignOut[reauthenticationReason] or Alerts.Default + + if reauthenticationReason == 6 then + alert = nil + end + + if alert then + ScreenManager:OpenScreen(ErrorOverlay(alert), false) + end +end + +local function onGameJoin(joinResult, placeId) + -- 0 is success, anything else is an error + local joinSuccess = joinResult == 0 + if not joinSuccess then + local err = Errors.GameJoin[joinResult] or Errors.GameJoin.Default + ScreenManager:OpenScreen(ErrorOverlay(err), false) + end + EventHub:dispatchEvent(EventHub.Notifications["GameJoin"], joinSuccess, placeId) +end + +if PlatformService then + PlatformService.GameJoined:connect(onGameJoin) +end + +if ThirdPartyUserService then + ThirdPartyUserService.ActiveUserSignedOut:connect(onReAuthentication) +end + +ControllerStateManager:Initialize() + +EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "authUserSuccess", function(isNewLinkedAccount) + Utility.DebugLog("User authenticated, initializing app shell") + onAuthenticationSuccess(isNewLinkedAccount) + end); +ScreenManager:OpenScreen(EngagementScreen) + +UserInputService.MouseIconEnabled = false + +return {} diff --git a/Client2018/content/internal/AppShell/Modules/Shell/AppHub.lua b/Client2018/content/internal/AppShell/Modules/Shell/AppHub.lua new file mode 100644 index 0000000..7600f6f --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/AppHub.lua @@ -0,0 +1,444 @@ +-- Written by Kip Turner, Copyright Roblox 2015 +local CoreGui = game:GetService("CoreGui") +local ContextActionService = game:GetService("ContextActionService") +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) +local UserInputService = game:GetService("UserInputService") +local GuiService = game:GetService('GuiService') + +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local AppTabDockModule = require(ShellModules:FindFirstChild('TabDock')) +local AppTabDockItemModule = require(ShellModules:FindFirstChild('TabDockItem')) +local HomePaneModule = require(ShellModules:FindFirstChild('HomePane')) +local GamesPaneModule = require(ShellModules:FindFirstChild('GamesPane')) +local AvatarPaneModule = require(ShellModules:FindFirstChild('AvatarPane')) + +local Errors = require(ShellModules:FindFirstChild('Errors')) +local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local SocialPaneModule; +local XboxRecommendedPeople = settings():GetFFlag("XboxRecommendedPeople2") +if XboxRecommendedPeople then + SocialPaneModule = require(ShellModules.Components.Social:FindFirstChild('SocialPane')) +else + SocialPaneModule = require(ShellModules:FindFirstChild('SocialPane')) +end + +local StorePaneModule = require(ShellModules:FindFirstChild('StorePane')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local SettingsScreen = require(ShellModules:FindFirstChild('SettingsScreen')) +local AvatarEditorScreen = require(ShellModules:FindFirstChild('AvatarEditorScreen')) +local GameSearchScreen = require(ShellModules:FindFirstChild('GameSearchScreen')) + +local AchievementManager = require(ShellModules:FindFirstChild('AchievementManager')) +local HintActionView = require(ShellModules:FindFirstChild('HintActionView')) + +local function CreateAppHub() + local this = {} + + local AppTabDock = AppTabDockModule( + UDim2.new(0,0,0.132,0), + UDim2.new(0,0,0,0)) + local appHubCns = {} + + local isShown = false + + local lastSelectedContentPane = nil + local lastParent = nil + + local HubContainer = Utility.Create'Frame' + { + Name = 'HubContainer'; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Visible = false; + } + + local PaneContainer = Utility.Create'Frame' + { + Name = 'PaneContainer'; + Size = UDim2.new(1, 0, 0.786, 0); + Position = UDim2.new(0,0,0.214,0); + BackgroundTransparency = 1; + Parent = HubContainer; + } + + AppTabDock:SetParent(HubContainer) + local HomeTab = AppTabDock:AddTab(AppTabDockItemModule(Strings:LocalizedString('HomeWord'), + HomePaneModule(PaneContainer))) + local AvatarTab = AppTabDock:AddTab(AppTabDockItemModule(Strings:LocalizedString('AvatarWord'), + AvatarPaneModule(PaneContainer))) + local GameTab = AppTabDock:AddTab(AppTabDockItemModule(Strings:LocalizedString('GamesWord'), + GamesPaneModule(PaneContainer))) + local SocialTab = AppTabDock:AddTab(AppTabDockItemModule(Strings:LocalizedString('FriendsWord'), + SocialPaneModule(PaneContainer))) + local StoreTab = AppTabDock:AddTab(AppTabDockItemModule(Strings:LocalizedString('CatalogWord'), + StorePaneModule(PaneContainer))) + + + Utility.Create'ImageLabel' + { + Name = 'RobloxLogo'; + Size = UDim2.new(0, 232, 0, 56); + Position = UDim2.new(0,0,0,0); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/ROBLOXLogoSmall@1080.png'; + Parent = HubContainer; + } + + local function SetSelectedTab(newTab) + AppTabDock:SetSelectedTab(newTab) + end + + -- Hint Action View + local hintActionViewX = HintActionView(HubContainer, "OpenHintAction") + hintActionViewX:SetImage('rbxasset://textures/ui/Shell/ButtonIcons/XButton.png') -- always X button for tab views + hintActionViewX:SetVisible(false) + + local hintActionViewY = HintActionView(HubContainer, "OpenYHintAction", UDim2.new(0, 0, 1, -1)) + hintActionViewY:SetImage('rbxasset://textures/ui/Shell/ButtonIcons/YButton.png') + hintActionViewY:SetVisible(false) + + -- Action Functions + local seenXButtonPressed = false + local seenYButtonPressed = false + + local function onOpenSettings(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + seenXButtonPressed = true + elseif inputState == Enum.UserInputState.End and seenXButtonPressed then + local settingsScreen = SettingsScreen() + EventHub:dispatchEvent(EventHub.Notifications["OpenSettingsScreen"], settingsScreen); + end + end + + local function onOpenAvatarEditor(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + seenYButtonPressed = true + elseif inputState == Enum.UserInputState.End and seenYButtonPressed then + local avatarEditorScreen = AvatarEditorScreen() + EventHub:dispatchEvent(EventHub.Notifications["OpenAvatarEditorScreen"], avatarEditorScreen); + seenYButtonPressed = false + end + end + + local function onOpenPartyUI(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + seenXButtonPressed = true + elseif inputState == Enum.UserInputState.End and seenXButtonPressed then + if UserSettings().GameSettings:InStudioMode() or UserInputService:GetPlatform() == Enum.Platform.Windows then + ScreenManager:OpenScreen(ErrorOverlay(Errors.Test.FeatureNotAvailableInStudio), false) + else + local success = pcall(function() + -- PlatformService may not exist in studio + return PlatformService:PopupPartyUI(inputObject.UserInputType) + end) + if not success then + ScreenManager:OpenScreen(ErrorOverlay(Errors.PlatformError.PopupPartyUI), false) + end + end + end + end + + local function onSearchGames(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + seenXButtonPressed = true + elseif inputState == Enum.UserInputState.End and seenXButtonPressed then + if PlatformService then + PlatformService:ShowKeyboard(Strings:LocalizedString("SearchGamesPhrase"), "", "", Enum.XboxKeyBoardType.Default) + end + seenXButtonPressed = false + end + end + + local function bindHintActionX(actionFunc, actionName) + hintActionViewX:SetText(actionName) + hintActionViewX:BindAction(actionFunc, Enum.KeyCode.ButtonX) + hintActionViewX:SetVisible(true) + end + + local function setHintAction(selectedTab) + hintActionViewX:UnbindAction() + hintActionViewX:SetVisible(false) + hintActionViewY:UnbindAction() + hintActionViewY:SetVisible(false) + + if selectedTab == HomeTab then + bindHintActionX(onOpenSettings, Strings:LocalizedString("SettingsWord")) + elseif selectedTab == GameTab then + if AchievementManager:AllGamesUnlocked() then + bindHintActionX(onSearchGames, Strings:LocalizedString("SearchWord")) + end + elseif selectedTab == SocialTab then + bindHintActionX(onOpenPartyUI, Strings:LocalizedString("StartPartyPhrase")) + elseif selectedTab == AvatarTab then + hintActionViewY:SetText(Strings:LocalizedString("AvatarEditorWord")) + hintActionViewY:BindAction(onOpenAvatarEditor, Enum.KeyCode.ButtonY) + hintActionViewY:SetVisible(true) + end + + -- NOTE: Avatar Tab has its own HintActionView as it needs to change visibility based on whats selected + end + + function this:GetName() + return lastSelectedContentPane and lastSelectedContentPane:GetName() or Strings:LocalizedString('HomeWord') + end + + --For analytics + function this:GetAnalyticsInfo() + local analyticsInfo = {} + local WidgetId = Analytics.WidgetNames('WidgetId') + local paneAnalyticsInfo = nil + if lastSelectedContentPane and type(lastSelectedContentPane.GetAnalyticsInfo) == "function" then + paneAnalyticsInfo = lastSelectedContentPane.GetAnalyticsInfo() + end + + --paneAnalyticsInfo should never be nil + if type(paneAnalyticsInfo) == "table" and paneAnalyticsInfo[WidgetId] then + analyticsInfo = paneAnalyticsInfo + else + analyticsInfo[WidgetId] = Analytics.WidgetNames('AppHubId') + end + return analyticsInfo + end + + function this:Show() + isShown = true + + HubContainer.Visible = true + HubContainer.Parent = lastParent + + EventHub:removeEventListener(EventHub.Notifications["NavigateToRobuxScreen"], 'AppHubListenToRobuxScreenSwitch') + EventHub:addEventListener(EventHub.Notifications["NavigateToRobuxScreen"], 'AppHubListenToRobuxScreenSwitch', + function() + if ScreenManager:ContainsScreen(this) then + while ScreenManager:GetTopScreen() ~= this and ScreenManager:ContainsScreen(this) do + ScreenManager:CloseCurrent() + end + if ScreenManager:GetTopScreen() == this then + if AppTabDock:GetSelectedTab() ~= StoreTab then + SetSelectedTab(StoreTab) + end + end + end + end) + + local openEquippedDebounce = false + EventHub:removeEventListener(EventHub.Notifications["NavigateToEquippedAvatar"], 'AppHubListenToAvatarScreenSwitch') + EventHub:addEventListener(EventHub.Notifications["NavigateToEquippedAvatar"], 'AppHubListenToAvatarScreenSwitch', + function() + if openEquippedDebounce then return end + openEquippedDebounce = true + if ScreenManager:ContainsScreen(this) then + while ScreenManager:GetTopScreen() ~= this and ScreenManager:ContainsScreen(this) do + ScreenManager:CloseCurrent() + end + if ScreenManager:GetTopScreen() == this then + if AppTabDock:GetSelectedTab() ~= AvatarTab then + SetSelectedTab(AvatarTab) + end + end + end + openEquippedDebounce = false + end) + + if not AchievementManager:AllGamesUnlocked() then + EventHub:removeEventListener(EventHub.Notifications["UnlockedUGC"], 'AppHubUnlockedUGC') + EventHub:addEventListener(EventHub.Notifications["UnlockedUGC"], "AppHubUnlockedUGC", function() + if isShown then + local selectedTab = AppTabDock:GetSelectedTab() + if selectedTab and selectedTab == GameTab then + hintActionViewX:UnbindAction() + hintActionViewX:SetVisible(false) + bindHintActionX(onSearchGames, Strings:LocalizedString("SearchWord")) + end + end + end) + end + + local currentlySelectedTab = AppTabDock:GetSelectedTab() + AppTabDock:SetSelectedTab(currentlySelectedTab) + if lastSelectedContentPane then + lastSelectedContentPane:Show() + end + end + + function this:Hide() + isShown = false + + if not ScreenManager:ContainsScreen(self) then + EventHub:removeEventListener(EventHub.Notifications["NavigateToRobuxScreen"], 'AppHubListenToRobuxScreenSwitch') + EventHub:removeEventListener(EventHub.Notifications["NavigateToEquippedAvatar"], 'AppHubListenToAvatarScreenSwitch') + end + EventHub:removeEventListener(EventHub.Notifications["UnlockedUGC"], 'AppHubUnlockedUGC') + + HubContainer.Visible = false + HubContainer.Parent = nil + + if lastSelectedContentPane then + lastSelectedContentPane:Hide() + end + end + + function this:Focus() + AppTabDock:ConnectEvents() + + local function initTabDock() + ContextActionService:BindCoreAction("CycleTabDock", + function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + if not AppTabDock:IsFocused() then + lastSelectedContentPane:RemoveFocus(true) + AppTabDock:Focus() + else + if inputObject.KeyCode == Enum.KeyCode.ButtonL1 then + local prevTab = AppTabDock:GetPreviousTab() + if prevTab then + AppTabDock:SetSelectedTab(prevTab) + end + elseif inputObject.KeyCode == Enum.KeyCode.ButtonR1 then + local nextTab = AppTabDock:GetNextTab() + if nextTab then + AppTabDock:SetSelectedTab(nextTab) + end + end + end + end + end, + false, + Enum.KeyCode.ButtonL1, Enum.KeyCode.ButtonR1) + + local seenBButtonBegin = false + ContextActionService:BindCoreAction("CloseAppHub", + function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + seenBButtonBegin = true + elseif inputState == Enum.UserInputState.End then + if seenBButtonBegin then + if not AppTabDock:IsFocused() then + lastSelectedContentPane:RemoveFocus(true) + AppTabDock:Focus() + end + end + end + end, + false, + Enum.KeyCode.ButtonB) + + local function focusTab(tab) + if tab then + if lastSelectedContentPane then + lastSelectedContentPane:Hide(true) + lastSelectedContentPane:RemoveFocus(true) + end + local selectedContentPane = tab:GetContentItem() + if selectedContentPane then + selectedContentPane:Show(true) + if not AppTabDock:IsFocused() then + AppTabDock:Focus() + end + end + lastSelectedContentPane = selectedContentPane + + -- set X actionf + setHintAction(tab) + end + end + + local function onSelectedTabChanged(selectedTab) + focusTab(selectedTab) + end + table.insert(appHubCns, AppTabDock.SelectedTabChanged:connect(onSelectedTabChanged)) + end + + initTabDock() + + local function onSelectedTabClicked(selectedTab) + local selectedContentPane = selectedTab and selectedTab:GetContentItem() + if selectedContentPane then + selectedContentPane:Focus(AppTabDock) + end + end + table.insert(appHubCns, AppTabDock.SelectedTabClicked:connect(onSelectedTabClicked)) + + local function onSelectionChanged(prop) + if prop == "SelectedCoreObject" then + if AppTabDock:IsFocused() then + AppTabDock:Show() + if lastSelectedContentPane then + lastSelectedContentPane:RemoveFocus() + end + end + end + + if prop == "SelectedObject" then + local currentSelection = GuiService.SelectedCoreObject + if currentSelection and lastSelectedContentPane then + -- first condition checks if function exist + if lastSelectedContentPane.IsFocused and not lastSelectedContentPane:IsFocused() and + lastSelectedContentPane.IsAncestorOf then + if lastSelectedContentPane:IsAncestorOf(currentSelection) then + lastSelectedContentPane:Focus(AppTabDock) + end + end + end + end + end + table.insert(appHubCns, GuiService.Changed:connect(onSelectionChanged)) + + local function onKeyboardClosed(searchWord) + searchWord = Utility.SpaceNormalizeString(searchWord) + if #searchWord > 0 then + local searchScreen = GameSearchScreen(searchWord) + searchScreen:SetParent(HubContainer.Parent) + ScreenManager:OpenScreen(searchScreen) + end + end + if PlatformService then + table.insert(appHubCns, PlatformService.KeyboardClosed:connect(onKeyboardClosed)) + end + + if AppTabDock:GetSelectedTab() == nil then + AppTabDock:SetSelectedTab(HomeTab) + end + + if lastSelectedContentPane then + lastSelectedContentPane:Focus(AppTabDock) + end + + setHintAction(AppTabDock:GetSelectedTab()) + end + + function this:RemoveFocus() + AppTabDock:DisconnectEvents() + + ContextActionService:UnbindCoreAction("CycleTabDock") + ContextActionService:UnbindCoreAction("CloseAppHub") + + if lastSelectedContentPane then + lastSelectedContentPane:RemoveFocus() + end + + for k,v in pairs(appHubCns) do + v:disconnect() + appHubCns[k] = nil + end + + ContextActionService:UnbindCoreAction("OpenHintAction") + ContextActionService:UnbindCoreAction("OpenYHintAction") + end + + function this:SetParent(newParent) + lastParent = newParent + end + + return this +end + +return CreateAppHub diff --git a/Client2018/content/internal/AppShell/Modules/Shell/AppState.lua b/Client2018/content/internal/AppShell/Modules/Shell/AppState.lua new file mode 100644 index 0000000..20078a2 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/AppState.lua @@ -0,0 +1,23 @@ +-- for now AppState will be singleton so we can more easily migrate to Rodux +-- but if we also migrate to Roact, this will need to change + +local ShellModules = script.Parent +local Modules = ShellModules.Parent +local Common = Modules.Common + +local AppShellReducer = require(ShellModules.Reducers.AppShellReducer) +local Store = require(Common.Rodux).Store + +local AppState = {} + +function AppState:Init() + self.store = Store.new(AppShellReducer) +end + +function AppState:Destruct() + self.store:destruct() +end + +AppState:Init() + +return AppState diff --git a/Client2018/content/internal/AppShell/Modules/Shell/AssetManager.lua b/Client2018/content/internal/AppShell/Modules/Shell/AssetManager.lua new file mode 100644 index 0000000..1a77be5 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/AssetManager.lua @@ -0,0 +1,27 @@ +-- Written by Kip Turner, Copyright Roblox 2015 + +local CoreGui = game:GetService("CoreGui") + +local RobloxGui = CoreGui:FindFirstChild("RobloxGui") +local Modules = RobloxGui:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local AssetManager = {} + +function AssetManager.CreateShadow(zIndex) + return Utility.Create'ImageLabel' + { + Name = 'Shadow'; + Image = 'rbxasset://textures/ui/Shell/Buttons/Generic9ScaleShadow.png'; + Size = UDim2.new(1,3,1,3); + Position = UDim2.new(0,0,0,0); + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(10,10,28,28); + BackgroundTransparency = 1; + ZIndex = zIndex or 1; + } +end + +return AssetManager diff --git a/Client2018/content/internal/AppShell/Modules/Shell/AvatarEditorScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/AvatarEditorScreen.lua new file mode 100644 index 0000000..ee2ac95 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/AvatarEditorScreen.lua @@ -0,0 +1,129 @@ + +local CoreGui = game:GetService('CoreGui') +local GuiRoot = CoreGui:FindFirstChild('RobloxGui') +local Modules = GuiRoot:FindFirstChild('Modules') +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) +local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local TweenController = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController) + +local ShellModules = Modules:FindFirstChild('Shell') +local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local CameraManager = require(ShellModules:FindFirstChild('CameraManager')) +local CreateAvatarEditorView = require(ShellModules:FindFirstChild('AvatarEditorView')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local AppContainer = require(ShellModules.AppContainer) + +local TitleSafeContainer = AppContainer.TitleSafeContainer + +local ThirdPartyUserService = nil +pcall(function() ThirdPartyUserService = game:GetService('ThirdPartyUserService') end) + +local AvatarEditorView = nil +local AvatarEditorReconstruct = false + +local function createAvatarEditorScreen() + local this = BaseScreen() + this.fixPosition = true + local view = this.GetView() + view.BackImage.ZIndex = 2 + view.BackText.ZIndex = 2 + view.TitleText.ZIndex = 2 + + local adjustPos = UDim2.new(0, TitleSafeContainer.AbsolutePosition.X, 0, TitleSafeContainer.AbsolutePosition.Y) + local backImagePosition = view.BackImage.Position + adjustPos + local backTextPosition = view.BackText.Position + adjustPos + local titleTextPosition = view.TitleText.Position + adjustPos + + view.BackImage.Position = backImagePosition + view.BackText.Position = backTextPosition + view.TitleText.Position = titleTextPosition + + --Reconstruct the AvatarEditorView if user switched/assets purchased + if AvatarEditorReconstruct and AvatarEditorView then + AvatarEditorView:Destruct() + AvatarEditorView = nil + AvatarEditorReconstruct = false + end + AvatarEditorView = AvatarEditorView or CreateAvatarEditorView() + + local storeChangedCn = nil + local tweenInfo = TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + local tweenPositionOffset = UDim2.new(0, -400, 0, 0) + + local parentShow = this.Show + function this:Show() + parentShow(self) + CameraManager:SwitchToAvatarEditor() + AvatarEditorView:Show(this.Container) + end + + local parentHide = this.Hide + function this:Hide() + parentHide(self) + AvatarEditorView:Hide() + CameraManager:SwitchToFlyThrough() + end + + local parentFocus = this.Focus + function this:Focus() + parentFocus(self) + storeChangedCn = AppState.Store.Changed:Connect(function(newState, oldState) + if newState.FullView ~= oldState.FullView then + if newState.FullView then + TweenController(view.BackImage, tweenInfo, { Position = backImagePosition + tweenPositionOffset }) + TweenController(view.BackText, tweenInfo, { Position = backTextPosition + tweenPositionOffset }) + TweenController(view.TitleText, tweenInfo, { Position = titleTextPosition + tweenPositionOffset }) + else + TweenController(view.BackImage, tweenInfo, { Position = backImagePosition }) + TweenController(view.BackText, tweenInfo, { Position = backTextPosition }) + TweenController(view.TitleText, tweenInfo, { Position = titleTextPosition }) + end + end + end) + AvatarEditorView:Focus() + end + + local parentRemoveFocus = this.RemoveFocus + function this:RemoveFocus() + parentRemoveFocus(self) + AvatarEditorView:RemoveFocus() + Utilities.disconnectEvent(storeChangedCn) + + --Don't save selectedObject, as we already saved it in AvatarEditorView + self.SavedSelectedObject = nil + end + + local parentSetParent = this.SetParent + function this:SetParent(newParent) + parentSetParent(self, newParent) + end + + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('AvatarEditorScreenId')} + end + + return this +end + +local function OnUserAccountChanged() + AvatarEditorReconstruct = true + EventHub:removeEventListener(EventHub.Notifications["AvatarPurchaseSuccess"], "AvatarEditorScreen") + EventHub:addEventListener(EventHub.Notifications["AvatarPurchaseSuccess"], "AvatarEditorScreen", + function() + AvatarEditorReconstruct = true + end) +end + +EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "AvatarEditorScreen", OnUserAccountChanged) + +local function OnUserSignOut() + AvatarEditorReconstruct = true + EventHub:removeEventListener(EventHub.Notifications["AvatarPurchaseSuccess"], "AvatarEditorScreen") +end + +if ThirdPartyUserService then + ThirdPartyUserService.ActiveUserSignedOut:connect(OnUserSignOut) +end + +return createAvatarEditorScreen diff --git a/Client2018/content/internal/AppShell/Modules/Shell/AvatarEditorView.lua b/Client2018/content/internal/AppShell/Modules/Shell/AvatarEditorView.lua new file mode 100644 index 0000000..ae42856 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/AvatarEditorView.lua @@ -0,0 +1,516 @@ +-------------- CONSTANTS -------------- +local STICK_ROTATION_MULTIPLIER = 3 +local THUMBSTICK_DEADZONE = 0.2 + +-------------- SERVICES -------------- +local ContextActionService = game:GetService("ContextActionService") +local ReplicatedStorage = game:GetService('ReplicatedStorage') +local CoreGui = game:GetService('CoreGui') + +------------ MODULES ------------------- +local GuiRoot = CoreGui:FindFirstChild('RobloxGui') +local Modules = GuiRoot:FindFirstChild('Modules') + +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) +local SetConsoleMenuLevel = require(Modules.LuaApp.Actions.SetConsoleMenuLevel) +local ResetCategory = require(Modules.LuaApp.Actions.ResetCategory) +local ToggleAvatarType = require(Modules.LuaApp.Actions.ToggleAvatarType) +local ToggleAvatarEditorFullView = require(Modules.LuaApp.Actions.ToggleAvatarEditorFullView) + +local CreateCharacterManager = require(Modules.LuaApp.Legacy.AvatarEditor.CharacterManager) +local CreatePageManager = require(Modules.LuaApp.Legacy.AvatarEditor.PageManagerConsole) +local CreateCategoryMenu = require(Modules.LuaApp.Legacy.AvatarEditor.CategoryMenuConsole) +local CreateTabList = require(Modules.LuaApp.Legacy.AvatarEditor.TabListConsole) +local ConsoleButtonIndicators = require(Modules.LuaApp.Legacy.AvatarEditor.ConsoleButtonIndicators) +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfoConsole) +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) +local XboxAppState = require(Modules.Shell.AppState) + +----------- UTILITIES -------------- +local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local TableUtilities = require(Modules.LuaApp.TableUtilities) + +------------ SHELL MODULES ------------------- +local ShellModules = script.Parent +local CameraManager = require(ShellModules:FindFirstChild('CameraManager')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) + +-------------- FFLAGS -------------- +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) + +------------ VARIABLES ------------------- +local characterTemplates = { + CharacterR6 = ReplicatedStorage:WaitForChild('CharacterR6'); + CharacterR15 = ReplicatedStorage:WaitForChild('CharacterR15'); + CharacterR15New = ReplicatedStorage:WaitForChild('CharacterR15New'); +} + + +local cameraTweenerObject = { + tweenCamera = function(newCFrame, newFOV) + CameraManager:UpdateAvatarEditorCamera(newCFrame, newFOV) + end +} + +----------- CLASS DECLARATION -------------- + +local function createAvatarEditorView() + AppState:Init() + local this = {} + local storeChangedCn = nil + local scrollingFrameCn = nil + local avatarEditorLoader = nil + local savedSelectedGuiObject = nil + local selectedGuiObjectEnterFullView = nil + local toggleButtonDebounce = true + local toggleViewDebounce = true + local showCount = 0 + local inShow = false + local inFocus = false + local lastCharacterSaveTime = nil + + local characterUpdated = false + local characterEquipped = false + local savingAvatarMutex = false + local rotationInfo = { + rotation = 0; + lastRotation = 0; + delta = 0; + } + + local Container = Utilities.create'ScrollingFrame' + { + Name = 'AvatarEditorContainer'; + Size = UDim2.new(1, 0, 1, 0); + CanvasSize = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + ScrollingEnabled = false; + Selectable = false; + Visible = true; + BorderSizePixel = 0; + ScrollBarThickness = 0; + } + + local BackgroundOverlay = Utilities.create'ImageLabel' + { + Name = 'BackgroundOverlay'; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Visible = true; + Image = 'rbxasset://textures/ui/Shell/AvatarEditor/graphic/gr-background overlay merge.png'; + ZIndex = LayoutInfo.BackgroundLayer; + Parent = Container; + } + + local frame = Utilities.create'Frame' + { + Position = UDim2.new(0, 0, 0, 270); + Size = UDim2.new(1, 0, 1, 0); + Name = "PageFrame"; + BackgroundTransparency = 1; + ClipsDescendants = true; + Visible = true; + Selectable = false; + ZIndex = LayoutInfo.BasicLayer; + Parent = Container; + } + local scrollingFrame = Utilities.create'ScrollingFrame' + { + AnchorPoint = Vector2.new(1, 0); + Position = UDim2.new(1, -99, 0, 0); + Size = UDim2.new(0, 491, 1, -270); + Name = "ScrollingFrame"; + BackgroundTransparency = 1; + ClipsDescendants = true; + Visible = true; + ScrollingEnabled = false; + ScrollBarThickness = 0; + Selectable = false; + ZIndex = LayoutInfo.BasicLayer; + Parent = frame; + } + + ConsoleButtonIndicators.init(Container) + local warningFrame = Utilities.create'Frame' + { + AnchorPoint = Vector2.new(0.5, 0); + Position = UDim2.new(0.5, 0, 0, 0); + Size = UDim2.new(0, LayoutInfo.WarningMaxLength, 1, 0); + Name = "WarningWidget"; + BackgroundTransparency = 1; + Visible = true; + Selectable = false; + ZIndex = LayoutInfo.IndicatorLayer; + Parent = Container; + } + + local CameraController = require(Modules.LuaApp.Legacy.AvatarEditor.CameraController)( + cameraTweenerObject, LayoutInfo.CameraCenterScreenPosition) + + + local characterManager = CreateCharacterManager( + { + get = Utilities.httpGet; + post = Utilities.httpPost; + }, + characterTemplates, + 1 + ) + + local WarningWidget = require(Modules.LuaApp.Legacy.AvatarEditor.WarningWidget)(warningFrame, characterManager, true) + + local PageManager = CreatePageManager( + XboxAppState.store:getState().RobloxUser.rbxuid, + scrollingFrame, + characterManager + ) + characterManager:setUpdateCameraCallback(CameraController.updateCamera) + + local CategoryMenu = CreateCategoryMenu(Container) + local TabList = CreateTabList(Container, PageManager) + + local function initStickRotation() + local gamepadInput = Vector2.new(0, 0) + ContextActionService:UnbindCoreAction("StickRotation") + ContextActionService:BindCoreAction("StickRotation", function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Change then + gamepadInput = inputObject.Position or gamepadInput + gamepadInput = Vector2.new(gamepadInput.X, gamepadInput.Y) + if math.abs(gamepadInput.X) > THUMBSTICK_DEADZONE then + rotationInfo.delta = STICK_ROTATION_MULTIPLIER * gamepadInput.X + else + rotationInfo.delta = 0 + end + end + end, + false, Enum.KeyCode.Thumbstick2) + end + + local function initKeyControls() + toggleButtonDebounce = true + toggleViewDebounce = true + ContextActionService:UnbindCoreAction("KeyControls") + ContextActionService:BindCoreAction("KeyControls", function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + if inputObject.KeyCode == Enum.KeyCode.ButtonSelect and not AppState.Store:getState().FullView then + if toggleButtonDebounce then + toggleButtonDebounce = false + SoundManager:Play('ButtonPress') + AppState.Store:dispatch(ToggleAvatarType()) + toggleButtonDebounce = true + end + end + + if inputObject.KeyCode == Enum.KeyCode.ButtonR3 then + if toggleViewDebounce then + toggleViewDebounce = false + SoundManager:Play('ScreenChange') + AppState.Store:dispatch(ToggleAvatarEditorFullView()) + toggleViewDebounce = true + end + end + end + end, + false, Enum.KeyCode.ButtonR3, Enum.KeyCode.ButtonSelect) + end + + local function initMenuActions() + ContextActionService:UnbindCoreAction("AvatarEditorMenu") + + ContextActionService:BindCoreAction("AvatarEditorMenu", function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + if inputObject.KeyCode == Enum.KeyCode.ButtonB then + if AppState.Store:getState().ConsoleMenuLevel > LayoutInfo.ConsoleMenuLevel.CategoryMenu then + local currentMenuLevel = AppState.Store:getState().ConsoleMenuLevel + SoundManager:Play('PopUp') + AppState.Store:dispatch(SetConsoleMenuLevel(currentMenuLevel - 1)) + else + -- Back to avatar page in AppShell + AppState.Store:dispatch(ResetCategory()) + AppState.Store:dispatch(SetConsoleMenuLevel(LayoutInfo.ConsoleMenuLevel.None)) + return Enum.ContextActionResult.Pass + end + end + elseif inputState == Enum.UserInputState.Begin then + if inputObject.KeyCode == Enum.KeyCode.ButtonL2 then + if AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.CategoryMenu then + CategoryMenu:SelectPreviousPage() + elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.TabList then + TabList:SelectPreviousPage() + elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.AssetsPage then + PageManager:SelectPreviousPage() + end + elseif inputObject.KeyCode == Enum.KeyCode.ButtonR2 then + if AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.CategoryMenu then + CategoryMenu:SelectNextPage() + elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.TabList then + TabList:SelectNextPage() + elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.AssetsPage then + PageManager:SelectNextPage() + end + end + end + end, + false, Enum.KeyCode.ButtonB, Enum.KeyCode.ButtonL2, Enum.KeyCode.ButtonR2) + end + + local function updateFullViewCoreAction(fullView) + ContextActionService:UnbindCoreAction("FullView") + if fullView == true then + ContextActionService:BindCoreAction("FullView", + function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + if inputObject.KeyCode == Enum.KeyCode.ButtonB then + SoundManager:Play('ScreenChange') + AppState.Store:dispatch(ToggleAvatarEditorFullView()) + end + return Enum.ContextActionResult.Sink + end + end, + false, Enum.KeyCode.ButtonB, Enum.KeyCode.ButtonA, Enum.KeyCode.ButtonL2, Enum.KeyCode.ButtonR2) + + BackgroundOverlay.Visible = false + elseif fullView == false then + BackgroundOverlay.Visible = true + end + end + + + local function FocusAll() + initStickRotation() + initKeyControls() + initMenuActions() + CameraController:Focus() + + CategoryMenu:Focus() + TabList:Focus() + PageManager:Focus() + WarningWidget:Focus() + + if not savedSelectedGuiObject and not AppState.Store:getState().FullView then + AppState.Store:dispatch(SetConsoleMenuLevel(LayoutInfo.ConsoleMenuLevel.CategoryMenu)) + elseif savedSelectedGuiObject then + Utilities.setSelectedCoreObject(savedSelectedGuiObject) + end + updateFullViewCoreAction(AppState.Store:getState().FullView) + + storeChangedCn = AppState.Store.Changed:Connect(function(newState, oldState) + if newState.FullView ~= oldState.FullView then + updateFullViewCoreAction(newState.FullView) + if newState.FullView == true then + local selectedCoreObject = Utilities.getSelectedCoreObject() + if selectedCoreObject then + selectedGuiObjectEnterFullView = selectedCoreObject + end + Utilities.setSelectedCoreObject(nil) + else + if selectedGuiObjectEnterFullView then + Utilities.setSelectedCoreObject(selectedGuiObjectEnterFullView) + end + end + end + + if not characterEquipped or not characterUpdated then + local didUpdate = false + local didEquip = false + + if newState.Character.AvatarType ~= oldState.Character.AvatarType then + didUpdate = true + end + + if newState.Character.Assets ~= oldState.Character.Assets then + for assetType, assetList in pairs(oldState.Character.Assets) do + if not newState.Character.Assets[assetType] and assetList then + if next(assetList) ~= nil then + didUpdate = true + didEquip = true + break + end + end + end + + if not didUpdate or not didEquip then + for assetType, _ in pairs(newState.Character.Assets) do + if newState.Character.Assets[assetType] ~= oldState.Character.Assets[assetType] then + local addTheseAssets = TableUtilities.ListDifference( + newState.Character.Assets[assetType] or {}, + oldState.Character.Assets[assetType] or {}) + local removeTheseAssets = + TableUtilities.ListDifference(oldState.Character.Assets[assetType] or {}, + newState.Character.Assets[assetType] or {}) + + if next(addTheseAssets) ~= nil or next(removeTheseAssets) ~= nil then + didUpdate = true + didEquip = true + break + end + end + end + end + end + + if newState.Character.BodyColors ~= oldState.Character.BodyColors then + local differentBodyColors = + TableUtilities.TableDifference(newState.Character.BodyColors, oldState.Character.BodyColors) + if next(differentBodyColors) ~= nil then + didUpdate = true + end + end + + if newState.Character.Scales ~= oldState.Character.Scales then + local differentScales = + TableUtilities.TableDifference(newState.Character.Scales, oldState.Character.Scales) + if next(differentScales) ~= nil then + didUpdate = true + end + end + + if not characterEquipped then + characterEquipped = didEquip + end + + if not characterUpdated then + characterUpdated = didUpdate + end + end + end) + + scrollingFrameCn = scrollingFrame.Changed:connect( + function(prop) + if prop == 'CanvasPosition' then + PageManager:updateListContent(scrollingFrame.CanvasPosition.Y) + end + end + ) + end + + local function RemoveFocusAll() + CameraController:RemoveFocus() + PageManager:RemoveFocus() + CategoryMenu:RemoveFocus() + TabList:RemoveFocus() + WarningWidget:RemoveFocus() + Utilities.disconnectEvent(storeChangedCn) + Utilities.disconnectEvent(scrollingFrameCn) + ContextActionService:UnbindCoreAction("FullView") + ContextActionService:UnbindCoreAction("AvatarEditorMenu") + ContextActionService:UnbindCoreAction("StickRotation") + ContextActionService:UnbindCoreAction("KeyControls") + end + + function this:Show(parent) + local startCount = showCount + inShow = true + characterUpdated = false + characterEquipped = false + local prevAvatarEditorLoader = avatarEditorLoader + + Utilities.fastSpawn(function() + if startCount == showCount then + avatarEditorLoader = LoadingWidget( + {Parent = parent}, + {function() + if prevAvatarEditorLoader then + prevAvatarEditorLoader:AwaitFinished() + prevAvatarEditorLoader:Cleanup() + end + if startCount == showCount then + characterManager.initFromServer() + characterManager.show() + end + end} + ) + avatarEditorLoader:AwaitFinished() + if startCount == showCount then + avatarEditorLoader:Cleanup() + avatarEditorLoader = nil + end + end + + if startCount == showCount then + Container.Parent = parent + if inFocus then + FocusAll() + end + + while startCount == showCount do + local deltaT = Utilities.renderWait() + if startCount == showCount then + rotationInfo.rotation = rotationInfo.rotation + deltaT * rotationInfo.delta + characterManager.setRotation(rotationInfo.rotation) + if not lastCharacterSaveTime or tick() - lastCharacterSaveTime > 5 then + characterManager.saveToServer(false) + lastCharacterSaveTime = tick() + end + end + end + end + end) + end + + function this:Hide() + inShow = false + showCount = showCount + 1 + characterManager.hide() + savedSelectedGuiObject = nil + selectedGuiObjectEnterFullView = nil + Container.Parent = nil + + if not avatarEditorLoader then + RemoveFocusAll() + CategoryMenu:Hide() + WarningWidget:Hide() + local savedCharacterUpdated = characterUpdated + local savedCharacterEquipped = characterEquipped + spawn(function() + while savingAvatarMutex do + wait() + end + savingAvatarMutex = true + characterManager.saveToServer(true) + savingAvatarMutex = false + + if savedCharacterUpdated then + Utilities.fastSpawn(function() + EventHub:dispatchEvent(EventHub.Notifications["CharacterUpdated"]) + end) + end + + if savedCharacterEquipped then + Utilities.fastSpawn(function() + EventHub:dispatchEvent(EventHub.Notifications["CharacterEquipped"], AppState.Store:getState().Character.Assets, savedCharacterUpdated) + end) + end + end) + end + end + + --Rebind actions, make them in the right order + function this:Focus() + inFocus = true + if inShow and not avatarEditorLoader then + FocusAll() + end + end + + function this:RemoveFocus() + inFocus = false + if inShow and not avatarEditorLoader then + savedSelectedGuiObject = Utilities.getSelectedCoreObject() + RemoveFocusAll() + end + end + + function this:Destruct() + this:RemoveFocus() + this:Hide() + Container:Destroy() + characterManager.destroy() + AppState:Destruct() + end + + return this +end + +return createAvatarEditorView diff --git a/Client2018/content/internal/AppShell/Modules/Shell/AvatarPane.lua b/Client2018/content/internal/AppShell/Modules/Shell/AvatarPane.lua new file mode 100644 index 0000000..e5e187a --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/AvatarPane.lua @@ -0,0 +1,706 @@ +-- Written by Kip Turner and Bo Zhang, Copyright Roblox 2017 + +local CoreGui = game:GetService("CoreGui") +local GuiService = game:GetService('GuiService') +local RunService = game:GetService('RunService') + +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local PackageData = require(ShellModules:FindFirstChild('PackageData')) +local ScrollingGridModule = require(ShellModules:FindFirstChild('ScrollingGrid')) +local AvatarTile = require(ShellModules:FindFirstChild('AvatarTile')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) +local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader')) + +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) + +local HintActionView = require(ShellModules:FindFirstChild('HintActionView')) +local XboxAppState = require(ShellModules:FindFirstChild('AppState')) + +local GLOW_BASE_RPM = 2 +local GLOW_TOP_RPM = -0.5 +local GLOW_TRANSPARENCY = 0.2 +local CATALOG_BELOW_POSITION = 320 + + +local function CreateAvatarPane(parent) + local this = {} + + local inFocus = false + local isShown = false + + local AvatarObjects = {} + + local OnGuiServiceChangedConn = nil + + local lastParent = parent + + + local MainContainer = Utility.Create'Frame' + { + Name = 'AvatarPane'; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Visible = false; + } + + + + local MyAvatarContainer = Utility.Create'Frame' + { + Name = 'MyAvatarContainer'; + Size = UDim2.new(0.38,0,1,0); + Position = UDim2.new(0,0,0,0); + BackgroundTransparency = 1; + ClipsDescendants = true; + Parent = MainContainer; + } + local MyNameLabel = Utility.Create'TextLabel' + { + Name = 'MyNameLabel'; + Text = ''; + Size = UDim2.new(1,0,0,25); + Position = UDim2.new(0,12,0,0); + TextXAlignment = 'Left'; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.SubHeaderSize; + BackgroundTransparency = 1; + Parent = MyAvatarContainer; + }; + local ProfileImageContainer = Utility.Create'Frame' + { + Name = 'ProfileImageContainer'; + Size = UDim2.new(0.68,0,0.9,-MyNameLabel.Size.Y.Offset); + Position = UDim2.new(0.16, 0, 0.05, MyNameLabel.Size.Y.Offset); + BackgroundTransparency = 1; + Parent = MyAvatarContainer; + } + local ProfileImage = Utility.Create'ImageLabel' + { + Name = 'ProfileImage'; + Size = UDim2.new(0,780,0,780); + Position = UDim2.new(0.5, 0, 0.5, 0); + BackgroundTransparency = 1; + ZIndex = 3; + Parent = ProfileImageContainer; + }; + local CrossfadeProfileImage = Utility.Create'ImageLabel' + { + Name = 'CrossfadeProfileImage'; + Size = UDim2.new(1,0,1,0); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + ImageTransparency = 1; + ZIndex = 3; + Parent = ProfileImage; + }; + local CharacterGlowBase = Utility.Create'ImageLabel' + { + Name = 'CharacterGlowBase'; + Size = UDim2.new(0,1015,0,1002); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Images/CharacterGlow/CharacterGlowBase.png'; + ImageTransparency = GLOW_TRANSPARENCY; + Parent = CrossfadeProfileImage; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + }; + local CharacterGlowTop = Utility.Create'ImageLabel' + { + Name = 'CharacterGlowTop'; + Size = UDim2.new(0,1026,0,1009); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Images/CharacterGlow/CharacterGlowTop.png'; + ImageTransparency = GLOW_TRANSPARENCY; + ZIndex = 2; + Parent = CrossfadeProfileImage; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + }; + local function onSizeChanged() + ProfileImage.Size = Utility.CalculateFill(ProfileImage, ThumbnailLoader.AvatarSizes.Size352x352) + ProfileImage.AnchorPoint = Vector2.new(0.5, 0.5) + ProfileImage.Position = UDim2.new(0.5, 0, 0.5, 0) + end + + -- Hint Action View + local hintActionView = HintActionView(nil, "AvatarPaneSelectAction") + hintActionView:SetImage('rbxasset://textures/ui/Shell/ButtonIcons/XButton.png') + hintActionView:SetText(Strings:LocalizedString('EquipWord')) + hintActionView:SetTransparency(1) + hintActionView:SetParent(lastParent.Parent) -- this assumes parent is HubContainer from AppHub.lua + + local function UpdateEquipButton(IsOwned, IsWearing) + hintActionView:SetVisibleWithTween( (IsOwned and not IsWearing) and 0 or 1) + end + + local SelectableAvatarsContainer = Utility.Create'Frame' + { + Name = 'SelectableAvatarsContainer'; + Size = UDim2.new(0.6,0,1,0); + Position = UDim2.new(0.4,0,0,0); + BackgroundTransparency = 1; + Parent = MainContainer; + } + + local NoCatalogStatusMessage = Utility.Create'TextLabel' + { + Name = 'NoCatalogStatusMessage'; + Text = Strings:LocalizedString('DefaultErrorPhrase'); + Size = UDim2.new(0.9,0,1,-125); + Position = UDim2.new(0.05, 0, 0, 0); + TextColor3 = GlobalSettings.GreyTextColor; + TextWrapped = true; + TextTransparency = GlobalSettings.FriendStatusTextTransparency; + Font = GlobalSettings.BoldFont; + FontSize = GlobalSettings.DescriptionSize; + BackgroundTransparency = 1; + Visible = false; + Parent = SelectableAvatarsContainer; + }; + + local MyCollectionTitle = Utility.Create'TextLabel' + { + Name = 'MyCollectionTitle'; + Text = Strings:LocalizedString('AvatarOutfitsTitle'); + Size = UDim2.new(1,0,0,40); + TextXAlignment = 'Left'; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.SubHeaderSize; + BackgroundTransparency = 1; + Visible = false; + Parent = SelectableAvatarsContainer; + }; + + local MyCollectionScroller = ScrollingGridModule() + MyCollectionScroller:SetSize(UDim2.new(1,0,1,-MyCollectionTitle.Size.Y.Offset - 40)) + MyCollectionScroller:SetScrollDirection(MyCollectionScroller.Enum.ScrollDirection.Horizontal) + MyCollectionScroller:SetCellSize(Vector2.new(220, 220)) + MyCollectionScroller:SetSpacing(Vector2.new(25,25)) + MyCollectionScroller:SetPosition(UDim2.new(0,0,0,MyCollectionTitle.Size.Y.Offset)) + MyCollectionScroller:SetRowColumnConstraint(1) + -- MyCollectionScroller:SetParent(SelectableAvatarsContainer) + + local CatalogTitle = Utility.Create'TextLabel' + { + Name = 'CatalogTitle'; + Text = Strings:LocalizedString('AvatarCatalogTitle'); + Size = UDim2.new(1,0,0,40); + Position = UDim2.new(0,0,0,0); + TextXAlignment = 'Left'; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.SubHeaderSize; + BackgroundTransparency = 1; + Parent = SelectableAvatarsContainer; + }; + + local AvatarScroller = ScrollingGridModule() + AvatarScroller:SetSize(UDim2.new(1,0,1,-CatalogTitle.Size.Y.Offset - 40)) + AvatarScroller:SetScrollDirection(AvatarScroller.Enum.ScrollDirection.Horizontal) + AvatarScroller:SetCellSize(Vector2.new(220, 220)) + AvatarScroller:SetPosition(UDim2.new(0,0,0,40)) + AvatarScroller:SetSpacing(Vector2.new(25,25)) + AvatarScroller:SetRowColumnConstraint(2) + AvatarScroller:SetParent(SelectableAvatarsContainer) + + local function SortMyCollectionScroller() + MyCollectionScroller:SortItems( + function(a, b) + local aObject = a and AvatarObjects[a] and AvatarObjects[a]:GetPackageInfo() + local bObject = b and AvatarObjects[b] and AvatarObjects[b]:GetPackageInfo() + local aIsEquipped = aObject and aObject:IsWearing() + local bIsEquipped = bObject and bObject:IsWearing() + local aName = aObject and aObject:GetName() + local bName = bObject and bObject:GetName() + + if aIsEquipped then return true elseif bIsEquipped then return false end + if aName and bName then + return aName < bName + end + return aObject ~= nil + end) + end + + --if all avatars owned, MyCollection should be 2 rows and catalog should be hidden + local function CheckAllAvatarsOwned() + if #AvatarScroller.GridItems == 0 then + MyCollectionScroller:SetRowColumnConstraint(2) + CatalogTitle.Visible = false + AvatarScroller.Visible = false + AvatarScroller:SetParent(nil) + else + MyCollectionScroller:SetRowColumnConstraint(1) + CatalogTitle.Visible = true + AvatarScroller.Visible = true + AvatarScroller:SetParent(SelectableAvatarsContainer) + end + end + + local function OnAddToMyCollectionScroller() + CatalogTitle.Position = UDim2.new(0,0,0, CATALOG_BELOW_POSITION) + AvatarScroller:SetPosition(UDim2.new(0,0,0,CATALOG_BELOW_POSITION + 40)) + AvatarScroller:SetRowColumnConstraint(1) + MyCollectionTitle.Visible = true + MyCollectionScroller:SetParent(SelectableAvatarsContainer) + CheckAllAvatarsOwned() + SortMyCollectionScroller() + end + + local LoaderSpinner = nil + local GlobalFadeCount = 1 + local function CrossfadeAvatarImage(frontImage, fadeImage, imageSource, duration) + if not imageSource then return end + duration = duration or 0.75 + + GlobalFadeCount = GlobalFadeCount + 1 + local thisFadeCount = GlobalFadeCount + local fadeoutDuration = duration + + spawn(function() + local dummyImage = nil + if LoaderSpinner then + LoaderSpinner:Cleanup() + end + + --If imageSource if a function, then it's maybe a async call to get new image and we show the loaderspinner. + --Otherwise, if the imageSource is a valid new image, we just do the Crossfade (without the spinner) + if type(imageSource) == 'function' then + local newLoaderSpinner = LoadingWidget({Parent = frontImage, ZIndex = 3}, { + function() dummyImage = imageSource() end + }) + LoaderSpinner = newLoaderSpinner + newLoaderSpinner:AwaitFinished() + newLoaderSpinner:Cleanup() + elseif type(imageSource) == 'table' and imageSource.Image then + dummyImage = imageSource + end + + if thisFadeCount == GlobalFadeCount then + --Avoid updating with same image + if dummyImage and frontImage and dummyImage.Image ~= frontImage.Image then + local noPreviousImage = (frontImage.Image == "" and fadeImage.Image == "") + if noPreviousImage then + fadeoutDuration = 0 + else + fadeImage.Image = frontImage.Image + end + + + Utility.PropertyTweener(fadeImage, 'ImageTransparency', noPreviousImage and 0 or frontImage.ImageTransparency, 1, fadeoutDuration, Utility.EaseInOutQuad, true) + Utility.PropertyTweener(CharacterGlowBase, 'ImageTransparency', CharacterGlowBase.ImageTransparency, 1, fadeoutDuration, Utility.EaseInOutQuad, true) + Utility.PropertyTweener(CharacterGlowTop, 'ImageTransparency', CharacterGlowTop.ImageTransparency, 1, fadeoutDuration, Utility.EaseInOutQuad, true) + frontImage.ImageTransparency = 1 + + frontImage.Image = dummyImage.Image + + Utility.PropertyTweener(frontImage, 'ImageTransparency', frontImage.ImageTransparency, 0, duration, Utility.EaseInOutQuad, true) + Utility.PropertyTweener(CharacterGlowBase, 'ImageTransparency', CharacterGlowBase.ImageTransparency, GLOW_TRANSPARENCY, duration, Utility.EaseInOutQuad, true) + Utility.PropertyTweener(CharacterGlowTop, 'ImageTransparency', CharacterGlowTop.ImageTransparency, GLOW_TRANSPARENCY, duration, Utility.EaseInOutQuad, true) + end + end + end) + end + + local function UpdateProfileImage(imageSource) + MyNameLabel.Text = XboxAppState.store:getState().XboxUser.gamertag + CrossfadeAvatarImage(ProfileImage, CrossfadeProfileImage, imageSource) + end + + local function onOwnershipChanged(tile, nowOwns) + -- Move purchased packages into my MyCollection + if nowOwns then + local guiObject = tile and tile:GetGuiObject() + if guiObject and AvatarScroller:ContainsItem(guiObject) then + AvatarScroller:RemoveItem(guiObject) + MyCollectionScroller:AddItem(guiObject) + OnAddToMyCollectionScroller() + if inFocus then + GuiService.SelectedCoreObject = guiObject + end + end + end + end + + local ownershipChangedCns = {} + + local function listenToOwnershipChanged(tile) + local packageInfo = tile and tile:GetPackageInfo() + if packageInfo and not packageInfo:IsOwned() and (packageInfo.OwnershipChanged ~= nil) then + if not ownershipChangedCns[tile] then + ownershipChangedCns[tile] = packageInfo.OwnershipChanged:connect(function(nowOwns) + onOwnershipChanged(tile, nowOwns) + end) + end + end + end + + local function removeListenToOwnershipChanged(tile) + if ownershipChangedCns[tile] then + ownershipChangedCns[tile]:disconnect() + ownershipChangedCns[tile] = nil + end + end + + local function onEquipChanged() + local selectedObject = GuiService.SelectedCoreObject + + if AvatarObjects[selectedObject] then + local packageInfo = AvatarObjects[selectedObject]:GetPackageInfo() + if packageInfo then + UpdateEquipButton(packageInfo:IsOwned(), packageInfo:IsWearing()) + end + else + -- user can select avatar tab button + UpdateEquipButton(false, false) + end + SortMyCollectionScroller() + end + + local lastSelectedObject = GuiService.SelectedCoreObject + local function OnSelectedCoreObjectChanged() + local selectedObject = GuiService.SelectedCoreObject + + if AvatarObjects[lastSelectedObject] then + AvatarObjects[lastSelectedObject]:RemoveFocus() + end + if AvatarObjects[selectedObject] then + AvatarObjects[selectedObject]:Focus() + lastSelectedObject = selectedObject + end + onEquipChanged() + end + + + function this:GetDefaultSelectableObject() + if MyCollectionScroller:GetFirstVisibleItem() then + return MyCollectionScroller:GetFirstVisibleItem() + end + if AvatarScroller:GetFirstVisibleItem() then + return AvatarScroller:GetFirstVisibleItem() + end + + --In case GetFirstVisibleItem breaks + if MyCollectionScroller.GridItems[1] then + return MyCollectionScroller.GridItems[1] + end + if AvatarScroller.GridItems[1] then + return AvatarScroller.GridItems[1] + end + end + + local packagesData = nil + local packagesDataConns = {} + local AvatarWebDataInitializeLoader = nil + local function removeListenToPackagesDataUpdate() + for _, conn in pairs(packagesDataConns) do + conn:disconnect() + end + packagesDataConns = {} + end + + local function listenToPackagesDataUpdate() + if packagesData and packagesDataConns then + table.insert(packagesDataConns, packagesData.OnDifferentWearing:connect(function(assetId) onEquipChanged() end)) + table.insert(packagesDataConns, packagesData.OnDifferentOwned:connect(function(assetId, owned) onEquipChanged() end)) + table.insert(packagesDataConns, packagesData.OnProfileImageUpdateEnd:connect(function(newProfileImage) + UpdateProfileImage(newProfileImage) + end)) + table.insert(packagesDataConns, packagesData.OnProfileImageUpdateBegin:connect(function() + UpdateProfileImage(function() + return packagesData.OnProfileImageUpdateEnd.wait() + end) + end)) + end + end + + local function LoadAvatarWebData() + local function clearCatalogPackages() + AvatarScroller:RemoveAllItems() + MyCollectionScroller:RemoveAllItems() + for k,_ in pairs(AvatarObjects) do + AvatarObjects[k] = nil + end + AvatarObjects = {} + end + + local function loadCatalogPackages(packages) + clearCatalogPackages() + if packages then + for _, packageInfo in pairs(packages) do + local avatarItemContainer = AvatarTile(packageInfo) + + AvatarObjects[avatarItemContainer:GetGuiObject()] = avatarItemContainer + + if packageInfo:IsOwned() then + MyCollectionScroller:AddItem(avatarItemContainer:GetGuiObject()) + OnAddToMyCollectionScroller() + else + AvatarScroller:AddItem(avatarItemContainer:GetGuiObject()) + listenToOwnershipChanged(avatarItemContainer) + end + + if isShown then + avatarItemContainer:Show() + end + end + end + end + + --Wait until initialize is done + if AvatarWebDataInitializeLoader then return end + + if PackageData:HasCachedData() then --Sync process to get and update packagesData, no spinner + local prevVersion = packagesData and packagesData.Version or nil + packagesData = PackageData:GetCachedData() + --If init or packagesData got updated + if not prevVersion or prevVersion ~= packagesData.Version then + SelectableAvatarsContainer.Visible = false + UpdateProfileImage(packagesData.ProfileImage.Data) + loadCatalogPackages(packagesData.Packages.Data) + SelectableAvatarsContainer.Visible = true + removeListenToPackagesDataUpdate() + listenToPackagesDataUpdate() + end + else --Async process to get and update packagesData, show spinner + SelectableAvatarsContainer.Visible = false + local containerSize = SelectableAvatarsContainer.Size + AvatarWebDataInitializeLoader = LoadingWidget( + {Parent = MainContainer, Position = SelectableAvatarsContainer.Position + UDim2.new(containerSize.X.Scale / 2, containerSize.X.Offset / 2, containerSize.Y.Scale / 2, containerSize.Y.Offset / 2)}, + {function() + packagesData = PackageData:GetCachedData() + if packagesData and packagesData.Packages then + loadCatalogPackages(packagesData.Packages.Data) + end + end} + ) + spawn(function() + NoCatalogStatusMessage.Visible = false + + UpdateProfileImage(function() + if AvatarWebDataInitializeLoader then + AvatarWebDataInitializeLoader:AwaitFinished() + end + if packagesData and packagesData.ProfileImage then + return packagesData.ProfileImage.Data + end + end) + + AvatarWebDataInitializeLoader:AwaitFinished() + AvatarWebDataInitializeLoader:Cleanup() + AvatarWebDataInitializeLoader = nil + SelectableAvatarsContainer.Visible = true + removeListenToPackagesDataUpdate() + + if not packagesData then + NoCatalogStatusMessage.Visible = true + end + + --Hook connections if the pane is still visible after loading + if isShown then + listenToPackagesDataUpdate() + end + + if inFocus and isShown and GuiService.SelectedCoreObject == nil then + GuiService.SelectedCoreObject = this:GetDefaultSelectableObject() + end + + if this.TransitionTweens == nil or #this.TransitionTweens == 0 then + this.TransitionTweens = ScreenManager:FadeInSitu(SelectableAvatarsContainer) + end + end) + end + end + + LoadAvatarWebData() + + --[[ Public API ]]-- + function this:GetName() + return Strings:LocalizedString('AvatarWord') + end + + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('AvatarPaneId')} + end + + function this:IsFocused() + return inFocus + end + + local debounceSelect = false + function this:OnSelectAction() + if debounceSelect then return end + debounceSelect = true + + local selectedObject = GuiService.SelectedCoreObject + if selectedObject and AvatarObjects[selectedObject] then + if AvatarObjects[selectedObject]:Select() then + SoundManager:Play('ButtonPress') + end + end + + debounceSelect = false + end + + function this:OpenEquippedPackage() + if isShown and inFocus then + for _, avatarItemContainer in pairs(AvatarObjects) do + local packageInfo = avatarItemContainer:GetPackageInfo() + if packageInfo then + if packageInfo:IsWearing() then + avatarItemContainer:OnClick() + end + end + end + end + end + --[[ End Public API ]]-- + + local profileImageChangeCn = nil + function this:Show() + isShown = true + + Utility.DisconnectEvent(profileImageChangeCn) + profileImageChangeCn = ProfileImageContainer:GetPropertyChangedSignal('AbsoluteSize'):connect(function() + onSizeChanged() + end) + onSizeChanged() + + local lastUpdate = tick() + RunService:BindToRenderStep("UpdateAvatarGlow", Enum.RenderPriority.Camera.Value, + function() + local now = tick() + local delta = now - lastUpdate + + CharacterGlowBase.Rotation = CharacterGlowBase.Rotation + delta * GLOW_BASE_RPM * 6 -- 6 = 360 / 60 + CharacterGlowTop.Rotation = CharacterGlowTop.Rotation + delta * GLOW_TOP_RPM * 6 + + lastUpdate = now + end) + + if packagesData then + listenToPackagesDataUpdate() + UpdateProfileImage(packagesData.ProfileImage.Data) + end + LoadAvatarWebData() + + for _, avatarItemContainer in pairs(AvatarObjects) do + avatarItemContainer:Show() + listenToOwnershipChanged(avatarItemContainer) + local packageInfo = avatarItemContainer and avatarItemContainer:GetPackageInfo() + if packageInfo then + onOwnershipChanged(avatarItemContainer, packageInfo:IsOwned()) + end + end + + local seenXButtonPressed = false + local seenXSelectedObject = nil + local function onSelectAvatar(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + seenXButtonPressed = true + seenXSelectedObject = GuiService.SelectedCoreObject + elseif inputState == Enum.UserInputState.End and seenXButtonPressed then + if seenXSelectedObject and seenXSelectedObject == GuiService.SelectedCoreObject then + self:OnSelectAction() + end + seenXButtonPressed = false + seenXSelectedObject = nil + end + end + + hintActionView:BindAction(onSelectAvatar, Enum.KeyCode.ButtonX) + + self.TransitionTweens = ScreenManager:DefaultFadeIn(MainContainer) + + SortMyCollectionScroller() + + MainContainer.Parent = lastParent + MainContainer.Visible = true + ScreenManager:PlayDefaultOpenSound() + end + + + function this:Hide() + isShown = false + MainContainer.Visible = false + removeListenToPackagesDataUpdate() + + profileImageChangeCn = Utility.DisconnectEvent(profileImageChangeCn) + + RunService:UnbindFromRenderStep("UpdateAvatarGlow") + hintActionView:UnbindAction() + hintActionView:SetTransparency(1) + + for _, avatarItemContainer in pairs(AvatarObjects) do + avatarItemContainer:Hide() + removeListenToOwnershipChanged(avatarItemContainer) + end + + ScreenManager:DefaultCancelFade(self.TransitionTweens) + self.TransitionTweens = nil + -- Clean out saved selected object so when we tab back + -- we will start with the default selection + lastSelectedObject = nil + self.SavedSelectObject = nil + end + + function this:Focus() + inFocus = true + + Utility.DisconnectEvent(OnGuiServiceChangedConn) + OnGuiServiceChangedConn = GuiService:GetPropertyChangedSignal('SelectedCoreObject'):connect(function() + OnSelectedCoreObjectChanged() + end) + + if self.SavedSelectObject and self.SavedSelectObject:IsDescendantOf(MainContainer) then + GuiService.SelectedCoreObject = self.SavedSelectObject + else + local defaultSelection = self:GetDefaultSelectableObject() + if defaultSelection then + GuiService.SelectedCoreObject = defaultSelection + end + end + end + + function this:RemoveFocus() + inFocus = false + --Remove Focus on AvatarObjects + if AvatarObjects and AvatarObjects[lastSelectedObject] then + AvatarObjects[lastSelectedObject]:RemoveFocus() + end + OnGuiServiceChangedConn = Utility.DisconnectEvent(OnGuiServiceChangedConn) + + local selectedObject = GuiService.SelectedCoreObject + if isShown then + if selectedObject and selectedObject:IsDescendantOf(MainContainer) then + self.SavedSelectObject = GuiService.SelectedCoreObject + GuiService.SelectedCoreObject = nil + else + self.SavedSelectObject = lastSelectedObject + end + end + OnSelectedCoreObjectChanged() + end + + function this:SetPosition(newPosition) + MainContainer.Position = newPosition + end + + function this:SetParent(newParent) + lastParent = newParent + MainContainer.Parent = newParent + end + + return this +end + +return CreateAvatarPane diff --git a/Client2018/content/internal/AppShell/Modules/Shell/AvatarTile.lua b/Client2018/content/internal/AppShell/Modules/Shell/AvatarTile.lua new file mode 100644 index 0000000..e0d90bd --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/AvatarTile.lua @@ -0,0 +1,254 @@ +--[[ +// AvatarTile.lua + +// Created by Kip Turner +// Copyright Roblox 2015 +]] + +local TextService = game:GetService('TextService') +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Http = require(ShellModules:FindFirstChild('Http')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) + +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local Errors = require(ShellModules:FindFirstChild('Errors')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local ErrorOverlayModule = require(ShellModules:FindFirstChild('ErrorOverlay')) +local PurchasePackagePrompt = require(ShellModules:FindFirstChild('PurchasePackagePrompt')) + +local BaseTile = require(ShellModules:FindFirstChild('BaseTile')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) + +local function createAvatarInfoContainer(packageInfo) + local this = BaseTile() + local focused = false + + local packageName = packageInfo:GetFullName() + + local function wearPackageAsync() + if packageInfo:IsOwned() and not packageInfo:IsWearing() then + + local result = packageInfo:WearAsync() + + if result and result['success'] == true then + this:UpdateEquipButton() + else + local err = Errors.PackageEquip['Default'] + ScreenManager:OpenScreen(ErrorOverlayModule(err), false) + end + + end + end + + local function buyPackageAsync() + local newPurchasePrompt = PurchasePackagePrompt(packageInfo) + ScreenManager:OpenScreen(newPurchasePrompt, false) + newPurchasePrompt:FadeInBackground() + spawn(function() + local didPurchase = newPurchasePrompt:ResultAsync() + if didPurchase then + SoundManager:Play('PurchaseSuccess') + wearPackageAsync() + end + end) + end + + local PriceText = Utility.Create'TextLabel' + { + Name = 'PriceText'; + Text = ''; + Size = UDim2.new(1, 0, 0, 36); + Position = UDim2.new(0, 0, 0, 0); + TextColor3 = GlobalSettings.BlackTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.DescriptionSize; + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.PriceLabelColor; + ZIndex = 2; + Visible = false; + Parent = this.AvatarItemContainer; + }; + + local PromoText = Utility.Create'TextLabel' + { + Name = 'PromoText'; + Text = ''; + Size = UDim2.new(1, 0, 0, 36); + Position = UDim2.new(0, 0, 0, 0); + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.DescriptionSize; + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.PromoLabelColor; + ZIndex = 2; + Visible = false; + Parent = this.AvatarItemContainer; + }; + + function this:UpdatePriceText() + local newText = "" + local price = packageInfo:GetRobuxPrice() + if price == 0 then + newText = Strings:LocalizedString('FreeWord') + elseif price then + newText = "R$ " .. Utility.FormatNumberString(price) + end + + PriceText.Text = newText + local priceTextSize = TextService:GetTextSize(PriceText.Text, Utility.ConvertFontSizeEnumToInt(PriceText.FontSize), PriceText.Font, Vector2.new()) + PriceText.Size = UDim2.new(0, priceTextSize.X + 28, 0, 36) + PriceText.AnchorPoint = Vector2.new(1, 0) + PriceText.Position = UDim2.new(1, -6, 0, 6) + PriceText.Visible = price ~= nil and not packageInfo:IsOwned() + end + + function this:UpdatePromoText() + if not packageInfo:IsOwned() then + local saleIdStr = Utility.GetFastVariable("XboxSaleAvatarPackageIds") + if type(saleIdStr) == "string" and saleIdStr ~= '' then + for saleId in string.gmatch(saleIdStr, '([^;]+)') do + if tonumber(saleId) ~= nil and tonumber(saleId) == tonumber(packageInfo:GetAssetId()) then --If the package is on sale + PromoText.Text = "SALE!" + local promoTextSize = TextService:GetTextSize(PromoText.Text, Utility.ConvertFontSizeEnumToInt(PromoText.FontSize), PromoText.Font, Vector2.new()) + PromoText.Size = UDim2.new(0, promoTextSize.X + 28, 0, 36) + PromoText.AnchorPoint = Vector2.new(1, 0) + PromoText.Position = UDim2.new(1, -6, 0, 42) + PromoText.Visible = true + break + end + end + end + else + PromoText.Visible = false + end + end + + if packageInfo:GetAssetId() then + this:SetImage(Http.GetThumbnailUrlForAsset(packageInfo:GetAssetId())) + else + --TODO: show a no package image? + end + + + this:ColorizeImage(packageInfo:IsOwned() and 1 or 0, 0) + this:SetPopupText(packageInfo:GetName()) + + function this:GetAssetId() + return packageInfo:GetAssetId() + end + + function this:GetPackageInfo() + return packageInfo + end + + function this:UpdateOwnership() + local ownsAsset = packageInfo:IsOwned() + self:SetActive(ownsAsset) + self:ColorizeImage(ownsAsset and 1 or 0, 0) + self:UpdateEquipButton() + self:UpdatePriceText() + --Added for promo label + self:UpdatePromoText() + end + + function this:UpdateEquipButton() + self.EquippedCheckmark.Visible = packageInfo:IsWearing() + end + + local selectDebounce = false + function this:Select() + if selectDebounce then return false end + local result = false + if packageInfo:IsOwned() and not packageInfo:IsWearing() then + selectDebounce = true + spawn(function() + wearPackageAsync() + selectDebounce = false + end) + result = true + end + return result + end + + function this:OnClick() + buyPackageAsync() + end + + local isWearingConn = nil + local ownershipChangedCn = nil + local baseShow = this.Show + function this:Show() + baseShow(self) + Utility.DisconnectEvent(isWearingConn) + packageInfo.IsWearingChanged:connect(function()self:UpdateEquipButton() end) + Utility.DisconnectEvent(ownershipChangedCn) + ownershipChangedCn = packageInfo.OwnershipChanged:connect(function() + self:UpdateOwnership() + end) + self:UpdateEquipButton() + + self:UpdateOwnership() + end + + local baseHide = this.Hide + function this:Hide() + baseHide(self) + isWearingConn = Utility.DisconnectEvent(isWearingConn) + ownershipChangedCn = Utility.DisconnectEvent(ownershipChangedCn) + end + + local baseFocus = this.Focus + local avatarItemClickConn = nil + + function this:GetAnalyticsInfo() + return + { + [Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('AvatarTileId'); + AssetId = packageInfo:GetAssetId(); + IsOwned = packageInfo:IsOwned(); + } + end + + function this:Focus() + baseFocus(self) + focused = true + + Utility.DisconnectEvent(avatarItemClickConn) + avatarItemClickConn = self.AvatarItemContainer.MouseButton1Click:connect(function() + self:OnClick() + end) + + self:UpdateEquipButton() + + spawn(function() + wait(0.17) + if focused then + if not packageInfo:IsOwned() then + self:ColorizeImage(1) + end + end + end) + end + + local baseRemoveFocus = this.RemoveFocus + function this:RemoveFocus() + baseRemoveFocus(self) + focused = false + avatarItemClickConn = Utility.DisconnectEvent(avatarItemClickConn) + + -- Decolorize unowned packages + if not packageInfo:IsOwned() then + self:ColorizeImage(0) + end + end + + return this +end + +return createAvatarInfoContainer diff --git a/Client2018/content/internal/AppShell/Modules/Shell/BackgroundSceneManager.lua b/Client2018/content/internal/AppShell/Modules/Shell/BackgroundSceneManager.lua new file mode 100644 index 0000000..60a7d2c --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/BackgroundSceneManager.lua @@ -0,0 +1,70 @@ +-- Starts and manages the background 3D scene +local RobloxGui = game:GetService('CoreGui').RobloxGui +local ShellModules = script.Parent + +local ContentProvider = game:GetService('ContentProvider') +local ThirdPartyUserService = nil +pcall(function() ThirdPartyUserService = game:GetService('ThirdPartyUserService') end) + +local CameraManager = require(ShellModules.CameraManager) +local EventHub = require(ShellModules.EventHub) +local SoundManager = require(ShellModules.SoundManager) +local Utility = require(ShellModules.Utility) + +local SceneManager = {} + +-- set up the image before 3D background is displayed +local BackgroundImage = Utility.Create'ImageLabel' +{ + Name = 'BackgroundImage'; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 0; + BackgroundColor3 = Color3.new(29/255, 47/255, 61/255); + BorderSizePixel = 0; + Image = 'rbxasset://textures/ui/Shell/Background/Home_screen_01.png'; + Parent = RobloxGui; +} + +-- We may want to move this to rodux flow later +EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "authSuccessCameraControl", function(isNewLinkedAccount) + CameraManager:DisableCameraControl() +end); + +if ThirdPartyUserService then + ThirdPartyUserService.ActiveUserSignedOut:connect(function() + CameraManager:EnableCameraControl() + end) +end + +-- start up background music +local backgroundSound = SoundManager:Play('BackgroundLoop', 0.33, true) +if backgroundSound then + local bgmLoopConn = nil + bgmLoopConn = backgroundSound.DidLoop:connect(function(soundId, loopCount) + if loopCount > 0 then + bgmLoopConn = Utility.DisconnectEvent(bgmLoopConn) + if backgroundSound then + SoundManager:TweenSound(backgroundSound, 0.1, 3) + end + end + end) +end + +-- Check for 3D Background being loaded, then start it up +local CROSSFADE_DURATION = 1.5 +spawn(function() + while ContentProvider.RequestQueueSize > 0 do + wait(0.01) + end + + CameraManager:EnableCameraControl() + spawn(function() + CameraManager:CameraMoveToAsync() + end) + Utility.PropertyTweener(BackgroundImage, 'BackgroundTransparency', 0, 1, CROSSFADE_DURATION, Utility.EaseInOutQuad, true) + Utility.PropertyTweener(BackgroundImage, 'ImageTransparency', 0, 1, CROSSFADE_DURATION, Utility.EaseInOutQuad, true, function() + BackgroundImage:Destroy() + end) +end) + +return SceneManager diff --git a/Client2018/content/internal/AppShell/Modules/Shell/BadgeOverlay.lua b/Client2018/content/internal/AppShell/Modules/Shell/BadgeOverlay.lua new file mode 100644 index 0000000..01d6212 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/BadgeOverlay.lua @@ -0,0 +1,135 @@ +--[[ + // BadgeOverlay.lua + + // Displays information for a single badge + // Used by GameDetail and BadgeScreen +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local ScrollingTextBox = require(ShellModules:FindFirstChild('ScrollingTextBox')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local BaseOverlay = require(ShellModules:FindFirstChild('BaseOverlay')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local createBadgeOverlay = function(badgeData) + local this = BaseOverlay() + + local hasBadge = badgeData["IsOwned"] + this:SetImageBackgroundTransparency(0) + this:SetImageBackgroundColor(hasBadge and GlobalSettings.BadgeOwnedColor or GlobalSettings.BadgeOverlayColor) + + local badgeImage = Utility.Create'ImageLabel' + { + Name = "BadgeImage"; + Size = UDim2.new(0, 394, 0, 394); + BackgroundTransparency = 1; + Image = 'http://www.roblox.com/Thumbs/Asset.ashx?width='.. + tostring(250)..'&height='..tostring(250)..'&assetId='..tostring(badgeData.AssetId); + } + badgeImage.Position = UDim2.new(0.5, -197, 0.5, -197) + this:SetImage(badgeImage) + + local titleText = Utility.Create'TextLabel' + { + Name = "TitleText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, this.RightAlign, 0, 88); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.HeaderSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = badgeData.Name; + TextXAlignment = Enum.TextXAlignment.Left; + Parent = this.Container; + } + + --[[ Has Badge ]]-- + local hasBadgeContainer = nil + if hasBadge then + hasBadgeContainer = Utility.Create'Frame' + { + Name = "HasBadgeContainer"; + Position = UDim2.new(0, titleText.Position.X.Offset, 0, titleText.Position.Y.Offset + 34); + BackgroundTransparency = 1; + Parent = this.Container; + } + local hasBadgeImage = Utility.Create'ImageLabel' + { + Name = "HasBadgeImage"; + BackgroundTransparency = 1; + Parent = hasBadgeContainer; + Image = "rbxasset://textures/ui/Shell/Icons/Checkmark@1080.png"; + Size = UDim2.new(0,35,0,35); + } + hasBadgeContainer.Size = UDim2.new(0, 200, 0, hasBadgeImage.Size.Y.Offset) + local hasBadgeText = Utility.Create'TextLabel' + { + Name = "HasBadgeText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, hasBadgeImage.Size.X.Offset + 12, 0.5, 0); + BackgroundTransparency = 1; + Font = GlobalSettings.ItalicFont; + FontSize = GlobalSettings.DescriptionSize; + TextColor3 = GlobalSettings.GreenTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + Text = Strings:LocalizedString("HaveBadgeWord"); + Parent = hasBadgeContainer; + } + end + + --[[ Description ]]-- + local descriptionYOffset = hasBadgeContainer and hasBadgeContainer.Position.Y.Offset + hasBadgeContainer.Size.Y.Offset + 10 or + titleText.Position.Y.Offset + 40 + + local descriptionScrollingTextBox = ScrollingTextBox(UDim2.new(0, 762, 0, 304), + UDim2.new(0, titleText.Position.X.Offset, 0, descriptionYOffset), + this.Container) + descriptionScrollingTextBox:SetText(badgeData.Description) + descriptionScrollingTextBox:SetFontSize(GlobalSettings.TitleSize) + local descriptionFrame = descriptionScrollingTextBox:GetContainer() + + local okButton = Utility.Create'TextButton' + { + Name = "OkButton"; + Size = UDim2.new(0, 320, 0, 66); + Position = UDim2.new(0, titleText.Position.X.Offset, 1, -66 - 55); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.BlueButtonColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = GlobalSettings.TextSelectedColor; + Text = Strings:LocalizedString("OkWord"); + Parent = this.Container; + + SoundManager:CreateSound('MoveSelection'); + } + Utility.ResizeButtonWithText(okButton, okButton, GlobalSettings.TextHorizontalPadding) + + --[[ Input Events ]]-- + okButton.MouseButton1Click:connect(function() + this:Close() + end) + local baseFocus = this.Focus + function this:Focus() + baseFocus(this) + Utility.SetSelectedCoreObject(okButton) + end + + function this:GetAnalyticsInfo() + return + { + [Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('BadgeOverlayId'); + AssetId = badgeData.AssetId; + } + end + + return this +end + +return createBadgeOverlay diff --git a/Client2018/content/internal/AppShell/Modules/Shell/BadgeScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/BadgeScreen.lua new file mode 100644 index 0000000..2e92dd2 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/BadgeScreen.lua @@ -0,0 +1,124 @@ +--[[ + // BadgeScreen.lua + + // Displays a 2xN grid of badges for a game +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local AssetManager = require(ShellModules:FindFirstChild('AssetManager')) +local BadgeOverlayModule = require(ShellModules:FindFirstChild('BadgeOverlay')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local ScrollingGrid = require(ShellModules:FindFirstChild('ScrollingGrid')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local PopupText = require(ShellModules:FindFirstChild('PopupText')) +local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) + +local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen')) + +local createBadgeScreen = function(badgeData) + local this = BaseScreen() + + local ROWS = 2 + -- columns is dynamic + + local BadgeContainer = this.Container + this:SetTitle(Strings:LocalizedString("GameBadgesTitle")) + + local defaultSelection = nil + + -- create grid + local BadgeScrollGrid = ScrollingGrid() + BadgeScrollGrid:SetPosition(UDim2.new(0, 0, 0.5 - (0.57 / 2), 0)) + BadgeScrollGrid:SetSize(UDim2.new(1, 0, 0, 570)) + BadgeScrollGrid:SetScrollDirection(BadgeScrollGrid.Enum.ScrollDirection.Horizontal) + BadgeScrollGrid:SetParent(BadgeContainer) + BadgeScrollGrid:SetClipping(false) + BadgeScrollGrid:SetCellSize(Vector2.new(276, 276)) + BadgeScrollGrid:SetSpacing(Vector2.new(18, 18)) + + local checkmarkImage = Utility.Create'ImageLabel' + { + Name = "CheckMarkImage"; + BackgroundTransparency = 1; + + Image = "rbxasset://textures/ui/Shell/Icons/Checkmark@1080.png"; + Size = UDim2.new(0,35,0,35); + + ZIndex = 2; + } + + local function connectImageInput(image, data) + image.MouseButton1Click:connect(function() + -- Do not play sound because we are opening a screen here + ScreenManager:OpenScreen(BadgeOverlayModule(data), false) + end) + end + + local baseItem = Utility.Create'TextButton' + { + Name = "BadgeImage"; + BorderSizePixel = 0; + BackgroundTransparency = 0.2; + BackgroundColor3 = Color3.new(64/255, 81/255, 93/255); + Text = ""; + ZIndex = 2; + + SoundManager:CreateSound('MoveSelection'); + AssetManager.CreateShadow(1); + } + local badgeIcon = Utility.Create'ImageLabel' + { + Name = "Thumb"; + Size = UDim2.new(0, 228, 0, 228); + Position = UDim2.new(0.5, -228/2, 0.5, -228/2); + BackgroundTransparency = 1; + Image = ""; + ZIndex = 2; + Parent = baseItem; + } + + for i = 1, #badgeData do + local data = badgeData[i] + local item = baseItem:Clone() + local thumb = item:FindFirstChild("Thumb") + if thumb then + local thumbLoader = ThumbnailLoader:Create(thumb, data.AssetId, + ThumbnailLoader.Sizes.Medium, ThumbnailLoader.AssetType.Icon) + spawn(function() + thumbLoader:LoadAsync() + end) + end + local hasBadge = data["IsOwned"] + if hasBadge then + item.BackgroundColor3 = GlobalSettings.BadgeOwnedColor; + item.BackgroundTransparency = 0; + -- + local check = checkmarkImage:Clone() + check.Position = UDim2.new(1, -check.Size.X.Offset - 8, 0, 8) + check.Parent = item + end + item.Name = tostring(i) + BadgeScrollGrid:AddItem(item) + connectImageInput(item, data) + PopupText(item, data["Name"]) + if not defaultSelection then + defaultSelection = item + end + end + + --[[ Public API ]]-- + --Override + function this:GetDefaultSelectionObject() + return defaultSelection + end + + return this +end + +return createBadgeScreen diff --git a/Client2018/content/internal/AppShell/Modules/Shell/BadgeSort.lua b/Client2018/content/internal/AppShell/Modules/Shell/BadgeSort.lua new file mode 100644 index 0000000..a437d00 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/BadgeSort.lua @@ -0,0 +1,185 @@ +--[[ + // BadgeSort.lua + // Creates a badge sort for a game + + // Handles the following for badges + // Displays 2x2 of badges on game details page + // Displays individual information about each badge (overlay) +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local AssetManager = require(ShellModules:FindFirstChild('AssetManager')) +local BadgeOverlayModule = require(ShellModules:FindFirstChild('BadgeOverlay')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local PopupText = require(ShellModules:FindFirstChild('PopupText')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader')) + +local CreateBadgeSort = function(placeName, size, position, parent) + local this = {} + + local badgeData = nil + local margin = 14 + local imageSize = (size.Y.Offset - margin) / 2 + + local gridImages = {} + + local GRID_SIZE = 4 + + --[[ Game Details Grid ]]-- + local container = Utility.Create'Frame' + { + Name = "ImageContainer"; + Size = size; + Position = position; + BackgroundTransparency = 1; + Parent = parent; + } + -- create 2x2 preview grid + local index = 1 + for i = 1, GRID_SIZE/2 do + for j = 1, GRID_SIZE/2 do + local image = Utility.Create'TextButton' + { + Name = tostring(index); + Size = UDim2.new(0, imageSize, 0, imageSize); + Position = UDim2.new(0, (i - 1) * imageSize + (i - 1) * margin, 0, (j - 1) * imageSize + (j - 1) * margin); + BackgroundTransparency = GlobalSettings.FriendStatusTextTransparency; + BackgroundColor3 = GlobalSettings.BadgeFrameColor; + BorderSizePixel = 0; + ZIndex = 2; + Text = ""; + ClipsDescendants = true; + Parent = container; + + SoundManager:CreateSound('MoveSelection'); + AssetManager.CreateShadow(1); + } + local thumb = Utility.Create'ImageLabel' + { + Name = "Thumb"; + Size = UDim2.new(0, 228, 0, 228); + Position = UDim2.new(0.5, -228/2, 0.5, -228/2); + BackgroundTransparency = 1; + Image = ""; + ZIndex = 2; + Parent = image; + } + gridImages[index] = image + index = index + 1 + end + end + + -- more button visible when #badges > 4 + local moreBadgesButton = Utility.Create'ImageButton' + { + Name = "MoreBadgesButton"; + BackgroundTransparency = 1; + Visible = false; + Parent = container; + Image = "rbxasset://textures/ui/Shell/Buttons/MoreButton@1080.png"; + Size = UDim2.new(0,108,0,50); + ZIndex = 2; + + SoundManager:CreateSound('MoveSelection'); + } + moreBadgesButton.Position = UDim2.new(1, - moreBadgesButton.AbsoluteSize.x, 1, 12) + + local function updateMoreButton(isSelected) + moreBadgesButton.Image = isSelected and 'rbxasset://textures/ui/Shell/Buttons/MoreButtonSelected@1080.png' + or 'rbxasset://textures/ui/Shell/Buttons/MoreButton@1080.png' + end + + moreBadgesButton.SelectionGained:connect(function() + updateMoreButton(true) + end) + moreBadgesButton.SelectionLost:connect(function() + updateMoreButton(false) + end) + + local checkmarkImage = Utility.Create'ImageLabel' + { + Name = "CheckMarkImage"; + BackgroundTransparency = 1; + + Image = "rbxasset://textures/ui/Shell/Icons/Checkmark@1080.png"; + Size = UDim2.new(0,35,0,35); + + ZIndex = 2; + } + + local function setBadgeData() + if not badgeData then + Utility.DebugLog("BadgeSort: failed to set badge data because data is nil.") + return + end + -- + for i = 1, #gridImages do + if badgeData[i] then + local data = badgeData[i] + local thumb = gridImages[i]:FindFirstChild("Thumb") + if thumb then + local thumbLoader = ThumbnailLoader:Create(thumb, data.AssetId, + ThumbnailLoader.Sizes.Medium, ThumbnailLoader.AssetType.Icon) + spawn(function() + thumbLoader:LoadAsync() + end) + end + local hasBadge = data["IsOwned"] + if hasBadge then + gridImages[i].BackgroundColor3 = GlobalSettings.BadgeOwnedColor + gridImages[i].BackgroundTransparency = 0 + -- + local check = checkmarkImage:Clone() + check.Position = UDim2.new(1, -check.Size.X.Offset - 8, 0, 8) + check.Parent = gridImages[i] + end + -- + gridImages[i].MouseButton1Click:connect(function() + ScreenManager:OpenScreen(BadgeOverlayModule(data), false) + end) + -- connect popup text + PopupText(gridImages[i], data["Name"]) + end + end + end + + --[[ Input Events ]]-- + moreBadgesButton.MouseButton1Click:connect(function() + EventHub:dispatchEvent(EventHub.Notifications["OpenBadgeScreen"], badgeData, placeName) + end) + + --[[ Public API ]]-- + function this:GetContainer() + return container + end + + function this:GetDefaultSelection() + return gridImages and gridImages[1] or nil + end + + function this:Initialize(data) + if not badgeData then + badgeData = data + setBadgeData() + if #data > GRID_SIZE then + moreBadgesButton.Visible = true + end + end + end + + function this:Destroy() + container:Destroy() + gridImages = nil + end + + return this +end + +return CreateBadgeSort diff --git a/Client2018/content/internal/AppShell/Modules/Shell/BaseCarouselScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/BaseCarouselScreen.lua new file mode 100644 index 0000000..dc8ad1e --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/BaseCarouselScreen.lua @@ -0,0 +1,454 @@ +--[[ + // BaseCarouselScreen.lua + + // Creates a base screen for a carousel view + // To be used for game genre and search screens + + // Creates a Play and Favorite Button, details view (votes, description), and + // a carousel view +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) + +local AssetManager = require(ShellModules:FindFirstChild('AssetManager')) +local Errors = require(ShellModules:FindFirstChild('Errors')) +local ErrorOverlayModule = require(ShellModules:FindFirstChild('ErrorOverlay')) +local GameData = require(ShellModules:FindFirstChild('GameData')) +local GameJoinModule = require(ShellModules:FindFirstChild('GameJoin')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen')) +local CarouselView = require(ShellModules:FindFirstChild('CarouselView')) +local CarouselController = require(ShellModules:FindFirstChild('CarouselController')) +local VoteFrame = require(ShellModules:FindFirstChild('VoteFrame')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local function CreateBaseCarouselScreen() + local this = BaseScreen() + + local newGameSelectedCn = nil + local dataModelViewChangedCn = nil + + local canJoinGame = true + local returnedFromGame = true + + -- we need to move ZIndex up because of drop shadows + local BASE_ZINDEX = 2 + + local playButtonColor = GlobalSettings.GreenButtonColor + local playButtonSelectedColor = GlobalSettings.GreenSelectedButtonColor + local favoriteButtonColor = GlobalSettings.GreyButtonColor + local favoriteSelectedButtonColor = GlobalSettings.GreySelectedButtonColor + local buttonTextColor = GlobalSettings.WhiteTextColor + local buttonSelectedTextColor = GlobalSettings.TextSelectedColor + + local viewContainer = Utility.Create'Frame' + { + Name = "ViewContainer"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Parent = this.Container; + } + this.ViewContainer = viewContainer + + local myCarouselView = CarouselView() + myCarouselView:SetSize(UDim2.new(0, 1724, 0, 450)) + myCarouselView:SetPosition(UDim2.new(0, 0, 0, 240)) + myCarouselView:SetPadding(18) + myCarouselView:SetItemSizePercentOfContainer(2/3) + myCarouselView:SetParent(viewContainer) + + local myCarouselController = CarouselController(myCarouselView) + + local playButton = Utility.Create'ImageButton' + { + Name = "PlayButton"; + Size = UDim2.new(0, 228, 0, 72); + Position = UDim2.new(0, 0, 1, -77); + BackgroundTransparency = 1; + ImageColor3 = playButtonColor; + Image = GlobalSettings.RoundCornerButtonImage; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28)); + ZIndex = BASE_ZINDEX; + Parent = viewContainer; + + SoundManager:CreateSound('MoveSelection'); + AssetManager.CreateShadow(1); + } + local playText = Utility.Create'TextLabel' + { + Name = "PlayText"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Text = Strings:LocalizedString("PlayWord"); + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = buttonTextColor; + ZIndex = BASE_ZINDEX; + Parent = playButton; + } + Utility.ResizeButtonWithText(playButton, playText, GlobalSettings.TextHorizontalPadding) + + local favoriteButton = Utility.Create'ImageButton' + { + Name = "FavoriteButton"; + Position = UDim2.new(0, playButton.Size.X.Offset + 10, 1, -77); + Size = UDim2.new(0, 228, 0, 72); + BackgroundTransparency = 1; + ImageColor3 = favoriteButtonColor; + Image = GlobalSettings.RoundCornerButtonImage; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28)); + ZIndex = BASE_ZINDEX; + Parent = viewContainer; + + SoundManager:CreateSound('MoveSelection'); + AssetManager.CreateShadow(1); + } + local favoriteText = Utility.Create'TextLabel' + { + Name = "FavoriteText"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Text = Strings:LocalizedString("FavoriteWord"); + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = buttonTextColor; + ZIndex = 2; + Parent = favoriteButton; + } + + local favoriteStarImage = Utility.Create'ImageLabel' + { + Name = "FavoriteStarImage"; + Size = UDim2.new(0, 32, 0, 31); + Position = UDim2.new(0, 16, 0.5, -31/2); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/FavoriteStar@1080.png'; + Visible = false; + ZIndex = BASE_ZINDEX; + Parent = favoriteButton; + } + + --Make it big enough to hold the star and text + Utility.ResizeButtonWithDynamicText(favoriteButton, favoriteText, {Strings:LocalizedString("FavoritedWord")}, + GlobalSettings.TextHorizontalPadding + (favoriteStarImage.Position.X.Offset + favoriteStarImage.Size.X.Offset + 12) / 2) + + -- begin game details content + local gameDetailsContainer = Utility.Create'Frame' + { + Name = "GameDetailsContainer"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, 18, 0, 732); + BackgroundTransparency = 1; + Parent = viewContainer; + } + local gameTitle = Utility.Create'TextLabel' + { + Name = "GameTitleLabel"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + Text = ""; + TextColor3 = GlobalSettings.WhiteTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.HeaderSize; + Parent = gameDetailsContainer; + } + local voteFrame = VoteFrame(gameDetailsContainer, UDim2.new(0, 38, 0, 46)) + local voteFrameContainer = voteFrame:GetContainer() + + local thumbsUpImage = Utility.Create'ImageLabel' + { + Name = "ThumbsUpImage"; + Size = UDim2.new(0, 28, 0, 28); + Position = UDim2.new(0, 0, 0, voteFrameContainer.Position.Y.Offset + voteFrameContainer.Size.Y.Offset - 28); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/ThumbsUpIcon@1080.png'; + Parent = gameDetailsContainer; + } + local thumbsDownImage = Utility.Create'ImageLabel' + { + Name = "ThumbsDownImage"; + Size = UDim2.new(0, 28, 0, 28); + Position = UDim2.new(0, voteFrameContainer.Position.X.Offset + voteFrameContainer.Size.X.Offset + 10, 0, voteFrameContainer.Position.Y.Offset); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/ThumbsDownIcon@1080.png'; + Parent = gameDetailsContainer; + } + local separatorDot = Utility.Create'ImageLabel' + { + Name = "SeparatorDot"; + Size = UDim2.new(0, 10, 0, 10); + Position = UDim2.new(0, thumbsDownImage.Position.X.Offset + thumbsDownImage.Size.X.Offset + 32, 0, voteFrameContainer.Position.Y.Offset + (voteFrameContainer.Size.Y.Offset/2) - (10/2)); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/SeparatorDot@1080.png'; + Parent = gameDetailsContainer; + } + local creatorIcon = Utility.Create'ImageLabel' + { + Name = "CreatorIcon"; + Size = UDim2.new(0, 24, 0, 24); + Position = UDim2.new(0, separatorDot.Position.X.Offset + separatorDot.Size.X.Offset + 32, 0, separatorDot.Position.Y.Offset + separatorDot.Size.Y.Offset/2 - 12); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/RobloxIcon24.png'; + Parent = gameDetailsContainer; + } + local creatorName = Utility.Create'TextLabel' + { + Name = "CreatorName"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, creatorIcon.Position.X.Offset + creatorIcon.Size.X.Offset + 8, 0, separatorDot.Position.Y.Offset + separatorDot.Size.Y.Offset/2 - 2); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.DescriptionSize; + TextColor3 = GlobalSettings.LightGreyTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + Text = ""; + Parent = gameDetailsContainer; + } + local descriptionText = Utility.Create'TextLabel' + { + Name = "DescriptionText"; + Size = UDim2.new(0, 850, 0, 64); + Position = UDim2.new(0, gameTitle.Position.X.Offset, 0, voteFrameContainer.Position.Y.Offset + voteFrameContainer.Size.Y.Offset + 20); + BackgroundTransparency = 1; + Text = ""; + TextColor3 = GlobalSettings.LightGreyTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + Font = GlobalSettings.LightFont; + TextWrapped = true; + FontSize = GlobalSettings.DescriptionSize; + Parent = gameDetailsContainer; + } + + local noResultsText = Utility.Create'TextLabel' + { + Name = "noResultsText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0.5, 0, 0.5, 0); + BackgroundTransparency = 1; + Text = Strings:LocalizedString("NoGamesPhrase"); + TextColor3 = GlobalSettings.LightGreyTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.MediumFontSize; + Visible = false; + Parent = this.Container; + } + + -- Selection overrides + playButton.NextSelectionLeft = playButton + favoriteButton.NextSelectionRight = favoriteButton + + playButton.SelectionGained:connect(function() + playButton.ImageColor3 = playButtonSelectedColor + playText.TextColor3 = buttonSelectedTextColor + end) + playButton.SelectionLost:connect(function() + playButton.ImageColor3 = playButtonColor + playText.TextColor3 = buttonTextColor + end) + favoriteButton.SelectionGained:connect(function() + favoriteButton.ImageColor3 = favoriteSelectedButtonColor + favoriteText.TextColor3 = buttonSelectedTextColor + end) + favoriteButton.SelectionLost:connect(function() + favoriteButton.ImageColor3 = favoriteButtonColor + favoriteText.TextColor3 = buttonTextColor + end) + + local function setIsFavorited(isFavorited) + if isFavorited == true then + favoriteStarImage.Visible = true + favoriteText.Position = UDim2.new(0, favoriteStarImage.Position.X.Offset + favoriteStarImage.Size.X.Offset + 12, 0, 0) + favoriteText.Text = Strings:LocalizedString("FavoritedWord") + favoriteText.TextXAlignment = Enum.TextXAlignment.Left + else + favoriteStarImage.Visible = false + favoriteText.Position = UDim2.new(0, 0, 0, 0) + favoriteText.Text = Strings:LocalizedString("FavoriteWord") + favoriteText.TextXAlignment = Enum.TextXAlignment.Center + end + end + local function setVoteView(voteData) + local upVotes = voteData and voteData.UpVotes or 0 + local downVotes = voteData and voteData.DownVotes or 0 + if upVotes == 0 and downVotes == 0 then + voteFrame:SetPercentFilled(nil) + else + voteFrame:SetPercentFilled(upVotes / (upVotes + downVotes)) + end + end + + local GameSelectedConns = {} + local onNewGameSelected = nil + + local function ClearGameSelectedView() + --Disconnect Events + Utility.DisconnectEvents(GameSelectedConns) + GameSelectedConns = {} + gameTitle.Text = "" + creatorName.Text = "" + descriptionText.Text = "" + setVoteView() + setIsFavorited() + end + + onNewGameSelected = function(placeId) + Utility.DisconnectEvents(GameSelectedConns) + GameSelectedConns = {} + + local data = GameData:GetGameData(placeId) + if data then + --Use signals to make sure that these fetched data corresponds to the game we focus on + table.insert(GameSelectedConns, data.OnGetVoteDataEnd: + connect(function(voteData) setVoteView(voteData) + end)) + + table.insert(GameSelectedConns, data.OnGetGameDetailsEnd: + connect(function(gameData) + descriptionText.Text = gameData.Description or "" + setIsFavorited(gameData.IsFavorited) + end)) + gameTitle.Text = data.Name + creatorName.Text = data.CreatorName + descriptionText.Text = data.Description or "" + setVoteView(data.VoteData) + setIsFavorited(data.IsFavorited) + spawn(function() + if not data.VoteData then + data:GetVoteDataAsync() + end + + if not data.Description or data.IsFavorited == nil then + data:GetGameDetailsAsync() + end + end) + else + ClearGameSelectedView() + end + end + + playButton.MouseButton1Click:connect(function() + SoundManager:Play('ButtonPress') + local placeId = myCarouselController:GetCurrentFocusGameData() + local data = GameData:GetGameData(placeId) + if data then + if canJoinGame and returnedFromGame then + canJoinGame = false + --Should never happen, as all game in carousel inits with CreatorUserId + if not data.CreatorUserId then + data:GetGameDetailsAsync() + end + GameJoinModule:StartGame(GameJoinModule.JoinType.Normal, data.PlaceId, data.CreatorUserId) + canJoinGame = true + end + end + end) + + favoriteButton.MouseButton1Click:connect(function() + SoundManager:Play('ButtonPress') + local placeId = myCarouselController:GetCurrentFocusGameData() + local data = GameData:GetGameData(placeId) + if data then + local success, reason = data:PostFavoriteAsync() + if success then + setIsFavorited(data.IsFavorited) + elseif reason then + local err = Errors.Favorite[reason] + ScreenManager:OpenScreen(ErrorOverlayModule(err), false) + end + end + end) + + function this:LoadGameCollection(gameCollection) + viewContainer.Visible = false + noResultsText.Visible = false + myCarouselView:SetParent(nil) + + spawn(function() + local loader = LoadingWidget( + { Parent = this.Container }, + { + function() + myCarouselController:InitializeAsync(gameCollection) + end + } + ) + + loader:AwaitFinished() + loader:Cleanup() + loader = nil + + myCarouselView:SetParent(viewContainer) + if this:IsFocused() then + if myCarouselController:HasResults() then + viewContainer.Visible = true + Utility.SetSelectedCoreObject(myCarouselView:GetFocusItem()) + else + noResultsText.Visible = true + end + end + end) + end + + -- Override Base Functions + function this:GetDefaultSelectionObject() + return myCarouselView:GetFocusItem() + end + + function this:GetAnalyticsInfo() + return + { + [Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('BaseCarouselScreenId'); + Title = this:GetTitle(); + } + end + + local baseFocus = this.Focus + function this:Focus() + baseFocus(self) + myCarouselView:Focus() + + if PlatformService then + dataModelViewChangedCn = PlatformService.ViewChanged:connect(function(viewType) + -- return from game debounce + if viewType == 0 then + returnedFromGame = false + wait(1) + returnedFromGame = true + end + end) + end + + newGameSelectedCn = myCarouselController.NewItemSelected:connect(onNewGameSelected) + onNewGameSelected(myCarouselController:GetCurrentFocusGameData()) + myCarouselController:Connect() + end + + local baseRemoveFocus = this.RemoveFocus + function this:RemoveFocus() + baseRemoveFocus(self) + dataModelViewChangedCn = Utility.DisconnectEvent(dataModelViewChangedCn) + newGameSelectedCn = Utility.DisconnectEvent(newGameSelectedCn) + myCarouselController:Disconnect() + Utility.DisconnectEvents(GameSelectedConns) + GameSelectedConns = {} + end + + return this +end + +return CreateBaseCarouselScreen diff --git a/Client2018/content/internal/AppShell/Modules/Shell/BaseOverlay.lua b/Client2018/content/internal/AppShell/Modules/Shell/BaseOverlay.lua new file mode 100644 index 0000000..3896885 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/BaseOverlay.lua @@ -0,0 +1,160 @@ +--[[ + // BaseOverlay.lua + + // Implements a base overlay for overlay screens. + // Any other overlay classes should require this module + // first, then implement its own logic +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local GuiService = game:GetService('GuiService') +local ContextActionService = game:GetService("ContextActionService") + +local AssetManager = require(ShellModules:FindFirstChild('AssetManager')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local FADE_TIME = 0.25 + +local MOCKUP_WIDTH = 1920 +local MOCKUP_HEIGHT = 1080 +local CONTENT_WIDTH = 1920 +local CONTENT_HEIGHT = 690 + +local CONTENT_POSITION = Vector2.new(0, 225) + +local createBaseOverlay = function() + local this = {} + + local OVERLAY_TRANSPARENCY = GlobalSettings.ModalBackgroundTransparency + + this.RightAlign = 776 + + local modalOverlay = Utility.Create'Frame' + { + Name = "ModalOverlay"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + BackgroundColor3 = Color3.new(); + BorderSizePixel = 0; + } + local container = Utility.Create'Frame' + { + Name = "Container"; + Size = UDim2.new(CONTENT_WIDTH/MOCKUP_WIDTH, 0, CONTENT_HEIGHT/MOCKUP_HEIGHT, 0); + Position = UDim2.new(CONTENT_POSITION.x/MOCKUP_WIDTH, 0, CONTENT_POSITION.y/MOCKUP_HEIGHT, 0); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.OverlayColor; + Parent = modalOverlay; + } + local imageContainer = Utility.Create'Frame' + { + Name = "ImageContainer"; + Size = UDim2.new(0, 576, 0, 642); + Position = UDim2.new(0, 100, 0.5, -321); + BorderSizePixel = 0; + BackgroundTransparency = 1; + BackgroundColor3 = Color3.new(); + ZIndex = 2; + Parent = container; + } + local imageDropShadow = AssetManager.CreateShadow(1) + imageDropShadow.ImageTransparency = 1 + imageDropShadow.Parent = imageContainer + + this.Container = container + + --[[ Public API ]]-- + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('BaseOverlayId')} + end + + function this:SetImageBackgroundTransparency(value) + imageContainer.BackgroundTransparency = value + -- if the image has any transparency we don't show drop shadow since it will clip + if value > 0 then + imageDropShadow.ImageTransparency = 1 + else + imageDropShadow.ImageTransparency = 0 + end + end + + function this:SetImageBackgroundColor(value) + imageContainer.BackgroundColor3 = value + end + + function this:SetImage(guiImage) + guiImage.AnchorPoint = Vector2.new(0.5, 0.5); + guiImage.Position = UDim2.new(0.5, 0, 0.5, 0); + guiImage.ZIndex = imageContainer.ZIndex + guiImage.Parent = imageContainer + end + + function this:GetOverlaySound() + return 'OverlayOpen' + end + + function this:GetPriority() + return GlobalSettings.OverlayPriority + end + + function this:Show() + modalOverlay.Parent = ScreenManager:GetScreenGuiByPriority(self:GetPriority()) + local overlayTweenIn = Utility.PropertyTweener(modalOverlay, "BackgroundTransparency", + 1, OVERLAY_TRANSPARENCY, FADE_TIME, Utility.EaseInOutQuad, nil) + SoundManager:Play(self:GetOverlaySound()) + + -- Show the modalOverlay when we are shown + modalOverlay.Visible = true + end + + function this:Hide() + local overlayTweenOut = Utility.PropertyTweener(modalOverlay, "BackgroundTransparency", + OVERLAY_TRANSPARENCY, 1, FADE_TIME, Utility.EaseInOutQuad, true, + function() + modalOverlay:Destroy() + end) + container.Parent = nil + container:Destroy() + end + + function this:Focus() + ContextActionService:BindCoreAction("CloseOverlay", + function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + ScreenManager:CloseCurrent() + end + end, + false, Enum.KeyCode.ButtonB) + GuiService:AddSelectionParent("Overlay", container) + + -- Don't show overlays when not focused + modalOverlay.Visible = true + end + + function this:RemoveFocus() + ContextActionService:UnbindCoreAction("CloseOverlay") + GuiService:RemoveSelectionGroup("Overlay") + + -- Don't show overlays when not focused + modalOverlay.Visible = false + end + + function this:Close() + if ScreenManager:GetTopScreen() == self then + SoundManager:Play('ButtonPress') + ScreenManager:CloseCurrent() + return true + end + return false + end + + return this +end + +return createBaseOverlay diff --git a/Client2018/content/internal/AppShell/Modules/Shell/BaseScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/BaseScreen.lua new file mode 100644 index 0000000..dc79402 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/BaseScreen.lua @@ -0,0 +1,187 @@ +--[[ + // BaseScreen.lua + + // Creates a base screen with breadcrumbs and title. Do not use for a pane/tab +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local ContextActionService = game:GetService("ContextActionService") +local GuiService = game:GetService('GuiService') + +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local function createBaseScreen(usingMVC) + local this = {} + + local inFocus = false + local isShown = false + local defaultSelectionObject = nil + local lastParent = nil + + local BackText = "" + + this.view = nil + this.fixPosition = false + + function this:GetBackText() + return BackText + end + + local function GetView() + if not this.view then + local Templates = ShellModules:FindFirstChild("Templates") + local BaseScreen = require(Templates:FindFirstChild('BaseScreen')) + this.view = BaseScreen(this) + end + return this.view + end + + if not usingMVC then + this.Container = GetView().Container + end + + local function CloseScreen() + if inFocus then + ScreenManager:CloseCurrent() + end + end + + --[[ Public API ]]-- + + function this:GetView() + return GetView() + end + function this:GetTitle() + return GetView().TitleText.Text + end + function this:SetTitle(newTitle) + GetView().TitleText.Text = newTitle + end + function this:SetTitleZIndex(newZIndex) + -- this needs some explantion... + -- we recently changed the way z ordering works. In the old system if you had a text label and a image at the + -- same zindex the text would render on top of the image. This is no longer the case. So in some cases + -- we need to render this text last, but as it's a base class, it becomes hard to reorder the child add order. + -- Instead I'm opting to allow changing the zindex + GetView().TitleText.ZIndex = newZIndex + end + function this:SetBackText(newText) + BackText = newText + GetView():SetBackText(BackText) + end + function this:GetDefaultSelectionObject() + return defaultSelectionObject + end + function this:Destroy() + GetView().Container:Destroy() + end + + --[[ Public API - Screen Management ]]-- + function this:SetPosition(newPosition) + GetView().Container.Position = newPosition + end + function this:SetParent(newParent) + lastParent = newParent + end + function this:GetName() + return GetView().TitleText.Text + end + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = GetView().TitleText.Text} + end + function this:IsFocused() + return inFocus + end + + -- View stuff -- + + function this:ReloadView(newView) + local currentView = this.view + local currentViewIsShown = isShown + local currentViewIsFocused = inFocus + + if currentView then + if currentViewIsFocused then + currentView:RemoveFocus() + end + if currentViewIsShown then + currentView:Hide() + end + end + if newView then + if currentViewIsShown then + newView:Show() + end + if currentViewIsFocused then + newView:Focus() + end + end + this.view = newView + end + + function this:OnBackButtonClick() + CloseScreen() + end + + ---------------- + + function this:Show() + isShown = true + local prevScreen = ScreenManager:GetScreenBelow(self) + if prevScreen and prevScreen.GetName then + self:SetBackText(prevScreen:GetName()) + else + self:SetBackText(Strings:LocalizedString("BackWord")) + end + + GetView().Container.Parent = lastParent + ScreenManager:DefaultCancelFade(self.TransitionTweens) + if self.fixPosition then + self.TransitionTweens = ScreenManager:FadeInSitu(GetView().Container) + else + self.TransitionTweens = ScreenManager:DefaultFadeIn(GetView().Container) + end + ScreenManager:PlayDefaultOpenSound() + end + function this:Hide() + isShown = false + GetView().Container.Parent = nil + ScreenManager:DefaultCancelFade(self.TransitionTweens) + self.TransitionTweens = nil + end + function this:Focus() + inFocus = true + if self.SavedSelectedObject and self.SavedSelectedObject:IsDescendantOf(GetView().Container) then + Utility.SetSelectedCoreObject(self.SavedSelectedObject) + else + Utility.SetSelectedCoreObject(self:GetDefaultSelectionObject()) + end + + ContextActionService:BindCoreAction("ReturnFromScreen", + function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + CloseScreen() + end + end, + false, Enum.KeyCode.ButtonB) + end + function this:RemoveFocus() + inFocus = false + local selectedObject = GuiService.SelectedCoreObject + if selectedObject and selectedObject:IsDescendantOf(GetView().Container) then + self.SavedSelectedObject = selectedObject + Utility.SetSelectedCoreObject(nil) + end + ContextActionService:UnbindCoreAction("ReturnFromScreen") + end + + return this +end + +return createBaseScreen diff --git a/Client2018/content/internal/AppShell/Modules/Shell/BaseSignInScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/BaseSignInScreen.lua new file mode 100644 index 0000000..76c80ef --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/BaseSignInScreen.lua @@ -0,0 +1,211 @@ +--[[ + // BaseSignInScreen.lua + + // Creates a base screen to be used for account linking and sign in +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local AssetManager = require(ShellModules:FindFirstChild('AssetManager')) +local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local TextBox = require(ShellModules:FindFirstChild('TextBox')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local ScrollingTextBox = require(ShellModules:FindFirstChild('ScrollingTextBox')) + + +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) + +local TERMS_OF_SERVICE_URI = "https://en.help.roblox.com/hc/en-us/articles/205358110" + +local function createBaseAccountScreen() + local this = BaseScreen() + + local DefaultButtonColor = GlobalSettings.GreyButtonColor + local SelectedButtonColor = GlobalSettings.GreySelectedButtonColor + local DefaultButtonTextColor = GlobalSettings.WhiteTextColor + local SelectedButtonTextColor = GlobalSettings.TextSelectedColor + + local ScreenDivide = Utility.Create'Frame' + { + Name = "ScreenDivide"; + Size = UDim2.new(0, 2, 0, 610); + Position = UDim2.new(0, 822, 0.5, -305); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.PageDivideColor; + Parent = this.Container; + } + + local TermsOfServiceText = Utility.Create'TextLabel' + { + Name = "TermsOfServiceText"; + Size = UDim2.new(0, 740, 0, 128); + Position = UDim2.new(0, 16, 0, 835); + BackgroundTransparency = 1; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + TextWrapped = true; + Text = Strings:LocalizedString("ToSInfoLinkPhrase"); + Parent = this.Container; + } + + local UsernameObject = TextBox(UDim2.new(0, 656, 0, 84)) + UsernameObject:SetPosition(UDim2.new(0, ScreenDivide.Position.X.Offset + ScreenDivide.Size.X.Offset + 86, 0, 334)) + UsernameObject:SetSpacing(Vector2.new(20, 0)) + UsernameObject:SetParent(this.Container) + UsernameObject:SetClipsDescendants(true) + local UsernameTextBox = UsernameObject:GetTextBox() + local UsernameSelection = UsernameObject:GetContainer() + this.UsernameObject = UsernameObject + this.UsernameTextBox = UsernameTextBox + this.UsernameSelection = UsernameSelection + + local PasswordObject = TextBox(UDim2.new(0, 656, 0, 84)) + PasswordObject:SetPosition(UDim2.new(0, ScreenDivide.Position.X.Offset + ScreenDivide.Size.X.Offset + 86, 0, 484)) + PasswordObject:SetSpacing(Vector2.new(20, 0)) + PasswordObject:SetParent(this.Container) + PasswordObject:SetClipsDescendants(true) + local PasswordTextBox = PasswordObject:GetTextBox() + local PasswordSelection = PasswordObject:GetContainer() + this.PasswordObject = PasswordObject + this.PasswordTextBox = PasswordTextBox + this.PasswordSelection = PasswordSelection + + local function CreateSlicedTextButton(name, text, position) + name = name or "" + + local newButton = Utility.Create'ImageButton' + { + Name = name .. "Button"; + Size = UDim2.new(0, 376, 0, 64); + Position = position or UDim2.new(); + BackgroundTransparency = 1; + ImageColor3 = DefaultButtonColor; + Image = GlobalSettings.RoundCornerButtonImage; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28)); + ZIndex = 2; + Parent = this.Container; + + SoundManager:CreateSound('MoveSelection'); + AssetManager.CreateShadow(1) + } + local newText = Utility.Create'TextLabel' + { + Name = name .. "Text"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = DefaultButtonTextColor; + Text = text; + ZIndex = 2; + Parent = newButton; + } + + newButton.SelectionGained:connect(function() + newButton.ImageColor3 = SelectedButtonColor + newText.TextColor3 = SelectedButtonTextColor + end) + newButton.SelectionLost:connect(function() + newButton.ImageColor3 = DefaultButtonColor + newText.TextColor3 = DefaultButtonTextColor + end) + + return newButton, newText + end + + local function TryLaunchUri(uri) + local success, msg = pcall(function() + assert(not UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows, "Can't use in studio") + PlatformService:LaunchPlatformUri(uri) + end) + if not success then + Utility.DebugLog(string.format("PlatformService:LaunchPlatformUri failed to launch uri: %s, for reason: %s", uri, msg)) + end + end + + local SignInButton, SignInText = CreateSlicedTextButton("SignIn", + Strings:LocalizedString("SignInPhrase"), + UDim2.new(0, PasswordSelection.Position.X.Offset, 0, + PasswordSelection.Position.Y.Offset + PasswordSelection.Size.Y.Offset + 66)) + Utility.ResizeButtonWithText(SignInButton, SignInText, GlobalSettings.TextHorizontalPadding) + this.SignInButton = SignInButton + + local tosButtonY = SignInButton.Position.Y.Offset -- + SignInButton.Size.Y.Offset + 30 + local ToSButton, ToSText = CreateSlicedTextButton("ToS", + Strings:LocalizedString("ToSPhrase"), + UDim2.new(0, SignInButton.Position.X.Offset + SignInButton.Size.X.Offset + 10, + 0, tosButtonY)) + ToSButton.Size = UDim2.new(0, 270, 0, ToSButton.Size.Y.Offset) + Utility.ResizeButtonWithText(ToSButton, ToSText, GlobalSettings.TextHorizontalPadding) + + local tosButtonLastPress = tick() - 1 + ToSButton.MouseButton1Click:connect(function() + if tick() - tosButtonLastPress < 1 then return end + tosButtonLastPress = tick() + TryLaunchUri(TERMS_OF_SERVICE_URI) + end) +--[[ + local PrivacyButton, PrivacyText = CreateSlicedTextButton("Privacy", + Strings:LocalizedString("PrivacyPhrase"), + UDim2.new(0, ToSButton.Position.X.Offset + ToSButton.Size.X.Offset + 5, + 0, tosButtonY)) + PrivacyButton.MouseButton1Click:connect(function() + TryLaunchUri("http://www.roblox.com/info/privacy") + end) +--]] + + + -- Override selection - issue with selections remembering their last selection, so in some cases + -- the password selection become unselectable. I've talk to Ben about this and we're going to fix it + -- TODO: Remove this when selection memory is fixed + UsernameSelection.NextSelectionDown = PasswordSelection + SignInButton.NextSelectionUp = PasswordSelection + + + local DescriptionText = ScrollingTextBox(UDim2.new(0, 740, 0, 460), UDim2.new(0, 16, 0, 334), this.Container) + DescriptionText:SetFont(GlobalSettings.RegularFont) + DescriptionText:SetFontSize(GlobalSettings.TitleSize) + + DescriptionText.OnSelectableChanged:connect(function(value) + local descriptionSelectionObject = DescriptionText:GetSelectableObject() + if value == true then + descriptionSelectionObject.NextSelectionRight = SignInButton + descriptionSelectionObject.NextSelectionLeft = descriptionSelectionObject + descriptionSelectionObject.NextSelectionUp = descriptionSelectionObject + descriptionSelectionObject.NextSelectionDown = descriptionSelectionObject + else + descriptionSelectionObject.NextSelectionRight = nil + descriptionSelectionObject.NextSelectionLeft = nil + descriptionSelectionObject.NextSelectionUp = nil + descriptionSelectionObject.NextSelectionDown = nil + end + end) + + --[[ Public API ]]-- + --Override + function this:GetDefaultSelectionObject() + return UsernameSelection + end + + function this:SetDescriptionText(newText) + DescriptionText:SetText(newText) + end + + function this:SetButtonText(newText) + SignInText.Text = newText + end + + return this +end + +return createBaseAccountScreen diff --git a/Client2018/content/internal/AppShell/Modules/Shell/BaseTile.lua b/Client2018/content/internal/AppShell/Modules/Shell/BaseTile.lua new file mode 100644 index 0000000..984ac1a --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/BaseTile.lua @@ -0,0 +1,136 @@ +--[[ + // BaseTile.lua + + // Created by Kip Turner + // Copyright Roblox 2015 +]] + + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + + +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local AssetManager = require(ShellModules:FindFirstChild('AssetManager')) +local PopupText = require(ShellModules:FindFirstChild('PopupText')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local ACTIVE_AVATAR_BACKGROUND_COLOR = Color3.new(45/255, 96/255, 128/255) +local INACTIVE_AVATAR_BACKGROUND_COLOR = Color3.new(39/255, 69/255, 82/255) --Color3.new(106/255, 120/255, 129/255) + +local function createBaseTileContainer() + local this = {} + this.focused = false + this.active = false + + + local avatarItemContainer = Utility.Create'ImageButton' + { + Name = 'AvatarItemContainer'; + Size = UDim2.new(0,220,0,220); + BorderSizePixel = 0; + BackgroundTransparency = 0; + BackgroundColor3 = this.active and ACTIVE_AVATAR_BACKGROUND_COLOR or INACTIVE_AVATAR_BACKGROUND_COLOR; + AutoButtonColor = false; + ClipsDescendants = true; + ZIndex = 2; + AssetManager.CreateShadow(1); + + SoundManager:CreateSound('MoveSelection'); + } + + local myPopText = PopupText(avatarItemContainer, '') + myPopText:SetZIndex(3) + + + local avatarImage = Utility.Create'ImageLabel' + { + Name = "AvatarImage"; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + ZIndex = 2; + Parent = avatarItemContainer; + } + + local equippedCheckmark = Utility.Create'ImageLabel' + { + Name = "EquippedCheckmark"; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + ZIndex = 3; + Visible = false; + Image = 'rbxasset://textures/ui/Shell/Icons/EquippedOverlay.png'; + Parent = avatarItemContainer; + } + + this.AvatarItemContainer = avatarItemContainer + this.AvatarImage = avatarImage + this.EquippedCheckmark = equippedCheckmark + + local function colorizeImage(newColor, duration) + duration = duration or 0.2 + Utility.PropertyTweener(avatarImage, 'ImageColor3', avatarImage.ImageColor3.r, newColor, duration, + function(...) local scalar = Utility.EaseOutQuad(...) return Color3.new(scalar, scalar, scalar) end, true) + end + + function this:UpdateEquipButton() + end + + function this:ColorizeImage(...) + colorizeImage(...) + end + + function this:SetPopupText(newText) + myPopText:SetText(newText) + end + + function this:SetImage(imgUrl) + avatarImage.Image = imgUrl + end + + function this:GetGuiObject() + return avatarItemContainer + end + + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('BaseTileId')} + end + + function this:GetPackageInfo() + end + + function this:OnClick() + end + + function this:SetActive(isActive) + self.active = isActive + avatarItemContainer.BackgroundColor3 = self.active and ACTIVE_AVATAR_BACKGROUND_COLOR or INACTIVE_AVATAR_BACKGROUND_COLOR; + end + + function this:Select() + end + + function this:Show() + end + + function this:Hide() + end + + function this:Focus() + self.focused = true + end + + function this:RemoveFocus() + self.focused = false + end + + return this +end + +return createBaseTileContainer diff --git a/Client2018/content/internal/AppShell/Modules/Shell/CachedData.lua b/Client2018/content/internal/AppShell/Modules/Shell/CachedData.lua new file mode 100644 index 0000000..569fa3e --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/CachedData.lua @@ -0,0 +1,48 @@ +local function CreateCacheData(data, version, getRefreshIntervalFunc, refreshFunc) + local this = {} + this.Data = data + this.Version = version + this.GetRefreshIntervalFunc = getRefreshIntervalFunc + this.RefreshFunc = refreshFunc + local refreshDebounce = false + + --Use new data to update, usually we do this at intervals in BG + function this:Update(newCached) + if newCached and newCached.Version then + if self.Version <= newCached.Version then + self.Version = newCached.Version + self.Data = newCached.Data + end + end + end + + --Refresh the data, usually we will call the Refresh when some data is required + function this:Refresh(forceRefresh) + if self.RefreshFunc and type(self.RefreshFunc) == 'function' then + while refreshDebounce do + wait() + end + + --Get RefreshInterval + local RefreshInterval = nil + if self.GetRefreshIntervalFunc and type(self.GetRefreshIntervalFunc) == 'function' then + RefreshInterval = tonumber(self.GetRefreshIntervalFunc()) + end + + if forceRefresh or not self.Version or not RefreshInterval or (tick() - self.Version >= RefreshInterval) then + refreshDebounce = true + --RefreshFunc will update data, and return whether the refresh was valid + if self.RefreshFunc(self.Data) then + self.Version = tick() + else --this refresh fails, we needs to reset Version to ensure next RefreshFunc call + self.Version = nil + end + refreshDebounce = false + end + end + end + return this +end + + +return CreateCacheData \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/CameraManager.lua b/Client2018/content/internal/AppShell/Modules/Shell/CameraManager.lua new file mode 100644 index 0000000..d6585fb --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/CameraManager.lua @@ -0,0 +1,404 @@ +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local Utility = require(ShellModules:FindFirstChild('Utility')) +local RunService = game:GetService("RunService") +local UserInputService = game:GetService('UserInputService') +local Lighting = game:GetService('Lighting') + +local BackgroundTintColor = Color3.new(0.0784, 0.1686, 0.2353) + +local ColorCorrection = Utility.Create'ColorCorrectionEffect' +{ + Brightness = 0.3; + Contrast = 0.5; + Saturation = -1; + TintColor = BackgroundTintColor; + Enabled = true; + Parent = Lighting; +} + +local Blur = Utility.Create'BlurEffect' +{ + Size = 10; + Enabled = true; + Parent = Lighting; +} + +local Bloom = Utility.Create'BloomEffect' +{ + Intensity = 0.05; + Size = 24; + Threshold = 0.95; + Enabled = true; + Parent = Lighting; +} + +local function GetCameraParts(model) + local parts = {} + for i, part in pairs(model:GetChildren()) do + parts[tonumber(part.Name:sub(4, -1))] = part + part.Transparency = 1 + end + return parts +end + +local myCameraSets = nil + +local function GetCameraSets() + if myCameraSets == nil then + myCameraSets = {} + for k, f in pairs(workspace:WaitForChild("Cameras"):GetChildren()) do + myCameraSets[f.Name] = GetCameraParts(f) + end + end + return myCameraSets +end + + +local function onCameraChanged() + if workspace.CurrentCamera then + workspace.CurrentCamera.CameraType = 'Scriptable' + end +end + +workspace:GetPropertyChangedSignal('CurrentCamera'):connect(function() + onCameraChanged() +end) +onCameraChanged() + + +local cameraMoveCn = nil + +local gamepadInput = Vector2.new(0, 0) + + + +local CameraManager = {} + +function CameraManager:EnableCameraControl() + cameraMoveCn = Utility.DisconnectEvent(cameraMoveCn) + cameraMoveCn = UserInputService.InputChanged:connect(function(input) + if input.KeyCode == Enum.KeyCode.Thumbstick2 then + gamepadInput = input.Position or gamepadInput + gamepadInput = Vector2.new(gamepadInput.X, gamepadInput.Y) + end + end) +end + +function CameraManager:DisableCameraControl() + cameraMoveCn = Utility.DisconnectEvent(cameraMoveCn) + gamepadInput = Vector2.new(0, 0) +end + + +local getGamepadInputCFrame; +do + local gamepadInputLerping = Vector2.new(0, 0) + local timestamp0 = tick() + function getGamepadInputCFrame() + local timestamp1 = tick() + local deltaTime = timestamp1 - timestamp0 + timestamp0 = timestamp1 + local unit = 0.125 ^ deltaTime + gamepadInputLerping = gamepadInputLerping * unit + gamepadInput * (1 - unit) + return CFrame.new( + gamepadInputLerping.X/8, gamepadInputLerping.Y/8, 0) * CFrame.Angles(0, -gamepadInputLerping.X / 12, 0) * CFrame.Angles(gamepadInputLerping.Y / 12, 0, 0) + end +end + + +local defaultCFrame = CFrame.new(); + +local function defaultGetFrameInfo() + return { + CFrame = defaultCFrame; + Contrast = 0.5; + Saturation = -1; + TintColor = BackgroundTintColor; + BlurSize = 10; + } +end + +local getFrameInfo = defaultGetFrameInfo + + +local function startEternalRenderStep() + RunService:BindToRenderStep("CameraScriptCutsceneLerp", Enum.RenderPriority.Camera.Value, + function() + local info = getFrameInfo() + workspace.CurrentCamera.CoordinateFrame = info.CFrame + ColorCorrection.Contrast = info.Contrast + ColorCorrection.Saturation = info.Saturation + ColorCorrection.TintColor = info.TintColor + Blur.Size = info.BlurSize + end + ) +end + + +local function transitionToCameraAnimator(cameraAnimator, transitionDuration, targetBrightness, targetBlurEnabled, targetBloomEnabled) + Utility.PropertyTweener(ColorCorrection, "Brightness", ColorCorrection.Brightness, + -1, transitionDuration, Utility.EaseInOutQuad, true, + function() + getFrameInfo = cameraAnimator:get_getFrameInfo() + Blur.Enabled = targetBlurEnabled + Bloom.Enabled = targetBloomEnabled + Utility.PropertyTweener(ColorCorrection, "Brightness", ColorCorrection.Brightness, + targetBrightness, transitionDuration, Utility.EaseInOutQuad, true) + end) +end + + +local function CFrameBezierLerp(cframes, t) + local cframes2 = {} + for i = 1, #cframes - 1 do + cframes2[i] = cframes[i]:lerp(cframes[i + 1], t) + end + if #cframes2 == 1 then + return cframes2[1] + end + return CFrameBezierLerp(cframes2, t) +end + + +local function getCFrameList(cameras) + local cframes = {} + for i = 1, #cameras do + cframes[i] = cameras[i].CFrame + end + return cframes +end + + +local function CameraSeriesAnimator(cameraSeries, length) + local timestamp0 = 0 + local cameraIndex = 0 + local cframes = nil + local isRunning = false + local tweenTable = {} + + local animationInfo = { + Contrast = -1; + BlurSize = 50; + } + + local function myGetFrameInfo() + local t = Utility.Clamp(0, 1, (tick() - timestamp0) / length) + + return { + CFrame = CFrameBezierLerp(cframes, t) * getGamepadInputCFrame(); + Contrast = animationInfo.Contrast; + Saturation = -1; + TintColor = BackgroundTintColor; + BlurSize = animationInfo.BlurSize; + } + end + + local function advance() + if not isRunning then + return + end + cameraIndex = cameraIndex + 1 + if cameraIndex > #cameraSeries then + cameraIndex = 1 + end + cameraSeries[cameraIndex].init() + cframes = getCFrameList(cameraSeries[cameraIndex].cameras) + timestamp0 = tick() + end + + runCameraSeries = function(transitionDuration) + local timePassed = tick() - timestamp0 + for i = #tweenTable, 1, -1 do + if tweenTable[i]:IsFinished() then + table.remove(tweenTable, i) + end + end + + if timePassed >= length then + local newTween = Utility.PropertyTweener(animationInfo, "Contrast", animationInfo.Contrast, 0.5, transitionDuration, Utility.EaseInOutQuad, true) + table.insert(tweenTable, newTween) + newTween = Utility.PropertyTweener(animationInfo, "BlurSize", animationInfo.BlurSize, 10, transitionDuration, Utility.EaseInOutQuad, true) + table.insert(tweenTable, newTween) + advance() + elseif timePassed >= length - transitionDuration and #tweenTable == 0 then + local newTween = Utility.PropertyTweener(animationInfo, "Contrast", animationInfo.Contrast, -1, transitionDuration, Utility.EaseInOutQuad, true) + table.insert(tweenTable, newTween) + newTween = Utility.PropertyTweener(animationInfo, "BlurSize", animationInfo.BlurSize, 50, transitionDuration, Utility.EaseInOutQuad, true) + table.insert(tweenTable, newTween) + end + end + + local function startCameraSeries() + local tween = Utility.PropertyTweener(animationInfo, "Contrast", animationInfo.Contrast, 0.5, 4, Utility.EaseInOutQuad, true) + table.insert(tweenTable, tween) + tween = Utility.PropertyTweener(animationInfo, "BlurSize", animationInfo.BlurSize, 10, 4, Utility.EaseInOutQuad, true) + table.insert(tweenTable, tween) + advance() + end + + local this = {} + + function this:Start() + isRunning = true + startCameraSeries() + RunService:BindToRenderStep("runCameraSeries", Enum.RenderPriority.Camera.Value + 1, function() + runCameraSeries(1.7) + end) + end + + function this:Stop() + isRunning = false + for i = #tweenTable, 1, -1 do + local tween = table.remove(tweenTable, i) + tween:Finish() + end + animationInfo = { + Contrast = 0.5; + BlurSize = 10; + } + RunService:UnbindFromRenderStep("runCameraSeries") + end + + function this:get_getFrameInfo() + return myGetFrameInfo + end + + return this +end + + +local function CameraZoomAnimator(cframes, length, pathFunc) + local timestamp0 = 0 + + local this = {} + + function this:Reset() + timestamp0 = tick() + end + + function this:Update(inCframes, length) + cframes = inCframes + timestamp0 = tick() + end + + local function myGetFrameInfo() + local t = Utility.Clamp(0, 1, (tick() - timestamp0) / length) + + return { + CFrame = CFrameBezierLerp(cframes, pathFunc(t)); + Contrast = 0.35; + Saturation = 0.175; + TintColor = Color3.fromRGB(255, 255, 255); + BlurSize = 1; + } + end + + function this:get_getFrameInfo() + return myGetFrameInfo + end + + return this +end + + +pcall(function() + local function recurse(model) + local children = model:GetChildren() + for i = 1, #children do + local child = children[i] + if child:IsA("BasePart") then + child.Locked = true + child.Anchored = true + child.CanCollide = false + end + recurse(child) + end + end + recurse(workspace) +end) + + +local ZoneManager = require(script.Parent:WaitForChild("CameraManagerModules"):WaitForChild("ZoneManager")) + +local avatarEditorCameraAnimator = nil +local flythroughAnimator = nil + +local avatarEditorZoomCFrames = { + CFrame.new( + 10.2426682, 5.1197648, -30.9536419, + -0.946675897, 0.123298854, -0.297661126, + 0.0000000, 0.92387563, 0.382692933, + 0.322187454, 0.36228618, -0.874610782), + + CFrame.new( + 12.50625, 4.83650, -24.764325, + -0.94241035, 0.0557777137, -0.329775006, + 0.0000000, 0.98599577, 0.166770056, + 0.334458828, 0.157165825, -0.92921263) +} + +function CameraManager:CameraMoveToAsync() + local cameraSets = GetCameraSets() + + startEternalRenderStep(); + + flythroughAnimator = CameraSeriesAnimator({ + { + init = function() ZoneManager:SetZone("City") end; + cameras = cameraSets["City"] + }, + { + init = function() ZoneManager:SetZone("Space") end; + cameras = cameraSets["Space"] + }, + { + init = function() ZoneManager:SetZone("Volcano") end; + cameras = cameraSets["Volcano"] + }, + }, 60) + + avatarEditorCameraAnimator = CameraZoomAnimator(avatarEditorZoomCFrames, 1.0, function(t) + t = Utility.Clamp(0, 1, t) + return t * (2 - t) + end) + + flythroughAnimator:Start() + transitionToCameraAnimator(flythroughAnimator, 0.25, 0.3, true, false) +end + + +function CameraManager:SwitchToFlyThrough() + transitionToCameraAnimator(flythroughAnimator, 0.25, 0.3, true, false) + flythroughAnimator:Start() +end + + +function CameraManager:SwitchToAvatarEditor() + flythroughAnimator:Stop() + ZoneManager:SetZone("AvatarEditor") + avatarEditorCameraAnimator:Reset() + + transitionToCameraAnimator(avatarEditorCameraAnimator, 0.25, 0.02, false, true) +end + +local targetCFrame = avatarEditorZoomCFrames[2] + +function CameraManager:UpdateAvatarEditorCamera(newCFrame) + targetCFrame = newCFrame + local frameInfo = avatarEditorCameraAnimator:get_getFrameInfo()() + + avatarEditorCameraAnimator:Update( + { + frameInfo.CFrame, + targetCFrame + }, + 0.5 + ) +end + + +return CameraManager + diff --git a/Client2018/content/internal/AppShell/Modules/Shell/CameraManagerModules/CameraManager_ZoneManager.lua b/Client2018/content/internal/AppShell/Modules/Shell/CameraManagerModules/CameraManager_ZoneManager.lua new file mode 100644 index 0000000..9facc36 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/CameraManagerModules/CameraManager_ZoneManager.lua @@ -0,0 +1,122 @@ +-- Clean up this file when remove FIntXboxAvatarEditorRolloutPercent2 + +-- Written by Tomarty, Copyright Roblox 2015 +local runService = game:GetService("RunService") + + + + +local ZoneAnimator; + +do + + local animators = {} + + local function GetAnimator(zone) + if animators[zone] then + return animators[zone] + end + local moduleScript = script.Parent:WaitForChild("CameraManager_Zones"):FindFirstChild("CameraManagerZone_" .. tostring(zone)) + if not moduleScript then + return + end + local animator = require(moduleScript) + animators[zone] = animator + return animator + end + + + ZoneAnimator = { + CurrentZone = nil; + } + + local connection; + + function ZoneAnimator:SetZone(zone) + if ZoneAnimator.CurrentZone == zone then + return + end + ZoneAnimator.CurrentZone = zone + if connection then + connection:disconnect() + connection = nil + end + local animator = GetAnimator(zone) + if not animator then + return + end + animator:SetEnabled(true) + connection = { + disconnect = function() + animator:SetEnabled(false) + end + } + end + +end + + +local SkyboxManager; + +do + SkyboxManager = {} + + local activeSkybox = nil + function SkyboxManager:SetZone(id) + if activeSkybox then + activeSkybox.Parent = nil + activeSkybox = nil + end + local Skyboxes = game:GetService("ReplicatedStorage"):WaitForChild("Skyboxes") + local skybox = Skyboxes:FindFirstChild(id or "default") + if skybox then + activeSkybox = skybox:Clone() + local lighting = game:GetService("Lighting") + skybox.Parent = lighting + pcall(function() + for k, v in pairs(skybox:GetChildren()) do + lighting[v.Name] = v.Value + end + end) + end + end + +end + + + + + + + +local ZoneManager = { + Zone = nil; +} + +--local ZoneAnimator = require(script.ZoneAnimator) +--local Skybox = require(script.Skybox) + +local function setZoneInternal(zone) + ZoneManager.Zone = zone + SkyboxManager:SetZone(zone) + ZoneAnimator:SetZone(zone) +end + +function ZoneManager:SetZone(zone) + if zone == ZoneManager.Zone then + return + end + if runService:IsRunning() then + setZoneInternal(zone) + else + spawn(function() + while not runService:IsRunning() do wait() end + setZoneInternal(zone) + end) + end +end + +return ZoneManager + + + diff --git a/Client2018/content/internal/AppShell/Modules/Shell/CameraManagerModules/CameraManager_Zones/CameraManagerZone_City.lua b/Client2018/content/internal/AppShell/Modules/Shell/CameraManagerModules/CameraManager_Zones/CameraManagerZone_City.lua new file mode 100644 index 0000000..9ed8379 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/CameraManagerModules/CameraManager_Zones/CameraManagerZone_City.lua @@ -0,0 +1,218 @@ +local runService = game:GetService("RunService") + + +local Zones = workspace:FindFirstChild("Zones") +local City = Zones and Zones:FindFirstChild("City") + +local CFramer = {}; do + function CFramer.GetAllParts(model, list) + list = list or {} + + if model:IsA("BasePart") then + list[#list + 1] = model + end + + local children = model:GetChildren() + for i = 1, #children do + CFramer.GetAllParts(children[i], list) + end + + return list + end + + function CFramer.GetAllPartOffsets(parts, cframeBase) + -- The offset is a part's cframe relative to cframeBase + + local offsets = {} + local cframeBase_inv = cframeBase:inverse() + for i = 1, #parts do + offsets[i] = cframeBase_inv * parts[i].CFrame + end + + return offsets + end + + function CFramer.newCFramer(parts, offsets) + return function(cf) + for i = 1, #parts do + parts[i].CFrame = cf * offsets[i] + end + end + end + + function CFramer.newCFramerFromModel(model, cframeBase) + local parts = CFramer.GetAllParts(model) + local offsets = CFramer.GetAllPartOffsets(parts, cframeBase) + return CFramer.newCFramer(parts, offsets) + end +end + +local renderTrains; +spawn(function() + while not runService:IsRunning() do wait(0.1) end + + if not City then + City = workspace:WaitForChild("Zones"):WaitForChild("City") + end + + local exampleTrain = City.Train:Clone() + local function newTrain() + local train = exampleTrain:Clone() + train.Parent = workspace + return CFramer.newCFramerFromModel(train.Parts, train.Track.CFrame) + end + + + local function newTrack(part, direction) + if not direction then + direction = math.random(0, 1) * 2 - 1 + end + local length = part.Size.Y + local cf = part.CFrame * CFrame.Angles(math.pi / 2 * (direction + 1), 0, 0) + local cf0 = cf * CFrame.new(0, -length/2, 0) + return function(train, time) + local distance = time * 48 + if distance >= 0 and distance <= length then + train(cf0 * CFrame.new(0, distance, 0)) + end + end, part.Size.Y + end + + local train0 = newTrain() + local train1 = newTrain() + local train2 = newTrain() + local train3 = newTrain() + local train4 = newTrain() + local train5 = newTrain() + local train6 = newTrain() + + local tracks = City.Tracks + + local track0 = newTrack(tracks.TrackA1)--, 1) + local track1 = newTrack(tracks.TrackA2)--, -1) + local track2 = newTrack(tracks.TrackA3)--, 1) + local track3 = newTrack(tracks.TrackB1)--, 1) + local track4 = newTrack(tracks.TrackB2)--, -1) + local track5 = newTrack(tracks.TrackC1)--, -1) + local track6 = newTrack(tracks.TrackC2)--, 1) + local track7 = newTrack(tracks.TrackC3)--, 1) + + function renderTrains(timestamp) + + track0(train0, timestamp % 15) + track1(train1, (timestamp - 3) % 20) + track2(train2, (timestamp - 5.5) % 18) + + track3(train3, (timestamp - 5.5) % 25) + + track6(train4, (timestamp - 2.5) % 30) + end +end) + + +local renderFlows; + +spawn(function() + + if not City then + City = workspace:WaitForChild("Zones"):WaitForChild("City") + end + + + local function newFlow(model, flowLength) + + local parts = {} + local partParts = {} + local partPartOffsets = {} + local cframes = {} + + for j, part in pairs(model:GetChildren()) do + local i = tonumber(part.Name) + parts[i] = part + cframes[i] = part.CFrame + local pParts = {} + local pPartOffsets = {} + for i, pPart in pairs(part:GetChildren()) do + if pPart:IsA("BasePart") then + pParts[#pParts + 1] = pPart + pPartOffsets[#pPartOffsets + 1] = part.CFrame:inverse() * pPart.CFrame + end + end + partParts[i] = pParts + partPartOffsets[i] = pPartOffsets + end + + + local partCycle = 0 + local unitOffset = 0 + local timestamp0 = 0 + return function(timestamp1) + if timestamp1 < timestamp0 then + timestamp0 = timestamp1 + end + local unit = (timestamp1 - timestamp0) / flowLength + unitOffset + + if unit >= 1 then + partCycle = (partCycle + math.floor(unit)) % #parts + unit = unit % 1 + unitOffset = unit + timestamp0 = timestamp1 -- - deltaTime * flowLength + end + + for x = 0, #parts - 2 do + local i = (x - partCycle) % #cframes + 1 + local cf = cframes[x + 1]:lerp(cframes[x + 2], unit) + parts[i].CFrame = cf + local pParts = partParts[i] + local pPartOffsets = partPartOffsets[i] + for n = 1, #pParts do + pParts[n].CFrame = cf * pPartOffsets[n] + end + end + + end + + end + + local flow1 = newFlow(City.Pipe1.Flow, 0.5) + local flow2 = newFlow(City.Pipe2.Flow, 0.65) + + function renderFlows(t) + flow1(t) + flow2(t) + end + +end) + + + +local connection; + +local self = {} + +function self:SetEnabled(enabled) + if enabled and connection then + return + end + if connection then + connection:disconnect() + connection = nil + end + if not enabled then + return + end + + local timestamp0 = tick() + connection = game:GetService("RunService").RenderStepped:connect(function() + local t = tick() - timestamp0 + if renderFlows then + renderFlows(t) + end + if renderTrains then + renderTrains(t) + end + end) + +end + +return self diff --git a/Client2018/content/internal/AppShell/Modules/Shell/CameraManagerModules/CameraManager_Zones/CameraManagerZone_Space.lua b/Client2018/content/internal/AppShell/Modules/Shell/CameraManagerModules/CameraManager_Zones/CameraManagerZone_Space.lua new file mode 100644 index 0000000..bc0cc9a --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/CameraManagerModules/CameraManager_Zones/CameraManagerZone_Space.lua @@ -0,0 +1,145 @@ +local runService = game:GetService("RunService") + +local CFrameOffsetter = {}; + +do + function CFrameOffsetter.GetAllParts(model, list) + list = list or {} + + if model:IsA("BasePart") then + list[#list + 1] = model + end + + local children = model:GetChildren() + for i = 1, #children do + CFrameOffsetter.GetAllParts(children[i], list) + end + + return list + end + function CFrameOffsetter.GetAllPartOffsets(parts, cframeBase) + -- The offset is a part's cframe relative to cframeBase + + local offsets = {} + local cframeBase_inv = cframeBase:inverse() + for i = 1, #parts do + offsets[i] = cframeBase_inv * parts[i].CFrame + end + + return offsets + end + function CFrameOffsetter.newOffsetter(parts, offsets, cframeBase) + return function(offset) + local cf = cframeBase * offset + for i = 1, #parts do + parts[i].CFrame = cf * offsets[i] + end + end + end + function CFrameOffsetter.newOffsetterFromModel(model, cframeBase) + local parts = CFrameOffsetter.GetAllParts(model) + local offsets = CFrameOffsetter.GetAllPartOffsets(parts, cframeBase) + return CFrameOffsetter.newOffsetter(parts, offsets, cframeBase) + end +end + +local newOffsetterFromModel = CFrameOffsetter.newOffsetterFromModel + +local renderStation; +spawn(function() + while not runService:IsRunning() do wait(0.1) end + + local Space = workspace:WaitForChild("Zones"):WaitForChild("Space") + local model = Space.Station + local cframeBase = model.rings.center.CFrame + + local ring1 = newOffsetterFromModel(model.rings.Ring1, cframeBase) + local ring2 = newOffsetterFromModel(model.rings.Ring2, cframeBase) + local ring3 = newOffsetterFromModel(model.rings.Ring3, cframeBase) + local station = newOffsetterFromModel(model.station, cframeBase) + + + function renderStation(t) + ring1( + CFrame.Angles(0, (t / 6 % (math.pi * 2)), 0) + * CFrame.new(0, math.sin(t / 4 % (math.pi * 2)) * 1, 0) + ) + ring2( + CFrame.Angles(0, -(t / 4 % (math.pi * 2)), 0) + * CFrame.new(0, math.sin(2 + t / 2 % (math.pi * 2)) * 1, 0) + ) + ring3( + CFrame.Angles(0, (t / 3.5 % (math.pi * 2)), 0) + * CFrame.new(0, math.sin(1 + t / 2 % (math.pi * 2)) * 1, 0) + ) + + station( + CFrame.Angles(0, (t / -32 % (math.pi * 2)), 0) + * CFrame.new(0, math.sin(t / 4 % (math.pi * 2)) * 3 - 2, 0) + ) + end +end) + +local renderOrbit; +spawn(function() + while not runService:IsRunning() do wait(0.1) end + + local Space = workspace:WaitForChild("Zones"):WaitForChild("Space") + + local model = Space.Orbit + local cframeBase = model.center.CFrame + + local cuteMoonThings = newOffsetterFromModel(model, cframeBase) + + local ringParts = {Space.Disk, Space.Disk2, Space.Disk3} + + local rings = {} + for i = 1, #ringParts do + rings[i] = newOffsetterFromModel(ringParts[i], ringParts[i].CFrame) + end + + function renderOrbit(t) + cuteMoonThings( + CFrame.Angles((t / 80 % (math.pi * 2)), (t / 60 % (math.pi * 2)), (t / 40 % (math.pi * 2))) + ) + for i = 1, #rings do + rings[i]( + CFrame.Angles(0, 1 / 4 + (t / 60 % (math.pi * 2)), 0) + ) + end + end +end) + + + + +local connection; + +local self = {} + +function self:SetEnabled(enabled) + if enabled and connection then + return + end + if connection then + connection:disconnect() + connection = nil + end + if not enabled then + return + end + + local timestamp0 = tick() + connection = game:GetService("RunService").RenderStepped:connect(function() + local t = (tick() - timestamp0) + if renderStation then + renderStation(t) + end + if renderOrbit then + renderOrbit(t) + end + end) + +end + +return self diff --git a/Client2018/content/internal/AppShell/Modules/Shell/CameraManagerModules/CameraManager_Zones/CameraManagerZone_Volcano.lua b/Client2018/content/internal/AppShell/Modules/Shell/CameraManagerModules/CameraManager_Zones/CameraManagerZone_Volcano.lua new file mode 100644 index 0000000..11d96e4 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/CameraManagerModules/CameraManager_Zones/CameraManagerZone_Volcano.lua @@ -0,0 +1,70 @@ +local runService = game:GetService("RunService") + + +local Volcano; +local lightning; + +spawn(function() + while not runService:IsRunning() do wait(0.1) end + + Volcano = workspace:WaitForChild("Zones"):WaitForChild("Volcano") + lightning = Volcano.Lightning:GetChildren() + for i = 1, #lightning do + lightning[i].Parent = nil + end +end) + +local function playLighting(model) + if not (model and Volcano) then + return + end + model.Parent = Volcano.Lightning + wait(0.125) + model.Parent = nil +end + + + +local connection; +local handle; + +local self = {} + +function self:SetEnabled(enabled) + if enabled and connection then + return + end + if connection then + connection:disconnect() + connection = nil + end + handle = nil + if not enabled then + return + end + + local h = {} -- our handle + handle = h -- Stops a previous loop + coroutine.wrap(function() + while handle == h do + wait(math.random() * 16 + 1) + + if lightning then + playLighting(lightning[math.random(1, #lightning)]) + end + + end + end)() + + --[[ + local timestamp0 = tick() + connection = game:GetService("RunService").RenderStepped:connect(function() + local t = (tick() - timestamp0) + --renderStation(t) + --renderOrbit(t) + end) + --]] + +end + +return self diff --git a/Client2018/content/internal/AppShell/Modules/Shell/CameraManagerModules/ZoneManager.lua b/Client2018/content/internal/AppShell/Modules/Shell/CameraManagerModules/ZoneManager.lua new file mode 100644 index 0000000..85e3046 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/CameraManagerModules/ZoneManager.lua @@ -0,0 +1,100 @@ +local runService = game:GetService("RunService") + +local ZoneAnimator; + +do + local animators = {} + + local function GetAnimator(zone) + if animators[zone] then + return animators[zone] + end + local moduleScript = script.Parent:WaitForChild("CameraManager_Zones"):FindFirstChild("CameraManagerZone_" .. tostring(zone)) + if not moduleScript then + return + end + local animator = require(moduleScript) + animators[zone] = animator + return animator + end + + ZoneAnimator = { + CurrentZone = nil; + } + + local connection; + + function ZoneAnimator:SetZone(zone) + if ZoneAnimator.CurrentZone == zone then + return + end + ZoneAnimator.CurrentZone = zone + if connection then + connection:disconnect() + connection = nil + end + local animator = GetAnimator(zone) + if not animator then + return + end + animator:SetEnabled(true) + connection = { + disconnect = function() + animator:SetEnabled(false) + end + } + end +end + +local SkyboxManager; + +do + SkyboxManager = {} + + local activeSkybox = nil + function SkyboxManager:SetZone(id) + if activeSkybox then + activeSkybox.Parent = nil + activeSkybox:Destroy() + activeSkybox = nil + end + local Skyboxes = game:GetService("ReplicatedStorage"):WaitForChild("Skyboxes") + local skybox = Skyboxes:FindFirstChild(id or "default") + if skybox then + activeSkybox = skybox:Clone() + local lighting = game:GetService("Lighting") + activeSkybox.Parent = lighting + pcall(function() + for k, v in pairs(activeSkybox:GetChildren()) do + lighting[v.Name] = v.Value + end + end) + end + end +end + +local ZoneManager = { + Zone = nil; +} + +local function setZoneInternal(zone) + ZoneManager.Zone = zone + SkyboxManager:SetZone(zone) + ZoneAnimator:SetZone(zone) +end + +function ZoneManager:SetZone(zone) + if zone == ZoneManager.Zone then + return + end + if runService:IsRunning() then + setZoneInternal(zone) + else + spawn(function() + while not runService:IsRunning() do wait() end + setZoneInternal(zone) + end) + end +end + +return ZoneManager diff --git a/Client2018/content/internal/AppShell/Modules/Shell/CarouselController.lua b/Client2018/content/internal/AppShell/Modules/Shell/CarouselController.lua new file mode 100644 index 0000000..3f782b4 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/CarouselController.lua @@ -0,0 +1,671 @@ +--[[ + // CarouselController.lua + + // Controls how the data is updated for a carousel view +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local ContextActionService = game:GetService('ContextActionService') +local GuiService = game:GetService('GuiService') + +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local GameData = require(ShellModules:FindFirstChild('GameData')) + + +local function createCarouselController(view, disableBumperBinds, FrontPageIndex, EndPageIndex, AbsoluteDataIndex) + disableBumperBinds = disableBumperBinds or false + local this = {} + + local PAGE_SIZE = 25 -- Can only be 10, 25, 50, 100 + local MAX_PAGES = 2 + + local sortCollection = nil + local sortData = nil + local currentFocusData = nil + local absoluteDataIndex = tonumber(AbsoluteDataIndex) or 1 + local guiServiceChangedCn = nil + + local loadBuffer = 10 + + local pages = {} + local pageViews = {} + local isLoading = false + local frontPageIndex = tonumber(FrontPageIndex) or 1 + local endPageIndex = tonumber(EndPageIndex) or 0 + + local isCuratedSort = false + + local reachFront = true --For all sorts to mark if there's no previous page + local reachEnd = false --For all sorts to mark if there's no next page + + -- Events + this.NewItemSelected = Utility.Signal() + + local function getNewItem(placeId) + local data = GameData:GetGameData(placeId) + + if data then + local item = Utility.Create'ImageButton' + { + Name = "CarouselViewImage"; + BackgroundColor3 = GlobalSettings.ModalBackgroundColor; + BorderSizePixel = 0; + SoundManager:CreateSound('MoveSelection'); + } + + local overrideSelection = view:GetSelectionImageObject() + if overrideSelection then + item.SelectionImageObject = overrideSelection + end + + local thumbnailLoader = ThumbnailLoader:Create(item, data.IconId, ThumbnailLoader.Sizes.Medium, ThumbnailLoader.AssetType.SquareIcon) + spawn(function() + thumbnailLoader:LoadAsync(true, false, nil) + end) + + local itemInfo = { + item = item; + thumbnailLoader = thumbnailLoader; + } + + itemInfo.PlaceId = placeId + + local transparencyTweens = {} + + function itemInfo:AnimateTransparency(initial, final, duration) + Utility.CancelTweens(transparencyTweens) + + local finished = nil + if initial < final then + thumbnailLoader:SetTransparency(final) + else + finished = function() + thumbnailLoader:SetTransparency(final) + end + end + + table.insert(transparencyTweens, + Utility.PropertyTweener( + item, + 'BackgroundTransparency', + initial, + final, + duration, + Utility.EaseOutQuad, + true)) + + table.insert(transparencyTweens, + Utility.PropertyTweener( + item, + 'ImageTransparency', + initial, + final, + duration, + Utility.EaseOutQuad, + true, + finished)) + end + + return itemInfo + end + end + + -- TODO: Remove this when caching is finished. Doing it this way is going to leave the old + -- data issues in place with the carousel + local function setInternalData(page) + if not page then + return {} + end + + local newData = {} + newData.PreviousUrl = page.PreviousUrl + newData.NextUrl = page.NextUrl + + local placeIds = page:GetPagePlaceIds() + local names = page:GetPagePlaceNames() + local voteData = page:GetPageVoteData() + local iconIds = page:GetPageIconIds() + local creatorNames = page:GetCreatorNames() + local creatorUserIds = page:GetPageCreatorUserIds() + + for i = 1, #page.Data do + local gameEntry = { + Name = names[i]; + PlaceId = placeIds[i]; + IconId = iconIds[i]; + VoteData = voteData[i]; + CreatorName = creatorNames[i]; + CreatorUserId = creatorUserIds[i]; + -- Description and IsFavorites needs to be queried for each game when needed + Description = nil; + IsFavorited = nil; + GameData = nil; + } + table.insert(newData, gameEntry) + end + + return newData + end + + + local onViewFocusChanged; + + local function createPageView(page) + local pageView = {} + local viewItems = {} + + for i = 1, #page do + local itemData = page[i] + local viewItemInfo = getNewItem(itemData) + if viewItemInfo then + viewItemInfo.item.MouseButton1Click:connect(function() + if itemData then + EventHub:dispatchEvent(EventHub.Notifications["OpenGameDetail"], itemData) + end + end) + table.insert(viewItems, viewItemInfo) + end + end + + function pageView:LockFirstItem() + if viewItems and viewItems[1] and viewItems[1].item then + viewItems[1].item.NextSelectionLeft = viewItems[1].item + end + end + function pageView:UnlockFirstItem() + if viewItems and viewItems[1] and viewItems[1].item then + viewItems[1].item.NextSelectionLeft = nil + end + end + function pageView:LockLastItem() + if viewItems and viewItems[#viewItems] and viewItems[#viewItems].item then + viewItems[#viewItems].item.NextSelectionRight = viewItems[#viewItems].item + end + end + function pageView:UnlockLastItem() + if viewItems and viewItems[#viewItems] and viewItems[#viewItems].item then + viewItems[#viewItems].item.NextSelectionRight = nil + end + end + function pageView:GetCount() + return #viewItems + end + function pageView:GetItems() + return viewItems + end + function pageView:Destroy() + for i,itemInfo in pairs(viewItems) do + viewItems[i] = nil + itemInfo.item:Destroy() + end + end + + return pageView + end + + local function getPagesRangeIndex() + local lowIndex, highIndex + for index, page in pairs(pages) do + if not lowIndex or lowIndex > index then + lowIndex = index + end + + if not highIndex or highIndex < index then + highIndex = index + end + end + + return lowIndex, highIndex + end + + -- For all sorts except curated sort + local function getPageAsync(pageIndex) + if pageIndex < 1 then + return nil + end + + --Caching is done inside sortsdata + local startIndex = (pageIndex - 1) * PAGE_SIZE + local newPageData = sortCollection:GetSortAsync(startIndex, PAGE_SIZE) + if not newPageData then + return nil + end + + if #newPageData > 0 then + pages[pageIndex] = newPageData + end + + return pages[pageIndex] + end + + -- For curated sort + local function getNextPageAsync(pageIndex) + --Caching is done inside sortsdata + local newPageData = sortCollection:GetCuratedSortAsync(pageIndex, PAGE_SIZE) + + -- No data in current page + if not newPageData or #newPageData == 0 then + reachEnd = true + return nil + end + + -- Current page is last page + if #newPageData < PAGE_SIZE then + reachEnd = true + end + + pages[pageIndex] = newPageData + return pages[pageIndex] + end + + -- For curated sort + local function getPreviousPageAsync(pageIndex) + -- pageIndex should be >= 0 + -- When pageIndex == 0, means hasPrevPage is true in page1. Then we should fetch page0, insert front, and move all page index +1 + if pageIndex < 0 then + reachFront = true + return nil + end + + --Caching is done inside sortsdata + local newPageData, hasPrevPage = sortCollection:GetCuratedSortAsync(pageIndex, PAGE_SIZE, true) + + -- No data in current page + if not newPageData or #newPageData == 0 then + reachFront = true + return nil + end + + -- Current page is first page + if not hasPrevPage or #newPageData < PAGE_SIZE then + reachFront = true + end + + if pageIndex == 0 then + --Move all pages to their next pages + local lowIndex, highIndex = getPagesRangeIndex() + for i = highIndex, 1, -1 do + pages[i + 1] = pages[i] + end + pages[1] = newPageData + else + pages[pageIndex] = newPageData + end + + -- Increase 1 for all page indexes if we insert page at front + if pageIndex == 0 then + pageIndex = 1 + frontPageIndex = frontPageIndex + 1 + endPageIndex = endPageIndex + 1 + end + + return pages[pageIndex] + end + +-- For curated sort. If getPreviousPageAsync() reach front and frontPageIndex > 1, reset page index + local function resetPageIndex() + local pageOffset = frontPageIndex - 1 + frontPageIndex = 1 + endPageIndex = endPageIndex - pageOffset + -- Remove previous pageviews + for i = 1, pageOffset do + local frontPageView = pageViews[i] + if frontPageView then + pageViews[i] = nil + view:RemoveAmountFromFront(frontPageView:GetCount()) + frontPageView:Destroy() + end + end + + -- Move pages and pageViews to left + for i = 1, endPageIndex do + pages[i] = pages[i + pageOffset] + pageViews[i] = pageViews[i + pageOffset] + end + for i = endPageIndex + 1, endPageIndex + pageOffset do + pages[i] = nil + pageViews[i] = nil + end + absoluteDataIndex = absoluteDataIndex - (pageOffset * PAGE_SIZE) + end + + local previousFocusItem = nil + function onViewFocusChanged(newFocusItem) + local offset = 0 + if previousFocusItem then + offset = view:GetItemIndex(newFocusItem) - view:GetItemIndex(previousFocusItem) + end + + local visibleItemCount = view:GetVisibleCount() + local itemCount = view:GetCount() + + if offset > 0 then + -- scrolled right + --lastVisibleItemIndex = firstVisibleItemIndex + visibleItemCount - 1 + local firstVisibleItemIndex = view:GetFirstVisibleItemIndex() + local lastVisibleItemIndex = view:GetLastVisibleItemIndex() + +-- Utility.DebugLog("Scroll right", frontPageIndex, endPageIndex, absoluteDataIndex + offset, offset, "needItemCount:", lastVisibleItemIndex + loadBuffer - itemCount, "reachEnd", reachEnd) + + --Scrolling forward: If the last visible item(right most one) is no more than loadBuffer's steps from the trail, we will truncate new ones at back + if not isLoading and not reachEnd and lastVisibleItemIndex + loadBuffer >= itemCount then + isLoading = true + spawn(function() + local page + if isCuratedSort then + page = getNextPageAsync(endPageIndex + 1) + else + page = getPageAsync(endPageIndex + 1) + end + if page then + local newView = createPageView(page) + endPageIndex = endPageIndex + 1 + view:InsertCollectionBack(newView:GetItems()) + pageViews[endPageIndex] = newView + if pageViews[endPageIndex - 1] then + pageViews[endPageIndex - 1]:UnlockLastItem() + end + newView:LockLastItem() + + if view:GetCount() > PAGE_SIZE * MAX_PAGES then + local frontPageView + frontPageView = pageViews[frontPageIndex] + --Don't erase the front page if the firstVisibleItem becomes too close to the leading edge + if firstVisibleItemIndex - frontPageView:GetCount() - loadBuffer > 0 then + pageViews[frontPageIndex] = nil + frontPageIndex = frontPageIndex + 1 + view:RemoveAmountFromFront(frontPageView:GetCount()) + frontPageView:Destroy() + if reachFront then + reachFront = false + end + end + end + else + reachEnd = true + end + isLoading = false + end) + end + elseif offset < 0 then + -- scrolled left + --firstVisibleItemIndex = lastVisibleItemIndex - visibleItemCount + 1 + local firstVisibleItemIndex = view:GetFirstVisibleItemIndex() + local lastVisibleItemIndex = view:GetLastVisibleItemIndex() + +-- Utility.DebugLog("Scroll left", frontPageIndex, endPageIndex, absoluteDataIndex + offset, offset, "firstVisibleItemIndex", firstVisibleItemIndex, "reachFront", reachFront) + + --Scrolling backward: If the first visible item(left most one) is no more than loadBuffer's steps from the head, we will truncate new ones at front + if not isLoading and not reachFront and firstVisibleItemIndex - loadBuffer <= 0 then + isLoading = true + spawn(function() + local page + if isCuratedSort then + page = getPreviousPageAsync(frontPageIndex - 1) + else + page = getPageAsync(frontPageIndex - 1) + end + if page then + local newView = createPageView(page) + frontPageIndex = frontPageIndex - 1 + view:InsertCollectionFront(newView:GetItems()) + pageViews[frontPageIndex] = newView + if pageViews[frontPageIndex + 1] then + pageViews[frontPageIndex + 1]:UnlockFirstItem() + end + newView:LockFirstItem() + + if view:GetCount() > PAGE_SIZE * MAX_PAGES then + local endPageView + endPageView = pageViews[endPageIndex] + --Don't erase the trail page if the lastVisibleItemIndex becomes too close to the trail + if lastVisibleItemIndex + loadBuffer < itemCount - endPageView:GetCount() then + pageViews[endPageIndex] = nil + endPageIndex = endPageIndex - 1 + view:RemoveAmountFromBack(endPageView:GetCount()) + endPageView:Destroy() + if reachEnd then + reachEnd = false + end + end + end + else + reachFront = true + end + + if isCuratedSort and reachFront and frontPageIndex > 1 then + resetPageIndex() + end + isLoading = false + end) + end + end + + absoluteDataIndex = absoluteDataIndex + offset + previousFocusItem = newFocusItem + view:ChangeFocus(newFocusItem) + + local pageNumber = math.ceil(absoluteDataIndex/PAGE_SIZE) + local pageDataIndex = absoluteDataIndex - ((pageNumber - 1) * PAGE_SIZE) + currentFocusData = pages[pageNumber][pageDataIndex] + this.NewItemSelected:fire(currentFocusData) + end + + --[[ Public API ]]-- + function this:GetFrontGameData() + if pages[frontPageIndex] then + return pages[frontPageIndex][1] + end + end + + function this:GetCurrentFocusGameData() + return currentFocusData + end + + function this:SelectFront() + local frontViewItem = view:GetFront() + if frontViewItem then + onViewFocusChanged(frontViewItem) + end + end + + function this:SetLoadBuffer(newValue) + loadBuffer = newValue + end + + function this:InitializeAsync(gameCollection) + view:RemoveAllItems() + frontPageIndex = frontPageIndex or 1 + endPageIndex = endPageIndex or 0 + absoluteDataIndex = absoluteDataIndex or 1 + currentFocusData = nil + pages = {} + pageViews = {} + sortCollection = gameCollection + isCuratedSort = gameCollection and gameCollection.GameSetTargetId and gameCollection.GameSetTargetId ~= 0 +-- Utility.DebugLog("CarouselController:InitializeAsync", gameCollection.GameSetTargetId, isCuratedSort) + + local loadPrevDataFail = true + -- For refreshed sorts, currenlty only RecentlyPlayed and Favorite sorts + if endPageIndex >= frontPageIndex and absoluteDataIndex > 0 then + local pageCount = 0 + local maxDataIndex = (frontPageIndex - 1) * PAGE_SIZE + for i = frontPageIndex, endPageIndex do + local page = isCuratedSort and getNextPageAsync(i) or getPageAsync(i) + if page then + local newView = createPageView(page) + view:InsertCollectionBack(newView:GetItems()) + pageViews[i] = newView + maxDataIndex = maxDataIndex + newView:GetCount() + pageCount = pageCount + 1 + else + break + end + end + endPageIndex = frontPageIndex + pageCount - 1 + + if pageCount > 0 then + local viewItem + --if requested index overflow, we select the trail + if absoluteDataIndex > maxDataIndex then + absoluteDataIndex = maxDataIndex + viewItem = view:GetBack() + else + local viewIndex = absoluteDataIndex - (frontPageIndex - 1) * PAGE_SIZE + viewItem = view:GetItemAt(viewIndex) + end + if viewItem then + previousFocusItem = viewItem + --Use ChangeFocus to focus on the prev index immediately + view:ChangeFocus(viewItem, 0) + loadPrevDataFail = false + end + end + + if loadPrevDataFail then --Clear data again + view:RemoveAllItems() + frontPageIndex = 1 + endPageIndex = 0 + absoluteDataIndex = 1 + currentFocusData = nil + pages = {} + for i = 1, #pageViews do + pageViews[i]:Destroy() + pageViews[i] = nil + end + pageViews = {} + end + end + + if loadPrevDataFail then + for i = 1, MAX_PAGES do + local page + if isCuratedSort then + page = getNextPageAsync(i) + else + page = getPageAsync(i) + end + if page then + local newView = createPageView(page) + endPageIndex = endPageIndex + 1 + view:InsertCollectionBack(newView:GetItems()) + pageViews[i] = newView + else + break + end + end + + local frontViewItem = view:GetFront() + if frontViewItem then + absoluteDataIndex = 1 + previousFocusItem = frontViewItem + view:SetFocus(frontViewItem) + end + end + + if pageViews and endPageIndex > 0 then + if pageViews[frontPageIndex] then + pageViews[frontPageIndex]:LockFirstItem() + end + if pageViews[endPageIndex] then + pageViews[endPageIndex]:LockLastItem() + end + end + end + + function this:GetIndexData() + return frontPageIndex, endPageIndex, absoluteDataIndex + end + + function this:Connect() + guiServiceChangedCn = Utility.DisconnectEvent(guiServiceChangedCn) + guiServiceChangedCn = GuiService:GetPropertyChangedSignal('SelectedCoreObject'):connect(function() + local newSelection = GuiService.SelectedCoreObject + if newSelection and view:ContainsItem(newSelection) then + onViewFocusChanged(newSelection) + end + end) + + local function getItemNextShiftItem(direction) + local currentFocusIndex = view:GetItemIndex(previousFocusItem) + local shiftAmount = view:GetFullVisibleItemCount() + local nextItem = view:GetItemAt(currentFocusIndex + shiftAmount * direction) + if not nextItem then + nextItem = direction == 1 and view:GetBack() or view:GetFront() + end + + return nextItem + end + + local function shiftRight() + local nextItem = getItemNextShiftItem(1) + if nextItem then + GuiService.SelectedCoreObject = nextItem + end + end + + local function shiftLeft() + local nextItem = getItemNextShiftItem(-1) + if nextItem then + GuiService.SelectedCoreObject = nextItem + end + end + + local seenRightBumper = false + local function onBumperRight(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + seenRightBumper = true + elseif seenRightBumper and inputState == Enum.UserInputState.End then + local currentSelection = GuiService.SelectedCoreObject + if currentSelection and view:ContainsItem(currentSelection) then + shiftRight() + end + seenRightBumper = false + end + end + + local seenLeftBumper = false + local function onBumperLeft(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + seenLeftBumper = true + elseif seenLeftBumper and inputState == Enum.UserInputState.End then + local currentSelection = GuiService.SelectedCoreObject + if currentSelection and view:ContainsItem(currentSelection) then + shiftLeft() + end + seenRightBumper = false + end + end + + -- Bumper Binds + if not disableBumperBinds then + ContextActionService:UnbindCoreAction("BumperRight") + ContextActionService:UnbindCoreAction("BumperLeft") + ContextActionService:BindCoreAction("BumperRight", onBumperRight, false, Enum.KeyCode.ButtonR1) + ContextActionService:BindCoreAction("BumperLeft", onBumperLeft, false, Enum.KeyCode.ButtonL1) + end + end + + function this:Disconnect() + guiServiceChangedCn = Utility.DisconnectEvent(guiServiceChangedCn) + if not disableBumperBinds then + ContextActionService:UnbindCoreAction("BumperRight") + ContextActionService:UnbindCoreAction("BumperLeft") + end + end + + function this:HasResults() + return endPageIndex - frontPageIndex >= 0 + end + + return this +end + +return createCarouselController diff --git a/Client2018/content/internal/AppShell/Modules/Shell/CarouselView.lua b/Client2018/content/internal/AppShell/Modules/Shell/CarouselView.lua new file mode 100644 index 0000000..8926050 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/CarouselView.lua @@ -0,0 +1,393 @@ +--[[ + // CarouselView.lua + + // View for a carousel. Used for GameGenre screen + // TODO: Support Vertical? + // + // Current this supports a focus that is aligned to the left (0, 0), in the future we + // could do other alignments if we need them +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local GameData = require(ShellModules:FindFirstChild('GameData')) + +local function createCarouselView() + local this = {} + + local itemInfos = {} + local padding = 0 + local itemSizePercentOfContainer = 1 + local focusItem = nil + + local BASE_TWEEN_TIME = 0.2 + + local isViewFocused = false + + local overrideSelectionImage = nil + local incomingSelectableValue = true; + local incomingImageColor = Color3.new(1,1,1); + + local DISABLED_COLOR = Color3.new(0.4, 0.4, 0.4) + local ENABLED_COLOR = Color3.new(1, 1, 1) + + local container = Utility.Create'ScrollingFrame' + { + Name = "CarouselContainer"; + BackgroundTransparency = 1; + ClipsDescendants = false; + ScrollingEnabled = false; + Selectable = false; + ScrollBarThickness = 0; + } + + local function isVisible(item) + return item.AbsolutePosition.x + item.AbsoluteSize.x >= 0 and + item.AbsolutePosition.x < GuiRoot.AbsoluteSize.x + end + + local function getFocusSize() + local scaling = itemSizePercentOfContainer + + if isViewFocused then + scaling = 1 + end + + local size = container.Size.Y.Offset * scaling + return UDim2.new(0, size, 0, size) + end + + local function getNonFocusSize() + local size = container.Size.Y.Offset * itemSizePercentOfContainer + return UDim2.new(0, size, 0, size) + end + + local function getItemSize(item) + if item == focusItem then + return getFocusSize() + else + return getNonFocusSize() + end + end + + local function getItemLayoutPosition(index) + local focusIndex = this:GetItemIndex(focusItem) + local offsetFromFocus = index - focusIndex + local x, y = 0, 0 + + if index > focusIndex then + -- items to the right of focus need additional buffer due to focus size being larger + x = getFocusSize().X.Offset + offsetFromFocus * padding + (offsetFromFocus - 1) * getNonFocusSize().X.Offset + else + x = offsetFromFocus * padding + offsetFromFocus * getNonFocusSize().X.Offset + end + + local size = (focusIndex == index) and getFocusSize() or getNonFocusSize() + + y = (container.Size.Y.Offset - size.Y.Offset) / 2 + + return UDim2.new(0, x, 0, y) + end + + local function recalcLayout(duration) + duration = duration or 0 + + for i = 1, #itemInfos do + local item = itemInfos[i].item + local size = getItemSize(item) + local position = getItemLayoutPosition(i) + + if item:IsDescendantOf(game) then + item:TweenSizeAndPosition(size, position, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, duration, true) + else + item.Size = size + item.Position = position + end + end + end + + --[[ Public API ]]-- + function this:ChangeFocus(newFocus, tweenTime) + -- We don't use SetFocus() as that function will do a recalc. We want to get the next position + -- from current position, not recalcd postion for each item + + if self:ContainsItem(newFocus) then + focusItem = newFocus + recalcLayout(tweenTime or BASE_TWEEN_TIME) + end + end + + function this:SetSize(newSize) + if newSize ~= container.Size then + container.Size = newSize + container.CanvasSize = UDim2.new(0, container.Size.X.Offset * 2, 1, 0) + recalcLayout() + end + end + + function this:SetPosition(newPosition) + container.Position = newPosition + end + + function this:SetPadding(newPadding) + if newPadding ~= padding then + padding = newPadding + recalcLayout() + end + end + + function this:SetItemSizePercentOfContainer(value) + if value ~= itemSizePercentOfContainer then + itemSizePercentOfContainer = value + recalcLayout() + end + end + + function this:SetParent(newParent) + container.Parent = newParent + end + + function this:SetFocus(newFocusItem) + if self:ContainsItem(newFocusItem) and newFocusItem ~= focusItem then + focusItem = newFocusItem + recalcLayout() + end + end + + function this:SetClipsDescendants(value) + container.ClipsDescendants = value + end + + local DEFAULT_FADE_DURATION = 0.2 + local targetTransparency = 0 + + function this:SetTransparency(value, duration, refresh) + if not refresh and value == targetTransparency then return end + + if duration then + targetTransparency = Utility.Clamp(0, 1, targetTransparency) + if not refresh and value == targetTransparency then return end + else + duration = DEFAULT_FADE_DURATION + end + + for _, itemInfo in pairs(itemInfos) do + itemInfo:AnimateTransparency(targetTransparency, value, duration) + end + + targetTransparency = value + end + + function this:GetTransparency() + return targetTransparency + end + + function this:SetSelectable(value) + incomingImageColor = ENABLED_COLOR + if not value then + incomingImageColor = DISABLED_COLOR + end + + incomingSelectableValue = value + + for _, itemInfo in pairs(itemInfos) do + itemInfo.item.Selectable = incomingSelectableValue + itemInfo.item.ImageColor3 = incomingImageColor + end + end + + function this:GetAvailableItem() + if focusItem and focusItem.Parent then + return focusItem + end + return self:GetFront() + end + + function this:GetFocusItem() + return focusItem + end + + function this:GetItemAt(index) + return itemInfos[index] and itemInfos[index].item + end + + function this:GetFront() + return itemInfos[1] and itemInfos[1].item + end + + function this:GetBack() + return itemInfos[#itemInfos] and itemInfos[#itemInfos].item + end + + function this:GetItemIndex(item) + for i = 1, #itemInfos do + if itemInfos[i].item == item then + return i + end + end + + return 0 + end + + function this:GetCount() + return #itemInfos + end + + function this:GetVisibleCount() + local visibleItemCount = 0 + for i = 1, #itemInfos do + if isVisible(itemInfos[i].item) then + visibleItemCount = visibleItemCount + 1 + end + end + + return visibleItemCount + end + + function this:GetFirstVisibleItemIndex() + for i = 1, #itemInfos do + if isVisible(itemInfos[i].item) then + return i + end + end + end + + function this:GetLastVisibleItemIndex() + for i = #itemInfos, 1, -1 do + if isVisible(itemInfos[i].item) then + return i + end + end + end + + function this:GetFullVisibleItemCount() + local containerSizeX = container.AbsoluteSize.x + -- remove focus from the size, and figure out how many other items can fit + local fittingSize = containerSizeX - getFocusSize().X.Offset + if fittingSize <= 0 then + return 0 + end + + local itemSize = getNonFocusSize().X.Offset + padding + local count = math.floor(fittingSize/itemSize) + 1 + return count + end + + + function this:InsertCollectionFront(collection) + for i = #collection, 1, -1 do + local itemInfo = collection[i] + local item = itemInfo.item + item.Selectable = incomingSelectableValue + item.ImageColor3 = incomingImageColor + item.ImageTransparency = targetTransparency + item.BackgroundTransparency = targetTransparency + + -- set item position in front of front item + item.Position = getItemLayoutPosition(0) + item.Size = getItemSize(item) + + table.insert(itemInfos, 1, itemInfo) + item.Parent = container + GameData:AddRelatedGuiObject(itemInfo.PlaceId, item) + end + end + + function this:InsertCollectionBack(collection) + for i = 1, #collection do + local itemInfo = collection[i] + local item = itemInfo.item + item.Selectable = incomingSelectableValue + item.ImageColor3 = incomingImageColor + item.ImageTransparency = targetTransparency + item.BackgroundTransparency = targetTransparency + + -- set item position behind last item + item.Position = getItemLayoutPosition(#itemInfos + 1) + item.Size = getItemSize(item) + + table.insert(itemInfos, itemInfo) + item.Parent = container + GameData:AddRelatedGuiObject(itemInfo.PlaceId, item) + end + end + + function this:RemoveAmountFromFront(amount) + for i = 1, amount do + local item = table.remove(itemInfos, 1).item + item.Parent = nil + end + recalcLayout() + end + + function this:RemoveAmountFromBack(amount) + for i = 1, amount do + local item = table.remove(itemInfos).item + item.Parent = nil + end + recalcLayout() + end + + function this:RemoveItem(item) + for i = 1, #itemInfos do + if itemInfos[i].item == item then + local removedItem = table.remove(itemInfos, i).item + removedItem.Parent = nil + break + end + end + recalcLayout() + end + + function this:RemoveAllItems() + for i = #itemInfos, 1, -1 do + local item = table.remove(itemInfos, #itemInfos).item + item.Parent = nil + end + end + + function this:ContainsItem(item) + if not item then + return false + end + return item.Parent == container + end + + local function onFocusChanged() + this:ChangeFocus(focusItem) + end + + function this:Focus() + if isViewFocused then return end + + isViewFocused = true + onFocusChanged() + end + + function this:GetContainer() + return container + end + + + function this:RemoveFocus() + if not isViewFocused then return end + + isViewFocused = false + onFocusChanged() + end + + function this:SetSelectionImageObject(guiObject) + overrideSelectionImage = guiObject + end + + function this:GetSelectionImageObject() + return overrideSelectionImage + end + + return this +end + +return createCarouselView diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/AccountAgeStatus.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/AccountAgeStatus.lua new file mode 100644 index 0000000..cdd03a1 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/AccountAgeStatus.lua @@ -0,0 +1,78 @@ +local TextService = game:GetService('TextService') +local ShellModules = script.Parent.Parent + +local GlobalSettings = require(ShellModules.GlobalSettings) +local Strings = require(ShellModules.LocalizedStrings) +local Utility = require(ShellModules.Utility) + +local AccountAgeStatus = {} +AccountAgeStatus.__index = AccountAgeStatus + +function AccountAgeStatus.new(store, parent) + local self = {} + + self.StoreChangedCn = nil + + self.rbx = Utility.Create'Frame' { + Name = "AccountAgeContainer", + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + BackgroundTransparency = 1, + Parent = parent, + + Utility.Create'TextLabel' { + Name = "AccountAgeText", + Size = UDim2.new(1, -12, 1, -12), + Position = UDim2.new(0, 0, 0, 0), + BackgroundTransparency = 1, + Font = GlobalSettings.RegularFont, + FontSize = Enum.FontSize.Size14, + TextColor3 = GlobalSettings.WhiteTextColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + Text = "", + ZIndex = 2, + } + } + + self.StoreChangedCn = store.Changed:Connect(function(newState, oldState) + if newState.RobloxUser.under13 ~= oldState.RobloxUser.under13 then + local isUnder13 = newState.RobloxUser.under13 + -- clear out if under13 is nil + if isUnder13 == nil then + self.rbx.AccountAgeText.Text = "" + return + end + + local newText = Strings:LocalizedString("AccountUnder13Phrase") + if isUnder13 == false then + newText = Strings:LocalizedString("AccountOver13Phrase") + end + + if newText == self.rbx.AccountAgeText.Text then + return + end + + self.rbx.AccountAgeText.Text = newText + -- update layout + local textSize = TextService:GetTextSize(newText, + Utility.ConvertFontSizeEnumToInt(self.rbx.AccountAgeText.FontSize), self.rbx.AccountAgeText.Font, Vector2.new(0, 0)) + self.rbx.Size = UDim2.new(0, textSize.x, 0, 50) + self.rbx.Position = UDim2.new(1, -textSize.x, 0, 0) + end + end) + + setmetatable(self, AccountAgeStatus) + return self +end + +function AccountAgeStatus:Destruct() + self.rbx:Destroy() + self.rbx = nil + if self.StoreChangedCn then + self.StoreChangedCn:Disconnect() + self.StoreChangedCn = nil + end +end + +return AccountAgeStatus \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/AccountAgeStatus.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/AccountAgeStatus.spec.lua new file mode 100644 index 0000000..d101aba --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/AccountAgeStatus.spec.lua @@ -0,0 +1,41 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Store = require(Modules.Common.Rodux).Store + local AccountAgeStatus = require(script.Parent.AccountAgeStatus) + local SetRobloxUser = require(Modules.Shell.Actions.SetRobloxUser) + + it("should construct and destroy the object", function() + local reducer = require(Modules.Shell.Reducers.AppShellReducer) + local store = Store.new(reducer, {}) + local object = AccountAgeStatus.new(store, nil) + + expect(object).to.be.ok() + expect(object).to.be.a("table") + + object:Destruct() + + expect(object.rbx).never.to.be.ok() + expect(object.StoreChangedCn).never.to.be.ok() + + store:destruct() + end) + + it("should update status on store changed", function() + local reducer = require(Modules.Shell.Reducers.AppShellReducer) + local store = Store.new(reducer, {}) + local object = AccountAgeStatus.new(store, nil) + + expect(object.rbx.AccountAgeText.Text).to.equal("") + + local userInfo = { + under13 = true, + } + + store:dispatch(SetRobloxUser(userInfo)) + store:flush() + + expect(object.rbx.AccountAgeText.Text).never.to.equal("") + + store:destruct() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/AlertOverlay.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/AlertOverlay.lua new file mode 100644 index 0000000..e7d54b1 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/AlertOverlay.lua @@ -0,0 +1,237 @@ +--[[ + A Roact alert overlay screen. + Props: + Key : Variant - The key of the overlay screen. + InFocus : bool - Is the component in focus. + Title : String - The title of the screen. + Description : String - The description of the screen. + ImageLabel : Roact.Component - the image to be displayed. + ButtonTextYes : String - The text on the yes button. + ButtonTextNo : String - The text on the no button. + CallbackYes : function() - The callback function for the yes button. + CallbackNo : function() - The callback function for the no button. + CallbackBack : function() - The callback function for when back is pressed. + DefaultButton : 0 or 1 - The default selection for the overlay screen. + 0 for Yes + 1 for No +]] +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui.RobloxGui +local Modules = RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local Utility = require(Modules.Shell.Utility) +local GlobalSettings = require(Modules.Shell.GlobalSettings) +local SoundManager = require(Modules.Shell.SoundManager) + +local RoundedButton = require(Modules.Shell.Components.Common.RoundedButton) +local RedirectComponent = require(Modules.Shell.Components.Common.RedirectComponent) +local AlertOverlay = Roact.PureComponent:extend("AlertOverlay") + +local TEXT_LEFT_EDGE = 776 +local CONFRIM_KEY = "ComfirmButtonKey" +local CANCEL_KEY = "CancelButtonKey" +function AlertOverlay:init() + self.key = self.props.Key + self.guiObjs = {} + self.defaultItemKey = CANCEL_KEY + if self.props.DefaultButton == 0 then + self.defaultItemKey = CONFRIM_KEY + end + self.state = + { + currentItemKey = self.defaultItemKey + } + self.onSelectionGained = function(key) + self:setState( + { + currentItemKey = key + }) + end +end + +function AlertOverlay:willUpdate(nextProps, nextState) + self.defaultItemKey = nil + if self.props.InFocus == nextProps.InFocus then + return + end + if nextProps.InFocus then + self.defaultItemKey = nextState.currentItemKey + end +end + + +function AlertOverlay:render() + local confirmButton; + if not self.props.CallbackYes and self.defaultItemKey == CONFRIM_KEY then + self.defaultItemKey = CANCEL_KEY + elseif not self.props.CallbackNo and self.defaultItemKey == CANCEL_KEY then + self.defaultItemKey = CONFRIM_KEY + end + if self.props.CallbackYes then + local confirmButtonProps = + { + Position = UDim2.new(0, TEXT_LEFT_EDGE, 1, -166); + AnchorPoint = Vector2.new(0,0), + Size = UDim2.new(0, 320, 0, 66); + } + local confirmButtonTextProps = + { + Text = self.props.ButtonTextYes, + TextXAlignment = Enum.TextXAlignment.Center, + } + confirmButton = Roact.createElement(RoundedButton, + { + Button = confirmButtonProps, + Text = confirmButtonTextProps, + Focused = self.props.InFocus and self.state.currentItemKey == CONFRIM_KEY, + Selected = self.defaultItemKey == CONFRIM_KEY, + OnSelectionGained = function() + self.onSelectionGained(CONFRIM_KEY) + end, + OnActivated = function() + SoundManager:Play('ButtonPress') + self.props.CallbackYes() + end + }) + end + local cancelButton; + if self.props.CallbackNo then + local cancelButtonProps = + { + Position = UDim2.new(0, 1106, 1 ,-166), + AnchorPoint = Vector2.new(0, 0), + Size = UDim2.new(0, 320, 0, 66), + } + local cancelButtonTextProps = + { + Text = self.props.ButtonTextNo, + TextXAlignment = Enum.TextXAlignment.Center, + } + cancelButton = Roact.createElement(RoundedButton, + { + Button = cancelButtonProps, + Text = cancelButtonTextProps, + Focused = self.props.InFocus and self.state.currentItemKey == CANCEL_KEY, + Selected = self.defaultItemKey == CANCEL_KEY, + OnSelectionGained = function() + self.onSelectionGained(CANCEL_KEY) + end, + OnActivated = function() + SoundManager:Play('ButtonPress') + self.props.CallbackNo() + end + }) + elseif self.defaultItemKey == CANCEL_KEY then + self.defaultItemKey = CONFRIM_KEY + end + local titleText = Roact.createElement("TextLabel", + { + Size = UDim2.new(0, 0, 0, 0), + Position = UDim2.new(0, TEXT_LEFT_EDGE, 0, 136), + BackgroundTransparency = 1, + Font = GlobalSettings.RegularFont, + FontSize = GlobalSettings.HeaderSize, + TextColor3 = GlobalSettings.WhiteTextColor, + Text = self.props.Title, + TextXAlignment = Enum.TextXAlignment.Left, + }) + local descriptionText = Roact.createElement("TextLabel", + { + Size = UDim2.new(0, 762, 0, 304), + Position = UDim2.new(0, TEXT_LEFT_EDGE, 0, 200), + BackgroundTransparency = 1, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + Font = GlobalSettings.LightFont, + FontSize = GlobalSettings.TitleSize, + TextColor3 = GlobalSettings.WhiteTextColor, + TextWrapped = true, + Text = self.props.Description, + }) + local reportIcon = self.props.ImageLabel + if reportIcon == nil then + reportIcon = Roact.createElement("ImageLabel", + { + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = GlobalSettings.Images.LargeErrorIcon, + Size = UDim2.new(0, 321, 0, 264), + }) + end + local imageContainer = Roact.createElement("Frame", + { + Size = UDim2.new(0, 576, 0, 642), + Position = UDim2.new(0, 100, 0.5, 0), + AnchorPoint = Vector2.new(0, 0.5), + BorderSizePixel = 0, + BackgroundTransparency = 1, + },{ + reportImage = reportIcon + }) + local container = Roact.createElement("Frame", + { + Size = UDim2.new(1, 0, 0, 640), + AnchorPoint = Vector2.new(0, 0.5), + Position = UDim2.new(0, 0, 0.5, 0), + BackgroundColor3 = GlobalSettings.Colors.OverlayColor; + },{ + ImageContainer = imageContainer, + TitleText = titleText, + DescriptionText = descriptionText, + CancelButton = cancelButton, + ConfirmButton = confirmButton, + }) + local modalOverlay = Roact.createElement("Frame", + { + Size = UDim2.new(1, 0, 1, 0), + AnchorPoint = Vector2.new(0, 0.5), + Position = UDim2.new(0, 0, 0.5, 0), + BackgroundColor3 = Color3.new(0, 0, 0), + BackgroundTransparency = 0.3, + [Roact.Ref] = function(rbx) + self.ref = rbx + end, + },{ + Container = container, + }) + local redirectObj; + if self.props.CallbackBack then + redirectObj = Roact.createElement(RedirectComponent, + { + Key = self.key, + InFocus = true, + RedirectBack = function() + self.props.CallbackBack() + end, + }) + end + return Roact.createElement(Roact.Portal, + { + target = CoreGui + },{ + [self.key] = Roact.createElement("ScreenGui", + { + ZIndexBehavior = Enum.ZIndexBehavior.Sibling, + DisplayOrder = 1 + }, + { + ModalOverlay = modalOverlay, + RedirectObj = redirectObj, + }), + }) +end + +function AlertOverlay:didMount() + delay(0,function() + Utility.AddSelectionParent(self.key, self.ref) + end) + SoundManager:Play("OverlayOpen") +end + +function AlertOverlay:willUnmount() + Utility.RemoveSelectionGroup(self.key) +end + +return AlertOverlay \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/BaseScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/BaseScreen.lua new file mode 100644 index 0000000..3b942bd --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/BaseScreen.lua @@ -0,0 +1,82 @@ +--[[ + A simple base screen for Roact components. + + Props: + BackPageTitle : string - The title of the parent page + Content : Roact.Component - The content of the frame +]] +local RobloxGui = game:GetService("CoreGui").RobloxGui +local Modules = RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local GlobalSettings = require(Modules.Shell.GlobalSettings) +local ContextActionEvent = require(Modules.Shell.Components.ContextActionEvent) + +local BaseScreen = Roact.PureComponent:extend("BaseScreen") + +local BACK_IMAGE = "rbxasset://textures/ui/Shell/Icons/BackIcon@1080.png" +local BACK_IMAGE_SIZE = 48 + +function BaseScreen:init() + self.onBack = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + self._seenPressed = true + elseif inputState == Enum.UserInputState.End and self._seenPressed then + self.props.onUnmount() + end + end + self.onCreate = function(rbx) + end +end + +function BaseScreen:render() + local backPageTitle = self.props.BackPageTitle + local onCreate = self.props.OnCreate or self.onCreate + return Roact.createElement("Frame", + { + [Roact.Ref] = function(rbx) + onCreate(rbx) + end, + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0.5,0,0.5,0), + AnchorPoint = Vector2.new(0.5,0.5), + BackgroundTransparency = 1, + },{ + backImage = Roact.createElement("ImageButton", + { + Size = UDim2.new(0,BACK_IMAGE_SIZE,0,BACK_IMAGE_SIZE), + BackgroundTransparency = 1, + Image = BACK_IMAGE, + Selectable = false, + [Roact.Event.Activated] = self.onBack + }), + BackPageTitleLabel = Roact.createElement("TextLabel", + { + Size = UDim2.new(0, 0, 0, BACK_IMAGE_SIZE), + Position = UDim2.new(0,BACK_IMAGE_SIZE+8,0,BACK_IMAGE_SIZE/2), + AnchorPoint = Vector2.new(0,0.5), + BackgroundTransparency = 1, + Font = GlobalSettings.RegularFont, + FontSize = GlobalSettings.ButtonSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = GlobalSettings.WhiteTextColor, + Text = backPageTitle + }), + -- NOTE: This will need to be changed when the screen is actually connected to the tree. + BackConnector = Roact.createElement(ContextActionEvent, { + name = "GoBackTo" .. backPageTitle, + callback = self.onBack, + binds = { Enum.KeyCode.ButtonB }, + }), + view = Roact.createElement("Frame", + { + Size = UDim2.new(1,0,1,-(BACK_IMAGE_SIZE+2)), + BackgroundTransparency = 1, + Position = UDim2.new(0,0,0,(BACK_IMAGE_SIZE+2)), + },{self.props.Content}) + }) +end + + + +return BaseScreen \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/CategoryMenuView.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/CategoryMenuView.lua new file mode 100644 index 0000000..594d985 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/CategoryMenuView.lua @@ -0,0 +1,156 @@ +--[[ + Creates a Roact component provides a vertical list menu for multiple elements + Props: + Key : Variant - The key for this category menu. The key cannot be changed after init. + InFocus : bool - Is the component in focus. + Navigator : Navigator - The Navigator object. + DefaultCategoryFocus : Variant - This is the key of the default focus button. + DefaultCategoryKey : Variant - The default selection of the category menu. + Categories : Categories - An object that provides data needed for the creating the categories + The object must have an order mapping and a StringKeys mapping for localization. + E.g. + Categories[CategoryKey].Key = "Category" + Categories[CategoryKey].Order = 1 + Categories[CategoryKey].StringKey = "CategoryStringKey" + + OnSelectSection : function(Key : Variant) - Callback for when a section of the key is select. + OnLeaveSection : function(Key : Variant) - Callback for when a section of the key is no longer selected. + EnterSection : function() - Callback functin to enter the menu section. + RedirectUp : function() - Callback functin when redirect up. + RedirectDown : function() - Callback functin when redirect down. + ActionPriority : int - The action priority. +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local Utility = require(Modules.Shell.Utility) +local LocalizedStrings = require(Modules.Shell.LocalizedStrings) + +local VerticalListView = require(Modules.Shell.Components.Common.VerticalListView) +local SelectorButton = require(Modules.Shell.Components.Common.SelectorButton) +local RedirectComponent = require(Modules.Shell.Components.Common.RedirectComponent) + +local CategoryMenuView = Roact.PureComponent:extend("CategoryMenuView") + +local BUTTON_WIDTH = 360 +local BUTTON_HEIGHT = 80 + +function CategoryMenuView:OnSelectSection(key) + if self.state.currentSectionKey ~= key then + self:setState( + { + currentSectionKey = key, + }) + end +end + +function CategoryMenuView:init() + self.key = self.props.Key + self.defaultCategoryKey = self.props.DefaultCategoryKey + self.defaultCategoryFocus = self.defaultCategoryKey or self.props.DefaultCategoryFocus + self.state = + { + currentSectionKey = self.props.DefaultCategoryFocus, + } + self.getCurrentPageIndex = function() + return self.CurrentSection + end +end + +function CategoryMenuView:willUpdate(nextProps, nextState) + self.defaultCategoryKey = nil + if self.props.InFocus == nextProps.InFocus then + return + end + if nextProps.InFocus then + self.defaultCategoryKey = nextState.currentSectionKey or self.props.DefaultCategoryKey + end +end + +function CategoryMenuView:render() + self.categories = self.props.Categories + local buttons = {} + for k in pairs(self.categories) do + buttons[k] = Roact.createElement(SelectorButton, + { + Size = UDim2.new(0, BUTTON_WIDTH, 0, BUTTON_HEIGHT), + AnchorPoint = Vector2.new(0, 0), + Text = LocalizedStrings:LocalizedString(self.categories[k].StringKey), + Key = k, + LayoutOrder = self.categories[k].Order, + Focused = self.state.currentSectionKey == k, + Selected = self.defaultCategoryKey == k, + OnSelectionGained = function(key) + self:OnSelectSection(key) + if self.props.OnSelectSection then + self.props.OnSelectSection(key) + end + end, + OnSelectionLost = self.props.OnLeaveSection, + OnActivated = self.props.EnterSection, + }) + end + + local navObj = Roact.createElement(RedirectComponent, + { + ActionPriority = self.props.ActionPriority, + Key = self.key, + InFocus = self.props.InFocus, + RedirectRight = self.props.EnterSection, + RedirectUp = self.props.RedirectUp, + RedirectDown = self.props.RedirectDown, + }) + + local buttonView = Roact.createElement(VerticalListView, + { + PaddingTop = UDim.new(0.005, 0), + PaddingBottom = UDim.new(0.005, 0), + Spacing = UDim.new(0.025, 0), + ScrollBarThickness = 0, + Items = buttons, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + ScrollingEnabled = false, + }) + + return Roact.createElement("Frame", + { + Size = UDim2.new(1, 0, 1, 0), + AnchorPoint = Vector2.new(0, 0), + Position = UDim2.new(0, 0, 0, 0), + BackgroundTransparency = 1, + Selectable = false, + [Roact.Ref] = function(rbx) + self.ref = rbx + end, + },{ + NavObj = navObj, + ButtonView = buttonView, + }) +end + +function CategoryMenuView:didMount() + delay(0, function() + if self.props.InFocus and self.ref then + Utility.RemoveSelectionGroup(self.key) + Utility.AddSelectionParent(self.key, self.ref) + end + end) +end + +function CategoryMenuView:didUpdate(previousProps, previousState) + if self.props.InFocus == previousProps.InFocus then + return + end + if self.props.InFocus and self.ref then + Utility.RemoveSelectionGroup(self.key) + Utility.AddSelectionParent(self.key, self.ref) + else + Utility.RemoveSelectionGroup(self.key) + end +end + +function CategoryMenuView:willUnmount() + Utility.RemoveSelectionGroup(self.key) +end + +return CategoryMenuView \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/Divider.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/Divider.lua new file mode 100644 index 0000000..15d996f --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/Divider.lua @@ -0,0 +1,33 @@ +--[[ + Creates a vertical divider + Props: + Position : UDim2 - The position of the divider. + DividerWidth : UDim - The width of the divider. + DividerLength : UDim - The length of the divider. + Color : Color3 - The color of the divider. Default GlobalSettings.PageDivideColor + FillDirection : FillDirection - The direction of the divider. +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local GlobalSettings = require(Modules.Shell.GlobalSettings) + +return function(props) + local color = props.Color or GlobalSettings.PageDivideColor + local dividerLength = props.DividerLength + local dividerWidth = props.DividerWidth or UDim.new(0,2) + local position = props.Position + local size + if props.FillDirection == Enum.FillDirection.Horizontal then + size = UDim2.new(dividerLength.Scale, dividerLength.Offset, dividerWidth.Scale, dividerWidth.Offset) + else + size = UDim2.new(dividerWidth.Scale, dividerWidth.Offset, dividerLength.Scale, dividerLength.Offset) + end + + return Roact.createElement("Frame",{ + BackgroundColor3 = color, + BorderSizePixel = 0, + Size = size, + Position = position, + }) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/RedirectComponent.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/RedirectComponent.lua new file mode 100644 index 0000000..e29b306 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/RedirectComponent.lua @@ -0,0 +1,149 @@ +--[[ + A component that is used for navigation and redirects. + Note: The parent must be a scolling + + Props: + Key : String + InFocus : bool + Scale : Vector2 + RedirectBack : function() + RedirectLeft : function(Ref) - If nil, + RedirectRight : function(Ref) + RedirectUp : function(Ref) + RedirectDown : function(Ref) + ActionPriority : The action priority +]] +local RobloxGui = game:GetService("CoreGui").RobloxGui +local Modules = RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local ContextActionService = game:GetService("ContextActionService") + +local RedirectComponent = Roact.PureComponent:extend("RedirectComponent") + +local BACK_KEY = Enum.KeyCode.ButtonB + +local DEFAULT_ACTION_PRIORITY = 2000 + +local function addBackButton(self) + self.exitActionName = "ExitSection"..self.Key + ContextActionService:UnbindCoreAction(self.exitActionName) + if self.props.ActionPriority then + ContextActionService:BindCoreActionAtPriority(self.exitActionName, self.exitAction, false, DEFAULT_ACTION_PRIORITY + self.props.ActionPriority, BACK_KEY) + else + ContextActionService:BindCoreAction(self.exitActionName, self.exitAction, false, BACK_KEY) + end +end + +local function removeBackButton(self) + if self.exitActionName ~= nil then + ContextActionService:UnbindCoreAction(self.exitActionName) + self.exitActionName = nil + end +end + +function RedirectComponent:init() + self.Key = self.props.Key or "RedirectComponent" + self.exitActionName = nil + self.backPressed = false + self.exitAction = function(actionName, inputState, inputObject) + if not self.props.InFocus then + return Enum.ContextActionResult.Pass + end + if inputState == Enum.UserInputState.Begin then + self.backPressed = true + elseif inputState == Enum.UserInputState.End and self.backPressed then + self.backPressed = false + self.props.RedirectBack() + end + end +end + +function RedirectComponent:render() + local props = self.props + local scale = props.Scale or Vector2.new(1,1) + if props.InFocus and props.RedirectBack then + addBackButton(self) + else + removeBackButton(self) + end + local redirectLeftButton; + if props.InFocus and props.RedirectLeft then + redirectLeftButton = Roact.createElement('TextButton', + { + Position = UDim2.new(-scale.X/2, -1, 0.5, 0), + Size = UDim2.new(0, 2, 1+scale.Y, 0), + AnchorPoint = Vector2.new(0.5,0.5), + BackgroundTransparency = 1, + Text = "", + [Roact.Event.SelectionGained] = function(rbx) + props.RedirectLeft(rbx) + end, + }) + end + local redirectRightButton; + if props.InFocus and props.RedirectRight then + redirectRightButton = Roact.createElement('TextButton', + { + Position = UDim2.new(1+scale.X/2, 1, 0.5, 0), + Size = UDim2.new(0, 2, 1+scale.Y, 0), + AnchorPoint = Vector2.new(0.5,0.5), + BackgroundTransparency = 1, + Text = "", + [Roact.Event.SelectionGained] = function(rbx) + props.RedirectRight(rbx) + end, + }) + end + local redirectUpButton; + if props.InFocus and props.RedirectUp then + redirectUpButton = Roact.createElement('TextButton', + { + Position = UDim2.new(0.5, 0, -scale.Y/2, -1), + Size = UDim2.new(1+scale.X, 0, 0, 2), + AnchorPoint = Vector2.new(0.5,0.5), + BackgroundTransparency = 1, + Text = "", + [Roact.Event.SelectionGained] = function(rbx) + props.RedirectUp(rbx) + end, + }) + end + local redirectDownButton; + if props.InFocus and props.RedirectDown then + redirectDownButton = Roact.createElement('TextButton', + { + Position = UDim2.new(0.5, 0, 1+scale.Y/2, 1), + Size = UDim2.new(1+scale.X, 0, 0, 2), + AnchorPoint = Vector2.new(0.5,0.5), + BackgroundTransparency = 1, + Text = "", + [Roact.Event.SelectionGained] = function(rbx) + props.RedirectDown(rbx) + end, + }) + end + return Roact.createElement('ScrollingFrame', + { + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, 0, 1, 0), + AnchorPoint = Vector2.new(0.5,0.5), + BackgroundTransparency = 1, + Selectable = false, + ScrollingEnabled = false, + ScrollBarThickness = 0, + CanvasSize = UDim2.new(0, 0, 1, 0), + },{ + RedirectLeftButton = redirectLeftButton, + RedirectRightButton = redirectRightButton, + RedirectUpButton = redirectUpButton, + RedirectDownButton = redirectDownButton, + }) +end + +function RedirectComponent:willUnmount() + removeBackButton(self) +end + +return RedirectComponent \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/RoundedButton.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/RoundedButton.lua new file mode 100644 index 0000000..b0dbc98 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/RoundedButton.lua @@ -0,0 +1,206 @@ +--[[ + Creates a Roact component that is a rounded button + Props: + Button : dictionary - Config for the button. + .Image : Content - The image of the button. + .Size : UDim2 - Size of the button. + .Position : UDim2 - Position of the button. + .AnchorPoint : UDim2 - The anchor point of the button. + .ZIndex: int - Determines the order in which GUI objects are rendered, with 10 being in front and 1 in back. + .LayoutOrder: int - Controls the sorting priority of this button. + .Selectable : bool - Whether or not this object should be selectable using joysticks (controller). + + Text : dictionary - A map of props for the text + .Text : string - The label of the button. + .Size : UDim2 - Size of the button. + .Position : UDim2 - Position of the button. + .AnchorPoint : The anchor point of the button. + .Font : Font - The font used to display the given text. + .TextSize : float - The font size in pixels. + .TextXAlignment : TextXAlignment - Sets where text is placed on the X axis within the TextLabel. + .ZIndex: int - Determines the order in which GUI objects are rendered, with 10 being in front and 1 in back. + + + Focused : bool - Is the button in focus. + Disabled : bool - Is the button disabled. By default a disabled button will not be selectable. + Selected : bool - Should the button be selected. + OnSelectionGained : bool function() - + Fired when the GuiObject is being focused on with the Gamepad selector. + Return true if it should be focused. False otherwise. + OnSelectionLost : bool function() - + Fired when the Gamepad selector stops focusing on the GuiObject. + Return false if it should be un-focused. True otherwise. + OnActivated : function() - Fires when the button is activated. + + HideSelectionImage : bool - Whether or not to hide the selection object + + DefaultProps : dictionary a map for the default props of the button. + .ImageColor3 : Color3 + .ImageTransparency : float + .TextColor3 : Color3 + + FocusedProps : dictionary a map for the focused props of the button. + .ImageColor3 : Color3 + .ImageTransparency : float + .TextColor3 : Color3 + + DisabledProps : dictionary a map for the disabled props of the button. + .ImageColor3 : Color3 + .ImageTransparency : float + .TextColor3 : Color3 +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local Immutable = require(Modules.Common.Immutable) +local Utility = require(Modules.Shell.Utility) +local GlobalSettings = require(Modules.Shell.GlobalSettings) +local SoundComponent = require(Modules.Shell.Components.Common.SoundComponent) +local RoundedButton = Roact.PureComponent:extend("RoundedButton") + +function RoundedButton:init() + self.selectionImageObject = Utility.Create "ImageLabel" + { + Name = "SelectorImage", + Image = GlobalSettings.Images.ButtonSelector, + Position = UDim2.new(0, -7, 0, -7), + Size = UDim2.new(1, 14, 1, 14), + BackgroundTransparency = 1, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(31, 31, 63, 63), + } + + self.defaultProps = + { + ImageColor3 = GlobalSettings.Colors.WhiteButton, + ImageTransparency = 0.8, + TextColor3 = GlobalSettings.Colors.WhiteText, + } + self.focusedProps = + { + ImageColor3 = GlobalSettings.Colors.BlueButton, + ImageTransparency = 0, + TextColor3 = GlobalSettings.Colors.TextSelected, + } + self.disabledProps = + { + ImageColor3 = GlobalSettings.Colors.WhiteButton, + ImageTransparency = 1, + TextColor3 = GlobalSettings.Colors.WhiteText, + } + self.buttonImage = GlobalSettings.Images.ButtonDefault + + --TODO: Change to new Ref API + self.onCreate = function(rbx) + self.ref = rbx + end +end + +function RoundedButton:render() + local button = self.props.Button or {} + local text = self.props.Text or {} + + local inputDefaultProps = self.props.DefaultProps or {} + local defaultProps = {} + for k in pairs(self.defaultProps) do + defaultProps[k] = inputDefaultProps[k] or self.defaultProps[k] + end + + local inputFocusedProps = self.props.FocusedProps or {} + local focusedProps = {} + for k in pairs(self.focusedProps) do + focusedProps[k] = inputFocusedProps[k] or self.focusedProps[k] + end + + local inputDisabledProps = self.props.DisabledProps or {} + local disabledProps = {} + for k in pairs(self.disabledProps) do + disabledProps[k] = inputDisabledProps[k] or self.disabledProps[k] + end + + if self.props.HideSelectionImage then + self.selectionImageObject.Visible = false + else + self.selectionImageObject.Visible = true + end + local selectable = true + if button.Selectable == false then + selectable = false + end + local currentProps = defaultProps + if self.props.Disabled then + currentProps = disabledProps + selectable = button.Selectable or false + elseif self.props.Focused then + currentProps = focusedProps + end + local baseButtonProps = + { + Image = self.buttonImage, + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Selectable = selectable, + [Roact.Ref] = self.onCreate, + [Roact.Event.SelectionGained] = self.props.OnSelectionGained, + [Roact.Event.SelectionLost] = self.props.OnSelectionLost, + [Roact.Event.Activated] = self.props.OnActivated, + ImageColor3 = currentProps.ImageColor3, + ImageTransparency = currentProps.ImageTransparency, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + SelectionImageObject = self.selectionImageObject, + BackgroundTransparency = 1, + } + local buttonProps = Immutable.JoinDictionaries(baseButtonProps, button) + + local baseTextProps = + { + Text = "", + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Font = GlobalSettings.Fonts.Regular, + TextSize = GlobalSettings.TextSizes.Button, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = currentProps.TextColor3, + TextTransparency = currentProps.TextTransparency, + BackgroundTransparency = 1, + } + + local textProps = Immutable.JoinDictionaries(baseTextProps, text) + + local textLabel = Roact.createElement("TextLabel", textProps) + local moveSelection = Roact.createElement(SoundComponent, + { + SoundName = "MoveSelection", + } + ) + + local children = self.props[Roact.Children] or {} + + return Roact.createElement("ImageButton", + buttonProps, + Immutable.JoinDictionaries( + { + Label = textLabel, + MoveSelection = moveSelection, + }, children) + ) +end + +function RoundedButton:didMount() + delay(0, function() + if self.props.Selected then + Utility.SetSelectedCoreObject(self.ref) + end + end) +end + +function RoundedButton:didUpdate(previousProps, previousState) + if not previousProps.Selected and self.props.Selected then + Utility.SetSelectedCoreObject(self.ref) + end +end + +return RoundedButton diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/SelectorButton.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/SelectorButton.lua new file mode 100644 index 0000000..6846c45 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/SelectorButton.lua @@ -0,0 +1,109 @@ +--[[ + Creates a Roact component that is a category selector button + Props: + Key : Variant - The key for the selector button. + Text : String - The label of the button. + Size : UDim2 - The size of the this button. + AnchorPoint : Vector2 - The anchor point of the button. + Position : UDim2 - The position of the button. + LayoutOrder : int - The layout order of the button. + + Focused : bool - Is the button in focus. + Disabled : bool - Is the button disabled. By default a disabled button will not be selectable. + Selected : bool - Should the button be selected. + OnSelectionGained : function(key : Variant) - Fired when the GuiObject + is being focused on with the Gamepad selector. + OnSelectionLost : function(key : Variant) - Fired when the Gamepad selector stops focusing on the GuiObject. + OnActivated : function(key : Variant) - Fires when the button is activated. +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules + + +local Roact = require(Modules.Common.Roact) +local Immutable = require(Modules.Common.Immutable) +local GlobalSettings = require(Modules.Shell.GlobalSettings) + +local RoundedButton = require(Modules.Shell.Components.Common.RoundedButton) + +local SelectorButton = Roact.PureComponent:extend("SelectorButton") + +local SELECTOR_ICON_SIZE_X = 15 +local SELECTOR_ICON_SIZE_Y = 30 + +local SELECTOR_OFFSET_X = 14 + +function SelectorButton:init() + self.key = self.props.Key + self.onSelectionGained = function() + if self.props.OnSelectionGained then self.props.OnSelectionGained(self.key) end + end + self.onSelectionLost = function() + if self.props.OnSelectionLost then self.props.OnSelectionLost(self.key) end + end + self.onActivated = function() + if self.props.OnActivated then self.props.OnActivated(self.key) end + end +end + +function SelectorButton:render() + local buttonProps = {} + local textProps = {} + local size = self.props.Size + local position = self.props.Position + local anchorPoint = self.props.AnchorPoint + local focused = self.props.Focused + local disabled = self.props.Disabled + local selected = self.props.Selected + self.layoutOrder = self.props.LayoutOrder + + buttonProps.Size = UDim2.new(1, 0, 1, 0) + buttonProps.AnchorPoint = Vector2.new(0.5, 0.5) + buttonProps.Position = UDim2.new(0.5, 0, 0.5, 0) + + textProps.Text = self.props.Text + textProps.Size = UDim2.new(1, 0, 1, 0) + textProps.AnchorPoint = Vector2.new(0, 0.5) + textProps.Position = UDim2.new(0, 24, 0.5, 0) + + local button = Roact.createElement(RoundedButton, + { + Button = buttonProps, + Text = textProps, + Focused = focused, + Disabled = disabled, + Selected = selected, + OnSelectionGained = self.onSelectionGained, + OnSelectionLost = self.onSelectionLost, + OnActivated = self.onActivated, + }) + + local selector; + if focused == true then + selector = Roact.createElement("ImageLabel", + { + Size = UDim2.new(0, SELECTOR_ICON_SIZE_X, 0, SELECTOR_ICON_SIZE_Y), + Image = GlobalSettings.Images.RightArrow, + Position = UDim2.new(1, SELECTOR_OFFSET_X, 0.5, 0), + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + }) + end + + local children = self.props[Roact.Children] or {} + + return Roact.createElement("Frame", + { + Size = size, + Position = position, + AnchorPoint = anchorPoint, + LayoutOrder = self.layoutOrder, + BackgroundTransparency = 1, + },Immutable.JoinDictionaries( + { + Button = button, + Selector = selector, + }, children) + ) +end + +return SelectorButton \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/ShadowImage.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/ShadowImage.lua new file mode 100644 index 0000000..5672869 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/ShadowImage.lua @@ -0,0 +1,20 @@ +--[[ + Creates a component with a shadow image +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local GlobalSettings = require(Modules.Shell.GlobalSettings) + +return function(props) + return Roact.createElement("ImageLabel", { + Name = 'Shadow', + Image = GlobalSettings.Images.Shadow, + Size = UDim2.new(1,3,1,3), + Position = UDim2.new(0,0,0,0), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(10,10,28,28), + BackgroundTransparency = 1, + ZIndex = props.ZIndex or 1, + }) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/SideBar.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/SideBar.lua new file mode 100644 index 0000000..783677e --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/SideBar.lua @@ -0,0 +1,260 @@ +--[[ + Creates a component for sidebar + Props: + buttons : array An array of the buttons to be added on the sidebar. + key - string The text to be shown on the sidebar button. + value - function() A callback which will be called when the button is activated. + text: string - The text to be shown on the sidebar. + inFocus: bool - The boolean which indicates whether the sidebar is open. + selectIndex: int - The button index we try to select when the sidebar is open. + paddingTop : UDim - The padding to apply on the top side relative to the sidebar's normal size. + paddingBottom : UDim - The padding to apply on the bottom side relative to the sidebar's normal size. + displayOrder: int - The order that the sidebar ScreenGui is drawn. + onRemoveFocus : function() - Callback function when the remove focus from sidebar + onClose: function() - Callback function when the sidebar is closed. + actionPriority : int - The action priority on sidebar. +]] +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules +local Roact = require(Modules.Common.Roact) +local GlobalSettings = require(Modules.Shell.GlobalSettings) +local RoactMotion = require(Modules.LuaApp.RoactMotion) +local SoundManager = require(Modules.Shell.SoundManager) +local Components = Modules.Shell.Components +local SoundComponent = require(Modules.Shell.Components.Common.SoundComponent) +local ContextActionEvent = require(Components.ContextActionEvent) +local Utility = require(Modules.Shell.Utility) +local INSET_X = 65 +local BUTTON_SIZE_Y = 75 +local SIDEBAR_SELECTION_GROUP_NAME = "SideBar" + +local SideBar = Roact.PureComponent:extend("SideBar") +function SideBar:init() + self.groupKey = SIDEBAR_SELECTION_GROUP_NAME + self.buttonImage = GlobalSettings.Images.ButtonDefault + self.selectionImageObject = Utility.Create "ImageLabel"({ + Name = "SelectorImage", + BackgroundTransparency = 1, + Visible = false + }) + + self.defaultProps = { + buttonColor3 = GlobalSettings.Colors.WhiteButton, + buttonTransparency = 1, + textColor3 = GlobalSettings.Colors.WhiteText, + } + + self.focusedProps = { + buttonColor3 = GlobalSettings.Colors.BlueButton, + buttonTransparency = 0, + textColor3 = GlobalSettings.Colors.TextSelected, + } +end + +function SideBar:render() + local props = self.props + + local onClose = function() + self.buttons = {} + Utility.RemoveSelectionGroup(self.groupKey) + if props.onClose then + props.onClose() + end + end + + local contents = { + --Make it inside the title safe container + UIPadding = Roact.createElement("UIPadding", { + PaddingTop = props.paddingTop or UDim.new(0, 156), + PaddingBottom = props.paddingBottom or UDim.new(0, 39) + }) + } + + if props.buttons then + local index = 0 + for _, buttonObj in ipairs(props.buttons) do + index = index + 1 + local btIndex = index + local focused = self.state.selectedIndex and self.state.selectedIndex == btIndex + local currProps = focused and self.focusedProps or self.defaultProps + local btText = Roact.createElement("TextLabel", { + Size = UDim2.new(1, -INSET_X, 1, 0), + Position = UDim2.new(0, INSET_X, 0, 0), + AnchorPoint = Vector2.new(0, 0), + Text = buttonObj.text, + TextSize = GlobalSettings.TextSizes.Medium, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = currProps.textColor3, + Font = GlobalSettings.RegularFont, + BackgroundTransparency = 1, + }) + local moveSelection = Roact.createElement(SoundComponent, { + SoundName = "MoveSelection", + }) + contents["Button"..btIndex] = Roact.createElement("ImageButton", { + Image = self.buttonImage, + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.new(1, -1, 0, BUTTON_SIZE_Y), + LayoutOrder = btIndex, + ImageColor3 = currProps.buttonColor3, + ImageTransparency = currProps.buttonTransparency, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + SelectionImageObject = self.selectionImageObject, + BackgroundTransparency = 1, + [Roact.Event.SelectionGained] = function() + self:setState({ + selectedIndex = btIndex + }) + end, + [Roact.Event.SelectionLost] = function() + self:setState({ + selectedIndex = Roact.None + }) + end, + [Roact.Event.Activated] = function() + SoundManager:Play("ButtonPress") + onClose() + buttonObj.callback() + end, + [Roact.Ref] = function(bt) + self.buttons = self.buttons or {} + self.buttons[btIndex] = bt + end, + }, { + ButtonText = btText, + MoveSelection = moveSelection, + }) + end + + if index > 0 then + contents.UIListLayout = Roact.createElement("UIListLayout", { + Padding = UDim.new(0, 0), + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + }) + end + else + contents.TextLabel = Roact.createElement("TextLabel", { + Size = UDim2.new(1, -INSET_X - 100, 1, 0), + Position = UDim2.new(0, INSET_X, 0, 0), + BorderSizePixel = 0, + BackgroundTransparency = 1, + Text = props.text, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = GlobalSettings.WhiteTextColor, + Font = GlobalSettings.RegularFont, + FontSize = GlobalSettings.DescriptionSize, + TextWrapped = true, + }) + end + + local inFocus = props.inFocus + local modalBackgroundTransparency = inFocus and GlobalSettings.ModalBackgroundTransparency or 1 + local containerPositionXScale = inFocus and 0.7 or 1 + if not inFocus then + self.seenPressed = false + end + + return Roact.createElement(RoactMotion.SimpleMotion, { + defaultStyle = { + modalBackgroundTransparency = 1, + containerPositionXScale = 1, + }, + style = { + modalBackgroundTransparency = RoactMotion.spring(modalBackgroundTransparency, 600, 60), + containerPositionXScale = RoactMotion.spring(containerPositionXScale, 600, 60), + }, + onRested = not inFocus and onClose, + render = function(values) + return Roact.createElement(Roact.Portal, { target = CoreGui }, { + SideBarGui = Roact.createElement("ScreenGui", { + ZIndexBehavior = Enum.ZIndexBehavior.Sibling, + DisplayOrder = props.displayOrder or 1 + }, { + BackConnector = inFocus and Roact.createElement(ContextActionEvent, { + name = "CloseSideBar", + callback = function(actionName, inputState, inputObject) + if inputObject.KeyCode == Enum.KeyCode.ButtonB then + if inputState == Enum.UserInputState.Begin then + self.seenPressed = true + elseif inputState == Enum.UserInputState.End and self.seenPressed then + self:setState({ + selectedIndex = Roact.None + }) + if props.onRemoveFocus then + props.onRemoveFocus() + end + end + end + end, + binds = { Enum.UserInputType.Gamepad1, Enum.UserInputType.Gamepad2, Enum.UserInputType.Gamepad3, Enum.UserInputType.Gamepad4 }, + actionPriority = props.actionPriority + }), + ModalOverlay = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = values.modalBackgroundTransparency, + BackgroundColor3 = GlobalSettings.ModalBackgroundColor, + BorderSizePixel = 0, + }, { + SideBarContainer = Roact.createElement("Frame", { + Size = UDim2.new(0.3, 0, 1, 0), + Position = UDim2.new(values.containerPositionXScale, 0, 0, 0), + BorderSizePixel = 0, + BackgroundColor3 = GlobalSettings.OverlayColor, + [Roact.Ref] = function(container) + self.container = container + end + }, contents) + }) + }), + }) + end + }) +end + +function SideBar:didMount() + delay(0, function() + if self.props.inFocus and self.container then + Utility.RemoveSelectionGroup(self.groupKey) + Utility.AddSelectionParent(self.groupKey, self.container) + local trySelectIndex = self.props.selectIndex or 1 + if self.buttons and self.buttons[trySelectIndex] then + if not self.state.selectedIndex then + Utility.SetSelectedCoreObject(self.buttons[trySelectIndex]) + end + else + Utility.SetSelectedCoreObject(nil) + end + end + end) +end + +function SideBar:didUpdate(previousProps, previousState) + if self.props.inFocus == previousProps.inFocus then + return + end + if self.props.inFocus and self.container then + Utility.RemoveSelectionGroup(self.groupKey) + Utility.AddSelectionParent(self.groupKey, self.container) + local trySelectIndex = self.props.selectIndex or 1 + if self.buttons and self.buttons[trySelectIndex] then + if not self.state.selectedIndex then + Utility.SetSelectedCoreObject(self.buttons[trySelectIndex]) + end + else + Utility.SetSelectedCoreObject(nil) + end + else + Utility.RemoveSelectionGroup(self.groupKey) + end +end + +function SideBar:willUnmount() + Utility.RemoveSelectionGroup(self.groupKey) +end + +return SideBar \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/SoundComponent.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/SoundComponent.lua new file mode 100644 index 0000000..698ab8f --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/SoundComponent.lua @@ -0,0 +1,20 @@ +--[[ + A simple sound component + + Props: + SoundName : string - The name of the sound +]] +local RobloxGui = game:GetService("CoreGui").RobloxGui +local Modules = RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local GlobalSettings = require(Modules.Shell.GlobalSettings) + +return function(props) + local soundName = props.SoundName + local soundsUrl = GlobalSettings.Sounds[soundName] + return Roact.createElement('Sound', + { + SoundId = soundsUrl + }) +end diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/Spinner.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/Spinner.lua new file mode 100644 index 0000000..a1a5aea --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/Spinner.lua @@ -0,0 +1,49 @@ +--[[ + Creates a component as a spinner +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Roact = require(Modules.Common.Roact) +local Components = Modules.Shell.Components +local RenderStep = require(Components.RenderStep) +local SPINNER_IMAGE = 'rbxasset://textures/ui/Shell/Icons/LoadingSpinner@1080.png' +local Spinner = Roact.PureComponent:extend("Spinner") + +function Spinner:init() + self.state = { + rotation = 0 + } + + self.speed = 360 + + self.update = function(dt) + self:setState({ + rotation = self.state.rotation + dt * self.speed + }) + end +end + +function Spinner:render() + local props = self.props + local state = self.state + local rotation = state.rotation + self.speed = props.speed or 360 + + return Roact.createElement("ImageLabel", { + Rotation = rotation, + BackgroundTransparency = 1, + Size = props.Size or UDim2.new(0, 100, 0, 100), + AnchorPoint = props.AnchorPoint or Vector2.new(0.5, 0.5), + Position = props.Position or UDim2.new(0.5, 0, 0.5, 0), + Image = SPINNER_IMAGE, + ZIndex = props.ZIndex or 10, + ImageTransparency = props.ImageTransparency or 0, + },{ + Render = Roact.createElement(RenderStep, { + name = tick(), + priority = Enum.RenderPriority.Input.Value, + callback = self.update, + }), + }) +end + +return Spinner \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/SplitViewLR.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/SplitViewLR.lua new file mode 100644 index 0000000..7032a5e --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/SplitViewLR.lua @@ -0,0 +1,54 @@ +--[[ + Creates a Roact component provides a left and a right view + Props: + Bias : number (0 - 1) - The bias for the size of left view vs the right view + LeftView : Roact.Component - The left roact element that will be a child of the SplitView + RightView : Roact.Component - The right roact element that will be a child of the SplitView + Divider : Roact.Component - The divide roact element that will divide the two sides. +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + + +local SplitViewLR = Roact.Component:extend("SplitViewLR") + +function SplitViewLR:init() +end + +function SplitViewLR:render() + local props = self.props + local bias = props.Bias + self.leftView = props.LeftView + self.rightView = props.RightView + self.divider = props.Divider + + return Roact.createElement("Frame", + { + Size = UDim2.new(1,0,1,0), + BackgroundTransparency = 1, + },{ + LeftFrame = Roact.createElement("Frame", + { + Size = UDim2.new(bias,0,1,0), + Position = UDim2.new(0,0,0,0), + BackgroundTransparency = 1, + },{Left = self.leftView}), + Divider = Roact.createElement("Frame", + { + Size = UDim2.new(0,0,0,0), + Position = UDim2.new(bias,0,0,0), + BackgroundTransparency = 1, + },{Divider = self.divider}), + RightFrame = Roact.createElement("Frame", + { + Size = UDim2.new(1-bias,0,1,0), + Position = UDim2.new(bias,0,0,0), + BackgroundTransparency = 1, + },{ + Right = self.rightView + }), + }) +end + +return SplitViewLR \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/ToggleButton.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/ToggleButton.lua new file mode 100644 index 0000000..e29d7e0 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/ToggleButton.lua @@ -0,0 +1,116 @@ +--[[ + Create a Roact toggle button + Props: + Key : Variant - The key for the toggle button. + Text : String - The text of the button. + Size : UDim2 - The size of the this button. + AnchorPoint : Vector2 - The anchor point of the button. + Position : UDim2 - The position of the button. + + Toggle : bool - Whether or not the button is toggled or not. + Focused : bool - Is the button selected. + Disabled : bool - Whether or not this button is disabled. + Selected : bool - Should the button be selected. + Selectable : bool - Whether or not the button is selectable. + OnSelectionGained : function(key : Variant) - Fired when the GuiObject is being focused on with the Gamepad selector. + OnSelectionLost : function(key : Variant) - Fired when the Gamepad selector stops focusing on the GuiObject. + OnActivated : function() - Fires when the button is activated. +]] + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local Immutable = require(Modules.Common.Immutable) +local GlobalSettings = require(Modules.Shell.GlobalSettings) + +local RoundedButton = require(Modules.Shell.Components.Common.RoundedButton) + +local ToggleButton = Roact.Component:extend("ToggleButton") + +local TOGGLE_ICON_SIZE = 32 + +local TOGGLE_ICON_OFFSET_X = 20 + +function ToggleButton:init() + self.key = self.props.Key + self.state = + { + selected = false, + active = false, + } + self.onSelectionGained = function() + if self.props.OnSelectionGained then self.props.OnSelectionGained(self.key) end + end + self.onSelectionLost = function() + if self.props.OnSelectionLost then self.props.OnSelectionLost(self.key) end + end + self.onActivated = function() + if self.props.OnActivated then self.props.OnActivated() end + end +end + +function ToggleButton:render() + local buttonProps = {} + local textProps = {} + local size = self.props.Size + local position = self.props.Position + local anchorPoint = self.props.AnchorPoint + local focused = self.props.Focused + local disabled = self.props.Disabled + local selected = self.props.Selected + --Default is unknown since its not on or off + local iconColor = GlobalSettings.Colors.StatusIconUnknown + if self.props.Toggle == true then + iconColor = GlobalSettings.Colors.StatusIconEnabled + elseif self.props.Toggle == false then + iconColor = GlobalSettings.Colors.StatusIconDisabled + end + + buttonProps.Size = UDim2.new(1, 0, 1, 0) + buttonProps.AnchorPoint = Vector2.new(0.5, 0.5) + buttonProps.Position = UDim2.new(0.5, 0, 0.5, 0) + buttonProps.Selectable = self.props.Selectable + + textProps.Text = self.props.Text + textProps.Size = UDim2.new(1, 0, 1, 0) + textProps.AnchorPoint = Vector2.new(0, 0.5) + textProps.Position = UDim2.new(0, TOGGLE_ICON_SIZE+2*TOGGLE_ICON_OFFSET_X, 0.5, 0) + + local toggleImage = Roact.createElement("ImageLabel", + { + Size = UDim2.new(0, TOGGLE_ICON_SIZE, 0, TOGGLE_ICON_SIZE), + Image = GlobalSettings.Images.EnabledStatusIcon, + ImageColor3 = iconColor, + Position = UDim2.new(0, TOGGLE_ICON_OFFSET_X, 0.5, 0), + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + }) + + local button = Roact.createElement(RoundedButton, + { + Button = buttonProps, + Text = textProps, + Focused = focused, + Disabled = disabled, + Selected = selected, + OnSelectionGained = self.onSelectionGained, + OnSelectionLost = self.onSelectionLost, + OnActivated = self.onActivated, + },{ + ToggleImage = toggleImage, + }) + + local children = self.props[Roact.Children] or {} + return Roact.createElement("Frame", + { + Size = size, + Position = position, + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + }, Immutable.JoinDictionaries( + { + Button = button, + }, children)) +end + +return ToggleButton \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/UserThumbnailLoader.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/UserThumbnailLoader.lua new file mode 100644 index 0000000..054f45c --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/UserThumbnailLoader.lua @@ -0,0 +1,92 @@ +--[[ + Creates a Image Loader + Props: + rbxuid : int - Roblox user id. + thumbnailType : Enum.ThumbnailType - Describes the type of user thumbnail that should be returned by GetUserThumbnailAsync. + thumbnailSize : Enum.ThumbnailSize - Describes the resolution of a user thumbnail being returned by GetUserThumbnailAsync. + Size : UDim2 - The thumbnail image size. + Position : UDim2 - The thumbnail image position. + BackgroundTransparency : float - Transparency of the thumbnail image background. + BackgroundColor3 : Color3 - Color of the thumbnail image background. + hasThumbnailData : bool - Whether we have the corresponding thumbnail data in store. + imageUrl : string - The imageUrl for the thumbnail. + isFetching : bool - Whether we are fetching the thumbnail. +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Components = Modules.Shell.Components +local Roact = require(Modules.Common.Roact) +local GlobalSettings = require(Modules.Shell.GlobalSettings) +local RoactRodux = require(Modules.Common.RoactRodux) +local ApiFetchUserThumbnail = require(Modules.Shell.Thunks.ApiFetchUserThumbnail) +local Spinner = require(Components.Common.Spinner) +local memoize = require(Modules.Common.memoize) +local RETRIES = 6 + +local UserThumbnailLoader = Roact.PureComponent:extend("UserThumbnailLoader") + +function UserThumbnailLoader:render() + local props = self.props + local rbxuid = props.rbxuid + local children = {} + + local imageUrl = "" + if rbxuid and rbxuid > 0 then + local hasThumbnailData = props.hasThumbnailData + --TODO: Try refetch if last fetched failed after some interval + if hasThumbnailData and props.imageUrl then + imageUrl = props.imageUrl + else + props.fetchImage(rbxuid, props.thumbnailType, props.thumbnailSize) + end + + if props.showSpinner then + children.Spinner = props.isFetching and Roact.createElement(Spinner) + end + else + children.XboxDefaultProfileImage = Roact.createElement("ImageLabel", { + Size = UDim2.new(0.5, 0, 0.5, 0), + Position = UDim2.new(0.25, 0, 0.25, 0), + BackgroundTransparency = 1, + Image = GlobalSettings.Images.DefaultProfile, + }) + end + + return Roact.createElement("ImageLabel", { + Image = imageUrl, + Size = props.size or UDim2.new(1, 0, 1, 0), + Position = props.position or UDim2.new(0, 0, 0, 0), + BackgroundTransparency = props.backgroundTransparency or 0, + BorderSizePixel = 0, + BackgroundColor3 = props.backgroundColor3 or GlobalSettings.Colors.CharacterBackground, + }, children) +end + +local getThumbnailData = memoize(function(thumbnailData) + return { + hasThumbnailData = thumbnailData ~= nil, + isFetching = thumbnailData and thumbnailData.isFetching, + imageUrl = thumbnailData and thumbnailData.imageUrl, + } +end) + +local function mapStateToProps(state, props) + local rbxuid = props.rbxuid + local thumbnailType = props.thumbnailType + local thumbnailSize = props.thumbnailSize + local thumbnailData; + if rbxuid and rbxuid > 0 and thumbnailType and thumbnailSize then + local thumbnailId = table.concat{ rbxuid, thumbnailType.Name, thumbnailSize.Name } + thumbnailData = state.UserThumbnails[thumbnailId] + end + return getThumbnailData(thumbnailData) +end + +local function mapDispatchToProps(dispatch) + return { + fetchImage = function(rbxuid, thumbnailType, thumbnailSize) + return dispatch(ApiFetchUserThumbnail(rbxuid, thumbnailType, thumbnailSize, RETRIES)) + end + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(UserThumbnailLoader) diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/Utility/Navigator.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/Utility/Navigator.lua new file mode 100644 index 0000000..0113da0 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/Utility/Navigator.lua @@ -0,0 +1,204 @@ +--[[ + Allows the selector to navigate between different pages/levels. + + How to use: + The Navigator should be created at the root of the pages/levels. + + - Call EnterRootSection(rootKey) to set the root page of the Navigator. + + - Call Set(pageKey, defaultGuiObj) to set the default selection object for the given pageKey. + + - Call Destruct() when done using the Navigator to clean up the event connections. + + To Enter a page: + 1. Call SetNextPage(nextPageKey) to set the destination for the next page/level. + 2. Call EnterSection() to enter the next selection page/level. + Set(pageKey, defaultGuiObj) needs to be called to set the default guiobject selection. + Otherwise, the selection will fail and this function will return false. + This function can be called anywhere to enter the next level. + Eg. - Call by a GuiObject on selection. + - Call by an event. + - Call by an action with Rodux. + + To Exit a page and return to the previous: + 1. Call ExitSection() to return to the previous page/level. + This function can be called from anywhere. + It will return false if it is unable to select the next that was set +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Utility = require(Modules.Shell.Utility) +local ContextActionService = game:GetService('ContextActionService') +local Nav = {} +Nav.__index = Nav + +local function AddEnterButtonHelper(self,keycode) + local enterActionName = "EnterSection"..tostring(keycode) + self._actions.EnterSection[keycode] = enterActionName + ContextActionService:BindCoreAction(enterActionName, + function(actionName, inputState, inputObject) + return self:_enterAction(actionName, inputState, inputObject) + end + ,false,keycode) +end + +local function AddExitButtonHelper(self,keycode) + local exitActionName = "ExitSection"..tostring(keycode) + self._actions.ExitSection[keycode] = exitActionName + ContextActionService:BindCoreAction(exitActionName, + function(actionName, inputState, inputObject) + return self:_exitAction(actionName, inputState, inputObject) + end + ,false,keycode) +end + +-- By default, the ButtonB binds to back +-- Needs to be called before this object can be used. +function Nav.new() + local self = {} + self._levels = {} + self._guiobjs = {} + self._nextPageKey = "" + + self._actions = {} + self._seenPressed = {} + + self._actions.EnterSection = {} + self._actions.ExitSection = {} + + AddExitButtonHelper(self, Enum.KeyCode.ButtonB) + setmetatable(self, Nav) + return self +end + +--[[ +Sets a custom key press to enter the next page. + args: + keycode : Enum.KeyCode - The key code of the button. +]] +function Nav:AddEnterButton(keycode) + AddEnterButtonHelper(self,keycode) +end + +--[[ +Sets a custom key press to exit to the previous page. + args: + keycode : Enum.KeyCode - The key code of the button. +]] +function Nav:AddExitButton(keycode) + AddExitButtonHelper(self,keycode) +end + +--[[ +Sets and enters the root page. Must be called before the navigator can be used. + args: + rootKey : Variant - The key for the page. +]] +function Nav:EnterRootSection(rootKey) + if self._guiobjs[rootKey] == nil then + return false + end + self._levels = {} + --Stack push to the first element + table.insert(self._levels,rootKey) + Utility.SetSelectedCoreObject(self._guiobjs[rootKey]) + return true +end + +--[[ +Enters the next page + return: + True : if it can enter the next page. + False: otherwise. +]] +function Nav:EnterSection() + local nextPageKey = self._nextPageKey + if nextPageKey == nil or self._guiobjs[nextPageKey] == nil then + return false + end + --Stack push + table.insert(self._levels,nextPageKey) + Utility.SetSelectedCoreObject(self._guiobjs[nextPageKey]) + self._nextPageKey = nil + return true +end + +--[[ +Exits to the previous page + return: + True : if it can enter the previous page + False: otherwise. +]] +function Nav:ExitSection() + --stack pop + table.remove(self._levels) + -- stack peak + local key = self._levels[#self._levels] + if self._guiobjs[key] == nil then + return false + end + Utility.SetSelectedCoreObject(self._guiobjs[key]) + return true +end + +function Nav:_enterAction(actionName, inputState, inputObject) + -- There is no next page to go to + if self._nextPageKey == nil then + return Enum.ContextActionResult.Pass + end + + if inputState == Enum.UserInputState.Begin then + self._seenPressed[inputObject.KeyCode] = true + return Enum.ContextActionResult.Sink + elseif inputState == Enum.UserInputState.End and self._seenPressed[inputObject.KeyCode] then + self._seenPressed[inputObject.KeyCode] = false + self:EnterSection() + return Enum.ContextActionResult.Sink + end +end + +function Nav:_exitAction(actionName, inputState, inputObject) + --If we are current at the root page + if(#self._levels <= 1) then + -- There is nothing to go back to. + return Enum.ContextActionResult.Pass + end + + if inputState == Enum.UserInputState.Begin then + self._seenPressed[inputObject.KeyCode] = true + elseif inputState == Enum.UserInputState.End and self._seenPressed[inputObject.KeyCode] then + self._seenPressed[inputObject.KeyCode] = false + self:ExitSection() + end + return Enum.ContextActionResult.Sink +end + +--[[ +Sets the default object to select. + args: + pageKey : Variant - The key for the page. + defaultGuiObj : GuiObject - the default selected object for the page. +]] +function Nav:Set(pageKey, defaultGuiObj) + self._guiobjs[pageKey] = defaultGuiObj +end + +--[[ +Sets the key of the next page. + args: + nextPageKey : Variant - The key for the next page. +]] +function Nav:SetNextPage(nextPageKey) + self._nextPageKey = nextPageKey +end + +-- Needs to be called to clean up event connection. +function Nav:Destruct() + for _,v in pairs(self._actions.EnterSection) do + ContextActionService:UnbindCoreAction(v) + end + for _,v in pairs(self._actions.ExitSection) do + ContextActionService:UnbindCoreAction(v) + end +end + +return Nav \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/VerticalListView.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/VerticalListView.lua new file mode 100644 index 0000000..1e3e809 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/VerticalListView.lua @@ -0,0 +1,64 @@ +--[[ + Creates a Roact component provides a vertical list view for multiple elements + Props: + PaddingTop : UDim - The top padding for the list + PaddingBottom : UDim - The bottom padding for the list + Spacing : UDim - The spacing between each item of the list + HorizontalAlignment : HorizontalAlignment - Determines how grid is placed within + it's parent's container in the x direction. + Can be Left, Center, or Right. + ScrollingEnabled : bool - Determines whether or not scrolling is allowed on the frame. + If false, no scroll bars will be rendered. + ScrollBarThickness : number - How thick the scroll bar appears. + This applies to both the horizontal and vertical scroll bars. + If set to 0, no scroll bars are rendered. + CanvasPosition : Vector2 - The location within the canvas, in pixels, + that should be drawn at the top left of the scroll frame. + CanvasSize : UDim2 - Determines the size of the area that is scrollable. + The UDim2 is calculated using the parent gui's size, + similar to the regular Size property on gui objects. + Items: array - The items in the list view +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + + +return function(props) + local paddingTop = props.PaddingTop + local paddingBottom = props.PaddingBottom + local spacing = props.Spacing + local horizontalAlignment = props.HorizontalAlignment + local scrollBarThickness = props.ScrollBarThickness + local scrollingEnabled = props.ScrollingEnabled + local canvasPosition = props.CanvasPosition + local canvasSize = props.CanvasSize + local ListItems = {} + ListItems["UIPadding"] = Roact.createElement("UIPadding", + { + PaddingTop = paddingTop, + PaddingBottom = paddingBottom, + }) + ListItems["UIListLayout"] = Roact.createElement("UIListLayout", + { + Padding = spacing, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = horizontalAlignment, + VerticalAlignment = Enum.VerticalAlignment.Top, + CanvasPosition = canvasPosition, + CanvasSize = canvasSize, + }) + for k,v in pairs(props.Items) do + ListItems[k] = v + end + + return Roact.createElement("ScrollingFrame", + { + Size = UDim2.new(1,0,1,0), + ScrollBarThickness = scrollBarThickness, + BackgroundTransparency = 1, + Selectable = false, + ScrollingEnabled = scrollingEnabled, + },ListItems) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/WindowedScrollingFrame.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/WindowedScrollingFrame.lua new file mode 100644 index 0000000..3666510 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/WindowedScrollingFrame.lua @@ -0,0 +1,252 @@ +--[[ + Creates a Roact component for inifinite scrolling + Props: + scrollingFrameProps : dictionary - props for the scrolling frame + .Selectable : bool - Whether or not this object should be selectable using joysticks (controller). + .ClipsDescendants : bool - Determines whether Roblox will render any portions of its GUI descendants that are outside of its own borders. + items : Array - An array of the input item data which is used to construct item component. + itemSize : Vector2 - The size for each item in the scrolling frame. + itemsPaddingOffset : int - The padding between each item. + scrollingDirection : Enum.ScrollingDirection - The scrolling direction of the scrolling frame, can't be Enum.ScrollingDirection.XY. + itemOffsetStart: int - The minimum distance of the selected guiobject to the window start border. + itemOffsetEnd: int - The minimum distance of the selected guiobject to the window end border. + customScrollDist: dictionary - Custom distances to trigger the scroll. + generateKey : function() - Used to generate a name for the item. + renderItem : function() - + Input: item data, item index and an onSelectionGained callback + Output: a Roact Component + State: + viewStart: int - The item start index. + viewSize: int - The number of items can be put in the window. + paddingStart: int - The padding to apply on the top / left side of the scrolling frame. +]] + +local GuiService = game:GetService("GuiService") +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Utility = require(Modules.Shell.Utility) +local Roact = require(Modules.Common.Roact) +local WindowedScrollingFrame = Roact.PureComponent:extend("WindowedScrollingFrame") + + +local function clipCanvasPosition(scrollingFrame, canvasPos) + local canvasPosX = canvasPos.X + local canvasPosY = canvasPos.Y + canvasPosX = math.min(canvasPosX, scrollingFrame.CanvasSize.X.Offset - scrollingFrame.AbsoluteWindowSize.X) + canvasPosX = math.max(0, canvasPosX) + canvasPosY = math.min(canvasPosY, scrollingFrame.CanvasSize.Y.Offset - scrollingFrame.AbsoluteWindowSize.Y) + canvasPosY = math.max(0, canvasPosY) + return Vector2.new(canvasPosX, canvasPosY) +end + +function WindowedScrollingFrame:init() + self.state = { + viewStart = 0, + viewSize = 0, + paddingStart = 0, + } + + self.scrollingFrameRef = function(rbx) + self.scrollingFrame = rbx + end +end + +function WindowedScrollingFrame:onSelectionChanged(selectedItem) + if not self.scrollingFrame then + return + end + if selectedItem == nil or selectedItem == self.savedSelectedObject or not selectedItem:IsDescendantOf(self.scrollingFrame) then + return + end + self.savedSelectedObject = selectedItem + + local scrollingFrame = self.scrollingFrame + local scrollingDirection = self.props.scrollingDirection or Enum.ScrollingDirection.Y + local absoluteWindowSize = scrollingFrame.AbsoluteWindowSize + local canvasPosition = scrollingFrame.CanvasPosition + + local axisKey = "X" + if scrollingDirection == Enum.ScrollingDirection.Y then + axisKey = "Y" + end + + -- If our scrolling frame has zero height / width, let's not bother trying to + -- recompute our sizing + if absoluteWindowSize[axisKey] == 0 then + return + end + + local itemOffsetStart = self.props.itemOffsetStart or 0 + local itemOffsetEnd = self.props.itemOffsetEnd or 0 + local customScrollDist = self.props.customScrollDist or {} + --If the selected guiobject is off-window, we move it back into the window instantly + --Then make the motion + local instantPos; + local tweenTargetPos; + if scrollingDirection == Enum.ScrollingDirection.Y then + local topDistance = selectedItem.AbsolutePosition.Y - scrollingFrame.AbsolutePosition.Y + local bottomDistance = (scrollingFrame.AbsolutePosition + scrollingFrame.AbsoluteWindowSize - selectedItem.AbsolutePosition - selectedItem.AbsoluteSize).Y + + local minDistTop = itemOffsetStart + local minDistBottom = itemOffsetEnd + + if topDistance < (customScrollDist.Top or minDistTop) then + if topDistance < 0 then + instantPos = Vector2.new(canvasPosition.X, canvasPosition.Y + topDistance) + end + tweenTargetPos = Vector2.new(canvasPosition.X, canvasPosition.Y - (minDistTop - topDistance)) + elseif bottomDistance < (customScrollDist.Bottom or minDistBottom) then + if bottomDistance < 0 then + instantPos = Vector2.new(canvasPosition.X, canvasPosition.Y - bottomDistance) + end + tweenTargetPos = Vector2.new(canvasPosition.X, canvasPosition.Y + minDistBottom - bottomDistance) + end + elseif scrollingDirection == Enum.ScrollingDirection.X then + local leftDistance = selectedItem.AbsolutePosition.X - scrollingFrame.AbsolutePosition.X + local rightDistance = (scrollingFrame.AbsolutePosition + scrollingFrame.AbsoluteWindowSize - selectedItem.AbsolutePosition - selectedItem.AbsoluteSize).X + + local minDistLeft = itemOffsetStart + local minDistRight = itemOffsetEnd + + if leftDistance < (customScrollDist.Left or minDistLeft) then + if leftDistance < 0 then + instantPos = Vector2.new(canvasPosition.X + leftDistance, canvasPosition.Y) + end + tweenTargetPos = Vector2.new(canvasPosition.X - (minDistLeft - leftDistance), canvasPosition.Y) + elseif rightDistance < (customScrollDist.Right or minDistRight) then + if rightDistance < 0 then + instantPos = Vector2.new(canvasPosition.X - rightDistance, canvasPosition.Y) + end + tweenTargetPos = Vector2.new(canvasPosition.X + minDistRight - rightDistance, canvasPosition.Y) + end + end + if instantPos then + instantPos = clipCanvasPosition(scrollingFrame, instantPos) + Utility.PropertyTweener(scrollingFrame, "CanvasPosition", instantPos, instantPos, 0, Utility.EaseOutQuad, true, function() + if tweenTargetPos then + tweenTargetPos = clipCanvasPosition(scrollingFrame, tweenTargetPos) + Utility.PropertyTweener(scrollingFrame, "CanvasPosition", instantPos, tweenTargetPos, 0.2, Utility.EaseOutQuad, true) + end + end) + end + if not instantPos and tweenTargetPos then + tweenTargetPos = clipCanvasPosition(scrollingFrame, tweenTargetPos) + Utility.PropertyTweener(scrollingFrame, "CanvasPosition", canvasPosition, tweenTargetPos, 0.2, Utility.EaseOutQuad, true) + end +end + +function WindowedScrollingFrame:updateViewBounds() + if not self.scrollingFrame then + return + end + + local scrollingFrame = self.scrollingFrame + local itemSize = self.props.itemSize + local itemsPaddingOffset = self.props.itemsPaddingOffset or 0 + local scrollingDirection = self.props.scrollingDirection or Enum.ScrollingDirection.Y + local absoluteWindowSize = scrollingFrame.AbsoluteWindowSize + local canvasPosition = scrollingFrame.CanvasPosition + + local axisKey = "X" + if scrollingDirection == Enum.ScrollingDirection.Y then + axisKey = "Y" + end + + -- If our scrolling frame has zero height / width, let's not bother trying to + -- recompute our sizing + if absoluteWindowSize[axisKey] == 0 then + return + end + + canvasPosition = clipCanvasPosition(scrollingFrame, canvasPosition) + local itemTotalSize = (itemSize[axisKey] + itemsPaddingOffset) + local viewSize = math.ceil(absoluteWindowSize[axisKey] / itemTotalSize) + 1 + local viewStart = math.floor(canvasPosition[axisKey] / itemTotalSize) + local paddingStart = math.max(0, (viewStart - 1) * itemTotalSize) + local shouldUpdate = viewSize ~= self.state.viewSize or viewStart ~= self.state.viewStart or paddingStart ~= self.state.paddingStart + if shouldUpdate then + self:setState({ + viewStart = viewStart, + viewSize = viewSize, + paddingStart = paddingStart, + }) + end +end + +function WindowedScrollingFrame:render() + local items = self.props.items + local generateKey = self.props.generateKey + local renderItem = self.props.renderItem + local itemSize = self.props.itemSize + local itemsPaddingOffset = self.props.itemsPaddingOffset or 0 + local scrollingDirection = self.props.scrollingDirection or Enum.ScrollingDirection.Y + assert(scrollingDirection ~= Enum.ScrollingDirection.XY, "Can't set ScrollingDirection as XY.") + + local children = {} + + children.UIListLayout = Roact.createElement("UIListLayout", { + Padding = UDim.new(0, itemsPaddingOffset), + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = scrollingDirection == Enum.ScrollingDirection.Y and Enum.FillDirection.Vertical or Enum.FillDirection.Horizontal + }) + + if scrollingDirection == Enum.ScrollingDirection.Y then + children.UIPadding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, self.state.paddingStart) + }) + elseif scrollingDirection == Enum.ScrollingDirection.X then + children.UIPadding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, self.state.paddingStart) + }) + end + + local lowerBound = math.max(1, self.state.viewStart) + local upperBound = math.min(#items, self.state.viewStart + self.state.viewSize) + + for i = lowerBound, upperBound do + local key = generateKey and generateKey(i) or i + children[key] = renderItem(items[i], i) + end + + local scrollingFrameProps = self.props.scrollingFrameProps or {} + local canvasSize = nil + if scrollingDirection == Enum.ScrollingDirection.Y then + canvasSize = UDim2.new(1, 0, 0, #items * itemSize.Y + (#items - 1) * itemsPaddingOffset) + elseif scrollingDirection == Enum.ScrollingDirection.X then + canvasSize = UDim2.new(0, #items * itemSize.X + (#items - 1) * itemsPaddingOffset, 1, 0) + end + + return Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, 0, 1, 0), + ScrollingEnabled = false, --Don't let the default select logic affect canvas position + CanvasSize = canvasSize, + Selectable = scrollingFrameProps.selectable or false, + ScrollBarThickness = 0, + ClipsDescendants = scrollingFrameProps.clipsDescendants, + BackgroundTransparency = 1, + ScrollingDirection = scrollingDirection, + [Roact.Ref] = self.scrollingFrameRef, + [Roact.Change.CanvasPosition] = function() self:updateViewBounds() end, + [Roact.Change.AbsoluteSize] = function() self:updateViewBounds() end, + }, children) +end + +function WindowedScrollingFrame:didMount() + self:updateViewBounds() +end + +function WindowedScrollingFrame:didUpdate(prevProps, prevState) + if not prevProps.inFocus and self.props.inFocus then + self.conn = GuiService:GetPropertyChangedSignal("SelectedCoreObject"):connect(function() + self:onSelectionChanged(GuiService.SelectedCoreObject) + end) + elseif prevProps.inFocus and not self.props.inFocus then + Utility.DisconnectEvent(self.conn) + end + + if self.props ~= prevProps then + self:updateViewBounds() + end +end + +return WindowedScrollingFrame diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/ContextActionEvent.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/ContextActionEvent.lua new file mode 100644 index 0000000..f829b44 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/ContextActionEvent.lua @@ -0,0 +1,57 @@ +--[[ + A simple component that allows you to bind to ContextActionService at CoreScript level + + Props + name - the name of the binded action + callback - the function that is invoked + binds - the input that triggers the action + this is a table - example { Enum.KeyCode.ButtonA, Enum.KeyCode.ButtonX } + actionPriority - the action priority + Usage: + ContextActionCn = Roact.createElement(ContextActionEvent, { + name = "MyContextActionBind", + callback = function() print("context event") end, + binds = { Enum.KeyCode.Thumbstick2, Enum.KeyCode.ButtonB, Enum.KeyCode.A }, + actionPriority = 1, + }), + + Note: Cannot currently write a unit test for this component because it uses functions that + are RobloxScript security. LuaCore team is looking into a solution for this +]] +local ContextActionService = game:GetService("ContextActionService") +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local ContextActionEvent = Roact.Component:extend("ContextActionEvent") + +local DEFAULT_ACTION_PRIORITY = 2000 + +function ContextActionEvent:render() + return nil +end + +function ContextActionEvent:didMount() + if self.props.actionPriority then + ContextActionService:BindCoreActionAtPriority(self.props.name, self.props.callback, false, DEFAULT_ACTION_PRIORITY + self.props.actionPriority, unpack(self.props.binds)) + else + ContextActionService:BindCoreAction(self.props.name, self.props.callback, false, unpack(self.props.binds)) + end +end + +function ContextActionEvent:didUpdate(oldProps) + if self.props.callback ~= oldProps.callback or self.props.name ~= oldProps.name then + ContextActionService:UnbindCoreAction(oldProps.name) + if self.props.actionPriority then + ContextActionService:BindCoreActionAtPriority(self.props.name, self.props.callback, false, DEFAULT_ACTION_PRIORITY + self.props.actionPriority, unpack(self.props.binds)) + else + ContextActionService:BindCoreAction(self.props.name, self.props.callback, false, unpack(self.props.binds)) + end + end +end + +function ContextActionEvent:willUnmount() + ContextActionService:UnbindCoreAction(self.props.name) +end + +return ContextActionEvent diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Overscan/ButtonHint.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Overscan/ButtonHint.lua new file mode 100644 index 0000000..ede4270 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Overscan/ButtonHint.lua @@ -0,0 +1,27 @@ +--[[ + Creates a component with a button image and hint text +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local GlobalSettings = require(Modules.Shell.GlobalSettings) + +return function(props) + return Roact.createElement("ImageLabel", { + Size = UDim2.new(0, 65, 0, 65), + Position = props.Position, + BackgroundTransparency = 1, + Image = props.Image, + }, { + Text = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 0, 1, 0), + Position = UDim2.new(1, 5, 0, -3), + BackgroundTransparency = 1, + Font = GlobalSettings.RegularFont, + FontSize = GlobalSettings.ButtonSize, + TextColor3 = GlobalSettings.WhiteTextColor, + TextXAlignment = Enum.TextXAlignment.Left, + Text = props.Text, + }) + }) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Overscan/ControllerHint.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Overscan/ControllerHint.lua new file mode 100644 index 0000000..8a93b3d --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Overscan/ControllerHint.lua @@ -0,0 +1,37 @@ +--[[ + Creates a component with a gamepad image with a resize hint for the overscan screen +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local GlobalSettings = require(Modules.Shell.GlobalSettings) +local Strings = require(Modules.Shell.LocalizedStrings) + +return function(props) + return Roact.createElement("ImageLabel", { + Size = UDim2.new(0, 599, 0, 404), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = "rbxasset://textures/ui/Shell/ScreenAdjustment/Controller@1080.png", + }, { + Line = Roact.createElement("Frame", { + Size = UDim2.new(0, 240, 0, 1), + Position = UDim2.new(0, 437, 0, 220), + BackgroundTransparency = 0, + BackgroundColor3 = Color3.new(1, 1, 1), + BorderSizePixel = 0, + }, { + InputHint = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 0, 0, 0), + Position = UDim2.new(1, 3, 0, -1), + BackgroundTransparency = 1, + Font = GlobalSettings.RegularFont, + FontSize = GlobalSettings.ButtonSize, + TextColor3 = GlobalSettings.WhiteTextColor, + TextXAlignment = Enum.TextXAlignment.Left, + Text = Strings:LocalizedString("ResizeScreenInputHint"), + }) + }), + }) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Overscan/Edges.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Overscan/Edges.lua new file mode 100644 index 0000000..7c60889 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Overscan/Edges.lua @@ -0,0 +1,230 @@ +local GameOptionsSettings = settings():FindFirstChild("Game Options") +local UserInputService = game:GetService("UserInputService") +local PlatformService = nil +pcall(function() PlatformService = game:GetService("PlatformService") end) +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local Utility = require(Modules.Shell.Utility) + +local ContextActionEvent = require(Modules.Shell.Components.ContextActionEvent) +local ExternalEventConnection = require(Modules.Common.RoactUtilities.ExternalEventConnection) +local RenderStep = require(Modules.Shell.Components.RenderStep) + +local MIN_EDGE_PERCENT = Vector2.new(0.85, 0.85) +local START_EDGE_PERCENT = Vector2.new(0.9, 0.9) + +local CONSOLE_RESOLUTION = Vector2.new(1920, 1080) +local ZERO_VEC2 = Vector2.new(0,0) +local MAX_STICK_ACCELERATION = 3 +local ACCELERATION_RATE = 1 + +local DPAD_STEP_AMOUNT = 2 +local DPAD_CODE_TO_EDGE_PUSH = { + [Enum.KeyCode.DPadDown] = Vector2.new(0, DPAD_STEP_AMOUNT); + [Enum.KeyCode.DPadUp] = Vector2.new(0, -DPAD_STEP_AMOUNT); + [Enum.KeyCode.DPadLeft] = Vector2.new(-DPAD_STEP_AMOUNT, 0); + [Enum.KeyCode.DPadRight] = Vector2.new(DPAD_STEP_AMOUNT, 0); +} + +local function EdgeImage(props) + return Roact.createElement("ImageLabel", { + Size = UDim2.new(0, 95, 0, 95), + Position = props.Position, + AnchorPoint = props.AnchorPoint, + BackgroundTransparency = 1, + Rotation = props.Rotation, + Image = "rbxasset://textures/ui/Shell/ScreenAdjustment/ScreenAdjustmentArrow.png", + }) +end + +local Edges = Roact.Component:extend("Edges") + +function Edges:init() + local function getCurrentEdgePercent(newEdgePercent) + return Utility.ClampVector2(MIN_EDGE_PERCENT, Vector2.new(1, 1), newEdgePercent) + end + + local function getCurrentEdgeSize(edgePercent) + local absoluteEdgeSize = edgePercent * CONSOLE_RESOLUTION + local roundedAbsoluteEdgeSize = Vector2.new(Utility.Round(absoluteEdgeSize.X/2), Utility.Round(absoluteEdgeSize.Y/2)) * 2 + return Utility.ClampVector2(ZERO_VEC2, CONSOLE_RESOLUTION, roundedAbsoluteEdgeSize) + end + + self.onAdjustThumbstick = function(actionName, inputState, inputObject) + self._stickPosition = Utility.GamepadLinearToCurve(Vector2.new(inputObject.Position.X, -inputObject.Position.Y), 0.2) + end + + self.onAdjustDPad = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + local pushAmount = DPAD_CODE_TO_EDGE_PUSH[inputObject.KeyCode] + if pushAmount then + pushAmount = pushAmount / CONSOLE_RESOLUTION + if Utility.IsFinite(pushAmount.X) and Utility.IsFinite(pushAmount.Y) then + self._edgePercent = getCurrentEdgePercent(self._edgePercent + pushAmount) + self:setState({ + currentSize = getCurrentEdgeSize(self._edgePercent) + }) + end + end + end + end + + self.onReset = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + self._stickPosition = ZERO_VEC2 + self._edgePercent = getCurrentEdgePercent(START_EDGE_PERCENT) + self._acceleration = 1 + self:setState({ + currentSize = getCurrentEdgeSize(self._edgePercent), + }) + end + end + + self.onAccept = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + self._seenAPressed = true + elseif inputState == Enum.UserInputState.End and self._seenAPressed then + local success, err = pcall(function() + GameOptionsSettings.OverscanPX = math.min(1, self._edgePercent.X) + GameOptionsSettings.OverscanPY = math.min(1, self._edgePercent.Y) + end) + if self.props.onSetEdges then + self.props.onSetEdges() + end + end + end + + self.onRenderStep = function() + local now = tick() + if self._lastUpdate then + if self._stickPosition ~= ZERO_VEC2 then + local delta = now - self._lastUpdate + local transformedStick = (self._stickPosition) * self._acceleration * delta * 0.05 + self._edgePercent = getCurrentEdgePercent(transformedStick + self._edgePercent) + self._acceleration = math.min(self._acceleration + delta * ACCELERATION_RATE, MAX_STICK_ACCELERATION) + + self:setState({ + currentSize = getCurrentEdgeSize(self._edgePercent) + }) + else + self._acceleration = 1 + end + end + self._lastUpdate = now + end + + self.onSuspended = function() + pcall(function() + GameOptionsSettings.OverscanPX = self._lastSavedOverscan.X + GameOptionsSettings.OverscanPY = self._lastSavedOverscan.Y + end) + end + + local overscansetting = getCurrentEdgePercent(START_EDGE_PERCENT) + local startSize = getCurrentEdgeSize(overscansetting) + if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then + pcall(function() + if GameOptionsSettings.OverscanPX > 0 and GameOptionsSettings.OverscanPY > 0 then + overscansetting = Vector2.new(GameOptionsSettings.OverscanPX, GameOptionsSettings.OverscanPY) + overscansetting = getCurrentEdgePercent(overscansetting) + startSize = getCurrentEdgeSize(overscansetting) + + -- set the overscan settings to max so the user can accurately estimate their TVs overscan + -- save previous settings so we can save on suspend + self._lastSavedOverscan = Vector2.new(GameOptionsSettings.OverscanPX, GameOptionsSettings.OverscanPY) + GameOptionsSettings.OverscanPX = 1 + GameOptionsSettings.OverscanPY = 1 + end + end) + end + + self._stickPosition = ZERO_VEC2 + self._edgePercent = overscansetting + self._lastUpdate = nil + self._acceleration = 1 + self._seenAPressed = false + + self.state = { + currentSize = startSize, + } +end + +function Edges:render() + return Roact.createElement("Frame", { + Size = UDim2.new(0, self.state.currentSize.X, 0, self.state.currentSize.Y), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + }, { + SelectionImage = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 2, 1, 2), + Position = UDim2.new(0, -1, 0, -1), + BackgroundTransparency = 1, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(21, 21, 41, 41), + Image = "rbxasset://textures/ui/Shell/ScreenAdjustment/ScreenRangeOverlay.png", + }), + + TopLeft = Roact.createElement(EdgeImage, { + Rotation = 0, + Position = UDim2.new(0, 0, 0, 0), + AnchorPoint = Vector2.new(0, 0), + }), + + TopRight = Roact.createElement(EdgeImage, { + Rotation = 90, + Position = UDim2.new(1, 0, 0, 0), + AnchorPoint = Vector2.new(1, 0), + }), + + BottomRight = Roact.createElement(EdgeImage, { + Rotation = 180, + Position = UDim2.new(1, 0, 1, 0), + AnchorPoint = Vector2.new(1, 1), + }), + + BottomLeft = Roact.createElement(EdgeImage, { + Rotation = 270, + Position = UDim2.new(0, 0, 1, 0), + AnchorPoint = Vector2.new(0, 1), + }), + + Render = Roact.createElement(RenderStep, { + name = "UpdateAdjustmentScreen", + priority = Enum.RenderPriority.Input.Value, + callback = self.onRenderStep, + }), + + AdjustConnectorThumbstick = Roact.createElement(ContextActionEvent, { + name = "ThumbstickAdjustmentScreen", + callback = self.onAdjustThumbstick, + binds = { Enum.KeyCode.Thumbstick2 }, + }), + + AdjustConnectorDPad = Roact.createElement(ContextActionEvent, { + name = "DPadAdjustmentScreen", + callback = self.onAdjustDPad, + binds = { Enum.KeyCode.DPadDown, Enum.KeyCode.DPadUp, Enum.KeyCode.DPadLeft, Enum.KeyCode.DPadRight }, + }), + + ResetConnector = Roact.createElement(ContextActionEvent, { + name = "ResetAdjustmentScreen", + callback = self.onReset, + binds = { Enum.KeyCode.ButtonX }, + }), + + AcceptConnector = Roact.createElement(ContextActionEvent, { + name = "AcceptAdjustmentScreen", + callback = self.onAccept, + binds = { Enum.KeyCode.ButtonA }, + }), + + SuspendedCn = PlatformService and Roact.createElement(ExternalEventConnection, { + event = PlatformService.Suspended, + callback = self.onSuspended, + }), + }) +end + +return Edges \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Overscan/Overscan.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Overscan/Overscan.lua new file mode 100644 index 0000000..3fc51e7 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Overscan/Overscan.lua @@ -0,0 +1,80 @@ +local TextService = game:GetService('TextService') +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local GlobalSettings = require(Modules.Shell.GlobalSettings) +local Strings = require(Modules.Shell.LocalizedStrings) +local Utility = require(Modules.Shell.Utility) + +local ControllerHint = require(script.Parent.ControllerHint) +local Edges = require(script.Parent.Edges) +local ButtonHint = require(script.Parent.ButtonHint) + +local Overscan = Roact.Component:extend("Overscan") + +function Overscan:render() + -- We should really have a better API to handle text fits + local resetOffset = TextService:GetTextSize( + Strings:LocalizedString('ResetWord'), + Utility.ConvertFontSizeEnumToInt(GlobalSettings.ButtonSize), + GlobalSettings.RegularFont, + Vector2.new(0, 0) + ) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BorderSizePixel = 1, + BackgroundTransparency = self.props.BackgroundTransparency, + BackgroundColor3 = Color3.new(3/255, 3/255, 3/255), + }, { + Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BackgroundColor3 = Color3.new(3/255, 3/255, 3/255), + BorderSizePixel = 1, + Visible = self.props.ImageVisible, + Image = "rbxasset://textures/ui/Shell/ScreenAdjustment/Background.png", + }), + + Title = Roact.createElement("TextLabel", { + Position = UDim2.new(0, 230, 0, 205), + BackgroundTransparency = 1, + Font = GlobalSettings.LightFont, + FontSize = GlobalSettings.HeaderSize, + TextColor3 = GlobalSettings.WhiteTextColor, + TextXAlignment = Enum.TextXAlignment.Left, + Text = Strings:LocalizedString("ScreenSizeWord"); + }), + + Prompt = Roact.createElement("TextLabel", { + Position = UDim2.new(0, 230, 0, 243), + BackgroundTransparency = 1, + Font = GlobalSettings.RegularFont, + FontSize = GlobalSettings.ButtonSize, + TextColor3 = GlobalSettings.WhiteTextColor, + TextXAlignment = Enum.TextXAlignment.Left, + Text = Strings:LocalizedString("ResizeScreenPrompt"), + }), + + Controller = Roact.createElement(ControllerHint), + + AcceptHint = Roact.createElement(ButtonHint, { + Position = UDim2.new(0.5, 25, 0.75, 0), + Image = "rbxasset://textures/ui/Shell/ButtonIcons/AButton.png", + Text = Strings:LocalizedString('AcceptWord'), + }), + + ResetHint = Roact.createElement(ButtonHint, { + Position = UDim2.new(0.5, -25 - 65 - resetOffset.x, 0.75, 0), + Image = "rbxasset://textures/ui/Shell/ButtonIcons/XButton.png", + Text = Strings:LocalizedString('ResetWord'), + }), + + EdgeSelector = Roact.createElement(Edges, { + onSetEdges = self.props.onUnmount, + }), + }) +end + +return Overscan \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/RenderStep.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/RenderStep.lua new file mode 100644 index 0000000..f74deb0 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/RenderStep.lua @@ -0,0 +1,43 @@ +--[[ + A simple component that allows you to bind to RenderStep + + Props: + name - the name of the bind + priority - when during the render step to call the function + callback - function that will be invoked on render step + + Usage: + + RenderCn = Roact.createElement(RenderStep, { + name = "MyRenderStep", + priority = Enum.RenderPriority.Input.Value, + callback = function() print("stepping") end + }) +]] +local RunService = game:GetService("RunService") +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local RenderStep = Roact.Component:extend("RenderStep") + +function RenderStep:render() + return nil +end + +function RenderStep:didMount() + RunService:BindToRenderStep(self.props.name, self.props.priority, self.props.callback) +end + +function RenderStep:didUpdate(oldProps) + if self.props.callback ~= oldProps.callback or self.props.name ~= oldProps.name then + RunService:UnbindFromRenderStep(oldProps.name) + RunService:BindToRenderStep(self.props.name, self.props.priority, self.props.callback) + end +end + +function RenderStep:willUnmount() + RunService:UnbindFromRenderStep(self.props.name) +end + +return RenderStep \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/RenderStep.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/RenderStep.spec.lua new file mode 100644 index 0000000..8ac7e95 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/RenderStep.spec.lua @@ -0,0 +1,14 @@ +return function() + local Roact = require(game:GetService("CoreGui").RobloxGui.Modules.Common.Roact) + local RenderStep = require(script.Parent.RenderStep) + + it("should create and destroy", function() + local element = Roact.createElement(RenderStep, { + name = "myRenderStep", + priority = Enum.RenderPriority.Input.Value, + callback = function() print("hello render step") end, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/RoactScreenManagerWrapper.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/RoactScreenManagerWrapper.lua new file mode 100644 index 0000000..4e9f117 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/RoactScreenManagerWrapper.lua @@ -0,0 +1,74 @@ +--[[ + This helper module will be a bridge to help migrate the console AppShell to using Roact. + The idea is to add the interface of a "Screen" to the Roact component, so the ScreenManager + can be happy and correctly manage screens that are not using Roact. + + You will only need to wrap Roact components with this if they are the root of a route. Anything + that is a child screen/compoent of this should use Roact Routing. + + Usage: + local MyScreen = require(ShellModules.Components.MyScreen) + local myRoactScreen = RoactScreenManagerWrapper.new(MyScreen, GuiRoot, { + backgroundTransparency = 0, + name = "MyScreen", + }) + + ScreenManager:OpenScreen(myRoactScreen) +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Roact = require(Modules.Common.Roact) +local ScreenManager = require(Modules.Shell.ScreenManager) +local AppState = require(Modules.Shell.AppState) +local RoactRodux = require(Modules.Common.RoactRodux) + +local RoactScreenManagerWrapper = {} +RoactScreenManagerWrapper.__index = RoactScreenManagerWrapper + +function RoactScreenManagerWrapper.new(roactComponent, parent, props, closeCallback) + local self = {} + props = props or {} + + -- this will be passed to top level Roact components in order to close the screen + -- since we need some way to route back to the previous screen through the screen manager + local function onUnmount() + if self == ScreenManager:GetTopScreen() then + ScreenManager:CloseCurrent() + if closeCallback then + closeCallback() + end + end + end + + props.onUnmount = onUnmount + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = AppState.store, + }, { + Roact.createElement(roactComponent, props) + }) + self._instance = nil + + function self:Show() + self._instance = Roact.mount(element, parent, tostring(roactComponent)) + end + + function self:Hide() + self:Destruct() + end + + function self:Focus() + -- do nothing + end + + function self:RemoveFocus() + -- do nothing + end + + setmetatable(self, RoactScreenManagerWrapper) + return self +end + +function RoactScreenManagerWrapper:Destruct() + Roact.unmount(self._instance) +end + +return RoactScreenManagerWrapper \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Social/FriendsScrollingView.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Social/FriendsScrollingView.lua new file mode 100644 index 0000000..b4fb4ae --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Social/FriendsScrollingView.lua @@ -0,0 +1,240 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local HttpService = game:GetService("HttpService") +local PlatformService = nil +pcall(function() PlatformService = game:GetService("PlatformService") end) +local Roact = require(Modules.Common.Roact) + +local Components = Modules.Shell.Components +local PresenceCard = require(Components.Social.PresenceCard) +local WindowedScrollingFrame = require(Components.Common.WindowedScrollingFrame) +local Immutable = require(Modules.Common.Immutable) +local Spinner = require(Components.Common.Spinner) +local SideBarComponent = require(Components.Common.SideBar) +local Utility = require(Modules.Shell.Utility) +local EventHub = require(Modules.Shell.EventHub) +local GameJoinModule = require(Modules.Shell.GameJoin) +local Strings = require(Modules.Shell.LocalizedStrings) +local SoundManager = require(Modules.Shell.SoundManager) +local RedirectComponent = require(Modules.Shell.Components.Common.RedirectComponent) +local UserThumbnailLoader = require(Components.Common.UserThumbnailLoader) + +local SIDE_BAR_ITEMS = { + JoinGame = Strings:LocalizedString("JoinGameWord"), + ViewDetails = Strings:LocalizedString("ViewGameDetailsWord"), + ViewProfile = Strings:LocalizedString("ViewGamerCardWord"), + EmptyFriendSideBar = Strings:LocalizedString("EmptyFriendSideBarWord"), +} + +local FriendsScrollingView = Roact.PureComponent:extend("FriendsScrollingView") + +function FriendsScrollingView:init() + self.state = { + sideBarInFocus = false, + sideBarShow = false, + currentSelectedIndex = 1, + } + + self.onSideBarClose = function() + self:setState({ + sideBarInFocus = false, + sideBarShow = false, + }) + end + self.onSideBarOpen = function(data) + Utility.SetSelectedCoreObject(nil) + self:setState({ + sideBarInFocus = true, + sideBarShow = true, + }) + end + + self.groupKey = HttpService:GenerateGUID(false) +end + +function FriendsScrollingView:render() + local props = self.props + local friendsData = props.friendsData + local initialized = props.initialized + local actionPriority = self.props.actionPriority or 0 + + local hide = props.hide + local inFocus = props.inFocus + local sideBarInFocus = false + local friendsScrollingFrameInFocus = false + + local children = {} + if not hide and inFocus then + sideBarInFocus = self.state.sideBarShow and self.state.sideBarInFocus + if not sideBarInFocus and #friendsData > 0 then + friendsScrollingFrameInFocus = true + end + end + + if initialized and friendsData then + if #friendsData > 0 then + local itemSize = Vector2.new(440, 120) + local itemsPaddingOffset = 20 + local itemTotalSizeY = itemSize.Y + itemsPaddingOffset + local itemsCount = math.floor(self.props.size.Y.Offset / itemTotalSizeY) + assert(itemsCount ~= 0, "The scrolling window is too small to accommodate any presence card.") + --We should have at least two items to ensure we can always select the top second item + --while keep the top first item fully in view. + --If the window is too small, we will always select the top first item. + local itemOffsetStart = itemsCount > 2 and itemTotalSizeY or 0 + --This make sure when we scroll down the top card won't be clipped + local itemOffsetEnd = self.props.size.Y.Offset - itemsCount * itemTotalSizeY + itemsPaddingOffset + children.FriendsScrollingFrame = Roact.createElement(WindowedScrollingFrame, { + items = friendsData, + itemSize = itemSize, + itemsPaddingOffset = itemsPaddingOffset, + itemOffsetStart = itemOffsetStart, + itemOffsetEnd = itemOffsetEnd, + scrollingDirection = Enum.ScrollingDirection.Y, + inFocus = friendsScrollingFrameInFocus, + renderItem = function(data, index) + local presenceCardProps = Immutable.JoinDictionaries(data, { + layoutOrder = index, + size = UDim2.new(0, itemSize.X, 0, itemSize.Y), + focused = inFocus and self.state.currentSelectedIndex == index, + selected = friendsScrollingFrameInFocus and self.state.currentSelectedIndex == index, + }) + --Set up callbacks + presenceCardProps.onActivated = function(bt) + SoundManager:Play("SideMenuSlideIn") + self.onSideBarOpen() + end + presenceCardProps.onSelectionGained = function() + if self.state.currentSelectedIndex ~= index then + self:setState({ + currentSelectedIndex = index, + }) + end + end + return Roact.createElement(PresenceCard, presenceCardProps) + end + }) + + + local data = friendsData[self.state.currentSelectedIndex] + if inFocus and data and data.robloxuid then + children.ProfileImage = Roact.createElement(UserThumbnailLoader, { + rbxuid = data.robloxuid, + thumbnailType = Enum.ThumbnailType.AvatarThumbnail, + thumbnailSize = Enum.ThumbnailSize.Size352x352, + position = UDim2.new(1, 101, 0, 0), + size = UDim2.new(0, 680, 0, 680), + backgroundTransparency = 1, + showSpinner = true + }) + end + + if self.state.sideBarShow and data then + local sideBarButtons = {} + if data.robloxuid and data.robloxuid > 0 and data.robloxStatus == "InGame" then + local placeId = data.placeId + local lastLocation = data.lastLocation + local robloxuid = data.robloxuid + table.insert(sideBarButtons, { + text = SIDE_BAR_ITEMS.JoinGame, + callback = function() + GameJoinModule:StartGame(GameJoinModule.JoinType.Follow, robloxuid) + end + }) + table.insert(sideBarButtons, { + text = SIDE_BAR_ITEMS.ViewDetails, + callback = function() + EventHub:dispatchEvent(EventHub.Notifications["OpenGameDetail"], placeId, lastLocation, nil) + end + }) + end + if data.xuid and #data.xuid > 0 and PlatformService then + local xuid = data.xuid + table.insert(sideBarButtons, { + text = SIDE_BAR_ITEMS.ViewProfile, + callback = function() + -- NOTE: This will try to pop up the xbox system gamer card, failure will be handled by the xbox. + pcall(function() + PlatformService:PopupProfileUI(Enum.UserInputType.Gamepad1, xuid) + end) + end + }) + end + + local sideBarText = nil + if #sideBarButtons == 0 then + sideBarText = SIDE_BAR_ITEMS.EmptyFriendSideBar + sideBarButtons = nil + end + children.SideBar = Roact.createElement(SideBarComponent, { + actionPriority = actionPriority + 1, + text = sideBarText, + buttons = sideBarButtons, + inFocus = sideBarInFocus, + onClose = self.onSideBarClose, + onRemoveFocus = function() + self:setState({ + sideBarInFocus = false + }) + end, + }) + end + else + children.NoFriendsView = props.noFriendsView + end + else + children.Spinner = Roact.createElement(Spinner) + end + + children.NavObj = Roact.createElement(RedirectComponent, { + ActionPriority = actionPriority, + Key = self.groupKey, + InFocus = inFocus, + RedirectBack = props.redirectBack, + RedirectLeft = props.redirectLeft, + RedirectRight = props.redirectRight, + RedirectUp = props.redirectUp, + RedirectDown = props.redirectDown, + }) + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = self.props.size, + Position = self.props.position, + [Roact.Ref] = function(rbx) + self.ref = rbx + end, + Visible = not hide, + }, children) +end + + +function FriendsScrollingView:didMount() + delay(0, function() + if self.props.hide == false and self.props.inFocus then + if self.ref ~= nil then + Utility.RemoveSelectionGroup(self.groupKey) + Utility.AddSelectionParent(self.groupKey, self.ref) + end + end + end) +end + +function FriendsScrollingView:didUpdate(previousProps, previousState) + if self.props.hide or self.props.inFocus == previousProps.inFocus then + return + end + if self.props.inFocus then + if self.ref then + Utility.RemoveSelectionGroup(self.groupKey) + Utility.AddSelectionParent(self.groupKey, self.ref) + end + else + Utility.RemoveSelectionGroup(self.groupKey) + end +end + +function FriendsScrollingView:willUnmount() + Utility.RemoveSelectionGroup(self.groupKey) +end + +return FriendsScrollingView \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Social/FriendsView.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Social/FriendsView.lua new file mode 100644 index 0000000..c950b8a --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Social/FriendsView.lua @@ -0,0 +1,158 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Roact = require(Modules.Common.Roact) +local memoize = require(Modules.Common.memoize) +local RoactRodux = require(Modules.Common.RoactRodux) +local CategoryMenuView = require(Modules.Shell.Components.Common.CategoryMenuView) +local SplitViewLR = require(Modules.Shell.Components.Common.SplitViewLR) +local Utility = require(Modules.Shell.Utility) +local SoundManager = require(Modules.Shell.SoundManager) +local Strings = require(Modules.Shell.LocalizedStrings) +local FriendsScrollingView = require(Modules.Shell.Components.Social.FriendsScrollingView) +local NoFriendsView = require(Modules.Shell.Components.Social.NoFriendsView) +local PageKeys = require(Modules.Shell.PageKeys) + +local FriendsView = Roact.PureComponent:extend("FriendsView") + +local MenuKey = PageKeys.FriendCategories.Key +local FriendCategoriesKeys = { + OnlineFriends = PageKeys.FriendCategories.OnlineFriends.Key, + AllFriends = PageKeys.FriendCategories.AllFriends.Key, +} +local FriendCategories = {} +FriendCategories[FriendCategoriesKeys.OnlineFriends] = { Order = 1, StringKey = "OnlineWord" } +FriendCategories[FriendCategoriesKeys.AllFriends] = { Order = 2, StringKey = "AllWord" } + +local function onSelectSection(self, key) + if self.state.currentPage ~= key then + self:setState({ + currentPage = key, + selectedPage = MenuKey, + }) + end +end + +function FriendsView:init() + self.selectedPage = MenuKey + self.onSelectionGained = function(key) + onSelectSection(self,key) + end + + self.state = { + currentPage = FriendCategoriesKeys.OnlineFriends, + selectedPage = MenuKey, + } + + self.enterSection = function() + SoundManager:Play("OverlayOpen") + Utility.SetSelectedCoreObject(nil) + self:setState({ + selectedPage = self.state.currentPage, + }) + end + self.exitSection = function() + SoundManager:Play("PopUp") + self:setState({ + selectedPage = MenuKey, + }) + end +end + +function FriendsView:render() + local actionPriority = self.props.actionPriority or 0 + local currentPage = self.state.currentPage + local friendsViewInFocus = self.props.inFocus + local friendsViewHide = self.props.hide + local friendCategoriesMenuInFocus = false + local friendPagesInFocus = false + if not friendsViewHide and friendsViewInFocus then + if self.state.selectedPage == MenuKey then + friendCategoriesMenuInFocus = true + end + if self.state.selectedPage ~= MenuKey then + friendPagesInFocus = true + end + end + + local enterSection = self.enterSection + if currentPage == PageKeys.FriendCategories.OnlineFriends.Key and #self.props.onlineFriendsData == 0 then + enterSection = nil + elseif currentPage == PageKeys.FriendCategories.AllFriends.Key and #self.props.allFriendsData == 0 then + enterSection = nil + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, -58, 1, 0), + Position = UDim2.new(0, 58, 0, 0), + BackgroundTransparency = 1, + },{ + Mainview = Roact.createElement(SplitViewLR, { + Bias = 0.265, + LeftView = Roact.createElement(CategoryMenuView, { + Key = MenuKey, + Categories = FriendCategories, + InFocus = friendCategoriesMenuInFocus, + DefaultCategoryFocus = FriendCategoriesKeys.OnlineFriends, + OnSelectSection = self.onSelectionGained, + EnterSection = enterSection, + RedirectUp = self.props.redirectUp, + ActionPriority = actionPriority, + }), + RightView = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + },{ + OnlineFriendsView = Roact.createElement(FriendsScrollingView, { + friendsData = self.props.onlineFriendsData, + initialized = self.props.initialized, + hide = currentPage ~= PageKeys.FriendCategories.OnlineFriends.Key, + inFocus = friendPagesInFocus, + redirectLeft = self.exitSection, + redirectBack = self.exitSection, + redirectUp = self.props.redirectUp, + redirectRight = self.props.redirectRight, + size = UDim2.new(0, 440, 0, 770), + noFriendsView = Roact.createElement(NoFriendsView, { + text = Strings:LocalizedString("NoFriendsOnlinePhrase"), + }), + actionPriority = actionPriority + 1, + }), + AllFriendsView = Roact.createElement(FriendsScrollingView, { + friendsData = self.props.allFriendsData, + initialized = self.props.initialized, + hide = currentPage ~= PageKeys.FriendCategories.AllFriends.Key, + inFocus = friendPagesInFocus, + redirectLeft = self.exitSection, + redirectBack = self.exitSection, + redirectUp = self.props.redirectUp, + redirectRight = self.props.redirectRight, + size = UDim2.new(0, 440, 0, 770), + noFriendsView = Roact.createElement(NoFriendsView, { + text = Strings:LocalizedString("PlayAndMakeFriendsPhrase"), + }), + actionPriority = actionPriority + 1, + }), + }), + }), + }) +end + +local filterOnlineFriends = memoize(function(friendsData) + local onlineFriendsData = {} + for _, data in ipairs(friendsData) do + if data.robloxStatus ~= "Offline" or data.xboxStatus == "Online" then + table.insert(onlineFriendsData, data) + end + end + return onlineFriendsData +end) + +local function mapStateToProps(state, props) + local friendsData = state.RenderedFriends.data + return { + allFriendsData = friendsData, + onlineFriendsData = filterOnlineFriends(friendsData), + initialized = state.RenderedFriends.initialized + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps)(FriendsView) \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Social/NoFriendsView.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Social/NoFriendsView.lua new file mode 100644 index 0000000..87569e7 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Social/NoFriendsView.lua @@ -0,0 +1,32 @@ +--[[ + Creates a component for no friends +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local GlobalSettings = require(Modules.Shell.GlobalSettings) + +return function(props) + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1 + }, { + NoFriendsIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, 296, 0, 259), + Position = UDim2.new(0.5, -148, 0, 100), + BackgroundTransparency = 1, + Image = "rbxasset://textures/ui/Shell/Icons/FriendsIcon@1080.png", + }), + NoFriendsText = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 440, 0, 72), + Position = UDim2.new(0.5, -220, 0, 392), + BackgroundTransparency = 1, + Font = GlobalSettings.RegularFont, + FontSize = GlobalSettings.ButtonSize, + TextColor3 = GlobalSettings.WhiteTextColor, + Text = props.text, + TextYAlignment = Enum.TextYAlignment.Top, + TextWrapped = true + }), + }) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Social/PresenceCard.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Social/PresenceCard.lua new file mode 100644 index 0000000..636c24f --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Social/PresenceCard.lua @@ -0,0 +1,223 @@ +--[[ + Creates a PresenceCard component + Props: + gamertag: string - Xbox Gamertag. + robloxName: string - Roblox name. + robloxuid: int - Roblox user id. + xuid: int - Xbox user id. + robloxStatus: string - User's roblox xboxStatus. + xboxStatus: string - User's xbox xboxStatus. + lastLocation: string - User's last location info. + layoutOrder: int - Controls the sorting priority of this button. + size: UDim2 - The size of the presence card. + onSelectionGained : function(guiObject : Ref) - + Fires when the GuiObject is being focused on with the Gamepad selector. + onSelectionLost : function(guiObject : Ref) - + Fires when the Gamepad selector stops focusing on the GuiObject. + onActivated : function(guiObject : Ref) - + Fires when the button is activated. +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local GlobalSettings = require(Modules.Shell.GlobalSettings) +local Components = Modules.Shell.Components +local UserThumbnailLoader = require(Components.Common.UserThumbnailLoader) +local Strings = require(Modules.Shell.LocalizedStrings) +local Utility = require(Modules.Shell.Utility) +local SoundComponent = require(Modules.Shell.Components.Common.SoundComponent) + +local PresenceCard = Roact.PureComponent:extend("PresenceCard") + +function PresenceCard:init() + self.selectionImageObject = Utility.Create "ImageLabel"({ + Name = "SelectorImage", + Image = GlobalSettings.Images.ButtonSelector, + Position = UDim2.new(0, -7, 0, -7), + Size = UDim2.new(1, 14, 1, 14), + BackgroundTransparency = 1, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(31, 31, 63, 63), + }) + self.onCreate = function(rbx) + self.ref = rbx + end + self.defaultProps = { + buttonColor3 = GlobalSettings.Colors.WhiteButton, + buttonTransparency = 0.8, + textColor3 = GlobalSettings.Colors.WhiteText, + iconColor3 = GlobalSettings.Colors.WhiteText, + } + self.focusedProps = { + buttonColor3 = GlobalSettings.Colors.BlueButton, + buttonTransparency = 0, + textColor3 = GlobalSettings.Colors.BlackText, + iconColor3 = GlobalSettings.Colors.BlackText, + } + self.buttonImage = GlobalSettings.Images.ButtonDefault +end + +function PresenceCard:render() + local props = self.props + + local focused = self.props.focused + local currProps = focused and self.focusedProps or self.defaultProps + local gamertagText = props.gamertag or "" + local robloxNameText = props.robloxName or "" + local showGamertag = gamertagText ~= "" + local showRobloxName = robloxNameText ~= "" + local statusText = "" + local statusImageColor3 = GlobalSettings.Colors.GreySelectedButton + + local function setPresence(statusStr, statusColor) + statusImageColor3 = statusColor + if statusStr and statusStr ~= "" then + statusText = statusStr + end + end + if props.robloxStatus == "InGame" then + setPresence(props.lastLocation, GlobalSettings.Colors.GreenText) + elseif props.robloxStatus == "InStudio" then + setPresence(props.lastLocation, GlobalSettings.Colors.OrangeText) + elseif props.robloxStatus == "Online" then + setPresence("Roblox", GlobalSettings.Colors.BlueText) + else + if props.xboxStatus and props.xboxStatus == "Online" then + setPresence(Strings:LocalizedString("OnlineWord"), GlobalSettings.Colors.BlueText) + else + setPresence(Strings:LocalizedString("OfflineWord"), GlobalSettings.Colors.GreyText) + end + end + + return Roact.createElement("ImageButton", { + Image = self.buttonImage, + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + LayoutOrder = props.layoutOrder, + Size = props.size or UDim2.new(0, 440, 0, 120), + ImageColor3 = currProps.buttonColor3, + ImageTransparency = currProps.buttonTransparency, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + SelectionImageObject = self.selectionImageObject, + BackgroundTransparency = 1, + [Roact.Event.SelectionGained] = props.onSelectionGained, + [Roact.Event.SelectionLost] = props.onSelectionLost, + [Roact.Event.Activated] = props.onActivated, + [Roact.Ref] = self.onCreate, + },{ + MoveSelection = Roact.createElement(SoundComponent, { + SoundName = "MoveSelection", + }), + + AvatarImage = Roact.createElement(UserThumbnailLoader, { + rbxuid = props.robloxuid, + thumbnailType = Enum.ThumbnailType.HeadShot, + thumbnailSize = Enum.ThumbnailSize.Size100x100, + position = UDim2.new(0, 10, 0, 10), + size = UDim2.new(0, 100, 0, 100), + }), + + ContentContainer = Roact.createElement("Frame",{ + Size = UDim2.new(1, -140, 1, 0), + Position = UDim2.new(0, 126, 0, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, 10), + PaddingBottom = UDim.new(0, 10) + }), + + UIListLayout = Roact.createElement("UIListLayout", { + Padding = UDim.new(0, 7), + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + + GamertagContainer = showGamertag and Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 30), + LayoutOrder = 1, + BackgroundTransparency = 1 + },{ + GamertagLabel = Roact.createElement("TextLabel", { + Text = gamertagText, + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = currProps.textColor3, + Font = GlobalSettings.Fonts.Regular, + TextSize = 30, + TextScaled = true, + BackgroundTransparency = 1, + }), + }), + + RobloxNameContainer = showRobloxName and Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 30), + LayoutOrder = 2, + BackgroundTransparency = 1 + },{ + RobloxIcon = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Image = GlobalSettings.Images.RobloxIcon, + ImageColor3 = currProps.iconColor3, + Position = UDim2.new(0, 0, 0, 1), + Size = UDim2.new(0, 28, 0, 28), + }), + RobloxNameLabel = Roact.createElement("TextLabel", { + Text = robloxNameText, + Size = UDim2.new(1, -38, 1, 0), + Position = UDim2.new(0, 38, 0, 0), + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = currProps.textColor3, + Font = GlobalSettings.Fonts.Regular, + TextSize = 30, + TextScaled = true, + BackgroundTransparency = 1, + }) + }), + + StatusContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 26), + LayoutOrder = 3, + BackgroundTransparency = 1 + },{ + PresenceStatusImage = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Image = GlobalSettings.Images.OnlineStatusIcon, + Size = UDim2.new(0, 18, 0, 18), + Position = UDim2.new(0, 5, 0, 4), + ImageColor3 = statusImageColor3, + }), + PresenceLabel = Roact.createElement("TextLabel", { + Text = statusText, + Size = UDim2.new(1, -38, 1, 0), + Position = UDim2.new(0, 33, 0, 0), + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = currProps.textColor3, + Font = GlobalSettings.Fonts.Regular, + TextSize = 26, + BackgroundTransparency = 1, + }) + }), + }) + }) +end + +function PresenceCard:didMount() + delay(0, function() + if self.props.selected then + Utility.SetSelectedCoreObject(self.ref) + end + end) +end + +function PresenceCard:didUpdate(previousProps, previousState) + if not previousProps.selected and self.props.selected then + Utility.SetSelectedCoreObject(self.ref) + end +end + +return PresenceCard \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Components/Social/SocialPane.lua b/Client2018/content/internal/AppShell/Modules/Shell/Components/Social/SocialPane.lua new file mode 100644 index 0000000..62216f9 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Components/Social/SocialPane.lua @@ -0,0 +1,147 @@ +local GuiService = game:GetService("GuiService") +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local ShellModules = Modules:FindFirstChild("Shell") +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) +local Strings = require(ShellModules:FindFirstChild("LocalizedStrings")) +local ScreenManager = require(ShellModules.ScreenManager) +local AppState = require(ShellModules.AppState) +local Analytics = require(ShellModules:FindFirstChild("Analytics")) +local Utility = require(ShellModules:FindFirstChild("Utility")) +local Components = ShellModules.Components +local FriendsView = require(Components.Social.FriendsView) +local FriendsData = require(ShellModules.FriendsData) + +local function CreateSocialPane(parent) + local this = {} + local isPaneFocused = false + local HubContainer = parent.Parent + local UpSelector = HubContainer:FindFirstChild("TabContainer") + + local noSelectionObject = Utility.Create"ImageLabel"({ + BackgroundTransparency = 1, + }) + + local SocialPaneContainer = Utility.Create"Frame"({ + Name = "SocialPane", + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Visible = false, + SelectionImageObject = noSelectionObject, + Parent = parent, + }) + + local FriendsContainer = Utility.Create"Frame"({ + Name = "FriendsContainer", + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 23), + BackgroundTransparency = 1, + Parent = SocialPaneContainer, + }) + + local friendsScrollerInstance; + local function ReconcileFriendsScrollerInstance() + if not friendsScrollerInstance then + return + end + local friendsScroller = Roact.createElement(RoactRodux.StoreProvider, { + store = AppState.store, + }, { + FriendsView = Roact.createElement(FriendsView, { + inFocus = isPaneFocused, + hide = not SocialPaneContainer.Visible, + redirectUp = function() + Utility.SetSelectedCoreObject(UpSelector) + end, + }) + }) + friendsScrollerInstance = Roact.reconcile(friendsScrollerInstance, friendsScroller) + end + + function this:GetName() + return Strings:LocalizedString("FriendsWord") + end + + function this:IsFocused() + return isPaneFocused + end + + --[[ Public API ]]-- + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames("WidgetId")] = Analytics.WidgetNames("SocialPaneId")} + end + + function this:Show(fromAppHub) + SocialPaneContainer.Visible = true + --Suspend Friends BG Update whenever we are on GamesPane + FriendsData:SuspendUpdate() + + --We rebuild Friends Scroller only if we navigate from other tabs + if fromAppHub then + if friendsScrollerInstance then + Roact.unmount(friendsScrollerInstance) + end + local friendsScroller = Roact.createElement(RoactRodux.StoreProvider, { + store = AppState.store, + }, { + FriendsView = Roact.createElement(FriendsView, { + hide = not SocialPaneContainer.Visible, + inFocus = isPaneFocused, + redirectUp = function() + Utility.SetSelectedCoreObject(UpSelector) + end, + }) + }) + friendsScrollerInstance = Roact.mount(friendsScroller, FriendsContainer, "FriendsViewContainer") + else + ReconcileFriendsScrollerInstance() + end + ScreenManager:PlayDefaultOpenSound() + end + + function this:Hide(fromAppHub) + SocialPaneContainer.Visible = false + + --We destroy Friends Scroller if we navigate to other tabs + if fromAppHub then + if friendsScrollerInstance then + Roact.unmount(friendsScrollerInstance) + end + friendsScrollerInstance = nil + --We resume Friends Update only if we navigate to other tabs + FriendsData:ResumeUpdate() + else + ReconcileFriendsScrollerInstance() + end + end + + function this:Focus() + isPaneFocused = true + ReconcileFriendsScrollerInstance() + end + + function this:RemoveFocus() + isPaneFocused = false + ReconcileFriendsScrollerInstance() + local selectedObject = GuiService.SelectedCoreObject + if selectedObject and selectedObject:IsDescendantOf(SocialPaneContainer) then + Utility.SetSelectedCoreObject(nil) + end + end + + function this:SetPosition(newPosition) + SocialPaneContainer.Position = newPosition + end + + function this:SetParent(newParent) + SocialPaneContainer.Parent = newParent + end + + function this:IsAncestorOf(object) + return SocialPaneContainer and SocialPaneContainer:IsAncestorOf(object) + end + + return this +end + +return CreateSocialPane \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/ConfirmPrompt.lua b/Client2018/content/internal/AppShell/Modules/Shell/ConfirmPrompt.lua new file mode 100644 index 0000000..ab96616 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/ConfirmPrompt.lua @@ -0,0 +1,435 @@ +--[[ + // ConfirmPrompt.lua + // Kip Turner + // Copyright Roblox 2015 +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local ContextActionService = game:GetService("ContextActionService") +local GuiService = game:GetService('GuiService') + +local UserDataModule = require(ShellModules:FindFirstChild('UserData')) + +local AssetManager = require(ShellModules:FindFirstChild('AssetManager')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) + +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local CurrencyWidgetModule = require(ShellModules:FindFirstChild('CurrencyWidget')) + +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local MOCKUP_WIDTH = 1920 +local MOCKUP_HEIGHT = 1080 +local CONTENT_WIDTH = 1920 +local CONTENT_HEIGHT = 690 +local PACKAGE_CONTAINER_WIDTH = 780 +local PACKAGE_CONTAINER_HEIGHT = 690 +local PACKAGE_BACKGROUND_WIDTH = 580 +local PACKAGE_BACKGROUND_HEIGHT = 640 + +local CONTENT_POSITION = Vector2.new(0, 225) + +local DETAILS_CONTAINER_WIDTH = 1140 --CONTENT_WIDTH - PACKAGE_CONTAINER_WIDTH +local DETAILS_CONTAINER_HEIGHT = 690 + +local BUY_BUTTON_WIDTH = 320 +local BUY_BUTTON_HEIGHT = 64 + +local BUY_BUTTON_OFFSET = Vector2.new(0, -50) + +local function CreateConfirmPrompt(confirmDetails, properties) + local this = {} + + properties = properties or {} + + local MyParent = nil + + local InFocus = false + local Result = nil + local ResultEvent = Utility.Signal() + + local OnResultCallbacks = {} + + local productName = confirmDetails.ProductName + + local ConfirmPrompt = Utility.Create'Frame' + { + Name = "ConfirmPrompt"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + BackgroundColor3 = GlobalSettings.ModalBackgroundColor; + BorderSizePixel = 0; + } + + local ContentContainer = Utility.Create'Frame' + { + Name = "ContentContainer"; + Size = UDim2.new(CONTENT_WIDTH/MOCKUP_WIDTH, 0, CONTENT_HEIGHT/MOCKUP_HEIGHT, 0); + Position = UDim2.new(CONTENT_POSITION.x/MOCKUP_WIDTH, 0, CONTENT_POSITION.y/MOCKUP_HEIGHT, 0); + BackgroundTransparency = 0; + BackgroundColor3 = GlobalSettings.OverlayColor; + BorderSizePixel = 0; + Parent = ConfirmPrompt; + } + + local PackageContainer = Utility.Create'Frame' + { + Name = "PackageContainer"; + Size = UDim2.new(PACKAGE_CONTAINER_WIDTH/CONTENT_WIDTH, 0, PACKAGE_CONTAINER_HEIGHT/CONTENT_HEIGHT, 0); + Position = UDim2.new(0,0,0,0); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Parent = ContentContainer; + } + local PackageBackground = Utility.Create'Frame' + { + Name = "PackageBackground"; + Size = UDim2.new(PACKAGE_BACKGROUND_WIDTH/PACKAGE_CONTAINER_WIDTH, 0, PACKAGE_BACKGROUND_HEIGHT/CONTENT_HEIGHT, 0); + BackgroundTransparency = 0; + BackgroundColor3 = GlobalSettings.ForegroundGreyColor; + BorderSizePixel = 0; + ZIndex = 2; + Parent = PackageContainer; + AssetManager.CreateShadow(1); + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + } + local PackageImage = Utility.Create'ImageLabel' + { + Name = 'PackageImage'; + Size = UDim2.new(1,0,1,0); + Image = ''; + BackgroundTransparency = 1; + ZIndex = PackageBackground.ZIndex; + Parent = PackageBackground; + }; + if confirmDetails and confirmDetails.ProductImage then + PackageImage.Image = confirmDetails.ProductImage + else + PackageBackground.Visible = false + end + + + local DetailsContainer = Utility.Create'Frame' + { + Name = "DetailsContainer"; + Size = UDim2.new(DETAILS_CONTAINER_WIDTH/CONTENT_WIDTH, 0, DETAILS_CONTAINER_HEIGHT/CONTENT_HEIGHT, 0); + Position = UDim2.new(PACKAGE_CONTAINER_WIDTH/CONTENT_WIDTH,0,0,0); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Parent = ContentContainer; + } + local ConfirmTitle = Utility.Create'TextLabel' + { + Name = 'ConfirmTitle'; + Text = Strings:LocalizedString('ConfirmPurchaseTitle'); + Position = UDim2.new(0, 0, 0, 66); + Size = UDim2.new(1,0,0,25); + TextXAlignment = 'Left'; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.HeadingFont; + FontSize = GlobalSettings.HeaderSize; + BackgroundTransparency = 1; + Parent = DetailsContainer; + }; + + local formattedPackageCost = ""; + if confirmDetails.Cost then + if type(confirmDetails.Cost) == 'string' then + formattedPackageCost = confirmDetails.Cost + else + formattedPackageCost = (confirmDetails.CurrencySymbol or '') .. Utility.FormatNumberString(confirmDetails.Cost) + end + end + + local RobuxIcon = Utility.Create'ImageLabel' + { + Name = 'RobuxIcon'; + Position = UDim2.new(0,0,0,125); + Size = UDim2.new(0,(properties.ShowRobuxIcon == true) and 50 or 0,0,50); + BackgroundTransparency = 1; + Parent = DetailsContainer; + }; + if confirmDetails.Currency == "Robux" then + RobuxIcon.Image = "rbxasset://textures/ui/Shell/Icons/ROBUXIcon@1080.png" + RobuxIcon.Size = UDim2.new(0,42,0,42) + end + local PackageCost = Utility.Create'TextLabel' + { + Name = 'PackageCost'; + Text = formattedPackageCost; + Size = UDim2.new(0,0,1,0); + Position = UDim2.new(1.3,0,0,0); + TextXAlignment = 'Left'; + TextColor3 = GlobalSettings.GreenTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.HeaderSize; + BackgroundTransparency = 1; + Parent = RobuxIcon; + }; + + if confirmDetails and confirmDetails.Cost and confirmDetails.Cost == 0 then + PackageCost.Text = Strings:LocalizedString('FreeWord') + end + + + local areYouSurePhrase; + if confirmDetails.Cost and confirmDetails.Cost == 0 then + areYouSurePhrase = string.format(Strings:LocalizedString('AreYouSureTakePhrase'), tostring(productName)) + elseif properties.ConfirmWithPrice then + areYouSurePhrase = string.format(Strings:LocalizedString('AreYouSureWithPricePhrase'), tostring(productName), tostring(formattedPackageCost)) + else + areYouSurePhrase = string.format(Strings:LocalizedString('AreYouSurePhrase'), tostring(productName)) + end + + local ConfirmItemDetail = Utility.Create'TextLabel' + { + Name = 'ConfirmItemDetail'; + Text = areYouSurePhrase; + Position = UDim2.new(0, 0, 0, 205); + Size = UDim2.new(1,0,0,25); + TextXAlignment = 'Left'; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.TitleSize; + BackgroundTransparency = 1; + Parent = DetailsContainer; + }; + + local RemaningBalance = Utility.Create'TextLabel' + { + Name = 'RemaningBalance'; + Text = ''; + Position = UDim2.new(0, 0, 0, 285); + Size = UDim2.new(1,0,0,25); + TextXAlignment = 'Left'; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.TitleSize; + BackgroundTransparency = 1; + Visible = properties.ShowRemainingBalance == true; + Parent = DetailsContainer; + }; + + + + local ConfirmButton = Utility.Create'ImageButton' + { + Name = "ConfirmButton"; + Size = UDim2.new(0, BUY_BUTTON_WIDTH, 0, BUY_BUTTON_HEIGHT); --Use offset so we can resize button correctly + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.BlueButtonColor; + BackgroundTransparency = 0; + Parent = DetailsContainer; + AnchorPoint = Vector2.new(0, 1); + Position = UDim2.new(0, 0, 1 + BUY_BUTTON_OFFSET.Y/DETAILS_CONTAINER_HEIGHT, 0); + } + local ConfirmText = Utility.Create'TextLabel' + { + Name = 'ConfirmText'; + Text = Strings:LocalizedString('ConfirmWord'); + Size = UDim2.new(1,0,1,0); + TextColor3 = GlobalSettings.TextSelectedColor; + Font = GlobalSettings.HeadingFont; + FontSize = GlobalSettings.ButtonSize; + BackgroundTransparency = 1; + Parent = ConfirmButton; + }; + Utility.ResizeButtonWithText(ConfirmButton, ConfirmText, GlobalSettings.TextHorizontalPadding) + + + local function SetResult(value) + Result = value + while #OnResultCallbacks > 0 do + local callback = table.remove(OnResultCallbacks, #OnResultCallbacks) + callback(Result) + end + end + + local function Decline() + if this == ScreenManager:GetTopScreen() then + SetResult(false) + ScreenManager:CloseCurrent() + end + end + + local function Confirm() + if this == ScreenManager:GetTopScreen() then + SetResult(true) + ScreenManager:CloseCurrent() + end + end + + function this:ResultAsync() + if Result then + return Result + end + ResultEvent:wait() + return Result + end + + function this:AddResultCallback(callback) + if Result ~= nil then + callback(Result) + else + table.insert(OnResultCallbacks, callback) + end + end + + function this:FadeInBackground() + Utility.PropertyTweener(ConfirmPrompt, "BackgroundTransparency", 1, GlobalSettings.ModalBackgroundTransparency, 0.25, Utility.EaseInOutQuad, true) + end + + function this:GetDefaultSelectableObject() + return ConfirmButton + end + + function this:GetPriority() + return GlobalSettings.OverlayPriority + end + + local currencyWidget = nil + local RobuxChangedConn = nil + function this:Show() + ConfirmPrompt.Visible = true + ConfirmPrompt.Parent = ScreenManager:GetScreenGuiByPriority(self:GetPriority()) + + if self.BackgroundTween then + self.BackgroundTween:Cancel() + end + self.BackgroundTween = Utility.PropertyTweener(ConfirmPrompt, "BackgroundTransparency", 1, GlobalSettings.ModalBackgroundTransparency, 0, Utility.EaseInOutQuad, nil) + SoundManager:Play('OverlayOpen') + + local function onPackageBackgroundResize() + local rawImageSize = Vector2.new(420, 420) + if confirmDetails.ProductImageSize then + rawImageSize = confirmDetails.ProductImageSize + end + PackageImage.Size = Utility.CalculateFill(PackageBackground, rawImageSize) + PackageImage.AnchorPoint = Vector2.new(0.5, 0.5) + PackageImage.Position = UDim2.new(0.5, 0, 0.5, 0) + end + + self.PackageBackgroundChangedConn = Utility.DisconnectEvent(self.PackageBackgroundChangedConn) + self.PackageBackgroundChangedConn = PackageBackground:GetPropertyChangedSignal('AbsoluteSize'):connect(function() + onPackageBackgroundResize() + end) + onPackageBackgroundResize() + + local function onBalanceLoaded(newBalance) + local balance = newBalance + if properties.ShowRemainingBalance then + if balance and confirmDetails and confirmDetails.Cost then + if confirmDetails.Cost and confirmDetails.Cost > 0 then + local newBalance = balance and confirmDetails.Cost and balance - confirmDetails.Cost + RemaningBalance.Text = string.format(Strings:LocalizedString('RemainingBalancePhrase'), Utility.FormatNumberString(tostring(newBalance))); + else + RemaningBalance.Text = '' + end + end + end + end + + if confirmDetails.Balance then + onBalanceLoaded(confirmDetails.Balance) + else + spawn(function() + local balance = UserDataModule.GetPlatformUserBalanceAsync() + onBalanceLoaded(balance) + end) + end + + if not currencyWidget then + currencyWidget = CurrencyWidgetModule({Parent = ConfirmPrompt; Position = UDim2.new(0.052, 0, 0.88, 0); }) + end + Utility.DisconnectEvent(RobuxChangedConn) + RobuxChangedConn = currencyWidget.RobuxChanged:connect(onBalanceLoaded) + + end + + function this:Hide() + ConfirmPrompt.Visible = false + ConfirmPrompt.Parent = nil + + if self.BackgroundTween then + self.BackgroundTween:Cancel() + end + self.BackgroundTween = nil + + self.PackageBackgroundChangedConn = Utility.DisconnectEvent(self.PackageBackgroundChangedConn) + RobuxChangedConn = Utility.DisconnectEvent(RobuxChangedConn) + end + + function this:ScreenRemoved() + if currencyWidget then + currencyWidget:Destroy() + currencyWidget = nil + end + if Result == nil then + SetResult(false) + end + ResultEvent:fire(Result) + end + + function this:GetAnalyticsInfo() + return + { + [Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('ConfirmPromptId'); + ProductName = confirmDetails.ProductName; + --Here the ProductId refers to assetid if it's a roblox product, + --refers to productId if it's a Xbox product + ProductId = confirmDetails.ProductId; + } + end + + function this:Focus() + InFocus = true + ContextActionService:BindCoreAction("ReturnFromCurrentConfirmScreen", + function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + Decline() + end + end, + false, + Enum.KeyCode.ButtonB) + + local isConfirmingPurchase = false + self.ConfirmButtonConn = Utility.DisconnectEvent(self.ConfirmButtonConn) + self.ConfirmButtonConn = ConfirmButton.MouseButton1Click:connect(function() + if isConfirmingPurchase then return end + isConfirmingPurchase = true + SoundManager:Play('ButtonPress') + Confirm() + isConfirmingPurchase = false + end) + + GuiService:AddSelectionParent("ConfirmOptionsSelectionGroup", ContentContainer) + if InFocus and GuiService.SelectedCoreObject == nil then + Utility.SetSelectedCoreObject(self:GetDefaultSelectableObject()) + end + end + + function this:RemoveFocus() + ContextActionService:UnbindCoreAction("ReturnFromCurrentConfirmScreen") + GuiService:RemoveSelectionGroup("ConfirmOptionsSelectionGroup") + self.ConfirmButtonConn = Utility.DisconnectEvent(self.ConfirmButtonConn) + Utility.SetSelectedCoreObject(nil) + InFocus = false + end + + + function this:SetParent(parent) + MyParent = parent + ConfirmPrompt.Parent = MyParent + end + + + return this +end + +return CreateConfirmPrompt diff --git a/Client2018/content/internal/AppShell/Modules/Shell/ControllerStateManager.lua b/Client2018/content/internal/AppShell/Modules/Shell/ControllerStateManager.lua new file mode 100644 index 0000000..d58be3d --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/ControllerStateManager.lua @@ -0,0 +1,180 @@ +--[[ + // ControllerStateManager.lua + + // Handles controller state changes +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) +local ThirdPartyUserService = nil +pcall(function() ThirdPartyUserService = game:GetService('ThirdPartyUserService') end) +local UserInputService = game:GetService('UserInputService') +local GuiService = game:GetService('GuiService') + +local Http = require(ShellModules:FindFirstChild('Http')) +local NoActionOverlay = require(ShellModules:FindFirstChild('NoActionOverlay')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local Alerts = require(ShellModules:FindFirstChild('Alerts')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local XboxAppState = require(ShellModules:FindFirstChild('AppState')) + +local ControllerStateManager = {} + +local LostUserGamepadCn = nil +local GainedUserGamepadCn = nil + +local noActionOverlay = nil + +local DATAMODEL_TYPE = { + APP_SHELL = 0; + GAME = 1; +} + + +local PRESENCE_POLL_INTERVAL = Utility.GetFastVariable("XboxPresencePolling") + +local AnyButtonBeganConnection = nil +local SelectionChangedConnection = nil +local ViewChangedConnection = nil +local AnyActionDone = false +local LastTimerInfo = {flag = true}; + +local function restartPresenceUpdateTimer() + LastTimerInfo.flag = false; + + local info = { flag = true } + + spawn(function() + AnyActionDone = true + while info.flag do + if AnyActionDone then + Http:RegisterAppPresence() + end + AnyActionDone = false + wait(PRESENCE_POLL_INTERVAL) + end + end) + + Utility.DisconnectEvent(AnyButtonBeganConnection) + AnyButtonBeganConnection = UserInputService.InputBegan:connect(function(inputObject) + AnyActionDone = true + end) + + Utility.DisconnectEvent(SelectionChangedConnection) + SelectionChangedConnection = GuiService:GetPropertyChangedSignal('SelectedCoreObject'):connect(function() + AnyActionDone = true + end) + + LastTimerInfo = info +end + +local function stopPresenceUpdateTimer() + LastTimerInfo.flag = false; + Utility.DisconnectEvent(AnyButtonBeganConnection) + Utility.DisconnectEvent(SelectionChangedConnection) +end + +local function closeOverlay(dataModelType) + if not noActionOverlay then return end + if dataModelType == DATAMODEL_TYPE.GAME then + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None + --Close overlay, don't focus on below screen + noActionOverlay:Hide() + else + ScreenManager:CloseCurrent() + end + noActionOverlay = nil +end + +local function showErrorOverlay(alert, dataModelType) + if noActionOverlay then return end + noActionOverlay = NoActionOverlay(alert) + if dataModelType == DATAMODEL_TYPE.GAME then + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide + noActionOverlay:Show() + else + ScreenManager:OpenScreen(noActionOverlay, false) + end +end + +local function onLostUserGamepad(dataModelType) + local userDisplayName = "" + if dataModelType == DATAMODEL_TYPE.APP_SHELL then + userDisplayName = XboxAppState.store:getState().XboxUser.gamertag + else + if ThirdPartyUserService then + userDisplayName = ThirdPartyUserService:GetUserDisplayName() + end + end + + -- create alert + local alert = Alerts.LostConnection["Controller"] + alert.Msg = string.format(alert.Msg, userDisplayName) + showErrorOverlay(alert, dataModelType) +end + +local function onGainedUserGamepad(dataModelType) + closeOverlay(dataModelType) +end + +local function disconnectEvents() + LostUserGamepadCn = Utility.DisconnectEvent(LostUserGamepadCn) + GainedUserGamepadCn = Utility.DisconnectEvent(GainedUserGamepadCn) +end + +local function initAppPresenceReporting() + restartPresenceUpdateTimer() + + Utility.DisconnectEvent(ViewChangedConnection) + ViewChangedConnection = PlatformService.ViewChanged:connect( + function(value) + if value == DATAMODEL_TYPE.APP_SHELL then + restartPresenceUpdateTimer() + else + stopPresenceUpdateTimer() + end + end + ) +end + +--ControllerStateManager initialized from AppHome.lua(when in app shell)/NotificationScript2.lua(when in game) +--Note: The ControllerStateManager module scirpt is required on different DMs (app shell & game). +--Thus, whenever we enter a new game, this module will run once as it's required on a new game DM. +function ControllerStateManager:Initialize() + if not PlatformService then return end + + local dataModelType = PlatformService.DatamodelType + + disconnectEvents() + if ThirdPartyUserService then + LostUserGamepadCn = ThirdPartyUserService.ActiveGamepadRemoved:connect(function() + onLostUserGamepad(dataModelType) + end) + GainedUserGamepadCn = ThirdPartyUserService.ActiveGamepadAdded:connect(function() + onGainedUserGamepad(dataModelType) + end) + end + + if dataModelType == DATAMODEL_TYPE.APP_SHELL then + EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "AchievementManager", + function() + initAppPresenceReporting(); + end + ) + end +end + +function ControllerStateManager:CheckUserConnected() + if not PlatformService then return end + + local isGamepadConnected = UserInputService:GetGamepadConnected(Enum.UserInputType.Gamepad1) + if not isGamepadConnected then + onLostUserGamepad(PlatformService.DatamodelType) + end +end + +return ControllerStateManager diff --git a/Client2018/content/internal/AppShell/Modules/Shell/CurrencyWidget.lua b/Client2018/content/internal/AppShell/Modules/Shell/CurrencyWidget.lua new file mode 100644 index 0000000..7a27692 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/CurrencyWidget.lua @@ -0,0 +1,254 @@ +--[[ + // CurrencyWidget.lua by Kip Turner +--]] + +local TextService = game:GetService('TextService') +local PlatformService; +pcall(function() PlatformService = game:GetService('PlatformService') end) + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local UserDataModule = require(ShellModules:FindFirstChild('UserData')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) + +local EventHubConnectCount = 0 + +local InternalPlatformRobuxAmountChangedSignal = Utility.Signal() +local InternalTotalRobuxAmountChangedSignal = Utility.Signal() + +local function CreateCurrencyWidget(properties) + properties = properties or {} + + local this = {} + + this.RobuxChanged = Utility.Signal() + + local internalPlatformRobuxChangedConn = nil + local internalTotalRobuxChangedConn = nil + + local CachedTotalRobuxValue = nil + local CachedRobuxValue = nil + local destroyed = false + + EventHubConnectCount = EventHubConnectCount + 1 + local myEventId = "CurrencyWidget" .. tostring(EventHubConnectCount) + + local RobuxBalanceTitle = Utility.Create'TextLabel' + { + Name = 'RobuxBalanceTitle'; + Size = UDim2.new(0,0,0,0); + Position = properties.Position or UDim2.new(0, 0, 0, 0); + TextXAlignment = 'Left'; + TextYAlignment = 'Top'; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = Strings:LocalizedString('RobuxBalanceTitle') .. ':'; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.HeaderSize; + BackgroundTransparency = 1; + ZIndex = properties.ZIndex or 2; + Parent = properties.Parent or nil; + }; + local robuxTitleSize = TextService:GetTextSize(RobuxBalanceTitle.Text, Utility.ConvertFontSizeEnumToInt(RobuxBalanceTitle.FontSize), RobuxBalanceTitle.Font, Vector2.new()) + RobuxBalanceTitle.Size = UDim2.new(0, robuxTitleSize.X, 0, robuxTitleSize.Y) + + local RobuxBalanceIcon = Utility.Create'ImageLabel' + { + Name = 'RobuxIcon'; + Size = UDim2.new(0,46,0,46); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/ROBUXIconOutlined@1080.png'; + ZIndex = properties.ZIndex or 2; + Parent = RobuxBalanceTitle; + AnchorPoint = Vector2.new(0, 0.5); + Position = UDim2.new(1, 17, 0.5, 0); + }; + local RobuxBalanceValue = Utility.Create'TextLabel' + { + Name = 'RobuxBalanceValue'; + Size = UDim2.new(0,0,1,0); + Position = UDim2.new(1,5,0,-2); + Text = ''; + TextXAlignment = 'Left'; + TextYAlignment = 'Center'; + TextColor3 = GlobalSettings.GreenTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.HeaderSize; + BackgroundTransparency = 1; + ZIndex = properties.ZIndex or 2; + TextTransparency = 1; + Parent = RobuxBalanceIcon; + }; + + + local function UpdateBalanceText() + local balanceValueString = CachedRobuxValue and Utility.FormatNumberString(tostring(CachedRobuxValue)) or '-' + local balanceStringWidth = TextService:GetTextSize(balanceValueString, Utility.ConvertFontSizeEnumToInt(RobuxBalanceValue.FontSize), RobuxBalanceValue.Font, Vector2.new()) + RobuxBalanceValue.Size = UDim2.new(0, balanceStringWidth.X, 1, 0) + RobuxBalanceValue.Text = balanceValueString + if RobuxBalanceValue.TextTransparency == 1 and CachedRobuxValue ~= nil then + Utility.PropertyTweener(RobuxBalanceValue, 'TextTransparency', 1, 0, 0.5, Utility.EaseOutQuad, true) + end + end + + + local robuxAmountChangedLoader = nil + local ConsumePurchasedConn = nil + if PlatformService then + local robuxChangedEventCount = 0 + + local function fetchNewRobuxAsync(thisEventCount) + if not (CachedRobuxValue and CachedTotalRobuxValue) then + this:RefreshRobuxAmountAsync() + end + local prepurchaseRobux = CachedRobuxValue + local prepurchaseTotalRobux = CachedTotalRobuxValue + Utility.ExponentialRepeat( + function() return thisEventCount == robuxChangedEventCount and not destroyed end, + function() + local balance = UserDataModule.GetPlatformUserBalanceAsync() + local totalBalance = UserDataModule.GetTotalUserBalanceAsync() + if balance and totalBalance and not destroyed then + if balance ~= prepurchaseRobux and totalBalance ~= prepurchaseTotalRobux then + CachedRobuxValue = balance + CachedTotalRobuxValue = totalBalance + UpdateBalanceText() + if this.RobuxChanged ~= nil then + this.RobuxChanged:fire(balance) + end + return true + end + end + end + ) + end + + local function OnRobuxAmountChanged() + robuxChangedEventCount = robuxChangedEventCount + 1 + local thisEventCount = robuxChangedEventCount + + if robuxAmountChangedLoader then + robuxAmountChangedLoader:Cleanup() + robuxAmountChangedLoader = nil + end + local loader = LoadingWidget({Parent = RobuxBalanceValue, Size = UDim2.new(0,50,0,50), Position = UDim2.new(1,75,0,25)}, {function() fetchNewRobuxAsync(thisEventCount) end}) + robuxAmountChangedLoader = loader + robuxAmountChangedLoader:AwaitFinished() + if robuxAmountChangedLoader and loader == robuxAmountChangedLoader then + robuxAmountChangedLoader:Cleanup() + robuxAmountChangedLoader = nil + end + end + + Utility.DisconnectEvent(ConsumePurchasedConn) + ConsumePurchasedConn = PlatformService.ConsumePurchased:connect(function(platformPurchaseResult, purchasedConsumablesInfo) + --Update Robux regardless of purchased type + if platformPurchaseResult == 3 then + OnRobuxAmountChanged() + else + if robuxAmountChangedLoader then + robuxAmountChangedLoader:Cleanup() + robuxAmountChangedLoader = nil + end + end + end) + + EventHub:addEventListener(EventHub.Notifications["RobuxCatalogPurchaseInitiated"], myEventId, function() + OnRobuxAmountChanged() + end) + end + + function this:GetAbsoluteSize() + return (RobuxBalanceValue.AbsolutePosition + RobuxBalanceValue.AbsoluteSize) - (RobuxBalanceTitle.AbsolutePosition) + end + + function this:GetRobuxAmount() + return CachedRobuxValue + end + + function this:RefreshRobuxAmountAsync() + local beforeTotalRobuxValue = CachedTotalRobuxValue + local beforeRobuxValue = CachedRobuxValue + + UserDataModule.GetLocalUserIdAsync() + CachedTotalRobuxValue = UserDataModule.GetTotalUserBalanceAsync() + CachedRobuxValue = UserDataModule.GetPlatformUserBalanceAsync() + UpdateBalanceText() + + if beforeTotalRobuxValue ~= CachedTotalRobuxValue then + InternalTotalRobuxAmountChangedSignal:fire(CachedTotalRobuxValue) + end + if beforeRobuxValue ~= CachedRobuxValue then + InternalPlatformRobuxAmountChangedSignal:fire(CachedRobuxValue) + end + end + + local GetRobuxAmountAsyncTempWidget = nil + function this:GetRobuxAmountAsync() + if CachedRobuxValue then + return CachedRobuxValue + end + UserDataModule.GetLocalUserIdAsync() + spawn(function() + wait(1) + if CachedRobuxValue == nil and GetRobuxAmountAsyncTempWidget == nil then + GetRobuxAmountAsyncTempWidget = LoadingWidget({Parent = RobuxBalanceValue, Size = UDim2.new(0,50,0,50), Position = UDim2.new(1,75,0,25)}, {function() while CachedRobuxValue == nil do wait() end end}) + GetRobuxAmountAsyncTempWidget:AwaitFinished() + if GetRobuxAmountAsyncTempWidget then + GetRobuxAmountAsyncTempWidget:Cleanup() + end + GetRobuxAmountAsyncTempWidget = nil + end + end) + self:RefreshRobuxAmountAsync() + return CachedRobuxValue + end + + function this:Destroy() + destroyed = true + RobuxBalanceTitle.Parent = nil + self.RobuxChanged = nil + if robuxAmountChangedLoader then + robuxAmountChangedLoader:Cleanup() + robuxAmountChangedLoader = nil + end + ConsumePurchasedConn = Utility.DisconnectEvent(ConsumePurchasedConn) + internalPlatformRobuxChangedConn = Utility.DisconnectEvent(internalPlatformRobuxChangedConn) + internalTotalRobuxChangedConn = Utility.DisconnectEvent(internalTotalRobuxChangedConn) + EventHub:removeEventListener(EventHub.Notifications["RobuxCatalogPurchaseInitiated"], myEventId) + end + + function this:GetGuiObject() + return RobuxBalanceTitle + end + + internalPlatformRobuxChangedConn = InternalPlatformRobuxAmountChangedSignal:connect( + function(newPlatformRobux) + if newPlatformRobux ~= CachedRobuxValue then + CachedRobuxValue = newPlatformRobux + UpdateBalanceText() + this.RobuxChanged:fire(CachedRobuxValue) + end + end) + internalTotalRobuxChangedConn = InternalTotalRobuxAmountChangedSignal:connect( + function(newTotalRobux) + if newTotalRobux ~= CachedTotalRobuxValue then + CachedTotalRobuxValue = newTotalRobux + this.RobuxChanged:fire(CachedRobuxValue) + end + end) + + spawn(function() + this:GetRobuxAmountAsync() + end) + + return this +end + +return CreateCurrencyWidget diff --git a/Client2018/content/internal/AppShell/Modules/Shell/DisableCrossplayOverlay.lua b/Client2018/content/internal/AppShell/Modules/Shell/DisableCrossplayOverlay.lua new file mode 100644 index 0000000..4375c12 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/DisableCrossplayOverlay.lua @@ -0,0 +1,146 @@ +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local BaseOverlay = require(ShellModules:FindFirstChild('BaseOverlay')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local function createDisableCrossplayOverlay(overlayInfo) + local this = BaseOverlay() + + local title = overlayInfo.Title + local message = overlayInfo.Msg + local callback = overlayInfo.Callback + + this:SetImage( + Utility.Create'ImageLabel' + { + Name = "ReportIcon"; + Position = UDim2.new(0, 226, 0, 204); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Shell/Icons/ErrorIconLargeCopy@1080.png"; + Size = UDim2.new(0,321,0,264); + } + ) + + local titleText = Utility.Create'TextLabel' + { + Name = "TitleText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, this.RightAlign, 0, 136); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.HeaderSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = title; + TextXAlignment = Enum.TextXAlignment.Left; + Parent = this.Container; + } + + local descriptionText = Utility.Create'TextLabel' + { + Name = "DescriptionText"; + Size = UDim2.new(0, 762, 0, 304); + Position = UDim2.new(0, this.RightAlign, 0, titleText.Position.Y.Offset + 62); + BackgroundTransparency = 1; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.TitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextWrapped = true; + Text = message; + Parent = this.Container; + } + + local keepEnabledButton = Utility.Create'TextButton' + { + Name = "KeepEnabledButton"; + Position = UDim2.new(0, this.RightAlign, 1, -100 - 66); + Size = UDim2.new(0, 320, 0, 66); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.BlueButtonColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = GlobalSettings.BlackTextColor; + Text = Strings:LocalizedString("KeepEnabledPhrase"); + Parent = this.Container; + + SoundManager:CreateSound('MoveSelection'); + } + Utility.ResizeButtonWithText(keepEnabledButton, keepEnabledButton, GlobalSettings.TextHorizontalPadding) + + local disableButton = Utility.Create'TextButton' + { + Name = "OkButton"; + Position = UDim2.new(0, keepEnabledButton.Position.X.Offset + keepEnabledButton.Size.X.Offset + 10, 1, -100 - 66); + Size = UDim2.new(0, 320, 0, 66); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.GreyButtonColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = Strings:LocalizedString("DisableWord"); + Parent = this.Container; + + SoundManager:CreateSound('MoveSelection'); + } + Utility.ResizeButtonWithText(disableButton, disableButton, GlobalSettings.TextHorizontalPadding) + + keepEnabledButton.SelectionGained:connect(function() + keepEnabledButton.BackgroundColor3 = GlobalSettings.GreySelectedButtonColor + keepEnabledButton.TextColor3 = GlobalSettings.TextSelectedColor + end) + keepEnabledButton.SelectionLost:connect(function() + keepEnabledButton.BackgroundColor3 = GlobalSettings.GreyButtonColor + keepEnabledButton.TextColor3 = GlobalSettings.WhiteTextColor + end) + + disableButton.SelectionGained:connect(function() + disableButton.BackgroundColor3 = GlobalSettings.GreySelectedButtonColor + disableButton.TextColor3 = GlobalSettings.TextSelectedColor + end) + disableButton.SelectionLost:connect(function() + disableButton.BackgroundColor3 = GlobalSettings.GreyButtonColor + disableButton.TextColor3 = GlobalSettings.WhiteTextColor + end) + + + --[[ Input Events ]]-- + function this:GetAnalyticsInfo() + return + { + [Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('DisableCrossplayOverlayId'); + Title = overlayInfo.Title; + } + end + + disableButton.MouseButton1Click:connect(function() + if this:Close() then + callback() + end + end) + + keepEnabledButton.MouseButton1Click:connect(function() + this:Close() + end) + + local baseFocus = this.Focus + function this:Focus() + baseFocus(self) + Utility.SetSelectedCoreObject(keepEnabledButton) + end + + function this:GetOverlaySound() + return 'Error' + end + + return this +end + +return createDisableCrossplayOverlay diff --git a/Client2018/content/internal/AppShell/Modules/Shell/EnableCrossplayOverlay.lua b/Client2018/content/internal/AppShell/Modules/Shell/EnableCrossplayOverlay.lua new file mode 100644 index 0000000..a988a3c --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/EnableCrossplayOverlay.lua @@ -0,0 +1,107 @@ +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local BaseOverlay = require(ShellModules:FindFirstChild('BaseOverlay')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local function createEnableCrossplayOverlay(overlayInfo) + local this = BaseOverlay() + + local title = overlayInfo.Title + local message = overlayInfo.Msg + local callback = overlayInfo.Callback + + this:SetImage( + Utility.Create'ImageLabel' + { + Name = "AlertIcon"; + Size = UDim2.new(0, 416, 0, 416); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/AlertIcon.png'; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + } + ) + + local titleText = Utility.Create'TextLabel' + { + Name = "TitleText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, this.RightAlign, 0, 136); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.HeaderSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = title; + TextXAlignment = Enum.TextXAlignment.Left; + Parent = this.Container; + } + + local descriptionText = Utility.Create'TextLabel' + { + Name = "DescriptionText"; + Size = UDim2.new(0, 762, 0, 304); + Position = UDim2.new(0, this.RightAlign, 0, titleText.Position.Y.Offset + 62); + BackgroundTransparency = 1; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.TitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextWrapped = true; + Text = message; + Parent = this.Container; + } + + local okButton = Utility.Create'TextButton' + { + Name = "OkButton"; + Position = UDim2.new(0, this.RightAlign, 1, -100 - 66); + Size = UDim2.new(0, 320, 0, 66); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.BlueButtonColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = GlobalSettings.BlackTextColor; + Text = Strings:LocalizedString("OkWord"); + Parent = this.Container; + + SoundManager:CreateSound('MoveSelection'); + } + Utility.ResizeButtonWithText(okButton, okButton, GlobalSettings.TextHorizontalPadding) + + --[[ Input Events ]]-- + function this:GetAnalyticsInfo() + return + { + [Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('EnableCrossplayOverlayId'); + Title = overlayInfo.Title; + } + end + + okButton.MouseButton1Click:connect(function() + if this:Close() then + callback() + end + end) + + local baseFocus = this.Focus + function this:Focus() + baseFocus(self) + Utility.SetSelectedCoreObject(okButton) + end + + function this:GetOverlaySound() + return 'OverlayOpen' + end + + return this +end + +return createEnableCrossplayOverlay diff --git a/Client2018/content/internal/AppShell/Modules/Shell/EngagementScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/EngagementScreen.lua new file mode 100644 index 0000000..63c0423 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/EngagementScreen.lua @@ -0,0 +1,339 @@ +local CoreGui = game:GetService("CoreGui") +local UserInputService = game:GetService("UserInputService") +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) +local ThirdPartyUserService = nil +pcall(function() ThirdPartyUserService = game:GetService("ThirdPartyUserService") end) +local AnalyticsService = game:GetService("AnalyticsService") + +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local AppState = require(ShellModules.AppState) +local AccountManager = require(ShellModules:FindFirstChild('AccountManager')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Errors = require(ShellModules:FindFirstChild('Errors')) +local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local SetAccountCredentialsScreen = require(ShellModules:FindFirstChild('SetAccountCredentialsScreen')) +local SignInScreen = require(ShellModules:FindFirstChild('SignInScreen')) +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local ACCEPTED_KEY_CODES = +{ + [Enum.KeyCode.ButtonA] = true; + [Enum.KeyCode.ButtonX] = true; +} + +local GAMEPAD_INPUT_TYPES = +{ + [Enum.UserInputType.Gamepad1] = true; + [Enum.UserInputType.Gamepad2] = true; + [Enum.UserInputType.Gamepad3] = true; + [Enum.UserInputType.Gamepad4] = true; +} + +local RegisterActiveUserResult = +{ + Unknown = -1; + Success = 0; + NoUser = 1; +} + +local AccountPickResult = +{ + Unknown = -1; + Success = 0; + NoUser = 1; +} + +local function CreateHomePane(parent) + local this = {} + + local ButtonBeganConnection, ButtonEndedConnection = nil + local EngagementHintText = string.gsub( + Strings:LocalizedString('EngagementHint'), + " {Button_A} ", " ") + + local EngagementScreenContainer = Utility.Create'Frame' + { + Name = 'EngagementScreen'; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Parent = parent; + } + + local RobloxLogo = Utility.Create'ImageLabel' + { + Name = 'RobloxLogo'; + BackgroundTransparency = 1; + Size = UDim2.new(0, 594, 0, 209); + Image = 'rbxasset://textures/ui/Shell/Icons/SplashLogo.png'; + Parent = EngagementScreenContainer; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + } + + local EngagementHint = Utility.Create'TextLabel' + { + Name = 'EngagementHint'; + AnchorPoint = Vector2.new(0.5, 0); + Position = UDim2.new(0.5, 0, 0, 331); + Size = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + Text = EngagementHintText; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + Parent = RobloxLogo; + } + + local EngagementIndicatorImage = Utility.Create'ImageLabel' + { + Name = 'EngagementIndicator'; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0, 105, 0.5, 0); + Size = UDim2.new(0, 58, 0, 58); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/ButtonIcons/AButtonEngagementScreen.png'; + Parent = EngagementHint; + } + + local SwitchAccountHint = Utility.Create'TextLabel' + { + Name = 'SwitchAccountHint'; + AnchorPoint = Vector2.new(1, 1); + Size = UDim2.new(0, 260, 0, 38); + Position = UDim2.new(1, 0, 1, -13); + BackgroundTransparency = 1; + Text = Strings:LocalizedString('SwitchAccountHint'); + TextColor3 = GlobalSettings.WhiteTextColor; + TextXAlignment = 'Right'; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + Parent = EngagementScreenContainer; + } + + local SwitchAccountIndicatorImage = Utility.Create'ImageLabel' + { + Name = 'SwitchAccountIndicator'; + AnchorPoint = Vector2.new(1, 0.5); + Position = UDim2.new(0, 0, 0.5, 0); + Size = UDim2.new(0, 58, 0, 58); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/ButtonIcons/XButtonEngagementScreen.png'; + Parent = SwitchAccountHint; + } + + local function adjustTextsAndImages() + local startIndex = string.find(EngagementHintText, " ") + if startIndex >= 1 then + local leftSize = 0 + if startIndex > 1 then + EngagementHint.Text = string.sub(EngagementHintText, 1, startIndex - 1) + Utility.ResizeButtonWithText(EngagementHint, EngagementHint, 0, 0) + leftSize = EngagementHint.Size.X.Offset + end + + EngagementHint.Text = " " + Utility.ResizeButtonWithText(EngagementHint, EngagementHint, 0, 0) + local imageXOffset = EngagementHint.Size.X.Offset/2 + leftSize + + EngagementIndicatorImage.Position = UDim2.new(0, imageXOffset, 0.5, 0); + + EngagementHint.Text = EngagementHintText + Utility.ResizeButtonWithText(EngagementHint, EngagementHint, 0, 0) + end + + Utility.ResizeButtonWithText(SwitchAccountHint, SwitchAccountHint, 0, 0) + end + adjustTextsAndImages() + + local function displayErrorScreen(err) + if err == nil then + err = Errors.Default + end + ScreenManager:OpenScreen(ErrorOverlay(err), false) + end + + local function setXboxUserState() + local userInfo = {} + if ThirdPartyUserService then + userInfo.gamertag = ThirdPartyUserService:GetUserDisplayName() + userInfo.xuid = ThirdPartyUserService:GetUserPlatformId() + else + userInfo.gamertag = "InStudioNoGamertag" + userInfo.xuid = -1 + end + + local SetXboxUser = require(ShellModules.Actions.SetXboxUser) + AppState.store:dispatch(SetXboxUser(userInfo)) + end + + -- new flow with new service + -- TODO: Will need to revist this if we move other calls into new services as errors may be handled differently + local function beginAuthenticationAsync2(gamepad) + if UserSettings().GameSettings:InStudioMode() then + setXboxUserState() + EventHub:dispatchEvent(EventHub.Notifications["AuthenticationSuccess"]) + return + end + + local function loginAsync() + -- register active user with user paired to gamepad + local success, result = pcall(function() + return ThirdPartyUserService:RegisterActiveUser(gamepad) + end) + -- catch pcall API error + if not success then + displayErrorScreen(Errors.RegisterActiveUser[RegisterActiveUserResult.Unknown]) + return + end + if result ~= RegisterActiveUserResult.Success then + displayErrorScreen(Errors.RegisterActiveUser[result]) + return + end + + setXboxUserState() + + -- check for linked account + local hasLinkedAccountResult = AccountManager:HasLinkedAccountAsync() + if hasLinkedAccountResult == AccountManager.AuthResults.AccountUnlinked then + AnalyticsService:ReportCounter("Xbox_SignUp_Start") + + local signInScreen = SignInScreen() + signInScreen:SetParent(EngagementScreenContainer.Parent) + ScreenManager:OpenScreen(signInScreen, true) + return + elseif hasLinkedAccountResult ~= AccountManager.AuthResults.Success then + displayErrorScreen(Errors.Authentication[hasLinkedAccountResult]) + return + end + + -- login user + local loginResult = AccountManager:LoginAsync() + if loginResult == AccountManager.AuthResults.Success then + EventHub:dispatchEvent(EventHub.Notifications["AuthenticationSuccess"]) + return + elseif result == AccountManager.AuthResults.UsernamePasswordNotSet then + local setAccountCredentialsScreen = SetAccountCredentialsScreen(Strings:LocalizedString("SetCredentialsTitle"), + Strings:LocalizedString("SetCredentialsPhrase"), Strings:LocalizedString("SetCredentialsWord")) + setAccountCredentialsScreen:SetParent(EngagementScreenContainer.Parent) + ScreenManager:OpenScreen(setAccountCredentialsScreen, true) + return + else + displayErrorScreen(Errors.Authentication[loginResult]) + return + end + end + + local loader = LoadingWidget( + { Parent = RobloxLogo, Position = UDim2.new(0.5, 0, 0, 415) }, { loginAsync }) + loader:AwaitFinished() + loader:Cleanup() + end + + local function onButtonAPressed(gamePad) + ButtonBeganConnection = Utility.DisconnectEvent(ButtonBeganConnection) + ButtonEndedConnection = Utility.DisconnectEvent(ButtonEndedConnection) + EngagementHint.TextColor3 = GlobalSettings.WhiteTextColor + Utility.PropertyTweener(EngagementHint, 'TextTransparency', 0, 1, 0.25, Utility.EaseOutQuad, true, + function() + beginAuthenticationAsync2(gamePad) + end) + Utility.PropertyTweener(EngagementIndicatorImage, 'ImageTransparency', 0, 1, 0.25, Utility.EaseOutQuad, true) + Utility.PropertyTweener(SwitchAccountHint, 'TextTransparency', 0, 1, 0.25, Utility.EaseOutQuad, true) + Utility.PropertyTweener(SwitchAccountIndicatorImage, 'ImageTransparency', 0, 1, 0.25, Utility.EaseOutQuad, true) + end + + local function showAccountPicker(gamePad) + local success, result = pcall(function() + return PlatformService:ShowAccountPicker(gamePad) + end) + -- catch pcall API error + if not success or result == AccountPickResult.Unknown then + displayErrorScreen(Errors.RegisterActiveUser[AccountPickResult.Unknown]) + return AccountPickResult.Unknown + end + return result + end + + function this:Show() + EngagementScreenContainer.Visible = true + end + + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('EngagementScreenId')} + end + + function this:Hide() + EngagementScreenContainer.Visible = false + end + + function this:Focus() + EngagementHint.TextColor3 = GlobalSettings.WhiteTextColor + EngagementHint.TextTransparency = 0 + SwitchAccountHint.TextTransparency = 0 + EngagementIndicatorImage.ImageTransparency = 0 + SwitchAccountIndicatorImage.ImageTransparency = 0 + + Utility.DisconnectEvent(ButtonBeganConnection) + local buttonDown = {} + ButtonBeganConnection = UserInputService.InputBegan:connect(function(inputObject) + if GAMEPAD_INPUT_TYPES[inputObject.UserInputType] then + if inputObject.KeyCode == Enum.KeyCode.ButtonA then + EngagementHint.TextColor3 = GlobalSettings.GreyTextColor + buttonDown[inputObject.KeyCode] = true + elseif inputObject.KeyCode == Enum.KeyCode.ButtonX then + buttonDown[inputObject.KeyCode] = true + end + end + end) + Utility.DisconnectEvent(ButtonEndedConnection) + local isAuthenticating = false + ButtonEndedConnection = UserInputService.InputEnded:connect(function(inputObject) + if isAuthenticating then return end + isAuthenticating = true + if GAMEPAD_INPUT_TYPES[inputObject.UserInputType] then + if ACCEPTED_KEY_CODES[inputObject.KeyCode] and buttonDown[inputObject.KeyCode] == true then + SoundManager:Play('ButtonPress') + if inputObject.KeyCode == Enum.KeyCode.ButtonA then + EngagementHint.TextColor3 = GlobalSettings.GreyTextColor + buttonDown[inputObject.KeyCode] = true + onButtonAPressed(inputObject.UserInputType) + elseif inputObject.KeyCode == Enum.KeyCode.ButtonX then + if PlatformService then + local showAccountPickerResult = showAccountPicker(inputObject.UserInputType) + if showAccountPickerResult == AccountPickResult.Success then + EngagementHint.TextColor3 = GlobalSettings.GreyTextColor + buttonDown[inputObject.KeyCode] = true + onButtonAPressed(inputObject.UserInputType) + end + end + end + end + end + isAuthenticating = false + buttonDown[inputObject.KeyCode] = false + end) + end + + function this:RemoveFocus() + ButtonBeganConnection = Utility.DisconnectEvent(ButtonBeganConnection) + ButtonEndedConnection = Utility.DisconnectEvent(ButtonEndedConnection) + end + + function this:SetParent(newParent) + EngagementScreenContainer.Parent = newParent + end + + return this +end + +return CreateHomePane diff --git a/Client2018/content/internal/AppShell/Modules/Shell/ErrorOverlay.lua b/Client2018/content/internal/AppShell/Modules/Shell/ErrorOverlay.lua new file mode 100644 index 0000000..b3a57c0 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/ErrorOverlay.lua @@ -0,0 +1,153 @@ +--[[ + // ErrorOverlay.lua + + // Creates and error overlay + + // NOTE: Right now error and alerts look the same, so we're + // using the same module to make both. If in the future this + // changes, we'll need to move alert to it's own module. +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local GuiService = game:GetService('GuiService') + +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local BaseOverlay = require(ShellModules:FindFirstChild('BaseOverlay')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) + +local createErrorOverlay = function(errorType) + if not errorType then + return + end + + local this = BaseOverlay() + + local title = errorType.Title + local message = errorType.Msg + --That how we distinguish error and alert, errors have a Code table entry while alerts have a Id table entry. + local errorCode = errorType.Code + local alertId = errorType.Id + + local iconImage = Utility.Create'ImageLabel' + { + Name = "IconImage"; + BackgroundTransparency = 1; + } + iconImage.Image = alertId and 'rbxasset://textures/ui/Shell/Icons/AlertIcon.png' or + 'rbxasset://textures/ui/Shell/Icons/ErrorIconLargeCopy@1080.png' + iconImage.Size = alertId and UDim2.new(0, 416, 0, 416) or UDim2.new(0, 321, 0, 264) + iconImage.AnchorPoint = Vector2.new(0.5, 0.5) + iconImage.Position = UDim2.new(0.5, 0, 0.5, 0) + this:SetImage(iconImage) + + local titleText = Utility.Create'TextLabel' + { + Name = "TitleText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, this.RightAlign, 0, 136); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.HeaderSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = title; + TextXAlignment = Enum.TextXAlignment.Left; + Parent = this.Container; + } + + local descriptionText = Utility.Create'TextLabel' + { + Name = "DescriptionText"; + Size = UDim2.new(0, 762, 0, 304); + Position = UDim2.new(0, this.RightAlign, 0, titleText.Position.Y.Offset + 62); + BackgroundTransparency = 1; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.TitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextWrapped = true; + Text = message; + Parent = this.Container; + } + if errorCode then + descriptionText.Text = string.format(Strings:LocalizedString('ErrorMessageAndCodePrase'), message, errorCode) + end + + local okButton = Utility.Create'TextButton' + { + Name = "OkButton"; + Size = UDim2.new(0, 320, 0, 66); + Position = UDim2.new(0, this.RightAlign, 1, -100 - 66); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.BlueButtonColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = GlobalSettings.TextSelectedColor; + Text = Strings:LocalizedString("OkWord"); + Parent = this.Container; + + SoundManager:CreateSound('MoveSelection'); + } + + --if the user can't join game b/c xbox settings, the button will open the xbox account settings page + local goToSettings = errorCode and (errorCode == 113 or errorCode == 116) + if goToSettings then + okButton.Text = Strings:LocalizedString("GoToSettingsPhrase"); + end + + Utility.ResizeButtonWithText(okButton, okButton, GlobalSettings.TextHorizontalPadding) + + -- Override + function this:GetAnalyticsInfo() + local analyticsInfo = {} + analyticsInfo[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('ErrorOverlayId') + analyticsInfo.Title = errorType.Title + if errorCode then + analyticsInfo.ErrorCode = errorCode + end + if alertId then + analyticsInfo.AlertId = alertId + end + return analyticsInfo + end + + function this:GetPriority() + return GlobalSettings.ElevatedPriority + end + + --[[ Input Events ]]-- + okButton.MouseButton1Click:connect(function() + this:Close() + if goToSettings then + EventHub:dispatchEvent(EventHub.Notifications["OpenAccountSettingsScreen"], errorCode) + end + end) + local baseFocus = this.Focus + function this:Focus() + baseFocus(this) + GuiService.SelectedCoreObject = okButton + end + + function this:GetOverlaySound() + return 'Error' + end + + -- Track ErrorCode Frequency + if errorCode then + Analytics.ReportCounter("Error-"..tostring(errorCode), 1) + Analytics.SetRBXEventStream("Error", {ErrorCode = errorCode}) + end + if alertId then + Analytics.ReportCounter("Alert-"..tostring(alertId), 1) + Analytics.SetRBXEventStream("Alert", {AlertId = alertId}) + end + return this +end + +return createErrorOverlay diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Errors.lua b/Client2018/content/internal/AppShell/Modules/Shell/Errors.lua new file mode 100644 index 0000000..8437556 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Errors.lua @@ -0,0 +1,148 @@ +--[[ + // Errors.lua + + // Global error codes, each error has an unique code +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) + +local Errors = +{ + Default = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("DefaultErrorPhrase"), Code = 0 }; + + GameJoin = + { + -- index mapped to error code returned from c++ + [-1] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("DefaultJoinFailPhrase"), Code = 112 }; + [1] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("AlreadyRunningPhrase"), Code = 101 }; + [2] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("WebServerConnectFailPhrase"), Code = 102 }; + [3] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("AccessDeniedByWeb"), Code = 103 }; + [4] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("InstanceNotFound"), Code = 104 }; + [5] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("GameFullPhrase"), Code = 105 }; + [6] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("FollowUserFailed"), Code = 106 }; + [7] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("InvalidPrivilegeMultiplayerSessionPhrase"), Code = 107 }; + [8] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("InvalidPrivilegeUGCPhrase"), Code = 108 }; + + -- following are new codes that 102 has been split into. This is to help drill down what is causing the recent 102 spike + -- 109 - out of retries + [9] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("AccessDeniedByWeb"), Code = 109 }; + -- 110 Exception in requestPlaceInfo (http exception or json parsing error) + [10] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("AccessDeniedByWeb"), Code = 110 }; + -- 111 http exception when calling join script + [11] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("AccessDeniedByWeb"), Code = 111 }; + + --Split 107 into 3 new error codes + --113 User's setting blocks Multiplayer Session + [12] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("MPSRestrictedPhrase"), Code = 113 }; + --114 Get banned from Multiplayer Session + [13] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("MPSBannedPhrase"), Code = 114 }; + --115 Don't have Xbox Live Gold + [14] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("MPSPurchaseRequiredPhrase"), Code = 115 }; + + --Split 108 into 2 new error codes + --116 User's setting blocks UGC + [15] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("UGCRestrictedPhrase"), Code = 116 }; + --117 Get banned from UGC + [16] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("UGCBannedPhrase"), Code = 117 }; + + --118 If crash happens when we check privilege + [17] = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("PrivilegeCheckFailPhrase"), Code = 118 }; + + Default = { Title = Strings:LocalizedString("UnableToJoinTitle"), Msg = Strings:LocalizedString("DefaultJoinFailPhrase"), Code = 112 }; + }; + + Vote = + { + FloodCheckThresholdMet = { Title = Strings:LocalizedString("CannotVoteTitle"), Msg = Strings:LocalizedString("VoteFloodPhrase"), Code = 201 }; + PlayGame = { Title = Strings:LocalizedString("CannotVoteTitle"), Msg = Strings:LocalizedString("VotePlayGamePhrase"), Code = 202 }; + }; + + Favorite = + { + Failed = { Title = Strings:LocalizedString("CannotFavoriteTitle"), Msg = Strings:LocalizedString("DefaultErrorPhrase"), Code = 301 }; + FloodCheck = { Title = Strings:LocalizedString("CannotFavoriteTitle"), Msg = Strings:LocalizedString("FavoriteFloodPhrase"), Code = 302 }; + }; + + Test = + { + CannotJoinGame = { Title = "An Error Occured", Msg = "Cannot join games from studio.", Code = 401 }; + StillInDev = { Title = "An Error Occured", Msg = "This feature is still in development.", Code = 402 }; + FeatureNotAvailableInStudio = { Title = "An Error Occured", Msg = "This feature is not available in Roblox Studio.", Code = 403 }; + }; + + PackageEquip = + { + Default = { Title = Strings:LocalizedString("UnableToEquipTitle"), Msg = Strings:LocalizedString("UnableToEquipPhrase"), Code = 501 }; + }; + + OutfitEquip = + { + Default = { Title = Strings:LocalizedString("UnableToWearOufitTitle"), Msg = Strings:LocalizedString("UnableToWearOufitPhrase"), Code = 601 }; + }; + + PackagePurchase = + { + { Title = Strings:LocalizedString("UnableToDoPurchaseTitle"), Msg = Strings:LocalizedString("UnableToDoPurchasePhrase"), Code = 701 }; + }; + + RobuxPurchase = + { + { Title = Strings:LocalizedString("UnableToDoRobuxPurchaseTitle"), Msg = Strings:LocalizedString("UnableToDoRobuxPurchasePhrase"), Code = 801 }; + }; + + Authentication = + { + -- index mapped to int error code from c++ + [-1] = { Title = Strings:LocalizedString("AuthenticationErrorTitle"), Msg = Strings:LocalizedString("AuthErrorPhrase"), Code = 901 }; + -- ["0"]; This is success + [1] = { Title = Strings:LocalizedString("AuthenticationErrorTitle"), Msg = Strings:LocalizedString("AuthInProgressPhrase"), Code = 902 }; + [2] = { Title = Strings:LocalizedString("AuthenticationErrorTitle"), Msg = Strings:LocalizedString("AuthAccountUnlinkedPhrase"), Code = 903 }; + [3] = { Title = Strings:LocalizedString("AuthenticationErrorTitle"), Msg = Strings:LocalizedString("AuthMissingGamePadPhrase"), Code = 904 }; + [4] = { Title = Strings:LocalizedString("AuthenticationErrorTitle"), Msg = Strings:LocalizedString("AuthNoUserDetectedPhrase"), Code = 905 }; + [5] = { Title = Strings:LocalizedString("AuthenticationErrorTitle"), Msg = Strings:LocalizedString("AuthHttpErrorDetected"), Code = 906 }; + [6] = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("LinkSignUpDisabled"), Code = 907 }; + [7] = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("LinkFlooded"), Code = 908 }; + [8] = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("LinkLeaseLocked"), Code = 909 }; + [9] = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("LinkAccountLinkingDisabled"), Code = 910 }; + [10] = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("LinkInvalidRobloxUser"), Code = 911 }; + [11] = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("LinkRobloxUserAlreadyLinked"), Code = 912 }; + [12] = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("LinkXboxUserAlreadyLinked"), Code = 913 }; + [13] = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("LinkIllegalChildAccountLinking"), Code = 914 }; + [14] = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("LinkInvalidPassword"), Code = 915 }; + [15] = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("LinkUsernamePasswordNotSet"), Code = 916 }; + [16] = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("LinkUsernameAlreadyTaken"), Code = 917 }; + [17] = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("LinkInvalidCredentials"), Code = 918 }; + [18] = { Title = Strings:LocalizedString("AuthenticationErrorTitle"), Msg = Strings:LocalizedString("UserIsGuestAccount"), Code = 919 }; + }; + + RegisterActiveUser = + { + [-1] = { Title = Strings:LocalizedString("AuthenticationErrorTitle"), Msg = Strings:LocalizedString("AuthErrorPhrase"), Code = 1301 }; + -- ["0"]; This is success + [1] = { Title = Strings:LocalizedString("AuthenticationErrorTitle"), Msg = Strings:LocalizedString("AuthNoUserDetectedPhrase"), Code = 1302 }; + }; + + SignIn = + { + InvalidUsername = { Title = Strings:LocalizedString("InvalidUsernameTitle"), Msg = Strings:LocalizedString("InvalidUsernamePhrase"), Code = 1101 }; + InvalidPassword = { Title = Strings:LocalizedString("InvalidPasswordTitle"), Msg = Strings:LocalizedString("InvalidPasswordPhrase"), Code = 1102 }; + NoUsernameOrPasswordEntered = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("NoUsernameOrPasswordEnteredPhrase"), Code = 1107 }; + ["ConnectionFailed"] = { Title = Strings:LocalizedString("AuthenticationErrorTitle"), Msg = Strings:LocalizedString("WebServerConnectFailPhrase"), Code = 1108 }; + }; + + PlatformError = + { + PopupPartyUI = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("PopupPartyUIErrorPhrase"), Code = 1201 }; + }; + + CPPSettingError = + { + SetCPPSettingError = { Title = Strings:LocalizedString("ErrorOccurredTitle"), Msg = Strings:LocalizedString("SetCPPSettingErrorPhrase"), Code = 1401 }; + }; +} + +return Errors diff --git a/Client2018/content/internal/AppShell/Modules/Shell/EventHub.lua b/Client2018/content/internal/AppShell/Modules/Shell/EventHub.lua new file mode 100644 index 0000000..bf5c9d8 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/EventHub.lua @@ -0,0 +1,96 @@ +-- Written by Kyler Mulherin, Copyright Roblox 2015 + +local listeners = {} +--listeners is a table that holds arrays of listener objects +--Ex - listeners["login"] = { Listener , Listener, Listener } + +local function createListener(idString, callbackFunction) + local Listener = { id = idString , callback = callbackFunction }; + return Listener; +end + +--Initialize all the functions for the EventHub +local EventHub = {} +do + function EventHub:addEventListener(eventString, objectIDString, callbackFunction) + if (listeners[eventString] == nil) then + listeners[eventString] = {} + end + + table.insert(listeners[eventString], createListener(objectIDString, callbackFunction)) + end + function EventHub:removeEventListener(eventString, objectIDString) + if (listeners[eventString] == nil) then return end + + --iterate through the listeners for an event string, remove all of the listeners with the provided objectIDString + for key, value in ipairs(listeners[eventString]) do + local listener = value + if (listener ~= nil) then + if (listener.id == objectIDString) then + table.remove(listeners[eventString], key) + end + end + end + end + function EventHub:removeCallbackFromEvent(eventString, objectIDString, callbackFunction) + --NOTE- Will not work with anonymous functions + if (listeners[eventString] == nil) then return end + + --iterate through the listeners for an event string, remove the one with the provided objectIDString and callback function + for key, value in ipairs(listeners[eventString]) do + local listener = value + if (listener ~= nil) then + if (listener.id == objectIDString) and (listener.callback == callbackFunction) then + table.remove(listeners[eventString], key) + break + end + end + end + end + function EventHub:dispatchEvent(eventString, ...) + if (listeners[eventString] == nil) then + return + end + + --loop through all the listeners and call the callback function + for key, value in ipairs(listeners[eventString]) do + value.callback(...) + end + end + + + --A comprehensive list of notification strings to read from + EventHub.Notifications = { + AuthenticationSuccess = "rbxNotificationAuthenticationSuccess"; + GameJoin = "rbxNotificationGameJoin"; + OpenGames = "rbxNotificationOpenGames"; + OpenGameDetail = "rbxNotificationOpenGameDetail"; + OpenGameGenre = "rbxNotificationOpenGameGenre"; + OpenBadgeScreen = "rbxNotificationOpenBadgeScreen"; + -- TODO: Remove UnlinkAccountConfirmation when FFlagXboxUseUnlinkCallback is removed + UnlinkAccountConfirmation = "rbxNotificationUnlinkAccountConfirmation"; + OpenSettingsScreen = "rbxNotificationOpenSettingsScreen"; + OpenAvatarEditorScreen = "rbxNotificationOpenAvatarEditorScreen"; + OpenAccountSettingsScreen = "rbxNotificationOpenAccountSettingsScreen"; + NavigateToEquippedAvatar = "rbxNotificationNavigateToEquippedAvatar"; + NavigateToRobuxScreen = "rbxNotificationNavigateToRobuxScreen"; + RobuxCatalogPurchaseInitiated = "rbxRobuxCatalogPurchaseInitiated"; + DonnedDifferentPackage = "rbxDonnedDifferentPackage"; + VotedOnPlace = "rbxVotedOnPlace"; + AvatarEquipBegin = "rbxAvatarEquipBegin"; + DonnedDifferentOutfit = "rbxDonnedDifferentOutfit"; + AvatarEquipSuccess = "rbxAvatarEquipSuccess"; + AvatarPurchaseBegin = "rbxAvatarPurchaseBegin"; + AvatarPurchaseSuccess = "rbxAvatarPurchaseSuccess"; + FavoriteToggle = "rbxFavoriteToggle"; + PlayedGamesChanged = "rbxPlayedGamesChanged"; + UnlockedUGC = "rbxNotificationUnlockedUGC"; + + --Add for avatar editor equip/update + CharacterEquipped = "CharacterEquipped"; + CharacterUpdated = "CharacterUpdated"; + }; +end + + +return EventHub diff --git a/Client2018/content/internal/AppShell/Modules/Shell/FriendPresenceItem.lua b/Client2018/content/internal/AppShell/Modules/Shell/FriendPresenceItem.lua new file mode 100644 index 0000000..9b27694 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/FriendPresenceItem.lua @@ -0,0 +1,298 @@ +--[[ + // FriendPresenceItem.lua + // Creates a friend activity gui item to be used with a ScrollingGrid + // for friends social status +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local AssetManager = require(ShellModules:FindFirstChild('AssetManager')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local XboxRecommendedPeople = settings():GetFFlag("XboxRecommendedPeople2") + +local FAIL_IMG = 'rbxasset://textures/ui/Shell/Icons/DefaultProfileIcon.png' + +local function FriendPresenceItem(size, idStr) + local this = {} + + local TEXT_OFFSET = 12 + + local container = Utility.Create'ImageButton' + { + Name = idStr; + Size = size; + Position = UDim2.new(0, 0, 0, 0); + BackgroundColor3 = GlobalSettings.WhiteTextColor; + BorderSizePixel = 0; + BackgroundTransparency = 1; + AutoButtonColor = false; + + SoundManager:CreateSound('MoveSelection'); + } + local containerTween = nil + container.SelectionGained:connect(function() + containerTween = Utility.PropertyTweener(container, "BackgroundTransparency", 1, GlobalSettings.AvatarBoxBackgroundSelectedTransparency, 0, + Utility.EaseInOutQuad, true, nil) + end) + container.SelectionLost:connect(function() + if containerTween then + containerTween:Cancel() + containerTween = nil + end + container.BackgroundTransparency = 1 + end) + local avatarImage = Utility.Create'ImageLabel' + { + Name = "AvatarImage"; + Image = ''; + Size = UDim2.new(0, 104, 0, 104); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 0; + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.CharacterBackgroundColor; + ZIndex = 2; + Parent = container; + AssetManager.CreateShadow(1); + } + + local contentContainer = Utility.Create'Frame' + { + Name = "ContentContainer"; + Size = UDim2.new(1, -avatarImage.Size.X.Offset - TEXT_OFFSET, 0, avatarImage.Size.Y.Offset); + Position = UDim2.new(0, avatarImage.Size.X.Offset + TEXT_OFFSET, 0, 0); + BackgroundTransparency = 1; + ClipsDescendants = true; + } + + local displayContainer = Utility.Create'Frame' + { + Name = "FriendDisplayContainer"; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 0.5, 0); + AnchorPoint = Vector2.new(0, 0.5); + BackgroundTransparency = 1; + Parent = contentContainer; + } + + local gamertagLabel = Utility.Create'TextLabel' + { + Name = "GamertagLabel"; + Text = ""; + Size = UDim2.new(1, 0, 0, 24); + Position = UDim2.new(0, 0, 0, 0); + TextXAlignment = Enum.TextXAlignment.Left; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + BackgroundTransparency = 1; + Parent = displayContainer; + } + + local robloxNameContainer = Utility.Create'Frame' + { + Name = "RobloxNameContainer"; + -- Update to size of icon + Size = UDim2.new(1, 0, 0, 24); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + Parent = displayContainer; + } + local robloxIcon = Utility.Create'ImageLabel' + { + Name = "RobloxIcon"; + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Shell/Icons/RobloxIcon24.png"; + Size = UDim2.new(0, 24, 0, 24); + Parent = robloxNameContainer; + } + local robloxNameLabel = Utility.Create'TextLabel' + { + Name = "RobloxNameLabel"; + Text = ""; + Size = UDim2.new(1, -robloxIcon.Size.X.Offset - 10, 1, 0); + Position = UDim2.new(0, robloxIcon.Size.X.Offset + 10, 0, -1); + TextXAlignment = Enum.TextXAlignment.Left; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + BackgroundTransparency = 1; + Parent = robloxNameContainer; + } + + local statusContainer = Utility.Create'Frame' + { + Name = "StatusContainer"; + Size = UDim2.new(1, 0, 0, 20); + Position = UDim2.new(0, 0, 0, gamertagLabel.Position.Y.Offset + gamertagLabel.Size.Y.Offset - 11); + BackgroundTransparency = 1; + Parent = displayContainer; + } + local presenceStatusImage = Utility.Create'ImageLabel' + { + Name = "PresenceStatusImage"; + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Shell/Icons/OnlineStatusIcon@1080.png"; + Size = UDim2.new(0, 19, 0, 20); + Position = UDim2.new(0, (robloxIcon.Size.X.Offset / 2) - 10, 0, 0); + Parent = statusContainer; + } + local presenceLabel = Utility.Create'TextLabel' + { + Name = "PresenceLabel"; + Text = ""; + Size = UDim2.new(1, -presenceStatusImage.Position.X.Offset - presenceStatusImage.Size.X.Offset, 1, 0); + Position = UDim2.new(0, presenceStatusImage.Position.X.Offset + presenceStatusImage.Size.X.Offset + 12, 0, 0); + TextXAlignment = Enum.TextXAlignment.Left; + TextColor3 = GlobalSettings.LightGreyTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.DescriptionSize; + BackgroundTransparency = 1; + Parent = statusContainer; + } + + contentContainer.Parent = container + + local lineBreak = Utility.Create'Frame' + { + Name = "Break"; + Size = UDim2.new(1, -avatarImage.Size.X.Offset - TEXT_OFFSET, 0, 2); + Position = UDim2.new(0, avatarImage.Size.X.Offset + TEXT_OFFSET, 1, -2); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.LineBreakColor; + Parent = container; + } + + local failImage = Utility.Create'ImageLabel' + { + Name = "FailImage"; + Size = UDim2.new(0.5, 0, 0.5, 0); + Position = UDim2.new(0.25, 0, 0.25, 0); + BackgroundTransparency = 1; + Image = FAIL_IMG; + ZIndex = 2; + } + + function this:GetContainer() + return container + end + + local function setAvatarImage(userId) + -- do not try to load thumb for invalid userIds + if userId == nil or userId < 1 then + failImage.Parent = avatarImage + return + end + + spawn(function() + local loader = ThumbnailLoader:LoadAvatarThumbnailAsync(avatarImage, userId, + XboxRecommendedPeople and Enum.ThumbnailType.HeadShot or Enum.ThumbnailType.AvatarThumbnail, + Enum.ThumbnailSize.Size100x100) + if not loader:LoadAsync(false, false) then + failImage.Parent = avatarImage + else + failImage.Parent = nil + end + end) + end + + local function setPresence(str, isInRobloxGame) + presenceLabel.Text = str or "" + presenceStatusImage.ImageColor3 = isInRobloxGame and GlobalSettings.GreenTextColor + or GlobalSettings.GreySelectedButtonColor + + presenceStatusImage.Visible = str ~= "" + end + + local function setPresence2(str, statusColor) + presenceLabel.Text = str or "" + presenceStatusImage.ImageColor3 = statusColor + presenceStatusImage.Visible = str ~= "" + end + + function this:SetDisplay(data) + local isNotConsole = UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows + setAvatarImage(data.robloxuid) + + local gamertag = data.display or "" + local robloxName = data.RobloxName or "" + if isNotConsole or XboxRecommendedPeople then + gamertag = data.gamertag or "" + robloxName = data.robloxName or "" + end + + local displayContainerYSize = 0 + + if gamertag ~= "" then + gamertagLabel.Text = gamertag + gamertagLabel.Position = UDim2.new(0, 0, 0, 0) + gamertagLabel.Visible = true + displayContainerYSize = displayContainerYSize + gamertagLabel.Size.Y.Offset + 12 + else + gamertagLabel.Text = "" + gamertagLabel.Visible = false + end + + if robloxName ~= "" then + robloxNameLabel.Text = robloxName + robloxNameContainer.Position = UDim2.new(0, 0, 0, displayContainerYSize) + robloxNameContainer.Visible = true + displayContainerYSize = displayContainerYSize + robloxNameContainer.Size.Y.Offset + 7 + else + robloxNameLabel.Text = "" + robloxNameContainer.Visible = false + end + + if isNotConsole or XboxRecommendedPeople then + if data.robloxStatus == "InGame" then + setPresence2(data.lastLocation, GlobalSettings.Colors.GreenText) + elseif data.robloxStatus == "InStudio" then + setPresence2(data.lastLocation, GlobalSettings.Colors.OrangeText) + elseif data.robloxStatus == "Online" then + setPresence2("Roblox", GlobalSettings.Colors.BlueText) + else + if data.xboxStatus and data.xboxStatus == "Online" then + setPresence2(Strings:LocalizedString("OnlineWord"), GlobalSettings.Colors.BlueText) + else + setPresence2(Strings:LocalizedString("OfflineWord"), GlobalSettings.Colors.GreyText) + end + end + else + if data.LastLocation and data.PlaceId then + setPresence(data.LastLocation, true) + elseif data.rich then + local richTbl = data.rich + if #richTbl > 0 then + local presence = richTbl[#richTbl] + -- should probably compare to titleId, but shouldn't expose titleId in case we open source this + if presence.title == "ROBLOX" then + setPresence("Roblox", false) + else + setPresence(Strings:LocalizedString("OnlineWord"), false) + end + else + setPresence("", false) + end + end + end + + statusContainer.Position = UDim2.new(0, 0, 0, displayContainerYSize) + + displayContainerYSize = displayContainerYSize + statusContainer.Size.Y.Offset + displayContainer.Size = UDim2.new(1, 0, 0, displayContainerYSize) + end + + function this:Destroy() + container:Destroy() + this = nil + end + + return this +end + +return FriendPresenceItem diff --git a/Client2018/content/internal/AppShell/Modules/Shell/FriendsData.lua b/Client2018/content/internal/AppShell/Modules/Shell/FriendsData.lua new file mode 100644 index 0000000..376c173 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/FriendsData.lua @@ -0,0 +1,444 @@ +--[[ + // FriendsData.lua + + // Caches the current friends pagination to used by anyone in the app + + // TODO: + Need polling to update friends. How are we going to handle all the cases + like the person you're selecting going offline, etc.. +]] +local CoreGui = game:GetService("CoreGui") +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) +local FriendService = nil +pcall(function() FriendService = game:GetService('FriendService') end) +local ThirdPartyUserService = nil +pcall(function() ThirdPartyUserService = game:GetService('ThirdPartyUserService') end) +local isNotConsole = UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows + +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Http = require(ShellModules:FindFirstChild('Http')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local TableUtilities = require(Modules.LuaApp.TableUtilities) +local ReloaderManager = require(ShellModules:FindFirstChild('ReloaderManager')) +local MakeSafeAsync = require(ShellModules:FindFirstChild('SafeAsync')) +local AppState = require(ShellModules.AppState) +local ResetUserThumbnails = require(ShellModules.Actions.ResetUserThumbnails) +local SetFriendsData = require(ShellModules.Actions.SetFriendsData) +local SetRenderedFriendsData = require(ShellModules.Actions.SetRenderedFriendsData) + + +-- NOTE: This is just required for fixing Usernames in auto-generatd games +local GameData = require(ShellModules:FindFirstChild('GameData')) +local XboxRecommendedPeople = settings():GetFFlag("XboxRecommendedPeople2") +local ConvertMyPlaceNameInXboxAppFlag = settings():GetFFlag("ConvertMyPlaceNameInXboxApp") + +local FriendsData = {} + +local function OnUserAccountChanged() + FriendsData.Setup() +end +EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "FriendsData", OnUserAccountChanged) +if ThirdPartyUserService then + ThirdPartyUserService.ActiveUserSignedOut:connect(function() + FriendsData.Reset() + end) +end + +local isOnlineFriendsPolling = false +local isFriendEventsConnected = false +local renderedFriendsUpdateSuspended = false +local cachedFriendsData = nil +local cachedFriendsDataMap = {} +local friendsDataConns = {} + +local function filterFriends(friendsData) + if isNotConsole or XboxRecommendedPeople then + for i = 1, #friendsData do + local data = friendsData[i] + + if data.gamertag == "" then + data.gamertag = nil + end + + if data.robloxuid <= 0 then + data.robloxuid = nil + end + + if data.xuid == "" then + data.xuid = nil + end + + if data.placeId == 0 then + data.placeId = nil + end + + if data.lastLocation == "" then + data.lastLocation = nil + end + + if data.robloxName == "" then + data.robloxName = nil + end + + local placeId = data.placeId + local lastLocation = data.lastLocation + + -- If the lastLocation for a user is some user place with a GeneratedUsername in it + -- then replace it with the actual creator name! + if ConvertMyPlaceNameInXboxAppFlag and placeId and lastLocation and GameData:ExtractGeneratedUsername(lastLocation) then + local gameCreator = GameData:GetGameCreatorAsync(placeId) + if gameCreator then + lastLocation = GameData:GetFilteredGameName(lastLocation, gameCreator) + end + end + + data.placeId = placeId + data.lastLocation = lastLocation + end + else + for i = 1, #friendsData do + local data = friendsData[i] + + if data["PlaceId"] == 0 then + data["PlaceId"] = nil + end + + if data["LastLocation"] == "" then + data["LastLocation"] = nil + end + + if data["RobloxName"] == "" then + data["RobloxName"] = nil + end + + local placeId = data["PlaceId"] + local lastLocation = data["LastLocation"] + + -- If the lastLocation for a user is some user place with a GeneratedUsername in it + -- then replace it with the actual creator name! + if ConvertMyPlaceNameInXboxAppFlag and placeId and lastLocation and GameData:ExtractGeneratedUsername(lastLocation) then + local gameCreator = GameData:GetGameCreatorAsync(placeId) + if gameCreator then + lastLocation = GameData:GetFilteredGameName(lastLocation, gameCreator) + end + end + + data["PlaceId"] = placeId + data["LastLocation"] = lastLocation + end + end + + return friendsData +end + +--TODO Remove when remove FFlag XboxRecommendedPeople2 +local function sortFriendsData(tempFriendsData) + table.sort(tempFriendsData, function(a, b) + if a["PlaceId"] and b["PlaceId"] then + return a["display"] < b["display"] + end + if a["PlaceId"] then + return true + end + if b["PlaceId"] then + return false + end + + return a["display"] < b["display"] + end) +end + +--TODO Remove when remove FFlag XboxRecommendedPeople2 +local function uploadFriendsAnalytics(friendsData) + if friendsData then + local numPlaying = 0 + for i, data in pairs(friendsData) do + if data["PlaceId"] then + numPlaying = numPlaying + 1 + end + end + + Analytics.UpdateHeartbeatObject({ + FriendsPlaying = numPlaying; + FriendsOnline = #friendsData; + }); + end +end + +-- Combine roblox uid and xbox id into a unique id that exists for all roblox and/or xbox friends +local function uniqueId(friendEntry) + return tostring(friendEntry.robloxuid) .. "#" .. tostring(friendEntry.xuid) +end + +-- Marks which friends have been updated or added in the newFriendsData table +local function diffFriends(oldFriendsData, newFriendsData) + -- Build lookup table of old friends using unique id + local oldFriendLookup = {} + if oldFriendsData and type(oldFriendsData) == "table" then + for _, friendEntry in pairs(oldFriendsData) do + oldFriendLookup[uniqueId(friendEntry)] = friendEntry + end + end + + -- Check if any friends in new table have been updated + if newFriendsData and type(newFriendsData) == "table" then + for _, newFriendEntry in pairs(newFriendsData) do + local oldFriendEntry = oldFriendLookup[uniqueId(newFriendEntry)] + if oldFriendEntry then + local isUpdated = false + for friendFieldKey, friendFieldValue in pairs(newFriendEntry) do + if friendFieldValue ~= oldFriendEntry[friendFieldKey] then + isUpdated = true + break + end + end + newFriendEntry.isUpdated = isUpdated + else + newFriendEntry.isUpdated = true + end + end + end +end + +--[[ Public API ]]-- +FriendsData.OnFriendsDataUpdated = Utility.Signal() + +--TODO Remove when remove FFlag XboxRecommendedPeople2 +local function processNewFriendsData(newFriendsData) + local myOnlineFriends = {} + if newFriendsData then + myOnlineFriends = filterFriends(newFriendsData) + sortFriendsData(myOnlineFriends) + end + return myOnlineFriends +end + +function FriendsData.GetOnlineFriendsAsync() + if not cachedFriendsData then + --Wait until we get cachedFriendsData from FriendService/FriendEvents disconnect(user sign out) + while isFriendEventsConnected and not cachedFriendsData do + wait() + end + end + + return cachedFriendsData or {} +end + +-- we make connections through this function so we can clean them all up upon +-- clearing the friends data +function FriendsData.ConnectUpdateEvent(cbFunc) + local cn = FriendsData.OnFriendsDataUpdated:connect(cbFunc) + table.insert(friendsDataConns, cn) +end + +function FriendsData.Reset() + isOnlineFriendsPolling = false + + for index,cn in pairs(friendsDataConns) do + cn = Utility.DisconnectEvent(cn) + friendsDataConns[index] = nil + end + isFriendEventsConnected = false + + cachedFriendsData = nil + cachedFriendsDataMap = {} + + if XboxRecommendedPeople then + AppState.store:dispatch(ResetUserThumbnails()) + AppState.store:dispatch(SetFriendsData()) + AppState.store:dispatch(SetRenderedFriendsData()) + renderedFriendsUpdateSuspended = false + end + if isNotConsole then + ReloaderManager:removeReloader("FriendsData") + FriendsData.ReloaderFuncId = nil + end +end + +local function CheckEntryUpdate(newFriendsData) + local validEntries = {} + for i = 1, #newFriendsData do + local data = newFriendsData[i] + local xuid = data.xuid or "" + local robloxuid = data.robloxuid or "" + local idStr = tostring(xuid.."#"..robloxuid) + validEntries[idStr] = true + if cachedFriendsDataMap[idStr] then --check whether entry changed + local differentAttributes = TableUtilities.TableDifference(cachedFriendsDataMap[idStr], data) + if next(differentAttributes) ~= nil then + data.isUpdated = true + end + end + cachedFriendsDataMap[idStr] = data + end + + for idStr in pairs(cachedFriendsDataMap) do + if not validEntries[idStr] then + cachedFriendsDataMap[idStr] = nil + end + end +end + +local UpdateRenderedFriendsData; + +if XboxRecommendedPeople then + UpdateRenderedFriendsData = function(newFriendsData) + AppState.store:dispatch(SetRenderedFriendsData(newFriendsData)) + --data for HomePane friend scroller, also from store, but need the CheckEntryUpdate + CheckEntryUpdate(newFriendsData) + cachedFriendsData = newFriendsData + FriendsData.OnFriendsDataUpdated:fire(newFriendsData) + end + function FriendsData:SuspendUpdate() + renderedFriendsUpdateSuspended = true + end + + function FriendsData:ResumeUpdate() + --Get latest data from store when ResumeUpdate(), don't dispatch if the friends data hasn't been fetched yet + if AppState.store:getState().Friends.initialized then + UpdateRenderedFriendsData(AppState.store:getState().Friends.data) + end + renderedFriendsUpdateSuspended = false + end +end + +function FriendsData.Setup() + FriendsData.Reset() + --We make the conns once user logged in, and once we get the cachedFriendsData from FriendService + --this func becomes sync call + if PlatformService and FriendService then + --Connect FriendsUpdated event to get newFriendsData at intervals + table.insert(friendsDataConns, FriendService.FriendsUpdated:connect(function(newFriendsData) + if not XboxRecommendedPeople then + cachedFriendsData = processNewFriendsData(newFriendsData) + uploadFriendsAnalytics(cachedFriendsData) + FriendsData.OnFriendsDataUpdated:fire(cachedFriendsData) + else + newFriendsData = filterFriends(newFriendsData) + AppState.store:dispatch(SetFriendsData(newFriendsData)) + if not AppState.store:getState().RenderedFriends.initialized or not renderedFriendsUpdateSuspended then + UpdateRenderedFriendsData(newFriendsData) + end + end + end)) + + isFriendEventsConnected = true + + --Try to get the cachedFriendsData, check if the friends data has been fetched on Friend Service + local success, result = pcall(function() + return FriendService:GetPlatformFriends() + end) + if success then + if not XboxRecommendedPeople then + cachedFriendsData = processNewFriendsData(result) + else + result = filterFriends(result) + AppState.store:dispatch(SetFriendsData(result)) + if not AppState.store:getState().RenderedFriends.initialized or not renderedFriendsUpdateSuspended then + UpdateRenderedFriendsData(result) + end + end + end + else + if isNotConsole then + local POLL_DELAY = 30 + local GetRecommendPeopleInStudio = MakeSafeAsync({ + asyncFunc = function() + local finalRecommendedUsers = {} + local result = Http.GetRecommendedUsersndsAsync() + if result and result["recommendedUsers"] then + local recommendedUsersMap = {} + local recommendedUsers = result["recommendedUsers"] + local userIds = {} + for i = 1, #recommendedUsers do + local data = recommendedUsers[i] + local robloxuid = data.userId + if robloxuid then + table.insert(userIds, robloxuid) + recommendedUsersMap[robloxuid] = + { + robloxuid = robloxuid, + robloxName = data.userName + } + end + end + + local presenceInfo = Http.GetUsersPresenceAsync(userIds) + if presenceInfo and presenceInfo["userPresences"] then + for _, presence in ipairs(presenceInfo["userPresences"]) do + local robloxuid = presence.userId + if robloxuid and recommendedUsersMap[robloxuid] then + recommendedUsersMap[robloxuid].placeId = presence.rootPlaceId + recommendedUsersMap[robloxuid].lastLocation = presence.lastLocation + recommendedUsersMap[robloxuid].friendsSource = "Roblox" + local robloxStatus = "Offline" + local rank = 4 + if presence.userPresenceType == 1 then + robloxStatus = "Online" + rank = 3 + elseif presence.userPresenceType == 2 then + if presence.rootPlaceId and presence.rootPlaceId > 0 then + robloxStatus = "InGame" + rank = 1 + else + robloxStatus = "Online" + rank = 3 + end + elseif presence.userPresenceType == 3 then + robloxStatus = "InStudio" + rank = 2 + end + recommendedUsersMap[robloxuid].robloxStatus = robloxStatus + recommendedUsersMap[robloxuid].rank = rank + table.insert(finalRecommendedUsers, recommendedUsersMap[robloxuid]) + end + end + end + local function sortFunc(a, b) + if a.rank == b.rank then + return a.robloxName:lower() < b.robloxName:lower() + end + return a.rank < b.rank + end + + table.sort(finalRecommendedUsers, sortFunc) + end + return finalRecommendedUsers + end, + callback = function(finalRecommendedUsers) + finalRecommendedUsers = filterFriends(finalRecommendedUsers) + if not XboxRecommendedPeople then + CheckEntryUpdate(finalRecommendedUsers) + cachedFriendsData = finalRecommendedUsers + FriendsData.OnFriendsDataUpdated:fire(cachedFriendsData) + else + finalRecommendedUsers = filterFriends(finalRecommendedUsers) + AppState.store:dispatch(SetFriendsData(finalRecommendedUsers)) + if not AppState.store:getState().RenderedFriends.initialized or not renderedFriendsUpdateSuspended then + UpdateRenderedFriendsData(finalRecommendedUsers) + end + end + end, + userRelated = true + }) + + if not isOnlineFriendsPolling then + isOnlineFriendsPolling = true + isFriendEventsConnected = true + spawn(function() + ReloaderManager:removeReloader("FriendsData") + FriendsData.ReloaderFuncId = ReloaderManager:addReloaderFunc("FriendsData", function() GetRecommendPeopleInStudio() end, POLL_DELAY, true) + ReloaderManager:callReloaderFunc("FriendsData", FriendsData.ReloaderFuncId) + end) + end + end + end +end + +return FriendsData diff --git a/Client2018/content/internal/AppShell/Modules/Shell/FriendsView.lua b/Client2018/content/internal/AppShell/Modules/Shell/FriendsView.lua new file mode 100644 index 0000000..3be81de --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/FriendsView.lua @@ -0,0 +1,203 @@ +--[[ + // FriendsView.lua + + // Creates a view for the users friends. + // Handles user input, updating view + + TODO: + Connect selected/deselected to change color +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) + +local FriendsData = require(ShellModules:FindFirstChild('FriendsData')) +local FriendPresenceItem = require(ShellModules:FindFirstChild('FriendPresenceItem')) +local SideBarModule = require(ShellModules:FindFirstChild('SideBar')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local GameJoinModule = require(ShellModules:FindFirstChild('GameJoin')) +local Errors = require(ShellModules:FindFirstChild('Errors')) +local ErrorOverlayModule = require(ShellModules:FindFirstChild('ErrorOverlay')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local XboxRecommendedPeople = settings():GetFFlag("XboxRecommendedPeople2") + +local SIDE_BAR_ITEMS = { + JoinGame = Strings:LocalizedString("JoinGameWord"); + ViewDetails = Strings:LocalizedString("ViewGameDetailsWord"); + ViewProfile = Strings:LocalizedString("ViewGamerCardWord"); + EmptyFriendSideBar = Strings:LocalizedString("EmptyFriendSideBarWord"); +} + +-- side bar is shared between all views +local SideBar = SideBarModule() + +local function setPresenceData(item, data) + item:SetDisplay(data) +end + +-- viewGridContainer - ScrollingGrid +-- friendData - FriendsData +-- updateFunc - function that will be called when FriendData update +local createFriendsView = function(viewGridContainer, friendsData, updateFunc) + local this = {} + -- map of userId to presenceItem, for dynamic scrolling grid, we generate + -- the presenceItem for the user until the user's grid item is shown on screen + local presenceItems = {} + local presenceItemDirty = {} + local presenceItemToSidebarEvent = {} + local currentFriendsData = nil + local isNotConsole = UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows + + local function connectSideBar(item, data) + Utility.DisconnectEvent(presenceItemToSidebarEvent[item]) + local container = item:GetContainer() + presenceItemToSidebarEvent[item] = container.MouseButton1Click:connect(function() + if data then + -- rebuild side bar based on current data + SideBar:RemoveAllItems() + function SideBar:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = "FriendsSideBar"} + end + if isNotConsole or XboxRecommendedPeople then + local emptySideBar = true + if data.robloxStatus == "InGame" then + local placeId = data.placeId + local lastLocation = data.lastLocation + local robloxuid = data.robloxuid + SideBar:AddItem(SIDE_BAR_ITEMS.JoinGame, function() + GameJoinModule:StartGame(GameJoinModule.JoinType.Follow, robloxuid) + end) + SideBar:AddItem(SIDE_BAR_ITEMS.ViewDetails, function() + -- pass nil for iconId, gameDetail will fetch + EventHub:dispatchEvent(EventHub.Notifications["OpenGameDetail"], placeId, lastLocation, nil) + end) + emptySideBar = false + end + if data.xuid and #data.xuid > 0 and PlatformService then + local xuid = data.xuid + SideBar:AddItem(SIDE_BAR_ITEMS.ViewProfile, function() + local success, result = pcall(function() + PlatformService:PopupProfileUI(Enum.UserInputType.Gamepad1, xuid) + end) + -- NOTE: This will try to pop up the xbox system gamer card, failure will be handled + -- by the xbox. + if not success then + Utility.DebugLog("PlatformService:PopupProfileUI failed because,", result) + end + end) + emptySideBar = false + end + if emptySideBar then + SideBar:SetText(SIDE_BAR_ITEMS.EmptyFriendSideBar) + end + else + local inGame = data["PlaceId"] ~= nil + if inGame and not data["IsPrivateSession"] then + SideBar:AddItem(SIDE_BAR_ITEMS.JoinGame, function() + GameJoinModule:StartGame(GameJoinModule.JoinType.Follow, data["robloxuid"]) + end) + SideBar:AddItem(SIDE_BAR_ITEMS.ViewDetails, function() + -- pass nil for iconId, gameDetail will fetch + EventHub:dispatchEvent(EventHub.Notifications["OpenGameDetail"], data["PlaceId"], data["LastLocation"], nil) + end) + end + SideBar:AddItem(SIDE_BAR_ITEMS.ViewProfile, function() + if PlatformService and data["xuid"] then + local success, result = pcall(function() + PlatformService:PopupProfileUI(Enum.UserInputType.Gamepad1, data["xuid"]) + end) + -- NOTE: This will try to pop up the xbox system gamer card, failure will be handled + -- by the xbox. + if not success then + Utility.DebugLog("PlatformService:PopupProfileUI failed because,", result) + end + end + end) + end + ScreenManager:OpenScreen(SideBar, false) + else + ScreenManager:OpenScreen(ErrorOverlayModule(Errors.Default), false) + end + end) + end + + local function getPresenceItemByIndex(i) + local data = currentFriendsData[i] + if data then + local idStr = tostring(data.xuid and data.xuid or data["robloxuid"]) + if isNotConsole or XboxRecommendedPeople then + local xuid = data.xuid or "" + local robloxuid = data.robloxuid or "" + idStr = tostring(xuid.."#"..robloxuid) + end + local presenceItem = presenceItems[idStr] + if presenceItem == nil then + presenceItem = FriendPresenceItem(UDim2.new(0, 446, 0, 114), idStr) + presenceItemDirty[idStr] = true + presenceItems[idStr] = presenceItem + end + + if presenceItemDirty[idStr] then + setPresenceData(presenceItem, data) + connectSideBar(presenceItem, data) + presenceItemDirty[idStr] = nil + end + return presenceItem:GetContainer() + end + end + + local function onFriendsUpdated(newFriendsData) + -- map of valid userIds to bool + local validEntries = {} + currentFriendsData = newFriendsData + for i = 1, #currentFriendsData do + local data = currentFriendsData[i] + if data then + local idStr = tostring(data.xuid and data.xuid or data["robloxuid"]) + if XboxRecommendedPeople then + local xuid = data.xuid or "" + local robloxuid = data.robloxuid or "" + idStr = tostring(xuid.."#"..robloxuid) + if data.isUpdated then + presenceItemDirty[idStr] = true + end + end + validEntries[idStr] = true + end + end + + -- remove items if needed + for idStr, presenceItem in pairs(presenceItems) do + if not validEntries[idStr] then + presenceItemDirty[idStr] = true + presenceItem:Destroy() + presenceItems[idStr] = nil + end + end + + --Update scrolling grid with new list + viewGridContainer:SetItemCallback(getPresenceItemByIndex) + viewGridContainer:RecalcLayout(#currentFriendsData) + + if updateFunc then + updateFunc(#currentFriendsData) + end + end + + onFriendsUpdated(friendsData) + FriendsData.ConnectUpdateEvent(onFriendsUpdated) + + function this:GetDefaultFocusItem() + return viewGridContainer:GetSelectableItem() + end + + return this +end + +return createFriendsView diff --git a/Client2018/content/internal/AppShell/Modules/Shell/GameCarouselItem.lua b/Client2018/content/internal/AppShell/Modules/Shell/GameCarouselItem.lua new file mode 100644 index 0000000..633b9fe --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/GameCarouselItem.lua @@ -0,0 +1,348 @@ +local CoreGui = game:GetService("CoreGui") +local GuiService = game:GetService('GuiService') +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) + +local CarouselView = require(ShellModules:FindFirstChild('CarouselView')) +local CarouselController = require(ShellModules:FindFirstChild('CarouselController')) + + +local function GameCarouselItem(size, sortName, getGameCollection, onNewGameSelected, hasResultsChanged) + local this = {} + + local myCarouselController = nil + local controllerMutex = false + local myCarouselView = CarouselView() + local TEXT_OFFSET = 112 + local CAROUSEL_OFFSET = 110 + local hasResults = true + local lockInPUP = false + --Initially is unlocked + local locked = false + local currentLoader = nil + local inFocus = false + + local noSelectionObject = Utility.Create'ImageLabel' + { + Name = 'NoSelectionObject'; + BackgroundTransparency = 1; + } + + local container = Utility.Create'ImageButton' + { + Name = sortName.." ImageButton"; + Size = size; + Position = UDim2.new(0, 0, 0, 0); + BorderSizePixel = 0; + BackgroundTransparency = 1; + ClipsDescendants = false; + AutoButtonColor = false; + SelectionImageObject = noSelectionObject; + Selectable = false; + } + + local selectionHolder = Utility.Create'ImageLabel' + { + Name = sortName.." SelectionHolder"; + BackgroundTransparency = 1; + Size = UDim2.new(0, 400, 1, 0); + Position = UDim2.new(0, 50, 0, 50); + SelectionImageObject = noSelectionObject; + Selectable = true; + Parent = container; + } + + selectionHolder.NextSelectionLeft = selectionHolder + selectionHolder.NextSelectionRight = selectionHolder + + local nameLabel = Utility.Create'TextLabel' + { + Name = "NameLabel"; + Text = Strings:LocalizedString("LoadingWord"); + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, TEXT_OFFSET, 0, 22); + TextXAlignment = Enum.TextXAlignment.Left; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.RegularFont; + FontSize = Enum.FontSize.Size36; -- GlobalSettings.TitleSize; + BackgroundTransparency = 1; + Selectable = false; + Parent = container; + } + + local function loadGameCollection(carouselController, carouselView, container) + local loaderImgTransparency = 0 + if currentLoader then + loaderImgTransparency = Utility.Clamp(0, 1, currentLoader:GetTransparency()) + currentLoader:Cleanup() + end + nameLabel.Text = Strings:LocalizedString("LoadingWord"); + selectionHolder.Selectable = true + + spawn(function() + local loader = LoadingWidget( + { Parent = container; Position = UDim2.new(0.5, CAROUSEL_OFFSET, 0.5, 50); ImageTransparency = loaderImgTransparency }, + { + function() + --The InitializeAsync will also remove old items if exist + carouselController:InitializeAsync(getGameCollection()) + --The Connect will also remove old connections if exist + carouselController:Connect() + carouselController.NewItemSelected:connect(onNewGameSelected) + + --Make sure this thread is the latest one, if so, + --make the callback and update selection + if this and myCarouselController == carouselController and myCarouselView == carouselView then + hasResults = carouselController:HasResults() + hasResultsChanged(this) + if hasResults then + selectionHolder.Selectable = false + nameLabel.Text = sortName + if this:IsSelected() then + this:RemoveFocus() + this:Focus() + end + else + selectionHolder.Selectable = true + nameLabel.Text = "" + end + else + if carouselView then + carouselView:SetParent(nil) + end + end + end + } + ) + + if currentLoader then + currentLoader:SetParent(nil) + end + + currentLoader = loader + loader:AwaitFinished() + loader:Cleanup() + end) + end + + function this:Init(Transparency) + Transparency = Transparency and Transparency or 0 + myCarouselView:SetTransparency(Transparency) + myCarouselView:SetSize(UDim2.new(0, 1700, 0, 232)) + myCarouselView:SetPosition(UDim2.new(0, CAROUSEL_OFFSET, 0, 50)) + myCarouselView:SetPadding(20) + myCarouselView:SetItemSizePercentOfContainer(0.84) + myCarouselView:SetSelectionImageObject( + Utility.Create'ImageLabel' + { + BackgroundTransparency = 1; + } + ) + myCarouselView:SetParent(container) + + if myCarouselController then + myCarouselController:Disconnect() + end + + if myCarouselController then + local prevFrontPageIndex, prevEndPageIndex, prevAbsoluteDataIndex = myCarouselController:GetIndexData() + myCarouselController = CarouselController(myCarouselView, true, prevFrontPageIndex, prevEndPageIndex, prevAbsoluteDataIndex) + else + myCarouselController = CarouselController(myCarouselView, true) + end + myCarouselController:SetLoadBuffer(15) + loadGameCollection(myCarouselController, myCarouselView, container) + end + + function this:Refresh() + if myCarouselView then + if this:IsSelected() then + selectionHolder.Selectable = true + Utility.SetSelectedCoreObject(selectionHolder) + --Clear game data + onNewGameSelected(nil) + end + --Reset the old CarouselView + myCarouselView:RemoveFocus() + myCarouselView:RemoveAllItems() + myCarouselView:SetParent(nil) + + --Init with previous Transparency + local prevTransparency = myCarouselView:GetTransparency() + myCarouselView = CarouselView() + this:Init(prevTransparency) + end + end + + function this:HasResults() + return hasResults + end + + function this:GetContainer() + return container + end + + function this:ContainsItem(item) + return myCarouselView:ContainsItem(item) + end + + function this:GetCarouselView() + return myCarouselView + end + + function this:GetSortName() + return sortName + end + + function this:GetNameText() + return nameLabel.Text + end + + function this:SetNameText(name) + nameLabel.Text = name + end + + -- Important subtle detail: the center of this needs to be in about the same + -- x-coordinate as the first item in each carousel so that selection doesn't + -- move laterally when you arrow up and down. + local LockOverlay = Utility.Create'ImageLabel' + { + Name = "LockOverlay"; + Size = UDim2.new(0, 400, 1, 0); + Position = UDim2.new(0, 50, 0, 50); + BackgroundTransparency = 1; + Selectable = true; + SelectionImageObject = Utility.Create'ImageLabel' + { + BackgroundTransparency = 1; + }; + ZIndex = 3; + } + + LockOverlay.NextSelectionRight = LockOverlay + LockOverlay.NextSelectionLeft = LockOverlay + + function this:Lock() + locked = true + LockOverlay.Parent = container + if this:IsSelected() then + this:RemoveFocus() + this:Focus() + end + myCarouselView:SetSelectable(false) + myCarouselView:SetClipsDescendants(true) + end + + function this:Unlock() + locked = false + myCarouselView:SetSelectable(true) + myCarouselView:SetClipsDescendants(false) + if this:IsSelected() then + this:RemoveFocus() + this:Focus() + end + LockOverlay.Parent = nil + end + + function this:IsLocked() + return locked + end + + function this:SetLockInPUP(State) + lockInPUP = State + end + + function this:GetLockInPUP() + return lockInPUP + end + + function this:Focus() + if inFocus then return end + inFocus = true + if this then + if locked then + if myCarouselController then + onNewGameSelected(myCarouselController:GetFrontGameData(), true) + end + Utility.SetSelectedCoreObject(LockOverlay) + else + myCarouselView:Focus() + if not selectionHolder.Selectable and myCarouselView:GetAvailableItem() then + Utility.SetSelectedCoreObject(myCarouselView:GetAvailableItem()) + else + Utility.SetSelectedCoreObject(selectionHolder) + --Clear game data + onNewGameSelected(nil) + end + end + end + end + + function this:RemoveFocus() + if not inFocus then return end + myCarouselView:RemoveFocus() + inFocus = false + end + + + local fadeDuration = 0.2 + local targetTextTransparency = 0 + local textTransparencyTweens = {} + + local function setTextTransparency(value, duration, refresh) + if not refresh and value == targetTextTransparency then return end + + if duration then + targetTextTransparency = Utility.Clamp(0, 1, targetTextTransparency) + if not refresh and value == targetTextTransparency then return end + else + duration = fadeDuration + end + + Utility.CancelTweens(textTransparencyTweens) + + table.insert(textTransparencyTweens, + Utility.PropertyTweener( + nameLabel, + 'TextTransparency', + targetTextTransparency, + value, + duration, + Utility.EaseOutQuad, + true)) + + targetTextTransparency = value + end + + function this:SetTransparency(imageTransparency, textTransparency, duration, refresh) + setTextTransparency(textTransparency, duration, refresh) + myCarouselView:SetTransparency(imageTransparency, duration, refresh) + if currentLoader then + currentLoader:SetTransparency(Utility.Clamp(0, 1, imageTransparency)) + end + end + + function this:Destroy() + container:Destroy() + this = nil + end + + function this:IsSelected() + if GuiService.SelectedCoreObject then + return GuiService.SelectedCoreObject == selectionHolder or myCarouselView:ContainsItem(GuiService.SelectedCoreObject) or + GuiService.SelectedCoreObject == LockOverlay + end + + return false + end + + return this +end + +return GameCarouselItem diff --git a/Client2018/content/internal/AppShell/Modules/Shell/GameData.lua b/Client2018/content/internal/AppShell/Modules/Shell/GameData.lua new file mode 100644 index 0000000..9d9af5d --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/GameData.lua @@ -0,0 +1,860 @@ +--[[ + // GameData.lua + + // Fetches data for a game to be used to fill out + // the details of that game +]] +local XboxUpdateBadgeEndpoints = settings():GetFFlag("XboxUpdateBadgeEndpoints") + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Http = require(ShellModules:FindFirstChild('Http')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local ConvertMyPlaceNameInXboxAppFlag = Utility.IsFastFlagEnabled("ConvertMyPlaceNameInXboxApp") +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local CreateCacheData = require(ShellModules:FindFirstChild('CachedData')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local XboxAppState = require(ShellModules:FindFirstChild('AppState')) + +local ThirdPartyUserService = nil +pcall(function()ThirdPartyUserService = game:GetService('ThirdPartyUserService') end) + +local GameData = {} + +local gameCreatorCache = {} +function GameData:GetGameCreatorAsync(placeId) + if placeId then + if not gameCreatorCache[placeId] then + local gameDataByPlaceId = self:GetGameDataAsync(placeId) + gameCreatorCache[placeId] = gameDataByPlaceId:GetCreatorName() + end + return gameCreatorCache[placeId] + end +end + +function GameData:ExtractGeneratedUsername(gameName) + local tempUsername = string.match(gameName, "^([0-9a-fA-F]+)'s Place$") + if tempUsername and #tempUsername == 32 then + return tempUsername + end +end + +-- Fix places that have been made with incorrect temporary usernames +-- creatorName is optional and must be used when querying a game that is +-- not the current user's creation +function GameData:GetFilteredGameName(gameName, creatorName) + if ConvertMyPlaceNameInXboxAppFlag and gameName and type(gameName) == 'string' then + local tempUsername = self:ExtractGeneratedUsername(gameName) + if tempUsername then + local realUsername = creatorName or XboxAppState.store:getState().RobloxUser.robloxName + if realUsername then + local newGameName = string.gsub(gameName, tempUsername, realUsername, 1) + if newGameName then + return newGameName + end + end + end + end + return gameName +end + +function GameData:GetGameDataAsync(placeId) + local this = {} + + local result = Http.GetGameDetailsAsync(placeId) + if not result then + Utility.DebugLog("GameData:GetGameDataAsync() failed to get web response for placeId "..tostring(placeId)) + result = {} + end + + this.Data = result + + --[[ Public API ]]-- + function this:GetCreatorName() + return self.Data["Builder"] or "" + end + function this:GetDescription() + return self.Data["Description"] or "" + end + function this:GetIsFavoritedByUser() + return self.Data["IsFavoritedByUser"] or false + end + function this:GetLastUpdated() + return self.Data["Updated"] or "" + end + function this:GetCreationDate() + return self.Data["Created"] or "" + end + function this:GetMaxPlayers() + return self.Data["MaxPlayers"] or 0 + end + function this:GetOverridesDefaultAvatar() + return self.Data["OverridesDefaultAvatar"] or false + end + --IsExperimental means not Filtering Enabled + function this:GetIsExperimental() + return self.Data["IsExperimental"] or false + end + function this:GetCreatorUserId() + return self.Data["BuilderId"] + end + + --[[ Async Public API ]]-- + function this:GetVoteDataAsync() + local result = Http.GetGameVotesAsync(placeId) + if not result then + Utility.DebugLog("GameData:GetVoteDataAsync() failed to get web response for placeId "..tostring(placeId)) + end + + local voteData = {} + local voteTable = result and result["VotingModel"] or nil + + if voteTable then + voteData.UpVotes = voteTable["UpVotes"] or 0 + voteData.DownVotes = voteTable["DownVotes"] or 0 + voteData.UserVote = voteTable["UserVote"] + voteData.CanVote = voteTable["CanVote"] or false + voteData.CantVoteReason = voteTable["ReasonForNotVoteable"] or "PlayGame" + end + + return voteData + end + + function this:GetGameIconIdAsync() + local iconId = nil + local result = Http.GetGameIconIdAsync(placeId) + if result then + iconId = result["ImageId"] + -- use placeId as backup + if not iconId then + iconId = placeId + end + end + + return iconId + end + + function this:GetRecommendedGamesAsync() + local result = Http.GetRecommendedGamesAsync(placeId) + if not result then + Utility.DebugLog("GameData:GetRecommendedGamesAsync() failed to get web response for placeId "..tostring(placeId)) + return {} + end + + local recommendedGames = {} + for i = 1, #result do + local data = result[i] + if data then + local game = {} + -- Temp fix for fixing game names + game.Name = GameData:GetFilteredGameName(data["GameName"], data["Creator"] and data["Creator"]["CreatorName"]) + game.PlaceId = data["PlaceId"] + game.IconId = data["ImageId"] + table.insert(recommendedGames, game) + end + end + + return recommendedGames + end + + function this:GetThumbnailIdsAsync() + local result = Http.GetGameThumbnailsAsync(placeId) + if not result then + Utility.DebugLog("GameData:GetThumbnailIdsAsync() failed to get web response for placeId "..tostring(placeId)) + return {} + end + + local thumbIds = {} + local thumbIdTable = result["thumbnails"] + if thumbIdTable then + for i = 1, #thumbIdTable do + local data = thumbIdTable[i] + -- AssetTypeId of 1 is a Image (33 is a video if can ever play videos) + if data and data["AssetTypeId"] == 1 then + local assetId = data["AssetId"] + if assetId then + table.insert(thumbIds, assetId) + end + end + end + end + + return thumbIds + end + + function this:GetBadgeDataAsync() + local result = Http.GetGameBadgeDataAsync(placeId) + if not result then + Utility.DebugLog("GameData:GetBadgeDataAsync() failed to get web response for placeId "..tostring(placeId)) + return {} + end + + local badgeData = {} + local badgeTable = result["GameBadges"] + if badgeTable then + for i = 1, #badgeTable do + local data = badgeTable[i] + if data then + local badge = {} + badge.Name = data["Name"] + badge.Description = data["Description"] + badge.AssetId = data["BadgeAssetId"] + badge.IsOwned = data["IsOwned"] + badge.Order = i + table.insert(badgeData, badge) + end + end + end + + table.sort(badgeData, function(a, b) + if a["IsOwned"] == true and b["IsOwned"] == true then + return a.Order < b.Order + elseif a["IsOwned"] then + return true + elseif b["IsOwned"] then + return false + end + return a.Order < b.Order + end) + + return badgeData + end + + --[[ Post Public API ]]-- + function this:PostFavoriteAsync() + local result = Http.PostFavoriteToggleAsync(placeId) + local success = result and result["success"] == true + EventHub:dispatchEvent(EventHub.Notifications["FavoriteToggle"], success) + if not success then + local reason = "Failed" + -- the floodcheck message is "Whoa. Slow Down.". So if there is a message, + -- let's just say flood check? + if result and result["message"] then + reason = "FloodCheck" + end + return success, reason + else + self.Data["IsFavoritedByUser"] = not self:GetIsFavoritedByUser() + end + return success + end + + function this:PostVoteAsync(status) + local result = Http.PostGameVoteAsync(placeId, status) + if not result then + return nil + end + + local success = result["Success"] == true + if not success then + return success, result["ModalType"] + end + + return success + end + + -- Temp fix for fixing game names + if this.Data then + this.Data.Name = GameData:GetFilteredGameName(this.Data.Name, this:GetCreatorName()) + end + + return this +end + + +--New GameData Cached +local UserChangedCount = 0 +local maxGameCachedDataCount = 5000 +local currGameCachedDataCount = 0 +local gameCachedData = {} + +if ThirdPartyUserService then + ThirdPartyUserService.ActiveUserSignedOut:connect(function() + GameData:FlushGameData() + UserChangedCount = UserChangedCount + 1 + end) +end + + +--Currently, UpdateGameData will be called in three places: +--1. When we request the games in sort, we UpdateGameData with full args(placeId, name, creatorName, iconId, voteData, creatorId) +--2. When we request the Recommended Games in sort, we UpdateGameData with first 4 args(placeId, name, creatorName, iconId) +--3. When we try to GameData:GetGameData(placeId) but found the data has been flushed out, we will UpdateGameData with placeId and re-fetch the data +--args order: placeId, name, creatorName, iconId, voteData, creatorId +function GameData:UpdateGameData(...) + local args = { n = select("#", ...); ... } + if args.n >= 1 then + local placeId = args[1] + local name = args[2] + local creatorName = args[3] + local iconId = args[4] + local voteData = args[5] + local creatorId = args[6] + + if not gameCachedData[placeId] then + gameCachedData[placeId] = {} + + local this = gameCachedData[placeId] + this.UpdateDebounce = true + this.RelatedGuiObjects = {} + this.AccessCount = 1 + + local function GetGameDetailsRefreshInterval() + return GlobalSettings.GameDetailsRefreshInterval + end + + --Game Details Data + local function RefreshGameDetailsAsync(gameData) + local startCount = UserChangedCount + local Valid = false + local result = Http.GetGameDetailsAsync(gameData.PlaceId) + if not result then + Utility.DebugLog("RefreshGameDetailsAsync() failed to get web response for placeId "..tostring(gameData.PlaceId)) + else + if startCount == UserChangedCount then + --for CreatorName and Name, we use the data from initialization unless they were not provided when init + gameData.CreatorName = gameData.CreatorName or result["Builder"] or "" + gameData.Name = gameData.Name or GameData:GetFilteredGameName(result["Name"], gameData.CreatorName) + + gameData.Description = result["Description"] or "" + gameData.IsFavorited = result["IsFavoritedByUser"] or false + gameData.LastUpdated = result["Updated"] or "" + gameData.CreationDate = result["Created"] or "" + gameData.MaxPlayers = result["MaxPlayers"] or 0 + gameData.OverridesDefaultAvatar = result["OverridesDefaultAvatar"] or false + gameData.IsExperimental = result["IsExperimental"] or false + gameData.CreatorUserId = result["BuilderId"] + gameData.UniverseId = result["UniverseId"] or 0 + Valid = true + end + end + + return Valid + end + local GameDetailsCachedData = CreateCacheData(this, nil, GetGameDetailsRefreshInterval, RefreshGameDetailsAsync) + + --Vote Data + local function RefreshVoteDataAsync(gameData) + local startCount = UserChangedCount + local Valid = false + local result = Http.GetGameVotesAsync(gameData.PlaceId) + if not result then + Utility.DebugLog("RefreshVoteDataAsync() failed to get web response for placeId "..tostring(gameData.PlaceId)) + else + if startCount == UserChangedCount then + local voteData = {} + local voteTable = result and result["VotingModel"] or nil + + if voteTable then + voteData.UpVotes = voteTable["UpVotes"] or 0 + voteData.DownVotes = voteTable["DownVotes"] or 0 + voteData.UserVote = voteTable["UserVote"] + voteData.CanVote = voteTable["CanVote"] or false + voteData.CantVoteReason = voteTable["ReasonForNotVoteable"] or "PlayGame" + gameData.VoteData = voteData + Valid = true + end + end + end + + return Valid + end + + local VoteCachedData = CreateCacheData(this, nil, GetGameDetailsRefreshInterval, RefreshVoteDataAsync) + + --Recommended Games + local function RefreshRecommendedGamesAsync(gameData) + local startCount = UserChangedCount + local Valid = false + local result = Http.GetRecommendedGamesAsync(gameData.PlaceId) + if not result then + Utility.DebugLog("RefreshRecommendedGamesAsync() failed to get web response for placeId "..tostring(gameData.PlaceId)) + else + if startCount == UserChangedCount then + --clear old data + if gameData.RecommendedGames then + for i = 1, #gameData.RecommendedGames do + local recommendedGamePlaceId = gameData.RecommendedGames[i] + if recommendedGamePlaceId then + local recommendedGameData = GameData:GetGameData(recommendedGamePlaceId) + if recommendedGameData then + recommendedGameData.AccessCount = recommendedGameData.AccessCount - 1 + end + end + gameData.RecommendedGames[i]= nil + end + end + + local recommendedGames = {} + for i = 1, #result do + local data = result[i] + if data then + -- Temp fix for fixing game names + local creatorName = data["Creator"] and data["Creator"]["CreatorName"] + local name = GameData:GetFilteredGameName(data["GameName"], creatorName) + local placeId = data["PlaceId"] + local iconId = data["ImageId"] + + GameData:UpdateGameData(placeId, name, creatorName, iconId) + table.insert(recommendedGames, placeId) + end + end + + gameData.RecommendedGames = recommendedGames + Valid = true + end + end + + return Valid + end + + local RecommendedGamesCachedData = CreateCacheData(this, nil, GetGameDetailsRefreshInterval, RefreshRecommendedGamesAsync) + + --ThumbnailIds + local function RefreshThumbnailIdsAsync(gameData) + local startCount = UserChangedCount + local Valid = false + local result = Http.GetGameThumbnailsAsync(gameData.PlaceId) + if not result then + Utility.DebugLog("RefreshThumbnailIdsAsync() failed to get web response for placeId "..tostring(gameData.PlaceId)) + else + if startCount == UserChangedCount then + local thumbIds = {} + local thumbIdTable = result["thumbnails"] + if thumbIdTable then + for i = 1, #thumbIdTable do + local data = thumbIdTable[i] + -- AssetTypeId of 1 is a Image (33 is a video if can ever play videos) + if data and data["AssetTypeId"] == 1 then + local assetId = data["AssetId"] + if assetId then + table.insert(thumbIds, assetId) + end + end + end + end + + gameData.ThumbnailIds = thumbIds + Valid = true + end + end + return Valid + end + + local ThumbnailIdsCachedData = CreateCacheData(this, nil, GetGameDetailsRefreshInterval, RefreshThumbnailIdsAsync) + + + --Badges + local function RefreshBadgeDataAsync(gameData) + if XboxUpdateBadgeEndpoints then + local startCount = UserChangedCount + + local badgesData = {} + local awardedBadges = {} + local nextPageCursor; + + repeat + local result = Http.GetBadgesForUniverseAsync(gameData.UniverseId, nextPageCursor) + if result and result.data then + local badgeIds = {} + for _,badgeData in ipairs(result.data) do + if badgeData.enabled then + table.insert(badgesData, badgeData) + table.insert(badgeIds, badgeData.id) + end + end + + if #badgeIds > 0 then + local awardedBadgesResult = Http.GetUserAwardedBadgesAsync( + XboxAppState.store:getState().RobloxUser.rbxuid, + badgeIds + ) + + if awardedBadgesResult and awardedBadgesResult.data then + for _,badge in ipairs(awardedBadgesResult.data) do + awardedBadges[badge.badgeId] = true + end + end + end + + nextPageCursor = result.nextPageCursor + else + nextPageCursor = nil + end + until nextPageCursor == nil + + if startCount ~= UserChangedCount then + return false + end + + local finalBadgesData = {} + for i, badge in ipairs(badgesData) do + local item = { + Name = badge.name, + Description = badge.description, + AssetId = badge.id, + Order = i, + } + + if awardedBadges[item.AssetId] then + item.IsOwned = true + end + + table.insert(finalBadgesData, item) + end + + table.sort(finalBadgesData, function(a, b) + if a["IsOwned"] == true and b["IsOwned"] == true then + return a.Order < b.Order + elseif a["IsOwned"] then + return true + elseif b["IsOwned"] then + return false + end + return a.Order < b.Order + end) + + gameData.BadgeData = finalBadgesData + return true + end + + local startCount = UserChangedCount + local Valid = false + local result = Http.GetGameBadgeDataAsync(gameData.PlaceId) + if not result then + Utility.DebugLog("GameData:GetBadgeDataAsync() failed to get web response for placeId "..tostring(gameData.PlaceId)) + else + if startCount == UserChangedCount then + local badgeData = {} + local badgeTable = result["GameBadges"] + if badgeTable then + for i = 1, #badgeTable do + local data = badgeTable[i] + if data then + local badge = {} + badge.Name = data["Name"] + badge.Description = data["Description"] + badge.AssetId = data["BadgeAssetId"] + badge.IsOwned = data["IsOwned"] + badge.Order = i + table.insert(badgeData, badge) + end + end + end + + table.sort(badgeData, function(a, b) + if a["IsOwned"] == true and b["IsOwned"] == true then + return a.Order < b.Order + elseif a["IsOwned"] then + return true + elseif b["IsOwned"] then + return false + end + return a.Order < b.Order + end) + + gameData.BadgeData = badgeData + Valid = true + end + end + return Valid + end + + local BadgeCachedData = CreateCacheData(this, nil, GetGameDetailsRefreshInterval, RefreshBadgeDataAsync) + + --[[ Async Public API ]]-- + --We don't want to run so many loading funcs in the BG at intervals, + --so, these reload will be made when the data is requested + function this:GetGameDetailsAsync() + GameDetailsCachedData:Refresh() + local gameData = self or {} + self.OnGetGameDetailsEnd:fire(gameData) + return gameData + end + + function this:GetVoteDataAsync() + VoteCachedData:Refresh() + local voteData = self.VoteData or {} + self.OnGetVoteDataEnd:fire(voteData) + return voteData + end + + function this:GetRecommendedGamesAsync() + RecommendedGamesCachedData:Refresh() + local recommendedGames = self.RecommendedGames or {} + self.OnGetRecommendedGamesEnd:fire(recommendedGames) + return recommendedGames + end + + function this:GetThumbnailIdsAsync() + ThumbnailIdsCachedData:Refresh() + local thumbnailIds = self.ThumbnailIds or {} + self.OnGetThumbnailIdsEnd:fire(thumbnailIds) + return thumbnailIds + end + + function this:GetBadgeDataAsync() + BadgeCachedData:Refresh() + local badgeData = self.BadgeData or {} + self.OnGetBadgeDataEnd:fire(badgeData) + return badgeData + end + + --We force update IconId whenever it get called + local debounceGetGameIconIdAsync = false + function this:GetGameIconIdAsync() + while debounceGetGameIconIdAsync do + wait() + end + debounceGetGameIconIdAsync = true + + local startCount = UserChangedCount + local iconId = nil + local result = Http.GetGameIconIdAsync(placeId) + if result then + iconId = result["ImageId"] + -- use placeId as backup + if not iconId then + iconId = placeId + end + end + + if startCount == UserChangedCount then + self.IconId = iconId + end + debounceGetGameIconIdAsync = false + return iconId + end + + function this:SetCanVote(value) + local voteData = self.VoteData + voteData.CanVote = value + voteData.CantVoteReason = value and "" or "PlayGame" + end + + + --[[ Post Public API ]]-- + function this:PostFavoriteAsync() + local result = Http.PostFavoriteToggleAsync(placeId) + local success = result and result["success"] == true + local reason = nil + if not success then + reason = "Failed" + -- the floodcheck message is "Whoa. Slow Down.". So if there is a message, + -- let's just say flood check? + if result and result["message"] then + reason = "FloodCheck" + end + else + self.IsFavorited = not self.IsFavorited + end + EventHub:dispatchEvent(EventHub.Notifications["FavoriteToggle"], success, self.PlaceId) + + if not success then + return success, reason + else + return success + end + end + + function this:PostVoteAsync(newVote) + local result = Http.PostGameVoteAsync(placeId, newVote) + if not result then + return nil + end + + local success = result["Success"] == true + if not success then + return success, result["ModalType"] + else + local voteData = self.VoteData + local prevVote = voteData.UserVote + if newVote == true then + voteData.UpVotes = (voteData.UpVotes or 0) + 1 + if prevVote == false then + voteData.DownVotes = (voteData.DownVotes or 0) - 1 + end + elseif newVote == false then + voteData.DownVotes = (voteData.DownVotes or 0) + 1 + if prevVote == true then + voteData.UpVotes = (voteData.UpVotes or 0) - 1 + end + elseif newVote == nil then + if prevVote == true then + voteData.UpVotes = (voteData.UpVotes or 0) - 1 + elseif prevVote == false then + voteData.DownVotes = (voteData.DownVotes or 0) - 1 + end + end + voteData.UserVote = newVote + end + + return success + end + + --Init Data and Signals + this.OnGetGameDetailsEnd = Utility.Signal() + this.OnGetVoteDataEnd = Utility.Signal() + this.OnGetRecommendedGamesEnd = Utility.Signal() + this.OnGetThumbnailIdsEnd = Utility.Signal() + this.OnGetBadgeDataEnd = Utility.Signal() + + if args.n > 1 then + if args.n > 3 then + this.PlaceId = placeId + this.Name = GameData:GetFilteredGameName(name, creatorName) + this.CreatorName = creatorName + this.IconId = iconId + end + + --these two are optional + if args.n > 5 then + this.VoteData = voteData + this.CreatorUserId = creatorId + end + else + this.PlaceId = placeId + --No init data, we fetch the essential ones (name, creatorName, iconId) + this:GetGameDetailsAsync() + this:GetGameIconIdAsync() + end + + this.UpdateDebounce = false + currGameCachedDataCount = currGameCachedDataCount + 1 + + --Need to clear Cached Data now + if currGameCachedDataCount > maxGameCachedDataCount then + local gameCachedDataSorted = {} + for key in pairs(gameCachedData) do + --Add key attribute + gameCachedData[key].Key = key + --Recalc the Total AccessCount for every cached data + gameCachedData[key].TotalAccessCount = 0 + if gameCachedData[key].RelatedGuiObjects then + local newRelatedGuiObjects = {} + for i = 1, #gameCachedData[key].RelatedGuiObjects do + local relatedGuiObject = gameCachedData[key].RelatedGuiObjects[i] + if relatedGuiObject and relatedGuiObject:IsA("GuiObject") and relatedGuiObject.Parent then + gameCachedData[key].TotalAccessCount = gameCachedData[key].TotalAccessCount + 1 + table.insert(newRelatedGuiObjects, relatedGuiObject) + end + end + gameCachedData[key].TotalAccessCount = gameCachedData[key].TotalAccessCount + gameCachedData[key].AccessCount + gameCachedData[key].RelatedGuiObjects = newRelatedGuiObjects + end + table.insert(gameCachedDataSorted, gameCachedData[key]) + end + + currGameCachedDataCount = #gameCachedDataSorted + --Double check, in case we miscounted the currGameCachedDataCount + if currGameCachedDataCount > maxGameCachedDataCount then + --Put not used data in the back + table.sort(gameCachedDataSorted, function(a, b) + return a.TotalAccessCount > b.TotalAccessCount + end) + + --Make sure the removed data is not used anywhere and clear all not in use cached data + --kind of optimization so we don't clear caching for each single overloaded item + while gameCachedDataSorted[#gameCachedDataSorted] and gameCachedDataSorted[#gameCachedDataSorted].TotalAccessCount <= 0 do + local RemovingKey = gameCachedDataSorted[#gameCachedDataSorted].Key + --Clear RecommendedGames Access + if gameCachedData[RemovingKey].RecommendedGames then + local recommendedGames = gameCachedData[RemovingKey].RecommendedGames + for i = 1, #recommendedGames do + local recommendedGamePlaceId = recommendedGames[i] + if recommendedGamePlaceId then + local recommendedGameData = GameData:GetGameData(recommendedGamePlaceId) + if recommendedGameData then + recommendedGameData.AccessCount = recommendedGameData.AccessCount - 1 + end + end + recommendedGames[i]= nil + end + end + gameCachedData[RemovingKey] = nil + gameCachedDataSorted[#gameCachedDataSorted] = nil + end + currGameCachedDataCount = #gameCachedDataSorted + end + end + else + local gameData = gameCachedData[placeId] + gameData.UpdateDebounce = true + gameData.AccessCount = gameData.AccessCount and gameData.AccessCount + 1 or 1 + + --Update Data + if args.n > 3 then + gameData.PlaceId = placeId + gameData.Name = GameData:GetFilteredGameName(name, creatorName) + gameData.CreatorName = creatorName + + --We only use the IconId comes with init to avoid game button icon updates + if not gameData.IconId then + gameData.IconId = iconId + end + end + + --these two are optional + if args.n > 5 then + gameData.VoteData = voteData + gameData.CreatorUserId = creatorId + end + + gameData.UpdateDebounce = false + end + end +end + + +function GameData:GetGameData(placeId, asyncFetch) + if placeId then + if asyncFetch then + if gameCachedData[placeId] then + --if asyncFetch, we wait until the update is done + while gameCachedData[placeId] and gameCachedData[placeId].UpdateDebounce do + wait() + end + else + --Will refetch data if the data has been flushed from cached data, + --should never happen, as we don't clear cached data as long as there is still any access to it + GameData:UpdateGameData(placeId) + end + end + return gameCachedData[placeId] + end +end + +--This is the most robust way to ensure that we don't flush cached gamedata when it's still used on some guiObject. +--Make sure the guiObject already has Parent set up when added +function GameData:AddRelatedGuiObject(placeId, guiObject) + if placeId and guiObject and guiObject:IsA("GuiObject") and guiObject.Parent then + local gameData = GameData:GetGameData(placeId) + if gameData then + table.insert(gameData.RelatedGuiObjects, guiObject) + end + end +end + +--This is the most robust way to ensure that we don't flush cached gamedata when it's still used on sort pages. +function GameData:ChangeGameDataAccessCount(placeId, changeValue) + if placeId then + local gameData = GameData:GetGameData(placeId) + if gameData then + gameData.AccessCount = gameData.AccessCount + changeValue + end + end +end + +function GameData:FlushGameData() + for placeId,_ in pairs(gameCachedData) do + gameCachedData[placeId] = nil + end + gameCachedData = {} + currGameCachedDataCount = 0 +end + +return GameData diff --git a/Client2018/content/internal/AppShell/Modules/Shell/GameDetailScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/GameDetailScreen.lua new file mode 100644 index 0000000..850f80c --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/GameDetailScreen.lua @@ -0,0 +1,1239 @@ +--[[ + // GameDetailScreen.lua +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local GuiService = game:GetService('GuiService') +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) + +local AssetManager = require(ShellModules:FindFirstChild('AssetManager')) +local GameData = require(ShellModules:FindFirstChild('GameData')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local ImageOverlayModule = require(ShellModules:FindFirstChild('ImageOverlay')) +local ReportOverlayModule = require(ShellModules:FindFirstChild('ReportOverlay')) +local BadgeSortModule = require(ShellModules:FindFirstChild('BadgeSort')) +local ScrollingTextBox = require(ShellModules:FindFirstChild('ScrollingTextBox')) +local PopupText = require(ShellModules:FindFirstChild('PopupText')) +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) +local Errors = require(ShellModules:FindFirstChild('Errors')) +local ErrorOverlayModule = require(ShellModules:FindFirstChild('ErrorOverlay')) +local GameJoinModule = require(ShellModules:FindFirstChild('GameJoin')) +local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local VoteViewModule = require(ShellModules:FindFirstChild('VoteView')) +local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local WidgetModules = ShellModules:FindFirstChild("Widgets") +local MoreButtonModule = require(WidgetModules:FindFirstChild("MoreButton")) +local AchievementManager = require(ShellModules:FindFirstChild('AchievementManager')) + +local function CreateGameDetail(placeId) + local this = BaseScreen() + + local PLAY_BUTTON_SIZE = Vector2.new(228,72) + local FAVORITE_BUTTON_SIZE = Vector2.new(228,72) + + local LEFT_MARGIN = 8 + local TWEEN_TIME = 0.5 + local BASE_ZINDEX = 2 + local baseButtonTextColor = GlobalSettings.WhiteTextColor + local selectedButtonTextColor = GlobalSettings.TextSelectedColor + + local selectionChangedCn = nil + local dataModelViewChangedCn = nil + local playButtonDebounce = false + local canJoinGame = true + local returnedFromGame = true + local gameData = nil + + --[[ Cache Game Data ]]-- + local getGameDataDebounce = false + --This func will be async if placeId not stored in gamecacheddata + local function getDataAsync() + while getGameDataDebounce do + wait() + end + getGameDataDebounce = true + if not gameData then + gameData = GameData:GetGameData(placeId, true) + --We get details(refresh) only once to make sure the game detail pieces are synced + gameData:GetGameDetailsAsync() + end + getGameDataDebounce = false + return gameData + end + + -- override selection image + local edgeSelectionImage = Utility.Create'ImageLabel' + { + Name = "EdgeSelectionImage"; + Size = UDim2.new(1, 32, 1, 32); + Position = UDim2.new(0, -16, 0, -16); + Image = 'rbxasset://textures/ui/SelectionBox.png'; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(21,21,41,41); + BackgroundTransparency = 1; + } + +--[[ Top Level Elements ]]-- + --This Scrolling Frame ensures that we can select sth off screen, so we won't lose selection + local GameDetailContainer = Utility.Create'ScrollingFrame' + { + Name = "ScrollingFrame"; + ClipsDescendants = false; + Size = UDim2.new(1, 0, 1, 0); + CanvasSize = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + Parent = this.Container; + ScrollingEnabled = false; + Selectable = false; + BorderSizePixel = 0; + ScrollBarThickness = 0; + } + + this:SetTitle('') + spawn(function() + local data = getDataAsync() + this:SetTitle(data.Name or '') + end) + + --[[ Inner Class - ContentManager - Handles Positioning/Visiblity of content on this screen ]]-- + local function createContentManager(size, position, parent) + local contentManager = {} + + -- items are an array from left to right + local contentItems = {} + local isTweeningContentLeft = false + local isTweeningContentRight = false + + local contentContainer = Utility.Create'Frame' + { + Name = "ContentContainer"; + Size = size; + Position = position; + BackgroundTransparency = 1; + Parent = parent; + } + + local function recalcLayout() + local offset = 0 + for i = 1, #contentItems do + local currentItem = contentItems[i] + currentItem.Item.Position = UDim2.new(0, offset + currentItem.Padding.x, 0, currentItem.Padding.y) + offset = offset + currentItem.Item.Size.X.Offset + currentItem.Padding.x + end + end + + function contentManager:AddItem(newItem, padding) + local contentItem = {} + contentItem.Item = newItem + contentItem.Padding = padding or Vector2.new(0, 0) + newItem.Parent = contentContainer + table.insert(contentItems, contentItem) + recalcLayout() + end + + function contentManager:RemoveItem(itemToRemove) + for i = 1, #contentItems do + local contentItem = contentItems[i] + if contentItem.Item == itemToRemove then + contentItem.Item.Parent = nil + table.remove(contentItems, i) + recalcLayout() + return + end + end + end + + function contentManager:TweenContentLeft() + if not isTweeningContentLeft then + isTweeningContentLeft = true + contentContainer:TweenPosition(UDim2.new(0, LEFT_MARGIN, 0, contentContainer.Position.Y.Offset), Enum.EasingDirection.InOut, + Enum.EasingStyle.Quad, TWEEN_TIME, true, function(tweenStatus) + isTweeningContentLeft = false + end) + end + end + + function contentManager:TweenContentRight() + if not isTweeningContentRight then + isTweeningContentRight = true + contentContainer:TweenPosition(UDim2.new(-1, LEFT_MARGIN, 0, contentContainer.Position.Y.Offset), Enum.EasingDirection.InOut, + Enum.EasingStyle.Quad, TWEEN_TIME, true, function(tweenStatus) + isTweeningContentRight = false + end) + end + end + + function contentManager:TweenContent(selectedObject) + if selectedObject and selectedObject:IsDescendantOf(contentContainer) then + -- find parent container of selection + local parentContainer = selectedObject + while parentContainer.Parent ~= contentContainer do + parentContainer = parentContainer.Parent + end + + if parentContainer.Position.X.Offset + parentContainer.Size.X.Offset > contentContainer.AbsoluteSize.x then + self:TweenContentRight() + elseif parentContainer.Position.X.Offset < contentContainer.AbsoluteSize.x then + self:TweenContentLeft() + end + end + end + + function contentManager:ResetTween() + isTweeningContentLeft = false + isTweeningContentRight = false + end + + return contentManager + end + + local MyContentManager = createContentManager(UDim2.new(1, 0, 0, 622), UDim2.new(0, LEFT_MARGIN, 0, 210), GameDetailContainer) + + local PlayButton = Utility.Create'ImageButton' + { + Name = "PlayButton"; + Size = UDim2.new(0, PLAY_BUTTON_SIZE.X, 0, PLAY_BUTTON_SIZE.Y); + Position = UDim2.new(0, 0, 1, -77); + BackgroundTransparency = 1; + ImageColor3 = GlobalSettings.GreenButtonColor; + Image = GlobalSettings.RoundCornerButtonImage; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28)); + Parent = GameDetailContainer; + ZIndex = BASE_ZINDEX; + + SoundManager:CreateSound('MoveSelection'); + AssetManager.CreateShadow(1) + } + + local PlayText = Utility.Create'TextLabel' + { + Name = "PlayText"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Text = Strings:LocalizedString("PlayWord"); + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = baseButtonTextColor; + ZIndex = BASE_ZINDEX; + Parent = PlayButton; + } + Utility.ResizeButtonWithText(PlayButton, PlayText, GlobalSettings.TextHorizontalPadding) + + local FavoriteButton = Utility.Create'ImageButton' + { + Name = "FavoriteButton"; + Size = UDim2.new(0, FAVORITE_BUTTON_SIZE.X, 0, FAVORITE_BUTTON_SIZE.Y); + BackgroundTransparency = 1; + ImageColor3 = GlobalSettings.GreyButtonColor; + ScaleType = Enum.ScaleType.Slice; + Parent = GameDetailContainer; + Position = UDim2.new(0, PlayButton.Size.X.Offset + 10, 1, -77); + Image = GlobalSettings.RoundCornerButtonImage; + SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28)); + ZIndex = BASE_ZINDEX; + + SoundManager:CreateSound('MoveSelection'); + AssetManager.CreateShadow(1) + } + + FavoriteButton.NextSelectionLeft = PlayButton; + FavoriteButton.NextSelectionDown = FavoriteButton; + FavoriteButton.NextSelectionUp = FavoriteButton; + FavoriteButton.NextSelectionRight = FavoriteButton; + + local FavoriteText = Utility.Create'TextLabel' + { + Name = "FavoriteText"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Text = Strings:LocalizedString("FavoriteWord"); + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = baseButtonTextColor; + ZIndex = BASE_ZINDEX; + Parent = FavoriteButton; + } + + local FavoriteStarImage = Utility.Create'ImageLabel' + { + Name = "FavoriteStarImage"; + BackgroundTransparency = 1; + Visible = false; + ZIndex = BASE_ZINDEX; + Parent = FavoriteButton; + Image = "rbxasset://textures/ui/Shell/Icons/FavoriteStar@1080.png"; + Size = UDim2.new(0,32,0,31); + } + FavoriteStarImage.Position = UDim2.new(0, 16, 0.5, -FavoriteStarImage.Size.Y.Offset / 2) + --Make it big enough to hold the star and text + Utility.ResizeButtonWithDynamicText(FavoriteButton, FavoriteText, {Strings:LocalizedString("FavoritedWord")}, + GlobalSettings.TextHorizontalPadding + (FavoriteStarImage.Position.X.Offset + FavoriteStarImage.Size.X.Offset + 12) / 2) + + local FavoriteRedirectFrame = Utility.Create'Frame' + { + Name = "FavoriteRedirectFrame"; + Size = UDim2.new(1, 0, 0, FavoriteButton.Size.Y.Offset); + Position = UDim2.new(0, FavoriteButton.Position.X.Offset + FavoriteButton.Size.X.Offset, 1, FavoriteButton.Position.Y.Offset); + BackgroundTransparency = 1; + Selectable = true; + Parent = GameDetailContainer; + } + +--[[ Scrolling Content Items ]]-- + + --[[ Icon Image ]]-- + local GameIconContainer = Utility.Create'Frame' + { + Name = "GameIconContainer"; + Size = UDim2.new(0, 566, 1, 0); + BackgroundTransparency = 1; + } + MyContentManager:AddItem(GameIconContainer, Vector2.new(0, 0)) + local GameIconImage = Utility.Create'ImageLabel' + { + Name = "GameIconImage"; + Size = UDim2.new(1, 0, 0, 566); + Position = UDim2.new(0, 0, 0.5, -566/2); + BackgroundTransparency = 0; + BorderSizePixel = 0; + BackgroundColor3 = Color3.new(); + ZIndex = BASE_ZINDEX; + Parent = GameIconContainer; + AssetManager.CreateShadow(1); + } + local function loadGameIcon(assetId) + local gameIconLoader = ThumbnailLoader:Create(GameIconImage, assetId, + ThumbnailLoader.Sizes.Medium, ThumbnailLoader.AssetType.SquareIcon) + spawn(function() + gameIconLoader:LoadAsync(true, true, { ZIndex = GameIconImage.ZIndex } ) + end) + end + + spawn(function() + local data = getDataAsync() + loadGameIcon(data.IconId) + GameData:AddRelatedGuiObject(placeId, GameIconImage) + end) + + --[[ Rating and Description ]]-- + local RatingDescriptionContainer = Utility.Create'Frame' + { + Name = "RatingDescriptionContainer"; + Size = UDim2.new(0, 394, 1, 0); + BackgroundTransparency = 1; + } + MyContentManager:AddItem(RatingDescriptionContainer, Vector2.new(38, 0)) + local RatingDescriptionTitle = Utility.Create'TextLabel' + { + Name = "RatingDescriptionTitle"; + Size = UDim2.new(0, 0, 0, 33); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + Text = Strings:LocalizedString("RatingDescriptionTitle"); + TextXAlignment = Enum.TextXAlignment.Left; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.SubHeaderSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Parent = RatingDescriptionContainer; + } + local VoteView = VoteViewModule() + VoteView:SetPosition(UDim2.new(0, 0, 0, RatingDescriptionTitle.Size.Y.Offset)) + local VoteViewContainer = VoteView.Container + + local RatingDescriptionLineBreak = Utility.Create'Frame' + { + Name = "RatingDescriptionLineBreak"; + Size = UDim2.new(0, 362, 0, 2); + Position = UDim2.new(0, 0, 0, VoteViewContainer.Position.Y.Offset + VoteViewContainer.Size.Y.Offset); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.LineBreakColor; + Parent = RatingDescriptionContainer; + } + + --[[ Description Container ]]-- + local DescriptionContainer = Utility.Create'Frame' + { + Name = "DescriptionContainer"; + Size = UDim2.new(1, 0, 0, 430); + Position = UDim2.new(0, 0, 0, RatingDescriptionLineBreak.Position.Y.Offset + RatingDescriptionLineBreak.Size.Y.Offset + 20); + BackgroundTransparency = 1; + Parent = RatingDescriptionContainer; + } + local DescriptionScrollingTextBox = ScrollingTextBox(UDim2.new(1, 0, 1, 0), UDim2.new(0, 0, 0, 0), DescriptionContainer) + + --Used to overwrite the selection when the voteview/descriptionScrollingTextBox selectable changes + local function updateRatingDescriptionSelection() + local defaultVoteSelection = VoteView:GetDefaultSelection() + if defaultVoteSelection then --Make sure the vote view is loaded when overwrite the selections + defaultVoteSelection.NextSelectionLeft = FavoriteButton + + if DescriptionScrollingTextBox:IsSelectable() then + local descriptionSelectionObject = DescriptionScrollingTextBox:GetSelectableObject() + descriptionSelectionObject.NextSelectionUp = defaultVoteSelection + defaultVoteSelection.NextSelectionDown = descriptionSelectionObject + FavoriteButton.NextSelectionRight = descriptionSelectionObject + FavoriteButton.NextSelectionUp = descriptionSelectionObject + + descriptionSelectionObject.NextSelectionLeft = FavoriteButton + descriptionSelectionObject.NextSelectionDown = FavoriteButton + else + defaultVoteSelection.NextSelectionDown = FavoriteButton + FavoriteButton.NextSelectionRight = defaultVoteSelection + FavoriteButton.NextSelectionUp = defaultVoteSelection + end + end + end + +--[[ Line Break ]]-- + local RatingThumbsLineBreak = Utility.Create'Frame' + { + Name = "LineBreak"; + Size = UDim2.new(0, 2, 0, 566); + BackgroundTransparency = 0; + BorderSizePixel = 0; + BackgroundColor3 = Color3.new(78/255, 78/255, 78/255); + BorderSizePixel = 0; + } + MyContentManager:AddItem(RatingThumbsLineBreak, Vector2.new(32, 33)) + +--[[ Additional Thumbs ]]-- + local ThumbsContainer = Utility.Create'Frame' + { + Name = "ThumbsContainer"; + Size = UDim2.new(0, 630, 1, 0); + BackgroundTransparency = 1; + } + MyContentManager:AddItem(ThumbsContainer, Vector2.new(52, 0)) + + local ThumbsTitle = RatingDescriptionTitle:Clone() + ThumbsTitle.Name = "ThumbsTitle" + ThumbsTitle.Text = Strings:LocalizedString("GameImagesTitle") + ThumbsTitle.Parent = ThumbsContainer + + local ThumbsContent = Utility.Create'Frame' + { + Name = "ThumbsContent"; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + Parent = ThumbsContainer; + } + + local MainThumbImage = Utility.Create'ImageButton' + { + Name = "MainThumbImage"; + Size = UDim2.new(1, 0, 0, 374); + Position = UDim2.new(0, 0, 0, ThumbsTitle.Size.Y.Offset); + BackgroundTransparency = 0; + BorderSizePixel = 0; + BackgroundColor3 = Color3.new(); + ZIndex = BASE_ZINDEX; + Parent = ThumbsContent; + + SoundManager:CreateSound('MoveSelection'); + AssetManager.CreateShadow(1); + } + local SmThumbLeft = Utility.Create'ImageButton' + { + Name = "SmThumbLeft"; + Size = UDim2.new(0, 310, 0, 184); + Position = UDim2.new(0, 0, 0, MainThumbImage.Position.Y.Offset + MainThumbImage.Size.Y.Offset + 8); + BackgroundTransparency = 0; + BackgroundColor3 = Color3.new(); + BorderSizePixel = 0; + ZIndex = BASE_ZINDEX; + Parent = ThumbsContent; + + SoundManager:CreateSound('MoveSelection'); + AssetManager.CreateShadow(1); + } + SmThumbRight = SmThumbLeft:Clone() + SmThumbRight.Name = "SmThumbRight" + SmThumbRight.Position = UDim2.new(0, SmThumbLeft.Size.X.Offset + 10, 0, SmThumbRight.Position.Y.Offset) + SmThumbRight.Parent = ThumbsContent + + local MoreThumbsButton = MoreButtonModule() + MoreThumbsButton.Size = UDim2.new(0, 108, 0, 50) + + MoreThumbsButton.Image = "rbxasset://textures/ui/Shell/Buttons/MoreButton@1080.png" + + MoreThumbsButton.Position = UDim2.new(1, -MoreThumbsButton.Size.X.Offset + 3, 0, + SmThumbRight.Position.Y.Offset + SmThumbRight.Size.Y.Offset + 12) + MoreThumbsButton.Visible = false + MoreThumbsButton.ZIndex = BASE_ZINDEX + MoreThumbsButton.SelectionImageObject = nil + MoreThumbsButton.Parent = ThumbsContent + +--[[ Badges ]]-- + local BadgeContainer = Utility.Create'Frame' + { + Name = "BadgeContainer"; + Size = UDim2.new(0, 568, 1, 0); + BackgroundTransparency = 1; + } + MyContentManager:AddItem(BadgeContainer, Vector2.new(52, 0)) + local BadgeTitle = RatingDescriptionTitle:Clone() + BadgeTitle.Name = "BadgeTitle" + BadgeTitle.Text = Strings:LocalizedString("GameBadgesTitle") + BadgeTitle.Parent = BadgeContainer + + --[[ Related Games ]]-- + local RelatedGamesContainer = Utility.Create'Frame' + { + Name = "RelatedGamesContainer"; + Size = BadgeContainer.Size; + BackgroundTransparency = 1; + } + + local HideRelatedGames = not AchievementManager:AllGamesUnlocked() + if not HideRelatedGames then + MyContentManager:AddItem(RelatedGamesContainer, Vector2.new(52, 0)) + end + + local RelatedGamesTitle = BadgeTitle:Clone() + RelatedGamesTitle.Name = "RelatedGamesTitle" + RelatedGamesTitle.Text = Strings:LocalizedString("RelatedGamesTitle") + RelatedGamesTitle.Parent = RelatedGamesContainer + + local RelatedGamesImageFrame = Utility.Create'Frame' + { + Name = "RelatedGamesImageFrame"; + Size = UDim2.new(1, 0, 566, 0); + Position = UDim2.new(0, 0, 0, RelatedGamesTitle.Size.Y.Offset); + BackgroundTransparency = 1; + Parent = RelatedGamesContainer; + } + -- 2x2 grid of related games + local RelatedGameImages = {} + local relatedIndex = 1 + local relatedMargin = 14 + local relatedSize = (566 - relatedMargin) / 2 + for i = 1, 2 do + for j = 1, 2 do + local image = Utility.Create'ImageButton' + { + Name = tostring(relatedIndex); + Size = UDim2.new(0, relatedSize, 0, relatedSize); + Position = UDim2.new(0, (i - 1) * relatedSize + (i - 1) * relatedMargin, 0, (j - 1) * relatedSize + (j - 1) * relatedMargin); + BackgroundTransparency = 0; + BackgroundColor3 = GlobalSettings.GreyButtonColor; + BorderSizePixel = 0; + ZIndex = BASE_ZINDEX; + Parent = RelatedGamesImageFrame; + + SoundManager:CreateSound('MoveSelection'); + AssetManager.CreateShadow(1); + } + RelatedGameImages[relatedIndex] = image + relatedIndex = relatedIndex + 1 + end + end + + local RelatedGamesMoreDetailsLineBreak = RatingThumbsLineBreak:Clone() + RelatedGamesMoreDetailsLineBreak.Name = "RelatedGamesMoreDetailsLineBreak" + MyContentManager:AddItem(RelatedGamesMoreDetailsLineBreak, Vector2.new(52, RelatedGamesMoreDetailsLineBreak.Position.Y.Offset)) + + --[[ More Details ]]-- + local MoreDetailsContainer = Utility.Create'Frame' + { + Name = "MoreDetailsContainer"; + Size = UDim2.new(0, 386, 1, 0); + BackgroundTransparency = 1; + Selectable = true; + } + MyContentManager:AddItem(MoreDetailsContainer, Vector2.new(52, 0)) + + local MoreDetailsTitle = RelatedGamesTitle:Clone() + MoreDetailsTitle.Name = "MoreDetailsTitle" + MoreDetailsTitle.Text = Strings:LocalizedString("MoreDetailsTitle") + MoreDetailsTitle.Parent = MoreDetailsContainer + + local MoreDetailsContent = Utility.Create'Frame' + { + Name = "MoreDetailsContent"; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + Parent = MoreDetailsContainer; + } + local UpdatedText = Utility.Create'TextLabel' + { + Name = "UpdatedText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, 0, 0, MoreDetailsTitle.Size.Y.Offset + 48); + BackgroundTransparency = 1; + Text = Strings:LocalizedString("LastUpdatedWord"); + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.SmallTitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + Parent = MoreDetailsContent; + } + local LastUpdatedText = Utility.Create'TextLabel' + { + Name = "LastUpdatedText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, 0, 0, UpdatedText.Position.Y.Offset + 34); + BackgroundTransparency = 1; + Text = ""; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.TitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + Parent = MoreDetailsContent; + } + local CreationText = UpdatedText:Clone() + CreationText.Name = "CreationText" + CreationText.Position = UDim2.new(0, 0, 0, LastUpdatedText.Position.Y.Offset + 70) + CreationText.Text = Strings:LocalizedString("CreationDateWord") + CreationText.Parent = MoreDetailsContent + + local CreationDateText = LastUpdatedText:Clone() + CreationDateText.Name = "CreationDateText" + CreationDateText.Position = UDim2.new(0, 0, 0, CreationText.Position.Y.Offset + 34) + CreationDateText.Parent = MoreDetailsContent + + local MaxPlayersText = UpdatedText:Clone() + MaxPlayersText.Name = "MaxPlayersText" + MaxPlayersText.Position = UDim2.new(0, 0, 0, CreationDateText.Position.Y.Offset + 70) + MaxPlayersText.Text = Strings:LocalizedString("MaxPlayersWord") + MaxPlayersText.Parent = MoreDetailsContent + + local MaxPlayersCountText = LastUpdatedText:Clone() + MaxPlayersCountText.Name = "MaxPlayersCountText" + MaxPlayersCountText.Position = UDim2.new(0, 0, 0, MaxPlayersText.Position.Y.Offset + 34) + MaxPlayersCountText.Parent = MoreDetailsContent + + local CreatorText = UpdatedText:Clone() + CreatorText.Name = "CreatorText" + CreatorText.Position = UDim2.new(0, 0, 0, MaxPlayersCountText.Position.Y.Offset + 70) + CreatorText.Text = Strings:LocalizedString("CreatedByWord") + CreatorText.Parent = MoreDetailsContent + + local CreatorIcon = Utility.Create'ImageLabel' + { + Name = "CreatorIcon"; + Size = UDim2.new(0, 32, 0, 32); + Position = UDim2.new(0, 0, 0, CreatorText.Position.Y.Offset + 34 - 14); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/RobloxIcon32.png'; + Parent = MoreDetailsContent; + } + local CreatorNameText = LastUpdatedText:Clone() + CreatorNameText.Name = "CreatorNameText" + CreatorNameText.Position = UDim2.new(0, CreatorIcon.Size.X.Offset + 8, 0, CreatorText.Position.Y.Offset + 34) + CreatorNameText.Parent = MoreDetailsContent + + local DetailsBottomLineBreak = Utility.Create'Frame' + { + Name = "DetailsBottomLineBreak"; + Size = UDim2.new(0, 308, 0, 2); + Position = UDim2.new(0, 107, 0, MoreDetailsTitle.Size.Y.Offset + 566 - 2); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.LineBreakColor; + Parent = MoreDetailsContainer; + } + local ReportFrame = Utility.Create'TextButton' + { + Name = "ReportFrame"; + Size = UDim2.new(0, 91, 0, 91); + Position = UDim2.new(0, 0, 0, DetailsBottomLineBreak.Position.Y.Offset - 95); + BackgroundColor3 = GlobalSettings.GreyButtonColor; + BackgroundTransparency = 0; + BorderSizePixel = 0; + Text = ""; + Parent = MoreDetailsContainer; + ZIndex = BASE_ZINDEX; + + SoundManager:CreateSound('MoveSelection'); + AssetManager.CreateShadow(1); + } + local ReportIcon = Utility.Create'ImageLabel' + { + Name = "ReportIcon"; + BackgroundTransparency = 1; + ZIndex = BASE_ZINDEX; + Parent = ReportFrame; + Image = "rbxasset://textures/ui/Shell/Icons/ReportIcon@1080.png"; + Size = UDim2.new(0,52,0,43); + } + ReportIcon.Position = UDim2.new(0.5, -ReportIcon.Size.X.Offset / 2, 0.5, -ReportIcon.Size.Y.Offset / 2) + local ReportText = Utility.Create'TextLabel' + { + Name = "ReportText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(1, 16, 0.5, 0); + BackgroundTransparency = 1; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.TitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + Text = Strings:LocalizedString("ReportGameWord"); + Parent = ReportFrame; + } + ReportFrame.MouseButton1Click:connect(function() + ScreenManager:OpenScreen(ReportOverlayModule:CreateReportOverlay(ReportOverlayModule.ReportType.REPORT_GAME, placeId), false) + end) + + --[[ Custom Selection Logic ]]-- + PlayButton.NextSelectionRight = FavoriteButton + SmThumbLeft.NextSelectionDown = MoreThumbsButton + SmThumbLeft.NextSelectionRight = SmThumbRight + + local function JoinGame() + if canJoinGame and returnedFromGame then + canJoinGame = false + local data = getDataAsync() + local creatorUserId = nil + if data then + creatorUserId = data.CreatorUserId + end + GameJoinModule:StartGame(GameJoinModule.JoinType.Normal, placeId, creatorUserId) + canJoinGame = true + end + end + + local function toggleFavoriteButton(value) + if value == true then + FavoriteStarImage.Visible = true + FavoriteText.Position = UDim2.new(0, FavoriteStarImage.Position.X.Offset + FavoriteStarImage.Size.X.Offset + 12, 0, 0) + FavoriteText.Text = Strings:LocalizedString("FavoritedWord") + FavoriteText.TextXAlignment = Enum.TextXAlignment.Left + elseif value == false then + FavoriteStarImage.Visible = false + FavoriteText.Position = UDim2.new(0, 0, 0, 0) + FavoriteText.Text = Strings:LocalizedString("FavoriteWord") + FavoriteText.TextXAlignment = Enum.TextXAlignment.Center + end + end + + --[[ SelectionGained/Lost Connections ]]-- + PlayButton.SelectionGained:connect(function() + PlayButton.ImageColor3 = GlobalSettings.GreenSelectedButtonColor + PlayText.TextColor3 = selectedButtonTextColor + end) + PlayButton.SelectionLost:connect(function() + PlayButton.ImageColor3 = GlobalSettings.GreenButtonColor + PlayText.TextColor3 = baseButtonTextColor + end) + FavoriteButton.SelectionGained:connect(function() + FavoriteButton.ImageColor3 = GlobalSettings.GreySelectedButtonColor + FavoriteText.TextColor3 = selectedButtonTextColor + end) + FavoriteButton.SelectionLost:connect(function() + FavoriteButton.ImageColor3 = GlobalSettings.GreyButtonColor + FavoriteText.TextColor3 = baseButtonTextColor + end) + MoreDetailsContainer.SelectionGained:connect(function() + -- redirect + Utility.SetSelectedCoreObject(ReportFrame) + end) + FavoriteRedirectFrame.SelectionGained:connect(function() + Utility.SetSelectedCoreObject(FavoriteButton) + end) + + --[[ Input Events ]]-- + PlayButton.MouseButton1Click:connect(function() + SoundManager:Play('ButtonPress') + if playButtonDebounce then return end + playButtonDebounce = true + JoinGame() + playButtonDebounce = false + end) + + local favoriteDebounce = false + FavoriteButton.MouseButton1Click:connect(function() + SoundManager:Play('ButtonPress') + if favoriteDebounce then return end + favoriteDebounce = true + local data = getDataAsync() + if data then + local success, reason = data:PostFavoriteAsync() + if success == true then + toggleFavoriteButton(data.IsFavorited) + elseif reason then + local err = Errors.Favorite[reason] + ScreenManager:OpenScreen(ErrorOverlayModule(err), false) + end + end + favoriteDebounce = false + end) + + --[[ Content Set Functions ]]-- + local function connectThumbImage(image, index, thumbIds) + local loader = ThumbnailLoader:Create(image, thumbIds[index], ThumbnailLoader.Sizes.Large, + ThumbnailLoader.AssetType.Icon, false) + image.MouseButton1Click:connect(function() + SoundManager:Play('OverlayOpen') + ScreenManager:OpenScreen(ImageOverlayModule(thumbIds, index), false) + end) + spawn(function() + loader:LoadAsync(true, true, { ZIndex = image.ZIndex } ) + end) + end + local function setAdditionalThumbs(thumbIds) + if #thumbIds >= 3 then + connectThumbImage(MainThumbImage, 1, thumbIds) + connectThumbImage(SmThumbLeft, 2, thumbIds) + connectThumbImage(SmThumbRight, 3, thumbIds) + + if #thumbIds > 3 then + MoreThumbsButton.Visible = true + MoreThumbsButton.MouseButton1Click:connect(function() + SoundManager:Play('OverlayOpen') + ScreenManager:OpenScreen(ImageOverlayModule(thumbIds, 1), false) + end) + end + else + MyContentManager:RemoveItem(ThumbsContainer) + end + end + local function setRelatedGames(placeIds) + if #placeIds > 0 then + for i = 1, #RelatedGameImages do + local image = RelatedGameImages[i] + if placeIds[i] then + local thisData = GameData:GetGameData(placeIds[i]) + local thumbLoader = ThumbnailLoader:Create(image, thisData.IconId, + ThumbnailLoader.Sizes.Medium, ThumbnailLoader.AssetType.SquareIcon) + spawn(function() + thumbLoader:LoadAsync(true, true, { ZIndex = image.ZIndex } ) + end) + -- connect open detail event + image.MouseButton1Click:connect(function() + EventHub:dispatchEvent(EventHub.Notifications["OpenGameDetail"], thisData.PlaceId, thisData.Name, thisData.IconId) + end) + PopupText(image, thisData["Name"]) + GameData:AddRelatedGuiObject(placeId, image) + end + end + else + MyContentManager:RemoveItem(RelatedGamesContainer) + end + end + local function setNoteDisplay(OverrideAvatar, IsExperimental) + local noteText = "" + if OverrideAvatar then + noteText = Strings:LocalizedString("CustomAvatarPhrase") + end + + if IsExperimental then + local experimentalGamePhrase = Strings:LocalizedString("ExperimentalGamePhrase") + noteText = noteText ~= "" and (noteText.."\n") or noteText + noteText = noteText..experimentalGamePhrase + end + + if noteText ~= "" then + local noteFrame = Utility.Create'Frame' + { + Name = "NoteFrame"; + Size = UDim2.new(1, 0, 0.1, 0); + BackgroundTransparency = 1; + Parent = DescriptionContainer; + } + local noteText = Utility.Create'TextLabel' + { + Name = "NoteText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, 190, 0.5, 0); + BackgroundTransparency = 1; + Font = GlobalSettings.BoldFont; + FontSize = GlobalSettings.SubHeaderSize; + TextColor3 = GlobalSettings.GreyTextColor; + Text = noteText; + Parent = noteFrame; + } + DescriptionScrollingTextBox:SetPosition(UDim2.new(0, 0, 0.15, 0)) + DescriptionScrollingTextBox:SetSize(UDim2.new(1, 0, 0.85, 0)) + else + DescriptionScrollingTextBox:SetPosition(UDim2.new(0, 0, 0, 0)) + DescriptionScrollingTextBox:SetSize(UDim2.new(1, 0, 1, 0)) + end + end + + --[[ Initialize Content - Don't Block ]]-- + local scrollingTextBoxSelectableChangedCn = nil + + local function waitForTweensToFinish() + while this and this.TransitionTweens and this.TransitionTweens[1] and not this.TransitionTweens[1]:IsFinished() do + wait() + end + end + local function concatTables(t1, t2) + for _, value in pairs(t2) do + table.insert(t1, value) + end + end + + --This is a utility class to update the selection between each game detail page "Chunk" + local function CreateChunk(container, titleFrame) + local this = {} + this.Container = container + this.SelectionHolder = Utility.Create'Frame' + { + Name = "SelectionHolder"; + Size = UDim2.new(1, 0, 1, -titleFrame.Size.Y.Offset); + Position = UDim2.new(0, 0, 0, titleFrame.Size.Y.Offset); + BackgroundTransparency = 1; + Parent = this.Container; + Selectable = true; + } + + this.DefaultSelection = this.SelectionHolder + this.Container.SelectionGained:connect(function() + Utility.SetSelectedCoreObject(this.DefaultSelection) + end) + + --Should be called when the container's internal data get loaded + function this:PostLoaded() + --If we are selecting the selectionHolder, we switch to the new defaultSelection + self.SelectionHolder.Selectable = false + if GuiService.SelectedCoreObject == self.SelectionHolder and self.DefaultSelection then + Utility.SetSelectedCoreObject(self.DefaultSelection) + end + self.SelectionHolder:Destroy() + end + + return this + end + + --[[Create the detail Chunk]] + local GameDetailChunks = {} + table.insert(GameDetailChunks, CreateChunk(RatingDescriptionContainer, RatingDescriptionTitle)) + table.insert(GameDetailChunks, CreateChunk(ThumbsContainer, ThumbsTitle)) + table.insert(GameDetailChunks, CreateChunk(BadgeContainer, BadgeTitle)) + if not HideRelatedGames then + table.insert(GameDetailChunks, CreateChunk(RelatedGamesContainer, RelatedGamesTitle)) + end + + + local function GetPrevDefaultSelectionForChunk(index) + local defaultSelection = FavoriteButton + index = index - 1 + while index > 0 and GameDetailChunks[index] do + if GameDetailChunks[index].DefaultSelection then + defaultSelection = GameDetailChunks[index].DefaultSelection + break + else + index = index - 1 + end + end + + return defaultSelection + end + + local function GetNextDefaultSelectionForChunk(index) + local defaultSelection = ReportFrame + index = index + 1 + while index > 0 and GameDetailChunks[index] do + if GameDetailChunks[index].DefaultSelection then + defaultSelection = GameDetailChunks[index].DefaultSelection + break + else + index = index + 1 + end + end + + return defaultSelection + end + + do + RatingDescriptionLineBreak.Visible = false + DescriptionContainer.Visible = false + + local function loadFavoritedDataAsync() + local data = getDataAsync() + if not data then + Utility.DebugLog("GameDetail:loadFavoritedDataAsync() could not fetch data.") + return + end + + -- set favorite + toggleFavoriteButton(data.IsFavorited) + end + + local function loadDescriptionDataAsync() + local data = getDataAsync() + if not data then + Utility.DebugLog("GameDetail:loadDescriptionDataAsync() could not fetch data.") + return + end + + local descriptionData = data.Description + local overridesDefaultAvatar = data.OverridesDefaultAvatar + --Only show Experimental text when the flag is on + setNoteDisplay(overridesDefaultAvatar, data.IsExperimental) + DescriptionScrollingTextBox:SetText(descriptionData) + end + + local function loadVoteDataAsync() + local data = getDataAsync() + if not data then + Utility.DebugLog("GameDetail:loadVoteDataAsync() could not fetch data.") + return + end + VoteView:InitializeAsync(data) + end + + FavoriteButton.NextSelectionRight = GameDetailChunks[1].SelectionHolder + FavoriteButton.NextSelectionUp = GameDetailChunks[1].SelectionHolder + GameDetailChunks[1].SelectionHolder.NextSelectionLeft = FavoriteButton + GameDetailChunks[1].SelectionHolder.NextSelectionDown = FavoriteButton + local loader = LoadingWidget({Parent = GameDetailChunks[1].SelectionHolder}, {loadFavoritedDataAsync, loadDescriptionDataAsync, loadVoteDataAsync, waitForTweensToFinish}) + spawn(function() + loader:AwaitFinished() + loader:Cleanup() + loader = nil + RatingDescriptionLineBreak.Visible = true + DescriptionContainer.Visible = true + VoteView:SetParent(RatingDescriptionContainer) + VoteView:SetVisible(true) + + --Hook up Selections after loading + updateRatingDescriptionSelection() + + GameDetailChunks[1].DefaultSelection = VoteView:GetDefaultSelection() or GetNextDefaultSelectionForChunk(1) + GameDetailChunks[1]:PostLoaded() + + if this then + this.TransitionTweens = this.TransitionTweens or {} + concatTables(this.TransitionTweens, ScreenManager:FadeInSitu(RatingDescriptionLineBreak)) + concatTables(this.TransitionTweens, ScreenManager:FadeInSitu(DescriptionContainer)) + --Recalc position + MyContentManager:TweenContent(GuiService.SelectedCoreObject) + end + end) + end + + do + local gameThumbIds = {} + local function loadThumbsAsync() + local data = getDataAsync() + if not data then + Utility.DebugLog("GameDetail:loadThumbsAsync() could not fetch data.") + return + end + + gameThumbIds = data:GetThumbnailIdsAsync() + end + ThumbsContent.Visible = false + local loader = LoadingWidget({Parent = GameDetailChunks[2].SelectionHolder}, {loadThumbsAsync, waitForTweensToFinish}) + + spawn(function() + loader:AwaitFinished() + loader:Cleanup() + loader = nil + ThumbsContent.Visible = true + + -- set additional thumbnails + local hasThumbs = #gameThumbIds >= 3 + setAdditionalThumbs(gameThumbIds) + + GameDetailChunks[2].DefaultSelection = hasThumbs and MainThumbImage or GetNextDefaultSelectionForChunk(2) + GameDetailChunks[2]:PostLoaded() + + if this then + this.TransitionTweens = this.TransitionTweens or {} + concatTables(this.TransitionTweens, ScreenManager:FadeInSitu(ThumbsContent)) + --Recalc position + MyContentManager:TweenContent(GuiService.SelectedCoreObject) + end + end) + end + + do + local badgeData = {} + local placeName = "" + local function loadBadgeDataAsync() + local data = getDataAsync() + if not data then + Utility.DebugLog("GameDetail:loadBadgeDataAsync() could not fetch data.") + return + end + -- set badge data + badgeData = data:GetBadgeDataAsync() + placeName = data.Name or "" + end + + local loader = LoadingWidget({Parent = GameDetailChunks[3].SelectionHolder}, {loadBadgeDataAsync, waitForTweensToFinish}) + spawn(function() + loader:AwaitFinished() + loader:Cleanup() + loader = nil + -- set badges + local hasBadges = #badgeData >= 4 + local BadgeSort + if hasBadges then + BadgeSort = BadgeSortModule(placeName, UDim2.new(1, 0, 0, 566), UDim2.new(0, 0, 0, BadgeTitle.Size.Y.Offset), BadgeContainer) + BadgeSort:Initialize(badgeData) + GameDetailChunks[3].DefaultSelection = BadgeSort:GetDefaultSelection() + else + MyContentManager:RemoveItem(BadgeContainer) + GameDetailChunks[3].DefaultSelection = GetNextDefaultSelectionForChunk(3) + end + GameDetailChunks[3]:PostLoaded() + + if this then + this.TransitionTweens = this.TransitionTweens or {} + if BadgeSort then + concatTables(this.TransitionTweens, ScreenManager:FadeInSitu(BadgeSort:GetContainer())) + end + --Recalc position + MyContentManager:TweenContent(GuiService.SelectedCoreObject) + end + end) + end + + + if not HideRelatedGames then + do + local relatedGames = {} + local function loadRelatedGamesAsync() + local data = getDataAsync() + if not data then + Utility.DebugLog("GameDetail:loadRelatedGamesAsync() could not fetch data.") + return + end + -- set related games sort data + relatedGames = data:GetRecommendedGamesAsync() + end + + RelatedGamesImageFrame.Visible = false + local loader = LoadingWidget({Parent = GameDetailChunks[4].SelectionHolder}, {loadRelatedGamesAsync, waitForTweensToFinish}) + spawn(function() + loader:AwaitFinished() + loader:Cleanup() + loader = nil + + RelatedGamesImageFrame.Visible = true + setRelatedGames(relatedGames) + + GameDetailChunks[4].DefaultSelection = (#relatedGames > 0) and RelatedGameImages[1] or GetNextDefaultSelectionForChunk(4) + GameDetailChunks[4]:PostLoaded() + + if this then + this.TransitionTweens = this.TransitionTweens or {} + concatTables(this.TransitionTweens, ScreenManager:FadeInSitu(RelatedGamesImageFrame)) + --Recalc position + MyContentManager:TweenContent(GuiService.SelectedCoreObject) + end + end) + end + end + + do + local function loadMoreGameDetailsAsync() + local data = getDataAsync() + if not data then + Utility.DebugLog("GameDetail:loadMoreGameDetailsAsync() could not fetch data.") + return + end + + -- set more details + LastUpdatedText.Text = data.LastUpdated or LastUpdatedText.Text + CreationDateText.Text = data.CreationDate or CreationDateText.Text + CreatorNameText.Text = data.CreatorName or CreatorNameText.Text + MaxPlayersCountText.Text = data.MaxPlayers or MaxPlayersCountText.Text + end + + MoreDetailsContent.Visible = false + local loader = LoadingWidget({Parent = MoreDetailsContent}, {loadMoreGameDetailsAsync, waitForTweensToFinish}) + spawn(function() + loader:AwaitFinished() + loader:Cleanup() + loader = nil + MoreDetailsContent.Visible = true + + if this then + this.TransitionTweens = this.TransitionTweens or {} + concatTables(this.TransitionTweens, ScreenManager:FadeInSitu(MoreDetailsContent)) + --Recalc position + MyContentManager:TweenContent(GuiService.SelectedCoreObject) + end + end) + end + + --[[ Public API ]]-- + + --Override + function this:GetAnalyticsInfo() + return + { + [Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('GameDetailId'); + PlaceId = placeId; + } + end + + function this:GetDefaultSelectionObject() + return PlayButton + end + + -- Override + local baseShow = this.Show + function this:Show() + baseShow(self) + end + + -- Override + local baseFocus = this.Focus + function this:Focus() + baseFocus(self) + --This ensures that we can still navigate even if the callback of TweenPosition fails to be called + MyContentManager:ResetTween() + MyContentManager:TweenContent(GuiService.SelectedCoreObject) + + selectionChangedCn = GuiService:GetPropertyChangedSignal('SelectedCoreObject'):connect(function() + local newSelectedObject = GuiService.SelectedCoreObject + if newSelectedObject then + MyContentManager:TweenContent(newSelectedObject) + end + end) + if PlatformService then + dataModelViewChangedCn = PlatformService.ViewChanged:connect(function(viewType) + if viewType == 0 then + returnedFromGame = false + wait(1) + returnedFromGame = true + end + end) + end + + EventHub:addEventListener(EventHub.Notifications["GameJoin"], "gameJoin", + function(success) + if not VoteView:GetCanVote() then + if success then + VoteView:SetCanVote(true) + updateRatingDescriptionSelection() + end + end + end) + + scrollingTextBoxSelectableChangedCn = DescriptionScrollingTextBox.OnSelectableChanged:connect(updateRatingDescriptionSelection) + end + + -- Override + local baseRemoveFocus = this.RemoveFocus + function this:RemoveFocus() + baseRemoveFocus(self) + selectionChangedCn = Utility.DisconnectEvent(selectionChangedCn) + dataModelViewChangedCn = Utility.DisconnectEvent(dataModelViewChangedCn) + EventHub:removeEventListener(EventHub.Notifications["GameJoin"], "gameJoin") + scrollingTextBoxSelectableChangedCn = Utility.DisconnectEvent(scrollingTextBoxSelectableChangedCn) + end + + return this +end + +return CreateGameDetail diff --git a/Client2018/content/internal/AppShell/Modules/Shell/GameGenreScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/GameGenreScreen.lua new file mode 100644 index 0000000..6173983 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/GameGenreScreen.lua @@ -0,0 +1,24 @@ +--[[ + // GameGenreScreen.lua + + // Creates a GameGenreScreen that is used to navigate games for a + // selected sort. +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local BaseCarouselScreen = require(ShellModules:FindFirstChild('BaseCarouselScreen')) + +local function CreateGameGenreScreen(sortName, gameCollection) + local this = BaseCarouselScreen() + + this:SetTitleZIndex(2) + this:SetTitle(sortName) + this:LoadGameCollection(gameCollection) + + return this +end + +return CreateGameGenreScreen diff --git a/Client2018/content/internal/AppShell/Modules/Shell/GameJoin.lua b/Client2018/content/internal/AppShell/Modules/Shell/GameJoin.lua new file mode 100644 index 0000000..4ba007b --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/GameJoin.lua @@ -0,0 +1,74 @@ +--[[ + // GameJoin.lua + + // Handles game join logic +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) +local GameOptionsSettings = settings():FindFirstChild("Game Options") + +local Errors = require(ShellModules:FindFirstChild('Errors')) +local ErrorOverlayModule = require(ShellModules:FindFirstChild('ErrorOverlay')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local XboxAppState = require(ShellModules:FindFirstChild('AppState')) + +local GameJoin = {} + +GameJoin.JoinType = { + Normal = 0; -- use placeId + GameInstance = 1; -- use game instance id + Follow = 2; -- use userId or user you are following + PMPCreator = 3; -- use placeId, used when a player joins their own place +} + +-- joinType - GameJoin.JoinType +-- joinId - can be a userId or placeId, see JoinType for which one to use +function GameJoin:StartGame(joinType, joinId, creatorUserId) + -- check if we need to open the overscan screen + local needToOverscan = false + pcall(function() + if GameOptionsSettings.OverscanPX < 0 or GameOptionsSettings.OverscanPY < 0 then + needToOverscan = true + end + end) + if game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows then + needToOverscan = false + end + + local function onJoinGame() + if UserSettings().GameSettings:InStudioMode() then + ScreenManager:OpenScreen(ErrorOverlayModule(Errors.Test.CannotJoinGame), false) + else + local success = pcall(function() + -- check if we are the creator for normal joins + if joinType == GameJoin.JoinType.Normal and creatorUserId == XboxAppState.store:getState().RobloxUser.rbxuid then + joinType = GameJoin.JoinType.PMPCreator + end + + return PlatformService:BeginStartGame3(joinType, joinId) + end) + -- catch pcall error, something went wrong with call into API + -- all other game join errors are caught in AppHome.lua + if not success then + ScreenManager:OpenScreen(ErrorOverlayModule(Errors.GameJoin.Default), false) + end + end + end + + if needToOverscan or UserSettings().GameSettings:InStudioMode() then + local RoactScreenManagerWrapper = require(ShellModules.Components.RoactScreenManagerWrapper) + local OverscanRoact = require(ShellModules.Components.Overscan.Overscan) + + local overscanRoact = RoactScreenManagerWrapper.new(OverscanRoact, GuiRoot, {}, onJoinGame) + ScreenManager:OpenScreen(overscanRoact) + else + onJoinGame() + end +end + +return GameJoin diff --git a/Client2018/content/internal/AppShell/Modules/Shell/GameSearchScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/GameSearchScreen.lua new file mode 100644 index 0000000..ecf84d9 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/GameSearchScreen.lua @@ -0,0 +1,89 @@ +--[[ + // GameSearchScreen.lua + + // Creates a screen for the results of a game search +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) + +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local SortsData = require(ShellModules:FindFirstChild('SortsData')) + +local BaseCarouselScreen = require(ShellModules:FindFirstChild('BaseCarouselScreen')) +local HintActionView = require(ShellModules:FindFirstChild('HintActionView')) + +local function CreateGameSearchScreen(searchKeyword) + local this = BaseCarouselScreen() + + local currentSearchWord = searchKeyword + local keyboardClosedCn = nil + local searchGameCollection = SortsData:GetGameSearchSort(currentSearchWord) + + local function setTitle(searchWord) + this:SetTitle(string.format(Strings:LocalizedString("SearchingForPhrase"), searchWord)) + end + setTitle(currentSearchWord) + this:LoadGameCollection(searchGameCollection) + + -- Hint Action View + local hintActionView = HintActionView(this.Container, "OpenSearchKeyboard") + hintActionView:SetImage('rbxasset://textures/ui/Shell/ButtonIcons/XButton.png') + hintActionView:SetText(Strings:LocalizedString("SearchWord")) + + local function onKeyboardClosed(searchWord) + searchWord = Utility.SpaceNormalizeString(searchWord) + if #searchWord > 0 and searchWord ~= currentSearchWord then + currentSearchWord = searchWord + setTitle(currentSearchWord) + searchGameCollection = SortsData:GetGameSearchSort(currentSearchWord) + this:LoadGameCollection(searchGameCollection) + end + end + + local seenYButtonPressed = false + local function onSearchGames(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + seenYButtonPressed = true + elseif inputState == Enum.UserInputState.End and seenYButtonPressed then + if PlatformService then + PlatformService:ShowKeyboard(Strings:LocalizedString("SearchGamesPhrase"), "", currentSearchWord, Enum.XboxKeyBoardType.Default) + end + seenYButtonPressed = false + end + end + + function this:GetAnalyticsInfo() + return + { + [Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('GameSearchScreenId'); + Title = currentSearchWord; + } + end + + local baseFocus = this.Focus + function this:Focus() + baseFocus(self) + hintActionView:BindAction(onSearchGames, Enum.KeyCode.ButtonX) + keyboardClosedConn = Utility.DisconnectEvent(keyboardClosedConn) + if PlatformService then + keyboardClosedConn = PlatformService.KeyboardClosed:connect(onKeyboardClosed) + end + end + + local baseRemoveFocus = this.RemoveFocus + function this:RemoveFocus() + baseRemoveFocus(self) + hintActionView:UnbindAction() + keyboardClosedConn = Utility.DisconnectEvent(keyboardClosedConn) + end + + return this +end + +return CreateGameSearchScreen diff --git a/Client2018/content/internal/AppShell/Modules/Shell/GameSort.lua b/Client2018/content/internal/AppShell/Modules/Shell/GameSort.lua new file mode 100644 index 0000000..ae22f20 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/GameSort.lua @@ -0,0 +1,376 @@ +--[[ + // GameSort.lua + // Creates a grid layout for a game sort +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local WidgetModules = ShellModules:FindFirstChild("Widgets") +local GuiService = game:GetService('GuiService') + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local PopupText = require(ShellModules:FindFirstChild('PopupText')) +local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Alerts = require(ShellModules:FindFirstChild('Alerts')) +local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay')) +local LockedGameSortView = require(ShellModules:FindFirstChild('LockedGameSortView')) +local MoreButtonModule = require(WidgetModules:FindFirstChild("MoreButton")) +local GameData = require(ShellModules:FindFirstChild('GameData')) +local SortsData = require(ShellModules:FindFirstChild('SortsData')) + +local GameSort = {} + +local function createSortTitle(name) + local sortTitleLabel = Utility.Create'TextLabel' + { + Name = "SortTitle"; + Size = UDim2.new(1, 0, 0, 36); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.SubHeaderSize; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = name; + } + + return sortTitleLabel +end + +local function createMoreButton(margin, parent) + local moreButton, overrideSelection = MoreButtonModule() + + -- we override the selection on moreButton to fit around the moreImage + overrideSelection = Utility.Create'ImageLabel' + { + Name = "OverrideSelection"; + Image = 'rbxasset://textures/ui/SelectionBox.png'; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(19,19,43,43); + BackgroundTransparency = 1; + } + + moreButton = Utility.Create'ImageButton' + { + Name = "MoreButton"; + Size = UDim2.new(1, 0, 0, 50); + Position = UDim2.new(0, 0, 1, margin); + BackgroundTransparency = 1; + ZIndex = 2; + Visible = false; + SelectionImageObject = overrideSelection; + Parent = parent; + SoundManager:CreateSound('MoveSelection'); + } + local moreImage = Utility.Create'ImageLabel' + { + Name = "MoreImage"; + BackgroundTransparency = 1; + BorderSizePixel = 0; + ZIndex = 2; + Parent = moreButton; + } + + local function updateMoreImage(isSelected) + moreImage.Image = isSelected and 'rbxasset://textures/ui/Shell/Buttons/MoreButtonSelected@1080.png' + or 'rbxasset://textures/ui/Shell/Buttons/MoreButton@1080.png' + moreImage.Size = UDim2.new(0,108,0,50); + + moreImage.Position = UDim2.new(1, -moreImage.Size.X.Offset, 0, 0) + + overrideSelection.Size = UDim2.new(0, moreImage.Size.X.Offset + 14, 0, moreImage.Size.Y.Offset + 14) + overrideSelection.Position = UDim2.new(1, -overrideSelection.Size.X.Offset + 7, 0, -7) + end + + moreButton.SelectionGained:connect(function() + updateMoreImage(true) + end) + moreButton.SelectionLost:connect(function() + updateMoreImage(false) + end) + + updateMoreImage(GuiService.SelectedCoreObject == moreButton) + + return moreButton +end + +local function createImageButton(size, position) + return Utility.Create'ImageButton' + { + Name = "GameThumbButton"; + Size = size; + Position = position; + BackgroundTransparency = 0; + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.ModalBackgroundColor; + SoundManager:CreateSound('MoveSelection'); + } +end + +local function createImageGrid(rows, columns, size, spacing, images) + for i = 1, rows do + for j = 1, columns do + local image = createImageButton(size, + UDim2.new(0, (j - 1) * size.X.Offset + (j - 1) * spacing.x, 0, (i - 1) * size.Y.Offset + (i - 1) * spacing.y)) + table.insert(images, image) + image.Name = tostring(#images) + end + end +end + +local function createPopupText(images) + local popupText = {} + for i = 1, #images do + local popup = PopupText(images[i], "") + table.insert(popupText, popup) + end + + return popupText +end + +local function setImages(imageIds, images) + local size = ThumbnailLoader.Sizes.Medium + local assetType = ThumbnailLoader.AssetType.SquareIcon + for i = 1, #images do + if imageIds[i] then + local thumbLoader = ThumbnailLoader:Create(images[i], imageIds[i], size, assetType) + spawn(function() + if not thumbLoader:LoadAsync(true, false) then + -- TODO + end + end) + else + images[i].BackgroundColor3 = GlobalSettings.GreyButtonColor + images[i].Image = "" + local image = Utility.Create'ImageLabel' + { + Name = "NoGameImage"; + Size = UDim2.new(0, 102, 0, 102); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/GamePlusIcon.png'; + ZIndex = images[i].ZIndex; + Parent = images[i]; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + } + end + end +end + +local function createBaseGrid(size, images, imageContainerSizeY) + local this = {} + + local moreButtonSpacing = 12 + local isViewLocked = false + local shouldShowMoreButton = false + + this.Container = Utility.Create'Frame' + { + Name = "SortContainer"; + Size = size; + BackgroundTransparency = 1; + } + local titleLabel = createSortTitle("") + titleLabel.Parent = this.Container + local moreButton = createMoreButton(moreButtonSpacing, this.Container) + local popupText = createPopupText(images) + + -- set up images + local imageContainer = Utility.Create'Frame' + { + Name = "ImageContainer"; + Size = UDim2.new(1, 0, 0, imageContainerSizeY); + Position = UDim2.new(0, 0, 0, 36); + BackgroundTransparency = 1; + Parent = this.Container; + } + for i = 1, #images do + images[i].Parent = imageContainer + end + + --[[ Begin UGC Locked UI ]]-- + local lockedView = LockedGameSortView() + + local function lockIcons() + for i = 1, #images do + lockedView:CreateLockedIconOverlay(images[i]) + images[i].Selectable = false + end + end + local function onLockedStateChanged() + if isViewLocked == true then + lockedView:SetParent(imageContainer) + lockIcons() + moreButton.Visible = false + else + lockedView:SetParent(nil) + lockedView:RemoveLockedIconOverlays() + moreButton.Visible = shouldShowMoreButton + end + end + + function this:SetLockState(newState) + isViewLocked = newState + onLockedStateChanged() + end + + --[[ End UGC Locked UI ]]-- + + function this:SetParent(newParent) + self.Container.Parent = newParent + end + function this:GetContainer() + return self.Container + end + function this:SetPosition(newPosition) + self.Container.Position = newPosition + end + function this:SetTitle(newTitle) + titleLabel.Text = newTitle + end + function this:SetVisible(value) + self.Container.Visible = value + end + function this:SetImages(imageIds) + setImages(imageIds, images) + end + + function this:SetPlaceIds(placeIds, sortName, gameSort) + if placeIds then + for i = 1, #images do + local placeId = placeIds[i] + local size = ThumbnailLoader.Sizes.Medium + local assetType = ThumbnailLoader.AssetType.SquareIcon + if placeId then + local gameData = GameData:GetGameData(placeId) + if gameData then + if images[i] then + local thumbLoader = ThumbnailLoader:Create(images[i], gameData.IconId, size, assetType) + spawn(function() + if not thumbLoader:LoadAsync(true, false) then + -- TODO + end + end) + + images[i].MouseButton1Click:connect(function() + EventHub:dispatchEvent(EventHub.Notifications["OpenGameDetail"], placeId) + end) + GameData:AddRelatedGuiObject(placeId, images[i]) + end + + if popupText[i] then + popupText[i]:SetText(gameData.Name) + end + end + else --If don't have enough games in the sort + images[i].BackgroundColor3 = GlobalSettings.GreyButtonColor + images[i].Image = "" + local image = Utility.Create'ImageLabel' + { + Name = "NoGameImage"; + Size = UDim2.new(0, 102, 0, 102); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/GamePlusIcon.png'; + ZIndex = images[i].ZIndex; + Parent = images[i]; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + } + + images[i].MouseButton1Click:connect(function() + -- If there are not enough games in my games section then let them know how to make more! + if gameSort == SortsData:GetUserPlaces() then + ScreenManager:OpenScreen(ErrorOverlay(Alerts.PlayMyPlaceMoreGames), false) + else + -- if there is no game to load in this slot, we're going to redirect to the featured sort if the user presses this button + EventHub:dispatchEvent(EventHub.Notifications["OpenGameGenre"], Strings:LocalizedString('FeaturedTitle'), + SortsData:GetSort(SortsData.DefaultSortId.Featured)) + end + end) + + if popupText[i] then + popupText[i]:SetText(Strings:LocalizedString("MoreGamesPhrase")) + end + end + end + + --If we have more games than the showing ones + shouldShowMoreButton = #placeIds > #images + moreButton.Visible = shouldShowMoreButton + moreButton.MouseButton1Click:connect(function() + EventHub:dispatchEvent(EventHub.Notifications["OpenGameGenre"], sortName, gameSort) + end) + EventHub:addEventListener(EventHub.Notifications["UnlockedUGC"], "unlockView", + function() + this:SetLockState(false) + end) + end + end + + function this:GetDefaultSelection() + local default = nil + if isViewLocked then + default = lockedView:GetSelection() + else + if #images > 0 then + default = images[1] + end + end + + return default + end + function this:Contains(guiObject) + for i = 1, #images do + if images[i] == guiObject then + return true + end + end + return false + end + function this:Destroy() + EventHub:removeEventListener(EventHub.Notifications["UnlockedUGC"], "unlockView") + self.Container:Destroy() + end + + return this +end + +function GameSort:CreateMainGridView(size, spacing, lgImageSize, smImageSize) + local images = {} + local imageContainerSizeY = lgImageSize.Y.Offset + spacing.y + smImageSize.Y.Offset + + local mainImage = createImageButton(lgImageSize, UDim2.new(0, 0, 0, 0)) + table.insert(images, mainImage) + mainImage.Name = tostring(#images) + + for i = 1, 2 do + local smImage = createImageButton(smImageSize, + UDim2.new(0, (i - 1) * smImageSize.X.Offset + (i - 1) * spacing.x, 1, -smImageSize.Y.Offset)) + table.insert(images, smImage) + smImage.Name = tostring(#images) + end + + local this = createBaseGrid(size, images, imageContainerSizeY) + + return this +end + +-- 2x2 grid for the games page +function GameSort:CreateGridView(size, imageSize, spacing, rows, columns) + local images = {} + local imageContainerSizeY = (imageSize.Y.Offset * columns) + ((columns - 1) * spacing.y) + createImageGrid(rows, columns, imageSize, spacing, images) + + local this = createBaseGrid(size, images, imageContainerSizeY) + + return this +end + +return GameSort diff --git a/Client2018/content/internal/AppShell/Modules/Shell/GameplaySettingsData.lua b/Client2018/content/internal/AppShell/Modules/Shell/GameplaySettingsData.lua new file mode 100644 index 0000000..3714e75 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/GameplaySettingsData.lua @@ -0,0 +1,152 @@ +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local Utility = require(ShellModules:FindFirstChild("Utility")) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local MakeSafeAsync = require(ShellModules:FindFirstChild('SafeAsync')) + +local Http = require(ShellModules:FindFirstChild("Http")) +local ThirdPartyUserService = nil +pcall(function()ThirdPartyUserService = game:GetService('ThirdPartyUserService') end) +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) + +local GameplaySettingsData = {} +local CrossplayEnabled = nil +local PrivilegeSettings = nil +local UserChangedCount = 0 +local Privileges = +{ + USER_CREATED_CONTENT = 247, + MULTIPLAYER_SESSIONS = 254 +} + +--CPP Settings +local function GetCrossplayEnabledStatusAsync() + local startCount = UserChangedCount + local waitDuration = 2 + while startCount == UserChangedCount do + local jsonobject = Http.GetCrossplayEnabledStatusAsync() + + if startCount == UserChangedCount then + if jsonobject ~= nil then + CrossplayEnabled = jsonobject.isEnabled + break + else + wait(waitDuration) + waitDuration = waitDuration * 2 + end + end + end +end + +function GameplaySettingsData.GetCrossplayEnabledStatusAsync(forceUpdate) + local startCount = UserChangedCount + if forceUpdate then + GetCrossplayEnabledStatusAsync() + else + while CrossplayEnabled == nil and startCount == UserChangedCount do + wait() + end + end + if startCount == UserChangedCount then + return CrossplayEnabled + end +end + +GameplaySettingsData.SetCrossplayEnabledStatusAsync = function(val) + Http.PostCrossplayStatusAsync(val) + CrossplayEnabled = val +end + +local SetCrossplayEnabledStatusAsync = nil +GameplaySettingsData.SetCrossplayEnabledStatusAsync = function(val) + if SetCrossplayEnabledStatusAsync then + SetCrossplayEnabledStatusAsync(val) + end +end + +GameplaySettingsData.UpdateSetCrossplayEnabledStatusAsyncFunc = function(callback) + SetCrossplayEnabledStatusAsync = MakeSafeAsync({ + asyncFunc = function(val) + if Http.PostCrossplayStatusAsync(val) then + CrossplayEnabled = val + return val + end + end, + callback = callback, + userRelated = true + }) +end + +--PrivilegeSettings +local OnGetPrivilegeSettingsBegin = Utility.Signal() +local OnGetPrivilegeSettingsEnd = Utility.Signal() +GameplaySettingsData.OnGetPrivilegeSettingsBegin = OnGetPrivilegeSettingsBegin +GameplaySettingsData.OnGetPrivilegeSettingsEnd = OnGetPrivilegeSettingsEnd +GameplaySettingsData.GetPrivilegeSettingsAsync = MakeSafeAsync({ + asyncFunc = function() + OnGetPrivilegeSettingsBegin:fire() + local newPrivilegeSettings = {} + + local success = pcall(function() + newPrivilegeSettings.Multiplayer = PlatformService:BeginCheckXboxPrivilege(Privileges.MULTIPLAYER_SESSIONS) + newPrivilegeSettings.SharedContent = PlatformService:BeginCheckXboxPrivilege(Privileges.USER_CREATED_CONTENT) + end) + if not success then + newPrivilegeSettings.Multiplayer = { CanJoinGame = false, LocalizedStringKey = "ErrorWord" , PrivilegeCheckResult = "Error", Success = false} + newPrivilegeSettings.SharedContent = { CanJoinGame = false, LocalizedStringKey = "ErrorWord" , PrivilegeCheckResult = "Error", Success = false} + end + return newPrivilegeSettings + end, + callback = function(newPrivilegeSettings) + PrivilegeSettings = newPrivilegeSettings + OnGetPrivilegeSettingsEnd:fire(PrivilegeSettings) + end, + userRelated = true +}) + +function GameplaySettingsData.GetPrivilegeSettings() + return PrivilegeSettings +end + +local function OnUserAccountChanged() + CrossplayEnabled = nil + PrivilegeSettings = nil + SetCrossplayEnabledStatusAsync = nil + + --Get CPP Settings + spawn(GetCrossplayEnabledStatusAsync) + + --Get Privilege Settings + spawn(function() + GameplaySettingsData.GetPrivilegeSettingsAsync() + end) +end + +EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "GameplaySettingsData", OnUserAccountChanged) + +local function OnUserSignOut() + UserChangedCount = UserChangedCount + 1 + CrossplayEnabled = nil + PrivilegeSettings = nil + OnGetPrivilegeSettingsEnd:fire() + if SetCrossplayEnabledStatusAsync then + SetCrossplayEnabledStatusAsync:Cancel() + SetCrossplayEnabledStatusAsync = nil + end +end + +if ThirdPartyUserService then + ThirdPartyUserService.ActiveUserSignedOut:connect(OnUserSignOut) +end + +if PlatformService then + PlatformService.OnLeaveConstrained:connect( + function() + GameplaySettingsData.GetPrivilegeSettingsAsync() + end) +end + +return GameplaySettingsData diff --git a/Client2018/content/internal/AppShell/Modules/Shell/GameplaySettingsView.lua b/Client2018/content/internal/AppShell/Modules/Shell/GameplaySettingsView.lua new file mode 100644 index 0000000..6afc981 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/GameplaySettingsView.lua @@ -0,0 +1,423 @@ +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local GameplaySettingsData = require(ShellModules:FindFirstChild('GameplaySettingsData')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local DisableCrossplayOverlay = require(ShellModules:FindFirstChild('DisableCrossplayOverlay')) +local EnableCrossplayOverlay = require(ShellModules:FindFirstChild('EnableCrossplayOverlay')) +local GuiService = game:GetService('GuiService') +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) + +local MULTIPLAYER_SETTING_URI = "ms-settings://CustomizePrivacyMultiplayer" +local GAME_CONTENT_SETTING_URI = "ms-settings://CustomizePrivacyGameContent" + +local function createGameplaySettingsView(errorCode) + local this = {} + + local dummySelection = Utility.Create'Frame' + { + BackgroundTransparency = 1; + } + + local Container = Utility.Create'Frame' + { + Name = "GameplaySettingsContainer"; + Position = UDim2.new(0, 0, 0, 0); + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Selectable = true; + SelectionImageObject = dummySelection; + } + + local function TryLaunchUri(uri) + if PlatformService then + PlatformService:LaunchPlatformUri(uri) + end + end + + local EnabledStatusButton = nil + local MultiplayerButton = nil + local SharedContentButton = nil + local function createCPPSettingsView() + Utility.Create'TextLabel' + { + Name = "CPPSettingsTitle"; + TextXAlignment = 'Left'; + TextYAlignment = 'Bottom'; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, 26, 0, 26); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.SubHeaderSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = Strings:LocalizedString('CrossPlatformGameplayPhrase'); + Parent = Container; + } + + EnabledStatusButton = Utility.Create'ImageButton' + { + Name = "EnabledStatusButton"; + Position = UDim2.new(0, 26, 0, 46); + Size = UDim2.new(0, 520, 0, 75); + BackgroundTransparency = 1; + ImageTransparency = 0.25; + ImageColor3 = GlobalSettings.GreyButtonColor; + Image = GlobalSettings.RoundCornerButtonImage; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(16, 16, 16, 16); + ZIndex = 1; + Parent = Container; + SoundManager:CreateSound('MoveSelection'); + } + + local EnabledStatusText = Utility.Create'TextLabel' + { + Name = "EnabledStatusText"; + TextXAlignment = 'Left'; + TextYAlignment = 'Center'; + Size = UDim2.new(0, 0, 1, 0); + Position = UDim2.new(0, 72, 0, 0); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = GlobalSettings.WhiteTextColor; + ZIndex = 2; + Text = Strings:LocalizedString("LoadingWord"); + Parent = EnabledStatusButton; + } + + local EnabledStatusIcon = Utility.Create'ImageLabel' + { + Name = "EnabledStatusIcon"; + Position = UDim2.new(0, 20, 0.5, 0); + AnchorPoint = Vector2.new(0, 0.5); + Size = UDim2.new(0, 32, 0, 32); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Shell/Icons/EnabledStatusIcon.png"; + ImageColor3 = Color3.new(1, 1, 1); + ZIndex = 2; + Parent = EnabledStatusButton; + } + + local DescriptionText = Utility.Create'TextLabel' + { + Name = "DescriptionText"; + Position = UDim2.new(0,26, 0, 135); + Size = UDim2.new(0, 520, 0, 110); + Text = ""; + TextXAlignment = 'Center'; + TextYAlignment = 'Top'; + BackgroundTransparency = 1; + Font = GlobalSettings.BoldFont; + TextColor3 = GlobalSettings.GreyTextColor; + FontSize = GlobalSettings.SmallTitleSize; + TextWrapped = true; + Parent = Container; + } + + --Make the EnabledStatusButton big enough + Utility.ResizeButtonWithDynamicText(EnabledStatusButton, EnabledStatusText, + {Strings:LocalizedString("EnabledWord"), Strings:LocalizedString("DisabledWord"), Strings:LocalizedString("LoadingWord")}, + GlobalSettings.TextHorizontalPadding + EnabledStatusText.Position.X.Offset/2) + + local isCPPEnabled = nil + local cppSettingDebounce = false + local function onCPPStatusChanged(val) + isCPPEnabled = val + if isCPPEnabled then + EnabledStatusIcon.ImageColor3 = Color3.new(0, 0.9, 0); + EnabledStatusText.Text = Strings:LocalizedString("EnabledWord"); + DescriptionText.Text = Strings:LocalizedString("CrossplayEnabledDescription"); + else + EnabledStatusIcon.ImageColor3 = Color3.new(0.9, 0, 0); + EnabledStatusText.Text = Strings:LocalizedString("DisabledWord"); + DescriptionText.Text = Strings:LocalizedString("CrossplayDisabledDescription"); + end + end + + local function SetCrossplayEnabledCallback(result) + if result == nil then + local Errors = require(ShellModules:FindFirstChild('Errors')) + local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay')) + ScreenManager:OpenScreen(ErrorOverlay(Errors.CPPSettingError.SetCPPSettingError), false) + else + onCPPStatusChanged(result) + if result then + ScreenManager:OpenScreen(EnableCrossplayOverlay( + { + Title = Strings:LocalizedString("EnableCrossplayOverlayTitle"), + Msg = Strings:LocalizedString("EnableCrossplayOverlayMessage"), + Callback = function() end + }), + false + ) + end + end + end + + local function SetCrossplayEnabled(val) + cppSettingDebounce = true + GameplaySettingsData.SetCrossplayEnabledStatusAsync(val) + cppSettingDebounce = false + end + + EnabledStatusButton.MouseButton1Click:connect(function() + SoundManager:Play('ButtonPress') + if isCPPEnabled == nil or cppSettingDebounce then return end + cppSettingDebounce = true + if isCPPEnabled then + ScreenManager:OpenScreen(DisableCrossplayOverlay( + { + Title = Strings:LocalizedString("DisableCrossplayOverlayTitle"), + Msg = Strings:LocalizedString("DisableCrossplayOverlayMessage"), + Callback = function() + SetCrossplayEnabled(false) + end + }), + false + ) + else + SetCrossplayEnabled(true) + end + cppSettingDebounce = false + end) + + GameplaySettingsData.UpdateSetCrossplayEnabledStatusAsyncFunc(SetCrossplayEnabledCallback) + spawn( + function() + onCPPStatusChanged(GameplaySettingsData.GetCrossplayEnabledStatusAsync()) + end + ) + + Container.SelectionGained:connect(function() + Utility.SetSelectedCoreObject(EnabledStatusButton) + end) + end + + local function createPrivilegeSettingsView() + local XASTitleTextLabel = Utility.Create'TextLabel' + { + Name = "XboxAccountSettingsTitle"; + TextXAlignment = 'Left'; + TextYAlignment = 'Bottom'; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, 26, 0, 252 + 26); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.SubHeaderSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = Strings:LocalizedString('XboxAccountSettingsPhrase'); + Parent = Container; + } + + MultiplayerButton = Utility.Create'ImageButton' + { + Name = "MultiplayerButton"; + Size = UDim2.new(0, 520, 0, 75); + BackgroundTransparency = 1; + ImageTransparency = 0.25; + ImageColor3 = GlobalSettings.GreyButtonColor; + Image = GlobalSettings.RoundCornerButtonImage; + ScaleType = Enum.ScaleType.Slice; + Selectable = true; + SliceCenter = Rect.new(16, 16, 16, 16); + ZIndex = 1; + Parent = Container; + SoundManager:CreateSound('MoveSelection'); + } + MultiplayerButton.Position = XASTitleTextLabel.Position + UDim2.new(0, 0, 0, XASTitleTextLabel.Size.Y.Offset + 20) + + SharedContentButton = MultiplayerButton:Clone() + SharedContentButton.Name = "SharedContentButton" + SharedContentButton.Parent = Container + SharedContentButton.Position = SharedContentButton.Position + UDim2.new(0, 0, 0, MultiplayerButton.Size.Y.Offset + 10) + + local MultiplayerButtonLinkable = false + local MultiplayerButtonSelectable = MultiplayerButton.Selectable + local SharedContentButtonLinkable = false + local SharedContentButtonSelectable = SharedContentButton.Selectable + + MultiplayerButton.MouseButton1Click:connect(function() + if MultiplayerButtonLinkable then + TryLaunchUri(MULTIPLAYER_SETTING_URI) + end + end) + SharedContentButton.MouseButton1Click:connect(function() + if SharedContentButtonLinkable then + TryLaunchUri(GAME_CONTENT_SETTING_URI) + end + end) + + local MultiplayerText = Utility.Create'TextLabel' + { + Name = "MultiplayerText"; + TextXAlignment = 'Left'; + TextYAlignment = 'Center'; + Size = UDim2.new(0, 0, 1, 0); + Position = UDim2.new(0, 72, 0, 0); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = GlobalSettings.WhiteTextColor; + ZIndex = 2; + Parent = MultiplayerButton; + Text = Strings:LocalizedString("MultiplayerWord")..": "..Strings:LocalizedString("LoadingWord"); + } + + local SharedContentText = MultiplayerText:Clone() + SharedContentText.Name = "SharedContentText" + SharedContentText.Parent = SharedContentButton + SharedContentText.Text = Strings:LocalizedString("SharedContentWord")..": "..Strings:LocalizedString("LoadingWord") + + local MultiplayerIcon = Utility.Create'ImageLabel' + { + Name = "MultiplayerIcon"; + Position = UDim2.new(0, 20, 0.5, 0); + AnchorPoint = Vector2.new(0, 0.5); + Size = UDim2.new(0, 32, 0, 32); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Shell/Icons/EnabledStatusIcon.png"; + ImageColor3 = Color3.new(1, 1, 1); + ZIndex = 2; + Parent = MultiplayerButton; + } + + local SharedContentIcon = MultiplayerIcon:Clone() + SharedContentIcon.Name = "SharedContentIcon" + SharedContentIcon.Parent = SharedContentButton + + local MultiplayerLinkIcon = Utility.Create'ImageLabel' + { + Name = "MultiplayerLinkIcon"; + Position = UDim2.new(1, -20, 0.5, 0); + AnchorPoint = Vector2.new(1, 0.5); + Size = UDim2.new(0, 32, 0, 32); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Shell/Icons/ExternalLink.png"; + ImageColor3 = Color3.new(1, 1, 1); + ZIndex = 2; + Parent = MultiplayerButton; + } + + local SharedContentLinkIcon = MultiplayerLinkIcon:Clone() + SharedContentLinkIcon.Name = "SharedContentLinkIcon" + SharedContentLinkIcon.Parent = SharedContentButton + + local DescriptionText = Utility.Create'TextLabel' + { + Name = "DescriptionText"; + Size = UDim2.new(0, 520, 0, 200); + Text = ""; + TextXAlignment = 'Center'; + TextYAlignment = 'Top'; + BackgroundTransparency = 1; + Font = GlobalSettings.BoldFont; + TextColor3 = GlobalSettings.GreyTextColor; + FontSize = GlobalSettings.SmallTitleSize; + TextWrapped = true; + Parent = Container; + Text = ""; + } + DescriptionText.Position = UDim2.new(0, 26, 0, SharedContentButton.Position.Y.Offset + SharedContentButton.Size.Y.Offset + 24) + + local function SetPrivilegeInView(privilegeSettings) + if privilegeSettings then + local MultiplayerSetting = privilegeSettings.Multiplayer + local SharedContentSetting = privilegeSettings.SharedContent + + if MultiplayerSetting.Success and SharedContentSetting.Success then + if MultiplayerSetting.PrivilegeCheckResult == "NoIssue" and SharedContentSetting.PrivilegeCheckResult == "NoIssue" then + DescriptionText.Text = Strings:LocalizedString("PrivilegeAllowedPhrase") + elseif MultiplayerSetting.PrivilegeCheckResult == "Banned" or SharedContentSetting.PrivilegeCheckResult == "Banned" then + DescriptionText.Text = Strings:LocalizedString("PrivilegeBannedPhrase") + elseif MultiplayerSetting.PrivilegeCheckResult == "PurchaseRequired" or SharedContentSetting.PrivilegeCheckResult == "PurchaseRequired" then + DescriptionText.Text = Strings:LocalizedString("PrivilegePurchaseRequiredPhrase") + else + DescriptionText.Text = Strings:LocalizedString("PrivilegeDeniedPhrase") + end + else + DescriptionText.Text = Strings:LocalizedString("PrivilegeErrorPhrase") + end + + MultiplayerIcon.ImageColor3 = MultiplayerSetting.CanJoinGame and Color3.new(0, 0.9, 0) or Color3.new(0.9, 0, 0) + SharedContentIcon.ImageColor3 = SharedContentSetting.CanJoinGame and Color3.new(0, 0.9, 0) or Color3.new(0.9, 0, 0) + MultiplayerText.Text = Strings:LocalizedString("MultiplayerWord")..": "..Strings:LocalizedString(MultiplayerSetting.LocalizedStringKey) + SharedContentText.Text = Strings:LocalizedString("SharedContentWord")..": "..Strings:LocalizedString(SharedContentSetting.LocalizedStringKey) + + SharedContentButtonLinkable = not(SharedContentSetting.PrivilegeCheckResult == "NoIssue" or SharedContentSetting.PrivilegeCheckResult == "Banned" or SharedContentSetting.PrivilegeCheckResult == "PurchaseRequired") + MultiplayerButtonLinkable = not(MultiplayerSetting.PrivilegeCheckResult == "NoIssue" or MultiplayerSetting.PrivilegeCheckResult == "Banned" or MultiplayerSetting.PrivilegeCheckResult == "PurchaseRequired") + SharedContentButtonSelectable = SharedContentButtonLinkable + MultiplayerButtonSelectable = MultiplayerButtonLinkable + else + SharedContentIcon.ImageColor3 = Color3.new(1, 1, 1) + MultiplayerIcon.ImageColor3 = Color3.new(1, 1, 1) + SharedContentText.Text = Strings:LocalizedString("SharedContentWord")..": "..Strings:LocalizedString("LoadingWord") + MultiplayerText.Text = Strings:LocalizedString("MultiplayerWord")..": "..Strings:LocalizedString("LoadingWord") + DescriptionText.Text = "" + SharedContentButtonLinkable = false + MultiplayerButtonLinkable = false + --Buttons are still selectable while loading, just don't launch deep link by click + SharedContentButtonSelectable = true + MultiplayerButtonSelectable = true + end + + SharedContentLinkIcon.Visible = SharedContentButtonLinkable + MultiplayerLinkIcon.Visible = MultiplayerButtonLinkable + SharedContentButton.Selectable = SharedContentButtonSelectable + MultiplayerButton.Selectable = MultiplayerButtonSelectable + SharedContentButton.ImageTransparency = SharedContentButtonSelectable and 0.25 or 1 + MultiplayerButton.ImageTransparency = MultiplayerButtonSelectable and 0.25 or 1 + if GuiService.SelectedCoreObject == SharedContentButton or GuiService.SelectedCoreObject == MultiplayerButton then + if not GuiService.SelectedCoreObject.Selectable then + Utility.SetSelectedCoreObject(Container.EnabledStatusButton) + end + end + end + + SetPrivilegeInView(GameplaySettingsData.GetPrivilegeSettings()) + GameplaySettingsData.OnGetPrivilegeSettingsBegin:connect(SetPrivilegeInView) + GameplaySettingsData.OnGetPrivilegeSettingsEnd:connect(SetPrivilegeInView) + end + + createCPPSettingsView() + createPrivilegeSettingsView() + + --[[ Public API ]]-- + function this:SetParent(newParent) + Container.Parent = newParent + end + + function this:Focus() + if errorCode then + if errorCode == 113 then + if MultiplayerButton and MultiplayerButton.Selectable then + Utility.SetSelectedCoreObject(MultiplayerButton) + else + Utility.SetSelectedCoreObject(EnabledStatusButton) + end + elseif errorCode == 116 then + if SharedContentButton and SharedContentButton.Selectable then + Utility.SetSelectedCoreObject(SharedContentButton) + else + Utility.SetSelectedCoreObject(EnabledStatusButton) + end + end + end + end + + function this:GetContainer() + return Container + end + + return this +end + +return createGameplaySettingsView diff --git a/Client2018/content/internal/AppShell/Modules/Shell/GamesPane.lua b/Client2018/content/internal/AppShell/Modules/Shell/GamesPane.lua new file mode 100644 index 0000000..20e3f2b --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/GamesPane.lua @@ -0,0 +1,226 @@ +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local GamesViewModule = require(ShellModules:FindFirstChild('GamesView')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local SortsData = require(ShellModules:FindFirstChild('SortsData')) +local GamesPaneDetailsView = require(ShellModules:FindFirstChild('GamesPaneDetailsView')) + +local function CreateGamesPane(parent) + local this = {} + local slideTweens = {} + + local isPaneFocused = false + + local noSelectionObject = Utility.Create'ImageLabel' + { + Name = 'NoSelectionObject'; + BackgroundTransparency = 1; + } + + local GamesPaneContainer = Utility.Create'Frame' + { + Name = 'GamesPane'; + Size = UDim2.new(1, 0, 0, 900); + Position = UDim2.new(0, 0, 0, -120); -- put it above the nav bar + BackgroundTransparency = 1; + Visible = false; + SelectionImageObject = noSelectionObject; + ClipsDescendants = false; + Parent = parent; + } + + local gameDetailsView = GamesPaneDetailsView() + gameDetailsView:TweenTransparency(1, 0) + gameDetailsView:SetParent(GamesPaneContainer) + + local GamesViewContainer = Utility.Create'Frame' + { + Name = "GamesViewContainer"; + + Size = UDim2.new(0, 1730, 0, 700); + Position = UDim2.new(0, 0, 0, 100); + + BackgroundTransparency = 1; + ClipsDescendants = false; + Parent = GamesPaneContainer; + } + + local UpPosition = UDim2.new(0, 0, 0, 100) + local UpSize = UDim2.new(0, 1730, 0, 700) + + local DownPosition = UDim2.new(0, 0, 0, 300) + local DownSize = UDim2.new(0, 1730, 0, 700) + + local function SlideGamesViewDown(tweens, duration) + local positionTweener = Utility.PropertyTweener( + GamesViewContainer, + 'Position', + UpPosition, + DownPosition, + duration, + Utility.SCurveUDim2 + ) + table.insert(tweens, positionTweener) + + local sizeTweener = Utility.PropertyTweener( + GamesViewContainer, + 'Size', + UpSize, + DownSize, + duration, + Utility.SCurveUDim2 + ) + table.insert(tweens, sizeTweener) + end + + local function SlideGamesViewUp(tweens, duration) + local positionTweener = Utility.PropertyTweener( + GamesViewContainer, + 'Position', + DownPosition, + UpPosition, + duration, + Utility.SCurveUDim2 + ) + table.insert(tweens, positionTweener) + + local sizeTweener = Utility.PropertyTweener( + GamesViewContainer, + 'Size', + DownSize, + UpSize, + duration, + Utility.SCurveUDim2 + ) + table.insert(tweens, sizeTweener) + end + + local function onNewGameSelected(data, faded) + gameDetailsView:SetGamePreview(data, faded) + end + + local GamesView = GamesViewModule( + { + Size = UDim2.new(1, 0, 0, 500); + Position = UDim2.new(0, -110, 0, 0); + CellSize = Vector2.new(1900, 300); + BackgroundTransparency = 1; + Spacing = Vector2.new(0, 0); + Padding = Vector2.new(0, 0); + ScrollDirection = "Vertical"; + ClipsDescendants = false; + SelectionMode = "TopLeft"; + Dynamic = true; + }, + onNewGameSelected + ) + + GamesView:SetParent(GamesViewContainer) + + -- Gradient for lower right corner + local gradient = Utility.Create'ImageLabel' + { + Name = "HintGradient"; + Size = UDim2.new(0, 631, 0, 276); + Position = UDim2.new(1, -631, 1, -276); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Images/IGG/IGGHintGradient.png'; + Parent = GamesPaneContainer; + } + + -- need to wait frame for abs size/pos to be set + spawn(function() + local xOffset = math.abs(GuiRoot.AbsoluteSize.x - GamesPaneContainer.AbsoluteSize.x - GamesPaneContainer.AbsolutePosition.x) + local yOffset = math.abs(GuiRoot.AbsoluteSize.y - GamesPaneContainer.AbsoluteSize.y - GamesPaneContainer.AbsolutePosition.y) + + gradient.Position = UDim2.new(1, -631 + xOffset, 1, -276 + yOffset) + end) + + function this:GetName() + return Strings:LocalizedString('GamesWord') + end + + function this:IsFocused() + return isPaneFocused + end + + --If we navigate to the gamespane from sub screens(game detail screen), the screen manager maniplulate the hide and show. + --If we navigate to the gamespane from other tabs, the apphub has it's logic: onSelectedTabChanged, to maniplulate the hide and show. + function this:Show(fromAppHub) + GamesPaneContainer.Visible = true + GamesViewContainer.Visible = true + --We reload Sorts only if we navigate from other tabs + if fromAppHub then + GamesView:Load() + end + + --Suspend Sorts BG Update whenever we are on GamesPane + SortsData:SuspendUpdate() + + self.TransitionTweens = ScreenManager:DefaultFadeIn(GamesPaneContainer) + ScreenManager:PlayDefaultOpenSound() + end + + function this:Hide(fromAppHub) + GamesPaneContainer.Visible = false + GamesViewContainer.Visible = false + ScreenManager:DefaultCancelFade(self.TransitionTweens) + self.TransitionTweens = nil + + --Don't delay the other pane showing + spawn(function() + --We resume Sorts BG Update only if we navigate to other tabs + if fromAppHub then + SortsData:ResumeUpdate() + SortsData:CallUpdate() + end + end) + end + + + function this:Focus(tabDock) + isPaneFocused = true + tabDock:Hide() + Utility.CancelTweens(slideTweens) + SlideGamesViewDown(slideTweens, GlobalSettings.TabDockTweenDuration) + GamesView:Focus() + gameDetailsView:Focus() + end + + --Whether the removefocus comes from screen manager/bumper/Button B + function this:RemoveFocus(fromAppHub) + if not isPaneFocused then return end + Utility.CancelTweens(slideTweens) + SlideGamesViewUp(slideTweens, GlobalSettings.TabDockTweenDuration) + GamesView:RemoveFocus(fromAppHub) + gameDetailsView:RemoveFocus() + isPaneFocused = false + end + + function this:SetPosition(newPosition) + GamesPaneContainer.Position = newPosition + end + + function this:SetParent(newParent) + GamesPaneContainer.Parent = newParent + end + + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('GamesPaneId')} + end + + function this:IsAncestorOf(object) + return GamesPaneContainer and GamesPaneContainer:IsAncestorOf(object) + end + + return this +end + +return CreateGamesPane \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/GamesPaneDetailsView.lua b/Client2018/content/internal/AppShell/Modules/Shell/GamesPaneDetailsView.lua new file mode 100644 index 0000000..d348309 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/GamesPaneDetailsView.lua @@ -0,0 +1,411 @@ +--[[ + // GamesPaneDetailsView.lua + + // Creates a details view for the currently selected game in the IGG +]] +local CoreGui = game:GetService("CoreGui") +local TextService = game:GetService('TextService') +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local GameData = require(ShellModules:FindFirstChild('GameData')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local VoteFrame = require(ShellModules:FindFirstChild('VoteFrame')) + +-- local Object, will handle the rotating of thumbnails. +local function createThumbnailView(thumbIds, parentContainer, faded) + local this = {} + + local PREVIEW_TIME = 4 + + local currentImageColor = faded and Color3.new(0.4, 0.4, 0.4) or Color3.new(1, 1, 1) + + local container = Utility.Create'Frame' + { + Name = "ThumbViewContainer"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Parent = parentContainer; + } + + local function createImage() + return Utility.Create'ImageLabel' + { + Name = "ThumbImage"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + ImageTransparency = 1; + ImageColor3 = currentImageColor; + Parent = container; + } + end + + local killView = false + function this:KillView() + killView = true + container.Parent = nil + end + + local thumbs = {} + if thumbIds and #thumbIds > 0 then + -- get first thumb loaded right away + local firstImage = createImage() + table.insert(thumbs, firstImage) + local loader = ThumbnailLoader:Create(firstImage, thumbIds[1], + ThumbnailLoader.Sizes.Large, ThumbnailLoader.AssetType.Icon, false) + + spawn(function() + loader:LoadAsync(true, true) + + -- start loading the rest + if #thumbIds > 1 then + for i = 2, #thumbIds do + local img = createImage() + table.insert(thumbs, img) + local ldr = ThumbnailLoader:Create(img, thumbIds[i], + ThumbnailLoader.Sizes.Large, ThumbnailLoader.AssetType.Icon, false) + spawn(function() + ldr:LoadAsync(true, false) + end) + end + + local currentThunbIndex = 1 + while not killView do + wait(PREVIEW_TIME) + if killView then + break + end + + -- cross fade + Utility.PropertyTweener(thumbs[currentThunbIndex], 'ImageTransparency', 0, 1, 1, Utility.EaseOutQuad, true) + + currentThunbIndex = currentThunbIndex + 1 + if currentThunbIndex > #thumbIds then + currentThunbIndex = 1 + end + + Utility.PropertyTweener(thumbs[currentThunbIndex], 'ImageTransparency', 1, 0, 1, Utility.EaseOutQuad, true) + end + end + end) + end + + function this:SetImageColor(color) + if not color then return end + + currentImageColor = color + for i = 1, #thumbs do + thumbs[i].ImageColor3 = currentImageColor + end + end + + return this +end + +local function createGamesPaneDetailsView() + local this = {} + + local inFocus = false + this.PlaceId = nil + local GamesPaneDetailsConns = {} + + local DETAILS_START_POS = UDim2.new(0, 0, 0, 196) + local DETAILS_FINAL_POS = UDim2.new(0, 0, 0, 116) + + -- GUI Objects + local viewContainer = Utility.Create'Frame' + { + Name = "ViewContainer"; + Size = UDim2.new(0, 1690, 0, 380); + Position = UDim2.new(0, 0, 0, -84); + BackgroundTransparency = 1; + } + + local detailsContainer = Utility.Create'Frame' + { + Name = "DetailsContainer"; + Size = UDim2.new(0, 900, 0, 216); + Position = DETAILS_START_POS; + BackgroundTransparency = 1; + Parent = viewContainer; + } + + local gameTitle = Utility.Create'TextLabel' + { + Name = "GameTitle"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, 0, 0, 18); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.HeaderSize; + Text = ""; + TextColor3 = GlobalSettings.WhiteTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + Parent = detailsContainer; + } + + local gameImageContainer = Utility.Create'Frame' + { + Name = "GameImageContainer"; + Size = UDim2.new(0, 700, 0, 380); + Position = UDim2.new(1, -700, 0, 6); + BackgroundTransparency = 1; + Parent = viewContainer; + } + + local voteFrame = VoteFrame(detailsContainer, UDim2.new(0, 40, 0, 76)) + local voteFrameContainer = voteFrame:GetContainer() + + local thumbsUpImage = Utility.Create'ImageLabel' + { + Name = "ThumbsUpImage"; + Size = UDim2.new(0, 28, 0, 28); + Position = UDim2.new(0, 0, 0, voteFrameContainer.Position.Y.Offset + voteFrameContainer.Size.Y.Offset - 28); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/ThumbsUpIcon@1080.png'; + Parent = detailsContainer; + } + + local thumbsDownImage = Utility.Create'ImageLabel' + { + Name = "ThumbsDownImage"; + Size = UDim2.new(0, 28, 0, 28); + Position = UDim2.new(0, voteFrameContainer.Position.X.Offset + voteFrameContainer.Size.X.Offset + 10, 0, voteFrameContainer.Position.Y.Offset); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/ThumbsDownIcon@1080.png'; + Parent = detailsContainer; + } + + local separatorDot = Utility.Create'ImageLabel' + { + Name = "SeparatorDot"; + Size = UDim2.new(0, 10, 0, 10); + Position = UDim2.new(0, thumbsDownImage.Position.X.Offset + thumbsDownImage.Size.X.Offset + 24, 0, voteFrameContainer.Position.Y.Offset + (voteFrameContainer.Size.Y.Offset/2) - (10/2)); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/SeparatorDot@1080.png'; + Parent = detailsContainer; + } + + + local creatorIcon = Utility.Create'ImageLabel' + { + Name = "CreatorIcon"; + Size = UDim2.new(0, 24, 0, 24); + Position = UDim2.new(0, separatorDot.Position.X.Offset + separatorDot.Size.X.Offset + 16, 0, separatorDot.Position.Y.Offset + separatorDot.Size.Y.Offset/2 - 12); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/RobloxIcon24.png'; + Parent = detailsContainer; + } + + local creatorName = Utility.Create'TextLabel' + { + Name = "CreatorName"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, creatorIcon.Position.X.Offset + creatorIcon.Size.X.Offset + 8, 0, separatorDot.Position.Y.Offset + separatorDot.Size.Y.Offset/2 - 2); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.DescriptionSize; + TextColor3 = GlobalSettings.LightGreyTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + Text = ""; + Parent = detailsContainer; + } + + local descriptionText = Utility.Create'TextLabel' + { + Name = "DescriptionText"; + Size = UDim2.new(1, 0, 0, 96); + Position = UDim2.new(0, 0, 1, -96); + BackgroundTransparency = 1; + Text = ""; + TextColor3 = GlobalSettings.LightGreyTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + Font = GlobalSettings.LightFont; + TextWrapped = true; + FontSize = GlobalSettings.DescriptionSize; + Parent = detailsContainer; + } + + local function setGameTitle(title) + if not title or title == gameTitle.Text then return end + + local function stringWidth(s) + return TextService:GetTextSize(s, + Utility.ConvertFontSizeEnumToInt(gameTitle.FontSize), + gameTitle.Font, + Vector2.new(0, 0)).X + end + + local suffix = "" + while stringWidth(title..suffix) > gameTitle.Parent.AbsoluteSize.X do + title = string.sub(title, 1,-2) + suffix = "..." + end + + gameTitle.Text = title..suffix + end + + local function setCreatorName(newName) + if not newName or newName == creatorName.Text then return end + creatorName.Text = newName + end + + local function setVotePanel(voteData) + local upVotes = voteData and voteData.UpVotes or 0 + local downVotes = voteData and voteData.DownVotes or 0 + if upVotes == 0 and downVotes == 0 then + voteFrame:SetPercentFilled(nil) + else + voteFrame:SetPercentFilled(upVotes / (upVotes + downVotes)) + end + end + + local function setDescription(newDescription) + if not newDescription or newDescription == descriptionText.Text then return end + + descriptionText.Text = newDescription + end + + local thumbnailView = nil + local function setThumbnailView(thumbIds, faded) + if thumbnailView then + thumbnailView:KillView() + thumbnailView = nil + end + + if thumbIds and #thumbIds > 1 then + thumbnailView = createThumbnailView(thumbIds, gameImageContainer, faded) + end + end + + local function setFaded(faded) + local fadeColor = faded and Color3.new(0.4, 0.4, 0.4) or Color3.new(1, 1, 1) + local tint = faded and 0.4 or 1 + + for _,child in pairs(detailsContainer:GetChildren()) do + if child:IsA('TextLabel') then + child.TextColor3 = fadeColor + elseif child:IsA('ImageLabel') then + child.ImageColor3 = fadeColor + end + end + voteFrame:SetImageColorTint(tint) + if thumbnailView then + thumbnailView:SetImageColor(fadeColor) + end + end + + function this:SetParent(newParent) + viewContainer.Parent = newParent + end + + function this:TweenTransparency(value, duration) + voteFrame:TweenTransparency(value, duration) + for _,child in pairs(detailsContainer:GetChildren()) do + if child:IsA('TextLabel') then + Utility.PropertyTweener(child, 'TextTransparency', child.TextTransparency, value, duration, Utility.Linear, true) + elseif child:IsA('ImageLabel') then + Utility.PropertyTweener(child, 'ImageTransparency', child.ImageTransparency, value, duration, Utility.Linear, true) + end + end + end + + + local function ClearGamePreview() + --Disconnect Events + Utility.DisconnectEvents(GamesPaneDetailsConns) + GamesPaneDetailsConns = {} + this.PlaceId = nil + setGameTitle("") + setCreatorName("") + setVotePanel() + setDescription("") + setThumbnailView() + end + + function this:SetGamePreview(placeId, faded) + Utility.DisconnectEvents(GamesPaneDetailsConns) + GamesPaneDetailsConns = {} + + local data = GameData:GetGameData(placeId) + if data then + --Use signals to make sure that these fetched data corresponds to the game we focus on + table.insert(GamesPaneDetailsConns, data.OnGetVoteDataEnd: + connect(function(voteData) setVotePanel(voteData) + end)) + + table.insert(GamesPaneDetailsConns, data.OnGetGameDetailsEnd: + connect(function(gameData) setDescription(gameData.Description or "") + end)) + + table.insert(GamesPaneDetailsConns, data.OnGetThumbnailIdsEnd: + connect(function(thumbnailIds) setThumbnailView(thumbnailIds, faded) + end)) + + setGameTitle(data.Name) + setCreatorName(data.CreatorName) + setVotePanel(data.VoteData) + setDescription(data.Description or "") + setThumbnailView(data.ThumbnailIds, faded) + setFaded(faded) + + + spawn(function() + if not data.VoteData and inFocus then + data:GetVoteDataAsync() + end + + if not data.Description and inFocus then + data:GetGameDetailsAsync() + end + + if not data.ThumbnailIds and inFocus then + data:GetThumbnailIdsAsync() + end + end) + else + ClearGamePreview() + end + end + + + function this:Remove() + viewContainer:Destroy() + end + + function this:Focus() + if inFocus then + return + end + + inFocus = true + self:TweenTransparency(0, GlobalSettings.TabDockTweenDuration) + Utility.PropertyTweener(detailsContainer, 'Position', detailsContainer.Position, DETAILS_FINAL_POS, + GlobalSettings.TabDockTweenDuration, Utility.SCurveUDim2, true) + end + + function this:RemoveFocus() + --Disconnect Events + Utility.DisconnectEvents(GamesPaneDetailsConns) + GamesPaneDetailsConns = {} + + if not inFocus then + return + end + this.PlaceId = nil + inFocus = false + Utility.PropertyTweener(detailsContainer, 'Position', detailsContainer.Position, DETAILS_START_POS, + GlobalSettings.TabDockTweenDuration, Utility.SCurveUDim2, true) + self:TweenTransparency(1, GlobalSettings.TabDockTweenDuration) + setThumbnailView(nil, false) + end + + return this +end + +return createGamesPaneDetailsView diff --git a/Client2018/content/internal/AppShell/Modules/Shell/GamesView.lua b/Client2018/content/internal/AppShell/Modules/Shell/GamesView.lua new file mode 100644 index 0000000..2a6bcb0 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/GamesView.lua @@ -0,0 +1,524 @@ + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local GuiService = game:GetService('GuiService') + +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local SortsData = require(ShellModules:FindFirstChild('SortsData')) +local FFlagXboxUseCuratedGameSort = SortsData.FFlagXboxNewGameSortsEndpoint + +local ScrollingGrid = require(ShellModules:FindFirstChild('ScrollingGrid')) +local GameCarouselItem = require(ShellModules:FindFirstChild('GameCarouselItem')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local LockedGameCarouselView = require(ShellModules:FindFirstChild('LockedGameCarouselView')) +local AchievementManager = require(ShellModules:FindFirstChild('AchievementManager')) + +local createGamesView = function(viewGridConfig, onNewGameSelected) + + local viewGridContainer = ScrollingGrid(viewGridConfig) + viewGridContainer.Container.Visible = false + local this = {} + + local carouselItems = {} + --The local savedCarouselItems which will always hold all the carouselItems even + --there is no games in it + local savedCarouselItems = {} + + local selectedCoreObjectChangedCn = nil + local setLoadDebounce = false + local inFocus = false + local lastLoadTime = nil + local sortCategories = nil + local savedSelectedCarouselItem = nil + + local staticSelectionImage = Utility.Create'ImageLabel' + { + -- Values pulled from GuiService.cpp; see constructor + -- these numbers will need to be updated if positions/sizes change + Name = "StaticSelectionImage"; + Size = UDim2.new(0, 232 + 14, 0, 232 + 14); + BackgroundTransparency = 1; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(19, 19, 43, 43); + Image = "rbxasset://textures/ui/SelectionBox.png"; + Selectable = false; + ZIndex = 3; + } + + local lockView = LockedGameCarouselView() + lockView:SetSelectionImageObject( + Utility.Create'ImageLabel' + { + BackgroundTransparency = 1; + } + ) + + lockView:SetParent(nil) + lockView:SetZIndex(2) + + local function GetSelectionRectangleFromFirstItem(firstItemContainer) + return UDim2.new( + firstItemContainer.Position.X.Scale, firstItemContainer.Position.X.Offset - 7, + firstItemContainer.Position.Y.Scale, firstItemContainer.Position.Y.Offset + 50 - 7) + end + + local function showStaticSelectionImage() + local firstSortItem = #carouselItems > 0 and carouselItems[1] or nil + if firstSortItem then + local firstItemContainer = firstSortItem:GetContainer() + local pos = GetSelectionRectangleFromFirstItem(firstItemContainer) + staticSelectionImage.Position = pos + staticSelectionImage.Size = UDim2.new(0, 232 + 14, 0, 232 + 14); + staticSelectionImage.Parent = viewGridContainer.Container.Parent + end + lockView:SetParent(nil) + end + + local function enterNormalSelectionMode() + lockView:SetParent(nil) + local firstSortItem = #carouselItems > 0 and carouselItems[1] or nil + if firstSortItem then + local firstItemContainer = firstSortItem:GetContainer() + local pos = GetSelectionRectangleFromFirstItem(firstItemContainer) + staticSelectionImage.Position = pos + staticSelectionImage.Size = UDim2.new(0, 232 + 14, 0, 232 + 14); + end + end + + local function enterLockedRowSelectionMode() + local firstSortItem = #carouselItems > 0 and carouselItems[1] or nil + if firstSortItem then + local firstItemView = firstSortItem:GetCarouselView() + if firstItemView then + local viewContainerSize = firstItemView:GetContainer().Size + + -- scaling comes from GameCarouselItem + local lockViewSize = UDim2.new(0, viewContainerSize.X.Offset, 0, viewContainerSize.Y.Offset * 0.84) + lockView:SetSize(lockViewSize) + lockView:SetPosition(UDim2.new(0, 8, 0, 8)) + + staticSelectionImage.Size = lockViewSize + UDim2.new(0, 16, 0, 16) + + local viewContainerPos = firstItemView:GetContainer().Position + staticSelectionImage.Position = UDim2.new( + viewContainerPos.X.Scale, + viewContainerPos.X.Offset - 118, + viewContainerPos.Y.Scale, + viewContainerPos.Y.Offset + 10 + ) + end + end + lockView:SetParent(staticSelectionImage) + end + + local function removeStaticSelectionImage() + staticSelectionImage.Parent = nil + end + + local fadableObjets = {} + + local function getItemByIndex(i) + local item = carouselItems[i] + return item and item:GetContainer() + end + + local function getIndexByItem(item) + for i = 1, #carouselItems do + if item == carouselItems[i] then + return i + end + end + + return 0 + end + + local function getMappedIndex(item) + for i = 1, #savedCarouselItems do + if item == savedCarouselItems[i] then + return i + end + end + + return 0 + end + + local lastSelectedRow = 0 + local function GetSelectedRow() + for i = 1, #carouselItems do + local item = carouselItems[i] + if item:IsSelected() then + return i + end + end + return 0 + end + + local function GetSelectedItem() + for i = 1, #carouselItems do + local item = carouselItems[i] + if item:IsSelected() then + return item + end + end + return nil + end + + local function onSelectedCoreObjectChanged() + if setLoadDebounce then return end + local currentSelectedRow = GetSelectedRow() + lastSelectedRow = currentSelectedRow + local selectedItem = nil + + if currentSelectedRow == 0 then + for i = 1, #carouselItems do + local item = carouselItems[i] + item:RemoveFocus() + item:SetTransparency(0, 0, 0.5) + end + else + for i = 1, #carouselItems do + local item = carouselItems[i] + local imageTransparency = 0 + local textTransparency = 0 + if item:IsSelected() then + item:Focus() + showStaticSelectionImage() + selectedItem = item + else + if i < currentSelectedRow then + -- Making these bigger than one causes the fadeout to happen before the carousel + -- overlaps with the game info and preview-image above. It looks better. + imageTransparency = 4 + textTransparency = 4 + elseif i == currentSelectedRow then + imageTransparency = 0 + textTransparency = 0 + else + imageTransparency = 0.5 + textTransparency = 0.5 + end + item:RemoveFocus() + end + + item:SetTransparency(imageTransparency, textTransparency, nil) + end + if selectedItem then + if selectedItem:IsLocked() then + enterLockedRowSelectionMode() + else + enterNormalSelectionMode() + end + end + end + end + + local function UpdateTransparency() + local currentSelectedRow = GetSelectedRow() + if currentSelectedRow > 0 then + for i = 1, #carouselItems do + local item = carouselItems[i] + local imageTransparency = 0 + local textTransparency = 0 + if i < currentSelectedRow then + imageTransparency = 4 + textTransparency = 4 + elseif i == currentSelectedRow then + imageTransparency = 0 + textTransparency = 0 + else + imageTransparency = 0.5 + textTransparency = 0.5 + end + + item:SetTransparency(imageTransparency, textTransparency, 0, true) + end + end + end + + + local function carouselItemsResultsUpdate(item) + if not item then return end + local lastItemIndex = getIndexByItem(item) + if item:HasResults() then + --Add a new row + if lastItemIndex == 0 then + local prevSelectedRow = GetSelectedRow() + local Inserted = false + for i = 1, #carouselItems do + if getMappedIndex(item) < getMappedIndex(carouselItems[i]) then + table.insert(carouselItems, i, item) + Inserted = true + break + end + end + if not Inserted then + table.insert(carouselItems, item) + end + --Recalc when carouselItems changes + viewGridContainer:RecalcLayout(#carouselItems + 1) + --Try to select the same row after insertion happened + carouselItems[prevSelectedRow]:Focus() + end + else + --Remove a row + if lastItemIndex > 0 then + local wasSelected = item:IsSelected() + item:RemoveFocus() + --If was in the carouselItems, remove + table.remove(carouselItems, lastItemIndex) + viewGridContainer:RecalcLayout(#carouselItems + 1) + + --The selected row get removed, select the same selected row or first row if doesn't exist + if inFocus and wasSelected then + if lastItemIndex > 0 and lastItemIndex <= #carouselItems then + carouselItems[lastItemIndex]:Focus() + else + viewGridContainer:BackToInitial() + if #carouselItems > 0 then + carouselItems[1]:Focus() + end + end + end + lastSelectedRow = GetSelectedRow() + end + end + + UpdateTransparency() + end + + local function createCarouselItem(name, getGameCollection) + local item = GameCarouselItem(UDim2.new(0, 1900, 0, 250), name, getGameCollection, onNewGameSelected, carouselItemsResultsUpdate) + table.insert(carouselItems, item) + table.insert(savedCarouselItems, item) + return item + end + + local function createCarouselItems(sortCategories) + local featuredSortId = 3 + for i = 1, #sortCategories do + local sort = sortCategories[i] + local item = createCarouselItem(sort["Name"], function() return sort end) + if not sort["userId"] then --If the sort is not related to user + if not FFlagXboxUseCuratedGameSort then + if item and sort["Id"] ~= featuredSortId then + item:SetLockInPUP(true) + end + else + local sortGameSetTargetId = sort["GameSetTargetId"] or 0 + if item and not (sort["Id"] == featuredSortId and sortGameSetTargetId == 0) then + item:SetLockInPUP(true) + end + end + end + end + + viewGridContainer:SetItemCallback(getItemByIndex) + viewGridContainer:RecalcLayout(#carouselItems + 1) + + for i = 1, #savedCarouselItems do + if not AchievementManager:AllGamesUnlocked() and savedCarouselItems[i]:GetLockInPUP() then + savedCarouselItems[i]:Lock() + else + savedCarouselItems[i]:Unlock() + end + end + + for i = 1, #savedCarouselItems do + savedCarouselItems[i]:Init() + end + end + + --Load function for SortsData + local sortsDataVersion = nil + local function LoadGameSorts() + if setLoadDebounce then return end + setLoadDebounce = true + + local needUpdate = false + local sortsData = nil + if SortsData:HasSorts() then --Sync Call + sortsData = SortsData:GetSorts() + if not sortsDataVersion or sortsData.Version ~= sortsDataVersion then + needUpdate = true + end + else + needUpdate = true + end + + if needUpdate then + viewGridContainer.Container.Visible = false + spawn(function() + local maxRetry = 2 + while maxRetry > 0 and not sortsData do + sortsData = SortsData:GetSorts() + maxRetry = maxRetry - 1 + end + + if sortsData then + if #savedCarouselItems > 0 then + --Remove all + for i = 1, #savedCarouselItems do + savedCarouselItems[i]:Destroy() + end + end + + --Reset all data + carouselItems = {} + savedCarouselItems = {} + savedSelectedCarouselItem = nil + onNewGameSelected(nil) + viewGridContainer:RecalcLayout(0) + viewGridContainer:BackToInitial() + lastSelectedRow = 0 + + --Create new CarouselItems + createCarouselItems(sortsData.Data) + sortsDataVersion = sortsData.Version + end + + viewGridContainer.Container.Visible = true + if inFocus then + if #carouselItems > 0 then + carouselItems[1]:Focus() + showStaticSelectionImage() + UpdateTransparency() + end + end + setLoadDebounce = false + end) + else + setLoadDebounce = false + end + end + + local function onGridViewFocused() + --Focus on viewGridContainer and the selected row + viewGridContainer:Focus() + if not setLoadDebounce then + local selectedRow = 0 + if savedSelectedCarouselItem then + selectedRow = getIndexByItem(savedSelectedCarouselItem) + savedSelectedCarouselItem = nil + else + selectedRow = GetSelectedRow() + end + + if selectedRow > 0 and selectedRow <= #carouselItems then + carouselItems[selectedRow]:Focus() + else + viewGridContainer:BackToInitial() + if #carouselItems > 0 then + carouselItems[1]:Focus() + end + end + UpdateTransparency() + end + end + + local function onGridViewFocusRemoved(fromAppHub) + savedSelectedCarouselItem = GetSelectedItem() + lastSelectedRow = 0 + + --If the bumper/button B caused the removing focus, reset to the top of viewGridContainer + if fromAppHub then + viewGridContainer:BackToInitial(GlobalSettings.TabDockTweenDuration) + savedSelectedCarouselItem = nil + end + --Remove image and the focus on all inside elements + removeStaticSelectionImage() + viewGridContainer:RemoveFocus() + for i = 1, #carouselItems do + local item = carouselItems[i] + item:RemoveFocus() + item:SetTransparency(0, 0, 0.5) + end + end + + function this:GetDefaultFocusItem() + return nil + end + + function this:SetParent(parent) + viewGridContainer:SetParent(parent) + end + + function this:Load() + LoadGameSorts() + end + + function this:Focus() + inFocus = true + Utility.DisconnectEvent(selectedCoreObjectChangedCn) + selectedCoreObjectChangedCn = GuiService:GetPropertyChangedSignal('SelectedCoreObject'):connect(function() + onSelectedCoreObjectChanged() + end) + onGridViewFocused() + end + + function this:RemoveFocus(fromAppHub) + inFocus = false + Utility.DisconnectEvent(selectedCoreObjectChangedCn) + onGridViewFocusRemoved(fromAppHub) + end + + + EventHub:removeEventListener(EventHub.Notifications["PlayedGamesChanged"], "GamesPaneRecentlyPlayedUpdate") + EventHub:removeEventListener(EventHub.Notifications["FavoriteToggle"], "GamesPaneFavoritesUpdate") + EventHub:removeEventListener(EventHub.Notifications["UnlockedUGC"], "GamesPaneUnlockedUGC") + + --Add listener to PlayedGamesChanged and FavoriteToggle to reload these two types timely + EventHub:addEventListener(EventHub.Notifications["PlayedGamesChanged"], "GamesPaneRecentlyPlayedUpdate", + function() + spawn(function() + local updateItemName = Strings:LocalizedString("RecentlyPlayedSortTitle") + for i = 1, #savedCarouselItems do + if updateItemName == savedCarouselItems[i]:GetSortName() then + savedCarouselItems[i]:Refresh() + break + end + end + end) + end) + + EventHub:addEventListener(EventHub.Notifications["FavoriteToggle"], "GamesPaneFavoritesUpdate", + function(success) + if success then + spawn(function() + local updateItemName = Strings:LocalizedString("FavoritesSortTitle") + for i = 1, #savedCarouselItems do + if updateItemName == savedCarouselItems[i]:GetSortName() then + savedCarouselItems[i]:Refresh() + break + end + end + end) + end + end) + + if not AchievementManager:AllGamesUnlocked() then + EventHub:addEventListener(EventHub.Notifications["UnlockedUGC"], "GamesPaneUnlockedUGC", + function() + spawn(function() + for i = 1, #savedCarouselItems do + if savedCarouselItems[i]:IsLocked() then + savedCarouselItems[i]:Unlock() + end + end + end) + end) + end + + --[[ Initialize - Don't Block ]]-- + spawn(function() + LoadGameSorts() + end) + + return this +end + +return createGamesView \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/GlobalSettings.lua b/Client2018/content/internal/AppShell/Modules/Shell/GlobalSettings.lua new file mode 100644 index 0000000..37e5009 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/GlobalSettings.lua @@ -0,0 +1,177 @@ +-- Written by Kip Turner, Copyright Roblox 2015 + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild("Utility")) + +local BASE_SOUND_URL = "rbxasset://sounds/ui/Shell/" +local BASE_IMAGE_URL = "rbxasset://textures/ui/Shell/" + +--Move all new settings to corresponding type table +local Settings = +{ + WhiteTextColor = Color3.new(1, 1, 1), + GreyTextColor = Color3.new(0.5, 0.5, 0.5), + LightGreyTextColor = Color3.new(184 / 255, 184 / 255, 184 / 255), + BlueTextColor = Color3.new(0, 116 / 255, 189 / 255), + BlackTextColor = Color3.new(0, 0, 0), + GreenTextColor = Color3.new(2 / 255, 183 / 255, 87 / 255), + RedTextColor = Color3.new(216 / 255, 104 / 255, 104 / 255), + OrangeTextColor = Color3.new(246 / 255, 136 / 255, 2 / 255), + TextSelectedColor = Color3.new(25 / 255, 25 / 255, 25 / 255), + LineBreakColor = Color3.new(78 / 255, 78 / 255, 78 / 255), + PageDivideColor = Color3.new(151 / 255, 151 / 255, 151 / 255), + BadgeOwnedColor = Color3.new(45 / 255, 96 / 255, 128 / 255), + BadgeOverlayColor = Color3.new(13 / 255, 28 / 255, 38 / 255), + OverlayColor = Color3.new(26 / 255, 57 / 255, 76 / 255), + BadgeFrameColor = Color3.new(106 / 255, 120 / 255, 129 / 255), + RobuxOverlayImageColor = Color3.new(42 / 255, 51 / 255, 57 / 255), + BlueButtonColor = Color3.fromRGB(0, 162, 255), + GreySelectionColor = Color3.new(84 / 255, 99 / 255, 109 / 255), + GreenButtonColor = Color3.new(2 / 255, 163 / 255, 77 / 255), + GreenSelectedButtonColor = Color3.new(63 / 255, 198 / 255, 121 / 255), + GreyButtonColor = Color3.new(78 / 255, 84 / 255, 96 / 255), + GreySelectedButtonColor = Color3.new(50 / 255, 181 / 255, 1), + CharacterBackgroundColor = Color3.new(39 / 255, 69 / 255, 82 / 255), + ForegroundGreyColor = Color3.new(58 / 255, 60 / 255, 64 / 255), + BackgroundGreyColor = Color3.new(78 / 255, 84 / 255, 96 / 255), + ModalBackgroundColor = Color3.new(0, 0, 0), + AvatarBoxBackgroundColor = Color3.new(255 / 255, 255 / 255, 255 / 255), + TabUnderlineColor = Color3.new(50 / 255, 181 / 255, 255 / 255), + PriceLabelColor = Color3.new(241 / 255, 116 / 255, 10 / 255), + PromoLabelColor = Color3.new(1, 0, 0), + StatusIconEnabledColor = Color3.new(2 / 255, 183 / 255, 87 / 255), + StatusIconDisabledColor = Color3.new(1, 0, 0), + TextBoxColor = Color3.new(1, 1, 1), + + TextBoxSelectedTransparency = 0.5, + TextBoxDefaultTransparency = 0.75, + AvatarBoxBackgroundSelectedTransparency = 0.75, + AvatarBoxBackgroundDeselectedTransparency = 0.875, + AvatarBoxTextSelectedTransparency = 0, + AvatarBoxTextDeselectedTransparency = 0.5, + ModalBackgroundTransparency = 0.3, + FriendStatusTextTransparency = 0.5, + + LargeHeadingSize = Enum.FontSize.Size48, + MediumLargeHeadingSize = Enum.FontSize.Size36, + MediumHeadingSize = Enum.FontSize.Size24, + SmallHeadingSize = Enum.FontSize.Size18, + ParagraphSize = Enum.FontSize.Size14, + -- Font Sizes: Deprecated + -- RobloxSize -> Mockup Sizes + LargeFontSize = Enum.FontSize.Size96, -- 72pt + HeaderSize = Enum.FontSize.Size60, -- 48pt + MediumFontSize = Enum.FontSize.Size48, -- 36pt + TitleSize = Enum.FontSize.Size42, -- 34pt + ButtonSize = Enum.FontSize.Size36, -- 30pt + DescriptionSize = Enum.FontSize.Size32, -- 26pt + SubHeaderSize = Enum.FontSize.Size28, -- 24pt + SmallTitleSize = Enum.FontSize.Size24, -- 20pt + HeadingFont = Enum.Font.SourceSans, + + -- Font Types + RegularFont = Enum.Font.SourceSans, + LightFont = Enum.Font.SourceSansLight, + BoldFont = Enum.Font.SourceSansBold, + ItalicFont = Enum.Font.SourceSansItalic, + + TabItemSpacing = 30, + -- Screen priority + DefaultPriority = 1, + OverlayPriority = 2, + ElevatedPriority = 3, + ImmediatePriority = 4, + TabDockTweenDuration = 0.35, + AvatarPaneRefreshInterval = Utility.GetFastVariable("XboxAvatarPaneRefreshInterval") and tonumber(Utility.GetFastVariable("XboxAvatarPaneRefreshInterval")) or 1800, + GameDetailsRefreshInterval = Utility.GetFastVariable("XboxGameDetailsRefreshInterval") and tonumber(Utility.GetFastVariable("XboxGameDetailsRefreshInterval")) or 1800, + GameSortsUpdateInterval = Utility.GetFastVariable("XboxGameSortsUpdateInterval") and tonumber(Utility.GetFastVariable("XboxGameSortsUpdateInterval")) or 1800, + + --Offset For Text + TextVerticalPadding = 10, + TextHorizontalPadding = 13, + --Image + RoundCornerButtonImage = BASE_IMAGE_URL.."Buttons/Generic9ScaleButton@720.png", + Fonts = + { + Heading = Enum.Font.SourceSans, + Regular = Enum.Font.SourceSans, + Light = Enum.Font.SourceSansLight, + Bold = Enum.Font.SourceSansBold, + Italic = Enum.Font.SourceSansItalic, + }, + Colors = + { + BlueButton = Color3.fromRGB(0, 162, 255), + WhiteButton = Color3.fromRGB(255, 255, 255), + GreySelectedButton = Color3.fromRGB(50, 181, 255), + TextSelected = Color3.fromRGB(25, 25, 25), + WhiteText = Color3.fromRGB(255, 255, 255), + GreenText = Color3.fromRGB(2, 183, 87), + BlueText = Color3.fromRGB(0, 116, 189), + OrangeText = Color3.fromRGB(246, 136, 2), + GreyText = Color3.fromRGB(127, 127, 127), + LightGreyText = Color3.fromRGB(184, 184, 184), + CharacterBackground = Color3.fromRGB(39, 69, 82), + BlackText = Color3.fromRGB(0, 0, 0), + }, + TextSizes = + { + Large = 96, -- 72pt + Header = 60, -- 48pt + Medium = 48, -- 36pt + Title = 42, -- 34pt + Button = 36, -- 30pt + Description = 32, -- 26pt + SubHeader = 28, -- 24pt + SmallTitle = 24, -- 20pt + }, + Images = + { + RoundCornerButton = "Buttons/Generic9ScaleButton@720.png", + ButtonDefault = "Buttons/Button_1080.png", + ButtonSelector = "Buttons/gr-item selector-8px corner.png", + RightArrow = "Icons/RightIndicatorIcon@1080.png", + EnabledStatusIcon = "Icons/EnabledStatusIcon.png", + Shadow = "/Buttons/Generic9ScaleShadow.png", + RobloxIcon = "Icons/RobloxIcon24.png", + OnlineStatusIcon = "Icons/OnlineStatusIcon@1080.png", + DefaultProfile = "Icons/DefaultProfileIcon.png", + }, + Sounds = + { + BackgroundLoop = "RobloxMusic.ogg", + Error = "Error.mp3", + ButtonPress = "ButtonPress.mp3", + MoveSelection = "MoveSelection.mp3", + OverlayOpen = "OverlayOpen.mp3", + PopUp = "PopUp.mp3", + PurchaseSuccess = "PurchaseSuccess.mp3", + ScreenChange = "ScreenChange.mp3", + SideMenuSlideIn = "SideMenuSlideIn.mp3", + }, + Transparency = + { + TextBoxSelected = 0.5, + TextBoxDefault = 0.75, + AvatarBoxBackgroundSelected = 0.75, + AvatarBoxBackgroundDeselected = 0.875, + AvatarBoxTextSelected = 0, + AvatarBoxTextDeselected = 0.5, + ModalBackground = 0.3, + FriendStatusText = 0.5, + } +} + +for k in pairs(Settings.Images) do + Settings.Images[k] = table.concat {BASE_IMAGE_URL, Settings.Images[k]} +end + +for k in pairs(Settings.Sounds) do + Settings.Sounds[k] = table.concat {BASE_SOUND_URL, Settings.Sounds[k]} +end + +return Settings diff --git a/Client2018/content/internal/AppShell/Modules/Shell/HeroStatsManager.lua b/Client2018/content/internal/AppShell/Modules/Shell/HeroStatsManager.lua new file mode 100644 index 0000000..268b523 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/HeroStatsManager.lua @@ -0,0 +1,114 @@ +-- Written by Kip Turner, Copyright Roblox 2015 + +-- Herostats Manager +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local Http = require(ShellModules:FindFirstChild('Http')) +local PlatformInterface = require(ShellModules:FindFirstChild('PlatformInterface')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local XboxAppState = require(ShellModules:FindFirstChild('AppState')) + +local VIEW_GAMETYPE_ENUM = +{ + AppShell = 0; + Game = 1; +} + +local HeroStatsManager = {} + +function HeroStatsManager:SendHeroStatsEventAsync(heroStatName, setValue) + Utility.DebugLog("HeroStatsManager - event name:" , heroStatName , "event value:" , setValue) + local heroStatStatus = nil + local success, msg = pcall(function() + -- NOTE: Yielding function + if PlatformService and not UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows then + heroStatStatus = PlatformService:BeginHeroStat(heroStatName, setValue) + end + end) + if not success then + -- NOTE: very likely this function ever throws an error but returns error codes + Utility.DebugLog("HeroStatsManager - event name:" , heroStatName , "value" , setValue , "for reason:" , msg) + end + + Utility.DebugLog("HeroStatsManager - event name:" , heroStatName , "event status:" , heroStatStatus) +end + +local function UpdateEquippedPackagesAsync() + local myUserId = XboxAppState.store:getState().RobloxUser.rbxuid + local packages = myUserId and Http.GetUserOwnedPackagesAsync(myUserId) + local data = packages and packages['IsValid'] and packages['Data'] + local items = data and data['Items'] + + -- local Utility = require(ShellModules:FindFirstChild('Utility')) + if items then + local numberOwnedPackages = #items + HeroStatsManager:SendHeroStatsEventAsync("AvatarsEquipped", numberOwnedPackages) + end +end + +local joinDebounce = false +local function OnJoinedGameAsync() + if joinDebounce then return end + joinDebounce = true + HeroStatsManager:SendHeroStatsEventAsync("GamesCount") + joinDebounce = false +end + +EventHub:addEventListener(EventHub.Notifications["DonnedDifferentPackage"], "HeroStatsManager", + function(packageId) + spawn(UpdateEquippedPackagesAsync) + end) + +EventHub:addEventListener(EventHub.Notifications["AvatarEquipSuccess"], "HeroStatsManager", + function(packageId) + spawn(UpdateEquippedPackagesAsync) + end) + +EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "HeroStatsManager", + function() + spawn(UpdateEquippedPackagesAsync) + end) + +if PlatformService then + PlatformService.ViewChanged:connect(function(newView) + if newView == VIEW_GAMETYPE_ENUM['Game'] then + spawn(OnJoinedGameAsync) + end + end) +end + +spawn(function() + if UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows then return end + + local last = nil + + while true do + local partyMembers = PlatformInterface:GetPartyMembersAsync() + local inParty = PlatformInterface:IsInAParty(partyMembers) + + local current = tick() + if inParty then + if last then + if current - last > 60 then + HeroStatsManager:SendHeroStatsEventAsync("PartyCount") + last = last + 60 + end + else + last = current + end + else + last = nil + end + + wait(60) + end +end) + +return HeroStatsManager diff --git a/Client2018/content/internal/AppShell/Modules/Shell/HintActionView.lua b/Client2018/content/internal/AppShell/Modules/Shell/HintActionView.lua new file mode 100644 index 0000000..55cb2f0 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/HintActionView.lua @@ -0,0 +1,131 @@ +--[[ + // HintActionView.lua + + // Creates a hint action view that can be used to bind an action + // to a button on a gamepad. +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local ContextActionService = game:GetService("ContextActionService") +local TextService = game:GetService('TextService') + +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local TEXT_PADDING = 8 + +local function createHintActionView(parent, actionName, positionMult) + if positionMult == nil then + positionMult = UDim2.new(1, -1, 1, -1) + end + + local this = {} + + if actionName == nil or #actionName == 0 then + actionName = "DefaultHintAction" + end + + -- GUIs + local container = Utility.Create'Frame' + { + Name = "HintActionContainer"; + Size = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + Parent = parent; + } + + local text = Utility.Create'TextLabel' + { + Name = "HintActionText"; + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.TitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextXAlignment = Enum.TextXAlignment.Right; + Text = ""; + ZIndex = 10; + Parent = container; + } + + local buttonImage = Utility.Create'ImageLabel' + { + Name = "HintActionImage"; + Size = UDim2.new(0, 83, 0, 83); + BackgroundTransparency = 1; + Image = ""; + ZIndex = 10; + Parent = container; + } + + local function updateLayout() + local size = TextService:GetTextSize(text.Text, Utility.ConvertFontSizeEnumToInt(text.FontSize), text.Font, Vector2.new(0, 0)) + text.Size = UDim2.new(0, size.x, 0, buttonImage.Size.Y.Offset) + text.Position = UDim2.new(1, -size.x, 0, -4) + + size = size.x + buttonImage.Size.X.Offset + TEXT_PADDING + + container.Size = UDim2.new(0, size, 0, buttonImage.Size.Y.Offset) + + container.Position = UDim2.new( + positionMult.X.Scale, + positionMult.X.Offset * container.Size.X.Offset, + positionMult.Y.Scale, + positionMult.Y.Offset * container.Size.Y.Offset) + end + + -- Action bind + function this:BindAction(actionFunc, keyCode) + if not actionFunc then + return + end + + ContextActionService:UnbindCoreAction(actionName) + ContextActionService:BindCoreAction(actionName, actionFunc, false, keyCode) + end + + function this:UnbindAction() + ContextActionService:UnbindCoreAction(actionName) + end + + function this:SetParent(newParent) + container.Parent = newParent + end + + function this:SetVisible(value) + container.Visible = value + end + + function this:SetTransparency(value) + text.TextTransparency = value + buttonImage.ImageTransparency = value + end + + function this:SetVisibleWithTween(newValue) + Utility.PropertyTweener(text, "TextTransparency", text.TextTransparency, newValue, 0.25, Utility.EaseOutQuad, true) + Utility.PropertyTweener(buttonImage, "ImageTransparency", buttonImage.ImageTransparency, newValue, 0.25, Utility.EaseOutQuad, true) + end + + function this:SetText(newText) + if newText == text.Text then + return + end + + text.Text = newText + updateLayout() + end + + function this:SetImage(newImage) + if newImage == buttonImage.Image then + return + end + + buttonImage.Image = newImage + end + + return this +end + +return createHintActionView diff --git a/Client2018/content/internal/AppShell/Modules/Shell/HomePane.lua b/Client2018/content/internal/AppShell/Modules/Shell/HomePane.lua new file mode 100644 index 0000000..bf0ace2 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/HomePane.lua @@ -0,0 +1,485 @@ +-- Written by Kip Turner, Copyright Roblox 2015 +local CoreGui = game:GetService("CoreGui") +local GuiService = game:GetService('GuiService') +local RunService = game:GetService("RunService") + +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local FriendsData = require(ShellModules:FindFirstChild('FriendsData')) +local FriendsView = require(ShellModules:FindFirstChild('FriendsView')) +local ScrollingGridModule = require(ShellModules:FindFirstChild('ScrollingGrid')) +local GameSort = require(ShellModules:FindFirstChild('GameSort')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local AssetManager = require(ShellModules:FindFirstChild('AssetManager')) +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader')) +local SortsData = require(ShellModules:FindFirstChild('SortsData')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local XboxAppState = require(ShellModules:FindFirstChild('AppState')) +local XboxRecommendedPeople = settings():GetFFlag("XboxRecommendedPeople2") + +local MOCKUP_SIZE = Vector2.new(1920, 1080) +local PROFILE_SIZE = Vector2.new(450, 343) +local PROFILE_NAME_SIZE = Vector2.new(450, 38) +local PROFILE_BUTTON_SIZE = Vector2.new(450, 300) +local PROFILE_IMAGE_SIZE = Vector2.new(450, 300) +local SORTS_SIZE = Vector2.new(1234, 608) + +local function CreateHomePane(parent) + local this = {} + + local inFocus = false + + local sortsObjects = {} + + + local HomePaneContainer = Utility.Create'Frame' + { + Name = 'HomePane'; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Parent = parent; + } + + local ProfileContainer = Utility.Create'Frame' + { + Name = 'ProfileContainer'; + Position = UDim2.new(0,0,0,0); + BackgroundTransparency = 1; + Parent = HomePaneContainer; + } + + local NameLabel = Utility.Create'TextLabel' + { + Name = 'NameLabel'; + Text = ''; + TextXAlignment = 'Left'; + TextYAlignment = 'Top'; + -- TextScaled = true; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.SubHeaderSize; + BackgroundTransparency = 1; + Parent = ProfileContainer; + }; + local ProfileButton = Utility.Create'ImageButton' + { + Name = 'ProfileButton'; + AutoButtonColor = false; + BackgroundTransparency = 1; + Parent = ProfileContainer; + + SoundManager:CreateSound('MoveSelection'); + } + local ProfileImage = Utility.Create'ImageLabel' + { + Name = 'ProfileImage'; + Position = UDim2.new(0,0,0,0); + BackgroundTransparency = 1; + BorderSizePixel = 0; + ZIndex = 3; + Parent = ProfileButton; + }; + Utility.Create'Frame' + { + Name = 'ProfileImageBackground'; + Size = UDim2.new(1,0,1,0); + BorderSizePixel = 0; + BackgroundTransparency = 0; + BackgroundColor3 = GlobalSettings.CharacterBackgroundColor; + ZIndex = 2; + Parent = ProfileImage; + AssetManager.CreateShadow(1); + } + local AvatarBrushBackground = Utility.Create'Frame' + { + Name = 'AvatarBrushImage'; + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.AvatarBoxBackgroundColor; + BackgroundTransparency = GlobalSettings.AvatarBoxBackgroundDeselectedTransparency; + ZIndex = 2; + -- Parent = ProfileButton; + } + local AvatarBrushImage = Utility.Create'ImageLabel' + { + Name = 'AvatarBrushImage'; + BorderSizePixel = 0; + BackgroundTransparency = 1; + ZIndex = 2; + Parent = AvatarBrushBackground; + Image = "rbxasset://textures/ui/Shell/Icons/CustomizeIcon@1080.png"; + Size = UDim2.new(0,47,0,48); + }; + local AvatarTextLabel = Utility.Create'TextLabel' + { + Name = 'AvatarTextLabel'; + Text = Strings:LocalizedString('EditAvatarPhrase'); + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + BackgroundTransparency = 1; + ZIndex = 2; + Parent = AvatarBrushBackground; + }; + + ProfileButton.MouseButton1Click:connect(function() + SoundManager:Play('ButtonPress') + EventHub:dispatchEvent(EventHub.Notifications["NavigateToEquippedAvatar"]) + -- EventHub:dispatchEvent(EventHub.Notifications["OpenProfileDetail"], "AppHub"); + end) + + local function OnProfileButtonSelectionChanged() + local selectedObject = GuiService.SelectedCoreObject + local newBackgroundTransparency = selectedObject == ProfileButton and + GlobalSettings.AvatarBoxBackgroundSelectedTransparency or + GlobalSettings.AvatarBoxBackgroundDeselectedTransparency + local newTextTransparency = selectedObject == ProfileButton and + GlobalSettings.AvatarBoxTextSelectedTransparency or + GlobalSettings.AvatarBoxTextDeselectedTransparency + + Utility.PropertyTweener(AvatarBrushBackground, 'BackgroundTransparency', newBackgroundTransparency, newBackgroundTransparency, 0, nil, true) + Utility.PropertyTweener(AvatarTextLabel, 'TextTransparency', newTextTransparency, newTextTransparency, 0, nil, true) + Utility.PropertyTweener(AvatarBrushImage, 'ImageTransparency', newTextTransparency, newTextTransparency, 0, nil, true) + end + + local existingThumbnailLoader = nil + local function UpdateProfileInfo() + local playerName = XboxAppState.store:getState().XboxUser.gamertag + NameLabel.Text = playerName and playerName or '' + + local rbxuid = XboxAppState.store:getState().RobloxUser.rbxuid + + if rbxuid then + if existingThumbnailLoader then + existingThumbnailLoader:Cancel() + end + local thumbnailSize = ThumbnailLoader.AvatarSizes.Size352x352 + local thumbLoader = ThumbnailLoader:LoadAvatarThumbnailAsync(ProfileImage, rbxuid, + Enum.ThumbnailType.AvatarThumbnail, Enum.ThumbnailSize.Size352x352, true) + existingThumbnailLoader = thumbLoader + spawn(function() + thumbLoader:LoadAsync() + ProfileImage.ImageRectSize = Vector2.new(thumbnailSize.X, (PROFILE_IMAGE_SIZE.Y/PROFILE_IMAGE_SIZE.X) * thumbnailSize.X) + end) + end + end + UpdateProfileInfo() + + local FriendActivityContainer = Utility.Create'Frame' + { + Name = 'FriendActivityContainer'; + BackgroundTransparency = 1; + Parent = HomePaneContainer; + } + local FriendActivityTitle = Utility.Create'TextLabel' + { + Name = 'FriendActivityTitle'; + Text = Strings:LocalizedString('FriendActivityWord'); + Size = UDim2.new(1,0,0,50); + Position = UDim2.new(0,0,0,10); + TextXAlignment = 'Left'; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.SubHeaderSize; + BackgroundTransparency = 1; + Parent = FriendActivityContainer; + }; + + local NoFriendsOnlineMessage = Utility.Create'TextLabel' + { + Name = 'NoFriendsOnlineMessage'; + Text = Strings:LocalizedString('NoFriendsOnlinePhrase'); + Size = UDim2.new(0.9,0,1,-125); + Position = UDim2.new(0.05, 0, 0, 125); + TextYAlignment = 'Top'; + TextColor3 = GlobalSettings.GreyTextColor; + TextWrapped = true; + TextTransparency = GlobalSettings.FriendStatusTextTransparency; + Font = GlobalSettings.BoldFont; + FontSize = GlobalSettings.DescriptionSize; + BackgroundTransparency = 1; + Visible = false; + Parent = FriendActivityContainer; + }; + + local friendsScroller = ScrollingGridModule({SelectionMode = "Middle"; Dynamic = true}) + friendsScroller:SetSize(UDim2.new(1,0,1,-60)) + friendsScroller:SetRowColumnConstraint(1) + friendsScroller:SetScrollDirection(friendsScroller.Enum.ScrollDirection.Vertical) + friendsScroller:SetCellSize(Vector2.new(446,114)) + friendsScroller:SetSpacing(Vector2.new(0, 26)) + friendsScroller:SetPosition(UDim2.new(0,0,0,FriendActivityTitle.Position.Y.Offset + FriendActivityTitle.Size.Y.Offset)) + local friendScrollerContainer = friendsScroller:GetGuiObject() + friendScrollerContainer.Visible = false + friendsScroller:SetParent(FriendActivityContainer) + + local function onFriendsUpdated(friendCount) + NoFriendsOnlineMessage.Visible = friendCount == 0 + friendScrollerContainer.Visible = friendCount > 0 + end + + local function setFriendItems() + local friendsData = FriendsData.GetOnlineFriendsAsync() + FriendsView(friendsScroller, friendsData, nil, onFriendsUpdated) + onFriendsUpdated(#friendsData) + end + + local friendsLoader = LoadingWidget( + { Parent = FriendActivityContainer }, { setFriendItems } ) + + -- Don't Block + spawn(function() + friendsLoader:AwaitFinished() + friendsLoader:Cleanup() + end) + + local SortsContainer = Utility.Create'Frame' + { + Name = 'SortsContainer'; + BackgroundTransparency = 1; + Parent = HomePaneContainer; + } + + local populateSortsDebounce = false + local function PopulateSorts() + if populateSortsDebounce then return end + populateSortsDebounce = true + local function loadGameSorts() + while #sortsObjects > 0 do + local sortObject = table.remove(sortsObjects) + if sortObject then + if GuiService.SelectedCoreObject and GuiService.SelectedCoreObject:IsDescendantOf(sortObject:GetContainer()) then + if inFocus then + Utility.SetSelectedCoreObject(this:GetDefaultSelectionObject()) + end + end + sortObject:Destroy() + end + end + + local favoriteGamesSort = SortsData:GetUserFavorites() + local favoritesPage1 = favoriteGamesSort:GetSortAsync(0, 4) + + local recentGamesSort = SortsData:GetUserRecent() + local recentlyPage1 = recentGamesSort:GetSortAsync(0, 4) + + local showRecent = recentlyPage1 and #recentlyPage1 > 2 + local showFavorites = favoritesPage1 and #favoritesPage1 > 2 + + local function setGridView(view, page, title, collection) + if page then + view:SetPlaceIds(page, title, collection) + end + view:SetTitle(title) + view:SetVisible(false) + view:SetParent(SortsContainer) + table.insert(sortsObjects, view) + end + + local function createMainSortGrid(title, page, collection) + local view = GameSort:CreateMainGridView(UDim2.new(0, 378, 1, 0), Vector2.new(10, 10), + UDim2.new(1, 0, 0, 378), UDim2.new(0, 184, 0, 184)) + setGridView(view, page, title, collection) + + return view + end + local function createGridSortView(size, imageSize, spacing, rows, columns, page, title, collection) + local view = GameSort:CreateGridView(size, imageSize, spacing, rows, columns) + setGridView(view, page, title, collection) + + return view + end + + local currentPosition = UDim2.new(0, 0, 0, 0) + local margin = (showFavorites and showRecent) and 50 or 28 + + if showFavorites then + local view = createMainSortGrid(Strings:LocalizedString('FavoritesSortTitle'), favoritesPage1, favoriteGamesSort) + view:SetPosition(currentPosition) + currentPosition = currentPosition + UDim2.new(0, view:GetContainer().Size.X.Offset + margin, 0, 0) + end + if showRecent then + local view = createMainSortGrid(Strings:LocalizedString('RecentlyPlayedSortTitle'), recentlyPage1, recentGamesSort) + view:SetPosition(currentPosition) + currentPosition = currentPosition + UDim2.new(0, view:GetContainer().Size.X.Offset + margin, 0, 0) + end + + local featuredGamesSort = SortsData:GetSort(SortsData.DefaultSortId.Featured) + if featuredGamesSort then + --Currently, assume SortsData.DefaultSortId.Featured corresponds to the feature sort Id + local featuredTitle = Strings:LocalizedString('FeaturedTitle') + local recommendedView; + if showRecent and showFavorites then + local page = featuredGamesSort:GetSortAsync(0, 4) + recommendedView = createMainSortGrid(featuredTitle, page, featuredGamesSort) + elseif showRecent or showFavorites then + local page = featuredGamesSort:GetSortAsync(0, 7) + recommendedView = createGridSortView(UDim2.new(0, 858, 1, 0), UDim2.new(0, 276, 0, 276), Vector2.new(15, 20), + 2, 3, page, featuredTitle, featuredGamesSort) + else + local page = featuredGamesSort:GetSortAsync(0, 9) + recommendedView = createGridSortView(UDim2.new(0, 1236, 0, 648), UDim2.new(0, 300, 0, 300), Vector2.new(12, 12), + 2, 4, page, featuredTitle, featuredGamesSort) + end + recommendedView:SetPosition(currentPosition) + end + end + local loader = LoadingWidget( + {Parent = SortsContainer}, {loadGameSorts}) + spawn(function() + loader:AwaitFinished() + loader:Cleanup() + loader = nil + populateSortsDebounce = false + for i = 1, #sortsObjects do + sortsObjects[i]:SetVisible(true) + end + end) + end + + local function UpdateLayout() + local guiRootSize = GuiRoot.AbsoluteSize + ProfileContainer.Size = Utility.CalculateRelativeDimensions(ProfileContainer, PROFILE_SIZE, MOCKUP_SIZE, guiRootSize) + + NameLabel.Size = Utility.CalculateRelativeDimensions(NameLabel, PROFILE_NAME_SIZE, MOCKUP_SIZE, guiRootSize) + + ProfileButton.Size = Utility.CalculateRelativeDimensions(ProfileButton, PROFILE_BUTTON_SIZE, MOCKUP_SIZE, guiRootSize) + ProfileButton.AnchorPoint = Vector2.new(0, 1) + ProfileButton.Position = UDim2.new(0, 0, 1, -6) + + + ProfileImage.Size = Utility.CalculateRelativeDimensions(ProfileImage, PROFILE_IMAGE_SIZE, MOCKUP_SIZE, guiRootSize) + AvatarBrushBackground.Size = UDim2.new(1 - ProfileImage.Size.X.Scale, -ProfileImage.Size.X.Offset, 1, 0) + AvatarBrushBackground.Position = UDim2.new(1 - AvatarBrushBackground.Size.X.Scale, AvatarBrushBackground.Size.X.Offset, 0, 0) + + AvatarBrushImage.AnchorPoint = Vector2.new(0.5, 0.5) + AvatarBrushImage.Position = UDim2.new(0.5, 0, 0.42, 0) + + AvatarTextLabel.AnchorPoint = Vector2.new(0.5, 0.5) + AvatarTextLabel.Position = UDim2.new(0.5, 0, 0.7, 0) + + + FriendActivityContainer.Size = UDim2.new(ProfileContainer.Size.X.Scale, 0, 0, 300); + FriendActivityContainer.Position = UDim2.new(0,0,ProfileContainer.Size.Y.Scale,0); + + SortsContainer.Size = Utility.CalculateRelativeDimensions(SortsContainer, SORTS_SIZE, MOCKUP_SIZE, guiRootSize) + SortsContainer.Position = UDim2.new(1 - SortsContainer.Size.X.Scale, 0, 0, 0) + end + + UpdateLayout() + + local screenResolutionChangedConn = nil + local profileButtonSelectedConn = nil + local profileButtonDeselectedConn = nil + + function this:GetName() + return Strings:LocalizedString('HomeWord') + end + + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('HomePaneId')} + end + + function this:IsFocused() + return inFocus + end + + function this:GetDefaultSelectionObject() + return ProfileButton + end + + function this:SetSelectionObject() + Utility.SetSelectedCoreObject(self:GetDefaultSelectionObject()) + -- TODO: Remember selection while staying in Pane. Pressing A on the Home tab should remember + -- last selection only while in pane. + end + + function this:Show(fromAppHub) + HomePaneContainer.Visible = true + PopulateSorts() + UpdateLayout() + + Utility.DisconnectEvent(screenResolutionChangedConn) + screenResolutionChangedConn = GuiRoot:GetPropertyChangedSignal('AbsoluteSize'):connect(function() + RunService.RenderStepped:wait() + UpdateLayout() + end) + + Utility.DisconnectEvent(profileButtonSelectedConn) + Utility.DisconnectEvent(profileButtonDeselectedConn) + profileButtonSelectedConn = ProfileButton.SelectionGained:connect(OnProfileButtonSelectionChanged) + profileButtonDeselectedConn = ProfileButton.SelectionLost:connect(OnProfileButtonSelectionChanged) + OnProfileButtonSelectionChanged() + UpdateProfileInfo() + + ScreenManager:DefaultCancelFade(self.TransitionTweens) + self.TransitionTweens = ScreenManager:DefaultFadeIn(HomePaneContainer) + delay(0.5, function() + if inFocus and GuiService.SelectedCoreObject == nil then + self:SetSelectionObject() + end + end) + + if XboxRecommendedPeople then + --Suspend Friends BG Update whenever we are on GamesPane + FriendsData:SuspendUpdate() + end + ScreenManager:PlayDefaultOpenSound() + end + + function this:Hide(fromAppHub) + HomePaneContainer.Visible = false + + profileButtonSelectedConn = Utility.DisconnectEvent(profileButtonSelectedConn) + profileButtonDeselectedConn = Utility.DisconnectEvent(profileButtonDeselectedConn) + screenResolutionChangedConn = Utility.DisconnectEvent(screenResolutionChangedConn) + + ScreenManager:DefaultCancelFade(self.TransitionTweens) + self.TransitionTweens = nil + + if XboxRecommendedPeople then + if fromAppHub then + --We resume Friends Update only if we navigate to other tabs + FriendsData:ResumeUpdate() + end + end + end + + function this:Focus() + inFocus = true + UpdateLayout() + self:SetSelectionObject() + OnProfileButtonSelectionChanged() + end + + function this:RemoveFocus() + inFocus = false + local selectedObject = GuiService.SelectedCoreObject + if selectedObject and selectedObject:IsDescendantOf(HomePaneContainer) then + Utility.SetSelectedCoreObject(nil) + end + end + + function this:SetPosition(newPosition) + HomePaneContainer.Position = newPosition + end + + function this:SetParent(newParent) + HomePaneContainer.Parent = newParent + end + + function this:IsAncestorOf(object) + return HomePaneContainer and HomePaneContainer:IsAncestorOf(object) + end + + return this +end + +return CreateHomePane diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Http.lua b/Client2018/content/internal/AppShell/Modules/Shell/Http.lua new file mode 100644 index 0000000..2a6c43a --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Http.lua @@ -0,0 +1,1015 @@ +--[[ + // Http.lua + // API for all web endpoints + // Calls are async + + // Any calls to roblox.com need to use game:HttpGetAynsc() and game:HttpPostAynsc() + // use rbxGetAsync() and rbxPostAsync() + // Any calls to api.roblox.com should use HttpRbxApiService + // use rbxApiGetAsync() and rbxApiPostAsync() + // For calls to new services like avatar.roblox.com use game:HttpGetAsync() + // user rbxGetAsync() and rbxPostAsync() + + // NOTE: You cannot currently get thumbnails with this API (please see the Thumbnail module), because + // Roblox GUIs cannot accept rbxcnd.com for the Image property. + + // NOTE: game:HttpGetAsync() can no longer be used in networked DataModels. + // You may need to move endpoint call to C++ if you need to use an + // endpoint in-game +]] +local HttpService = game:GetService("HttpService") +local HttpRbxApiService = game:GetService("HttpRbxApiService") + +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:FindFirstChild("RobloxGui") +local Modules = RobloxGui:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local IsNewUsernameCheckEnabled = settings():GetFFlag("XboxUseNewValidUsernameCheck2") + +local Http = {} + +local BaseUrl = game:GetService("ContentProvider").BaseUrl:lower() +BaseUrl = string.gsub(BaseUrl, "/m.", "/www.") +BaseUrl = string.gsub(BaseUrl, "http://", "https://") + +local AssetGameBaseUrl = string.gsub(BaseUrl, "https://www.", "https://assetgame.") +local AccountSettingsUrl = string.gsub(BaseUrl, "https://www.", "https://accountsettings.") + +Http.BaseUrl = BaseUrl +Http.AssetGameBaseUrl = AssetGameBaseUrl + +-- Helper Functions +local function decodeJSON(json) + if json == nil or #json == 0 then + return nil + end + + local success, result = pcall(function() + return HttpService:JSONDecode(json) + end) + if not success then + Utility.DebugLog("decodeJSON() failed because", result, "Input:", json) + return nil + end + + return result +end + +local function rbxGetAsync(path, returnRaw) + local success, result = pcall(function() + return game:HttpGetAsync(path) + end) + -- + if not success then + Utility.DebugLog(path, "rbxGetAsync() failed because", result) + return nil + end + + if returnRaw then + return result + end + return decodeJSON(result) +end + +local function rbxPostAsync(path, params, contentType) + local success, result = pcall(function() + return game:HttpPostAsync(path, params, contentType) + end) + -- + if not success then + Utility.DebugLog(path, "rbxPostAsync() failed because", result) + return nil + end + + return decodeJSON(result) +end + +local function rbxApiGetAsync(path) + local success, result = pcall(function() + return HttpRbxApiService:GetAsync(path) + end) + -- + if not success then + Utility.DebugLog(path, "rbxApiGetAsync() failed because", result) + return nil + end + + return decodeJSON(result) +end + +local function rbxApiPostAsync(path, params, throttlePriority, contentType) + local success, result = pcall(function() + return HttpRbxApiService:PostAsync(path, params, throttlePriority, contentType) + end) + -- + if not success then + Utility.DebugLog(path..params, "rbxApiPostAsync() failed because", result) + return nil + end + + return decodeJSON(result) +end + +local function buildServiceUrl(service) + return string.gsub(BaseUrl, "https://www.", "https://"..service..".") +end + +-- Game Endpoints + +--[[ + Response json + [ + { + "Id": 0, + "Name": "string", + "TimeOptionsAvailable": false, + "DefaultTimeOption": 0, + "GenresOptionsAvailable": true, + "NumberOfRows": 0, + "GameSetTargetId": 0 + }, + ] +]] +function Http.GetGameSortsAsync() + return rbxGetAsync(BaseUrl.."games/default-sorts") +end + +--[[ + Respone json + { + "data": [ + { + "CreatorID": 0, + "CreatorName": "string", + "CreatorUrl": "string", + "Plays": 0, + "Price": 0, + "ProductID": 0, + "IsOwned": false, + "IsVotingEnabled": true, + "TotalUpVotes": 0, + "TotalDownVotes": 0, + "TotalBought": 0, + "UniverseID": 0, + "HasErrorOcurred":false, + "GameDetailReferralUrl": "string", + "Url": "string", + "RetryUrl": "string", + "Final": true, + "Name": "string", + "PlaceID": 0, + "PlayerCount": 0, + "ImageId": 0, + "IsSecure": false, + "ShowExperimentalMode": false + }, + ] + "paging" { + "previousCursor": "string", + "previousUrl": "string", + "nextCursor": "string", + "nextUrl": "string" + } + } +]] +function Http.GetCuratedSortAsync(gameSetTargetId, maxRows) + local path = string.format("%sgames/set?GameSetTargetId=%d&MaxRows=%d", BaseUrl, gameSetTargetId, maxRows) + return rbxGetAsync(path) +end + +-- the subsequent page request's maxRows depends on the first request (Http.GetCuratedSortAsync) +-- the maxRows info is included in pageUrl +-- this has the same response as Http.GetCuratedSortAsync +function Http.GetCuratedSortByUrlAsync(pageUrl) + return rbxGetAsync(pageUrl) +end + +--[[ + Response json + [ + { + "CreatorID": 0, + "CreatorName": "string", + "CreatorUrl": "string", + "Plays": 0, + "Price": 0, + "ProductID": 0, + "IsOwned": false, + "IsVotingEnabled": true, + "TotalUpVotes": 0, + "TotalDownVotes": 0, + "TotalBought": 0, + "UniverseID": 0, + "HasErrorOcurred": false, + "GameDetailReferralUrl": "string", + "Url": "string", + "RetryUrl": "string", + "Final": true, + "Name": "string", + "PlaceID": 0, + "PlayerCount": 0, + "ImageId": 0, + "IsSecure": false, + "ShowExperimentalMode": false + }, + ] +]] +function Http.GetSortAsync(startRows, maxRows, sortId, timeFilter) + local path = + string.format("%sgames/list-json?sortFilter=%d&StartRows=%d&MaxRows=%d&searchAllGames=false&filterByDeviceType=true", + BaseUrl, sortId, startRows, maxRows) + + if timeFilter then + path = string.format("%s&timeFilter=%d", path, timeFilter) + end + + return rbxGetAsync(path) +end + +-- Response, see Http.GetSortAsync +function Http.GetUserFavoritesAsync(startRows, maxRows) + local path = string.format("%sgames/moreresultsuncached-json?sortFilter=MyFavorite&StartRows=%d&".. + "MaxRows=%d&searchAllGames=true&filterByDeviceType=true", + BaseUrl, startRows, maxRows) + + return rbxGetAsync(path) +end + +-- Response, see Http.GetSortAsync +function Http.GetUserRecentAsync(startRows, maxRows) + local path = string.format("%sgames/moreresultsuncached-json?sortFilter=MyRecent&StartRows=%d&".. + "MaxRows=%d&searchAllGames=true&filterByDeviceType=true", + BaseUrl, startRows, maxRows) + + return rbxGetAsync(path) +end + +-- Response, see Http.GetSortAsync +function Http.GetUserPlacesAsync(startIndex, pageSize, userid) + local path = string.format("%sgames/list-users-games-json?userid=%d&startIndex=%d&pageSize=%d", + BaseUrl, userid, startIndex, pageSize) + + return rbxGetAsync(path) +end + +-- Response, see Http.GetSortAsync +function Http.SearchGamesAsync(startRows, maxRows, keyword) + local path = string.format("%sgames/list-json?keyword=%s&StartRows=%d&MaxRows=%d&filterByDeviceType=true", + BaseUrl, HttpService:UrlEncode(keyword), startRows, maxRows) + + return rbxGetAsync(path) +end + +--[[ + Response json + { + "AssetId": 0, + "Name": "string", + "Description": "string", + "Created": "string", + "Updated": "string", + "FavoritedCount": 0, + "Url": "string", + "ReportAbuseAbsoluteUrl": "string", + "IsFavoritedByUser": false, + "IsFavoritesUnavailable": false, + "UserCanManagePlace": false, + "VisitedCount": 0, + "MaxPlayers": 8, + "Builder": "string", + "BuilderId": 0, + "BuilderAbsoluteUrl": "string", + "IsPlayable": true, + "ReasonProhibited": "string", + "ReasonProhibitedMessage": "string", + "IsBuildersClubOnly": false, + "IsCopyingAllowed": true, + "BuildersClubOverlay": "string", + "PlayButtonType": "string", + "AssetGenre": "string", + "AssetGenreViewModel": { + "DisplayName": "string", + "Id": 0 + }, + "OnlineCount": 0, + "UniverseId": 0, + "UniverseRootPlaceId": 0, + "TotalUpVotes": 0, + "TotalDownVotes": 0, + "UserVote": true, + "OverridesDefaultAvatar": false, + "UsePortraitMode": false, + "IsExperimental": false, + "Price": 0 + } +]] +function Http.GetGameDetailsAsync(placeId) + return rbxGetAsync(BaseUrl.."places/api-get-details?assetId="..tostring(placeId)) +end + +--[[ + Response json + { + "Id": 0, + "PlaceId": 0, + "ImageId": 0, + "IconUrl": "string", + "IconFinal": true, + "WikiUrl": "string", + "ReleaseDate": "string", + "IconUpdateSuccess": true, + "IconUpdateMessage": "string" + } +]] +function Http.GetGameIconIdAsync(placeId) + return rbxGetAsync(BaseUrl.."places/icons/json?placeId="..tostring(placeId)) +end + +--[[ + Response json + { + "VotingModel":{ + "ShowVotes": true, + "UpVotes": 0, + "DownVotes": 0, + "CanVote": false, + "UserVote": false, + "HasVoted": false, + "ReasonForNotVoteable": "string" + }, + "ShowVotes": true + } +]] +function Http.GetGameVotesAsync(placeId) + return rbxGetAsync(BaseUrl.."PlaceItem/GameDetailsVotingPanelJson?placeId="..tostring(placeId)) +end + +--[[ + Response json + [ + { + "Creator":{ + "CreatorName": "string", + "CreatorTargetId": 0, + "CreatorType": 0 + }, + "GameName": "string", + "GameSeoUrl": "string", + "GameThumbnail": { + "AssetId": 0, + "AssetHash": null, + "AssetTypeId": 0, + "Url": "string", + "IsFinal": true + }, + "PlaceId": 0, + "ImageId": 0 + }, + ] +]] +function Http.GetRecommendedGamesAsync(currentPlaceId) + return rbxGetAsync(BaseUrl.."Games/GetRecommendedGamesJson?currentPlaceId="..tostring(currentPlaceId)) +end + +--[[ + Response json + { + "IsJpegThumbnailEnabled": true, + "PlaceId": 0, + "thumbnails": [ + { + "AssetId": 0, + "AssetHash":null, + "AssetTypeId": 0, + "Url": "string", + "IsFinal": true + } + ], + "thumbnailCount": 1, + "IsVideoAutoplayedOnReady": false, + "ShowYouTubeVideo": true, + "IsMobile": false, + "PlaceThumbnailsResources": { + "LabelNext": "string", + "LabelPrevious": "string", + "State": 0 + } + } +]] +function Http.GetGameThumbnailsAsync(placeId) + return rbxGetAsync(BaseUrl.."thumbnail/place-thumbnails?placeId="..tostring(placeId)) +end + +--[[ + Response json + { + "PlaceId": 0, + "GameBadges": [ + { + "BadgeAssetId": 0, + "CreatorId": 0, + "IsOwned": true, + "Rarity": 0, + "RarityName": "string", + "TotalAwarded": 0, + "TotalAwardedYesterday": 0, + "Created": "string", + "Updated": "string", + "AssetSeoUrl": "string", + "Thumbnail": { + "Final": false, + "Url": "string", + "RetryUrl": "string", + "UserId": 0, + "EndpointType": "string" + }, + "Name": "string", + "FormatName": "string", + "Description": "string", + "AssetRestrictionIcon": null + }, + ], + "GameBadgesResources": { + "HeadingGameBadges":"Game Badges", + "LabelRarityCakeWalk":"Cake Walk", + "LabelRarityChallenging":"Challenging", + "LabelRarityEasy":"Easy", + "LabelRarityExtreme":"Extreme", + "LabelRarityFreebie":"Freebie", + "LabelRarityHard":"Hard", + "LabelRarityImpossible":"Impossible", + "LabelRarityInsane":"Insane", + "LabelRarityModerate":"Moderate", + "LabelRarity":"Rarity", + "LabelSeeMore":"See More", + "LabelWonEver":"Won Ever", + "LabelWonYesterday":"Won Yesterday", + "State":0 + } + } +]] +-- TODO: Remove with FFlagXboxUpdateBadgeEndpoints +function Http.GetGameBadgeDataAsync(placeId) + return rbxGetAsync(BaseUrl.."badges/list-badges-for-place/json?placeId="..tostring(placeId)) +end + +--[[ + Response json + { + "previousPageCursor": "string", + "nextPageCursor": "string", + "data": [ + { + "id": 0, + "name": "string", + "description": "string", + "enabled": true, + "iconImageId": 0, + "created": "2018-07-05T17:45:05.868Z", + "updated": "2018-07-05T17:45:05.868Z", + "statistics": { + "pastDayAwardedCount": 0, + "awardedCount": 0, + "winRatePercentage": 0 + }, + "awardingUniverse": { + "id": 0, + "name": "string", + "rootPlaceId": 0 + } + } + ] + } +]] +function Http.GetBadgesForUniverseAsync(universeId, cursor) + local url = string.format("%sv1/universes/%d/badges?sortOrder=Asc&limit=100", buildServiceUrl("badges"), universeId) + + if cursor then + url = string.format(url.."&cursor=%s", cursor) + end + + return rbxGetAsync(url) +end + +--[[ + Response json + { + "data": [ + { + "badgeId": 0, + "awardedDate": "2018-07-05T17:52:25.648Z" + } + ] + } +]] +function Http.GetUserAwardedBadgesAsync(userId, badgeIdsTable) + local badgeIds = table.concat(badgeIdsTable, ",") + local url = string.format("%sv1/users/%d/badges/awarded-dates?badgeIds=%s", buildServiceUrl("badges"), userId, badgeIds) + return rbxGetAsync(url) +end + +--[[ + Response json + { + "success": true, + "message": "string" + } +]] +function Http.PostFavoriteToggleAsync(assetId) + return rbxPostAsync(BaseUrl.."favorite/toggle?assetID="..tostring(assetId), "favoriteToggle") +end + +--[[ + Response json + { + "Success": true, + "Message": "string", + "ModalType": "string", + "Model": { + "ShowVotes": true, + "UpVotes": 0, + "DownVotes": 0, + "CanVote": true, + "UserVote": true, + "HasVoted": false, + "ReasonForNotVoteable": "string" + } + } +]] +function Http.PostGameVoteAsync(assetId, status) + return rbxPostAsync(BaseUrl.."voting/vote?assetId="..tostring(assetId).."&vote="..tostring(status), "vote") +end + +-- Social + +--[[ + Response json - none +]] +function Http.RegisterAppPresence() + return rbxApiPostAsync("presence/register-app-presence", "", + Enum.ThrottlingPriority.Default, Enum.HttpContentType.ApplicationJson) +end + +--[[ + Response json + { + "userId": 0, + "isEnabled": true, + "created": "string", + "updated": "string" + } +]] +function Http.GetCrossplayEnabledStatusAsync() + return rbxApiGetAsync("user/CrossPlayStatus") +end + +--[[ + Response json + { + "success": true + } +]] +function Http.PostCrossplayStatusAsync(value) + return rbxApiPostAsync("user/CrossPlayStatus?isEnabled="..tostring(value), "") +end + +--[[ + Response json + { + { + "VisitorId": 0, + "GameId": "string", + "IsOnline": true, + "LastOnline": "string", + "LastLocation": "string", + "LocationType": 0, + "PlaceId": 0, + "UserName": "string" + }, + } +]] +function Http.GetOnlineFriendsAsync() + return rbxApiGetAsync("my/friendsonline") +end + +--[[ + Response json + { + "recommendedUsers": [ + { + "userId": 0, + "userName": "string", + "userProfilePageUrl": "string", + "userPresenceType": "Offline" + } + ] + } +]] +--Only get used in studio mode +function Http.GetRecommendedUsersndsAsync() + local url = string.format("%s/v1/recommended-users", buildServiceUrl("friends")) + return rbxGetAsync(url) +end + +--[[ + Response json + { + "userPresences": [ + { + "userPresenceType": "Offline", + "lastLocation": "string", + "placeId": 0, + "rootPlaceId": 0, + "gameId": "string", + "userId": 0, + "lastOnline": "2018-05-07T22:18:08.290Z" + } + ] + } +]] +--Only get used in studio mode +function Http.GetUsersPresenceAsync(userIds) + local userIdsModel = { + userIds = userIds, + } + userIdsModel = HttpService:JSONEncode(userIdsModel) + local url = string.format("%s/v1/presence/users", buildServiceUrl("presence")) + return rbxPostAsync(url, userIdsModel) +end + + +-- Account + +--[[ + Response json + { + "Robux": 0 + } +]] +function Http.GetPlatformUserBalanceAsync() + return rbxApiGetAsync("my/platform-currency-budget") +end + +--[[ + Response json + { + "robux": 0 + } +]] +function Http.GetTotalUserBalanceAsync() + return rbxApiGetAsync("currency/balance") +end + +--[[ + Response json + { + "IsValid": true + "Data": { + "TotalItems": null, + "Start": 0, + "End": 0, + "Page": 0, + "nextPageCursor": "string", + "previousPageCursor": "string", + "ItemsPerPage":0 , + "PageType": "string", + "Items": [ + { + "AssetRestrictionIcon": { + "TooltipText": "string", + "CssTag": "string", + "LoadAssetRestrictionIconCss": true, + "HasTooltip": false + }, + "Item":{ + "AssetId": 0, + "Name": "string", + "AbsoluteUrl": "string", + "AssetType": 0, + "AssetTypeFriendlyLabel": "string", + "Description": "string", + "Genres": "string", + "GearAttributes": "string", + "AssetCategory": 0, + "CurrentVersionId": 0, + "IsApproved": false, + "LastUpdated": "string", + "LastUpdatedBy": "string", + "AudioUrl": "string" + }, + "Creator":{ + "Id": 1, + "Name": "string", + "Type": 1, + "CreatorProfileLink": "string" + }, + "Product":{ + "Id": 0, + "PriceInRobux": 0, + "IsForSale": false, + "IsPublicDomain": true, + "IsResellable": false, + "IsLimited": false, + "IsLimitedUnique": false, + "SerialNumber": 0, + "IsRental": false, + "RentalDurationInHours": 0, + "BcRequirement": 0, + "TotalPrivateSales": 0, + "SellerId": 0, + "SellerName": "string", + "LowestPrivateSaleUserAssetId": 0, + "IsXboxExclusiveItem": false, + "OffsaleDeadline": "string", + "NoPriceText": "string", + "IsFree": true + }, + "PrivateServer": false, + "Thumbnail": { + "Final": true, + "Url": "string", + "RetryUrl": "string", + "IsApproved": false + }, + "UserItem":{ + "UserAsset": 0, + "IsItemOwned": false, + "ItemOwnedCount": 0, + "IsRentalExpired": false, + "IsItemCurrentlyRented": false, + "CanUserBuyItem": false, + "RentalExpireTime": "string", + "CanUserRentItem": false + } + } + ] + } + } +]] +function Http.GetUserOwnedPackagesAsync(userId, currentPage) + currentPage = currentPage or 1 + local packageAssetIdType = 32 + + return rbxGetAsync(BaseUrl.."users/inventory/list-json?userId="..tostring(userId).. + "&assetTypeId="..tostring(packageAssetIdType).."&pageNumber="..tostring(currentPage)) +end + +--[[ + Response json + { + "assetIds": [ + 0 + ] + } +]] +function Http.GetCurrentlyWearingAsync(userId) + local url = string.format("%s/v1/users/%d/currently-wearing", + buildServiceUrl("avatar"), userId) + + return rbxGetAsync(url) +end + +-- Avatar/Inventory + +--[[ + Response json + { + "assetIds:" [ + 0 + ] + } +]] +function Http.GetPackageAssetsAsync(packageId) + local url = string.format("%s/v1/packages/%d/assets", buildServiceUrl("inventory"), packageId) + return rbxGetAsync(url) +end + +--[[ + Respone json + { + "invalidAssets": [ + { + "id": 0, + "name": "string", + "assetType": { + "id": 0, + "name": "string" + } + } + ], + "invalidAssetIds": [ + 0, + ], + "success": true + } +]] +function Http.SetWearingAssetsAsync(assetIds) + local assetIdsModel = { + assetIds = assetIds, + } + assetIdsModel = HttpService:JSONEncode(assetIdsModel) + + local url = string.format("%s/v1/avatar/set-wearing-assets", buildServiceUrl("avatar")) + return rbxPostAsync(url, assetIdsModel) +end + +--[[ + Response json + { + "sl_translate": "string", + "AssetID": 0, + "AssetName": "string", + "AssetType": "string", + "AssetIsWearable": true, + "SellerName": "string", + "TransactionVerb": "string", + "Price": 0, + "Currency": 0, + "IsMultiPrivateSale": false + } +]] +function Http.PurchaseProductAsync(productId, expectedPrice, expectedSellerId, expectedCurrency) + local formattedUrl = string.format("%sAPI/Item.ashx?rqtype=purchase&productID=%d&expectedCurrency=".. + "%d&expectedPrice=%d&expectedSellerID=%d", + BaseUrl, productId, expectedCurrency, expectedPrice, expectedSellerId) + return rbxPostAsync(formattedUrl, "") +end + +--[[ + Response json + { + "Products": [ + { + "TargetId": 0, + "ProductType": "string", + "AssetId": 0, + "ProductId": 0, + "Name": "string", + "Description": "string", + "AssetTypeId": 0, + "Creator": { + "Id": 0, + "Name": "string", + "CreatorType": "string", + "CreatorTargetId": 0 + }, + "IconImageAssetId": 0, + "Created": "string", + "Updated": "string", + "PriceInRobux": 0, + "PriceInTickets": null, + "Sales": 0, + "IsNew": false, + "IsForSale": true, + "IsPublicDomain": false, + "IsLimited": false, + "IsLimitedUnique": false, + "Remaining": 0, + "MinimumMembershipLevel": 0, + "ContentRatingTypeId": 0 + }, + ] + } +]] +function Http.GetXboxProductsAsync(startIndex, count) + startIndex = startIndex or 0 + count = count or 100 + local url = string.format("xbox/catalog/contents?startIndex=%d&count=%d", startIndex, count) + return rbxApiGetAsync(url) +end + +--[[ + Response - redirects to the cdn url; game engine takes care of this +]] +function Http.GetThumbnailUrlForAsset(assetId, width, height) + width = width or 420 + height = height or 420 + + return AssetGameBaseUrl.."Thumbs/Asset.ashx?width="..tostring(width).."&height="..tostring(height).. + "&assetId="..tostring(assetId) +end + +--[[ + Response json + { + "Url": "string", + "Final": true, + "SubstitutionType": 0 + } +]] +function Http.GetAssetThumbnailFinalAsync(assetId, width, height, format) + return rbxGetAsync(AssetGameBaseUrl.."asset-thumbnail/json?assetId="..tostring(assetId).."&width="..tostring(width).. + "&height="..tostring(height).."&format="..format) +end + +--[[ + Response json + { + "Url": "string", + "Final": true, + "SubstitutionType": 0 + } +]] +function Http.GetAssetAvatarFinalAsync(userId, width, height, format) + local result = rbxGetAsync(BaseUrl..'avatar-thumbnail/json?userId='..tostring(userId)..'&width='..tostring(width).. + '&height='..tostring(height)..'&format='..format) + return result +end + +--[[ +]] +function Http.GetOutfitThumbnailFinalAsync(outfitId, width, height, format) + local requestData = {["userOutfitId"] = outfitId} + local requestDataJson = HttpService:JSONEncode(requestData) + local encodedRequestData = requestDataJson and HttpService:UrlEncode(requestDataJson) or "" + + local url = string.format("%savatar-thumbnails?params=%s", BaseUrl, encodedRequestData) + + local result = rbxGetAsync(url) + return result +end + +--[[ + Response json - none +]] +function Http.ReportAbuseAsync(reportingItemTypeName, reportingItemId, reportCategoryId, comment) + local jsonPostBody = { + reportingItemTypeName = reportingItemTypeName; + reportingItemId = tostring(reportingItemId); + reportCategoryId = tostring(reportCategoryId); + comment = comment; + } + local params = HttpService:JSONEncode(jsonPostBody) + if params then + return rbxApiPostAsync("moderation/reportabuse", params, + Enum.ThrottlingPriority.Default, Enum.HttpContentType.ApplicationJson) + end +end + +-- Achievements + +--[[ + Response json + { + "count": 1 + } +]] +function Http.GetConsecutiveDaysLoggedInAsync() + local url = string.format("/xbox/get-login-consecutive-days") + return rbxApiGetAsync(url) +end + +--[[ + Response json + { + "UserId": 0, + "TargetType": "string", + "PlatformTypeId": 0, + "VoteCount": 0 + } +]] +function Http.GetVoteCountAsync() + local url = string.format("/user/get-vote-count?targetType=Place") + return rbxApiGetAsync(url) +end + +-- Account Linking + +--[[ + Response - TODO +]] +function Http.IsValidUsername(username) + if not username then + return + end + + if IsNewUsernameCheckEnabled then + local url = string.format("%sv1/xbox/is-username-valid?username=%s", + AccountSettingsUrl, HttpService:UrlEncode(username)) + return rbxGetAsync(url) + else + local url = string.format("signup/is-username-valid?username=%s", HttpService:UrlEncode(username)) + return rbxApiGetAsync(url) + end +end + +--[[ + Response json + { + "IsValid": true, + "ErrorCode": 0, + "ErrorMessage": "string" + } +]] +function Http.IsValidPassword(username, password) + if not username or not password then + return + end + local url = string.format("signup/is-password-valid?username=%s&password=%s", + HttpService:UrlEncode(username), HttpService:UrlEncode(password)) + return rbxApiGetAsync(url) +end + +return Http diff --git a/Client2018/content/internal/AppShell/Modules/Shell/ImageOverlay.lua b/Client2018/content/internal/AppShell/Modules/Shell/ImageOverlay.lua new file mode 100644 index 0000000..5ce810c --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/ImageOverlay.lua @@ -0,0 +1,218 @@ +--[[ + // ImageOverlay.lua + // Creates an image overlay. Used with the game details page to see more thumbnails +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local GuiService = game:GetService('GuiService') +local ContextActionService = game:GetService("ContextActionService") + +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local createImageOverlay = function(thumbIds, selectedThumbIndex) + local this = {} + + local thumbCount = #thumbIds + if selectedThumbIndex < 1 or selectedThumbIndex > thumbCount then + Utility.DebugLog("ImageOverlay: Invalid index to selectedThumbIndex") + return + end + + local thumbnailImages = nil + local currentSelectedIndex = selectedThumbIndex + + local shield = Utility.Create'Frame' + { + Name = "Shield"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + BackgroundColor3 = Color3.new(0, 0, 0); + BorderSizePixel = 0; + } + local container = Utility.Create'Frame' + { + Name = "ImageOverlayContainer"; + Size = UDim2.new(1, 0, 0, 668); + Position = UDim2.new(0, 0, 0, 226); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.OverlayColor; + } + local dummySelectionImage = Utility.Create'TextButton' + { + Name = "DummySelectionImage"; + Size = UDim2.new(0, 0, 0, 0); + Visible = false; + } + local imageSelection = Utility.Create'Frame' + { + Name = "imageSelection"; + Size = UDim2.new(0, 1030, 0, 580); + Position = UDim2.new(0.5, -1030/2, 0.5, -580/2); + BackgroundTransparency = 1; + Selectable = true; + SelectionImageObject = dummySelectionImage; + Parent = container; + SoundManager:CreateSound('MoveSelection'); + } + local leftArrowImage = Utility.Create'ImageButton' + { + Name = "LeftArrowImage"; + Size = UDim2.new(0, 26, 0, 45); + Position = UDim2.new(0.5, imageSelection.Position.X.Offset - 18 - 75, 0.5, -45/2); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Settings/Slider/Left.png'; + SelectionImageObject = dummySelectionImage; + Parent = container; + SoundManager:CreateSound('MoveSelection'); + } + local rightArrowImage = leftArrowImage:Clone() + rightArrowImage.Name = "RightArrowImage" + rightArrowImage.Position = UDim2.new(0.5, imageSelection.Position.X.Offset + imageSelection.Size.X.Offset + 75, 0.5, -45/2) + rightArrowImage.Image = 'rbxasset://textures/ui/Settings/Slider/Right.png'; + rightArrowImage.Parent = container + + local selectedText = Utility.Create'TextLabel' + { + Name = "SelectedText"; + Size = UDim2.new(); + Position = UDim2.new(0.5, 0, 1, -24); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = ""; + Parent = container; + } + + local function createThumbImages() + if not thumbnailImages then + thumbnailImages = {} + for i = 1, #thumbIds do + local image = Utility.Create'ImageLabel' + { + Name = tostring(i); + Size = imageSelection.Size; + Position = imageSelection.Position; + BackgroundTransparency = 1; + Parent = container; + SoundManager:CreateSound('MoveSelection'); + } + local loader = ThumbnailLoader:Create(image, thumbIds[i], + ThumbnailLoader.Sizes.Large, ThumbnailLoader.AssetType.Icon, false) + spawn(function() + loader:LoadAsync(true, true, { ZIndex = image.ZIndex } ) + end) + thumbnailImages[i] = image + end + end + end + + local function setImagePositions(startIndex) + for i = 1, #thumbnailImages do + local image = thumbnailImages[i] + local xScale = i == startIndex and 0.5 or 1.5 + image.Position = UDim2.new(xScale, -image.Size.X.Offset / 2, 0.5, image.Position.Y.Offset) + end + end + + local function tweenImagePositions(currentIndex, nextIndex, direction) + if currentIndex == nextIndex then return end + local nextStartPosition = direction * 1.5 + local currentEndPosition = -nextStartPosition + -- + local currentImage = thumbnailImages[currentIndex] + local nextImage = thumbnailImages[nextIndex] + nextImage.Position = UDim2.new(nextStartPosition, -nextImage.Size.X.Offset / 2, 0.5, nextImage.Position.Y.Offset) + -- + Utility.SetSelectedCoreObject(imageSelection) + Utility.TweenPositionOrSet(currentImage, UDim2.new(currentEndPosition, -currentImage.Size.X.Offset / 2, 0.5, currentImage.Position.Y.Offset), + Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, 0.25, true) + Utility.TweenPositionOrSet(nextImage, UDim2.new(0.5, -nextImage.Size.X.Offset / 2, 0.5, nextImage.Position.Y.Offset), + Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, 0.25, true) + selectedText.Text = tostring(nextIndex)..'/'..tostring(#thumbnailImages) + end + + local function onArrowSelected(direction) + local nextIndex = nil + nextIndex = currentSelectedIndex + direction + if nextIndex < 1 then + nextIndex = thumbCount + elseif nextIndex > thumbCount then + nextIndex = 1 + end + if nextIndex then + tweenImagePositions(currentSelectedIndex, nextIndex, direction) + currentSelectedIndex = nextIndex + end + end + + --[[ Public API ]]-- + function this:GetAnalyticsInfo() + return + { + [Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('ImageOverlayId'); + Selected = tostring(currentSelectedIndex).."/"..tostring(thumbCount); + } + end + + function this:GetPriority() + return GlobalSettings.OverlayPriority + end + + function this:Show() + createThumbImages() + setImagePositions(currentSelectedIndex) + selectedText.Text = tostring(currentSelectedIndex).."/"..tostring(thumbCount) + if thumbCount == 1 then + leftArrowImage.Visible = false + rightArrowImage.Visible = false + end + + shield.Parent = ScreenManager:GetScreenGuiByPriority(self:GetPriority()) + container.Parent = shield.Parent + local shieldTweenIn = Utility.PropertyTweener(shield, "BackgroundTransparency", 1, 0.3, 0.25, Utility.EaseInOutQuad, nil) + end + + function this:Hide() + local shieldTweenOut = Utility.PropertyTweener(shield, "BackgroundTransparency", 0.3, 1, 0.25, Utility.EaseInOutQuad, true, function() + shield:Destroy() + end) + container:Destroy() + end + + function this:Focus() + ContextActionService:BindCoreAction("CloseImageOverlay", + function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + ScreenManager:CloseCurrent() + end + end, + false, Enum.KeyCode.ButtonB) + + leftArrowImage.SelectionGained:connect(function() + onArrowSelected(-1) + end) + rightArrowImage.SelectionGained:connect(function() + onArrowSelected(1) + end) + + GuiService:AddSelectionParent("ImageOverlay", container) + Utility.SetSelectedCoreObject(imageSelection) + end + + function this:RemoveFocus() + ContextActionService:UnbindCoreAction("CloseImageOverlay") + GuiService:RemoveSelectionGroup("ImageOverlay") + end + + return this +end + +return createImageOverlay diff --git a/Client2018/content/internal/AppShell/Modules/Shell/LinkAccountScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/LinkAccountScreen.lua new file mode 100644 index 0000000..45d16b9 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/LinkAccountScreen.lua @@ -0,0 +1,152 @@ +--[[ + // LinkAccountScreen.lua +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local ContextActionService = game:GetService('ContextActionService') +local GuiService = game:GetService('GuiService') + +local AccountManager = require(ShellModules:FindFirstChild('AccountManager')) +local BaseSignInScreen = require(ShellModules:FindFirstChild('BaseSignInScreen')) +local Errors = require(ShellModules:FindFirstChild('Errors')) +local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local function createLinkAccountScreen() + local this = BaseSignInScreen() + + this:SetTitle(Strings:LocalizedString("LinkAccountTitle")) + this:SetDescriptionText(Strings:LocalizedString("LinkAccountPhrase")) + + local ModalOverlay = Utility.Create'Frame' + { + Name = "ModalOverlay"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = GlobalSettings.ModalBackgroundTransparency; + BackgroundColor3 = GlobalSettings.ModalBackgroundColor; + BorderSizePixel = 0; + ZIndex = 4; + } + + local myUsername = nil + local myPassword = nil + + this.UsernameObject:SetDefaultText(Strings:LocalizedString("UsernameWord")) + this.UsernameObject:SetKeyboardTitle(Strings:LocalizedString("UsernameWord")) + local usernameChangedCn = nil + + this.PasswordObject:SetDefaultText(Strings:LocalizedString("PasswordWord")) + this.PasswordObject:SetKeyboardTitle(Strings:LocalizedString("PasswordWord")) + if not UserSettings().GameSettings:InStudioMode() then + this.PasswordObject:SetKeyboardType(Enum.XboxKeyBoardType.Password) + end + local passwordChangedCn = nil + + local function linkAccountAsync() + local linkResult = nil + local signInResult = nil + + local function doLinkAccountAsync() + linkResult = AccountManager:LinkAccountAsync(myUsername, myPassword) + + if linkResult == AccountManager.AuthResults.Success then + signInResult = AccountManager:LoginAsync() + end + end + + local loader = LoadingWidget( + { Parent = this.Container }, { doLinkAccountAsync }) + + -- set up full screen loader + ModalOverlay.Parent = GuiRoot + ContextActionService:BindCoreAction("BlockB", function() end, false, Enum.KeyCode.ButtonB) + local selectedObject = GuiService.SelectedCoreObject + Utility.SetSelectedCoreObject(nil) + + -- call loader + loader:AwaitFinished() + + -- clean up + loader:Cleanup() + loader = nil + Utility.SetSelectedCoreObject(selectedObject) + ContextActionService:UnbindCoreAction("BlockB") + ModalOverlay.Parent = nil + + if linkResult ~= AccountManager.AuthResults.Success then + local err = linkResult and Errors.Authentication[linkResult] or Errors.Default + ScreenManager:OpenScreen(ErrorOverlay(err), false) + else + if signInResult == AccountManager.AuthResults.Success then + ScreenManager:CloseCurrent() + EventHub:dispatchEvent(EventHub.Notifications["AuthenticationSuccess"]) + else + local err = signInResult and Errors.Authentication[signInResult] or Errors.Default + ScreenManager:OpenScreen(ErrorOverlay(err), false) + end + end + end + + local isSigningIn = false + this.SignInButton.MouseButton1Click:connect(function() + if isSigningIn then return end + isSigningIn = true + SoundManager:Play('ButtonPress') + if (myUsername and #myUsername > 0) and (myPassword and #myPassword > 0) then + linkAccountAsync() + else + local err = Errors.SignIn.NoUsernameOrPasswordEntered + ScreenManager:OpenScreen(ErrorOverlay(err), false) + end + isSigningIn = false + end) + + --[[ Public API ]]-- + --override + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('LinkAccountScreenId')} + end + + local baseFocus = this.Focus + function this:Focus() + baseFocus(self) + usernameChangedCn = this.UsernameObject.OnTextChanged:connect(function(text) + myUsername = text + if #myUsername > 0 then + Utility.SetSelectedCoreObject(this.PasswordSelection) + else + Utility.SetSelectedCoreObject(this.UsernameSelection) + end + end) + passwordChangedCn = this.PasswordObject.OnTextChanged:connect(function(text) + myPassword = text + if #myPassword > 0 then + Utility.SetSelectedCoreObject(this.SignInButton) + else + Utility.SetSelectedCoreObject(this.PasswordSelection) + end + end) + end + + --override + local baseRemoveFocus = this.RemoveFocus + function this:RemoveFocus() + baseRemoveFocus(self) + Utility.DisconnectEvent(usernameChangedCn) + Utility.DisconnectEvent(passwordChangedCn) + end + + return this +end + +return createLinkAccountScreen diff --git a/Client2018/content/internal/AppShell/Modules/Shell/LoadingWidget.lua b/Client2018/content/internal/AppShell/Modules/Shell/LoadingWidget.lua new file mode 100644 index 0000000..75db513 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/LoadingWidget.lua @@ -0,0 +1,102 @@ +--[[ + // LoadingWidget.lua + + // Created by Kip Turner + // Copyright Roblox 2015 +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local function CreateLoadingWidget(properties, loadingFunctions) + properties = properties or {} + loadingFunctions = loadingFunctions or {} + + local this = {} + + local completedFunctions = {} + local cancelled = false + local ImageTransparency = properties.ImageTransparency or 0 + local finishedConn = Utility.Signal() + + local loadIcon = Utility.Create'ImageLabel' + { + Name = "LoadIcon"; + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/LoadingSpinner@1080.png'; + Size = properties.Size or UDim2.new(0, 100, 0, 100); + ZIndex = properties.ZIndex or 7; + Parent = properties.Parent; + ImageTransparency = ImageTransparency; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = properties.Position or UDim2.new(0.5, 0, 0.5, 0); + } + + if properties.Visible == false then + loadIcon.Visible = false + end + + local function isLoadingComplete() + return #completedFunctions == #loadingFunctions + end + + function this:AwaitFinished() + if isLoadingComplete() then + return true + end + finishedConn:wait() + if cancelled then + return false + end + return true + end + + function this:Cleanup() + loadIcon.Parent = nil + loadIcon.ImageTransparency = 1 + loadIcon:Destroy() + cancelled = true + end + + function this:SetTransparency(transparency) + ImageTransparency = transparency + end + + function this:SetParent(parent) + loadIcon.Parent = parent + end + + function this:GetTransparency() + return ImageTransparency + end + + -- Run it! + for _, loadingFunction in pairs(loadingFunctions) do + spawn(function() + loadingFunction() + table.insert(completedFunctions, loadingFunction) + end) + end + + spawn(function() + local t = tick() + while not (cancelled or isLoadingComplete()) do + local now = tick() + local rotation = (now - t) * 360 + if loadIcon.Parent then + loadIcon.ImageTransparency = ImageTransparency + loadIcon.Rotation = loadIcon.Rotation + rotation + end + t = now + wait() + end + finishedConn.fire() + end) + + return this +end + +return CreateLoadingWidget diff --git a/Client2018/content/internal/AppShell/Modules/Shell/LocalizedStrings.lua b/Client2018/content/internal/AppShell/Modules/Shell/LocalizedStrings.lua new file mode 100644 index 0000000..504393d --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/LocalizedStrings.lua @@ -0,0 +1,51 @@ +-- Written by Kip Turner, Copyright Roblox 2015 +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local Utility = require(ShellModules:FindFirstChild('Utility')) +local enUS = require(Modules:FindFirstChild('en-US')) + +local defaultLocale = nil +local success = false +local LocaleId = nil +success, LocaleId = pcall(function() return game:GetService("LocalizationService").RobloxLocaleId end) + +local this = {} + +function this:GetLocale() + local localeModule = Modules:FindFirstChild(LocaleId) + if success and localeModule then + defaultLocale = require(localeModule) + else + -- we don't have a country code for the module, so find it by language + -- ex. we store Spanish as "es.lua" + local languageCode = nil + for lang in string.gmatch(LocaleId ,"([^-]+)") do + local module = Modules:FindFirstChild(lang) + if module then + defaultLocale = require(module) + break + end + end + end + + if defaultLocale == nil then + defaultLocale = enUS + end + + return defaultLocale +end + +function this:LocalizedString(stringKey) + local locale = defaultLocale and defaultLocale or self:GetLocale() + local result = locale and locale[stringKey] + if not result then + Utility.DebugLog("LocalizedString: Could not find string for:" , stringKey , "using locale:" , locale) + result = enUS[stringKey] or stringKey + end + return result +end + + +return this diff --git a/Client2018/content/internal/AppShell/Modules/Shell/LockedGameCarouselView.lua b/Client2018/content/internal/AppShell/Modules/Shell/LockedGameCarouselView.lua new file mode 100644 index 0000000..23c39ea --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/LockedGameCarouselView.lua @@ -0,0 +1,122 @@ + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) + +local function LockedGameCarouselView() + local this = {} + + local overlayFrames = {} + + local container = Utility.Create'Frame' + { + Name = "LockedViewContainer"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 0.2; + BackgroundColor3 = Color3.new(0.1, 0.1, 0.1); + BorderSizePixel = 0; + Selectable = false; + SoundManager:CreateSound('MoveSelection'); + } + local lockedTextLabel = Utility.Create'TextLabel' + { + Name = "LockedTextLabel"; + Size = UDim2.new(0.7, 0, 0.5, 0); + Position = UDim2.new(0.125, 0, 0.25, 0); + BackgroundTransparency = 1; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.ButtonSize; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Center; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = Strings:LocalizedString("UnlockGamesPhrase"); + TextWrapped = true; + Parent = container; + } + local lockIcon = Utility.Create'ImageLabel' + { + Name = "LockLabel"; + Position = UDim2.new(0, 50, 0.5, -70); + Size = UDim2.new(0, 116, 0, 140); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/LockIcon.png'; + Parent = container; + } + + --[[ Public API ]]-- + function this:SetParent(newParent) + container.Parent = newParent + end + + function this:SetSize(newSize) + container.Size = newSize + end + + function this:SetPosition(newPosition) + container.Position = newPosition + end + + function this:GetContainer() + return container + end + + function this:SetSelectionImageObject(guiObject) + container.SelectionImageObject = guiObject + end + + function setProperty(instance, propName, val) + pcall(function(i, p, v) i[p] = v end, instance, propName, val) + end + + local function setZIndexHelper(element, val) + if not element:IsA('GuiObject') then + return + end + + setProperty(element, "ZIndex", val) + + local children = element:GetChildren() + for i = 1, #children do + local child = children[i] + if child:IsA('GuiObject') then + setZIndexHelper(child, val) + end + end + end + + function this:SetZIndex(val) + setZIndexHelper(container, val) + end + + function this:GetSelection() + return container + end + + function this:CreateLockedIconOverlay(iconImage) + local overlayFrame = Utility.Create'Frame' + { + Name = "LockedOverlay"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = GlobalSettings.ModalBackgroundTransparency; + BackgroundColor3 = GlobalSettings.ModalBackgroundColor; + BorderSizePixel = 0; + Parent = iconImage; + } + table.insert(overlayFrames, overlayFrame) + end + function this:RemoveLockedIconOverlays() + for i = 1, #overlayFrames do + overlayFrames[i].Parent = nil + end + end + + return this +end + +return LockedGameCarouselView diff --git a/Client2018/content/internal/AppShell/Modules/Shell/LockedGameSortView.lua b/Client2018/content/internal/AppShell/Modules/Shell/LockedGameSortView.lua new file mode 100644 index 0000000..0940481 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/LockedGameSortView.lua @@ -0,0 +1,102 @@ +--[[ + // LockedGameSortView.lua + + // Creates a locked view for game sorts. This is for UGC. Sorts will remained in a + // locked state until the user plays 5 games. +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) + +local function createLockedGameSortView() + local this = {} + + local overlayFrames = {} + + local container = Utility.Create'Frame' + { + Name = "LockedViewContainer"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + BackgroundColor3 = GlobalSettings.ModalBackgroundColor; + BorderSizePixel = 0; + Selectable = true; + SoundManager:CreateSound('MoveSelection'); + } + local lockedTextLabel = Utility.Create'TextLabel' + { + Name = "LockedTextLabel"; + Size = UDim2.new(0.6, 0, 0.5, 0); + Position = UDim2.new(0.2, 0, 0.5, 8); + BackgroundTransparency = 1; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.ButtonSize; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = Strings:LocalizedString("UnlockGamesPhrase"); + TextWrapped = true; + Visible = false; + Parent = container; + } + local lockIcon = Utility.Create'ImageLabel' + { + Name = "LockLabel"; + Size = UDim2.new(0, 116, 0, 140); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/LockIcon.png'; + Visible = false; + Parent = container; + AnchorPoint = Vector2.new(0.5, 1); + Position = UDim2.new(0.5, 0, 0.5, -20); + } + + container.SelectionGained:connect(function() + Utility.PropertyTweener(container, "BackgroundTransparency", 1, GlobalSettings.ModalBackgroundTransparency, 0, + Utility.Linear, true, nil) + lockedTextLabel.Visible = true + lockIcon.Visible = true + end) + container.SelectionLost:connect(function() + Utility.PropertyTweener(container, "BackgroundTransparency", GlobalSettings.ModalBackgroundTransparency, 1, 0, + Utility.Linear, true, nil) + lockedTextLabel.Visible = false + lockIcon.Visible = false + end) + + --[[ Public API ]]-- + function this:SetParent(newParent) + container.Parent = newParent + end + function this:GetSelection() + return container + end + + function this:CreateLockedIconOverlay(iconImage) + local overlayFrame = Utility.Create'Frame' + { + Name = "LockedOverlay"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = GlobalSettings.ModalBackgroundTransparency; + BackgroundColor3 = GlobalSettings.ModalBackgroundColor; + BorderSizePixel = 0; + Parent = iconImage; + } + table.insert(overlayFrames, overlayFrame) + end + function this:RemoveLockedIconOverlays() + for i = 1, #overlayFrames do + overlayFrames[i].Parent = nil + end + end + + return this +end + +return createLockedGameSortView diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Models/ScreenItem.lua b/Client2018/content/internal/AppShell/Modules/Shell/Models/ScreenItem.lua new file mode 100644 index 0000000..707556f --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Models/ScreenItem.lua @@ -0,0 +1,21 @@ +local ScreenItem = {} + +ScreenItem.Priority = { + Default = 1, + Overlay = 2, + Elevated = 3, + Immediate = 4, +} + +function ScreenItem.new(id, priority, data) + local self = {} + + self.id = id + self.priority = priority + self.createdAt = tick() + self.data = data + + return self +end + +return ScreenItem diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Models/ScreenItem.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Models/ScreenItem.spec.lua new file mode 100644 index 0000000..e6a427f --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Models/ScreenItem.spec.lua @@ -0,0 +1,17 @@ +return function() + local ScreenItem = require(script.Parent.ScreenItem) + + it("should create without errors", function() + ScreenItem.new("foo", 1, {}) + end) + + it("should set fields without errors", function() + local screenItem = ScreenItem.new("foo", 1, {}) + + expect(screenItem).to.be.a("table") + expect(screenItem.id).to.equal("foo") + expect(screenItem.priority).to.equal(1) + expect(screenItem.data).to.be.a("table") + expect(screenItem.createdAt).to.be.a("number") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/NoActionOverlay.lua b/Client2018/content/internal/AppShell/Modules/Shell/NoActionOverlay.lua new file mode 100644 index 0000000..b0411bd --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/NoActionOverlay.lua @@ -0,0 +1,146 @@ +--[[ + // NoActionOverlay.lua + + // Creates an overlay where the user cannot take an action + // to remove. + + // This is used when we detect something wrong with input or the active user + // being lost +]] + +local DATAMODEL_TYPE = { + APP_SHELL = 0; + GAME = 1; +} + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local GuiService = game:GetService('GuiService') +local ContextActionService = game:GetService("ContextActionService") +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) + +local BaseOverlay = require(ShellModules:FindFirstChild('BaseOverlay')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local Strings = require(ShellModules.LocalizedStrings) + +local createNoActionOverlay = function(errorType) + local this = BaseOverlay() + + local title = errorType.Title + local message = errorType.Msg + --That how we distinguish error and alert, errors have a Code table entry while alerts have a Id table entry. + local errorCode = errorType.Code + local alertId = errorType.Id + + local iconImage = Utility.Create'ImageLabel' + { + Name = "IconImage"; + Size = UDim2.new(0, 416, 0, 416); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/AlertIcon.png'; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + } + this:SetImage(iconImage) + + local titleText = Utility.Create'TextLabel' + { + Name = "TitleText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, this.RightAlign, 0, 136); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.HeaderSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = title; + TextXAlignment = Enum.TextXAlignment.Left; + Parent = this.Container; + } + + local descriptionText = Utility.Create'TextLabel' + { + Name = "DescriptionText"; + Size = UDim2.new(0, 762, 0, 304); + Position = UDim2.new(0, this.RightAlign, 0, titleText.Position.Y.Offset + 62); + BackgroundTransparency = 1; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.TitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextWrapped = true; + Text = message; + Parent = this.Container; + } + if errorCode then + descriptionText.Text = string.format(Strings:LocalizedString('ErrorMessageAndCodePrase'), message, errorCode) + end + + -- Override + function this:GetPriority() + return GlobalSettings.ImmediatePriority + end + + --[[ Public API ]]-- + -- override + function this:GetAnalyticsInfo() + local analyticsInfo = {} + analyticsInfo[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('NoActionOverlayId') + analyticsInfo.Title = errorType.Title + if errorCode then + analyticsInfo.ErrorCode = errorCode + end + if alertId then + analyticsInfo.AlertId = alertId + end + return analyticsInfo + end + + function this:Focus() + -- DO NOTHING + end + function this:RemoveFocus() + -- DO NOTHING + end + + + local baseShow = this.Show + function this:Show() + ContextActionService:BindCoreAction("StopControllerInput", function() end, false, Enum.UserInputType.Gamepad1) + baseShow(self) + end + + local baseHide = this.Hide + function this:Hide() + baseHide(self) + ContextActionService:UnbindCoreAction("StopControllerInput") + end + + local baseFocus = this.Focus + function this:Focus() + baseFocus() + -- NOTE: This might want to be: + -- `not PlatformService or` + if PlatformService and PlatformService.DatamodelType == DATAMODEL_TYPE.APP_SHELL then + GuiService.SelectedCoreObject = nil + end + end + + -- Track Error and Alert + if errorCode then + Analytics.ReportCounter("Error-"..tostring(errorCode), 1) + Analytics.SetRBXEventStream("Error", {ErrorCode = errorCode}) + end + if alertId then + Analytics.ReportCounter("Alert-"..tostring(alertId), 1) + Analytics.SetRBXEventStream("Alert", {AlertId = alertId}) + end + return this +end + +return createNoActionOverlay diff --git a/Client2018/content/internal/AppShell/Modules/Shell/NotificationHandler.lua b/Client2018/content/internal/AppShell/Modules/Shell/NotificationHandler.lua new file mode 100644 index 0000000..547a833 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/NotificationHandler.lua @@ -0,0 +1,83 @@ +--[[ + // NotificationHandler.lua + + // Handles the display of in-app notifications to the users when certain issues are + // happening. For example, when we're doing an RCC release, we want to let the users + // know they may experience game join issues + + // TODO: Make this system more generic. Right now this is going to be bare bolts to get + // working for disabling parties for the update to the new party system. +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local TextService = game:GetService('TextService') + +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local function CreateNotificationHandler(parentContainer) + local this = {} + + local TEXT_PADDING = 4 + local MAX_LENGTH = parentContainer.AbsoluteSize.x * 0.5 + + local notificationContainer = Utility.Create'Frame' + { + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(1, 0, 0, 0); + BackgroundTransparency = 1; + Visible = false; + Parent = parentContainer; + } + + local notificationIcon = Utility.Create'ImageLabel' + { + Size = UDim2.new(0, 40, 0, 40); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/AlertIcon.png'; + Parent = notificationContainer; + } + + local notificationText = Utility.Create"TextLabel" + { + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, 52 + TEXT_PADDING, 0, 43/2); + BackgroundTransparency = 1; + Text = ""; + TextColor3 = GlobalSettings.WhiteTextColor; + FontSize = GlobalSettings.DescriptionSize; + Font = GlobalSettings.RegularFont; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Center; + Parent = notificationContainer; + } + + local function updateLayout() + local textSize = TextService:GetTextSize(notificationText.Text, Utility.ConvertFontSizeEnumToInt(notificationText.FontSize), notificationText.Font, Vector2.new(0, 0)) + textSize = math.min(textSize.x, MAX_LENGTH) + + local rightAlignOffset = textSize + notificationIcon.Size.X.Offset + TEXT_PADDING + notificationContainer.Position = UDim2.new(1, -rightAlignOffset, 0, notificationIcon.Size.Y.Offset/2) + end + + function this:SetVisible(value) + notificationContainer.Visible = value + end + + function this:SetText(newText) + if newText == notificationText.Text then + return + end + + notificationText.Text = newText + updateLayout() + end + + return this +end + +return CreateNotificationHandler diff --git a/Client2018/content/internal/AppShell/Modules/Shell/OrderedMap.lua b/Client2018/content/internal/AppShell/Modules/Shell/OrderedMap.lua new file mode 100644 index 0000000..aa2db46 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/OrderedMap.lua @@ -0,0 +1,314 @@ +--This is a copy from LuaChat OrderedMap, should keep in consistent with the one in LuaChat. +--TODO: Use the Immutable instead after it embeds the OrderedMap +local OrderedMap = {} + +OrderedMap.__index = OrderedMap + +--[[ + Create a new OrderedMap with the given ID function and sort predicates. + + getId might look like: + + function(object) + return object.id + end + + And the sortPredicate might look like: + + function(objectA, objectB) + return objectA.id < objectB.id + end + + The rest of the arguments are inserted into the OrderedMap as values. +]] +function OrderedMap.new(getId, sortPredicate, ...) + local self = { + keys = {}, + values = {}, + getId = getId, + sortPredicate = sortPredicate + } + + setmetatable(self, OrderedMap) + + OrderedMap._InsertInPlace(self, ...) + + return self +end + +--[[ + Gets the value in the map associated with the given ID. +]] +function OrderedMap:Get(id) + return self.values[id] +end + +--[[ + Gets the value in the map located at the given index. +]] +function OrderedMap:GetByIndex(index) + local id = self.keys[index] + + if id == nil then + return nil + end + + return self.values[id] +end + +--[[ + Gets the list of IDs that are present in the OrderedMap. +]] +function OrderedMap:GetIds() + return self.keys +end + +--[[ + Returns the number of elements in the OrderedMap. +]] +function OrderedMap:Length() + return #self.keys +end + +--[[ + Deletes the given keys from the map. + + This is an immutable operation, so the original map is not modified. + + Example: + + map = map:Delete("my-key", "another-key") +]] +function OrderedMap:Delete(...) + local new = OrderedMap.new(self.getId, self.sortPredicate) + + local len = select("#", ...) + + for key, value in pairs(self.values) do + new.values[key] = value + end + + for i = 1, len do + local key = select(i, ...) + + new.values[key] = nil + end + + for _, value in ipairs(self.keys) do + if new.values[value] ~= nil then + new.keys[#(new.keys)+1] = value + end + end + + return new +end + +--[[ + Inserts the given values into the map. + + This is an immutable operation, so the original map is not modified and a + new map is returned instead. + + Example: + + map = map:Insert("Hello", "World") +]] +function OrderedMap:Insert(...) + local new = self:_Copy() + + OrderedMap._InsertInPlace(new, ...) + + return new +end + +--[[ + Returns the first value in the OrderedMap. +]] +function OrderedMap:First() + if self.keys[1] then + return self:Get(self.keys[1]) + end +end + +--[[ + Returns the last value in the OrderedMap. +]] +function OrderedMap:Last() + local i = #self.keys + if self.keys[i] then + return self:Get(self.keys[i]) + end +end + +--[[ + Create an iterator to traverse the map front-to-back. + + Example: + + for id, item in map:CreateIterator() do + print(id, item) + end +]] +function OrderedMap:CreateIterator() + local i = 0 + local length = #self.keys + + -- Iterator function + return function() + i = i + 1 + if i <= length then + local key = self.keys[i] + return key, self.values[key], i + end + end +end + +--[[ + Create an iterator to traverse the map back-to-front. + + Example: + + for id, item in map:CreateReverseIterator() do + print(id, item) + end +]] +function OrderedMap:CreateReverseIterator() + local i = #self.keys + 1 + + -- Iterator function + return function() + i = i - 1 + if i > 0 then + local key = self.keys[i] + return key, self.values[key], i + end + end +end + +--[[ + Create a new OrderedMap, applying the given predicate to each element in + this OrderedMap. + + Analogous to 'map' on a list. + + Example: + + local doubled = map:Map(function(value, key) + return value * 2 + end) +]] +function OrderedMap:Map(predicate) + local new = OrderedMap.new(self.getId, self.sortPredicate) + + for key, value in self:CreateIterator() do + new:_InsertInPlaceUnsorted(predicate(value, key)) + end + + new:_Sort() + + return new +end + +--[[ + Merges two or more OrderedMap objects by combining their values. + + Values from the right-most arguments will overwrite values from the left. + + Example: + + local merged = OrderedMap.Merge(a, b, c) + + OR + + local merged = a:Merge(b, c) + + NOTE: This function is not guaranteed to create a new OrderedMap. +]] +function OrderedMap:Merge(...) + local new + + for i = 1, select("#", ...) do + local other = select(i, ...) + + if other:Length() > 0 then + if not new then + new = self:_Copy() + end + + for _, value in other:CreateIterator() do + new:_InsertInPlaceUnsorted(value) + end + end + end + + if new then + new:_Sort() + + return new + end + + return self +end + +--[[ + Internal method for creating a copy of this OrderedMap. +]] +function OrderedMap:_Copy() + local new = OrderedMap.new(self.getId, self.sortPredicate) + + for key, value in ipairs(self.keys) do + new.keys[key] = value + end + + for key, value in pairs(self.values) do + new.values[key] = value + end + + return new +end + +--[[ + Internal method for inserting a value without sorting the map. + + This means that the invariants that the map exposes will be broken until + the _Sort() method is called. +]] +function OrderedMap:_InsertInPlaceUnsorted(...) + local len = select("#", ...) + + for i = 1, len do + local item = select(i, ...) + local key = self.getId(item) + + if not self.values[key] then + table.insert(self.keys, key) + end + + self.values[key] = item + end +end + +--[[ + Sorts the map; used in cases where the map would become out-of-order when + using internal recommendations. +]] +function OrderedMap:_Sort() + table.sort(self.keys, function(keyA, keyB) + local a = self.values[keyA] + local b = self.values[keyB] + + return self.sortPredicate(a, b) + end) +end + +--[[ + Inserts the given values into the map in-place. + + This operation mutates the map; generally you should use Insert instead. +]] +function OrderedMap:_InsertInPlace(...) + self:_InsertInPlaceUnsorted(...) + self:_Sort() +end + +return OrderedMap \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/OrderedMap.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/OrderedMap.spec.lua new file mode 100644 index 0000000..40f61bd --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/OrderedMap.spec.lua @@ -0,0 +1,278 @@ +return function() + local OrderedMap = require(script.Parent.OrderedMap) + + local function getId(item) + return item + end + + local function compare(a, b) + return a < b + end + + describe("new", function() + it("should accept getId, a sort predicate and a list of items", function() + local map = OrderedMap.new(getId, compare, 3, 1, 2) + + expect(map).to.be.a("table") + expect(#map.keys).to.equal(3) + + for _, key in ipairs(map.keys) do + local value = map.values[key] + expect(value).to.equal(key) + end + end) + end) + + describe("Insert", function() + it("should insert elements in order", function() + local map = OrderedMap.new(getId, compare, 2) + + expect(map).to.be.a("table") + + map = map:Insert(1) + + expect(map).to.be.a("table") + + for _, key in ipairs(map.keys) do + local value = map.values[key] + expect(value).to.equal(key) + end + end) + end) + + describe("Delete", function() + it("should delete elements by key", function() + local map = OrderedMap.new(getId, compare, 1, 2, 3) + + expect(map).to.be.a("table") + + map = map:Delete(2) + + expect(map).to.be.a("table") + + for _, key in ipairs(map.keys) do + local value = map.values[key] + + expect(value).to.equal(key) + expect(value).never.to.equal(2) + end + end) + end) + + describe("First", function() + it("should return nil if no elements in the list", function() + local map = OrderedMap.new(getId, compare) + + expect(map).to.be.a("table") + + expect(map:First()).to.equal(nil) + end) + + it("should handle modifications", function() + local map = OrderedMap.new(getId, compare, 1, 2, 3) + + expect(map).to.be.a("table") + + expect(map:First()).to.equal(1) + + map = map:Delete(1) + + expect(map:First()).to.equal(2) + + map = map:Delete(2) + + expect(map:First()).to.equal(3) + + map = map:Delete(3) + + expect(map:First()).to.equal(nil) + + map = map:Insert(5) + + expect(map:First()).to.equal(5) + + map = map:Insert(1) + + expect(map:First()).to.equal(1) + end) + end) + + + describe("Last", function() + it("should return nil if no elements in the list", function() + local map = OrderedMap.new(getId, compare) + + expect(map).to.be.a("table") + + expect(map:Last()).to.equal(nil) + end) + + it("should handle modifications", function() + local map = OrderedMap.new(getId, compare, 1, 2, 3) + + expect(map).to.be.a("table") + + expect(map:Last()).to.equal(3) + + map = map:Delete(1) + + expect(map:Last()).to.equal(3) + + map = map:Delete(3) + + expect(map:Last()).to.equal(2) + + map = map:Delete(2) + + expect(map:Last()).to.equal(nil) + + map = map:Insert(5) + + expect(map:First()).to.equal(5) + end) + end) + + describe("CreateIterator", function() + it("should iterate elements in order", function() + local values = {"a", "b", "c", "d"} + local map = OrderedMap.new(getId, compare, unpack(values)) + + expect(map).to.be.ok() + + local lastIndex = 0 + + for key, value, index in map:CreateIterator() do + expect(key).to.equal(value) + expect(values[index]).to.equal(value) + + expect(index > lastIndex).to.equal(true) + lastIndex = index + end + end) + + it("should work on empty map", function() + local map = OrderedMap.new(getId, compare) + + expect(map).to.be.ok() + + for _, _ in map:CreateIterator() do + error("This should never be called!") + end + end) + end) + + describe("CreateReverseIterator", function() + it("should iterate elements in order", function() + local values = {"a", "b", "c", "d"} + local map = OrderedMap.new(getId, compare, unpack(values)) + + expect(map).to.be.ok() + + local lastIndex = math.huge + + for key, value, index in map:CreateReverseIterator() do + expect(key).to.equal(value) + expect(values[index]).to.equal(value) + + expect(index < lastIndex).to.equal(true) + lastIndex = index + end + end) + + it("should work on empty map", function() + local map = OrderedMap.new(getId, compare) + + expect(map).to.be.ok() + + for _, _ in map:CreateReverseIterator() do + error("This should never be called!") + end + end) + end) + + describe("Map", function() + it("should use the map predicate", function() + local map = OrderedMap.new(getId, compare, 1, 2, 3, 4) + + expect(map).to.be.ok() + + local result = map:Map(function(value, key) + expect(value).to.equal(key) + + return value * 2 + end) + + for index = 1, #result.keys do + local key = result.keys[index] + local value = result.values[key] + + expect(value).to.equal(index * 2) + end + end) + end) + + describe("Merge", function() + it("should merge one map", function() + local a = OrderedMap.new(getId, compare, 1, 2) + + expect(a).to.be.ok() + + local b = OrderedMap.Merge(a) + + -- This is an optimization + expect(b).to.equal(a) + end) + + it("should merge one map with multiple empty maps", function() + local a = OrderedMap.new(getId, compare, 1, 2) + local empty = OrderedMap.new(getId, compare) + + expect(a).to.be.ok() + expect(empty).to.be.ok() + + local b = OrderedMap.Merge(a, empty, empty, empty) + + -- This is a nifty optimization + expect(b).to.equal(a) + end) + + it("should merge values into a new map", function() + local a = OrderedMap.new(getId, compare, 1, 2) + local b = OrderedMap.new(getId, compare, 2, 3) + + expect(a).to.be.ok() + expect(b).to.be.ok() + + local c = OrderedMap.Merge(a, b) + + expect(c).to.never.equal(a) + expect(c).to.never.equal(b) + + expect(c:Length()).to.equal(3) + + for key, value, index in c:CreateIterator() do + expect(key).to.equal(value) + expect(key).to.equal(index) + end + end) + + it("should work with more than 2 arguments", function() + local a = OrderedMap.new(getId, compare, 1) + local b = OrderedMap.new(getId, compare, 2) + local c = OrderedMap.new(getId, compare, 3) + + local d = OrderedMap.Merge(a, b, c) + + expect(d).never.to.equal(a) + expect(d).never.to.equal(b) + expect(d).never.to.equal(c) + + expect(d:Length()).to.equal(3) + + for key, value, index in d:CreateIterator() do + expect(key).to.equal(value) + expect(key).to.equal(index) + end + end) + end) +end diff --git a/Client2018/content/internal/AppShell/Modules/Shell/OutfitData.lua b/Client2018/content/internal/AppShell/Modules/Shell/OutfitData.lua new file mode 100644 index 0000000..0f912b7 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/OutfitData.lua @@ -0,0 +1,144 @@ + +--[[ + // OutfitData.lua + + // Created by Kip Turner + // Copyright Roblox 2015 +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local UserData = require(ShellModules:FindFirstChild('UserData')) +local Http = require(ShellModules:FindFirstChild('Http')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local XboxAppState = require(ShellModules:FindFirstChild('AppState')) + +local OutfitData = {} + +local wearingOutfit = nil + +local function CreateOutfitItem(outfitInfo) + local this = {} + local isWearing = false + this.IsWearingChanged = Utility.Signal() + + function this:GetUserId() + return outfitInfo['UserId'] + end + + function this:GetOutfitId() + return outfitInfo['OutfitId'] + end + + function this:GetId() + return outfitInfo['Id'] + end + + function this:GetName() + return outfitInfo['Name'] + end + + function this:IsOwned() + return true + end + + function this:IsWearing() + local wasWearing = isWearing + isWearing = (self:GetId() == wearingOutfit) + if isWearing ~= wasWearing then + self.IsWearingChanged:fire(isWearing) + end + return isWearing + end + + function this:WearAsync() + Http.PostWearUserOutfitAsync(self:GetId()) + wearingOutfit = self:GetId() + EventHub:dispatchEvent(EventHub.Notifications["DonnedDifferentOutfit"], self:GetId()) + end + + return this +end + + +local OutfitCache = nil +local RbxUid = nil + +local debounceGetGetMyOutfitsAsync = false +function OutfitData:GetMyOutfitsAsync() + while debounceGetGetMyOutfitsAsync do wait() end + + debounceGetGetMyOutfitsAsync = true + UserData.GetLocalPlayerAsync() + + if RbxUid ~= XboxAppState.store:getState().RobloxUser.rbxuid then + OutfitCache = nil + end + + while not OutfitCache do + local startRbxUid = XboxAppState.store:getState().RobloxUser.rbxuid + local outfits = {} + local index = 0 + local count = 20 + + repeat + local result = nil + + Utility.ExponentialRepeat( + function() return result == nil end, + function() result = Http.GetMyUserOutfitsAsync(index, count) end, + 5) + + if result then + local userOutfits = result['UserOutfits'] + if userOutfits then + for _, outfitInfo in pairs(userOutfits) do + table.insert(outfits, CreateOutfitItem(outfitInfo)) + end + end + end + + index = index + count + until result == nil or result['FinalPage'] + + local nowRbxUid = XboxAppState.store:getState().RobloxUser.rbxuid + if startRbxUid == nowRbxUid then + OutfitCache = outfits + end + Utility.DebugLog("Getting info cache again" , "now" , nowRbxUid , "startRbxUid" , startRbxUid) + end + + debounceGetGetMyOutfitsAsync = false + return OutfitCache +end + +function OutfitData:GetCachedWearingOutfitId() + return wearingOutfit +end + +EventHub:addEventListener(EventHub.Notifications["DonnedDifferentPackage"], "OutfitData", +function() + wearingOutfit = nil + if OutfitCache then + for _, outfit in pairs(OutfitCache) do + outfit:IsWearing() + end + end +end) +EventHub:addEventListener(EventHub.Notifications["DonnedDifferentOutfit"], "OutfitData", +function() + if OutfitCache then + for _, outfit in pairs(OutfitCache) do + outfit:IsWearing() + end + end +end) + + +return OutfitData + + + diff --git a/Client2018/content/internal/AppShell/Modules/Shell/OutfitTile.lua b/Client2018/content/internal/AppShell/Modules/Shell/OutfitTile.lua new file mode 100644 index 0000000..76b8478 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/OutfitTile.lua @@ -0,0 +1,87 @@ +--[[ + // OutfitTile.lua + + // Created by Kip Turner + // Copyright Roblox 2015 +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local BaseTile = require(ShellModules:FindFirstChild('BaseTile')) + +local function createOutfitTileContainer(outfitData) + local this = BaseTile() + + local function wearOutfitAsync() + local result = outfitData:WearAsync() + end + + local thumbnailLoader = ThumbnailLoader:Create(this.AvatarImage, outfitData:GetOutfitId(), ThumbnailLoader.Sizes.Medium, ThumbnailLoader.AssetType.Outfit, true) + spawn(function() + thumbnailLoader:LoadAsync(false, true) + end) + + this:SetPopupText(outfitData:GetName()) + + function this:GetAnalyticsInfo() + return + { + [Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('OutfitTileId'); + AssetId = outfitData:GetOutfitId(); + } + end + + function this:UpdateEquipButton() + self.EquippedCheckmark.Visible = outfitData:IsWearing() + end + + function this:GetPackageInfo() + return outfitData + end + + local selectDebounce = false + function this:Select() + if selectDebounce then return false end + selectDebounce = true + spawn(function() + wearOutfitAsync() + selectDebounce = false + end) + return true + end + + + local isWearingConn = nil + local baseShow = this.Show + function this:Show() + baseShow(self) + self:SetActive(true) + Utility.DisconnectEvent(isWearingConn) + isWearingConn = outfitData.IsWearingChanged:connect(function() self:UpdateEquipButton() end) + end + + local baseHide = this.Hide + function this:Hide() + baseHide(self) + isWearingConn = Utility.DisconnectEvent(isWearingConn) + end + + local baseFocus = this.Focus + function this:Focus() + baseFocus(self) + end + + local baseRemoveFocus = this.RemoveFocus + function this:RemoveFocus() + baseRemoveFocus(self) + end + + return this +end + +return createOutfitTileContainer diff --git a/Client2018/content/internal/AppShell/Modules/Shell/PackageData.lua b/Client2018/content/internal/AppShell/Modules/Shell/PackageData.lua new file mode 100644 index 0000000..c2c853a --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/PackageData.lua @@ -0,0 +1,806 @@ +--[[ +// PackageData.lua + +// Created by Kip Turner, Bo Zhang +// Copyright Roblox 2017 +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local ContentProvider = game:GetService("ContentProvider") +local MarketplaceService = game:GetService('MarketplaceService') +local PlatformService = nil +pcall(function()PlatformService = game:GetService('PlatformService') end) +local ThirdPartyUserService = nil +pcall(function()ThirdPartyUserService = game:GetService('ThirdPartyUserService') end) + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Http = require(ShellModules:FindFirstChild('Http')) +local UserData = require(ShellModules:FindFirstChild('UserData')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local ReloaderManager = require(ShellModules:FindFirstChild('ReloaderManager')) +local CreateCacheData = require(ShellModules:FindFirstChild('CachedData')) +local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader')) +local XboxAppState = require(ShellModules:FindFirstChild('AppState')) + +local RequestingWearAsset = false +local function AwaitWearAssetRequest() + while RequestingWearAsset do wait(0.1) end +end + +local RequestingBuyAsset = false +local function AwaitBuyAssetRequest() + while RequestingBuyAsset do wait(0.1) end +end + +local function PreloadCharacterAppearanceAsync() + local character = nil + local success = pcall(function() + character = game.Players:GetCharacterAppearanceAsync(UserData:GetLocalUserIdAsync()) + end) + if character then + ContentProvider:PreloadAsync({ character }) + end + + return success +end + +--Hard code the assetId to productId map +local AvatarAssetId_XboxProductIdMap = +{ + ['807301633'] = '899a379d-0a66-4b07-8bcd-29b1e38699ba'; --Boy Avatar + ['807340263'] = '7dba5b02-02be-4442-814e-9b9ebf6d66bf'; --Girl Avatar +} + +local function CreatePackageItem(data) + local this = {} + + this.Owned = false + this.Wearing = false + this.OwnershipChanged = Utility.Signal() + this.IsWearingChanged = Utility.Signal() + + local productInfo = nil + local partIds = {} + + function this:GetAssetId() + local assetId = data and data['AssetId'] + if not assetId then + assetId = data and data['Item'] and data['Item']['AssetId'] + end + return tonumber(assetId) + end + + function this:OpenAvatarDetailInXboxStore() + local assetId = self:GetAssetId() + if PlatformService then + local XboxProductId = AvatarAssetId_XboxProductIdMap[tostring(assetId)] + if XboxProductId then + PlatformService:OpenProductDetail(XboxProductId) + end + end + end + + function this:IsXboxAddOn() + return AvatarAssetId_XboxProductIdMap[tostring(self:GetAssetId())] and true or false + end + + function this:GetProductIdAsync() + while not productInfo do wait() end + return productInfo and productInfo['ProductId'] + end + + function this:GetPartIdsAsync() + while not partIds do wait() end + return partIds + end + + function this:UpdatePartIdsAsync() + local assetId = self:GetAssetId() + if assetId then + while not partIds do wait() end + --Reset partIds + partIds = nil + + local response = Http.GetPackageAssetsAsync(assetId) + if response then + partIds = response["assetIds"] + end + + if partIds == nil then + partIds = {} + else + table.sort(partIds) + end + end + return partIds + end + + function this:IsWearing() + return self.Wearing + end + + function this:SetWearing(newWearing) + if newWearing ~= self.Wearing then + self.Wearing = newWearing + self.IsWearingChanged:fire(newWearing) + end + end + + + function this:BuyAsync() + EventHub:dispatchEvent(EventHub.Notifications["AvatarPurchaseBegin"], self:GetAssetId()) + + Utility.DebugLog("Do buy", 'productId', self:GetProductIdAsync(), 'robuxPrice', self:GetRobuxPrice()) + AwaitBuyAssetRequest() + + RequestingBuyAsset = true + local purchaseResult = Http.PurchaseProductAsync(self:GetProductIdAsync(), self:GetRobuxPrice(), self:GetCreatorId(), 1) + RequestingBuyAsset = false + + local nowOwns = purchaseResult and purchaseResult['TransactionVerb'] == 'bought' + if nowOwns then + EventHub:dispatchEvent(EventHub.Notifications["AvatarPurchaseSuccess"], self:GetAssetId(), nowOwns) + end + return purchaseResult + end + + function this:IsOwned() + return self.Owned + end + + function this:SetOwned(newOwned) + if self.Owned ~= newOwned then + self.Owned = newOwned + self.OwnershipChanged:fire(newOwned) + end + end + + function this:GetRobuxPrice() + local robuxPrice = data and data['PriceInRobux'] + if not robuxPrice then + robuxPrice = data and data['Product'] and data['Product']['PriceInRobux'] + end + local isPublicDomain = data and data['IsPublicDomain'] == true + if isPublicDomain == nil then + isPublicDomain = data and data['Product'] and data['Product']['IsPublicDomain'] == true + end + if not robuxPrice and isPublicDomain then + robuxPrice = 0 + end + + return robuxPrice + end + + function this:GetCreatorId() + return data and data['Creator'] and data['Creator']['Id'] + end + + function this:WearAsync() + local assetId = self:GetAssetId() + if assetId then + EventHub:dispatchEvent(EventHub.Notifications["AvatarEquipBegin"], assetId) + AwaitWearAssetRequest() + + RequestingWearAsset = true + + local result; + result = Http.SetWearingAssetsAsync(self:GetPartIdsAsync()) + + RequestingWearAsset = false + + if result and result['success'] == true then + EventHub:dispatchEvent(EventHub.Notifications["AvatarEquipSuccess"], assetId) + end + return result + end + end + + function this:GetName() + local resultPackageName = self:GetFullName() + + if resultPackageName then + local colonPosition = string.find(resultPackageName, ":") + if colonPosition then + resultPackageName = string.sub(resultPackageName, 1, colonPosition - 1) + end + else + resultPackageName = "Unknown" + end + + return resultPackageName + end + + function this:GetFullName() + local name = data and data['Name'] + if not name then + name = data and data['Item'] and data["Item"]['Name'] + end + return name or "Unknown" + end + + function this:GetDescriptionAsync() + while not productInfo do wait() end + return productInfo and productInfo['Description'] + end + + spawn(function() + productInfo = MarketplaceService:GetProductInfo(this:GetAssetId()) + if productInfo == nil then productInfo = {} end + end) + + --We get partIds when we create package + this:UpdatePartIdsAsync() + return this +end + +local PackageData = {} +do + --Cached Data + --InternalPackageDataCache only gets updated at intervals + local InternalPackageDataCache = nil + + --VisiblePackageDataCache gets updated by user operation + --and will be checked and updated when GetCachedData() is called + local VisiblePackageDataCache = nil + local ProfileImageThumbnailLoader = nil + local UpdateFuncId = nil + + --Start Image Update Times for wearing + local ProfileImageUpdateForWearing = 0 + + --Bool to check whether we update pane while updating caching data + local visiblePackageUpdating = false + + local function ResetCacheData() + InternalPackageDataCache = nil + VisiblePackageDataCache = nil + ProfileImageThumbnailLoader = nil + UpdateFuncId = nil + ProfileImageUpdateForWearing = 0 + visiblePackageUpdating = false + end + + local function deepcopy(orig) + local orig_type = type(orig) + local copy + if orig_type == 'table' then + copy = {} + for orig_key, orig_value in next, orig, nil do + copy[deepcopy(orig_key)] = deepcopy(orig_value) + end + setmetatable(copy, deepcopy(getmetatable(orig))) + else -- number, string, boolean, etc + copy = orig + end + return copy + end + + local UserChangedCount = 0 + --Add to cope with avatar purchase through Xbox Store + local ConsumePurchasedConn = nil + local function OnUserAccountChanged() + local startCount = UserChangedCount + ResetCacheData() + spawn(function() + if startCount == UserChangedCount then + spawn(PreloadCharacterAppearanceAsync) + local RefreshInterval = GlobalSettings.AvatarPaneRefreshInterval + + ReloaderManager:removeReloader("PackageData") + UpdateFuncId = ReloaderManager:addReloaderFunc("PackageData", PackageData.UpdateCachedDataAsync, RefreshInterval) + ReloaderManager:callReloaderFunc("PackageData", UpdateFuncId) + + Utility.DisconnectEvent(ConsumePurchasedConn) + if PlatformService then + ConsumePurchasedConn = PlatformService.ConsumePurchased:connect(function(platformPurchaseResult, purchasedConsumablesInfo) + if platformPurchaseResult == 3 then + spawn(function() + PackageData:UpdatePurchasedConsumablesAsync(purchasedConsumablesInfo) + end) + end + end) + end + end + end) + end + + EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "PackageData", OnUserAccountChanged) + + local function OnUserSignOut() + UserChangedCount = UserChangedCount + 1 + ReloaderManager:removeReloader("PackageData") + EventHub:removeEventListener(EventHub.Notifications["AvatarEquipBegin"], "VisiblePackageDataCache") + EventHub:removeEventListener(EventHub.Notifications["AvatarPurchaseBegin"], "VisiblePackageDataCache") + EventHub:removeEventListener(EventHub.Notifications["AvatarEquipSuccess"], "VisiblePackageDataCache") + EventHub:removeEventListener(EventHub.Notifications["AvatarPurchaseSuccess"], "VisiblePackageDataCache") + EventHub:removeEventListener(EventHub.Notifications["CharacterUpdated"], "VisiblePackageDataCache") + EventHub:removeEventListener(EventHub.Notifications["CharacterEquipped"], "VisiblePackageDataCache") + Utility.DisconnectEvent(ConsumePurchasedConn) + end + + if ThirdPartyUserService then + ThirdPartyUserService.ActiveUserSignedOut:connect(OnUserSignOut) + end + + + local function GetAvailableXboxCatalogPackagesAsync() + local isFinalPage = false + + local packages = {} + local index = 0 + local count = 100 + + repeat + local result = nil + + Utility.ExponentialRepeat( + function() return result == nil end, + function()result = Http.GetXboxProductsAsync(index, count) end, + 2) + + if result then + local items = result['Products'] + if items then + if #items < count then + isFinalPage = true + end + for _, itemInfo in pairs(items) do + table.insert(packages, itemInfo) + end + end + end + + index = index + count + until result == nil or isFinalPage + if isFinalPage then + return packages + end + end + + local function GetOwnedCatalogPackageIdsByUserAsync(userId) + local packages = Http.GetUserOwnedPackagesAsync(userId) + if packages then + local data = packages['IsValid'] and packages['Data'] + local items = data and data['Items'] + local result = {} + if items then + for _, itemInfo in pairs(items) do + local assetId = itemInfo and itemInfo['Item'] and itemInfo['Item']['AssetId'] + result[assetId] = itemInfo + end + end + return result + end + end + + + local function getCatalogPackagesAsync() + local xboxCatalogPackages = GetAvailableXboxCatalogPackagesAsync() + local myPackages = GetOwnedCatalogPackageIdsByUserAsync(XboxAppState.store:getState().RobloxUser.rbxuid) + + if xboxCatalogPackages and myPackages then + local result = {} + result.Packages = {} + result.OwnedInfo = {} + + --We use array to store xboxCatalogPackages as the order is important + --and then use result.OwnedInfo to store OwnedInfo to avoid duplicate with myPackages + for _, packageInfo in pairs(xboxCatalogPackages) do + local package = CreatePackageItem(packageInfo) + local assetId = package:GetAssetId() + local owned = (myPackages[assetId] ~= nil) + result.OwnedInfo[assetId] = owned + table.insert(result.Packages, package) + end + + for assetId, packageInfo in pairs(myPackages) do + if result.OwnedInfo[assetId] == nil then + local package = CreatePackageItem(packageInfo) + result.OwnedInfo[assetId] = true + table.insert(result.Packages, package) + end + end + return result + end + end + + + --Get new packages and owned info + function PackageData:GetPackagesAndOwnedInfoAsync() + local startCount = UserChangedCount + local result = nil + + Utility.ExponentialRepeat( + function() return result == nil and startCount == UserChangedCount end, + function()result = getCatalogPackagesAsync() end, + 3) + + if startCount ~= UserChangedCount then + result = nil + end + + return result + end + + --Utility function to compare arrays + local function CompareAssetIdArrays(arr1, arr2) + if type(arr1) == 'table' and type(arr2) == 'table' then + if #arr1 == #arr2 then + for i = 1, #arr1 do + if tonumber(arr1[i]) ~= tonumber(arr2[i]) then + return false + end + end + return true + end + end + + return false + end + + --Get WearingAssetId from packages + function PackageData:GetWearingPackageAssetIdAsync(packages) + local startCount = UserChangedCount + local newWearingAssetId = nil + + local rbxuid = XboxAppState.store:getState().RobloxUser.rbxuid + if rbxuid then + if packages then + local response = Http.GetCurrentlyWearingAsync(rbxuid) + + if not response then + return + end + + local wornAssetIds = response["assetIds"] + + for _, package in pairs(packages) do + if CompareAssetIdArrays(package:GetPartIdsAsync(), wornAssetIds) then + newWearingAssetId = tonumber(package:GetAssetId()) + break + end + end + end + end + + if startCount ~= UserChangedCount then + newWearingAssetId = nil + end + + return newWearingAssetId + end + + function PackageData:GetProfileImageAsync() + local startCount = UserChangedCount + local newProfileImage = {} + if ProfileImageThumbnailLoader then + ProfileImageThumbnailLoader:Cancel() + end + ProfileImageThumbnailLoader = ThumbnailLoader:Create(newProfileImage, XboxAppState.store:getState().RobloxUser.rbxuid, + ThumbnailLoader.AvatarSizes.Size352x352, ThumbnailLoader.AssetType.Avatar, true) + ProfileImageThumbnailLoader:LoadAsync(false, false) + + --If user has changed or we fail to get the ImageUrl, return nil + if newProfileImage.Image == "" or startCount ~= UserChangedCount then + newProfileImage = nil + end + return newProfileImage + end + + --Called at intervals, try to fetch latest CachedData + local debounceUpdateCachedData = false + function PackageData.UpdateCachedDataAsync() + if debounceUpdateCachedData then + while debounceUpdateCachedData do wait() end + end + debounceUpdateCachedData = true + + local startCount = UserChangedCount + local maxRetry = 2 + local valid = false + Utility.ExponentialRepeat( + function() return valid == false and startCount == UserChangedCount end, + function() valid = PackageData:UpdateCachedData() end, + maxRetry) + + debounceUpdateCachedData = false + end + + --Three Utility functions to update data + local function UpdateDataProfileImage(data, newProfileImage) + if not data then return end + data.ProfileImage:Update(CreateCacheData(newProfileImage, tick())) + end + + local function UpdateDataWearing(data, newWearingAssetId) + if not data then return end + local packages = data.Packages.Data + data.WearingAssetId:Update(CreateCacheData(newWearingAssetId, tick())) + + for _, package in pairs(packages) do + if package then + if package:IsOwned() then + package:SetWearing(newWearingAssetId == package:GetAssetId()) + else + package:SetWearing(false) + end + end + end + end + + local function UpdateDataOwned(data, newOwnedAssetId, newOwned) + if not data then return end + local packages = data.Packages.Data + if data.OwnedInfo then + if data.OwnedInfo[newOwnedAssetId] then + data.OwnedInfo[newOwnedAssetId]:Update(CreateCacheData(newOwned, tick())) + else + data.OwnedInfo[newOwnedAssetId] = CreateCacheData(newOwned, tick()) + end + end + for _, package in pairs(packages) do + if package then + if package:GetAssetId() == newOwnedAssetId then + package:SetOwned(newOwned) + if not newOwned then --We can't wear not owned packages + package:SetWearing(false) + end + break + end + end + end + end + + function PackageData:UpdateCachedData() + local validUpdate = false + local startCount = UserChangedCount + visiblePackageUpdating = false + + --Get Packages and OwnedInfo + local newResult = PackageData:GetPackagesAndOwnedInfoAsync() + local newPackages = newResult and newResult.Packages + local newOwnedInfo = newResult and newResult.OwnedInfo + + --Must make sure we get valid Packages and OwnedInfo + if newResult and newPackages and newOwnedInfo then + local newPackageDataCache = {} + newPackageDataCache.Packages = CreateCacheData(newPackages, tick()) + newPackageDataCache.OwnedInfo = {} + if newOwnedInfo then + for assetId, owned in pairs(newOwnedInfo) do + newPackageDataCache.OwnedInfo[assetId] = CreateCacheData(owned, tick()) + end + end + + --Get WearingAssetId + local newWearingAssetId = PackageData:GetWearingPackageAssetIdAsync(newPackages) + newPackageDataCache.WearingAssetId = CreateCacheData(newWearingAssetId, tick()) + + --Get ProfileImage + local newProfileImage = PackageData:GetProfileImageAsync() + newPackageDataCache.ProfileImage = CreateCacheData(newProfileImage, tick()) + + + --Make sure the user doesn't change, then + --if the user worn/purchased while we update in BG, we damp the data as it may be stale + if startCount == UserChangedCount and not visiblePackageUpdating then + --New CachedData + InternalPackageDataCache = newPackageDataCache + InternalPackageDataCache.Version = tick() + + --VisiblePackageDataCache is the CacheData used in app and will turn into visible elements + --user can operate on these visible elements and update VisiblePackageDataCache like wear/buy and so on + --We need the PackageDataCache to init VisiblePackageDataCache + if not VisiblePackageDataCache then + VisiblePackageDataCache = deepcopy(InternalPackageDataCache) + VisiblePackageDataCache.OnProfileImageUpdateBegin = Utility.Signal() + VisiblePackageDataCache.OnProfileImageUpdateEnd = Utility.Signal() + VisiblePackageDataCache.OnDifferentWearing = Utility.Signal() + VisiblePackageDataCache.OnDifferentOwned = Utility.Signal() + + --Each single pacakge can fire AvatarEquipSuccess and AvatarPurchaseSuccess. + --As whenever one single package data changes, other packages(previous wearing/not owned) may also be influenced, + --so we need listen to the signals to update all packages wearing and owned + EventHub:removeEventListener(EventHub.Notifications["AvatarEquipBegin"], "VisiblePackageDataCache") + EventHub:removeEventListener(EventHub.Notifications["AvatarPurchaseBegin"], "VisiblePackageDataCache") + EventHub:removeEventListener(EventHub.Notifications["AvatarEquipSuccess"], "VisiblePackageDataCache") + EventHub:removeEventListener(EventHub.Notifications["AvatarPurchaseSuccess"], "VisiblePackageDataCache") + + --Listen to Character update/equip from Avatar Editor + EventHub:removeEventListener(EventHub.Notifications["CharacterUpdated"], "VisiblePackageDataCache") + EventHub:removeEventListener(EventHub.Notifications["CharacterEquipped"], "VisiblePackageDataCache") + + EventHub:addEventListener(EventHub.Notifications["AvatarEquipBegin"], "VisiblePackageDataCache", function(assetId) visiblePackageUpdating = true end) + EventHub:addEventListener(EventHub.Notifications["AvatarPurchaseBegin"], "VisiblePackageDataCache", function(assetId) visiblePackageUpdating = true end) + EventHub:addEventListener(EventHub.Notifications["AvatarEquipSuccess"], "VisiblePackageDataCache", + function(assetId, skipProfileImageUpdate) + local startCount = UserChangedCount + UpdateDataWearing(VisiblePackageDataCache, assetId) + VisiblePackageDataCache.OnDifferentWearing:fire(assetId) + spawn(PreloadCharacterAppearanceAsync) + + if not skipProfileImageUpdate then + --Update Profile Image for wearing + ProfileImageUpdateForWearing = ProfileImageUpdateForWearing + 1 + local thisProfileImageUpdateForWearing = ProfileImageUpdateForWearing + VisiblePackageDataCache.OnProfileImageUpdateBegin:fire() + visiblePackageUpdating = true + local newProfileImage = PackageData:GetProfileImageAsync() + if startCount == UserChangedCount and VisiblePackageDataCache then + if thisProfileImageUpdateForWearing == ProfileImageUpdateForWearing and newProfileImage then + UpdateDataProfileImage(VisiblePackageDataCache, newProfileImage) + VisiblePackageDataCache.OnProfileImageUpdateEnd:fire(newProfileImage) + else + VisiblePackageDataCache.OnProfileImageUpdateEnd:fire() + end + end + end + end) + EventHub:addEventListener(EventHub.Notifications["AvatarPurchaseSuccess"], "VisiblePackageDataCache", + function(assetId, owned) + UpdateDataOwned(VisiblePackageDataCache, assetId, owned) + VisiblePackageDataCache.OnDifferentOwned:fire(assetId, owned) + end) + + --CharacterUpdate: If anything changed on character, reload the profile image + EventHub:addEventListener(EventHub.Notifications["CharacterUpdated"], "VisiblePackageDataCache", + function() + local startCount = UserChangedCount + --Update Profile Image for wearing + ProfileImageUpdateForWearing = ProfileImageUpdateForWearing + 1 + local thisProfileImageUpdateForWearing = ProfileImageUpdateForWearing + VisiblePackageDataCache.OnProfileImageUpdateBegin:fire() + visiblePackageUpdating = true + local newProfileImage = PackageData:GetProfileImageAsync() + if startCount == UserChangedCount and VisiblePackageDataCache then + if thisProfileImageUpdateForWearing == ProfileImageUpdateForWearing and newProfileImage then + UpdateDataProfileImage(VisiblePackageDataCache, newProfileImage) + VisiblePackageDataCache.OnProfileImageUpdateEnd:fire(newProfileImage) + else + VisiblePackageDataCache.OnProfileImageUpdateEnd:fire() + end + end + end) + + --If we equipped any asset on character, make wearing update + EventHub:addEventListener(EventHub.Notifications["CharacterEquipped"], "VisiblePackageDataCache", + function(newAssets, skipProfileImageUpdate) + --Get assets array from store assets + local assetsArray = {} + for _, assetIds in pairs(newAssets) do + for _, assetId in pairs(assetIds) do + table.insert(assetsArray, tonumber(assetId)) + end + end + table.sort(assetsArray) + + local newWearingAssetId = nil + local curWearingAssetId = nil + local packages = VisiblePackageDataCache.Packages.Data + --Compare package assetids with wearing assetids + for _, package in pairs(packages) do + if package:IsWearing() then + curWearingAssetId = tonumber(package:GetAssetId()) + end + end + for _, package in pairs(packages) do + if CompareAssetIdArrays(package:GetPartIdsAsync(), assetsArray) then + newWearingAssetId = tonumber(package:GetAssetId()) + break + end + end + if curWearingAssetId ~= newWearingAssetId then + --Fire AvatarEquipSuccess event for achievement and wearing update + EventHub:dispatchEvent(EventHub.Notifications["AvatarEquipSuccess"], newWearingAssetId, skipProfileImageUpdate) + end + end) + end + + validUpdate = true + end + end + return validUpdate + end + + --"Sync" call to get VisiblePackageDataCache unless VisiblePackageDataCache is not initialized + function PackageData:GetCachedData() + --If not cached data, we wait until UpdateCachedData is done + if not VisiblePackageDataCache then + while debounceUpdateCachedData do wait() end + --If still no data, try to fetch VisiblePackageDataCache again manually + if not VisiblePackageDataCache then + ReloaderManager:callReloaderFunc("PackageData", UpdateFuncId) + end + else + if InternalPackageDataCache then + --We merge VisiblePackageDataCache with InternalPackageDataCache if it's Version is behind + --use Update to make sure we are using the latest cached data + if VisiblePackageDataCache.Version < InternalPackageDataCache.Version then + VisiblePackageDataCache.Packages:Update(InternalPackageDataCache.Packages) + VisiblePackageDataCache.WearingAssetId:Update(InternalPackageDataCache.WearingAssetId) + VisiblePackageDataCache.ProfileImage:Update(InternalPackageDataCache.ProfileImage) + for assetId,_ in pairs(InternalPackageDataCache.OwnedInfo) do + if VisiblePackageDataCache.OwnedInfo[assetId] then + VisiblePackageDataCache.OwnedInfo[assetId]:Update(InternalPackageDataCache.OwnedInfo[assetId]) + else + VisiblePackageDataCache.OwnedInfo[assetId] = InternalPackageDataCache.OwnedInfo[assetId] + end + end + VisiblePackageDataCache.Version = InternalPackageDataCache.Version + end + end + end + + if VisiblePackageDataCache then + --Update Packages Owned and Wearing based on the merged data + for _, package in pairs(VisiblePackageDataCache.Packages.Data) do + local assetId = package:GetAssetId() + local wearingAssetId = VisiblePackageDataCache.WearingAssetId.Data + if VisiblePackageDataCache.OwnedInfo[assetId].Data then + package:SetOwned(true) + package:SetWearing(wearingAssetId == assetId) + else + package:SetOwned(false) + package:SetWearing(false) + end + end + end + + return VisiblePackageDataCache + end + + --Check whether the VisiblePackageDataCache has been initialized + function PackageData:HasCachedData() + return VisiblePackageDataCache ~= nil + end + + + --Update Avatar Pane for purchasing consumables + local debounceUpdatePurchasedConsumables = false + function PackageData:UpdatePurchasedConsumablesAsync(purchasedConsumablesInfo) + local startCount = UserChangedCount + --Wait until we get VisiblePackageDataCache + if not VisiblePackageDataCache then + while debounceUpdateCachedData do wait() end + end + + while debounceUpdatePurchasedConsumables do wait() end + debounceUpdatePurchasedConsumables = true + if startCount == UserChangedCount and purchasedConsumablesInfo and #purchasedConsumablesInfo > 0 and VisiblePackageDataCache then + --Check if any consumable is an Avatar + local purchasedAvatarAssetIdMap = {} + local purchasedAvatar = false + for _, consumable in pairs(purchasedConsumablesInfo) do + if consumable and tostring(consumable['Type']) == 'Avatar' then + purchasedAvatarAssetIdMap[tostring(consumable['Roblox_AssetId'])] = true + purchasedAvatar = true + end + end + + if purchasedAvatar then + local consumedAssetIds = {} + for _, package in pairs(VisiblePackageDataCache.Packages.Data) do + local assetId = package:GetAssetId() + if purchasedAvatarAssetIdMap[tostring(assetId)] then + table.insert(consumedAssetIds, assetId) + end + end + + --if user consumed Avatar and still in the same session, we update the owned + if startCount == UserChangedCount and VisiblePackageDataCache then + --inform the BG update that we are updating the visible packages + visiblePackageUpdating = true + for i = 1, #consumedAssetIds do + EventHub:dispatchEvent(EventHub.Notifications["AvatarPurchaseSuccess"], consumedAssetIds[i], true) + end + end + end + end + debounceUpdatePurchasedConsumables = false + end +end +return PackageData diff --git a/Client2018/content/internal/AppShell/Modules/Shell/PageKeys.lua b/Client2018/content/internal/AppShell/Modules/Shell/PageKeys.lua new file mode 100644 index 0000000..c06a590 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/PageKeys.lua @@ -0,0 +1,57 @@ +local PageKeys = +{ + AccountSettings = + { + Key = "AccountSettingsKey", + AccountInfo = + { + Key = "AccountInfoKey", + LinkAccountSetting = + { + Key = "LinkAccountSetting", + UnlinkAccountOverlay = + { + Key = "UnlinkAccountOverlayKey" + } + } + }, + Security = + { + Key = "SecurityKey", + }, + XboxSettings = + { + Key = "XboxSettingsKey", + CrossPlatformSetting = + { + Key = "CrossPlatformSettingKey" + }, + PrivilegeSettings = + { + Key = "PrivilegeSettings", + MultiplayerSetting = + { + Key = "MultiplayerSettingKey" + }, + SharedContentSetting = + { + Key = "SharedContentSettingKey" + }, + }, + } + }, + FriendCategories = + { + Key = "FriendCategories", + AllFriends = + { + Key = "AllFriends", + }, + OnlineFriends = + { + Key = "OnlineFriends", + }, + } +} + +return PageKeys \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/PlatformCatalogData.lua b/Client2018/content/internal/AppShell/Modules/Shell/PlatformCatalogData.lua new file mode 100644 index 0000000..c171ba2 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/PlatformCatalogData.lua @@ -0,0 +1,53 @@ +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) + +local PlatformCatalogData = {} + +local function getStudioDummyData() + return {{ReducedName = 'Default Short Title', Description = 'Default Description', DisplayListPrice = '$199.99', IsPartOfAnyBundle = false, DisplayPrice = '$0.80', ProductId = '210d1d69-5189-40f4-a59b-ecfb4f849847', Name = '22,500 Robux', TitleId = 0, IsBundle = false}, {ReducedName = 'Default Short Title', Description = 'Default Description', DisplayListPrice = '$3.00', IsPartOfAnyBundle = false, DisplayPrice = '$3.00', ProductId = '70c2075d-5e2f-4ffd-8de5-8a6d2f5e65ad', Name = '400 Robux', TitleId = 0, IsBundle = false}, {ReducedName = 'Default Short Title', Description = 'Default Description', DisplayListPrice = '$2.20', IsPartOfAnyBundle = false, DisplayPrice = '$2.20', ProductId = '878c642b-cb27-4d5e-a150-a408ea40c41c', Name = '240 Robux', TitleId = 0, IsBundle = false}} +end + +function PlatformCatalogData:GetCatalogInfoAsync() + if UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows then + return getStudioDummyData(), + true, + '' + end + + local numRetries = 5 + local catalogInfo, success, errormsg; + for i = 1, numRetries do + success, errormsg = pcall(function() + catalogInfo = PlatformService:BeginGetCatalogInfo() + end) + if success and catalogInfo then + return catalogInfo, success, errormsg + end + wait(10) + end + + return catalogInfo, success, errormsg +end + +function PlatformCatalogData:ParseMoneyValue(productInfo) + local price = productInfo and tonumber(productInfo.Price) or 0.99 + return price +end + +function PlatformCatalogData:ParseRobuxValue(productInfo) + local rawText = productInfo and productInfo.Name + local noJunk = string.gsub(rawText, ",", "") + noJunk = noJunk and string.match(noJunk, "[0-9]+") or nil + return noJunk and tonumber(noJunk) or 1000 +end + +function PlatformCatalogData:CalculateRobuxRatio(productInfo) + local robuxValue = self:ParseRobuxValue(productInfo) + local moneyValue = self:ParseMoneyValue(productInfo) + if moneyValue == 0 or robuxValue == 0 then + return 0 + end + return robuxValue / moneyValue +end + +return PlatformCatalogData diff --git a/Client2018/content/internal/AppShell/Modules/Shell/PlatformInterface.lua b/Client2018/content/internal/AppShell/Modules/Shell/PlatformInterface.lua new file mode 100644 index 0000000..3bf7b86 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/PlatformInterface.lua @@ -0,0 +1,37 @@ +-- Written by Kip Turner, Copyright Roblox 2015 + +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:FindFirstChild("RobloxGui") +local Modules = RobloxGui:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local Utility = require(ShellModules:FindFirstChild('Utility')) + +-- Platform Interface + +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) + +local PlatformInterface = {} + +local gettingFriends = false +function PlatformInterface:GetPartyMembersAsync() + while gettingFriends do wait() end + gettingFriends = true + + local partyMembers; + local success, msg = pcall(function() + partyMembers = PlatformService:GetPlatformPartyMembers() + end) + if not success then + Utility.DebugLog("HeroStatsManager - Error getting party members:" , msg) + end + + gettingFriends = false + return partyMembers +end + +function PlatformInterface:IsInAParty(partyMembers) + return (partyMembers and #partyMembers > 1) +end + +return PlatformInterface diff --git a/Client2018/content/internal/AppShell/Modules/Shell/PopupText.lua b/Client2018/content/internal/AppShell/Modules/Shell/PopupText.lua new file mode 100644 index 0000000..8946ae8 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/PopupText.lua @@ -0,0 +1,123 @@ +--[[ + // PopupText.lua + + // Creates a transparent text label that pops up when + // its parent it selected +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local TextService = game:GetService('TextService') + +local VERTICAL_PADDING = 12 +local HORIZONTAL_PADDING = 18 +local SELECTION_BORDER = 7 + +local createPopupText = function(parent, text) + local this = {} + + local tweenTime = 0.3 + local easingStyle = Enum.EasingStyle.Quad + local easingDirection = Enum.EasingDirection.Out + + local currentZIndex = 2 + + local clipFrame = Utility.Create'Frame' + { + Name = "ClipFrame"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + ZIndex = currentZIndex; + ClipsDescendants = true; + Parent = parent + } + + local bg = Utility.Create'Frame' + { + Name = "PopupBG"; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 1, 5); + BackgroundTransparency = GlobalSettings.ModalBackgroundTransparency; + BackgroundColor3 = GlobalSettings.ModalBackgroundColor; + BorderSizePixel = 0; + ZIndex = currentZIndex; + Parent = clipFrame; + } + local nameLabel = Utility.Create'TextLabel' + { + Name = "NameLabel"; + Size = UDim2.new(1, -HORIZONTAL_PADDING, 1, -VERTICAL_PADDING); + Position = UDim2.new(0, HORIZONTAL_PADDING/2 + SELECTION_BORDER, 0, VERTICAL_PADDING/2); + BackgroundTransparency = 1; + TextColor3 = GlobalSettings.WhiteTextColor; + TextWrapped = true; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.TitleSize; + ZIndex = currentZIndex; + Text = text; + Parent = bg; + } + + -- resize based on text bounds + local function resizeBounds() + local nameLabelTextSize = TextService:GetTextSize( + nameLabel.Text, + Utility.ConvertFontSizeEnumToInt(nameLabel.FontSize), + nameLabel.Font, + Vector2.new(clipFrame.AbsoluteSize.x - SELECTION_BORDER - HORIZONTAL_PADDING, clipFrame.AbsoluteSize.y - SELECTION_BORDER -VERTICAL_PADDING)) + + local newSizeX = nameLabelTextSize.x + HORIZONTAL_PADDING + local newSizeY = math.min(nameLabelTextSize.y + VERTICAL_PADDING, parent.AbsoluteSize.y * 0.75) + bg.Size = UDim2.new(0, newSizeX + SELECTION_BORDER, 0, newSizeY + SELECTION_BORDER) + end + spawn(function() + resizeBounds() + end) + + parent.SelectionGained:connect(function() + if #nameLabel.Text > 0 then + resizeBounds() + Utility.TweenPositionOrSet(bg, UDim2.new(0, 0, 1, -bg.Size.Y.Offset), easingDirection, easingStyle, tweenTime, true) + end + end) + parent.SelectionLost:connect(function() + Utility.TweenPositionOrSet(bg, UDim2.new(0, 0, 1, 5), easingDirection, easingStyle, tweenTime, true) + end) + + function this:SetTweenTime(value) + tweenTime = value + end + function this:SetEasingStyle(style) + easingStyle = style + end + function this:SetEasingDirection(direction) + easingDirection = direction + end + function this:SetText(text) + nameLabel.Text = text + resizeBounds() + if #text == 0 then + bg.Position = UDim2.new(0, 0, 1, 5) + end + end + function this:SetZIndex(zindex) + if zindex ~= currentZIndex then + currentZIndex = zindex + + clipFrame.ZIndex = currentZIndex + bg.ZIndex = currentZIndex + nameLabel.ZIndex = currentZIndex + end + end + + return this +end + +return createPopupText diff --git a/Client2018/content/internal/AppShell/Modules/Shell/PostProcessing.lua b/Client2018/content/internal/AppShell/Modules/Shell/PostProcessing.lua new file mode 100644 index 0000000..66f13f4 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/PostProcessing.lua @@ -0,0 +1,53 @@ +-- TODO: Clean up this file along with FFlagXboxAvatarEditor + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Lighting = game:GetService('Lighting') +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local PostProcessing = {} + +local Brightness = 0.3 +local Contrast = 0.5 +local Saturation = -1 +local TintColor = Color3.new(20.0/255.0, 43.0/255.0, 60.0/255.0) + +local MotionBlurIntensity = 10 + +-- create post processing objects +local ColorCorrection = Utility.Create'ColorCorrectionEffect' +{ + Brightness = Brightness; + Contrast = Contrast; + Saturation = Saturation; + TintColor = TintColor; + Enabled = true; + Parent = Lighting; +} + +local Blur = Utility.Create'BlurEffect' +{ + Size = MotionBlurIntensity; + Enabled = true; + Parent = Lighting; +} + +function PostProcessing.TransitionIn(time) + Utility.PropertyTweener(ColorCorrection, 'Contrast', ColorCorrection.Contrast, Contrast, time, Utility.EaseInOutQuad, true) + Utility.PropertyTweener(Blur, 'Size', Blur.Size, MotionBlurIntensity, time, Utility.EaseInOutQuad, true) +end + +function PostProcessing.TransitionOut(time) + Utility.PropertyTweener(ColorCorrection, 'Contrast', Contrast, -1, time, Utility.EaseInOutQuad, true) + Utility.PropertyTweener(Blur, 'Size', MotionBlurIntensity, 50, time, Utility.EaseInOutQuad, true) +end + +function PostProcessing.SetEnabled(nowEnabled) + ColorCorrection.Enabled = nowEnabled + Blur.Enabled = nowEnabled +end + +return PostProcessing diff --git a/Client2018/content/internal/AppShell/Modules/Shell/PurchasePackagePrompt.lua b/Client2018/content/internal/AppShell/Modules/Shell/PurchasePackagePrompt.lua new file mode 100644 index 0000000..2efc9d8 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/PurchasePackagePrompt.lua @@ -0,0 +1,498 @@ +--[[ + // PurchasePackagePrompt.lua + // Kip Turner + // Copyright Roblox 2015 +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local ContextActionService = game:GetService("ContextActionService") +local GuiService = game:GetService('GuiService') + +local UserDataModule = require(ShellModules:FindFirstChild('UserData')) +local Http = require(ShellModules:FindFirstChild('Http')) +local ScrollingTextBox = require(ShellModules:FindFirstChild('ScrollingTextBox')) +local CreateConfirmPrompt = require(ShellModules:FindFirstChild('ConfirmPrompt')) +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) + +local AssetManager = require(ShellModules:FindFirstChild('AssetManager')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) + +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local ErrorOverlayModule = require(ShellModules:FindFirstChild('ErrorOverlay')) +local Errors = require(ShellModules:FindFirstChild('Errors')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local CurrencyWidgetModule = require(ShellModules:FindFirstChild('CurrencyWidget')) + +local MOCKUP_WIDTH = 1920 +local MOCKUP_HEIGHT = 1080 +local CONTENT_WIDTH = 1920 +local CONTENT_HEIGHT = 690 +local PACKAGE_CONTAINER_WIDTH = 780 +local PACKAGE_CONTAINER_HEIGHT = 690 +local PACKAGE_BACKGROUND_WIDTH = 580 +local PACKAGE_BACKGROUND_HEIGHT = 640 + +local CONTENT_POSITION = Vector2.new(0, 225) + +local DETAILS_CONTAINER_WIDTH = CONTENT_WIDTH - PACKAGE_CONTAINER_WIDTH +local DETAILS_CONTAINER_HEIGHT = 690 + +local DESCRIPTION_WIDTH = 800 +local DESCRIPTION_HEIGHT = 320 + +local BUY_BUTTON_WIDTH = 320 +local BUY_BUTTON_HEIGHT = 64 + +local BUY_BUTTON_OFFSET = Vector2.new(0, -50) + +local function CreatePurchasePackagePrompt(packageInfo) + local this = {} + + local MyParent = nil + local Result = nil + local purchasing = false + local finishedLoading = false + local balance = nil + local inFocus = false + local ResultEvent = Utility.Signal() + + local packageName = packageInfo:GetFullName() + local robuxPrice = packageInfo:GetRobuxPrice() + local creatorId = packageInfo:GetCreatorId() + + + local ModalBackground = Utility.Create'Frame' + { + Name = "PurchasePackagePrompt"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + BackgroundColor3 = GlobalSettings.ModalBackgroundColor; + BorderSizePixel = 0; + } + + local ContentContainer = Utility.Create'Frame' + { + Name = "ContentContainer"; + Size = UDim2.new(CONTENT_WIDTH/MOCKUP_WIDTH, 0, CONTENT_HEIGHT/MOCKUP_HEIGHT, 0); + Position = UDim2.new(CONTENT_POSITION.x/MOCKUP_WIDTH, 0, CONTENT_POSITION.y/MOCKUP_HEIGHT, 0); + BackgroundTransparency = 0; + BackgroundColor3 = GlobalSettings.OverlayColor; + BorderSizePixel = 0; + Parent = ModalBackground; + } + + local PackageContainer = Utility.Create'Frame' + { + Name = "PackageContainer"; + Size = UDim2.new(PACKAGE_CONTAINER_WIDTH/CONTENT_WIDTH, 0, PACKAGE_CONTAINER_HEIGHT/CONTENT_HEIGHT, 0); + Position = UDim2.new(0,0,0,0); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Parent = ContentContainer; + } + local PackageBackground = Utility.Create'Frame' + { + Name = "PackageBackground"; + Size = UDim2.new(PACKAGE_BACKGROUND_WIDTH/PACKAGE_CONTAINER_WIDTH, 0, PACKAGE_BACKGROUND_HEIGHT/CONTENT_HEIGHT, 0); + BackgroundTransparency = 0; + BackgroundColor3 = GlobalSettings.ForegroundGreyColor; + BorderSizePixel = 0; + ZIndex = 2; + Parent = PackageContainer; + AssetManager.CreateShadow(1); + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + } + local PackageImage = Utility.Create'ImageLabel' + { + Name = 'PackageImage'; + Size = UDim2.new(1,0,1,0); + Image = Http.GetThumbnailUrlForAsset(packageInfo:GetAssetId()); + BackgroundTransparency = 1; + ZIndex = PackageBackground.ZIndex; + Parent = PackageBackground; + }; + local DetailsContainer = Utility.Create'Frame' + { + Name = "DetailsContainer"; + Size = UDim2.new(DETAILS_CONTAINER_WIDTH/CONTENT_WIDTH, 0, DETAILS_CONTAINER_HEIGHT/CONTENT_HEIGHT, 0); + Position = UDim2.new(PACKAGE_CONTAINER_WIDTH/CONTENT_WIDTH,0,0,0); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Parent = ContentContainer; + } + local PurchasingTitle = Utility.Create'TextLabel' + { + Name = 'PurchasingTitle'; + Text = Strings:LocalizedString('PurchasingTitle'); + Position = UDim2.new(0, 0, 0, 66); + Size = UDim2.new(1,0,0,25); + TextXAlignment = 'Left'; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.HeadingFont; + FontSize = GlobalSettings.HeaderSize; + BackgroundTransparency = 1; + Visible = false; + Parent = DetailsContainer; + }; + local DetailsContent = Utility.Create'Frame' + { + Name = "DetailsContent"; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0,0,0,0); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Parent = DetailsContainer; + } + + local PackageName = Utility.Create'TextLabel' + { + Name = 'PackageName'; + Text = packageName or "Unknown Package"; + Position = UDim2.new(0, 0, 0, 66); + Size = UDim2.new(1,0,0,25); + TextXAlignment = 'Left'; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.HeadingFont; + FontSize = GlobalSettings.HeaderSize; + BackgroundTransparency = 1; + Parent = DetailsContent; + }; + local RobuxIcon = Utility.Create'ImageLabel' + { + Name = 'RobuxIcon'; + Position = UDim2.new(0,0,0,125); + Size = UDim2.new(0,50,0,50); + BackgroundTransparency = 1; + Parent = DetailsContent; + + Image = "rbxasset://textures/ui/Shell/Icons/ROBUXIcon@1080.png"; + Size = UDim2.new(0,42,0,42); + }; + local PackageCost = Utility.Create'TextLabel' + { + Name = 'PackageCost'; + Text = ''; + Size = UDim2.new(0,0,1,0); + Position = UDim2.new(1.3,0,0,0); + TextXAlignment = 'Left'; + TextColor3 = GlobalSettings.GreenTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.HeaderSize; + BackgroundTransparency = 1; + Parent = RobuxIcon; + }; + + local AlreadyOwnTextLabel = Utility.Create'TextLabel' + { + Name = 'AlreadyOwnTextLabel'; + Text = ''; + Size = UDim2.new(0,0,0,50); + Position = UDim2.new(0,0,0,125); + TextXAlignment = 'Left'; + TextColor3 = GlobalSettings.GreenTextColor; + Font = GlobalSettings.ItalicFont; + FontSize = GlobalSettings.DescriptionSize; + BackgroundTransparency = 1; + Visible = false; + Parent = DetailsContent; + }; + + local descriptionScrollingTextBox = ScrollingTextBox(UDim2.new(DESCRIPTION_WIDTH/DETAILS_CONTAINER_WIDTH, 0, DESCRIPTION_HEIGHT/DETAILS_CONTAINER_HEIGHT, 0), + UDim2.new(0, 0, 0, 200), + DetailsContent) + descriptionScrollingTextBox:SetFontSize(GlobalSettings.TitleSize) + descriptionScrollingTextBox:SetFont(GlobalSettings.LightFont) + + local BuyButton = Utility.Create'ImageButton' + { + Name = "BuyButton"; + Size = UDim2.new(0, BUY_BUTTON_WIDTH, 0, BUY_BUTTON_HEIGHT); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.BlueButtonColor; + BackgroundTransparency = 0; + Parent = DetailsContent; + AnchorPoint = Vector2.new(0, 1); + Position = UDim2.new(0, 0, 1 + BUY_BUTTON_OFFSET.Y/DETAILS_CONTAINER_HEIGHT, 0); + } + local BuyText = Utility.Create'TextLabel' + { + Name = 'BuyText'; + Text = ''; + Size = UDim2.new(1,0,1,0); + TextColor3 = GlobalSettings.TextSelectedColor; + Font = GlobalSettings.HeadingFont; + FontSize = GlobalSettings.MediumLargeHeadingSize; + BackgroundTransparency = 1; + Parent = BuyButton; + }; + + --Make the BuyButton big enough + Utility.ResizeButtonWithDynamicText(BuyButton, BuyText, + {Strings:LocalizedString("OkWord"), Strings:LocalizedString("BuyWord"), Strings:LocalizedString("GetRobuxPhrase"), Strings:LocalizedString("TakeWord")}, + GlobalSettings.TextHorizontalPadding) + + + local function OnOwnedUpdate() + if packageInfo:IsOwned() or robuxPrice == nil then + if packageInfo:IsOwned() then + if robuxPrice and robuxPrice > 0 then + AlreadyOwnTextLabel.Text = string.format(Strings:LocalizedString('PurchasedThisPhrase'), Utility.FormatNumberString(tostring(robuxPrice))) + else + AlreadyOwnTextLabel.Text = Strings:LocalizedString('AlreadyOwnedPhrase') + end + end + AlreadyOwnTextLabel.Visible = (packageInfo:IsOwned() == true) + RobuxIcon.Visible = false + BuyText.Text = Strings:LocalizedString("OkWord") + + --V1: Show buy button for Xbox add ons (boy/girl avatar) if they aren't owned + if packageInfo:IsXboxAddOn() and not packageInfo:IsOwned() then + BuyText.Text = Strings:LocalizedString('BuyWord') + end + else + if robuxPrice and robuxPrice == 0 then + PackageCost.Text = Strings:LocalizedString('FreeWord'); + else + PackageCost.Text = robuxPrice and Utility.FormatNumberString(tostring(robuxPrice)) or '-'; + end + RobuxIcon.Visible = true + AlreadyOwnTextLabel.Visible = false + if robuxPrice == 0 then + BuyText.Text = Strings:LocalizedString('TakeWord'); + elseif balance and robuxPrice > balance then + BuyText.Text = Strings:LocalizedString('GetRobuxPhrase'); + else + BuyText.Text = Strings:LocalizedString('BuyWord'); + end + end + end + + local function SetBalance(newBalance) + balance = newBalance + OnOwnedUpdate() + end + + function this:UpdateOwned() + OnOwnedUpdate() + end + + function this:ResultAsync() + if Result then + return Result + end + ResultEvent:wait() + return Result + end + + + do + local function loadBalanceAsync() + local balance = UserDataModule.GetPlatformUserBalanceAsync() + SetBalance(balance) + end + local function loadDescription() + local descriptionText = packageInfo:GetDescriptionAsync() + descriptionScrollingTextBox:SetText(descriptionText or "") + end + + DetailsContent.Visible = false + + local loader = LoadingWidget({Parent = DetailsContainer}, {loadBalanceAsync, loadDescription}) + spawn(function() + loader:AwaitFinished() + loader:Cleanup() + loader = nil + ScreenManager:DefaultFadeIn(DetailsContent) + DetailsContent.Visible = true + if inFocus then + Utility.SetSelectedCoreObject(this:GetDefaultSelectableObject()) + end + finishedLoading = true + end) + end + + local function DoPurchase() + purchasing = true + local wasOwned = packageInfo:IsOwned() + local purchaseResult = packageInfo:BuyAsync() + local newBalance = purchaseResult and purchaseResult['balanceAfterSale'] or UserDataModule.GetPlatformUserBalanceAsync() + SetBalance(newBalance) + local nowOwns = packageInfo:IsOwned() and not wasOwned + Result = nowOwns + if nowOwns then + this:UpdateOwned() + end + purchasing = false + if not wasOwned and not nowOwns then + if ScreenManager:GetTopScreen() == this then + ScreenManager:CloseCurrent() + end + ScreenManager:OpenScreen(ErrorOverlayModule(Errors.PackagePurchase[1]), false) + end + Utility.DebugLog("Done with purchase") + end + + + OnOwnedUpdate() + + function this:GetAnalyticsInfo() + return + { + [Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('PurchasePackagePromptId'); + AssetId = packageInfo:GetAssetId(); + IsOwned = packageInfo:IsOwned(); + } + end + + function this:GetDefaultSelectableObject() + return BuyButton + end + + function this:FadeInBackground() + Utility.PropertyTweener(ModalBackground, "BackgroundTransparency", 1, GlobalSettings.ModalBackgroundTransparency, 0.25, Utility.EaseInOutQuad, true) + end + + function this:GetPriority() + return GlobalSettings.OverlayPriority + end + + local currencyWidget = nil + local RobuxChangedConn = nil + function this:Show() + ModalBackground.Visible = true + ModalBackground.Parent = ScreenManager:GetScreenGuiByPriority(self:GetPriority()) + + local function onPackageBackgroundResize() + PackageImage.Size = Utility.CalculateFill(PackageBackground, Vector2.new(420, 420)) + PackageImage.AnchorPoint = Vector2.new(0.5, 0.5) + PackageImage.Position = UDim2.new(0.5, 0, 0.5, 0) + end + + self.PackageBackgroundChangedConn = Utility.DisconnectEvent(self.PackageBackgroundChangedConn) + self.PackageBackgroundChangedConn = PackageBackground:GetPropertyChangedSignal('AbsoluteSize'):connect(function() + onPackageBackgroundResize() + end) + onPackageBackgroundResize() + + if not currencyWidget then + currencyWidget = CurrencyWidgetModule({Parent = ModalBackground; Position = UDim2.new(0.052, 0, 0.88, 0); }) + end + Utility.DisconnectEvent(RobuxChangedConn) + RobuxChangedConn = currencyWidget.RobuxChanged:connect(SetBalance) + + SoundManager:Play('OverlayOpen') + end + + function this:Hide() + ModalBackground.Visible = false + ModalBackground.Parent = nil + + self.PackageBackgroundChangedConn = Utility.DisconnectEvent(self.PackageBackgroundChangedConn) + RobuxChangedConn = Utility.DisconnectEvent(RobuxChangedConn) + end + + function this:ScreenRemoved() + if currencyWidget then + currencyWidget:Destroy() + currencyWidget = nil + end + ResultEvent:fire() + end + + function this:Focus() + inFocus = true + ContextActionService:BindCoreAction("ReturnFromPurchasePackageScreen", + function(actionName, inputState, inputObject) + if not purchasing then + if inputState == Enum.UserInputState.End then + ScreenManager:CloseCurrent() + end + end + end, + false, + Enum.KeyCode.ButtonB) + + local buyButtonDebounce = false + self.BuyButtonConn = Utility.DisconnectEvent(self.BuyButtonConn) + self.BuyButtonConn = BuyButton.MouseButton1Click:connect(function() + if buyButtonDebounce or purchasing or not finishedLoading then + return + end + buyButtonDebounce = true + + if packageInfo:IsOwned() or robuxPrice == nil then + SoundManager:Play('ButtonPress') + ScreenManager:CloseCurrent() + + --Open the product detail if it's a xbox add-on and not owned + if packageInfo:IsXboxAddOn() and not packageInfo:IsOwned() then + packageInfo:OpenAvatarDetailInXboxStore() + end + else + if balance then + if robuxPrice > 0 and balance < robuxPrice then + Utility.DebugLog("Goto robux screen") + EventHub:dispatchEvent(EventHub.Notifications["NavigateToRobuxScreen"]) + else + local confirmPrompt = CreateConfirmPrompt({ProductId = packageInfo and packageInfo:GetAssetId() or "Unknown"; ProductName = packageName; Cost = robuxPrice; Balance = balance; ProductImage = Http.GetThumbnailUrlForAsset(packageInfo:GetAssetId()); Currency = "Robux"; CurrencySymbol = ""}, + {ShowRemainingBalance = true; ShowRobuxIcon = true;}) + ScreenManager:OpenScreen(confirmPrompt) + local result = confirmPrompt:ResultAsync() + if result == true then + local loader = LoadingWidget({Parent = DetailsContainer}, {DoPurchase}) + DetailsContent.Visible = false + PurchasingTitle.Visible = true + spawn(function() + loader:AwaitFinished() + loader:Cleanup() + loader = nil + ScreenManager:DefaultFadeIn(DetailsContent) + DetailsContent.Visible = true + PurchasingTitle.Visible = false + + if ScreenManager:GetTopScreen() == this then + ScreenManager:CloseCurrent() + end + end) + + else + Utility.DebugLog("Declined to buy") + end + end + end + end + + buyButtonDebounce = false + end) + + GuiService:AddSelectionParent("PurchasePackagePromptSelectionGroup", ContentContainer) + Utility.SetSelectedCoreObject(self:GetDefaultSelectableObject()) + end + + function this:RemoveFocus() + inFocus = false + ContextActionService:UnbindCoreAction("ReturnFromPurchasePackageScreen") + GuiService:RemoveSelectionGroup("PurchasePackagePromptSelectionGroup") + self.BuyButtonConn = Utility.DisconnectEvent(self.BuyButtonConn) + Utility.SetSelectedCoreObject(nil) + end + + + function this:SetParent(parent) + MyParent = parent + ModalBackground.Parent = MyParent + end + + + return this +end + +return CreatePurchasePackagePrompt diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/AppShellReducer.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/AppShellReducer.lua new file mode 100644 index 0000000..a8a0a70 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/AppShellReducer.lua @@ -0,0 +1,22 @@ +-- main AppShell reducer +local Reducers = script.Parent +local RobloxUser = require(Reducers.RobloxUser) +local ScreenList = require(Reducers.ScreenList) +local XboxUser = require(Reducers.XboxUser) +local UserThumbnails = require(Reducers.UserThumbnails) +local Friends = require(Reducers.Friends) +local RenderedFriends = require(Reducers.RenderedFriends) + +return function(state, action) + state = state or {} + + return { + -- Use reducer composition to add reducers here + RobloxUser = RobloxUser(state.RobloxUser, action), + ScreenList = ScreenList(state.ScreenList, action), + XboxUser = XboxUser(state.XboxUser, action), + UserThumbnails = UserThumbnails(state.UserThumbnails, action), + Friends = Friends(state.Friends, action), + RenderedFriends = RenderedFriends(state.RenderedFriends, action), + } +end diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/AppShellReducer.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/AppShellReducer.spec.lua new file mode 100644 index 0000000..b1a156f --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/AppShellReducer.spec.lua @@ -0,0 +1,10 @@ +return function() + local AppShellReducer = require(script.Parent.AppShellReducer) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = AppShellReducer(nil, {}) + expect(state).to.be.a("table") + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/CrossPlayEnabledState.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/CrossPlayEnabledState.lua new file mode 100644 index 0000000..9a9919e --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/CrossPlayEnabledState.lua @@ -0,0 +1,32 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) +local RequestCrossPlayEnabled = require(Modules.Shell.Actions.RequestCrossPlayEnabled) +local SetCrossPlayEnabled = require(Modules.Shell.Actions.SetCrossPlayEnabled) +local PostCrossPlayEnabledFailed = require(Modules.Shell.Actions.PostCrossPlayEnabledFailed) +local GetCrossPlayEnabledFailed = require(Modules.Shell.Actions.GetCrossPlayEnabledFailed) + +--To use: Add CrossPlayEnabledState = CrossPlayEnabledState(state.CrossPlayEnabledState, action) to the AppShellReducer +return function(state, action) + state = state or {} + + if action.type == RequestCrossPlayEnabled.name then + state = Immutable.Set(state, "isRequesting", true) + elseif action.type == SetCrossPlayEnabled.name then + if action.enabled ~= nil then + state = + { + enabled = action.enabled, + lastUpdated = action.timestamp, + isRequesting = false, + } + else + state = {} + end + elseif action.type == PostCrossPlayEnabledFailed.name then + state = Immutable.Set(state, "isRequesting", false) + elseif action.type == GetCrossPlayEnabledFailed.name then + state = Immutable.Set(state, "isRequesting", false) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/CrossPlayEnabledState.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/CrossPlayEnabledState.spec.lua new file mode 100644 index 0000000..3ba41e7 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/CrossPlayEnabledState.spec.lua @@ -0,0 +1,71 @@ +return function() + local CrossPlayEnabledStateReducer = require(script.Parent.CrossPlayEnabledState) + local Actions = script.Parent.Parent.Actions + + local RequestCrossPlayEnabled = require(Actions.RequestCrossPlayEnabled) + local SetCrossPlayEnabled = require(Actions.SetCrossPlayEnabled) + local PostCrossPlayEnabledFailed = require(Actions.PostCrossPlayEnabledFailed) + local GetCrossPlayEnabledFailed = require(Actions.GetCrossPlayEnabledFailed) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = CrossPlayEnabledStateReducer(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("Action RequestCrossPlayEnabled", function() + it("should set isRequesting to true in the store", function() + local action = RequestCrossPlayEnabled() + local state = CrossPlayEnabledStateReducer({}, action) + + expect(state).to.be.a("table") + expect(state.isRequesting).to.equal(true) + end) + end) + + describe("Action SetCrossPlayEnabled", function() + it("should set enabled, lastUpdated and reset isRequesting to false in the store", function() + local action = SetCrossPlayEnabled(true, tick()) + local state = CrossPlayEnabledStateReducer({}, action) + + expect(state).to.be.a("table") + expect(state.enabled).to.equal(true) + expect(state.lastUpdated).to.be.a("number") + expect(state.isRequesting).to.equal(false) + end) + + it("should clear enabled, lastUpdated to nil and reset isRequesting to nil when SetCrossPlayEnabled with nil", function() + local action = SetCrossPlayEnabled(true, tick()) + local state = CrossPlayEnabledStateReducer({}, action) + + action = SetCrossPlayEnabled() + state = CrossPlayEnabledStateReducer(state, action) + + expect(state).to.be.a("table") + expect(state.enabled).never.to.be.ok() + expect(state.lastUpdated).never.to.be.ok() + expect(state.isRequesting).never.to.be.ok() + end) + end) + + describe("Action PostCrossPlayEnabledFailed", function() + it("should set isRequesting to false in the store", function() + local action = PostCrossPlayEnabledFailed() + local state = CrossPlayEnabledStateReducer({}, action) + + expect(state).to.be.a("table") + expect(state.isRequesting).to.equal(false) + end) + end) + + describe("Action GetCrossPlayEnabledFailed", function() + it("should set isRequesting to false in the store", function() + local action = GetCrossPlayEnabledFailed() + local state = CrossPlayEnabledStateReducer({}, action) + + expect(state).to.be.a("table") + expect(state.isRequesting).to.equal(false) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/ErrorHandler.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/ErrorHandler.lua new file mode 100644 index 0000000..955a224 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/ErrorHandler.lua @@ -0,0 +1,26 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local OrderedMap = require(Modules.Shell.OrderedMap) +local DeleteError = require(Modules.Shell.Actions.DeleteError) + +local function getErrorCode(error) + return error.Code +end + +local function errorSortPredicate(error1, error2) + return error1.timestamp < error2.timestamp +end + +return function(state, action) + state = state or OrderedMap.new(getErrorCode, errorSortPredicate) + + --OrderedMap.Delete and OrderedMap.Insert are immutable operations and a new table is returned. + if action.error and type(getErrorCode(action.error)) == "number" then + if action.type == DeleteError.name then + state = OrderedMap.Delete(state, getErrorCode(action.error)) + else + state = OrderedMap.Insert(state, action.error) + end + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/ErrorHandler.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/ErrorHandler.spec.lua new file mode 100644 index 0000000..d280736 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/ErrorHandler.spec.lua @@ -0,0 +1,45 @@ +return function() + local ErrorHandler = require(script.Parent.ErrorHandler) + local Actions = script.Parent.Parent.Actions + + local AddError = require(Actions.AddError) + local DeleteError = require(Actions.DeleteError) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = ErrorHandler(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("Action AddError", function() + it("should add the error to the errormap when an action with error dispatched", function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local ShellModules = Modules:FindFirstChild("Shell") + local Errors = require(ShellModules:FindFirstChild('Errors')) + local DefaultError = Errors.Default + local action = AddError(DefaultError, tick()) + local state = ErrorHandler(nil, action) + + expect(state).to.be.a("table") + expect(state:Length()).to.equal(1) + expect(state:First()).to.equal(action.error) + end) + end) + + describe("Action DeleteError", function() + it("should delete the corresponding error in the errormap when DeleteError", function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local ShellModules = Modules:FindFirstChild("Shell") + local Errors = require(ShellModules:FindFirstChild('Errors')) + local DefaultError = Errors.Default + local action = AddError(DefaultError, tick()) + local state = ErrorHandler(nil, action) + action = DeleteError(action.error) + state = ErrorHandler(state, action) + + expect(state).to.be.a("table") + expect(state:Length()).to.equal(0) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/Friends.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/Friends.lua new file mode 100644 index 0000000..f7c4f29 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/Friends.lua @@ -0,0 +1,49 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local SetFriendsData = require(Modules.Shell.Actions.SetFriendsData) +local Immutable = require(Modules.Common.Immutable) + +--[[ + // action is table + // Table keys: + // [index number] - table + // xuid - number + // robloxName - string + // placeId - number + // robloxStatus - string + // robloxuid - number + // lastLocation - string + // gamertag - string + // xboxStatus - string + // friendsSource - string +]] + +return function(state, action) + state = state or { + initialized = false, + data = {} + } + + if action.type == SetFriendsData.name then + -- Use nil in setFriendsData to reset + if not action.data then + return { + initialized = false, + data = {} + } + end + + -- Make a copy of new friends + local newFriendsData = {} + for i in ipairs(action.data) do + local friendDataTable = Immutable.JoinDictionaries(action.data[i]) + table.insert(newFriendsData, friendDataTable) + end + + return { + initialized = true, + data = newFriendsData + } + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/Friends.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/Friends.spec.lua new file mode 100644 index 0000000..2070601 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/Friends.spec.lua @@ -0,0 +1,65 @@ +return function() + local FriendsReducer = require(script.Parent.Friends) + local Actions = script.Parent.Parent.Actions + + local SetFriendsData = require(Actions.SetFriendsData) + + describe("initial state", function() + it("should return an table when passed nil", function() + local state = FriendsReducer(nil, {}) + expect(state).to.be.a("table") + end) + + it("should set initial values when passed nil", function() + local state = FriendsReducer(nil, {}) + expect(state.initialized).to.be.a("boolean") + expect(state.initialized).to.equal(false) + expect(state.data).to.be.a("table") + expect(#state.data).to.equal(0) + end) + end) + + describe("Action SetFriendsData", function() + it("should initialize friends in data", function() + local action = SetFriendsData({ {}, {} }) + local state = FriendsReducer(nil, action) + + expect(state.data).to.be.a("table") + expect(state.data[2]).to.be.a("table") + expect(state.initialized).to.equal(true) + end) + + it("should put friend data into the entries", function() + local action = SetFriendsData({ { xuid=12345, robloxName="TestName", xboxStatus="online" } }) + local state = FriendsReducer(nil, action) + + expect(state.data[1].xuid).to.be.a("number") + expect(state.data[1].xuid).to.equal(12345) + expect(state.data[1].robloxName).to.be.a("string") + expect(state.data[1].robloxName).to.equal("TestName") + end) + + it("should clear the friends array when passed an empty table", function() + local action = SetFriendsData({ { xuid=12345, robloxName="TestName", xboxStatus="online" } }) + local state = FriendsReducer(nil, action) + + action = SetFriendsData({}) + state = FriendsReducer(state, action) + + expect(state.data).to.be.a("table") + expect(#state.data).to.equal(0) + end) + + it("should reset the state when passed a nil SetFriendsData action", function() + local action = SetFriendsData({ { xuid=12345, robloxName="TestName", xboxStatus="online" } }) + local state = FriendsReducer(nil, action) + action = SetFriendsData(nil) + state = FriendsReducer(state, action) + + expect(state.initialized).to.be.a("boolean") + expect(state.initialized).to.equal(false) + expect(state.data).to.be.a("table") + expect(#state.data).to.equal(0) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/PrivilegeSettingsState.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/PrivilegeSettingsState.lua new file mode 100644 index 0000000..f14d8c4 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/PrivilegeSettingsState.lua @@ -0,0 +1,26 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) +local FetchPrivilegeSettings = require(Modules.Shell.Actions.FetchPrivilegeSettings) +local SetPrivilegeSettings = require(Modules.Shell.Actions.SetPrivilegeSettings) + +return function(state, action) + state = state or {} + if action.type == FetchPrivilegeSettings.name then + state = Immutable.Set(state, "isRequesting", true) + elseif action.type == SetPrivilegeSettings.name then + if action.Multiplayer and action.SharedContent then + local multiplayerSetting = state.Multiplayer or {} + local sharedContentSetting = state.SharedContent or {} + state = { + Multiplayer = Immutable.JoinDictionaries(multiplayerSetting, action.Multiplayer), + SharedContent = Immutable.JoinDictionaries(sharedContentSetting, action.SharedContent), + lastUpdated = action.timestamp, + isRequesting = false + } + else + state = {} + end + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/PrivilegeSettingsState.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/PrivilegeSettingsState.spec.lua new file mode 100644 index 0000000..0c0828d --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/PrivilegeSettingsState.spec.lua @@ -0,0 +1,51 @@ +return function() + local PrivilegeSettingsStateReducer = require(script.Parent.PrivilegeSettingsState) + local Actions = script.Parent.Parent.Actions + + local FetchPrivilegeSettings = require(Actions.FetchPrivilegeSettings) + local SetPrivilegeSettings = require(Actions.SetPrivilegeSettings) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = PrivilegeSettingsStateReducer(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("Action FetchPrivilegeSettings", function() + it("should set isRequesting to true in the store", function() + local action = FetchPrivilegeSettings() + local state = PrivilegeSettingsStateReducer({}, action) + + expect(state).to.be.a("table") + expect(state.isRequesting).to.equal(true) + end) + end) + + describe("Action SetPrivilegeSettings", function() + it("should set privilege settings, lastUpdated and reset isRequesting to false in the store", function() + local action = SetPrivilegeSettings({Multiplayer = {}, SharedContent = {}, timestamp = tick()}) + local state = PrivilegeSettingsStateReducer({}, action) + + expect(state).to.be.a("table") + expect(state.Multiplayer).to.be.a("table") + expect(state.SharedContent).to.be.a("table") + expect(state.lastUpdated).to.be.a("number") + expect(state.isRequesting).to.equal(false) + end) + + it("should set privilege settings, lastUpdated to nil and reset isRequesting to nil when SetPrivilegeSettings with nil", function() + local action = SetPrivilegeSettings({Multiplayer = {}, SharedContent = {}, timestamp = tick()}) + local state = PrivilegeSettingsStateReducer({}, action) + + action = SetPrivilegeSettings() + state = PrivilegeSettingsStateReducer(state, action) + + expect(state).to.be.a("table") + expect(state.Multiplayer).never.to.be.ok() + expect(state.SharedContent).never.to.be.ok() + expect(state.lastUpdated).never.to.be.ok() + expect(state.isRequesting).never.to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/RenderedFriends.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/RenderedFriends.lua new file mode 100644 index 0000000..8a3cdc7 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/RenderedFriends.lua @@ -0,0 +1,49 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local SetRenderedFriendsData = require(Modules.Shell.Actions.SetRenderedFriendsData) +local Immutable = require(Modules.Common.Immutable) + +--[[ + // action is table + // Table keys: + // [index number] - table + // xuid - number + // robloxName - string + // placeId - number + // robloxStatus - string + // robloxuid - number + // lastLocation - string + // gamertag - string + // xboxStatus - string + // friendsSource - string +]] + +return function(state, action) + state = state or { + initialized = false, + data = {} + } + + if action.type == SetRenderedFriendsData.name then + -- Use nil in setFriendsData to reset + if not action.data then + return { + initialized = false, + data = {} + } + end + + -- Make a copy of new friends + local newFriendsData = {} + for i in ipairs(action.data) do + local friendDataTable = Immutable.JoinDictionaries(action.data[i]) + table.insert(newFriendsData, friendDataTable) + end + + return { + initialized = true, + data = newFriendsData + } + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/RenderedFriends.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/RenderedFriends.spec.lua new file mode 100644 index 0000000..5ae6d48 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/RenderedFriends.spec.lua @@ -0,0 +1,65 @@ +return function() + local RenderedFriendsReducer = require(script.Parent.RenderedFriends) + local Actions = script.Parent.Parent.Actions + + local SetRenderedFriendsData = require(Actions.SetRenderedFriendsData) + + describe("initial state", function() + it("should return an table when passed nil", function() + local state = RenderedFriendsReducer(nil, {}) + expect(state).to.be.a("table") + end) + + it("should set initial values when passed nil", function() + local state = RenderedFriendsReducer(nil, {}) + expect(state.initialized).to.be.a("boolean") + expect(state.initialized).to.equal(false) + expect(state.data).to.be.a("table") + expect(#state.data).to.equal(0) + end) + end) + + describe("Action SetRenderedFriendsData", function() + it("should initialize friends in data", function() + local action = SetRenderedFriendsData({ {}, {} }) + local state = RenderedFriendsReducer(nil, action) + + expect(state.data).to.be.a("table") + expect(state.data[2]).to.be.a("table") + expect(state.initialized).to.equal(true) + end) + + it("should put friend data into the entries", function() + local action = SetRenderedFriendsData({ { xuid=12345, robloxName="TestName", xboxStatus="online" } }) + local state = RenderedFriendsReducer(nil, action) + + expect(state.data[1].xuid).to.be.a("number") + expect(state.data[1].xuid).to.equal(12345) + expect(state.data[1].robloxName).to.be.a("string") + expect(state.data[1].robloxName).to.equal("TestName") + end) + + it("should clear the friends array when passed an empty table", function() + local action = SetRenderedFriendsData({ { xuid=12345, robloxName="TestName", xboxStatus="online" } }) + local state = RenderedFriendsReducer(nil, action) + + action = SetRenderedFriendsData({}) + state = RenderedFriendsReducer(state, action) + + expect(state.data).to.be.a("table") + expect(#state.data).to.equal(0) + end) + + it("should reset the state when passed a nil SetRenderedFriendsData action", function() + local action = SetRenderedFriendsData({ { xuid=12345, robloxName="TestName", xboxStatus="online" } }) + local state = RenderedFriendsReducer(nil, action) + action = SetRenderedFriendsData(nil) + state = RenderedFriendsReducer(state, action) + + expect(state.initialized).to.be.a("boolean") + expect(state.initialized).to.equal(false) + expect(state.data).to.be.a("table") + expect(#state.data).to.equal(0) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/RobloxUser.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/RobloxUser.lua new file mode 100644 index 0000000..3a37208 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/RobloxUser.lua @@ -0,0 +1,16 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local SetRobloxUser = require(Modules.Shell.Actions.SetRobloxUser) + +return function(state, action) + state = state or {} + + if action.type == SetRobloxUser.name then + return { + robloxName = action.robloxName, + rbxuid = action.rbxuid, + under13 = action.under13 + } + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/RobloxUser.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/RobloxUser.spec.lua new file mode 100644 index 0000000..06fd58f --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/RobloxUser.spec.lua @@ -0,0 +1,41 @@ +return function() + local RobloxUserReducer = require(script.Parent.RobloxUser) + local Actions = script.Parent.Parent.Actions + + local SetRobloxUser = require(Actions.SetRobloxUser) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = RobloxUserReducer(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("Action SetRobloxUser", function() + it("should set the robloxName, rbxuid, and under13 values in the store", function() + local action = SetRobloxUser({robloxName = "TestRobloxName", rbxuid = 12345, under13 = true}) + local state = RobloxUserReducer(state, action) + + expect(state).to.be.a("table") + expect(state.robloxName).to.be.a("string") + expect(state.robloxName).to.equal("TestRobloxName") + expect(state.rbxuid).to.be.a("number") + expect(state.rbxuid).to.equal(12345) + expect(state.under13).to.be.a("boolean") + expect(state.under13).to.equal(true) + end) + + it("should clear the robloxName, rbxuid, and under13 to nil when passed an empty SetRobloxUser action", function() + local action = SetRobloxUser({robloxName = "TestRobloxName", rbxuid = 12345, under13 = true}) + local state = RobloxUserReducer(state, action) + + action = SetRobloxUser({}) + local state = RobloxUserReducer(state, action) + + expect(state).to.be.a("table") + expect(state.robloxName).to.equal(nil) + expect(state.rbxuid).to.equal(nil) + expect(state.under13).to.equal(nil) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/ScreenList.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/ScreenList.lua new file mode 100644 index 0000000..e142eb8 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/ScreenList.lua @@ -0,0 +1,30 @@ +local CoreGui = game:GetService("CoreGui") +local Actions = CoreGui.RobloxGui.Modules.Shell.Actions +local Common = CoreGui.RobloxGui.Modules.Common + +local InsertScreen = require(Actions.InsertScreen) +local RemoveScreen = require(Actions.RemoveScreen) + +local Immutable = require(Common.Immutable) + +return function(state, action) + state = state or {} + + if action.type == InsertScreen.name then + local newList = Immutable.Append(state, action.item) + + table.sort(newList, function(item1, item2) + if item1.priority == item2.priority then + return item1.createdAt > item2.createdAt + end + + return item1.priority > item2.priority + end) + + return newList + elseif action.type == RemoveScreen.name then + return Immutable.RemoveValueFromList(state, action.item) + end + + return state +end diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/ScreenList.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/ScreenList.spec.lua new file mode 100644 index 0000000..0465c46 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/ScreenList.spec.lua @@ -0,0 +1,72 @@ +return function() + local ScreenList = require(script.Parent.ScreenList) + local ScreenItem = require(script.Parent.Parent.Models.ScreenItem) + local InsertScreen = require(script.Parent.Parent.Actions.InsertScreen) + local RemoveScreen = require(script.Parent.Parent.Actions.RemoveScreen) + + describe("require", function() + it("should require without error", function() + require(script.Parent.ScreenList) + end) + end) + + describe("call", function() + it("should create with initial state", function() + local state = ScreenList(nil, {}) + expect(state).to.be.a("table") + end) + + it("should insert a single screen", function() + local item = ScreenItem.new("foo", 1, {}) + local action = InsertScreen(item) + + local state = ScreenList({}, action) + expect(#state).to.equal(1) + expect(state[1].id).to.equal("foo") + expect(state[1].priority).to.equal(1) + expect(state[1].data).to.be.a("table") + end) + + it("should insert multiple screens", function() + local item1 = ScreenItem.new("foo", 1, {}) + local item2 = ScreenItem.new("bar", 2, {}) + local action1 = InsertScreen(item1) + local action2 = InsertScreen(item2) + + local state = ScreenList({}, action1) + state = ScreenList(state, action2) + + expect(#state).to.equal(2) + expect(state[1].id).to.equal("bar") + expect(state[1].priority).to.equal(2) + expect(state[1].data).to.be.a("table") + end) + + it("should sort the screens by descending priority", function() + local item1 = ScreenItem.new("foo", 3, {}) + local item2 = ScreenItem.new("bar", 1, {}) + local action1 = InsertScreen(item1) + local action2 = InsertScreen(item2) + + local state = ScreenList({}, action1) + state = ScreenList(state, action2) + + expect(#state).to.equal(2) + expect(state[1].id).to.equal("foo") + expect(state[1].priority).to.equal(3) + expect(state[1].data).to.be.a("table") + end) + + it("should remove screens", function() + local item = ScreenItem.new("foo", 1, {}) + local action = InsertScreen(item) + + local state = ScreenList({}, action) + + local action2 = RemoveScreen(item) + state = ScreenList(state, action2) + + expect(#state).to.equal(0) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/UserThumbnails.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/UserThumbnails.lua new file mode 100644 index 0000000..fa23bb8 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/UserThumbnails.lua @@ -0,0 +1,48 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) +local FetchUserThumbnail = require(Modules.Shell.Actions.FetchUserThumbnail) +local SetUserThumbnail = require(Modules.Shell.Actions.SetUserThumbnail) +local ResetUserThumbnails = require(Modules.Shell.Actions.ResetUserThumbnails) + +return function(state, action) + state = state or {} + + if action.type == FetchUserThumbnail.name then + local rbxuid = action.rbxuid + local thumbnailType = action.thumbnailType + local thumbnailSize = action.thumbnailSize + local thumbnailId = table.concat{ rbxuid, thumbnailType.Name, thumbnailSize.Name } + local thumbnailData = state[thumbnailId] or {} + state = Immutable.Set(state, thumbnailId, Immutable.Set(thumbnailData, "isFetching", true)) + elseif action.type == SetUserThumbnail.name then + local rbxuid = action.rbxuid + local thumbnailType = action.thumbnailType + local thumbnailSize = action.thumbnailSize + local thumbnailId = table.concat{ rbxuid, thumbnailType.Name, thumbnailSize.Name } + local thumbnailData = state[thumbnailId] or {} + --add fetchSuccess as fetchSuccess will indicate the last fetchSuccess or not (We can deduce it from imageUrl and lastUpdated, but it's more clear to have this in store) + if action.success and action.isFinal then --Only update image if fetch success and is final image + state = Immutable.Set(state, thumbnailId, + Immutable.JoinDictionaries(thumbnailData, { + fetchSuccess = true, + isFetching = false, + imageUrl = action.imageUrl, + lastUpdated = action.timestamp + }) + ) + else + state = Immutable.Set(state, thumbnailId, + Immutable.JoinDictionaries(thumbnailData, { + fetchSuccess = false, + isFetching = false, + --We need lastUpdated time + lastUpdated = action.timestamp + }) + ) + end + elseif action.type == ResetUserThumbnails.name then + state = {} + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/UserThumbnails.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/UserThumbnails.spec.lua new file mode 100644 index 0000000..6143b8b --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/UserThumbnails.spec.lua @@ -0,0 +1,183 @@ +return function() + local UserThumbnailsReducer = require(script.Parent.UserThumbnails) + local Actions = script.Parent.Parent.Actions + + local FetchUserThumbnail = require(Actions.FetchUserThumbnail) + local SetUserThumbnail = require(Actions.SetUserThumbnail) + local ResetUserThumbnails = require(Actions.ResetUserThumbnails) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = UserThumbnailsReducer(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("Action FetchUserThumbnail", function() + it("should set the user's thumbnail table based on rbxuid, thumbnailType and thumbnailSize, make the isFetching to be true", function() + local action = FetchUserThumbnail({ rbxuid = 12345, thumbnailType = Enum.ThumbnailType.AvatarThumbnail, thumbnailSize = Enum.ThumbnailSize.Size420x420 }) + local state = UserThumbnailsReducer(nil, action) + local thumbnailId = table.concat{ action.rbxuid, action.thumbnailType.Name, action.thumbnailSize.Name } + + expect(state).to.be.a("table") + expect(state[thumbnailId]).to.be.a("table") + expect(state[thumbnailId].isFetching).to.equal(true) + end) + + it("should concat the thumbnail tables", function() + local thumbnailIds = {} + local action = FetchUserThumbnail({ rbxuid = 12345, thumbnailType = Enum.ThumbnailType.AvatarThumbnail, thumbnailSize = Enum.ThumbnailSize.Size420x420 }) + table.insert(thumbnailIds, table.concat{ action.rbxuid, action.thumbnailType.Name, action.thumbnailSize.Name }) + + local state = UserThumbnailsReducer(nil, action) + action = FetchUserThumbnail({ rbxuid = 12345, thumbnailType = Enum.ThumbnailType.HeadShot, thumbnailSize = Enum.ThumbnailSize.Size180x180 }) + table.insert(thumbnailIds, table.concat{ action.rbxuid, action.thumbnailType.Name, action.thumbnailSize.Name }) + + state = UserThumbnailsReducer(state, action) + action = FetchUserThumbnail({ rbxuid = 12345, thumbnailType = Enum.ThumbnailType.AvatarThumbnail, thumbnailSize = Enum.ThumbnailSize.Size180x180 }) + table.insert(thumbnailIds, table.concat{ action.rbxuid, action.thumbnailType.Name, action.thumbnailSize.Name }) + + state = UserThumbnailsReducer(state, action) + action = FetchUserThumbnail({ rbxuid = 54321, thumbnailType = Enum.ThumbnailType.AvatarThumbnail, thumbnailSize = Enum.ThumbnailSize.Size180x180 }) + table.insert(thumbnailIds, table.concat{ action.rbxuid, action.thumbnailType.Name, action.thumbnailSize.Name }) + state = UserThumbnailsReducer(state, action) + + expect(state).to.be.a("table") + for _, thumbnailId in ipairs(thumbnailIds) do + expect(state[thumbnailId]).to.be.a("table") + expect(state[thumbnailId].isFetching).to.equal(true) + end + end) + end) + + describe("Action SetUserThumbnail", function() + it("should set the user's thumbnail table based on rbxuid, fetchSuccess, lastUpdated, thumbnailTypeand thumbnailSize, make the isFetching to be false and set thumbnail assets if fetch success and is final image", + function() + local action = SetUserThumbnail( + { + rbxuid = 12345, + thumbnailType = Enum.ThumbnailType.HeadShot, + thumbnailSize = Enum.ThumbnailSize.Size180x180, + success = true, + isFinal = true, + imageUrl = "x", + timestamp = tick() + }) + local thumbnailId = table.concat{ action.rbxuid, action.thumbnailType.Name, action.thumbnailSize.Name } + local state = UserThumbnailsReducer(nil, action) + + expect(state).to.be.a("table") + expect(state[thumbnailId]).to.be.a("table") + expect(state[thumbnailId].isFetching).to.equal(false) + expect(state[thumbnailId].fetchSuccess).to.equal(true) + expect(state[thumbnailId].imageUrl).to.equal("x") + expect(state[thumbnailId].lastUpdated).to.be.a("number") + end) + + it("should set the user's thumbnail table based on rbxuid, fetchSuccess, lastUpdated, thumbnailType and thumbnailSize, make the isFetching to be false w/o set thumbnail assets if fetch failed or isn't final image", + function() + local action = SetUserThumbnail( + { + rbxuid = 12345, + thumbnailType = Enum.ThumbnailType.HeadShot, + thumbnailSize = Enum.ThumbnailSize.Size180x180, + success = true, + isFinal = false, + imageUrl = "y", + timestamp = tick() + }) + local thumbnailId = table.concat{ action.rbxuid, action.thumbnailType.Name, action.thumbnailSize.Name } + local state = UserThumbnailsReducer(nil, action) + expect(state).to.be.a("table") + expect(state[thumbnailId]).to.be.a("table") + expect(state[thumbnailId].isFetching).to.equal(false) + expect(state[thumbnailId].fetchSuccess).to.equal(false) + expect(state[thumbnailId].imageUrl).never.to.be.ok() + expect(state[thumbnailId].lastUpdated).to.be.a("number") + + action = SetUserThumbnail( + { + rbxuid = 12345, + thumbnailType = Enum.ThumbnailType.HeadShot, + thumbnailSize = Enum.ThumbnailSize.Size180x180, + success = true, + isFinal = true, + imageUrl = "x", + timestamp = tick() + }) + thumbnailId = table.concat{ action.rbxuid, action.thumbnailType.Name, action.thumbnailSize.Name } + state = UserThumbnailsReducer(state, action) + expect(state).to.be.a("table") + expect(state[thumbnailId]).to.be.a("table") + expect(state[thumbnailId].isFetching).to.equal(false) + expect(state[thumbnailId].fetchSuccess).to.equal(true) + expect(state[thumbnailId].imageUrl).to.equal("x") + expect(state[thumbnailId].lastUpdated).to.be.a("number") + + action = SetUserThumbnail( + { + rbxuid = 12345, + thumbnailType = Enum.ThumbnailType.HeadShot, + thumbnailSize = Enum.ThumbnailSize.Size180x180, + success = false, + timestamp = tick() + }) + thumbnailId = table.concat{ action.rbxuid, action.thumbnailType.Name, action.thumbnailSize.Name } + state = UserThumbnailsReducer(state, action) + expect(state).to.be.a("table") + expect(state[thumbnailId]).to.be.a("table") + expect(state[thumbnailId].isFetching).to.equal(false) + expect(state[thumbnailId].fetchSuccess).to.equal(false) + expect(state[thumbnailId].imageUrl).to.equal("x") + expect(state[thumbnailId].lastUpdated).to.be.a("number") + + action = SetUserThumbnail( + { + rbxuid = 12345, + thumbnailType = Enum.ThumbnailType.HeadShot, + thumbnailSize = Enum.ThumbnailSize.Size180x180, + success = true, + isFinal = false, + imageUrl = "y", + timestamp = tick() + }) + thumbnailId = table.concat{ action.rbxuid, action.thumbnailType.Name, action.thumbnailSize.Name } + state = UserThumbnailsReducer(state, action) + expect(state).to.be.a("table") + expect(state[thumbnailId]).to.be.a("table") + expect(state[thumbnailId].isFetching).to.equal(false) + expect(state[thumbnailId].fetchSuccess).to.equal(false) + expect(state[thumbnailId].imageUrl).to.equal("x") + expect(state[thumbnailId].lastUpdated).to.be.a("number") + end) + end) + + describe("Action ResetUserThumbnails", function() + it("should reset the state to initial state after reset", function() + local action = FetchUserThumbnail({ rbxuid = 12345, thumbnailType = Enum.ThumbnailType.HeadShot, thumbnailSize = Enum.ThumbnailSize.Size180x180 }) + local state = UserThumbnailsReducer(nil, action) + action = ResetUserThumbnails() + state = UserThumbnailsReducer(state, action) + + expect(state).to.be.a("table") + expect(next(state)).never.to.be.ok() + + action = SetUserThumbnail( + { + rbxuid = 12345, + thumbnailType = Enum.ThumbnailType.HeadShot, + thumbnailSize = Enum.ThumbnailSize.Size180x180, + success = true, + isFinal = true, + imageUrl = "x", + timestamp = tick() + }) + state = UserThumbnailsReducer(nil, action) + action = ResetUserThumbnails() + state = UserThumbnailsReducer(state, action) + + expect(state).to.be.a("table") + expect(next(state)).never.to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/XboxUser.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/XboxUser.lua new file mode 100644 index 0000000..668be2c --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/XboxUser.lua @@ -0,0 +1,15 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local SetXboxUser = require(Modules.Shell.Actions.SetXboxUser) + +return function(state, action) + state = state or {} + + if action.type == SetXboxUser.name then + return { + gamertag = action.gamertag, + xuid = action.xuid + } + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Reducers/XboxUser.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/XboxUser.spec.lua new file mode 100644 index 0000000..8b2d012 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Reducers/XboxUser.spec.lua @@ -0,0 +1,38 @@ +return function() + local XboxUserReducer = require(script.Parent.XboxUser) + local Actions = script.Parent.Parent.Actions + + local SetXboxUser = require(Actions.SetXboxUser) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = XboxUserReducer(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("Action SetXboxUser", function() + it("should set the gamertag and xuid values in the store", function() + local action = SetXboxUser({gamertag = "TestGamerTag", xuid = 12345}) + local state = XboxUserReducer(state, action) + + expect(state).to.be.a("table") + expect(state.gamertag).to.be.a("string") + expect(state.gamertag).to.equal("TestGamerTag") + expect(state.xuid).to.be.a("number") + expect(state.xuid).to.equal(12345) + end) + + it("should clear the gamertag and xuid values to nil when passed an empty SetXboxUser action", function() + local action = SetXboxUser({gamertag = "TestGamerTag", xuid = 12345}) + local state = XboxUserReducer(state, action) + + action = SetXboxUser({}) + local state = XboxUserReducer(state, action) + + expect(state).to.be.a("table") + expect(state.gamertag).to.equal(nil) + expect(state.xuid).to.equal(nil) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/ReloaderManager.lua b/Client2018/content/internal/AppShell/Modules/Shell/ReloaderManager.lua new file mode 100644 index 0000000..5085bf5 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/ReloaderManager.lua @@ -0,0 +1,182 @@ +local HttpService = game:GetService('HttpService') +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) +local ThirdPartyUserService = nil +pcall( + function() + ThirdPartyUserService = game:GetService("ThirdPartyUserService") + end +) + +local currentUserState = nil +if ThirdPartyUserService then + ThirdPartyUserService.ActiveUserSignedOut:connect( + function() + currentUserState = {} + end + ) +end + +local reloaders = {} + +local ReloaderManager = {} +do + --[Reloader]-- + --Add + function ReloaderManager:addReloader(reloaderName) + if (reloaders[reloaderName] == nil) then + reloaders[reloaderName] = {} + end + end + + --Call + function ReloaderManager:callReloader(reloaderName, internal) + if reloaders[reloaderName] == nil then return end + for key, value in pairs(reloaders[reloaderName]) do + ReloaderManager:callReloaderFunc(reloaderName, key, internal) + end + end + + --Remove + function ReloaderManager:removeReloader(reloaderName) + if reloaders[reloaderName] == nil then return end + for key, value in pairs(reloaders[reloaderName]) do + reloaders[reloaderName][key] = nil + end + reloaders[reloaderName] = nil + end + + --[Reloader Func]-- + local function GetReloaderFuncObj(reloaderId, funcId) + if not reloaderId or not funcId then return end + if reloaders[reloaderId] and reloaders[reloaderId][funcId] then + return reloaders[reloaderId][funcId] + end + end + + --Add reloaderfunc, will excute the reloaderFunc at intervals + --Each reloader may has several reloaderFuncs since we may want to refresh different element at different intervals + --reloaderName: the name of reloader, usually describle what the reloader works for + --reloaderFunc: function to be called when reloader + --interval: interval to reload(seconds) + function ReloaderManager:addReloaderFunc(reloaderName, reloaderFunc, interval, userRelated) + --Don't make duplicated reloaderfuncs + if reloaders[reloaderName] then + return + end + + ReloaderManager:addReloader(reloaderName) + + --We use GUID to differentiate reloaderfuncs, avoid to make redundant thread + local funcId = HttpService:GenerateGUID() + reloaders[reloaderName][funcId] = + { + reloaderFunc = reloaderFunc, + interval = interval, + userRelated = userRelated, + lastUpdateTime = nil, + suspend = false, + mutex = false, + lastUserState = userRelated and currentUserState + }; + + --Reload logic + spawn(function() + while GetReloaderFuncObj(reloaderName, funcId) do + local ReloaderFuncObj = GetReloaderFuncObj(reloaderName, funcId) + local realInterval = ReloaderFuncObj.interval + if ReloaderFuncObj.lastUpdateTime then + --If we made any manually call between intervals, we should use the ReloaderFuncObj.lastUpdateTime as the new + --start time to make sure the data is always not outdated + local timeToLastUpdate = tick() - ReloaderFuncObj.lastUpdateTime + if timeToLastUpdate < realInterval then + realInterval = realInterval - timeToLastUpdate + end + end + wait(realInterval) + + local lastUserState = ReloaderFuncObj.lastUserState + local userRelated = ReloaderFuncObj.userRelated + if not userRelated or lastUserState == currentUserState then + ReloaderManager:callReloaderFunc(reloaderName, funcId, true) + else + ReloaderManager:removeReloaderFunc(reloaderName, funcId) + end + end + end) + + return funcId + end + + --Remove + function ReloaderManager:removeReloaderFunc(reloaderName, funcId) + if reloaders[reloaderName] == nil or reloaders[reloaderName][funcId] == nil then + return + end + reloaders[reloaderName][funcId] = nil + end + + --Update(interval, reloaderFunc, suspend) + function ReloaderManager:updateReloaderFunc(reloaderName, funcId, newAttrs) + local Reloader = GetReloaderFuncObj(reloaderName, funcId) + if not Reloader then + return + end + + if newAttrs and type(newAttrs) == 'table' then + for k, v in pairs(newAttrs) do + Reloader[k] = v + end + end + end + + function ReloaderManager:suspendReloaderFunc(reloaderName, funcId) + ReloaderManager:updateReloaderFunc(reloaderName, funcId, {suspend = true}) + end + + function ReloaderManager:resumeReloaderFunc(reloaderName, funcId) + ReloaderManager:updateReloaderFunc(reloaderName, funcId, {suspend = false}) + end + + --Call reloaderFunc, default is a manually call on reloaderFunc + function ReloaderManager:callReloaderFunc(reloaderName, funcId, internal) + local ReloaderFuncObj = GetReloaderFuncObj(reloaderName, funcId) + + if ReloaderFuncObj and ReloaderFuncObj.reloaderFunc then + local CallFunc = false + --Call inside Reloader Manager + if internal then + if not ReloaderFuncObj.suspend and not ReloaderFuncObj.mutex then + if not ReloaderFuncObj.lastUpdateTime or tick() - ReloaderFuncObj.lastUpdateTime >= ReloaderFuncObj.interval then + CallFunc = true + end + end + else --Get Called from outside, manually call + if not ReloaderFuncObj.mutex then + CallFunc = true + end + end + + + if CallFunc then + ReloaderFuncObj.mutex = true + ReloaderFuncObj.lastUpdateTime = tick() + ReloaderFuncObj.reloaderFunc() + ReloaderFuncObj.mutex = false + end + end + end + + --Call all reloader funcs on resume + if not UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows then + pcall(function() PlatformService.Resumed:connect(function() + for _, reloader in pairs(reloaders) do + spawn(function() + ReloaderManager:callReloader(reloader, true) + end) + end + end) + end) + end +end +return ReloaderManager diff --git a/Client2018/content/internal/AppShell/Modules/Shell/ReportOverlay.lua b/Client2018/content/internal/AppShell/Modules/Shell/ReportOverlay.lua new file mode 100644 index 0000000..781c76b --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/ReportOverlay.lua @@ -0,0 +1,166 @@ +--[[ + // ReportOverlay.lua +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local AssetManager = require(ShellModules:FindFirstChild('AssetManager')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local BaseOverlay = require(ShellModules:FindFirstChild('BaseOverlay')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Http = require(ShellModules:FindFirstChild('Http')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local ReportOverlay = {} + +ReportOverlay.ReportType = { + REPORT_GAME = 0; +} + +local REPORT_COMMENT = "Game reported from the Xbox App."; + +function ReportOverlay:CreateReportOverlay(reportType, assetId) + local this = BaseOverlay() + + local DefaultButtonColor = GlobalSettings.GreyButtonColor + local SelectedButtonColor = GlobalSettings.GreySelectedButtonColor + local DefaultButtonTextColor = GlobalSettings.WhiteTextColor + local SelectedButtonTextColor = GlobalSettings.TextSelectedColor + + local submitButton = Utility.Create'ImageButton' + { + Name = "SubmitButton"; + Size = UDim2.new(0, 320, 0, 66); + Position = UDim2.new(0, 776, 1, -100 - 66); + BackgroundTransparency = 1; + ImageColor3 = DefaultButtonColor; + Image = GlobalSettings.RoundCornerButtonImage; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28)); + ZIndex = 2; + + SoundManager:CreateSound('MoveSelection'); + AssetManager.CreateShadow(1) + } + local submitText = Utility.Create'TextLabel' + { + Name = "SubmitText"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = DefaultButtonTextColor; + Text = Strings:LocalizedString("SubmitWord"); + ZIndex = submitButton.ZIndex; + } + Utility.ResizeButtonWithText(submitButton, submitText, GlobalSettings.TextHorizontalPadding) + + local cancelButton = submitButton:Clone() + local cancelText = submitText:Clone() + + cancelButton.Position = UDim2.new(cancelButton.Position.X.Scale, submitButton.Position.X.Offset + submitButton.Size.X.Offset + 10, + cancelButton.Position.Y.Scale, cancelButton.Position.Y.Offset) + cancelText.Text = Strings:LocalizedString("CancelWord"); + Utility.ResizeButtonWithText(cancelButton, cancelText, GlobalSettings.TextHorizontalPadding) + + submitText.Parent = submitButton + submitButton.Parent = this.Container + cancelText.Parent = cancelButton + cancelButton.Parent = this.Container + + submitButton.SelectionGained:connect(function() + submitButton.ImageColor3 = SelectedButtonColor + submitText.TextColor3 = SelectedButtonTextColor + end) + submitButton.SelectionLost:connect(function() + submitButton.ImageColor3 = DefaultButtonColor + submitText.TextColor3 = DefaultButtonTextColor + end) + cancelButton.SelectionGained:connect(function() + cancelButton.ImageColor3 = SelectedButtonColor + cancelText.TextColor3 = SelectedButtonTextColor + end) + cancelButton.SelectionLost:connect(function() + cancelButton.ImageColor3 = DefaultButtonColor + cancelText.TextColor3 = DefaultButtonTextColor + end) + + local titleText = Utility.Create'TextLabel' + { + Name = "TitleText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, submitButton.Position.X.Offset, 0, 136); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.HeaderSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = Strings:LocalizedString("ReportGameWord"); + TextXAlignment = Enum.TextXAlignment.Left; + Parent = this.Container; + } + local descriptionText = Utility.Create'TextLabel' + { + Name = "DescriptionText"; + Size = UDim2.new(0, 762, 0, 304); + Position = UDim2.new(0, titleText.Position.X.Offset, 0, titleText.Position.Y.Offset + 62); + BackgroundTransparency = 1; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.TitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextWrapped = true; + Text = Strings:LocalizedString("ReportPhrase"); + Parent = this.Container; + } + + local reportIcon = Utility.Create'ImageLabel' + { + Name = "ReportIcon"; + Position = UDim2.new(0, 226, 0, 204); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Shell/Icons/ErrorIconLargeCopy@1080.png"; + Size = UDim2.new(0,321,0,264); + } + this:SetImage(reportIcon) + + submitButton.MouseButton1Click:connect(function() + if this:Close() then + if assetId then + spawn(function() + local result = Http.ReportAbuseAsync("Asset", assetId, 7, REPORT_COMMENT) + end) + end + end + end) + cancelButton.MouseButton1Click:connect(function() + this:Close() + end) + + function this:GetAnalyticsInfo() + local analyticsInfo = {} + analyticsInfo[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('ReportOverlayId') + if assetId then + analyticsInfo.AssetId = assetId + end + return analyticsInfo + end + + function this:GetPriority() + return GlobalSettings.ElevatedPriority + end + + local baseFocus = this.Focus + function this:Focus() + baseFocus(this) + Utility.SetSelectedCoreObject(cancelButton) + end + + return this +end + +return ReportOverlay diff --git a/Client2018/content/internal/AppShell/Modules/Shell/RobuxBalanceOverlay.lua b/Client2018/content/internal/AppShell/Modules/Shell/RobuxBalanceOverlay.lua new file mode 100644 index 0000000..23b259e --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/RobuxBalanceOverlay.lua @@ -0,0 +1,172 @@ +--[[ + // RobuxBalanceOverlay.lua +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local TextService = game:GetService('TextService') + +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local BaseOverlay = require(ShellModules:FindFirstChild('BaseOverlay')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local CurrencyWidgetModule = require(ShellModules:FindFirstChild('CurrencyWidget')) +local UserData = require(ShellModules:FindFirstChild('UserData')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local function createRobuxBalanceOverlay(platformBalance, totalBalance) + local this = BaseOverlay() + + local currencyWidget = nil + local onRobuxChangedCn = nil + + local overlayImage = Utility.Create'ImageLabel' + { + Name = "OverlayImage"; + Size = UDim2.new(0, 416, 0, 416); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/AlertIcon.png'; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + } + this:SetImage(overlayImage) + + local titleText = Utility.Create'TextLabel' + { + Name = "TitleText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, this.RightAlign, 0, 88); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.HeaderSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = Strings:LocalizedString("RobuxBalanceOverlayTitle"); + TextXAlignment = Enum.TextXAlignment.Left; + Parent = this.Container; + } + local descriptionText = Utility.Create'TextLabel' + { + Name = "DescriptionText"; + Size = UDim2.new(0, 762, 0, 126); + Position = UDim2.new(0, this.RightAlign, 0, titleText.Position.Y.Offset + 62); + BackgroundTransparency = 1; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.TitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextWrapped = true; + Text = Strings:LocalizedString("RobuxBalanceOverlayPhrase"); + Parent = this.Container; + } + local platformBalanceTitle = Utility.Create'TextLabel' + { + Name = "platformBalanceTitle"; + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.TitleSize; + TextXAlignment = Enum.TextXAlignment.Left; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = Strings:LocalizedString("PlatformBalanceTitle"); + Parent = this.Container; + } + local offsetSize = TextService:GetTextSize(platformBalanceTitle.Text, 42, GlobalSettings.RegularFont, Vector2.new()) + platformBalanceTitle.Size = UDim2.new(0, offsetSize.x, 0, offsetSize.y) + platformBalanceTitle.AnchorPoint = Vector2.new(0, 0.5) + platformBalanceTitle.Position = UDim2.new(0, this.RightAlign, 0, descriptionText.Position.Y.Offset + descriptionText.Size.Y.Offset + 60) + + local totalBalanceTitle = platformBalanceTitle:Clone() + totalBalanceTitle.Name = "TotalBalanceTitle" + totalBalanceTitle.Text = Strings:LocalizedString("TotalBalanceTitle"); + totalBalanceTitle.Parent = this.Container + offsetSize = TextService:GetTextSize(totalBalanceTitle.Text, 42, GlobalSettings.RegularFont, Vector2.new()) + totalBalanceTitle.Size = UDim2.new(0, offsetSize.x, 0, offsetSize.y) + totalBalanceTitle.AnchorPoint = Vector2.new(0, 0.5) + totalBalanceTitle.Position = UDim2.new(0, this.RightAlign, 0, platformBalanceTitle.Position.Y.Offset + platformBalanceTitle.Size.Y.Offset + 16) + + local platformBalanceText = Utility.Create'TextLabel' + { + Name = "XboxBalanceText"; + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.TitleSize; + TextXAlignment = Enum.TextXAlignment.Left; + TextColor3 = GlobalSettings.GreenTextColor; + Parent = this.Container; + } + + local function setPlatformBalance(newBalance) + platformBalanceText.Text = Utility.FormatNumberString(newBalance) + offsetSize = TextService:GetTextSize(platformBalanceText.Text, 42, GlobalSettings.RegularFont, Vector2.new()) + platformBalanceText.Size = UDim2.new(0, offsetSize.x, 0, offsetSize.y) + platformBalanceText.AnchorPoint = Vector2.new(0, 0.5) + platformBalanceText.Position = UDim2.new(0, this.RightAlign + platformBalanceTitle.Size.X.Offset + 8, 0, platformBalanceTitle.Position.Y.Offset) + end + setPlatformBalance(platformBalance) + + local totalBalanceText = platformBalanceText:Clone() + totalBalanceText.Name = "TotalBalanceText" + totalBalanceText.Parent = this.Container + + local function setTotalBalance(newBalance) + totalBalanceText.Text = Utility.FormatNumberString(newBalance) + offsetSize = TextService:GetTextSize(totalBalanceText.Text, 42, GlobalSettings.RegularFont, Vector2.new()) + totalBalanceText.Size = UDim2.new(0, offsetSize.x, 0, offsetSize.y) + totalBalanceText.Position = UDim2.new(0, this.RightAlign + totalBalanceTitle.Size.X.Offset + 8, 0, totalBalanceTitle.Position.Y.Offset) + end + setTotalBalance(totalBalance) + + local okButton = Utility.Create'TextButton' + { + Name = "OkButton"; + Size = UDim2.new(0, 320, 0, 66); + Position = UDim2.new(0, titleText.Position.X.Offset, 1, -66 - 55); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.BlueButtonColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = GlobalSettings.TextSelectedColor; + Text = Strings:LocalizedString("OkWord"); + Parent = this.Container; + + SoundManager:CreateSound('MoveSelection'); + } + Utility.ResizeButtonWithText(okButton, okButton, GlobalSettings.TextHorizontalPadding) + + --[[ Input Events ]]-- + okButton.MouseButton1Click:connect(function() + this:Close() + end) + + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('RobuxBalanceOverlayId')} + end + + local baseFocus = this.Focus + function this:Focus() + baseFocus(this) + Utility.SetSelectedCoreObject(okButton) + + -- listen to robux changing + if not currencyWidget then + currencyWidget = CurrencyWidgetModule() + end + onRobuxChangedCn = Utility.DisconnectEvent(onRobuxChangedCn) + onRobuxChangedCn = currencyWidget.RobuxChanged:connect(function(newPlatformBalance) + setPlatformBalance(newPlatformBalance) + setTotalBalance(UserData.GetTotalUserBalanceAsync()) + end) + end + + function this:ScreenRemoved() + onRobuxChangedCn = Utility.DisconnectEvent(onRobuxChangedCn) + currencyWidget:Destroy() + currencyWidget = nil + end + + return this +end + +return createRobuxBalanceOverlay diff --git a/Client2018/content/internal/AppShell/Modules/Shell/SafeAsync.lua b/Client2018/content/internal/AppShell/Modules/Shell/SafeAsync.lua new file mode 100644 index 0000000..c2ae164 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/SafeAsync.lua @@ -0,0 +1,106 @@ +local ThirdPartyUserService = nil +pcall( + function() + ThirdPartyUserService = game:GetService("ThirdPartyUserService") + end +) + +local currentUserState = nil +if ThirdPartyUserService then + ThirdPartyUserService.ActiveUserSignedOut:connect( + function() + currentUserState = {} + end + ) +end + +--A generic module to make safe async function calls +--[[ + When to use: Have an async call which may be called several times simultaneously + I.e, don't want to block user input(can't use debounce) and don't want to pile up the calls which may lead to extremely long respond time(can't use mutex) + Can use the following makeSafeAsync which takes in an async function, a callback function(optinal) and an boolean(optinal) which indicates whether the reponse is user related. + If several async calls happened concurrently, only the latest async call's return value will be used as the arguments of the callback(if provided). + Set userRelated as true if want to cancel callback if user switch happens. + Call Cancel() if you want to cancel the callback manually. + + Example: + local httpAsync = makeSafeAsync + { + asyncFunc = local function() return http.getAsync() end, --This is the async function + callback = local function(reponse) process(reponse) end, --This is the the callback(optinal), take the return of asyncFunc as arguments + userRelated = true --user related bool(optinal) + } + + spawn(httpAsync) + spawn(httpAsync) --The first call's callback won't get called now + httpAsync:Cancel() --The second call's callback is cancenlled +]] +local function makeSafeAsync(input) + local this = {} + local currentFuncState = nil + --The asyncFunc should always be the same + local asyncFunc = input.asyncFunc + assert(type(asyncFunc) == "function", "Must init with an async function.") + + --Many async funtion calls are user related, so we add this attribute + local userRelated = input.userRelated + + local callback = input.callback + local cancelled = false + --By default, we don't have the retry logic + local retries = input.retries or 0 + --By default, the retry function return false and will terminate retry + local retryFunc = input.retryFunc or function() return false end + --By default, the wait function will wait exponential of tryCount + local waitFunc = input.waitFunc or function(tryCount) wait(tryCount * tryCount) end + --Add this cancel which enables us to cancel callback + function this:Cancel() + cancelled = true + end + + setmetatable(this, { + __call = function(self, ...) + local lastFuncState = {} + currentFuncState = lastFuncState + + local lastUserState = currentUserState + + local function terminate() + if currentFuncState ~= lastFuncState then + return true + end + + if userRelated and lastUserState ~= currentUserState then + return true + end + + if cancelled then + return true + end + end + + local results = {asyncFunc(...)} + local tryCount = 1 + local terminated = terminate() + while not terminated and tryCount <= retries and retryFunc(unpack(results)) do + waitFunc(tryCount) + tryCount = tryCount + 1 + terminated = terminate() + if not terminated then + results = {asyncFunc(...)} + terminated = terminate() + end + end + + if not terminated then + if type(callback) == "function" then + callback(unpack(results)) + end + end + end + }) + + return this +end + +return makeSafeAsync diff --git a/Client2018/content/internal/AppShell/Modules/Shell/SafeAsync.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/SafeAsync.spec.lua new file mode 100644 index 0000000..123d66a --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/SafeAsync.spec.lua @@ -0,0 +1,100 @@ +return function() + local MakeSafeAsync = require(script.Parent.SafeAsync) + + describe("initial state", function() + it("should return an initial table when init with async function", function() + local safeAsync = MakeSafeAsync({asyncFunc = function() end}) + expect(safeAsync).to.be.a("table") + end) + end) + + describe("async function", function() + it("async function gets called after calling the safeAsync object", function() + local asyncCalledCounter = 0 + local safeAsync = MakeSafeAsync({asyncFunc = function() asyncCalledCounter = asyncCalledCounter + 1 end}) + safeAsync() + expect(asyncCalledCounter).to.equal(1) + end) + end) + + --TODO: add more unit tests like: "if async function gets called multiple times before return, only the latest callback will be called" + --If Async Test is supported + describe("callback", function() + it("callback gets called after async function returns", function() + local callbackCalledCounter = 0 + local safeAsync = MakeSafeAsync({ + asyncFunc = function() end, + callback = function() callbackCalledCounter = callbackCalledCounter + 1 end + }) + safeAsync() + expect(callbackCalledCounter).to.equal(1) + end) + + it("async function return values will be passed to callback as arguments", function() + local safeAsync = MakeSafeAsync({ + asyncFunc = function() return true, 1, {} end, + callback = function(b, n, t) + expect(b).to.equal(true) + expect(n).to.equal(1) + expect(t).to.be.a("table") + end + }) + safeAsync() + end) + end) + + + describe("retry logic", function() + it("no retry by default", function() + local asyncCalledCounter = 0 + local safeAsync = MakeSafeAsync( + { + asyncFunc = function() asyncCalledCounter = asyncCalledCounter + 1 end + }) + safeAsync() + expect(asyncCalledCounter).to.equal(1) + end) + + it("retryFunc gets called with return values from async function", function() + local safeAsync = MakeSafeAsync( + { + asyncFunc = function() return true, 1, {} end, + retryFunc = function(b, n, t) + expect(b).to.equal(true) + expect(n).to.equal(1) + expect(t).to.be.a("table") + end + }) + safeAsync() + end) + + it("async function gets called additional times if set up retries and proper retryFunc", function() + local asyncCalledCounter = 0 + local retries = 3 + local safeAsync = MakeSafeAsync( + { + asyncFunc = function() asyncCalledCounter = asyncCalledCounter + 1 end, + waitFunc = function() end, + retryFunc = function() return true end, + retries = retries + }) + safeAsync() + expect(asyncCalledCounter).to.equal(1 + retries) + end) + end) + + describe("cancel", function() + it("callback won't get called if the task was cancelled", function() + local callbackCalledCounter = 0 + local safeAsync = nil + safeAsync = MakeSafeAsync({ + asyncFunc = function() + safeAsync:Cancel() + end, + callback = function() callbackCalledCounter = callbackCalledCounter + 1 end + }) + safeAsync() + expect(callbackCalledCounter).to.equal(0) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/SafeAsyncRodux.lua b/Client2018/content/internal/AppShell/Modules/Shell/SafeAsyncRodux.lua new file mode 100644 index 0000000..557475f --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/SafeAsyncRodux.lua @@ -0,0 +1,76 @@ +--A generic module to make safe async function calls for rodux async actions +--Similar to makeSafeAsync, but it requests the store whenever get called +--Will put the store as the first parameter for asyncFunc and callback +--Also provide a retry logic if set up retries(retry times), retryFunc(optional - whether retry) and waitFunc(optional - a wait function between retries) +local function makeSafeAsyncRodux(input) + local this = {} + local currentFuncState = nil + + --The asyncFunc should always be the same + local asyncFunc = input.asyncFunc + assert(type(asyncFunc) == "function", "Must init with an async function.") + + --Many async funtion calls are user related, so we add this attribute + local userRelated = input.userRelated + + local callback = input.callback + --By default, we don't have the retry logic + local retries = input.retries or 0 + --By default, the retry function return false and will terminate retry + local retryFunc = input.retryFunc or function() return false end + --By default, the wait function will wait exponential of tryCount + local waitFunc = input.waitFunc or function(tryCount) wait(tryCount * tryCount) end + local cancelled = false + --Add this cancel which enables us to cancel callback + function this:Cancel() + cancelled = true + end + + setmetatable(this, { + __call = function(self, store, ...) + assert(type(store) == "table", "Must call with the store.") + + local lastFuncState = {} + currentFuncState = lastFuncState + + local lastUserState = store:getState().RobloxUser + + local function terminate() + if currentFuncState ~= lastFuncState then + return true + end + + if userRelated and lastUserState ~= store:getState().RobloxUser then + return true + end + + if cancelled then + return true + end + end + + local results = {asyncFunc(store, ...)} + local tryCount = 1 + local terminated = terminate() + while not terminated and tryCount <= retries and retryFunc(store, unpack(results)) do + waitFunc(tryCount) + tryCount = tryCount + 1 + terminated = terminate() + if not terminated then + results = {asyncFunc(store, ...)} + terminated = terminate() + end + end + + if not terminated then + if type(callback) == "function" then + callback(store, unpack(results)) + end + end + end + }) + + return this +end + +return makeSafeAsyncRodux diff --git a/Client2018/content/internal/AppShell/Modules/Shell/SafeAsyncRodux.spec.lua b/Client2018/content/internal/AppShell/Modules/Shell/SafeAsyncRodux.spec.lua new file mode 100644 index 0000000..380a04c --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/SafeAsyncRodux.spec.lua @@ -0,0 +1,130 @@ +return function() + local MakeSafeAsyncRodux = require(script.Parent.SafeAsyncRodux) + local Store = + { + getState = function() + return "user1" + end + } + describe("initial state", function() + it("should return an initial table when init with async function", function() + local safeAsyncRodux = MakeSafeAsyncRodux({asyncFunc = function() end}) + expect(safeAsyncRodux).to.be.a("table") + end) + end) + + describe("async function", function() + it("async function gets called after calling the safeAsyncRodux object", function() + local asyncCalledCounter = 0 + local safeAsyncRodux = MakeSafeAsyncRodux({asyncFunc = function() asyncCalledCounter = asyncCalledCounter + 1 end}) + safeAsyncRodux(Store) + expect(asyncCalledCounter).to.equal(1) + end) + + it("async function gets called with the store as the first argument", function() + local safeAsyncRodux = MakeSafeAsyncRodux( + { + asyncFunc = function(store) + expect(store == Store).to.equal(true) + end + }) + safeAsyncRodux(Store) + end) + end) + + --TODO: add more unit tests like: "if async function gets called multiple times before return, only the latest callback will be called" + --When Async Test is supported + describe("callback", function() + it("callback gets called after async function returns", function() + local callbackCalledCounter = 0 + local safeAsyncRodux = MakeSafeAsyncRodux( + { + asyncFunc = function() end, + callback = function() callbackCalledCounter = callbackCalledCounter + 1 end + }) + safeAsyncRodux(Store) + expect(callbackCalledCounter).to.equal(1) + end) + + it("callback gets called with the store as the first argument", function() + local safeAsyncRodux = MakeSafeAsyncRodux( + { + asyncFunc = function() return true, 1, {} end, + callback = function(store) + expect(store == Store).to.equal(true) + end + }) + safeAsyncRodux(Store) + end) + + it("async function return values will be passed to callback as arguments after store", function() + local safeAsyncRodux = MakeSafeAsyncRodux( + { + asyncFunc = function() return true, 1, {} end, + callback = function(store, b, n, t) + expect(store == Store).to.equal(true) + expect(b).to.equal(true) + expect(n).to.equal(1) + expect(t).to.be.a("table") + end + }) + safeAsyncRodux(Store) + end) + end) + + describe("retry logic", function() + it("no retry by default", function() + local asyncCalledCounter = 0 + local safeAsyncRodux = MakeSafeAsyncRodux( + { + asyncFunc = function() asyncCalledCounter = asyncCalledCounter + 1 end + }) + safeAsyncRodux(Store) + expect(asyncCalledCounter).to.equal(1) + end) + + it("retryFunc gets called with the store as the first argument, following by return values from async function", function() + local safeAsyncRodux = MakeSafeAsyncRodux( + { + asyncFunc = function() return true, 1, {} end, + retryFunc = function(store, b, n, t) + expect(store == Store).to.equal(true) + expect(b).to.equal(true) + expect(n).to.equal(1) + expect(t).to.be.a("table") + end + }) + safeAsyncRodux(Store) + end) + + it("async function gets called additional times if set up retries and proper retryFunc", function() + local asyncCalledCounter = 0 + local retries = 3 + local safeAsyncRodux = MakeSafeAsyncRodux( + { + asyncFunc = function() asyncCalledCounter = asyncCalledCounter + 1 end, + waitFunc = function() end, + retryFunc = function() return true end, + retries = retries + }) + safeAsyncRodux(Store) + expect(asyncCalledCounter).to.equal(1 + retries) + end) + end) + + describe("cancel", function() + it("callback won't get called if the task was cancelled", function() + local callbackCalledCounter = 0 + local safeAsyncRodux = nil + safeAsyncRodux = MakeSafeAsyncRodux( + { + asyncFunc = function() + safeAsyncRodux:Cancel() + end, + callback = function() callbackCalledCounter = callbackCalledCounter + 1 end + }) + safeAsyncRodux(Store) + expect(callbackCalledCounter).to.equal(0) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/ScreenManager.lua b/Client2018/content/internal/AppShell/Modules/Shell/ScreenManager.lua new file mode 100644 index 0000000..f420a7a --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/ScreenManager.lua @@ -0,0 +1,273 @@ +-- Written by Kip Turner, Copyright Roblox 2015 + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) + +local XboxRoduxScreenFocus = settings():GetFFlag("XboxRoduxScreenFocus") + +local ScreenManager = {} + +local ScreenStack = {} +local ScreenToHideMap = {} + +local ScreenGuis = {[1] = GuiRoot} + +local function ContainsScreenInternal(screen) + local foundScreenIndex = nil + for i, otherScreen in pairs(ScreenStack) do + if otherScreen == screen then + foundScreenIndex = i + end + end + + return foundScreenIndex +end + +local function GetScreenPriorityInternal(screen) + local priority = GlobalSettings.DefaultPriority + if screen.GetPriority ~= nil then + priority = screen:GetPriority() + end + return priority +end + +local function SetRBXEventStream_Screen(screen, status) + if screen and type(screen.GetAnalyticsInfo) == "function" then + local screenAnalyticsInfo = screen:GetAnalyticsInfo() + if type(screenAnalyticsInfo) == "table" and screenAnalyticsInfo[Analytics.WidgetNames('WidgetId')] then + screenAnalyticsInfo.Status = status + Analytics.SetRBXEventStream("Widget", screenAnalyticsInfo) + end + end +end + +function ScreenManager:GetInsertIndexForScreen(screen) + local screenPriority = GetScreenPriorityInternal(screen) + local currentScreen = self:GetTopScreen() + while currentScreen and GetScreenPriorityInternal(currentScreen) > screenPriority do + currentScreen = self:GetScreenBelow(currentScreen) + end + if currentScreen then + local currentScreenIndex = ContainsScreenInternal(currentScreen) + if currentScreenIndex then + return currentScreenIndex + 1 + end + end + return 1 +end + +function ScreenManager:GetGuiRoot() + return GuiRoot +end + +function ScreenManager:GetScreenGuiByPriority(priority) + priority = math.max(1, priority) + if not ScreenGuis[priority] then + for i = 1, priority do + if not ScreenGuis[i] then + ScreenGuis[i] = Utility.Create'ScreenGui' + { + Name = 'AppShell' .. tostring(i); + Parent = CoreGui; + } + end + end + end + + return ScreenGuis[priority] +end + +-- TODO: handle race conditions for opening multiple screens +local openScreenEntryCount = 0 +local openScreenExitCount = 0 +function ScreenManager:OpenScreen(screen, hideCurrent) + if openScreenEntryCount ~= openScreenExitCount then + Utility.DebugLog("ScreenManager: OpenScreen Re-entry detected" , openScreenEntryCount, openScreenExitCount) + end + openScreenEntryCount = openScreenEntryCount + 1 + + if hideCurrent == nil then + hideCurrent = true + end + + local currentScreen = self:GetTopScreen() + local insertIndex = self:GetInsertIndexForScreen(screen) + local isNewTop = insertIndex > #ScreenStack + + if not isNewTop then + hideCurrent = false + end + + if currentScreen ~= screen then + local foundScreenIndex = ContainsScreenInternal(screen) + + if foundScreenIndex then + table.remove(ScreenStack, foundScreenIndex) + end + + if currentScreen then + currentScreen:RemoveFocus() + if hideCurrent then + currentScreen:Hide() + end + ScreenToHideMap[currentScreen] = hideCurrent + end + + table.insert(ScreenStack, insertIndex, screen) + + if isNewTop then + if ScreenToHideMap[screen] ~= false then + screen:Show() + SetRBXEventStream_Screen(screen, "Show") + end + + if screen == self:GetTopScreen() then + screen:Focus() + SetRBXEventStream_Screen(screen, "Focus") + end + else + ScreenToHideMap[screen] = true + end + end + + openScreenExitCount = openScreenExitCount + 1 +end + +local closeCurrentEntryCount = 0 +local closeCurrentExitCount = 0 +function ScreenManager:CloseCurrent() + if closeCurrentEntryCount ~= closeCurrentExitCount then + Utility.DebugLog("ScreenManager: CloseScreen Re-entry detected" , closeCurrentEntryCount, closeCurrentExitCount) + end + closeCurrentEntryCount = closeCurrentEntryCount + 1 + + local currentScreen = ScreenStack[#ScreenStack] + local belowScreen = currentScreen and self:GetScreenBelow(currentScreen) + if currentScreen then + currentScreen:Hide() + currentScreen:RemoveFocus() + if currentScreen.ScreenRemoved then + currentScreen:ScreenRemoved() + end + SetRBXEventStream_Screen(currentScreen, "Close") + table.remove(ScreenStack, #ScreenStack) + ScreenToHideMap[currentScreen] = nil + end + + + -- if belowScreen and belowScreen ~= self:GetTopScreen() then return end + local newTop = belowScreen + if newTop and newTop == self:GetTopScreen() then + local showNewTop = (ScreenToHideMap[newTop] == true) + -- spawn(function() + if showNewTop then + newTop:Show() + SetRBXEventStream_Screen(newTop, "Show") + end + if newTop == self:GetTopScreen() then + newTop:Focus() + SetRBXEventStream_Screen(newTop, "Focus") + end + -- end) + end + closeCurrentExitCount = closeCurrentExitCount + 1 +end + +function ScreenManager:ContainsScreen(screen) + local index = ContainsScreenInternal(screen) + if index then + return ScreenStack[index] + end + return nil +end + +function ScreenManager:GetScreenBelow(screen) + local thisScreenIndex = ContainsScreenInternal(screen) + if thisScreenIndex then + return ScreenStack[thisScreenIndex - 1] + end + return nil +end + +function ScreenManager:GetTopScreen() + return ScreenStack[#ScreenStack] +end + + +----- TWEENS ----- + +local function FadeInElement(element, tweeners) + if element == nil then return end + if element:IsA('ImageLabel') or element:IsA('ImageButton') then + table.insert(tweeners, Utility.PropertyTweener(element, 'ImageTransparency', 1, element.ImageTransparency, 0.5, Utility.EaseOutQuad)) + end + if element:IsA('GuiObject') then + table.insert(tweeners, Utility.PropertyTweener(element, 'BackgroundTransparency', 1, element.BackgroundTransparency, 0.5, Utility.EaseOutQuad)) + end + if element:IsA('TextLabel') or element:IsA('TextBox') or element:IsA('TextButton') then + table.insert(tweeners, Utility.PropertyTweener(element, 'TextTransparency', 1, element.TextTransparency, 0.5, Utility.EaseOutQuad)) + end + for _, child in pairs(element:GetChildren()) do + FadeInElement(child, tweeners) + end +end + +function ScreenManager:FadeInSitu(guiObject) + local tweeners = {} + if guiObject then + FadeInElement(guiObject, tweeners) + end + return tweeners +end + +function ScreenManager:DefaultFadeIn(guiObject) + local tweeners = {} + + if guiObject then + table.insert(tweeners, Utility.PropertyTweener(guiObject, 'Position', guiObject.Position + UDim2.new(0.15, 0, 0, 0), guiObject.Position, 0.5, + function(t,b,c,d) + if t >= d then return b + c end + t = t / d; + local tComputed = t*(t-2) + return -UDim2.new(c.X.Scale * tComputed, c.X.Offset * tComputed, c.Y.Scale * tComputed, c.Y.Offset * tComputed) + b + end)) + + FadeInElement(guiObject, tweeners) + end + + return tweeners +end + +function ScreenManager:DefaultCancelFade(tweens) + if tweens then + for _, tween in pairs(tweens) do + tween:Finish() + end + end + + return nil +end + +function ScreenManager:PlayDefaultOpenSound() + SoundManager:Play('ScreenChange') +end + +-- replace current ScreenManager with new APIs +if XboxRoduxScreenFocus then + local screenManager2 = require(ShellModules.ScreenManager2) + + ScreenManager.OpenScreen = screenManager2.OpenScreen + ScreenManager.CloseCurrent = screenManager2.CloseCurrent + ScreenManager.GetTopScreen = screenManager2.GetTopScreen + ScreenManager.ContainsScreen = screenManager2.ContainsScreen +end + + +return ScreenManager diff --git a/Client2018/content/internal/AppShell/Modules/Shell/ScreenManager2.lua b/Client2018/content/internal/AppShell/Modules/Shell/ScreenManager2.lua new file mode 100644 index 0000000..1b99826 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/ScreenManager2.lua @@ -0,0 +1,180 @@ +--[[ + This manager class allows the current screens to work with Roact screens + using the new screen list that is implemented in Rodux. This allows + certain screen lifecycle events to be respected when crossing the boundaries + of the two systems. + + For example, when the controller lost overlay pops up, this allows both + a Roact screen and a current screen to correctly take focus when + the overlay is dismissed. + + This will only be used by the older screen system. It will keep the API + compatable with the current ScreenManager +]] +local Analytics = require(script.Parent.Analytics) +local AppState = require(script.Parent.AppState) +local GlobalSettings = require(script.Parent.GlobalSettings) +local ScreenItem = require(script.Parent.Models.ScreenItem) + +local InsertScreen = require(script.Parent.Actions.InsertScreen) +local RemoveScreen = require(script.Parent.Actions.RemoveScreen) + +local ScreenManager = {} + +local ScreenMap = {} + +local function setRBXEventStream_Screen(screen, status) + if screen and type(screen.GetAnalyticsInfo) == "function" then + local screenAnalyticsInfo = screen:GetAnalyticsInfo() + if type(screenAnalyticsInfo) == "table" and screenAnalyticsInfo[Analytics.WidgetNames('WidgetId')] then + screenAnalyticsInfo.Status = status + Analytics.SetRBXEventStream("Widget", screenAnalyticsInfo) + end + end +end + +local function getScreenPriority(screen) + local priority = GlobalSettings.DefaultPriority + if screen.GetPriority ~= nil then + priority = screen:GetPriority() + end + + return priority +end + +function ScreenManager:ContainsScreen(desiredScreen) + for _,item in pairs(ScreenMap) do + if item.screen == desiredScreen then + return true + end + end + + return false +end + +function ScreenManager:GetTopScreen() + local screenList = AppState.store:getState().ScreenList + if screenList and #screenList > 0 then + local frontScreen = screenList[1] + return ScreenMap[frontScreen.id].screen + end + + return nil +end + +function ScreenManager:OpenScreen(screen, hidePrevious) + if hidePrevious == nil then + hidePrevious = true + end + + local data = { + hidePrevious = hidePrevious, + } + + local id = tostring(screen) + ScreenMap[id] = { + screen = screen, + isShown = false, + } + + local screenItem = ScreenItem.new(id, getScreenPriority(screen), data) + AppState.store:dispatch(InsertScreen(screenItem)) +end + +function ScreenManager:CloseCurrent() + local screenList = AppState.store:getState().ScreenList + local frontScreen = #screenList > 0 and screenList[1] + + if not frontScreen or not ScreenMap[frontScreen.id] then + return + end + + AppState.store:dispatch(RemoveScreen(frontScreen)) +end + +local function handleScreensRemoved(screenList) + local currentListToMap = {} + for _, item in ipairs(screenList) do + currentListToMap[item.id] = true + end + + local idsToRemove = {} + for id,_ in pairs(ScreenMap) do + if not currentListToMap[id] then + table.insert(idsToRemove, id) + end + end + + for _, id in ipairs(idsToRemove) do + local screenItem = ScreenMap[id] + if screenItem then + local screen = screenItem.screen + + screen:RemoveFocus() + screen:Hide() + + if screen.ScreenRemoved then + screen:ScreenRemoved() + end + + setRBXEventStream_Screen(screen, "Close") + end + + ScreenMap[id] = nil + end +end + +local function handleScreensAdded(screenList) + for i = #screenList, 1, -1 do + local screenListItem = screenList[i] + + local screenMapItem = ScreenMap[screenListItem.id] + if screenMapItem then + local screen = screenMapItem.screen + if i > 1 then + local doHide = true + local prevListItem = screenList[i - 1] + if prevListItem and prevListItem.data then + doHide = prevListItem.data.hidePrevious + end + + screen:RemoveFocus() + if doHide then + screen:Hide() + screenMapItem.isShown = false + end + + if not screenMapItem.isShown and not doHide then + screen:Show() + screenMapItem.isShown = true + end + else + if not screenMapItem.isShown then + screen:Show() + screenMapItem.isShown = true + setRBXEventStream_Screen(screen, "Show") + end + screen:Focus() + setRBXEventStream_Screen(screen, "Focus") + end + end + end +end + +local function update(screenList) + handleScreensRemoved(screenList) + handleScreensAdded(screenList) +end + +AppState.store.changed:connect(function(newState, oldState) + local currentScreenList = newState.ScreenList + local previousScreenList = oldState.ScreenList + + if currentScreenList == previousScreenList then + return + end + + update(currentScreenList) +end) + +return ScreenManager diff --git a/Client2018/content/internal/AppShell/Modules/Shell/ScrollingGrid.lua b/Client2018/content/internal/AppShell/Modules/Shell/ScrollingGrid.lua new file mode 100644 index 0000000..73ee80f --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/ScrollingGrid.lua @@ -0,0 +1,885 @@ +-- Written by Kip Turner, Copyright Roblox 2015 + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local GuiService = game:GetService('GuiService') + +local function ScrollingGrid(config) + local this = {} + this.Enum = + { + ScrollDirection = {["Vertical"] = 1; ["Horizontal"] = 2;}; + StartCorner = {["UpperLeft"] = 1; ["UpperRight"] = 2; ["BottomLeft"] = 3; ["BottomRight"] = 4;}; + } + + this.GridItems = {} + this.ItemSet = {} + + --Config + config = config or {} + this.ScrollDirection = (config.ScrollDirection and this.Enum.ScrollDirection[config.ScrollDirection]) or this.Enum.ScrollDirection.Vertical + this.StartCorner = (config.StartCorner and this.Enum.StartCorner[config.StartCorner]) or this.Enum.StartCorner.UpperLeft + this.FixedRowColumnCount = config.FixedRowColumnCount + this.CellSize = config.CellSize or Vector2.new(100,100) + this.Padding = config.Padding or Vector2.new(0,0) + this.Spacing = config.Spacing or Vector2.new(0,0) + --Whether the content in scorlling area is dynamic + this.Dynamic = config.Dynamic or false + + --build the base guis + local ContainerAttributes = + { + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 0, 0); + CanvasSize = UDim2.new(1, 0, 1, 0); + Name = "ScrollingGridContainer"; + BackgroundTransparency = 1; + ClipsDescendants = true; + Visible = true; + ScrollingEnabled = false; + ScrollBarThickness = 0; + Selectable = false; + } + + for key, value in pairs(config) do + if ContainerAttributes[key] ~= nil then + ContainerAttributes[key] = value + end + end + + local container = Utility.Create'ScrollingFrame'(ContainerAttributes) + + + this.Container = container + + this.DefaultSelection = this.Container + + this.Container:GetPropertyChangedSignal('AbsoluteSize'):connect(function() + this:RecalcLayout() + end) + + --[Common APIs]-- + function this:GetPadding() + return self.Padding + end + + function this:SetPadding(newPadding) + if newPadding ~= self.Padding then + self.Padding = newPadding + self:RecalcLayout() + end + end + + function this:GetSpacing() + return self.Spacing + end + + function this:SetSpacing(newSpacing) + if newSpacing ~= self.Spacing then + self.Spacing = newSpacing + self:RecalcLayout() + end + end + + function this:GetCellSize() + return self.CellSize + end + + function this:GetGridItemSize() + return self.CellSize + end + + function this:SetCellSize(cellSize) + if cellSize ~= self.CellSize then + self.CellSize = cellSize + self:RecalcLayout() + end + end + + function this:GetScrollDirection() + return self.ScrollDirection + end + + function this:SetScrollDirection(newDirection) + if newDirection ~= self.ScrollDirection then + self.ScrollDirection = newDirection + self:RecalcLayout() + end + end + + function this:GetStartCorner() + return self.StartCorner + end + + function this:SetStartCorner(newStartCorner) + if newStartCorner ~= self.StartCorner then + self.StartCorner = newStartCorner + self:RecalcLayout() + end + end + + function this:GetRowColumnConstraint() + return self.FixedRowColumnCount + end + + function this:SetRowColumnConstraint(fixedRowColumnCount) + if fixedRowColumnCount < 1 then + fixedRowColumnCount = nil + end + if fixedRowColumnCount ~= self.FixedRowColumnCount then + self.FixedRowColumnCount = fixedRowColumnCount + self:RecalcLayout() + end + end + + + function this:GetClipping() + return self.Container.ClipsDescendants + end + + function this:SetClipping(clippingEnabled) + self.Container.ClipsDescendants = clippingEnabled; + end + + function this:GetVisible() + return self.Container.Visible + end + + function this:SetVisible(isVisible) + self.Container.Visible = isVisible; + end + + function this:GetSize() + return self.Container.Size + end + + function this:SetSize(size) + self.Container.Size = size + end + + function this:GetPosition() + return self.Container.Position + end + + function this:SetPosition(position) + self.Container.Position = position + end + + function this:GetParent() + return self.Container.Parent + end + + function this:SetParent(parent) + self.Container.Parent = parent + end + + function this:GetGuiObject() + return self.Container + end + + -- Default selection handles the case of removing the last item in the grid while it is selected + -- Set to nil if do not want a default selection + function this:SetDefaultSelection(selectionObject) + self.DefaultSelection = selectionObject + end + + function this:ResetDefaultSelection() + self.DefaultSelection = self.Container + end + + function this:ContainsItem(gridItem) + return self.ItemSet[gridItem] ~= nil + end + + function this:FindAncestorGridItem(object) + if object ~= nil then + if self:ContainsItem(object) then + return object + end + return self:FindAncestorGridItem(object.Parent) + end + end + + function this:Get2DGridIndex(index) + -- 0 base index + local zerobasedIndex = index - 1 + local rows, columns = self:GetNumRowsColumns() + local row, column + + -- TODO: implement StartCorner here + if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then + row = math.floor(zerobasedIndex / columns) + column = zerobasedIndex % columns + else + column = math.floor(zerobasedIndex / rows) + row = zerobasedIndex % rows + end + + return row, column + end + + function this:GetGridPosition(row, column) + local cellSize = self:GetCellSize() + local spacing = self:GetSpacing() + local padding = self:GetPadding() + return UDim2.new(0, padding.X + column * cellSize.X + column * spacing.X, + 0, padding.Y + row * cellSize.Y + row * spacing.Y) + end + + function this:GetCanvasPositionForOffscreenItem(selectedObject) + -- NOTE: using <= and >= instead of < and > because scrollingframe + -- code may automatically bump it while we are observing the change + if selectedObject and self.Container and self:ContainsItem(selectedObject) then + if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then + if selectedObject.AbsolutePosition.Y <= self.Container.AbsolutePosition.Y then + return Utility.ClampCanvasPosition(self.Container, Vector2.new(0, selectedObject.Position.Y.Offset)) -- - selectedObject.AbsoluteSize.Y/2)) + elseif selectedObject.AbsolutePosition.Y + selectedObject.AbsoluteSize.Y >= self.Container.AbsolutePosition.Y + self.Container.AbsoluteWindowSize.Y then + return Utility.ClampCanvasPosition(self.Container, Vector2.new(0, -(self.Container.AbsoluteWindowSize.Y - selectedObject.Position.Y.Offset - selectedObject.AbsoluteSize.Y) )) --+ selectedObject.AbsoluteSize.Y/2)) + end + else -- Horizontal + if selectedObject.AbsolutePosition.X <= self.Container.AbsolutePosition.X then + return Utility.ClampCanvasPosition(self.Container, Vector2.new(selectedObject.Position.X.Offset, 0)) + elseif selectedObject.AbsolutePosition.X + selectedObject.AbsoluteSize.X >= self.Container.AbsolutePosition.X + self.Container.AbsoluteWindowSize.X then + return Utility.ClampCanvasPosition(self.Container, Vector2.new(-(self.Container.AbsoluteWindowSize.X - selectedObject.Position.X.Offset - selectedObject.AbsoluteSize.X), 0)) + end + end + end + end + + function this:Destroy() + if self.Container then + self.Container:Destroy() + end + end + + + ----Make API based on the scrolling grid type + if not this.Dynamic then + function this:GetFirstVisibleItem() + local firstVisibleItem = nil + for i = #self.GridItems, 1, -1 do + local item = self.GridItems[i] + if item then + if item.Position.X.Offset >= self.Container.CanvasPosition.X and + item.Position.X.Offset + item.AbsoluteSize.X <= self.Container.CanvasPosition.X + self.Container.AbsoluteWindowSize.X then + firstVisibleItem = item + end + end + end + return firstVisibleItem + end + + function this:SortItems(sortFunc) + table.sort(self.GridItems, sortFunc) + self:RecalcLayout() + + local selectedObject = self:FindAncestorGridItem(GuiService.SelectedCoreObject) + if selectedObject and self:ContainsItem(selectedObject) then + local thisPos = self:GetCanvasPositionForOffscreenItem(selectedObject) + if thisPos then + Utility.PropertyTweener(self.Container, 'CanvasPosition', thisPos, thisPos, 0, Utility.EaseOutQuad, true) + end + end + end + + function this:AddItem(gridItem) + if not self:ContainsItem(gridItem) then + table.insert(self.GridItems, gridItem) + self.ItemSet[gridItem] = true + gridItem.Parent = self.Container + if GuiService.SelectedCoreObject == self.DefaultSelection then + Utility.SetSelectedCoreObject(gridItem) + end + self:RecalcLayout() + end + end + + function this:RemoveItem(gridItem) + if self:ContainsItem(gridItem) then + for i, otherItem in pairs(self.GridItems) do + if otherItem == gridItem then + table.remove(self.GridItems, i) + -- Assign a new selection + if GuiService.SelectedCoreObject == gridItem then + GuiService.SelectedCoreObject = self.GridItems[i] or self.GridItems[i-1] or self.GridItems[1] or self.DefaultSelection + end + -- Clean-up + self.ItemSet[gridItem] = nil + gridItem.Parent = nil + self:RecalcLayout() + return + end + end + end + end + + function this:RemoveAllItems() + local wasSelected = false + do + local currentSelection = GuiService.SelectedCoreObject + while currentSelection ~= nil and wasSelected == false do + wasSelected = wasSelected or self:ContainsItem(currentSelection) + currentSelection = currentSelection.Parent + end + end + for i = #self.GridItems, 1, -1 do + local removed = table.remove(self.GridItems, i) + self.ItemSet[removed] = nil + removed.Parent = nil + end + + if wasSelected then + GuiService.SelectedCoreObject = self.Container + end + + self:RecalcLayout() + self.Container.CanvasPosition = Vector2.new(0, 0) + end + + function this:GetNumRowsColumns() + local rows, columns = 0, 0 + + local windowSize = self.Container.AbsoluteWindowSize + local padding = self:GetPadding() + local cellSize = self:GetCellSize() + local cellSpacing = self:GetSpacing() + local adjustedWindowSize = Utility.ClampVector2(Vector2.new(0, 0), windowSize - padding, windowSize - padding) + local absoluteCellSize = Utility.ClampVector2(Vector2.new(1,1), cellSize + cellSpacing, cellSize + cellSpacing) + local windowSizeCalc = (adjustedWindowSize + cellSpacing) / absoluteCellSize + + if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then + columns = math.max(1, self:GetRowColumnConstraint() or math.floor(windowSizeCalc.x)) + rows = math.ceil(math.max(1, #self.GridItems) / columns) + else + rows = math.max(1, self:GetRowColumnConstraint() or math.floor(windowSizeCalc.y)) + columns = math.ceil(math.max(1, #self.GridItems) / rows) + end + + return rows, columns + end + + function this:RecalcLayout() + local padding = self:GetPadding() + local cellSpacing = self:GetSpacing() + local gridItemSize = self:GetGridItemSize() + local rows, columns = self:GetNumRowsColumns() + + if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then + self.Container.CanvasSize = UDim2.new(self.Container.Size.X.Scale, self.Container.Size.X.Offset, 0, padding.Y * 2 + rows * gridItemSize.Y + (math.max(0, rows - 1)) * cellSpacing.Y) + else + self.Container.CanvasSize = UDim2.new(0, padding.X * 2 + columns * gridItemSize.X + (math.max(0, columns - 1)) * cellSpacing.X, self.Container.Size.Y.Scale, self.Container.Size.Y.Offset) + end + + local grid2DtoIndex = {} + for i = 1, #self.GridItems do + local row, column = self:Get2DGridIndex(i) + local gridItem = self.GridItems[i] + + gridItem.Size = UDim2.new(0, gridItemSize.X, 0, gridItemSize.Y) + gridItem.Position = self:GetGridPosition(row, column, gridItemSize) + + grid2DtoIndex[row] = grid2DtoIndex[row] or {} + grid2DtoIndex[row][column] = gridItem + end + + for rowNum, row in pairs(grid2DtoIndex) do + for columnNum, column in pairs(row) do + local gridItem = grid2DtoIndex[rowNum][columnNum] + if gridItem then + if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then + gridItem.NextSelectionUp = grid2DtoIndex[rowNum - 1] and grid2DtoIndex[rowNum - 1][columnNum] or nil + gridItem.NextSelectionDown = grid2DtoIndex[rowNum + 1] and grid2DtoIndex[rowNum + 1][columnNum] or nil + if gridItem.NextSelectionDown == nil and grid2DtoIndex[rowNum + 1] ~= nil then + gridItem.NextSelectionDown = self.GridItems[#self.GridItems] + end + gridItem.NextSelectionLeft = nil + gridItem.NextSelectionRight = nil + else + gridItem.NextSelectionLeft = grid2DtoIndex[rowNum] and grid2DtoIndex[rowNum][columnNum - 1] or gridItem + gridItem.NextSelectionRight = grid2DtoIndex[rowNum] and grid2DtoIndex[rowNum][columnNum + 1] or nil + if gridItem.NextSelectionRight == nil then + if grid2DtoIndex[0] and grid2DtoIndex[0][columnNum + 1] then + -- Move to the last position + gridItem.NextSelectionRight = self.GridItems[#self.GridItems] + else + -- Avoid selector from moving to other selectable objects + gridItem.NextSelectionRight = gridItem + end + end + gridItem.NextSelectionUp = nil + gridItem.NextSelectionDown = nil + end + end + end + end + end + + do + this:RecalcLayout() + local lastSelectedObject = nil + GuiService:GetPropertyChangedSignal('SelectedCoreObject'):connect(function() + local selectedObject = this:FindAncestorGridItem(GuiService.SelectedCoreObject) + if selectedObject and this:ContainsItem(selectedObject) then + local upDirection = (this.ScrollDirection == this.Enum.ScrollDirection.Vertical) and 'NextSelectionUp' or 'NextSelectionLeft' + local downDirection = (this.ScrollDirection == this.Enum.ScrollDirection.Vertical) and 'NextSelectionDown' or 'NextSelectionRight' + local upObject = selectedObject[upDirection] + local downObject = selectedObject[downDirection] + + local nextPos, upPos, downPos; + + + local gridItemSize = this:GetGridItemSize() + local thisPos = this:GetCanvasPositionForOffscreenItem(selectedObject) + + if lastSelectedObject then + local lastUpObject = lastSelectedObject[upDirection] + local lastDownObject = lastSelectedObject[downDirection] + + if upObject and lastUpObject == selectedObject then + upPos = this:GetCanvasPositionForOffscreenItem(upObject) + if upObject ~= selectedObject and upPos then + upPos = upPos + gridItemSize / 2 + end + elseif downObject and lastDownObject == selectedObject then + downPos = this:GetCanvasPositionForOffscreenItem(downObject) + if downObject ~= selectedObject and downPos then + downPos = downPos - gridItemSize / 2 + end + end + end + + if upPos and (upPos.Y < this.Container.CanvasPosition.Y or upPos.X < this.Container.CanvasPosition.X) then + nextPos = upPos + elseif downPos and (downPos.Y > this.Container.CanvasPosition.Y or downPos.X > this.Container.CanvasPosition.X) then + nextPos = downPos + else + nextPos = thisPos + end + + if nextPos then + nextPos = Utility.ClampCanvasPosition(this.Container, nextPos) + if thisPos then + -- Sort of a hack to not snap on the last one + if (upObject and downObject) then + Utility.PropertyTweener(this.Container, 'CanvasPosition', thisPos, thisPos, 0, Utility.EaseOutQuad, true, function() + Utility.PropertyTweener(this.Container, 'CanvasPosition', this.Container.CanvasPosition, nextPos, 0.2, Utility.EaseOutQuad, true) + end) + end + else + Utility.PropertyTweener(this.Container, 'CanvasPosition', this.Container.CanvasPosition, nextPos, 0.2, Utility.EaseOutQuad, true) + end + end + lastSelectedObject = selectedObject + else + lastSelectedObject = nil + end + end) + end + else + --[APIs for dynamic scrolling grid]-- + --Set Item Call back, the item will be generated when it's in the scrolling area + this.targetCanvasPosition = Vector2.new(0, 0) + this.gridCount = config.gridCount or 0 + + --The selection mode decides how we cope with SelectedCoreObject change + --Middle: Keep the selection in the middle of the scrolling area if possible, make some offset on each side + --TopLeft: Keep the selection on top/left edge of scrolling area if possible + --Normal: Behave like normal scrolling grid + this.Enum.SelectionMode = {["Middle"] = 1; ["TopLeft"] = 2; ["Normal"] = 3;} + this.SelectionMode = (config.SelectionMode and this.Enum.SelectionMode[config.SelectionMode]) or this.Enum.SelectionMode.Normal + + function this:GetItemVisible(item, fully) + if fully then --Whether item is fully visible + if this.ScrollDirection == this.Enum.ScrollDirection.Vertical then + return item.Position.Y.Offset >= self.Container.CanvasPosition.Y and + item.Position.Y.Offset + item.AbsoluteSize.Y <= self.Container.CanvasPosition.Y + self.Container.AbsoluteWindowSize.Y + else + return item.Position.X.Offset >= self.Container.CanvasPosition.X and + item.Position.X.Offset + item.AbsoluteSize.X <= self.Container.CanvasPosition.X + self.Container.AbsoluteWindowSize.X + end + else + if this.ScrollDirection == this.Enum.ScrollDirection.Vertical then + return item.Position.Y.Offset < self.Container.CanvasPosition.Y + self.Container.AbsoluteWindowSize.Y or + item.Position.Y.Offset + item.AbsoluteSize.Y > self.Container.CanvasPosition.Y + else + return item.Position.X.Offset < self.Container.CanvasPosition.X + self.Container.AbsoluteWindowSize.X or + item.Position.X.Offset + item.AbsoluteSize.X > self.Container.CanvasPosition.X + end + end + end + + function this:SetItemCallback(callback, recalc) + self.getItemFunc = callback + if recalc then + self:RecalcLayout() + end + end + + function this:GetNumRowsColumns() + local rows, columns = 0, 0 + + local windowSize = self.Container.AbsoluteWindowSize + local padding = self:GetPadding() + local cellSize = self:GetCellSize() + local cellSpacing = self:GetSpacing() + local adjustedWindowSize = Utility.ClampVector2(Vector2.new(0, 0), windowSize - padding, windowSize - padding) + local absoluteCellSize = Utility.ClampVector2(Vector2.new(1,1), cellSize + cellSpacing, cellSize + cellSpacing) + local windowSizeCalc = (adjustedWindowSize + cellSpacing) / absoluteCellSize + + local GridItemsCount = 0 + for _, item in pairs(self.GridItems) do + if item then + GridItemsCount = GridItemsCount + 1 + end + end + + if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then + columns = math.max(1, math.floor(windowSizeCalc.x)) + rows = math.ceil(math.max(1, GridItemsCount) / columns) + else + rows = math.max(1, math.floor(windowSizeCalc.y)) + columns = math.ceil(math.max(1, GridItemsCount) / rows) + end + + return rows, columns + end + + function this:GetIndexFrom2D(row, column) + local rows, columns = self:GetNumRowsColumns() + + local result = 0; + + if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then + result = (row-1) * columns + column + else + result = (column-1) * rows + row + end + + return result + end + + function this:GetItemRowColumnFromScreenPosition(x, y) + local cellSize = self:GetCellSize() + local spacing = self:GetSpacing() + local padding = self:GetPadding() + + local cellWidth = (spacing.X + cellSize.X) + local cellHeight = (spacing.Y + cellSize.Y) + + return math.floor(y / cellHeight) + 1, math.floor(x / cellWidth) + 1 + end + + function this:Add(index, gridItem) + self.GridItems[index] = gridItem + gridItem.Parent = self.Container + self.ItemSet[gridItem] = true + end + + function this:Remove(index) + local gridItem = self.GridItems[index] + self.GridItems[index] = nil + if gridItem then + gridItem.Parent = nil + if GuiService.SelectedCoreObject == gridItem then + Utility.SetSelectedCoreObject(nil) + end + self.ItemSet[gridItem] = nil + end + end + + function this:GetActiveItemsRange(overwriteWindowSize) + local windowSize = overwriteWindowSize or self.Container.AbsoluteWindowSize + local windowWidth = windowSize.X + local windowHeight = windowSize.Y + local canvasPosition = self.targetCanvasPosition + local x = canvasPosition.X + local y = canvasPosition.Y + local cellSize = self:GetCellSize() + local spacing = self:GetSpacing() + local padding = self:GetPadding() + + local cellWidth = (spacing.X + cellSize.X) + local cellHeight = (spacing.Y + cellSize.Y) + + local left = math.floor( x / cellWidth ) + 1 + local right = math.ceil( (x + windowWidth) / cellWidth ) + + local top = math.floor( y / cellHeight ) + 1 + local bottom = math.ceil( (y + windowHeight) / cellHeight ) + + local firstIndex = self:GetIndexFrom2D(top, left) + local lastIndex = self:GetIndexFrom2D(bottom, right) + + local rows, columns = self:GetNumRowsColumns() + + -- add a row/column of padding on either side + if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then + lastIndex = lastIndex + columns + firstIndex = firstIndex - columns + else + lastIndex = lastIndex + rows + firstIndex = firstIndex - rows + end + + -- clamp to 1... + if firstIndex < 1 then firstIndex = 1 end + if lastIndex < 1 then lastIndex = 1 end + + return firstIndex, lastIndex + end + + --Re-allocate the griditems based on (target) canvasposition + function this:Rewindow(overwriteWindowSize) + if self.getItemFunc then + local removeMe = {} + local moveMe = {} + + for index, gridItem in pairs(self.GridItems) do + removeMe[gridItem] = index + end + + local addMe = {} + local firstIndex, lastIndex = self:GetActiveItemsRange(overwriteWindowSize) + for index = firstIndex, lastIndex do + local gridItem = self.getItemFunc(index) + if gridItem then + local prevIndex = removeMe[gridItem] + if prevIndex then --It's already there. + removeMe[gridItem] = nil + if prevIndex ~= index then + moveMe[prevIndex] = index + end + else + addMe[gridItem] = index + end + end + end + + for gridItem, index in pairs(removeMe) do + self:Remove(index) + end + + for fromIndex, toIndex in pairs(moveMe) do + local gridItem = self.GridItems[fromIndex] + addMe[gridItem] = toIndex + self.GridItems[fromIndex] = nil + end + + for gridItem, index in pairs(addMe) do + self:Add(index, gridItem) + end + + for index, gridItem in pairs(self.GridItems) do + local row, column = self:Get2DGridIndex(index) + gridItem.Position = self:GetGridPosition(row, column) + end + end + end + + function this:GetSelectableItem(includeContainer, prevSelectedIndex) + local windowSize = self.Container.AbsoluteWindowSize + local width = windowSize.X + local height = windowSize.Y + local centerRow, centerColumn = self:GetItemRowColumnFromScreenPosition( + self.Container.CanvasPosition.X + width / 2, + self.Container.CanvasPosition.Y + height / 2) + + -- Find the item closest to the top left + local bestGridItem = nil + local score = math.huge + + for i, gridItem in pairs(self.GridItems) do + local newScore = score + if prevSelectedIndex then + --If has prevSelectedIndex, select the gridItem with nearest Index + newScore = math.abs(i - prevSelectedIndex) + else + local row, column = self:Get2DGridIndex(i) + newScore = math.abs(column) + math.abs(row) + end + + -- Favor on-screen items + if gridItem.Position.X.Offset < this.targetCanvasPosition.X or + gridItem.Position.Y.Offset < this.targetCanvasPosition.Y then + newScore = newScore + 10000 + end + + if newScore < score then + bestGridItem = gridItem + score = newScore + end + end + + local selectableItem = bestGridItem or (includeContainer and self.Container) + return selectableItem + end + + + function this:SelectAvailableItem(includeContainer, prevSelectedIndex) + local selectableItem = self:GetSelectableItem(includeContainer, prevSelectedIndex) + if selectableItem then + Utility.SetSelectedCoreObject(selectableItem) + end + end + + function this:Focus() + local selectedObject = self:FindAncestorGridItem(GuiService.SelectedCoreObject) + + if not( selectedObject and self:ContainsItem(selectedObject) ) then + self:SelectAvailableItem() + end + end + + function this:RemoveFocus() + if this:ContainsItem(GuiService.SelectedCoreObject) then + Utility.SetSelectedCoreObject(nil) + end + end + + function this:RecalcLayout(newGridCount) + if newGridCount then + self.gridCount = newGridCount + end + local prevSelectedObject = self:FindAncestorGridItem(GuiService.SelectedCoreObject) + local wasSelected = GuiService.SelectedCoreObject == self.Container or (prevSelectedObject and self:ContainsItem(prevSelectedObject)) + local prevSelectedIndex = nil + if wasSelected and prevSelectedObject then + local prevRow, prevColumn = self:GetItemRowColumnFromScreenPosition(prevSelectedObject.Position.X.Offset, prevSelectedObject.Position.Y.Offset) + prevSelectedIndex = self:GetIndexFrom2D(prevRow, prevColumn) + end + + --Recalc the proper CanvasSize + local padding = self:GetPadding() + local cellSpacing = self:GetSpacing() + local gridItemSize = self:GetGridItemSize() + local rows, columns = self:GetNumRowsColumns() + + --Need overwrite the gridCount as there hasn't been that many grid items in the scrolligGrid when recalc + if self.gridCount then + if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then + rows = math.ceil(math.max(1, self.gridCount) / columns) + else + columns = math.ceil(math.max(1, self.gridCount) / rows) + end + end + + if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then + self.Container.CanvasSize = UDim2.new(self.Container.Size.X.Scale, self.Container.Size.X.Offset, 0, padding.Y * 2 + rows * gridItemSize.Y + (math.max(0, rows - 1)) * cellSpacing.Y) + else + self.Container.CanvasSize = UDim2.new(0, padding.X * 2 + columns * gridItemSize.X + (math.max(0, columns - 1)) * cellSpacing.X, self.Container.Size.Y.Scale, self.Container.Size.Y.Offset) + end + + --The previous target canvasPos may become non-reachable now + self.targetCanvasPosition = Utility.ClampCanvasPosition(self.Container, self.targetCanvasPosition) + + --Re allocate grid items + self:Rewindow() + + local selectedObject = self:FindAncestorGridItem(GuiService.SelectedCoreObject) + if selectedObject and self:ContainsItem(selectedObject) then + --The selected object is still in the scrolling grid + local thisPos = self:GetCanvasPositionForOffscreenItem(selectedObject) + if thisPos then + self.targetCanvasPosition = thisPos + --Here use the tween to overwrite and stop all other tweens, replace this with TweenService + Utility.PropertyTweener(self.Container, 'CanvasPosition', thisPos, thisPos, 0.0, Utility.EaseOutQuad, true, + function() + self:Rewindow() + end) + end + elseif wasSelected then + --Selected object got removed, select the nearest gridItem or container + self:SelectAvailableItem(true, prevSelectedIndex) + end + end + + function this:BackToInitial(duration) + if not duration then + duration = 0 + end + + local origin = Vector2.new(0, 0) + local overwriteWindowSize = self.Container.AbsoluteWindowSize + self.targetCanvasPosition + self.targetCanvasPosition = origin + + -- Rewindow to include all items in grid up to the current position. + self:Rewindow(overwriteWindowSize) + + -- Then animate the move to the origin, and after the animation, rewindow again to fit the view + Utility.PropertyTweener(self.Container, 'CanvasPosition', self.Container.CanvasPosition, origin, duration, Utility.SCurveVector2, true, + function() + self:Rewindow() + end) + end + + GuiService:GetPropertyChangedSignal('SelectedCoreObject'):connect(function() + local selectedObject = this:FindAncestorGridItem(GuiService.SelectedCoreObject) + + if selectedObject and this:ContainsItem(selectedObject) then + local nextPos = nil + local row, column = this:GetItemRowColumnFromScreenPosition(selectedObject.Position.X.Offset, selectedObject.Position.Y.Offset) + local cellSize = this:GetCellSize() + local spacing = this:GetSpacing() + local cellWidth = (spacing.X + cellSize.X) + local cellHeight = (spacing.Y + cellSize.Y) + + if this.SelectionMode == this.Enum.SelectionMode.Middle then + local windowSize = this.Container.AbsoluteWindowSize + local width = windowSize.X + local height = windowSize.Y + local centerRow, centerColumn = this:GetItemRowColumnFromScreenPosition( this.Container.CanvasPosition.X + width / 2, this.Container.CanvasPosition.Y + height / 2) + + if this.ScrollDirection == this.Enum.ScrollDirection.Vertical then + local maxaway = math.floor( (math.floor(height / cellHeight) - 1) / 2 ); + + if row > centerRow + maxaway then + local newCenter = row - maxaway + nextPos = Vector2.new(0, (newCenter-0.5) * cellHeight - height / 2) + elseif row < centerRow - maxaway then + local newCenter = row + maxaway + nextPos = Vector2.new(0, (newCenter-0.5) * cellHeight - height / 2) + end + else + local maxaway = math.floor( (math.floor(width / cellWidth) - 1) / 2 ); + + if column > centerColumn + maxaway then + local newCenter = column - maxaway + nextPos = Vector2.new((newCenter-0.5) * cellWidth - width / 2, 0) + elseif column < centerColumn - maxaway then + local newCenter = column + maxaway + nextPos = Vector2.new((newCenter-0.5) * cellWidth - width / 2, 0) + end + end + elseif this.SelectionMode == this.Enum.SelectionMode.TopLeft then + if this.ScrollDirection == this.Enum.ScrollDirection.Vertical then + nextPos = Vector2.new(0, (row-1) * cellHeight) + else + nextPos = Vector2.new((column-1) * cellWidth, 0) + end + else + nextPos = this:GetCanvasPositionForOffscreenItem(selectedObject) + end + + if nextPos and nextPos.X == this.targetCanvasPosition.X and nextPos.Y == this.targetCanvasPosition.Y then + return + end + + if nextPos then + local oldTargetCanvasPosition = this.targetCanvasPosition or this.Container.CanvasPosition + nextPos = Utility.ClampCanvasPosition(this.Container, nextPos) + this.targetCanvasPosition = nextPos + this:Rewindow() + Utility.PropertyTweener(this.Container, 'CanvasPosition', oldTargetCanvasPosition, nextPos, 0.2, Utility.EaseOutQuad, true) + end + end + end) + end + + return this +end + +return ScrollingGrid diff --git a/Client2018/content/internal/AppShell/Modules/Shell/ScrollingTextBox.lua b/Client2018/content/internal/AppShell/Modules/Shell/ScrollingTextBox.lua new file mode 100644 index 0000000..7169fd3 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/ScrollingTextBox.lua @@ -0,0 +1,240 @@ +--[[ + // ScrollingTextBox.lua + + // Creates a scrolling text box to be used with controlers and selectable + // guis + + // NOTE: Add any api needed to further expand this module +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local TextService = game:GetService('TextService') + +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) + +local createScrollingTextBox = function(size, position, parent) + local this = {} + + local SCROLL_BUFFER = 2 + + this.OnSelectableChanged = Utility.Signal() + + -- adjust selection image + local edgeSelectionImage = Utility.Create'ImageLabel' + { + Name = "EdgeSelectionImage"; + Size = UDim2.new(1, 32, 1, 32); + Position = UDim2.new(0, -16, 0, -16); + Image = 'rbxasset://textures/ui/SelectionBox.png'; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(21,21,41,41); + BackgroundTransparency = 1; + } + + local container = Utility.Create'Frame' + { + Name = "ScrollingTextBox"; + Size = size or UDim2.new(); + Position = position or UDim2.new(); + BackgroundTransparency = 1; + Parent = parent; + } + local scrollingFrame = Utility.Create'ScrollingFrame' + { + Name = "ScrollingBox"; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + ScrollBarThickness = 0; + SelectionImageObject = edgeSelectionImage; + Selectable = false; + Parent = container; + + SoundManager:CreateSound('MoveSelection'); + } + local textLabel = Utility.Create'TextLabel' + { + Name = "TextLabel"; + Size = UDim2.new(1, 0, 4, 0); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.DescriptionSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextWrapped = true; + Text = ""; + Parent = scrollingFrame; + } + local upArrow = Utility.Create'ImageLabel' + { + Name = "UpArrow"; + BackgroundTransparency = 1; + ImageColor3 = GlobalSettings.WhiteTextColor; + Visible = false; + Parent = container; + Image = "rbxasset://textures/ui/Shell/Icons/UpIndicatorIcon@1080.png"; + Size = UDim2.new(0, 20, 0, 18); + Position = UDim2.new(1, -20, 1, 21); + } + local downArrow = Utility.Create'ImageLabel' + { + Name = "DownArrow"; + BackgroundTransparency = 1; + ImageColor3 = GlobalSettings.WhiteTextColor; + Visible = false; + Parent = container; + Image = "rbxasset://textures/ui/Shell/Icons/DownIndicatorIcon@1080.png"; + Size = UDim2.new(0, 20, 0, 18); + Position = UDim2.new(1, -20, 1, 43); + } + + local hintImage = Utility.Create'ImageLabel' + { + Name = "RightStickHint"; + BackgroundTransparency = 1; + ImageColor3 = GlobalSettings.WhiteTextColor; + Visible = false; + Parent = container; + Image = "rbxasset://textures/ui/Shell/Icons/RightStickHint@1080.png"; + Size = UDim2.new(0, 32, 0, 32); + Position = UDim2.new(1, -64, 1, 25); + } + + --[[ Private Functions ]]-- + local function setArrowState() + local canvasPosition = scrollingFrame.CanvasPosition + local maxSizeY = textLabel.AbsoluteSize.y - scrollingFrame.AbsoluteWindowSize.y + if canvasPosition.y >= maxSizeY - SCROLL_BUFFER then + downArrow.ImageColor3 = GlobalSettings.GreyTextColor + else + downArrow.ImageColor3 = GlobalSettings.WhiteTextColor + end + if canvasPosition.y <= SCROLL_BUFFER then + upArrow.ImageColor3 = GlobalSettings.GreyTextColor + else + upArrow.ImageColor3 = GlobalSettings.WhiteTextColor + end + end + + local function setScrollSize() + local textSize = TextService:GetTextSize(textLabel.Text, Utility.ConvertFontSizeEnumToInt(textLabel.FontSize), textLabel.Font, Vector2.new(textLabel.AbsoluteSize.X, 6000)) + local ySize = textSize.y + + textLabel.Size = UDim2.new(1, 0, 0, ySize) + scrollingFrame.CanvasSize = UDim2.new(0, 0, 0, ySize) + local areArrowsVisible = ySize > scrollingFrame.AbsoluteSize.y + this:SetArrowsVisible(areArrowsVisible) + this:SetSelectable(areArrowsVisible) + setArrowState(); + end + + --[[ Events ]]-- + scrollingFrame:GetPropertyChangedSignal('CanvasPosition'):connect(function() + setArrowState() + end) + scrollingFrame:GetPropertyChangedSignal('AbsoluteWindowSize'):connect(function() + setArrowState() + end) + scrollingFrame.SelectionGained:Connect(function() + hintImage.Visible = true + end) + scrollingFrame.SelectionLost:Connect(function() + hintImage.Visible = false + end) + + + --[[ Public API ]]-- + function this:SetParent(newParent) + if scrollingFrame.Parent ~= newParent then + scrollingFrame.Parent = newParent + spawn(function() + setScrollSize() + end) + end + end + + function this:SetPosition(newPosition) + scrollingFrame.Position = newPosition + end + + function this:SetSize(newSize) + if scrollingFrame.Size ~= newSize then + scrollingFrame.Size = newSize + spawn(function() + setScrollSize() + end) + end + end + + function this:SetFontSize(newFontSize) + if textLabel.FontSize ~= newFontSize then + textLabel.FontSize = newFontSize + spawn(function() + setScrollSize() + end) + end + end + + function this:SetFont(newFont) + if textLabel.Font ~= newFont then + textLabel.Font = newFont + spawn(function() + setScrollSize() + end) + end + end + + function this:SetText(text) + if textLabel.Text ~= tostring(text) then + textLabel.Text = tostring(text) + spawn(function() + setScrollSize() + end) + end + end + + function this:SetSelectable(value) + scrollingFrame.Selectable = value + this.OnSelectableChanged:fire(value) + end + + function this:SetZIndex(value) + container.ZIndex = value + textLabel.ZIndex = value + upArrow.ZIndex = value + downArrow.ZIndex = value + hintImage.ZIndex = value + end + + function this:SetArrowsVisible(value) + upArrow.Visible = value + downArrow.Visible = value + end + + function this:GetContainer() + return container + end + + function this:GetSelectableObject() + return scrollingFrame + end + + function this:GetArrowsVisible() + return upArrow.Visible + end + + function this:IsSelectable() + return scrollingFrame.Selectable + end + + return this +end + +return createScrollingTextBox diff --git a/Client2018/content/internal/AppShell/Modules/Shell/SetAccountCredentialsScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/SetAccountCredentialsScreen.lua new file mode 100644 index 0000000..ed5f6cb --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/SetAccountCredentialsScreen.lua @@ -0,0 +1,282 @@ +--[[ + // SetAccountCredentialsScreen.lua +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local ContextActionService = game:GetService('ContextActionService') +local GuiService = game:GetService('GuiService') + +local AccountManager = require(ShellModules:FindFirstChild('AccountManager')) +local BaseSignInScreen = require(ShellModules:FindFirstChild('BaseSignInScreen')) +local Errors = require(ShellModules:FindFirstChild('Errors')) +local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local function createSetAccountCredentialsScreen(title, description, buttonText) + local this = BaseSignInScreen() + + this:SetTitle(title or "") + this:SetDescriptionText(description or "") + this:SetButtonText(buttonText or "") + + local ModalOverlay = Utility.Create'Frame' + { + Name = "ModalOverlay"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = GlobalSettings.ModalBackgroundTransparency; + BackgroundColor3 = GlobalSettings.ModalBackgroundColor; + BorderSizePixel = 0; + ZIndex = 4; + } + + local myUsername = nil + local myPassword = nil + + local isUsernameValid = false + local isPasswordValid = false + + local UserNameProcessingCount = 0 + local PasswordProcessingCount = 0 + + this.UsernameObject:SetDefaultText(Strings:LocalizedString("UsernameWord").." (".. + Strings:LocalizedString("UsernameRulePhrase")..")") + this.UsernameObject:SetKeyboardTitle(Strings:LocalizedString("UsernameWord")) + this.UsernameObject:SetKeyboardDescription(Strings:LocalizedString("UsernameRulePhrase")) + local usernameChangedCn = nil + + this.PasswordObject:SetDefaultText(Strings:LocalizedString("PasswordWord").." (".. + Strings:LocalizedString("PasswordRulePhrase")..")") + this.PasswordObject:SetKeyboardTitle(Strings:LocalizedString("PasswordWord")) + this.PasswordObject:SetKeyboardDescription(Strings:LocalizedString("PasswordRulePhrase")) + if not UserSettings().GameSettings:InStudioMode() then + this.PasswordObject:SetKeyboardType(Enum.XboxKeyBoardType.Password) + end + local passwordChangedCn = nil + + local function createAndSetCredentialsAsync() + local result = nil + + local function signupAsync() + -- legacy check + -- Users may be in state where they are linked, but have not set credentials from previous + -- versions of log in logic. If they are in this state, we correct it by having them set their + -- credentials. Keep this until we can unlink all accounts in this state. + result = AccountManager:HasLinkedAccountAsync() + + if result == AccountManager.AuthResults.Success then + result = AccountManager:SetRobloxCredentialsAsync(myUsername, myPassword) + elseif result == AccountManager.AuthResults.AccountUnlinked then + result = AccountManager:SignupAsync(myUsername, myPassword) + end + + if result == AccountManager.AuthResults.Success then + result = AccountManager:LoginAsync() + end + end + + local loader = LoadingWidget( + { Parent = ModalOverlay }, { signupAsync } ) + + -- set up full screen loader + ModalOverlay.Parent = GuiRoot + ContextActionService:BindCoreAction("BlockB", function() end, false, Enum.KeyCode.ButtonB) + local selectedObject = GuiService.SelectedCoreObject + GuiService.SelectedCoreObject = nil + + -- call loader + loader:AwaitFinished() + + -- clean up + loader:Cleanup() + loader = nil + GuiService.SelectedCoreObject = selectedObject + ContextActionService:UnbindCoreAction("BlockB") + ModalOverlay.Parent = nil + + if result == AccountManager.AuthResults.Success then + EventHub:dispatchEvent(EventHub.Notifications["AuthenticationSuccess"]) + else + local err = result and Errors.Authentication[result] or Errors.Default + ScreenManager:OpenScreen(ErrorOverlay(err), false) + end + end + + local function validatePassword(silent) + PasswordProcessingCount = PasswordProcessingCount + 1 + + local reason = nil + isPasswordValid, reason = AccountManager:IsValidPasswordAsync(myUsername or "", myPassword) + if isPasswordValid then + if myUsername and #myUsername > 0 then + GuiService.SelectedCoreObject = this.SignInButton + else + GuiService.SelectedCoreObject = this.UsernameSelection + end + elseif isPasswordValid == false then + if not silent then + -- web returns long strings on password error. Lets create our own error type + GuiService.SelectedCoreObject = this.PasswordSelection + local err = Errors.Default; + if reason then + err = Errors.SignIn.InvalidPassword + err.Msg = reason + end + ScreenManager:OpenScreen(ErrorOverlay(err), false) + end + else -- Http failure + ScreenManager:OpenScreen(ErrorOverlay(Errors.Default), false) + end + + PasswordProcessingCount = PasswordProcessingCount - 1 + end + + local function validateUsername(silent) + UserNameProcessingCount = UserNameProcessingCount + 1 + + -- 1. Check if valid user name + local reason = nil + isUsernameValid, reason = AccountManager:IsValidUsernameAsync(myUsername) + if isUsernameValid then + -- 2. if password set, need to recheck password rules + if myPassword and #myPassword > 0 then + validatePassword() + else + GuiService.SelectedCoreObject = this.PasswordSelection + end + elseif isUsernameValid == false then + if not silent then + GuiService.SelectedCoreObject = this.UsernameSelection + -- Use the same errorcode to collect all InvalidUsername errors, diffrentiate by the latest reasons return by Web + local err = Errors.Default + if reason then + err = Errors.SignIn.InvalidUsername + err.Msg = reason + end + ScreenManager:OpenScreen(ErrorOverlay(err), false) + end + else -- Http Request failed + ScreenManager:OpenScreen(ErrorOverlay(Errors.Default), false) + end + + UserNameProcessingCount = UserNameProcessingCount - 1 + end + + local function onUsernameChanged(text) + myUsername = text + if #myUsername > 0 then + validateUsername() + else + GuiService.SelectedCoreObject = this.UsernameSelection + isUsernameValid = false + end + end + + local function onPasswordChanged(text) + myPassword = text + if #myPassword > 0 then + validatePassword() + else + GuiService.SelectedCoreObject = this.PasswordSelection + isPasswordValid = false + end + end + + + local isSettingCredentials = false + this.SignInButton.MouseButton1Click:connect(function() + if isSettingCredentials then return end + isSettingCredentials = true + + local function stillValidatingUserInfo() + return UserNameProcessingCount > 0 or PasswordProcessingCount > 0 + end + local function awaitValidatingUserInfo() + while stillValidatingUserInfo() do wait() end + end + + SoundManager:Play('ButtonPress') + + + local processingFunctions = nil + -- Wait for current validation to finish + if stillValidatingUserInfo() then + + processingFunctions = { awaitValidatingUserInfo } + + -- Retry our username and password validation + elseif isUsernameValid == nil or isPasswordValid == nil then + + processingFunctions = {} + if isUsernameValid == nil then + table.insert(processingFunctions, function() validateUsername(true) awaitValidatingUserInfo() end) + end + if isPasswordValid == nil then + table.insert(processingFunctions, function() validatePassword(true) awaitValidatingUserInfo() end) + end + + end + + if processingFunctions then + local processingLoader = LoadingWidget( + { Parent = ModalOverlay }, + processingFunctions) + + -- NOTE: may need to get a separate overlay for this spinner + -- Also should we disable input while overlay is active? + ModalOverlay.Parent = GuiRoot + + processingLoader:AwaitFinished() + processingLoader:Cleanup() + processingLoader = nil + + ModalOverlay.Parent = nil + end + + if isUsernameValid and isPasswordValid then + createAndSetCredentialsAsync() + elseif isUsernameValid == false or isPasswordValid == false then + local err = Errors.SignIn.NoUsernameOrPasswordEntered + ScreenManager:OpenScreen(ErrorOverlay(err), false) + else -- Http failed to validate your password or username + local err = Errors.SignIn.ConnectionFailed + ScreenManager:OpenScreen(ErrorOverlay(err), false) + end + isSettingCredentials = false + end) + + + --[[ Public API ]]-- + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('SetAccountCredentialsScreenId')} + end + + -- override + local baseFocus = this.Focus + function this:Focus() + baseFocus(self) + usernameChangedCn = this.UsernameObject.OnTextChanged:connect(onUsernameChanged) + passwordChangedCn = this.PasswordObject.OnTextChanged:connect(onPasswordChanged) + end + + -- override + local baseRemoveFocus = this.RemoveFocus + function this:RemoveFocus() + baseRemoveFocus(self) + Utility.DisconnectEvent(usernameChangedCn) + Utility.DisconnectEvent(passwordChangedCn) + end + + return this +end + +return createSetAccountCredentialsScreen diff --git a/Client2018/content/internal/AppShell/Modules/Shell/SettingsScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/SettingsScreen.lua new file mode 100644 index 0000000..94c7539 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/SettingsScreen.lua @@ -0,0 +1,118 @@ +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local Templates = ShellModules:FindFirstChild("Templates") + +local SettingsScreenConsole = require(Templates:FindFirstChild('SettingsScreenConsole')) + +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) +local UserInputService = game:GetService('UserInputService') +local ThirdPartyUserService = nil +pcall(function() ThirdPartyUserService = game:GetService('ThirdPartyUserService') end) + +local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen')) +local Errors = require(ShellModules:FindFirstChild('Errors')) +local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local AccountScreen = require(ShellModules:FindFirstChild('AccountScreen')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local function createSettingsScreen() + local this = BaseScreen(true) + + local userInputServiceChangedCn = nil + + function this:GetVersionInfo() + if UserSettings().GameSettings:InStudioMode() or UserInputService:GetPlatform() == Enum.Platform.Windows then + return {Major = 1, Minor = 0, Build = 0, Revision = 0} + elseif PlatformService then + return PlatformService:GetVersionIdInfo(); + end + + return {Major = 1, Minor = 1, Build = 1, Revision = 1} + end + + function this:OpenAccountScreen() + local accountScreen = AccountScreen() + if accountScreen then + accountScreen:SetParent(this.view.Container.Parent) + ScreenManager:OpenScreen(accountScreen, true) + else + ScreenManager:OpenScreen(ErrorOverlay(Errors.Default), false) + end + end + + function this:OpenSwitchProfileScreen() + if UserSettings().GameSettings:InStudioMode() or UserInputService:GetPlatform() == Enum.Platform.Windows then + ScreenManager:OpenScreen(ErrorOverlay(Errors.Test.FeatureNotAvailableInStudio), false) + return + end + + if ThirdPartyUserService then + ThirdPartyUserService:ShowAccountPicker() + end + end + + function this:OpenOverscanScreen() + local RoactScreenManagerWrapper = require(ShellModules.Components.RoactScreenManagerWrapper) + local overscanComponent = require(ShellModules.Components.Overscan.Overscan) + local overscanElement = RoactScreenManagerWrapper.new(overscanComponent, GuiRoot, { + ImageVisible = true, + BackgroundTransparency = 0, + }) + + ScreenManager:OpenScreen(overscanElement) + end + + function this:OpenHelpScreen() + if UserSettings().GameSettings:InStudioMode() or UserInputService:GetPlatform() == Enum.Platform.Windows then + ScreenManager:OpenScreen(ErrorOverlay(Errors.Test.FeatureNotAvailableInStudio), false) + else + pcall(function() + -- errors will be handled by xbox + return PlatformService:PopupHelpUI() + end) + end + end + + local function CreateView() + return SettingsScreenConsole(this) + end + + --[[ Public API ]]-- + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('SettingsScreenId')} + end + + --Override + function this:GetDefaultSelectionObject() + return this.view and this.view:GetDefaultSelectionObject() + end + + local baseHide = this.Hide + function this:Hide() + baseHide(self) + if userInputServiceChangedCn then + userInputServiceChangedCn:disconnect() + userInputServiceChangedCn = nil + end + end + + function this:ScreenRemoved() + spawn(function() + if PlatformService ~= nil then + PlatformService:SaveSettings() + end + end) + end + + this.view = CreateView() + this:SetTitle(Strings:LocalizedString("SettingsWord")) + + return this +end + +return createSettingsScreen diff --git a/Client2018/content/internal/AppShell/Modules/Shell/SideBar.lua b/Client2018/content/internal/AppShell/Modules/Shell/SideBar.lua new file mode 100644 index 0000000..91d872c --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/SideBar.lua @@ -0,0 +1,229 @@ +--[[ + // SideBar.lua + // Creates a side bar to be used for certain pages + // Currently used by: + // GameGenre + // Friends +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local ContextActionService = game:GetService("ContextActionService") +local GuiService = game:GetService('GuiService') + +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local CreateSideBar = function() + local this = {} + + local buttons = {} + local selectedObject = nil + local inFocus = false + + local INSET_Y = 156 + local INSET_X = 65 + local BUTTON_SIZE_Y = 75 + + local modalOverlay = Utility.Create'Frame' + { + Name = "ModalOverlay"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + BackgroundColor3 = GlobalSettings.ModalBackgroundColor; + BorderSizePixel = 0; + ZIndex = 4; + } + + local container = Utility.Create'Frame' + { + Name = "SideBarContainer"; + Size = UDim2.new(0.3, 0, 1, 0); + Position = UDim2.new(1, 0, 0, 0); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.OverlayColor; + ZIndex = 5; + Parent = modalOverlay; + } + local dummySelectionImage = Utility.Create'TextButton' + { + Name = "DummySelectionImage"; + Size = UDim2.new(0, 0, 0, 0); + Visible = false; + + SoundManager:CreateSound('MoveSelection'); + } + local textLabel = Utility.Create'TextLabel' + { + Name = "Text"; + Size = UDim2.new(1, -INSET_X - 100, 1, -INSET_Y); --make it inside the TitleSafeContainer + Position = UDim2.new(0, INSET_X, 0, INSET_Y); + BorderSizePixel = 0; + BackgroundTransparency = 1; + ZIndex = 6; + Text = ""; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.DescriptionSize; + TextWrapped = true; + } + + local function recalcPositions() + for i = 1, #buttons do + buttons[i].Position = UDim2.new(0, 0, 0, INSET_Y + (BUTTON_SIZE_Y * (i - 1))) + end + end + + --[[ Public API ]]-- + --Add closeEvent + local closedEvent = Instance.new("BindableEvent") + closedEvent.Name = "ClosedEvent" + this.Closed = closedEvent.Event + + --Never shown, will be overwritten by child-- + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('SideBarId')} + end + + function this:AddItem(newItemName, callback) + local button = Utility.Create'TextButton' + { + Name = "SortButton"; + Size = UDim2.new(1, 0, 0, BUTTON_SIZE_Y); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.BlueButtonColor; + BackgroundTransparency = 1; + ZIndex = 6; + Text = ""; + SelectionImageObject = dummySelectionImage; + Parent = container; + + SoundManager:CreateSound('MoveSelection'); + } + local text = Utility.Create'TextLabel' + { + Name = "SortName"; + Size = UDim2.new(1, -INSET_X, 1, 0); + Position = UDim2.new(0, INSET_X, 0, 0); + BackgroundTransparency = 1; + Text = newItemName; + TextXAlignment = Enum.TextXAlignment.Left; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.MediumFontSize; + ZIndex = 7; + Parent = button; + } + + button.MouseButton1Click:connect(function() + SoundManager:Play('ButtonPress') + ScreenManager:CloseCurrent() + closedEvent:Fire() + callback() + end) + button.SelectionGained:connect(function() + button.BackgroundTransparency = 0 + text.TextColor3 = GlobalSettings.TextSelectedColor + end) + button.SelectionLost:connect(function() + button.BackgroundTransparency = 1 + text.TextColor3 = GlobalSettings.WhiteTextColor + end) + + buttons[#buttons + 1] = button + recalcPositions() + end + + function this:SetText(text) + textLabel.Text = text + textLabel.Parent = container + end + + function this:ResetText() + textLabel.Text = "" + textLabel.Parent = nil + end + + function this:RemoveAllItems() + this:ResetText() + for i,button in pairs(buttons) do + button:Destroy() + buttons[i] = nil + end + end + + function this:SetSelectedObject(indexToOpenTo, selectCoreObject) + if #buttons > 0 then + selectedObject = indexToOpenTo and buttons[indexToOpenTo] or buttons[1] + --If change SelectedCoreObject and sidebar is focused + if selectCoreObject == true and selectedObject and inFocus then + GuiService.SelectedCoreObject = selectedObject + end + else + selectedObject = nil + end + end + + function this:GetPriority() + return GlobalSettings.OverlayPriority + end + + function this:Show() + modalOverlay.Parent = ScreenManager:GetScreenGuiByPriority(self:GetPriority()) + local tweenIn = Utility.PropertyTweener(modalOverlay, "BackgroundTransparency", 1, + GlobalSettings.ModalBackgroundTransparency, 0.25, Utility.EaseInOutQuad, true, nil) + container:TweenPosition(UDim2.new(1 - container.Size.X.Scale, 0, 0, 0), + Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, 0.25, true) + SoundManager:Play('SideMenuSlideIn') + end + + function this:Hide() + local tweenOut = Utility.PropertyTweener(modalOverlay, "BackgroundTransparency", 0.3, 1, 0.25, Utility.EaseInOutQuad, true, + function() + modalOverlay.Parent = nil + end) + container:TweenPosition(UDim2.new(1, 0, 0, 0), Enum.EasingDirection.InOut, Enum.EasingStyle.Sine, 0.25, true) + end + + function this:IsFocused() + return inFocus + end + + function this:Focus() + inFocus = true + GuiService:AddSelectionParent("SideBar", container) + if selectedObject then + Utility.SetSelectedCoreObject(selectedObject) + else + self:SetSelectedObject(1) + Utility.SetSelectedCoreObject(selectedObject) + end + + -- connect back button + ContextActionService:BindCoreAction("CloseSideBar", + function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + ScreenManager:CloseCurrent() + closedEvent:Fire() + end + end, + false, Enum.KeyCode.ButtonB) + end + + function this:RemoveFocus() + inFocus = false + GuiService:RemoveSelectionGroup("SideBar") + ContextActionService:UnbindCoreAction("CloseSideBar") + selectedObject = nil + end + + return this +end + +return CreateSideBar diff --git a/Client2018/content/internal/AppShell/Modules/Shell/SignInScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/SignInScreen.lua new file mode 100644 index 0000000..376fb20 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/SignInScreen.lua @@ -0,0 +1,239 @@ +--[[ + // SignInScreen.lua +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local GuiService = game:GetService('GuiService') +local ThirdPartyUserService = nil +pcall(function() ThirdPartyUserService = game:GetService("ThirdPartyUserService") end) +local TextService = game:GetService('TextService') +local ContextActionService = game:GetService("ContextActionService") + +local AssetManager = require(ShellModules:FindFirstChild('AssetManager')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local LinkAccountScreen = require(ShellModules:FindFirstChild('LinkAccountScreen')) +local SetAccountCredentialsScreen = require(ShellModules:FindFirstChild('SetAccountCredentialsScreen')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local XboxAppState = require(ShellModules:FindFirstChild('AppState')) + +local function createSignInScreen() + local this = {} + + local isFocused = false + local currentParent = nil + + local DefaultButtonColor = GlobalSettings.GreyButtonColor + local SelectedButtonColor = GlobalSettings.GreySelectedButtonColor + local DefaultButtonTextColor = GlobalSettings.WhiteTextColor + local SelectedButtonTextColor = GlobalSettings.TextSelectedColor + + -- get gamertag and set display text + local createAccountText = Strings:LocalizedString("PlayAsPhrase") + local gamertag = XboxAppState.store:getState().XboxUser.gamertag + createAccountText = string.format(createAccountText, gamertag) + + Utility.Create'Frame' + { + Name = "ModalOverlay"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = GlobalSettings.ModalBackgroundTransparency; + BackgroundColor3 = GlobalSettings.ModalBackgroundColor; + BorderSizePixel = 0; + ZIndex = 4; + } + + local Container = Utility.Create'Frame' + { + Name = "SignInScreen"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Visible = false; + } + + local RobloxLogo = Utility.Create'ImageLabel' + { + Name = "RobloxLogo"; + Size = UDim2.new(0, 594, 0, 199); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/ROBLOXSplashLogo.png'; + Parent = Container; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + } + + RobloxLogo.Image = 'rbxasset://textures/ui/Shell/Icons/SplashLogo.png' + RobloxLogo.Size = UDim2.new(0, 594, 0, 209) + + local BackImage = Utility.Create'ImageLabel' + { + Name = "BackImage"; + Size = UDim2.new(0,48,0,48); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Shell/Icons/BackIcon@1080.png"; + Parent = Container; + } + + Utility.Create'TextLabel' + { + Name = "BackText"; + Size = UDim2.new(0, 0, 0, BackImage.Size.Y.Offset); + Position = UDim2.new(0, BackImage.Size.X.Offset + 8, 0, 0); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextXAlignment = Enum.TextXAlignment.Left; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = Strings:LocalizedString("BackWord"); + Parent = Container; + } + + local FadeInFrame = Utility.Create'Frame' + { + Name = "FadeInFrame"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Visible = false; + Parent = Container; + } + + local LinkAccountButton = Utility.Create'ImageButton' + { + Name = "LinkAccountButton"; + Size = UDim2.new(0, 200, 0, 64); + Position = UDim2.new(0.5, -100, 1, -64 - 140); + BackgroundTransparency = 1; + ImageColor3 = DefaultButtonColor; + Image = GlobalSettings.RoundCornerButtonImage; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28)); + ZIndex = 2; + Parent = FadeInFrame; + + SoundManager:CreateSound('MoveSelection'); + AssetManager.CreateShadow(1) + } + + local CreateAccountButton = LinkAccountButton:Clone() + CreateAccountButton.Name = "CreateAccountButton" + CreateAccountButton.Size = UDim2.new(0, 360, 0, 64); + CreateAccountButton.Position = UDim2.new(0.5, -180, 1, LinkAccountButton.Position.Y.Offset - 64 - 44) + CreateAccountButton.Parent = FadeInFrame + + local LinkAccountText = Utility.Create'TextLabel' + { + Name = "LinkAccountText"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = DefaultButtonTextColor; + Text = Strings:LocalizedString("SignInPhrase"); + ZIndex = 2; + Parent = LinkAccountButton; + } + Utility.ResizeButtonWithText(LinkAccountButton, LinkAccountText, GlobalSettings.TextHorizontalPadding) + LinkAccountButton.Position = UDim2.new(0.5, -LinkAccountButton.Size.X.Offset/2, 1, LinkAccountButton.Position.Y.Offset) + + local CreateAccountText = LinkAccountText:Clone() + CreateAccountText.Text = createAccountText + CreateAccountText.Parent = CreateAccountButton + local createAccountTextSize = TextService:GetTextSize(createAccountText, + Utility.ConvertFontSizeEnumToInt(CreateAccountText.FontSize), CreateAccountText.Font, Vector2.new(0, 0)) + CreateAccountButton.Size = UDim2.new(0, createAccountTextSize.X + 64, 0, CreateAccountButton.Size.Y.Offset) + Utility.ResizeButtonWithText(CreateAccountButton, CreateAccountText, GlobalSettings.TextHorizontalPadding) + CreateAccountButton.Position = UDim2.new(0.5, -CreateAccountButton.Size.X.Offset/2, 1, + CreateAccountButton.Position.Y.Offset) + + CreateAccountButton.SelectionGained:connect(function() + CreateAccountButton.ImageColor3 = SelectedButtonColor + CreateAccountText.TextColor3 = SelectedButtonTextColor + end) + CreateAccountButton.SelectionLost:connect(function() + CreateAccountButton.ImageColor3 = DefaultButtonColor + CreateAccountText.TextColor3 = DefaultButtonTextColor + end) + LinkAccountButton.SelectionGained:connect(function() + LinkAccountButton.ImageColor3 = SelectedButtonColor + LinkAccountText.TextColor3 = SelectedButtonTextColor + end) + LinkAccountButton.SelectionLost:connect(function() + LinkAccountButton.ImageColor3 = DefaultButtonColor + LinkAccountText.TextColor3 = DefaultButtonTextColor + end) + + local function animateOnShow() + ScreenManager:DefaultCancelFade(this.TransitionTweens) + FadeInFrame.Visible = true + this.TransitionTweens = ScreenManager:FadeInSitu(FadeInFrame) + if isFocused then + Utility.SetSelectedCoreObject(CreateAccountButton) + end + end + + --[[ Input ]]-- + CreateAccountButton.MouseButton1Click:connect(function() + SoundManager:Play('ButtonPress') + local setAccountCredentialsScreen = SetAccountCredentialsScreen(Strings:LocalizedString("SignUpTitle"), + Strings:LocalizedString("SignUpPhrase"), Strings:LocalizedString("SignUpWord")) + setAccountCredentialsScreen:SetParent(Container.Parent) + ScreenManager:OpenScreen(setAccountCredentialsScreen, true) + end) + + LinkAccountButton.MouseButton1Click:connect(function() + SoundManager:Play('ButtonPress') + local linkAccountScreen = LinkAccountScreen() + linkAccountScreen:SetParent(Container.Parent) + ScreenManager:OpenScreen(linkAccountScreen, true) + end) + + --[[ Public API ]]-- + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('SignInScreenId')} + end + + function this:SetParent(newParent) + currentParent = newParent + end + function this:Show() + RobloxLogo.AnchorPoint = Vector2.new(0.5, 0.5) + RobloxLogo.Position = UDim2.new(0.5, 0, 0.5, 0) + Container.Visible = true + Container.Parent = currentParent + animateOnShow() + end + function this:Hide() + Container.Visible = false + FadeInFrame.Visible = false + end + function this:Focus() + isFocused = true + GuiService:AddSelectionParent("SignInScreen", FadeInFrame) + Utility.SetSelectedCoreObject(CreateAccountButton) + if ThirdPartyUserService then + ContextActionService:BindCoreAction("ReturnToEngagement", + function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + ThirdPartyUserService:ReturnToEngagement() + end + end, + false, Enum.KeyCode.ButtonB) + end + end + function this:RemoveFocus() + isFocused = false + GuiService:RemoveSelectionGroup("SignInScreen") + Utility.SetSelectedCoreObject(nil) + ContextActionService:UnbindCoreAction("ReturnToEngagement") + end + + return this +end + +return createSignInScreen diff --git a/Client2018/content/internal/AppShell/Modules/Shell/SiteInfoWidget.lua b/Client2018/content/internal/AppShell/Modules/Shell/SiteInfoWidget.lua new file mode 100644 index 0000000..faf5897 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/SiteInfoWidget.lua @@ -0,0 +1,83 @@ +--[[ + // SiteInfoWidget.lua + + // Displays site info that the app is pointing to for test builds running non-production environments +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) + +local TitleSafeContainer = require(ShellModules.AppContainer).TitleSafeContainer + +local TextService = game:GetService('TextService') + +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local SiteInfoWidget = {} +SiteInfoWidget.__index = SiteInfoWidget + +function SiteInfoWidget.new() + local self = {} + + setmetatable(self, SiteInfoWidget) + + self.container = Utility.Create'Frame' + { + Name = "SiteInfoContainer"; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 1, 0); + AnchorPoint = Vector2.new(0, 1); + BackgroundTransparency = 1; + } + + self.text = Utility.Create'TextLabel' + { + Name = "SiteInfoText"; + Size = UDim2.new(1, -12, 0, 12); + Position = UDim2.new(0, 0, 1, 0); + AnchorPoint = Vector2.new(0, 1); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.TitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Bottom; + Text = ""; + Parent = self.container; + } + + self.updateLayout = function() + local textSize = TextService:GetTextSize(self.text.Text, Utility.ConvertFontSizeEnumToInt(self.text.FontSize), self.text.Font, Vector2.new(0, 0)) + self.container.Size = UDim2.new(0, textSize.x, 0, 50) + end + + if PlatformService then + spawn(function() + local text = PlatformService:GetSiteInfo(); + if text and text ~= "" then + self:SetText(text) + self:SetParent(TitleSafeContainer) + end + end) + end + + return self +end + +function SiteInfoWidget:SetParent(newParent) + self.container.Parent = newParent +end + +function SiteInfoWidget:SetText(newText) + if newText ~= self.text.Text then + self.text.Text = newText + self.updateLayout() + end +end + +return SiteInfoWidget \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/SiteInfoWidgetOld.lua b/Client2018/content/internal/AppShell/Modules/Shell/SiteInfoWidgetOld.lua new file mode 100644 index 0000000..5371e35 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/SiteInfoWidgetOld.lua @@ -0,0 +1,63 @@ +--[[ + // SiteInfoWidget.lua + + // Displays site info that the app is pointing to for test builds running non-production environments +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local TextService = game:GetService('TextService') + +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local function createSiteInfoWidget() + local this = {} + + local container = Utility.Create'Frame' + { + Name = "SiteInfoContainer"; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 1, 0); + AnchorPoint = Vector2.new(0, 1); + BackgroundTransparency = 1; + } + + local text = Utility.Create'TextLabel' + { + Name = "SiteInfoText"; + Size = UDim2.new(1, -12, 0, 12); + Position = UDim2.new(0, 0, 1, 0); + AnchorPoint = Vector2.new(0, 1); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.TitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Bottom; + Text = ""; + Parent = container; + } + + local function updateLayout() + local textSize = TextService:GetTextSize(text.Text, Utility.ConvertFontSizeEnumToInt(text.FontSize), text.Font, Vector2.new(0, 0)) + container.Size = UDim2.new(0, textSize.x, 0, 50) + end + + function this:SetParent(newParent) + container.Parent = newParent + end + + function this:SetText(newText) + if newText ~= text.Text then + text.Text = newText + updateLayout() + end + end + + return this +end + +return createSiteInfoWidget \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/SocialPane.lua b/Client2018/content/internal/AppShell/Modules/Shell/SocialPane.lua new file mode 100644 index 0000000..39f2d17 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/SocialPane.lua @@ -0,0 +1,179 @@ +--[[ + // SocialPane.lua +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local GuiService = game:GetService('GuiService') + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local FriendsData = require(ShellModules:FindFirstChild('FriendsData')) +local FriendsView = require(ShellModules:FindFirstChild('FriendsView')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local ScrollingGridModule = require(ShellModules:FindFirstChild('ScrollingGrid')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local function CreateSocialPane(parent) + local this = {} + + local myFriendsView = nil + local isPaneFocused = false + + local noSelectionObject = Utility.Create'ImageLabel' + { + BackgroundTransparency = 1; + } + + local SocialPaneContainer = Utility.Create'Frame' + { + Name = 'SocialPane'; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Visible = false; + SelectionImageObject = noSelectionObject; + Parent = parent; + } + --[[ Online Friends ]]-- + local FriendsContainer = Utility.Create'Frame' + { + Size = UDim2.new(0, 1720, 0, 610); + Position = UDim2.new(0, 0, 0, 33); + BackgroundTransparency = 1; + Parent = SocialPaneContainer; + } + + local friendsScrollingGrid = ScrollingGridModule({SelectionMode = "Middle"; Dynamic = true}) + friendsScrollingGrid:SetSize(UDim2.new(1, 0, 1, 0)) + friendsScrollingGrid:SetCellSize(Vector2.new(446, 114)) + friendsScrollingGrid:SetSpacing(Vector2.new(50, 10)) + friendsScrollingGrid:SetScrollDirection(friendsScrollingGrid.Enum.ScrollDirection.Horizontal) + friendsScrollingGrid:SetPosition(UDim2.new(0, 0, 0, 0)) + local friendsScrollingGridContainer = friendsScrollingGrid:GetGuiObject() + friendsScrollingGridContainer.Visible = false + friendsScrollingGrid:SetParent(FriendsContainer) + + --[[ No Friends Online ]]-- + local noFriendsIcon = Utility.Create'ImageLabel' + { + Name = "noFriendsIcon"; + Size = UDim2.new(0, 296, 0, 259); + Position = UDim2.new(0.5, -296/2, 0, 100); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/FriendsIcon@1080.png'; + Visible = false; + Parent = SocialPaneContainer; + } + local noFriendsText = Utility.Create'TextLabel' + { + Name = "NoFriendsText"; + Size = UDim2.new(0, 500, 0, 72); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = Strings:LocalizedString("NoFriendsPhrase"); + TextYAlignment = Enum.TextYAlignment.Top; + TextWrapped = true; + Visible = false; + Parent = SocialPaneContainer; + } + noFriendsText.Position = UDim2.new(0.5, -noFriendsText.Size.X.Offset/2, 0, + noFriendsIcon.Position.Y.Offset + noFriendsIcon.Size.Y.Offset + 32) + + --[[ Content Functions ]]-- + local function setPaneContentVisible(hasOnlineFriends) + noFriendsIcon.Visible = not hasOnlineFriends + noFriendsText.Visible = not hasOnlineFriends + end + + local function onFriendsUpdated(friendCount) + local hasOnlineFriends = friendCount > 0 + setPaneContentVisible(hasOnlineFriends) + end + + local function setSelectedObject() + if isPaneFocused then + local focusItem = myFriendsView and myFriendsView:GetDefaultFocusItem() + if focusItem then + Utility.SetSelectedCoreObject(focusItem) + end + end + end + + local function loadFriendsView() + local friendsData = FriendsData.GetOnlineFriendsAsync() + myFriendsView = FriendsView(friendsScrollingGrid, friendsData, onFriendsUpdated) + onFriendsUpdated(#friendsData) + setSelectedObject() + end + + local loader = LoadingWidget( + { Parent = SocialPaneContainer }, { loadFriendsView } + ) + + spawn(function() + loader:AwaitFinished() + loader:Cleanup() + friendsScrollingGridContainer.Visible = true + end) + + function this:GetName() + return Strings:LocalizedString('FriendsWord') + end + + function this:IsFocused() + return isPaneFocused + end + + --[[ Public API ]]-- + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('SocialPaneId')} + end + + function this:Show() + SocialPaneContainer.Visible = true + self.TransitionTweens = ScreenManager:DefaultFadeIn(SocialPaneContainer) + ScreenManager:PlayDefaultOpenSound() + end + + function this:Hide() + SocialPaneContainer.Visible = false + ScreenManager:DefaultCancelFade(self.TransitionTweens) + self.TransitionTweens = nil + end + + function this:Focus() + -- TODO: Hook in the hidden selection after figuring how how to know how + -- panes take focus (ie, bumper, tab, etc) + isPaneFocused = true + setSelectedObject() + end + + function this:RemoveFocus() + isPaneFocused = false + local selectedObject = GuiService.SelectedCoreObject + if selectedObject and selectedObject:IsDescendantOf(SocialPaneContainer) then + Utility.SetSelectedCoreObject(nil) + end + end + + function this:SetPosition(newPosition) + SocialPaneContainer.Position = newPosition + end + + function this:SetParent(newParent) + SocialPaneContainer.Parent = newParent + end + + function this:IsAncestorOf(object) + return SocialPaneContainer and SocialPaneContainer:IsAncestorOf(object) + end + + return this +end + +return CreateSocialPane diff --git a/Client2018/content/internal/AppShell/Modules/Shell/SortsData.lua b/Client2018/content/internal/AppShell/Modules/Shell/SortsData.lua new file mode 100644 index 0000000..223ac5f --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/SortsData.lua @@ -0,0 +1,1000 @@ +--[[ +// SortsData.lua +//-- Written by Bo Zhang, Copyright Roblox 2017 +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local ThirdPartyUserService = nil +pcall(function()ThirdPartyUserService = game:GetService('ThirdPartyUserService') end) + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local GameData = require(ShellModules:FindFirstChild('GameData')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Http = require(ShellModules:FindFirstChild('Http')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local ReloaderManager = require(ShellModules:FindFirstChild('ReloaderManager')) +local FFlagXboxUseCuratedGameSort = Utility.IsFastFlagEnabled('XboxUseCuratedGameSort2') +local XboxAppState = require(ShellModules:FindFirstChild('AppState')) + +--SortsData +--Stores all the sorts +local SortsData = {} +SortsData.FFlagXboxUseCuratedGameSort = FFlagXboxUseCuratedGameSort + +do + SortsData.DefaultSortId = { + Favorites = -1; + RecentlyPlayed = -2; + MyGames = -3; + Popular = 1; + Featured = 3; + TopEarning = 8; + TopRated = 11; + } + + local InternalSortsDataCached = nil + local InternalSortsId_IndexMap = nil + local VisibleSortsDataCached = nil + local VisibleSortsId_IndexMap = nil + local UpdateFuncId = nil + local UpdateSuspended = false + local GameSearchSort = nil + --These three sorts are static + local StaticSortsCount = 3 + local FavoritesSort = nil + local RecentSort = nil + local MyGamesSort = nil + + local function ResetCacheData() + --Clear All Data + if InternalSortsDataCached then + for i = 1, #InternalSortsDataCached.Data do + InternalSortsDataCached.Data[i]:Destroy() + InternalSortsDataCached.Data[i] = nil + end + end + + if VisibleSortsDataCached then + for i = 1, #VisibleSortsDataCached.Data do + VisibleSortsDataCached.Data[i]:Destroy() + VisibleSortsDataCached.Data[i] = nil + end + end + + if GameSearchSort then + GameSearchSort:Destroy() + end + if FavoritesSort then + FavoritesSort:Destroy() + end + if RecentSort then + RecentSort:Destroy() + end + if MyGamesSort then + MyGamesSort:Destroy() + end + + InternalSortsDataCached = nil + InternalSortsId_IndexMap = nil + VisibleSortsDataCached = nil + VisibleSortsId_IndexMap = nil + UpdateFuncId = nil + UpdateSuspended = false + GameSearchSort = nil + FavoritesSort = nil + RecentSort = nil + MyGamesSort = nil + end + + local UserChangedCount = 0 + local function OnUserAccountChanged() + local startCount = UserChangedCount + ResetCacheData() + spawn(function() + if startCount == UserChangedCount then + local UpdateInterval = GlobalSettings.GameSortsUpdateInterval + + ReloaderManager:removeReloader("SortsData") + UpdateFuncId = ReloaderManager:addReloaderFunc("SortsData", SortsData.UpdateSorts, UpdateInterval) + ReloaderManager:callReloaderFunc("SortsData", UpdateFuncId) + end + end) + end + + EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "SortsData", OnUserAccountChanged) + if ThirdPartyUserService then + ThirdPartyUserService.ActiveUserSignedOut:connect(function() + ResetCacheData() + UserChangedCount = UserChangedCount + 1 + ReloaderManager:removeReloader("SortsData") + end) + end + + + --Just in case some Sort doesn't exist, we return this base sort + --should never happen. + local function createBaseSort() + local this = {} + + function this:GetSortAsync(startIndex, pageSize) + Utility.DebugLog("Sort GetSortAsync() must be implemented by sub class") + end + + function this:GetCuratedSortAsync(pageIndex, scrollLeft) + Utility.DebugLog("Sort GetCuratedSortAsync() must be implemented by sub class") + end + + return this + end + + --Create a sort + local function createSort(sortId, sortName, httpFunc, userId, gameSetTargetId) + local DEFAULT_PAGE_SIZE = 25 --Same with PAGE_SIZE in CarouselController + + local sort = {} + -- Note: For curated sort, each data in cachedGamesData has a "PagingIndex" to store which web page that data belongs to, which is also the index of sort.Paging + local cachedGamesData = {} + local maxGameCount = 200 + local currGameCount = 0 + local isLoading = false + local initLoadGameCount = 100 + local upperIndex = nil + + sort.Id = sortId + sort.Name = sortName + sort.httpFunc = httpFunc + sort.GameSetTargetId = gameSetTargetId or 0 + sort.Paging = {} --Array of web paging data returned from http request + --Each entry is a table of "cachedGamesData_StartIndex" "cachedGamesData_EndIndex" "PreviousUrl" "NextUrl" + --Note: "cachedGamesData_StartIndex" "cachedGamesData_EndIndex" is the index in cachedGamesData + + --If the sort is related to user, we store the userId (Favorite, RecentlyPlayed and MyPlaces) + --Note: Only MyPlaces really needs userId info, the other two can fecth user info on the web side + if userId then + sort.userId = userId + end + + sort.timeFilter = nil + + --Get index range and the index correspoding to the placeId if provided + local function GetRangeIndex(placeId) + local lowIndex, highIndex, targetIndex + for index, game in pairs(cachedGamesData) do + if not lowIndex or lowIndex > index then + lowIndex = index + end + + if not highIndex or highIndex < index then + highIndex = index + end + + if placeId then + if tonumber(game.PlaceId) and tonumber(placeId) and tonumber(game.PlaceId) == tonumber(placeId) then + targetIndex = index + end + end + end + + return lowIndex, highIndex, targetIndex + end + + --Clear Caching based on maxGameCount and upperIndex + local function ClearCaching() + local lowIndex, highIndex = GetRangeIndex() + + --If there is a max games limit + if upperIndex then + for i = upperIndex + 1, highIndex do + if cachedGamesData[i] and cachedGamesData[i].PlaceId then + local placeId = cachedGamesData[i].PlaceId + GameData:ChangeGameDataAccessCount(placeId, -1) + cachedGamesData[i] = nil + currGameCount = currGameCount - 1 + end + end + end + + --Clear Caching + if currGameCount > maxGameCount then + local cachedGamesDataSorted = {} + for key in pairs(cachedGamesData) do + cachedGamesData[key].Key = key + table.insert(cachedGamesDataSorted, cachedGamesData[key]) + end + + currGameCount = #cachedGamesDataSorted + --Double check, in case we miscount the currGameCachedDataCount + if currGameCount > maxGameCount then + --Put not the oldest data in the back + table.sort(cachedGamesDataSorted, function(a, b) + return a.LastAccessTime > b.LastAccessTime + end) + + while #cachedGamesDataSorted > maxGameCount do + local placeId = cachedGamesDataSorted[#cachedGamesDataSorted].PlaceId + GameData:ChangeGameDataAccessCount(placeId, -1) + local RemovingKey = cachedGamesDataSorted[#cachedGamesDataSorted].Key + cachedGamesData[RemovingKey] = nil + cachedGamesDataSorted[#cachedGamesDataSorted] = nil + end + currGameCount = #cachedGamesDataSorted + end + end + end + + -- top rated is time filtered to show most recent top rated + if sortId == SortsData.DefaultSortId.TopRated and sort.GameSetTargetId == 0 then + sort.timeFilter = 2 + elseif sortId == SortsData.DefaultSortId.Favorites and sort.GameSetTargetId == 0 then + sort.eventString = EventHub.Notifications["FavoriteToggle"] + sort.objectIDString = tostring(sortId) + EventHub:addEventListener(sort.eventString, sort.objectIDString, + function(success, placeId) + while isLoading do + wait() + end + isLoading = true + if success and placeId then + local gameData = GameData:GetGameData(placeId) + if gameData then + local lowIndex, highIndex, origIndex = GetRangeIndex(placeId) + if origIndex then + --Remove from favorites + if not gameData.IsFavorited then + for i = origIndex, highIndex do + cachedGamesData[i] = cachedGamesData[i + 1] + end + currGameCount = currGameCount - 1 + GameData:ChangeGameDataAccessCount(placeId, -1) + end + else + --Add to front + if gameData.IsFavorited then + for i = highIndex, lowIndex, -1 do + cachedGamesData[i + 1] = cachedGamesData[i] + end + + --Insert at front + cachedGamesData[1] = {} + cachedGamesData[1].HasData = true + cachedGamesData[1].PlaceId = placeId + cachedGamesData[1].LastAccessTime = tick() + currGameCount = currGameCount + 1 + GameData:ChangeGameDataAccessCount(placeId, 1) + end + end + ClearCaching() + end + end + isLoading = false + end) + elseif sortId == SortsData.DefaultSortId.RecentlyPlayed and sort.GameSetTargetId == 0 then + --Recently Played Sort should have at most 50 games + upperIndex = 50 + maxGameCount = 50 + initLoadGameCount = 50 + + sort.eventString = EventHub.Notifications["GameJoin"] + sort.objectIDString = tostring(sortId) + EventHub:addEventListener(sort.eventString, sort.objectIDString, + function(success, placeId) + while isLoading do + wait() + end + isLoading = true + if success and placeId and placeId ~= -1 then + local lowIndex, highIndex, origIndex = GetRangeIndex(placeId) + if origIndex then + for i = origIndex, lowIndex + 1, -1 do + cachedGamesData[i] = cachedGamesData[i - 1] + end + else + for i = highIndex, lowIndex, -1 do + cachedGamesData[i + 1] = cachedGamesData[i] + end + currGameCount = currGameCount + 1 + GameData:ChangeGameDataAccessCount(placeId, 1) + end + + --Insert at front + cachedGamesData[1] = {} + cachedGamesData[1].HasData = true + cachedGamesData[1].PlaceId = placeId + cachedGamesData[1].LastAccessTime = tick() + ClearCaching() + end + isLoading = false + end) + end + + --Flush old data + local function FlushGameData(index) + if cachedGamesData[index] then + GameData:ChangeGameDataAccessCount(cachedGamesData[index].PlaceId, -1) + cachedGamesData[index] = nil + currGameCount = currGameCount - 1 + end + end + + --Flush paging and game data after pagingIndex (include) + function sort:FlushPagingDataBack(pagingIndex) + while #self.Paging >= pagingIndex do + table.remove(self.Paging) + end + for index, game in pairs(cachedGamesData) do + if game.PagingIndex and game.PagingIndex >= pagingIndex then + FlushGameData(index) + end + end + end + + --Flush paging and game data before pagingIndex (exclude) + function sort:FlushPagingDataFront(pagingIndex, pageSize) + local pagingOffset = pagingIndex - 1 + local gameIndexOffset = pagingOffset * pageSize + local newPagingSize = #self.Paging - pagingOffset + + -- No previous page, move all self.Paging left, current pagingIndex will be 1 + for i = 1, newPagingSize do + self.Paging[i] = self.Paging[i + pagingOffset] + end + for i = 1, pagingOffset do + table.remove(self.Paging) + end + + -- Update cachedGamesData + -- Clear page caching that are in previous pages + for i = 1, self.Paging[1]["cachedGamesData_StartIndex"] - 1 do + FlushGameData(i) + end + + -- Move cachedGamesData to left by gameIndexOffset and PagingIndex by pagingOffset + local lowIndex, highIndex = GetRangeIndex() + for i = gameIndexOffset + 1, highIndex do + cachedGamesData[i - gameIndexOffset] = cachedGamesData[i] + if cachedGamesData[i] and cachedGamesData[i].PagingIndex then + cachedGamesData[i - gameIndexOffset].PagingIndex = cachedGamesData[i].PagingIndex - pagingOffset + end + end + for i = highIndex - gameIndexOffset + 1, highIndex do + cachedGamesData[i] = nil + end + end + + -- Update next paging(scroll right) for curated sort, return Data in current page + function sort:UpdateNextPaging(pagingIndex, pageSize) + local result = nil + -- Get next page + if pagingIndex == 1 then + -- Get first page for curated sort + result = Http.GetCuratedSortAsync(self.GameSetTargetId, pageSize) + else + -- Get page from url + local nextUrl = self.Paging[pagingIndex - 1] and self.Paging[pagingIndex - 1]["NextUrl"] + result = nextUrl and Http.GetCuratedSortByUrlAsync(nextUrl) + end + + local data = result and result["data"] + local paging = result and result["paging"] + + -- No data, but has nextUrl: loop loading next page until there's data or reach the end + while not (data and #data > 0) and paging and paging["nextUrl"] and paging["nextUrl"] ~= "" do + result = Http.GetCuratedSortByUrlAsync(paging["nextUrl"]) + data = result and result["data"] + paging = result and result["paging"] + end + + -- Still no data, flush current Paging and next pagings, return nil + if not data or #data == 0 then + self:FlushPagingDataBack(pagingIndex) + return nil + end + + -- Clean up pagings and games in cachedGamesData with PagingIndex >= current pagingIndex + if #self.Paging >= pagingIndex then + self:FlushPagingDataBack(pagingIndex) + end + + -- Get new cachedGamesData_StartIndex and cachedGamesData_EndIndex, based on cachedGamesData_EndIndex of previous page + -- Set cachedGamesData_StartIndex, cachedGamesData_EndIndex and pageUrl for current page + local pagingData = {} + pagingData["cachedGamesData_StartIndex"] = (pagingIndex == 1) and 1 or (self.Paging[pagingIndex - 1]["cachedGamesData_EndIndex"] + 1) + pagingData["cachedGamesData_EndIndex"] = pagingData["cachedGamesData_StartIndex"] + #data - 1 + pagingData["PreviousUrl"] = paging and paging["previousUrl"] + pagingData["NextUrl"] = paging and paging["nextUrl"] + + -- Append new pagingData to Paging + table.insert(self.Paging, pagingData) + + return data + end + + --Update previous paging(scroll left) for curated sort + --Return data in current page, hasPrevUrl(whether there's prevUrl), and resetPagingIndex(whether current pagingIndex has been reset to 1) + function sort:UpdatePreviousPaging(pagingIndex, pageSize, dataNeeded) + local prevUrl = self.Paging[pagingIndex + 1] and self.Paging[pagingIndex + 1]["PreviousUrl"] + local result = prevUrl and Http.GetCuratedSortByUrlAsync(prevUrl) + local data = result and result["data"] + local paging = result and result["paging"] + + --No data, but has previousUrl: loop loading next page until there's data or reach the end + while not (data and #data > 0) and paging and paging["previousUrl"] and paging["previousUrl"] ~= "" do + result = Http.GetCuratedSortByUrlAsync(paging["previousUrl"]) + data = result and result["data"] + paging = result and result["paging"] + end + + --Still no data, flush current Paging, return nil + if not data or #data == 0 then + if self.Paging[pagingIndex] then + self.Paging[pagingIndex] = {} + end + return nil + end + + -- Check if we need to insert new paging at front + local resetPagingIndex = false + if pagingIndex == 0 then + --Insert new paging & change pagingIndex in cachedGamesData + resetPagingIndex = true + pagingIndex = 1 + table.insert(self.Paging, 1, {}) + + --move all PagingIndex + 1 in cachedGamesData + for index, game in pairs(cachedGamesData) do + if cachedGamesData[index].PagingIndex then + cachedGamesData[index].PagingIndex = cachedGamesData[index].PagingIndex + 1 + end + end + end + + --Get new cachedGamesData_StartIndex and cachedGamesData_EndIndex, based on cachedGamesData_StartIndex of next page + local cachedGamesData_EndIndex = self.Paging[pagingIndex + 1]["cachedGamesData_StartIndex"] - 1 + local cachedGamesData_StartIndex = cachedGamesData_EndIndex - #data + 1 + + --Check if we need data with index <= 0 in cachedGamesData, which is the situation when we request page 0 in CarouselController + if cachedGamesData_StartIndex < 1 and dataNeeded > cachedGamesData_EndIndex then + --Move the whole sort to next page, here page means the page in CarouselController, which has "pageSize"(25) games in each page + local lowIndex, highIndex = GetRangeIndex() + for i = highIndex, lowIndex, -1 do + --move all cachedGamesData + pageSize + cachedGamesData[i + pageSize] = cachedGamesData[i] + end + for i = 1, pageSize do + cachedGamesData[i] = nil + end + + --update all index + pageSize in self.Paging so that we can still get the correct index in cachedGamesData for current web paging + cachedGamesData_StartIndex = cachedGamesData_StartIndex + pageSize + cachedGamesData_EndIndex = cachedGamesData_EndIndex + pageSize + for i = 1, #self.Paging do + if self.Paging[i] then + self.Paging[i]["cachedGamesData_StartIndex"] = self.Paging[i]["cachedGamesData_StartIndex"] + pageSize + self.Paging[i]["cachedGamesData_EndIndex"] = self.Paging[i]["cachedGamesData_EndIndex"] + pageSize + end + end + end + + -- Update cachedGamesData_StartIndex, cachedGamesData_EndIndex and pageUrl for current page + self.Paging[pagingIndex]["cachedGamesData_StartIndex"] = cachedGamesData_StartIndex + self.Paging[pagingIndex]["cachedGamesData_EndIndex"] = cachedGamesData_EndIndex + self.Paging[pagingIndex]["PreviousUrl"] = paging and paging["previousUrl"] + self.Paging[pagingIndex]["NextUrl"] = paging and paging["nextUrl"] + + local hasPrevUrl = self.Paging[pagingIndex]["PreviousUrl"] and self.Paging[pagingIndex]["PreviousUrl"] ~= "" + + -- No previous page, move all pages left + if not hasPrevUrl and #data <= dataNeeded and pagingIndex > 1 then + resetPagingIndex = true + self:FlushPagingDataFront(pagingIndex, pageSize) + end + + return data, hasPrevUrl, resetPagingIndex + end + + -- Update cachedGamesData for curated sort + function sort:UpdateCachedGamesData(data, pagingIndex, scrollLeft) + -- If scrolls right, handle data from left to right, otherwise update data from right to left + local cachedGamesData_StartIndex = self.Paging[pagingIndex] and self.Paging[pagingIndex]["cachedGamesData_StartIndex"] + local cachedGamesData_EndIndex = self.Paging[pagingIndex] and self.Paging[pagingIndex]["cachedGamesData_EndIndex"] + if cachedGamesData_StartIndex < 1 then + -- We don't need the data with cachedGamesData_StartIndex < 1 + -- When we need those data, UpdatePreviousPaging() will move the whole page right, so we don't need to consider about it here + cachedGamesData_StartIndex = 1 + end + + local indexFrom = scrollLeft and cachedGamesData_EndIndex or cachedGamesData_StartIndex + local indexTo = scrollLeft and cachedGamesData_StartIndex or cachedGamesData_EndIndex + local step = scrollLeft and -1 or 1 + for i = indexFrom, indexTo, step do + local dataIndex = i - cachedGamesData_StartIndex + 1 + local creatorName = data[dataIndex]["CreatorName"] + local iconId = data[dataIndex]["ImageId"] + local name = data[dataIndex]["Name"] + local placeId = data[dataIndex]["PlaceID"] + local voteData = { + UpVotes = data[dataIndex]["TotalUpVotes"]; + DownVotes = data[dataIndex]["TotalDownVotes"]; + } + local creatorId = data[dataIndex]["CreatorID"] + + FlushGameData(i) + GameData:UpdateGameData(placeId, name, creatorName, iconId, voteData, creatorId) + + cachedGamesData[i] = {} + cachedGamesData[i].PlaceId = placeId + cachedGamesData[i].PagingIndex = pagingIndex + cachedGamesData[i].LastAccessTime = tick() + currGameCount = currGameCount + 1 + +-- Utility.DebugLog("SortsData: cachedGamesData", i, placeId, pagingIndex, indexFrom, indexTo) + end + ClearCaching() + end + + -- Return number of entries in current paging, hasPrevUrl(whether there's previousUrl), and resetPagingIndex(whether current pagingIndex has been reset to 1) + function sort:GetPagingData(pagingIndex, pageSize, scrollLeft, dataNeeded) + -- pagingIndex is the page number in self.Paging, which is the page returned from the http request(might have <= pageSize games in each page) + local cachedGamesData_StartIndex = self.Paging[pagingIndex] and self.Paging[pagingIndex]["cachedGamesData_StartIndex"] + local cachedGamesData_EndIndex = self.Paging[pagingIndex] and self.Paging[pagingIndex]["cachedGamesData_EndIndex"] + local pagingHasCached = true + local hasPrevUrl = false + local resetPagingIndex = false + if cachedGamesData_StartIndex and cachedGamesData_EndIndex and cachedGamesData_StartIndex > 0 and cachedGamesData_EndIndex >= cachedGamesData_StartIndex then + for i = cachedGamesData_StartIndex, cachedGamesData_EndIndex do + if not cachedGamesData[i] then + pagingHasCached = false + break + end + end + else + pagingHasCached = false + end +-- Utility.DebugLog("SortsData: UpdateCachedGamesData.pagingHasCached", pagingIndex, pagingHasCached, cachedGamesData_StartIndex, cachedGamesData_EndIndex) + + -- The whole page has already in cachedGamesData + if pagingHasCached then + hasPrevUrl = self.Paging[pagingIndex]["PreviousUrl"] and self.Paging[pagingIndex]["PreviousUrl"] ~= "" + return cachedGamesData_EndIndex - cachedGamesData_StartIndex + 1, hasPrevUrl, false + end + + local data = nil + if not scrollLeft then + data = self:UpdateNextPaging(pagingIndex, pageSize) + else + data, hasPrevUrl, resetPagingIndex = self:UpdatePreviousPaging(pagingIndex, pageSize, dataNeeded) + if resetPagingIndex then + pagingIndex = 1 + end + end + + if data then + self:UpdateCachedGamesData(data, pagingIndex, scrollLeft) + return #data, hasPrevUrl, resetPagingIndex + end + + return 0 + end + + -- Get PlaceIds in current page, and whether there's previous page if scrolls left + function sort:GetCuratedSortAsync(pageIndex, pageSize, scrollLeft) + -- pageIndex is the page number in CarouselController, which has "pageSize"(25) games in each page unless it's the first or last page + while isLoading do + wait() + end + + isLoading = true + local gamesData = {} + local hasCached = true + local hasPrevPage = false + local startIndex = (pageIndex - 1) * pageSize + if pageIndex > 0 then + for i = startIndex + 1, startIndex + pageSize do + if not cachedGamesData[i] then + hasCached = false + break + end + end + else + hasCached = false + end + + -- If we don't have full cached data, we fetch the games + if not hasCached then + -- Load next page + if not scrollLeft then + local webPagingIndex = 1 -- paging index based on web returns that we need to fetch + local dataNeeded = pageSize -- How many games we need to fetch + + -- If page > 1, we should get current page based on the previous page + if pageIndex > 1 then + -- Get paging index from the previous game + webPagingIndex = cachedGamesData[startIndex].PagingIndex + if self.Paging[webPagingIndex]["cachedGamesData_EndIndex"] == startIndex then + -- index from self.Paging[webPagingIndex]["cachedGamesData_StartIndex"] to self.Paging[webPagingIndex]["cachedGamesData_EndIndex"] in cachedGamesData has already been fetched in previous page + webPagingIndex = webPagingIndex + 1 + else + -- We need to add the amount of games in the same paging with cachedGamesData[startIndex].PagingIndex to dataNeeded + -- If the whole paging is already in cache, it won't be fetch again, otherwise we'll fetch the whole paging. This part is handled in self:GetPagingData(), right now we assume current paging need to be fetched again + for i = startIndex, 1, -1 do + if cachedGamesData[i].PagingIndex == webPagingIndex then + dataNeeded = dataNeeded + 1 + else + break + end + end + end + end +-- Utility.DebugLog("SortsData: scroll right", pageIndex, startIndex, webPagingIndex, dataNeeded) + + -- Get & update paging data when we need more games + while dataNeeded > 0 do + local dataNum = self:GetPagingData(webPagingIndex, pageSize) + dataNeeded = dataNeeded - dataNum + webPagingIndex = webPagingIndex + 1 + if dataNum == 0 then + break + end + end + + if dataNeeded > 0 then + --We hit the edge, flush old data + for i = pageSize - dataNeeded + 1, pageSize do + FlushGameData(startIndex + i) + end + end + else + -- Scroll back: we should fetch page from right to left + -- Get start paging index that we need to fetch from next item after the last one in current page + local webPagingIndex = cachedGamesData[startIndex + pageSize + 1].PagingIndex + local dataNeeded = pageSize -- How many games we need to fetch + + if self.Paging[webPagingIndex]["cachedGamesData_StartIndex"] == startIndex + pageSize + 1 then + -- index from self.Paging[webPagingIndex]["cachedGamesData_StartIndex"] to self.Paging[webPagingIndex]["cachedGamesData_EndIndex"] in cachedGamesData has already been fetched in next page + webPagingIndex = webPagingIndex - 1 + else + -- We need to add the amount of games in the same paging with cachedGamesData[startIndex].PagingIndex to dataNeeded + -- If the whole paging is already in cache, it won't be fetch again, otherwise we'll fetch the whole paging. This part is handled in self:GetPagingData(), right now we assume current paging need to be fetched again + for i = startIndex + pageSize + 1, self.Paging[#self.Paging]["cachedGamesData_EndIndex"] do + if cachedGamesData[i].PagingIndex == webPagingIndex then + dataNeeded = dataNeeded + 1 + else + break + end + end + end + +-- Utility.DebugLog("SortsData: scroll left", pageIndex, startIndex, webPagingIndex, dataNeeded) + -- Get & update page data when we need more games + while dataNeeded > 0 do + local dataNum, hasPrevUrl, resetPagingIndex = self:GetPagingData(webPagingIndex, pageSize, scrollLeft, dataNeeded) + dataNeeded = dataNeeded - dataNum + + -- When game numbers in current web paging > dataNeeded, even if current web paging doesn't have prevUrl, we still have prev page for CarouselController with the games left in current web paging + hasPrevPage = (dataNeeded < 0) or hasPrevUrl + + if resetPagingIndex then + -- current pagingIndex has been reset to 1 + webPagingIndex = 1 + -- pageIndex should be reset when 1)Insert page at front 2)All pages moved left because of no data at front + startIndex = 0 + pageIndex = 1 + end + webPagingIndex = webPagingIndex - 1 + + if dataNum == 0 or not hasPrevUrl or webPagingIndex < 0 then + break + end + end + + if dataNeeded > 0 then + --We hit the edge, flush old data + for i = 1, dataNeeded do + FlushGameData(startIndex + i) + end + end + end + else + hasPrevPage = pageIndex > 1 +-- Utility.DebugLog("SortsData: has cached data", pageIndex, hasPrevPage) + end + + local indexFrom = scrollLeft and startIndex + pageSize or startIndex + 1 + local indexTo = scrollLeft and startIndex + 1 or startIndex + pageSize + local step = scrollLeft and -1 or 1 + for index = indexFrom, indexTo, step do + if cachedGamesData[index] then + --Update LastAccessTime for the requested section and the data in threshold + cachedGamesData[index].LastAccessTime = tick() + if not scrollLeft then + table.insert(gamesData, cachedGamesData[index].PlaceId) + else + table.insert(gamesData, 1, cachedGamesData[index].PlaceId) + end + else + break + end + end + + isLoading = false + return gamesData, hasPrevPage + end + + function sort:GetSortAsync(startIndex, pageSize) + while isLoading do + wait() + end + isLoading = true + local gamesData = {} + local hasCached = true + for i = startIndex + 1, startIndex + pageSize do + if not cachedGamesData[i] then + hasCached = false + break + end + end + + --If we don't have full cached data, we fetch the pages + if not hasCached then + local data = self.httpFunc(startIndex, pageSize, self.userId or self.Id, self.timeFilter) + if data then + for i = 1, #data do + local creatorName = data[i]["CreatorName"] + local iconId = data[i]["ImageId"] + local name = data[i]["Name"] + local placeId = data[i]["PlaceID"] + local voteData = { + UpVotes = data[i]["TotalUpVotes"]; + DownVotes = data[i]["TotalDownVotes"]; + } + local creatorId = data[i]["CreatorID"] + + FlushGameData(startIndex + i) + + GameData:UpdateGameData(placeId, name, creatorName, iconId, voteData, creatorId) + + cachedGamesData[startIndex + i] = {} + cachedGamesData[startIndex + i].HasData = true + cachedGamesData[startIndex + i].PlaceId = placeId + cachedGamesData[startIndex + i].LastAccessTime = tick() + currGameCount = currGameCount + 1 + end + + --We hit the edge, store empty data + for i = #data + 1, pageSize do + cachedGamesData[startIndex + i] = {} + cachedGamesData[startIndex + i].HasData = false + cachedGamesData[startIndex + i].LastAccessTime = tick() + end + + ClearCaching() + end + end + for index = startIndex + 1, startIndex + pageSize do + if cachedGamesData[index] then + --Update LastAccessTime for the requested section and the data in threshold + cachedGamesData[index].LastAccessTime = tick() + if cachedGamesData[index].HasData then + table.insert(gamesData, cachedGamesData[index].PlaceId) + else + break + end + end + end + isLoading = false + return gamesData + end + + function sort:InitSortAsync() + if FFlagXboxUseCuratedGameSort and self.GameSetTargetId ~= 0 then + -- Keep returning DEFAULT_PAGE_SIZE items in each page until there's no more page + local loadPages = math.ceil(initLoadGameCount/DEFAULT_PAGE_SIZE) + for i = 1, loadPages do + local page = self:GetCuratedSortAsync(i, DEFAULT_PAGE_SIZE) +-- Utility.DebugLog("sortsData:InitSortAsync: pageIndex", i, "hasPage", page and #page) + if not page or #page < DEFAULT_PAGE_SIZE then + break + end + end + else + self:GetSortAsync(0, initLoadGameCount) + end + end + + function sort:FlushGamesData() + for index in pairs(cachedGamesData) do + GameData:ChangeGameDataAccessCount(cachedGamesData[index].PlaceId, -1) + cachedGamesData[index] = nil + end + cachedGamesData = {} + self.Paging = {} + currGameCount = 0 + end + + function sort:Destroy() + self:FlushGamesData() + if self.eventString and self.objectIDString then + EventHub:removeEventListener(self.eventString, self.objectIDString) + end + end + + return sort + end + + + --Internal utility function to get latest sort + local function GetSortFromSortId(SortId, GameSetTargetId) + local sort = nil + if VisibleSortsDataCached and VisibleSortsDataCached.Data and VisibleSortsId_IndexMap and VisibleSortsId_IndexMap[SortId] and VisibleSortsId_IndexMap[SortId][GameSetTargetId or 0] then + sort = VisibleSortsDataCached.Data[VisibleSortsId_IndexMap[SortId][GameSetTargetId or 0]] + end + + return sort or createBaseSort() + end + + -- Get all sorts used for Xbox + local function GetSortsAsync(userId) + local Sorts = {} + + local result = Http.GetGameSortsAsync() + if result then + for i = 1, #result do + if FFlagXboxUseCuratedGameSort then + local sortId = result[i]["Id"] + local gameSetTargetId = result[i]["GameSetTargetId"] or 0 + local sortName = result[i]["Name"] + local sort = createSort(sortId, sortName, gameSetTargetId == 0 and Http.GetSortAsync or Http.GetCuratedSortAsync, nil, gameSetTargetId) + table.insert(Sorts, sort) +-- Utility.DebugLog(i, "/", #result, ".SortsData.GetSortsAsync", sortId, ", ", gameSetTargetId, ", ", sortName) + else + --ignore sort that GameSetTargetId ~= 0 + local gameSetTargetId = result[i]["GameSetTargetId"] or 0 + if gameSetTargetId == 0 then + local sortId = result[i]["Id"] + local sortName = result[i]["Name"] + local sort = createSort(sortId, sortName, Http.GetSortAsync) + table.insert(Sorts, sort) + end + end + end + end + + --The Custom Static Sorts will be created only once + FavoritesSort = FavoritesSort or createSort(SortsData.DefaultSortId.Favorites, Strings:LocalizedString("FavoritesSortTitle"), Http.GetUserFavoritesAsync, userId) + RecentSort = RecentSort or createSort(SortsData.DefaultSortId.RecentlyPlayed, Strings:LocalizedString("RecentlyPlayedSortTitle"), Http.GetUserRecentAsync, userId) + MyGamesSort = MyGamesSort or createSort(SortsData.DefaultSortId.MyGames, Strings:LocalizedString("PlayMyPlaceMoreGamesTitle"), Http.GetUserPlacesAsync, userId) + + table.insert(Sorts, FavoritesSort) + table.insert(Sorts, RecentSort) + table.insert(Sorts, MyGamesSort) + return Sorts + end + + local debounceUpdateCachedData = false + function SortsData:UpdateSorts() + if debounceUpdateCachedData then + while debounceUpdateCachedData do wait() end + end + debounceUpdateCachedData = true + local userId = XboxAppState.store:getState().RobloxUser.rbxuid + local startCount = UserChangedCount + local newSorts = GetSortsAsync(userId) + --If BG update is suspended, the update shouldn't replace the internal data + if startCount == UserChangedCount then + --Update InternalSortsDataCached when it doesn't contain data or not UpdateSuspended + if not InternalSortsDataCached or not UpdateSuspended then + if #newSorts > 0 then + InternalSortsId_IndexMap = {} + for i = 1, #newSorts do + spawn(function() + if newSorts[i] then + newSorts[i]:FlushGamesData() + newSorts[i]:InitSortAsync() + end + end) + if InternalSortsId_IndexMap[newSorts[i]["Id"]] == nil then + InternalSortsId_IndexMap[newSorts[i]["Id"]] = {} + end + InternalSortsId_IndexMap[newSorts[i]["Id"]][newSorts[i]["GameSetTargetId"] or 0] = i + end + InternalSortsDataCached = {Data = newSorts, Version = tick()} + end + end + end + debounceUpdateCachedData = false + end + + + function SortsData:CallUpdate(manual) + if manual then + ReloaderManager:callReloaderFunc("SortsData", UpdateFuncId) + else + ReloaderManager:callReloaderFunc("SortsData", UpdateFuncId, true) + end + end + + function SortsData:SuspendUpdate() + UpdateSuspended = true + ReloaderManager:suspendReloaderFunc("SortsData", UpdateFuncId) + end + + function SortsData:ResumeUpdate() + UpdateSuspended = false + ReloaderManager:resumeReloaderFunc("SortsData", UpdateFuncId) + end + + local function CheckCachedData() + local startCount = UserChangedCount + --Check whether the cachedData exists, if not, fetch it manually + if not InternalSortsDataCached then + while debounceUpdateCachedData do wait() end + --If still no data, try to fetch InternalSortsDataCached again manually + if not InternalSortsDataCached then + ReloaderManager:callReloaderFunc("SortsData", UpdateFuncId) + end + end + + if startCount == UserChangedCount then + if VisibleSortsDataCached and InternalSortsDataCached then + if VisibleSortsDataCached.Version ~= InternalSortsDataCached.Version then + --Clear the data in old sorts, the static sorts won't be clear as they are static + for i = 1, #VisibleSortsDataCached.Data - StaticSortsCount do + VisibleSortsDataCached.Data[i]:Destroy() + VisibleSortsDataCached.Data[i] = nil + end + end + end + VisibleSortsId_IndexMap = InternalSortsId_IndexMap + VisibleSortsDataCached = InternalSortsDataCached + end + end + + --Get sort by sortId, we assume that the sortId corresponds to each sort won't change + function SortsData:GetSort(sortId, gameSetTargetId) + CheckCachedData() + return GetSortFromSortId(sortId, gameSetTargetId) + end + + function SortsData:GetUserFavorites() + CheckCachedData() + return GetSortFromSortId(SortsData.DefaultSortId.Favorites) + end + + function SortsData:GetUserRecent() + CheckCachedData() + return GetSortFromSortId(SortsData.DefaultSortId.RecentlyPlayed) + end + + function SortsData:GetUserPlaces() + CheckCachedData() + return GetSortFromSortId(SortsData.DefaultSortId.MyGames) + end + + function SortsData:GetGameSearchSort(searchWord) + if not GameSearchSort then + GameSearchSort = createSort("GameSearch", "GameSearch", Http.SearchGamesAsync) + end + Analytics.SetRBXEventStream("GameSearch", {SearchWord = searchWord}) + GameSearchSort:FlushGamesData() + GameSearchSort.Id = searchWord + return GameSearchSort + end + + function SortsData:GetSorts() + CheckCachedData() + return VisibleSortsDataCached + end + + --Check whether the VisibleSortsDataCached has been initialized + function SortsData:HasSorts() + return VisibleSortsDataCached ~= nil + end +end + + +return SortsData diff --git a/Client2018/content/internal/AppShell/Modules/Shell/SoundManager.lua b/Client2018/content/internal/AppShell/Modules/Shell/SoundManager.lua new file mode 100644 index 0000000..24f5a37 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/SoundManager.lua @@ -0,0 +1,191 @@ +-- local CoreGui = Game:GetService("CoreGui") + +-- local GuiRoot = CoreGui:FindFirstChild("RobloxGui") + +local GuiService = game:GetService('GuiService') +local runService = game:GetService("RunService") + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local BASE_SOUND_URL = 'rbxasset://sounds/ui/Shell/' + +local SOUNDS = +{ + --[[ BGM ]]-- + ['BackgroundLoop'] = 'RobloxMusic.ogg'; + + + --[[ UI Sounds ]]-- + ['Error'] = 'Error.mp3'; + ['ButtonPress'] = 'ButtonPress.mp3'; + ['MoveSelection'] = 'MoveSelection.mp3'; + ['OverlayOpen'] = 'OverlayOpen.mp3'; + ['PopUp'] = 'PopUp.mp3'; + ['PurchaseSuccess'] = 'PurchaseSuccess.mp3'; + ['ScreenChange'] = 'ScreenChange.mp3'; + ['SideMenuSlideIn'] = 'SideMenuSlideIn.mp3'; +} + +local SoundQueue = {} + +local function EaseOutCirc(currentTime, startValue, deltaValue, duration) + currentTime = currentTime / duration; + currentTime = currentTime - 1; + return deltaValue * math.sqrt(1 - currentTime*currentTime) + startValue; +end + +local function IsGameRunning() + if not UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows then + return true + end + return runService:IsRunning() +end + +GetSoundManager = function() + local this = {} + + local rawVolumes = {} + + local function FindSoundObjectForName(soundName) + if SoundQueue[soundName] then + local soundObj = table.remove(SoundQueue[soundName], 1) + if soundObj then + table.insert(SoundQueue[soundName], #SoundQueue[soundName], soundObj) + return soundObj + end + end + end + + function this:CreateSound(soundName) + local fileName = SOUNDS[soundName] + local soundsUrl = BASE_SOUND_URL .. fileName + + local soundObj = Instance.new('Sound') + soundObj.Name = soundName + soundObj.SoundId = soundsUrl + + return soundObj + end + + function this:Play(soundName, vol, isLoop, pitch, ...) + local result = nil + + if SOUNDS[soundName] then + local soundObj = FindSoundObjectForName(soundName) + if soundObj then + soundObj.Volume = vol or 1 + + soundObj.Looped = isLoop or false + soundObj.Pitch = pitch or 1 + + soundObj:Play(...) + + rawVolumes[soundObj] = soundObj.Volume + if not IsGameRunning() then + soundObj.Volume = 0 + end + + result = soundObj + else + Utility.DebugLog("No sound:" , soundName , "in the queue.") + end + else + spawn(function() + error("Unable to find sound: " .. tostring(soundName)) + end) + end + + return result + end + + function this:IsPlaying(soundName) + local sound = this.SoundHolder:FindFirstChild(soundName) + if sound then + return sound.IsPlaying + end + return false + end + + function this:Stop(soundName, ...) + if this.SoundHolder and SOUNDS[soundName] then + local soundObj = this.SoundHolder:FindFirstChild(soundName) + if soundObj then + soundObj:Stop() + end + end + end + + function this:TweenSound(soundObj, newVolume, duration) + rawVolumes[soundObj] = nil + Utility.PropertyTweener(soundObj, 'Volume', soundObj.Volume, newVolume, duration, + function(...) + return IsGameRunning() and Utility.EaseInOutQuad(...) or 0 + end, + true, + function() + rawVolumes[soundObj] = newVolume + end) + end + + -- function this:PlayOnEvent(event, soundName) + -- event:connect(function() + + -- end) + -- end + + local function Initialize() + local appshellSounds = Instance.new('Folder') + appshellSounds.Name = 'AppShellSounds' + appshellSounds.Parent = CoreGui + + this.SoundHolder = appshellSounds + + for name, fileName in pairs(SOUNDS) do + local soundsForFile = Instance.new('Folder') + soundsForFile.Name = name + soundsForFile.Parent = this.SoundHolder + + SoundQueue[name] = {} + for i = 1, 3 do + local soundObj = this:CreateSound(name) + soundObj.Parent = soundsForFile + table.insert(SoundQueue[name], soundObj) + end + end + + local lastSelection = nil + GuiService:GetPropertyChangedSignal('SelectedCoreObject'):connect(function() + local currentSelection = GuiService.SelectedCoreObject + if currentSelection and lastSelection then + local moveSelectionSound = currentSelection:FindFirstChild('MoveSelection') + if moveSelectionSound and moveSelectionSound:IsA('Sound') then + moveSelectionSound.Volume = 0.35 + moveSelectionSound:Play() + end + end + lastSelection = currentSelection + end) + + if not IsGameRunning() then + spawn(function() + while not IsGameRunning() do + wait(0.1) + end + for soundObj, rawVolume in pairs(rawVolumes) do + soundObj.Volume = rawVolume + end + end) + end + end + + Initialize() + + return this +end + +return GetSoundManager() diff --git a/Client2018/content/internal/AppShell/Modules/Shell/StorePane.lua b/Client2018/content/internal/AppShell/Modules/Shell/StorePane.lua new file mode 100644 index 0000000..3c45e91 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/StorePane.lua @@ -0,0 +1,584 @@ +--[[ + // StorePane.lua by Kip Turner +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local UserDataModule = require(ShellModules:FindFirstChild('UserData')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local AssetManager = require(ShellModules:FindFirstChild('AssetManager')) +local CreateConfirmPrompt = require(ShellModules:FindFirstChild('ConfirmPrompt')) +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local PlatformCatalogData = require(ShellModules:FindFirstChild('PlatformCatalogData')) +local CurrencyWidgetModule = require(ShellModules:FindFirstChild('CurrencyWidget')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) + +local RobuxBalanceOverlay = require(ShellModules:FindFirstChild('RobuxBalanceOverlay')) + +local ErrorOverlayModule = require(ShellModules:FindFirstChild('ErrorOverlay')) +local Errors = require(ShellModules:FindFirstChild('Errors')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local TextService = game:GetService('TextService') +local GuiService = game:GetService('GuiService') +local PlatformService; +pcall(function() PlatformService = game:GetService('PlatformService') end) + +--[[ Constants ]]-- +local GRID_COLUMNS = 3 +local GRID_SIZE = UDim2.new(0, 1620, 0, 610) +local GRID_POSITION = UDim2.new(0, 40, 0, 65) +local GRID_PADDING = UDim2.new(0, 20, 0, 20) +local GRID_CELL_SIZE = UDim2.new(0, 520, 0, 285) +local DESCRIPTION_SIZE = UDim2.new(1, 0, 0, 50) +local PRICE_CORNER_OFFSET = Vector2.new(-15, -12) +local NO_ITEMS_MSG_POSITION = UDim2.new(0.1, 0, 0, 275) +local NO_ITEMS_MSG_SIZE = UDim2.new(0.8, 0, 0, 150) + +local ROBUX_ASSETS = +{ + { + Wide = 'Robux01.png'; + Square = 'RobuxSquare01.png'; + }; + { + Wide = 'Robux02.png'; + Square = 'RobuxSquare02.png'; + }; + { + Wide = 'Robux03.png'; + Square = 'RobuxSquare03.png'; + }; + { + Wide = 'Robux04.png'; + Square = 'RobuxSquare04.png'; + }; + { + Wide = 'Robux05.png'; + Square = 'RobuxSquare05.png'; + }; + { + Wide = 'Robux06.png'; + Square = 'RobuxSquare06.png'; + }; +} + +local function createGridItem(productInfo) + local this = {} + + local container = Utility.Create'ImageButton' + { + Name = "StoreItemContainer", + BackgroundTransparency = 1, + BackgroundColor3 = Color3.new(220/255, 220/255, 220/255), + -- Image = '', + AutoButtonColor = false, + BorderSizePixel = 0, + ZIndex = 2; + AssetManager.CreateShadow(1); + + SoundManager:CreateSound('MoveSelection'); + } + local priceText = Utility.Create'TextLabel' + { + Name = "PriceText", + Size = UDim2.new(0, 0, 0, 0), + Position = UDim2.new(1, -15, 1, -12), + BackgroundTransparency = 1, + TextXAlignment = Enum.TextXAlignment.Right, + TextYAlignment = 'Bottom'; + TextColor3 = GlobalSettings.WhiteTextColor, + Font = GlobalSettings.HeadingFont, + FontSize = GlobalSettings.LargeFontSize, + Text = '', + ZIndex = 2; + Parent = container, + } + + local robuxIcon = Utility.Create'ImageLabel' + { + Name = 'RobuxIcon'; + Position = UDim2.new(0,5,0,5); + Size = UDim2.new(0,80,0,80); + Image = 'rbxasset://textures/ui/Shell/Icons/ROBUXIcon@1080.png'; + BackgroundTransparency = 1; + ZIndex = 2; + Parent = container; + }; + local robuxAmount = Utility.Create'TextLabel' + { + Name = 'RobuxAmount'; + Text = ''; + Size = UDim2.new(0,0,1,0); + Position = UDim2.new(1,10,0,0); + TextXAlignment = 'Left'; + TextColor3 = GlobalSettings.WhiteTextColor; + Font = GlobalSettings.BoldFont; + FontSize = GlobalSettings.LargeFontSize; + BackgroundTransparency = 1; + ZIndex = 2; + Parent = robuxIcon; + }; + local percentMoreText = Utility.Create'TextLabel' + { + Name = "PercentMoreText", + Size = UDim2.new(0, 0, 0, 0), + Position = UDim2.new(0, 5, 1, 10), + BackgroundTransparency = 1, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = GlobalSettings.GreenTextColor, + Font = GlobalSettings.BoldFont, + FontSize = GlobalSettings.ButtonSize, + -- Visible = false, + Text = '', + ZIndex = 2; + Parent = robuxIcon, + } + + + local function UpdateInfo() + local priceTextSize = TextService:GetTextSize(priceText.Text, Utility.ConvertFontSizeEnumToInt(priceText.FontSize), priceText.Font, Vector2.new()) + priceText.Size = UDim2.new(0, priceTextSize.x , 0, priceTextSize.y) + priceText.Position = UDim2.new(1, PRICE_CORNER_OFFSET.x - priceTextSize.x, 1, PRICE_CORNER_OFFSET.y - priceTextSize.y) + end + + UpdateInfo() + + function this:GetContainer() + return container + end + + function this:SetPrice(value) + priceText.Text = value + UpdateInfo() + end + + function this:SetRobuxValue(value) + robuxAmount.Text = Utility.FormatNumberString(tostring(value)) + end + + function this:SetPercentMore(value) + percentMoreText.Visible = value > 0 + percentMoreText.Text = string.format(Strings:LocalizedString('PercentMoreRobuxPhrase'), tostring(value)) + end + + function this:SetImage(image) + container.Image = image + end + + return this +end + +local function CreateStorePane(parent) + local this = {} + local gridItems = {} + local itemSet = {} + + local storeItemClickConns = {} + + local cachedBalance = nil + local cachedTotalBalance = nil + local inFocus = false + + local currencyWidget = nil + + local StorePaneContainer = Utility.Create'Frame' + { + Name = 'StorePane', + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Visible = false, + Parent = parent, + } + local StoreDescriptionText = Utility.Create'TextLabel' + { + Name = "StoreDescriptionText", + Size = DESCRIPTION_SIZE, + Position = UDim2.new(0, 0, 0, 0), + BackgroundTransparency = 1, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = GlobalSettings.WhiteTextColor, + Font = GlobalSettings.LightFont, + FontSize = GlobalSettings.TitleSize, + TextWrapped = true, + Visible = false; + Text = Strings:LocalizedString('RobuxStoreDescription'), + Parent = StorePaneContainer, + } + local StoreNoItemsText = Utility.Create'TextLabel' + { + Name = "StoreNoItemsText", + Size = NO_ITEMS_MSG_SIZE, + Position = NO_ITEMS_MSG_POSITION, + BackgroundTransparency = 1, + TextXAlignment = Enum.TextXAlignment.Center, + FontSize = GlobalSettings.TitleSize, + TextWrapped = true, + + TextColor3 = GlobalSettings.GreyTextColor; + TextTransparency = GlobalSettings.FriendStatusTextTransparency; + Font = GlobalSettings.BoldFont; + + Text = Strings:LocalizedString('RobuxStoreNoItemsPhrase'), + Visible = false; + Parent = StorePaneContainer, + } + + local RobuxBalanceButton = Utility.Create'ImageButton' + { + Name = "RobuxBalanceButton"; + -- size will change based on text bounds of balance + Size = UDim2.new(0, 436, 0, 75); + Position = UDim2.new(0, 0, 1, -100); + BackgroundTransparency = 1; + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.GreySelectionColor; + Selectable = false; + Parent = StorePaneContainer; + + SoundManager:CreateSound('MoveSelection') + }; + RobuxBalanceButton.SelectionGained:connect(function() + RobuxBalanceButton.BackgroundTransparency = 0; + end) + RobuxBalanceButton.SelectionLost:connect(function() + RobuxBalanceButton.BackgroundTransparency = 1; + end) + + local RobuxHelpIcon = Utility.Create'ImageLabel' + { + Name = "RobuxHelpIcon"; + Size = UDim2.new(0, 42, 0, 42); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/HelpIconSmall.png'; + Visible = false; + Parent = RobuxBalanceButton; + AnchorPoint = Vector2.new(0, 0.5); + Position = UDim2.new(0, 10, 0.5, 4); + }; + + local function setBalanceButtonSize(balanceObjectSize) + local sizeX = 56 + RobuxHelpIcon.Size.X.Offset + (balanceObjectSize and balanceObjectSize.X or 0) + 10 + RobuxBalanceButton.Size = UDim2.new(0, sizeX, 0, 75) + end + + + local showBalanceHelp = true + local showBalanceOverlayDebounce = false + RobuxBalanceButton.MouseButton1Click:connect(function() + if showBalanceOverlayDebounce then return end + -- + showBalanceOverlayDebounce = true + if showBalanceHelp then + local robuxBalanceOverlay = RobuxBalanceOverlay(cachedBalance, cachedTotalBalance) + ScreenManager:OpenScreen(robuxBalanceOverlay, false) + end + showBalanceOverlayDebounce = false + end) + + local function setBalanceHelpOption(platformBalance) + local totalBalance = UserDataModule.GetTotalUserBalanceAsync() + if totalBalance then + cachedTotalBalance = totalBalance + showBalanceHelp = platformBalance ~= totalBalance + RobuxHelpIcon.Visible = showBalanceHelp + RobuxBalanceButton.Selectable = showBalanceHelp + end + end + + local function PopulateBalance() + spawn(function() + local platformBalance = currencyWidget and currencyWidget:GetRobuxAmountAsync() or UserDataModule.GetPlatformUserBalanceAsync() + if platformBalance then + cachedBalance = platformBalance + setBalanceHelpOption(platformBalance) + if currencyWidget then + setBalanceButtonSize(currencyWidget:GetAbsoluteSize()) + end + else + Utility.DebugLog("Unable to update user's balance because web call failed.") + end + end) + end + + + local StoreContainer = Utility.Create'Frame' + { + Size = GRID_SIZE; + Name = "StoreContainer"; + BackgroundTransparency = 1; + ClipsDescendants = true; + Position = GRID_POSITION; + Parent = StorePaneContainer; + } + local StoreUIGridLayout = Utility.Create'UIGridLayout' + { + Name = "StoreUIGridLayout"; + CellPadding = GRID_PADDING; + CellSize = GRID_CELL_SIZE; + FillDirectionMaxCells = GRID_COLUMNS; + Parent = StoreContainer; + }; + + local function ContainsItem(gridItem) + return itemSet[gridItem] ~= nil + end + + local function AddItem(gridItem) + if not ContainsItem(gridItem) then + table.insert(gridItems, gridItem) + itemSet[gridItem] = true + gridItem.Parent = StoreContainer + if GuiService.SelectedCoreObject == StoreContainer then + Utility.SetSelectedCoreObject(gridItem) + end + end + end + + local SuccessfullyLoadedCatalog = false + local catalogLoading = false + local function OnLoad() + if catalogLoading or SuccessfullyLoadedCatalog then return end + catalogLoading = true + + if PlatformService then + local catalogInfo, success, errormsg; + + -- Hide these text labels while we are loading + StoreDescriptionText.Visible = false + StoreNoItemsText.Visible = false + + local loader = LoadingWidget({Parent = StorePaneContainer}, {function() catalogInfo, success, errormsg = PlatformCatalogData:GetCatalogInfoAsync() end}) + loader:AwaitFinished() + loader:Cleanup() + loader = nil + ScreenManager:DefaultFadeIn(StoreContainer) + if inFocus and GuiService.SelectedCoreObject == nil then + if gridItems[1] then + Utility.SetSelectedCoreObject(gridItems[1]) + end + end + + if success and catalogInfo then + while #storeItemClickConns > 0 do + Utility.DisconnectEvent(table.remove(storeItemClickConns, 1)) + end + + table.sort(catalogInfo, function(a, b) + local aPrice = PlatformCatalogData:ParseRobuxValue(a) + local bPrice = PlatformCatalogData:ParseRobuxValue(b) + if aPrice and bPrice then + return aPrice < bPrice + end + return a < b + end) + + local worstRatio = nil + for _, productInfo in pairs(catalogInfo) do + local ratio = PlatformCatalogData:CalculateRobuxRatio(productInfo) + if Utility.IsFinite(ratio) and ratio ~= 0 then + if worstRatio == nil or ratio < worstRatio then + worstRatio = ratio + end + end + end + + if #catalogInfo == 0 then + StoreDescriptionText.Visible = false + StoreNoItemsText.Visible = true + elseif not SuccessfullyLoadedCatalog then + SuccessfullyLoadedCatalog = true + + StoreDescriptionText.Visible = true + StoreNoItemsText.Visible = false + + local i = 1 + for _, productInfo in pairs(catalogInfo) do + local productImageData = ROBUX_ASSETS[math.min(i, #ROBUX_ASSETS)] + local catalogItemImage = 'rbxasset://textures/ui/Shell/Images/Robux/' .. productImageData['Wide'] + local confirmItemImage = 'rbxasset://textures/ui/Shell/Images/Robux/' .. productImageData['Square'] + + local debounce = false + local function onClick() + if debounce then return end + debounce = true + + local confirmPrompt = CreateConfirmPrompt({ProductId = productInfo and productInfo.ProductId or "Unknown"; ProductName = productInfo and productInfo.Name or 'Unknown'; Balance = cachedBalance; Cost = productInfo and productInfo.DisplayPrice or "Unknown"; ProductImage = confirmItemImage; ProductImageSize = Vector2.new(484, 540); CurrencySymbol = '';}, + {ShowRemainingBalance = false; ShowRobuxIcon = false; ConfirmWithPrice = true;}) + + do + local selectedObject = GuiService.SelectedCoreObject + if selectedObject and selectedObject:IsDescendantOf(StorePaneContainer) then + this.SavedSelection = selectedObject + end + end + + ScreenManager:OpenScreen(confirmPrompt) + confirmPrompt:FadeInBackground() + local function onConfirmFinished(result) + if result == true then + Utility.DebugLog("Do buy") + local purchaseResult = nil + if not UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows then + local purchaseCallSuccess, purchaseErrorMsg = pcall(function() + purchaseResult = PlatformService:BeginPlatformStorePurchase(productInfo.ProductId) + end) + if purchaseCallSuccess then + -- 0 means we bought it + if purchaseResult == 0 then + EventHub:dispatchEvent(EventHub.Notifications["RobuxCatalogPurchaseInitiated"], purchaseResult); + end + else + Utility.DebugLog("Purchase Robux failed with pcall status:" , purchaseCallSuccess , "and purchaseResult:" , purchaseResult , "because of:" , purchaseErrorMsg) + end + else + spawn(function() + ScreenManager:OpenScreen(ErrorOverlayModule(Errors.RobuxPurchase[1]), false) + end) + end + PopulateBalance() + Utility.DebugLog("Done with purchase; result was:" , purchaseResult) + else + Utility.DebugLog("Declined to buy") + end + end + -- confirmPrompt:AddResultCallback(onConfirmFinished) + local result = confirmPrompt:ResultAsync() + onConfirmFinished(result) + + debounce = false + end + + local extractedPrice = productInfo and productInfo.DisplayPrice or "" + local thisRatio = PlatformCatalogData:CalculateRobuxRatio(productInfo) + + local item = createGridItem() + item:SetPrice(extractedPrice) + item:SetRobuxValue(PlatformCatalogData:ParseRobuxValue(productInfo)) + if thisRatio and worstRatio then + item:SetPercentMore(math.floor(((thisRatio / worstRatio) - 1) * 100)) + else + item:SetPercentMore(0) + end + item:SetImage(catalogItemImage) + AddItem(item:GetContainer()) + table.insert(storeItemClickConns, item:GetContainer().MouseButton1Click:connect(onClick)) + + i = math.min(#ROBUX_ASSETS, i + 1) + end + end + else + StoreNoItemsText.Visible = true + Utility.DebugLog("StorePane - BeginGetCatalogInfo failed because:" , errormsg) + end + end + catalogLoading = false + end + + if not SuccessfullyLoadedCatalog then + spawn(OnLoad) + end + + --[[ Public API ]]-- + function this:GetName() + return Strings:LocalizedString('CatalogWord') + end + + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('StorePaneId')} + end + + function this:IsFocused() + return inFocus + end + + + local RobuxChangedConn = nil + local robuxChangedEventCount = 0 + local robuxAmountChangedLoader = nil + + function this:Show() + StorePaneContainer.Visible = true + + if not currencyWidget then + currencyWidget = CurrencyWidgetModule({Parent = RobuxBalanceButton; Position = UDim2.new(0, RobuxHelpIcon.Position.X.Offset + RobuxHelpIcon.Size.X.Offset + 10, 0.5, -30);}) + else + spawn(function() + currencyWidget:RefreshRobuxAmountAsync() + end) + end + setBalanceButtonSize(currencyWidget:GetAbsoluteSize()) + Utility.DisconnectEvent(RobuxChangedConn) + RobuxChangedConn = currencyWidget.RobuxChanged:connect(function() + PopulateBalance() + currencyWidget:GetRobuxAmountAsync() + setBalanceButtonSize(currencyWidget:GetAbsoluteSize()) + SoundManager:Play('PurchaseSuccess') + end) + PopulateBalance() + + self.TransitionTweens = ScreenManager:DefaultFadeIn(StorePaneContainer) + ScreenManager:PlayDefaultOpenSound() + + if not SuccessfullyLoadedCatalog then + spawn(OnLoad) + end + end + + function this:Hide() + StorePaneContainer.Visible = false + + RobuxChangedConn = Utility.DisconnectEvent(RobuxChangedConn) + + ScreenManager:DefaultCancelFade(self.TransitionTweens) + self.TransitionTweens = nil + + -- Let's not do this it creates weird race conditions + -- if currencyWidget then + -- currencyWidget:Destroy() + -- currencyWidget = nil + -- end + end + + function this:Focus() + -- TODO: What is the selection if the packages fail to load? + inFocus = true + if self.SavedSelection and self.SavedSelection:IsDescendantOf(StorePaneContainer) then + Utility.SetSelectedCoreObject(self.SavedSelection) + elseif gridItems[1] then + Utility.SetSelectedCoreObject(gridItems[1]) + end + self.SavedSelection = nil + end + + function this:RemoveFocus() + inFocus = false + local selectedObject = GuiService.SelectedCoreObject + if selectedObject and selectedObject:IsDescendantOf(StorePaneContainer) then + Utility.SetSelectedCoreObject(nil) + end + end + + function this:SetPosition(newPosition) + StorePaneContainer.Position = newPosition + end + + function this:SetParent(newParent) + StorePaneContainer.Parent = newParent + end + + function this:IsAncestorOf(object) + return StorePaneContainer and StorePaneContainer:IsAncestorOf(object) + end + + return this +end + +return CreateStorePane diff --git a/Client2018/content/internal/AppShell/Modules/Shell/TabDock.lua b/Client2018/content/internal/AppShell/Modules/Shell/TabDock.lua new file mode 100644 index 0000000..cb33500 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/TabDock.lua @@ -0,0 +1,356 @@ +-- Written by Kip Turner, Copyright Roblox 2015 + +local GuiService = game:GetService('GuiService') +local ContextActionService = game:GetService("ContextActionService") +local UserInputService = game:GetService("UserInputService") + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local Utility = require(ShellModules:FindFirstChild('Utility')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local TAB_DOCK_HEIGHT = 36 + +local function CreateTabDock(restPosition, offscreenPosition) + local this = {} + + local Tabs = {} + local SelectedTab = nil + local SizeChangedConns = {} + this.SelectedTabChanged = Utility.Signal() + this.SelectedTabClicked = Utility.Signal() + this.TabItemClickedConn = nil + local guiServiceChangedCn = nil + + local TabContainer = Utility.Create'ImageButton' + { + Size = UDim2.new(1, 0, 0, TAB_DOCK_HEIGHT); + Position = restPosition; + BackgroundTransparency = 1; + Name = 'TabContainer'; + } + + local SelectionBorderObject = Utility.Create'ImageLabel' + { + Name = 'SelectionBorderObject'; + Size = UDim2.new(1,0,0,4); + Position = UDim2.new(0,0,1,5); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.TabUnderlineColor; + -- Image = 'rbxasset://textures/ui/SelectionBox.png'; + -- ScaleType = Enum.ScaleType.Slice; + -- SliceCenter = Rect.new(19,19,43,43); + BackgroundTransparency = 0; + } + + local DownSelector = Utility.Create'ImageButton' + { + Size = UDim2.new(1, 0, 0, TAB_DOCK_HEIGHT); + BackgroundTransparency = 1; + Name = 'DownSelector'; + Selectable = false; + Parent = TabContainer; + } + + DownSelector.SelectionGained:connect(function() + if SelectedTab then + Utility.SetSelectedCoreObject(SelectedTab:GetGuiObject()) + this.SelectedTabClicked:fire(SelectedTab) + end + end) + + local function onGuiServiceChanged(prop) + if prop == 'SelectedCoreObject' then + + if GuiService.SelectedCoreObject == TabContainer then + local currentTab = this:GetSelectedTab() + local currentTabItem = currentTab and currentTab:GetGuiObject() + if currentTabItem then + Utility.SetSelectedCoreObject(currentTabItem) + end + end + + local selectedObject = GuiService.SelectedCoreObject + local focusedTab = this:FindFocusedTabByGuiObject(selectedObject) + + if SelectedTab and selectedObject ~= SelectedTab:GetGuiObject() then + SelectedTab:OnClickRelease() + end + + if focusedTab then + this:SetSelectedTab(focusedTab) + + for _, inputObject in pairs(UserInputService:GetGamepadState(Enum.UserInputType.Gamepad1)) do + if inputObject.KeyCode == Enum.KeyCode.ButtonA and inputObject.UserInputState == Enum.UserInputState.Begin then + if SelectedTab then + SelectedTab:OnClick() + end + end + end + + ContextActionService:UnbindCoreAction("OnClickSelectedTab") + + ContextActionService:BindCoreAction("OnClickSelectedTab", + function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + if SelectedTab then + SelectedTab:OnClick() + end + elseif inputState == Enum.UserInputState.End then + if SelectedTab then + SelectedTab:OnClickRelease() + end + this.SelectedTabClicked:fire(SelectedTab) + end + end, + false, + Enum.KeyCode.ButtonA) + else + ContextActionService:UnbindCoreAction("OnClickSelectedTab") + end + end + end + + --Never shown-- + function this:GetAnalyticsInfo() + return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('TabDockId')} + end + + function this:FindFocusedTabByGuiObject(selectedObject) + -- NOTE: This is a sort of cheater way of culling look-up checks + if selectedObject and selectedObject:IsDescendantOf(TabContainer) then + for _, currTab in pairs(Tabs) do + local guiObject = currTab and currTab:GetGuiObject() + if guiObject and guiObject == selectedObject then + return currTab + end + end + end + end + + function this:IsFocused() + local selectedObject = GuiService.SelectedCoreObject + return self:FindFocusedTabByGuiObject(selectedObject) ~= nil + end + + function this:SetSelectedTab(newSelectedTab) + if newSelectedTab ~= SelectedTab then + local prevSelectedTab = SelectedTab + if SelectedTab then + SelectedTab:SetSelected(false) + SelectedTab:OnClickRelease() + end + + SelectedTab = newSelectedTab + + if SelectedTab then + SelectedTab:SetSelected(true) + if self:IsFocused() then + local currentTabItem = SelectedTab and SelectedTab:GetGuiObject() + if currentTabItem then + Utility.SetSelectedCoreObject(currentTabItem) + end + end + end + this.SelectedTabChanged:fire(SelectedTab) + end + end + + function this:Focus() + self:Show() + if SelectedTab then + SelectedTab:SetSelected(true) + local currentTabItem = SelectedTab and SelectedTab:GetGuiObject() + if currentTabItem then + Utility.SetSelectedCoreObject(currentTabItem) + end + end + end + + function this:GetSelectedTab() + return SelectedTab + end + + local arrangeCount = 0 + function this:ArrangeTabs() + arrangeCount = arrangeCount + 1 + local currentCount = arrangeCount + + local x = 0 + for i, tabItem in pairs(Tabs) do + local tabItemSize = tabItem:GetSize() + local xSize = tabItemSize.X.Offset + + local spacing = GlobalSettings.TabItemSpacing + if i == 1 then + spacing = 0 + end + -- Stop recursion in its tracks + if currentCount == arrangeCount then + tabItem:SetPosition(UDim2.new(0, x + spacing, 0, 0)) + + local tabItemGuiObject = tabItem:GetGuiObject() + if tabItemGuiObject then + local prevItemGuiObject = Tabs[i-1] and Tabs[i-1]:GetGuiObject() + local nextItemGuiObject = Tabs[i+1] and Tabs[i+1]:GetGuiObject() + tabItemGuiObject.NextSelectionLeft = prevItemGuiObject or tabItemGuiObject + tabItemGuiObject.NextSelectionRight = nextItemGuiObject or tabItemGuiObject + end + + else + return + end + x = x + spacing + xSize + end + end + + function this:FindTabIndex(tab) + for i, currTab in pairs(Tabs) do + if tab == currTab then + return i + end + end + end + + function this:ContainsTab(tab) + return self:FindTabIndex(tab) ~= nil + end + + function this:GetTab(index) + return Tabs[index] + end + + function this:GetNextTab() + if SelectedTab then + local index = this:FindTabIndex(SelectedTab) + return index and Tabs[index + 1] + end + end + + function this:GetPreviousTab() + if SelectedTab then + local index = this:FindTabIndex(SelectedTab) + return index and Tabs[index - 1] + end + end + + function this:AddTab(newTab) + local existingIndex = self:FindTabIndex(newTab) + if existingIndex then + Utility.DebugLog("Not adding tab:" , newTab:GetName() , "because that tab already exists.") + return + end + + local guiObject = newTab and newTab:GetGuiObject() + if guiObject then + guiObject.SelectionImageObject = SelectionBorderObject + guiObject.NextSelectionDown = DownSelector + end + + table.insert(Tabs, newTab) + newTab:SetParent(TabContainer) + + Utility.DisconnectEvent(SizeChangedConns[newTab]) + SizeChangedConns[newTab] = newTab.SizeChanged:connect(function() + self:ArrangeTabs() + end) + + self:ArrangeTabs() + + return newTab + end + + function this:RemoveTab(tab) + local removeIndex = self:FindTabIndex(tab) + + if removeIndex then + table.remove(Tabs, removeIndex) + if tab == SelectedTab then + this:SetSelectedTab(nil) + end + tab:SetParent(nil) + Utility.DisconnectEvent(SizeChangedConns[tab]) + self:ArrangeTabs() + return true + end + + return false + end + + function this:SetParent(newParent) + TabContainer.Parent = newParent + end + + function this:ConnectEvents() + onGuiServiceChanged('SelectedCoreObject') + guiServiceChangedCn = GuiService.Changed:connect(onGuiServiceChanged) + end + + function this:DisconnectEvents() + if guiServiceChangedCn then + guiServiceChangedCn:disconnect() + guiServiceChangedCn = nil + end + end + + local motionDuration = GlobalSettings.TabDockTweenDuration + + local function FadeText(element, tweens, a, b, duration) + if element == nil then return end + if element:IsA('TextLabel') or element:IsA('TextBox') or element:IsA('TextButton') then + table.insert(tweens, Utility.PropertyTweener(element, 'TextTransparency', a, b, duration, Utility.EaseOutQuad)) + end + for _, child in pairs(element:GetChildren()) do + FadeText(child, tweens, a, b, duration) + end + end + + local tweens = {} + + local showing = false; + + function this:Hide() + if not showing then return end + showing = false; + + Utility.CancelTweens(tweens) + + local positionTweener = Utility.PropertyTweener( + TabContainer, + 'Position', + restPosition, + offscreenPosition, + motionDuration, + Utility.SCurveUDim2 + ) + table.insert(tweens, positionTweener) + + FadeText(TabContainer, tweens, 0, 1, motionDuration) + end + + function this:Show() + if showing then return end + showing = true; + + Utility.CancelTweens(tweens) + + local positionTweener = Utility.PropertyTweener( + TabContainer, + 'Position', + offscreenPosition, + restPosition, + motionDuration, + Utility.SCurveUDim2 + ) + table.insert(tweens, positionTweener) + + FadeText(TabContainer, tweens, 1, 0, motionDuration) + end + + return this +end + +return CreateTabDock diff --git a/Client2018/content/internal/AppShell/Modules/Shell/TabDockItem.lua b/Client2018/content/internal/AppShell/Modules/Shell/TabDockItem.lua new file mode 100644 index 0000000..acb33d4 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/TabDockItem.lua @@ -0,0 +1,159 @@ +-- Written by Kip Turner, Copyright Roblox 2015 + +local TextService = game:GetService('TextService') +local UserInputService = game:GetService('UserInputService') + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Analytics = require(ShellModules:FindFirstChild('Analytics')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) + +local function CreateTabDockItem(tabName, contentItem) + local this = {} + local name = tabName + local selected = false + local focused = false + local content = contentItem + + this.SizeChanged = Utility.Signal() + this.Clicked = Utility.Signal() + + local tabItem = Utility.Create'TextLabel' + { + Size = UDim2.new(0, 100, 1, 0); + BackgroundTransparency = 1; + Name = 'TabItem'; + Selectable = false; + Text = ""; + } + + local tabText = Utility.Create'TextLabel' + { + Text = name; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + Name = 'LargeText'; + FontSize = GlobalSettings.HeaderSize; + Font = GlobalSettings.LightFont; + TextColor3 = GlobalSettings.WhiteTextColor; + Visible = true; + Parent = tabItem; + Selectable = false; + } + do + local tabItemTextSize = TextService:GetTextSize(tabText.Text, Utility.ConvertFontSizeEnumToInt(tabText.FontSize), tabText.Font, Vector2.new()) + tabItem.Size = UDim2.new(0,tabItemTextSize.X + 20,1,0) + this.SizeChanged:fire(tabItem.Size) + end + local smallText = Utility.Create'TextLabel' + { + Text = name; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0.5, 0, 0.5, 0); + BackgroundTransparency = 1; + Name = 'SmallText'; + FontSize = GlobalSettings.MediumFontSize; + Font = GlobalSettings.LightFont; + TextColor3 = GlobalSettings.WhiteTextColor; + BackgroundTransparency = 1; + Visible = false; + Selectable = false; + Parent = tabItem; + } + + local function SetRBXEventStream_Screen(screen, status) + if screen and type(screen.GetAnalyticsInfo) == "function" then + local screenAnalyticsInfo = screen:GetAnalyticsInfo() + if type(screenAnalyticsInfo) == "table" and screenAnalyticsInfo[Analytics.WidgetNames('WidgetId')] then + screenAnalyticsInfo.Status = status + Analytics.SetRBXEventStream("Widget", screenAnalyticsInfo) + end + end + end + + local function OnSelectionChanged() + if selected then + tabText.TextColor3 = GlobalSettings.WhiteTextColor; + SetRBXEventStream_Screen(content, "Select") + else + tabText.TextColor3 = GlobalSettings.BlueTextColor; + end + end + + function this:GetContentItem() + return content + end + + function this:GetGuiObject() + return tabItem + end + + function this:SetSelected(isSelected) + if selected ~= isSelected then + selected = isSelected + OnSelectionChanged() + end + end + + function this:GetSelected() + return selected + end + + function this:GetName() + return name + end + + function this:GetSize() + return tabItem.Size + end + + function this:OnClick() + smallText.Visible = true; + tabText.Visible = false; + SoundManager:Play('ButtonPress') + end + + function this:OnClickRelease() + smallText.Visible = false; + tabText.Visible = true; + end + + function this:SetFocused(nowFocused) + if focused ~= nowFocused then + focused = nowFocused + + if focused then + for _, inputObject in pairs(UserInputService:GetGamepadState(Enum.UserInputType.Gamepad1)) do + if inputObject.KeyCode == Enum.KeyCode.ButtonA and + inputObject.UserInputState == Enum.UserInputState.Begin then + self:OnClick() + end + end + else + self:OnClickRelease() + end + + end + end + + function this:SetPosition(newPosition) + tabItem.Position = newPosition + end + + function this:SetParent(newParent) + tabItem.Parent = newParent + end + -- Initialize + OnSelectionChanged() + + return this +end + + +return CreateTabDockItem diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Templates/BaseScreen.lua b/Client2018/content/internal/AppShell/Modules/Shell/Templates/BaseScreen.lua new file mode 100644 index 0000000..cf37b80 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Templates/BaseScreen.lua @@ -0,0 +1,100 @@ +--[[ + // BaseScreen.lua + + // Creates a base screen with breadcrumbs and title. Do not use for a pane/tab +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local function createBaseScreen(controller) + local this = {} + + local container = Utility.Create'Frame' + { + Name = "Container"; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + } + local backButton = Utility.Create'ImageButton' + { + Name = "BackButton"; + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Lobby/Buttons/nine_slice_button.png'; + ImageColor3 = GlobalSettings.GreyButtonColor; + Size = UDim2.new(0,175,0,48); + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(9,9,39,39); + } + local backImage = Utility.Create'ImageLabel' + { + Name = "BackImage"; + Size = UDim2.new(0,48,0,48); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Shell/Icons/BackIcon@1080.png"; + Parent = container; + } + local backText = Utility.Create'TextLabel' + { + Name = "BackText"; + Size = UDim2.new(0, 0, 0, backImage.Size.Y.Offset); + Position = UDim2.new(0, backImage.Size.X.Offset + 8, 0, 0); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextXAlignment = Enum.TextXAlignment.Left; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = ""; + Parent = container; + } + local titleText = Utility.Create'TextLabel' + { + Name = "TitleText"; + Size = UDim2.new(0, 0, 0, 35); + Position = UDim2.new(0, 16, 0, backImage.Size.Y.Offset + 74); + BackgroundTransparency = 1; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.HeaderSize; + TextXAlignment = Enum.TextXAlignment.Left; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = ""; + Parent = container; + } + + --[[ Public API ]]-- + this.Container = container + this.BackImage = backImage + this.BackText = backText + this.TitleText = titleText + + function this:SetBackText(newText) + local TextService = game:GetService('TextService') + + local textSize = TextService:GetTextSize( + newText, + Utility.ConvertFontSizeEnumToInt(backText.FontSize), + backText.Font, + Vector2.new()) -- Essentially, we don't want to bound our textbox + + local spacing = (backButton.Size.Y.Offset - backImage.Size.Y.Offset) / 2 + + backText.Text = newText + + backButton.Size = UDim2.new(0, spacing * 2 + textSize.X + backImage.Size.X.Offset + 8, + backButton.Size.Y.Scale, backButton.Size.Y.Offset) + end + + this:SetBackText(controller:GetBackText()) + + backButton.MouseButton1Click:connect(function() + controller:OnBackButtonClick() + end) + + return this +end + +return createBaseScreen diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Templates/SettingsScreenBase.lua b/Client2018/content/internal/AppShell/Modules/Shell/Templates/SettingsScreenBase.lua new file mode 100644 index 0000000..c1d89e1 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Templates/SettingsScreenBase.lua @@ -0,0 +1,108 @@ + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local Templates = ShellModules:FindFirstChild("Templates") + + +local BaseScreen = require(Templates:FindFirstChild('BaseScreen')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local function createSettingsScreenBase(controller) + local this = BaseScreen(controller) + + local VersionBuildIdText = Utility.Create'TextLabel' + { + Name = "VersionBuildIdText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.TitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextXAlignment = Enum.TextXAlignment.Right; + TextYAlignment = Enum.TextYAlignment.Bottom; + Text = ''; + Parent = this.Container; + } + do + local versionInfo = controller:GetVersionInfo() + local versionStr = string.format(Strings:LocalizedString('VersionIdString'), tostring(versionInfo['Major']) , tostring(versionInfo['Minor']), tostring(versionInfo['Build']), tostring(versionInfo['Revision'])) + VersionBuildIdText.Text = versionStr + end + + + + local spacing = 40 + local DefaultTransparency = GlobalSettings.TextBoxDefaultTransparency + local SelectedTransparency = GlobalSettings.TextBoxSelectedTransparency + + local AccountButton = Utility.Create'TextButton' + { + Name = "AccountButton"; + Size = UDim2.new(0, 394, 0, 612); + Position = UDim2.new(0, 0, 0, 238); + BackgroundTransparency = DefaultTransparency; + BackgroundColor3 = GlobalSettings.TextBoxColor; + BorderSizePixel = 0; + Text = ""; + Parent = this.Container; + SoundManager:CreateSound('MoveSelection'); + } + this.AccountButton = AccountButton + this.Spacing = spacing + + + local AccountIcon = Utility.Create'ImageLabel' + { + Name = "AccountIcon"; + Size = UDim2.new(0, 256, 0, 256); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/AccountIcon.png'; + Parent = AccountButton; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + } + local AccountText = Utility.Create'TextLabel' + { + Name = "AccountText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0.5, 0, 1, -96); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.TitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = Strings:LocalizedString("AccountWord"); + Parent = AccountButton; + } + + AccountButton.SelectionGained:connect(function() + Utility.PropertyTweener(AccountButton, "BackgroundTransparency", SelectedTransparency, + SelectedTransparency, 0, Utility.EaseInOutQuad, true) + end) + AccountButton.SelectionLost:connect(function() + AccountButton.BackgroundTransparency = DefaultTransparency + end) + + --[[ Input ]]-- + AccountButton.MouseButton1Click:connect(function() + SoundManager:Play('ButtonPress') + controller:OpenAccountScreen() + end) + + + + --[[ Public API ]]-- + --Override + function this:GetDefaultSelectionObject() + return AccountButton + end + + return this +end + +return createSettingsScreenBase diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Templates/SettingsScreenConsole.lua b/Client2018/content/internal/AppShell/Modules/Shell/Templates/SettingsScreenConsole.lua new file mode 100644 index 0000000..b38d5db --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Templates/SettingsScreenConsole.lua @@ -0,0 +1,160 @@ + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local Templates = ShellModules:FindFirstChild("Templates") + +local SettingsScreenBase = require(Templates:FindFirstChild('SettingsScreenBase')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) + + +local function createSettingsScreenConsole(controller) + local this = SettingsScreenBase(controller) + + local DefaultTransparency = GlobalSettings.TextBoxDefaultTransparency + local SelectedTransparency = GlobalSettings.TextBoxSelectedTransparency + + local spacing = this.Spacing + + local SwitchProfileButton = Utility.Create'TextButton' + { + Name = "SwitchProfileButton"; + Size = UDim2.new(0, 394, 0, 612); + Position = UDim2.new(0, this.AccountButton.Size.X.Offset + spacing, 0, 238); + BackgroundTransparency = DefaultTransparency; + BackgroundColor3 = GlobalSettings.TextBoxColor; + BorderSizePixel = 0; + Text = ""; + Parent = this.Container; + SoundManager:CreateSound('MoveSelection'); + } + + local OverscanButton = SwitchProfileButton:Clone() + OverscanButton.Name = "OverscanButton" + OverscanButton.Position = UDim2.new(0, SwitchProfileButton.Position.X.Offset + SwitchProfileButton.Size.X.Offset + spacing, 0, 238) + OverscanButton.Parent = this.Container + + local HelpButton = OverscanButton:Clone() + HelpButton.Name = "HelpButton" + HelpButton.Position = UDim2.new(0, OverscanButton.Position.X.Offset + OverscanButton.Size.X.Offset + spacing, 0, 238) + HelpButton.Parent = this.Container + + + local SwitchProfileIcon = Utility.Create'ImageLabel' + { + Name = "SwitchProfileIcon"; + Size = UDim2.new(0, 224, 0, 255); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/ProfileIcon.png'; + Parent = SwitchProfileButton; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + } + local SwitchProfileText = Utility.Create'TextLabel' + { + Name = "SwitchProfileText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0.5, 0, 1, -96); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.TitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = Strings:LocalizedString("SwitchProfileWord"); + Parent = SwitchProfileButton; + } + + local OverscanIcon = Utility.Create'ImageLabel' + { + Name = "OverscanIcon"; + Size = UDim2.new(0, 256, 0, 181); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/TVIcon.png'; + Parent = OverscanButton; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + } + local OverscanText = SwitchProfileText:Clone() + OverscanText.Name = "OverscanText"; + OverscanText.Text = Strings:LocalizedString("OverscanWord"); + OverscanText.Parent = OverscanButton + + local HelpIcon = Utility.Create'ImageLabel' + { + Name = "HelpIcon"; + Size = UDim2.new(0, 256, 0, 256); + BackgroundTransparency = 1; + Image = 'rbxasset://textures/ui/Shell/Icons/HelpIcon.png'; + Parent = HelpButton; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + } + local HelpText = SwitchProfileText:Clone() + HelpText.Name = "HelpText"; + HelpText.Text = Strings:LocalizedString("HelpWord"); + HelpText.Parent = HelpButton + + + SwitchProfileButton.SelectionGained:connect(function() + Utility.PropertyTweener(SwitchProfileButton, "BackgroundTransparency", SelectedTransparency, + SelectedTransparency, 0, Utility.EaseInOutQuad, true) + end) + SwitchProfileButton.SelectionLost:connect(function() + SwitchProfileButton.BackgroundTransparency = DefaultTransparency + end) + OverscanButton.SelectionGained:connect(function() + Utility.PropertyTweener(OverscanButton, "BackgroundTransparency", SelectedTransparency, + SelectedTransparency, 0, Utility.EaseInOutQuad, true) + end) + OverscanButton.SelectionLost:connect(function() + OverscanButton.BackgroundTransparency = DefaultTransparency + end) + HelpButton.SelectionGained:connect(function() + Utility.PropertyTweener(HelpButton, "BackgroundTransparency", SelectedTransparency, + SelectedTransparency, 0, Utility.EaseInOutQuad, true) + end) + HelpButton.SelectionLost:connect(function() + HelpButton.BackgroundTransparency = DefaultTransparency + end) + + + local switchProfileDebounce = false + SwitchProfileButton.MouseButton1Click:connect(function() + SoundManager:Play('ButtonPress') + if switchProfileDebounce then return end + switchProfileDebounce = true + + controller:OpenSwitchProfileScreen() + + switchProfileDebounce = false + end) + + local overscanDebounce = false + OverscanButton.MouseButton1Click:connect(function() + SoundManager:Play('ButtonPress') + if overscanDebounce then return end + overscanDebounce = true + + controller:OpenOverscanScreen() + + overscanDebounce = false + end) + + local helpDebounce = false + HelpButton.MouseButton1Click:connect(function() + SoundManager:Play('ButtonPress') + if helpDebounce then return end + helpDebounce = true + + controller:OpenHelpScreen() + + helpDebounce = false + end) + + return this +end + +return createSettingsScreenConsole diff --git a/Client2018/content/internal/AppShell/Modules/Shell/TextBox.lua b/Client2018/content/internal/AppShell/Modules/Shell/TextBox.lua new file mode 100644 index 0000000..af3b708 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/TextBox.lua @@ -0,0 +1,171 @@ +--[[ + // TextBox.lua + + // Creates a custom TextBox object that uses the platform virtual keyboard. +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) + +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local function createTextBox(size) + local this = {} + + local spacing = Vector2.new() + local defaultText = "" + + local keyboardTitle = "" + local keyboardDescription = "" + local keyboardType = not UserSettings().GameSettings:InStudioMode() and Enum.XboxKeyBoardType.Default + local currentInputText = "" + + local keyboardClosedCn = nil + local isEnabled = true + + this.OnTextChanged = Utility.Signal() + + -- need custom selection box to fit with the spacing + local SelectionBox = Utility.Create'ImageLabel' + { + Name = "SelectionBox"; + Image = 'rbxasset://textures/ui/SelectionBox.png'; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(21,21,41,41); + BackgroundTransparency = 1; + } + + local TextBoxFrame = Utility.Create'ImageButton' + { + Name = "TextBoxFrame"; + Size = size; + BackgroundTransparency = 1; + ImageColor3 = GlobalSettings.TextBoxColor; + ImageTransparency = GlobalSettings.TextBoxDefaultTransparency; + Image = GlobalSettings.RoundCornerButtonImage; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28)); + + SoundManager:CreateSound('MoveSelection'); + } + local DefaultTextLabel = Utility.Create'TextLabel' + { + Name = "DefaultTextLabel"; + Size = UDim2.new(1, -spacing.x * 2, 1, -spacing.y * 2); + Position = UDim2.new(0, spacing.x, 0, spacing.y); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = GlobalSettings.TextSelectedColor; + TextXAlignment = Enum.TextXAlignment.Left; + ZIndex = 2; + Parent = TextBoxFrame; + } + + local function setTextBoxSizeAndPosition() + DefaultTextLabel.Size = UDim2.new(1, -spacing.x * 2, 1, -spacing.y * 2) + DefaultTextLabel.Position = UDim2.new(0, spacing.x, 0, spacing.y) + SelectionBox.Size = UDim2.new(1, spacing.x * 2 + 24, 1, spacing.y * 2 + 24) + SelectionBox.Position = UDim2.new(0, -spacing.x - 12, 0, -spacing.y - 12) + end + setTextBoxSizeAndPosition() + + --[[ Input ]]-- + local function onKeyboardClosed(inputText) + if UserSettings().GameSettings:InStudioMode() then + return + end + currentInputText = inputText + if #currentInputText == 0 then + DefaultTextLabel.Text = this:GetDefaultText() + elseif keyboardType == Enum.XboxKeyBoardType.Password then + DefaultTextLabel.Text = string.rep("*", #currentInputText) + else + DefaultTextLabel.Text = currentInputText + end + DefaultTextLabel.Visible = true + Utility.DisconnectEvent(keyboardClosedCn) + this.OnTextChanged:fire(currentInputText) + end + + TextBoxFrame.MouseButton1Click:connect(function() + if isEnabled then + if UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows then + Utility.DebugLog('Warning: virtual keyboard not accessable in studio') + elseif PlatformService then + DefaultTextLabel.Visible = false + PlatformService:ShowKeyboard(keyboardTitle, keyboardDescription, + currentInputText or "", keyboardType) + end + Utility.DisconnectEvent(keyboardClosedCn) + if PlatformService then + keyboardClosedCn = PlatformService.KeyboardClosed:connect(onKeyboardClosed) + end + end + end) + + TextBoxFrame.SelectionGained:connect(function() + Utility.PropertyTweener(TextBoxFrame, "ImageTransparency", GlobalSettings.TextBoxSelectedTransparency, + GlobalSettings.TextBoxSelectedTransparency, 0, Utility.EaseInOutQuad, true) + end) + TextBoxFrame.SelectionLost:connect(function() + Utility.PropertyTweener(TextBoxFrame, "ImageTransparency", GlobalSettings.TextBoxDefaultTransparency, + GlobalSettings.TextBoxDefaultTransparency, 0, Utility.EaseInOutQuad, true) + end) + + function this:SetKeyboardTitle(newTitle) + keyboardTitle = newTitle + end + function this:SetKeyboardDescription(newDescription) + keyboardDescription = newDescription + end + function this:SetKeyboardType(newType) + keyboardType = newType + end + function this:SetEnabled(value) + isEnabled = value + end + function this:SetParent(newParent) + TextBoxFrame.Parent = newParent + end + function this:SetPosition(newPosition) + TextBoxFrame.Position = newPosition + end + function this:SetSpacing(newSpacing) + spacing = newSpacing + setTextBoxSizeAndPosition() + end + function this:SetDefaultText(newText) + defaultText = newText + DefaultTextLabel.Text = defaultText + end + function this:SetFont(newFont) + DefaultTextLabel.Font = newFont + end + function this:SetFontSize(newFontSize) + DefaultTextLabel.FontSize = newFontSize + end + function this:SetClipsDescendants(clipsDescendants) + DefaultTextLabel.ClipsDescendants = clipsDescendants + end + + function this:GetContainer() + return TextBoxFrame + end + function this:GetTextBox() + return DefaultTextLabel + end + function this:GetDefaultText() + return defaultText + end + + return this +end + +return createTextBox diff --git a/Client2018/content/internal/AppShell/Modules/Shell/ThumbnailLoader.lua b/Client2018/content/internal/AppShell/Modules/Shell/ThumbnailLoader.lua new file mode 100644 index 0000000..8bb5d61 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/ThumbnailLoader.lua @@ -0,0 +1,276 @@ +--[[ + // ThumbnailLoader.lua + + // Creates a thumbnail loader object that handles the loading + // of thumb nails. + + // Thumbnails may not yet be generated, so this will retry generation and + // assign the final thumbnail +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local ContentProvider = game:GetService('ContentProvider') +local PlayersService = game:GetService("Players") + +local Http = require(ShellModules:FindFirstChild('Http')) +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local ThumbnailLoader = {} + +local AssetGameBaseUrl = Http.AssetGameBaseUrl + +local RETRIES = 6 +local FORMAT = "png" +local FADE_IN_TIME = 0.25 +local TEMPLATE_DECAL = Instance.new("Decal") +local function preloadThumbnailAsync(assetId) + TEMPLATE_DECAL.Texture = assetId + ContentProvider:PreloadAsync({ TEMPLATE_DECAL }) +end + +--[[ Sizes ]]-- +ThumbnailLoader.Sizes = { + Small = Vector2.new(100, 100); + Medium = Vector2.new(250, 250); + Large = Vector2.new(576, 324); +} + +ThumbnailLoader.AvatarSizes = { + Size48x48 = Vector2.new(48, 48); + Size150x150 = Vector2.new(150, 150); + Size352x352 = Vector2.new(352, 352); + Size420x420 = Vector2.new(420, 420); +} + +ThumbnailLoader.SubstitutionTypeResult = { + None = 0; + Unapproved = 1; + PendingReview = 2; + Broken = 3; + Unavailable = 4; + Unknown = 5; +} + +ThumbnailLoader.AssetType = { + Icon = { IsFinal = Http.GetAssetThumbnailFinalAsync; + SetImageUrl = 'Thumbs/Asset.ashx?width=%d&height=%d&assetId=%d&ignorePlaceMediaItems=true'; }; + SquareIcon = { IsFinal = Http.GetAssetThumbnailFinalAsync; + SetImageUrl = 'Thumbs/GameIcon.ashx?width=%d&height=%d&assetId=%d&ignorePlaceMediaItems=true'; }; + Avatar = { IsFinal = Http.GetAssetAvatarFinalAsync; + SetImageUrl = 'Thumbs/Avatar.ashx?width=%d&height=%d&userId=%d&ignorePlaceMediaItems=true'; }; + Outfit = { IsFinal = Http.GetOutfitThumbnailFinalAsync; + SetImageUrl = 'Thumbs/Avatar.ashx?width=%d&height=%d&userId=%d&ignorePlaceMediaItems=true'; }; +} + +--[[ + imageObject - a roblox gui image object (ImageLabel, ImageButton) + assetId - the id of the asset you want an image for + size - a ThumbnailLoader.Sizes + assetType - a ThumbnailLoader.AssetType +]] +function ThumbnailLoader:Create(imageObject, assetId, size, assetType, cachebust) + local this = {} + + local isLoading = false + local cancelled = false + local isFinalSuccess = false + local uri = AssetGameBaseUrl..string.format(assetType.SetImageUrl, size.x, size.y, assetId or -1) + if cachebust then + uri = uri .. '&cb=' .. tostring(tick()) + end + local getIsFinalFunc = assetType.IsFinal + + local function tryGetFinalAsync() + local result = getIsFinalFunc(assetId, size.x, size.y, FORMAT) + if result then + local isFinal = result["Final"] or result["thumbnailFinal"] + local substitutionType = result["substitutionType"] or result["SubstitutionType"] + if isFinal == true and + (substitutionType == nil or substitutionType == ThumbnailLoader.SubstitutionTypeResult.None) then + isFinalSuccess = true + preloadThumbnailAsync(uri) + return true + end + end + return false + end + + local function loadThumbInternalAsync() + local tryCount = 0 + isLoading = true + isFinalSuccess = false + while tryCount <= RETRIES and isLoading and not cancelled do + if tryGetFinalAsync() then + break + end + tryCount = tryCount + 1 + wait(tryCount ^ 2) + end + isLoading = false + end + + local loader = nil + + function this:LoadAsync(showSpinner, fadeImage, spinnerProperties) + spinnerProperties = spinnerProperties or {} + + if not assetId then return end + if showSpinner == nil then + showSpinner = true + end + if fadeImage == nil then + fadeImage = true + end + -- reset image + imageObject.Image = "" + if fadeImage then + local tween = Utility.PropertyTweener(imageObject, "ImageTransparency", 1, 1, 0, + Utility.EaseInOutQuad, true, nil) + end + + --try first time before starting loading widget + if not tryGetFinalAsync() then + if showSpinner then + spinnerProperties['Parent'] = spinnerProperties['Parent'] or imageObject + loader = LoadingWidget( + spinnerProperties, + { loadThumbInternalAsync } ) + loader:AwaitFinished() + loader:Cleanup() + else + loadThumbInternalAsync() + end + end + + if not cancelled then + imageObject.Image = isFinalSuccess and uri or "" + if fadeImage then + local tween = Utility.PropertyTweener(imageObject, "ImageTransparency", 1, 0, FADE_IN_TIME, + Utility.EaseInOutQuad, true, nil) + end + end + + return isFinalSuccess + end + + function this:Cancel() + isLoading = false + cancelled = true + end + + function this:SetTransparency(value) + if loader then + loader:SetTransparency(value) + end + end + + return this +end + +--[[ + imageObject - a roblox gui image object (ImageLabel, ImageButton) + userId - userId of the player you want the avatar thumbnail for + thumbnailType - Enum.ThumbnailType (HeadShot = 0, AvatarBust = 1, AvatarThumbnail = 2) + thumbnailSize - Enum.ThumbnailSize (Size48x48 = 0, Size180x180 = 1, Size420x420 = 2) +]] +function ThumbnailLoader:LoadAvatarThumbnailAsync(imageObject, userId, thumbnailType, thumbnailSize, cachebust) + local this = {} + + local isLoading = false + local cancelled = false + local isFinalSuccess = false + local uri = nil + local isFinal = false + + local function tryGetFinalAsync() + local success, msg = pcall(function() + uri, isFinal = PlayersService:GetUserThumbnailAsync(userId, thumbnailType, thumbnailSize) + end) + if success and uri and isFinal then + isFinalSuccess = true + if cachebust then + uri = uri .. '&cb=' .. tostring(tick()) + end + preloadThumbnailAsync(uri) + return true + end + return false + end + + local function loadThumbInternalAsync() + local tryCount = 0 + isLoading = true + isFinalSuccess = false + while tryCount <= RETRIES and isLoading and not cancelled do + if tryGetFinalAsync() then + break + end + tryCount = tryCount + 1 + wait(tryCount ^ 2) + end + isLoading = false + end + + local loader = nil + + function this:LoadAsync(showSpinner, fadeImage, spinnerProperties) + spinnerProperties = spinnerProperties or {} + + if not userId then return end + + if showSpinner == nil then + showSpinner = true + end + if fadeImage == nil then + fadeImage = true + end + -- reset image + imageObject.Image = "" + if fadeImage then + local tween = Utility.PropertyTweener(imageObject, "ImageTransparency", 1, 1, 0, + Utility.EaseInOutQuad, true, nil) + end + + --try first time before starting loading widget + if not tryGetFinalAsync() then + if showSpinner then + spinnerProperties['Parent'] = spinnerProperties['Parent'] or imageObject + loader = LoadingWidget( + spinnerProperties, + { loadThumbInternalAsync } ) + loader:AwaitFinished() + loader:Cleanup() + else + loadThumbInternalAsync() + end + end + + if not cancelled then + imageObject.Image = isFinalSuccess and uri or "" + if fadeImage then + local tween = Utility.PropertyTweener(imageObject, "ImageTransparency", 1, 0, FADE_IN_TIME, + Utility.EaseInOutQuad, true, nil) + end + end + + return isFinalSuccess + end + + function this:Cancel() + isLoading = false + cancelled = true + end + + function this:SetTransparency(value) + if loader then + loader:SetTransparency(value) + end + end + + return this +end + +return ThumbnailLoader diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Thunks/ApiFetchUserThumbnail.lua b/Client2018/content/internal/AppShell/Modules/Shell/Thunks/ApiFetchUserThumbnail.lua new file mode 100644 index 0000000..9b36a5b --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Thunks/ApiFetchUserThumbnail.lua @@ -0,0 +1,69 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local ShellModules = Modules:FindFirstChild("Shell") +local MakeSafeAsyncRodux = require(ShellModules:FindFirstChild("SafeAsyncRodux")) +local PlayersService = game:GetService("Players") +local FetchUserThumbnail = require(ShellModules.Actions.FetchUserThumbnail) +local SetUserThumbnail = require(ShellModules.Actions.SetUserThumbnail) +local ContentProvider = game:GetService("ContentProvider") + +local TEMPLATE_DECAL = Instance.new("Decal") +local function preloadThumbnailAsync(assetId) + TEMPLATE_DECAL.Texture = assetId + ContentProvider:PreloadAsync({ TEMPLATE_DECAL }) +end + +local GetUserThumbnailAsync = function(store, rbxuid, thumbnailType, thumbnailSize, retryTime) + MakeSafeAsyncRodux({ + asyncFunc = function(store, rbxuid, thumbnailType, thumbnailSize) + local imageUrl = nil + local isFinal = nil + local success = pcall(function() + imageUrl, isFinal = PlayersService:GetUserThumbnailAsync(rbxuid, thumbnailType, thumbnailSize) + end) + if success and isFinal and imageUrl then + preloadThumbnailAsync(imageUrl) + else + imageUrl = nil + end + return { + success = success, + rbxuid = rbxuid, + thumbnailType = thumbnailType, + thumbnailSize = thumbnailSize, + imageUrl = imageUrl, + isFinal = isFinal, + timestamp = tick() + } + end, + callback = function(store, result) + store:dispatch(SetUserThumbnail(result)) + end, + retries = retryTime, + retryFunc = function(store, result) + return not (result.success and result.isFinal) + end, + userRelated = true + })(store, rbxuid, thumbnailType, thumbnailSize) +end + +return function(rbxuid, thumbnailType, thumbnailSize, retryTime, forceUpdate) + return function(store) + local state = store:getState() + local userThumbnailsState = state.UserThumbnails + local thumbnailId = table.concat{ rbxuid, thumbnailType.Name, thumbnailSize.Name } + local thumbnailData = userThumbnailsState[thumbnailId] + --TODO: may use lastUpdated timestamp to determine whether to refetch + if thumbnailData then + if thumbnailData.isFetching then + return + end + if thumbnailData.imageUrl and not forceUpdate then + return + end + end + store:dispatch(FetchUserThumbnail({ rbxuid = rbxuid, thumbnailType = thumbnailType, thumbnailSize = thumbnailSize })) + spawn(function() + GetUserThumbnailAsync(store, rbxuid, thumbnailType, thumbnailSize, retryTime) + end) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Thunks/ApiRequestCrossPlayEnabled.lua b/Client2018/content/internal/AppShell/Modules/Shell/Thunks/ApiRequestCrossPlayEnabled.lua new file mode 100644 index 0000000..7624dad --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Thunks/ApiRequestCrossPlayEnabled.lua @@ -0,0 +1,70 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local ShellModules = Modules:FindFirstChild("Shell") +local Http = require(ShellModules:FindFirstChild("Http")) +local MakeSafeAsyncRodux = require(ShellModules:FindFirstChild('SafeAsyncRodux')) +local RequestCrossPlayEnabled = require(ShellModules.Actions.RequestCrossPlayEnabled) +local SetCrossPlayEnabled = require(ShellModules.Actions.SetCrossPlayEnabled) +local GetCrossPlayEnabledFailed = require(ShellModules.Actions.GetCrossPlayEnabledFailed) +local PostCrossPlayEnabledFailed = require(ShellModules.Actions.PostCrossPlayEnabledFailed) +local AddError = require(ShellModules.Actions.AddError) +local Errors = require(ShellModules:FindFirstChild('Errors')) + +local GetCrossplayEnabledStatusAsync = MakeSafeAsyncRodux({ + asyncFunc = function(store) + local jsonobject = Http.GetCrossplayEnabledStatusAsync() + if jsonobject ~= nil then + return jsonobject.isEnabled + end + end, + callback = function(store, enabled) + if enabled ~= nil then + store:dispatch(SetCrossPlayEnabled(enabled, tick())) + else + store:dispatch(GetCrossPlayEnabledFailed()) + store:dispatch(AddError(Errors.CPPSettingError.SetCPPSettingError, tick())) + end + end, + userRelated = true +}) + +local PostCrossplayEnabledStatusAsync = MakeSafeAsyncRodux({ + asyncFunc = function(store, val) + if Http.PostCrossplayStatusAsync(val) then + return val + end + end, + callback = function(store, enabled) + if enabled ~= nil then + store:dispatch(SetCrossPlayEnabled(enabled, tick())) + else + store:dispatch(PostCrossPlayEnabledFailed()) + end + end, + userRelated = true +}) + + +return function(method) + return function(store) + local state = store:getState() + local crossPlayEnabledState = state.CrossPlayEnabledState + local isRequesting = crossPlayEnabledState.isRequesting + if method == "GET" then + if not isRequesting then + store:dispatch(RequestCrossPlayEnabled()) + spawn(function() + GetCrossplayEnabledStatusAsync(store) + end) + end + elseif method == "POST" then + local crossPlayEnabled = crossPlayEnabledState.enabled + if not isRequesting and crossPlayEnabled ~= nil then + store:dispatch(RequestCrossPlayEnabled()) + local targetVal = not crossPlayEnabled + spawn(function() + PostCrossplayEnabledStatusAsync(store, targetVal) + end) + end + end + end +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Thunks/ApiRequestPrivilegeSettings.lua b/Client2018/content/internal/AppShell/Modules/Shell/Thunks/ApiRequestPrivilegeSettings.lua new file mode 100644 index 0000000..798a724 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Thunks/ApiRequestPrivilegeSettings.lua @@ -0,0 +1,56 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local ShellModules = Modules:FindFirstChild("Shell") +local MakeSafeAsyncRodux = require(ShellModules:FindFirstChild('SafeAsyncRodux')) +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) +local FetchPrivilegeSettings = require(ShellModules.Actions.FetchPrivilegeSettings) +local SetPrivilegeSettings = require(ShellModules.Actions.SetPrivilegeSettings) + +local Privileges = +{ + USER_CREATED_CONTENT = 247, + MULTIPLAYER_SESSIONS = 254 +} + +local GetPrivilegeSettingsAsync = MakeSafeAsyncRodux({ + asyncFunc = function(store) + local newPrivilegeSettings = {} + + local success = pcall(function() + local multiplayerSettings = PlatformService:BeginCheckXboxPrivilege(Privileges.MULTIPLAYER_SESSIONS) + local sharedContentSettings = PlatformService:BeginCheckXboxPrivilege(Privileges.USER_CREATED_CONTENT) + newPrivilegeSettings.Multiplayer = + { + hasPrivilege = multiplayerSettings.CanJoinGame, + status = multiplayerSettings.PrivilegeCheckResult, + } + newPrivilegeSettings.SharedContent = + { + hasPrivilege = sharedContentSettings.CanJoinGame, + status = sharedContentSettings.PrivilegeCheckResult, + } + end) + if not success then + newPrivilegeSettings.Multiplayer = { hasPrivilege = false, status = "Error"} + newPrivilegeSettings.SharedContent = { hasPrivilege = false, status = "Error"} + end + newPrivilegeSettings.timestamp = tick() + return newPrivilegeSettings + end, + callback = function(store, newPrivilegeSettings) + store:dispatch(SetPrivilegeSettings(newPrivilegeSettings)) + end, + userRelated = true +}) + + +return function() + return function(store) + --Note: we don't check isRequesting state for privilege settings update, + --as we always want to fetch the latest privilege settings + store:dispatch(FetchPrivilegeSettings()) + spawn(function() + GetPrivilegeSettingsAsync(store) + end) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/UnlinkAccountOverlay.lua b/Client2018/content/internal/AppShell/Modules/Shell/UnlinkAccountOverlay.lua new file mode 100644 index 0000000..069614f --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/UnlinkAccountOverlay.lua @@ -0,0 +1,157 @@ +--[[ + // UnlinkAccountOverlay.lua + + // Confirmation overlay for when you unlink your account +]] +local XboxUseUnlinkCallback = settings():GetFFlag("XboxUseUnlinkCallback") + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local BaseOverlay = require(ShellModules:FindFirstChild('BaseOverlay')) +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local Analytics = require(ShellModules:FindFirstChild('Analytics')) + +local function createUnlinkAccountOverlay(titleAndMsg, unlinkCallback) + local this = BaseOverlay() + + local title = titleAndMsg.Title + local message = titleAndMsg.Msg + + local errorIcon = Utility.Create'ImageLabel' + { + Name = "ReportIcon"; + Position = UDim2.new(0, 226, 0, 204); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Shell/Icons/ErrorIconLargeCopy@1080.png"; + Size = UDim2.new(0,321,0,264); + } + this:SetImage(errorIcon) + + local titleText = Utility.Create'TextLabel' + { + Name = "TitleText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, this.RightAlign, 0, 136); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.HeaderSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = title; + TextXAlignment = Enum.TextXAlignment.Left; + Parent = this.Container; + } + + Utility.Create'TextLabel' + { + Name = "DescriptionText"; + Size = UDim2.new(0, 762, 0, 304); + Position = UDim2.new(0, this.RightAlign, 0, titleText.Position.Y.Offset + 62); + BackgroundTransparency = 1; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + Font = GlobalSettings.LightFont; + FontSize = GlobalSettings.TitleSize; + TextColor3 = GlobalSettings.WhiteTextColor; + TextWrapped = true; + Text = message; + Parent = this.Container; + } + + local okButton = Utility.Create'TextButton' + { + Name = "OkButton"; + Size = UDim2.new(0, 320, 0, 66); + Position = UDim2.new(0, this.RightAlign, 1, -100 - 66); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.GreyButtonColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Text = Strings:LocalizedString("ConfirmWord"); + Parent = this.Container; + + SoundManager:CreateSound('MoveSelection'); + } + Utility.ResizeButtonWithText(okButton, okButton, GlobalSettings.TextHorizontalPadding) + + local cancelButton = Utility.Create'TextButton' + { + Name = "CancelButton"; + Position = UDim2.new(0, okButton.Position.X.Offset + okButton.Size.X.Offset + 10, 1, -100 - 66); + Size = UDim2.new(0, 320, 0, 66); + BorderSizePixel = 0; + BackgroundColor3 = GlobalSettings.BlueButtonColor; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.ButtonSize; + TextColor3 = GlobalSettings.TextSelectedColor; + Text = Strings:LocalizedString("CancelWord"); + Parent = this.Container; + + SoundManager:CreateSound('MoveSelection'); + } + Utility.ResizeButtonWithText(cancelButton, cancelButton, GlobalSettings.TextHorizontalPadding) + + okButton.SelectionGained:connect(function() + okButton.BackgroundColor3 = GlobalSettings.GreySelectedButtonColor + okButton.TextColor3 = GlobalSettings.TextSelectedColor + end) + okButton.SelectionLost:connect(function() + okButton.BackgroundColor3 = GlobalSettings.GreyButtonColor + okButton.TextColor3 = GlobalSettings.WhiteTextColor + end) + + cancelButton.SelectionGained:connect(function() + cancelButton.BackgroundColor3 = GlobalSettings.GreySelectedButtonColor + cancelButton.TextColor3 = GlobalSettings.TextSelectedColor + end) + cancelButton.SelectionLost:connect(function() + cancelButton.BackgroundColor3 = GlobalSettings.GreyButtonColor + cancelButton.TextColor3 = GlobalSettings.WhiteTextColor + end) + + cancelButton.MouseButton1Click:connect(function() + this:Close() + end) + + --[[ Input Events ]]-- + function this:GetAnalyticsInfo() + return + { + [Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('UnlinkAccountOverlayId'); + Title = titleAndMsg.Title; + } + end + + okButton.MouseButton1Click:connect(function() + if this:Close() then + if XboxUseUnlinkCallback then + if unlinkCallback then + unlinkCallback() + end + else + EventHub:dispatchEvent(EventHub.Notifications["UnlinkAccountConfirmation"]) + end + end + end) + + local baseFocus = this.Focus + function this:Focus() + baseFocus(self) + Utility.SetSelectedCoreObject(cancelButton) + end + + function this:GetOverlaySound() + return 'Error' + end + + return this +end + +return createUnlinkAccountOverlay diff --git a/Client2018/content/internal/AppShell/Modules/Shell/UserData.lua b/Client2018/content/internal/AppShell/Modules/Shell/UserData.lua new file mode 100644 index 0000000..fbb826e --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/UserData.lua @@ -0,0 +1,145 @@ +--[[ + // UserData.lua + // API for all user related data + + // TODO: + Eventually all of this will move into Rodux +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") +local Players = game:GetService('Players') +local UserInputService = game:GetService("UserInputService") + +local AccountManager = require(ShellModules:FindFirstChild('AccountManager')) +local Http = require(ShellModules:FindFirstChild('Http')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local UserData = {} + +local currentUserData = nil + +local CONSTANT_RETRY_TIME = 30 + +local function setVoteCountAsync() + local voteResult = Http.GetVoteCountAsync() + currentUserData["VoteCount"] = voteResult and voteResult['VoteCount'] or 0 +end + +local function verifyHasLinkedAccountAsync() + local result = AccountManager:HasLinkedAccountAsync() + + while result ~= AccountManager.AuthResults.Success and result ~= AccountManager.AuthResults.AccountUnlinked do + result = AccountManager:HasLinkedAccountAsync() + wait(CONSTANT_RETRY_TIME) + end + + currentUserData["LinkedAccountResult"] = result +end + +local function verifyHasRobloxCredentialsAsync() + local result = AccountManager:HasRobloxCredentialsAsync() + + while result ~= AccountManager.AuthResults.Success and result ~= AccountManager.AuthResults.UsernamePasswordNotSet do + result = AccountManager:HasRobloxCredentialsAsync() + wait(CONSTANT_RETRY_TIME) + end + + currentUserData["RobloxCredentialsResult"] = result +end + +function UserData:Initialize() + if currentUserData then + Utility.DebugLog("Trying to initialize UserData when we already have valid data.") + end + + currentUserData = {} + + if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then + spawn(setVoteCountAsync) + -- TODO: When all accounts that are linked but have no credentials are cleaned up, we can remove these checks + spawn(verifyHasLinkedAccountAsync) + spawn(verifyHasRobloxCredentialsAsync) + end +end + +function UserData:GetVoteCount() + if not currentUserData then + Utility.DebugLog("Error: UserData:GetVoteCount() - UserData has not been initialized. Don't do that!") + return nil + end + return currentUserData["VoteCount"] +end + +function UserData:IncrementVote() + currentUserData["VoteCount"] = (currentUserData["VoteCount"] or 0) + 1 +end + +function UserData:DecrementVote() + currentUserData["VoteCount"] = math.max((currentUserData["VoteCount"] or 0) - 1, 0) +end + +-- returns true, false or nil in the case of error +function UserData:HasLinkedAccount() + local result = currentUserData["LinkedAccountResult"] + if result == AccountManager.AuthResults.Success then + return true + elseif result == AccountManager.AuthResults.AccountUnlinked then + return false + else + return nil + end +end + +-- returns true, false or nil in the case of error +function UserData:HasRobloxCredentials() + local result = currentUserData["RobloxCredentialsResult"] + if result == AccountManager.AuthResults.Success then + return true + elseif result == AccountManager.AuthResults.UsernamePasswordNotSet then + return false + else + return nil + end +end + +function UserData:Reset() + currentUserData = nil +end + +--[[ This should no longer be used ]]-- +function UserData.GetLocalUserIdAsync() + return UserData.GetLocalPlayerAsync().userId +end + +function UserData.GetLocalPlayerAsync() + local localPlayer = Players.LocalPlayer + while not localPlayer do + wait() + localPlayer = Players.LocalPlayer + end + return localPlayer +end + +function UserData.GetPlatformUserBalanceAsync() + local result = Http.GetPlatformUserBalanceAsync() + if not result then + -- TODO: Error Code + return nil + end + -- + + return result["Robux"] +end + +function UserData.GetTotalUserBalanceAsync() + local result = Http.GetTotalUserBalanceAsync() + if not result then + return nil + end + + return result["robux"] +end + +return UserData diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Utility.lua b/Client2018/content/internal/AppShell/Modules/Shell/Utility.lua new file mode 100644 index 0000000..563ba58 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Utility.lua @@ -0,0 +1,623 @@ +-- Written by Kip Turner, Copyright Roblox 2015 +local RunService = game:GetService('RunService') +local GuiService = game:GetService('GuiService') +local TextService = game:GetService('TextService') +local Players = game:GetService('Players') + +local PlatformService = nil +pcall(function() PlatformService = game:GetService('PlatformService') end) + +local Util = {} +do + + function Util.IsFinite(num) + return num == num and num ~= 1/0 and num ~= -1/0 + end + + function Util.FindAncestorOfType(guiObject, class) + local parent = guiObject and guiObject.Parent + if parent then + if parent:IsA(class) then + return parent + else + return Util.FindAncestorOfType(parent, class) + end + end + return nil + end + + function Util.CalculateRelativeDimensions(guiObject, guiDims, mockup_dims, rootGuiSize) + local guiResolution = GuiService:GetScreenResolution() + local parentSurfaceGui = Util.FindAncestorOfType(guiObject, 'SurfaceGui') + if parentSurfaceGui then + guiResolution = parentSurfaceGui.CanvasSize + end + local absolutePercentSize = (guiDims / mockup_dims) + if mockup_dims.y > 0 and guiResolution.y > 0 then + local mockupAspectRatio = mockup_dims.x / mockup_dims.y + local globalAspectRatio = guiResolution.x / guiResolution.y + absolutePercentSize = absolutePercentSize * (mockupAspectRatio / globalAspectRatio) + local parentObject = guiObject.Parent + if parentObject then + local parentPercentScreen = parentObject.AbsoluteSize / guiResolution + local parentSizeInverse = 1 / parentPercentScreen + if Util.IsFinite(parentSizeInverse.x) and Util.IsFinite(parentSizeInverse.y) then + return UDim2.new(parentSizeInverse.x * absolutePercentSize.x, 0, parentSizeInverse.y * absolutePercentSize.y, 0) + end + end + end + return UDim2.new(absolutePercentSize.x, 0, absolutePercentSize.y, 0) + end + + + -- Anchor Graph + -- 1 2 3 + -- 4 5 6 + -- 7 8 9 + + Util.Enum = + { + Anchor = + { + TopLeft = 1; + TopMiddle = 2; + TopRight = 3; + CenterLeft = 4; + Center = 5; + CenterRight = 6; + BottomLeft = 7; + BottomMiddle = 8; + BottomRight = 9; + }; + }; + + function Util.CalculateFit(containerObject, rawImageSize) + local absSize = containerObject.AbsoluteSize + local scalar = absSize / rawImageSize + local fixedSize = rawImageSize * math.min(scalar.X, scalar.Y) + + return UDim2.new(0, fixedSize.X , 0, fixedSize.Y) + end + + function Util.CalculateFill(containerObject, rawImageSize) + local absSize = containerObject.AbsoluteSize + local scalar = absSize / rawImageSize + local fixedSize = rawImageSize * math.max(scalar.X, scalar.Y) + + return UDim2.new(0, fixedSize.X , 0, fixedSize.Y) + end + + function Util.Create(instanceType) + return function(data) + local obj = Instance.new(instanceType) + for k, v in pairs(data) do + if type(k) == 'number' then + v.Parent = obj + else + obj[k] = v + end + end + return obj + end + end + + ---- TWEENING ---- + local ActiveTweens = {} + + local function getActiveTween(prop, instance) + return ActiveTweens[prop] and ActiveTweens[prop][instance] + end + + local function setActiveTween(prop, instance, newTween) + if not ActiveTweens[prop] then + ActiveTweens[prop] = {} + end + ActiveTweens[prop][instance] = newTween + end + + function Util.Linear(t, b, c, d) + if t >= d then return b + c end + + return c*t/d + b + end + + function Util.EaseOutQuad(t, b, c, d) + if t >= d then return b + c end + + t = t/d; + return -c * t*(t-2) + b + end + + function Util.EaseInOutQuad(t, b, c, d) + if t >= d then return b + c end + + t = t / (d/2); + if (t < 1) then return c/2*t*t + b end; + t = t - 1; + return -c/2 * (t*(t-2) - 1) + b; + end + + function Util.SCurveUDim2(t, b, c, d) + if t >= d then return b + c end + t = t / d; + if t < 0 then t = 0 end + if t > 1 then t = 1 end + local T = 3 * t * t - 2 * t * t * t + return UDim2.new( + T * c.X.Scale, + T * c.X.Offset, + T * c.Y.Scale, + T * c.Y.Offset) + b + end + + function Util.SCurveVector2(t, b, c, d) + if t >= d then return b + c end + t = t / d; + if t < 0 then t = 0 end + if t > 1 then t = 1 end + local T = 3 * t * t - 2 * t * t * t + return Vector2.new( + T * c.X, + T * c.Y) + b + end + + function Util.PropertyTweener(instance, prop, start, final, duration, easingFunc, override, callbackFunction) + easingFunc = easingFunc or Util.Linear + override = override or false + + local this = {} + this.StartTime = tick() + this.EndTime = this.StartTime + duration + this.Cancelled = false + + local finished = false + local percentComplete = 0 + + + local function setValue(newValue) + if instance then + instance[prop] = newValue + end + end + + local function finalize() + setValue(easingFunc(1, start, final - start, 1)) + finished = true + percentComplete = 1 + + if getActiveTween(prop, instance) == this then + setActiveTween(prop, instance, nil) + end + + if callbackFunction then + callbackFunction() + end + end + + if override or not getActiveTween(prop, instance) then + if getActiveTween(prop, instance) then + getActiveTween(prop, instance):Cancel() + end + setActiveTween(prop, instance, this) + + -- Initial set + setValue(easingFunc(0, start, final - start, duration)) + spawn(function() + local now = tick() + while now < this.EndTime and instance and not this.Cancelled do + setValue(easingFunc(now - this.StartTime, start, final - start, duration)) + percentComplete = Util.Clamp(0, 1, (now - this.StartTime) / duration) + RunService.RenderStepped:wait() + now = tick() + end + if this.Cancelled == false and instance then + finalize() + end + + if getActiveTween(prop, instance) == this then + setActiveTween(prop, instance, nil) + end + end) + else + finished = true + end + + function this:GetFinal() + return final + end + + function this:GetPercentComplete() + return percentComplete + end + + function this:IsFinished() + return finished + end + + function this:Finish() + if not finished then + self:Cancel() + finalize() + end + end + + function this:Cancel() + this.Cancelled = true + finished = true + if getActiveTween(prop, instance) == this then + setActiveTween(prop, instance, nil) + end + end + + return this + end + + function Util.CancelTweens(tweens) + for i, tween in pairs(tweens) do + tween:Finish() + tweens[i] = nil + end + end + --------------- + + --- EVENTS ---- + function Util.Signal() + local sig = {} + + local mSignaler = Instance.new('BindableEvent') + + local mArgData = nil + local mArgDataCount = nil + + function sig:fire(...) + mArgData = {...} + mArgDataCount = select('#', ...) + mSignaler:Fire() + end + + function sig:connect(f) + if not f then error("connect(nil)", 2) end + return mSignaler.Event:connect(function() + f(unpack(mArgData, 1, mArgDataCount)) + end) + end + + function sig:wait() + mSignaler.Event:wait() + assert(mArgData, "Missing arg data, likely due to :TweenSize/Position corrupting threadrefs.") + return unpack(mArgData, 1, mArgDataCount) + end + + return sig + end + + function Util.DisconnectEvent(conn) + if conn then + conn:disconnect() + end + return nil + end + + function Util.DisconnectEvents(conns) + if conns and type(conns) == 'table' then + for _, conn in pairs(conns) do + conn:disconnect() + end + end + end + -------------- + + -- MATH -- + function Util.Clamp(low, high, input) + return math.max(low, math.min(high, input)) + end + + function Util.ClampVector2(low, high, input) + return Vector2.new(Util.Clamp(low.x, high.x, input.x), Util.Clamp(low.y, high.y, input.y)) + end + + function Util.TweenPositionOrSet(guiObject, ...) + if guiObject:IsDescendantOf(game) then + guiObject:TweenPosition(...) + else + guiObject.Position = select(1, ...) + end + end + ---- + + function Util.ClampCanvasPosition(scrollingContainer, position) + local container = scrollingContainer + local parentSize = container.Parent and container.Parent.AbsoluteSize or Vector2.new(0,0) + local absoluteCanvasSize = Vector2.new(container.CanvasSize.X.Scale * parentSize.X + container.CanvasSize.X.Offset, + container.CanvasSize.Y.Scale * parentSize.Y + container.CanvasSize.Y.Offset) + local nextX = Util.Clamp(0, absoluteCanvasSize.X - container.AbsoluteWindowSize.X, position.X) + local nextY = Util.Clamp(0, absoluteCanvasSize.Y - container.AbsoluteWindowSize.Y, position.Y) + + return Vector2.new(nextX, nextY) + end + + function Util.Round(num, roundToNearest) + roundToNearest = roundToNearest or 1 + return math.floor((num + roundToNearest/2) / roundToNearest) * roundToNearest + end + -------------- + + -- FORMATING -- + -- Removed whitespace from the beginning and end of the string + function Util.ChompString(str) + return tostring(str):gsub("^%s+" , ""):gsub("%s+$" , "") + end + + -- replace multiple whitespace with one; remove leading and trailing whitespace + function Util.SpaceNormalizeString(str) + return tostring(str):gsub("%s+", " "):gsub("^%s+" , ""):gsub("%s+$" , "") + end + + function Util.FormatNumberString(value) + -- Make sure beginning and end of the string is clipped + local stringValue = Util.ChompString(value) + return stringValue:reverse():gsub("%d%d%d", "%1,"):reverse():gsub("^,", "") + end + + -- PrettyPrint function for formatting data structures into flat strings, useful for debugging + + function Util.PrettyPrint(tb) + if type(tb) == 'table' then + local str = "{" + for k, v in pairs(tb) do + str = ((str == "{") and str or str..", ") + if type(k) == 'string' then + str = str..k.." = " + elseif type(k) == 'number' then + -- nothing + else + str = str.."["..k.."] = " + end + str = str..Util.PrettyPrint(v) + end + return str.."}" + elseif type(tb) == 'string' then + return "'"..tb.."'" + else + return tostring(tb) + end + end + + local isSandboxed = nil + function Util.DebugLog(...) + if isSandboxed == nil then + isSandboxed = PlatformService and PlatformService:IsSandboxed() + end + if isSandboxed or PlatformService == nil then + print(...) + end + end + + -- K is a tunable parameter that changes the shape of the S-curve + -- the larger K is the more straight/linear the curve gets + local function SCurveTranform(t, k, lowerK) + k = k or 0.35 + lowerK = lowerK or 0.8 + t = Util.Clamp(-1,1,t) + if t >= 0 then + return (k*t) / (k - t + 1) + end + return -((lowerK*-t) / (lowerK + t + 1)) + end + + local function toSCurveSpace(t, deadzone) + deadzone = deadzone or 0.1 + return (1 + deadzone) * (2*math.abs(t) - 1) - deadzone + end + + local function fromSCurveSpace(t) + return t/2 + 0.5 + end + + function Util.GamepadLinearToCurve(thumbstickPosition, deadzone, k, lowerK) + local function onAxis(axisValue) + local sign = axisValue < 0 and -1 or 1 + + local point = fromSCurveSpace(SCurveTranform(toSCurveSpace(math.abs(axisValue), deadzone)), k, lowerK) + return Util.Clamp(-1,1, point * sign) + end + return Vector2.new(onAxis(thumbstickPosition.x), onAxis(thumbstickPosition.y)) + end + + + function Util.IsFastFlagEnabled(flagName) + local success, isFlagEnabled = pcall(function() + return settings():GetFFlag(flagName) + end) + + if success and not isFlagEnabled then + Util.DebugLog("Fast Flag:", flagName, "is currently not enabled.") + elseif not success then + Util.DebugLog("GetFFlag failed for flag:", flagName) + end + + return success and isFlagEnabled + end + + + function Util.GetFastVariable(variableName) + local success, value = pcall(function() + return settings():GetFVariable(variableName) + end) + + return success and value + end + + local function getLocalPlayer() + while Players.LocalPlayer == nil do + wait() + end + return Players.LocalPlayer + end + + function Util.IsFeatureNonZero(fastIntName) + return (tonumber(Util.GetFastVariable(fastIntName)) or 0) ~= 0 + end + + function Util.IsFeatureRolledOut(fastIntName) + return getLocalPlayer().UserId % 100 < (tonumber(Util.GetFastVariable(fastIntName)) or 0) + end + + + function Util.ExponentialRepeat(loopPredicate, loopBody, repeatCount) + repeatCount = repeatCount or 6 + local retryCount = 1 + local numRetries = repeatCount + + while retryCount <= numRetries and loopPredicate() do + local done = loopBody() + if done then return end + wait(retryCount ^ 2) + retryCount = retryCount + 1 + end + + end + + function Util.SplitString(str, sep) + local result = {} + if str and sep then + for word in string.gmatch(str, '([^' .. sep .. ']+)') do + table.insert(result, word) + end + end + return result + end + + + local function findAssetsHelper(object, result, baseUrl) + if not object then return end + + if object:IsA('CharacterMesh') then + if object.MeshId > 0 then + table.insert(result, baseUrl .. tostring(object.MeshId)) + end + if object.BaseTextureId > 0 then + table.insert(result, baseUrl .. tostring(object.BaseTextureId)) + end + if object.OverlayTextureId > 0 then + table.insert(result, baseUrl .. tostring(object.OverlayTextureId)) + end + elseif object:IsA('FileMesh') then + table.insert(result, object.MeshId) + table.insert(result, object.TextureId) + elseif object:IsA('Decal') then + table.insert(result, object.Texture) + elseif object:IsA('Pants') then + table.insert(result, object.PantsTemplate) + elseif object:IsA('Shirt') then + table.insert(result, object.ShirtTemplate) + end + + for _, child in pairs(object:GetChildren()) do + findAssetsHelper(child, result, baseUrl) + end + end + function Util.FindAssetsInModel(object, baseUrl) + baseUrl = baseUrl or 'https://assetgame.roblox.com/asset/?id=' + local result = {} + findAssetsHelper(object, result, baseUrl) + return result + end + + function Util.ConvertFontSizeEnumToInt(fontSizeEnum) + local name = fontSizeEnum.Name + -- TODO: this is sort of gross? + local result = string.match(name, '%d+') + return result or 10 + end + + function Util.Upper(text) + if PlatformService then + return PlatformService:Upper(text) + else + return string.upper(text) + end + end + + function Util.SetSelectedCoreObject(obj) + GuiService.SelectedCoreObject = obj + end + + function Util.AddSelectionParent(selectionName, selectionParent) + GuiService:AddSelectionParent(selectionName, selectionParent) + end + + function Util.RemoveSelectionGroup(selectionName) + GuiService:RemoveSelectionGroup(selectionName) + end + + function Util.GetTextBounds(textGui) + local getFinalBounds = false + local textBound = nil + if textGui then + if textGui.TextFits then + textBound = textGui.TextBounds + getFinalBounds = true + else + --If TextWrapped is true, we can't really figure out what are the bounds should be, we need at least get some constraints on X/Y + if not textGui.TextWrapped then + textBound = TextService:GetTextSize(textGui.Text, textGui.TextSize, textGui.Font, Vector2.new(0, 0)) + getFinalBounds = true + end + end + end + + return getFinalBounds, textBound + end + + --A utility func to resize button based on the textGui + --(textGui doesn't necessarily a child of the button, this func just makes sure the textGui can fit in the buttonGui), + --offsetX and offsetY are used to specify the distance from text border to button border + --We assume the alignment style is 'Center', so always add offsetX * 2 and offsetY * 2, need to adjust the input offset if the alignment is not 'Center' + --if it's nil, it means we don't overwrite the button's size on that axis + --Return the result which indicateds whether we resized the button + --Note: We resize the button by changing it's Size.Offset, so if it's original size is based on Size.Scale, the button won't be resized + function Util.ResizeButtonWithText(buttonGui, textGui, offsetX, offsetY) + local resized = false + local success, textBound = Util.GetTextBounds(textGui) + if success then + if buttonGui.Size.X.Scale == 0 and buttonGui.Size.Y.Scale == 0 then + --Can't depend on the AbsoluteSize, which will be 0,0 if the buttonGui is not a descendant of worksapce + local buttonAbsSize = Vector2.new(buttonGui.Size.X.Offset, buttonGui.Size.Y.Offset) + local newAbsSizeX, newAbsSizeY = buttonAbsSize.X, buttonAbsSize.Y + if offsetX and buttonAbsSize.X < textBound.X + offsetX * 2 then + newAbsSizeX = textBound.X + offsetX * 2 + resized = true + end + if offsetY and buttonAbsSize.Y < textBound.Y + offsetY * 2 then + newAbsSizeY = textBound.Y + offsetY * 2 + resized = true + end + + if resized then + buttonGui.Size = UDim2.new(0, newAbsSizeX, 0, newAbsSizeY) + end + end + end + return resized + end + + --This is used to get the suitable size for the buttonGui to fit different texts + --alternativeTexts is an array of all possible texts in the buttonGui + function Util.ResizeButtonWithDynamicText(buttonGui, textGui, alternativeTexts, offsetX, offsetY) + local resized = false + resized = Util.ResizeButtonWithText(buttonGui, textGui, offsetX, offsetY) or resized + local originalText = textGui.Text + if alternativeTexts and #alternativeTexts > 0 then + for i = 1, #alternativeTexts do + textGui.Text = alternativeTexts[i] + resized = Util.ResizeButtonWithText(buttonGui, textGui, offsetX, offsetY) or resized + end + end + + textGui.Text = originalText + return resized + end +end + +return Util diff --git a/Client2018/content/internal/AppShell/Modules/Shell/VoteFrame.lua b/Client2018/content/internal/AppShell/Modules/Shell/VoteFrame.lua new file mode 100644 index 0000000..df8f598 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/VoteFrame.lua @@ -0,0 +1,140 @@ +--[[ + // VoteFrame.lua + // Creates a vote frame for a game +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) + +local CreateVoteFrame = function(parent, position) + local this = {} + + -- Assume 1080p + local MAX_SIZE = 203 + + local currentRedColor = GlobalSettings.RedTextColor + local currentGreenColor = GlobalSettings.GreenTextColor + local currentGreyColor = GlobalSettings.GreyTextColor + + local voteContainer = Utility.Create'Frame' + { + Name = "VoteContainer"; + Size = UDim2.new(0, MAX_SIZE, 0, 16); + Position = position; + BackgroundTransparency = 1; + Parent = parent; + } + + local greenContainer = Utility.Create'Frame' + { + Name = "VoteContainer"; + BackgroundTransparency = 1; + Size = UDim2.new(0, 0, 1, 0); + Position = UDim2.new(0, 0, 0, 0); + ClipsDescendants = true; + Parent = voteContainer; + } + + local redContainer = Utility.Create'Frame' + { + Name = "VoteContainer"; + BackgroundTransparency = 1; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 0, 0); + ClipsDescendants = true; + Parent = voteContainer; + } + + local greyContainer = Utility.Create'Frame' + { + Name = "VoteContainer"; + BackgroundTransparency = 1; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 0, 0); + ClipsDescendants = true; + Parent = voteContainer; + } + + local batteryImageRed = Utility.Create'ImageLabel' + { + Name = "BatteryImageRed"; + BackgroundTransparency = 1; + ImageColor3 = currentRedColor; + Parent = redContainer; + Image = "rbxasset://textures/ui/Shell/Icons/RatingBar@1080.png"; + Size = UDim2.new(0, redContainer.AbsoluteSize.X, 0, redContainer.AbsoluteSize.Y); + Position = UDim2.new(0, 0, 0, 0); + Visible = false; + } + + local batteryImageGreen = batteryImageRed:Clone() + batteryImageGreen.ImageColor3 = currentGreenColor + batteryImageGreen.ZIndex = 2 + batteryImageGreen.Name = "BatteryImageGreen" + batteryImageGreen.Parent = greenContainer + + local batteryImageGrey = batteryImageRed:Clone() + batteryImageGrey.ImageColor3 = currentGreyColor + batteryImageGrey.ZIndex = 3 + batteryImageGrey.Name = "BatteryImageGrey" + batteryImageGrey.Visible = true + batteryImageGrey.Parent = greyContainer + + --[[ Public API ]]-- + function this:SetPercentFilled(percent) + if percent and tonumber(percent) then + batteryImageGrey.Visible = false + batteryImageGreen.Visible = true + batteryImageRed.Visible = true + percent = tonumber(percent) + percent = Utility.Round(percent, 0.1) + greenContainer.Size = UDim2.new(percent, 0, 1, 0) + redContainer.Position = UDim2.new(percent, 0, 0, 0) + batteryImageRed.Position = UDim2.new(-percent, 0, 0, 0) + else + batteryImageGreen.Visible = false + batteryImageRed.Visible = false + batteryImageGrey.Visible = true + end + end + + function this:SetImageColorTint(value) + currentRedColor = Color3.new(0,0,0):lerp(GlobalSettings.RedTextColor, value) + currentGreenColor = Color3.new(0,0,0):lerp(GlobalSettings.GreenTextColor, value) + currentGreyColor = Color3.new(0,0,0):lerp(GlobalSettings.GreyTextColor, value) + + batteryImageRed.ImageColor3 = currentRedColor + batteryImageGreen.ImageColor3 = currentGreenColor + batteryImageGrey.ImageColor3 = currentGreyColor + end + + function this:TweenTransparency(value, duration) + Utility.PropertyTweener(batteryImageRed, 'ImageTransparency', batteryImageRed.ImageTransparency, + value, duration, Utility.Linear, true) + Utility.PropertyTweener(batteryImageGreen, 'ImageTransparency', batteryImageGreen.ImageTransparency, + value, duration, Utility.Linear, true) + Utility.PropertyTweener(batteryImageGrey, 'ImageTransparency', batteryImageGrey.ImageTransparency, + value, duration, Utility.Linear, true) + end + + function this:SetVisible(value) + voteContainer.Visible = value + end + + function this:GetContainer() + return voteContainer + end + + function this:Destroy() + voteContainer:Destroy() + this = nil + end + + return this +end + +return CreateVoteFrame \ No newline at end of file diff --git a/Client2018/content/internal/AppShell/Modules/Shell/VoteView.lua b/Client2018/content/internal/AppShell/Modules/Shell/VoteView.lua new file mode 100644 index 0000000..f14ddd2 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/VoteView.lua @@ -0,0 +1,298 @@ +--[[ + // VoteView.lua + + // Manages the vote view for the game details page +]] +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local EventHub = require(ShellModules:FindFirstChild('EventHub')) +local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) +local Strings = require(ShellModules:FindFirstChild('LocalizedStrings')) +local Utility = require(ShellModules:FindFirstChild('Utility')) +local VoteFrame = require(ShellModules:FindFirstChild('VoteFrame')) +local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager')) +local ErrorOverlayModule = require(ShellModules:FindFirstChild('ErrorOverlay')) +local Errors = require(ShellModules:FindFirstChild('Errors')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local UserData = require(ShellModules:FindFirstChild('UserData')) + +local function createVoteView() + local this = {} + + local canVote = false + local myVote = nil + local upVotes = 0 + local downVotes = 0 + local thisGameData = nil + local defaultSelection = nil + + this.Container = Utility.Create'Frame' + { + Name = "VoteContainer"; + Size = UDim2.new(1, 0, 0, 114); + BackgroundTransparency = 1; + Visible = false; + } + + -- Can't Vote Objects + -- override selection image + local SelectionImage = Utility.Create'ImageLabel' + { + Name = "SelectionImage"; + Size = UDim2.new(1, 32, 1, 32); + Position = UDim2.new(0, -16, 0, -16); + Image = 'rbxasset://textures/ui/SelectionBox.png'; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(21,21,41,41); + BackgroundTransparency = 1; + } + local CannotVoteSelection = Utility.Create'TextButton' + { + Name = "CannotVoteSelection"; + Text = ''; + Size = UDim2.new(1, 0, 0, 94); + Position = UDim2.new(0, 0, 0, 5); + BackgroundTransparency = 1; + Selectable = true; + SelectionImageObject = SelectionImage; + SoundManager:CreateSound('MoveSelection'); + } + local CannotVoteText = Utility.Create'TextLabel' + { + Name = "CannotVoteText"; + Size = UDim2.new(0.8, 0, 1, 0); + Position = UDim2.new(0.1, 0, 0, 0); + BackgroundTransparency = 1; + Font = GlobalSettings.BoldFont; + FontSize = GlobalSettings.SubHeaderSize; + TextColor3 = GlobalSettings.WhiteTextColor; + Visible = false; + TextWrapped = true; + Text = Strings:LocalizedString("CannotVoteWord"); + Parent = CannotVoteSelection; + } + + -- Vote Objects + local VoteWidget = VoteFrame(this.Container, UDim2.new(0, 88, 0, 34)) + local VoteContainer = VoteWidget:GetContainer() + local ThumbsUpImage = Utility.Create'ImageLabel' + { + Name = "ThumbsUpImage"; + Size = UDim2.new(0, 48, 0, 48); + BackgroundTransparency = 1; + } + local ThumbsDownImage = ThumbsUpImage:Clone() + ThumbsDownImage.Name = "ThumbsDownImage" + -- Buttons act as buffers for selection gui + local ThumbsUpButton = Utility.Create'ImageButton' + { + Name = "ThumbsUpButton"; + Size = UDim2.new(0, ThumbsUpImage.Size.X.Offset + 18, 0, ThumbsUpImage.Size.Y.Offset + 18); + BackgroundTransparency = 1; + Image = ""; + Parent = VoteContainer; + SoundManager:CreateSound('MoveSelection'); + } + ThumbsUpButton.Position = UDim2.new(0, -ThumbsUpButton.Size.X.Offset - 12, 0, + -ThumbsUpButton.Size.Y.Offset / 2 + VoteContainer.Size.Y.Offset / 2) + local ThumbsDownButton = ThumbsUpButton:Clone() + ThumbsDownButton.Name = "ThumbsDownButton" + ThumbsDownButton.Position = UDim2.new(1, 12, 0, ThumbsDownButton.Position.Y.Offset) + ThumbsDownButton.Parent = VoteContainer + + this.ThumbsUpButton = ThumbsUpButton + this.ThumbsDownButton = ThumbsDownButton + + ThumbsUpImage.Parent = ThumbsUpButton + ThumbsDownImage.Parent = ThumbsDownButton + ThumbsUpImage.Position = UDim2.new(0.5, -ThumbsUpImage.Size.X.Offset / 2, 0.5, -ThumbsUpImage.Size.Y.Offset / 2) + ThumbsDownImage.Position = UDim2.new(0.5, -ThumbsDownImage.Size.X.Offset / 2, 0.5, -ThumbsDownImage.Size.Y.Offset / 2) + + local RatingText = Utility.Create'TextLabel' + { + Name = "RatingText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, VoteContainer.Position.X.Offset + VoteContainer.Size.X.Offset / 2, 1, -20); + BackgroundTransparency = 1; + Font = GlobalSettings.BoldFont; + FontSize = GlobalSettings.SubHeaderSize; + Text = ""; + Parent = this.Container; + } + local UpCountText = Utility.Create'TextLabel' + { + Name = "UpCountText"; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0, 43, 1, RatingText.Position.Y.Offset); + BackgroundTransparency = 1; + Font = GlobalSettings.RegularFont; + FontSize = GlobalSettings.SubHeaderSize; + TextColor3 = GlobalSettings.GreenTextColor; + Text = ""; + Parent = this.Container; + } + local DownCountText = UpCountText:Clone() + DownCountText.Name = "DownCountText" + DownCountText.Position = UDim2.new(0, 336, 1, RatingText.Position.Y.Offset) + DownCountText.TextColor3 = GlobalSettings.RedTextColor; + DownCountText.Parent = this.Container; + + local function setImagesAndText(vote, noVote) + ThumbsUpImage.Image = vote == true and "rbxasset://textures/ui/Shell/Icons/ThumbsUpFilled@1080.png" + or "rbxasset://textures/ui/Shell/Icons/ThumbsUp@1080.png" + ThumbsUpImage.Size = UDim2.new(0,48,0,48) + + ThumbsDownImage.Image = vote == false and "rbxasset://textures/ui/Shell/Icons/ThumbsDownFilled@1080.png" + or "rbxasset://textures/ui/Shell/Icons/ThumbsDown@1080.png" + ThumbsDownImage.Size = UDim2.new(0,48,0,48) + + RatingText.TextColor3 = + (vote == true and GlobalSettings.GreenTextColor) or + (vote == false and GlobalSettings.RedTextColor) or GlobalSettings.GreyTextColor + local textWord = (noVote and "FirstToRateWord") or + (vote == true and "LikedWord") or (vote == false and "DislikedWord") or nil + RatingText.Text = textWord and Strings:LocalizedString(textWord) or "" + end + + local function updateView(newVote) + local noVote = upVotes == 0 and downVotes == 0 + + VoteWidget:SetPercentFilled(not noVote and (upVotes / (upVotes + downVotes)) or nil) + setImagesAndText(newVote, noVote) + UpCountText.Text = Utility.FormatNumberString(noVote and "" or upVotes) + DownCountText.Text = Utility.FormatNumberString(noVote and "" or downVotes) + + ThumbsUpButton.Selectable = canVote + ThumbsDownButton.Selectable = canVote + + CannotVoteSelection.Parent = not canVote and this.Container.Parent or nil + defaultSelection = not canVote and CannotVoteSelection or ThumbsUpButton + end + + local function updateVoteCount(prevVote, newVote) + -- we must check for a unique vote before sending off the achivement event + -- unique votes are when prevVote is null. + if prevVote == nil then + UserData:IncrementVote() + elseif newVote == nil then + UserData:DecrementVote() + end + EventHub:dispatchEvent(EventHub.Notifications["VotedOnPlace"]) + end + + local isVotingDebouce = false + local function postVoteAsync(newVote) + if isVotingDebouce then return end + isVotingDebouce = true + local success, reason = nil, nil + if thisGameData then + success, reason = thisGameData:PostVoteAsync(newVote) + else + success = false + end + if success then + local prevVote = myVote + if newVote == true then + upVotes = upVotes + 1 + if prevVote == false then + downVotes = downVotes - 1 + end + elseif newVote == false then + downVotes = downVotes + 1 + if prevVote == true then + upVotes = upVotes - 1 + end + elseif newVote == nil then + if prevVote == true then + upVotes = upVotes - 1 + elseif prevVote == false then + downVotes = downVotes - 1 + end + end + myVote = newVote + updateView(newVote) + updateVoteCount(prevVote, newVote) + else + if reason and Errors.Vote[reason] then + ScreenManager:OpenScreen(ErrorOverlayModule(Errors.Vote[reason]), false) + else + ScreenManager:OpenScreen(ErrorOverlayModule(Errors.Default), false) + end + end + isVotingDebouce = false + end + + local function toggleCannotVoteDisplay() + CannotVoteText.Visible = not CannotVoteText.Visible + this.Container.Visible = not this.Container.Visible + end + CannotVoteSelection.SelectionGained:connect(toggleCannotVoteDisplay) + CannotVoteSelection.SelectionLost:connect(toggleCannotVoteDisplay) + + ThumbsUpButton.MouseButton1Click:connect(function() + local newVote = true + if myVote == true then + newVote = nil + end + postVoteAsync(newVote) + end) + ThumbsDownButton.MouseButton1Click:connect(function() + local newVote = false + if myVote == false then + newVote = nil + end + postVoteAsync(newVote) + end) + + --[[ Public API ]]-- + function this:SetParent(newParent) + self.Container.Parent = newParent + updateView(myVote) + end + + function this:SetPosition(newPosition) + self.Container.Position = newPosition + CannotVoteSelection.Position = UDim2.new(0, 0, 0, self.Container.Position.Y.Offset + 5) + end + + function this:SetVisible(value) + self.Container.Visible = value + end + + function this:GetDefaultSelection() + return defaultSelection + end + + function this:SetCanVote(value) + canVote = value + if thisGameData then + thisGameData:SetCanVote(value) + end + updateView(myVote) + end + + function this:GetCanVote() + return canVote + end + + function this:InitializeAsync(gameData) + thisGameData = gameData + local voteData = gameData:GetVoteDataAsync() + if voteData then + upVotes = voteData.UpVotes + downVotes = voteData.DownVotes + myVote = voteData.UserVote + canVote = voteData.CanVote or (voteData.CantVoteReason ~= "PlayGame") + end + + updateView(myVote) + VoteWidget:SetVisible(true) + end + + return this +end + +return createVoteView diff --git a/Client2018/content/internal/AppShell/Modules/Shell/Widgets/MoreButton.lua b/Client2018/content/internal/AppShell/Modules/Shell/Widgets/MoreButton.lua new file mode 100644 index 0000000..ce5fc43 --- /dev/null +++ b/Client2018/content/internal/AppShell/Modules/Shell/Widgets/MoreButton.lua @@ -0,0 +1,52 @@ + +local GuiService = game:GetService('GuiService') + +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild("RobloxGui") +local Modules = GuiRoot:FindFirstChild("Modules") +local ShellModules = Modules:FindFirstChild("Shell") + +local Utility = require(ShellModules:FindFirstChild('Utility')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) + +local function CreateMoreButton() + -- we override the selection on moreButton to fit around the moreImage + local overrideSelection = Utility.Create'ImageLabel' + { + Name = "OverrideSelection"; + Image = 'rbxasset://textures/ui/SelectionBox.png'; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(19,19,43,43); + BackgroundTransparency = 1; + } + + local moreButton = Utility.Create'ImageButton' + { + Name = "MoreButton"; + BackgroundTransparency = 1; + Visible = false; + SelectionImageObject = overrideSelection; + + Image = "rbxasset://textures/ui/Shell/Buttons/MoreButton@1080.png"; + Size = UDim2.new(0,108,0,50); + + SoundManager:CreateSound('MoveSelection'); + } + + local function updateMoreImage(isSelected) + moreButton.Image = isSelected and "rbxasset://textures/ui/Shell/Buttons/MoreButtonSelected@1080.png" + or "rbxasset://textures/ui/Shell/Buttons/MoreButton@1080.png" + moreButton.Size = UDim2.new(0,108,0,50) + end + moreButton.SelectionGained:connect(function() + updateMoreImage(true) + end) + moreButton.SelectionLost:connect(function() + updateMoreImage(false) + end) + updateMoreImage(GuiService.SelectedCoreObject == moreButton) + + return moreButton, overrideSelection +end + +return CreateMoreButton diff --git a/Client2018/content/internal/AppShell/VRAppShellStarterScript.lua b/Client2018/content/internal/AppShell/VRAppShellStarterScript.lua new file mode 100644 index 0000000..cf1a351 --- /dev/null +++ b/Client2018/content/internal/AppShell/VRAppShellStarterScript.lua @@ -0,0 +1,29 @@ +-- Start up the VR Engines +local RobloxGui = Game:GetService("CoreGui"):FindFirstChild("RobloxGui") + +-- Boot up the VR App Shell +local UserInputService = game:GetService('UserInputService') +local function onVREnabled(prop) + if prop == "VREnabled" then + if UserInputService.VREnabled then + + local shellInVRSuccess, shellInVRFlagValue = pcall(function() return settings():GetFFlag("EnabledAppShell3D") end) + local shellInVR = (shellInVRSuccess and shellInVRFlagValue == true) + + if shellInVR then + local modulesFolder = RobloxGui.Modules + local appHomeModule = modulesFolder:FindFirstChild('Shell') and modulesFolder:FindFirstChild('Shell'):FindFirstChild('AppHome') + if appHomeModule then + require(appHomeModule) + end + end + end + end +end + +spawn(function() + if UserInputService.VREnabled then + onVREnabled("VREnabled") + end + UserInputService.Changed:connect(onVREnabled) +end) diff --git a/Client2018/content/internal/AppShell/XStarterScript.lua b/Client2018/content/internal/AppShell/XStarterScript.lua new file mode 100644 index 0000000..342e343 --- /dev/null +++ b/Client2018/content/internal/AppShell/XStarterScript.lua @@ -0,0 +1,10 @@ +local rbxGui = game:GetService("CoreGui"):FindFirstChild("RobloxGui") + +-- Main entry into AppShell +require(rbxGui.Modules.Shell.AppHome) + +-- TODO: Clean this up so it doesn't get created anymore, but for now we should remove it +spawn(function() + local controlFrame = rbxGui:WaitForChild('ControlFrame') + controlFrame:Destroy() +end) diff --git a/Client2018/content/internal/AppShellLocalization/Modules/de.lua b/Client2018/content/internal/AppShellLocalization/Modules/de.lua new file mode 100644 index 0000000..14b717d --- /dev/null +++ b/Client2018/content/internal/AppShellLocalization/Modules/de.lua @@ -0,0 +1,230 @@ +local locale = +{ + ["HomeWord"] = "Hauptmenü"; + ["GamesWord"] = "Spiele"; + ["FriendsWord"] = "Freunde"; + ["CatalogWord"] = "Robux"; + ["AvatarWord"] = "Avatar"; + ["PlayWord"] = "Spielen"; + ["FavoriteWord"] = "Favorit"; + ["FavoritedWord"] = "Favorit"; + ["LikedWord"] = "Gefällt dir"; + ["DislikedWord"] = "Gefällt dir nicht"; + ["BackWord"] = "Zurück"; + ["SettingsWord"] = "Einstellungen"; + ["AccountWord"] = "Konto"; + ["OverscanWord"] = "Bildschirm anpassen"; + ["HelpWord"] = "Hilfe"; + ["SwitchProfileWord"] = "Profil wechseln"; + ["RecenterButtonWord"] = "Zentrieren"; + ["MoreWord"] = "Mehr"; + ["SearchWord"] = "Suchen"; + ["SearchGamesPhrase"] = "Spiele suchen"; + ["SearchingForPhrase"] = "Suchen nach: %s"; + ["NoGamesPhrase"] = "Keine Spiele gefunden."; + ["EditAvatarPhrase"] = "Avatar bearbeiten"; + ["FriendActivityWord"] = "Freundesaktivität"; + ["OnlineFriendsWords"] = "Online-Freunde"; + ["RecentlyPlayedWithSortTitle"] = "Zuletzt gemeinsam gespielt"; + ["StartPartyPhrase"] = "Party-App starten"; + ["RecommendedSortTitle"] = "Empfohlen"; + ["FavoritesSortTitle"] = "Favoriten"; + ["RecentlyPlayedSortTitle"] = "Zuletzt gespielt"; + ["MyRecentTitle"] = "Zuletzt gespielt"; + ["TopEarningTitle"] = "Top-Verdienst"; + ["TopRatedTitle"] = "Top-Bewertung"; + ["PopularTitle"] = "Beliebt"; + ["FeaturedTitle"] = "Highlight"; + ["MoreGamesPhrase"] = "Mehr Spiele"; + ["NoFriendsOnlinePhrase"] = "Deine Freunde sind nicht online."; + ["JoinGameWord"] = "Spiel beitreten"; + ["ViewGameDetailsWord"] = "Spielinfos anzeigen"; + ["InviteToPartyWord"] = "Zu Party einladen"; + ["ViewGamerCardWord"] = "Spielerkarte anzeigen"; + ["RatingDescriptionTitle"] = "Bewertung und Beschreibung"; + ["GameImagesTitle"] = "Spielbilder"; + ["GameBadgesTitle"] = "Spielabzeichen"; + ["FriendActivityTitle"] = "Freundesaktivität"; + ["RelatedGamesTitle"] = "Ähnliche Spiele"; + ["MoreDetailsTitle"] = "Mehr Infos"; + ["GameBadgesTitle"] = "Spielabzeichen"; + ["SelectYourAvatarTitle"] = "Wähle deinen Avatar"; + ["LastUpdatedWord"] = "Zuletzt aktualisiert"; + ["CreationDateWord"] = "Erstellungsdatum"; + ["CreatedByWord"] = "Erstellt von"; + ["ReportGameWord"] = "Spiel melden"; + ["HaveBadgeWord"] = "Du hast dieses Abzeichen"; + ["MaxPlayersWord"] = "Max. Spieler"; + ["ScreenSizeWord"] = "Bildschirmgröße"; + ["ResizeScreenPrompt"] = "Verwende den rechten Stick, um die Kanten der weißen Box so anzupassen, dass sie gerade nicht mehr zu sehen sind."; + ["ResizeScreenInputHint"] = "Bildschirmgröße anpassen"; + ["AcceptWord"] = "Annehmen"; + ["ResetWord"] = "Zurücksetzen"; + ["CannotVoteWord"] = "Du musst dieses Spiel erst spielen, bevor du es bewerten kannst."; + ["FirstToRateWord"] = "Gib die erste Bewertung für dieses Spiel ab."; + ["CustomAvatarPhrase"] = "Spiel mit benutzerdefinierten Avataren."; + ["EngagementScreenHint"] = "Zum Starten beliebige Taste drücken"; + ["EquipWord"] = "Avatar wechseln"; + ["BuyWord"] = "Kaufen"; + ["OkWord"] = "Okay"; + ["FreeWord"] = "Gratis"; + ["TakeWord"] = "Nehmen"; + ["GetRobuxPhrase"] = "Hol dir Robux"; + ["AlreadyOwnedPhrase"] = "Das besitzt du bereits."; + ["PurchasedThisPhrase"] = "Du hast das für %s Robux gekauft."; + ["RobuxBalanceTitle"] = "Mein Guthaben"; + ["RobuxBalanceOverlayTitle"] = "Robux-Guthaben"; + ["RobuxBalanceOverlayPhrase"] = "Auf Xbox können nur Robux verwendet werden, die im Xbox Store erworben wurden."; + ["PlatformBalanceTitle"] = "Auf Xbox verfügbar:"; + ["TotalBalanceTitle"] = "Gesamtguthaben:"; + ["AvatarCatalogTitle"] = "Katalog"; + ["AvatarOutfitsTitle"] = "Meine Sammlung"; + ["PurchasingTitle"] = "Wird gekauft ..."; + ["ConfirmPurchaseTitle"] = "Kauf bestätigen"; + ["AreYouSurePhrase"] = "Möchtest du „%s“ wirklich kaufen?"; + ["AreYouSureTakePhrase"] = "Möchtest du „%s“ wirklich nehmen?"; + ["AreYouSureWithPricePhrase"] = "Möchtest du „%s“ wirklich für %s kaufen?"; + ["RemainingBalancePhrase"] = "Danach hast du noch %s Robux."; + ["ConfirmWord"] = "Bestätigen"; + ["DeclineWord"] = "Ablehnen"; + ["OnlineWord"] = "Online"; + ["OfflineWord"] = "Offline"; + ["SubmitWord"] = "Senden"; + ["CancelWord"] = "Abbrechen"; + ["ReportPhrase"] = "Du kannst unserem Moderationsteam einen Bericht schicken. Wir werden das Spiel überprüfen und wenn nötig entsprechende Schritte einleiten."; + ["CurrencySymbol"] = "$"; + ["RobuxStoreDescription"] = "Hol dir Robux, um tolle neue Outfits für deinen Avatar sowie Vorteile und Fähigkeiten in Spielen kaufen zu können."; + ["RobuxStoreNoItemsPhrase"] = "Derzeit gibt es nichts zu kaufen. Bitte versuche es später erneut."; + ["PercentMoreRobuxPhrase"] = "%s%% mehr"; + ["RobuxStoreError"] = "Robux-Gegenstände sind derzeit nicht verfügbar."; + ["NoFriendsPhrase"] = "Deine Freunde sind offline. Probiere andere Spiele und lerne neue Freunde kennen!"; + ["PopupPartyUIErrorPhrase"] = "Beim Versuch, eine Party zu starten, ist ein Fehler aufgetreten. Bitte versuche es erneut."; + ["AuthenticationErrorTitle"] = "Authentifizierungsfehler"; + ["AuthInProgressPhrase"] = "Authentifizierung läuft bereits."; + ["AuthAccountUnlinkedPhrase"] = "This is a special case handled by EngagementScreen"; + ["AuthMissingGamePadPhrase"] = "Kein Controller gefunden. Bitte schalte einen Controller ein."; + ["AuthNoUserDetectedPhrase"] = "Kein Xbox Live-Benutzer gefunden. Bitte melde dich bei einem Xbox Live-Konto an."; + ["AuthHttpErrorDetected"] = "Problem bei der Kommunikation mit den Roblox-Servern. Auf www.roblox.com/help/xbox findest du weitere Infos."; + ["AuthErrorPhrase"] = "Problem bei der Kommunikation mit den Roblox-Servern. Bitte versuche es erneut."; + ["NewUserPhrase"] = "Von der Community erstellte Spiele. Endlose Möglichkeiten."; + ["SignInPhrase"] = "Anmelden"; + ["PlayAsPhrase"] = "Als %s registrieren"; + ["UsernameWord"] = "Benutzername"; + ["PasswordWord"] = "Passwort"; + ["UsernameRulePhrase"] = "3-20 Zeichen, keine Leerzeichen"; + ["PasswordRulePhrase"] = "Mind. 6 Buchstaben und 2 Ziffern"; + ["AccountSettingsTitle"] = "Kontoeinstellungen"; + ["InvalidUsernameTitle"] = "Ungültiger Benutzername"; + ["InvalidPasswordTitle"] = "Ungültiges Passwort"; + ["InvalidUsernamePhrase"] = "Benutzernamen müssen aus 3–20 Zeichen bestehen."; + ["InvalidPasswordPhrase"] = "Passwörter müssen aus mind. 6 Buchstaben und 2 Ziffern bestehen."; + ["AlreadyTakenTitle"] = "Benutzername belegt"; + ["AlreadyTakenPhrase"] = "Dieser Benutzername wird bereits verwendet. Bitte wähle einen anderen."; + ["InvalidCharactersUsedPhrase"] = "Der von dir eingegebene Benutzername enthält ungültige Zeichen. Es sind nur Buchstaben und Ziffern zulässig."; + ["UsernameCannotContainSpacesPhrase"] = "Der von dir eingegebene Benutzername enthält Leerzeichen. Es sind nur Buchstaben und Ziffern zulässig."; + ["NoUsernameEnteredPhrase"] = "Benutzername ist erforderlich."; + ["NoUsernameOrPasswordEnteredPhrase"] = "Du musst einen gültigen Benutzernamen und ein gültiges Passwort eingeben, um dein Roblox-Konto zu erstellen."; + ["LinkedAsPhrase"] = "%s ist derzeit mit deinem Roblox-Konto %s verknüpft."; + ["LinkSignUpDisabled"] = "Die Registrierung kann derzeit nicht durchgeführt werden. Bitte versuche es später erneut."; + ["LinkFlooded"] = "Du hast dich heute bereits zu oft registriert Bitte melde dich mit einem existierenden Roblox-Konto an oder versuche es später erneut."; + ["LinkLeaseLocked"] = "Vorgang läuft. Bitte warten."; + ["LinkAccountLinkingDisabled"] = "Die Kontoverknüpfung kann derzeit nicht durchgeführt werden. Bitte versuche es später erneut."; + ["LinkInvalidRobloxUser"] = "Der von dir eingegebene Roblox-Benutzername ist ungültig. Bitte gib einen gültigen Roblox-Benutzernamen ein."; + ["LinkRobloxUserAlreadyLinked"] = "Dein Xbox Live-Konto ist bereits mit einem Roblox-Konto verknüpft."; + ["LinkXboxUserAlreadyLinked"] = "Dein Xbox Live-Konto ist bereits mit einem Roblox-Konto verknüpft."; + ["LinkIllegalChildAccountLinking"] = "Die Konten konnten nicht verknüpft werden. Bitte überprüfe die Xbox-Jugendschutzeinstellungen."; + ["LinkInvalidPassword"] = "Das von dir eingegebene Passwort ist ungültig. Bitte gib das korrekte Passwort ein."; + ["LinkUsernamePasswordNotSet"] = "Benutzername oder Passwort fehlt. Bitte gib einen Benutzernamen und ein Passwort ein und versuche es erneut."; + ["LinkUsernameAlreadyTaken"] = "Dieser Benutzername wird bereits verwendet. Bitte wähle einen anderen Benutzernamen und versuche es erneut."; + ["LinkInvalidCredentials"] = "Benutzername oder Passwort ungültig. Bitte gib einen gültigen Benutzernamen und ein gültiges Passwort ein und versuche es erneut."; + ["UserIsGuestAccount"] = "Gastkonten können nicht authentifiziert werden. Bitte melde dich mit einem Hauptkonto an."; + ["LinkUnknownError"] = "Ein unbekannter Fehler ist aufgetreten. Bitte versuche es erneut."; + ["LinkAccountTitle"] = "Bei Roblox anmelden"; + ["LinkAccountPhrase"] = "Melde dich mit einem bestehenden Roblox-Konto an, um auf das Design deines Avatars zugreifen und deinen Spielfortschritt speichern zu können. Dein Roblox-Konto wird mit deinem Xbox Live-Konto verknüpft, und bei deiner nächsten Spielsitzung wirst du automatisch angemeldet!\n\nIn den Einstellungen kannst du die Verknüpfung jederzeit rückgängig machen."; + ["SignUpWord"] = "Registrieren"; + ["SignUpTitle"] = "Erstelle ein Roblox-Konto"; + ["SignUpPhrase"] = "Mit einem Roblox-Konto kannst du von jeder Plattform aus auf Roblox zugreifen. Lege einen Benutzernamen und ein Passwort fest, um dich von jedem Gerät aus anzumelden und weiterzuspielen!\n\nDein neues Roblox-Konto wird mit deinem Xbox Live-Konto verknüpft, und bei deiner nächsten Spielsitzung wirst du automatisch angemeldet!\n\nIn den Einstellungen kannst du die Verknüpfung jederzeit rückgängig machen."; + ["SetCredentialsTitle"] = "Benutzername und Passwort zuweisen"; + ["SetCredentialsPhrase"] = "Wie es scheint, wurdest du bei der Registrierung unterbrochen! Keine Sorge, du kannst die Erstellung deines Roblox-Kontos abschließen, indem du hier einen Benutzername und ein Passwort wählst.\n\nDein Roblox-Konto wird mit deinem Xbox Live-Konto verknüpft, und bei deiner nächsten Spielsitzung wirst du automatisch angemeldet!\n\nIn den Einstellungen kannst du die Verknüpfung jederzeit rückgängig machen."; + ["SetCredentialsWord"] = "Zuweisen"; + ["UnlinkTitle"] = "Kontoverknüpfung trennen"; + ["UnlinkGamerTagPhrase"] = "%s trennen"; + ["UnlinkPhrase"] = "Möchtest du die Verknüpfung zwischen diesem Roblox-Konto und deinem Xbox Live-Konto wirklich trennen? Deine Speicherdaten und getätigten Käufe sind an dieses Roblox-Konto gebunden und stehen erst dann wieder zur Verfügung, wenn du dich erneut anmeldest!"; + ["GameplaySettingsTitle"] = "Spieloptionen"; + ["EnabledWord"] = "Aktiviert (Empfohlen)"; + ["DisabledWord"] = "Deaktiviert"; + ["LoadingWord"] = "Wird geladen ..."; + ["KeepEnabledPhrase"] = "Einstellung beibehalten"; + ["DisableWord"] = "Deaktivieren"; + ["CrossPlatformGameplayPhrase"] = "Plattformübergreifendes Spielen"; + ["DisableCrossplayOverlayTitle"] = "Plattformübergreifendes Spielen deaktivieren?"; + ["DisableCrossplayOverlayMessage"] = "Möchtest du Plattformübergreifendes Spielen wirklich deaktivieren?\nDas wird nicht empfohlen. Du wirst danach keinen Spielen mit Teilnehmern von anderen Plattformen beitreten und du wirst auf weniger Teilnehmer in Spielen treffen."; + ["EnableCrossplayOverlayTitle"] = "Plattformübergreifendes Spielen ist jetzt aktiviert!"; + ["EnableCrossplayOverlayMessage"] = "Du kannst jetzt Roblox auf Xbox One mit Spielern von jeder Plattform, darunter PC-Systeme, Handys und Tablets, spielen! Das bedeutet, du wirst häufiger auf mehr Teilnehmer treffen und du kannst mit ALL deinen Freunden spielen, egal, was für ein Gerät diese verwenden!"; + ["CrossplayEnabledDescription"] = "Du wirst Spielen mit Teilnehmern von jeder Plattform beitreten und du wirst häufiger auf mehr Teilnehmer treffen."; + ["CrossplayDisabledDescription"] = "Das wird nicht empfohlen. Du wirst danach keinen Spielen mit Teilnehmern von anderen Plattformen beitreten und du wirst auf weniger Teilnehmer in Spielen treffen."; + ["UnableToJoinTitle"] = "Beitritt fehlgeschlagen"; + ["ErrorOccurredTitle"] = "Ein Fehler ist aufgetreten."; + ["DefaultErrorPhrase"] = "Verbindung zu Roblox konnte nicht hergestellt werden. Bitte versuche es später erneut."; + ["DefaultJoinFailPhrase"] = "Verbindung zu Roblox konnte nicht hergestellt werden. Bitte versuche es später erneut."; + ["AlreadyRunningPhrase"] = "Du befindest dich bereits in diesem Roblox-Spiel."; + ["WebServerConnectFailPhrase"] = "Verbindung zu Roblox-Servern konnte nicht hergestellt werden. Bitte versuche es später erneut."; + ["AccessDeniedByWeb"] = "Das Roblox-Spiel, dem du beitreten möchtest, ist derzeit nicht verfügbar."; + ["InstanceNotFound"] = "Das Roblox-Spiel, dem du beitreten möchtest, ist derzeit nicht verfügbar."; + ["GameFullPhrase"] = "Das Roblox-Spiel, dem du beitreten möchtest, ist derzeit voll."; + ["FollowUserFailed"] = "Das Roblox-Spiel, dem du beitreten möchtest, ist derzeit nicht verfügbar."; + ["InvalidPrivilegeMultiplayerSessionPhrase"] = "Dein Xbox Live-Konto ist derzeit nicht berechtigt, an Mehrspielersitzungen teilzunehmen."; + ["InvalidPrivilegeUGCPhrase"] = "Dein Xbox Live-Konto ist derzeit nicht berechtigt, von Spielern erstellte Inhalte zu spielen."; + ["UnableToEquipTitle"] = "Auswahl fehlgeschlagen"; + ["UnableToEquipPhrase"] = "Dieser Avatar konnte nicht ausgewählt werden. Bitte versuche es später erneut."; + ["UnableToWearOufitTitle"] = "Auswahl fehlgeschlagen"; + ["UnableToWearOufitPhrase"] = "Dieses Outfit konnte nicht ausgewählt werden. Bitte versuche es später erneut."; + ["UnableToDoPurchaseTitle"] = "Kauf nicht abgeschlossen"; + ["UnableToDoPurchasePhrase"] = "Dein Kauf konnte nicht abgeschlossen werden. Bitte versuche es später erneut."; + ["UnableToDoRobuxPurchaseTitle"] = "Kauf nicht abgeschlossen"; + ["UnableToDoRobuxPurchasePhrase"] = "Dein Kauf konnte nicht abgeschlossen werden. Bitte versuche es später erneut."; + ["CannotVoteTitle"] = "Abstimmen fehlgeschlagen"; + ["VoteFloodPhrase"] = "Du stimmst zu häufig ab. Komm später wieder und versuche es erneut."; + ["VotePlayGamePhrase"] = "Du musst dieses Spiel erst spielen, bevor du abstimmen kannst."; + ["CannotFavoriteTitle"] = "Spiel nicht als Favorit festgelegt"; + ["FavoriteFloodPhrase"] = "Du legst zu viele Spiele als Favoriten fest. Komm später wieder und versuche es erneut."; + ["ToSPhrase"] = "Bedingungen"; + ["ToSInfoLinkPhrase"] = "Nutzungsbedingungen und Datenschutzrichtlinien:\nwww.roblox.com/info/terms-of-service\nwww.roblox.com/info/privacy"; + ["PrivacyPhrase"] = "Datenschutz"; + ["VersionIdString"] = "Version: %s.%s.%s.%s"; + ["ErrorMessageAndCodePrase"] = "%s\nFehlercode: %d"; + ["UnlockGamesPhrase"] = "Spiele 5 Highlight-Spiele, um weitere Inhalte freizuschalten, die von Mitgliedern der Community wie dir erstellt wurden!"; + ["AlertOccurredTitle"] = "Warnung"; + ["DefaultAlertPhrase"] = "Verbindung zu Roblox konnte nicht hergestellt werden. Bitte versuche es später erneut."; + ["UnlockedUGCTitle"] = "Alle Spiele freigeschaltet!"; + ["UnlockedUGCPhrase"] = "Du hast 5 Highlight-Spiele gespielt und kannst nun auf die gesamte Auswahl an Spielen zugreifen, die von Mitgliedern der Community wie dir erstellt wurden!\n\nAuf dem SPIELE-Bildschirm findest du alle neuen Spiele, die du freigeschaltet hast!"; + ["PlatformLinkInfoTitle"] = "Willkommen bei Roblox!"; + ["PlatformLinkInfoMessage"] = "Wir haben ein neues Roblox-Konto für dich erstellt. Dein gesamter Spielfortschritt sowie deine getätigten Käufe werden unter diesem Roblox-Konto gespeichert. Jetzt kannst du dich von jeder Plattform aus anmelden und nahtlos weiterspielen!"; + ["ControllerLostConnectionTitle"] = "Controller fehlt"; + ["ControllerLostConnectionPhrase"] = "Controller von Benutzer „%s“ wurde getrennt. Bitte drücke die A-Taste auf dem Controller, mit dem du weiterspielen möchtest."; + ["ActiveUserLostConnectionTitle"] = "Aktiver Benutzer entfernt"; + ["ActiveUserLostConnectionPhrase"] = "Benutzer „%s“ wurde abgemeldet. Bitte drücke die A-Taste auf dem Controller, mit dem du weiterspielen möchtest."; + ["PlayMyPlaceMoreGamesTitle"] = "Meine Spiele"; + ["PlayMyPlaceMoreGamesPhrase"] = "Melde dich auf Roblox.com an, um weitere Spiele zu erstellen und deine bestehenden Inhalte zu bearbeiten!"; + ["PrivateSessionPhrase"] = "In privatem Spiel"; + ["ReauthSignedOutTitle"] = "Abgemeldet"; + ["ReauthSignedOutPhrase"] = "Du hast dich von deinem Xbox Live-Konto abgemeldet. Bitte melde dich an, um fortzufahren."; + ["ReauthRemovedTitle"] = "Anderer Benutzer"; + ["ReauthRemovedPhrase"] = "Wir haben festgestellt, dass sich der aktive Benutzer geändert hat. Bitte melde dich erneut an."; + ["ReauthInvalidSessionPhrase"] = "Du wurdest von allen aktuellen Roblox-Sitzungen abgemeldet."; + ["ReauthUnlinkTitle"] = "Kontoverknüpfung getrennt"; + ["ReauthUnlinkPhrase"] = "Du hast die Verknüpfung zu deinem Roblox-Konto getrennt. Du kannst dich jetzt erneut anmelden oder ein neues Roblox-Konto erstellen."; + ["ReauthUnknownPhrase"] = "Ein Fehler ist aufgetreten und du wurdest abgemeldet. Bitte melde dich erneut an, um weiterzuspielen."; + ["AccountUnder13Phrase"] = "Konto: Unter 13 J."; + ["AccountOver13Phrase"] = "Konto: Über 13 J."; + ["CPPWelcomeTitle"] = "Willkommen beim Plattformübergreifenden Spielen!"; + ["CPPWelcomePhrase"] = "Du kannst jetzt Roblox auf Xbox One mit Spielern von jeder Plattform, darunter PC-Systeme, Handys und Tablets, spielen! Das bedeutet, du wirst häufiger auf mehr Teilnehmer treffen und du kannst mit ALL deinen Freunden spielen, egal, was für ein Gerät diese verwenden! Die Spieloptionen können in den Kontoeinstellungen angepasst werden."; + ["MPSRestrictedPhrase"] = "Die Einstellungen deines Xbox Live-Kontos erlauben keine Mehrspielersitzungen. Du kannst dies in den Einstellungen deiner Xbox oder auf Xbox.com ändern."; + ["MPSBannedPhrase"] = "Dein Xbox Live-Konto ist derzeit von der Teilnahme an Mehrspielersitzungen ausgeschlossen."; + ["MPSPurchaseRequiredPhrase"] = "Du musst Xbox Live Gold haben, um an Online-Mehrspielersitzungen teilzunehmen."; + ["UGCRestrictedPhrase"] = "Die Einstellungen deines Xbox Live-Kontos verhindern, dass du von Spielern erstellte Inhalte spielen kannst. Du kannst dies in den Einstellungen deiner Xbox oder auf Xbox.com ändern."; + ["UGCBannedPhrase"] = "Dein Xbox Live-Konto ist derzeit davon ausgeschlossen, von Spielern erstellte Inhalte zu spielen."; + ["PrivilegeCheckFailPhrase"] = "Dein Xbox Live-Konto ist derzeit nicht zum Spielen berechtigt. Bitte versuche es später erneut."; +} + +return locale diff --git a/Client2018/content/internal/AppShellLocalization/Modules/en-US.lua b/Client2018/content/internal/AppShellLocalization/Modules/en-US.lua new file mode 100644 index 0000000..f2def6a --- /dev/null +++ b/Client2018/content/internal/AppShellLocalization/Modules/en-US.lua @@ -0,0 +1,330 @@ +local enUS = +{ + ["HomeWord"] = "Home"; + ["GamesWord"] = "Games"; + ["FriendsWord"] = "Friends"; + ["CatalogWord"] = "Robux"; + ["AvatarWord"] = "Avatar"; + ["PlayWord"] = "Play"; + ["FavoriteWord"] = "Favorite"; + ["FavoritedWord"] = "Favorited"; + ["LikedWord"] = "Liked"; + ["DislikedWord"] = "Disliked"; + ["BackWord"] = "Back"; + ["SettingsWord"] = "Settings"; + ["AvatarEditorWord"] = "Avatar Editor"; + ["AccountWord"] = "Account"; + ["OverscanWord"] = "Adjust Screen"; + ["HelpWord"] = "Help"; + ["SwitchProfileWord"] = "Switch Profile"; + ["RecenterButtonWord"] = "Recenter"; + ["MoreWord"] = "More"; + + ["SearchWord"] = "Search"; + ["SearchGamesPhrase"] = "Search Games"; + ["SearchingForPhrase"] = "Searching For: %s"; + ["NoGamesPhrase"] = "No games found."; + + ["EditAvatarPhrase"] = "Edit Avatar"; + ["FriendActivityWord"] = "Friend Activity"; + ["OnlineFriendsWords"] = "Online Friends"; + ["RecentlyPlayedWithSortTitle"] = "Recently Played With"; + ["StartPartyPhrase"] = "Snap Party App"; + + ["RecommendedSortTitle"] = "Recommended"; + ["FavoritesSortTitle"] = "Favorites"; + ["RecentlyPlayedSortTitle"] = "Recently Played"; + ["MyRecentTitle"] = "My Recent"; + ["TopEarningTitle"] = "Top Earning"; + ["TopRatedTitle"] = "Top Rated"; + ["PopularTitle"] = "Popular"; + ["FeaturedTitle"] = "Featured"; + ["MoreGamesPhrase"] = "More Games"; + + ["NoFriendsOnlinePhrase"] = "Your friends are not online"; + ["JoinGameWord"] = "Join Game"; + ["ViewGameDetailsWord"] = "View Game Details"; + ["EmptyFriendSideBarWord"] = "When this user is playing a game, you can join them from here"; + ["InviteToPartyWord"] = "Invite To Party"; + ["ViewGamerCardWord"] = "View Gamer Card"; + + ["RatingDescriptionTitle"] = "Rating and Description"; + ["GameImagesTitle"] = "Game Images"; + ["GameBadgesTitle"] = "Game Badges"; + ["FriendActivityTitle"] = "Friend Activity"; + ["RelatedGamesTitle"] = "Related Games"; + ["MoreDetailsTitle"] = "More Details"; + + ["SelectYourAvatarTitle"] = "Select Your Avatar"; + + ["LastUpdatedWord"] = "Last Updated"; + ["CreationDateWord"] = "Creation Date"; + ["CreatedByWord"] = "Created By"; + ["ReportGameWord"] = "Report Game"; + ["HaveBadgeWord"] = "You Have This Badge"; + ["MaxPlayersWord"] = "Max Players"; + + ["ScreenSizeWord"] = "Screen Size"; + ["ResizeScreenPrompt"] = "Use the right stick to adjust the edges of the white box until it is just off the screen"; + ["ResizeScreenInputHint"] = "Resize Screen"; + ["AcceptWord"] = "Accept"; + ["ResetWord"] = "Reset"; + ["CannotVoteWord"] = "You must play this game before rating"; + ["FirstToRateWord"] = "Be the first to rate this game"; + ["CustomAvatarPhrase"] = "This game uses custom Avatars"; + ["ExperimentalGamePhrase"] = "Experimental Game: Not Cross-Platform"; + + ["EngagementScreenHint"] = "Press Any Button To Begin"; + ["EngagementHint"] = "Press {Button_A} to Begin"; + ["SwitchAccountHint"] = "Switch Xbox Account"; + + ["EquipWord"] = "Swap Avatar"; + ["BuyWord"] = "Buy"; + ["OkWord"] = "Ok"; + ["FreeWord"] = "Free"; + ["TakeWord"] = "Take"; + ['GetRobuxPhrase'] = "Get Robux"; + ["AlreadyOwnedPhrase"] = "You already own this"; + ["PurchasedThisPhrase"] = "You purchased this for %s Robux"; + ["RobuxBalanceTitle"] = "My Balance"; + + ["RobuxBalanceOverlayTitle"] = "Robux Balance"; + ["RobuxBalanceOverlayPhrase"] = "Only Robux purchased from the Xbox Store may be used on Xbox."; + ["PlatformBalanceTitle"] = "Available on Xbox:"; + ["TotalBalanceTitle"] = "Total Balance:"; + + ["AvatarCatalogTitle"] = "Catalog"; + ["AvatarOutfitsTitle"] = "My Collection"; + + + ["PurchasingTitle"] = "Purchasing..."; + ["ConfirmPurchaseTitle"] = "Confirm Purchase"; + ["AreYouSurePhrase"] = "Are you sure you want to buy \"%s\"?"; + ["AreYouSureTakePhrase"] = "Are you sure you want to take \"%s\"?"; + ["AreYouSureWithPricePhrase"] = "Are you sure you want to buy \"%s\" for %s?"; + ["RemainingBalancePhrase"] = "Your remaining balance will be %s Robux"; + ["ConfirmWord"] = "Confirm"; + ["DeclineWord"] = "Decline"; + + ["OnlineWord"] = "Online"; + ["OfflineWord"] = "Offline"; + ["AllWord"] = "All"; + + ["SubmitWord"] = "Submit"; + ["CancelWord"] = "Cancel"; + ["ReportPhrase"] = "You can send a report to our moderation team. We will review the game and take appropriate action."; + + + ["CurrencySymbol"] = "$"; + ["RobuxStoreDescription"] = "Get Robux to buy great new looks for your Avatar, plus perks and abilities in games."; + ["RobuxStoreNoItemsPhrase"] = "There are no items for purchase right now, please try again later"; + ["PercentMoreRobuxPhrase"] = "%s%% More"; + --["RobuxStoreError"] = "Robux items are inaccessable at this time."; + + ["NoFriendsPhrase"] = "Your friends are not online. Play some games and make new friends!"; + ["PlayAndMakeFriendsPhrase"] = "Play some games and make new friends!"; + + -- Platform Service Errors + ["PopupPartyUIErrorPhrase"] = "There was an error trying to start a party. Please try again."; + + -- Auth Errors + ["AuthenticationErrorTitle"] = "Authentication Error"; + ["AuthInProgressPhrase"] = "Authentication already in progress"; + ["AuthAccountUnlinkedPhrase"] = ""; -- This is a special case handled by EngagementScreen + ["AuthMissingGamePadPhrase"] = "No gamepad detected. Please turn on a gamepad."; + ["AuthNoUserDetectedPhrase"] = "No Xbox Live user detected. Please sign in to an Xbox Live account."; + ["AuthHttpErrorDetected"] = "Trouble communicating with Roblox servers. Please check www.roblox.com/help/xbox for more info."; + ["AuthErrorPhrase"] = "Trouble communicating with Roblox servers. Please try again."; + + -- Sign In + ["NewUserPhrase"] = "Community Created Gaming. Limitless Possibilities."; + ["SignInPhrase"] = "Sign In"; + ["PlayAsPhrase"] = "Sign Up Using %s"; + ["UsernameWord"] = "Username"; + ["PasswordWord"] = "Password"; + ["UsernameRulePhrase"] = "3-20 characters, no spaces"; + ["PasswordRulePhrase"] = "6 letters and 2 numbers minimum"; + ["AccountSettingsTitle"] = "Account Settings"; + + + -- Sign In/Up Errors + ["InvalidUsernameTitle"] = "Invalid Username"; + ["InvalidPasswordTitle"] = "Invalid Password"; + ["InvalidUsernamePhrase"] = "Usernames must have 3-20 characters."; + ["InvalidPasswordPhrase"] = "Passwords must have at least 6 letters and 2 numbers"; + ["AlreadyTakenTitle"] = "Username Taken"; + ["AlreadyTakenPhrase"] = "That username is already taken, please try another."; + ["InvalidCharactersUsedPhrase"] = "The username you entered contains invalid characters. Only letters and numbers are allowed."; + ["UsernameCannotContainSpacesPhrase"] = "The username you entered contains spaces. Only letters and numbers are allowed."; + ["NoUsernameEnteredPhrase"] = "Username is required."; + ["NoUsernameOrPasswordEnteredPhrase"] = "You must enter a valid username and password to set up your Roblox account."; + ["LinkedAsPhrase"] = "%s is currently linked to your Roblox account, %s."; + + -- Linking Errors + ["LinkSignUpDisabled"] = "Sign up is currently disabled. Please try again later."; + ["LinkFlooded"] = "You have signed up too many times today. Please sign in with an existing Roblox account or try again later."; + ["LinkLeaseLocked"] = "Transaction in progress. Please wait."; + ["LinkAccountLinkingDisabled"] = "Account linking is currently disabled. Please try again later."; + ["LinkInvalidRobloxUser"] = "The Roblox username you entered is invalid. Please enter a valid Roblox username."; + ["LinkRobloxUserAlreadyLinked"] = "Your Xbox Live account is already linked to a Roblox account"; + ["LinkXboxUserAlreadyLinked"] = "Your Xbox Live account is already linked to a Roblox account."; + ["LinkIllegalChildAccountLinking"] = "The accounts could not be linked. Please review your Xbox age settings."; + ["LinkInvalidPassword"] = "The password you entered is invalid. Please enter the correct password."; + ["LinkUsernamePasswordNotSet"] = "Username or password is empty. Please enter a username and password and try again."; + ["LinkUsernameAlreadyTaken"] = "That username is already taken. Please choose another username and try again."; + ["LinkInvalidCredentials"] = "The username or password is invalid. Please enter a valid username and password and try again."; + ["UserIsGuestAccount"] = "Guest accounts cannot be authenticated. Please sign in to a non-Guest account."; + ["LinkUnknownError"] = "An unknown error has occurred. Please try again."; + + -- Linking + ["LinkAccountTitle"] = "Sign in to Roblox"; + ["LinkAccountPhrase"] = "Sign in with an existing Roblox account to access your Avatar appearance and save game progress.\n\nYour Roblox account will be linked to your Xbox Live account, and the next time you play you will sign in automatically!\n\nYou can unlink from the Settings screen at any time."; + + -- Sign Up + ["SignUpWord"] = "Sign up"; + ["SignUpTitle"] = "Create a Roblox Account"; + ["SignUpPhrase"] = "A Roblox account is your access to Roblox on every platform. Simply set a username and password and you can sign in anywhere and continue where you left off!\n\nYour new Roblox account will be linked to your Xbox Live account, and the next time you play you will sign in automatically!\n\nYou can unlink from the Settings screen at any time."; + + -- Edge Case - have linked account but no credentials + ["SetCredentialsTitle"] = "Assign Username and Password"; + ["SetCredentialsPhrase"] = "Looks like you were interrupted before you could finish signing up! Don't worry, you can finish setting up your Roblox account by choosing a username and password here.\n\nYour Roblox account will be linked to your Xbox Live account, and the next time you play you will sign in automatically!\n\nYou can unlink from the Settings screen at any time."; + ["SetCredentialsWord"] = "Assign"; + + -- Unlink + ["UnlinkTitle"] = "Unlink Account"; + ["AccountLinkingTitle"] = "Account Linking"; + ["UnlinkGamerTagPhrase"] = "Unlink %s"; + ["UnlinkGamerTagWord"] = "Unlink"; + ["UnlinkPhrase"] = "Are you sure you want to unlink this Roblox account from your Xbox account? Your save data and purchases are associated with this Roblox account and will be inaccessible until you sign back in!"; + + -- Gameplay Settings + ["GameplaySettingsTitle"] = "Gameplay Options"; + ["EnabledWord"] = "Enabled (Recommended)"; + ["DisabledWord"] = "Disabled"; + ["LoadingWord"] = "Loading..."; + ["KeepEnabledPhrase"] = "Keep Enabled"; + ["DisableWord"] = "Disable"; + ["CrossPlatformGameplayPhrase"] = "Cross-Platform Play"; + ["DisableCrossplayOverlayTitle"] = "Disable Cross-Platform Play?"; + ["DisableCrossplayOverlayMessage"] = "Are you sure you want to disable cross-platform play?\nThis is not recommended. You will no longer join games with players on other platforms, and you will see less players in games."; + ["EnableCrossplayOverlayTitle"] = "Cross-Platform Play is Now Enabled!"; + ["EnableCrossplayOverlayMessage"] = "You can now play Roblox on Xbox One with players on every platform including Desktop, Phone, and Tablet!\n\nThis means you’ll see more players in more games, and you can play with ALL of your friends, no matter what device they are using!"; + ["CrossplayEnabledDescription"] = "You will join games with players from all platforms, and you will see more players in more games."; + ["CrossplayDisabledDescription"] = "This is not recommended. You will not join games with players on other platforms, and you will see less players in games."; + + -- Error Strings + ["UnableToJoinTitle"] = "Unable to Join"; + ["ErrorOccurredTitle"] = "An Error Occurred"; + + ["DefaultErrorPhrase"] = "Could not connect to Roblox. Please try again later."; + + ["DefaultJoinFailPhrase"] = "Could not connect to Roblox. Please try again later."; + ["AlreadyRunningPhrase"] = "You are already in this Roblox game."; + ["WebServerConnectFailPhrase"] = "Could not connect to Roblox servers. Please try again later."; + ["AccessDeniedByWeb"] = "The Roblox game you are trying to join is currently not available."; + ["InstanceNotFound"] = "The Roblox game you are trying to join is currently not available."; + ["GameFullPhrase"] = "The Roblox game you are trying to join is currently full."; + ["FollowUserFailed"] = "The Roblox game you are trying to join is currently not available."; + ["InvalidPrivilegeMultiplayerSessionPhrase"] = "Your Xbox account is currently not permitted to play multiplayer sessions."; + ["InvalidPrivilegeUGCPhrase"] = "Your Xbox account is currently not permitted to play user generated content."; + ["MPSRestrictedPhrase"] = "Your Xbox account settings prevent you from playing multiplayer sessions. You can change this in your Xbox Settings or on Xbox.com"; + ["MPSBannedPhrase"] = "Your Xbox account is currently banned from playing in multiplayer sessions."; + ["MPSPurchaseRequiredPhrase"] = "You must have Xbox Live Gold in order to play online multiplayer sessions."; + ["UGCRestrictedPhrase"] = "Your Xbox account settings prevent you from playing user generated content. You can change this in your Xbox Settings or on Xbox.com"; + ["UGCBannedPhrase"] = "Your Xbox account is currently banned from playing user generated content."; + ["PrivilegeCheckFailPhrase"] = "Your Xbox account is currently not permitted to play. Please try again later."; + + ["UnableToEquipTitle"] = "Unable to Select"; + ["UnableToEquipPhrase"] = "We were unable to select that Avatar. Please try again later."; + + ["UnableToWearOufitTitle"] = "Unable to Select"; + ["UnableToWearOufitPhrase"] = "We were unable to select that Outfit. Please try again later."; + + ["UnableToDoPurchaseTitle"] = "Unable to Complete Purchase"; + ["UnableToDoPurchasePhrase"] = "We were unable to complete your purchase. Please try again later."; + + ["UnableToDoRobuxPurchaseTitle"] = "Unable to Complete Purchase"; + ["UnableToDoRobuxPurchasePhrase"] = "We were unable to complete your purchase. Please try again later."; + + ["CannotVoteTitle"] = "Cannot Vote"; + ["VoteFloodPhrase"] = "You're voting too often. Come back later and try again."; + ["VotePlayGamePhrase"] = "You must play this game before voting."; + + ["CannotFavoriteTitle"] = "Cannot Favorite Game"; + ["FavoriteFloodPhrase"] = "You're favoriting games too often. Come back later and try again."; + + + -- Terms of Service & Privacy + ["ToSPhrase"] = "View Terms..."; + ["ToSInfoLinkPhrase"] = "Terms & Privacy Policy:\nwww.roblox.com/info/terms-of-service\nwww.roblox.com/info/privacy"; + ["PrivacyPhrase"] = "Privacy"; + + -- Codes + ["VersionIdString"] = "Version: %s.%s.%s.%s"; + ["ErrorMessageAndCodePrase"] = "%s\nError Code: %d"; + + + -- UGC + ["UnlockGamesPhrase"] = "Play 5 Featured games to unlock more creations made by community members just like you!"; + + + -- Alert Strings + ["AlertOccurredTitle"] = "An Alert Occurred"; + ["DefaultAlertPhrase"] = "Could not connect to Roblox. Please try again later."; + + ["UnlockedUGCTitle"] = "All Games Unlocked!"; + ["UnlockedUGCPhrase"] = "Now that you've played 5 Featured games, you can access the full selection of games created by community members just like you!\n\nGo to the GAMES screen to see all the new games you've unlocked!"; + + ["PlatformLinkInfoTitle"] = "Welcome to Roblox!"; + ["PlatformLinkInfoMessage"] = "We have created a new Roblox account for you. All game progress and purchases will be saved to this Roblox account. Now you can sign in on any platform and continue where you left off!"; + + ["ControllerLostConnectionTitle"] = "Missing Controller"; + ["ControllerLostConnectionPhrase"] = "Controller for user '%s' has been disconnected, please press 'A' on the controller you would like to continue with."; + + ["ActiveUserLostConnectionTitle"] = "Active User Removed"; + ["ActiveUserLostConnectionPhrase"] = "User '%s' has been logged out, please press 'A' on the controller you would like to continue with."; + + ["PlayMyPlaceMoreGamesTitle"] = "My Games"; + ["PlayMyPlaceMoreGamesPhrase"] = "Sign in on Roblox.com to create more games and edit your existing creations!"; + ["PrivateSessionPhrase"] = "In Private Game"; + + -- Reauth (Booted to engagement screen), alerts + ["ReauthSignedOutTitle"] = "Signed Out"; + ["ReauthSignedOutPhrase"] = "You have signed out of your Xbox Live account. Please sign in to continue."; + ["ReauthRemovedTitle"] = "User Changed"; + ["ReauthRemovedPhrase"] = "We have detected a change in the active user. Please sign in again."; + ["ReauthInvalidSessionPhrase"] = "You have been signed out of all current Roblox sessions."; + ["ReauthUnlinkTitle"] = "Account Unlinked"; + ["ReauthUnlinkPhrase"] = "You have successfully unlinked from your Roblox account. You can now choose to sign in again, or sign up for a new Roblox account."; + ["ReauthUnknownPhrase"] = "An error occurred and you have been signed out. Please sign in again to continue playing."; + + -- Account Age + ["AccountUnder13Phrase"] = "Account: Under 13 yrs"; + ["AccountOver13Phrase"] = "Account: Over 13 yrs"; + + -- Cross-Platform Play Welcome Popup + ["CPPWelcomeTitle"] = "Welcome to Cross-Platform Play!"; + ["CPPWelcomePhrase"] = "You can now play Roblox on Xbox One with players on every platform including Desktop, Phone, and Tablet!\n\nThis means you’ll see more players in more games, and you can play with ALL of your friends, no matter what device they are using! Gameplay options can be updated in Account Settings."; + ["SetCPPSettingErrorPhrase"] = "Could not toggle Cross-Platform setting. Please try again later."; + + --Privilege Settings + ["ErrorWord"] = "Error"; + ["BannedWord"] = "Banned"; + ["DeniedWord"] = "Denied"; + ["AllowedWord"] = "Allowed"; + ["SharedContentWord"] = "Shared Content"; + ["MultiplayerWord"] = "Multiplayer"; + ["XboxAccountSettingsPhrase"] = "Xbox Account Settings"; + ["PurchaseRequiredPhrase"] = "Gold Required"; + ["PrivilegeAllowedPhrase"] = "Your Xbox Settings are correctly set and you are ready to play!"; + ["PrivilegeErrorPhrase"] = "There was an error retrieving your Xbox Settings. This is likely due to a temporary outage. Please try again later."; + ["PrivilegeBannedPhrase"] = "Your Xbox account is currently banned from Xbox Live. You won't be able to play games until the ban is lifted."; + ["PrivilegePurchaseRequiredPhrase"] = "Xbox Live Gold is required to play online multiplayer games on Xbox."; + ["PrivilegeDeniedPhrase"] = table.concat{"Your Xbox Settings are blocking you from playing games. Select a red option above to go to your Xbox Settings.", + "\n\nThe settings should be:", + "\n\"You can join multiplayer games\" > Allow", + "\n\"You can see and share content\" > Everybody"}; + ["GoToSettingsPhrase"] = "Go to Settings"; +} + +return enUS diff --git a/Client2018/content/internal/AppShellLocalization/Modules/es.lua b/Client2018/content/internal/AppShellLocalization/Modules/es.lua new file mode 100644 index 0000000..daf3f56 --- /dev/null +++ b/Client2018/content/internal/AppShellLocalization/Modules/es.lua @@ -0,0 +1,230 @@ +local locale = +{ + ["HomeWord"] = "Inicio"; + ["GamesWord"] = "Juegos"; + ["FriendsWord"] = "Amigos"; + ["CatalogWord"] = "Robux"; + ["AvatarWord"] = "Avatar"; + ["PlayWord"] = "Jugar"; + ["FavoriteWord"] = "Añadir favorito"; + ["FavoritedWord"] = "Favoritos"; + ["LikedWord"] = "Te gusta"; + ["DislikedWord"] = "No te gusta"; + ["BackWord"] = "Atrás"; + ["SettingsWord"] = "Configuración"; + ["AccountWord"] = "Cuenta"; + ["OverscanWord"] = "Ajuste de pantalla"; + ["HelpWord"] = "Ayuda"; + ["SwitchProfileWord"] = "Cambiar de perfil"; + ["RecenterButtonWord"] = "Centrar de nuevo"; + ["MoreWord"] = "Más"; + ["SearchWord"] = "Buscar"; + ["SearchGamesPhrase"] = "Buscar juegos"; + ["SearchingForPhrase"] = "Buscando: %s"; + ["NoGamesPhrase"] = "No se han encontrado juegos."; + ["EditAvatarPhrase"] = "Editar avatar"; + ["FriendActivityWord"] = "Actividad de tus amigos"; + ["OnlineFriendsWords"] = "Amigos conectados"; + ["RecentlyPlayedWithSortTitle"] = "Has jugado recientemente con"; + ["StartPartyPhrase"] = "Acoplar aplicación de grupo"; + ["RecommendedSortTitle"] = "Recomendados"; + ["FavoritesSortTitle"] = "Favoritos"; + ["RecentlyPlayedSortTitle"] = "Jugados recientemente"; + ["MyRecentTitle"] = "Mis juegos recientes"; + ["TopEarningTitle"] = "Los más rentables"; + ["TopRatedTitle"] = "Los mejor valorados"; + ["PopularTitle"] = "Populares"; + ["FeaturedTitle"] = "Promocionados"; + ["MoreGamesPhrase"] = "Más juegos"; + ["NoFriendsOnlinePhrase"] = "Tus amigos no están conectados."; + ["JoinGameWord"] = "Unirse al juego"; + ["ViewGameDetailsWord"] = "Ver detalles del juego"; + ["InviteToPartyWord"] = "Invitar al grupo"; + ["ViewGamerCardWord"] = "Ver tarjeta de jugador"; + ["RatingDescriptionTitle"] = "Valoración y descripción"; + ["GameImagesTitle"] = "Imágenes del juego"; + ["GameBadgesTitle"] = "Emblemas del juego"; + ["FriendActivityTitle"] = "Actividad de amigos"; + ["RelatedGamesTitle"] = "Juegos relacionados"; + ["MoreDetailsTitle"] = "Más detalles"; + ["GameBadgesTitle"] = "Emblemas del juego"; + ["SelectYourAvatarTitle"] = "Selecciona tu avatar"; + ["LastUpdatedWord"] = "Actualización más reciente"; + ["CreationDateWord"] = "Fecha de creación"; + ["CreatedByWord"] = "Creado por"; + ["ReportGameWord"] = "Denunciar juego"; + ["HaveBadgeWord"] = "Tienes este emblema"; + ["MaxPlayersWord"] = "Máximo de jugadores"; + ["ScreenSizeWord"] = "Tamaño de pantalla"; + ["ResizeScreenPrompt"] = "Usa el stick derecho para ajustar los extremos de la caja blanca justo hasta que desaparezcan de la pantalla."; + ["ResizeScreenInputHint"] = "Redimensionar pantalla"; + ["AcceptWord"] = "Aceptar"; + ["ResetWord"] = "Reiniciar"; + ["CannotVoteWord"] = "Debes jugar a este juego antes de valorarlo."; + ["FirstToRateWord"] = "Sé la primera persona en valorar este juego."; + ["CustomAvatarPhrase"] = "Este juego utiliza avatares personalizados."; + ["EngagementScreenHint"] = "Pulsa cualquier botón para empezar."; + ["EquipWord"] = "Alternar avatar"; + ["BuyWord"] = "Comprar"; + ["OkWord"] = "Aceptar"; + ["FreeWord"] = "Gratis"; + ["TakeWord"] = "Obtener"; + ["GetRobuxPhrase"] = "Consigue Robux"; + ["AlreadyOwnedPhrase"] = "Ya lo tienes."; + ["PurchasedThisPhrase"] = "Has comprado esto por %s Robux."; + ["RobuxBalanceTitle"] = "Mi saldo"; + ["RobuxBalanceOverlayTitle"] = "Saldo de Robux"; + ["RobuxBalanceOverlayPhrase"] = "En Xbox solo se pueden utilizar Robux comprados en la Tienda Xbox."; + ["PlatformBalanceTitle"] = "Disponible en Xbox:"; + ["TotalBalanceTitle"] = "Saldo total:"; + ["AvatarCatalogTitle"] = "Catálogo"; + ["AvatarOutfitsTitle"] = "Mi colección"; + ["PurchasingTitle"] = "Comprando..."; + ["ConfirmPurchaseTitle"] = "Confirmar compra"; + ["AreYouSurePhrase"] = "¿Seguro que quieres comprar %s?"; + ["AreYouSureTakePhrase"] = "¿Seguro que quieres obtener %s?"; + ["AreYouSureWithPricePhrase"] = "¿Seguro que quieres comprar %s por %s?"; + ["RemainingBalancePhrase"] = "Tu saldo restante será de %s Robux."; + ["ConfirmWord"] = "Confirmar"; + ["DeclineWord"] = "Rechazar"; + ["OnlineWord"] = "Conectado"; + ["OfflineWord"] = "Sin conexión"; + ["SubmitWord"] = "Enviar"; + ["CancelWord"] = "Cancelar"; + ["ReportPhrase"] = "Puedes enviar una denuncia a nuestro equipo de moderadores. Revisaremos el juego y tomaremos las medidas adecuadas."; + ["CurrencySymbol"] = "$"; + ["RobuxStoreDescription"] = "Consigue Robux para comprar geniales aspectos nuevos para tu avatar además de ventajas y habilidades para juegos."; + ["RobuxStoreNoItemsPhrase"] = "En este momento no hay objetos en venta. Inténtalo de nuevo más tarde."; + ["PercentMoreRobuxPhrase"] = "%s%% más"; + ["RobuxStoreError"] = "No se puede acceder a los objetos Robux en este momento."; + ["NoFriendsPhrase"] = "Tus amigos no están conectados. ¡Entra en algunos juegos y haz nuevos amigos!"; + ["PopupPartyUIErrorPhrase"] = "Se ha producido un error al intentar formar un grupo. Inténtalo de nuevo."; + ["AuthenticationErrorTitle"] = "Error de autenticación"; + ["AuthInProgressPhrase"] = "La autenticación ya está en curso."; + ["AuthAccountUnlinkedPhrase"] = "Este es un caso especial gestionado por EngagementScreen"; + ["AuthMissingGamePadPhrase"] = "No se ha detectado ningún mando. Por favor, activa un mando."; + ["AuthNoUserDetectedPhrase"] = "No se ha detectado ningún usuario de Xbox Live. Por favor, inicia sesión con una cuenta Xbox Live."; + ["AuthHttpErrorDetected"] = "Hay problemas para comunicarse con los servidores de Roblox. Visita www.roblox.com/help/xbox para ver más información."; + ["AuthErrorPhrase"] = "Hay problemas para comunicarse con los servidores de Roblox. Inténtalo de nuevo."; + ["NewUserPhrase"] = "Juegos creados por la comunidad. Posibilidades ilimitadas."; + ["SignInPhrase"] = "Iniciar sesión"; + ["PlayAsPhrase"] = "Registrarse como %s"; + ["UsernameWord"] = "Usuario"; + ["PasswordWord"] = "Contraseña"; + ["UsernameRulePhrase"] = "De 3 a 20 caracteres, sin espacios"; + ["PasswordRulePhrase"] = "Al menos 6 letras y 2 números"; + ["AccountSettingsTitle"] = "Configuración de cuenta"; + ["InvalidUsernameTitle"] = "Nombre de usuario no válido"; + ["InvalidPasswordTitle"] = "Contraseña no válida"; + ["InvalidUsernamePhrase"] = "Los nombres de usuario deben tener entre 3 y 20 caracteres."; + ["InvalidPasswordPhrase"] = "Las contraseñas deben tener al menos 6 letras y 2 números."; + ["AlreadyTakenTitle"] = "Nombre de usuario ocupado"; + ["AlreadyTakenPhrase"] = "Ese nombre de usuario ya está en uso. Inténtalo con otro."; + ["InvalidCharactersUsedPhrase"] = "El nombre de usuario que has introducido contiene caracteres ilegales. Solo se permiten letras y números."; + ["UsernameCannotContainSpacesPhrase"] = "El nombre de usuario que has introducido contiene espacios. Solo se permiten letras y números."; + ["NoUsernameEnteredPhrase"] = "Es necesario un nombre de usuario."; + ["NoUsernameOrPasswordEnteredPhrase"] = "Debes introducir un nombre de usuario y una contraseña válidos para configurar tu cuenta de Roblox."; + ["LinkedAsPhrase"] = "%s está vinculada con tu cuenta de Roblox %s."; + ["LinkSignUpDisabled"] = "El registro está desactivado en este momento. Inténtalo de nuevo más tarde."; + ["LinkFlooded"] = "Hoy te has registrado demasiadas veces. Inicia sesión con una cuenta de Roblox existente o inténtalo de nuevo más tarde."; + ["LinkLeaseLocked"] = "Transacción en curso. Espera, por favor."; + ["LinkAccountLinkingDisabled"] = "El vínculo de cuentas está desactivado en este momento. Inténtalo de nuevo más tarde."; + ["LinkInvalidRobloxUser"] = "El nombre de usuario de Roblox que has introducido no es válido. Introduce un nombre de usuario de Roblox válido."; + ["LinkRobloxUserAlreadyLinked"] = "Tu cuenta de Xbox Live ya está vinculada con una cuenta de Roblox."; + ["LinkXboxUserAlreadyLinked"] = "Tu cuenta de Xbox Live ya está vinculada con una cuenta de Roblox."; + ["LinkIllegalChildAccountLinking"] = "No se han podido vincular las cuentas. Comprueba tu configuración de edad de Xbox."; + ["LinkInvalidPassword"] = "La contraseña que has introducido no es válida. Introduce la contraseña correcta."; + ["LinkUsernamePasswordNotSet"] = "No has introducido un nombre de usuario o una contraseña. Introduce un nombre de usuario y una contraseña e inténtalo de nuevo."; + ["LinkUsernameAlreadyTaken"] = "Ese nombre de usuario ya está en uso. Elige otro nombre de usuario e inténtalo de nuevo."; + ["LinkInvalidCredentials"] = "El nombre de usuario o la contraseña no son válidos. Introduce un nombre de usuario y una contraseña válidos e inténtalo de nuevo."; + ["UserIsGuestAccount"] = "Las cuentas de invitado no pueden autenticarse. Inicia sesión con una cuenta que no sea de invitado."; + ["LinkUnknownError"] = "Se ha producido un error desconocido. Inténtalo de nuevo."; + ["LinkAccountTitle"] = "Iniciar sesión en Roblox"; + ["LinkAccountPhrase"] = "Inicia sesión con una cuenta existente de Roblox para acceder a la apariencia de tu avatar y guardar tu progreso de juego.\n\n¡Tu cuenta de Roblox se vinculará con tu cuenta de Xbox Live y la próxima vez que juegues iniciarás sesión automáticamente!\n\nPuedes desvincularla en cualquier momento desde la pantalla de Configuración."; + ["SignUpWord"] = "Registro"; + ["SignUpTitle"] = "Crear una cuenta de Roblox"; + ["SignUpPhrase"] = "Una cuenta de Roblox te da acceso a Roblox en cualquier plataforma. ¡Solo introduce un usuario y una contraseña y podrás iniciar sesión donde quieras y seguir donde lo dejaste!\n\n¡Tu nueva cuenta de Roblox se vinculará con tu cuenta de Xbox Live y la próxima vez que juegues iniciarás sesión automáticamente!\n\nPuedes desvincularla en cualquier momento desde la pantalla de Configuración."; + ["SetCredentialsTitle"] = "Asignar nombre de usuario y contraseña"; + ["SetCredentialsPhrase"] = "¡Parece que te interrumpieron antes de que pudieras finalizar el registro! No te preocupes, puedes terminar de configurar tu cuenta de Roblox eligiendo un nombre de usuario y una contraseña aquí.\n\n¡Tu nueva cuenta de Roblox se vinculará con tu cuenta de Xbox Live y la próxima vez que juegues iniciarás sesión automáticamente!\n\nPuedes desvincularla en cualquier momento desde la pantalla de Configuración."; + ["SetCredentialsWord"] = "Asignar"; + ["UnlinkTitle"] = "Desvincular cuenta"; + ["UnlinkGamerTagPhrase"] = "Desvincular %s"; + ["UnlinkPhrase"] = "¿Seguro que quieres desvincular esta cuenta de Roblox de tu cuenta de Xbox? Tus datos guardados y tus compras están asociadas con esta cuenta de Roblox y no tendrás acceso a ellos hasta que vuelvas a iniciar sesión con ella."; + ["GameplaySettingsTitle"] = "Opciones de juego"; + ["EnabledWord"] = "Activado (recomendado)"; + ["DisabledWord"] = "Desactivado"; + ["LoadingWord"] = "Cargando..."; + ["KeepEnabledPhrase"] = "Mantener activado"; + ["DisableWord"] = "Desactivar"; + ["CrossPlatformGameplayPhrase"] = "Juego multiplataforma"; + ["DisableCrossplayOverlayTitle"] = "¿Desactivar el juego multiplataforma?"; + ["DisableCrossplayOverlayMessage"] = "¿Seguro que quieres desactivar el juego multiplataforma?\nNo se recomienda esta opción. Ya no te unirás a juegos con jugadores de otras plataformas y verás a menos jugadores en los juegos."; + ["EnableCrossplayOverlayTitle"] = "¡El juego multiplataforma está activado!"; + ["EnableCrossplayOverlayMessage"] = "Ahora puedes jugar a Roblox en Xbox One con jugadores de cualquier plataforma, incluidas ordenador, móvil y tableta. ¡De esta manera verás a más jugadores en más juegos y podrás jugar con TODOS tus amigos, sin importar en qué dispositivo jueguen!"; + ["CrossplayEnabledDescription"] = "Te unirás a juegos con jugadores de todas las plataformas y verás a más jugadores en los juegos."; + ["CrossplayDisabledDescription"] = "No se recomienda esta opción. No te unirás a juegos con jugadores de otras plataformas y verás a menos jugadores en los juegos."; + ["UnableToJoinTitle"] = "No has podido unirte"; + ["ErrorOccurredTitle"] = "Se ha producido un error"; + ["DefaultErrorPhrase"] = "No se ha podido conectar con Roblox. Inténtalo de nuevo más tarde."; + ["DefaultJoinFailPhrase"] = "No se ha podido conectar con Roblox. Inténtalo de nuevo más tarde."; + ["AlreadyRunningPhrase"] = "Ya estás en este juego de Roblox."; + ["WebServerConnectFailPhrase"] = "No se ha podido conectar con los servidores de Roblox. Inténtalo de nuevo más tarde."; + ["AccessDeniedByWeb"] = "El juego de Roblox al que intentas unirte no está disponible en este momento."; + ["InstanceNotFound"] = "El juego de Roblox al que intentas unirte no está disponible en este momento."; + ["GameFullPhrase"] = "El juego de Roblox al que intentas unirte está lleno en este momento."; + ["FollowUserFailed"] = "El juego de Roblox al que intentas unirte no está disponible en este momento."; + ["InvalidPrivilegeMultiplayerSessionPhrase"] = "Tu cuenta de Xbox no tiene permitido jugar en sesiones multijugador en este momento."; + ["InvalidPrivilegeUGCPhrase"] = "Tu cuenta de Xbox no tiene permitido jugar con contenidos generados por usuarios en este momento."; + ["UnableToEquipTitle"] = "No se ha podido seleccionar"; + ["UnableToEquipPhrase"] = "No hemos podido seleccionar ese avatar. Inténtalo de nuevo más tarde."; + ["UnableToWearOufitTitle"] = "No se ha podido seleccionar"; + ["UnableToWearOufitPhrase"] = "No hemos podido seleccionar esa vestimenta. Inténtalo de nuevo más tarde."; + ["UnableToDoPurchaseTitle"] = "No se ha podido completar la compra"; + ["UnableToDoPurchasePhrase"] = "No hemos podido completar tu compra. Inténtalo de nuevo más tarde."; + ["UnableToDoRobuxPurchaseTitle"] = "No se ha podido completar la compra"; + ["UnableToDoRobuxPurchasePhrase"] = "No hemos podido completar tu compra. Inténtalo de nuevo más tarde."; + ["CannotVoteTitle"] = "No puedes votar"; + ["VoteFloodPhrase"] = "Estás votando demasiado a menudo. Vuelve más tarde e inténtalo de nuevo."; + ["VotePlayGamePhrase"] = "Debes jugar a este juego antes de votar."; + ["CannotFavoriteTitle"] = "No se ha podido añadir este juego a favoritos"; + ["FavoriteFloodPhrase"] = "Estás añadiendo juegos a favoritos demasiado a menudo. Vuelve más tarde e inténtalo de nuevo."; + ["ToSPhrase"] = "Ver términos"; + ["ToSInfoLinkPhrase"] = "Términos y política de privacidad:\nwww.roblox.com/info/termsofservice\nwww.roblox.com/info/privacy"; + ["PrivacyPhrase"] = "Privacidad"; + ["VersionIdString"] = "Versión: %s.%s.%s.%s"; + ["ErrorMessageAndCodePrase"] = "%s\nCódigo de error: %d"; + ["UnlockGamesPhrase"] = "¡Juega a 5 juegos promocionados para desbloquear más creaciones hechas por miembros de la comunidad como tú!"; + ["AlertOccurredTitle"] = "Se ha producido una alerta"; + ["DefaultAlertPhrase"] = "No se ha podido conectar con Roblox. Inténtalo de nuevo más tarde."; + ["UnlockedUGCTitle"] = "¡Se han desbloqueado todos los juegos!"; + ["UnlockedUGCPhrase"] = "Ahora que has jugado a 5 juegos promocionados, ¡puedes acceder a la selección completa de juegos creados por miembros de la comunidad como tú!\n\n¡Ve a la pantalla de JUEGOS para ver todos los juegos nuevos que has desbloqueado!"; + ["PlatformLinkInfoTitle"] = "¡Te damos la bienvenida a Roblox!"; + ["PlatformLinkInfoMessage"] = "Hemos creado una cuenta de Roblox nueva para ti. Todos tus progresos del juego y tus compras se guardarán en esta cuenta de Roblox. ¡Ahora puedes iniciar sesión en cualquier plataforma y seguir desde donde lo dejaste!"; + ["ControllerLostConnectionTitle"] = "Falta un mando"; + ["ControllerLostConnectionPhrase"] = "El mando del usuario \"%s\" no está conectado. Pulsa \"A\" en el mando con el que quieres continuar."; + ["ActiveUserLostConnectionTitle"] = "Se ha eliminado el usuario activo"; + ["ActiveUserLostConnectionPhrase"] = "El usuario \"%s\" se ha desconectado. Pulsa \"A\" en el mando con el que quieres continuar."; + ["PlayMyPlaceMoreGamesTitle"] = "Mis juegos"; + ["PlayMyPlaceMoreGamesPhrase"] = "¡Inicia sesión en Roblox.com para crear más juegos y editar tus creaciones existentes!"; + ["PrivateSessionPhrase"] = "En juego privado"; + ["ReauthSignedOutTitle"] = "Te has desconectado"; + ["ReauthSignedOutPhrase"] = "Te has desconectado de tu cuenta de Xbox Live. Inicia sesión para continuar."; + ["ReauthRemovedTitle"] = "Cambio de usuario"; + ["ReauthRemovedPhrase"] = "Hemos detectado un cambio del usuario activo. Inicia sesión de nuevo."; + ["ReauthInvalidSessionPhrase"] = "Te has desconectado de todas las sesiones de Roblox en curso."; + ["ReauthUnlinkTitle"] = "Se ha desvinculado la cuenta"; + ["ReauthUnlinkPhrase"] = "Has desvinculado tu cuenta de Roblox correctamente. Ahora puedes iniciar sesión de nuevo o registrar una cuenta de Roblox nueva."; + ["ReauthUnknownPhrase"] = "Se ha producido un error y te has desconectado. Inicia sesión de nuevo para seguir jugando."; + ["AccountUnder13Phrase"] = "Cuenta: Menor de 13 años"; + ["AccountOver13Phrase"] = "Cuenta: Mayor de 13 años"; + ["CPPWelcomeTitle"] = "¡Te damos la bienvenida al juego multiplataforma!"; + ["CPPWelcomePhrase"] = "Ahora puedes jugar a Roblox en Xbox One con jugadores de cualquier plataforma, incluidas ordenador, móvil y tableta. ¡De esta manera verás a más jugadores en más juegos y podrás jugar con TODOS tus amigos, sin importar en qué dispositivo jueguen! Puedes actualizar las opciones de juego en la configuración de cuenta."; + ["MPSRestrictedPhrase"] = "La configuración de tu cuenta de Xbox te impide jugar en sesiones multijugador. Puedes cambiar esta opción desde la configuración de Xbox o en Xbox.com."; + ["MPSBannedPhrase"] = "Tu cuenta de Xbox tiene prohibido jugar en sesiones multijugador en este momento."; + ["MPSPurchaseRequiredPhrase"] = "Debes tener una suscripción a Xbox Live Gold para jugar en sesiones multijugador en línea."; + ["UGCRestrictedPhrase"] = "La configuración de tu cuenta de Xbox te impide jugar con contenidos generados por usuarios. Puedes cambiar esta opción desde la configuración de Xbox o en Xbox.com."; + ["UGCBannedPhrase"] = "Tu cuenta de Xbox tiene prohibido jugar con contenidos generados por usuarios en este momento."; + ["PrivilegeCheckFailPhrase"] = "Tu cuenta de Xbox no tiene permitido jugar en este momento. Inténtalo de nuevo más tarde."; +} + +return locale diff --git a/Client2018/content/internal/AppShellLocalization/Modules/fr.lua b/Client2018/content/internal/AppShellLocalization/Modules/fr.lua new file mode 100644 index 0000000..2703331 --- /dev/null +++ b/Client2018/content/internal/AppShellLocalization/Modules/fr.lua @@ -0,0 +1,230 @@ +local locale = +{ + ["HomeWord"] = "Accueil"; + ["GamesWord"] = "Jeux"; + ["FriendsWord"] = "Amis"; + ["CatalogWord"] = "Robux"; + ["AvatarWord"] = "Avatar"; + ["PlayWord"] = "Jouer"; + ["FavoriteWord"] = "Mettre en Favori"; + ["FavoritedWord"] = "Mis en Favori"; + ["LikedWord"] = "Aimé"; + ["DislikedWord"] = "Pas aimé"; + ["BackWord"] = "Retour"; + ["SettingsWord"] = "Paramètres"; + ["AccountWord"] = "Compte"; + ["OverscanWord"] = "Ajuster écran"; + ["HelpWord"] = "Aide"; + ["SwitchProfileWord"] = "Changer de profil"; + ["RecenterButtonWord"] = "Recentrer"; + ["MoreWord"] = "Plus"; + ["SearchWord"] = "Rechercher"; + ["SearchGamesPhrase"] = "Rechercher des jeux"; + ["SearchingForPhrase"] = "Rechercher : %s"; + ["NoGamesPhrase"] = "Aucun jeu trouvé."; + ["EditAvatarPhrase"] = "Modifier avatar"; + ["FriendActivityWord"] = "Activité de vos amis"; + ["OnlineFriendsWords"] = "Amis en ligne"; + ["RecentlyPlayedWithSortTitle"] = "Récemment joué avec"; + ["StartPartyPhrase"] = "Ancrer application de groupe"; + ["RecommendedSortTitle"] = "Recommandé"; + ["FavoritesSortTitle"] = "Favoris"; + ["RecentlyPlayedSortTitle"] = "Joué récemment"; + ["MyRecentTitle"] = "Récents"; + ["TopEarningTitle"] = "Gain max"; + ["TopRatedTitle"] = "Score max"; + ["PopularTitle"] = "Populaire"; + ["FeaturedTitle"] = "En vedette"; + ["MoreGamesPhrase"] = "Plus de jeux"; + ["NoFriendsOnlinePhrase"] = "Vos amis ne sont pas connectés"; + ["JoinGameWord"] = "Rejoindre le jeu"; + ["ViewGameDetailsWord"] = "Voir les infos du jeu"; + ["InviteToPartyWord"] = "Inviter dans le groupe"; + ["ViewGamerCardWord"] = "Voir les infos du joueur"; + ["RatingDescriptionTitle"] = "Note et description"; + ["GameImagesTitle"] = "Images du jeu"; + ["GameBadgesTitle"] = "Badges du jeu"; + ["FriendActivityTitle"] = "Activité de vos amis"; + ["RelatedGamesTitle"] = "Jeux similaires"; + ["MoreDetailsTitle"] = "Plus de détails"; + ["GameBadgesTitle"] = "Badges du jeu"; + ["SelectYourAvatarTitle"] = "Sélectionnez votre avatar"; + ["LastUpdatedWord"] = "Dernière mise à jour"; + ["CreationDateWord"] = "Date de création"; + ["CreatedByWord"] = "Créé par"; + ["ReportGameWord"] = "Signaler le jeu"; + ["HaveBadgeWord"] = "Vous avez ce badge"; + ["MaxPlayersWord"] = "Joueurs max"; + ["ScreenSizeWord"] = "Taille d'écran"; + ["ResizeScreenPrompt"] = "Utilisez le stick analogique droit pour ajuster les bords de la case blanche jusqu'à ce qu'elle dépasse à peine de l'écran"; + ["ResizeScreenInputHint"] = "Redimensionner l'écran"; + ["AcceptWord"] = "Accepter"; + ["ResetWord"] = "Réinitialiser"; + ["CannotVoteWord"] = "Vous devez jouer à ce jeu avant de le noter"; + ["FirstToRateWord"] = "Soyez le premier à noter ce jeu"; + ["CustomAvatarPhrase"] = "Ce jeu utilise des avatars personnalisés"; + ["EngagementScreenHint"] = "Appuyez sur n'importe quelle touche pour commencer"; + ["EquipWord"] = "Changer d'avatar"; + ["BuyWord"] = "Acheter"; + ["OkWord"] = "Ok"; + ["FreeWord"] = "Gratuit"; + ["TakeWord"] = "Prendre"; + ["GetRobuxPhrase"] = "Prenez des Robux"; + ["AlreadyOwnedPhrase"] = "Vous possédez déjà ceci"; + ["PurchasedThisPhrase"] = "Vous avez acheté ceci %s Robux"; + ["RobuxBalanceTitle"] = "Mon solde"; + ["RobuxBalanceOverlayTitle"] = "Solde de Robux"; + ["RobuxBalanceOverlayPhrase"] = "Seuls les Robux achetés sur le Marché Xbox peuvent être utilisé sur Xbox."; + ["PlatformBalanceTitle"] = "Disponible sur Xbox :"; + ["TotalBalanceTitle"] = "Solde total :"; + ["AvatarCatalogTitle"] = "Catalogue"; + ["AvatarOutfitsTitle"] = "Ma collection"; + ["PurchasingTitle"] = "Achat en cours..."; + ["ConfirmPurchaseTitle"] = "Confirmer l'achat"; + ["AreYouSurePhrase"] = "Voulez-vous vraiment acheter : %s ?"; + ["AreYouSureTakePhrase"] = "Voulez-vous vraiment prendre : %s ?"; + ["AreYouSureWithPricePhrase"] = "Voulez-vous vraiment acheter : %s pour %s ?"; + ["RemainingBalancePhrase"] = "Votre solde sera de %s Robux ?"; + ["ConfirmWord"] = "Confirmer"; + ["DeclineWord"] = "Décliner"; + ["OnlineWord"] = "Connecté"; + ["OfflineWord"] = "Déconnecté"; + ["SubmitWord"] = "Soumettre"; + ["CancelWord"] = "Annuler"; + ["ReportPhrase"] = "Vous pouvez envoyer un rapport à notre équipe modération. Nous examinerons le jeu et prendront les mesures appropriées."; + ["CurrencySymbol"] = "$"; + ["RobuxStoreDescription"] = "Prenez des Robux pour acheter de super nouveaux looks pour votre avatar, ainsi que des avantages et des capacités pour les jeux."; + ["RobuxStoreNoItemsPhrase"] = "Il n'y a aucun objet à acheter pour l'instant, veuillez réessayer plus tard."; + ["PercentMoreRobuxPhrase"] = "%s%% en plus"; + ["RobuxStoreError"] = "Les objets Robux sont inaccessibles pour l'instant."; + ["NoFriendsPhrase"] = "Vos amis ne sont pas connectés. Jouez et faites-vous de nouveaux amis !"; + ["PopupPartyUIErrorPhrase"] = "Une erreur est survenue lors de la constitution du groupe. Veuillez réessayer."; + ["AuthenticationErrorTitle"] = "Erreur d'authentification"; + ["AuthInProgressPhrase"] = "Authentification déjà en cours"; + ["AuthAccountUnlinkedPhrase"] = "C'est un cas spécial géré par EngagementScreen"; + ["AuthMissingGamePadPhrase"] = "Aucune manette détectée. Veuillez allumer une manette."; + ["AuthNoUserDetectedPhrase"] = "Aucun utilisateur Xbox Live détecté. Veuillez vous connecter à un compte Xbox Live."; + ["AuthHttpErrorDetected"] = "Problèmes de communication avec les serveurs Roblox. Veuillez consulter www.roblox.com/help/xbox pour plus d'informations."; + ["AuthErrorPhrase"] = "Problèmes de communication avec les serveurs Roblox. Veuillez réessayer."; + ["NewUserPhrase"] = "Jeux créés par la communauté. Possibilités infinies."; + ["SignInPhrase"] = "Se connecter"; + ["PlayAsPhrase"] = "S'inscrire avec %s"; + ["UsernameWord"] = "Nom d'utilisateur"; + ["PasswordWord"] = "Mot de passe"; + ["UsernameRulePhrase"] = "3 à 20 caractères, aucun espace"; + ["PasswordRulePhrase"] = "6 lettres et 2 chiffres au minimum"; + ["AccountSettingsTitle"] = "Paramètres du compte"; + ["InvalidUsernameTitle"] = "Nom d'utilisateur invalide"; + ["InvalidPasswordTitle"] = "Mot de passe invalide"; + ["InvalidUsernamePhrase"] = "Les noms d'utilisateur doivent faire de 3 à 20 caractères"; + ["InvalidPasswordPhrase"] = "Les mots de passe doivent comprendre au moins 6 lettres et 2 chiffres"; + ["AlreadyTakenTitle"] = "Nom d'utilisateur déjà pris"; + ["AlreadyTakenPhrase"] = "Ce nom d'utilisateur est déjà pris, veuillez en essayer un autre."; + ["InvalidCharactersUsedPhrase"] = "Le nom d'utilisateur que vous avez saisi contient des caractères invalides. Seuls les lettres et les chiffres sont autorisés."; + ["UsernameCannotContainSpacesPhrase"] = "Le nom d'utilisateur que vous avez saisi contient des espaces. Seuls les lettres et les chiffres sont autorisés."; + ["NoUsernameEnteredPhrase"] = "Un nom d'utilisateur est requis."; + ["NoUsernameOrPasswordEnteredPhrase"] = "Vous devez saisir un nom d'utilisateur et un mot de passe valides pour créer votre compte Roblox."; + ["LinkedAsPhrase"] = "%s est actuellement lié à votre compte Roblox, %s."; + ["LinkSignUpDisabled"] = "L'inscription est actuellement désactivée. Veuillez réessayer plus tard."; + ["LinkFlooded"] = "Vous vous êtes inscrit trop de fois aujourd'hui. Veuillez vous connecter avec un compte Roblox existant ou réessayer plus tard."; + ["LinkLeaseLocked"] = "Transaction en cours. Veuillez patienter."; + ["LinkAccountLinkingDisabled"] = "Le lien de compte est actuellement désactivé. Veuillez réessayer plus tard."; + ["LinkInvalidRobloxUser"] = "Le nom d'utilisateur Roblox saisi est invalide. Veuillez saisir un nom d'utilisateur Roblox valide."; + ["LinkRobloxUserAlreadyLinked"] = "Votre compte Xbox Live est déjà lié à un compte Roblox"; + ["LinkXboxUserAlreadyLinked"] = "Votre compte Xbox Live est déjà lié à un compte Roblox."; + ["LinkIllegalChildAccountLinking"] = "Les comptes n'ont pas pu être liés. Veuillez revoir les paramètres d'âge de votre Xbox."; + ["LinkInvalidPassword"] = "Le mot de passe que vous avez saisi est invalide. Veuillez saisir le bon mot de passe."; + ["LinkUsernamePasswordNotSet"] = "Champ du nom d'utilisateur ou du mot de passe vide. Veuillez saisir un nom d'utilisateur et un mot de passe, puis réessayer."; + ["LinkUsernameAlreadyTaken"] = "Ce nom d'utilisateur est déjà pris. Veuillez choisir un autre nom d'utilisateur, puis réessayer."; + ["LinkInvalidCredentials"] = "Le nom d'utilisateur ou le mot de passe est invalide. Veuillez saisir un nom d'utilisateur et un mot de passe valide, puis réessayer."; + ["UserIsGuestAccount"] = "Les comptes Invité ne peuvent être authentifiés. Veuillez vous connecter à un compte non Invité."; + ["LinkUnknownError"] = "Une erreur inconnue est survenue. Veuillez réessayer."; + ["LinkAccountTitle"] = "Se connecter à Roblox"; + ["LinkAccountPhrase"] = "Connectez-vous avec un compte Roblox existant pour accéder à votre avatar et sauvegarder votre progression dans le jeu.\n\nVotre compte Roblox sera lié à votre compte Xbox Live, ainsi la prochaine fois que vous jouerez vous vous connecterez automatiquement !\n\nVous pouvez supprimer ce lien à tout moment depuis l'écran des paramètres."; + ["SignUpWord"] = "S'inscrire"; + ["SignUpTitle"] = "Créer un compte Roblox"; + ["SignUpPhrase"] = "Un compte Roblox vous permet d'accéder à Roblox depuis n'importe quelle plateforme. Créez juste un nom d'utilisateur et un mot de passe et vous pourrez vous connecter depuis n'importe où et reprendre là où vous vous étiez arrêté !\n\nVotre nouveau compte Roblox sera lié à votre compte Xbox Live, ainsi la prochaine fois que vous jouerez vous vous connecterez automatiquement !\n\nVous pouvez supprimer ce lien à tout moment depuis l'écran des paramètres."; + ["SetCredentialsTitle"] = "Assigner un nom d'utilisateur et un mot de passe"; + ["SetCredentialsPhrase"] = "Il semblerait que vous ayez été interrompu avant d'avoir terminé votre inscription ! Ne vous inquiétez pas, vous pouvez finir de créer votre compte Roblox en choisissant un nom d'utilisateur et un mot de passe ici.\n\nVotre nouveau compte Roblox sera lié à votre compte Xbox Live, ainsi la prochaine fois que vous jouerez vous vous connecterez automatiquement !\n\nVous pouvez supprimer ce lien à tout moment depuis l'écran des paramètres."; + ["SetCredentialsWord"] = "Assigner"; + ["UnlinkTitle"] = "Supprimer le lien du compte"; + ["UnlinkGamerTagPhrase"] = "Supprimer le lien de %s"; + ["UnlinkPhrase"] = "Voulez-vous vraiment supprimer le lien entre ce compte Roblox et votre compte Xbox ? Vos données sauvegardées et vos achats sont associés à ce compte Roblox et seront inaccessibles jusqu'à ce que vous vous reconnectiez !"; + ["GameplaySettingsTitle"] = "Options de jouabilité"; + ["EnabledWord"] = "Activé (Recommandé)"; + ["DisabledWord"] = "Désactivé"; + ["LoadingWord"] = "En cours de chargement..."; + ["KeepEnabledPhrase"] = "Garder activé"; + ["DisableWord"] = "Désactiver"; + ["CrossPlatformGameplayPhrase"] = "Jeu inter-plateforme"; + ["DisableCrossplayOverlayTitle"] = "Désactiver le jeu inter-plateforme ?"; + ["DisableCrossplayOverlayMessage"] = "Êtes vous sûr(e) de vouloir désactiver le jeu inter-plateforme ?\nCette option n'est pas recommandée. Vous ne serez plus capable de rejoindre les joueurs d'autres plateformes et verrez donc moins de joueurs en ligne."; + ["EnableCrossplayOverlayTitle"] = "Jeu inter-plateforme activé !"; + ["EnableCrossplayOverlayMessage"] = "Vous pouvez maintenant jouer à Roblox sur Xbox One avec des joueurs sur d'autres plateformes, comme sur PC, smartphone ou tablette ! Ceci signifie que vous verrez davantage de joueurs et de parties, vous permettant de jouer avec TOUS vos amis quel que soit l'appareil qu'ils utilisent !"; + ["CrossplayEnabledDescription"] = "Vous serez capable de rejoindre les joueurs d'autres plateformes et verrez donc plus de joueurs en ligne."; + ["CrossplayDisabledDescription"] = "Ceci n'est pas recommandé. Vous ne serez plus capable de rejoindre les joueurs d'autres plateformes et verrez donc moins de joueurs en ligne."; + ["UnableToJoinTitle"] = "Impossible de rejoindre"; + ["ErrorOccurredTitle"] = "Une erreur est survenue"; + ["DefaultErrorPhrase"] = "Impossible de se connecter à Roblox. Veuillez réessayer plus tard."; + ["DefaultJoinFailPhrase"] = "Impossible de se connecter à Roblox. Veuillez réessayer plus tard."; + ["AlreadyRunningPhrase"] = "Vous êtes déjà dans ce jeu Roblox."; + ["WebServerConnectFailPhrase"] = "Impossible de se connecter aux serveurs Roblox. Veuillez réessayer plus tard."; + ["AccessDeniedByWeb"] = "Le jeu Roblox que vous tentez de rejoindre est actuellement indisponible."; + ["InstanceNotFound"] = "Le jeu Roblox que vous tentez de rejoindre est actuellement indisponible."; + ["GameFullPhrase"] = "Le jeu Roblox que vous tentez de rejoindre est actuellement complet."; + ["FollowUserFailed"] = "Le jeu Roblox que vous tentez de rejoindre est actuellement indisponible."; + ["InvalidPrivilegeMultiplayerSessionPhrase"] = "Actuellement, votre compte Xbox n'est pas autorisé à jouer lors de sessions multijoueur."; + ["InvalidPrivilegeUGCPhrase"] = "Actuellement, votre compte Xbox n'est pas autorisé à jouer à du contenu généré par les utilisateurs."; + ["UnableToEquipTitle"] = "Sélection impossible"; + ["UnableToEquipPhrase"] = "Nous n'avons pas pu sélectionner cet avatar. Veuillez réessayer plus tard."; + ["UnableToWearOufitTitle"] = "Sélection impossible"; + ["UnableToWearOufitPhrase"] = "Nous n'avons pas pu sélectionner cette tenue. Veuillez réessayer plus tard."; + ["UnableToDoPurchaseTitle"] = "Achat impossible"; + ["UnableToDoPurchasePhrase"] = "Nous n'avons pas pu mener à bien cet achat. Veuillez réessayer plus tard."; + ["UnableToDoRobuxPurchaseTitle"] = "Achat impossible"; + ["UnableToDoRobuxPurchasePhrase"] = "Nous n'avons pas pu mener à bien cet achat. Veuillez réessayer plus tard."; + ["CannotVoteTitle"] = "Vote impossible"; + ["VoteFloodPhrase"] = "Vous votez trop souvent. Revenez plus tard et réessayez."; + ["VotePlayGamePhrase"] = "Vous devez jouer à ce jeu avant de voter."; + ["CannotFavoriteTitle"] = "Impossible de mettre ce jeu en Favori"; + ["FavoriteFloodPhrase"] = "Vous mettez trop souvent des jeux en Favori. Revenez plus tard et réessayez."; + ["ToSPhrase"] = "Voir les conditions..."; + ["ToSInfoLinkPhrase"] = "Conditions d'utilisation et politique de confidentialité :\nwww.roblox.com/info/termsofservice\nwww.roblox.com/info/privacy"; + ["PrivacyPhrase"] = "Confidentialité"; + ["VersionIdString"] = "Version : %s.%s.%s.%s"; + ["ErrorMessageAndCodePrase"] = "%s\nCode d'erreur : %d"; + ["UnlockGamesPhrase"] = "Jouez à 5 jeux en vedette pour déverrouiller plus de créations élaborées par des membres de la communauté, comme vous !"; + ["AlertOccurredTitle"] = "Une alerte est survenue"; + ["DefaultAlertPhrase"] = "Impossible de se connecter à Roblox. Veuillez réessayer plus tard."; + ["UnlockedUGCTitle"] = "Tous les jeux déverrouillés !"; + ["UnlockedUGCPhrase"] = "Maintenant que vous avez joué à 5 jeux en vedette, vous pouvez accéder à toute la sélection des jeux créés par les membres de la communauté, comme vous !\n\nAllez à l'écran des JEUX pour voir tous les nouveaux jeux que vous avez déverrouillés !"; + ["PlatformLinkInfoTitle"] = "Bienvenue à Roblox !"; + ["PlatformLinkInfoMessage"] = "Nous avons créé un nouveau compte Roblox pour vous. Toutes les progressions de jeux et les achats seront sauvegardés sur ce compte Roblox. Vous pouvez désormais vous connecter depuis n'importe quelle plateforme et reprendre là où vous aviez arrêté !"; + ["ControllerLostConnectionTitle"] = "Manette introuvable"; + ["ControllerLostConnectionPhrase"] = "La manette de l'utilisateur %s a été déconnectée, veuillez appuyer sur la touche A de la manette avec laquelle vous souhaitez continuer."; + ["ActiveUserLostConnectionTitle"] = "Utilisateur actif désactivé"; + ["ActiveUserLostConnectionPhrase"] = "L'utilisateur %s a été déconnecté, veuillez appuyer sur la touche A de la manette avec laquelle vous souhaitez continuer."; + ["PlayMyPlaceMoreGamesTitle"] = "Mes jeux"; + ["PlayMyPlaceMoreGamesPhrase"] = "Connectez-vous à Roblox.com pour créer plus de jeux et modifier vos créations !"; + ["PrivateSessionPhrase"] = "En jeu privé"; + ["ReauthSignedOutTitle"] = "Déconnecté"; + ["ReauthSignedOutPhrase"] = "Vous vous êtes déconnecté de votre compte Xbox Live. Veuillez vous connecter pour continuer."; + ["ReauthRemovedTitle"] = "Changement d'utilisateur"; + ["ReauthRemovedPhrase"] = "Nous avons détecté un changement de l'utilisateur actif. Veuillez vous reconnecter."; + ["ReauthInvalidSessionPhrase"] = "Vous avez été déconnecté de toutes les sessions actuelles de Roblox."; + ["ReauthUnlinkTitle"] = "Lien du compte supprimé"; + ["ReauthUnlinkPhrase"] = "Vous avez supprimé le lien de votre compte Roblox avec succès. Vous pouvez maintenant choisir de vous reconnecter ou de créer un nouveau compte Roblox en vous inscrivant."; + ["ReauthUnknownPhrase"] = "Une erreur est survenue et vous avez été déconnecté. Veuillez vous reconnecter pour continuer à jouer."; + ["AccountUnder13Phrase"] = "Compte : Moins de 13 ans"; + ["AccountOver13Phrase"] = "Compte : Plus de 13 ans"; + ["CPPWelcomeTitle"] = "Bienvenue sur le jeu inter-plateforme !"; + ["CPPWelcomePhrase"] = "Vous pouvez maintenant jouer à Roblox sur Xbox One avec des joueurs sur d'autres plateformes, comme sur PC, smartphone ou tablette ! Ceci signifie que vous verrez davantage de joueurs et de parties, vous permettant de jouer avec TOUS vos amis quel que soit l'appareil qu'ils utilisent ! Les options de jouabilité peuvent être mises à jour depuis les paramètres du compte."; + ["MPSRestrictedPhrase"] = "Les paramètres de votre compte Xbox vous empêchent de rejoindre des sessions multijoueur. Pour les modifier, rendez-vous dans les Paramètres de votre console, ou sur Xbox.com"; + ["MPSBannedPhrase"] = "Actuellement, votre compte Xbox a été banni et n'est pas autorisé à rejoindre des sessions multijoueur."; + ["MPSPurchaseRequiredPhrase"] = "Vous devez disposer du Xbox Live Gold afin de jouer en multijoueur en ligne."; + ["UGCRestrictedPhrase"] = "Les paramètres de votre compte Xbox vous empêchent de jouer au contenu créé par les utilisateurs. Pour les modifier, rendez-vous dans les Paramètres de votre console, ou sur Xbox.com"; + ["UGCBannedPhrase"] = "Actuellement, votre compte Xbox a été banni et n'est pas autorisé à jouer au contenu créé par les utilisateurs."; + ["PrivilegeCheckFailPhrase"] = "Actuellement, votre compte Xbox n'est pas autorisé à jouer. Veuillez réessayer ultérieurement."; +} + +return locale diff --git a/Client2018/content/internal/AppShellLocalization/Modules/it.lua b/Client2018/content/internal/AppShellLocalization/Modules/it.lua new file mode 100644 index 0000000..e3322a6 --- /dev/null +++ b/Client2018/content/internal/AppShellLocalization/Modules/it.lua @@ -0,0 +1,230 @@ +local locale = +{ + ["HomeWord"] = "Home"; + ["GamesWord"] = "Giochi"; + ["FriendsWord"] = "Amici"; + ["CatalogWord"] = "Robux"; + ["AvatarWord"] = "Avatar"; + ["PlayWord"] = "Gioca"; + ["FavoriteWord"] = "Preferito"; + ["FavoritedWord"] = "Preferiti"; + ["LikedWord"] = "Aggiunto agli amati"; + ["DislikedWord"] = "Aggiunto agli odiati"; + ["BackWord"] = "Indietro"; + ["SettingsWord"] = "Impostazioni"; + ["AccountWord"] = "Account"; + ["OverscanWord"] = "Calibra schermo"; + ["HelpWord"] = "Guida"; + ["SwitchProfileWord"] = "Cambia profilo"; + ["RecenterButtonWord"] = "Ricentra"; + ["MoreWord"] = "Altro"; + ["SearchWord"] = "Cerca"; + ["SearchGamesPhrase"] = "Cerca giochi"; + ["SearchingForPhrase"] = "Ricerca di: %s"; + ["NoGamesPhrase"] = "Giochi non trovati."; + ["EditAvatarPhrase"] = "Modifica avatar"; + ["FriendActivityWord"] = "Attività amici"; + ["OnlineFriendsWords"] = "Amici online"; + ["RecentlyPlayedWithSortTitle"] = "Giocato di recente con"; + ["StartPartyPhrase"] = "Affianca app Gruppo"; + ["RecommendedSortTitle"] = "Consigliati"; + ["FavoritesSortTitle"] = "Preferiti"; + ["RecentlyPlayedSortTitle"] = "Giocati di recente"; + ["MyRecentTitle"] = "I miei recenti"; + ["TopEarningTitle"] = "Più redditizi"; + ["TopRatedTitle"] = "Più votati"; + ["PopularTitle"] = "Popolari"; + ["FeaturedTitle"] = "In evidenza"; + ["MoreGamesPhrase"] = "Altri giochi"; + ["NoFriendsOnlinePhrase"] = "I tuoi amici non sono online"; + ["JoinGameWord"] = "Accedi al gioco"; + ["ViewGameDetailsWord"] = "Visualizza dettagli di gioco"; + ["InviteToPartyWord"] = "Invita nel gruppo"; + ["ViewGamerCardWord"] = "Vedi scheda giocatore"; + ["RatingDescriptionTitle"] = "Valutazione e descrizione"; + ["GameImagesTitle"] = "Immagini di gioco"; + ["GameBadgesTitle"] = "Contrassegni di gioco"; + ["FriendActivityTitle"] = "Attività amici"; + ["RelatedGamesTitle"] = "Giochi relativi"; + ["MoreDetailsTitle"] = "Altre informazioni"; + ["GameBadgesTitle"] = "Contrassegni di gioco"; + ["SelectYourAvatarTitle"] = "Seleziona il tuo avatar"; + ["LastUpdatedWord"] = "Ultimo aggiornamento"; + ["CreationDateWord"] = "Data di creazione"; + ["CreatedByWord"] = "Creato da"; + ["ReportGameWord"] = "Segnala gioco"; + ["HaveBadgeWord"] = "Hai questo contrassegno"; + ["MaxPlayersWord"] = "Giocatori max"; + ["ScreenSizeWord"] = "Dimensioni schermo"; + ["ResizeScreenPrompt"] = "Usa la levetta destra per spostare i bordi del rettangolo bianco appena oltre quelli dello schermo"; + ["ResizeScreenInputHint"] = "Ridimensiona schermo"; + ["AcceptWord"] = "Accetta"; + ["ResetWord"] = "Azzera"; + ["CannotVoteWord"] = "Per poter votare devi prima giocare"; + ["FirstToRateWord"] = "Valuta per primo questo gioco"; + ["CustomAvatarPhrase"] = "Questo gioco usa avatar personalizzati"; + ["EngagementScreenHint"] = "Premi un pulsante per iniziare"; + ["EquipWord"] = "Cambia avatar"; + ["BuyWord"] = "Compra"; + ["OkWord"] = "OK"; + ["FreeWord"] = "Gratis"; + ["TakeWord"] = "Prendi"; + ["GetRobuxPhrase"] = "Ottieni Robux"; + ["AlreadyOwnedPhrase"] = "Ce l'hai già"; + ["PurchasedThisPhrase"] = "Acquistato per %s Robux"; + ["RobuxBalanceTitle"] = "I miei fondi"; + ["RobuxBalanceOverlayTitle"] = "Fondi in Robux"; + ["RobuxBalanceOverlayPhrase"] = "Su Xbox, possono essere usati solo Robux acquistati nell'Xbox Store."; + ["PlatformBalanceTitle"] = "Disponibili su Xbox:"; + ["TotalBalanceTitle"] = "Fondi totali:"; + ["AvatarCatalogTitle"] = "Catalogo"; + ["AvatarOutfitsTitle"] = "La mia collezione"; + ["PurchasingTitle"] = "Acquisto..."; + ["ConfirmPurchaseTitle"] = "Conferma acquisto"; + ["AreYouSurePhrase"] = "Vuoi davvero comprare %s?"; + ["AreYouSureTakePhrase"] = "Vuoi davvero prendere %s?"; + ["AreYouSureWithPricePhrase"] = "Vuoi davvero comprare %s per %s?"; + ["RemainingBalancePhrase"] = "Ti rimarranno %s Robux"; + ["ConfirmWord"] = "Conferma"; + ["DeclineWord"] = "Declina"; + ["OnlineWord"] = "Online"; + ["OfflineWord"] = "Offline"; + ["SubmitWord"] = "Invia"; + ["CancelWord"] = "Annulla"; + ["ReportPhrase"] = "Puoi inviare una segnalazione ai nostri moderatori. Verificheremo il gioco e prenderemo i provvedimenti opportuni."; + ["CurrencySymbol"] = "$"; + ["RobuxStoreDescription"] = "Ottieni Robux per comprare nuovi look per il tuo avatar, oltre a vantaggi ed abilità nei giochi."; + ["RobuxStoreNoItemsPhrase"] = "Non ci sono oggetti da acquistare in questo momento. Riprova più tardi."; + ["PercentMoreRobuxPhrase"] = "%s%% in più"; + ["RobuxStoreError"] = "Gli oggetti Robux non sono accessibili in questo momento."; + ["NoFriendsPhrase"] = "I tuoi amici non sono online. Gioca e stringi nuove amicizie!"; + ["PopupPartyUIErrorPhrase"] = "Si è verificato un errore nel tentativo di iniziare un gruppo. Riprova più tardi."; + ["AuthenticationErrorTitle"] = "Errore di autenticazione"; + ["AuthInProgressPhrase"] = "Autenticazione già in corso"; + ["AuthAccountUnlinkedPhrase"] = "Questo è un caso speciale gestito da EngagementScreen"; + ["AuthMissingGamePadPhrase"] = "Nessun controller individuato. Accendi un controller."; + ["AuthNoUserDetectedPhrase"] = "Nessun utente Xbox Live individuato. Accedi a un account Xbox Live."; + ["AuthHttpErrorDetected"] = "Problemi di comunicazione con i server Roblox. Consulta il sito www.roblox.com/help/xbox per maggiori informazioni."; + ["AuthErrorPhrase"] = "Problemi di comunicazione con i server Roblox. Riprova più tardi."; + ["NewUserPhrase"] = "Giochi creati dalla comunità. Infinite possibilità."; + ["SignInPhrase"] = "Accedi"; + ["PlayAsPhrase"] = "Registrati usando %s"; + ["UsernameWord"] = "Nome utente"; + ["PasswordWord"] = "Password"; + ["UsernameRulePhrase"] = "3-20 caratteri, senza spazi"; + ["PasswordRulePhrase"] = "minimo 6 lettere e 2 numeri"; + ["AccountSettingsTitle"] = "Impostazioni account"; + ["InvalidUsernameTitle"] = "Nome utente non valido"; + ["InvalidPasswordTitle"] = "Password non valida"; + ["InvalidUsernamePhrase"] = "I nomi utenti devono avere 320 caratteri."; + ["InvalidPasswordPhrase"] = "Le password devono avere almeno 6 lettere e 2 numeri"; + ["AlreadyTakenTitle"] = "Nome utente non disponibile"; + ["AlreadyTakenPhrase"] = "Questo nome utente è già in uso. Provane un altro."; + ["InvalidCharactersUsedPhrase"] = "Il nome utente immesso contiene caratteri non validi. Sono permessi solo lettere e numeri."; + ["UsernameCannotContainSpacesPhrase"] = "Il nome utente immesso contiene spazi. Sono permessi solo lettere e numeri."; + ["NoUsernameEnteredPhrase"] = "Serve un nome utente."; + ["NoUsernameOrPasswordEnteredPhrase"] = "Devi immettere un nome utente e una password validi per impostare il tuo account Roblox."; + ["LinkedAsPhrase"] = "%s è attualmente collegato al tuo account Roblox, %s."; + ["LinkSignUpDisabled"] = "La registrazione è attualmente disattivata. Riprova più tardi."; + ["LinkFlooded"] = "Oggi ti sei registrato troppe volte. Accedi con un account Roblox esistente o riprova più tardi."; + ["LinkLeaseLocked"] = "Transazione in corso. Attendi."; + ["LinkAccountLinkingDisabled"] = "Il collegamento di account è attualmente disattivato. Riprova più tardi."; + ["LinkInvalidRobloxUser"] = "Il nome utente Roblox che hai immesso non è valido. Immetti un nome utente Roblox valido."; + ["LinkRobloxUserAlreadyLinked"] = "Il tuo account Xbox Live è già collegato a un account Roblox"; + ["LinkXboxUserAlreadyLinked"] = "Il tuo account Xbox Live è già collegato a un account Roblox."; + ["LinkIllegalChildAccountLinking"] = "Gli account non possono essere collegati. Verifica le tue impostazioni di età di Xbox."; + ["LinkInvalidPassword"] = "La password che hai immesso non è valida. Immetti la password corretta."; + ["LinkUsernamePasswordNotSet"] = "Nome utente o password sono vuoti. Immetti un nome utente e una password e riprova."; + ["LinkUsernameAlreadyTaken"] = "Questo nome utente è già in uso. Scegline un altro e riprova."; + ["LinkInvalidCredentials"] = "Il nome utente o la password non sono validi. Immetti un nome utente e una password validi e riprova."; + ["UserIsGuestAccount"] = "Gli account ospite non possono essere autenticati. Accedi a un account che non sia ospite."; + ["LinkUnknownError"] = "Si è verificato un errore imprevisto. Riprova più tardi."; + ["LinkAccountTitle"] = "Accedi a Roblox"; + ["LinkAccountPhrase"] = "Accedi con un account Roblox esistente per avere accesso al tuo avatar e salvare i progressi di gioco.\n\nIl tuo account Roblox sarà collegato al tuo account Xbox Live e, la prossima volta che giocherai, effettuerai l'accesso automaticamente!\n\nPuoi scollegare i due account dalla schermata delle Impostazioni in qualsiasi momento."; + ["SignUpWord"] = "Registrati"; + ["SignUpTitle"] = "Crea un account Roblox"; + ["SignUpPhrase"] = "L'account Roblox è il tuo accesso a Roblox su qualsiasi piattaforma. Devi solo impostare un nome utente e una password per poter accedere da dove vuoi e continuare da dove avevi interrotto!\n\nIl tuo nuovo account Roblox sarà collegato all'account Xbox Live e, la prossima volta che giocherai, effettuerai l'accesso automaticamente!\n\nPuoi scollegare i due account dalla schermata delle Impostazioni in qualsiasi momento."; + ["SetCredentialsTitle"] = "Assegna nome utente e password"; + ["SetCredentialsPhrase"] = "Sembra che tu sia stato interrotto prima di poter completare la registrazione! Nessun problema: puoi finire di impostare il tuo account Roblox scegliendo un nome utente e una password qui.\n\nIl tuo account Roblox sarà collegato all'account Xbox Live e, la prossima volta che giocherai, effettuerai l'accesso automaticamente!\n\nPuoi scollegare i due account dalla schermata delle Impostazioni in qualsiasi momento."; + ["SetCredentialsWord"] = "Assegna"; + ["UnlinkTitle"] = "Scollega account"; + ["UnlinkGamerTagPhrase"] = "Scollega %s"; + ["UnlinkPhrase"] = "Vuoi davvero scollegare questo account Roblox e il tuo account Xbox? I tuoi salvataggi e i tuoi acquisti sono associati a questo account Roblox e saranno accessibili solo quando avrai effettuato l'accesso!"; + ["GameplaySettingsTitle"] = "Opzioni di gioco"; + ["EnabledWord"] = "Attivato (Consigliata)"; + ["DisabledWord"] = "Disattivato"; + ["LoadingWord"] = "Caricamento..."; + ["KeepEnabledPhrase"] = "Mantieni attivato"; + ["DisableWord"] = "Disattiva"; + ["CrossPlatformGameplayPhrase"] = "Gioco multipiattaforma"; + ["DisableCrossplayOverlayTitle"] = "Disattivare il gioco multipiattaforma?"; + ["DisableCrossplayOverlayMessage"] = "Vuoi davvero disattivare il gioco multipiattaforma?\nNon è l'opzione consigliata. Non potrai più unirti a partite di giocatori che usano altre piattaforme, vedrai meno giocatori e meno partite."; + ["EnableCrossplayOverlayTitle"] = "Il gioco multipiattaforma è attivato!"; + ["EnableCrossplayOverlayMessage"] = "Puoi giocare a Roblox su Xbox One insieme a giocatori di tutte le piattaforme, tra cui PC, telefono e tablet! Ciò significa che vedrai sia più giocatori che più partite e potrai giocare con TUTTI i tuoi amici, a prescindere dal dispositivo utilizzato!"; + ["CrossplayEnabledDescription"] = "Potrai accedere a partite insieme a coloro che usano le altre piattaforme, vedrai più giocatori e più partite."; + ["CrossplayDisabledDescription"] = "Questa opzione non è consigliata. Non potrai unirti a partite di giocatori che usano altre piattaforme, vedrai meno giocatori e meno partite."; + ["UnableToJoinTitle"] = "Impossibile partecipare"; + ["ErrorOccurredTitle"] = "Si è verificato un errore"; + ["DefaultErrorPhrase"] = "Impossibile connettersi a Roblox. Riprova più tardi."; + ["DefaultJoinFailPhrase"] = "Impossibile connettersi a Roblox. Riprova più tardi."; + ["AlreadyRunningPhrase"] = "Sei già in questo gioco Roblox."; + ["WebServerConnectFailPhrase"] = "Impossibile connettersi ai server Roblox. Riprova più tardi."; + ["AccessDeniedByWeb"] = "Il gioco Roblox a cui stai cercando di partecipare non è attualmente disponibile."; + ["InstanceNotFound"] = "Il gioco Roblox a cui stai cercando di partecipare non è attualmente disponibile."; + ["GameFullPhrase"] = "Il gioco Roblox a cui stai cercando di partecipare è attualmente al completo."; + ["FollowUserFailed"] = "Il gioco Roblox a cui stai cercando di partecipare non è attualmente disponibile."; + ["InvalidPrivilegeMultiplayerSessionPhrase"] = "Il tuo account Xbox non può attualmente giocare a sessioni multigiocatore."; + ["InvalidPrivilegeUGCPhrase"] = "Il tuo account Xbox non può attualmente giocare a contenuto generato dagli utenti."; + ["UnableToEquipTitle"] = "Impossibile selezionare"; + ["UnableToEquipPhrase"] = "Non è stato possibile selezionare quell'avatar. Riprova più tardi."; + ["UnableToWearOufitTitle"] = "Impossibile selezionare"; + ["UnableToWearOufitPhrase"] = "Non è stato possibile selezionare quel completo. Riprova più tardi."; + ["UnableToDoPurchaseTitle"] = "Impossibile completare l'acquisto"; + ["UnableToDoPurchasePhrase"] = "Non è stato possibile completare il tuo acquisto. Riprova più tardi."; + ["UnableToDoRobuxPurchaseTitle"] = "Impossibile completare l'acquisto"; + ["UnableToDoRobuxPurchasePhrase"] = "Non è stato possibile completare il tuo acquisto. Riprova più tardi."; + ["CannotVoteTitle"] = "Impossibile votare"; + ["VoteFloodPhrase"] = "Stai votando troppo spesso. Torna più tardi e riprova."; + ["VotePlayGamePhrase"] = "Per poter votare devi prima giocare."; + ["CannotFavoriteTitle"] = "Impossibile aggiungere ai preferiti"; + ["FavoriteFloodPhrase"] = "Stai aggiungendo giochi ai preferiti troppo spesso. Torna più tardi e riprova."; + ["ToSPhrase"] = "Vedi i termini..."; + ["ToSInfoLinkPhrase"] = "Termini e informativa sulla privacy:\nwww.roblox.com/info/terms-of-service\nwww.roblox.com/info/privacy"; + ["PrivacyPhrase"] = "Privacy"; + ["VersionIdString"] = "Versione: %s.%s.%s.%s"; + ["ErrorMessageAndCodePrase"] = "%s\nCodice di errore: %d"; + ["UnlockGamesPhrase"] = "Prova 5 giochi in evidenza per sbloccare altre creazioni di membri della community come te!"; + ["AlertOccurredTitle"] = "Si è attivato un allarme"; + ["DefaultAlertPhrase"] = "Impossibile connettersi a Roblox. Riprova più tardi."; + ["UnlockedUGCTitle"] = "Hai sbloccato tutti i giochi!"; + ["UnlockedUGCPhrase"] = "Ora che hai provato 5 giochi in evidenza, puoi accedere all'intera selezione di giochi creati da membri della community come te!\n\nVai alla schermata GIOCHI per scoprire le novità che hai sbloccato!"; + ["PlatformLinkInfoTitle"] = "Roblox ti dà il benvenuto!"; + ["PlatformLinkInfoMessage"] = "Abbiamo creato un nuovo account Roblox per te. Tutti i progressi di gioco e gli acquisti verranno salvati in questo account Roblox. Ora puoi accedere da una piattaforma qualsiasi e continuare da dove avevi interrotto!"; + ["ControllerLostConnectionTitle"] = "Controller mancante"; + ["ControllerLostConnectionPhrase"] = "Il controller dell'utente '%s' è stato disconnesso. Premi 'A' sul controller con cui desideri continuare."; + ["ActiveUserLostConnectionTitle"] = "Utente attivo rimosso"; + ["ActiveUserLostConnectionPhrase"] = "L'utente '%s' è stato disconnesso. Premi 'A' sul controller con cui desideri continuare."; + ["PlayMyPlaceMoreGamesTitle"] = "I miei giochi"; + ["PlayMyPlaceMoreGamesPhrase"] = "Accedi a Roblox.com per creare altri giochi e modificare le tue creazioni esistenti!"; + ["PrivateSessionPhrase"] = "In un gioco privato"; + ["ReauthSignedOutTitle"] = "Disconnesso"; + ["ReauthSignedOutPhrase"] = "Ti sei disconnesso dall'account Xbox Live. Accedi per continuare."; + ["ReauthRemovedTitle"] = "Utente cambiato"; + ["ReauthRemovedPhrase"] = "È stato rilevato un cambiamento nell'utente attivo. Accedi di nuovo."; + ["ReauthInvalidSessionPhrase"] = "Sei stato disconnesso da tutte le tue attuali sessioni Roblox."; + ["ReauthUnlinkTitle"] = "Account scollegato"; + ["ReauthUnlinkPhrase"] = "Ti sei scollegato con successo dall'account Roblox. Ora puoi scegliere di accedere di nuovo o registrarti per un nuovo account Roblox."; + ["ReauthUnknownPhrase"] = "Si è verificato un errore e sei stato disconnesso. Accedi di nuovo e continua a giocare."; + ["AccountUnder13Phrase"] = "Account: meno di 13 anni"; + ["AccountOver13Phrase"] = "Account: più di 13 anni"; + ["CPPWelcomeTitle"] = "Ti diamo il benvenuto al gioco multipiattaforma!"; + ["CPPWelcomePhrase"] = "Puoi giocare a Roblox su Xbox One insieme a giocatori di tutte le piattaforme, tra cui PC, telefono e tablet! Ciò significa che vedrai sia più giocatori che più partite e potrai giocare con TUTTI i tuoi amici, a prescindere dal dispositivo utilizzato! Le opzioni di gioco possono essere aggiornate nelle Impostazioni account."; + ["MPSRestrictedPhrase"] = "Non puoi giocare a sessioni multigiocatore a causa delle impostazioni del tuo account Xbox. Puoi modificarle nelle impostazioni di Xbox o su Xbox.com"; + ["MPSBannedPhrase"] = "Il tuo account è attualmente sospeso dalle sessioni multigiocatore."; + ["MPSPurchaseRequiredPhrase"] = "Devi avere un abbonamento a Xbox Live Gold per giocare alle sessioni multigiocatore online."; + ["UGCRestrictedPhrase"] = "Non puoi accedere ai contenuti generati dagli utenti a causa delle impostazioni del tuo account Xbox. Puoi modificarle nelle impostazioni di Xbox o su Xbox.com"; + ["UGCBannedPhrase"] = "Il tuo account Xbox è attualmente sospeso dal giocare con contenuti generati dagli utenti."; + ["PrivilegeCheckFailPhrase"] = "Il tuo account Xbox non può attualmente giocare in sessioni multiplayer. Riprova più tardi."; +} + +return locale diff --git a/Client2018/content/internal/AppShellLocalization/Modules/ja.lua b/Client2018/content/internal/AppShellLocalization/Modules/ja.lua new file mode 100644 index 0000000..95c7ef5 --- /dev/null +++ b/Client2018/content/internal/AppShellLocalization/Modules/ja.lua @@ -0,0 +1,230 @@ +local locale = +{ + ["HomeWord"] = "ホーム"; + ["GamesWord"] = "ゲーム"; + ["FriendsWord"] = "友達"; + ["CatalogWord"] = "Robux"; + ["AvatarWord"] = "アバター"; + ["PlayWord"] = "プレイ"; + ["FavoriteWord"] = "お気に入り"; + ["FavoritedWord"] = "お気に入り済み"; + ["LikedWord"] = "いいね"; + ["DislikedWord"] = "ひどいね"; + ["BackWord"] = "戻る"; + ["SettingsWord"] = "設定"; + ["AccountWord"] = "アカウント"; + ["OverscanWord"] = "画面調節"; + ["HelpWord"] = "ヘルプ"; + ["SwitchProfileWord"] = "プロファイルの切替え"; + ["RecenterButtonWord"] = "再び中央へ"; + ["MoreWord"] = "もっと"; + ["SearchWord"] = "サーチ"; + ["SearchGamesPhrase"] = "ゲームをサーチ"; + ["SearchingForPhrase"] = "%s : をサーチ"; + ["NoGamesPhrase"] = "ゲームが見つかりません。"; + ["EditAvatarPhrase"] = "アバターを編集"; + ["FriendActivityWord"] = "友達のアクティビティ"; + ["OnlineFriendsWords"] = "オンラインの友達"; + ["RecentlyPlayedWithSortTitle"] = "最近遊んだ友達"; + ["StartPartyPhrase"] = "パーティの写真を撮る"; + ["RecommendedSortTitle"] = "おすすめ"; + ["FavoritesSortTitle"] = "お気に入り"; + ["RecentlyPlayedSortTitle"] = "最近のプレイ"; + ["MyRecentTitle"] = "最近の"; + ["TopEarningTitle"] = "高稼得"; + ["TopRatedTitle"] = "最評価"; + ["PopularTitle"] = "人気"; + ["FeaturedTitle"] = "注目"; + ["MoreGamesPhrase"] = "他のゲーム"; + ["NoFriendsOnlinePhrase"] = "あなたの友達はオンラインではありません。"; + ["JoinGameWord"] = "ゲームに参加"; + ["ViewGameDetailsWord"] = "ゲームの詳細を見る"; + ["InviteToPartyWord"] = "パーティに招待"; + ["ViewGamerCardWord"] = "ゲーマーカードを見る"; + ["RatingDescriptionTitle"] = "評価と説明"; + ["GameImagesTitle"] = "ゲームイメージ"; + ["GameBadgesTitle"] = "ゲームバッジ"; + ["FriendActivityTitle"] = "友達のアクティビティ"; + ["RelatedGamesTitle"] = "関連ゲーム"; + ["MoreDetailsTitle"] = "もっと詳しく"; + ["GameBadgesTitle"] = "ゲームバッジ"; + ["SelectYourAvatarTitle"] = "自分のアバターを選ぶ"; + ["LastUpdatedWord"] = "最終更新"; + ["CreationDateWord"] = "作成日時"; + ["CreatedByWord"] = "作成者"; + ["ReportGameWord"] = "ゲームをレポート"; + ["HaveBadgeWord"] = "このバッジは持っています。"; + ["MaxPlayersWord"] = "最大プレーヤー数"; + ["ScreenSizeWord"] = "画面サイズ"; + ["ResizeScreenPrompt"] = "右スティックを使ってホワイトボックスが画面の外に出るように調整"; + ["ResizeScreenInputHint"] = "画面をリサイズ"; + ["AcceptWord"] = "了解"; + ["ResetWord"] = "リセット"; + ["CannotVoteWord"] = "プレイしてから評価してください。"; + ["FirstToRateWord"] = "一番最初にこのゲームの評価をしよう。"; + ["CustomAvatarPhrase"] = "このゲームでは特製アバターが使われます。"; + ["EngagementScreenHint"] = "開始するには何かキーを押してください。"; + ["EquipWord"] = "アバターの交換"; + ["BuyWord"] = "買う"; + ["OkWord"] = "Ok"; + ["FreeWord"] = "無料"; + ["TakeWord"] = "取る"; + ["GetRobuxPhrase"] = "Robuxをゲット"; + ["AlreadyOwnedPhrase"] = "これはすでに所有しています。"; + ["PurchasedThisPhrase"] = "%s Robuxで買いました。"; + ["RobuxBalanceTitle"] = "自分の残高"; + ["RobuxBalanceOverlayTitle"] = "Robuxの残高"; + ["RobuxBalanceOverlayPhrase"] = "Xboxで利用可能なのはXbox Storeで購入したRobuxだけです。"; + ["PlatformBalanceTitle"] = "Xboxで利用可能:"; + ["TotalBalanceTitle"] = "合計残高"; + ["AvatarCatalogTitle"] = "カタログ"; + ["AvatarOutfitsTitle"] = "自分のコレクション"; + ["PurchasingTitle"] = "購入中..."; + ["ConfirmPurchaseTitle"] = "購入の確認"; + ["AreYouSurePhrase"] = "ほんとうに%sを買いたいですか?"; + ["AreYouSureTakePhrase"] = "ほんとうに%sが欲しいですか?"; + ["AreYouSureWithPricePhrase"] = "ほんとうに %s を %sで買いたいですか?"; + ["RemainingBalancePhrase"] = "残高は%s Robuxです"; + ["ConfirmWord"] = "確認"; + ["DeclineWord"] = "やめる"; + ["OnlineWord"] = "オンライン"; + ["OfflineWord"] = "オフライン"; + ["SubmitWord"] = "送信する"; + ["CancelWord"] = "キャンセル"; + ["ReportPhrase"] = "レポートは弊社モデレーションチームにお送りください。ゲームを調査した上で対応致します。"; + ["CurrencySymbol"] = "$"; + ["RobuxStoreDescription"] = "Robuxをゲットしたらアバターの身につけるもの、ゲームにおけるチップや能力を買おう。"; + ["RobuxStoreNoItemsPhrase"] = "買えるアイテムがありません、しばらくしてからやり直してください。"; + ["PercentMoreRobuxPhrase"] = "%s%% 以上"; + ["RobuxStoreError"] = "現在Robux アイテムにアクセスできません。"; + ["NoFriendsPhrase"] = "あなたの友達はオンラインではありません。ゲームをプレイして新しい友だちを作ろう。"; + ["PopupPartyUIErrorPhrase"] = "パーティを開始しようとしてエラーがありました。もう一度お試しください。"; + ["AuthenticationErrorTitle"] = "認証エラー"; + ["AuthInProgressPhrase"] = "すでに認証中です。"; + ["AuthAccountUnlinkedPhrase"] = "EngagementScreenがハンドルする特別なケースです。"; + ["AuthMissingGamePadPhrase"] = "ゲームパッドが見つかりません。ゲームパッドの電源をオンにしてください。"; + ["AuthNoUserDetectedPhrase"] = "Xbox Liveユーザーが見つかりません。Xbox Liveアカウントでサインインしてください。"; + ["AuthHttpErrorDetected"] = "Roblox サーバと通信中のトラブル詳細にいては、 www.roblox.com/help/xbox を参照してください。"; + ["AuthErrorPhrase"] = "Roblox サーバと通信中のトラブルもう一度お試しください。"; + ["NewUserPhrase"] = "コミュニティで作成されたゲーム無限の可能性"; + ["SignInPhrase"] = "サインイン"; + ["PlayAsPhrase"] = "%sを使ってサインアップ"; + ["UsernameWord"] = "ユーザー名"; + ["PasswordWord"] = "パスワード"; + ["UsernameRulePhrase"] = "3-20 文字, スペースは不可"; + ["PasswordRulePhrase"] = "最低6文字と数字2文字"; + ["AccountSettingsTitle"] = "アカウント設定"; + ["InvalidUsernameTitle"] = "無効なユーザー名"; + ["InvalidPasswordTitle"] = "無効なパスワード"; + ["InvalidUsernamePhrase"] = "ユーザー名は最大320文字までです。"; + ["InvalidPasswordPhrase"] = "パスワードは最小6文字と数字2文字必要です。"; + ["AlreadyTakenTitle"] = "使用済みユーザー名"; + ["AlreadyTakenPhrase"] = "そのユーザー名は既に使用済みです。別の名前にしてください。"; + ["InvalidCharactersUsedPhrase"] = "ユーザ名に使えない文字が含まれています。文字と数字のみ使用できます。"; + ["UsernameCannotContainSpacesPhrase"] = "ユーザ名にスペースが含まれています。文字と数字のみ使用できます。"; + ["NoUsernameEnteredPhrase"] = "ユーザー名が必要です"; + ["NoUsernameOrPasswordEnteredPhrase"] = "有効なユーザー名とパスワードを入力してRobloxアカウントをセットアップしてください。"; + ["LinkedAsPhrase"] = "%s は現在あなたのRoblox アカウント %s にリンクしています。"; + ["LinkSignUpDisabled"] = "サインアップは現在無効です。しばらくしてからやり直してください。"; + ["LinkFlooded"] = "本日のサインアップ最大回数を超えました。有効なRobloxアカウントでサインインするか、しばらくしてからやり直してください。"; + ["LinkLeaseLocked"] = "トランザクション中です。お待ちください。"; + ["LinkAccountLinkingDisabled"] = "アカウントのリンク付けは現在無効です。しばらくしてからやり直してください。"; + ["LinkInvalidRobloxUser"] = "入力したRobloxユーザー名は正しくありません。有効なRobloxユーザー名を入力して下さい。"; + ["LinkRobloxUserAlreadyLinked"] = "あなたのXbox Liveアカウントは、すでにRobloxアカウントにリンクしています。"; + ["LinkXboxUserAlreadyLinked"] = "あなたのXbox Liveアカウントは、すでにRobloxアカウントにリンクしています。"; + ["LinkIllegalChildAccountLinking"] = "アカウントをリンクできませんでした。Xboxの年齢設定を確認してください。"; + ["LinkInvalidPassword"] = "入力したパスワードが正しくありません。正しいパスワードを入力してください。"; + ["LinkUsernamePasswordNotSet"] = "ユーザー名またはパスワードが入力されていません。ユーザ名とパスワードを入力してもう一度やり直してください。"; + ["LinkUsernameAlreadyTaken"] = "そのユーザー名は既に使われています。別のユーザー名を選択して、もう一度やり直してください。"; + ["LinkInvalidCredentials"] = "ユーザー名またはパスワードが無効です。有効なユーザ名とパスワードを入力してもう一度やり直してください。"; + ["UserIsGuestAccount"] = "ゲストアカウントは認証不可です。ゲストアカウント以外でサインインしてください。"; + ["LinkUnknownError"] = "不明なエラーが発生しました。もう一度お試しください。"; + ["LinkAccountTitle"] = "Robloxにサインイン"; + ["LinkAccountPhrase"] = "有効なRobloxアカウントでサインインしてアバターの容姿にアクセスしたりゲームの進捗をセーブ。RobloxアカウントはXbox Liveアカウントにリンクするので、次回プレイの際は自動サインインします!関連付けは設定画面からいつでも解除できます。"; + ["SignUpWord"] = "新規登録"; + ["SignUpTitle"] = "Robloxアカウントを作成"; + ["SignUpPhrase"] = "Robloxアカウントでプラットフォームを問わずRobloxにアクセス可能です。ユーザー名とパスワードを設定すれば、どこからでもサインインして中断した所から再開できます。RobloxアカウントはXbox Liveアカウントにリンクするので、次回プレイの際は自動サインインします!関連付けは設定画面からいつでも解除できます。"; + ["SetCredentialsTitle"] = "ユーザー名とパスワードを決定します。"; + ["SetCredentialsPhrase"] = "新規登録が完了する前に手続きが中断されたようです。大丈夫、こちらからユーザー名とパスワードを選んで新規登録を完了できます。RobloxアカウントはXbox Liveアカウントにリンクするので、次回プレイの際は自動サインインします!関連付けは設定画面からいつでも解除できます。"; + ["SetCredentialsWord"] = "割り当て"; + ["UnlinkTitle"] = "アカウントのリンクを解除"; + ["UnlinkGamerTagPhrase"] = "%s をリンク解除"; + ["UnlinkPhrase"] = "ほんとうにこのRobloxアカウントとXboxアカウントのリンクを解除したいですか?セーブデータと購入品はこのRobloxアカウントに紐付いているので、再度サインインするまでアクセスできません。"; + ["GameplaySettingsTitle"] = "ゲームオプション"; + ["EnabledWord"] = "有効(おすすめ)"; + ["DisabledWord"] = "無効"; + ["LoadingWord"] = "ロード中..."; + ["KeepEnabledPhrase"] = "有効状態を保つ"; + ["DisableWord"] = "無効化"; + ["CrossPlatformGameplayPhrase"] = "異機種間プレイ"; + ["DisableCrossplayOverlayTitle"] = "異機種間プレイを無効にしますか?"; + ["DisableCrossplayOverlayMessage"] = "本当に異機種間プレイを無効にしますか?\nこの操作は推奨しません。異機種プレーヤーのゲームに参加できなくなり、会えるプレーヤーの数も減ってしまいますよ。"; + ["EnableCrossplayOverlayTitle"] = "異機種間プレイが有効になりました!"; + ["EnableCrossplayOverlayMessage"] = "デスクトップ、スマートフォン、タブレット、全ての対応プラットフォームのプレーヤー達とXbox One上でRobloxのプレイが可能になりました!つまり、使用機種を問わず、友達全員とプレイできて、より多くのゲームで、より多くのプレーヤー達に会えることでしょう!"; + ["CrossplayEnabledDescription"] = "全対応機種のプレーヤーのゲームに参加できるようになり、より多くのゲームで、より多くのプレーヤー達に会えることでしょう!"; + ["CrossplayDisabledDescription"] = "非推奨です。異機種プレーヤーのゲームに参加できなくなり、会えるプレーヤーの数も減ってしまいますよ。"; + ["UnableToJoinTitle"] = "参加不可"; + ["ErrorOccurredTitle"] = "エラーが発生"; + ["DefaultErrorPhrase"] = "Robloxに接続できませんでした。しばらくしてからやり直してください。"; + ["DefaultJoinFailPhrase"] = "Robloxに接続できませんでした。しばらくしてからやり直してください。"; + ["AlreadyRunningPhrase"] = "すでにこのRobloxゲームに参加済みです。"; + ["WebServerConnectFailPhrase"] = "Robloxサーバーに接続できませんでした。しばらくしてからやり直してください。"; + ["AccessDeniedByWeb"] = "参加しようとしているRobloxゲームは現在利用できません。"; + ["InstanceNotFound"] = "参加しようとしているRobloxゲームは現在利用できません。"; + ["GameFullPhrase"] = "参加しようとしているRobloxゲームは現在満員です。"; + ["FollowUserFailed"] = "参加しようとしているRobloxゲームは現在利用できません。"; + ["InvalidPrivilegeMultiplayerSessionPhrase"] = "あなたのXboxアカウントは現在、マルチプレイヤーセッションでのプレイが許可されていません。"; + ["InvalidPrivilegeUGCPhrase"] = "あなたのXboxアカウントは現在、ユーザーが作成したコンテンツのプレイが許可されていません。"; + ["UnableToEquipTitle"] = "参加不可"; + ["UnableToEquipPhrase"] = "そのアバターを選択できませんでした。しばらくしてからやり直してください。"; + ["UnableToWearOufitTitle"] = "参加不可"; + ["UnableToWearOufitPhrase"] = "その衣装を選択できませんでした。しばらくしてからやり直してください。"; + ["UnableToDoPurchaseTitle"] = "購入の完了が不可"; + ["UnableToDoPurchasePhrase"] = "購入を完了できませんでした。しばらくしてからやり直してください。"; + ["UnableToDoRobuxPurchaseTitle"] = "購入の完了が不可"; + ["UnableToDoRobuxPurchasePhrase"] = "購入を完了できませんでした。しばらくしてからやり直してください。"; + ["CannotVoteTitle"] = "投票不可"; + ["VoteFloodPhrase"] = "頻繁に投票し過ぎです。しばらくしてから戻ってきてやり直してください。"; + ["VotePlayGamePhrase"] = "プレイしてから評価してください。"; + ["CannotFavoriteTitle"] = "ゲームをお気に入りにできない"; + ["FavoriteFloodPhrase"] = "頻繁にゲームをお気に入りにし過ぎです。しばらくしてから戻ってきてやり直してください。"; + ["ToSPhrase"] = "契約条件をみる..."; + ["ToSInfoLinkPhrase"] = "契約条件とプライバシーに関する方針:nwww.roblox.com/info/termsofservicenwww.roblox.com/info/privacy"; + ["PrivacyPhrase"] = "プライバシー"; + ["VersionIdString"] = "バージョン: %s.%s.%s.%s"; + ["ErrorMessageAndCodePrase"] = "%sエラーコード: %d"; + ["UnlockGamesPhrase"] = "5つの注目ゲームをプレイして、まさにキミのようなコミュニティメンバーによる作品をどんどんアンロックしよう!"; + ["AlertOccurredTitle"] = "エラーが発生"; + ["DefaultAlertPhrase"] = "Robloxに接続できませんでした。しばらくしてからやり直してください。"; + ["UnlockedUGCTitle"] = "全ゲーム解除!"; + ["UnlockedUGCPhrase"] = "5つの注目ゲームをプレイしたので、まさにキミのようなコミュニティメンバーが作った全種類のゲームにアクセスできるようになったぞ。ゲーム画面に行ってキミがアンロックしたゲームをみてみよう!"; + ["PlatformLinkInfoTitle"] = "Robloxへようこそ!"; + ["PlatformLinkInfoMessage"] = "あなたのRobloxアカウントが作成されました。全てのゲーム進捗と購入品はこのRobloxアカウントにセーブされます。どんなプラットフォームからでもサインインしてプレイを続行できるよ!"; + ["ControllerLostConnectionTitle"] = "不明なコントローラ"; + ["ControllerLostConnectionPhrase"] = "ユーザー '%s' のコントローラが切断しました。続行したいコントローラの'A' を押してください。"; + ["ActiveUserLostConnectionTitle"] = "アクティブユーザーの削除"; + ["ActiveUserLostConnectionPhrase"] = "ユーザー '%s' がログアウトしました。続行したいコントローラの'A' を押してください。"; + ["PlayMyPlaceMoreGamesTitle"] = "マイゲーム"; + ["PlayMyPlaceMoreGamesPhrase"] = "Roblox.com にサインインしてゲームを作ったり、自分の作品を編集したりしよう!"; + ["PrivateSessionPhrase"] = "非公開ゲームで"; + ["ReauthSignedOutTitle"] = "ログアウト完了"; + ["ReauthSignedOutPhrase"] = "Xbox Liveアカウントからログアウト完了しました。再開するにはサインインしてください。"; + ["ReauthRemovedTitle"] = "ユーザー変更"; + ["ReauthRemovedPhrase"] = "アクティブなユーザーにおいて変更が検出されました。再度サインインしてください。"; + ["ReauthInvalidSessionPhrase"] = "全Robloxセッションからサインアウトしました。"; + ["ReauthUnlinkTitle"] = "アカウントのリンク解除"; + ["ReauthUnlinkPhrase"] = "Robloxアカウントのリンク解除に成功しました。再度サインインするかRobloxアカウントを新規登録するか選択可能です。"; + ["ReauthUnknownPhrase"] = "エラーが発生、サインアウトしました。再度サインインしてプレイを再開してください。"; + ["AccountUnder13Phrase"] = "アカウント:13才以下"; + ["AccountOver13Phrase"] = "アカウント:13才以上"; + ["CPPWelcomeTitle"] = "異機種間プレイにようこそ!"; + ["CPPWelcomePhrase"] = "デスクトップ、スマートフォン、タブレット、全ての対応プラットフォームのプレーヤー達とXbox One上でRobloxのプレイが可能になりました!つまり、使用機種を問わず、友達全員とプレイできて、より多くのゲームで、より多くのプレーヤー達に会えることでしょう!ゲームオプションはアカウントセッティングで変更可能です。"; + ["MPSRestrictedPhrase"] = "あなたの Xbox アカウントは、マルチプレイヤー セッションでプレイできないように設定されています。これは Xbox 設定または Xbox.com から変更可能です。"; + ["MPSBannedPhrase"] = "あなたの Xbox アカウントは現在、マルチプレイヤー セッションでのプレイを禁止されています。"; + ["MPSPurchaseRequiredPhrase"] = "オンライン マルチプレイヤー セッションでプレイするには Xbox Live ゴールドが必要です。"; + ["UGCRestrictedPhrase"] = "あなたの Xbox アカウントは、ユーザーが作成したコンテンツをプレイできないように設定されています。これは Xbox 設定 または Xbox.com から変更可能です。"; + ["UGCBannedPhrase"] = "あなたの Xbox アカウントは現在、マルチプレイヤー セッションでのプレイを禁止されています。"; + ["PrivilegeCheckFailPhrase"] = "あなたの Xbox アカウントは現在、プレイを禁止されています。しばらくしてから再度お試しください。"; +} + +return locale diff --git a/Client2018/content/internal/AppShellLocalization/Modules/pt-BR.lua b/Client2018/content/internal/AppShellLocalization/Modules/pt-BR.lua new file mode 100644 index 0000000..076b057 --- /dev/null +++ b/Client2018/content/internal/AppShellLocalization/Modules/pt-BR.lua @@ -0,0 +1,230 @@ +local locale = +{ + ["HomeWord"] = "Início"; + ["GamesWord"] = "Jogos"; + ["FriendsWord"] = "Amigos"; + ["CatalogWord"] = "Robux"; + ["AvatarWord"] = "Avatar"; + ["PlayWord"] = "Jogar"; + ["FavoriteWord"] = "Favoritar"; + ["FavoritedWord"] = "Favorito"; + ["LikedWord"] = "Curtiu"; + ["DislikedWord"] = "Não curtiu"; + ["BackWord"] = "Voltar"; + ["SettingsWord"] = "Configurações"; + ["AccountWord"] = "Conta"; + ["OverscanWord"] = "Ajustar tela"; + ["HelpWord"] = "Ajuda"; + ["SwitchProfileWord"] = "Trocar perfil"; + ["RecenterButtonWord"] = "Centralizar"; + ["MoreWord"] = "Mais"; + ["SearchWord"] = "Pesquisar"; + ["SearchGamesPhrase"] = "Pesquisar jogos"; + ["SearchingForPhrase"] = "Pesquisando: %s"; + ["NoGamesPhrase"] = "Nenhum jogo encontrado."; + ["EditAvatarPhrase"] = "Editar avatar"; + ["FriendActivityWord"] = "Atividade de amigo"; + ["OnlineFriendsWords"] = "Amigos online"; + ["RecentlyPlayedWithSortTitle"] = "Jogou recentemente com"; + ["StartPartyPhrase"] = "Iniciar aplicativo de grupo"; + ["RecommendedSortTitle"] = "Recomendado"; + ["FavoritesSortTitle"] = "Favoritos"; + ["RecentlyPlayedSortTitle"] = "Jogou recentemente"; + ["MyRecentTitle"] = "Meus recentes"; + ["TopEarningTitle"] = "Maior ganho"; + ["TopRatedTitle"] = "Melhor avaliação"; + ["PopularTitle"] = "Populares"; + ["FeaturedTitle"] = "Destaques"; + ["MoreGamesPhrase"] = "Mais jogos"; + ["NoFriendsOnlinePhrase"] = "Seus amigos não estão online"; + ["JoinGameWord"] = "Entrar no jogo"; + ["ViewGameDetailsWord"] = "Ver detalhes do jogo"; + ["InviteToPartyWord"] = "Convidar para o grupo"; + ["ViewGamerCardWord"] = "Ver cartão do jogador"; + ["RatingDescriptionTitle"] = "Avaliação e descrição"; + ["GameImagesTitle"] = "Imagens do jogo"; + ["GameBadgesTitle"] = "Emblemas do jogo"; + ["FriendActivityTitle"] = "Atividade de amigo"; + ["RelatedGamesTitle"] = "Jogos relacionados"; + ["MoreDetailsTitle"] = "Mais detalhes"; + ["GameBadgesTitle"] = "Emblemas do jogo"; + ["SelectYourAvatarTitle"] = "Selecione o seu avatar"; + ["LastUpdatedWord"] = "Última atualização"; + ["CreationDateWord"] = "Data de criação"; + ["CreatedByWord"] = "Criado por"; + ["ReportGameWord"] = "Denunciar jogo"; + ["HaveBadgeWord"] = "Você possui este emblema"; + ["MaxPlayersWord"] = "Máximo de jogadores"; + ["ScreenSizeWord"] = "Tamanho da tela"; + ["ResizeScreenPrompt"] = "Use o direcional analógico direito para ajustar as bordas da caixa branca até que elas fiquem fora da tela"; + ["ResizeScreenInputHint"] = "Redimensionar tela"; + ["AcceptWord"] = "Aceitar"; + ["ResetWord"] = "Reiniciar"; + ["CannotVoteWord"] = "Você precisa jogar este jogo antes de avaliar"; + ["FirstToRateWord"] = "Seja o primeiro a avaliar este jogo"; + ["CustomAvatarPhrase"] = "Este jogo utiliza avatares personalizados"; + ["EngagementScreenHint"] = "Aperte qualquer botão para começar"; + ["EquipWord"] = "Trocar avatar"; + ["BuyWord"] = "Comprar"; + ["OkWord"] = "Ok"; + ["FreeWord"] = "Grátis"; + ["TakeWord"] = "Obter"; + ["GetRobuxPhrase"] = "Obtenha Robux"; + ["AlreadyOwnedPhrase"] = "Você já possui isto"; + ["PurchasedThisPhrase"] = "Você comprou isto por %s Robux"; + ["RobuxBalanceTitle"] = "Meu saldo"; + ["RobuxBalanceOverlayTitle"] = "Saldo de Robux"; + ["RobuxBalanceOverlayPhrase"] = "Apenas Robux comprados na Loja Xbox podem ser usados no Xbox."; + ["PlatformBalanceTitle"] = "Disponível no Xbox:"; + ["TotalBalanceTitle"] = "Saldo total:"; + ["AvatarCatalogTitle"] = "Catálogo"; + ["AvatarOutfitsTitle"] = "Minha coleção"; + ["PurchasingTitle"] = "Comprando..."; + ["ConfirmPurchaseTitle"] = "Confirmar compra"; + ["AreYouSurePhrase"] = "Tem certeza de que deseja comprar %s?"; + ["AreYouSureTakePhrase"] = "Tem certeza de que deseja obter %s?"; + ["AreYouSureWithPricePhrase"] = "Tem certeza de que deseja comprar %s por %s?"; + ["RemainingBalancePhrase"] = "Seu saldo restante será de %s Robux"; + ["ConfirmWord"] = "Confirmar"; + ["DeclineWord"] = "Recusar"; + ["OnlineWord"] = "Online"; + ["OfflineWord"] = "Offline"; + ["SubmitWord"] = "Enviar"; + ["CancelWord"] = "Cancelar"; + ["ReportPhrase"] = "Você pode enviar um relatório para nossa equipe de moderadores. Vamos analisar o jogo e realizar os procedimentos apropriados."; + ["CurrencySymbol"] = "$"; + ["RobuxStoreDescription"] = "Obtenha Robux para comprar visuais novos incríveis para o seu avatar, além de benefícios e habilidades no jogo."; + ["RobuxStoreNoItemsPhrase"] = "Não há itens para compra no momento. Tente de novo mais tarde."; + ["PercentMoreRobuxPhrase"] = "Mais %s%%"; + ["RobuxStoreError"] = "Itens de Robux indisponíveis no momento."; + ["NoFriendsPhrase"] = "Seus amigos não estão online. Jogue alguns jogos e faça novas amizades!"; + ["PopupPartyUIErrorPhrase"] = "Houve um erro ao tentar iniciar um grupo. Tente novamente."; + ["AuthenticationErrorTitle"] = "Erro de autenticação"; + ["AuthInProgressPhrase"] = "A autenticação já está em andamento"; + ["AuthAccountUnlinkedPhrase"] = "Este é um caso especial tratado na EngagementScreen"; + ["AuthMissingGamePadPhrase"] = "Nenhum gamepad detectado. Ligue um gamepad."; + ["AuthNoUserDetectedPhrase"] = "Nenhum usuário Xbox Live detectado. Conecte-se em uma conta Xbox Live."; + ["AuthHttpErrorDetected"] = "Problema na comunicação com os servidores do Roblox. Confira www.roblox.com/help/xbox para mais informações."; + ["AuthErrorPhrase"] = "Problema na comunicação com os servidores do Roblox. Tente novamente."; + ["NewUserPhrase"] = "Jogos criados pela comunidade. Possibilidades infinitas."; + ["SignInPhrase"] = "Conectar-se"; + ["PlayAsPhrase"] = "Conectar-se usando %s"; + ["UsernameWord"] = "Nome de usuário"; + ["PasswordWord"] = "Senha"; + ["UsernameRulePhrase"] = "3-20 caracteres, sem espaços"; + ["PasswordRulePhrase"] = "Mínimo de 6 letras e 2 números"; + ["AccountSettingsTitle"] = "Configurações de conta"; + ["InvalidUsernameTitle"] = "Nome de usuário inválido"; + ["InvalidPasswordTitle"] = "Senha inválida"; + ["InvalidUsernamePhrase"] = "Nomes de usuário devem ter de 3 a 20 caracteres."; + ["InvalidPasswordPhrase"] = "A senha deve ter pelo menos 6 letras e 2 números"; + ["AlreadyTakenTitle"] = "Nome de usuário já usado"; + ["AlreadyTakenPhrase"] = "Este nome de usuário já está sendo usado por alguém. Tente outro."; + ["InvalidCharactersUsedPhrase"] = "O nome de usuário que você inseriu contém caracteres inválidos. Apenas letras e números são permitidos."; + ["UsernameCannotContainSpacesPhrase"] = "O nome de usuário que você inseriu contém espaços. Apenas letras e números são permitidos."; + ["NoUsernameEnteredPhrase"] = "Requer nome de usuário."; + ["NoUsernameOrPasswordEnteredPhrase"] = "Você precisa inserir um nome de usuário e senha válidos para criar sua conta Roblox."; + ["LinkedAsPhrase"] = "%s está vinculado à sua conta Roblox, %s."; + ["LinkSignUpDisabled"] = "Cadastro desabilitado no momento. Tente de novo mais tarde."; + ["LinkFlooded"] = "Você se cadastrou muitas vezes hoje. Conecte-se com uma conta Roblox existente ou tente novamente."; + ["LinkLeaseLocked"] = "Transação em andamento Aguarde, por favor."; + ["LinkAccountLinkingDisabled"] = "Vinculação de conta desabilitada no momento. Tente de novo mais tarde."; + ["LinkInvalidRobloxUser"] = "O nome de usuário do Roblox que você inseriu é inválido. Insira um nome de usuário do Roblox válido."; + ["LinkRobloxUserAlreadyLinked"] = "Sua conta do Xbox Live já está vinculada a uma conta Roblox"; + ["LinkXboxUserAlreadyLinked"] = "Sua conta do Xbox Live já está vinculada a uma conta Roblox."; + ["LinkIllegalChildAccountLinking"] = "As contas não puderam ser vinculadas. Verifique as suas configurações de idade do Xbox."; + ["LinkInvalidPassword"] = "A senha do Roblox que você inseriu é inválida. Insira a senha correta."; + ["LinkUsernamePasswordNotSet"] = "Nome de usuário ou senha estão vazios. Insira um nome de usuário e senha e tente novamente."; + ["LinkUsernameAlreadyTaken"] = "Este nome de usuário já está sendo usado. Escolha um outro nome de usuário e tente novamente."; + ["LinkInvalidCredentials"] = "Nome de usuário ou senha inválidos. Insira um nome de usuário e senha válidos e tente novamente."; + ["UserIsGuestAccount"] = "Contas de visitantes não podem ser autenticadas. Conecte-se com uma conta que não seja de visitante."; + ["LinkUnknownError"] = "Um erro desconhecido ocorreu. Tente novamente."; + ["LinkAccountTitle"] = "Conectar-se no Roblox"; + ["LinkAccountPhrase"] = "Conecte-se com um conta Roblox existente para acessar a aparência do seu avatar e salvar o progresso do seu jogo.\n\nSua conta Roblox será vinculada à sua conta do Xbox Live e na próxima vez que jogar, você será conectado automaticamente!\n\nVocê pode desvincular na tela de Configurações a qualquer momento."; + ["SignUpWord"] = "Conectar-se"; + ["SignUpTitle"] = "Criar uma conta Roblox"; + ["SignUpPhrase"] = "A conta Roblox permitirá o seu acesso ao Roblox em qualquer plataforma. É só definir um nome de usuário e senha e você poderá se conectar em qualquer lugar e continuar de onde parou! \n\nSua nova conta Roblox será vinculada à sua conta do Xbox Live e na próxima vez que jogar, você será conectado automaticamente!\n\nVocê pode desvincular na tela de Configurações a qualquer momento."; + ["SetCredentialsTitle"] = "Definir nome de usuário e senha"; + ["SetCredentialsPhrase"] = "Parece que você foi interrompido antes de terminar seu cadastro! Não se preocupe. Você pode terminar de configurar sua conta Roblox escolhendo um nome de usuário e senha aqui.\n\nSua nova conta Roblox será vinculada à sua conta do Xbox Live e na próxima vez que jogar, você será conectado automaticamente!\n\nVocê pode desvincular na tela de Configurações a qualquer momento."; + ["SetCredentialsWord"] = "Definir"; + ["UnlinkTitle"] = "Desvincular conta"; + ["UnlinkGamerTagPhrase"] = "Desvincular %s"; + ["UnlinkPhrase"] = "Tem certeza de que deseja desvincular esta conta Roblox da sua conta do Xbox? Seus dados de salvamento e compras estão associados com esta conta Roblox e ficarão inacessíveis até você se conectar de novo!"; + ["GameplaySettingsTitle"] = "Opções de jogo"; + ["EnabledWord"] = "Habilitado (Recomendado)"; + ["DisabledWord"] = "Desabilitado"; + ["LoadingWord"] = "Carregando..."; + ["KeepEnabledPhrase"] = "Manter habilitado"; + ["DisableWord"] = "Desabilitar"; + ["CrossPlatformGameplayPhrase"] = "Jogo multiplataformas"; + ["DisableCrossplayOverlayTitle"] = "Desabilitar jogo multiplataformas?"; + ["DisableCrossplayOverlayMessage"] = "Tem certeza de que deseja desabilitar o jogo multiplataformas?\nIsto não é recompensado. Você não poderá mais entrar em jogos com jogadores em outras plataforma e verá menos jogadores nos jogos."; + ["EnableCrossplayOverlayTitle"] = "Jogo multiplataformas habilitado!"; + ["EnableCrossplayOverlayMessage"] = "Você agora pode jogar Roblox no Xbox One com outros jogadores em todas as plataformas, incluindo computador, celular e tablet! Isto significa que você verá mais jogadores em mais jogos e poderá jogar com TODOS os seus amigos, independente do dispositivo que eles usem!"; + ["CrossplayEnabledDescription"] = "Você poderá entrar em jogos com jogadores de todas as plataformas e verá mais jogadores em mais jogos."; + ["CrossplayDisabledDescription"] = "Isto não é recomendado. Você não poderá entrar em jogos com jogadores em outras plataforma e verá menos jogadores no jogo."; + ["UnableToJoinTitle"] = "Impossível entrar"; + ["ErrorOccurredTitle"] = "Ocorreu um erro"; + ["DefaultErrorPhrase"] = "Impossível conectar ao Roblox. Tente de novo mais tarde."; + ["DefaultJoinFailPhrase"] = "Impossível conectar ao Roblox. Tente de novo mais tarde."; + ["AlreadyRunningPhrase"] = "Você já está neste jogo do Roblox."; + ["WebServerConnectFailPhrase"] = "Impossível conectar aos servidores do Roblox. Tente de novo mais tarde."; + ["AccessDeniedByWeb"] = "O jogo do Roblox no qual você está tentando entrar não está disponível no momento."; + ["InstanceNotFound"] = "O jogo do Roblox no qual você está tentando entrar não está disponível no momento."; + ["GameFullPhrase"] = "O jogo do Roblox no qual você está tentando entrar está cheio."; + ["FollowUserFailed"] = "O jogo do Roblox no qual você está tentando entrar não está disponível no momento."; + ["InvalidPrivilegeMultiplayerSessionPhrase"] = "Sua conta do Xbox não tem permissão para jogar em sessões multijogador no momento."; + ["InvalidPrivilegeUGCPhrase"] = "Sua conta do Xbox não tem permissão para jogar conteúdo gerado por usuários no momento."; + ["UnableToEquipTitle"] = "Impossível selecionar"; + ["UnableToEquipPhrase"] = "Não conseguimos selecionar este avatar. Tente de novo mais tarde."; + ["UnableToWearOufitTitle"] = "Impossível selecionar"; + ["UnableToWearOufitPhrase"] = "Não conseguimos selecionar este traje. Tente de novo mais tarde."; + ["UnableToDoPurchaseTitle"] = "Impossível completar compra"; + ["UnableToDoPurchasePhrase"] = "Não conseguimos completar a sua compra. Tente de novo mais tarde."; + ["UnableToDoRobuxPurchaseTitle"] = "Impossível completar compra"; + ["UnableToDoRobuxPurchasePhrase"] = "Não conseguimos completar a sua compra. Tente de novo mais tarde."; + ["CannotVoteTitle"] = "Impossível votar"; + ["VoteFloodPhrase"] = "Você está votando com frequência excessiva. Volte mais tarde e tente novamente."; + ["VotePlayGamePhrase"] = "Você precisa jogar este jogo antes de votar."; + ["CannotFavoriteTitle"] = "Impossível adicionar jogo aos favoritos"; + ["FavoriteFloodPhrase"] = "Você está adicionando aos favoritos com frequência excessiva. Volte mais tarde e tente novamente."; + ["ToSPhrase"] = "Ver termos..."; + ["ToSInfoLinkPhrase"] = "Termos e Política de Privacidade:\nwww.roblox.com/info/termsofservice\nwww.roblox.com/info/privacy"; + ["PrivacyPhrase"] = "Privacidade"; + ["VersionIdString"] = "Versão: %s.%s.%s.%s"; + ["ErrorMessageAndCodePrase"] = "%s\nCódigo de erro: %d"; + ["UnlockGamesPhrase"] = "Jogue 5 jogos em destaque para desbloquear mais criações feitas por membros da comunidade como você!"; + ["AlertOccurredTitle"] = "Ocorreu um alerta"; + ["DefaultAlertPhrase"] = "Impossível conectar ao Roblox. Tente de novo mais tarde."; + ["UnlockedUGCTitle"] = "Todos os jogos desbloqueados!"; + ["UnlockedUGCPhrase"] = "Agora que você jogou 5 jogos em destaque, terá acesso a todos os jogos criados por membros da comunidade como você!\n\nVá para a tela de JOGOS para ver todos os novos jogos que você desbloqueou!"; + ["PlatformLinkInfoTitle"] = "Bem-vindo ao Roblox!"; + ["PlatformLinkInfoMessage"] = "Criamos uma nova conta Roblox para você. Todos o progresso de jogo e compras serão salvos nesta conta Roblox. Agora você pode se conectar em qualquer plataforma e continuar de onde parou!"; + ["ControllerLostConnectionTitle"] = "Controle não encontrado"; + ["ControllerLostConnectionPhrase"] = "O controle do usuário '%s' foi desconectado. Pressione 'A' no controle com o qual você deseja continuar."; + ["ActiveUserLostConnectionTitle"] = "Usuário ativo removido"; + ["ActiveUserLostConnectionPhrase"] = "O usuário '%s' foi desconectado. Pressione 'A' no controle com o qual você deseja continuar."; + ["PlayMyPlaceMoreGamesTitle"] = "Meus jogos"; + ["PlayMyPlaceMoreGamesPhrase"] = "Conecte-se em Roblox.com para criar mais jogos e editar suas criações existentes!"; + ["PrivateSessionPhrase"] = "Em jogo privado"; + ["ReauthSignedOutTitle"] = "Desconectado"; + ["ReauthSignedOutPhrase"] = "Você desconectou-se da sua conta do Xbox Live. Conecte-se de novo para continuar."; + ["ReauthRemovedTitle"] = "Usuário alterado"; + ["ReauthRemovedPhrase"] = "Detectamos uma mudança no usuário ativo. Conecte-se novamente."; + ["ReauthInvalidSessionPhrase"] = "Você foi desconectado da sua sessão do Roblox atual."; + ["ReauthUnlinkTitle"] = "Conta desvinculada"; + ["ReauthUnlinkPhrase"] = "Você desvinculou sua conta Roblox com sucesso. Agora você pode escolher se conectar de novo ou cadastrar uma nova conta Roblox."; + ["ReauthUnknownPhrase"] = "Um erro ocorreu e você foi desconectado. Conecte-se novamente para continuar a jogar."; + ["AccountUnder13Phrase"] = "Conta: Menor de 13 anos"; + ["AccountOver13Phrase"] = "Conta: Mais de 13 anos"; + ["CPPWelcomeTitle"] = "Bem-vindo ao jogo multiplataformas!"; + ["CPPWelcomePhrase"] = "Você agora pode jogar Roblox no Xbox One com outros jogadores em todas as plataformas, incluindo computador, celular e tablet! Isto significa que você verá mais jogadores em mais jogos e poderá jogar com TODOS os seus amigos, independente do dispositivo que eles usem! As opções de jogo podem ser atualizadas nas Configurações de Conta."; + ["MPSRestrictedPhrase"] = "Suas configurações de conta do Xbox estão impedindo que você jogue sessões multijogador. Você pode alterá-las nas Configurações do Xbox ou em Xbox.com"; + ["MPSBannedPhrase"] = "Sua conta do Xbox está banida para sessões multijogador no momento."; + ["MPSPurchaseRequiredPhrase"] = "Você precisa ter Xbox Live Gold para poder jogar sessões multijogador online."; + ["UGCRestrictedPhrase"] = "Suas configurações de conta do Xbox estão impedindo que você jogue conteúdo gerado por usuários. Você pode alterá-las nas Configurações do Xbox ou em Xbox.com"; + ["UGCBannedPhrase"] = "Sua conta do Xbox está banida para conteúdo gerado por usuários."; + ["PrivilegeCheckFailPhrase"] = "Sua conta do Xbox não tem permissão para jogar. Tente de novo mais tarde."; +} + +return locale diff --git a/Client2018/content/internal/AppShellLocalization/Modules/ru.lua b/Client2018/content/internal/AppShellLocalization/Modules/ru.lua new file mode 100644 index 0000000..7a7e182 --- /dev/null +++ b/Client2018/content/internal/AppShellLocalization/Modules/ru.lua @@ -0,0 +1,230 @@ +local locale = +{ + ["HomeWord"] = "Главная"; + ["GamesWord"] = "Игры"; + ["FriendsWord"] = "Друзья"; + ["CatalogWord"] = "Robux"; + ["AvatarWord"] = "Аватар"; + ["PlayWord"] = "Играть"; + ["FavoriteWord"] = "В избранное"; + ["FavoritedWord"] = "Добавлено"; + ["LikedWord"] = "Понравилось"; + ["DislikedWord"] = "Не понравилось"; + ["BackWord"] = "Назад"; + ["SettingsWord"] = "Настройки"; + ["AccountWord"] = "Учетная запись"; + ["OverscanWord"] = "Настройка экрана"; + ["HelpWord"] = "Справка"; + ["SwitchProfileWord"] = "Сменить профиль"; + ["RecenterButtonWord"] = "Центрировать заново"; + ["MoreWord"] = "Больше"; + ["SearchWord"] = "Поиск"; + ["SearchGamesPhrase"] = "Искать игры"; + ["SearchingForPhrase"] = "Искать: %s"; + ["NoGamesPhrase"] = "Игр не найдено."; + ["EditAvatarPhrase"] = "Изменить аватар"; + ["FriendActivityWord"] = "Действия друзей"; + ["OnlineFriendsWords"] = "Друзья в сети"; + ["RecentlyPlayedWithSortTitle"] = "Недавно играли с"; + ["StartPartyPhrase"] = "Прикрепить приложение «Команда»"; + ["RecommendedSortTitle"] = "Рекомендуемые"; + ["FavoritesSortTitle"] = "Избранные"; + ["RecentlyPlayedSortTitle"] = "Недавно играли"; + ["MyRecentTitle"] = "Мои недавние"; + ["TopEarningTitle"] = "Самые прибыльные"; + ["TopRatedTitle"] = "С наилучшим рейтингом"; + ["PopularTitle"] = "Популярное"; + ["FeaturedTitle"] = "Подборка"; + ["MoreGamesPhrase"] = "Больше игр"; + ["NoFriendsOnlinePhrase"] = "Друзья не в сети"; + ["JoinGameWord"] = "Присоединиться к игре"; + ["ViewGameDetailsWord"] = "Сведения об игре"; + ["InviteToPartyWord"] = "Пригласить в команду"; + ["ViewGamerCardWord"] = "Просмотреть карту игрока"; + ["RatingDescriptionTitle"] = "Рейтинг и описание"; + ["GameImagesTitle"] = "Изображения игры"; + ["GameBadgesTitle"] = "Значки игры"; + ["FriendActivityTitle"] = "Действия друзей"; + ["RelatedGamesTitle"] = "Похожие игры"; + ["MoreDetailsTitle"] = "Подробнее"; + ["GameBadgesTitle"] = "Значки игры"; + ["SelectYourAvatarTitle"] = "Выберите свой аватар"; + ["LastUpdatedWord"] = "Последнее обновление"; + ["CreationDateWord"] = "Дата создания"; + ["CreatedByWord"] = "Создатель"; + ["ReportGameWord"] = "Пожаловаться на игру"; + ["HaveBadgeWord"] = "У вас есть этот значок"; + ["MaxPlayersWord"] = "Макс. кол-во игроков"; + ["ScreenSizeWord"] = "Размер экрана"; + ["ResizeScreenPrompt"] = "Переместите границы белого поля правым мини-джойстиком, чтобы они оказались вне экрана"; + ["ResizeScreenInputHint"] = "Изменить размер экрана"; + ["AcceptWord"] = "Принять"; + ["ResetWord"] = "Сброс"; + ["CannotVoteWord"] = "Прежде чем оценить игру, необходимо в нее сыграть"; + ["FirstToRateWord"] = "Оцените игру раньше всех"; + ["CustomAvatarPhrase"] = "Используются пользовательские аватары"; + ["EngagementScreenHint"] = "Нажмите любую кнопку, чтобы начать"; + ["EquipWord"] = "Сменить аватар"; + ["BuyWord"] = "Купить"; + ["OkWord"] = "OK"; + ["FreeWord"] = "Бесплатно"; + ["TakeWord"] = "Получить"; + ["GetRobuxPhrase"] = "Получить Robux"; + ["AlreadyOwnedPhrase"] = "У вас уже есть этот предмет"; + ["PurchasedThisPhrase"] = "Вы купили этот товар за %s Robux"; + ["RobuxBalanceTitle"] = "Мой баланс"; + ["RobuxBalanceOverlayTitle"] = "Баланс Robux"; + ["RobuxBalanceOverlayPhrase"] = "В Xbox можно использовать только Robux, купленные в магазине Xbox."; + ["PlatformBalanceTitle"] = "Доступно в Xbox:"; + ["TotalBalanceTitle"] = "Общий баланс:"; + ["AvatarCatalogTitle"] = "Каталог"; + ["AvatarOutfitsTitle"] = "Моя коллекция"; + ["PurchasingTitle"] = "Покупка..."; + ["ConfirmPurchaseTitle"] = "Подтверждение покупки"; + ["AreYouSurePhrase"] = "Купить товар %s?"; + ["AreYouSureTakePhrase"] = "Получить предмет %s?"; + ["AreYouSureWithPricePhrase"] = "Купить товар %s за %s?"; + ["RemainingBalancePhrase"] = "На вашем балансе останется %s Robux"; + ["ConfirmWord"] = "Подтвердить"; + ["DeclineWord"] = "Отклонить"; + ["OnlineWord"] = "В сети"; + ["OfflineWord"] = "Не в сети"; + ["SubmitWord"] = "Отправить"; + ["CancelWord"] = "Отмена"; + ["ReportPhrase"] = "Вы можете отправить жалобу нашим модераторам. Мы проверим игру и предпримем соответствующие меры."; + ["CurrencySymbol"] = "$"; + ["RobuxStoreDescription"] = "Приобретите Robux, чтобы покупать новые образы для вашего аватара, а также бонусы и способности в играх."; + ["RobuxStoreNoItemsPhrase"] = "Сейчас в продаже ничего нет. Повторите попытку позже."; + ["PercentMoreRobuxPhrase"] = "Еще %s%%"; + ["RobuxStoreError"] = "Товары, продающиеся за Robux, сейчас недоступны."; + ["NoFriendsPhrase"] = "Друзья не в сети. Сыграйте в игры и заведите новых друзей!"; + ["PopupPartyUIErrorPhrase"] = "При создании команды возникла ошибка. Повторите попытку."; + ["AuthenticationErrorTitle"] = "Ошибка авторизации"; + ["AuthInProgressPhrase"] = "Авторизация уже выполняется"; + ["AuthAccountUnlinkedPhrase"] = "Это особая ситуация, которую нужно разрешить на экране EngagementScreen"; + ["AuthMissingGamePadPhrase"] = "Геймпад не обнаружен. Включите геймпад."; + ["AuthNoUserDetectedPhrase"] = "Пользователей Xbox Live не найдено. Войдите в учетную запись Xbox Live."; + ["AuthHttpErrorDetected"] = "Не удается установить связь с серверами Roblox. Посетите страницу www.roblox.com/help/xbox, чтобы узнать больше."; + ["AuthErrorPhrase"] = "Не удается установить связь с серверами Roblox. Повторите попытку."; + ["NewUserPhrase"] = "Игры, созданные пользователями. Безграничные возможности."; + ["SignInPhrase"] = "Войти"; + ["PlayAsPhrase"] = "Зарегистрироваться с помощью %s"; + ["UsernameWord"] = "Имя пользователя"; + ["PasswordWord"] = "Пароль"; + ["UsernameRulePhrase"] = "3-20 симв. без пробелов"; + ["PasswordRulePhrase"] = "Минимум 6 букв и 2 цифры"; + ["AccountSettingsTitle"] = "Настройки учетной записи"; + ["InvalidUsernameTitle"] = "Недопустимое имя пользователя"; + ["InvalidPasswordTitle"] = "Недопустимый пароль"; + ["InvalidUsernamePhrase"] = "Имя пользователя должно содержать 3-20 символов."; + ["InvalidPasswordPhrase"] = "В пароле должно быть как минимум 6 букв и 2 цифры"; + ["AlreadyTakenTitle"] = "Имя пользователя занято"; + ["AlreadyTakenPhrase"] = "Это имя пользователя уже занято. Попробуйте другое имя."; + ["InvalidCharactersUsedPhrase"] = "Имя пользователя содержит недопустимые символы. Можно использовать только буквы и цифры."; + ["UsernameCannotContainSpacesPhrase"] = "Ваше имя пользователя содержит пробелы. Можно использовать только буквы и цифры."; + ["NoUsernameEnteredPhrase"] = "Требуется указать имя пользователя."; + ["NoUsernameOrPasswordEnteredPhrase"] = "Для создания учетной записи Roblox необходимо ввести допустимое имя пользователя и пароль."; + ["LinkedAsPhrase"] = "Учетная запись %s привязана к вашей учетной записи Roblox, %s."; + ["LinkSignUpDisabled"] = "Регистрация в настоящее время недоступна. Повторите попытку позже."; + ["LinkFlooded"] = "Вы выполнили слишком много регистраций за сегодняшний день. Войдите в существующую учетную запись Roblox или повторите попытку позже."; + ["LinkLeaseLocked"] = "Операция выполняется. Подождите."; + ["LinkAccountLinkingDisabled"] = "Привязка учетной записи в настоящее время недоступна. Повторите попытку позже."; + ["LinkInvalidRobloxUser"] = "Вы ввели недействительное имя пользователя Roblox. Введите правильное имя."; + ["LinkRobloxUserAlreadyLinked"] = "Ваша учетная запись Xbox Live уже привязана к учетной записи Roblox"; + ["LinkXboxUserAlreadyLinked"] = "Ваша учетная запись Xbox Live уже привязана к учетной записи Roblox."; + ["LinkIllegalChildAccountLinking"] = "Нельзя привязать учетную запись. Проверьте возрастные настройки Xbox."; + ["LinkInvalidPassword"] = "Введенный вами пароль недействителен. Введите правильный пароль."; + ["LinkUsernamePasswordNotSet"] = "Вы не ввели имя пользователя или пароль. Укажите имя пользователя и пароль и повторите попытку."; + ["LinkUsernameAlreadyTaken"] = "Это имя пользователя уже занято. Укажите другое имя и повторите попытку."; + ["LinkInvalidCredentials"] = "Введены недопустимое имя пользователя или пароль. Укажите правильные имя пользователя и пароль и повторите попытку."; + ["UserIsGuestAccount"] = "Гостевая учетная запись не может быть авторизована. Войдите в не гостевую учетную запись."; + ["LinkUnknownError"] = "Произошла неизвестная ошибка. Повторите попытку."; + ["LinkAccountTitle"] = "Войдите в Roblox"; + ["LinkAccountPhrase"] = "Войдите в существующую учетную запись Roblox, чтобы получить доступ к настройкам внешности аватара и сохранять прогресс в играх.\n\nВаша учетная запись Roblox будет привязана к учетной записи Xbox Live, и в следующий раз вход будет выполнен автоматически!\n\nВы можете отвязать учетную запись в настройках в любой момент."; + ["SignUpWord"] = "Регистрация"; + ["SignUpTitle"] = "Создайте учетную запись Roblox"; + ["SignUpPhrase"] = "Создав учетную запись, вы получите доступ к Roblox на любой платформе. Введите имя пользователя и пароль, чтобы входить в приложение с любого устройства и продолжать с того места, на котором вы остановились!\n\nВаша новая учетная запись Roblox будет привязана к учетной записи Xbox Live, и в следующий раз вход будет выполнен автоматически!\n\nВы можете отвязать учетную запись в настройках в любой момент."; + ["SetCredentialsTitle"] = "Укажите имя пользователя и пароль"; + ["SetCredentialsPhrase"] = "Кажется, ваша регистрация была прервана! Не беспокойтесь, здесь вы сможете создать учетную запись Roblox, указав имя пользователя и пароль.\n\nВаша учетная запись Roblox будет привязана к учетной записи Xbox Live, и в следующий раз вход будет выполнен автоматически!\n\nВы можете отвязать учетную запись в настройках в любой момент."; + ["SetCredentialsWord"] = "Назначить"; + ["UnlinkTitle"] = "Отвязать учетную запись"; + ["UnlinkGamerTagPhrase"] = "Отвязать %s"; + ["UnlinkPhrase"] = "Отвязать учетную запись Roblox от учетной записи Xbox? Сохраненный прогресс и покупки, связанные с учетной записью Roblox, будут недоступны, пока вы снова в нее не войдете!"; + ["GameplaySettingsTitle"] = "Игровые параметры"; + ["EnabledWord"] = "Включено (рекомендуется)"; + ["DisabledWord"] = "Отключено"; + ["LoadingWord"] = "Загрузка..."; + ["KeepEnabledPhrase"] = "Не отключать"; + ["DisableWord"] = "Отключить"; + ["CrossPlatformGameplayPhrase"] = "Игра с пользователями, у которых другая платформа"; + ["DisableCrossplayOverlayTitle"] = "Отключить возможность игры с пользователями, у которых другая платформа?"; + ["DisableCrossplayOverlayMessage"] = "Точно отключить возможность игры с пользователями, у которых другая платформа?\nЭто не рекомендуется. После этого вы больше не сможете играть с пользователями, у которых другая платформа, и будете видеть меньше пользователей в играх."; + ["EnableCrossplayOverlayTitle"] = "Включена возможность игры с пользователями, у которых другая платформа!"; + ["EnableCrossplayOverlayMessage"] = "Теперь вы можете играть в Roblox на Xbox One с пользователями, у которых другая платформа, в том числе стационарный компьютер, ноутбук, телефон или планшет! Это значит, что вам будет доступно больше игр, а также больше партнеров и соперников — вы сможете играть со ВСЕМИ друзьями, независимо от того, какое устройство они используют."; + ["CrossplayEnabledDescription"] = "Вы сможете присоединяться к играм пользователей, у которых другая платформа, и будете видеть больше пользователей в играх."; + ["CrossplayDisabledDescription"] = "Это не рекомендуется. Вы не сможете присоединяться к играм пользователей, у которых другая платформа, и будете видеть меньше пользователей в играх."; + ["UnableToJoinTitle"] = "Не удалось войти"; + ["ErrorOccurredTitle"] = "Произошла ошибка"; + ["DefaultErrorPhrase"] = "Не удалось подключиться к Roblox. Повторите попытку позже."; + ["DefaultJoinFailPhrase"] = "Не удалось подключиться к Roblox. Повторите попытку позже."; + ["AlreadyRunningPhrase"] = "Вы уже находитесь в этой игре Roblox."; + ["WebServerConnectFailPhrase"] = "Не удалось подключиться к серверам Roblox. Повторите попытку позже."; + ["AccessDeniedByWeb"] = "Игра Roblox, в которую вы пытаетесь войти, в настоящее время недоступна."; + ["InstanceNotFound"] = "Игра Roblox, в которую вы пытаетесь войти, в настоящее время недоступна."; + ["GameFullPhrase"] = "Игра Roblox, в которую вы пытаетесь войти, в настоящее время переполнена."; + ["FollowUserFailed"] = "Игра Roblox, в которую вы пытаетесь войти, в настоящее время недоступна."; + ["InvalidPrivilegeMultiplayerSessionPhrase"] = "Для вашей учетной записи Xbox заблокирована возможность играть в многопользовательском режиме."; + ["InvalidPrivilegeUGCPhrase"] = "Для вашей учетной записи Xbox заблокирована возможность играть в игры, созданные пользователями."; + ["UnableToEquipTitle"] = "Не удалось выбрать"; + ["UnableToEquipPhrase"] = "Не удалось выбрать этот аватар. Повторите попытку позже."; + ["UnableToWearOufitTitle"] = "Не удалось выбрать"; + ["UnableToWearOufitPhrase"] = "Не удалось выбрать этот наряд. Повторите попытку позже."; + ["UnableToDoPurchaseTitle"] = "Не удалось совершить покупку"; + ["UnableToDoPurchasePhrase"] = "Ваша покупка не выполнена. Повторите попытку позже."; + ["UnableToDoRobuxPurchaseTitle"] = "Не удалось совершить покупку"; + ["UnableToDoRobuxPurchasePhrase"] = "Ваша покупка не выполнена. Повторите попытку позже."; + ["CannotVoteTitle"] = "Не удалось проголосовать"; + ["VoteFloodPhrase"] = "Вы голосуете слишком часто. Вернитесь позже и повторите попытку."; + ["VotePlayGamePhrase"] = "Прежде чем проголосовать за игру, необходимо в нее сыграть."; + ["CannotFavoriteTitle"] = "Не удалось добавить игру в избранное"; + ["FavoriteFloodPhrase"] = "Вы слишком часто добавляете игры в избранное. Вернитесь позже и повторите попытку."; + ["ToSPhrase"] = "Прочитать условия"; + ["ToSInfoLinkPhrase"] = "Условия и политика конфиденциальности:\nwww.roblox.com/info/termsofservicen\nwww.roblox.com/info/privacy"; + ["PrivacyPhrase"] = "Конфиденциальность"; + ["VersionIdString"] = "Версия: %s.%s.%s.%s"; + ["ErrorMessageAndCodePrase"] = "%s\nКод ошибки: %d"; + ["UnlockGamesPhrase"] = "Сыграйте в 5 игр из подборки, чтобы получить доступ к другим играм, созданным такими же пользователями, как вы!"; + ["AlertOccurredTitle"] = "Предупреждение"; + ["DefaultAlertPhrase"] = "Не удалось подключиться к Roblox. Повторите попытку позже."; + ["UnlockedUGCTitle"] = "Все игры открыты!"; + ["UnlockedUGCPhrase"] = "Вы сыграли в 5 игр из подборки, и теперь вам доступны все игры, созданные такими же пользователями, как вы!\n\nПерейдите в раздел «ИГРЫ», чтобы увидеть все доступные вам игры!"; + ["PlatformLinkInfoTitle"] = "Добро пожаловать в Roblox!"; + ["PlatformLinkInfoMessage"] = "Мы создали для вас новую учетную запись Roblox. Весь сохраненный прогресс в играх и покупки будут привязаны к этой учетной записи Roblox. Теперь вы сможете войти в приложение на любой платформе и продолжить с того места, на котором остановились!"; + ["ControllerLostConnectionTitle"] = "Не обнаружен контроллер"; + ["ControllerLostConnectionPhrase"] = "Контроллер пользователя %s отключен. Нажмите кнопку «A» на контроллере, который вы хотите использовать."; + ["ActiveUserLostConnectionTitle"] = "Активный пользователь вышел"; + ["ActiveUserLostConnectionPhrase"] = "Пользователь %s вышел. Нажмите кнопку «A» на контроллере, который вы хотите использовать."; + ["PlayMyPlaceMoreGamesTitle"] = "Мои игры"; + ["PlayMyPlaceMoreGamesPhrase"] = "Войдите в учетную запись на сайте Roblox.com, чтобы создавать новые игры и изменять уже созданные!"; + ["PrivateSessionPhrase"] = "В частной игре"; + ["ReauthSignedOutTitle"] = "Вы вышли"; + ["ReauthSignedOutPhrase"] = "Вы вышли из учетной записи Xbox Live. Войдите, чтобы продолжить."; + ["ReauthRemovedTitle"] = "Пользователь изменен"; + ["ReauthRemovedPhrase"] = "Обнаружено изменение активного пользователя. Войдите снова."; + ["ReauthInvalidSessionPhrase"] = "Вы покинули все текущие сеансы игры в Roblox."; + ["ReauthUnlinkTitle"] = "Учетная запись отключена"; + ["ReauthUnlinkPhrase"] = "Вы отвязали свою учетную запись Roblox. Теперь вы можете снова войти в старую учетную запись Roblox или создать новую."; + ["ReauthUnknownPhrase"] = "Произошла ошибка, и вы вышли из учетной записи. Войдите снова, чтобы продолжить игру."; + ["AccountUnder13Phrase"] = "Учетная запись: Не старше 13 лет"; + ["AccountOver13Phrase"] = "Учетная запись: Старше 13 лет"; + ["CPPWelcomeTitle"] = "Добро пожаловать в игру с пользователями, у которых другая платформа!"; + ["CPPWelcomePhrase"] = "Теперь вы можете играть в Roblox на Xbox One с пользователями, у которых другая платформа, в том числе стационарный компьютер, ноутбук, телефон или планшет! Это значит, что вам будет доступно больше игр, а также больше партнеров и соперников — вы сможете играть со ВСЕМИ друзьями, независимо от того, какое устройство они используют. Игровые параметры можно изменить в настройках учетной записи."; + ["MPSRestrictedPhrase"] = "Для вашей учетной записи Xbox заблокирована возможность играть в многопользовательском режиме. Ее можно разблокировать в настройках Xbox или на сайте Xbox.com"; + ["MPSBannedPhrase"] = "Вашей учетной записи Xbox недоступна возможность играть в многопользовательском режиме."; + ["MPSPurchaseRequiredPhrase"] = "Для того, чтобы играть в многопользовательском режиме по сети, необходим золотой статус Xbox Live Gold."; + ["UGCRestrictedPhrase"] = "Для вашей учетной записи Xbox заблокирована возможность играть в игры, созданные пользователями. Ее можно разблокировать в настройках Xbox или на сайте Xbox.com"; + ["UGCBannedPhrase"] = "Вашей учетной записи Xbox недоступна возможность играть в игры, созданные пользователями."; + ["PrivilegeCheckFailPhrase"] = "Для вашей учетной записи Xbox недоступна возможность игры. Повторите попытку позже."; +} + +return locale diff --git a/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-box-shadow.png b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-box-shadow.png new file mode 100644 index 0000000..36f28bc Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-box-shadow.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-box-shadow@2x.png b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-box-shadow@2x.png new file mode 100644 index 0000000..55f90c4 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-box-shadow@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-box-shadow@3x.png b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-box-shadow@3x.png new file mode 100644 index 0000000..e2ee10b Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-box-shadow@3x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-shadow.png b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-shadow.png new file mode 100644 index 0000000..8ee5d0f Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-shadow.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-shadow@2x.png b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-shadow@2x.png new file mode 100644 index 0000000..12a82e2 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-shadow@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-shadow@3x.png b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-shadow@3x.png new file mode 100644 index 0000000..86f1b7c Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-shadow@3x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-white.png b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-white.png new file mode 100644 index 0000000..2e2907f Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-white@2x.png b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-white@2x.png new file mode 100644 index 0000000..6a4dc2b Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-white@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-white@3x.png b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-white@3x.png new file mode 100644 index 0000000..65595be Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-circle-white@3x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-ring-selector.png b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-ring-selector.png new file mode 100644 index 0000000..1934cfd Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-ring-selector.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-ring-selector@2x.png b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-ring-selector@2x.png new file mode 100644 index 0000000..226bc4e Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-ring-selector@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-ring-selector@3x.png b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-ring-selector@3x.png new file mode 100644 index 0000000..68eb6ba Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ColorSelection/gr-ring-selector@3x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-climb-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-climb-on.png new file mode 100644 index 0000000..4eaf010 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-climb-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-climb-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-climb-on@2x.png new file mode 100644 index 0000000..e664c21 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-climb-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-climb.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-climb.png new file mode 100644 index 0000000..d426358 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-climb.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-climb@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-climb@2x.png new file mode 100644 index 0000000..aee3343 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-climb@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-fall-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-fall-on.png new file mode 100644 index 0000000..6cb9c04 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-fall-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-fall-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-fall-on@2x.png new file mode 100644 index 0000000..7348145 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-fall-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-fall.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-fall.png new file mode 100644 index 0000000..556d175 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-fall.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-fall@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-fall@2x.png new file mode 100644 index 0000000..9f83852 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-fall@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-idle-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-idle-on.png new file mode 100644 index 0000000..dfc83a7 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-idle-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-idle-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-idle-on@2x.png new file mode 100644 index 0000000..878c11f Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-idle-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-idle.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-idle.png new file mode 100644 index 0000000..5fef71c Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-idle.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-idle@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-idle@2x.png new file mode 100644 index 0000000..f70c427 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-idle@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-jump-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-jump-on.png new file mode 100644 index 0000000..e5f1ba8 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-jump-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-jump-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-jump-on@2x.png new file mode 100644 index 0000000..de2ba43 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-jump-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-jump.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-jump.png new file mode 100644 index 0000000..7e80c05 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-jump.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-jump@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-jump@2x.png new file mode 100644 index 0000000..1bc6a8f Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-jump@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-run-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-run-on.png new file mode 100644 index 0000000..2a3c735 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-run-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-run-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-run-on@2x.png new file mode 100644 index 0000000..a575ad4 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-run-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-run.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-run.png new file mode 100644 index 0000000..22dc92d Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-run.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-run@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-run@2x.png new file mode 100644 index 0000000..c1571af Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-run@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-swim-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-swim-on.png new file mode 100644 index 0000000..37e33a8 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-swim-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-swim-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-swim-on@2x.png new file mode 100644 index 0000000..4b804a4 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-swim-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-swim.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-swim.png new file mode 100644 index 0000000..9ea0613 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-swim.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-swim@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-swim@2x.png new file mode 100644 index 0000000..87cd8d0 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-swim@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-walk-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-walk-on.png new file mode 100644 index 0000000..5445690 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-walk-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-walk-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-walk-on@2x.png new file mode 100644 index 0000000..f6d7c74 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-walk-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-walk.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-walk.png new file mode 100644 index 0000000..844941c Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-walk.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-walk@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-walk@2x.png new file mode 100644 index 0000000..881306d Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-walk@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-color-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-color-on.png new file mode 100644 index 0000000..a69db70 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-color-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-color-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-color-on@2x.png new file mode 100644 index 0000000..b6a46b7 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-color-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-color.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-color.png new file mode 100644 index 0000000..bc98fd1 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-color.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-color@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-color@2x.png new file mode 100644 index 0000000..1768016 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-color@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-head-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-head-on.png new file mode 100644 index 0000000..170a0e4 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-head-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-head-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-head-on@2x.png new file mode 100644 index 0000000..83a2e38 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-head-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-head.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-head.png new file mode 100644 index 0000000..e4bfeaf Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-head.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-head@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-head@2x.png new file mode 100644 index 0000000..ea136ec Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-head@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-arm-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-arm-on.png new file mode 100644 index 0000000..0b81110 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-arm-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-arm-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-arm-on@2x.png new file mode 100644 index 0000000..7acf06d Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-arm-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-arm.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-arm.png new file mode 100644 index 0000000..3ad53c0 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-arm.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-arm@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-arm@2x.png new file mode 100644 index 0000000..5d1bc27 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-arm@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-leg-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-leg-on.png new file mode 100644 index 0000000..a8fb7de Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-leg-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-leg-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-leg-on@2x.png new file mode 100644 index 0000000..e83211c Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-leg-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-leg.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-leg.png new file mode 100644 index 0000000..b84a563 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-leg.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-leg@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-leg@2x.png new file mode 100644 index 0000000..7e120a2 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-left-leg@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-arm-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-arm-on.png new file mode 100644 index 0000000..6b3f936 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-arm-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-arm-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-arm-on@2x.png new file mode 100644 index 0000000..b961aea Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-arm-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-arm.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-arm.png new file mode 100644 index 0000000..a5bf7cc Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-arm.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-arm@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-arm@2x.png new file mode 100644 index 0000000..68b5c4d Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-arm@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-leg-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-leg-on.png new file mode 100644 index 0000000..cccb516 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-leg-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-leg-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-leg-on@2x.png new file mode 100644 index 0000000..c5597ab Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-leg-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-leg.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-leg.png new file mode 100644 index 0000000..45f8f99 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-leg.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-leg@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-leg@2x.png new file mode 100644 index 0000000..1a0d32b Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-right-leg@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-scaling-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-scaling-on.png new file mode 100644 index 0000000..8b5ed04 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-scaling-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-scaling-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-scaling-on@2x.png new file mode 100644 index 0000000..3bd3941 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-scaling-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-scaling.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-scaling.png new file mode 100644 index 0000000..1a56418 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-scaling.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-scaling@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-scaling@2x.png new file mode 100644 index 0000000..24cf0f4 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-scaling@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-torso-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-torso-on.png new file mode 100644 index 0000000..cefe90a Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-torso-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-torso-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-torso-on@2x.png new file mode 100644 index 0000000..bcba51d Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-torso-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-torso.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-torso.png new file mode 100644 index 0000000..78e9be6 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-torso.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-torso@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-torso@2x.png new file mode 100644 index 0000000..0f729c6 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Body-Part/ic-torso@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-all-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-all-on.png new file mode 100644 index 0000000..0126753 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-all-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-all-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-all-on@2x.png new file mode 100644 index 0000000..1c256b9 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-all-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-all.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-all.png new file mode 100644 index 0000000..8f53a34 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-all.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-all@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-all@2x.png new file mode 100644 index 0000000..4f387ec Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-all@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation-on.png new file mode 100644 index 0000000..ec69768 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation-on@2x.png new file mode 100644 index 0000000..5516914 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation.png new file mode 100644 index 0000000..02bab65 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation@2x.png new file mode 100644 index 0000000..9d99d43 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-body-part-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-body-part-on.png new file mode 100644 index 0000000..ea9f3d8 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-body-part-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-body-part-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-body-part-on@2x.png new file mode 100644 index 0000000..55babcb Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-body-part-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-body-part.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-body-part.png new file mode 100644 index 0000000..3768153 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-body-part.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-body-part@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-body-part@2x.png new file mode 100644 index 0000000..a3b52af Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-body-part@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-clothing-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-clothing-on.png new file mode 100644 index 0000000..1adb966 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-clothing-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-clothing-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-clothing-on@2x.png new file mode 100644 index 0000000..d93ded8 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-clothing-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-clothing.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-clothing.png new file mode 100644 index 0000000..fc3e98c Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-clothing.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-clothing@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-clothing@2x.png new file mode 100644 index 0000000..6acbf1b Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-clothing@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-recent-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-recent-on.png new file mode 100644 index 0000000..a57d106 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-recent-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-recent-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-recent-on@2x.png new file mode 100644 index 0000000..a5c938e Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-recent-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-recent.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-recent.png new file mode 100644 index 0000000..23201a0 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-recent.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-recent@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-recent@2x.png new file mode 100644 index 0000000..e7bd9cd Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Category/ic-recent@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-back-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-back-on.png new file mode 100644 index 0000000..c28d8d8 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-back-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-back-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-back-on@2x.png new file mode 100644 index 0000000..ecf2cce Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-back-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-back.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-back.png new file mode 100644 index 0000000..7189ece Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-back.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-back@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-back@2x.png new file mode 100644 index 0000000..2010924 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-back@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-bundle-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-bundle-on.png new file mode 100644 index 0000000..80ac372 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-bundle-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-bundle-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-bundle-on@2x.png new file mode 100644 index 0000000..28c003d Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-bundle-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-bundle.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-bundle.png new file mode 100644 index 0000000..56cbbce Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-bundle.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-bundle@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-bundle@2x.png new file mode 100644 index 0000000..f48ba4d Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-bundle@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-face-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-face-on.png new file mode 100644 index 0000000..83170ca Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-face-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-face-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-face-on@2x.png new file mode 100644 index 0000000..5aa89d6 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-face-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-face.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-face.png new file mode 100644 index 0000000..c088d33 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-face.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-face@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-face@2x.png new file mode 100644 index 0000000..52f0707 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-face@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-front-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-front-on.png new file mode 100644 index 0000000..ebf8315 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-front-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-front-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-front-on@2x.png new file mode 100644 index 0000000..bb1f3a1 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-front-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-front.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-front.png new file mode 100644 index 0000000..ec6443f Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-front.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-front@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-front@2x.png new file mode 100644 index 0000000..e76cfd3 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-front@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-gear-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-gear-on.png new file mode 100644 index 0000000..4ffc818 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-gear-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-gear-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-gear-on@2x.png new file mode 100644 index 0000000..10ced2f Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-gear-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-gear.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-gear.png new file mode 100644 index 0000000..20b312f Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-gear.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-gear@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-gear@2x.png new file mode 100644 index 0000000..9bcc695 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-gear@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hair-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hair-on.png new file mode 100644 index 0000000..c348b6e Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hair-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hair-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hair-on@2x.png new file mode 100644 index 0000000..12766fb Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hair-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hair.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hair.png new file mode 100644 index 0000000..ce3c5c9 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hair.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hair@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hair@2x.png new file mode 100644 index 0000000..ab6e78e Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hair@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hat-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hat-on.png new file mode 100644 index 0000000..ba31acd Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hat-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hat-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hat-on@2x.png new file mode 100644 index 0000000..27c2d75 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hat-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hat.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hat.png new file mode 100644 index 0000000..c329fd9 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hat.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hat@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hat@2x.png new file mode 100644 index 0000000..ff6bb49 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-hat@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-neck-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-neck-on.png new file mode 100644 index 0000000..18e3465 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-neck-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-neck-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-neck-on@2x.png new file mode 100644 index 0000000..63a7eb9 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-neck-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-neck.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-neck.png new file mode 100644 index 0000000..76e9366 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-neck.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-neck@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-neck@2x.png new file mode 100644 index 0000000..9ef7cba Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-neck@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-pants-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-pants-on.png new file mode 100644 index 0000000..2a657b2 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-pants-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-pants-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-pants-on@2x.png new file mode 100644 index 0000000..a8dab8b Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-pants-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-pants.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-pants.png new file mode 100644 index 0000000..ad78f97 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-pants.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-pants@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-pants@2x.png new file mode 100644 index 0000000..c91497f Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-pants@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shirt-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shirt-on.png new file mode 100644 index 0000000..b08396c Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shirt-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shirt-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shirt-on@2x.png new file mode 100644 index 0000000..6b5ba0c Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shirt-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shirt.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shirt.png new file mode 100644 index 0000000..24a8012 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shirt.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shirt@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shirt@2x.png new file mode 100644 index 0000000..11c763d Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shirt@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shoulder-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shoulder-on.png new file mode 100644 index 0000000..bc407cd Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shoulder-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shoulder-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shoulder-on@2x.png new file mode 100644 index 0000000..f22a70a Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shoulder-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shoulder.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shoulder.png new file mode 100644 index 0000000..d3d3435 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shoulder.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shoulder@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shoulder@2x.png new file mode 100644 index 0000000..1959201 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-shoulder@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-tshirt-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-tshirt-on.png new file mode 100644 index 0000000..d6e5736 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-tshirt-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-tshirt-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-tshirt-on@2x.png new file mode 100644 index 0000000..e1c2245 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-tshirt-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-tshirt.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-tshirt.png new file mode 100644 index 0000000..62b7ad4 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-tshirt.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-tshirt@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-tshirt@2x.png new file mode 100644 index 0000000..fae388d Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-tshirt@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-waist-on.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-waist-on.png new file mode 100644 index 0000000..8e1cc3b Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-waist-on.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-waist-on@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-waist-on@2x.png new file mode 100644 index 0000000..7abbbfb Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-waist-on@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-waist.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-waist.png new file mode 100644 index 0000000..e478dfc Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-waist.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-waist@2x.png b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-waist@2x.png new file mode 100644 index 0000000..a611b5d Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/PageIcons/Clothing/ic-waist@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-head.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-head.png new file mode 100644 index 0000000..1e26ccf Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-head.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-head@2x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-head@2x.png new file mode 100644 index 0000000..2560b93 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-head@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-head@3x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-head@3x.png new file mode 100644 index 0000000..f8cd5d1 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-head@3x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-mid.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-mid.png new file mode 100644 index 0000000..c2c751e Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-mid.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-mid@2x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-mid@2x.png new file mode 100644 index 0000000..32d55f8 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-mid@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-mid@3x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-mid@3x.png new file mode 100644 index 0000000..de31bc7 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-mid@3x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-tail.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-tail.png new file mode 100644 index 0000000..5e21ef0 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-tail.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-tail@2x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-tail@2x.png new file mode 100644 index 0000000..8fda959 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-tail@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-tail@3x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-tail@3x.png new file mode 100644 index 0000000..39b6552 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-empty-tail@3x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-head.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-head.png new file mode 100644 index 0000000..e7366b0 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-head.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-head@2x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-head@2x.png new file mode 100644 index 0000000..df8bde4 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-head@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-head@3x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-head@3x.png new file mode 100644 index 0000000..8db4138 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-head@3x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-mid.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-mid.png new file mode 100644 index 0000000..5990100 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-mid.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-mid@2x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-mid@2x.png new file mode 100644 index 0000000..a4f9403 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-mid@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-mid@3x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-mid@3x.png new file mode 100644 index 0000000..eeb9e7e Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-mid@3x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-tail.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-tail.png new file mode 100644 index 0000000..7c2e1e2 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-tail.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-tail@2x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-tail@2x.png new file mode 100644 index 0000000..a5c4254 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-tail@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-tail@3x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-tail@3x.png new file mode 100644 index 0000000..2879482 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/bar-full-tail@3x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger-highlight.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger-highlight.png new file mode 100644 index 0000000..cc411e0 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger-highlight.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger-highlight@2x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger-highlight@2x.png new file mode 100644 index 0000000..a16b3eb Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger-highlight@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger-highlight@3x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger-highlight@3x.png new file mode 100644 index 0000000..2dcedb7 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger-highlight@3x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger.png new file mode 100644 index 0000000..2a77a56 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger@2x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger@2x.png new file mode 100644 index 0000000..2380f1f Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger@3x.png b/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger@3x.png new file mode 100644 index 0000000..ca7a089 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/Scaling/dragger@3x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-avatareditor-filled.png b/Client2018/content/internal/AvatarEditorIcons/ic-avatareditor-filled.png new file mode 100644 index 0000000..e10168e Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-avatareditor-filled.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-avatareditor.png b/Client2018/content/internal/AvatarEditorIcons/ic-avatareditor.png new file mode 100644 index 0000000..015395c Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-avatareditor.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-back-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-back-white.png new file mode 100644 index 0000000..cfd90b3 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-back-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-back.png b/Client2018/content/internal/AvatarEditorIcons/ic-back.png new file mode 100644 index 0000000..c908590 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-back.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-bundle-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-bundle-white.png new file mode 100644 index 0000000..d7fd3b0 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-bundle-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-bundle.png b/Client2018/content/internal/AvatarEditorIcons/ic-bundle.png new file mode 100644 index 0000000..c1e312d Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-bundle.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-close-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-close-white.png new file mode 100644 index 0000000..af8f7b8 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-close-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-close.png b/Client2018/content/internal/AvatarEditorIcons/ic-close.png new file mode 100644 index 0000000..357b24e Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-close.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-color-filled.png b/Client2018/content/internal/AvatarEditorIcons/ic-color-filled.png new file mode 100644 index 0000000..cdf3b41 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-color-filled.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-color.png b/Client2018/content/internal/AvatarEditorIcons/ic-color.png new file mode 100644 index 0000000..4a291e4 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-color.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-download.png b/Client2018/content/internal/AvatarEditorIcons/ic-download.png new file mode 100644 index 0000000..c8447b5 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-download.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-face-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-face-white.png new file mode 100644 index 0000000..6babd76 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-face-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-face.png b/Client2018/content/internal/AvatarEditorIcons/ic-face.png new file mode 100644 index 0000000..d358cb5 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-face.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-gear-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-gear-white.png new file mode 100644 index 0000000..2f930ce Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-gear-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-gear.png b/Client2018/content/internal/AvatarEditorIcons/ic-gear.png new file mode 100644 index 0000000..c451477 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-gear.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-hat-gray4.png b/Client2018/content/internal/AvatarEditorIcons/ic-hat-gray4.png new file mode 100644 index 0000000..ed9bbf4 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-hat-gray4.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-hat-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-hat-white.png new file mode 100644 index 0000000..9d33f9d Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-hat-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-hat.png b/Client2018/content/internal/AvatarEditorIcons/ic-hat.png new file mode 100644 index 0000000..47c3946 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-hat.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-head-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-head-white.png new file mode 100644 index 0000000..2076462 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-head-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-head.png b/Client2018/content/internal/AvatarEditorIcons/ic-head.png new file mode 100644 index 0000000..f058e54 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-head.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-history-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-history-white.png new file mode 100644 index 0000000..9f3d691 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-history-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-history.png b/Client2018/content/internal/AvatarEditorIcons/ic-history.png new file mode 100644 index 0000000..baaaf82 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-history.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-leftarm-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-leftarm-white.png new file mode 100644 index 0000000..bc191f9 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-leftarm-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-leftarm.png b/Client2018/content/internal/AvatarEditorIcons/ic-leftarm.png new file mode 100644 index 0000000..bc2ba0b Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-leftarm.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-leftleg-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-leftleg-white.png new file mode 100644 index 0000000..d99dc0c Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-leftleg-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-leftleg.png b/Client2018/content/internal/AvatarEditorIcons/ic-leftleg.png new file mode 100644 index 0000000..8deeea2 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-leftleg.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-pant-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-pant-white.png new file mode 100644 index 0000000..ee72099 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-pant-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-pant.png b/Client2018/content/internal/AvatarEditorIcons/ic-pant.png new file mode 100644 index 0000000..db3f6aa Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-pant.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-rewind.png b/Client2018/content/internal/AvatarEditorIcons/ic-rewind.png new file mode 100644 index 0000000..d5b6eda Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-rewind.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-rightarm-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-rightarm-white.png new file mode 100644 index 0000000..0776a3e Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-rightarm-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-rightarm.png b/Client2018/content/internal/AvatarEditorIcons/ic-rightarm.png new file mode 100644 index 0000000..b4667df Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-rightarm.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-rightleg-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-rightleg-white.png new file mode 100644 index 0000000..3709283 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-rightleg-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-rightleg.png b/Client2018/content/internal/AvatarEditorIcons/ic-rightleg.png new file mode 100644 index 0000000..675132d Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-rightleg.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-robux-filled.png b/Client2018/content/internal/AvatarEditorIcons/ic-robux-filled.png new file mode 100644 index 0000000..3989c65 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-robux-filled.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-robux.png b/Client2018/content/internal/AvatarEditorIcons/ic-robux.png new file mode 100644 index 0000000..6ea73b7 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-robux.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-scale-filled.png b/Client2018/content/internal/AvatarEditorIcons/ic-scale-filled.png new file mode 100644 index 0000000..91cb02d Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-scale-filled.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-scale-filled@2x.png b/Client2018/content/internal/AvatarEditorIcons/ic-scale-filled@2x.png new file mode 100644 index 0000000..1117ece Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-scale-filled@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-scale.png b/Client2018/content/internal/AvatarEditorIcons/ic-scale.png new file mode 100644 index 0000000..1a56418 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-scale.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-scale@2x.png b/Client2018/content/internal/AvatarEditorIcons/ic-scale@2x.png new file mode 100644 index 0000000..24cf0f4 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-scale@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-search.png b/Client2018/content/internal/AvatarEditorIcons/ic-search.png new file mode 100644 index 0000000..0f277ce Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-search.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-star-filled.png b/Client2018/content/internal/AvatarEditorIcons/ic-star-filled.png new file mode 100644 index 0000000..05d5085 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-star-filled.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-star.png b/Client2018/content/internal/AvatarEditorIcons/ic-star.png new file mode 100644 index 0000000..d07faa5 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-star.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-tee-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-tee-white.png new file mode 100644 index 0000000..93e5cbb Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-tee-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-tee.png b/Client2018/content/internal/AvatarEditorIcons/ic-tee.png new file mode 100644 index 0000000..bfec67b Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-tee.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-torso-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-torso-white.png new file mode 100644 index 0000000..bdb3423 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-torso-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-torso.png b/Client2018/content/internal/AvatarEditorIcons/ic-torso.png new file mode 100644 index 0000000..5fa3752 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-torso.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-tshirt-white.png b/Client2018/content/internal/AvatarEditorIcons/ic-tshirt-white.png new file mode 100644 index 0000000..e9f7d21 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-tshirt-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ic-tshirt.png b/Client2018/content/internal/AvatarEditorIcons/ic-tshirt.png new file mode 100644 index 0000000..a0cf972 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ic-tshirt.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/btn-expand.png b/Client2018/content/internal/AvatarEditorIcons/ingame/btn-expand.png new file mode 100644 index 0000000..b5659de Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/btn-expand.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gr-card corner.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-card corner.png new file mode 100644 index 0000000..65fb65e Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-card corner.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gr-card corner@2x.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-card corner@2x.png new file mode 100644 index 0000000..875848a Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-card corner@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gr-category-selector.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-category-selector.png new file mode 100644 index 0000000..2a1d997 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-category-selector.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gr-category-selector@2x.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-category-selector@2x.png new file mode 100644 index 0000000..e1a7978 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-category-selector@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gr-corner.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-corner.png new file mode 100644 index 0000000..3ea1eee Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-corner.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gr-half-circle.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-half-circle.png new file mode 100644 index 0000000..73d0fba Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-half-circle.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gr-half-circle@2x.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-half-circle@2x.png new file mode 100644 index 0000000..74b9b55 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-half-circle@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gr-orange-circle.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-orange-circle.png new file mode 100644 index 0000000..be9f4d3 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-orange-circle.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gr-orange-circle@2x.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-orange-circle@2x.png new file mode 100644 index 0000000..779485e Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-orange-circle@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gr-ring01.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-ring01.png new file mode 100644 index 0000000..aadd004 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-ring01.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gr-ring01@2x.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-ring01@2x.png new file mode 100644 index 0000000..e448340 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-ring01@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gr-tail.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-tail.png new file mode 100644 index 0000000..deb2988 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-tail.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gr-tail@2x.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-tail@2x.png new file mode 100644 index 0000000..1a88af0 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gr-tail@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gra-toggle-button.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gra-toggle-button.png new file mode 100644 index 0000000..ae73335 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gra-toggle-button.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gra-toggle-button@2x.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gra-toggle-button@2x.png new file mode 100644 index 0000000..801c426 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gra-toggle-button@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gra-toggle-frame.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gra-toggle-frame.png new file mode 100644 index 0000000..db67fc1 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gra-toggle-frame.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/gra-toggle-frame@2x.png b/Client2018/content/internal/AvatarEditorIcons/ingame/gra-toggle-frame@2x.png new file mode 100644 index 0000000..0ad1a60 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/gra-toggle-frame@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/ic-back-white.png b/Client2018/content/internal/AvatarEditorIcons/ingame/ic-back-white.png new file mode 100644 index 0000000..49b992a Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/ic-back-white.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/ic-back-white@2x.png b/Client2018/content/internal/AvatarEditorIcons/ingame/ic-back-white@2x.png new file mode 100644 index 0000000..1127f35 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/ic-back-white@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/ic-close.png b/Client2018/content/internal/AvatarEditorIcons/ingame/ic-close.png new file mode 100644 index 0000000..31a4ba7 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/ic-close.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/ic-close@2x.png b/Client2018/content/internal/AvatarEditorIcons/ingame/ic-close@2x.png new file mode 100644 index 0000000..1624ef9 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/ic-close@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/ic-up-black.png b/Client2018/content/internal/AvatarEditorIcons/ingame/ic-up-black.png new file mode 100644 index 0000000..f7e2f09 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/ic-up-black.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/ic-up-black@2x.png b/Client2018/content/internal/AvatarEditorIcons/ingame/ic-up-black@2x.png new file mode 100644 index 0000000..499b5f3 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/ic-up-black@2x.png differ diff --git a/Client2018/content/internal/AvatarEditorIcons/ingame/img-selector-box.png b/Client2018/content/internal/AvatarEditorIcons/ingame/img-selector-box.png new file mode 100644 index 0000000..0cf7c68 Binary files /dev/null and b/Client2018/content/internal/AvatarEditorIcons/ingame/img-selector-box.png differ diff --git a/Client2018/content/internal/Chat/.luacheckrc b/Client2018/content/internal/Chat/.luacheckrc new file mode 100644 index 0000000..a10c346 --- /dev/null +++ b/Client2018/content/internal/Chat/.luacheckrc @@ -0,0 +1,52 @@ +stds.roblox = { + read_globals = { + game = { + other_fields = true, + }, + + -- Roblox globals + "script", "workspace", + + -- Extra functions + "tick", "warn", "spawn", + "wait", "settings", + + -- Types + "Vector2", "Vector3", + "Color3", + "UDim", "UDim2", + "Rect", + "CFrame", + "Enum", + "Instance", + "TweenInfo", + "Random", + } +} + +stds.testez = { + read_globals = { + "describe", + "it", "itFOCUS", "itSKIP", + "FOCUS", "SKIP", "HACK_NO_XPCALL", + "expect", + } +} + +ignore = { + "212", -- unused arguments + "421", -- shadowing local variable + "422", -- shadowing argument + "431", -- shadowing upvalue + "432", -- shadowing upvalue argument +} + +std = "lua51+roblox" + +files["**/*.spec.lua"] = { + std = "+testez", +} + +files["**/*Locale.lua"] = { + ignore = { "631" }, --Line is too long +} \ No newline at end of file diff --git a/Client2018/content/internal/Chat/MobileChatStarterScript.lua b/Client2018/content/internal/Chat/MobileChatStarterScript.lua new file mode 100644 index 0000000..8131621 --- /dev/null +++ b/Client2018/content/internal/Chat/MobileChatStarterScript.lua @@ -0,0 +1,17 @@ +-- This is the entry point for the Roblox Chat Program + +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules + +local ChatMaster = require(Modules.ChatMaster) +local LuaErrorReporter = require(Modules.Common.LuaErrorReporter) + +-- start the error observer +-- NOTE - Remove this once we have a shared store across all of the different apps +local ler = LuaErrorReporter.new() +ler:setCurrentApp("Chat") + +-- Start the Lua Chat +local chatMaster = ChatMaster.new() +chatMaster:Start() + diff --git a/Client2018/content/internal/Chat/Modules/ChatMaster.lua b/Client2018/content/internal/Chat/Modules/ChatMaster.lua new file mode 100644 index 0000000..bfcf2a2 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/ChatMaster.lua @@ -0,0 +1,180 @@ +local CoreGui = game:GetService("CoreGui") +local RunService = game:GetService("RunService") +local Players = game:GetService("Players") +local GuiService = game:GetService("GuiService") + +local Modules = CoreGui.RobloxGui.Modules +local LuaChat = Modules.LuaChat +local LuaApp = Modules.LuaApp + +local Rodux = require(Modules.Common.Rodux) + +local AppReducer = require(LuaApp.AppReducer) +local AppState = require(LuaChat.AppState) +local Config = require(LuaApp.Config) +local NotificationType = require(LuaApp.Enum.NotificationType) +local DebugManager = require(LuaChat.Debug.DebugManager) +local Device = require(LuaChat.Device) +local DialogInfo = require(LuaChat.DialogInfo) +local NotificationBroadcaster = require(Modules.LuaChat.NotificationBroadcaster) +local PerformanceTesting = require(LuaApp.PerformanceTesting) +local RobloxEventReceiver = require(Modules.LuaChat.RobloxEventReceiver) + +local SetRoute = require(LuaChat.Actions.SetRoute) +local PopRoute = require(LuaChat.Actions.PopRoute) +local ToggleChatPaused = require(LuaChat.Actions.ToggleChatPaused) + +local Alert = require(LuaChat.Views.Phone.Alert) +local ToastView = require(LuaChat.Views.ToastView) + +local Intent = DialogInfo.Intent +local luaChatDisconnectBackButtonWhenOffScreen = settings():GetFFlag("LuaChatDisconnectBackButtonWhenOffScreen") + +local ChatMaster = {} +ChatMaster.__index = ChatMaster + +ChatMaster.Type = { + Default = "Default", + GameShare = "GameShare", +} + +function ChatMaster.new(roduxStore) + local self = {} + setmetatable(self, ChatMaster) + + if Players.LocalPlayer == nil then + Players.PlayerAdded:Wait() + end + + -- In debug mode, load the DebugManager overlay and logging system + if Config.LuaChat.Debug then + warn("CHAT DEBUG MODE IS ENABLED") + DebugManager:Initialize(CoreGui) + DebugManager:Start() + end + + -- Reduce render quality to optimize performance + if settings():GetFFlag("AppShellManagementRefactor4") then + local renderSteppedConnection = nil + renderSteppedConnection = game:GetService("RunService").RenderStepped:Connect(function() + if renderSteppedConnection then + renderSteppedConnection:Disconnect() + end + settings().Rendering.QualityLevel = 1 + end) + else + settings().Rendering.QualityLevel = 1 + end + + roduxStore = roduxStore or Rodux.Store.new(AppReducer) + + -- Device has to be called before AppState is constructed because constructor for + -- ScreenManager needs to know device type/orientation. ScreenManager is bound to + -- AppState since Views in LuaChat needs to access it directly for GetCurrentView(). + Device.simulatePlatformIfInStudio(roduxStore) + + self._appState = AppState.new(CoreGui, roduxStore) + self._chatRunning = false + self._gameShareRunning = false + + PerformanceTesting:Initialize(self._appState) + + RobloxEventReceiver:init(roduxStore) + self._notificationBroadcaster = NotificationBroadcaster.new(roduxStore) + + do + self._screenGui = Instance.new("ScreenGui") + self._screenGui.DisplayOrder = 9 + self._screenGui.Parent = CoreGui + self._alertView = Alert.new(self._appState) + self._alertView.rbx.Parent = self._screenGui + self._alertView.rbx.Name = "AlertView" + self._toastView = ToastView.new(self._appState) + self._toastView.rbx.Parent = self._screenGui + self._toastView.rbx.Name = "ToastView" + end + + -- Connection for dealing with the Android native back button + self.backButtonConnection = nil + self.onBackButtonPressed = function() + if #self._appState.store:getState().ChatAppReducer.Location.history > 1 then + self._appState.store:dispatch(PopRoute()) + else + GuiService:BroadcastNotification("", NotificationType.BACK_BUTTON_NOT_CONSUMED) + end + end + + return self +end + +function ChatMaster:Start(startType, parameters) + if not startType then + startType = ChatMaster.Type.Default + end + + --pcall since tests run at a lower security context + pcall(function() + RunService:setThrottleFramerateEnabled(Config.General.PerformanceTestingMode == Enum.VirtualInputMode.None) + RunService:Set3dRenderingEnabled(false) + + if luaChatDisconnectBackButtonWhenOffScreen then + self.backButtonConnection = GuiService.ShowLeaveConfirmation:Connect(self.onBackButtonPressed) + end + end) + + self._appState.store:dispatch(ToggleChatPaused(false)) + + if startType == ChatMaster.Type.Default then + + if not next(self._appState.store:getState().ChatAppReducer.Location.current) then + self._appState.store:dispatch(SetRoute(Intent.ConversationHub, {})) + end + self._chatRunning = true + + elseif startType == ChatMaster.Type.GameShare then + + self._appState.store:dispatch(SetRoute(Intent.GameShare, parameters)) + self._gameShareRunning = true + end +end + +function ChatMaster:Stop(stopType) + if not stopType then + stopType = ChatMaster.Type.Default + end + + if stopType == ChatMaster.Type.Default and self._gameShareRunning then + warn('cannot stop chat while share game to chat is running') + return + end + + if stopType == ChatMaster.Type.GameShare and self._chatRunning then + warn('cannot stop share game to chat while chat is running') + return + end + + PerformanceTesting:Stop() + + self._chatRunning = false + self._gameShareRunning = false + + --pcall since tests run at a lower security context + pcall(function() + RunService:setThrottleFramerateEnabled(false) + RunService:Set3dRenderingEnabled(true) + end) + + if luaChatDisconnectBackButtonWhenOffScreen and self.backButtonConnection ~= nil then + self.backButtonConnection:Disconnect() + end + + self._appState.store:dispatch(ToggleChatPaused(true)) +end + +function ChatMaster:Destruct() + -- Doesn't Destruct AppState since the store could be still used elsewhere. + self._screenGui:Destroy() + self._notificationBroadcaster:Destruct() +end + +return ChatMaster diff --git a/Client2018/content/internal/Chat/Modules/ChatMaster.spec.lua b/Client2018/content/internal/Chat/Modules/ChatMaster.spec.lua new file mode 100644 index 0000000..c7043d8 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/ChatMaster.spec.lua @@ -0,0 +1,119 @@ +return function() + local CoreGui = game:GetService("CoreGui") + local Modules = CoreGui.RobloxGui.Modules + + local ChatMaster = require(Modules.ChatMaster) + local Rodux = require(Modules.Common.Rodux) + local AppReducer = require(Modules.LuaApp.AppReducer) + local DialogInfo = require(Modules.LuaChat.DialogInfo) + local RemoveRoute = require(Modules.LuaChat.Actions.RemoveRoute) + local SetRoute = require(Modules.LuaChat.Actions.SetRoute) + + local function closeGameShare(chatMaster) + chatMaster._appState.store:dispatch(RemoveRoute(DialogInfo.Intent.GameShare)) + chatMaster:Stop(ChatMaster.Type.GameShare) + end + + describe("new", function() + it("should create a ChatMaster object", function() + local chatMaster = ChatMaster.new() + + expect(chatMaster).to.be.ok() + end) + + it("should optionally take a rodux store as an argument", function() + local store = Rodux.Store.new(AppReducer) + local chatMaster = ChatMaster.new(store) + + expect(chatMaster).to.be.ok() + end) + end) + + describe("Start", function() + it("should open chat with no arguments", function() + local chatMaster = ChatMaster.new() + chatMaster:Start() + + expect(chatMaster).to.be.ok() + end) + + it("should open Share Game To Chat with valid arguments", function() + local chatMaster = ChatMaster.new() + chatMaster:Start(ChatMaster.Type.GameShare, {placeId = "1818"}) + + expect(chatMaster).to.be.ok() + end) + end) + + describe("Stop", function() + it("should close chat with no arguments", function() + local chatMaster = ChatMaster.new() + chatMaster:Start() + chatMaster:Stop() + expect(chatMaster).to.be.ok() + end) + + it("should close given GameShare as an argument", function() + local chatMaster = ChatMaster.new() + chatMaster:Start(ChatMaster.Type.GameShare, {placeId = "1818"}) + chatMaster:Stop(ChatMaster.Type.GameShare) + expect(chatMaster).to.be.ok() + end) + + it("closing after GameShare should remove GameShare route", function() + local chatMaster = ChatMaster.new() + local appState = chatMaster._appState + + chatMaster:Start(ChatMaster.Type.GameShare, {placeId = "1818"}) + expect(appState.store:getState().ChatAppReducer.Location.current.intent).to.equal(DialogInfo.Intent.GameShare) + closeGameShare(chatMaster) + expect(appState.store:getState().ChatAppReducer.Location.current.intent).to.never.equal(DialogInfo.Intent.GameShare) + end) + end) + + describe("LuaChat and GameShare interaction", function() + it("opening LuaChat after closing GameShare should open the ConversationHub", function() + local chatMaster = ChatMaster.new() + local appState = chatMaster._appState + + chatMaster:Start(ChatMaster.Type.GameShare, {placeId = "1818"}) + expect(appState.store:getState().ChatAppReducer.Location.current.intent).to.equal(DialogInfo.Intent.GameShare) + closeGameShare(chatMaster) + chatMaster:Start(ChatMaster.Type.Default) + expect(appState.store:getState().ChatAppReducer.Location.current.intent).to.equal(DialogInfo.Intent.ConversationHub) + end) + + it("opening GameShare after closing LuaChat should open the GameShare screen", function() + local chatMaster = ChatMaster.new() + local appState = chatMaster._appState + + chatMaster:Start(ChatMaster.Type.Default) + expect(appState.store:getState().ChatAppReducer.Location.current.intent).to.equal(DialogInfo.Intent.ConversationHub) + chatMaster:Stop(ChatMaster.Type.Default) + chatMaster:Start(ChatMaster.Type.GameShare, {placeId = "1818"}) + expect(appState.store:getState().ChatAppReducer.Location.current.intent).to.equal(DialogInfo.Intent.GameShare) + + closeGameShare(chatMaster) + chatMaster:Start(ChatMaster.Type.Default) + expect(appState.store:getState().ChatAppReducer.Location.current.intent).to.equal(DialogInfo.Intent.ConversationHub) + end) + + it("closing GameShare should preserve the user's location history", function() + local chatMaster = ChatMaster.new() + local appState = chatMaster._appState + + chatMaster:Start() + appState.store:dispatch(SetRoute(DialogInfo.Intent.Conversation, {conversationId = "1"})) + chatMaster:Stop() + + local conversationLocation = appState.store:getState().ChatAppReducer.Location.current + expect(conversationLocation).to.be.ok() + + chatMaster:Start(ChatMaster.Type.GameShare, {placeId = "1818"}) + closeGameShare(chatMaster) + + chatMaster:Start() + expect(appState.store:getState().ChatAppReducer.Location.current).to.equal(conversationLocation) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/AddUser.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/AddUser.lua new file mode 100644 index 0000000..c067298 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/AddUser.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(user) + return { + user = user, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ChangedParticipants.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ChangedParticipants.lua new file mode 100644 index 0000000..2c60845 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ChangedParticipants.lua @@ -0,0 +1,14 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(id, participants, title, lastUpdated) + return { + conversationId = id, + participants = participants, + title = title, + lastUpdated = lastUpdated, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ConversationActions.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ConversationActions.lua new file mode 100644 index 0000000..6eefe4e --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ConversationActions.lua @@ -0,0 +1,1057 @@ +local Players = game:GetService("Players") +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local Functional = require(Common.Functional) +local WebApi = require(LuaChat.WebApi) +local DateTime = require(LuaChat.DateTime) +local MockId = require(LuaApp.MockId) +local Constants = require(LuaChat.Constants) +local Alert = require(LuaChat.Models.Alert) +local ToastModel = require(LuaChat.Models.ToastModel) +local ConversationModel = require(LuaChat.Models.Conversation) +local UserModel = require(LuaApp.Models.User) +local getConversationDisplayTitle = require(LuaChat.Utils.getConversationDisplayTitle) +local reportToDiagByCountryCode = require(LuaChat.Utils.reportToDiagByCountryCode) +local truncateAssetLink = require(LuaChat.Utils.truncateAssetLink) + +local AddUser = require(LuaApp.Actions.AddUser) +local ChangedParticipants = require(LuaChat.Actions.ChangedParticipants) +local DecrementUnreadConversationCount = require(LuaChat.Actions.DecrementUnreadConversationCount) +local FetchingOlderMessages = require(LuaChat.Actions.FetchingOlderMessages) +local FetchedOldestMessage = require(LuaChat.Actions.FetchedOldestMessage) +local GetFriendCount = require(LuaChat.Actions.GetFriendCount) +local RequestAllFriends = require(LuaChat.Actions.RequestAllFriends) +local ReceivedAllFriends = require(LuaChat.Actions.ReceivedAllFriends) +local IncrementUnreadConversationCount = require(LuaChat.Actions.IncrementUnreadConversationCount) +local MessageFailedToSend = require(LuaChat.Actions.MessageFailedToSend) +local MessageModerated = require(LuaChat.Actions.MessageModerated) +local ReadConversation = require(LuaChat.Actions.ReadConversation) +local ReceivedConversation = require(LuaChat.Actions.ReceivedConversation) +local ReceivedOldestConversation = require(LuaChat.Actions.ReceivedOldestConversation) +local RequestPageConversations = require(LuaChat.Actions.RequestPageConversations) +local ReceivedPageConversations = require(LuaChat.Actions.ReceivedPageConversations) +local ReceivedMessages = require(LuaChat.Actions.ReceivedMessages) +local RequestLatestMessages = require(LuaChat.Actions.RequestLatestMessages) +local ReceivedLatestMessages = require(LuaChat.Actions.ReceivedLatestMessages) +local RequestUserPresence = require(LuaChat.Actions.RequestUserPresence) +local ReceivedUserPresence = require(LuaChat.Actions.ReceivedUserPresence) +local RemovedConversation = require(LuaChat.Actions.RemovedConversation) +local RenamedGroupConversation = require(LuaChat.Actions.RenamedGroupConversation) +local SendingMessage = require(LuaChat.Actions.SendingMessage) +local SentMessage = require(LuaChat.Actions.SentMessage) +local SetConversationLoadingStatus = require(LuaChat.Actions.SetConversationLoadingStatus) +local SetUnreadConversationCount = require(LuaChat.Actions.SetUnreadConversationCount) +local SetUserIsFriend = require(LuaApp.Actions.SetUserIsFriend) +local ShowAlert = require(LuaChat.Actions.ShowAlert) +local ShowToast = require(LuaChat.Actions.ShowToast) +local SetUserLeavingConversation = require(LuaChat.Actions.SetUserLeavingConversation) +local ShareGameToChatThunks = require(LuaChat.Actions.ShareGameToChatFromChat.ShareGameToChatFromChatThunks) + +local LuaChatUseNewFriendsAndPresenceEndpoint = settings():GetFFlag("LuaChatUseNewFriendsAndPresenceEndpoint") +local LuaChatPerformanceTracking = settings():GetFFlag("LuaChatPerformanceTracking") + +local LuaChatCreateChatEnabled = settings():GetFFlag("LuaChatCreateChatEnabled") +local FFlagShareGameToChatStatusAnalytics = settings():GetFFlag("ShareGameToChatStatusAnalytics") + +local Promise = require(LuaApp.Promise) + +local GET_MESSAGES_PAGE_SIZE = Constants.PageSize.GET_MESSAGES + +local lastAscendingNumber = 0 + +local function getAscendingNumber() + lastAscendingNumber = lastAscendingNumber + 1 + return lastAscendingNumber +end + +local ConversationActions = {} + +local function processConversations(store, status, result) + local state = store:getState() + + if status ~= WebApi.Status.OK then + warn("WebApi failure in processConversation, Status: "..tostring(status)) + return + end + + local conversations = result.conversations + local users = result.users + + local convoIds = {} + local userIds = {} + for _, convo in ipairs(conversations) do + store:dispatch(ReceivedConversation(convo)) + table.insert(convoIds, convo.id) + end + + for _, user in pairs(users) do + if state.Users[user.id] == nil then + store:dispatch(AddUser(user)) + table.insert(userIds, user.id) + end + end + + store:dispatch(ConversationActions.GetLatestMessages(convoIds)) + store:dispatch(ConversationActions.GetUserPresences(userIds)) +end + +local function getUserConversations(store, pageNumber, pageSize) + local status, result = WebApi.GetUserConversations(pageNumber, pageSize) + + if status ~= WebApi.Status.OK then + return status, result + end + + if #result.conversations < pageSize then + store:dispatch(ReceivedOldestConversation(true)) + store:dispatch(ConversationActions.CreateMockOneOnOneConversationsAsync()) + end + + return status, result +end + +local function shouldFetchPageConversations(state) + if state.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching then + return false + end + return true +end + +function ConversationActions.GetLocalUserConversationsAsync(pageNumber, pageSize) + return function(store) + if not shouldFetchPageConversations(store:getState()) then + return Promise.new(function(resolve) resolve() end) + end + -- sets status in state we are fetching pages + store:dispatch(RequestPageConversations()) + + return Promise.new(function(resolve) + local status, result = getUserConversations(store, pageNumber, pageSize) + processConversations(store, status, result) + store:dispatch(ReceivedPageConversations()) + + resolve() + end) + end +end + +local function refreshMessages(conversationId, store) + --Returns true if their are no new messages + local status, messages = WebApi.GetMessages(conversationId, 1) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in refreshMessages", status) + return true + end + + local conversation = store:getState().ChatAppReducer.Conversations[conversationId] + if not conversation then + return false + end + + local hasNewMessages = false + if conversation.messages:Length() > 0 then + local mostRecentKnown = conversation.messages:Last().sent:GetUnixTimestamp() + for _, message in ipairs(messages) do + if message.sent:GetUnixTimestamp() > mostRecentKnown then + hasNewMessages = true + break + end + end + else + hasNewMessages = #messages > 0 + end + + if hasNewMessages then + local newMessageNotificationReceivedLocalTime = tick() + store:dispatch( + ConversationActions.GetNewMessages( + conversationId, + false, + newMessageNotificationReceivedLocalTime + ) + ) + end + + return not hasNewMessages +end + +local function hasSameParticipants(existingConvo, newConvo) + --O(n^2), but n is at most 6! + if #existingConvo.participants ~= #newConvo.participants then + return false + end + for _, existingPart in ipairs(existingConvo.participants) do + local found = false + for _, newPart in ipairs(newConvo.participants) do + if existingPart == newPart then + found = true + break + end + end + if not found then + return false + end + end + return true +end + +local function refreshConversations(pageNumber, store) + + local status, result = getUserConversations(store, pageNumber, Constants.PageSize.GET_CONVERSATIONS) + if status ~= WebApi.Status.OK then + warn("WebApi failure in WebApi.GetUserConversations") + return + end + + local state = store:getState() + local conversations = result.conversations + local users = result.users + + for _, user in pairs(users) do + if state.Users[user.id] == nil then + store:dispatch(AddUser(user)) + end + end + + local needInitialMessages = {} + + for _, convo in ipairs(conversations) do + local convoIsIdentical = true + if state.ChatAppReducer.Conversations[convo.id] then + local existing = state.ChatAppReducer.Conversations[convo.id] + if existing.title ~= convo.title then + convoIsIdentical = false + store:dispatch(RenamedGroupConversation(convo.id, convo.title)) + end + if not hasSameParticipants(convo, existing) then + convoIsIdentical = false + store:dispatch(ChangedParticipants(convo.id, convo.participants)) + end + if not existing.fetchingOlderMessages then + convoIsIdentical = convoIsIdentical and refreshMessages(existing.id, store) + end + else + convoIsIdentical = false + store:dispatch(ReceivedConversation(convo)) + table.insert(needInitialMessages, convo.id) + end + if convoIsIdentical then + return + end + end + + --We're going to continue getting conversations + --until we run into one that hasn't changed. This, + --potentially, means that we're getting all conversations, + --if all conversations have changed. + if #conversations == Constants.PageSize.GET_CONVERSATIONS then + refreshConversations(pageNumber+1, store) + end +end + +function ConversationActions.StartOneToOneConversation(conversation, onSuccess) + return function(store) + spawn(function() + local userId = nil + for _, participantId in ipairs(conversation.participants) do + if participantId ~= tostring(Players.LocalPlayer.UserId) then + userId = participantId + end + end + local status, result = WebApi.StartOneToOneConversation(userId, conversation.clientId) + if status ~= WebApi.Status.OK then + warn("WebApi failure in StartOneToOneConversation, status:", status) + return + end + + --The StartOneToOneConversation API endpoint returns a conversation with a null + --title. Have to call GetConversation to get correct data. + status, result = WebApi.GetConversations({result.id}) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in GetConversations, status:", status) + return + end + + if #result.conversations <= 0 then + warn("WebApi invalid result from GetConversations") + return + end + + local serverConversation = result.conversations[1] + store:dispatch(ReceivedConversation(serverConversation)) + + store:dispatch(RemovedConversation(conversation.id)) + if onSuccess then + if LuaChatCreateChatEnabled then + onSuccess(serverConversation.id) + else + onSuccess(serverConversation) + end + end + end) + end +end + +function ConversationActions.CreateMockOneOnOneConversationsAsync() + return function(store) + local onFetchedAllFriends = function() + local state = store:getState() + local needsMockConversation = {} + for userId, user in pairs(state.Users) do + if user.isFriend then + needsMockConversation[userId] = user + end + end + + for _, conversation in pairs(state.ChatAppReducer.Conversations) do + if conversation.conversationType == ConversationModel.Type.ONE_TO_ONE_CONVERSATION then + for _, userId in ipairs(conversation.participants) do + needsMockConversation[userId] = nil + end + end + end + + for _, user in pairs(needsMockConversation) do + local conversation = ConversationModel.fromUser(user) + store:dispatch(ReceivedConversation(conversation)) + end + end + + return Promise.new(function(resolve) + store:dispatch(ConversationActions.GetAllFriendsAsync()) + onFetchedAllFriends() + resolve() + end) + end +end + +function ConversationActions.RefreshConversations() + return function(store) + local state = store:getState() + if next(state.ChatAppReducer.Conversations) == nil then + spawn(function() + store:dispatch(ConversationActions.GetLocalUserConversationsAsync(1, Constants.PageSize.GET_CONVERSATIONS)) + end) + else + spawn(function() + refreshConversations(1, store) + end) + end + end +end + +function ConversationActions.GetConversations(convoIds) + return function(store) + local status, result = WebApi.GetConversations(convoIds) + + processConversations(store, status, result) + return status + end +end + +local function shouldGetAllFriends(state) + if state.UsersAsync.allFriendsIsFetching then + return false + end + return true +end + +function ConversationActions.GetAllFriendsAsync() + return function(store) + if not shouldGetAllFriends(store:getState()) then + return Promise.new(function(resolve) resolve() end) + end + --marks in store we are fetching + store:dispatch(RequestAllFriends()) + + if LuaChatUseNewFriendsAndPresenceEndpoint then + -- New endpoint lets us pass in a uid to get one page of all of a user's friends. + return Promise.new(function(resolve, reject) + -- We cannot use LocalUserId here unless LuaAppStarterScript is enabled. + local localUserId = tostring(Players.LocalPlayer.UserId) + local status, result = WebApi.GetFriends(localUserId) + if status == WebApi.Status.OK then + local state = store:getState() + + local friendsWhoNeedPresence = {} + for friendUserId, user in pairs(result) do + if state.Users[friendUserId] == nil then + store:dispatch(AddUser(user)) + table.insert(friendsWhoNeedPresence, friendUserId) + else + store:dispatch(SetUserIsFriend(friendUserId, user.isFriend)) + end + end + + local presenceStatus, presenceResult = WebApi.GetUserPresences(friendsWhoNeedPresence) + if presenceStatus == WebApi.Status.OK then + for userId, presenceModel in pairs(presenceResult) do + store:dispatch(ReceivedUserPresence( + userId, presenceModel.presence, presenceModel.lastLocation, presenceModel.placeId + )) + end + end + + store:dispatch(ReceivedAllFriends()) + resolve(status, result) + else + reject(status, result) + end + end) + else + -- Continue using the old endpoint. We need to do extra work by fetching the amount + -- of friends a user has to determine if we've fetched the final friend page. + return Promise.new(function(resolve) + local state = store:getState() + local getFriendCountStatus, totalCount = WebApi.GetFriendCount() + if getFriendCountStatus ~= WebApi.Status.OK then + return + end + local count = 0 + local page = 1 + local needsPresence = {} + while count < totalCount do + local getFriendsStatus, result = WebApi.GetFriends(page) + page = page + 1 + if getFriendsStatus ~= WebApi.Status.OK then + return + end + + local lastCount = count + for userId, user in pairs(result) do + count = count + 1 + if state.Users[userId] == nil then + store:dispatch(AddUser(user)) + table.insert(needsPresence, userId) + else + store:dispatch(SetUserIsFriend(userId, user.isFriend)) + end + end + if lastCount == count then + return + end + end + + store:dispatch(ReceivedAllFriends()) + store:dispatch(ConversationActions.GetUserPresences(needsPresence)) + resolve() + end) + end + end +end + +function ConversationActions.FriendshipCreated(userId) + return function(store) + spawn(function() + local status, result = WebApi.GetUser(userId) + if status ~= WebApi.Status.OK then + warn("WebApi.GetUser failure with status", status, " for user id", userId) + return + end + + -- request updated friend count when new friendship is formed + store:dispatch(GetFriendCount()) + + local user = UserModel.fromData(userId, result.Username, true) + store:dispatch(AddUser(user)) + store:dispatch(ConversationActions.GetUserPresences({userId})) + + local state = store:getState() + + local needsMockConversation = true + for _, conversation in pairs(state.ChatAppReducer.Conversations) do + if conversation.conversationType == ConversationModel.Type.ONE_TO_ONE_CONVERSATION then + for _, participantId in ipairs(conversation.participants) do + if participantId == userId then + needsMockConversation = false + break + end + end + end + end + + if needsMockConversation then + local conversation = ConversationModel.fromUser(user) + store:dispatch(ReceivedConversation(conversation)) + end + end) + end +end + +function ConversationActions.GetAllUserPresences() + return function(store) + spawn(function() + local users = store:getState().Users; + local userIds = {} + for userId, _ in pairs(users) do + table.insert(userIds, userId) + end + store:dispatch(ConversationActions.GetUserPresences(userIds)) + end) + end +end + +local function shouldFetchUserPresences(state, userIds) + local filteredUserIds = Functional.Filter(userIds, function(userId) + local userAS = state.UsersAsync[userId] + if userAS and userAS.presenceIsFetching then + return false + end + return true + end) + + if #filteredUserIds == 0 then + return false, filteredUserIds + end + + return true, filteredUserIds +end + +function ConversationActions.GetUserPresences(userIds) + return function(store) + local ret, newUserIds = shouldFetchUserPresences(store:getState(), userIds) + if not ret then + return + end + + for _, v in ipairs(newUserIds) do + store:dispatch(RequestUserPresence(v)) + end + + spawn(function() + local status, result = WebApi.GetUserPresences(newUserIds) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in GetUserPresences") + return + end + + for userId, result in pairs(result) do + store:dispatch(ReceivedUserPresence(userId, result.presence, result.lastLocation, result.placeId)) + end + end) + end +end + +local function shouldFetchLatestMessages(state) + if state.ChatAppReducer.ConversationsAsync.latestMessagesIsFetching then + return false + end + return true +end + +function ConversationActions.GetLatestMessages(convoIds) + return function(store) + if not shouldFetchLatestMessages(store:getState()) then + return + end + + store:dispatch(RequestLatestMessages()) + + spawn(function() + local status, messages = WebApi.GetLatestMessages(convoIds) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in GetLatestMessages") + return + end + + local state = store:getState() + for _, message in ipairs(messages) do + local conversation = state.ChatAppReducer.Conversations[message.conversationId] + if conversation.messages:Get(message.id) == nil then + if conversation.messages:Last() ~= nil then + message.previousMessageId = conversation.messages:Last().id + end + store:dispatch(ReceivedMessages(message.conversationId, {message})) + end + end + + store:dispatch(ReceivedLatestMessages()) + end) + end +end + +function ConversationActions.GetNewMessages(convoId, fromSelf, newMessageNotificationReceivedLocalTime) + local function ConversationContainsOldest(conversation, messages) + return #messages > 0 and conversation.messages:Get(messages[#messages].id) ~= nil + end + + return function(store) + spawn(function() + local conversation = store:getState().ChatAppReducer.Conversations[convoId] + if not conversation then + -- If we have not previously cached the conversation, we should first get it. + local status = store:dispatch(ConversationActions.GetConversations({convoId})) + if status ~= WebApi.Status.OK then + warn("WebApi failure in GetNewMessages") + return + end + conversation = store:getState().ChatAppReducer.Conversations[convoId] + if not conversation then + warn("Was not able to GetConversation in GetNewMessages") + return + end + end + + local pageSize = Constants.PageSize.GET_NEW_MESSAGES + local status, messages = WebApi.GetMessages(convoId, pageSize) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in GetNewMessages") + return + end + + local getNewMessageRoundTripTime = tick() - newMessageNotificationReceivedLocalTime + if LuaChatPerformanceTracking then + reportToDiagByCountryCode( + Constants.PerformanceMeasurement.LUA_CHAT_RECEIVE_MESSAGE, + "MessageReceivedTime", + getNewMessageRoundTripTime + ) + end + -- Did many messages get sent at once? + -- We may have missed something and need to get catched up + local lastMessage = conversation and conversation.messages:Last() + if messages[pageSize] and lastMessage and not ConversationContainsOldest(conversation, messages) then + repeat + pageSize = math.min(2 * pageSize, 50) + local exclusiveMessageStartId = messages[#messages].id + local moreStatus, moreMessages = WebApi.GetMessages(convoId, pageSize, exclusiveMessageStartId) + if moreStatus ~= WebApi.Status.OK then + warn("WebApi failure in GetNewMessages") + return + end + for _, message in ipairs(moreMessages) do + table.insert(messages, message) + end + until ConversationContainsOldest(conversation, messages) or moreMessages[pageSize] == nil + end + + -- We got a bunch of extra messages in these request + -- So don't update the ones we already knew about + local hasUnreadMessages = false + if conversation then + local filteredMessages = {} + local previousMessageId = nil + for i = #messages, 1, -1 do + local message = messages[i] + if not conversation.messages:Get(message.id) then + hasUnreadMessages = hasUnreadMessages or (not message.read) + message.previousMessageId = previousMessageId + table.insert(filteredMessages, message) + end + previousMessageId = message.id + end + messages = filteredMessages + end + + local shouldMarkConversationUnread = (not conversation.hasUnreadMessages) + and (not fromSelf) and hasUnreadMessages + + store:dispatch(ReceivedMessages(convoId, messages, shouldMarkConversationUnread)) + + if shouldMarkConversationUnread then + store:dispatch(IncrementUnreadConversationCount()) + end + end) + end +end + +function ConversationActions.GetInitialMessages(convoId, exclusiveMessageStartId) + return function(store) + store:dispatch(SetConversationLoadingStatus(convoId, Constants.ConversationLoadingState.LOADING)) + + spawn(function() + local status, messages = WebApi.GetMessages(convoId, GET_MESSAGES_PAGE_SIZE, exclusiveMessageStartId) + + if status ~= WebApi.Status.OK then + return + end + + store:dispatch(ReceivedMessages(convoId, messages)) + + store:dispatch(SetConversationLoadingStatus(convoId, Constants.ConversationLoadingState.DONE)) + + end) + end +end + +function ConversationActions.RemoveUserFromConversation(userId, convoId, callback) + return function(store) + local conversation = store:getState().ChatAppReducer.Conversations[convoId] + if conversation and not conversation.isUserLeaving then + store:dispatch(SetUserLeavingConversation(convoId, true)) + spawn(function() + local status = WebApi.RemoveUserFromConversation(userId, convoId) + + if status ~= WebApi.Status.OK then + warn("WebApi.RemoveUserFromConversation failure", status) + local updatedConversation = store:getState().ChatAppReducer.Conversations[convoId] + if userId == tostring(Players.LocalPlayer.UserId) then + local titleKey = "Feature.Chat.Heading.FailedToLeaveGroup" + local messageKey = "Feature.Chat.Message.FailedToLeaveGroup" + local messageArguments = { + CONVERSATION_TITLE = getConversationDisplayTitle(updatedConversation), + } + local alert = Alert.new(titleKey, messageKey, messageArguments, Alert.AlertType.DIALOG) + store:dispatch(ShowAlert(alert)) + else + local user = store:getState().Users[userId] + local titleKey = "Feature.Chat.Heading.FailedToRemoveUser" + local messageKey = "Feature.Chat.Message.FailedToRemoveUser" + local messageArguments = { + CONVERSATION_TITLE = getConversationDisplayTitle(updatedConversation), + USERNAME = user.name, + } + local alert = Alert.new(titleKey, messageKey, messageArguments, Alert.AlertType.DIALOG) + store:dispatch(ShowAlert(alert)) + end + end + if callback then + callback(status == WebApi.Status.OK) + end + store:dispatch(SetUserLeavingConversation(convoId, false)) + end) + end + end +end + +function ConversationActions.RenameGroupConversation(convoId, newName, callback) + return function(store) + spawn(function() + + local status = WebApi.RenameGroupConversation(convoId, newName) + + if status == WebApi.Status.MODERATED then + warn("Message was moderated") + local messageKey = "Feature.Chat.Response.ChatNameFullyModerated" + local toastModel = ToastModel.new(Constants.ToastIDs.GROUP_NAME_MODERATED, messageKey) + store:dispatch(ShowToast(toastModel)) + elseif status ~= WebApi.Status.OK then + local conversation = store:getState().ChatAppReducer.Conversations[convoId] + local titleKey = "Feature.Chat.Heading.FailedToRenameConversation" + local messageKey = "Feature.Chat.Message.FailedToRenameConversation" + local messageArguments = { + EXISTING_NAME = getConversationDisplayTitle(conversation), + NEW_NAME = newName, + } + local alert = Alert.new(titleKey, messageKey, messageArguments, Alert.AlertType.DIALOG) + store:dispatch(ShowAlert(alert)) + end + + if callback then + callback() + end + end) + end +end + +local function SendMessageHelper(store, conversationId, messageText, isSharedGameUrl) + local conversation = store:GetState().ChatAppReducer.Conversations[conversationId] + + local function GetSpoofedLatestMessageTime() + -- Get the most recent message date of our messages so we can create a fake date after those + local lastMessageInConvo = conversation.messages:Last() + local lastSendingMessageInConvo = conversation.sendingMessages:Last() + + local lastSendingDate; + if lastMessageInConvo then + lastSendingDate = lastMessageInConvo.sent:GetUnixTimestamp() + end + if lastSendingMessageInConvo then + local tempDate = lastSendingMessageInConvo.sent:GetUnixTimestamp() + lastSendingDate = lastSendingDate and math.max(lastSendingDate, tempDate) or tempDate + end + + -- Add 0.001 seconds to the message date so that we show up slightly after the current one + local fakeSendingDate = lastSendingDate and DateTime.fromUnixTimestamp(lastSendingDate + 0.001) or DateTime.now() + return fakeSendingDate + end + + local previousMessageId = nil + + local message = { + id = "sending-message-" .. MockId(), + order = getAscendingNumber(), + content = messageText, + conversationId = conversationId, + senderTargetId = tostring(Players.LocalPlayer.UserId), + senderType = "User", + sent = GetSpoofedLatestMessageTime(), + moderated = false, + failed = false, + previousMessageId = previousMessageId, + } + + if not isSharedGameUrl then + store:dispatch(SendingMessage(conversationId, message)) + end + + --Making the assumption that when a message is sent, there are no new messages + --not already in the store... potential race condition + local status, result = WebApi.SendMessage(conversationId, messageText, previousMessageId) + return conversation, message, status, result +end + +if FFlagShareGameToChatStatusAnalytics then + function ConversationActions.SendMessage(conversationId, messageText, messageSentLocalTime) + return function(store) + return Promise.new(function(resolve) + spawn(function() + local conversation, message, status, result = SendMessageHelper(store, conversationId, messageText, false) + + if status == WebApi.Status.OK then + local sendMessageRoundTripTime = tick() - messageSentLocalTime + if LuaChatPerformanceTracking then + reportToDiagByCountryCode( + Constants.PerformanceMeasurement.LUA_CHAT_SEND_MESSAGE, + "MessageSentTime", + sendMessageRoundTripTime + ) + end + if conversation.messages:Length() > 0 then + result.previousMessageId = conversation.messages:Last().id + end + store:dispatch(SentMessage(conversationId, message.id)) + + store:dispatch(ReceivedMessages(conversationId, {result})) + elseif status == WebApi.Status.MODERATED then + store:dispatch(MessageModerated(conversationId, message.id)) + warn("Message was moderated.") + else + store:dispatch(MessageFailedToSend(conversationId, message.id)) + warn("Message could not be sent.") + end + + resolve(status) + end) + end) + end + end +else + function ConversationActions.SendMessage(conversationId, messageText, toastModeratedMessageKey, messageSentLocalTime) + return function(store) + spawn(function() + local conversation, message, status, result = SendMessageHelper(store, conversationId, messageText, false) + + if status == WebApi.Status.OK then + local sendMessageRoundTripTime = tick() - messageSentLocalTime + if LuaChatPerformanceTracking then + reportToDiagByCountryCode( + Constants.PerformanceMeasurement.LUA_CHAT_SEND_MESSAGE, + "MessageSentTime", + sendMessageRoundTripTime + ) + end + if conversation.messages:Length() > 0 then + result.previousMessageId = conversation.messages:Last().id + end + store:dispatch(SentMessage(conversationId, message.id)) + + store:dispatch(ReceivedMessages(conversationId, {result})) + elseif status == WebApi.Status.MODERATED then + store:dispatch(MessageModerated(conversationId, message.id)) + warn("Message was moderated.") + + if toastModeratedMessageKey and toastModeratedMessageKey:len() > 0 then + local toastModel = ToastModel.new(Constants.ToastIDs.MESSAGE_WAS_MODERATED, toastModeratedMessageKey) + store:dispatch(ShowToast(toastModel)) + end + else + store:dispatch(MessageFailedToSend(conversationId, message.id)) + warn("Message could not be sent.") + end + end) + end + end +end + +function ConversationActions.ShareGame(conversationId, gameUrl) + return function(store) + spawn(function() + if store:GetState().ChatAppReducer.ShareGameToChatAsync.sharingGame or + store:GetState().ChatAppReducer.ShareGameToChatAsync.sharedGame then + return + end + + ShareGameToChatThunks.Sharing(store) + local messageText = truncateAssetLink(gameUrl) + local conversation, message, status, result = SendMessageHelper(store, conversationId, messageText, true) + + if status == WebApi.Status.OK then + if conversation.messages:Length() > 0 then + result.previousMessageId = conversation.messages:Last().id + end + store:dispatch(SentMessage(conversationId, message.id)) + + store:dispatch(ReceivedMessages(conversationId, {result})) + + ShareGameToChatThunks.Shared(store) + elseif status == WebApi.Status.MODERATED then + ShareGameToChatThunks.FailedToShare(store) + warn("game was moderated.") + else + ShareGameToChatThunks.FailedToShare(store) + warn("game could not be sent.") + end + end) + end +end + +function ConversationActions.CreateConversation(conversation, callback) + return function(store) + spawn(function() + if LuaChatCreateChatEnabled then + if #conversation.participants == 1 then + store:dispatch(ConversationActions.StartOneToOneConversation(conversation,callback)) + else + local status, realConversation = WebApi.StartGroupConversation(conversation) + if status == WebApi.Status.OK then + store:dispatch(ReceivedConversation(realConversation)) + if callback then + callback(realConversation.id) + end + else + warn("Conversation could not be created, status:", status) + if callback then + callback(nil) + end + end + end + else + local status, realConversation = WebApi.StartGroupConversation(conversation) + if status == WebApi.Status.OK then + if realConversation.isDefaultTitle and #conversation.title > 0 then + --When calling the StartGroupConversation endpoint, + --No explicit feedback regarding whether or not conversation + --title was moderated, have to infer like this + warn("Group name was moderated") + local messageKey = "Feature.Chat.Response.ChatNameFullyModerated" + local toastModel = ToastModel.new(Constants.ToastIDs.GROUP_NAME_MODERATED, messageKey) + store:dispatch(ShowToast(toastModel)) + end + store:dispatch(ReceivedConversation(realConversation)) + if callback then + callback(realConversation.id) + end + else + warn("Conversation could not be created, status:", status) + if callback then + callback(nil) + end + end + end + end) + end +end + +function ConversationActions.AddUsersToConversation(convoId, participants, callback) + return function(store) + spawn(function() + local status = WebApi.AddUsersToConversation(convoId, participants) + if status ~= WebApi.Status.OK then + warn("Users could not be added to conversation, status:", status) + end + if callback then + callback(status == WebApi.Status.OK) + end + end) + end +end + +function ConversationActions.GetOlderMessages(convoId, messageId) -- Message ID of the message to collect more after + return function(store) + store:dispatch(FetchingOlderMessages(convoId, true)) + spawn(function() + local status, messages = WebApi.GetMessages(convoId, GET_MESSAGES_PAGE_SIZE, messageId) + store:dispatch(FetchingOlderMessages(convoId, false)) + if status ~= WebApi.Status.OK then + warn("WebApi failure in GetMessages, with status:", status) + return + end + + if #messages < GET_MESSAGES_PAGE_SIZE then + store:dispatch(FetchedOldestMessage(convoId, true)) + end + + if #messages <= 0 then + return + end + + store:dispatch(ReceivedMessages(convoId, messages, nil, messageId)) + end) + end +end + +function ConversationActions.GetUnreadConversationCountAsync() + return function(store) + return Promise.new(function() + local status, unreadConversationCount = WebApi.GetUnreadConversationCount() + + if status ~= WebApi.Status.OK then + warn("WebApi failure in GetUnreadConversationCount, with status", status) + return + end + + store:dispatch(SetUnreadConversationCount(unreadConversationCount)) + end) + end +end + +function ConversationActions.MarkConversationAsRead(conversationId) + return function(store) + local conversation = store:getState().ChatAppReducer.Conversations[conversationId] + + if not conversation then + warn("Conversation not found in MarkConversationAsRead") + return + end + + local messages = conversation.messages + + local lastUnreadMessage = nil + local count = 0 + local lastUreadMessageId = '' + for _, message in messages:CreateReverseIterator() do + count = count + 1 + if not message.read then + lastUnreadMessage = message + lastUreadMessageId = lastUnreadMessage.id + break + end + end + + spawn(function() + + if lastUnreadMessage or (conversation.hasUnreadMessages and count == 0) then + + local status = WebApi.MarkAsRead(conversationId, lastUreadMessageId) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in MarkConversationAsRead") + return + end + end + end) + + if not conversation.hasUnreadMessages then + -- Conversation is already read, we can safely return early + return + end + + store:dispatch(DecrementUnreadConversationCount()) + + store:dispatch(ReadConversation(conversationId)) + + end +end + +return ConversationActions diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/DecrementUnreadConversationCount.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/DecrementUnreadConversationCount.lua new file mode 100644 index 0000000..c3314db --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/DecrementUnreadConversationCount.lua @@ -0,0 +1,9 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/DeleteAlert.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/DeleteAlert.lua new file mode 100644 index 0000000..889abf8 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/DeleteAlert.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(alert) + return { + alert = alert, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FailedToFetchMostRecentlyPlayedGames.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FailedToFetchMostRecentlyPlayedGames.lua new file mode 100644 index 0000000..c3314db --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FailedToFetchMostRecentlyPlayedGames.lua @@ -0,0 +1,9 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FailedToFetchMultiplePlaceInfos.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FailedToFetchMultiplePlaceInfos.lua new file mode 100644 index 0000000..b6e85e5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FailedToFetchMultiplePlaceInfos.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(placeIds) + return { + placeIds = placeIds, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FailedToFetchPlaceThumbnail.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FailedToFetchPlaceThumbnail.lua new file mode 100644 index 0000000..0a7d334 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FailedToFetchPlaceThumbnail.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(imageToken) + return { + imageToken = imageToken, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchChatEnabled.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchChatEnabled.lua new file mode 100644 index 0000000..dcd8670 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchChatEnabled.lua @@ -0,0 +1,19 @@ +local LuaChat = script.Parent.Parent +local WebApi = require(LuaChat.WebApi) +local SetChatEnabled = require(LuaChat.Actions.SetChatEnabled) + +return function(onSuccess) + return function(store) + spawn(function() + local status, response = WebApi.GetChatSettings() + if status ~= WebApi.Status.OK then + warn("Failure in WebApi.GetChatSettings", status) + return + end + store:dispatch(SetChatEnabled(response.chatEnabled)) + if onSuccess then + onSuccess(response.chatEnabled) + end + end) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchedMostRecentlyPlayedGames.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchedMostRecentlyPlayedGames.lua new file mode 100644 index 0000000..c3314db --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchedMostRecentlyPlayedGames.lua @@ -0,0 +1,9 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchedOldestConversation.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchedOldestConversation.lua new file mode 100644 index 0000000..c2bbc81 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchedOldestConversation.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(isFetchedOldestConversation) + return { + value = isFetchedOldestConversation, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchedOldestMessage.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchedOldestMessage.lua new file mode 100644 index 0000000..0b76888 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchedOldestMessage.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId, isFetchedOldestMessage) + return { + conversationId = conversationId, + fetchedOldestMessage = isFetchedOldestMessage, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchingMostRecentlyPlayedGames.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchingMostRecentlyPlayedGames.lua new file mode 100644 index 0000000..c3314db --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchingMostRecentlyPlayedGames.lua @@ -0,0 +1,9 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchingOlderMessages.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchingOlderMessages.lua new file mode 100644 index 0000000..a9faca4 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchingOlderMessages.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId, isFetchingOlderMessages) + return { + conversationId = conversationId, + fetchingOlderMessages = isFetchingOlderMessages, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchingUser.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchingUser.lua new file mode 100644 index 0000000..787b519 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/FetchingUser.lua @@ -0,0 +1,13 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) +local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) + +return Action(script.Name, function(userId) + return { + userId = userId, + status = RetrievalStatus.Fetching, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GameFailedToPin.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GameFailedToPin.lua new file mode 100644 index 0000000..dd6eac5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GameFailedToPin.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId) + return { + conversationId = conversationId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GameFailedToUnpin.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GameFailedToUnpin.lua new file mode 100644 index 0000000..dd6eac5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GameFailedToUnpin.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId) + return { + conversationId = conversationId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GetFriendCount.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GetFriendCount.lua new file mode 100644 index 0000000..51c828c --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GetFriendCount.lua @@ -0,0 +1,18 @@ +local Modules = script.Parent.Parent + +local WebApi = require(Modules.WebApi) + +local SetFriendCount = require(Modules.Actions.SetFriendCount) + +return function() + return function(store) + spawn(function() + local status, totalCount = WebApi.GetFriendCount() + if status ~= WebApi.Status.OK then + store:dispatch(SetFriendCount(0)) -- Remember to come back and add status instead of setting to zero + else + store:dispatch(SetFriendCount(totalCount)) + end + end) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GetMultiplePlaceInfos.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GetMultiplePlaceInfos.lua new file mode 100644 index 0000000..12d9694 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GetMultiplePlaceInfos.lua @@ -0,0 +1,47 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local LuaChat = Modules.LuaChat + +local WebApi = require(LuaChat.WebApi) +local PlaceInfoModel = require(LuaChat.Models.PlaceInfoModel) + +local RequestMultiplePlaceInfos = require(LuaChat.Actions.RequestMultiplePlaceInfos) +local FailedToFetchMultiplePlaceInfos = require(LuaChat.Actions.FailedToFetchMultiplePlaceInfos) +local ReceivedMultiplePlaceInfos = require(LuaChat.Actions.ReceivedMultiplePlaceInfos) + +return function(placeIdList) + return function(store) + local state = store:getState() + local placesToFetch = {} + + if state.ChatAppReducer.PlaceInfos then + for _, placeId in pairs(placeIdList) do + if not state.ChatAppReducer.PlaceInfosAsync[placeId] then + table.insert(placesToFetch, placeId) + end + end + if #placesToFetch == 0 then + return + end + end + + store:dispatch(RequestMultiplePlaceInfos(placesToFetch)) + + spawn(function() + local status, result = WebApi.GetMultiplePlaceInfos(placesToFetch) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in GetMultiplePlaceInfos") + store:dispatch(FailedToFetchMultiplePlaceInfos(placesToFetch)) + return + end + + local placeInfos = {} + for _, placeInfoData in pairs(result) do + table.insert(placeInfos, PlaceInfoModel.fromWeb(placeInfoData)) + end + store:dispatch(ReceivedMultiplePlaceInfos(placeInfos)) + end) + end +end diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GetPlaceThumbnail.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GetPlaceThumbnail.lua new file mode 100644 index 0000000..29dc219 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GetPlaceThumbnail.lua @@ -0,0 +1,52 @@ +local Modules = script.Parent.Parent + +local WebApi = require(Modules.WebApi) +local ThumbnailModel = require(Modules.Models.ThumbnailModel) + +local RequestPlaceThumbnail = require(Modules.Actions.RequestPlaceThumbnail) +local ReceivedPlaceThumbnail = require(Modules.Actions.ReceivedPlaceThumbnail) +local FailedToFetchPlaceThumbnail = require(Modules.Actions.FailedToFetchPlaceThumbnail) + +local RETRY_COUNT = 3 +local WAIT_TIME = 2 + +return function(imageToken, width, height) + return function(store) + local state = store:getState() + if state.ChatAppReducer.PlaceThumbnailsAsync[imageToken] then + return + end + store:dispatch(RequestPlaceThumbnail(imageToken)) + + spawn(function() + local thumbnail = '' + local retryCount = RETRY_COUNT + local waitTime = WAIT_TIME + + while (retryCount > 0) do + local status, result = WebApi.GetPlaceThumbnail(imageToken, width, height) + if status ~= WebApi.Status.OK then + warn("WebApi failure in GetPlaceThumbnail") + store:dispatch(FailedToFetchPlaceThumbnail(imageToken)) + break + else + local placeThumbnailData = result[1] + if placeThumbnailData.final == true then + thumbnail = placeThumbnailData.url + break + end + end + + retryCount = retryCount - 1 + if retryCount > 0 then + wait(waitTime) + waitTime = waitTime * 2 + end + end + + local thumbnailModel = ThumbnailModel.fromWeb(thumbnail) + store:dispatch(ReceivedPlaceThumbnail(imageToken, thumbnailModel)) + + end) + end +end diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GetUser.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GetUser.lua new file mode 100644 index 0000000..7ead911 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/GetUser.lua @@ -0,0 +1,31 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local AddUser = require(LuaChat.Actions.AddUser) +local FetchingUser = require(LuaChat.Actions.FetchingUser) +local UserModel = require(LuaApp.Models.User) +local WebApi = require(LuaChat.WebApi) + +return function(userId) + return function(store) + local oldUser = store:getState().Users[userId] + if not oldUser or not oldUser.isFetching then + store:dispatch(FetchingUser(userId, true)) + + spawn(function() + local status, result = WebApi.GetUser(userId) + store:dispatch(FetchingUser(userId, false)) + if status ~= WebApi.Status.OK then + + warn("WebApi failure in GetUser") + return + end + + store:dispatch(AddUser(UserModel.fromData(result.Id, result.Username, false))) + end) + end + end +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/IncrementUnreadConversationCount.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/IncrementUnreadConversationCount.lua new file mode 100644 index 0000000..c3314db --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/IncrementUnreadConversationCount.lua @@ -0,0 +1,9 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/MessageFailedToSend.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/MessageFailedToSend.lua new file mode 100644 index 0000000..b92c7b5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/MessageFailedToSend.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId, messageId) + return { + conversationId = conversationId, + messageId = messageId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/MessageModerated.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/MessageModerated.lua new file mode 100644 index 0000000..b92c7b5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/MessageModerated.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId, messageId) + return { + conversationId = conversationId, + messageId = messageId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/PinnedGame.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/PinnedGame.lua new file mode 100644 index 0000000..dd6eac5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/PinnedGame.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId) + return { + conversationId = conversationId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/PinningGame.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/PinningGame.lua new file mode 100644 index 0000000..dd6eac5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/PinningGame.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId) + return { + conversationId = conversationId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/PlayTogetherActions.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/PlayTogetherActions.lua new file mode 100644 index 0000000..47f9266 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/PlayTogetherActions.lua @@ -0,0 +1,185 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local LuaChat = Modules.LuaChat +local Actions = LuaChat.Actions + +local Constants = require(LuaChat.Constants) +local PlaceInfoModel = require(LuaChat.Models.PlaceInfoModel) +local ToastModel = require(LuaChat.Models.ToastModel) +local WebApi = require(LuaChat.WebApi) + +local FailedToFetchedMostRecentlyPlayedGames = require(Actions.FailedToFetchMostRecentlyPlayedGames) +local FailedToFetchMultiplePlaceInfos = require(Actions.FailedToFetchMultiplePlaceInfos) +local FetchedMostRecentlyPlayedGames = require(Actions.FetchedMostRecentlyPlayedGames) +local FetchingMostRecentlyPlayedGames = require(Actions.FetchingMostRecentlyPlayedGames) +local GameFailedToPin = require(Actions.GameFailedToPin) +local GameFailedToUnpin = require(Actions.GameFailedToUnpin) +local PinnedGame = require(Actions.PinnedGame) +local PinningGame = require(Actions.PinningGame) +local ReceivedMultiplePlaceInfos = require(Actions.ReceivedMultiplePlaceInfos) +local RequestMultiplePlaceInfos = require(Actions.RequestMultiplePlaceInfos) +local SetMostRecentlyPlayedGamesForUser = require(Actions.SetMostRecentlyPlayedGamesForUser) +local SetMostRecentlyPlayedPlayableGameForUser = require(Actions.SetMostRecentlyPlayedPlayableGameForUser) +local SetPinnedGameForConversation = require(Actions.SetPinnedGameForConversation) +local ShowToast = require(Actions.ShowToast) +local UnpinnedGame = require(Actions.UnpinnedGame) +local UnpinningGame = require(Actions.UnpinningGame) + +local PlayTogetherActions = {} + +local HOME_GAMES_SORTS = "HomeSorts" +local MOST_RECENTLY_PLAYED_GAMES = "MyRecent" + +function PlayTogetherActions.PinGame(conversationId, universeId) + return function(store) + if universeId == store:getState().ChatAppReducer.Conversations[conversationId].pinnedGame.universeId then + local messageKey = "Feature.Chat.Message.AlreadyPinnedGame" + local toastModel = ToastModel.new(Constants.ToastIDs.PIN_PINNED_GAME, messageKey) + store:Dispatch(ShowToast(toastModel)) + return + end + + if store:getState().ChatAppReducer.PlayTogetherAsync.pinningGames[conversationId] then + return + end + + store:Dispatch(PinningGame(conversationId)) + + spawn(function() + local status, _ = WebApi.PinGame(conversationId, universeId) + if status == WebApi.Status.OK then + store:Dispatch(PinnedGame(conversationId)) + else + store:Dispatch(GameFailedToPin(conversationId)) + + local messageKey = "Feature.Chat.Message.PinFailed" + local toastModel = ToastModel.new(Constants.ToastIDs.PIN_GAME_FAILED, messageKey) + store:Dispatch(ShowToast(toastModel)) + + warn("Game could not be pinned.") + end + end) + end +end + +function PlayTogetherActions.UnpinGame(conversationId) + return function(store) + if store:getState().ChatAppReducer.PlayTogetherAsync.unPinningGames[conversationId] then + return + end + + store:Dispatch(UnpinningGame(conversationId)) + + spawn(function() + local status, _ = WebApi.UnpinGame(conversationId) + if status == WebApi.Status.OK then + store:Dispatch(UnpinnedGame(conversationId)) + else + store:Dispatch(GameFailedToUnpin(conversationId)) + + local messageKey = "Feature.Chat.Message.UnpinFailed" + local toastModel = ToastModel.new(Constants.ToastIDs.UNPIN_GAME_FAILED, messageKey) + store:Dispatch(ShowToast(toastModel)) + + warn("Game could not be unpinned.") + end + end) + end +end + +function PlayTogetherActions.SetPinnedGameForConversation(universeId, rootPlaceId, conversationId) + return function(store) + spawn(function() + store:Dispatch(SetPinnedGameForConversation( + universeId, + rootPlaceId, + conversationId + )) + end) + end +end + +function PlayTogetherActions.GetMostRecentlyPlayedGames() + return function(store) + spawn(function() + if store:getState().ChatAppReducer.PlayTogetherAsync.fetchingMostRecentlyPlayedGames then + return + end + + store:Dispatch(FetchingMostRecentlyPlayedGames()) + + local gameSorts = WebApi.GetGamesSorts(HOME_GAMES_SORTS) + if not gameSorts then + warn("Failed to get game sorts") + store:Dispatch(FailedToFetchedMostRecentlyPlayedGames()) + return + end + + for _, sort in pairs(gameSorts) do + if sort.name == MOST_RECENTLY_PLAYED_GAMES then + local games = WebApi.GetMostRecentlyPlayedGames(sort.token) + if games then + store:Dispatch(FetchedMostRecentlyPlayedGames()) + store:Dispatch(SetMostRecentlyPlayedGamesForUser(games)) + else + warn("No most recently played games found") + store:Dispatch(FailedToFetchedMostRecentlyPlayedGames()) + end + + return + end + end + + warn("No most recently played game sort") + store:Dispatch(FailedToFetchedMostRecentlyPlayedGames()) + end) + end +end + +function PlayTogetherActions.GetMostRecentlyPlayedPlayableGame() + return function(store) + local mostRecentlyPlayedGames = store:getState().ChatAppReducer.MostRecentlyPlayedGames.games + if not mostRecentlyPlayedGames or #mostRecentlyPlayedGames == 0 then + return + end + + spawn(function() + local mostRecentlyPlayedPlayableGamePlaceId = nil + for _, game in pairs(mostRecentlyPlayedGames) do + local placeId = tostring(game.placeId) + local placeInfo = store:getState().ChatAppReducer.PlaceInfos[placeId] + + if (placeInfo == nil) and (not store:getState().ChatAppReducer.PlaceInfosAsync[placeId]) then + local placeIds = { placeId } + store:Dispatch(RequestMultiplePlaceInfos(placeIds)) + + local status, result = WebApi.GetMultiplePlaceInfos(placeIds) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in GetMostRecentlyPlayedPlayableGame") + store:Dispatch(FailedToFetchMultiplePlaceInfos(placeIds)) + else + local placeInfos = {} + for _, placeInfoData in pairs(result) do + table.insert(placeInfos, PlaceInfoModel.fromWeb(placeInfoData)) + end + store:Dispatch(ReceivedMultiplePlaceInfos(placeInfos)) + end + break + end + + if placeInfo and placeInfo.isPlayable then + mostRecentlyPlayedPlayableGamePlaceId = placeId + break + end + end + + if mostRecentlyPlayedPlayableGamePlaceId then + store:Dispatch(SetMostRecentlyPlayedPlayableGameForUser(mostRecentlyPlayedPlayableGamePlaceId)) + end + end) + end +end + +return PlayTogetherActions \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/PopRoute.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/PopRoute.lua new file mode 100644 index 0000000..c3314db --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/PopRoute.lua @@ -0,0 +1,9 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReadConversation.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReadConversation.lua new file mode 100644 index 0000000..dd6eac5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReadConversation.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId) + return { + conversationId = conversationId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedAllFriends.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedAllFriends.lua new file mode 100644 index 0000000..1806d56 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedAllFriends.lua @@ -0,0 +1,7 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Action = require(Modules.Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedConversation.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedConversation.lua new file mode 100644 index 0000000..d8adae6 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedConversation.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(convo) + return { + conversation = convo, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedLatestMessages.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedLatestMessages.lua new file mode 100644 index 0000000..1806d56 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedLatestMessages.lua @@ -0,0 +1,7 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Action = require(Modules.Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedMessages.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedMessages.lua new file mode 100644 index 0000000..9b3e4e1 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedMessages.lua @@ -0,0 +1,15 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId, messages, shouldMarkConversationUnread, messageId) + return { + conversationId = conversationId, + messages = messages, + shouldMarkConversationUnread = shouldMarkConversationUnread, + exclusiveStartMessageId = messageId, + } + end +) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedMultiplePlaceInfos.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedMultiplePlaceInfos.lua new file mode 100644 index 0000000..6c711f7 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedMultiplePlaceInfos.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(placeInfos) + return { + placeInfos = placeInfos, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedOldestConversation.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedOldestConversation.lua new file mode 100644 index 0000000..503711e --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedOldestConversation.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(value) + return { + value = value, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedPageConversations.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedPageConversations.lua new file mode 100644 index 0000000..1806d56 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedPageConversations.lua @@ -0,0 +1,7 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Action = require(Modules.Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedPlaceThumbnail.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedPlaceThumbnail.lua new file mode 100644 index 0000000..3d53c74 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedPlaceThumbnail.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(imageToken, thumbnail) + return { + imageToken = imageToken, + thumbnail = thumbnail, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedUserPresence.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedUserPresence.lua new file mode 100644 index 0000000..13de17f --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedUserPresence.lua @@ -0,0 +1,12 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(userId, presence, lastLocation, placeId) + return { + userId = userId, + presence = presence, + lastLocation = lastLocation, + placeId = placeId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedUserTyping.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedUserTyping.lua new file mode 100644 index 0000000..834ac1f --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ReceivedUserTyping.lua @@ -0,0 +1,24 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local LuaChat = Modules.LuaChat +local SetUserTyping = require(LuaChat.Actions.SetUserTyping) + +local typingCount = 0 + +return function(conversationId, userId) + return function(store) + spawn(function() + typingCount = typingCount + 1 + local thisTypingCount = typingCount + + store:dispatch(SetUserTyping(conversationId, userId, true)) + + wait(5) + + if typingCount == thisTypingCount then + store:dispatch(SetUserTyping(conversationId, userId, false)) + end + end) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RemoveRoute.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RemoveRoute.lua new file mode 100644 index 0000000..a90a69d --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RemoveRoute.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(intent) + return { + intent = intent, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RemovedConversation.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RemovedConversation.lua new file mode 100644 index 0000000..dd6eac5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RemovedConversation.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId) + return { + conversationId = conversationId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RenamedGroupConversation.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RenamedGroupConversation.lua new file mode 100644 index 0000000..7e7b7fe --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RenamedGroupConversation.lua @@ -0,0 +1,14 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId, title, isDefaultTitle, lastUpdated) + return { + conversationId = conversationId, + title = title, + isDefaultTitle = isDefaultTitle, + lastUpdated = lastUpdated, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestAllFriends.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestAllFriends.lua new file mode 100644 index 0000000..1806d56 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestAllFriends.lua @@ -0,0 +1,7 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Action = require(Modules.Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestLatestMessages.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestLatestMessages.lua new file mode 100644 index 0000000..1806d56 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestLatestMessages.lua @@ -0,0 +1,7 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Action = require(Modules.Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestMultiplePlaceInfos.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestMultiplePlaceInfos.lua new file mode 100644 index 0000000..b6e85e5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestMultiplePlaceInfos.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(placeIds) + return { + placeIds = placeIds, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestPageConversations.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestPageConversations.lua new file mode 100644 index 0000000..1806d56 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestPageConversations.lua @@ -0,0 +1,7 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Action = require(Modules.Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestPlaceThumbnail.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestPlaceThumbnail.lua new file mode 100644 index 0000000..0a7d334 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestPlaceThumbnail.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(imageToken) + return { + imageToken = imageToken, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestUserPresence.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestUserPresence.lua new file mode 100644 index 0000000..22fc0b3 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/RequestUserPresence.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(userId) + return { + userId = userId + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SendingMessage.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SendingMessage.lua new file mode 100644 index 0000000..adb6b01 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SendingMessage.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId, message) + return { + conversationId = conversationId, + message = message + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SentMessage.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SentMessage.lua new file mode 100644 index 0000000..1488ec6 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SentMessage.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId, messageId) + return { + conversationId = conversationId, + messageId = messageId + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetActiveConversationId.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetActiveConversationId.lua new file mode 100644 index 0000000..dd6eac5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetActiveConversationId.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId) + return { + conversationId = conversationId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetAppLoaded.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetAppLoaded.lua new file mode 100644 index 0000000..1f53294 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetAppLoaded.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(isLoaded) + return { + value = isLoaded, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetChatEnabled.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetChatEnabled.lua new file mode 100644 index 0000000..0c2c510 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetChatEnabled.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(isEnabled) + return { + value = isEnabled, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetConnectionState.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetConnectionState.lua new file mode 100644 index 0000000..d41a689 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetConnectionState.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(connectionState) + return { + connectionState = connectionState; + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetConversationLoadingStatus.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetConversationLoadingStatus.lua new file mode 100644 index 0000000..969a26d --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetConversationLoadingStatus.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(convoId, conversationLoadingState) + return { + conversationId = convoId, + value = conversationLoadingState + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetFetchingConversations.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetFetchingConversations.lua new file mode 100644 index 0000000..e5001eb --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetFetchingConversations.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(isFetchingConversations) + return { + value = isFetchingConversations, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetFormFactor.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetFormFactor.lua new file mode 100644 index 0000000..9aeb0ad --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetFormFactor.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(formFactor) + return { + formFactor = formFactor, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetFriendCount.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetFriendCount.lua new file mode 100644 index 0000000..4772e59 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetFriendCount.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(count) + return { + count = count, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetMostRecentlyPlayedGamesForUser.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetMostRecentlyPlayedGamesForUser.lua new file mode 100644 index 0000000..3c6e84a --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetMostRecentlyPlayedGamesForUser.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(games) + return { + games = games, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetMostRecentlyPlayedPlayableGameForUser.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetMostRecentlyPlayedPlayableGameForUser.lua new file mode 100644 index 0000000..67c87ec --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetMostRecentlyPlayedPlayableGameForUser.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(placeId) + return { + placeId = placeId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetPinnedGameForConversation.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetPinnedGameForConversation.lua new file mode 100644 index 0000000..c4c492b --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetPinnedGameForConversation.lua @@ -0,0 +1,13 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(universeId, rootPlaceId, conversationId) + return { + universeId = universeId, + rootPlaceId = rootPlaceId, + conversationId = conversationId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetRoute.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetRoute.lua new file mode 100644 index 0000000..7884bf7 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetRoute.lua @@ -0,0 +1,13 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(intent, parameters, popToIntent) + return { + intent = intent, + parameters = parameters or {}, + popToIntent = popToIntent, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetUnreadConversationCount.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetUnreadConversationCount.lua new file mode 100644 index 0000000..da7d3d4 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetUnreadConversationCount.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(unreadConversationCount) + return { + count = unreadConversationCount, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetUserIsFriend.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetUserIsFriend.lua new file mode 100644 index 0000000..09b6464 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetUserIsFriend.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(userId, isFriend) + return { + isFriend = isFriend, + userId = userId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetUserLeavingConversation.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetUserLeavingConversation.lua new file mode 100644 index 0000000..48bd46a --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetUserLeavingConversation.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId, isLeaving) + return { + id = conversationId, + isLeaving = isLeaving, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetUserTyping.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetUserTyping.lua new file mode 100644 index 0000000..b3fd4e3 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/SetUserTyping.lua @@ -0,0 +1,14 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId, userId, isUserTyping) + return { + conversationId = conversationId, + userId = userId, + value = isUserTyping, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/AddGamesBySortShareGameToChatFromChat.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/AddGamesBySortShareGameToChatFromChat.lua new file mode 100644 index 0000000..6aa6162 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/AddGamesBySortShareGameToChatFromChat.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(gameSortName, placeIds) + return { + name = gameSortName, + placeIds = placeIds or {}, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/AddGamesInformationShareGameToChatFromChat.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/AddGamesInformationShareGameToChatFromChat.lua new file mode 100644 index 0000000..3c6e84a --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/AddGamesInformationShareGameToChatFromChat.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(games) + return { + games = games, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/ClearAllGamesInSortsShareGameToChatFromChat.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/ClearAllGamesInSortsShareGameToChatFromChat.lua new file mode 100644 index 0000000..c3314db --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/ClearAllGamesInSortsShareGameToChatFromChat.lua @@ -0,0 +1,9 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/FailedToFetchGamesBySortShareGameToChatFromChat.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/FailedToFetchGamesBySortShareGameToChatFromChat.lua new file mode 100644 index 0000000..da850ba --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/FailedToFetchGamesBySortShareGameToChatFromChat.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(gameSortName) + return { + gameSortName = gameSortName, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/FailedToShareGameToChatFromChat.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/FailedToShareGameToChatFromChat.lua new file mode 100644 index 0000000..c3314db --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/FailedToShareGameToChatFromChat.lua @@ -0,0 +1,9 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/FetchedGamesBySortShareGameToChatFromChat.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/FetchedGamesBySortShareGameToChatFromChat.lua new file mode 100644 index 0000000..da850ba --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/FetchedGamesBySortShareGameToChatFromChat.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(gameSortName) + return { + gameSortName = gameSortName, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/FetchingGamesBySortShareGameToChatFromChat.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/FetchingGamesBySortShareGameToChatFromChat.lua new file mode 100644 index 0000000..da850ba --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/FetchingGamesBySortShareGameToChatFromChat.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(gameSortName) + return { + gameSortName = gameSortName, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/ResetShareGameToChatFromChat.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/ResetShareGameToChatFromChat.lua new file mode 100644 index 0000000..c3314db --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/ResetShareGameToChatFromChat.lua @@ -0,0 +1,9 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/ResetShareGameToChatFromChatAsync.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/ResetShareGameToChatFromChatAsync.lua new file mode 100644 index 0000000..c3314db --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/ResetShareGameToChatFromChatAsync.lua @@ -0,0 +1,9 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/ShareGameToChatFromChatThunks.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/ShareGameToChatFromChatThunks.lua new file mode 100644 index 0000000..28e40e5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/ShareGameToChatFromChatThunks.lua @@ -0,0 +1,126 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat +local ShareGameToChatActions = LuaChat.Actions.ShareGameToChatFromChat + +local AddGamesBySort = require(ShareGameToChatActions.AddGamesBySortShareGameToChatFromChat) +local AddGamesInformation = require(ShareGameToChatActions.AddGamesInformationShareGameToChatFromChat) +local Constants = require(LuaChat.Constants) +local ClearAllGamesInSorts = require(ShareGameToChatActions.ClearAllGamesInSortsShareGameToChatFromChat) +local FailedToFetchGamesBySort = require(ShareGameToChatActions.FailedToFetchGamesBySortShareGameToChatFromChat) +local FailedToShareGameToChat = require(ShareGameToChatActions.FailedToShareGameToChatFromChat) +local FetchedGamesBySort = require(ShareGameToChatActions.FetchedGamesBySortShareGameToChatFromChat) +local FetchingGamesBySort = require(ShareGameToChatActions.FetchingGamesBySortShareGameToChatFromChat) +local PopRoute = require(LuaChat.Actions.PopRoute) +local ResetShareGame = require(ShareGameToChatActions.ResetShareGameToChatFromChat) +local ResetShareGameToChatAsync = require(ShareGameToChatActions.ResetShareGameToChatFromChatAsync) +local SharedGameToChat = require(ShareGameToChatActions.SharedGameToChatFromChat) +local SharingGameToChat = require(ShareGameToChatActions.SharingGameToChatFromChat) +local ShowToast = require(LuaChat.Actions.ShowToast) +local ToastModel = require(LuaChat.Models.ToastModel) +local SetGameThumbnails = require(LuaApp.Actions.SetGameThumbnails) +local UpdateGameSortsTokens = require(ShareGameToChatActions.UpdateGameSortsTokensShareGameToChatFromChat) +local WebApi = require(LuaChat.WebApi) + +local SHARED_GAMES_SORT = "GamesAllSorts" + +local ShareGameToChatFromChatThunks = {} + +function ShareGameToChatFromChatThunks.FetchGames(gameSortName, fetchedThumbnailSize) + return function(store) + spawn(function() + if store:getState().ChatAppReducer.ShareGameToChatAsync.fetchingGamesBySort[gameSortName] then + return + end + + store:dispatch(FetchingGamesBySort(gameSortName)) + + if not store:getState().ChatAppReducer.SharedGameSorts[gameSortName] + or not store:getState().ChatAppReducer.SharedGameSorts[gameSortName].tokenExpiry + or store:getState().ChatAppReducer.SharedGameSorts[gameSortName].tokenExpiry < tick() then + local gameSorts = WebApi.GetGamesSorts(SHARED_GAMES_SORT) + if not gameSorts then + store:dispatch(FailedToFetchGamesBySort(gameSortName)) + warn("Failed to get game sorts") + return + end + + store:dispatch(UpdateGameSortsTokens(gameSorts)) + end + + local gamesList = nil + if store:getState().ChatAppReducer.SharedGameSorts[gameSortName] and + store:getState().ChatAppReducer.SharedGameSorts[gameSortName].token then + gamesList = WebApi.GetGamesInSortByToken(store:getState().ChatAppReducer.SharedGameSorts[gameSortName].token) + end + + if gamesList then + local placeIds = {} + local newPlaceIds = {} + local games = {} + + for _, game in pairs(gamesList) do + table.insert(placeIds, game.placeId) + if not store:getState().ChatAppReducer.SharedGamesInfo[game.placeId] then + games[game.placeId] = game + end + + if not store:getState().ChatAppReducer.SharedGamesInfo[game.placeId] or + not store:getState().ChatAppReducer.SharedGamesInfo[game.placeId].url then + table.insert(newPlaceIds, game.placeId) + end + end + + if #newPlaceIds > 0 then + local _, placesInfo = WebApi.GetMultiplePlaceInfos(newPlaceIds) + local imageTokens = {} + for _, placeInfo in pairs(placesInfo) do + games[placeInfo.placeId].url = placeInfo.url + games[placeInfo.placeId].isPlayable = placeInfo.isPlayable + table.insert(imageTokens, placeInfo.imageToken) + end + + store:dispatch(AddGamesInformation(games)) + + local thumbnails = WebApi.GetPlacesThumbnails(imageTokens, fetchedThumbnailSize, fetchedThumbnailSize) + store:dispatch(SetGameThumbnails(thumbnails)) + end + + store:dispatch(FetchedGamesBySort(gameSortName)) + store:dispatch(AddGamesBySort(gameSortName, placeIds)) + else + store:dispatch(AddGamesBySort(gameSortName, nil)) + store:dispatch(FailedToFetchGamesBySort(gameSortName)) + warn("No " .. gameSortName .. " games found") + end + end) + end +end + +function ShareGameToChatFromChatThunks.HasGameFetchRequestCompleted(sortName, shareGameToChatAsync) + return shareGameToChatAsync.fetchedGamesBySort[sortName] or + shareGameToChatAsync.failedToFetchGamesBySort[sortName] +end + +function ShareGameToChatFromChatThunks.Sharing(store) + store:dispatch(SharingGameToChat()) +end + +function ShareGameToChatFromChatThunks.Shared(store) + store:dispatch(PopRoute()) + store:dispatch(SharedGameToChat()) + + store:dispatch(ResetShareGame()) + store:dispatch(ClearAllGamesInSorts()) + store:dispatch(ResetShareGameToChatAsync()) +end + +function ShareGameToChatFromChatThunks.FailedToShare(store) + local messageKey = "Feature.Chat.ShareGameToChat.FailedToShareTheGame" + local toastModel = ToastModel.new(Constants.ToastIDs.GAME_NOT_SHAREABLE, messageKey) + + store:dispatch(ShowToast(toastModel)) + store:dispatch(FailedToShareGameToChat()) +end + +return ShareGameToChatFromChatThunks \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/SharedGameToChatFromChat.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/SharedGameToChatFromChat.lua new file mode 100644 index 0000000..c3314db --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/SharedGameToChatFromChat.lua @@ -0,0 +1,9 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/SharingGameToChatFromChat.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/SharingGameToChatFromChat.lua new file mode 100644 index 0000000..c3314db --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/SharingGameToChatFromChat.lua @@ -0,0 +1,9 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/UpdateGameSortsTokensShareGameToChatFromChat.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/UpdateGameSortsTokensShareGameToChatFromChat.lua new file mode 100644 index 0000000..5211bac --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShareGameToChatFromChat/UpdateGameSortsTokensShareGameToChatFromChat.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(gameSorts) + return { + gameSorts = gameSorts, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShowAlert.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShowAlert.lua new file mode 100644 index 0000000..889abf8 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShowAlert.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(alert) + return { + alert = alert, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShowToast.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShowToast.lua new file mode 100644 index 0000000..dcab278 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ShowToast.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(toast) + return { + toast = toast, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ToastComplete.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ToastComplete.lua new file mode 100644 index 0000000..dcab278 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ToastComplete.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(toast) + return { + toast = toast, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ToggleChatPaused.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ToggleChatPaused.lua new file mode 100644 index 0000000..a7baa60 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/ToggleChatPaused.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(isPaused) + return { + value = isPaused, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/UnpinnedGame.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/UnpinnedGame.lua new file mode 100644 index 0000000..dd6eac5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/UnpinnedGame.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId) + return { + conversationId = conversationId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Actions/UnpinningGame.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/UnpinningGame.lua new file mode 100644 index 0000000..dd6eac5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Actions/UnpinningGame.lua @@ -0,0 +1,11 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(conversationId) + return { + conversationId = conversationId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Analytics/Events/loadGameLinkCardInChat.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Analytics/Events/loadGameLinkCardInChat.lua new file mode 100644 index 0000000..4156f76 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Analytics/Events/loadGameLinkCardInChat.lua @@ -0,0 +1,21 @@ +local PlayerService = game:GetService("Players") + +return function(eventStreamImpl, eventContext, conversationId, placeId) + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(conversationId) == "string", "Expected conversationId to be a string") + assert(type(placeId) == "string", "Expected placeId to be a string") + + local eventName = "loadGameLinkCardInChat" + + local player = PlayerService.LocalPlayer + local userId = "UNKNOWN" + if player then + userId = tostring(player.UserId) + end + + eventStreamImpl:setRBXEventStream(eventContext, eventName, { + uid = userId, + cid = conversationId, + pid = placeId, + }) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Analytics/Events/shareGameToChatFromChat.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Analytics/Events/shareGameToChatFromChat.lua new file mode 100644 index 0000000..646b3c9 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Analytics/Events/shareGameToChatFromChat.lua @@ -0,0 +1,19 @@ +local PlayerService = game:GetService("Players") + +return function(eventStreamImpl, eventContext, conversationId, placeId) + assert(type(eventContext) == "string", "Expected eventContext to be a string") + + local eventName = "shareGameToChatFromChat" + + local player = PlayerService.LocalPlayer + local userId = "UNKNOWN" + if player then + userId = tostring(player.UserId) + end + + eventStreamImpl:setRBXEventStream(eventContext, eventName, { + uid = userId, + cid = conversationId, + pid = placeId, + }) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/AppReducer.lua b/Client2018/content/internal/Chat/Modules/LuaChat/AppReducer.lua new file mode 100644 index 0000000..6b7177a --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/AppReducer.lua @@ -0,0 +1,56 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local LuaChat = Modules.LuaChat + +local ActiveConversationId = require(LuaChat.Reducers.ActiveConversationId) +local ShareGameToChatAsync = require(LuaChat.Reducers.ShareGameToChatAsync) +local SharedGameSorts = require(LuaChat.Reducers.SharedGameSorts) +local SharedGamesInfo = require(LuaChat.Reducers.SharedGamesInfo) + +local AppLoaded = require(LuaChat.Reducers.AppLoaded) +local AppState = require(LuaChat.Reducers.AppState) +local ChatEnabled = require(LuaChat.Reducers.ChatEnabled) +local Conversations = require(LuaChat.Reducers.Conversations) +local ConversationsAsync = require(LuaChat.Reducers.ConversationsAsync) +local Location = require(LuaChat.Reducers.Location) +local MostRecentlyPlayedGames = require(LuaChat.Reducers.MostRecentlyPlayedGames) +local PlaceInfos = require(Modules.LuaChat.Reducers.PlaceInfos) +local PlaceInfosAsync = require(LuaChat.Reducers.PlaceInfosAsync) +local PlaceThumbnails = require(Modules.LuaChat.Reducers.PlaceThumbnails) +local PlaceThumbnailsAsync = require(LuaChat.Reducers.PlaceThumbnailsAsync) +local PlayTogetherAsync = require(LuaChat.Reducers.PlayTogetherAsync) +local Toast = require(LuaChat.Reducers.Toast) +local ToggleChatPaused = require(LuaChat.Reducers.ToggleChatPaused) +local UnreadConversationCount = require(LuaChat.Reducers.UnreadConversationCount) + +return function(state, action) + state = state or {} + + return { + -- Unique to Chat + ActiveConversationId = ActiveConversationId(state.ActiveConversationId, action), + AppState = AppState(state.AppState, action), + Location = Location(state.Location, action), + ChatEnabled = ChatEnabled(state.ChatEnabled, action), + AppLoaded = AppLoaded(state.AppLoaded, action), + ToggleChatPaused = ToggleChatPaused(state.ToggleChatPaused, action), + + -- TODO: update and move the following to the LuaApp state when WebApi is refactored: + -- See: https://jira.roblox.com/browse/SOC-1737 + PlaceInfos = PlaceInfos(state.PlaceInfos, action), + PlaceInfosAsync = PlaceInfosAsync(state.PlaceInfosAsync, action), + PlaceThumbnails = PlaceThumbnails(state.PlaceThumbnails, action), + PlaceThumbnailsAsync = PlaceThumbnailsAsync(state.PlaceThumbnailsAsync, action), + + -- May be able to be shared with other pages + Toast = Toast(state.Toast, action), + Conversations = Conversations(state.Conversations, action), + ConversationsAsync = ConversationsAsync(state.ConversationsAsync, action), + UnreadConversationCount = UnreadConversationCount(state.UnreadConversationCount, action), + MostRecentlyPlayedGames = MostRecentlyPlayedGames(state.MostRecentlyPlayedGames, action), + PlayTogetherAsync = PlayTogetherAsync(state.PlayTogetherAsync, action), + -- Share game to chat from chat + ShareGameToChatAsync = ShareGameToChatAsync(state.ShareGameToChatAsync, action), + SharedGameSorts = SharedGameSorts(state.SharedGameSorts, action), + SharedGamesInfo = SharedGamesInfo(state.SharedGamesInfo, action), + } +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/AppState.lua b/Client2018/content/internal/Chat/Modules/LuaChat/AppState.lua new file mode 100644 index 0000000..4722ce2 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/AppState.lua @@ -0,0 +1,44 @@ +local CoreGui = game:GetService("CoreGui") +local LocalizationService = game:GetService("LocalizationService") + +local Modules = CoreGui.RobloxGui.Modules + +local Common = Modules.Common + +local ScreenManager = require(Modules.LuaChat.ScreenManager) +local AppReducer = require(Modules.LuaApp.AppReducer) +local Localization = require(Modules.LuaApp.Localization) + +local Analytics = require(Common.Analytics) +local Rodux = require(Common.Rodux) + +local AppState = {} + +local function appStateConstructor(chatGui, store, analyticsImpl) + local state = {} + + state.store = store + state.localization = Localization.new(LocalizationService.RobloxLocaleId) + state.analytics = analyticsImpl + state.screenManager = ScreenManager.new(chatGui, state) + return state +end + +function AppState.new(chatGui, store) + local analyticsImpl = Analytics.new() + return appStateConstructor(chatGui, store, analyticsImpl) +end + +function AppState.mock(chatGui, store, analyticsImpl) + analyticsImpl = analyticsImpl or Analytics.mock() + chatGui = chatGui or CoreGui + store = store or Rodux.Store.new(AppReducer) + + return appStateConstructor(chatGui, store, analyticsImpl) +end + +function AppState:Destruct() + self.store:destruct() +end + +return AppState \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/BaseView.lua b/Client2018/content/internal/Chat/Modules/LuaChat/BaseView.lua new file mode 100644 index 0000000..38a6275 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/BaseView.lua @@ -0,0 +1,33 @@ +-- Creating BaseView -- + +local BaseView = {} + +-- Naive implementation of get +function BaseView:Get(...) + return self.new(...) +end + +function BaseView:Template() + local class = {} + for key, value in pairs(self) do + class[key] = value + end + return class +end + +function BaseView:Start() +end + +function BaseView:Stop() +end + +function BaseView:Resume() +end + +function BaseView:Pause() +end + +function BaseView:Destruct() +end + +return BaseView \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ActionEntry.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ActionEntry.lua new file mode 100644 index 0000000..83056db --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ActionEntry.lua @@ -0,0 +1,117 @@ + + +local Modules = script.Parent.Parent +local Create = require(Modules.Create) +local Constants = require(Modules.Constants) +local Text = require(Modules.Text) + +local ListEntry = require(Modules.Components.ListEntry) + +local ICON_CELL_WIDTH = 60 +local HEIGHT = 48 + +local LABEL_SPACING = 12 + +local ActionEntry = {} + +function ActionEntry.new(appState, icon, localizationKey, size) + local self = {} + + size = size or 24 + + setmetatable(self, {__index = ActionEntry}) + + local text = appState.localization:Format(localizationKey) + + local listEntry = ListEntry.new(appState, HEIGHT) + self.rbx = listEntry.rbx + + local iconFrame = Create.new"Frame" { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, ICON_CELL_WIDTH, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + Create.new"ImageLabel" { + BackgroundTransparency = 1, + Size = UDim2.new(0, size, 0, size), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Image = icon, + BorderSizePixel = 0, + }, + } + iconFrame.Parent = self.rbx + + local label = Create.new"TextLabel" { + Name = "Label", + BackgroundTransparency = 1, + Size = UDim2.new(1, -ICON_CELL_WIDTH, 1, 0), + Position = UDim2.new(0, ICON_CELL_WIDTH, 0, 0), + TextSize = Constants.Font.FONT_SIZE_18, + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSans, + Text = text, + TextXAlignment = Enum.TextXAlignment.Left, + } + label.Parent = self.rbx + + local labelEffectiveWidth = Text.GetTextWidth(text, Enum.Font.SourceSans, Constants.Font.FONT_SIZE_18) + LABEL_SPACING + local value = Create.new"TextLabel" { + Name = "Value", + BackgroundTransparency = 1, + Size = UDim2.new(1, -ICON_CELL_WIDTH-12-labelEffectiveWidth, 1, 0), + Position = UDim2.new(0, ICON_CELL_WIDTH+labelEffectiveWidth, 0, 0), + TextSize = Constants.Font.FONT_SIZE_18, + TextColor3 = Constants.Color.GRAY2, + Font = Enum.Font.SourceSans, + Text = "", + TextXAlignment = Enum.TextXAlignment.Right, + ClipsDescendants = true, + } + value.Parent = self.rbx + + local divider = Create.new"Frame" { + Name = "Divider", + BackgroundColor3 = Constants.Color.GRAY4, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 1), + Position = UDim2.new(0, 0, 1, -1), + } + divider.Parent = self.rbx + self.divider = divider + + self.tapped = Instance.new("BindableEvent") + self.tapped.Parent = self.rbx + + listEntry.tapped:Connect(function() + self.tapped:Fire() + end) + + value:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + self:AdjustLayout() + end) + + return self +end + +function ActionEntry:SetDividerOffset(dividerOffset) + self.divider.Size = UDim2.new(1, -dividerOffset, 0, 1) + self.divider.Position = UDim2.new(0, dividerOffset, 1, -1) +end + +function ActionEntry:AdjustLayout() + local text = self.rbx.Value.Text + local valueWidth = Text.GetTextWidth(text, Enum.Font.SourceSans, Constants.Font.FONT_SIZE_18) + if valueWidth > self.rbx.Value.AbsoluteSize.X then + self.rbx.Value.TextXAlignment = Enum.TextXAlignment.Right + else + self.rbx.Value.TextXAlignment = Enum.TextXAlignment.Left + end +end + +function ActionEntry:Update(state) + self.rbx.Value.Text = state + self:AdjustLayout() +end + +return ActionEntry diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/AssetCard.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/AssetCard.lua new file mode 100644 index 0000000..bad1c4e --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/AssetCard.lua @@ -0,0 +1,513 @@ +local CoreGui = game:GetService("CoreGui") +local Players = game:GetService("Players") +local GuiService = game:GetService("GuiService") +local TweenService = game:GetService("TweenService") +local HttpService = game:GetService("HttpService") +local UserInputService = game:GetService("UserInputService") + +local Modules = CoreGui.RobloxGui.Modules +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local FlagSettings = require(LuaChat.FlagSettings) +local GameParams = require(LuaChat.Models.GameParams) +local getInputEvent = require(LuaChat.Utils.getInputEvent) +local GetMultiplePlaceInfos = require(LuaChat.Actions.GetMultiplePlaceInfos) +local GetPlaceThumbnail = require(LuaChat.Actions.GetPlaceThumbnail) +local LoadingIndicator = require(LuaChat.Components.LoadingIndicator) +local NotificationType = require(LuaApp.Enum.NotificationType) +local PlayTogetherActions = require(LuaChat.Actions.PlayTogetherActions) +local Text = require(LuaChat.Text) + +local UseCppTextTruncation = FlagSettings.UseCppTextTruncation() + +local BUBBLE_PADDING = 10 +local DEFAULT_THUMBNAIL = "rbxasset://textures/ui/LuaChat/icons/share-game-thumbnail.png" +local EXTERIOR_PADDING = 3 +local GAME_CARD_BUTTON_CLICKED_EVENT = "clickBtnFromLinkCardInChat" +local GAME_MASK_IMAGE = "rbxasset://textures/ui/LuaChat/9-slice/gr-mask-game-icon.png" +local GAME_PLAY_INTENT = "gamePlayIntent" +local GAME_PLAY_EVENT_CONTEXT = "PlayGameFromLinkCard" +local ICON_SIZE = 64 +local INTERIOR_PADDING = 12 +local LINK_CARD_CLICKED_EVENT = "clickLinkCardInChat" +local PIN_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-pin.png" +local PIN_ICON_HORIZONTAL_PADDING = 10 +local PIN_ICON_SIZE = 20 +local PIN_ICON_VERTICAL_PADDING = 8 +local PIN_PRESSED_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-pinpressed.png" +local PLACE_INFO_THUMBNAIL_SIZE = 50 +local TOUCH_CONTEXT = "touch" + +local function isOutgoingMessage(message) + local localUserId = tostring(Players.LocalPlayer.UserId) + return message.senderTargetId == localUserId +end + +local UrlSupportNewGamesAPI = settings():GetFFlag("UrlSupportNewGamesAPI") +local LuaChatAssetCardsSelfTerminateConnection = settings():GetFFlag("LuaChatAssetCardsSelfTerminateConnection") + +local AssetCard = {} +AssetCard.__index = AssetCard + +function AssetCard.new(appState, message, assetId) + local self = {} + setmetatable(self, AssetCard) + + local state = appState.store:getState() + local user = state.Users[message.senderTargetId] + local username = user and user.name or "unknown user" + + self._analytics = appState.analytics + self.appState = appState + self.paddingObject = nil + self.message = message + self.bubbleType = "AssetCard" + self.connections = {} + self.cardBodyClick = nil + self.assetId = assetId + self.conversationId = message.conversationId + self.universeId = nil + self.pinButtonClick = nil + + self.luaChatPlayTogetherEnabled = FlagSettings.IsLuaChatPlayTogetherEnabled( + self.appState.store:getState().FormFactor) + + self.tail = Create.new "ImageLabel" { + Name = "Tail", + Size = UDim2.new(0, 6, 0, 6), + BackgroundTransparency = 1, + } + + self.actionLabel = Create.new "TextLabel" { + Name = "ActionLabel", + BackgroundTransparency = 1, + + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.new(0.8, 0, 0.8, 0), + Position = UDim2.new(0.5, 0, 0.5, 0), + TextSize = Constants.Font.FONT_SIZE_20, + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSans, + Text = self.appState.localization:Format("Feature.Chat.Action.ViewAssetDetails"), + } + + self.actionButton = Create.new "ImageButton" { + Name = "Action", + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(0.5, 1), + Position = UDim2.new(0.5, 0, 1, 0), + Size = UDim2.new(1, 0, 0, 32), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(3,3,4,4), + Image = "rbxasset://textures/ui/LuaChat/9-slice/input-default.png", + self.actionLabel, + } + + local textLabelWidth + if self.luaChatPlayTogetherEnabled then + textLabelWidth = -(PIN_ICON_SIZE + (2 * INTERIOR_PADDING) + BUBBLE_PADDING) + else + textLabelWidth = -(INTERIOR_PADDING + BUBBLE_PADDING) + end + self.Title = Create.new "TextLabel" { + Name = "Title", + TextTruncate = UseCppTextTruncation and Enum.TextTruncate.AtEnd or nil, + BackgroundTransparency = 1, + + AnchorPoint = Vector2.new(0, 0), + TextSize = Constants.Font.FONT_SIZE_20, + Size = UDim2.new(1, textLabelWidth, 0, 20), + Position = UDim2.new(0, 0, 0, 0), + + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSans, + TextXAlignment = Enum.TextXAlignment.Left + } + + self.Icon = Create.new "ImageLabel" { + Name = "Icon", + BackgroundTransparency = 1, + Position = UDim2.new(0, 0, 0, self.Title.TextSize + INTERIOR_PADDING), + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + + Create.new "ImageLabel" { + Name = "Mask", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = GAME_MASK_IMAGE, + ImageColor3 = Constants.Color.WHITE, + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(1, 0, 1, 0), + SliceCenter = Rect.new(3,3,4,4), + } + } + + self.Details = Create.new "TextLabel" { + Name = "Details", + BackgroundTransparency = 1, + Size = UDim2.new(1, - INTERIOR_PADDING -ICON_SIZE, 0, ICON_SIZE), + Position = self.Icon.Position + UDim2.new(0 , INTERIOR_PADDING + ICON_SIZE, 0, 0), + + TextColor3 = Constants.Color.GRAY2, + Font = Enum.Font.SourceSans, + TextSize = Constants.Font.FONT_SIZE_14, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextWrapped = true, + } + + self.fadeScreen = Create.new "Frame" { + Name = "FadeScreen", + BackgroundTransparency = 0, + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Color3.new(1, 1, 1), + BorderSizePixel = 0, + } + + + self.Content = Create.new "ImageButton" { + Name = "Content", + BackgroundTransparency = 1, + Size = UDim2.new(1, -BUBBLE_PADDING * 2, 1, -BUBBLE_PADDING * 2), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Visible = false, + + self.actionButton, + self.Title, + self.Icon, + self.Details, + self.fadeScreen, + } + + if self.luaChatPlayTogetherEnabled then + self.PinIcon = Create.new "ImageLabel" { + Name = "PinIcon", + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, PIN_ICON_SIZE, 0, PIN_ICON_SIZE), + Image = PIN_ICON, + } + + self.PinButton = Create.new "TextButton" { + Name = "PinButton", + Text = "", + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(1, 0), + Position = UDim2.new(1, BUBBLE_PADDING, 0, -BUBBLE_PADDING), + Size = UDim2.new(0, PIN_ICON_SIZE + 2 * PIN_ICON_HORIZONTAL_PADDING, + 0, PIN_ICON_SIZE + 2 * PIN_ICON_VERTICAL_PADDING), + + self.PinIcon + } + self.PinButton.Parent = self.Content + end + + self.bubble = Create.new "ImageLabel" { + Name = "Bubble", + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(1, 0), + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(0, 267, 1, 0), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(10, 10, 11, 11), + LayoutOrder = 2, + + self.Content, + self.tail, + } + + self.usernameLabel = Create.new "TextLabel" { + Name = "UsernameLabel", + Font = Enum.Font.SourceSans, + TextSize = Constants.Font.FONT_SIZE_12, + Visible = false, + BackgroundTransparency = 1, + Size = UDim2.new(1, -56, 0, 16), + Position = UDim2.new(0, 56, 0, 0), + TextColor3 = Constants.Color.GRAY2, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + Text = username, + } + + self.bubbleContainer = Create.new "Frame" { + Name = "BubbleContainer", + BackgroundTransparency = 1, + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, 0), + + self.bubble, + self.usernameLabel, + } + + self.rbx = Create.new "Frame" { + Name = "AssetCard", + BackgroundTransparency = 1, + + Create.new "UIListLayout" { + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + }, + + self.bubbleContainer, + } + + -- 'isOutgoing' means "is sent by the local user". This function separates the tail position & color + if isOutgoingMessage(message) then + self.tail.AnchorPoint = Vector2.new(0, 0) + self.tail.Position = UDim2.new(1, 0, 0, 0) + self.tail.ImageColor3 = Color3.new(1, 1, 1) + + self.bubble.ImageColor3 = Color3.new(1, 1, 1) + self.bubble.AnchorPoint = Vector2.new(1, 0) + self.bubble.Position = UDim2.new(1, -10, 0, 0) + + self.rbx.UIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Right + else + self.tail.AnchorPoint = Vector2.new(1, 0) + self.tail.Position = UDim2.new(0, 0, 0, 0) + self.tail.ImageColor3 = Color3.new(1, 1, 1) + + self.bubble.ImageColor3 = Color3.new(1, 1, 1) + self.bubble.AnchorPoint = Vector2.new(0, 0) + self.bubble.Position = UDim2.new(0, 54, 0, 0) + + self.rbx.UIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Left + end + + self.appStateConnection = self.appState.store.Changed:Connect(function(state) + self:Update(state) + end) + table.insert(self.connections, self.appStateConnection) + + local connection = self.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + self:Resize() + end) + table.insert(self.connections, connection) + + self:Update(state) + + return self +end + +function AssetCard:Resize() + local formFactor = self.appState.store:getState().FormFactor + local bubbleSizeOffsetY = Constants:GetFormFactorSpecific(formFactor).ASSET_CARD_HORIZONTAL_MARGIN + self.bubble.Size = UDim2.new(1, -bubbleSizeOffsetY, 0, 92 + ICON_SIZE) + + local containerHeight = self.bubble.AbsoluteSize.Y + + if self.usernameLabel.Visible then + containerHeight = containerHeight + self.usernameLabel.AbsoluteSize.Y + end + + self.bubbleContainer.Size = UDim2.new(1, 0, 0, containerHeight) + + local height = 0 + for _, child in ipairs(self.rbx:GetChildren()) do + if child:IsA("GuiObject") then + height = height + child.AbsoluteSize.Y + end + end + + if not UseCppTextTruncation then + Text.TruncateTextLabel(self.Title, "...") + end + self.rbx.Size = UDim2.new(1, 0, 0, height + EXTERIOR_PADDING*2) +end + +function AssetCard:onPinPressed() + self.PinIcon.Image = PIN_PRESSED_ICON + self.userInputServiceCon = UserInputService.InputEnded:Connect(function() + self:onPinRelease() + end) +end + +function AssetCard:onPinRelease() + if self.userInputServiceCon then + self.userInputServiceCon:Disconnect() + self.userInputServiceCon = nil + end + self.PinIcon.Image = PIN_ICON +end + +function AssetCard:Update(newState) + local placeInfo = newState.ChatAppReducer.PlaceInfos[self.assetId] + if placeInfo == nil then + self:ShowLoadingIndicator(true) + self.appState.store:dispatch(GetMultiplePlaceInfos({self.assetId})) + else + self.placeInfo = placeInfo + self.Title.Text = placeInfo.name + + local description = placeInfo.description:gsub("%s", " ") + if description:gsub("^%s+$", "") == "" then + description = self.appState.localization:Format("Feature.Chat.Label.NoDescriptionYet") + end + self.Details.Text = description + self.universeId = placeInfo.universeId + + if self.luaChatPlayTogetherEnabled then + if self.pinButtonClick then self.pinButtonClick:Disconnect() end + if self.pinButtonInputBegin then self.pinButtonInputBegin:Disconnect() end + + self.pinButtonClick = self.PinButton.Activated:Connect(function() + self.appState.store:dispatch(PlayTogetherActions.PinGame(self.conversationId, self.universeId)) + end) + self.pinButtonInputBegin = self.PinButton.InputBegan:Connect(function() + self:onPinPressed() + end) + end + + if UrlSupportNewGamesAPI then + local thumbnail = newState.ChatAppReducer.PlaceThumbnails[placeInfo.imageToken] + if thumbnail == nil then + self.appState.store:dispatch(GetPlaceThumbnail( + placeInfo.imageToken, PLACE_INFO_THUMBNAIL_SIZE, PLACE_INFO_THUMBNAIL_SIZE + )) + else + if thumbnail.image == '' then + self.thumbnail = DEFAULT_THUMBNAIL + else + self.thumbnail = thumbnail.image + end + self:FillThumbnail() + self:Show() + end + else + self.thumbnail = DEFAULT_THUMBNAIL + self:Show() + end + end + if self.cardBodyClick then self.cardBodyClick:Disconnect() end + if self.detailsButtonClick then self.detailsButtonClick:Disconnect() end + + self:StyleViewDetailsAsPlay(self.placeInfo ~= nil and self.placeInfo.isPlayable) + + self.cardBodyClick = getInputEvent(self.Content):Connect(function() + self:ReportAnEvent(LINK_CARD_CLICKED_EVENT, TOUCH_CONTEXT) + if self.placeInfo then + GuiService:BroadcastNotification(self.assetId, + NotificationType.VIEW_GAME_DETAILS) + end + end) + + self.detailsButtonClick = getInputEvent(self.actionButton):Connect(function() + self:ReportAnEvent(GAME_CARD_BUTTON_CLICKED_EVENT, TOUCH_CONTEXT) + if self.placeInfo then + if self.placeInfo.isPlayable then + self:ReportAnEvent(GAME_PLAY_INTENT, GAME_PLAY_EVENT_CONTEXT) + local gameParams = GameParams.fromPlaceId(self.assetId) + local payload = HttpService:JSONEncode(gameParams) + + GuiService:BroadcastNotification(payload, + NotificationType.LAUNCH_GAME) + else + GuiService:BroadcastNotification(self.assetId, + NotificationType.VIEW_GAME_DETAILS) + end + end + end) + + self:Resize() +end + +function AssetCard:ReportAnEvent(eventName, eventContext) + local additionalArgs + if eventName == GAME_PLAY_INTENT then + additionalArgs = { + conversationId = self.conversationId, + rootPlaceId = self.assetId + } + else + additionalArgs = { + conversationId = self.conversationId, + placeId = self.assetId + } + end + + self._analytics.EventStream:setRBXEventStream(eventContext, eventName, additionalArgs) +end + +function AssetCard:StyleViewDetailsAsPlay(isShowingAsPlay) + if isShowingAsPlay then + self.actionButton.ImageColor3 = Constants.Color.GREEN_PRIMARY + self.actionLabel.Text = self.appState.localization:Format("Common.VisitGame.Label.Play") + self.actionLabel.TextColor3 = Constants.Color.WHITE + else + self.actionButton.ImageColor3 = Constants.Color.WHITE + self.actionLabel.Text = self.appState.localization:Format("Feature.Chat.Action.ViewAssetDetails") + self.actionLabel.TextColor3 = Constants.Color.GRAY1 + end +end + +function AssetCard:Show() + self.Content.Visible = true + spawn(function() + while (not self.Icon.IsLoaded) do wait() end + + self:ShowLoadingIndicator(false) + local fadeInTween = TweenService:Create( + self.fadeScreen, + TweenInfo.new(0.4), + {BackgroundTransparency = 1} + ) + fadeInTween:Play() + end) + + if LuaChatAssetCardsSelfTerminateConnection then + if self.appStateConnection then + self.appStateConnection:Disconnect() + end + end +end + +function AssetCard:FillThumbnail() + self.Icon.Image = self.thumbnail or "" +end + +function AssetCard:ShowLoadingIndicator(isVisible) + if isVisible then + if not self.loadingIndicator then + local loadingIndicator = LoadingIndicator.new(self.appState) + loadingIndicator.rbx.AnchorPoint = Vector2.new(0.5, 0.5) + loadingIndicator.rbx.Position = UDim2.new(0.5, 0, 0.5, 0) + loadingIndicator.rbx.Size = UDim2.new(0.5, 0, 0.25, 0) + loadingIndicator.rbx.Parent = self.bubble + loadingIndicator:SetVisible(true) + self.loadingIndicator = loadingIndicator + end + else + if self.loadingIndicator then + self.loadingIndicator:Destroy() + end + end +end + +if not LuaChatAssetCardsSelfTerminateConnection then + function AssetCard:DisconnectUpdate() + if self.Content.Visible then + if self.appStateConnection then + self.appStateConnection:Disconnect() + self.appStateConnection = nil + end + end + end +end + +function AssetCard:Destruct() + for _, connection in pairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + if self.pinButtonClick then self.pinButtonClick:Disconnect() end + if self.pinButtonInputBegin then self.pinButtonInputBegin:Disconnect() end + self.rbx:Destroy() +end + +return AssetCard diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/BaseHeader.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/BaseHeader.lua new file mode 100644 index 0000000..9770a7f --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/BaseHeader.lua @@ -0,0 +1,324 @@ +local PlayerService = game:GetService("Players") +local CoreGui = game:GetService("CoreGui") +local TweenService = game:GetService("TweenService") + +local Modules = CoreGui.RobloxGui.Modules + +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Components = LuaChat.Components + +local ChatGameDrawer = require(Components.ChatGameDrawer) +local Constants = require(LuaChat.Constants) +local DialogInfo = require(LuaChat.DialogInfo) +local GetUser = require(LuaChat.Actions.GetUser) +local PaddedImageButton = require(LuaChat.Components.PaddedImageButton) +local Roact = require(CoreGui.RobloxGui.Modules.Common.Roact) +local RoactRodux = require(CoreGui.RobloxGui.Modules.Common.RoactRodux) +local Text = require(Common.Text) + +local PLATFORM_SPECIFIC_CONSTANTS = { + [Enum.Platform.Android] = { + BACK_BUTTON_ASSET_ID = "rbxasset://textures/ui/LuaChat/icons/ic-back-android.png", + }, + + Default = { + BACK_BUTTON_ASSET_ID = "rbxasset://textures/ui/LuaChat/icons/ic-back.png", + }, +} + +local TITLE_LABEL_MAX_WIDTH = 200 +local TITLE_LABEL_HEIGHT = 25 + +local function getPlatformSpecific(platform) + return PLATFORM_SPECIFIC_CONSTANTS[platform] or PLATFORM_SPECIFIC_CONSTANTS.Default +end + +local BaseHeader = {} + +--[[ + This type of pseudo-inheritance for components is usually bad, but this is a special exception. + It is not recommended to do this. +]] +function BaseHeader:Template() + local class = {} + for key, value in pairs(self) do + class[key] = value + end + return class +end + +function BaseHeader:SetPlatform(platform) + self.platform = platform +end + +function BaseHeader:SetTitle(text) + local label = self.titleLabel + if label then + local labelFont = label.Font + local labelTextSize = label.TextSize + label.Text = Text.Truncate(text, labelFont, labelTextSize, TITLE_LABEL_MAX_WIDTH, "...") + local titleTextLength = Text.GetTextWidth(label.Text, labelFont, labelTextSize) + + self.titleLabel.Size = UDim2.new(0, titleTextLength, 0, TITLE_LABEL_HEIGHT) + end + + self.title = text +end + +function BaseHeader:SetDefaultSubtitle() + if (self.dialogType ~= DialogInfo.DialogType.Centered) and (self.dialogType ~= DialogInfo.DialogType.Left) then + self:SetSubtitle("") + return + end + + local displayText = "" + local player = PlayerService.LocalPlayer + if player then + local userId = tostring(player.UserId) + local localUser = self.appState.store:getState().Users[userId] + if localUser and not localUser.isFetching then + if player:GetUnder13() then + displayText = string.format("%s: <13", localUser.name) + else + displayText = string.format("%s: 13+", localUser.name) + end + else + self.appState.store:dispatch(GetUser(userId)) + return + end + end + + self:SetSubtitle(displayText) +end + +--[[ + Sets the Header's subtitle + + Pass an empty string to hide the subtitle completely. + + Otherwise pass nil to default to the userAge label. +]] +function BaseHeader:SetSubtitle(displayText) + assert(type(displayText) == "nil" or type(displayText) == "string", + "Invalid argument number #1 to SetSubtitle, expected string or nil.") + + self.subtitle = displayText + + if displayText == "" then + self.innerSubtitle.Visible = false + else + self.innerSubtitle.Visible = true + self.innerSubtitle.Text = displayText + end +end + +function BaseHeader:SetBackButtonEnabled(enabled) + self.backButton.rbx.Visible = enabled +end + + +function BaseHeader:AddButton(button) + table.insert(self.buttons, button) + button.rbx.Parent = self.innerButtons + button.rbx.LayoutOrder = #self.buttons +end + +function BaseHeader:AddContent(content) + content.rbx.Parent = self.innerContent +end + +function BaseHeader:SetConnectionState(connectionState) + if not self.subtitle then + self:SetDefaultSubtitle() + end + + if self.dialogType == DialogInfo.DialogType.Right then + return + end + + if connectionState ~= self.connectionState and self.rbx.Parent ~= nil and self.rbx.Parent.Parent ~= nil then + if connectionState == Enum.ConnectionState.Disconnected then + self.isDisconnectedOpen = true + self:DoSizeTweening() + else + self.isDisconnectedOpen = false + self:DoSizeTweening() + end + self.connectionState = connectionState + end +end + +function BaseHeader:GetNewBackButton(dialogType) + local backButton + if dialogType == DialogInfo.DialogType.Modal then + backButton = PaddedImageButton.new(self.appState, "Close", "rbxasset://textures/ui/LuaChat/icons/ic-close-gray2.png") + elseif dialogType == DialogInfo.DialogType.Popup then + backButton = PaddedImageButton.new(self.appState, "Close", "rbxasset://textures/ui/LuaChat/icons/ic-close-white.png") + else + backButton = PaddedImageButton.new(self.appState, "Back", getPlatformSpecific(self.platform).BACK_BUTTON_ASSET_ID) + end + backButton.rbx.Position = UDim2.new(0, 0, 0.5, 0) + backButton.rbx.AnchorPoint = Vector2.new(0, 0.5) + return backButton +end + +function BaseHeader:Destroy() + for _, conn in pairs(self.connections) do + conn:Disconnect() + end + self.connections = {} + self.buttons = {} + + -- Destroy an attached Roact Gamedrawer if we have one: + if self.roactInstanceGameDrawer ~= nil then + Roact.teardown(self.roactInstanceGameDrawer) + end +end + +function BaseHeader:CreateGameDrawer(store, conversationId, autoshow) + -- Create the game drawer. Note it's probably not shown yet: + self.heightOfGameDrawer = 0 + self.hasFriendsInGame = false + self.isGameDrawerSized = false + self.isGameDrawerOpen = false + local newGameDrawer = Roact.createElement(ChatGameDrawer, { + AnchorPoint = Vector2.new(0, 0), + conversationId = conversationId, + Localization = self.appState.localization, + Position = UDim2.new(0, 0, 0, 0), + onSize = function(newSize, forceOpen) + self:SetGameDrawerSize(newSize, forceOpen) + end, + }) + + -- Wrap this in a store provider so we can monitor store state: + self.roactInstanceGameDrawer = Roact.mount(Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + newGameDrawer + }), self.rbx.GameDrawer, "ChatGameDrawer") +end + +function BaseHeader:ToggleGameDrawer() + if self.isGameDrawerOpen == false then + self.isGameDrawerOpen = true + else + self.isGameDrawerOpen = false + end + self:DoSizeTweening() +end + +-- Note: Start is called whenever we return to a conversation: +function BaseHeader:Start() + self.lastHeight = -1 + self.lastHeightDisconnected = -1 + self.lastHeightDrawer = -1 + if self.luaChatPlayTogetherEnabled then + if self.hasFriendsInGame then + self:OpenDrawer() + else + self:CloseDrawer() + end + end +end + +function BaseHeader:OpenDrawer() + if self.isGameDrawerSized and (not self.isGameDrawerOpen) then + self.isGameDrawerOpen = true + self:DoSizeTweening() + end +end + +function BaseHeader:CloseDrawer() + if self.isGameDrawerSized and self.isGameDrawerOpen then + self.isGameDrawerOpen = false + self:DoSizeTweening() + end +end + +function BaseHeader:InputFocus() + if self.luaChatPlayTogetherEnabled then + self:CloseDrawer() + end +end + +-- Set the open game drawer size - and tween it open: +function BaseHeader:SetGameDrawerSize(drawerSize, hasFriendsInGame) + if drawerSize > 0 and hasFriendsInGame and not self.isGameDrawerSized then + self.isGameDrawerOpen = true + end + self.heightOfGameDrawer = drawerSize + self.hasFriendsInGame = hasFriendsInGame + self.isGameDrawerSized = true + self:DoSizeTweening() +end + +-- Do tweening for any drawers that might be open: +function BaseHeader:DoSizeTweening() + -- Base height of our header is the main contents: + local desiredHeight = self.heightOfHeader + + -- Disconnected is the network error message: + if self.isDisconnectedOpen then + desiredHeight = desiredHeight + self.heightOfDisconnected + if not self.rbx.Disconnected.Visible or + (self.lastHeightDisconnected ~= self.heightOfDisconnected) then + local size = UDim2.new(1, 0, 0, self.heightOfDisconnected) + local resizeTween = TweenService:Create( + self.rbx.Disconnected, + TweenInfo.new(Constants.Tween.DEFAULT_TWEEN_TIME, Constants.Tween.DEFAULT_TWEEN_STYLE, Enum.EasingDirection.In), + { Size = size } + ) + resizeTween:Play() + self.rbx.Disconnected.Visible = true + self.lastHeightDisconnected = self.heightOfDisconnected + end + else + if self.rbx.Disconnected.Visible then + self.rbx.Disconnected.Size = UDim2.new(1, 0, 0, 0) + self.rbx.Disconnected.Visible = false + self.lastHeightDisconnected = 0 + end + end + + if self.luaChatPlayTogetherEnabled then + -- Game drawer is the list of games being played by participants of this conversation: + if self.isGameDrawerOpen and self.heightOfGameDrawer then + desiredHeight = desiredHeight + self.heightOfGameDrawer + if not self.rbx.GameDrawer.Visible or + (self.lastHeightDrawer ~= self.heightOfGameDrawer) then + self.rbx.GameDrawer.Visible = true + local size = UDim2.new(1, 0, 0, self.heightOfGameDrawer) + local resizeTween = TweenService:Create( + self.rbx.GameDrawer, + TweenInfo.new(Constants.Tween.DEFAULT_TWEEN_TIME, Constants.Tween.DEFAULT_TWEEN_STYLE, Enum.EasingDirection.In), + { Size = size } + ) + resizeTween:Play() + self.lastHeightDrawer = self.heightOfGameDrawer + end + else + if self.rbx.GameDrawer.Visible then + self.rbx.GameDrawer.Size = UDim2.new(1, 0, 0, 0) + self.rbx.GameDrawer.Visible = false + self.lastHeightDrawer = 0 + end + end + end + + -- Set the size of the main drawer: + if (self.lastHeight ~= desiredHeight) then + local size = UDim2.new(1, 0, 0, desiredHeight) + local resizeTween = TweenService:Create( + self.rbx, + TweenInfo.new(Constants.Tween.DEFAULT_TWEEN_TIME, Constants.Tween.DEFAULT_TWEEN_STYLE, Enum.EasingDirection.In), + { Size = size } + ) + resizeTween:Play() + self.lastHeight = desiredHeight + end +end + +return BaseHeader \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/BrowseGames.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/BrowseGames.lua new file mode 100644 index 0000000..bc0f9bb --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/BrowseGames.lua @@ -0,0 +1,148 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local ClearAllGames = require(LuaChat.Actions.ShareGameToChatFromChat.ClearAllGamesInSortsShareGameToChatFromChat) +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local DialogInfo = require(LuaChat.DialogInfo) +local Intent = DialogInfo.Intent +local ResetShareGameToChatAsync = require(LuaChat.Actions.ShareGameToChatFromChat.ResetShareGameToChatFromChatAsync) +local Roact = require(Common.Roact) +local RoactAnalytics = require(Modules.LuaApp.Services.RoactAnalytics) +local RoactLocalization = require(LuaApp.Services.RoactLocalization) +local RoactRodux = require(Common.RoactRodux) +local RoactServices = require(LuaApp.RoactServices) +local Signal = require(Common.Signal) + +local Components = LuaChat.Components +local HeaderLoader = require(Components.HeaderLoader) +local ResponseIndicator = require(Components.ResponseIndicator) +local SharedGameList = require(Components.SharedGameList) +local TabBarView = require(LuaChat.TabBarView) +local TabPageParameters = require(LuaChat.Models.TabPageParameters) + +local BrowseGames = {} +BrowseGames.__index = BrowseGames + +function BrowseGames.new(appState) + local self = { + appState = appState, + } + setmetatable(self, BrowseGames) + self.connections = {} + self._analytics = self.appState.analytics + self._localization = self.appState.localization + + self.responseIndicator = ResponseIndicator.new(appState) + self.responseIndicator:SetVisible(false) + + self.header = HeaderLoader.GetHeader(appState, Intent.BrowseGames) + self.header:SetDefaultSubtitle() + self.header:SetTitle(self.appState.localization:Format("Feature.Chat.ShareGameToChat.BrowseGames")) + self.header:SetBackButtonEnabled(true) + self.header:SetConnectionState(Enum.ConnectionState.Disconnected) + + local sharedGamesConfig = Constants.SharedGamesConfig + local GAME_PAGES = {} + + for _, sortName in ipairs(sharedGamesConfig.SortNames) do + table.insert( + GAME_PAGES, + TabPageParameters( + self._localization:Format(sharedGamesConfig.SortsAttribute[sortName].TILE_LOCALIZATION_KEY), + SharedGameList, + { + gameSort = sortName, + } + ) + ) + end + + self.rbx = Create.new"Frame" { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + + Create.new("UIListLayout") { + Name = "ListLayout", + SortOrder = Enum.SortOrder.LayoutOrder, + }, + + self.header.rbx, + + Create.new"Frame" { + Name = "Content", + BackgroundColor3 = Constants.Color.GRAY5, + BorderSizePixel = 0, + ClipsDescendants = true, + LayoutOrder = 1, + Size = UDim2.new(1, 0, 1, -self.header.heightOfHeader), + + self.responseIndicator.rbx, + }, + } + + self.roactInstanceGamesTabBarView = Roact.mount(Roact.createElement(RoactRodux.StoreProvider, { + store = appState.store, + }, { + Roact.createElement(RoactServices.ServiceProvider, { + services = { + [RoactAnalytics] = self._analytics, + [RoactLocalization] = self._localization, + } + }, { + Roact.createElement(TabBarView, { + tabs = GAME_PAGES, + }) + }), + + }), self.rbx.Content, "TabBarView") + + self.BackButtonPressed = Signal.new() + self.header.BackButtonPressed:Connect(function() + self:CleanGamesInSorts() + self.BackButtonPressed:Fire() + end) + + local headerSizeConnection = self.header.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + self:Resize() + end) + table.insert(self.connections, headerSizeConnection) + + return self +end + +function BrowseGames:CleanGamesInSorts() + self.appState.store:dispatch(ClearAllGames()) + self.appState.store:dispatch(ResetShareGameToChatAsync()) +end + +function BrowseGames:Resize() + local sizeContent = UDim2.new(1, 0, 1, -self.header.rbx.AbsoluteSize.Y) + self.rbx.Content.Size = sizeContent +end + +function BrowseGames:Update(current, previous) + self.header:SetConnectionState(current.ConnectionState) + + local isSharing = self.appState.store:getState().ChatAppReducer.ShareGameToChatAsync.sharingGame or false + self.responseIndicator:SetVisible(isSharing) +end + +function BrowseGames:Destruct() + for _, connection in pairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + + self.header:Destroy() + self.responseIndicator:Destruct() + Roact.unmount(self.roactInstanceGamesTabBarView) + self.rbx:Destroy() +end + +return BrowseGames \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatBubble.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatBubble.lua new file mode 100644 index 0000000..5c02186 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatBubble.lua @@ -0,0 +1,321 @@ +local Players = game:GetService("Players") +local GuiService = game:GetService("GuiService") + +local LuaChat = script.Parent.Parent +local UserThumbnail = require(script.Parent.UserThumbnail) +local TypingIndicator = require(script.Parent.TypingIndicator) + +local UserChatBubble = require(script.Parent.UserChatBubble) +local AssetCard = require(script.Parent.AssetCard) + +local Create = require(LuaChat.Create) +local Constants = require(LuaChat.Constants) +local WebApi = require(LuaChat.WebApi) + + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local LuaApp = Modules.LuaApp +local NotificationType = require(LuaApp.Enum.NotificationType) + +local RECEIVED_BUBBLE = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble2.png" +local RECEIVED_BUBBLE_WITH_TAIL = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble.png" +local RECEIVED_TAIL = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble-tip.png" + +local SENT_BUBBLE = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble-self2.png" +local SENT_BUBBLE_WITH_TAIL = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble-self.png" +local SENT_TAIL = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble-self-tip.png" + +local SENT_BUBBLE_OUTLINE = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble2.png" +local SENT_BUBBLE_OUTLINE_WITH_TAIL = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble-right.png" +local SENT_OUTLINE_TAIL = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble-tip-right.png" + +local function isOutgoingMessage(message) + local localUserId = tostring(Players.LocalPlayer.UserId) + return message.senderTargetId == localUserId +end + +local function isMessageSending(conversation, message) + if conversation and conversation.sendingMessages then + return conversation.sendingMessages:Get(message.id) ~= nil + end + return false +end + +local PROTOCOL_IDENTIFIERS = { + "https?://", "" +} + +local RESOURCE_NAMES = { + "www%.", "web%.", "" +} + +local WHITELISTED_DOMAINS = { + "roblox", "sitetest%d%.robloxlabs", "gametest%d%.robloxlabs" +} + +local MESSAGE_CONTENT_PATTERNS = { + GAME_LINK = "%.com/games[^%d]*(%d+)/?", +} + +local ChatBubble = {} + +ChatBubble.__index = ChatBubble + +ChatBubble.BubbleType = { + AssetCard = "AssetCard", + ChatBubble = "UserChatBubble", +} + +local function isFFlagEnabled(flagName) + local success, isFlagEnabled = pcall(function() + return settings():GetFFlag(flagName) + end) + + if success and not isFlagEnabled then + warn("Fast Flag:", flagName, "is currently not enabled.") + elseif not success then + warn("GetFFlag failed for flag:", flagName) + end + + return success and isFlagEnabled +end + +local function getBubbleImages(message, bubbleType) + if isOutgoingMessage(message) and bubbleType ~= ChatBubble.BubbleType.AssetCard then + return SENT_BUBBLE, SENT_BUBBLE_WITH_TAIL, SENT_TAIL + elseif isOutgoingMessage(message) then + return SENT_BUBBLE_OUTLINE, SENT_BUBBLE_OUTLINE_WITH_TAIL, SENT_OUTLINE_TAIL + else + return RECEIVED_BUBBLE, RECEIVED_BUBBLE_WITH_TAIL, RECEIVED_TAIL + end +end + +function ChatBubble.new(appState, message) + local self = {} + setmetatable(self, ChatBubble) + + local conversationId = message.conversationId + local isSending = isMessageSending(appState.store:getState().ChatAppReducer.Conversations[conversationId], message) + + self.appState = appState + self.message = message + self.bubbles = {} + self.connections = {} + self.tailVisible = false + + + self.rbx = Create.new "Frame" { + Name = "ChatContainer", + BackgroundTransparency = 1, + + Size = UDim2.new(1, 0, 0, 0), + + Create.new "UIListLayout" { + SortOrder = Enum.SortOrder.LayoutOrder, + }, + } + + if isFFlagEnabled("LuaChatAssetCards") then + if message.moderated or isSending then + self:AddBubble(UserChatBubble.new(appState, message), 1) + + -- Specifically whitelist strings with .com/games in the url + elseif message.content:lower():match(MESSAGE_CONTENT_PATTERNS.GAME_LINK) then + local text = self:FilterForLinks() + + -- Flush remaining text if it is not empty + if text:gsub("%s+","") ~= "" then + self:AddBubble(UserChatBubble.new(appState, message, text)) + end + else + self:AddBubble(UserChatBubble.new(appState, message), 1) + end + else + -- Default if lua chat asset cards is off + self:AddBubble(UserChatBubble.new(appState, message), 1) + end + + return self +end + +function ChatBubble:FilterForLinks() + local text = self.message.content + for _, protocol in pairs(PROTOCOL_IDENTIFIERS) do + for _, resource in pairs(RESOURCE_NAMES) do + for _, domain in pairs(WHITELISTED_DOMAINS) do + + local constructedUrlPattern = protocol .. resource .. domain .. MESSAGE_CONTENT_PATTERNS.GAME_LINK + for assetId in text:lower():gmatch(constructedUrlPattern) do + local linkStart, endLink = text:lower():find("[^%s*]*" .. constructedUrlPattern .. "[^%s*]*") + if linkStart then + local textBefore = text:sub(1, linkStart - 1) + + if textBefore:gsub("%s+","") ~= "" then + self:AddBubble(UserChatBubble.new(self.appState, self.message, textBefore)) + end + + self:AddBubble(AssetCard.new(self.appState, self.message, assetId)) + + text = text:sub(endLink + 1) + else + return text + end + end + + end + end + end + + return text +end + +function ChatBubble:AddBubble(bubble, placement) + table.insert(self.bubbles, placement or #self.bubbles+1 ,bubble) + bubble.rbx.Parent = self.rbx + bubble.LayoutOrder = placement or #self.bubbles + + for i=1,#self.bubbles do + self.bubbles[i].LayoutOrder = i + end + + local connection = bubble.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + self:Resize() + end) + table.insert(self.connections, connection) + + self:Update() +end + +function ChatBubble:SetUsernameVisible(value) + local bubblePos = self.bubbles[1].bubble.Position + + if value then + self.bubbles[1].usernameLabel.Visible = true + + self.bubbles[1].bubble.Position = UDim2.new( + bubblePos.X.Scale, + bubblePos.X.Offset, + 0, + 16 + ) + else + self.bubbles[1].usernameLabel.Visible = false + + self.bubbles[1].bubble.Position = UDim2.new( + bubblePos.X.Scale, + bubblePos.X.Offset, + 0, + 0 + ) + end + + self:Resize() +end + +function ChatBubble:SetTypingIndicatorVisible(value) + if value and not self.indicator then + local indicator = TypingIndicator.new(self.appState, .4) + indicator.rbx.AnchorPoint = Vector2.new(0,0.5) + indicator.rbx.Position = UDim2.new(0, self.bubbles[1].usernameLabel.TextBounds.X + 3, 0.5, 0) + indicator.rbx.Parent = self.bubbles[1].usernameLabel + + self.indicator = indicator + elseif self.indicator and not value then + self.indicator:Destroy() + self.indicator = nil + end +end + +function ChatBubble:SetThumbnailVisible(value) + if value then + self.thumbnail = UserThumbnail.new(self.appState, self.message.senderTargetId, true) + self.thumbnail.rbx.Position = UDim2.new(0, 10, 0, 0) + self.thumbnail.rbx.Overlay.ImageColor3 = Constants.Color.GRAY6 + self.thumbnail.rbx.Parent = self.bubbles[1].bubbleContainer + + self.thumbnail.clicked:Connect(function() + local user = self.appState.store:getState().Users[self.message.senderTargetId] + local userId = user and user.id + if userId then + GuiService:BroadcastNotification(WebApi.MakeUserProfileUrl(userId), + NotificationType.VIEW_PROFILE) + end + end) + else + if self.thumbnail then + self.thumbnail:Destruct() + end + end +end + +function ChatBubble:SetTailVisible(value) + self.tailVisible = value + if not self.bubbles[1] then return end + + for i, bubble in pairs(self.bubbles) do + local bubbleImage, bubbleWithTail, tailImage = getBubbleImages(self.message, bubble.bubbleType) + if value and i == 1 then + bubble.bubble.Image = bubbleWithTail + bubble.tail.Image = tailImage + bubble.tail.Visible = true + else + bubble.bubble.Image = bubbleImage + bubble.tail.Visible = false + end + end +end + +function ChatBubble:SetPaddingObject(object) + if not self.bubbles[1] then return end + + if self.bubbles[1].paddingObject then + self.bubbles[1].paddingObject:Destroy() + end + + object.LayoutOrder = 1 + object.Parent = self.bubbles[1].rbx + self.bubbles[1].paddingObject = object + self.bubbles[1]:Resize() +end + +function ChatBubble:Resize() + for _,bubble in pairs(self.bubbles) do + bubble:Resize() + end + local height = 0 + for _, child in ipairs(self.rbx:GetChildren()) do + if child:IsA("GuiObject") then + height = height + child.AbsoluteSize.Y + end + end + + self.rbx.Size = UDim2.new(1, 0, 0, height) +end + + +function ChatBubble:Update() + self:SetTailVisible(self.tailVisible) + self:Resize() +end + + + +function ChatBubble:Destruct() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + + for _, bubble in ipairs(self.bubbles) do + bubble:Destruct() + end + + if self.thumbnail then + self.thumbnail:Destruct() + end + self.thumbnail = nil + + self.rbx:Destroy() +end + +return ChatBubble \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatBubble.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatBubble.spec.lua new file mode 100644 index 0000000..5e9a230 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatBubble.spec.lua @@ -0,0 +1,443 @@ +return function() + local CoreGui = game:GetService("CoreGui") + + local Modules = CoreGui.RobloxGui.Modules + local LuaChat = Modules.LuaChat + + local MessageModel = require(LuaChat.Models.Message) + local ChatBubble = require(LuaChat.Components.ChatBubble) + local AppState = require(LuaChat.AppState) + + describe("new", function() + it("should create with no errors", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "testing", + }) + + local chat = ChatBubble.new(appState, message) + + expect(chat).to.be.ok() + end) + end) + + describe("asset card creation", function() + describe("web protocol filtering", function() + it("should make a card with http:// prefix", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "http://www.roblox.com/games/1818/Classic-Crossroads" + }) + + local chat = ChatBubble.new(appState, message) + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + end) + + it("should make a card with https:// prefix", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "https://www.roblox.com/games/1818/Classic-Crossroads" + }) + + local chat = ChatBubble.new(appState, message) + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + end) + + it("should make a card with no protocol prefix", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "www.roblox.com/games/1818/Classic-Crossroads" + }) + + local chat = ChatBubble.new(appState, message) + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + end) + end) + + describe("web resource names", function() + describe("www.", function() + it("should make a card with https:// protocol", function() + local appState = AppState.mock() + local message1 = MessageModel.mock({ + content = "https://www.roblox.com/games/1818/Classic-Crossroads" + }) + local chat1 = ChatBubble.new(appState, message1) + expect(#chat1.bubbles).to.equal(1) + expect(chat1.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat1.bubbles[1].assetId).to.equal("1818") + end) + it("should make a card with http:// protocol", function() + local appState = AppState.mock() + local message2 = MessageModel.mock({ + content = "http://www.roblox.com/games/1818/Classic-Crossroads" + }) + local chat2 = ChatBubble.new(appState, message2) + expect(#chat2.bubbles).to.equal(1) + expect(chat2.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat2.bubbles[1].assetId).to.equal("1818") + end) + + it("should make a card with no protocol", function() + local appState = AppState.mock() + local message3 = MessageModel.mock({ + content = "www.roblox.com/games/1818/Classic-Crossroads" + }) + local chat3 = ChatBubble.new(appState, message3) + expect(#chat3.bubbles).to.equal(1) + expect(chat3.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat3.bubbles[1].assetId).to.equal("1818") + end) + end) + + describe("web.", function() + it("should make a card with https:// protocol", function() + local appState = AppState.mock() + local message1 = MessageModel.mock({ + content = "https://web.roblox.com/games/1818/Classic-Crossroads" + }) + local chat1 = ChatBubble.new(appState, message1) + expect(#chat1.bubbles).to.equal(1) + expect(chat1.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat1.bubbles[1].assetId).to.equal("1818") + end) + it("should make a card with http:// protocol", function() + local appState = AppState.mock() + local message2 = MessageModel.mock({ + content = "http://web.roblox.com/games/1818/Classic-Crossroads" + }) + local chat2 = ChatBubble.new(appState, message2) + expect(#chat2.bubbles).to.equal(1) + expect(chat2.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat2.bubbles[1].assetId).to.equal("1818") + end) + + it("should make a card with no protocol", function() + local appState = AppState.mock() + local message3 = MessageModel.mock({ + content = "web.roblox.com/games/1818/Classic-Crossroads" + }) + local chat3 = ChatBubble.new(appState, message3) + expect(#chat3.bubbles).to.equal(1) + expect(chat3.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat3.bubbles[1].assetId).to.equal("1818") + end) + end) + end) + + describe("whitelisted domains", function() + describe("roblox.com", function() + it("should make a card with http protocol prefixes", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "https://www.roblox.com/games/1818/Classic-Crossroads" + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + end) + + it("should make a card with only a resouce name", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "www.roblox.com/games/1818/Classic-Crossroads" + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + end) + + it("should make a card without a http protocol prefixes", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "roblox.com/games/1818/Classic-Crossroads" + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + end) + end) + + describe("sitetest1.robloxlabs.com", function() + it("should make a card with http protocol prefixes", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "https://www.sitetest1.robloxlabs.com/games/1818/Classic-Crossroads" + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + end) + + it("should make a card with only a resouce name", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "web.sitetest1.robloxlabs.com/games/1818/Classic-Crossroads" + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + end) + + it("should make a card without a http protocol prefixes", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "sitetest1.robloxlabs.com/games/1818/Classic-Crossroads" + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + end) + end) + + describe("gametest1.robloxlabs.com", function() + it("should make a card with http protocol prefixes", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "https://www.gametest1.robloxlabs.com/games/1818/Classic-Crossroads" + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + end) + + it("should make a card with only a resouce name", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "web.gametest1.robloxlabs.com/games/1818/Classic-Crossroads" + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + end) + + it("should make a card without a http protocol prefixes", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "gametest1.robloxlabs.com/games/1818/Classic-Crossroads" + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + end) + end) + + describe("invalid domains", function() + it("should not create an asset card for non roblox domains", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "https://www.google.com/games/1818/Classic-Crossroads" + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.never.equal("AssetCard") + end) + end) + end) + + describe("game link format", function() + it("should create an asset card with link without appended title text", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "https://www.roblox.com/games/1818/" + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + end) + + it("should ignore case when creating an asset card with link", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "https://www.ROBLOX.com/games/1818/" + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + end) + + it("should not create an asset card for non games", function() + local appState = AppState.mock() + local message1 = MessageModel.mock({ + content = "https://www.roblox.com/users/1922632/profile" + }) + + local chat1 = ChatBubble.new(appState, message1) + + expect(#chat1.bubbles).to.equal(1) + expect(chat1.bubbles[1].bubbleType).to.never.equal("AssetCard") + + local message2 = MessageModel.mock({ + content = "https://www.roblox.com/Groups/Group.aspx?gid=3475371" + }) + + local chat2 = ChatBubble.new(appState, message2) + + expect(#chat2.bubbles).to.equal(1) + expect(chat2.bubbles[1].bubbleType).to.never.equal("AssetCard") + + local message3 = MessageModel.mock({ + content = "https://www.roblox.com/catalog/100929604/Green-Sparkle-Time-Fedora" + }) + + local chat3 = ChatBubble.new(appState, message3) + + expect(#chat3.bubbles).to.equal(1) + expect(chat3.bubbles[1].bubbleType).to.never.equal("AssetCard") + end) + end) + + describe("roblox links with text", function() + it("should create one UserChatBubble when presented with raw text", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "testing", + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("UserChatBubble") + expect(chat.bubbles[1].textContent.Text).to.equal("testing") + end) + + it("should create one an AssetCard when presented with roblox link", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "https://www.roblox.com/games/1818/Classic-Crossroads", + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(1) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + end) + + it("should create two cards when presented a link with text if the link is first", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "https://www.roblox.com/games/1818/Classic-Crossroads Play my game!" + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(2) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + expect(chat.bubbles[2].bubbleType).to.equal("UserChatBubble") + expect(chat.bubbles[2].textContent.Text).to.equal(" Play my game!") + end) + + it("should create two cards when presented a link with text if the link is second", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "Play my game! https://www.roblox.com/games/1818/Classic-Crossroads" + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(2) + expect(chat.bubbles[1].bubbleType).to.equal("UserChatBubble") + expect(chat.bubbles[1].textContent.Text).to.equal("Play my game! ") + expect(chat.bubbles[2].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[2].assetId).to.equal("1818") + end) + + it("should create three cards when presented a link with text", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "Play my game! https://www.roblox.com/games/1818/Classic-Crossroads Or dont." + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(3) + expect(chat.bubbles[1].bubbleType).to.equal("UserChatBubble") + expect(chat.bubbles[1].textContent.Text).to.equal("Play my game! ") + expect(chat.bubbles[2].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[2].assetId).to.equal("1818") + expect(chat.bubbles[3].bubbleType).to.equal("UserChatBubble") + expect(chat.bubbles[3].textContent.Text).to.equal(" Or dont.") + end) + + it("should optionally accept the final / when parsing a roblox link", function() + local appState = AppState.mock() + local message1 = MessageModel.mock({ + content = "https://www.roblox.com/games/1818/" + }) + local chat1 = ChatBubble.new(appState, message1) + + expect(#chat1.bubbles).to.equal(1) + expect(chat1.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat1.bubbles[1].assetId).to.equal("1818") + + local message2 = MessageModel.mock({ + content = "https://www.roblox.com/games/1337" + }) + local chat2 = ChatBubble.new(appState, message2) + + expect(#chat2.bubbles).to.equal(1) + expect(chat2.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat2.bubbles[1].assetId).to.equal("1337") + end) + + it("should handle text in between two links", function() + local appState = AppState.mock() + local message = MessageModel.mock({ + content = "https://www.roblox.com/games/1818/ or https://www.roblox.com/games/1337" + }) + + local chat = ChatBubble.new(appState, message) + + expect(#chat.bubbles).to.equal(3) + expect(chat.bubbles[1].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[1].assetId).to.equal("1818") + expect(chat.bubbles[2].bubbleType).to.equal("UserChatBubble") + expect(chat.bubbles[2].textContent.Text).to.equal(" or ") + expect(chat.bubbles[3].bubbleType).to.equal("AssetCard") + expect(chat.bubbles[3].assetId).to.equal("1337") + end) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatDisabledIndicator.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatDisabledIndicator.lua new file mode 100644 index 0000000..7ff2e3a --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatDisabledIndicator.lua @@ -0,0 +1,114 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local Signal = require(Common.Signal) + +local Text = require(LuaChat.Text) +local getInputEvent = require(LuaChat.Utils.getInputEvent) + +local PADDING_HEIGHT = 32 +local TEXT_FONT = Enum.Font.SourceSans +local TEXT_FONT_SIZE = Constants.Font.FONT_SIZE_18 + +local ChatDisabledIndicator = {} + +ChatDisabledIndicator.__index = ChatDisabledIndicator + +function ChatDisabledIndicator.new(appState) + local self = {} + + local imageButtonText = appState.localization:Format("Feature.Chat.Label.PrivacySettings") + local ibtWidth = Text.GetTextWidth(imageButtonText, Enum.Font.SourceSans, Constants.Font.FONT_SIZE_16) + + local imageButton = Create.new "ImageButton" { + Name = "PrivacySettings", + AutoButtonColor = false, + Size = UDim2.new(0, ibtWidth+6, 0, 36), + BackgroundTransparency = 1, + LayoutOrder = 3, + BorderSizePixel = 0, + ScaleType = "Slice", + SliceCenter = Rect.new(3,3,4,4), + Image = "rbxasset://textures/ui/LuaChat/9-slice/input-default.png", + ImageColor3 = Constants.Color.GREEN_PRIMARY, + + Create.new "TextLabel" { + Name = "Title", + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Font = Enum.Font.SourceSans, + TextSize = Constants.Font.FONT_SIZE_16, + TextColor3 = Constants.Color.WHITE, + Text = imageButtonText, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + } + } + + local textLabelText = appState.localization:Format("Feature.Chat.Message.TurnOnChat") + + local textLabel = Create.new "TextLabel" { + BackgroundTransparency = 1, + LayoutOrder = 2, + Font = TEXT_FONT, + TextColor3 = Constants.Color.GRAY2, + TextSize = TEXT_FONT_SIZE, + Text = textLabelText, + TextWrapped = true + } + + self.rbx = Create.new "Frame" { + Name = "ChatDisabledIndicator", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 300), + + Create.new "Frame" { + Name = "IndicatorInner", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 160), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + + Create.new "UIListLayout" { + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }, + + Create.new "ImageLabel" { + BackgroundTransparency = 1, + Size = UDim2.new(0, 72, 0, 72), + LayoutOrder = 1, + Image = "rbxasset://textures/ui/LuaChat/icons/ic-friends.png", + }, + + textLabel, + + imageButton, + }, + } + + local function rescaleFromParentSize() + local parentSize = self.rbx.AbsoluteSize + local tltHeight = Text.GetTextHeight(textLabelText, TEXT_FONT, TEXT_FONT_SIZE, parentSize.X) + textLabel.Size = UDim2.new(0,parentSize.X,0,tltHeight+PADDING_HEIGHT) + end + + self.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(rescaleFromParentSize) + rescaleFromParentSize() + + self.openPrivacySettings = Signal.new() + getInputEvent(self.rbx.IndicatorInner.PrivacySettings):Connect(function() + self.openPrivacySettings:Fire() + end) + + setmetatable(self, ChatDisabledIndicator) + + return self +end + +return ChatDisabledIndicator \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatGameCard.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatGameCard.lua new file mode 100644 index 0000000..aa26864 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatGameCard.lua @@ -0,0 +1,467 @@ +-- +-- ChatGameCard +-- +-- This is a game that is shown (and possibly pinned) at the top of a chat +-- conversation. +-- + +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local Constants = require(LuaApp.Constants) +local ContextualMenu = require(LuaApp.Components.ContextualMenu) +local FriendCarousel = require(LuaChat.Components.FriendCarousel) +local GetMultiplePlaceInfos = require(LuaChat.Actions.GetMultiplePlaceInfos) +local GetPlaceThumbnail = require(LuaChat.Actions.GetPlaceThumbnail) +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) +local RoactAnalyticsGameCardLoaded = require(LuaChat.Services.RoactAnalyticsGameCardLoaded) +local RoactServices = require(LuaApp.RoactServices) + +local ChatGameCard = Roact.PureComponent:extend("ChatGameCard") + +local ICON_SIZE_SMALL = 36 +local ICON_SIZE_LARGE = 60 +local ICON_SIZE_SMALL_AMENDED = 48 + +local CARD_MARGINS = 12 +local CARD_HEIGHT_SMALL = ICON_SIZE_SMALL + (CARD_MARGINS * 2) +local CARD_HEIGHT_LARGE = ICON_SIZE_LARGE + (CARD_MARGINS * 2) + +local GAME_TEXT_COLOR = Constants.Color.GRAY1 +local GAME_TEXT_FONT = Enum.Font.SourceSans +local GAME_TEXT_HEIGHT = 25 +local GAME_TEXT_HEIGHT_WITH_SUBTITLE = 20 +local GAME_TEXT_SIZE = 23 +local GAME_TEXT_SIZE_WITH_SUBTITLE = 20 + +local SUBTITLE_TEXT_SIZE = 18 +local SUBTITLE_TEXT_COLOR = Constants.Color.GRAY2 + +local CAROUSEL_SMALL_ICON_SIZE = 24 +local CAROUSEL_LARGE_ICON_SIZE = 32 +local CAROUSEL_SMALL_GAP = 3 +local CAROUSEL_LARGE_GAP = 9 +local CAROUSEL_SMALL_DOT_SIZE = 8 +local CAROUSEL_LARGE_DOT_SIZE = 10 + +local ACTION_BUTTON_WIDTH = 60 +local ACTION_BUTTON_HEIGHT = 32 +local ACTION_COLOR_PLAY = Constants.Color.GREEN_PRIMARY +local ACTION_COLOR_TEXT = Constants.Color.WHITE + +local DEBUG_OUTLINE = 0 +local DEBUG_TRANSPARENCY = 1 + +local DEFAULT_GAME_ICON = "rbxasset://textures/ui/LuaApp/icons/ic-game.png" +local FADEOUT_MASK_IMAGE = "rbxasset://textures/ui/LuaChat/graphic/friendmask.png" +local FADEOUT_MASK_WIDTH = 10 +local GAME_MASK_IMAGE = "rbxasset://textures/ui/LuaChat/9-slice/gr-mask-game-icon.png" +local ROUNDED_BUTTON = "rbxasset://textures/ui/LuaChat/9-slice/input-default.png" + +-- Set up some default state for this control: +function ChatGameCard:init() + self.state = { + isMenuOpen = false, + } + + -- Localize strings. Needs to be done in context because of the way the localization object is being passed to us: + local localization = self.props.Localization + self.MenuInfoPlayGame = { + displayIcon = "rbxasset://textures/ui/LuaApp/icons/ic-games.png", + name = "PlayGameButton", + displayName = localization:Format("Feature.Chat.Drawer.PlayGame"), + } + self.MenuInfoPinGame = { + displayIcon = "rbxasset://textures/ui/LuaChat/icons/ic-pin.png", + name = "PinGameButton", + displayName = localization:Format("Feature.Chat.Drawer.PinGame") + } + self.MenuInfoUnpinGame = { + displayIcon = "rbxasset://textures/ui/LuaChat/icons/ic-unpin-20x20.png", + name = "UnpinGameButton", + displayName = localization:Format("Feature.Chat.Drawer.UnpinGame") + } + self.MenuItemInfoViewGameDetail = { + displayIcon = "rbxasset://textures/ui/LuaChat/icons/ic-viewdetails-20x20.png", + name = "ViewDetailsButton", + displayName = localization:Format("Feature.Chat.Drawer.ViewDetails"), + } +end + +function ChatGameCard:render() + -- Information about the game is passed in as properties. Action to take + -- *on* the game should also be passed in, so we have containment and this + -- module only knows the miniumum necessary. + + local parentLayoutOrder = self.props.LayoutOrder + + -- Visual properties of this game card: + local isPinnedGame = self.props.isPinnedGame or false + local isRecommendedGame = self.props.isRecommended or false + + local game = self.props.game + local getPlaceInfo = self.props.getPlaceInfo + local getPlaceThumbnail = self.props.getPlaceThumbnail + local localization = self.props.Localization + local onGamePin = self.props.onGamePin + local onGameStart = self.props.onGameStart + local onGameUnpin = self.props.onGameUnpin + local onViewDetails = self.props.onViewDetails + local placeInfos = self.props.placeInfos + local placeThumbnails = self.props.placeThumbnails or {} + local renderWidth = self.props.renderWidth or 0 + + -- Unpack from our properties: + local gameFriends = game.friends or {} + local placeId = game.placeId + + -- Read or retrieve information about our place: + local placeInfo = placeInfos[placeId] + local gameName + local imageToken = nil + local universeId = nil + local isPlayable = false + if (placeInfo == nil) then + getPlaceInfo(placeId) + gameName = "(" .. localization:Format("Feature.Chat.Drawer.Loading") .. ")" + else + gameName = placeInfo.name + imageToken = placeInfo.imageToken + universeId = placeInfo.universeId + isPlayable = placeInfo.isPlayable + end + + -- Configure some dimensions based on properties, the GameInfo section in + -- particular changes for a large (pinned) vs regular size card: + local cardHeight = CARD_HEIGHT_SMALL + local carouselItemDotSize = CAROUSEL_SMALL_DOT_SIZE + local carouselItemGap = CAROUSEL_SMALL_GAP + local carouselItemHeight = CAROUSEL_SMALL_ICON_SIZE + local friendAlignment = Enum.HorizontalAlignment.Right + local gameIconHeight = ICON_SIZE_SMALL + local gameIconWidth = gameIconHeight + local gameTextHeight = GAME_TEXT_HEIGHT + local gameTextSize = GAME_TEXT_SIZE + local infoFillDirection = Enum.FillDirection.Horizontal + local subtitle = "" + local subtitleVisibility = false + + -- If we don't have an action button, zero the reserved width: + local buttonWidth = ACTION_BUTTON_WIDTH + if not isPlayable then + buttonWidth = 0 + end + + -- Scaling of elements: + local friendsWidthOffset = 0 + local friendsWidthScale = 1 + local textWidthOffset = 0 + local textWidthScale = 1 + + if isPinnedGame then + cardHeight = CARD_HEIGHT_LARGE + carouselItemDotSize = CAROUSEL_LARGE_DOT_SIZE + carouselItemGap = CAROUSEL_LARGE_GAP + carouselItemHeight = CAROUSEL_LARGE_ICON_SIZE + friendAlignment = Enum.HorizontalAlignment.Left + gameIconHeight = ICON_SIZE_LARGE + gameIconWidth = gameIconHeight + infoFillDirection = Enum.FillDirection.Vertical + elseif isRecommendedGame then + carouselItemHeight = 0 + gameTextHeight = GAME_TEXT_HEIGHT_WITH_SUBTITLE + gameTextSize = GAME_TEXT_SIZE_WITH_SUBTITLE + infoFillDirection = Enum.FillDirection.Vertical + subtitle = localization:Format("Feature.Chat.Drawer.Recommended") + subtitleVisibility = true + else + friendsWidthScale = 0.5 + friendsWidthOffset = 0 + textWidthScale = 0.5 + textWidthOffset = 0 + end + + -- This is how much space we have in the center of the card: + local centerNegativeSpace = CARD_MARGINS + gameIconWidth + CARD_MARGINS + CARD_MARGINS + local countFriends = #gameFriends + + -- Default is Play if nobody else is in the game, Join if we have friends: + local actionText + if isPlayable then + centerNegativeSpace = centerNegativeSpace + buttonWidth + CARD_MARGINS + local actionTextKey = "Feature.Chat.Drawer.Play" + if countFriends > 0 then + actionTextKey = "Feature.Chat.Drawer.Join" + end + actionText = localization:Format(actionTextKey) + end + local actionColor = ACTION_COLOR_PLAY + local actionTextColor = ACTION_COLOR_TEXT + + -- ...and as a last metrics step, adjust the ratio of the game name and friend carousel: + if not (isPinnedGame or isRecommendedGame) then + local actualSpace = renderWidth - centerNegativeSpace + if actualSpace > 0 then + local friendSpace = carouselItemHeight * countFriends + local textAdjust = (actualSpace * 0.5) - (friendSpace + CARD_MARGINS) + if textAdjust > 0 then + textWidthOffset = textWidthOffset + textAdjust + friendsWidthOffset = friendsWidthOffset - textAdjust + end + end + end + + -- Obtain the thumbnail for this game: + local gameIcon = DEFAULT_GAME_ICON + if placeInfo then + local thumbnail = placeThumbnails[imageToken] + if thumbnail == nil then + if imageToken and imageToken ~= "" then + if gameIconWidth == ICON_SIZE_SMALL then + getPlaceThumbnail(imageToken, ICON_SIZE_SMALL_AMENDED, ICON_SIZE_SMALL_AMENDED) + else + getPlaceThumbnail(imageToken, gameIconWidth, gameIconHeight) + end + end + elseif thumbnail.image ~= "" then + gameIcon = thumbnail.image + end + end + + -- Build up a horizontal list of items for our card: + local cardItems = {} + cardItems["Layout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + Padding = UDim.new(0, CARD_MARGINS) + }) + + -- Game icon: + local layoutOrder = 1 + cardItems["GameIcon"] = Roact.createElement("ImageLabel", { + BackgroundTransparency = DEBUG_TRANSPARENCY, + BorderSizePixel = DEBUG_OUTLINE, + LayoutOrder = layoutOrder, + Size = UDim2.new(0, gameIconHeight, 0, gameIconHeight), + Image = gameIcon, + }, { + Mask = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = GAME_MASK_IMAGE, + ImageColor3 = Constants.Color.WHITE, + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(1, 0, 1, 0), + SliceCenter = Rect.new(3,3,4,4), + }), + }) + layoutOrder = layoutOrder + 1 + + cardItems["GameInfo"] = Roact.createElement("Frame", { + BackgroundTransparency = DEBUG_TRANSPARENCY, + BorderSizePixel = DEBUG_OUTLINE, + ClipsDescendants = true, + LayoutOrder = layoutOrder, + Size = UDim2.new(1, -centerNegativeSpace, 1, 0), + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = infoFillDirection, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + + GameName = Roact.createElement("TextLabel", { + BackgroundTransparency = DEBUG_TRANSPARENCY, + BorderSizePixel = DEBUG_OUTLINE, + ClipsDescendants = true, + Font = GAME_TEXT_FONT, + LayoutOrder = 1, + Size = UDim2.new(textWidthScale, textWidthOffset, 0, gameTextHeight), + Text = gameName, + TextColor3 = GAME_TEXT_COLOR, + TextSize = gameTextSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top + },{ + MaskRight = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = FADEOUT_MASK_IMAGE, + Position = UDim2.new(1, -FADEOUT_MASK_WIDTH, 0, 0), + Size = UDim2.new(0, FADEOUT_MASK_WIDTH, 1, 0), + ZIndex = 2, + }), + }), + + Subtitle = Roact.createElement("TextLabel", { + BackgroundTransparency = DEBUG_TRANSPARENCY, + BorderSizePixel = DEBUG_OUTLINE, + ClipsDescendants = true, + Font = GAME_TEXT_FONT, + LayoutOrder = 1, + Size = UDim2.new(textWidthScale, textWidthOffset, 0, ICON_SIZE_SMALL - gameTextHeight), + Text = subtitle, + TextColor3 = SUBTITLE_TEXT_COLOR, + TextSize = SUBTITLE_TEXT_SIZE, + TextXAlignment = Enum.TextXAlignment.Left, + Visible = subtitleVisibility, + }), + + GameFriends = Roact.createElement(FriendCarousel, { + dotSize = carouselItemDotSize, + friends = gameFriends, + HorizontalAlignment = friendAlignment, + itemGap = carouselItemGap, + itemSize = carouselItemHeight, + LayoutOrder = 2, + Size = UDim2.new(friendsWidthScale, friendsWidthOffset, 0, carouselItemHeight), + }), + }) + layoutOrder = layoutOrder + 1 + + if isPlayable then + cardItems["ActionButton"] = Roact.createElement("ImageButton", { + AutoButtonColor = false, + BackgroundTransparency = 1, + BorderSizePixel = DEBUG_OUTLINE, + Image = ROUNDED_BUTTON, + ImageColor3 = actionColor, + LayoutOrder = layoutOrder, + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(0, ACTION_BUTTON_WIDTH, 0, ACTION_BUTTON_HEIGHT), + SliceCenter = Rect.new(3,3,4,4), + + [Roact.Event.Activated] = function(rbx) + onGameStart() + end + },{ + ActionLabel = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = GAME_TEXT_FONT, + LayoutOrder = layoutOrder, + Size = UDim2.new(1, 0, 1, 0), + Text = actionText, + TextColor3 = actionTextColor, + TextSize = GAME_TEXT_SIZE, + }), + }) + end + + if self.state.isMenuOpen then + local menuItems + if isPinnedGame then + menuItems = { self.MenuInfoUnpinGame } + else + menuItems = { self.MenuInfoPinGame } + end + + if isPlayable then + table.insert(menuItems, 1, self.MenuInfoPlayGame) + end + table.insert(menuItems, self.MenuItemInfoViewGameDetail) + + local callbackCancel = function() + self:setState({ isMenuOpen = false }) + end + + local callbackSelect = function(item) + if item.name == self.MenuInfoPlayGame.name then + onGameStart() + elseif item.name == self.MenuInfoPinGame.name then + onGamePin(universeId) + elseif item.name == self.MenuInfoUnpinGame.name then + onGameUnpin() + elseif item.name == self.MenuItemInfoViewGameDetail.name then + onViewDetails() + end + callbackCancel() + end + + cardItems["ContextMenu"] = Roact.createElement(ContextualMenu, { + callbackCancel = callbackCancel, + callbackSelect = callbackSelect, + menuItems = menuItems, + screenShape = self.state.screenShape, + }) + end + + -- Put a clickable wrapper around the entire card: + return Roact.createElement("TextButton", { + AutoButtonColor = false, + BackgroundTransparency = DEBUG_TRANSPARENCY, + BorderSizePixel = DEBUG_OUTLINE, + LayoutOrder = parentLayoutOrder, + Size = UDim2.new(1, 0, 0, cardHeight), + Text = "", + [Roact.Event.Activated] = function(rbx) + -- TODO: Move this screen size functionality into a helper component + -- so that it doesn't get repeated everywhere (see: MOBLUAPP-241). + + -- We need to know the size of the screen, so we can position the + -- popout component appropriately. So we climb up the object + -- heirachy until we find the current ScreenGui: + local screenWidth = 0 + local screenHeight = 0 + local screenGui = rbx:FindFirstAncestorOfClass("ScreenGui") + if screenGui ~= nil then + screenWidth = screenGui.AbsoluteSize.X + screenHeight = screenGui.AbsoluteSize.Y + end + + self:setState({ + isMenuOpen = true, + screenShape = { + x = rbx.AbsolutePosition.X, + y = rbx.AbsolutePosition.Y, + width = rbx.AbsoluteSize.X, + height = rbx.AbsoluteSize.Y, + parentWidth = screenWidth, + parentHeight = screenHeight, + }, + }) + end, + + }, cardItems) +end + +function ChatGameCard:didMount() + -- TODO: tkim has a fix for this in progress, but it's incomplete. + -- This code needs to be removed for now. + -- See: https://jira.roblox.com/browse/SOC-2232 + -- local conversationId = tostring(self.props.conversationId) + -- local placeId = tostring(self.props.game.placeId) + -- self.props.analytics.reportGameCardLoadedInLuaChat(conversationId, placeId) +end + +ChatGameCard = RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + placeInfos = state.ChatAppReducer.PlaceInfos, + placeThumbnails = state.ChatAppReducer.PlaceThumbnails, + } + end, + function(dispatch) + return { + getPlaceInfo = function(placeId) + dispatch(GetMultiplePlaceInfos({placeId})) + end, + getPlaceThumbnail = function(imageToken, iconWidth, iconHeight) + dispatch(GetPlaceThumbnail(imageToken, iconWidth, iconHeight)) + end, + } + end +)(ChatGameCard) + +ChatGameCard = RoactServices.connect({ + analytics = RoactAnalyticsGameCardLoaded, +})(ChatGameCard) + +return ChatGameCard \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatGameCard.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatGameCard.spec.lua new file mode 100644 index 0000000..8f70824 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatGameCard.spec.lua @@ -0,0 +1,41 @@ +return function() + local LocalizationService = game:GetService("LocalizationService") + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local AppReducer = require(Modules.LuaApp.AppReducer) + local Localization = require(Modules.LuaApp.Localization) + local MockId = require(Modules.LuaApp.MockId) + local Roact = require(Modules.Common.Roact) + local RoactRodux = require(Modules.Common.RoactRodux) + local Rodux = require(Modules.Common.Rodux) + + local ChatGameCard = require(Modules.LuaChat.Components.ChatGameCard) + + local localization = Localization.new(LocalizationService.RobloxLocaleId) + + it("should create and destroy without errors", function() + + local store = Rodux.Store.new(AppReducer) + + local renderWidth = 500 + local game = { + placeId = MockId(), + friends = { { uid = MockId() }, { uid = MockId() }, + { uid = MockId() }, { uid = MockId() }, { uid = MockId() } }, + } + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + GameCard = Roact.createElement(ChatGameCard, { + game = game, + isPinnedGame = true, + Localization = localization, + renderWidth = renderWidth, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatGameDrawer.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatGameDrawer.lua new file mode 100644 index 0000000..d97d8ec --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatGameDrawer.lua @@ -0,0 +1,577 @@ +-- +-- ChatGameDrawer +-- +-- Contains ChatGameCard objects that represent pinned or in progress games. +-- This lives at the top of a conversation window. +-- + +local CoreGui = game:GetService("CoreGui") +local GuiService = game:GetService("GuiService") +local HttpService = game:GetService("HttpService") + +local Modules = CoreGui.RobloxGui.Modules + +local Common = Modules.Common +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local Analytics = require(Common.Analytics) +local ChatGameCard = require(LuaChat.Components.ChatGameCard) +local Constants = require(LuaApp.Constants) +local GameParams = require(LuaChat.Models.GameParams) +local PlayTogetherActions = require(LuaChat.Actions.PlayTogetherActions) +local Roact = require(Common.Roact) +local RoactRodux = require(Common.RoactRodux) +local SortedActivelyPlayedGames = require(LuaChat.SortedActivelyPlayedGames) +local User = require(LuaApp.Models.User) + +local ChatGameDrawer = Roact.PureComponent:extend("ChatGameDrawer") + +local urlSupportNewGamesAPI = settings():GetFFlag("UrlSupportNewGamesAPI") + +-- Drawer properties: +local DRAWER_BACKGROUND_COLOR = Constants.Color.WHITE +local BORDER_SIZE = 12 +local DRAWER_SHADOW_IMAGE = "rbxasset://textures/ui/LuaChat/graphic/gr-overlay-shadow.png" +local DRAWER_SHADOW_HEIGHT = 5 + +-- Pointer up to the image: +local ICON_POINTER_HEIGHT = 6 +local ICON_POINTER_WIDTH = 12 +local ICON_POINTER_UP = "rbxasset://textures/ui/LuaApp/dropdown/gr-tip-up.png" +local ICON_POINTER_FROMEDGE = 20 + +-- "Pinned Game" text properties: +local PINNED_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-pin.png" +local PINNED_ICON_SIZE = 12 +local PINNED_SPACER = 6 + +local PINNED_TEXT_COLOR = Constants.Color.GRAY2 +local PINNED_TEXT_FONT = Enum.Font.SourceSans +local PINNED_TEXT_SIZE = 15 + +local PINNED_DIVIDER_COLOR = Constants.Color.GRAY4 +local PINNED_BOTTOM_BORDER = Constants.Color.GRAY4 +local PINNED_BOTTOM_BACKGROUND = Constants.Color.GRAY6 + +local SMALL_DIVIDER_OFFSET = 60 +local CARD_HEIGHT_SMALL = 60 +local CARD_HEIGHT_LARGE = 84 + +local MORE_TEXT_SIZE = 18 +local MORE_TEXT_PADDING = 9 +local MORE_TEXT_COLOR = Constants.Color.GRAY1 + +-- Set up some default state for this control: +function ChatGameDrawer:init() + self._analytics = Analytics.new() + self.state = { + isExpanded = false, + pointerPosition = UDim2.new(1, -ICON_POINTER_FROMEDGE, 0, 0), + pointerSet = false, + renderWidth = 0, + } + + self.isGameDrawerSized = false +end + +function ChatGameDrawer:UnpinGame() + local playTogetherUnpinGame = self.props.playTogetherUnpinGame + playTogetherUnpinGame(self.props.conversationId) +end + +-- Games are pinned by universeId (but they're accessed using placeId elsewhere): +function ChatGameDrawer:PinGame(universeId) + local playTogetherPinGame = self.props.playTogetherPinGame + playTogetherPinGame(self.props.conversationId, universeId) +end + +function ChatGameDrawer:ViewGameDetails(placeId) + GuiService:BroadcastNotification(placeId, GuiService:GetNotificationTypeList().VIEW_GAME_DETAILS_ANIMATED) +end + +function ChatGameDrawer:GameStart(placeId) + -- Report player join game via play together + self:ReportPlayerJoinInAGame(placeId) + -- Start a game here. + local gameParams = GameParams.fromPlaceId(placeId) + local payload = HttpService:JSONEncode(gameParams) + GuiService:BroadcastNotification(payload, GuiService:GetNotificationTypeList().LAUNCH_GAME) +end + +function ChatGameDrawer:GetGamesFromConversation(conversationId) + local conversations = self.props.conversations + local mostRecentlyPlayedGames = self.props.mostRecentlyPlayedGames + local users = self.props.users + + -- Don't know if this is necessary: + if not urlSupportNewGamesAPI then + error("Server doesn't support new games API.") + return { countFriendsInGames = 0, games = {}, } + end + + -- Early out if we have no conversation: + if conversationId == nil or conversationId == "nil" then + return { countFriendsInGames = 0, games = {}, } + end + + -- Find the specific conversation we're interested in: + if not conversations then + return { countFriendsInGames = 0, games = {}, } + end + + local conversation = conversations[conversationId] + if not conversation then + warn("ChatGameDrawer - Can't find conversation, id:" .. conversationId .. " t:" .. type(conversationId)) + return { countFriendsInGames = 0, games = {}, } + end + + local pinnedGameRootPlaceId = conversation.pinnedGame.rootPlaceId + local inGameParticipants = {} + local mostRecentPlayedPlayableGamePlaceId = mostRecentlyPlayedGames.playableGamePlaceId + + for _, userId in pairs(conversation.participants) do + local user = users[userId] + if user ~= nil then + if (user.presence == User.PresenceType.IN_GAME) and user.placeId then + table.insert(inGameParticipants, user) + end + end + end + + local countFriendsInGames = #inGameParticipants + if countFriendsInGames > 0 then + return { + countFriendsInGames = countFriendsInGames, + games = SortedActivelyPlayedGames.getSortedGamesPlusEmptyPinned(pinnedGameRootPlaceId, inGameParticipants), + } + end + + if pinnedGameRootPlaceId then + return { + countFriendsInGames = 0, + games = { + { + friends = {}, + pinned = true, + placeId = pinnedGameRootPlaceId, + recommended = false, + } + } + } + end + + if mostRecentPlayedPlayableGamePlaceId then + return { + countFriendsInGames = 0, + games = { + { + friends = {}, + pinned = false, + placeId = mostRecentPlayedPlayableGamePlaceId, + recommended = true, + } + } + } + end + + return { + countFriendsInGames = 0, + games = {} + } +end + +function ChatGameDrawer:ReportPlayerJoinInAGame(placeId) + local conversationId = self.props.conversationId + local eventContext = "touch" + local eventName = "playTogether" + + local PlayerService = game:GetService("Players") + local player = PlayerService.LocalPlayer + local userId = "UNKNOWN" + if player then + userId = tostring(player.UserId) + end + + local additionalArgs = { + uid = userId, + cid = conversationId, + placeId = placeId + } + self._analytics.EventStream:setRBXEventStream(eventContext, eventName, additionalArgs) +end + +function ChatGameDrawer:render() + local anchorPoint = self.props.AnchorPoint + local conversationId = self.props.conversationId + local localization = self.props.Localization + local onSize = self.props.onSize + local parentLayoutOrder = self.props.layoutOrder + local position = self.props.Position + + local isExpanded = self.state.isExpanded + local pointerPosition = self.state.pointerPosition or UDim2.new(1, -ICON_POINTER_FROMEDGE, 0, 0) + local pointerSet = self.state.pointerSet + local pointerTransparency = 1 + if pointerSet then + pointerTransparency = 0 + end + + local gameInfo = self:GetGamesFromConversation(conversationId) + local countGames = #gameInfo.games + + -- Early exit if we have nothing to display in the drawer: + if countGames == 0 then + spawn(function() + onSize(0, false) + end) + return nil + end + + local hasFriendsActive = gameInfo.countFriendsInGames > 0 + + -- If we have active friends and this is the first time rendering, expand: + if hasFriendsActive and not self.isGameDrawerSized then + self.isGameDrawerSized = true + if not isExpanded then + spawn(function() + self:setState({ isExpanded = true }) + end) + return nil + end + end + + -- Build up our drop-down items here for display inside our main element: + local gameItems = {} + local drawerHeight = 0 + gameItems["Layout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Top, + }) + + -- Display our pinned game (if we have one): + local layoutOrder = 1 + local hasPinnedGame = false + + for _, game in ipairs(gameInfo.games) do + if game.pinned then + hasPinnedGame = true + local placeId = game.placeId + + gameItems["PinnedTitle"] = Roact.createElement("TextButton", { + AutoButtonColor = false, + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + Size = UDim2.new(1, 0, 0, PINNED_ICON_SIZE + (BORDER_SIZE * 2)), + Text = "", + }, { + Icon = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = PINNED_ICON, + Position = UDim2.new(0, BORDER_SIZE, 0.5, 0), + Size = UDim2.new(0, PINNED_ICON_SIZE, 0, PINNED_ICON_SIZE), + }), + + Text = Roact.createElement("TextLabel", { + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = PINNED_TEXT_FONT, + Position = UDim2.new(0, BORDER_SIZE + PINNED_ICON_SIZE + PINNED_SPACER, 0.5, 0), + Size = UDim2.new(1, -(PINNED_ICON_SIZE + PINNED_SPACER + (BORDER_SIZE * 2)), 1, 0), + Text = localization:Format("Feature.Chat.Drawer.PinnedGame"), + TextColor3 = PINNED_TEXT_COLOR, + TextSize = PINNED_TEXT_SIZE, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + }), + }) + layoutOrder = layoutOrder + 1 + drawerHeight = drawerHeight + PINNED_ICON_SIZE + (BORDER_SIZE * 2) + + gameItems["Divider"] = Roact.createElement("Frame", { + BackgroundColor3 = PINNED_DIVIDER_COLOR, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + Size = UDim2.new(1, -(BORDER_SIZE * 2), 0, 1), + }) + layoutOrder = layoutOrder + 1 + drawerHeight = drawerHeight + 1 + + -- Index by pinned status and placeId: + gameItems["Pinned" .. placeId] = Roact.createElement(ChatGameCard, { + game = game, + conversationId = self.conversationId, + isPinnedGame = true, + LayoutOrder = layoutOrder, + Localization = localization, + renderWidth = self.state.renderWidth, + onGameStart = function() + self:GameStart(placeId) + end, + onGameUnpin = function() + self:UnpinGame() + end, + onViewDetails = function() + self:ViewGameDetails(placeId) + end, + }) + layoutOrder = layoutOrder + 1 + drawerHeight = drawerHeight + CARD_HEIGHT_LARGE + + -- If we have more games to display, add a spacer between the pinned and regular games: + if (countGames > 1) then + gameItems["Spacer"] = Roact.createElement("Frame", { + BackgroundColor3 = PINNED_BOTTOM_BACKGROUND, + BackgroundTransparency = 0, + BorderColor3 = PINNED_BOTTOM_BORDER, + BorderSizePixel = 1, + LayoutOrder = layoutOrder, + Size = UDim2.new(1, 0, 0, PINNED_SPACER), + }) + layoutOrder = layoutOrder + 1 + drawerHeight = drawerHeight + PINNED_SPACER + end + + -- Done, we found our pinned game in the list: + break + end + end + + -- Display all the other games in progress - but only if we don't have a + -- pinned game or we're expanded: + if (not hasPinnedGame) or isExpanded then + local hasRegularGame = false + for _, game in ipairs(gameInfo.games) do + if not game.pinned then + -- If we've already added a regular game, insert a spacer before the next: + if hasRegularGame then + gameItems[layoutOrder] = Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + Size = UDim2.new(1, 0, 0, 1), + },{ + divider = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(1, 0), + BorderSizePixel = 0, + BackgroundColor3 = PINNED_DIVIDER_COLOR, + Position = UDim2.new(1, 0, 0, 0), + Size = UDim2.new(1, -SMALL_DIVIDER_OFFSET, 0, 1), + }), + }) + layoutOrder = layoutOrder + 1 + drawerHeight = drawerHeight + 1 + end + + local placeId = game.placeId + -- Index as an unpinned game and placeId: + gameItems["Game" .. placeId] = Roact.createElement(ChatGameCard, { + game = game, + conversationId = self.conversationId, + isPinnedGame = false, + isRecommended = game.recommended, + LayoutOrder = layoutOrder, + Localization = self.props.Localization, + renderWidth = self.state.renderWidth, + onGameStart = function() + self:GameStart(placeId) + end, + onGamePin = function(universeId) + self:PinGame(universeId) + end, + onViewDetails = function() + self:ViewGameDetails(placeId) + end, + }) + layoutOrder = layoutOrder + 1 + drawerHeight = drawerHeight + CARD_HEIGHT_SMALL + + -- If we're not expanded, we've hit our limit: + if not isExpanded then + break + end + + -- Next card will have a spacer before it. + hasRegularGame = true + end + end + end + + -- The final item in the list should be the text to either show more or hide: + local endTextDivider = false + local endTextShow = false + local endLocalizeText = "" + if countGames == 0 then + endTextShow = true + endLocalizeText = localization:Format("Feature.Chat.Drawer.NoGames") + elseif countGames > 1 then + if isExpanded then + endLocalizeText = localization:Format("Feature.Chat.Drawer.ShowLess") + else + endLocalizeText = localization:Format("Feature.Chat.Drawer.ShowMore") .. " (+" .. (countGames - 1) .. ")" + end + endTextDivider = true + endTextShow = true + end + + if endTextShow then + if endTextDivider then + gameItems["DividerBottom"] = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = PINNED_DIVIDER_COLOR, + LayoutOrder = layoutOrder, + Size = UDim2.new(1, 0, 0, 1), + }) + layoutOrder = layoutOrder + 1 + drawerHeight = drawerHeight + PINNED_SPACER + end + + gameItems["ShowButton"] = Roact.createElement("TextButton", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = PINNED_TEXT_FONT, + LayoutOrder = layoutOrder, + Size = UDim2.new(1, 0, 0, MORE_TEXT_SIZE + (MORE_TEXT_PADDING * 2)), + Text = endLocalizeText, + TextColor3 = MORE_TEXT_COLOR, + TextSize = MORE_TEXT_SIZE, + [Roact.Event.Activated] = function() + self:setState({ isExpanded = not self.state.isExpanded }) + end + }) + drawerHeight = drawerHeight + MORE_TEXT_SIZE + (MORE_TEXT_PADDING * 2) + end + + -- Define the shadow component to hang off the bottom of the list: + -- Note: Do not count the height since this isn't inside the frame. + local shadow = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Image = DRAWER_SHADOW_IMAGE, + Size = UDim2.new(1, 0, 0, DRAWER_SHADOW_HEIGHT), + Position = UDim2.new(0, 0, 1, 0), + }) + + spawn(function() + onSize(drawerHeight, hasFriendsActive) + end) + + -- Create and return the main control itself: + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BackgroundColor3 = DRAWER_BACKGROUND_COLOR, + BackgroundTransparency = 0, + BorderSizePixel = 0, + ClipsDescendants = false, + LayoutOrder = parentLayoutOrder, + Position = position, + Size = UDim2.new(1, 0, 1, 0), + [Roact.Ref] = function(rbx) + if not rbx then + return + end + spawn(function() + self:resolveMetrics(rbx) + end) + end, + }, { + Pointer = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 1), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = ICON_POINTER_UP, + ImageTransparency = pointerTransparency, + Position = pointerPosition, + Size = UDim2.new(0, ICON_POINTER_WIDTH, 0, ICON_POINTER_HEIGHT), + }), + Shadow = shadow, + + Frame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = true, + Size = UDim2.new(1, 0, 1, 0), + }, + gameItems + ) + }) +end + +function ChatGameDrawer:resolveMetrics(rbx) + -- This function finds the ActiveGameIcon and positions an arrow pointing + -- at it from our open drawer. The position of the icon can change depending + -- on a number of factors so it can't be hardcoded. + -- + -- Also retrieves the actual width of our drawer to pass to child components + -- which need to be width-aware to render properly. + -- + -- Note 1: I didn't want to attach this on the icon because it needs to line + -- up with the edge of the drawer.) + -- + -- Note 2: we can't examine the GameDrawer position here because on the + -- initial pass through it is invisible with a position of 0,0 on screen. + + -- Find the conversation header: + local header = rbx:FindFirstAncestor("HeaderFrame") + if header == nil then + warn("Couldn't find header.") + return + end + + -- Find the "Play Together" icon: + local iconPlayTogether = header:FindFirstChild("TopGameIcon", true) + if iconPlayTogether == nil then + warn("Couldn't find Play Together icon.") + return + end + + -- Figure out where on the screen iconPlayTogether is, so + -- we can position our pointer directly underneath it: + local iconFromEdge = (header.AbsolutePosition.X + header.AbsoluteSize.X) - + (iconPlayTogether.AbsolutePosition.X + (iconPlayTogether.AbsoluteSize.X * 0.5)) + + -- Track our drawer's width for content-aware scaling: + local renderWidth = rbx.AbsoluteSize.X + + -- Prevent updating metrics if we already have the correct values: + if (not self.state.pointerSet) or + (self.state.renderWidth ~= renderWidth) or + (self.state.pointerPosition.X.Offset ~= -iconFromEdge) then + -- Update the pointer position state so it will render in the correct location: + -- Yes, we're using another spawn call here - but we need the 1-frame delay. + spawn(function() + self:setState({ + pointerPosition = UDim2.new(1, -iconFromEdge, 0, 0), + pointerSet = true, + renderWidth = renderWidth, + }) + end) + end +end + +ChatGameDrawer = RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + conversations = state.ChatAppReducer.Conversations, + mostRecentlyPlayedGames = state.ChatAppReducer.MostRecentlyPlayedGames, + users = state.Users, + } + end, + function(dispatch) + return { + playTogetherUnpinGame = function(conversationId) + dispatch(PlayTogetherActions.UnpinGame(conversationId)) + end, + playTogetherPinGame = function(conversationId, universeId) + dispatch(PlayTogetherActions.PinGame(conversationId, universeId)) + end, + } + end +)(ChatGameDrawer) + +return ChatGameDrawer \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatGameDrawer.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatGameDrawer.spec.lua new file mode 100644 index 0000000..aebb4f3 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatGameDrawer.spec.lua @@ -0,0 +1,37 @@ +return function() + local LocalizationService = game:GetService("LocalizationService") + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local AppReducer = require(Modules.LuaApp.AppReducer) + local Localization = require(Modules.LuaApp.Localization) + local MockId = require(Modules.LuaApp.MockId) + local Roact = require(Modules.Common.Roact) + local RoactRodux = require(Modules.Common.RoactRodux) + local Rodux = require(Modules.Common.Rodux) + + local ChatGameDrawer = require(Modules.LuaChat.Components.ChatGameDrawer) + + local localization = Localization.new(LocalizationService.RobloxLocaleId) + + it("should create and destroy without errors", function() + + local store = Rodux.Store.new(AppReducer) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + GameDrawer = Roact.createElement(ChatGameDrawer, { + AnchorPoint = Vector2.new(0, 0), + conversationId = MockId(), + Localization = localization, + Position = UDim2.new(0, 0, 0, 0), + onSize = function(newSize, forceOpen) + -- pass + end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatInputBar.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatInputBar.lua new file mode 100644 index 0000000..53b887c --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatInputBar.lua @@ -0,0 +1,346 @@ +local CoreGui = game:GetService("CoreGui") +local PlayerService = game:GetService("Players") +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat +local LuaApp = Modules.LuaApp + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local DialogInfo = require(LuaChat.DialogInfo) +local FormFactor = require(LuaApp.Enum.FormFactor) +local getInputEvent = require(LuaChat.Utils.getInputEvent) +local Intent = DialogInfo.Intent +local SetRoute = require(LuaChat.Actions.SetRoute) +local Signal = require(Common.Signal) +local Text = require(LuaChat.Text) + +local FFlagLuaChatShareGameToChatFromChat = settings():GetFFlag("LuaChatShareGameToChatFromChat") + +local GAME_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-game.png" +local INPUT_FRAME_IMAGE = "rbxasset://textures/ui/LuaChat/9-slice/input-default.png" +local PRESSED_GAME_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-game-pressed-24x24.png" +local SEND_BUTTON_IMAGE = "rbxasset://textures/ui/LuaChat/graphic/send-white.png" +local SEND_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-send.png" + +local GAME_ICON_BOTTOM_PADDING = 8 +local LINE_CUTOFF_PHONE = 4.5/5.0 +local LINE_CUTOFF_TABLET = 0.95 +local RIGHT_BUTTON_HEIGHT = 48 +local RIGHT_BUTTON_WIDTH = 52 +local SEND_BUTTON_BOTTOM_PADDING = 8 + +local function isMessageValid(text) + if #text >= 160 then + return false + end + + -- Only whitespace + if text:match("^%s*$") then + return false + end + + return true +end + +local ChatInputBar = {} + +function ChatInputBar.new(appState) + local self = {} + self.appState = appState + self.sendButtonEnabled = false + self.SendButtonPressed = Signal.new() + self.UserChangedText = Signal.new() + self.blockUserChangedText = true + self._analytics = appState.analytics + + local isTablet = appState.store:GetState().FormFactor == FormFactor.TABLET + + local lineCutoff + if isTablet then + lineCutoff = LINE_CUTOFF_TABLET + else + lineCutoff = LINE_CUTOFF_PHONE + end + + local function getTextButtonHeight(text, font, textSize, textBoxAbsoluteSizeX) + local textHeight = Text.GetTextHeight(text, font, textSize, + textBoxAbsoluteSizeX) + local maxTextHeight = Text.GetTextHeight("A\nB\nC\nD\nE", font, textSize, + textBoxAbsoluteSizeX) * lineCutoff + local textButtonHeight = 24 + math.min(textHeight, maxTextHeight) + return textHeight, maxTextHeight, textButtonHeight + end + + local textButtonInputTextFont = Enum.Font.SourceSans + local textButtonInputTextSize = Constants.Font.FONT_SIZE_18 + local _, _, textButtonHeight = getTextButtonHeight("", textButtonInputTextFont, textButtonInputTextSize, 0) + + local textBoxPosition + if isTablet then + textBoxPosition = UDim2.new(0, 0, 0, 0) + else + textBoxPosition = UDim2.new(0, 12, 0, 6) + end + + local textBoxInstance = Create.new "TextBox" { + Name = "InputText", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + Position = textBoxPosition, + Text = "", + Font = textButtonInputTextFont, + TextSize = textButtonInputTextSize, + TextColor3 = Constants.Text.INPUT, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextWrapped = true, + OverlayNativeInput = true, + ClearTextOnFocus = false, + ManualFocusRelease = true, + MultiLine = true, + PlaceholderText = appState.localization:Format("Feature.Chat.Label.ChatInputPlaceholder"), + PlaceholderColor3 = Constants.Text.INPUT_PLACEHOLDER, + } + + local inputBarInstance + if isTablet then + inputBarInstance = Create.new "ImageLabel" { + Name = "InputBarFrame", + Size = UDim2.new(1, -68, 1, -24), + AnchorPoint = Vector2.new(0, 0), + Position = UDim2.new(0, 12, 0, 12), + BackgroundTransparency = 1, + Image = "", + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(3, 3, 4, 4), + textBoxInstance, + } + else + inputBarInstance = Create.new "ImageLabel" { + Name = "InputBarFrame", + Size = UDim2.new(1, -62, 1, -10), + AnchorPoint = Vector2.new(0, 0.5), + Position = UDim2.new(0, 10, 0.5, 0), + BackgroundTransparency = 1, + Image = INPUT_FRAME_IMAGE, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(3, 3, 4, 4), + Create.new "Frame" { + Name = "InnerFrame", + Size = UDim2.new(1, -24, 1, -12), + BackgroundTransparency = 1, + ClipsDescendants = false, + + textBoxInstance + }, + } + end + + self.rbx = Create.new "TextButton" { + Name = "ChatInputBar", + Text = "", + AutoButtonColor = false, + BackgroundColor3 = Constants.Color.WHITE, + Size = UDim2.new(1, 0, 0, textButtonHeight), + BorderSizePixel = 0, + + Create.new "Frame" { + Name = "TopBorder", + Size = UDim2.new(1, 0, 0, 1), + BackgroundColor3 = Constants.Color.GRAY3, + BorderSizePixel = 0, + Position = UDim2.new(0, 0, 0, 0), + }, + + inputBarInstance, + + Create.new "ImageButton" { + Name = "GameButton", + BackgroundTransparency = 1, + Size = UDim2.new(0, RIGHT_BUTTON_WIDTH, 0, RIGHT_BUTTON_HEIGHT), + AnchorPoint = Vector2.new(0, 0), + Position = UDim2.new(1, -RIGHT_BUTTON_WIDTH, 1, -RIGHT_BUTTON_HEIGHT - (isTablet and GAME_ICON_BOTTOM_PADDING or 0)), + Visible = FFlagLuaChatShareGameToChatFromChat, + + Create.new "ImageLabel" { + Name = "GameIcon", + BackgroundTransparency = 1, + Size = UDim2.new(0, 24, 0, 24), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Image = GAME_ICON, + } + }, + + Create.new "ImageButton" { + Name = "SendButton", + BackgroundTransparency = 1, + Size = UDim2.new(0, 32, 0, 32), + AnchorPoint = Vector2.new(0, 1), + Position = UDim2.new(1, -42, 1, -SEND_BUTTON_BOTTOM_PADDING), + Image = SEND_BUTTON_IMAGE, + ImageColor3 = Constants.Color.GRAY3, + Visible = not FFlagLuaChatShareGameToChatFromChat, + + Create.new "ImageLabel" { + Name = "Icon", + BackgroundTransparency = 1, + Size = UDim2.new(0, 16, 0, 16), + Position = UDim2.new(0.5, 1, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Image = SEND_ICON, + } + }, + } + + + self.rbx.TouchTap:Connect(function() + --Sink this tap so the keyboard doesn't close + end) + + self.textBox = textBoxInstance + + local function updateTextBoxSize() + local textBoxText = self.textBox.Text + if textBoxText:len() == 0 then + textBoxText = self.textBox.PlaceholderText + end + + local textHeight, maxTextHeight, textButtonHeight = getTextButtonHeight(textBoxText, self.textBox.Font, + self.textBox.TextSize, self.textBox.AbsoluteSize.X) + self.rbx.Size = UDim2.new(1, 0, 0, textButtonHeight) + + if not self.textBox:IsFocused() and textHeight > maxTextHeight then + self.textBox.Size = UDim2.new(1, 0, 0, 24 + textHeight); + self.rbx.InputBarFrame.InnerFrame.ClipsDescendants = true + else + self.textBox.Size = UDim2.new(1, 0, 1, 0); + self.rbx.InputBarFrame.InnerFrame.ClipsDescendants = false + end + end + + self.textBox.Focused:Connect(function() + updateTextBoxSize() + self.textBox.Size = UDim2.new(1, 0, 1, 0); + self.blockUserChangedText = false + end) + + self.textBox.FocusLost:Connect(function() + updateTextBoxSize() + + if #self.textBox.Text <= 0 then + self:Reset() + end + end) + + self.textBox:GetPropertyChangedSignal("Text"):Connect(function() + local text = self.textBox.Text + + updateTextBoxSize() + + if FFlagLuaChatShareGameToChatFromChat then + self:RightButtonVisibility(string.len(text) == 0) + end + + if isMessageValid(text) then + self:SetSendButtonEnabled(true) + else + self:SetSendButtonEnabled(false) + end + + if not self.blockUserChangedText then + self.UserChangedText:Fire() + end + end) + + getInputEvent(self.rbx.SendButton):Connect(function() + self:SendMessage() + end) + + if FFlagLuaChatShareGameToChatFromChat then + getInputEvent(self.rbx.GameButton):Connect(function() + self:ReportBrowseGamesButtonTappedEvent() + if self.textBox:IsFocused() then + self.textBox:ReleaseFocus() + end + appState.store:dispatch(SetRoute(Intent.BrowseGames, {})) + end) + + self.rbx.GameButton.InputBegan:Connect(function() + self:SetGameButtonIcon(true) + end) + + self.rbx.GameButton.InputEnded:Connect(function() + self:SetGameButtonIcon(false) + end) + end + + setmetatable(self, ChatInputBar) + return self +end + +function ChatInputBar:RightButtonVisibility(isInputBoxEmpty) + self.rbx.SendButton.Visible = not isInputBoxEmpty + self.rbx.GameButton.Visible = isInputBoxEmpty +end + +function ChatInputBar:Reset() + self.blockUserChangedText = true + self.textBox.Text = "" + self.textBox:ResetKeyboardMode() + self.blockUserChangedText = false +end + +function ChatInputBar:SendMessage() + local text = self.textBox.Text + if not isMessageValid(text) then + return + end + + self:Reset() + self.SendButtonPressed:Fire(text) +end + +function ChatInputBar:SetSendButtonEnabled(value) + if self.sendButtonEnabled == value then + return + end + self.sendButtonEnabled = value + + local color = value and Constants.Color.BLUE_PRIMARY or Constants.Color.GRAY3 + self.rbx.SendButton.ImageColor3 = color +end + +function ChatInputBar:SetGameButtonIcon(isPressed) + if isPressed then + self.rbx.GameButton.GameIcon.Image = PRESSED_GAME_ICON + else + self.rbx.GameButton.GameIcon.Image = GAME_ICON + end +end + +function ChatInputBar:GetHeight() + return self.rbx.Size.Y.Offset +end + +function ChatInputBar:ReportBrowseGamesButtonTappedEvent() + local eventContext = "touch" + local eventName = "chooseGameToShare" + + local player = PlayerService.LocalPlayer + local userId = "UNKNOWN" + if player then + userId = tostring(player.UserId) + end + + local additionalArgs = { + uid = userId, + cid = self.appState.store:getState().ChatAppReducer.ActiveConversationId + } + + self._analytics.EventStream:setRBXEventStream(eventContext, eventName, additionalArgs) +end + +ChatInputBar.__index = ChatInputBar +return ChatInputBar \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatInputBarTablet.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatInputBarTablet.lua new file mode 100644 index 0000000..ad205cd --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatInputBarTablet.lua @@ -0,0 +1,270 @@ +local CoreGui = game:GetService("CoreGui") +local PlayerService = game:GetService("Players") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local DialogInfo = require(LuaChat.DialogInfo) +local getInputEvent = require(LuaChat.Utils.getInputEvent) +local Intent = DialogInfo.Intent +local SetRoute = require(LuaChat.Actions.SetRoute) +local Signal = require(Common.Signal) +local Text = require(LuaChat.Text) + +local FFlagLuaChatShareGameToChatFromChat = settings():GetFFlag("LuaChatShareGameToChatFromChat") + +local GAME_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-game.png" +local PRESSED_GAME_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-game-pressed-24x24.png" +local SEND_BUTTON_IMAGE = "rbxasset://textures/ui/LuaChat/graphic/send-white.png" +local SEND_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-send.png" + +local GAME_ICON_BOTTOM_PADDING = 8 +local MIN_TEXT_HEIGHT = 36 +local RIGHT_BUTTON_WIDTH = 52 +local RIGHT_BUTTON_HEIGHT = 48 +local SEND_BUTTON_BOTTOM_PADDING = 16 +local TEXT_FRAME_HEIGHT_DIFF = 28 + +local function isMessageValid(text) + if #text >= 160 then + return false + end + + -- Only whitespace + if text:match("^%s*$") then + return false + end + + return true +end + +local ChatInputBar = {} + +function ChatInputBar.new(appState) + local self = {} + self.appState = appState + self.sendButtonEnabled = false + self.SendButtonPressed = Signal.new() + self.UserChangedText = Signal.new() + self.blockUserChangedText = true + self._analytics = appState.analytics + + self.rbx = Create.new "TextButton" { + Name = "ChatInputBar", + Text = "", + AutoButtonColor = false, + BackgroundColor3 = Constants.Color.WHITE, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 64), + + Create.new "Frame" { + Name = "TopBorder", + Size = UDim2.new(1, 0, 0, 1), + BackgroundColor3 = Constants.Color.GRAY3, + BorderSizePixel = 0, + Position = UDim2.new(0, 0, 0, 0), + }, + + Create.new "Frame" { + Name = "InputBarFrame", + Size = UDim2.new(1, -68, 1, -12), + Position = UDim2.new(0, 12, 0, 6), + BackgroundTransparency = 1, + + Create.new "TextBox" { + Name = "InputText", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + Text = "", + Font = Enum.Font.SourceSans, + TextSize = Constants.Font.FONT_SIZE_18, + TextColor3 = Constants.Text.INPUT, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + TextWrapped = true, + OverlayNativeInput = true, + ClearTextOnFocus = false, + ManualFocusRelease = true, + MultiLine = true, + PlaceholderText = appState.localization:Format("Feature.Chat.Label.ChatInputPlaceholder"), + PlaceholderColor3 = Constants.Text.INPUT_PLACEHOLDER, + } + }, + + Create.new "ImageButton" { + Name = "GameButton", + BackgroundTransparency = 1, + Size = UDim2.new(0, RIGHT_BUTTON_WIDTH, 0, RIGHT_BUTTON_HEIGHT), + AnchorPoint = Vector2.new(0, 0), + Position = UDim2.new(1, -RIGHT_BUTTON_WIDTH, 1, -RIGHT_BUTTON_HEIGHT - GAME_ICON_BOTTOM_PADDING), + Visible = FFlagLuaChatShareGameToChatFromChat, + + Create.new "ImageLabel" { + Name = "GameIcon", + BackgroundTransparency = 1, + Size = UDim2.new(0, 24, 0, 24), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Image = GAME_ICON, + } + }, + + Create.new "ImageButton" { + Name = "SendButton", + BackgroundTransparency = 1, + Size = UDim2.new(0, 32, 0, 32), + AnchorPoint = Vector2.new(0, 1), + Position = UDim2.new(1, -12 - 32, 1, -SEND_BUTTON_BOTTOM_PADDING), + Image = SEND_BUTTON_IMAGE, + ImageColor3 = Constants.Color.GRAY3, + Visible = not FFlagLuaChatShareGameToChatFromChat, + + Create.new "ImageLabel" { + Name = "Icon", + BackgroundTransparency = 1, + Size = UDim2.new(0, 16, 0, 16), + Position = UDim2.new(0.5, 1, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Image = SEND_ICON, + } + }, + } + + self.rbx.TouchTap:Connect(function() + --Sink this tap so the keyboard doesn't close + end) + + self.textBox = self.rbx.InputBarFrame.InputText + + self.textBox.Focused:Connect(function() + self.blockUserChangedText = false + end) + + self.textBox.FocusLost:Connect(function() + if #self.textBox.Text <= 0 then + self:Reset() + end + end) + + self.textBox:GetPropertyChangedSignal("Text"):Connect(function() + local text = self.textBox.Text + if FFlagLuaChatShareGameToChatFromChat then + self:RightButtonVisibility(string.len(text) == 0) + end + local sizeY = Text.GetTextHeight(text, self.textBox.Font, self.textBox.TextSize, self.textBox.AbsoluteSize.X) + local maxTextHeight = Text.GetTextHeight("A\nB\nC\nD\nE", self.textBox.Font, self.textBox.TextSize, + self.textBox.AbsoluteSize.X) * 0.95 + + if sizeY < MIN_TEXT_HEIGHT then + sizeY = MIN_TEXT_HEIGHT + elseif sizeY > maxTextHeight then + sizeY = maxTextHeight + end + + self.rbx.Size = UDim2.new(1, 0, 0, sizeY + TEXT_FRAME_HEIGHT_DIFF) + + if isMessageValid(text) then + self:SetSendButtonEnabled(true) + else + self:SetSendButtonEnabled(false) + end + + if not self.blockUserChangedText then + self.UserChangedText:Fire() + end + end) + + getInputEvent(self.rbx.SendButton):Connect(function() + self:SendMessage() + end) + + if FFlagLuaChatShareGameToChatFromChat then + getInputEvent(self.rbx.GameButton):Connect(function() + self:ReportBrowseGamesButtonTappedEvent() + if self.textBox:IsFocused() then + self.textBox:ReleaseFocus() + end + appState.store:dispatch(SetRoute(Intent.BrowseGames, {})) + end) + + self.rbx.GameButton.InputBegan:Connect(function() + self:SetGameButtonIcon(true) + end) + + self.rbx.GameButton.InputEnded:Connect(function() + self:SetGameButtonIcon(false) + end) + end + + setmetatable(self, ChatInputBar) + return self +end + +function ChatInputBar:Reset() + self.blockUserChangedText = true + self.textBox.Text = "" + self.textBox:ResetKeyboardMode() + self.blockUserChangedText = false +end + +function ChatInputBar:RightButtonVisibility(isInputBoxEmpty) + self.rbx.SendButton.Visible = not isInputBoxEmpty + self.rbx.GameButton.Visible = isInputBoxEmpty +end + +function ChatInputBar:SendMessage() + local text = self.textBox.Text + if not isMessageValid(text) then + return + end + + self:Reset() + self.SendButtonPressed:Fire(text) +end + +function ChatInputBar:SetSendButtonEnabled(value) + if self.sendButtonEnabled == value then + return + end + self.sendButtonEnabled = value + + local color = value and Constants.Color.BLUE_PRIMARY or Constants.Color.GRAY3 + self.rbx.SendButton.ImageColor3 = color +end + +function ChatInputBar:SetGameButtonIcon(isPressed) + if isPressed then + self.rbx.GameButton.GameIcon.Image = PRESSED_GAME_ICON + else + self.rbx.GameButton.GameIcon.Image = GAME_ICON + end +end + +function ChatInputBar:GetHeight() + return self.rbx.Size.Y.Offset +end + +function ChatInputBar:ReportBrowseGamesButtonTappedEvent() + local eventContext = "touch" + local eventName = "chooseGameToShare" + + local player = PlayerService.LocalPlayer + local userId = "UNKNOWN" + if player then + userId = tostring(player.UserId) + end + + local additionalArgs = { + uid = userId, + cid = self.appState.store:getState().ChatAppReducer.ActiveConversationId + } + + self._analytics.EventStream:setRBXEventStream(eventContext, eventName, additionalArgs) +end + +ChatInputBar.__index = ChatInputBar +return ChatInputBar diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatLoadingIndicator.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatLoadingIndicator.lua new file mode 100644 index 0000000..c670d76 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatLoadingIndicator.lua @@ -0,0 +1,46 @@ +local LuaChat = script.Parent.Parent +local Create = require(LuaChat.Create) +local LoadingIndicator = require(script.Parent.LoadingIndicator) + +local ChatLoadingIndicator = {} + +function ChatLoadingIndicator.new(appState) + local self = {} + + local indicator = LoadingIndicator.new(appState, 2) + self.super = indicator + + self.rbx = Create "Frame" { + Name = "ChatLoadingIndicator", + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + + Create "Frame" { + Name = "Inner", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 200), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + + Create "UIListLayout" { + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }, + + indicator.rbx, + }, + } + + setmetatable(self, ChatLoadingIndicator) + + return self +end + +function ChatLoadingIndicator:SetVisible(visible) + self.rbx.Visible = visible + self.super:SetVisible(visible) +end + +ChatLoadingIndicator.__index = ChatLoadingIndicator + +return ChatLoadingIndicator diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatTimestamp.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatTimestamp.lua new file mode 100644 index 0000000..ea26706 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ChatTimestamp.lua @@ -0,0 +1,63 @@ +local LuaChat = script.Parent.Parent + +local Create = require(LuaChat.Create) +local Text = require(LuaChat.Text) +local Constants = require(LuaChat.Constants) + +local ChatTimestamp = {} + +ChatTimestamp.__index = ChatTimestamp + +function ChatTimestamp.new(appState, text) + local self = {} + + self.rbx = Create.new "Frame" { + Name = "ChatTimestamp", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 50), + + Create.new "UIPadding" { + PaddingTop = UDim.new(0, 20), + PaddingBottom = UDim.new(0, 10), + }, + + Create.new "ImageLabel" { + BackgroundTransparency = 0, + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(224, 224, 224), + Size = UDim2.new(0, 150, 0, 30), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Image = "rbxasset://textures/ui/LuaChat/9-slice/system-message.png", + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(3, 3, 4, 4), + + Create.new "UIPadding" { + PaddingTop = UDim.new(0, 4), + PaddingBottom = UDim.new(0, 4), + PaddingLeft = UDim.new(0, 6), + PaddingRight = UDim.new(0, 6), + }, + + Create.new "TextLabel" { + BackgroundTransparency = 1, + TextColor3 = Color3.fromRGB(128, 128, 128), + Text = text, + TextSize = Constants.Font.FONT_SIZE_14, + Font = Enum.Font.SourceSans, + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5) + }, + }, + } + + local textBounds = Text.GetTextBounds(text, Enum.Font.SourceSans, Constants.Font.FONT_SIZE_14, Vector2.new(1000, 1000)) + self.rbx.ImageLabel.Size = UDim2.new(0, textBounds.X + 12, 0, textBounds.Y + 8) + + setmetatable(self, ChatTimestamp) + + return self +end + +return ChatTimestamp \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/Conversation.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/Conversation.lua new file mode 100644 index 0000000..e34bd53 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/Conversation.lua @@ -0,0 +1,519 @@ +local CoreGui = game:GetService("CoreGui") +local TweenService = game:GetService("TweenService") +local UserInputService = game:GetService("UserInputService") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat +local LuaApp = Modules.LuaApp + +local Constants = require(LuaChat.Constants) +local Conversation = require(LuaChat.Models.Conversation) +local ConversationActions = require(LuaChat.Actions.ConversationActions) +local Create = require(LuaChat.Create) +local DialogInfo = require(LuaChat.DialogInfo) +local FlagSettings = require(LuaChat.FlagSettings) +local FormFactor = require(LuaApp.Enum.FormFactor) +local Signal = require(Common.Signal) +local WebApi = require(LuaChat.WebApi) + +local Components = LuaChat.Components + +local PlayTogetherGameIcon = require(Components.PlayTogetherGameIcon) +local ChatInputBar = require(Components.ChatInputBar) +local ChatInputBarTablet = require(Components.ChatInputBarTablet) +local HeaderLoader = require(Components.HeaderLoader) +local LoadingIndicator = require(Components.LoadingIndicator) +local MessageList = require(Components.MessageList) +local PaddedImageButton = require(Components.PaddedImageButton) +local UserTypingIndicator = require(Components.UserTypingIndicator) + +local getConversationDisplayTitle = require(LuaChat.Utils.getConversationDisplayTitle) + +local Intent = DialogInfo.Intent + +local LuaChatAssetCardsSelfTerminateConnection = settings():GetFFlag("LuaChatAssetCardsSelfTerminateConnection") +local FFlagLuaChatRefactoredChatInputBar = settings():GetFFlag("LuaChatRefactoredChatInputBar") +local LuaChatGroupChatIconEnabled = settings():GetFFlag("LuaChatGroupChatIconEnabled") +local FFlagShareGameToChatStatusAnalytics = settings():GetFFlag("ShareGameToChatStatusAnalytics") + +local ConversationView = {} + +ConversationView.__index = ConversationView + +local function getNewestWithNilPreviousMessageId(messages) + for id, message, _ in messages:CreateReverseIterator() do + if message.previousMessageId == nil then + return id + end + end + return messages.keys[1] +end + +local function sendPreprocess(inputText) + if inputText == "/shrug" then + return "¯\\_(ツ)_/¯" + end + + -- Future chat commands will go here + + return inputText +end + +function ConversationView.new(appState) + local self = {} + self.connections = {} + + self.conversationId = nil + self.appState = appState + self.lastTypingTimestamp = 0 + self.BackButtonPressed = Signal.new() + self.GroupDetailsButtonPressed = Signal.new() + self.wasTouchingBottom = false + self.oldConversation = nil + + self.luaChatPlayTogetherEnabled = FlagSettings.IsLuaChatPlayTogetherEnabled( + self.appState.store:getState().FormFactor) + + setmetatable(self, ConversationView) + + self.rbx = Create.new "TextButton" { + Name = "Conversation", + Text = "", + AutoButtonColor = false, + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 0, + BackgroundColor3 = Constants.Color.GRAY6, + BorderSizePixel = 0, + } + -- Component Setup + local header = HeaderLoader.GetHeader(appState, Intent.Conversation) + header:SetDefaultSubtitle() + if appState.store:getState().FormFactor == FormFactor.PHONE then + header:SetBackButtonEnabled(true) + else + header:SetBackButtonEnabled(false) + end + self.header = header + + header.rbx.Parent = self.rbx + header.rbx.LayoutOrder = 1 + header.rbx.ZIndex = 2 -- Render on top of the conversation (which is a peer) + + local groupDetailsButton + groupDetailsButton = PaddedImageButton.new(appState, "GroupDetails", + "rbxasset://textures/ui/LuaChat/icons/ic-info.png") + header:AddButton(groupDetailsButton) + groupDetailsButton.Pressed:Connect(function() + self.GroupDetailsButtonPressed:Fire() + end) + + -- Play Together feature gating: + if self.luaChatPlayTogetherEnabled then + local playTogetherGameIcon = PlayTogetherGameIcon.new(appState, nil, PlayTogetherGameIcon.Size.SMALL) + playTogetherGameIcon.Pressed:Connect(function() + self.header:ToggleGameDrawer() + self.chatInputBar.textBox:ReleaseFocus() + end) + self.playTogetherGameIcon = playTogetherGameIcon + + header:AddButton(playTogetherGameIcon) + end + + -- Conversation contents are now in this frame so the drawer can render + -- on top of it as necessary. Note the "HeaderSpacer" element which + -- copies the size from the real header above it. + local contents = Create.new "Frame" { + Name = "Contents", + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 0, + BorderSizePixel = 0, + Create.new "UIListLayout" { + SortOrder = "LayoutOrder", + }, + Create.new "Frame" { + Name = "HeaderSpacer", + Size = header.rbx.Size, + BackgroundTransparency = 0, + BorderSizePixel = 0, + }, + } + contents.Parent = self.rbx + + local chatInputBar + if FFlagLuaChatRefactoredChatInputBar then + chatInputBar = ChatInputBar.new(appState) + else + if appState.store:getState().FormFactor == FormFactor.PHONE then + chatInputBar = ChatInputBar.new(appState) + else + chatInputBar = ChatInputBarTablet.new(appState) + end + end + + --These now get initialized in Update, based on conversationId of CurrentRoute in store + self.messageList = nil + self.messageListConnection = nil + self.typingIndicator = nil + self.initialLoadingFrame = nil + + chatInputBar.rbx.Parent = contents + chatInputBar.rbx.Position = UDim2.new(0, 0, 1, -42) + chatInputBar.rbx.LayoutOrder = 4 + self.chatInputBar = chatInputBar + + --Close keyboard when tapping outside of both keyboard and input area + --Per spec at: https://confluence.roblox.com/display/SOCIAL/Misc+Notes + --This is a bit of a hack, but a tap that focuses self.chatInputBar.textBox + --Can also, it seems, be interpreted as a tap of self.rbx + --So if the self.chatInputBar.textBox was just focused, I won't release focus + --on tap. + local lastFocus = nil + self.chatInputBar.textBox.Focused:Connect(function() + lastFocus = tick() + self.header:InputFocus() + end) + self.rbx.TouchTap:Connect(function() + if (not lastFocus) or (tick() - lastFocus) > .3 then + self.chatInputBar.textBox:ReleaseFocus() + end + end) + + -- Component Event Setup + header.BackButtonPressed:Connect(function() + self.BackButtonPressed:Fire() + end) + + header.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + self:Rescale() + end) + + chatInputBar.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + self:Rescale() + end) + + chatInputBar.SendButtonPressed:Connect(function(text) + local messageSentLocalTime = tick() + text = sendPreprocess(text) + + if FFlagShareGameToChatStatusAnalytics then + appState.store:dispatch(ConversationActions.SendMessage(self.conversationId, text, messageSentLocalTime)) + else + appState.store:dispatch(ConversationActions.SendMessage(self.conversationId, text, nil, messageSentLocalTime)) + end + end) + + chatInputBar.UserChangedText:Connect(function() + if tick() - self.lastTypingTimestamp > Constants.Text.POST_TYPING_STATUS_INTERVAL then + self.lastTypingTimestamp = tick() + WebApi.PostTypingStatus(self.conversationId, true) + end + end) + + return self +end + +function ConversationView:Start() + self.header:Start() + self.header:SetConnectionState(self.appState.store:getState().ConnectionState) + + if self.messageList and self.messageList.isTouchingBottom then + self.appState.store:dispatch(ConversationActions.MarkConversationAsRead(self.conversationId)) + end + + -- initial sizing + self:Rescale() + + local propertyChangeSignal = UserInputService:GetPropertyChangedSignal("OnScreenKeyboardVisible") + local keyboardVisibleConnection = propertyChangeSignal:Connect(function() + self:TweenRescale() + end) + table.insert(self.connections, keyboardVisibleConnection) + + propertyChangeSignal = UserInputService:GetPropertyChangedSignal("OnScreenKeyboardPosition") + local keyboardSizeConnection = propertyChangeSignal:Connect(function() + self:TweenRescale() + end) + table.insert(self.connections, keyboardSizeConnection) + propertyChangeSignal = self.rbx:GetPropertyChangedSignal("AbsoluteSize") + local absoluteSizeConnection = propertyChangeSignal:Connect(function() + self:TweenRescale() + end) + table.insert(self.connections, absoluteSizeConnection) + + local statusBarTappedConnection = UserInputService.StatusBarTapped:Connect(function() + if self.appState.store:getState().ChatAppReducer.Location.current.intent ~= Intent.Conversation then + return + end + self.messageList.rbx:ScrollToTop() + end) + table.insert(self.connections, statusBarTappedConnection) + self:Update(self.appState.store:getState()) +end + +function ConversationView:Stop() + self.chatInputBar.textBox:ReleaseFocus() + + if not LuaChatAssetCardsSelfTerminateConnection then + if self.messageList then + self.messageList:DisconnectChatBubbles() + end + end + + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + + self.connections = {} +end + +function ConversationView:Pause() + self.chatInputBar.textBox:ReleaseFocus() +end + +function ConversationView:Resume() + if self.messageList.isTouchingBottom then + self.appState.store:dispatch(ConversationActions.MarkConversationAsRead(self.conversationId)) + end + +end + +function ConversationView:Update(state) + self.header:SetConnectionState(state.ConnectionState) + + local currentConversationId = state.ChatAppReducer.Location.current.parameters.conversationId + + local conversation = state.ChatAppReducer.Conversations[currentConversationId] + + if not conversation then + return + end + + -- The game icon might not exist: + if self.playTogetherGameIcon then + self.playTogetherGameIcon:Update(conversation) + end + + if currentConversationId and currentConversationId ~= self.conversationId then + + self.conversationId = currentConversationId + + self.isFetchingOlderMessages = conversation.fetchingOlderMessages + + self.header:SetTitle(getConversationDisplayTitle(conversation)) + + if self.messageList then + self.messageList:Destruct() + end + + local messageList = MessageList.new(self.appState, conversation) + messageList.rbx.LayoutOrder = 3 + messageList.rbx.Parent = self.rbx.Contents + messageList:ResizeCanvas() + self.messageList = messageList + + if self.messageListConnection ~= nil then + self.messageListConnection:Disconnect() + end + local propertyChangeSignal = self.messageList.rbx:GetPropertyChangedSignal("AbsoluteSize") + self.messageListConnection = propertyChangeSignal:Connect(function() + if self.messageList.isTouchingBottom or self.wasTouchingBottom then + self:TweenScrollToBottom() + self.wasTouchingBottom = false + end + end) + + local function onRequestOlderMessages() + local conversationModel = self.appState.store:getState().ChatAppReducer.Conversations[self.conversationId] + if conversationModel == nil then + return + end + local messages = conversationModel.messages + local exclusiveMessageStartId = getNewestWithNilPreviousMessageId(messages) + if conversationModel.fetchingOlderMessages or conversationModel.fetchedOldestMessage then + return + end + + self.messageList:StartLoadingMessageHistoryAnimation() + + self.appState.store:dispatch(ConversationActions.GetOlderMessages(self.conversationId, exclusiveMessageStartId)) + end + if self.requestOlderMessagesConnection then + self.requestOlderMessagesConnection:Disconnect() + end + self.requestOlderMessagesConnection = messageList.RequestOlderMessages:Connect(onRequestOlderMessages) + + --Make sure this gets called at least once + onRequestOlderMessages() + + if self.readAllMessagesConnection then + self.readAllMessagesConnection:Disconnect() + end + self.readAllMessagesConnection = messageList.ReadAllMessages:Connect(function() + self.appState.store:dispatch(ConversationActions.MarkConversationAsRead(self.conversationId)) + end) + + if conversation.conversationType == Conversation.Type.ONE_TO_ONE_CONVERSATION then + if self.typingIndicator then + self.typingIndicator:Destruct() + end + local typingIndicator = UserTypingIndicator.new(self.appState, conversation) + typingIndicator.rbx.LayoutOrder = 2 + typingIndicator.rbx.Parent = self.rbx.Contents + self.typingIndicator = typingIndicator + + typingIndicator.Resized:Connect(function() + self:Rescale() + end) + end + + if self.initialLoadingFrame then + self.initialLoadingFrame:Destroy() + end + local initialLoadingFrame = Create.new "Frame" { + Name = "InitialLoadingFrame", + Size = self.messageList.rbx.Size, + Position = self.messageList.rbx.Position, + BackgroundTransparency = 1, + LayoutOrder = 1, + Visible = false + } + initialLoadingFrame.Parent = self.rbx.Contents + self.initialLoadingFrame = initialLoadingFrame + + if self.messageList.isTouchingBottom then + self.appState.store:dispatch(ConversationActions.MarkConversationAsRead(self.conversationId)) + end + + if self.luaChatPlayTogetherEnabled then + self.header:CreateGameDrawer(self.appState.store, self.conversationId, nil, self.appState.analytics) + end + + self:Rescale() + elseif conversation == self.oldConversation then + return + end + self.oldConversation = conversation + + if not conversation.fetchingOlderMessages then + self.messageList:StopLoadingMessageHistoryAnimation() + end + + if conversation.initialLoadingStatus == Constants.ConversationLoadingState.LOADING then + self:StartInitialLoadingAnimation() + else + self:StopInitialLoadingAnimation() + end + + self.messageList:Update(conversation) + self.header:SetTitle(getConversationDisplayTitle(conversation)) + + if LuaChatGroupChatIconEnabled then + if conversation.conversationType == Conversation.Type.MULTI_USER_CONVERSATION then + self.header:SetGroupChatIconVisibility(true) + else + self.header:SetGroupChatIconVisibility(false) + end + end + + if self.typingIndicator then + self.typingIndicator:Update(conversation) + end +end + +function ConversationView:GetYOffset() + local keyboardSize = 0 + if UserInputService.OnScreenKeyboardVisible and self.chatInputBar.textBox:IsFocused() then + keyboardSize = self.rbx.AbsoluteSize.Y - UserInputService.OnScreenKeyboardPosition.Y + end + local offset = keyboardSize + for _, child in ipairs(self.rbx.Contents:GetChildren()) do + if child:IsA("GuiObject") and (self.messageList == nil or child ~= self.messageList.rbx) + and child ~= self.initialLoadingFrame then + offset = offset + child.AbsoluteSize.Y + end + end + + return offset +end + +function ConversationView:Rescale() + + if not self.messageList then + return + end + + local offset = self:GetYOffset() + + local newSize = UDim2.new(1, 0, 1, -offset) + + local wasTouchingBottom = self.messageList.isTouchingBottom + self.messageList.rbx.Size = newSize + if wasTouchingBottom then + self.messageList:ScrollToBottom() + end + + self.initialLoadingFrame.Size = newSize +end + +function ConversationView:TweenRescale() + if self.messageList == nil then + return + end + + local offset = self:GetYOffset() + local newSize = UDim2.new(1, 0, 1, -offset) + self.wasTouchingBottom = self.messageList.isTouchingBottom + self.initialLoadingFrame.Size = newSize + + local duration = UserInputService.OnScreenKeyboardAnimationDuration + local tweenInfo = TweenInfo.new(duration) + + local propertyGoals = { + Size = newSize, + } + local tween = TweenService:Create(self.messageList.rbx, tweenInfo, propertyGoals) + tween:Play() +end + +function ConversationView:TweenScrollToBottom() + local offset = self:GetYOffset() + local height = self.messageList.rbx.CanvasSize.Y.Offset - self.messageList.rbx.AbsoluteWindowSize.Y + offset + + local duration = UserInputService.OnScreenKeyboardAnimationDuration + local tweenInfo = TweenInfo.new(duration) + + local propertyGoals = + { + CanvasPosition = Vector2.new(0, height) + } + local tween = TweenService:Create(self.messageList.rbx, tweenInfo, propertyGoals) + tween:Play() +end + +function ConversationView:StartInitialLoadingAnimation() + if not self.loadingAnimationRunning then + self.loadingAnimationRunning = true + + self.messageList.rbx.Visible = false + self.initialLoadingFrame.Visible = true + + local loadingIndicator = LoadingIndicator.new(self.appState, 3) + loadingIndicator.rbx.AnchorPoint = Vector2.new(0.5, 0.5) + loadingIndicator.rbx.Position = UDim2.new(0.5, 0, 0.5, 0) + loadingIndicator.rbx.Parent = self.initialLoadingFrame + end +end + +function ConversationView:StopInitialLoadingAnimation() + if self.loadingAnimationRunning then + self.loadingAnimationRunning = false + + self.messageList.rbx.Visible = true + self.initialLoadingFrame.Visible = false + + self.initialLoadingFrame:ClearAllChildren() + end +end + +return ConversationView diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ConversationEntry.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ConversationEntry.lua new file mode 100644 index 0000000..4cb738c --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ConversationEntry.lua @@ -0,0 +1,286 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat +local LuaApp = Modules.LuaApp + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local FlagSettings = require(LuaChat.FlagSettings) +local FormFactor = require(LuaApp.Enum.FormFactor) +local getConversationDisplayTitle = require(LuaChat.Utils.getConversationDisplayTitle) +local getInputEvent = require(LuaChat.Utils.getInputEvent) +local OrderedMap = require(LuaChat.OrderedMap) +local Signal = require(Common.Signal) +local Text = require(LuaChat.Text) + +local Components = LuaChat.Components +local ConversationThumbnail = require(Components.ConversationThumbnail) +local PlayTogetherGameIcon = require(Components.PlayTogetherGameIcon) + +local UseCppTextTruncation = FlagSettings.UseCppTextTruncation() + +local UNREAD_COUNTER_ENABLED = false + +local ConversationEntry = {} + +ConversationEntry.__index = ConversationEntry + +function ConversationEntry.new(appState, conversation) + local self = { + appState = appState, + } + self.conversation = nil + self.Tapped = Signal.new() + self.luaChatPlayTogetherEnabled = FlagSettings.IsLuaChatPlayTogetherEnabled( + self.appState.store:getState().FormFactor) + + local activeGameIconRbx = nil + if self.luaChatPlayTogetherEnabled then + self.activeGameIcon = PlayTogetherGameIcon.new(appState, conversation, + PlayTogetherGameIcon.Size.LARGE, PlayTogetherGameIcon.Type.ACTIVE) + self.activeGameIcon.rbx.AnchorPoint = Vector2.new(1, 0) + self.activeGameIcon.rbx.Position = UDim2.new(1, -12, 0, 12) + activeGameIconRbx = self.activeGameIcon.rbx + end + + self.thumb = ConversationThumbnail.new(appState, conversation) + self.thumb.rbx.Size = UDim2.new(0, 48, 0, 48) + self.thumb.rbx.Position = UDim2.new(0, 12, 0, 12) + + self.content = Create.new "TextLabel" { + Name = "Content", + AnchorPoint = Vector2.new(0, 1), + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Bottom, + TextTruncate = UseCppTextTruncation and Enum.TextTruncate.AtEnd or nil, + BackgroundTransparency = 1, + Size = UDim2.new(1, -58, 0, Constants.Font.FONT_SIZE_16), + Position = UDim2.new(0, 0, 1, -14), + TextSize = Constants.Font.FONT_SIZE_16, + Font = Enum.Font.SourceSans, + TextColor3 = Constants.Color.GRAY2, + ClipsDescendants = true, + } + + self.lastMessageTime = Create.new "TextLabel" { + Name = "LastMessageTime", + AnchorPoint = Vector2.new(1,0), + TextXAlignment = Enum.TextXAlignment.Right, + TextYAlignment = Enum.TextYAlignment.Top, + BackgroundTransparency = 1, + Size = UDim2.new(0, 50, 0, Constants.Font.FONT_SIZE_14), + Position = UDim2.new(1, -12, 0, Constants.Font.FONT_SIZE_18_POS_OFFSET + 20), + TextSize = Constants.Font.FONT_SIZE_14, + Font = Enum.Font.SourceSans, + TextColor3 = Constants.Color.GRAY2, + } + + self.title = Create.new "TextLabel" { + Name = "Title", + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextTruncate = Enum.TextTruncate.AtEnd, + BackgroundTransparency = 1, + Size = UDim2.new(1, -90, 0, Constants.Font.FONT_SIZE_18), + Position = UDim2.new(0, 0, 0, Constants.Font.FONT_SIZE_18_POS_OFFSET + 20), + Font = Enum.Font.SourceSans, + TextSize = Constants.Font.FONT_SIZE_18, + TextColor3 = Constants.Color.GRAY1, + ClipsDescendants = true, + } + + self.unreadMessageIndicator = Create.new "ImageLabel" { + Name = "UnreadMessageCount", + AnchorPoint = Vector2.new(1,1), + BackgroundTransparency = 1, + BackgroundColor3 = Color3.fromRGB(226, 35, 26), + Size = UDim2.new(0, 24, 0, 16), + Position = UDim2.new(1, -12, 1, -14), + Image = "rbxasset://textures/ui/LuaChat/9-slice/new-message-indicator.png", + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 10, 10), + Visible = false, + + Create.new "TextLabel" { + Name = "Label", + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + TextColor3 = Constants.Color.WHITE, + TextSize = Constants.Font.FONT_SIZE_12, + Font = Enum.Font.SourceSans, + Text = "1", + Position = UDim2.new(0, 0, 0, -1), + } + } + + self.rbx = Create.new "TextButton" { + Name = "ConversationEntry", + Size = UDim2.new(1, 0, 0, 72), + BackgroundColor3 = Constants.Color.WHITE, + AutoButtonColor = false, + BorderSizePixel = 0, + Text = "", + Font = Enum.Font.SourceSans, + + self.thumb.rbx, + + Create.new "Frame" { + Name = "Body", + BackgroundTransparency = 1, + Size = UDim2.new(1, -72, 1, 0), + Position = UDim2.new(0, 72, 0, 0), + + Create.new "Frame" { + Name = "Inner", + BackgroundTransparency = 1, + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + + self.content, + self.lastMessageTime, + self.title, + self.unreadMessageIndicator, + activeGameIconRbx, + }, + + Create.new "Frame" { + Name = "BottomBorder", + BackgroundColor3 = Constants.Color.GRAY4, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 1), + Position = UDim2.new(0, 0, 1, -1), + }, + }, + + Create.new "Frame" { + Name = "ImageContainer", + BackgroundTransparency = 1, + Size = UDim2.new(0, 72, 0, 72), + Position = UDim2.new(0, 0, 0, 0), + }, + } + + getInputEvent(self.rbx):Connect(function() + self.Tapped:Fire() + end) + + if self.thumb.clicked then + self.thumb.clicked:Connect(function() + self.Tapped:Fire() + end) + end + + setmetatable(self, ConversationEntry) + + self:Update(conversation) + + self.title:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + self.title.Text = self.conversation.title + if not UseCppTextTruncation then + Text.TruncateTextLabel(self.title, "...") + end + end) + + if self.activeGameIcon then + self:AdjustForGameIcon(self.activeGameIcon.rbx.Visible) + self.activeGameIcon.rbx:GetPropertyChangedSignal("Visible"):Connect(function() + self:AdjustForGameIcon(self.activeGameIcon.rbx.Visible) + end) + end + + return self +end + +function ConversationEntry:AdjustForGameIcon(isIconVisible) + if isIconVisible then + self.lastMessageTime.Visible = false + self.content.Size = UDim2.new(1, -72, 0, 18) + else + self.lastMessageTime.Visible = true + self.content.Size = UDim2.new(1, -58, 0, 18) + end + if not UseCppTextTruncation then + Text.TruncateTextLabel(self.content, "...") + end +end + +function ConversationEntry:SetBackgroundColor(color3) + self.rbx.BackgroundColor3 = color3 + if self.thumb.rbx:FindFirstChild("Mask") then + self.thumb.rbx.Mask.ImageColor3 = color3 + elseif self.thumb.rbx:FindFirstChild("Overlay") then + self.thumb.rbx.Overlay.ImageColor3 = color3 + end +end + +function ConversationEntry:Update(conversation) + + local state = self.appState.store:getState() + if state.FormFactor == FormFactor.TABLET then + local currentLocationParameters = state.ChatAppReducer.Location.current.parameters + local currentConversationId = currentLocationParameters and currentLocationParameters.conversationId or nil + if currentConversationId == conversation.id then + self:SetBackgroundColor(Constants.Color.GRAY5) + else + self:SetBackgroundColor(Constants.Color.WHITE) + end + end + + if conversation == self.conversation then + return + end + + local oldConversationState = self.conversation + self.conversation = conversation + local lastMessageId = conversation.messages.keys[#conversation.messages.keys] + local lastMessage = conversation.messages.values[lastMessageId] + local lastMessageText = "" + local lastMessageTime = "" + + if lastMessage then + lastMessageText = Text.RightTrim(lastMessage.content) + lastMessageText = lastMessageText:gsub("%s", " ") + lastMessageTime = conversation.lastUpdated:GetShortRelativeTime() + elseif conversation.lastUpdated then + lastMessageTime = conversation.lastUpdated:GetShortRelativeTime() + end + + local textColor = conversation.hasUnreadMessages and Constants.Color.GRAY1 or Constants.Color.GRAY2 + + self.content.Font = conversation.hasUnreadMessages and Constants.Font.TITLE or Enum.Font.SourceSans + self.content.Text = lastMessageText + + self.content.TextColor3 = textColor + self.lastMessageTime.TextColor3 = textColor + + self.lastMessageTime.Text = lastMessageTime + + self.title.Text = getConversationDisplayTitle(conversation) + if not UseCppTextTruncation then + Text.TruncateTextLabel(self.title, "...") + end + + self.thumb:Update(conversation) + if self.activeGameIcon then + self.activeGameIcon:Update(conversation) + self:AdjustForGameIcon(self.activeGameIcon.rbx.Visible) + end + + if UNREAD_COUNTER_ENABLED then + self.unreadMessageIndicator.Visible = conversation.hasUnreadMessages + if conversation.hasUnreadMessages and + (not oldConversationState or oldConversationState.messages ~= conversation.messages) then + local numUnread = 0 + for _, message in OrderedMap.CreateIterator(conversation.messages) do + if not message.read then + numUnread = numUnread + 1 + end + end + self.unreadMessageIndicator.Label.Text = tostring(numUnread) + end + end +end + +return ConversationEntry diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ConversationHub.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ConversationHub.lua new file mode 100644 index 0000000..08584af --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ConversationHub.lua @@ -0,0 +1,357 @@ +local CoreGui = game:GetService("CoreGui") +local GuiService = game:GetService("GuiService") +local TweenService = game:GetService("TweenService") +local UserInputService = game:GetService("UserInputService") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local DialogInfo = require(LuaChat.DialogInfo) +local Signal = require(Common.Signal) +local NotificationType = require(LuaApp.Enum.NotificationType) + +local Components = LuaChat.Components +local ChatDisabledIndicator = require(Components.ChatDisabledIndicator) +local ChatLoadingIndicator = require(Components.ChatLoadingIndicator) +local ConversationList = require(Components.ConversationList) +local ConversationSearchBox = require(Components.ConversationSearchBox) +local ConversationEntry = require(Components.ConversationEntry) +local HeaderLoader = require(Components.HeaderLoader) +local NoFriendsIndicator = require(Components.NoFriendsIndicator) +local PaddedImageButton = require(Components.PaddedImageButton) + +local ConversationActions = require(LuaChat.Actions.ConversationActions) +local FetchChatEnabled = require(LuaChat.Actions.FetchChatEnabled) +local GetFriendCount = require(LuaChat.Actions.GetFriendCount) +local SetActiveConversationId = require(LuaChat.Actions.SetActiveConversationId) +local SetAppLoaded = require(LuaChat.Actions.SetAppLoaded) + +local appStageLoaded = require(LuaApp.Analytics.Events.appStageLoaded) + +local CREATE_CHAT_IMAGE = "rbxasset://textures/ui/LuaChat/icons/ic-createchat1-24x24.png" +local CREATE_GROUP_CHAT_IMAGE = "rbxasset://textures/ui/LuaChat/icons/ic-create-group.png" + +local Intent = DialogInfo.Intent + +local ConversationHub = {} + +ConversationHub.__index = ConversationHub + +local LuaChatNotificationButtonEnabled = settings():GetFFlag("LuaChatNotificationButtonEnabled") +local LuaChatCreateChatEnabled = settings():GetFFlag("LuaChatCreateChatEnabled") + +local function requestOlderConversations(appState) + -- Don't fetch older conversations if the oldest conversation has already been fetched. + if appState.store:getState().ChatAppReducer.ConversationsAsync.oldestConversationIsFetched then + return + end + + -- Don't fetch older conversations if the oldest conversation is fetched. + if appState.store:getState().ChatAppReducer.ConversationsAsync.pageConversationsIsFetching then + return + end + + -- Ask for new conversations + local convoCount = 0 + for _, _ in pairs(appState.store:getState().ChatAppReducer.Conversations) do + convoCount = convoCount + 1 + end + local pageSize = Constants.PageSize.GET_CONVERSATIONS + local currentPage = math.floor(convoCount / pageSize) + spawn(function() + appState.store:dispatch(ConversationActions.GetLocalUserConversationsAsync(currentPage + 1, pageSize)) + end) +end + +function ConversationHub.new(appState) + local self = {} + self.connections = {} + + setmetatable(self, ConversationHub) + + spawn(function() + appState.store:dispatch(FetchChatEnabled()) + appState.store:dispatch(ConversationActions.GetUnreadConversationCountAsync()) + appState.store:dispatch(GetFriendCount()) + appState.store:dispatch( + ConversationActions.GetLocalUserConversationsAsync(1, Constants.PageSize.GET_CONVERSATIONS) + ):andThen(function() + appState.store:dispatch(SetAppLoaded(true)) + end) + end) + + self.appState = appState + self._analytics = appState.analytics + + self.rbx = Create.new "Frame" { + Name = "ConversationHub", + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Constants.Color.WHITE, + BorderSizePixel = 0, + + Create.new "UIListLayout" { + SortOrder = "LayoutOrder", + } + } + + self.ConversationTapped = Signal.new() + self.CreateChatButtonPressed = Signal.new() + self.isSearchOpen = false + + local header = HeaderLoader.GetHeader(appState, Intent.ConversationHub) + header:SetTitle(appState.localization:Format("CommonUI.Features.Label.Chat")) + header:SetDefaultSubtitle() + + header.rbx.Parent = self.rbx + header.rbx.LayoutOrder = 0 + self.header = header + + local createChatButton + if LuaChatCreateChatEnabled then + createChatButton = PaddedImageButton.new(self.appState, "CreateChat", CREATE_CHAT_IMAGE) + else + createChatButton = PaddedImageButton.new(self.appState, "CreateGroup", CREATE_GROUP_CHAT_IMAGE) + end + + createChatButton:SetVisible(false) + createChatButton.Pressed:Connect(function() + self.CreateChatButtonPressed:Fire() + end) + + self.createChatButton = createChatButton + + local searchConversationsButton = PaddedImageButton.new(self.appState, + "SearchConversations", "rbxasset://textures/ui/LuaChat/icons/ic-search.png") + + if LuaChatCreateChatEnabled then + header:AddButton(searchConversationsButton) + header:AddButton(createChatButton) + else + header:AddButton(createChatButton) + header:AddButton(searchConversationsButton) + end + + if LuaChatNotificationButtonEnabled then + local notificationButton = PaddedImageButton.new(self.appState, "Notification", + "rbxasset://textures/Icon_Stream_Off.png") + notificationButton.Pressed:Connect(function() + GuiService:BroadcastNotification("", NotificationType.VIEW_NOTIFICATIONS) + end) + header:AddButton(notificationButton) + end + + local searchHeader = HeaderLoader.GetHeader(appState, Intent.ConversationHub) + searchHeader:SetTitle("") + searchHeader:SetSubtitle("") + searchHeader.rbx.LayoutOrder = 0 + + local conversationSearchBox = ConversationSearchBox.new(self.appState) + searchHeader:AddContent(conversationSearchBox) + + local noFriendsIndicator = NoFriendsIndicator.new(appState) + self.noFriendsIndicator = noFriendsIndicator + noFriendsIndicator.rbx.Size = UDim2.new(1, 0, 1, -header.rbx.Size.Y.Offset) + noFriendsIndicator.rbx.Parent = self.rbx + noFriendsIndicator.rbx.LayoutOrder = 1 + + local chatDisabledIndicator = ChatDisabledIndicator.new(appState) + self.chatDisabledIndicator = chatDisabledIndicator + chatDisabledIndicator.rbx.Size = UDim2.new(1, 0, 1, -header.rbx.Size.Y.Offset) + chatDisabledIndicator.rbx.Parent = self.rbx + chatDisabledIndicator.rbx.LayoutOrder = 1 + + local chatLoadingIndicator = ChatLoadingIndicator.new(appState) + self.chatLoadingIndicator = chatLoadingIndicator + chatLoadingIndicator.rbx.Size = UDim2.new(1, 0, 1, -header.rbx.Size.Y.Offset) + chatLoadingIndicator.rbx.Parent = self.rbx + chatLoadingIndicator.rbx.LayoutOrder = 1 + + chatDisabledIndicator.openPrivacySettings:Connect(function() + GuiService:BroadcastNotification("", NotificationType.PRIVACY_SETTINGS) + end) + + local list = ConversationList.new(appState, appState.store:getState().ChatAppReducer.Conversations, ConversationEntry) + self.list = list + list.rbx.Size = UDim2.new(1, 0, 1, -header.rbx.Size.Y.Offset) + list.rbx.Parent = self.rbx + list.rbx.LayoutOrder = 1 + + list.ConversationTapped:Connect(function(convoId) + conversationSearchBox:Cancel() + appState.store:dispatch(SetActiveConversationId(convoId)) + self.ConversationTapped:Fire(convoId) + end) + + list.RequestOlderConversations:Connect(function() + requestOlderConversations(appState) + end) + + searchConversationsButton.Pressed:Connect(function() + header.rbx.Parent = nil + searchHeader.rbx.Parent = self.rbx + self.rbx.BackgroundColor3 = Constants.Color.GRAY5 + list:SetFilterPredicate(conversationSearchBox.SearchFilterPredicate) + conversationSearchBox.rbx.SearchBoxContainer.SearchBoxBackground.Search:CaptureFocus() + self.isSearchOpen = true + end) + + conversationSearchBox.SearchChanged:Connect(function() + list:SetFilterPredicate(conversationSearchBox.SearchFilterPredicate) + self:getOlderConversationsForSearchIfNecessary() + end) + + conversationSearchBox.Closed:Connect(function() + searchHeader.rbx.Parent = nil + header.rbx.Parent = self.rbx + self.rbx.BackgroundColor3 = Constants.Color.WHITE + list:SetFilterPredicate(nil) + self.isSearchOpen = false + end) + + appState.store.Changed:Connect(function(state, oldState) + + self:Update(state, oldState) + + if state.ChatAppReducer.Conversations ~= oldState.ChatAppReducer.Conversations + or state.ChatAppReducer.Location.current ~= oldState.ChatAppReducer.Location.current then + list:Update(state, oldState) + end + end) + + local state = appState.store:getState() + self:Update(state, state) + + local appRoutes = state.Navigation.history + local currentRoute = appRoutes[#appRoutes] + local currentSection = currentRoute[1] + appStageLoaded(self._analytics.EventStream, currentSection.name, "chatRender") + + return self +end + +function ConversationHub:Start() + local inputServiceConnection = UserInputService:GetPropertyChangedSignal('OnScreenKeyboardVisible'):Connect(function() + self:TweenRescale() + end) + table.insert(self.connections, inputServiceConnection) + + local statusBarTappedConnection = UserInputService.StatusBarTapped:Connect(function() + if self.appState.store:getState().ChatAppReducer.Location.current.intent ~= Intent.ConversationHub then + return + end + self.list.rbx:ScrollToTop() + end) + table.insert(self.connections, statusBarTappedConnection) +end + +function ConversationHub:Stop() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + + self.connections = {} +end + +function ConversationHub:Update(state, oldState) + self.header:SetConnectionState(state.ConnectionState) + + local conversations = state.ChatAppReducer.Conversations + local appLoaded = state.ChatAppReducer.AppLoaded + + local haveConversations = next(conversations) ~= nil + + if state.ChatAppReducer.ChatEnabled then + self.chatDisabledIndicator.rbx.Visible = false + + if state.ChatAppReducer.ChatEnabled ~= oldState.ChatAppReducer.ChatEnabled then + spawn(function() + self.appState.store:dispatch( + ConversationActions.GetLocalUserConversationsAsync(1, Constants.PageSize.GET_CONVERSATIONS) + ) + end) + end + else + self.chatDisabledIndicator.rbx.Visible = true + self.list.rbx.Visible = false + self.noFriendsIndicator.rbx.Visible = false + self.chatLoadingIndicator:SetVisible(false) + + return + end + + if appLoaded then + self.chatLoadingIndicator:SetVisible(false) + else + self.chatLoadingIndicator:SetVisible(true) + self.list.rbx.Visible = false + self.noFriendsIndicator.rbx.Visible = false + + return + end + + if haveConversations then + self.list.rbx.Visible = true + self.noFriendsIndicator.rbx.Visible = false + else + self.list.rbx.Visible = false + self.noFriendsIndicator.rbx.Visible = true + end + + if state.FriendCount < Constants.MIN_PARTICIPANT_COUNT then + self.createChatButton:SetVisible(false) + else + self.createChatButton:SetVisible(true) + end + + if state.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching + ~= oldState.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching then + self.list:Update(state, oldState) + self:getOlderConversationsForSearchIfNecessary() + end +end + +function ConversationHub:getOlderConversationsForSearchIfNecessary(appState) + -- To Check: + -- 1) Search is open + -- 2) Not have loaded all oldest conversations + -- 3) Not currently getting conversations + -- 4) Has enough items to show + -- Note that we already try to load more conversations if we scroll down to the bottom of the list + local state = self.appState.store:getState() + if not self.isSearchOpen + or state.ChatAppReducer.ConversationsAsync.oldestConversationIsFetched + or state.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching then + return + end + + if self.list.rbx.CanvasSize.Y.Offset > self.list.rbx.AbsoluteSize.Y then + return + end + + requestOlderConversations(self.appState) +end + +function ConversationHub:TweenRescale() + local keyboardSize = 0 + if UserInputService.OnScreenKeyboardVisible then + keyboardSize = self.rbx.AbsoluteSize.Y - UserInputService.OnScreenKeyboardPosition.Y + end + local newSize = UDim2.new(1, 0, 1, -(self.header.rbx.Size.Y.Offset + keyboardSize)) + + local duration = UserInputService.OnScreenKeyboardAnimationDuration + local tweenInfo = TweenInfo.new(duration) + + local propertyGoals = + { + Size = newSize + } + local tween = TweenService:Create(self.list.rbx, tweenInfo, propertyGoals) + + tween:Play() +end + +return ConversationHub \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ConversationList.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ConversationList.lua new file mode 100644 index 0000000..95ba1d9 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ConversationList.lua @@ -0,0 +1,402 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local FlagSettings = require(LuaChat.FlagSettings) +local PlayTogetherActions = require(LuaChat.Actions.PlayTogetherActions) +local Signal = require(Common.Signal) + +local Components = LuaChat.Components +local LoadingIndicator = require(Components.LoadingIndicator) + +local getConversationDisplayTitle = require(LuaChat.Utils.getConversationDisplayTitle) + +local ConversationList = {} + +local function conversationSortPredicate(a, b) + --For conversations that faked based on friend relations, + --for now there is no meaningful lastUpdated value to give them, + --and this property is set to nil, so the sort predicate needs to + --be able to handle that. + if a.lastUpdated ~= nil and b.lastUpdated ~= nil then + return a.lastUpdated:GetUnixTimestamp() > b.lastUpdated:GetUnixTimestamp() + elseif a.lastUpdated ~= nil then + return true + elseif b.lastUpdated ~= nil then + return false + else + return getConversationDisplayTitle(a) < getConversationDisplayTitle(b) + end +end + +local function conversationEntrySortPredicate(a, b) + --For conversations that faked based on friend relations, + --for now there is no meaningful lastUpdated value to give them, + --and this property is set to nil, so the sort predicate needs to + --be able to handle that. + if a.conversation.lastUpdated ~= nil and b.conversation.lastUpdated ~= nil then + return a.conversation.lastUpdated:GetUnixTimestamp() > b.conversation.lastUpdated:GetUnixTimestamp() + elseif a.conversation.lastUpdated ~= nil then + return true + elseif b.conversation.lastUpdated ~= nil then + return false + else + return getConversationDisplayTitle(a.conversation) < getConversationDisplayTitle(b.conversation) + end +end + +function ConversationList.new(appState, conversations, entryCard) + local self = {} + self.appState = appState + + self.connections = {} + self.conversations = {} + self.conversationEntries = {} + self.conversationUsers = {} + self.isTouchingBottom = false + self.RequestOlderConversations = Signal.new() + self.noSearchResultsFound = false + self.sortWithConversationEntry = false + self.filterPredicate = nil + + self.ConversationTapped = Signal.new() + self.lastTappedConversationEntry = nil + self._oldState = nil + self.entryCard = entryCard + + self.luaChatPlayTogetherEnabled = FlagSettings.IsLuaChatPlayTogetherEnabled( + self.appState.store:getState().FormFactor) + + self.rbx = Create.new "ScrollingFrame" { + Name = "ConversationList", + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScrollBarThickness = 5, + ElasticBehavior = "Always", + ScrollingDirection = "Y", + BottomImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png", + MidImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png", + TopImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png", + + Create.new "UIListLayout" { + SortOrder = Enum.SortOrder.LayoutOrder + } + } + + self.convoLoadingIndicatorFrame = Create.new "Frame" { + Name = "LoadingIndicatorFrame", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 72), + Visible = false + } + self.convoLoadingIndicatorFrame.Parent = self.rbx + + local canvasSizeConnection = self.rbx:GetPropertyChangedSignal("CanvasPosition"):Connect(function() + local canvasSizeYOffset = self.rbx.CanvasSize.Y.Offset + if self.convoLoadingIndicatorFrame.Visible then + canvasSizeYOffset = canvasSizeYOffset - self.convoLoadingIndicatorFrame.Size.Y.Offset + end + + if not self.isTouchingBottom and + self.rbx.CanvasPosition.Y + self.rbx.AbsoluteSize.Y >= canvasSizeYOffset then + self.isTouchingBottom = true + self.RequestOlderConversations:Fire() + elseif self.rbx.CanvasPosition.Y + self.rbx.AbsoluteSize.Y < canvasSizeYOffset then + self.isTouchingBottom = false + end + end) + table.insert(self.connections, canvasSizeConnection) + + self.noResultsFrame = Create.new "Frame" { + Name = "NoResultsFrame", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 72), + Visible = false, + + Create.new "TextLabel" { + Name = "Content", + AnchorPoint = Vector2.new(0, 1), + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + Font = Enum.Font.SourceSans, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 18), + Position = UDim2.new(0, 0, 1, -18), + TextSize = Constants.Font.FONT_SIZE_18, + TextColor3 = Constants.Color.GRAY3, + Text = appState.localization:Format("Feature.GameLeaderboard.Label.NoResults"), + } + } + self.noResultsFrame.Parent = self.rbx + + local appStateConnection = appState.store.Changed:Connect(function(state, oldState) + if not oldState.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching + and state.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching then + self:StartFetchingConversationsAnimation() + elseif oldState.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching + and not state.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching then + self:StopFetchingConversationsAnimation() + end + self:CheckToShowNoSearchResults() + end) + table.insert(self.connections, appStateConnection) + + setmetatable(self, ConversationList) + + if conversations then + self:Update(self.appState.store:getState(), nil) + end + + return self +end + +function ConversationList:Update(current, previous) + local users = current.Users + local conversations = current.ChatAppReducer.Conversations + + for _, conversation in pairs(conversations) do + local existing = self.conversations[conversation.id] + local existingEntry = self.conversationEntries[conversation.id] + local userCache = self.conversationUsers[conversation.id] + + local doUpdate = false + if previous == nil then + doUpdate = true + elseif conversation ~= existing + or current.ChatAppReducer.Location.current ~= previous.ChatAppReducer.Location.current then + doUpdate = true + else + if userCache then + for _, id in ipairs(conversation.participants) do + if userCache[id] ~= users[id] then + doUpdate = true + break + end + end + else + doUpdate = true + end + end + + if not userCache then + userCache = {} + self.conversationUsers[conversation.id] = userCache + end + + if doUpdate then + if existingEntry then + existingEntry:Update(conversation) + else + local entry = self.entryCard.new(self.appState, conversation) + entry.rbx.Parent = self.rbx + local tappedConnection = entry.Tapped:Connect(function() + self.ConversationTapped:Fire(conversation.id) + end) + table.insert(self.connections, tappedConnection) + + self.conversationEntries[conversation.id] = entry + end + + self.conversations[conversation.id] = conversation + + -- TODO: May not correctly handle users leaving? + for _, id in ipairs(conversation.participants) do + userCache[id] = users[id] + end + end + end + + local toDelete = {} + for _, conversation in pairs(self.conversations) do + local hasBeenRemoved = conversations[conversation.id] == nil + if hasBeenRemoved then + table.insert(toDelete, conversation.id) + end + end + for _, id in ipairs(toDelete) do + local entry = self.conversationEntries[id] + entry.rbx:Destroy() + self.conversationEntries[id] = nil + self.conversations[id] = nil + end + + self:Filter() + self:Sort() + self:ResizeCanvas() + if self.luaChatPlayTogetherEnabled then + self:FetchMostRecentlyPlayedGames() + end + + self._oldState = current +end + +function ConversationList:FetchMostRecentlyPlayedGames() + local state = self.appState.store:getState() + if state.ChatAppReducer.PlayTogetherAsync.fetchedMostRecentlyPlayedGames then + self:GetMostRecentlyPlayedPlayableGame() + return + end + + if self.luaChatPlayTogetherEnabled then + self.appState.store:dispatch(PlayTogetherActions.GetMostRecentlyPlayedGames()) + end +end + +function ConversationList:GetMostRecentlyPlayedPlayableGame() + local state = self.appState.store:getState() + if state.ChatAppReducer.MostRecentlyPlayedGames.setPlayableGame then + return + end + + self.appState.store:dispatch(PlayTogetherActions.GetMostRecentlyPlayedPlayableGame()) +end + +function ConversationList:SetFilterPredicate(filterPredicate) + self.filterPredicate = filterPredicate + + local state = self.appState.store:getState() + self:Update(state, self._oldState or state) +end + +function ConversationList:Filter() + local conversationCount = 0 + local visibleCount = 0 + + for _, conversationEntry in pairs(self.conversationEntries) do + local visible + local conversation = conversationEntry.conversation + local filterPredicate = self.filterPredicate + if filterPredicate and conversation then + visible = filterPredicate(getConversationDisplayTitle(conversation)) + else + visible = true + end + + conversationEntry.rbx.Visible = visible + conversationCount = conversationCount + 1 + if visible then + visibleCount = visibleCount + 1 + end + end + + self.noSearchResultsFound = ((conversationCount > 0) and (visibleCount == 0)) + self:CheckToShowNoSearchResults() +end + +function ConversationList:SetSortWithConversationEntry(value) + self.sortWithConversationEntry = value +end + +function ConversationList:Sort() + if self.sortWithConversationEntry then + self:SortWithConversationEntry() + else + self:SortWithConversation() + end +end + +function ConversationList:SortWithConversation() + local sorted = {} + for _, conversation in pairs(self.conversations) do + table.insert(sorted, conversation) + end + + table.sort(sorted, conversationSortPredicate) + + for key, conversation in ipairs(sorted) do + local entry = self.conversationEntries[conversation.id] + entry.rbx.LayoutOrder = key + end + + self.convoLoadingIndicatorFrame.LayoutOrder = #sorted + 1 +end + +function ConversationList:SortWithConversationEntry() + local sorted = {} + for _, entry in pairs(self.conversationEntries) do + table.insert(sorted, entry) + end + + table.sort(sorted, conversationEntrySortPredicate) + + for key, conversationEntry in ipairs(sorted) do + local entry = self.conversationEntries[conversationEntry.conversation.id] + entry.rbx.LayoutOrder = key + end + + self.convoLoadingIndicatorFrame.LayoutOrder = #sorted + 1 +end + +function ConversationList:ResizeCanvas() + local height = 0 + for _, entry in pairs(self.conversationEntries) do + if entry.rbx.Visible then + height = height + entry.rbx.AbsoluteSize.Y + end + end + + if self.convoLoadingIndicatorFrame.Visible then + height = height + self.convoLoadingIndicatorFrame.AbsoluteSize.Y + end + + self.rbx.CanvasSize = UDim2.new(1, 0, 0, height) +end + +function ConversationList:StartFetchingConversationsAnimation() + if not self.currentFetchConvoIndicator then + local loadingIndicator = LoadingIndicator.new(self.appState, 1) + loadingIndicator.rbx.AnchorPoint = Vector2.new(0.5, 0.5) + loadingIndicator.rbx.Position = UDim2.new(0.5, 0, 0.5, 0) + loadingIndicator.rbx.Parent = self.convoLoadingIndicatorFrame + + self.currentFetchConvoIndicator = loadingIndicator + + self.convoLoadingIndicatorFrame.Visible = true + + self:ResizeCanvas() + self:Sort() + end +end + +function ConversationList:StopFetchingConversationsAnimation() + if self.currentFetchConvoIndicator then + self.currentFetchConvoIndicator:Destroy() + self.currentFetchConvoIndicator = nil + self.convoLoadingIndicatorFrame.Visible = false + + self:ResizeCanvas() + end +end + +function ConversationList:CheckToShowNoSearchResults() + local visible = false + + if not self.convoLoadingIndicatorFrame.Visible then + visible = self.noSearchResultsFound + end + + self.noResultsFrame.Visible = visible +end + +function ConversationList:Destruct() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + + for _, entry in pairs(self.conversationEntries) do + entry:Destruct() + end + self.conversations = {} + self.conversationEntries = {} + self.conversationUsers = {} + self.rbx:Destroy() +end + +ConversationList.__index = ConversationList + +return ConversationList \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ConversationSearchBox.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ConversationSearchBox.lua new file mode 100644 index 0000000..f83e4e0 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ConversationSearchBox.lua @@ -0,0 +1,168 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local Signal = require(Common.Signal) + +local Text = require(LuaChat.Text) +local getInputEvent = require(LuaChat.Utils.getInputEvent) + +local FFlagTextBoxOverrideManualFocusRelease = settings():GetFFlag("TextBoxOverrideManualFocusRelease") + +local CANCEL_BUTTON_PADDING = 16 + +local ConversationSearchBox = {} + +function ConversationSearchBox.new(appState) + local self = {} + setmetatable(self, {__index = ConversationSearchBox}) + + self.rbx = Create.new"Frame" + { + Name = "ConversationSearchBox", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + Create.new "UIListLayout" + { + SortOrder = "LayoutOrder", + FillDirection = "Horizontal", + HorizontalAlignment = "Right", + }, + } + + local cancelButton = Create.new"TextButton" + { + Name = "Cancel", + BackgroundTransparency = 1, + Position = UDim2.new(0, 0, 0, 0), + TextSize = Constants.Font.FONT_SIZE_18, + TextColor3 = Constants.Color.WHITE, + Font = Enum.Font.SourceSans, + Text = appState.localization:Format("Feature.Chat.Action.Cancel"), + TextXAlignment = Enum.TextXAlignment.Center, + LayoutOrder = 1, + } + cancelButton.Size = UDim2.new(0, + Text.GetTextWidth(cancelButton.Text, cancelButton.Font, cancelButton.TextSize) + CANCEL_BUTTON_PADDING * 2, 1, 0) + cancelButton.Parent = self.rbx + + local searchBoxContainer = Create.new"Frame" + { + Name = "SearchBoxContainer", + BackgroundTransparency = 1, + Size = UDim2.new(1, -cancelButton.Size.X.Offset, 1, 0), + Position = UDim2.new(0, 8, 0, 0), + LayoutOrder = 0, + } + searchBoxContainer.Parent = self.rbx + + local searchBoxBackground = Create.new"ImageLabel" + { + Name = "SearchBoxBackground", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, -12), + Position = UDim2.new(0, 8, 0.5, 0), + AnchorPoint = Vector2.new(0, 0.5), + Image = "rbxasset://textures/ui/LuaChat/9-slice/search.png", + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(3,3,4,4), + Create.new"ImageLabel" + { + Name = "SearchIcon", + BackgroundTransparency = 1, + Size = UDim2.new(0, 16, 0, 16), + Position = UDim2.new(0, 8, 0.5, 0), + AnchorPoint = Vector2.new(0, 0.5), + ImageColor3 = Constants.Color.GRAY3, + Image = "rbxasset://textures/ui/LuaChat/icons/ic-search.png", + }, + } + searchBoxBackground.Parent = searchBoxContainer + + local clearSearchButton = Create.new"ImageButton" + { + Name = "ClearButton", + BackgroundTransparency = 1, + Size = UDim2.new(0, 16, 0, 16), + Position = UDim2.new(1, -8, 0.5, 0), + AnchorPoint = Vector2.new(1, 0.5), + Image = "rbxasset://textures/ui/LuaChat/icons/ic-clear-solid.png", + Visible = false, + } + clearSearchButton.Parent = searchBoxBackground + + local search = Create.new"TextBox" + { + Name = "Search", + BackgroundTransparency = 1, + ClipsDescendants = true, + Size = UDim2.new(1, -36 - 8 - 32, 1, 0), + Position = UDim2.new(0, 36, 0, 0), + TextSize = Constants.Font.FONT_SIZE_14, + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSans, + Text = "", + PlaceholderText = appState.localization:Format("Feature.Chat.Label.SearchWord"), + PlaceholderColor3 = Constants.Color.GRAY3, + TextXAlignment = Enum.TextXAlignment.Left, + OverlayNativeInput = true, + ClearTextOnFocus = false, + } + search.Parent = searchBoxBackground + if FFlagTextBoxOverrideManualFocusRelease then + search.ManualFocusRelease = true + end + self.search = search + + self.SearchChanged = Signal.new() + self.Closed = Signal.new() + + local searchText = "" + + search:GetPropertyChangedSignal("Text"):Connect(function() + searchText = string.lower(search.Text) + clearSearchButton.Visible = searchText ~= "" + self.SearchChanged:Fire(search.Text) + end) + + search.FocusLost:Connect(function() + if search.Text:len() == 0 then + self:Cancel() + end + end) + + getInputEvent(cancelButton):Connect(function() + self:Cancel() + end) + + getInputEvent(clearSearchButton):Connect(function() + search.Text = "" + self:Cancel() + end) + + self.SearchFilterPredicate = function(other) + if searchText == "" then + return true + end + return string.find(string.lower(other), searchText, 1, true) ~= nil + end + + return self +end + +function ConversationSearchBox:Cancel() + self.search:ReleaseFocus() + self.search.Text = "" + self.Closed:Fire() +end + +function ConversationSearchBox:Update(participants) + +end + +return ConversationSearchBox diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ConversationThumbnail.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ConversationThumbnail.lua new file mode 100644 index 0000000..3117e21 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ConversationThumbnail.lua @@ -0,0 +1,222 @@ +local CoreGui = game:GetService("CoreGui") +local Players = game:GetService("Players") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat +local LuaApp = Modules.LuaApp + +local Constants = require(LuaChat.Constants) +local Conversation = require(LuaChat.Models.Conversation) +local Create = require(LuaChat.Create) +local FormFactor = require(LuaApp.Enum.FormFactor) +local Functional = require(Common.Functional) +local HeadshotLoader = require(LuaChat.HeadshotLoader) +local UserThumbnail = require(LuaChat.Components.UserThumbnail) + +local MAX_USERS_IN_THUMBNAIL = 4 + +local ConversationThumbnail = {} + +ConversationThumbnail.__index = ConversationThumbnail + +-- This lets us determine how to build the group thumbnail. Index represents how many people are in the thumbnail! +local IMAGE_LAYOUT = { + [1] = { + { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + FrameSize = UDim2.new(1, 0, 1, 0), + FramePosition = UDim2.new(0, 0, 0, 0), + }, + }, + [2] = { + { + Size = UDim2.new(2, 0, 1, 0), + Position = UDim2.new(-0.5, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 1, 0), + FramePosition = UDim2.new(0, 0, 0, 0), + Border = { + BorderPosition = UDim2.new(0.5, -1, 0, 0), + BorderSize = UDim2.new(0, 2, 1, 0), + }, + }, + { + Size = UDim2.new(2, 0, 1, 0), + Position = UDim2.new(-0.5, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 1, 0), + FramePosition = UDim2.new(0.5, 1, 0, 0), + }, + }, + [3] = { + { + Size = UDim2.new(2, 0, 1, 0), + Position = UDim2.new(-0.5, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 1, 0), + FramePosition = UDim2.new(0, 0, 0, 0), + Border = { + BorderPosition = UDim2.new(0.5, -1, 0, 0), + BorderSize = UDim2.new(0, 2, 1, 0), + }, + }, + { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 0.5, -1), + FramePosition = UDim2.new(0.5, 1, 0, 0), + Border = { + BorderPosition = UDim2.new(0.5, 0, 0.5, -1), + BorderSize = UDim2.new(0.5, 0, 0, 2), + }, + }, + { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 0.5, -1), + FramePosition = UDim2.new(0.5, 1, 0.5, 1), + }, + }, + [4] = { + { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 0.5, -1), + FramePosition = UDim2.new(0, 0, 0, 0), + Border = { + BorderPosition = UDim2.new(0, 0, 0.5, -1), + BorderSize = UDim2.new(1, 0, 0, 2), + }, + }, + { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 0.5, -1), + FramePosition = UDim2.new(0.5, 1, 0, 0), + }, + { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 0.5, -1), + FramePosition = UDim2.new(0, 0, 0.5, 1), + Border = { + BorderPosition = UDim2.new(0.5, -1, 0, 0), + BorderSize = UDim2.new(0, 2, 1, 0), + }, + }, + { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 0.5, -1), + FramePosition = UDim2.new(0.5, 1, 0.5, 1), + }, + }, +} + + +function ConversationThumbnail.new(appState, conversation) + local self = { + appState = appState, + } + + local localId = tostring(Players.LocalPlayer.UserId) + local otherIds = Functional.Filter(conversation.participants, function(participantId) + return participantId ~= localId + end) + + if conversation.conversationType == Conversation.Type.ONE_TO_ONE_CONVERSATION then + return UserThumbnail.new(appState, otherIds[1]) + end + + self.rbx = Create.new "Frame" { + Name = "ConversationThumbnail", + BackgroundTransparency = 0, + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.WHITE, + } + + setmetatable(self, ConversationThumbnail) + + self:Update(conversation) + + return self +end + +function ConversationThumbnail:Update(conversation) + self.rbx:ClearAllChildren() + + local localId = tostring(Players.LocalPlayer.UserId) + local otherIds = Functional.Filter(conversation.participants, function(participantId) + return participantId ~= localId + end) + + local showUserIds + + if conversation.conversationType == Conversation.Type.MULTI_USER_CONVERSATION then + local hasRoomForLocalUser = #conversation.participants <= MAX_USERS_IN_THUMBNAIL + + if hasRoomForLocalUser then + showUserIds = conversation.participants + else + showUserIds = Functional.Take(otherIds, MAX_USERS_IN_THUMBNAIL) + end + else + showUserIds = otherIds + end + + for userIndex, userId in ipairs(showUserIds) do + local layoutData = IMAGE_LAYOUT[#showUserIds][userIndex] + local frame = Create.new "Frame" { + Name = "AvatarHolder", + BackgroundTransparency = 1, + ClipsDescendants = true, + Size = layoutData.FrameSize, + Position = layoutData.FramePosition, + } + local headshot = Create.new "ImageLabel" { + Name = "Avatar", + Size = layoutData.Size, + Position = layoutData.Position, + BackgroundTransparency = 1, + } + headshot.Parent = frame + + HeadshotLoader:Load(headshot, userId) + + if layoutData.Border then + Create.new "Frame" { + Name = "Border", + Size = layoutData.Border.BorderSize, + Position = layoutData.Border.BorderPosition, + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.GRAY4 + }.Parent = self.rbx + end + + frame.Parent = self.rbx + end + + Create.new "ImageLabel" { + Name = "Mask", + Image = "rbxasset://textures/ui/LuaChat/graphic/gr-profile-border-48x48.png", + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }.Parent = self.rbx + + local state = self.appState.store:getState() + if state.FormFactor == FormFactor.TABLET then + local currentLocationParameters = state.ChatAppReducer.Location.current.parameters + local currentConversationId = currentLocationParameters and currentLocationParameters.conversationId or nil + if currentConversationId == conversation.id then + self.rbx.Mask.ImageColor3 = Constants.Color.GRAY5 + else + self.rbx.Mask.ImageColor3 = Constants.Color.WHITE + end + end +end + +function ConversationThumbnail:Destruct() + self.rbx:Destroy() +end + +return ConversationThumbnail \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/CreateChat.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/CreateChat.lua new file mode 100644 index 0000000..5296f13 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/CreateChat.lua @@ -0,0 +1,177 @@ +local CoreGui = game:GetService("CoreGui") +local Players = game:GetService("Players") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local ConversationActions = require(LuaChat.Actions.ConversationActions) +local ConversationModel = require(LuaChat.Models.Conversation) +local Create = require(LuaChat.Create) +local DialogInfo = require(LuaChat.DialogInfo) +local Immutable = require(Common.Immutable) +local Signal = require(Common.Signal) + + +local Components = LuaChat.Components +local FriendSearchBoxComponent = require(Components.FriendSearchBox) +local HeaderLoader = require(Components.HeaderLoader) +local ResponseIndicator = require(Components.ResponseIndicator) +local SectionComponent = require(Components.ListSection) + +local RemoveRoute = require(LuaChat.Actions.RemoveRoute) + +local Intent = DialogInfo.Intent + +local CreateChat = {} +CreateChat.__index = CreateChat + +function CreateChat.new(appState) + local self = { + appState = appState, + } + setmetatable(self, CreateChat) + self.connections = {} + + self.conversation = ConversationModel.empty() + + self.responseIndicator = ResponseIndicator.new(appState) + self.responseIndicator:SetVisible(false) + + -- Header: + self.header = HeaderLoader.GetHeader(appState, Intent.CreateChat) + self.header:SetDefaultSubtitle() + self.header:SetTitle(appState.localization:Format("Feature.Chat.Heading.ChatWithFriends")) + self.header:SetBackButtonEnabled(true) + self.header:SetConnectionState(Enum.ConnectionState.Disconnected) + + -- Search for friends: + self.searchComponent = FriendSearchBoxComponent.new( + appState, + self.conversation.participants, + Constants.MAX_PARTICIPANT_COUNT, + function(user) + return user.isFriend and user.id ~= tostring(Players.LocalPlayer.UserId) + end + ) + self.searchComponent.rbx.LayoutOrder = 3 + local addParticipantConnection = self.searchComponent.addParticipant:Connect(function(id) + self.searchComponent.search:ReleaseFocus() + self:ChangeParticipants(Immutable.Set(self.conversation.participants, #self.conversation.participants+1, id)) + end) + table.insert(self.connections, addParticipantConnection) + + local removeParticipantConnection = self.searchComponent.removeParticipant:Connect(function(id) + self.searchComponent.search:ReleaseFocus() + self:ChangeParticipants(Immutable.RemoveValueFromList(self.conversation.participants, id)) + end) + table.insert(self.connections, removeParticipantConnection) + + -- Assemble the dialog from components we just made: + self.sectionComponent = SectionComponent.new(appState, nil, 2) + self.rbx = Create.new"Frame" { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Create.new("UIListLayout") { + Name = "ListLayout", + SortOrder = Enum.SortOrder.LayoutOrder, + }, + self.header.rbx, + Create.new"Frame" { + Name = "Content", + Size = UDim2.new(1, 0, 1, -(self.header.heightOfHeader)), + BackgroundColor3 = Constants.Color.GRAY5, + BorderSizePixel = 0, + LayoutOrder = 1, + ClipsDescendants = true, + + Create.new"UIListLayout" { + Name = "ListLayout", + SortOrder = Enum.SortOrder.LayoutOrder, + }, + self.sectionComponent.rbx, + self.searchComponent.rbx, + }, + self.responseIndicator.rbx, + } + + -- Wire up the save button to actually create our new chat group: + self.createChat = self.header:CreateHeaderButton("CreateChat", "Feature.Chat.Action.StartChatWithFriends") + self.header.innerButtons.Position = UDim2.new(1, 0, 0, 0) + self.createChat.rbx.Size = UDim2.new(0, 60, 1, 0) + self.createChat:SetEnabled(false) + local createChatConnection = self.createChat.Pressed:Connect(function() + self.searchComponent.search:ReleaseFocus() + if #self.conversation.participants >= Constants.MIN_PARTICIPANT_COUNT then + self.responseIndicator:SetVisible(true) + self.appState.store:dispatch( + ConversationActions.CreateConversation(self.conversation, function(id) + self.responseIndicator:SetVisible(false) + if id ~= nil then + self.ConversationSaved:Fire(id) + end + self.appState.store:dispatch(RemoveRoute(Intent.CreateChat)) + end) + ) + end + end) + table.insert(self.connections, createChatConnection) + + self.BackButtonPressed = Signal.new() + self.header.BackButtonPressed:Connect(function() + self.searchComponent.search:ReleaseFocus() + self.BackButtonPressed:Fire() + end) + self.ConversationSaved = Signal.new() + + spawn(function() + self.appState.store:dispatch(ConversationActions.GetAllFriendsAsync()) + end) + + self.tooManyFriendsAlertId = nil + + local headerSizeConnection = self.header.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + self:Resize() + end) + table.insert(self.connections, headerSizeConnection) + + return self +end + +function CreateChat:Resize() + -- Content frame must resize if the header changes size (which happens when it shows the "Connecting" message): + local sizeContent = UDim2.new(1, 0, 1, -self.header.rbx.AbsoluteSize.Y) + self.rbx.Content.Size = sizeContent + + -- Friends Search frame must resize to fit properly with their peers: + local sizeSearch = UDim2.new(1, 0, 1, -self.sectionComponent.rbx.AbsoluteSize.Y) + self.searchComponent.rbx.Size = sizeSearch +end + +function CreateChat:ChangeParticipants(participants) + self.conversation = Immutable.Set(self.conversation, "participants", participants) + self.searchComponent:Update(participants) + self.createChat:SetEnabled(#participants >= Constants.MIN_PARTICIPANT_COUNT) +end + +function CreateChat:Update(current) + self.header:SetConnectionState(current.ConnectionState) + self.searchComponent:Update(self.conversation.participants) +end + +function CreateChat:Destruct() + for _, connection in pairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + + self.header:Destroy() + self.responseIndicator:Destruct() + self.searchComponent:Destruct() + self.rbx:Destroy() +end + +return CreateChat \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/DefaultScreen.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/DefaultScreen.lua new file mode 100644 index 0000000..6a066d4 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/DefaultScreen.lua @@ -0,0 +1,56 @@ +local LuaChat = script.Parent.Parent + +local Create = require(LuaChat.Create) +local Constants = require(LuaChat.Constants) +local DialogInfo = require(LuaChat.DialogInfo) + +local Components = LuaChat.Components +local HeaderLoader = require(Components.HeaderLoader) + +local Intent = DialogInfo.Intent + +local DefaultScreen = {} + +DefaultScreen.__index = DefaultScreen + +function DefaultScreen.new(appState) + local self = {} + + setmetatable(self, DefaultScreen) + + local header = HeaderLoader.GetHeader(appState, Intent.DefaultScreen) + header:SetDefaultSubtitle() + + self.rbx = Create.new "Frame" { + Name = "DefaultScreen", + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Constants.Color.GRAY6, + BorderSizePixel = 0, + + Create.new "UIListLayout" { + SortOrder = "LayoutOrder", + }, + + header.rbx, + + Create.new "Frame" { + Name = "Main", + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1.0, + + Create.new "ImageLabel" { + BackgroundTransparency = 1.0, + Size = UDim2.new(0, 150, 0, 150), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Image = "rbxasset://textures/ui/LuaChat/icons/ic-chat-large.png", + } + }, + } + + return self +end + +function DefaultScreen:Update(current, previous) end + +return DefaultScreen diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/DialogComponents.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/DialogComponents.lua new file mode 100644 index 0000000..fe85a7a --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/DialogComponents.lua @@ -0,0 +1,673 @@ +-- TweenService and UserInputService required by TextInputDialog +local CoreGui = game:GetService("CoreGui") +local TweenService = game:GetService("TweenService") +local UserInputService = game:GetService("UserInputService") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local FormFactor = require(LuaApp.Enum.FormFactor) +local Signal = require(Common.Signal) + +local getInputEvent = require(LuaChat.Utils.getInputEvent) + +local Components = LuaChat.Components +local TextInputEntryComponent = require(Components.TextInputEntry) + +local PopRoute = require(LuaChat.Actions.PopRoute) + +local OptionDialog = {} +local TextInputDialog = {} +local ConfirmationDialog = {} +local AlertDialog = {} + +local DialogComponents = { + OptionDialog = OptionDialog, + TextInputDialog = TextInputDialog, + ConfirmationDialog = ConfirmationDialog, + AlertDialog = AlertDialog, +} + +local DEFAULT_DIALOG_WIDTH = 400 +local OPTION_DIALOG_GAP_HEIGHT = 8 + +local function GetDialogWidth(appState) + if appState.store:getState().FormFactor == FormFactor.TABLET then + return UDim.new(0, DEFAULT_DIALOG_WIDTH) + else + return UDim.new(1, -Constants.ModalDialog.CLEARANCE_DIALOG_SIDE) + end +end + +local function PushBackLayout(newObject, parentObject) + parentObject = parentObject or newObject.Parent + + local listLayout = parentObject:FindFirstChildOfClass('UIListLayout') + local highestLayout = -1 + if listLayout then + for _, child in pairs(parentObject:GetChildren()) do + if child:IsA('GuiBase') then + highestLayout = math.max(highestLayout, child.LayoutOrder) + end + end + end + newObject.LayoutOrder = highestLayout + 1 + return newObject +end + +local function makeDivider(height, color, transparency, layoutOrder) + transparency = transparency or 0 + layoutOrder = layoutOrder or 0 + + return Create.new"Frame" { + Name = "Divider", + BackgroundColor3 = color, + BackgroundTransparency = transparency, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, height), + Position = UDim2.new(0, 0, 1, -height), + LayoutOrder = layoutOrder, + } +end + +local function outerFrameAndHeader(appState, title, color, event) + color = color or Constants.Color.BLUE_PRIMARY + local dialogWidth = GetDialogWidth(appState) + + local dialogLayout = Create.new"UIListLayout" { + Name = "ListLayout", + SortOrder = Enum.SortOrder.LayoutOrder, + } + + local rbx = Create.new"TextButton" { + Name = title, + AutoButtonColor = false, + Text = "", + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1-Constants.Color.ALPHA_SHADOW_PRIMARY, + BackgroundColor3 = Constants.Color.GRAY1, + BorderSizePixel = 0, + Visible = false, + Active = true, + Create.new"ImageButton" { + Name = "Dialog", + AutoButtonColor = false, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(5,5,6,6), + Image = "rbxasset://textures/ui/LuaChat/9-slice/modal.png", + Size = UDim2.new(dialogWidth.Scale, dialogWidth.Offset, 0, 42), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = true, + + dialogLayout, + + Create.new"TextLabel" { + Name = "Title", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = Constants.Font.TITLE, + TextSize = Constants.Font.FONT_SIZE_18, + BackgroundColor3 = Constants.Color.WHITE, + Text = title, + Size = UDim2.new(1, 0, 0, 42), + TextColor3 = color, + LayoutOrder = 0, + }, + makeDivider(3, color, nil, 1), + }, + } + + --Seems to be necessary to make sure clicks get sunk and don't fall through when dialog is open + getInputEvent(rbx):Connect(function() + rbx.Visible = false + appState.store:dispatch(PopRoute()) + if event ~= nil then + event:Fire() + end + end) + + getInputEvent(rbx.Dialog):Connect(function() end) + + return rbx +end + +local function addCancelButtonCallbacks(dialogComponent, cancelButton, event) + local function onInputBegan(input) + cancelButton.BackgroundColor3 = Constants.Color.GRAY5 + end + cancelButton.InputBegan:Connect(onInputBegan) + + local function onInputEnded(input) + cancelButton.BackgroundColor3 = Constants.Color.WHITE + end + cancelButton.InputEnded:Connect(onInputEnded) + + getInputEvent(cancelButton):Connect(function() + dialogComponent:Close() + if event ~= nil then + event:Fire() + end + end) +end + +local function makeConfirmCancelButtons(cancelTitle, confirmTitle) + return Create.new"Frame" { + Name = "ConfirmAndCancelButtons", + BackgroundColor3 = Constants.Color.WHITE, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 48), + BorderSizePixel = 0, + Create.new"TextButton" { + Name = "Cancel", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = Enum.Font.SourceSans, + TextSize = Constants.Font.FONT_SIZE_18, + TextColor3 = Constants.Color.GRAY1, + BackgroundColor3 = Constants.Color.WHITE, + Text = cancelTitle, + Size = UDim2.new(0.5, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + AutoButtonColor = false, + }, + Create.new"Frame" { + BackgroundColor3 = Constants.Color.GRAY4, + BorderSizePixel = 0, + Size = UDim2.new(0, 1, 1, 0), + Position = UDim2.new(0.5, -1, 0, 0), + }, + Create.new"TextButton" { + Name = "Save", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = Enum.Font.SourceSans, + TextSize = Constants.Font.FONT_SIZE_18, + TextColor3 = Constants.Color.BLUE_PRIMARY, + BackgroundColor3 = Constants.Color.WHITE, + Text = confirmTitle, + Size = UDim2.new(0.5, 0, 1, 0), + Position = UDim2.new(0.5, 0, 0, 0), + AutoButtonColor = false, + }, + } +end + +-- Vertically resize the given frame so that it fits the contents: +local function ResizeFrame(frame) + local verticalSize = 0 + local horizontalSize = frame.Size.X + for _, v in pairs(frame:GetChildren()) do + if v:IsA("GuiObject") and (v.Visible == true) then + verticalSize = verticalSize + v.AbsoluteSize.Y + end + end + + local sizeFrame = UDim2.new(horizontalSize.Scale, horizontalSize.Offset, 0, verticalSize) + frame.Size = sizeFrame +end + +function AlertDialog.new(appState, titleKey, messageKey) + local self = { + appState = appState, + } + setmetatable(self, {__index = AlertDialog}) + + local title = titleKey ~= nil and appState.localization:Format(titleKey) or "" + local message = messageKey ~= nil and appState.localization:Format(messageKey) or "" + + self.rbx = outerFrameAndHeader(appState, title, Constants.Color.RED_PRIMARY) + + local messageFrame = Create.new"Frame" { + Name = "MessageFrame", + BackgroundTransparency = 0, + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.WHITE, + Size = UDim2.new(1, 0, 0, 100), + Create.new"TextLabel" { + Name = "Message", + Text = message, + TextWrapped = true, + Font = Enum.Font.SourceSans, + TextSize = Constants.Font.FONT_SIZE_16, + TextColor3 = Constants.Color.GRAY1, + BackgroundTransparency = 1, + Size = UDim2.new(1, -48, 1, -48), + Position = UDim2.new(0, 24, 0, 24), + } + } + messageFrame.Parent = self.rbx.Dialog + + local divider = makeDivider(1, Constants.Color.GRAY4) + divider.Parent = self.rbx.Dialog + + local cancelButtonFrame = Create.new"Frame" { + Name = "CancelFrame", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 42), + BorderSizePixel = 0, + Create.new"TextButton" { + Name = "Cancel", + BackgroundTransparency = 0, + BorderSizePixel = 0, + Font = Constants.Font.TITLE, + TextSize = Constants.Font.FONT_SIZE_18, + TextColor3 = Constants.Color.GRAY1, + BackgroundColor3 = Constants.Color.WHITE, + Text = appState.localization:Format("Feature.Chat.Action.Confirm"), + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + AutoButtonColor = false, + } + } + cancelButtonFrame.Parent = self.rbx.Dialog + + self.accepted = Instance.new("BindableEvent") + self.accepted.Parent = self.rbx + + addCancelButtonCallbacks(self, cancelButtonFrame.Cancel, self.accepted) + ResizeFrame(self.rbx.Dialog) + + return self +end + +function AlertDialog:Update(alert) + + local title = alert.titleKey ~= nil and self.appState.localization:Format(alert.titleKey) or "" + local message = alert.messageKey ~= nil and + self.appState.localization:Format(alert.messageKey, alert.messageArguments) or "" + + self.rbx.Dialog.Title.Text = title + self.rbx.Dialog.MessageFrame.Message.Text = message +end + +function AlertDialog:Prompt() + self.rbx.Visible = true +end + +function AlertDialog:Close() + self.rbx.Visible = false +end + +function ConfirmationDialog.new(appState, titleKey, messageKey, cancelTitleKey, confirmTitleKey) + local self = { + appState = appState, + } + setmetatable(self, {__index = ConfirmationDialog}) + + local title = titleKey ~= nil and appState.localization:Format(titleKey) or "" + local message = messageKey ~= nil and appState.localization:Format(messageKey) or "" + local cancelTitle = cancelTitleKey ~= nil and appState.localization:Format(cancelTitleKey) or "" + local confirmTitle = confirmTitleKey ~= nil and appState.localization:Format(confirmTitleKey) or "" + + self.rbx = outerFrameAndHeader(appState, title, Constants.Color.RED_PRIMARY) + + local messageFrame = Create.new"Frame" { + Name = "MessageFrame", + BackgroundTransparency = 1, + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.WHITE, + Size = UDim2.new(1, 0, 0, 100), + Create.new"TextButton" { + Name = "Message", + Text = message, + TextWrapped = true, + Font = Enum.Font.SourceSans, + TextSize = Constants.Font.FONT_SIZE_16, + TextColor3 = Constants.Color.GRAY1, + BackgroundTransparency = 1, + Size = UDim2.new(1, -48, 1, -48), + Position = UDim2.new(0, 24, 0, 24), + AutoButtonColor = false, + } + } + messageFrame.Parent = self.rbx.Dialog + PushBackLayout(messageFrame) + + local divider = makeDivider(1, Constants.Color.GRAY4) + divider.Parent = self.rbx.Dialog + PushBackLayout(divider) + + local buttons = makeConfirmCancelButtons(cancelTitle, confirmTitle) + buttons.Parent = self.rbx.Dialog + PushBackLayout(buttons) + + local cancelButton = buttons.Cancel + addCancelButtonCallbacks(self, cancelButton) + + local saveButton = buttons.Save + buttons.Save.TextColor3 = Constants.Color.RED_PRIMARY + + local function onInputBegan(input) + saveButton.BackgroundColor3 = Constants.Color.GRAY5 + end + + local function onInputEnded(input) + saveButton.BackgroundColor3 = Constants.Color.WHITE + end + + saveButton.InputBegan:Connect(onInputBegan) + saveButton.InputEnded:Connect(onInputEnded) + + getInputEvent(saveButton):Connect(function() + self.rbx.Visible = false + appState.store:dispatch(PopRoute()) + self.saved:Fire(self.data) + end) + + self.saved = Signal.new() + ResizeFrame(self.rbx.Dialog) + + self.data = nil + + return self +end + +function ConfirmationDialog:Close() + self.appState.store:dispatch(PopRoute()) +end + +function ConfirmationDialog:Update(messageKey, data, messageArguments) + self.data = data + local message = messageKey ~= nil and self.appState.localization:Format(messageKey, messageArguments) or "" + + self.rbx.Dialog.MessageFrame.Message.Text = message +end + +function ConfirmationDialog:Destruct() + self.rbx:Destroy() +end + +function TextInputDialog.new(appState, titleLocalizationKey, maxChar) + + local self = { + appState = appState, + } + setmetatable(self, {__index = TextInputDialog}) + + self.connections = {} + + local title = appState.localization:Format(titleLocalizationKey) + + self.cancel = Signal.new() + self.rbx = outerFrameAndHeader(appState, title, Constants.Color.BLUE_PRIMARY, self.cancel) + + local placeholderText = appState.localization:Format("Feature.Chat.Description.NameGroupChat") + local textInput = TextInputEntryComponent.new(appState, nil, placeholderText) + textInput.rbx.BackgroundTransparency = 1 + textInput.rbx.Size = UDim2.new(1,-10,1,0) + textInput.rbx.Position = UDim2.new(0,10,0,0) + textInput:ShowDivider(false) + self.textInputComponent = textInput + + self.value = textInput.value + + local textCount = Create.new"TextLabel"{ + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 18), + Position = UDim2.new(1, 0, 1, 0), + AnchorPoint = Vector2.new(1,0), + TextXAlignment = Enum.TextXAlignment.Right, + Text = "0/" .. maxChar, + TextColor3 = Constants.Color.GRAY2, + Font = Enum.Font.SourceSans, + TextSize = Constants.Font.FONT_SIZE_12, + TextYAlignment = "Top", + } + + local textInputContainer = Create.new"Frame"{ + BackgroundTransparency = 1, + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.WHITE, + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, 98), + Create.new"ImageLabel"{ + Name = "TextBackground", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1, -40, 0, 36), + Position = UDim2.new(0.5, 0, 0.5, -5), + AnchorPoint = Vector2.new(0.5, 0.5), + ScaleType = "Slice", + SliceCenter = Rect.new(3,3,4,4), + Image = "rbxasset://textures/ui/LuaChat/9-slice/input-default.png", + textInput.rbx, -- set parent + textCount, -- set parent + }, + } + + textInputContainer.Parent = self.rbx.Dialog + + local divider = makeDivider(1, Constants.Color.GRAY4) + divider.LayoutOrder = 3 + divider.Parent = self.rbx.Dialog + + local cancelTitle = appState.localization:Format("Feature.Chat.Action.Cancel") + local confirmTitle = appState.localization:Format("Feature.Chat.Action.Save") + + local buttons = makeConfirmCancelButtons(cancelTitle, confirmTitle) + buttons.LayoutOrder = 4 + buttons.Parent = self.rbx.Dialog + + local function isSubmitable() + return self.value:len() <= maxChar + end + + local cancelButton = buttons.Cancel + + addCancelButtonCallbacks(self, cancelButton, self.cancel) + + local saveButton = buttons.Save + local function onInputBegan(input) + saveButton.BackgroundColor3 = Constants.Color.GRAY5 + end + + local function onInputEnded(input) + saveButton.BackgroundColor3 = Constants.Color.WHITE + end + + saveButton.MouseButton1Down:Connect(onInputBegan) + saveButton.MouseButton1Up:Connect(onInputEnded) + + getInputEvent(saveButton):Connect(function() + if isSubmitable() then + self:Close() + self.saved:Fire(self.value) + end + end) + + textInput.textBoxChanged:Connect(function() + self.value = textInput.value + + saveButton.TextColor3 = isSubmitable() and Constants.Color.BLUE_PRIMARY or Constants.Color.RED_PRIMARY + textCount.TextColor3 = isSubmitable() and Constants.Color.GRAY2 or Constants.Color.RED_PRIMARY + textCount.Text = string.format("%s/%s", tostring(self.value:len()), tostring(maxChar)) + end) + + self.saved = Signal.new() + ResizeFrame(self.rbx.Dialog) + + self:SetupTweenForKeyboardEvents() + + return self +end + +function TextInputDialog:SetupTweenForKeyboardEvents() + self.tweenGroupNameDialogUp = nil + self.tweenGroupNameDialogDown = nil + local inputServiceConnection = UserInputService:GetPropertyChangedSignal('OnScreenKeyboardVisible'):Connect(function() + if UserInputService.OnScreenKeyboardVisible then + if self.tweenGroupNameDialogUp == nil then + local duration = UserInputService.OnScreenKeyboardAnimationDuration + local yPos = UserInputService.OnScreenKeyboardPosition.Y / 2 + local tweenInfo = TweenInfo.new(duration) + local propertyGoals = + { + Position = UDim2.new(0.5, 0, 0, yPos) + } + self.tweenGroupNameDialogUp = TweenService:Create(self.rbx.Dialog, tweenInfo, propertyGoals) + end + self.tweenGroupNameDialogUp:Play() + else + if self.tweenGroupNameDialogDown == nil then + local duration = UserInputService.OnScreenKeyboardAnimationDuration + local tweenInfo = TweenInfo.new(duration) + local propertyGoals = + { + Position = UDim2.new(0.5, 0, 0.5, 0) + } + self.tweenGroupNameDialogDown = TweenService:Create(self.rbx.Dialog, tweenInfo, propertyGoals) + end + self.tweenGroupNameDialogDown:Play() + end + end) + table.insert(self.connections, inputServiceConnection) +end + +function TextInputDialog:Close() + self.textInputComponent.rbx.TextBox:ReleaseFocus() + self.appState.store:dispatch(PopRoute()) +end + +function TextInputDialog:Update(value) + self.textInputComponent:Update(value) +end + +function TextInputDialog:Destruct() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + + self.connections = {} + self.textInputComponent:Destruct() + self.rbx:Destroy() +end + +function OptionDialog.new(appState, titleKey, options, userId) + local self = { + appState = appState, + } + setmetatable(self, {__index = OptionDialog}) + + local title = titleKey ~= nil and appState.localization:Format(titleKey) or "" + + self.rbx = outerFrameAndHeader(appState, title) + self.rbx.Dialog.AnchorPoint = Vector2.new(0.5, 1) + self.rbx.Dialog.Position = UDim2.new(0.5, 0, 1, -10) + + self.selected = Signal.new() + + self.optionGuis = {} + + local layout = Create.new"UIListLayout"{ + Name = "ListLayout", + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = "Center", + VerticalAlignment = "Bottom", + } + layout.Parent = self.rbx + + for optionId, optionTitleKey in pairs(options) do + + local optionTitle = appState.localization:Format(optionTitleKey) + + local optionGui = Create.new"TextButton"{ + Name = optionTitle, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = Enum.Font.SourceSans, + TextSize = Constants.Font.FONT_SIZE_18, + BackgroundColor3 = Constants.Color.WHITE, + Text = optionTitle, + Size = UDim2.new(1, 0, 0, 42 + 1), + TextColor3 = Constants.Color.GRAY1, + AutoButtonColor = false, + } + + local divider = makeDivider(1, Constants.Color.GRAY4) + divider.Position = UDim2.new(0,0,1,0) + divider.Parent = optionGui + + local function onInputBegan(input) + optionGui.BackgroundColor3 = Constants.Color.GRAY5 + end + optionGui.InputBegan:Connect(onInputBegan) + + local function onInputEnded(input) + optionGui.BackgroundColor3 = Constants.Color.WHITE + end + optionGui.InputEnded:Connect(onInputEnded) + + getInputEvent(optionGui):Connect(function() + self.rbx.Visible = false + appState.store:dispatch(PopRoute()) + self.selected:Fire(optionId, self.data or {}) + end) + + optionGui.Parent = self.rbx.Dialog + PushBackLayout(optionGui) + + self.optionGuis[optionId] = optionGui + end + + local firstDivider = makeDivider(OPTION_DIALOG_GAP_HEIGHT, Constants.Color.WHITE, 1, 1) + firstDivider.BackgroundTransparency = 1 + firstDivider.Parent = self.rbx + + local cancelButtonFrame = Create.new"ImageLabel" { + Name = "CancelButton", + BackgroundTransparency = 1, + Size = UDim2.new(self.rbx.Dialog.Size.X.Scale, self.rbx.Dialog.Size.X.Offset, 0, Constants.ModalDialog.BUTTON_HEIGHT), + BorderSizePixel = 0, + ScaleType = "Slice", + SliceCenter = Rect.new(5,5,6,6), + Image = "rbxasset://textures/ui/LuaChat/9-slice/modal.png", + LayoutOrder = 2, + Create.new"TextButton" { + Name = "Cancel", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = Constants.Font.TITLE, + TextSize = Constants.Font.FONT_SIZE_18, + TextColor3 = Constants.Color.GRAY1, + BackgroundColor3 = Constants.Color.WHITE, + Text = appState.localization:Format("Feature.Chat.Action.Cancel"), + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + AutoButtonColor = false, + } + } + cancelButtonFrame.Parent = self.rbx + addCancelButtonCallbacks(self, cancelButtonFrame.Cancel) + + local secondDivider = makeDivider(OPTION_DIALOG_GAP_HEIGHT, Constants.Color.WHITE, 1, 3) + secondDivider.BackgroundTransparency = 1 + secondDivider.Parent = self.rbx + + self.data = userId + self:Resize() + + return self +end + +-- Helper function so ResizeFrame doesn't have to be called externally: +function OptionDialog:Resize() + ResizeFrame(self.rbx.Dialog) +end + +function OptionDialog:Close() + self.appState.store:dispatch(PopRoute()) + +end + +function OptionDialog:Destruct() + self.rbx:Destroy() +end + +return DialogComponents diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/EditChatGroup.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/EditChatGroup.lua new file mode 100644 index 0000000..d0a7ece --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/EditChatGroup.lua @@ -0,0 +1,189 @@ +local CoreGui = game:GetService("CoreGui") +local Players = game:GetService("Players") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local ConversationActions = require(LuaChat.Actions.ConversationActions) +local ConversationModel = require(LuaChat.Models.Conversation) +local Create = require(LuaChat.Create) +local DialogInfo = require(LuaChat.DialogInfo) +local Immutable = require(Common.Immutable) +local Signal = require(Common.Signal) + + +local Components = LuaChat.Components +local FriendSearchBoxComponent = require(Components.FriendSearchBox) +local HeaderLoader = require(Components.HeaderLoader) +local ResponseIndicator = require(Components.ResponseIndicator) + +local SetRoute = require(LuaChat.Actions.SetRoute) + +local Intent = DialogInfo.Intent + +local EditChatGroup = {} +EditChatGroup.__index = EditChatGroup + +function EditChatGroup.new(appState, maxSize, convoId) + local self = { + appState = appState, + maxSize = maxSize, + convoId = convoId, + } + self.connections = {} + setmetatable(self, EditChatGroup) + + self.conversation = ConversationModel.empty() + self.alreadyParticipants = {} + + local oldConversation = self.appState.store:getState().ChatAppReducer.Conversations[self.convoId] + self.fromType = oldConversation.conversationType + for _, userId in pairs(oldConversation.participants) do + self.alreadyParticipants[userId] = true + end + + self.searchComponent = FriendSearchBoxComponent.new( + appState, + self.conversation.participants, + self.maxSize, + function(user) + local isNotPlayer = user.id ~= tostring(Players.LocalPlayer.UserId) + local isNotInList = not self.alreadyParticipants[user.id] + return user.isFriend and isNotPlayer and isNotInList + end + ) + local addParticipantConnection = self.searchComponent.addParticipant:Connect(function(id) + self.searchComponent.search:ReleaseFocus() + self:ChangeParticipants(Immutable.Set(self.conversation.participants, #self.conversation.participants+1, id)) + end) + table.insert(self.connections, addParticipantConnection) + + local removeParticipantConnection = self.searchComponent.removeParticipant:Connect(function(id) + self.searchComponent.search:ReleaseFocus() + self:ChangeParticipants(Immutable.RemoveValueFromList(self.conversation.participants, id)) + end) + table.insert(self.connections, removeParticipantConnection) + + -- Header: + self.header = HeaderLoader.GetHeader(appState, Intent.EditChatGroup) + self.header:SetDefaultSubtitle() + self.header:SetTitle(appState.localization:Format("Feature.Chat.Label.AddFriends")) + self.header:SetBackButtonEnabled(true) + + self.responseIndicator = ResponseIndicator.new(appState) + self.responseIndicator:SetVisible(false) + + self.rbx = Create.new"Frame" { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Constants.Color.GRAY5, + BorderSizePixel = 0, + BackgroundTransparency = 1, + + self.header.rbx, + Create.new"Frame" { + Name = "Content", + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Constants.Color.GRAY5, + BorderSizePixel = 0, + + self.searchComponent.rbx, + }, + self.responseIndicator.rbx, + } + + self.saveGroup = self.header:CreateHeaderButton("SaveGroup", "Feature.Chat.Action.Add") + self.saveGroup:SetEnabled(false) + + self.header:AddButton(self.saveGroup) + local saveGroupConnection = self.saveGroup.Pressed:Connect(function() + self.searchComponent.search:ReleaseFocus() + if #self.conversation.participants == 0 then + return + end + if self.fromType == ConversationModel.Type.MULTI_USER_CONVERSATION then + self.responseIndicator:SetVisible(true) + + self.appState.store:dispatch( + ConversationActions.AddUsersToConversation(self.convoId, self.conversation.participants, function() + self.responseIndicator:SetVisible(false) + self.appState.store:dispatch(SetRoute( + Intent.Conversation, + {conversationId = self.convoId}, + Intent.ConversationHub + )) + end) + ) + elseif self.fromType == ConversationModel.Type.ONE_TO_ONE_CONVERSATION then + self.responseIndicator:SetVisible(true) + + local originalConvo = self.appState.store:getState().ChatAppReducer.Conversations[self.convoId] + local allParticipants = Immutable.Append(self.conversation.participants) + for _, userId in ipairs(originalConvo.participants) do + if userId ~= tostring(Players.LocalPlayer.UserId) then + table.insert(allParticipants, userId) + end + end + local newConvo = Immutable.JoinDictionaries(originalConvo, { + participants = allParticipants, + clientId = self.conversation.clientId, + title = "", + }) + + self.appState.store:dispatch( + ConversationActions.CreateConversation(newConvo, function(passedConversationId) + self.responseIndicator:SetVisible(false) + self.appState.store:dispatch( + SetRoute(Intent.Conversation, {conversationId = passedConversationId}, Intent.ConversationHub) + ) + end) + ) + end + end) + table.insert(self.connections, saveGroupConnection) + + self.rbx.Content.Position = UDim2.new(0, 0, 0, self.header.rbx.Size.Y.Offset) + self.rbx.Content.Size = UDim2.new(1, 0, 1, -self.header.rbx.Size.Y.Offset) + + self.BackButtonPressed = Signal.new() + self.header.BackButtonPressed:Connect(function() + self.searchComponent.search:ReleaseFocus() + self.BackButtonPressed:Fire() + end) + + spawn(function() + self.appState.store:dispatch(ConversationActions.GetAllFriendsAsync()) + end) + + self.tooManyFriendsAlertId = nil + + return self +end + +function EditChatGroup:ChangeParticipants(participants) + self.conversation = Immutable.Set(self.conversation, "participants", participants) + self.searchComponent:Update(participants) + self.saveGroup:SetEnabled(#participants > 0) +end + +function EditChatGroup:Update(current, previous) + self.header:SetConnectionState(current.ConnectionState) + self.searchComponent:Update(self.conversation.participants) + + self.saveGroup:SetEnabled(#self.conversation.participants ~= #self.alreadyParticipants) +end + +function EditChatGroup:Destruct() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + + self.header:Destroy() + self.responseIndicator:Destruct() + self.searchComponent:Destruct() + self.rbx:Destroy() +end + +return EditChatGroup \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/FriendCarousel.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/FriendCarousel.lua new file mode 100644 index 0000000..0608f1d --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/FriendCarousel.lua @@ -0,0 +1,227 @@ +-- +-- FriendCarousel +-- +-- This is a scrollable list of friend icons. +-- + +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaApp = Modules.LuaApp + +local ApiFetchUsersThumbnail = require(LuaApp.Thunks.ApiFetchUsersThumbnail) +local Constants = require(LuaApp.Constants) +local Roact = require(Common.Roact) +local RoactRodux = require(Common.RoactRodux) +local UserModel = require(LuaApp.Models.User) + +local FriendCarousel = Roact.Component:extend("FriendCarousel") + +local IMAGE_DOT_INGAME = "rbxasset://textures/ui/LuaApp/icons/ic-green-dot.png" +local IMAGE_DOT_ONLINE = "rbxasset://textures/ui/LuaApp/icons/ic-blue-dot.png" +local IMAGE_DOT_STUDIO = "rbxasset://textures/ui/LuaApp/icons/ic-orange-dot.png" +local DEFAULT_DOT_SIZE = 8 + +local IMAGE_MASK = "rbxasset://textures/ui/LuaChat/graphic/friendmask.png" +local MASK_WIDTH = 10 + +-- Frame around the profile image: +local IMAGE_PROFILE_BORDER = "rbxasset://textures/ui/LuaChat/graphic/gr-profile-border-36x36.png" +local IMAGE_PROFILE_DEFAULT = "rbxasset://textures/ui/LuaChat/icons/ic-profile.png" + +function FriendCarousel:init() + self.state = { + fadeScrollLeft = false, + fadeScrollRight = false, + } +end + +function FriendCarousel:onPositionChanged(rbx) + -- Programatically show / hide the fade bars at either side of the carousel. + -- This hides items that are partly visible, but completely reveals the + -- first / last items when they're present. + + -- Early return if we're not set up yet: + if (rbx.CanvasSize.X.Offset == 0) or (rbx.CanvasSize.Y.Offset == 0) or + (rbx.AbsoluteWindowSize.X == 0) or (rbx.AbsoluteWindowSize.Y == 0) then + return + end + + local fadeLeft = (0 < rbx.CanvasPosition.X) + local fadeRight = (rbx.CanvasSize.X.Offset - rbx.CanvasPosition.X) > rbx.AbsoluteWindowSize.X + + if (fadeLeft ~= self.state.fadeScrollLeft) or + (fadeRight ~= self.state.fadeScrollRight) then + spawn(function() + self:setState( { + fadeScrollLeft = fadeLeft, + fadeScrollRight = fadeRight, + }) + end) + end +end + +function FriendCarousel:didMount() + if self.rbxScroller then + self:onPositionChanged(self.rbxScroller) + end +end + +function FriendCarousel:render() + -- Visual properties of this game card: + local dotSize = self.props.dotSize or DEFAULT_DOT_SIZE + local friends = self.props.friends or {} + local getUserThumbnail = self.props.getUserThumbnail + local horizontalAlignment = self.props.HorizontalAlignment or Enum.HorizontalAlignment.Left + local itemGap = self.props.itemGap + local itemSize = self.props.itemSize + local layoutOrder = self.props.LayoutOrder + local size = self.props.Size or UDim2.new(1, 0, 0, itemSize) + local users = self.props.users + + -- Build up a horizontal list of items for our card: + local friendItems = {} + friendItems["Layout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = horizontalAlignment, + Padding = UDim.new(0, itemGap), + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + }) + + local countFriends = #friends + if countFriends > 0 then + for index, friend in ipairs(friends) do + -- Look up the presence information: + local imageFriend = nil + local iconDot = IMAGE_DOT_ONLINE + local userFriend = users[friend.uid] + if userFriend then + if userFriend.presence == UserModel.PresenceType.IN_GAME then + iconDot = IMAGE_DOT_INGAME + elseif userFriend.presence == UserModel.PresenceType.IN_STUDIO then + iconDot = IMAGE_DOT_STUDIO + end + + -- Find images for the friend portraits: + if userFriend.thumbnails and userFriend.thumbnails.HeadShot + and userFriend.thumbnails.HeadShot.Size48x48 then + imageFriend = userFriend.thumbnails.HeadShot.Size48x48 + end + + if imageFriend == nil then + imageFriend = IMAGE_PROFILE_DEFAULT + getUserThumbnail(friend.uid) + end + end + + friendItems[index] = Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = index, + Size = UDim2.new(0, itemSize, 0, itemSize), + }, { + Profile = Roact.createElement("ImageButton", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = imageFriend, + Size = UDim2.new(0, itemSize, 0, itemSize), + ZIndex = 1, + }), + + Border = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = IMAGE_PROFILE_BORDER, + Size = UDim2.new(0, itemSize, 0, itemSize), + ZIndex = 2, + }), + + Dot = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = iconDot, + Position = UDim2.new(1, -dotSize, 1, -dotSize), + Size = UDim2.new(0, dotSize, 0, dotSize), + ZIndex = 3, + }), + }) + end + end + + local maskLeft = nil + if self.state.fadeScrollLeft then + maskLeft = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = IMAGE_MASK, + Position = UDim2.new(0, 0, 0, 0), + Rotation = 180, + Size = UDim2.new(0, MASK_WIDTH, 1, 0), + ZIndex = 2, + }) + end + + local maskRight = nil + if self.state.fadeScrollRight then + maskRight = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = IMAGE_MASK, + Position = UDim2.new(1, -MASK_WIDTH, 0, 0), + Size = UDim2.new(0, MASK_WIDTH, 1, 0), + ZIndex = 2, + }) + end + + -- This frame arrangement adds a semi-transparent overlay to + -- fade out items at the edge of the frame: + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + Position = UDim2.new(0, 0, 0, 0), + Size = size, + },{ + MaskLeft = maskLeft, + + MaskRight = maskRight, + + ScrollyFrame = Roact.createElement("ScrollingFrame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + CanvasSize = UDim2.new(0, (itemSize + itemGap) * countFriends, 0, itemSize), + ClipsDescendants = true, + ScrollBarThickness = 0, + Size = UDim2.new(1, 0, 1, 0), + [Roact.Change.AbsolutePosition] = function(rbx, changed) + self:onPositionChanged(rbx) + end, + [Roact.Ref] = function(rbx) + self.rbxScroller = rbx + end, + }, friendItems) + }) +end + +FriendCarousel = RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + users = state.Users, + } + end, + function(dispatch) + return { + getUserThumbnail = function(friendId) + spawn(function() + dispatch(ApiFetchUsersThumbnail(nil, { friendId }, + Constants.AvatarThumbnailRequests.FRIEND_CAROUSEL + )) + end) + end, + } + end +)(FriendCarousel) + +return FriendCarousel \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/FriendCarousel.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/FriendCarousel.spec.lua new file mode 100644 index 0000000..b5f2490 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/FriendCarousel.spec.lua @@ -0,0 +1,38 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local AppReducer = require(Modules.LuaApp.AppReducer) + local MockId = require(Modules.LuaApp.MockId) + local Roact = require(Modules.Common.Roact) + local RoactRodux = require(Modules.Common.RoactRodux) + local Rodux = require(Modules.Common.Rodux) + + local FriendCarousel = require(Modules.LuaChat.Components.FriendCarousel) + + it("should create and destroy without errors", function() + + local store = Rodux.Store.new(AppReducer) + + local gameFriends = { { uid = MockId() }, { uid = MockId() }, + { uid = MockId() }, { uid = MockId() }, { uid = MockId() } } + local carouselItemGap = 9 + local carouselItemHeight = 32 + local carouselItemDotSize = 10 + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + GameFriends = Roact.createElement(FriendCarousel, { + dotSize = carouselItemDotSize, + friends = gameFriends, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + itemGap = carouselItemGap, + itemSize = carouselItemHeight, + Size = UDim2.new(1, 0, 1, carouselItemHeight), + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/FriendSearchBox.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/FriendSearchBox.lua new file mode 100644 index 0000000..682c651 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/FriendSearchBox.lua @@ -0,0 +1,292 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local Functional = require(Common.Functional) +local Signal = require(Common.Signal) +local ToastModel = require(LuaChat.Models.ToastModel) +local getInputEvent = require(LuaChat.Utils.getInputEvent) + +local Components = LuaChat.Components +local ListSection = require(Components.ListSection) +local UserList = require(Components.UserList) +local UserThumbnailBar = require(Components.UserThumbnailBar) + +local ShowToast = require(LuaChat.Actions.ShowToast) + +local FFlagTextBoxOverrideManualFocusRelease = settings():GetFFlag("TextBoxOverrideManualFocusRelease") + +local CLEAR_TEXT_WIDTH = 44 +local ICON_CELL_WIDTH = 60 +local SEARCH_BOX_HEIGHT = 48 + +local FriendSearchBox = {} +FriendSearchBox.__index = FriendSearchBox + +function FriendSearchBox.new(appState, participants, maxParticipantCount, filter) + local self = { + appState = appState, + participants = participants, + users = appState.store:getState().Users, + maxParticipantCount = maxParticipantCount, + } + self.connections = {} + setmetatable(self, FriendSearchBox) + + self.friendThumbnails = UserThumbnailBar.new(appState, maxParticipantCount, 1) + local removedConnection = self.friendThumbnails.removed:Connect(function(id) + self:RemoveParticipant(id) + end) + table.insert(self.connections, removedConnection) + + self.userList = UserList.new(appState, nil, filter) + local userSelectedConnection = self.userList.userSelected:Connect(function(user) + local selected = Functional.Find(self.participants, user.id) + if selected then + self:RemoveParticipant(user.id) + else + self:AddParticipant(user.id) + end + self:ClearText() + end) + table.insert(self.connections, userSelectedConnection) + + self.rbx = Create.new"Frame" { + Name = "FriendSearchBox", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + + Create.new"UIListLayout" { + Name = "ListLayout", + SortOrder = "LayoutOrder", + }, + self.friendThumbnails.rbx, + Create.new"Frame" { + Name = "Divider1", + BackgroundColor3 = Constants.Color.GRAY4, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 1), + LayoutOrder = 2, + }, + ListSection.new(appState, nil, 3).rbx, + Create.new"Frame" { + Name = "SearchContainer", + BackgroundTransparency = 0, + BackgroundColor3 = Constants.Color.WHITE, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, SEARCH_BOX_HEIGHT), + LayoutOrder = 4, + + Create.new"ImageLabel" { + Name = "SearchIcon", + BackgroundTransparency = 1, + Size = UDim2.new(0, 24, 0, 24), + Position = UDim2.new(0, ICON_CELL_WIDTH/2, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + ImageColor3 = Constants.Color.GRAY3, + Image = "rbxasset://textures/ui/LuaChat/icons/ic-search.png", + }, + Create.new"TextBox" { + Name = "Search", + BackgroundTransparency = 1, + Size = UDim2.new(1, -CLEAR_TEXT_WIDTH-ICON_CELL_WIDTH, 1, 0), + Position = UDim2.new(0, ICON_CELL_WIDTH, 0, 0), + TextSize = Constants.Font.FONT_SIZE_18, + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSans, + Text = "", + PlaceholderText = appState.localization:Format("Feature.Friends.Label.SearchFriends"), + PlaceholderColor3 = Constants.Color.GRAY3, + TextXAlignment = Enum.TextXAlignment.Left, + OverlayNativeInput = true, + ClearTextOnFocus = false, + ClipsDescendants = true, + }, + Create.new"ImageButton" { + Name = "Clear", + BackgroundTransparency = 1, + Size = UDim2.new(0, 16, 0, 16), + Position = UDim2.new(1, -(CLEAR_TEXT_WIDTH/2), 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + AutoButtonColor = false, + Image = "rbxasset://textures/ui/LuaChat/icons/ic-clear-solid.png", + ImageTransparency = 1, + }, + }, + Create.new"Frame" { + Name = "Divider2", + BackgroundColor3 = Constants.Color.GRAY4, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 1), + LayoutOrder = 5, + }, + Create.new"ScrollingFrame" { + Name = "ScrollingFrame", + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScrollBarThickness = 5, + BottomImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png", + MidImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png", + TopImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png", + LayoutOrder = 6, + + self.userList.rbx, + }, + } + + self.searchContainer = self.rbx.SearchContainer + local clearButton = self.searchContainer.Clear + local search = self.searchContainer.Search + self.search = search + if FFlagTextBoxOverrideManualFocusRelease then + search.ManualFocusRelease = true + end + + local clearButtonConnection = getInputEvent(clearButton):Connect(function() + self:ClearText() + end) + table.insert(self.connections, clearButtonConnection) + + self:Resize() + + local function updateClearButtonVisibility() + -- If we were to set the visible property of the clear button on the textbox focus lost event + -- it would disable the clear button, which in turn would stop the click event + -- from being able to notify the button + local visible = search:IsFocused() and (search.Text ~= "") + clearButton.ImageTransparency = visible and 0 or 1 + end + + self.searchChanged = Signal.new() + self.addParticipant = Signal.new() + self.removeParticipant = Signal.new() + + local searchChangedConnection = search:GetPropertyChangedSignal("Text"):Connect(function() + updateClearButtonVisibility() + self.userList:ApplySearch(search.Text) + self:Resize() + self:ResizeCanvas() + end) + table.insert(self.connections, searchChangedConnection) + + local focusedConnection = search.Focused:Connect(updateClearButtonVisibility) + table.insert(self.connections, focusedConnection) + local focusLostConnection = search.FocusLost:Connect(updateClearButtonVisibility) + table.insert(self.connections, focusLostConnection) + + self:UpdateFriends(appState.store:getState().Users, self.participants) + + local userListAddConnection = self.userList.rbx.ChildAdded:Connect(function(child) + self:ResizeCanvas(); + end) + table.insert(self.connections, userListAddConnection) + + local userListRemoveConnection = self.userList.rbx.ChildRemoved:Connect(function(child) + self:ResizeCanvas(); + end) + table.insert(self.connections, userListRemoveConnection) + + local userListSizeConnection = self.userList.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + self:ResizeCanvas() + end) + table.insert(self.connections, userListSizeConnection) + + return self +end + +function FriendSearchBox:Resize() + + local height = 0 + for _, element in pairs(self.rbx:GetChildren()) do + if element:IsA("GuiObject") and element.Visible and element.Name ~= "ScrollingFrame" then + height = height + element.AbsoluteSize.Y + end + end + + self.rbx.ScrollingFrame.Size = UDim2.new(1, 0, 1, -height) +end + +function FriendSearchBox:ResizeCanvas() + + local height = 0 + for _, element in pairs(self.userList.rbx:GetChildren()) do + if element:IsA("GuiObject") and element.Visible then + height = height + element.AbsoluteSize.Y + end + end + self.rbx.ScrollingFrame.CanvasSize = UDim2.new(1, 0, 0, height) +end + +function FriendSearchBox:AddParticipant(userId) + if #self.participants >= self.maxParticipantCount then + + if self.tooManyFriendsToastModel == nil then + local messageKey = "Feature.Chat.Message.ToastText" + local messageArguments = { + friendNum = tostring(Constants.MAX_PARTICIPANT_COUNT+1), + } + local toastModel = ToastModel.new(Constants.ToastIDs.TOO_MANY_PEOPLE, messageKey, messageArguments) + self.tooManyFriendsToastModel = toastModel + end + + self.appState.store:dispatch(ShowToast(self.tooManyFriendsToastModel)) + else + self.addParticipant:Fire(userId) + end +end + +function FriendSearchBox:RemoveParticipant(userId) + self.removeParticipant:Fire(userId) +end + +function FriendSearchBox:UpdateFriends(users, selectedList) + local friends = {} + for _, user in pairs(users) do + table.insert(friends, user) + end + self.userList:Update(friends, selectedList) + self:Resize() +end + +function FriendSearchBox:ClearText() + self.searchContainer.Search.Text = "" + self:Resize() +end + +function FriendSearchBox:Update(participants) + local state = self.appState.store:getState() + local users = state.Users + + if participants ~= self.participants then + self.friendThumbnails:Update(participants) + end + + if participants ~= self.participants or users ~= self.users then + self:UpdateFriends(users, participants) + end + + self.participants = participants + self.users = users + + self:Resize() +end + +function FriendSearchBox:Destruct() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + + self.connections = {} + self.userList:Destruct() + self.friendThumbnails:Destruct() + self.rbx.Parent = nil + self.rbx:Destroy() +end + +return FriendSearchBox diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/GameShareCard.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GameShareCard.lua new file mode 100644 index 0000000..c873106 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GameShareCard.lua @@ -0,0 +1,206 @@ +local CoreGui = game:GetService("CoreGui") +local Players = game:GetService("Players") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local ConversationThumbnail = require(LuaChat.Components.ConversationThumbnail) +local Create = require(LuaChat.Create) +local DateTime = require(LuaChat.DateTime) +local getConversationDisplayTitle = require(LuaChat.Utils.getConversationDisplayTitle) +local Signal = require(Common.Signal) +local Text = require(LuaChat.Text) +local TextButton = require(LuaChat.Components.TextButton) +local UserPresenceTextLabel = require(LuaChat.Components.UserPresenceTextLabel) +local FlagSettings = require(LuaChat.FlagSettings) + +local UseCppTextTruncation = FlagSettings.UseCppTextTruncation() + +local GameShareCard = {} + +GameShareCard.__index = GameShareCard + +local ICON_CELL_WIDTH = 60 +local HEIGHT = 54 + +local function getOneToOneConversationFriend(appState, conversation) + local friend = nil + if #conversation.participants == 2 then + local friendId = nil + for _, userId in ipairs(conversation.participants) do + if userId ~= tostring(Players.LocalPlayer.UserId) then + friendId = userId + break + end + end + if friendId then + friend = appState.store:getState().Users[friendId] + end + end + return friend +end + +function GameShareCard.new(appState, conversation) + local self = {} + self.appState = appState + self.conversation = conversation + self.conversationId = conversation.id + self.connections = {} + self.Tapped = Signal.new() + self.startTime = DateTime.now() + setmetatable(self, GameShareCard) + + local friend = getOneToOneConversationFriend(appState, conversation) + + self.rbx = Create.new"Frame" { + Name = "GameShareCard", + BackgroundTransparency = 0, + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.WHITE, + Size = UDim2.new(1, 0, 0, HEIGHT), + } + + local conversationThumbnail = ConversationThumbnail.new(appState, conversation) + conversationThumbnail.rbx.Size = UDim2.new(0, 36, 0, 36) + conversationThumbnail.rbx.Position = UDim2.new(0.5, 0, 0.5, 0) + conversationThumbnail.rbx.AnchorPoint = Vector2.new(0.5, 0.5) + self.conversationThumbnail = conversationThumbnail + + if friend then + self.presenceSubLabel = UserPresenceTextLabel.new(appState, friend.id, { + Name = "subLabel", + Size = UDim2.new(1, -ICON_CELL_WIDTH, 0.35, 0), + Position = UDim2.new(0, ICON_CELL_WIDTH, 0.55, 0), + }) + self.presenceSubLabel.rbx.Parent = self.rbx + end + + self.sendButton = TextButton.new(self.appState, "SendButton", "Feature.Chat.Action.Send") + local label = self.sendButton.rbx.Label + label.TextColor3 = Constants.Color.GRAY1 + label.TextSize = Constants.Font.FONT_SIZE_16 + label.Font = Enum.Font.SourceSans + self.sendButton:SetEnabled(true) + self.sendButton.rbx.AnchorPoint = Vector2.new(0.5, 0.5) + self.sendButton.rbx.Position = UDim2.new(0.5, 0, 0.5, 0) + local sendTextWidth = Text.GetTextWidth(label.Text, label.Font, label.TextSize) + + local conversationThumbnailFrame = Create.new"Frame" { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, ICON_CELL_WIDTH, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + conversationThumbnail.rbx, + } + conversationThumbnailFrame.Parent = self.rbx + + self.sentLabel = Create.new "TextLabel" { + Name = "SentLabel", + BackgroundTransparency = 1, + + AnchorPoint = Vector2.new(0.5, 0.5), + TextSize = Constants.Font.FONT_SIZE_16, + Size = UDim2.new(1, 0, 0, 20), + Position = UDim2.new(0.5, 0, 0.5, 0), + + TextColor3 = Constants.Color.GRAY2, + Font = Enum.Font.SourceSans, + TextXAlignment = Enum.TextXAlignment.Right, + Text = self.appState.localization:Format("Feature.Chat.Label.Sent"), + Visible = false + } + + local sendButtonFrame = Create.new"Frame" { + Name = "SendButtonFrame", + BackgroundTransparency = 1, + Size = UDim2.new(0, sendTextWidth + 14, 0, 32), + Position = UDim2.new(1, -((sendTextWidth + 14) /2) - 12, .5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + + Create.new"ImageLabel"{ + Name = "SendImageLabel", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + ScaleType = "Slice", + SliceCenter = Rect.new(3,3,4,4), + Image = "rbxasset://textures/ui/LuaChat/9-slice/btn-control-sm.png", + self.sendButton.rbx, + }, + self.sentLabel, + } + self.sendImageLabel = sendButtonFrame.SendImageLabel + + sendButtonFrame.Parent = self.rbx + local sendButtonConnection = self.sendButton.Pressed:Connect(function() + self.Tapped:Fire() + self.sendImageLabel.Visible = false + self.sentLabel.Visible = true + end) + table.insert(self.connections, sendButtonConnection) + + self.conversationTitle = Create.new"TextLabel" { + Name = "ConversationTitle", + BackgroundTransparency = 1, + Size = UDim2.new(1, -ICON_CELL_WIDTH - sendButtonFrame.Size.X.Offset - 20, 0.75, 0), + Position = UDim2.new(0, ICON_CELL_WIDTH, 0, 0), + TextSize = Constants.Font.FONT_SIZE_18, + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSans, + Text = getConversationDisplayTitle(conversation), + TextTruncate = UseCppTextTruncation and Enum.TextTruncate.AtEnd or nil, + TextXAlignment = Enum.TextXAlignment.Left, + } + + self.conversationTitle.Parent = self.rbx + + local convoTitleChanged = self.conversationTitle:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + self.conversationTitle.Text = getConversationDisplayTitle(conversation) + if not UseCppTextTruncation then + Text.TruncateTextLabel(self.conversationTitle, "...") + end + end) + table.insert(self.connections, convoTitleChanged) + + + local divider = Create.new"Frame" { + Name = "Divider", + BackgroundColor3 = Constants.Color.GRAY4, + BorderSizePixel = 0, + Size = UDim2.new(1, -ICON_CELL_WIDTH, 0, 1), + Position = UDim2.new(0, ICON_CELL_WIDTH, 1, -1), + } + divider.Parent = self.rbx + + return self +end + +function GameShareCard:Update(conversation) + if not conversation.lastUpdated then + return + end + if conversation.lastUpdated:GetUnixTimestamp() < self.startTime:GetUnixTimestamp() then + self.conversation = conversation + end +end + +function GameShareCard:Destruct() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + + if self.conversationThumbnail then + self.conversationThumbnail:Destruct() + end + if self.presenceSubLabel then + self.presenceSubLabel:Destruct() + end + self.rbx:Destroy() +end + +return GameShareCard \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/GameShareComponent.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GameShareComponent.lua new file mode 100644 index 0000000..a95fc60 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GameShareComponent.lua @@ -0,0 +1,403 @@ +local CoreGui = game:GetService("CoreGui") +local GuiService = game:GetService("GuiService") +local PlayerService = game:GetService("Players") + +local Modules = CoreGui.RobloxGui.Modules + +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local Create = require(LuaChat.Create) +local Constants = require(LuaChat.Constants) +local DialogInfo = require(LuaChat.DialogInfo) +local ConversationActions = require(LuaChat.Actions.ConversationActions) +local GetMultiplePlaceInfos = require(LuaChat.Actions.GetMultiplePlaceInfos) +local HeaderLoader = require(LuaChat.Components.HeaderLoader) +local PlaceInfoCard = require(LuaChat.Components.PlaceInfoCard) +local GameShareCard = require(LuaChat.Components.GameShareCard) +local ConversationList = require(LuaChat.Components.ConversationList) +local getInputEvent = require(LuaChat.Utils.getInputEvent) +local truncateAssetLink = require(LuaChat.Utils.truncateAssetLink) +local FlagSettings = require(LuaApp.FlagSettings) + +local HttpStatus = require(LuaChat.WebApi).Status + +local ToastModel = require(LuaChat.Models.ToastModel) +local ShowToast = require(LuaChat.Actions.ShowToast) + +local NavigateBack = require(Modules.LuaApp.Thunks.NavigateBack) +local RemoveRoute = require(LuaChat.Actions.RemoveRoute) +local SetAppLoaded = require(LuaChat.Actions.SetAppLoaded) + +local NotificationType = require(LuaApp.Enum.NotificationType) + +local Intent = DialogInfo.Intent + +local FFlagShareGameToChatStatusAnalytics = settings():GetFFlag("ShareGameToChatStatusAnalytics") + +local GameShareComponent = {} +GameShareComponent.__index = GameShareComponent + +local ICON_CELL_WIDTH = 60 +local SEARCH_BOX_HEIGHT = 48 +local CLEAR_TEXT_WIDTH = 44 +local PLACE_INFO_FRAME_HEIGHT = 84 + +local function requestOlderConversations(appState) + -- Don't fetch older conversations if the oldest conversation has already been fetched. + if appState.store:getState().ChatAppReducer.ConversationsAsync.oldestConversationIsFetched then + return + end + + -- Ask for new conversations + local convoCount = 0 + for _, _ in pairs(appState.store:getState().ChatAppReducer.Conversations) do + convoCount = convoCount + 1 + end + local pageSize = Constants.PageSize.GET_CONVERSATIONS + local currentPage = math.floor(convoCount / pageSize) + spawn(function() + appState.store:dispatch(ConversationActions.GetLocalUserConversationsAsync(currentPage + 1, pageSize)) + end) +end + +function GameShareComponent.new(appState, placeId, innerFrame) + local self = {} + self._analytics = appState.analytics + self.appState = appState + self.placeId = placeId + self.placeInfo = appState.store:getState().ChatAppReducer.PlaceInfos[placeId] + setmetatable(self, GameShareComponent) + + -- Header + self.header = HeaderLoader.GetHeader(appState, Intent.GameShare) + self.header:SetDefaultSubtitle() + self.header:SetTitle(appState.localization:Format("Feature.Chat.Heading.ShareGameToChat")) + self.header:SetBackButtonEnabled(true) + + -- Place Info Card Frame + self.placeInfoCardFrame = Create.new"Frame" { + Name = "PlaceInfoCardFrame", + BackgroundColor3 = Constants.Color.GRAY5, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, PLACE_INFO_FRAME_HEIGHT), + LayoutOrder = 2, + } + + -- Search Container + self.searchContainer = Create.new"Frame" { + Name = "SearchContainer", + BackgroundTransparency = 0, + BackgroundColor3 = Constants.Color.WHITE, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, SEARCH_BOX_HEIGHT), + Visible = false, + LayoutOrder = 3, + + Create.new"ImageLabel" { + Name = "SearchIcon", + BackgroundTransparency = 1, + Size = UDim2.new(0, 24, 0, 24), + Position = UDim2.new(0, ICON_CELL_WIDTH/2, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + ImageColor3 = Constants.Color.GRAY3, + Image = "rbxasset://textures/ui/LuaChat/icons/ic-search.png", + }, + Create.new"TextBox" { + Name = "Search", + BackgroundTransparency = 1, + Size = UDim2.new(1, -CLEAR_TEXT_WIDTH-ICON_CELL_WIDTH, 1, 0), + Position = UDim2.new(0, ICON_CELL_WIDTH, 0, 0), + TextSize = Constants.Font.FONT_SIZE_18, + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSans, + Text = "", + PlaceholderText = appState.localization:Format("Feature.Chat.Label.SearchForFriendsAndChat"), + PlaceholderColor3 = Constants.Color.GRAY3, + TextXAlignment = Enum.TextXAlignment.Left, + OverlayNativeInput = true, + ClearTextOnFocus = false, + ClipsDescendants = true, + }, + Create.new"ImageButton" { + Name = "Clear", + BackgroundTransparency = 1, + Size = UDim2.new(0, 16, 0, 16), + Position = UDim2.new(1, -(CLEAR_TEXT_WIDTH/2), 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + AutoButtonColor = false, + Image = "rbxasset://textures/ui/LuaChat/icons/ic-clear-solid.png", + ImageTransparency = 1, + }, + } + self.clearSearchButton = self.searchContainer.Clear + self.searchBox = self.searchContainer.Search + self.SearchFilterPredicate = function(other) + if self.searchBox.Text == "" then + return true + end + return string.find(string.lower(other), string.lower(self.searchBox.Text), 1, true) ~= nil + end + + local divider = Create.new"Frame" { + Name = "Divider", + BackgroundColor3 = Constants.Color.GRAY4, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 1), + LayoutOrder = 4, + } + + -- Conversation List Frame + self.conversationListFrame = Create.new "Frame" { + Name = "ConversationListFrame", + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = 5, + Size = UDim2.new(1, 0, 1, - self.header.rbx.Size.Y.Offset - SEARCH_BOX_HEIGHT - PLACE_INFO_FRAME_HEIGHT), + } + + self.placeInfoCardFrame.Parent = innerFrame + self.searchContainer.Parent = innerFrame + divider.Parent = innerFrame + self.conversationListFrame.Parent = innerFrame + + self.rbx = Create.new"ImageButton" { + Active = true, + AutoButtonColor = false, + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Constants.Color.GRAY5, + BorderSizePixel = 0, + + Create.new("UIListLayout") { + Name = "ListLayout", + SortOrder = "LayoutOrder", + HorizontalAlignment = "Center", + }, + + self.header.rbx, + innerFrame, + } + + if not appState.store:getState().ChatAppReducer.AppLoaded then + spawn(function() + appState.store:dispatch( + ConversationActions.GetLocalUserConversationsAsync(1, Constants.PageSize.GET_CONVERSATIONS) + ):andThen(function() + appState.store:dispatch(SetAppLoaded(true)) + end) + end) + end + + return self +end + +function GameShareComponent:Start() + local isLuaAppStarterScriptEnabled = FlagSettings:IsLuaAppStarterScriptEnabled() + + self.connections = {} + + -- back button + local backButtonConnection + if isLuaAppStarterScriptEnabled then + backButtonConnection = self.header.BackButtonPressed:Connect(function() + self.appState.store:dispatch(NavigateBack()) + GuiService:BroadcastNotification("", NotificationType.CLOSE_MODAL) + end) + else + backButtonConnection = self.header.BackButtonPressed:Connect(function() + self.appState.store:dispatch(RemoveRoute(DialogInfo.Intent.GameShare)) + GuiService:BroadcastNotification("", NotificationType.CLOSE_MODAL) + end) + end + table.insert(self.connections, backButtonConnection) + + -- update + local appStateConnection = self.appState.store.Changed:Connect(function(state, oldState) + self:Update(state, oldState) + end) + table.insert(self.connections, appStateConnection) + + -- search clear button + local clearButtonConnection = getInputEvent(self.clearSearchButton):Connect(function() + self.searchBox.Text = "" + end) + table.insert(self.connections, clearButtonConnection) + + local function updateClearButtonVisibility() + -- If we were to set the visible property of the clear button on the textbox focus lost event + -- it would disable the clear button, which in turn would stop the click event + -- from being able to notify the button + local visible = self.searchBox:IsFocused() and (self.searchBox.Text ~= "") + self.clearSearchButton.ImageTransparency = visible and 0 or 1 + end + + -- search box + local searchChangedConnection = self.searchBox:GetPropertyChangedSignal("Text"):Connect(function() + if self.list then + updateClearButtonVisibility() + self.list:SetFilterPredicate(self.SearchFilterPredicate) + self:getOlderConversationsForSearchIfNecessary() + end + end) + table.insert(self.connections, searchChangedConnection) + + local focusedConnection = self.searchBox.Focused:Connect(updateClearButtonVisibility) + table.insert(self.connections, focusedConnection) + local focusLostConnection = self.searchBox.FocusLost:Connect(updateClearButtonVisibility) + table.insert(self.connections, focusLostConnection) + + if not self.placeInfo then + self.appState.store:dispatch(GetMultiplePlaceInfos({self.placeId})) + end + + if self.appState.store:getState().ChatAppReducer.AppLoaded and self.placeInfo then + self:FillContent() + end +end + +function GameShareComponent:Update(newState, oldState) + if (not oldState.ChatAppReducer.AppLoaded) and newState.ChatAppReducer.AppLoaded and self.placeInfo then + self:FillContent() + end + + if (not self.placeInfo) and (newState.ChatAppReducer.PlaceInfos[self.placeId]) then + self.placeInfo = newState.ChatAppReducer.PlaceInfos[self.placeId] + if newState.ChatAppReducer.AppLoaded then + self:FillContent() + end + end + + local newPageConversationsIsFetching = newState.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching + local oldPageConversationsIsFetching = oldState.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching + if newPageConversationsIsFetching ~= oldPageConversationsIsFetching and self.list then + self.list:Update(newState, oldState) + self:getOlderConversationsForSearchIfNecessary() + end + + if newState.ChatAppReducer.Conversations ~= oldState.ChatAppReducer.Conversations and self.list then + self.list:Update(newState, oldState) + end +end + +function GameShareComponent:FillContent() + self.searchContainer.Visible = true + self:FillPlaceInfo() + self:FillConversations() +end + +function GameShareComponent:FillPlaceInfo() + if self.placeInfoCard then + return + end + + self.placeInfoCard = PlaceInfoCard.new(self.appState, self.placeInfo) + self.placeInfoCard.rbx.Parent = self.placeInfoCardFrame +end + +function GameShareComponent:FillConversations() + local conversations = self.appState.store:getState().ChatAppReducer.Conversations + local list = ConversationList.new(self.appState, conversations, GameShareCard) + list:SetSortWithConversationEntry(true) + self.list = list + list.rbx.Size = UDim2.new(1, 0, 1, 0) + list.rbx.Parent = self.conversationListFrame + + local tappedConnection = list.ConversationTapped:Connect(function(convoId) + local messageSentLocalTime = tick() + + if FFlagShareGameToChatStatusAnalytics then + self.appState.store:dispatch( + ConversationActions.SendMessage( + convoId, + truncateAssetLink(self.placeInfo.url), + messageSentLocalTime + )):andThen(function(webStatus) + self:ReportSendButtonTappedEvent(convoId, webStatus) + + if webStatus == HttpStatus.MODERATED then + local toastModel = ToastModel.new(Constants.ToastIDs.MESSAGE_WAS_MODERATED, "Feature.Chat.Message.GameLinkWasModerated") + self.appState.store:dispatch(ShowToast(toastModel)) + end + end) + else + self:ReportSendButtonTappedEvent(convoId) + self.appState.store:dispatch( + ConversationActions.SendMessage( + convoId, + truncateAssetLink(self.placeInfo.url), + "Feature.Chat.Message.GameLinkWasModerated", + messageSentLocalTime + ) + ) + end + end) + table.insert(self.connections, tappedConnection) + + local requestOlderConversationConnection = list.RequestOlderConversations:Connect(function() + requestOlderConversations(self.appState) + end) + table.insert(self.connections, requestOlderConversationConnection) +end + +function GameShareComponent:getOlderConversationsForSearchIfNecessary(appState) + -- To Check: + -- 1) Search is open + -- 2) Not have loaded all conversations. + -- 3) Not Ccrrently getting older conversations + -- 4) Having enouth search items to show + -- Note that we already try to load more conversations if we scroll down to the bottom of the list + local state = self.appState.store:getState() + local isSearchOpen = (self.searchBox.Text) ~= nil and (self.searchBox.Text ~= "") + if (not isSearchOpen) or state.ChatAppReducer.ConversationsAsync.oldestConversationIsFetched + or state.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching then + return + end + + if self.list.rbx.CanvasSize.Y.Offset > self.list.rbx.AbsoluteSize.Y then + return + end + + requestOlderConversations(self.appState) +end + +function GameShareComponent:ReportSendButtonTappedEvent(convoId, httpStatus) + local eventName = "clickSendBtnFromGameShareCard" + local eventContext = "touch" + + local player = PlayerService.LocalPlayer + local userId = "UNKNOWN" + if player then + userId = tostring(player.UserId) + end + + local additionalArgs = { + uid = userId, + placeid = self.placeId, + cid = convoId, + httpStatus = httpStatus, + } + + self._analytics.EventStream:setRBXEvent(eventContext, eventName, additionalArgs) +end + +function GameShareComponent:Stop() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + self.connections = {} +end + +function GameShareComponent:Destruct() + if self.list then + self.list:Destruct() + end + if self.header then + self.header:Destroy() + end + if self.placeInfoCard then + self.placeInfoCard:Destruct() + end + self.rbx:Destroy() +end + +return GameShareComponent \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetail.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetail.lua new file mode 100644 index 0000000..51581d9 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetail.lua @@ -0,0 +1,325 @@ +local Players = game:GetService("Players") +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Signal = require(Common.Signal) +local Create = require(LuaChat.Create) +local Constants = require(LuaChat.Constants) +local Functional = require(Common.Functional) +local DialogInfo = require(LuaChat.DialogInfo) + +local Components = LuaChat.Components +local HeaderLoader = require(Components.HeaderLoader) +local SectionComponent = require(Components.ListSection) +local ActionEntryComponent = require(Components.ActionEntry) +local UserListComponent = require(Components.UserList) +local ListEntryComponent = require(Components.ListEntry) +local ResponseIndicator = require(Components.ResponseIndicator) +local GenericDialogType = require(Components.GroupDetailDialogs.GenericDialogType) + +local getConversationDisplayTitle = require(LuaChat.Utils.getConversationDisplayTitle) + +local ConversationModel = require(LuaChat.Models.Conversation) + +local SetRoute = require(LuaChat.Actions.SetRoute) + +local Intent = DialogInfo.Intent + +local PARTICIPANT_VIEW = 1 +local PARTICIPANT_REPORT = 2 +local PARTICIPANT_REMOVE = 3 + +local function getAsset(name) + return "rbxasset://textures/ui/LuaChat/"..name..".png" +end + +local SeeMoreButton = {} +SeeMoreButton.__index = SeeMoreButton + +function SeeMoreButton.new(appState) + local self = {} + setmetatable(self, SeeMoreButton) + + local listEntry = ListEntryComponent.new(appState, 40) + + self.rbx = listEntry.rbx + self.tapped = listEntry.tapped + + local label = Create.new"TextLabel" { + Name = "Label", + BackgroundTransparency = 1, + Size = UDim2.new(1, -12, 1, 0), + Position = UDim2.new(0, 12, 0, 0), + TextSize = Constants.Font.FONT_SIZE_16, + TextColor3 = Constants.Color.BLUE_PRIMARY, + Font = Enum.Font.SourceSans, + TextXAlignment = Enum.TextXAlignment.Left, + Text = appState.localization:Format("Feature.Chat.Action.SeeMoreFriends"), + } + label.Parent = self.rbx + + local divider = Create.new"Frame" { + Name = "Divider", + BackgroundColor3 = Constants.Color.GRAY4, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 1), + Position = UDim2.new(0, 0, 1, -1), + } + divider.Parent = self.rbx + + return self +end + +function SeeMoreButton:Update(text) + self.rbx.Label.Text = text +end + +local GroupDetail = {} +GroupDetail.__index = GroupDetail + +function GroupDetail.new(appState, convoId) + local self = {} + self.connections = {} + setmetatable(self, GroupDetail) + + self.appState = appState + self.conversationId = convoId + self.AddFriendsPressed = Signal.new() + + self.oldState = nil + self.header = HeaderLoader.GetHeader(appState, Intent.GroupDetail) + self.header:SetTitle(appState.localization:Format("Feature.Chat.Label.ChatDetails")) + self.header:SetDefaultSubtitle() + self.header:SetBackButtonEnabled(true) + + self.rbx = Create.new"Frame" { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Constants.Color.GRAY5, + BorderSizePixel = 0, + + self.header.rbx, + Create.new "ScrollingFrame" { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScrollBarThickness = 0, + Create.new"Frame" { + Name = "Content", + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Constants.Color.GRAY5, + BorderSizePixel = 0, + Create.new"UIListLayout" { + Name = "ListLayout", + SortOrder = Enum.SortOrder.LayoutOrder, + }, + }, + }, + } + local scrollingFrame = self.rbx.ScrollingFrame + local content = scrollingFrame.Content + + self.BackButtonPressed = self.header.BackButtonPressed + + scrollingFrame.Position = UDim2.new(0, 0, 0, self.header.rbx.Size.Y.Offset) + scrollingFrame.Size = UDim2.new(1, 0, 1, -self.header.rbx.Size.Y.Offset) + + self.general = SectionComponent.new(appState, "Feature.Chat.Label.General") + self.general.rbx.LayoutOrder = 1 + self.general.rbx.Parent = content + + self.groupName = ActionEntryComponent.new(appState, getAsset("icons/ic-nametag"), "Feature.Chat.Label.ChatGroupName") + self.groupName.rbx.LayoutOrder = 2 + self.groupName.rbx.Parent = content + local groupNameConnection = self.groupName.tapped.Event:Connect(function() + self.appState.store:dispatch(SetRoute(Intent.GenericDialog, { + dialog = GenericDialogType.EditChatGroupNameDialog, + dialogParameters = { + titleLocalizationKey = "Feature.Chat.Label.ChatGroupName", + maxChar = 150, + conversation = self.conversation, + } + } + )) + end) + table.insert(self.connections, groupNameConnection) + + self.responseIndicator = ResponseIndicator.new(appState) + self.responseIndicator:SetVisible(false) + self.responseIndicator.rbx.Parent = self.rbx + + local members = SectionComponent.new(appState, "Feature.Chat.Label.Members") + members.rbx.LayoutOrder = 5 + members.rbx.Parent = content + + self.addFriends = ActionEntryComponent.new(appState, getAsset("icons/ic-add-friends"), + "Feature.Chat.Label.AddFriends", 36) + self.addFriends.rbx.LayoutOrder = 6 + self.addFriends:SetDividerOffset(60) + self.addFriends.rbx.Parent = content + + self.AddFriendsPressed = self.addFriends.tapped.Event + + self.participantsList = UserListComponent.new(appState, getAsset("icons/ic-more")) + self.participantsList.rbx.LayoutOrder = 7 + self.participantsList.rbx.Parent = content + local userSelectedConnection = self.participantsList.userSelected:Connect(function(user) + if user.id ~= tostring(Players.LocalPlayer.UserId) then + self.appState.store:dispatch(SetRoute(Intent.GenericDialog, { + dialog = GenericDialogType.ParticipantDialog, + dialogParameters = { + titleKey = "Feature.Chat.Heading.Option", + options = { + [PARTICIPANT_VIEW] = "Feature.Chat.Label.ViewProfile", + [PARTICIPANT_REPORT] = "Feature.Chat.Action.ReportUser", + [PARTICIPANT_REMOVE] = "Feature.Chat.Action.RemoveFromGroup", + }, + conversationId = self.conversationId, + conversation = self.conversation, + userId = user.id + } + } + )) + end + end) + table.insert(self.connections, userSelectedConnection) + + self.seeMore = SeeMoreButton.new(appState) + self.seeMore.rbx.LayoutOrder = 9 + self.seeMore.rbx.Parent = content + self.showAllParticipants = false + local seeMoreConnection = self.seeMore.tapped:Connect(function() + if self.showAllParticipants then + self.showAllParticipants = false + self:Update(appState.store:getState()) + else + self.showAllParticipants = true + self:Update(appState.store:getState()) + end + end) + table.insert(self.connections, seeMoreConnection) + + self.blankSection = SectionComponent.new(appState) + self.blankSection.rbx.LayoutOrder = 10 + self.blankSection.rbx.Parent = content + + self.leaveGroup = ActionEntryComponent.new(appState, getAsset("icons/ic-leave"), "Feature.Chat.Heading.LeaveGroup") + self.leaveGroup.rbx.LayoutOrder = 11 + self.leaveGroup.rbx.Parent = content + local leaveGroupConnection = self.leaveGroup.tapped.Event:Connect(function() + self.appState.store:dispatch(SetRoute(Intent.GenericDialog, { + dialog = GenericDialogType.LeaveGroupDialog, + dialogParameters = { + titleKey = "Feature.Chat.Heading.LeaveGroup", + messageKey = "Feature.Chat.Message.LeaveGroup", + cancelTitleKey = "Feature.Chat.Action.Stay", + confirmationTitleKey = "Feature.Chat.Action.Leave", + conversation = self.conversation + } + } + )) + end) + table.insert(self.connections, leaveGroupConnection) + + content.ListLayout:ApplyLayout() + + self.conversation = ConversationModel.empty() + + self:Update(appState.store:getState()) + + return self +end + +function GroupDetail:Update(state) + local conversationId = state.ChatAppReducer.Location.current.parameters.conversationId + local conversation = state.ChatAppReducer.Conversations[conversationId] + self.header:SetConnectionState(state.ConnectionState) + if conversation ~= nil then --if conversation ~= self.conversation then + if conversation.id ~= self.conversation.id then + self.showAllParticipants = false + end + + if conversation.isUserLeaving ~= self.conversation.isUserLeaving then + self.responseIndicator:SetVisible(conversation.isUserLeaving) + end + + if conversation.isDefaultTitle then + local notSetLocalized = self.appState.localization:Format("Feature.Chat.Label.NotSet") + self.groupName:Update(notSetLocalized) + else + self.groupName:Update(getConversationDisplayTitle(conversation)) + end + + local count = 0 + local users = Functional.Map(conversation.participants, function(userId) + count = count + 1 + return (count <= 3 or self.showAllParticipants) and state.Users[userId] or nil + end) + + if count > 3 and not self.showAllParticipants then + local messageArguments = { + NUMBER_OF_FRIENDS = tostring(count-3) + } + local message = self.appState.localization:Format("Feature.Chat.Action.SeeMoreFriends", messageArguments) + self.seeMore:Update(message) + self.seeMore.rbx.Visible = true + elseif count > 3 then + self.seeMore:Update(self.appState.localization:Format("Feature.Chat.Label.SeeLess")) + self.seeMore.rbx.Visible = true + else + self.seeMore.rbx.Visible = false + end + + self.participantsList:Update(users) + + if conversation.conversationType == ConversationModel.Type.MULTI_USER_CONVERSATION then + self.general.rbx.Visible = true + self.groupName.rbx.Visible = true + self.leaveGroup.rbx.Visible = true + self.blankSection.rbx.Visible = true + elseif conversation.conversationType == ConversationModel.Type.ONE_TO_ONE_CONVERSATION then + self.general.rbx.Visible = false + self.groupName.rbx.Visible = false + self.leaveGroup.rbx.Visible = false + self.blankSection.rbx.Visible = false + end + + self.conversation = conversation + end + if self.oldState == nil or state.ChatAppReducer.Location.current ~= self.oldState.ChatAppReducer.Location.current then + if self.oldState ~= nil and state.ChatAppReducer.Location.current.intent == Intent.GroupDetail then + -- If any Dialog is mounted on ModalBase, close them. + if self.oldState.ChatAppReducer.Location.current.intent == Intent.GenericDialog then + self.oldState.ChatAppReducer.Location.current.parameters.dialog:Close() + end + end + end + local highestYValue = 0 + for _, element in pairs(self.rbx.ScrollingFrame.Content:GetChildren()) do + if element:IsA("GuiObject") then + highestYValue = highestYValue + element.Size.Y.Offset + end + end + self.rbx.ScrollingFrame.CanvasSize = UDim2.new(1, 0, 0, highestYValue) + self.rbx.ScrollingFrame.Content.Size = UDim2.new(1, 0, 0, highestYValue) + + self.oldState = state +end + +function GroupDetail:Stop() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + + self.connections = {} +end + +function GroupDetail:Destruct() + self.responseIndicator:Destruct() + + self.rbx:Destroy() +end + +return GroupDetail diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetailDialogs/EditChatGroupNameDialog.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetailDialogs/EditChatGroupNameDialog.lua new file mode 100644 index 0000000..e5ae48b --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetailDialogs/EditChatGroupNameDialog.lua @@ -0,0 +1,53 @@ +local Modules = script.Parent.Parent.Parent +local Components = Modules.Components + +local DialogComponents = require(Components.DialogComponents) +local ConversationActions = require(Modules.Actions.ConversationActions) +local ResponseIndicator = require(Components.ResponseIndicator) + +local EditChatGroupNameDialog = {} + +function EditChatGroupNameDialog.new(appState, titleLocalizationKey, maxChar, conversation) + local self = {} + setmetatable(self, {__index = EditChatGroupNameDialog}) + + self.dialog = DialogComponents.TextInputDialog.new(appState, titleLocalizationKey, maxChar) + + -- Setup ResponseIndicator + self.responseIndicator = ResponseIndicator.new(appState) + self.responseIndicator:SetVisible(false) + self.responseIndicator.rbx.Parent = self.dialog.rbx + + self.conversation = conversation + + self:UpdateGroupName() + + -- Define saved action + self.dialog.saved:Connect(function(newName) + self.responseIndicator:SetVisible(true) + local callback = function() + self.responseIndicator:SetVisible(false) + end + local action = ConversationActions.RenameGroupConversation(self.conversation.id, newName, callback) + appState.store:dispatch(action) + end) + + return self +end + +function EditChatGroupNameDialog:UpdateGroupName() + local groupName = "" + + if not self.conversation.isDefaultTitle then + groupName = self.conversation.title + end + + self.dialog:Update(groupName) +end + +function EditChatGroupNameDialog:Destruct() + self.responseIndicator:Destruct() + self.dialog:Destruct() +end + +return EditChatGroupNameDialog \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetailDialogs/GenericDialogType.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetailDialogs/GenericDialogType.lua new file mode 100644 index 0000000..781a2b1 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetailDialogs/GenericDialogType.lua @@ -0,0 +1,8 @@ +local GenericDialogType = { + EditChatGroupNameDialog = "EditChatGroupNameDialog", + LeaveGroupDialog = "LeaveGroupDialog", + ParticipantDialog = "ParticipantDialog", + RemoveUserDialog = "RemoveUserDialog", +} + +return GenericDialogType \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetailDialogs/LeaveGroupDialog.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetailDialogs/LeaveGroupDialog.lua new file mode 100644 index 0000000..0daf619 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetailDialogs/LeaveGroupDialog.lua @@ -0,0 +1,33 @@ +local Players = game:GetService("Players") + +local Modules = script.Parent.Parent.Parent +local Components = Modules.Components + +local DialogComponents = require(Components.DialogComponents) +local ConversationActions = require(Modules.Actions.ConversationActions) + +local LeaveGroupDialog = {} + +function LeaveGroupDialog.new(appState, titleKey, messageKey, cancelTitleKey, confirmTitleKey, conversation) + local self = {} + setmetatable(self, {__index = LeaveGroupDialog}) + + self.dialog = DialogComponents.ConfirmationDialog.new(appState, titleKey, messageKey, cancelTitleKey, confirmTitleKey) + + self.conversation = conversation + + self.dialog.saved:Connect(function() + local userId = tostring(Players.LocalPlayer.UserId) + local convoId = self.conversation.id + local action = ConversationActions.RemoveUserFromConversation(userId, convoId) + appState.store:dispatch(action) + end) + + return self +end + +function LeaveGroupDialog:Destruct() + self.dialog:Destruct() +end + +return LeaveGroupDialog \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetailDialogs/ParticipantDialog.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetailDialogs/ParticipantDialog.lua new file mode 100644 index 0000000..8678f70 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetailDialogs/ParticipantDialog.lua @@ -0,0 +1,93 @@ +local Players = game:GetService("Players") +local GuiService = game:GetService("GuiService") +local CoreGui = game:GetService("CoreGui") + +local LuaApp = CoreGui.RobloxGui.Modules.LuaApp +local LuaChat = CoreGui.RobloxGui.Modules.LuaChat + +local GenericDialogType = require(LuaChat.Components.GroupDetailDialogs.GenericDialogType) +local DialogComponents = require(LuaChat.Components.DialogComponents) +local WebApi = require(LuaChat.WebApi) +local ConversationModel = require(LuaChat.Models.Conversation) +local SetRoute = require(LuaChat.Actions.SetRoute) +local DialogInfo = require(LuaChat.DialogInfo) + +local NotificationType = require(LuaApp.Enum.NotificationType) + +local Intent = DialogInfo.Intent + +local PARTICIPANT_VIEW = 1 +local PARTICIPANT_REPORT = 2 +local PARTICIPANT_REMOVE = 3 + +local ParticipantDialog = {} + +function ParticipantDialog.new(appState, titleKey, options, conversationId, conversation, userId) + local self = {} + setmetatable(self, {__index = ParticipantDialog}) + + self.appState = appState + self.dialog = DialogComponents.OptionDialog.new(appState, titleKey, options, userId) + + self.conversationId = conversationId + self.conversation = conversation + + if conversation ~= nil then + if conversation.initiator == tostring(Players.LocalPlayer.UserId) + and conversation.conversationType == ConversationModel.Type.MULTI_USER_CONVERSATION then + self.dialog.optionGuis[PARTICIPANT_REMOVE].Visible = true + else + self.dialog.optionGuis[PARTICIPANT_REMOVE].Visible = false + end + self.dialog:Resize() + end + + self.dialog.selected:Connect(function(optionId, userId) + local user = self.appState.store:getState().Users[userId] + if user == nil then + return + end + + if optionId == PARTICIPANT_VIEW then + if user and user.id and (type(user.id) == 'string' or type(user.id) == 'number') then + GuiService:BroadcastNotification(WebApi.MakeUserProfileUrl(user.id), + NotificationType.VIEW_PROFILE) + else + print("Bad input to RequestNativeView, show error prompt here") + end + elseif optionId == PARTICIPANT_REPORT then + if user and user.id and (type(user.id) == 'string' or type(user.id) == 'number') then + GuiService:BroadcastNotification(WebApi.MakeReportUserUrl(user.id, conversationId), + NotificationType.REPORT_ABUSE) + else + print("Bad input to RequestNativeView, show error prompt here") + end + elseif optionId == PARTICIPANT_REMOVE then + local messageArguments = { + USERNAME = user.name, + } + self.appState.store:dispatch(SetRoute(Intent.GenericDialog, { + dialog = GenericDialogType.RemoveUserDialog, + dialogParameters = { + titleKey = "Feature.Chat.Action.RemoveUser", + messageKey = "Feature.Chat.Message.RemoveUser", + cancelTitleKey = "Feature.Chat.Action.Cancel", + confirmationTitleKey = "Feature.Chat.Action.Remove", + conversation = self.conversation, + user = user, + messageArguments = messageArguments + } + } + )) + + end + end) + + return self +end + +function ParticipantDialog:Destruct() + self.dialog:Destruct() +end + +return ParticipantDialog \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetailDialogs/RemoveUserDialog.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetailDialogs/RemoveUserDialog.lua new file mode 100644 index 0000000..0a98b98 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/GroupDetailDialogs/RemoveUserDialog.lua @@ -0,0 +1,45 @@ +local Modules = script.Parent.Parent.Parent +local Components = Modules.Components + +local DialogComponents = require(Components.DialogComponents) +local ConversationActions = require(Modules.Actions.ConversationActions) +local ResponseIndicator = require(Components.ResponseIndicator) + +local RemoveUserDialog = {} + +function RemoveUserDialog.new(appState, titleKey, messageKey, cancelTitleKey, confirmTitleKey, conversation) + local self = {} + setmetatable(self, {__index = RemoveUserDialog}) + + self.appState = appState + self.dialog = DialogComponents.ConfirmationDialog.new(appState, titleKey, messageKey, cancelTitleKey, confirmTitleKey) + + -- Setup ResponseIndicator + self.responseIndicator = ResponseIndicator.new(appState) + self.responseIndicator:SetVisible(false) + self.responseIndicator.rbx.Parent = self.dialog.rbx + + self.conversation = conversation + + self.dialog.saved:Connect(function(user) + local userId = user.id + local convoId = self.conversation.id + + self.responseIndicator.rbx.Parent = self.rbx + self.responseIndicator:SetVisible(true) + + local action = ConversationActions.RemoveUserFromConversation(userId, convoId, function() + self.responseIndicator:SetVisible(false) + end) + self.appState.store:dispatch(action) + end) + + return self +end + +function RemoveUserDialog:Destruct() + self.responseIndicator:Destruct() + self.dialog:Destruct() +end + +return RemoveUserDialog \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/Header.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/Header.lua new file mode 100644 index 0000000..03b7567 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/Header.lua @@ -0,0 +1,325 @@ +local CoreGui = game:GetService("CoreGui") +local UserInputService = game:GetService("UserInputService") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat +local LuaApp = Modules.LuaApp + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local FlagSettings = require(LuaChat.FlagSettings) +local LuaAppFlagSettings = require(LuaApp.FlagSettings) +local Signal = require(Common.Signal) + +local Components = LuaChat.Components +local BaseHeader = require(Components.BaseHeader) +local TextButton = require(Components.TextButton) + +local UseCppTextTruncation = FlagSettings.UseCppTextTruncation() +local GroupChatIconEnabled = settings():GetFFlag("LuaChatGroupChatIconEnabled") + +local HEIGHT_OF_DISCONNECTED = 32 + +local PLATFORM_SPECIFIC_CONSTANTS = { + [Enum.Platform.Android] = { + HEADER_CONTENT_FRAME_Y_OFFSET = 0, + HEADER_TITLE_FRAME_POSITION_NO_BACK_BUTTON = UDim2.new(0, 15, 0, 0), + HEADER_TITLE_FRAME_POSITION = UDim2.new(0, 72, 0, 0), + HEADER_TITLE_FRAME_ANCHOR_POINT = Vector2.new(0, 0), + HEADER_VERTICAL_ALIGNMENT = Enum.VerticalAlignment.Center, + HEADER_TEXT_X_ALIGNMENT = 0, + }, + Default = { + HEADER_CONTENT_FRAME_Y_OFFSET = 24, + HEADER_TITLE_FRAME_POSITION_NO_BACK_BUTTON = UDim2.new(0.5, 0, 0, 0), + HEADER_TITLE_FRAME_POSITION = UDim2.new(0.5, 0, 0, 0), + HEADER_TITLE_FRAME_ANCHOR_POINT = Vector2.new(0.5, 0), + HEADER_VERTICAL_ALIGNMENT = Enum.VerticalAlignment.Top, + HEADER_TEXT_X_ALIGNMENT = 2, + }, +} + +local GROUP_CHAT_ICON_HEIGHT = 25 +local GROUP_CHAT_ICON_WIDTH = 25 +local GROUP_CHAT_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-group-16x16.png" +local TITLE_LABEL_HEIGHT = 25 +local TITLE_LABEL_WIDTH = 200 +local SUBTITLE_LABEL_HEIGHT = 12 +local SUBTITLE_LABEL_WIDTH = 200 + +local function getPlatformSpecific(platform) + return PLATFORM_SPECIFIC_CONSTANTS[platform] or PLATFORM_SPECIFIC_CONSTANTS.Default +end + +local Header = BaseHeader:Template() +Header.__index = Header + +function Header.new(appState, dialogType) + local self = {} + setmetatable(self, Header) + + local platform = appState.store:getState().Platform + + local isLuaAppStarterScriptEnabled = LuaAppFlagSettings:IsLuaAppStarterScriptEnabled() + + self:SetPlatform(platform) + local platformConstants = getPlatformSpecific(platform) + + self.heightOfHeader = UserInputService.NavBarSize.Y + UserInputService.StatusBarSize.Y + self.heightOfDisconnected = HEIGHT_OF_DISCONNECTED + + self.buttons = {} + self.connections = {} + self.appState = appState + self.dialogType = dialogType + self.backButton = BaseHeader:GetNewBackButton(dialogType) + self.backButton.rbx.Visible = false + self.title = "" + self.subtitle = nil + self.connectionState = Enum.ConnectionState.Connected + + self.luaChatPlayTogetherEnabled = FlagSettings.IsLuaChatPlayTogetherEnabled( + self.appState.store:getState().FormFactor) + + self.BackButtonPressed = Signal.new() + local backButtonConnection = self.backButton.Pressed:Connect(function() + self.BackButtonPressed:Fire() + end) + table.insert(self.connections, backButtonConnection) + + self.titleLabel = Create.new "TextLabel" { + Name = "Title", + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Font = Enum.Font.SourceSansBold, + LayoutOrder = 1, + Size = UDim2.new(0, TITLE_LABEL_WIDTH, 0, TITLE_LABEL_HEIGHT), + Text = self.title, + TextColor3 = Constants.Color.WHITE, + TextSize = Constants.Font.FONT_SIZE_20, + TextXAlignment = platformConstants.HEADER_TEXT_X_ALIGNMENT, + } + + if GroupChatIconEnabled then + self.groupChatIcon = Create.new "ImageLabel" { + Name = "GroupChatIcon", + Visible = false, + BackgroundTransparency = 1, + LayoutOrder = 0, + Size = UDim2.new(0, GROUP_CHAT_ICON_WIDTH, 0, GROUP_CHAT_ICON_HEIGHT), + AnchorPoint = Vector2.new(1, 0), + Image = GROUP_CHAT_ICON, + } + + self.innerTitleFrame = Create.new "Frame" { + Name = "InnerTitleFrame", + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + LayoutOrder = 0, + Size = UDim2.new(0, TITLE_LABEL_WIDTH + GROUP_CHAT_ICON_WIDTH, 0, GROUP_CHAT_ICON_HEIGHT), + + Create.new "UIListLayout" { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 5), + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }, + + self.groupChatIcon, + self.titleLabel, + } + end + + self.innerSubtitle = Create.new "TextLabel" { + Name = "Subtitle", + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Font = Enum.Font.SourceSans, + LayoutOrder = 2, + Size = UDim2.new(0, SUBTITLE_LABEL_WIDTH, 0, SUBTITLE_LABEL_HEIGHT), + Text = "", + TextColor3 = Constants.Color.WHITE, + TextSize = Constants.Font.FONT_SIZE_12, + TextXAlignment = platformConstants.HEADER_TEXT_X_ALIGNMENT, + } + + self.innerTitles = Create.new "Frame" { + Name = "Titles", + AnchorPoint = platformConstants.HEADER_TITLE_FRAME_ANCHOR_POINT, + BackgroundTransparency = 1, + Position = self:GetHeaderTitleFramePosition(), + Size = UDim2.new(0, TITLE_LABEL_WIDTH, 1, 0), + + Create.new "UIListLayout" { + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = isLuaAppStarterScriptEnabled and Enum.VerticalAlignment.Center + or platformConstants.HEADER_VERTICAL_ALIGNMENT, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }, + } + + if GroupChatIconEnabled then + self.innerTitleFrame.Parent = self.innerTitles + else + self.titleLabel.Parent = self.innerTitles + end + + if not isLuaAppStarterScriptEnabled then + self.innerSubtitle.Parent = self.innerTitles + end + + self.innerButtons = Create.new "Frame" { + Name = "Buttons", + AnchorPoint = Vector2.new(1, 0), + BackgroundTransparency = 1, + Position = UDim2.new(1, -5, 0, 0), + Size = UDim2.new(0, 100, 1, 0), + + Create.new "UIListLayout" { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = platformConstants.HEADER_VERTICAL_ALIGNMENT, + }, + } + + self.innerContent = Create.new "Frame" { + Name = "Content", + BackgroundTransparency = 1, + Position = UDim2.new(0, 0, 0, UserInputService.StatusBarSize.Y), + Size = UDim2.new(1, 0, 0, UserInputService.NavBarSize.Y), + + self.backButton.rbx, + self.innerTitles, + self.innerButtons, + } + + self.innerHeader = Create.new "Frame" { + Name = "Header", + BackgroundColor3 = Constants.Color.BLUE_PRESSED, + BorderSizePixel = 0, + LayoutOrder = 1, + Size = UDim2.new(1, 0, 0, self.heightOfHeader), + + self.innerContent, + } + + self.rbx = Create.new "Frame" { + Name = "HeaderFrame", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, self.heightOfHeader), + + Create.new "UIListLayout" { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Top, + }, + + self.innerHeader, + + Create.new "Frame" { + Name = "Disconnected", + AnchorPoint = Vector2.new(0, 1), + BackgroundColor3 = Constants.Color.GRAY3, + BorderSizePixel = 0, + ClipsDescendants = true, + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, 0), -- Note: Deliberately has zero vertical height, will be scaled when shown. + + Create.new "TextLabel" { + Name = "Title", + AnchorPoint = Vector2.new(0.5, 1), + BackgroundTransparency = 1, + Font = Enum.Font.SourceSans, + LayoutOrder = 0, + Position = UDim2.new(0.5, 0, 1, 0), + Size = UDim2.new(1, 0, 0, HEIGHT_OF_DISCONNECTED), + Text = appState.localization:Format("Feature.Chat.Message.NoConnectionMsg"), + TextColor3 = Constants.Color.WHITE, + TextSize = Constants.Font.FONT_SIZE_14, + }, + }, + + Create.new "Frame" { + Name = "GameDrawer", + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = false, + LayoutOrder = 3, + Size = UDim2.new(1, 0, 0, 0), -- Note: Deliberately zero height, will be scaled open. + Visible = false, + }, + } + + local parentChangedConnection = self.rbx:GetPropertyChangedSignal("Parent"):Connect(function() + if self.rbx and self.rbx.Parent then + if not UseCppTextTruncation then + game:GetService("RunService").Stepped:wait() -- TextBounds isn't recalculated when this fires so we wait + end + self:SetTitle(self.title) -- Again, this can be much cleaner once we have proper truncation support + end + end) + table.insert(self.connections, parentChangedConnection) + + local navBarSignal = UserInputService:GetPropertyChangedSignal("NavBarSize") + local navBarConnection = navBarSignal:Connect(function() + self:AdjustLayout() + end) + local statusBarSignal = UserInputService:GetPropertyChangedSignal("StatusBarSize") + local statusBarConnection = statusBarSignal:Connect(function() + self:AdjustLayout() + end) + self:AdjustLayout() + table.insert(self.connections, navBarConnection) + table.insert(self.connections, statusBarConnection) + + do + local connection = appState.store.Changed:Connect(function(state, oldState) + self:SetPlatform(state.Platform) + self:SetConnectionState(state.ConnectionState) + end) + table.insert(self.connections, connection) + end + + return self +end + +function Header:AdjustLayout() + self.heightOfHeader = UserInputService.NavBarSize.Y + UserInputService.StatusBarSize.Y + self.rbx.Size = UDim2.new(1, 0, 0, self.heightOfHeader) + self.innerHeader.Size = UDim2.new(1, 0, 0, self.heightOfHeader) + + self.innerContent.Position = UDim2.new(0, 0, 0, UserInputService.StatusBarSize.Y) + self.innerContent.Size = UDim2.new(1, 0, 0, UserInputService.NavBarSize.Y) +end + +function Header:CreateHeaderButton(name, textKey) + local saveGroup = TextButton.new(self.appState, name, textKey) + self:AddButton(saveGroup) + return saveGroup +end + +function Header:SetBackButtonEnabled(enabled) + self.backButton.rbx.Visible = enabled + self.innerTitles.Position = self:GetHeaderTitleFramePosition() +end + +function Header:GetHeaderTitleFramePosition() + if self.backButton and self.backButton.rbx and self.backButton.rbx.Visible then + return getPlatformSpecific(self.platform).HEADER_TITLE_FRAME_POSITION + end + + return getPlatformSpecific(self.platform).HEADER_TITLE_FRAME_POSITION_NO_BACK_BUTTON +end + +function Header:SetGroupChatIconVisibility(enabled) + if enabled then + self.groupChatIcon.Visible = true + else + self.groupChatIcon.Visible = false + end +end + +return Header diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/HeaderLoader.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/HeaderLoader.lua new file mode 100644 index 0000000..99d8e72 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/HeaderLoader.lua @@ -0,0 +1,26 @@ +local LuaChat = script.Parent.Parent +local Components = LuaChat.Components + +local DialogInfo = require(LuaChat.DialogInfo) + +local Header = require(Components.Header) +local ModalHeader = require(Components.ModalHeader) + +local HeaderLoader = {} + +function HeaderLoader.GetHeader(appState, intent) + if intent == nil then + warn("intent passed to HeaderLoader.GetHeader is null.") + return Header.new(appState, DialogInfo.DialogType.Centered) + end + + local dialogType = DialogInfo.GetTypeBasedOnIntent(appState.store:getState().FormFactor, intent) + + if dialogType == DialogInfo.DialogType.Modal then + return ModalHeader.new(appState, dialogType) + else + return Header.new(appState, dialogType) + end +end + +return HeaderLoader \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ListEntry.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ListEntry.lua new file mode 100644 index 0000000..b56c07f --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ListEntry.lua @@ -0,0 +1,79 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local Signal = require(Common.Signal) +local getInputEvent = require(LuaChat.Utils.getInputEvent) + +local ListEntry = {} + +ListEntry.__index = ListEntry + +function ListEntry.new(appState, height) + local self = {} + self.connections = {} + setmetatable(self, ListEntry) + + self.rbx = Create.new"TextButton" { + Name = "Entry", + BackgroundTransparency = 0, + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.WHITE, + Size = UDim2.new(1, 0, 0, height), + AutoButtonColor = false, + Text = "", + } + + self.tapped = Signal.new() + self.beginHover = Signal.new() + self.endHover = Signal.new() + + local beginningInput = nil + local function onInputBegan(input) + if input.UserInputType ~= Enum.UserInputType.Touch or + (input.UserInputState ~= Enum.UserInputState.Begin and input ~= beginningInput) then + return + end + beginningInput = input + self.rbx.BackgroundColor3 = Constants.Color.GRAY5 + self.beginHover:Fire() + return + end + + local function onInputEnded(input, processed) + if input.UserInputType ~= Enum.UserInputType.Touch then + return + end + beginningInput = nil + self.rbx.BackgroundColor3 = Constants.Color.WHITE + self.endHover:Fire() + return + end + + local inputBeganConnection = self.rbx.InputBegan:Connect(onInputBegan) + table.insert(self.connections, inputBeganConnection) + local inputEndedConnection = self.rbx.InputEnded:Connect(onInputEnded) + table.insert(self.connections, inputEndedConnection) + + local mouseClickedConnection = getInputEvent(self.rbx):Connect(function() + self.tapped:Fire() + end) + table.insert(self.connections, mouseClickedConnection) + + return self +end + +function ListEntry:Destruct() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + + self.rbx:Destroy() +end + +return ListEntry diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ListSection.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ListSection.lua new file mode 100644 index 0000000..423e937 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ListSection.lua @@ -0,0 +1,61 @@ +local function findAncestor(child, name) + local parent = child.Parent + while parent and parent.Name ~= name do + parent = parent.Parent + end + return parent +end + +local DIVIDER_SIZE_LARGE = 26 +local DIVIDER_SIZE_SMALL = 12 + +local Modules = findAncestor(script, "LuaChat") +local Create = require(Modules.Create) +local Constants = require(Modules.Constants) + +local ListSection = {} + +ListSection.__index = ListSection + +function ListSection.new(appState, labelKey, layoutOrder) + local self = {} + setmetatable(self, ListSection) + + local labelHeight = labelKey and DIVIDER_SIZE_LARGE or DIVIDER_SIZE_SMALL + + self.rbx = Create.new"Frame" { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, labelHeight), + LayoutOrder = layoutOrder, + } + + if labelKey then + local labelText = appState.localization:Format(labelKey) + local label = Create.new"TextLabel" { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 12, 1, 0), + BorderSizePixel = 0, + Text = labelText, + TextColor3 = Constants.Color.GRAY2, + TextSize = Constants.Font.FONT_SIZE_14, + Font = Enum.Font.SourceSans, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + AnchorPoint = Vector2.new(0,1), + } + label.Parent = self.rbx + end + + local divider = Create.new"Frame" { + Name = "Divider", + BackgroundColor3 = Constants.Color.GRAY4, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 1), + Position = UDim2.new(0, 0, 1, -1), + } + divider.Parent = self.rbx + return self +end + +return ListSection diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/LoadingIndicator.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/LoadingIndicator.lua new file mode 100644 index 0000000..5e6f5ab --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/LoadingIndicator.lua @@ -0,0 +1,146 @@ +local CoreGui = game:GetService("CoreGui") +local RunService = game:GetService("RunService") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local FlagSettings = require(LuaApp.FlagSettings) +local LoadingBar = require(LuaApp.Components.LoadingBar) +local Roact = require(Common.Roact) + +local LOADING_INDICATOR_WIDTH = 70 +local LOADING_INDICATOR_HEIGHT = 16 +local DOT_COUNT = 3 +local DOT_ANIMATION_SPEED_MULTIPLIER = 1.75 +local DOT_BASE_RELATIVE_HEIGHT = 0.7 +local DOT_BASE_RELATIVE_WIDTH = 0.7 +local DOT_HEIGHT_LERP_AMPLITUDE = 0.3 + +local LoadingIndicator = {} + +local function makeDot() + return Create.new "ImageLabel" { + Name = "DotContainer", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + SizeConstraint = Enum.SizeConstraint.RelativeYY, + + Create.new "Frame" { + Name = "Dot", + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + SizeConstraint = Enum.SizeConstraint.RelativeYY, + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5) + }, + } +end + +local function renderLoadingDots(self) + if not self.rbx.Visible then + return + end + local dotsTime = (time() * DOT_ANIMATION_SPEED_MULTIPLIER) % #self.dots + for i, dot in ipairs(self.dots) do + local dotHeight = DOT_BASE_RELATIVE_HEIGHT + if dotsTime >= i - 1 and dotsTime <= i then + dotHeight = DOT_BASE_RELATIVE_HEIGHT + DOT_HEIGHT_LERP_AMPLITUDE * math.sin(math.pi * (dotsTime % 1)) + local colorAlpha = math.sin(math.pi * (dotsTime % 1)) + dot.Dot.BackgroundColor3 = Constants.Color.GRAY3:lerp(Constants.Color.BLUE_PRIMARY, colorAlpha) + else + dot.Dot.BackgroundColor3 = Constants.Color.GRAY3 + end + dot.Dot.Size = UDim2.new(DOT_BASE_RELATIVE_WIDTH, 0, dotHeight, 0) + end +end + +function LoadingIndicator.new(appState, scale) + scale = scale or 1 + + local self = {} + self.connections = {} + + self.rbx = Create.new "Frame" { + Name = "LoadingIndicator", + BackgroundTransparency = 1, + Size = UDim2.new(0, scale * LOADING_INDICATOR_WIDTH, 0, scale * LOADING_INDICATOR_HEIGHT) + } + + local platform = appState.store:GetState().Platform + local isLuaHomePageEnabled = FlagSettings.IsLuaHomePageEnabled(platform) + local isLuaGamesPageEnabled = FlagSettings.IsLuaGamesPageEnabled(platform) + self.isLoadingBarEnabled = isLuaHomePageEnabled and isLuaGamesPageEnabled + + if self.isLoadingBarEnabled then + self.loadingBar = Roact.mount(Roact.createElement(LoadingBar), self.rbx) + else -- use dots loading indicator for non-lua pages + self.dots = {} + for i = 1, DOT_COUNT do + local value = (i - 1) / (DOT_COUNT - 1) + + local dot = makeDot() + dot.Position = UDim2.new(value, 0, 0.5, 0) + dot.AnchorPoint = Vector2.new(value, 0.5) + dot.Parent = self.rbx + + table.insert(self.dots, dot) + end + end + + setmetatable(self, LoadingIndicator) + + do + local connection = self.rbx.AncestryChanged:Connect(function(object, parent) + if object == self.rbx and parent == nil then + self:Destroy() + end + end) + table.insert(self.connections, connection) + end + + if not self.isLoadingBarEnabled then + local connection = RunService.RenderStepped:Connect(function() + renderLoadingDots(self) + end) + table.insert(self.connections, connection) + end + + return self +end + +function LoadingIndicator:SetZIndex(index) + self.rbx.ZIndex = index + if self.isLoadingBarEnabled then + self.loadingBar.ZIndex = index + else + for _, dot in ipairs(self.dots) do + dot.ZIndex = index + dot.Dot.ZIndex = index + end + end +end + +function LoadingIndicator:SetVisible(visible) + self.rbx.Visible = visible +end + +function LoadingIndicator:Destroy() + if self.isLoadingBarEnabled then + Roact.unmount(self.loadingBar) + end + + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + + self.rbx:Destroy() + self.connections = {} +end + +LoadingIndicator.__index = LoadingIndicator + +return LoadingIndicator \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/MessageList.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/MessageList.lua new file mode 100644 index 0000000..c174b1c --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/MessageList.lua @@ -0,0 +1,408 @@ +local CoreGui = game:GetService("CoreGui") +local Players = game:GetService("Players") +local TweenService = game:GetService("TweenService") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local ChatBubble = require(LuaChat.Components.ChatBubble) +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local OrderedMap = require(LuaChat.OrderedMap) +local Signal = require(Common.Signal) + +local ChatTimestamp = require(LuaChat.Components.ChatTimestamp) +local Conversation = require(LuaChat.Models.Conversation) +local LoadingIndicator = require(LuaChat.Components.LoadingIndicator) + +local IS_BOTTOM_BUFFER = 5 + +local LuaChatAssetCardsSelfTerminateConnection = settings():GetFFlag("LuaChatAssetCardsSelfTerminateConnection") + +local MessageList = {} + +MessageList.__index = MessageList + +local function didLocalUserSend(message) + local localUserId = tostring(Players.LocalPlayer.UserId) + + return message.senderTargetId == localUserId +end + +local function getSpacer(height) + return Create.new "Frame" { + Name = "Spacer", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, height), + LayoutOrder = 1, + } +end + +--[[ + Updates the spacing and timestamp display of message identified by + messages and messageIndex. +]] + +local function isFirstMessageInCluster(messages, messageIndex) + local message = messages:GetByIndex(messageIndex) + local previousMessage = messages:GetByIndex(messageIndex - 1) + + if not previousMessage or message.sent:GetUnixTimestamp() - previousMessage.sent:GetUnixTimestamp() > 5 * 60 then + return true + elseif previousMessage and message.senderTargetId == previousMessage.senderTargetId then + return false + end + return true +end + +local function updateBubblePadding(appState, conversationType, bubble, messages, messageIndex) + local message = messages:GetByIndex(messageIndex) + local previousMessage = messages:GetByIndex(messageIndex - 1) + + local padding + local extraInfoVisible = true + + if previousMessage then + if message.sent:GetUnixTimestamp() - previousMessage.sent:GetUnixTimestamp() > 5 * 60 then + local relativeTime = message.sent:GetLongRelativeTime() + padding = ChatTimestamp.new(appState, relativeTime).rbx + else + if message.senderTargetId == previousMessage.senderTargetId then + padding = getSpacer(2) + extraInfoVisible = false + else + padding = getSpacer(10) + end + end + else + local relativeTime = message.sent:GetLongRelativeTime() + padding = ChatTimestamp.new(appState, relativeTime).rbx + end + + bubble:SetTailVisible(extraInfoVisible) + + if not didLocalUserSend(message) then + bubble:SetThumbnailVisible(extraInfoVisible) + + if conversationType == Conversation.Type.MULTI_USER_CONVERSATION then + bubble:SetUsernameVisible(extraInfoVisible) + end + end + + bubble:SetPaddingObject(padding) +end + +local function createChatHistoryMessagePadding() + local padding = Create.new "Frame" { + Name = "LoadingIndicatorPadding", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 50), + + Create.new "UIPadding" { + PaddingBottom = UDim.new(0, 10), + PaddingTop = UDim.new(0, 20) + }, + + Create.new "Frame" { + Name = "Container", + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, 84, 0, 22), + } + } + + return padding +end + +function MessageList.new(appState, conversation) + local self = {} + self.appState = appState + self.conversation = Conversation.mock() + self.messageIdToBubble = {} + self.RequestOlderMessages = Signal.new() + self.ReadAllMessages = Signal.new() + self.isTouchingBottom = true + self.lastThumbnailLocation = {} + self.loadingIndicatorPadding = createChatHistoryMessagePadding() + self.connections = {} + + self.layout = Create.new "UIListLayout" { + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Bottom, + } + + self.rbx = Create.new "ScrollingFrame" { + Name = "MessageList", + ElasticBehavior = "Always", + ScrollingDirection = "Y", + BackgroundTransparency = 0, + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.GRAY6, + Size = UDim2.new(1, 0, 0, 0), + ScrollBarThickness = 5, + BottomImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png", + MidImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png", + TopImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png", + + self.layout, + + Create.new "UIPadding" { + PaddingBottom = UDim.new(0, 10), + }, + } + + self.loadingIndicatorPadding.Parent = self.rbx + self.loadingIndicatorPadding.Visible = false + + self.rbx:GetPropertyChangedSignal("CanvasPosition"):Connect(function() + if self.rbx.CanvasPosition.Y + self.rbx.AbsoluteWindowSize.Y + IS_BOTTOM_BUFFER >= self.rbx.CanvasSize.Y.Offset then + self.isTouchingBottom = true + self.ReadAllMessages:Fire() + else + self.isTouchingBottom = false + end + if self.rbx.CanvasSize.Y.Offset > self.rbx.AbsoluteSize.Y then + if self.lastCanvasPosition then + if self.lastCanvasPosition < 0 and self.rbx.CanvasPosition.Y >= 0 then + self.RequestOlderMessages:Fire() + + elseif self.rbx.CanvasPosition.Y == 0 then + self.RequestOlderMessages:Fire() + + end + end + + self.lastCanvasPosition = self.rbx.CanvasPosition.Y + end + end) + + setmetatable(self, MessageList) + + self:Update(conversation) + + return self +end + +function MessageList:Update(conversation) + if conversation == nil then + return + end + + -- NOTE The following loop is currently used to set the typing indiactor visiblity + -- to true. We currently pass an empty table for usersTyping when receiving a message. + for userId, typing in pairs(conversation.usersTyping) do + if self.lastThumbnailLocation[userId] then + local bubble = self.messageIdToBubble[self.lastThumbnailLocation[userId].id] + if bubble then + bubble:SetTypingIndicatorVisible(typing) + end + end + end + + if conversation.messages == self.conversation.messages and + conversation.sendingMessages == self.conversation.sendingMessages then + return + end + + -- Remove sending messages that have been moved to sent + for id, _ in self.conversation.sendingMessages:CreateIterator() do + if conversation.sendingMessages:Get(id) == nil then + if self.messageIdToBubble[id] then + self.messageIdToBubble[id].rbx:Destroy() + self.messageIdToBubble[id] = nil + end + end + end + + local combinedMessages = OrderedMap.Merge(conversation.messages, conversation.sendingMessages) + + -- Map containing whether a message has been adjusted, indexed by numeric keys + -- into the combinedMessages map. + local adjustedMessages = {} + + local oldThumbnailLocation = self.lastThumbnailLocation + self.lastThumbnailLocation = {} + + local havePassedNewestMessage = false + local havePassedDiscontiguousMessage = false + for id, message, index in combinedMessages:CreateReverseIterator() do + local existingMessage = self.conversation.messages:Get(id) or self.conversation.sendingMessages:Get(id) + local bubble = self.messageIdToBubble[id] + local senderId = message.senderTargetId + + + local isSending = conversation.sendingMessages:Get(id) ~= nil + + if message ~= existingMessage then + if bubble then + bubble:Destruct() + end + + bubble = ChatBubble.new(self.appState, message) + bubble.rbx.Parent = self.rbx + + self.messageIdToBubble[id] = bubble + + -- When a message updates, we need to recalculate layout on it and + -- the message immediately after it. + + updateBubblePadding(self.appState, conversation.conversationType, bubble, combinedMessages, index) + + if not adjustedMessages[index + 1] then + local nextMessage = combinedMessages:GetByIndex(index + 1) + + if nextMessage then + local nextBubble = self.messageIdToBubble[nextMessage.id] + updateBubblePadding(self.appState, conversation.conversationType, nextBubble, combinedMessages, index + 1) + end + end + + adjustedMessages[index] = true + adjustedMessages[index + 1] = true + end + + if (not self.lastThumbnailLocation[senderId]) and isFirstMessageInCluster(combinedMessages, index) then + self.lastThumbnailLocation[senderId] = message + + if ( oldThumbnailLocation and oldThumbnailLocation[senderId] and + oldThumbnailLocation[senderId] ~= message ) and self.messageIdToBubble[oldThumbnailLocation[senderId].id] then + self.messageIdToBubble[oldThumbnailLocation[senderId].id]:SetTypingIndicatorVisible(false) + end + + end + + + -- When the MessageList updates, we assume the most recent message has + -- been received from a previously typing user for Group Conversations. + if ( havePassedNewestMessage == false and isSending == false and self.lastThumbnailLocation[senderId] ) then + havePassedNewestMessage = true + local thumbnailBubble = self.messageIdToBubble[self.lastThumbnailLocation[senderId].id] + if thumbnailBubble then + thumbnailBubble:SetTypingIndicatorVisible(false) + end + end + + bubble.rbx.LayoutOrder = index + + bubble.rbx.Visible = not havePassedDiscontiguousMessage + + if (not isSending) and message.previousMessageId == nil then + havePassedDiscontiguousMessage = true + end + + local connection = bubble.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + local wasTouchingBottom = self.isTouchingBottom + self:ResizeCanvas() + if wasTouchingBottom then + self:ScrollToBottom() + end + end) + + table.insert(self.connections, connection) + end + + self.conversation = conversation + + local wasTouchingBottom = self.isTouchingBottom + self:ResizeCanvas() + if wasTouchingBottom then + self:ScrollToBottom() + end +end + +function MessageList:CalculateCanvasSize() + local height = 0 + + for _, child in pairs(self.messageIdToBubble) do + if child.rbx.Visible then + height = height + child.rbx.AbsoluteSize.Y + end + end + + if self.loadingIndicatorPadding.Visible then + height = height + self.loadingIndicatorPadding.Size.Y.Offset + end + + return UDim2.new(1, 0, 0, height) +end + +function MessageList:ResizeCanvas() + local canvas = self.rbx + local oldHeight = canvas.CanvasSize.Y.Offset + + canvas.CanvasSize = self:CalculateCanvasSize() + --Restore distance to bottom + canvas.CanvasPosition = canvas.CanvasPosition + Vector2.new(0, canvas.CanvasSize.Y.Offset - oldHeight) +end + +function MessageList:ScrollToBottom() + local height = self.rbx.CanvasSize.Y.Offset - self.rbx.AbsoluteWindowSize.Y + self.rbx.CanvasPosition = Vector2.new(0, height) +end + +function MessageList:TweenScrollToBottom(offsetY, tweenInfo) + local height = self.rbx.CanvasSize.Y.Offset - self.rbx.AbsoluteWindowSize.Y + offsetY + local propertyGoals = + { + CanvasPosition = Vector2.new(0, height) + } + local tween = TweenService:Create(self.rbx, tweenInfo, propertyGoals) + tween:Play() +end + +function MessageList:StartLoadingMessageHistoryAnimation() + if self.loadingIndicator == nil then + self.loadingIndicator = LoadingIndicator.new(self.appState, 1) + end + + local padding = self.loadingIndicatorPadding + + self.loadingIndicator.rbx.Parent = padding.Container + padding.Visible = true + + self.rbx.CanvasSize = self:CalculateCanvasSize() +end + +function MessageList:StopLoadingMessageHistoryAnimation() + local loadingIndicator = self.loadingIndicator + if loadingIndicator then + + local padding = self.loadingIndicatorPadding + padding.Visible = false + + self.rbx.CanvasSize = self:CalculateCanvasSize() + + self.loadingIndicator:Destroy() + self.loadingIndicator = nil + end +end + +if not LuaChatAssetCardsSelfTerminateConnection then + function MessageList:DisconnectChatBubbles() + for _, chatEntry in pairs(self.messageIdToBubble) do + for _, childBubble in pairs(chatEntry.bubbles) do + if childBubble.bubbleType == "AssetCard" then + childBubble:DisconnectUpdate() + end + end + end + end +end + +function MessageList:Destruct() + for _, bubble in pairs(self.messageIdToBubble) do + bubble:Destruct() + end + + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + + self.messageIdToBubble = {} + self.rbx:Destroy() +end + +return MessageList diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ModalHeader.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ModalHeader.lua new file mode 100644 index 0000000..41a5669 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ModalHeader.lua @@ -0,0 +1,219 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local Signal = require(Common.Signal) + +local Components = LuaChat.Components + +local BaseHeader = require(Components.BaseHeader) +local FlagSettings = require(LuaChat.FlagSettings) +local ModalTextButton = require(Components.ModalTextButton) + +local HEIGHT_OF_BORDER = 1 +local HEIGHT_OF_DISCONNECTED = 32 +local HEIGHT_OF_HEADER = 44 +local ICON_CELL_WIDTH = 60 + +local TOTAL_HEIGHT_OF_HEADER = HEIGHT_OF_HEADER + HEIGHT_OF_BORDER + +local ModalHeader = BaseHeader:Template() +ModalHeader.__index = ModalHeader + +function ModalHeader.new(appState, dialogType) + local self = {} + + self.heightOfHeader = TOTAL_HEIGHT_OF_HEADER + self.heightOfDisconnected = HEIGHT_OF_DISCONNECTED + + self.buttons = {} + self.connections = {} + self.appState = appState + self.dialogType = dialogType + self.backButton = BaseHeader:GetNewBackButton(dialogType) + self.backButton.rbx.Visible = false + self.backButton.rbx.Size = UDim2.new(0, 24, 0, 24) + self.backButton.rbx.Position = UDim2.new(0.5, -2, 0.5, 0) + self.backButton.rbx.AnchorPoint = Vector2.new(0.5, 0.5) + self.title = "" + self.subtitle = nil + self.connectionState = Enum.ConnectionState.Connected + + self.luaChatPlayTogetherEnabled = FlagSettings.IsLuaChatPlayTogetherEnabled( + self.appState.store:getState().FormFactor) + + self.BackButtonPressed = Signal.new() + local backButtonConnection = self.backButton.Pressed:Connect(function() + self.BackButtonPressed:Fire() + end) + table.insert(self.connections, backButtonConnection) + + self.innerSubtitle = Create.new "TextLabel" { + Name = "Subtitle", + BackgroundTransparency = 1, + TextSize = Constants.Font.FONT_SIZE_12, + TextColor3 = Constants.Color.GRAY1, + Size = UDim2.new(0, 200, 0, 18), + AnchorPoint = Vector2.new(0.5, 0.5), + Text = "", + Font = "SourceSans", + LayoutOrder = 1, + } + + self.innerTitles = Create.new "Frame" { + Name = "Titles", + BackgroundTransparency = 1, + Size = UDim2.new(0, 200, 1, 0), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(0.5, 0), + + Create.new "UIListLayout" { + SortOrder = "LayoutOrder", + VerticalAlignment = Enum.VerticalAlignment.Center + }, + + Create.new "TextLabel" { + Name = "Title", + BackgroundTransparency = 1, + TextSize = Constants.Font.FONT_SIZE_20, + TextColor3 = Constants.Color.GRAY1, + Size = UDim2.new(0, 200, 0, 18), + AnchorPoint = Vector2.new(0.5, 0.5), + Text = "Title", + Font = "SourceSans", + LayoutOrder = 0, + }, + + self.innerSubtitle, + } + + self.innerButtons = Create.new "Frame" { + Name = "Buttons", + BackgroundTransparency = 1, + Position = UDim2.new(1, -12, 0, 0), + Size = UDim2.new(0, 100, 1, 0), + AnchorPoint = Vector2.new(1, 0), + + Create.new "UIListLayout" { + SortOrder = "LayoutOrder", + HorizontalAlignment = "Right", + FillDirection = "Horizontal", + }, + } + + self.innerContent = Create.new "Frame" { + Name = "Content", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, -4), + Position = UDim2.new(0, 0, 0, 4), + + Create.new"Frame" { + Name = "Icon", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, ICON_CELL_WIDTH, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + + self.backButton.rbx, + }, + + self.innerTitles, + self.innerButtons, + } + + self.innerHeader = Create.new "Frame" { + Name = "Header", + BackgroundTransparency = 1, -- Set transparent so the rounded corners will show. + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, HEIGHT_OF_HEADER), + LayoutOrder = 0, + + self.innerContent, + } + + self.rbx = Create.new "Frame" { + Name = "HeaderFrame", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, TOTAL_HEIGHT_OF_HEADER), + + Create.new("UIListLayout") { + Name = "ModalHeaderListLayout", + SortOrder = "LayoutOrder", + FillDirection = Enum.FillDirection.Vertical, + }, + + self.innerHeader, + + Create.new "Frame" { + Name = "BottomBorder", + BackgroundColor3 = Constants.Color.GRAY3, + Size = UDim2.new(1, 0, 0, HEIGHT_OF_BORDER), + BorderSizePixel = 0, + LayoutOrder = 1, + }, + + Create.new "Frame" { + Name = "Disconnected", + AnchorPoint = Vector2.new(0, 1), + BackgroundColor3 = Constants.Color.GRAY3, + BorderSizePixel = 0, + ClipsDescendants = true, + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, 0), -- Note: Deliberately has zero vertical height, will be scaled. + + Create.new "TextLabel" { + Name = "Title", + AnchorPoint = Vector2.new(0.5, 1), + BackgroundTransparency = 1, + Font = Constants.Font.TITLE, + LayoutOrder = 0, + Position = UDim2.new(0.5, 0, 1, 0), + Size = UDim2.new(1, 0, 0, HEIGHT_OF_DISCONNECTED), + Text = appState.localization:Format("Feature.Chat.Message.NoConnectionMsg"), + TextColor3 = Constants.Color.WHITE, + TextSize = Constants.Font.FONT_SIZE_14, + }, + }, + + Create.new "Frame" { + Name = "GameDrawer", + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = false, + LayoutOrder = 3, + Size = UDim2.new(1, 0, 0, 0), -- Note: Deliberately zero height, will be scaled open. + Visible = false, + }, + + } + + local parentChangedConnection = self.rbx:GetPropertyChangedSignal("Parent"):Connect(function() + if self.rbx and self.rbx.Parent then + game:GetService("RunService").Stepped:wait() -- TextBounds isn't recalculated when this fires so we wait + self:SetTitle(self.title) -- Again, this can be much cleaner once we have proper truncation support + end + end) + table.insert(self.connections, parentChangedConnection) + + do + local connection = appState.store.Changed:Connect(function(state, oldState) + self:SetConnectionState(state.ConnectionState) + end) + table.insert(self.connections, connection) + end + + setmetatable(self, ModalHeader) + return self +end + +function ModalHeader:CreateHeaderButton(name, textKey) + local saveGroup = ModalTextButton.new(self.appState, name, textKey) + self:AddButton(saveGroup) + return saveGroup +end + +return ModalHeader diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ModalTextButton.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ModalTextButton.lua new file mode 100644 index 0000000..59c3942 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ModalTextButton.lua @@ -0,0 +1,76 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local Signal = require(Common.Signal) +local Text = require(LuaChat.Text) +local getInputEvent = require(LuaChat.Utils.getInputEvent) + +local FONT = Enum.Font.SourceSans +local TEXT_SIZE = Constants.Font.FONT_SIZE_18 +local X_PADDING = 8 + +local TextButton = {} + +TextButton.__index = TextButton + +function TextButton.new(appState, name, textKey) + local self = {} + + self.enabled = true + + + local text = appState.localization:Format(textKey) + + local textWidth = Text.GetTextWidth(text, FONT, TEXT_SIZE) + + self.rbx = Create.new "TextButton" { + Name = name, + BackgroundTransparency = 1, + Text = "", + Size = UDim2.new(0, textWidth + X_PADDING, 1, 0), + + Create.new "TextLabel" { + Name = "Label", + Size = UDim2.new(0, textWidth, 0, TEXT_SIZE), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundTransparency = 1, + Text = text, + Font = FONT, + TextSize = TEXT_SIZE, + TextColor3 = Constants.Color.BLUE_PRIMARY, + }, + } + + self.Pressed = Signal.new() + + getInputEvent(self.rbx):Connect(function() + if not self.enabled then + return + end + + self.Pressed:Fire() + end) + + setmetatable(self, TextButton) + + return self +end + +function TextButton:SetEnabled(value) + if value then + self.rbx.Label.TextColor3 = Constants.Color.BLUE_PRIMARY + self.rbx.Label.TextTransparency = 0 + else + self.rbx.Label.TextColor3 = Constants.Color.GRAY3 + end + + self.enabled = value +end + +return TextButton diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/NewChatGroup.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/NewChatGroup.lua new file mode 100644 index 0000000..83aa4d1 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/NewChatGroup.lua @@ -0,0 +1,210 @@ +local CoreGui = game:GetService("CoreGui") +local Players = game:GetService("Players") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local ConversationActions = require(LuaChat.Actions.ConversationActions) +local ConversationModel = require(LuaChat.Models.Conversation) +local Create = require(LuaChat.Create) +local DialogInfo = require(LuaChat.DialogInfo) +local Immutable = require(Common.Immutable) +local Signal = require(Common.Signal) + + +local Components = LuaChat.Components +local FriendSearchBoxComponent = require(Components.FriendSearchBox) +local HeaderLoader = require(Components.HeaderLoader) +local ResponseIndicator = require(Components.ResponseIndicator) +local SectionComponent = require(Components.ListSection) +local TextInputEntry = require(Components.TextInputEntry) + +local RemoveRoute = require(LuaChat.Actions.RemoveRoute) + +local Intent = DialogInfo.Intent + +local NewChatGroup = {} +NewChatGroup.__index = NewChatGroup + +local function getAsset(name) + return "rbxasset://textures/ui/LuaChat/"..name..".png" +end + +function NewChatGroup.new(appState) + local self = { + appState = appState, + } + setmetatable(self, NewChatGroup) + self.connections = {} + + self.conversation = ConversationModel.empty() + + self.responseIndicator = ResponseIndicator.new(appState) + self.responseIndicator:SetVisible(false) + + -- Header: + self.header = HeaderLoader.GetHeader(appState, Intent.NewChatGroup) + self.header:SetDefaultSubtitle() + self.header:SetTitle(appState.localization:Format("Feature.Chat.Heading.NewChatGroup")) + self.header:SetBackButtonEnabled(true) + self.header:SetConnectionState(Enum.ConnectionState.Disconnected) + + -- Name the group: + local placeholderText = appState.localization:Format("Feature.Chat.Description.NameGroupChat") + self.groupName = TextInputEntry.new(appState, getAsset("icons/ic-nametag"), placeholderText) + self.groupName.rbx.LayoutOrder = 1 + + local sanitizeGroupName = function(input) + return input:gsub("\n", "") + end + local textboxChangedConnection = self.groupName.textBoxChanged:Connect(function(newGroupName) + self.conversation.title = sanitizeGroupName(newGroupName) + end) + table.insert(self.connections, textboxChangedConnection) + + local textboxFocusLostConnection = self.groupName.textBoxFocusLost:Connect(function() + self.groupName:SanitizeInput(sanitizeGroupName) + end) + table.insert(self.connections, textboxFocusLostConnection) + + -- Search for friends: + self.searchComponent = FriendSearchBoxComponent.new( + appState, + self.conversation.participants, + Constants.MAX_PARTICIPANT_COUNT, + function(user) + return user.isFriend and user.id ~= tostring(Players.LocalPlayer.UserId) + end + ) + self.searchComponent.rbx.LayoutOrder = 3 + local addParticipantConnection = self.searchComponent.addParticipant:Connect(function(id) + self.groupName:ReleaseFocus() + self.searchComponent.search:ReleaseFocus() + self:ChangeParticipants(Immutable.Set(self.conversation.participants, #self.conversation.participants+1, id)) + end) + table.insert(self.connections, addParticipantConnection) + + local removeParticipantConnection = self.searchComponent.removeParticipant:Connect(function(id) + self.groupName:ReleaseFocus() + self.searchComponent.search:ReleaseFocus() + self:ChangeParticipants(Immutable.RemoveValueFromList(self.conversation.participants, id)) + end) + table.insert(self.connections, removeParticipantConnection) + + -- Assemble the dialog from components we just made: + self.sectionComponent = SectionComponent.new(appState, nil, 2) + self.rbx = Create.new"Frame" { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Create.new("UIListLayout") { + Name = "ListLayout", + SortOrder = "LayoutOrder", + }, + self.header.rbx, + Create.new"Frame" { + Name = "Content", + Size = UDim2.new(1, 0, 1, -(self.header.heightOfHeader)), + BackgroundColor3 = Constants.Color.GRAY5, + BorderSizePixel = 0, + LayoutOrder = 1, + ClipsDescendants = true, + + Create.new"UIListLayout" { + Name = "ListLayout", + SortOrder = "LayoutOrder", + }, + self.groupName.rbx, + self.sectionComponent.rbx, + self.searchComponent.rbx, + }, + self.responseIndicator.rbx, + } + + -- Wire up the save button to actually create our new chat group: + self.saveGroup = self.header:CreateHeaderButton("SaveGroup", "Feature.Chat.Action.Create") + self.saveGroup:SetEnabled(false) + local saveGroupConnection = self.saveGroup.Pressed:Connect(function() + self.groupName:ReleaseFocus() + self.searchComponent.search:ReleaseFocus() + if #self.conversation.participants >= Constants.MIN_PARTICIPANT_COUNT then + self.responseIndicator:SetVisible(true) + self.appState.store:dispatch( + ConversationActions.CreateConversation(self.conversation, function(id) + self.responseIndicator:SetVisible(false) + if id ~= nil then + self.ConversationSaved:Fire(id) + end + self.appState.store:dispatch(RemoveRoute(Intent.NewChatGroup)) + end) + ) + end + end) + table.insert(self.connections, saveGroupConnection) + + self.BackButtonPressed = Signal.new() + self.header.BackButtonPressed:Connect(function() + self.groupName:ReleaseFocus() + self.searchComponent.search:ReleaseFocus() + self.BackButtonPressed:Fire() + end) + self.ConversationSaved = Signal.new() + + spawn(function() + self.appState.store:dispatch(ConversationActions.GetAllFriendsAsync()) + end) + + self.tooManyFriendsAlertId = nil + + -- Monitor for several size changes to properly scale dialog elements: + local groupNameConnection = self.groupName.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + self:Resize() + end) + table.insert(self.connections, groupNameConnection) + + local headerSizeConnection = self.header.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + self:Resize() + end) + table.insert(self.connections, headerSizeConnection) + + return self +end + +function NewChatGroup:Resize() + -- Content frame must resize if the header changes size (which happens when it shows the "Connecting" message): + local sizeContent = UDim2.new(1, 0, 1, -self.header.rbx.AbsoluteSize.Y) + self.rbx.Content.Size = sizeContent + + -- Friends Search frame must resize to fit properly with their peers: + local sizeSearch = UDim2.new(1, 0, 1, -(self.groupName.rbx.AbsoluteSize.Y + self.sectionComponent.rbx.AbsoluteSize.Y)) + self.searchComponent.rbx.Size = sizeSearch +end + +function NewChatGroup:ChangeParticipants(participants) + self.conversation = Immutable.Set(self.conversation, "participants", participants) + self.searchComponent:Update(participants) + self.saveGroup:SetEnabled(#participants >= Constants.MIN_PARTICIPANT_COUNT) +end + +function NewChatGroup:Update(current, previous) + self.header:SetConnectionState(current.ConnectionState) + self.searchComponent:Update(self.conversation.participants) +end + +function NewChatGroup:Destruct() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + + self.header:Destroy() + self.groupName:Destruct() + self.responseIndicator:Destruct() + self.searchComponent:Destruct() + self.rbx:Destroy() +end + +return NewChatGroup \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/NoFriendsIndicator.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/NoFriendsIndicator.lua new file mode 100644 index 0000000..8101dd1 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/NoFriendsIndicator.lua @@ -0,0 +1,56 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local LuaChat = Modules.LuaChat + +local Create = require(LuaChat.Create) +local Constants = require(LuaChat.Constants) + +local NoFriendsIndicator = {} + +NoFriendsIndicator.__index = NoFriendsIndicator + +function NoFriendsIndicator.new(appState) + local self = {} + + self.rbx = Create.new "Frame" { + Name = "NoFriendsIndicator", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 300), + + Create.new "Frame" { + Name = "IndicatorInner", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 160), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + + Create.new "UIListLayout" { + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }, + + Create.new "ImageLabel" { + BackgroundTransparency = 1, + Size = UDim2.new(0, 72, 0, 72), + LayoutOrder = 1, + Image = "rbxasset://textures/ui/LuaChat/icons/ic-friends.png", + }, + + Create.new "TextLabel" { + Size = UDim2.new(1, -32, 0, 66), + BackgroundTransparency = 1, + LayoutOrder = 2, + Font = Enum.Font.SourceSans, + TextColor3 = Constants.Color.GRAY2, + TextSize = Constants.Font.FONT_SIZE_18, + TextWrapped = true, + Text = appState.localization:Format("Feature.Chat.Message.MakeFriendsToChat"), + }, + }, + } + + setmetatable(self, NoFriendsIndicator) + + return self +end + +return NoFriendsIndicator \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/PaddedImageButton.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/PaddedImageButton.lua new file mode 100644 index 0000000..35b491e --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/PaddedImageButton.lua @@ -0,0 +1,49 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat +local getInputEvent = require(LuaChat.Utils.getInputEvent) + +local Create = require(LuaChat.Create) +local Signal = require(Common.Signal) + +local PaddedImageButton = {} + +function PaddedImageButton.new(appState, name, imageUrl) + local self = {} + + self.rbx = Create.new "ImageButton" { + Name = name, + Size = UDim2.new(0, 40, 0, 40), + BackgroundTransparency = 1, + + Create.new "ImageLabel" { + Name = "ImageLabel", + Size = UDim2.new(0, 24, 0, 24), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundTransparency = 1, + Image = imageUrl + }, + } + + self.Pressed = Signal.new() + + getInputEvent(self.rbx):Connect(function() + self.Pressed:Fire() + end) + + setmetatable(self, PaddedImageButton) + + return self +end + + +function PaddedImageButton:SetVisible(value) + self.rbx.Visible = value +end + +PaddedImageButton.__index = PaddedImageButton + +return PaddedImageButton diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/PlaceInfoCard.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/PlaceInfoCard.lua new file mode 100644 index 0000000..cecbce3 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/PlaceInfoCard.lua @@ -0,0 +1,114 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local LuaChat = Modules.LuaChat + +local Create = require(LuaChat.Create) +local Constants = require(LuaChat.Constants) +local GetPlaceThumbnail = require(LuaChat.Actions.GetPlaceThumbnail) + +local PlaceInfoCard = {} +PlaceInfoCard.__index = PlaceInfoCard + +local PLACE_INFO_CARD_HEIGHT = 72 +local PLACE_INFO_THUMBNAIL_SIZE = 50 + +local UrlSupportNewGamesAPI = settings():GetFFlag("UrlSupportNewGamesAPI") + +function PlaceInfoCard.new(appState, placeInfo) + local self = {} + self.appState = appState + self.placeInfo = placeInfo + self.thumbnail = appState.store:getState().ChatAppReducer.PlaceThumbnails[placeInfo.imageToken] + self.connections = {} + setmetatable(self, PlaceInfoCard) + + self.rbx = Create.new "Frame" { + Name = "PlaceInfoCardFrame", + BackgroundColor3 = Constants.Color.WHITE, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, PLACE_INFO_CARD_HEIGHT), + + Create.new "Frame" { + Name = "PlaceThumbnailFrame", + BackgroundColor3 = Constants.Color.WHITE, + BorderSizePixel = 0, + Size = UDim2.new(0, PLACE_INFO_CARD_HEIGHT, 0, PLACE_INFO_CARD_HEIGHT), + Position = UDim2.new(0, 0, 0, 0) + }, + + Create.new"TextLabel" { + Name = "PlaceTitle", + BackgroundTransparency = 1, + Size = UDim2.new(1, -PLACE_INFO_CARD_HEIGHT, 0.75, 0), + Position = UDim2.new(0, PLACE_INFO_CARD_HEIGHT, 0, 0), + TextSize = Constants.Font.FONT_SIZE_16, + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSans, + Text = placeInfo.name, + TextXAlignment = Enum.TextXAlignment.Left, + }, + + Create.new"TextLabel" { + Name = "BuilderLabel", + BackgroundTransparency = 1, + Size = UDim2.new(1, -PLACE_INFO_CARD_HEIGHT, 0.35, 0), + Position = UDim2.new(0, PLACE_INFO_CARD_HEIGHT, 0.5, 0), + TextSize = Constants.Font.FONT_SIZE_14, + TextColor3 = Constants.Color.GRAY2, + Font = Enum.Font.SourceSans, + Text = appState.localization:Format("Feature.Chat.Label.ByBuilder", { USERNAME = placeInfo.builder }), + TextXAlignment = Enum.TextXAlignment.Left, + }, + } + + if not UrlSupportNewGamesAPI then + self.thumbnail = "rbxasset://textures/ui/LuaChat/icons/share-game-thumbnail.png" + end + + if not self.thumbnail then + self.appState.store:dispatch(GetPlaceThumbnail(self.placeInfo.imageToken, + PLACE_INFO_THUMBNAIL_SIZE, PLACE_INFO_THUMBNAIL_SIZE)) + + local appStateConnection = self.appState.store.Changed:Connect(function(state, oldState) + self:Update(state, oldState) + end) + table.insert(self.connections, appStateConnection) + else + self:FillThumbnail() + end + + return self +end + +function PlaceInfoCard:Update(newState, oldState) + local thumbnail = newState.ChatAppReducer.PlaceThumbnails[self.placeInfo.imageToken] + if (not self.thumbnail) and thumbnail then + if thumbnail == '' then + self.thumbnail = "rbxasset://textures/ui/LuaChat/icons/share-game-thumbnail.png" + else + self.thumbnail = thumbnail + end + self:FillThumbnail() + end +end + +function PlaceInfoCard:FillThumbnail() + self.placeThumbnail = Create.new "ImageLabel" { + Name = "PlaceThumbnail", + Image = self.thumbnail.image, + Size = UDim2.new(0, 48, 0, 48), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + } + self.placeThumbnail.Parent = self.rbx.PlaceThumbnailFrame +end + +function PlaceInfoCard:Destruct() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + self.rbx:Destroy() +end + +return PlaceInfoCard \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/PlayTogetherGameIcon.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/PlayTogetherGameIcon.lua new file mode 100644 index 0000000..e899abf --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/PlayTogetherGameIcon.lua @@ -0,0 +1,247 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local Create = require(LuaChat.Create) +local getInputEvent = require(LuaChat.Utils.getInputEvent) +local GetMultiplePlaceInfos = require(LuaChat.Actions.GetMultiplePlaceInfos) +local GetPlaceThumbnail = require(LuaChat.Actions.GetPlaceThumbnail) +local Signal = require(Modules.Common.Signal) +local SortedActivelyPlayedGames = require(LuaChat.SortedActivelyPlayedGames) +local User = require(LuaApp.Models.User) + +local ICON_MASK = "rbxasset://textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48.png" +local STACKED_ICON_MASK = "rbxasset://textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52.png" +local BORDER_ICON_MASK = "rbxasset://textures/ui/LuaChat/graphic/gr-game-border-24x24.png" +local DEFAULT_THUMBNAIL = "rbxasset://textures/ui/LuaChat/icons/share-game-thumbnail.png" + +local LARGE_GAME_ICON_OUTER_SIZE = UDim2.new(0, 48, 0, 48) +local LARGE_GAME_STACK_ICON_SIZE = UDim2.new(0, 52, 0, 52) + +local SMALL_GAME_ICON_OUTER_SIZE = UDim2.new(0, 40, 0, 40) +local SMALL_GAME_ICON_CHILD_SIZE = UDim2.new(0, 24, 0, 24) + +local PLACE_INFO_THUMBNAIL_SIZE = 48 + +local UrlSupportNewGamesAPI = settings():GetFFlag("UrlSupportNewGamesAPI") + +local PlayTogetherGameIcon = {} +PlayTogetherGameIcon.__index = PlayTogetherGameIcon + +PlayTogetherGameIcon.Size = { + SMALL = "SMALL", + LARGE = "LARGE", +} + +PlayTogetherGameIcon.Type = { + DEFAULT = "DEFAULT", + ACTIVE = "ACTIVE" +} + +function PlayTogetherGameIcon.new(appState, conversation, iconSize, iconType) + local self = { + appState = appState, + } + self.cachedConversation = nil + self.cachedThumbnailPlaceInfo = nil + self.conversationId = nil + self.conversationUsers = {} + self.fetchedMostRecentlyPlayedGames = false + self.setIconPending = false + self.type = iconType or PlayTogetherGameIcon.Type.DEFAULT + self.updateConnection = nil + + local childImage = BORDER_ICON_MASK + local childSize = SMALL_GAME_ICON_CHILD_SIZE + local size = SMALL_GAME_ICON_CHILD_SIZE + local outerSize = SMALL_GAME_ICON_OUTER_SIZE + if iconSize == PlayTogetherGameIcon.Size.LARGE then + childImage = ICON_MASK + childSize = LARGE_GAME_STACK_ICON_SIZE + size = LARGE_GAME_ICON_OUTER_SIZE + outerSize = LARGE_GAME_ICON_OUTER_SIZE + end + + self.rbx = Create.new "Frame" { + Name = "Frame", + BackgroundTransparency = 1, + Size = outerSize, + + Create.new "ImageButton" { + Name = "TopGameIcon", + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = DEFAULT_THUMBNAIL, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = size, + + Create.new "ImageLabel" { + Name = "Mask", + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = childImage, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = childSize, + } + } + } + + self.iconSize = iconSize + self.mask = self.rbx.TopGameIcon.Mask + + setmetatable(self, PlayTogetherGameIcon) + + self:SetVisible(false) + + if conversation ~= nil then + self.conversationId = conversation.id + self.cachedConversation = conversation + self:Update(conversation) + + self.updateConnection = appState.store.Changed:Connect(function(state, oldState) + -- Update if there's a change in which conversation we're viewing: + local newConversation = state.ChatAppReducer.Conversations[self.conversationId] + + -- Note conversations can be nil if we're a dummy 1:1 conversation + -- which was removed and replaced by a server conversation: + if newConversation == nil then + return + end + + if self.cachedConversation ~= newConversation then + self.cachedConversation = newConversation + self:Update(self.cachedConversation) + return + end + + -- Update if there's any change in our conversation participants: + local users = state.Users + local participantsCache = self.cachedConversation.participants + for _, id in ipairs(conversation.participants) do + if participantsCache[id] ~= users[id] then + self:Update(self.cachedConversation) + return + end + end + + -- Update if we were waiting for our icon to load: + if self.setIconPending then + self:Update(self.cachedConversation) + return + end + end) + end + + self.Pressed = Signal.new() + getInputEvent(self.rbx.TopGameIcon):Connect(function() + self.Pressed:Fire() + end) + + return self +end + +function PlayTogetherGameIcon:Update(conversation) + if not UrlSupportNewGamesAPI then + self.rbx.TopGameIcon.Image = DEFAULT_THUMBNAIL + return + end + + local state = self.appState.store:getState() + + local pinnedGameRootPlaceId = nil + if conversation.pinnedGame and conversation.pinnedGame.rootPlaceId then + pinnedGameRootPlaceId = conversation.pinnedGame.rootPlaceId + end + + local inGameParticipants = {} + local mostRecentPlayedPlayableGamePlaceId = + self.appState.store:getState().ChatAppReducer.MostRecentlyPlayedGames.playableGamePlaceId + + for _, userId in pairs(conversation.participants) do + local user = state.Users[userId] + if user ~= nil then + if (user.presence == User.PresenceType.IN_GAME) and user.placeId then + table.insert(inGameParticipants, user) + end + end + end + + if self.type == PlayTogetherGameIcon.Type.ACTIVE then + if #inGameParticipants > 0 then + local activelyPlayedGames = SortedActivelyPlayedGames.getSortedGames(pinnedGameRootPlaceId, + inGameParticipants) + local isMultiple = #activelyPlayedGames > 1 + self:SetThumbnail(state, activelyPlayedGames[1].placeId, isMultiple) + else + self:SetVisible(false) + end + else + if #inGameParticipants > 0 then + local activelyPlayedGames = SortedActivelyPlayedGames.getSortedGames(pinnedGameRootPlaceId, + inGameParticipants) + self:SetThumbnail(state, activelyPlayedGames[1].placeId, false) + elseif pinnedGameRootPlaceId then + self:SetThumbnail(state, pinnedGameRootPlaceId, false) + elseif mostRecentPlayedPlayableGamePlaceId then + self:SetThumbnail(state, mostRecentPlayedPlayableGamePlaceId, false) + else + self:SetVisible(false) + end + end +end + +function PlayTogetherGameIcon:SetThumbnail(state, chosenPlaceId, isMultiple) + local placeInfo = state.ChatAppReducer.PlaceInfos[chosenPlaceId] + self:SetVisible(true) + if placeInfo == nil then + self.appState.store:dispatch(GetMultiplePlaceInfos({chosenPlaceId})) + self.setIconPending = true + else + self.cachedThumbnailPlaceInfo = placeInfo + local thumbnail = state.ChatAppReducer.PlaceThumbnails[placeInfo.imageToken] + if thumbnail == nil then + self.appState.store:dispatch(GetPlaceThumbnail( + placeInfo.imageToken, PLACE_INFO_THUMBNAIL_SIZE, PLACE_INFO_THUMBNAIL_SIZE + )) + self.setIconPending = true + else + self:SetIsMultipleGames(isMultiple) + self.setIconPending = false + if thumbnail.image ~= '' then + self.rbx.TopGameIcon.Image = thumbnail.image + else + self.rbx.TopGameIcon.Image = DEFAULT_THUMBNAIL + end + end + end +end + +function PlayTogetherGameIcon:SetVisible(value) + self.rbx.Visible = value +end + +function PlayTogetherGameIcon:SetIsMultipleGames(isMultiple) + if self.iconSize == PlayTogetherGameIcon.Size.SMALL then + return + end + + if isMultiple then + self.mask.Image = STACKED_ICON_MASK + self.mask.Size = LARGE_GAME_STACK_ICON_SIZE + else + self.mask.Image = ICON_MASK + self.mask.Size = LARGE_GAME_ICON_OUTER_SIZE + end +end + +function PlayTogetherGameIcon:Destruct() + if self.updateConnection then + self.updateConnection:Disconnect() + end + + self.rbx:Destroy() +end + +return PlayTogetherGameIcon diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ResponseIndicator.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ResponseIndicator.lua new file mode 100644 index 0000000..2826e40 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ResponseIndicator.lua @@ -0,0 +1,65 @@ +local LuaChat = script.Parent.Parent +local Create = require(LuaChat.Create) +local Constants = require(LuaChat.Constants) +local LoadingIndicatorComponent = require(LuaChat.Components.LoadingIndicator) + +local PADDING = 48 + +local ResponseIndicator = {} + +function ResponseIndicator.new(appState) + local self = {} + + setmetatable(self, {__index = ResponseIndicator}) + + self.connections = {} + + self.indicator = LoadingIndicatorComponent.new(appState) + self.indicator:SetZIndex(3) + + self.indicator.rbx.AnchorPoint = Vector2.new(0.5, 0.5) + self.indicator.rbx.Position = UDim2.new(0.5, 0, 0.5, 0) + + self.rbx = Create.new"ImageButton" { -- So you can't select buttons underneath + Name = "ResponseIndicator", + BackgroundTransparency = Constants.Color.ALPHA_SHADOW_HOVER, + BackgroundColor3 = Constants.Color.GRAY1, + AutoButtonColor = false, + BorderSizePixel = 0, + Active = false, + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + ZIndex = 3, + Create.new"ImageLabel" { + BackgroundTransparency = 1, + Size = self.indicator.rbx.Size + UDim2.new(0, PADDING, 0, PADDING), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(3,3,4,4), + Image = "rbxasset://textures/ui/LuaChat/9-slice/input-default.png", + BorderSizePixel = 0, + ZIndex = 3, + self.indicator.rbx, + }, + } + + return self +end + +function ResponseIndicator:SetVisible(value) + self.rbx.Visible = value + self.indicator:SetVisible(value) +end + +function ResponseIndicator:Destruct() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + + self.connections = {} + self.indicator:Destroy() + self.rbx:Destroy() +end + +return ResponseIndicator diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/SharedGameItem.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/SharedGameItem.lua new file mode 100644 index 0000000..3c1e63b --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/SharedGameItem.lua @@ -0,0 +1,463 @@ +local GuiService = game:GetService("GuiService") +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local UserInputService = game:GetService("UserInputService") + +local Constants = require(Modules.LuaChat.Constants) +local ConversationActions = require(Modules.LuaChat.Actions.ConversationActions) +local formatInteger = require(Modules.LuaChat.Utils.formatInteger) +local LocalizedTextLabel = require(Modules.LuaApp.Components.LocalizedTextLabel) +local Roact = require(Modules.Common.Roact) +local RoactAnalyticsSharedGameItem = require(Modules.LuaChat.Services.RoactAnalyticsSharedGameItem) +local RoactRodux = require(Modules.Common.RoactRodux) +local RoactServices = require(Modules.LuaApp.RoactServices) +local Text = require(Modules.Common.Text) + +local DEFAULT_BACKGROUND_COLOR = Constants.Color.WHITE +local DEFAULT_GAME_ADDITIONAL_INFO_LABEL_HEIGHT = 14 +local DEFAULT_GAME_ADDITIONAL_INFO_LABEL_TEXT_SIZE = 14 +local DEFAULT_GAME_CREATOR_LABEL_BOTTOM_PADDING = 3 +local DEFAULT_GAME_CREATOR_LABEL_COLOR = Constants.Color.GRAY2 +local DEFAULT_GAME_CREATOR_LABEL_HEIGHT = 14 +local DEFAULT_GAME_CREATOR_LABEL_TEXT_SIZE = 15 +local DEFAULT_GAME_CREATOR_LABEL_TOP_PADDING = 6 +local DEFAULT_GAME_NAME_LABEL_COLOR = Constants.Color.GRAY1 +local DEFAULT_GAME_NAME_LABEL_HEIGHT = 20 +local DEFAULT_GAME_NAME_LABEL_TEXT_SIZE = 23 +local DEFAULT_GAME_ICON_LEFT_PADDING = 15 +local DEFAULT_GAME_ICON_RIGHT_PADDING = 12 +local DEFAULT_GAME_ICON_SIZE = Constants.SharedGamesConfig.Thumbnail.SHOWN_SIZE +local DEFAULT_GAME_ICON_TOP_PADDING = 12 +local DEFAULT_ITEM_HEIGHT = 84 +local DEFAULT_ITEM_PRESSED_BACKGROUND_COLOR = Constants.Color.GRAY5 +local DEFAULT_PRICE_COLOR = Constants.Color.GREEN_PRIMARY +local DEFAULT_ROBUX_ICON_SIZE = 12 +local DEFAULT_SEND_BUTTON_ICON_SIZE = 24 +local DEFAULT_SEND_BUTTON_LEFT_PADDING = 12 +local DEFAULT_SEND_BUTTON_RIGHT_PADDING = 25 +local DEFAULT_SEPARATOR_LINE_COLOR = Constants.Color.GRAY4 +local DEFAULT_TEXT_FONT = Enum.Font.SourceSans + +local GAME_LOADING_ICON = "rbxasset://textures/ui/LuaApp/icons/ic-game.png" +local GAME_BORDER_ICON = "rbxasset://textures/ui/LuaChat/graphic/gr-game-border-60x60.png" +local ROBUX_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-robux.png" +local SEND_BUTTON_ICON = "rbxasset://textures/ui/LuaChat/icons/icon-share-game-24x24.png" +local SENT_BUTTON_ICON = "rbxasset://textures/ui/LuaChat/icons/icon-share-game-pressed-24x24.png" + +local SharedGameItem = Roact.PureComponent:extend("SharedGameItem") + +SharedGameItem.defaultProps = { + backgroundColor = DEFAULT_BACKGROUND_COLOR, + gameAdditionalLabelHeight = DEFAULT_GAME_ADDITIONAL_INFO_LABEL_HEIGHT, + gameAdditionalLabelTextSize = DEFAULT_GAME_ADDITIONAL_INFO_LABEL_TEXT_SIZE, + gameCreatorLabelBottomPadding = DEFAULT_GAME_CREATOR_LABEL_BOTTOM_PADDING, + gameCreatorLabelColor = DEFAULT_GAME_CREATOR_LABEL_COLOR, + gameCreatorLabelHeight = DEFAULT_GAME_CREATOR_LABEL_HEIGHT, + gameCreatorLabelTopPadding = DEFAULT_GAME_CREATOR_LABEL_TOP_PADDING, + gameCreatorLabelTextSize = DEFAULT_GAME_CREATOR_LABEL_TEXT_SIZE, + gameIconSize = DEFAULT_GAME_ICON_SIZE, + gameIconLeftPadding = DEFAULT_GAME_ICON_LEFT_PADDING, + gameIconRightPadding = DEFAULT_GAME_ICON_RIGHT_PADDING, + gameIconTopPadding = DEFAULT_GAME_ICON_TOP_PADDING, + gameNameLabelColor = DEFAULT_GAME_NAME_LABEL_COLOR, + gameNameLabelHeight = DEFAULT_GAME_NAME_LABEL_HEIGHT, + gameNameLabelTextSize = DEFAULT_GAME_NAME_LABEL_TEXT_SIZE, + itemPressedBackgroundColor = DEFAULT_ITEM_PRESSED_BACKGROUND_COLOR, + itemHeight = DEFAULT_ITEM_HEIGHT, + priceColor = DEFAULT_PRICE_COLOR, + robuxIconSize = DEFAULT_ROBUX_ICON_SIZE, + sendButtonIconSize = DEFAULT_SEND_BUTTON_ICON_SIZE, + sendButtonLeftPadding = DEFAULT_SEND_BUTTON_LEFT_PADDING, + sendButtonRightPadding = DEFAULT_SEND_BUTTON_RIGHT_PADDING, + separatorLineColor = DEFAULT_SEPARATOR_LINE_COLOR, + textFont = DEFAULT_TEXT_FONT +} + +function SharedGameItem:init() + self.state = { + gameItemDown = false, + gameItemActivated = false, + sendButtonDown = false, + sendButtonActivated = false, + } + + self.creatorName = nil + self.gameNameTextLabel = nil + self.gameCreatorTextLabel = nil + + self.onGameButtonActivated = function() + self:openGameDetails() + end + + self.onSendButtonInputBegan = function(_, inputObject) + if (inputObject.UserInputType == Enum.UserInputType.Touch or + inputObject.UserInputType == Enum.UserInputType.MouseButton1) and + inputObject.UserInputState == Enum.UserInputState.Begin then + self:onSendButtonDown() + end + end + + self.onSendButtonInputEnded = function(_, inputObject) + self:onSendButtonUp() + end + + self.onGameItemInputBegan = function(_, inputObject) + if (inputObject.UserInputType == Enum.UserInputType.Touch or + inputObject.UserInputType == Enum.UserInputType.MouseButton1) and + inputObject.UserInputState == Enum.UserInputState.Begin then + self:onGameItemDown() + end + end + + self.onGameItemInputEnded = function(_, inputObject) + self:onGameItemUp() + end +end + +function SharedGameItem:onSendButtonDown() + if not self.state.sendButtonDown then + self:eventDisconnect() + + self.userInputServiceCon = UserInputService.InputEnded:Connect(function() + self:onSendButtonUp() + end) + + self:setState({ + sendButtonDown = true, + sendButtonActivated = false, + }) + end +end + +function SharedGameItem:onSendButtonUp(buttonActivated) + if self.state.sendButtonDown or self.state.sendButtonActivated ~= buttonActivated then + self:setState({ + sendButtonDown = false, + sendButtonActivated = buttonActivated, + }) + end + + self:eventDisconnect() +end + +function SharedGameItem:onGameItemDown() + if not self.state.gameItemDown then + self:eventDisconnect() + + self.userInputServiceCon = UserInputService.InputEnded:Connect(function() + self:onGameItemUp() + end) + + self:setState({ + gameItemDown = true, + gameItemActivated = false, + }) + end +end + +function SharedGameItem:onGameItemUp(buttonActivated) + if self.state.gameItemDown or self.state.gameItemActivated ~= buttonActivated then + self:setState({ + gameItemDown = false, + gameItemActivated = buttonActivated, + }) + end + + self:eventDisconnect() +end + +function SharedGameItem:openGameDetails() + local notificationType = GuiService:GetNotificationTypeList().VIEW_GAME_DETAILS_ANIMATED + GuiService:BroadcastNotification(string.format("%d", self.props.game.placeId), notificationType) +end + +function SharedGameItem:render() + local activeConversationId = self.props.activeConversationId + local analytics = self.props.analytics + local backgroundColor = self.props.backgroundColor + local gameAdditionalLabelHeight = self.props.gameAdditionalLabelHeight + local gameAdditionalLabelTextSize = self.props.gameAdditionalLabelTextSize + local gameCreatorLabelBottomPadding = self.props.gameCreatorLabelBottomPadding + local gameCreatorLabelColor = self.props.gameCreatorLabelColor + local gameCreatorLabelHeight = self.props.gameCreatorLabelHeight + local gameCreatorLabelTextSize = self.props.gameCreatorLabelTextSize + local gameCreatorLabelTopPadding = self.props.gameCreatorLabelTopPadding + local gameIconLeftPadding = self.props.gameIconLeftPadding + local gameIconRightPadding = self.props.gameIconRightPadding + local gameIconSize = self.props.gameIconSize + local gameIconTopPadding = self.props.gameIconTopPadding + local gameIconWidth = gameIconSize + gameIconLeftPadding + gameIconRightPadding + local gameNameLabelHeight = self.props.gameNameLabelHeight + local gameNameLabelColor = self.props.gameNameLabelColor + local gameNameLabelTextSize = self.props.gameNameLabelTextSize + local gameThumbnails = self.props.gameThumbnails + local gameUrl = self.props.game.url + local itemHeight = self.props.itemHeight + local itemPressedBackgroundColor = self.props.itemPressedBackgroundColor + local isSharing = self.props.isSharing + local placeId = tostring(self.props.game.placeId) + local playable = self.props.game.isPlayable + local priceColor = self.props.priceColor + local robuxIconSize = self.props.robuxIconSize + local sendButtonIconSize = self.props.sendButtonIconSize + local sendButtonLeftPadding = self.props.sendButtonLeftPadding + local sendButtonRightPadding = self.props.sendButtonRightPadding + local sendButtonWidth = sendButtonIconSize + sendButtonLeftPadding + sendButtonRightPadding + local separatorLineColor = self.props.separatorLineColor + local textFont = self.props.textFont + + local additionalInfoFrameHeight = gameAdditionalLabelHeight + gameCreatorLabelBottomPadding + local showPrice = self.props.game.price ~= nil and playable + local showAdditionalInfo = showPrice or (not playable) + local gameIcon = gameThumbnails[placeId] or GAME_LOADING_ICON + local gameInfoHeight = showAdditionalInfo and + (gameNameLabelHeight + gameCreatorLabelHeight + 2 * gameCreatorLabelBottomPadding + gameAdditionalLabelHeight) or + (gameNameLabelHeight + gameCreatorLabelHeight + gameCreatorLabelBottomPadding) + + + return Roact.createElement("Frame", { + BackgroundColor3 = self.state.gameItemDown and itemPressedBackgroundColor or backgroundColor, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, itemHeight), + },{ + Separator = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0, 1), + BackgroundColor3 = separatorLineColor, + BorderSizePixel = 0, + Position = UDim2.new(0, gameIconWidth, 1, 0), + Size = UDim2.new(1, -gameIconWidth, 0, 1), + }), + + Game = Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, itemHeight), + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + + GameButtonContainer = Roact.createElement("TextButton", { + BackgroundTransparency = 1, + LayoutOrder = 1, + Size = UDim2.new(1, -sendButtonWidth, 1, 0), + Text = "", + + [Roact.Event.Activated] = self.onGameButtonActivated, + [Roact.Event.InputBegan] = self.onGameItemInputBegan, + [Roact.Event.InputEnded] = self.onGameItemInputEnded, + },{ + Icon = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = gameIcon, + Position = UDim2.new(0, gameIconLeftPadding, 0, gameIconTopPadding), + Size = UDim2.new(0, gameIconSize, 0, gameIconSize), + }, { + RoundCornerOverlay = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = GAME_BORDER_ICON, + Size = UDim2.new(0, gameIconSize, 0, gameIconSize), + }), + }), + + GameInfo = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Position = UDim2.new(0, gameIconWidth, 0.5, 0), + Size = UDim2.new(1, -gameIconWidth, 0, gameInfoHeight), + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + + Name = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = textFont, + LayoutOrder = 1, + Size = UDim2.new(1, 0, 0, gameNameLabelHeight), + Text = self.props.game.name, + TextColor3 = gameNameLabelColor, + TextSize = gameNameLabelTextSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + [Roact.Ref] = function(rbx) + if rbx then + self.gameNameTextLabel = rbx + end + end, + }), + + Creator = Roact.createElement(LocalizedTextLabel, { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = textFont, + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, gameCreatorLabelHeight + gameCreatorLabelTopPadding), + Text = {"Feature.Chat.ShareGameToChat.By", creatorName = self.props.game.creatorName}, + TextColor3 = gameCreatorLabelColor, + TextSize = gameCreatorLabelTextSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Bottom, + + [Roact.Ref] = function(rbx) + if rbx then + self.gameCreatorTextLabel = rbx + self.creatorName = rbx.Text + end + end + }), + + NotAvailableTip = not playable and Roact.createElement(LocalizedTextLabel, { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = textFont, + LayoutOrder = 3, + Size = UDim2.new(1, 0, 0, additionalInfoFrameHeight), + Text = "Feature.Chat.ShareGameToChat.GameNotAvailable", + TextColor3 = gameCreatorLabelColor, + TextSize = gameAdditionalLabelTextSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Bottom, + }), + + GamePrice = showPrice and Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = 3, + Size = UDim2.new(1, 0, 0, additionalInfoFrameHeight), + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Bottom, + Padding = UDim.new(0, 3), + }), + + RobuxIcon = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = ROBUX_ICON, + LayoutOrder = 1, + ScaleType = Enum.ScaleType.Fit, + Size = UDim2.new(0, robuxIconSize, 0, robuxIconSize), + }), + + Price = Roact.createElement("TextLabel",{ + BackgroundTransparency = 1, + Size = UDim2.new(1, -15, 1, 0), + Font = DEFAULT_TEXT_FONT, + LayoutOrder = 2, + Text = formatInteger(self.props.game.price), + TextColor3 = priceColor, + TextSize = gameAdditionalLabelTextSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Bottom, + }, { + padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(3, 0), + }), + }), + }), + }) + }), + + SendButtonContainer = Roact.createElement("TextButton", { + Active = not isSharing, + BackgroundTransparency = 1, + ClipsDescendants = true, + LayoutOrder = 2, + Position = UDim2.new(1, -sendButtonWidth, 0, 0), + Size = UDim2.new(0, sendButtonWidth, 1, 0), + Text = "", + + [Roact.Event.InputBegan] = self.onSendButtonInputBegan, + [Roact.Event.InputEnded] = self.onSendButtonInputEnded, + [Roact.Event.Activated] = function() + if not isSharing then + self.props.shareGameToChat(activeConversationId, analytics, placeId, gameUrl) + end + end, + },{ + SendButton = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = false, + Image = self.state.sendButtonDown and SENT_BUTTON_ICON or SEND_BUTTON_ICON, + Position = UDim2.new(0, sendButtonLeftPadding, 0.5, 0), + Size = UDim2.new(0, sendButtonIconSize, 0, sendButtonIconSize), + }), + }), + }) + }) +end + +function SharedGameItem:didMount() + local function resizeGameName() + self.gameNameTextLabel.Text = Text.Truncate(self.props.game.name, self.props.textFont, + self.gameNameTextLabel.TextSize, self.gameNameTextLabel.AbsoluteSize.X, "...") + end + + local function resizeGameCreator() + self.gameCreatorTextLabel.Text = Text.Truncate(self.creatorName, self.props.textFont, + self.gameCreatorTextLabel.TextSize, self.gameCreatorTextLabel.AbsoluteSize.X, "...") + end + + resizeGameName() + resizeGameCreator() + + self.connections = {} + table.insert(self.connections, self.gameNameTextLabel:GetPropertyChangedSignal("Text"):Connect(resizeGameName)) + table.insert(self.connections, self.gameNameTextLabel:GetPropertyChangedSignal("AbsoluteSize"):Connect(resizeGameName)) + table.insert(self.connections, self.gameCreatorTextLabel:GetPropertyChangedSignal("Text"):Connect(resizeGameCreator)) + table.insert( + self.connections, + self.gameCreatorTextLabel:GetPropertyChangedSignal("AbsoluteSize"):Connect(resizeGameCreator) + ) +end + +function SharedGameItem:willUnmount() + for _, connection in pairs(self.connections) do + connection:Disconnect() + end + + self:eventDisconnect() +end + +function SharedGameItem:eventDisconnect() + if self.userInputServiceCon then + self.userInputServiceCon:Disconnect() + self.userInputServiceCon = nil + end +end + +SharedGameItem = RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + activeConversationId = state.ChatAppReducer.ActiveConversationId, + gameThumbnails = state.GameThumbnails, + isSharing = state.ChatAppReducer.ShareGameToChatAsync.sharingGame, + } + end, + function(dispatch) + return { + shareGameToChat = function(activeConversationId, analytics, placeId, url) + analytics.reportShareGameToChatFromChat(activeConversationId, tostring(placeId)) + return dispatch(ConversationActions.ShareGame(activeConversationId, url)) + end, + } + end +)(SharedGameItem) + +SharedGameItem = RoactServices.connect({ + analytics = RoactAnalyticsSharedGameItem, +})(SharedGameItem) + +return SharedGameItem \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/SharedGameList.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/SharedGameList.lua new file mode 100644 index 0000000..d05d2b0 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/SharedGameList.lua @@ -0,0 +1,160 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local LocalizedTextLabel = require(LuaApp.Components.LocalizedTextLabel) +local LoadingIndicator = require(LuaApp.Components.LoadingIndicator) +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) +local SharedGameItem = require(LuaChat.Components.SharedGameItem) +local ShareGameToChatThunks = require(LuaChat.Actions.ShareGameToChatFromChat.ShareGameToChatFromChatThunks) + +local DEFAULT_BACKGROUND_COLOR = Constants.Color.GRAY6 +local DEFAULT_ITEM_HEIGHT = 84 +local DEFAULT_TIPS_LABEL_COLOR = Constants.Color.GRAY2 +local DEFAULT_TIPS_LABEL_FONT = Enum.Font.SourceSans +local DEFAULT_TIPS_LABEL_HEIGHT = 25 +local DEFAULT_TIPS_LABEL_TEXT_SIZE = 20 +local DEFAULT_TIPS_LABEL_TOP_MARGIN = 30 + +local SCROLLING_FRAME_IMAGE = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png" + +local SharedGameList = Roact.PureComponent:extend("SharedGameList") + +SharedGameList.defaultProps = { + backgroundColor = DEFAULT_BACKGROUND_COLOR, + itemHeight = DEFAULT_ITEM_HEIGHT, + tipsLabelColor = DEFAULT_TIPS_LABEL_COLOR, + tipsLabelFont = DEFAULT_TIPS_LABEL_FONT, + tipsLabelHeight = DEFAULT_TIPS_LABEL_HEIGHT, + tipsLabelTextSize = DEFAULT_TIPS_LABEL_TEXT_SIZE, + tipsLabelTopMargin = DEFAULT_TIPS_LABEL_TOP_MARGIN, +} + +function SharedGameList:init() + self.gamesList = nil + self.isLoading = false + + self.onScrollingFrameRef = function(rbx) + if rbx then + self.gamesList = rbx + else + warn("can not capture scrolling frame") + end + end + + self:ShouldFetchGames(self.props) +end + +function SharedGameList:render() + local backgroundColor = self.props.backgroundColor + local frameHeight = self.props.frameHeight + local gamesDetailInfos = self.props.gamesInfo + local gameSorts = self.props.gameSorts + local itemHeight = self.props.itemHeight + local sortName = self.props.gameSort + local tipsLabelColor = self.props.tipsLabelColor + local tipsLabelHeight = self.props.tipsLabelHeight + local tipsLabelFont = self.props.tipsLabelFont + local tipsLabelTextSize = self.props.tipsLabelTextSize + local tipsLabelTopMargin = self.props.tipsLabelTopMargin + + local itemsCount = 0 + local gamesListItems = {} + + if not self.isLoading then + gamesListItems["Layout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + VerticalAlignment = Enum.VerticalAlignment.Center, + }) + + local games = gameSorts[sortName] + itemsCount = #games.placeIds + for index, placeId in ipairs(games.placeIds) do + gamesListItems[index] = Roact.createElement(SharedGameItem, { + itemHeight = itemHeight, + game = gamesDetailInfos[placeId], + }) + end + end + + local contentHeight = itemsCount * itemHeight + + return Roact.createElement("Frame", { + BackgroundColor3 = backgroundColor, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + }, { + GamesList = itemsCount ~= 0 and Roact.createElement("ScrollingFrame", { + BackgroundTransparency = 1, + BottomImage = SCROLLING_FRAME_IMAGE, + BorderSizePixel = 0, + CanvasSize = UDim2.new(1, 0, 0, contentHeight), + MidImage = SCROLLING_FRAME_IMAGE, + Size = contentHeight > frameHeight and UDim2.new(1, 0, 1, 0) or UDim2.new(1, 0, 0, contentHeight), + ScrollBarThickness = 5, + ScrollingDirection = Enum.ScrollingDirection.Y, + TopImage = SCROLLING_FRAME_IMAGE, + + [Roact.Ref] = self.onScrollingFrameRef, + }, gamesListItems), + + Indicator = self.isLoading and Roact.createElement(LoadingIndicator, { + AnchorPoint = Vector2.new(0.5, 0), + Position = UDim2.new(0.5, 0, 0, tipsLabelTopMargin), + }), + + NoGamesTip = ((not self.isLoading) and itemsCount == 0) and Roact.createElement(LocalizedTextLabel, { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = tipsLabelFont, + Position = UDim2.new(0, 0, 0, tipsLabelTopMargin), + Size = UDim2.new(1, 0, 0, tipsLabelHeight), + Text = Constants.SharedGamesConfig.SortsAttribute[sortName].ERROR_TIP_LOCALIZATION_KEY, + TextColor3 = tipsLabelColor, + TextSize = tipsLabelTextSize, + }), + }) +end + +function SharedGameList:didUpdate() + if self.gamesList then + self.gamesList.CanvasPosition = Vector2.new(0, 0) + end +end + +function SharedGameList:willUpdate(newProps) + self:ShouldFetchGames(newProps) +end + +function SharedGameList:ShouldFetchGames(props) + local gameSorts = props.gameSorts + local sortName = props.gameSort + local games = gameSorts[sortName] + + self.isLoading = false + if games == nil or games.placeIds == nil then + if not ShareGameToChatThunks.HasGameFetchRequestCompleted(sortName, props.shareGameToChatAsync) then + self.isLoading = true + self.props.fetchGames(sortName, Constants.SharedGamesConfig.Thumbnail.FETCHED_SIZE) + end + end +end + +return RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + gamesInfo = state.ChatAppReducer.SharedGamesInfo, + gameSorts = state.ChatAppReducer.SharedGameSorts, + shareGameToChatAsync = state.ChatAppReducer.ShareGameToChatAsync, + } + end, + function(dispatch) + return { + fetchGames = function(gameSortName, fetchedThumbnailSize) + return dispatch(ShareGameToChatThunks.FetchGames(gameSortName, fetchedThumbnailSize)) + end, + } + end +)(SharedGameList) \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/TextButton.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/TextButton.lua new file mode 100644 index 0000000..8bc7392 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/TextButton.lua @@ -0,0 +1,74 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local Signal = require(Common.Signal) +local Text = require(LuaChat.Text) +local getInputEvent = require(LuaChat.Utils.getInputEvent) + +local FONT = Constants.Font.TITLE +local TEXT_SIZE = Constants.Font.FONT_SIZE_18 +local X_PADDING = 8 + +local TextButton = {} + +TextButton.__index = TextButton + +function TextButton.new(appState, name, textKey) + local self = {} + + self.enabled = true + + local text = appState.localization:Format(textKey) + + local textWidth = Text.GetTextWidth(text, FONT, TEXT_SIZE) + + self.rbx = Create.new "TextButton" { + Name = name, + BackgroundTransparency = 1, + Text = "", + Size = UDim2.new(0, textWidth + X_PADDING, 1, 0), + + Create.new "TextLabel" { + Name = "Label", + Size = UDim2.new(0, textWidth, 0, TEXT_SIZE), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundTransparency = 1, + Text = text, + Font = FONT, + TextSize = TEXT_SIZE, + TextColor3 = Constants.Color.WHITE, + }, + } + + self.Pressed = Signal.new() + + getInputEvent(self.rbx):Connect(function() + if not self.enabled then + return + end + + self.Pressed:Fire() + end) + + setmetatable(self, TextButton) + + return self +end + +function TextButton:SetEnabled(value) + if value then + self.rbx.Label.TextTransparency = 0 + else + self.rbx.Label.TextTransparency = 0.7 + end + + self.enabled = value +end + +return TextButton diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/TextInputEntry.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/TextInputEntry.lua new file mode 100644 index 0000000..2142b8a --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/TextInputEntry.lua @@ -0,0 +1,178 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local Signal = require(Common.Signal) +local getInputEvent = require(LuaChat.Utils.getInputEvent) + +local ListEntry = require(LuaChat.Components.ListEntry) + +local FFlagTextBoxOverrideManualFocusRelease = settings():GetFFlag("TextBoxOverrideManualFocusRelease") + +local ICON_CELL_WIDTH = 60 +local CLEAR_TEXT_WIDTH = 48 +local HEIGHT = 48 + +local TextInputEntry = {} + +function TextInputEntry.new(appState, icon, placeholder) + local self = {} + self.connections = {} + setmetatable(self, {__index = TextInputEntry}) + + local size = 24 + local iconWidth = 0 + + local listEntry = ListEntry.new(appState, HEIGHT) + self.listEntry = listEntry + self.rbx = listEntry.rbx + self.placeholder = placeholder + + if icon then + iconWidth = ICON_CELL_WIDTH + local iconImageLabel = Create.new"Frame" { + Name = "Icon", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, iconWidth, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + Create.new"ImageLabel" { + Name = "IconImage", + BackgroundTransparency = 1, + Size = UDim2.new(0, size, 0, size), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Image = icon, + BorderSizePixel = 0, + }, + } + iconImageLabel.Parent = self.rbx + end + + local textBox = Create.new"TextBox" { + Name = "TextBox", + BackgroundTransparency = 1, + Size = UDim2.new(1, -iconWidth - CLEAR_TEXT_WIDTH, 1, 0), + Position = UDim2.new(0, iconWidth, 0, 0), + TextSize = Constants.Font.FONT_SIZE_18, + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSans, + Text = "", + PlaceholderText = placeholder or "", + PlaceholderColor3 = Constants.Color.GRAY3, + TextXAlignment = Enum.TextXAlignment.Left, + OverlayNativeInput = true, + ClearTextOnFocus = false, + ClipsDescendants = true, + } + if FFlagTextBoxOverrideManualFocusRelease then + textBox.ManualFocusRelease = true + end + textBox.Parent = self.rbx + + local clearButton = Create.new"ImageButton" { + Name = "Clear", + BackgroundTransparency = 1, + ImageTransparency = 1, + Size = UDim2.new(0, CLEAR_TEXT_WIDTH, 0, CLEAR_TEXT_WIDTH), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(1, -CLEAR_TEXT_WIDTH/2, 0.5, 0), + AutoButtonColor = false, + + Create.new"ImageLabel" { + Name = "ClearImage", + BackgroundTransparency = 1, + ImageTransparency = 0, + Size = UDim2.new(0, 16, 0, 16), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Image = "rbxasset://textures/ui/LuaChat/icons/ic-clear-solid.png", + }, + + } + + clearButton.Parent = self.rbx + + local clearButtonConnection = getInputEvent(clearButton):Connect(function() + self.textBoxComponent.Text = "" + end) + table.insert(self.connections, clearButtonConnection) + + local divider = Create.new"Frame"{ + Name = "Divider", + BackgroundColor3 = Constants.Color.GRAY4, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 1), + Position = UDim2.new(0, 0, 1, -1), + } + divider.Parent = self.rbx + + self.textBoxComponent = textBox + self.value = textBox.Text + self.textBoxChanged = Signal.new() + self.textBoxFocusLost = Signal.new() + + local function updateClearButtonVisibility() + local visible = (self.textBoxComponent.Text ~= "") + clearButton.Visible = visible + end + + updateClearButtonVisibility() + + local textChangedConnection = textBox:GetPropertyChangedSignal("Text"):Connect(function() + self.value = self.textBoxComponent.Text + self.textBoxChanged:Fire(self.value) + updateClearButtonVisibility() + end) + table.insert(self.connections, textChangedConnection) + + local focusedConnection = textBox.Focused:Connect(updateClearButtonVisibility) + table.insert(self.connections, focusedConnection) + local focusLostConnection = textBox.FocusLost:Connect(function() + self.textBoxFocusLost:Fire() + updateClearButtonVisibility() + end) + table.insert(self.connections, focusLostConnection) + + return self +end + +function TextInputEntry:SanitizeInput(sanitizeFunc) + self.value = sanitizeFunc(self.value) + self.textBoxComponent.Text = self.value + return self.value +end + +function TextInputEntry:ReleaseFocus() + self.textBoxComponent:ReleaseFocus() +end + +function TextInputEntry:ShowDivider(show) + self.rbx.Divider.Visible = show +end + +function TextInputEntry:Update(value) + if value ~= self.value then + self.rbx.TextBox.Text = value + if self.placeholder == nil and value ~= "" then + self.placeholder = value + self.rbx.TextBox.PlaceholderText = value + end + end +end + +function TextInputEntry:Destruct() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + + self.listEntry:Destruct() + self.rbx:Destroy() +end + +return TextInputEntry diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/ToastComponent.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ToastComponent.lua new file mode 100644 index 0000000..68dcf59 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/ToastComponent.lua @@ -0,0 +1,97 @@ +local Modules = script.Parent.Parent + +local Create = require(Modules.Create) +local Constants = require(Modules.Constants) + +local ToastComplete = require(Modules.Actions.ToastComplete) + +local ToastComponent = {} +ToastComponent.__index = ToastComponent + +local POSITION_HIDE = UDim2.new(0.5, 0, 1, 72) +local POSITION_SHOW = UDim2.new(0.5, 0, 1, -56-48) +local TEXT_SIZE = Constants.Font.FONT_SIZE_16 +local PADDING = 12 + +function ToastComponent.new(appState, route) + local self = {} + self.appState = appState + self.route = route + setmetatable(self, ToastComponent) + + self.rbx = Create.new"Frame" { + Name = "ToastComponent", + Size = UDim2.new(1, -48*2, 0, 56), + Position = POSITION_HIDE, + AnchorPoint = Vector2.new(0.5, 0), + BackgroundTransparency = 0.1, + BackgroundColor3 = Constants.Color.GRAY1, + BorderSizePixel = 0, + Visible = true, + Create.new"TextLabel" { + Name = "Message", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = Enum.Font.SourceSans, + TextSize = TEXT_SIZE, + TextColor3 = Constants.Color.WHITE, + Text = "", + Size = UDim2.new(1, 0, 1, 0), + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + } + } + + self.appState.store.Changed:Connect(function(current, previous) + if current ~= previous then + self:Update(current.ChatAppReducer.Toast) + end + end) + + return self +end + +function ToastComponent:Update(toast) + if toast == nil then + return + end + + -- We don't want to show the toast if another one with the same id is being shown. + if self.toast and (self.toast.id == toast.id) then + return + end + + self.toast = toast + self:Show(toast) +end + +function ToastComponent:Hide() + self.rbx:TweenPosition(POSITION_HIDE, Enum.EasingDirection.In, + Enum.EasingStyle.Quad, 0.25, false, function(status) + + self.appState.store:dispatch(ToastComplete(self.toast)) + self.toast = nil + end) +end + +function ToastComponent:Show(toast) + + local message = toast.messageKey ~= nil and + self.appState.localization:Format(toast.messageKey, toast.messageArguments) or "" + self.rbx.Message.Text = message + + local textWidth = self.rbx.Message.TextBounds.X + + self.rbx.Size = UDim2.new(0, textWidth + PADDING * 2, 0, 56) + self.rbx.Position = POSITION_HIDE + + self.rbx:TweenPosition(POSITION_SHOW, Enum.EasingDirection.Out, + Enum.EasingStyle.Quad, 0.25, false, function(status) + wait(2) + if self.toast.id == toast.id then + self:Hide() + end + end) +end + +return ToastComponent \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/TypingIndicator.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/TypingIndicator.lua new file mode 100644 index 0000000..e76510c --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/TypingIndicator.lua @@ -0,0 +1,105 @@ +local RunService = game:GetService("RunService") +local Workspace = game:GetService("Workspace") + +local LuaChat = script.Parent.Parent +local Create = require(LuaChat.Create) + +local INDICATOR_WIDTH = 60 +local INDICATOR_HEIGHT = 16 +local DOT_COUNT = 3 +local ANIMATION_SPEED_MULTIPLIER = 1.75 + +local TypingIndicator = {} + +local function makeDot() + return Create.new "ImageLabel" { + Name = "DotContainer", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + SizeConstraint = Enum.SizeConstraint.RelativeYY, + + Create.new "ImageLabel" { + Name = "Dot", + BackgroundTransparency = 1, + Image = "rbxasset://textures/ui/LuaChat/graphic/send-white.png", + ImageColor3 = Color3.new(0.5, 0.5, 0.5), + Size = UDim2.new(1, 0, 1, 0), + SizeConstraint = Enum.SizeConstraint.RelativeYY, + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5) + }, + } +end + +function TypingIndicator.new(appState, scale) + scale = scale or 1 + + local self = {} + self.connections = {} + + self.rbx = Create.new "Frame" { + Name = "TypingIndicator", + BackgroundTransparency = 1, + Size = UDim2.new(0, scale * INDICATOR_WIDTH, 0, scale * INDICATOR_HEIGHT) + } + + self.dots = {} + + for i = 1, DOT_COUNT do + local value = (i - 1) / (DOT_COUNT - 1) + + local dot = makeDot() + dot.Position = UDim2.new(value, 0, 0, 0) + dot.AnchorPoint = Vector2.new(value, 0) + dot.Parent = self.rbx + + table.insert(self.dots, dot) + end + + setmetatable(self, TypingIndicator) + + do + local connection = self.rbx.AncestryChanged:Connect(function(object, parent) + if object == self.rbx and parent == nil then + self:Destroy() + end + end) + table.insert(self.connections, connection) + end + + do + local connection = RunService.RenderStepped:Connect(function() + if not self.rbx.Visible then + return + end + + local time = (Workspace.DistributedGameTime * ANIMATION_SPEED_MULTIPLIER) % #self.dots + + for i, dot in ipairs(self.dots) do + local size = 0.5 + if time >= i - 1 and time <= i then + size = 0.5 + 0.5 * math.sin(math.pi * (time % 1)) + end + + dot.Dot.Size = UDim2.new(size, 0, size, 0) + end + end) + table.insert(self.connections, connection) + end + + return self +end + +function TypingIndicator:Destroy() + self.rbx:Destroy() + + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + + self.connections = {} +end + +TypingIndicator.__index = TypingIndicator + +return TypingIndicator \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserChatBubble.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserChatBubble.lua new file mode 100644 index 0000000..ce61800 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserChatBubble.lua @@ -0,0 +1,278 @@ +local Players = game:GetService("Players") +local CoreGui = game:GetService("CoreGui") + +local LuaApp = CoreGui.RobloxGui.Modules.LuaApp + +local LuaChat = script.Parent.Parent + +local Create = require(LuaChat.Create) +local Constants = require(LuaChat.Constants) +local Text = require(LuaChat.Text) +local FormFactor = require(LuaApp.Enum.FormFactor) + +local BUBBLE_PADDING = 10 +local TEXT_BUBBLE_X_PADDING = 54 +local TEXT_BUBBLE_X_TABLET_ADDITIONAL_PADDING = 112 + +local function isOutgoingMessage(message) + local localUserId = tostring(Players.LocalPlayer.UserId) + return message.senderTargetId == localUserId +end + +local function isMessageSending(conversation, message) + if conversation and conversation.sendingMessages then + return conversation.sendingMessages:Get(message.id) ~= nil + end + return false +end + +local UserChatBubble = {} + +UserChatBubble.__index = UserChatBubble + +function UserChatBubble.new(appState, message, newContent) + + local self = {} + setmetatable(self, UserChatBubble) + + local conversationId = message.conversationId + local isSending = isMessageSending(appState.store:getState().ChatAppReducer.Conversations[conversationId], message) + + local state = appState.store:getState() + local user = state.Users[message.senderTargetId] + local username = user and user.name or "unknown user" + + self.appState = appState + self.paddingObject = nil + self.message = message + self.bubbleType = "UserChatBubble" + self.connections = {} + + self.displayMessage = newContent and newContent or self.message.content + + self.textContent = Create.new "TextLabel" { + Name = "TextContent", + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + TextColor3 = Constants.Color.WHITE, + TextSize = Constants.Font.FONT_SIZE_18, + Text = "", + Font = Enum.Font.SourceSans, + TextWrapped = true, + TextXAlignment = Enum.TextXAlignment.Left, + } + + self.tail = Create.new "ImageLabel" { + Name = "Tail", + Size = UDim2.new(0, 6, 0, 6), + BackgroundTransparency = 1, + } + + self.bubble = Create.new "ImageLabel" { + Name = "Bubble", + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(1, 0), + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(10, 10, 11, 11), + LayoutOrder = 2, + + Create.new "Frame" { + Name = "Content", + BackgroundTransparency = 1, + Size = UDim2.new(1, -BUBBLE_PADDING * 2, 1, -BUBBLE_PADDING * 2), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + + self.textContent, + }, + + self.tail, + } + + self.usernameLabel = Create.new "TextLabel" { + Name = "UsernameLabel", + Font = Enum.Font.SourceSans, + TextSize = Constants.Font.FONT_SIZE_12, + Visible = false, + BackgroundTransparency = 1, + Size = UDim2.new(1, -56, 0, 16), + Position = UDim2.new(0, 56, 0, 0), + TextColor3 = Constants.Color.GRAY2, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + Text = username, + } + + self.bubbleContainer = Create.new "Frame" { + Name = "BubbleContainer", + BackgroundTransparency = 1, + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, 1), + + self.bubble, + self.usernameLabel, + } + + self.rbx = Create.new "Frame" { + Name = "Message", + BackgroundTransparency = 1, + Size = UDim2.new(0, 0, 0, 0), + + Create.new "UIListLayout" { + SortOrder = Enum.SortOrder.LayoutOrder, + }, + + self.bubbleContainer, + } + + -- 'isOutgoing' means "is sent by the local user". This function separates the tail position & color + if isOutgoingMessage(message) then + local bubbleColor = isSending and Constants.Color.BLUE_DISABLED or Constants.Color.BLUE_PRIMARY + local textTransparency = isSending and Constants.Color.ALPHA_SHADOW_PRIMARY or 0 + + self.tail.AnchorPoint = Vector2.new(0, 0) + self.tail.Position = UDim2.new(1, 0, 0, 0) + self.tail.ImageColor3 = bubbleColor + + self.bubble.ImageColor3 = bubbleColor + self.bubble.AnchorPoint = Vector2.new(1, 0) + self.bubble.Position = UDim2.new(1, -10, 0, 0) + + self.textContent.TextColor3 = Constants.Color.WHITE + self.textContent.TextTransparency = textTransparency + + self.rbx.UIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Right + else + self.tail.AnchorPoint = Vector2.new(1, 0) + self.tail.Position = UDim2.new(0, 0, 0, 0) + self.tail.ImageColor3 = Color3.new(1, 1, 1) + + self.bubble.ImageColor3 = Color3.new(1, 1, 1) + self.bubble.AnchorPoint = Vector2.new(0, 0) + self.bubble.Position = UDim2.new(0, TEXT_BUBBLE_X_PADDING, 0, 0) + + self.textContent.TextColor3 = Constants.Color.GRAY1 + + self.rbx.UIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Left + end + self:Update(message) + + local connection = self.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + self:Resize() + end) + table.insert(self.connections, connection) + + return self +end + +function UserChatBubble:Resize() + local padding = (TEXT_BUBBLE_X_PADDING + BUBBLE_PADDING) * 2 -- * 2 for both sides + if self.appState.store:getState().FormFactor == FormFactor.TABLET then + padding = padding + TEXT_BUBBLE_X_TABLET_ADDITIONAL_PADDING + end + + local viewportWidth = self.rbx.AbsoluteSize.X + + local textWidth = (viewportWidth - padding) + + local textBounds = Text.GetTextBounds(self.textContent.Text, self.textContent.Font, + self.textContent.TextSize, Vector2.new(textWidth, 10000)) + + + local doublePadding = BUBBLE_PADDING * 2 + self.bubble.Size = UDim2.new(0, textBounds.X + doublePadding, 0, textBounds.Y + doublePadding) + + local containerHeight = self.bubble.AbsoluteSize.Y + + if self.usernameLabel.Visible then + containerHeight = containerHeight + self.usernameLabel.AbsoluteSize.Y + end + + self.bubbleContainer.Size = UDim2.new(1, 0, 0, containerHeight) + + local height = 0 + for _, child in ipairs(self.rbx:GetChildren()) do + if child:IsA("GuiObject") then + height = height + child.AbsoluteSize.Y + end + end + + self.rbx.Size = UDim2.new(1, 0, 0, height) +end + +local function CreateFilteringText(appState, filteringTextKey, textColor) + return Create.new "Frame" { + Name = "ModeratedNotice", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 18), + LayoutOrder = 3, + + Create.new "TextLabel" { + Name = "ModeratedNoticeText", + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(1, -10, 0, 0), + AnchorPoint = Vector2.new(1, 0), + BackgroundTransparency = 1, + TextColor3 = textColor, + TextSize = Constants.Font.FONT_SIZE_14, + Text = appState.localization:Format(filteringTextKey), + Font = Enum.Font.SourceSans, + TextXAlignment = Enum.TextXAlignment.Right, + TextYAlignment = Enum.TextYAlignment.Top, + } + } +end + +function UserChatBubble:Update(message) + self.message = message + + if message.moderated then + self.displayMessage = string.gsub(self.displayMessage, "[^%s]", "#") + end + + self.textContent.Text = self.displayMessage + + local textColor + local filteringTextKey = nil + if message.moderated then + filteringTextKey = "Feature.Chat.Message.MessageContentModerated" + textColor = Constants.Color.RED_NEGATIVE + elseif message.filteredForReceivers then + filteringTextKey = "Feature.Chat.Message.Default" + textColor = Constants.Color.GRAY3 + end + + -- Either the message was moderated or it was filtered for some users (minors). + if filteringTextKey and (not self.moderatedText) then + self.moderatedText = CreateFilteringText(self.appState, filteringTextKey, textColor) + self.moderatedText.Parent = self.rbx + end + + -- There was an error and the message was not sent. + if (message.failed or message.moderated) and (not self.alertIcon) then + self.alertIcon = Create.new "ImageLabel" { + Name = "FailedIcon", + Size = UDim2.new(0, 18, 0, 18), + AnchorPoint = Vector2.new(1, 0.5), + Position = UDim2.new(0, -10, 0.5, 0), + ImageColor3 = Constants.Color.RED_PRIMARY, + BackgroundTransparency = 1, + Image = "rbxasset://textures/ui/LuaChat/icons/ic-alert.png" + } + self.alertIcon.Parent = self.bubble + end + + self:Resize() +end + +function UserChatBubble:Destruct() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + self.rbx:Destroy() +end + +return UserChatBubble \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserEntry.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserEntry.lua new file mode 100644 index 0000000..1916374 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserEntry.lua @@ -0,0 +1,168 @@ +local CoreGui = game:GetService("CoreGui") +local Players = game:GetService("Players") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local Signal = require(Common.Signal) + +local User = require(LuaApp.Models.User) + +local Components = LuaChat.Components +local ListEntry = require(Components.ListEntry) +local UserThumbnail = require(Components.UserThumbnail) + +local ICON_CELL_WIDTH = 60 +local HEIGHT = 56 + +local function userPresenceToText(localization, presence, lastLocation) + if presence == User.PresenceType.OFFLINE then + return localization:Format("Common.Presence.Label.Offline") + elseif presence == User.PresenceType.ONLINE then + return localization:Format("Common.Presence.Label.Online") + elseif presence == User.PresenceType.IN_GAME then + if lastLocation ~= nil then + return lastLocation + else + return localization:Format("Common.Presence.Label.Online") + end + elseif presence == User.PresenceType.IN_STUDIO then + if lastLocation ~= nil then + return lastLocation + else + return localization:Format("Common.Presence.Label.Online") + end + end +end + +local UserEntry = {} +UserEntry.__index = UserEntry + +function UserEntry.new(appState, user, icon, selected) + local self = { + user = user, + appState = appState, + } + setmetatable(self, UserEntry) + + self.icon = icon or "rbxasset://textures/ui/LuaChat/graphic/ic-checkbox.png" + + local listEntry = ListEntry.new(appState, HEIGHT) + self.rbx = listEntry.rbx + + local userThumbnail = UserThumbnail.new(appState, user.id, true) + userThumbnail.rbx.Size = UDim2.new(0, 36, 0, 36) + userThumbnail.rbx.Position = UDim2.new(0.5, 0, 0.5, 0) + userThumbnail.rbx.AnchorPoint = Vector2.new(0.5, 0.5) + self.userThumbnail = userThumbnail + + local userThumbnailFrame = Create.new"Frame" { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, ICON_CELL_WIDTH, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + userThumbnail.rbx, + } + userThumbnailFrame.Parent = self.rbx + + local label = Create.new"TextLabel" { + Name = "Label", + BackgroundTransparency = 1, + Size = UDim2.new(1, -ICON_CELL_WIDTH, 0.75, 0), + Position = UDim2.new(0, ICON_CELL_WIDTH, 0.5, 1 - Constants.Font.FONT_SIZE_18), + TextSize = Constants.Font.FONT_SIZE_18, + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSans, + Text = user.name, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + } + label.Parent = self.rbx + + local sublabel = Create.new"TextLabel" { + Name = "SubLabel", + BackgroundTransparency = 1, + Size = UDim2.new(1, -ICON_CELL_WIDTH, 0.35, 0), + Position = UDim2.new(0, ICON_CELL_WIDTH, 0.5, 1), + TextSize = Constants.Font.FONT_SIZE_14, + TextColor3 = Constants.Color.GRAY2, + Font = Enum.Font.SourceSans, + Text = userPresenceToText(appState.localization, user.presence, user.lastLocation), + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + } + sublabel.Parent = self.rbx + + local value = Create.new"ImageLabel" { + Name = "Icon", + BackgroundTransparency = 1, + Size = UDim2.new(0, 20, 0, 20), + Position = UDim2.new(1, -32, .5, 0), + Image = self.icon, + AnchorPoint = Vector2.new(0.5, 0.5), + Visible = user.id ~= tostring(Players.LocalPlayer.UserId), + } + value.Parent = self.rbx + + local divider = Create.new"Frame" { + Name = "Divider", + BackgroundColor3 = Constants.Color.GRAY4, + BorderSizePixel = 0, + Size = UDim2.new(1, -ICON_CELL_WIDTH, 0, 1), + Position = UDim2.new(0, ICON_CELL_WIDTH, 1, -1), + } + divider.Parent = self.rbx + + self:SetIsSelected(selected) + + self.tapped = Signal.new() + + listEntry.tapped:Connect(function() + self.tapped:Fire() + end) + + listEntry.beginHover:Connect(function() + userThumbnail.rbx.Overlay.ImageColor3 = Constants.Color.GRAY5 + end) + + listEntry.endHover:Connect(function() + userThumbnail.rbx.Overlay.ImageColor3 = Constants.Color.WHITE + end) + + return self +end + +function UserEntry:SetIsSelected(selected) + if selected then + self.rbx.Icon.Image = "rbxasset://textures/ui/LuaChat/graphic/ic-checkbox-on.png" + else + self.rbx.Icon.Image = self.icon + end +end + +function UserEntry:Update(user, selected) + self.user = user + self.rbx.Label.Text = user.name + self.rbx.SubLabel.Text = userPresenceToText(self.appState.localization, user.presence, user.lastLocation) + + self:SetIsSelected(selected) + + if user.id == tostring(Players.LocalPlayer.UserId) then + self.rbx.Icon.Visible = false + else + self.rbx.Icon.Visible = true + end + + self.userThumbnail:Update(user) +end + +function UserEntry:Destruct() + self.userThumbnail:Destruct() + self.rbx:Destroy() +end + +return UserEntry diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserList.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserList.lua new file mode 100644 index 0000000..155e00d --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserList.lua @@ -0,0 +1,146 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Create = require(LuaChat.Create) +local Functional = require(Common.Functional) +local Signal = require(Common.Signal) + +local Components = LuaChat.Components +local UserEntry = require(Components.UserEntry) + +local ICON_CELL_WIDTH = 60 + +local UserList = {} +UserList.__index = UserList + +function UserList.new(appState, icon, filter) + local self = { + appState = appState, + icon = icon, + filter = filter, + } + setmetatable(self, UserList) + + self.users = nil + self.userEntries = {} + + self.rbx = Create.new"Frame" { + Size = UDim2.new(1, 0, 0, 0), + BackgroundTransparency = 1, + Create.new"UIListLayout" { + Name = "ListLayout", + SortOrder = "LayoutOrder", + Padding = UDim.new(0, 0), + }, + } + + self.userSelected = Signal.new() + + return self +end + +function UserList:ResetFinalDivider() + local children = self.rbx:GetChildren() + local lastChild = nil + for i = 1, #children do + local child = children[#children-i+1] + if child:IsA("GuiObject") and child.Visible then + if lastChild == nil or lastChild.LayoutOrder < child.LayoutOrder then + lastChild = child + end + end + end + if lastChild then + local divider = lastChild.Divider + divider.Size = UDim2.new(1, -ICON_CELL_WIDTH, 0, 1) + divider.Position = UDim2.new(0, ICON_CELL_WIDTH, 1, -1) + end +end + +function UserList:FormatFinalDivider() + local children = self.rbx:GetChildren() + local lastChild = nil + for i = 1, #children do + local child = children[#children-i+1] + if child:IsA("GuiObject") and child.Visible then + if lastChild == nil or lastChild.LayoutOrder < child.LayoutOrder then + lastChild = child + end + end + end + if lastChild then + local divider = lastChild.Divider + divider.Size = UDim2.new(1, -ICON_CELL_WIDTH, 0, 1) + divider.Position = UDim2.new(0, ICON_CELL_WIDTH, 1, -1) + end +end + +function UserList:ApplySearch(searchTerm) + searchTerm = searchTerm:upper() + self:ResetFinalDivider() + for _, userEntry in pairs(self.userEntries) do + local name = userEntry.rbx.Label.Text:upper() + local first = name:find(searchTerm) + if first ~= nil then + userEntry.rbx.LayoutOrder = first + userEntry.rbx.Visible = true + else + userEntry.rbx.Visible = false + end + end + self.rbx.ListLayout:ApplyLayout() + self:FormatFinalDivider() +end + +function UserList:Update(users, selectedList) + if self.users == users then + return + end + self.users = users + + for _, userEntry in pairs(self.userEntries) do + userEntry.rbx.Visible = false + end + + local height = 0 + for _, user in pairs(users) do + if self.filter == nil or self.filter(user) then + local userEntry = self.userEntries[user.id] + local selected = selectedList and Functional.Find(selectedList, user.id) or false + + if userEntry == nil then + self:ResetFinalDivider() + + userEntry = UserEntry.new(self.appState, user, self.icon, selected) + userEntry.tapped:Connect(function() + userEntry.selected = not userEntry.selected + self.userSelected:Fire(user) + end) + userEntry.rbx.Parent = self.rbx + self.userEntries[user.id] = userEntry + + self:FormatFinalDivider() + else + userEntry.rbx.Visible = true + userEntry:Update(user, selected) + end + height = height + userEntry.rbx.AbsoluteSize.Y + end + end + self.rbx.Size = UDim2.new(1, 0, 0, height) +end + +function UserList:Destruct() + for _, userEntry in pairs(self.userEntries) do + userEntry:Destruct() + end + + self.users = {} + self.userEntries = {} + self.rbx:Destroy() +end + +return UserList \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserPresenceTextLabel.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserPresenceTextLabel.lua new file mode 100644 index 0000000..b33ca40 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserPresenceTextLabel.lua @@ -0,0 +1,69 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local LuaChat = Modules.LuaChat +local LuaApp = Modules.LuaApp + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local User = require(LuaApp.Models.User) + +local UserPresenceTextLabel = {} +UserPresenceTextLabel.__index = UserPresenceTextLabel + +function UserPresenceTextLabel.new(appState, userId, additionalProps) + local self = { + appState = appState, + connections = {}, + lastUserModel = nil, + userId = userId, + } + setmetatable(self, UserPresenceTextLabel) + + self.rbx = Create.new("TextLabel")( + { + BackgroundTransparency = 1, + TextSize = Constants.Font.FONT_SIZE_14, + TextColor3 = Constants.Color.GRAY3, + Font = Enum.Font.SourceSans, + TextXAlignment = Enum.TextXAlignment.Left, + }, + additionalProps + ) + + table.insert(self.connections, appState.store.Changed:Connect(function(newState) + self:Update(newState) + end)) + self:Update(appState.store:getState()) + + return self +end + +function UserPresenceTextLabel:RenderPresenceText(user) + self.rbx.Text = User.userPresenceToText(self.appState.localization, user) +end + +function UserPresenceTextLabel:Update(state) + local user = state.Users[self.userId] + + if not user then + return + end + + if user == self.lastUserModel then + return + end + self.lastUserModel = user + + self:RenderPresenceText(user) +end + +function UserPresenceTextLabel:Destruct() + for _, connection in pairs(self.connections) do + connection:Disconnect() + end + self.rbx:Destroy() +end + +return UserPresenceTextLabel + diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserThumbnail.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserThumbnail.lua new file mode 100644 index 0000000..f58dd04 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserThumbnail.lua @@ -0,0 +1,136 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local HeadshotLoader = require(LuaChat.HeadshotLoader) +local Signal = require(Common.Signal) +local getInputEvent = require(LuaChat.Utils.getInputEvent) + +local OVERLAY_IMAGE_BIG = "rbxasset://textures/ui/LuaChat/graphic/gr-profile-border-48x48.png" +local OVERLAY_IMAGE_SMALL = "rbxasset://textures/ui/LuaChat/graphic/gr-profile-border-36x36.png" + +local UserThumbnail = {} + +UserThumbnail.__index = UserThumbnail + +function UserThumbnail.new(appState, userId, small) + local self = {} + self.connections = {} + self.appState = appState + self.userId = userId + self.clicked = Signal.new() + + local size = small and 36 or 48 + local overlayImage = small and OVERLAY_IMAGE_SMALL or OVERLAY_IMAGE_BIG + + self.headshot = Create.new "ImageLabel" { + Name = "Avatar", + Image = "", + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + BackgroundTransparency = 1, + } + + local mask = Create.new "ImageLabel" { + Name = "Overlay", + Image = overlayImage, + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + } + + local pIndicatorBg = Create.new "ImageLabel" { + Name = "Presence", + Size = UDim2.new(0, 14, 0, 14), + BackgroundTransparency = 1, + Image = "rbxasset://textures/ui/LuaChat/graphic/indicator-background.png", + Position = UDim2.new(1, -14, 1, -14), + Visible = false, + } + self.pIndicatorBg = pIndicatorBg + + self.rbx = Create.new "ImageButton" { + Name = "UserThumbnail", + BackgroundTransparency = 1, + ImageTransparency = 1, + Image = "", + Size = UDim2.new(0, size, 0, size), + AutoButtonColor = false, + + self.headshot, + mask, + pIndicatorBg + } + + setmetatable(self, UserThumbnail) + + self:Update() + + self.rbx.AncestryChanged:Connect(function(rbx, parent) + if rbx == self.rbx and parent == nil then + self:Destruct() + end + end) + + do + local connection = appState.store.Changed:Connect(function(state, oldState) + if state.Users == oldState.Users then + return + end + + if state.Users[userId] == oldState.Users[userId] then + return + end + + self:Update() + end) + table.insert(self.connections, connection) + end + + table.insert(self.connections, + getInputEvent(self.rbx):Connect(function() + self.clicked:Fire(self.user) + end)) + + return self +end + +function UserThumbnail:Destruct() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + + self.connections = {} + + self.rbx:Destroy() +end + +function UserThumbnail:Update() + local user = self.appState.store:getState().Users[self.userId] + + if not user then + return + end + + if user == self.user then + return + end + + self.user = user + + HeadshotLoader:Load(self.headshot, self.userId) + + local presenceImage = Constants.PresenceIndicatorImages[user.presence] + + if presenceImage then + self.pIndicatorBg.Visible = true + self.pIndicatorBg.Image = presenceImage + else + self.pIndicatorBg.Visible = false + end +end + +return UserThumbnail diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserThumbnailBar.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserThumbnailBar.lua new file mode 100644 index 0000000..db18c68 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserThumbnailBar.lua @@ -0,0 +1,211 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) +local Signal = require(Common.Signal) + +local UserThumbnail = require(LuaChat.Components.UserThumbnail) + +local GROUP_ICON_SIZE = 24 +local ICON_CELL_WIDTH = 60 +local REMOVE_BUTTON_SIZE = 16 +local THUMBNAIL_LABEL_HEIGHT = 15 +local THUMBNAIL_PADDING_HEIGHT = 10 +local THUMBNAIL_PADDING_WIDTH = 16 +local THUMBNAIL_SIZE = 48 + +local THUMBNAIL_PLUS_HEIGHT = THUMBNAIL_SIZE + THUMBNAIL_LABEL_HEIGHT + THUMBNAIL_PADDING_HEIGHT * 2 +local THUMBNAIL_PLUS_WIDTH = THUMBNAIL_SIZE + THUMBNAIL_PADDING_WIDTH + +local UserThumbnailPlus = {} +UserThumbnailPlus.__index = UserThumbnailPlus + +function UserThumbnailPlus.new(appState) + local self = { + appState = appState + } + setmetatable(self, UserThumbnailPlus) + + self.rbx = Create.new"Frame" { + Name = "ThumbnailFrame", + BackgroundTransparency = 1, + Size = UDim2.new(0, THUMBNAIL_PLUS_WIDTH, 0, THUMBNAIL_PLUS_HEIGHT), + } + + self:SetEmptyThumbnail() + + self.removed = Signal.new() + + return self +end + +function UserThumbnailPlus:Update(user) + if user == self.user then + return + end + self.user = user + + if user == nil then + self:SetEmptyThumbnail(user) + else + self:SetThumbnail(user) + end +end + +function UserThumbnailPlus:SetEmptyThumbnail() + if self.userThumbnailFrame ~= nil then + self.userThumbnailFrame.Parent = nil + end + self.userThumbnailFrame = Create.new"ImageLabel" { + Name = "UserThumbnail", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, THUMBNAIL_SIZE, 0, THUMBNAIL_SIZE), + Position = UDim2.new(0, 0, 0, THUMBNAIL_PADDING_HEIGHT), + Image = "rbxasset://textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted.png", + } + self.userThumbnailFrame.Parent = self.rbx +end + +function UserThumbnailPlus:SetThumbnail(user) + if self.userThumbnailFrame ~= nil then + self.userThumbnailFrame.Parent = nil + end + + self.userThumbnail = UserThumbnail.new(self.appState, user.id, false) + self.userThumbnail.rbx.Size = UDim2.new(0, THUMBNAIL_SIZE, 0, THUMBNAIL_SIZE) + + self.userThumbnailFrame = Create.new"Frame" { + Name = "UserThumbnail", + BackgroundTransparency = 1, + Size = UDim2.new(0, THUMBNAIL_SIZE, 1, -THUMBNAIL_PADDING_HEIGHT*2), + Position = UDim2.new(0, 0, 0, THUMBNAIL_PADDING_HEIGHT), + + self.userThumbnail.rbx, + Create.new"TextLabel" { + Name = "UserName", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, THUMBNAIL_LABEL_HEIGHT), + Position = UDim2.new(0, 0, 1, -THUMBNAIL_LABEL_HEIGHT), + TextSize = Constants.Font.FONT_SIZE_12, + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSans, + Text = user.name, + TextXAlignment = Enum.TextXAlignment.Left, + ClipsDescendants = true, + }, + Create.new"ImageLabel" { + Name = "RemoveImage", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, REMOVE_BUTTON_SIZE, 0, REMOVE_BUTTON_SIZE), + Position = UDim2.new(1, 0, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Image = "rbxasset://textures/ui/LuaChat/icons/ic-remove.png", + }, + } + + self.userThumbnailFrame.Parent = self.rbx + + self.tapped = self.userThumbnail.clicked + self.tapped:Connect(function() + self.removed:Fire(self.user.id) + end) +end + +function UserThumbnailPlus:Destruct() + if self.userThumbnail then + self.userThumbnail:Destruct() + end + self.rbx:Destroy() +end + +local UserThumbnailBar = {} +UserThumbnailBar.__index = UserThumbnailBar + +function UserThumbnailBar.new(appState, maxSize, layoutOrder) + local self = { + appState = appState, + maxSize = maxSize, + } + setmetatable(self, UserThumbnailBar) + + self.rbx = Create.new"Frame" { + Name = "UserThumbnailBar", + BackgroundTransparency = 0, + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.WHITE, + Size = UDim2.new(1, 0, 0, THUMBNAIL_PLUS_HEIGHT), + LayoutOrder = layoutOrder, + + Create.new"Frame" { + Name = "GroupIcon", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, ICON_CELL_WIDTH, 0, THUMBNAIL_SIZE), + Position = UDim2.new(0, 0, 0, THUMBNAIL_PADDING_HEIGHT), + Create.new"ImageLabel" { + Name = "IconImage", + BackgroundTransparency = 1, + Size = UDim2.new(0, GROUP_ICON_SIZE, 0, GROUP_ICON_SIZE), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(0.5, 0), + Image = "rbxasset://textures/ui/LuaChat/icons/ic-group.png", + }, + Create.new"TextLabel" { + Name = "FriendCount", + BackgroundTransparency = 1, + TextSize = Constants.Font.FONT_SIZE_14, + TextColor3 = Constants.Color.GRAY2, + Size = UDim2.new(1, 0, 0, THUMBNAIL_LABEL_HEIGHT), + Position = UDim2.new(0, 0, 1, -THUMBNAIL_LABEL_HEIGHT), + AnchorPoint = Vector2.new(0, 0), + Text = "0/" .. maxSize, + Font = "SourceSans", + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Bottom, + }, + }, + } + + self.removed = Signal.new() + self.thumbnails = {} + for i = 1, self.maxSize do + local thumb = UserThumbnailPlus.new(appState) + table.insert(self.thumbnails, thumb) + thumb.rbx.Position = UDim2.new(0, ICON_CELL_WIDTH + THUMBNAIL_PLUS_WIDTH * (i-1), 0, 0) + thumb.rbx.Parent = self.rbx + thumb.removed:Connect(function(id) + self.removed:Fire(id) + end) + end + + return self +end + +function UserThumbnailBar:Update(participants) + local users = self.appState.store:getState().Users + local participantCount = #participants + for index,thumbnail in ipairs(self.thumbnails) do + if index <= participantCount then + thumbnail:Update(users[participants[index]]) + else + thumbnail:Update(nil) + end + end + self.rbx.GroupIcon.FriendCount.Text = participantCount .. "/" .. self.maxSize +end + +function UserThumbnailBar:Destruct() + for _,thumbnail in ipairs(self.thumbnails) do + thumbnail:Destruct() + end + self.thumbnails = {} + self.rbx:Destroy() +end + +return UserThumbnailBar \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserTypingIndicator.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserTypingIndicator.lua new file mode 100644 index 0000000..7612894 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Components/UserTypingIndicator.lua @@ -0,0 +1,147 @@ +local CoreGui = game:GetService("CoreGui") +local Players = game:GetService("Players") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Constants = require(LuaChat.Constants) +local Conversation = require(LuaChat.Models.Conversation) +local Create = require(LuaChat.Create) +local Signal = require(Common.Signal) +local Text = require(LuaChat.Text) + +local Components = LuaChat.Components +local TypingIndicator = require(Components.TypingIndicator) +local UserThumbnail = require(Components.UserThumbnail) + +local BUBBLE_PADDING = 10 +local DEFAULT_SPACER_BETWEEN_BUBBLES = 2 +local RECEIVED_BUBBLE_WITH_TAIL = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble.png" +local RECEIVED_TAIL = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble-tip.png" + +local UserTypingIndicator = {} + +UserTypingIndicator.__index = UserTypingIndicator + +function UserTypingIndicator.new(appState, conversation) + local self = { + lastTyping = 0 + } + + self.Resized = Signal.new() + self.conversation = conversation + + self.indicator = TypingIndicator.new(appState) + self.indicator.rbx.AnchorPoint = Vector2.new(0.5, 0.5) + self.indicator.rbx.Position = UDim2.new(0.5, 0, 0.5, 0) + self.indicator.rbx.Size = UDim2.new(0, 50, 0, 12) + + local localUserId = tostring(Players.LocalPlayer.UserId) + local otherUserId + for _, participant in ipairs(conversation.participants) do + if participant ~= localUserId then + otherUserId = participant + break + end + end + + self.otherUserId = otherUserId + + self.thumbnail = UserThumbnail.new(appState, otherUserId, true) + self.thumbnail.rbx.Position = UDim2.new(0, 10, 0, 0) + self.thumbnail.rbx.Overlay.ImageColor3 = Constants.Color.GRAY6 + + self.tail = Create.new "ImageLabel" { + Name = "Tail", + Size = UDim2.new(0, 6, 0, 6), + AnchorPoint = Vector2.new(1, 0), + BackgroundTransparency = 1, + Image = RECEIVED_TAIL, + } + + self.bubble = Create.new "ImageLabel" { + Name = "Bubble", + BackgroundTransparency = 1, + Position = UDim2.new(0, 56, 0, 0), + Size = UDim2.new(0, 70, 0, 38), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(10, 10, 11, 11), + Image = RECEIVED_BUBBLE_WITH_TAIL, + LayoutOrder = 2, + + Create.new "Frame" { + Name = "Content", + BackgroundTransparency = 1, + Size = UDim2.new(1, -BUBBLE_PADDING * 2, 1, -BUBBLE_PADDING * 2), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + + self.indicator.rbx + }, + + self.tail, + } + + self.rbx = Create.new "Frame" { + Name = "UserTypingIndicator", + Size = UDim2.new(1, 0, 0, 0), + BackgroundTransparency = 1, + + self.thumbnail.rbx, + self.bubble, + } + + setmetatable(self, UserTypingIndicator) + + self:Hide() + + return self +end + +function UserTypingIndicator:Update(conversation) + if conversation.conversationType == Conversation.Type.MULTI_USER_CONVERSATION then + return + end + + if conversation.usersTyping == self.conversation.usersTyping then + return + end + + self.conversation = conversation + + local timeNow = tick() + + self.lastTyping = timeNow + + if conversation.usersTyping[self.otherUserId] then + self:Show() + else + self:Hide() + end +end + +function UserTypingIndicator:Show() + self.rbx.Visible = true + self.indicator.rbx.Visible = true + -- In order to treat indicator as a single line message, generate text + -- bubble height from default font size 18 to ensure typing indiactor + -- height will scale with font scale. + local temporaryTextHeight = Text.GetTextHeight("", Enum.Font.SourceSans, Constants.Font.FONT_SIZE_18, + self.rbx.AbsoluteSize.X) + self.rbx.Size = UDim2.new(1, 0, 0, temporaryTextHeight + BUBBLE_PADDING * 2 + DEFAULT_SPACER_BETWEEN_BUBBLES) + self.Resized:Fire() +end + +function UserTypingIndicator:Hide() + self.rbx.Visible = false + self.indicator.rbx.Visible = false + self.rbx.Size = UDim2.new(1, 0, 0, 0) + self.Resized:Fire() +end + +function UserTypingIndicator:Destruct() + self.rbx:Destroy() +end + +return UserTypingIndicator diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Constants.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Constants.lua new file mode 100644 index 0000000..91396b6 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Constants.lua @@ -0,0 +1,159 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local FormFactor = require(Modules.LuaApp.Enum.FormFactor) + +local LuaChatCreateChatEnabled = settings():GetFFlag("LuaChatCreateChatEnabled") + +local Constants = { + Color = { + GRAY1 = Color3.fromRGB(25, 25, 25), + GRAY2 = Color3.fromRGB(117, 117, 117), + GRAY3 = Color3.fromRGB(184, 184, 184), + GRAY4 = Color3.fromRGB(227, 227, 227), + GRAY5 = Color3.fromRGB(242, 242, 242), + GRAY6 = Color3.fromRGB(245, 245, 245), + WHITE = Color3.fromRGB(255, 255, 255), + BLUE_PRIMARY = Color3.fromRGB(0, 162, 255), + BLUE_HOVER = Color3.fromRGB(50, 181, 255), + BLUE_PRESSED = Color3.fromRGB(0, 116, 189), + BLUE_DISABLED = Color3.fromRGB(153, 218, 255), + GREEN_PRIMARY = Color3.fromRGB(2, 183, 87), + GREEN_HOVER = Color3.fromRGB(63, 198, 121), + GREEN_PRESSED = Color3.fromRGB(17, 130, 55), + GREEN_DISABLED = Color3.fromRGB(163, 226, 189), + RED_PRIMARY = Color3.fromRGB(226, 35, 26), + RED_NEGATIVE = Color3.fromRGB(216, 104, 104), + RED_HOVER = Color3.fromRGB(226, 118, 118), + RED_PRESSED = Color3.fromRGB(172, 30, 45), + ORANGE_WARNING = Color3.fromRGB(246, 136, 2), + ORANGE_FAVORITE = Color3.fromRGB(246, 183, 2), + BROWN_TIX = Color3.fromRGB(204, 158, 113), + ALPHA_SHADOW_PRIMARY = 0.3, -- Used with Gray1 + ALPHA_SHADOW_HOVER = 0.75, -- Used with Gray1 + CONVERSATION_BACKGROUND = Color3.fromRGB(224, 224, 224), + }, + Font = { + TITLE = Enum.Font.SourceSansSemibold, + -- These values appear differently because of the discrepancy between design sizes and + -- the engine sizes + FONT_SIZE_12 = 15, + FONT_SIZE_14 = 17, + FONT_SIZE_16 = 20, + FONT_SIZE_18 = 23, + FONT_SIZE_20 = 23, + FONT_SIZE_18_POS_OFFSET = -8, + }, + Tween = { + DEFAULT_TWEEN_TIME = 0.25, + DEFAULT_TWEEN_STYLE = Enum.EasingStyle.Quad, + DEFAULT_TWEEN_EASING_DIRECTION = Enum.EasingDirection.Out, + }, + PresenceType = { + NONE = "NONE", + ONLINE = "ONLINE", + IN_GAME = "IN_GAME", + IN_STUDIO = "IN_STUDIO", + }, + ServerState = { + NONE = "NONE", + CREATING = "CREATING", + CREATED = "CREATED", + }, + ConversationLoadingState = { + NONE = "NONE", + LOADING = "LOADING", + DONE = "DONE" + }, + PresenceIndicatorImages = { + NONE = nil, + ONLINE = "rbxasset://textures/ui/LuaChat/graphic/gr-indicator-online.png", + IN_GAME = "rbxasset://textures/ui/LuaChat/graphic/gr-indicator-ingame.png", + IN_STUDIO = "rbxasset://textures/ui/LuaChat/graphic/gr-indicator-instudio.png", + }, + Text = { + INPUT_PLACEHOLDER = Color3.fromRGB(189, 189, 189), + INPUT = Color3.fromRGB(25, 25, 25), + POST_TYPING_STATUS_INTERVAL = 3, --How frequently do we POST our typing status if we're still typing + }, + PageSize = { + GET_MESSAGES = 30, + GET_NEW_MESSAGES = 4, + GET_CONVERSATIONS = 30, + }, + MAX_PARTICIPANT_COUNT = 5, + MIN_PARTICIPANT_COUNT = LuaChatCreateChatEnabled and 1 or 2, + -- This value actually comes from iOS, but we are shortcutting actually getting the value from there. + + ModalDialog = { + CLEARANCE_CORNER_ROUNDING = 5, + CLEARANCE_DIALOG_SIDE = 48, + CLEARANCE_DIALOG_BOTTOM = 36, + BUTTON_HEIGHT = 42, + }, + + SharedGamesConfig = { + SortNames = {"Popular", "MyRecent", "MyFavorite", "FriendActivity"}, + SortsAttribute = { + Popular = { + TILE_LOCALIZATION_KEY = "Feature.Chat.ShareGameToChat.Popular", + ERROR_TIP_LOCALIZATION_KEY = "Feature.Chat.ShareGameToChat.NoPopularGames" + }, + MyRecent = { + TILE_LOCALIZATION_KEY = "Feature.Chat.ShareGameToChat.Recent", + ERROR_TIP_LOCALIZATION_KEY = "Feature.Chat.ShareGameToChat.NoRecentGames" + }, + MyFavorite = { + TILE_LOCALIZATION_KEY = "Feature.Chat.ShareGameToChat.Favorites", + ERROR_TIP_LOCALIZATION_KEY = "Feature.Chat.ShareGameToChat.NoFavoriteGames" + }, + FriendActivity = { + TILE_LOCALIZATION_KEY = "Feature.Chat.ShareGameToChat.FriendActivity", + ERROR_TIP_LOCALIZATION_KEY = "Feature.Chat.ShareGameToChat.NoFriendActivity" + } + }, + Thumbnail = { + SHOWN_SIZE = 60, + FETCHED_SIZE = 150, + }, + }, + + GameShareView = { + TABLET_HORIZONTAL_DIVIDER_HEIGHT = 15, + TABLET_VIEW_WIDTH = 540, + }, + + PerformanceMeasurement = { + LUA_CHAT_SEND_MESSAGE = "LuaChatSendMessage", + LUA_CHAT_RECEIVE_MESSAGE = "LuaChatReceiveMessage", + }, + + ToastIDs = { + TOO_MANY_PEOPLE = "TOO_MANY_PEOPLE", + GROUP_NAME_MODERATED = "GROUP_NAME_MODERATED", + MESSAGE_WAS_MODERATED = "MESSAGE_WAS_MODERATED", + REMOVED_FROM_CONVERSATION = "REMOVED_FROM_CONVERSATION", + PIN_GAME_FAILED = "PIN_GAME_FAILED", + PIN_PINNED_GAME = "PIN_PINNED_GAME", + UNPIN_GAME_FAILED = "UNPIN_GAME_FAILED", + GAME_NOT_SHAREABLE = "GAME_NOT_SHAREABLE", + }, + + FormFactor = { + PHONE = { + ASSET_CARD_HORIZONTAL_MARGIN = 108, + }, + TABLET = { + ASSET_CARD_HORIZONTAL_MARGIN = 224, + } + }, + +} + +function Constants:GetFormFactorSpecific(formFactor) + if formFactor == FormFactor.TABLET then + return Constants.FormFactor.TABLET + else + return Constants.FormFactor.PHONE + end +end + +return Constants \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Create.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Create.lua new file mode 100644 index 0000000..8375071 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Create.lua @@ -0,0 +1,82 @@ +local Create = { + events = {} +} + +--[[ + Merge a list of dictionary tables into one table +]] +function Create.merge(...) + if select("#", ...) == 1 then + return (...) + end + + local new = {} + + for i = 1, select("#", ...) do + for key, value in pairs(select(i, ...)) do + -- Push numeric keys as a list + if (type(key) == "number") then + table.insert(new, value) + else + new[key] = value + end + end + end + + return new +end + +--[[ + Create a new instance with the given type properties. + + Usage: + Create.new "Frame" { + Name = "MyFrame" + } + + -- OR -- + + Create "Frame" { + Name = "MyFrame" + } + + Makes no assumptions about the types of children added. The only requirement + is that the "Parent" property on them can be assigned. +]] +function Create.new(name) + return function(...) + local props = Create.merge(...) + local new = Instance.new(name) + + -- Add properties to this instance; all string keys are property names + for key, value in pairs(props) do + if type(key) == "string" then + assert(key ~= "Parent", "Don't set 'Parent' using Create!") + + new[key] = value + elseif type(key) == "table" then + -- Events use a special-case key + if key == Create.events then + for name, event in pairs(value) do + new[name]:Connect(event) + end + end + end + end + + -- Add children after all the properties are set + for _, child in ipairs(props) do + child.Parent = new + end + + return new + end +end + +setmetatable(Create, { + __call = function(self, ...) + return Create.new(...) + end +}) + +return Create \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Create.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Create.spec.lua new file mode 100644 index 0000000..7335a21 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Create.spec.lua @@ -0,0 +1,112 @@ +return function() + local Create = require(script.Parent.Create) + + describe("merge", function() + it("should merge lists", function() + local a = {1, 2} + local b = {3, 4} + local c = {5, 6} + + local d = Create.merge(a, b, c) + + for index, value in ipairs(d) do + expect(value).to.equal(index) + end + end) + + it("should merge dictionaries", function() + local a = { + hello = "world", + foo = "bar" + } + + local b = { + hello = "mom", + baz = "qux" + } + + local c = Create.merge(a, b) + + expect(c.foo).to.equal(a.foo) + expect(c.hello).to.equal(b.hello) + expect(c.baz).to.equal(b.baz) + end) + + it("should merge mixed tables", function() + local a = { + 1, 2, 3, + foo = "foo", + bar = "bar" + } + + local b = { + 4, 5, 6, + foo = "hello", + baz = "baz" + } + + local c = Create.merge(a, b) + + for index, value in ipairs(c) do + expect(value).to.equal(index) + end + + expect(c.bar).to.equal(a.bar) + expect(c.foo).to.equal(b.foo) + expect(c.baz).to.equal(b.baz) + end) + end) + + describe("new", function() + it("should create objects with the correct type", function() + local created = Create.new("StringValue")() + + expect(created).to.be.ok() + expect(created.ClassName).to.equal("StringValue") + end) + + it("should assign properties", function() + local created = Create.new("StringValue")({ + Name = "Created" + }) + + expect(created).to.be.ok() + expect(created.Name).to.equal("Created") + end) + + it("should merge multiple property declarations", function() + local created = Create.new("StringValue")({ + Name = "Foo", + Value = "Some Value" + }, { + Name = "Bar" + }) + + expect(created).to.be.ok() + expect(created.Name).to.equal("Bar") + expect(created.Value).to.equal("Some Value") + end) + + it("should throw on assignment of Parent", function() + expect(function() + local root = Instance.new("StringValue") + + Create.new("StringValue")({ + Parent = root + }) + end).to.throw() + end) + + it("should add children from construction", function() + local marker = Instance.new("StringValue") + marker.Name = "Marker" + + local created = Create.new("StringValue")({ + Name = "Created", + marker + }) + + expect(marker.Parent).to.equal(created) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/DateTime.lua b/Client2018/content/internal/Chat/Modules/LuaChat/DateTime.lua new file mode 100644 index 0000000..ec0de02 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/DateTime.lua @@ -0,0 +1,487 @@ +--[[ + This is a Lua implementation of the DateTime API proposal. It'll eventually + be implemented in C++ and merged into the rest of the codebase if this model + of working with dates ends up being useful. +]] + +local TimeZone = require(script.Parent.TimeZone) +local TimeUnit = require(script.Parent.TimeUnit) + +local DateTime = {} + +local monthShortNames = { + "Jan", "Feb", "Mar", "Apr", + "May", "Jun", "Jul", "Aug", + "Sep", "Oct", "Nov", "Dec" +} + +local monthLongNames = { + "January", "February", "March", "April", + "May", "June", "July", "August", + "September", "October", "November", "December" +} + +local dayShortNames = { + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" +} + +local dayLongNames = { + "Sunday", "Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday" +} + +--[[ + We structure tokens like this to preserve order, since Lua associative + arrays have no inherent order. +]] +local tokens = { + {"YYYY", function(values) + return tostring(values.Year) + end}, + {"MMMM", function(values) + return monthLongNames[values.Month] + end}, + {"MMM", function(values) + return monthShortNames[values.Month] + end}, + {"MM", function(values) + return ("%02d"):format(values.Month) + end}, + {"M", function(values) + return tostring(values.Month) + end}, + {"DDDD", function(values) + return dayLongNames[values.WeekDay] + end}, + {"DDD", function(values) + return dayShortNames[values.WeekDay] + end}, + {"DD", function(values) + return ("%02d"):format(values.Day) + end}, + {"D", function(values) + return tostring(values.Day) + end}, + {"HH", function(values) + local hour = values.Hour + + return ("%02d"):format(hour) + end}, + {"H", function(values) + local hour = values.Hour + + return tostring(hour) + end}, + {"hh", function(values) + local hour = values.Hour % 12 + if hour == 0 then + hour = 12 + end + + return ("%02d"):format(hour) + end}, + {"h", function(values) + local hour = values.Hour % 12 + if hour == 0 then + hour = 12 + end + + return tostring(hour) + end}, + {"mm", function(values) + return ("%02d"):format(values.Minute) + end}, + {"m", function(values) + return tostring(values.Minute) + end}, + {"ss", function(values) + return ("%02d"):format(values.Seconds) + end}, + {"s", function(values) + return tostring(values.Seconds) + end}, + {"A", function(values) + return values.Hour >= 12 and "PM" or "AM" + end}, + {"a", function(values) + return values.Hour >= 12 and "pm" or "am" + end} +} + +local tokenKeys = {} +for _, pair in ipairs(tokens) do + table.insert(tokenKeys, pair[1]) +end + +local tokenMap = {} +for _, pair in ipairs(tokens) do + tokenMap[pair[1]] = pair[2] +end + +--[[ + What's the next token in this source? +]] +local function getToken(source, i) + local char = source:sub(i, i) + + for _, token in ipairs(tokenKeys) do + -- Only keep checking if the first character matches the token + if token:sub(1, 1) == char then + local match = source:sub(i, i + token:len() - 1) + + if match == token then + return token + end + end + end +end + +--[[ + An estimate of the current time zone's offset from UTC in seconds. + + This might fail for weird timezones (UTC +/- 14), but we can fix that by + picking a reference time that's further away from the Unix epoch. +]] +local function getTimeZoneOffset() + local actualEpoch = 86400 + 43200 + local epoch = os.time({year = 1970, month = 1, day = 2, isdst = -1}) + if epoch then + return actualEpoch - epoch + else + return 0 + end +end + +--[[ + Create a DateTime with the given values in UTC. + + All values are optional! +]] +function DateTime.new(year, month, day, hour, minute, seconds) + local tzOffset = getTimeZoneOffset() + local timestamp = os.time({ + year = year or 1970, + month = month or 1, + day = day or 1, + hour = hour or 0, + min = minute or 0, + sec = seconds or 0, + isdst = -1 + }) + if timestamp == nil then + timestamp = 0 + end + + if seconds then + local subseconds = seconds - math.floor(seconds) + timestamp = timestamp + subseconds + end + + return DateTime.fromUnixTimestamp(timestamp + tzOffset) +end + +--[[ + Create a DateTime representing now. +]] +function DateTime.now() + return DateTime.fromUnixTimestamp(os.time()) +end + +--[[ + Create a Datetime from the given Unix timestamp. + + Limited to the range [0, 2^32), which lets us represent dates out to about + 2038. +]] +function DateTime.fromUnixTimestamp(timestamp) + assert(type(timestamp) == "number", "Invalid argument #1 to fromUnixTimestamp, expected number.") + + local self = {} + + self.value = timestamp + + setmetatable(self, DateTime) + + return self +end + +--[[ + Attempt to create a DateTime from an ISO 8601 date-time string. + + Will return nil on failure and output a warning to a console denoting what + went wrong. This can probably turned into a second return value if we need + to handle that data programmatically. +]] +function DateTime.fromIsoDate(isoDate) + assert(type(isoDate) == "string", "Invalid argument #1 to DateTime.fromIsoDate, expected string.") + + local datePattern = "^(%d+)%-(%d+)%-(%d+)" -- 0000-00-00 + local timePattern = "T(%d+):(%d+):(%d+%.?%d*)" -- T00:00:00 + local utcPattern = "Z$" + local timeZonePattern = "([+-]%d+):(%d+)$" -- either Z or +/- followed by "00:00" + + local timezone = 0 + local values = {1970, 1, 1, 0, 0, 0} + local year, month, day = isoDate:match(datePattern) + + if not year then + warn(("Invalid ISO 8601 date: %q"):format(isoDate)) + return nil + end + + values[1] = tonumber(year) + values[2] = tonumber(month) + values[3] = tonumber(day) + + local hour, minute, seconds = isoDate:match(timePattern) + + if hour then + values[4] = tonumber(hour) + values[5] = tonumber(minute) + values[6] = tonumber(seconds) + + local isUtc = isoDate:match(utcPattern) + + if not isUtc then + local offsetHours, offsetMinutes = isoDate:match(timeZonePattern) + + if not offsetHours then + local offsetTotal = getTimeZoneOffset() + offsetHours = offsetTotal / 3600 + offsetMinutes = 0 + + warn(("Invalid time zone in ISO 8601 date: %q -- falling back to local time"):format(isoDate)) + end + + timezone = 3600 * tonumber(offsetHours) + 60 * tonumber(offsetMinutes) + end + end + + local date = DateTime.new(unpack(values)) + date.value = date.value - timezone + + return date +end + +--[[ + Format our current date using a formatting string. Look at the DateTime + proposal to see information about the different formatting tokens. + Generally, they try to resemble LDML and/or Moment.js-style formatting. + + The time zone parameter is optional and defaults to the current time zone, + TimeZone.Current. +]] +function DateTime:Format(formatString, tz) + assert(type(formatString) == "string", "Invalid argument #1 to Format, expected string.") + + tz = tz or TimeZone.Current + + local values = self:GetValues(tz) + + local buffer = {} + + local i = 1 + while i <= formatString:len() do + local char = formatString:sub(i, i) + local token = getToken(formatString, i) + + if token then + table.insert(buffer, tokenMap[token](values)) + i = i + token:len() + elseif char == "[" then + -- Crawl forward until the next ] and interpret that text literally + local j = i + while j <= formatString:len() do + j = j + 1 + + if formatString:sub(j, j) == "]" then + break + end + end + + table.insert(buffer, formatString:sub(i + 1, j - 1)) + i = j + 1 + else + table.insert(buffer, char) + i = i + 1 + end + end + + local result = table.concat(buffer) + + return result +end + +--[[ + Get a table of values representing the date-time in the given timezone. + + The time zone parameter is optional and defaults to the current zime zone, + TimeZone.Current. +]] +function DateTime:GetValues(tz) + tz = tz or TimeZone.Current + + local reference + + if tz == TimeZone.Current then + reference = os.date("*t", self.value) + elseif tz == TimeZone.UTC then + reference = os.date("!*t", self.value) + end + + if not reference then + error(("Invalid TimeZone \"%s\""):format(tostring(tz)), 2) + end + + return { + Year = reference.year, + Month = reference.month, + Day = reference.day, + Hour = reference.hour, + Minute = reference.min, + Seconds = reference.sec, + WeekDay = reference.wday + } +end + +--[[ + Recover a Unix timestamp representing the DateTime's value. +]] +function DateTime:GetUnixTimestamp() + return self.value +end + +--[[ + Format the DateTime as an ISO 8601 date string with time attached. + + Always formats the time as UTC. There generally aren't many reasons to + generate an ISO 8601 date in another time zone. +]] +function DateTime:GetIsoDate() + return self:Format("YYYY-MM-DD[T]HH:mm:ss[Z]", TimeZone.UTC) +end + +-- Used by IsSame +local descendingGranularityUnits = { + { + unit = TimeUnit.Years, + key = "Year" + }, + { + unit = TimeUnit.Months, + key = "Month" + }, + { + unit = TimeUnit.Days, + key = "Day" + }, + { + unit = TimeUnit.Hours, + key = "Hour" + }, + { + unit = TimeUnit.Minutes, + key = "Minute" + }, + { + unit = TimeUnit.Seconds, + key = "Seconds" + } +} + +--[[ + Checks whether two DateTime values are the same, given a granularity and + timezone value. + + Granularity defaults to seconds and time zone defaults to the current local + time zone. +]] +function DateTime:IsSame(other, granularity, timezone) + granularity = granularity or TimeUnit.Seconds + timezone = timezone or TimeZone.Current + + local selfUnix = self:GetUnixTimestamp() + local otherUnix = other:GetUnixTimestamp() + + if selfUnix == otherUnix then + return true + end + + local selfValues = self:GetValues(timezone) + local otherValues = other:GetValues(timezone) + + -- Week logic is special + if granularity == TimeUnit.Weeks then + local diff = math.abs(selfUnix - otherUnix) + local diffDays = diff / (60 * 60 * 24) + + -- Two dates separated by 7 or more whole days are never in the same week + if diffDays >= 7 then + return false + end + + -- Two dates separated by less than 7 days will be sorted monotonically + -- if they're in the same week + -- TODO: Use start-of-week value to shift WeekDay for locale + if selfUnix > otherUnix then + return selfValues.WeekDay >= otherValues.WeekDay + else + return selfValues.WeekDay <= otherValues.WeekDay + end + end + + for _, unit in ipairs(descendingGranularityUnits) do + local selfValue = selfValues[unit.key] + local otherValue = otherValues[unit.key] + + if selfValue ~= otherValue then + return false + end + + if unit.unit == granularity then + break + end + end + + return true +end + +--[[ + Get a human-readable timestamp relative to the given epoch, which defaults + to now. The format of the time is contextual to how far away the times are. +]] +function DateTime:GetLongRelativeTime(epoch) + epoch = epoch or DateTime.now() + + if self:IsSame(epoch, TimeUnit.Days) then + return self:Format("h:mm A") + elseif self:IsSame(epoch, TimeUnit.Weeks) then + return self:Format("DDD | h:mm A") + elseif self:IsSame(epoch, TimeUnit.Years) then + return self:Format("MMM D | h:mm A") + else + return self:Format("MMM D, YYYY | h:mm A") + end +end + +--[[ + Get a human-readable timestamp relative to the given epoch, which defaults + to now. The format of the time is contextual to how far away the times are. +]] +function DateTime:GetShortRelativeTime(epoch) + epoch = epoch or DateTime.now() + + if self:IsSame(epoch, TimeUnit.Days) then + return self:Format("h:mm A") + elseif self:IsSame(epoch, TimeUnit.Weeks) then + return self:Format("DDD") + elseif self:IsSame(epoch, TimeUnit.Years) then + return self:Format("MMM D") + else + return self:Format("MMM D, YYYY") + end +end + +DateTime.__index = DateTime + +return DateTime \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/DateTime.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/DateTime.spec.lua new file mode 100644 index 0000000..672db97 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/DateTime.spec.lua @@ -0,0 +1,355 @@ +return function() + local DateTime = require(script.Parent.DateTime) + local TimeZone = require(script.Parent.TimeZone) + local TimeUnit = require(script.Parent.TimeUnit) + + describe("Constructors", function() + it("should construct with 'new'", function() + expect(DateTime.new()).to.be.ok() + expect(DateTime.new(2017)).to.be.ok() + expect(DateTime.new(2017, 5)).to.be.ok() + expect(DateTime.new(2017, 5, 3)).to.be.ok() + expect(DateTime.new(2017, 5, 3, 12)).to.be.ok() + expect(DateTime.new(2017, 5, 3, 12, 34)).to.be.ok() + expect(DateTime.new(2017, 5, 3, 12, 34, 51)).to.be.ok() + end) + + it("should construct with 'now'", function() + expect(DateTime.now()).to.be.ok() + end) + + it("should construct from a Unix timestamp", function() + expect(DateTime.fromUnixTimestamp(0)).to.be.ok() + expect(DateTime.fromUnixTimestamp(os.time())).to.be.ok() + end) + + it("should construct from an ISO 8601 date", function() + -- Basic date + do + local date = DateTime.fromIsoDate("1988-03-17") + expect(date).to.be.ok() + end + + -- Date and time + do + local date = DateTime.fromIsoDate("2017-04-10T20:40:16Z") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816) + end + + -- Date and time with no time zone + do + local date = DateTime.fromIsoDate("2017-04-10T20:40:16") + expect(date).to.be.ok() + end + + -- Date, time, and time zone offset + do + local date = DateTime.fromIsoDate("2017-04-10T20:40:16+01:00") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816 - 3600) + end + + -- Date, time, and negative time zone offset + do + local date = DateTime.fromIsoDate("2017-04-10T20:40:16-01:00") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816 + 3600) + end + end) + end) + + describe("Measurements", function() + it("should get values in UTC", function() + local date = DateTime.new() + local values = date:GetValues(TimeZone.UTC) + + expect(values).to.be.ok() + expect(values.Year).to.be.a("number") + expect(values.Month).to.be.a("number") + expect(values.Day).to.be.a("number") + expect(values.Hour).to.be.a("number") + expect(values.Minute).to.be.a("number") + expect(values.Seconds).to.be.a("number") + + -- Locale specific! + expect(values.WeekDay).to.be.a("number") + end) + + it("should get values in local time", function() + local date = DateTime.new() + local values = date:GetValues(TimeZone.Current) + + expect(values).to.be.ok() + expect(values.Year).to.be.a("number") + expect(values.Month).to.be.a("number") + expect(values.Day).to.be.a("number") + expect(values.Hour).to.be.a("number") + expect(values.Minute).to.be.a("number") + expect(values.Seconds).to.be.a("number") + + -- Locale specific! + expect(values.WeekDay).to.be.a("number") + end) + + it("should preserve values from 'new' constructor", function() + local date = DateTime.new(2017, 11, 3, 12, 34, 51) + local values = date:GetValues(TimeZone.UTC) + + expect(values.Year).to.equal(2017) + expect(values.Month).to.equal(11) + expect(values.Day).to.equal(3) + expect(values.Hour).to.equal(12) + expect(values.Minute).to.equal(34) + expect(values.Seconds).to.equal(51) + end) + + it("should preserve Unix timestamp values", function() + do + local date = DateTime.fromUnixTimestamp(0) + expect(date:GetUnixTimestamp()).to.equal(0) + end + + do + local date = DateTime.fromUnixTimestamp(123456789) + expect(date:GetUnixTimestamp()).to.equal(123456789) + end + end) + end) + + describe("Formatting", function() + it("should have correct formatting tokens", function() + local date = DateTime.new(2016, 1, 2, 15, 8, 9) + + -- Shortcut time zone specification + local function format(str) + return date:Format(str, TimeZone.UTC) + end + + expect(format("YYYY")).to.equal("2016") + expect(format("M")).to.equal("1") + expect(format("MM")).to.equal("01") + expect(format("D")).to.equal("2") + expect(format("DD")).to.equal("02") + expect(format("H")).to.equal("15") + expect(format("HH")).to.equal("15") + expect(format("h")).to.equal("3") + expect(format("hh")).to.equal("03") + expect(format("m")).to.equal("8") + expect(format("mm")).to.equal("08") + expect(format("s")).to.equal("9") + expect(format("ss")).to.equal("09") + + -- Locale-specific tests! + expect(format("MMM")).to.equal("Jan") + expect(format("MMMM")).to.equal("January") + expect(format("A")).to.equal("PM") + expect(format("a")).to.equal("pm") + end) + + it("should preserve text within brackets", function() + local date = DateTime.new(2017, 1, 2, 15, 8, 9) + + local function format(str) + return date:Format(str, TimeZone.UTC) + end + + expect(format("[Hello, world!]")).to.equal("Hello, world!") + expect(format("[YYYY-MM-DD]")).to.equal("YYYY-MM-DD") + end) + + it("should create identical ISO 8601 dates for UTC inputs", function() + local date = DateTime.fromIsoDate("2017-04-10T20:40:16Z") + expect(date:GetIsoDate()).to.equal("2017-04-10T20:40:16Z") + end) + + it("should handle dates around midnight", function() + local date = DateTime.new(2015, 4, 20, 0, 0, 0) + + expect(date:Format("H", TimeZone.UTC)).to.equal("0") + expect(date:Format("HH", TimeZone.UTC)).to.equal("00") + expect(date:Format("h", TimeZone.UTC)).to.equal("12") + expect(date:Format("hh", TimeZone.UTC)).to.equal("12") + expect(date:Format("a", TimeZone.UTC)).to.equal("am") + end) + + it("should handle dates around noon", function() + local date = DateTime.new(2017, 5, 23, 12, 0, 0) + + expect(date:Format("H", TimeZone.UTC)).to.equal("12") + expect(date:Format("HH", TimeZone.UTC)).to.equal("12") + expect(date:Format("h", TimeZone.UTC)).to.equal("12") + expect(date:Format("hh", TimeZone.UTC)).to.equal("12") + expect(date:Format("a", TimeZone.UTC)).to.equal("pm") + end) + + it("should return correct 24-hour clock sequences", function() + local date = DateTime.new(2017, 9, 13, 0, 0, 0) + + local formatString = "YYYY-MM-DD HH:mm:ss" + + local expected = { + "2017-09-13 00:00:00", + "2017-09-13 01:00:00", + "2017-09-13 02:00:00", + "2017-09-13 03:00:00", + "2017-09-13 04:00:00", + "2017-09-13 05:00:00", + "2017-09-13 06:00:00", + "2017-09-13 07:00:00", + "2017-09-13 08:00:00", + "2017-09-13 09:00:00", + "2017-09-13 10:00:00", + "2017-09-13 11:00:00", + "2017-09-13 12:00:00", + "2017-09-13 13:00:00", + "2017-09-13 14:00:00", + "2017-09-13 15:00:00", + "2017-09-13 16:00:00", + "2017-09-13 17:00:00", + "2017-09-13 18:00:00", + "2017-09-13 19:00:00", + "2017-09-13 20:00:00", + "2017-09-13 21:00:00", + "2017-09-13 22:00:00", + "2017-09-13 23:00:00", + "2017-09-14 00:00:00", + "2017-09-14 01:00:00", + } + + for i = 1, #expected do + local result = date:Format(formatString, TimeZone.UTC) + expect(result).to.equal(expected[i]) + + -- Advance once hour + date = date.fromUnixTimestamp(date:GetUnixTimestamp() + 3600) + end + end) + + it("should return correct 12-hour clock sequences", function() + local date = DateTime.new(2017, 9, 13, 0, 0, 0) + + local formatString = "YYYY-MM-DD hh:mm:ss a" + + local expected = { + "2017-09-13 12:00:00 am", + "2017-09-13 01:00:00 am", + "2017-09-13 02:00:00 am", + "2017-09-13 03:00:00 am", + "2017-09-13 04:00:00 am", + "2017-09-13 05:00:00 am", + "2017-09-13 06:00:00 am", + "2017-09-13 07:00:00 am", + "2017-09-13 08:00:00 am", + "2017-09-13 09:00:00 am", + "2017-09-13 10:00:00 am", + "2017-09-13 11:00:00 am", + "2017-09-13 12:00:00 pm", + "2017-09-13 01:00:00 pm", + "2017-09-13 02:00:00 pm", + "2017-09-13 03:00:00 pm", + "2017-09-13 04:00:00 pm", + "2017-09-13 05:00:00 pm", + "2017-09-13 06:00:00 pm", + "2017-09-13 07:00:00 pm", + "2017-09-13 08:00:00 pm", + "2017-09-13 09:00:00 pm", + "2017-09-13 10:00:00 pm", + "2017-09-13 11:00:00 pm", + "2017-09-14 12:00:00 am", + "2017-09-14 01:00:00 am", + } + + for i = 1, #expected do + local result = date:Format(formatString, TimeZone.UTC) + expect(result).to.equal(expected[i]) + + -- Advance once hour + date = date.fromUnixTimestamp(date:GetUnixTimestamp() + 3600) + end + end) + + it("should have LongRelativeTime", function() + local date = DateTime.new(2015, 4, 20, 0, 0, 0) + expect(date:GetLongRelativeTime()).to.be.a("string") + end) + + it("should have ShortRelativeTime", function() + local date = DateTime.new(2015, 4, 20, 0, 0, 0) + expect(date:GetShortRelativeTime()).to.be.a("string") + end) + end) + + describe("Comparisons", function() + describe("IsSame", function() + it("should equate dates with different granularity", function() + local value = DateTime.new(2003, 6, 11, 15, 8, 9) + local same = DateTime.new(2003, 6, 11, 15, 8, 9) + + expect(value:IsSame(value)).to.equal(true) + expect(value:IsSame(same)).to.equal(true) + + local units = {TimeUnit.Years, TimeUnit.Months, TimeUnit.Days, TimeUnit.Hours, TimeUnit.Minutes} + for _, unit in ipairs(units) do + expect(value:IsSame(same, unit)).to.equal(true) + end + + local sameMinute = DateTime.new(2003, 6, 11, 15, 8, 10) + + expect(value:IsSame(sameMinute)).to.equal(false) + expect(value:IsSame(sameMinute, TimeUnit.Minutes)).to.equal(true) + expect(value:IsSame(sameMinute, TimeUnit.Years)).to.equal(true) + + local sameHour = DateTime.new(2003, 6, 11, 15, 9, 0) + + expect(value:IsSame(sameHour)).to.equal(false) + expect(value:IsSame(sameHour, TimeUnit.Hours)).to.equal(true) + expect(value:IsSame(sameHour, TimeUnit.Years)).to.equal(true) + + local sameDay = DateTime.new(2003, 6, 11, 14, 8, 9) + + expect(value:IsSame(sameDay)).to.equal(false) + expect(value:IsSame(sameDay, TimeUnit.Days)).to.equal(true) + expect(value:IsSame(sameDay, TimeUnit.Years)).to.equal(true) + + local sameMonth = DateTime.new(2003, 6, 12, 15, 8, 9) + + expect(value:IsSame(sameMonth)).to.equal(false) + expect(value:IsSame(sameMonth, TimeUnit.Months)).to.equal(true) + expect(value:IsSame(sameMonth, TimeUnit.Years)).to.equal(true) + + local sameYear = DateTime.new(2003, 7, 12, 15, 8, 9) + + expect(value:IsSame(sameYear)).to.equal(false) + expect(value:IsSame(sameYear, TimeUnit.Years)).to.equal(true) + + local diffYear = DateTime.new(2004, 6, 11, 15, 8, 9) + + expect(value:IsSame(diffYear)).to.equal(false) + expect(value:IsSame(diffYear, TimeUnit.Years)).to.equal(false) + end) + + it("should equate values using week boundaries", function() + local sunday = DateTime.new(2017, 5, 7) + local saturday = DateTime.new(2017, 5, 13) + local monday = DateTime.new(2017, 5, 8) + local tuesday = DateTime.new(2017, 5, 9) + + -- TODO: Specify locale when that lands; default may break tests + local function sameWeek(a, b) + return a:IsSame(b, TimeUnit.Weeks, TimeZone.UTC) + end + + expect(sameWeek(monday, monday)).to.equal(true) + + expect(sameWeek(sunday, monday)).to.equal(true) + expect(sameWeek(tuesday, monday)).to.equal(true) + expect(sameWeek(saturday, monday)).to.equal(true) + + local nextSunday = DateTime.new(2017, 5, 14) + + expect(sameWeek(nextSunday, monday)).to.equal(false) + end) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Debug/ActionDebug.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Debug/ActionDebug.lua new file mode 100644 index 0000000..a0f982d --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Debug/ActionDebug.lua @@ -0,0 +1,223 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Create = require(LuaChat.Create) +local Signal = require(Common.Signal) + +local COLOR_BUTTON_ENABLED = Color3.new(0.85, 0.85, 0.85) + +local ActionDebug = { + Updated = Signal.new(), + log = {}, + enabled = false, + updateQueued = false +} + +function ActionDebug:SetEnabled(value) + self.enabled = value +end + +function ActionDebug:QueueUpdate() + if not self.updateQueued then + spawn(function() + self.Updated:Fire() + end) + end +end + +function ActionDebug:AddAction(data) + if not self.enabled then + return "DEBUG DISABLED" + end + + local action = { + data = data, + status = "pending", + startTime = tick() + } + + table.insert(self.log, action) + + local actionId = #self.log + + self:QueueUpdate() + + return actionId +end + +function ActionDebug:SetActionMutated(actionId) + if not self.enabled then + return + end + + local action = self.log[actionId] + + if not action then + warn("Couldn't find action with ID " .. tostring(actionId)) + return + end + + action.status = "mutated" + action.mutatedTime = tick() + + self:QueueUpdate() +end + +function ActionDebug:FinishAction(actionId) + if not self.enabled then + return + end + + local action = self.log[actionId] + + if not action then + warn("Couldn't find action with ID " .. tostring(actionId)) + return + end + + action.status = "done" + action.endTime = tick() + + self:QueueUpdate() +end + +function ActionDebug:Render() + self.updateQueued = false + + local children = {} + + for key, action in ipairs(self.log) do + local actionType + local actionBody + + if type(action.data) == "function" then + actionType = "thunk" + actionBody = "" + elseif type(action.data) == "table" then + actionType = action.data.type + actionBody = game:GetService("HttpService"):JSONEncode(action.data) + else + actionType = type(action.data) + actionBody = tostring(action.data) + end + + local mutationDuration = 0 + if action.mutatedTime then + mutationDuration = (action.mutatedTime - action.startTime) * 1000 + end + + local duration = 0 + if action.endTime then + duration = (action.endTime - action.startTime) * 1000 + end + + local object = Create "Frame" { + Name = "log-" .. key, + LayoutOrder = key, + Size = UDim2.new(1, 0, 0, 34), + BackgroundColor3 = Color3.new(0.7, 0.7, 0.7), + + Create "Frame" { + Name = "Inner", + Size = UDim2.new(1, -16, 1, -2), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + + Create "UIListLayout" { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + Padding = UDim.new(0, 8) + }, + + Create "TextLabel" { + Name = "LogNumber", + LayoutOrder = 0, + BackgroundTransparency = 1, + Size = UDim2.new(0, 30, 1, 0), + TextXAlignment = Enum.TextXAlignment.Left, + Text = tostring(key) + }, + + Create "TextLabel" { + Name = "MutationTime", + LayoutOrder = 1, + BackgroundTransparency = 1, + Size = UDim2.new(0, 60, 1, 0), + TextXAlignment = Enum.TextXAlignment.Left, + Text = ("%.2f ms"):format(mutationDuration) + }, + + Create "TextLabel" { + Name = "ElapsedTime", + LayoutOrder = 2, + BackgroundTransparency = 1, + Size = UDim2.new(0, 60, 1, 0), + TextXAlignment = Enum.TextXAlignment.Left, + Text = ("%.2f ms"):format(duration) + }, + + Create "TextLabel" { + Name = "Status", + LayoutOrder = 3, + BackgroundTransparency = 1, + Size = UDim2.new(0, 60, 1, 0), + TextXAlignment = Enum.TextXAlignment.Left, + Text = action.status + }, + + Create "TextLabel" { + Name = "ActionType", + LayoutOrder = 4, + BackgroundTransparency = 1, + Size = UDim2.new(0, 150, 1, 0), + TextXAlignment = Enum.TextXAlignment.Left, + Text = actionType + }, + + Create "TextButton" { + Name = "Details", + LayoutOrder = 5, + Size = UDim2.new(0, 90, 1, -10), + Text = "Details", + BackgroundColor3 = COLOR_BUTTON_ENABLED, + + [Create.events] = { + MouseButton1Click = function() + print(("Action %d:\n"):format(key)) + print(actionBody) + print("\n") + end + } + } + } + } + + table.insert(children, object) + end + + local container = Create "ScrollingFrame"({ + Size = UDim2.new(0, 520, 1, 0), + Name = "ActionDebug", + Active = true, + + Create "UIListLayout" { + SortOrder = Enum.SortOrder.LayoutOrder + } + }, children) + + local height = 0 + for _, child in ipairs(children) do + height = height + child.AbsoluteSize.Y + end + + container.CanvasSize = UDim2.new(0, 350, 0, height) + + return container +end + +return ActionDebug \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Debug/DebugManager.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Debug/DebugManager.lua new file mode 100644 index 0000000..d777ad7 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Debug/DebugManager.lua @@ -0,0 +1,189 @@ +local UserInputService = game:GetService("UserInputService") +local RunService = game:GetService("RunService") + +local LuaChat = script.Parent.Parent +local Create = require(LuaChat.Create) +local HttpDebug = require(script.Parent.HttpDebug) +local ActionDebug = require(script.Parent.ActionDebug) + +local DebugManager = { + connections = {}, + screenGui = nil, + container = nil, + root = nil, + + http = nil, + action = nil, + + running = false, + open = false +} + +function DebugManager:Initialize(root) + self.root = root + + HttpDebug:SetEnabled(true) + ActionDebug:SetEnabled(true) +end + +function DebugManager:Start() + if self.running then + warn("DebugManager is already running!") + return + end + + self.running = true + self.httpDirty = false + self.actionDirty = false + + self.screenGui = Instance.new("ScreenGui") + self.screenGui.Name = "ChatDebugScreen" + self.screenGui.DisplayOrder = 1e6 + self.screenGui.Parent = self.root + + self.container = Create "Frame" { + Name = "DebugContainer", + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + + Create "UIListLayout" { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Top + } + } + self.container.Parent = self.screenGui + + do + local connection = UserInputService.InputBegan:Connect(function(input, gameProcessed) + if input.UserInputType == Enum.UserInputType.Keyboard then + if input.KeyCode == Enum.KeyCode.H and UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) then + self:ToggleOpen() + end + end + end) + table.insert(self.connections, connection) + end + + do + local connection = HttpDebug.Updated:Connect(function() + if not self.open then + return + end + + self.httpDirty = true + end) + table.insert(self.connections, connection) + end + + do + local connection = ActionDebug.Updated:Connect(function() + if not self.open then + return + end + + self.actionDirty = true + end) + table.insert(self.connections, connection) + end + + do + local connection = RunService.Heartbeat:Connect(function() + if not self.open then + return + end + + if self.httpDirty then + self.httpDirty = false + + if self.http then + self.http:Destroy() + end + + self.http = HttpDebug:Render() + self.http.LayoutOrder = 2 + self.http.Parent = self.container + end + + if self.actionDirty then + self.actionDirty = false + + if self.action then + self.action:Destroy() + end + + self.action = ActionDebug:Render() + self.action.LayoutOrder = 1 + self.action.Parent = self.container + end + end) + table.insert(self.connections, connection) + end +end + +function DebugManager:Stop() + if not self.running then + warn("DebugManager is already stopped!") + return + end + + self.running = false + + self:Close() + + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + + self.connections = {} + + self.screenGui:Destroy() + self.screenGui = nil + self.container = nil +end + +function DebugManager:Open() + if self.open then + return + end + + self.open = true + + if self.action then + self.action:Destroy() + end + + self.action = ActionDebug:Render() + self.action.LayoutOrder = 1 + self.action.Parent = self.container + + if self.http then + self.http:Destroy() + end + + self.http = HttpDebug:Render() + self.http.LayoutOrder = 2 + self.http.Parent = self.container + + self.container.Visible = true +end + +function DebugManager:Close() + if not self.open then + return + end + + self.open = false + + self.container.Visible = false +end + +function DebugManager:ToggleOpen() + if self.open then + self:Close() + else + self:Open() + end +end + +return DebugManager \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Debug/HttpDebug.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Debug/HttpDebug.lua new file mode 100644 index 0000000..22d742f --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Debug/HttpDebug.lua @@ -0,0 +1,210 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Create = require(LuaChat.Create) +local Signal = require(Common.Signal) + +local COLOR_BUTTON_DISABLED = Color3.new(0.6, 0.6, 0.6) +local COLOR_BUTTON_ENABLED = Color3.new(0.85, 0.85, 0.85) + +local function reportButton(order, displayName, value) + local enabled = (value ~= nil) + + return Create "TextButton" { + Name = displayName, + LayoutOrder = 4, + Size = UDim2.new(0, 90, 1, -10), + Text = displayName, + AutoButtonColor = enabled, + BackgroundColor3 = enabled and COLOR_BUTTON_ENABLED or COLOR_BUTTON_DISABLED, + BorderSizePixel = enabled and 1 or 0, + + [Create.events] = { + MouseButton1Click = function() + if value == nil then + return + end + + print("\n") + print(value) + print("\n") + end + } + } +end + +local function getColor(status) + if status == "pending" then + return Color3.fromRGB(255, 255, 0) + elseif type(status) == "number" then + if status < 100 or status >= 400 then + return Color3.fromRGB(200, 0, 0) + else + return Color3.fromRGB(0, 200, 0) + end + else + return Color3.fromRGB(128, 128, 128) + end +end + +local HttpDebug = { + Updated = Signal.new(), + log = {}, + enabled = false +} + +function HttpDebug:SetEnabled(value) + self.enabled = value +end + +function HttpDebug:AddRequest(method, url, requestBody) + if not self.enabled then + return + end + + local request = { + method = method, + url = url, + status = "pending", + startTime = tick(), + requestBody = requestBody, + stackTrace = debug.traceback() + } + + table.insert(self.log, request) + + local requestId = #self.log + + self.Updated:Fire() + + return requestId +end + +function HttpDebug:FinishRequest(requestId, status, response) + if not self.enabled then + return + end + + local request = self.log[requestId] + + if not request then + warn("Couldn't find request with ID " .. tostring(requestId)) + return + end + + request.status = status + request.response = response + request.endTime = tick() + + self.Updated:Fire() +end + +function HttpDebug:Render() + local children = {} + + for key, request in ipairs(self.log) do + local color = getColor(request.status) + local duration = 0 + + if request.endTime then + duration = (request.endTime - request.startTime) * 1000 + end + + local object = Create "Frame" { + Name = "log-" .. key, + LayoutOrder = key, + Size = UDim2.new(1, 0, 0, 40), + BackgroundColor3 = color, + + Create "Frame" { + Name = "Inner", + Size = UDim2.new(1, -16, 1, -8), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + + Create "UIListLayout" { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + Padding = UDim.new(0, 8) + }, + + Create "TextLabel" { + Name = "Method", + LayoutOrder = 0, + BackgroundTransparency = 1, + Size = UDim2.new(0, 60, 1, 0), + TextXAlignment = Enum.TextXAlignment.Left, + Text = request.method + }, + + Create "TextLabel" { + Name = "Status", + LayoutOrder = 1, + BackgroundTransparency = 1, + Size = UDim2.new(0, 60, 1, 0), + TextXAlignment = Enum.TextXAlignment.Left, + Text = request.status + }, + + Create "TextLabel" { + Name = "Duration", + LayoutOrder = 2, + BackgroundTransparency = 1, + Size = UDim2.new(0, 80, 1, 0), + TextXAlignment = Enum.TextXAlignment.Left, + Text = ("%.2f ms"):format(duration) + }, + + Create "TextButton" { + Name = "URL", + LayoutOrder = 3, + BackgroundTransparency = 1, + Size = UDim2.new(0, 375, 1, 0), + TextXAlignment = Enum.TextXAlignment.Left, + Text = request.url, + ClipsDescendants = true, + + [Create.events] = { + MouseButton1Click = function() + print("\n") + print(request.url) + print("\n") + end + } + }, + + reportButton(4, "Stack Trace", request.stackTrace), + reportButton(5, "Request", request.requestBody), + reportButton(6, "Response", request.response) + } + } + + table.insert(children, object) + end + + local container = Create "ScrollingFrame"({ + Size = UDim2.new(0, 940, 1, 0), + Name = "HttpDebug", + Active = true, + + Create "UIListLayout" { + SortOrder = Enum.SortOrder.LayoutOrder + } + }, children) + + local height = 0 + for _, child in ipairs(children) do + height = height + child.AbsoluteSize.Y + end + + container.CanvasSize = UDim2.new(0, 350, 0, height) + + return container +end + +return HttpDebug \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Device.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Device.lua new file mode 100644 index 0000000..07d9b98 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Device.lua @@ -0,0 +1,107 @@ +local UserInputService = game:GetService("UserInputService") +local GuiService = game:GetService("GuiService") +local RunService = game:GetService("RunService") +local CoreGui = game:GetService("CoreGui") +local Workspace = game:GetService("Workspace") + +local Modules = CoreGui.RobloxGui.Modules + +local Config = require(Modules.LuaApp.Config) +local Create = require(Modules.LuaChat.Create) +local SetPlatform = require(Modules.LuaApp.Actions.SetPlatform) +local SetFormFactor = require(Modules.LuaApp.Actions.SetFormFactor) +local FormFactor = require(Modules.LuaApp.Enum.FormFactor) + +local STATUS_BAR_HEIGHT_IOS = 20 +local STATUS_BAR_HEIGHT_ANDROID = 24 +local NAV_BAR_HEIGHT = 44 + +local FlagSettings = require(Modules.LuaApp.FlagSettings) + +local Device = {} + +local function simulateIOS() + local statusBarSize = Vector2.new(0, STATUS_BAR_HEIGHT_IOS) + local navBarSize = Vector2.new(0, NAV_BAR_HEIGHT) + local bottomBarSize = Vector2.new(0, 0) + --Pcall because Tests have a lower security context + pcall(function() + UserInputService:SendAppUISizes(statusBarSize, navBarSize, bottomBarSize) + GuiService:SetSafeZoneOffsets(0, 0, 0, 0) + end) +end + +local function simulateAndroid() + local statusBarSize = Vector2.new(0, STATUS_BAR_HEIGHT_ANDROID) + local navBarSize = Vector2.new(0, NAV_BAR_HEIGHT) + local bottomBarSize = Vector2.new(0, 0) + --Pcall because Tests have a lower security context + pcall(function() + UserInputService:SendAppUISizes(statusBarSize, navBarSize, bottomBarSize) + GuiService:SetSafeZoneOffsets(0, 0, 0, 0) + end) + + local screenGui = Create.new "ScreenGui" { + Name = "StudioShellSimulation", + DisplayOrder = 10, + + Create.new "Frame" { + Name = "StatusBar", + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 0, UserInputService.StatusBarSize.Y), + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(117, 117, 117), + } + } + screenGui.Parent = CoreGui +end + +local function getDevicePlatform() + if _G.__TESTEZ_RUNNING_TEST__ then + return Enum.Platform.None + end + + return UserInputService:GetPlatform() +end + +function Device.simulatePlatformIfInStudio(store) + if not FlagSettings.IsLuaAppDeterminingFormFactorAndPlatform() then + local function setFormFactor(viewportSize) + local formFactor = FormFactor.TABLET + if viewportSize.X <= 1 then + -- Camera.ViewportSize hasn't been properly set yet + formFactor = FormFactor.UNKNOWN + elseif viewportSize.Y > viewportSize.X then + formFactor = FormFactor.PHONE + end + store:dispatch(SetFormFactor(formFactor)) + end + local camera = Workspace:WaitForChild("Camera") + setFormFactor(camera.ViewportSize) + camera.Changed:Connect(function(prop) + if prop == "ViewportSize" then + setFormFactor(camera.ViewportSize) + end + end) + end + + if RunService:IsStudio() then + store:dispatch(SetPlatform(Config.General.SimulatePlatform)) + + if Config.General.SimulatePlatform == Enum.Platform.IOS then + simulateIOS() + elseif Config.General.SimulatePlatform == Enum.Platform.Android then + simulateAndroid() + end + else + -- We only want to set Platform here if: + -- 1) We are not in Studio + -- 2) LuaApp is not determining Platform in LuaApp/App.lua + if not FlagSettings.IsLuaAppDeterminingFormFactorAndPlatform() then + local platform = getDevicePlatform() + store:dispatch(SetPlatform(platform)) + end + end +end + +return Device \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/DialogInfo.lua b/Client2018/content/internal/Chat/Modules/LuaChat/DialogInfo.lua new file mode 100644 index 0000000..95f29f8 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/DialogInfo.lua @@ -0,0 +1,79 @@ +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules + +local FormFactor = require(Modules.LuaApp.Enum.FormFactor) + +local DialogInfo = {} + +DialogInfo.Intent = { + BrowseGames = "BrowseGames", + ConversationHub = "ConversationHub", + Conversation = "Conversation", + GroupDetail = "GroupDetail", + NewChatGroup = "NewChatGroup", + CreateChat = "CreateChat", + EditChatGroup = "EditChatGroup", + EditGroupName = "EditGroupName", + ParticipantOptions = "ParticipantOptions", + RemoveUser = "RemoveUser", + LeaveGroup = "LeaveGroup", + DefaultScreen = "DefaultScreen", + GenericDialog = "GenericDialog", + GameShare = "GameShare", +} + +DialogInfo.DialogType = { + Centered = "Centered", + Left = "Left", + Right = "Right", + Modal = "Modal", + Popup = "Popup", +} + +DialogInfo.DialogTypeMap = { + [FormFactor.PHONE] = { + [DialogInfo.Intent.BrowseGames] = DialogInfo.DialogType.Centered, + [DialogInfo.Intent.ConversationHub] = DialogInfo.DialogType.Centered, + [DialogInfo.Intent.Conversation] = DialogInfo.DialogType.Centered, + [DialogInfo.Intent.GroupDetail] = DialogInfo.DialogType.Centered, + [DialogInfo.Intent.NewChatGroup] = DialogInfo.DialogType.Centered, + [DialogInfo.Intent.CreateChat] = DialogInfo.DialogType.Centered, + [DialogInfo.Intent.EditChatGroup] = DialogInfo.DialogType.Centered, + [DialogInfo.Intent.EditGroupName] = DialogInfo.DialogType.Centered, + [DialogInfo.Intent.ParticipantOptions] = DialogInfo.DialogType.Centered, + [DialogInfo.Intent.RemoveUser] = DialogInfo.DialogType.Centered, + [DialogInfo.Intent.LeaveGroup] = DialogInfo.DialogType.Centered, + [DialogInfo.Intent.DefaultScreen] = DialogInfo.DialogType.Centered, + [DialogInfo.Intent.GenericDialog] = DialogInfo.DialogType.Popup, + [DialogInfo.Intent.GameShare] = DialogInfo.DialogType.Popup, + }, + [FormFactor.TABLET] = { + [DialogInfo.Intent.BrowseGames] = DialogInfo.DialogType.Modal, + [DialogInfo.Intent.ConversationHub] = DialogInfo.DialogType.Left, + [DialogInfo.Intent.Conversation] = DialogInfo.DialogType.Right, + [DialogInfo.Intent.GroupDetail] = DialogInfo.DialogType.Right, + [DialogInfo.Intent.NewChatGroup] = DialogInfo.DialogType.Modal, + [DialogInfo.Intent.CreateChat] = DialogInfo.DialogType.Modal, + [DialogInfo.Intent.EditChatGroup] = DialogInfo.DialogType.Modal, + [DialogInfo.Intent.EditGroupName] = DialogInfo.DialogType.Right, + [DialogInfo.Intent.ParticipantOptions] = DialogInfo.DialogType.Right, + [DialogInfo.Intent.RemoveUser] = DialogInfo.DialogType.Right, + [DialogInfo.Intent.LeaveGroup] = DialogInfo.DialogType.Right, + [DialogInfo.Intent.DefaultScreen] = DialogInfo.DialogType.Right, + [DialogInfo.Intent.GenericDialog] = DialogInfo.DialogType.Popup, + [DialogInfo.Intent.GameShare] = DialogInfo.DialogType.Popup, + }, +} + +function DialogInfo.GetTypeBasedOnIntent(formFactor, intent) + local formFactorTypeMap = DialogInfo.DialogTypeMap[formFactor] + local dialogType = formFactorTypeMap[intent] + + if not dialogType then + return DialogInfo.DialogType.Centered + end + + return dialogType +end + +return DialogInfo diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/FlagSettings.lua b/Client2018/content/internal/Chat/Modules/LuaChat/FlagSettings.lua new file mode 100644 index 0000000..6777121 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/FlagSettings.lua @@ -0,0 +1,69 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") + +local FormFactor = require(Modules.LuaApp.Enum.FormFactor) + +-- Note: Can fail when called, GetPlatform requires restricted permissions: +local ok, platform = pcall(function() + return UserInputService:GetPlatform() +end) +if not ok then + platform = Enum.Platform.None + warn("FlagSettings - couldn't identify platform.") +end + +-- Read all the flags up front. This is to throw an exception at import time +-- if they don't exist, while also letting them get picked up by scanners: +local luaChatPlayTogetherThrottleiOSPhone = settings():GetFVariable("LuaChatPlayTogetherThrottleiOSPhone3") +local luaChatPlayTogetherThrottleiOSTablet = settings():GetFVariable("LuaChatPlayTogetherThrottleiOSTablet3") +local luaChatPlayTogetherThrottleAndroidPhone = settings():GetFVariable("LuaChatPlayTogetherThrottleAndroidPhone3") +local luaChatPlayTogetherThrottleAndroidTablet = settings():GetFVariable("LuaChatPlayTogetherThrottleAndroidTablet3") +local luaChatUseCppTextTruncation = settings():GetFFlag("LuaChatUseCppTextTruncation") +local textTruncationEnabled = settings():GetFFlag("TextTruncationEnabled") + +local FlagSettings = {} + +-- Helper function to throttle based on player Id: +function FlagSettings.ThrottleUserId(throttle, userId) + assert(type(throttle) == "number") + assert(type(userId) == "number") + + -- Determine userRollout using last two digits of user ID: + -- (+1 to change range from 0-99 to 1-100 as 0 is off, 100 is full on): + local userRollout = (userId % 100) + 1 + return userRollout <= throttle +end + +function FlagSettings.IsLuaChatPlayTogetherEnabled(formFactor) + -- Read throttle value based on platform and form factor: + -- Note: defaults to iOS Tablet in Studio: + local throttle + if platform == Enum.Platform.Android then + if formFactor == FormFactor.PHONE then + throttle = luaChatPlayTogetherThrottleAndroidPhone + else + throttle = luaChatPlayTogetherThrottleAndroidTablet + end + else + if formFactor == FormFactor.PHONE then + throttle = luaChatPlayTogetherThrottleiOSPhone + else + throttle = luaChatPlayTogetherThrottleiOSTablet + end + end + + local throttleNumber = tonumber(throttle) + if throttleNumber == nil then + return false + end + + local userId = Players.LocalPlayer.UserId + return FlagSettings.ThrottleUserId(throttleNumber, userId) +end + +function FlagSettings.UseCppTextTruncation() + return luaChatUseCppTextTruncation and textTruncationEnabled +end + +return FlagSettings \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/FlagSettings.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/FlagSettings.spec.lua new file mode 100644 index 0000000..52daa05 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/FlagSettings.spec.lua @@ -0,0 +1,69 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local FlagSettings = require(script.Parent.FlagSettings) + + describe("ThrottleUserId", function() + it("should always reject zero%", function() + local gating = FlagSettings.ThrottleUserId(0, 10000) + expect(gating).to.equal(false) + + gating = FlagSettings.ThrottleUserId(0, 10001) + expect(gating).to.equal(false) + + gating = FlagSettings.ThrottleUserId(0, 10025) + expect(gating).to.equal(false) + + gating = FlagSettings.ThrottleUserId(0, 10075) + expect(gating).to.equal(false) + + gating = FlagSettings.ThrottleUserId(0, 10099) + expect(gating).to.equal(false) + + gating = FlagSettings.ThrottleUserId(0, 10100) + expect(gating).to.equal(false) + end) + + it("should always accept 100%", function() + local gating = FlagSettings.ThrottleUserId(100, 10000) + expect(gating).to.equal(true) + + gating = FlagSettings.ThrottleUserId(100, 10001) + expect(gating).to.equal(true) + + gating = FlagSettings.ThrottleUserId(100, 10025) + expect(gating).to.equal(true) + + gating = FlagSettings.ThrottleUserId(100, 10075) + expect(gating).to.equal(true) + + gating = FlagSettings.ThrottleUserId(100, 10099) + expect(gating).to.equal(true) + + gating = FlagSettings.ThrottleUserId(100, 10100) + expect(gating).to.equal(true) + end) + + it("should reject IDs over throttle percent", function() + local gating = FlagSettings.ThrottleUserId(25, 10050) + expect(gating).to.equal(false) + + gating = FlagSettings.ThrottleUserId(50, 10075) + expect(gating).to.equal(false) + + gating = FlagSettings.ThrottleUserId(75, 10099) + expect(gating).to.equal(false) + end) + + it("should accept IDs under throttle percent", function() + local gating = FlagSettings.ThrottleUserId(1, 10100) + expect(gating).to.equal(true) + + gating = FlagSettings.ThrottleUserId(10, 10109) + expect(gating).to.equal(true) + + gating = FlagSettings.ThrottleUserId(25, 10023) + expect(gating).to.equal(true) + end) + end) +end diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/HeadshotLoader.lua b/Client2018/content/internal/Chat/Modules/LuaChat/HeadshotLoader.lua new file mode 100644 index 0000000..a93ae0f --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/HeadshotLoader.lua @@ -0,0 +1,52 @@ +local CoreGui = game:GetService("CoreGui") +local PlayersService = game:GetService("Players") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common + +local Functional = require(Common.Functional) + +local THUMB_TYPE = Enum.ThumbnailType.HeadShot +local THUMB_SIZE = Enum.ThumbnailSize.Size48x48 + +local HeadshotLoader = {} +HeadshotLoader.uriCache = {} +HeadshotLoader.requestPools = {} + +function HeadshotLoader:Load(imageObject, userId) + if (not imageObject) or (not userId) then + return + end + + local hit = self.uriCache[userId] + if hit then + imageObject.Image = hit + return + end + + local pool = self.requestPools[userId] + if not pool then + pool = { imageObject } + self.requestPools[userId] = pool + else + if not Functional.Find(pool, imageObject) then + table.insert(pool, imageObject) + end + return + end + + spawn(function() + local uri, _ = PlayersService:GetUserThumbnailAsync(userId, THUMB_TYPE, THUMB_SIZE) + + if uri then + for i = 1, #pool do + pool[i].Image = uri + end + HeadshotLoader.uriCache[userId] = uri + end + + HeadshotLoader.requestPools[userId] = nil + end) +end + +return HeadshotLoader diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Models/Alert.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Models/Alert.lua new file mode 100644 index 0000000..369543d --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Models/Alert.lua @@ -0,0 +1,27 @@ +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules +local LuaApp = Modules.LuaApp + +local MockId = require(LuaApp.MockId) + +local Alert = {} + +Alert.AlertType = { + DIALOG = "DIALOG", +} + +function Alert.new(titleKey, messageKey, messageArguments, alertType) + local self = {} + + self.titleKey = titleKey + self.messageKey = messageKey + self.messageArguments = messageArguments + self.createdAt = tick() + self.id = MockId() + self.type = alertType + + return self +end + +return Alert + diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Models/AssetInfo.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Models/AssetInfo.lua new file mode 100644 index 0000000..d522e93 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Models/AssetInfo.lua @@ -0,0 +1,33 @@ +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules +local LuaApp = Modules.LuaApp + +local MockId = require(LuaApp.MockId) + +local AssetInfo = {} + +function AssetInfo.new() + local self = {} + + return self +end + +function AssetInfo.mock(mergeTable) + local self = AssetInfo.new(mergeTable) + + self.id = MockId() + self.Name = "BarricadeZ" + self.AssetId = 1055125381 + self.AssetTypeId = Enum.AssetType.Place.Value + self.Description = "The Best Game Ever" + + if mergeTable ~= nil then + for key, value in pairs(mergeTable) do + self[key] = value + end + end + + return self +end + +return AssetInfo \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Models/Conversation.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Models/Conversation.lua new file mode 100644 index 0000000..5aa4fbc --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Models/Conversation.lua @@ -0,0 +1,183 @@ +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local MockId = require(LuaApp.MockId) +local OrderedMap = require(LuaChat.OrderedMap) +local Constants = require(LuaChat.Constants) +local DateTime = require(LuaChat.DateTime) + +local Players = game:GetService("Players") + +local function getMessageId(message) + return message.id +end + +local function messageSortPredicate(a, b) + local aValue = a.sent:GetUnixTimestamp() + local bValue = b.sent:GetUnixTimestamp() + + return aValue < bValue +end + +local function sendingMessageSortPredicate(a, b) + return a.order < b.order +end + +local Conversation = {} + +Conversation.Type = { + MULTI_USER_CONVERSATION = "MultiUserConversation", + ONE_TO_ONE_CONVERSATION = "OneToOneConversation", +} + +function Conversation.IdForFakeOneOnOne(participants) + --For any two users, there can only exist a single one-to-one + --conversation. We can use this invariant to create a unique id + --that persists before and after it's created on the server. + local id = Conversation.Type.ONE_TO_ONE_CONVERSATION + table.sort(participants) + for _, userId in ipairs(participants) do + id = id.."-"..tostring(userId) + end + return id +end + +function Conversation.new() + local self = {} + + return self +end + +function Conversation.mock(mergeTable) + local self = Conversation.new() + + self.messages = OrderedMap.new(getMessageId, messageSortPredicate) + self.sendingMessages = OrderedMap.new(getMessageId, sendingMessageSortPredicate) + self.id = MockId() + --I'm adding a clientId here so that the NewChatGroup view can know that a + --conversation was successfully saved. We could also do a callback, + --but this seems to fit more cleanly with the flux architecture we're already using + --...though we could also create a second store for pending conversations, store.PendingConversations + self.clientId = MockId() + self.title = "" + self.initiator = MockId() + self.hasUnreadMessages = false + self.conversationType = Conversation.Type.MULTI_USER_CONVERSATION + self.participants = {} + self.usersTyping = {} + self.isUserLeaving = false + self.fetchingOlderMessages = false + self.fetchedOldestMessage = false + self.serverState = Constants.ServerState.NONE + self.pinnedGame = {} + self.pinnedGame.universeId = MockId() + self.pinnedGame.rootPlaceId = MockId() + + self.lastUpdated = DateTime.now() + + self.titleForViewer = "title" + self.isDefaultTitle = true + + if mergeTable ~= nil then + for key, value in pairs(mergeTable) do + self[key] = value + end + end + + return self +end + +function Conversation.fromUser(user) + + local self = Conversation.new() + + self.messages = OrderedMap.new(getMessageId, messageSortPredicate) + self.sendingMessages = OrderedMap.new(getMessageId, sendingMessageSortPredicate) + self.clientId = MockId() + self.title = user.name + self.initiator = nil + self.hasUnreadMessages = false + self.conversationType = Conversation.Type.ONE_TO_ONE_CONVERSATION + self.participants = { tostring(Players.LocalPlayer.UserId), user.id } + self.usersTyping = {} + self.isUserLeaving = false + self.fetchingOlderMessages = false + self.fetchedOldestMessage = false + self.serverState = Constants.ServerState.NONE + + self.id = Conversation.IdForFakeOneOnOne(self.participants) + + self.lastUpdated = nil + + self.isDefaultTitle = true + + return self +end + +function Conversation.empty(mergeTable) + local self = Conversation.new() + + self.id = "-1" + self.messages = OrderedMap.new(getMessageId, messageSortPredicate) + self.sendingMessages = OrderedMap.new(getMessageId, sendingMessageSortPredicate) + self.title = "" + self.initiator = "0" + self.hasUnreadMessages = false + self.conversationType = Conversation.Type.MULTI_USER_CONVERSATION + self.participants = {} + self.usersTyping = {} + self.isUserLeaving = false + self.fetchingOlderMessages = false + self.serverState = Constants.ServerState.NONE + self.pinnedGame = {} + + self.lastUpdated = DateTime.now() + + self.isDefaultTitle = true + + if mergeTable ~= nil then + for key, value in pairs(mergeTable) do + self[key] = value + end + end + + return self +end + +function Conversation.fromWeb(data, clientId) + local self = Conversation.new() + + self.messages = OrderedMap.new(getMessageId, messageSortPredicate) + self.sendingMessages = OrderedMap.new(getMessageId, sendingMessageSortPredicate) + self.id = tostring(data.id) + self.clientId = clientId or MockId() + self.title = data.title + self.initiator = tostring(data.initiator.targetId) + self.hasUnreadMessages = data.hasUnreadMessages + self.conversationType = data.conversationType + self.participants = {} + self.usersTyping = {} + self.isUserLeaving = false + self.fetchingOlderMessages = false + self.serverState = Constants.ServerState.CREATED + self.pinnedGame = {} + if data.conversationUniverse ~= nil then + self.pinnedGame.universeId = tostring(data.conversationUniverse.universeId) + self.pinnedGame.rootPlaceId = tostring(data.conversationUniverse.rootPlaceId) + end + + self.lastUpdated = DateTime.fromIsoDate(data.lastUpdated) + + self.titleForViewer = data.conversationTitle.titleForViewer + self.isDefaultTitle = data.conversationTitle.isDefaultTitle + + for _, webParticipant in ipairs(data.participants) do + table.insert(self.participants, tostring(webParticipant.targetId)) + end + + return self +end + +return Conversation \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Models/GameParams.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Models/GameParams.lua new file mode 100644 index 0000000..b1e861e --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Models/GameParams.lua @@ -0,0 +1,17 @@ +local GameParams = {} + +function GameParams.new() + local self = {} + + return self +end + +function GameParams.fromPlaceId(placeId) + local self = GameParams.new() + + self.placeId = placeId + + return self +end + +return GameParams \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Models/Message.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Models/Message.lua new file mode 100644 index 0000000..a605e65 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Models/Message.lua @@ -0,0 +1,73 @@ +local CoreGui = game:GetService("CoreGui") +local Players = game:GetService("Players") + +local Modules = CoreGui.RobloxGui.Modules +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local MockId = require(LuaApp.MockId) +local DateTime = require(LuaChat.DateTime) + +local Message = {} + +function Message.new() + local self = {} + + return self +end + +function Message.mock(mergeTable) + local self = Message.new() + + self.id = MockId() + self.senderTargetId = MockId() + self.conversationId = MockId() + self.senderType = "MESSAGE SENDERTYPE" + self.content = "MESSAGE CONTENT" + self.read = false + self.sent = DateTime.now() + self.previousMessageId = nil + self.filteredForReceivers = false + + if mergeTable ~= nil then + for key, value in pairs(mergeTable) do + self[key] = value + end + end + + return self +end + +function Message.fromWeb(data, conversationId, previousMessageId) + local self = Message.new() + + self.id = data.id + self.senderTargetId = tostring(data.senderTargetId) + self.senderType = data.senderType + self.content = data.content + self.read = data.read + self.sent = DateTime.fromIsoDate(data.sent) + self.conversationId = tostring(conversationId) + self.previousMessageId = previousMessageId + self.filteredForReceivers = false + + return self +end + +function Message.fromSentWeb(data, conversationId, previousMessageId) + local self = Message.new() + + self.id = data.messageId + self.senderTargetId = tostring(Players.LocalPlayer.UserId) + self.senderType = "User" + self.content = data.content + self.read = true + self.sent = DateTime.fromIsoDate(data.sent) + self.conversationId = tostring(conversationId) + self.previousMessageId = previousMessageId + self.filteredForReceivers = data.filteredForReceivers + + return self +end + +return Message \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Models/PlaceInfoModel.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Models/PlaceInfoModel.lua new file mode 100644 index 0000000..5d34aba --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Models/PlaceInfoModel.lua @@ -0,0 +1,42 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local LuaApp = Modules.LuaApp + +local MockId = require(LuaApp.MockId) + +local PlaceInfoModel = {} + +function PlaceInfoModel.new() + local self = {} + + return self +end + +function PlaceInfoModel.mock() + local self = PlaceInfoModel.new() + + self.builder = "builder" + self.builderId = MockId() + self.description = "description" + self.imageToken = MockId() + self.isPlayable = true + self.name = "name" + self.placeId = MockId() + self.price = 0 + self.reasonProhibited = nil + self.universeId = MockId() + self.universeRootPlaceId = MockId() + self.url = "url" + + return self +end + +function PlaceInfoModel.fromWeb(data) + local self = data or {} + self.placeId = tostring(self.placeId) + + return self +end + +return PlaceInfoModel \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Models/TabPageParameters.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Models/TabPageParameters.lua new file mode 100644 index 0000000..1de6f24 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Models/TabPageParameters.lua @@ -0,0 +1,9 @@ +return function(title, pageView, configs) + return { + title = title, + content = { + component = pageView, + options = configs + }, + } +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Models/ThumbnailModel.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Models/ThumbnailModel.lua new file mode 100644 index 0000000..2f62990 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Models/ThumbnailModel.lua @@ -0,0 +1,26 @@ +local ThumbnailModel = {} + +function ThumbnailModel.new() + local self = {} + + return self +end + +function ThumbnailModel.mock() + local self = ThumbnailModel.new() + + self.image = "imageToken" + self.width = 0 + self.height = 0 + + return self +end + +function ThumbnailModel.fromWeb(data) + local self = {} + self.image = data or "" + + return self +end + +return ThumbnailModel \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Models/ToastModel.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Models/ToastModel.lua new file mode 100644 index 0000000..9439b1b --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Models/ToastModel.lua @@ -0,0 +1,14 @@ +local ToastModel = {} + +function ToastModel.new(id, messageKey, messageArguments) + local self = {} + + self.messageKey = messageKey + self.messageArguments = messageArguments + self.id = id + + return self +end + +return ToastModel + diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/NotificationBroadcaster.lua b/Client2018/content/internal/Chat/Modules/LuaChat/NotificationBroadcaster.lua new file mode 100644 index 0000000..1d93c28 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/NotificationBroadcaster.lua @@ -0,0 +1,53 @@ +local GuiService = game:GetService("GuiService") +local NotificationService = game:GetService("NotificationService") + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local NotificationType = require(Modules.LuaApp.Enum.NotificationType) + +local NotificationBroadcaster = {} +NotificationBroadcaster.__index = NotificationBroadcaster + +function NotificationBroadcaster.new(store) + local self = { + store = store, + } + setmetatable(self, NotificationBroadcaster) + + self.unreadConversationCount = 0 + self.hasLoadedConversations = false + + self.storeConnection = store.Changed:Connect(function(state, oldState) + self:Update(state, oldState) + end) + + return self +end + +function NotificationBroadcaster:Update(state, oldState) + if state == oldState then + return + end + + if state.ChatAppReducer.UnreadConversationCount ~= oldState.ChatAppReducer.UnreadConversationCount then + local currentUnreadConversationCount = state.ChatAppReducer.UnreadConversationCount + local count = currentUnreadConversationCount > 0 and tostring(currentUnreadConversationCount) or "" + GuiService:BroadcastNotification(count, NotificationType.UNREAD_COUNT) + end + + if not self.hasLoadedConversations then + local hasLoadedConversations = next(state.ChatAppReducer.Conversations) ~= nil + if hasLoadedConversations and not state.Startup.Preloading then + NotificationService:ActionEnabled(Enum.AppShellActionType.TapConversationEntry) + self.hasLoadedConversations = true + end + end +end + +function NotificationBroadcaster:Destruct() + if self.storeConnection then + self.storeConnection:Disconnect() + end +end + +return NotificationBroadcaster \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/OrderedMap.lua b/Client2018/content/internal/Chat/Modules/LuaChat/OrderedMap.lua new file mode 100644 index 0000000..4fc8b71 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/OrderedMap.lua @@ -0,0 +1,312 @@ +local OrderedMap = {} + +OrderedMap.__index = OrderedMap + +--[[ + Create a new OrderedMap with the given ID function and sort predicates. + + getId might look like: + + function(object) + return object.id + end + + And the sortPredicate might look like: + + function(objectA, objectB) + return objectA.id < objectB.id + end + + The rest of the arguments are inserted into the OrderedMap as values. +]] +function OrderedMap.new(getId, sortPredicate, ...) + local self = { + keys = {}, + values = {}, + getId = getId, + sortPredicate = sortPredicate + } + + setmetatable(self, OrderedMap) + + OrderedMap._InsertInPlace(self, ...) + + return self +end + +--[[ + Gets the value in the map associated with the given ID. +]] +function OrderedMap:Get(id) + return self.values[id] +end + +--[[ + Gets the value in the map located at the given index. +]] +function OrderedMap:GetByIndex(index) + local id = self.keys[index] + + if id == nil then + return nil + end + + return self.values[id] +end + +--[[ + Gets the list of IDs that are present in the OrderedMap. +]] +function OrderedMap:GetIds() + return self.keys +end + +--[[ + Returns the number of elements in the OrderedMap. +]] +function OrderedMap:Length() + return #self.keys +end + +--[[ + Deletes the given keys from the map. + + This is an immutable operation, so the original map is not modified. + + Example: + + map = map:Delete("my-key", "another-key") +]] +function OrderedMap:Delete(...) + local new = OrderedMap.new(self.getId, self.sortPredicate) + + local len = select("#", ...) + + for key, value in pairs(self.values) do + new.values[key] = value + end + + for i = 1, len do + local key = select(i, ...) + + new.values[key] = nil + end + + for _, value in ipairs(self.keys) do + if new.values[value] ~= nil then + new.keys[#(new.keys)+1] = value + end + end + + return new +end + +--[[ + Inserts the given values into the map. + + This is an immutable operation, so the original map is not modified and a + new map is returned instead. + + Example: + + map = map:Insert("Hello", "World") +]] +function OrderedMap:Insert(...) + local new = self:_Copy() + + OrderedMap._InsertInPlace(new, ...) + + return new +end + +--[[ + Returns the first value in the OrderedMap. +]] +function OrderedMap:First() + if self.keys[1] then + return self:Get(self.keys[1]) + end +end + +--[[ + Returns the last value in the OrderedMap. +]] +function OrderedMap:Last() + local i = #self.keys + if self.keys[i] then + return self:Get(self.keys[i]) + end +end + +--[[ + Create an iterator to traverse the map front-to-back. + + Example: + + for id, item in map:CreateIterator() do + print(id, item) + end +]] +function OrderedMap:CreateIterator() + local i = 0 + local length = #self.keys + + -- Iterator function + return function() + i = i + 1 + if i <= length then + local key = self.keys[i] + return key, self.values[key], i + end + end +end + +--[[ + Create an iterator to traverse the map back-to-front. + + Example: + + for id, item in map:CreateReverseIterator() do + print(id, item) + end +]] +function OrderedMap:CreateReverseIterator() + local i = #self.keys + 1 + + -- Iterator function + return function() + i = i - 1 + if i > 0 then + local key = self.keys[i] + return key, self.values[key], i + end + end +end + +--[[ + Create a new OrderedMap, applying the given predicate to each element in + this OrderedMap. + + Analogous to 'map' on a list. + + Example: + + local doubled = map:Map(function(value, key) + return value * 2 + end) +]] +function OrderedMap:Map(predicate) + local new = OrderedMap.new(self.getId, self.sortPredicate) + + for key, value in self:CreateIterator() do + new:_InsertInPlaceUnsorted(predicate(value, key)) + end + + new:_Sort() + + return new +end + +--[[ + Merges two or more OrderedMap objects by combining their values. + + Values from the right-most arguments will overwrite values from the left. + + Example: + + local merged = OrderedMap.Merge(a, b, c) + + OR + + local merged = a:Merge(b, c) + + NOTE: This function is not guaranteed to create a new OrderedMap. +]] +function OrderedMap:Merge(...) + local new + + for i = 1, select("#", ...) do + local other = select(i, ...) + + if other:Length() > 0 then + if not new then + new = self:_Copy() + end + + for _, value in other:CreateIterator() do + new:_InsertInPlaceUnsorted(value) + end + end + end + + if new then + new:_Sort() + + return new + end + + return self +end + +--[[ + Internal method for creating a copy of this OrderedMap. +]] +function OrderedMap:_Copy() + local new = OrderedMap.new(self.getId, self.sortPredicate) + + for key, value in ipairs(self.keys) do + new.keys[key] = value + end + + for key, value in pairs(self.values) do + new.values[key] = value + end + + return new +end + +--[[ + Internal method for inserting a value without sorting the map. + + This means that the invariants that the map exposes will be broken until + the _Sort() method is called. +]] +function OrderedMap:_InsertInPlaceUnsorted(...) + local len = select("#", ...) + + for i = 1, len do + local item = select(i, ...) + local key = self.getId(item) + + if not self.values[key] then + table.insert(self.keys, key) + end + + self.values[key] = item + end +end + +--[[ + Sorts the map; used in cases where the map would become out-of-order when + using internal recommendations. +]] +function OrderedMap:_Sort() + table.sort(self.keys, function(keyA, keyB) + local a = self.values[keyA] + local b = self.values[keyB] + + return self.sortPredicate(a, b) + end) +end + +--[[ + Inserts the given values into the map in-place. + + This operation mutates the map; generally you should use Insert instead. +]] +function OrderedMap:_InsertInPlace(...) + self:_InsertInPlaceUnsorted(...) + self:_Sort() +end + +return OrderedMap \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/OrderedMap.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/OrderedMap.spec.lua new file mode 100644 index 0000000..40f61bd --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/OrderedMap.spec.lua @@ -0,0 +1,278 @@ +return function() + local OrderedMap = require(script.Parent.OrderedMap) + + local function getId(item) + return item + end + + local function compare(a, b) + return a < b + end + + describe("new", function() + it("should accept getId, a sort predicate and a list of items", function() + local map = OrderedMap.new(getId, compare, 3, 1, 2) + + expect(map).to.be.a("table") + expect(#map.keys).to.equal(3) + + for _, key in ipairs(map.keys) do + local value = map.values[key] + expect(value).to.equal(key) + end + end) + end) + + describe("Insert", function() + it("should insert elements in order", function() + local map = OrderedMap.new(getId, compare, 2) + + expect(map).to.be.a("table") + + map = map:Insert(1) + + expect(map).to.be.a("table") + + for _, key in ipairs(map.keys) do + local value = map.values[key] + expect(value).to.equal(key) + end + end) + end) + + describe("Delete", function() + it("should delete elements by key", function() + local map = OrderedMap.new(getId, compare, 1, 2, 3) + + expect(map).to.be.a("table") + + map = map:Delete(2) + + expect(map).to.be.a("table") + + for _, key in ipairs(map.keys) do + local value = map.values[key] + + expect(value).to.equal(key) + expect(value).never.to.equal(2) + end + end) + end) + + describe("First", function() + it("should return nil if no elements in the list", function() + local map = OrderedMap.new(getId, compare) + + expect(map).to.be.a("table") + + expect(map:First()).to.equal(nil) + end) + + it("should handle modifications", function() + local map = OrderedMap.new(getId, compare, 1, 2, 3) + + expect(map).to.be.a("table") + + expect(map:First()).to.equal(1) + + map = map:Delete(1) + + expect(map:First()).to.equal(2) + + map = map:Delete(2) + + expect(map:First()).to.equal(3) + + map = map:Delete(3) + + expect(map:First()).to.equal(nil) + + map = map:Insert(5) + + expect(map:First()).to.equal(5) + + map = map:Insert(1) + + expect(map:First()).to.equal(1) + end) + end) + + + describe("Last", function() + it("should return nil if no elements in the list", function() + local map = OrderedMap.new(getId, compare) + + expect(map).to.be.a("table") + + expect(map:Last()).to.equal(nil) + end) + + it("should handle modifications", function() + local map = OrderedMap.new(getId, compare, 1, 2, 3) + + expect(map).to.be.a("table") + + expect(map:Last()).to.equal(3) + + map = map:Delete(1) + + expect(map:Last()).to.equal(3) + + map = map:Delete(3) + + expect(map:Last()).to.equal(2) + + map = map:Delete(2) + + expect(map:Last()).to.equal(nil) + + map = map:Insert(5) + + expect(map:First()).to.equal(5) + end) + end) + + describe("CreateIterator", function() + it("should iterate elements in order", function() + local values = {"a", "b", "c", "d"} + local map = OrderedMap.new(getId, compare, unpack(values)) + + expect(map).to.be.ok() + + local lastIndex = 0 + + for key, value, index in map:CreateIterator() do + expect(key).to.equal(value) + expect(values[index]).to.equal(value) + + expect(index > lastIndex).to.equal(true) + lastIndex = index + end + end) + + it("should work on empty map", function() + local map = OrderedMap.new(getId, compare) + + expect(map).to.be.ok() + + for _, _ in map:CreateIterator() do + error("This should never be called!") + end + end) + end) + + describe("CreateReverseIterator", function() + it("should iterate elements in order", function() + local values = {"a", "b", "c", "d"} + local map = OrderedMap.new(getId, compare, unpack(values)) + + expect(map).to.be.ok() + + local lastIndex = math.huge + + for key, value, index in map:CreateReverseIterator() do + expect(key).to.equal(value) + expect(values[index]).to.equal(value) + + expect(index < lastIndex).to.equal(true) + lastIndex = index + end + end) + + it("should work on empty map", function() + local map = OrderedMap.new(getId, compare) + + expect(map).to.be.ok() + + for _, _ in map:CreateReverseIterator() do + error("This should never be called!") + end + end) + end) + + describe("Map", function() + it("should use the map predicate", function() + local map = OrderedMap.new(getId, compare, 1, 2, 3, 4) + + expect(map).to.be.ok() + + local result = map:Map(function(value, key) + expect(value).to.equal(key) + + return value * 2 + end) + + for index = 1, #result.keys do + local key = result.keys[index] + local value = result.values[key] + + expect(value).to.equal(index * 2) + end + end) + end) + + describe("Merge", function() + it("should merge one map", function() + local a = OrderedMap.new(getId, compare, 1, 2) + + expect(a).to.be.ok() + + local b = OrderedMap.Merge(a) + + -- This is an optimization + expect(b).to.equal(a) + end) + + it("should merge one map with multiple empty maps", function() + local a = OrderedMap.new(getId, compare, 1, 2) + local empty = OrderedMap.new(getId, compare) + + expect(a).to.be.ok() + expect(empty).to.be.ok() + + local b = OrderedMap.Merge(a, empty, empty, empty) + + -- This is a nifty optimization + expect(b).to.equal(a) + end) + + it("should merge values into a new map", function() + local a = OrderedMap.new(getId, compare, 1, 2) + local b = OrderedMap.new(getId, compare, 2, 3) + + expect(a).to.be.ok() + expect(b).to.be.ok() + + local c = OrderedMap.Merge(a, b) + + expect(c).to.never.equal(a) + expect(c).to.never.equal(b) + + expect(c:Length()).to.equal(3) + + for key, value, index in c:CreateIterator() do + expect(key).to.equal(value) + expect(key).to.equal(index) + end + end) + + it("should work with more than 2 arguments", function() + local a = OrderedMap.new(getId, compare, 1) + local b = OrderedMap.new(getId, compare, 2) + local c = OrderedMap.new(getId, compare, 3) + + local d = OrderedMap.Merge(a, b, c) + + expect(d).never.to.equal(a) + expect(d).never.to.equal(b) + expect(d).never.to.equal(c) + + expect(d:Length()).to.equal(3) + + for key, value, index in d:CreateIterator() do + expect(key).to.equal(value) + expect(key).to.equal(index) + end + end) + end) +end diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ActiveConversationId.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ActiveConversationId.lua new file mode 100644 index 0000000..b8ffa21 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ActiveConversationId.lua @@ -0,0 +1,14 @@ +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules + +local SetActiveConversationId = require(Modules.LuaChat.Actions.SetActiveConversationId) + +return function(state, action) + state = state or {} + + if action.type == SetActiveConversationId.name then + return action.conversationId + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/AppLoaded.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/AppLoaded.lua new file mode 100644 index 0000000..4ffbdad --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/AppLoaded.lua @@ -0,0 +1,14 @@ +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules + +local SetAppLoaded = require(Modules.LuaChat.Actions.SetAppLoaded) + +return function(state, action) + state = state or false + + if action.type == SetAppLoaded.name then + return action.value + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/AppLoaded.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/AppLoaded.spec.lua new file mode 100644 index 0000000..d012181 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/AppLoaded.spec.lua @@ -0,0 +1,26 @@ +return function() + local CoreGui = game:GetService("CoreGui") + local Modules = CoreGui.RobloxGui.Modules + local LuaChat = Modules.LuaChat + local SetAppLoaded = require(LuaChat.Actions.SetAppLoaded) + local AppLoaded = require(script.Parent.AppLoaded) + describe("Action AppLoaded", function() + it("should be unloaded by default", function() + local state = AppLoaded(nil, {}) + + expect(state).to.equal(false) + end) + + it("should be changed using SetAppLoaded", function() + local state = AppLoaded(nil, {}) + + state = AppLoaded(state, SetAppLoaded(false)) + + expect(state).to.equal(false) + + state = AppLoaded(state, SetAppLoaded(true)) + + expect(state).to.equal(true) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/AppState.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/AppState.lua new file mode 100644 index 0000000..4d954b6 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/AppState.lua @@ -0,0 +1,38 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules + +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local DeleteAlert = require(LuaChat.Actions.DeleteAlert) +local ShowAlert = require(LuaChat.Actions.ShowAlert) + +local Immutable = require(Common.Immutable) +local OrderedMap = require(LuaChat.OrderedMap) + +local function getAlertId(alert) + return alert.id +end + +local function alertSortPredicate(alert1, alert2) + return alert1.createdAt < alert2.createdAt +end + +return function(state, action) + state = state or {} + + if action.type == ShowAlert.name then + local alert = action.alert + local alerts = state["alerts"] or OrderedMap.new(getAlertId, alertSortPredicate) + alerts = OrderedMap.Insert(alerts, alert) + state = Immutable.Set(state, "alerts", alerts) + elseif action.type == DeleteAlert.name then + local alert = action.alert + local alerts = state["alerts"] or OrderedMap.new(getAlertId, alertSortPredicate) + alerts = OrderedMap.Delete(alerts, alert.id) + state = Immutable.Set(state, "alerts", alerts) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ChatEnabled.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ChatEnabled.lua new file mode 100644 index 0000000..c660051 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ChatEnabled.lua @@ -0,0 +1,14 @@ +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules + +local SetChatEnabled = require(Modules.LuaChat.Actions.SetChatEnabled) + +return function(state, action) + state = (state == nil) and true or state + + if action.type == SetChatEnabled.name then + return action.value + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ChatEnabled.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ChatEnabled.spec.lua new file mode 100644 index 0000000..165f741 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ChatEnabled.spec.lua @@ -0,0 +1,26 @@ +return function() + local CoreGui = game:GetService("CoreGui") + local Modules = CoreGui.RobloxGui.Modules + local LuaChat = Modules.LuaChat + local SetChatEnabled = require(LuaChat.Actions.SetChatEnabled) + + local ChatEnabled = require(script.Parent.ChatEnabled) + + it("should be enabled by default", function() + local state = ChatEnabled(nil, {}) + + expect(state).to.equal(true) + end) + + it("should be changed using SetChatEnabled", function() + local state = ChatEnabled(nil, {}) + + state = ChatEnabled(state, SetChatEnabled(false)) + + expect(state).to.equal(false) + + state = ChatEnabled(state, SetChatEnabled(true)) + + expect(state).to.equal(true) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ConnectionState.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ConnectionState.lua new file mode 100644 index 0000000..9c4472d --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ConnectionState.lua @@ -0,0 +1,14 @@ +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules + +local SetConnectionState = require(Modules.LuaChat.Actions.SetConnectionState) + +return function(state, action) + state = state or Enum.ConnectionState.Connected + + if action.type == SetConnectionState.name then + state = action.connectionState + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ConnectionState.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ConnectionState.spec.lua new file mode 100644 index 0000000..ce51d9c --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ConnectionState.spec.lua @@ -0,0 +1,21 @@ +return function() + local CoreGui = game:GetService("CoreGui") + local Modules = CoreGui.RobloxGui.Modules + local LuaChat = Modules.LuaChat + local SetConnectionState = require(LuaChat.Actions.SetConnectionState) + local ConnectionStateReducer = require(script.Parent.ConnectionState) + + it("should default to Connected", function() + local state = ConnectionStateReducer(nil, {}) + + expect(state).to.equal(Enum.ConnectionState.Connected) + end) + + it("should update the ConnectionState", function() + local state = ConnectionStateReducer(nil, {}) + + state = ConnectionStateReducer(state, SetConnectionState(Enum.ConnectionState.Disconnected)) + + expect(state).to.equal(Enum.ConnectionState.Disconnected) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Conversations.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Conversations.lua new file mode 100644 index 0000000..b450cb4 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Conversations.lua @@ -0,0 +1,248 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat +local Actions = LuaChat.Actions + +local ChangedParticipants = require(Actions.ChangedParticipants) +local FetchedOldestMessage = require(Actions.FetchedOldestMessage) +local FetchingOlderMessages = require(Actions.FetchingOlderMessages) +local MessageFailedToSend = require(Actions.MessageFailedToSend) +local MessageModerated = require(Actions.MessageModerated) +local ReadConversation = require(Actions.ReadConversation) +local ReceivedConversation = require(Actions.ReceivedConversation) +local ReceivedMessages = require(Actions.ReceivedMessages) +local RemovedConversation = require(Actions.RemovedConversation) +local RenamedGroupConversation = require(Actions.RenamedGroupConversation) +local SendingMessage = require(Actions.SendingMessage) +local SentMessage = require(Actions.SentMessage) +local SetConversationLoadingStatus = require(Actions.SetConversationLoadingStatus) +local SetPinnedGameForConversation = require(Actions.SetPinnedGameForConversation) +local SetUserLeavingConversation = require(Actions.SetUserLeavingConversation) +local SetUserTyping = require(Actions.SetUserTyping) + +local ConversationModel = require(LuaChat.Models.Conversation) +local Immutable = require(Common.Immutable) +local OrderedMap = require(LuaChat.OrderedMap) + +return function(state, action) + state = state or {} + + if action.type == ReceivedConversation.name then + local conversation = action.conversation + + if conversation.conversationType == ConversationModel.Type.ONE_TO_ONE_CONVERSATION then + local idForFake = ConversationModel.IdForFakeOneOnOne(conversation.participants) + if state[idForFake] ~= nil then + state = Immutable.Set(state, idForFake, nil) + end + end + + if not state[conversation.id] then + state = Immutable.Set(state, conversation.id, conversation) + end + elseif action.type == ReceivedMessages.name then + local conversationId = action.conversationId + local conversation = state[conversationId] + + if conversation then + + local lastMessage = nil + for i = 1, #action.messages do + local message = action.messages[i] + local existing = conversation.messages:Get(message.id) + if existing then + if lastMessage then + lastMessage.previousMessageId = existing.id + end + if message.previousMessageId == nil and existing.previousMessageId ~= nil then + message.previousMessageId = existing.previousMessageId + end + message.read = message.read or existing.read + end + lastMessage = message + end + + local messages = OrderedMap.Insert(conversation.messages, unpack(action.messages)) + + if action.exclusiveStartMessageId and #action.messages > 0 then + if messages:Get(action.exclusiveStartMessageId) then + local prevMessage = action.messages[1] + local nextMessage = messages:Get(action.exclusiveStartMessageId) + nextMessage = Immutable.Set(nextMessage, "previousMessageId", prevMessage.id) + messages = messages:Insert(nextMessage) + end + end + + local lastConversationKey = messages.keys[#messages.keys] + lastMessage = messages.values[lastConversationKey] + + if lastMessage ~= nil then + local lastUpdatedConvo = conversation.lastUpdated and conversation.lastUpdated:GetUnixTimestamp() or 0 + + local lastUpdated = (lastMessage.sent:GetUnixTimestamp() > lastUpdatedConvo) and + lastMessage.sent or conversation.lastUpdated + + local hasUnreadMessages = action.shouldMarkConversationUnread or conversation.hasUnreadMessages + + local newConversation = Immutable.JoinDictionaries(conversation, { + messages = messages; + lastUpdated = lastUpdated; + hasUnreadMessages = hasUnreadMessages; + }) + + -- Mark all users as no longer typing + -- We should only do this if a specific user sent a message and it's + -- the last message in the list, but that check would be expensive + -- to make every time we request more messages. + -- Notably, this will fail for group conversations. + newConversation = Immutable.Set(newConversation, "usersTyping", {}) + state = Immutable.Set(state, newConversation.id, newConversation) + end + end + elseif action.type == SendingMessage.name then + local conversationId = action.conversationId + local conversation = state[conversationId] + + if conversation then + local sendingMessages = OrderedMap.Insert(conversation.sendingMessages, action.message) + local newConversation = Immutable.Set(conversation, "sendingMessages", sendingMessages) + state = Immutable.Set(state, newConversation.id, newConversation) + end + elseif action.type == SentMessage.name then + local conversationId = action.conversationId + local conversation = state[conversationId] + + if conversation then + local sendingMessages = OrderedMap.Delete(conversation.sendingMessages, action.messageId) + local newConversation = Immutable.Set(conversation, "sendingMessages", sendingMessages) + state = Immutable.Set(state, newConversation.id, newConversation) + end + elseif action.type == RenamedGroupConversation.name then + local conversationId = action.conversationId + + local conversation = state[conversationId] + + if conversation then + local newConversation = Immutable.JoinDictionaries(conversation, { + lastUpdated = action.lastUpdated, + title = action.title, + isDefaultTitle = action.isDefaultTitle, + }) + state = Immutable.Set(state, conversationId, newConversation) + end + elseif action.type == ChangedParticipants.name then + local convoId = action.conversationId + local newParticipants = action.participants + + local conversation = state[convoId] + + if conversation then + local newConversation = Immutable.JoinDictionaries(conversation, { + participants = newParticipants, + lastUpdated = action.lastUpdated, + title = action.title, + }) + state = Immutable.Set(state, convoId, newConversation) + end + elseif action.type == RemovedConversation.name then + local conversationId = action.conversationId + state = Immutable.Set(state, conversationId, nil) + + elseif action.type == SetUserTyping.name then + local conversation = state[action.conversationId] + + if conversation then + local usersTyping = conversation.usersTyping + local newUsersTyping = Immutable.Set(usersTyping, action.userId, action.value) + local newConversation = Immutable.Set(conversation, "usersTyping", newUsersTyping) + state = Immutable.Set(state, newConversation.id, newConversation) + end + elseif action.type == FetchingOlderMessages.name then + local conversation = state[action.conversationId] + if conversation then + local newConversation = Immutable.Set(conversation, "fetchingOlderMessages", action.fetchingOlderMessages) + state = Immutable.Set(state, newConversation.id, newConversation) + end + elseif action.type == FetchedOldestMessage.name then + local conversation = state[action.conversationId] + local newConversation = Immutable.Set(conversation, "fetchedOldestMessage", action.fetchedOldestMessage) + state = Immutable.Set(state, newConversation.id, newConversation) + + elseif action.type == ReadConversation.name then + local conversation = state[action.conversationId] + + if not conversation then + warn("Conversation " .. action.conversationId .. " not found in ReadConversation reducer") + return + end + + local newMessages = conversation.messages:Map(function(message) + if message.read then + return message + else + return Immutable.Set(message, "read", true) + end + end) + + local newConversation = Immutable.Set(conversation, "messages", newMessages) + newConversation = Immutable.Set(newConversation, "hasUnreadMessages", false) + + state = Immutable.Set(state, newConversation.id, newConversation) + + elseif action.type == MessageModerated.name then + local conversation = state[action.conversationId] + local message = conversation.sendingMessages:Get(action.messageId) + + if message then + local newMessage = Immutable.Set(message, "moderated", true) + local newSendingMessages = conversation.sendingMessages:Insert(newMessage) + local newConversation = Immutable.Set(conversation, "sendingMessages", newSendingMessages) + state = Immutable.Set(state, newConversation.id, newConversation) + end + + elseif action.type == MessageFailedToSend.name then + local conversation = state[action.conversationId] + local message = conversation.sendingMessages:Get(action.messageId) + + if message then + local newMessage = Immutable.Set(message, "failed", true) + local newSendingMessages = conversation.sendingMessages:Insert(newMessage) + local newConversation = Immutable.Set(conversation, "sendingMessages", newSendingMessages) + state = Immutable.Set(state, newConversation.id, newConversation) + end + + elseif action.type == SetConversationLoadingStatus.name then + local conversation = state[action.conversationId] + local newConversation = Immutable.Set(conversation, "initialLoadingStatus", action.value) + state = Immutable.Set(state, newConversation.id, newConversation) + + elseif action.type == SetUserLeavingConversation.name then + local conversation = state[action.id] + if conversation then + local newConversation = Immutable.Set(conversation, "isUserLeaving", action.isLeaving) + state = Immutable.Set(state, newConversation.id, newConversation) + end + + elseif action.type == SetPinnedGameForConversation.name then + + local conversationId = action.conversationId + local conversation = state[conversationId] + + if conversation then + local newPinnedGame = { + rootPlaceId = action.rootPlaceId; + universeId = action.universeId; + } + + local newConversation = Immutable.JoinDictionaries(conversation, { + pinnedGame = newPinnedGame; + }) + + state = Immutable.Set(state, conversationId, newConversation) + end + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Conversations.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Conversations.spec.lua new file mode 100644 index 0000000..79c7af3 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Conversations.spec.lua @@ -0,0 +1,455 @@ +return function() + local CoreGui = game:GetService("CoreGui") + + local Modules = CoreGui.RobloxGui.Modules + local LuaApp = Modules.LuaApp + local LuaChat = Modules.LuaChat + + local ChangedParticipants = require(LuaChat.Actions.ChangedParticipants) + local Constants = require(LuaChat.Constants) + local Conversation = require(LuaChat.Models.Conversation) + local DateTime = require(LuaChat.DateTime) + local FetchedOldestMessage = require(LuaChat.Actions.FetchedOldestMessage) + local FetchingOlderMessages = require(LuaChat.Actions.FetchingOlderMessages) + local Message = require(LuaChat.Models.Message) + local MessageFailedToSend = require(LuaChat.Actions.MessageFailedToSend) + local MessageModerated = require(LuaChat.Actions.MessageModerated) + local MockId = require(LuaApp.MockId) + local ReadConversation = require(LuaChat.Actions.ReadConversation) + local ReceivedConversation = require(LuaChat.Actions.ReceivedConversation) + local ReceivedMessages = require(LuaChat.Actions.ReceivedMessages) + local RemovedConversation = require(LuaChat.Actions.RemovedConversation) + local RenamedGroupConversation = require(LuaChat.Actions.RenamedGroupConversation) + local SendingMessage = require(LuaChat.Actions.SendingMessage) + local SentMessage = require(LuaChat.Actions.SentMessage) + local SetConversationLoadingStatus = require(LuaChat.Actions.SetConversationLoadingStatus) + local SetPinnedGameForConversation = require(LuaChat.Actions.SetPinnedGameForConversation) + local SetUserTyping = require(LuaChat.Actions.SetUserTyping) + local User = require(LuaApp.Models.User) + + local Conversations = require(script.Parent.Conversations) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = Conversations(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("Action ReceivedConversation", function() + it("should add a conversation to the store", function() + local conversation = Conversation.mock() + local state = nil + local action = ReceivedConversation(conversation) + + state = Conversations(state, action) + + expect(state).to.be.a("table") + expect(state[conversation.id]).to.be.a("table") + expect(state[conversation.id].id).to.equal(conversation.id) + end) + end) + + describe("Action ReceivedMessages", function() + it("should add messages to an existing conversation", function() + local conversation = Conversation.mock() + local message = Message.mock() + local state = { + [conversation.id] = conversation + } + local action = ReceivedMessages(conversation.id, {message}) + + state = Conversations(state, action) + + local messages = state[conversation.id].messages + expect(#messages.keys).to.equal(1) + expect(messages.values[messages.keys[1]].id).to.equal(message.id) + end) + + it("should insert messages in-order", function() + local conversation = Conversation.mock() + local message1 = Message.mock({ sent = DateTime.new(1992) }) + local message2 = Message.mock({ sent = DateTime.new(1990) }) + + local state = { + [conversation.id] = conversation + } + + local action = ReceivedMessages(conversation.id, { message1, message2 }) + state = Conversations(state, action) + + do + local messages = state[conversation.id].messages + expect(#messages.keys).to.equal(2) + expect(messages.values[messages.keys[1]].id).to.equal(message2.id) + expect(messages.values[messages.keys[2]].id).to.equal(message1.id) + end + + local message3 = Message.mock({ sent = DateTime.new(1991) }) + + action = ReceivedMessages(conversation.id, { message3 }) + state = Conversations(state, action) + + do + local messages = state[conversation.id].messages + expect(#messages.keys).to.equal(3) + expect(messages.values[messages.keys[1]].id).to.equal(message2.id) + expect(messages.values[messages.keys[2]].id).to.equal(message3.id) + expect(messages.values[messages.keys[3]].id).to.equal(message1.id) + end + end) + + it("should set hasUnreadMessages if action.shouldMarkConversationUnread is true", function() + local conversation = Conversation.mock() + local message1 = Message.mock({ read = true }) + local message2 = Message.mock({ read = false }) + + local state = { + [conversation.id] = conversation + } + + expect(state[conversation.id].hasUnreadMessages).to.equal(false) + + local action = ReceivedMessages(conversation.id, { message1, message2 }, true) + state = Conversations(state, action) + + expect(state[conversation.id].hasUnreadMessages).to.equal(true) + end) + + it("should not change hasUnreadMessages if action.shouldMarkConversationUnread is false", function() + local conversation = Conversation.mock() + local message1 = Message.mock({ read = false }) + local message2 = Message.mock({ read = false }) + + local state = { + [conversation.id] = conversation + } + + expect(state[conversation.id].hasUnreadMessages).to.equal(false) + + local action = ReceivedMessages(conversation.id, { message1, message2 }, false) + state = Conversations(state, action) + + expect(state[conversation.id].hasUnreadMessages).to.equal(false) + end) + end) + + describe("Action SendingMessage", function() + it("should add to the list of sending messages", function() + local conversation = Conversation.mock() + local message = Message.mock() + local state = { + [conversation.id] = conversation + } + + local action = SendingMessage(conversation.id, message) + state = Conversations(state, action) + + expect(state[conversation.id].sendingMessages:Get(message.id)).to.be.ok() + end) + end) + + describe("Action SentMessage", function() + it("should remove from the list of sending messages", function() + local conversation = Conversation.mock() + local message = Message.mock() + local state = { + [conversation.id] = conversation + } + + local action1 = SendingMessage(conversation.id, message) + state = Conversations(state, action1) + + expect(state[conversation.id].sendingMessages:Get(message.id)).to.be.ok() + + local action2 = SentMessage(conversation.id, message.id) + state = Conversations(state, action2) + + expect(state[conversation.id].sendingMessages:Get(message.id)).to.never.be.ok() + end) + end) + + describe("Action RenamedGroupConversation", function() + it("should rename the conversation", function() + local conversation = Conversation.mock() + local state = { + [conversation.id] = conversation + } + local action = RenamedGroupConversation(conversation.id, "Fleebledegoop, Ham Sammich and Lemur Face") + state = Conversations(state, action) + + expect(state[conversation.id].title).to.equal("Fleebledegoop, Ham Sammich and Lemur Face") + end) + + it("should update isDefaultTitle value", function() + local conversation = Conversation.mock({ + isDefaultTitle = true, + }) + local state = { + [conversation.id] = conversation + } + state = Conversations(state, RenamedGroupConversation(conversation.id, "test", false)) + + expect(state[conversation.id].isDefaultTitle).to.equal(false) + end) + + it("should update lastUpdated value", function() + local oldTick = 0 + local newTick = 1 + local conversation = Conversation.mock({ + isDefaultTitle = true, + lastUpdated = oldTick, + }) + local state = { + [conversation.id] = conversation + } + state = Conversations(state, RenamedGroupConversation(conversation.id, "test", nil, newTick)) + + expect(state[conversation.id].lastUpdated).to.equal(newTick) + end) + end) + + describe("Action ChangedParticipants", function() + it("should update the participant list", function() + local conversation = Conversation.mock() + local user1 = User.mock() + local user2 = User.mock() + local state = { + [conversation.id] = conversation + } + + local action = ChangedParticipants(conversation.id, { user1.id, user2.id }) + state = Conversations(state, action) + + expect(#state[conversation.id].participants).to.equal(2) + expect(state[conversation.id].participants[1]).to.equal(user1.id) + expect(state[conversation.id].participants[2]).to.equal(user2.id) + + action = ChangedParticipants(conversation.id, { user2.id }) + state = Conversations(state, action) + + expect(#state[conversation.id].participants).to.equal(1) + expect(state[conversation.id].participants[1]).to.equal(user2.id) + end) + end) + + describe("Action RemovedConversation", function() + it("should update the conversation list", function() + local conversation1 = Conversation.mock() + local conversation2 = Conversation.mock() + local state = { + [conversation1.id] = conversation1, + [conversation2.id] = conversation2 + } + + local action = RemovedConversation(conversation1.id) + state = Conversations(state, action) + + expect(state[conversation1.id]).to.equal(nil) + expect(state[conversation2.id]).to.be.a("table") + expect(state[conversation2.id].id).to.equal(conversation2.id) + end) + end) + + describe("Action SetUserTyping", function() + it("should set userTyping flag for user", function() + local user = User.mock() + local conversation = Conversation.mock({ participants = { user.id } }) + + local state = { + [conversation.id] = conversation + } + + expect(state[conversation.id].usersTyping[user.id]).to.never.be.ok() + + local action1 = SetUserTyping(conversation.id, user.id, true) + state = Conversations(state, action1) + + expect(state[conversation.id].usersTyping[user.id]).to.equal(true) + + local action2 = SetUserTyping(conversation.id, user.id, false) + state = Conversations(state, action2) + + expect(state[conversation.id].usersTyping[user.id]).to.equal(false) + end) + end) + + describe("Action FetchingOlderMessages", function() + it("sets the fetchingOlderMessages flag", function() + local conversation = Conversation.mock() + local state = { + [conversation.id] = conversation + } + + expect(state[conversation.id].fetchingOlderMessages).to.equal(false) + + local action1 = FetchingOlderMessages(conversation.id, true) + state = Conversations(state, action1) + + expect(state[conversation.id].fetchingOlderMessages).to.equal(true) + + local action2 = FetchingOlderMessages(conversation.id, false) + state = Conversations(state, action2) + + expect(state[conversation.id].fetchingOlderMessages).to.equal(false) + end) + end) + + describe("Action FetchedOldestMessage", function() + it("sets the fetchedOldestMessage flag", function() + local conversation = Conversation.mock() + local state = { + [conversation.id] = conversation + } + + expect(state[conversation.id].fetchedOldestMessage).to.equal(false) + + local action1 = FetchedOldestMessage(conversation.id, true) + state = Conversations(state, action1) + + expect(state[conversation.id].fetchedOldestMessage).to.equal(true) + + local action2 = FetchedOldestMessage(conversation.id, false) + state = Conversations(state, action2) + + expect(state[conversation.id].fetchedOldestMessage).to.equal(false) + end) + end) + + describe("Action ReadConversation", function() + it("should read messages and mark the conversation's unread state", function() + local message1 = Message.mock({ sent = DateTime.new(1992), read = true }) + local message2 = Message.mock({ sent = DateTime.new(1993), read = false }) + local message3 = Message.mock({ sent = DateTime.new(1994), read = false }) + local message4 = Message.mock({ sent = DateTime.new(1995), read = false }) + local message5 = Message.mock({ sent = DateTime.new(1996), read = false }) + local conversation = Conversation.mock() + + local state = { + [conversation.id] = conversation + } + + local actionAddMessages = ReceivedMessages(conversation.id, { message1, message2, message3, message4 }, true) + state = Conversations(state, actionAddMessages) + + expect(state[conversation.id].hasUnreadMessages).to.equal(true) + + local actionReadAll = ReadConversation(conversation.id) + state = Conversations(state, actionReadAll) + + do + expect(state[conversation.id].hasUnreadMessages).to.equal(false) + local messages = state[conversation.id].messages + expect(messages.values[message1.id].read).to.equal(true) + expect(messages.values[message2.id].read).to.equal(true) + expect(messages.values[message3.id].read).to.equal(true) + end + + local action2 = ReceivedMessages(conversation.id, { message4, message5 }, true) + + state = Conversations(state, action2) + + do + expect(state[conversation.id].hasUnreadMessages).to.equal(true) + local messages = state[conversation.id].messages + expect(messages.values[message1.id].read).to.equal(true) + expect(messages.values[message2.id].read).to.equal(true) + expect(messages.values[message3.id].read).to.equal(true) + expect(messages.values[message4.id].read).to.equal(true) + expect(messages.values[message5.id].read).to.equal(false) + end + + local actionReadAll2 = ReadConversation(conversation.id) + state = Conversations(state, actionReadAll2) + + do + expect(state[conversation.id].hasUnreadMessages).to.equal(false) + local messages = state[conversation.id].messages + expect(messages.values[message1.id].read).to.equal(true) + expect(messages.values[message2.id].read).to.equal(true) + expect(messages.values[message3.id].read).to.equal(true) + expect(messages.values[message4.id].read).to.equal(true) + end + end) + end) + + describe("Action MessageModerated", function() + it("should mark the sending message as moderated", function() + local conversation = Conversation.mock() + local message = Message.mock() + local state = { + [conversation.id] = conversation + } + + local action = SendingMessage(conversation.id, message) + state = Conversations(state, action) + + expect(state[conversation.id].sendingMessages:Get(message.id).moderated).to.never.be.ok() + + action = MessageModerated(conversation.id, message.id) + state = Conversations(state, action) + + expect(state[conversation.id].sendingMessages:Get(message.id).moderated).to.equal(true) + end) + end) + + describe("Action MessageFailedToSend", function() + it("should mark the sending message as failed", function() + local conversation = Conversation.mock() + local message = Message.mock() + local state = { + [conversation.id] = conversation + } + + local action = SendingMessage(conversation.id, message) + state = Conversations(state, action) + + expect(state[conversation.id].sendingMessages:Get(message.id).failed).to.equal(nil) + + action = MessageFailedToSend(conversation.id, message.id) + state = Conversations(state, action) + + expect(state[conversation.id].sendingMessages:Get(message.id).failed).to.equal(true) + end) + end) + + describe("Action SetConversationLoadingStatus", function() + it("should set the conversation loading status", function() + local conversation = Conversation.mock() + local state = { + [conversation.id] = conversation + } + + expect(state[conversation.id].initialLoadingStatus).to.never.be.ok() + + local action = SetConversationLoadingStatus(conversation.id, Constants.ConversationLoadingState.LOADING) + state = Conversations(state, action) + + expect(state[conversation.id].initialLoadingStatus).to.equal(Constants.ConversationLoadingState.LOADING) + + action = SetConversationLoadingStatus(conversation.id, Constants.ConversationLoadingState.DONE) + state = Conversations(state, action) + + expect(state[conversation.id].initialLoadingStatus).to.equal(Constants.ConversationLoadingState.DONE) + end) + end) + + describe("Action SetPinnedGameForConversation", function() + it("should set the pinned game for conversation", function() + local conversation = Conversation.mock() + local state = { + [conversation.id] = conversation + } + + local mockRootPlaceId = MockId() + local mockUniverseId = MockId() + + local action = SetPinnedGameForConversation(mockUniverseId, mockRootPlaceId, conversation.id) + state = Conversations(state, action) + + expect(state[conversation.id].pinnedGame.rootPlaceId).to.equal(mockRootPlaceId) + expect(state[conversation.id].pinnedGame.universeId).to.equal(mockUniverseId) + end) + end) + +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ConversationsAsync.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ConversationsAsync.lua new file mode 100644 index 0000000..fb6af60 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ConversationsAsync.lua @@ -0,0 +1,39 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local LuaChat = Modules.LuaChat + +local ReceivedLatestMessages = require(LuaChat.Actions.ReceivedLatestMessages) +local ReceivedOldestConversation = require(LuaChat.Actions.ReceivedOldestConversation) +local ReceivedPageConversations = require(LuaChat.Actions.ReceivedPageConversations) +local RequestLatestMessages = require(LuaChat.Actions.RequestLatestMessages) +local RequestPageConversations = require(LuaChat.Actions.RequestPageConversations) + +local Immutable = require(Modules.Common.Immutable) + +return function(state, action) + state = state or {} + + if action.type == RequestPageConversations.name then + return Immutable.JoinDictionaries(state, { + pageConversationsIsFetching = true, + }) + elseif action.type == ReceivedPageConversations.name then + return Immutable.JoinDictionaries(state, { + pageConversationsIsFetching = false, + }) + elseif action.type == RequestLatestMessages.name then + return Immutable.JoinDictionaries(state, { + latestMessagesIsFetching = true, + }) + elseif action.type == ReceivedLatestMessages.name then + return Immutable.JoinDictionaries(state, { + latestMessagesIsFetching = false, + }) + elseif action.type == ReceivedOldestConversation.name then + return Immutable.JoinDictionaries(state, { + oldestConversationIsFetched = true, + }) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ConversationsAsync.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ConversationsAsync.spec.lua new file mode 100644 index 0000000..e82ac07 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ConversationsAsync.spec.lua @@ -0,0 +1,66 @@ +return function() + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local LuaChat = Modules.LuaChat + + local ConversationsAsyncReducer = require(LuaChat.Reducers.ConversationsAsync) + + local ReceivedLatestMessages = require(LuaChat.Actions.ReceivedLatestMessages) + local ReceivedOldestConversation = require(LuaChat.Actions.ReceivedOldestConversation) + local ReceivedPageConversations = require(LuaChat.Actions.ReceivedPageConversations) + local RequestLatestMessages = require(LuaChat.Actions.RequestLatestMessages) + local RequestPageConversations = require(LuaChat.Actions.RequestPageConversations) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = ConversationsAsyncReducer(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("RequestPageConversations", function() + it("should set the async state to true", function() + local state = ConversationsAsyncReducer(nil, {}) + state = ConversationsAsyncReducer(state, RequestPageConversations()) + + expect(state.pageConversationsIsFetching).to.equal(true) + end) + end) + + describe("ReceivedPageConversations", function() + it("should set the async state to false", function() + local state = ConversationsAsyncReducer(nil, {}) + state = ConversationsAsyncReducer(state, ReceivedPageConversations()) + + expect(state.pageConversationsIsFetching).to.equal(false) + end) + end) + + describe("RequestLatestMessages", function() + it("should set the async state to true", function() + local state = ConversationsAsyncReducer(nil, {}) + state = ConversationsAsyncReducer(state, RequestLatestMessages()) + + expect(state.latestMessagesIsFetching).to.equal(true) + end) + end) + + describe("ReceivedLatestMessages", function() + it("should set the async state to false", function() + local state = ConversationsAsyncReducer(nil, {}) + state = ConversationsAsyncReducer(state, ReceivedLatestMessages()) + + expect(state.latestMessagesIsFetching).to.equal(false) + end) + end) + + describe("ReceivedOldestConversation", function() + it("should set the oldestConversationIsFetched flag to true", function() + local state = ConversationsAsyncReducer(nil, {}) + state = ConversationsAsyncReducer(state, ReceivedOldestConversation()) + + expect(state.oldestConversationIsFetched).to.equal(true) + end) + end) + +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/FriendCount.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/FriendCount.lua new file mode 100644 index 0000000..70e2fe8 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/FriendCount.lua @@ -0,0 +1,14 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local LuaChat = Modules.LuaChat + +local SetFriendCount = require(LuaChat.Actions.SetFriendCount) + +return function(state, action) + state = state or 0 + + if action.type == SetFriendCount.name then + return action.count + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/FriendCount.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/FriendCount.spec.lua new file mode 100644 index 0000000..fc29a9b --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/FriendCount.spec.lua @@ -0,0 +1,18 @@ +return function() + local LuaChat = script.Parent.Parent + local FriendCount = require(script.Parent.FriendCount) + local SetFriendCount = require(LuaChat.Actions.SetFriendCount) + + it("should be zero by default", function() + local state = FriendCount(nil, {}) + + expect(state).to.equal(0) + end) + + it("should respond to SetFriendCount", function() + local state = FriendCount(nil, {}) + state = FriendCount(state, SetFriendCount(520)) + + expect(state).to.equal(520) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Location.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Location.lua new file mode 100644 index 0000000..20bc648 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Location.lua @@ -0,0 +1,97 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local PopRoute = require(LuaChat.Actions.PopRoute) +local RemoveRoute = require(LuaChat.Actions.RemoveRoute) +local SetRoute = require(LuaChat.Actions.SetRoute) + +local Immutable = require(Common.Immutable) + +return function(state, action) + state = state or { + current = {}, + history = {} + } + + if action.type == SetRoute.name then + local current = state.current + local history = state.history + + local routeData = { + intent = action.intent, + popToIntent = action.popToIntent, + parameters = action.parameters + } + + if action.popToIntent then + local found = false + for i = #history, 1, -1 do + local loc = history[i] + if loc.intent == action.popToIntent then + history = Immutable.RemoveRangeFromList(history, i + 1, #history - i) + current = history[#history] + found = true + break + end + end + + if not found then + warn("Could not pop to unavailable intent: " .. action.popToIntent) + end + end + + if routeData.intent ~= nil then + current = routeData + history = Immutable.Append(history, routeData) + end + + return { + current = current, + history = history + } + + elseif action.type == PopRoute.name then + local current + local history = state.history + + if #history <= 1 then + return state + end + + history = Immutable.RemoveFromList(history, #history) + current = history[#history] + if not current then + current = {} + end + + return { + current = current, + history = history + } + + elseif action.type == RemoveRoute.name then + local intent = action.intent + local history = state.history + + for i = #history, 1, -1 do + local loc = history[i] + if loc.intent == intent then + history = Immutable.RemoveFromList(history, i) + break + end + end + + local current = history[#history] or {} + + return { + current = current, + history = history + } + + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Location.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Location.spec.lua new file mode 100644 index 0000000..bb059d5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Location.spec.lua @@ -0,0 +1,236 @@ +return function() + local LuaChat = script.Parent.Parent + local Location = require(script.Parent.Location) + + local DialogInfo = require(LuaChat.DialogInfo) + local Intent = DialogInfo.Intent + + local PopRoute = require(LuaChat.Actions.PopRoute) + local RemoveRoute = require(LuaChat.Actions.RemoveRoute) + local SetRoute = require(LuaChat.Actions.SetRoute) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = Location(nil, {}) + expect(state).to.be.a("table") + expect(state.current).to.be.a("table") + expect(state.history).to.be.a("table") + end) + end) + + describe("SetRoute", function() + it("should set route to ConversationHub", function() + local intent = Intent.ConversationHub + + local state = Location(nil, SetRoute(intent, {})) + + expect(state.current.intent).to.equal(intent) + end) + + it("should set route to Conversation with correct conversationId", function() + local intent = Intent.Conversation + local conversationId = "testConvoId" + + local state = Location(nil, SetRoute( + intent, + { + conversationId = conversationId, + } + )) + + expect(state.current.intent).to.equal(intent) + expect(state.current.parameters.conversationId).to.equal(conversationId) + end) + end) + + describe("SetRoute with popToIntent", function() + it("should pop to the right intent first and then add the new intent on top", function() + local state = Location(nil, SetRoute(Intent.ConversationHub, {})) + + state = Location(state, SetRoute( + Intent.Conversation, + { + conversationId = "testConvoId1", + } + )) + + state = Location(state, SetRoute(Intent.NewChatGroup)) + + state = Location(state, SetRoute( + Intent.Conversation, + { + conversationId = "testConvoId2", + } + )) + + expect(#state.history).to.equal(4) + + state = Location(state, SetRoute( + Intent.Conversation, + { + conversationId = "testConvoId3", + }, + Intent.ConversationHub + )) + + expect(#state.history).to.equal(2) + expect(state.history[1].intent).to.equal(Intent.ConversationHub) + expect(state.current.parameters.conversationId).to.equal("testConvoId3") + expect(state.history[#state.history].parameters.conversationId).to.equal("testConvoId3") + end) + it("should pop to the right intent and stop if the new intent is nil", function() + local state = Location(nil, SetRoute( + Intent.ConversationHub, + {} + )) + + state = Location(state, SetRoute( + Intent.Conversation, + { + conversationId = "testConvoId1", + } + )) + + state = Location(state, SetRoute(Intent.NewChatGroup)) + + state = Location(state, SetRoute( + Intent.Conversation, + { + conversationId = "testConvoId2", + } + )) + + state = Location(state, SetRoute( + nil, + { + conversationId = "testConvoId3", + }, + Intent.NewChatGroup + )) + + expect(#state.history).to.equal(3) + expect(state.current.intent).to.equal(Intent.NewChatGroup) + expect(state.history[#state.history].intent).to.equal(Intent.NewChatGroup) + end) + end) + + describe("route history", function() + it("should push history", function() + local state = Location(nil, SetRoute( + Intent.ConversationHub, + {} + )) + + expect(#state.history).to.equal(1) + end) + + it("should push history twice and have correct number of history entries", function() + local state = Location(nil, SetRoute( + Intent.ConversationHub, + {} + )) + + state = Location(state, SetRoute( + Intent.Conversation, + { + conversationId = "testConvoId", + } + )) + + expect(#state.history).to.equal(2) + end) + + it("should have current be equal to top of history", function() + local state = Location(nil, SetRoute( + Intent.ConversationHub, + {} + )) + + expect(state.history[1].intent).to.equal(state.current.intent) + end) + + it("should pop history", function() + local state = Location(nil, SetRoute( + Intent.ConversationHub, + {} + )) + + state = Location(state, SetRoute(Intent.Conversation, + { + conversationId = "testConvoId", + } + )) + + state = Location(state, PopRoute()) + + expect(state.current.intent).to.equal(Intent.ConversationHub) + expect(#state.history).to.equal(1) + expect(state.history[1].intent).to.equal(state.current.intent) + end) + + it("should never pop routes where the history is empty", function() + local state = Location(nil, SetRoute( + Intent.ConversationHub, + {} + )) + + state = Location(state, SetRoute(Intent.Conversation, + { + conversationId = "testConvoId", + } + )) + + state = Location(state, PopRoute()) + + expect(state.current.intent).to.equal(Intent.ConversationHub) + expect(#state.history).to.equal(1) + expect(state.history[1].intent).to.equal(state.current.intent) + + state = Location(state, PopRoute()) + + expect(state.current.intent).to.equal(Intent.ConversationHub) + expect(#state.history).to.equal(1) + expect(state.history[1].intent).to.equal(state.current.intent) + end) + + it("should remove from history properly", function() + local state = Location(nil, SetRoute( + Intent.ConversationHub, + {} + )) + + state = Location(state, SetRoute( + Intent.Conversation, + { + conversationId = "testConvoId1" + } + )) + + state = Location(state, SetRoute( + Intent.NewChatGroup + )) + + state = Location(state, SetRoute( + Intent.Conversation, + { + conversationId = "testConvoId2" + } + )) + + expect(#state.history).to.equal(4) + + state = Location(state, RemoveRoute(Intent.NewChatGroup)) + + expect(#state.history).to.equal(3) + + state = Location(state, RemoveRoute(Intent.EditChatGroup)) + + expect(#state.history).to.equal(3) + + state = Location(state, RemoveRoute(Intent.Conversation)) + + expect(state.current.intent).to.equal(Intent.Conversation) + expect(state.current.parameters.conversationId).to.equal("testConvoId1") + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/MostRecentlyPlayedGames.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/MostRecentlyPlayedGames.lua new file mode 100644 index 0000000..104d75c --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/MostRecentlyPlayedGames.lua @@ -0,0 +1,28 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat +local Actions = LuaChat.Actions + +local SetMostRecentlyPlayedGamesForUser = require(Actions.SetMostRecentlyPlayedGamesForUser) +local SetMostRecentlyPlayedPlayableGameForUser = require(Actions.SetMostRecentlyPlayedPlayableGameForUser) + +local Immutable = require(Common.Immutable) + +return function(state, action) + state = state or {} + + if action.type == SetMostRecentlyPlayedGamesForUser.name then + return Immutable.JoinDictionaries(state, { + games = action.games + }) + elseif action.type == SetMostRecentlyPlayedPlayableGameForUser.name then + return Immutable.JoinDictionaries(state, { + playableGamePlaceId = action.placeId, + setPlayableGame = true + }) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceInfos.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceInfos.lua new file mode 100644 index 0000000..ee8f252 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceInfos.lua @@ -0,0 +1,23 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local ReceivedMultiplePlaceInfos = require(LuaChat.Actions.ReceivedMultiplePlaceInfos) + +local Immutable = require(Common.Immutable) + +return function(state, action) + state = state or {} + if action.type == ReceivedMultiplePlaceInfos.name then + + local newInfos = {} + for _, placeInfo in ipairs(action.placeInfos) do + newInfos[placeInfo.placeId] = placeInfo + end + + state = Immutable.JoinDictionaries(state, newInfos) + end + return state +end diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceInfos.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceInfos.spec.lua new file mode 100644 index 0000000..075534e --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceInfos.spec.lua @@ -0,0 +1,36 @@ +return function() + local CoreGui = game:GetService("CoreGui") + local Modules = CoreGui.RobloxGui.Modules + local LuaApp = Modules.LuaApp + local LuaChat = Modules.LuaChat + + local MockId = require(LuaApp.MockId) + local ReceivedMultiplePlaceInfos = require(LuaChat.Actions.ReceivedMultiplePlaceInfos) + + local PlaceInfosReducer = require(script.Parent.PlaceInfos) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = PlaceInfosReducer(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("ReceivedMultiplePlaceInfos", function() + it("should add place info to the store", function() + local state = PlaceInfosReducer(nil, {}) + + local placeId = MockId() + local returnedPlaceInfo = ReceivedMultiplePlaceInfos({ + { + placeId = placeId, + imageToken = "image-token", + }, + }) + + state = PlaceInfosReducer(state, returnedPlaceInfo) + + expect(state[placeId]).to.equal(returnedPlaceInfo.placeInfos[1]) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceInfosAsync.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceInfosAsync.lua new file mode 100644 index 0000000..9cb403b --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceInfosAsync.lua @@ -0,0 +1,33 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local LuaChat = Modules.LuaChat +local Actions = LuaChat.Actions + +local Immutable = require(Modules.Common.Immutable) + +local RequestMultiplePlaceInfos = require(Actions.RequestMultiplePlaceInfos) +local FailedToFetchMultiplePlaceInfos = require(Actions.FailedToFetchMultiplePlaceInfos) +local ReceivedMultiplePlaceInfos = require(Actions.ReceivedMultiplePlaceInfos) + +return function(state, action) + state = state or {} + if action.type == RequestMultiplePlaceInfos.name then + local newFlags = {} + for _, placeId in ipairs(action.placeIds) do + newFlags[placeId] = true + end + return Immutable.JoinDictionaries(state, newFlags) + elseif action.type == ReceivedMultiplePlaceInfos.name then + local newFlags = {} + for _, placeInfo in ipairs(action.placeInfos) do + newFlags[placeInfo.placeId] = false + end + return Immutable.JoinDictionaries(state, newFlags) + elseif action.type == FailedToFetchMultiplePlaceInfos.name then + local newFlags = {} + for _, placeId in ipairs(action.placeIds) do + newFlags[placeId] = false + end + return Immutable.JoinDictionaries(state, newFlags) + end + return state +end diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceThumbnails.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceThumbnails.lua new file mode 100644 index 0000000..bf25607 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceThumbnails.lua @@ -0,0 +1,16 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat + +local Immutable = require(Common.Immutable) +local ReceivedPlaceThumbnail = require(LuaChat.Actions.ReceivedPlaceThumbnail) + +return function(state, action) + state = state or {} + if action.type == ReceivedPlaceThumbnail.name then + state = Immutable.Set(state, action.imageToken, action.thumbnail) + end + return state +end diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceThumbnails.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceThumbnails.spec.lua new file mode 100644 index 0000000..00dac03 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceThumbnails.spec.lua @@ -0,0 +1,25 @@ +return function() + local CoreGui = game:GetService("CoreGui") + local Modules = CoreGui.RobloxGui.Modules + local LuaChat = Modules.LuaChat + local ReceivedPlaceThumbnail = require(LuaChat.Actions.ReceivedPlaceThumbnail) + local PlaceThumbnailsReducer = require(script.Parent.PlaceThumbnails) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = PlaceThumbnailsReducer(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("ReceivedPlaceThumbnail", function() + it("should add place thumbnail to the store", function() + local state = PlaceThumbnailsReducer(nil, {}) + + local returnedThumbnail = ReceivedPlaceThumbnail("imageToken", "thumbnail") + state = PlaceThumbnailsReducer(state, returnedThumbnail) + + expect(state["imageToken"]).to.equal("thumbnail") + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceThumbnailsAsync.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceThumbnailsAsync.lua new file mode 100644 index 0000000..4030c6b --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlaceThumbnailsAsync.lua @@ -0,0 +1,25 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Immutable = require(Modules.Common.Immutable) + +local RequestPlaceThumbnail = require(Modules.LuaChat.Actions.RequestPlaceThumbnail) +local ReceivedPlaceThumbnail = require(Modules.LuaChat.Actions.ReceivedPlaceThumbnail) +local FailedToFetchPlaceThumbnail = require(Modules.LuaChat.Actions.FailedToFetchPlaceThumbnail) + +return function(state, action) + state = state or {} + if action.type == RequestPlaceThumbnail.name then + return Immutable.JoinDictionaries(state, { + [action.imageToken] = true, + }) + elseif action.type == ReceivedPlaceThumbnail.name then + return Immutable.JoinDictionaries(state, { + [action.imageToken] = false, + }) + elseif action.type == FailedToFetchPlaceThumbnail.name then + return Immutable.JoinDictionaries(state, { + [action.imageToken] = false, + }) + end + return state +end diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlayTogetherAsync.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlayTogetherAsync.lua new file mode 100644 index 0000000..d8203b4 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlayTogetherAsync.lua @@ -0,0 +1,74 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaChat = Modules.LuaChat +local Actions = LuaChat.Actions + +local FailedToFetchMostRecentlyPlayedGames = require(Actions.FailedToFetchMostRecentlyPlayedGames) +local FetchedMostRecentlyPlayedGames = require(Actions.FetchedMostRecentlyPlayedGames) +local FetchingMostRecentlyPlayedGames = require(Actions.FetchingMostRecentlyPlayedGames) +local GameFailedToPin = require(Actions.GameFailedToPin) +local GameFailedToUnpin = require(Actions.GameFailedToUnpin) +local PinnedGame = require(Actions.PinnedGame) +local PinningGame = require(Actions.PinningGame) +local UnpinnedGame = require(Actions.UnpinnedGame) +local UnpinningGame = require(Actions.UnpinningGame) + +local Immutable = require(Common.Immutable) + +return function(state, action) + state = state or { + pinningGames = {}, + unPinningGames = {}, + } + -- play together async + if action.type == PinningGame.name then + local newPinningGames = Immutable.Set(state.pinningGames, action.conversationId, true) + return Immutable.JoinDictionaries(state, { + pinningGames = newPinningGames; + }) + elseif action.type == PinnedGame.name then + local newPinningGames = Immutable.Set(state.pinningGames, action.conversationId, false) + return Immutable.JoinDictionaries(state, { + pinningGames = newPinningGames; + }) + elseif action.type == GameFailedToPin.name then + local newPinningGames = Immutable.Set(state.pinningGames, action.conversationId, false) + return Immutable.JoinDictionaries(state, { + pinningGames = newPinningGames; + }) + elseif action.type == UnpinningGame.name then + local newUnpinningGames = Immutable.Set(state.unPinningGames, action.conversationId, true) + return Immutable.JoinDictionaries(state, { + unPinningGames = newUnpinningGames; + }) + elseif action.type == UnpinnedGame.name then + local newUnpinningGames = Immutable.Set(state.unPinningGames, action.conversationId, false) + return Immutable.JoinDictionaries(state, { + unPinningGames = newUnpinningGames; + }) + elseif action.type == GameFailedToUnpin.name then + local newUnpinningGames = Immutable.Set(state.unPinningGames, action.conversationId, false) + return Immutable.JoinDictionaries(state, { + unPinningGames = newUnpinningGames; + }) + elseif action.type == FetchingMostRecentlyPlayedGames.name then + return Immutable.JoinDictionaries(state, { + fetchingMostRecentlyPlayedGames = true, + fetchedMostRecentlyPlayedGames = false + }) + elseif action.type == FetchedMostRecentlyPlayedGames.name then + return Immutable.JoinDictionaries(state, { + fetchingMostRecentlyPlayedGames = false, + fetchedMostRecentlyPlayedGames = true, + }) + elseif action.type == FailedToFetchMostRecentlyPlayedGames.name then + return Immutable.JoinDictionaries(state, { + fetchingMostRecentlyPlayedGames = false, + fetchedMostRecentlyPlayedGames = false + }) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlayTogetherAsync.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlayTogetherAsync.spec.lua new file mode 100644 index 0000000..e9f0060 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/PlayTogetherAsync.spec.lua @@ -0,0 +1,119 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat +local Actions = LuaChat.Actions + +local MockId = require(LuaApp.MockId) + +local PlayTogetherAsync = require(script.Parent.PlayTogetherAsync) + +local FailedToFetchMostRecentlyPlayedGames = require(Actions.FailedToFetchMostRecentlyPlayedGames) +local FetchedMostRecentlyPlayedGames = require(Actions.FetchedMostRecentlyPlayedGames) +local FetchingMostRecentlyPlayedGames = require(Actions.FetchingMostRecentlyPlayedGames) +local GameFailedToPin = require(Actions.GameFailedToPin) +local GameFailedToUnpin = require(Actions.GameFailedToUnpin) +local PinnedGame = require(Actions.PinnedGame) +local PinningGame = require(Actions.PinningGame) +local UnpinnedGame = require(Actions.UnpinnedGame) +local UnpinningGame = require(Actions.UnpinningGame) + +return function() + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = PlayTogetherAsync(nil, {}) + expect(state).to.be.a("table") + expect(state.pinningGames).to.be.a("table") + expect(state.unPinningGames).to.be.a("table") + end) + end) + + describe("PinningGame", function() + it("should set the pinning game async state to true", function() + local state = PlayTogetherAsync(nil, {}) + local testConversationId = MockId() + state = PlayTogetherAsync(state, PinningGame(testConversationId)) + + expect(state.pinningGames[testConversationId]).to.equal(true) + end) + end) + + describe("PinnedGame", function() + it("should set the pinning game async state to false", function() + local state = PlayTogetherAsync(nil, {}) + local testConversationId = MockId() + state = PlayTogetherAsync(state, PinnedGame(testConversationId)) + + expect(state.pinningGames[testConversationId]).to.equal(false) + end) + end) + + describe("GameFailedToPin", function() + it("should set the pinning game async state to false", function() + local state = PlayTogetherAsync(nil, {}) + local testConversationId = MockId() + state = PlayTogetherAsync(state, GameFailedToPin(testConversationId)) + + expect(state.pinningGames[testConversationId]).to.equal(false) + end) + end) + + describe("UnpinningGame", function() + it("should set the unpinning game async state to true", function() + local state = PlayTogetherAsync(nil, {}) + local testConversationId = MockId() + state = PlayTogetherAsync(state, UnpinningGame(testConversationId)) + + expect(state.unPinningGames[testConversationId]).to.equal(true) + end) + end) + + describe("UnpinnedGame", function() + it("should set the unpinning game async state to false", function() + local state = PlayTogetherAsync(nil, {}) + local testConversationId = MockId() + state = PlayTogetherAsync(state, UnpinnedGame(testConversationId)) + + expect(state.unPinningGames[testConversationId]).to.equal(false) + end) + end) + + describe("GameFailedToUnpin", function() + it("should set the unpinning game async state to false", function() + local state = PlayTogetherAsync(nil, {}) + local testConversationId = MockId() + state = PlayTogetherAsync(state, GameFailedToUnpin(testConversationId)) + + expect(state.unPinningGames[testConversationId]).to.equal(false) + end) + end) + describe("FetchingMostRecentlyPlayedGames", function() + it("should update fecth state of most recently played games", function() + local state = PlayTogetherAsync(nil, {}) + state = PlayTogetherAsync(state, FetchingMostRecentlyPlayedGames()) + + expect(state.fetchingMostRecentlyPlayedGames).to.equal(true) + expect(state.fetchedMostRecentlyPlayedGames).to.equal(false) + end) + end) + describe("FetchedMostRecentlyPlayedGames", function() + it("should update fecth state of most recently played games", function() + local state = PlayTogetherAsync(nil, {}) + state = PlayTogetherAsync(state, FetchedMostRecentlyPlayedGames()) + + expect(state.fetchingMostRecentlyPlayedGames).to.equal(false) + expect(state.fetchedMostRecentlyPlayedGames).to.equal(true) + end) + end) + describe("FailedToFetchMostRecentlyPlayedGames", function() + it("should update fecth state of most recently played games", function() + local state = PlayTogetherAsync(nil, {}) + state = PlayTogetherAsync(state, FailedToFetchMostRecentlyPlayedGames()) + + expect(state.fetchingMostRecentlyPlayedGames).to.equal(false) + expect(state.fetchedMostRecentlyPlayedGames).to.equal(false) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ShareGameToChatAsync.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ShareGameToChatAsync.lua new file mode 100644 index 0000000..89af180 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ShareGameToChatAsync.lua @@ -0,0 +1,84 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Immutable = require(Common.Immutable) +local LuaChat = Modules.LuaChat +local ShareGameToChatActions = LuaChat.Actions.ShareGameToChatFromChat + +local FailedToFetchGamesBySort = require(ShareGameToChatActions.FailedToFetchGamesBySortShareGameToChatFromChat) +local FailedToShareGameToChat = require(ShareGameToChatActions.FailedToShareGameToChatFromChat) +local FetchedGamesBySort = require(ShareGameToChatActions.FetchedGamesBySortShareGameToChatFromChat) +local FetchingGamesBySort = require(ShareGameToChatActions.FetchingGamesBySortShareGameToChatFromChat) +local ResetShareGameToChatAsync = require(ShareGameToChatActions.ResetShareGameToChatFromChatAsync) +local ResetShareGame = require(ShareGameToChatActions.ResetShareGameToChatFromChat) +local SharedGameToChat = require(ShareGameToChatActions.SharedGameToChatFromChat) +local SharingGameToChat = require(ShareGameToChatActions.SharingGameToChatFromChat) + +return function(state, action) + state = state or { + fetchingGamesBySort = {}, + fetchedGamesBySort = {}, + failedToFetchGamesBySort = {}, + } + + if action.type == FetchingGamesBySort.name then + local newFetchingGamesBySort = Immutable.Set(state.fetchingGamesBySort, action.gameSortName, true) + local newFetchedGamesBySort = Immutable.Set(state.fetchedGamesBySort, action.gameSortName, false) + local newFailedToFetchGamesBySort = Immutable.Set(state.failedToFetchGamesBySort, action.gameSortName, false) + + return Immutable.JoinDictionaries(state, { + fetchingGamesBySort = newFetchingGamesBySort; + fetchedGamesBySort = newFetchedGamesBySort; + failedToFetchGamesBySort = newFailedToFetchGamesBySort; + }) + elseif action.type == FetchedGamesBySort.name then + local newFetchingGamesBySort = Immutable.Set(state.fetchingGamesBySort, action.gameSortName, false) + local newFetchedGamesBySort = Immutable.Set(state.fetchedGamesBySort, action.gameSortName, true) + local newFailedToFetchGamesBySort = Immutable.Set(state.failedToFetchGamesBySort, action.gameSortName, false) + + return Immutable.JoinDictionaries(state, { + fetchingGamesBySort = newFetchingGamesBySort; + fetchedGamesBySort = newFetchedGamesBySort; + failedToFetchGamesBySort = newFailedToFetchGamesBySort; + }) + elseif action.type == FailedToFetchGamesBySort.name then + local newFetchingGamesBySort = Immutable.Set(state.fetchingGamesBySort, action.gameSortName, false) + local newFetchedGamesBySort = Immutable.Set(state.fetchedGamesBySort, action.gameSortName, false) + local newFailedToFetchGamesBySort = Immutable.Set(state.failedToFetchGamesBySort, action.gameSortName, true) + + return Immutable.JoinDictionaries(state, { + fetchingGamesBySort = newFetchingGamesBySort; + fetchedGamesBySort = newFetchedGamesBySort; + failedToFetchGamesBySort = newFailedToFetchGamesBySort; + }) + elseif action.type == ResetShareGameToChatAsync.name then + return { + fetchingGamesBySort = {}, + fetchedGamesBySort = {}, + failedToFetchGamesBySort = {}, + } + elseif action.type == SharingGameToChat.name then + return Immutable.JoinDictionaries(state, { + sharingGame = true, + sharedGame = false, + }) + elseif action.type == SharedGameToChat.name then + return Immutable.JoinDictionaries(state, { + sharingGame = false, + sharedGame = true, + }) + elseif action.type == FailedToShareGameToChat.name then + return Immutable.JoinDictionaries(state, { + sharedGame = false, + sharingGame = false, + }) + elseif action.type == ResetShareGame.name then + return Immutable.JoinDictionaries(state, { + sharedGame = false, + sharingGame = false, + }) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/SharedGameSorts.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/SharedGameSorts.lua new file mode 100644 index 0000000..85e6fc8 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/SharedGameSorts.lua @@ -0,0 +1,40 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Immutable = require(Common.Immutable) +local LuaChat = Modules.LuaChat +local ShareGameToChatActions = LuaChat.Actions.ShareGameToChatFromChat + +local AddGamesBySort = require(ShareGameToChatActions.AddGamesBySortShareGameToChatFromChat) +local UpdateGameSortsTokens = require(ShareGameToChatActions.UpdateGameSortsTokensShareGameToChatFromChat) +local ClearAllGamesInSorts = require(ShareGameToChatActions.ClearAllGamesInSortsShareGameToChatFromChat) + +return function(state, action) + state = state or {} + + if action.type == AddGamesBySort.name then + local sort = state[action.name] or {} + local newSort = Immutable.JoinDictionaries(sort, { + token = sort.token, + tokenExpiry = sort.tokenExpiry, + placeIds = action.placeIds + }) + state = Immutable.Set(state, action.name, newSort) + elseif action.type == UpdateGameSortsTokens.name then + for _, gameSort in pairs(action.gameSorts) do + local sort = state[gameSort.name] or {} + local placeIds = state[gameSort.name] ~= nil and state[gameSort.name].placeIds or nil + local newSort = Immutable.JoinDictionaries(sort, { + token = gameSort.token, + tokenExpiry = gameSort.tokenExpiryInSeconds + tick(), + placeIds = placeIds + }) + state = Immutable.Set(state, gameSort.name, newSort) + end + elseif action.type == ClearAllGamesInSorts.name then + state = {} + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/SharedGamesInfo.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/SharedGamesInfo.lua new file mode 100644 index 0000000..d6beb45 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/SharedGamesInfo.lua @@ -0,0 +1,22 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local Immutable = require(Common.Immutable) +local LuaChat = Modules.LuaChat +local ShareGameToChatActions = LuaChat.Actions.ShareGameToChatFromChat + +local AddGamesInformation = require(ShareGameToChatActions.AddGamesInformationShareGameToChatFromChat) + +return function(state, action) + state = state or {} + if action.type == AddGamesInformation.name then + local tmpTable = {} + for _, gameInfo in pairs(action.games) do + tmpTable[gameInfo.placeId] = gameInfo + end + state = Immutable.JoinDictionaries(state, tmpTable) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Toast.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Toast.lua new file mode 100644 index 0000000..d896b6d --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Toast.lua @@ -0,0 +1,16 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local ShowToast = require(Modules.LuaChat.Actions.ShowToast) +local ToastComplete = require(Modules.LuaChat.Actions.ToastComplete) + +return function(state, action) + state = state or nil + + if action.type == ShowToast.name then + state = action.toast + elseif action.type == ToastComplete.name then + state = nil + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Toast.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Toast.spec.lua new file mode 100644 index 0000000..3eeca9f --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/Toast.spec.lua @@ -0,0 +1,42 @@ +return function() + local CoreGui = game:GetService("CoreGui") + local Modules = CoreGui.RobloxGui.Modules + local LuaChat = Modules.LuaChat + local ShowToast = require(LuaChat.Actions.ShowToast) + local ToastComplete = require(LuaChat.Actions.ToastComplete) + local ToastReducer = require(script.Parent.Toast) + local ToastModel = require(LuaChat.Models.ToastModel) + describe("Action Toast", function() + it("should return nil when passed nil", function() + local state = ToastReducer(nil, {}) + expect(state).to.equal(nil) + end) + + it("Action ShowToast", function() + local state = ToastReducer(nil, {}) + local myToast = ToastModel.new() + state = ToastReducer(state, ShowToast(myToast)) + + expect(state).to.equal(myToast) + end) + + it("Action ToastComplete", function() + local state = ToastReducer(nil, {}) + state = ToastReducer(state, ToastComplete()) + + expect(state).to.equal(nil) + end) + + it("ToastComplete should clear current toast", function() + local state = ToastReducer(nil, {}) + local myToast = ToastModel.new() + state = ToastReducer(state, ShowToast(myToast)) + + expect(state).to.equal(myToast) + + state = ToastReducer(state, ToastComplete()) + + expect(state).to.equal(nil) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ToggleChatPaused.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ToggleChatPaused.lua new file mode 100644 index 0000000..f5fdeee --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ToggleChatPaused.lua @@ -0,0 +1,13 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local ToggleChatPaused = require(Modules.LuaChat.Actions.ToggleChatPaused) + +return function(state, action) + state = state or false + + if action.type == ToggleChatPaused.name then + state = action.value + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ToggleChatPaused.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ToggleChatPaused.spec.lua new file mode 100644 index 0000000..bc78bf1 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/ToggleChatPaused.spec.lua @@ -0,0 +1,21 @@ +return function() + local LuaChat = script.Parent.Parent + local ToggleChatPaused = require(script.Parent.ToggleChatPaused) + local ActionToggleChatPaused = require(LuaChat.Actions.ToggleChatPaused) + + describe("Action ToggleChatPaused", function() + it("sets the ToggleChatPaused flag", function() + local state = ToggleChatPaused(nil, {}) + + expect(state).to.equal(false) + + state = ToggleChatPaused(state, ActionToggleChatPaused(false)) + + expect(state).to.equal(false) + + state = ToggleChatPaused(state, ActionToggleChatPaused(true)) + + expect(state).to.equal(true) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/UnreadConversationCount.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/UnreadConversationCount.lua new file mode 100644 index 0000000..3fe4325 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/UnreadConversationCount.lua @@ -0,0 +1,18 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local SetUnreadConversationCount = require(Modules.LuaChat.Actions.SetUnreadConversationCount) +local IncrementUnreadConversationCount = require(Modules.LuaChat.Actions.IncrementUnreadConversationCount) +local DecrementUnreadConversationCount = require(Modules.LuaChat.Actions.DecrementUnreadConversationCount) + +return function(state, action) + state = state or 0 + + if action.type == SetUnreadConversationCount.name then + state = action.count + elseif action.type == IncrementUnreadConversationCount.name then + state = state + 1 + elseif action.type == DecrementUnreadConversationCount.name then + state = state - 1 + end + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/UnreadConversationCount.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/UnreadConversationCount.spec.lua new file mode 100644 index 0000000..b6f3f72 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/UnreadConversationCount.spec.lua @@ -0,0 +1,48 @@ +return function() + local LuaChat = script.Parent.Parent + local UnreadConversationCount = require(LuaChat.Reducers.UnreadConversationCount) + local SetUnreadConversationCount = require(LuaChat.Actions.SetUnreadConversationCount) + local IncrementUnreadConversationCount = require(LuaChat.Actions.IncrementUnreadConversationCount) + local DecrementUnreadConversationCount = require(LuaChat.Actions.DecrementUnreadConversationCount) + + describe("Action SetUnreadConversationCount", function() + it("should set the value of UnreadConversationCount", function() + local state = nil + local action = SetUnreadConversationCount(5) + + state = UnreadConversationCount(state, action) + + expect(state).to.equal(5) + end) + end) + + describe("Action IncrementUnreadConversationCount", function() + it("should increment the value of UnreadConversationCount", function() + local state = nil + local action = SetUnreadConversationCount(5) + + state = UnreadConversationCount(state, action) + + action = IncrementUnreadConversationCount() + + state = UnreadConversationCount(state, action) + + expect(state).to.equal(6) + end) + end) + + describe("Action DecrementUnreadConversationCount", function() + it("should decrement the value of UnreadConversationCount", function() + local state = nil + local action = SetUnreadConversationCount(5) + + state = UnreadConversationCount(state, action) + + action = DecrementUnreadConversationCount() + + state = UnreadConversationCount(state, action) + + expect(state).to.equal(4) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/UsersAsync.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/UsersAsync.lua new file mode 100644 index 0000000..69f45d9 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/UsersAsync.lua @@ -0,0 +1,40 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local ReceivedAllFriends = require(Modules.LuaChat.Actions.ReceivedAllFriends) +local ReceivedUserPresence = require(Modules.LuaChat.Actions.ReceivedUserPresence) +local RequestAllFriends = require(Modules.LuaChat.Actions.RequestAllFriends) +local RequestUserPresence = require(Modules.LuaChat.Actions.RequestUserPresence) + +local Immutable = require(Modules.Common.Immutable) + +return function(state, action) + state = state or {} + + if action.type == RequestAllFriends.name then + return Immutable.JoinDictionaries(state, { + allFriendsIsFetching = true, + }) + elseif action.type == ReceivedAllFriends.name then + return Immutable.JoinDictionaries(state, { + allFriendsIsFetching = false, + }) + elseif action.type == RequestUserPresence.name then + local userAsync = state[action.userId] or {} + return Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(userAsync, { + presenceIsFetching = true, + }), + }) + elseif action.type == ReceivedUserPresence.name then + local userAsync = state[action.userId] + if userAsync then + return Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(userAsync, { + presenceIsFetching = false, + }), + }) + end + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/UsersAsync.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/UsersAsync.spec.lua new file mode 100644 index 0000000..133bc32 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Reducers/UsersAsync.spec.lua @@ -0,0 +1,59 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local LuaApp = Modules.LuaApp + local LuaChat = Modules.LuaChat + + local UsersAsyncReducer = require(script.Parent.UsersAsync) + local User = require(LuaApp.Models.User) + + local ReceivedAllFriends = require(LuaChat.Actions.ReceivedAllFriends) + local ReceivedUserPresence = require(LuaChat.Actions.ReceivedUserPresence) + local RequestAllFriends = require(LuaChat.Actions.RequestAllFriends) + local RequestUserPresence = require(LuaChat.Actions.RequestUserPresence) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = UsersAsyncReducer(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("RequestAllFriends", function() + it("should set the async state to true", function() + local state = UsersAsyncReducer(nil, {}) + state = UsersAsyncReducer(state, RequestAllFriends()) + + expect(state.allFriendsIsFetching).to.equal(true) + end) + end) + + describe("ReceivedAllFriends", function() + it("should set the async state to false", function() + local state = UsersAsyncReducer(nil, {}) + state = UsersAsyncReducer(state, ReceivedAllFriends()) + + expect(state.allFriendsIsFetching).to.equal(false) + end) + end) + + describe("RequestUserPresence", function() + it("should set the async state to true", function() + local user = User.mock() + local state = UsersAsyncReducer(nil, {}) + state = UsersAsyncReducer(state, RequestUserPresence(user.id)) + + expect(state[user.id].presenceIsFetching).to.equal(true) + end) + end) + + describe("ReceivedUserPresence", function() + it("should set the async state to false", function() + local user = User.mock() + local state = UsersAsyncReducer({[user.id] = {presenceIsFetching = true}}, {}) + state = UsersAsyncReducer(state, ReceivedUserPresence(user.id)) + + expect(state[user.id].presenceIsFetching).to.equal(false) + end) + end) + +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/RobloxEventReceiver.lua b/Client2018/content/internal/Chat/Modules/LuaChat/RobloxEventReceiver.lua new file mode 100644 index 0000000..5186563 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/RobloxEventReceiver.lua @@ -0,0 +1,340 @@ +local CoreGui = game:GetService("CoreGui") +local HttpService = game:GetService("HttpService") +local NotificationService = game:GetService("NotificationService") +local GuiService = game:GetService("GuiService") +local UserInputService = game:GetService("UserInputService") +local Players = game:GetService("Players") + +local Modules = CoreGui.RobloxGui.Modules +local LuaChat = Modules.LuaChat +local LuaApp = Modules.LuaApp + +local Constants = require(LuaChat.Constants) +local WebApi = require(LuaChat.WebApi) +local ConversationActions = require(LuaChat.Actions.ConversationActions) +local FetchChatEnabled = require(LuaChat.Actions.FetchChatEnabled) +local ReceivedUserTyping = require(LuaChat.Actions.ReceivedUserTyping) +local PlayTogetherActions = require(LuaChat.Actions.PlayTogetherActions) +local DialogInfo = require(LuaChat.DialogInfo) +local Config = require(LuaApp.Config) +local ToastModel = require(LuaChat.Models.ToastModel) +local NotificationType = require(LuaApp.Enum.NotificationType) + +local ChangedParticipants = require(LuaChat.Actions.ChangedParticipants) +local PopRoute = require(LuaChat.Actions.PopRoute) +local RemovedConversation = require(LuaChat.Actions.RemovedConversation) +local RenamedGroupConversation = require(LuaChat.Actions.RenamedGroupConversation) +local SetChatEnabled = require(LuaChat.Actions.SetChatEnabled) +local SetConnectionState = require(LuaChat.Actions.SetConnectionState) +local SetRoute = require(LuaChat.Actions.SetRoute) +local ShowToast = require(LuaChat.Actions.ShowToast) +local SetPreloading = require(LuaApp.Actions.SetPreloading) + +local FlagSettings = require(LuaApp.FlagSettings) + +local isLuaAppFriendshipCreatedSignalREnabled = FlagSettings.IsLuaAppFriendshipCreatedSignalREnabled() +local luaChatDisconnectBackButtonWhenOffScreen = settings():GetFFlag("LuaChatDisconnectBackButtonWhenOffScreen") + +local Intent = DialogInfo.Intent + +local function jsonDecode(data) + return HttpService:JSONDecode(data) +end + +local function getNewestWithNilPreviousMessageId(messages) + for id, message, _ in messages:CreateReverseIterator() do + if message.previousMessageId == nil then + return id + end + end + return messages.keys[1] +end + +local RobloxEventReceiver = {} +function RobloxEventReceiver:init(store) + local function onChatNotifications(eventData) + local detail = jsonDecode(eventData.detail) + local detailType = detail.Type or eventData.detailType + if detailType == "RemovedFromConversation" then + local conversationId = tostring(detail.ConversationId) + store:dispatch(RemovedConversation(conversationId)) + + local chatReducer = store:getState().ChatAppReducer + if chatReducer and chatReducer.Location then + local currentLocation = chatReducer.Location.current + if currentLocation and currentLocation.parameters then + if currentLocation.parameters.conversationId == conversationId then + local messageKey = "Feature.Chat.Message.RemovedFromConversation" + local toastModel = ToastModel.new(Constants.ToastIDs.REMOVED_FROM_CONVERSATION, messageKey, {}) + store:dispatch(ShowToast(toastModel)) + end + end + end + elseif detailType == "ConversationRemoved" then + local conversationId = tostring(detail.ConversationId) + store:dispatch(RemovedConversation(conversationId)) + elseif detailType == "ConversationTitleChanged" then + local conversationId = tostring(detail.ConversationId) + spawn(function() + local status, result = WebApi.GetConversations({conversationId}) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in RobloxEventReceiver->ConversationTitleChanged") + return + end + + local conversations = result.conversations + + if #conversations > 0 then + local conversation = conversations[1] + local title = conversation.title + local isDefaultTitle = conversation.isDefaultTitle + store:dispatch( + RenamedGroupConversation(conversationId, title, isDefaultTitle, conversation.lastUpdated) + ) + end + end) + elseif detailType == "ParticipantAdded" then + local convoId = tostring(detail.ConversationId) + spawn(function() + local status, result = WebApi.GetConversations({convoId}) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in RobloxEventReceiver->ParticipantAdded") + return + end + + local conversations = result.conversations + + if #conversations > 0 then + local conversation = conversations[1] + local participants = conversation.participants + local title = conversation.title + store:dispatch(ChangedParticipants(convoId, participants, title, conversation.lastUpdated)) + end + end) + elseif detailType == "ParticipantLeft" then + local convoId = tostring(detail.ConversationId) + spawn(function() + local status, result = WebApi.GetConversations({convoId}) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in RobloxEventReceiver->ParticipantLeft", status) + return + end + + local conversations = result.conversations + + if #conversations > 0 then + local conversation = conversations[1] + local participants = conversation.participants + local title = conversation.title + store:dispatch(ChangedParticipants(convoId, participants, title, conversation.lastUpdated)) + end + end) + elseif detailType == "AddedToConversation" then + local conversationId = tostring(detail.ConversationId) + spawn(function() + local status = store:dispatch(ConversationActions.GetConversations(conversationId)) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in RobloxEventReceiver->AddedToConversation") + return + end + end) + elseif detailType == "NewConversation" then + local conversationId = tostring(detail.ConversationId) + spawn(function() + local status = store:dispatch(ConversationActions.GetConversations(conversationId)) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in RobloxEventReceiver->NewConversation") + return + end + end) + elseif detailType == "NewMessage" or detailType == "NewMessageBySelf" then + local newMessageNotificationReceivedLocalTime = tick() + local conversationId = tostring(detail.ConversationId) + store:dispatch( + ConversationActions.GetNewMessages( + conversationId, + detailType == "NewMessageBySelf", + newMessageNotificationReceivedLocalTime + ) + ) + elseif detailType == "ParticipantTyping" then + local conversationId = tostring(detail.ConversationId) + local userId = tostring(detail.UserId) + store:dispatch(ReceivedUserTyping(conversationId, userId)) + elseif detailType == "ConversationUniverseChanged" then + local conversationId = tostring(detail.ConversationId) + local universeId = detail.UniverseId and tostring(detail.UniverseId) + local rootPlaceId = detail.RootPlaceId and tostring(detail.RootPlaceId) + store:dispatch(PlayTogetherActions.SetPinnedGameForConversation(universeId, rootPlaceId, conversationId)) + end + end + + local function onPresenceNotifications(eventData) + local detail = jsonDecode(eventData.detail) + local userId = tostring(detail.UserId) + store:dispatch(ConversationActions.GetUserPresences({userId})) + end + + local function onPresenceBulkNotifications(eventData) + local detail = jsonDecode(eventData.detail) + local userIds = {} + for _, update in ipairs(detail) do + table.insert(userIds, tostring(update.UserId)) + end + store:dispatch(ConversationActions.GetUserPresences(userIds)) + end + + local function onAppShellNotifications(eventData) + -- Note: AppShellNotifications are local and don't come from the + -- network. eventData.detail is not a structure, unlike other messages. + local detailType = eventData.detailType + if detailType == "StartConversationWithUserId" then + local userId = eventData.detail + spawn(function() + local status, result = WebApi.StartOneToOneConversation(userId) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in RobloxEventReceiver->AppShellNotifications, Status: "..tostring(status) ) + return + end + + if store:getState().ChatAppReducer.Conversations[result.id] == nil then + --Call GetConversations to make sure we hit the user and presence + --endpoints if need be. Being a bit lazy I suppose + local status = store:dispatch( + ConversationActions.GetConversations({result.id}) + ) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in RobloxEventReceiver->StartConversationWithUserId, Status: "..tostring(status)) + return + end + end + + store:dispatch(SetRoute(Intent.Conversation, {conversationId = result.id}, Intent.ConversationHub)) + end) + elseif detailType == "StartConversationWithId" then + local convoId = eventData.detail + if store:getState().ChatAppReducer.Conversations[convoId] == nil then + local status = store:dispatch( + ConversationActions.GetConversations({convoId}) + ) + + if status ~= WebApi.Status.OK then + warn("WebApi failure in RobloxEventReceiver->StartConversationWithId, Status: "..tostring(status)) + return + end + end + + store:dispatch(SetRoute(Intent.Conversation, {conversationId = convoId}, Intent.ConversationHub)) + elseif detailType == "Preloading" then + local isPreloading = eventData.detail == "true" + store:dispatch(SetPreloading(isPreloading)) + end + end + + local function onPrivacyNotifications(eventData) + local detail = jsonDecode(eventData.detail) + local detailType = detail.Type + + if detailType == "ChatDisabled" then + store:dispatch(SetChatEnabled(false)) + elseif detailType == "ChatEnabled" then + store:dispatch(SetChatEnabled(true)) + end + end + + local function onFriendshipNotifications(eventData) + local detail = jsonDecode(eventData.detail) + local detailType = detail.Type + + if detailType == "FriendshipCreated" then + -- LuaApp's RobloxEventReceiver will create the new user and mock conversation if this flag is on + if not isLuaAppFriendshipCreatedSignalREnabled then + local userId = tostring(Players.LocalPlayer.UserId) == tostring(detail.EventArgs.UserId1) + and detail.EventArgs.UserId2 or detail.EventArgs.UserId1 + store:dispatch(ConversationActions.FriendshipCreated(tostring(userId))) + end + end + end + + local function onRobloxEventReceived(eventData) + if eventData.namespace == "ChatNotifications" then + onChatNotifications(eventData) + elseif eventData.namespace == "PresenceNotifications" then + onPresenceNotifications(eventData) + elseif eventData.namespace == "PresenceBulkNotifications" then + onPresenceBulkNotifications(eventData) + elseif eventData.namespace == "ChatPrivacySettingNotifications" then + onPrivacyNotifications(eventData) + elseif eventData.namespace == "AppShellNotifications" then + onAppShellNotifications(eventData) + elseif eventData.namespace == "FriendshipNotifications" then + onFriendshipNotifications(eventData) + end + end + + local lastSeqNum = nil + local function onRobloxConnectionChanged(connectionHubName, connectionState, seqNum) + if connectionHubName == "signalR" then + store:dispatch(SetConnectionState(connectionState)) + if connectionState == Enum.ConnectionState.Connected then + if seqNum ~= lastSeqNum then + store:dispatch(FetchChatEnabled(function(chatEnabled) + if chatEnabled then + store:dispatch(ConversationActions.RefreshConversations()) + spawn(function() + store:dispatch(ConversationActions.GetAllFriendsAsync()) + end) + store:dispatch(ConversationActions.GetAllUserPresences()) + end + end)) + lastSeqNum = seqNum + end + local conversations = store:getState().ChatAppReducer.Conversations + for conversationId, conversation in pairs(conversations) do + if conversation.fetchingOlderMessages then + local messages = conversation.messages + local exclusiveMessageStartId = getNewestWithNilPreviousMessageId(messages) + store:dispatch(ConversationActions.GetOlderMessages(conversationId, exclusiveMessageStartId)) + end + end + end + + end + end + + local function onBackButtonPressed() + if #store:getState().ChatAppReducer.Location.history > 1 then + store:dispatch(PopRoute()) + else + GuiService:BroadcastNotification("", NotificationType.BACK_BUTTON_NOT_CONSUMED) + end + end + + --Protect this call because Tests run in a downgraded security context + pcall(function() + NotificationService.RobloxEventReceived:Connect(onRobloxEventReceived) + NotificationService.RobloxConnectionChanged:Connect(onRobloxConnectionChanged) + if not luaChatDisconnectBackButtonWhenOffScreen then + GuiService.ShowLeaveConfirmation:Connect(onBackButtonPressed) + end + end) + + if Config.LuaChat.Debug then + UserInputService.InputEnded:Connect(function(input, gameProcessed) + if input.UserInputType == Enum.UserInputType.Keyboard then + if input.KeyCode == Enum.KeyCode.Left then + onBackButtonPressed() + end + end + end) + end +end + +return RobloxEventReceiver \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/ScreenManager.lua b/Client2018/content/internal/Chat/Modules/LuaChat/ScreenManager.lua new file mode 100644 index 0000000..13fc8cd --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/ScreenManager.lua @@ -0,0 +1,286 @@ +local GuiService = game:GetService("GuiService") +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local PlayerService = game:GetService("Players") + +local FlagSettings = require(Modules.LuaApp.FlagSettings) +local NotificationType = require(Modules.LuaApp.Enum.NotificationType) + +local ScreenRouter = require(Modules.LuaChat.ScreenRouter) +local FormFactor = require(Modules.LuaApp.Enum.FormFactor) +local DialogFrame = require(Modules.LuaChat.Views.DialogFrame) + +local UseLuaBottomBar = FlagSettings.IsLuaBottomBarEnabled() + +local ScreenManager = {} + +--[[ + Create a new ScreenManager that uses the given 'root' +]] +function ScreenManager.new(root, appState) + local self = { + _stack = {}, + _framesToViews = {}, + rbx = root, + appState = appState, + route = nil, + routeMap = nil, + paused = false, + _analytics = appState.analytics, + } + + local dialogFrame = DialogFrame.new(appState, nil) + dialogFrame.rbx.Parent = self.rbx + self.dialogFrame = dialogFrame + + setmetatable(self, { + __index = ScreenManager + }) + + if self.appState then + self.appState.store.Changed:Connect(function(current, previous) + self:Update(current, previous) + end) + end + + return self +end + +function ScreenManager:Update(current, previous) + if current == previous then + return + end + + if self.paused ~= self.appState.store:getState().ChatAppReducer.ToggleChatPaused then + self.paused = self.appState.store:getState().ChatAppReducer.ToggleChatPaused + if self.paused then + self:DisableAllViews() + else + self:EnableAllViews() + self:SetTabBarVisible(current.TabBarVisible) + end + end + + if self.paused then + return + end + + if current.TabBarVisible ~= previous.TabBarVisible then + self:SetTabBarVisible(current.TabBarVisible) + end + + if not next(current.ChatAppReducer.Location.current) then + for _, view in ipairs(self._stack) do + self:PopView(view) + end + return + end + + if current.ChatAppReducer.Location.current == self.route then + + return + end + + if current.FormFactor == FormFactor.UNKNOWN then + return + end + + local routeMap = ScreenRouter.RouteMaps[current.FormFactor] + + if routeMap[current.ChatAppReducer.Location.current.intent] == nil then + return + end + + --If a view representing this route already exists on the stack, we'll just pop down to it + local viewInStack = false + for _, view in ipairs(self._stack) do + if ScreenRouter:Compare(view.route, current.ChatAppReducer.Location.current) then + viewInStack = true + break + end + end + + if viewInStack then + while not ScreenRouter:Compare(self:GetCurrentView().route, current.ChatAppReducer.Location.current) do + self:PopView(self:GetCurrentView()) + end + else + if current.ChatAppReducer.Location.current.popToIntent ~= nil then + while #self._stack > 1 + and current.ChatAppReducer.Location.current.popToIntent ~= self:GetCurrentView().route.intent do + self:PopView(self:GetCurrentView()) + end + end + local newView = ScreenRouter:GetView(self.appState, current.ChatAppReducer.Location.current, routeMap) + self:PushView(newView, current.ChatAppReducer.Location.current.intent) + end + + self.route = current.ChatAppReducer.Location.current +end + +function ScreenManager:GetViewOrderMap() + local indexMap = {} + + for i = #self._stack, 1, -1 do + local view = self._stack[i] + if not indexMap[view] then + indexMap[view] = i + end + end + + return indexMap +end + +function ScreenManager:SortViews() + local indexMap = self:GetViewOrderMap() + + for frame, view in pairs(self._framesToViews) do + local order = indexMap[view] or 0 + -- TODO: CLIPLAYEREX-1051: We shoud set the frame sort order here. This is something that the player experience + -- team is working on. It should be fine for us not to do this for now. + frame.Name = ("LuaChat-layer%d"):format(order) + end +end + +function ScreenManager:GetCurrentView() + return self._stack[#self._stack] +end + +function ScreenManager:GetViewStack() + return self._stack +end + +function ScreenManager:PushView(view, intent) + + local current = self:GetCurrentView() + + -- There are no reasons to push a view on top of itself! + if current == view then + error("Tried to push a view on top of itself!", 2) + end + + local newViewIntent + if view then + newViewIntent = view.route.intent + end + local curIntent + if current then + curIntent = current.route.intent + end + + if current then + current:Pause(view) + self.dialogFrame:TransitionDialogFrame(current, curIntent, newViewIntent, DialogFrame.TransitionType.Pause, nil) + end + + table.insert(self._stack, view) + + if view and view.rbx then + local frame = self.dialogFrame:AddDialogFrame(intent) + self._framesToViews[frame] = view + + frame.ChildRemoved:Connect(function() + self._framesToViews[frame] = nil + frame:Destroy() + end) + + view.rbx.Parent = frame + + self:SortViews() + end + + view:Start(current) + -- If we're the only view, we don't need to transition in. + if #self._stack > 1 then + self.dialogFrame:TransitionDialogFrame(view, newViewIntent, curIntent, DialogFrame.TransitionType.Start, nil) + end +end + +--[[ + Pop the view that's currently being shown. + + Optionally, pass in a view to make the ScreenManager only pop if that view + is the one on top of the stack. This makes the pop operation idempotent. +]] +function ScreenManager:PopView(expectView) + if expectView and expectView ~= self:GetCurrentView() then + return + end + + local last = self._stack[#self._stack] + local current = self._stack[#self._stack - 1] + local lastIntent + if last then + lastIntent = last.route.intent + end + local curIntent + if current then + curIntent = current.route.intent + end + + if last then + self:SortViews() + + last:Stop(current) + self.dialogFrame:TransitionDialogFrame(last, lastIntent, curIntent, DialogFrame.TransitionType.Stop, + function(playbackState) + if playbackState == Enum.PlaybackState.Completed then + last:Destruct() + if last.rbx then + last.rbx.Parent = nil + end + end + end) + end + + table.remove(self._stack, #self._stack) + + if current then + current:Resume(last) + self.dialogFrame:TransitionDialogFrame(current, curIntent, lastIntent, DialogFrame.TransitionType.Resume, nil) + end + + self.dialogFrame:ConfigureModalFrame() + + return last +end + +function ScreenManager:EnableAllViews() + self.dialogFrame.rbx.Enabled = true +end + +function ScreenManager:DisableAllViews() + self.dialogFrame.rbx.Enabled = false +end + +function ScreenManager:SetTabBarVisible(visible) + if UseLuaBottomBar then + return + end + + if visible then + GuiService:BroadcastNotification("", NotificationType.SHOW_TAB_BAR) + else + GuiService:BroadcastNotification("", NotificationType.HIDE_TAB_BAR) + end + + self:ReportLuaSettingOfBottomBarVisibility(visible) +end + +function ScreenManager:ReportLuaSettingOfBottomBarVisibility(visible) + local eventContext = "broadcastBottomBarVisibilitySetting" + local eventName = "setBottomBarVisibilityFromLua" + + local player = PlayerService.LocalPlayer + local userId = "UNKNOWN" + if player then + userId = tostring(player.UserId) + end + + local additionalArgs = { + uid = userId, + visible = visible, + } + self._analytics.EventStream:setRBXEventStream(eventContext, eventName, additionalArgs) +end + +return ScreenManager \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/ScreenManager.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/ScreenManager.spec.lua new file mode 100644 index 0000000..6a14baf --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/ScreenManager.spec.lua @@ -0,0 +1,123 @@ +return function() + local CoreGui = game:GetService("CoreGui") + + local Modules = CoreGui.RobloxGui.Modules + + local AppState = require(Modules.LuaChat.AppState) + local BaseView = require(Modules.LuaChat.BaseView) + local ScreenManager = require(Modules.LuaChat.ScreenManager) + + local FormFactor = require(Modules.LuaApp.Enum.FormFactor) + local AppReducer = require(Modules.LuaApp.AppReducer) + + local Rodux = require(Modules.Common.Rodux) + + local roduxStore = Rodux.Store.new(AppReducer, { + FormFactor = FormFactor.PHONE, + }) + local appState = AppState.mock(nil, roduxStore) + + --[[ + Make an approximation of a view using BaseView + ]] + local function makeView(intentValue) + local view = { + route = { + intent = intentValue, + }, + startCount = 0, + stopCount = 0, + resumeCount = 0, + pauseCount = 0, + rbx = Instance.new("Frame") + } + + setmetatable(view, { + __index = BaseView + }) + + function view:Start(...) + BaseView.Start(self, ...) + self.startCount = self.startCount + 1 + end + + function view:Stop(...) + BaseView.Stop(self, ...) + self.stopCount = self.stopCount + 1 + end + + function view:Resume(...) + BaseView.Resume(self, ...) + self.resumeCount = self.resumeCount + 1 + end + + function view:Pause(...) + BaseView.Pause(self, ...) + self.pauseCount = self.pauseCount + 1 + end + + function view:Destruct(...) + BaseView.Destruct(self, ...) + end + + return view + end + + it("should have no current view by default", function() + local screenManager = ScreenManager.new(nil, appState) + + expect(screenManager).to.be.ok() + end) + + it("should push views onto an empty stack", function() + local screenManager = ScreenManager.new(nil, appState) + local view = makeView("view") + + screenManager:PushView(view) + + expect(view.startCount).to.equal(1) + + expect(screenManager:GetCurrentView()).to.equal(view) + end) + + it("should push and pop views onto eachother", function() + local screenManager = ScreenManager.new(nil, appState) + local viewA = makeView("viewA") + local viewB = makeView("viewB") + + screenManager:PushView(viewA) + screenManager:PushView(viewB) + + expect(screenManager:GetCurrentView()).to.equal(viewB) + + expect(viewA.startCount).to.equal(1) + expect(viewA.pauseCount).to.equal(1) + + expect(viewB.startCount).to.equal(1) + + local popped = screenManager:PopView() + + expect(popped).to.equal(viewB) + expect(screenManager:GetCurrentView()).to.equal(viewA) + + expect(viewA.resumeCount).to.equal(1) + expect(viewB.stopCount).to.equal(1) + end) + + it("should prevent pushing a view on top of itself", function() + local screenManager = ScreenManager.new(nil, appState) + local viewA = makeView("viewA") + + expect(screenManager).to.be.ok() + + expect(function() + screenManager:PushView(viewA) + screenManager:PushView(viewA) + end).to.throw() + end) + + it("should have a valid Analytics object", function() + local screenManager = ScreenManager.new(nil, appState) + expect(screenManager._analytics).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/ScreenRouter.lua b/Client2018/content/internal/Chat/Modules/LuaChat/ScreenRouter.lua new file mode 100644 index 0000000..80ffb8a --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/ScreenRouter.lua @@ -0,0 +1,135 @@ +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules + +local LuaChat = Modules.LuaChat + +local FormFactor = require(Modules.LuaApp.Enum.FormFactor) +local DialogInfo = require(Modules.LuaChat.DialogInfo) + +local Intent = DialogInfo.Intent + +local ScreenRouter = {} + +ScreenRouter.RouteMaps = { + + [FormFactor.PHONE] = { + + BrowseGames = function(appState, route) + local BrowseGames = require(LuaChat.Views.BrowseGames) + return BrowseGames:Get(appState, route) + end, + ConversationHub = function(appState, route) + local ConversationHub = require(LuaChat.Views.Phone.ConversationHub) + return ConversationHub:Get(appState, route) + end, + Conversation = function(appState, route) + local Conversation = require(LuaChat.Views.Phone.Conversation) + return Conversation:Get(appState, route) + end, + GroupDetail = function(appState, route) + local GroupDetail = require(LuaChat.Views.Phone.GroupDetail) + + return GroupDetail:Get(appState, route) + end, + NewChatGroup = function(appState, route) + local NewChatGroup = require(LuaChat.Views.Phone.NewChatGroup) + return NewChatGroup:Get(appState, route) + end, + CreateChat = function(appState, route) + local CreateChat = require(LuaChat.Views.CreateChat) + return CreateChat:Get(appState, route) + end, + EditChatGroup = function(appState, route) + local EditChatGroup = require(LuaChat.Views.Phone.EditChatGroup) + return EditChatGroup:Get(appState, route) + end, + GenericDialog = function(appState, route) + local GenericDialog = require(LuaChat.Views.GenericDialog) + return GenericDialog:Get(appState, route) + end, + GameShare = function(appState, route) + local GameShare = require(LuaChat.Views.Phone.GameShareView) + return GameShare:Get(appState, route) + end, + }, + + [FormFactor.TABLET] = { + + BrowseGames = function(appState, route) + local BrowseGames = require(LuaChat.Views.BrowseGames) + return BrowseGames:Get(appState, route) + end, + ConversationHub = function(appState, route) + local ConversationHub = require(LuaChat.Views.Tablet.ConversationHub) + return ConversationHub:Get(appState, route) + end, + Conversation = function(appState, route) + local Conversation = require(LuaChat.Views.Phone.Conversation) + return Conversation:Get(appState, route) + end, + GroupDetail = function(appState, route) + local GroupDetail = require(LuaChat.Views.Phone.GroupDetail) + return GroupDetail:Get(appState, route) + end, + NewChatGroup = function(appState, route) + local NewChatGroup = require(LuaChat.Views.Phone.NewChatGroup) + return NewChatGroup:Get(appState, route) + end, + CreateChat = function(appState, route) + local CreateChat = require(LuaChat.Views.CreateChat) + return CreateChat:Get(appState, route) + end, + EditChatGroup = function(appState, route) + local EditChatGroup = require(LuaChat.Views.Phone.EditChatGroup) + return EditChatGroup:Get(appState, route) + end, + GenericDialog = function(appState, route) + local GenericDialog = require(LuaChat.Views.GenericDialog) + return GenericDialog:Get(appState, route) + end, + GameShare = function(appState, route) + local GameShare = require(LuaChat.Views.Tablet.GameShareView) + return GameShare:Get(appState, route) + end, + }, +} + +function ScreenRouter:Compare(firstRoute, secondRoute) + + if firstRoute.intent ~= secondRoute.intent then + return false + end + + for key, value in pairs(firstRoute.parameters) do + if value ~= secondRoute.parameters[key] then + return false + end + end + + for key, value in pairs(secondRoute.parameters) do + if value ~= firstRoute.parameters[key] then + return false + end + end + + return true +end + +function ScreenRouter:GetView(appState, route, routeMap) + if not Intent[route.intent] then + error(("Invalid intent value '%s'"):format( + ), 2) + end + + local mapper = routeMap[route.intent] + + if not mapper then + error(("No route map defined for intent '%s'"):format( + route.intent + ), 2) + end + + return mapper(appState, route) +end + +return ScreenRouter diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Services/RoactAnalyticsGameCardLoaded.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Services/RoactAnalyticsGameCardLoaded.lua new file mode 100644 index 0000000..c213424 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Services/RoactAnalyticsGameCardLoaded.lua @@ -0,0 +1,20 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Events = Modules.LuaChat.Analytics.Events +local loadGameLinkCardInChat = require(Events.loadGameLinkCardInChat) +local RoactAnalytics = require(Modules.LuaApp.Services.RoactAnalytics) + +local GameCardLoadedAnalytics = {} +function GameCardLoadedAnalytics.get(context) + local analyticsImpl = RoactAnalytics.get(context) + + local analyticsConsumer = {} + + function analyticsConsumer.reportGameCardLoadedInLuaChat(conversationId, placeId) + loadGameLinkCardInChat(analyticsImpl.EventStream, "luaAppChat", conversationId, placeId) + end + + return analyticsConsumer +end + +return GameCardLoadedAnalytics \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Services/RoactAnalyticsSharedGameItem.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Services/RoactAnalyticsSharedGameItem.lua new file mode 100644 index 0000000..c8fdd8f --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Services/RoactAnalyticsSharedGameItem.lua @@ -0,0 +1,19 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local RoactAnalytics = require(Modules.LuaApp.Services.RoactAnalytics) +local shareGameToChatFromChat = require(Modules.LuaChat.Analytics.Events.shareGameToChatFromChat) + +local RoactAnalyticsSharedGameItem = {} +function RoactAnalyticsSharedGameItem.get(context) + local analyticsImpl = RoactAnalytics.get(context) + + local AnalyticsObj = {} + + function AnalyticsObj.reportShareGameToChatFromChat(cid, pid) + shareGameToChatFromChat(analyticsImpl.EventStream, "touch", cid, pid) + end + + return AnalyticsObj +end + +return RoactAnalyticsSharedGameItem \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/SortedActivelyPlayedGames.lua b/Client2018/content/internal/Chat/Modules/LuaChat/SortedActivelyPlayedGames.lua new file mode 100644 index 0000000..4c648b7 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/SortedActivelyPlayedGames.lua @@ -0,0 +1,65 @@ +local SortActivelyPlayedGames = {} + +local function getSortedActivelyPlayedGames(pinnedGameRootPlaceId, inGameParticipants, includeEmptyPinned) + local sortedGames = {} + local activeGamesDict = {} + local pinnedGameBeingPlayed = false + + for _, user in pairs(inGameParticipants) do + if not activeGamesDict[user.placeId] then + activeGamesDict[user.placeId] = {} + end + + table.insert(activeGamesDict[user.placeId], {uid = user.id, joinedAt = user.joinedAt or 0}) + end + + for placeId, players in pairs(activeGamesDict) do + table.sort(players, function(a, b) + return a.joinedAt > b.joinedAt + end) + if placeId == pinnedGameRootPlaceId then + pinnedGameBeingPlayed = true + table.insert(sortedGames, {placeId = placeId, friends = players, pinned = true, recommended = false}) + else + table.insert(sortedGames, {placeId = placeId, friends = players, pinned = false, recommended = false}) + end + end + + table.sort(sortedGames, function(a, b) + if #a.friends > #b.friends then + return true + end + if #a.friends == #b.friends then + return a.friends[1].joinedAt > b.friends[1].joinedAt + end + return false + end) + + -- if pinned game is being played, move it to the first Position + if pinnedGameBeingPlayed then + for index, game in pairs(sortedGames) do + if game.placeId == pinnedGameRootPlaceId then + if index == 1 then + break + end + table.insert(sortedGames, 1, table.remove(sortedGames, index)) + break + end + end + elseif pinnedGameRootPlaceId and includeEmptyPinned then + -- Our pinned game isn't included, but it should be: + table.insert(sortedGames, 1, {placeId = pinnedGameRootPlaceId, friends = {} , pinned = true, recommended = false}) + end + + return sortedGames +end + +function SortActivelyPlayedGames.getSortedGames(pinnedGameRootPlaceId, inGameParticipants) + return getSortedActivelyPlayedGames(pinnedGameRootPlaceId, inGameParticipants, false) +end + +function SortActivelyPlayedGames.getSortedGamesPlusEmptyPinned(pinnedGameRootPlaceId, inGameParticipants) + return getSortedActivelyPlayedGames(pinnedGameRootPlaceId, inGameParticipants, true) +end + +return SortActivelyPlayedGames \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/TabBarView.lua b/Client2018/content/internal/Chat/Modules/LuaChat/TabBarView.lua new file mode 100644 index 0000000..82151fb --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/TabBarView.lua @@ -0,0 +1,282 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) +local RoactMotion = require(LuaApp.RoactMotion) + +local Constants = require(LuaApp.Constants) +local Device = require(LuaChat.Device) +local FormFactor = require(LuaApp.Enum.FormFactor) +local Text = require(LuaChat.Text) + +local NO_TABS = "Tabbed frame has no tabs. If this functionality is desired, use a Frame." +local NO_CONTENT = "Content function didn't return a table. If no content is desired, return an empty table." + +local DEFAULT_DROPS_SHADOW_HEIGHT = 5 + +local DEFAULT_INDICATOR_HEIGHT = 4 +local DEFAULT_INDICATOR_COLOR = Constants.Color.BLUE_PRIMARY + +local DEFAULT_TAB_BAR_HEIGHT = 44 +local DEFAULT_TAB_BAR_BUTTON_MINIMUM_WIDTH = 72 +local DEFAULT_TAB_BAR_BUTTON_PADDING = 12 +local DEFAULT_TAB_BAR_BUTTON_BACKGROUND_COLOR = Constants.Color.WHITE + +local DEFAULT_BUTTON_TEXT_SIZE = 20 +local DEFAULT_BUTTON_TEXT_COLOR = Constants.Color.GRAY1 +local DEFAULT_BUTTON_TEXT_FONT = Enum.Font.SourceSans + +local DEFAULT_BACKGROUND_COLOR = Constants.Color.GRAY6 +local DEFAULT_SELECTED_INDEX = 1 +local DEFAULT_TAB_COUNT = 4 + +local DEFAULT_SPRING_DAMPING = 35 +local DEFAULT_SPRING_STIFFNESS = 390 +local DEFAULT_PRECISION = 4 + +local DROPS_SHADOW_IMAGE = "rbxasset://textures/ui/LuaChat/graphic/gr-overlay-shadow.png" + +local function getButtonWidth( + formFactor, text, buttonTextFont, buttonTextSize, + tabBarButtonMinimumWidth, tabBarButtonPadding, frameWidth + ) + if formFactor == FormFactor.TABLET then + return frameWidth / DEFAULT_TAB_COUNT + end + + local textWidth = Text.GetTextWidth(text, buttonTextFont, buttonTextSize) + return 2 * tabBarButtonPadding + ((tabBarButtonMinimumWidth > textWidth) and tabBarButtonMinimumWidth or textWidth) +end + +local TabBarView = Roact.PureComponent:extend("TabBarView") + +TabBarView.defaultProps = { + BackgroundColor = DEFAULT_BACKGROUND_COLOR, + ButtonTextColor = DEFAULT_BUTTON_TEXT_COLOR, + ButtonTextFont = DEFAULT_BUTTON_TEXT_FONT, + ButtonTextSize = DEFAULT_BUTTON_TEXT_SIZE, + DropsShadowHeight = DEFAULT_DROPS_SHADOW_HEIGHT, + IndicatorColor = DEFAULT_INDICATOR_COLOR, + IndicatorHeight = DEFAULT_INDICATOR_HEIGHT, + Precision = DEFAULT_PRECISION, + SpringDamping = DEFAULT_SPRING_DAMPING, + SpringStiffness = DEFAULT_SPRING_STIFFNESS, + TabBarHeight = DEFAULT_TAB_BAR_HEIGHT, + TabBarButtonColor = DEFAULT_TAB_BAR_BUTTON_BACKGROUND_COLOR, + TabBarButtonMinimumWidth = DEFAULT_TAB_BAR_BUTTON_MINIMUM_WIDTH, + TabBarButtonPadding = DEFAULT_TAB_BAR_BUTTON_PADDING, +} + +function TabBarView:init() + self.state = { + autoScroll = false, + frameWidth = 0, + frameHeight = 0, + selectedTabIndex = self.props.SelectedTabIndex or DEFAULT_SELECTED_INDEX, + scrollingFrameXOffset = 0, + } + + self.onRef = function(rbx) + if rbx then + if self.onAbsoluteSizeChanged then + self.onAbsoluteSizeChanged:Disconnect() + end + self.onAbsoluteSizeChanged = rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + if self.state.frameWidth ~= rbx.AbsoluteSize.X then + self:setState({ + frameWidth = rbx.AbsoluteSize.X, + frameHeight = rbx.AbsoluteSize.Y, + }) + end + end) + else + if self.onAbsoluteSizeChanged then + self.onAbsoluteSizeChanged:Disconnect() + self.onAbsoluteSizeChanged = nil + end + end + end + + self.onScrollingFrameRef = function(rbx) + if rbx then + self.onScrollingFrameCanvasPositionChanged = rbx:GetPropertyChangedSignal("CanvasPosition"):Connect(function() + if not self.state.autoScroll then + self:setState({ + scrollingFrameXOffset = rbx.CanvasPosition.X + }) + end + end) + else + if self.onScrollingFrameCanvasPositionChanged then + self.onScrollingFrameCanvasPositionChanged:Disconnect() + self.onScrollingFrameCanvasPositionChanged = nil + end + end + end + + self.onSelected = function(rbx, index) + if index == self.state.index then + return + end + + local scrollingFrameXOffsetCorrection = 0 + if rbx.AbsolutePosition.X < 0 then + scrollingFrameXOffsetCorrection = rbx.AbsolutePosition.X + elseif rbx.AbsolutePosition.X + rbx.AbsoluteSize.X > self.state.frameWidth then + scrollingFrameXOffsetCorrection = (rbx.AbsolutePosition.X + rbx.AbsoluteSize.X) - self.state.frameWidth + end + + self:setState({ + selectedTabIndex = index, + autoScroll = scrollingFrameXOffsetCorrection ~= 0, + scrollingFrameXOffset = self.state.scrollingFrameXOffset + scrollingFrameXOffsetCorrection, + }) + end +end + +function TabBarView:render() + local tabs = self.props.tabs or {} + if #tabs <= 0 then + warn(NO_TABS) + return nil + end + + local backgroundColor = self.props.BackgroundColor + local buttonTextColor = self.props.ButtonTextColor + local buttonTextFont = self.props.ButtonTextFont + local buttonTextSize = self.props.ButtonTextSize + local dropsShadowHeight = self.props.DropsShadowHeight + local formFactor = self.props.FormFactor or Device.FormFactor.PHONE + local indicatorColor = self.props.IndicatorColor + local indicatorHeight = self.props.IndicatorHeight + local precision = self.props.Precision + local selectedTabIndex = self.state.selectedTabIndex + local springStiffness = self.props.SpringStiffness + local springDamping = self.props.SpringDamping + local tabBarButtonColor = self.props.TabBarButtonColor + local tabBarButtonMinimumWidth = self.props.TabBarButtonMinimumWidth + local tabBarButtonPadding = self.props.TabBarButtonPadding + local tabBarHeight = self.props.TabBarHeight + + local tarBarContentWidth = 0 + + local tabBarButtons = {} + tabBarButtons["Layout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + }) + for index, tab in ipairs(tabs) do + local buttonWidth = getButtonWidth(formFactor, tab.title, buttonTextFont, buttonTextSize, + tabBarButtonMinimumWidth, tabBarButtonPadding, self.state.frameWidth) + tarBarContentWidth = buttonWidth + tarBarContentWidth + + tabBarButtons[index] = Roact.createElement("Frame", { + BackgroundColor3 = tabBarButtonColor, + BorderSizePixel = 0, + Size = UDim2.new(0, buttonWidth, 0, tabBarHeight), + },{ + Roact.createElement("TextButton", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = buttonTextFont, + Size = UDim2.new(1, 0, 1, 0), + Text = tab.title, + TextColor3 = buttonTextColor, + TextSize = buttonTextSize, + + [Roact.Event.Activated] = function(rbx) + self.onSelected(rbx, index) + end + }), + + Indicator = selectedTabIndex == index and Roact.createElement("Frame", { + BackgroundColor3 = indicatorColor, + Size = UDim2.new(1, 0, 0, indicatorHeight), + BorderSizePixel = 0, + AnchorPoint = Vector2.new(0, 1), + Position = UDim2.new(0, 0, 1, 0), + }), + }) + end + + local scrollFrameWidth = (tarBarContentWidth < self.state.frameWidth) + and tarBarContentWidth + or self.state.frameWidth + + if tabs[selectedTabIndex].content.component == nil then + warn(NO_CONTENT) + return nil + end + + tabs[selectedTabIndex].content.options.frameHeight = self.state.frameHeight + return Roact.createElement("Frame", { + BackgroundColor3 = backgroundColor, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + + [Roact.Ref] = self.onRef, + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + + TabBarMenusContainer = Roact.createElement("Frame", { + BackgroundColor3 = tabBarButtonColor, + BorderSizePixel = 0, + LayoutOrder = 1, + Size = UDim2.new(1, 0, 0, tabBarHeight), + }, { + TabBarMenus = Roact.createElement(RoactMotion.SimpleMotion, { + style = { + offsetX = RoactMotion.spring(self.state.scrollingFrameXOffset, springStiffness, springDamping, precision), + }, + + render = function (values) + local canvasPositionX = self.state.autoScroll and values.offsetX or self.state.scrollingFrameXOffset + return Roact.createElement("ScrollingFrame", { + AnchorPoint = Vector2.new(0.5, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + CanvasSize = UDim2.new(0, tarBarContentWidth, 0, tabBarHeight), + CanvasPosition = Vector2.new(canvasPositionX, 0), + ClipsDescendants = true, + Position = UDim2.new(0.5, 0, 0, 0), + ScrollBarThickness = 0, + Size = UDim2.new(0, scrollFrameWidth, 1, 0), + [Roact.Ref] = self.onScrollingFrameRef, + }, tabBarButtons) + end + }) + }), + + ContentContainer = Roact.createElement("Frame", { + BackgroundColor3 = DEFAULT_BACKGROUND_COLOR, + BorderSizePixel = 0, + LayoutOrder = 2, + Size = UDim2.new(1, 0, 1, -tabBarHeight), + }, { + Content = Roact.createElement(tabs[selectedTabIndex].content.component, + tabs[selectedTabIndex].content.options), + + DropsShadow = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = DROPS_SHADOW_IMAGE, + Size = UDim2.new(1, 0, 0, dropsShadowHeight), + }), + }) + }) +end + +return RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + FormFactor = state.FormFactor, + } + end +)(TabBarView) diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Text.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Text.lua new file mode 100644 index 0000000..faf8869 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Text.lua @@ -0,0 +1,4 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Text = require(Modules.Common.Text) +-- Redirect to Common.Text Module +return Text \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/TimeUnit.lua b/Client2018/content/internal/Chat/Modules/LuaChat/TimeUnit.lua new file mode 100644 index 0000000..1ce9701 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/TimeUnit.lua @@ -0,0 +1,17 @@ +local TimeUnit = setmetatable({}, { + __index = function(self, key) + error(("Invalid TimeUnit \"%s\""):format(tostring(key)), 2) + end +}) + +TimeUnit.Seconds = "Seconds" +TimeUnit.Minutes = "Minutes" +TimeUnit.Hours = "Hours" +TimeUnit.Days = "Days" +TimeUnit.Months = "Months" +TimeUnit.Years = "Years" + +-- Locale-specific +TimeUnit.Weeks = "Weeks" + +return TimeUnit \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/TimeZone.lua b/Client2018/content/internal/Chat/Modules/LuaChat/TimeZone.lua new file mode 100644 index 0000000..8c1e0c5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/TimeZone.lua @@ -0,0 +1,10 @@ +local TimeZone = setmetatable({}, { + __index = function(self, key) + error(("Invalid TimeZone \"%s\""):format(tostring(key)), 2) + end +}) + +TimeZone.UTC = -2 +TimeZone.Current = -1 + +return TimeZone \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Utils/formatInteger.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Utils/formatInteger.lua new file mode 100644 index 0000000..3f4c6e6 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Utils/formatInteger.lua @@ -0,0 +1,22 @@ +return function(num, sep, sepCount) + assert(type(num) == "number", "formatInteger expects a number; was given type: " .. type(num)) + + sep = sep or "," + sepCount = sepCount or 3 + + local parsedInt = string.format("%.0f", math.abs(num)) + local firstSeperatorIndex = #parsedInt % sepCount + if firstSeperatorIndex == 0 then + firstSeperatorIndex = sepCount + end + + local seperatorPattern = "(" .. string.rep("%d", sepCount) .. ")" + local seperatorReplacement = sep .. "%1" + local result = parsedInt:sub(1, firstSeperatorIndex) .. + parsedInt:sub(firstSeperatorIndex+1):gsub(seperatorPattern, seperatorReplacement) + if num < 0 then + result = "-" .. result + end + + return result +end diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Utils/formatInteger.spec.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Utils/formatInteger.spec.lua new file mode 100644 index 0000000..894deb6 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Utils/formatInteger.spec.lua @@ -0,0 +1,99 @@ +return function() + local formatInteger = require(script.Parent.formatInteger) + + describe("FormatIntegerString", function() + it("should throw an error if called an a non-number type", function() + local num = "123" + expect(function() + formatInteger(num) + end).to.throw() + end) + + it("Should format positive integer whose length less than or equal to sepCount with comma properly", function() + local num = 123 + local expectedResult = "123" + expect(formatInteger(num)).to.equal(expectedResult) + end) + it("Should format negative integer whose length less than or equal to sepCount with comma properly", function() + local num = -123 + local expectedResult = "-123" + expect(formatInteger(num)).to.equal(expectedResult) + end) + it("Should format positive integer whose length greater than sepCount with comma properly", function() + local num = 1234 + local expectedResult = "1,234" + expect(formatInteger(num)).to.equal(expectedResult) + end) + it("Should format negative integer whose length greater than sepCount with comma properly", function() + local num = -1234 + local expectedResult = "-1,234" + expect(formatInteger(num)).to.equal(expectedResult) + end) + + it("Should format positive integer in the form for scientific notation with comma properly", function() + local num = 4.5e21 + local expectedResult = "4,500,000,000,000,000,000,000" + expect(formatInteger(num)).to.equal(expectedResult) + end) + it("Should format negative integer in the form for scientific notation with comma properly", function() + local num = -4.5e21 + local expectedResult = "-4,500,000,000,000,000,000,000" + expect(formatInteger(num)).to.equal(expectedResult) + end) + + it("Should format positive and negative zero with comma properly ", function() + local num1 = 0 + local num2 = -0 + local expectedResult = "0" + expect(formatInteger(num1)).to.equal(expectedResult) + expect(formatInteger(num2)).to.equal(expectedResult) + end) + + it("Should format positive integer whose length less than or equal to sepCount with dot properly", function() + local num = 12 + local expectedResult = "12" + expect(formatInteger(num, ".", 2)).to.equal(expectedResult) + end) + it("Should format negative integer whose length less than or equal to sepCount with dot properly", function() + local num = -12 + local expectedResult = "-12" + expect(formatInteger(num, ".", 2)).to.equal(expectedResult) + end) + it("Should format positive integer whose length greater than sepCount with dot properly", function() + local num = 123 + local expectedResult = "1.23" + expect(formatInteger(num, ".", 2)).to.equal(expectedResult) + end) + it("Should format negative integer whose length greater than sepCount with comma properly", function() + local num = -123 + local expectedResult = "-1.23" + expect(formatInteger(num, ".", 2)).to.equal(expectedResult) + end) + + it("Should format positive integer whose length less than or equal to sepCount with dot properly", function() + local num = 123 + local expectedResult = "123" + expect(formatInteger(num, ".", 4)).to.equal(expectedResult) + end) + it("Should format negative integer whose length less than or equal to sepCount with dot properly", function() + local num = -123 + local expectedResult = "-123" + expect(formatInteger(num, ".", 4)).to.equal(expectedResult) + end) + it("Should format positive integer whose length greater than sepCount with dot properly", function() + local num = 12345 + local expectedResult = "1.2345" + expect(formatInteger(num, ".", 4)).to.equal(expectedResult) + end) + it("Should format negative integer whose length greater than sepCount with comma properly", function() + local num = -12345 + local expectedResult = "-1.2345" + expect(formatInteger(num, ".", 4)).to.equal(expectedResult) + end) + it("Should format positive integer whose length greater than sepCount with dot properly", function() + local num = 12345 + local expectedResult = "1.2345" + expect(formatInteger(num, ".", 4)).to.equal(expectedResult) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Utils/getConversationDisplayTitle.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Utils/getConversationDisplayTitle.lua new file mode 100644 index 0000000..2290ffe --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Utils/getConversationDisplayTitle.lua @@ -0,0 +1,3 @@ +return function(conversation) + return (conversation.title or conversation.titleForViewer):gsub("\n", "") +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Utils/getInputEvent.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Utils/getInputEvent.lua new file mode 100644 index 0000000..55be066 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Utils/getInputEvent.lua @@ -0,0 +1,9 @@ +local luaAppLegacyInputDisabledGlobally = settings():GetFFlag('LuaAppLegacyInputDisabledGlobally2') + +return function(component) + if luaAppLegacyInputDisabledGlobally then + return component.Activated + else + return component.MouseButton1Click + end +end diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Utils/reportToDiagByCountryCode.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Utils/reportToDiagByCountryCode.lua new file mode 100644 index 0000000..7278aaf --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Utils/reportToDiagByCountryCode.lua @@ -0,0 +1,19 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local PercentReportingByCountryCode = tonumber(settings():GetFVariable("PercentReportingByCountryCode")) or 0 +local WebApi = require(Modules.LuaChat.WebApi) + +local function shouldReportForLocation() + return math.random(0, 99) < PercentReportingByCountryCode +end + +return function(featureName, measureName, seconds) + if not shouldReportForLocation() then + return + end + + local status = WebApi.ReportToDiagByCountryCode(featureName, measureName, seconds) + if status ~= WebApi.Status.OK then + warn("Failed to report ".. measureName .." to Diag") + end +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Utils/truncateAssetLink.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Utils/truncateAssetLink.lua new file mode 100644 index 0000000..b76f529 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Utils/truncateAssetLink.lua @@ -0,0 +1,3 @@ +return function(gameUrl) + return string.match(gameUrl, ".*%d+") +end \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Views/BrowseGames.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Views/BrowseGames.lua new file mode 100644 index 0000000..060b9a5 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Views/BrowseGames.lua @@ -0,0 +1,63 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local LuaChat = Modules.LuaChat +local Components = LuaChat.Components + +local BaseScreen = require(LuaChat.Views.Phone.BaseScreen) +local BrowseGamesComponent = require(Components.BrowseGames) +local PopRoute = require(LuaChat.Actions.PopRoute) + +local BrowseGames = BaseScreen:Template() +BrowseGames.__index = BrowseGames + +function BrowseGames.new(appState, route) + local self = { + appState = appState, + route = route, + connections = {}; + } + setmetatable(self, BrowseGames) + + self.BrowseGamesComponent = BrowseGamesComponent.new(appState) + self.rbx = self.BrowseGamesComponent.rbx + + local backButtonPressedConnection = self.BrowseGamesComponent.BackButtonPressed:Connect(function() + self.appState.store:Dispatch(PopRoute()) + end) + table.insert(self.connections, backButtonPressedConnection) + + return self +end + +function BrowseGames:Start() + BaseScreen.Start(self) + do + local connection = self.appState.store.Changed:Connect(function(current, previous) + self:Update(current, previous) + end) + table.insert(self.connections, connection) + end +end + +function BrowseGames:Stop() + for _, connection in pairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + + BaseScreen.Stop(self) +end + +function BrowseGames:Destruct() + self.BrowseGamesComponent:Destruct() + self.BrowseGamesComponent = nil + + BaseScreen.Destruct(self) +end + +function BrowseGames:Update(current, previous) + self.BrowseGamesComponent:Update(current, previous) +end + +return BrowseGames \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Views/CreateChat.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Views/CreateChat.lua new file mode 100644 index 0000000..8d82ce1 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Views/CreateChat.lua @@ -0,0 +1,73 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local LuaChat = Modules.LuaChat +local Components = LuaChat.Components + +local DialogInfo = require(LuaChat.DialogInfo) + +local BaseScreen = require(LuaChat.Views.Phone.BaseScreen) + +local createChatComponent = require(Components.CreateChat) + +local PopRoute = require(LuaChat.Actions.PopRoute) +local SetRoute = require(LuaChat.Actions.SetRoute) + +local Intent = DialogInfo.Intent + +local CreateChat = BaseScreen:Template() +CreateChat.__index = CreateChat + +function CreateChat.new(appState, route) + local self = { + appState = appState, + route = route, + connections = {}; + } + setmetatable(self, CreateChat) + + self.createChatComponent = createChatComponent.new(appState) + self.rbx = self.createChatComponent.rbx + + local backButtonPressedConnection = self.createChatComponent.BackButtonPressed:Connect(function() + self.appState.store:dispatch(PopRoute()) + end) + table.insert(self.connections, backButtonPressedConnection) + + local conversationSavedConnection = self.createChatComponent.ConversationSaved:Connect(function(id) + self.appState.store:dispatch(SetRoute(Intent.Conversation, {conversationId = id}, Intent.ConversationHub)) + end) + table.insert(self.connections, conversationSavedConnection) + + return self +end + +function CreateChat:Start() + BaseScreen.Start(self) + do + local connection = self.appState.store.Changed:Connect(function(current, previous) + self:Update(current, previous) + end) + table.insert(self.connections, connection) + end +end + +function CreateChat:Stop() + for _, connection in pairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + + BaseScreen.Stop(self) +end + +function CreateChat:Destruct() + self.createChatComponent:Destruct() + self.createChatComponent = nil + + BaseScreen.Destruct(self) +end + +function CreateChat:Update(current, previous) + self.createChatComponent:Update(current, previous) +end + +return CreateChat \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Views/DialogFrame.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Views/DialogFrame.lua new file mode 100644 index 0000000..389ffa8 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Views/DialogFrame.lua @@ -0,0 +1,232 @@ +local TweenService = game:GetService("TweenService") + +local Modules = script.Parent.Parent + +local BaseScreen = require(Modules.Views.Phone.BaseScreen) +local Create = require(Modules.Create) +local Constants = require(Modules.Constants) +local DialogInfo = require(Modules.DialogInfo) +local DefaultScreenComponent = require(Modules.Components.DefaultScreen) + +local DialogFrame = BaseScreen:Template() +DialogFrame.__index = DialogFrame + +--Constants +local RIGHT_SIDE_POS = UDim2.new(1, 0, 0, 0) +local LEFT_SIDE_POS = UDim2.new(-1, 0, 0, 0) +local CENTERED_POS = UDim2.new(0, 0, 0, 0) + +local FFlagLuaChatUsesZIndexSibling = settings():GetFFlag("LuaChatUsesZIndexSibling") + + +DialogFrame.TransitionType = { + Start = "Start", + Stop = "Stop", + Resume = "Resume", + Pause = "Pause", +} + +local LuaChatDisplayInFront = settings():GetFFlag("LuaChatDisplayInFront") + +function DialogFrame.new(appState, route) + local self = {} + self.appState = appState + self.route = route + setmetatable(self, DialogFrame) + + self.rbx = Instance.new("ScreenGui") + if FFlagLuaChatUsesZIndexSibling then + self.rbx.ZIndexBehavior = Enum.ZIndexBehavior.Sibling + end + self.rbx.Name = "ChatScreen" + + if LuaChatDisplayInFront then + --Offseting the display order by 2 in hopes of working around + --un-reproducable bug where the AE screenGui isn't being de-parented + self.rbx.DisplayOrder = 3 + else + self.rbx.DisplayOrder = 1 + end + + self.baseFrame = Create.new "Frame" { + Visible = false, + Name = "BaseFrame", + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Constants.Color.GRAY6, + BorderSizePixel = 0, + + Create.new "Frame" { + Name = "LeftHandFrame", + Size = UDim2.new(0.37, 0, 1, 0), + BackgroundTransparency = 1.0, + }, + + Create.new "Frame" { + Name = "RightHandFrame", + Size = UDim2.new(0.63, 0, 1, 0), + Position = UDim2.new(0.37, 0, 0, 0), + BackgroundTransparency = 1.0, + }, + + Create.new "Frame" { + Name = "Divider", + BackgroundColor3 = Constants.Color.GRAY1, + BackgroundTransparency = Constants.Color.ALPHA_SHADOW_HOVER, + BorderSizePixel = 0.0, + Size = UDim2.new(0, 1, 1, 0), + Position = UDim2.new(0.37, 0, 0, 0), + }, + + Create.new "Frame" { + Name = "ModalFrameBase", + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + BackgroundColor3 = Color3.fromRGB(0, 0, 0), + BackgroundTransparency = Constants.Color.ALPHA_SHADOW_PRIMARY, + Visible = false, + + Create.new "TextButton" { + Name = "TapBlocker", + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + BackgroundTransparency = 1 + }, + + Create.new "ImageLabel" { + Name = "ModalFrame", + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.new(1, -360, 1, -36 -36), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundTransparency = 1, + + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(5,5,6,6), + Image = "rbxasset://textures/ui/LuaChat/9-slice/modal.png", + + Create.new "UIPadding" { + PaddingBottom = UDim.new(0, Constants.ModalDialog.CLEARANCE_CORNER_ROUNDING) + } + } + } + } + + self.baseFrame.Parent = self.rbx + self.leftHandFrame = self.baseFrame.LeftHandFrame + self.rightHandFrame = self.baseFrame.RightHandFrame + self.modalFrameBase = self.baseFrame.ModalFrameBase + self.modalFrame = self.modalFrameBase.ModalFrame + self.initialized = false + + return self +end + +function DialogFrame:Initialize() + self.baseFrame.Visible = true + self.initialized = true + + local defaultScreen = DefaultScreenComponent.new(self.appState) + defaultScreen.rbx.Parent = self.rightHandFrame +end + +function DialogFrame:AddDialogFrame(intent) + if not self.initialized then + self:Initialize() + end + + local dialogType = DialogInfo.GetTypeBasedOnIntent(self.appState.store:getState().FormFactor, intent) + + local newFrame = Create.new "Frame" { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1.0, + } + + if dialogType == DialogInfo.DialogType.Centered then + newFrame.Parent = self.baseFrame + elseif dialogType == DialogInfo.DialogType.Left then + newFrame.Parent = self.leftHandFrame + elseif dialogType == DialogInfo.DialogType.Right then + newFrame.Parent = self.rightHandFrame + elseif dialogType == DialogInfo.DialogType.Modal then + newFrame.Parent = self.modalFrame + elseif dialogType == DialogInfo.DialogType.Popup then + newFrame.Parent = self.baseFrame + end + + self:ConfigureModalFrame() + + return newFrame +end + +function DialogFrame:ConfigureModalFrame() + local hasGuis = false + for _, child in pairs(self.modalFrame:GetChildren()) do + if child:IsA("GuiBase") then + hasGuis = true + break + end + end + + if hasGuis then + self.modalFrameBase.Visible = true + else + self.modalFrameBase.Visible = false + end +end + +function DialogFrame:TransitionDialogFrame(frame, intent, otherIntent, transitionType, callback) + if (not self.appState) or (not intent) or (not otherIntent) then + if callback ~= nil then + callback(Enum.PlaybackState.Completed) + end + return + end + + local dialogType = DialogInfo.GetTypeBasedOnIntent(self.appState.store:getState().FormFactor, intent) + local otherDialogType = DialogInfo.GetTypeBasedOnIntent(self.appState.store:getState().FormFactor, otherIntent) + + if dialogType ~= DialogInfo.DialogType.Centered or otherDialogType ~= DialogInfo.DialogType.Centered then + if callback ~= nil then + callback(Enum.PlaybackState.Completed) + end + return + end + + local startingPos + local endingPos + if transitionType == self.TransitionType.Start then + startingPos = RIGHT_SIDE_POS + endingPos = CENTERED_POS + elseif transitionType == self.TransitionType.Stop then + startingPos = CENTERED_POS + endingPos = RIGHT_SIDE_POS + elseif transitionType == self.TransitionType.Resume then + startingPos = LEFT_SIDE_POS + endingPos = CENTERED_POS + elseif transitionType == self.TransitionType.Pause then + startingPos = CENTERED_POS + endingPos = LEFT_SIDE_POS + end + + frame.rbx.Position = startingPos + local tweenPosition = TweenService:Create( + frame.rbx, + TweenInfo.new( + Constants.Tween.DEFAULT_TWEEN_TIME, + Constants.Tween.DEFAULT_TWEEN_STYLE, + Constants.Tween.DEFAULT_TWEEN_EASING_DIRECTION + ), + { + Position = endingPos, + } + ) + + tweenPosition:Play() + if callback then + spawn(function() + local playbackState = tweenPosition.Completed:wait() + callback(playbackState) + end) + end +end + +return DialogFrame \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Views/GenericDialog.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Views/GenericDialog.lua new file mode 100644 index 0000000..8c81674 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Views/GenericDialog.lua @@ -0,0 +1,99 @@ +local Modules = script.Parent.Parent +local Components = Modules.Components + +local BaseScreen = require(Modules.Views.Phone.BaseScreen) + +local GenericDialogType = require(Components.GroupDetailDialogs.GenericDialogType) +local EditChatGroupNameDialog = require(Components.GroupDetailDialogs.EditChatGroupNameDialog) +local LeaveGroupDialog = require(Components.GroupDetailDialogs.LeaveGroupDialog) +local ParticipantDialog = require(Components.GroupDetailDialogs.ParticipantDialog) +local RemoveUserDialog = require(Components.GroupDetailDialogs.RemoveUserDialog) + +local GenericDialog = BaseScreen:Template() +GenericDialog.__index = GenericDialog + +function GenericDialog.new(appState, route) + local self = { + appState = appState, + route = route, + connections = {}, + } + setmetatable(self, GenericDialog) + + self.dialogComponent = self.route.parameters.dialog + self.parameters = self.route.parameters.dialogParameters + + if self.route.parameters.dialog == GenericDialogType.EditChatGroupNameDialog then + self.currentDialog = EditChatGroupNameDialog.new( + self.appState, + self.parameters.titleLocalizationKey, + self.parameters.maxChar, + self.parameters.conversation + ) + self.rbx = self.currentDialog.dialog.rbx + self.textBoxToFocus = self.currentDialog.dialog.textInputComponent.textBoxComponent + elseif self.route.parameters.dialog == GenericDialogType.LeaveGroupDialog then + self.currentDialog = LeaveGroupDialog.new( + self.appState, + self.parameters.titleKey, + self.parameters.messageKey, + self.parameters.cancelTitleKey, + self.parameters.confirmationTitleKey, + self.parameters.conversation + ) + self.rbx = self.currentDialog.dialog.rbx + elseif self.route.parameters.dialog == GenericDialogType.ParticipantDialog then + self.currentDialog = ParticipantDialog.new( + self.appState, + self.parameters.titleKey, + self.parameters.options, + self.parameters.conversationId, + self.parameters.conversation, + self.parameters.userId + ) + self.rbx = self.currentDialog.dialog.rbx + elseif self.route.parameters.dialog == GenericDialogType.RemoveUserDialog then + self.currentDialog = RemoveUserDialog.new( + self.appState, + self.parameters.titleKey, + self.parameters.messageKey, + self.parameters.cancelTitleKey, + self.parameters.confirmationTitleKey, + self.parameters.conversation + ) + self.currentDialog.dialog:Update(self.parameters.messageKey, self.parameters.user, self.parameters.messageArguments) + self.rbx = self.currentDialog.dialog.rbx + else + print("Attempting to open unknown type of Dialog: ", self.route.parameters.dialog) + end + + return self +end + +function GenericDialog:Start() + self.rbx.Visible = true + if self.textBoxToFocus then + self.textBoxToFocus:CaptureFocus() + end +end + +function GenericDialog:Stop() + self.rbx.Visible = false + self.rbx.Parent = nil + self.currentDialog:Destruct() + self.textBoxToFocus = nil +end + +function GenericDialog:Resume() + +end + +function GenericDialog:Pause() + +end + +function GenericDialog:Update(current, previous) + self.dialogComponent:Update(current, previous) +end + +return GenericDialog \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/Alert.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/Alert.lua new file mode 100644 index 0000000..c72fc99 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/Alert.lua @@ -0,0 +1,66 @@ +local Modules = script.Parent.Parent.Parent + +local Components = Modules.Components +local BaseView = require(Modules.Views.Phone.BaseScreen) + +local Create = require(Modules.Create) +local AlertModel = require(Modules.Models.Alert) + +local DialogComponents = require(Components.DialogComponents) + +local DeleteAlert = require(Modules.Actions.DeleteAlert) + +local Alert = BaseView:Template() + +function Alert.new(appState, route) + local self = { + appState = appState, + route = route, + alerts = {}, + } + setmetatable(self, {__index = Alert}) + + self.rbx = Create.new"Frame" { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + } + + self.alertDialog = DialogComponents.AlertDialog.new(appState, nil, nil) + self.alertDialog.rbx.Parent = self.rbx + + self.alerts = nil + self.alert = nil + + self.alertDialog.accepted.Event:Connect(function() + if self.alert ~= nil then + self.appState.store:dispatch(DeleteAlert(self.alert)) + end + end) + + self.appState.store.Changed:Connect(function(current, previous) + if current ~= previous then + self:Update(current.ChatAppReducer.AppState.alerts) + end + end) + + return self +end + +function Alert:Update(alerts) + if alerts and alerts ~= self.alerts then + if #(alerts.keys) >= 1 then + local alertId = alerts.keys[1] + local alert = alerts.values[alertId] + if alert.type == AlertModel.AlertType.DIALOG then + self.alertDialog:Update(alert) + self.alertDialog:Prompt() + else + warn("Unhandled AlertType") + end + self.alert = alert + end + self.alerts = alerts + end +end + +return Alert diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/BaseScreen.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/BaseScreen.lua new file mode 100644 index 0000000..21a64b0 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/BaseScreen.lua @@ -0,0 +1,27 @@ +--[[ + Defines a set of tweens and default lifecycle handlers appropriate for this + platform. +]] + +local Modules = script.Parent.Parent.Parent + +local BaseView = require(Modules.BaseView) + +local BaseScreen = BaseView:Template() + +function BaseScreen:Start() +end + +function BaseScreen:Stop() +end + +function BaseScreen:Resume() +end + +function BaseScreen:Pause() +end + +function BaseScreen:Destruct() +end + +return BaseScreen \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/Conversation.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/Conversation.lua new file mode 100644 index 0000000..8794edf --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/Conversation.lua @@ -0,0 +1,109 @@ +local LuaChat = script.Parent.Parent.Parent +local BaseScreen = require(script.Parent.BaseScreen) + +local Components = LuaChat.Components +local ConversationComponent = require(Components.Conversation) + +local DialogInfo = require(LuaChat.DialogInfo) + +local PopRoute = require(LuaChat.Actions.PopRoute) +local SetRoute = require(LuaChat.Actions.SetRoute) + +local Intent = DialogInfo.Intent + +local ConversationView = BaseScreen:Template() + +ConversationView.__index = ConversationView +ConversationView.viewCache = {} + +function ConversationView:Get(appState, route) + if self.viewCache[route.parameters.conversationId] then + return self.viewCache[route.parameters.conversationId] + end + + local view = self.new(appState, route) + self.viewCache[route.parameters.conversationId] = view + + return view +end + +function ConversationView.new(appState, route) + local self = {} + self.route = route + self.conversationId = route.parameters.conversationId + self.appState = appState + self.connections = {} + + setmetatable(self, ConversationView) + + self.conversationComponent = ConversationComponent.new(appState) + self.rbx = self.conversationComponent.rbx + + return self +end + +function ConversationView:Start() + BaseScreen.Start(self) + + local backButtonConnection = self.conversationComponent.BackButtonPressed:Connect(function() + self.appState.store:dispatch(PopRoute()) + end) + table.insert(self.connections, backButtonConnection) + + local groupDetailConnection = self.conversationComponent.GroupDetailsButtonPressed:Connect(function() + self.appState.store:dispatch(SetRoute( + Intent.GroupDetail, + { + conversationId = self.conversationId, + } + )) + end) + table.insert(self.connections, groupDetailConnection) + + do + local connection = self.appState.store.Changed:Connect(function(state, oldState) + local conversation = state.ChatAppReducer.Conversations[self.conversationId] + + if not conversation then + if self.appState.screenManager:GetCurrentView() == self then + self.appState.store:dispatch(SetRoute( + nil, + {}, + Intent.ConversationHub + )) + end + self:Stop() + self.viewCache[self.conversationId] = nil + return + end + self.conversationComponent:Update(state, oldState) + end) + table.insert(self.connections, connection) + end + + self.conversationComponent:Start() +end + +function ConversationView:Stop() + BaseScreen.Stop(self) + self.conversationComponent:Stop() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + + self.connections = {} +end + +function ConversationView:Pause() + BaseScreen.Pause(self) + self.conversationComponent:Pause() +end + +function ConversationView:Resume() + BaseScreen.Resume(self) + self.conversationComponent:Resume() +end + + + +return ConversationView diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/ConversationHub.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/ConversationHub.lua new file mode 100644 index 0000000..87a7f15 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/ConversationHub.lua @@ -0,0 +1,107 @@ +local UserInputService = game:GetService("UserInputService") + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local BaseScreen = require(LuaChat.Views.Phone.BaseScreen) +local Create = require(LuaChat.Create) + +local ConversationHubComponent = require(LuaChat.Components.ConversationHub) + +local DialogInfo = require(LuaChat.DialogInfo) +local Constants = require(LuaChat.Constants) +local ConversationActions = require(LuaChat.Actions.ConversationActions) + +local SetRoute = require(LuaChat.Actions.SetRoute) +local SetTabBarVisible = require(LuaApp.Actions.SetTabBarVisible) + +local LuaChatCreateChatEnabled = settings():GetFFlag("LuaChatCreateChatEnabled") + +local Intent = DialogInfo.Intent + +local ConversationHub = BaseScreen:Template() + +ConversationHub.__index = ConversationHub + +function ConversationHub.new(appState, route) + local self = {} + + setmetatable(self, ConversationHub) + + self.appState = appState + self.route = route + + self.ConversationHubComponent = ConversationHubComponent.new(appState) + self.rbx = self.ConversationHubComponent.rbx + + local spacer = Create.new "Frame" { + Name = "Spacer", + Size = UDim2.new(1, 0, 0, UserInputService.BottomBarSize.Y), + BackgroundColor3 = Constants.Color.WHITE, + BorderColor3 = Constants.Color.WHITE, + BackgroundTransparency = 0, + LayoutOrder = 2, + } + spacer.Parent = self.rbx + + self.ConversationHubComponent.ConversationTapped:Connect(function(convoId) + if self.appState.screenManager:GetCurrentView() ~= self then + return + end + + local conversation = self.appState.store:getState().ChatAppReducer.Conversations[convoId] + + if conversation == nil then + return + end + + if conversation.serverState == Constants.ServerState.NONE then + if LuaChatCreateChatEnabled then + self.appState.store:dispatch(ConversationActions.StartOneToOneConversation(conversation, function(id) + self.appState.store:dispatch(SetRoute(Intent.Conversation, {conversationId = id})) + end)) + else + self.appState.store:dispatch(ConversationActions.StartOneToOneConversation(conversation, function(serverConversation) + self.appState.store:dispatch(SetRoute(Intent.Conversation, {conversationId = serverConversation.id})) + end)) + end + else + self.appState.store:dispatch(SetRoute(Intent.Conversation, {conversationId = convoId})) + end + end) + + self.ConversationHubComponent.CreateChatButtonPressed:Connect(function() + self.appState.store:dispatch(SetRoute(LuaChatCreateChatEnabled and Intent.CreateChat or Intent.NewChatGroup, {})) + end) + + return self +end + +function ConversationHub:Start() + BaseScreen.Start(self) + self.ConversationHubComponent:Start() + self.appState.store:dispatch(SetTabBarVisible(true)) +end + +function ConversationHub:Stop() + BaseScreen.Stop(self) + self.ConversationHubComponent:Stop() + self.appState.store:dispatch(SetTabBarVisible(false)) +end + +function ConversationHub:Resume() + BaseScreen.Resume(self) + self.appState.store:dispatch(SetTabBarVisible(true)) +end + +function ConversationHub:Pause() + BaseScreen.Pause(self) + self.appState.store:dispatch(SetTabBarVisible(false)) +end + +function ConversationHub:Update(state, oldState) + self.ConversationHubComponent:Update(state, oldState) +end + +return ConversationHub \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/EditChatGroup.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/EditChatGroup.lua new file mode 100644 index 0000000..cf11996 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/EditChatGroup.lua @@ -0,0 +1,81 @@ +local Modules = script.Parent.Parent.Parent +local Components = Modules.Components + +local Constants = require(Modules.Constants) + +local BaseScreen = require(Modules.Views.Phone.BaseScreen) + +local EditChatGroupComponent = require(Components.EditChatGroup) + +local DialogInfo = require(Modules.DialogInfo) +local Intent = DialogInfo.Intent + +local PopRoute = require(Modules.Actions.PopRoute) +local SetRoute = require(Modules.Actions.SetRoute) + +local EditChatGroup = BaseScreen:Template() +EditChatGroup.__index = EditChatGroup + +function EditChatGroup.new(appState, route) + local self = { + appState = appState, + route = route, + convoId = route.parameters.conversationId, + connections = {}, + } + setmetatable(self, EditChatGroup) + + local participantCount = #appState.store:getState().ChatAppReducer.Conversations[self.convoId].participants + local maxSize = Constants.MAX_PARTICIPANT_COUNT + 1 - participantCount + self.editChatGroupComponent = EditChatGroupComponent.new(appState, maxSize, self.convoId) + self.rbx = self.editChatGroupComponent.rbx + + local backButtonConnection = self.editChatGroupComponent.BackButtonPressed:Connect(function() + self.appState.store:dispatch(PopRoute()) + end) + table.insert(self.connections, backButtonConnection) + + return self +end + +function EditChatGroup:Start() + BaseScreen.Start(self) + + do + local connection = self.appState.store.Changed:Connect(function(current, previous) + local currentConversationId = current.ChatAppReducer.Location.current.parameters.conversationId + local conversation = current.ChatAppReducer.Conversations[currentConversationId] + if current ~= previous and conversation then + self:Update(current, previous) + else + if self.appState.screenManager:GetCurrentView() == self then + self.appState.store:dispatch(SetRoute(nil, {}, Intent.ConversationHub)) + end + end + self:Update(current, previous) + end) + table.insert(self.connections, connection) + end +end + +function EditChatGroup:Stop() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + + BaseScreen.Stop(self) +end + +function EditChatGroup:Destruct() + self.editChatGroupComponent:Destruct() + self.editChatGroupComponent = nil + + BaseScreen.Destruct(self) +end + +function EditChatGroup:Update(current, previous) + self.editChatGroupComponent:Update(current, previous) +end + +return EditChatGroup \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/GameShareView.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/GameShareView.lua new file mode 100644 index 0000000..5a8102a --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/GameShareView.lua @@ -0,0 +1,66 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local BaseScreen = require(LuaChat.Views.Phone.BaseScreen) +local Create = require(LuaChat.Create) +local Constants = require(LuaChat.Constants) +local GameShareComponent = require(LuaChat.Components.GameShareComponent) + +local SetTabBarVisible = require(LuaApp.Actions.SetTabBarVisible) + +local GameShareView = BaseScreen:Template() +GameShareView.__index = GameShareView + +function GameShareView.new(appState, route) + local self = {} + self.appState = appState + self.route = route + + setmetatable(self, GameShareView) + + local innerFrame = Create.new"Frame" { + Name = "InnerFrame", + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(0.5, 0), + BackgroundColor3 = Constants.Color.GRAY5, + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = 2, + Create.new("UIListLayout") { + Name = "ListLayout", + SortOrder = Enum.SortOrder.LayoutOrder, + }, + } + + self.gameShareComponent = GameShareComponent.new(appState, route.parameters.placeId, innerFrame) + self.rbx = self.gameShareComponent.rbx + + return self +end + +function GameShareView:Start() + self.gameShareComponent:Start() + self.prevTabBarVisibility = self.appState.store:getState().TabBarVisible + + BaseScreen.Start(self) + + self.appState.store:dispatch(SetTabBarVisible(false)) +end + +function GameShareView:Stop() + self.gameShareComponent:Stop() + + BaseScreen.Stop(self) + + self.appState.store:dispatch(SetTabBarVisible(self.prevTabBarVisibility)) +end + +function GameShareView:Destruct() + self.gameShareComponent:Destruct() + + BaseScreen.Destruct(self) +end + +return GameShareView \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/GroupDetail.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/GroupDetail.lua new file mode 100644 index 0000000..740d950 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/GroupDetail.lua @@ -0,0 +1,118 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local LuaChat = Modules.LuaChat + +local BaseScreen = require(LuaChat.Views.Phone.BaseScreen) +local Constants = require(LuaChat.Constants) +local ToastModel = require(LuaChat.Models.ToastModel) +local DialogInfo = require(LuaChat.DialogInfo) + +local GroupDetailComponent = require(LuaChat.Components.GroupDetail) + +local PopRoute = require(LuaChat.Actions.PopRoute) +local SetRoute = require(LuaChat.Actions.SetRoute) +local ShowToast = require(LuaChat.Actions.ShowToast) + +local Intent = DialogInfo.Intent + +local GroupDetail = BaseScreen:Template() +GroupDetail.__index = GroupDetail + +function GroupDetail.new(appState, route) + local self = {} + + self.appState = appState + self.route = route + + self.groupDetailComponent = GroupDetailComponent.new(appState, route.parameters.conversationId) + self.rbx = self.groupDetailComponent.rbx + self.connections = {} + + setmetatable(self, GroupDetail) + + local backButtonConnection = self.groupDetailComponent.BackButtonPressed:Connect(function() + self.appState.store:dispatch(PopRoute()) + end) + table.insert(self.connections, backButtonConnection) + + local addFriendsConnection = self.groupDetailComponent.AddFriendsPressed:Connect(function() + if self.appState.screenManager:GetCurrentView() ~= self then + return + end + + local participantCount = #self.groupDetailComponent.conversation.participants + if participantCount >= Constants.MAX_PARTICIPANT_COUNT + 1 then + local messageKey = "Feature.Chat.Message.ToastText" + local messageArguments = { + friendNum = tostring(Constants.MAX_PARTICIPANT_COUNT+1), + } + local toastModel = ToastModel.new(Constants.ToastIDs.TOO_MANY_PEOPLE, messageKey, messageArguments) + self.appState.store:dispatch(ShowToast(toastModel)) + else + self.appState.store:dispatch(SetRoute(Intent.EditChatGroup, { + conversationId = self.groupDetailComponent.conversation.id + })) + end + end) + table.insert(self.connections, addFriendsConnection) + + return self +end + +function GroupDetail:Start() + BaseScreen.Start(self) + + do + local connection = self.appState.store.Changed:Connect(function(current, previous) + local currentConversationId = current.ChatAppReducer.Location.current.parameters.conversationId + local conversation = current.ChatAppReducer.Conversations[currentConversationId] + if current ~= previous and conversation then + self.groupDetailComponent:Update(current, previous) + else + if self.appState.screenManager:GetCurrentView() == self then + self.appState.store:dispatch(SetRoute(nil, {}, Intent.ConversationHub)) + end + end + end) + table.insert(self.connections, connection) + end +end + +-- GroupDetail does not need to slide off-screen when spawning Dialogs. +function GroupDetail:Pause() + local state = self.appState.store:getState() + local dialogType = DialogInfo.GetTypeBasedOnIntent( + self.appState.store:getState().FormFactor, + state.ChatAppReducer.Location.current.intent + ) + + if dialogType == DialogInfo.DialogType.Popup then + self.isNextPageGenericDialog = true + else + self.isNextPageGenericDialog = false + BaseScreen.Pause(self) + end +end + +function GroupDetail:Resume() + if self.isNextPageGenericDialog == nil or self.isNextPageGenericDialog == false then + BaseScreen.Resume(self) + end + self.isNextPageGenericDialog = false +end + +function GroupDetail:Stop() + BaseScreen.Stop(self) + + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + + self.connections = {} + self.groupDetailComponent:Stop() +end + +function GroupDetail:Destruct() + self.groupDetailComponent:Destruct() +end + +return GroupDetail \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/NewChatGroup.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/NewChatGroup.lua new file mode 100644 index 0000000..c692526 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Phone/NewChatGroup.lua @@ -0,0 +1,72 @@ +local Modules = script.Parent.Parent.Parent +local Components = Modules.Components + +local DialogInfo = require(Modules.DialogInfo) + +local BaseScreen = require(Modules.Views.Phone.BaseScreen) + +local NewChatGroupComponent = require(Components.NewChatGroup) + +local PopRoute = require(Modules.Actions.PopRoute) +local SetRoute = require(Modules.Actions.SetRoute) + +local Intent = DialogInfo.Intent + +local NewChatGroup = BaseScreen:Template() +NewChatGroup.__index = NewChatGroup + +function NewChatGroup.new(appState, route) + local self = { + appState = appState, + route = route, + connections = {}; + } + setmetatable(self, NewChatGroup) + + self.newChatGroupComponent = NewChatGroupComponent.new(appState) + self.rbx = self.newChatGroupComponent.rbx + + local backButtonPressedConnection = self.newChatGroupComponent.BackButtonPressed:Connect(function() + self.appState.store:dispatch(PopRoute()) + end) + table.insert(self.connections, backButtonPressedConnection) + + local conversationSavedConnection = self.newChatGroupComponent.ConversationSaved:Connect(function(id) + self.appState.store:dispatch(SetRoute(Intent.Conversation, {conversationId = id}, Intent.ConversationHub)) + end) + table.insert(self.connections, conversationSavedConnection) + + return self +end + +function NewChatGroup:Start() + BaseScreen.Start(self) + do + local connection = self.appState.store.Changed:Connect(function(current, previous) + self:Update(current, previous) + end) + table.insert(self.connections, connection) + end +end + +function NewChatGroup:Stop() + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + self.connections = {} + + BaseScreen.Stop(self) +end + +function NewChatGroup:Destruct() + self.newChatGroupComponent:Destruct() + self.newChatGroupComponent = nil + + BaseScreen.Destruct(self) +end + +function NewChatGroup:Update(current, previous) + self.newChatGroupComponent:Update(current, previous) +end + +return NewChatGroup \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Views/Tablet/ConversationHub.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Tablet/ConversationHub.lua new file mode 100644 index 0000000..3d66a88 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Tablet/ConversationHub.lua @@ -0,0 +1,106 @@ +local LuaChat = script.Parent.Parent.Parent + +local Constants = require(LuaChat.Constants) +local ConversationActions = require(LuaChat.Actions.ConversationActions) +local DialogInfo = require(LuaChat.DialogInfo) + +local BaseScreen = require(script.Parent.Parent.Phone.BaseScreen) + +local ConversationHubComponent = require(LuaChat.Components.ConversationHub) +local ConversationComponent = require(LuaChat.Components.Conversation) + +local SetRoute = require(LuaChat.Actions.SetRoute) + +local LuaChatCreateChatEnabled = settings():GetFFlag("LuaChatCreateChatEnabled") + +local Intent = DialogInfo.Intent + +local ConversationHub = BaseScreen:Template() + +ConversationHub.__index = ConversationHub + +ConversationHub.conversationCache = {} + +function ConversationHub.new(appState, route) + local self = { + appState = appState, + route = route, + connections = {}, + } + + setmetatable(self, ConversationHub) + + self.conversationHubComponent = ConversationHubComponent.new(appState) + self.rbx = self.conversationHubComponent.rbx + self.conversationToGroupDetailsConnection = nil + + self.conversationComponent = ConversationComponent.new(appState) + self.conversationToGroupDetailsConnection = self.conversationComponent.GroupDetailsButtonPressed:Connect(function() + self.appState.store:dispatch(SetRoute(Intent.GroupDetail, { + conversationId = self.conversationComponent.conversationId, + })) + end) + + self.conversationHubComponent.ConversationTapped:Connect(function(convoId) + local conversation = self.appState.store:getState().ChatAppReducer.Conversations[convoId] + if conversation == nil then + return + end + + if conversation.serverState == Constants.ServerState.NONE then + if LuaChatCreateChatEnabled then + self.appState.store:dispatch(ConversationActions.StartOneToOneConversation(conversation, + function(id) + self.appState.store:dispatch(SetRoute( + Intent.Conversation, + {conversationId = id}, + Intent.ConversationHub + )) + end) + ) + else + self.appState.store:dispatch(ConversationActions.StartOneToOneConversation(conversation, + function(serverConversation) + self.appState.store:dispatch(SetRoute( + Intent.Conversation, + {conversationId = serverConversation.id}, + Intent.ConversationHub + )) + end) + ) + end + else + self.appState.store:dispatch(SetRoute( + Intent.Conversation, + {conversationId = convoId}, + Intent.ConversationHub + )) + end + end) + + self.conversationHubComponent.CreateChatButtonPressed:Connect(function() + self.appState.store:dispatch(SetRoute(LuaChatCreateChatEnabled and Intent.CreateChat or Intent.NewChatGroup, {})) + end) + + return self +end + +function ConversationHub:Start() + BaseScreen.Start(self) + self.conversationHubComponent:Start() + self.conversationComponent:Start() +end + +function ConversationHub:Stop() + BaseScreen.Start(self) + self.conversationHubComponent:Stop() + self.conversationComponent:Stop() + + for _, connection in ipairs(self.connections) do + connection:Disconnect() + end + + self.connections = {} +end + +return ConversationHub diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Views/Tablet/GameShareView.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Tablet/GameShareView.lua new file mode 100644 index 0000000..c287017 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Views/Tablet/GameShareView.lua @@ -0,0 +1,78 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local BaseScreen = require(LuaChat.Views.Phone.BaseScreen) +local Create = require(LuaChat.Create) +local Constants = require(LuaChat.Constants) +local GameShareComponent = require(LuaChat.Components.GameShareComponent) + +local SetTabBarVisible = require(LuaApp.Actions.SetTabBarVisible) + +local GameShareView = BaseScreen:Template() +GameShareView.__index = GameShareView + +function GameShareView.new(appState, route) + local self = {} + self.appState = appState + self.route = route + + setmetatable(self, GameShareView) + + local dividerHeight = Constants.GameShareView.TABLET_HORIZONTAL_DIVIDER_HEIGHT + local viewWidth = Constants.GameShareView.TABLET_VIEW_WIDTH + + local tabletDivider = Create.new"Frame" { + Name = "TabletDivider", + BackgroundColor3 = Constants.Color.GRAY5, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, dividerHeight), + LayoutOrder = 1, + } + + local innerFrame = Create.new"Frame" { + Name = "InnerFrame", + Size = UDim2.new(0, viewWidth, 1, -dividerHeight), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(0.5, 0), + BackgroundColor3 = Constants.Color.GRAY5, + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = 2, + Create.new("UIListLayout") { + Name = "ListLayout", + SortOrder = Enum.SortOrder.LayoutOrder, + }, + + tabletDivider, + } + + self.gameShareComponent = GameShareComponent.new(appState, route.parameters.placeId, innerFrame) + self.rbx = self.gameShareComponent.rbx + + return self +end + +function GameShareView:Start() + self.gameShareComponent:Start() + + BaseScreen.Start(self) + + self.appState.store:dispatch(SetTabBarVisible(false)) +end + +function GameShareView:Stop() + self.gameShareComponent:Stop() + + BaseScreen.Stop(self) + + self.appState.store:dispatch(SetTabBarVisible(true)) +end + +function GameShareView:Destruct() + self.gameShareComponent:Destruct() + + BaseScreen.Destruct(self) +end + +return GameShareView \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/Views/ToastView.lua b/Client2018/content/internal/Chat/Modules/LuaChat/Views/ToastView.lua new file mode 100644 index 0000000..77960c3 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/Views/ToastView.lua @@ -0,0 +1,22 @@ +local Modules = script.Parent.Parent +local Components = Modules.Components + +local ToastComponent = require(Components.ToastComponent) +local BaseScreen = require(Modules.Views.Phone.BaseScreen) + +local ToastView = BaseScreen:Template() +ToastView.__index = ToastView + +function ToastView.new(appState, route) + local self = {} + self.appState = appState + self.route = route + setmetatable(self, ToastView) + + self.ToastComponent = ToastComponent.new(appState, route) + self.rbx = self.ToastComponent.rbx + + return self +end + +return ToastView \ No newline at end of file diff --git a/Client2018/content/internal/Chat/Modules/LuaChat/WebApi.lua b/Client2018/content/internal/Chat/Modules/LuaChat/WebApi.lua new file mode 100644 index 0000000..91e1529 --- /dev/null +++ b/Client2018/content/internal/Chat/Modules/LuaChat/WebApi.lua @@ -0,0 +1,801 @@ +local ContentProvider = game:GetService("ContentProvider") +local CoreGui = game:GetService("CoreGui") +local HttpService = game:GetService("HttpService") +local Players = game:GetService("Players") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common +local LuaApp = Modules.LuaApp +local LuaChat = Modules.LuaChat + +local Config = require(LuaApp.Config) +local Constants = require(LuaChat.Constants) +local Conversation = require(LuaChat.Models.Conversation) +local Functional = require(Common.Functional) +local HttpDebug = require(LuaChat.Debug.HttpDebug) +local Message = require(LuaChat.Models.Message) +local User = require(LuaApp.Models.User) + +local LuaChatUseNewFriendsAndPresenceEndpoint = settings():GetFFlag("LuaChatUseNewFriendsAndPresenceEndpoint") + +local WebApi = {} + +local BASE_URL = ContentProvider.BaseUrl +if BASE_URL:find("https://www.") then + BASE_URL = BASE_URL:sub(13) +elseif BASE_URL:find("http://www.") then + BASE_URL = BASE_URL:sub(12) +end + +--In the future, maybe change this to broom setting for Chat URL +local API_URL = "https://api.".. BASE_URL +local CHAT_URL = "https://chat." .. BASE_URL .. "v2/" +local WEB_URL = "https://www." .. BASE_URL +local GAMES_URL = "https://games." .. BASE_URL .. "v1/" + +-- Used when LuaChatUseNewFriendsAndPresenceEndpoint is true +local FRIENDS_URL = string.format("https://friends.%sv1/", BASE_URL) +local PRESENCE_URL = string.format("https://presence.%sv1/", BASE_URL) +local THUMBNAIL_TOKEN_LIMIT = 20 + +WebApi.Status = { + PENDING = 0, + UNKNOWN_ERROR = -1, + NO_CONNECTIVITY = -2, + INVALID_JSON = -3, + BAD_TLS = -4, + MODERATED = -5, + + OK = 200, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + FORBIDDEN = 403, + NOT_FOUND = 404, + REQUEST_TIMEOUT = 408, + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, + BAD_GATEWAY = 502, + SERVICE_UNAVAILABLE = 503, + GATEWAY_TIMEOUT = 504, +} + +local EMPTY_JSON_STRING = "{}" +local GET_FRIENDS_MAX_RETRIES = 4 + +-- Util Funcs + +local function getHttpStatus(response) + for _, code in pairs(WebApi.Status) do + if code >= 100 and response:find(tostring(code)) then + return code + end + end + + if response:find("2%d%d") then + return WebApi.Status.OK + end + + if response:find("curl_easy_perform") and response:find("SSL") then + return WebApi.Status.BAD_TLS + end + + return WebApi.Status.UNKNOWN_ERROR +end + +local function jsonEncode(data) + return HttpService:JSONEncode(data) +end + +local function jsonDecode(data) + return HttpService:JSONDecode(data) +end + +local function debugRandomDelay() + if not Config.General.HttpDelay then + return + end + + local min = Config.General.HttpDelay[1] + local max = Config.General.HttpDelay[2] + + local jitter = math.random(min, max) / 1000 + + wait(jitter) +end + +local function httpGet(url) + debugRandomDelay() + + return game:HttpGetAsync(url) +end + +local function httpPost(url, payload) + debugRandomDelay() + + return game:HttpPostAsync(url, payload, "application/json") +end + +-- TODO SOC-2264 Remove preprocessResponse function once the new performance tracking endpoint is ready +local function preprocessResponse(response) + return response == "" and EMPTY_JSON_STRING or response +end + +local function httpGetJson(url) + local debugRequest = HttpDebug:AddRequest("GET", url) + + local success, response = pcall(httpGet, url) + local status = success and WebApi.Status.OK or getHttpStatus(response) + + HttpDebug:FinishRequest(debugRequest, status, response) + + if success then + response = preprocessResponse(response) + success, response = pcall(jsonDecode, response) + status = success and status or WebApi.Status.INVALID_JSON + end + + return response, status +end + +local function httpPostJson(url, payload) + local debugRequest = HttpDebug:AddRequest("POST", url, payload) + + local success, response = pcall(httpPost, url, payload) + local status = success and WebApi.Status.OK or getHttpStatus(response) + + HttpDebug:FinishRequest(debugRequest, status, response) + + if success then + response = preprocessResponse(response) + success, response = pcall(jsonDecode, response) + status = success and status or WebApi.Status.INVALID_JSON + end + + return response, status +end + +local function subdivideThumbnailTokenArray(thumbnailTokens, tokenLimit) + local someTokens = {} + for i = 1, #thumbnailTokens, tokenLimit do + local subArray = Functional.Take(thumbnailTokens, tokenLimit, i) + table.insert(someTokens, subArray) + end + + return someTokens +end + +--[[ + Create a web request query string to put on the end of a URL given a data + table. + + Arrays are handled, but generally data is expected to be flat. +]] +local function makeQueryString(data) + local params = {} + + for key, value in pairs(data) do + if value ~= nil then --for optional params + if type(value) == "table" then + for i = 1, #value do + table.insert(params, key .. "=" .. value[i]) + end + else + table.insert(params, key .. "=" .. tostring(value)) + end + end + end + + return table.concat(params, "&") +end + +local function extractConvosAndUsers(response) + local conversations = {} + local users = {} + + for _, webConversation in ipairs(response) do + for _, participant in ipairs(webConversation.participants) do + if users[tostring(participant.targetId)] == nil then + local user = User.fromData(participant.targetId, participant.name, false) + users[user.id] = user + end + end + local conversation = Conversation.fromWeb(webConversation) + table.insert(conversations, conversation) + end + + return conversations, users +end + + +local webPresenceMap = { + [0] = User.PresenceType.OFFLINE, + [1] = User.PresenceType.ONLINE, + [2] = User.PresenceType.IN_GAME, + [3] = User.PresenceType.IN_STUDIO +} + +-- Fetch Functions + +function WebApi.MakeUserProfileUrl(userId) + return string.format("%susers/%s/profile", WEB_URL, tostring(userId)) +end + +function WebApi.MakeItemUrl(itemId) + return string.format("%scatalog/%s", WEB_URL, tostring(itemId)) +end + + +function WebApi.MakeReportUserUrl(userId, conversationId) + -- Web is fixing a bug that requires a redirectUrl for this page to work + -- Until then we will use a redirect url + -- once fixed we can switch to: "%sabusereport/embedded/chat?id=%s&actionName=%s&conversationId=%s" + local redirectUrl = string.format("%shome&conversationid=%s#!/", WEB_URL, tostring(conversationId)) + return string.format("%sabusereport/embedded/chat?id=%s&actionName=%s&conversationId=%s&redirecturl=%s", + WEB_URL, tostring(userId), "chat", tostring(conversationId), redirectUrl) +end + +if LuaChatUseNewFriendsAndPresenceEndpoint then + function WebApi.GetUserPresences(userIds) + -- Endpoint documented here: + -- https://presence.roblox.com/docs + + -- Endpoint only accepts number ids, however we have string tokens. + local userIdsToNumber = {} + for _, id in pairs(userIds) do + local idToNumber = tonumber(id) + if idToNumber and idToNumber > 0 then + table.insert(userIdsToNumber, idToNumber) + end + end + + local payload = jsonEncode({ + userIds = userIdsToNumber, + }) + + local requestUrl = string.format("%s/presence/users", + PRESENCE_URL + ) + + local parsed, status = httpPostJson(requestUrl, payload) + + if status ~= WebApi.Status.OK then + return status, nil + end + + local result = {} + for _, presence in ipairs(parsed.userPresences) do + local userPresence = webPresenceMap[presence.userPresenceType] + + result[tostring(presence.userId)] = { + presence = userPresence, + lastLocation = presence.lastLocation, + placeId = presence.placeId and tostring(presence.placeId), + } + end + + return status, result + end + +else + function WebApi.GetUserPresences(userIds) + -- Continue using the old presence endpoint. + -- Endpoint documented here: + -- https://api.roblox.com/docs#Friends + local query = makeQueryString({ + userIds = userIds + }) + + local url = WEB_URL .. "/presence/users?" .. query + + local parsed, status = httpGetJson(url) + + if status ~= WebApi.Status.OK then + return status, nil + end + + local result = {} + for _, presence in ipairs(parsed) do + local userPresence = webPresenceMap[presence.UserPresenceType] + + result[tostring(presence.UserId)] = { + presence = userPresence, + lastLocation = presence.LastLocation, + placeId = presence.PlaceId and tostring(presence.PlaceId), + } + end + + return status, result + end +end + +function WebApi.GetFriendCount() + --Endpoint documented here: + --https://api.roblox.com/docs#Friends + local query = makeQueryString({ + userId = Players.LocalPlayer.UserId, + }) + local url = API_URL .. "/user/get-friendship-count?" .. query + + local parsed, status = httpGetJson(url) + + if status ~= WebApi.Status.OK then + return status, 0 + end + + return status, parsed.count +end + + + +if LuaChatUseNewFriendsAndPresenceEndpoint then + function WebApi.GetFriends(targetUserId) + -- Endpoint documented here: + -- https://friends.roblox.com/docs + + local url = string.format("%susers/%s/friends", + FRIENDS_URL, targetUserId + ) + + local success, parsed = pcall(function() + return httpGetJson(url) + end) + + local result = {} + for _, user in ipairs(parsed.data) do + local userId = tostring(user.id) + local userFromData = User.fromData(userId, user.name, true) + result[userId] = userFromData + end + + return success and WebApi.Status.OK or WebApi.Status.UNKNOWN_ERROR, result + end +else + function WebApi.GetFriends(page) + --Endpoint documented here: + --https://api.roblox.com/docs#Friends + local query = makeQueryString({ + page = page, + }) + local url = API_URL .. "/users/" .. tostring(Players.LocalPlayer.UserId) .. "/friends?" .. query + + local status = nil + local parsed = nil + local retryCount = 0 + local waitTime = 1 + while status ~= WebApi.Status.OK do + if retryCount >= GET_FRIENDS_MAX_RETRIES then + return status, nil + end + + if retryCount > 0 then + wait(waitTime) + end + + parsed, status = httpGetJson(url) + waitTime = waitTime * 2 + retryCount = retryCount + 1 + end + + local result = {} + for _, user in ipairs(parsed) do + local userId = tostring(user.Id) + local userFromData = User.fromData(userId, user.Username, true) + result[userId] = userFromData + end + + return status, result + end +end + +function WebApi.GetUser(userId) + local url = API_URL .. "/users/" .. tostring(userId) + local parsed, status = httpGetJson(url) + + return status, parsed +end + +function WebApi.GetUserConversations(pageNumber, pageSize) + pageNumber = pageNumber or 1 + pageSize = pageSize or Constants.PageSize.GET_CONVERSATIONS + + local queryString = makeQueryString({ + pageNumber = pageNumber, + pageSize = pageSize + }) + local requestUrl = CHAT_URL .. "get-user-conversations?" .. queryString + + local response, status = httpGetJson(requestUrl) + + if status ~= WebApi.Status.OK then + return status, nil + end + + local conversations, users = extractConvosAndUsers(response, status) + + local result = { + conversations = conversations, + users = users + } + + return status, result +end + +--[[ + Takes a table of conversation IDs and gets the latest messages for these + conversations + + TODO: handle paging +]] +function WebApi.GetLatestMessages(conversationIds) + local queryString = makeQueryString({ + conversationIds = conversationIds, + pageSize = 1, + }) + local requestUrl = CHAT_URL .. "multi-get-latest-messages?" .. queryString + + local response, status = httpGetJson(requestUrl) + + if status ~= WebApi.Status.OK then + return status, nil + end + + local messages = {} + if status == WebApi.Status.OK then + for _, webConversation in ipairs(response) do + local lastMessage = webConversation.chatMessages[1] + + if lastMessage ~= nil then + local message = Message.fromWeb(lastMessage, tostring(webConversation.conversationId)) + table.insert(messages, message) + end + end + end + + return status, messages +end + +function WebApi.GetMessages(convoId, pageSize, exclusiveStartMessageId) + local queryString = makeQueryString({ + conversationId = convoId, + pageSize = pageSize, + exclusiveStartMessageId = exclusiveStartMessageId, + }) + local requestUrl = CHAT_URL .. "get-messages?" .. queryString + + local response, status = httpGetJson(requestUrl) + + if status ~= WebApi.Status.OK then + return status, nil + end + + --Assumption: Messages received are temporally contiguous + --and ordered from most recent to least recent + local previousMessageId = nil + local messages = Functional.MapReverse(response, function(web) + local message = Message.fromWeb(web, convoId, previousMessageId) + previousMessageId = message.id + return message + end) + return status, messages +end + +function WebApi.GetConversations(convoIds) + local queryString = makeQueryString({ + conversationIds = convoIds, + }) + local requestUrl = CHAT_URL .. "get-conversations?" .. queryString + + local response, status = httpGetJson(requestUrl) + + if status ~= WebApi.Status.OK then + return status, nil + end + + local conversations, users = extractConvosAndUsers(response) + + local result = { + conversations = conversations, + users = users, + } + + return status, result +end + +function WebApi.RemoveUserFromConversation(userId, convoId) + local payload = jsonEncode({ + participantUserId = userId, + conversationId = convoId, + }) + local requestUrl = CHAT_URL .. "remove-from-conversation" + local _, status = httpPostJson(requestUrl, payload) + + return status +end + +function WebApi.RenameGroupConversation(convoId, newTitle) + local payload = jsonEncode({ + conversationId = convoId, + newTitle = newTitle, + }) + local requestUrl = CHAT_URL .. "rename-group-conversation" + local response, status = httpPostJson(requestUrl, payload) + + if status ~= WebApi.Status.OK then + return status + end + + if response.resultType ~= "Success" then + warn("Message was not sent successfully.") + return WebApi.Status.MODERATED + end + + return status +end + +function WebApi.SendMessage(conversationId, messageText, previousMessageId) + local payload = jsonEncode({ + conversationId = conversationId, + message = messageText + }) + local requestUrl = CHAT_URL .. "send-message" + local response, status = httpPostJson(requestUrl, payload) + + if status ~= WebApi.Status.OK then + return status, response + end + + if response.resultType ~= "Success" then + warn("Message was not sent successfully.") + return WebApi.Status.MODERATED, nil + end + + return status, Message.fromSentWeb(response, conversationId, previousMessageId) +end + +function WebApi.StartGroupConversation(conversation) + local participantUserIds = Functional.Map(conversation.participants, function(value) + return tonumber(value) + end) + local payload = jsonEncode({ + participantUserIds = participantUserIds, + title = conversation.title, + }) + local requestUrl = CHAT_URL .. "start-group-conversation" + local response, status = httpPostJson(requestUrl, payload) + + if status == WebApi.Status.OK then + if not response.resultType == "Success" then + status = WebApi.Status.UNKNOWN_ERROR + else + local conversation = Conversation.fromWeb(response.conversation, conversation.clientId) + return status, conversation + end + end + + return status, nil +end + +function WebApi.StartOneToOneConversation(userId, clientId) + local payload = jsonEncode({ + participantuserId = userId, + }) + + local requestUrl = CHAT_URL .. "start-one-to-one-conversation" + local response, status = httpPostJson(requestUrl, payload) + + if status == WebApi.Status.OK then + if not response.resultType == "Success" then + warn("Server returned error:" .. response) + status = WebApi.Status.UNKNOWN_ERROR + else + local conversation = Conversation.fromWeb(response.conversation, clientId) + return status, conversation + end + end + + return status, nil +end + +function WebApi.PostTypingStatus(conversationId, isTyping) + local payload = jsonEncode({ + conversationId = conversationId, + isTyping = isTyping, + }) + local requestUrl = CHAT_URL .. "update-user-typing-status" + local _, status = httpPostJson(requestUrl, payload) + + return status +end + +function WebApi.GetUnreadConversationCount() + + local requestUrl = CHAT_URL .. "get-unread-conversation-count" + local result, status = httpGetJson(requestUrl) + local count = tonumber(result.count) + return status, count +end + +function WebApi.MarkAsRead(conversationId, endMessageId) + local payload = jsonEncode({ + conversationId = conversationId, + endMessageId = endMessageId, + }) + local requestUrl = CHAT_URL .. "mark-as-read" + local _, status = httpPostJson(requestUrl, payload) + return status +end + +function WebApi.AddUsersToConversation(convoId, participants) + local payload = jsonEncode({ + participantUserIds = participants, + conversationId = convoId, + }) + local requestUrl = CHAT_URL .. "add-to-conversation" + local response, status = httpPostJson(requestUrl, payload) + + if status == WebApi.Status.OK then + if not response.resultType == "Success" then + status = WebApi.Status.UNKNOWN_ERROR + end + end + + return status +end + +function WebApi.GetChatSettings() + local requestUrl = CHAT_URL .. "chat-settings" + local response, status = httpGetJson(requestUrl) + + return status, response +end + +function WebApi.GetMultiplePlaceInfos(placeIds) + local payload = makeQueryString({ + placeIds = placeIds + }) + local requestUrl = GAMES_URL .. "games/multiget-place-details?" .. payload + local parsed, status = httpGetJson(requestUrl) + + if status ~= WebApi.Status.OK then + return status, nil + end + + return status, parsed +end + +function WebApi.GetPlaceThumbnail(imageToken, width, height) + local payload = makeQueryString({ + imageTokens = {imageToken}, + width = width, + height = height + }) + local requestUrl = GAMES_URL .. "games/game-thumbnails?" .. payload + + local parsed, status = httpGetJson(requestUrl) + + if status ~= WebApi.Status.OK then + return status, nil + end + + return status, parsed +end + +function WebApi.GetGameIcon(gameId) + local payload = makeQueryString({ + placeId = gameId + }) + local requestUrl = WEB_URL .. "places/icons/json?" .. payload + local info, status = httpGetJson(requestUrl) + + local assetid = status == WebApi.Status.OK and "rbxassetid://" .. info.ImageId or "" + + return status, assetid +end + +function WebApi.PinGame(conversationId, universeId) + local payload = jsonEncode({ + conversationId = conversationId, + universeId = universeId + }) + local requestUrl = CHAT_URL .. "set-conversation-universe" + local response, status = httpPostJson(requestUrl, payload) + + if status ~= WebApi.Status.OK then + return status, response + end + + return status +end + +function WebApi.UnpinGame(conversationId) + local payload = jsonEncode({ + conversationId = conversationId, + }) + local requestUrl = CHAT_URL .. "reset-conversation-universe" + local response, status = httpPostJson(requestUrl, payload) + + if status ~= WebApi.Status.OK then + return status, response + end + + return status +end + +function WebApi.GetGamesSorts(gamesSorts) + local requestUrl = GAMES_URL .. "games/sorts?model.gameSortsContext=" .. gamesSorts + local response, status = httpGetJson(requestUrl) + + if status ~= WebApi.Status.OK then + warn("Server returned error:" .. response) + return nil + end + + return response.sorts +end + +function WebApi.GetMostRecentlyPlayedGames(myRecentGameToken) + local requestUrl = GAMES_URL .. "games/list?model.sortToken=" .. myRecentGameToken + local response, status = httpGetJson(requestUrl) + + if status ~= WebApi.Status.OK then + warn("Server returned error:" .. response) + return nil + end + + return response.games +end + +function WebApi.GetPlacesThumbnails(imageTokens, width, height) + local splitTokens = subdivideThumbnailTokenArray(imageTokens, THUMBNAIL_TOKEN_LIMIT) + local thumbnails = {} + + for _, tokens in ipairs(splitTokens) do + local payload = makeQueryString({ + imageTokens = tokens, + width = width, + height = height + }) + + local requestUrl = GAMES_URL .. "games/game-thumbnails?" .. payload + local parsed, status = httpGetJson(requestUrl) + + if status ~= WebApi.Status.OK then + warn("Server returned error:" .. parsed) + return thumbnails + end + + for _, image in pairs(parsed) do + local imageIndex = tostring(image.placeId) + thumbnails[imageIndex] = image + end + end + + return thumbnails +end + +function WebApi.GetGamesInSortByToken(token) + local requestUrl = GAMES_URL .. "games/list?model.sortToken=" .. token + local response, status = httpGetJson(requestUrl) + + if status ~= WebApi.Status.OK then + warn("Server returned error:" .. response) + return nil + end + + return response.games +end + +function WebApi.ReportToDiagByCountryCode(featureName, measureName, seconds) + local payload = jsonEncode({ + featureName = featureName, + measureName = measureName, + value = seconds * 1000, + }) + + local requestUrl = WEB_URL .. "performance/send-measurement" + local response, status = httpPostJson(requestUrl, payload) + + return status +end + +return WebApi \ No newline at end of file diff --git a/Client2018/content/internal/Common/.luacheckrc b/Client2018/content/internal/Common/.luacheckrc new file mode 100644 index 0000000..9847379 --- /dev/null +++ b/Client2018/content/internal/Common/.luacheckrc @@ -0,0 +1,46 @@ +stds.roblox = { + globals = { + "game" + }, + read_globals = { + -- Roblox globals + "script", + "utf8", + + -- Extra functions + "tick", "warn", "spawn", + "wait", "settings", "typeof", + + -- Types + "Vector2", "Vector3", + "Color3", + "UDim", "UDim2", + "Rect", + "CFrame", + "Enum", + "Instance", + } +} + +stds.testez = { + read_globals = { + "describe", + "it", "itFOCUS", "itSKIP", + "FOCUS", "SKIP", "HACK_NO_XPCALL", + "expect", + } +} + +ignore = { + "212", -- unused arguments + "421", -- shadowing local variable + "422", -- shadowing argument + "431", -- shadowing upvalue + "432", -- shadowing upvalue argument +} + +std = "lua51+roblox" + +files["**/*.spec.lua"] = { + std = "+testez", +} diff --git a/Client2018/content/internal/Common/Modules/Common/Action.lua b/Client2018/content/internal/Common/Modules/Common/Action.lua new file mode 100644 index 0000000..d3d1ff4 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Action.lua @@ -0,0 +1,68 @@ +--[[ + A helper function to define a Rodux action creator with an associated name. + + Normally when creating a Rodux action, you can just create a function: + + return function(value) + return { + type = "MyAction", + value = value, + } + end + + And then when you check for it in your reducer, you either use a constant, + or type out the string name: + + if action.type == "MyAction" then + -- change some state + end + + Typos here are a remarkably common bug. We also have the issue that there's + no link between reducers and the actions that they respond to! + + `Action` (this helper) provides a utility that makes this a bit cleaner. + + Instead, define your Rodux action like this: + + return Action("MyAction", function(value) + return { + value = value, + } + end) + + We no longer need to add the `type` field manually. + + Additionally, the returned action creator now has a 'name' property that can + be checked by your reducer: + + local MyAction = require(Reducers.MyAction) + + ... + + if action.type == MyAction.name then + -- change some state! + end + + Now we have a clear link between our reducers and the actions they use, and + if we ever typo a name, we'll get a warning in LuaCheck as well as an error + at runtime! +]] + +return function(name, fn) + assert(type(name) == "string", "A name must be provided to create an Action") + assert(type(fn) == "function", "A function must be provided to create an Action") + + return setmetatable({ + name = name, + }, { + __call = function(self, ...) + local result = fn(...) + + assert(type(result) == "table", "An action must return a table") + + result.type = name + + return result + end + }) +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/Action.spec.lua b/Client2018/content/internal/Common/Modules/Common/Action.spec.lua new file mode 100644 index 0000000..799c2b3 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Action.spec.lua @@ -0,0 +1,85 @@ +return function() + local Action = require(script.Parent.Action) + + it("should return a table", function() + local action = Action("foo", function() + return {} + end) + + expect(action).to.be.a("table") + end) + + it("should set the name of the action", function() + local action = Action("foo", function() + return {} + end) + + expect(action.name).to.equal("foo") + end) + + it("should be able to be called as a function", function() + local action = Action("foo", function() + return {} + end) + + expect(action).never.to.throw() + end) + + it("should return a table when called as a function", function() + local action = Action("foo", function() + return {} + end) + + expect(action()).to.be.a("table") + end) + + it("should set the type of the action", function() + local action = Action("foo", function() + return {} + end) + + expect(action().type).to.equal("foo") + end) + + it("should set values", function() + local action = Action("foo", function(value) + return { + value = value + } + end) + + expect(action(100).value).to.equal(100) + end) + + it("should throw when passed a function", function() + local action = Action("foo", function() + return function() end + end) + + expect(action).to.throw() + end) + + it("should throw with a invalid name", function() + expect(function() + Action(nil, function() + return {} + end) + end).to.throw() + + expect(function() + Action(100, function() + return {} + end) + end).to.throw() + end) + + it("should throw when passed a invalid function", function() + expect(function() + Action("foo", nil) + end).to.throw() + + expect(function() + Action("foo", {}) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/Analytics.lua b/Client2018/content/internal/Common/Modules/Common/Analytics.lua new file mode 100644 index 0000000..fb69cfa --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Analytics.lua @@ -0,0 +1,57 @@ +--[[ + A centralized hub for basic metrics reporting. + This class is designed to provide a baseline exposure to the reporters. + + Context specific Analytics.lua objects should be created in sub-projects, + to report specific actions like chat interactions, or game page interactions. + + Analytics.lua and the reporters here in Common should serve to cover the + most common interactions. +]] + +local AnalyticsService = game:GetService("AnalyticsService") +local CoreGui = game:GetService("CoreGui") +local Reporters = CoreGui.RobloxGui.Modules.Common.AnalyticsReporters + +local DiagReporter = require(Reporters.Diag) +local EventStreamReporter = require(Reporters.EventStream) +local GoogleAnalyticsReporter = require(Reporters.GoogleAnalytics) +local InfluxDbReporter = require(Reporters.Influx) + + +local Analytics = {} +Analytics.__index = Analytics + +-- reportingService : (Service, optional) an object that exposes the same functions as AnalyticsService +function Analytics.new(reportingService) + if not reportingService then + reportingService = AnalyticsService + end + + -- All public reporting functions are exposed by the objects defined in the properties + local self = {} + self.Diag = DiagReporter.new(reportingService) + self.EventStream = EventStreamReporter.new(reportingService) + self.GoogleAnalytics = GoogleAnalyticsReporter.new(reportingService) + self.InfluxDb = InfluxDbReporter.new(reportingService) + + setmetatable(self, Analytics) + + return self +end + +function Analytics.mock() + -- create a reporting service that does not fire any requests out to the world + local fakeReportingService = {} + function fakeReportingService.ReportCounter() end + function fakeReportingService.ReportInfluxSeries() end + function fakeReportingService.ReportStats() end + function fakeReportingService.SetRBXEvent() end + function fakeReportingService.SetRBXEventStream() end + function fakeReportingService.TrackEvent() end + function fakeReportingService.UpdateHeartbeatObject() end + + return Analytics.new(fakeReportingService) +end + +return Analytics diff --git a/Client2018/content/internal/Common/Modules/Common/Analytics.spec.lua b/Client2018/content/internal/Common/Modules/Common/Analytics.spec.lua new file mode 100644 index 0000000..794b160 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Analytics.spec.lua @@ -0,0 +1,69 @@ +return function() + local Analytics = require(script.Parent.Analytics) + + describe("new()", function() + it("should properly construct a new object", function() + local na = Analytics.new() + expect(na).to.be.ok() + end) + + it("should accept a custom reporting service", function() + local fakeService = {} + local na = Analytics.new(fakeService) + expect(na).to.be.ok() + end) + + it("should have a reporter specifically for Diag", function() + local na = Analytics.new() + expect(na.Diag).to.be.ok() + end) + + it("should have a reporter specifically for RBXEventStream", function() + local na = Analytics.new() + expect(na.EventStream).to.be.ok() + end) + + it("should have a reporter specifically for Google Analytics", function() + local na = Analytics.new() + expect(na.GoogleAnalytics).to.be.ok() + end) + + it("should have a reporter specifically for Influx", function() + local na = Analytics.new() + expect(na.InfluxDb).to.be.ok() + end) + end) + + describe("mock()", function() + it("should properly construct a new object", function() + local ma = Analytics.mock() + expect(ma).to.be.ok() + expect(ma.Diag).to.be.ok() + expect(ma.EventStream).to.be.ok() + expect(ma.GoogleAnalytics).to.be.ok() + expect(ma.InfluxDb).to.be.ok() + end) + + it("should succeed for all function calls in Diag", function() + local ma = Analytics.mock() + ma.Diag:reportCounter("fakeCounter", 1) + ma.Diag:reportStats("fakeCategory", 1) + end) + + it("should succeed for all function call in EventStream", function() + local ma = Analytics.mock() + ma.EventStream:setRBXEvent("fakeContext", "fakeEventName") + ma.EventStream:setRBXEventStream("fakeContext", "fakeEventName") + end) + + it("should succeed for all function call in GoogleAnalytics", function() + local ma = Analytics.mock() + ma.GoogleAnalytics:trackEvent("fakeCategory", "fakeAction", "fakeLabel") + end) + + it("should succeed for all function call in Influx", function() + local ma = Analytics.mock() + ma.InfluxDb:reportSeries("fakeSeries", {}, 1) + end) + end) +end diff --git a/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/Diag.lua b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/Diag.lua new file mode 100644 index 0000000..b33b490 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/Diag.lua @@ -0,0 +1,64 @@ +--[[ + Specialized analytics reporter for ephemeral counters. + Useful for tracking at-a-glance health of a feature. +]] + +local UserInputService = game:GetService("UserInputService") + +local Diag = {} +Diag.__index = Diag + +-- reportingService - (object) any object that defines the same functions for Diag as AnalyticsService +function Diag.new(reportingService) + local rsType = type(reportingService) + assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService") + + local self = { + _reporter = reportingService, + _isEnabled = true, + } + setmetatable(self, Diag) + + return self +end + +-- isEnabled : (boolean) +function Diag:setEnabled(isEnabled) + assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean") + self._isEnabled = isEnabled +end + +-- counterName : (string) the name of the ephemeral counter to increment +-- amount : (int) the value to increment the counter by +function Diag:reportCounter(counterName, amount) + assert(type(counterName) == "string", "Expected counterName to be a string") + assert(type(amount) == "number", "Expected amount to be a number") + assert(self._isEnabled, "This reporting service is disabled") + + -- use special naming convention for Xbox counters + -- the call to GetPlatform is wrapped in a pcall() because the Testing Service + -- executes the scripts in the wrong authorization level + local platformID = Enum.Platform.None + pcall(function() + platformID = UserInputService:GetPlatform() + end) + if platformID == Enum.Platform.XBoxOne then + counterName = "Xbox-" .. tostring(counterName) + end + + -- the AnalyticsService should automatically handle batch reporting + self._reporter:ReportCounter(counterName, amount) +end + + +-- category : (string) the name of the statistics buffer which to append the data +-- value : (number) any type of numeric value +function Diag:reportStats(category, value) + assert(type(category) == "string", "Expected category to be a string") + assert(type(value) == "number", "Expected value to be a number") + assert(self._isEnabled, "This reporting service is disabled") + + self._reporter:ReportStats(category, value) +end + +return Diag diff --git a/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/Diag.spec.lua b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/Diag.spec.lua new file mode 100644 index 0000000..6dadff2 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/Diag.spec.lua @@ -0,0 +1,176 @@ +return function() + local Diag = require(script.Parent.Diag) + + local testCounterName = "testCounter" + local testCounterAmount = 1 + local testCategoryName = "testCategory" + local testCategoryValue = 98 + + local badTestCounterName = 5 + local badTestCounterAmount = "hello" + local badTestCategoryName = {} + local badTestCategoryValue = {} + + local DebugReportingService = {} + function DebugReportingService:ReportCounter(counterName, amount) + assert(counterName == testCounterName, "Unexpected value for counterName: " .. counterName) + assert(amount == testCounterAmount, "Unexpected value for amount: " .. amount) + end + function DebugReportingService:ReportStats(categoryName, value) + assert(categoryName == testCategoryName, "Unexpected value for category: " .. categoryName) + if value then + assert(value == testCategoryValue, "Unexpected value for value: " .. value) + end + end + + + describe("new()", function() + it("should construct with a Reporting Service", function() + local diag = Diag.new(DebugReportingService) + expect(diag).to.be.ok() + end) + + it("should throw an error to be constructed without a Reporting Service", function() + expect(function() + Diag.new(nil) + end).to.throw() + end) + end) + + describe("setEnabled()", function() + it("should succeed with valid input", function() + local diag = Diag.new(DebugReportingService) + diag:setEnabled(false) + diag:setEnabled(true) + end) + it("should disable the reporter", function() + local diag = Diag.new(DebugReportingService) + diag:setEnabled(false) + expect(function() + diag:reportCounter(testCounterName, testCounterAmount) + end).to.throw() + end) + end) + + describe("reportCounter()", function() + it("should work when appropriately enabled / disabled", function() + local diag = Diag.new(DebugReportingService) + + expect(function() + diag:setEnabled(false) + diag:reportCounter(testCounterName, testCounterAmount) + end).to.throw() + + diag:setEnabled(true) + diag:reportCounter(testCounterName, testCounterAmount) + end) + + it("should succeed with valid input", function() + local diag = Diag.new(DebugReportingService) + diag:reportCounter(testCounterName, testCounterAmount) + end) + + it("should throw an error with invalid input for the counter name", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(badTestCounterName, testCounterAmount) + end).to.throw() + end) + + it("should throw an error with invalid input for the amount", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(testCounterName, badTestCounterAmount) + end).to.throw() + end) + + it("should throw an error with completely invalid input", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(badTestCounterName, badTestCounterAmount) + end).to.throw() + end) + + it("should throw an error if it is missing a counter name", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(nil, testCounterAmount) + end).to.throw() + end) + + it("should throw an error if it is missing an amount", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(testCounterName, nil) + end).to.throw() + end) + + it("should throw an error if it is missing any input", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(nil, nil) + end).to.throw() + end) + end) + + describe("reportStats()", function() + it("should work when appropriately enabled / disabled", function() + local diag = Diag.new(DebugReportingService) + + expect(function() + diag:setEnabled(false) + diag:reportStats(testCategoryName, testCategoryValue) + end).to.throw() + + diag:setEnabled(true) + diag:reportStats(testCategoryName, testCategoryValue) + end) + + it("should succeed with valid input", function() + local diag = Diag.new(DebugReportingService) + diag:reportStats(testCategoryName, testCategoryValue) + end) + + it("should throw an error with invalid input for the category name", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(badTestCategoryName, testCategoryValue) + end).to.throw() + end) + + it("should throw an error with invalid input for the value", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(testCategoryName, badTestCategoryValue) + end).to.throw() + end) + + it("should throw an error with completely invalid input", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(badTestCategoryName, badTestCategoryValue) + end).to.throw() + end) + + it("should throw an error if it is missing a category name", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(nil, testCategoryValue) + end).to.throw() + end) + + it("should throw an error if it is missing a value", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(testCategoryName, nil) + end).to.throw() + end) + + it("should throw an error if it is missing any input", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(nil, nil) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/EventStream.lua b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/EventStream.lua new file mode 100644 index 0000000..68539ef --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/EventStream.lua @@ -0,0 +1,120 @@ +--[[ + Specialized reporter for RBX Event Ingest data. + Useful for tracking explicit user interactions with screens and guis. +]] + +local UserInputService = game:GetService("UserInputService") + +local function getPlatformTarget() + local platformTarget = "unknownLua" + local platformEnum = Enum.Platform.None + + -- the call to GetPlatform is wrapped in a pcall() because the Testing Service + -- executes the scripts in the wrong authorization level + pcall(function() + platformEnum = UserInputService:GetPlatform() + end) + + -- bucket the platform based on consumer platform + local isDesktopClient = (platformEnum == Enum.Platform.Windows) or (platformEnum == Enum.Platform.OSX) + + local isMobileClient = (platformEnum == Enum.Platform.IOS) or (platformEnum == Enum.Platform.Android) + isMobileClient = isMobileClient or (platformEnum == Enum.Platform.UWP) + + local isConsole = (platformEnum == Enum.Platform.XBox360) or (platformEnum == Enum.Platform.XBoxOne) + isConsole = isConsole or (platformEnum == Enum.Platform.PS3) or (platformEnum == Enum.Platform.PS4) + isConsole = isConsole or (platformEnum == Enum.Platform.WiiU) + + -- assign a target based on the form factor + if isDesktopClient then + platformTarget = "client" + elseif isMobileClient then + platformTarget = "mobile" + elseif isConsole then + platformTarget = "console" + else + -- if we don't have a name for the form factor, report it here so that we can eventually track it down + platformTarget = platformTarget .. tostring(platformEnum) + end + + + return platformTarget +end + + +local EventStream = {} +EventStream.__index = EventStream + +-- reportingService - (object) any object that defines the same functions for Event Stream as AnalyticsService +function EventStream.new(reportingService) + local rsType = type(reportingService) + assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService") + + local self = { + _reporter = reportingService, + _isEnabled = true, + } + setmetatable(self, EventStream) + + return self +end + +-- isEnabled : (boolean) +function EventStream:setEnabled(isEnabled) + assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean") + self._isEnabled = isEnabled +end + +-- eventContext : (string) the location or context in which the event is occurring. +-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example. +-- additionalArgs : (optional, map) table for additional information to appear in the event stream. +function EventStream:setRBXEvent(eventContext, eventName, additionalArgs) + local target = getPlatformTarget() + additionalArgs = additionalArgs or {} + + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(eventName) == "string", "Expected eventName to be a string") + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(self._isEnabled, "This reporting service is disabled") + + -- This function fires reports to the server right away + self._reporter:SetRBXEvent(target, eventContext, eventName, additionalArgs) +end + + +-- eventContext : (string) the location or context in which the event is occurring. +-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example. +-- additionalArgs : (optional, map) map for extra keys to appear in the event stream. +function EventStream:setRBXEventStream(eventContext, eventName, additionalArgs) + local target = getPlatformTarget() + additionalArgs = additionalArgs or {} + + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(eventName) == "string", "Expected eventName to be a string") + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(self._isEnabled, "This reporting service is disabled") + + -- this function sends reports to the server in batches, not real-time + self._reporter:SetRBXEventStream(target, eventContext, eventName, additionalArgs) +end + + +function EventStream:releaseRBXEventStream() + assert(self._isEnabled, "This reporting service is disabled") + + self._reporter:ReleaseRBXEventStream(getPlatformTarget()) +end + + +-- additionalArgs : (optional, map) table for extra keys to appear in the event stream. +function EventStream:updateHeartbeatObject(additionalArgs) + additionalArgs = additionalArgs or {} + + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(self._isEnabled, "This reporting service is disabled") + + self._reporter:UpdateHeartbeatObject(additionalArgs) +end + + +return EventStream diff --git a/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/EventStream.spec.lua b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/EventStream.spec.lua new file mode 100644 index 0000000..e9b4765 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/EventStream.spec.lua @@ -0,0 +1,269 @@ +return function() + + local EventStream = require(script.Parent.EventStream) + + local testArgs = { + testKey = "testValue" + } + local testContext = "testContext" + local testEvent = "testEventName" + local badTestArgs = "hello" + local badTestContext = {} + local badTestEvent = {} + + + local function isTableEqual(table1, table2) + if table1 == table2 then + return true + end + + if type(table1) ~= "table" then + return false + end + + if type(table2) ~= "table" then + return false + end + + for key, _ in pairs(table1) do + if table1[key] ~= table2[key] then + return false + end + end + for key, _ in pairs(table2) do + if table2[key] ~= table1[key] then + return false + end + end + + return true + end + + local function createDebugReportingService() + local function validateInputs(eventTarget, eventContext, eventName, additionalArgs) + assert(eventTarget, "no value found for eventTarget") + assert(eventContext == testContext, "unexpected value for eventContext : " .. eventContext) + assert(eventName == testEvent, "unexpected value for eventName : " .. eventName) + if additionalArgs and not isTableEqual(additionalArgs, {}) then + assert(isTableEqual(additionalArgs, testArgs), "unexpected value for additionalArgs") + end + end + + local DebugReportingService = {} + function DebugReportingService:SetRBXEvent(eventTarget, eventContext, eventName, additionalArgs) + validateInputs(eventTarget, eventContext, eventName, additionalArgs) + end + function DebugReportingService:SetRBXEventStream(eventTarget, eventContext, eventName, additionalArgs) + validateInputs(eventTarget, eventContext, eventName, additionalArgs) + end + function DebugReportingService:ReleaseRBXEventStream(eventTarget) + assert(eventTarget, "no value found for eventTarget") + end + function DebugReportingService:UpdateHeartbeatObject(additionalArgs) + if additionalArgs and not isTableEqual(additionalArgs, {}) then + assert(isTableEqual(additionalArgs, testArgs), "unexpected value for additionalArgs") + end + end + + return DebugReportingService + end + + + describe("new()", function() + it("should construct with a Reporting Service", function() + local es = EventStream.new(createDebugReportingService()) + expect(es).to.be.ok() + end) + + it("should not allow construction without a Reporting Service", function() + expect(function() + EventStream.new(nil) + end).to.throw() + end) + end) + + describe("setEnabled()", function() + it("should succeed with valid input", function() + local reporter = EventStream.new(createDebugReportingService()) + reporter:setEnabled(false) + reporter:setEnabled(true) + end) + it("should disable the reporter", function() + local reporter = EventStream.new(createDebugReportingService()) + reporter:setEnabled(false) + expect(function() + reporter:updateHeartbeatObject() + end).to.throw() + end) + end) + + describe("setRBXEvent()", function() + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:setRBXEvent(testContext, testEvent, testArgs) + end) + + it("should work when appropriately enabled / disabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:setRBXEvent(testContext, testEvent, testArgs) + end).to.throw() + + es:setEnabled(true) + es:setRBXEvent(testContext, testEvent, testArgs) + end) + + it("should throw an error if it is missing a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEvent(nil, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is missing an event name", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEvent(testContext, nil, testArgs) + end).to.throw() + end) + + it("should succeed even if there aren't any additional args", function() + local es = EventStream.new(createDebugReportingService()) + es:setRBXEvent(testContext, testEvent, nil) + end) + + it("should throw an error if it is given bad input for a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEvent(badTestContext, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for a event", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEvent(testContext, badTestEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for additionalArgs", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEvent(testContext, testEvent, badTestArgs) + end).to.throw() + end) + end) + + describe("setRBXEventStream()", function() + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:setRBXEventStream(testContext, testEvent, testArgs) + end) + + it("should work when appropriately enabled / disabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:setRBXEventStream(testContext, testEvent, testArgs) + end).to.throw() + + es:setEnabled(true) + es:setRBXEventStream(testContext, testEvent, testArgs) + end) + + it("should throw an error if it is missing a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEventStream(nil, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is missing an event name", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEventStream(testContext, nil, testArgs) + end).to.throw() + end) + + it("should succeed even if there aren't any additional args", function() + local es = EventStream.new(createDebugReportingService()) + es:setRBXEventStream(testContext, testEvent, nil) + end) + + it("should throw an error if it is given bad input for a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEventStream(badTestContext, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for a event", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEventStream(testContext, badTestEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for additionalArgs", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEventStream(testContext, testEvent, badTestArgs) + end).to.throw() + end) + end) + + describe("releaseRBXEventStream()", function() + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:releaseRBXEventStream() + end) + + it("should throw when disabled and succeed when enabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:releaseRBXEventStream() + end).to.throw() + + es:setEnabled(true) + es:releaseRBXEventStream() + end) + end) + + describe("updateHeartbeatObject()", function() + it("should work when appropriately enabled / disabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:updateHeartbeatObject(testArgs) + end).to.throw() + + expect(function() + es:setEnabled(true) + es:updateHeartbeatObject(testArgs) + end).never.to.throw() + end) + + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:updateHeartbeatObject(testArgs) + end) + + it("should succeed even if there aren't any additional args", function() + local es = EventStream.new(createDebugReportingService()) + es:updateHeartbeatObject(nil) + end) + + it("should throw an error with invalid input", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:updateHeartbeatObject(badTestArgs) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/GoogleAnalytics.lua b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/GoogleAnalytics.lua new file mode 100644 index 0000000..91e979c --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/GoogleAnalytics.lua @@ -0,0 +1,51 @@ +--[[ + Specialized reporter for sending data to GA. + Useful for creating a breadcrumb trail of user interactions. + + Events in GA are aggregated and organized in order by category, action, label. +]] + +local GoogleAnalytics = {} +GoogleAnalytics.__index = GoogleAnalytics + +-- reportingService : (table or userdata) any object that defines the same functions for GA as AnalyticsService +function GoogleAnalytics.new(reportingService) + local rsType = type(reportingService) + assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService") + + local self = { + _reporter = reportingService, + _isEnabled = true, + } + setmetatable(self, GoogleAnalytics) + + return self +end + +-- isEnabled : (boolean) +function GoogleAnalytics:setEnabled(isEnabled) + assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean") + self._isEnabled = isEnabled +end + +-- category : (string) the most generic category by which to organize data, ex) LuaApp, Errors, GameSettings, etc. +-- action : (string) a specific event to record, ex) ButtonPressed, GameExit +-- label : (string, optional) a detail to differentiate one action over another, ex) LoginButton, Exit Code 0 +-- value : (integer, optional) the number of times this event has occurred +function GoogleAnalytics:trackEvent(category, action, label, value) + assert(type(category) == "string", "Expected category to be a string") + assert(type(action) == "string", "Expected action to be a string") + if label then + assert(type(label) == "string", "Expected label to be a string") + end + if value then + assert(type(value) == "number", "Expected value to be a number") + assert(value >= 0, "Expected value must not be a negative value") + end + assert(self._isEnabled, "This reporting service is disabled") + + self._reporter:TrackEvent(category, action, label, value) +end + + +return GoogleAnalytics diff --git a/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/GoogleAnalytics.spec.lua b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/GoogleAnalytics.spec.lua new file mode 100644 index 0000000..ede7486 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/GoogleAnalytics.spec.lua @@ -0,0 +1,140 @@ +return function() + local GoogleAnalytics = require(script.Parent.GoogleAnalytics) + + local testCategory = "testCategory" + local testAction = "testAction" + local testLabel = "testLabel" + local testValue = 6 + local badTestCategory = 13141 + local badTestAction = {} + local badTestLabel = {} + local badTestValue = "heyo" + + + local DebugReportingService = {} + function DebugReportingService:TrackEvent(category, action, label, value) + if category ~= testCategory then + error("unexpected value for category: " .. category) + end + if action ~= testAction then + error("unexpected value for action: " .. action) + end + if label then + if label ~= testLabel then + error("unexpected value for label: " .. label) + end + end + if value then + if value ~= testValue then + error("unexpected value for value: " .. value) + end + end + end + + + describe("new()", function() + it("should construct with a Reporting Service and Logging Service", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(ga).to.be.ok() + end) + + it("should fail to be constructed without a Reporting Service", function() + expect(function() + GoogleAnalytics.new(nil) + end).to.throw() + end) + end) + + describe("setEnabled()", function() + it("should succeed with valid input", function() + local reporter = GoogleAnalytics.new(DebugReportingService) + reporter:setEnabled(false) + reporter:setEnabled(true) + end) + it("should disable the reporter", function() + local reporter = GoogleAnalytics.new(DebugReportingService) + reporter:setEnabled(false) + expect(function() + reporter:trackEvent(testCategory, testAction, testLabel, testValue) + end).to.throw() + end) + end) + + describe("trackEvent()", function() + it("should work when appropriately enabled / disabled", function() + local ga = GoogleAnalytics.new(DebugReportingService) + + expect(function() + ga:setEnabled(false) + ga:trackEvent(testCategory, testAction, testLabel) + end).to.throw() + + ga:setEnabled(true) + ga:trackEvent(testCategory, testAction, testLabel) + end) + + it("should succeed with valid input", function() + local ga = GoogleAnalytics.new(DebugReportingService) + ga:trackEvent(testCategory, testAction, testLabel, testValue) + end) + + it("should throw an error if it is missing a category", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(nil, testAction, testLabel, testValue) + end).to.throw() + end) + + it("should throw an error if it is missing a testAction", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(testCategory, nil, testLabel, testValue) + end).to.throw() + end) + + it("should not throw an error if it is missing a label", function() + local ga = GoogleAnalytics.new(DebugReportingService) + ga:trackEvent(testCategory, testAction, nil, testValue) + end) + + it("should not throw an error if it is missing a value", function() + local ga = GoogleAnalytics.new(DebugReportingService) + ga:trackEvent(testCategory, testAction, testLabel) + end) + + it("should throw an error if it is given invalid input for category", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(badTestCategory, testAction, testLabel, testValue) + end).to.throw() + end) + + it("should throw an error if it is given invalid input for action", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(testCategory, badTestAction, testLabel, testValue) + end).to.throw() + end) + + it("should throw an error if it is given invalid input for label", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(testCategory, testAction, badTestLabel, testValue) + end).to.throw() + end) + + it("should throw an error if it is given invalid input for value", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(testCategory, testAction, testLabel, badTestValue) + end).to.throw() + + expect(function() + ga:trackEvent(testCategory, testAction, testLabel, -1) + end).to.throw() + end) + end) + + + +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/Influx.lua b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/Influx.lua new file mode 100644 index 0000000..89f287e --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/Influx.lua @@ -0,0 +1,47 @@ +--[[ + Specialized reporter for sending data to InfluxDb. + Useful for very detailed information about specific errors. + + Due to how Influx sends data, it is disallowed on XBox. + ~Kyler Mulherin (9/12/2017) +]] + +local Influx = {} +Influx.__index = Influx + +-- reportingService - (object) any object that defines the same functions for Influx as AnalyticsService +function Influx.new(reportingService) + local rsType = type(reportingService) + assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService") + + local self = { + _reporter = reportingService, + _isEnabled = true, + } + setmetatable(self, Influx) + + return self +end + +-- isEnabled : (boolean) +function Influx:setEnabled(isEnabled) + assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean") + self._isEnabled = isEnabled +end + +-- seriesName : (string) the name of the series as it will appear in InfluxDb +-- additionalArgs : (map) extra key/values to appear in each series +-- throttlingPercent : (int) the chance to actually report this series +function Influx:reportSeries(seriesName, additionalArgs, throttlingPercent) + additionalArgs = additionalArgs or {} + + assert(type(seriesName) == "string", "Expected seriesName to be a string") + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(type(throttlingPercent) == "number", "Expected throttlingPercent to be a number") + assert(throttlingPercent >= 0 and throttlingPercent <= 10000, "throttlingPercent must be between 0 - 10,000") + assert(self._isEnabled, "This reporting service is disabled") + + self._reporter:ReportInfluxSeries(seriesName, additionalArgs, throttlingPercent) +end + +return Influx diff --git a/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/Influx.spec.lua b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/Influx.spec.lua new file mode 100644 index 0000000..8d417ba --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/AnalyticsReporters/Influx.spec.lua @@ -0,0 +1,165 @@ +return function() + local Influx = require(script.Parent.Influx) + + local testSeriesName = "testSeries" + local testArgs = { + testKey = "testValue" + } + local testThrottlingPercentage = 1000 + + local badTestSeriesName = 114 + local badTestArgs = "someString" + local badThrottlingPercentage1 = -15 + local badThrottlingPercentage2 = 150000 + local badThrottlingPercentage3 = "a lot" + + + local function isTableEqual(table1, table2) + if table1 == table2 then + return true + end + + if type(table2) ~= "table" then + return false + end + + for key, _ in pairs(table1) do + if table1[key] ~= table2[key] then + return false + end + end + for key, _ in pairs(table2) do + if table2[key] ~= table1[key] then + return false + end + end + + return true + end + + + local DebugReportingService = {} + function DebugReportingService:ReportInfluxSeries(seriesName, additionalArgs, throttlingPercentage) + if seriesName ~= seriesName then + error("Unexpected value for seriesName: " .. seriesName) + end + if throttlingPercentage ~= testThrottlingPercentage then + error("Unexpected value for throttlingPercentage: " .. throttlingPercentage) + end + if isTableEqual(additionalArgs, {}) == false then + if isTableEqual(additionalArgs, testArgs) == false then + error("Unexpected value for additionalArgs") + end + end + end + + + describe("new()", function() + it("should construct with a Reporting Service", function() + local influx = Influx.new(DebugReportingService) + expect(influx).to.be.ok() + end) + + it("should fail to be constructed without a Reporting Service", function() + expect(function() + Influx.new(nil) + end).to.throw() + end) + end) + + describe("setEnabled()", function() + it("should succeed with valid input", function() + local influx = Influx.new(DebugReportingService) + influx:setEnabled(false) + influx:setEnabled(true) + end) + it("should disable the reporter", function() + local influx = Influx.new(DebugReportingService) + influx:setEnabled(false) + expect(function() + influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage) + end).to.throw() + end) + end) + + describe("reportSeries()", function() + it("should work when appropriately enabled / disabled", function() + local influx = Influx.new(DebugReportingService) + + expect(function() + influx:setEnabled(false) + influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage) + end).to.throw() + + + influx:setEnabled(true) + influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage) + end) + + it("should succeed with valid input", function() + local influx = Influx.new(DebugReportingService) + influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage) + end) + + it("should succeed even if it is missing any additionalArgs", function() + local influx = Influx.new(DebugReportingService) + influx:reportSeries(testSeriesName, nil, testThrottlingPercentage) + end) + + it("should throw an error with invalid input for the seriesName", function() + local influx = Influx.new(DebugReportingService) + expect(function() + influx:reportSeries(badTestSeriesName, testArgs, testThrottlingPercentage) + end).to.throw() + end) + + it("should throw an error with invalid input for the throttlingPercentage - out of range - below zero", function() + expect(function() + local influx = Influx.new(DebugReportingService) + influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage1) + end).to.throw() + end) + + it("should throw an error with invalid input for the throttlingPercentage - out of range - above cap", function() + expect(function() + local influx = Influx.new(DebugReportingService) + influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage2) + end).to.throw() + end) + + it("should throw an error with invalid input for the throttlingPercentage - bad type", function() + expect(function() + local influx = Influx.new(DebugReportingService) + influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage3) + end).to.throw() + end) + + it("should throw an error with completely invalid input", function() + local influx = Influx.new(DebugReportingService) + expect(function() + influx:reportSeries(badTestSeriesName, badTestArgs, badThrottlingPercentage1) + end).to.throw() + end) + + it("should throw an error if it is missing a seriesName", function() + local influx = Influx.new(DebugReportingService) + expect(function() + influx:reportSeries(nil, testArgs, testThrottlingPercentage) + end).to.throw() + end) + + it("should throw an error if it is missing a throttlingPercentage", function() + local influx = Influx.new(DebugReportingService) + expect(function() + influx:reportSeries(testSeriesName, testArgs, nil) + end).to.throw() + end) + + it("should throw an error if it is missing any input", function() + local influx = Influx.new(DebugReportingService) + expect(function() + influx:reportSeries(nil, nil, nil) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/Examples/Roact-asteroids.lua b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-asteroids.lua new file mode 100644 index 0000000..1cfe706 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-asteroids.lua @@ -0,0 +1,469 @@ +--[[ + Implements the start of an Asteroids clone using Roact and Rodux. + + This is intended to be an advanced example that uses most edge cases of both + Roact and Rodux. +]] + +local Workspace = game:GetService("Workspace") +local RunService = game:GetService("RunService") +local UserInputService = game:GetService("UserInputService") +local PlayerGui = game.Players.LocalPlayer.PlayerGui + +local Common = script.Parent.Parent + +local Roact = require(Common.Roact) +local ExternalEventConnection = require(Common.RoactUtilities.ExternalEventConnection) +local Rodux = require(Common.Rodux) +local RoactRodux = require(Common.RoactRodux) +local Immutable = require(Common.Immutable) + +local function playerInputReducer(state, action) + state = state or { + left = 0, + right = 0, + up = 0, + pause = 0, + } + + if action.type == "SetPlayerInput" then + return Immutable.Set(state, action.playerInput, action.value) + end + + return state +end + +local function playerReducer(state, action) + state = state or { + position = Vector2.new(0, 0), + velocity = Vector2.new(0, 0), + angle = 0, + turnRate = math.pi / 2, + throttleRate = 10, + } + + if action.type == "GameStep" then + local inputs = action.inputs + local delta = action.delta + local direction = Vector2.new(math.sin(state.angle), -math.cos(state.angle)) + local turnImpulse = inputs["right"] - inputs["left"] + local throttleImpulse = inputs["up"] + + return Immutable.JoinDictionaries(state, { + position = state.position + (state.velocity * delta), + velocity = state.velocity + (throttleImpulse * state.throttleRate * delta * direction), + angle = state.angle + (turnImpulse * state.turnRate * delta), + }) + elseif action.type == "SetPlayerRotation" then + return Immutable.Set(state, "angle", action.value) + end + + return state +end + +local function timeReducer(state, action) + state = state or 0 + + if action.type == "GameStep" then + return state + action.delta + end + + return state +end + +local function asteroidsReducer(state, action) + + local function initAsteroids() + return { + { + position = Vector2.new(-5,-5), + velocity = Vector2.new(1,0), + size = 5, + }, { + position = Vector2.new(-5,5), + velocity = Vector2.new(1,0), + size = 3, + }, { + position = Vector2.new(5,5), + velocity = Vector2.new(0,-1), + size = 5, + }, { + position = Vector2.new(5,-5), + velocity = Vector2.new(0,1), + size = 3, + }, + } + end + + state = state or initAsteroids() + + if action.type == "GameStep" then + local delta = action.delta + local adjustedAsteroids = {} + + for index, asteroid in pairs(state) do + local posX = asteroid.position.X + local posY = asteroid.position.Y + local velX = asteroid.velocity.X + local velY = asteroid.velocity.Y + + adjustedAsteroids[index] = { + position = Vector2.new(posX + velX * delta, posY + velY * delta), + velocity = Vector2.new(velX, velY), + size = asteroid.size + } + end + + return adjustedAsteroids + end + + return state +end + +local function pausePressed(action) + return action.playerInput == "pause" and action.value == 1 +end + +local function pauseReducer(state, action) + state = state or false + + if action.type == "SetPlayerInput" and pausePressed(action) then + return not state + end + + return state +end + +local function reducer(state, action) + state = state or {} + + return { + playerInputs = playerInputReducer(state.playerInputs, action), + paused = pauseReducer(state.paused, action), + player = playerReducer(state.player, action), + time = timeReducer(state.time, action), + asteroids = asteroidsReducer(state.asteroids, action), + } +end + +-- Defines the current camera declaratively +local Camera = Roact.Component:extend("Camera") + +function Camera:init() + self.cameraRef = function(rbx) + self.cameraRbx = rbx + end +end + +function Camera:render() + local focus = self.props.focus + + return Roact.createElement("Camera", { + CameraType = Enum.CameraType.Scriptable, + CFrame = CFrame.new(Vector3.new(focus.x, 20, focus.y)) * CFrame.fromEulerAnglesXYZ(-math.pi / 2, 0, 0), + FieldOfView = 90, + + [Roact.Ref] = self.cameraRef, + }) +end + +function Camera:didMount() + Workspace.CurrentCamera = self.cameraRbx +end + +-- A version of the camera that's tied to the player's position. +local PlayerCamera = RoactRodux.connect(function(store) + local player = store:getState().player + + return { + focus = player.position + } +end)(Camera) + +-- Any asteroid in the game world. +local Asteroid = Roact.Component:extend("Asteroid") + +function Asteroid:render() + local position = self.props.position + local size = self.props.size + + return Roact.createElement("Part", { + Anchored = true, + Shape = Enum.PartType.Ball, + Size = Vector3.new(size, size, size), + Color = Color3.fromRGB(170, 170, 170), + CFrame = CFrame.new(Vector3.new(position.x, 0, position.y)), + TopSurface = Enum.SurfaceType.Smooth, + }) +end + +-- A collection of Asteroids in the world. +local Asteroids = Roact.Component:extend("Asteroids") + +function Asteroids:render() + local asteroids = self.props + local asteroidComponentTable = {} + + for index, asteroid in pairs(asteroids) do + asteroidComponentTable[index] = Roact.createElement(Asteroid, { + position = asteroid.position, + size = asteroid.size, + }) + end + + return Roact.createElement("Folder", { + Name = "Asteroids" + }, asteroidComponentTable) +end + +-- Connect the Asteroids component to state.asteroids +Asteroids = RoactRodux.connect(function(store) + local state = store:getState() + local asteroids = state.asteroids + + return asteroids +end)(Asteroids) + +-- Any ship in the game world. +local Ship = Roact.Component:extend("Ship") + +function Ship:render() + local position = self.props.position + local angle = self.props.angle + local time = self.props.time + + local color = Color3.fromHSV((time % 5) / 5, 1, 1) + + return Roact.createElement("Part", { + Anchored = true, + Size = Vector3.new(2, 2, 6), + Color = color, + CFrame = CFrame.new(Vector3.new(position.x, 0, position.y)) * CFrame.fromEulerAnglesXYZ(0, -angle, 0), + FrontSurface = Enum.SurfaceType.Hinge, + }) +end + +-- A version of the ship that's tied to the player's data. +local PlayerShip = RoactRodux.connect(function(store) + local state = store:getState() + local player = state.player + + return { + position = player.position, + angle = player.angle, + time = state.time, + } +end)(Ship) + +-- A static background +local GameBackground = Roact.Component:extend("GameBackground") + +function GameBackground:render() + return Roact.createElement("Part", { + Anchored = true, + Size = Vector3.new(512, 10, 512), + CFrame = CFrame.new(Vector3.new(0, -5, 0)), + Color = Color3.new(0, 0, 0), + }) +end + +-- Text displayed when paused +local PauseText = Roact.Component:extend("PauseText") + +function PauseText:render() + return Roact.createElement("TextLabel", { + Text = "Paused", + TextColor3 = Color3.new(255, 255, 255), + Font = Enum.Font.Arcade, + TextSize = 70, + Position = UDim2.new(0.5, 0, 0.5, 0), + }) +end + +-- Connects to UserInputService when created, disconnects when removed. +local InputConnector = Roact.Component:extend("InputConnector") + +function InputConnector:render() + local function inputBeganCallback(input) + if self.props.onInputBegan then + self.props.onInputBegan(input) + end + end + + local function inputChangedCallback(input) + if self.props.onInputChanged then + self.props.onInputChanged(input) + end + end + + local function inputEndedCallback(input) + if self.props.onInputEnded then + self.props.onInputEnded(input) + end + end + + return Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputBegan, + callback = inputBeganCallback, + }, { + Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputChanged, + callback = inputChangedCallback, + }, { + Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputEnded, + callback = inputEndedCallback, + }) + }) + }) +end + +local KEYBOARD_KEY_MAPPING = { + [Enum.KeyCode.Left] = "left", + [Enum.KeyCode.A] = "left", + [Enum.KeyCode.Right] = "right", + [Enum.KeyCode.D] = "right", + [Enum.KeyCode.Up] = "up", + [Enum.KeyCode.W] = "up", + [Enum.KeyCode.P] = "pause", +} + +local function getPlayerInput(input) + if input.UserInputType ~= Enum.UserInputType.Keyboard then + return nil + end + + return KEYBOARD_KEY_MAPPING[input.KeyCode] +end + +local KeyboardInputConnector = RoactRodux.connect(function(store) + local function onInputBegan(input) + local playerInput = getPlayerInput(input) + + if playerInput then + store:dispatch({ + type = "SetPlayerInput", + playerInput = playerInput, + value = 1, + }) + end + end + + local function onInputEnded(input) + local playerInput = getPlayerInput(input) + + if playerInput then + store:dispatch({ + type = "SetPlayerInput", + playerInput = playerInput, + value = 0, + }) + end + end + + return { + onInputBegan = onInputBegan, + onInputEnded = onInputEnded, + } +end)(InputConnector) + +local RenderSteppedConnector = Roact.Component:extend("RenderSteppedConnector") + +function RenderSteppedConnector:render() + return Roact.createElement(ExternalEventConnection, { + event = RunService.RenderStepped, + callback = self.props.onStepped, + }) +end + +-- Connect RenderSteppedConnector to actions dispatched to the store +RenderSteppedConnector = RoactRodux.connect(function(store) + return { + onStepped = function(delta) + store:dispatch({ + type = "GameStep", + delta = delta, + inputs = store:getState().playerInputs, + }) + end + } +end)(RenderSteppedConnector) + +--[[ + Only renders the given component if the given route is the current one. + + The component is passed as a prop so that we don't worry about its + descendants until they're relevant. +]] +local function PauseRoute(props) + -- Injected by RoactRodux + local paused = props.paused + + -- Provided by parent component + local match = props.match + local component = props.component + + if paused == match then + return Roact.createElement(component) + end +end + +PauseRoute = RoactRodux.connect(function(store) + local state = store:getState() + + return { + paused = state.paused + } +end)(PauseRoute) + +-- Package everything up in Workspace. +local App = Roact.Component:extend("App") + +function App:render() + return Roact.createElement("Folder", {}, { + Player = Roact.createElement(PlayerShip), + PlayerCamera = Roact.createElement(PlayerCamera), + GameBackground = Roact.createElement(GameBackground), + Asteroids = Roact.createElement(Asteroids), + KeyboardInputConnector = Roact.createElement(KeyboardInputConnector), + RenderSteppedConnector = Roact.createElement(PauseRoute, { + match = false, + component = RenderSteppedConnector, + }), + }) +end + +-- Package everything up in PlayerGui. +local Ui = Roact.Component:extend("Ui") + +function Ui:render() + return Roact.createElement("ScreenGui", nil, { + PauseText = Roact.createElement(PauseRoute, { + match = true, + component = PauseText, + }) + }) +end + +local function main() + RunService.Stepped:Wait() + + local store = Rodux.Store.new(reducer) + + local app = Roact.createElement(RoactRodux.StoreProvider, { + store = store + }, { + App = Roact.createElement(App) + }) + + local ui = Roact.createElement(RoactRodux.StoreProvider, { + store = store + }, { + Ui = Roact.createElement(Ui) + }) + + Roact.reify(app, Workspace, "Roact-asteroids") + Roact.reify(ui, PlayerGui, "Roact-asteroids-ui") +end + +return main \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/Examples/Roact-context.lua b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-context.lua new file mode 100644 index 0000000..9cbf22a --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-context.lua @@ -0,0 +1,48 @@ +--[[ + Roact inherit's React's concept of 'context' used for putting an object + into the tree and retrieving it without passing it explicitly. + + This feature should be used sparingly; in most cases, you can use Rodux, + which is implemented using context under the hood. + + Context does not update and is only passed when a component is initially + constructed; you should use another mechanism to listen for changes if + using context. +]] + +local Common = script.Parent.Parent +local Roact = require(Common.Roact) + +local ContextProvider = Roact.Component:extend("ContextProvider") + +function ContextProvider:init() + -- Set self._context to add values to the tree + self._context = { + someValue = 5 + } +end + +function ContextProvider:render() + -- Utility provided by Roact to pull exactly one child out of Children. + return Roact.oneChild(self.props[Roact.Children]) +end + +local ContextReader = Roact.Component:extend("ContextReader") + +function ContextReader:init() + -- self._context contains all of the values from our ancestor components + print("Context value of", self._context.someValue) +end + +function ContextReader:render() + -- No need to return anything +end + +return function() + local element = Roact.createElement(ContextProvider, { + }, { + SomeChild = Roact.createElement(ContextReader) + }) + + Roact.reify(element, nil, "SomeName") +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/Examples/Roact-counter.lua b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-counter.lua new file mode 100644 index 0000000..b498d50 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-counter.lua @@ -0,0 +1,71 @@ +--[[ + Demonstrates use of state updates to re-render the UI. +]] + +local CoreGui = game:GetService("CoreGui") + +local Common = script.Parent.Parent +local Roact = require(Common.Roact) + +-- A functional component to render the current tick value. +local function TickLabel(props) + local value = props.value + + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Text = ("Current tick is %d!"):format(value), + }) +end + +local App = Roact.Component:extend("App") + +function App:init() + -- State that cannot affect rendering exists directly on the instance. + self.running = false + + -- State that the component changes that affects rendering exists on `state` + -- You can only assign to `state` directly in the constructor. + self.state = { + count = 0 + } +end + +function App:render() + return Roact.createElement("ScreenGui", { + Name = "Roact-demo-counter", + }, { + Count = Roact.createElement(TickLabel, { + value = self.state.count, + }), + }) +end + +function App:didMount() + -- Use 'didMount' to be notified when a component instance is created + -- and added to the Roblox tree. + + spawn(function() + self.running = true + + while self.running do + -- Use 'setState' to update the component and patch the current + -- state with new properties. + -- Don't set `state` directly! + self:setState({ + count = self.state.count + 1 + }) + + wait(1) + end + end) +end + +function App:willUnmount() + -- Use 'willUnmount' to be notified when your component is about to be + -- removed from the tree. Do any cleanup here, like terminating a loop! + self.running = false +end + +return function() + Roact.reify(Roact.createElement(App), CoreGui, "Roact-counter") +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/Examples/Roact-events.lua b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-events.lua new file mode 100644 index 0000000..db55fb0 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-events.lua @@ -0,0 +1,31 @@ +local CoreGui = game:GetService("CoreGui") + +local Common = script.Parent.Parent + +local Roact = require(Common.Roact) + +local App = Roact.Component:extend("App") + +function App:render() + return Roact.createElement("ScreenGui", { + }, { + Button = Roact.createElement("TextButton", { + Size = UDim2.new(0.5, 0, 0.5, 0), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + + -- Attach event listeners using `Roact.Event[eventName]` + -- Event listeners get `rbx` as their first parameter + -- followed by their normal event arguments. + [Roact.Event.MouseButton1Click] = function(rbx) + print("The button was clicked!") + end + }), + }) +end + +return function() + local element = Roact.createElement(App) + + Roact.reify(element, CoreGui, "Roact-events") +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/Examples/Roact-ref.lua b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-ref.lua new file mode 100644 index 0000000..78b991b --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-ref.lua @@ -0,0 +1,32 @@ +--[[ + 'Refs' are a concept that let you break out of the Roact paradigm and access + Roblox instances directly. + + Pass a callback as a prop using the key [Roact.Ref] to receive + the reference. + + When the object is destructed or the ref is replaced in an update, the ref + callback will be passed nil. + + This feature is intended to be an escape hatch; it should not be necessary + for the majority of work using Roact. In many cases, code using refs can be + isolated and exposed with a friendlier API. +]] + +local Common = script.Parent.Parent +local Roact = require(Common.Roact) + +return function() + local currentFrame + + local element = Roact.createElement("Frame", { + [Roact.Ref] = function(rbx) + -- All properties are already set and the object has been parented + currentFrame = rbx + end + }) + + Roact.reify(element, nil, "SomeName") + + print("Have object", currentFrame) +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/Examples/Roact-rodux.lua b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-rodux.lua new file mode 100644 index 0000000..56e120a --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-rodux.lua @@ -0,0 +1,83 @@ +--[[ + Demonstrates how to use RoactRodux to link Roact and Rodux toegther into + a single project. +]] + +local CoreGui = game:GetService("CoreGui") + +local Common = script.Parent.Parent +local Roact = require(Common.Roact) +local Rodux = require(Common.Rodux) +local RoactRodux = require(Common.RoactRodux) + +-- React Portion +-- This code doesn't know anything about Rodux. +-- It can function as an isolated component and should be able to do so. +local App = Roact.Component:extend("App") + +function App:render() + local count = self.props.count + local onClick = self.props.onClick + + return Roact.createElement("ScreenGui", nil, { + Label = Roact.createElement("TextButton", { + Size = UDim2.new(1, 0, 1, 0), + Text = "Count: " .. tostring(count), + AutoButtonColor = false, + + [Roact.Event.MouseButton1Click] = onClick, + }) + }) +end + +-- React-Rodux Portion +-- This code ties together Roact and Rodux by generating a wrapper component. +-- The wrapper component maps the 'store' to a set of props we can use. +-- It also receives (and passes on) 'props' given from the parent component. +local connector = RoactRodux.connect(function(store, props) + local state = store:getState() + + local function increment() + store:dispatch("increment") + end + + return { + count = state.count, + onClick = increment, + } +end) + +-- In a lot of cases it's useful to preserve the original component +-- For this example, we don't need the unwrapped App +App = connector(App) + +-- Rodux Portion +-- This is a reducer that lets you increment a value. +local function reducer(state, action) + state = state or { + count = 0, + } + + if action == "increment" then + return { + count = state.count + 1 + } + end + + return state +end + +-- Setup +return function() + local store = Rodux.Store.new(reducer) + + -- We wrap our Roact-Rodux app in a `StoreProvider`, which makes sure our + -- components know what store they should be connecting to. + local app = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + App = Roact.createElement(App), + }) + + Roact.reify(app, CoreGui, "Roact-demo-rodux") +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/Examples/Roact-router.lua b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-router.lua new file mode 100644 index 0000000..0735098 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-router.lua @@ -0,0 +1,244 @@ +--[[ + Demonstrates creation of a complete routing system using Roact and Rodux. + + This example mimics the philosophy of React Router 4 with dynamic routes: + https://reacttraining.com/react-router/web/guides/philosophy +]] + +local CoreGui = game:GetService("CoreGui") + +local Common = script.Parent.Parent + +local Immutable = require(Common.Immutable) +local Roact = require(Common.Roact) +local Rodux = require(Common.Rodux) +local RoactRodux = require(Common.RoactRodux) + +-- A component that can be clicked to navigate around the app. +local function Link(props) + local text = props.text + local onClick = props.onClick + + return Roact.createElement("TextButton", { + Size = UDim2.new(0, 100, 0, 30), + Text = text, + + [Roact.Event.MouseButton1Click] = onClick + }) +end + +Link = RoactRodux.connect(function(store, props) + return { + onClick = function() + store:dispatch({ + type = "Navigate", + location = props.target + }) + end + } +end)(Link) + +--[[ + A component that can be clicked to navigate back one step, if available. + + Appears darker if there is no history to navigate backwards to. +]] +local function BackButton(props) + local onClick = props.onClick + local enabled = props.enabled + + return Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Text = "<", + AutoButtonColor = enabled, + BackgroundColor3 = enabled and Color3.new(0.8, 0.8, 0.8) or Color3.new(0.5, 0.5, 0.5), + + [Roact.Event.MouseButton1Click] = onClick, + }) +end + +BackButton = RoactRodux.connect(function(store) + local state = store:getState() + + return { + onClick = function() + store:dispatch({ + type = "Back" + }) + end, + enabled = #state.history > 0, + } +end)(BackButton) + +--[[ + Only renders the given component if the given route is the current one. + + The component is passed as a prop so that we don't worry about its + descendants until they're relevant. +]] +local function Route(props) + -- Injected by RoactRodux + local current = props.current + + -- Provided by parent component + local match = props.match + local component = props.component + + if current == match then + return Roact.createElement(component) + end +end + +Route = RoactRodux.connect(function(store) + local state = store:getState() + + return { + current = state.current, + } +end)(Route) + +--[[ + A navigation bar that lets us go to one of three locations, or + travel backwards. +]] +local function NavBar() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 30), + Position = UDim2.new(0, 0, 1, 0), + BackgroundColor3 = Color3.new(1, 1, 1), + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + }), + + Back = Roact.createElement(BackButton), + + GoHome = Roact.createElement(Link, { + text = "Go to Home", + target = "home", + }), + + GoAbout = Roact.createElement(Link, { + text = "Go to About", + target = "about", + }), + + GoContact = Roact.createElement(Link, { + text = "Go to Contact", + target = "contact", + }), + }) +end + +-- 'Home' page, our default view +local function Home() + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Color3.new(0.5, 0.3, 0.7), + Text = "Home", + TextSize = 30, + TextColor3 = Color3.new(0.95, 0.95, 0.95), + }) +end + +-- 'About' page +local function About() + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Color3.new(0.5, 0.6, 0.2), + Text = "About", + TextSize = 30, + TextColor3 = Color3.new(0.95, 0.95, 0.95), + }) +end + +-- 'Contact' page +local function Contact() + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Color3.new(0.8, 0.3, 0.4), + Text = "Contact", + TextSize = 30, + TextColor3 = Color3.new(0.95, 0.95, 0.95), + }) +end + +--[[ + The wrapper used to package up all of our components and display them. +]] +local App = Roact.Component:extend("App") + +function App:render() + return Roact.createElement("ScreenGui", { + }, { + Body = Roact.createElement("Frame", { + Size = UDim2.new(0, 600, 0, 400), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + }, { + -- These components sort of act like a static routing table. + Home = Roact.createElement(Route, { + match = "home", + component = Home, + }), + + About = Roact.createElement(Route, { + match = "about", + component = About, + }), + + Contact = Roact.createElement(Route, { + match = "contact", + component = Contact, + }), + + NavBar = Roact.createElement(NavBar), + }), + }) +end + +--[[ + All of our navigation is described using a Redux reducer. + + This means we can time-travel debug and log actions just like other + Redux data. +]] +local function reducer(state, action) + state = state or { + current = "home", + history = {} + } + + if action.type == "Navigate" then + return { + current = action.location, + history = Immutable.Append(state.history, state.current), + } + elseif action.type == "Back" then + local length = #state.history + + if length == 0 then + return state + end + + return { + current = state.history[length], + history = Immutable.RemoveFromList(state.history, length), + } + end + + return state +end + +return function() + local store = Rodux.Store.new(reducer) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + App = Roact.createElement(App) + }) + + Roact.reify(element, CoreGui, "Roact-router") +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/Examples/Roact-stress.lua b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-stress.lua new file mode 100644 index 0000000..b848ae2 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Examples/Roact-stress.lua @@ -0,0 +1,63 @@ +local CoreGui = game:GetService("CoreGui") +local RunService = game:GetService("RunService") + +local GRID_SIZE = 50 +local NODE_SIZE = 10 + +local Common = script.Parent.Parent +local Roact = require(Common.Roact) + +local Node = Roact.Component:extend("Node") + +function Node:render() + local x = self.props.x + local y = self.props.y + local time = self.props.time + + local n = time + x / NODE_SIZE + y / NODE_SIZE + + return Roact.createElement("Frame", { + Size = UDim2.new(0, NODE_SIZE, 0, NODE_SIZE), + Position = UDim2.new(0, NODE_SIZE * x, 0, NODE_SIZE * y), + BackgroundColor3 = Color3.new(0.5 + 0.5 * math.sin(n), 0.5, 0.5), + }) +end + +local App = Roact.Component:extend("App") + +function App:init() + self.state = { + time = tick(), + } +end + +function App:render() + local time = self.state.time + local nodes = {} + + local n = 0 + for x = 0, GRID_SIZE - 1 do + for y = 0, GRID_SIZE - 1 do + n = n + 1 + nodes[n] = Roact.createElement(Node, { + x = x, + y = y, + time = time, + }) + end + end + + return Roact.createElement("ScreenGui", nil, nodes) +end + +function App:didMount() + RunService.Stepped:Connect(function() + self:setState({ + time = tick(), + }) + end) +end + +return function() + Roact.reify(Roact.createElement(App), CoreGui) +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/Functional.lua b/Client2018/content/internal/Common/Modules/Common/Functional.lua new file mode 100644 index 0000000..a40c8ad --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Functional.lua @@ -0,0 +1,131 @@ +--[[ + Provides an implementation of functional programming primitives. +]] + +local Functional = {} + +--[[ + Create a copy of a list with only values for which `callback` returns true +]] +function Functional.Filter(list, callback) + local new = {} + + for key = 1, #list do + local value = list[key] + if callback(value, key) then + table.insert(new, value) + end + end + + return new +end + +--[[ + Create a copy of a list where each value is transformed by `callback` +]] +function Functional.Map(list, callback) + local new = {} + + for key = 1, #list do + new[key] = callback(list[key], key) + end + + return new +end + +--[[ + Identical to Map, except that the result will be reversed. +]] +function Functional.MapReverse(list, callback) + local new = {} + + for key = #list, 1, -1 do + new[key] = callback(list[key], key) + end + + return new +end + +--[[ + Create a copy of a list doing a combination filter and map. + + If callback returns nil for any item, it is considered filtered from the + list. Any other value is considered the result of the 'map' operation. +]] +function Functional.FilterMap(list, callback) + local new = {} + + for key = 1, #list do + local value = list[key] + local result = callback(value, key) + + if result ~= nil then + table.insert(new, result) + end + end + + return new +end + +--[[ + Performs a left-fold of the list with the given initial value and callback. +]] +function Functional.Fold(list, initial, callback) + local accum = initial + + for key = 1, #list do + accum = callback(accum, list[key], key) + end + + return accum +end + +--[[ + Performs a fold over the entries in the given dictionary. +]] +function Functional.FoldDictionary(dictionary, initial, callback) + local accum = initial + + for key, value in pairs(dictionary) do + accum = callback(accum, key, value) + end + + return accum +end + +--[[ + Returns a list that contains at most `count` values from the given list. +]] +function Functional.Take(list, count, startingIndex) + startingIndex = startingIndex or 1 + + local maxIndex = count + (startingIndex - 1) + if maxIndex > #list then + maxIndex = #list + end + + local new = {} + + for i = startingIndex, maxIndex do + local value = list[i] + local newIndex = i - (startingIndex - 1) + new[newIndex] = value + end + + return new +end + +--[[ + If the list contains the sought-after element, return its index, or nil otherwise. +]] +function Functional.Find(list, value) + for index, element in ipairs(list) do + if element == value then + return index + end + end + + return nil +end + +return Functional \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/Functional.spec.lua b/Client2018/content/internal/Common/Modules/Common/Functional.spec.lua new file mode 100644 index 0000000..4ea87a4 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Functional.spec.lua @@ -0,0 +1,218 @@ +return function() + local Functional = require(script.Parent.Functional) + + local function identity(...) + return ... + end + + local function add(a, b) + return a + b + end + + describe("Filter", function() + it("should copy lists correctly", function() + local listA = {1, 2, 3} + local listB = Functional.Filter(listA, function() + return true + end) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the filter predicate", function() + local listA = {1, 2, 3, 4, 5} + local listB = Functional.Filter(listA, function(value, key) + expect(value).to.equal(key) + + return value % 2 == 0 + end) + + expect(listB[1]).to.equal(2) + expect(listB[2]).to.equal(4) + end) + end) + + describe("Map", function() + it("should copy lists correctly using the identity function", function() + local listA = {1, 2, 3} + local listB = Functional.Map(listA, identity) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the map predicate", function() + local listA = {1, 2, 3} + local listB = Functional.Map(listA, function(value, key) + expect(value).to.equal(key) + + return value * 2 + end) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i] * 2) + end + end) + end) + + describe("MapReverse", function() + it("should copy lists correctly using the identity function", function() + local listA = {1, 2, 3} + local listB = Functional.MapReverse(listA, identity) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the map predicate", function() + local listA = {1, 2, 3} + local listB = Functional.MapReverse(listA, function(value, key) + expect(value).to.equal(key) + + return value * 2 + end) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i] * 2) + end + end) + + it("should iterate backwards", function() + local list = {1, 2, 3} + local nextKey = 3 + + Functional.MapReverse(list, function(value, key) + expect(value).to.equal(nextKey) + expect(key).to.equal(nextKey) + + nextKey = nextKey - 1 + end) + + expect(nextKey).to.equal(0) + end) + end) + + describe("FilterMap", function() + it("should copy truthy lists using the identity function", function() + local listA = {1, 2, 3} + local listB = Functional.FilterMap(listA, identity) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the filter-map predicate", function() + local listA = {1, 2, 3, 4, 5} + + -- Create a list containing only the odd numbers, and double those numbers + local listB = Functional.FilterMap(listA, function(value, key) + expect(value).to.equal(key) + + if value % 2 == 0 then + return nil + end + + return value * 2 + end) + + expect(listB[1]).to.equal(2) + expect(listB[2]).to.equal(6) + expect(listB[3]).to.equal(10) + end) + end) + + describe("Fold", function() + it("should left-fold lists", function() + local list = {1, 2, 3, 4, 5} + + local sum = Functional.Fold(list, 0, add) + + expect(sum).to.equal(15) + end) + end) + + describe("Take", function() + it("should take values from a list", function() + local a = {1, 2, 3} + local b = Functional.Take(a, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + end) + + it("should not take past the end of a list", function() + local a = {1, 2, 3} + local b = Functional.Take(a, 4) + + expect(#b).to.equal(3) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + end) + + it("should copy all values when taking past the end of a list", function() + local a = {1, 2, 3} + local b = Functional.Take(a, 4) + + expect(#b).to.equal(#a) + expect(a[1]).to.equal(b[1]) + expect(a[2]).to.equal(b[2]) + expect(a[3]).to.equal(b[3]) + end) + + it("should take values from a starting index when provided", function() + local a = {1, 2, 3, 4} + local b = Functional.Take(a, 2, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(2) + expect(b[2]).to.equal(3) + end) + + it("should not take past the end of a list when the starting index is provided", function() + local a = {1, 2, 3, 4} + local b = Functional.Take(a, 3, 3) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(3) + expect(b[2]).to.equal(4) + end) + end) + + describe("Find", function() + it("should return index of matched item", function() + local a = {"foo", "bar", "garply"} + local b = Functional.Find(a, "bar") + + expect(b).to.equal(2) + end) + + it("should find the first example in the case of duplicates", function() + local a = {"foo", "bar", "garply", "bar"} + local b = Functional.Find(a, "bar") + + expect(b).to.equal(2) + end) + + it("should return nil if item is not found", function() + local a = {"foo", "bar", "garply"} + local b = Functional.Find(a, "fleebledegoop") + + expect(b).to.equal(nil) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/Immutable.lua b/Client2018/content/internal/Common/Modules/Common/Immutable.lua new file mode 100644 index 0000000..a73d203 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Immutable.lua @@ -0,0 +1,141 @@ +--[[ + Provides functions for manipulating immutable data structures. +]] + +local Immutable = {} + +--[[ + Merges dictionary-like tables together. +]] +function Immutable.JoinDictionaries(...) + local result = {} + + for i = 1, select("#", ...) do + local dictionary = select(i, ...) + for key, value in pairs(dictionary) do + result[key] = value + end + end + + return result +end + +--[[ + Joins any number of lists together into a new list +]] +function Immutable.JoinLists(...) + local new = {} + + for listKey = 1, select("#", ...) do + local list = select(listKey, ...) + local len = #new + + for itemKey = 1, #list do + new[len + itemKey] = list[itemKey] + end + end + + return new +end + +--[[ + Creates a new copy of the dictionary and sets a value inside it. +]] +function Immutable.Set(dictionary, key, value) + local new = {} + + for key, value in pairs(dictionary) do + new[key] = value + end + + new[key] = value + + return new +end + +--[[ + Creates a new copy of the list with the given elements appended to it. +]] +function Immutable.Append(list, ...) + local new = {} + local len = #list + + for key = 1, len do + new[key] = list[key] + end + + for i = 1, select("#", ...) do + new[len + i] = select(i, ...) + end + + return new +end + +--[[ + Remove elements from a dictionary +]] +function Immutable.RemoveFromDictionary(dictionary, ...) + local result = {} + + for key, value in pairs(dictionary) do + local found = false + for listKey = 1, select("#", ...) do + if key == select(listKey, ...) then + found = true + break + end + end + if not found then + result[key] = value + end + end + + return result +end + +--[[ + Remove the given key from the list. +]] +function Immutable.RemoveFromList(list, removeIndex) + local new = {} + + for i = 1, #list do + if i ~= removeIndex then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Remove the range from the list starting from the index. +]] +function Immutable.RemoveRangeFromList(list, index, count) + local new = {} + + for i = 1, #list do + if i < index or i >= index + count then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Creates a new list that has no occurrences of the given value. +]] +function Immutable.RemoveValueFromList(list, removeValue) + local new = {} + + for i = 1, #list do + if list[i] ~= removeValue then + table.insert(new, list[i]) + end + end + + return new +end + +return Immutable diff --git a/Client2018/content/internal/Common/Modules/Common/Immutable.spec.lua b/Client2018/content/internal/Common/Modules/Common/Immutable.spec.lua new file mode 100644 index 0000000..c9adc5f --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Immutable.spec.lua @@ -0,0 +1,284 @@ +return function() + local Immutable = require(script.Parent.Immutable) + + describe("JoinDictionaries", function() + it("should preserve immutability", function() + local a = {} + local b = {} + + local c = Immutable.JoinDictionaries(a, b) + + expect(c).never.to.equal(a) + expect(c).never.to.equal(b) + end) + + it("should treat list-like values like dictionary values", function() + local a = { + [1] = 1, + [2] = 2, + [3] = 3 + } + + local b = { + [1] = 11, + [2] = 22 + } + + local c = Immutable.JoinDictionaries(a, b) + + expect(c[1]).to.equal(b[1]) + expect(c[2]).to.equal(b[2]) + expect(c[3]).to.equal(a[3]) + end) + + it("should merge dictionary values correctly", function() + local a = { + hello = "world", + foo = "bar" + } + + local b = { + foo = "baz", + tux = "penguin" + } + + local c = Immutable.JoinDictionaries(a, b) + + expect(c.hello).to.equal(a.hello) + expect(c.foo).to.equal(b.foo) + expect(c.tux).to.equal(b.tux) + end) + + it("should merge multiple dictionaries", function() + local a = { + foo = "yes" + } + + local b = { + bar = "yup" + } + + local c = { + baz = "sure" + } + + local d = Immutable.JoinDictionaries(a, b, c) + + expect(d.foo).to.equal(a.foo) + expect(d.bar).to.equal(b.bar) + expect(d.baz).to.equal(c.baz) + end) + end) + + describe("JoinLists", function() + it("should preserve immutability", function() + local a = {} + local b = {} + + local c = Immutable.JoinLists(a, b) + + expect(c).never.to.equal(a) + expect(c).never.to.equal(b) + end) + + it("should treat list-like values correctly", function() + local a = {1, 2, 3} + local b = {4, 5, 6} + + local c = Immutable.JoinLists(a, b) + + expect(#c).to.equal(6) + + for i = 1, #c do + expect(c[i]).to.equal(i) + end + end) + + it("should merge multiple lists", function() + local a = {1, 2} + local b = {3, 4} + local c = {5, 6} + + local d = Immutable.JoinLists(a, b, c) + + expect(#d).to.equal(6) + + for i = 1, #d do + expect(d[i]).to.equal(i) + end + end) + end) + + describe("Set", function() + it("should preserve immutability", function() + local a = {} + + local b = Immutable.Set(a, "foo", "bar") + + expect(b).never.to.equal(a) + end) + + it("should treat numeric keys normally", function() + local a = {1, 2, 3} + + local b = Immutable.Set(a, 2, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(4) + expect(b[3]).to.equal(3) + end) + + it("should overwrite dictionary-like keys", function() + local a = { + foo = "bar", + baz = "qux" + } + + local b = Immutable.Set(a, "foo", "hello there") + + expect(b.foo).to.equal("hello there") + expect(b.baz).to.equal(a.baz) + end) + end) + + describe("Append", function() + it("should preserve immutability", function() + local a = {} + + local b = Immutable.Append(a, "another happy landing") + + expect(b).never.to.equal(a) + end) + + it("should append values", function() + local a = {1, 2, 3} + local b = Immutable.Append(a, 4, 5) + + expect(#b).to.equal(5) + + for i = 1, #b do + expect(b[i]).to.equal(i) + end + end) + end) + + describe("RemoveFromDictionary", function() + it("should preserve immutability", function() + local a = { foo = "bar" } + + local b = Immutable.RemoveFromDictionary(a, "foo") + + expect(b).to.never.equal(a) + end) + + it("should remove fields from the dictionary", function() + local a = { + foo = "bar", + baz = "qux", + boof = "garply", + } + + local b = Immutable.RemoveFromDictionary(a, "foo", "boof") + + expect(b.foo).to.never.be.ok() + expect(b.baz).to.equal("qux") + expect(b.boof).to.never.be.ok() + end) + end) + + describe("RemoveFromList", function() + it("should preserve immutability", function() + local a = {1, 2, 3} + local b = Immutable.RemoveFromList(a, 2) + + expect(b).never.to.equal(a) + end) + + it("should remove elements from the list", function() + local a = {1, 2, 3} + local b = Immutable.RemoveFromList(a, 2) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + end) + + describe("RemoveRangeFromList", function() + it("should preserve immutability", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 1) + + expect(b).never.to.equal(a) + end) + + it("should remove elements properly from the list", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 1) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + + it("should remove elements properly from the list", function() + local a = {1, 2, 3, 4, 5, 6} + local b = Immutable.RemoveRangeFromList(a, 1, 4) + + expect(b[1]).to.equal(5) + expect(b[2]).to.equal(6) + expect(b[3]).never.to.be.ok() + end) + + it("should remove elements properly from the list", function() + local a = {1, 2, 3, 4, 5, 6} + local b = Immutable.RemoveRangeFromList(a, 2, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(6) + expect(b[3]).never.to.be.ok() + end) + + it("should remove elements properly from the list", function() + local a = {1, 2, 3, 4, 5, 6, 7} + local b = Immutable.RemoveRangeFromList(a, 4, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + expect(b[4]).never.to.be.ok() + end) + + it("should not remove any elements when count is 0 or less", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 0) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + + local c = Immutable.RemoveRangeFromList(a, 2, -1) + expect(c[1]).to.equal(1) + expect(c[2]).to.equal(2) + expect(c[3]).to.equal(3) + end) + end) + + describe("RemoveValueFromList", function() + it("should preserve immutability", function() + local a = {1, 1, 1} + local b = Immutable.RemoveValueFromList(a, 1) + + expect(b).never.to.equal(a) + end) + + it("should remove all elements from the list", function() + local a = {1, 2, 2, 3} + local b = Immutable.RemoveValueFromList(a, 2) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/LuaErrorReporter.lua b/Client2018/content/internal/Common/Modules/Common/LuaErrorReporter.lua new file mode 100644 index 0000000..b2a9116 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/LuaErrorReporter.lua @@ -0,0 +1,285 @@ +--[[ + A simple observer for errors on the script context. + + By default, sends formatted error reports to the AnalyticsService. +]] +local ScriptContext = game:GetService("ScriptContext") +local RunService = game:GetService("RunService") +local Analytics = require(script.Parent.Analytics).new() + +-- flag dependencies +local influxSeriesName = settings():GetFVariable("LuaErrorsInfluxSeries") +local influxThrottlingPercentage = tonumber(settings():GetFVariable("LuaErrorsInfluxThrottling")) +local diagCounterName = settings():GetFVariable("LuaAppsDiagErrorCounter") +local isEnabled = settings():GetFFlag("UseNewGoogleAnalyticsImpl2") + +-- defaults +local defaultVerboseErrors = false +local defaultShouldReportDiag = true +local defaultShouldReportGoogleAnalytics = true +local defaultShouldReportInflux = false +local defaultCurrentApp = "Unknown" +local defaultQueuedReportTotalLimit = 30 + + +-- string formatting functions +local function createProductName(currentApp) + local versionString = RunService:GetRobloxVersion() + return string.format("%s-%s", currentApp, versionString) +end + +local function convertNewlinesToPipes(stack) + local rebuiltStack = "" + local first = true + for line in stack:gmatch("[^\r\n]+") do + if first then + rebuiltStack = line + first = false + else + rebuiltStack = rebuiltStack .. " | " .. line + end + end + return rebuiltStack +end + +local function removePlayerNameFromStack(stack) + stack = string.gsub(stack, "Players%.[^.]+%.", "Players..") + return stack +end + +local function printError(currentApp, message, stack, offendingScript) + local outMessages = { + "---- Unhandled Error Handler -----", + string.format("Current App<%s, %d> : \n%s\n", type(currentApp), #currentApp, currentApp), + string.format("Message<%s,%d> :\n%s\n", type(message), #message, message), + string.format("Stack<%s,%d> :\n%s", type(stack), #stack, stack), + string.format("Script<%s> :\n%s", type(offendingScript), offendingScript:GetFullName()), + "----------------------------------"} + + print(table.concat(outMessages, "\n")) +end + +-- analytics reporting functions +local function reportErrorToGA(currentApp, errorMsg, stack, value) + Analytics.GoogleAnalytics:trackEvent(currentApp, errorMsg, stack, value) +end + +local function reportErrorToInflux(currentApp, message, stack, offendingScript) + local additionalArgs = { + app = currentApp, + err = message, + stack = stack, + script = offendingScript:GetFullName() + } + + -- fire the error report + Analytics.InfluxDb:reportSeries(influxSeriesName, additionalArgs, influxThrottlingPercentage) +end + +local function reportErrorToDiag(currentApp) + -- these reports may be broken down further based on current app + Analytics.Diag:reportCounter(diagCounterName, 1) +end + +-- helper queue object +local function createErrorQueue() + -- NOTE - if error batching other types of reports becomes more important, + -- this object can be generalized to work for more errors, not just GA + local ErrorQueue = { + errors = {}, + totalErrors = 0, + totalKeys = 0, + countdown = defaultQueuedReportTotalLimit, + shouldCountdown = true, + } + + function ErrorQueue:addError(currentApp, message, stack) + local key = string.format("%s | %s | %s", currentApp, message, stack) + if not self.errors[key] then + self.errors[key] = { + app = currentApp, + message = message, + stack = stack, + value = 1 } + self.totalKeys = self.totalKeys + 1 + else + self.errors[key].value = self.errors[key].value + 1 + end + + self.totalErrors = self.totalErrors + 1 + end + + function ErrorQueue:isReadyToReport() + -- NOTE - GA has limits on how many reports that it will accept at a time. + -- According to : https://developers.google.com/analytics/devguides/config/mgmt/v3/limits-quotas + -- the Collection API is limited to 10 queries / second per IP Address + return self.totalKeys > 10 or + self.totalErrors > defaultQueuedReportTotalLimit or + self.countdown <= 0 + end + + function ErrorQueue:reportAllErrors() + -- copy the error queue and instantly clear it out + local errors = {} + for k, v in pairs(self.errors) do + errors[k] = v + end + + self.errors = {} + self.totalErrors = 0 + self.totalKeys = 0 + self.countdown = defaultQueuedReportTotalLimit + + -- report the errors + for _, errData in pairs(errors) do + reportErrorToGA(errData.app, errData.message, errData.stack, errData.value) + end + end + + function ErrorQueue:startTimer() + spawn(function() + while self.shouldCountdown do + self.countdown = self.countdown - 1 + if self:isReadyToReport() then + self:reportAllErrors() + end + wait(1.0) + end + end) + end + + function ErrorQueue:stopTimer() + self.shouldCountdown = false + end + + return ErrorQueue +end + + + +local LuaErrorReporter = {} +LuaErrorReporter.__index = LuaErrorReporter + +-- observedSignal : the Signal to listen for errors on +function LuaErrorReporter.new(observedSignal) + -- sanitize input + if not observedSignal then + observedSignal = ScriptContext.Error + end + + -- _isInstance : (boolean) simple flag to identify that this object was created with new() + -- _verbose : (boolean) when true, prints out debug information before sending error reports + -- _shouldReportDiag : (boolean) when true, increments a counter of total errors in Diag + -- _shouldReportGoogleAnalytics : (boolean) when true, reports the error to GoogleAnalytics + -- _shouldReportInflux : (boolean) when true, reports the error to InfluxDb + -- _currentScreen : (string) the name of the screen that is currently presented to the user + -- _signalConnectionToken : (RBXScriptConnection) a token issued when connecting to the Error signal + -- _reportQueueGA : (ErrorQueue) + local instance = { + _isInstance = true, + _verbose = defaultVerboseErrors, + _signalConnectionToken = nil, + _shouldReportDiag = defaultShouldReportDiag, + _shouldReportGoogleAnalytics = defaultShouldReportGoogleAnalytics, + _shouldReportInflux = defaultShouldReportInflux, + _currentApp = defaultCurrentApp, + _reportQueueGA = createErrorQueue(), + } + setmetatable(instance, LuaErrorReporter) + + -- connect a listener for errors on the provided Signal. + instance._signalConnectionToken = observedSignal:Connect(function(message, stack, offendingScript) + + -- protect against endless chains of errors when actually reporting + local success, reportMessage = pcall(function() + instance:handleError(message, stack, offendingScript) + end) + + if not success then + warn(string.format("An error occurred while reporting an error : %s", reportMessage)) + end + end) + + -- the BindToClose function does not play nicely with Studio. + if not RunService:IsStudio() then + -- BindToClose has about a 30 second timeout before the datamodel will kill any running scripts, + -- but this function will only need to fire off, at most, 9 http requests in parallel. + -- And since we're not binding any callbacks to these http requests, it's fine. + game:BindToClose(function() + instance:delete() + end) + end + + return instance +end + +function LuaErrorReporter:delete() + -- we're cleaning up this crash observer, disconnect from the Signal + self._signalConnectionToken:Disconnect() + + -- when the game closes down, send off all the remaining reports left in the queue + self._reportQueueGA:reportAllErrors() + self._reportQueueGA.shouldCountdown = false +end + +-- appName : (string) the english, human readable name of the current app that is hosting the lua app +function LuaErrorReporter:setCurrentApp(appName) + if type(appName) ~= "string" then + error("appName must be a string") + end + + self._currentApp = appName +end + +function LuaErrorReporter:startQueueTimers() + self._reportQueueGA:startTimer() +end + +function LuaErrorReporter:stopQueueTimers() + self._reportQueueGA:stopTimer() +end + +-- message : (string) the message passed from the error() call +-- stack : (string) the stack trace +-- offendingScript : (LuaSourceContainer) the specific script that threw the error +function LuaErrorReporter:handleError(message, stack, offendingScript) + -- NOTE - offendingScript is intended to show where in the workspace the error originated. + -- It will not be useful for the Lua Apps as all files originate out of the ***StarterScript.lua + + -- if the fast flag isn't on yet, escape + if not isEnabled then + return + end + + -- make a descriptive name to categorize the errors under- : - widthInPixels then + if LuaUseUtf8TextTruncation then + -- A binary search may be more efficient + local lastText = "" + for _, stopIndex in utf8.graphemes(text) do + local newText = string.sub(text, 1, stopIndex) .. overflowMarker + if Text.GetTextWidth(newText, font, fontSize) > widthInPixels then + return lastText + end + lastText = newText + end + else + for len = #text, 1, -1 do + local newText = string.sub(text, 1, len) .. overflowMarker + if Text.GetTextWidth(newText, font, fontSize) <= widthInPixels then + return newText + end + end + end + else -- No truncation needed + return text + end + + return "" +end + +function Text.TruncateTextLabel(textLabel, overflowMarker) + textLabel.Text = Text.Truncate(textLabel.Text, textLabel.Font, + textLabel.TextSize, textLabel.AbsoluteSize.X, overflowMarker) +end + +-- Remove whitespace from the beginning and end of the string +function Text.Trim(str) + if type(str) ~= "string" then + error(string.format("Text.Trim called on non-string type %s.", type(str)), 2) + end + return (str:gsub("^%s*(.-)%s*$", "%1")) +end + +-- Remove whitespace from the end of the string +function Text.RightTrim(str) + if type(str) ~= "string" then + error(string.format("Text.RightTrim called on non-string type %s.", type(str)), 2) + end + return (str:gsub("%s+$", "")) +end + +-- Remove whitespace from the beginning of the string +function Text.LeftTrim(str) + if type(str) ~= "string" then + error(string.format("Text.LeftTrim called on non-string type %s.", type(str)), 2) + end + return (str:gsub("^%s+", "")) +end + +-- Replace multiple whitespace with one; remove leading and trailing whitespace +function Text.SpaceNormalize(str) + if type(str) ~= "string" then + error(string.format("Text.SpaceNormalize called on non-string type %s.", type(str)), 2) + end + return (str:gsub("%s+", " "):gsub("^%s+" , ""):gsub("%s+$" , "")) +end + +-- Splits a string by the provided pattern into a table. The pattern is interpreted as plain text. +function Text.Split(str, pattern) + if type(str) ~= "string" then + error(string.format("Text.Split called on non-string type %s.", type(str)), 2) + elseif type(pattern) ~= "string" then + error(string.format("Text.Split called with a pattern that is non-string type %s.", type(pattern)), 2) + elseif pattern == "" then + error("Text.Split called with an empty pattern.", 2) + end + + local result = {} + local currentPosition = 1 + + while true do + local patternStart, patternEnd = string.find(str, pattern, currentPosition, true) + if not patternStart or not patternEnd then break end + table.insert(result, string.sub(str, currentPosition, patternStart - 1)) + currentPosition = patternEnd + 1 + end + + table.insert(result, string.sub(str, currentPosition, string.len(str))) + + return result +end + +return Text \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/Text.spec.lua b/Client2018/content/internal/Common/Modules/Common/Text.spec.lua new file mode 100644 index 0000000..4fbd73a --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/Text.spec.lua @@ -0,0 +1,410 @@ +return function() + local Text = require(script.Parent.Text) + + describe("GetTextBounds", function() + it("should return a bounds of padding width and font-size height when the string is empty", function() + local bounds = Text.GetTextBounds("", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000)) + expect(bounds.X).to.equal(Text._TEMP_PATCHED_PADDING.x) + expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y) + end) + it("should return the height and width of a string as one line with large bounds", function() + local bounds = Text.GetTextBounds("One Two Three", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000)) + expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y) + end) + + it("should return the height of the string as multiple lines with short bounds", function() + local bounds = Text.GetTextBounds("One Two Three Four", Enum.Font.SourceSans, 18, Vector2.new(32, 1000)) + expect(bounds.Y > 18).to.equal(true) + end) + end) + + describe("GetTextHeight", function() + it("should return height equal to font size when string is empty", function() + local height = Text.GetTextHeight("", Enum.Font.SourceSans, 18, 0) + expect(height).to.equal(18 + Text._TEMP_PATCHED_PADDING.y) + end) + end) + + describe("GetTextWidth", function() + it("should return width equal to 1 when string is empty", function() + local width = Text.GetTextWidth("", Enum.Font.SourceSans, 18, 18) + expect(width).to.equal(Text._TEMP_PATCHED_PADDING.x) + end) + end) + + describe("Truncate", function() + it("should return empty string", function() + local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 0, "...") + expect(emptyQuery).to.be.a("string") + expect(emptyQuery).to.equal("") + end) + + it("should return empty string for not empty box", function() + local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 50, "...") + expect(emptyQuery).to.be.a("string") + expect(emptyQuery).to.equal("") + end) + + it("should truncate with ...", function() + local reallyLongQuery = Text.Truncate( + "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100, "...") + expect(reallyLongQuery).to.equal("One Two Thre...") + end) + + it("should truncate without a ...", function() + local reallyLongQueryNoOverflowMarker = Text.Truncate( + "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100) + expect(reallyLongQueryNoOverflowMarker).to.equal("One Two Three ") + end) + + it("should not truncate", function() + local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, 100) + expect(shouldFitQuery).to.equal("One Two") + end) + + it("should not truncate, off by one check", function() + local oneCharQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 100) + expect(oneCharQuery).to.equal("O") + end) + + it("should truncate, off by one check", function() + local oneCharNoRoomQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 0) + expect(oneCharNoRoomQuery).to.equal("") + end) + + it("should perform a negative width check", function() + local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, -100, "...") + expect(shouldFitQuery).to.equal("") + end) + + it("should truncate long graphemes properly", function() + -- 11-byte rainbow flag grapheme + -- Flag, zero-space-joiner, rainbow + local rainbowFlag = utf8.char(127987) .. utf8.char(8205) .. utf8.char(127752) + local oneFlagWithinLimit = Text.Truncate( + rainbowFlag, Enum.Font.SourceSans, 18, 100, "...") + expect(oneFlagWithinLimit).to.equal(rainbowFlag) + + local twoRainbowFlags = rainbowFlag .. rainbowFlag + local twoFlagsAreFine = Text.Truncate( + twoRainbowFlags, Enum.Font.SourceSans, 18, 100, "...") + expect(twoFlagsAreFine).to.equal(twoRainbowFlags) + + local fourRainbowFlags = twoRainbowFlags .. twoRainbowFlags + local fourFlagsIsTooLong = Text.Truncate( + fourRainbowFlags, Enum.Font.SourceSans, 18, 100, "...") + expect(fourFlagsIsTooLong).to.equal(twoRainbowFlags .. "...") + end) + end) + + describe("TruncateTextLabel", function() + it("should use text label attributes to truncate text", function() + local screenGui = Instance.new("ScreenGui") + local textLabel = Instance.new("TextLabel") + textLabel.Size = UDim2.new(0, 100, 0, 32) + textLabel.Text = "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve" + textLabel.Font = Enum.Font.SourceSans + textLabel.TextSize = 18 + textLabel.Parent = screenGui + Text.TruncateTextLabel(textLabel) + + expect(textLabel.Text).to.equal("One Two Three ") + end) + end) + + + describe("TrimString", function() + it("Should trim the string properly", function() + local trimmedInput = Text.Trim("") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly", function() + local trimmedInput = Text.Trim(" ") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly", function() + local trimmedInput = Text.Trim("ab") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly", function() + local trimmedInput = Text.Trim(" ab ") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly", function() + local trimmedInput = Text.Trim(" a b ") + local expected = "a b" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly", function() + local trimmedInput = Text.Trim("\r\n\t\f a\r\n\t\f ") + local expected = "a" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string with unicode characters properly", function() + local trimmedInput = Text.Trim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly", function() + local trimmedInput = Text.Trim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly", function() + local trimmedInput = Text.Trim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n") + local expected = "😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + end) + + + describe("RightTrimString", function() + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim("") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim(" ") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim("ab") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim(" ab ") + local expected = " ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim(" a b ") + local expected = " a b" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim("\r\n\t\f a\r\n\t\f ") + local expected = "\r\n\t\f a" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string with unicode characters properly", function() + local trimmedInput = Text.RightTrim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ") + local expected = " 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly", function() + local trimmedInput = Text.RightTrim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n") + local expected = "\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + end) + + + describe("LeftTrimString", function() + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim("") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim(" ") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim("ab") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim(" ab ") + local expected = "ab " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim(" a b ") + local expected = " a b " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim("\r\n\t\f a\r\n\t\f ") + local expected = "a\r\n\t\f " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string with unicode characters properly", function() + local trimmedInput = Text.LeftTrim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n") + local expected = "😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n" + expect(trimmedInput).to.equal(expected) + end) + end) + + + describe("SpaceNormalize", function() + it("should remove multiple spaces between words", function() + local a = "This is not a normal sentence." + + expect(Text.SpaceNormalize(a)).to.equal("This is not a normal sentence.") + end) + + it("should remove leading and trailing whitespace", function() + local a = " SpaceTabSpaceTab " + + expect(Text.SpaceNormalize(a)).to.equal("SpaceTabSpaceTab") + end) + + it("should not change a string with no whitespace", function() + local a = "There'sNo%Whit.e\\space--InThis." + + expect(Text.SpaceNormalize(a)).to.equal(a) + end) + + it("should remove all whitespace in a string that is nothing but whitespace", function() + local a = " " + + expect(Text.SpaceNormalize(a)).to.equal("") + end) + + it("should handle the case where the string is empty", function() + local a = "" + + expect(Text.SpaceNormalize(a)).to.equal(a) + end) + + it("should throw an error if called an a non-string type", function() + local a = { first = 1, second = 2 } + + expect(function() + Text.SpaceNormalize(a) + end).to.throw() + end) + end) + + + describe("Split", function() + local function tableEquals(tb1, tb2) + local tables = { tb1, tb2 } + + for _,tb in ipairs(tables) do + for key in pairs(tb) do + if tb1[key] ~= tb2[key] then + return false + end + end + end + + return true + end + + it("should return the correct table for your standard use case", function() + local a = "this,is,comma,separated" + local pattern = "," + local expectedResult = { + [1] = "this", + [2] = "is", + [3] = "comma", + [4] = "separated", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should not remove whitespace", function() + local a = " SpaceTab , , Space" + local pattern = "," + local expectedResult = { + [1] = " SpaceTab ", + [2] = " ", + [3] = " Space", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should treat regular expressions as plain text", function() + local a = "Notyour^%s+normalstring.Thisisasecondsentence." + local b = "." + local c = "^%s+" + local d = "%A" + + local expectedB = { + [1] = "Notyour^%s+normalstring", + [2] = "Thisisasecondsentence", + [3] = "", + } + local expectedC = { + [1] = "Notyour", + [2] = "normalstring.Thisisasecondsentence." + } + local expectedD = { + [1] = "Notyour^%s+normalstring.Thisisasecondsentence." + } + + expect(tableEquals(Text.Split(a, b), expectedB)).to.equal(true) + expect(tableEquals(Text.Split(a, c), expectedC)).to.equal(true) + expect(tableEquals(Text.Split(a, d), expectedD)).to.equal(true) + end) + + it("should work when pattern is not in string", function() + local a = "The pattern you are looking for does not exist." + local pattern = "," + local expectedResult = { + [1] = "The pattern you are looking for does not exist.", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should work when called on an empty string", function() + local a = "" + local pattern = "," + local expectedResult = { + [1] = "", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should throw an error if called on an empty pattern", function() + local a = "The pattern definitely doesn't exist here." + local pattern = "" + + expect(function() + Text.Split(a, pattern) + end).to.throw() + end) + + it("should throw an error if called an a non-string type", function() + local a = { first = 1, second = 2 } + local b = "an actual string" + + expect(function() + Text.Split(a, b) + end).to.throw() + + expect(function() + Text.Split(b, a) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/memoize.lua b/Client2018/content/internal/Common/Modules/Common/memoize.lua new file mode 100644 index 0000000..261dcd0 --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/memoize.lua @@ -0,0 +1,68 @@ +--[[ + memoize creates a function as a wrapper that caches the last outputs of a function. + This is useful if you know that the function should return the same output every + time it is run with the same inputs. The function should only return an output, and + not have any side effects. These side effects are not cached. + + Without memoize's caching, even though the function ouputs the same values, the + memory locations of the values are different; tables made in the function, even if + they have the same values, won't be the same tables. + + memoize only caches the last set of inputs and ouputs. This means that it is only + helpful when the function is likely to be called with the same inputs multiple + times in a row. This is the case with most Roact use cases. + + Note that memoize only does a ** shallow check on table inputs ** . This means + that if the same table is input but the elements of the table are different then + it will be assumed that the table has not changed. + + In addition to all the previous warnings, memoize strips trailing nils. This means + that if foo is a memoized function and we call foo(), then foo(nil) will return a + cached value. This is opposed to how print handles input. print() only outputs a + new line, but print(nil) outputs "nil". This is because varargs can detect the + number of arguments passed in. So, be careful when using memoize with varargs. + Trailing nils will be stripped. + + The wrapper can take any number of inputs and give any number of outputs. + Leading and interspersed nils are handled gracefully. Trailing nils on the input + are stripped. +]] +local function captureSize(...) + return {...}, select("#", ...) +end + +local function memoize(func) + assert(type(func) == "function", "memoize requires a function to memoize") + + local lastArgs + local lastNumArgs + local lastOutput + local lastNumOutput + + return function(...) + local numArgs = select("#", ...) + + while numArgs > 0 and select(numArgs, ...) == nil do + numArgs = numArgs - 1 + end + + if numArgs ~= lastNumArgs then + lastArgs = {...} + lastNumArgs = numArgs + lastOutput, lastNumOutput = captureSize(func(...)) + return unpack(lastOutput, 1, lastNumOutput) + end + + for i = 1, lastNumArgs do + if select(i, ...) ~= lastArgs[i] then + lastArgs = {...} + lastOutput, lastNumOutput = captureSize(func(...)) + break + end + end + + return unpack(lastOutput, 1, lastNumOutput) + end +end + +return memoize \ No newline at end of file diff --git a/Client2018/content/internal/Common/Modules/Common/memoize.spec.lua b/Client2018/content/internal/Common/Modules/Common/memoize.spec.lua new file mode 100644 index 0000000..5ae2b6e --- /dev/null +++ b/Client2018/content/internal/Common/Modules/Common/memoize.spec.lua @@ -0,0 +1,213 @@ +return function() + local memoize = require(script.Parent.memoize) + + describe("memoize", function() + it("should handle arity 0", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + expect(identity()).to.equal(nil) + expect(identity(nil)).to.equal(nil) + expect(identity(nil, nil)).to.equal(nil) + expect(callCount).to.equal(1) + end) + + it("should handle arity 1", function() + local callCount = 0 + local identity = memoize(function(a) + callCount = callCount + 1 + return a + end) + + expect(identity(5)).to.equal(5) + expect(identity(5)).to.equal(5) + expect(callCount).to.equal(1) + + expect(identity(6)).to.equal(6) + expect(callCount).to.equal(2) + + expect(identity(5)).to.equal(5) + expect(callCount).to.equal(3) + end) + + it("should handle arity 2", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + expect(callCount).to.equal(1) + + a, b = identity(6, 5) + expect(a).to.equal(6) + expect(b).to.equal(5) + + expect(callCount).to.equal(2) + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + expect(callCount).to.equal(3) + end) + + it("should handle mixed arity", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + expect(callCount).to.equal(1) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b = identity() + expect(a).to.equal(nil) + expect(b).to.equal(nil) + + a, b = identity() + expect(a).to.equal(nil) + expect(b).to.equal(nil) + + expect(callCount).to.equal(3) + end) + + it("should handle trailing nils", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(5, nil) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + expect(callCount).to.equal(1) + + a, b = identity(7) + expect(a).to.equal(7) + expect(b).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + expect(callCount).to.equal(3) + end) + + it("should handle leading nils", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(nil, 7) + expect(a).to.equal(nil) + expect(b).to.equal(7) + + a, b = identity(nil, 7) + expect(a).to.equal(nil) + expect(b).to.equal(7) + + expect(callCount).to.equal(1) + + a, b = identity(7) + expect(a).to.equal(7) + expect(b).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b = identity(nil, 7) + expect(a).to.equal(nil) + expect(b).to.equal(7) + + expect(callCount).to.equal(3) + end) + + it("should handle interspersed nils", function() + local callCount = 0 + local identity = memoize(function(a, b, c, d) + callCount = callCount + 1 + return a, b, c, d + end) + + local a, b, c, d + + a, b, c, d = identity(7, nil, 7, nil) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(7) + expect(d).to.equal(nil) + + -- Trailing nils can affect how interspersed nils are handled + a, b, c, d = identity(7, nil, 7) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(7) + expect(d).to.equal(nil) + + expect(callCount).to.equal(1) + + a, b, c, d = identity(7, nil, nil, nil) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(nil) + expect(d).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b, c, d = identity(7, nil, 7, nil) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(7) + expect(d).to.equal(nil) + + expect(callCount).to.equal(3) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/.luacheckrc b/Client2018/content/internal/LuaApp/.luacheckrc new file mode 100644 index 0000000..f295c4b --- /dev/null +++ b/Client2018/content/internal/LuaApp/.luacheckrc @@ -0,0 +1,67 @@ +stds.roblox = { + read_globals = { + game = { + other_fields = true, + }, + + -- Roblox globals + "script", + + -- Extra functions + "tick", "warn", "spawn", "delay", + "wait", "settings", "UserSettings", "typeof", + + -- Types + "Vector2", "Vector3", + "Color3", + "UDim", "UDim2", + "Ray", + "Rect", + "CFrame", + "Enum", + "Instance", + "TweenInfo", + "Random", + "NumberRange", + "NumberSequence", + "NumberSequenceKeypoint", + "ColorSequence", + "BrickColor", + } +} + +stds.testez = { + read_globals = { + "describe", + "it", "itFOCUS", "itSKIP", + "FOCUS", "SKIP", "HACK_NO_XPCALL", + "expect", + } +} + +ignore = { + "212", -- unused arguments + "421", -- shadowing local variable + "422", -- shadowing argument + "431", -- shadowing upvalue + "432", -- shadowing upvalue argument +} + +std = "lua51+roblox" + +files["**/*.spec.lua"] = { + std = "+testez", + ignore = { "631" }, --Line is too long +} + +files["**/*Locale.lua"] = { + ignore = { "631" }, --Line is too long +} + +files["**/Locales/*.lua"] = { + ignore = { "631" }, --Line is too long +} + +files["**/Legacy/AvatarEditor/*.lua"] = { + ignore = { "121", "122", "211", "213", "612", "614", "631", }, -- Bunch of warnings for legacy code +} diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddGameSortContents.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddGameSortContents.lua new file mode 100644 index 0000000..1c9c37a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddGameSortContents.lua @@ -0,0 +1,16 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +--[[ + { + sort : String , + gameSortContents : table [] , + } +]] + +return Action(script.Name, function(sortName, gameSortContents) + return { + sort = sortName, + gameSortContents = gameSortContents + } +end) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddGameSorts.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddGameSorts.lua new file mode 100644 index 0000000..110dbf5 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddGameSorts.lua @@ -0,0 +1,29 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +--[[ + Passes a table that looks like this... + sorts : { + 1 : { + timeOptionsAvailable : false , + name : Popular , + isDefaultSort : true , + token : 1 , + numberOfRows : 0 , + genreOptionsAvailable : true , + }, + 2 : { + timeOptionsAvailable : false , + name : TopRated , + isDefaultSort : true , + token : 8 , + numberOfRows : 0 , + genreOptionsAvailable : true , + }, {...}, ... } +]] + +return Action(script.Name, function(sortsTable) + return { + sorts = sortsTable + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddGames.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddGames.lua new file mode 100644 index 0000000..7708499 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddGames.lua @@ -0,0 +1,28 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + + +--[[ + Passes a table that looks like this... { universeId = { gameData }, ... } + + { + "149757" : { + universeId : 149757 , + imageToken : 70395446 , + totalDownVotes : 0 , + placeId : 70395446 , + name : test , + totalUpVotes : 0 , + creatorId : 22915773 , + playerCount : 0 , + creatorName : Raeglyn , + creatorType : User + }, {...}, ... + } +]] + +return Action(script.Name, function(games) + return { + games = games + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddPlaceIdsToUniverseIds.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddPlaceIdsToUniverseIds.lua new file mode 100644 index 0000000..90dd579 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddPlaceIdsToUniverseIds.lua @@ -0,0 +1,13 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + + +--[[ + Passes a table that looks like this... { placeId = universeId, ... } +]] + +return Action(script.Name, function(placeIdsToUniverseIds) + return { + placeIdsToUniverseIds = placeIdsToUniverseIds + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddUser.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddUser.lua new file mode 100644 index 0000000..707c0f8 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddUser.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(user) + return { + user = user + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddUsers.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddUsers.lua new file mode 100644 index 0000000..7a2f564 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AddUsers.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(users) + return { + users = users + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AppendSearchInGames.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AppendSearchInGames.lua new file mode 100644 index 0000000..617fd61 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/AppendSearchInGames.lua @@ -0,0 +1,16 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +--[[ + { + searchUuid : number + searchesInGames : table [] (SearchInGames model), + } +]] + +return Action(script.Name, function(searchUuid, searchInGames) + return { + searchUuid = searchUuid, + searchInGames = searchInGames + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ApplyNavigateBack.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ApplyNavigateBack.lua new file mode 100644 index 0000000..040c7d6 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ApplyNavigateBack.lua @@ -0,0 +1,11 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(timeout) + assert(type(timeout) == "nil" or type(timeout) == "number", + "NavigateBack action expects timeout to be nil or a number") + + return { + timeout = timeout, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ApplyNavigateBack.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ApplyNavigateBack.spec.lua new file mode 100644 index 0000000..5c6d11c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ApplyNavigateBack.spec.lua @@ -0,0 +1,24 @@ +return function() + local ApplyNavigateBack = require(script.Parent.ApplyNavigateBack) + + it("should assert if given a non-nil non-number for navLockEndTime", function() + ApplyNavigateBack(nil) + ApplyNavigateBack(0) + + expect(function() + ApplyNavigateBack("Blargle!") + end).to.throw() + + expect(function() + ApplyNavigateBack({}) + end).to.throw() + + expect(function() + ApplyNavigateBack(false) + end).to.throw() + + expect(function() + ApplyNavigateBack(function() end) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ApplyNavigateToRoute.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ApplyNavigateToRoute.lua new file mode 100644 index 0000000..610a8ec --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ApplyNavigateToRoute.lua @@ -0,0 +1,13 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(route, timeout) + assert(type(route) == "table", "NavigateToRoute action expects route to be a table") + assert(type(timeout) == "nil" or type(timeout) == "number", + "NavigateToRoute action expects timeout to be nil or a number") + + return { + route = route, + timeout = timeout, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ApplyNavigateToRoute.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ApplyNavigateToRoute.spec.lua new file mode 100644 index 0000000..21d1ce2 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ApplyNavigateToRoute.spec.lua @@ -0,0 +1,48 @@ +return function() + local ApplyNavigateToRoute = require(script.Parent.ApplyNavigateToRoute) + + it("should assert if given a non-table for route", function() + ApplyNavigateToRoute({}) + + expect(function() + ApplyNavigateToRoute(nil) + end).to.throw() + + expect(function() + ApplyNavigateToRoute("Blargle!") + end).to.throw() + + expect(function() + ApplyNavigateToRoute(false) + end).to.throw() + + expect(function() + ApplyNavigateToRoute(0) + end).to.throw() + + expect(function() + ApplyNavigateToRoute(function() end) + end).to.throw() + end) + + it("should assert if given a non-nil non-number for navLockEndTime", function() + ApplyNavigateToRoute({}, nil) + ApplyNavigateToRoute({}, 0) + + expect(function() + ApplyNavigateToRoute({}, "Blargle!") + end).to.throw() + + expect(function() + ApplyNavigateToRoute({}, {}) + end).to.throw() + + expect(function() + ApplyNavigateToRoute({}, false) + end).to.throw() + + expect(function() + ApplyNavigateToRoute({}, function() end) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/EquipAsset.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/EquipAsset.lua new file mode 100644 index 0000000..926c3fc --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/EquipAsset.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("EquipAsset", function(assetType, assetId) + return { + assetType = assetType, + assetId = assetId + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/EquipAsset.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/EquipAsset.spec.lua new file mode 100644 index 0000000..ad89eda --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/EquipAsset.spec.lua @@ -0,0 +1,23 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local EquipAsset = require(Modules.LuaApp.Actions.EquipAsset) + + describe("Action EquipAsset", function() + it("should return a correct action name", function() + expect(EquipAsset.name).to.equal("EquipAsset") + end) + it("should return a correct action type name", function() + local action = EquipAsset("Hat", 1) + expect(action.type).to.equal(EquipAsset.name) + end) + it("should return a EquipAsset action with the correct status", function() + local action = EquipAsset("Hat", 1) + + expect(action).to.be.a("table") + expect(action.assetType).to.be.a("string") + expect(action.assetType).to.equal("Hat") + expect(action.assetId).to.be.a("number") + expect(action.assetId).to.equal(1) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/RemoveSearchInGames.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/RemoveSearchInGames.lua new file mode 100644 index 0000000..0a115ac --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/RemoveSearchInGames.lua @@ -0,0 +1,15 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +--[[ + { + searchUuid : number + searchesInGames : table [] (SearchInGames model), + } +]] + +return Action(script.Name, function(searchUuid) + return { + searchUuid = searchUuid + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/RemoveUser.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/RemoveUser.lua new file mode 100644 index 0000000..f8c51ed --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/RemoveUser.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(userId) + return { + userId = userId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ResetCategory.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ResetCategory.lua new file mode 100644 index 0000000..4a45ba2 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ResetCategory.lua @@ -0,0 +1,6 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("ResetCategory", function() + return {} +end) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ResetCategory.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ResetCategory.spec.lua new file mode 100644 index 0000000..ada6b51 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ResetCategory.spec.lua @@ -0,0 +1,18 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local ResetCategory = require(Modules.LuaApp.Actions.ResetCategory) + + describe("Action ResetCategory", function() + it("should return a correct action name", function() + expect(ResetCategory.name).to.equal("ResetCategory") + end) + it("should return a correct action type name", function() + local action = ResetCategory() + expect(action.type).to.equal(ResetCategory.name) + end) + it("should return a ResetCategory action with the correct status", function() + local action = ResetCategory() + expect(action).to.be.a("table") + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ResetSearchesInGames.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ResetSearchesInGames.lua new file mode 100644 index 0000000..58df7a1 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ResetSearchesInGames.lua @@ -0,0 +1,6 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SelectCategory.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SelectCategory.lua new file mode 100644 index 0000000..7e0e7ad --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SelectCategory.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("SelectCategory", function(categoryIndex) + return { + categoryIndex = categoryIndex + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SelectCategory.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SelectCategory.spec.lua new file mode 100644 index 0000000..df86c06 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SelectCategory.spec.lua @@ -0,0 +1,21 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SelectCategory = require(Modules.LuaApp.Actions.SelectCategory) + + describe("Action SelectCategory", function() + it("should return a correct action name", function() + expect(SelectCategory.name).to.equal("SelectCategory") + end) + it("should return a correct action type name", function() + local action = SelectCategory(1) + expect(action.type).to.equal(SelectCategory.name) + end) + it("should return a SelectCategory action with the correct status", function() + local action = SelectCategory(1) + + expect(action).to.be.a("table") + expect(action.categoryIndex).to.be.a("number") + expect(action.categoryIndex).to.equal(1) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SelectCategoryTab.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SelectCategoryTab.lua new file mode 100644 index 0000000..506c0d7 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SelectCategoryTab.lua @@ -0,0 +1,11 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("SelectCategoryTab", function(categoryIndex, tabIndex, position) + return + { + categoryIndex = categoryIndex, + tabIndex = tabIndex, + position = position + } +end) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SelectCategoryTab.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SelectCategoryTab.spec.lua new file mode 100644 index 0000000..fdf2a97 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SelectCategoryTab.spec.lua @@ -0,0 +1,25 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SelectCategoryTab = require(Modules.LuaApp.Actions.SelectCategoryTab) + + describe("Action SelectCategoryTab", function() + it("should return a correct action name", function() + expect(SelectCategoryTab.name).to.equal("SelectCategoryTab") + end) + it("should return a correct action type name", function() + local action = SelectCategoryTab(1, 2, Vector2.new(0,0)) + expect(action.type).to.equal(SelectCategoryTab.name) + end) + it("should return a SelectCategoryTab action with the correct status", function() + local action = SelectCategoryTab(1, 2, Vector2.new(0,0)) + + expect(action).to.be.a("table") + expect(action.categoryIndex).to.be.a("number") + expect(action.categoryIndex).to.equal(1) + expect(action.tabIndex).to.be.a("number") + expect(action.tabIndex).to.equal(2) + expect(action.position).to.be.a("userdata") + expect(action.position).to.equal(Vector2.new(0,0)) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAssets.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAssets.lua new file mode 100644 index 0000000..b88cd44 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAssets.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("SetAssets", function(assets) + return + { + assets = assets + } +end) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAssets.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAssets.spec.lua new file mode 100644 index 0000000..2324ab6 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAssets.spec.lua @@ -0,0 +1,53 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetAssets = require(Modules.LuaApp.Actions.SetAssets) + + describe("Action SetAssets", function() + it("should return a correct action name", function() + expect(SetAssets.name).to.equal("SetAssets") + end) + it("should return a correct action type name", function() + local action = SetAssets({}) + expect(action.type).to.equal(SetAssets.name) + end) + it("should return a SetAssets action with the correct status", function() + local action = SetAssets({}) + + expect(action).to.be.a("table") + expect(action.assets).to.be.a("table") + expect(next(action.assets)).never.to.be.ok() + + action = SetAssets({ + ["Head"] = {1} + }) + + expect(action).to.be.a("table") + expect(action.assets).to.be.a("table") + + expect(action.assets["Head"]).to.be.a("table") + expect(action.assets["Head"][1]).to.equal(1) + + action = SetAssets({ + ["Hat"] = {2, 3, 4}, + ["Shirt"] = {5}, + ["Pants"] = {6}, + }) + + expect(action).to.be.a("table") + expect(action.assets).to.be.a("table") + + expect(action.assets["Head"]).never.to.be.ok() + + expect(action.assets["Hat"]).to.be.a("table") + expect(action.assets["Hat"][1]).to.equal(2) + expect(action.assets["Hat"][2]).to.equal(3) + expect(action.assets["Hat"][3]).to.equal(4) + + expect(action.assets["Shirt"]).to.be.a("table") + expect(action.assets["Shirt"][1]).to.equal(5) + + expect(action.assets["Pants"]).to.be.a("table") + expect(action.assets["Pants"][1]).to.equal(6) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarBodyType.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarBodyType.lua new file mode 100644 index 0000000..2351cb3 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarBodyType.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("SetAvatarBodyType", function(bodyType) + return { + bodyType = bodyType + } +end) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarBodyType.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarBodyType.spec.lua new file mode 100644 index 0000000..ea2ea7d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarBodyType.spec.lua @@ -0,0 +1,21 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetAvatarBodyType = require(Modules.LuaApp.Actions.SetAvatarBodyType) + + describe("Action SetAvatarBodyType", function() + it("should return a correct action name", function() + expect(SetAvatarBodyType.name).to.equal("SetAvatarBodyType") + end) + it("should return a correct action type name", function() + local action = SetAvatarBodyType(0.5) + expect(action.type).to.equal(SetAvatarBodyType.name) + end) + it("should return a SetAvatarBodyType action with the correct status", function() + local action = SetAvatarBodyType(0.5) + + expect(action).to.be.a("table") + expect(action.bodyType).to.be.a("number") + expect(action.bodyType).to.equal(0.5) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarEditorFullView.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarEditorFullView.lua new file mode 100644 index 0000000..17848ba --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarEditorFullView.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("SetAvatarEditorFullView", function(fullView) + return + { + fullView = fullView + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarEditorFullView.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarEditorFullView.spec.lua new file mode 100644 index 0000000..a537964 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarEditorFullView.spec.lua @@ -0,0 +1,27 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetAvatarEditorFullView = require(Modules.LuaApp.Actions.SetAvatarEditorFullView) + + describe("Action SetAvatarEditorFullView", function() + it("should return a correct action name", function() + expect(SetAvatarEditorFullView.name).to.equal("SetAvatarEditorFullView") + end) + it("should return a correct action type name", function() + local action = SetAvatarEditorFullView(true) + expect(action.type).to.equal(SetAvatarEditorFullView.name) + end) + it("should return a SetAvatarEditorFullView action with the correct status", function() + local action = SetAvatarEditorFullView(true) + + expect(action).to.be.a("table") + expect(action.fullView).to.be.a("boolean") + expect(action.fullView).to.equal(true) + + action = SetAvatarEditorFullView(false) + + expect(action).to.be.a("table") + expect(action.fullView).to.be.a("boolean") + expect(action.fullView).to.equal(false) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarHeadSize.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarHeadSize.lua new file mode 100644 index 0000000..5ad56c9 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarHeadSize.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("SetAvatarHeadSize", function(head) + return + { + head = head + } +end) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarHeadSize.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarHeadSize.spec.lua new file mode 100644 index 0000000..c4c8a26 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarHeadSize.spec.lua @@ -0,0 +1,21 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetAvatarHeadSize = require(Modules.LuaApp.Actions.SetAvatarHeadSize) + + describe("Action SetAvatarHeadSize", function() + it("should return a correct action name", function() + expect(SetAvatarHeadSize.name).to.equal("SetAvatarHeadSize") + end) + it("should return a correct action type name", function() + local action = SetAvatarHeadSize(0.95) + expect(action.type).to.equal(SetAvatarHeadSize.name) + end) + it("should return a SetAvatarHeadSize action with the correct status", function() + local action = SetAvatarHeadSize(0.95) + + expect(action).to.be.a("table") + expect(action.head).to.be.a("number") + expect(action.head).to.equal(0.95) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarHeight.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarHeight.lua new file mode 100644 index 0000000..5a00bdd --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarHeight.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("SetAvatarHeight", function(height) + return + { + height = height + } +end) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarHeight.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarHeight.spec.lua new file mode 100644 index 0000000..965c206 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarHeight.spec.lua @@ -0,0 +1,21 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetAvatarHeight = require(Modules.LuaApp.Actions.SetAvatarHeight) + + describe("Action SetAvatarHeight", function() + it("should return a correct action name", function() + expect(SetAvatarHeight.name).to.equal("SetAvatarHeight") + end) + it("should return a correct action type name", function() + local action = SetAvatarHeight(0.97) + expect(action.type).to.equal(SetAvatarHeight.name) + end) + it("should return a SetAvatarHeight action with the correct status", function() + local action = SetAvatarHeight(0.97) + + expect(action).to.be.a("table") + expect(action.height).to.be.a("number") + expect(action.height).to.equal(0.97) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarProportion.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarProportion.lua new file mode 100644 index 0000000..81979ef --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarProportion.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("SetAvatarProportion", function(proportion) + return { + proportion = proportion + } +end) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarProportion.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarProportion.spec.lua new file mode 100644 index 0000000..d7c6553 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarProportion.spec.lua @@ -0,0 +1,21 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetAvatarProportion = require(Modules.LuaApp.Actions.SetAvatarProportion) + + describe("Action SetAvatarProportion", function() + it("should return a correct action name", function() + expect(SetAvatarProportion.name).to.equal("SetAvatarProportion") + end) + it("should return a correct action type name", function() + local action = SetAvatarProportion(0.3) + expect(action.type).to.equal(SetAvatarProportion.name) + end) + it("should return a SetAvatarProportion action with the correct status", function() + local action = SetAvatarProportion(0.3) + + expect(action).to.be.a("table") + expect(action.proportion).to.be.a("number") + expect(action.proportion).to.equal(0.3) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarType.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarType.lua new file mode 100644 index 0000000..e7e2f94 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarType.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("SetAvatarType", function(avatarType) + return + { + avatarType = avatarType + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarType.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarType.spec.lua new file mode 100644 index 0000000..45ec7ef --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarType.spec.lua @@ -0,0 +1,27 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetAvatarType = require(Modules.LuaApp.Actions.SetAvatarType) + + describe("Action SetAvatarType", function() + it("should return a correct action name", function() + expect(SetAvatarType.name).to.equal("SetAvatarType") + end) + it("should return a correct action type name", function() + local action = SetAvatarType("R6") + expect(action.type).to.equal(SetAvatarType.name) + end) + it("should return a SetAvatarType action with the correct status", function() + local action = SetAvatarType("R6") + + expect(action).to.be.a("table") + expect(action.avatarType).to.be.a("string") + expect(action.avatarType).to.equal("R6") + + action = SetAvatarType("R15") + + expect(action).to.be.a("table") + expect(action.avatarType).to.be.a("string") + expect(action.avatarType).to.equal("R15") + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarWidth.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarWidth.lua new file mode 100644 index 0000000..9585b35 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarWidth.lua @@ -0,0 +1,10 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("SetAvatarWidth", function(width, depth) + return + { + width = width, + depth = depth + } +end) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarWidth.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarWidth.spec.lua new file mode 100644 index 0000000..8d47a50 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetAvatarWidth.spec.lua @@ -0,0 +1,23 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetAvatarWidth = require(Modules.LuaApp.Actions.SetAvatarWidth) + + describe("Action SetAvatarWidth", function() + it("should return a correct action name", function() + expect(SetAvatarWidth.name).to.equal("SetAvatarWidth") + end) + it("should return a correct action type name", function() + local action = SetAvatarWidth(0.7, 0.85) + expect(action.type).to.equal(SetAvatarWidth.name) + end) + it("should return a SetAvatarWidth action with the correct status", function() + local action = SetAvatarWidth(0.7, 0.85) + + expect(action).to.be.a("table") + expect(action.width).to.be.a("number") + expect(action.width).to.equal(0.7) + expect(action.depth).to.be.a("number") + expect(action.depth).to.equal(0.85) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetBodyColors.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetBodyColors.lua new file mode 100644 index 0000000..7e48596 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetBodyColors.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("SetBodyColors", function(bodyColors) + return + { + bodyColors = bodyColors + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetBodyColors.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetBodyColors.spec.lua new file mode 100644 index 0000000..e37cac0 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetBodyColors.spec.lua @@ -0,0 +1,46 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetBodyColors = require(Modules.LuaApp.Actions.SetBodyColors) + + describe("Action SetBodyColors", function() + it("should return a correct action name", function() + expect(SetBodyColors.name).to.equal("SetBodyColors") + end) + it("should return a correct action type name", function() + local action = SetBodyColors({}) + expect(action.type).to.equal(SetBodyColors.name) + end) + it("should return a SetBodyColors action with the correct status", function() + local bodyColors = { + ["HeadColor"] = 194, + ["LeftArmColor"] = 0, + ["LeftLegColor"] = 100, + ["RightArmColor"] = 50, + ["RightLegColor"] = 150, + ["TorsoColor"] = 255, + } + local action = SetBodyColors(bodyColors) + + expect(action).to.be.a("table") + expect(action.bodyColors).to.be.a("table") + + expect(action.bodyColors["HeadColor"]).to.be.a("number") + expect(action.bodyColors["HeadColor"]).to.equal(194) + + expect(action.bodyColors["LeftArmColor"]).to.be.a("number") + expect(action.bodyColors["LeftArmColor"]).to.equal(0) + + expect(action.bodyColors["LeftLegColor"]).to.be.a("number") + expect(action.bodyColors["LeftLegColor"]).to.equal(100) + + expect(action.bodyColors["RightArmColor"]).to.be.a("number") + expect(action.bodyColors["RightArmColor"]).to.equal(50) + + expect(action.bodyColors["RightLegColor"]).to.be.a("number") + expect(action.bodyColors["RightLegColor"]).to.equal(150) + + expect(action.bodyColors["TorsoColor"]).to.be.a("number") + expect(action.bodyColors["TorsoColor"]).to.equal(255) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetConsoleMenuLevel.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetConsoleMenuLevel.lua new file mode 100644 index 0000000..362e62c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetConsoleMenuLevel.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("SetConsoleMenuLevel", function(menuLevel) + return { + menuLevel = menuLevel + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetConsoleMenuLevel.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetConsoleMenuLevel.spec.lua new file mode 100644 index 0000000..67ead1d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetConsoleMenuLevel.spec.lua @@ -0,0 +1,21 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetConsoleMenuLevel = require(Modules.LuaApp.Actions.SetConsoleMenuLevel) + + describe("Action SetConsoleMenuLevel", function() + it("should return a correct action name", function() + expect(SetConsoleMenuLevel.name).to.equal("SetConsoleMenuLevel") + end) + it("should return a correct action type name", function() + local action = SetConsoleMenuLevel(1) + expect(action.type).to.equal(SetConsoleMenuLevel.name) + end) + it("should return a SetConsoleMenuLevel action with the correct status", function() + local action = SetConsoleMenuLevel(1) + + expect(action).to.be.a("table") + expect(action.menuLevel).to.be.a("number") + expect(action.menuLevel).to.equal(1) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetDeviceOrientation.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetDeviceOrientation.lua new file mode 100644 index 0000000..7ce767a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetDeviceOrientation.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(deviceOrientation) + return { + deviceOrientation = deviceOrientation, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetDeviceOrientation.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetDeviceOrientation.spec.lua new file mode 100644 index 0000000..80887b9 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetDeviceOrientation.spec.lua @@ -0,0 +1,28 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetDeviceOrientation = require(Modules.LuaApp.Actions.SetDeviceOrientation) + local DeviceOrientationMode = require(Modules.LuaApp.DeviceOrientationMode) + + describe("Action SetDeviceOrientation", function() + it("should return a correct action name", function() + expect(SetDeviceOrientation.name).to.equal("SetDeviceOrientation") + end) + it("should return a correct action type name", function() + local action = SetDeviceOrientation(DeviceOrientationMode.Portrait) + expect(action.type).to.equal(SetDeviceOrientation.name) + end) + it("should return a SetDeviceOrientation action with the correct status", function() + local action = SetDeviceOrientation(DeviceOrientationMode.Landscape) + + expect(action).to.be.a("table") + expect(action.deviceOrientation).to.be.a("string") + expect(action.deviceOrientation).to.equal(DeviceOrientationMode.Landscape) + + action = SetDeviceOrientation(DeviceOrientationMode.Portrait) + + expect(action).to.be.a("table") + expect(action.deviceOrientation).to.be.a("string") + expect(action.deviceOrientation).to.equal(DeviceOrientationMode.Portrait) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetFormFactor.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetFormFactor.lua new file mode 100644 index 0000000..00fcd18 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetFormFactor.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(formFactor) + return { + formFactor = formFactor, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetFriendCount.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetFriendCount.lua new file mode 100644 index 0000000..e441e71 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetFriendCount.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(count) + return { + count = count, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGameSortContents.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGameSortContents.lua new file mode 100644 index 0000000..1c9c37a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGameSortContents.lua @@ -0,0 +1,16 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +--[[ + { + sort : String , + gameSortContents : table [] , + } +]] + +return Action(script.Name, function(sortName, gameSortContents) + return { + sort = sortName, + gameSortContents = gameSortContents + } +end) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGameSortStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGameSortStatus.lua new file mode 100644 index 0000000..62a4d35 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGameSortStatus.lua @@ -0,0 +1,16 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +--[[ + { + sortName : string + fetchStatus : RetrievalStatus, + } +]] + +return Action(script.Name, function(sortName, status) + return { + sortName = sortName, + status = status, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGameSortTokenFetchingStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGameSortTokenFetchingStatus.lua new file mode 100644 index 0000000..10aea43 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGameSortTokenFetchingStatus.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(sortCategory, fetchStatus) + return { + sortCategory = sortCategory, + fetchStatus = fetchStatus, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGameSortsInGroup.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGameSortsInGroup.lua new file mode 100644 index 0000000..9f3eceb --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGameSortsInGroup.lua @@ -0,0 +1,17 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +--[[ + Passes a table that looks like this... + { + groupId : "games", + sorts : {1, 6, 12, 13} + } +]] + +return Action(script.Name, function(groupId, sortsTable) + return { + groupId = groupId, + sorts = sortsTable + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGameThumbnails.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGameThumbnails.lua new file mode 100644 index 0000000..8fe3ad8 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGameThumbnails.lua @@ -0,0 +1,22 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + + +--[[ + Passes a table that looks like this : { universeId : {json}, ... } + + { + 26034470 : { + universeId : 26034470, + placeId : 70542190, + url : https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814, + final : true + }, {...}, ... + } + +]] +return Action(script.Name, function(thumbnailsTable) + return { + thumbnails = thumbnailsTable + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGamesPageDataStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGamesPageDataStatus.lua new file mode 100644 index 0000000..f59029d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGamesPageDataStatus.lua @@ -0,0 +1,11 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(status) + assert(type(status) == "string", + "SetGamesPageDataStatus action expects status to be an RetrievalStatus (string)") + + return { + status = status, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGamesPageDataStatus.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGamesPageDataStatus.spec.lua new file mode 100644 index 0000000..0218621 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetGamesPageDataStatus.spec.lua @@ -0,0 +1,11 @@ +return function() + local SetGamesPageDataStatus = require(script.Parent.SetGamesPageDataStatus) + + it("should assert if given a non-string for status", function() + SetGamesPageDataStatus("hello") + + expect(function() + SetGamesPageDataStatus(nil) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetHomePageDataStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetHomePageDataStatus.lua new file mode 100644 index 0000000..51b5d4d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetHomePageDataStatus.lua @@ -0,0 +1,11 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(status) + assert(type(status) == "string", + "SetHomePageDataStatus action expects status to be an RetrievalStatus (string)") + + return { + status = status, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetHomePageDataStatus.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetHomePageDataStatus.spec.lua new file mode 100644 index 0000000..a06469b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetHomePageDataStatus.spec.lua @@ -0,0 +1,11 @@ +return function() + local SetHomePageDataStatus = require(script.Parent.SetHomePageDataStatus) + + it("should assert if given a non-string for status", function() + SetHomePageDataStatus("hello") + + expect(function() + SetHomePageDataStatus(nil) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetLocalUserId.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetLocalUserId.lua new file mode 100644 index 0000000..cd1425d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetLocalUserId.lua @@ -0,0 +1,12 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(userId) + return { + userId = userId, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetNextTokenRefreshTime.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetNextTokenRefreshTime.lua new file mode 100644 index 0000000..31f0165 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetNextTokenRefreshTime.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(sortCategory, nextRefreshTime) + return { + sortCategory = sortCategory, + nextRefreshTime = nextRefreshTime, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetNotificationCount.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetNotificationCount.lua new file mode 100644 index 0000000..027edbe --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetNotificationCount.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(notificationCount) + return { + notificationCount = notificationCount, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetOutfit.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetOutfit.lua new file mode 100644 index 0000000..efc54fc --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetOutfit.lua @@ -0,0 +1,10 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("SetOutfit", function(assets, bodyColors) + return + { + assets = assets, + bodyColors = bodyColors + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetOutfit.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetOutfit.spec.lua new file mode 100644 index 0000000..1c6b8d9 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetOutfit.spec.lua @@ -0,0 +1,70 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetOutfit = require(Modules.LuaApp.Actions.SetOutfit) + + describe("Action SetOutfit", function() + it("should return a correct action name", function() + expect(SetOutfit.name).to.equal("SetOutfit") + end) + it("should return a correct action type name", function() + local action = SetOutfit({},{}) + expect(action.type).to.equal(SetOutfit.name) + end) + it("should return a SetOutfit action with the correct status", function() + local action = SetOutfit({},{}) + + expect(action).to.be.a("table") + expect(action.assets).to.be.a("table") + expect(next(action.assets)).never.to.be.ok() + expect(action.bodyColors).to.be.a("table") + expect(next(action.bodyColors)).never.to.be.ok() + + action = SetOutfit({ + ["Hat"] = {2, 3, 4}, + ["Shirt"] = {5}, + ["Pants"] = {6}, + }, { + ["HeadColor"] = 194, + ["LeftArmColor"] = 0, + ["LeftLegColor"] = 100, + ["RightArmColor"] = 50, + ["RightLegColor"] = 150, + ["TorsoColor"] = 255, + }) + + expect(action).to.be.a("table") + expect(action.assets).to.be.a("table") + + expect(action.assets["Hat"]).to.be.a("table") + expect(action.assets["Hat"][1]).to.equal(2) + expect(action.assets["Hat"][2]).to.equal(3) + expect(action.assets["Hat"][3]).to.equal(4) + + expect(action.assets["Shirt"]).to.be.a("table") + expect(action.assets["Shirt"][1]).to.equal(5) + + expect(action.assets["Pants"]).to.be.a("table") + expect(action.assets["Pants"][1]).to.equal(6) + + expect(action.bodyColors).to.be.a("table") + + expect(action.bodyColors["HeadColor"]).to.be.a("number") + expect(action.bodyColors["HeadColor"]).to.equal(194) + + expect(action.bodyColors["LeftArmColor"]).to.be.a("number") + expect(action.bodyColors["LeftArmColor"]).to.equal(0) + + expect(action.bodyColors["LeftLegColor"]).to.be.a("number") + expect(action.bodyColors["LeftLegColor"]).to.equal(100) + + expect(action.bodyColors["RightArmColor"]).to.be.a("number") + expect(action.bodyColors["RightArmColor"]).to.equal(50) + + expect(action.bodyColors["RightLegColor"]).to.be.a("number") + expect(action.bodyColors["RightLegColor"]).to.equal(150) + + expect(action.bodyColors["TorsoColor"]).to.be.a("number") + expect(action.bodyColors["TorsoColor"]).to.equal(255) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetPlatform.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetPlatform.lua new file mode 100644 index 0000000..08cc990 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetPlatform.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(platform) + return { + platform = platform + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetPlayabilityStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetPlayabilityStatus.lua new file mode 100644 index 0000000..5c6c218 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetPlayabilityStatus.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(universeId, playabilityStatus) + return { + universeId = universeId, + playabilityStatus = playabilityStatus, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetPreloading.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetPreloading.lua new file mode 100644 index 0000000..02a3051 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetPreloading.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(isPreloading) + return { + isPreloading = isPreloading, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetScales.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetScales.lua new file mode 100644 index 0000000..f330893 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetScales.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("SetScales", function(scales) + return + { + scales = scales + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetScales.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetScales.spec.lua new file mode 100644 index 0000000..cb03976 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetScales.spec.lua @@ -0,0 +1,46 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetScales = require(Modules.LuaApp.Actions.SetScales) + + describe("Action SetScales", function() + it("should return a correct action name", function() + expect(SetScales.name).to.equal("SetScales") + end) + it("should return a correct action type name", function() + local action = SetScales({}) + expect(action.type).to.equal(SetScales.name) + end) + it("should return a SetScales action with the correct status", function() + local scales = { + ["Height"] = 1.01, + ["Width"] = 0.70, + ["Depth"] = 0.85, + ["Head"] = 0.95, + ["BodyType"] = 0.30, + ["Proportion"] = 0.50 + } + local action = SetScales(scales) + + expect(action).to.be.a("table") + expect(action.scales).to.be.a("table") + + expect(action.scales["Height"]).to.be.a("number") + expect(action.scales["Height"]).to.equal(1.01) + + expect(action.scales["Width"]).to.be.a("number") + expect(action.scales["Width"]).to.equal(0.70) + + expect(action.scales["Depth"]).to.be.a("number") + expect(action.scales["Depth"]).to.equal(0.85) + + expect(action.scales["Head"]).to.be.a("number") + expect(action.scales["Head"]).to.equal(0.95) + + expect(action.scales["BodyType"]).to.be.a("number") + expect(action.scales["BodyType"]).to.equal(0.30) + + expect(action.scales["Proportion"]).to.be.a("number") + expect(action.scales["Proportion"]).to.equal(0.50) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetScreenSize.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetScreenSize.lua new file mode 100644 index 0000000..813563a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetScreenSize.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(screenSize) + return { + screenSize = screenSize + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetSearchInGames.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetSearchInGames.lua new file mode 100644 index 0000000..617fd61 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetSearchInGames.lua @@ -0,0 +1,16 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +--[[ + { + searchUuid : number + searchesInGames : table [] (SearchInGames model), + } +]] + +return Action(script.Name, function(searchUuid, searchInGames) + return { + searchUuid = searchUuid, + searchInGames = searchInGames + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetSearchInGamesStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetSearchInGamesStatus.lua new file mode 100644 index 0000000..83b755e --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetSearchInGamesStatus.lua @@ -0,0 +1,16 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +--[[ + { + searchUuid : number + fetchStatus : SearchRetrievalStatus, + } +]] + +return Action(script.Name, function(searchUuid, status) + return { + searchUuid = searchUuid, + status = status, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetTabBarVisible.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetTabBarVisible.lua new file mode 100644 index 0000000..21aa6e6 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetTabBarVisible.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(isVisible) + return { + isVisible = isVisible, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetTopBarHeight.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetTopBarHeight.lua new file mode 100644 index 0000000..a33e581 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetTopBarHeight.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(topBarHeight) + return { + topBarHeight = topBarHeight, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetUserIsFriend.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetUserIsFriend.lua new file mode 100644 index 0000000..6349176 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetUserIsFriend.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(userId, isFriend) + return { + userId = userId, + isFriend = isFriend, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetUserMembershipType.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetUserMembershipType.lua new file mode 100644 index 0000000..e6b18a9 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetUserMembershipType.lua @@ -0,0 +1,13 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(userId, membershipType) + return { + userId = userId, + membershipType = membershipType, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetUserPresence.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetUserPresence.lua new file mode 100644 index 0000000..491ee59 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetUserPresence.lua @@ -0,0 +1,10 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(userId, presence, lastLocation) + return { + userId = tostring(userId), + presence = presence, + lastLocation = lastLocation, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetUserThumbnail.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetUserThumbnail.lua new file mode 100644 index 0000000..ba34f06 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/SetUserThumbnail.lua @@ -0,0 +1,15 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Common = Modules.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(userId, image, thumbnailType, thumbnailSize) + return { + userId = userId, + image = image, + thumbnailType = thumbnailType, + thumbnailSize = thumbnailSize, + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ToggleAvatarEditorFullView.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ToggleAvatarEditorFullView.lua new file mode 100644 index 0000000..a6572ee --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ToggleAvatarEditorFullView.lua @@ -0,0 +1,6 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("ToggleAvatarEditorFullView", function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ToggleAvatarEditorFullView.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ToggleAvatarEditorFullView.spec.lua new file mode 100644 index 0000000..9fb19b4 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ToggleAvatarEditorFullView.spec.lua @@ -0,0 +1,18 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local ToggleAvatarEditorFullView = require(Modules.LuaApp.Actions.ToggleAvatarEditorFullView) + + describe("Action ToggleAvatarEditorFullView", function() + it("should return a correct action name", function() + expect(ToggleAvatarEditorFullView.name).to.equal("ToggleAvatarEditorFullView") + end) + it("should return a correct action type name", function() + local action = ToggleAvatarEditorFullView() + expect(action.type).to.equal(ToggleAvatarEditorFullView.name) + end) + it("should return a ToggleAvatarEditorFullView action with the correct status", function() + local action = ToggleAvatarEditorFullView() + expect(action).to.be.a("table") + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ToggleAvatarType.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ToggleAvatarType.lua new file mode 100644 index 0000000..a50623a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ToggleAvatarType.lua @@ -0,0 +1,6 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("ToggleAvatarType", function() + return {} +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ToggleAvatarType.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ToggleAvatarType.spec.lua new file mode 100644 index 0000000..a5f1e0c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/ToggleAvatarType.spec.lua @@ -0,0 +1,18 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local ToggleAvatarType = require(Modules.LuaApp.Actions.ToggleAvatarType) + + describe("Action ToggleAvatarType", function() + it("should return a correct action name", function() + expect(ToggleAvatarType.name).to.equal("ToggleAvatarType") + end) + it("should return a correct action type name", function() + local action = ToggleAvatarType() + expect(action.type).to.equal(ToggleAvatarType.name) + end) + it("should return a ToggleAvatarType action with the correct status", function() + local action = ToggleAvatarType() + expect(action).to.be.a("table") + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/UnequipAsset.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/UnequipAsset.lua new file mode 100644 index 0000000..07dd742 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/UnequipAsset.lua @@ -0,0 +1,10 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Action = require(Modules.Common.Action) + +return Action("UnequipAsset", function(assetType, assetId) + return + { + assetType = assetType, + assetId = assetId + } +end) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/UnequipAsset.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/UnequipAsset.spec.lua new file mode 100644 index 0000000..4e9b17b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Actions/UnequipAsset.spec.lua @@ -0,0 +1,23 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local UnequipAsset = require(Modules.LuaApp.Actions.UnequipAsset) + + describe("Action UnequipAsset", function() + it("should return a correct action name", function() + expect(UnequipAsset.name).to.equal("UnequipAsset") + end) + it("should return a correct action type name", function() + local action = UnequipAsset("Hat", 1) + expect(action.type).to.equal(UnequipAsset.name) + end) + it("should return a UnequipAsset action with the correct status", function() + local action = UnequipAsset("Hat", 1) + + expect(action).to.be.a("table") + expect(action.assetType).to.be.a("string") + expect(action.assetType).to.equal("Hat") + expect(action.assetId).to.be.a("number") + expect(action.assetId).to.equal(1) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/appStageLoaded.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/appStageLoaded.lua new file mode 100644 index 0000000..8d51084 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/appStageLoaded.lua @@ -0,0 +1,14 @@ +-- appStageLoaded : fires when the app loads and passes a milestone (otherwise known as an `appStage`) +-- eventContext : (string) the location or context in which the event is occurring. +-- appStage : (string) the name of the appStage that has been completed +return function(eventStreamImpl, eventContext, appStage) + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(appStage) == "string", "Expected appStage to be a string") + + local eventName = "appStageLoaded" + local additionalArgs = { + stage = appStage + } + + eventStreamImpl:setRBXEventStream(eventContext, eventName, additionalArgs) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/buttonClick.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/buttonClick.lua new file mode 100644 index 0000000..f730970 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/buttonClick.lua @@ -0,0 +1,20 @@ +-- buttonClick : fires when a button is pressed +-- eventContext : (string) the location or context in which the event is occurring. +-- buttonName : (string) the name of the pressed button +-- extraData : (optional, string) contextual info about the button, when multiple buttons have the same name. +return function(eventStreamImpl, eventContext, buttonName, extraData) + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(buttonName) == "string", "Expected buttonName to be a string") + + local eventName = "buttonClick" + local additionalArgs = { + btn = buttonName + } + + if extraData ~= nil then + assert(type(extraData) == "string", "Expected extraData to be a string") + additionalArgs.cstm = extraData + end + + eventStreamImpl:setRBXEventStream(eventContext, eventName, additionalArgs) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/formFieldValidation.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/formFieldValidation.lua new file mode 100644 index 0000000..b217fd8 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/formFieldValidation.lua @@ -0,0 +1,17 @@ +-- formFieldValidation : fires when a field is validated with local logic and an error is displayed to a user +-- eventContext : (string) the location or context in which the event is occurring. +-- field : (string) the name of the validated field. +-- errorText : (string) the error message displayed. +return function(eventStreamImpl, eventContext, field, errorText) + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(field) == "string", "Expected field to be a string") + assert(type(errorText) == "string", "Expected errorText to be a string") + + local eventName = "formFieldValidation" + local additionalArgs = { + field = field, + error = errorText + } + + eventStreamImpl:setRBXEventStream(eventContext, eventName, additionalArgs) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/gameDetailReferral.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/gameDetailReferral.lua new file mode 100644 index 0000000..8491905 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/gameDetailReferral.lua @@ -0,0 +1,42 @@ +local PlayerService = game:GetService("Players") + +-- gameDetailReferral : sent when a user clicks to navigate to a game detail page. +-- eventContext : (string) a cryptic value to specify where the referral happened. See below... +-- page : (string) the page where the event originated +-- totalItems : (number) the total number of items in the sort +-- placeId : (string) the placeId of the game being referred +-- isAd : (bool) is this a native ad +return function(eventStreamImpl, eventContext, page, totalItems, placeId, isAd) + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(page) == "string", "Expected page to be a string") + assert(type(totalItems) == "number", "Expected totalItems to be a number") + assert(type(placeId) == "number", "Expected placeId to be a number") + assert(type(isAd) == "boolean", "Expected isAd to be a bool") + + local eventName = "gameDetailReferral" + local userId = tostring(PlayerService.LocalPlayer.UserId) + local adValue = isAd and "yes" or "no" + + eventStreamImpl:setRBXEventStream(eventContext, eventName, { + pg = page, + tis = totalItems, + pid = placeId, + uid = userId, + ad = adValue, + }) +end + +-- possible values for eventContext include : +-- gamesort_SortFilter<1>_TimeFilter<1>_GenreFilter<1>_Position<1> +-- recommendation_Type<1>_Position<1> +-- gamesearch_Position<1> +-- home_SortFilter<1>_Position<1> +-- profile_Position<1> + +-- possible values for page include : +-- profile +-- games +-- gamesSeeAll +-- home +-- gameSearch +-- recommendation \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/gamesPageInteraction.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/gamesPageInteraction.lua new file mode 100644 index 0000000..75272ca --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/gamesPageInteraction.lua @@ -0,0 +1,42 @@ +local PlayerService = game:GetService("Players") + +-- gamesPageInteraction : sent when a user interacts with the games page +-- eventContext : (string) the location or context in which the event is occurring. +-- actionType : (string) the specific action performed by the user. +-- actionValue : (string) the id associated with the eventContext. +-- selectedIndex : (number) the index of the interacted object. +return function(eventStreamImpl, eventContext, actionType, actionValue, selectedIndex) + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(actionType) == "string", "Expected actionType to be a string") + assert(type(actionValue) == "string", "Expected actionValue to be a string") + assert(type(selectedIndex) == "number", "Expected selectedIndex to be a number") + + local eventName = "gamesPageInteraction" + local userId = tostring(PlayerService.LocalPlayer.UserId) + + eventStreamImpl:setRBXEventStream(eventContext, eventName, { + aType = actionType, + aValue = actionValue, + pos = selectedIndex, + uid = userId, + }) +end + +--[[ + This event is super shitty and encapsulates too many things. + Here's some possible values. Choose a row : + ------------------------------------------------------------------ + | actionType | eventContext | actionValue | selectedIndex | + ------------------------------------------------------------------ + | click | TFMenu | | | + ------------------------------------------------------------------ + | | GFMenu | | | + ------------------------------------------------------------------ + | | SFMenu | | | + ------------------------------------------------------------------ + | | SeeAll | | | + ------------------------------------------------------------------ + + The web is only used to seeing actionType be "click", "focus", and "offFocus". + This does not cover mobile interactions, so "touch" is an acceptable alternative. +]]-- \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/nsOpenContent.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/nsOpenContent.lua new file mode 100644 index 0000000..a1429ee --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/nsOpenContent.lua @@ -0,0 +1,12 @@ +-- nsOpenContent : fired when the Notification Stream button is tapped. +-- eventContext: (string) The current page that is opened or context +-- countOfUnreadNotification: (string) count of unread messages in the notification stream +return function(eventStreamImpl, eventContext, countOfUnreadNotification) + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(countOfUnreadNotification) == "number", "Expected countOfUnreadNotification to be a number") + + local eventName = "nsOpenContent" + eventStreamImpl:setRBXEventStream(eventContext, eventName, { + property = countOfUnreadNotification, + }) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/pageHeartbeat.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/pageHeartbeat.lua new file mode 100644 index 0000000..a3f32d4 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/pageHeartbeat.lua @@ -0,0 +1,13 @@ +-- pageHeartbeat : fires periodically when a view has been presented to a user for predetermined intervals +-- beatInterval : (number) the current interval of beats. This should increment once per pulse and reset every pageLoad +-- luaPage : (optional, string) the current page the event is firing from +return function(eventStreamImpl, beatInterval, luaPage) + assert(type(beatInterval) == "number", "Expected beatInterval to be a number") + + local eventName = "pageHeartbeat" + local eventContext = string.format("%s%d", "heartbeat", beatInterval) + + eventStreamImpl:setRBXEventStream(eventContext, eventName, { + url = luaPage, + }) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/screenLoaded.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/screenLoaded.lua new file mode 100644 index 0000000..559f78a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/screenLoaded.lua @@ -0,0 +1,12 @@ +-- screenLoaded : fires when a screen is presented to a user +-- eventContext : (string) the location or context in which the event is occurring. +return function(eventStreamImpl, eventContext) + assert(type(eventContext) == "string", "Expected eventContext to be a string") + + local eventName = "screenLoaded" + + eventStreamImpl:setRBXEventStream(eventContext, eventName, nil) +end + + +-- NOTE - this should not be confused with pageLoad, which is identical in every way except event name \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/search.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/search.lua new file mode 100644 index 0000000..448a72e --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/search.lua @@ -0,0 +1,17 @@ +-- search : sent when a user interacts with the search bar. +-- eventContext : (string) games, people, catalog, etc... +-- act : (string) The perfromed action. Can be open, submit, or cancel +-- keyword : (optional, string) what is being searched for +return function(eventStreamImpl, eventContext, act, keyword) + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(act) == "string", "Expected act to be a string") + if keyword then + assert(type(keyword) == "string", "Expected keyword to be a string") + end + + local eventName = "search" + eventStreamImpl:setRBXEventStream(eventContext, eventName, { + act = act, + kwd = keyword, + }) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/userInteractions.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/userInteractions.lua new file mode 100644 index 0000000..fd5dd90 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Analytics/Events/userInteractions.lua @@ -0,0 +1,13 @@ +-- userInteractions : sent when an interaction is detected on any page. +-- eventContext : (string) the location or context in which the event is occurring. +return function(eventStreamImpl, eventContext) + local eventName = "userInteractions" + + assert(type(eventContext) == "string", "Expected eventContext to be a string") + + eventStreamImpl:setRBXEventStream(eventContext, eventName, nil) +end + +-- possible values for userInteractions include: +-- mouse +-- touch \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/AppPage.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/AppPage.lua new file mode 100644 index 0000000..5c1a072 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/AppPage.lua @@ -0,0 +1,17 @@ +return { + -- "None" page is an empty page that opens when the native codes + -- sends out a navigation event with no pages specified. This page + -- option should be removed once we remove this navigation event. + None = "None", + Home = "Home", + Games = "Games", + GamesList = "GamesList", + GameDetail = "GameDetail", + SearchPage = "SearchPage", + AvatarEditor = "AvatarEditor", + Chat = "Chat", + ShareGameToChat = "ShareGameToChat", + Catalog = "Catalog", + Friends = "Friends", + More = "More", +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/AppPageLocalizationKeys.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/AppPageLocalizationKeys.lua new file mode 100644 index 0000000..a1089cb --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/AppPageLocalizationKeys.lua @@ -0,0 +1,12 @@ +local AppPage = require(game:GetService("CoreGui").RobloxGui.Modules.LuaApp.AppPage) + +return { + [AppPage.None] = "CommonUI.Features.Label.Nil", + [AppPage.Home] = "CommonUI.Features.Label.Home", + [AppPage.Games] = "CommonUI.Features.Label.Game", + [AppPage.Catalog] = "CommonUI.Features.Label.Catalog", + [AppPage.AvatarEditor] = "CommonUI.Features.Label.Avatar", + [AppPage.Friends] = "CommonUI.Features.Label.Friends", + [AppPage.Chat] = "CommonUI.Features.Label.Chat", + [AppPage.More] = "CommonUI.Features.Label.More", +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/AppReducer.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/AppReducer.lua new file mode 100644 index 0000000..54d77ad --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/AppReducer.lua @@ -0,0 +1,73 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local DeviceOrientation = require(Modules.LuaApp.Reducers.DeviceOrientation) +local TopBar = require(Modules.LuaApp.Reducers.TopBar) +local TabBarVisible = require(Modules.LuaApp.Reducers.TabBarVisible) + +local Games = require(Modules.LuaApp.Reducers.Games) +local GameSorts = require(Modules.LuaApp.Reducers.GameSorts) +local GameSortGroups = require(Modules.LuaApp.Reducers.GameSortGroups) +local GameThumbnails = require(Modules.LuaApp.Reducers.GameThumbnails) +local NextTokenRefreshTime = require(Modules.LuaApp.Reducers.NextTokenRefreshTime) +local GameSortsContents = require(Modules.LuaApp.Reducers.GameSortsContents) +local PlaceIdsToUniverseIds = require(Modules.LuaApp.Reducers.PlaceIdsToUniverseIds) +local LocalUserId = require(Modules.LuaApp.Reducers.LocalUserId) +local Users = require(Modules.LuaApp.Reducers.Users) +local UsersAsync = require(Modules.LuaChat.Reducers.UsersAsync) +local UserStatuses = require(Modules.LuaApp.Reducers.UserStatuses) +local Navigation = require(Modules.LuaApp.Reducers.Navigation) +local Search = require(Modules.LuaApp.Reducers.Search) +local Startup = require(Modules.LuaApp.Reducers.Startup) +local NotificationBadgeCounts = require(Modules.LuaApp.Reducers.NotificationBadgeCounts) +local RequestsStatus = require(Modules.LuaApp.Reducers.RequestsStatus) +local ScreenSize = require(Modules.LuaApp.Reducers.ScreenSize) +local FormFactor = require(Modules.LuaApp.Reducers.FormFactor) +local Platform = require(Modules.LuaApp.Reducers.Platform) + +local FriendCount = require(Modules.LuaChat.Reducers.FriendCount) +local ConnectionState = require(Modules.LuaChat.Reducers.ConnectionState) + +local ChatAppReducer = require(Modules.LuaChat.AppReducer) + +return function(state, action) + state = state or {} + + return { + DeviceOrientation = DeviceOrientation(state.DeviceOrientation, action), + TopBar = TopBar(state.TopBar, action), + TabBarVisible = TabBarVisible(state.TabBarVisible, action), + + -- Users + Users = Users(state.Users, action), + UsersAsync = UsersAsync(state.UsersAsync, action), + UserStatuses = UserStatuses(state.UserStatuses, action), + LocalUserId = LocalUserId(state.LocalUserId, action), + + -- Game Data + Games = Games(state.Games, action), + GameSorts = GameSorts(state.GameSorts, action), + GameSortGroups = GameSortGroups(state.GameSortGroups, action), + GameThumbnails = GameThumbnails(state.GameThumbnails, action), + NextTokenRefreshTime = NextTokenRefreshTime(state.NextTokenRefreshTime, action), + GameSortsContents = GameSortsContents(state.GameSortsContents, action), + PlaceIdsToUniverseIds = PlaceIdsToUniverseIds(state.PlaceIdsToUniverseIds, action), + + RequestsStatus = RequestsStatus(state.RequestsStatus, action), + + Navigation = Navigation(state.Navigation, action), + + Search = Search(state.Search, action), + + FriendCount = FriendCount(state.FriendCount, action), + ConnectionState = ConnectionState(state.ConnectionState, action), + + ScreenSize = ScreenSize(state.ScreenSize, action), + FormFactor = FormFactor(state.FormFactor, action), + Platform = Platform(state.Platform, action), + + ChatAppReducer = ChatAppReducer(state.ChatAppReducer, action), + + Startup = Startup(state.Startup, action), + NotificationBadgeCounts = NotificationBadgeCounts(state.NotificationBadgeCounts, action), + } +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/AppReducer.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/AppReducer.spec.lua new file mode 100644 index 0000000..be9f93c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/AppReducer.spec.lua @@ -0,0 +1,44 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local AppReducer = require(Modules.LuaApp.AppReducer) + + it("has the expected fields, and only the expected fields", function() + local state = AppReducer(nil, {}) + + local expectedKeys = { + ChatAppReducer = true, + ConnectionState = true, + DeviceOrientation = true, + ScreenSize = true, + FormFactor = true, + FriendCount = true, + Games = true, + GameSortGroups = true, + GameSorts = true, + GameSortsContents = true, + GameThumbnails = true, + RequestsStatus = true, + LocalUserId = true, + Navigation = true, + NextTokenRefreshTime = true, + NotificationBadgeCounts = true, + PlaceIdsToUniverseIds = true, + Platform = true, + Search = true, + Startup = true, + TopBar = true, + TabBarVisible = true, + Users = true, + UsersAsync = true, + UserStatuses = true, + } + + for key in pairs(expectedKeys) do + assert(state[key] ~= nil, string.format("Expected field %q", key)) + end + + for key in pairs(state) do + assert(expectedKeys[key] ~= nil, string.format("Did not expect field %q", key)) + end + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Analytics/PageHeartbeatTimer.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Analytics/PageHeartbeatTimer.lua new file mode 100644 index 0000000..d9e5ef3 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Analytics/PageHeartbeatTimer.lua @@ -0,0 +1,89 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local PAGE_HEARTBEAT_TIMERS = {2, 10} + +local PageHeartbeatTimer = Roact.PureComponent:extend("PageHeartbeatTimer") + +local RoactServices = require(Modules.LuaApp.RoactServices) +local RoactAnalyticsAppRouter = require(Modules.LuaApp.Services.RoactAnalyticsAppRouter) + +function PageHeartbeatTimer:ConnectSignals(newProps, oldProps) + local analytics = newProps.analytics + local currentPageGoal = self.props.currentPageGoal + + if currentPageGoal ~= oldProps.currentPageGoal then + self:setState({ + currentPage = currentPageGoal, + currentBeat = 1, + lastPageTick = tick() + }) + end + + local heartBeatSignal = newProps.heartBeatSignal + local sendHeartbeatSignal = newProps.sendHeartbeatSignal + + if heartBeatSignal ~= oldProps.heartBeatSignal then + if self.connectionOnHeartbeat then + self.connectionOnHeartbeat:Disconnect() + end + self.connectionOnHeartbeat = newProps.heartBeatSignal:Connect(function() + if self.state.currentPage then + local targetTime = PAGE_HEARTBEAT_TIMERS[self.state.currentBeat] + if targetTime then + if tick() - self.state.lastPageTick > targetTime then + sendHeartbeatSignal:Fire() + end + end + end + end) + end + + if sendHeartbeatSignal ~= oldProps.sendHeartbeatSignal then + if self.connectionSendPageHeartbeat then + self.connectionSendPageHeartbeat:Disconnect() + end + self.connectionSendPageHeartbeat = sendHeartbeatSignal:Connect(function() + analytics.reportPageHeartbeat(self.state.currentBeat, self.state.currentPage) + + self:setState({ + currentBeat = self.state.currentBeat + 1, + }) + end) + end +end + +function PageHeartbeatTimer:init() + self.state = { + currentPage = nil, + currentBeat = 1, + lastPageTick = tick() + } + self.connectionSendPageHeartbeat = nil + self.connectionOnHeartbeat = nil +end + +function PageHeartbeatTimer:render() +end + +function PageHeartbeatTimer:didUpdate(oldProps, oldState) + self:ConnectSignals(self.props, oldProps) +end + +function PageHeartbeatTimer:didMount() + self:ConnectSignals(self.props, {}) +end + +function PageHeartbeatTimer:willUnmount() + if self.connectionSendPageHeartbeat then + self.connectionSendPageHeartbeat:Disconnect() + end + if self.connectionOnHeartbeat then + self.connectionOnHeartbeat:Disconnect() + end +end + +return RoactServices.connect({ + analytics = RoactAnalyticsAppRouter, +})(PageHeartbeatTimer) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Analytics/PageHeartbeatTimer.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Analytics/PageHeartbeatTimer.spec.lua new file mode 100644 index 0000000..086e57a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Analytics/PageHeartbeatTimer.spec.lua @@ -0,0 +1,161 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local AppPage = require(Modules.LuaApp.AppPage) + local RoactAnalytics = require(Modules.LuaApp.Services.RoactAnalytics) + local Analytics = require(Modules.Common.Analytics) + local Roact = require(Modules.Common.Roact) + local Signal = require(Modules.Common.Signal) + + local PageHeartbeatTimer = require(Modules.LuaApp.Components.Analytics.PageHeartbeatTimer) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + --test helper method + local function createMockAnalytics(initFakeEventStream) + local analytics = Analytics.mock() + initFakeEventStream(analytics.EventStream) + + return analytics + end + + describe("new", function() + it("should create and destroy without errors", function() + local element = mockServices({ + PageHeartbeatTimer = Roact.createElement(PageHeartbeatTimer, { + currentPageGoal = AppPage.None, + }), + }, { + includeStoreProvider = true, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) + + describe("sendHeartbeatSignal", function() + it("should increment eventContext by one per pulse", function() + + local savedHeartbeats = { + heartbeat1 = 0, + heartbeat2 = 0, + } + local mockAnalytics = createMockAnalytics(function(eventStreamImpl) + function eventStreamImpl:setRBXEventStream(eventContext, eventName, extraArgs) + if eventName == "pageHeartbeat" then + savedHeartbeats[eventContext] = savedHeartbeats[eventContext] + 1 + end + end + + return eventStreamImpl + end) + + local component = Roact.Component:extend("TestComponent") + local setUseHeartBeatSignal + + function component:init() + self.state = { + heartBeatSignal = nil, + } + setUseHeartBeatSignal = function(heartBeatSignal) + self:setState({ + heartBeatSignal = heartBeatSignal + }) + end + end + + function component:render() + return Roact.createElement(PageHeartbeatTimer, { + currentPageGoal = AppPage.None, + sendHeartbeatSignal = self.state.heartBeatSignal, + }) + end + + local element = mockServices({ + testComp = Roact.createElement(component), + }, { + includeStoreProvider = true, + extraServices = { + [RoactAnalytics] = mockAnalytics + }, + }) + + local instance = Roact.mount(element) + + local pulseSignal = Signal.new() + setUseHeartBeatSignal(pulseSignal) + + pulseSignal:Fire() + expect(savedHeartbeats.heartbeat1).to.equal(1) + pulseSignal:Fire() + expect(savedHeartbeats.heartbeat2).to.equal(1) + Roact.unmount(instance) + end) + + it("should reset eventContext to 0 when currentPageGoal changes", function() + local savedHeartbeats = { + heartbeat1 = 0, + heartbeat2 = 0, + } + local mockAnalytics = createMockAnalytics(function(eventStreamImpl) + function eventStreamImpl:setRBXEventStream(eventContext, eventName, extraArgs) + if eventName == "pageHeartbeat" then + savedHeartbeats[eventContext] = savedHeartbeats[eventContext] + 1 + end + end + + return eventStreamImpl + end) + + local component = Roact.Component:extend("TestComponent") + local setUseHeartBeatSignal + local setCurrentPage + + function component:init() + self.state = { + heartBeatSignal = nil, + } + setUseHeartBeatSignal = function(heartBeatSignal) + self:setState({ + heartBeatSignal = heartBeatSignal + }) + end + setCurrentPage = function(currentPageGoal) + self:setState({ + currentPageGoal = currentPageGoal + }) + end + end + + function component:render() + return Roact.createElement(PageHeartbeatTimer, { + currentPageGoal = self.state.currentPageGoal, + sendHeartbeatSignal = self.state.heartBeatSignal, + }) + end + + local element = mockServices({ + testComp = Roact.createElement(component), + }, { + includeStoreProvider = true, + extraServices = { + [RoactAnalytics] = mockAnalytics + }, + }) + + local instance = Roact.mount(element) + + local pulseSignal = Signal.new() + setUseHeartBeatSignal(pulseSignal) + + pulseSignal:Fire() + expect(savedHeartbeats.heartbeat1).to.equal(1) + pulseSignal:Fire() + expect(savedHeartbeats.heartbeat2).to.equal(1) + setCurrentPage(AppPage.Home) + pulseSignal:Fire() + expect(savedHeartbeats.heartbeat1).to.equal(2) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Analytics/RouterAnalyticsReporter.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Analytics/RouterAnalyticsReporter.lua new file mode 100644 index 0000000..5219e09 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Analytics/RouterAnalyticsReporter.lua @@ -0,0 +1,42 @@ +local RunService = game:GetService("RunService") + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Signal = require(Modules.Common.Signal) +local Roact = require(Modules.Common.Roact) +local PageHeartbeatTimer = require(Modules.LuaApp.Components.Analytics.PageHeartbeatTimer) +local AppPage = require(Modules.LuaApp.AppPage) + +local RoactAnalyticsAppRouter = require(Modules.LuaApp.Services.RoactAnalyticsAppRouter) +local RoactServices = require(Modules.LuaApp.RoactServices) + +local RouterAnalyticsReporter = Roact.Component:extend("RouterAnalyticsReporter") + +function RouterAnalyticsReporter:init() + self.sendHeartbeatSignal = Signal.new() +end + +function RouterAnalyticsReporter:render() + local currentPage = self.props.currentPage + + return currentPage and currentPage ~= AppPage.None and Roact.createElement(PageHeartbeatTimer, { + currentPageGoal = currentPage, + heartBeatSignal = RunService.Heartbeat, + sendHeartbeatSignal = self.sendHeartbeatSignal, + }) +end + +function RouterAnalyticsReporter:didUpdate(oldProps) + local currentPage = self.props.currentPage + local analytics = self.props.analytics + + if currentPage ~= oldProps.currentPage then + if analytics then + analytics.reportPageChanged(currentPage) + end + end +end + +return RoactServices.connect({ + analytics = RoactAnalyticsAppRouter, +})(RouterAnalyticsReporter) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Analytics/RouterAnalyticsReporter.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Analytics/RouterAnalyticsReporter.spec.lua new file mode 100644 index 0000000..aeecb34 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Analytics/RouterAnalyticsReporter.spec.lua @@ -0,0 +1,21 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local AppPage = require(Modules.LuaApp.AppPage) + local Roact = require(Modules.Common.Roact) + + local RouterAnalyticsReporter = require(Modules.LuaApp.Components.Analytics.RouterAnalyticsReporter) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local element = mockServices({ + RouterAnalyticsReporter = Roact.createElement(RouterAnalyticsReporter, { + currentPage = AppPage.None, + }), + }, { + includeStoreProvider = true, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/App.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/App.lua new file mode 100644 index 0000000..579bed8 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/App.lua @@ -0,0 +1,344 @@ +local CoreGui = game:GetService("CoreGui") +local Players = game:GetService("Players") +local LocalizationService = game:GetService("LocalizationService") +local RunService = game:GetService("RunService") +local Workspace = game:GetService("Workspace") +local UserInputService = game:GetService("UserInputService") +local NotificationService = game:GetService("NotificationService") +local GuiService = game:GetService("GuiService") + +local Modules = CoreGui.RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local Rodux = require(Modules.Common.Rodux) +local RoactRodux = require(Modules.Common.RoactRodux) +local ExternalEventConnection = require(Modules.Common.RoactUtilities.ExternalEventConnection) + +local Promise = require(Modules.LuaApp.Promise) +local Localization = require(Modules.LuaApp.Localization) +local RoactServices = require(Modules.LuaApp.RoactServices) +local RoactAnalytics = require(Modules.LuaApp.Services.RoactAnalytics) +local RoactLocalization = require(Modules.LuaApp.Services.RoactLocalization) +local RoactNetworking = require(Modules.LuaApp.Services.RoactNetworking) +local AppNotificationService = require(Modules.LuaApp.Services.AppNotificationService) +local AppGuiService = require(Modules.LuaApp.Services.AppGuiService) +local FlagSettings = require(Modules.LuaApp.FlagSettings) +local AppPage = require(Modules.LuaApp.AppPage) + +local AppRouter = require(Modules.LuaApp.Components.AppRouter) +local ScreenGuiWrap = require(Modules.LuaApp.Components.ScreenGuiWrap) +local HomePage = require(Modules.LuaApp.Components.Home.HomePage) +local GamesHub = require(Modules.LuaApp.Components.Games.GamesHub) +local GamesList = require(Modules.LuaApp.Components.Games.GamesList) +local SearchPage = require(Modules.LuaApp.Components.Search.SearchPage) +local RoactAvatarEditorWrapper = require(Modules.LuaApp.Components.Avatar.RoactAvatarEditorWrapper) +local RoactChatWrapper = require(Modules.LuaApp.Components.Chat.RoactChatWrapper) +local RoactDummyPageWrap = require(Modules.LuaApp.Components.RoactDummyPageWrap) +local RoactGameShareWrapper = require(Modules.LuaApp.Components.Chat.RoactGameShareWrapper) +local MorePage = require(Modules.LuaApp.Components.More.MorePage) +local BadgeEventReceiver = require(Modules.LuaApp.Components.EventReceivers.BadgeEventReceiver) +local NavigationEventReceiver = require(Modules.LuaApp.Components.EventReceivers.NavigationEventReceiver) + +local AppReducer = require(Modules.LuaApp.AppReducer) + +local RobloxEventReceiver = require(Modules.LuaApp.RobloxEventReceiver) + +local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) +local Constants = require(Modules.LuaApp.Constants) +local SetFormFactor = require(Modules.LuaApp.Actions.SetFormFactor) +local SetPlatform = require(Modules.LuaApp.Actions.SetPlatform) +local DeviceOrientationMode = require(Modules.LuaApp.DeviceOrientationMode) +local SetDeviceOrientation = require(Modules.LuaApp.Actions.SetDeviceOrientation) +local SetLocalUserId = require(Modules.LuaApp.Actions.SetLocalUserId) +local SetHomePageDataStatus = require(Modules.LuaApp.Actions.SetHomePageDataStatus) +local SetScreenSize = require(Modules.LuaApp.Actions.SetScreenSize) +local SetUserMembershipType = require(Modules.LuaApp.Actions.SetUserMembershipType) +local AddUser = require(Modules.LuaApp.Actions.AddUser) +local ApiFetchUsersThumbnail = require(Modules.LuaApp.Thunks.ApiFetchUsersThumbnail) +local Analytics = require(Modules.Common.Analytics) +local request = require(Modules.LuaApp.Http.request) +local ApiFetchSortTokens = require(Modules.LuaApp.Thunks.ApiFetchSortTokens) +local ApiFetchGamesData = require(Modules.LuaApp.Thunks.ApiFetchGamesData) +local ApiFetchUsersFriends = require(Modules.LuaApp.Thunks.ApiFetchUsersFriends) +local BottomBar = require(Modules.LuaApp.Components.BottomBar) +local UserModel = require(Modules.LuaApp.Models.User) +local FormFactor = require(Modules.LuaApp.Enum.FormFactor) +local ApiFetchUnreadNotificationCount = require(Modules.LuaApp.Thunks.ApiFetchUnreadNotificationCount) +local FetchGamesPageData = require(Modules.LuaApp.Thunks.FetchGamesPageData) + +local ChatMaster = require(Modules.ChatMaster) + +-- flag dependencies +local luaAppLegacyInputDisabledGlobally = settings():GetFFlag('LuaAppLegacyInputDisabledGlobally2') + +local function getDevicePlatform() + if _G.__TESTEZ_RUNNING_TEST__ then + return Enum.Platform.None + end + + return UserInputService:GetPlatform() +end + +local App = Roact.Component:extend("App") + +function App:init() + self.state = { + store = Rodux.Store.new(AppReducer) + } + + self._analytics = Analytics.new() + self._networkRequest = request + self._localization = Localization.new(LocalizationService.RobloxLocaleId) + self._robloxEventReceiver = RobloxEventReceiver.new(NotificationService) + + self.updateLocalization = function(newLocale) + self._localization:SetLocale(newLocale) + end + + self._chatMaster = ChatMaster.new(self.state.store) + + local function wrapPageInScreenGui(component, pageType, visible, props) + return Roact.createElement(ScreenGuiWrap, { + component = component, + pageType = pageType, + isVisible = visible, + props = props, + }) + end + + self.pageConstructors = { + [AppPage.None] = function() + return nil + end, + [AppPage.Home] = function(visible) + if FlagSettings.IsLuaHomePageEnabled(getDevicePlatform()) then + return wrapPageInScreenGui(HomePage, AppPage.Home, visible) + end + return nil + end, + [AppPage.Games] = function(visible) + if FlagSettings.IsLuaGamesPageEnabled(getDevicePlatform()) then + return wrapPageInScreenGui(GamesHub, AppPage.Games, visible) + end + return nil + end, + [AppPage.GamesList] = function(visible, detail) + return wrapPageInScreenGui(GamesList, AppPage.GamesList, visible, { sortName = detail }) + end, + [AppPage.SearchPage] = function(visible, detail) + local parameters = detail and { searchUuid = detail } or nil + return wrapPageInScreenGui(SearchPage, AppPage.SearchPage, visible, parameters) + end, + [AppPage.AvatarEditor] = function(visible) + return Roact.createElement(RoactAvatarEditorWrapper, { + isVisible = visible, + }) + end, + [AppPage.Chat] = function(visible, detail) + return Roact.createElement(RoactChatWrapper, { + chatMaster = self._chatMaster, + isVisible = visible, + pageType = AppPage.Chat, + parameters = detail and { conversationId = detail } or nil + }) + end, + [AppPage.ShareGameToChat] = function(visible, detail) + local parameters = { + chatMaster = self._chatMaster, + } + if detail then + parameters.placeId = detail + end + return wrapPageInScreenGui(RoactGameShareWrapper, AppPage.ShareGameToChat, visible, parameters) + end, + [AppPage.Catalog] = function(visible) + return Roact.createElement(RoactDummyPageWrap, { + isVisible = visible, + pageType = "Catalog", + }) + end, + [AppPage.Friends] = function(visible) + return Roact.createElement(RoactDummyPageWrap, { + isVisible = visible, + pageType = "Friends", + }) + end, + [AppPage.More] = function(visible) + return wrapPageInScreenGui(MorePage, AppPage.More, visible) + end, + } + + self.alwaysRenderedPages = { + { name = AppPage.Home }, + { name = AppPage.Games }, + { name = AppPage.AvatarEditor }, + { name = AppPage.Chat }, + } + + self.updateDeviceOrientation = function(viewportSize) + local deviceOrientation = viewportSize.x > viewportSize.y and + DeviceOrientationMode.Landscape or DeviceOrientationMode.Portrait + + if self._deviceOrientation ~= deviceOrientation then + self._deviceOrientation = deviceOrientation + self.state.store:dispatch(SetDeviceOrientation(self._deviceOrientation)) + end + end + + self.updateDeviceFormFactor = function(viewportSize) + local formFactor = FormFactor.TABLET + + if viewportSize.Y > viewportSize.X then + formFactor = FormFactor.PHONE + end + + self.state.store:dispatch(SetFormFactor(formFactor)) + end + + self.updateViewport = function() + local viewportSize = Workspace.CurrentCamera.ViewportSize + + -- Hacky code awaits underlying mechanism fix. + -- Viewport will get a 0,0,1,1 rect before it is properly set. + if viewportSize.X > 1 and viewportSize.Y > 1 then + self.state.store:dispatch(SetScreenSize(viewportSize)) + self.updateDeviceOrientation(viewportSize) + + if FlagSettings.IsLuaAppDeterminingFormFactorAndPlatform() then + self.updateDeviceFormFactor(viewportSize) + end + end + end + + self.updateLocalPlayerMembership = function() + local localPlayer = Players.LocalPlayer + local userId = tostring(localPlayer.UserId) + + self.state.store:dispatch(SetUserMembershipType(userId, localPlayer.MembershipType)) + end + + self.updateDevicePlatform = function() + -- Have to filter this to handle studio testing plugin which runs in a + -- downgraded security context + local platform = getDevicePlatform() + self.state.store:dispatch(SetPlatform(platform)) + end + + self.onClose = function() + -- there is currently a bug with the EventStream, where the stream is not released + -- by the game engine. This call is a temporary work around until a new api is available. + self._analytics.EventStream:releaseRBXEventStream() + end + + -- the BindToClose function does not play nicely with Studio. + if not RunService:IsStudio() then + game:BindToClose(self.onClose) + end + + if FlagSettings.IsLuaAppDeterminingFormFactorAndPlatform() then + self.updateDevicePlatform() + end +end + +function App:didMount() + local platform = self.state.store:getState().Platform + + RunService:setThrottleFramerateEnabled(true) + UserInputService.LegacyInputEventsEnabled = (not luaAppLegacyInputDisabledGlobally) + + -- Set the device orientation for the 1st time + -- TODO: this should be put in a seperate file. + self.updateViewport() + + -- Add the local player info to the store for the 1st time + -- TODO: this should be put in a seperate file. + local localPlayer = Players.LocalPlayer + local userId = tostring(localPlayer.UserId) + + self.state.store:dispatch(AddUser(UserModel.fromData(userId, localPlayer.Name, false))) + self.state.store:dispatch(ApiFetchUsersThumbnail( + self._networkRequest, {userId}, Constants.AvatarThumbnailRequests.HOME_HEADER_USER + )) + self.state.store:dispatch(SetLocalUserId(userId)) + self.updateLocalPlayerMembership() + + self.state.store:dispatch(ApiFetchUnreadNotificationCount(self._networkRequest)) + + -- start loading information for Home Page + if FlagSettings.IsLuaHomePageEnabled(platform) then + + self.state.store:dispatch(SetHomePageDataStatus(RetrievalStatus.Fetching)) + Promise.all({ + self.state.store:dispatch(ApiFetchUsersFriends( + self._networkRequest, userId, Constants.AvatarThumbnailRequests.USER_CAROUSEL + )), + self.state.store:dispatch(ApiFetchSortTokens(self._networkRequest, Constants.GameSortGroups.HomeGames) + ):andThen( + function(result) + return self.state.store:dispatch(ApiFetchGamesData(self._networkRequest, Constants.GameSortGroups.HomeGames)) + end + ), + }):andThen( + function() + self.state.store:dispatch(SetHomePageDataStatus(RetrievalStatus.Done)) + + -- start loading information for the Games Page + local status = self.state.store:getState().Startup.GamesPageDataStatus + if FlagSettings.IsLuaGamesPageEnabled(platform) and status == RetrievalStatus.NotStarted then + self.state.store:dispatch(FetchGamesPageData(self._networkRequest, self._analytics)) + end + end + ) + end +end + +function App:render() + return Roact.createElement(RoactRodux.StoreProvider, { + store = self.state.store, + }, { + services = Roact.createElement(RoactServices.ServiceProvider, { + services = { + [RoactAnalytics] = self._analytics, + [RoactLocalization] = self._localization, + [RoactNetworking] = self._networkRequest, + [AppNotificationService] = NotificationService, + [AppGuiService] = GuiService, + } + }, { + PageWrapper = Roact.createElement("Folder", {}, { + NavigationEventReceiver = Roact.createElement(NavigationEventReceiver,{ + RobloxEventReceiver = self._robloxEventReceiver, + }), + BadgeEventReceiver = Roact.createElement(BadgeEventReceiver, { + RobloxEventReceiver = self._robloxEventReceiver, + }), + BottomBar = Roact.createElement(BottomBar, { + displayOrder = 10, + }), + AppRouter = Roact.createElement(AppRouter, { + pageConstructors = self.pageConstructors, + alwaysRenderedPages = self.alwaysRenderedPages, + }), + localizationListener = Roact.createElement(ExternalEventConnection, { + event = LocalizationService:GetPropertyChangedSignal("RobloxLocaleId"), + callback = self.updateLocalization, + }), + viewportSizeListener = Roact.createElement(ExternalEventConnection, { + event = Workspace.CurrentCamera:GetPropertyChangedSignal("ViewportSize"), + callback = self.updateViewport, + }), + playerMembershipListener = Roact.createElement(ExternalEventConnection, { + event = Players.LocalPlayer:GetPropertyChangedSignal("MembershipType"), + callback = self.updateLocalPlayerMembership, + }), + }) + }), + }) +end + +function App:willUnmount() + RunService:setThrottleFramerateEnabled(false) + self._chatNotificationBroadcaster:Destruct() +end + +return App \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/AppRouter.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/AppRouter.lua new file mode 100644 index 0000000..0ff5f3e --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/AppRouter.lua @@ -0,0 +1,116 @@ +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) +local RoactServices = require(Modules.LuaApp.RoactServices) +local RoactNetworking = require(Modules.LuaApp.Services.RoactNetworking) +local RoactAnalytics = require(Modules.LuaApp.Services.RoactAnalytics) +local AppNotificationService = require(Modules.LuaApp.Services.AppNotificationService) + +local AppPage = require(Modules.LuaApp.AppPage) +local RouterAnalyticsReporter = require(Modules.LuaApp.Components.Analytics.RouterAnalyticsReporter) +local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) +local FetchGamesPageData = require(Modules.LuaApp.Thunks.FetchGamesPageData) + +local FlagSettings = require(Modules.LuaApp.FlagSettings) + +local AppRouter = Roact.PureComponent:extend("AppRouter") + +AppRouter.defaultProps = { + alwaysRenderedPages = {}, +} + +local function getPageName(page) + return page.detail and (page.name .. ":" .. page.detail) or page.name +end + +function AppRouter:render() + local routeHistory = self.props.routeHistory + local pageConstructors = self.props.pageConstructors + local alwaysRenderedPages = self.props.alwaysRenderedPages + + local currentRoute = routeHistory[#routeHistory] + local currentPage = currentRoute[#currentRoute].name + local pages = { + RouterAnalyticsReporter = Roact.createElement(RouterAnalyticsReporter, { + currentPage = currentPage, + }), + } + + for index = #routeHistory, 1, -1 do + local route = routeHistory[index] + local pageInfo = route[#route] + local pageName = getPageName(pageInfo) + local isVisible = index == #routeHistory + if not pages[pageName] then + pages[pageName] = pageConstructors[pageInfo.name](isVisible, pageInfo.detail) + end + end + + for index = 1, #alwaysRenderedPages do + local pageInfo = alwaysRenderedPages[index] + local pageName = getPageName(pageInfo) + if not pages[pageName] then + pages[pageName] = pageConstructors[pageInfo.name](false, pageInfo.detail) + end + end + + return Roact.createElement("Folder", {}, pages) +end + +function AppRouter:didUpdate(prevProps, prevState) + local notificationService = self.props.NotificationService + local newRouteHistory = self.props.routeHistory + local newRoute = newRouteHistory[#newRouteHistory] + local newPage = newRoute[#newRoute] + + local oldRouteHistory = prevProps.routeHistory + local oldRoute = oldRouteHistory[#oldRouteHistory] + local oldPage = oldRoute[#oldRoute] + + local UseLuaGamesPage = FlagSettings.IsLuaGamesPageEnabled(self.props.platform) + + if UseLuaGamesPage and newPage.name == AppPage.Games + and self.props.gamesPageDataStatus == RetrievalStatus.NotStarted then + self.props.loadGamesPage(RoactNetworking.get(self._context), RoactAnalytics.get(self._context)) + end + + local fetchedGames = newPage.name == AppPage.Games + and self.props.gamesPageDataStatus == RetrievalStatus.Done + local oldFetchedGames = oldPage.name == AppPage.Games + and prevProps.gamesPageDataStatus == RetrievalStatus.Done + if fetchedGames and not oldFetchedGames then + notificationService:ActionEnabled(Enum.AppShellActionType.GamePageLoaded) + end + + local fetchedHome = newPage.name == AppPage.Home + and self.props.homePageDataStatus == RetrievalStatus.Done + local oldFetchedHome = oldPage.name == AppPage.Home + and prevProps.homePageDataStatus == RetrievalStatus.Done + if fetchedHome and not oldFetchedHome then + notificationService:ActionEnabled(Enum.AppShellActionType.HomePageLoaded) + end +end + +AppRouter = RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + routeHistory = state.Navigation.history, + gamesPageDataStatus = state.Startup.GamesPageDataStatus, + homePageDataStatus = state.Startup.HomePageDataStatus, + platform = state.Platform, + } + end, + function(dispatch) + return { + loadGamesPage = function(networking, analytics) + return dispatch(FetchGamesPageData(networking, analytics)) + end, + } + end +)(AppRouter) + +return RoactServices.connect({ + NotificationService = AppNotificationService, +})(AppRouter) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/AppRouter.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/AppRouter.spec.lua new file mode 100644 index 0000000..854b271 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/AppRouter.spec.lua @@ -0,0 +1,94 @@ +return function() + local AppRouter = require(script.Parent.AppRouter) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + local Rodux = require(Modules.Common.Rodux) + + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + local AppPage = require(Modules.LuaApp.AppPage) + local AppReducer = require(Modules.LuaApp.AppReducer) + local NavigateDown = require(Modules.LuaApp.Thunks.NavigateDown) + + it("should create and destroy without errors", function() + local element = mockServices({ + Router = Roact.createElement(AppRouter, { + pageConstructors = { + [AppPage.Home] = function(visible) + return nil + end, + } + }), + }, { + includeStoreProvider = true, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create a page for each route in the history, with only the top one visible", function() + local store = Rodux.Store.new(AppReducer) + + local element = mockServices({ + Router = Roact.createElement(AppRouter, { + pageConstructors = { + [AppPage.Home] = function(visible) + return Roact.createElement("TextLabel", { + Visible = visible, + Text = AppPage.Home, + }) + end, + [AppPage.GamesList] = function(visible, detail) + return Roact.createElement("TextLabel", { + Visible = visible, + Text = string.format(AppPage.GamesList .. ":" .. detail), + }) + end, + [AppPage.GameDetail] = function(visible, detail) + return Roact.createElement("TextLabel", { + Visible = visible, + Text = string.format(AppPage.GameDetail .. ":" .. detail), + }) + end, + } + }), + }, { + includeStoreProvider = true, + store = store, + }) + local container = Instance.new("Folder") + Roact.mount(element, container, "RouteTest") + + expect(container).to.be.ok() + expect(container.RouteTest).to.be.ok() + expect(container.RouteTest[AppPage.Home]).to.be.ok() + expect(container.RouteTest[AppPage.Home].Text).to.equal(AppPage.Home) + expect(container.RouteTest[AppPage.Home].Visible).to.equal(true) + + store:dispatch(NavigateDown({ name = AppPage.GamesList, detail = "popular" })) + store:flush() + local gamesListName = AppPage.GamesList .. ":popular" + + expect(container.RouteTest[AppPage.Home]).to.be.ok() + expect(container.RouteTest[AppPage.Home].Text).to.equal(AppPage.Home) + expect(container.RouteTest[AppPage.Home].Visible).to.equal(false) + expect(container.RouteTest[gamesListName]).to.be.ok() + expect(container.RouteTest[gamesListName].Text).to.equal(gamesListName) + expect(container.RouteTest[gamesListName].Visible).to.equal(true) + + store:dispatch(NavigateDown({ name = AppPage.GameDetail, detail = "123456" })) + store:flush() + local gameDetailName = AppPage.GameDetail .. ":123456" + + expect(container.RouteTest[AppPage.Home]).to.be.ok() + expect(container.RouteTest[AppPage.Home].Text).to.equal(AppPage.Home) + expect(container.RouteTest[AppPage.Home].Visible).to.equal(false) + expect(container.RouteTest[gamesListName]).to.be.ok() + expect(container.RouteTest[gamesListName].Text).to.equal(gamesListName) + expect(container.RouteTest[gamesListName].Visible).to.equal(false) + expect(container.RouteTest[gameDetailName]).to.be.ok() + expect(container.RouteTest[gameDetailName].Text).to.equal(gameDetailName) + expect(container.RouteTest[gameDetailName].Visible).to.equal(true) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Avatar/RoactAvatarEditorWrapper.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Avatar/RoactAvatarEditorWrapper.lua new file mode 100644 index 0000000..a2fe494 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Avatar/RoactAvatarEditorWrapper.lua @@ -0,0 +1,86 @@ +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) + +local AppPage = require(Modules.LuaApp.AppPage) +local TopBar = require(Modules.LuaApp.Components.TopBar) +local NotificationType = require(Modules.LuaApp.Enum.NotificationType) +local AvatarEditorSetup = require(Modules.Mobile.AvatarEditorSetup) + +local RoactAvatarEditorWrapper = Roact.PureComponent:extend("RoactAvatarEditorWrapper") + +local AppGuiService = require(Modules.LuaApp.Services.AppGuiService) +local RoactServices = require(Modules.LuaApp.RoactServices) + +function RoactAvatarEditorWrapper:init() + local function notifyAppReady() + -- Staging broadcasting of APP_READY to accomodate for unpredictable + -- delay on the native side. + -- Once Lua tab bar is integrated, there will be no use for this + self.props.guiService:BroadcastNotification(AppPage.AvatarEditor, NotificationType.APP_READY) + end + + AvatarEditorSetup:Initialize(notifyAppReady, true) + self.isPageOpen = false + self.topBarHeight = 0 +end + +function RoactAvatarEditorWrapper:render() + local isVisible = self.props.isVisible + + return Roact.createElement(Roact.Portal, { + target = CoreGui, + }, { + AvatarEditor = Roact.createElement("ScreenGui", { + Enabled = isVisible, + ZIndexBehavior = Enum.ZIndexBehavior.Sibling, + DisplayOrder = 3, -- This is because AvatarEditor ScreenGui DisplayOrder is 2 + }, { + TopBar = Roact.createElement(TopBar, { + showBuyRobux = true, + showNotifications = true, + }) + }) + }) +end + +function RoactAvatarEditorWrapper:didMount() + self:updateAvatarEditor() +end + +function RoactAvatarEditorWrapper:didUpdate(prevProps, prevState) + self:updateAvatarEditor() +end + +function RoactAvatarEditorWrapper:willUnmount() + AvatarEditorSetup:Close() +end + +function RoactAvatarEditorWrapper:updateAvatarEditor() + if not self.isPageOpen and self.props.isVisible then + AvatarEditorSetup:Open() + self.isPageOpen = true + elseif self.isPageOpen and not self.props.isVisible then + AvatarEditorSetup:Close() + self.isPageOpen = false + end + + if self.topBarHeight ~= self.props.topBarHeight then + AvatarEditorSetup:UpdateTopBarHeight(self.props.topBarHeight) + self.topBarHeight = self.props.topBarHeight + end +end + +RoactAvatarEditorWrapper = RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + topBarHeight = state.TopBar.topBarHeight, + } + end +)(RoactAvatarEditorWrapper) + +return RoactServices.connect({ + guiService = AppGuiService +})(RoactAvatarEditorWrapper) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/BottomBar.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/BottomBar.lua new file mode 100644 index 0000000..6a246cc --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/BottomBar.lua @@ -0,0 +1,217 @@ +local CoreGui = game:GetService("CoreGui") +local UserInputService = game:GetService("UserInputService") + +local Modules = CoreGui.RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) +local ExternalEventConnection = require(Modules.Common.RoactUtilities.ExternalEventConnection) + +local Constants = require(Modules.LuaApp.Constants) +local AppPage = require(Modules.LuaApp.AppPage) +local DeviceOrientationMode = require(Modules.LuaApp.DeviceOrientationMode) +local FlagSettings = require(Modules.LuaApp.FlagSettings) +local getScreenBottomInset = require(Modules.LuaApp.getScreenBottomInset) +local NotificationType = require(Modules.LuaApp.Enum.NotificationType) + +local BottomBarButton = require(Modules.LuaApp.Components.BottomBarButton) + +local AppGuiService = require(Modules.LuaApp.Services.AppGuiService) +local RoactServices = require(Modules.LuaApp.RoactServices) + +local UseLuaBottomBar = FlagSettings.IsLuaBottomBarEnabled() + +local HomeButtonDefaultImage = "rbxasset://textures/ui/LuaApp/icons/ic-home.png" +local GamesButtonDefaultImage = "rbxasset://textures/ui/LuaApp/icons/ic-games.png" +local CatalogButtonDefaultImage = "rbxasset://textures/ui/LuaApp/icons/ic-catalog.png" +local AvatarButtonDefaultImage = "rbxasset://textures/ui/LuaApp/icons/ic-avatar.png" +local FriendsButtonDefaultImage = "rbxasset://textures/ui/LuaApp/icons/ic-friend.png" +local ChatButtonDefaultImage = "rbxasset://textures/ui/LuaApp/icons/ic-chat.png" +local MoreButtonDefaultImage = "rbxasset://textures/ui/LuaApp/icons/ic-more.png" + +local HomeButtonSelectedImage = "rbxasset://textures/ui/LuaApp/icons/ic-home-on.png" +local GamesButtonSelectedImage = "rbxasset://textures/ui/LuaApp/icons/ic-games-on.png" +local CatalogButtonSelectedImage = "rbxasset://textures/ui/LuaApp/icons/ic-catalog-on.png" +local AvatarButtonSelectedImage = "rbxasset://textures/ui/LuaApp/icons/ic-avatar-on.png" +local FriendsButtonSelectedImage = "rbxasset://textures/ui/LuaApp/icons/ic-friend-on.png" +local ChatButtonSelectedImage = "rbxasset://textures/ui/LuaApp/icons/ic-chat-on.png" +local MoreButtonSelectedImage = "rbxasset://textures/ui/LuaApp/icons/ic-more-on.png" + +local BottomBar = Roact.PureComponent:extend("BottomBar") + +function BottomBar:init() + -- Android device might still have BottomBarSize when we HIDE_TAB_BAR + -- Which is for system virtual navigation bar + -- BottomBarSize might change while app is running depending on the device + -- iOS device should fall back to safeZoneOffsets.bottom when we HIDE_TAB_BAR + self.updateGlobalGuiInset = function() + self.props.guiService:SetGlobalGuiInset(0, 0, 0, self.luaBottomBarSize + getScreenBottomInset()) + end +end + +function BottomBar:render() + local deviceOrientation = self.props.deviceOrientation + local bottomBarVisible = self.props.bottomBarVisible + local displayOrder = self.props.displayOrder + local guiService = self.props.guiService + local isLandscape = deviceOrientation == DeviceOrientationMode.Landscape + + local bottomBarSizeListener = (not _G.__TESTEZ_RUNNING_TEST__) and Roact.createElement(ExternalEventConnection, { + event = UserInputService:GetPropertyChangedSignal("BottomBarSize"), + callback = self.updateGlobalGuiInset, + }) + + local safeZoneOffsetsListener = (not _G.__TESTEZ_RUNNING_TEST__) and Roact.createElement(ExternalEventConnection, { + event = guiService.SafeZoneOffsetsChanged, + callback = self.updateGlobalGuiInset, + }) + + if not bottomBarVisible or not UseLuaBottomBar then + return Roact.createElement("Folder", {}, { + bottomBarSizeListener, + safeZoneOffsetsListener, + }) + end + + local homeButton = Roact.createElement(BottomBarButton, { + defaultImage = HomeButtonDefaultImage, + selectedImage = HomeButtonSelectedImage, + associatedPageType = AppPage.Home, + }) + + local gamesButton = Roact.createElement(BottomBarButton, { + defaultImage = GamesButtonDefaultImage, + selectedImage = GamesButtonSelectedImage, + associatedPageType = AppPage.Games, + }) + + local catalogButton = Roact.createElement(BottomBarButton, { + defaultImage = CatalogButtonDefaultImage, + selectedImage = CatalogButtonSelectedImage, + associatedPageType = AppPage.Catalog, + }) + + local avatarButton = Roact.createElement(BottomBarButton, { + defaultImage = AvatarButtonDefaultImage, + selectedImage = AvatarButtonSelectedImage, + associatedPageType = AppPage.AvatarEditor, + }) + + local friendsButton = Roact.createElement(BottomBarButton, { + defaultImage = FriendsButtonDefaultImage, + selectedImage = FriendsButtonSelectedImage, + associatedPageType = AppPage.Friends, + }) + + local chatButton = Roact.createElement(BottomBarButton, { + defaultImage = ChatButtonDefaultImage, + selectedImage = ChatButtonSelectedImage, + associatedPageType = AppPage.Chat, + }) + + local moreButton = Roact.createElement(BottomBarButton, { + defaultImage = MoreButtonDefaultImage, + selectedImage = MoreButtonSelectedImage, + associatedPageType = AppPage.More, + }) + + local uiListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + local portraitButtons = { + UIListLayout = uiListLayout, + HomeButton = homeButton, + GamesButton = gamesButton, + AvatarButton = avatarButton, + ChatButton = chatButton, + MoreButton = moreButton, + } + + local landscapeButtons = { + UIListLayout = uiListLayout, + HomeButton = homeButton, + GamesButton = gamesButton, + CatalogButton = catalogButton, + AvatarButton = avatarButton, + FriendsButton = friendsButton, + ChatButton = chatButton, + MoreButton = moreButton, + } + + local children = isLandscape and landscapeButtons or portraitButtons + + return Roact.createElement(Roact.Portal, { + target = CoreGui, + }, { + BottomBar = Roact.createElement("ScreenGui", { + ZIndexBehavior = Enum.ZIndexBehavior.Sibling, + DisplayOrder = displayOrder, + }, { + TopLine = Roact.createElement("Frame", { + Position = UDim2.new(0, 0, 1, 0), + Size = UDim2.new(1, 0, 0, 1), + BorderSizePixel = 0, + BackgroundTransparency = 0, + BackgroundColor3 = Constants.Color.GRAY_SEPARATOR, + ZIndex = 2, + }), + Contents = Roact.createElement("Frame", { + Position = UDim2.new(0, 0, 1, 0), + Size = UDim2.new(1, 0, 1, 0), + BorderSizePixel = 0, + BackgroundTransparency = 0, + BackgroundColor3 = Constants.Color.WHITE, + ZIndex = 1, + }, { + Frame = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0.5, 0), + Position = UDim2.new(0.5, 0, 0, 0), + Size = UDim2.new(isLandscape and 0.92 or 1, 0, 0, Constants.BOTTOM_BAR_SIZE), + BorderSizePixel = 0, + BackgroundTransparency = 1, + }, children) + }) + }), + bottomBarSizeListener, + safeZoneOffsetsListener, + }) +end + +function BottomBar:didMount() + self:updateInset(self.props.bottomBarVisible) +end + +function BottomBar:willUpdate(newProps) + if (self.props.bottomBarVisible ~= newProps.bottomBarVisible) then + self:updateInset(newProps.bottomBarVisible) + end +end + +function BottomBar:updateInset(visible) + local guiService = self.props.guiService + -- Setting the view size to consider bottom bar space. + -- TODO Needs to be checked if this will be necessary after integrating Lua bottom bar. + + -- self.luaBottomBarSize is not a local variable because BottomBarSize changed signal callback isn't + -- re-created each time, it could use the old upvalue if luaBottomBarSize is local variable. + self.luaBottomBarSize = (UseLuaBottomBar and visible) and Constants.BOTTOM_BAR_SIZE or 0 + if UseLuaBottomBar then + guiService:BroadcastNotification("", NotificationType.HIDE_TAB_BAR) + end + + self.updateGlobalGuiInset() +end + +BottomBar = RoactRodux.connect(function(store) + local state = store:getState() + return { + deviceOrientation = state.DeviceOrientation, + bottomBarVisible = state.TabBarVisible, + } +end)(BottomBar) + +return RoactServices.connect({ + guiService = AppGuiService +})(BottomBar) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/BottomBar.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/BottomBar.spec.lua new file mode 100644 index 0000000..880eaae --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/BottomBar.spec.lua @@ -0,0 +1,23 @@ +return function() + local BottomBar = require(script.Parent.BottomBar) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + + it("should create and destroy without errors", function() + local element = mockServices({ + BottomBar = Roact.createElement(BottomBar, { + isVisible = true, + displayOrder = 4, + }), + }, { + includeStoreProvider = true, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/BottomBarButton.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/BottomBarButton.lua new file mode 100644 index 0000000..d45929b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/BottomBarButton.lua @@ -0,0 +1,115 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) + +local DeviceOrientationMode = require(Modules.LuaApp.DeviceOrientationMode) +local PageIndex = require(Modules.LuaApp.PageIndex) +local Constants = require(Modules.LuaApp.Constants) + +local FitChildren = require(Modules.LuaApp.FitChildren) + +local NavigateToRoute = require(Modules.LuaApp.Thunks.NavigateToRoute) + +local LocalizedFitTextLabel = require(Modules.LuaApp.Components.LocalizedFitTextLabel) +local AppPageLocalizationKeys = require(Modules.LuaApp.AppPageLocalizationKeys) + +local ICON_SIZE = 24 + +local BottomBarButton = Roact.PureComponent:extend("BottomBarButton") + +function BottomBarButton:render() + local defaultImage = self.props.defaultImage + local selectedImage = self.props.selectedImage + local associatedPageType = self.props.associatedPageType + + local deviceOrientation = self.props.deviceOrientation + local currentPage = self.props.currentPage + local navigateToPage = self.props.navigateToPage + + local totalPages = PageIndex.GetTotalPages(deviceOrientation) + local pageIndex = PageIndex.GetIndexByPageType(associatedPageType, + deviceOrientation) or 1 + local isLandscape = deviceOrientation == DeviceOrientationMode.Landscape + + local iconImage, textColor + if associatedPageType == currentPage then + iconImage = selectedImage + textColor = Constants.Color.BLUE_PRESSED + else + iconImage = defaultImage + textColor = Constants.Color.GRAY2 + end + + return Roact.createElement("ImageButton", { + AnchorPoint = Vector2.new(0, 1), + Position = UDim2.new((pageIndex - 1)/totalPages, 0, 1, 0), + Size = UDim2.new(1/totalPages, 0, 1, -1), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ImageTransparency = 1, + AutoButtonColor = false, + LayoutOrder = pageIndex, + + [Roact.Event.Activated] = function() + navigateToPage(associatedPageType) + end, + }, { + ButtonFrame = Roact.createElement(FitChildren.FitFrame, { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + + fitAxis = FitChildren.FitAxis.Width, + },{ + Icon = Roact.createElement("ImageLabel", { + Image = iconImage, + AnchorPoint = isLandscape and Vector2.new(0, 0.5) or Vector2.new(0.5, 0), + Position = isLandscape and UDim2.new(0, 0, 0.5, 0) or UDim2.new(0.5, 0, 0, 5), + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }), + Title = Roact.createElement(LocalizedFitTextLabel, { + AnchorPoint = isLandscape and Vector2.new(0, 0.5) or Vector2.new(0.5, 1), + Position = isLandscape and UDim2.new(0, ICON_SIZE + 8, 0.5, 0) or UDim2.new(0.5, 0, 1, -5), + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Text = AppPageLocalizationKeys[associatedPageType], + TextColor3 = textColor, + TextSize = isLandscape and 14 or 13, + TextXAlignment = isLandscape and Enum.TextXAlignment.Left or Enum.TextXAlignment.Center, + TextYAlignment = isLandscape and Enum.TextYAlignment.Center or Enum.TextYAlignment.Bottom, + Font = Enum.Font.SourceSans, + + fitAxis = FitChildren.FitAxis.Width, + }), + }), + }) +end + +local function selectCurrentPage(routeHistory) + local currentRoute = routeHistory[#routeHistory] + return currentRoute[1].name +end + +return RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + deviceOrientation = state.DeviceOrientation, + currentPage = selectCurrentPage(state.Navigation.history), + } + end, + function(dispatch) + return { + navigateToPage = function(page) + dispatch(NavigateToRoute({ { name = page } })) + end + } + end +)(BottomBarButton) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/BottomBarButton.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/BottomBarButton.spec.lua new file mode 100644 index 0000000..e3561b3 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/BottomBarButton.spec.lua @@ -0,0 +1,24 @@ +return function() + local BottomBarButton = require(script.Parent.BottomBarButton) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local AppPage = require(Modules.LuaApp.AppPage) + local Roact = require(Modules.Common.Roact) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local element = mockServices({ + BottomBarButton = Roact.createElement(BottomBarButton, { + defaultImage = "", + selectedImage = "", + associatedPageType = AppPage.Home, + }), + }, { + includeStoreProvider = true, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Carousel.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Carousel.lua new file mode 100644 index 0000000..c624d99 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Carousel.lua @@ -0,0 +1,37 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local FitChildren = require(Modules.LuaApp.FitChildren) +local Immutable = require(Modules.Common.Immutable) + +local ESTIMATED_HEIGHT = 150 -- TODO: Remove with quantum gui + +local function Carousel(props) + local layoutOrder = props.LayoutOrder + local children = props[Roact.Children] or {} + local childPadding = props.childPadding + + local carouselItems = { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + Padding = UDim.new(0, childPadding), + HorizontalAlignment = Enum.HorizontalAlignment.Left, + }), + } + + return Roact.createElement(FitChildren.FitScrollingFrame, { + Size = UDim2.new(1, 0, 0, ESTIMATED_HEIGHT), + ScrollBarThickness = 0, + BackgroundTransparency = 1, + ClipsDescendants = false, -- Needed to display drop shadows + LayoutOrder = layoutOrder, + fitFields = { + CanvasSize = FitChildren.FitAxis.Both, + Size = FitChildren.FitAxis.Height, + }, + }, Immutable.JoinDictionaries(children, carouselItems)) +end + +return Carousel \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Carousel.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Carousel.spec.lua new file mode 100644 index 0000000..5d604f3 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Carousel.spec.lua @@ -0,0 +1,11 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local Carousel = require(Modules.LuaApp.Components.Carousel) + + it("should create and destroy without errors", function() + local element = Roact.createElement(Carousel) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Chat/RoactChatWrapper.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Chat/RoactChatWrapper.lua new file mode 100644 index 0000000..23c1a6f --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Chat/RoactChatWrapper.lua @@ -0,0 +1,83 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) +local RoactAnalyticsAppStageLoaded = require(Modules.LuaApp.Services.RoactAnalyticsAppStageLoaded) +local AppGuiService = require(Modules.LuaApp.Services.AppGuiService) +local RoactServices = require(Modules.LuaApp.RoactServices) + +local NotificationType = require(Modules.LuaApp.Enum.NotificationType) + +local ChatMaster = require(Modules.ChatMaster) +local AppPage = require(Modules.LuaApp.AppPage) + +local RoactChatWrapper = Roact.PureComponent:extend("RoactChatWrapper") + +local PageTypeToChatType = { + [AppPage.Chat] = ChatMaster.Type.Default, + [AppPage.ShareGameToChat] = ChatMaster.Type.GameShare, +} + +function RoactChatWrapper:init() + self.isPageOpen = false + self.currentChatType = nil +end + +function RoactChatWrapper:didMount() + self:updateChat() +end + +function RoactChatWrapper:render() + return nil +end + +function RoactChatWrapper:didUpdate() + self:updateChat() +end + +function RoactChatWrapper:willUnmount() + self.props.chatMaster:Stop(self.currentChatType) +end + +function RoactChatWrapper:updateChat() + local chatMaster = self.props.chatMaster + local isVisible = self.props.isVisible + local pageType = self.props.pageType + local parameters = self.props.parameters + local analytics = self.props.analytics + local guiService = self.props.guiService + + if not self.isPageOpen and isVisible then + local chatType = PageTypeToChatType[pageType] + chatMaster:Start(chatType, parameters) + self.isPageOpen = true + self.currentChatType = chatType + + -- Staging broadcasting of APP_READY to accomodate for unpredictable + -- delay on the native side. + -- Once Lua tab bar is integrated, there will be no use for this + guiService:BroadcastNotification(pageType, NotificationType.APP_READY) + local appRoutes = self.props.store:getState().Navigation.history + local currentRoute = appRoutes[#appRoutes] + local currentSection = currentRoute[1] + analytics.reportAppReady(currentSection.name) + elseif self.isPageOpen and not isVisible then + chatMaster:Stop(self.currentChatType) + self.isPageOpen = false + self.currentChatType = nil + end +end + +-- While you should NOT pass around store like this, store was made visible as ChatMaster +-- is not yet Roactified and expects a store reference to be passed down. +RoactChatWrapper = RoactRodux.connect(function(store) + return { + store = store + } +end)(RoactChatWrapper) + +return RoactServices.connect({ + analytics = RoactAnalyticsAppStageLoaded, + guiService = AppGuiService, +})(RoactChatWrapper) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Chat/RoactGameShareWrapper.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Chat/RoactGameShareWrapper.lua new file mode 100644 index 0000000..7caad01 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Chat/RoactGameShareWrapper.lua @@ -0,0 +1,103 @@ +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) + +local Constants = require(Modules.LuaChat.Constants) +local Create = require(Modules.LuaChat.Create) +local GameShareComponent = require(Modules.LuaChat.Components.GameShareComponent) +local FormFactor = require(Modules.LuaApp.Enum.FormFactor) +local FlagSettings = require(Modules.LuaApp.FlagSettings) + +local isLoadingHUDOniOSEnabledForGameShare = FlagSettings.IsLoadingHUDOniOSEnabledForGameShare() +local RoactGameShareWrapper = Roact.PureComponent:extend("RoactGameShareWrapper") + +function RoactGameShareWrapper:init() + self.appState = self.props.chatMaster._appState +end + +function RoactGameShareWrapper:didMount() + local placeId = self.props.placeId + local formFactor = self.props.formFactor + + local innerFrame = Create.new"Frame" { + Name = "InnerFrame", + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(0.5, 0), + BackgroundColor3 = Constants.Color.GRAY5, + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = 2, + Create.new("UIListLayout") { + Name = "ListLayout", + SortOrder = Enum.SortOrder.LayoutOrder, + }, + } + + if formFactor == FormFactor.TABLET then + local dividerHeight = Constants.GameShareView.TABLET_HORIZONTAL_DIVIDER_HEIGHT + local viewWidth = Constants.GameShareView.TABLET_VIEW_WIDTH + + innerFrame.Size = UDim2.new(0, viewWidth, 1, -dividerHeight) + + local tabletDivider = Create.new"Frame" { + Name = "TabletDivider", + BackgroundColor3 = Constants.Color.GRAY5, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, dividerHeight), + LayoutOrder = 1, + } + tabletDivider.Parent = innerFrame + end + + self.gameShareInstance = GameShareComponent.new(self.appState, placeId, innerFrame) + self.gameShareInstance.rbx.Parent = self.shareGameWrapper + self.gameShareInstance:Start() + + if isLoadingHUDOniOSEnabledForGameShare then + spawn(function() + wait(0.1) + local GuiService = game:GetService("GuiService") + local AppPage = require(Modules.LuaApp.AppPage) + local NotificationType = require(Modules.LuaApp.Enum.NotificationType) + -- Staging broadcasting of APP_READY to accomodate for unpredictable + -- delay on the native side. + -- Once Lua tab bar is integrated, there will be no use for this + GuiService:BroadcastNotification(AppPage.ShareGameToChat, NotificationType.APP_READY) + end) + end +end + +function RoactGameShareWrapper:render() + return Roact.createElement("Frame", { + Name = "ShareGameToChatFromGameDetails", + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Constants.Color.GRAY5, + BackgroundTransparency = 1, + BorderSizePixel = 0, + [Roact.Ref] = function(rbx) + if not self.shareGameWrapper then + self.shareGameWrapper = rbx + end + end, + }) +end + +function RoactGameShareWrapper:willUnmount() + self.gameShareInstance:Stop() + self.gameShareInstance:Destruct() + self.gameShareInstance = nil +end + +RoactGameShareWrapper = RoactRodux.connect(function(store) + local state = store:getState() + + return { + store = store, + formFactor = state.FormFactor, + } +end)(RoactGameShareWrapper) + +return RoactGameShareWrapper \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ContextualMenu.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ContextualMenu.lua new file mode 100644 index 0000000..4c0829b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ContextualMenu.lua @@ -0,0 +1,100 @@ +-- +-- ContextualMenu +-- +-- This module wraps the drop-down pop-out and pop-up menus and provides some +-- common functionality for managing those menus. +-- (Contains some code from PlayTogetherContextualMenu but made more generic.) +-- + +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules +local LuaApp = Modules.LuaApp + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) + +local Constants = require(LuaApp.Constants) +local FormFactor = require(LuaApp.Enum.FormFactor) +local FramePopOut = require(LuaApp.Components.FramePopOut) +local FramePopup = require(LuaApp.Components.FramePopup) +local ListPicker = require(LuaApp.Components.ListPicker) + +local ContextualMenu = Roact.Component:extend("ContextualMenu") + +local ITEM_HEIGHT = 54 +local ITEM_WIDTH = 320 +local VISIBLE_ITEMS = 5.58 +local ITEM_FONT_SIZE = 23 +local ITEM_TEXT_FONT = Enum.Font.SourceSans +local ITEM_TEXT_COLOR = Constants.Color.GRAY1 + +function ContextualMenu:render() + -- Unpack props: + local callbackCancel = self.props.callbackCancel + local callbackSelect = self.props.callbackSelect + local formFactor = self.props.formFactor + local itemHeight = self.props.itemHeight or ITEM_HEIGHT + local itemWidth = self.props.itemWidth or ITEM_WIDTH + local menuItems = self.props.menuItems or {} + local screenShape = self.props.screenShape + + -- Calculate local vars from props: + local isTablet = (formFactor == FormFactor.TABLET) + local itemCount = #menuItems + + local listContentsWidth = 0 + if isTablet then + listContentsWidth = itemWidth + end + + local listContents = { + ListPicker = Roact.createElement(ListPicker, { + onSelectItem = callbackSelect, + items = menuItems, + + itemHeight = ITEM_HEIGHT, + itemWidth = listContentsWidth, + + textColor = ITEM_TEXT_COLOR, + textFont = ITEM_TEXT_FONT, + textSize = ITEM_FONT_SIZE, + }), + } + + local portalContents + if isTablet then + portalContents = Roact.createElement(FramePopOut, { + heightAllItems = itemHeight * itemCount, + itemWidth = itemWidth, + onCancel = callbackCancel, + parentShape = screenShape, + }, listContents) + else + portalContents = Roact.createElement(FramePopup, { + heightAllItems = itemHeight * itemCount, + heightScrollContainer = itemHeight * math.min(itemCount, VISIBLE_ITEMS), + onCancel = callbackCancel, + }, listContents) + end + + return Roact.createElement(Roact.Portal, { + target = CoreGui, + }, { + PortalUI = Roact.createElement("ScreenGui", { + ZIndexBehavior = Enum.ZIndexBehavior.Sibling, + DisplayOrder = 3 + }, { + Contents = portalContents + }), + }) +end + +ContextualMenu = RoactRodux.connect(function(store, props) + local state = store:getState() + + return { + formFactor = state.FormFactor, + } +end)(ContextualMenu) + +return ContextualMenu \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ContextualMenu.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ContextualMenu.spec.lua new file mode 100644 index 0000000..b16a8e8 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ContextualMenu.spec.lua @@ -0,0 +1,65 @@ +return function() + local LocalizationService = game:GetService("LocalizationService") + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local AppReducer = require(Modules.LuaApp.AppReducer) + local Localization = require(Modules.LuaApp.Localization) + local Roact = require(Modules.Common.Roact) + local Rodux = require(Modules.Common.Rodux) + + local ContextualMenu = require(Modules.LuaApp.Components.ContextualMenu) + local localization = Localization.new(LocalizationService.RobloxLocaleId) + local FormFactor = require(Modules.LuaApp.Enum.FormFactor) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local store = Rodux.Store.new(AppReducer, { + FormFactor = FormFactor.PHONE, + }) + + local menuItems = { + { + displayIcon = "rbxasset://textures/ui/LuaApp/icons/ic-games.png", + name = "PlayGameButton", + displayName = localization:Format("Feature.Chat.Drawer.PlayGame"), + }, + { + displayIcon = "rbxasset://textures/ui/LuaChat/icons/ic-pin.png", + name = "PinGameButton", + displayName = localization:Format("Feature.Chat.Drawer.PinGame") + }, + } + + local screenShape = { + x = 0, + y = 0, + width = 320, + height = 240, + parentWidth = 320, + parentHeight = 240, + } + + local callbackCancel = function() + -- cancelled. + end + local callbackSelect = function(item) + -- selected + end + + local element = mockServices({ + contextualMenu = Roact.createElement(ContextualMenu, { + callbackCancel = callbackCancel, + callbackSelect = callbackSelect, + menuItems = menuItems, + screenShape = screenShape, + }) + }, { + includeStoreProvider = true, + store = store + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + store:Destruct() + end) +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/DropDownList.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/DropDownList.lua new file mode 100644 index 0000000..5e10a9f --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/DropDownList.lua @@ -0,0 +1,224 @@ +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) + +local Constants = require(Modules.LuaApp.Constants) +local FormFactor = require(Modules.LuaApp.Enum.FormFactor) +local FramePopOut = require(Modules.LuaApp.Components.FramePopOut) +local FramePopup = require(Modules.LuaApp.Components.FramePopup) +local ListPicker = require(Modules.LuaApp.Components.ListPicker) + +local DROPDOWN_HEIGHT = 38 +local DROPDOWN_ARROW_MARGIN = 7 +local DROPDOWN_ARROW_SIZE = 12 +local DROPDOWN_TEXT_MARGIN = 10 + +local DEFAULT_TEXT_COLOR = Constants.Color.GRAY1 +local DEFAULT_TEXT_FONT = Enum.Font.SourceSans +local DEFAULT_TEXT_SIZE = 18 + +local DEFAULT_ITEM_TEXT_COLOR = Constants.Color.GRAY1 +local DEFAULT_ITEM_TEXT_FONT = Enum.Font.SourceSans +local DEFAULT_ITEM_TEXT_SIZE = 23 + +local ITEM_HEIGHT = 54 +local ITEM_WIDTH = 320 +local VISIBLE_ITEMS = 5.58 + +local DropDownList = Roact.Component:extend("DropDownList") + +DropDownList.defaultProps = { + height = DROPDOWN_HEIGHT, + itemHeight = ITEM_HEIGHT, + itemTextColor = DEFAULT_ITEM_TEXT_COLOR, + itemFont = DEFAULT_ITEM_TEXT_FONT, + itemTextSize = DEFAULT_ITEM_TEXT_SIZE, + itemWidth = ITEM_WIDTH, + size = UDim2.new(1, 0, 0, DROPDOWN_HEIGHT), + textColor = DEFAULT_TEXT_COLOR, + font = DEFAULT_TEXT_FONT, + textSize = DEFAULT_TEXT_SIZE, +} + +-- Set up some default state for this control: +function DropDownList:init() + self.state = { + isOpen = false, + } + + self.onActivated = function(rbx) + -- We need to know the size of the screen, so we can position the + -- popout component appropriately. So we climb up the object + -- heirachy until we find the current ScreenGui: + local screenWidth = 0 + local screenHeight = 0 + local screenGui = rbx:FindFirstAncestorOfClass("ScreenGui") + if screenGui ~= nil then + screenWidth = screenGui.AbsoluteSize.x + screenHeight = screenGui.AbsoluteSize.y + end + + self:setState({ + isOpen = true, + screenShape = { + x = rbx.AbsolutePosition.x, + y = rbx.AbsolutePosition.y, + width = rbx.AbsoluteSize.x, + height = rbx.AbsoluteSize.y, + parentWidth = screenWidth, + parentHeight = screenHeight, + }, + }) + end + + self.callbackCancel = function() + self:setState({ isOpen = false }) + end + + -- The user just selected an item, change to it: + self.callbackSelect = function(item, position) + -- Close the selector + self:setState({ + isOpen = false, + }) + -- Fire our callback to notify parent of the new index + value: + if self.props.onSelected then + return self.props.onSelected(item, position) + end + end +end + +function DropDownList:render() + local position = self.props.position + local anchorPoint = self.props.anchorPoint + local formFactor = self.props.formFactor + local height = self.props.height + local itemHeight = self.props.itemHeight + local items = self.props.items + local itemSelected = self.props.itemSelected + local itemTextColor = self.props.itemTextColor + local itemTextFont = self.props.itemFont + local itemTextSize = self.props.itemTextSize + local itemWidth = self.props.itemWidth + local layoutOrder = self.props.layoutOrder + local screenShape = self.state.screenShape + local size = self.props.size + local textColor = self.props.textColor + local textFont = self.props.font + local textSize = self.props.textSize + local isTablet = (formFactor == FormFactor.TABLET) + + -- Build up our drop-down items here for display inside our main element: + local dropdownItems = nil + + -- Show the drop down if it is enabled: + if self.state.isOpen then + -- For phones, we want the width to stretch to the screen size. + -- For tablets, we want the width from the parent. + local listContentsWidth = 0 + if isTablet then + listContentsWidth = itemWidth + end + local itemCount = #items + local listContents = { + ListPicker = Roact.createElement(ListPicker, { + onSelectItem = self.callbackSelect, + items = items, + + itemHeight = itemHeight, + itemWidth = listContentsWidth, + + textColor = itemTextColor, + textFont = itemTextFont, + textSize = itemTextSize, + }), + } + + -- Show a different style of dropdown on tablets: + local portalContents + if isTablet then + portalContents = Roact.createElement(FramePopOut, { + heightAllItems = itemHeight * itemCount, + itemWidth = itemWidth, + isAnimated = true, + onCancel = self.callbackCancel, + parentShape = screenShape, + }, listContents) + else + portalContents = Roact.createElement(FramePopup, { + heightAllItems = itemHeight * itemCount, + heightScrollContainer = itemHeight * math.min(itemCount, VISIBLE_ITEMS), + isAnimated = true, + onCancel = self.callbackCancel, + }, listContents) + end + + -- Show our contents as topmost via a portal: + dropdownItems = Roact.createElement(Roact.Portal, { + target = CoreGui, + }, { + PortalUI = Roact.createElement("ScreenGui", { + ZIndexBehavior = Enum.ZIndexBehavior.Sibling, + }, { + Contents = portalContents + }) + }) + + end + + -- Note: Padding doesn't work on text controls, so manually calculate this. + -- The size needs to fill the available space but leave room for the + -- dropdown icon and margins: + local textPadding = -((DROPDOWN_TEXT_MARGIN * 2) + DROPDOWN_ARROW_SIZE + DROPDOWN_ARROW_MARGIN) + + -- Create and return the main control itself: + return Roact.createElement("ImageButton", { + Position = position, + AnchorPoint = anchorPoint, + AutoButtonColor = false, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = false, + Image = "rbxasset://textures/ui/LuaChat/9-slice/input-default.png", + LayoutOrder = layoutOrder, + ScaleType = Enum.ScaleType.Slice, + Size = size, + SliceCenter = Rect.new(3, 3, 4, 4), + [Roact.Event.Activated] = self.onActivated, + }, { + Text = Roact.createElement("TextLabel", { + BackgroundColor3 = Constants.Color.WHITE, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = textFont, + Size = UDim2.new(1, textPadding, 0, height), + Text = itemSelected.displayName, + TextColor3 = textColor, + TextSize = textSize, + Position = UDim2.new(0, DROPDOWN_TEXT_MARGIN, 0, 0), + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + }), + Arrow = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(1, 0.5), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = "rbxasset://textures/ui/LuaApp/icons/ic-arrow-down.png", + Position = UDim2.new(1, -DROPDOWN_ARROW_MARGIN, 0.5, 0), + Size = UDim2.new(0, DROPDOWN_ARROW_SIZE, 0, DROPDOWN_ARROW_SIZE), + }), + Items = dropdownItems, + }) +end + +DropDownList = RoactRodux.connect(function(store) + local state = store:getState() + + return { + formFactor = state.FormFactor, + } +end)(DropDownList) + +return DropDownList \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/DropDownList.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/DropDownList.spec.lua new file mode 100644 index 0000000..17d73c5 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/DropDownList.spec.lua @@ -0,0 +1,44 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + local Rodux = require(Modules.Common.Rodux) + local AppReducer = require(Modules.LuaApp.AppReducer) + local FormFactor = require(Modules.LuaApp.Enum.FormFactor) + local DropDownList = require(Modules.LuaApp.Components.DropDownList) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local store = Rodux.Store.new(AppReducer, { + FormFactor = FormFactor.PHONE, + }) + + local listItems = { + { + displayName = "Featured", + displayIcon = "rbxasset://textures/ui/LuaApp/category/ic-featured.png", + }, { + displayName = "Popular", + displayIcon = "rbxasset://textures/ui/LuaApp/category/ic-popular.png", + }, { + displayName = "Top Rated", + displayIcon = "rbxasset://textures/ui/LuaApp/category/ic-top rated.png", + } + } + + local element = mockServices({ + dropDownList = Roact.createElement(DropDownList, { + itemSelected = listItems[1], + items = listItems, + size = UDim2.new(0, 300, 0, 40), + }) + }, { + includeStoreProvider = true, + store = store + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + store:Destruct() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/DropshadowFrame.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/DropshadowFrame.lua new file mode 100644 index 0000000..2ae41fa --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/DropshadowFrame.lua @@ -0,0 +1,47 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local SHADOW_SPREAD_TOP = 5 +local SHADOW_SPREAD_BOTTOM = 6 +local SHADOW_SPREAD_LEFT = 6 +local SHADOW_SPREAD_RIGHT = 6 +local SHADOW_SLICE_CENTER = Rect.new(11, 11, 12, 12) + +local function DropshadowFrame(props) + local anchorPoint = props.AnchorPoint + local layoutOrder = props.LayoutOrder + local position = props.Position + local size = props.Size + local backgroundColor3 = props.BackgroundColor3 + local children = props[Roact.Children] + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + Position = position, + Size = size, + }, { + Shadow = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, SHADOW_SPREAD_LEFT + SHADOW_SPREAD_RIGHT, 1, SHADOW_SPREAD_TOP + SHADOW_SPREAD_BOTTOM), + Position = UDim2.new(0, -SHADOW_SPREAD_LEFT, 0, -SHADOW_SPREAD_TOP), + Image = "rbxasset://textures/ui/LuaApp/9-slice/gr-shadow.png", + ScaleType = "Slice", + SliceCenter = SHADOW_SLICE_CENTER, + BackgroundTransparency = 1, + BorderSizePixel = 0, + }), + MainFrame = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = backgroundColor3, + BorderSizePixel = 0, + ZIndex = 2, + }, children), + }) +end + +return DropshadowFrame \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/DropshadowFrame.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/DropshadowFrame.spec.lua new file mode 100644 index 0000000..9b3bd1d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/DropshadowFrame.spec.lua @@ -0,0 +1,17 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local DropshadowFrame = require(Modules.LuaApp.Components.DropshadowFrame) + + it("should create and destroy without errors", function() + local element = Roact.createElement(DropshadowFrame, { + Size = UDim2.new(1, 0, 1, 0), + }, { + Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/EndOfScroll.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/EndOfScroll.lua new file mode 100644 index 0000000..84e4dba --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/EndOfScroll.lua @@ -0,0 +1,51 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local LocalizedTextLabel = require(Modules.LuaApp.Components.LocalizedTextLabel) +local LocalizedTextButton = require(Modules.LuaApp.Components.LocalizedTextButton) +local Constants = require(Modules.LuaApp.Constants) + +local END_OF_SCROLL_PADDING = 20 +local END_OF_SCROLL_FONT_SIZE = 20 +local END_OF_SCROLL_MESSAGE_HEIGHT = END_OF_SCROLL_FONT_SIZE +local END_OF_SCROLL_BUTTON_HEIGHT = END_OF_SCROLL_FONT_SIZE + 10 +local END_OF_SCROLL_HEIGHT = END_OF_SCROLL_PADDING * 2 + END_OF_SCROLL_MESSAGE_HEIGHT + END_OF_SCROLL_BUTTON_HEIGHT + +local function GamesListEndOfScroll(props) + local backToTopCallback = props.backToTopCallback + local LayoutOrder = props.LayoutOrder + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, END_OF_SCROLL_HEIGHT), + LayoutOrder = LayoutOrder, + }, { + Message = Roact.createElement(LocalizedTextLabel, { + Size = UDim2.new(1, 0, 0, END_OF_SCROLL_MESSAGE_HEIGHT), + Position = UDim2.new(0, 0, 0, END_OF_SCROLL_PADDING), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Text = "Feature.GamePage.Message.EndOfList", + TextSize = END_OF_SCROLL_FONT_SIZE, + Font = Enum.Font.SourceSans, + TextColor3 = Constants.Color.GRAY2, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + }), + BackToTopButton = Roact.createElement(LocalizedTextButton, { + Size = UDim2.new(0.5, 0, 0, END_OF_SCROLL_BUTTON_HEIGHT), + Position = UDim2.new(0.25, 0, 0, END_OF_SCROLL_MESSAGE_HEIGHT + END_OF_SCROLL_PADDING), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Text = "Feature.GamePage.Action.BackToTop", + TextSize = END_OF_SCROLL_FONT_SIZE, + Font = Enum.Font.SourceSans, + TextColor3 = Constants.Color.BLUE_PRIMARY, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + [Roact.Event.Activated] = backToTopCallback, + }), + }) +end + +return GamesListEndOfScroll \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/EndOfScroll.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/EndOfScroll.spec.lua new file mode 100644 index 0000000..d61657c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/EndOfScroll.spec.lua @@ -0,0 +1,14 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local EndOfScroll = require(Modules.LuaApp.Components.EndOfScroll) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local element = mockServices({ + EndOfScroll = Roact.createElement(EndOfScroll) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/EventReceivers/BadgeEventReceiver.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/EventReceivers/BadgeEventReceiver.lua new file mode 100644 index 0000000..1589c51 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/EventReceivers/BadgeEventReceiver.lua @@ -0,0 +1,90 @@ +local CoreGui = game:GetService("CoreGui") +local HttpService = game:GetService("HttpService") +local Players = game:GetService("Players") + +local Modules = CoreGui.RobloxGui.Modules +local RoactRodux = require(Modules.Common.RoactRodux) +local FlagSettings = require(Modules.LuaApp.FlagSettings) + +local Roact = require(Modules.Common.Roact) +local RoactServices = require(Modules.LuaApp.RoactServices) +local RoactNetworking = require(Modules.LuaApp.Services.RoactNetworking) +local isLuaAppFriendshipCreatedSignalREnabled = FlagSettings.IsLuaAppFriendshipCreatedSignalREnabled() + +local SetNotificationCount = require(Modules.LuaApp.Actions.SetNotificationCount) +local SetUserIsFriend = require(Modules.LuaApp.Actions.SetUserIsFriend) +local ApiFetchUsersFriendCount = require(Modules.LuaApp.Thunks.ApiFetchUsersFriendCount) +local FriendshipCreated = require(Modules.LuaApp.Thunks.FriendshipCreated) + +local BadgeEventReceiver = Roact.Component:extend("BadgeEventReceiver") + +function BadgeEventReceiver:init() + local setNotificationCount = self.props.setNotificationCount + local setUserIsFriend = self.props.setUserIsFriend + local apiFetchUsersFriendCount = self.props.apiFetchUsersFriendCount + local friendshipCreated = self.props.friendshipCreated + + local networking = self.props.networking + local robloxEventReceiver = self.props.RobloxEventReceiver + + if not isLuaAppFriendshipCreatedSignalREnabled then + return -- Short circuit if flag is disabled + end + self.tokens = { + robloxEventReceiver:observeEvent("UpdateNotificationBadge", "NotificationIcon", function(detail) + local eventDetails = HttpService:JSONDecode(detail) + setNotificationCount(eventDetails.badgeString) + end), + robloxEventReceiver:observeEvent("FriendshipNotifications", "FriendshipDestroyed", function(detail) + local eventDetails = HttpService:JSONDecode(detail) + local removedFriendUserId = tostring(Players.LocalPlayer.UserId) == tostring(eventDetails.EventArgs.UserId1) + and tostring(eventDetails.EventArgs.UserId2) or tostring(eventDetails.EventArgs.UserId1) + apiFetchUsersFriendCount(networking):andThen( + function() + setUserIsFriend(removedFriendUserId, false) + end + ) + end), + robloxEventReceiver:observeEvent("FriendshipNotifications", "FriendshipCreated", function(detail) + local eventDetails = HttpService:JSONDecode(detail) + local addedFriendUserId = tostring(Players.LocalPlayer.UserId) == tostring(eventDetails.EventArgs.UserId1) + and tostring(eventDetails.EventArgs.UserId2) or tostring(eventDetails.EventArgs.UserId1) + friendshipCreated(networking, addedFriendUserId) + end), + } +end + +function BadgeEventReceiver:render() +end + +function BadgeEventReceiver:willUnmount() + for _, connection in pairs(self.tokens) do + connection:Disconnect() + end +end + +BadgeEventReceiver = RoactRodux.UNSTABLE_connect2( + nil, + function(dispatch) + return { + apiFetchUsersFriendCount = function(...) + return dispatch(ApiFetchUsersFriendCount(...)) + end, + friendshipCreated = function(...) + return dispatch(FriendshipCreated(...)) + end, + setNotificationCount = function(...) + return dispatch(SetNotificationCount(...)) + end, + setUserIsFriend = function(...) + return dispatch(SetUserIsFriend(...)) + end, + } + end +)(BadgeEventReceiver) + +BadgeEventReceiver = RoactServices.connect({ + networking = RoactNetworking, +})(BadgeEventReceiver) + +return BadgeEventReceiver \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/EventReceivers/NavigationEventReceiver.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/EventReceivers/NavigationEventReceiver.lua new file mode 100644 index 0000000..b7f2ed9 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/EventReceivers/NavigationEventReceiver.lua @@ -0,0 +1,102 @@ +local CoreGui = game:GetService("CoreGui") +local HttpService = game:GetService("HttpService") +local GuiService = game:GetService("GuiService") + +local Modules = CoreGui.RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) + +local NotificationType = require(Modules.LuaApp.Enum.NotificationType) +local AppPage = require(Modules.LuaApp.AppPage) +local NavigateToRoute = require(Modules.LuaApp.Thunks.NavigateToRoute) +local NavigateDown = require(Modules.LuaApp.Thunks.NavigateDown) +local NavigateBack = require(Modules.LuaApp.Thunks.NavigateBack) + +local NavigationEventReceiver = Roact.Component:extend("NavigationEventReceiver") + +function NavigationEventReceiver:handleNavigationEvent(detail) + local eventDetails = HttpService:JSONDecode(detail) + if eventDetails.appName == AppPage.ShareGameToChat then + self.props.navigateDown({ + name = AppPage.ShareGameToChat, + detail = eventDetails.parameters.placeId, + }) + elseif eventDetails.appName == AppPage.Chat then + self.props.setPage({ + name = AppPage.Chat, + detail = eventDetails.parameters and eventDetails.parameters.conversationId, + }) + else + self.props.setPage({ + name = AppPage[eventDetails.appName] or AppPage.None, + }) + end +end + +function NavigationEventReceiver:handleBackButtonPressed() + local currentPage = self.props.currentRoute[1].name + + -- Currently, AvatarEditor and LuaChat both have their own handlers for + -- the BackButtonPressed event, and they both send BACK_BUTTON_NOT_CONSUMED. + -- To avoid sending the notification multiple times, we need to check if + -- we're on Avatar or Chat page. + -- TODO: we should remove this code, along with Avatar and Chat's code for + -- connecting with the back button signal, once they use our AppRouter. + -- Related ticket: MOBLUAPP-631 + if currentPage == AppPage.AvatarEditor or currentPage == AppPage.Chat then + return + end + + if #self.props.currentRoute > 1 then + self.props.navigateBack() + else + GuiService:BroadcastNotification("", NotificationType.BACK_BUTTON_NOT_CONSUMED) + end +end + +function NavigationEventReceiver:init() + local robloxEventReceiver = self.props.RobloxEventReceiver + + self.tokens = { + robloxEventReceiver:observeEvent("Navigations", "Destination", function(detail) + self:handleNavigationEvent(detail) + end), + robloxEventReceiver:observeEvent("Navigations", "Reload", function(detail) + self:handleNavigationEvent(detail) + end), + GuiService.ShowLeaveConfirmation:Connect(function() + self:handleBackButtonPressed() + end), + } +end + +function NavigationEventReceiver:render() +end + +function NavigationEventReceiver:willUnmount() + for _, connection in pairs(self.tokens) do + connection:Disconnect() + end +end + +return RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + currentRoute = state.Navigation.history[#state.Navigation.history] + } + end, + function(dispatch) + return { + setPage = function(page) + return dispatch(NavigateToRoute({ page })) + end, + navigateDown = function(page) + return dispatch(NavigateDown(page)) + end, + navigateBack = function() + return dispatch(NavigateBack()) + end, + } + end +)(NavigationEventReceiver) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FitTextButton.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FitTextButton.lua new file mode 100644 index 0000000..1720919 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FitTextButton.lua @@ -0,0 +1,59 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local Immutable = require(Modules.Common.Immutable) +local Text = require(Modules.Common.Text) + +local FitChildren = require(Modules.LuaApp.FitChildren) + +local FitTextButton = Roact.PureComponent:extend("FitTextButton") + +FitTextButton.defaultProps = { + fitAxis = FitChildren.FitAxis.Height, +} + +function FitTextButton:init() + self.onRef = function(rbx) + self.ref = rbx + end + + self.resize = function(rbx) + local fitAxis = self.props.fitAxis + + -- Unlike the other fit components, FitTextButton defaults to expanding only height, instead of + -- expanding on both axis. This is because expanding on both is functionally the same as expanding + -- on width only, because if a width isn't specified there's no way to know where to break the + -- lines, so it'll end up entirely on one line anyway. And the common case of a resizable text field + -- is to have a set width with an unknown number of lines depending on the length of the text. + if fitAxis == FitChildren.FitAxis.Width or fitAxis == FitChildren.FitAxis.Both then + local width = UDim.new(0, Text.GetTextWidth(rbx.Text, rbx.Font, rbx.TextSize)) + local height = fitAxis == FitChildren.FitAxis.Both + and UDim.new(0, rbx.TextSize) or rbx.Size.Height + rbx.Size = UDim2.new(width, height) + else + local height = UDim.new(0, Text.GetTextHeight(rbx.Text, rbx.Font, rbx.TextSize, + rbx.AbsoluteSize.X)) + rbx.Size = UDim2.new(rbx.Size.Width, height) + end + end +end + +function FitTextButton:render() + local fitAxis = self.props.fitAxis or FitChildren.FitAxis.Height + local frameProps = Immutable.RemoveFromDictionary(self.props, "fitAxis") + + return Roact.createElement("TextButton", + Immutable.JoinDictionaries(frameProps, { + [Roact.Ref] = self.onRef, + [Roact.Change.Text] = self.resize, + [Roact.Change.TextSize] = self.resize, + [Roact.Change.AbsoluteSize] = (fitAxis == FitChildren.FitAxis.Height) and self.resize or nil, + }) + ) +end + +function FitTextButton:didMount() + self.resize(self.ref) +end + +return FitTextButton \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FitTextButton.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FitTextButton.spec.lua new file mode 100644 index 0000000..1cbdacd --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FitTextButton.spec.lua @@ -0,0 +1,56 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local Text = require(Modules.Common.Text) + + local FitChildren = require(Modules.LuaApp.FitChildren) + + local FitTextButton = require(script.parent.FitTextButton) + + describe("FitTextButton", function() + it("should create and destroy without errors", function() + local element = Roact.createElement(FitTextButton) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should expand vertically to fit by default", function() + -- Need an extra containing frame, because it doesn't seem to set AbsoluteSize on the root UI element + local element = Roact.createElement("Frame", {}, { + Text = Roact.createElement(FitTextButton, { + Size = UDim2.new(0, 100, 0, 0), + TextSize = 16, + Font = "SourceSans", + Text = "More than 100 pixels of text", + }) + }) + local container = Instance.new("Folder") + Roact.mount(element, container, "FitTest") + + local textElement = container.FitTest.Text + local textHeight = Text.GetTextHeight(textElement.Text, textElement.Font, textElement.TextSize, 100) + + expect(textElement.Size.X.Offset).to.equal(100) + expect(textElement.Size.Y.Offset).to.equal(textHeight) + end) + + it("should expand horizontally to fit if told to", function() + local textSize = 16 + -- Need an extra containing frame, because it doesn't seem to set AbsoluteSize on the root UI element + local element = Roact.createElement("Frame", {}, { + Text = Roact.createElement(FitTextButton, { + Size = UDim2.new(0, 100, 0, textSize), + TextSize = textSize, + Font = "SourceSans", + Text = "More than 100 pixels of text", + fitAxis = FitChildren.FitAxis.Width, + }) + }) + local container = Instance.new("Folder") + Roact.mount(element, container, "FitTest") + + expect(container.FitTest.Text.Size.X.Offset > 100).to.equal(true) + expect(container.FitTest.Text.Size.Y.Offset).to.equal(16) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FitTextLabel.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FitTextLabel.lua new file mode 100644 index 0000000..0a215ea --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FitTextLabel.lua @@ -0,0 +1,59 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local Immutable = require(Modules.Common.Immutable) +local Text = require(Modules.Common.Text) + +local FitChildren = require(Modules.LuaApp.FitChildren) + +local FitTextLabel = Roact.PureComponent:extend("FitTextLabel") + +FitTextLabel.defaultProps = { + fitAxis = FitChildren.FitAxis.Height, +} + +function FitTextLabel:init() + self.onRef = function(rbx) + self.ref = rbx + end + + self.resize = function(rbx) + local fitAxis = self.props.fitAxis + + -- Unlike the other fit components, FitTextLabel defaults to expanding only height, instead of + -- expanding on both axis. This is because expanding on both is functionally the same as expanding + -- on width only, because if a width isn't specified there's no way to know where to break the + -- lines, so it'll end up entirely on one line anyway. And the common case of a resizable text field + -- is to have a set width with an unknown number of lines depending on the length of the text. + if fitAxis == FitChildren.FitAxis.Width or fitAxis == FitChildren.FitAxis.Both then + local width = UDim.new(0, Text.GetTextWidth(rbx.Text, rbx.Font, rbx.TextSize)) + local height = fitAxis == FitChildren.FitAxis.Both + and UDim.new(0, rbx.TextSize) or rbx.Size.Height + rbx.Size = UDim2.new(width, height) + else + local height = UDim.new(0, Text.GetTextHeight(rbx.Text, rbx.Font, rbx.TextSize, + rbx.AbsoluteSize.X)) + rbx.Size = UDim2.new(rbx.Size.Width, height) + end + end +end + +function FitTextLabel:render() + local fitAxis = self.props.fitAxis or FitChildren.FitAxis.Height + local frameProps = Immutable.RemoveFromDictionary(self.props, "fitAxis") + + return Roact.createElement("TextLabel", + Immutable.JoinDictionaries(frameProps, { + [Roact.Ref] = self.onRef, + [Roact.Change.Text] = self.resize, + [Roact.Change.TextSize] = self.resize, + [Roact.Change.AbsoluteSize] = (fitAxis == FitChildren.FitAxis.Height) and self.resize or nil, + }) + ) +end + +function FitTextLabel:didMount() + self.resize(self.ref) +end + +return FitTextLabel \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FitTextLabel.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FitTextLabel.spec.lua new file mode 100644 index 0000000..fd0323d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FitTextLabel.spec.lua @@ -0,0 +1,56 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local Text = require(Modules.Common.Text) + + local FitChildren = require(Modules.LuaApp.FitChildren) + + local FitTextLabel = require(script.parent.FitTextLabel) + + describe("FitTextLabel", function() + it("should create and destroy without errors", function() + local element = Roact.createElement(FitTextLabel) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should expand vertically to fit by default", function() + -- Need an extra containing frame, because it doesn't seem to set AbsoluteSize on the root UI element + local element = Roact.createElement("Frame", {}, { + Text = Roact.createElement(FitTextLabel, { + Size = UDim2.new(0, 100, 0, 0), + TextSize = 16, + Font = "SourceSans", + Text = "More than 100 pixels of text", + }) + }) + local container = Instance.new("Folder") + Roact.mount(element, container, "FitTest") + + local textElement = container.FitTest.Text + local textHeight = Text.GetTextHeight(textElement.Text, textElement.Font, textElement.TextSize, 100) + + expect(textElement.Size.X.Offset).to.equal(100) + expect(textElement.Size.Y.Offset).to.equal(textHeight) + end) + + it("should expand horizontally to fit if told to", function() + local textSize = 16 + -- Need an extra containing frame, because it doesn't seem to set AbsoluteSize on the root UI element + local element = Roact.createElement("Frame", {}, { + Text = Roact.createElement(FitTextLabel, { + Size = UDim2.new(0, 100, 0, textSize), + TextSize = textSize, + Font = "SourceSans", + Text = "More than 100 pixels of text", + fitAxis = FitChildren.FitAxis.Width, + }) + }) + local container = Instance.new("Folder") + Roact.mount(element, container, "FitTest") + + expect(container.FitTest.Text.Size.X.Offset > 100).to.equal(true) + expect(container.FitTest.Text.Size.Y.Offset).to.equal(16) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FramePopOut.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FramePopOut.lua new file mode 100644 index 0000000..b32cd46 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FramePopOut.lua @@ -0,0 +1,248 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local Constants = require(Modules.LuaApp.Constants) +local RoactMotion = require(Modules.LuaApp.RoactMotion) + +local DEFAULT_WIDTH = 320 +local SCREEN_MARGIN = 10 + +local ICON_POINTER_HEIGHT = 6 -- Note: height/width may be swapped if pointing left/right. +local ICON_POINTER_WIDTH = 12 +local ICON_POINTER_UP = "rbxasset://textures/ui/LuaApp/dropdown/gr-tip-up.png" +local ICON_POINTER_DOWN = "rbxasset://textures/ui/LuaApp/dropdown/gr-tip-down.png" +local ICON_POINTER_LEFT = "rbxasset://textures/ui/LuaApp/dropdown/gr-tip-left.png" +local ICON_POINTER_RIGHT = "rbxasset://textures/ui/LuaApp/dropdown/gr-tip-right.png" +local ICON_POINTER_OFFSET = -1 -- Offset and enlarge are used to remove a 1-pixel gap.. +local ICON_POINTER_ENLARGE = 2 -- ..between the edge of the pointer and the menu. + +local FRAME_BACKGROUND = "rbxasset://textures/ui/LuaApp/dropdown/gr-contextual menu.png" + +local ANIMATED_MENU_START_SIZE_RATIO = 0.1 -- A percentage value relative to menu's size at full expansion. + +-- Returns an interpolation between position0 and position1. +-- Returns position0 when t = 0, and position1 when t = 1. +local function lerp(t, position0, position1) + return (1 - t) * position0 + t * position1 +end + +local function findPosition(parentShape, widthContainer, heightContainer) + local positionInfo = {} + + -- Calculate the center of our trigger shape on screen: + local centershapeHorizontal = parentShape.x + (parentShape.width * 0.5) + local centerShapeVertical = parentShape.y + (parentShape.height * 0.5) + + local animatedMenuStartSize = ANIMATED_MENU_START_SIZE_RATIO * heightContainer + + -- Initially assume our pointer has a normal up/down orientation: + positionInfo.pointerHeight = ICON_POINTER_HEIGHT + positionInfo.pointerWidth = ICON_POINTER_WIDTH + + -- First calculate where our shape would be if we open up or down: + positionInfo.x = parentShape.x + (parentShape.width * 0.5) - (widthContainer * 0.5) + positionInfo.pointerX = centershapeHorizontal - (positionInfo.pointerWidth * 0.5) + + -- Check to see if the center of our trigger shape is *above* the center of the screen: + if centerShapeVertical < (parentShape.parentHeight * 0.5) then + -- Display centered *below* our trigger shape: + positionInfo.y = parentShape.y + parentShape.height + positionInfo.pointerHeight + positionInfo.height = math.min(parentShape.parentHeight - positionInfo.y - SCREEN_MARGIN, heightContainer) + + positionInfo.pointerIcon = ICON_POINTER_UP + positionInfo.pointerY = parentShape.y + parentShape.height + positionInfo.menuStartY = positionInfo.y + else + -- Display centered *above* our trigger shape: + local bottomEdge = parentShape.y - positionInfo.pointerHeight + positionInfo.height = math.min(bottomEdge - SCREEN_MARGIN, heightContainer) + positionInfo.y = bottomEdge - positionInfo.height + + positionInfo.pointerIcon = ICON_POINTER_DOWN + positionInfo.pointerY = bottomEdge + positionInfo.menuStartY = positionInfo.pointerY - animatedMenuStartSize + end + + -- Before we commit to the above, see if we're being clipped and should display at the side: + if positionInfo.height < heightContainer then + + -- Pointer is sideways, rotate width/height values to match: + positionInfo.pointerHeight = ICON_POINTER_WIDTH + positionInfo.pointerWidth = ICON_POINTER_HEIGHT + + -- Calculate vertical position: + positionInfo.height = math.min(parentShape.parentHeight - (SCREEN_MARGIN * 2), heightContainer) + positionInfo.y = centerShapeVertical - (positionInfo.height * 0.5) + if (positionInfo.y < SCREEN_MARGIN) then + positionInfo.y = SCREEN_MARGIN + elseif ((parentShape.parentHeight - SCREEN_MARGIN) < (positionInfo.y + positionInfo.height)) then + positionInfo.y = parentShape.parentHeight - SCREEN_MARGIN - positionInfo.height + end + + positionInfo.pointerY = centerShapeVertical - (positionInfo.pointerHeight * 0.5) + positionInfo.menuStartY = + positionInfo.pointerY + positionInfo.pointerHeight / 2 - animatedMenuStartSize / 2 + + if centershapeHorizontal <= (parentShape.parentWidth * 0.5) then + -- Position on the right: + positionInfo.x = parentShape.x + parentShape.width + positionInfo.pointerWidth + if (positionInfo.x + widthContainer) > (parentShape.parentWidth - SCREEN_MARGIN) then + positionInfo.x = parentShape.parentWidth - widthContainer - SCREEN_MARGIN + end + + positionInfo.pointerIcon = ICON_POINTER_LEFT + positionInfo.pointerX = positionInfo.x - positionInfo.pointerWidth + else + -- Position on the left: + positionInfo.x = parentShape.x - widthContainer - positionInfo.pointerWidth + if positionInfo.x < SCREEN_MARGIN then + positionInfo.x = SCREEN_MARGIN + end + + positionInfo.pointerIcon = ICON_POINTER_RIGHT + positionInfo.pointerX = positionInfo.x + widthContainer + end + end + + positionInfo.pointerX = positionInfo.pointerX + ICON_POINTER_OFFSET + positionInfo.pointerY = positionInfo.pointerY + ICON_POINTER_OFFSET + + positionInfo.pointerWidth = positionInfo.pointerWidth + ICON_POINTER_ENLARGE + positionInfo.pointerHeight = positionInfo.pointerHeight + ICON_POINTER_ENLARGE + + return positionInfo +end + +-- == Rules for displaying the popout box == +-- +-- By default the position should be below when there's room available. +-- Not enough space below, it should flip to display on top. +-- When the list is very long and there's no room on top or bottom, display on the right. +-- If there's no room on the right, flip and display on the left side. +-- +-- parentShape fields are: +-- x = rbx.AbsolutePosition.x, -- Position of the triggering shape (that the user clicked on to open this menu.) +-- y = rbx.AbsolutePosition.y, +-- width = rbx.AbsoluteSize.x, -- Size of the triggering shape. +-- height = rbx.AbsoluteSize.y, +-- parentWidth = rbx.Parent.AbsoluteSize.x, -- Absolute dimensions of the screen that we can fill. +-- parentHeight = rbx.Parent.AbsoluteSize.y, +-- + +local FramePopOut = Roact.PureComponent:extend("FramePopOut") + +FramePopOut.defaultProps = { + itemWidth = DEFAULT_WIDTH, +} + +function FramePopOut:init() + self.state = { + hidden = self.props.isAnimated, + } + + -- Initialize parameters for Roact Motion + self.animationPercentage = self.state.hidden and 0 or 1 + self.stiffness = nil -- use default stiffness + self.damping = nil -- use default damping + self.onRested = nil + + self.onActivated = function() + self:close() + end +end + +function FramePopOut:open() + if self.state.hidden == true and self.props.isAnimated then + self.animationPercentage = 1 + self.stiffness = nil + self.damping = nil + self.onRested = nil + + self:setState({ + hidden = false, + }) + end +end + +function FramePopOut:close() + local onCancel = self.props.onCancel + + if self.state.hidden == false and self.props.isAnimated then + self.animationPercentage = 0 + -- Closing animation should be a bit faster than the open animation + self.stiffness = 270 + self.damping = nil + self.onRested = onCancel + + self:setState({ + hidden = true, + }) + elseif not self.props.isAnimated then + onCancel() + end +end + +function FramePopOut:didMount() + self:open() +end + +-- Returns a frame for our list in the style of an anywhere-on-the-screen popup. +-- Intended use is to wrap the ListPicker control, inside a DropDownList control. +function FramePopOut:render() + local children = self.props[Roact.Children] + local heightAllItems = self.props.heightAllItems + local itemWidth = self.props.itemWidth + local parentShape = self.props.parentShape + + -- Figure out where we're going to display our list on screen: + local positionInfo = findPosition(parentShape, itemWidth, heightAllItems) + + return Roact.createElement("TextButton", { + AutoButtonColor = false, + BackgroundColor3 = Constants.Color.GRAY1, + BackgroundTransparency = 0.5, + Size = UDim2.new(1, 0, 1, 0), + Text = "", + [Roact.Event.Activated] = self.onActivated, + }, { + Pointer = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = false, + Image = positionInfo.pointerIcon, + Position = UDim2.new(0, positionInfo.pointerX, 0, positionInfo.pointerY), + Size = UDim2.new(0, positionInfo.pointerWidth, 0, positionInfo.pointerHeight), + }), + AnimatedPopout = Roact.createElement(RoactMotion.SimpleMotion, { + style = { + animationPercentage = RoactMotion.spring(self.animationPercentage, self.stiffness, self.damping), + }, + onRested = self.onRested, + render = function(values) + return Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = FRAME_BACKGROUND, + Position = UDim2.new(0, positionInfo.x, + 0, lerp(values.animationPercentage, positionInfo.menuStartY, positionInfo.y)), + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(0, itemWidth, + 0, lerp(values.animationPercentage, ANIMATED_MENU_START_SIZE_RATIO, 1) * positionInfo.height), + SliceCenter = Rect.new(3, 3, 4, 4), + }, { + Scroller = Roact.createElement("ScrollingFrame", { + BackgroundColor3 = Constants.Color.WHITE, + BackgroundTransparency = 1, + BorderSizePixel = 0, + CanvasSize = UDim2.new(0, itemWidth, 0, heightAllItems), + Position = UDim2.new(0, 0, 0, 0), + ScrollBarThickness = 0, + Size = UDim2.new(1, 0, 1, 0), + }, children ), + }) + end + }), + }) +end + +return FramePopOut \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FramePopOut.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FramePopOut.spec.lua new file mode 100644 index 0000000..1c8fb83 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FramePopOut.spec.lua @@ -0,0 +1,26 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local FramePopOut = require(Modules.LuaApp.Components.FramePopOut) + + it("should create and destroy without errors", function() + local element = Roact.createElement(FramePopOut, { + heightAllItems = 100, + heightScrollContainer = 50, + onCancel = nil, + parentShape = { + x = 10, + y = 10, + width = 100, + height = 20, + parentWidth = 600, + parentHeight = 600, + }, + }, + nil + ) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FramePopup.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FramePopup.lua new file mode 100644 index 0000000..2767588 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FramePopup.lua @@ -0,0 +1,211 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local Constants = require(Modules.LuaApp.Constants) +local FitChildren = require(Modules.LuaApp.FitChildren) +local RoactMotion = require(Modules.LuaApp.RoactMotion) + +-- Some defaults: +local SEPARATOR_HEIGHT = 1 +local CANCEL_FONT = Enum.Font.SourceSans +local CANCEL_FONT_SIZE = 23 +local CANCEL_HEIGHT = 64 +local CANCEL_TEXT_COLOR = Constants.Color.GREY1 +local CANCEL_ICON = "rbxasset://textures/ui/LuaApp/category/ic-cancel.png" + +local ICON_SIZE = 20 +local ICON_HORIZONTAL_SPACE = 20 + +local DROPDOWN_TEXT_MARGIN = 10 +local DROPDOWN_PULL_DISMISS_THRESHHOLD = 40 + +local FramePopup = Roact.PureComponent:extend("FramePopup") + +function FramePopup:init() + -- If we start dragging at the top, this is a candidate for closing the popup: + self.dragStartedAtTop = false + + self.state = { + hidden = self.props.isAnimated, + } + + -- Initialize parameters for Roact Motion + self.yAnchorPoint = self.state.hidden and 0 or 1 + self.stiffness = nil -- use default stiffness + self.damping = nil -- use default damping + self.onRested = nil + + self.closeCallback = function() + self:close() + end + + self.onInputBegin = function(rbx) + if rbx.CanvasPosition.y == 0 then + self.dragStartedAtTop = true + else + self.dragStartedAtTop = false + end + end + + self.changed = function(rbx, changed) + if changed == "CanvasPosition" then + self:onPositionChanged(rbx) + end + end + + self.onRender = function(values) + local heightAllItems = self.props.heightAllItems + local heightScrollContainer = self.props.heightScrollContainer + local children = self.props[Roact.Children] + + -- Text offset to make space for the cancel icon (padding doesn't work for text): + local iconSpacing = ICON_SIZE + (ICON_HORIZONTAL_SPACE * 2) + + return Roact.createElement(FitChildren.FitFrame, { + Size = UDim2.new(1, 0, 0, 0), + Position = UDim2.new(0, 0, 1, 0), + AnchorPoint = Vector2.new(0, values.yAnchorPoint), + BorderSizePixel = 0, + fitAxis = FitChildren.FitAxis.Height, + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Bottom, + }), + + Scroller = Roact.createElement("ScrollingFrame", { + BackgroundColor3 = Constants.Color.WHITE, + BackgroundTransparency = 0, + BorderSizePixel = 0, + CanvasSize = UDim2.new(1, 0, 0, heightAllItems), + LayoutOrder = 1, + ScrollBarThickness = 0, + Size = UDim2.new(1, 0, 0, heightScrollContainer), + + [Roact.Event.InputBegan] = self.onInputBegin, + [Roact.Event.Changed] = self.changed, + }, { + Items = Roact.createElement("TextButton", { + BackgroundColor3 = Constants.Color.WHITE, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = true, + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 0, heightAllItems), + Text = "", + }, children), + }), + + Separator = Roact.createElement("Frame", { + BackgroundColor3 = Constants.Color.GRAY4, + BackgroundTransparency = 0, + BorderSizePixel = 0, + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, SEPARATOR_HEIGHT), + }), + + Cancel = Roact.createElement("Frame", { + BorderSizePixel = 0, + LayoutOrder = 3, + BackgroundColor3 = Constants.Color.WHITE, + BackgroundTransparency = 0, + Size = UDim2.new(1, 0, 0, CANCEL_HEIGHT), + }, { + Icon = Roact.createElement("ImageButton", { + AnchorPoint = Vector2.new(0, 0.5), + AutoButtonColor = false, + BackgroundColor3 = Constants.Color.WHITE, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = false, + Image = CANCEL_ICON, + Position = UDim2.new(0, ICON_HORIZONTAL_SPACE, 0.5, 0), + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }), + Cancel = Roact.createElement("TextButton", { + AnchorPoint = Vector2.new(0, 0.5), + BackgroundColor3 = Constants.Color.WHITE, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = CANCEL_FONT, + Position = UDim2.new(0, iconSpacing, 0.5, 0), + Size = UDim2.new(1, -(iconSpacing + DROPDOWN_TEXT_MARGIN), 1, 0), + Text = "Cancel", + TextColor3 = CANCEL_TEXT_COLOR, + TextSize = CANCEL_FONT_SIZE, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + [Roact.Event.Activated] = self.closeCallback, + }), -- Cancel Button + }), -- Cancel Frame + }) -- Popup Frame + end -- Render Function +end + +function FramePopup:open() + if self.state.hidden == true and self.props.isAnimated then + self.yAnchorPoint = 1 + self.stiffness = nil + self.damping = nil + self.onRested = nil + + self:setState({ + hidden = false, + }) + end +end + +function FramePopup:close() + local onCancel = self.props.onCancel + + if self.state.hidden == false and self.props.isAnimated then + self.yAnchorPoint = 0 + -- Closing animation should be a bit faster than the open animation + self.stiffness = 270 + self.damping = nil + self.onRested = onCancel + + self:setState({ + hidden = true, + }) + elseif not self.props.isAnimated then + onCancel() + end +end + +function FramePopup:onPositionChanged(rbx) + if self.dragStartedAtTop and (rbx.CanvasPosition.y < -DROPDOWN_PULL_DISMISS_THRESHHOLD) then + self.dragStartedAtTop = false + self:close() + end +end + +function FramePopup:didMount() + self:open() +end + +-- Returns a frame for our list in the style of a full-ish screen popup. +-- Intended use is to wrap the ListPicker control, inside a DropDownList control. +function FramePopup:render() + return Roact.createElement("TextButton", { + AutoButtonColor = false, + BackgroundColor3 = Constants.Color.GRAY1, + BackgroundTransparency = 0.5, + Size = UDim2.new(1, 0, 1, 0), + Text = "", + [Roact.Event.Activated] = self.closeCallback, + }, { + AnimatedPopup = Roact.createElement(RoactMotion.SimpleMotion, { + style = { + yAnchorPoint = RoactMotion.spring(self.yAnchorPoint, self.stiffness, self.damping), + }, + onRested = self.onRested, + render = self.onRender, + }) -- Animated Popup + }) -- Transparent Popup Background +end + +return FramePopup \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FramePopup.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FramePopup.spec.lua new file mode 100644 index 0000000..7909429 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/FramePopup.spec.lua @@ -0,0 +1,18 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local FramePopup = require(Modules.LuaApp.Components.FramePopup) + + it("should create and destroy without errors", function() + local element = Roact.createElement(FramePopup, { + heightAllItems = 100, + heightScrollContainer = 50, + onCancel = nil, + }, + nil + ) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameCarousels.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameCarousels.lua new file mode 100644 index 0000000..421d7b7 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameCarousels.lua @@ -0,0 +1,59 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) + +local Constants = require(Modules.LuaApp.Constants) +local FitChildren = require(Modules.LuaApp.FitChildren) + +local GameCarousel = require(Modules.LuaApp.Components.Games.GameCarousel) + +local INTERNAL_PADDING = 15 +local CAROUSEL_PADDING_DIM = UDim.new(0, Constants.GAME_CAROUSEL_PADDING) + +local GameCarousels = Roact.PureComponent:extend("GameCarousels") + +function GameCarousels:render() + local layoutOrder = self.props.LayoutOrder + local sorts = self.props.sorts + local analytics = self.props.analytics + + local carousels = { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = UDim.new(0, INTERNAL_PADDING), + }), + Padding = Roact.createElement("UIPadding", { + PaddingTop = CAROUSEL_PADDING_DIM, + PaddingBottom = CAROUSEL_PADDING_DIM, + PaddingLeft = CAROUSEL_PADDING_DIM, + PaddingRight = CAROUSEL_PADDING_DIM, + }), + } + + for sortLayoutOrder, sortName in ipairs(sorts) do + carousels[sortLayoutOrder] = Roact.createElement(GameCarousel, { + sortName = sortName, + LayoutOrder = sortLayoutOrder, + analytics = analytics, + }) + end + + return Roact.createElement(FitChildren.FitFrame, { + Size = UDim2.new(1, 0, 0, 0), + fitFields = { Size = FitChildren.FitAxis.Height }, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + + [Roact.Change.AbsoluteSize] = self.onAbsoluteSizeChanged, + }, carousels) +end + +return RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + sorts = state.GameSortGroups[props.gameSortGroup].sorts + } + end +)(GameCarousels) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameCarousels.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameCarousels.spec.lua new file mode 100644 index 0000000..e717c4d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameCarousels.spec.lua @@ -0,0 +1,95 @@ +return function() + local GameCarousels = require(script.Parent.GameCarousels) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local AppReducer = require(Modules.LuaApp.AppReducer) + local GameSortGroup = require(Modules.LuaApp.Models.GameSortGroup) + local GameSortEntry = require(Modules.LuaApp.Models.GameSortEntry) + local GameSortContents = require(Modules.LuaApp.Models.GameSortContents) + local GameSort = require(Modules.LuaApp.Models.GameSort) + local Game = require(Modules.LuaApp.Models.Game) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + local Roact = require(Modules.Common.Roact) + local Rodux = require(Modules.Common.Rodux) + + + it("should create and destroy without errors with one carousel", function() + local gameSortGroup = GameSortGroup.mock() + local gameSort = GameSort.mock() + local gameSortContents = GameSortContents.mock() + local entry = GameSortEntry.mock() + local game = Game.mock() + gameSortContents.entries = { entry } + gameSort.name = "popular" + gameSort.displayName = "Popular" + table.insert(gameSortGroup.sorts, gameSort.name) + + local store = Rodux.Store.new(AppReducer, { + GameSortGroups = { Games = gameSortGroup, sorts = { gameSort.name } }, + GameSorts = { [gameSort.name] = gameSort }, + GameSortsContents = { [gameSort.name] = gameSortContents }, + Games = { [entry.universeId] = game }, + }) + + local element = mockServices({ + GameCarousel = Roact.createElement(GameCarousels, { + gameSortGroup = "Games", + }) + }, { + includeStoreProvider = true, + store = store + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + store:destruct() + end) + + it("should create and destroy without errors with more than one carousel", function() + local gameSortGroup = GameSortGroup.mock() + local gameSort1 = GameSort.mock() + gameSort1.name = "Sort1" + local gameSort2 = GameSort.mock() + gameSort2.name = "Sort2" + table.insert(gameSortGroup.sorts, gameSort1.name) + table.insert(gameSortGroup.sorts, gameSort2.name) + local gameSortContents1 = GameSortContents.mock() + local gameSortContents2 = GameSortContents.mock() + local entry1 = GameSortEntry.mock() + local entry2 = GameSortEntry.mock() + local game1 = Game.mock() + local game2 = Game.mock() + entry2.universeId = 666 + game2.universeId = entry2.universeId + gameSortContents1.entries = { entry1 } + gameSortContents2.entries = { entry1, entry2 } + + local store = Rodux.Store.new(AppReducer, { + GameSortGroups = { Games = gameSortGroup }, + GameSorts = { + [gameSort1.name] = gameSort1, + [gameSort2.name] = gameSort2, + }, + GameSortsContents = { + [gameSort1.name] = gameSortContents1, + [gameSort2.name] = gameSortContents2, + }, + Games = { + [entry1.universeId] = game1, + [entry2.universeId] = game2, + }, + }) + local element = mockServices({ + GameCarousel = Roact.createElement(GameCarousels, { + gameSortGroup = "Games", + }) + }, { + includeStoreProvider = true, + store = store + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + store:destruct() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/Details.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/Details.lua new file mode 100644 index 0000000..b653824 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/Details.lua @@ -0,0 +1,114 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local FitChildren = require(Modules.LuaApp.FitChildren) +local FitTextLabel = require(Modules.LuaApp.Components.FitTextLabel) +local Line = require(Modules.LuaApp.Components.Line) +local TextTable = require(Modules.LuaApp.Components.TextTable) +local Constants = require(Modules.LuaApp.Constants) + +local LocalizedTextLabel = require(Modules.LuaApp.Components.LocalizedTextLabel) + +local Details = Roact.Component:extend("Details") + +local STAT_SECTION_LANDSCAPE_HEIGHT = 60 +local STAT_SECTION_PORTRAIT_HEIGHT = 256 + +local function Layout(props) + return Roact.createElement("UIListLayout", { + Padding = props.Padding, + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + }) +end + +function Details:render() + local gameDetail = self.props.gameDetail + local layoutOrder = self.props.LayoutOrder + local padding = self.props.padding + local isMaxWidth = self.props.isMaxWidth + + local paddingDim = UDim.new(0, padding) + local statsHeight = isMaxWidth and STAT_SECTION_LANDSCAPE_HEIGHT or STAT_SECTION_PORTRAIT_HEIGHT + local statsFillDirection = isMaxWidth and Enum.FillDirection.Horizontal or Enum.FillDirection.Vertical + + local curLayout = 0 + local function nextLayout() + curLayout = curLayout + 1 + return curLayout + end + + return Roact.createElement(FitChildren.FitFrame, { + LayoutOrder = layoutOrder, + BackgroundColor3 = Constants.Color.WHITE, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 0), + }, { + Layout = Roact.createElement(Layout, { + Padding = paddingDim, + }), + Padding = Roact.createElement("UIPadding", { + PaddingTop = paddingDim, + PaddingRight = paddingDim, + PaddingBottom = paddingDim, + PaddingLeft = paddingDim, + }), + Description = Roact.createElement(FitTextLabel, { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 0), + Text = gameDetail.description, + TextXAlignment = Enum.TextXAlignment.Left, + LayoutOrder = nextLayout(), + }), + TopLine = Roact.createElement(Line, { + LayoutOrder = nextLayout(), + }), + Stats = Roact.createElement(TextTable, { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, statsHeight), + FillDirection = statsFillDirection, + LayoutOrder = nextLayout(), + + table = { + {"Feature.GameDetails.Label.Playing", gameDetail.playing}, + {"Feature.GameDetails.Label.Visits", gameDetail.visits}, + {"Feature.GameDetails.Label.Created", gameDetail.created}, + {"Feature.GameDetails.Label.Updated", gameDetail.updated}, + {"Feature.GameDetails.Label.MaxPlayers", gameDetail.maxPlayers}, + {"Feature.GameDetails.Label.Genre", gameDetail.genre}, + }, + minorAxisProps = { + { + TextColor3 = Constants.Color.GRAY2, + }, + }, + }), + BottomLine = Roact.createElement(Line, { + LayoutOrder = nextLayout(), + }), + Security = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 10), + BackgroundTransparency = 1, + LayoutOrder = nextLayout(), + }, { + Copylock = Roact.createElement(LocalizedTextLabel, { + Size = UDim2.new(1, -100, 1, 0), + Text = "Feature.GameDetails.Label.GameCopyLocked", + TextColor3 = Constants.Color.GRAY3, + BackgroundTransparency = 1, + TextXAlignment = Enum.TextXAlignment.Left, + }), + ReportAbuse = Roact.createElement(LocalizedTextLabel, { + Size = UDim2.new(0, 100, 1, 0), + Position = UDim2.new(1, -100, 0, 0), + Text = "Feature.GameDetails.Label.ReportAbuse", + TextColor3 = Constants.Color.RED_PRIMARY, + TextXAlignment = Enum.TextXAlignment.Right, + BackgroundTransparency = 1, + }) + }), + }) +end + +return Details \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/Details.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/Details.spec.lua new file mode 100644 index 0000000..7577edb --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/Details.spec.lua @@ -0,0 +1,23 @@ +return function() + local Details = require(script.Parent.Details) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local GameDetail = require(Modules.LuaApp.Models.GameDetail) + local Roact = require(Modules.Common.Roact) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + + it("should create and destroy without errors", function() + local root = mockServices({ + element = Roact.createElement(Details, { + gameDetail = GameDetail.mock(), + padding = 12, + isMaxWidth = true, + }), + }) + + local instance = Roact.mount(root) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/GameDetailsPage.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/GameDetailsPage.lua new file mode 100644 index 0000000..9dae46c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/GameDetailsPage.lua @@ -0,0 +1,272 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) + +local Overview = require(Modules.LuaApp.Components.GameDetails.Overview) +local FitChildren = require(Modules.LuaApp.FitChildren) +local TabbedFrame = require(Modules.LuaApp.Components.TabbedFrame) +local Details = require(Modules.LuaApp.Components.GameDetails.Details) + +local LocalizedTextLabel = require(Modules.LuaApp.Components.LocalizedTextLabel) +local DropshadowFrame = require(Modules.LuaApp.Components.DropshadowFrame) +local Functional = require(Modules.Common.Functional) +local Constants = require(Modules.LuaApp.Constants) +local Carousel = require(Modules.LuaApp.Components.Carousel) +local GameCard = require(Modules.LuaApp.Components.Games.GameCard) + +local GameDetailsPage = Roact.Component:extend("GameDetailsPage") + +local PADDING = 12 +local PADDING_UDIM = UDim.new(0, PADDING) +local MIN_WIDTH = 300 + 2*PADDING +local MAX_WIDTH = 1000 +local TITLE_SIZE = UDim2.new(1, 0, 0, 30) +local SHOW_SHARE_WIDTH = 370 + +local function Layout(props) + props.SortOrder = props.SortOrder or Enum.SortOrder.LayoutOrder + props.FillDirection = props.FillDirection or Enum.FillDirection.Vertical + props.Padding = props.Padding or PADDING_UDIM + return Roact.createElement("UIListLayout", props) +end + +local function Title(props) + props.Size = props.Size or TITLE_SIZE + props.BackgroundTransparency = props.BackgroundTransparency or 1 + props.TextSize = props.TextSize or 20 + props.TextYAlignment = props.TextYAlignment or Enum.TextYAlignment.Center + props.TextXAlignment = props.TextXAlignment or Enum.TextXAlignment.Left + return Roact.createElement(LocalizedTextLabel, props) +end + +local function AboutTab(props) + local gameDetail = props.gameDetail + local recommendedGames = props.recommendedGames + local navGameDetails = props.navGameDetails + + local recommendedGameCards = {} + for gameLayoutOrder, entry in ipairs(recommendedGames) do + recommendedGameCards[tostring(entry.universeId) .. tostring(entry.isSponsored)] = Roact.createElement(GameCard, { + entry = entry, + navGameDetails = navGameDetails, + LayoutOrder = gameLayoutOrder, + }) + end + + return { + Layout = Roact.createElement(Layout), + DetailsTitle = Roact.createElement(Title, { + Text = "Feature.GameDetails.Heading.Description", + LayoutOrder = 2, + }), + Details = Roact.createElement(Details, { + gameDetail = gameDetail, + LayoutOrder = 3, + padding = PADDING, + isMaxWidth = props.width == MAX_WIDTH, + }), + VIPServersTitle = Roact.createElement(Title, { + Text = "Feature.PrivateServers.Heading.VipServers", + LayoutOrder = 4, + }), + GameBadgesTitle = Roact.createElement(Title, { + Text = "Feature.GameBadges.HeadingGameBadges", + LayoutOrder = 5, + }), + RecommendedGamesTitle = Roact.createElement(Title, { + Text = "Feature.GameDetails.Heading.RecommendedGames", + LayoutOrder = 6, + }), + RecommendedGames = Roact.createElement(Carousel, { + childPadding = Constants.GAME_CAROUSEL_CHILD_PADDING, + LayoutOrder = 7, + }, recommendedGameCards) + } +end + +local function makeStoreCard(children, index, card) + children["Card " .. index] = Roact.createElement(DropshadowFrame, { + Size = UDim2.new(0, 150, 0, 240), + LayoutOrder = index, + BackgroundColor3 = Constants.Color.WHITE, + }, { + Roact.createElement("ImageLabel", { + Size = UDim2.new(0, 150, 0, 150), + BorderSizePixel = 0, + }), + Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 150, 0, 20), + Position = UDim2.new(0, 0, 0, 150), + Text = card.name + }), + }) + return children +end + +local function GridDisplay(props) + return Roact.createElement(FitChildren.FitFrame, { + Size = UDim2.new(1, 0, 0, 0), + Position = UDim2.new(0, 0, 0, 30), + BackgroundTransparency = 1, + LayoutOrder = props.LayoutOrder, + fitFields = { + Size = FitChildren.FitAxis.Height, + }, + }, Functional.FoldDictionary(props.cardData, { + Layout = Roact.createElement("UIGridLayout", { + FillDirection = Enum.FillDirection.Horizontal, + CellPadding = UDim2.new(0, PADDING, 0, PADDING), + CellSize = UDim2.new(0, 150, 0, 240), + SortOrder = Enum.SortOrder.LayoutOrder, + }), + }, makeStoreCard)) +end + +local function StoreTab(props) + local passes = props.passes + local gear = props.gear + + return { + Layout = Roact.createElement(Layout), + PassesTitle = Roact.createElement(Title, { + Text = "Feature.GamePass.Heading.PassesForThisGame", + LayoutOrder = 2, + }), + Passes = Roact.createElement(GridDisplay, { + LayoutOrder = 3, + cardData = passes, + }), + GearTitle = Roact.createElement(Title, { + Text = "Feature.GameGear.GearForThisGame", + LayoutOrder = 4, + }), + Gear = Roact.createElement(GridDisplay, { + LayoutOrder = 5, + cardData = gear, + }), + } +end + +local function LeaderboardsTab(props) + return { + Layout = Roact.createElement(Layout), + Players = Roact.createElement(Title, { + Text = "CommonUI.Features.Label.Players", + LayoutOrder = 2, + }), + Groups = Roact.createElement(Title, { + Text = "Fearture.GameLeaderboard.Label.Clans", + LayoutOrder = 3, + }), + } +end + +local function ServersTab(props) + return { + Layout = Roact.createElement(Layout), + VIPTitle = Roact.createElement(Title, { + Text = "Feature.PrivateServers.Heading.VipServers", + LayoutOrder = 2, + }), + FriendsTitle = Roact.createElement(Title, { + Text = "Feature.ServerList.Heading.ServersMyFriendsAreIn", + LayoutOrder = 3, + }), + OtherTitle = Roact.createElement(Title, { + Text = "Feature.ServerList.Heading.OtherServers", + LayoutOrder = 4, + }), + } +end + +function GameDetailsPage:recalcWidth() + local screenGui = Instance.new("ScreenGui", game.StarterGui) + + local width = screenGui.AbsoluteSize.X + screenGui:Destroy() + + width = width < MIN_WIDTH and MIN_WIDTH or width + self.props.width = width >= MAX_WIDTH and MAX_WIDTH or width +end + +function GameDetailsPage:render() + local game = self.props.game + + self.props.passes = {{ -- Dummy data until we get thunks for these + name = "Epic Content", + }, { + name = "Additional Bosses", + }, { + name = "Hacking Tools", + }} + self.props.gear = {{ + name = "Banhammer", + }, { + name = "Darkheart", + }} + self:recalcWidth() + + return Roact.createElement("Frame", { + BackgroundColor3 = Constants.Color.GRAY4, + Size = UDim2.new(1, 0, 1, 0), + }, { + Roact.createElement(FitChildren.FitScrollingFrame, { + ScrollBarThickness = 0, + BackgroundTransparency = 1, + Position = UDim2.new(0.5, -self.props.width/2, 0, 0), + Size = UDim2.new(0, self.props.width, 1, 0), + fitFields = { + CanvasSize = FitChildren.FitAxis.Height, + }, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingTop = PADDING_UDIM, + PaddingRight = PADDING_UDIM, + PaddingBottom = PADDING_UDIM, + PaddingLeft = PADDING_UDIM, + }), + Layout = Roact.createElement(Layout), + Overview = Roact.createElement(Overview, { + game = game, + padding = PADDING, + isMaxWidth = self.props.width == MAX_WIDTH, + showShare = self.props.width > SHOW_SHARE_WIDTH, + + LayoutOrder = 1, + }), + TabbedFrame = Roact.createElement(TabbedFrame, { + props = { + Size = UDim2.new(1, 0, 0, 0), + BackgroundTransparency = 1, + LayoutOrder = 2, + + fitAxis = FitChildren.FitAxis.Height, + }, + tabs = {{ + label = "Feature.GameDetails.Label.About", + content = function () return AboutTab(self.props) end, + }, { + label = "Feature.GameDetails.Label.Store", + content = function () return StoreTab(self.props) end, + }, { + label = "Feature.GameDetails.Label.Leaderboards", + content = function () return LeaderboardsTab(self.props) end, + }, { + label = "Feature.GameDetails.Label.Servers", + content = function () return ServersTab(self.props) end, + }}, + }), + }) + }) +end + +GameDetailsPage = RoactRodux.connect(function(store) + local state = store:getState() + return { + recommendedGames = state.GameSortsContents.Recommended + } +end)(GameDetailsPage) + +return GameDetailsPage \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/GameDetailsPage.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/GameDetailsPage.spec.lua new file mode 100644 index 0000000..5e9f687 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/GameDetailsPage.spec.lua @@ -0,0 +1,41 @@ +return function() + local GameDetailsPage = require(script.Parent.GameDetailsPage) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + local Rodux = require(Modules.Common.Rodux) + + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + local AppReducer = require(Modules.LuaApp.AppReducer) + local Game = require(Modules.LuaApp.Models.Game) + local GameDetail = require(Modules.LuaApp.Models.GameDetail) + local GameSortEntry = require(Modules.LuaApp.Models.GameSortEntry) + local GameSortContents = require(Modules.LuaApp.Models.GameSortContents) + + it("should create and destroy without errors", function() + local entry = GameSortEntry.mock() + local gameSortContents = GameSortContents.mock() + local game = Game.mock() + + gameSortContents.entries = { entry } + + local store = Rodux.Store.new(AppReducer, { + GameSortsContents = { Recommended = gameSortContents }, + Games = { [entry.universeId] = game }, + }) + + local element = mockServices({ + GameDetailsPage = Roact.createElement(GameDetailsPage, { + game = Game.mock(), + gameDetail = GameDetail.mock(), + }), + }, { + includeStoreProvider = true, + store = store, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/Overview.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/Overview.lua new file mode 100644 index 0000000..f8ce5be --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/Overview.lua @@ -0,0 +1,264 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local SnappingCarousel = "ImageLabel" -- require(Modules.LuaApp.Components.SnappingCarousel) +local ShareButton = "ImageButton" -- require(Modules.LuaApp.Components.ShareButton) +local Line = require(Modules.LuaApp.Components.Line) +local LargeGameVoteBar = require(Modules.LuaApp.Components.LargeGameVoteBar) +local Constants = require(Modules.LuaApp.Constants) +local FitTextLabel = require(Modules.LuaApp.Components.FitTextLabel) +local FitChildren = require(Modules.LuaApp.FitChildren) + +local LocalizedFitTextLabel = require(Modules.LuaApp.Components.LocalizedFitTextLabel) +local LocalizedTextButton = require(Modules.LuaApp.Components.LocalizedTextButton) + +local Overview = Roact.Component:extend("Overview") + +local function joinDictionaries(to, ...) + for _, dictionary in ipairs({...}) do + for key, value in pairs(dictionary) do + to[key] = value + end + end + return to +end + +local IMAGE_MAX_WIDTH = 640 +local OVERVIEW_LANDSCAPE_HEIGHT = 360 +local IMAGE_RATIO = OVERVIEW_LANDSCAPE_HEIGHT / IMAGE_MAX_WIDTH +local CALLS_TO_ACTION_WIDTH = 302 +local CALLS_TO_ACTION_HEIGHT = 190 +local TITLE_HEIGHT = 20 +local VOTE_BAR_WIDTH = 162 +local ICON_SIZE = 24 +local PLAY_BUTTON_WIDTH = 270 +local PLAY_BUTTON_HEIGHT = 53 +local PLAY_BUTTON_FONT_SIZE = 15 +local GAME_BUTTONS_PORTRAIT_HEIGHT = 80 +local GAME_BUTTONS_LANDSCAPE_HEIGHT = 140 +local SHARE_BUTTON_WIDTH = 24 +local SHARE_BUTTON_SIZE = UDim2.new(0, SHARE_BUTTON_WIDTH, 0, SHARE_BUTTON_WIDTH) +local SHARE_BUTTON_PADDING = 2 +local BY_CREATOR_PADDING = 3 +local CREATOR_LINE_HEIGHT = 20 + +function Overview:render() + local game = self.props.game + local padding = self.props.padding + local layoutOrder = self.props.LayoutOrder + local isMaxWidth = self.props.isMaxWidth + local showShare = self.props.showShare + + local gameButtonsHeight = isMaxWidth and GAME_BUTTONS_LANDSCAPE_HEIGHT or GAME_BUTTONS_PORTRAIT_HEIGHT + + -- Offset may need to add some padding between a line and the icons when in landscape. + local offset = isMaxWidth and ICON_SIZE + 4 or ICON_SIZE + local ratingButtons = { + Favorite = Roact.createElement("ImageButton", { + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + Position = UDim2.new(0, 0, 1, -offset), + BackgroundTransparency = 1, + Image = "rbxasset://textures/ui/LuaApp/icons/ic-favorite.png", + }), + FavoriteCount = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 0, 0, offset), + Position = UDim2.new(0, ICON_SIZE, 1, -offset), + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + TextColor3 = Constants.Color.ORANGE_FAVORITE, + Text = "0", + }), + Votes = Roact.createElement(LargeGameVoteBar, { + upVotes = game.totalUpVotes, + downVotes = game.totalDownVotes, + iconSize = ICON_SIZE, + frameProps = isMaxWidth and { + BackgroundTransparency = 1, + Size = UDim2.new(0, VOTE_BAR_WIDTH, 0, offset), + Position = UDim2.new(1, -VOTE_BAR_WIDTH, 1, -offset), + } or { + BackgroundTransparency = 1, + Size = UDim2.new(0.5, 0, 0, offset), + Position = UDim2.new(0.25, 0, 1, -offset), + } + }), + } + + local shareButtons = showShare and { + Facebook = Roact.createElement(ShareButton, { + Position = UDim2.new( + 1, -2*(SHARE_BUTTON_PADDING + SHARE_BUTTON_WIDTH) - SHARE_BUTTON_WIDTH, + 1, -SHARE_BUTTON_PADDING - SHARE_BUTTON_WIDTH + ), + BackgroundTransparency = 1, + Size = SHARE_BUTTON_SIZE, + Image = "rbxasset://textures/ui/LuaApp/icons/ic-facebook.png", + [Roact.Event.Activated] = function() + -- TODO: Share on facebook. + end, + }), + Twitter = Roact.createElement(ShareButton, { + Position = UDim2.new(1 + , -1*(SHARE_BUTTON_PADDING + SHARE_BUTTON_WIDTH) - SHARE_BUTTON_WIDTH, + 1, -SHARE_BUTTON_PADDING - SHARE_BUTTON_WIDTH + ), + BackgroundTransparency = 1, + Size = SHARE_BUTTON_SIZE, + Image = "rbxasset://textures/ui/LuaApp/icons/ic-twitter.png", + [Roact.Event.Activated] = function() + -- TODO: Share on Twitter. + end, + }), + Google = Roact.createElement(ShareButton, { + Position = UDim2.new( + 1, -0*(SHARE_BUTTON_PADDING + SHARE_BUTTON_WIDTH) - SHARE_BUTTON_WIDTH, + 1, -SHARE_BUTTON_PADDING - SHARE_BUTTON_WIDTH + ), + BackgroundTransparency = 1, + Size = SHARE_BUTTON_SIZE, + Image = "rbxasset://textures/ui/LuaApp/icons/ic-google.png", + [Roact.Event.Activated] = function() + -- TODO: Share on Google+. + end, + }), + } or {} + + return Roact.createElement(FitChildren.FitFrame, { + LayoutOrder = layoutOrder, + Size = UDim2.new(1, 0, 0, 0), + BackgroundColor3 = Constants.Color.WHITE, + BorderSizePixel = 0, + fitFields = { + Size = FitChildren.FitAxis.Height, + } + }, { + ListLayout = Roact.createElement("UIListLayout", { + Padding = UDim.new(0, padding), + FillDirection = isMaxWidth and Enum.FillDirection.Horizontal or Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Padding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, padding), + PaddingRight = UDim.new(0, padding), + PaddingBottom = UDim.new(0, padding), + PaddingLeft = UDim.new(0, padding), + }), + Image = Roact.createElement(SnappingCarousel, { + LayoutOrder = 1, + Size = UDim2.new(1, 0, 1, 0), + Image = game.Image, + }, { + RatioConstraint = Roact.createElement("UIAspectRatioConstraint", { + AspectRatio = 1/IMAGE_RATIO, + AspectType = Enum.AspectType.FitWithinMaxSize, + }), + SizeConstraint = Roact.createElement("UISizeConstraint", { + MaxSize = Vector2.new(IMAGE_MAX_WIDTH, OVERVIEW_LANDSCAPE_HEIGHT), + MinSize = Vector2.new(0, 0), + }), + }), + CallsToAction = Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = 2, + Size = isMaxWidth + and UDim2.new(0, CALLS_TO_ACTION_WIDTH, 0, OVERVIEW_LANDSCAPE_HEIGHT) + or UDim2.new(1, 0, 0, CALLS_TO_ACTION_HEIGHT), + }, { + ListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, padding), + }), + TopSection = Roact.createElement("Frame", { + LayoutOrder = 1, + Size = UDim2.new(1, 0, 1, -ICON_SIZE -gameButtonsHeight -2*padding), + BackgroundTransparency = 1, + }, { + ListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, padding), + }), + Title = Roact.createElement(FitTextLabel, { + BackgroundTransparency = 1, + TextSize = TITLE_HEIGHT, + TextWrapped = true, + TextYAlignment = Enum.TextYAlignment.Center, + Text = game.name, + Size = UDim2.new(1, 0, 0, TITLE_HEIGHT), + LayoutOrder = 1, + }), + ByCreatorLine = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 0, 0, CREATOR_LINE_HEIGHT), + LayoutOrder = 2, + }, { + ListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, BY_CREATOR_PADDING), + }), + By = Roact.createElement(LocalizedFitTextLabel, { + fitAxis = FitChildren.FitAxis.Width, + BackgroundTransparency = 1, + Size = UDim2.new(0, 0, 1, 0), + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + Text = "Feature.GameDetails.Label.By", + LayoutOrder = 1, + }), + Creator = Roact.createElement("TextButton", { -- TODO: make this button fit to contents + BackgroundTransparency = 1, + Size = UDim2.new(0, 300, 1, 0), + TextColor3 = Constants.Color.BLUE_PRIMARY, + Text = game.creatorName, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + LayoutOrder = 2, + [Roact.Event.Activated] = function() + -- TODO: navigate to group or user page depending on game.creatorType and Id. + end, + }), + }) + }), + GameButtons = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, gameButtonsHeight), + LayoutOrder = 2, + }, joinDictionaries({ + TopLine = Roact.createElement(Line), + Play = Roact.createElement(LocalizedTextButton, { + Size = UDim2.new(0, PLAY_BUTTON_WIDTH, 0, PLAY_BUTTON_HEIGHT), + Position = UDim2.new( + 0.5, + -PLAY_BUTTON_WIDTH/2, + 0.5, + -PLAY_BUTTON_HEIGHT/2 -(isMaxWidth and ICON_SIZE/2 or 0) + ), + BackgroundColor3 = Constants.Color.GREEN_PRIMARY, + TextSize = PLAY_BUTTON_FONT_SIZE, + TextColor3 = Color3.new(1, 1, 1), + Text = "Common.VisitGame.Label.Play", + BorderSizePixel = 0, + [Roact.Event.Activated] = function() + -- TODO: jump into the game! + end, + }), + BottomLine = Roact.createElement(Line, { + Position = UDim2.new(0, 0, 1, -1), + }), + }, isMaxWidth and ratingButtons or nil)), + UnfocusedActions = Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = 3, + Size = UDim2.new(1, 0, 0, ICON_SIZE), + }, joinDictionaries(shareButtons, not isMaxWidth and ratingButtons or nil)) + }), + }) +end + +return Overview \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/Overview.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/Overview.spec.lua new file mode 100644 index 0000000..1c07a80 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameDetails/Overview.spec.lua @@ -0,0 +1,26 @@ +return function() + local Overview = require(script.Parent.Overview) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Game = require(Modules.LuaApp.Models.Game) + local Roact = require(Modules.Common.Roact) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + + it("should create and destroy without errors", function() + local element = mockServices({ + overview = Roact.createElement(Overview, { + game = Game.mock(), + padding = 12, + isMaxWidth = false, + showShare = true, + + LayoutOrder = 1, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameThumbnail.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameThumbnail.lua new file mode 100644 index 0000000..b50f2ca --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameThumbnail.lua @@ -0,0 +1,34 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) + +local LoadableImage = require(Modules.LuaApp.Components.LoadableImage) + +local GameThumbnail = Roact.PureComponent:extend("GameThumbnail") + +function GameThumbnail:render() + local image = self.props.image + local loadingImage = self.props.loadingImage + + local size = self.props.Size + local position = self.props.Position + local borderSizePixel = self.props.BorderSizePixel + local backgroundColor3 = self.props.BackgroundColor3 + + return Roact.createElement(LoadableImage, { + Image = image, + Size = size, + Position = position, + BorderSizePixel = borderSizePixel, + BackgroundColor3 = backgroundColor3, + loadingImage = loadingImage, + }) +end + +return RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + image = state.GameThumbnails[props.universeId], + } + end +)(GameThumbnail) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameThumbnail.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameThumbnail.spec.lua new file mode 100644 index 0000000..e80080d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/GameThumbnail.spec.lua @@ -0,0 +1,30 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local Rodux = require(Modules.Common.Rodux) + local GameThumbnail = require(Modules.LuaApp.Components.GameThumbnail) + local AppReducer = require(Modules.LuaApp.AppReducer) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local store = Rodux.Store.new(AppReducer, { + GameThumbnails = { + ["70542190"] = "https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814", + }, + }) + + local element = mockServices({ + gameThumbnail = Roact.createElement(GameThumbnail, { + loadingImage = "rbxasset://textures/ui/LuaApp/icons/ic-game.png", + universeId = 70542190, + }) + }, { + includeStoreProvider = true, + store = store + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + store:destruct() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameCard.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameCard.lua new file mode 100644 index 0000000..fe7db66 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameCard.lua @@ -0,0 +1,336 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) +local Text = require(Modules.Common.Text) +local memoize = require(Modules.Common.memoize) + +local Constants = require(Modules.LuaApp.Constants) +local RoactMotion = require(Modules.LuaApp.RoactMotion) +local NotificationType = require(Modules.LuaApp.Enum.NotificationType) + +local UIScaler = require(Modules.LuaApp.Components.UIScaler) +local GameVoteBar = require(Modules.LuaApp.Components.Games.GameVoteBar) +local GameThumbnail = require(Modules.LuaApp.Components.GameThumbnail) +local LocalizedTextLabel = require(Modules.LuaApp.Components.LocalizedTextLabel) + +local AppGuiService = require(Modules.LuaApp.Services.AppGuiService) +local RoactServices = require(Modules.LuaApp.RoactServices) + +local FlagSettings = require(Modules.LuaApp.FlagSettings) + +local useCppTextTruncation = FlagSettings.UseCppTextTruncation() + +-- Define static positions on the card: +local DEFAULT_ICON_SCALE = 1 +local PRESSED_ICON_SCALE = 0.9 +local BUTTON_DOWN_STIFFNESS = 1000 +local BUTTON_DOWN_DAMPING = 50 +local BUTTON_DOWN_SPRING_PRECISION = 0.5 + +local OUTER_MARGIN = 6 +local INNER_MARGIN = 3 +local TITLE_HEIGHT = 15 +local PLAYER_COUNT_HEIGHT = 15 +local THUMB_ICON_SIZE = 12 +local VOTE_FRAME_HEIGHT = THUMB_ICON_SIZE +local SPONSOR_HEIGHT = 13 + +local VOTE_BAR_HEIGHT = 4 +local VOTE_BAR_TOP_MARGIN = 5 +local VOTE_BAR_LEFT_MARGIN = THUMB_ICON_SIZE + 3 + +local TITLE_COLOR = Constants.Color.GRAY1 +local COUNT_COLOR = Constants.Color.GRAY2 +local SPONSOR_COLOR = Constants.Color.GRAY2 +local SPONSOR_TEXT_COLOR = Constants.Color.WHITE + +local SHADOW_SPREAD_TOP = 5 +local SHADOW_SPREAD_BOTTOM = 6 +local SHADOW_SPREAD_LEFT = 6 +local SHADOW_SPREAD_RIGHT = 6 +local SHADOW_SLICE_CENTER = Rect.new(11, 11, 12, 12) + +local defaultGameIcon = "rbxasset://textures/ui/LuaApp/icons/ic-game.png" +local thumbUpImage = "rbxasset://textures/ui/LuaApp/voteBar/thumbup.png" + +local function FormatInteger(num, sep, sepCount) + assert(type(num) == "number", "FormatInteger expects a number; was given type: " .. type(num)) + + sep = sep or "," + sepCount = sepCount or 3 + + local parsedInt = string.format("%.0f", math.abs(num)) + local firstSeperatorIndex = #parsedInt % sepCount + if firstSeperatorIndex == 0 then + firstSeperatorIndex = sepCount + end + + local seperatorPattern = "(" .. string.rep("%d", sepCount) .. ")" + local seperatorReplacement = sep .. "%1" + local result = parsedInt:sub(1, firstSeperatorIndex) .. + parsedInt:sub(firstSeperatorIndex+1):gsub(seperatorPattern, seperatorReplacement) + if num < 0 then + result = "-" .. result + end + + return result +end + +local GameCard = Roact.PureComponent:extend("GameCard") + +function GameCard:eventDisconnect() + if self.onAbsolutePositionChanged then + self.onAbsolutePositionChanged:Disconnect() + self.onAbsolutePositionChanged = nil + end +end + +function GameCard:onButtonUp(buttonActivated) + if self.state.buttonDown or self.state.buttonActivated ~= buttonActivated then + self:setState({ + buttonDown = false, + buttonActivated = buttonActivated, + }) + end + self:eventDisconnect() +end + +function GameCard:onButtonDown() + if not self.state.buttonDown then + self:eventDisconnect() + self.onAbsolutePositionChanged = self.ref:GetPropertyChangedSignal("AbsolutePosition"):Connect(function() + self:onButtonUp(false) + end) + self:setState({ + buttonDown = true, + buttonActivated = false, + }) + end +end + +function GameCard:init() + -- Truncating the title is really slow so lets memoize it for later use + -- We need to memoize per instance because memoize only saves the last input + self.makeTitle = memoize(Text.Truncate) + + self.state = { + buttonDown = false, + buttonActivated = false, + } + + local openGameDetails = self.openGameDetails + self.openGameDetails = function(...) + openGameDetails(self, ...) + end + + self.onButtonInputBegan = function(_, inputObject) + if inputObject.UserInputState == Enum.UserInputState.Begin and + (inputObject.UserInputType == Enum.UserInputType.Touch or + inputObject.UserInputType == Enum.UserInputType.MouseButton1) then + self:onButtonDown() + end + end + + self.onButtonActivated = function() + self:onButtonUp(true) + end + + self.onButtonInputEnded = function() + self:onButtonUp(false) + end + + self.onRef = function(rbx) + self.ref = rbx + end +end + +local lastGameDetailsOpenTime = 0 + +function GameCard:openGameDetails() + -- This is a temporary fix to debounce when the user taps two GameCards at once. + -- Otherwise, it opens two web overlays. + -- The proper solution is in MOBLUAPP-435, and this code should be removed when that is done. + local currentTime = tick() + if currentTime < (lastGameDetailsOpenTime + 1) then + return + end + lastGameDetailsOpenTime = currentTime + + local notificationType = NotificationType.VIEW_GAME_DETAILS + self.props.guiService:BroadcastNotification(string.format("%d", self.props.game.placeId), notificationType) + + -- fire some analytics + local index = self.props.index + local reportGameDetailOpened = self.props.reportGameDetailOpened + reportGameDetailOpened(index) +end + +function GameCard:render() + local entry = self.props.entry + local size = self.props.size + local layoutOrder = self.props.layoutOrder + local game = self.props.game + + local name = game.name + local universeId = game.universeId + local totalDownVotes = game.totalDownVotes + local totalUpVotes = game.totalUpVotes + + local playerCount = entry.playerCount + local isSponsored = entry.isSponsored + + local totalVotes = totalUpVotes + totalDownVotes + local votePercentage + if totalVotes == 0 then + votePercentage = 0 + else + votePercentage = totalUpVotes / totalVotes + end + + return Roact.createElement("Frame", { + Size = UDim2.new(0, size.X, 0, size.Y), + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + + [Roact.Ref] = self.onRef, + }, { + GameButton = Roact.createElement("TextButton", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + AutoButtonColor = false, + ZIndex = 2, + + [Roact.Event.InputBegan] = self.onButtonInputBegan, + [Roact.Event.InputEnded] = self.onButtonInputEnded, + [Roact.Event.Activated] = self.onButtonActivated, + }, { + UIScaler = Roact.createElement(UIScaler, { + scaleValue = RoactMotion.spring(self.state.buttonDown and PRESSED_ICON_SCALE or + DEFAULT_ICON_SCALE, BUTTON_DOWN_STIFFNESS, BUTTON_DOWN_DAMPING, BUTTON_DOWN_SPRING_PRECISION), + onRested = self.state.buttonActivated and self.openGameDetails or nil, + }), + Shadow = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, SHADOW_SPREAD_LEFT + SHADOW_SPREAD_RIGHT, 1, SHADOW_SPREAD_TOP + SHADOW_SPREAD_BOTTOM), + Position = UDim2.new(0, -SHADOW_SPREAD_LEFT, 0, -SHADOW_SPREAD_TOP), + Image = "rbxasset://textures/ui/LuaApp/9-slice/gr-shadow.png", + ScaleType = "Slice", + SliceCenter = SHADOW_SLICE_CENTER, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ZIndex = 1, + }), + Icon = Roact.createElement(GameThumbnail, { + Size = UDim2.new(0, size.X, 0, size.X), + universeId = universeId, + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.GRAY5, + loadingImage = defaultGameIcon, + ZIndex = 2, + }), + + GameInfo = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, size.Y - size.X), + Position = UDim2.new(0, 0, 0, size.X), + BackgroundTransparency = 0, + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.WHITE, + ZIndex = 2, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, INNER_MARGIN), + }), + + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, OUTER_MARGIN), + PaddingRight = UDim.new(0, OUTER_MARGIN), + PaddingTop = UDim.new(0, OUTER_MARGIN), + }), + + Title = Roact.createElement("TextLabel", { + LayoutOrder = 1, + Size = UDim2.new(1, 0, 0, TITLE_HEIGHT), + BackgroundTransparency = 1, + BorderSizePixel = 0, + TextSize = TITLE_HEIGHT, + TextColor3 = TITLE_COLOR, + Font = Enum.Font.SourceSans, + Text = useCppTextTruncation and name + or self.makeTitle(name, Enum.Font.SourceSans, TITLE_HEIGHT, size.X-OUTER_MARGIN*2, "..."), + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, -- Center sinks the text down by 2 pixels + }), + PlayerCount = not isSponsored and Roact.createElement(LocalizedTextLabel, { + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, PLAYER_COUNT_HEIGHT), + BackgroundTransparency = 1, + BorderSizePixel = 0, + TextSize = PLAYER_COUNT_HEIGHT, + TextColor3 = COUNT_COLOR, + Font = Enum.Font.SourceSans, + Text = { "Feature.GamePage.LabelPlayingPhrase", playerCount = FormatInteger(playerCount) }, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, -- Center sinks the text down by 2 pixels + }), + VoteFrame = not isSponsored and Roact.createElement("Frame", { + LayoutOrder = 3, + Size = UDim2.new(1, 0, 0, VOTE_FRAME_HEIGHT), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + ThumbUpIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, THUMB_ICON_SIZE, 0, THUMB_ICON_SIZE), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = thumbUpImage, + }), + VoteBar = Roact.createElement(GameVoteBar, { + Size = UDim2.new(1, -THUMB_ICON_SIZE, 0, VOTE_BAR_HEIGHT), + Position = UDim2.new(0, VOTE_BAR_LEFT_MARGIN, 0, VOTE_BAR_TOP_MARGIN), + votePercentage = votePercentage, + }) + }), + }), + Sponsor = isSponsored and Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, SPONSOR_HEIGHT+OUTER_MARGIN*2), + Position = UDim2.new(0, 0, 1, 0), + AnchorPoint = Vector2.new(0, 1), + BackgroundColor3 = SPONSOR_COLOR, + BorderSizePixel = 0, + ZIndex = 3, + }, { + SponsorText = Roact.createElement(LocalizedTextLabel, { + Size = UDim2.new(1, -OUTER_MARGIN*2, 0, SPONSOR_HEIGHT), + Position = UDim2.new(0, OUTER_MARGIN, 0, OUTER_MARGIN), + BackgroundTransparency = 1, + BorderSizePixel = 0, + TextSize = SPONSOR_HEIGHT, + TextColor3 = SPONSOR_TEXT_COLOR, + Font = Enum.Font.SourceSans, + Text = "Feature.GamePage.Label.Sponsored", + }) + }), + }) + }) +end + +function GameCard:willUnmount() + self:eventDisconnect() +end + +GameCard = RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + game = state.Games[props.entry.universeId], + } + end +)(GameCard) + +return RoactServices.connect({ + guiService = AppGuiService +})(GameCard) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameCard.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameCard.spec.lua new file mode 100644 index 0000000..7e5fb18 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameCard.spec.lua @@ -0,0 +1,58 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local Rodux = require(Modules.Common.Rodux) + + local AppReducer = require(Modules.LuaApp.AppReducer) + local GameCard = require(Modules.LuaApp.Components.Games.GameCard) + local GameSortEntry = require(Modules.LuaApp.Models.GameSortEntry) + local Game = require(Modules.LuaApp.Models.Game) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local entry = GameSortEntry.mock() + local gameModel = Game.mock() + + local store = Rodux.Store.new(AppReducer, { + Games = { [entry.universeId] = gameModel }, + }) + + local element = mockServices({ + gameCard = Roact.createElement(GameCard, { + entry = entry, + size = Vector2.new(60, 60), + }) + }, { + includeStoreProvider = true, + store = store + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + store:Destruct() + end) + + it("should create and destroy without errors when a game is sponsored", function() + local entry = GameSortEntry.mock() + entry.isSponsored = true + local gameModel = Game.mock() + + local store = Rodux.Store.new(AppReducer, { + Games = { [entry.universeId] = gameModel }, + }) + + local element = mockServices({ + gameCard = Roact.createElement(GameCard, { + entry = entry, + size = Vector2.new(60, 60), + }) + }, { + includeStoreProvider = true, + store = store + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + store:Destruct() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameCarousel.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameCarousel.lua new file mode 100644 index 0000000..61d373c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameCarousel.lua @@ -0,0 +1,262 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) +local RoactServices = require(Modules.LuaApp.RoactServices) +local RoactNetworking = require(Modules.LuaApp.Services.RoactNetworking) + +local FitChildren = require(Modules.LuaApp.FitChildren) +local Constants = require(Modules.LuaApp.Constants) +local AppPage = require(Modules.LuaApp.AppPage) +local getGameCardSize = require(Modules.LuaApp.getGameCardSize) + +local SectionHeaderWithSeeAll = require(Modules.LuaApp.Components.SectionHeaderWithSeeAll) +local GameCard = require(Modules.LuaApp.Components.Games.GameCard) +local ApiFetchGamesInSort = require(Modules.LuaApp.Thunks.ApiFetchGamesInSort) + +local NavigateDown = require(Modules.LuaApp.Thunks.NavigateDown) + +local CAROUSEL_MARGIN = Constants.GAME_CAROUSEL_PADDING +local CARD_MARGIN = Constants.GAME_CAROUSEL_CHILD_PADDING +local CAROUSEL_AND_HEADER_HEIGHT = 183 + +-- We would like to start loading more before user reaches the end. +-- The default distance from the bottom of that would be 1000. +local DEFAULT_PRELOAD_DISTANCE = 1000 + +local GameCarousel = Roact.PureComponent:extend("GameCarousel") + +function GameCarousel:init() + self.state = { + cardWindowStart = 1, + cardsInWindow = 0, + gameCardSize = Vector2.new(0, 0), + } + + self.isLoadingMoreGames = false + + self.scrollingFrameRefCallback = function(rbx) + self.scrollingFrameRef = rbx + end + + self.onCanvasPositionChanged = function() + -- Since this function is spawned, it's possible that the component + -- has been destroyed. + if not self.scrollingFrameRef then + return + end + + local gameSortContents = self.props.gameSortContents + local loadMoreGames = self.loadMoreGames + local canLoadMore = gameSortContents.hasMoreRows + + if canLoadMore and not self.isLoadingMoreGames then + local canvasPosition = self.scrollingFrameRef.CanvasPosition.X + local windowWidth = self.scrollingFrameRef.AbsoluteWindowSize.X + local canvasWidth = self.scrollingFrameRef.CanvasSize.X.Offset + local loadMoreThreshold = canvasWidth - windowWidth - DEFAULT_PRELOAD_DISTANCE + + if canvasPosition > loadMoreThreshold then + self.isLoadingMoreGames = true + + loadMoreGames():andThen( + function() + self.isLoadingMoreGames = false + end, + function() + self.isLoadingMoreGames = false + end + ) + end + end + end + + self.updateCardWindowBounds = function() + if not self.scrollingFrameRef then + return + end + + local screenSize = self.props.screenSize + local containerWidth = screenSize.X - CAROUSEL_MARGIN + local windowOffset = self.scrollingFrameRef.CanvasPosition.X + + local gameCardSize, fractionalCardsPerRow = getGameCardSize(containerWidth, 0, CARD_MARGIN, 0.25) + + local cardWindowStart = math.max(1, math.floor(windowOffset / (gameCardSize.X + CARD_MARGIN))) + local cardsInWindow = math.ceil(fractionalCardsPerRow) + 2 + + local shouldUpdate = cardWindowStart ~= self.state.cardWindowStart + or cardsInWindow ~= self.state.cardsInWindow + or gameCardSize ~= self.state.gameCardSize + + if shouldUpdate then + self:setState({ + cardWindowStart = cardWindowStart, + cardsInWindow = cardsInWindow, + gameCardSize = gameCardSize, + }) + end + end + + self.onSeeAll = function() + local navigateToSort = self.props.navigateToSort + local sort = self.props.sort + local analytics = self.props.analytics + local layoutOrder = self.props.LayoutOrder + + -- show the sort + navigateToSort(self.props.sortName) + + -- report to the server that we've tapped on the SeeAll button + analytics.reportSeeAll(sort.name, layoutOrder) + end + + self.reportGameDetailOpened = function(index) + local sort = self.props.sort + local gameSortContents = self.props.gameSortContents + local analytics = self.props.analytics + + local entries = gameSortContents.entries + + local sortName = sort.name + local itemsInSort = #entries + local indexInSort = index + local entry = entries[index] + local placeId = entry.placeId + local isAd = entry.isSponsored + + analytics.reportOpenGameDetail( + placeId, + sortName, + indexInSort, + itemsInSort, + isAd) + end + + self.loadMoreGames = function(count) + local loadCount = count or Constants.DEFAULT_GAME_FETCH_COUNT + local networking = self.props.networking + local sort = self.props.sort + local gameSortContents = self.props.gameSortContents + local dispatchLoadMoreGames = self.props.dispatchLoadMoreGames + + return dispatchLoadMoreGames(networking, sort, gameSortContents.rowsRequested, loadCount, + gameSortContents.nextPageExclusiveStartId) + end +end + +function GameCarousel:render() + local sort = self.props.sort + local gameSortContents = self.props.gameSortContents + local layoutOrder = self.props.LayoutOrder + + local entries = gameSortContents.entries + + local gameCardSize = self.state.gameCardSize + local cardWindowStart = self.state.cardWindowStart + local cardsInWindow = self.state.cardsInWindow + + local cardWindowEnd = math.min(#entries, cardWindowStart + cardsInWindow - 1) + + local canvasWidth = math.max(0, #entries * (CARD_MARGIN + gameCardSize.X)) + local leftPadding = (cardWindowStart - 1) * (gameCardSize.X + CARD_MARGIN) + + local gameCards = {} + + gameCards.Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + Padding = UDim.new(0, CARD_MARGIN), + HorizontalAlignment = Enum.HorizontalAlignment.Left, + }) + + gameCards.Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, leftPadding), + }) + + for index = cardWindowStart, cardWindowEnd do + local entry = entries[index] + local key = index % cardsInWindow + + gameCards[key] = Roact.createElement(GameCard, { + entry = entry, + layoutOrder = index, + size = gameCardSize, + reportGameDetailOpened = self.reportGameDetailOpened, + index = index, + }) + end + + return Roact.createElement(FitChildren.FitFrame, { + LayoutOrder = layoutOrder, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, CAROUSEL_AND_HEADER_HEIGHT), + fitFields = { + Size = FitChildren.FitAxis.Height, + }, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + }), + Title = Roact.createElement(SectionHeaderWithSeeAll, { + LayoutOrder = 1, + text = sort.displayName, + value = sort, + onSelected = self.onSeeAll, + }), + Carousel = Roact.createElement("ScrollingFrame", { + LayoutOrder = 2, + Size = UDim2.new(1, CAROUSEL_MARGIN, 0, gameCardSize.Y), + ScrollBarThickness = 0, + BackgroundTransparency = 1, + ClipsDescendants = false, -- Needed to display drop shadows + CanvasSize = UDim2.new(0, canvasWidth, 0, gameCardSize.Y), + ScrollingDirection = Enum.ScrollingDirection.X, + + [Roact.Change.CanvasPosition] = function() + self.onCanvasPositionChanged() + spawn(self.updateCardWindowBounds) + end, + [Roact.Ref] = self.scrollingFrameRefCallback, + }, gameCards), + }) +end + +function GameCarousel:didMount() + self.updateCardWindowBounds() +end + +function GameCarousel:didUpdate(prevProps) + if self.props.screenSize ~= prevProps.screenSize then + self.updateCardWindowBounds() + end +end + +GameCarousel = RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + sort = state.GameSorts[props.sortName], + gameSortContents = state.GameSortsContents[props.sortName], + screenSize = state.ScreenSize, + } + end, + function(dispatch) + return { + navigateToSort = function(sortName) + dispatch(NavigateDown({ name = AppPage.GamesList, detail = sortName })) + end, + dispatchLoadMoreGames = function(networking, sort, startRows, maxRows, nextPageExclusiveStartId) + return dispatch(ApiFetchGamesInSort(networking, sort, true, { + startRows = startRows, + maxRows = maxRows, + exclusiveStartId = nextPageExclusiveStartId + })) + end + } + end +)(GameCarousel) + +return RoactServices.connect({ + networking = RoactNetworking, +})(GameCarousel) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameCarousel.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameCarousel.spec.lua new file mode 100644 index 0000000..41e851b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameCarousel.spec.lua @@ -0,0 +1,46 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local Rodux = require(Modules.Common.Rodux) + local AppReducer = require(Modules.LuaApp.AppReducer) + local GameSortGroup = require(Modules.LuaApp.Models.GameSortGroup) + local GameSortEntry = require(Modules.LuaApp.Models.GameSortEntry) + local GameSortContents = require(Modules.LuaApp.Models.GameSortContents) + local GameSort = require(Modules.LuaApp.Models.GameSort) + local Game = require(Modules.LuaApp.Models.Game) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + local GameCarousel = require(Modules.LuaApp.Components.Games.GameCarousel) + + it("should create and destroy without errors", function() + local gameSortGroup = GameSortGroup.mock() + local gameSortContents = GameSortContents.mock() + local gameSort = GameSort.mock() + local gameSortEntry = GameSortEntry.mock() + local gameModel = Game.mock() + gameSort.name = "popular" + gameSort.displayName = "Popular" + gameSortContents.entries = { gameSortEntry } + table.insert(gameSortGroup.sorts, gameSort.name) + + local store = Rodux.Store.new(AppReducer, { + GameSortGroups = { Games = gameSortGroup }, + GameSorts = { [gameSort.name] = gameSort }, + GameSortsContents = { [gameSort.name] = gameSortContents }, + Games = { [gameSortEntry.universeId] = gameModel }, + }) + + local element = mockServices({ + gameCarousel = Roact.createElement(GameCarousel, { + sortName = gameSort.name, + LayoutOrder = 1, + }) + }, { + includeStoreProvider = true, + store = store, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + store:Destruct() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameGrid.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameGrid.lua new file mode 100644 index 0000000..811421c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameGrid.lua @@ -0,0 +1,143 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local Constants = require(Modules.LuaApp.Constants) +local getGameCardSize = require(Modules.LuaApp.getGameCardSize) + +local GameCard = require(Modules.LuaApp.Components.Games.GameCard) + +local CARD_MARGIN = Constants.GAME_GRID_CHILD_PADDING + +local GameGrid = Roact.PureComponent:extend("GameGrid") + +function GameGrid:init() + self.state = { + cardSize = Vector2.new(0, 0), + cardsPerRow = 0, + cardWindowStart = 1, + } + + self.containerRef = Roact.createRef() + + self.isMounted = false + + self.updateCardWindowBounds = function() + if not self.containerRef.current then + return + end + + local windowSize = self.props.windowSize + local windowOffSet = -self.containerRef.current.AbsolutePosition.Y + + local newCardSize, newCardsPerRow = getGameCardSize(windowSize.X, 0, CARD_MARGIN, 0) + local topInvisibleRows = math.max(0, math.floor(windowOffSet / (newCardSize.Y + CARD_MARGIN))) + local newCardWindowStart = math.max(1, topInvisibleRows * newCardsPerRow + 1) + + local shouldUpdate = newCardSize ~= self.state.cardSize + or newCardsPerRow ~= self.state.cardsPerRow + or newCardWindowStart ~= self.state.cardWindowStart + + if shouldUpdate then + -- QuantumGui bug: when using AbsolutePosition to trigger windowing, + -- elements do not get updated correctly because the windowing is + -- happening during a UILayout. This can be temporarly fixed by + -- delaying the windowing for 1 frame. + delay(0, function() + if self.isMounted then + self:setState({ + cardSize = newCardSize, + cardsPerRow = newCardsPerRow, + cardWindowStart = newCardWindowStart, + }) + end + end) + end + end +end + +function GameGrid:render() + local entries = self.props.entries + local layoutOrder = self.props.LayoutOrder + local numberOfRowsToShow = self.props.numberOfRowsToShow + local reportGameDetailOpened = self.props.reportGameDetailOpened + local windowSize = self.props.windowSize + + local cardSize = self.state.cardSize + local cardsPerRow = self.state.cardsPerRow + local cardWindowStart = self.state.cardWindowStart + + local totalRows = math.ceil(#entries / cardsPerRow) + + if numberOfRowsToShow ~= nil then + totalRows = math.min(totalRows, numberOfRowsToShow) + end + + local totalHeight = math.max(0, cardSize.Y * totalRows + CARD_MARGIN * (totalRows - 1)) + + local cardsInWindow = (math.ceil(windowSize.Y / (cardSize.Y + CARD_MARGIN)) + 1) * cardsPerRow + local cardWindowEnd = math.min(#entries, totalRows * cardsPerRow, cardWindowStart + cardsInWindow - 1) + local topPadding = (cardWindowStart - 1) / cardsPerRow * (cardSize.Y + CARD_MARGIN) + + local gameCards = { + Layout = Roact.createElement("UIGridLayout", { + CellPadding = UDim2.new(0, CARD_MARGIN, 0, CARD_MARGIN), + CellSize = UDim2.new(0, cardSize.X, 0, cardSize.Y), + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + } + + for index = cardWindowStart, cardWindowEnd do + local entry = entries[index] + local key = index % cardsInWindow + + gameCards[key] = Roact.createElement(GameCard, { + entry = entry, + size = cardSize, + index = index, + reportGameDetailOpened = reportGameDetailOpened, + layoutOrder = index, + }) + end + + return Roact.createElement("Frame", { + -- There's a bug in UIGridLayout that, sometimes although the size + -- fit perfectly, it would fit 1 element less than desired... + -- This can be fixed with flags: + -- FFlagQuantumGui and FFlagRoundInAdorn + -- However since QuantumGui is not on for now, we will fix this + -- by giving 1 extra pixel to the width of the grid. + Size = UDim2.new(1, 1, 0, totalHeight), + LayoutOrder = layoutOrder, + BackgroundTransparency = 1, + [Roact.Ref] = self.containerRef, + [Roact.Change.AbsolutePosition] = self.updateCardWindowBounds, + }, { + -- There's some QuantumGui bug with UIPadding. So we're using Position + -- for the padding. + Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, topPadding), + BackgroundTransparency = 1, + }, gameCards) + }) +end + +function GameGrid:didMount() + self.isMounted = true + self.updateCardWindowBounds() +end + +function GameGrid:willUnmount() + self.isMounted = false +end + +function GameGrid:didUpdate(prevProps) + if self.props.windowSize ~= prevProps.windowSize then + self.updateCardWindowBounds() + end +end + +return GameGrid \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameGrid.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameGrid.spec.lua new file mode 100644 index 0000000..11ae491 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameGrid.spec.lua @@ -0,0 +1,38 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + local Rodux = require(Modules.Common.Rodux) + + local AppReducer = require(Modules.LuaApp.AppReducer) + local GameGrid = require(Modules.LuaApp.Components.Games.GameGrid) + local GameSortEntry = require(Modules.LuaApp.Models.GameSortEntry) + local Game = require(Modules.LuaApp.Models.Game) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local entry = GameSortEntry.mock() + local game = Game.mock() + + local entries = { entry } + + local store = Rodux.Store.new(AppReducer, { + Games = { [entry.universeId] = game }, + }) + + local element = mockServices({ + gameGrid = Roact.createElement(GameGrid, { + LayoutOrder = 7, + entries = entries, + numberOfRowsToShow = 1, + windowSize = Vector2.new(500, 800), + }) + }, { + includeStoreProvider = true, + store = store, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameVoteBar.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameVoteBar.lua new file mode 100644 index 0000000..733326f --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GameVoteBar.lua @@ -0,0 +1,41 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local Constants = require(Modules.LuaApp.Constants) + +local VOTE_BAR_BACKGROUND_COLOR = Constants.Color.GRAY3 +local VOTE_BAR_FOREGROUND_COLOR = Constants.Color.GRAY2 + +local voteBarImage = "rbxasset://textures/ui/LuaApp/voteBar/bar.png" +local VOTE_BAR_IMAGE_SIZE = Vector2.new(56, 4) + +local GameVoteBar = Roact.PureComponent:extend("GameVoteBar") + +function GameVoteBar:render() + local size = self.props.Size + local position = self.props.Position + local votePercentage = self.props.votePercentage + local voteEmptyPercentage = 1 - votePercentage + + return Roact.createElement("ImageLabel", { + Size = size, + Position = position, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = voteBarImage, + ImageColor3 = VOTE_BAR_BACKGROUND_COLOR, + }, { + VotePercentage = Roact.createElement("ImageLabel", { + Size = UDim2.new(votePercentage, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = voteBarImage, + ImageColor3 = VOTE_BAR_FOREGROUND_COLOR, + ImageRectSize = VOTE_BAR_IMAGE_SIZE, + ImageRectOffset = Vector2.new(-VOTE_BAR_IMAGE_SIZE.X * voteEmptyPercentage, 0), + }), + }) +end + +return GameVoteBar \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GamesHub.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GamesHub.lua new file mode 100644 index 0000000..0a7c52b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GamesHub.lua @@ -0,0 +1,94 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) +local RoactAnalyticsGamesPage = require(Modules.LuaApp.Services.RoactAnalyticsGamesPage) +local RoactNetworking = require(Modules.LuaApp.Services.RoactNetworking) +local RoactServices = require(Modules.LuaApp.RoactServices) + +local Constants = require(Modules.LuaApp.Constants) +local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) + +local TopBar = require(Modules.LuaApp.Components.TopBar) +local RefreshScrollingFrame = require(Modules.LuaApp.Components.RefreshScrollingFrame) +local ApiFetchGamesData = require(Modules.LuaApp.Thunks.ApiFetchGamesData) +local GameCarousels = require(Modules.LuaApp.Components.GameCarousels) +local TokenRefreshComponent = require(Modules.LuaApp.Components.TokenRefreshComponent) +local LoadingBar = require(Modules.LuaApp.Components.LoadingBar) + +local GamesHub = Roact.PureComponent:extend("GamesHub") + +function GamesHub:init() + self.refresh = function() + return self.props.refresh(self.props.networking) + end +end + +function GamesHub:render() + local fetchedGamesPageData = self.props.gamesPageDataStatus == RetrievalStatus.Done + local topBarHeight = self.props.topBarHeight + local analytics = self.props.analytics + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BorderSizePixel = 0, + }, { + TokenRefreshComponent = Roact.createElement(TokenRefreshComponent, { + sortToRefresh = Constants.GameSortGroups.Games, + }), + TopBar = Roact.createElement(TopBar, { + showBuyRobux = true, + showNotifications = true, + showSearch = true, + ZIndex = 2, + }), + Loader = not fetchedGamesPageData and Roact.createElement("Frame", { + BackgroundTransparency = 0, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, topBarHeight/2), + Size = UDim2.new(1, 0, 1, -topBarHeight), + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.GRAY4, + }, { + LoadingIndicator = Roact.createElement(LoadingBar), + }), + Scroller = fetchedGamesPageData and Roact.createElement(RefreshScrollingFrame, { + Position = UDim2.new(0, 0, 0, topBarHeight), + Size = UDim2.new(1, 0, 1, -topBarHeight), + BackgroundColor3 = Constants.Color.GRAY4, + CanvasSize = UDim2.new(1, 0, 0, 0), + refresh = self.refresh, + }, { + --[[ + Adding UIListLayout to go around the issue with FitChildren wrongly + calculating when the AbsolutePosition of its only child is negative + ]] + layout = Roact.createElement("UIListLayout"), + GameCarousels = Roact.createElement(GameCarousels, { + gameSortGroup = Constants.GameSortGroups.Games, + analytics = analytics, + }), + }), + }) +end + +GamesHub = RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + topBarHeight = state.TopBar.topBarHeight, + gamesPageDataStatus = state.Startup.GamesPageDataStatus, + } + end, + function(dispatch) + return { + refresh = function(networking) + return dispatch(ApiFetchGamesData(networking)) + end, + } + end +)(GamesHub) + +return RoactServices.connect({ + networking = RoactNetworking, + analytics = RoactAnalyticsGamesPage, +})(GamesHub) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GamesHub.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GamesHub.spec.lua new file mode 100644 index 0000000..26b7a45 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GamesHub.spec.lua @@ -0,0 +1,21 @@ +return function() + local GamesHub = require(script.Parent.GamesHub) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local element = mockServices({ + GamesHub = Roact.createElement(GamesHub, { + topBarHeight = 60, + }) + }, { + includeStoreProvider = true, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GamesList.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GamesList.lua new file mode 100644 index 0000000..ea442c5 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GamesList.lua @@ -0,0 +1,221 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) +local memoize = require(Modules.Common.memoize) +local RoactServices = require(Modules.LuaApp.RoactServices) +local RoactNetworking = require(Modules.LuaApp.Services.RoactNetworking) +local RoactAnalyticsGamesPage = require(Modules.LuaApp.Services.RoactAnalyticsGamesPage) +local RoactAnalyticsHomePage = require(Modules.LuaApp.Services.RoactAnalyticsHomePage) + +local Constants = require(Modules.LuaApp.Constants) +local AppPage = require(Modules.LuaApp.AppPage) + +local NavigateSideways = require(Modules.LuaApp.Thunks.NavigateSideways) + +local DropDownList = require(Modules.LuaApp.Components.DropDownList) +local GameGrid = require(Modules.LuaApp.Components.Games.GameGrid) +local TopBar = require(Modules.LuaApp.Components.TopBar) +local RefreshScrollingFrame = require(Modules.LuaApp.Components.RefreshScrollingFrame) +local ApiFetchGamesData = require(Modules.LuaApp.Thunks.ApiFetchGamesData) +local ApiFetchGamesInSort = require(Modules.LuaApp.Thunks.ApiFetchGamesInSort) + +local GAME_GRID_PADDING = Constants.GAME_GRID_PADDING +local DROPDOWN_HEIGHT = 38 +local DROPDOWN_SECTION_HEADER_GAP = 12 +local SECTION_HEADER_HEIGHT = Constants.SECTION_HEADER_HEIGHT +local SECTION_HEADER_GAME_GRID_GAP = 14 +local TOP_SECTION_HEIGHT = DROPDOWN_HEIGHT + DROPDOWN_SECTION_HEADER_GAP + + SECTION_HEADER_HEIGHT + SECTION_HEADER_GAME_GRID_GAP + +local GamesList = Roact.PureComponent:extend("GamesList") + +function GamesList:init() + self.refresh = function() + return self.props.dispatchRefresh(self.props.networking, self.props.sortName) + end + + self.loadMoreGames = function(count) + local loadCount = count or Constants.DEFAULT_GAME_FETCH_COUNT + local sorts = self.props.sorts + local selectedSortName = self.props.sortName + local dispatchLoadMoreGames = self.props.dispatchLoadMoreGames + local networking = self.props.networking + + local selectedSort = sorts[selectedSortName] + + return dispatchLoadMoreGames(networking, selectedSort, selectedSort.rowsRequested, loadCount, + selectedSort.nextPageExclusiveStartId) + end + + self.navigateToSort = function(sort, position) + self.props.analytics.reportFilterChange(sort.name, position) + self.props.navigateToSort(sort) + end + + self.reportGameDetailOpened = function(index) + local selectedSortName = self.props.sortName + local sorts = self.props.sorts + local analytics = self.props.analytics + + local selectedSort = sorts[selectedSortName] + local entries = selectedSort.entries + + local itemsInSort = #entries + local entry = entries[index] + local placeId = entry.placeId + local isAd = entry.isSponsored + + analytics.reportOpenGameDetail( + placeId, + selectedSortName, + index, + itemsInSort, + isAd) + end +end + +function GamesList:render() + local topBarHeight = self.props.topBarHeight + local selectedSortName = self.props.sortName + local sorts = self.props.sorts + local screenSize = self.props.screenSize + + local selectedSort = sorts[selectedSortName] + + -- Build up the outer frame of the page, to include our components: + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BorderSizePixel = 0, + },{ + TopBar = Roact.createElement(TopBar, { + ZIndex = 2, + showBackButton = true, + showBuyRobux = true, + showNotifications = true, + showSearch = true, + }), + Scroller = Roact.createElement(RefreshScrollingFrame, { + Position = UDim2.new(0, 0, 0, topBarHeight), + Size = UDim2.new(1, 0, 1, -topBarHeight), + BackgroundColor3 = Constants.Color.GRAY4, + CanvasSize = UDim2.new(1, 0, 0, 0), + refresh = self.refresh, + onLoadMore = selectedSort.hasMoreRows and self.loadMoreGames, + -- If there're no more games, create an end section. + createEndOfScrollElement = not selectedSort.hasMoreRows, + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, GAME_GRID_PADDING), + PaddingRight = UDim.new(0, GAME_GRID_PADDING), + PaddingTop = UDim.new(0, GAME_GRID_PADDING), + }), + TopSection = Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = 1, + Size = UDim2.new(1, 0, 0, TOP_SECTION_HEIGHT), + }, { + DropDown = Roact.createElement(DropDownList, { + size = UDim2.new(1, 0, 0, DROPDOWN_HEIGHT), + position = UDim2.new(0, 0, 0, 0), + itemSelected = selectedSort, + items = sorts, + onSelected = self.navigateToSort, + }), + Title = Roact.createElement("TextLabel", { + Text = selectedSort.displayName, + Position = UDim2.new(0, 0, 0, DROPDOWN_HEIGHT + DROPDOWN_SECTION_HEADER_GAP), + TextSize = SECTION_HEADER_HEIGHT, + Font = Enum.Font.SourceSansLight, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + BackgroundTransparency = 1, + TextTruncate = Enum.TextTruncate.AtEnd, + Size = UDim2.new(1, 0, 0, SECTION_HEADER_HEIGHT) + }), + }), + ["GameGrid " .. selectedSortName] = Roact.createElement(GameGrid, { + LayoutOrder = 2, + entries = selectedSort.entries, + reportGameDetailOpened = self.reportGameDetailOpened, + windowSize = Vector2.new(screenSize.X - 2 * GAME_GRID_PADDING, screenSize.Y), + }), + }), + }) +end + +local function sortSorts(a, b) + return a.displayName < b.displayName +end + +local selectSorts = memoize(function(gameSorts, gameSortsContents) + local sorts = {} + + for _, sortInfo in pairs(gameSorts) do + local sort = sortInfo + local gameSortContents = gameSortsContents[sort.name] + + -- These fields are used for infinite scroll. + sort.entries = gameSortContents.entries + sort.rowsRequested = gameSortContents.rowsRequested + sort.hasMoreRows = gameSortContents.hasMoreRows + sort.nextPageExclusiveStartId = gameSortContents.nextPageExclusiveStartId + + table.insert(sorts, sort) + sorts[sort.name] = sort + end + + table.sort(sorts, sortSorts) + return sorts +end) + +local selectAnalytics = function(appRoutes, gamesAnalytics, homeAnalytics) + local currentRoute = appRoutes[#appRoutes] + local currentPage = currentRoute[1].name + if currentPage == AppPage.Games then + return gamesAnalytics + elseif currentPage == AppPage.Home then + return homeAnalytics + else + assert(false, string.format("Can not provide GamesList analytics for current page %s.", currentPage)) + end +end + +GamesList = RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + sorts = selectSorts(state.GameSorts, state.GameSortsContents), + topBarHeight = state.TopBar.topBarHeight, + analytics = selectAnalytics(state.Navigation.history, props.gamesAnalytics, props.homeAnalytics), + screenSize = state.ScreenSize, + } + end, + function(dispatch) + return { + dispatchRefresh = function(networking, sortName) + return dispatch(ApiFetchGamesData(networking, nil, sortName)) + end, + navigateToSort = function(sort) + dispatch(NavigateSideways({ name = AppPage.GamesList, detail = sort.name })) + end, + dispatchLoadMoreGames = function(networking, sort, startRows, maxRows, nextPageExclusiveStartId) + return dispatch(ApiFetchGamesInSort(networking, sort, true, { + startRows = startRows, + maxRows = maxRows, + exclusiveStartId = nextPageExclusiveStartId + })) + end + } + end +)(GamesList) + +return RoactServices.connect({ + networking = RoactNetworking, + gamesAnalytics = RoactAnalyticsGamesPage, + homeAnalytics = RoactAnalyticsHomePage, +})(GamesList) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GamesList.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GamesList.spec.lua new file mode 100644 index 0000000..d75a71d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Games/GamesList.spec.lua @@ -0,0 +1,81 @@ +return function() + local GamesList = require(script.Parent.GamesList) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + local Rodux = require(Modules.Common.Rodux) + local AppReducer = require(Modules.LuaApp.AppReducer) + local GameSortGroup = require(Modules.LuaApp.Models.GameSortGroup) + local GameSortEntry = require(Modules.LuaApp.Models.GameSortEntry) + local GameSortContents = require(Modules.LuaApp.Models.GameSortContents) + local GameSort = require(Modules.LuaApp.Models.GameSort) + local Game = require(Modules.LuaApp.Models.Game) + local AppPage = require(Modules.LuaApp.AppPage) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local gameSortGroup = GameSortGroup.mock() + local gameSortContents = GameSortContents.mock() + local gameSort = GameSort.mock() + local entry = GameSortEntry.mock() + local game = Game.mock() + gameSort.name = "popular" + gameSort.displayName = "Popular" + gameSortContents.entries = { entry } + table.insert(gameSortGroup.sorts, gameSort.name) + + local store = Rodux.Store.new(AppReducer, { + GameSortGroups = { Games = gameSortGroup }, + GameSorts = { [gameSort.name] = gameSort }, + GameSortsContents = { [gameSort.name] = gameSortContents }, + Games = { [entry.universeId] = game }, + }) + + local element = mockServices({ + GamesList = Roact.createElement(GamesList, { + sortName = gameSort.name, + }) + }, { + includeStoreProvider = true, + store = store, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should assert on creating analytics if not in the path of home page or games hub", function() + local gameSortGroup = GameSortGroup.mock() + local gameSort = GameSort.mock() + local entry = GameSortEntry.mock() + local game = Game.mock() + gameSort.name = "popular" + gameSort.displayName = "Popular" + table.insert(gameSortGroup.sorts, gameSort.name) + + local store = Rodux.Store.new(AppReducer, { + GameSortGroups = { Games = gameSortGroup }, + GameSorts = { [gameSort.name] = gameSort }, + EntriesInSort = { [gameSort.name] = { entry } }, + Games = { [entry.universeId] = game }, + Navigation = { + history = { { { name = AppPage.Chat } } }, + lockTimer = 0, + } + }) + + local element = mockServices({ + GamesList = Roact.createElement(GamesList, { + sortName = gameSort.name, + }) + }, { + includeStoreProvider = true, + store = store, + }) + + expect(function() + Roact.mount(element) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeFTUEGameGrid.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeFTUEGameGrid.lua new file mode 100644 index 0000000..9102318 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeFTUEGameGrid.lua @@ -0,0 +1,113 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) + +local Constants = require(Modules.LuaApp.Constants) +local FitChildren = require(Modules.LuaApp.FitChildren) +local FormFactor = require(Modules.LuaApp.Enum.FormFactor) + +local GameGrid = require(Modules.LuaApp.Components.Games.GameGrid) +local SectionHeader = require(Modules.LuaApp.Components.SectionHeader) + +local FTUE_NUMBER_OF_ROWS_FOR_GRID = { + [FormFactor.PHONE] = 4, + [FormFactor.TABLET] = 2, +} + +local GAME_GRID_PADDING = Constants.GAME_GRID_PADDING +local SECTION_HEADER_HEIGHT = Constants.SECTION_HEADER_HEIGHT +local SECTION_HEADER_GAME_GRID_GAP = 12 +local TOP_SECTION_HEIGHT = SECTION_HEADER_HEIGHT + SECTION_HEADER_GAME_GRID_GAP + +local HomeFTUEGameGrid = Roact.PureComponent:extend("HomeFTUEGameGrid") + +function HomeFTUEGameGrid:init() + self.reportGameDetailOpened = function(index) + local sort = self.props.sort + local gameSortContents = self.props.gameSortContents + local analytics = self.props.analytics + + local entries = gameSortContents.entries + + local itemsInSort = #entries + local entry = entries[index] + local placeId = entry.placeId + local isAd = entry.isSponsored + + analytics.reportOpenGameDetail( + placeId, + sort.name, + index, + itemsInSort, + isAd) + end +end + +function HomeFTUEGameGrid:render() + local sort = self.props.sort + local gameSortContents = self.props.gameSortContents + local formFactor = self.props.formFactor + local screenSize = self.props.screenSize + local layoutOrder = self.props.LayoutOrder + local hasTopPadding = self.props.hasTopPadding + + local paddingTop = hasTopPadding and GAME_GRID_PADDING or 0; + + return Roact.createElement(FitChildren.FitFrame, { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + fitFields = { + Size = FitChildren.FitAxis.Height + }, + LayoutOrder = layoutOrder, + },{ + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, GAME_GRID_PADDING), + PaddingRight = UDim.new(0, GAME_GRID_PADDING), + PaddingTop = UDim.new(0, paddingTop), + }), + SectionHeader = Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = 1, + Size = UDim2.new(1, 0, 0, TOP_SECTION_HEIGHT), + }, { + Title = Roact.createElement(SectionHeader, { + text = sort.displayName, + }), + }), + ["GameGrid " .. sort.name] = Roact.createElement(GameGrid, { + LayoutOrder = 2, + entries = gameSortContents.entries, + reportGameDetailOpened = self.reportGameDetailOpened, + numberOfRowsToShow = FTUE_NUMBER_OF_ROWS_FOR_GRID[formFactor], + windowSize = Vector2.new(screenSize.X - 2 * GAME_GRID_PADDING, screenSize.Y), + }), + }) +end + +local selectFTUESortName = function(sortGroups) + local homeSortGroup = Constants.GameSortGroups.HomeGames + local sorts = sortGroups[homeSortGroup].sorts + + -- This isn't the cleanest thing, but I can't figure out a better way + return sorts[1] +end + +return RoactRodux.UNSTABLE_connect2( + function(state, props) + local sortName = selectFTUESortName(state.GameSortGroups) + + return { + sort = state.GameSorts[sortName], + gameSortContents = state.GameSortsContents[sortName], + formFactor = state.FormFactor, + screenSize = state.ScreenSize, + } + end +)(HomeFTUEGameGrid) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeFTUEGameGrid.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeFTUEGameGrid.spec.lua new file mode 100644 index 0000000..683a61d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeFTUEGameGrid.spec.lua @@ -0,0 +1,49 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + local Rodux = require(Modules.Common.Rodux) + + local AppReducer = require(Modules.LuaApp.AppReducer) + local Constants = require(Modules.LuaApp.Constants) + + local HomeFTUEGameGrid = require(Modules.LuaApp.Components.Home.HomeFTUEGameGrid) + local FormFactor = require(Modules.LuaApp.Enum.FormFactor) + local GameSortGroup = require(Modules.LuaApp.Models.GameSortGroup) + local GameSortEntry = require(Modules.LuaApp.Models.GameSortEntry) + local GameSortContents = require(Modules.LuaApp.Models.GameSortContents) + local GameSort = require(Modules.LuaApp.Models.GameSort) + local Game = require(Modules.LuaApp.Models.Game) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local homeSortGroup = GameSortGroup.mock() + local gameSort = GameSort.mock() + local entry = GameSortEntry.mock() + local gameSortContents = GameSortContents.mock() + local game = Game.mock() + + gameSortContents.entries = { entry } + homeSortGroup.sorts = { gameSort.name } + + local store = Rodux.Store.new(AppReducer, { + GameSorts = { [gameSort.name] = gameSort }, + GameSortsContents = { [gameSort.name] = gameSortContents }, + Games = { [entry.universeId] = game }, + FormFactor = FormFactor.PHONE, + GameSortGroups = { [Constants.GameSortGroups.HomeGames] = homeSortGroup }, + }) + + local element = mockServices({ + gameGrid = Roact.createElement(HomeFTUEGameGrid, { + LayoutOrder = 7, + }), + }, { + includeStoreProvider = true, + store = store, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeHeaderUserAvatar.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeHeaderUserAvatar.lua new file mode 100644 index 0000000..c2c3381 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeHeaderUserAvatar.lua @@ -0,0 +1,37 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Common = Modules.Common +local LuaApp = Modules.LuaApp + +local Constants = require(LuaApp.Constants) +local Roact = require(Common.Roact) + +local PROFILE_PICTURE_SIZE = 150 +local THUMBNAIL_IMAGE_SIZE_ENUM = Constants.AvatarThumbnailSizes.Size150x150 +local DEFAULT_THUMBNAIL_ICON = "rbxasset://textures/ui/LuaApp/graphic/ph-avatar-portrait.png" +local OVERLAY_IMAGE_BIG = "rbxasset://textures/ui/LuaApp/graphic/gr-profile-150x150px.png" + +local HomeUserAvatarHeader = Roact.PureComponent:extend("HomeUserAvatarHeader") + +function HomeUserAvatarHeader:render() + local localUserModel = self.props.localUserModel + local thumbnailType = self.props.thumbnailType + local onActivated = self.props.onActivated + + return Roact.createElement("ImageLabel", { + Size = UDim2.new(0, PROFILE_PICTURE_SIZE, 0, PROFILE_PICTURE_SIZE), + BackgroundColor3 = Constants.Color.WHITE, + BorderSizePixel = 0, + Image = localUserModel and localUserModel.thumbnails and localUserModel.thumbnails[thumbnailType] + and localUserModel.thumbnails[thumbnailType][THUMBNAIL_IMAGE_SIZE_ENUM] or DEFAULT_THUMBNAIL_ICON, + }, { + MaskFrame = Roact.createElement("ImageButton", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Image = OVERLAY_IMAGE_BIG, + [Roact.Event.Activated] = onActivated, + }), + }) +end + +return HomeUserAvatarHeader \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeHeaderUserAvatar.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeHeaderUserAvatar.spec.lua new file mode 100644 index 0000000..5dc4936 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeHeaderUserAvatar.spec.lua @@ -0,0 +1,27 @@ +return function() + + local HomeHeaderUserAvatar = require(script.Parent.HomeHeaderUserAvatar) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + local UserModel = require(Modules.LuaApp.Models.User) + local Constants = require(Modules.LuaApp.Constants) + + local function MockHomeHeaderUserAvatar() + local localUserModel = UserModel.mock() + + return Roact.createElement(HomeHeaderUserAvatar, { + localUserModel = localUserModel, + thumbnailType = Constants.AvatarThumbnailTypes.HeadShot + }) + end + + it("should create and destroy without errors", function() + local element = MockHomeHeaderUserAvatar() + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeHeaderUserInfo.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeHeaderUserInfo.lua new file mode 100644 index 0000000..0804e6d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeHeaderUserInfo.lua @@ -0,0 +1,188 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Common = Modules.Common +local LuaApp = Modules.LuaApp + +local Constants = require(LuaApp.Constants) +local Roact = require(Common.Roact) +local Text = require(Common.Text) +local Url = require(Modules.LuaApp.Http.Url) +local NotificationType = require(Modules.LuaApp.Enum.NotificationType) +local FormFactor = require(Modules.LuaApp.Enum.FormFactor) + +local AppGuiService = require(Modules.LuaApp.Services.AppGuiService) +local RoactServices = require(Modules.LuaApp.RoactServices) + +local HomeHeaderUserAvatar = require(LuaApp.Components.Home.HomeHeaderUserAvatar) +local FitChildren = require(LuaApp.FitChildren) + +local BUILDERCLUB_LOGO_WIDTH = 48 +local BUILDERCLUB_LOGO_HEIGHT = 24 +local BUILDERCLUB_LOGOS = { + [Enum.MembershipType.BuildersClub] = "rbxasset://textures/ui/LuaApp/icons/ic-bc.png", + [Enum.MembershipType.TurboBuildersClub] = "rbxasset://textures/ui/LuaApp/icons/ic-tbc.png", + [Enum.MembershipType.OutrageousBuildersClub] = "rbxasset://textures/ui/LuaApp/icons/ic-obc.png", +} + +local BUILDERCLUB_LOGO_WIDTH_PHONE = 24 +local BUILDERCLUB_LOGO_HEIGHT_PHONE = 24 +local BUILDERCLUB_LOGOS_PHONE = { + [Enum.MembershipType.BuildersClub] = "rbxasset://textures/ui/LuaApp/icons/ic-bc-small.png", + [Enum.MembershipType.TurboBuildersClub] = "rbxasset://textures/ui/LuaApp/icons/ic-tbc-small.png", + [Enum.MembershipType.OutrageousBuildersClub] = "rbxasset://textures/ui/LuaApp/icons/ic-obc-small.png", +} + +local USERNAME_BC_PADDING = 12 +local USERNAME_TEXT_SIZE = 38 +local TITLE_SECTION_HEIGHT = math.max(USERNAME_TEXT_SIZE, BUILDERCLUB_LOGO_HEIGHT) + +local USERNAME_BC_VERTICAL_PADDING = 20 +local PROFILE_PICTURE_TO_BC_PADDING = 24 +local PROFILE_PICTURE_THUMBNAIL_TYPE = Constants.AvatarThumbnailTypes.HeadShot + + +local function createNormalHeaderInfo(props) + local localUserModel = props.localUserModel + local sidePadding = props.sidePadding + local sectionPadding = props.sectionPadding + local onUsernameActivated = props.onUsernameActivated + local username = localUserModel.name + local membership = localUserModel.membership + + local isLocalPlayerBC = membership ~= Enum.MembershipType.None + + -- clickable area is equal to the text bounds + local textBounds = Text.GetTextBounds(username, Enum.Font.SourceSans, USERNAME_TEXT_SIZE, Vector2.new(10000, 10000)) + + return Roact.createElement(FitChildren.FitFrame, { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 0), + fitFields = { Size = FitChildren.FitAxis.Height, }, + }, { + HorizontalLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + Padding = UDim.new(0, PROFILE_PICTURE_TO_BC_PADDING), + }), + Padding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, sectionPadding), + PaddingBottom = UDim.new(0, sectionPadding), + PaddingLeft = UDim.new(0, sidePadding), + }), + UserAvatar = Roact.createElement(HomeHeaderUserAvatar, { + localUserModel = localUserModel, + thumbnailType = PROFILE_PICTURE_THUMBNAIL_TYPE, + onActivated = onUsernameActivated, + LayoutOrder = 1, + }), + BuildersClubUsernameFrame = Roact.createElement("Frame", { + Size = UDim2.new( + 1, -BUILDERCLUB_LOGO_WIDTH - USERNAME_BC_PADDING, + 0, USERNAME_TEXT_SIZE + BUILDERCLUB_LOGO_HEIGHT + ), + BackgroundTransparency = 1, + LayoutOrder = 2, + }, { + VerticalLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + VerticalAlignment = Enum.VerticalAlignment.Center, + Padding = UDim.new(0, USERNAME_BC_VERTICAL_PADDING), + }), + Username = Roact.createElement("TextButton", { + Size = UDim2.new(0, textBounds.X, 0, textBounds.Y), + BackgroundTransparency = 1, + TextSize = USERNAME_TEXT_SIZE, + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSans, + Text = username, + TextXAlignment = Enum.TextXAlignment.Left, + LayoutOrder = 1, + [Roact.Event.Activated] = onUsernameActivated, + }), + BuildersClub = isLocalPlayerBC and Roact.createElement("ImageLabel", { + Size = UDim2.new(0, BUILDERCLUB_LOGO_WIDTH, 0, BUILDERCLUB_LOGO_HEIGHT), + Image = BUILDERCLUB_LOGOS[membership], + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = 2, + }), + }) + }) +end + +-- Mobile phone is a special case. +local function createPhoneHeaderInfo(props) + local username = props.localUserModel.name + local membership = props.localUserModel.membership + local sidePadding = props.sidePadding + local sectionPadding = props.sectionPadding + local onUsernameActivated = props.onUsernameActivated + + local isLocalPlayerBC = membership ~= Enum.MembershipType.None + + -- clickable area is equal to the text bounds + local textBounds = Text.GetTextBounds(username, Enum.Font.SourceSans, USERNAME_TEXT_SIZE, Vector2.new(10000, 10000)) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, TITLE_SECTION_HEIGHT + sectionPadding * 2), + BackgroundTransparency = 1, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + Padding = UDim.new(0, isLocalPlayerBC and USERNAME_BC_PADDING or 0), + }), + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, sidePadding), + }), + BuildersClub = isLocalPlayerBC and Roact.createElement("ImageLabel", { + Size = UDim2.new(0, BUILDERCLUB_LOGO_WIDTH_PHONE, 0, BUILDERCLUB_LOGO_HEIGHT_PHONE), + Image = BUILDERCLUB_LOGOS_PHONE[membership], + BackgroundTransparency = 1, + LayoutOrder = 1, + }), + Username = Roact.createElement("TextButton", { + Size = UDim2.new(0, textBounds.X, 0, textBounds.Y), + BackgroundTransparency = 1, + TextSize = USERNAME_TEXT_SIZE, + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSans, + Text = username, + TextXAlignment = Enum.TextXAlignment.Left, + LayoutOrder = 2, + [Roact.Event.Activated] = onUsernameActivated, + }), + }) +end + +local HomeHeaderUserInfo = Roact.PureComponent:extend("HomeHeaderUserInfo") + +function HomeHeaderUserInfo:init() + self.onUsernameActivated = function() + local localUserId = self.props.localUserModel.id + local url = Url:getUserProfileUrl(localUserId) + self.props.guiService:BroadcastNotification(url, NotificationType.VIEW_PROFILE) + end +end + +function HomeHeaderUserInfo:render() + local localUserModel = self.props.localUserModel + local formFactor = self.props.formFactor + local sidePadding = self.props.sidePadding + local sectionPadding = self.props.sectionPadding + + local isPhone = formFactor == FormFactor.PHONE + return (isPhone and createPhoneHeaderInfo or createNormalHeaderInfo)({ + localUserModel = localUserModel, + sidePadding = sidePadding, + sectionPadding = sectionPadding, + onUsernameActivated = self.onUsernameActivated, + }) +end + +return RoactServices.connect({ + guiService = AppGuiService +})(HomeHeaderUserInfo) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeHeaderUserInfo.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeHeaderUserInfo.spec.lua new file mode 100644 index 0000000..8902323 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomeHeaderUserInfo.spec.lua @@ -0,0 +1,85 @@ +return function() + local HomeHeaderUserInfo = require(script.Parent.HomeHeaderUserInfo) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + local UserModel = require(Modules.LuaApp.Models.User) + local FormFactor = require(Modules.LuaApp.Enum.FormFactor) + + local function MockHomeHeaderUserInfo(formFactor, membershipType) + local localUserModel = UserModel.mock() + localUserModel.membership = membershipType + + return Roact.createElement(HomeHeaderUserInfo, { + localUserModel = localUserModel, + formFactor = formFactor, + sidePadding = 12, + sectionPadding = 12, + }) + end + + it("should create and destroy without errors", function() + local element = MockHomeHeaderUserInfo(FormFactor.PHONE, Enum.MembershipType.None) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + describe("it should adapt to different formfactors", function() + it("should render PHONE formfactor properly", function() + local element = MockHomeHeaderUserInfo(FormFactor.PHONE, Enum.MembershipType.None) + local container = Instance.new("Folder") + Roact.mount(element, container, "Test") + expect(container.Test:FindFirstChild("UserAvatar", true)).to.never.be.ok() + end) + + it("should render TABLET formfactor properly", function() + local element = MockHomeHeaderUserInfo(FormFactor.TABLET, Enum.MembershipType.None) + local container = Instance.new("Folder") + Roact.mount(element, container, "Test") + expect(container.Test:FindFirstChild("UserAvatar", true)).to.be.ok() + end) + + it("should render with an UNKNOWN formfactor without issues", function() + local element = MockHomeHeaderUserInfo(FormFactor.UNKNOWN, Enum.MembershipType.None) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) + + describe("it should properly display user membership information", function() + describe("should hide membership info if user is not a paid member", function() + it("PHONE", function() + local element = MockHomeHeaderUserInfo(FormFactor.PHONE, Enum.MembershipType.None) + local container = Instance.new("Folder") + Roact.mount(element, container, "Test") + expect(container.Test:FindFirstChild("BuildersClub", true)).to.never.be.ok() + end) + + it("TABLET", function() + local element = MockHomeHeaderUserInfo(FormFactor.TABLET, Enum.MembershipType.None) + local container = Instance.new("Folder") + Roact.mount(element, container, "Test") + expect(container.Test:FindFirstChild("BuildersClub", true)).to.never.be.ok() + end) + end) + + describe("should display membership info if user is a paid member", function() + it("PHONE", function() + local element = MockHomeHeaderUserInfo(FormFactor.PHONE, Enum.MembershipType.BuildersClub) + local container = Instance.new("Folder") + Roact.mount(element, container, "Test") + expect(container.Test:FindFirstChild("BuildersClub", true)).to.be.ok() + end) + + it("TABLET", function() + local element = MockHomeHeaderUserInfo(FormFactor.TABLET, Enum.MembershipType.BuildersClub) + local container = Instance.new("Folder") + Roact.mount(element, container, "Test") + expect(container.Test:FindFirstChild("BuildersClub", true)).to.be.ok() + end) + end) + end) + +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomePage.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomePage.lua new file mode 100644 index 0000000..1229d5d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomePage.lua @@ -0,0 +1,305 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Common = Modules.Common +local LuaApp = Modules.LuaApp + +local Roact = require(Common.Roact) +local RoactRodux = require(Common.RoactRodux) +local RoactAnalyticsHomePage = require(Modules.LuaApp.Services.RoactAnalyticsHomePage) +local RoactNetworking = require(Modules.LuaApp.Services.RoactNetworking) +local AppGuiService = require(Modules.LuaApp.Services.AppGuiService) +local RoactServices = require(Modules.LuaApp.RoactServices) + +local Promise = require(Modules.LuaApp.Promise) +local RefreshScrollingFrame = require(Modules.LuaApp.Components.RefreshScrollingFrame) +local UserCarouselEntry = require(LuaApp.Components.Home.UserCarouselEntry) +local HomeHeaderUserInfo = require(LuaApp.Components.Home.HomeHeaderUserInfo) +local MyFeedButton = require(LuaApp.Components.Home.MyFeedButton) +local DropshadowFrame = require(LuaApp.Components.DropshadowFrame) +local Carousel = require(LuaApp.Components.Carousel) +local TopBar = require(LuaApp.Components.TopBar) +local GameCarousels = require(LuaApp.Components.GameCarousels) +local LoadingBar = require(LuaApp.Components.LoadingBar) +local HomeFTUEGameGrid = require(LuaApp.Components.Home.HomeFTUEGameGrid) +local LocalizedSectionHeaderWithSeeAll = require(Modules.LuaApp.Components.LocalizedSectionHeaderWithSeeAll) +local User = require(LuaApp.Models.User) +local Constants = require(LuaApp.Constants) +local FitChildren = require(LuaApp.FitChildren) +local Functional = require(Common.Functional) +local Immutable = require(Common.Immutable) +local memoize = require(Common.memoize) +local TokenRefreshComponent = require(Modules.LuaApp.Components.TokenRefreshComponent) +local NotificationType = require(LuaApp.Enum.NotificationType) + +local Url = require(Modules.LuaApp.Http.Url) +local ApiFetchGamesData = require(Modules.LuaApp.Thunks.ApiFetchGamesData) +local ApiFetchUsersFriends = require(Modules.LuaApp.Thunks.ApiFetchUsersFriends) +local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) + +local MAX_FRIENDS_IN_CAROUSEL = tonumber(settings():GetFVariable("LuaHomeMaxFriends")) or 0 + +local SIDE_PADDING = 15 +local SECTION_PADDING = 15 +local CAROUSEL_PADDING = Constants.GAME_CAROUSEL_PADDING +local CAROUSEL_PADDING_DIM = UDim.new(0, CAROUSEL_PADDING) + +local FRIEND_SECTION_MARGIN = 15 - UserCarouselEntry.horizontalPadding() + +local FEED_SECTION_PADDING = 60 +local FEED_SECTION_PADDING_TOP = FEED_SECTION_PADDING - CAROUSEL_PADDING +local FEED_SECTION_PADDING_BOTTOM = FEED_SECTION_PADDING +local FEED_BUTTON_HEIGHT = 32 +local FEED_SECTION_HEIGHT = FEED_SECTION_PADDING_TOP + FEED_BUTTON_HEIGHT + FEED_SECTION_PADDING_BOTTOM + +local PRESENCE_WEIGHTS = { + [User.PresenceType.IN_GAME] = 3, + [User.PresenceType.ONLINE] = 2, + [User.PresenceType.IN_STUDIO] = 1, + [User.PresenceType.OFFLINE] = 0, +} + +local function Spacer(props) + local height = props.height + local layoutOrder = props.LayoutOrder + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, height), + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }) +end + + +local HomePage = Roact.PureComponent:extend("HomePage") + +function HomePage:init() + self.refresh = function() + return self.props.refresh(self.props.networking, self.props.localUserModel) + end + + self.onSeeAllFriends = function() + local url = string.format("%susers/friends", Url.BASE_URL) + self.props.guiService:BroadcastNotification(url, NotificationType.VIEW_PROFILE) + end +end + +function HomePage:render() + local fetchedHomePageData = self.props.homePageDataStatus == RetrievalStatus.Done + local topBarHeight = self.props.topBarHeight + local friends = self.props.friends + local localUserModel = self.props.localUserModel + local formFactor = self.props.formFactor + local friendCount = self.props.friendCount + local isFTUE = self.props.isFTUE + local analytics = self.props.analytics + + local hasFriends = #friends > 0 + local friendSectionHeight = UserCarouselEntry.height(formFactor) + + local function createUserEntry(user, count) + return Roact.createElement(UserCarouselEntry, { + user = user, + formFactor = formFactor, + count = count, + highlightColor = Constants.Color.WHITE, + thumbnailType = Constants.AvatarThumbnailTypes.AvatarThumbnail, + }) + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BorderSizePixel = 0, + }, { + TokenRefreshComponent = Roact.createElement(TokenRefreshComponent, { + sortToRefresh = Constants.GameSortGroups.HomeGames, + }), + TopBar = Roact.createElement(TopBar, { + showBuyRobux = true, + showNotifications = true, + showSearch = true, + ZIndex = 2, + }), + Loader = not fetchedHomePageData and Roact.createElement("Frame", { + BackgroundTransparency = 0, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, topBarHeight/2), + Size = UDim2.new(1, 0, 1, -topBarHeight), + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.GRAY4, + }, { + LoadingIndicator = Roact.createElement(LoadingBar), + }), + Scroller = fetchedHomePageData and Roact.createElement(RefreshScrollingFrame, { + Position = UDim2.new(0, 0, 0, topBarHeight), + Size = UDim2.new(1, 0, 1, -topBarHeight), + CanvasSize = UDim2.new(1, 0, 0, 0), + BackgroundColor3 = Constants.Color.GRAY4, + BorderSizePixel = 0, + ScrollBarThickness = 0, + refresh = self.refresh, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + }), + TitleSection = localUserModel and Roact.createElement(HomeHeaderUserInfo, { + sidePadding = SIDE_PADDING, + sectionPadding = SECTION_PADDING, + LayoutOrder = 2, + localUserModel = localUserModel, + formFactor = formFactor, + }), + FriendSection = hasFriends and Roact.createElement(FitChildren.FitFrame, { + Size = UDim2.new(1, 0, 0, 0), + fitAxis = FitChildren.FitAxis.Height, + BackgroundTransparency = 1, + LayoutOrder = 4, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Container = Roact.createElement(FitChildren.FitFrame, { + Size = UDim2.new(1, 0, 0, 0), + BackgroundTransparency = 1, + fitFields = { + Size = FitChildren.FitAxis.Height, + }, + }, { + SidePadding = Roact.createElement("UIPadding", { + PaddingLeft = CAROUSEL_PADDING_DIM, + PaddingRight = CAROUSEL_PADDING_DIM, + }), + Header = Roact.createElement(LocalizedSectionHeaderWithSeeAll, { + text = { + "Feature.Home.HeadingFriends", + friendCount = friendCount, + }, + LayoutOrder = 1, + onSelected = self.onSeeAllFriends + }), + }), + CarouselFrame = Roact.createElement(DropshadowFrame, { + Size = UDim2.new(1, 0, 0, friendSectionHeight), + BackgroundColor3 = Constants.Color.WHITE, + LayoutOrder = 2, + }, { + Carousel = Roact.createElement(Carousel, { + childPadding = 0, + }, Immutable.JoinDictionaries(Functional.Map(friends, createUserEntry), { + leftAlignSpacer = Roact.createElement("UIPadding", { + PaddingRight = UDim.new(0, FRIEND_SECTION_MARGIN), + PaddingLeft = UDim.new(0, FRIEND_SECTION_MARGIN), + }) + })) + }), + }), + GameDisplay = isFTUE and Roact.createElement(HomeFTUEGameGrid, { + LayoutOrder = 5, + analytics = analytics, + hasTopPadding = hasFriends, + }) or Roact.createElement(GameCarousels, { + gameSortGroup = Constants.GameSortGroups.HomeGames, + LayoutOrder = 5, + analytics = analytics, + }), + FeedSection = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, FEED_SECTION_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = 6, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + }), + MyFeedPadding1 = Roact.createElement(Spacer, { + height = FEED_SECTION_PADDING_TOP, + LayoutOrder = 1, + }), + MyFeedButton = Roact.createElement(MyFeedButton, { + Size = UDim2.new(1, 0, 0, FEED_BUTTON_HEIGHT), + LayoutOrder = 2, + }), + MyFeedPadding2 = Roact.createElement(Spacer, { + height = FEED_SECTION_PADDING_BOTTOM, + LayoutOrder = 3, + }), + }), + }), + }) +end + +local selectFriends = memoize(function(users) + local allFriends = {} + local function friendPreference(friend1, friend2) + local friend1Weight = PRESENCE_WEIGHTS[friend1.presence] + local friend2Weight = PRESENCE_WEIGHTS[friend2.presence] + + if friend1Weight == friend2Weight then + return friend1.name < friend2.name + else + return friend1Weight > friend2Weight + end + end + + for _, user in pairs(users) do + if user.isFriend then + allFriends[#allFriends + 1] = user + end + end + + table.sort(allFriends, friendPreference) + + local filteredFriends = {} + for index, user in ipairs(allFriends) do + filteredFriends[index] = user + if index >= MAX_FRIENDS_IN_CAROUSEL then + break + end + end + + return filteredFriends +end) + +local selectLocalUser = memoize(function(users, id) + return users[id] +end) + +local selectIsFTUE = function(sortGroups) + local homeSortGroup = Constants.GameSortGroups.HomeGames + local sorts = sortGroups[homeSortGroup].sorts + + return #sorts == 1 +end + +HomePage = RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + friends = selectFriends( + state.Users + ), + localUserModel = selectLocalUser(state.Users, state.LocalUserId), + isFTUE = selectIsFTUE(state.GameSortGroups), + formFactor = state.FormFactor, + friendCount = state.FriendCount, + topBarHeight = state.TopBar.topBarHeight, + homePageDataStatus = state.Startup.HomePageDataStatus, + } + end, + function(dispatch) + return { + refresh = function(networking, localUserModel) + local fetchPromises = {} + table.insert(fetchPromises, dispatch(ApiFetchUsersFriends( + networking, + localUserModel.id, + Constants.AvatarThumbnailRequests.USER_CAROUSEL + ))) + table.insert(fetchPromises, dispatch(ApiFetchGamesData(networking, Constants.GameSortGroups.HomeGames))) + return Promise.all(fetchPromises) + end, + } + end +)(HomePage) + +return RoactServices.connect({ + networking = RoactNetworking, + analytics = RoactAnalyticsHomePage, + guiService = AppGuiService +})(HomePage) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomePage.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomePage.spec.lua new file mode 100644 index 0000000..5f10701 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/HomePage.spec.lua @@ -0,0 +1,92 @@ +return function() + local HomePage = require(script.Parent.HomePage) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local AddUser = require(Modules.LuaApp.Actions.AddUser) + local AppReducer = require(Modules.LuaApp.AppReducer) + local Roact = require(Modules.Common.Roact) + local Rodux = require(Modules.Common.Rodux) + local SetLocalUserId = require(Modules.LuaApp.Actions.SetLocalUserId) + local SetUserMembershipType = require(Modules.LuaApp.Actions.SetUserMembershipType) + local SetHomePageDataStatus = require(Modules.LuaApp.Actions.SetHomePageDataStatus) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + local User = require(Modules.LuaApp.Models.User) + local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) + + local function MockStore(eachUserIsFriend, membership) + local store = Rodux.Store.new(AppReducer) + if eachUserIsFriend then + for i, isFriend in ipairs(eachUserIsFriend) do + store:dispatch(AddUser(User.fromData(i, "User " .. i, isFriend))) + end + end + local localUser = User.mock() + store:dispatch(AddUser(localUser)) + store:dispatch(SetLocalUserId(localUser.id)) + store:dispatch(SetHomePageDataStatus(RetrievalStatus.Done)) + if membership then + store:dispatch(SetUserMembershipType(localUser.id, membership)) + else + store:dispatch(SetUserMembershipType(localUser.id, Enum.MembershipType.None)) + end + return store + end + + local function MockHomepage(store) + return mockServices({ + HomePage = Roact.createElement(HomePage), + }, { + includeStoreProvider = true, + store = store, + }) + end + + it("should create and destroy without errors", function() + local store = MockStore() + local element = MockHomepage(store) + local instance = Roact.mount(element) + Roact.unmount(instance) + store:destruct() + end) + + it("should show the friends section if there are friends", function() + local store = MockStore({false, true, true}) + local element = MockHomepage(store) + local container = Instance.new("Folder") + Roact.mount(element, container, "Test") + expect(container.Test:FindFirstChild("FriendSection", true)).to.be.ok() + store:destruct() + end) + + it("should hide the friends section if there are no friends", function() + local store = MockStore({false, false, false}) + local element = MockHomepage(store) + local container = Instance.new("Folder") + Roact.mount(element, container, "Test") + + expect(container.Test:FindFirstChild("FriendSection", true)).to.never.be.ok() + store:destruct() + end) + + it("should show the builders club icon if the local user is builders club", function() + local store = MockStore(nil, Enum.MembershipType.BuildersClub) + local element = MockHomepage(store) + local container = Instance.new("Folder") + Roact.mount(element, container, "Test") + + expect(container.Test:FindFirstChild("BuildersClub", true)).to.be.ok() + + store:destruct() + end) + + it("should hide the builders club icon if the local user is not builders club", function() + local store = MockStore() + local element = MockHomepage(store) + local container = Instance.new("Folder") + Roact.mount(element, container, "Test") + + expect(container.Test:FindFirstChild("BuildersClub", true)).to.never.be.ok() + store:destruct() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/MyFeedButton.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/MyFeedButton.lua new file mode 100644 index 0000000..f81b96a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/MyFeedButton.lua @@ -0,0 +1,88 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local AppGuiService = require(Modules.LuaApp.Services.AppGuiService) +local RoactServices = require(Modules.LuaApp.RoactServices) + +local Common = Modules.Common +local LuaApp = Modules.LuaApp + +local Roact = require(Common.Roact) + +local NotificationType = require(LuaApp.Enum.NotificationType) + +local LocalizedTextLabel = require(LuaApp.Components.LocalizedTextLabel) + +local FEED_BUTTON_ASSET_DEFAULT = "rbxasset://textures/ui/LuaApp/9-slice/gr-btn-white-3px.png" +local FEED_BUTTON_ASSET_PRESSED = "rbxasset://textures/ui/LuaApp/9-slice/gr-btn-white-3px-pressed.png" + +local MyFeedButton = Roact.PureComponent:extend("MyFeedButton") + +function MyFeedButton:init() + self.state = { + isMyFeedButtonPressed = false, + } + + self.onViewFeed = function() + self.props.guiService:BroadcastNotification("", NotificationType.VIEW_MY_FEED) + end + + self.onInputBegan = function() + self:setState({ + isMyFeedButtonPressed = true + }) + end + + self.onInputEnded = function() + self:setState({ + isMyFeedButtonPressed = false + }) + end +end + +function MyFeedButton:render() + local Size = self.props.Size + local LayoutOrder = self.props.LayoutOrder + local isMyFeedButtonPressed = self.state.isMyFeedButtonPressed + + local myFeedButtonAsset + if isMyFeedButtonPressed then + myFeedButtonAsset = FEED_BUTTON_ASSET_PRESSED + else + myFeedButtonAsset = FEED_BUTTON_ASSET_DEFAULT + end + + return Roact.createElement("Frame", { + Size = Size, + BackgroundTransparency = 1, + LayoutOrder = LayoutOrder, + }, { + Button = Roact.createElement("ImageButton", { + Size = UDim2.new(0.5, 0, 1, 0), + Position = UDim2.new(0.25, 0, 0, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(3,3,4,4), + Image = myFeedButtonAsset, + AutoButtonColor = true, + [Roact.Event.Activated] = self.onViewFeed, + [Roact.Event.InputBegan] = self.onInputBegan, + [Roact.Event.InputEnded] = self.onInputEnded, + }, { + Text = Roact.createElement(LocalizedTextLabel, { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = Enum.Font.SourceSans, + Text = "Feature.Home.Action.ViewMyFeed", + TextSize = 20, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + }), + }), + }) +end + +return RoactServices.connect({ + guiService = AppGuiService +})(MyFeedButton) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/UserCarouselEntry.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/UserCarouselEntry.lua new file mode 100644 index 0000000..f2171fd --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/UserCarouselEntry.lua @@ -0,0 +1,126 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local AppGuiService = require(Modules.LuaApp.Services.AppGuiService) +local RoactServices = require(Modules.LuaApp.RoactServices) + + +local UserThumbnailPortraitOrientation = require(Modules.LuaApp.Components.Home.UserThumbnailPortraitOrientation) +local UserThumbnailDefaultOrientation = require(Modules.LuaApp.Components.Home.UserThumbnailDefaultOrientation) +local Constants = require(Modules.LuaApp.Constants) +local Url = require(Modules.LuaApp.Http.Url) +local NotificationType = require(Modules.LuaApp.Enum.NotificationType) +local FormFactor = require(Modules.LuaApp.Enum.FormFactor) + +local USER_ENTRY_WIDTH = 105 +local USER_ENTRY_WIDTH_PHONE = 115 +local VERTICAL_PADDING = 15 +local HORIZONTAL_PADDING = 7.5 + +local UserCarouselEntry = Roact.PureComponent:extend("UserCarouselEntry") + +function UserCarouselEntry:init() + self.state = { + highlighted = false + } + + self.onInputBegan = function(_, inputObject) + --TODO: Remove after CLIPLAYEREX-1468 + local inputStateChangedConnection = nil + inputStateChangedConnection = inputObject:GetPropertyChangedSignal("UserInputState"):Connect(function() + if inputObject.UserInputState == Enum.UserInputState.End + or inputObject.UserInputState == Enum.UserInputState.Cancel then + inputStateChangedConnection:Disconnect() + self.onInputEnded() + end + end) + self:setState({ + highlighted = true, + }) + end + + self.onInputEnded = function() + self:setState({ + highlighted = false, + }) + end + + self.onInputChanged = self.onInputEnded + + self.onActivated = function(_, inputObject) + if inputObject.UserInputState == Enum.UserInputState.End then + local user = self.props.user + if user then + local url = Url:getUserProfileUrl(user.id) + self.props.guiService:BroadcastNotification(url, NotificationType.VIEW_PROFILE) + end + end + end +end + +function UserCarouselEntry:render() + local count = self.props.count + local user = self.props.user + local formFactor = self.props.formFactor + local highlightColor = self.state.highlighted and Constants.Color.GRAY5 or Constants.Color.WHITE + local thumbnailType = self.props.thumbnailType + + local totalHeight = UserCarouselEntry.height(formFactor) + local thumbnailSize = UserCarouselEntry.thumbnailSize(formFactor) + + local isPhone = formFactor == FormFactor.PHONE + local userThumbnailComponent = isPhone and UserThumbnailPortraitOrientation + or UserThumbnailDefaultOrientation + + return Roact.createElement("ImageButton", { + AutoButtonColor = false, + Size = UDim2.new(0, isPhone and USER_ENTRY_WIDTH_PHONE or USER_ENTRY_WIDTH, 0, totalHeight), + BackgroundColor3 = highlightColor, + BorderSizePixel = 0, + LayoutOrder = count, + [Roact.Event.InputBegan] = self.onInputBegan, + [Roact.Event.InputEnded] = self.onInputEnded, + -- When Touch is used for scrolling, InputEnded gets sunk into scrolling action + [Roact.Event.InputChanged] = self.onInputChanged, + [Roact.Event.Activated] = self.onActivated, + }, { + ThumbnailFrame = Roact.createElement("Frame", { + Size = UDim2.new(0, thumbnailSize, 0, thumbnailSize), + Position = UDim2.new(0.5, 0, 0, VERTICAL_PADDING), + AnchorPoint = Vector2.new(0.5, 0), + BackgroundTransparency = 1, + }, { + Thumbnail = Roact.createElement(userThumbnailComponent, { + user = user, + formFactor = formFactor, + maskColor = Constants.Color.WHITE, + highlightColor = highlightColor, + thumbnailType = thumbnailType, + }), + }), + }) +end + +UserCarouselEntry = RoactServices.connect({ + guiService = AppGuiService +})(UserCarouselEntry) + +function UserCarouselEntry.thumbnailSize(formFactor) + return formFactor == FormFactor.PHONE and UserThumbnailPortraitOrientation.size(formFactor) + or UserThumbnailDefaultOrientation.size(formFactor) +end + +function UserCarouselEntry.height(formFactor) + local component = formFactor == FormFactor.PHONE and UserThumbnailPortraitOrientation + or UserThumbnailDefaultOrientation + + return VERTICAL_PADDING + + component.height(formFactor) + + VERTICAL_PADDING +end + +function UserCarouselEntry.horizontalPadding() + return HORIZONTAL_PADDING +end + +return UserCarouselEntry \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/UserThumbnailDefaultOrientation.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/UserThumbnailDefaultOrientation.lua new file mode 100644 index 0000000..1d9935a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/UserThumbnailDefaultOrientation.lua @@ -0,0 +1,67 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local FlagSettings = require(Modules.LuaApp.FlagSettings) +local Roact = require(Modules.Common.Roact) +local User = require(Modules.LuaApp.Models.User) +local UserThumbnail = require(Modules.LuaApp.Components.UserThumbnail) + +local isPeopleListV1Enabled = FlagSettings.IsPeopleListV1Enabled() + +local MEASUREMENT_CONSTANTS = { + THUMBNAIL_SIZE = 90, + DROPSHADOW_SIZE = 94, +} + +MEASUREMENT_CONSTANTS.USERNAME = { + TEXT_LINE_HEIGHT = isPeopleListV1Enabled and 25 or 20, + TEXT_FONT_SIZE = 18, + TEXT_TOP_PADDING = 3, +} + +MEASUREMENT_CONSTANTS.PRESENCE = { + TEXT_TOP_PADDING = 3, + TEXT_LINE_HEIGHT = 20, + TEXT_FONT_SIZE = 15, + + ICONS = { + [User.PresenceType.ONLINE] = "rbxasset://textures/ui/LuaApp/icons/ic-blue-online.png", + [User.PresenceType.IN_GAME] = "rbxasset://textures/ui/LuaApp/icons/ic-green-ingame.png", + [User.PresenceType.IN_STUDIO] = "rbxasset://textures/ui/LuaApp/icons/ic-orange-instudio.png", + }, + + DROPSHADOW_MARGIN = (MEASUREMENT_CONSTANTS.DROPSHADOW_SIZE - MEASUREMENT_CONSTANTS.THUMBNAIL_SIZE) / 2, + BORDER_DIAMETER = 14, + ICON_OFFSET = 5, + ICON_SIZE = 24, +} + +MEASUREMENT_CONSTANTS.PRESENCE_TEXT_HEIGHT = isPeopleListV1Enabled and MEASUREMENT_CONSTANTS.PRESENCE.TEXT_TOP_PADDING + + MEASUREMENT_CONSTANTS.PRESENCE.TEXT_LINE_HEIGHT or 0 + +local UserThumbnailDefaultOrientation = Roact.PureComponent:extend("UserThumbnailDefaultOrientation") + +function UserThumbnailDefaultOrientation.size() + return MEASUREMENT_CONSTANTS.THUMBNAIL_SIZE +end + +function UserThumbnailDefaultOrientation.height() + return MEASUREMENT_CONSTANTS.THUMBNAIL_SIZE + + MEASUREMENT_CONSTANTS.USERNAME.TEXT_TOP_PADDING + + MEASUREMENT_CONSTANTS.USERNAME.TEXT_LINE_HEIGHT + + MEASUREMENT_CONSTANTS.PRESENCE_TEXT_HEIGHT +end + +function UserThumbnailDefaultOrientation:render() + local user = self.props.user + local highlightColor = self.props.highlightColor + local thumbnailType = self.props.thumbnailType + + return Roact.createElement(UserThumbnail, { + measurements = MEASUREMENT_CONSTANTS, + user = user, + highlightColor = highlightColor, + thumbnailType = thumbnailType, + }) +end + +return UserThumbnailDefaultOrientation \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/UserThumbnailDefaultOrientation.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/UserThumbnailDefaultOrientation.spec.lua new file mode 100644 index 0000000..df5be8e --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/UserThumbnailDefaultOrientation.spec.lua @@ -0,0 +1,14 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local User = require(Modules.LuaApp.Models.User) + local UserThumbnailDefaultOrientation = require(Modules.LuaApp.Components.Home.UserThumbnailDefaultOrientation) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UserThumbnailDefaultOrientation, { + user = User.mock() + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/UserThumbnailPortraitOrientation.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/UserThumbnailPortraitOrientation.lua new file mode 100644 index 0000000..cb9326a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/UserThumbnailPortraitOrientation.lua @@ -0,0 +1,67 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local FlagSettings = require(Modules.LuaApp.FlagSettings) +local Roact = require(Modules.Common.Roact) +local User = require(Modules.LuaApp.Models.User) +local UserThumbnail = require(Modules.LuaApp.Components.UserThumbnail) + +local isPeopleListV1Enabled = FlagSettings.IsPeopleListV1Enabled() + +local MEASUREMENT_CONSTANTS = { + THUMBNAIL_SIZE = 84, + DROPSHADOW_SIZE = 94, +} + +MEASUREMENT_CONSTANTS.USERNAME = { + TEXT_LINE_HEIGHT = isPeopleListV1Enabled and 25 or 20, + TEXT_FONT_SIZE = 18, + TEXT_TOP_PADDING = 3, +} + +MEASUREMENT_CONSTANTS.PRESENCE = { + TEXT_TOP_PADDING = 3, + TEXT_LINE_HEIGHT = 20, + TEXT_FONT_SIZE = 15, + + ICONS = { + [User.PresenceType.ONLINE] = "rbxasset://textures/ui/LuaApp/icons/ic-blue-dot.png", + [User.PresenceType.IN_GAME] = "rbxasset://textures/ui/LuaApp/icons/ic-green-dot.png", + [User.PresenceType.IN_STUDIO] = "rbxasset://textures/ui/LuaApp/icons/ic-orange-dot.png", + }, + + DROPSHADOW_MARGIN = (MEASUREMENT_CONSTANTS.DROPSHADOW_SIZE - MEASUREMENT_CONSTANTS.THUMBNAIL_SIZE) / 2, + BORDER_DIAMETER = 14, + ICON_OFFSET = 7, + ICON_SIZE = 10, +} + +MEASUREMENT_CONSTANTS.PRESENCE_TEXT_HEIGHT = isPeopleListV1Enabled and MEASUREMENT_CONSTANTS.PRESENCE.TEXT_TOP_PADDING + + MEASUREMENT_CONSTANTS.PRESENCE.TEXT_LINE_HEIGHT or 0 + +local UserThumbnailPortraitOrientation = Roact.PureComponent:extend("UserThumbnailPortraitOrientation") + +function UserThumbnailPortraitOrientation.size() + return MEASUREMENT_CONSTANTS.THUMBNAIL_SIZE +end + +function UserThumbnailPortraitOrientation.height() + return MEASUREMENT_CONSTANTS.THUMBNAIL_SIZE + + MEASUREMENT_CONSTANTS.USERNAME.TEXT_TOP_PADDING + + MEASUREMENT_CONSTANTS.USERNAME.TEXT_LINE_HEIGHT + + MEASUREMENT_CONSTANTS.PRESENCE_TEXT_HEIGHT +end + +function UserThumbnailPortraitOrientation:render() + local user = self.props.user + local highlightColor = self.props.highlightColor + local thumbnailType = self.props.thumbnailType + + return Roact.createElement(UserThumbnail, { + measurements = MEASUREMENT_CONSTANTS, + user = user, + highlightColor = highlightColor, + thumbnailType = thumbnailType, + }) +end + +return UserThumbnailPortraitOrientation \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/UserThumbnailPortraitOrientation.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/UserThumbnailPortraitOrientation.spec.lua new file mode 100644 index 0000000..868f056 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Home/UserThumbnailPortraitOrientation.spec.lua @@ -0,0 +1,14 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local User = require(Modules.LuaApp.Models.User) + local UserThumbnailPortraitOrientation = require(Modules.LuaApp.Components.Home.UserThumbnailPortraitOrientation) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UserThumbnailPortraitOrientation, { + user = User.mock() + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LargeGameVoteBar.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LargeGameVoteBar.lua new file mode 100644 index 0000000..d24148f --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LargeGameVoteBar.lua @@ -0,0 +1,64 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local GameVoteBar = require(Modules.LuaApp.Components.Games.GameVoteBar) +local Constants = require(Modules.LuaApp.Constants) + +local VOTE_BAR_SIZE = 6 +local VOTE_BAR_Y = 2 +local COUNT_SIZE = 20 + +local function LargeGameVoteBar(props) + local frameProps = props.frameProps + local iconSize = props.iconSize + local upVotes = props.upVotes + local downVotes = props.downVotes + + local percent + if upVotes + downVotes == 0 then + percent = upVotes + else + percent = upVotes/(upVotes + downVotes) + end + + return Roact.createElement("Frame", frameProps, { + VoteUp = Roact.createElement("ImageButton", { + Size = UDim2.new(0, iconSize, 0, iconSize), + Position = UDim2.new(0, 0, 0, 0), + BackgroundTransparency = 1, + Image = "rbxasset://textures/ui/LuaApp/icons/ic-thumbs-up.png", + }), + VoteUpCount = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Position = UDim2.new(0, iconSize, 1, -COUNT_SIZE), + Size = UDim2.new(0, 0, 0, COUNT_SIZE), + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + TextColor3 = Constants.Color.GREEN_PRIMARY, + Text = tostring(upVotes), + }), + VoteBar = Roact.createElement(GameVoteBar, { + Size = UDim2.new(1, -2*iconSize, 0, VOTE_BAR_SIZE), + Position = UDim2.new(0, iconSize, 0, VOTE_BAR_Y), + votePercentage = percent, + }), + VoteDown = Roact.createElement("ImageButton", { + Position = UDim2.new(1, -iconSize, 0, 0), + Size = UDim2.new(0, iconSize, 0, iconSize), + BackgroundTransparency = 1, + Image = "rbxasset://textures/ui/LuaApp/icons/ic-thumbs-down.png", + }), + VoteDownCount = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Position = UDim2.new(1, -iconSize, 1, -COUNT_SIZE), + Size = UDim2.new(0, 0, 0, COUNT_SIZE), + TextXAlignment = Enum.TextXAlignment.Right, + TextYAlignment = Enum.TextYAlignment.Center, + TextColor3 = Constants.Color.RED_NEGATIVE, + Text = tostring(downVotes), + }), + }) +end + +return LargeGameVoteBar \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LargeGameVoteBar.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LargeGameVoteBar.spec.lua new file mode 100644 index 0000000..ab58923 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LargeGameVoteBar.spec.lua @@ -0,0 +1,27 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local LargeGameVoteBar = require(Modules.LuaApp.Components.LargeGameVoteBar) + + it("should create and destroy without errors", function() + local element = Roact.createElement(LargeGameVoteBar, { + frameProps = {}, + iconSize = 28, + upVotes = 0, + downVotes = 0, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when the total number of votes is 0", function() + local element = Roact.createElement(LargeGameVoteBar, { + frameProps = {}, + iconSize = 28, + upVotes = 0, + downVotes = 0, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Line.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Line.lua new file mode 100644 index 0000000..8b523d9 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Line.lua @@ -0,0 +1,17 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local Constants = require(Modules.LuaApp.Constants) + +local Line = function (props) + return Roact.createElement("Frame", { + Position = props.Position, + Size = UDim2.new(1, 0, 0, 1), + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.GRAY4, + LayoutOrder = props.LayoutOrder, + }) +end + +return Line \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Line.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Line.spec.lua new file mode 100644 index 0000000..28ab983 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Line.spec.lua @@ -0,0 +1,11 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local Line = require(Modules.LuaApp.Components.Line) + + it("should create and destroy without errors", function() + local element = Roact.createElement(Line) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ListPicker.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ListPicker.lua new file mode 100644 index 0000000..62bacc2 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ListPicker.lua @@ -0,0 +1,102 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local Constants = require(Modules.LuaApp.Constants) + +local DEFAULT_ITEM_HEIGHT = 54 +local DEFAULT_TEXT_COLOR = Constants.Color.GRAY1 +local DEFAULT_TEXT_FONT = Enum.Font.SourceSans +local DEFAULT_TEXT_SIZE = 23 + +local DROPDOWN_TEXT_MARGIN = 10 +local ICON_HORIZONTAL_SPACE = 20 +local ICON_SIZE = 20 +local ICON_VERTICAL_SPACE = 17 + +local ListPicker = Roact.PureComponent:extend("ListPicker") + +ListPicker.defaultProps = { + textColor = DEFAULT_TEXT_COLOR, + font = DEFAULT_TEXT_FONT, + textSize = DEFAULT_TEXT_SIZE, + itemHeight = DEFAULT_ITEM_HEIGHT, + itemWidth = 0, +} + +-- Returns a list of items (with text and an icon) that the user can pick from. +-- Intended to be the core functionality of the DropDownList control. +function ListPicker:render() + local itemList = self.props.items + + local textColor = self.props.textColor + local textFont = self.props.font + local textSize = self.props.textSize + + local itemHeight = self.props.itemHeight + local itemWidth = self.props.itemWidth + + -- Build a table of items that the user is able to pick from: + local listContents = {} + listContents["Layout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + Name = "Layout", + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + -- Text offset to make space for the cancel icon (padding doesn't work for text yet): + local iconSpacing = ICON_SIZE + (ICON_HORIZONTAL_SPACE * 2) + + local itemSize + if itemWidth > 0 then + itemSize = UDim2.new(0, itemWidth, 0, itemHeight) + else + itemSize = UDim2.new(1, 0, 0, itemHeight) + end + + for position, item in ipairs(itemList) do + listContents[position] = Roact.createElement("ImageButton", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = position, + Size = itemSize, + [Roact.Event.Activated] = function() + self.props.onSelectItem(item, position) + end, + }, { + Image = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = false, + Image = item.displayIcon, + Position = UDim2.new(0, ICON_HORIZONTAL_SPACE, 0, ICON_VERTICAL_SPACE), + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }), + Text = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = textFont, + Position = UDim2.new(0, iconSpacing, 0, 0), + Size = UDim2.new(1, -(iconSpacing + DROPDOWN_TEXT_MARGIN), 1, 0), + Text = item.displayName, + TextColor3 = textColor, + TextSize = textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + }), + + }) + end + + -- Using a regular frame instead of a FitFrame because: right now the + -- ListPicker is used by the Popout menu which have animation-controlled + -- size and FitFrame doesn't work very well with that. + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + }, listContents) +end + +return ListPicker \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ListPicker.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ListPicker.spec.lua new file mode 100644 index 0000000..f2b0fb0 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ListPicker.spec.lua @@ -0,0 +1,27 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local ListPicker = require(Modules.LuaApp.Components.ListPicker) + + it("should create and destroy without errors", function() + local listIcons = { + "rbxasset://textures/ui/LuaApp/category/ic-featured.png", + "rbxasset://textures/ui/LuaApp/category/ic-popular.png", + "rbxasset://textures/ui/LuaApp/category/ic-top rated.png" + } + local listItems = { + "Featured", + "Popular", + "Top Rated" + } + + local element = Roact.createElement(ListPicker, { + onSelectItem = nil, + items = listItems, + icons = listIcons, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadableImage.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadableImage.lua new file mode 100644 index 0000000..67e2df5 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadableImage.lua @@ -0,0 +1,77 @@ +local ContentProvider = game:GetService("ContentProvider") + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Roact = require(Modules.Common.Roact) + +local decal = Instance.new("Decal") + +local loadedImagesByUri = {} + +local LoadableImage = Roact.PureComponent:extend("LoadableImage") + +function LoadableImage:init() + self.imageLabelRef = Roact.createRef() +end + +function LoadableImage:render() + local size = self.props.Size + local position = self.props.Position + local borderSizePixel = self.props.BorderSizePixel + local backgroundColor3 = self.props.BackgroundColor3 + + return Roact.createElement("ImageLabel", { + Position = position, + BorderSizePixel = borderSizePixel, + BackgroundColor3 = backgroundColor3, + Size = size, + [Roact.Ref] = self.imageLabelRef, + }) +end + +function LoadableImage:didUpdate(oldProps) + self:_loadImage() +end + +function LoadableImage:_loadImage() + local image = self.props.Image + + if not image or image == "" then + return + end + + -- Check if the image is already the current GUI image + if self.imageLabelRef.current.Image == image then + return + end + + -- Check if the image URI is already loaded + if loadedImagesByUri[image] then + self.imageLabelRef.current.Image = image + return + end + + -- Set default loading image + self.imageLabelRef.current.Image = self.props.loadingImage + + -- Synchronization/Batching work should be done in engine for performance improvements + -- related ticket: CLIPLAYEREX-1764 + spawn(function() + decal.Texture = image + ContentProvider:PreloadAsync({decal}) + + loadedImagesByUri[image] = true + + -- Context might be changed when resume, so we should check if roblox object is still valid here + if self.imageLabelRef.current and self.props.Image == image then + self.imageLabelRef.current.Image = image + end + end) +end + +LoadableImage.didMount = LoadableImage._loadImage + +function LoadableImage._mockPreloadDone(image) + loadedImagesByUri[image] = true +end + +return LoadableImage \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadableImage.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadableImage.spec.lua new file mode 100644 index 0000000..5f62669 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadableImage.spec.lua @@ -0,0 +1,39 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local LoadableImage = require(script.parent.LoadableImage) + + local testImage = "https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814" + local defaultLoadImage = "rbxasset://textures/ui/LuaApp/icons/ic-game.png" + + describe("LoadableImage", function() + it("should create and destroy without errors", function() + local element = Roact.createElement(LoadableImage, { + Image = testImage, + Size = UDim2.new(0, 80, 0, 80), + Position = UDim2.new(0, 50, 0, 50), + BorderSizePixel = 0, + BackgroundColor3 = Color3.new(0,0,0), + loadingImage = defaultLoadImage, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should not set loading image if image is already in cache", function() + LoadableImage._mockPreloadDone(testImage) + local element = Roact.createElement(LoadableImage, { + Image = testImage, + Size = UDim2.new(0, 80, 0, 80), + Position = UDim2.new(0, 50, 0, 50), + BorderSizePixel = 0, + BackgroundColor3 = Color3.new(0,0,0), + loadingImage = defaultLoadImage, + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "LoadableImageSample") + expect(container.LoadableImageSample.Image).never.to.equal(defaultLoadImage) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadingBar.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadingBar.lua new file mode 100644 index 0000000..587577c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadingBar.lua @@ -0,0 +1,48 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Workspace = game:GetService("Workspace") +local RunService = game:GetService('RunService') + +local Roact = require(Modules.Common.Roact) + +local BAR_SLICE_CENTER = Rect.new(1, 0, 2, 3) +local BAR_MAX_SIZE = 15 +local BAR_MAX_AMPLITUDE = 40 +local BAR_DIAMETER = 4 +local BAR_PERIOD = 1.25 + +local LoadingBar = Roact.Component:extend("LoadingBar") + +function LoadingBar:init() + self.barRef = Roact.createRef() +end + +function LoadingBar:render() + return Roact.createElement("ImageLabel", { + Image = "rbxasset://textures/ui/LuaApp/9-slice/gr-loading-indicator.png", + ScaleType = "Slice", + SliceCenter = BAR_SLICE_CENTER, + BackgroundTransparency = 1, + BorderSizePixel = 0, + [Roact.Ref] = self.barRef, + }) +end + +function LoadingBar:didMount() + self.connection = RunService.RenderStepped:Connect(function() + local t = Workspace.DistributedGameTime + local instance = self.barRef.current + local period = 2.0 * math.pi / BAR_PERIOD + + local width = (BAR_MAX_SIZE/2) * (1 - math.cos(2*t*period)) + instance.Size = UDim2.new(0, BAR_DIAMETER + width, 0, BAR_DIAMETER) + + local x = BAR_MAX_AMPLITUDE * math.cos(t*period) + instance.Position = UDim2.new(0.5, x - width/2 - BAR_DIAMETER/2, 0.5, 0) + end) +end + +function LoadingBar:willUnmount() + self.connection:Disconnect() +end + +return LoadingBar \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadingBar.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadingBar.spec.lua new file mode 100644 index 0000000..86cb473 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadingBar.spec.lua @@ -0,0 +1,17 @@ +return function() + local LoadingBar = require(script.Parent.LoadingBar) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + + it("should create and destroy without errors", function() + local element = Roact.createElement(LoadingBar, { + Position = UDim2.new(0.5, 0, 0.5, 5), + AnchorPoint = Vector2.new(0.5, 0.5), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadingIndicator.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadingIndicator.lua new file mode 100644 index 0000000..c03a1ef --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadingIndicator.lua @@ -0,0 +1,92 @@ +local CoreGui = game:GetService("CoreGui") +local RunService = game:GetService("RunService") +local Workspace = game:GetService("Workspace") + +local Modules = CoreGui.RobloxGui.Modules +local Constants = require(Modules.LuaChat.Constants) +local ExternalEventConnection = require(Modules.Common.RoactUtilities.ExternalEventConnection) +local Roact = require(Modules.Common.Roact) + +local DEFAULT_ANIMATION_SPEED_MULTIPLIER = 1.75 +local DEFAULT_DOT_COLOR = Constants.Color.GRAY3 +local DEFAULT_DOT_HIGHLIGHT_COLOR = Constants.Color.BLUE_PRIMARY +local DEFAULT_DOT_SCALE = 0.7 +local DEFAULT_INDICATOR_HEIGHT = 16 +local DEFAULT_INDICATOR_WIDTH = 70 + +local DOT_COUNT = 3 +local INDICATOR_RATIO = 35 / 8 + +local LoadingIndicator = Roact.PureComponent:extend("LoadingIndicator") + +LoadingIndicator.defaultProps = { + AnimationSpeedMultiplier = DEFAULT_ANIMATION_SPEED_MULTIPLIER, + DotColor = DEFAULT_DOT_COLOR, + DotHighlightColor = DEFAULT_DOT_HIGHLIGHT_COLOR, + DotScale = DEFAULT_DOT_SCALE, + Size = UDim2.new(0, DEFAULT_INDICATOR_WIDTH, 0, DEFAULT_INDICATOR_HEIGHT) +} + +function LoadingIndicator:init() + self.state = { + timer = true, + } + + self.renderSteppedCallback = function(dt) + local nextState = { + timer = not self.state.timer, + } + self:setState(nextState) + end +end + +function LoadingIndicator:render() + local animationSpeedMultiplier = self.props.AnimationSpeedMultiplier + local dotColor = self.props.DotColor + local dotHighlightColor = self.props.DotHighlightColor + local dotScale = self.props.DotScale + local size = self.props.Size + + local deltaTime = (Workspace.DistributedGameTime * animationSpeedMultiplier) % DOT_COUNT + local dotItems = {} + + dotItems["RatioConstraint"] = Roact.createElement("UIAspectRatioConstraint", { + AspectRatio = INDICATOR_RATIO, + AspectType = Enum.AspectType.FitWithinMaxSize, + }) + + for index = 1, DOT_COUNT do + local backgroundColor = dotColor + local currentYScale = dotScale + local horizontalPosition = (index - 1) / (DOT_COUNT - 1) + + if deltaTime >= index - 1 and deltaTime <= index then + local curve = math.sin(math.pi * (deltaTime % 1)) + currentYScale = currentYScale + (1 - currentYScale) * curve + backgroundColor = dotColor:lerp(dotHighlightColor, curve) + end + + dotItems[index] = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(horizontalPosition, 0.5), + BackgroundColor3 = backgroundColor, + BorderSizePixel = 0, + Size = UDim2.new(dotScale, 0, currentYScale, 0), + SizeConstraint = Enum.SizeConstraint.RelativeYY, + Position = UDim2.new(horizontalPosition, 0, 0.5, 0), + }) + end + + dotItems["RenderStepped"] = Roact.createElement(ExternalEventConnection, { + callback = self.renderSteppedCallback, + event = RunService.renderStepped, + }) + + return Roact.createElement("Frame", { + AnchorPoint = self.props.AnchorPoint, + BackgroundTransparency = 1, + Position = self.props.Position, + Size = size, + }, dotItems) +end + +return LoadingIndicator \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadingIndicator.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadingIndicator.spec.lua new file mode 100644 index 0000000..2a31b7c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LoadingIndicator.spec.lua @@ -0,0 +1,14 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local LoadingIndicator = require(Modules.LuaApp.Components.LoadingIndicator) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local element = mockServices({ + LoadingIndicator = Roact.createElement(LoadingIndicator) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedFitTextLabel.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedFitTextLabel.lua new file mode 100644 index 0000000..b08023a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedFitTextLabel.lua @@ -0,0 +1,5 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local RoactLocalization = require(Modules.LuaApp.Services.RoactLocalization) +local FitTextLabel = require(Modules.LuaApp.Components.FitTextLabel) + +return RoactLocalization.connect({ "Text" })(FitTextLabel) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedSectionHeader.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedSectionHeader.lua new file mode 100644 index 0000000..76db2ce --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedSectionHeader.lua @@ -0,0 +1,6 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local RoactLocalization = require(Modules.LuaApp.Services.RoactLocalization) +local SectionHeader = require(Modules.LuaApp.Components.SectionHeader) + +return RoactLocalization.connect({ "text" })(SectionHeader) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedSectionHeaderWithSeeAll.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedSectionHeaderWithSeeAll.lua new file mode 100644 index 0000000..6936bbc --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedSectionHeaderWithSeeAll.lua @@ -0,0 +1,6 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local RoactLocalization = require(Modules.LuaApp.Services.RoactLocalization) +local SectionHeaderWithSeeAll = require(Modules.LuaApp.Components.SectionHeaderWithSeeAll) + +return RoactLocalization.connect({ "text" })(SectionHeaderWithSeeAll) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedTextBox.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedTextBox.lua new file mode 100644 index 0000000..c4f3061 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedTextBox.lua @@ -0,0 +1,5 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local RoactLocalization = require(Modules.LuaApp.Services.RoactLocalization) + +return RoactLocalization.connect({ "PlaceholderText" })("TextBox") diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedTextButton.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedTextButton.lua new file mode 100644 index 0000000..e9904e5 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedTextButton.lua @@ -0,0 +1,5 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local RoactLocalization = require(Modules.LuaApp.Services.RoactLocalization) + +return RoactLocalization.connect({ "Text" })("TextButton") diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedTextLabel.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedTextLabel.lua new file mode 100644 index 0000000..05e954d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/LocalizedTextLabel.lua @@ -0,0 +1,5 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local RoactLocalization = require(Modules.LuaApp.Services.RoactLocalization) + +return RoactLocalization.connect({ "Text" })("TextLabel") diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MorePage.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MorePage.lua new file mode 100644 index 0000000..fa344e5 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MorePage.lua @@ -0,0 +1,288 @@ +--[[ +The More page +_____________________ +| | +| TopBar | +|___________________| +| | +| List 1 | +| List 2 | +| List 3 | +| List 4 | +| Log Out | +|___________________| +]] + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) + +local Constants = require(Modules.LuaApp.Constants) +local TopBar = require(Modules.LuaApp.Components.TopBar) +local FitChildren = require(Modules.LuaApp.FitChildren) + +local MoreTable = require(Modules.LuaApp.Components.More.MoreTable) +local MoreRow = require(Modules.LuaApp.Components.More.MoreRow) + +local AppGuiService = require(Modules.LuaApp.Services.AppGuiService) +local RoactServices = require(Modules.LuaApp.RoactServices) + +local SECTION_PADDING = 25 +local ROW_HEIGHT = 40 +local X_PADDING = -10 + +local MorePage = Roact.PureComponent:extend("MorePage") + +local function Spacer(props) + local height = props.height or SECTION_PADDING + local LayoutOrder = props.LayoutOrder + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, height), + BackgroundTransparency = 1, + LayoutOrder = LayoutOrder, + }) +end + +function MorePage:init() + local notificationTypeList = self.props.guiService:GetNotificationTypeList() + + self.itemList1 = { + { + Text = "CommonUI.Features.Label.Catalog", + Image = "rbxasset://textures/ui/LuaApp/icons/ic-catalog.png", + OnActivatedData = { + NotificationType = notificationTypeList.VIEW_SUB_PAGE_IN_MORE, + NotificationData = "Catalog" + }, + }, + { + Text = "CommonUI.Features.Label.BuildersClub", + Image = "rbxasset://textures/ui/LuaApp/category/ic-bc.png", + OnActivatedData = { + NotificationType = notificationTypeList.VIEW_SUB_PAGE_IN_MORE, + NotificationData = "BuildersClub" + }, + }, + } + + self.itemList2 = { + { + Text = "CommonUI.Features.Label.Profile", + Image = "rbxasset://textures/ui/LuaApp/icons/ic-avatar.png", + OnActivatedData = { + NotificationType = notificationTypeList.VIEW_SUB_PAGE_IN_MORE, + NotificationData = "Profile" + }, + }, + { + Text = "CommonUI.Features.Label.Friends", + Image = "rbxasset://textures/ui/LuaApp/icons/ic-friend.png", + OnActivatedData = { + NotificationType = notificationTypeList.VIEW_SUB_PAGE_IN_MORE, + NotificationData = "Friends" + }, + }, + { + Text = "CommonUI.Features.Label.Groups", + Image = "rbxasset://textures/ui/LuaApp/category/ic-featured.png", + OnActivatedData = { + NotificationType = notificationTypeList.VIEW_SUB_PAGE_IN_MORE, + NotificationData = "Groups" + }, + }, + { + Text = "CommonUI.Features.Label.Inventory", + Image = "rbxasset://textures/ui/LuaApp/category/ic-featured.png", + OnActivatedData = { + NotificationType = notificationTypeList.VIEW_SUB_PAGE_IN_MORE, + NotificationData = "Inventory" + } + }, + { + Text = "CommonUI.Features.Label.Messages", + Image = "rbxasset://textures/ui/LuaApp/category/ic-featured.png", + OnActivatedData = { + NotificationType = notificationTypeList.VIEW_SUB_PAGE_IN_MORE, + NotificationData = "Messages" + }, + }, + { + Text = "CommonUI.Features.Label.CreateGames", + Image = "rbxasset://textures/ui/LuaApp/icons/ic-games.png", + OnActivatedData = { + NotificationType = notificationTypeList.VIEW_SUB_PAGE_IN_MORE, + NotificationData = "CreateGames" + }, + }, + } + + self.itemList3 = { + { + Text = "CommonUI.Features.Label.Events", + Image = "rbxasset://textures/ui/LuaApp/category/ic-featured.png", + OnActivatedData = { + NotificationType = notificationTypeList.VIEW_SUB_PAGE_IN_MORE, + NotificationData = "Events" + }, + }, + { + Text = "CommonUI.Features.Label.Blog", + Image = "rbxasset://textures/ui/LuaApp/category/ic-featured.png", + OnActivatedData = { + NotificationType = notificationTypeList.VIEW_SUB_PAGE_IN_MORE, + NotificationData = "Blog" + }, + }, + } + + self.itemList4 = { + { + Text = "CommonUI.Features.Label.Settings", + Image = "rbxasset://textures/ui/LuaApp/category/ic-featured.png", + OnActivatedData = { + NotificationType = notificationTypeList.VIEW_SUB_PAGE_IN_MORE, + NotificationData = "Settings" + }, + }, + { + Text = "CommonUI.Features.Label.About", + Image = "rbxasset://textures/ui/LuaApp/category/ic-featured.png", + OnActivatedData = { + NotificationType = notificationTypeList.VIEW_SUB_PAGE_IN_MORE, + NotificationData = "About" + }, + }, + { + Text = "CommonUI.Features.Label.Help", + Image = "rbxasset://textures/ui/LuaApp/category/ic-featured.png", + OnActivatedData = { + NotificationType = notificationTypeList.VIEW_SUB_PAGE_IN_MORE, + NotificationData = "Help" + }, + }, + } + + self.onActivated = function(activatedData) + local notificationType = activatedData.NotificationType + local notificationData = activatedData.NotificationData or "" + self.props.guiService:BroadcastNotification(notificationData, notificationType) + end +end + +function MorePage:render() + local topBarHeight = self.props.topBarHeight + local currentLayoutOrder = 0 + local function nextLayoutOrder() + currentLayoutOrder = currentLayoutOrder + 1 + return currentLayoutOrder + end + local notificationTypeList = self.props.guiService:GetNotificationTypeList() + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BorderSizePixel = 0, + }, { + TopBar = Roact.createElement(TopBar, { + showBackButton = false, + showBuyRobux = true, + showNotifications = true, + showSearch = true, + textKey = "CommonUI.Features.Label.More", + }), + Scroller = Roact.createElement(FitChildren.FitScrollingFrame, { + Position = UDim2.new(0, 0, 0, topBarHeight), + Size = UDim2.new(1, 0, 1, -topBarHeight), + CanvasSize = UDim2.new(1, 0, 0, 0), + BackgroundColor3 = Constants.Color.GRAY4, + BorderSizePixel = 0, + ScrollBarThickness = 0, + fitFields = { + CanvasSize = FitChildren.FitAxis.Height, + }, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }), + + Roact.createElement(Spacer, { + LayoutOrder = nextLayoutOrder(), + }), + + Roact.createElement(MoreTable, { + Items = self.itemList1, + RowHeight = ROW_HEIGHT, + LayoutOrder = nextLayoutOrder(), + onActivated = self.onActivated, + }), + + Roact.createElement(Spacer, { + LayoutOrder = nextLayoutOrder(), + }), + + Roact.createElement(MoreTable, { + Items = self.itemList2, + RowHeight = ROW_HEIGHT, + LayoutOrder = nextLayoutOrder(), + onActivated = self.onActivated, + }), + + Roact.createElement(Spacer, { + LayoutOrder = nextLayoutOrder(), + }), + + Roact.createElement(MoreTable, { + Items = self.itemList3, + RowHeight = ROW_HEIGHT, + LayoutOrder = nextLayoutOrder(), + onActivated = self.onActivated, + }), + + Roact.createElement(Spacer, { + LayoutOrder = nextLayoutOrder(), + }), + + Roact.createElement(MoreTable, { + Items = self.itemList4, + RowHeight = ROW_HEIGHT, + LayoutOrder = nextLayoutOrder(), + onActivated = self.onActivated, + }), + + -- The last Log Out row. + Roact.createElement(Spacer, { + LayoutOrder = nextLayoutOrder(), + }), + + Roact.createElement(MoreRow, { + Text = "Application.Logout.Action.Logout", + Size = UDim2.new(1, X_PADDING, 0, ROW_HEIGHT), + Image = nil, + LayoutOrder = nextLayoutOrder(), + onActivatedData = { + NotificationType = notificationTypeList.ACTION_LOG_OUT, + NotificationData = "" + }, + onActivated = self.onActivated, + }), + + Roact.createElement(Spacer, { + LayoutOrder = nextLayoutOrder(), + }), + }), + }) +end + +MorePage = RoactRodux.connect(function(store, props) + local state = store:getState() + + return { + topBarHeight = state.TopBar.topBarHeight, + } +end)(MorePage) + +return RoactServices.connect({ + guiService = AppGuiService +})(MorePage) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MorePage.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MorePage.spec.lua new file mode 100644 index 0000000..374a836 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MorePage.spec.lua @@ -0,0 +1,22 @@ +return function() + local MorePage = require(script.Parent.MorePage) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + local function MockMorePage() + return mockServices({ + MorePage = Roact.createElement(MorePage), + }, { + includeStoreProvider = true, + }) + end + + it("should create and destroy without errors", function() + local element = MockMorePage() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MoreRow.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MoreRow.lua new file mode 100644 index 0000000..3afcbc0 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MoreRow.lua @@ -0,0 +1,119 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local Constants = require(Modules.LuaApp.Constants) +local LocalizedTextLabel = require(Modules.LuaApp.Components.LocalizedTextLabel) +local LocalizedTextButton = require(Modules.LuaApp.Components.LocalizedTextButton) + +local FONT = Enum.Font.SourceSans +local TEXT_SIZE = 20 + +local IMAGE_WIDTH = 20 +local IMAGE_HEIGHT = 20 +local IMAGE_X_OFFSET = 15 +local IMAGE_Y_OFFSET = 10 +local LABEL_X_OFFSET = 60 + +local MoreRow = Roact.PureComponent:extend("MoreRow") + +function MoreRow:init() + self.state = { + isButtonPressed = false, + } + + self.onInputBegan = function() + self:setState({ + isButtonPressed = true + }) + end + + self.onInputEnded = function() + self:setState({ + isButtonPressed = false + }) + end + + self.onActivated = function() + local onActivated = self.props.onActivated + if onActivated then + onActivated(self.props.onActivatedData) + end + end +end + +function MoreRow:render() + local size = self.props.Size + local position = self.props.Position or UDim2.new(0, 0, 0, 0) + local text = self.props.Text + local image = self.props.Image + local LayoutOrder = self.props.LayoutOrder + local isButtonPressed = self.state.isButtonPressed + + local buttonBackgroundColor + if isButtonPressed then + buttonBackgroundColor = Constants.Color.GRAY5 + else + buttonBackgroundColor = Constants.Color.WHITE + end + + -- We have two cases: one with an image and one without. + if image then + return Roact.createElement("ImageButton", { + Size = size, + Position = position, + BackgroundTransparency = 1, + BorderSizePixel = 0, + AutoButtonColor = false, + LayoutOrder = LayoutOrder, + [Roact.Event.Activated] = self.onActivated, + -- Until we can get a better visual effect of the touch input, disable it! + -- Also, remember to set BackgroundTransparency = 0 also. + --[[ + BackgroundColor3 = buttonBackgroundColor, + [Roact.Event.InputBegan] = self.onInputBegan, + [Roact.Event.InputEnded] = self.onInputEnded, + --]] + }, { + Image = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, IMAGE_WIDTH, 0, IMAGE_HEIGHT), + Position = UDim2.new(0, IMAGE_X_OFFSET, 0, IMAGE_Y_OFFSET), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = false, + Image = image, + }), + Text = Roact.createElement(LocalizedTextLabel, { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, LABEL_X_OFFSET, 0, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = FONT, + Text = text, + TextSize = TEXT_SIZE, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + }), + }) + else + return Roact.createElement(LocalizedTextButton, { + Size = size, + Position = position, + BackgroundTransparency = 0, + BorderSizePixel = 0, + AutoButtonColor = false, + LayoutOrder = LayoutOrder, + Font = FONT, + Text = text, + TextSize = TEXT_SIZE, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + [Roact.Event.Activated] = self.onActivated, + -- Implement the effect of the touch input. + BackgroundColor3 = buttonBackgroundColor, + [Roact.Event.InputBegan] = self.onInputBegan, + [Roact.Event.InputEnded] = self.onInputEnded, + }) + end +end + +return MoreRow \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MoreRow.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MoreRow.spec.lua new file mode 100644 index 0000000..5610537 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MoreRow.spec.lua @@ -0,0 +1,46 @@ +return function() + local MoreRow = require(script.Parent.MoreRow) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create/destroy OK with an image", function() + local root = mockServices({ + element = Roact.createElement(MoreRow, { + Text = "Application.Logout.Action.Logout", + Size = UDim2.new(1, 25, 0, 40), + Image = "rbxasset://textures/ui/LuaApp/category/ic-featured.png", + LayoutOrder = 1, + onActivatedData = { + NotificationType = "ACTION_LOG_OUT", + NotificationData = "" + }, + onActivated = function() end, + }), + }) + + local instance = Roact.mount(root) + Roact.unmount(instance) + end) + + it("should create/destroy OK without an image", function() + local root = mockServices({ + element = Roact.createElement(MoreRow, { + Text = "Application.Logout.Action.Logout", + Size = UDim2.new(1, 25, 0, 40), + Image = nil, + LayoutOrder = 1, + onActivatedData = { + NotificationType = "ACTION_LOG_OUT", + NotificationData = "" + }, + onActivated = function() end, + }), + }) + + local instance = Roact.mount(root) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MoreTable.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MoreTable.lua new file mode 100644 index 0000000..98d7642 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MoreTable.lua @@ -0,0 +1,80 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local Constants = require(Modules.LuaApp.Constants) +local Line = require(Modules.LuaApp.Components.Line) + +local MoreRow = require(Modules.LuaApp.Components.More.MoreRow) + +local X_PADDING = -10 +local DIVIDER_HEIGHT = 1 +local DIVIDER_LEFT_MARGIN = 50 + +local MoreTable = Roact.PureComponent:extend("MoreTable") + +local function Divider(props) + local height = props.height or DIVIDER_HEIGHT + local xOffset = props.xOffset or DIVIDER_LEFT_MARGIN + local LayoutOrder = props.LayoutOrder + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, height), + BorderSizePixel = 0, + BackgroundTransparency = 1, + LayoutOrder = LayoutOrder + }, { + Roact.createElement(Line, { + Position = UDim2.new(0, xOffset, 0, 0), + }), + }) +end + +function MoreTable:render() + local items = self.props.Items + local rowHeight = self.props.RowHeight + local LayoutOrder = self.props.LayoutOrder + local onActivated = self.props.onActivated + + local listContents = {} + + listContents["Layout"] = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }) + + local currentLayoutOrder = 0 + local function nextLayoutOrder() + currentLayoutOrder = currentLayoutOrder + 1 + return currentLayoutOrder + end + + local rowCount = 0 + for position, item in ipairs(items) do + rowCount = rowCount + 1 + listContents[rowCount] = Roact.createElement(MoreRow, { + LayoutOrder = nextLayoutOrder(), + Size = UDim2.new(1, 0, 0, rowHeight), + Text = item.Text, + Image = item.Image, + onActivatedData = item.OnActivatedData, + onActivated = onActivated, + }) + + if position < #items then + rowCount = rowCount + 1 + listContents[rowCount] = Roact.createElement(Divider, { + LayoutOrder = nextLayoutOrder(), + }) + end + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, X_PADDING, 0, rowHeight * #items), + BackgroundTransparency = 0, + BackgroundColor3 = Constants.Color.WHITE, + BorderColor3 = Constants.Color.WHITE, + LayoutOrder = LayoutOrder + }, listContents) +end + +return MoreTable \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MoreTable.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MoreTable.spec.lua new file mode 100644 index 0000000..d84e5fe --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/More/MoreTable.spec.lua @@ -0,0 +1,40 @@ +return function() + local MoreTable = require(script.Parent.MoreTable) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local itemList = { + { + Text = "CommonUI.Features.Label.Events", + Image = "rbxasset://textures/ui/LuaApp/category/ic-featured.png", + OnActivatedData = { + NotificationType = "VIEW_SUB_PAGE_IN_MORE", + NotificationData = "Events" + }, + }, + { + Text = "CommonUI.Features.Label.Blog", + Image = "rbxasset://textures/ui/LuaApp/category/ic-featured.png", + OnActivatedData = { + NotificationType = "VIEW_SUB_PAGE_IN_MORE", + NotificationData = "Blog" + }, + }, + } + local root = mockServices({ + element = Roact.createElement(MoreTable, { + Items = itemList, + RowHeight = 40, + LayoutOrder = 1, + onActivated = function() end, + }), + }) + + local instance = Roact.mount(root) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/NotificationBadge.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/NotificationBadge.lua new file mode 100644 index 0000000..22e075e --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/NotificationBadge.lua @@ -0,0 +1,66 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local Constants = require(Modules.LuaApp.Constants) + +local BADGE = "rbxasset://textures/ui/LuaApp/9-slice/gr-search.png" + +local BADGE_HEIGHT = 20 +local BADGE_WIDTH = 20 +local BADGE_WIDTH_3_DIGIT = 24 +local BADGE_SLICE_CENTER = Rect.new(9, 9, 9, 9) +local BADGE_SLICE_CENTER_3_DIGIT = Rect.new(9, 9, 9, 16) + +local BADGE_Y_OFFSET = BADGE_HEIGHT/2 +local BADGE_X_OFFSET = BADGE_HEIGHT/2 - 1 + +local FONT = Enum.Font.SourceSans + +local function NotificationBadge(props) + local layoutOrder = props.layoutOrder + local notificationCount = props.notificationCount + + if not notificationCount or tonumber(notificationCount) == 0 then + return nil + end + + local badgeWidth = BADGE_WIDTH + local badgeHeight = BADGE_HEIGHT + local sliceCenter = BADGE_SLICE_CENTER + + local useExpandedSize = string.len(notificationCount) > 2 + + if useExpandedSize then + badgeWidth = BADGE_WIDTH_3_DIGIT + sliceCenter = BADGE_SLICE_CENTER_3_DIGIT + end + + return Roact.createElement("ImageLabel", { + Size = UDim2.new(0, badgeWidth, 0, badgeHeight), + AnchorPoint = Vector2.new(1, 0), + Position = UDim2.new(1, BADGE_X_OFFSET, 0, -BADGE_Y_OFFSET), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = BADGE, + LayoutOrder = layoutOrder, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = sliceCenter, + ImageColor3 = Color3.new(255,0,0), + }, { + Count = Roact.createElement("TextLabel", { + Size = UDim2.new(0.6, 0, 0.6, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, -1), + BackgroundTransparency = 1, + Font = FONT, + Text = notificationCount, + TextColor3 = Constants.Color.WHITE, + TextSize = 14, + TextScaled = useExpandedSize, + TextWrapped = false, + }), + }) +end + +return NotificationBadge \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/RefreshScrollingFrame.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/RefreshScrollingFrame.lua new file mode 100644 index 0000000..03795a4 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/RefreshScrollingFrame.lua @@ -0,0 +1,460 @@ +--[[ +A scrolling frame wraps pages for pulling down to refresh +props: +currentPage -- identify if native mobile buttonclick events comes from current page +refresh -- refresh function for this page +Size -- Size of the content in the scrolling frame +BackgroundColor3 +Position -- TopLeft Corner of ScrollingContent +_____________________ +| | +| TopBar | +|___________________| +| | +| | +| | +| ScrollingContent | +|___________________| +]] + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local UserInputService = game:GetService("UserInputService") +local RunService = game:GetService("RunService") +local HttpService = game:GetService("HttpService") + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) +local RoactServices = require(Modules.LuaApp.RoactServices) +local AppNotificationService = require(Modules.LuaApp.Services.AppNotificationService) +local ExternalEventConnection = require(Modules.Common.RoactUtilities.ExternalEventConnection) + +local FitChildren = require(Modules.LuaApp.FitChildren) +local RoactMotion = require(Modules.LuaApp.RoactMotion) +local EndOfScroll = require(Modules.LuaApp.Components.EndOfScroll) +local LoadingBar = require(Modules.LuaApp.Components.LoadingBar) + +local REFRESH_THRESHOLD = 25 +local TWEEN_BACK_TIME = 0.5 +local SPRING_STIFFNESS = 150 +local SPRING_DAMPING = 18 +local PRECISION = 2 + +local ROTATION_SCALE = 9.6 +local ROTATION_ORIGIN = 240 +local TRANSPARENCY_SCALE = 0.04 +local DEFAULT_SPINNER_SIZE = 20 +local CONFIRM_SCALE = 1.1 +local ROTATING_SPEED = 540 +local FADE_OUT_SCALE = 2 +local BLUE_ARROW_PATH = "rbxasset://textures/ui/LuaApp/icons/ic-blue-arrow.png" +local GRAY_ARROW_PATH = "rbxasset://textures/ui/LuaApp/icons/ic-gray-arrow.png" + +-- We would like to start loading more before user reaches the bottom. +-- The default distance from the bottom of that would be 2000. +local DEFAULT_PRELOAD_DISTANCE = 2000 + +local LOADING_BAR_PADDING = 20 +local LOADING_BAR_HEIGHT = 16 +local LOADING_BAR_TOTAL_HEIGHT = LOADING_BAR_PADDING * 2 + LOADING_BAR_HEIGHT + +local function Spinner(props) + -- should be spinning right now + local activated = props.activated + local offset = props.offset + local position = props.Position + local timer = props.timer + local tween = props.tween + + local rotation = 0 + local scale = 1 + local image = BLUE_ARROW_PATH + local imageTransparency = 0 + + if offset > 0 then + return + end + + if activated then + rotation = timer * ROTATING_SPEED + offset = 0 + + elseif tween then + offset = 0 + imageTransparency = FADE_OUT_SCALE * timer + image = GRAY_ARROW_PATH + + elseif offset > -REFRESH_THRESHOLD then + offset = -offset + rotation = ROTATION_SCALE * offset - ROTATION_ORIGIN + imageTransparency = 1 - TRANSPARENCY_SCALE * offset + image = GRAY_ARROW_PATH + + else + scale = CONFIRM_SCALE + offset = REFRESH_THRESHOLD + end + + return Roact.createElement("ImageLabel", { + Size = UDim2.new(0, DEFAULT_SPINNER_SIZE * scale, 0, DEFAULT_SPINNER_SIZE * scale), + Position = position + UDim2.new(0.5, 0, 0, offset - DEFAULT_SPINNER_SIZE / 2), + ImageTransparency = imageTransparency, + Image = image, + BackgroundTransparency = 1, + Rotation = rotation, + AnchorPoint = Vector2.new(0.5, 0.5), + }) +end + +local RefreshScrollingFrame = Roact.Component:extend("RefreshScrollingFrame") + +RefreshScrollingFrame.defaultProps = { + preloadDistance = DEFAULT_PRELOAD_DISTANCE, + createEndOfScrollElement = false, +} + +function RefreshScrollingFrame:startTweenBack() + + -- refresh finishes, spinner stops spin, animate the spinner poping back + self:setState({ + activated = false, + tween = true, + timer = 0, + offset = 0, + }) +end + +function RefreshScrollingFrame:startSpin() + + -- spinner hanging and spinning + self:setState({ + activated = true, + tween = false, + timer = 0, + }) +end + +function RefreshScrollingFrame:startTween() + + -- spinner appear with a tweened animation + self:setState({ + activated = true, + tween = true, + timer = 0, + }) +end + +function RefreshScrollingFrame:didMount() + self._isMounted = true +end + +function RefreshScrollingFrame:willUnmount() + self._isMounted = false +end + + +function RefreshScrollingFrame:init() + self._inputCount = 0 + self._shouldRefreshOnScroll = false + self._isMounted = false + + self.state = { + -- for refresh spinner: + activated = false, + tween = false, + timer = 0, + offset = 0, + -- for loadMore: + isLoadingMore = false + } + self.fitFieldCanvasSize = { + CanvasSize = FitChildren.FitAxis.Height, + } + + -- store ref so the [Roact.Ref] doesn't change everyupdate + self._refCallBack = function(rbx) + self.ref = rbx + end + + self.scrollBack = function() + if self.ref then + self.ref:ScrollToTop() + end + end + + self.onCanvasPositionChanged = function(rbx) + local preloadDistance = self.props.preloadDistance + local refresh = self.props.refresh + local onLoadMore = self.props.onLoadMore + local isLoadingMore = self.state.isLoadingMore + + local newPosition = rbx.CanvasPosition.Y + + -- Offset is used for the refreshing spinner. + if refresh and self._shouldRefreshOnScroll and + newPosition < REFRESH_THRESHOLD or self.state.offset < REFRESH_THRESHOLD then + self:setState({ + offset = newPosition, + }) + end + + -- Check if we want to load more things + if onLoadMore and not isLoadingMore then + if rbx.CanvasSize.Y.Scale ~= 0 then + warn([[RefreshScrollingFrame: Scrollingframe.CanvasSize.Y.Scale is not 0! + Content loading would not work properly.]]) + return + end + + local loadMoreThreshold = rbx.CanvasSize.Y.Offset - rbx.AbsoluteWindowSize.Y - preloadDistance + + if newPosition > loadMoreThreshold then + self:setState({ + isLoadingMore = true + }) + + onLoadMore():andThen( + -- Succeed: + function() + if self._isMounted then + self:setState({ + isLoadingMore = false + }) + end + end, + + -- Failed: + function() + -- Allow us to retry. + if self._isMounted then + self:setState({ + isLoadingMore = false + }) + end + end + ) + end + end + end + + self.renderSteppedCallback = function(dt) + if self.state.activated or self.state.tween then + local nextState = { + timer = self.state.timer + dt, + } + if self.state.tween and self.state.timer > TWEEN_BACK_TIME then + nextState.tween = false + end + self:setState(nextState) + end + end + + self.inputBeganCallback = function(input) + + -- To support desktop apps this check should be dependent on platform + if input.UserInputType ~= Enum.UserInputType.Touch then + return + end + self._shouldRefreshOnScroll = true + self._inputCount = self._inputCount + 1 + end + + self.inputEndedCallback = function(input) + local refresh = self.props.refresh + + if input.UserInputType ~= Enum.UserInputType.Touch then + return + end + + -- Count should always > 0 whenever input ended, otherwise we missed a begin here. + if self._inputCount > 0 then + self._inputCount = self._inputCount - 1 + end + + -- only determine refresh or not when input count drops back to 0 again (end of multi-touch) + if self._inputCount > 0 then + return + end + + self._shouldRefreshOnScroll = false + + if self.state.offset < -REFRESH_THRESHOLD and not self.state.activated then + self:startSpin() + + refresh():andThen( + function() + if self._isMounted then + self:startTweenBack() + end + end, + + -- failure handler + function() + if self._isMounted then + self:startTweenBack() + end + end + ) + elseif self.state.offset < 0 then + self._shouldRefreshOnScroll = true + end + end + + self.statusBarTapCallback = function() + self.scrollBack() + end + + -- Hooking to rbxevent is a temp solution and signals will be passed in by the new lua bottom bar + self.bottomBarButtonPressedCallback = function(event) + local refresh = self.props.refresh + + if self.state.activated then + return + end + + local currentRoute = self.props.currentRoute + + if event.namespace == "Navigations" and event.detailType == "Reload" then + local eventDetails = HttpService:JSONDecode(event.detail) + if eventDetails.appName == currentRoute[1].name then + self.scrollBack() + + if refresh then + self._shouldRefreshOnScroll = true + self:startTween() + refresh():andThen( + function() + self:startTweenBack() + end, + + -- failure handler + function() + self:startTweenBack() + end + ) + end + end + end + end +end + +function RefreshScrollingFrame:render() + local size = self.props.Size + local backgroundColor3 = self.props.BackgroundColor3 + local targetYPadding = self.props.Position.Y.Offset + local currentRoute = self.props.currentRoute + local notificationService = self.props.NotificationService + local isLoadingMore = self.state.isLoadingMore + + local refreshOnNavBar = #currentRoute == 1 + + if self.state.activated then + if self.state.offset > 0 and self.state.offset < REFRESH_THRESHOLD then + targetYPadding = targetYPadding - self.state.offset + REFRESH_THRESHOLD + elseif self.state.offset <= 0 then + targetYPadding = targetYPadding + REFRESH_THRESHOLD + end + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = backgroundColor3, + }, { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + VerticalAlignment = Enum.VerticalAlignment.Top, + }), + spinnerFrame = Roact.createElement(RoactMotion.SimpleMotion, { + style = { + sizeY = RoactMotion.spring(targetYPadding, SPRING_STIFFNESS, SPRING_DAMPING, PRECISION), + }, + render = function(values) + local spinnerPosition = self.state.tween and values.sizeY or targetYPadding + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, values.sizeY), + BackgroundTransparency = 1, + LayoutOrder = 1, + },{ + spinner = Spinner({ + Position = UDim2.new(0, 0, 0, spinnerPosition), + offset = self.state.offset, + activated = self.state.activated, + timer = self.state.timer, + tween = self.state.tween, + }) + }) + end, + }), + scrollingFrame = Roact.createElement(FitChildren.FitScrollingFrame, { + Size = size, + ScrollBarThickness = 0, + BorderSizePixel = 0, + BackgroundTransparency = 1, + LayoutOrder = 2, + ElasticBehavior = Enum.ElasticBehavior.Always, + ScrollingDirection = Enum.ScrollingDirection.Y, + fitFields = self.fitFieldCanvasSize, + [Roact.Ref] = self._refCallBack, + [Roact.Change.CanvasPosition] = self.onCanvasPositionChanged, + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Content = Roact.createElement(FitChildren.FitFrame, { + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = 1, + Size = UDim2.new(1, 0, 1, 0), + fitFields = { + Size = FitChildren.FitAxis.Height, + }, + }, self.props[Roact.Children]), + LoadingBarFrame = isLoadingMore and Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, LOADING_BAR_TOTAL_HEIGHT), + }, { + LoadingBar = Roact.createElement(LoadingBar), + }), + EndOfScroll = self.props.createEndOfScrollElement and Roact.createElement(EndOfScroll, { + backToTopCallback = self.scrollBack, + LayoutOrder = 3, + }), + }), + renderStepped = Roact.createElement(ExternalEventConnection, { + event = RunService.renderStepped, + callback = self.renderSteppedCallback, + }), + inputBegan = Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputBegan, + callback = self.inputBeganCallback, + }), + inputEnded = Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputEnded, + callback = self.inputEndedCallback, + }), + statusBarTapped = (not _G.__TESTEZ_RUNNING_TEST__) and Roact.createElement(ExternalEventConnection, { + event = UserInputService.StatusBarTapped, + callback = self.statusBarTapCallback, + }), + bottomBarButtonPressed = refreshOnNavBar and + Roact.createElement(ExternalEventConnection, { + event = notificationService.RobloxEventReceived, + callback = self.bottomBarButtonPressedCallback, + }), + }) +end + +RefreshScrollingFrame = RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + currentRoute = state.Navigation.history[#state.Navigation.history] + } + end +)(RefreshScrollingFrame) + +return RoactServices.connect({ + NotificationService = AppNotificationService, +})(RefreshScrollingFrame) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/RefreshScrollingFrame.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/RefreshScrollingFrame.spec.lua new file mode 100644 index 0000000..43eee4f --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/RefreshScrollingFrame.spec.lua @@ -0,0 +1,66 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + local RefreshScrollingFrame = require(Modules.LuaApp.Components.RefreshScrollingFrame) + + it("should create and destroy without errors, given only refresh function", function() + local element = mockServices({ + scrollingFrame = Roact.createElement(RefreshScrollingFrame, { + currentPage = "Games", + refresh = function() + return 1 + end, + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Color3.fromRGB(0,0,0), + Position = UDim2.new(0, 0, 0, 0), + }), + }, { + includeStoreProvider = true, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors, given only loadMore function", function() + local element = mockServices({ + scrollingFrame = Roact.createElement(RefreshScrollingFrame, { + currentPage = "Games", + loadMore = function() + return 1 + end, + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Color3.fromRGB(0,0,0), + Position = UDim2.new(0, 0, 0, 0), + }), + }, { + includeStoreProvider = true, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors, given both refresh and loadMore function", function() + local element = mockServices({ + scrollingFrame = Roact.createElement(RefreshScrollingFrame, { + currentPage = "Games", + refresh = function() + return 1 + end, + loadMore = function() + return 1 + end, + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Color3.fromRGB(0,0,0), + Position = UDim2.new(0, 0, 0, 0), + }), + }, { + includeStoreProvider = true, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/RoactDummyPageWrap.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/RoactDummyPageWrap.lua new file mode 100644 index 0000000..b5bbfb7 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/RoactDummyPageWrap.lua @@ -0,0 +1,27 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local Constants = require(Modules.LuaApp.Constants) + +local RoactDummyPageWrap = Roact.PureComponent:extend("RoactDummyPageWrap") + +function RoactDummyPageWrap:render() + local isVisible = self.props.isVisible + local pageType = self.props.pageType + + return Roact.createElement("ScreenGui", { + Enabled = isVisible, + ZIndexBehavior = Enum.ZIndexBehavior.Sibling, + }, { + Title = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Text = pageType, + TextSize = 30, + BackgroundColor3 = Constants.Color.GRAY4, + BorderSizePixel = 0, + }), + }) +end + +return RoactDummyPageWrap \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ScreenGuiWrap.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ScreenGuiWrap.lua new file mode 100644 index 0000000..75b400d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ScreenGuiWrap.lua @@ -0,0 +1,60 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local NotificationType = require(Modules.LuaApp.Enum.NotificationType) + +local AppPage = require(Modules.LuaApp.AppPage) +local AppGuiService = require(Modules.LuaApp.Services.AppGuiService) +local RoactServices = require(Modules.LuaApp.RoactServices) + +local FlagSettings = require(Modules.LuaApp.FlagSettings) +local isLoadingHUDOniOSEnabledForGameShare = FlagSettings.IsLoadingHUDOniOSEnabledForGameShare() + +-- TODO Once HomePage and GamesHub creates their own ScreenGui, +-- the ScreenGuiWrap should be removed. +local ScreenGuiWrap = Roact.PureComponent:extend("ScreenGuiWrap") + +function ScreenGuiWrap:didMount() + local isVisible = self.props.isVisible + local pageType = self.props.pageType + local guiService = self.props.guiService + + if isLoadingHUDOniOSEnabledForGameShare then + if isVisible and pageType ~= AppPage.ShareGameToChat then + guiService:BroadcastNotification(pageType, NotificationType.APP_READY) + end + else + if isVisible then + guiService:BroadcastNotification(pageType, NotificationType.APP_READY) + end + end +end + +function ScreenGuiWrap:render() + local component = self.props.component + local isVisible = self.props.isVisible + local props = self.props.props + + return Roact.createElement("ScreenGui", { + Enabled = isVisible, + ZIndexBehavior = Enum.ZIndexBehavior.Sibling, + }, { + Contents = Roact.createElement(component, props), + }) +end + +-- Staging broadcasting of APP_READY to accomodate for unpredictable delay on the native side. +-- Once Lua tab bar is integrated, there will be no use for this, as current page information +-- will be propagated instantly within the Roact paradigm. +function ScreenGuiWrap:didUpdate(prevProps, prevState) + local guiService = self.props.guiService + + if not prevProps.isVisible and self.props.isVisible then + guiService:BroadcastNotification(self.props.pageType, NotificationType.APP_READY) + end +end + +return RoactServices.connect({ + guiService = AppGuiService +})(ScreenGuiWrap) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ScreenGuiWrap.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ScreenGuiWrap.spec.lua new file mode 100644 index 0000000..c26db51 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/ScreenGuiWrap.spec.lua @@ -0,0 +1,18 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local ScreenGuiWrap = require(script.parent.ScreenGuiWrap) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local element = mockServices({ + ScreenGuiWrap = Roact.createElement(ScreenGuiWrap, { + component = "Frame", + isVisible = true, + props = {}, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Search/GamesSearch.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Search/GamesSearch.lua new file mode 100644 index 0000000..a5ef18d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Search/GamesSearch.lua @@ -0,0 +1,290 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) +local RoactServices = require(Modules.LuaApp.RoactServices) +local RoactNetworking = require(Modules.LuaApp.Services.RoactNetworking) +local RoactAnalyticsSearchPage = require(Modules.LuaApp.Services.RoactAnalyticsSearchPage) + +local AppPage = require(Modules.LuaApp.AppPage) +local Constants = require(Modules.LuaApp.Constants) +local SearchUuid = require(Modules.LuaApp.SearchUuid) +local SearchRetrievalStatus = require(Modules.LuaApp.Enum.SearchRetrievalStatus) + +local LocalizedFitTextLabel = require(Modules.LuaApp.Components.LocalizedFitTextLabel) +local FitTextLabel = require(Modules.LuaApp.Components.FitTextLabel) +local FitTextButton = require(Modules.LuaApp.Components.FitTextButton) +local FitChildren = require(Modules.LuaApp.FitChildren) +local LoadingBar = require(Modules.LuaApp.Components.LoadingBar) +local RefreshScrollingFrame = require(Modules.LuaApp.Components.RefreshScrollingFrame) +local GameGrid = require(Modules.LuaApp.Components.Games.GameGrid) + +local RemoveSearchInGames = require(Modules.LuaApp.Actions.RemoveSearchInGames) +local SetSearchInGamesStatus = require(Modules.LuaApp.Actions.SetSearchInGamesStatus) +local ApiFetchSearchInGames = require(Modules.LuaApp.Thunks.ApiFetchSearchInGames) +local NavigateSideways = require(Modules.LuaApp.Thunks.NavigateSideways) + +local HEADER_GRID_PADDING = 12 +local HEADER_INNER_PADDING = 6 +local TITLE_KEYWORD_PADDING = 2 +local SEARCH_RESULT_TEXT_SIZE = 18 + +local GamesSearch = Roact.PureComponent:extend("GamesSearch") + +function GamesSearch:getSearchedKeyword() + local searchInGames = self.props.searchInGames + local keyword = searchInGames.keyword + local correctedKeyword = searchInGames.correctedKeyword + return correctedKeyword and correctedKeyword or keyword +end + +function GamesSearch:getDisplayedSearchKeyword() + local searchInGames = self.props.searchInGames + local keyword = searchInGames.keyword + local correctedKeyword = searchInGames.correctedKeyword + local filteredKeyword = searchInGames.filteredKeyword + return filteredKeyword and filteredKeyword or correctedKeyword or keyword +end + +function GamesSearch:getDisplayedSuggestedKeyword() + local searchInGames = self.props.searchInGames + local keyword = searchInGames.keyword + local suggestedKeyword = searchInGames.suggestedKeyword + local correctedKeyword = searchInGames.correctedKeyword + return correctedKeyword and keyword or suggestedKeyword +end + +function GamesSearch:init() + self.refresh = function() + local networking = self.props.networking + local searchUuid = self.props.searchUuid + local searchInGames = self.props.searchInGames + local dispatchSearch = self.props.dispatchSearch + + return dispatchSearch(networking, searchInGames.keyword, searchUuid, searchInGames.isKeywordSuggestionEnabled) + end + + self.loadMore = function() + local loadCount = Constants.DEFAULT_GAME_FETCH_COUNT + local networking = self.props.networking + local searchUuid = self.props.searchUuid + local searchInGames = self.props.searchInGames + local dispatchLoadMore = self.props.dispatchLoadMore + local searchedKeyword = self:getSearchedKeyword() + + return dispatchLoadMore(networking, searchedKeyword, searchUuid, searchInGames.rowsRequested, loadCount) + end + + self.onKeywordButtonActivated = function() + local searchUuid = SearchUuid() + local searchKeyword = self:getDisplayedSuggestedKeyword() + + self.props.dispatchSearch(self.props.networking, searchKeyword, searchUuid) + self.props.navigateSideways(searchUuid) + end +end + +function GamesSearch:render() + local searchInGames = self.props.searchInGames + local searchInGamesStatus = self.props.searchInGamesStatus + local screenSize = self.props.screenSize + local analytics = self.props.analytics + + if searchInGamesStatus == SearchRetrievalStatus.Failed then + -- TODO: add failure handler/retry button. + return nil + + elseif not searchInGames and searchInGamesStatus == SearchRetrievalStatus.Fetching then + -- We're doing our initial load of the results + return Roact.createElement(LoadingBar) + + elseif searchInGames then + local entries = searchInGames.entries + local suggestedKeyword = searchInGames.suggestedKeyword + local correctedKeyword = searchInGames.correctedKeyword + local hasSuggestion = suggestedKeyword or correctedKeyword + local displayedSearchKeyword = self:getDisplayedSearchKeyword() + local displayedSuggestedKeyword = self:getDisplayedSuggestedKeyword() + local suggestionTitleText = correctedKeyword and {"Feature.GamePage.LabelSearchInsteadFor"} or + {"Feature.GamePage.LabelSearchYouMightMean"} + + return Roact.createElement(RefreshScrollingFrame, { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + BackgroundColor3 = Constants.Color.GRAY4, + CanvasSize = UDim2.new(1, 0, 1, 0), + refresh = self.refresh, + onLoadMore = searchInGames.hasMoreRows and self.loadMore, + createEndOfScrollElement = not searchInGames.hasMoreRows, + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, HEADER_GRID_PADDING), + }), + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, Constants.GAME_GRID_PADDING), + PaddingRight = UDim.new(0, Constants.GAME_GRID_PADDING), + PaddingTop = UDim.new(0, Constants.GAME_GRID_PADDING), + PaddingBottom = UDim.new(0, Constants.GAME_GRID_PADDING), + }), + SearchResultHeader = Roact.createElement(FitChildren.FitFrame, { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 0), + LayoutOrder = 1, + fitAxis = FitChildren.FitAxis.Height, + }, { + Layout = hasSuggestion and Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, HEADER_INNER_PADDING), + }), + ShowingResultsFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, SEARCH_RESULT_TEXT_SIZE), + LayoutOrder = 1, + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, TITLE_KEYWORD_PADDING), + }), + ShowingResultsText = Roact.createElement(LocalizedFitTextLabel, { + Text = "Feature.GamePage.LabelShowingResultsFor", + LayoutOrder = 1, + Size = UDim2.new(0, 0, 1, 0), + BackgroundTransparency = 1, + TextSize = SEARCH_RESULT_TEXT_SIZE, + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSansLight, + TextWrapped = true, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + fitAxis = FitChildren.FitAxis.Width, + }), + Keyword = Roact.createElement(FitTextLabel, { + Text = displayedSearchKeyword, + LayoutOrder = 2, + Size = UDim2.new(0, 0, 1, 0), + BackgroundTransparency = 1, + TextSize = SEARCH_RESULT_TEXT_SIZE, + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSansBold, + TextWrapped = true, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + fitAxis = FitChildren.FitAxis.Width, + }), + }), + SuggestionFrame = hasSuggestion and Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, SEARCH_RESULT_TEXT_SIZE), + LayoutOrder = 2, + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, TITLE_KEYWORD_PADDING), + }), + SuggestionTitleText = Roact.createElement(LocalizedFitTextLabel, { + Text = suggestionTitleText, + LayoutOrder = 1, + Size = UDim2.new(0, 0, 1, 0), + BackgroundTransparency = 1, + TextSize = SEARCH_RESULT_TEXT_SIZE, + TextColor3 = Constants.Color.GRAY1, + Font = Enum.Font.SourceSansLight, + TextWrapped = true, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + fitAxis = FitChildren.FitAxis.Width, + }), + SuggestedKeyword = Roact.createElement(FitTextButton, { + Text = displayedSuggestedKeyword, + LayoutOrder = 2, + Size = UDim2.new(0, 0, 1, 0), + BackgroundTransparency = 1, + TextSize = SEARCH_RESULT_TEXT_SIZE, + TextColor3 = Constants.Color.BLUE_PRIMARY, + Font = Enum.Font.SourceSansBold, + TextWrapped = true, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + fitAxis = FitChildren.FitAxis.Width, + [Roact.Event.Activated] = self.onKeywordButtonActivated, + }), + }), + }), + GameGrid = Roact.createElement(GameGrid, { + LayoutOrder = 2, + entries = entries, + reportGameDetailOpened = function(index) + local entry = entries[index] + local placeId = entry.placeId + local isAd = entry.isSponsored + local sortName = self:getSearchedKeyword() + local itemsInSort = #entries + analytics.reportOpenGameDetail(placeId, sortName, index, itemsInSort, isAd) + end, + windowSize = Vector2.new(screenSize.X - 2 * Constants.GAME_GRID_PADDING, screenSize.Y), + }), + }) + end +end + +function GamesSearch:willUnmount() + local searchUuid = self.props.searchUuid + local dispatchRemoveSearch = self.props.dispatchRemoveSearch + + dispatchRemoveSearch(searchUuid) +end + +GamesSearch = RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + searchInGames = state.Search.SearchesInGames[props.searchUuid], + searchInGamesStatus = state.RequestsStatus.SearchesInGamesStatus[props.searchUuid], + screenSize = state.ScreenSize, + } + end, + function(dispatch) + return { + dispatchSearch = function(networking, searchKeyword, searchUuid, isKeywordSuggestionEnabled) + return dispatch(ApiFetchSearchInGames(networking, { + searchKeyword = searchKeyword, + searchUuid = searchUuid, + isAppend = false, + }, { + isKeywordSuggestionEnabled = isKeywordSuggestionEnabled, + })) + end, + dispatchLoadMore = function(networking, searchKeyword, searchUuid, startRows, maxRows, isKeywordSuggestionEnabled) + return dispatch(ApiFetchSearchInGames(networking, { + searchKeyword = searchKeyword, + searchUuid = searchUuid, + isAppend = true, + }, { + startRows = startRows, + maxRows = maxRows, + isKeywordSuggestionEnabled = isKeywordSuggestionEnabled, + })) + end, + dispatchRemoveSearch = function(searchUuid) + dispatch(RemoveSearchInGames(searchUuid)) + dispatch(SetSearchInGamesStatus(searchUuid, SearchRetrievalStatus.Removed)) + end, + navigateSideways = function(searchUuid) + dispatch(NavigateSideways({ name = AppPage.SearchPage, detail = searchUuid })) + end, + } + end +)(GamesSearch) + +return RoactServices.connect({ + networking = RoactNetworking, + analytics = RoactAnalyticsSearchPage, +})(GamesSearch) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Search/GamesSearch.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Search/GamesSearch.spec.lua new file mode 100644 index 0000000..9714c94 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Search/GamesSearch.spec.lua @@ -0,0 +1,82 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local Rodux = require(Modules.Common.Rodux) + local SearchRetrievalStatus = require(Modules.LuaApp.Enum.SearchRetrievalStatus) + local AppReducer = require(Modules.LuaApp.AppReducer) + local GamesSearch = require(Modules.LuaApp.Components.Search.GamesSearch) + local SearchInGames = require(Modules.LuaApp.Models.SearchInGames) + local GameSortEntry = require(Modules.LuaApp.Models.GameSortEntry) + local Game = require(Modules.LuaApp.Models.Game) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors when there is search data", function() + local searchKeyword = "Meep" + local searchUuid = 1 + local searchInGames = SearchInGames.mock() + local entry = GameSortEntry.mock() + local game = Game.mock() + local entries = { entry } + + searchInGames.keyword = searchKeyword + searchInGames.entries = entries + + local store = Rodux.Store.new(AppReducer, { + SearchesInGames = { [searchUuid] = searchInGames }, + Games = { [entry.universeId] = game }, + RequestsStatus = { SearchesInGamesStatus = { [searchUuid] = SearchRetrievalStatus.Done } } + }) + + local element = mockServices({ + gamesSearch = Roact.createElement(GamesSearch, { + searchUuid = searchUuid, + }) + }, { + includeStoreProvider = true, + store = store, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when search is fetching", function() + local searchUuid = 1 + + local store = Rodux.Store.new(AppReducer, { + RequestsStatus = { SearchesInGamesStatus = { [searchUuid] = SearchRetrievalStatus.Fetching } }, + }) + + local element = mockServices({ + gamesSearch = Roact.createElement(GamesSearch, { + searchUuid = searchUuid, + }) + }, { + includeStoreProvider = true, + store = store, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when search failed", function() + local searchUuid = 1 + + local store = Rodux.Store.new(AppReducer, { + RequestsStatus = { SearchesInGamesStatus = { [searchUuid] = SearchRetrievalStatus.Failed } }, + }) + + local element = mockServices({ + gamesSearch = Roact.createElement(GamesSearch, { + searchUuid = searchUuid, + }) + }, { + includeStoreProvider = true, + store = store, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Search/SearchPage.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Search/SearchPage.lua new file mode 100644 index 0000000..dfc4e62 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Search/SearchPage.lua @@ -0,0 +1,61 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) + +local Constants = require(Modules.LuaApp.Constants) + +local TopBar = require(Modules.LuaApp.Components.TopBar) +local GamesSearch = require(Modules.LuaApp.Components.Search.GamesSearch) + +local ComponentMap = { + [Constants.SearchTypes.Games] = GamesSearch, + -- [Constants.SearchTypes.Groups] = GroupsSearch, + -- [Constants.SearchTypes.Players] = PlayersSearch, + -- [Constants.SearchTypes.Catalog] = CatalogSearch, + -- [Constants.SearchTypes.Library] = LibrarySearch, +} + +local SearchPage = Roact.PureComponent:extend("SearchPage") + +SearchPage.defaultProps = { + searchType = Constants.SearchTypes.Games, +} + +function SearchPage:render() + local topBarHeight = self.props.topBarHeight + local searchUuid = self.props.searchUuid + local searchType = self.props.searchType + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BorderSizePixel = 0, + },{ + TopBar = Roact.createElement(TopBar, { + ZIndex = 2, + showBackButton = true, + showBuyRobux = true, + showNotifications = true, + showSearch = true, + }), + SearchPage = Roact.createElement("Frame", { + BackgroundColor3 = Constants.Color.GRAY4, + Position = UDim2.new(0, 0, 0, topBarHeight), + Size = UDim2.new(1, 0, 1, -topBarHeight), + }, { + Roact.createElement(ComponentMap[searchType], { + searchUuid = searchUuid, + }) + }) + }) +end + +SearchPage = RoactRodux.UNSTABLE_connect2( + function(state, props) + return { + topBarHeight = state.TopBar.topBarHeight, + } + end +)(SearchPage) + +return SearchPage \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Search/SearchPage.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Search/SearchPage.spec.lua new file mode 100644 index 0000000..e9da785 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/Search/SearchPage.spec.lua @@ -0,0 +1,25 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local Rodux = require(Modules.Common.Rodux) + local AppReducer = require(Modules.LuaApp.AppReducer) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + local SearchPage = require(Modules.LuaApp.Components.Search.SearchPage) + it("should create and destroy without errors", function() + local store = Rodux.Store.new(AppReducer, { + SearchesInGames = {}, + }) + + local element = mockServices({ + searchPage = Roact.createElement(SearchPage, { + searchUuid = 1, + }) + }, { + includeStoreProvider = true, + store = store, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SearchBar.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SearchBar.lua new file mode 100644 index 0000000..84d6665 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SearchBar.lua @@ -0,0 +1,164 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Roact = require(Modules.Common.Roact) +local Constants = require(Modules.LuaApp.Constants) +local LocalizedTextBox = require(Modules.LuaApp.Components.LocalizedTextBox) +local LocalizedTextButton = require(Modules.LuaApp.Components.LocalizedTextButton) + +local SEARCH_BAR_HEIGHT = 28 +local SEARCH_BAR_PADDING = 12 +local SEARCH_BAR_TEXT_SIZE = 20 +local SEARCH_BAR_ICON_PADDING = 9 +local SEARCH_BAR_TEXT_PADDING_WITH_ICON = 36 +local SEARCH_BAR_TEXT_PADDING_WITHOUT_ICON = 12 +local CLEAR_IMAGE_SIZE = 16 +local CLEAR_IMAGE_PADDING = 6 +local CLEAR_BUTTON_SIZE = CLEAR_IMAGE_PADDING*2 + CLEAR_IMAGE_SIZE +local CANCEL_BUTTON_WIDTH = 88 +local CANCEL_TEXT_SIZE = 23 +local SEARCH_BAR_FONT = Enum.Font.SourceSans +local SEARCH_FRAME_IMAGE = "rbxasset://textures/ui/LuaApp/9-slice/gr-search.png" +local SEARCH_BAR_ICON = "rbxasset://textures/ui/LuaApp/icons/ic-search.png" +local CLEAR_BUTTON_IMAGE = "rbxasset://textures/ui/LuaApp/icons/ic-clear.png" + +local SearchBar = Roact.PureComponent:extend("SearchBar") + +SearchBar.defaultProps = { + Size = UDim2.new(1, 0, 1, 0), +} + +function SearchBar:init() + self.state = { + clearButtonVisible = false + } + self.searchBoxRef = Roact.createRef() + self.searchBoxTextChangedConn = nil + + self.onFocused = function() + if self.props.onFocused then + self.props.onFocused() + end + if self.searchBoxRef.current and not self.searchBoxTextChangedConn then + self.searchBoxTextChangedConn = self.searchBoxRef.current:GetPropertyChangedSignal("Text"):Connect(function() + if self.searchBoxRef.current then + local clearButtonVisible = self.searchBoxRef.current.Text ~= "" + if clearButtonVisible ~= self.state.clearButtonVisible then + self:setState({ + clearButtonVisible = clearButtonVisible + }) + end + end + end) + end + end + + self.onFocusLost = function(rbx, enterPressed) + if enterPressed then + self.props.confirmSearch(rbx.Text) + end + end + + self.onCancelButtonActivated = self.props.cancelSearch + + self.onClearText = function() + if self.searchBoxRef.current then + self.searchBoxRef.current.Text = "" + self.searchBoxRef.current:captureFocus() + end + end +end + +function SearchBar:didMount() + if self.props.isPhone and self.searchBoxRef.current then + self.searchBoxRef.current:captureFocus() + end +end + +function SearchBar:render() + local size = self.props.Size + local isPhone = self.props.isPhone + local clearButtonVisible = self.state.clearButtonVisible + local searchTextOffset = isPhone and SEARCH_BAR_TEXT_PADDING_WITHOUT_ICON or SEARCH_BAR_TEXT_PADDING_WITH_ICON + local searchBoxMargin = clearButtonVisible and searchTextOffset + CLEAR_BUTTON_SIZE or searchTextOffset + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + BorderSizePixel = 0, + },{ + Layout = isPhone and Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + SearchBoxBackground = Roact.createElement("ImageLabel",{ + AnchorPoint = Vector2.new(0, 0.5), + Size = UDim2.new(1, isPhone and -SEARCH_BAR_PADDING - CANCEL_BUTTON_WIDTH or 0, 0, SEARCH_BAR_HEIGHT), + Position = UDim2.new(0, 0, 0.5, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = SEARCH_FRAME_IMAGE, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(14, 14, 14, 14), + LayoutOrder = 1, + },{ + SearchIcon = not isPhone and Roact.createElement("ImageLabel",{ + Size = UDim2.new(0, SEARCH_BAR_TEXT_SIZE, 0, SEARCH_BAR_TEXT_SIZE), + Position = UDim2.new(0, SEARCH_BAR_ICON_PADDING, 0.5, 0), + Image = SEARCH_BAR_ICON, + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(0, 0.5), + ImageColor3 = Constants.Color.GRAY2, + }), + SearchBox = Roact.createElement(LocalizedTextBox, { + Size = UDim2.new(1, -searchBoxMargin, 1, 0), + Position = UDim2.new(0, searchTextOffset, 0.5, 0), + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Text = "", + TextWrapped = true, + TextXAlignment = Enum.TextXAlignment.Left, + TextSize = SEARCH_BAR_TEXT_SIZE, + Font = SEARCH_BAR_FONT, + PlaceholderText = "Search.GlobalSearch.Example.SearchGames", + PlaceholderColor3 = Constants.Color.GRAY2, + OverlayNativeInput = true, + ClearTextOnFocus = false, + LayoutOrder = 1, + [Roact.Ref] = self.searchBoxRef, + [Roact.Event.FocusLost] = self.onFocusLost, + [Roact.Event.Focused] = self.onFocused, + }), + ClearButton = Roact.createElement("ImageButton", { + AnchorPoint = Vector2.new(1, 0.5), + Size = UDim2.new(0, CLEAR_BUTTON_SIZE, 1, 0), + Position = UDim2.new(1, 0, 0.5, 0), + BackgroundTransparency = 1, + Visible = clearButtonVisible, + [Roact.Event.Activated] = self.onClearText, + }, { + ClearImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(1, 0.5), + Size = UDim2.new(0, CLEAR_IMAGE_SIZE, 0, CLEAR_IMAGE_SIZE), + Position = UDim2.new(1, -CLEAR_IMAGE_PADDING, 0.5, 0), + Image = CLEAR_BUTTON_IMAGE, + BackgroundTransparency = 1, + }), + }), + }), + CancelButton = isPhone and Roact.createElement(LocalizedTextButton, { + Size = UDim2.new(0, CANCEL_BUTTON_WIDTH, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = SEARCH_BAR_FONT, + Text = "Feature.GamePage.LabelCancelField", + TextSize = CANCEL_TEXT_SIZE, + TextColor3 = Constants.Color.WHITE, + LayoutOrder = 2, + [Roact.Event.Activated] = self.onCancelButtonActivated, + }), + }) +end + +return SearchBar \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SearchBar.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SearchBar.spec.lua new file mode 100644 index 0000000..8529465 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SearchBar.spec.lua @@ -0,0 +1,18 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local SearchBar = require(Modules.LuaApp.Components.SearchBar) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local element = mockServices({ + SearchBar = Roact.createElement(SearchBar, { + isPhone = true, + confirmSearch = function() end, + cancelSearch = function() end, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SectionHeader.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SectionHeader.lua new file mode 100644 index 0000000..d11039c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SectionHeader.lua @@ -0,0 +1,41 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local Constants = require(Modules.LuaApp.Constants) +local FitChildren = require(Modules.LuaApp.FitChildren) +local FitTextLabel = require(Modules.LuaApp.Components.FitTextLabel) + +local SECTION_HEADER_HEIGHT = Constants.SECTION_HEADER_HEIGHT +local TEXT_SIZE = SECTION_HEADER_HEIGHT +local TEXT_FONT = Enum.Font.SourceSansLight + +local SectionHeader = Roact.PureComponent:extend("SectionHeader") + +SectionHeader.defaultProps = { + Size = UDim2.new(1, 0, 0, SECTION_HEADER_HEIGHT), +} + +function SectionHeader:render() + local text = self.props.text + local layoutOrder = self.props.LayoutOrder + local size = self.props.Size + local position = self.props.Position + + return Roact.createElement(FitTextLabel, { + LayoutOrder = layoutOrder, + Size = size, + Position = position, + BackgroundTransparency = 1, + TextSize = TEXT_SIZE, + TextColor3 = Constants.Color.GRAY1, + Font = TEXT_FONT, + Text = text, + TextWrapped = true, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + fitAxis = FitChildren.FitAxis.Height, + }) +end + +return SectionHeader \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SectionHeader.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SectionHeader.spec.lua new file mode 100644 index 0000000..e30e380 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SectionHeader.spec.lua @@ -0,0 +1,14 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local SectionHeader = require(Modules.LuaApp.Components.SectionHeader) + + it("should create and destroy without errors", function() + local element = Roact.createElement(SectionHeader, { + text = "Best Section Ever!", + width = 100, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SectionHeaderWithSeeAll.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SectionHeaderWithSeeAll.lua new file mode 100644 index 0000000..21b7861 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SectionHeaderWithSeeAll.lua @@ -0,0 +1,102 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local Constants = require(Modules.LuaApp.Constants) +local FitChildren = require(Modules.LuaApp.FitChildren) +local SectionHeader = require(Modules.LuaApp.Components.SectionHeader) +local LocalizedTextLabel = require(Modules.LuaApp.Components.LocalizedTextLabel) + +local SectionHeaderWithSeeAll = Roact.PureComponent:extend("SectionHeaderWithSeeAll") + +local TEXT_MARGIN = 7 + +local BUTTON_WIDTH = 90 +local BUTTON_HEIGHT = 24 +local BUTTON_TEXT_SIZE = 18 +local BUTTON_FONT = Enum.Font.SourceSans +local TOTAL_HEIGHT = 33 + +local SEE_ALL_BUTTON_ASSET_DEFAULT = "rbxasset://textures/ui/LuaApp/9-slice/gr-btn-blue-3px.png" +local SEE_ALL_BUTTON_ASSET_PRESSED = "rbxasset://textures/ui/LuaApp/9-slice/gr-btn-blue-3px-pressed.png" + +function SectionHeaderWithSeeAll:init() + self.state = { + isSeeAllPressed = false, + } + self.onSelected = function() + self.props.onSelected(self.props.value) + end + self.onBegan = function() + self:setState({ + isSeeAllPressed = true + }) + end + self.onEnded = function() + self:setState({ + isSeeAllPressed = false + }) + end +end + +function SectionHeaderWithSeeAll:render() + local text = self.props.text + local onSelected = self.props.onSelected + local layoutOrder = self.props.LayoutOrder + local isSeeAllPressed = self.state.isSeeAllPressed + + local seeAllButtonAsset + if isSeeAllPressed then + seeAllButtonAsset = SEE_ALL_BUTTON_ASSET_PRESSED + else + seeAllButtonAsset = SEE_ALL_BUTTON_ASSET_DEFAULT + end + + return Roact.createElement(FitChildren.FitFrame, { + Size = UDim2.new(1, 0, 0, TOTAL_HEIGHT), + fitAxis = FitChildren.FitAxis.Height, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Title = Roact.createElement(SectionHeader, { + Size = UDim2.new(1, -BUTTON_WIDTH, 0, 0), + text = text, + }), + Spacer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, TEXT_MARGIN), + LayoutOrder = 2, + }, { + Button = Roact.createElement("ImageButton", { + Size = UDim2.new(0, BUTTON_WIDTH, 0, BUTTON_HEIGHT), + Position = UDim2.new(1, -BUTTON_WIDTH, 0, -BUTTON_HEIGHT), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(3,3,4,4), + Image = seeAllButtonAsset, + AutoButtonColor = true, + [Roact.Event.Activated] = onSelected and self.onSelected or nil, + [Roact.Event.InputBegan] = self.onBegan, + [Roact.Event.InputEnded] = self.onEnded, + }, { + Text = Roact.createElement(LocalizedTextLabel, { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Text = "Feature.GamePage.ActionSeeAll", + TextSize = BUTTON_TEXT_SIZE, + Font = BUTTON_FONT, + TextColor3 = Constants.Color.WHITE, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + }), + }), + }) + }) +end + +return SectionHeaderWithSeeAll \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SectionHeaderWithSeeAll.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SectionHeaderWithSeeAll.spec.lua new file mode 100644 index 0000000..f6ce5a3 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/SectionHeaderWithSeeAll.spec.lua @@ -0,0 +1,21 @@ +return function() + local SectionHeaderWithSeeAll = require(script.parent.SectionHeaderWithSeeAll) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local element = mockServices({ + header = Roact.createElement(SectionHeaderWithSeeAll, { + text = "Best Section Ever!", + width = 100, + + onActivated = function() end + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TabbedFrame.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TabbedFrame.lua new file mode 100644 index 0000000..334f26c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TabbedFrame.lua @@ -0,0 +1,77 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) + +local FitChildren = require(Modules.LuaApp.FitChildren) +local Constants = require(Modules.LuaApp.Constants) +local LocalizedTextButton = require(Modules.LuaApp.Components.LocalizedTextButton) + +local TabbedFrame = Roact.Component:extend("TabbedFrame") + +local NO_TABS = "Tabbed frame has no tabs. If this functionality is desired, use a Frame." +local NO_CONTENT = "Content function didn't return a table. If no content is desired, return an empty table." + +function TabbedFrame:render() + local tabs = self.props.tabs + local props = self.props.props or {} + local selectedTabIndex = self.state.selectedTabIndex or 1 + + assert(#tabs > 0, NO_TABS) + props[Roact.Children] = tabs[selectedTabIndex].content() + assert(type(props[Roact.Children]) == "table", NO_CONTENT) + + local tabButtonSize = UDim2.new(1/#tabs, 0, 1, 0) + local tabBarChildren = { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + }), + } + + for tabIndex, tab in ipairs(tabs) do + local component, name + if type(tab.label) == "table" then + component = LocalizedTextButton + name = tab.label[1] + else + component = "TextButton" + name = tab.label + end + + local selectedLine + if selectedTabIndex == tabIndex then + selectedLine = Roact.createElement("Frame", { + BackgroundColor3 = Constants.Color.BLUE_PRIMARY, + Size = UDim2.new(1, 0, 0, 2), + BorderSizePixel = 0, + Position = UDim2.new(0, 0, 1, -2), + }) + end + + tabBarChildren[name] = Roact.createElement(component, { + Text = tab.label, + BackgroundColor3 = Constants.Color.WHITE, + Size = tabButtonSize, + BorderSizePixel = 0, + LayoutOrder = tabIndex, + [Roact.Event.Activated] = function () + self:setState({ + selectedTabIndex = tabIndex + }) + end, + }, { + selectedLine + }) + end + + props[Roact.Children].TabBar = Roact.createElement("Frame", { + LayoutOrder = 1, + Size = UDim2.new(1, 0, 0, 30), + BackgroundColor3 = Constants.Color.WHITE, + BorderSizePixel = 0, + }, tabBarChildren) + + return Roact.createElement(FitChildren.FitFrame, props) +end + +return TabbedFrame \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TabbedFrame.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TabbedFrame.spec.lua new file mode 100644 index 0000000..349c63b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TabbedFrame.spec.lua @@ -0,0 +1,123 @@ +return function() + local TabbedFrame = require(script.Parent.TabbedFrame) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors if it has tabs", function() + local element = Roact.createElement(TabbedFrame, { + tabs = { + { + label = "test", + content = function () return {} end, + } + } + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should throw when created without tabs", function() + local element = Roact.createElement(TabbedFrame) + expect(function () return Roact.mount(element) end).to.throw() + end) + + it("should display all given tab buttons", function() + local tabButtons = {"tab 1 name", "tab 2 name"} + + local element = Roact.createElement(TabbedFrame, { + tabs = { + { + label = "test1", + content = function () return {} end, + }, { + label = "test2", + content = function () return {} end, + } + } + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "TabbedFrame") + + local tabbedFrame = container:FindFirstChild("TabbedFrame") + expect(tabbedFrame).to.be.ok() + + local tabBar = tabbedFrame:FindFirstChild("TabBar") + expect(tabBar).to.be.ok() + + expect(tabBar:FindFirstChild("test1")).to.be.ok() + expect(tabBar:FindFirstChild("test2")).to.be.ok() + expect(#tabBar:GetChildren()).to.equal(#tabButtons + 1) + + Roact.unmount(instance) + end) + + it("should throw if the wrong number of tab buttons to tabs was given", function() + local element = Roact.createElement(TabbedFrame, { + tabButtons = {"tab 1 name", "tab 2 name"}, + tabContents = { + {}, + }, + }) + expect(function () return Roact.mount(element) end).to.throw() + end) + + it("should only render the contents of the selected tab", function () + local element = Roact.createElement(TabbedFrame, { + tabs = { + { + label = "test1", + content = function () return { + Decoy = Roact.createElement("Frame") + } + end, + }, { + label = "test2", + content = function () return { + Secret = Roact.createElement("Frame") + } + end, + } + }, + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "TabbedFrame") + + local decoy = container.TabbedFrame:FindFirstChild("Decoy") + local secret = container.TabbedFrame:FindFirstChild("Secret") + + expect(decoy).to.be.ok() + expect(secret).to.never.be.ok() + + Roact.unmount(instance) + end) + + it("should display localized text when provided", function () + local text = "Feature.GameDetails.Label.About" + + local element = mockServices({ + TabbedFrame = Roact.createElement(TabbedFrame, { + tabs = { + { + label = text, + content = function () + return { + Decoy = Roact.createElement("Frame") + } + end, + } + }, + }), + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "TabbedFrame") + + expect(container.TabbedFrame.TabBar[text]).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TextTable.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TextTable.lua new file mode 100644 index 0000000..411b3df --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TextTable.lua @@ -0,0 +1,67 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local Immutable = require(Modules.Common.Immutable) +local LocalizedTextLabel = require(Modules.LuaApp.Components.LocalizedTextLabel) + +local TextTable = Roact.Component:extend("TextTable") + +local function fixLayout(rbx) + if rbx then + spawn(function() + rbx:ApplyLayout() + end) + end +end + +function TextTable:render() + local size = self.props.Size + local layoutOrder = self.props.LayoutOrder + local fillDirection = self.props.FillDirection + + local majorAxisData = self.props.table + local majorAxisProps = self.props.majorAxisProps or {} + local minorAxisProps = self.props.minorAxisProps or {} + + local majorElements = { + Roact.createElement("UITableLayout", { + FillDirection = fillDirection, + MajorAxis = Enum.TableMajorAxis.RowMajor, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + FillEmptySpaceColumns = true, + FillEmptySpaceRows = true, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim2.new(0, 0, 0, 0), + [Roact.Ref] = fixLayout, + }), + } + + for i, majorAxis in ipairs(majorAxisData) do + local minorElements = {} + + for j, text in ipairs(majorAxis) do + local component = type(text) == "table" and LocalizedTextLabel or "TextLabel" + local props = Immutable.JoinDictionaries({ + Text = text, + BackgroundTransparency = 1, + LayoutOrder = j, + }, majorAxisProps[i] or {}, minorAxisProps[j] or {}) + + minorElements["MinorElement " .. j] = Roact.createElement(component, props) + end + + majorElements["MajorElement " .. i] = Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = i, + }, minorElements) + end + + return Roact.createElement("Frame", { + LayoutOrder = layoutOrder, + Size = size, + BackgroundTransparency = 1, + }, majorElements) +end + +return TextTable \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TextTable.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TextTable.spec.lua new file mode 100644 index 0000000..a68a390 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TextTable.spec.lua @@ -0,0 +1,46 @@ +return function() + local TextTable = require(script.parent.TextTable) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors when empty", function() + local root = mockServices({ + element = Roact.createElement(TextTable, { + table = {}, + }), + }) + local instance = Roact.mount(root) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with data", function() + local root = mockServices({ + element = Roact.createElement(TextTable, { + table = { + { "this", "is" }, + { "a", "test" }, + }, + }), + }) + local instance = Roact.mount(root) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with data and props", function() + local root = mockServices({ + element = Roact.createElement(TextTable, { + table = { + { "this", "is" }, + { "a", "test", "hehe" }, + }, + majorProps = { + { BackgroundTransparency = 0 }, + }, + }), + }) + local instance = Roact.mount(root) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TokenRefreshComponent.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TokenRefreshComponent.lua new file mode 100644 index 0000000..bb9dbcd --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TokenRefreshComponent.lua @@ -0,0 +1,63 @@ +--[[ + A component refreshes sort tokens so that they are up-to-date. + Props: + - sortToRefresh : target sort that needs token refresh +]] +local RunService = game:GetService("RunService") +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) +local ApiFetchSortTokens = require(Modules.LuaApp.Thunks.ApiFetchSortTokens) +local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) +local ExternalEventConnection = require(Modules.Common.RoactUtilities.ExternalEventConnection) +local RoactNetworking = require(Modules.LuaApp.Services.RoactNetworking) +local RoactServices = require(Modules.LuaApp.RoactServices) + +local TokenRefreshComponent = Roact.PureComponent:extend("TokenRefreshComponent") + +function TokenRefreshComponent:init() + self.steppedCallback = function() + local sortToRefresh = self.props.sortToRefresh + local nextTokenRefreshTime = self.props.nextTokenRefreshTime[sortToRefresh] + local fetchingStatus = self.props.GameSortTokenFetchingStatus[sortToRefresh] + + --[[ if state is RetrievalStatus.NotStarted (uninitialized)/ + RetrievalStatus.Fetching (is fetching data)/ + RetrievalStatus.Failed (failed), + stop the autorefresh until they get correctly handled. ]] + if fetchingStatus ~= RetrievalStatus.Done then + return + end + local currentTime = tick() + if currentTime > nextTokenRefreshTime then + self.props.refresh(sortToRefresh):catch(function() + -- Failure handler only + end) + end + end +end + +function TokenRefreshComponent:render() + return Roact.createElement(ExternalEventConnection, { + event = RunService.stepped, + callback = self.steppedCallback, + }) +end + +TokenRefreshComponent = RoactRodux.connect(function(store, props) + local state = store:getState() + + return { + nextTokenRefreshTime = state.NextTokenRefreshTime, + GameSortTokenFetchingStatus = state.RequestsStatus.GameSortTokenFetchingStatus, + refresh = function(sortToRefresh) + return store:dispatch(ApiFetchSortTokens(props.networking, sortToRefresh)) + end, + } +end)(TokenRefreshComponent) + +return RoactServices.connect({ + networking = RoactNetworking, +})(TokenRefreshComponent) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TokenRefreshComponent.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TokenRefreshComponent.spec.lua new file mode 100644 index 0000000..9ab5876 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TokenRefreshComponent.spec.lua @@ -0,0 +1,24 @@ +return function() + local TokenRefreshComponent = require(script.Parent.TokenRefreshComponent) + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) + + local Roact = require(Modules.Common.Roact) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + it("should create and destroy without errors", function() + local element = mockServices({ + TokenRefreshComponent = Roact.createElement(TokenRefreshComponent, { + sortToRefresh = "Games", + refresh = function() end, + nextTokenRefreshTime = {["Games"] = 1}, + GameSortTokenFetchingStatus = {["Games"] = RetrievalStatus.NotStarted}, + }) + }, { + includeStoreProvider = true, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TopBar.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TopBar.lua new file mode 100644 index 0000000..f1a6fd5 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TopBar.lua @@ -0,0 +1,426 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local UserInputService = game:GetService("UserInputService") + +local Roact = require(Modules.Common.Roact) +local RoactRodux = require(Modules.Common.RoactRodux) +local ExternalEventConnection = require(Modules.Common.RoactUtilities.ExternalEventConnection) +local RoactAnalyticsTopBar = require(Modules.LuaApp.Services.RoactAnalyticsTopBar) +local RoactServices = require(Modules.LuaApp.RoactServices) +local AppGuiService = require(Modules.LuaApp.Services.AppGuiService) +local RoactNetworking = require(Modules.LuaApp.Services.RoactNetworking) + +local AppPage = require(Modules.LuaApp.AppPage) +local NavigateUp = require(Modules.LuaApp.Thunks.NavigateUp) +local NavigateDown = require(Modules.LuaApp.Thunks.NavigateDown) +local NavigateSideways = require(Modules.LuaApp.Thunks.NavigateSideways) +local NavigateBack = require(Modules.LuaApp.Thunks.NavigateBack) +local ApiFetchSearchInGames = require(Modules.LuaApp.Thunks.ApiFetchSearchInGames) + +local SetTopBarHeight = require(Modules.LuaApp.Actions.SetTopBarHeight) + +local FormFactor = require(Modules.LuaApp.Enum.FormFactor) +local Constants = require(Modules.LuaApp.Constants) +local AppPageLocalizationKeys = require(Modules.LuaApp.AppPageLocalizationKeys) +local NotificationType = require(Modules.LuaApp.Enum.NotificationType) +local SearchUuid = require(Modules.LuaApp.SearchUuid) +local LocalizedTextLabel = require(Modules.LuaApp.Components.LocalizedTextLabel) +local SearchBar = require(Modules.LuaApp.Components.SearchBar) +local NotificationBadge = require(Modules.LuaApp.Components.NotificationBadge) + +local NAV_BAR_SIZE = 44 + +local ICON_IMAGE_SIZE = 24 +local ICON_BUTTON_SIZE = 44 +local BACK_BUTTON_SIZE = 72 +local BACK_BUTTON_IMAGE = "rbxasset://textures/ui/LuaApp/icons/ic-back.png" +local SEARCH_ICON_IMAGE = "rbxasset://textures/ui/LuaChat/icons/ic-search.png" +local ROBUX_ICON_IMAGE = "rbxasset://textures/ui/LuaApp/icons/ic-ROBUX.png" +local NOTIFICATION_ICON_IMAGE = "rbxasset://textures/ui/LuaApp/icons/ic-notification.png" + +local SEARCH_BAR_SIZE = 260 +local SEARCH_BAR_PADDING = 6 + +local DeviceSpecificTopBarIconSpec = { + --[[ + [Form Factor Type] = { + MarginRight = Right margin for the list layout of the icons + Padding = Padding between icons in the list layout + IconButtonSize = Size of the icon button(touchable area) + BackImageOffset = Space between back button edge to back button image + }, + --]] + [FormFactor.PHONE] = { + MarginRight = 13, + Padding = 2, + IconButtonSize = 34, + BackImageOffset = 16, + }, + [FormFactor.TABLET] = { + MarginRight = 5, + Padding = 3, + IconButtonSize = 44, + BackImageOffset = 20, + }, +} + +local TOP_BAR_COLOR = Constants.Color.BLUE_PRESSED +local TOP_SYSTEM_BACKGROUND_COLOR = Constants.Color.BLUE_PRESSED + +local DEFAULT_TEXT_COLOR = Constants.Color.WHITE + +local DEFAULT_TITLE_FONT = Enum.Font.SourceSansSemibold +local DEFAULT_TITLE_FONT_SIZE = 23 + +local DEFAULT_ZINDEX = 2 + +local function getStatusBarHeight() + if not _G.__TESTEZ_RUNNING_TEST__ then + return UserInputService.StatusBarSize.Y + else + return 0 + end +end + +local function TouchFriendlyImageIcon(props) + local image = props.Image + local anchorPoint = props.AnchorPoint or Vector2.new(0, 0) + local position = props.Position or UDim2.new(0, 0, 0, 0) + local layoutOrder = props.LayoutOrder + local onActivated = props.onActivated + local hasNotificationBadge = props.hasNotificationBadge + local notificationCount = props.notificationCount + + local iconImageAnchorPoint = props.iconImageAnchorPoint or Vector2.new(0.5, 0.5) + local iconImagePosition = props.iconImagePosition or UDim2.new(0.5, 0, 0.5, 0) + local iconImageSize = props.iconImageSize + local iconButtonSize = props.iconButtonSize + + return Roact.createElement("ImageButton", { + AnchorPoint = anchorPoint, + Position = position, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, iconButtonSize, 1, 0), + LayoutOrder = layoutOrder, + [Roact.Event.Activated] = onActivated, + }, { + IconImage = Roact.createElement("ImageLabel", { + AnchorPoint = iconImageAnchorPoint, + Position = iconImagePosition, + Size = UDim2.new(0, iconImageSize, 0, iconImageSize), + Image = image, + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + NotificationBadge = hasNotificationBadge and Roact.createElement(NotificationBadge, { + notificationCount = notificationCount, + }), + }), + }) +end + +local TopBar = Roact.PureComponent:extend("TopBar") + +TopBar.defaultProps = { + textColor = DEFAULT_TEXT_COLOR, + titleFont = DEFAULT_TITLE_FONT, + titleSize = DEFAULT_TITLE_FONT_SIZE, + showBackButton = false, + showBuyRobux = false, + showNotifications = false, + showSearch = false, + ZIndex = DEFAULT_ZINDEX, +} + +function TopBar:updateTopBarHeight() + local newTopBarHeight = getStatusBarHeight() + NAV_BAR_SIZE + if newTopBarHeight ~= self.props.topBarHeight + and self.props.setTopBarHeight then + self.props.setTopBarHeight(newTopBarHeight) + end +end + +function TopBar:init() + self.state = { + isSearching = false, + } + + self.onSearchButtonActivated = function() + self:setState({ + isSearching = true, + }) + end + + self.onExitSearch = function() + self:setState({ + isSearching = false, + }) + end + + self.cancelSearchCallback = function() + self.props.analytics.reportSearchCanceled("games") + self.onExitSearch() + end + + self:updateTopBarHeight() + + self.updateTopBarHeightCallback = function() + self:updateTopBarHeight() + end + + self.showBuyRobuxCallback = function() + local currentRoute = self.props.currentRoute + local currentPage = currentRoute[1].name + + self.props.guiService:BroadcastNotification("", NotificationType.PURCHASE_ROBUX) + self.props.analytics.reportRobuxButtonClick(currentPage) + end + + self.showNotificationsCallback = function() + self.props.guiService:BroadcastNotification("", NotificationType.VIEW_NOTIFICATIONS) + self.props.analytics.reportNSButtonTouch(tonumber(self.props.numberOfNotifications)) + end + + self.onSearchBarFocused = function() + self.props.analytics.reportSearchFocused("games") + if self.props.formFactor == FormFactor.TABLET then + self:setState({ + isSearching = true, + }) + end + end + + self.confirmSearchCallback = function(keyword) + local searchUuid = SearchUuid() + + self.props.dispatchSearch(self.props.networking, keyword, searchUuid) + self.props.analytics.reportSearched("games", keyword) + self.onExitSearch() + self.props.navigateToSearch(self.props.currentRoute, searchUuid) + end + +end + +function TopBar:render() + local topBarHeight = self.props.topBarHeight + + local formFactor = self.props.formFactor + local currentRoute = self.props.currentRoute + local platform = self.props.platform + + local textColor = self.props.textColor + local textTitleFont = self.props.titleFont + local textTitleFontSize = self.props.titleSize + + local showBackButton = self.props.showBackButton + local showBuyRobux = self.props.showBuyRobux + local showNotifications = self.props.showNotifications + local showSearch = self.props.showSearch + + local numberOfNotifications = self.props.numberOfNotifications + + local zIndex = self.props.ZIndex + + local navigateUp = self.props.navigateUp + local navigateBack = self.props.navigateBack + + local currentPage = currentRoute[1].name + + local currentTopBarIconSpec = DeviceSpecificTopBarIconSpec[formFactor] + + local iconMarginRight = currentTopBarIconSpec and currentTopBarIconSpec.MarginRight or 0 + local iconPadding = currentTopBarIconSpec and currentTopBarIconSpec.Padding or 0 + local iconButtonSize = currentTopBarIconSpec and currentTopBarIconSpec.IconButtonSize or ICON_BUTTON_SIZE + local backImageOffset = currentTopBarIconSpec and currentTopBarIconSpec.BackImageOffset or 0 + + local isPhone = formFactor == FormFactor.PHONE + + local navBarLayout = {} + + if isPhone and self.state.isSearching then + navBarLayout["SearchBar"] = Roact.createElement(SearchBar, { + cancelSearch = self.cancelSearchCallback, + confirmSearch = self.confirmSearchCallback, + onFocused = self.onSearchBarFocused, + isPhone = isPhone, + }) + else + if showBackButton then + navBarLayout["BackButton"] = Roact.createElement(TouchFriendlyImageIcon, { + iconImageAnchorPoint = Vector2.new(0, 0.5), + iconImagePosition = UDim2.new(0, backImageOffset, 0.5, 0), + iconImageSize = ICON_IMAGE_SIZE, + iconButtonSize = BACK_BUTTON_SIZE, + Image = BACK_BUTTON_IMAGE, + onActivated = (platform == Enum.Platform.IOS) and navigateBack or navigateUp, + }) + end + + navBarLayout["Title"] = Roact.createElement(LocalizedTextLabel, { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + Font = textTitleFont, + Text = { AppPageLocalizationKeys[currentPage] }, + TextColor3 = textColor, + TextSize = textTitleFontSize, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + }) + + local rightIcons = {} + rightIcons["Layout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + Padding = UDim.new(0, iconPadding), + }) + + if showSearch then + rightIcons["Search"] = isPhone and Roact.createElement(TouchFriendlyImageIcon, { + iconImageSize = ICON_IMAGE_SIZE, + iconButtonSize = iconButtonSize, + Image = SEARCH_ICON_IMAGE, + LayoutOrder = 3, + onActivated = self.onSearchButtonActivated, + }) or Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, SEARCH_BAR_SIZE + SEARCH_BAR_PADDING, 1, 0), + LayoutOrder = 3, + }, { + SearchBar = Roact.createElement(SearchBar, { + Size = UDim2.new(0, SEARCH_BAR_SIZE, 1, 0), + cancelSearch = self.cancelSearchCallback, + confirmSearch = self.confirmSearchCallback, + onFocused = self.onSearchBarFocused, + isPhone = isPhone, + }) + }) + end + + if showBuyRobux then + rightIcons["Robux"] = Roact.createElement(TouchFriendlyImageIcon, { + iconImageSize = ICON_IMAGE_SIZE, + iconButtonSize = iconButtonSize, + Image = ROBUX_ICON_IMAGE, + LayoutOrder = 4, + onActivated = self.state.isSearching and self.cancelSearchCallback or self.showBuyRobuxCallback, + }) + end + + if showNotifications then + rightIcons["Notifications"] = Roact.createElement(TouchFriendlyImageIcon, { + iconImageSize = ICON_IMAGE_SIZE, + iconButtonSize = iconButtonSize, + Image = NOTIFICATION_ICON_IMAGE, + LayoutOrder = 5, + onActivated = self.state.isSearching and self.cancelSearchCallback or self.showNotificationsCallback, + hasNotificationBadge = true, + notificationCount = numberOfNotifications, + }) + end + + navBarLayout["RightIcons"] = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(1, 0.5), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Position = UDim2.new(1, -iconMarginRight, 0.5, 0), + Size = UDim2.new(1, -iconMarginRight, 1, 0), + }, rightIcons) + end + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + ZIndex = zIndex, + }, { + TopBar = Roact.createElement("Frame", { + BackgroundColor3 = TOP_SYSTEM_BACKGROUND_COLOR, + BackgroundTransparency = 0, + BorderSizePixel = 0, + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 0, topBarHeight), + ZIndex = 2, + }, { + NavBar = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0, 1), + BackgroundColor3 = TOP_BAR_COLOR, + BackgroundTransparency = 0, + BorderSizePixel = 0, + Position = UDim2.new(0, 0, 1, 0), + Size = UDim2.new(1, 0, 0, NAV_BAR_SIZE), + }, navBarLayout), + statusBarSizeListener = Roact.createElement(ExternalEventConnection, { + event = UserInputService:GetPropertyChangedSignal("StatusBarSize"), + callback = self.updateTopBarHeightCallback, + }), + }), + DarkOverlay = Roact.createElement("TextButton", { + Size = UDim2.new(1, 0, 1, 0), + AutoButtonColor = false, + BackgroundColor3 = Constants.Color.GRAY1, + BackgroundTransparency = 0.5, + Text = "", + Visible = self.state.isSearching, + [Roact.Event.Activated] = self.cancelSearchCallback, + ZIndex = 1, + }), + }) +end + +TopBar = RoactRodux.UNSTABLE_connect2( + function(state, props) + local currentRoute = state.Navigation.history[#state.Navigation.history] + + return { + formFactor = state.FormFactor, + topBarHeight = state.TopBar.topBarHeight, + numberOfNotifications = state.NotificationBadgeCounts.TopBarNotificationIcon, + currentRoute = currentRoute, + -- Show back button only if we're not on a root page, i.e. current route longer than 1. + showBackButton = #currentRoute > 1, + platform = state.Platform, + } + end, + function(dispatch) + return { + setTopBarHeight = function(newTopBarHeight) + return dispatch(SetTopBarHeight(newTopBarHeight)) + end, + navigateUp = function() + return dispatch(NavigateUp()) + end, + navigateBack = function() + return dispatch(NavigateBack()) + end, + navigateToSearch = function(currentRoute, searchUuid) + local isOnRootPage = (#currentRoute == 1) + + if isOnRootPage then + dispatch(NavigateDown({ name = AppPage.SearchPage, detail = searchUuid })) + else + dispatch(NavigateSideways({ name = AppPage.SearchPage, detail = searchUuid })) + end + end, + dispatchSearch = function(networking, searchKeyword, searchUuid) + return dispatch(ApiFetchSearchInGames(networking, { + searchKeyword = searchKeyword, + searchUuid = searchUuid, + isAppend = false, + }, { + isKeywordSuggestionEnabled = true, + })) + end, + } + end +)(TopBar) + + +return RoactServices.connect({ + analytics = RoactAnalyticsTopBar, + guiService = AppGuiService, + networking = RoactNetworking, +})(TopBar) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TopBar.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TopBar.spec.lua new file mode 100644 index 0000000..7b978c5 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/TopBar.spec.lua @@ -0,0 +1,58 @@ +return function() + local TopBar = require(script.Parent.TopBar) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + local Rodux = require(Modules.Common.Rodux) + local AppReducer = require(Modules.LuaApp.AppReducer) + local SetTopBarHeight = require(Modules.LuaApp.Actions.SetTopBarHeight) + local mockServices = require(Modules.LuaApp.TestHelpers.mockServices) + + + local function MockStore() + return Rodux.Store.new(AppReducer) + end + + local function MockTopBarElement(store) + return mockServices({ + TopBar = Roact.createElement(TopBar, { + showBackButton = true, + showBuyRobux = true, + showNotifications = true, + showSearch = true, + textKey = "CommonUI.Features.Label.Game", + }), + }, { + includeStoreProvider = true, + store = store + }) + end + + it("should create and destroy without errors", function() + local store = MockStore() + local topBar = MockTopBarElement(store) + + local screenGui = Instance.new("ScreenGui") + local instance = Roact.mount(topBar, screenGui) + + Roact.unmount(instance) + store:destruct() + end) + + it("should update when status bar size changes", function() + local store = MockStore() + local newTopBarHeight = 100 + store:dispatch(SetTopBarHeight(newTopBarHeight)) + + local topBar = MockTopBarElement(store) + local container = Instance.new("ScreenGui") + local instance = Roact.mount(topBar, container, "TopBar") + + expect(container.TopBar.TopBar.AbsoluteSize.Y).to.equal(newTopBarHeight) + + Roact.unmount(instance) + store:destruct() + end) + +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/UIScaler.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/UIScaler.lua new file mode 100644 index 0000000..4094d28 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/UIScaler.lua @@ -0,0 +1,25 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local RoactMotion = require(Modules.LuaApp.RoactMotion) + +local UIScaler = Roact.PureComponent:extend("UIScaler") + +function UIScaler:render() + local scaleValue = self.props.scaleValue + local onRested = self.props.onRested + + return Roact.createElement(RoactMotion.SimpleMotion, { + style = { + scale = scaleValue, + }, + onRested = onRested, + render = function(values) + return Roact.createElement("UIScale", { + Scale = values.scale, + }) + end, + }) +end + +return UIScaler \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/UIScaler.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/UIScaler.spec.lua new file mode 100644 index 0000000..c1f3240 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/UIScaler.spec.lua @@ -0,0 +1,15 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local RoactMotion = require(Modules.LuaApp.RoactMotion) + local UIScaler = require(Modules.LuaApp.Components.UIScaler) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UIScaler, { + scaleValue = RoactMotion.spring(0.5), + onRested = nil, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/UserThumbnail.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/UserThumbnail.lua new file mode 100644 index 0000000..f3b57cf --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/UserThumbnail.lua @@ -0,0 +1,146 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Constants = require(Modules.LuaApp.Constants) +local Roact = require(Modules.Common.Roact) +local Text = require(Modules.Common.Text) +local User = require(Modules.LuaApp.Models.User) +local FlagSettings = require(Modules.LuaApp.FlagSettings) + +local isPeopleListV1Enabled = FlagSettings.IsPeopleListV1Enabled() +local useCppTextTruncation = FlagSettings.UseCppTextTruncation() + +local DEFAULT_THUMBNAIL_ICON = Constants.AVATAR_PLACEHOLDER_IMAGE +local OVERLAY_IMAGE_BIG = "rbxasset://textures/ui/LuaApp/graphic/gr-friend.png" +local PRESENCE_BORDER_IMAGE = "rbxasset://textures/ui/LuaApp/graphic/gr-card@2x.png" +local PRESENCE_FONT = Enum.Font.SourceSans +local THUMBNAIL_IMAGE_SIZE_ENUM = Constants.AvatarThumbnailSizes.Size100x100 +local USER_NAME_FONT = Enum.Font.SourceSansLight +local DEFAULT_PRESENCE_TEXT = "Online" + +local function getLastLocationText(lastLocation) + local locationWithoutPlayingPrefix = lastLocation and lastLocation:gsub("^Playing%s*", "") + return locationWithoutPlayingPrefix == "" and DEFAULT_PRESENCE_TEXT or locationWithoutPlayingPrefix +end + +local UserThumbnail = Roact.PureComponent:extend("UserThumbnail") + +function UserThumbnail:init() + self.resize = function() + if useCppTextTruncation then + return + end + if self.usernameTextLabel then + Text.TruncateTextLabel(self.usernameTextLabel, "...") + end + if self.presenceTextLabel then + Text.TruncateTextLabel(self.presenceTextLabel, "...") + end + end +end + +function UserThumbnail:render() + local measurements = self.props.measurements + local user = self.props.user + local highlightColor = self.props.highlightColor + local thumbnailType = self.props.thumbnailType + + local thumbnailSize = measurements.THUMBNAIL_SIZE + local dropShadowSize = measurements.DROPSHADOW_SIZE + local dropShadowMargin = measurements.PRESENCE.DROPSHADOW_MARGIN + local totalHeight = measurements.TOTAL_HEIGHT + + local usernameLineHeight = measurements.USERNAME.TEXT_LINE_HEIGHT + local usernameTopPadding = measurements.USERNAME.TEXT_TOP_PADDING + local usernameTextFontSize = measurements.USERNAME.TEXT_FONT_SIZE + + local presenceIcons = measurements.PRESENCE.ICONS + local presenceTextFontSize = measurements.PRESENCE.TEXT_FONT_SIZE + local presenceTextLineHeight = measurements.PRESENCE.TEXT_LINE_HEIGHT + local presenceTextTopPadding = measurements.PRESENCE.TEXT_TOP_PADDING + local presenceIconSize = measurements.PRESENCE.ICON_SIZE + local presenceBorderDiameter = measurements.PRESENCE.BORDER_DIAMETER + local presenceIconMargin = dropShadowMargin + measurements.PRESENCE.ICON_OFFSET + + local userLastLocation = getLastLocationText(user.lastLocation) + local isPresenceLabelVisible = isPeopleListV1Enabled and user.presence == User.PresenceType.IN_GAME + + return Roact.createElement("Frame", { + Size = UDim2.new(0, thumbnailSize, 0, totalHeight), + BackgroundTransparency = 1, + [Roact.Ref] = function(rbx) + self.mainFrame = rbx + end, + }, { + Image = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, thumbnailSize, 0, thumbnailSize), + BackgroundColor3 = Constants.Color.WHITE, + BorderSizePixel = 0, + Image = user and user.thumbnails and user.thumbnails[thumbnailType] + and user.thumbnails[thumbnailType][THUMBNAIL_IMAGE_SIZE_ENUM] or DEFAULT_THUMBNAIL_ICON, + }, { + MaskFrame = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, dropShadowSize, 0, dropShadowSize), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = OVERLAY_IMAGE_BIG, + ImageColor3 = highlightColor, + },{ + PresenceIconBorder = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, presenceBorderDiameter, 0, presenceBorderDiameter), + AnchorPoint = Vector2.new(1, 1), + Position = UDim2.new(1, -presenceIconMargin, 1, -presenceIconMargin), + BackgroundTransparency = 1, + Image = PRESENCE_BORDER_IMAGE, + Visible = user.presence ~= User.PresenceType.OFFLINE, + },{ + PresenceIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, presenceIconSize, 0, presenceIconSize), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundTransparency = 1, + Image = presenceIcons[user.presence], + }), + }), + }), + }), + Username = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, usernameLineHeight), + Position = UDim2.new(0, 0, 0, thumbnailSize + usernameTopPadding), + BackgroundTransparency = 1, + Text = user.name, + TextTruncate = Enum.TextTruncate.AtEnd, + TextSize = usernameTextFontSize, + TextColor3 = Constants.Color.GRAY1, + Font = USER_NAME_FONT, + -- Remove these functions when we take out useCppTextTruncation flag. + [Roact.Ref] = function(rbx) + self.usernameTextLabel = rbx + end, + [Roact.Change.Text] = self.resize, + }), + Presence = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, presenceTextLineHeight), + Position = UDim2.new(0, 0, 0, thumbnailSize + usernameTopPadding + usernameLineHeight + presenceTextTopPadding), + BackgroundTransparency = 1, + Text = userLastLocation, + TextTruncate = Enum.TextTruncate.AtEnd, + TextSize = presenceTextFontSize, + TextColor3 = Constants.Color.GRAY2, + Font = PRESENCE_FONT, + Visible = isPresenceLabelVisible, + -- Remove these functions when we take out useCppTextTruncation flag. + [Roact.Ref] = function(rbx) + self.presenceTextLabel = rbx + end, + [Roact.Change.Text] = self.resize, + }), + }) +end + +function UserThumbnail:didMount() + self.resize() +end + + +return UserThumbnail diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/UserThumbnail.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/UserThumbnail.spec.lua new file mode 100644 index 0000000..cbe7a79 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Components/UserThumbnail.spec.lua @@ -0,0 +1,43 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local User = require(Modules.LuaApp.Models.User) + local UserThumbnail = require(Modules.LuaApp.Components.UserThumbnail) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UserThumbnail, { + measurements = { + THUMBNAIL_SIZE = 90, + DROPSHADOW_SIZE = 98, + + USERNAME = { + TEXT_LINE_HEIGHT = 20, + TEXT_FONT_SIZE = 18, + TEXT_TOP_PADDING = 3, + }, + + PRESENCE = { + TEXT_TOP_PADDING = 3, + TEXT_LINE_HEIGHT = 20, + TEXT_FONT_SIZE = 15, + + ICONS = { + [User.PresenceType.ONLINE] = "", + [User.PresenceType.IN_GAME] = "", + [User.PresenceType.IN_STUDIO] = "", + }, + + DROPSHADOW_MARGIN = 0, + BORDER_DIAMETER = 14, + ICON_OFFSET = 5, + ICON_SIZE = 24, + }, + + PRESENCE_TEXT_HEIGHT = 0 + }, + user = User.mock() + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Config.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Config.lua new file mode 100644 index 0000000..c53728b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Config.lua @@ -0,0 +1,20 @@ +return { + General = { + --[[ + Set HttpDelay to a list with two values, like {50, 3000} to set minimum + and maximum bounds on artificial HTTP connection jitter. + + Set to false to disable this feature, which should be the default. + ]] + HttpDelay = false, + + SimulatePlatform = Enum.Platform.IOS, + + PerformanceTestingMode = Enum.VirtualInputMode.None, + PerformanceTestFilename = "GalaxyS6.json", + }, + + LuaChat = { + Debug = false, -- Do not submit with this enabled! + }, +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Config.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Config.spec.lua new file mode 100644 index 0000000..a41af36 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Config.spec.lua @@ -0,0 +1,25 @@ +return function() + local Config = require(script.Parent.Config) + + describe("General", function() + describe("HttpDelay", function() + it("should be disabled", function() + expect(Config.General.HttpDelay).to.equal(false) + end) + end) + + describe("PerformanceTestingMode", function() + it("should be disabled", function() + expect(Config.General.PerformanceTestingMode).to.equal(Enum.VirtualInputMode.None) + end) + end) + end) + + describe("LuaChat", function() + describe("Debug", function() + it("should be disabled", function() + expect(Config.LuaChat.Debug).to.equal(false) + end) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Constants.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Constants.lua new file mode 100644 index 0000000..d6c966d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Constants.lua @@ -0,0 +1,97 @@ +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules +local ThumbnailRequest = require(Modules.LuaApp.Models.ThumbnailRequest) + +local Constants = { + Color = { + GRAY1 = Color3.fromRGB(25, 25, 25), + GRAY2 = Color3.fromRGB(117, 117, 117), + GRAY3 = Color3.fromRGB(184, 184, 184), + GRAY4 = Color3.fromRGB(227, 227, 227), + GRAY5 = Color3.fromRGB(242, 242, 242), + GRAY6 = Color3.fromRGB(245, 245, 245), + GRAY_SEPARATOR = Color3.fromRGB(172, 170, 161), + WHITE = Color3.fromRGB(255, 255, 255), + BLUE_PRIMARY = Color3.fromRGB(0, 162, 255), + BLUE_HOVER = Color3.fromRGB(50, 181, 255), + BLUE_PRESSED = Color3.fromRGB(0, 116, 189), + BLUE_DISABLED = Color3.fromRGB(153, 218, 255), + GREEN_PRIMARY = Color3.fromRGB(2, 183, 87), + GREEN_HOVER = Color3.fromRGB(63, 198, 121), + GREEN_PRESSED = Color3.fromRGB(17, 130, 55), + GREEN_DISABLED = Color3.fromRGB(163, 226, 189), + RED_PRIMARY = Color3.fromRGB(226, 35, 26), + RED_NEGATIVE = Color3.fromRGB(216, 104, 104), + RED_HOVER = Color3.fromRGB(226, 118, 118), + RED_PRESSED = Color3.fromRGB(172, 30, 45), + ORANGE_FAVORITE = Color3.fromRGB(246, 183, 2), + ALPHA_SHADOW_PRIMARY = 0.3, -- Used with Gray1 + ALPHA_SHADOW_HOVER = 0.75, -- Used with Gray1 + }, + DEFAULT_GAME_FETCH_COUNT = 40, + TOP_BAR_SIZE = 64, + BOTTOM_BAR_SIZE = 49, + SECTION_HEADER_HEIGHT = 26, + GAME_CAROUSEL_PADDING = 15, + GAME_CAROUSEL_CHILD_PADDING = 12, + GAME_GRID_PADDING = 15, + GAME_GRID_CHILD_PADDING = 12, + GameSortGroups = { + Games = "Games", + HomeGames = "HomeGames", + }, + ApiUsedForSorts = { + Games = "GamesDefaultSorts", + HomeGames = "HomeSorts", + }, + SearchTypes = { + Games = "Games", + Groups = "Groups", + Players = "Players", + Catalog = "Catalog", + Library = "Library", + }, + AvatarThumbnailTypes = { + AvatarThumbnail = "AvatarThumbnail", + HeadShot = "HeadShot", + }, + AvatarThumbnailSizes = { + Size48x48 = "Size48x48", + Size100x100 = "Size100x100", + Size150x150 = "Size150x150", + }, + AVATAR_PLACEHOLDER_IMAGE = "rbxasset://textures/ui/LuaApp/graphic/ph-avatar-portrait.png", + + LEGACY_GAME_SORT_IDS = { + default = 0, + BuildersClub = 14, + Featured = 3, + FriendActivity = 17, + MyFavorite = 5, + MyRecent = 6, + Popular = 1, + PopularInCountry = 20, + PopularInVr = 19, + Purchased = 10, + Recommended = 16, + TopFavorite = 2, + TopGrossing = 8, + TopPaid = 9, + TopRated = 11, + TopRetaining = 16, + }, +} + +Constants.AvatarThumbnailRequests = { + USER_CAROUSEL = {ThumbnailRequest.fromData( + Constants.AvatarThumbnailTypes.AvatarThumbnail, Constants.AvatarThumbnailSizes.Size100x100 + )}, + HOME_HEADER_USER = {ThumbnailRequest.fromData( + Constants.AvatarThumbnailTypes.HeadShot, Constants.AvatarThumbnailSizes.Size150x150 + )}, + FRIEND_CAROUSEL = {ThumbnailRequest.fromData( + Constants.AvatarThumbnailTypes.HeadShot, Constants.AvatarThumbnailSizes.Size48x48 + )}, +} + +return Constants \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/DeviceOrientationMode.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/DeviceOrientationMode.lua new file mode 100644 index 0000000..d51ab2e --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/DeviceOrientationMode.lua @@ -0,0 +1,6 @@ +local DeviceOrientationMode = { + Portrait = "Portrait", + Landscape = "Landscape", +} + +return DeviceOrientationMode \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Enum/FormFactor.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Enum/FormFactor.lua new file mode 100644 index 0000000..cdc2409 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Enum/FormFactor.lua @@ -0,0 +1,5 @@ +return { + UNKNOWN = "UNKNOWN", + TABLET = "TABLET", + PHONE = "PHONE", +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Enum/NotificationType.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Enum/NotificationType.lua new file mode 100644 index 0000000..da87e69 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Enum/NotificationType.lua @@ -0,0 +1,29 @@ +local GuiService = game:GetService("GuiService") + +local Success, EnumValues = pcall(GuiService.GetNotificationTypeList, GuiService) + +--[[ + NOTE: This table is supposed to mirror the C++ enum values, when new types are added we will + need to add we values to this table or tests may fail or produce unexpected results. +]] +local TESTEZ_ENUM_VALUES = { + VIEW_PROFILE = 0, + REPORT_ABUSE = 1, + VIEW_GAME_DETAILS = 2, + SHOW_TAB_BAR = 3, + HIDE_TAB_BAR = 4, + UNREAD_COUNT = 5, + PRIVACY_SETTINGS = 6, + BACK_BUTTON_NOT_CONSUMED = 7, + PURCHASE_ROBUX = 8, + VIEW_NOTIFICATIONS = 9, + APP_READY = 10, + CLOSE_MODAL = 11, + VIEW_GAME_DETAILS_ANIMATED = 12, + LAUNCH_GAME = 13, + VIEW_MY_FEED = 14, + SEARCH_GAMES = 15, + GAMES_SEE_ALL = 16, +} + +return Success and EnumValues or TESTEZ_ENUM_VALUES diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Enum/PlayabilityStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Enum/PlayabilityStatus.lua new file mode 100644 index 0000000..4e77fb9 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Enum/PlayabilityStatus.lua @@ -0,0 +1,18 @@ +return { + Fetching = "Fetching", + RequestFailed = "RequestFailed", + + UnplayableOtherUnkown = "UnplayableOtherUnkown", + Playable = "Playable", + GuestProhibited = "GuestProhibited", + GameUnapproved = "GameUnapproved", + IncorrectConfiguration = "IncorrectConfiguration", + UniverseRootPlaceIsPrivate = "UniverseRootPlaceIsPrivate", + InsufficientPermissionFriendsOnly = "InsufficientPermissionFriendsOnly", + InsufficientPermissionGroupOnly = "InsufficientPermissionGroupOnly", + DeviceRestricted = "DeviceRestricted", + UnderReview = "UnderReview", + PurchaseRequired = "PurchaseRequired", + GameInsecure = "GameInsecure", + TemporarilyUnavailable = "TemporarilyUnavailable", +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Enum/RetrievalStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Enum/RetrievalStatus.lua new file mode 100644 index 0000000..0ea027a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Enum/RetrievalStatus.lua @@ -0,0 +1,21 @@ +local RetrievalStatus = {} + +local EnumValues = +{ + NotStarted = "NotStarted", + Fetching = "Fetching", + Done = "Done", + Failed = "Failed", +} + +setmetatable(RetrievalStatus, + { + __newindex = function(t, key, index) + end, + __index = function(t, index) + assert(EnumValues[index] ~= nil, ("RetrievalStatus Enum has no value: " .. tostring(index))) + return EnumValues[index] + end + }) + +return RetrievalStatus \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Enum/SearchRetrievalStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Enum/SearchRetrievalStatus.lua new file mode 100644 index 0000000..882a3c9 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Enum/SearchRetrievalStatus.lua @@ -0,0 +1,21 @@ +local SearchRetrievalStatus = {} + +local EnumValues = +{ + Fetching = "Fetching", + Done = "Done", + Failed = "Failed", + Removed = "Removed", +} + +setmetatable(SearchRetrievalStatus, + { + __newindex = function(t, key, index) + end, + __index = function(t, index) + assert(EnumValues[index] ~= nil, ("SearchRetrievalStatus Enum has no value: " .. tostring(index))) + return EnumValues[index] + end + }) + +return SearchRetrievalStatus \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/FitChildren.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/FitChildren.lua new file mode 100644 index 0000000..ea72a45 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/FitChildren.lua @@ -0,0 +1,162 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local Immutable = require(Modules.Common.Immutable) +local Symbol = require(Modules.Common.Symbol) + +local FitChildren = {} + +FitChildren.FitAxis = { + Width = Symbol.named("Width"), + Height = Symbol.named("Height"), + Both = Symbol.named("Both"), +} + +function FitChildren.wrap(component) + local connection = Roact.PureComponent:extend(("FitChildren(%s)"):format(tostring(component))) + + function connection:disconnectSignals() + for _, signal in ipairs(self.signals) do + signal:Disconnect() + end + end + + function connection:resize() + local fitFields = self.props.fitFields + if not fitFields then + if self.props.fitAxis then + fitFields = { + Size = self.props.fitAxis, + } + else + fitFields = { + Size = FitChildren.FitAxis.Both, + } + end + end + + self:disconnectSignals() + self.signals = {} + + local uiLayout = self.frame:FindFirstChildWhichIsA("UIGridStyleLayout") + if uiLayout then + -- The UIListLayout.AbsoluteContentSize isn't yet populated when didMount is called, so we need to use + -- a property changed signal to update the size as soon as it is populated. This is also important in + -- case it an element moves and AbsoluteContentSize changes. + local connection = uiLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function() + local size = uiLayout.AbsoluteContentSize + self:applyFit(fitFields, size.x, size.y) + end) + table.insert(self.signals, connection) + else + self:resizeFromChildren(fitFields) + + -- Need to respond to the children changing size or position later, and redo the sizing. + local children = self.frame:GetChildren() + for _, child in ipairs(children) do + if child:IsA("GuiBase2d") then + local sizeConnection = child:GetPropertyChangedSignal("Size"):Connect(function() + child:GetPropertyChangedSignal("AbsoluteSize"):Wait() + self:resizeFromChildren(fitFields) + end) + table.insert(self.signals, sizeConnection) + local posConnection = child:GetPropertyChangedSignal("Position"):Connect(function() + child:GetPropertyChangedSignal("AbsolutePosition"):Wait() + self:resizeFromChildren(fitFields) + end) + table.insert(self.signals, posConnection) + end + end + end + end + + function connection:resizeFromChildren(fitFields) + local basePos = self.frame.AbsolutePosition + local children = self.frame:GetChildren() + local maxx = 0 + local maxy = 0 + for _, child in ipairs(children) do + if child:IsA("GuiBase2d") then + local childSize = child.AbsoluteSize + local childPos = child.AbsolutePosition + maxx = math.max(maxx, (childPos.x - basePos.x) + childSize.x) + maxy = math.max(maxy, (childPos.y - basePos.y) + childSize.y) + end + end + self:applyFit(fitFields, maxx, maxy) + end + + function connection:applyFit(fitFields, x, y) + local padding = self.frame:FindFirstChildOfClass("UIPadding") + local baseSize = self.props.Size or UDim2.new(0, 0, 0, 0) + local axisWidth = FitChildren.FitAxis.Width + local axisHeight = FitChildren.FitAxis.Height + + local function calculateAxis(fitAxis, fitAxisAgainst, baseValue, baseDims, padding1, padding2) + local doFit = fitAxis == fitAxisAgainst or fitAxis == FitChildren.FitAxis.Both + local fitScale, fitOffset + if doFit then + fitScale = 0 + if padding then + local paddingScale = padding[padding1].Scale + padding[padding2].Scale + local paddingOffset = padding[padding1].Offset + padding[padding2].Offset + if paddingScale == 1 then + error("Can not apply FitChildren to a component with 100% padding width") + else + fitOffset = (baseValue + paddingOffset) / (1 - paddingScale) + end + else + fitOffset = baseValue + end + else + fitScale = baseDims.Scale + fitOffset = baseDims.Offset + end + return fitScale, fitOffset + end + + for field, axis in pairs(fitFields) do + local xScale, xOffset = calculateAxis(axis, axisWidth, x, baseSize.X, "PaddingLeft", "PaddingRight") + local yScale, yOffset = calculateAxis(axis, axisHeight, y, baseSize.Y, "PaddingTop", "PaddingBottom") + + self.frame[field] = UDim2.new(xScale, xOffset, yScale, yOffset) + end + end + + function connection:init() + self.signals = {} + + self.ref = function(rbx) + self.frame = rbx + if self.props[Roact.Ref] then + self.props[Roact.Ref](rbx) + end + end + end + + function connection:render() + local frameProps = Immutable.RemoveFromDictionary(self.props, "fitAxis", "fitFields") + frameProps[Roact.Ref] = self.ref + + return Roact.createElement(component, frameProps) + end + + function connection:didMount() + self:resize() + end + + function connection:didUpdate() + self:resize() + end + + function connection:willUnmount() + self:disconnectSignals() + end + + return connection +end + +FitChildren.FitFrame = FitChildren.wrap("Frame") +FitChildren.FitScrollingFrame = FitChildren.wrap("ScrollingFrame") + +return FitChildren \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/FitChildren.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/FitChildren.spec.lua new file mode 100644 index 0000000..10bf609 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/FitChildren.spec.lua @@ -0,0 +1,256 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + + local FitChildren = require(script.parent.FitChildren) + + describe("FitFrame", function() + it("should create and destroy without errors", function() + local element = Roact.createElement(FitChildren.FitFrame) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should be sized based on its contents", function() + local element = Roact.createElement(FitChildren.FitFrame, {}, { + frame1 = Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0, 200, 0, 50), + }), + frame2 = Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0, 25, 0, 150), + }), + }) + local container = Instance.new("Folder") + Roact.mount(element, container, "FitTest") + + expect(container.FitTest.Size.X.Offset).to.equal(250) + expect(container.FitTest.Size.Y.Offset).to.equal(200) + end) + + it("should account for UIPadding", function() + -- Normally, UIPadding pushes the contents inward from the specified size, fitting it inside a smaller, + -- internal rectangle. However, with FitChildren that internal rectangle is specified by the children, + -- so we need to do that math in reverse, and extrapolate what containing rectangle, with the specified + -- padding, would result in this internal rectangle. To make sure the math on this is right, we do the + -- math for normal padding here, and then test that FitChildren will properly reverse it. + local baseX = 200 + local paddingXScale = 0.1 + local paddingXOffset = 10 + local internalX = baseX - (baseX * paddingXScale + paddingXOffset) + local baseY = 300 + local paddingYScale = 0.25 + local paddingYOffset = 7 + local internalY = baseY - (baseY * paddingYScale + paddingYOffset) + + local element = Roact.createElement(FitChildren.FitFrame, {}, { + padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, paddingXOffset), + PaddingRight = UDim.new(paddingXScale, 0), + PaddingTop = UDim.new(0, paddingYOffset), + PaddingBottom = UDim.new(paddingYScale, 0), + }), + frame = Roact.createElement("Frame", { + Size = UDim2.new(0, internalX, 0, internalY), + }), + }) + local container = Instance.new("Folder") + Roact.mount(element, container, "FitTest") + + expect(container.FitTest.Size.X.Offset).to.equal(baseX) + expect(container.FitTest.Size.Y.Offset).to.equal(baseY) + end) + + it("should get the same results for 0 padding as it would for not having a UIPadding", function() + local element = Roact.createElement(FitChildren.FitFrame, {}, { + padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, 0), + PaddingRight = UDim.new(0, 0), + PaddingTop = UDim.new(0, 0), + PaddingBottom = UDim.new(0, 0), + }), + frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 200), + }), + }) + local container = Instance.new("Folder") + Roact.mount(element, container, "FitTest") + + expect(container.FitTest.Size.X.Offset).to.equal(100) + expect(container.FitTest.Size.Y.Offset).to.equal(200) + end) + + it("should resize when one of its children changes", function() + local element = Roact.createElement(FitChildren.FitFrame, {}, { + frame1 = Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0, 200, 0, 50), + }), + frame2 = Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0, 25, 0, 150), + }), + }) + local container = Instance.new("Folder") + Roact.mount(element, container, "FitTest") + container.FitTest.frame1.Position = UDim2.new(0, 300, 0, 50) + + expect(container.FitTest.Size.X.Offset).to.equal(350) + expect(container.FitTest.Size.Y.Offset).to.equal(200) + end) + + it("should only resize one axis when specified as such", function() + local element = Roact.createElement(FitChildren.FitFrame, { + Size = UDim2.new(0, 100, 0, 100), + fitAxis = FitChildren.FitAxis.Width, + }, { + frame1 = Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0, 200, 0, 50), + }), + frame2 = Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0, 25, 0, 150), + }), + }) + local container = Instance.new("Folder") + Roact.mount(element, container, "FitTest") + + expect(container.FitTest.Size.X.Offset).to.equal(250) + expect(container.FitTest.Size.Y.Offset).to.equal(100) + end) + + it("should update if the props change", function() + local component = Roact.Component:extend("TestComponent") + local setUseHeight + + function component:init() + self.state = { + useHeight = false, + } + setUseHeight = function() + self:setState({ + useHeight = true + }) + end + end + + function component:render() + return Roact.createElement(FitChildren.FitFrame, { + Size = UDim2.new(0, 100, 0, 100), + fitAxis = self.state.useHeight and FitChildren.FitAxis.Height or FitChildren.FitAxis.Width, + }, { + frame1 = Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0, 200, 0, 50), + }), + frame2 = Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0, 25, 0, 150), + }), + }) + end + + local element = Roact.createElement(component) + local container = Instance.new("Folder") + Roact.mount(element, container, "FitTest") + + expect(container.FitTest.Size.X.Offset).to.equal(250) + expect(container.FitTest.Size.Y.Offset).to.equal(100) + + setUseHeight() + + expect(container.FitTest.Size.X.Offset).to.equal(100) + expect(container.FitTest.Size.Y.Offset).to.equal(200) + end) + + it("should update if children are added", function() + local component = Roact.Component:extend("TestComponent") + local setExtraChild + + function component:init() + self.state = { + extraChild = false, + } + setExtraChild = function() + self:setState({ + extraChild = true + }) + end + end + + function component:render() + if self.state.extraChild then + return Roact.createElement(FitChildren.FitFrame, { + Size = UDim2.new(0, 100, 0, 100), + }, { + frame1 = Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0, 200, 0, 50), + }), + frame2 = Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0, 25, 0, 150), + }), + }) + else + return Roact.createElement(FitChildren.FitFrame, { + Size = UDim2.new(0, 100, 0, 100), + }, { + frame1 = Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0, 200, 0, 50), + }), + }) + end + end + + local element = Roact.createElement(component) + local container = Instance.new("Folder") + Roact.mount(element, container, "FitTest") + + expect(container.FitTest.Size.X.Offset).to.equal(250) + expect(container.FitTest.Size.Y.Offset).to.equal(100) + + setExtraChild() + + expect(container.FitTest.Size.X.Offset).to.equal(250) + expect(container.FitTest.Size.Y.Offset).to.equal(200) + end) + end) + + describe("FitScrollingFrame", function() + it("should create and destroy without errors", function() + local element = Roact.createElement(FitChildren.FitScrollingFrame) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should resize the specified props on the specified axis", function() + local element = Roact.createElement(FitChildren.FitScrollingFrame, { + Size = UDim2.new(0, 100, 0, 100), + fitFields = { + Size = FitChildren.FitAxis.Width, + CanvasSize = FitChildren.FitAxis.Both, + }, + }, { + frame1 = Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0, 200, 0, 50), + }), + frame2 = Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0, 25, 0, 150), + }), + }) + local container = Instance.new("Folder") + Roact.mount(element, container, "FitTest") + + expect(container.FitTest.Size.X.Offset).to.equal(250) + expect(container.FitTest.Size.Y.Offset).to.equal(100) + expect(container.FitTest.CanvasSize.X.Offset).to.equal(250) + expect(container.FitTest.CanvasSize.Y.Offset).to.equal(200) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/FlagSettings.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/FlagSettings.lua new file mode 100644 index 0000000..e7e8894 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/FlagSettings.lua @@ -0,0 +1,68 @@ +local NotificationService = game:GetService("NotificationService") + +local FlagSettings = { + isLuaAppStarterScriptEnabled = false, +} + +local function IsRunningInStudio() + return game:GetService("RunService"):IsStudio() +end + +-- Intended to be used by LuaAppStarterScript.lua only. +function FlagSettings:SetIsLuaAppStarterScriptEnabled(isEnabled) + self.isLuaAppStarterScriptEnabled = isEnabled +end + +function FlagSettings:IsLuaAppStarterScriptEnabled() + return self.isLuaAppStarterScriptEnabled +end + +function FlagSettings.IsLuaHomePageEnabled(platform) + if IsRunningInStudio() then + return true + end + + if platform == Enum.Platform.IOS or platform == Enum.Platform.Android then + return NotificationService.IsLuaHomePageEnabled + else + return false + end +end + +function FlagSettings.IsLuaGamesPageEnabled(platform) + if IsRunningInStudio() then + return true + end + + if platform == Enum.Platform.IOS or platform == Enum.Platform.Android then + return NotificationService.IsLuaGamesPageEnabled + else + return false + end +end + +function FlagSettings.IsLuaBottomBarEnabled() + return IsRunningInStudio() +end + +function FlagSettings.IsLuaAppFriendshipCreatedSignalREnabled() + return settings():GetFFlag("LuaAppFriendshipCreatedSignalREnabled") +end + +function FlagSettings.IsLuaAppDeterminingFormFactorAndPlatform() + return settings():GetFFlag("UseLuaAppStarterScriptOniOS") and settings():GetFFlag("EnableLuaAppFormFactorAndPlatform") +end + +function FlagSettings.IsLoadingHUDOniOSEnabledForGameShare() + return settings():GetFFlag("UseLuaAppStarterScriptOniOS") and settings():GetFFlag("EnableLoadingHUDOniOSForGameShare") +end + +function FlagSettings.IsPeopleListV1Enabled() + return settings():GetFFlag("LuaAppPeopleListV1") +end + +function FlagSettings:UseCppTextTruncation() + return settings():GetFFlag("TextTruncationEnabled") +end + +return FlagSettings \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/GeneratedStrings.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/GeneratedStrings.lua new file mode 100644 index 0000000..533ce2c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/GeneratedStrings.lua @@ -0,0 +1,154 @@ +local HttpService = game:GetService('HttpService') + + +return HttpService:JSONDecode([=[ +{ + "OPTION":{ + "es":"Opción" + }, + "FAILED_TO_LEAVE_GROUP_MESSAGE":{ + "es":"No se te ha podido eliminar de la conversación {CONVERSATION_TITLE}." + }, + "SEE_MORE_FRIENDS":{ + "es":"Ver más ({NUMBER_OF_FRIENDS})" + }, + "CHAT_GROUP_NAME":{ + "es":"Nombre del grupo de chat" + }, + "SEE_LESS_FRIENDS":{ + "es":"Ver menos" + }, + "SEARCH":{ + "es":"Buscar" + }, + "CANCEL":{ + "es":"Cancelar" + }, + "LEAVE":{ + "es":"Salir" + }, + "CHAT_INPUT_PLACEHOLDER":{ + "es":"Di algo" + }, + "GENERAL":{ + "es":"General" + }, + "MAKE_FRIENDS_TO_CHAT":{ + "es":"Haz amigos en los juegos para empezar a chatear y jugar juntos." + }, + "SEND":{ + "es":"Enviar" + }, + "SHARE_GAME_TO_CHAT":{ + "es":"Compartir en el chat" + }, + "ONLINE":{ + "es":"Conectado" + }, + "REPORT_USER":{ + "es":"Denunciar usuario" + }, + "SENT":{ + "es":"Enviado" + }, + "CONFIRM":{ + "es":"Aceptar" + }, + "REMOVE_USER_CONFIRMATION_MESSAGE":{ + "es":"¿Seguro que quieres eliminar a {USERNAME} de este grupo de chat?" + }, + "FAILED_TO_LEAVE_GROUP":{ + "es":"Error al salir del grupo" + }, + "SEARCH_FOR_FRIENDS":{ + "es":"Buscar amigos" + }, + "SAVE_ADDED_FRIENDS":{ + "es":"Añadir" + }, + "CHAT":{ + "es":"Chat" + }, + "SAVE":{ + "es":"Guardar" + }, + "MEMBERS":{ + "es":"Miembros" + }, + "TOO_MANY_PEOPLE":{ + "es":"Puedes tener hasta {MAX_GROUP_SIZE} amigos en un grupo de chat." + }, + "FAILED_TO_REMOVE_USER_MESSAGE":{ + "es":"No se ha podido eliminar al usuario {USERNAME} de la conversación {CONVERSATION_TITLE}." + }, + "NO_NETWORK_CONNECTION":{ + "es":"Conectando..." + }, + "GROUP_NAME_MODERATED":{ + "es":"El nombre introducido ha sido moderado" + }, + "LEAVE_GROUP":{ + "es":"Salir del grupo" + }, + "NOT_SET":{ + "es":"Sin configurar" + }, + "PRIVACY_SETTINGS":{ + "es":"Configuración de privacidad" + }, + "TURN_ON_CHAT":{ + "es":"Para chatear con tus amigos, activa el chat en la configuración de privacidad." + }, + "SAVE_NEW_GROUP":{ + "es":"Crear" + }, + "FAILED_TO_RENAME_MESSAGE":{ + "es":"No se ha podido cambiar el nombre de la conversación de {EXISTING_NAME} a {NEW_NAME}." + }, + "FAILED_TO_RENAME_TITLE":{ + "es":"Error al cambiar el nombre de la conversación" + }, + "NAME_THIS_CHAT_GROUP":{ + "es":"Ponle un nombre al grupo" + }, + "ADD_FRIENDS":{ + "es":"Añadir amigos" + }, + "LEAVE_GROUP_MESSAGE":{ + "es":"No podrás seguir chateando en este grupo." + }, + "FAILED_TO_REMOVE_USER":{ + "es":"Error al eliminar usuario" + }, + "REMOVE_USER":{ + "es":"Eliminar usuario" + }, + "CHAT_DETAILS":{ + "es":"Detalles del chat" + }, + "REMOVE_FROM_GROUP":{ + "es":"Eliminar del grupo" + }, + "VIEW_PROFILE":{ + "es":"Ver perfil" + }, + "REMOVE":{ + "es":"Eliminar" + }, + "NEW_CHAT_GROUP":{ + "es":"Nuevo grupo de chat" + }, + "THIS_MESSAGE_WAS_MODERATED":{ + "es":"Este mensaje ha sido moderado y no se ha enviado." + }, + "NOTIFICATIONS":{ + "es":"Notificaciones" + }, + "OFFLINE":{ + "es":"Sin conexión" + }, + "STAY":{ + "es":"Quedarse" + } +} +]=]) diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/HttpError.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/HttpError.lua new file mode 100644 index 0000000..22d781e --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/HttpError.lua @@ -0,0 +1,45 @@ +local HttpError = {} +HttpError.__index = HttpError + +HttpError.Kind = { + -- A catch-all for all errors we can't otherwise classify + Unknown = "Unknown", + + -- We could not resolve the request + RequestFailure = "Request Failed", + + -- We could not resolve the request, but it's a server issue, go ahead and retry + RequireExternalRetry = "Require External Retry", + + -- We bailed out before we got a response from the server + LuaTimeout = "Lua Timeout", + + -- We expected JSON in the response body, but it was malformed + InvalidJson = "Invalid Json", +} + +function HttpError.new(targetUrl, errKind, errMessage) + assert(type(targetUrl) == "string", "Expected targetUrl to be a string") + assert(type(errKind) == "string", "Expected errKind to be a string") + assert(type(errMessage) == "string", "Expected errMessage to be a string") + + local err = { + targetUrl = targetUrl, + kind = errKind, + message = errMessage + } + setmetatable(err, HttpError) + + return err +end + +function HttpError:__tostring() + return table.concat({ + "HttpError :", + string.format("\tTarget Url - %s", self.targetUrl), + string.format("\tKind - %s", self.kind), + string.format("\tMessage - %s", self.message), + }, "\n") +end + +return HttpError \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/HttpError.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/HttpError.spec.lua new file mode 100644 index 0000000..c6e3b23 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/HttpError.spec.lua @@ -0,0 +1,64 @@ +return function() + local HttpError = require(script.Parent.HttpError) + + describe("new", function() + it("should construct without a problem", function() + local err = HttpError.new("testUrl", HttpError.Kind.Unknown, "testMessage") + expect(err).to.be.ok() + end) + + it("should just pass data through", function() + local testUrl = "testUrl" + local testErrKind = HttpError.Kind.RequestFailure + local testMessage = "test" + + local err = HttpError.new("testUrl", testErrKind, testMessage) + + expect(err.targetUrl).to.equal(testUrl) + expect(err.kind).to.equal(testErrKind) + expect(err.message).to.equal(testMessage) + end) + + it("should validate its inputs", function() + local validUrl = "test" + local validKind = HttpError.Kind.Unknown + local validMsg = "test" + + local function createHttpError(url, kind, msg) + -- helper function for checking if the constructor throws errors + return function() + HttpError.new(url, kind, msg) + end + end + + expect(createHttpError(validUrl, validKind, validMsg)).to.be.ok() + + -- invalid Url values + expect(createHttpError(nil, validKind, validMsg)).to.throw() + expect(createHttpError(1, validKind, validMsg)).to.throw() + expect(createHttpError(true, validKind, validMsg)).to.throw() + expect(createHttpError({}, validKind, validMsg)).to.throw() + expect(createHttpError(function() end, validKind, validMsg)).to.throw() + + -- invalid Kind values + expect(createHttpError(validUrl, nil, validMsg)).to.throw() + expect(createHttpError(validUrl, 1, validMsg)).to.throw() + expect(createHttpError(validUrl, true, validMsg)).to.throw() + expect(createHttpError(validUrl, {}, validMsg)).to.throw() + expect(createHttpError(validUrl, function() end, validMsg)).to.throw() + + -- invalid Message values + expect(createHttpError(validUrl, validKind, nil)).to.throw() + expect(createHttpError(validUrl, validKind, 1)).to.throw() + expect(createHttpError(validUrl, validKind, true)).to.throw() + expect(createHttpError(validUrl, validKind, {})).to.throw() + expect(createHttpError(validUrl, validKind, function() end)).to.throw() + end) + end) + + describe("Kind", function() + it("should return a table", function() + expect(type(HttpError.Kind)).to.equal("table") + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/HttpResponse.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/HttpResponse.lua new file mode 100644 index 0000000..b0cf0dc --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/HttpResponse.lua @@ -0,0 +1,36 @@ +--[[ + Encapsulates the response from an http request. Nothing fancy +]] + + +local HttpResponse = {} +HttpResponse.__index = HttpResponse + +function HttpResponse.new(url, response, responseTime, statusCode) + assert(type(url) == "string", "Expected url to be a string") + assert(type(response) == "string" or type(response) == "table" , "Expected response to be a string or table") + assert(type(responseTime) == "number", "Expected responseTimeMs to be a number") + assert(type(statusCode) == "number", "Expected statusCode to be a number") + + local responseObj = { + requestUrl = url, + responseTimeMs = responseTime, + responseCode = statusCode, + responseBody = response + } + setmetatable(responseObj, HttpResponse) + + return responseObj +end + +function HttpResponse:__tostring() + return table.concat({ + "HttpResponse :", + string.format("\tRequested Url - %s", self.requestUrl), + string.format("\tCode - %s", tostring(self.responseCode)), + string.format("\tBody - %s", tostring(self.responseBody)), + string.format("\tTime(ms) - %s", tostring(self.responseTimeMs)) + }, "\n") +end + +return HttpResponse \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/HttpResponse.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/HttpResponse.spec.lua new file mode 100644 index 0000000..65a07e4 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/HttpResponse.spec.lua @@ -0,0 +1,71 @@ +return function() + local HttpResponse = require(script.Parent.HttpResponse) + local StatusCodes = require(script.Parent.StatusCodes) + + describe("new", function() + it("should construct without a problem", function() + local hr = HttpResponse.new("testUrl", "testBody", 0, StatusCodes.OK) + expect(hr).to.be.ok() + end) + + it("should just pass data through", function() + local testUrl = "testUrl" + local testBody = "testBody" + local testTime = 203 + local testCode = StatusCodes.OK + + local response = HttpResponse.new(testUrl, testBody, testTime, testCode) + + expect(response.requestUrl).to.equal(testUrl) + expect(response.responseBody).to.equal(testBody) + expect(response.responseTimeMs).to.equal(testTime) + expect(response.responseCode).to.equal(testCode) + end) + + it("should validate its inputs", function() + local validUrl = "test" + local validBodyString = "test" + local validBodyTable = {} + local validTime = 0 + local validCode = StatusCodes.OK + + + local function createHttpResponse(url, body, responseTime, status) + -- helper function for checking if the constructor throws errors + return function() + HttpResponse.new(url, body, responseTime, status) + end + end + + expect(createHttpResponse(validUrl, validBodyString, validTime, validCode)).to.be.ok() + expect(createHttpResponse(validUrl, validBodyTable, validTime, validCode)).to.be.ok() + + -- invalid Url values + expect(createHttpResponse(nil, validBodyString, validTime, validCode)).to.throw() + expect(createHttpResponse(1, validBodyString, validTime, validCode)).to.throw() + expect(createHttpResponse(true, validBodyString, validTime, validCode)).to.throw() + expect(createHttpResponse({}, validBodyString, validTime, validCode)).to.throw() + expect(createHttpResponse(function() end, validBodyString, validTime, validCode)).to.throw() + + -- invalid ResponseBody values + expect(createHttpResponse(validUrl, nil, validTime, validCode)).to.throw() + expect(createHttpResponse(validUrl, 1, validTime, validCode)).to.throw() + expect(createHttpResponse(validUrl, true, validTime, validCode)).to.throw() + expect(createHttpResponse(validUrl, function() end, validTime, validCode)).to.throw() + + -- invalid ResponseTime values + expect(createHttpResponse(validUrl, validBodyString, nil, validCode)).to.throw() + expect(createHttpResponse(validUrl, validBodyString, "test", validCode)).to.throw() + expect(createHttpResponse(validUrl, validBodyString, true, validCode)).to.throw() + expect(createHttpResponse(validUrl, validBodyString, {}, validCode)).to.throw() + expect(createHttpResponse(validUrl, validBodyString, function() end, validCode)).to.throw() + + -- invalid ResponseCode values + expect(createHttpResponse(validUrl, validBodyString, validTime, nil)).to.throw() + expect(createHttpResponse(validUrl, validBodyString, validTime, "test")).to.throw() + expect(createHttpResponse(validUrl, validBodyString, validTime, true)).to.throw() + expect(createHttpResponse(validUrl, validBodyString, validTime, {})).to.throw() + expect(createHttpResponse(validUrl, validBodyString, validTime, function() end)).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/json.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/json.lua new file mode 100644 index 0000000..ed2d417 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/json.lua @@ -0,0 +1,31 @@ +--[[ + Given an http promise, attempt to parse the resulting response body from JSON to a table +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local HttpService = game:GetService("HttpService") +local Promise = require(Modules.LuaApp.Promise) +local HttpError = require(Modules.LuaApp.Http.HttpError) +local HttpResponse = require(Modules.LuaApp.Http.HttpResponse) + +-- requestFunc : (function>(url, requestMethod, options)) +-- RETURNS : (function>(url, requestMethod, options)) replaces the response's body with a table +return function(requestFunc) + return function(url, requestMethod, options) + local httpPromise = requestFunc(url, requestMethod, options):andThen(function(response) + local ok, result = pcall(HttpService.JSONDecode, HttpService, response.responseBody) + + if not ok then + local errMsg = string.format("Cannot parse : %s", response.responseBody or "nil") + return Promise.reject(HttpError.new(url, HttpError.Kind.InvalidJson, errMsg)) + end + + return HttpResponse.new( + response.requestUrl, + result, -- the response body is now a table, not a string + response.responseTimeMs, + response.responseCode) + end) + + return httpPromise + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/json.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/json.spec.lua new file mode 100644 index 0000000..a18168a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/json.spec.lua @@ -0,0 +1,58 @@ +return function() + local json = require(script.Parent.json) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local HttpError = require(Modules.LuaApp.Http.HttpError) + local MockRequest = require(Modules.LuaApp.TestHelpers.MockRequest) + + it("should return an HttpResponse object with a parsed table", function() + local testBody = [[{ "foo": 123, "bar": "hello world" }]] + local request = MockRequest.simpleSuccessRequest(testBody) + request = json(request) + + local matchesType = false + local matchesValueFoo = false + local matchesValueBar = false + + local httpPromise = request("fakeUrl", "GET") + httpPromise:andThen(function(result) + matchesType = type(result.responseBody) == "table" + matchesValueFoo = result.responseBody.foo == 123 + matchesValueBar = result.responseBody.bar == "hello world" + end) + + expect(matchesType).to.equal(true) + expect(matchesValueFoo).to.equal(true) + expect(matchesValueBar).to.equal(true) + end) + + it("should return an HttpError when invalid json is returned", function() + local testBody = "this isn't json" + local request = MockRequest.simpleSuccessRequest(testBody) + request = json(request) + + local matchesKind = false + + local httpPromise = request("fakeUrl", "GET") + httpPromise:catch(function(httpError) + matchesKind = httpError.kind == HttpError.Kind.InvalidJson + end) + + expect(matchesKind).to.equal(true) + end) + + it("should pass errors through from a lower level", function() + local testErrKind = HttpError.Kind.RequireExternalRetry + local request = MockRequest.simpleFailRequest(testErrKind) + request = json(request) + + local matchesKind = false + + local httpPromise = request("fakeUrl", "GET") + httpPromise:catch(function(httpError) + matchesKind = httpError.kind == testErrKind + end) + + expect(matchesKind).to.equal(true) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/requestDataModel.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/requestDataModel.lua new file mode 100644 index 0000000..87b520c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/requestDataModel.lua @@ -0,0 +1,132 @@ +--[[ + Abstracts out the networking logic to provide a unified interface for request.lua. + Exposes a single function to return a promise object with an HttpResponse. + + This implementation utilizes the networking functions on the DataModel. +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Promise = require(Modules.LuaApp.Promise) +local HttpError = require(Modules.LuaApp.Http.HttpError) +local HttpResponse = require(Modules.LuaApp.Http.HttpResponse) +local StatusCodes = require(Modules.LuaApp.Http.StatusCodes) + + +-- url : (string) +-- requestMethod : (string) +-- args : (table) +-- RETURNS : (HttpResponse) +local function makeRequest(url, httpFunc, args) + -- this function handles the actual network request and any and all additional + -- business logic around the request. + + -- fetch the raw response from the server and time how long it takes + -- this pcall will prevent the server from throwing errors on a 404 or other server problem + local startTime = tick() + local success, responseString = pcall(httpFunc, unpack(args)) + local endTime = tick() + + -- package information about the response into a single object + local responseTimeMs = (endTime - startTime) * 1000 + local statusCode + if success then + statusCode = StatusCodes.OK + else + statusCode = StatusCodes.UNKNOWN_ERROR + -- The expected failure response should look like this : + -- HTTP 404 (HTTP/1.1 404 Not Found) + -- HTTP 500 (HTTP/1.1 500 Internal Server Error) + + -- capture all the text within the parentheses + local parenText = string.match(responseString, ".*%((.*)%)") + if parenText then + local codeIndex = string.find(parenText, "%d%d%d") + if codeIndex then + -- capture the error code and the error message + statusCode = tonumber(string.sub(parenText, codeIndex, codeIndex + 2)) + responseString = string.sub(parenText, codeIndex + 4) + end + end + end + + return HttpResponse.new(url, responseString, responseTimeMs, statusCode) +end + +-- httpResponse : (HttpResponse) +-- RETURNS : (HttpError) +local function getErrorFromResponse(httpResponse) + local errorKind = HttpError.Kind.Unknown + local message = httpResponse.responseBody + + if httpResponse.responseCode ~= StatusCodes.UNKNOWN_ERROR then + local code = httpResponse.responseCode + if code >= 500 then + -- there was a server error, flag this request for retry + errorKind = HttpError.Kind.RequireExternalRetry + else + -- the request simply failed for some reason or another, return the code so someone else can handle the error + errorKind = HttpError.Kind.RequestFailure + message = tostring(code) + end + end + + return HttpError.new(httpResponse.requestUrl, errorKind, message) +end + +-- requestService : (table, optional) an object that implements the same http functions as the data model +return function(requestService) + -- for tests, allow the object that makes the request to be mocked + if not requestService then + requestService = game + end + + -- url : (string) + -- requestMethod : (string) "GET", "POST", "PUT", etc. + -- args : (table, optional) + -- options.contentType : (Enum.HttpContentType, optional) + -- options.postBody : (string, optional ("POST" only)) + -- RETURNS : (promise) + return function(url, requestMethod, options) + assert(type(url) == "string", "Expected url to be a string") + assert(type(requestMethod) == "string", "Expected requestMethod to be a string") + requestMethod = string.upper(requestMethod) + if options then + assert(type(options) == "table", "Expected extra args to be a table") + end + if requestMethod == "POST" then + assert(options.postBody, "Expected a postBody to be specified with this request") + if not options.contentType then + options.contentType = "application/json" + end + end + + -- assemble the arguments to make the request + local httpFunc + local httpFuncArgs + if requestMethod == "GET" then + httpFunc = requestService.HttpGetAsync + httpFuncArgs = { requestService, url } + + elseif requestMethod == "POST" then + httpFunc = requestService.HttpPostAsync + httpFuncArgs = { requestService, url, options.postBody, options.contentType } + + else + error(string.format("Unsupported requestMethod : %s", requestMethod or "nil")) + end + + local httpPromise = Promise.new(function(resolve, reject) + spawn(function() + local httpResponse = makeRequest(url, httpFunc, httpFuncArgs) + if httpResponse.responseCode == StatusCodes.OK then + resolve(httpResponse) + else + local httpError = getErrorFromResponse(httpResponse) + reject(httpError) + end + end) + end) + + return httpPromise + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/requestDataModel.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/requestDataModel.spec.lua new file mode 100644 index 0000000..669c82f --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/requestDataModel.spec.lua @@ -0,0 +1,143 @@ +return function() + local request = require(script.Parent.requestDataModel) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local HttpError = require(Modules.LuaApp.Http.HttpError) + local StatusCodes = require(Modules.LuaApp.Http.StatusCodes) + + local function createTestRequestFunc(testResponse) + local requestService = {} + function requestService:HttpGetAsync() + return testResponse + end + function requestService:HttpPostAsync() + return testResponse + end + + return request(requestService) + end + + it("should return a function", function() + expect(request()).to.be.ok() + expect(type(request())).to.equal("function") + end) + + it("should validate its inputs", function() + local testRequest = createTestRequestFunc() + local function testParams(url, requestMethod, args) + return function() + testRequest(url, requestMethod, args) + end + end + + local validUrl = "testUrl" + local validMethod = "GET" + local validArgs = {} + + -- url checks + expect(testParams(nil, validMethod, validArgs)).to.throw() + expect(testParams(123, validMethod, validArgs)).to.throw() + expect(testParams({}, validMethod, validArgs)).to.throw() + expect(testParams(true, validMethod, validArgs)).to.throw() + expect(testParams(function() end, validMethod, validArgs)).to.throw() + + -- request method checks + expect(testParams(validUrl, nil, validArgs)).to.throw() + expect(testParams(validUrl, 123, validArgs)).to.throw() + expect(testParams(validUrl, {}, validArgs)).to.throw() + expect(testParams(validUrl, true, validArgs)).to.throw() + expect(testParams(validUrl, function() end, validArgs)).to.throw() + + -- args checks + expect(testParams(validUrl, validMethod, 123)).to.throw() + expect(testParams(validUrl, validMethod, "Test")).to.throw() + expect(testParams(validUrl, validMethod, true)).to.throw() + expect(testParams(validUrl, validMethod, function() end)).to.throw() + end) + + it("should throw an error if the requestMethod isn't supported", function() + local testRequest = createTestRequestFunc("foo") + + expect(function() + testRequest("testUrl", "GIVEANDTAKE") + end).to.throw() + end) + + describe("the request interface", function() + -- request yields when it attempts the request + HACK_NO_XPCALL() + + it("should return a promise that resolves to an HttpResponse", function() + local responseUpval + + local testRequest = createTestRequestFunc("foo") + local httpPromise = testRequest("testUrl", "GET") + httpPromise:andThen(function(response) + responseUpval = response + end) + + wait() + + expect(responseUpval.requestUrl).to.equal("testUrl") + expect(responseUpval.responseBody).to.equal("foo") + expect(responseUpval.responseCode).to.equal(StatusCodes.OK) + end) + + it("should return the error code when a request fails for non-server reasons", function() + local failingTestService = {} + function failingTestService:HttpGetAsync() + error("HTTP 404 (HTTP/1.1 404 Not Found)") + end + + local errUpval + + local testRequest = request(failingTestService) + testRequest("testUrl", "GET"):catch(function(httpError) + errUpval = httpError + end) + + wait() + + expect(errUpval.kind).to.equal(HttpError.Kind.RequestFailure) + expect(errUpval.message).to.equal("404") + end) + + it("should parse out an http error message when a request fails", function() + local failingTestService = {} + function failingTestService:HttpGetAsync() + error("HTTP 500 (HTTP/1.1 500 Internal Server Error)") + end + + local errUpval + + local testRequest = request(failingTestService) + testRequest("testUrl", "GET"):catch(function(httpError) + errUpval = httpError + end) + + wait() + + expect(errUpval.kind).to.equal(HttpError.Kind.RequireExternalRetry) + expect(errUpval.message).to.equal("Internal Server Error") + end) + + it("should return an unknown error when the error is bonkers", function() + local failingTestService = {} + function failingTestService:HttpGetAsync() + error("BAD_TLS") + end + + local errUpval + + local testRequest = request(failingTestService) + testRequest("testUrl", "GET"):catch(function(httpError) + errUpval = httpError + end) + + wait() + + expect(errUpval.kind).to.equal(HttpError.Kind.Unknown) + expect(string.find(errUpval.message, "BAD_TLS") > 0).to.equal(true) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/retry.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/retry.lua new file mode 100644 index 0000000..2b665b8 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/retry.lua @@ -0,0 +1,83 @@ +--[[ + Implements exponential backoff HTTP request retry. +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Promise = require(Modules.LuaApp.Promise) +local HttpError = require(Modules.LuaApp.Http.HttpError) + + +-- seconds : (number) the number of seconds to wait before resuming +local function defer(seconds) + return Promise.new(function(resolve) + delay(seconds, function() + resolve() + end) + end) +end + +-- httpFunc : (function>()) a function that fires a network request until it succeeds +-- options : (table) retry configuration parameters +-- options.remainingAttempts : (number) a counter for determining when we've failed +-- options.maxAttempts : (number) a value to let us know how many attempts we started with +-- options.backoffRate : (number) +-- options.shouldRetryFunc : (function(HttpError)) custom logic +-- options.shouldImmediateRetry : (bool, TESTING ONLY) when true, disregards backoff rate +local function retryRequest(httpFunc, options) + return httpFunc():catch(function(httpError) + options.remainingAttempts = options.remainingAttempts - 1 + + -- decide whether to retry + local shouldRetry = options.remainingAttempts > 0 and + (httpError.kind == HttpError.Kind.RequireExternalRetry or + httpError.kind == HttpError.Kind.LuaTimeout) + + if not shouldRetry then + return Promise.reject(httpError) + end + + if options.shouldImmediateRetry then + -- In tests, resolve the retry logic immediately. + -- This functionality should one day be replaced with a service that mocks the passage of time, + -- that way, tests will be able to resolve synchronously. + return retryRequest(httpFunc, options) + else + -- wait for an increasing amount of time before retrying + local nextDelay = options.backoffRate ^ (options.maxAttempts - options.remainingAttempts) + return defer(nextDelay):andThen(function() + return retryRequest(httpFunc, options) + end) + end + end) +end + +-- requestFunc : (function>(url, requestMethod, options)) +-- options : (table, optional) +-- shouldImmediateRetry : (bool) when true, disregards backoff rate +return function(requestFunc, options) + + -- default retry delays are 2, 4, 8 seconds + local retryConfigParams = { + maxAttempts = 3, + backoffRate = 2, + shouldImmediateRetry = false + } + + if options then + if options.shouldImmediateRetry then + assert(type(options.shouldImmediateRetry) == "boolean", "shouldImmediateRetry must be a bool") + retryConfigParams.shouldImmediateRetry = options.shouldImmediateRetry + end + end + + retryConfigParams.remainingAttempts = retryConfigParams.maxAttempts + + return function(url, requestMethod, options) + -- wrap the request into a function that can be called multiple times until we succeed + local function makeRequest() + return requestFunc(url, requestMethod, options) + end + + local httpPromise = retryRequest(makeRequest, retryConfigParams) + return httpPromise + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/retry.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/retry.spec.lua new file mode 100644 index 0000000..6ce99eb --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/retry.spec.lua @@ -0,0 +1,90 @@ + +return function() + local retry = require(script.Parent.retry) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local HttpError = require(Modules.LuaApp.Http.HttpError) + local HttpResponse = require(Modules.LuaApp.Http.HttpResponse) + local StatusCodes = require(Modules.LuaApp.Http.StatusCodes) + local Promise = require(Modules.LuaApp.Promise) + + it("should retry when the request fails up to the maximum", function() + local numberOfRequests = 0 + local matchesErrorKind = false + local matchesRetryCount = false + + local maxAttempts = 3 + local testErrKind = HttpError.Kind.RequireExternalRetry + + local request = function(url, requestMethod, options) + numberOfRequests = numberOfRequests + 1 + return Promise.reject(HttpError.new(url, testErrKind, "retry.spec - retry maximum test")) + end + request = retry(request, { + shouldImmediateRetry = true, + }) + + local httpPromise = request("fakeUrl", "GET") + httpPromise:catch(function(httpError) + matchesErrorKind = httpError.kind == testErrKind + matchesRetryCount = numberOfRequests == maxAttempts + end) + + expect(matchesErrorKind).to.equal(true) + expect(matchesRetryCount).to.equal(true) + end) + + it("should not retry if the request succeeds", function() + local numberOfRequests = 0 + local requestCountCorrect = false + + local request = function(url, requestMethod, options) + numberOfRequests = numberOfRequests + 1 + return Promise.resolve(HttpResponse.new(url, "retry.spec - successful request test", 0, StatusCodes.OK)) + end + request = retry(request, { + shouldImmediateRetry = true, + }) + + local httpPromise = request("fakeUrl", "GET") + httpPromise:andThen(function(httpResponse) + requestCountCorrect = numberOfRequests == 1 + end) + + expect(requestCountCorrect).to.equal(true) + end) + + it("should not retry when an error isn't flagged for retry", function() + -- make a helper function for testing a bunch of different errors + local function testRetryLogicWithError(errKind) + local numberOfRequests = 0 + local matchesErrorKind = false + local requestCountCorrect = false + + local request = function(url, requestMethod, options) + numberOfRequests = numberOfRequests + 1 + return Promise.reject(HttpError.new(url, errKind, "retry.spec - bad request test")) + end + request = retry(request, { + shouldImmediateRetry = true, + }) + + local httpPromise = request("fakeUrl", "GET") + httpPromise:catch(function(httpError) + matchesErrorKind = httpError.kind == errKind + requestCountCorrect = numberOfRequests == 1 + end) + + -- return these expectations to the caller + return matchesErrorKind and requestCountCorrect + end + + -- test different errors here... + expect( testRetryLogicWithError(HttpError.Kind.RequestFailure) ).to.equal(true) + expect( testRetryLogicWithError(HttpError.Kind.Unknown) ).to.equal(true) + + -- these should succeed ... + expect( testRetryLogicWithError(HttpError.Kind.LuaTimeout) ).to.equal(false) + expect( testRetryLogicWithError(HttpError.Kind.RequireExternalRetry) ).to.equal(false) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/timeout.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/timeout.lua new file mode 100644 index 0000000..ee52408 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/timeout.lua @@ -0,0 +1,45 @@ +--[[ + Times out a request if it has not resolved within the expected time limit +]] + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Promise = require(Modules.LuaApp.Promise) +local HttpError = require(Modules.LuaApp.Http.HttpError) + +-- requestFunc - (function>(url, requestMethod, options)) +-- timeout - (number, optional) number of seconds before the request should fail +return function(requestFunc, timeout) + assert(type(requestFunc) == "function", "Expected requestFunc to be a function") + + local expectedTimeout = 30 + if timeout then + assert(type(timeout) == "number", "Expected timout to be a number") + assert(timeout > 0, "Expected timeout to be greater than or equal to 0") + expectedTimeout = timeout + end + + return function(url, requestMethod, options) + -- make the request + local httpPromise = requestFunc(url, requestMethod, options) + + -- wrap the callbacks that in a promise that will fail if the request takes too long + + -- Should the network request succeed after the lua imposed timeout, + -- the promise will not call the resolve callback. + -- Once a promise is resolved or rejected, it stays that way. + -- The inverse is true as well, if the promise is resolved or rejected before the delay completes, + -- the promise will not call the timeout rejection callback. + local timeoutPromise = Promise.new(function(resolve, reject) + -- succeed if the promise succeeds, pass the results through. + httpPromise:andThen(resolve, reject) + + -- escape if the promise has not resolved within the expected timeout + delay(expectedTimeout, function() + local errMsg = string.format("Lua Timeout of %s seconds reached.", tostring(expectedTimeout)) + reject(HttpError.new(url, HttpError.Kind.LuaTimeout, errMsg)) + end) + end) + + return timeoutPromise + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/timeout.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/timeout.spec.lua new file mode 100644 index 0000000..b087c0a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/NetworkLayers/timeout.spec.lua @@ -0,0 +1,65 @@ +return function() + local timeout = require(script.Parent.timeout) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Promise = require(Modules.LuaApp.Promise) + local HttpError = require(Modules.LuaApp.Http.HttpError) + local HttpResponse = require(Modules.LuaApp.Http.HttpResponse) + local StatusCodes = require(Modules.LuaApp.Http.StatusCodes) + local MockRequest = require(Modules.LuaApp.TestHelpers.MockRequest) + + + it("should return a function", function() + local request = MockRequest.simpleSuccessRequest("timeout.spec - simple constructor test") + request = timeout(request) + + expect(request).to.be.ok() + expect(type(request)).to.equal("function") + end) + + it("should pass the resolved value through", function() + local testUrl = "fakeUrl" + local testResponse = "fakeResponse" + local testResponseTime = 0 + local testStatusCode = StatusCodes.OK + + local timeBeforeBail = 0.1 + local request = function(url, requestMethod, options) + return Promise.resolve(HttpResponse.new(url, testResponse, testResponseTime, testStatusCode)) + end + request = timeout(request, timeBeforeBail) + + local httpPromise = request(testUrl, "GET") + httpPromise:resolve(function(hr) + expect(hr.requestUrl).to.equal(testUrl) + expect(hr.responseTimeMs).to.equal(testResponseTime) + expect(hr.responseCode).to.equal(testStatusCode) + expect(hr.responseBody).to.equal(testResponse) + end) + end) + + describe("the retry behavior", function() + HACK_NO_XPCALL() + + it("should return a rejected promise if a request takes too long", function() + local errUpval + local timeBeforeBail = 0.01 + local request = function(url, requestMethod, options) + return Promise.new(function(resolve, reject) + -- don't resolve or reject, so timeout will fire + end) + end + request = timeout(request, timeBeforeBail) + + local httpPromise = request("fakeUrl", "GET") + httpPromise:catch(function(httpError) + errUpval = httpError + end) + + wait() + + expect(errUpval.kind).to.equal(HttpError.Kind.LuaTimeout) + expect(errUpval.targetUrl).to.equal("fakeUrl") + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetAllFiltersGame.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetAllFiltersGame.lua new file mode 100644 index 0000000..b9c7988 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetAllFiltersGame.lua @@ -0,0 +1,25 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Url = require(Modules.LuaApp.Http.Url) + +--[[ + This endpoint returns a promise that resolves to: + + [ + { + "token": "0", + "name": "Any" + }, + { + "token": "1", + "name": "Classic" + } + ] +]]-- + +-- requestImpl - (function>(url, requestMethod, options)) +return function(requestImpl) + local url = string.format("%sv1/games/all-game-filters", Url.GAME_URL) + + -- return a promise of the result listed above + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetAllFiltersGenre.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetAllFiltersGenre.lua new file mode 100644 index 0000000..863c7a6 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetAllFiltersGenre.lua @@ -0,0 +1,22 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Url = require(Modules.LuaApp.Http.Url) + +--[[ + This endpoint returns a promise that resolves to: + + [ + { + "token": "string", + "name": "string" + }, {...}, ... + ] + +]]-- + +-- requestImpl - (function>(url, requestMethod, options)) +return function(requestImpl) + local url = string.format("%sv1/games/all-genre-filters", Url.GAME_URL) + + -- return a promise of the result listed above + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetAllFiltersTime.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetAllFiltersTime.lua new file mode 100644 index 0000000..f01bef5 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetAllFiltersTime.lua @@ -0,0 +1,22 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Url = require(Modules.LuaApp.Http.Url) + +--[[ + This endpoint returns a promise that resolves to: + + [ + { + "token": "string", + "name": "string" + }, {...}, ... + ] + +]]-- + +-- requestImpl - (function>(url, requestMethod, options)) +return function(requestImpl) + local url = string.format("%sv1/games/all-time-filters", Url.GAME_URL) + + -- return a promise of the result listed above + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetAllSorts.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetAllSorts.lua new file mode 100644 index 0000000..817e6d5 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetAllSorts.lua @@ -0,0 +1,25 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Url = require(Modules.LuaApp.Http.Url) + +--[[ + This endpoint returns a promise that resolves to: + + [ + { + "token": "string", + "name": "string", + "timeOptionsAvailable": true, + "genreOptionsAvailable": true, + "numberOfRows": 0, + "isDefaultSort": true + }, {...}, ... + ] +]]-- + +-- requestImpl - (function>(url, requestMethod, options)) +return function(requestImpl) + local url = string.format("%sv1/games/all-sorts", Url.GAME_URL) + + -- return a promise of the result listed above + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetList.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetList.lua new file mode 100644 index 0000000..4f7f698 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetList.lua @@ -0,0 +1,73 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Url = require(Modules.LuaApp.Http.Url) + +--[[ + This endpoint returns a promise that resolves to: + + { + "games": [ + { + "creatorId": 0, + "creatorName": "string", + "creatorType": "string", + "totalUpVotes": 0, + "totalDownVotes": 0, + "universeId": 0, + "name": "string", + "placeId": 0, + "playerCount": 0, + "imageToken": "string", + "imageTokenExpiryInSeconds": 0, + "users": [ + { + "userId": 0, + "gameId": "string" + }, {...}, ... ], + "isSponsored": true, + "nativeAdData": "string" + }, {...}, ... ], + "suggestedKeyword": "string", + "correctedKeyword": "string", + "filteredKeyword": "string", + "hasMoreRows": true, + "nextPageExclusiveStartId": number / nil + } + + requestImpl - (function>(url, requestMethod, options)) + argTable - (Table) of argument that is passed into the request + + A sample argTable: + { + sortToken = "SOME_SORT_TOKEN", + gameFilter = "SOME_GAME_FILTER", + timeFilter = "SOME_TIMER_FILTER", + genreFilter = "SOME_GENRE_FILTER", + + -- This value has to be filled for the featured sort to work properly. + exclusiveStartId = 1298471975, + sortOrder = 1, + keyword = "WHAT_YOU_SEARCH_FOR", + + -- When specifying startRows, the number doesn't include sponsored games. + -- And games start counting from 0. + startRows = 10, + + -- Must be aware that the games returned might be less than (filtered content) + -- or even more than (sponsored games) the number specified in maxRows. + -- When not specified, Api defaults this to 40. + maxRows = 100, + + isKeywordSuggestionEnabled = true, + contextCountryRegionId = 50, + contextUniverseId = 192146914, + } +]]-- +return function(requestImpl, argTable) + + -- construct the url + local args = Url:makeQueryString(argTable) + local url = string.format("%sv1/games/list?%s", Url.GAME_URL, args) + + -- return a promise of the result listed above + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetSorts.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetSorts.lua new file mode 100644 index 0000000..07ebd5d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetSorts.lua @@ -0,0 +1,48 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Url = require(Modules.LuaApp.Http.Url) +local Constants = require(Modules.LuaApp.Constants) + +--[[ + This endpoint returns a promise that resolves to: + { + sorts : [ + { + "token": "flgk4234Gad", + "name": "Popular", + "timeOptionsAvailable": false, + "genreOptionsAvailable": true, + "numberOfRows": 0, + "isDefaultSort" : true + }, {...}, ... ], + + timeFilters : [ + { + "token": "adfGad23", + "name": "All Time", + }, {...}, ... ], + + genreFilters : [ + { + "token": "fgjfdl23f", + "name": "Adventure", + }, {...}, ...], + + gameFilters : [ + { + "token": "SDFGjkd3", + "name": "Classic", + }, {...}, ... ] + } +]] + +-- requestImpl - (function>(url, requestMethod, options)) +-- sortCategory - (string/Constants.GameSortGroups.Games/HomeGames) which GameSortGroup does the sort belong to +return function(requestImpl, sortCategory) + + local url = string.format("%sv1/games/sorts?gameSortsContext=%s", + Url.GAME_URL, + Constants.ApiUsedForSorts[sortCategory]) + + -- return a promise of the result listed above + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetThumbnails.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetThumbnails.lua new file mode 100644 index 0000000..b382322 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesGetThumbnails.lua @@ -0,0 +1,47 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Url = require(Modules.LuaApp.Http.Url) + +--[[ + This endpoint returns a promise that resolves to: + [ + { + "final": true, + "url": "string", + "retryToken": "string", + "universeId": 0, + "placeId": 0 + }, {...}, ... + ] +]] + +-- requestImpl - (function>(url, requestMethod, options)) +-- imageTokens - (array) the placeIds of the places you want to get thumbnails for +-- height - (int) the height of the asset to render +-- width - (int) the width of the asset to render +return function(requestImpl, imageTokens, height, width) + local args = {} + + if height then + table.insert(args, string.format("height=%d", height)) + end + + if width then + table.insert(args, string.format("width=%d", width)) + end + + -- append all of the thumbnail tokens + local totalTokens = 0 + for _, value in pairs(imageTokens) do + totalTokens = totalTokens + 1 + table.insert(args, string.format("imageTokens=%s", value)) + end + if totalTokens == 0 then + error("cannot fetch thumbnails without tokens") + end + + -- construct the url + local url = string.format("%sv1/games/game-thumbnails?%s", Url.GAME_URL, table.concat(args, "&")) + + -- return a promise of the result listed above + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesPlayabilityStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesPlayabilityStatus.lua new file mode 100644 index 0000000..bb42945 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GamesPlayabilityStatus.lua @@ -0,0 +1,21 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Url = require(Modules.LuaApp.Http.Url) + +--[[ + This endpoint returns a promise that resolves to: + { + "playabilityStatus": "UnplayableOtherReason" + } + + requestImpl - (function>(url, requestMethod, options)) + universeId - universeId that is passed into the request +]]-- + +return function(requestImpl, universeId) + assert(type(universeId) == "string", "GamesPlayabilityStatus request expects universeId to be a string") + + local url = string.format("%s/v1/games/%s/playability-status", Url.GAME_URL, universeId) + + -- return a promise of the result listed above + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GetUnreadNotificationCount.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GetUnreadNotificationCount.lua new file mode 100644 index 0000000..8c38852 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/GetUnreadNotificationCount.lua @@ -0,0 +1,19 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Url = require(Modules.LuaApp.Http.Url) + +--[[ + This endpoint returns a promise that resolves to: + + { + "unreadNotifications": 0, + "statusMessage": "string" + } + +]]-- + +return function(requestImpl) + local url = string.format("%sv2/stream-notifications/unread-count", Url.NOTIFICATION_URL) + + -- return a promise of the result listed above + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/UsersGetFriendCount.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/UsersGetFriendCount.lua new file mode 100644 index 0000000..29816c0 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/UsersGetFriendCount.lua @@ -0,0 +1,30 @@ +local Players = game:GetService("Players") + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Url = require(Modules.LuaApp.Http.Url) + +--[[ + This endpoint returns a promise that resolves to: + + [ + { + "success:" true, + "count": "0" + }, + ] +]]-- + +-- requestImpl - (function>(url, requestMethod, options)) +return function(requestImpl, page) + + local argTable = { + userId = Players.LocalPlayer.UserId, + } + + local args = Url:makeQueryString(argTable) + local url = string.format("%s/user/get-friendship-count?%s", + Url.API_URL, tostring(Players.LocalPlayer.UserId), args + ) + + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/UsersGetFriends.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/UsersGetFriends.lua new file mode 100644 index 0000000..67410b9 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/UsersGetFriends.lua @@ -0,0 +1,10 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Url = require(Modules.LuaApp.Http.Url) + +return function(requestImpl, userId) + local url = string.format("%s/users/%s/friends", + Url.FRIEND_URL, userId + ) + + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/UsersGetPresence.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/UsersGetPresence.lua new file mode 100644 index 0000000..e48eb6b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/UsersGetPresence.lua @@ -0,0 +1,24 @@ +local HttpService = game:GetService("HttpService") +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Url = require(Modules.LuaApp.Http.Url) + +-- Endpoint documented here: +-- https://presence.roblox.com/docs + +return function(requestImpl, userIds) + local userIdsToNumber = {} + for _, id in pairs(userIds) do + local idToNumber = tonumber(id) + if idToNumber then + table.insert(userIdsToNumber, idToNumber) + end + end + + local payload = HttpService:JSONEncode({ + userIds = userIdsToNumber, + }) + + local url = string.format("%s/presence/users", Url.PRESENCE_URL) + + return requestImpl(url, "POST", { postBody = payload }) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/UsersGetThumbnail.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/UsersGetThumbnail.lua new file mode 100644 index 0000000..7f61c95 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Requests/UsersGetThumbnail.lua @@ -0,0 +1,46 @@ +local Players = game:GetService("Players") + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Promise = require(Modules.LuaApp.Promise) + +local THUMBNAIL_TYPE_BY_NAME = { + AvatarThumbnail = Enum.ThumbnailType.AvatarThumbnail, + HeadShot = Enum.ThumbnailType.HeadShot, +} + +local THUMBNAIL_SIZE_BY_NAME = { + Size48x48 = Enum.ThumbnailSize.Size48x48, + Size100x100 = Enum.ThumbnailSize.Size100x100, + Size150x150 = Enum.ThumbnailSize.Size150x150, +} + +return function(userId, thumbnailType, thumbnailSize) + return Promise.new(function(resolve, reject) + --Async methods will yield the thread + spawn(function() + local result = {success = false} + local success, message = pcall(function() + local image, isFinal = Players:GetUserThumbnailAsync( + userId, THUMBNAIL_TYPE_BY_NAME[thumbnailType], THUMBNAIL_SIZE_BY_NAME[thumbnailSize] + ) + + result = { + success = true, + id = userId, + thumbnailType = thumbnailType, + thumbnailSize = thumbnailSize, + + image = isFinal and image or nil, + isFinal = isFinal, + } + end) + + if success then + resolve(result) + else + result.message = message + reject(result) + end + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/StatusCodes.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/StatusCodes.lua new file mode 100644 index 0000000..42be9d2 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/StatusCodes.lua @@ -0,0 +1,16 @@ + +return { + UNKNOWN_ERROR = -1, + + OK = 200, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + FORBIDDEN = 403, + NOT_FOUND = 404, + REQUEST_TIMEOUT = 408, + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, + BAD_GATEWAY = 502, + SERVICE_UNAVAILABLE = 503, + GATEWAY_TIMEOUT = 504, +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Url.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Url.lua new file mode 100644 index 0000000..4201c06 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Url.lua @@ -0,0 +1,110 @@ +--[[ + Url Constructor + + Provides a single location for base urls. + +]]-- +local ContentProvider = game:GetService("ContentProvider") + +-- helper functions +local function parseBaseUrlInformation() + -- get the current base url from the current configuration + local baseUrl = ContentProvider.BaseUrl + + -- keep a copy of the base url (https://www.roblox.com/) + -- append a trailing slash if there isn't one + if baseUrl:sub(#baseUrl) ~= "/" then + baseUrl = baseUrl .. "/" + end + + -- parse out scheme (http, https) + local _, schemeEnd = baseUrl:find("://") + + -- parse out the prefix (www, kyle, ying, etc.) + local prefixIndex, prefixEnd = baseUrl:find("%.", schemeEnd + 1) + local basePrefix = baseUrl:sub(schemeEnd + 1, prefixIndex - 1) + + -- parse out the domain (roblox.com/, sitetest1.robloxlabs.com/, etc.) + local baseDomain = baseUrl:sub(prefixEnd + 1) + + return baseUrl, basePrefix, baseDomain +end +local function preventTableModification(aTable, key, value) + error("Attempt to modify read-only table") +end +local function createReadOnlyTable(aTable) + return setmetatable({}, { + __index = aTable, + __newindex = preventTableModification, + __metatable = false + }); +end + + +-- url construction building blocks +local _baseUrl, _basePrefix, _baseDomain = parseBaseUrlInformation() + +-- construct urls once +local _baseApiUrl = string.format("https://api.%s", _baseDomain) +local _baseAuthUrl = string.format("https://auth.%s", _baseDomain) +local _baseChatUrl = string.format("https://chat.%sv2", _baseDomain) +local _baseFriendUrl = string.format("https://friends.%sv1", _baseDomain) +local _baseGameAssetUrl = string.format("https://assetgame.%s", _baseDomain) +local _baseGamesUrl = string.format("https://games.%s", _baseDomain) +local _baseNotificationUrl = string.format("https://notifications.%s", _baseDomain) +local _basePresenceUrl = string.format("https://presence.%sv1", _baseDomain) +local _baseRealtimeUrl = string.format("https://realtime.%s", _baseDomain) +local _baseWebUrl = string.format("https://web.%s", _baseDomain) + +-- public api +local Url = { + DOMAIN = _baseDomain, + PREFIX = _basePrefix, + BASE_URL = _baseUrl, + API_URL = _baseApiUrl, + AUTH_URL = _baseAuthUrl, + GAME_URL = _baseGamesUrl, + GAME_ASSET_URL = _baseGameAssetUrl, + CHAT_URL = _baseChatUrl, + FRIEND_URL = _baseFriendUrl, + PRESENCE_URL = _basePresenceUrl, + NOTIFICATION_URL = _baseNotificationUrl, + REALTIME_URL = _baseRealtimeUrl, + WEB_URL = _baseWebUrl +} + +function Url:getUserProfileUrl(userId) + return string.format("%susers/%s/profile", self.BASE_URL, userId) +end + +function Url:isVanitySite() + return self.PREFIX ~= "www" +end + +-- data - (table) a table of key/value pairs to format +function Url:makeQueryString(data) + --NOTE - This function can be used to create a query string of parameters + -- at the end of url query, or create a application/form-url-encoded post body string + local params = {} + + -- NOTE - Arrays are handled, but generally data is expected to be flat. + for key, value in pairs(data) do + if value ~= nil then --for optional params + if type(value) == "table" then + for i = 1, #value do + table.insert(params, key .. "=" .. value[i]) + end + else + table.insert(params, key .. "=" .. tostring(value)) + end + end + end + + return table.concat(params, "&") +end + + +-- prevent anyone from modifying this table: +Url = createReadOnlyTable(Url) + +return Url \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Url.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Url.spec.lua new file mode 100644 index 0000000..62b8fab --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/Url.spec.lua @@ -0,0 +1,12 @@ +return function() + + local ContentProvider = game:GetService("ContentProvider") + local Url = require(script.Parent.Url) + + it("The base url has not been changed for debugging", function() + local baseUrl = ContentProvider.BaseUrl + + expect(baseUrl).to.equal(Url.BASE_URL) + end) + +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/request.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/request.lua new file mode 100644 index 0000000..7295869 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Http/request.lua @@ -0,0 +1,18 @@ +--[[ + Provides a configured networking stack to store in the ServiceProvider +]]-- +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local json = require(Modules.LuaApp.Http.NetworkLayers.json) +local requestDataModel = require(Modules.LuaApp.Http.NetworkLayers.requestDataModel) +local retry = require(Modules.LuaApp.Http.NetworkLayers.retry) +local timeout = require(Modules.LuaApp.Http.NetworkLayers.timeout) + +-- construct the networking stack +local request = requestDataModel() +request = timeout(request) -- the timeout default configuration is fine +request = retry(request) -- the retry default configuration is fine +request = json(request) + +-- RETURNS : function>(url, requestMethod, args) +return request \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AccessoriesColumn.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AccessoriesColumn.lua new file mode 100644 index 0000000..c2172b1 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AccessoriesColumn.lua @@ -0,0 +1,198 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) +local UnequipAsset = require(Modules.LuaApp.Actions.UnequipAsset) + +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfo) +local AssetInfo = require(Modules.LuaApp.Legacy.AvatarEditor.AssetInfo) +local Strings = require(Modules.LuaApp.Legacy.AvatarEditor.LocalizedStrings) +local categories = require(Modules.LuaApp.Legacy.AvatarEditor.Categories) +local tween = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController) +local Urls = require(Modules.LuaApp.Legacy.AvatarEditor.Urls) +local GetButtonClickEvent = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonClickEvent) + + +local accessoriesColumn = nil +local longPressMenu = nil + +local NUMBER_OF_ALLOWED_HATS = 3 + +local userInputService = game:GetService('UserInputService') + +local currentHats = {} +local this = {} + + +local function getCurrentHats() + return AppState.Store:getState().Character.Assets.Hat or {} +end + +local function updateAccessoriesColumnVisuals() + currentHats = getCurrentHats() + for i = 1, NUMBER_OF_ALLOWED_HATS do + local accessoryButton = accessoriesColumn:FindFirstChild('AccessoryButton'..tostring(i)) + if accessoryButton then + local assetId = currentHats[i] + if assetId then + if accessoryButton.Image ~= Urls.assetImageUrl..tostring(assetId) then + local numberOfHatsScale = (i - 1)/(NUMBER_OF_ALLOWED_HATS - 1) + delay(numberOfHatsScale * 0.25, function() + local basePosition = + LayoutInfo.isLandscape + and UDim2.new(0, 0, 0, 84 * (i - 1)) + or UDim2.new(0.5, -24, numberOfHatsScale, numberOfHatsScale * -48) + + local tweenInfoDown = TweenInfo.new( + 0.05, Enum.EasingStyle.Quad, Enum.EasingDirection.Out, 0, false, 0) + local tweenInfoUp = TweenInfo.new( + 0.15, Enum.EasingStyle.Back, Enum.EasingDirection.Out, 0, false, 0) + + tween( + accessoryButton, + tweenInfoDown, + { + Position = basePosition + UDim2.new(0, 0, 0, 15) + } + ).Completed:Connect( + function() + tween(accessoryButton, tweenInfoUp, { Position = basePosition }) + end + ) + accessoryButton.Image = Urls.assetImageUrl..tostring(currentHats[i]) + end) + end + else + if accessoryButton.Image ~= '' then + local tweenInfoLeft = TweenInfo.new( + 0.05, Enum.EasingStyle.Quad, Enum.EasingDirection.Out, 0, false, 0) + local tweenInfoRight = TweenInfo.new( + 0.05, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + local tweenInfoMiddle = + LayoutInfo.isLandscape + and tweenInfoLeft + or TweenInfo.new(0.25, Enum.EasingStyle.Back, Enum.EasingDirection.Out, 0, false, 0) + + local basePosition = + LayoutInfo.isLandscape + and UDim2.new(0, 0, 0, 84 * (i - 1)) + or UDim2.new(0.5, -24, accessoryButton.Position.Y.Scale, accessoryButton.Position.Y.Offset) + + tween(accessoryButton, tweenInfoLeft, + { + Position = basePosition + UDim2.new(0, -15, 0, 0) + } + ).Completed:Connect( + function() + tween(accessoryButton, tweenInfoRight, + { Position = basePosition + UDim2.new(0, 15, 0, 0) }).Completed:Connect( + function() + tween(accessoryButton, tweenInfoMiddle, { Position = basePosition }) + end + ) + end + ) + + accessoryButton.Image = '' + end + end + + local fadeIcon = accessoryButton:FindFirstChild('FadeIcon') --This is the faded icon of an accessory + if fadeIcon then + fadeIcon.Visible = not assetId --Make the faded icon visible if there is no asset in the slot + end + end + end +end + +local function updateViewMode(isFullView) + local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + if isFullView then + if LayoutInfo.isLandscape then + tween(accessoriesColumn, tweenInfo, { Position = UDim2.new(0, -156, 0, 76) }) + else + tween(accessoriesColumn, tweenInfo, { Position = UDim2.new(-0.1, -50, 0.05, 0) }) + end + else + if LayoutInfo.isLandscape then + tween(accessoriesColumn, tweenInfo, { Position = UDim2.new(0, 56,0, 76) }) + else + tween(accessoriesColumn, tweenInfo, { Position = UDim2.new(0.05, 25, 0.05, 0) }) + end + end +end + +local function updateState(newState, oldState) + if newState.FullView ~= oldState.FullView then + updateViewMode(newState.FullView) + end + + -- Either CategoryIndex changed (by selecting new category) or TabsInfo changed (by selecting new tab) + if newState.Category.CategoryIndex ~= oldState.Category.CategoryIndex or + newState.Category.TabsInfo ~= oldState.Category.TabsInfo then + local categoryIndex = newState.Category.CategoryIndex + local tabInfo = categoryIndex and newState.Category.TabsInfo[categoryIndex] + local tabIndex = tabInfo and tabInfo.TabIndex or 1 + local desiredPage = categories[categoryIndex].pages[tabIndex] + accessoriesColumn.Visible = desiredPage.name == 'Hats' + end + + if newState.Character.Assets.Hat ~= oldState.Character.Assets.Hat then + updateAccessoriesColumnVisuals() + end +end + +local function init() + if LayoutInfo.isLandscape then + accessoriesColumn.AnchorPoint = Vector2.new(0, 0) + accessoriesColumn.Position = UDim2.new(0, 24, 0, 24) + accessoriesColumn.Size = UDim2.new(0, 60, 0.3, 30) + for i = 1, 3 do + accessoriesColumn['AccessoryButton'..i].Position = UDim2.new(0.5, -30, 0, (i - 1) * 84) + accessoriesColumn['AccessoryButton'..i].Size = UDim2.new(0, 60, 0, 60) + end + end + + currentHats = getCurrentHats() + for index = 1, NUMBER_OF_ALLOWED_HATS do + local accessoryButton = accessoriesColumn:WaitForChild('AccessoryButton'..tostring(index)) + if accessoryButton then + + local takeOffFunction = function() + local assetId = currentHats[index] + if assetId then + longPressMenu:hideMenu() + AppState.Store:dispatch(UnequipAsset(AssetInfo.getAssetType(assetId), assetId)) + end + end + + local showMenuFunction = function() + local assetId = currentHats[index] + if assetId then + longPressMenu:showMenu({text = Strings:LocalizedString("TakeOffWord"), func = takeOffFunction}, assetId) + end + end + + local clickFunction = function() + if userInputService:IsKeyDown(Enum.KeyCode.Q) then + showMenuFunction() + else + takeOffFunction() + end + end + + GetButtonClickEvent(accessoryButton):connect(clickFunction) + accessoryButton.TouchLongPress:connect(showMenuFunction) + end + end +end + +return function(inAccessoriesColumn, inLongPressMenu) + accessoriesColumn = inAccessoriesColumn + longPressMenu = inLongPressMenu + init() + + AppState.Store.Changed:Connect(updateState) + + return this +end + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/ActionReporter.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/ActionReporter.lua new file mode 100644 index 0000000..1cf38f3 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/ActionReporter.lua @@ -0,0 +1,100 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local Analytics = require(Modules.Common.Analytics) + +local analytics = nil +local function getAnalytics() + analytics = analytics or Analytics.new() + return analytics +end + + +local function ValuesAsCommaSeparatedString(myTable) + local result = "" + for _,v in pairs(myTable) do + result = result .. (result ~= "" and ", " or "") + result = result .. tostring(v) + end + return result +end + + +return function(store) + local this = store + + local function sendEvent(context, propertyName, value) + local state = this:getState() + + local currentCategory = state.Category or {} + local currentCategoryIndex = currentCategory.CategoryIndex or 1 + local tabInfo = state.Category.TabsInfo[currentCategoryIndex] + local tabIndex = tabInfo and tabInfo.TabIndex or 1 + + getAnalytics().EventStream:setRBXEventStream( + context, + "avatarEditorPropertyChanged", + { + prop = tostring(propertyName), + val = tostring(value), + ci = tostring(currentCategoryIndex), + ti = tostring(tabIndex) + } + ) + end + + local handler = {} + + function handler.EquipAsset(action) + sendEvent( action.type, action.assetType, + ValuesAsCommaSeparatedString(this:getState().Character.Assets[action.assetType]) ) + end + + function handler.UnequipAsset(action) + sendEvent( action.type, action.assetType, + ValuesAsCommaSeparatedString(this:getState().Character.Assets[action.assetType]) ) + end + + function handler.SetAvatarHeadSize(action) + sendEvent( action.type, "HeadSize", action.head ) + end + + function handler.SetAvatarHeight(action) + sendEvent( action.type, "Height", action.height ) + end + + function handler.SetAvatarType(action) + sendEvent( action.type, "Type", action.avatarType ) + end + + function handler.ToggleAvatarType(action) + sendEvent( action.type, "Type", this:getState().Character.AvatarType ) + end + + function handler.SetAvatarWidth(action) + sendEvent( action.type, "Width", action.width ) + end + + function handler.SetBodyColors(action) + local bodyColors = action.bodyColors or {} + local commonColor = bodyColors[next(bodyColors)] or "empty" + + for _,color in pairs(action.bodyColors) do + commonColor = (color == commonColor) and commonColor or "mixed" + end + + sendEvent( action.type, "BodyColors", commonColor ) + end + + local baseDispatch = this.dispatch + function this:dispatch(action) + local result = baseDispatch(self, action) + + local handlerFunction = handler[action.type] or function() end + handlerFunction(action) + + return result + end + + return this +end + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AppGui.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AppGui.lua new file mode 100644 index 0000000..359b532 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AppGui.lua @@ -0,0 +1,34 @@ +local ServerStorage = game:GetService('ServerStorage') +local StarterGui = game:GetService('StarterGui') + +local RobloxGui = game:GetService("CoreGui").RobloxGui +local Create = require(RobloxGui.Modules.Mobile.Create) + +return function(position, size) + local AppGui = {} + + AppGui.ScreenGui = ServerStorage:WaitForChild('ScreenGuiV2'):Clone() + AppGui.ScreenGui.Name = 'ScreenGui' + AppGui.ScreenGui.DisplayOrder = 2 + + AppGui.RootGui = Create"Frame" + { + Position = position; + Size = size; + BackgroundTransparency = 1; + Name = "RootGui"; + } + + for _,child in pairs(AppGui.ScreenGui:GetChildren()) do + child.Parent = AppGui.RootGui + end + AppGui.RootGui.Parent = AppGui.ScreenGui + + function AppGui:setDimensions(position, size) + self.RootGui.Position = position + self.RootGui.Size = size + end + + return AppGui; +end + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AppScene.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AppScene.lua new file mode 100644 index 0000000..ea06759 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AppScene.lua @@ -0,0 +1,20 @@ +local ReplicatedStorage = game:GetService('ReplicatedStorage') + +local AppScene = {} + +function AppScene:Init() + if not self.RootScene then + self.RootScene = ReplicatedStorage:WaitForChild('AvatarEditorScene'):Clone() + self.RootScene.Name = 'AvatarEditorScene' + end +end + +local appScene = game:GetService('Workspace'):FindFirstChild('AvatarEditorScene') + +if appScene then + AppScene.RootScene = appScene +else + AppScene:Init() +end + +return AppScene \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AppState.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AppState.lua new file mode 100644 index 0000000..520796a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AppState.lua @@ -0,0 +1,19 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local AvatarReducer = require(Modules.LuaApp.Reducers.AvatarEditor.Avatar) +local Store = require(Modules.Common.Rodux).Store +local ActionReporter = require(Modules.LuaApp.Legacy.AvatarEditor.ActionReporter) + +local AppState = {} + +function AppState:Init() + self.Store = ActionReporter(Store.new(AvatarReducer)) +end + +function AppState:Destruct() + self.Store:destruct() +end + +AppState:Init() + +return AppState diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AssetInfo.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AssetInfo.lua new file mode 100644 index 0000000..a1cf860 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AssetInfo.lua @@ -0,0 +1,42 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local marketplaceService = game:GetService('MarketplaceService') + +local assetTypeNames = require(Modules.LuaApp.Legacy.AvatarEditor.AssetTypeNames) + +local this = {} +local cachedAssetInfo = {} +function this.getAssetInfo(assetId) + if assetId then + local assetInfo = cachedAssetInfo['id'..assetId] + if assetInfo then + return assetInfo + end + + local success, assetData = pcall(function() + return marketplaceService:GetProductInfo(assetId) + end) + if success and assetData then + assetInfo = assetData + cachedAssetInfo['id'..assetId] = assetInfo + return assetInfo + end + end +end + +function this.setCachedAssetInfo(assetId, info) + cachedAssetInfo['id'..assetId] = info +end + +function this.getAssetType(assetId) + local assetInfo = this.getAssetInfo(assetId) + + if assetInfo == nil then + return 'UNKNOWN_ASSET_TYPE' + end + + return assetTypeNames[assetInfo.AssetTypeId] or 'UNKNOWN_ASSET_TYPE' +end + +return this + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AssetTypeNames.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AssetTypeNames.lua new file mode 100644 index 0000000..fc9ec28 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AssetTypeNames.lua @@ -0,0 +1,67 @@ + +local ASSET_TO_STRING = { + [1] = "Image"; + [2] = "T-Shirt"; + [3] = "Audio"; + [4] = "Mesh"; + [5] = "Lua"; + [6] = "HTML"; + [7] = "Text"; + [8] = "Hat"; + [9] = "Place"; + [10] = "Model"; + [11] = "Shirt"; + [12] = "Pants"; + [13] = "Decal"; + [16] = "Avatar"; + [17] = "Head"; + [18] = "Face"; + [19] = "Gear"; + [21] = "Badge"; + [22] = "Group Emblem"; + [24] = "Animation"; + [25] = "Arms"; + [26] = "Legs"; + [27] = "Torso"; + [28] = "Right Arm"; + [29] = "Left Arm"; + [30] = "Left Leg"; + [31] = "Right Leg"; + [32] = "Package"; + [33] = "YouTube Video"; + -- NOTE: GamePass and Plugin AssetTypeIds are different on ST1, ST2 and ST3 + + [34] = "Game Pass"; + [38] = "Plugin"; + [41] = "Hair Accessory"; + [42] = "Face Accessory"; + [43] = "Neck Accessory"; + [44] = "Shoulder Accessory"; + [45] = "Front Accessory"; + [46] = "Back Accessory"; + [47] = "Waist Accessory"; + + [48] = "Climb Animation"; + [49] = "Death Animation"; + [50] = "Fall Animation"; + [51] = "Idle Animation"; + [52] = "Jump Animation"; + [53] = "Run Animation"; + [54] = "Swim Animation"; + [55] = "Walk Animation"; + [56] = "Pose Animation"; + + [57] = "Ear Accessory"; + [58] = "Eye Accessory"; + + [0] = "Product"; +} + +local reversible = {} +for id, name in next, ASSET_TO_STRING do + reversible[id] = name + reversible[name] = id + reversible[name:gsub(' ','')] = id +end + +return reversible diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AvatarEditorMain.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AvatarEditorMain.lua new file mode 100644 index 0000000..8953286 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AvatarEditorMain.lua @@ -0,0 +1,512 @@ +local NotificationService = game:GetService("NotificationService") +local userInputService = game:GetService('UserInputService') +local sharedStorage = game:GetService('ReplicatedStorage') +local CoreGui = game:GetService("CoreGui") +local GuiService = game:GetService("GuiService") +local RobloxGui = CoreGui:FindFirstChild("RobloxGui") +local Modules = RobloxGui.Modules + +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) +local AppGui = require(Modules.LuaApp.Legacy.AvatarEditor.AppGui) +local AppScene = require(Modules.LuaApp.Legacy.AvatarEditor.AppScene) + +local AvatarEditorScene = AppScene.RootScene + +local NotificationType = require(Modules.LuaApp.Enum.NotificationType) + +local CreateCharacterManager = require(Modules.LuaApp.Legacy.AvatarEditor.CharacterManager) +local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local tween = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController) + +local spriteManager = require(Modules.LuaApp.Legacy.AvatarEditor.SpriteSheetManager) +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfo) +local CameraController = require(Modules.LuaApp.Legacy.AvatarEditor.CameraController) + +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) +local BackButtonConnection = nil +local UseNewAELoadTimeMetrics = settings():GetFFlag("UseNewAELoadTimeMetrics") +local UseAndroidBackButton = settings():GetFFlag("EnableAEAndroidBackButton") + +while not game:GetService('Players').LocalPlayer do wait() end + + +return { new = function(topOffset_or_AppGui) + +local appGui = topOffset_or_AppGui + +local camera = game.Workspace.CurrentCamera +local topFrame = appGui.RootGui:WaitForChild('TopFrame') +local mainFrame = appGui.RootGui:WaitForChild('Frame') +local scrollingFrame = mainFrame:WaitForChild('ScrollingFrame') +local equippedFrameTemplate = appGui.RootGui:WaitForChild('SelectionFrameTemplate') +local fakeScrollBar = mainFrame:WaitForChild('FakeScrollBar') +local detailsMenuFrame = appGui.RootGui:WaitForChild('DetailsFrame') + +-- Add the gui into the renderable PlayerGui (this does not +-- happen automatically because characterAutoLoads is false) +local starterGuiChildren = game.StarterGui:GetChildren() +for i = 1, #starterGuiChildren do + local guiClone = starterGuiChildren[i]:clone() + guiClone.Parent = RobloxGui.Parent +end + + +scrollingFrame.ClipsDescendants = true +if LayoutInfo.isLandscape then + scrollingFrame.Position = UDim2.new(0, 108, 0, 0) + scrollingFrame.Size = UDim2.new(1, -111, 1, 0) +else + scrollingFrame.Position = UDim2.new(0, 0, 0, 50) + scrollingFrame.Size = UDim2.new(1, 0, 1, -50) +end + + +spriteManager.equipDescendants(appGui.RootGui) + + +if LayoutInfo.isLandscape then + topFrame.Size = UDim2.new(0.5, 0, 1, 0) + + mainFrame.Position = UDim2.new(0.5, -60, 0, 0) + mainFrame.Size = UDim2.new(0.5, 60, 1, 0) + mainFrame.BackgroundColor3 = Color3.new(0, 0, 0) + mainFrame.BackgroundTransparency = 0.5 + + scrollingFrame.Size = UDim2.new(1, -128, 1, 0) + scrollingFrame.Position = UDim2.new(0, 116, 0, 0) + + detailsMenuFrame.BackgroundTransparency = 1 + detailsMenuFrame.ClipsDescendants = false + detailsMenuFrame.Background.Visible = true +end + + +local function GetNameValue(playerName) + local value = 0 + for index = 1, #playerName do + local cValue = string.byte(string.sub(playerName, index, index)) + local reverseIndex = #playerName - index + 1 + if #playerName%2 == 1 then + reverseIndex = reverseIndex - 1 + end + if reverseIndex%4 >= 2 then + cValue = -cValue + end + value = value + cValue + end + return value +end + + +local characterManager = CreateCharacterManager( + { + get = function(url) + return Utilities.httpGet(url) + end; + + post = function(url, data) + return Utilities.httpPost(url, data) + end; + }, + { + CharacterR6 = sharedStorage:WaitForChild('CharacterR6'); + CharacterR15 = sharedStorage:WaitForChild('CharacterR15'); + CharacterR15New = sharedStorage:WaitForChild('CharacterR15New'); + }, + GetNameValue(game.Players.LocalPlayer.Name)+1 +) + +local WarningWidget = require(Modules.LuaApp.Legacy.AvatarEditor.WarningWidget)(topFrame.Warning, characterManager) + +local function updateViewMode(desiredViewMode) + local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + if desiredViewMode then + if LayoutInfo.isLandscape then + tween(mainFrame, tweenInfo, { Position = UDim2.new(1, 0, 0, 0) }) + tween(topFrame, tweenInfo, { Size = UDim2.new(1, 0, 1, 0) }) + else + tween(mainFrame, tweenInfo, { Position = UDim2.new(0, 0, 1, 0) }) + tween(topFrame, tweenInfo, { Size = UDim2.new(1, 0, 1, 0) }) + end + else + if LayoutInfo.isLandscape then + tween(mainFrame, tweenInfo, { Position = UDim2.new(0.5, -60, 0, 0) }) + tween(topFrame, tweenInfo, { Size = UDim2.new(0.5, 0, 1, 0) }) + else + tween(mainFrame, tweenInfo, { Position = UDim2.new(0, 0, .5, -18) }) + tween(topFrame, tweenInfo, { Size = UDim2.new(1, 0, 0.5, -18) }) + end + end +end + +require(Modules.LuaApp.Legacy.AvatarEditor.AvatarTypeSwitch)( + appGui.RootGui:WaitForChild('AvatarTypeSwitch')) + +characterManager.initFromServer() + + +local longPressMenu = require(Modules.LuaApp.Legacy.AvatarEditor.LongPressMenu)( + characterManager, + appGui.RootGui:WaitForChild('ShadeLayer'), + appGui.RootGui:WaitForChild('MenuFrame'), + appGui.RootGui:WaitForChild('DetailsFrame') +) + +require(Modules.LuaApp.Legacy.AvatarEditor.AccessoriesColumn)( + appGui.RootGui:WaitForChild('AccessoriesColumn'), longPressMenu) + +local PageManager = require(Modules.LuaApp.Legacy.AvatarEditor.PageManagerMobile)( + game.Players.LocalPlayer.userId, + equippedFrameTemplate, + scrollingFrame, + characterManager, + longPressMenu) + +local categoryButtonTemplate = LayoutInfo.isLandscape and appGui.RootGui:WaitForChild('TabletCategoryButtonTemplate') or + appGui.RootGui:WaitForChild('CategoryButtonTemplate') +local CategoryMenu = require(Modules.LuaApp.Legacy.AvatarEditor.CategoryMenu)( + mainFrame:WaitForChild('TopMenuContainer'), + categoryButtonTemplate) + +require(Modules.LuaApp.Legacy.AvatarEditor.DarkCoverManager)( + appGui.RootGui:WaitForChild('DarkCover'), CategoryMenu) + +require(Modules.LuaApp.Legacy.AvatarEditor.TabList)(CategoryMenu, + mainFrame:WaitForChild('TabList'), + mainFrame:WaitForChild('TabListContainer')) + + + +camera.CameraType = Enum.CameraType.Scriptable +camera.CFrame = CFrame.new( + LayoutInfo.CameraDefaultPosition, + LayoutInfo.CameraDefaultFocusPoint) +camera.FieldOfView = 70 + +local cameraController = CameraController( + { + tweenCamera = function(targetCFrame, targetFOV) + local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + local propGoals = { + CFrame = targetCFrame; + FieldOfView = targetFOV; + } + tween(camera, tweenInfo, propGoals) + end + }, + LayoutInfo.CameraCenterScreenPosition +) + +characterManager:setUpdateCameraCallback(cameraController.updateCamera) + + + +--This function fades the fake scrollbar out after not being used for 3 seconds +local lastScrollPosition = fakeScrollBar.AbsolutePosition.Y +local lastScrollCount = 0 +local function updateScrollBarVisibility() + local thisScrollPosition = fakeScrollBar.AbsolutePosition.Y + if thisScrollPosition ~= lastScrollPosition then + lastScrollPosition = thisScrollPosition + + lastScrollCount = lastScrollCount + 1 + local thisScrollCount = lastScrollCount + + fakeScrollBar.ImageTransparency = .65 + wait(2) + if thisScrollCount == lastScrollCount then + local tweenInfo = TweenInfo.new(1, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + tween(fakeScrollBar, tweenInfo, { ImageTransparency = 1 }).Completed:wait() + end + end +end + +local fakeScrollBarWidth = 4 + +scrollingFrame.Changed:connect(function(prop) + local barSize = 0 + local scrollingFrameSize = scrollingFrame.AbsoluteSize.Y + local canvasPosition = scrollingFrame.CanvasPosition.Y + local canvasSize = scrollingFrame.CanvasSize.Y.Offset + if scrollingFrameSize < canvasSize then + barSize = (scrollingFrameSize / canvasSize) * scrollingFrameSize + end + if barSize > 0 then + fakeScrollBar.Visible = true + fakeScrollBar.Size = UDim2.new(0,fakeScrollBarWidth,0,barSize) + local scrollPercent = canvasPosition/(canvasSize-scrollingFrameSize) + fakeScrollBar.Position = + UDim2.new( 1, -fakeScrollBarWidth, 0, (scrollingFrameSize-barSize) * scrollPercent ) + + scrollingFrame.Position + else + fakeScrollBar.Visible = false + end + + Utilities.fastSpawn(updateScrollBarVisibility) + + if prop == 'CanvasPosition' then + PageManager:updateListContent(canvasPosition) + end +end) + + +AppState.Store.Changed:Connect(function(newState, oldState) + if newState.FullView ~= oldState.FullView then + updateViewMode(newState.FullView) + end +end) + + +local SetAvatarEditorFullView = require(Modules.LuaApp.Actions.SetAvatarEditorFullView) + +local function setViewMode(desiredViewMode) + if desiredViewMode ~= AppState.Store:getState().FullView then + AppState.Store:dispatch(SetAvatarEditorFullView(desiredViewMode)) + end +end + +local rotation = 0 +local lastRotation = 0 +local downKeys = {} +local lastTouchInput = nil +local lastTouchPosition = nil +local lastInputBeganPosition = Vector3.new(0,0,0) +local lastEmptyInput = 0 +local characterRotationSpeed = .0065 +local tapDistanceThreshold = 10 -- maximum distance between input begin and end for it to count as a tap +local doubleTapThreshold = .25 + +local function handleInput(input, soaked) + if input.UserInputState == Enum.UserInputState.Begin then + downKeys[input.KeyCode] = true + if not soaked then + + if input.UserInputType == Enum.UserInputType.MouseButton1 then + downKeys[Enum.UserInputType.MouseButton1] = true + lastTouchPosition = input.Position + lastTouchInput = input + end + if input.UserInputType == Enum.UserInputType.Touch then + lastTouchInput = input + lastTouchPosition = input.Position + end + + if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then + --This is used for doubletap detection + lastInputBeganPosition = input.Position + end + end + elseif input.UserInputState == Enum.UserInputState.End then + downKeys[input.KeyCode] = false + if input.UserInputType == Enum.UserInputType.MouseButton1 then + downKeys[Enum.UserInputType.MouseButton1] = false + end + if lastTouchInput == input or input.UserInputType == Enum.UserInputType.MouseButton1 then + lastTouchInput = nil + end + + if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then + if (lastInputBeganPosition and lastInputBeganPosition - input.Position).magnitude <= tapDistanceThreshold then + local thisEmptyInput = tick() + if thisEmptyInput - lastEmptyInput <= doubleTapThreshold then + setViewMode(not AppState.Store:getState().FullView) + end + lastEmptyInput = thisEmptyInput + end + end + elseif input.UserInputState == Enum.UserInputState.Change then + if (lastTouchInput == input and input.UserInputType == Enum.UserInputType.Touch) + or (input.UserInputType == Enum.UserInputType.MouseMovement and downKeys[Enum.UserInputType.MouseButton1]) then + + local touchDelta = (input.Position - lastTouchPosition) + lastTouchPosition = input.Position + rotation = rotation + touchDelta.x * characterRotationSpeed + end + end +end + +local function onLastInputTypeChanged(inputType) + local isGamepad = inputType.Name:find('Gamepad') + local isTouch = inputType == Enum.UserInputType.Touch + local isMouse = inputType.Name:find('Mouse') or inputType == Enum.UserInputType.Keyboard + + if not isGamepad and not isTouch and not isMouse then + return + end + + if isGamepad then + userInputService.MouseIconEnabled = false + else + userInputService.MouseIconEnabled = true + end +end + +userInputService.LastInputTypeChanged:connect(onLastInputTypeChanged) +onLastInputTypeChanged(userInputService:GetLastInputType()) + +local function lockPart(part) + if part:IsA('BasePart') then + part.Locked = true + end +end + +game.Workspace.DescendantAdded:connect(lockPart) +for _, v in next, Utilities.getDescendants(game.Workspace) do + lockPart(v) +end + +game.OnClose = function() + -- Passing true here makes the save function block until the save is complete, + -- otherwise, the game shutdown process will probably kill the threads it spanws. + characterManager.saveToServer(true) +end + +require(Modules.LuaApp.Legacy.AvatarEditor.FullView)( + topFrame:WaitForChild('FullViewButton')) + + + +local rotationalInertia = .9 + +spawn(function() + local rotationalMomentum = 0 + local delta = 0 + + while true do + local tilt = 0 + local offsetTheseRots = 0 + + if downKeys[Enum.KeyCode.Left] or downKeys[Enum.KeyCode.A] then + rotation = rotation - delta*math.rad(180) + elseif downKeys[Enum.KeyCode.Right] or downKeys[Enum.KeyCode.D] then + rotation = rotation + delta*math.rad(180) + end + + if userInputService.GamepadEnabled then + for _, gamepad in next, userInputService:GetNavigationGamepads() do + local state = userInputService:GetGamepadState(gamepad) + + for _, obj in next, state do + if obj.KeyCode == Enum.KeyCode.Thumbstick2 then + if math.abs(obj.Position.x) > 0.25 then + local deltaRotation = obj.Position.x*delta*math.rad(180) + rotation = rotation + deltaRotation + if userInputService.TouchEnabled or userInputService.MouseEnabled then + offsetTheseRots = offsetTheseRots - deltaRotation + end + end + if math.abs(obj.Position.y) > 0.25 then + tilt = tilt + obj.Position.y*math.rad(45) + end + end + end + end + end + + if lastTouchInput then + rotationalMomentum = rotation - lastRotation + elseif rotationalMomentum ~= 0 then + rotationalMomentum = rotationalMomentum * rotationalInertia + if math.abs(rotationalMomentum) < .001 then + rotationalMomentum = 0 + end + rotation = rotation + rotationalMomentum + end + + characterManager.setRotation(rotation) + lastRotation = rotation + + delta = Utilities.renderWait() + end +end) + + + +appGui.ScreenGui.Parent = CoreGui +appGui.ScreenGui.Enabled = false + +local AppMain = {} + +AppMain.appGui = appGui.ScreenGui + +local inputBeganConnection = nil +local inputChangedConnection = nil +local inputEndedConnection = nil +local touchSwipeConnection = nil + +function AppMain:Start() + if inputBeganConnection == nil or not inputBeganConnection.Connected then + inputBeganConnection = userInputService.InputBegan:connect(handleInput) + end + if inputChangedConnection == nil or not inputChangedConnection.Connected then + inputChangedConnection = userInputService.InputChanged:connect(handleInput) + end + if inputEndedConnection == nil or not inputEndedConnection.Connected then + inputEndedConnection = userInputService.InputEnded:connect(handleInput) + end + + if touchSwipeConnection == nil or not touchSwipeConnection.Connected then + touchSwipeConnection = userInputService.TouchSwipe:connect(function(swipeDirection, numberOfTouches, soaked) + if not soaked and not LayoutInfo.isLandscape then + if AppState.Store:getState().FullView and swipeDirection == Enum.SwipeDirection.Up then + setViewMode(false) + elseif not AppState.Store:getState().FullView and swipeDirection == Enum.SwipeDirection.Down then + setViewMode(true) + end + end + end) + end + + appGui.ScreenGui.Enabled = true + AvatarEditorScene.Parent = game.Workspace + cameraController:Focus() + characterManager.show() + WarningWidget:Focus() + + -- Adding back button functionality for Android + if UseAndroidBackButton then + local success, res = pcall(function() + return GuiService.ShowLeaveConfirmation:Connect(function() + GuiService:BroadcastNotification("", NotificationType.BACK_BUTTON_NOT_CONSUMED) + end) + end) + + if success then + BackButtonConnection = res + end + end + + if UseNewAELoadTimeMetrics then + NotificationService:ActionEnabled(Enum.AppShellActionType.AvatarEditorPageLoaded) + end +end + +function AppMain:Stop() + if inputBeganConnection ~= nil then + inputBeganConnection:Disconnect() + end + if inputChangedConnection ~= nil then + inputChangedConnection:Disconnect() + end + if inputEndedConnection ~= nil then + inputEndedConnection:Disconnect() + end + if touchSwipeConnection ~= nil then + touchSwipeConnection:Disconnect() + end + + appGui.ScreenGui.Enabled = false + AvatarEditorScene.Parent = nil + cameraController:RemoveFocus() + characterManager.hide() + WarningWidget:RemoveFocus() + + if BackButtonConnection then + BackButtonConnection:Disconnect() + end +end + +return AppMain; + + +end } \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AvatarTypeSwitch.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AvatarTypeSwitch.lua new file mode 100644 index 0000000..9cee7c1 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/AvatarTypeSwitch.lua @@ -0,0 +1,74 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) +local ToggleAvatarType = require(Modules.LuaApp.Actions.ToggleAvatarType) +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfo) +local Tween = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController) +local GetButtonClickEvent = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonClickEvent) + +return function(AvatarTypeFrame) + local avatarTypeButton = AvatarTypeFrame.ButtonSoak + local toggleLabel = AvatarTypeFrame.Switch + local r6Label = AvatarTypeFrame.R6Label + local r15Label = AvatarTypeFrame.R15Label + + AvatarTypeFrame.Position = LayoutInfo.AvatarTypeSwitchInitialPosition + r6Label.TextSize = LayoutInfo.AvatarTypeSwitchTextSize + r15Label.TextSize = LayoutInfo.AvatarTypeSwitchTextSize + + -- not sure if this will need to be cleaned up, will depend on what we do when we get multiple apps hooked up together + -- but for now, this toggle is always on screen so no need to clean up right now + local isInitialized = false + + local function updateAvatarType(avatarType) + if avatarType == "R6" then + r6Label.TextColor3 = LayoutInfo.AvatarTypeSwitchOnColor + r15Label.TextColor3 = LayoutInfo.AvatarTypeSwitchOffColor + else + r6Label.TextColor3 = LayoutInfo.AvatarTypeSwitchOffColor + r15Label.TextColor3 = LayoutInfo.AvatarTypeSwitchOnColor + end + + local positionGoal = avatarType == "R6" and UDim2.new(0, 2, 0, 2) or UDim2.new(1, -32, 0, 2) + + -- don't tween when we first set this up + if not isInitialized then + isInitialized = true + toggleLabel.Position = positionGoal + return + end + + local tweenInfo = TweenInfo.new(0.1, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + local tweenGoals = { + Position = positionGoal + } + Tween(toggleLabel, tweenInfo, tweenGoals) + end + + local function updateOnFullViewChanged(isFullView) + local finalPosition = isFullView and + LayoutInfo.AvatarTypeSwitchPositionFullView or + LayoutInfo.AvatarTypeSwitchPosition + + local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + + local tweenGoals = { + Position = finalPosition + } + Tween(AvatarTypeFrame, tweenInfo, tweenGoals) + end + + AppState.Store.Changed:Connect(function(newState, oldState) + if newState.Character.AvatarType ~= oldState.Character.AvatarType then + updateAvatarType(newState.Character.AvatarType) + elseif newState.FullView ~= oldState.FullView then + updateOnFullViewChanged(newState.FullView) + end + end) + + local function onAvatarTypeClicked() + AppState.Store:dispatch(ToggleAvatarType()) + end + + GetButtonClickEvent(avatarTypeButton):connect(onAvatarTypeClicked) +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CameraController.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CameraController.lua new file mode 100644 index 0000000..1e78bde --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CameraController.lua @@ -0,0 +1,94 @@ +local CoreGui = game:GetService("CoreGui") +local GuiRoot = CoreGui:FindFirstChild('RobloxGui') +local Modules = GuiRoot:FindFirstChild('Modules') +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) +local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) + + +local function getScreenSize() + return game.Workspace.CurrentCamera.ViewportSize +end + +return function(cameraTweener, cameraCenterScreenPosition) + local this = {} + local storeChangedCn + local fullView = false + + local currentCameraPosition = Vector3.new(7.2618074, 4.74155569, -22.701086) + local currentCameraFocusPoint = Vector3.new(15.2762003, 3.28499985, -16.8211994) + local currentCameraFov = 0 + + local function tweenCameraIntoPlace(position, focusPoint, targetFOV) + local screenWidth = getScreenSize().X + local screenHeight = getScreenSize().Y + + local fy = 0.5 * targetFOV * math.pi / 180.0 -- half vertical field of view (in radians) + local fx = math.atan( math.tan(fy) * screenWidth / screenHeight ) -- half horizontal field of view (in radians) + + local anglesX = math.atan( math.tan(fx) + * (cameraCenterScreenPosition.X.Scale + 2.0 * cameraCenterScreenPosition.X.Offset / screenWidth)) + local anglesY = math.atan( math.tan(fy) + * (cameraCenterScreenPosition.Y.Scale + 2.0 * cameraCenterScreenPosition.Y.Offset / screenHeight)) + + local targetCFrame + = CFrame.new(position) + * CFrame.new(Vector3.new(), focusPoint-position) + * CFrame.Angles(anglesY,anglesX,0) + + cameraTweener.tweenCamera(targetCFrame, targetFOV) + end + + function this.tweenCameraToFullView() + fullView = true + + local targetCFrame = CFrame.new( + 13.2618074, 4.74155569, -22.701086, + -0.94241035, 0.0557777137, -0.329775006, + 0.000000000, 0.98599577, 0.166770056, + 0.334458828, 0.157165825, -0.92921263) + + cameraTweener.tweenCamera(targetCFrame, 70) + end + + + function this.tweenCameraToPageView() + fullView = false + + tweenCameraIntoPlace( + currentCameraPosition, + currentCameraFocusPoint, + currentCameraFov) + end + + + function this.updateCamera(position, focusPoint, fov) + currentCameraPosition = position + currentCameraFocusPoint = focusPoint + currentCameraFov = fov + + if not fullView then + tweenCameraIntoPlace(position, focusPoint, fov) + end + end + + local function update(newState, oldState) + if newState.FullView ~= oldState.fullView then + if newState.FullView == true then + this.tweenCameraToFullView() + else + this.tweenCameraToPageView() + end + end + end + + function this:Focus() + storeChangedCn = AppState.Store.Changed:Connect(update) + end + + function this:RemoveFocus() + storeChangedCn = Utilities.disconnectEvent(storeChangedCn) + end + + return this +end + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CardGrid.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CardGrid.lua new file mode 100644 index 0000000..af64676 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CardGrid.lua @@ -0,0 +1,347 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) + +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) +local spriteManager = require(Modules.LuaApp.Legacy.AvatarEditor.SpriteSheetManager) +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfo) +local utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local TableUtilities = require(Modules.LuaApp.TableUtilities) +local GetButtonClickEvent = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonClickEvent) + +--Constants +local BUTTONS_PER_ROW = LayoutInfo.ButtonsPerRow +local GRID_PADDING = LayoutInfo.GridPadding + +--Mutables +local equippedFrameTemplate = nil +local scrollingFrame = nil +local getAssetList = function() return {} end +local recycledAssetCardStack = {} +local assetCardConnections = {} +local activeAssetCards = {} +local activeAssetCardsByIndex = {} +local assetCardVersionKeys = {} +local lastSuccessfulAssetCardUpdateAtY = 0 +local renderAssetCard = function() end +local currentAssetRegion = {} + +local this = {} + + +local function popRecycledAssetCard() + local n = #recycledAssetCardStack + + if n > 0 then + local card = recycledAssetCardStack[n] + recycledAssetCardStack[n] = nil + return card + end +end + + +local function recycleAssetCard(card) + if not card then return end + + card.Parent = nil + + assetCardVersionKeys[card] = assetCardVersionKeys[card] + 1 + + if activeAssetCards[card] then + activeAssetCardsByIndex[activeAssetCards[card]] = nil + activeAssetCards[card] = nil + end + + if assetCardConnections[card] then + for _, con in next, assetCardConnections[card] do + con:disconnect() + end + assetCardConnections[card] = nil + end + + if card:FindFirstChild'EquippedFrame' then + card.EquippedFrame:Destroy() + end + + table.insert(recycledAssetCardStack, card) +end + + +function this:invalidateAllAssetCards() + for _, card in pairs(activeAssetCardsByIndex) do + if typeof(card) == 'Instance' then + recycleAssetCard(card) + end + end + scrollingFrame:ClearAllChildren() + lastSuccessfulAssetCardUpdateAtY = 0 + currentAssetRegion = {0, 0} +end + + + +if LayoutInfo.isLandscape then + local leadingOffset = 11 + + function this:getButtonSize() + local availableWidth = scrollingFrame.AbsoluteSize.X + 9 + local buttonWidthBudget = availableWidth/BUTTONS_PER_ROW + return buttonWidthBudget - GRID_PADDING - 5 + end + + function this:getAssetCardY(i) + local row = math.floor((i-1)/BUTTONS_PER_ROW)+1 + local rowHeight = this:getButtonSize() + GRID_PADDING + return (row-1)*rowHeight + leadingOffset + end +else + local leadingOffset = 30 + + function this:getButtonSize() + local availableWidth = scrollingFrame.AbsoluteSize.X + 0 + local buttonWidthBudget = availableWidth/BUTTONS_PER_ROW + return buttonWidthBudget - GRID_PADDING + end + + function this:getAssetCardY(i) + local row = math.floor((i-1)/BUTTONS_PER_ROW)+1 + local rowHeight = this:getButtonSize() + GRID_PADDING + return (row-1)*rowHeight + leadingOffset + end +end + + +local function makeNewAssetCard(cardName, image) + local assetButton = Instance.new('ImageButton') + + assetButton.AutoButtonColor = false + assetButton.BackgroundColor3 = Color3.fromRGB(255,255,255) + assetButton.BorderColor3 = Color3.fromRGB(208,208,208) + assetButton.BackgroundTransparency = 1 + assetButton.ZIndex = scrollingFrame.ZIndex + + local assetButtonBackgroud = Instance.new('ImageLabel', assetButton) + assetButtonBackgroud.Name = 'AssetButtonBackground' + assetButtonBackgroud.BackgroundTransparency = 1 + assetButtonBackgroud.ScaleType = Enum.ScaleType.Slice + assetButtonBackgroud.SliceCenter = Rect.new(6,6,7,7) + assetButtonBackgroud.ZIndex = assetButton.ZIndex + assetButtonBackgroud.Size = UDim2.new(1, 6, 1, 6) + assetButtonBackgroud.Position = UDim2.new(0, -3, 0, -3) + spriteManager.equip(assetButtonBackgroud, "gr-card corner", true) + + local assetImageLabel = Instance.new('ImageLabel') + assetImageLabel.BackgroundTransparency = 1 + assetImageLabel.BorderSizePixel = 0 + if LayoutInfo.isLandscape then + assetImageLabel.Size = UDim2.new(1, -6, 1, -6) + assetImageLabel.Position = UDim2.new(0,3,0,3) + else + assetImageLabel.Size = UDim2.new(1,-8,1,-8) + assetImageLabel.Position = UDim2.new(0,4,0,4) + end + assetImageLabel.ZIndex = assetButton.ZIndex + 1 + assetImageLabel.Parent = assetButton + return assetButton +end + +local function makeEquippedFrame(equippedFrameTemplate, card) + local equippedFrame = card:FindFirstChild('EquippedFrame') + if not equippedFrame then + local equippedFrame = equippedFrameTemplate:clone() + equippedFrame.Name = 'EquippedFrame' + equippedFrame.ZIndex = card.ZIndex + 2 + equippedFrame.Visible = true + equippedFrame.Parent = card + end +end + +function this:makeAssetCard(i, cardName, image, clickFunction, longPressFunction, isSelected, positionOverride) + local card = popRecycledAssetCard() or makeNewAssetCard(cardName, image) + + local column = ((i-1)%BUTTONS_PER_ROW)+1 + local buttonSize = this:getButtonSize() + + card.Name = 'AssetButton'..cardName + card.Size = UDim2.new(0, buttonSize, 0, buttonSize) + card.Position = positionOverride or UDim2.new( + 0, + GRID_PADDING + (column-1) * (buttonSize+GRID_PADDING) -3, + 0, + this:getAssetCardY(i) + ) + + assetCardConnections[card] = {} + activeAssetCardsByIndex[i] = card + activeAssetCards[card] = i + + local myVersionKey = (assetCardVersionKeys[card] or 0) + 1 + assetCardVersionKeys[card] = myVersionKey + + if clickFunction then + table.insert(assetCardConnections[card], GetButtonClickEvent(card):connect(clickFunction)) + end + if longPressFunction then + table.insert(assetCardConnections[card], card.TouchLongPress:connect(longPressFunction)) + end + if isSelected then + makeEquippedFrame(equippedFrameTemplate, card) + end + + if type(image) == 'function' then + utilities.fastSpawn(function() + local image = image() + if myVersionKey == assetCardVersionKeys[card] then + card.ImageLabel.Image = image + end + end) + else + card.ImageLabel.Image = image + end + + return card +end + +function this:getFirstCard() + if activeAssetCardsByIndex[1] then + return activeAssetCardsByIndex[1] + end + + return nil +end + + +local function getVisibleAssetRegion() + local assetList = getAssetList() + return {1, #assetList} +end + + +local function renderAssetCardRegion(a, b) + for i = a, b do + renderAssetCard(i) + end +end + + +local function updateVisibleAssetCards() + local visibleRegion = getVisibleAssetRegion() + + if visibleRegion[1] == currentAssetRegion[1] and visibleRegion[2] == currentAssetRegion[2] then + return + end + + if currentAssetRegion[1] then + renderAssetCardRegion(currentAssetRegion[2]+1, visibleRegion[2]) + else + renderAssetCardRegion(1, visibleRegion[2]) + currentAssetRegion[1] = 1 + end + + currentAssetRegion[2] = visibleRegion[2] +end + + +function this:freshUpdateVisibleAssetCards() + currentAssetRegion = {} + updateVisibleAssetCards() +end + + +function this:tryUpdateVisibleAssetCards() + local y = scrollingFrame.CanvasPosition.y + + if math.abs(y - lastSuccessfulAssetCardUpdateAtY) > 0 then --scrollingFrame.AbsoluteSize.x/4 then + updateVisibleAssetCards() + + lastSuccessfulAssetCardUpdateAtY = y + end +end + + +function this:setRenderAssetCardCallback(callback) + renderAssetCard = callback +end + + +local function equipAsset(assetId) + -- Create selection frames + local assetButtonName = 'AssetButton'..tostring(assetId) + for _, assetButton in pairs(scrollingFrame:GetChildren()) do + if assetButton.Name == assetButtonName then + makeEquippedFrame(equippedFrameTemplate, assetButton) + end + end +end + +local function unequipAsset(assetId) + --Remove selectionBoxes + local assetButtonName = 'AssetButton'..tostring(assetId) + for _,assetButton in pairs(scrollingFrame:GetChildren()) do + if assetButton.Name == assetButtonName then + local equippedFrame = assetButton:FindFirstChild('EquippedFrame') + if equippedFrame then + equippedFrame:Destroy() + end + end + end +end + + +local function stateChanged(newState, oldState) + if newState.Character.Assets ~= oldState.Character.Assets then + --Remove assets which only exist in oldState + for assetType, assetList in pairs(oldState.Character.Assets) do + if not newState.Character.Assets[assetType] and assetList then + for _, assetId in pairs(assetList) do + unequipAsset(assetId) + end + end + end + + for assetType, _ in pairs(newState.Character.Assets) do + if newState.Character.Assets[assetType] ~= oldState.Character.Assets[assetType] then + local addTheseAssets = + TableUtilities.ListDifference( + newState.Character.Assets[assetType] or {}, + oldState.Character.Assets[assetType] or {}) + + local removeTheseAssets = TableUtilities.ListDifference( + oldState.Character.Assets[assetType] or {}, + newState.Character.Assets[assetType] or {}) + + for _, assetId in pairs(addTheseAssets) do + equipAsset(assetId) + end + + for _, assetId in pairs(removeTheseAssets) do + unequipAsset(assetId) + end + end + end + end +end + +return function(inEquippedFrameTemplate, inScrollingFrame, inGetAssetList) + --Reset all mutables when recreate the new card grid + equippedFrameTemplate = nil + scrollingFrame = nil + getAssetList = function() return {} end + recycledAssetCardStack = {} + assetCardConnections = {} + activeAssetCards = {} + activeAssetCardsByIndex = {} + assetCardVersionKeys = {} + lastSuccessfulAssetCardUpdateAtY = 0 + renderAssetCard = function() end + currentAssetRegion = {} + + --Set up + equippedFrameTemplate = inEquippedFrameTemplate + scrollingFrame = inScrollingFrame + getAssetList = inGetAssetList + + AppState.Store.Changed:Connect(stateChanged) + + return this +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CardGridConsole.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CardGridConsole.lua new file mode 100644 index 0000000..0dfcc8b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CardGridConsole.lua @@ -0,0 +1,426 @@ +local GuiService = game:GetService("GuiService") +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) + +local Categories = require(Modules.LuaApp.Legacy.AvatarEditor.Categories) +local AssetInfo = require(Modules.LuaApp.Legacy.AvatarEditor.AssetInfo) +local Urls = require(Modules.LuaApp.Legacy.AvatarEditor.Urls) +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfoConsole) +local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local TableUtilities = require(Modules.LuaApp.TableUtilities) + +local EquipAsset = require(Modules.LuaApp.Actions.EquipAsset) +local UnequipAsset = require(Modules.LuaApp.Actions.UnequipAsset) +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) + +local ShellModules = Modules:FindFirstChild("Shell") +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local GetButtonClickEvent = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonClickEvent) + +--Constants +local BUTTONS_PER_ROW = LayoutInfo.ButtonsPerRow +local BUTTON_ROWS_PER_PAGE = LayoutInfo.ButtonRowsPerPage +local GRID_PADDING = LayoutInfo.GridPadding +local BUTTONS_SIZE = LayoutInfo.ButtonSize +local SELECTOR_BOTTOM_MIN_DISTANCE = LayoutInfo.SelectorBottomMinDistance + +local function createCardGrid(scrollingFrame, getAssetList, characterManager) + local this = {} + + local recycledAssetCardStack = {} + local assetCardConnections = {} + local activeAssetCards = {} + local activeAssetCardsByIndex = {} + local assetCardVersionKeys = {} + local lastSuccessfulAssetCardUpdateAtY = 0 + local currentAssetRegion = {} + local selectedAssetCardIndex = 0 + local storeChangedCn = nil + + local itemCardSelector = Utilities.create'ImageLabel' + { + Name = 'CardSelector'; + Image = LayoutInfo.ItemCardSelectorImage; + Position = UDim2.new(0, -7, 0, -7); + Size = UDim2.new(1, 14, 1, 14); + BackgroundTransparency = 1; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(51, 51, 103, 103); + } + + local function recycleAssetCard(card) + if not card then return end + + card.Parent = nil + + assetCardVersionKeys[card] = assetCardVersionKeys[card] + 1 + + if activeAssetCards[card] then + activeAssetCardsByIndex[activeAssetCards[card]] = nil + activeAssetCards[card] = nil + end + + if assetCardConnections[card] then + for _, con in next, assetCardConnections[card] do + con:disconnect() + end + assetCardConnections[card] = nil + end + + table.insert(recycledAssetCardStack, card) + end + + function this:invalidateAllAssetCards() + for _, card in pairs(activeAssetCardsByIndex) do + if typeof(card) == 'Instance' then + recycleAssetCard(card) + end + end + scrollingFrame:ClearAllChildren() + lastSuccessfulAssetCardUpdateAtY = 0 + currentAssetRegion = {0, 0} + selectedAssetCardIndex = 0 + end + + local function popRecycledAssetCard() + local n = #recycledAssetCardStack + + if n > 0 then + local card = recycledAssetCardStack[n] + recycledAssetCardStack[n] = nil + return card + end + end + + -- Check if scrollingFrame need to scroll up/down + local function checkScroll(card) + if activeAssetCards[card] then + local bottonYBottom = (card.Position + card.Size).Y.Offset + local scrollingFrameYBottom = scrollingFrame.AbsoluteWindowSize.Y + scrollingFrame.CanvasPosition.Y + local bottomDistance = scrollingFrameYBottom - bottonYBottom + local topDistance = card.Position.Y.Offset - scrollingFrame.CanvasPosition.Y + + local newCanvasPositionY = scrollingFrame.CanvasPosition.Y + if bottomDistance < SELECTOR_BOTTOM_MIN_DISTANCE then + newCanvasPositionY = newCanvasPositionY + SELECTOR_BOTTOM_MIN_DISTANCE - bottomDistance + elseif topDistance < 0 then + newCanvasPositionY = newCanvasPositionY + topDistance - GRID_PADDING - 20 + end + + newCanvasPositionY = + math.max(0, + math.min( + newCanvasPositionY, + scrollingFrame.CanvasSize.Y.Offset - scrollingFrame.AbsoluteWindowSize.Y + ) + ) + + scrollingFrame.CanvasPosition = Vector2.new(scrollingFrame.CanvasPosition.X, newCanvasPositionY) + end + end + + local function makeNewAssetCard(cardName, image) + local assetButton = Utilities.create'ImageButton' + { + AutoButtonColor = false; + BackgroundColor3 = Color3.fromRGB(255, 255, 255); + BorderColor3 = Color3.fromRGB(208, 208, 208); + BackgroundTransparency = 1; + ZIndex = LayoutInfo.BasicLayer; + SelectionImageObject = itemCardSelector; + } + + local MoveSelectionSound = SoundManager:CreateSound('MoveSelection') + MoveSelectionSound.Parent = assetButton + + Utilities.create'ImageLabel' + { + Name = 'AssetButtonBackground'; + BackgroundTransparency = 1; + ScaleType = Enum.ScaleType.Slice; + ZIndex = LayoutInfo.BasicLayer; + Image = ""; + SliceCenter = Rect.new(16, 16, 17, 17); + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 0, 0); + Parent = assetButton; + } + + Utilities.create'ImageLabel' + { + Name = 'AssetItemImage'; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Size = UDim2.new(1, -10, 1, -10); + Position = UDim2.new(0, 5, 0, 5); + ZIndex = LayoutInfo.AssetImageLayer; + Parent = assetButton; + } + + Utilities.create'ImageLabel' + { + Name = 'AssetBorderMask'; + Position = UDim2.new(0, 0, 0, 0); + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = ""; + ZIndex = LayoutInfo.BorderMaskLayer; + Parent = assetButton; + } + + return assetButton + end + + function this:makeAssetCard(i, cardName, image, clickFunction, isEquipped, unAvailable, YOffset) + local card = popRecycledAssetCard() or makeNewAssetCard(cardName, image) + YOffset = YOffset or 0 + + local column = ((i-1) % BUTTONS_PER_ROW) + 1 + local row = math.ceil(i / BUTTONS_PER_ROW) + card.Name = 'AssetButton'..cardName + card.Size = UDim2.new(0, BUTTONS_SIZE, 0, BUTTONS_SIZE) + card.Position = UDim2.new( + 0, + (column-1) * (BUTTONS_SIZE + GRID_PADDING), + 0, + (row - 1) * (BUTTONS_SIZE + GRID_PADDING) + YOffset + ) + + assetCardConnections[card] = {} + activeAssetCardsByIndex[i] = card + activeAssetCards[card] = i + + local myVersionKey = (assetCardVersionKeys[card] or 0) + 1 + assetCardVersionKeys[card] = myVersionKey + + table.insert(assetCardConnections[card], card.SelectionGained:connect(function() + checkScroll(card) + selectedAssetCardIndex = i + end)) + + if clickFunction then + table.insert(assetCardConnections[card], GetButtonClickEvent(card):connect(clickFunction)) + end + + if type(image) == 'function' then + Utilities.fastSpawn(function() + local image = image() + if myVersionKey == assetCardVersionKeys[card] then + card.AssetItemImage.Image = image + end + end) + else + card.AssetItemImage.Image = image + end + + if unAvailable then + card.AssetButtonBackground.Image = LayoutInfo.ItemUnavailableBackgroundImage + card.AssetBorderMask.Image = LayoutInfo.ItemMaskNotOwnedImage + else + card.AssetButtonBackground.Image = LayoutInfo.ItemAvailableBackgroundImage + card.AssetBorderMask.Image = isEquipped and + LayoutInfo.WearingIndicatorImage or + LayoutInfo.ItemMaskImage + end + + return card + end + + local function renderAssetCardByIndex(i) + local assetList = getAssetList() + local assetId = assetList[i] + local cardName, cardImage, clickFunction, isEquipped + + local state = AppState.Store:getState() + local categoryIndex = state.Category.CategoryIndex or 1 + local tabInfo = state.Category.TabsInfo[categoryIndex] + local tabIndex = tabInfo and tabInfo.TabIndex or 1 + local currentPage = Categories[categoryIndex].pages[tabIndex] + + if currentPage.specialPageType == 'Outfits' or currentPage.specialPageType == 'Recent Outfits' then + cardName = 'Outfit'..tostring(assetId) + + cardImage = function() + return Urls.outfitImageUrlPrefix..tostring(assetId) + .."&width=100&height=100&format=png" + end + + clickFunction = function() + characterManager.addToRecentAssetList('outfits', assetId) + characterManager.wearOutfit(assetId) + end + else + cardName = tostring(assetId) + cardImage = Urls.assetImageUrl..tostring(assetId) + isEquipped = characterManager.findIfEquipped(assetId) + + local wearFunction = function() + AppState.Store:dispatch(EquipAsset(AssetInfo.getAssetType(assetId), assetId)) + end + + local takeOffFunction = function() + AppState.Store:dispatch(UnequipAsset(AssetInfo.getAssetType(assetId), assetId)) + end + + local wearOrTakeOffFunction = function() + if characterManager.findIfEquipped(assetId) then + takeOffFunction() + else + wearFunction() + end + end + + clickFunction = function() + SoundManager:Play('ButtonPress') + wearOrTakeOffFunction() + end + end + + local card = this:makeAssetCard(i, cardName, cardImage, clickFunction, isEquipped) + card.Parent = scrollingFrame + end + + local function renderAssetCardRegion(a, b) + for i = a, b do + renderAssetCardByIndex(i) + end + end + + local function getVisibleAssetRegion() + local assetList = getAssetList() + return {1, #assetList} + end + + local function updateVisibleAssetCards() + local visibleRegion = getVisibleAssetRegion() + + if visibleRegion[1] == currentAssetRegion[1] and visibleRegion[2] == currentAssetRegion[2] then + return + end + + if currentAssetRegion[1] then + renderAssetCardRegion(currentAssetRegion[2]+1, visibleRegion[2]) + else + renderAssetCardRegion(1, visibleRegion[2]) + currentAssetRegion[1] = 1 + end + + currentAssetRegion[2] = visibleRegion[2] + end + + function this:freshUpdateVisibleAssetCards() + currentAssetRegion = {} + updateVisibleAssetCards() + end + + function this:tryUpdateVisibleAssetCards() + local y = scrollingFrame.CanvasPosition.y + + if math.abs(y - lastSuccessfulAssetCardUpdateAtY) > 0 then --scrollingFrame.AbsoluteSize.x/4 then + updateVisibleAssetCards() + + lastSuccessfulAssetCardUpdateAtY = y + end + end + + local function equipAsset(assetId) + -- Create selection frames + local assetButtonName = 'AssetButton'..tostring(assetId) + for _, assetButton in pairs(scrollingFrame:GetChildren()) do + if assetButton.Name == assetButtonName then + assetButton.AssetBorderMask.Image = LayoutInfo.WearingIndicatorImage + end + end + end + + local function unequipAsset(assetId) + --Remove selectionBoxes + local assetButtonName = 'AssetButton'..tostring(assetId) + for _,assetButton in pairs(scrollingFrame:GetChildren()) do + if assetButton.Name == assetButtonName then + assetButton.AssetBorderMask.Image = LayoutInfo.ItemMaskImage + end + end + end + + local function stateChanged(newState, oldState) + if newState.Character.Assets ~= oldState.Character.Assets then + --Remove assets which only exist in oldState + for assetType, assetList in pairs(oldState.Character.Assets) do + if not newState.Character.Assets[assetType] and assetList then + for _, assetId in pairs(assetList) do + unequipAsset(assetId) + end + end + end + + for assetType, _ in pairs(newState.Character.Assets) do + if newState.Character.Assets[assetType] ~= oldState.Character.Assets[assetType] then + local addTheseAssets = + TableUtilities.ListDifference( + newState.Character.Assets[assetType] or {}, + oldState.Character.Assets[assetType] or {}) + + local removeTheseAssets = TableUtilities.ListDifference( + oldState.Character.Assets[assetType] or {}, + newState.Character.Assets[assetType] or {}) + + for _, assetId in pairs(addTheseAssets) do + equipAsset(assetId) + end + + for _, assetId in pairs(removeTheseAssets) do + unequipAsset(assetId) + end + end + end + end + end + + function this:getFirstCard() + if activeAssetCardsByIndex[1] then + return activeAssetCardsByIndex[1] + end + + return nil + end + + function this:SelectNextPage() + if selectedAssetCardIndex > 0 then + local nextIndex = selectedAssetCardIndex + BUTTONS_PER_ROW * BUTTON_ROWS_PER_PAGE + if nextIndex > #activeAssetCardsByIndex then + local rows = math.floor(#activeAssetCardsByIndex / BUTTONS_PER_ROW) + local remainderColumns = #activeAssetCardsByIndex % BUTTONS_PER_ROW + local selectedColumn = selectedAssetCardIndex % BUTTONS_PER_ROW + rows = (selectedColumn > remainderColumns) and (rows - 1) or rows + nextIndex = rows * BUTTONS_PER_ROW + selectedColumn + end + GuiService.SelectedCoreObject = activeAssetCardsByIndex[nextIndex] + end + end + + function this:SelectPreviousPage() + if selectedAssetCardIndex > 0 then + local prevIndex = selectedAssetCardIndex - BUTTONS_PER_ROW * BUTTON_ROWS_PER_PAGE + if prevIndex < 1 then + local selectedColumn = (selectedAssetCardIndex - 1) % BUTTONS_PER_ROW + 1 + prevIndex = selectedColumn + end + GuiService.SelectedCoreObject = activeAssetCardsByIndex[prevIndex] + end + end + + function this:Focus() + storeChangedCn = AppState.Store.Changed:Connect(stateChanged) + end + + function this:RemoveFocus() + storeChangedCn = Utilities.disconnectEvent(storeChangedCn) + end + + return this +end + +return createCardGrid diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Categories.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Categories.lua new file mode 100644 index 0000000..2d36e1f --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Categories.lua @@ -0,0 +1,528 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfo) +local Strings = require(Modules.LuaApp.Legacy.AvatarEditor.LocalizedStrings) + +local legsFocus = {Parts={'RightUpperLeg', 'LeftUpperLeg'}} +local faceFocus = {Parts={'Head'}} +local armsFocus = {Parts={'UpperTorso'}} +local headWideFocus = {Parts={'Head'}} +local neckFocus = {Parts={'Head', 'UpperTorso'}} +local shoulderFocus = {Parts={'Head', 'RightUpperArm', 'LeftUpperArm'}} +local waistFocus = {Parts={'LowerTorso', 'RightUpperLeg', 'LeftUpperLeg'}} + +local recentPage = { + name = 'Recent All', + title = Strings:LocalizedString("RecentAllTitle"), + titleLandscape = Strings:LocalizedString("RecentAllLandscapeTitle"), + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-all.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-all-on.png', + iconImageName = 'ic-all', + iconImageSelectedName = 'ic-all-on', + specialPageType = 'Recent All', +} +local recentClothingPage = { + name = 'Recent Clothing', + title = Strings:LocalizedString("RecentClothingTitle"), + titleLandscape = Strings:LocalizedString("RecentClothingLandscapeTitle"), + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-clothing.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-clothing-on.png', + iconImageName = 'ic-clothing', + iconImageSelectedName = 'ic-clothing-on', + specialPageType = 'Recent Clothing', +} +local recentBodyPage = { + name = 'Recent Body', + title = Strings:LocalizedString("RecentBodyTitle"), + titleLandscape = Strings:LocalizedString("RecentBodyLandscapeTitle"), + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-body-part.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-body-part-on.png', + iconImageName = 'ic-body', + iconImageSelectedName = 'ic-body-on', + specialPageType = 'Recent Body', +} +local recentAnimationPage = { + name = 'Recent Animation', + title = Strings:LocalizedString("RecentAnimationsTitle"), + titleLandscape = Strings:LocalizedString("RecentAnimationsLandscapeTitle"), + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation-on.png', + iconImageName = 'ic-animations', + iconImageSelectedName = 'ic-animations-on', + specialPageType = 'Recent Animation', +} +local recentOutfitsPage = { + name = 'Recent Outfits', + title = Strings:LocalizedString("RecentOutfitsTitle"), + titleLandscape = Strings:LocalizedString("RecentOutfitsLandscapeTitle"), + iconImageName = 'ic-costumes', + iconImageSelectedName = 'ic-costumes-on', + specialPageType = 'Recent Outfits', +} +local outfitsPage = { + name = 'Outfits', --outfits will include packages in some way + title = Strings:LocalizedString("OutfitsTabTitle"), + titleLandscape = Strings:LocalizedString("OutfitsTabLandscapeTitle"), + titleConsole = Strings:LocalizedString("OutfitsTabLandscapeTitle"), + iconImage = 'rbxasset://textures/AvatarEditorIcons/ic-bundle.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/ic-bundle-white.png', + iconImageName = 'ic-all', + iconImageSelectedName = 'ic-all-on', + infiniteScrolling = true, + specialPageType = 'Outfits', +} +local hatsPage = { + name = 'Hats', + title = Strings:LocalizedString("HatsTitle"), + titleLandscape = Strings:LocalizedString("HatsLandscapeTitle"), + typeName = 'Hat', + iconImage = 'rbxasset://textures/AvatarEditorIcons/ic-hat.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/ic-hat-white.png', + iconImageName = 'ic-hat', + iconImageSelectedName = 'ic-hat-on', + infiniteScrolling = true, + CameraFocus = headWideFocus, + CameraZoomRadius = 7.5, + recommendedSort = true, + shopUrl = "/catalog/?Category=11&Subcategory=9", +} +local hairPage = { + name = 'Hair', + title = Strings:LocalizedString("HairTitle"), + typeName = 'Hair Accessory', + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Clothing/ic-hair.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Clothing/ic-hair-on.png', + iconImageName = 'ic-hair', + iconImageSelectedName = 'ic-hair-on', + infiniteScrolling = true, + CameraFocus = headWideFocus, + CameraZoomRadius = 7.5, + recommendedSort = true, + shopUrl = "/catalog/?Category=11&Subcategory=20", +} +local faceAccessoryPage = { + name = 'Face Accessories', + title = Strings:LocalizedString("FaceAccessoryTitle"), + titleLandscape = Strings:LocalizedString("FaceAccessoryLandscapeTitle"), + typeName = 'Face Accessory', + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Clothing/ic-face.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Clothing/ic-face-on.png', + iconImageName = 'ic-face-accessories', + iconImageSelectedName = 'ic-face-accessories-on', + infiniteScrolling = true, + CameraFocus = faceFocus, + CameraZoomRadius = 4.5, + recommendedSort = true, + shopUrl = "/catalog/?Category=11&Subcategory=21", +} +local neckAccessoryPage = { + name = 'Neck Accessories', + title = Strings:LocalizedString("NeckAccessoryTitle"), + titleLandscape = Strings:LocalizedString("NeckAccessoryLandscapeTitle"), + typeName = 'Neck Accessory', + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Clothing/ic-neck.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Clothing/ic-neck-on.png', + iconImageName = 'ic-neck', + iconImageSelectedName = 'ic-neck-on', + infiniteScrolling = true, + CameraFocus = neckFocus, + CameraZoomRadius = 6.5, + recommendedSort = true, + shopUrl = "/catalog/?Category=11&Subcategory=22", +} +local shoulderAccessoryPage = { + name = 'Shoulder Accessories', + title = Strings:LocalizedString("ShoulderAccessoryTitle"), + titleLandscape = Strings:LocalizedString("ShoulderAccessoryLandscapeTitle"), + typeName = 'Shoulder Accessory', + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Clothing/ic-shoulder.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Clothing/ic-shoulder-on.png', + iconImageName = 'ic-shoulders', + iconImageSelectedName = 'ic-shoulders-on', + infiniteScrolling = true, + CameraFocus = shoulderFocus, + CameraZoomRadius = 6.5, + recommendedSort = true, + shopUrl = "/catalog/?Category=11&Subcategory=23", +} +local frontAccessoryPage = { + name = 'Front Accessories', + title = Strings:LocalizedString("FrontAccessoryTitle"), + titleLandscape = Strings:LocalizedString("FrontAccessoryLandscapeTitle"), + typeName = 'Front Accessory', + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Clothing/ic-front.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Clothing/ic-front-on.png', + iconImageName = 'ic-front', + iconImageSelectedName = 'ic-front-on', + infiniteScrolling = true, + CameraFocus = armsFocus, + CameraZoomRadius = 7.5, + recommendedSort = true, + shopUrl = "/catalog/?Category=11&Subcategory=24", +} +local backAccessoryPage = { + name = 'Back Accessories', + title = Strings:LocalizedString("BackAccessoryTitle"), + titleLandscape = Strings:LocalizedString("BackAccessoryLandscapeTitle"), + typeName = 'Back Accessory', + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Clothing/ic-back.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Clothing/ic-back-on.png', + iconImageName = 'ic-back', + iconImageSelectedName = 'ic-back-on', + infiniteScrolling = true, + CameraFocus = armsFocus, + CameraZoomRadius = 7.5, + recommendedSort = true, + shopUrl = "/catalog/?Category=11&Subcategory=25", +} +local waistAccessoryPage = { + name = 'Waist Accessories', + title = Strings:LocalizedString("WaistAccessoryTitle"), + titleLandscape = Strings:LocalizedString("WaistAccessoryLandscapeTitle"), + typeName = 'Waist Accessory', + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Clothing/ic-waist.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Clothing/ic-waist-on.png', + iconImageName = 'ic-waist', + iconImageSelectedName = 'ic-waist-on', + infiniteScrolling = true, + CameraFocus = waistFocus, + CameraZoomRadius = 6.5, + recommendedSort = true, + shopUrl = "/catalog/?Category=11&Subcategory=26", +} +local shirtsPage = { + name = 'Shirts', + title = Strings:LocalizedString("ShirtsTitle"), + titleLandscape = Strings:LocalizedString("ShirtsLandscapeTitle"), + typeName = 'Shirt', + iconImage = 'rbxasset://textures/AvatarEditorIcons/ic-tshirt.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/ic-tshirt-white.png', + iconImageName = 'ic-shirts', + iconImageSelectedName = 'ic-shirts-on', + infiniteScrolling = true, + CameraFocus = armsFocus, + CameraZoomRadius = 7.5, + recommendedSort = true, + shopUrl = "/catalog/?Category=3&Subcategory=12", +} +local pantsPage = { + name = 'Pants', + title = Strings:LocalizedString("PantsTitle"), + typeName = 'Pants', + iconImage = 'rbxasset://textures/AvatarEditorIcons/ic-pant.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/ic-pant-white.png', + iconImageName = 'ic-pants', + iconImageSelectedName = 'ic-pants-on', + infiniteScrolling = true, + CameraFocus = legsFocus, + CameraZoomRadius = 7.5, + recommendedSort = true, + shopUrl = "/catalog/?Category=3&Subcategory=14", +} +local facesPage = { + name = 'Faces', + title = Strings:LocalizedString("FacesTitle"), + titleLandscape = Strings:LocalizedString("FacesLandscapeTitle"), + typeName = 'Face', + iconImage = 'rbxasset://textures/AvatarEditorIcons/ic-face.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/ic-face-white.png', + iconImageName = 'ic-face', + iconImageSelectedName = 'ic-face-on', + infiniteScrolling = true, + CameraFocus = faceFocus, + CameraZoomRadius = 4.5, + recommendedSort = true, + shopUrl = "/catalog/?Category=4&Subcategory=10", +} +local headsPage = { + name = 'Heads', + title = Strings:LocalizedString("HeadsTitle"), + titleLandscape = Strings:LocalizedString("HeadsLandscapeTitle"), + typeName = 'Head', + iconImage = 'rbxasset://textures/AvatarEditorIcons/ic-head.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/ic-head-white.png', + iconImageName = 'ic-head', + iconImageSelectedName = 'ic-head-on', + infiniteScrolling = true, + CameraFocus = faceFocus, + CameraZoomRadius = 4.5, + recommendedSort = true, + shopUrl = "/catalog/?Category=4&Subcategory=15", +} +local torsosPage = { + name = 'Torsos', + title = Strings:LocalizedString("TorsosTitle"), + titleLandscape = Strings:LocalizedString("TorsosLandscapeTitle"), + typeName = 'Torso', + iconImage = 'rbxasset://textures/AvatarEditorIcons/ic-torso.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/ic-torso-white.png', + iconImageName = 'ic-torso', + iconImageSelectedName = 'ic-torso-on', + infiniteScrolling = true, + CameraFocus = armsFocus, + CameraZoomRadius = 7.5, +} +local rightArmsPage = { + name = 'Right Arms', + title = Strings:LocalizedString("RightArmsTitle"), + typeName = 'RightArm', + iconImage = 'rbxasset://textures/AvatarEditorIcons/ic-rightarm.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/ic-rightarm-white.png', + iconImageName = 'ic-right-arms', + iconImageSelectedName = 'ic-right-arms-on', + infiniteScrolling = true, + CameraFocus = armsFocus, + CameraZoomRadius = 7.5, +} +local leftArmsPage = { + name = 'Left Arms', + title = Strings:LocalizedString("LeftArmsTitle"), + typeName = 'LeftArm', + iconImage = 'rbxasset://textures/AvatarEditorIcons/ic-leftarm.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/ic-leftarm-white.png', + iconImageName = 'ic-left-arms', + iconImageSelectedName = 'ic-left-arms-on', + infiniteScrolling = true, + CameraFocus = armsFocus, + CameraZoomRadius = 7.5, +} +local rightLegsPage = { + name = 'Right Legs', + title = Strings:LocalizedString("RightLegsTitle"), + typeName = 'RightLeg', + iconImage = 'rbxasset://textures/AvatarEditorIcons/ic-rightleg.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/ic-rightleg-white.png', + iconImageName = 'ic-right-legs', + iconImageSelectedName = 'ic-right-legs-on', + infiniteScrolling = true, + CameraFocus = legsFocus, + CameraZoomRadius = 7.5, +} +local leftLegsPage = { + name = 'Left Legs', + title = Strings:LocalizedString("LeftLegsTitle"), + typeName = 'LeftLeg', + iconImage = 'rbxasset://textures/AvatarEditorIcons/ic-leftleg.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/ic-leftleg-white.png', + iconImageName = 'ic-left-legs', + iconImageSelectedName = 'ic-left-legs-on', + infiniteScrolling = true, + CameraFocus = legsFocus, + CameraZoomRadius = 7.5, +} +local gearPage = { + name = 'Gear', + title = Strings:LocalizedString("GearTitle"), + typeName = 'Gear', + iconImage = 'rbxasset://textures/AvatarEditorIcons/ic-gear.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/ic-gear-white.png', + iconImageName = 'ic-gear', + iconImageSelectedName = 'ic-gear-on', + infiniteScrolling = true, + recommendedSort = true, + shopUrl = "/catalog/?Category=5", +} +local skinTonePage = { + name = 'Skin Tone', + title = Strings:LocalizedString("SkinToneTitle"), + iconImage = 'rbxasset://textures/AvatarEditorIcons/ic-color.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/ic-color-filled.png', + iconImageName = 'ic-skintone', + iconImageSelectedName = 'ic-skintone-on', + special = true, + specialPageType = 'Skin Tone', +} +local scalePage = { + name = 'Scale', + title = Strings:LocalizedString("ScaleTitle"), + iconImage = 'rbxasset://textures/AvatarEditorIcons/ic-scale.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/ic-scale-filled.png', + iconImageName = 'ic-scale', + iconImageSelectedName = 'ic-scale-on', + special = true, + specialPageType = 'Scale', +} +local climbAnimPage = { + name = 'Climb Animations', + title = Strings:LocalizedString("ClimbAnimationsWord"), + titleLandscape = Strings:LocalizedString("ClimbWord"), + typeName = 'ClimbAnimation', + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-climb.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-climb-on.png', + iconImageName = 'ic-climb', + iconImageSelectedName = 'ic-climb-on', + infiniteScrolling = true, +} +local jumpAnimPage = { + name = 'Jump Animations', + title = Strings:LocalizedString("JumpAnimationsWord"), + titleLandscape = Strings:LocalizedString("JumpWord"), + typeName = 'JumpAnimation', + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-jump.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-jump-on.png', + iconImageName = 'ic-jump', + iconImageSelectedName = 'ic-jump-on', + infiniteScrolling = true, +} +local fallAnimPage = { + name = 'Fall Animations', + title = Strings:LocalizedString("FallAnimationsWord"), + titleLandscape = Strings:LocalizedString("FallWord"), + typeName = 'FallAnimation', + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-fall.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-fall-on.png', + iconImageName = 'ic-fall', + iconImageSelectedName = 'ic-fall-on', + infiniteScrolling = true, +} +local idleAnimPage = { + name = 'Idle Animations', + title = Strings:LocalizedString("IdleAnimationsWord"), + titleLandscape = Strings:LocalizedString("IdleWord"), + typeName = 'IdleAnimation', + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-idle.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-idle-on.png', + iconImageName = 'ic-idle', + iconImageSelectedName = 'ic-idle-on', + infiniteScrolling = true, +} +local walkAnimPage = { + name = 'Walk Animations', + title = Strings:LocalizedString("WalkAnimationsWord"), + titleLandscape = Strings:LocalizedString("WalkWord"), + typeName = 'WalkAnimation', + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-walk.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-walk-on.png', + iconImageName = 'ic-walk', + iconImageSelectedName = 'ic-walk-on', + infiniteScrolling = true, +} +local runAnimPage = { + name = 'Run Animations', + title = Strings:LocalizedString("RunAnimationsWord"), + titleLandscape = Strings:LocalizedString("RunWord"), + typeName = 'RunAnimation', + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-run.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-run-on.png', + iconImageName = 'ic-run', + iconImageSelectedName = 'ic-run-on', + infiniteScrolling = true, +} +local swimAnimPage = { + name = 'Swim Animations', + title = Strings:LocalizedString("SwimAnimationsWord"), + titleLandscape = Strings:LocalizedString("SwimWord"), + typeName = 'SwimAnimation', + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-swim.png', + iconImageSelected = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Avatar-Animation/ic-swim-on.png', + iconImageName = 'ic-swim', + iconImageSelectedName = 'ic-swim-on', + infiniteScrolling = true, +} +local recentCategory = { + name = 'Recent', + title = Strings:LocalizedString("RecentCategoryTitle"), + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-recent.png', + selectedIconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-recent-on.png', + iconImageConsole = 'rbxasset://textures/ui/Shell/AvatarEditor/icon/ic-recent-wht.png', + selectedIconImageConsole = 'rbxasset://textures/ui/Shell/AvatarEditor/icon/ic-recent-blk.png', + iconImageName = 'ic-recent', + selectedIconImageName = 'ic-recent-on', + pages = {recentPage, recentClothingPage, recentBodyPage, recentAnimationPage} +} +local clothingCategory = { + name = 'Clothing', + title = Strings:LocalizedString("ClothingCategoryTitle"), + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-clothing.png', + selectedIconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-clothing-on.png', + iconImageConsole = 'rbxasset://textures/ui/Shell/AvatarEditor/icon/ic-clothing-wht.png', + selectedIconImageConsole = 'rbxasset://textures/ui/Shell/AvatarEditor/icon/ic-clothing-blk.png', + iconImageName = 'ic-clothing', + selectedIconImageName = 'ic-clothing-on', + pages = {hatsPage, + shirtsPage, + pantsPage, + hairPage, + faceAccessoryPage, + neckAccessoryPage, + shoulderAccessoryPage, + frontAccessoryPage, + backAccessoryPage, + waistAccessoryPage, + gearPage,}, +} +local bodyCategory = { + name = 'Body', + title = Strings:LocalizedString("BodyCategoryTitle"), + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-body-part.png', + selectedIconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-body-part-on.png', + iconImageConsole = 'rbxasset://textures/ui/Shell/AvatarEditor/icon/ic-body-wht.png', + selectedIconImageConsole = 'rbxasset://textures/ui/Shell/AvatarEditor/icon/ic-body-blk.png', + iconImageName = 'ic-body', + selectedIconImageName = 'ic-body-on', + pages = { + skinTonePage, + scalePage, + facesPage, + headsPage, + torsosPage, + rightArmsPage, + leftArmsPage, + rightLegsPage, + leftLegsPage}, +} +local animationCategory = { + name = 'Animation', + title = Strings:LocalizedString("AnimationCategoryTitle"), + titleLandscape = Strings:LocalizedString("AnimationCategoryLandscapeTitle"), + iconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation.png', + selectedIconImage = 'rbxasset://textures/AvatarEditorIcons/PageIcons/Category/ic-avatar-animation-on.png', + iconImageConsole = 'rbxasset://textures/ui/Shell/AvatarEditor/icon/ic-animation-wht.png', + selectedIconImageConsole = 'rbxasset://textures/ui/Shell/AvatarEditor/icon/ic-animation-blk.png', + iconImageName = 'ic-animations', + selectedIconImageName = 'ic-animations-on', + pages = {idleAnimPage, walkAnimPage, runAnimPage, jumpAnimPage, fallAnimPage, climbAnimPage, swimAnimPage} +} +local outfitsCategory = { + name = 'Outfits', + title = Strings:LocalizedString("OutfitsCategoryTitle"), + iconImageConsole = 'rbxasset://textures/ui/Shell/AvatarEditor/icon/ic-costume-wht.png', + selectedIconImageConsole = 'rbxasset://textures/ui/Shell/AvatarEditor/icon/ic-costume-blk.png', + iconImageName = 'ic-costumes', + selectedIconImageName = 'ic-costumes-on', + pages = {outfitsPage} +} +local categories = { + recentCategory, + clothingCategory, + bodyCategory, + animationCategory, + outfitsCategory, +} + +if LayoutInfo.isLandscape then + animationCategory.iconImageName = 'ic-run' + animationCategory.selectedIconImageName = 'ic-run-on' + + recentAnimationPage.iconImageName = 'ic-run' + recentAnimationPage.iconImageSelectedName = 'ic-run-on' + + table.insert(recentCategory.pages, recentOutfitsPage) + + clothingCategory.pages = { + hatsPage, + hairPage, + faceAccessoryPage, + neckAccessoryPage, + shoulderAccessoryPage, + frontAccessoryPage, + backAccessoryPage, + waistAccessoryPage, + shirtsPage, + pantsPage, + gearPage} +end + + +return categories + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CategoryMenu.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CategoryMenu.lua new file mode 100644 index 0000000..22e8965 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CategoryMenu.lua @@ -0,0 +1,316 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) +local SelectCategory = require(Modules.LuaApp.Actions.SelectCategory) +local CreateSignal = require(Modules.Common.Signal).new + +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfo) +local SpriteManager = require(Modules.LuaApp.Legacy.AvatarEditor.SpriteSheetManager) +local Categories = require(Modules.LuaApp.Legacy.AvatarEditor.Categories) +local TweenController = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController) +local GetButtonClickEvent = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonClickEvent) + +local luaAppLegacyInputDisabledGlobally = settings():GetFFlag('LuaAppLegacyInputDisabledGlobally2') + +local topMenuContainer = nil +local topMenuIndexIndicator = nil +local topMenuSelectedIcon = nil +local categoryButtonTemplate = nil + +local this = {} +this.openCategoryMenuEvent = CreateSignal() +this.closeCategoryMenuEvent = CreateSignal() + +local STATE = { + OPEN = "Open"; + CLOSED = "Closed"; + HIDDEN = "Hidden"; +} + +local function getCurrentCategoryIndex() + return AppState.Store:getState().Category.CategoryIndex +end + +local function getCurrentCategory() + local categoryIndex = getCurrentCategoryIndex() + if categoryIndex then + return Categories[categoryIndex] + end + return nil +end + +local function tweenTopMenuContainerState(state) + if LayoutInfo.isLandscape then + local tweenTime = 0.2 + local tweenInfo = TweenInfo.new(tweenTime, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + + if state ~= STATE.HIDDEN then + if state == STATE.CLOSED then + TweenController(topMenuContainer, tweenInfo, { Size = UDim2.new(0, 98, 0, 98) }) + elseif state == STATE.OPEN then + TweenController(topMenuContainer, tweenInfo, { Size = UDim2.new(0, 98, 1, -22) }) + end + end + else + local tweenTime = 0.1 + local tweenInfo = TweenInfo.new(tweenTime, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + + if state == STATE.HIDDEN then + TweenController(topMenuContainer, tweenInfo, { Position = UDim2.new(0, -300, 0, 0) }) + elseif state == STATE.CLOSED then + TweenController(topMenuContainer, tweenInfo, { Position = UDim2.new(0, -300, 0, -10) }) + elseif state == STATE.OPEN then + local openTweenInfo = TweenInfo.new(tweenTime, Enum.EasingStyle.Quad, Enum.EasingDirection.Out, 0, false, 0) + TweenController(topMenuContainer, openTweenInfo, { Position = UDim2.new(0, -357 + #Categories * 61, 0, -10) }) + end + end +end + + +local function setTopMenuContainerState(state) + if LayoutInfo.isLandscape then + if state ~= STATE.HIDDEN then + if state == STATE.CLOSED then + topMenuContainer.Size = UDim2.new(0, 98, 0, 98) + elseif state == STATE.OPEN then + topMenuContainer.Size = UDim2.new(0, 98, 1, -22) + end + end + else + if state == STATE.HIDDEN then + topMenuContainer.Position =UDim2.new(0, -300, 0, 0) + elseif state == STATE.CLOSED then + topMenuContainer.Position = UDim2.new(0, -300, 0, -10) + elseif state == STATE.OPEN then + topMenuContainer.Position = UDim2.new(0, -357 + #Categories * 61, 0, -10) + end + end +end + + +function this:openTopMenu() + self.openCategoryMenuEvent:Fire() -- Fire event + + tweenTopMenuContainerState(STATE.OPEN) + topMenuIndexIndicator.Visible = false + topMenuSelectedIcon.Visible = false + + if LayoutInfo.isLandscape then + local closeButton = Instance.new('ImageButton') + closeButton.Name = 'CloseButton' + closeButton.BackgroundTransparency = 1 + closeButton.Image = '' + closeButton.AnchorPoint = Vector2.new(0.5, 0.5) + closeButton.Position = UDim2.new(0.5, 0, 0, 45) + closeButton.Size = UDim2.new(0, 90, 0, 90) + closeButton.ZIndex = 7 + local closeButtonImage = Instance.new('ImageLabel', closeButton) + closeButtonImage.BackgroundTransparency = 1 + closeButtonImage.AnchorPoint = Vector2.new(0.5, 0.5) + closeButtonImage.Position = UDim2.new(0, 45, 0, 45) + closeButtonImage.Size = UDim2.new(0, 28, 0, 28) + closeButtonImage.ZIndex = 7 + SpriteManager.equip(closeButtonImage, 'ic-close') + + GetButtonClickEvent(closeButton):connect(function() + this:closeTopMenu() + end) + + local tweenInfo = TweenInfo.new(0.05, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + TweenController(closeButtonImage, tweenInfo, { ImageTransparency = 0 }, { ImageTransparency = 1 }) + + closeButton.Parent = topMenuContainer + + end + + local currentCategory = getCurrentCategory() + for index, category in pairs(Categories) do + local categoryButton = categoryButtonTemplate:clone() + categoryButton.Name = 'CategoryButton'..category.name + + if LayoutInfo.isLandscape then + categoryButton.Position = UDim2.new(0.5, -45, 0, 90 * (index - 1)) + categoryButton.TextLabel.Text = category.titleLandscape or category.title + + if category == currentCategory then + SpriteManager.equip(categoryButton.CircleLabel, "icon-border-on") + SpriteManager.equip(categoryButton.IconLabel, category.selectedIconImageName) + categoryButton.TextLabel.TextColor3 = Color3.fromRGB(255, 161, 47) + else + SpriteManager.equip(categoryButton.CircleLabel, "icon-border") + SpriteManager.equip(categoryButton.IconLabel, category.iconImageName) + end + + categoryButton.CircleLabel.ImageTransparency = 1 + categoryButton.IconLabel.ImageTransparency = 1 + categoryButton.TextLabel.TextTransparency = 1 + delay((index-1)/6*0.2, function() + if categoryButton:IsDescendantOf(game) then + local tweenInfo = TweenInfo.new(0.1, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + local imageGoals = { ImageTransparency = 0 } + local textGoals = { TextTransparency = 0 } + TweenController(categoryButton.CircleLabel, tweenInfo, imageGoals) + TweenController(categoryButton.IconLabel, tweenInfo, imageGoals) + TweenController(categoryButton.TextLabel, tweenInfo, textGoals) + end + end) + else + categoryButton.Position = UDim2.new(1, -(#Categories - index + 1) * 61, .5, -26) + + if category == currentCategory then + SpriteManager.equip(categoryButton, "gr-orange-circle") + SpriteManager.equip(categoryButton.IconLabel, category.selectedIconImageName) + else + SpriteManager.equip(categoryButton, "gr-category-selector") + SpriteManager.equip(categoryButton.IconLabel, category.iconImageName) + end + end + + categoryButton.Visible = true + GetButtonClickEvent(categoryButton):connect(function() + if getCurrentCategoryIndex() == index then + this:closeTopMenu() + else + AppState.Store:dispatch(SelectCategory(index)) + end + end) + categoryButton.Parent = LayoutInfo.isLandscape and topMenuContainer.CategoryScroller or topMenuContainer + + if luaAppLegacyInputDisabledGlobally then + -- CLIAVATAR-1289: If categoryButton.ZIndex is less than its container's ZIndex, Activated will not work + categoryButton.ZIndex = categoryButton.Parent.ZIndex + 1 + end + end + if LayoutInfo.isLandscape then + topMenuContainer.CategoryScroller.CanvasSize = UDim2.new(1, 0, 0, #Categories * 90) + end +end + + +function this:closeTopMenu() + self.closeCategoryMenuEvent:Fire() -- Fire event + + local index = 0 + local currentCategory = getCurrentCategory() + for i, category in pairs(Categories) do -- We need to index of the category, this is how we find it. + if currentCategory == category then + index = i + break + end + end + + tweenTopMenuContainerState(STATE.CLOSED) + topMenuIndexIndicator.Visible = true + + if #Categories == 4 then + topMenuIndexIndicator.Rotation = (index - 1) * 90 --((index-1)/#Categories)*360 + else + SpriteManager.equip(topMenuIndexIndicator, 'ring'..((index - 1) % 5 + 1)) + end + topMenuSelectedIcon.Visible = true + SpriteManager.equip(topMenuSelectedIcon, currentCategory.iconImageName) + local categoryParent = LayoutInfo.isLandscape and topMenuContainer.CategoryScroller or topMenuContainer + for _, child in pairs(categoryParent:GetChildren()) do + if child and child.Parent and string.sub(child.Name, 1, 14) == 'CategoryButton' then + child:Destroy() + end + end + if LayoutInfo.isLandscape then + local closeButton = topMenuContainer:FindFirstChild('CloseButton') + if closeButton then + local tweenInfo = TweenInfo.new(0.05, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + local propGoals = { ImageTransparency = 1 } + TweenController(closeButton, tweenInfo, propGoals).Completed:Connect(function() + closeButton:Destroy() + end) + end + end +end + +local function initTopMenu() + if LayoutInfo.isLandscape then + topMenuContainer.Size = UDim2.new(0, 98, 0, 98) + topMenuContainer.Position = UDim2.new(0, 11, 0, 11) + topMenuContainer.BackgroundFill.Visible = false + + topMenuIndexIndicator.AnchorPoint = Vector2.new(0.5, 0.5) + topMenuIndexIndicator.Position = UDim2.new(0.5, 0, 0.5, 0) + topMenuIndexIndicator.Size = UDim2.new(0, 60, 0, 60) + + topMenuSelectedIcon.AnchorPoint = Vector2.new(0.5, 0.5) + topMenuSelectedIcon.Position = UDim2.new(0.5, 0, 0.5, 0) + + topMenuContainer.RoundedEnd.AnchorPoint = Vector2.new(0, 0) + topMenuContainer.RoundedEnd.Position = UDim2.new(0, 0, 0, 0) + topMenuContainer.RoundedEnd.Size = UDim2.new(1, 0, 1, 0) + topMenuContainer.RoundedEnd.ScaleType = 'Slice' + topMenuContainer.RoundedEnd.SliceCenter = Rect.new(48, 48, 48, 48) + SpriteManager.equip(topMenuContainer.RoundedEnd, 'ctn-primary nav') + + local categoryScroller = Instance.new('ScrollingFrame') + categoryScroller.Name = 'CategoryScroller' + categoryScroller.ScrollBarThickness = 0 + categoryScroller.BackgroundTransparency = 1 + categoryScroller.BorderSizePixel = 0 + categoryScroller.Position = UDim2.new(0, 0, 0, 90) + categoryScroller.Size = UDim2.new(1, 0, 1, -135) + categoryScroller.ZIndex = 7 + + local categoryScrollerSizeConstraint = Instance.new('UISizeConstraint') + categoryScrollerSizeConstraint.Parent = categoryScroller + categoryScroller.Parent = topMenuContainer + end +end + +local function init(state) + if state.FullView then + setTopMenuContainerState(STATE.HIDDEN, 0) + else + setTopMenuContainerState(STATE.CLOSED, 0) + end + + local savedCategory = state.Category.CategoryIndex + if savedCategory then + AppState.Store:dispatch(SelectCategory(savedCategory)) + else + AppState.Store:dispatch(SelectCategory(1)) + end +end + + +local function update(newState, oldState) + if newState.Category.CategoryIndex ~= oldState.Category.CategoryIndex then + this:closeTopMenu() + end + if newState.FullView ~= oldState.FullView then + if newState.FullView then + tweenTopMenuContainerState(STATE.HIDDEN) + else + tweenTopMenuContainerState(STATE.CLOSED) + end + end +end + + +return function( + inTopMenuContainer, + inCategoryButtonTemplate) + + topMenuContainer = inTopMenuContainer + categoryButtonTemplate = inCategoryButtonTemplate + + topMenuIndexIndicator = topMenuContainer.IndexIndicator + topMenuSelectedIcon = topMenuContainer.SelectedIcon + + initTopMenu() + + GetButtonClickEvent(topMenuContainer):connect(function() + this:openTopMenu() + end) + + init(AppState.Store:getState()) + AppState.Store.Changed:Connect(update) + + return this +end + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CategoryMenuConsole.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CategoryMenuConsole.lua new file mode 100644 index 0000000..c881a9c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CategoryMenuConsole.lua @@ -0,0 +1,285 @@ +local GuiService = game:GetService("GuiService") + +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) +local SetConsoleMenuLevel = require(Modules.LuaApp.Actions.SetConsoleMenuLevel) +local SelectCategory = require(Modules.LuaApp.Actions.SelectCategory) + +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfoConsole) +local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local Categories = require(Modules.LuaApp.Legacy.AvatarEditor.Categories) +local TweenController = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController) +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) + +local ShellModules = Modules:FindFirstChild("Shell") +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local GetButtonClickEvent = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonClickEvent) + +local function createCategoryMenu(container) + local this = {} + + local storeChangedCn = nil + + local categoryButtons = {} + + local currentCategoryIndex = nil + + local CategoryMenu = Utilities.create'Frame' + { + Name = 'CategoryMenu'; + Position = LayoutInfo.CategoryMenuPosition; + Size = UDim2.new(0, 360, 1, 0); + BackgroundTransparency = 1; + Parent = container; + } + + Utilities.create'UIListLayout' + { + Name = 'CategoryMenuUIListLayout'; + Padding = UDim.new(0, LayoutInfo.CategoryButtonsPadding); + Parent = CategoryMenu; + } + + local buttonSelector = Utilities.create'ImageLabel' + { + Name = 'CategoryMenuSelector'; + Image = LayoutInfo.CategoryButtonSelectorImage; + Position = UDim2.new(0, -7, 0, -7); + Size = UDim2.new(1, 14, 1, 14); + BackgroundTransparency = 1; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(31, 31, 63, 63); + } + + local function tweenCategoryMenuState(isFullView) + local tweenInfo = TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + + if isFullView then + TweenController(CategoryMenu, tweenInfo, { Position = LayoutInfo.CategoryMenuFullviewPosition }) + else + TweenController(CategoryMenu, tweenInfo, { Position = LayoutInfo.CategoryMenuPosition }) + end + end + + -- Navigate on category button to highlight categoryButton + local function highlightCategoryButton(index) + local categoryButton = categoryButtons[index] + local category = Categories[index] + categoryButton.Image = LayoutInfo.CategoryButtonImageSelected + categoryButton.CategoryIcon.Image = category.selectedIconImageConsole + categoryButton.CategoryIcon.ImageTransparency = 0 + categoryButton.CategoryText.TextColor3 = LayoutInfo.GrayTextColor + categoryButton.CategoryText.TextTransparency = 0 + end + + -- Default categoryButton style + local function defaultCategoryButton(index, categoryButton) + local category = Categories[index] + categoryButton.Image = LayoutInfo.CategoryButtonImageDefault + categoryButton.CategoryIcon.Image = category.iconImageConsole + categoryButton.CategoryIcon.ImageTransparency = 0 + categoryButton.CategoryText.TextColor3 = LayoutInfo.WhiteTextColor + categoryButton.CategoryText.TextTransparency = 0 + end + + -- Inactive categoryButton style + local function inactiveCategoryButton(categoryButton) + categoryButton.CategoryIcon.ImageTransparency = 0.5 + end + + -- Open one of the categories + local function openCategory() + local tweenInfo = TweenInfo.new(LayoutInfo.DefaultTweenTime, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + local sizeGoals = { + Size = LayoutInfo.CategoryButtonSmallSize + } + local textStarts = { + TextTransparency = 0 + } + local textGoals = { + TextTransparency = 1 + } + + for i = 1, #categoryButtons do + local categoryButton = categoryButtons[i] + TweenController(categoryButton, tweenInfo, sizeGoals) + TweenController(categoryButton.CategoryText, tweenInfo, textGoals, textStarts) + if currentCategoryIndex ~= i then + inactiveCategoryButton(categoryButton) + end + end + end + + -- Close category and back to navigation between all categories + local function closeCategory() + local tweenInfo = TweenInfo.new(LayoutInfo.DefaultTweenTime, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + local sizeGoals = { + Size = LayoutInfo.CategoryButtonDefaultSize + } + local textStarts = { + TextTransparency = 1 + } + local textGoals = { + TextTransparency = 0 + } + + for i = 1, #categoryButtons do + local categoryButton = categoryButtons[i] + TweenController(categoryButton, tweenInfo, sizeGoals) + TweenController(categoryButton.CategoryText, tweenInfo, textGoals, textStarts) + if currentCategoryIndex ~= i then + defaultCategoryButton(i, categoryButton) + end + end + end + + local function selectCategory(index) + if categoryButtons[currentCategoryIndex] then + defaultCategoryButton(currentCategoryIndex, categoryButtons[currentCategoryIndex]) + end + highlightCategoryButton(index) + currentCategoryIndex = index + end + + -- Select current category when initializes or jumps back from tabs + local function selectCurrentCategory() + closeCategory() + + local newCurrentCategoryIndex = AppState.Store:getState().Category.CategoryIndex or 1 + if newCurrentCategoryIndex == currentCategoryIndex then + highlightCategoryButton(currentCategoryIndex) + end + + -- Select new button, if newCurrentCategoryIndex ~= currentCategoryIndex, SelectCategory action will be dispatched + GuiService.SelectedCoreObject = categoryButtons[newCurrentCategoryIndex] + end + + local function createCategoryButton(container, index, image, title) + local CategoryButton = Utilities.create'ImageButton' + { + Name = 'Category'..index; + Size = LayoutInfo.CategoryButtonDefaultSize; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = LayoutInfo.CategoryButtonImageDefault; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(8, 8, 9, 9); + Parent = container; + Selectable = true; + SelectionImageObject = buttonSelector; + } + + local MoveSelectionSound = SoundManager:CreateSound('MoveSelection') + MoveSelectionSound.Parent = CategoryButton + + Utilities.create'ImageLabel' + { + Name = 'CategoryIcon'; + AnchorPoint = Vector2.new(0, 0.5); + Position = UDim2.new(0, 24, 0.5, 0); + Size = LayoutInfo.CategoryIconSize; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = image; + ImageTransparency = 0; + Parent = CategoryButton; + } + + Utilities.create'TextLabel' + { + Name = 'CategoryText'; + AnchorPoint = Vector2.new(0, 0.5); + Position = UDim2.new(0, 80, 0.5, 0); + Size = LayoutInfo.CategoryTextSize; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Text = title; + TextXAlignment = 'Left'; + TextColor3 = LayoutInfo.WhiteTextColor; + TextTransparency = 0; + TextSize = LayoutInfo.ButtonFontSize; + Font = LayoutInfo.RegularFont; + Parent = CategoryButton; + } + + CategoryButton.SelectionGained:connect(function() + if currentCategoryIndex ~= index then + AppState.Store:dispatch(SelectCategory(index)) + end + end) + + GetButtonClickEvent(CategoryButton):connect(function() + SoundManager:Play('OverlayOpen') + AppState.Store:dispatch(SetConsoleMenuLevel(LayoutInfo.ConsoleMenuLevel.TabList)) + end) + + return CategoryButton + end + + local function createCategoryButtons() + for index, category in pairs(Categories) do + local categoryButton = createCategoryButton(CategoryMenu, index, category.iconImageConsole, category.title) + table.insert(categoryButtons, categoryButton) + end + end + + local function resetCategoryMenu() + currentCategoryIndex = nil + for i = 1, #categoryButtons do + local categoryButton = categoryButtons[i] + defaultCategoryButton(i, categoryButton) + end + end + + local function update(newState, oldState) + if newState.ConsoleMenuLevel ~= oldState.ConsoleMenuLevel then + if newState.ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.CategoryMenu then + selectCurrentCategory() + elseif newState.ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.TabList and + oldState.ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.CategoryMenu then + openCategory() + end + end + + if newState.Category.CategoryIndex ~= oldState.Category.CategoryIndex then + if newState.Category.CategoryIndex then + selectCategory(newState.Category.CategoryIndex) + end + end + + if newState.FullView ~= oldState.FullView then + tweenCategoryMenuState(newState.FullView) + end + end + + function this:SelectNextPage() + local nextIndex = math.min(currentCategoryIndex + LayoutInfo.CategoryButtonRowsPerPage, #categoryButtons) + GuiService.SelectedCoreObject = categoryButtons[nextIndex] + end + + function this:SelectPreviousPage() + local prevIndex = math.max(currentCategoryIndex - LayoutInfo.CategoryButtonRowsPerPage, 1) + GuiService.SelectedCoreObject = categoryButtons[prevIndex] + end + + function this:Focus() + storeChangedCn = AppState.Store.Changed:Connect(update) + GuiService:AddSelectionParent("CategoryMenu", CategoryMenu) + end + + function this:RemoveFocus() + storeChangedCn = Utilities.disconnectEvent(storeChangedCn) + GuiService:RemoveSelectionGroup("CategoryMenu") + end + + function this:Hide() + resetCategoryMenu() + end + + createCategoryButtons() + + return this +end + +return createCategoryMenu diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CharacterManager.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CharacterManager.lua new file mode 100644 index 0000000..9a6663c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/CharacterManager.lua @@ -0,0 +1,2457 @@ +-------------- CONSTANTS -------------- +local MAX_RECENT_ASSETS = 200 + +-- These sync up with chat username color order. The last clothing is a teal +-- color instead of a tan color like the usernames. +-- Username color list: "Bright red", "Bright blue", "Earth green", "Bright violet", +-- "Bright orange", "Bright yellow", "Light reddish violet", "Brick yellow" +local BODY_COLOR_NAME_MAP = { + ["HeadColor"] = 'headColorId', + ["LeftArmColor"] = 'leftArmColorId', + ["LeftLegColor"] = 'leftLegColorId', + ["RightArmColor"] = 'rightArmColorId', + ["RightLegColor"] = 'rightLegColorId', + ["TorsoColor"] = 'torsoColorId', +} + +local BODY_COLOR_MAPPED_PARTS = { + ['Head'] = 'HeadColor', + + ['Torso'] = 'TorsoColor', + ['UpperTorso'] = 'TorsoColor', + ['LowerTorso'] = 'TorsoColor', + + ['Left Arm'] = 'LeftArmColor', + ['LeftUpperArm'] = 'LeftArmColor', + ['LeftLowerArm'] = 'LeftArmColor', + ['LeftHand'] = 'LeftArmColor', + + ['Left Leg'] = 'LeftLegColor', + ['LeftUpperLeg'] = 'LeftLegColor', + ['LeftLowerLeg'] = 'LeftLegColor', + ['LeftFoot'] = 'LeftLegColor', + + ['Right Arm'] = 'RightArmColor', + ['RightUpperArm'] = 'RightArmColor', + ['RightLowerArm'] = 'RightArmColor', + ['RightHand'] = 'RightArmColor', + + ['Right Leg'] = 'RightLegColor', + ['RightUpperLeg'] = 'RightLegColor', + ['RightLowerLeg'] = 'RightLegColor', + ['RightFoot'] = 'RightLegColor', +} + +local DEFAULT_SCALES = { + Height = 1.00, + Width = 1.00, + Depth = 1.00, + Head = 1.00, +} + +local DEFAULT_BODY_COLORS = { + ["HeadColor"] = 194, + ["LeftArmColor"] = 194, + ["LeftLegColor"] = 194, + ["RightArmColor"] = 194, + ["RightLegColor"] = 194, + ["TorsoColor"] = 194, +} + +-------------- SERVICES -------------- +local HttpService = game:GetService('HttpService') +local MarketplaceService = game:GetService('MarketplaceService') +local InsertService = game:GetService('InsertService') +local CoreGui = game:GetService('CoreGui') + +------------ MODULES ------------------- +local Modules = CoreGui:FindFirstChild("RobloxGui").Modules + +local DefaultClothesIds = require(Modules.LuaApp.Legacy.AvatarEditor.DefaultClothesIds) +local AssetTypeNames = require(Modules.LuaApp.Legacy.AvatarEditor.AssetTypeNames) +local Categories = require(Modules.LuaApp.Legacy.AvatarEditor.Categories) +local AssetInfo = require(Modules.LuaApp.Legacy.AvatarEditor.AssetInfo) +local Urls = require(Modules.LuaApp.Legacy.AvatarEditor.Urls) +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfo) +local ParticleScreen = require(Modules.LuaApp.Legacy.AvatarEditor.ParticleScreen) + +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) + +local SetOutfit = require(Modules.LuaApp.Actions.SetOutfit) +local SetBodyColors = require(Modules.LuaApp.Actions.SetBodyColors) +local SetScales = require(Modules.LuaApp.Actions.SetScales) +local SetAssets = require(Modules.LuaApp.Actions.SetAssets) +local SetAvatarType = require(Modules.LuaApp.Actions.SetAvatarType) + +local function getModule(moduleName) + return script.Parent:FindFirstChild(moduleName) +end + +local tween = require(getModule('TweenPropertyController')).tween +local easeFilters = require(getModule('EaseFilters')) + +----------- UTILITIES -------------- +local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local TableUtilities = require(Modules.LuaApp.TableUtilities) + +-------------- FFLAGS -------------- +local AvatarEditorAnthroSliders = Flags:GetFlag("AvatarEditorAnthroSlidersUIOnly") +local AvatarEditorSelectivelyUseDefaultAsset = Flags:GetFlag("AvatarEditorSelectivelyUseDefaultAsset") + +local AvatarEditorUseNewR15Model = Flags:GetFlag("AvatarEditorUseNewR15Model") +local AvatarEditorUseArtistIntentFolder = Flags:GetFlag("AvatarEditorUseArtistIntentFolder") + +local AvatarEditorRecomputeCameraLookAt = + Flags:GetFlag("AvatarEditorRecomputeCameraLookAt") + +local GetDefaultClothesFromWeb = + Flags:GetFlag("GetDefaultClothesFromWeb") + +if AvatarEditorAnthroSliders then + DEFAULT_SCALES = { + Height = 1.00, + Width = 1.00, + Depth = 1.00, + Head = 1.00, + BodyType = 0.00, + Proportion = 0.00, + } +end + +local useAnthroValues = Flags:GetFlag("UseAdvancedCharacterScales3") + + +----------- CLASS DECLARATION -------------- + +return function(webServer, characterTemplates, defaultClothesIndex) + local this = {} + + local isInitialAssets = true + local recentAssetLists = {} + local assetsLinkedContent = {} + + local savedWearingAssets = {} + local savedAvatarType = nil + local savedScales = DEFAULT_SCALES + local savedBodyColors = DEFAULT_BODY_COLORS + + local wearOutfitRequestCount = 0 + + local resumeAnimationEvent = Instance.new('BindableEvent') + local currentLookAroundAnimation = 0 + local currentAnimationPreview = nil + + local defaultAnimation = 'IdleAnimation' + local rootAnimationRotation = {x=0, y=0, z=0} + local animationIsPaused = false + + local defaultShirtModel = nil + local defaultPantsModel = nil + + local myDefaultShirtTemplate = nil + local myDefaultPantsTemplate = nil + local myDefaultShirtTemplateTemp = nil + local myDefaultPantsTemplateTemp = nil + local useCurrentlyLoadedDefaultClothesAssets = false + + local templateCharacterR6 = characterTemplates['CharacterR6'] + + local templateCharacterR15 + if not AvatarEditorUseNewR15Model then + templateCharacterR15 = characterTemplates['CharacterR15'] + else + templateCharacterR15 = characterTemplates['CharacterR15New'] + end + + local waitingForInitialLoad = true + + local characterNode = nil + local currentCharacter + + -- Can delete partRestingOffsets along with AvatarEditorRecomputeCameraLookAt clean up + local partRestingOffsets = {} + local toolHoldAnimationTrack = nil + local itemsOnR15 = {} + + local buildingCharacterLock = false + local queuedRebuild = "" + + local createR15Rig = function() end + local createR6Rig = function() end + local updateCamera = function() end + + local minDeltaEBodyColorDifference = 0 + + local cachedOldState + local cachedNewState + local handleStateChanged + + + ParticleScreen.init() + + if not GetDefaultClothesFromWeb then + -- initDefaultClothes + Utilities.fastSpawn(function() + local myColorIndex = ((defaultClothesIndex-1) % DefaultClothesIds.getDefaultClothesCount()) + 1 + myDefaultShirtTemplate = InsertService:LoadAsset( + DefaultClothesIds.getDefaultShirtIds()[myColorIndex]):GetChildren()[1] + myDefaultShirtTemplate.Name = 'ShirtDefault' + myDefaultPantsTemplate = InsertService:LoadAsset( + DefaultClothesIds.getDefaultPantIds()[myColorIndex]):GetChildren()[1] + myDefaultPantsTemplate.Name = 'PantsDefault' + end) + else + -- Keep these default clothes for efficiency on prod + Utilities.fastSpawn(function() + local myColorIndex = ((defaultClothesIndex-1) % DefaultClothesIds.getDefaultClothesCount()) + 1 + + myDefaultShirtTemplateTemp = InsertService:LoadAsset( + DefaultClothesIds.getDefaultShirtIds()[myColorIndex]):GetChildren()[1] + myDefaultPantsTemplateTemp = InsertService:LoadAsset( + DefaultClothesIds.getDefaultPantIds()[myColorIndex]):GetChildren()[1] + + -- If the web call returned first, then set the templates immediately. + if useCurrentlyLoadedDefaultClothesAssets then + myDefaultShirtTemplate = myDefaultShirtTemplateTemp + myDefaultShirtTemplate.Name = 'ShirtDefault' + myDefaultPantsTemplate = myDefaultPantsTemplateTemp + myDefaultPantsTemplate.Name = 'PantsDefault' + end + end) + + -- Initialize the default clothes from the web + Utilities.fastSpawn(function() + local avatarFetchRequest = Utilities.httpGet(Urls.avatarUrlPrefix.."/v1/avatar-rules") + avatarFetchRequest = Utilities.decodeJSON(avatarFetchRequest) + + if avatarFetchRequest then + local defaultClothingAssetLists = avatarFetchRequest['defaultClothingAssetLists'] + local defaultShirtAssetIds = defaultClothingAssetLists['defaultShirtAssetIds'] + local defaultPantsAssetIds = defaultClothingAssetLists['defaultPantAssetIds'] + local myColorIndex = ((defaultClothesIndex-1) % #defaultShirtAssetIds) + 1 + + -- If the id's are the same, don't call LoadAsset again. Dump cached values in case + -- LoadAsset already finished first. + if defaultShirtAssetIds[myColorIndex] == DefaultClothesIds.getDefaultShirtIds()[myColorIndex] + and defaultPantsAssetIds[myColorIndex] == DefaultClothesIds.getDefaultPantIds()[myColorIndex] then + useCurrentlyLoadedDefaultClothesAssets = true + myDefaultShirtTemplate = myDefaultShirtTemplateTemp + myDefaultPantsTemplate = myDefaultPantsTemplateTemp + else + myDefaultShirtTemplate = InsertService:LoadAsset(defaultShirtAssetIds[myColorIndex]):GetChildren()[1] + myDefaultPantsTemplate = InsertService:LoadAsset(defaultPantsAssetIds[myColorIndex]):GetChildren()[1] + end + + if myDefaultShirtTemplate and myDefaultPantsTemplate then + myDefaultShirtTemplate.Name = 'ShirtDefault' + myDefaultPantsTemplate.Name = 'PantsDefault' + end + end + end) + end + + local function getRecentAssetList(name) + return recentAssetLists[name] + end + + local function createRecentAssetList(name) + recentAssetLists[name] = {} + return getRecentAssetList(name) + end + + function this.getOrCreateRecentAssetList(name) + return getRecentAssetList(name) or createRecentAssetList(name) + end + + local function removeFromRecentAssetList(name, index) + table.remove(this.getOrCreateRecentAssetList(name), index) + end + + function this.addToRecentAssetList(name, item) + local list = this.getOrCreateRecentAssetList(name) + + for i=#list, 1, -1 do + if list[i] == item then + removeFromRecentAssetList(name, i) + end + end + + table.insert(list, 1, item) + while #list > MAX_RECENT_ASSETS do + removeFromRecentAssetList(name, MAX_RECENT_ASSETS) + end + end + + local function addAssetToRecentAssetList(assetId) + -- add to the All list + this.addToRecentAssetList('allAssets', assetId) + + -- add to specific lists + Utilities.fastSpawn(function() + local assetInfo = AssetInfo.getAssetInfo(assetId) + if assetInfo then + local assetTypeName = AssetTypeNames[assetInfo.AssetTypeId] or 'Failed Name' + if string.find(assetTypeName, 'Accessory') + or assetTypeName == 'Shirt' + or assetTypeName == 'Pants' + or assetTypeName == 'Gear' + or assetTypeName == 'Hat' + or assetTypeName == '' then + this.addToRecentAssetList('clothing', assetId) + elseif assetTypeName == 'Left Arm' + or assetTypeName == 'Left Leg' + or assetTypeName == 'Right Arm' + or assetTypeName == 'Right Leg' + or assetTypeName == 'Head' + or assetTypeName == 'Torso' + or assetTypeName == 'Face' then + this.addToRecentAssetList('body', assetId) + elseif string.find(assetTypeName, 'Animation') then + this.addToRecentAssetList('animation', assetId) + end + end + end) + end + + + local function getAllEquippedAssets() + local result = {} + local assets = AppState.Store:getState().Character.Assets + + for _, assetList in pairs(assets) do + for _, assetId in pairs(assetList) do + table.insert(result, assetId) + end + end + + return result + end + + local function getAvatarType() + return AppState.Store:getState().Character.AvatarType + end + + local function getScales() + return AppState.Store:getState().Character.Scales + end + + local function getBodyColors() + return AppState.Store:getState().Character.BodyColors + end + + local function setSavedScales(scales) + savedScales = Utilities.copyTable(scales) + end + + local function setSavedBodyColors(colors) + savedBodyColors = Utilities.copyTable(colors) + end + + local function syncSaved() + savedWearingAssets = getAllEquippedAssets() + end + + + function this.isWearingAssetType(assetTypeName) + for _, assetId in pairs(getAllEquippedAssets()) do + if AssetTypeNames[AssetInfo.getAssetInfo(assetId)] == assetTypeName then + return true + end + end + return false + end + + function this.getCompleteAssetInfo(assetId) + -- Ensures that certain fields are filled out. The new web endpoint doesn't return descriptions + -- so we need to get them as we need them. + local assetInfo = AssetInfo.getAssetInfo(assetId) + + if assetInfo and not assetInfo.Description then + local productInfo = MarketplaceService:GetProductInfo(assetId) + assetInfo.Description = productInfo.Description + end + + return assetInfo + end + + function this.findIfEquipped(assetId) + for _,currentAssetId in pairs(getAllEquippedAssets()) do + if currentAssetId == assetId then + return true + end + end + return false + end + + + function this.saveToServer(doWait) + if waitingForInitialLoad then + warn("Did not save because initial character hasn't fully loaded") + return + end + local bodyColorsFinished = false + Utilities.fastSpawn(function() + if not this.Destroyed then + local bodyColorsChanged = false + local bodyColors = getBodyColors() + for index, value in pairs(bodyColors) do + if value ~= savedBodyColors[index] then + bodyColorsChanged = true + break + end + end + if bodyColorsChanged then + local sendingBodyColorsTable = {} + for name,sendingName in pairs(BODY_COLOR_NAME_MAP) do + sendingBodyColorsTable[sendingName] = bodyColors[name] + end + local sendingBodyColorsData = HttpService:JSONEncode(sendingBodyColorsTable) + local successfulSave = webServer.post(Urls.avatarUrlPrefix.."/v1/avatar/set-body-colors", sendingBodyColorsData) + if successfulSave then + setSavedBodyColors(bodyColors) + else + warn('Failure Saving BodyColors') + end + end + end + bodyColorsFinished = true + end) + + local scalesFinished = false + + local function finishSavingScales() + if not this.Destroyed then + local scalesChanged = false + local scales = getScales() + for index, value in pairs(scales) do + if value ~= savedScales[index] then + scalesChanged = true + break + end + end + if scalesChanged then + local sendingScalesData + if AvatarEditorAnthroSliders then + sendingScalesData = + '{"depth":' + ..string.format("%.4f", scales.Depth) + ..',"height":' + ..string.format("%.4f", scales.Height) + ..',"width":' + ..string.format("%.4f", scales.Width) + ..',"head":' + ..string.format("%.4f", scales.Head) + ..',"bodyType":' + ..string.format("%.4f", scales.BodyType) + ..',"proportion":' + ..string.format("%.4f", scales.Proportion) + ..'}' + else + sendingScalesData = + '{"depth":' + ..string.format("%.4f", scales.Depth) + ..',"height":' + ..string.format("%.4f", scales.Height) + ..',"width":' + ..string.format("%.4f", scales.Width) + ..',"head":' + ..string.format("%.4f", scales.Head) + ..'}' + end + + local successfulSave = webServer.post(Urls.avatarUrlPrefix.."/v1/avatar/set-scales", sendingScalesData) + if successfulSave then + setSavedScales(scales) + else + warn('Failure Saving Scales') + end + end + end + scalesFinished = true + end + + Utilities.fastSpawn(finishSavingScales) + + local avatarTypeFinished = false + Utilities.fastSpawn(function() + if not this.Destroyed then + local newAvatarType = getAvatarType() + local avatarTypeChanged = savedAvatarType ~= newAvatarType + if avatarTypeChanged then + local successfulSave = webServer.post(Urls.avatarUrlPrefix.."/v1/avatar/set-player-avatar-type", + '{"playerAvatarType":"'..newAvatarType..'"}') + if successfulSave then + savedAvatarType = newAvatarType + else + warn('Failure Saving AvatarType') + end + end + end + avatarTypeFinished = true + end) + + local assetsFinished = false + Utilities.fastSpawn(function() + if not this.Destroyed then + local assetsChanged = false + local currentlyWearing = getAllEquippedAssets() + for index, value in pairs(currentlyWearing) do + if value ~= savedWearingAssets[index] then + assetsChanged = true + break + end + end + for index, value in pairs(savedWearingAssets) do + if value ~= currentlyWearing[index] then + assetsChanged = true + break + end + end + if assetsChanged then + + local sendingAssetsData = HttpService:JSONEncode({['assetIds']=currentlyWearing}) + local successfulSave = + webServer.post( + Urls.avatarUrlPrefix.."/v1/avatar/set-wearing-assets", sendingAssetsData) + if successfulSave then + savedWearingAssets = Utilities.copyTable(currentlyWearing) + else + warn('Failure Saving WearingAssets') + end + end + end + assetsFinished = true + end) + + if doWait then + while not (bodyColorsFinished + and avatarTypeFinished + and assetsFinished + and scalesFinished) do + wait() + end + return + end + end + + -- Can delete along with AvatarEditorRecomputeCameraLookAt clean up + local function getRestingPartOffset(partName) + return partRestingOffsets[partName] or CFrame.new() + end + + -- Can delete along with AvatarEditorRecomputeCameraLookAt clean up + local function recalculateRestingPartOffsets() + local root = currentCharacter:FindFirstChild('HumanoidRootPart') + + local joints = {} + for _, v in next, Utilities.getDescendants(currentCharacter) do + if v:IsA('Motor6D') then + if joints[v.Part0] == nil then + joints[v.Part0] = {v} + else + table.insert(joints[v.Part0], v) + end + if joints[v.Part1] == nil then + joints[v.Part1] = {v} + else + table.insert(joints[v.Part1], v) + end + end + end + + if root then + local open = {[root]=CFrame.new()} + local closed = {[root]=CFrame.new()} + + while next(open) do + local newOpen = {} + for part, cframe in next, open do + if joints[part] then + for _, joint in next, joints[part] do + local thisCFrame + local other + + if joint.Part1 == part then + other = joint.Part0 + thisCFrame = cframe * joint.C1 * joint.C0:inverse() + else + other = joint.Part1 + thisCFrame = cframe * joint.C0 * joint.C1:inverse() + end + + if other and not closed[other] then + newOpen[other] = thisCFrame + closed[other] = thisCFrame + end + end + end + end + open = newOpen + end + + partRestingOffsets = {} + for part, cframe in next, closed do + partRestingOffsets[part.Name] = cframe + end + end + end + + + local function getCharacterNode() + if this.Destroyed then + return nil + else + if characterNode == nil then + characterNode = Instance.new("Folder") + end + return characterNode + end + end + + + local function humanoidLoadAnimation(animation) + local characterNodeParent = getCharacterNode().Parent + getCharacterNode().Parent = game.Workspace + local humanoid = currentCharacter:WaitForChild('Humanoid') + local result = humanoid:LoadAnimation(animation) + getCharacterNode().Parent = characterNodeParent + return result + end + + + local function holdToolPos(state) + if toolHoldAnimationTrack then + toolHoldAnimationTrack:Stop() + toolHoldAnimationTrack = nil + end + if currentCharacter and currentCharacter.Parent then + local humanoid = currentCharacter:WaitForChild('Humanoid') + if humanoid and humanoid:IsDescendantOf(game.Workspace) and state == 'Up' then + local animationsFolder = currentCharacter:FindFirstChild('Animations') + if animationsFolder then + local toolHoldAnimationObject = animationsFolder:FindFirstChild('Tool') + if toolHoldAnimationObject then + toolHoldAnimationTrack = humanoidLoadAnimation(toolHoldAnimationObject) + toolHoldAnimationTrack:Play(0) + end + end + end + end + end + + local function findFirstMatchingAttachment(model, name) + if model and name then + for _, child in pairs(model:GetChildren()) do + if child then + if child:IsA('Attachment') and (not name or child.Name == name) then + return child + elseif child:IsA('Accoutrement') ~= true and child:IsA('Tool') ~= true then + local foundAttachment = findFirstMatchingAttachment(child, name) + if foundAttachment then + return foundAttachment + end + end + end + end + end + end + + local function buildWeld(part0, part1, c0, c1, weldName) + local weld = Instance.new('Weld') + weld.C0 = c0 + weld.C1 = c1 + weld.Part0 = part0 + weld.Part1 = part1 + weld.Name = weldName + weld.Parent = part0 + return weld + end + + local function equipItemToCharacter(item) -- Item should be an accessory or tool. + if item and currentCharacter then + item.Parent = currentCharacter + local handle = item:FindFirstChild('Handle') + if handle then + handle.CanCollide = false + + local attachment = Utilities.findFirstChildOfType(handle,'Attachment') + local matchingAttachment = nil + local matchingAttachmentPart = nil + if attachment then + matchingAttachment = findFirstMatchingAttachment(currentCharacter, attachment.Name) + if matchingAttachment then + matchingAttachmentPart = matchingAttachment.Parent + end + end + if matchingAttachmentPart then -- This infers that both attachments were found + buildWeld( + handle, + matchingAttachmentPart, + attachment.CFrame, + matchingAttachment.CFrame, + "AccessoryWeld") + else + if item:IsA('Accoutrement') then + local head = currentCharacter:FindFirstChild('Head') + if head then + buildWeld(handle, head, item.AttachmentPoint, CFrame.new(0,.5,0), "AccessoryWeld") + end + elseif item:IsA('Tool') then + local rightHand = currentCharacter:FindFirstChild('RightHand') + local rightArm = currentCharacter:FindFirstChild('Right Arm') + if rightHand then + local gripCF = CFrame.new(0.0108650923, -0.168664441, -0.0154389441, 1, 0, -0, 0, + 6.12323426e-017, 1, 0, -1, 6.12323426e-017) + buildWeld(handle, rightHand, item.Grip, gripCF, "RightGrip") + elseif rightArm then + local gripCF = CFrame.new(Vector3.new(0,-1,0))*CFrame.Angles(-math.pi*.5,0,0) + buildWeld(handle, rightArm, item.Grip, gripCF, "RightGrip") + end + end + + end + end + end + end + + + local function sortAndEquipItemToCharacter(character, thing, assetInsertedContentList) + if thing then + Utilities.recursiveDisable(thing) + + if thing:IsA("DataModelMesh") then -- Head mesh + local head = currentCharacter:FindFirstChild('Head') + if head then + local replacedAsset = Utilities.findFirstChildOfType(head, "DataModelMesh") + if replacedAsset then + replacedAsset:Destroy() + end + thing.Parent = head + table.insert(assetInsertedContentList, thing) + end + + elseif thing:IsA("Decal") then -- Face + local head = currentCharacter:FindFirstChild('Head') + if head then + local replacedAsset = Utilities.findFirstChildOfType(head, "Decal") + if replacedAsset then + replacedAsset:Destroy() + end + thing.Parent = head + table.insert(assetInsertedContentList, thing) + end + + elseif thing:IsA("CharacterAppearance") then -- Thing, just parent it. + thing.Parent = currentCharacter + table.insert(assetInsertedContentList, thing) + + elseif thing:IsA("Accoutrement") then -- Hat + equipItemToCharacter(thing) + table.insert(assetInsertedContentList, thing) + + elseif thing:IsA("Tool") then -- Gear + equipItemToCharacter(thing) + table.insert(assetInsertedContentList, thing) + holdToolPos('Up') + end + end + end + + + local function replaceHead(newMesh) + if currentCharacter then + if currentCharacter:FindFirstChild('Head') and currentCharacter.Head:FindFirstChild('Mesh') then + currentCharacter.Head.Mesh:Destroy() + end + + newMesh.Parent = currentCharacter.Head + end + end + + + local function replaceFace(newFace) + if currentCharacter then + if currentCharacter:FindFirstChild('Head') and currentCharacter.Head:FindFirstChild('face') then + currentCharacter.Head.face:Destroy() + end + + newFace.Parent = currentCharacter.Head + end + end + + + local function repositionR15Joints(joints) + for _, v in next, joints or Utilities.getDescendants(currentCharacter) do + if v:IsA('JointInstance') then + local attachment0 = v.Part0:FindFirstChild(v.Name..'RigAttachment') + local attachment1 = v.Part1:FindFirstChild(v.Name..'RigAttachment') + + if attachment0 and attachment1 then + v.C0 = attachment0.CFrame + v.C1 = attachment1.CFrame + end + end + end + end + + local function adjustHeightToStandOnPlatform(character) + local hrp = character:WaitForChild('HumanoidRootPart') + local humanoid = character:WaitForChild('Humanoid') + + local heightBonus = 3 + if getAvatarType() == "R15" then + heightBonus = hrp.Size.y * 0.5 + end + + heightBonus = heightBonus + humanoid.HipHeight + + local _,_,_, r0,r1,r2, r3,r4,r5, r6,r7,r8 = hrp.CFrame:components() + + hrp.CFrame = CFrame.new( + 15.2762, heightBonus + 0.7100, -16.8212, + r0,r1,r2, r3,r4,r5, r6,r7,r8 ) + end + + local function engineScaleCharacter(character, newScale) + if getAvatarType() == 'R6' then return end + + local humanoid = character and character.Humanoid + + local function applyScaleProperty(name, scale) + local tag = humanoid and humanoid:FindFirstChild(name) + if tag == nil then + tag = Instance.new('NumberValue') + tag.Name = name + tag.Parent = humanoid + end + tag.Value = scale + end + + if humanoid then + applyScaleProperty('BodyDepthScale', newScale.Depth) + applyScaleProperty('BodyHeightScale', newScale.Height) + applyScaleProperty('BodyWidthScale', newScale.Width) + applyScaleProperty('HeadScale', newScale.Head) + applyScaleProperty('BodyTypeScale', newScale.BodyType) + applyScaleProperty('BodyProportionScale', newScale.Proportion) + humanoid:BuildRigFromAttachments() + adjustHeightToStandOnPlatform(character) + end + end + + local function scaleCharacter(character, newBodyScale, newHeadScale) + if getAvatarType() == 'R6' then return end + + local humanoid = character:WaitForChild('Humanoid') + + local bodyScaleVector = newBodyScale + local headScaleVector = Vector3.new(newHeadScale, newHeadScale, newHeadScale) + + local jointInfo = {} + local parts = {} + local joints = {} + + for _, child in next, Utilities.getDescendants(character) do + if child:IsA('JointInstance') then + jointInfo[child] = {Part0=child.Part0, Part1=child.Part1, Parent=child.Parent} + table.insert(joints, child) + end + end + + for _, part in next, character:GetChildren() do + if part:IsA('BasePart') and part.Name ~= 'HumanoidRootPart' then + local defaultScale = part:FindFirstChild('DefaultScale') + and part.DefaultScale.Value or Vector3.new(1, 1, 1) + + local originalSize = part:FindFirstChild('OriginalSize') and part.OriginalSize.Value + if not originalSize then + local value = Instance.new'Vector3Value' + value.Name = 'OriginalSize' + value.Value = part.Size + value.Parent = part + originalSize = value.Value + end + + local newScaleVector3 = part.Name == 'Head' and headScaleVector or bodyScaleVector + local currentScaleVector3 = part.Size/originalSize * defaultScale + local relativeScaleVector3 = newScaleVector3/currentScaleVector3 + + for _, child in next, part:GetChildren() do + if child:IsA('Attachment') then + local pivot = child.Position + child.Position = pivot * relativeScaleVector3 + elseif child:IsA('SpecialMesh') then + child.Scale = child.Scale * relativeScaleVector3 + end + end + + part.Size = originalSize * newScaleVector3/defaultScale + + table.insert(parts, part) + end + end + + for joint, info in next, jointInfo do + joint.Part0 = info.Part0 + joint.Part1 = info.Part1 + joint.Parent = info.Parent + end + + for _, part in next, parts do + part.Parent = character + end + + repositionR15Joints(joints) + + humanoid.HipHeight = 1.5 * newBodyScale.y + end + + + local function refreshCharacterScale(character) + local scales = getScales() + if AvatarEditorAnthroSliders then + engineScaleCharacter(character, scales) + else + scaleCharacter( + character, + Vector3.new( + scales.Width, + scales.Height, + scales.Depth), + scales.Head + ) + end + end + + + local function updateCharacterBodyColors(bodyColors) + bodyColors = bodyColors or getBodyColors() + if currentCharacter then + for _,v in pairs(currentCharacter:GetChildren()) do + local foundLink = BODY_COLOR_MAPPED_PARTS[v.Name] + if v:IsA('BasePart') and foundLink then + local bodyColorNumber = bodyColors[foundLink] + if bodyColorNumber then + v.BrickColor = BrickColor.new(bodyColorNumber) + end + end + end + end + end + + local function getChildrenOfModel(model) + local children = {} + local modelChildren = model:GetChildren() + + if not modelChildren then + return nil + else + for _, piece in next, modelChildren do + table.insert(children, piece) + end + end + + return children + end + + local function amendR15ForItemAddedAsModel(character, model) + local info = { + easyRemove = {}, + replacesR15Parts = {}, + replacesHead = false, + replacesFace = false, + hasTool = false + } + local bodyStuff = {} + local otherStuff = {} + + -- Collect assets + local childrenOfModel = {model} + if childrenOfModel[1].ClassName == 'Model' then + childrenOfModel = childrenOfModel[1]:GetChildren() + end + + for _, childInModel in next, childrenOfModel do + if AvatarEditorUseNewR15Model then + if AvatarEditorUseArtistIntentFolder then + if childInModel.Name:lower() == 'r15artistintent' then + bodyStuff = getChildrenOfModel(childInModel) + + if not bodyStuff then + error("R15ArtistIntent folder is empty on this model!") + end + elseif childInModel.className ~= 'Folder' then + table.insert(otherStuff, childInModel) + end + else + if childInModel.Name:lower() == 'r15fixed' then + bodyStuff = getChildrenOfModel(childInModel) + elseif childInModel.className ~= 'Folder' then + table.insert(otherStuff, childInModel) + end + end + else + if childInModel.Name:lower() == 'r15' then + for _, piece in next, childInModel:GetChildren() do + table.insert(bodyStuff, piece) + end + elseif childInModel.className ~= 'Folder' then + table.insert(otherStuff, childInModel) + end + end + end + + -- Replace body parts + for _, thing in next, bodyStuff do + if thing:IsA('MeshPart') then + info.replacesR15Parts[thing.Name] = true + + if thing.Size.magnitude <= 0.002 then -- lua can't resize parts below 0.2, so just make them invisible + thing.Transparency = 1 + end + + local oldThing = character:FindFirstChild(thing.Name) + if oldThing then + local thingClone = thing:Clone() + + if not useAnthroValues then + thing.Parent = character + end + + local repositionTheseJoints = {} + + -- Reassign old joints, move important stuff to the new part + for _, v in next, Utilities.getDescendants(character) do + if v:IsA('JointInstance') then + if v.Part0 == oldThing then + v.Part0 = thing + table.insert(repositionTheseJoints, v) + elseif v.Part1 == oldThing then + v.Part1 = thing + table.insert(repositionTheseJoints, v) + end + if v.Parent == oldThing then + if thing:FindFirstChild(v.Name) then + thing[v.Name]:Destroy() + end + v.Parent = thing + end + elseif v:IsA('Attachment') then + if v.Parent == oldThing then + if thing:FindFirstChild(v.Name) then + thing[v.Name]:Destroy() + end + if useAnthroValues and v:FindFirstChild("OriginalPosition") then + v["OriginalPosition"]:Destroy() + end + + v.Parent = thing + end + end + end + + oldThing:Destroy() + + for _, v in next, thing:GetChildren() do + if v:IsA('Attachment') then + if thingClone:FindFirstChild(v.Name) then + v.CFrame = thingClone[v.Name].CFrame + v.Axis = thingClone[v.Name].Axis + v.SecondaryAxis = thingClone[v.Name].SecondaryAxis + end + end + end + + if useAnthroValues then + thing.Parent = character + end + + repositionR15Joints(repositionTheseJoints) + end + else + table.insert(otherStuff, thing) + end + end + + -- Equip tool + for _, thing in next, otherStuff do + if thing:IsA('DataModelMesh') then + info.replacesHead = true + replaceHead(thing) + elseif thing:IsA('Decal') then + info.replacesFace = true + replaceFace(thing) + elseif thing:IsA('CharacterAppearance') then + thing.Parent = character + table.insert(info.easyRemove, thing) + + -- have to refresh the character texture because clothes dont update + character.Head.Transparency = character.Head.Transparency+1 + character.Head.Transparency = character.Head.Transparency-1 + elseif thing:IsA('Accoutrement') then + equipItemToCharacter(thing) + table.insert(info.easyRemove, thing) + elseif thing:IsA('Tool') then + equipItemToCharacter(thing) + table.insert(info.easyRemove, thing) + holdToolPos('Up') + info.hasTool = true + end + end + + refreshCharacterScale(character) + updateCharacterBodyColors() + + return info + end + + + + local function replaceR15PartWithDefault(character, assetId, replaceParts) + local info = itemsOnR15[assetId] + itemsOnR15[assetId] = nil + + if info then + if info.easyRemove then + for _, v in next, info.easyRemove do + v:Destroy() + end + end + if (not AvatarEditorSelectivelyUseDefaultAsset) or replaceParts then + if info.replacesR15Parts then + local replaceFolder = Instance.new'Folder' + replaceFolder.Name = 'R15' + + if AvatarEditorUseNewR15Model then + if AvatarEditorUseArtistIntentFolder then + replaceFolder.Name = "R15ArtistIntent" + else + replaceFolder.Name = "R15Fixed" + end + end + + + for v in next, info.replacesR15Parts do + if templateCharacterR15:FindFirstChild(v) then + templateCharacterR15[v]:Clone().Parent = replaceFolder + end + end + + amendR15ForItemAddedAsModel(character, replaceFolder) + end + end + if info.replacesHead then + replaceHead(templateCharacterR15.Head.Mesh:Clone()) + end + if info.replacesFace then + replaceFace(templateCharacterR15.Head.face:Clone()) + end + if info.hasTool then + holdToolPos('Down') + end + + updateCharacterBodyColors() + end + end + + + local function amendR15ForItemAdded(character, assetId, areDefaultReplacementsRequired) + -- InsertService:LoadAsset() is a yeilding call and will break the store. So this needs to be spawned + spawn(function() + replaceR15PartWithDefault(character, assetId, areDefaultReplacementsRequired) + + local model = InsertService:LoadAsset(assetId) + Utilities.recursiveDisable(model) + + local stillWearing = false + local currentlyWearing = getAllEquippedAssets() + for _, v in next, currentlyWearing do + if v == assetId then + stillWearing = true + break + end + end + + if not stillWearing then return end + + local info = amendR15ForItemAddedAsModel(character, model) + + itemsOnR15[assetId] = info + end) + end + + + local function dismissCurrentCharacter() + if currentCharacter then + currentCharacter.Parent = nil + currentCharacter = nil + end + end + + + local function replaceCurrentCharacter(newCharacter) + dismissCurrentCharacter() + + newCharacter.Name = "Character" + if getCharacterNode() then + newCharacter.Parent = getCharacterNode() + + currentCharacter = newCharacter + + for assetId, contentList in pairs(assetsLinkedContent) do + for _,item in pairs(contentList) do + if item then + item:Destroy() + end + end + assetsLinkedContent[assetId] = nil + end + + local hrp = newCharacter:WaitForChild('HumanoidRootPart') + hrp.Anchored = true + +if not AvatarEditorRecomputeCameraLookAt then + recalculateRestingPartOffsets() +end + end + end + + local function updateDefaultShirtAndPants() + if currentCharacter == nil or waitingForInitialLoad then return end + + -- These two if-checks are for cases where the character might be destroyed. + if defaultShirtModel and not currentCharacter:IsAncestorOf(defaultShirtModel) then + defaultShirtModel:Destroy() + defaultShirtModel = nil + end + if defaultPantsModel and not currentCharacter:IsAncestorOf(defaultPantsModel) then + defaultPantsModel:Destroy() + defaultPantsModel = nil + end + + local hasShirt = false + local hasPants = false + for _, assetId in pairs(getAllEquippedAssets()) do + local assetTypeName = AssetTypeNames[AssetInfo.getAssetInfo(assetId).AssetTypeId] + if assetTypeName == "Shirt" then + hasShirt = true + elseif assetTypeName == "Pants" then + hasPants = true + end + end + + local characterShouldHaveDefaultShirt = not hasShirt and not hasPants + local characterShouldHaveDefaultPants = not hasPants + + if characterShouldHaveDefaultShirt or characterShouldHaveDefaultPants then + local rightLegColor = Color3.new(0, 0, 0) + local leftLegColor = Color3.new(0, 0, 0) + local torsoColor = Color3.new(0, 0, 0) + local bodyColors = getBodyColors() + for index, value in pairs(bodyColors) do + if index == "RightLegColor" then + rightLegColor = BrickColor.new(value).Color + elseif index == "LeftLegColor" then + leftLegColor = BrickColor.new(value).Color + elseif index == "TorsoColor" then + torsoColor = BrickColor.new(value).Color + end + end + local minDeltaE = math.min( + Utilities.delta_CIEDE2000(rightLegColor, torsoColor), + Utilities.delta_CIEDE2000(leftLegColor, torsoColor)) + + characterShouldHaveDefaultShirt = + minDeltaE <= minDeltaEBodyColorDifference and characterShouldHaveDefaultShirt + characterShouldHaveDefaultPants = + minDeltaE <= minDeltaEBodyColorDifference and characterShouldHaveDefaultPants + end + + Utilities.fastSpawn(function() + if characterShouldHaveDefaultShirt and not defaultShirtModel then + while not myDefaultShirtTemplate do wait() end + defaultShirtModel = myDefaultShirtTemplate:clone() + defaultShirtModel.Parent = currentCharacter + elseif not characterShouldHaveDefaultShirt and defaultShirtModel then + defaultShirtModel:Destroy() + defaultShirtModel = nil + end + + if characterShouldHaveDefaultPants and not defaultPantsModel then + while not myDefaultPantsTemplate do wait() end + defaultPantsModel = myDefaultPantsTemplate:clone() + defaultPantsModel.Parent = currentCharacter + elseif not characterShouldHaveDefaultPants and defaultPantsModel then + defaultPantsModel:Destroy() + defaultPantsModel = nil + end + + -- Forces the character to recomposite its textures: (this fixes some bug) + local head = currentCharacter:FindFirstChild('Head') + if head then + head.Transparency = head.Transparency+1 + head.Transparency = head.Transparency-1 + end + end) + end + + + local function createRigFromQueue(character) + if queuedRebuild == "R15" then + queuedRebuild = "" + createR15Rig() + elseif queuedRebuild == "R6" then + queuedRebuild = "" + createR6Rig() + else + updateDefaultShirtAndPants() + adjustHeightToStandOnPlatform(character) + end + end + + + createR15Rig = function(callback) + if buildingCharacterLock then + queuedRebuild = "R15" + return + end + buildingCharacterLock = true + + replaceCurrentCharacter(templateCharacterR15:clone()) + + local currentlyWearing = getAllEquippedAssets() + for _, assetId in next, currentlyWearing do + amendR15ForItemAdded(currentCharacter, assetId, true) + end + + refreshCharacterScale(currentCharacter) + updateCharacterBodyColors() + + buildingCharacterLock = false + createRigFromQueue(currentCharacter) + end + + + createR6Rig = function(callback) + if buildingCharacterLock then + queuedRebuild = "R6" + return + end + buildingCharacterLock = true + + replaceCurrentCharacter(templateCharacterR6:clone()) + + local function finishCreatingR6Rig(character) + spawn(function() + for _,assetId in pairs(getAllEquippedAssets()) do + local assetModel = InsertService:LoadAsset(assetId) --Get all waiting over with early + + local insertedStuff = {} + if not assetsLinkedContent[assetId] then + assetsLinkedContent[assetId] = insertedStuff + else + insertedStuff = assetsLinkedContent[assetId] + end + + local stuff = {assetModel} + if stuff[1].className == 'Model' then + stuff = assetModel:GetChildren() + end + + for _,thing in pairs(stuff) do --Equip asset differently depending on what it is. + if string.lower(thing.Name) == 'r6' then + for _,r6SpecificThing in pairs(thing:GetChildren()) do + sortAndEquipItemToCharacter(character, r6SpecificThing, insertedStuff) + end + elseif thing.className ~= 'Folder' then + sortAndEquipItemToCharacter(character, thing, insertedStuff) + end + end + end + + updateCharacterBodyColors() + + buildingCharacterLock = false + createRigFromQueue(character) + end) + end + + finishCreatingR6Rig(currentCharacter) + end + + function this.setMinDeltaEBodyColorDifference(minimumDeltaEBodyColorDifference) + minDeltaEBodyColorDifference = minimumDeltaEBodyColorDifference + end + + function this.hasDefaultShirt() + return defaultShirtModel and true or false + end + + function this.hasDefaultPants() + return defaultPantsModel and true or false + end + + local function setDefaultAnimation(animation) + defaultAnimation = animation + end + + local function setAnimationRotation(x, y, z) + local t = 0.5 + tween(rootAnimationRotation, 'x', 'Number', nil, x, t, easeFilters.quad, easeFilters.easeInOut) + tween(rootAnimationRotation, 'y', 'Number', nil, y, t, easeFilters.quad, easeFilters.easeInOut) + tween(rootAnimationRotation, 'z', 'Number', nil, z, t, easeFilters.quad, easeFilters.easeInOut) + end + + local function pauseAnimation() + animationIsPaused = true + end + + local function resumeAnimation() + if animationIsPaused then + resumeAnimationEvent:Fire() + animationIsPaused = false + end + end + + local function stopAllAnimationTracks() + local humanoid = currentCharacter:WaitForChild('Humanoid') + for _, v in next, humanoid:GetPlayingAnimationTracks() do + if v ~= toolHoldAnimationTrack then + v:Stop() + end + end + end + + local function getDefaultAnimationAssets(assetTypeName) + local anims = {} + + if getAvatarType() == 'R15' then + if assetTypeName == 'ClimbAnimation' then + table.insert(anims, templateCharacterR15.Animations.climb) + elseif assetTypeName == 'FallAnimation' then + table.insert(anims, templateCharacterR15.Animations.fall) + elseif assetTypeName == 'IdleAnimation' then + table.insert(anims, templateCharacterR15.Animations.idle) + elseif assetTypeName == 'JumpAnimation' then + table.insert(anims, templateCharacterR15.Animations.jump) + elseif assetTypeName == 'RunAnimation' then + table.insert(anims, templateCharacterR15.Animations.run) + elseif assetTypeName == 'WalkAnimation' then + table.insert(anims, templateCharacterR15.Animations.walk) + elseif assetTypeName == 'SwimAnimation' then + table.insert(anims, templateCharacterR15.Animations.swim) + table.insert(anims, templateCharacterR15.Animations.swimidle) + else + error('Tried to get bad default animation for R15 '..tostring(assetTypeName)) + end + elseif getAvatarType() == 'R6' then + if assetTypeName == 'ClimbAnimation' then + table.insert(anims, templateCharacterR6.Animations.climb) + elseif assetTypeName == 'FallAnimation' then + table.insert(anims, templateCharacterR6.Animations.fall) + elseif assetTypeName == 'IdleAnimation' then + table.insert(anims, templateCharacterR6.Animations.idle) + elseif assetTypeName == 'JumpAnimation' then + table.insert(anims, templateCharacterR6.Animations.jump) + elseif assetTypeName == 'RunAnimation' then + table.insert(anims, templateCharacterR6.Animations.run) + elseif assetTypeName == 'WalkAnimation' then + table.insert(anims, templateCharacterR6.Animations.walk) + elseif assetTypeName == 'SwimAnimation' then + local swimAnim = templateCharacterR6.Animations.run:Clone() + swimAnim.Name = 'swim' + + table.insert(anims, swimAnim) + else + error('Tried to get bad default animation for R6 '..tostring(assetTypeName)) + end + end + + return anims + end + + local function getAnimationAssets(assetId) + local asset = InsertService:LoadAsset(assetId) + local animAssets = asset.R15Anim:GetChildren() + + return animAssets + end + + local function getEquippedAnimationAssets(assetTypeName) + local assetTypeId = AssetTypeNames[assetTypeName] + local assetId + + local currentlyWearing = getAllEquippedAssets() + if getAvatarType() == 'R15' then + for _, asset in next, currentlyWearing do + local info = AssetInfo.getAssetInfo(asset) + if info and info.AssetTypeId == assetTypeId then + assetId = asset + break + end + end + end + + if assetId then + return getAnimationAssets(assetId) + else + return getDefaultAnimationAssets(assetTypeName) + end + end + + local function getWeightedAnimations(possible) + local options, totalWeight = {}, 0 + + for _, v in next, possible do + local weight = v:FindFirstChild('Weight') and v.Weight.Value or 1 + options[v] = weight + totalWeight = totalWeight + weight + end + + return options, totalWeight + end + + local function playLookAround() + pauseAnimation() + stopAllAnimationTracks() + + local thisLookAroundAnimation = currentLookAroundAnimation + 1 + currentLookAroundAnimation = thisLookAroundAnimation + + local assets = getEquippedAnimationAssets('IdleAnimation') + + local options, _ = getWeightedAnimations(assets[1]:GetChildren()) + + local lightest, lightestWeight + for v, weight in next, options do + if lightest == nil or weight < lightestWeight then + lightest, lightestWeight = v, weight + end + end + + if lightest then + if currentCharacter and currentCharacter.Parent then + local humanoid = currentCharacter:WaitForChild('Humanoid') + if humanoid and humanoid:IsDescendantOf(game.Workspace) then + local track = humanoidLoadAnimation(lightest) + track:Play(0) + wait(track.Length) + track:Stop() + track:Destroy() + end + end + end + + if thisLookAroundAnimation == currentLookAroundAnimation then + resumeAnimation() + end + end + + local function stopAnimationPreview() + if currentAnimationPreview ~= nil then + currentAnimationPreview.Stop() + end + currentAnimationPreview = nil + setAnimationRotation(0, 0, 0) + end + + local function startAnimationPreviewFromAssets(animAssets) -- Array of StringValues containing Animation objects + if this.Destroyed then + return + end + if animationIsPaused then + resumeAnimation() + end + if currentAnimationPreview ~= nil then + stopAnimationPreview() + end + + local thisAnimationPreview = {} + currentAnimationPreview = thisAnimationPreview + + if thisAnimationPreview ~= currentAnimationPreview then return end + + local stop = false + local stopCurrentLoop = false + local pauseMainLoop = false + local switch = false + local currentAnim + local currentTrack + local isMultipleAnims = #animAssets > 1 + local currentAnimIndex = 1 + local forceHeaviestAnim = true + local resumeMainLoopEvent = Instance.new('BindableEvent') + + thisAnimationPreview.Stop = function() + stop = true + if currentTrack and currentTrack.IsPlaying then + currentTrack:Stop() + end + end + + if isMultipleAnims then -- alternate between the animations when there's more than one + for i, v in next, animAssets do + if v.Name == 'swimidle' then + currentAnimIndex = i + break + end + end + + Utilities.fastSpawn(function() + while not stop do + switch = true + while switch do + Utilities.renderWait() + end + wait(4) + end + end) + end + + local loopAnimation = function() + stopCurrentLoop = false + + if switch then + currentAnimIndex = currentAnimIndex%#animAssets + 1 + switch = false + end + + -- weighted random + local newAnim + local possibleAnims = animAssets[currentAnimIndex]:GetChildren() + local options, totalWeight = getWeightedAnimations(possibleAnims) + + if forceHeaviestAnim then + local heaviest, heaviestWeight + + for v, weight in next, options do + if heaviest == nil or weight > heaviestWeight then + heaviest, heaviestWeight = v, weight + end + end + + newAnim = heaviest + forceHeaviestAnim = false + else + local chosenValue = math.random()*totalWeight + for v, weight in next, options do + if chosenValue <= weight then + newAnim = v + break + else + chosenValue = chosenValue - weight + end + end + end + + -- stop the old track, play the new one + if currentAnim == newAnim then + if not currentTrack.IsPlaying then + currentTrack:Play() + end + else + local fadeInTime = 0.1 + + if currentTrack ~= nil then + if currentTrack.IsPlaying then + currentTrack:Stop(0.5) + fadeInTime = 0.5 + end + currentTrack = nil + end + + currentTrack = humanoidLoadAnimation(newAnim) + currentTrack:Play(fadeInTime) + + if newAnim.Parent.Name == 'swim' then + setAnimationRotation(-math.rad(60), 0, 0) + else + setAnimationRotation(0, 0, 0) + end + end + currentAnim = newAnim + + -- wait for the animation to end, or for the switcher to switch, or for the whole thing to be stopped + local animEnded = false + local animEndedCon = currentTrack.Stopped:connect(function() + animEnded = true + end) + + local lastTimePosition = 0 -- keep track of this so we know when it loops + + while true and not this.Destroyed do + if animEnded then + break + elseif stop then + break + elseif stopCurrentLoop then + break + else + if currentTrack.TimePosition < lastTimePosition then + break + end + lastTimePosition = currentTrack.TimePosition + Utilities.renderWait() + end + end + + animEndedCon:disconnect() + + stopCurrentLoop = false + end + + local animationResumedConnection = resumeAnimationEvent.Event:connect(function() + stopCurrentLoop = true + pauseMainLoop = true + loopAnimation() + pauseMainLoop = false + forceHeaviestAnim = true + resumeMainLoopEvent:Fire() + end) + + Utilities.fastSpawn(function() + while not stop and not this.Destroyed do + loopAnimation() + + if pauseMainLoop then + resumeMainLoopEvent.Event:wait() + end + end + + if currentTrack then + if currentTrack.IsPlaying then + currentTrack:Stop() + end + currentTrack:Destroy() + currentTrack = nil + end + if currentAnim then + currentAnim = nil + end + + animationResumedConnection:disconnect() + end) + end + + local function startEquippedAnimationPreview(assetTypeName) + startAnimationPreviewFromAssets(getEquippedAnimationAssets(assetTypeName)) + end + + local function startAnimationPreview(assetId) + startAnimationPreviewFromAssets(getAnimationAssets(assetId)) + end + + local function deleteAssetR6(assetId) + --Destroy rendered content + local currentAssetContent = assetsLinkedContent[assetId] + if currentAssetContent then + for _,thing in pairs(currentAssetContent) do + if thing and thing.Parent then + thing:Destroy() + end + end + assetsLinkedContent[assetId] = nil + end + end + + local function setDefaultAssetR6(character, assetTypeName) + --Special cases where we need to replace removed asset with a default + if assetTypeName == 'Head' then + if character and character.Parent then + local head = character:FindFirstChild('Head') + if head then + local defaultHeadMesh = Instance.new('SpecialMesh') + defaultHeadMesh.MeshType = 'Head' + defaultHeadMesh.Scale = Vector3.new(1.2,1.2,1.2) + defaultHeadMesh.Parent = head + end + end + elseif assetTypeName == 'Face' then + if character and character.Parent then + local head = character:FindFirstChild('Head') + if head then + local currentFace = Utilities.findFirstChildOfType(head,'Decal') + if not currentFace then + local face = Instance.new('Decal') + face.Name = 'face' + face.Texture = "rbxasset://textures/face.png" + face.Parent = head + end + end + end + elseif assetTypeName == 'Gear' then + holdToolPos('Down') + end + end + + local function getAssetTypeName(assetId) + local assetInfo = AssetInfo.getAssetInfo(assetId) + local assetTypeName = nil + if assetInfo then + assetTypeName = AssetTypeNames[assetInfo.AssetTypeId] + end + return assetTypeName + end + + -- unequip an R6 asset which has already been replaced + local function unequipSupercededAssetR6(character, assetId) + local assetTypeName = getAssetTypeName(assetId) + Utilities.fastSpawn(function() + if assetTypeName and assetTypeName:find'Animation' then + startEquippedAnimationPreview(defaultAnimation) + else + deleteAssetR6(assetId) + end + end) + + end + + local function unequipAsset(character, assetId, isDefaultRequired) + if AvatarEditorSelectivelyUseDefaultAsset then + local assetTypeName = getAssetTypeName(assetId) + Utilities.fastSpawn(function() + if assetTypeName and assetTypeName:find'Animation' then + startEquippedAnimationPreview(defaultAnimation) + else + if getAvatarType() == 'R15' then + replaceR15PartWithDefault(character, assetId, isDefaultRequired) + elseif isDefaultRequired then + deleteAssetR6(assetId, assetTypeName) + setDefaultAssetR6(character, assetTypeName) + end + + end + end) + else + local assetInfo = AssetInfo.getAssetInfo(assetId) + local assetTypeName = nil + if assetInfo then + assetTypeName = AssetTypeNames[assetInfo.AssetTypeId] + end + + Utilities.fastSpawn(function() + if assetTypeName and assetTypeName:find'Animation' then + startEquippedAnimationPreview(defaultAnimation) + else + if getAvatarType() == 'R15' then + replaceR15PartWithDefault(character, assetId) + else + --Destroy rendered content + local currentAssetContent = assetsLinkedContent[assetId] + if currentAssetContent then + for _,thing in pairs(currentAssetContent) do + if thing and thing.Parent then + thing:Destroy() + end + end + assetsLinkedContent[assetId] = nil + end + + --Special cases where we need to replace removed asset with a default + if assetTypeName == 'Head' then + if character and character.Parent then + local head = character:FindFirstChild('Head') + if head then + local defaultHeadMesh = Instance.new('SpecialMesh') + defaultHeadMesh.MeshType = 'Head' + defaultHeadMesh.Scale = Vector3.new(1.2,1.2,1.2) + defaultHeadMesh.Parent = head + end + end + elseif assetTypeName == 'Face' then + if character and character.Parent then + local head = character:FindFirstChild('Head') + if head then + local currentFace = Utilities.findFirstChildOfType(head,'Decal') + if not currentFace then + local face = Instance.new('Decal') + face.Name = 'face' + face.Texture = "rbxasset://textures/face.png" + face.Parent = head + end + end + end + elseif assetTypeName == 'Gear' then + holdToolPos('Down') + end + end + + end + end) + + end + end + + + local function equipAssetInternal(character, assetId, isDefaultRequired) + addAssetToRecentAssetList(assetId) + + local assetInfo = AssetInfo.getAssetInfo(assetId) + + if assetInfo then + local assetTypeName = AssetTypeNames[assetInfo.AssetTypeId] + + local assetModel = InsertService:LoadAsset(assetId) + + -- If the new asset is not an animation then we need to update the render of the character + -- Also, after loading model, check to make sure it is still equipped before dressing character + if not string.find(assetTypeName, 'Animation') then + -- Render changes + if getAvatarType() == 'R6' then + local insertedStuff = {} + if not assetsLinkedContent[assetId] then + assetsLinkedContent[assetId] = insertedStuff + else + insertedStuff = assetsLinkedContent[assetId] + end + local stuff = {assetModel} + if stuff[1].className == 'Model' then + stuff = assetModel:GetChildren() + end + for _,thing in pairs(stuff) do -- Equip asset differently depending on what it is. + if string.lower(thing.Name) == 'r6' then + for _,r6SpecificThing in pairs(thing:GetChildren()) do + sortAndEquipItemToCharacter(character, r6SpecificThing, insertedStuff) + end + elseif thing.className ~= 'Folder' then + sortAndEquipItemToCharacter(character, thing, insertedStuff) + end + end + else + amendR15ForItemAdded(character, assetId, isDefaultRequired) + end + end + + -- If the new asset is a bodypart, then we need to update the colors + if assetInfo.AssetTypeId >= 25 and assetInfo.AssetTypeId <= 31 then + updateCharacterBodyColors() + end + end + end + + local function batchR6EquipmentChange(character, addTheseAssets, removeTheseAssets) + local numWaitingOn = 0 + local numSpawned = 0 + + for _, assetId in pairs(addTheseAssets) do + numWaitingOn = numWaitingOn + 1 + end + + for _, assetId in pairs(addTheseAssets) do + spawn(function() + numSpawned = numSpawned + 1 + + equipAssetInternal(character, assetId, true) + + if numSpawned == numWaitingOn then + for _, removeAssetId in pairs(removeTheseAssets) do + unequipSupercededAssetR6(character, removeAssetId) + end + end + end) + end + end + + local function equipAsset(character, assetId, isDefaultRequired) + spawn(function() + equipAssetInternal(character, assetId, isDefaultRequired) + end) + end + + + local function addAssetToList(assets, assetId) + local assetType = AssetInfo.getAssetType(assetId) + assets[assetType] = assets[assetType] or {} + table.insert(assets[assetType], assetId) + end + + + function this.wearOutfit(outfitId) + -- This code is used for cutting off the process of previous + -- outfit equip calls. Not a debounce per-se, but a.. bounce? + wearOutfitRequestCount = wearOutfitRequestCount + 1 + local thisWearOutfitRequestCount = wearOutfitRequestCount + local stillValidCheckFunction = function() + return wearOutfitRequestCount == thisWearOutfitRequestCount + end + + local outfitData = webServer.get(Urls.avatarUrlPrefix.."/v1/outfits/"..outfitId.."/details") + if outfitData and stillValidCheckFunction() and not this.Destroyed then + + local assets = {} + + -- Equip outfit assets + local outfitData = Utilities.decodeJSON(outfitData) + if outfitData then + local outfitAssets = outfitData['assets'] + if outfitAssets then + for _, assetInfo in pairs(outfitAssets) do + addAssetToList(assets, assetInfo.id) + end + end + end + + local bodyColors = { + HeadColor = 194; + LeftArmColor = 194; + LeftLegColor = 194; + RightArmColor = 194; + RightLegColor = 194; + TorsoColor = 194; + } + + local outfitBodyColors = outfitData['bodyColors'] + if outfitBodyColors then + for name, mapName in pairs(BODY_COLOR_NAME_MAP) do + local color = outfitBodyColors[mapName] + if color then + bodyColors[name] = color + end + end + end + + AppState.Store:dispatch(SetOutfit(assets, bodyColors)) + end + end + + + function this.setRotation(rotation) + if currentCharacter then + local hrp = currentCharacter:WaitForChild('HumanoidRootPart') + hrp.CFrame = CFrame.new(hrp.CFrame.p) + * CFrame.Angles(0, 0.6981 + rotation, 0) + * CFrame.Angles(rootAnimationRotation.x, rootAnimationRotation.y, rootAnimationRotation.z) + end + end + + + local function updateAvatarType(newType) + if newType == 'R15' then + createR15Rig() + else + createR6Rig() + end + end + + + function this.initFromServer() + local avatarFetchRequest = Utilities.httpGet(Urls.avatarUrlPrefix.."/v1/avatar") + avatarFetchRequest = Utilities.decodeJSON(avatarFetchRequest) + if avatarFetchRequest and not this.Destroyed then + local bodyColorsRequest = avatarFetchRequest['bodyColors'] + local bodyColors = {} + for name, mapName in pairs(BODY_COLOR_NAME_MAP) do + local color = bodyColorsRequest[mapName] + if color then + bodyColors[name] = color + end + end + setSavedBodyColors(bodyColors) + AppState.Store:dispatch(SetBodyColors(bodyColors)) + + local requestedAvatarType = avatarFetchRequest['playerAvatarType'] + if requestedAvatarType then + AppState.Store:dispatch(SetAvatarType(requestedAvatarType)) + end + + local scalesRequest = avatarFetchRequest['scales'] + + if scalesRequest then + local scales = {} + local height = scalesRequest['height'] + if height then + scales.Height = height + end + local width = scalesRequest['width'] + if width then + scales.Width = width + end + local depth = scalesRequest['depth'] + if depth then + scales.Depth = depth + end + local head = scalesRequest['head'] + if head then + scales.Head = head + end + if AvatarEditorAnthroSliders then + local bodyType = scalesRequest['bodyType'] + if bodyType then + scales.BodyType = bodyType + end + local proportion = scalesRequest['proportion'] + if proportion then + scales.Proportion = proportion + end + end + setSavedScales(scales) + AppState.Store:dispatch(SetScales(scales)) + end + + local requestedAssetsData = avatarFetchRequest['assets'] + if requestedAssetsData then + local waitingAssets = {} + local numQueuedAssets = 0 + + local assets = {} + + for _,assetData in pairs(requestedAssetsData) do + local assetId = assetData['id'] + + addAssetToList(assets, assetId) + + if assetId and type(assetId) == 'number' then + waitingAssets[assetId] = false + numQueuedAssets = numQueuedAssets + 1 + Utilities.fastSpawn(function() + waitingAssets[assetId] = true + addAssetToRecentAssetList(assetId) + pcall(function() + InsertService:LoadAsset(assetId) -- Prime the cache + end) + numQueuedAssets = numQueuedAssets - 1 + end) + end + end + + while numQueuedAssets > 0 do wait() end + + AppState.Store:dispatch(SetAssets(assets)) + + -- After all fetched assets are equipped, we can update the savedWearingAssets table. + waitingForInitialLoad = true + Utilities.fastSpawn(function() + local startTime = tick() + while true and not this.Destroyed do + local allAssetsLoaded = true + for _, handled in pairs(waitingAssets) do + if not handled then + allAssetsLoaded = false + break + end + end + if allAssetsLoaded then + syncSaved() + break + end + if tick()-startTime > 30 then + if UserSettings().GameSettings:InStudioMode() then + print('Took too long to load character.') + end + break + end + wait() + end + waitingForInitialLoad = false + end) + end + end + end + + local function getPartPosition(partName) + local root = currentCharacter:FindFirstChild('HumanoidRootPart') + + for _, v in next, Utilities.getDescendants(currentCharacter) do + if v.Name == partName then + local result = v.cFrame + return result + end + end + + return CFrame.new() + end + + local function getFocusPoint(partNames) +if AvatarEditorRecomputeCameraLookAt then + local numParts = #partNames + + if numParts == 0 then + local humanoid = currentCharacter:WaitForChild('Humanoid') + return humanoid.Torso.CFrame.p + end + + local sumOfPartPositions = Vector3.new() + + for _, partName in next, partNames do + sumOfPartPositions = sumOfPartPositions + getPartPosition(partName).p + end + + return sumOfPartPositions / numParts +else + local focusPointRelative = Vector3.new() + + for _, partName in next, partNames do + focusPointRelative = focusPointRelative + getRestingPartOffset(partName).p / #partNames + end + + local humanoid = currentCharacter:WaitForChild('Humanoid') + return humanoid.Torso.CFrame * focusPointRelative +end + end + + + local function handleCameraChange(page) + local position = LayoutInfo.CameraDefaultPosition + local cameraFocus = page.CameraFocus or {Parts = {'HumanoidRootPart'}} + local focusPoint = getFocusPoint(cameraFocus.Parts) + + if page.CameraZoomRadius then + local toCamera = (LayoutInfo.CameraDefaultPosition - focusPoint) + toCamera = Vector3.new(toCamera.x, 0, toCamera.z).unit + position = focusPoint + page.CameraZoomRadius * toCamera + end + + updateCamera(position, focusPoint, page.CameraFOV or 70) + end + + + function this:setUpdateCameraCallback(callback) + updateCamera = callback + + updateCamera( + LayoutInfo.CameraDefaultPosition, + LayoutInfo.CameraDefaultFocusPoint, + LayoutInfo.CameraDefaultFOV) + end + + + + local function applyStateChanged(newState, oldState) + local didUpdate = false + local didEquip = false + local characterRebuilt = false + + if newState.Character.AvatarType ~= oldState.Character.AvatarType or currentCharacter == nil then + updateAvatarType(newState.Character.AvatarType) + characterRebuilt = true + didUpdate = true + end + + local currentCategoryIndex = newState.Category.CategoryIndex or 1 + local tabInfo = newState.Category.TabsInfo[currentCategoryIndex] + local tabIndex = tabInfo and tabInfo.TabIndex or 1 + local currentCategoryName = currentCategoryIndex and Categories[currentCategoryIndex].name or "" + local canPlayAnimation = + string.find(currentCategoryName, 'Animation') or string.find(currentCategoryName, 'Recent') + + if newState.Category.CategoryIndex ~= oldState.Category.CategoryIndex or + newState.Category.TabsInfo ~= oldState.Category.TabsInfo then + + handleCameraChange( + Categories[currentCategoryIndex].pages[tabIndex]) + end + + -- Set default animation in animation category + if string.find(currentCategoryName, 'Animation') then + local pageName = Categories[currentCategoryIndex].pages[tabIndex].typeName + setDefaultAnimation(string.gsub(pageName, ' ', '')) + else + setDefaultAnimation('IdleAnimation') + end + + local animationId = nil + + if newState.Character.Assets ~= oldState.Character.Assets then + --Remove assets which only exist in oldState + for assetType, assetList in pairs(oldState.Character.Assets) do + if not newState.Character.Assets[assetType] and assetList then + for _, assetId in pairs(assetList) do + unequipAsset(currentCharacter, assetId, true) + end + end + end + + for assetType, _ in pairs(newState.Character.Assets) do + if newState.Character.Assets[assetType] ~= oldState.Character.Assets[assetType] then + local addTheseAssets = TableUtilities.ListDifference( + newState.Character.Assets[assetType] or {}, + oldState.Character.Assets[assetType] or {}) + local removeTheseAssets = + TableUtilities.ListDifference(oldState.Character.Assets[assetType] or {}, + newState.Character.Assets[assetType] or {}) + + if AvatarEditorSelectivelyUseDefaultAsset then -- AvatarEditorSelectivelyUseDefaultAsset flag is intended to stop the default body part appearing, when a new one will shortly be equipped + local addTheseAssetsHasItems = next(addTheseAssets) ~= nil + if addTheseAssetsHasItems then + didEquip = true + end + didUpdate = didUpdate or (addTheseAssetsHasItems or (next(removeTheseAssets) ~= nil)) + local isAnimationAssetType = string.find(assetType, 'Animation') + + if getAvatarType() == 'R15' then + for _, assetId in pairs(addTheseAssets) do + equipAsset(currentCharacter, assetId, false) + if isAnimationAssetType then + animationId = assetId + end + end + + local isDefaultForTypeRequired = not addTheseAssetsHasItems + for _, assetId in pairs(removeTheseAssets) do + unequipAsset(currentCharacter, assetId, isDefaultForTypeRequired) + end + else + if isAnimationAssetType then + for _, assetId in pairs(addTheseAssets) do + animationId = assetId + end + end + if addTheseAssetsHasItems then + batchR6EquipmentChange(currentCharacter, addTheseAssets, removeTheseAssets) + else + for _, assetId in pairs(removeTheseAssets) do + unequipAsset(currentCharacter, assetId, true) + end + end + end + else + for _, assetId in pairs(addTheseAssets) do + equipAsset(currentCharacter, assetId, true) + didUpdate = true + didEquip = true + + if string.find(assetType, 'Animation') then + animationId = assetId + end + end + + for _, assetId in pairs(removeTheseAssets) do + unequipAsset(currentCharacter, assetId) + didUpdate = true + end + end + end + end + + if isInitialAssets and oldState.Character.Assets and next(oldState.Character.Assets) == nil then + isInitialAssets = false + canPlayAnimation = false + end + end + + if newState.Character.BodyColors ~= oldState.Character.BodyColors then + local differentBodyColors = + TableUtilities.TableDifference(newState.Character.BodyColors, oldState.Character.BodyColors) + + if next(differentBodyColors) ~= nil then + updateCharacterBodyColors(newState.Character.BodyColors) + didUpdate = true + end + end + + if newState.Character.Scales ~= oldState.Character.Scales then + local differentScales = + TableUtilities.TableDifference(newState.Character.Scales, oldState.Character.Scales) + + if next(differentScales) ~= nil then + if AvatarEditorAnthroSliders then + engineScaleCharacter(currentCharacter, newState.Character.Scales) + else + scaleCharacter( + currentCharacter, + Vector3.new( + newState.Character.Scales.Width, + newState.Character.Scales.Height, + newState.Character.Scales.Depth), + newState.Character.Scales.Head + ) + adjustHeightToStandOnPlatform(currentCharacter) + end + end + end + + if newState.Character ~= oldState.Character then + updateDefaultShirtAndPants() + end + + if didUpdate then + ParticleScreen.runParticleEmitter() + end + + -- Play animations + Utilities.fastSpawn(function() + if animationId and canPlayAnimation and newState.Character.AvatarType == "R15" then + startAnimationPreview(animationId) + else + if didEquip or (characterRebuilt and currentCharacter ~= nil) then + startEquippedAnimationPreview(defaultAnimation) + playLookAround() + elseif newState.Category.CategoryIndex ~= oldState.Category.CategoryIndex or + newState.Category.TabsInfo ~= oldState.Category.TabsInfo or + newState.Character.AvatarType ~= oldState.Character.AvatarType or + (animationId and not canPlayAnimation) then + startEquippedAnimationPreview(defaultAnimation) + end + end + end) + end + + + local cacheNewState = function(newState, oldState) + if cachedOldState == nil then + cachedOldState = oldState + end + cachedNewState = newState + end + + handleStateChanged = cacheNewState + + local storeChangedCn = AppState.Store.Changed:Connect( + function(newState, oldState) + handleStateChanged(newState, oldState) + end + ) + + local function AutoSave(timeBetweenSaves) + local keepGoing = true + local obj = {stop = function() keepGoing = false end} + + Utilities.fastSpawn( + function() + wait(timeBetweenSaves) + while keepGoing do + this.saveToServer() + wait(timeBetweenSaves) + end + end + ) + + return obj + end + + local autoSave = {stop = function() end} + + function this.hide() + cachedOldState = AppState.Store:getState() + cachedNewState = cachedOldState + handleStateChanged = cacheNewState + dismissCurrentCharacter() + getCharacterNode().Parent = nil + autoSave.stop() + this.saveToServer() + end + + function this.show() + if not this.Destroyed then + getCharacterNode().Parent = game.Workspace + if cachedNewState then + applyStateChanged(cachedNewState, cachedOldState) + cachedNewState = cachedOldState + end + handleStateChanged = applyStateChanged + autoSave = AutoSave(5) + end + end + + function this.destroy() + storeChangedCn = Utilities.disconnectEvent(storeChangedCn) + if characterNode then + this.Destroyed = true + characterNode:Destroy() + characterNode = nil + end + end + + return this +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/ConsoleButtonIndicators.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/ConsoleButtonIndicators.lua new file mode 100644 index 0000000..b477231 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/ConsoleButtonIndicators.lua @@ -0,0 +1,180 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) +local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local Strings = require(Modules.LuaApp.Legacy.AvatarEditor.LocalizedStrings) +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfoConsole) +local TweenController = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController) + +local TextService = game:GetService("TextService") + +local ICON_HEIGHT = LayoutInfo.IndicatorHeight --indicator icon size +local BOTTOM_DISTANCE = LayoutInfo.IndicatorBottomDistance --Distance from bottom of indicator to bottom of screen +local BUTTON_DISTANCE = LayoutInfo.IndicatorButtonsDistance --Distance between two indicators +local ICON_TEXT_DISTANCE = LayoutInfo.IndicatorIconTextDistance --Distance between indicator icon and text +local MAX_TEXT_LENGTH = LayoutInfo.IndicatorMaxLength - BUTTON_DISTANCE - 2 * ICON_HEIGHT - 2 * ICON_TEXT_DISTANCE +local MAX_FULLVIEW_EXIT_TEXT_SIZE_X = LayoutInfo.IndicatorMaxLength - ICON_HEIGHT - ICON_TEXT_DISTANCE + +local this = {} + +local function getTextSizeX(text) + local textSize = TextService:GetTextSize(text, + LayoutInfo.ButtonFontSize, + LayoutInfo.RegularFont, + Vector2.new(0, 0)) + return textSize.X +end + +--switchType size +local switchToR15TextSizeX = getTextSizeX(Strings:LocalizedString("SwitchToR15Word")) +local switchToR6TextSizeX = getTextSizeX(Strings:LocalizedString("SwitchToR6Word")) +local switchTypeTextSizeX = math.max(switchToR15TextSizeX, switchToR6TextSizeX) + +--fullView size +local fullViewTextSizeX = getTextSizeX(Strings:LocalizedString("FullViewWord")) + +if switchTypeTextSizeX + fullViewTextSizeX > MAX_TEXT_LENGTH then + switchTypeTextSizeX = math.min(switchTypeTextSizeX, MAX_TEXT_LENGTH/2) + fullViewTextSizeX = math.min(fullViewTextSizeX, MAX_TEXT_LENGTH/2) +end + +local switchTypeContainerSizeX = ICON_HEIGHT + ICON_TEXT_DISTANCE + switchTypeTextSizeX +local fullViewContainerSizeX = ICON_HEIGHT + ICON_TEXT_DISTANCE + fullViewTextSizeX + +--total size +local totalContainerSizeX = switchTypeContainerSizeX + fullViewContainerSizeX + BUTTON_DISTANCE + +--fullViewExit size +local fullViewExitTextSizeX = getTextSizeX(Strings:LocalizedString("ReturnToEditWord")) +fullViewExitTextSizeX = math.min(fullViewExitTextSizeX, MAX_FULLVIEW_EXIT_TEXT_SIZE_X) +local fullViewExitContainerSizeX = ICON_HEIGHT + ICON_TEXT_DISTANCE + fullViewExitTextSizeX + +--positions +local textPositionX = ICON_HEIGHT + ICON_TEXT_DISTANCE +local switchTypeContainerPosition = UDim2.new(0.5, -totalContainerSizeX/2, 1, -BOTTOM_DISTANCE) +local switchTypeContainerTweenPosition = UDim2.new(0.5, -totalContainerSizeX/2, 1, 100) +local fullViewContainerPosition = UDim2.new( + 0.5, -totalContainerSizeX/2 + switchTypeContainerSizeX + BUTTON_DISTANCE, 1, -BOTTOM_DISTANCE) +local fullViewExitContainerPosition = UDim2.new(0.5, -fullViewExitContainerSizeX/2, 1, -BOTTOM_DISTANCE) + +--tween +local tweenInfo = TweenInfo.new(LayoutInfo.DefaultTweenTime, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + +function this.init(parent) + local SwitchTypeContainer = Utilities.create'Frame' + { + Name = "SwitchTypeContainer"; + AnchorPoint = Vector2.new(0, 1); + Position = switchTypeContainerPosition; + Size = UDim2.new(0, switchTypeContainerSizeX, 0, ICON_HEIGHT); + BackgroundTransparency = 1; + BorderSizePixel = 0; + ZIndex = LayoutInfo.IndicatorLayer; + Parent = parent; + } + + local SwitchTypeLabel = Utilities.create'TextLabel' + { + Name = "HintActionText"; + AnchorPoint = Vector2.new(0, 0.5); + Position = UDim2.new(0, textPositionX, 0.5, 0); + Size = UDim2.new(0, switchTypeTextSizeX, 0, ICON_HEIGHT); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Font = LayoutInfo.RegularFont; + TextSize = LayoutInfo.ButtonFontSize; + TextColor3 = LayoutInfo.WhiteTextColor; + TextWrapped = true; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Center; + Text = Strings:LocalizedString("SwitchToR6Word"); + ZIndex = LayoutInfo.IndicatorLayer; + Parent = SwitchTypeContainer; + } + + Utilities.create'ImageLabel' + { + Name = "SwitchTypeButtonIcon"; + AnchorPoint = Vector2.new(0, 0.5); + Position = UDim2.new(0, 0, 0.5, 0); + Size = UDim2.new(0, ICON_HEIGHT, 0, ICON_HEIGHT); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = "rbxasset://textures/ui/Shell/ButtonIcons/SelectButtonDark.png"; + ZIndex = LayoutInfo.IndicatorLayer; + Parent = SwitchTypeContainer; + } + + local FullViewContainer = Utilities.create'Frame' + { + Name = "FullViewContainer"; + AnchorPoint = Vector2.new(0, 1); + Position = fullViewContainerPosition; + Size = UDim2.new(0, fullViewContainerSizeX, 0, ICON_HEIGHT); + BackgroundTransparency = 1; + ZIndex = LayoutInfo.IndicatorLayer; + Parent = parent; + } + + local FullViewLabel = Utilities.create'TextLabel' + { + Name = "HintActionText"; + AnchorPoint = Vector2.new(0, 0.5); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Position = UDim2.new(0, textPositionX, 0.5, 0); + Size = UDim2.new(0, fullViewTextSizeX, 0, ICON_HEIGHT); + Font = LayoutInfo.RegularFont; + TextSize = LayoutInfo.ButtonFontSize; + TextColor3 = LayoutInfo.WhiteTextColor; + TextWrapped = true; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Center; + Text = Strings:LocalizedString("FullViewWord"); + ZIndex = LayoutInfo.IndicatorLayer; + Parent = FullViewContainer; + } + + Utilities.create'ImageLabel' + { + Name = "FullViewButtonIcon"; + AnchorPoint = Vector2.new(0, 0.5); + Position = UDim2.new(0, 0, 0.5, 0); + Size = UDim2.new(0, ICON_HEIGHT, 0, ICON_HEIGHT); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = "rbxasset://textures/ui/Shell/ButtonIcons/R3ButtonDark.png"; + ZIndex = LayoutInfo.IndicatorLayer; + Parent = FullViewContainer; + } + + AppState.Store.Changed:Connect( + function(newState, oldState) + if newState.Character.AvatarType == "R6" then + SwitchTypeLabel.Text = Strings:LocalizedString("SwitchToR15Word") + end + + if newState.Character.AvatarType == "R15" then + SwitchTypeLabel.Text = Strings:LocalizedString("SwitchToR6Word") + end + + if newState.FullView ~= oldState.FullView then + if newState.FullView then + FullViewLabel.Text = Strings:LocalizedString("ReturnToEditWord") + FullViewLabel.Size = UDim2.new(0, fullViewExitTextSizeX, 0, ICON_HEIGHT) + FullViewContainer.Size = UDim2.new(0, fullViewExitContainerSizeX, 0, ICON_HEIGHT) + TweenController(SwitchTypeContainer, tweenInfo, { Position = switchTypeContainerTweenPosition }) + TweenController(FullViewContainer, tweenInfo, { Position = fullViewExitContainerPosition }) + else + FullViewLabel.Text = Strings:LocalizedString("FullViewWord") + FullViewLabel.Size = UDim2.new(0, fullViewTextSizeX, 0, ICON_HEIGHT) + FullViewContainer.Size = UDim2.new(0, fullViewContainerSizeX, 0, ICON_HEIGHT) + TweenController(SwitchTypeContainer, tweenInfo, { Position = switchTypeContainerPosition }) + TweenController(FullViewContainer, tweenInfo, { Position = fullViewContainerPosition }) + end + end + end + ) +end + +return this diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/DarkCoverManager.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/DarkCoverManager.lua new file mode 100644 index 0000000..9e95398 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/DarkCoverManager.lua @@ -0,0 +1,42 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local tween = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController) +local GetButtonClickEvent = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonClickEvent) + +return function(DarkCover, CategoryMenu) + local function showDarkCover() + DarkCover.ZIndex = 5 + DarkCover.Visible = true + local tweenInfo = TweenInfo.new(0.1, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + local propGoals = { + BackgroundTransparency = 0.4 + } + tween(DarkCover, tweenInfo, propGoals) + end + + local function hideDarkCover() + local tweenInfo = TweenInfo.new(0.05, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + + local propGoals = { + BackgroundTransparency = 1 + } + + utilities.fastSpawn(tween, DarkCover, tweenInfo, propGoals).Completed:Connect(function() + DarkCover.Visible = false + end) + end + + GetButtonClickEvent(DarkCover):connect(function() + CategoryMenu:closeTopMenu() + end) + + CategoryMenu.openCategoryMenuEvent:Connect(function() + showDarkCover() + end) + + CategoryMenu.closeCategoryMenuEvent:Connect(function() + hideDarkCover() + end) +end + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/DefaultClothesIds.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/DefaultClothesIds.lua new file mode 100644 index 0000000..73c2dcc --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/DefaultClothesIds.lua @@ -0,0 +1,31 @@ +local domainUrl = game:GetService('ContentProvider').BaseUrl + +local this = {} +local defaultShirtIds +local defaultPantIds + +if string.find(domainUrl, "sitetest2") then + defaultShirtIds = {92632399} + defaultPantIds = {92650676} +else + -- These sync up with chat username color order. The last clothing is a teal + -- color instead of a tan color like the usernames. + -- Username color list: "Bright red", "Bright blue", "Earth green", "Bright violet", + -- "Bright orange", "Bright yellow", "Light reddish violet", "Brick yellow" + defaultShirtIds = {855776103, 855760101, 855766176, 855777286, 855768342, 855779323, 855773575, 855778084} + defaultPantIds = {855783877, 855780360, 855781078, 855782781, 855781508, 855785499, 855782253, 855784936} +end + +function this.getDefaultClothesCount( ... ) + return #defaultShirtIds +end + +function this.getDefaultShirtIds() + return defaultShirtIds +end + +function this.getDefaultPantIds() + return defaultPantIds +end + +return this diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/EaseFilters.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/EaseFilters.lua new file mode 100644 index 0000000..453863b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/EaseFilters.lua @@ -0,0 +1,120 @@ +local pi = math.pi +local tau = pi*2 +local halfpi = pi/2 + +local sin = math.sin +local cos = math.cos +local sqrt = math.sqrt + +local function easeIn(t,func) + return func(t) +end + +local function easeOut(t,func) + return 1-func(1-t) +end + +local function easeInOut(t,func) + t=t*2 + if t < 1 then + return easeIn(t,func)*.5 + else + return .5+easeOut(t-1,func)*.5 + end +end + +local function easeOutIn(t,func) + t=t*2 + if t < 1 then + return easeOut(t,func)*.5 + else + return .5+easeIn(t-1,func)*.5 + end +end + + +local function linear(t) + return t +end + +local function quad(t) + return t^2 +end + +local function cubic(t) + return t^3 +end + +local function quart(t) + return t^4 +end + +local function quint(t) + return t^5 +end + +local function sine(t) + return 1-cos(t*halfpi) +end + +local function expo(t) + if t == 0 then + return 0 + else + return 2^(10*(t-1)) - .001 + end +end + +local function circ(t) + return 1-sqrt(1-t^2) +end + +local function elastic(t) + if t <= 0 then + return 0 + end + if t >= 1 then + return 1 + end + t=t-1 + return -(2^(10*t)*sin(.30833*tau)) +end + + +local function back(t) + return t * t * (2.70158*t - 1.70158) +end + +local function bounce(t) + if t<.36363636 then + return 7.5625*t*t + elseif t<.72727272 then + t=t-.54545454 + return 7.5625*t*t+.75 + elseif t<.90909090 then + t=t-.81818181 + return 7.5625*t*t+.9375 + else + t=t-.95454545 + return 7.5625*t*t+.984375 + end +end + +return { + easeIn = easeIn, + easeOut = easeOut, + easeInOut = easeInOut, + easeOutIn = easeOutIn, + + linear = linear, + quad = quad, + cubic = cubic, + quart = quart, + quint = quint, + sine = sine, + expo = expo, + circ = circ, + elastic = elastic, + back = back, + bounce = bounce, +} diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Flags.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Flags.lua new file mode 100644 index 0000000..2757d0f --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Flags.lua @@ -0,0 +1,26 @@ + +local settings = settings() +local flagMap = {} + +local this = {} + +local getFlag = function(name) + return settings:GetFFlag(name) +end + +if UserSettings().GameSettings:InStudioMode() then + getFlag = function(name) + print(name.." = TRUE (while testing in Studio)") + return true + end +end + +function this:GetFlag(name) + if flagMap[name] == nil then + flagMap[name] = getFlag(name) + end + + return flagMap[name] +end + +return this diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/FullView.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/FullView.lua new file mode 100644 index 0000000..7c6fb9e --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/FullView.lua @@ -0,0 +1,36 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) +local ToggleAvatarEditorFullView = require(Modules.LuaApp.Actions.ToggleAvatarEditorFullView) +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfo) +local SpriteManager = require(Modules.LuaApp.Legacy.AvatarEditor.SpriteSheetManager) +local Tween = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController) +local GetButtonClickEvent = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonClickEvent) + +return function(FullViewButton) + FullViewButton.Position = LayoutInfo.FullViewInitialPosition + + local function update(isFullView) + local image = isFullView and 'ic-collapse' or 'ic-expand' + SpriteManager.equip(FullViewButton, image) + + if LayoutInfo.isLandscape then + local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + local finalPosition = isFullView and UDim2.new(1, -52, 1, -52) or UDim2.new(1, -112, 1, -52) + local tweenGoals = { + Position = finalPosition + } + Tween(FullViewButton, tweenInfo, tweenGoals) + end + end + + AppState.Store.Changed:Connect(function(newState, oldState) + if newState.FullView ~= oldState.FullView then + update(newState.FullView) + end + end) + + GetButtonClickEvent(FullViewButton):connect(function() + AppState.Store:dispatch(ToggleAvatarEditorFullView()) + end) +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/GetButtonClickEvent.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/GetButtonClickEvent.lua new file mode 100644 index 0000000..55be066 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/GetButtonClickEvent.lua @@ -0,0 +1,9 @@ +local luaAppLegacyInputDisabledGlobally = settings():GetFFlag('LuaAppLegacyInputDisabledGlobally2') + +return function(component) + if luaAppLegacyInputDisabledGlobally then + return component.Activated + else + return component.MouseButton1Click + end +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/GetButtonDownEventConnected.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/GetButtonDownEventConnected.lua new file mode 100644 index 0000000..ff9bcfa --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/GetButtonDownEventConnected.lua @@ -0,0 +1,23 @@ +--[[ + This module will never be called in console platforms + That is, console will never use the modules which are related to MouseButton1Down + So this patch is safe for consoles +]] + +local luaAppLegacyInputDisabledGlobally = settings():GetFFlag('LuaAppLegacyInputDisabledGlobally2') + +return function(component, toDoFunc) + if luaAppLegacyInputDisabledGlobally then + component.InputBegan:connect(function(inputObject) + if inputObject.UserInputState == Enum.UserInputState.Begin then + local inputType = inputObject.UserInputType + if inputType == Enum.UserInputType.Touch or inputType == Enum.UserInputType.MouseButton1 then + toDoFunc(inputObject.Position.x, inputObject.Position.y) + end + end + end) + else + component.MouseButton1Down:connect(toDoFunc) + end +end + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Header.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Header.lua new file mode 100644 index 0000000..c14e7d7 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Header.lua @@ -0,0 +1,217 @@ + +local GuiService = game:GetService("GuiService") +local CoreGui = game:GetService("CoreGui") +local UserInputService = game:GetService("UserInputService") +local PlayerService = game:GetService("Players") + +local Create = require(CoreGui.RobloxGui.Modules.Mobile.Create) +local PaddedImageButton = require(CoreGui.RobloxGui.Modules.LuaApp.Legacy.AvatarEditor.PaddedImageButton) + +local Urls = require(CoreGui.RobloxGui.Modules.LuaApp.Legacy.AvatarEditor.Urls) +local Utilities = require(CoreGui.RobloxGui.Modules.LuaApp.Legacy.AvatarEditor.Utilities) + +local NotificationType = require(CoreGui.RobloxGui.Modules.LuaApp.Enum.NotificationType) + +local ok, Platform = pcall(function() + return UserInputService:GetPlatform() +end) +if not ok then + Platform = Enum.Platform.None +end + +local Constants = { + Color = { + WHITE = Color3.fromRGB(255, 255, 255), + BLUE_PRESSED = Color3.fromRGB(0, 116, 189), + }, + Platforms = { + Android = { + HEADER_CONTENT_FRAME_Y_OFFSET = 0, + HEADER_TITLE_FRAME_POSITION = UDim2.new(0, 15, 0, 0), + HEADER_TITLE_FRAME_ANCHOR_POINT = Vector2.new(0, 0), + HEADER_VERTICAL_ALIGNMENT = 0, + HEADER_TEXT_X_ALIGNMENT = 0, + }, + Default = { + HEADER_CONTENT_FRAME_Y_OFFSET = 24, + HEADER_TITLE_FRAME_POSITION = UDim2.new(0.5, 0, 0, 0), + HEADER_TITLE_FRAME_ANCHOR_POINT = Vector2.new(0.5, 0), + HEADER_VERTICAL_ALIGNMENT = 1, + HEADER_TEXT_X_ALIGNMENT = 2, + }, + }, +} + +if Platform == Enum.Platform.Android then + Constants.PlatformSpecific = Constants.Platforms.Android +else + Constants.PlatformSpecific = Constants.Platforms.Default +end + + +local Header = {} +Header.__index = Header + +function Header.new(title, navBarHeight, statusBarHeight) + + local self = {} + setmetatable(self, Header) + + self.buttons = {} + + self.title = title + self.subtitle = nil + + self.rbx = Create.new "Frame" { + Name = "HeaderFrame", + BackgroundTransparency = 1, + ZIndex = 10, + + Create.new "Frame" { + Name = "Header", + BackgroundColor3 = Constants.Color.BLUE_PRESSED, + BorderSizePixel = 0, + ZIndex = 10, + + Create.new "Frame" { + Name = "Content", + BackgroundTransparency = 1, + ZIndex = 10, + + Create.new "Frame" { + Name = "Titles", + BackgroundTransparency = 1, + Size = UDim2.new(0, 200, 1, 0), + Position = Constants.PlatformSpecific.HEADER_TITLE_FRAME_POSITION, + AnchorPoint = Constants.PlatformSpecific.HEADER_TITLE_FRAME_ANCHOR_POINT, + ZIndex = 10, + + Create.new "UIListLayout" { + SortOrder = "LayoutOrder", + VerticalAlignment = Constants.PlatformSpecific.HEADER_VERTICAL_ALIGNMENT, + }, + + Create.new "TextLabel" { + Name = "Title", + BackgroundTransparency = 1, + TextSize = 20, + TextColor3 = Constants.Color.WHITE, + Size = UDim2.new(0, 200, 0, 25), + AnchorPoint = Vector2.new(0.5, 0.5), + TextXAlignment = Constants.PlatformSpecific.HEADER_TEXT_X_ALIGNMENT, + Text = self.title, + Font = "SourceSansBold", + LayoutOrder = 0, + ZIndex = 10, + }, + + Create.new "TextLabel" { + Name = "Subtitle", + BackgroundTransparency = 1, + TextSize = 12, + TextColor3 = Constants.Color.WHITE, + Size = UDim2.new(0, 200, 0, 12), + AnchorPoint = Vector2.new(0.5, 0.5), + TextXAlignment = Constants.PlatformSpecific.HEADER_TEXT_X_ALIGNMENT, + Text = "", + Font = "SourceSans", + LayoutOrder = 1, + ZIndex = 10, + }, + }, + + Create.new "Frame" { + Name = "Buttons", + BackgroundTransparency = 1, + Position = UDim2.new(1, -5, 0, 0), + Size = UDim2.new(0, 100, 1, 0), + AnchorPoint = Vector2.new(1, 0), + + Create.new "UIListLayout" { + SortOrder = "LayoutOrder", + HorizontalAlignment = "Right", + FillDirection = "Horizontal", + VerticalAlignment = Constants.PlatformSpecific.HEADER_VERTICAL_ALIGNMENT, + }, + }, + }, + }, + } + + local robuxButton = PaddedImageButton.new("robux", "rbxasset://textures/icon_ROBUX.png", 28) + robuxButton.Pressed:Connect(function() + GuiService:BroadcastNotification("", NotificationType.PURCHASE_ROBUX) + end) + self:AddButton(robuxButton) + + local notificationsButton = PaddedImageButton.new("robux", "rbxasset://textures/Icon_Stream_Off.png", 28) + notificationsButton.Pressed:Connect(function() + GuiService:BroadcastNotification("", NotificationType.VIEW_NOTIFICATIONS) + end) + self:AddButton(notificationsButton) + + self:SetNavAndStatusBarHeight(navBarHeight, statusBarHeight) + + if Platform == Enum.Platform.Android then + self:SetDefaultSubtitle() + end + + return self +end + +function Header:SetNavAndStatusBarHeight(navBarHeight, statusBarHeight) + local heightOfHeader = navBarHeight + statusBarHeight + + self.rbx.Size = UDim2.new(1, 0, 0, heightOfHeader) + self.rbx.Header.Size = UDim2.new(1, 0, 0, heightOfHeader) + + self.rbx.Header.Content.Position = UDim2.new(0, 0, 0, statusBarHeight) + self.rbx.Header.Content.Size = UDim2.new(1, 0, 0, navBarHeight) +end + +function Header:AddButton(button) + button.rbx.Parent = self.rbx.Header.Content.Buttons + button.rbx.LayoutOrder = #self.buttons + button.rbx.ZIndex = 10 +end + + +function Header:SetDefaultSubtitle() + spawn(function() + local player = PlayerService.LocalPlayer + + while player == nil do + wait(0.2) + player = PlayerService.LocalPlayer + end + + local usernameFetchRequest = Utilities.httpGet(Urls.api.."/users/"..tostring(player.UserId)) + local result = Utilities.decodeJSON( usernameFetchRequest ) + + if result and result.Username then + local displayText + if player:GetUnder13() then + displayText = string.format("%s: <13", result.Username) + else + displayText = string.format("%s: 13+", result.Username) + end + self:SetSubtitle(displayText) + end + end) +end + +function Header:SetSubtitle(displayText) + assert(type(displayText) == "nil" or type(displayText) == "string", + "Invalid argument number #1 to SetSubtitle, expected string or nil.") + + self.subtitle = displayText + + if displayText == "" then + self.rbx.Header.Content.Titles.Subtitle.Visible = false + else + self.rbx.Header.Content.Titles.Subtitle.Visible = true + self.rbx.Header.Content.Titles.Subtitle.Text = displayText + end +end + +return Header diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/LayoutInfo.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/LayoutInfo.lua new file mode 100644 index 0000000..73f2cb6 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/LayoutInfo.lua @@ -0,0 +1,70 @@ +local windowSize = game:GetService("CoreGui").RobloxGui.AbsoluteSize +local isLandscape = windowSize.x > windowSize.y + +local module = { + isLandscape = isLandscape; + ButtonsPerRow = 4; + SkinColorsPerRow = 5; + SkinColorGridPadding = 12; +} + +if isLandscape then + module.FullViewInitialPosition = UDim2.new(1, -112, 1, -52) + module.AvatarTypeSwitchOffColor = Color3.fromRGB(182, 182, 182) + module.AvatarTypeSwitchOnColor = Color3.new(1, 1, 1) + module.AvatarTypeSwitchTextSize = 14 + module.AvatarTypeSwitchInitialPosition = UDim2.new(0.5, -148, 0, 24) + module.AvatarTypeSwitchPositionFullView = UDim2.new(0.5, -148, 0, -86) + module.AvatarTypeSwitchPosition = UDim2.new(0.5, -148, 0, 24) + + module.TabWidth = 84 + module.TabHeight = 72 + module.FirstTabBonusWidth = 45 + module.GridPadding = 12 + module.ExtraVerticalShift = 8 + module.SkinColorExtraVerticalShift = 0 + + module.CameraCenterScreenPosition = UDim2.new(-0.5, 0, 0, 10) + module.CameraDefaultPosition = Vector3.new(11.4540, 4.4313, -24.0810) + module.CameraDefaultFocusPoint = Vector3.new(15.1598082, 0.25, -16.9464092) + + module.CameraDefaultFOV = 70 + + module.BackgroundTextColor = Color3.new(.9, .9, .9) + module.BackgroundTextFont = Enum.Font.SourceSans + + module.ScaleSliderSize = UDim2.new(1, -57, 0, 30) + module.SliderPositionY = 56 + module.SliderVeritcalOffset = 67 +else + module.FullViewInitialPosition = UDim2.new(1, -52, 1, -52) + module.AvatarTypeSwitchOffColor = Color3.new(0.44, 0.44, 0.44) + module.AvatarTypeSwitchOnColor = Color3.new(1, 1, 1) + module.AvatarTypeSwitchTextSize = 18 + module.AvatarTypeSwitchInitialPosition = UDim2.new(1, -88, 0, 24) + module.AvatarTypeSwitchPositionFullView = UDim2.new(1, -88, 0, -86) + module.AvatarTypeSwitchPosition = UDim2.new(1, -88, 0, 24) + + module.TabWidth = 60 + module.TabHeight = 50 + module.FirstTabBonusWidth = 10 + module.GridPadding = 6 + module.ExtraVerticalShift = 25 + module.SkinColorExtraVerticalShift = 25 + + module.CameraCenterScreenPosition = UDim2.new(0, 0, -0.5, 40) + module.CameraDefaultPosition = Vector3.new(10.2427, 5.1198, -30.9536) + module.CameraDefaultFocusPoint = Vector3.new(15.1598082, 0.25, -16.9464092) + + module.CameraDefaultFOV = 70 + + module.BackgroundTextColor = Color3.fromRGB(65, 78, 89) + module.BackgroundTextFont = Enum.Font.SourceSansLight + + module.ScaleSliderSize = UDim2.new(0.8, 0, 0, 30) + module.SliderPositionY = 70 + module.SliderVeritcalOffset = 70 +end + +return module + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/LayoutInfoConsole.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/LayoutInfoConsole.lua new file mode 100644 index 0000000..aff9f13 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/LayoutInfoConsole.lua @@ -0,0 +1,98 @@ +return { + RegularFont = Enum.Font.SourceSans; + + WhiteTextColor = Color3.fromRGB(255, 255, 255); + GrayTextColor = Color3.fromRGB(25, 25, 25); + + -- Font Sizes + ButtonFontSize = 36; + SubHeaderFontSize = 30; + + --CategoryMenu & Tablist + CategoryMenuPosition = UDim2.new(0, 100, 0, 270); + CategoryMenuFullviewPosition = UDim2.new(0, -360, 0, 270); + TabListPosition = UDim2.new(0, 220, 0, 170); + TabListFullviewPosition = UDim2.new(0, -360, 0, 170); + + CategoryButtonsPadding = 20; + CategoryButtonRowsPerPage = 6; + + CategoryButtonDefaultSize = UDim2.new(0, 360, 0, 80); + CategoryButtonSmallSize = UDim2.new(0, 80, 0, 80); + + CategoryButtonImageDefault = "rbxasset://textures/ui/Shell/AvatarEditor/button/btn-category.png"; + CategoryButtonImageSelected = "rbxasset://textures/ui/Shell/AvatarEditor/button/btn-category-selected.png"; + CategoryButtonSelectorImage = "rbxasset://textures/ui/Shell/AvatarEditor/graphic/gr-item selector-8px corner.png"; + + CategoryIconSize = UDim2.new(0, 32, 0, 32); + CategoryTextSize = UDim2.new(0, 200, 0, 50); + + --Indicator + IndicatorHeight = 83; + IndicatorBottomDistance = 60; + IndicatorIconTextDistance = 16; + IndicatorButtonsDistance = 60; + IndicatorMaxLength = 580; + + --Warning + WarningMaxLength = 580; + WarningTextPadding = 24; + TooltilsTipOffset = 25.5; + + --Tween + DefaultTweenTime = 0.2; + + --Layers + BackgroundLayer = 1; + BasicLayer = 2; + AssetImageLayer = 3; + BorderMaskLayer = 4; + ShadingOverlayLayer = 5; + IndicatorLayer = 6; + + --Page & CardGrid + ButtonsPerRow = 3; + ButtonRowsPerPage = 3; + SkinColorsPerRow = 5; + SkinColorRowsPerPage = 6; + ButtonSize = 150; + GridPadding = 20; + SkinColorGridPadding = 20; + ExtraVerticalShift = 0; + SkinColorExtraVerticalShift = 0; + SkinColorButtonSize = 74; + SelectorBottomMinDistance = 190; + SelectorTopMinDistance = 270; + + ItemCardSelectorImage = "rbxasset://textures/ui/Shell/AvatarEditor/graphic/gr-item selector-16px corner.png"; + + ItemAvailableBackgroundImage = "rbxasset://textures/ui/Shell/AvatarEditor/card/item card-available.png"; + ItemUnavailableBackgroundImage = "rbxasset://textures/ui/Shell/AvatarEditor/card/item card-unavailable.png"; + + ItemMaskImage = "rbxasset://textures/ui/Shell/AvatarEditor/graphic/gr-item mask.png"; + ItemMaskNotOwnedImage = "rbxasset://textures/ui/Shell/AvatarEditor/graphic/gr-item mask-not owned.png"; + WearingIndicatorImage = "rbxasset://textures/ui/Shell/AvatarEditor/graphic/gr-wearing indicator.png"; + + ColorPanelImage = "rbxasset://textures/ui/Shell/AvatarEditor/color selector/color panel.png"; + ColorEquippedImage = "rbxasset://textures/ui/Shell/AvatarEditor/color selector/color dot-selected.png"; + CheckMarkImage = "rbxasset://textures/ui/Shell/AvatarEditor/icon/ic-checkmark.png"; + ColorSelectorImage = "rbxasset://textures/ui/Shell/AvatarEditor/color selector/color dot-select.png"; + ColorDotImage = "rbxasset://textures/ui/Shell/AvatarEditor/color selector/color dot.png"; + ColorDotShadowImage = "rbxasset://textures/ui/Shell/AvatarEditor/color selector/color dot shadow.png"; + + --Scales + ScaleSliderSize = UDim2.new(1, 0, 0, 30); + SliderPositionY = 56; + SliderVeritcalOffset = 120; + + --Camera + CameraCenterScreenPosition = UDim2.new(0, 0, 0, 0); + + --Menu level + ConsoleMenuLevel = { + None = 0, + CategoryMenu = 1, + TabList = 2, + AssetsPage = 3 + }; +} diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/LocalizedStrings.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/LocalizedStrings.lua new file mode 100644 index 0000000..d1746da --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/LocalizedStrings.lua @@ -0,0 +1,1252 @@ +local LocalizationService = game:GetService("LocalizationService") +local HttpService = game:GetService("HttpService") +local CoreGui = game:GetService('CoreGui') + +local Modules = CoreGui:FindFirstChild("RobloxGui").Modules +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) +local LoadAvatarEditorTranslations = Flags:GetFlag("LoadAvatarEditorTranslations") + +local this = {} + +local function createLocalizationTable(contents) + local localTable = Instance.new("LocalizationTable") + localTable.SourceLocaleId = "en-us" + localTable:SetContents(HttpService:JSONEncode(contents)) + return localTable +end + +local AvatarEditorStringsTable = createLocalizationTable({ + { + key = "FullViewWord"; + values = + { + ["en-us"] = "Full View"; + ["es"] = "Vista completa"; + ["de"] = "Vollansicht"; + ["fr"] = "Affichage complet"; + ["pt-br"] = "Visão completa"; + ["ko"] = "전체 보기"; + ["zh-tw"] = "大螢幕"; + ["zh-cn"] = "全视图"; + } + }, + { + key = "ReturnToEditWord"; + values = + { + ["en-us"] = "Return to edit"; + ["es"] = "Volver a edición"; + ["de"] = "Zurück zum Bearbeiten"; + ["fr"] = "Revenir à l'édition"; + ["pt-br"] = "Voltar para editar"; + ["ko"] = "편집으로 돌아가기"; + ["zh-tw"] = "返回編輯"; + ["zh-cn"] = "返回编辑"; + } + }, + { + key = "SwitchToR6Word"; + values = + { + ["en-us"] = "Switch to R6"; + ["es"] = "Cambiar a R6"; + ["de"] = "Zu R6 wechseln"; + ["fr"] = "Passer au modèle R6"; + ["pt-br"] = "Trocar para R6"; + ["ko"] = "R6로 전환"; + ["zh-tw"] = "切換至 R6"; + ["zh-cn"] = "转换至 R6"; + } + }, + { + key = "SwitchToR15Word"; + values = + { + ["en-us"] = "Switch to R15"; + ["es"] = "Cambiar a R15"; + ["de"] = "Zu R15 wechseln"; + ["fr"] = "Passer au modèle R15"; + ["pt-br"] = "Trocar para R15"; + ["ko"] = "R15로 전환"; + ["zh-tw"] = "切換至 R15"; + ["zh-cn"] = "转换至 R6"; + } + }, + { + key = "RecentCategoryTitle"; + values = + { + ["en-us"] = "Recent"; + ["es"] = "Recientes"; + ["de"] = "Vor Kurzem verwendet"; + ["fr"] = "Récents"; + ["pt-br"] = "Recentes"; + ["ko"] = "최근"; + ["zh-tw"] = "近期使用"; + ["zh-cn"] = "最近使用"; + } + }, + { + key = "ClothingCategoryTitle"; + values = + { + ["en-us"] = "Clothing"; + ["es"] = "Ropa"; + ["de"] = "Kleidung"; + ["fr"] = "Vêtements"; + ["pt-br"] = "Roupas"; + ["ko"] = "복장"; + ["zh-tw"] = "衣物"; + ["zh-cn"] = "服装"; + } + }, + { + key = "BodyCategoryTitle"; + values = + { + ["en-us"] = "Body"; + ["es"] = "Cuerpo"; + ["de"] = "Körper"; + ["fr"] = "Corps"; + ["pt-br"] = "Corpo"; + ["ko"] = "신체"; + ["zh-tw"] = "身體"; + ["zh-cn"] = "身体"; + } + }, + { + key = "AnimationCategoryTitle"; + values = + { + ["en-us"] = "Animation"; + ["es"] = "Animación"; + ["de"] = "Animation"; + ["fr"] = "Animation"; + ["pt-br"] = "Animação"; + ["ko"] = "애니메이션"; + ["zh-tw"] = "動畫"; + ["zh-cn"] = "动画"; + } + }, + { + key = "AnimationCategoryLandscapeTitle"; + values = + { + ["en-us"] = "Animations"; + ["es"] = "Animaciones"; + ["de"] = "Animationen"; + ["fr"] = "Animations"; + ["pt-br"] = "Animações"; + ["ko"] = "애니메이션"; + ["zh-tw"] = "動畫"; + ["zh-cn"] = "动画"; + } + }, + { + key = "OutfitsCategoryTitle"; + values = + { + ["en-us"] = "Outfits"; + ["es"] = "Conjuntos"; + ["de"] = "Outfits"; + ["fr"] = "Tenues"; + ["pt-br"] = "Trajes"; + ["ko"] = "옷차림"; + ["zh-tw"] = "行頭"; + ["zh-cn"] = "装扮"; + } + }, + { + key = "RecentAllTitle"; + values = + { + ["en-us"] = "Recent All"; + ["es"] = "Todos los recientes"; + ["de"] = "Vor Kurzem verwendet: alles"; + ["fr"] = "Récents (tous)"; + ["pt-br"] = "Todos os recentes"; + ["ko"] = "최근 전체"; + ["zh-tw"] = "近期全部"; + ["zh-cn"] = "最近全部"; + } + }, + { + key = "RecentAllLandscapeTitle"; + values = + { + ["en-us"] = "All"; + ["es"] = "Todos"; + ["de"] = "Alle"; + ["fr"] = "Tous"; + ["pt-br"] = "Todos"; + ["ko"] = "전체"; + ["zh-tw"] = "全部"; + ["zh-cn"] = "全部"; + } + }, + { + key = "RecentClothingTitle"; + values = + { + ["en-us"] = "Recent Clothing"; + ["es"] = "Ropa reciente"; + ["de"] = "Vor Kurzem verwendet: Kleidung"; + ["fr"] = "Vêtements récents"; + ["pt-br"] = "Roupas recentes"; + ["ko"] = "최근 복장"; + ["zh-tw"] = "近期使用的衣物"; + ["zh-cn"] = "最近使用的服装"; + } + }, + { + key = "RecentClothingLandscapeTitle"; + values = + { + ["en-us"] = "Clothing"; + ["es"] = "Ropa"; + ["de"] = "Kleidung"; + ["fr"] = "Vêtements"; + ["pt-br"] = "Roupas"; + ["ko"] = "복장"; + ["zh-tw"] = "衣物"; + ["zh-cn"] = "服装"; + } + }, + { + key = "RecentBodyTitle"; + values = + { + ["en-us"] = "Recent Body"; + ["es"] = "Cuerpos recientes"; + ["de"] = "Vor Kurzem verwendet: Körper"; + ["fr"] = "Corps récents"; + ["pt-br"] = "Corpos recentes"; + ["ko"] = "최근 신체"; + ["zh-tw"] = "近期身體"; + ["zh-cn"] = "最近使用的身体类型"; + } + }, + { + key = "RecentBodyLandscapeTitle"; + values = + { + ["en-us"] = "Body"; + ["es"] = "Cuerpo"; + ["de"] = "Körper"; + ["fr"] = "Corps"; + ["pt-br"] = "Corpo"; + ["ko"] = "신체"; + ["zh-tw"] = "身體"; + ["zh-cn"] = "身体类型"; + } + }, + { + key = "RecentAnimationsTitle"; + values = + { + ["en-us"] = "Recent Animations"; + ["es"] = "Animaciones recientes"; + ["de"] = "Vor Kurzem verwendet: Animationen"; + ["fr"] = "Animations récentes"; + ["pt-br"] = "Animações recentes"; + ["ko"] = "최근 애니메이션"; + ["zh-tw"] = "近期使用的動畫"; + ["zh-cn"] = "最近使用的动画"; + } + }, + { + key = "RecentAnimationsLandscapeTitle"; + values = + { + ["en-us"] = "Animations"; + ["es"] = "Animaciones"; + ["de"] = "Animationen"; + ["fr"] = "Animations"; + ["pt-br"] = "Animações"; + ["ko"] = "애니메이션"; + ["zh-tw"] = "動畫"; + ["zh-cn"] = "动画"; + } + }, + { + key = "RecentOutfitsTitle"; + values = + { + ["en-us"] = "Recent Outfits"; + ["es"] = "Vestimentas recientes"; + ["de"] = "Vor Kurzem verwendet: Outfits"; + ["fr"] = "Tenues récentes"; + ["pt-br"] = "Trajes recentes"; + ["ko"] = "최근 옷차림"; + ["zh-tw"] = "近期使用的行頭"; + ["zh-cn"] = "最近使用的装扮"; + } + }, + { + key = "RecentOutfitsLandscapeTitle"; + values = + { + ["en-us"] = "Outfits"; + ["es"] = "Conjuntos"; + ["de"] = "Outfits"; + ["fr"] = "Tenues"; + ["pt-br"] = "Trajes"; + ["ko"] = "옷차림"; + ["zh-tw"] = "行頭"; + ["zh-cn"] = "装扮"; + } + }, + { + key = "OutfitsTabTitle"; + values = + { + ["en-us"] = "Outfits"; + ["es"] = "Conjuntos"; + ["de"] = "Outfits"; + ["fr"] = "Tenues"; + ["pt-br"] = "Trajes"; + ["ko"] = "옷차림"; + ["zh-tw"] = "行頭"; + ["zh-cn"] = "装扮"; + } + }, + { + key = "OutfitsTabLandscapeTitle"; + values = + { + ["en-us"] = "All"; + ["es"] = "Todos"; + ["de"] = "Alle"; + ["fr"] = "Tous"; + ["pt-br"] = "Todos"; + ["ko"] = "전체"; + ["zh-tw"] = "全部"; + ["zh-cn"] = "全部"; + } + }, + { + key = "HatsTitle"; + values = + { + ["en-us"] = "Hats"; + ["es"] = "Sombreros"; + ["de"] = "Hüte"; + ["fr"] = "Chapeaux"; + ["pt-br"] = "Chapéus"; + ["ko"] = "모자"; + ["zh-tw"] = "帽子"; + ["zh-cn"] = "帽子"; + } + }, + { + key = "HatsLandscapeTitle"; + values = + { + ["en-us"] = "Hat"; + ["es"] = "Sombrero"; + ["de"] = "Hut"; + ["fr"] = "Chapeau"; + ["pt-br"] = "Chapéu"; + ["ko"] = "모자"; + ["zh-tw"] = "帽子"; + ["zh-cn"] = "帽子"; + } + }, + { + key = "HairTitle"; + values = + { + ["en-us"] = "Hair"; + ["es"] = "Pelo"; + ["de"] = "Haare"; + ["fr"] = "Cheveux"; + ["pt-br"] = "Cabelo"; + ["ko"] = "헤어"; + ["zh-tw"] = "頭髮"; + ["zh-cn"] = "头发"; + } + }, + { + key = "FaceAccessoryTitle"; + values = + { + ["en-us"] = "Face Accessories"; + ["es"] = "Accesorios para la cara"; + ["de"] = "Gesicht-Accessoires"; + ["fr"] = "Accessoires de visage"; + ["pt-br"] = "Acessórios de rosto"; + ["ko"] = "얼굴 액세서리"; + ["zh-tw"] = "臉部配件"; + ["zh-cn"] = "脸部配饰"; + } + }, + { + key = "FaceAccessoryLandscapeTitle"; + values = + { + ["en-us"] = "Face"; + ["es"] = "Cara"; + ["de"] = "Gesicht"; + ["fr"] = "Visage"; + ["pt-br"] = "Rosto"; + ["ko"] = "얼굴"; + ["zh-tw"] = "臉"; + ["zh-cn"] = "脸部"; + } + }, + { + key = "NeckAccessoryTitle"; + values = + { + ["en-us"] = "Neck Accessories"; + ["es"] = "Accesorios para el cuello"; + ["de"] = "Hals-Accessoires"; + ["fr"] = "Accessoires de cou"; + ["pt-br"] = "Acessórios de pescoço"; + ["ko"] = "목 액세서리"; + ["zh-tw"] = "頸部配件"; + ["zh-cn"] = "颈部配饰"; + } + }, + { + key = "NeckAccessoryLandscapeTitle"; + values = + { + ["en-us"] = "Neck"; + ["es"] = "Cuello"; + ["de"] = "Hals"; + ["fr"] = "Cou"; + ["pt-br"] = "Pescoço"; + ["ko"] = "목"; + ["zh-tw"] = "頸"; + ["zh-cn"] = "颈部"; + } + }, + { + key = "ShoulderAccessoryTitle"; + values = + { + ["en-us"] = "Shoulder Accessories"; + ["es"] = "Accesorios para el hombro"; + ["de"] = "Schulter-Accessoires"; + ["fr"] = "Accessoires d'épaule"; + ["pt-br"] = "Acessórios de ombro"; + ["ko"] = "어깨 액세서리"; + ["zh-tw"] = "肩膀配件"; + ["zh-cn"] = "肩膀配饰"; + } + }, + { + key = "ShoulderAccessoryLandscapeTitle"; + values = + { + ["en-us"] = "Shoulder"; + ["es"] = "Hombro"; + ["de"] = "Schulter"; + ["fr"] = "Épaules"; + ["pt-br"] = "Ombro"; + ["ko"] = "어깨"; + ["zh-tw"] = "肩"; + ["zh-cn"] = "肩膀"; + } + }, + { + key = "FrontAccessoryTitle"; + values = + { + ["en-us"] = "Front Accessories"; + ["es"] = "Accesorios frontales"; + ["de"] = "Vorderseite-Accessoires"; + ["fr"] = "Accessoires avant"; + ["pt-br"] = "Acessórios da frente"; + ["ko"] = "가슴 액세서리"; + ["zh-tw"] = "正面配件"; + ["zh-cn"] = "正面配饰"; + } + }, + { + key = "FrontAccessoryLandscapeTitle"; + values = + { + ["en-us"] = "Front"; + ["es"] = "Frontal"; + ["de"] = "Vorderseite"; + ["fr"] = "Avant"; + ["pt-br"] = "Frente"; + ["ko"] = "가슴"; + ["zh-tw"] = "正面"; + ["zh-cn"] = "正面"; + } + }, + { + key = "BackAccessoryTitle"; + values = + { + ["en-us"] = "Back Accessories"; + ["es"] = "Accesorios traseros"; + ["de"] = "Rückseite-Accessoires"; + ["fr"] = "Accessoires arrière"; + ["pt-br"] = "Acessórios de costas"; + ["ko"] = "등 액세서리"; + ["zh-tw"] = "背面配件"; + ["zh-cn"] = "背面配饰"; + } + }, + { + key = "BackAccessoryLandscapeTitle"; + values = + { + ["en-us"] = "Back"; + ["es"] = "Trasero"; + ["de"] = "Rückseite"; + ["fr"] = "Retour"; + ["pt-br"] = "Costas"; + ["ko"] = "등"; + ["zh-tw"] = "背面"; + ["zh-cn"] = "背面"; + } + }, + { + key = "WaistAccessoryTitle"; + values = + { + ["en-us"] = "Waist Accessories"; + ["es"] = "Accesorios para la cintura"; + ["de"] = "Taille-Accessoires"; + ["fr"] = "Accessoires de taille"; + ["pt-br"] = "Acessórios de cintura"; + ["ko"] = "허리 액세서리"; + ["zh-tw"] = "腰部配件"; + ["zh-cn"] = "腰部配饰"; + } + }, + { + key = "WaistAccessoryLandscapeTitle"; + values = + { + ["en-us"] = "Waist"; + ["es"] = "Cintura"; + ["de"] = "Taille"; + ["fr"] = "Taille"; + ["pt-br"] = "Cintura"; + ["ko"] = "허리"; + ["zh-tw"] = "腰"; + ["zh-cn"] = "腰部"; + } + }, + { + key = "ShirtsTitle"; + values = + { + ["en-us"] = "Shirts"; + ["es"] = "Camisas"; + ["de"] = "Hemden"; + ["fr"] = "Chemises"; + ["pt-br"] = "Camisas"; + ["ko"] = "셔츠"; + ["zh-tw"] = "上衣"; + ["zh-cn"] = "衬衫"; + } + }, + { + key = "ShirtsLandscapeTitle"; + values = + { + ["en-us"] = "Shirt"; + ["es"] = "Camisa"; + ["de"] = "Hemd"; + ["fr"] = "Chemise"; + ["pt-br"] = "Camisa"; + ["ko"] = "셔츠"; + ["zh-tw"] = "上衣"; + ["zh-cn"] = "衬衫"; + } + }, + { + key = "PantsTitle"; + values = + { + ["en-us"] = "Pants"; + ["es"] = "Pantalones"; + ["de"] = "Hosen"; + ["fr"] = "Pantalons"; + ["pt-br"] = "Calças"; + ["ko"] = "바지"; + ["zh-tw"] = "褲子"; + ["zh-cn"] = "裤子"; + } + }, + { + key = "FacesTitle"; + values = + { + ["en-us"] = "Faces"; + ["es"] = "Caras"; + ["de"] = "Gesichter"; + ["fr"] = "Visages"; + ["pt-br"] = "Rostos"; + ["ko"] = "얼굴"; + ["zh-tw"] = "臉"; + ["zh-cn"] = "脸部"; + } + }, + { + key = "FacesLandscapeTitle"; + values = + { + ["en-us"] = "Face"; + ["es"] = "Cara"; + ["de"] = "Gesicht"; + ["fr"] = "Visage"; + ["pt-br"] = "Rosto"; + ["ko"] = "얼굴"; + ["zh-tw"] = "臉"; + ["zh-cn"] = "脸部"; + } + }, + { + key = "HeadsTitle"; + values = + { + ["en-us"] = "Heads"; + ["es"] = "Cabezas"; + ["de"] = "Köpfe"; + ["fr"] = "Têtes"; + ["pt-br"] = "Cabeças"; + ["ko"] = "머리"; + ["zh-tw"] = "頭"; + ["zh-cn"] = "头部"; + } + }, + { + key = "HeadsLandscapeTitle"; + values = + { + ["en-us"] = "Head"; + ["es"] = "Cabeza"; + ["de"] = "Kopf"; + ["fr"] = "Tête"; + ["pt-br"] = "Cabeça"; + ["ko"] = "머리"; + ["zh-tw"] = "頭"; + ["zh-cn"] = "头部"; + } + }, + { + key = "TorsosTitle"; + values = + { + ["en-us"] = "Torsos"; + ["es"] = "Torsos"; + ["de"] = "Torsos"; + ["fr"] = "Torses"; + ["pt-br"] = "Torsos"; + ["ko"] = "상체"; + ["zh-tw"] = "身體"; + ["zh-cn"] = "身体主干"; + } + }, + { + key = "TorsosLandscapeTitle"; + values = + { + ["en-us"] = "Torso"; + ["es"] = "Torso"; + ["de"] = "Torso"; + ["fr"] = "Torse"; + ["pt-br"] = "Torso"; + ["ko"] = "상체"; + ["zh-tw"] = "軀幹"; + ["zh-cn"] = "身体主干"; + } + }, + { + key = "RightArmsTitle"; + values = + { + ["en-us"] = "Right Arms"; + ["es"] = "Brazos derechos"; + ["de"] = "Rechte Arme"; + ["fr"] = "Bras droits"; + ["pt-br"] = "Braços direitos"; + ["ko"] = "오른팔"; + ["zh-tw"] = "右臂"; + ["zh-cn"] = "右臂"; + } + }, + { + key = "LeftArmsTitle"; + values = + { + ["en-us"] = "Left Arms"; + ["es"] = "Brazos izquierdos"; + ["de"] = "Linke Arme"; + ["fr"] = "Bras gauches"; + ["pt-br"] = "Braços esquerdos"; + ["ko"] = "왼팔"; + ["zh-tw"] = "左臂"; + ["zh-cn"] = "左臂"; + } + }, + { + key = "RightLegsTitle"; + values = + { + ["en-us"] = "Right Legs"; + ["es"] = "Piernas derechas"; + ["de"] = "Rechte Beine"; + ["fr"] = "Jambes droites"; + ["pt-br"] = "Pernas direitas"; + ["ko"] = "오른 다리"; + ["zh-tw"] = "右腿"; + ["zh-cn"] = "右腿"; + } + }, + { + key = "LeftLegsTitle"; + values = + { + ["en-us"] = "Left Legs"; + ["es"] = "Piernas izquierdas"; + ["de"] = "Linke Beine"; + ["fr"] = "Jambes gauches"; + ["pt-br"] = "Pernas esquerdas"; + ["ko"] = "왼 다리"; + ["zh-tw"] = "左腿"; + ["zh-cn"] = "左腿"; + } + }, + { + key = "GearTitle"; + values = + { + ["en-us"] = "Gear"; + ["es"] = "Equipamiento"; + ["de"] = "Ausrüstung"; + ["fr"] = "Équipement"; + ["pt-br"] = "Equipamentos"; + ["ko"] = "기어"; + ["zh-tw"] = "裝備"; + ["zh-cn"] = "装备"; + } + }, + { + key = "SkinToneTitle"; + values = + { + ["en-us"] = "Skin Tone"; + ["es"] = "Tono de piel"; + ["de"] = "Hautfarbe"; + ["fr"] = "Teint"; + ["pt-br"] = "Cor de pele"; + ["ko"] = "피부 색조"; + ["zh-tw"] = "膚色"; + ["zh-cn"] = "肤色"; + } + }, + { + key = "ScaleTitle"; + values = + { + ["en-us"] = "Scale"; + ["es"] = "Escala"; + ["de"] = "Größe"; + ["fr"] = "Taille"; + ["pt-br"] = "Dimensionar"; + ["ko"] = "크기"; + ["zh-tw"] = "比例"; + ["zh-cn"] = "大小"; + } + }, + { + key = "ScaleHeightTitle"; + values = + { + ["en-us"] = "Height"; + ["es"] = "Altura"; + ["de"] = "Höhe"; + ["fr"] = "Hauteur"; + ["pt-br"] = "Altura"; + ["ko"] = "높이"; + ["zh-tw"] = "高度"; + ["zh-cn"] = "高度"; + } + }, + { + key = "ScaleWidthTitle"; + values = + { + ["en-us"] = "Width"; + ["es"] = "Anchura"; + ["de"] = "Breite"; + ["fr"] = "Largeur"; + ["pt-br"] = "Largura"; + ["ko"] = "넓이"; + ["zh-tw"] = "寬度"; + ["zh-cn"] = "宽度"; + } + }, + { + key = "ScaleHeadTitle"; + values = + { + ["en-us"] = "Head"; + ["es"] = "Cabeza"; + ["de"] = "Kopf"; + ["fr"] = "Tête"; + ["pt-br"] = "Cabeça"; + ["ko"] = "머리"; + ["zh-tw"] = "頭"; + ["zh-cn"] = "头部"; + } + }, + { + key = "ScaleBodyTypeTitle"; + values = + { + ["en-us"] = "Body Type"; + ["es"] = "Tipo de cuerpo"; + ["de"] = "Körpertyp"; + ["fr"] = "Type de corps"; + ["pt-br"] = "Tipo de corpo"; + ["ko"] = "신체 유형"; + ["zh-tw"] = "內文類型"; + ["zh-cn"] = "体形"; + } + }, + { + key = "ScaleProportionTitle"; + values = + { + ["en-us"] = "Proportions"; + ["es"] = "Proporciones"; + ["de"] = "Proportionen"; + ["fr"] = "Proportions"; + ["pt-br"] = "Proporções"; + ["ko"] = "비율"; + ["zh-tw"] = "比例"; + ["zh-cn"] = "比例"; + } + }, + { + key = "ClimbAnimationsWord"; + values = + { + ["en-us"] = "Climb Animations"; + ["es"] = "Animaciones de escalada"; + ["de"] = "Kletteranimationen"; + ["fr"] = "Animations d'escalade"; + ["pt-br"] = "Animações de escalada"; + ["ko"] = "오르기 애니메이션"; + ["zh-tw"] = "攀查動畫"; + ["zh-cn"] = "攀爬动画"; + } + }, + { + key = "JumpAnimationsWord"; + values = + { + ["en-us"] = "Jump Animations"; + ["es"] = "Animaciones de salto"; + ["de"] = "Springanimationen"; + ["fr"] = "Animations de saut"; + ["pt-br"] = "Animações de salto"; + ["ko"] = "점프 애니메이션"; + ["zh-tw"] = "跳躍動畫"; + ["zh-cn"] = "跳跃动画"; + } + }, + { + key = "FallAnimationsWord"; + values = + { + ["en-us"] = "Fall Animations"; + ["es"] = "Animaciones de caída"; + ["de"] = "Fallanimationen"; + ["fr"] = "Animations de chute"; + ["pt-br"] = "Animações de queda"; + ["ko"] = "낙하 애니메이션"; + ["zh-tw"] = "下降動畫"; + ["zh-cn"] = "下降动画"; + } + }, + { + key = "IdleAnimationsWord"; + values = + { + ["en-us"] = "Idle Animations"; + ["es"] = "Animaciones de inactividad"; + ["de"] = "Untätige Animationen"; + ["fr"] = "Animations d'inaction"; + ["pt-br"] = "Animações de inatividade"; + ["ko"] = "기본 애니메이션"; + ["zh-tw"] = "閒置動畫"; + ["zh-cn"] = "闲置动画"; + } + }, + { + key = "WalkAnimationsWord"; + values = + { + ["en-us"] = "Walk Animations"; + ["es"] = "Animaciones de marcha"; + ["de"] = "Gehanimationen"; + ["fr"] = "Animations de marche"; + ["pt-br"] = "Animações de caminhada"; + ["ko"] = "걷기 애니메이션"; + ["zh-tw"] = "步行動畫"; + ["zh-cn"] = "行走动画"; + } + }, + { + key = "RunAnimationsWord"; + values = + { + ["en-us"] = "Run Animations"; + ["es"] = "Animaciones de carrera"; + ["de"] = "Laufanimationen"; + ["fr"] = "Animations de course"; + ["pt-br"] = "Animações de corrida"; + ["ko"] = "달리기 애니메이션"; + ["zh-tw"] = "奔跑動畫"; + ["zh-cn"] = "跑步动画"; + } + }, + { + key = "SwimAnimationsWord"; + values = + { + ["en-us"] = "Swim Animations"; + ["es"] = "Animaciones de nado"; + ["de"] = "Schwimmanimationen"; + ["fr"] = "Animations de nage"; + ["pt-br"] = "Animações de nado"; + ["ko"] = "수영 애니메이션"; + ["zh-tw"] = "游泳動畫"; + ["zh-cn"] = "游泳动画"; + } + }, + { + key = "ClimbWord"; + values = + { + ["en-us"] = "Climb"; + ["es"] = "Escalada"; + ["de"] = "Klettern"; + ["fr"] = "Escalade"; + ["pt-br"] = "Escalar"; + ["ko"] = "오르기"; + ["zh-tw"] = "攀爬"; + ["zh-cn"] = "攀爬"; + } + }, + { + key = "JumpWord"; + values = + { + ["en-us"] = "Jump"; + ["es"] = "Salto"; + ["de"] = "Springen"; + ["fr"] = "Saut"; + ["pt-br"] = "Pular"; + ["ko"] = "점프"; + ["zh-tw"] = "跳起"; + ["zh-cn"] = "跳跃"; + } + }, + { + key = "FallWord"; + values = + { + ["en-us"] = "Fall"; + ["es"] = "Caída"; + ["de"] = "Fallen"; + ["fr"] = "Chute"; + ["pt-br"] = "Cair"; + ["ko"] = "낙하"; + ["zh-tw"] = "下降"; + ["zh-cn"] = "下降"; + } + }, + { + key = "IdleWord"; + values = + { + ["en-us"] = "Idle"; + ["es"] = "Inactividad"; + ["de"] = "Untätig"; + ["fr"] = "Inaction"; + ["pt-br"] = "Inatividade"; + ["ko"] = "기본"; + ["zh-tw"] = "閒置"; + ["zh-cn"] = "闲置"; + } + }, + { + key = "WalkWord"; + values = + { + ["en-us"] = "Walk"; + ["es"] = "Marcha"; + ["de"] = "Gehen"; + ["fr"] = "Marche"; + ["pt-br"] = "Andar"; + ["ko"] = "걷기"; + ["zh-tw"] = "步行"; + ["zh-cn"] = "行走"; + } + }, + { + key = "RunWord"; + values = + { + ["en-us"] = "Run"; + ["es"] = "Carrera"; + ["de"] = "Laufen"; + ["fr"] = "Course"; + ["pt-br"] = "Correr"; + ["ko"] = "달리기"; + ["zh-tw"] = "奔跑"; + ["zh-cn"] = "跑步"; + } + }, + { + key = "SwimWord"; + values = + { + ["en-us"] = "Swim"; + ["es"] = "Nado"; + ["de"] = "Schwimmen"; + ["fr"] = "Nage"; + ["pt-br"] = "Nadar"; + ["ko"] = "수영"; + ["zh-tw"] = "游泳"; + ["zh-cn"] = "游泳"; + } + }, + { + key = "NoAssetsPhrase"; + values = + { + ["en-us"] = "You don't have any %s"; + ["es"] = "No tienes %s"; + ["de"] = "Du hast keine %s."; + ["fr"] = "%s : rien à afficher"; + ["pt-br"] = "Você não possui nenhum(a) %s"; + ["ko"] = "보유한 %s이(가) 없어요."; + ["zh-tw"] = "您完全沒有%s"; + ["zh-cn"] = "你没有 %s"; + } + }, + { + key = "RecentItemsWord"; + values = + { + ["en-us"] = "recent items"; + ["es"] = "objetos recientes"; + ["de"] = "kürzlich verwendeten Gegenstände"; + ["fr"] = "objets récents"; + ["pt-br"] = "itens recentes"; + ["ko"] = "최근 아이템"; + ["zh-tw"] = "近期項目"; + ["zh-cn"] = "最近项目"; + } + }, + { + key = "RecommendedWord"; + values = + { + ["en-us"] = "Recommended"; + ["es"] = "Recomendado"; + ["de"] = "Empfohlen"; + ["fr"] = "Recommandés"; + ["pt-br"] = "Recomendado"; + ["ko"] = "추천"; + ["zh-tw"] = "推薦"; + ["zh-cn"] = "推荐"; + } + }, + { + key = "WearWord"; + values = + { + ["en-us"] = "Wear"; + ["es"] = "Vestir"; + ["de"] = "Tragen"; + ["fr"] = "Porter"; + ["pt-br"] = "Usar"; + ["ko"] = "착용"; + ["zh-tw"] = "穿戴"; + ["zh-cn"] = "穿戴"; + } + }, + { + key = "TakeOffWord"; + values = + { + ["en-us"] = "Take Off"; + ["es"] = "Quitar"; + ["de"] = "Ablegen"; + ["fr"] = "Retirer"; + ["pt-br"] = "Remover"; + ["ko"] = "해제"; + ["zh-tw"] = "取下"; + ["zh-cn"] = "移去"; + } + }, + { + key = "CancelWord"; + values = + { + ["en-us"] = "Cancel"; + ["es"] = "Cancelar"; + ["de"] = "Abbrechen"; + ["fr"] = "Annuler"; + ["pt-br"] = "Cancelar"; + ["ko"] = "취소"; + ["zh-tw"] = "取消"; + ["zh-cn"] = "取消"; + } + }, + { + key = "ViewDetailsWord"; + values = + { + ["en-us"] = "View details"; + ["es"] = "Ver detalles"; + ["de"] = "Infos anzeigen"; + ["fr"] = "Voir les détails"; + ["pt-br"] = "Ver detalhes"; + ["ko"] = "자세히 보기"; + ["zh-tw"] = "檢視詳情"; + ["zh-cn"] = "查看详情"; + } + }, + { + key = "ScalingForR15Phrase"; + values = + { + ["en-us"] = "Scaling only works\nfor R15 avatars"; + ["es"] = "El escalado solo funciona\ncon avatares R15"; + ["de"] = "Skalierung funktioniert nur\nfür R15-Avatare."; + ["fr"] = "Le changement de taille ne fonctionne\nque pour les avatars R15"; + ["pt-br"] = "Dimensionamento só funciona\npara avatares R15"; + ["ko"] = "크기 변경은\nR15 아바타만 가능"; + ["zh-tw"] = "縮放僅適用於\nR15 虛擬人偶"; + ["zh-cn"] = "缩放仅适用于\nR15 虚拟形象"; + } + }, + { + key = "ScalingForR15ConsolePhrase"; + values = + { + ["en-us"] = "Scaling only works for R15 avatars"; + ["es"] = "El escalado solo funciona con avatares R15"; + ["de"] = "Skalierung funktioniert nur für R15-Avatare."; + ["fr"] = "Le changement de taille ne fonctionne que pour les avatars R15"; + ["pt-br"] = "Dimensionamento só funciona para avatares R15"; + ["ko"] = "크기 변경은 R15 아바타만 가능"; + ["zh-tw"] = "縮放僅適用於 R15 虛擬人偶"; + ["zh-cn"] = "缩放仅适用于 R15 虚拟形象"; + } + }, + { + key = "AnimationsForR15Phrase"; + values = + { + ["en-us"] = "Animations only work\nfor R15 avatars"; + ["es"] = "Las animaciones solo funcionan\ncon avatares R15"; + ["de"] = "Animationen funktionieren nur\nfür R15-Avatare."; + ["fr"] = "Les animations ne fonctionnent\nque pour les avatars R15"; + ["pt-br"] = "Animações só funcionam\npara avatares R15"; + ["ko"] = "애니메이션은\nR15 아바타만 가능"; + ["zh-tw"] = "動畫僅適用於\nR15 虛擬人偶"; + ["zh-cn"] = "动画仅适用于\nR15 虚拟形象"; + } + }, + { + key = "AnimationsForR15ConsolePhrase"; + values = + { + ["en-us"] = "Animations only work for R15 avatars"; + ["es"] = "Las animaciones solo funcionan con avatares R15"; + ["de"] = "Animationen funktionieren nur für R15-Avatare."; + ["fr"] = "Les animations ne fonctionnent que pour les avatars R15"; + ["pt-br"] = "Animações só funcionam para avatares R15"; + ["ko"] = "애니메이션은 R15 아바타만 가능"; + ["zh-tw"] = "動畫僅適用於 R15 虛擬人偶"; + ["zh-cn"] = "动画仅适用于 R15 虚拟形象"; + } + }, + { + key = "R15OnlyPhrase"; + values = + { + ["en-us"] = "This feature is only available for R15"; + ["es"] = "Esta función solo está disponible con R15"; + } + }, + { + key = "DefaultClothingAppliedPhrase"; + values = + { + ["en-us"] = "Default clothing has been applied to your avatar - wear something from your wardrobe"; + ["es"] = "Se le ha aplicado la ropa predeterminada a tu avatar. Viste algo de tu guardarropa"; + ["de"] = "Dein Avatar trägt die Standardkleidung. Zieh doch etwas aus deinem Kleiderschrank an."; + ["fr"] = "Les vêtements par défaut ont été appliqués à votre avatar ; enfilez des pièces de votre garde-robe."; + ["pt-br"] = "Roupas padrão foram aplicadas ao avatar. Vista algo do seu guarda-roupa."; + ["ko"] = "기본 복장이 아바타에 적용되었어요. 옷장에서 선택하여 착용해보세요."; + ["zh-tw"] = "預設衣物已套用至您的虛擬人偶 - 請自您的衣櫃取一些衣物穿戴。"; + ["zh-cn"] = "默认服装已应用至你的虚拟形象 - 从你的衣柜里挑选喜爱的搭配吧!"; + } + }, + { + key = "ShopNowWord"; + values = + { + ["en-us"] = "Shop Now"; + ["es"] = "Comprar"; + ["de"] = "Jetzt einkaufen"; + ["fr"] = "Acheter maintenant"; + ["pt-br"] = "Comprar agora"; + ["ko"] = "지금 구매"; + ["zh-tw"] = "立即選購"; + ["zh-cn"] = "立即购买"; + } + }, +}) + +function this:GetLocale() + if LoadAvatarEditorTranslations then + return game:GetService("LocalizationService").RobloxLocaleId + end + return "en-us" +end + +function this:GetAvatarEditorString(locale, stringKey) + local success, result = pcall(function() + return AvatarEditorStringsTable:GetString(locale, stringKey) + end) + + if success and result then + return result + end + + return nil +end + +function this:LocalizedString(stringKey) + local locale = self:GetLocale() + local localeLanguage = locale and string.sub(locale, 1, 2) + local result = locale and self:GetAvatarEditorString(locale, stringKey) or + self:GetAvatarEditorString(localeLanguage, stringKey) + if not result then + if UserSettings().GameSettings:InStudioMode() then + print("LocalizedString: Could not find string for:" , stringKey , "using locale:" , locale) + end + result = self:GetAvatarEditorString("en-us", stringKey) or stringKey + end + return result +end + +return this diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/LongPressMenu.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/LongPressMenu.lua new file mode 100644 index 0000000..e57350c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/LongPressMenu.lua @@ -0,0 +1,238 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local guiService = game:GetService('GuiService') + +local utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local tween = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController) +local Strings = require(Modules.LuaApp.Legacy.AvatarEditor.LocalizedStrings) +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfo) +local AssetInfo = require(Modules.LuaApp.Legacy.AvatarEditor.AssetInfo) +local Urls = require(Modules.LuaApp.Legacy.AvatarEditor.Urls) +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) +local GetButtonClickEvent = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonClickEvent) +local GetButtonDownEventConnected = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonDownEventConnected) + +local shadeLayer = nil +local menuFrame = nil +local menuTitleLabel +local menuCancelButton = nil +local detailsMenuFrame = nil +local detailsNameLabel +local detailsScrollingFrame = nil +local detailsDescriptionLabel = nil +local detailsCreatorLabel +local detailsImageLabel +local detailsCloseButton = nil +local detailsCloseImageLabel + +local this = {} + +local characterManager = nil + +local detailsMenuCount = 0 + +local function closeMenu() + local tweenInfo = TweenInfo.new(0.13, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + if LayoutInfo.isLandscape then + tween(menuFrame, tweenInfo, { Position = UDim2.new(0.5, -200, 1, 0) }) + else + tween(menuFrame, tweenInfo, { Position = UDim2.new(0, 15, 1, 0) }) + end + + tween(shadeLayer, tweenInfo, { BackgroundTransparency = 1 }).Completed:Connect(function() + shadeLayer.Visible = false + end) +end + + +local function closeDetails() + local tweenInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + + if LayoutInfo.isLandscape then + tween(detailsMenuFrame, tweenInfo, { Position = UDim2.new(0.5, -200, -0.5, -200) }) + else + tween(detailsMenuFrame, tweenInfo, { Position = UDim2.new(0, 15, -0.7, -40) }) + end + tween(shadeLayer, tweenInfo, { BackgroundTransparency = 1 }).Completed:Connect(function() + shadeLayer.Visible = false + end) + detailsCloseImageLabel.ImageTransparency = 0.25 +end + + +local openMenuCount = 0 +local function openMenu(title, tableOfButtons, assetId) + openMenuCount = openMenuCount + 1 + local myOpenMenuCount = openMenuCount + + menuTitleLabel.Text = title + + if assetId then + utilities.fastSpawn(function() + local assetInfo = AssetInfo.getAssetInfo(assetId) + if assetInfo and openMenuCount == myOpenMenuCount then + menuTitleLabel.Text = assetInfo.Name + end + end) + end + + --destroy previous buttons + for _,v in pairs(menuFrame:GetChildren()) do + if v.Name == 'OptionButton' then + v:Destroy() + end + end + + for i,v in pairs(tableOfButtons) do + local button = Instance.new('TextButton') + button.Name = 'OptionButton' + button.ZIndex = menuCancelButton.ZIndex + button.AutoButtonColor = false + button.Size = UDim2.new(1, 0, 0, 48) + button.Position = UDim2.new(0, 0, 0, 48 * i) + button.BorderSizePixel = 0 + button.Text = v.text + button.BackgroundColor3 = Color3.new(1, 1, 1) --Color3.fromRGB(255,255,255) + button.TextColor3 = Color3.new(.295, .295, .295) --Color3.fromRGB(75,75,75) + button.FontSize = 'Size18' + button.Font = 'SourceSansLight' + local divider = Instance.new('Frame') + divider.ZIndex = button.ZIndex + divider.BorderSizePixel = 0 + divider.BackgroundColor3 = Color3.new(.816, .816, .816) --Color3.fromRGB(208,208,208) + divider.Size = UDim2.new(1, 0, 0, 1) + divider.Position = UDim2.new(0, 0, 1, -1) + divider.Parent = button + button.Parent = menuFrame + if v.func then + GetButtonClickEvent(button):connect(v.func) + end + end + menuFrame.Size = UDim2.new(1, -30, 0, 48 * (#tableOfButtons + 2)) + + local tweenInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + if LayoutInfo.isLandscape then + menuFrame.Size = UDim2.new(0, 400, 0, 48 * (#tableOfButtons + 2)) + menuFrame.Position = UDim2.new(0.5, -200, 1, 0) + tween(menuFrame, tweenInfo, { Position = UDim2.new(0.5, -200, 1, -menuFrame.Size.Y.Offset) }) + else + tween(menuFrame, tweenInfo, { Position = UDim2.new(0, 15, 1, -menuFrame.Size.Y.Offset) }) + end + shadeLayer.Visible = true + tween(shadeLayer, tweenInfo, { BackgroundTransparency = 0.45 }) +end + + +local AvatarEditorCatalogRecommended = Flags:GetFlag("AvatarEditorCatalogRecommended") + +function this:openDetails(assetId) + if AvatarEditorCatalogRecommended then + closeMenu() + guiService:OpenNativeOverlay("Catalog", Urls.catalogUrlBase..assetId) + else + detailsMenuCount = detailsMenuCount + 1 + local myDetailsMenuCount = detailsMenuCount + + detailsCloseImageLabel.ImageTransparency = 0.7 + + if assetId then + utilities.fastSpawn(function() + local assetInfo = characterManager.getCompleteAssetInfo(assetId) + if assetInfo and detailsMenuCount == myDetailsMenuCount then + if assetInfo.Name then + detailsNameLabel.Text = assetInfo.Name + end + if assetInfo.Description then + detailsDescriptionLabel.Text = assetInfo.Description + detailsScrollingFrame.CanvasSize = UDim2.new(1, -30, 0, detailsDescriptionLabel.TextBounds.Y) + detailsScrollingFrame.CanvasPosition = Vector2.new(0, 0) + end + if assetInfo.Creator and assetInfo.Creator.Name then + detailsCreatorLabel.Text = 'By '..assetInfo.Creator.Name + end + detailsImageLabel.Image = Urls.assetImageUrl150..tostring(assetId) + end + end) + end + + closeMenu() + + local tweenInfo = TweenInfo.new(0.35, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0) + + if LayoutInfo.isLandscape then + detailsMenuFrame.Size = UDim2.new(0, 400, 0, 400) + detailsMenuFrame.Position = UDim2.new(0.5, -200, -0.5, -200) + tween(detailsMenuFrame, tweenInfo, { Position = UDim2.new(0.5, -200, 0.5, -200) }) + else + tween(detailsMenuFrame, tweenInfo, { Position = UDim2.new(0, 15, 0.15, 0) }) + end + + shadeLayer.Visible = true + utilities.fastSpawn(function() + wait() --This is to make sure that the fade in happens after the fadeout of the menu, to overwrite that shade tween + tween(shadeLayer, tweenInfo, { BackgroundTransparency = 0.45 }) + end) + end +end + + +function this:showMenu(wearOrUnwearOption, assetId) + utilities.fastSpawn(characterManager.getCompleteAssetInfo, assetId) + openMenu('', + { + wearOrUnwearOption, + {text = Strings:LocalizedString("ViewDetailsWord"), func = function() this:openDetails(assetId) end}, + }, + assetId + ) +end + + +function this:hideMenu() + local tweenInfo = TweenInfo.new(0.13, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + if LayoutInfo.isLandscape then + tween(menuFrame, tweenInfo, { Position = UDim2.new(0.5, -200, 1, 0) }) + else + tween(menuFrame, tweenInfo, { Position = UDim2.new(0, 15, 1, 0) }) + end + tween(shadeLayer, tweenInfo, { BackgroundTransparency = 1 }).Completed:Connect(function() + shadeLayer.Visible = false + end) +end + + +return function(inCharacterManager, inShadeLayer, inMenuFrame, inDetailsMenuFrame) + shadeLayer = inShadeLayer + characterManager = inCharacterManager + menuFrame = inMenuFrame + detailsMenuFrame = inDetailsMenuFrame + + menuTitleLabel = menuFrame:WaitForChild('TitleLabel') + menuCancelButton = menuFrame:WaitForChild('CancelButton') + detailsNameLabel = detailsMenuFrame:WaitForChild('NameLabel') + detailsScrollingFrame = detailsMenuFrame:WaitForChild('ScrollingDescription') + detailsDescriptionLabel = detailsScrollingFrame:WaitForChild('TextLabel') + detailsCreatorLabel = detailsMenuFrame:WaitForChild('CreatorLabel') + detailsImageLabel = detailsMenuFrame:WaitForChild('ImageLabel') + detailsCloseButton = detailsMenuFrame:WaitForChild('CloseButton') + detailsCloseImageLabel = detailsCloseButton:WaitForChild('ImageLabel') + + menuCancelButton.Text = Strings:LocalizedString("CancelWord") + + GetButtonClickEvent(detailsCloseButton):connect(function() + closeDetails() + end) + + GetButtonDownEventConnected(detailsCloseButton, function() + detailsCloseImageLabel.ImageTransparency = 0.25 + end) + + GetButtonClickEvent(menuCancelButton):connect(closeMenu) + GetButtonClickEvent(shadeLayer):connect(function() + closeMenu() + closeDetails() + end) + + return this +end + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/OldHeader.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/OldHeader.lua new file mode 100644 index 0000000..c8842fd --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/OldHeader.lua @@ -0,0 +1,119 @@ + +local GuiService = game:GetService("GuiService") +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules + +local Create = require(Modules.Mobile.Create) +local Constants = require(Modules.Mobile.Constants) +local NotificationType = require(Modules.LuaApp.Enum.NotificationType) +local PaddedImageButton = require(Modules.LuaApp.Legacy.AvatarEditor.PaddedImageButton) + +local Header = {} +Header.__index = Header + +function Header.new(title) + + local self = {} + setmetatable(self, Header) + + self.buttons = {} + + self.title = title + self.subtitle = nil + + self.rbx = Create.new "Frame" { + Name = "HeaderFrame", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, Constants.Header.HEIGHT), + Position = UDim2.new(0, 0, 0, -Constants.Header.HEIGHT), + + Create.new "Frame" { + Name = "Header", + BackgroundColor3 = Constants.Color.BLUE_PRESSED, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, Constants.Header.HEIGHT), + ZIndex = 10, + + Create.new "Frame" { + Name = "Content", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, -24), + Position = UDim2.new(0, 0, 0, 24), + + Create.new "Frame" { + Name = "Titles", + BackgroundTransparency = 1, + Size = UDim2.new(0, 200, 1, 0), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(0.5, 0), + + Create.new "UIListLayout" { + SortOrder = "LayoutOrder", + }, + + Create.new "TextLabel" { + Name = "Title", + BackgroundTransparency = 1, + TextSize = 20, + TextColor3 = Constants.Color.WHITE, + Size = UDim2.new(0, 200, 0, 18), + AnchorPoint = Vector2.new(0.5, 0.5), + Text = title, + Font = "SourceSansBold", + LayoutOrder = 0, + ZIndex = 10, + }, + + Create.new "TextLabel" { + Name = "Subtitle", + BackgroundTransparency = 1, + TextSize = 12, + TextColor3 = Constants.Color.WHITE, + Size = UDim2.new(0, 200, 0, 12), + AnchorPoint = Vector2.new(0.5, 0.5), + Text = "", + Font = "SourceSans", + LayoutOrder = 1, + ZIndex = 10, + }, + }, + + Create.new "Frame" { + Name = "Buttons", + BackgroundTransparency = 1, + Position = UDim2.new(1, -5, 0, 0), + Size = UDim2.new(0, 100, 1, 0), + AnchorPoint = Vector2.new(1, 0), + + Create.new "UIListLayout" { + SortOrder = "LayoutOrder", + HorizontalAlignment = "Right", + FillDirection = "Horizontal", + }, + }, + }, + } + } + + local robuxButton = PaddedImageButton.new("robux", "rbxasset://textures/icon_ROBUX.png", 28) + robuxButton.Pressed:Connect(function() + GuiService:BroadcastNotification("", NotificationType.PURCHASE_ROBUX) + end) + self:AddButton(robuxButton) + + local notificationsButton = PaddedImageButton.new("robux", "rbxasset://textures/Icon_Stream_Off.png", 28) + notificationsButton.Pressed:Connect(function() + GuiService:BroadcastNotification("", NotificationType.VIEW_NOTIFICATIONS) + end) + self:AddButton(notificationsButton) + + return self +end + +function Header:AddButton(button) + table.insert(self.buttons, button) + button.rbx.Parent = self.rbx.Header.Content.Buttons + button.rbx.LayoutOrder = #self.buttons +end + +return Header \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/PaddedImageButton.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/PaddedImageButton.lua new file mode 100644 index 0000000..9c368d6 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/PaddedImageButton.lua @@ -0,0 +1,52 @@ + +local CoreGui = game:GetService("CoreGui") + +local Common = CoreGui.RobloxGui.Modules.Common +local Mobile = CoreGui.RobloxGui.Modules.Mobile + +local Create = require(Mobile.Create) +local Signal = require(Common.Signal) + +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local GetButtonClickEvent = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonClickEvent) + +local PaddedImageButton = {} +PaddedImageButton.__index = PaddedImageButton + +function PaddedImageButton.new(name, imageUrl, size) + local self = {} + setmetatable(self, PaddedImageButton) + + size = size or 24 + + self.rbx = Create.new "ImageButton" { + Name = name, + Size = UDim2.new(0, 40, 0, 40), + BackgroundTransparency = 1, + + Create.new "ImageLabel" { + Name = "ImageLabel", + Size = UDim2.new(0, size, 0, size), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundTransparency = 1, + Image = imageUrl, + ZIndex = 10, + }, + } + + self.Pressed = Signal.new() + + GetButtonClickEvent(self.rbx):Connect(function() + self.Pressed:Fire() + end) + + return self +end + + +function PaddedImageButton:SetVisible(value) + self.rbx.Visible = value +end + +return PaddedImageButton \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/PageManagerBase.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/PageManagerBase.lua new file mode 100644 index 0000000..59dd5d0 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/PageManagerBase.lua @@ -0,0 +1,211 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) + +local Strings = require(Modules.LuaApp.Legacy.AvatarEditor.LocalizedStrings) +local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local Urls = require(Modules.LuaApp.Legacy.AvatarEditor.Urls) + +local SetAvatarHeight = require(Modules.LuaApp.Actions.SetAvatarHeight) +local SetAvatarWidth = require(Modules.LuaApp.Actions.SetAvatarWidth) +local SetAvatarHeadSize = require(Modules.LuaApp.Actions.SetAvatarHeadSize) +local SetAvatarBodyType = require(Modules.LuaApp.Actions.SetAvatarBodyType) +local SetAvatarProportion = require(Modules.LuaApp.Actions.SetAvatarProportion) + +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) +local AvatarEditorAnthroSliders = Flags:GetFlag("AvatarEditorAnthroSlidersUIOnly") + + +local SKIN_COLORS = { + 'Dark taupe','Brown','Linen','Nougat','Light orange', + 'Dirt brown','Reddish brown','Cork','Burlap','Brick yellow', + 'Sand red','Dusty Rose','Medium red','Pastel orange','Carnation pink', + 'Sand blue','Steel blue','Pastel Blue','Pastel violet','Lilac', + 'Bright bluish green','Shamrock','Moss','Medium green','Br. yellowish orange', + 'Bright yellow','Daisy orange','Dark stone grey','Mid grey','Institutional white', +} + + +local function createPageManagerBase(characterManager) + local this = {} + local ScalesInfo = { + { + StoreName = "Height", + WebPropertyName = "height", + Title = Strings:LocalizedString("ScaleHeightTitle"), + Min = 0.95, + Max = 1.05, + Default = 1, + Increment = .01, + SetScale = function(scale) + AppState.Store:dispatch( SetAvatarHeight(scale) ) + end, + }, + { + StoreName = "Width", + WebPropertyName = "width", + Title = Strings:LocalizedString("ScaleWidthTitle"), + Min = 0.70, + Max = 1.00, + Default = 1.0, + Increment = .01, + SetScale = function(scale) + AppState.Store:dispatch( SetAvatarWidth(scale, 0.5 * scale + 0.5) ) + end, + }, + { + StoreName = "Head", + WebPropertyName = "head", + Title = Strings:LocalizedString("ScaleHeadTitle"), + Min = 0.95, + Max = 1.00, + Default = 1, + Increment = .01, + SetScale = function(scale) + AppState.Store:dispatch( SetAvatarHeadSize(scale) ) + end, + }, + } + + local AnthroEnabledScalesInfo = { + { + StoreName = "Height", + WebPropertyName = "height", + Title = Strings:LocalizedString("ScaleHeightTitle"), + Min = 0.95, + Max = 1.05, + Default = 1, + Increment = .01, + SetScale = function(scale) + AppState.Store:dispatch( SetAvatarHeight(scale) ) + end, + }, + { + StoreName = "Width", + WebPropertyName = "width", + Title = Strings:LocalizedString("ScaleWidthTitle"), + Min = 0.70, + Max = 1.00, + Default = 1.0, + Increment = .01, + SetScale = function(scale) + AppState.Store:dispatch( SetAvatarWidth(scale, 0.5 * scale + 0.5) ) + end, + }, + { + StoreName = "Head", + WebPropertyName = "head", + Title = Strings:LocalizedString("ScaleHeadTitle"), + Min = 0.95, + Max = 1.00, + Default = 1, + Increment = .01, + SetScale = function(scale) + AppState.Store:dispatch( SetAvatarHeadSize(scale) ) + end, + }, + { + StoreName = "BodyType", + WebPropertyName = "bodyType", + Title = Strings:LocalizedString("ScaleBodyTypeTitle"), + Min = 0.00, + Max = 0.30, + Default = 0.00, + Increment = 0.01, + SetScale = function(scale) + AppState.Store:dispatch( SetAvatarBodyType(scale) ) + end, + }, + { + StoreName = "Proportion", + WebPropertyName = "proportion", + Title = Strings:LocalizedString("ScaleProportionTitle"), + Min = 0.00, + Max = 1.00, + Default = 0.0, + Increment = 0.01, + SetScale = function(scale) + AppState.Store:dispatch( SetAvatarProportion(scale) ) + end, + } + } + + local skinColorList = {} + for i, v in pairs(SKIN_COLORS) do + skinColorList[i] = BrickColor.new(v) + end + + local function updateAvatarRules() + local avatarRulesRequest = Utilities.httpGet(Urls.avatarUrlPrefix.."/v1/avatar-rules") + avatarRulesRequest = Utilities.decodeJSON(avatarRulesRequest) + if avatarRulesRequest then + if AvatarEditorAnthroSliders and avatarRulesRequest['proportionsAndBodyTypeEnabledForUser'] then + ScalesInfo = AnthroEnabledScalesInfo + end + + local scaleRulesRequest = avatarRulesRequest['scales'] + if scaleRulesRequest then + for _, info in pairs(ScalesInfo) do + local rules = scaleRulesRequest[info.WebPropertyName] + if rules then + info.Min = rules['min'] or info.Min + info.Max = rules['max'] or info.Max + if rules['increment'] and rules['increment'] ~= 0 then + info.Increment = rules['increment'] + end + end + end + end + + local minDeltaEBodyColorDifference = avatarRulesRequest['minimumDeltaEBodyColorDifference'] + if minDeltaEBodyColorDifference then + characterManager.setMinDeltaEBodyColorDifference(minDeltaEBodyColorDifference) + end + end + end + + Utilities.fastSpawn(updateAvatarRules) + + function this:getScalesInfoCount() + return #ScalesInfo + end + + function this:getSkinColorList() + return skinColorList + end + + --Return bodyColor value if all parts are using the same color, otherwise return nil + function this:getSameBodyColor() + local bodyColors = AppState.Store:getState().Character.BodyColors + local bodyColor = nil + for _, value in pairs(bodyColors) do + if bodyColor == nil then + bodyColor = value + elseif bodyColor ~= value then + return nil + end + end + return bodyColor + end + + function this:makeSlider(Slider, index, scrollingFrame) + local info = ScalesInfo[index] + local scales = AppState.Store:getState().Character.Scales + + return Slider.renderSlider( + info.StoreName, + info.Title, + function(_, value) + info.SetScale( math.min(info.Max, math.max(info.Min, info.Min + value * info.Increment)) ) + end, + (scales[info.StoreName] - info.Min) / (info.Max - info.Min), + ((info.Max - info.Min) / info.Increment) + 1, + (info.Default - info.Min) / info.Increment, + scrollingFrame + ) + end + + return this +end + +return createPageManagerBase diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/PageManagerConsole.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/PageManagerConsole.lua new file mode 100644 index 0000000..21676dc --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/PageManagerConsole.lua @@ -0,0 +1,665 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) + +-------------- FFLAGS -------------- +local AvatarEditorUsesBrowserWindowCall = Flags:GetFlag("AvatarEditorUsesBrowserWindowCall") + +-------------- SERVICES -------------- +local GuiService = game:GetService("GuiService") + +------------ MODULES ------------------- +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) + +local PageManagerBase = require(Modules.LuaApp.Legacy.AvatarEditor.PageManagerBase) +local CreateCardGrid = require(Modules.LuaApp.Legacy.AvatarEditor.CardGridConsole) +local Slider = require(Modules.LuaApp.Legacy.AvatarEditor.SliderConsole) + +local AssetInfo = require(Modules.LuaApp.Legacy.AvatarEditor.AssetInfo) +local AssetTypeNames = require(Modules.LuaApp.Legacy.AvatarEditor.AssetTypeNames) +local Categories = require(Modules.LuaApp.Legacy.AvatarEditor.Categories) +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfoConsole) +local Strings = require(Modules.LuaApp.Legacy.AvatarEditor.LocalizedStrings) +local TweenController = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController) +local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local Urls = require(Modules.LuaApp.Legacy.AvatarEditor.Urls) + +local SetBodyColors = require(Modules.LuaApp.Actions.SetBodyColors) + +local ShellModules = Modules:FindFirstChild("Shell") +local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local GetButtonClickEvent = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonClickEvent) + +-------------- CONSTANTS -------------- +local BUTTONS_PER_ROW = LayoutInfo.ButtonsPerRow +local GRID_PADDING = LayoutInfo.GridPadding +local BUTTONS_SIZE = LayoutInfo.ButtonSize +local SKIN_COLORS_PER_ROW = LayoutInfo.SkinColorsPerRow +local SKIN_COLOR_ROWS_PER_PAGE = LayoutInfo.SkinColorRowsPerPage +local SKIN_COLOR_GRID_PADDING = LayoutInfo.SkinColorGridPadding +local SKIN_COLOR_EXTRA_VERTICAL_SHIFT = LayoutInfo.SkinColorExtraVerticalShift +local SKIN_COLOR_BUTTONS_SIZE = LayoutInfo.SkinColorButtonSize +local SELECTOR_BOTTOM_MIN_DISTANCE = LayoutInfo.SelectorBottomMinDistance + +local ITEMS_PER_PAGE = 24 -- Number of items per http request for infinite scrolling +local ITEMS_PER_PAGE_NEW_URL = 25 + +----------- CLASS DECLARATION -------------- +local function createPageManager(userId, scrollingFrame, characterManager) + local this = PageManagerBase(characterManager) + + local currentPage = nil + local colorButtons = {} + local colorButtonIndex = 0 + local scaleButtons = {} + local loadingContent = false + local loadingSpinner = nil + local loadingSpinnerFrame = nil + local reachedBottomOfCurrentPage = false + local renderedRecommended = false + local recommendedDebounceCounter = 0 + local renderedNoAssetsMessage = false + local currentLoadingContentCall = 0 + local isAssetInList = {} + local nextCursor = '' + local cachedPages = {} + local assetList = {} -- all owned assets of a type + + local storeChangedCn = nil + + local skinColorList = this:getSkinColorList() + + local CardGrid = CreateCardGrid( + scrollingFrame, + function() + return assetList + end, + characterManager + ) + + local function loadPage(assetTypeId, cursor) + if cachedPages[assetTypeId] then + if cachedPages[assetTypeId][cursor] then + return cachedPages[assetTypeId][cursor] + end + else + cachedPages[assetTypeId] = {} + end + + -- This prevents a previous recommended sort from loading over this page + recommendedDebounceCounter = recommendedDebounceCounter + 1 + + local pageInfo = { + assets = {}, + reachedBottom = false, + nextCursor = '', + } + + if loadingSpinner then + loadingSpinner:Cleanup() + loadingSpinner = nil + end + + loadingSpinnerFrame = loadingSpinnerFrame or + Utilities.create'Frame' + { + Name = 'loadingSpinnerFrame'; + AnchorPoint = scrollingFrame.AnchorPoint; + Position = scrollingFrame.Position; + Size = scrollingFrame.Size; + BackgroundTransparency = 1; + BorderSizePixel = 0; + ZIndex = LayoutInfo.BasicLayer; + Parent = scrollingFrame.Parent; + } + + loadingSpinner = LoadingWidget( + {Parent = loadingSpinnerFrame, Position = UDim2.new(0.5, 0, 0.5, -SELECTOR_BOTTOM_MIN_DISTANCE/2)}, + {function() + local typeStuff = Utilities.httpGet( + "https://www." + ..Urls.domainUrl + .."/users/inventory/list-json?assetTypeId=" + ..assetTypeId + .."&itemsPerPage=" + ..ITEMS_PER_PAGE_NEW_URL + .."&userId=" + ..userId + .."&cursor=" + ..cursor) + + typeStuff = Utilities.decodeJSON(typeStuff) + + if typeStuff and typeStuff.IsValid and typeStuff.Data and typeStuff.Data.Items then + pageInfo.nextCursor = typeStuff.Data.nextPageCursor + + if pageInfo.nextCursor == nil then + pageInfo.reachedBottom = true + end + + for _, item in next, typeStuff.Data.Items do + if (not item.UserItem or not item.UserItem.IsRentalExpired) then + table.insert(pageInfo.assets, item.Item.AssetId) + + AssetInfo.setCachedAssetInfo(item.Item.AssetId, { + AssetId = item.Item.AssetId, + AssetTypeId = assetTypeId, + Description = item.Item.Description, + Name = item.Item.Name + }) + end + end + end + + cachedPages[assetTypeId][cursor] = pageInfo + end} + ) + + loadingSpinner:AwaitFinished() + + if loadingSpinner then + loadingSpinner:Cleanup() + loadingSpinner = nil + end + + return pageInfo + end + + local function loadMoreListContent() + loadingContent = true + currentLoadingContentCall = currentLoadingContentCall + 1 + local thisLoadingContentCall = currentLoadingContentCall + local reachedBottom = false + + scrollingFrame.ClipsDescendants = true + if currentPage.typeName then + local assetTypeId = AssetTypeNames[currentPage.typeName] + local pageInfo = loadPage(assetTypeId, nextCursor) + reachedBottom = pageInfo.reachedBottom + nextCursor = pageInfo.nextCursor + + if thisLoadingContentCall == currentLoadingContentCall then + for _, assetId in next, pageInfo.assets do + if not isAssetInList[assetId] then + table.insert(assetList, assetId) + isAssetInList[assetId] = true + end + end + end + + elseif currentPage.specialPageType == 'Recent All' then + assetList = Utilities.copyTable(characterManager.getOrCreateRecentAssetList('allAssets')) + + elseif currentPage.specialPageType == 'Recent Clothing' then + assetList = Utilities.copyTable(characterManager.getOrCreateRecentAssetList('clothing')) + + elseif currentPage.specialPageType == 'Recent Body' then + assetList = Utilities.copyTable(characterManager.getOrCreateRecentAssetList('body')) + + elseif currentPage.specialPageType == 'Recent Animation' then + assetList = Utilities.copyTable(characterManager.getOrCreateRecentAssetList('animation')) + + elseif currentPage.specialPageType == 'Recent Outfits' then + local newAssetList = {} + for _, outfitId in pairs(characterManager.getOrCreateRecentAssetList('outfits')) do + table.insert(newAssetList, outfitId) + end + assetList = newAssetList + + elseif currentPage.specialPageType == 'Outfits' then + local desiredPageNumber = math.ceil((#assetList)/ITEMS_PER_PAGE)+1 + local typeStuff = Utilities.httpGet( + Urls.avatarUrlPrefix + .. "/v1/users/" + .. userId + .. "/outfits?page=" + .. desiredPageNumber + .. "&itemsPerPage=" + .. ITEMS_PER_PAGE) + + if thisLoadingContentCall == currentLoadingContentCall then + typeStuff = Utilities.decodeJSON(typeStuff) + if typeStuff and typeStuff['data'] then + local pageData = typeStuff['data'] + local outfitIds = {} + for i,v in pairs(pageData) do + outfitIds[i] = v.id + end + Utilities.addTables(assetList, outfitIds) + if #outfitIds < ITEMS_PER_PAGE then + reachedBottom = true + end + end + end + elseif currentPage.specialPageType == 'Skin Tone' then + local rows = math.ceil(#skinColorList/SKIN_COLORS_PER_ROW) + Utilities.create'ImageLabel' + { + Name = 'SimpleBackgroundTemplate'; + Image = LayoutInfo.ColorPanelImage; + Position = UDim2.new(0, -4, 0, -3); + Size = UDim2.new(0, 490, 0, rows * SKIN_COLOR_BUTTONS_SIZE + (rows+1)*SKIN_COLOR_GRID_PADDING + 8); + BackgroundTransparency = 1; + BorderSizePixel = 0; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(8, 8, 9, 9); + ZIndex = LayoutInfo.BasicLayer; + Parent = scrollingFrame + } + + local equippedFrame = Utilities.create'ImageLabel' + { + Name = 'EquippedFrame'; + Image = LayoutInfo.ColorEquippedImage; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + Size = UDim2.new(1, 10, 1, 10); + BackgroundTransparency = 1; + BorderSizePixel = 0; + ZIndex = LayoutInfo.BorderMaskLayer; + } + + Utilities.create'ImageLabel' + { + Name = 'CheckMark'; + Image = LayoutInfo.CheckMarkImage; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + Size = UDim2.new(0, 32, 0, 32); + BackgroundTransparency = 1; + BorderSizePixel = 0; + ZIndex = LayoutInfo.BorderMaskLayer; + Parent = equippedFrame; + } + + local colorSelector = Utilities.create'ImageLabel' + { + Name = 'Selector'; + Image = LayoutInfo.ColorSelectorImage; + Position = UDim2.new(0, -12, 0, -12); + Size = UDim2.new(1, 24, 1, 24); + BackgroundTransparency = 1; + BorderSizePixel = 0; + ZIndex = LayoutInfo.BorderMaskLayer; + } + + scrollingFrame.CanvasSize = UDim2.new(0, 0, 0, + math.ceil( + #skinColorList/SKIN_COLORS_PER_ROW)*(SKIN_COLOR_BUTTONS_SIZE + SKIN_COLOR_GRID_PADDING) + + SKIN_COLOR_GRID_PADDING + + SKIN_COLOR_EXTRA_VERTICAL_SHIFT) + + colorButtons = {} + colorButtonIndex = 0 + local sameBodyColor = this:getSameBodyColor() + + for i, brickColor in pairs(skinColorList) do + local row = math.ceil(i/SKIN_COLORS_PER_ROW) + local column = ((i-1)%SKIN_COLORS_PER_ROW)+1 + local colorButton = Utilities.create'ImageButton' + { + Name = i < 10 and 'Color0'..i or 'Color'..i; + Position = UDim2.new( + 0, + SKIN_COLOR_GRID_PADDING + (column-1)*(SKIN_COLOR_BUTTONS_SIZE + SKIN_COLOR_GRID_PADDING), + 0, + SKIN_COLOR_GRID_PADDING + (row-1)*(SKIN_COLOR_BUTTONS_SIZE + SKIN_COLOR_GRID_PADDING) + + SKIN_COLOR_EXTRA_VERTICAL_SHIFT + ); + Size = UDim2.new(0, SKIN_COLOR_BUTTONS_SIZE, 0, SKIN_COLOR_BUTTONS_SIZE); + Image = LayoutInfo.ColorDotImage; + ImageColor3 = brickColor.Color; + AutoButtonColor = false; + BackgroundTransparency = 1; + BorderSizePixel = 0; + ZIndex = scrollingFrame.ZIndex + 1; + SelectionImageObject = colorSelector; + Parent = scrollingFrame; + } + + Utilities.create'ImageLabel' + { + Name = 'DropShadow'; + Image = LayoutInfo.ColorDotShadowImage; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 1); + Size = UDim2.new(1, 8, 1, 9); + BackgroundTransparency = 1; + BorderSizePixel = 0; + ZIndex = colorButton.ZIndex - 1; + Parent = colorButton; + } + + table.insert(colorButtons, colorButton) + + local selectFunction = function() + SoundManager:Play('ButtonPress') + equippedFrame.Parent = colorButton + local bodyColors = { + ["HeadColor"] = brickColor.Number, + ["LeftArmColor"] = brickColor.Number, + ["LeftLegColor"] = brickColor.Number, + ["RightArmColor"] = brickColor.Number, + ["RightLegColor"] = brickColor.Number, + ["TorsoColor"] = brickColor.Number, + } + AppState.Store:dispatch(SetBodyColors(bodyColors)) + end + GetButtonClickEvent(colorButton):connect(selectFunction) + colorButton.SelectionGained:connect(function() + colorButtonIndex = i + end) + + if sameBodyColor == brickColor.number then + equippedFrame.Parent = colorButton + end + end + + elseif currentPage.specialPageType == 'Scale' then + scrollingFrame.ClipsDescendants = false + scaleButtons = {} + + local scalesCount = this:getScalesInfoCount() + local scrollingFrameSizeY = scalesCount * LayoutInfo.SliderVeritcalOffset + LayoutInfo.SelectorBottomMinDistance + scrollingFrame.CanvasSize = UDim2.new(0, 0, 0, scrollingFrameSizeY) + + local sliderPositionY = LayoutInfo.SliderPositionY + local sliderSize = LayoutInfo.ScaleSliderSize + + for i = 1, scalesCount do + local slider = this:makeSlider(Slider, i, scrollingFrame) + slider.Position = UDim2.new(0, 0, 0, sliderPositionY) + slider.Size = sliderSize + slider.Parent = scrollingFrame + local draggerButton = slider.Dragger.DraggerButton + table.insert(scaleButtons, draggerButton) + + sliderPositionY = sliderPositionY + LayoutInfo.SliderVeritcalOffset + end + end + + -- This is for rendering a generically layed out page + if thisLoadingContentCall == currentLoadingContentCall then + local noAssetsLabel = nil + if not currentPage.special then + scrollingFrame.CanvasSize = + UDim2.new(0, 0, 0, + math.floor((#assetList - 1) / BUTTONS_PER_ROW) * (BUTTONS_SIZE + GRID_PADDING) + + BUTTONS_SIZE + + SELECTOR_BOTTOM_MIN_DISTANCE) + + if #assetList == 0 and + (reachedBottom or (currentPage.specialPageType and string.find(currentPage.specialPageType, 'Recent'))) and + not renderedNoAssetsMessage then + + renderedNoAssetsMessage = true + + noAssetsLabel = Utilities.create'TextLabel' + { + Name = "NoAssetsLabel"; + Position = UDim2.new(0, 0, 0, 0); + Size = UDim2.new(1, 0, 1, -SELECTOR_BOTTOM_MIN_DISTANCE); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Font = LayoutInfo.RegularFont; + TextSize = LayoutInfo.ButtonFontSize; + TextColor3 = LayoutInfo.WhiteTextColor; + TextXAlignment = Enum.TextXAlignment.Center; + TextYAlignment = Enum.TextYAlignment.Center; + TextWrapped = true; + ZIndex = 3; + Parent = scrollingFrame; + } + + scrollingFrame.CanvasSize = UDim2.new(0,scrollingFrame.AbsoluteSize.X,0,scrollingFrame.AbsoluteSize.Y) + + if currentPage.specialPageType and string.find(currentPage.specialPageType, 'Recent') then + noAssetsLabel.Text = string.format( + Strings:LocalizedString("NoAssetsPhrase"), + Strings:LocalizedString("RecentItemsWord")) + else + noAssetsLabel.Text = string.format(Strings:LocalizedString("NoAssetsPhrase"), currentPage.title) + end + end + end + + + Utilities.fastSpawn(function() + if reachedBottom + and not renderedRecommended + and currentPage.recommendedSort + and AvatarEditorUsesBrowserWindowCall + and currentPage.typeName + and thisLoadingContentCall == currentLoadingContentCall then + + -- Render recommended section + recommendedDebounceCounter = recommendedDebounceCounter + 1 + local thisRecommendedDebounceCounter = recommendedDebounceCounter + renderedRecommended = true + + local recommendedLabel = Utilities.create'TextLabel' + { + Name = "recommendedTextLabel"; + Size = UDim2.new(1, 0, 0, 24); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Font = LayoutInfo.RegularFont; + Text = Strings:LocalizedString("RecommendedWord"); + TextSize = LayoutInfo.SubHeaderFontSize; + TextColor3 = LayoutInfo.WhiteTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Center; + ZIndex = LayoutInfo.AssetImageLayer; + } + + local recommendedAssetListRequest = + Utilities.httpGet( + "https://" + ..Urls.domainUrl + .."/assets/recommended-json?assetTypeId=" + ..AssetTypeNames[currentPage.typeName] + .."&numItems=4") + + recommendedAssetListRequest = Utilities.decodeJSON(recommendedAssetListRequest) + if recommendedAssetListRequest + and recommendedAssetListRequest.data + and recommendedAssetListRequest.data.Items + and thisRecommendedDebounceCounter == recommendedDebounceCounter + and thisLoadingContentCall == currentLoadingContentCall then + + local currRow = (#assetList == 0) and 1 or math.ceil(#assetList / BUTTONS_PER_ROW) + local adjustedStartIndex = currRow * BUTTONS_PER_ROW + local recommendedCount = 0 + local recommendedYOffset = 40 + for i, itemData in ipairs(recommendedAssetListRequest.data.Items) do + if itemData and itemData.Item then + recommendedCount = recommendedCount + 1 + -- Create card for recommended item + local assetId = itemData.Item.AssetId + + local index = adjustedStartIndex + i + local cardName = tostring(assetId) + local cardImage = Urls.assetImageUrl..tostring(assetId) + + local clickFunction = function() + end + + local card = CardGrid:makeAssetCard( + index, + cardName, + cardImage, + clickFunction, + false, + true, + recommendedYOffset) + + card.Parent = scrollingFrame + end + end + + if recommendedCount > 0 then + if noAssetsLabel then + noAssetsLabel.Size = UDim2.new(1, 0, 0, BUTTONS_SIZE) + end + recommendedLabel.Position = UDim2.new(0, 0, 0, currRow * (BUTTONS_SIZE + GRID_PADDING) + 4) + recommendedLabel.Parent = scrollingFrame + scrollingFrame.CanvasSize = UDim2.new(0, 0, 0, + math.floor((adjustedStartIndex + recommendedCount - 1) / BUTTONS_PER_ROW) * (BUTTONS_SIZE + GRID_PADDING) + + BUTTONS_SIZE + + SELECTOR_BOTTOM_MIN_DISTANCE + + recommendedYOffset) + end + end + end + end) + end + + loadingContent = false + + return reachedBottom + end + + local function selectPage(categoryIndex, tabIndex) + local desiredPage = Categories[categoryIndex].pages[tabIndex] + if desiredPage ~= currentPage then + currentPage = desiredPage + + CardGrid:invalidateAllAssetCards() + scrollingFrame.CanvasPosition = Vector2.new(0, 0) + + if desiredPage.typeName then + loadPage(AssetTypeNames[desiredPage.typeName], '') + end + + -- Check again in case CategoryIndex or TabIndex has already been changed + if currentPage ~= desiredPage then + return + end + + loadingContent = false + renderedRecommended = false + renderedNoAssetsMessage = false + reachedBottomOfCurrentPage = false + assetList = {} + isAssetInList = {} + nextCursor = '' + + reachedBottomOfCurrentPage = loadMoreListContent() + + CardGrid:freshUpdateVisibleAssetCards() + end + end + + local function tweenPage(isFullview) + local tweenInfo = TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + + if isFullview then + TweenController(scrollingFrame, tweenInfo, { Position = UDim2.new(1, 600, 0, 0) }) + else + TweenController(scrollingFrame, tweenInfo, { Position = UDim2.new(1, -99, 0, 0) }) + end + end + + local function update(newState, oldState) + if newState.FullView ~= oldState.FullView then + tweenPage(newState.FullView) + end + + -- Either CategoryIndex changed (by selecting new category) or TabsInfo changed (by selecting new tab) + if newState.Category.CategoryIndex ~= oldState.Category.CategoryIndex or + newState.Category.TabsInfo ~= oldState.Category.TabsInfo then + + local categoryIndex = newState.Category.CategoryIndex + if categoryIndex then + local tabInfo = newState.Category.TabsInfo[categoryIndex] + local tabIndex = tabInfo and tabInfo.TabIndex or 1 + + Utilities.fastSpawn(function() + selectPage(categoryIndex, tabIndex) + end) + end + end + end + + function this:updateListContent(canvasPositionY) + -- -100 pixels to give us a buffer to load more content so the user doesn't actually reach the end of the page + local currBottomY = canvasPositionY + scrollingFrame.AbsoluteWindowSize.Y + local scrollingFrameBottomY = scrollingFrame.CanvasSize.Y.Offset + local loadingBufferY = SELECTOR_BOTTOM_MIN_DISTANCE + 2 * (BUTTONS_SIZE + GRID_PADDING) + if currBottomY + loadingBufferY >= scrollingFrameBottomY then + if not loadingContent + and not renderedRecommended + and currentPage.infiniteScrolling + and not reachedBottomOfCurrentPage then + + reachedBottomOfCurrentPage = loadMoreListContent() + CardGrid:tryUpdateVisibleAssetCards() + end + end + end + + function this:hasAssets() + return currentPage.special or (CardGrid and CardGrid:getFirstCard()) + end + + function this:focusOnCard() + if CardGrid and CardGrid:getFirstCard() then + GuiService.SelectedCoreObject = CardGrid:getFirstCard() + elseif currentPage.specialPageType == 'Skin Tone' then + GuiService.SelectedCoreObject = colorButtons[1] + elseif currentPage.specialPageType == 'Scale' then + GuiService.SelectedCoreObject = scaleButtons[1] + end + end + + function this:SelectNextPage() + if CardGrid and CardGrid:getFirstCard() then + CardGrid:SelectNextPage() + elseif currentPage.specialPageType == 'Skin Tone' then + if colorButtonIndex <= 0 then return end + local nextIndex = colorButtonIndex + SKIN_COLORS_PER_ROW * SKIN_COLOR_ROWS_PER_PAGE + if nextIndex > #colorButtons then + local rows = math.floor(#colorButtons / SKIN_COLORS_PER_ROW) + local remainderColumns = #colorButtons % SKIN_COLORS_PER_ROW + local selectedColumn = colorButtonIndex % SKIN_COLORS_PER_ROW + rows = (selectedColumn > remainderColumns) and (rows - 1) or rows + nextIndex = rows * SKIN_COLORS_PER_ROW + selectedColumn + end + GuiService.SelectedCoreObject = colorButtons[nextIndex] + elseif currentPage.specialPageType == 'Scale' then + GuiService.SelectedCoreObject = scaleButtons[#scaleButtons] + end + end + + function this:SelectPreviousPage() + if CardGrid and CardGrid:getFirstCard() then + CardGrid:SelectPreviousPage() + elseif currentPage.specialPageType == 'Skin Tone' then + if colorButtonIndex <= 0 then return end + local prevIndex = colorButtonIndex - SKIN_COLORS_PER_ROW * SKIN_COLOR_ROWS_PER_PAGE + if prevIndex < 1 then + local selectedColumn = (colorButtonIndex - 1) % SKIN_COLORS_PER_ROW + 1 + prevIndex = selectedColumn + end + GuiService.SelectedCoreObject = colorButtons[prevIndex] + elseif currentPage.specialPageType == 'Scale' then + GuiService.SelectedCoreObject = scaleButtons[1] + end + end + + function this:Focus() + storeChangedCn = AppState.Store.Changed:Connect(update) + CardGrid:Focus() + GuiService:AddSelectionParent("ScrollingFrame", scrollingFrame) + end + + function this:RemoveFocus() + storeChangedCn = Utilities.disconnectEvent(storeChangedCn) + CardGrid:RemoveFocus() + GuiService:RemoveSelectionGroup("ScrollingFrame") + end + + return this +end + +return createPageManager diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/PageManagerMobile.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/PageManagerMobile.lua new file mode 100644 index 0000000..18a3285 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/PageManagerMobile.lua @@ -0,0 +1,785 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) + +-------------- FFLAGS -------------- +local AvatarEditorCatalogRecommended = Flags:GetFlag("AvatarEditorCatalogRecommended") +local AvatarEditorAnthroSliders = Flags:GetFlag("AvatarEditorAnthroSlidersUIOnly") + +local AvatarEditorSliderUIAdjustments = + Flags:GetFlag("AvatarEditorSliderUIAdjustments") + +-------------- SERVICES -------------- +local UserInputService = game:GetService('UserInputService') +local GuiService = game:GetService("GuiService") + +------------ MODULES ------------------- +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) + +local PageManagerBase = require(Modules.LuaApp.Legacy.AvatarEditor.PageManagerBase) +local SpriteManager = require(Modules.LuaApp.Legacy.AvatarEditor.SpriteSheetManager) +local CardGrid = require(Modules.LuaApp.Legacy.AvatarEditor.CardGrid) +local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfo) +local AssetInfo = require(Modules.LuaApp.Legacy.AvatarEditor.AssetInfo) +local Categories = require(Modules.LuaApp.Legacy.AvatarEditor.Categories) +local Strings = require(Modules.LuaApp.Legacy.AvatarEditor.LocalizedStrings) +local AssetTypeNames = require(Modules.LuaApp.Legacy.AvatarEditor.AssetTypeNames) +local Slider = require(Modules.LuaApp.Legacy.AvatarEditor.Slider) +local Urls = require(Modules.LuaApp.Legacy.AvatarEditor.Urls) + +local EquipAsset = require(Modules.LuaApp.Actions.EquipAsset) +local UnequipAsset = require(Modules.LuaApp.Actions.UnequipAsset) +local SetBodyColors = require(Modules.LuaApp.Actions.SetBodyColors) +local GetButtonClickEvent = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonClickEvent) + +-------------- CONSTANTS -------------- +local BUTTONS_PER_ROW = LayoutInfo.ButtonsPerRow +local SKIN_COLORS_PER_ROW = LayoutInfo.SkinColorsPerRow +local GRID_PADDING = LayoutInfo.GridPadding +local SKIN_COLOR_GRID_PADDING = LayoutInfo.SkinColorGridPadding +local EXTRA_VERTICAL_SHIFT = LayoutInfo.ExtraVerticalShift -- This is to make space for page title labels +local SKIN_COLOR_EXTRA_VERTICAL_SHIFT = LayoutInfo.SkinColorExtraVerticalShift + +local ITEMS_PER_PAGE = 24 -- Number of items per http request for infinite scrolling +local ITEMS_PER_PAGE_NEW_URL = 25 + + +return function( + userId, + equippedFrameTemplate, + scrollingFrame, + characterManager, + longPressMenu) + +local currentPage = nil +local loadingContent = false +local renderedRecommended = false +local recommendedDebounceCounter = 0 +local renderedNoAssetsMessage = false +local currentLoadingContentCall = 0 +local isAssetInList = {} +local nextCursor = '' +local cachedPages = {} +local assetList = {} -- all owned assets of a type + +local cardGrid = CardGrid( + equippedFrameTemplate, + scrollingFrame, + function() + return assetList + end +) + +local this = PageManagerBase(characterManager) +local skinColorList = this:getSkinColorList() + + +local function loadPage(assetTypeId, cursor) + + if cachedPages[assetTypeId] then + if cachedPages[assetTypeId][cursor] then + return cachedPages[assetTypeId][cursor] + end + else + cachedPages[assetTypeId] = {} + end + + -- This prevents a previous recommended sort from loading over this page + recommendedDebounceCounter = recommendedDebounceCounter + 1 + + local pageInfo = { + assets = {}, + reachedBottom = false, + nextCursor = '', + totalItems = nil + } + + local typeStuff = Utilities.httpGet( + "https://www." + ..Urls.domainUrl + .."/users/inventory/list-json?assetTypeId=" + ..assetTypeId + .."&itemsPerPage=" + ..ITEMS_PER_PAGE_NEW_URL + .."&userId=" + ..userId + .."&cursor=" + ..cursor) + + typeStuff = Utilities.decodeJSON(typeStuff) + + if typeStuff and typeStuff.IsValid and typeStuff.Data and typeStuff.Data.Items then + pageInfo.nextCursor = typeStuff.Data.nextPageCursor + + if pageInfo.nextCursor == nil then + pageInfo.reachedBottom = true + end + + pageInfo.totalItems = tonumber(typeStuff.Data.TotalItems) + + for _, item in next, typeStuff.Data.Items do + if (not item.UserItem or not item.UserItem.IsRentalExpired) then + table.insert(pageInfo.assets, item.Item.AssetId) + + AssetInfo.setCachedAssetInfo(item.Item.AssetId, { + AssetId = item.Item.AssetId, + AssetTypeId = assetTypeId, + Description = item.Item.Description, + Name = item.Item.Name + }) + end + end + end + + cachedPages[assetTypeId][cursor] = pageInfo + + return pageInfo +end + +function this:renderAssetCardByIndex(i) + local assetId = assetList[i] + local cardName, cardImage, clickFunction, longPressFunction, isSelected + + if currentPage.specialPageType == 'Outfits' or currentPage.specialPageType == 'Recent Outfits' then + cardName = 'Outfit'..tostring(assetId) + + cardImage = function() + return "https://www." + ..Urls.domainUrl + .."/outfit-thumbnail/image?userOutfitId=" + ..assetId + .."&width=100&height=100&format=png" + end + + clickFunction = function() + characterManager.addToRecentAssetList('outfits', assetId) + characterManager.wearOutfit(assetId) + longPressMenu:hideMenu() + end + longPressFunction = clickFunction + else + cardName = tostring(assetId) + cardImage = Urls.assetImageUrl..tostring(assetId) + isSelected = characterManager.findIfEquipped(assetId) + + local wearFunction = function() + AppState.Store:dispatch(EquipAsset(AssetInfo.getAssetType(assetId), assetId)) + longPressMenu:hideMenu() + end + + local takeOffFunction = function() + AppState.Store:dispatch(UnequipAsset(AssetInfo.getAssetType(assetId), assetId)) + longPressMenu:hideMenu() + end + + local wearOrTakeOffFunction = function() + if characterManager.findIfEquipped(assetId) then + takeOffFunction() + else + wearFunction() + end + end + + longPressFunction = function() + local wearOrUnwearOption + if characterManager.findIfEquipped(assetId) then + wearOrUnwearOption = {text = Strings:LocalizedString("TakeOffWord"), func = takeOffFunction} + else + wearOrUnwearOption = {text = Strings:LocalizedString("WearWord"), func = wearFunction} + end + longPressMenu:showMenu(wearOrUnwearOption, assetId) + end + + clickFunction = function() + if UserInputService:IsKeyDown(Enum.KeyCode.Q) then + longPressFunction() + else + wearOrTakeOffFunction() + end + end + end + + local card = cardGrid:makeAssetCard(i, cardName, cardImage, clickFunction, longPressFunction, isSelected) + card.Parent = scrollingFrame +end + +local function makeShopPressFunction(url) + return function() + local url = "https://www." .. Urls.domainUrl .. (url or "/catalog") + GuiService:OpenNativeOverlay( "Catalog", url ) + end +end + +local function loadMoreListContent() + loadingContent = true + currentLoadingContentCall = currentLoadingContentCall + 1 + local thisLoadingContentCall = currentLoadingContentCall + local reachedBottom = false + local totalItems = nil + + if currentPage.typeName then + local assetTypeId = AssetTypeNames[currentPage.typeName] + + local pageInfo = loadPage(assetTypeId, nextCursor) + reachedBottom = pageInfo.reachedBottom + nextCursor = pageInfo.nextCursor + totalItems = pageInfo.totalItems + + if thisLoadingContentCall == currentLoadingContentCall then + for _, assetId in next, pageInfo.assets do + if not isAssetInList[assetId] then + table.insert(assetList, assetId) + isAssetInList[assetId] = true + end + end + end + + elseif currentPage.specialPageType == 'Recent All' then + assetList = Utilities.copyTable(characterManager.getOrCreateRecentAssetList('allAssets')) + + elseif currentPage.specialPageType == 'Recent Clothing' then + assetList = Utilities.copyTable(characterManager.getOrCreateRecentAssetList('clothing')) + + elseif currentPage.specialPageType == 'Recent Body' then + assetList = Utilities.copyTable(characterManager.getOrCreateRecentAssetList('body')) + + elseif currentPage.specialPageType == 'Recent Animation' then + assetList = Utilities.copyTable(characterManager.getOrCreateRecentAssetList('animation')) + + elseif currentPage.specialPageType == 'Recent Outfits' then + local newAssetList = {} + for _, outfitId in pairs(characterManager.getOrCreateRecentAssetList('outfits')) do + table.insert(newAssetList, outfitId) + end + assetList = newAssetList + + elseif currentPage.specialPageType == 'Outfits' then + local desiredPageNumber = math.ceil((#assetList)/ITEMS_PER_PAGE)+1 + local typeStuff = Utilities.httpGet( + Urls.avatarUrlPrefix + .. "/v1/users/" + .. userId + .. "/outfits?page=" + .. desiredPageNumber + .. "&itemsPerPage=" + .. ITEMS_PER_PAGE) + + if thisLoadingContentCall == currentLoadingContentCall then + typeStuff = Utilities.decodeJSON(typeStuff) + if typeStuff and typeStuff['data'] then + local pageData = typeStuff['data'] + local outfitIds = {} + for i,v in pairs(pageData) do + outfitIds[i] = v.id + end + Utilities.addTables(assetList, outfitIds) + if #outfitIds < ITEMS_PER_PAGE then + reachedBottom = true + end + end + end + + elseif currentPage.specialPageType == 'Skin Tone' then + if LayoutInfo.isLandscape then + scrollingFrame.Position = UDim2.new(0, 120, 0, 15) + scrollingFrame.Size = UDim2.new(1, -135, 1, 0) + scrollingFrame.ClipsDescendants = false + end + + local availibleWidth = scrollingFrame.AbsoluteSize.X + local buttonSize = (availibleWidth - ((SKIN_COLORS_PER_ROW+1) * SKIN_COLOR_GRID_PADDING)) / SKIN_COLORS_PER_ROW + + if LayoutInfo.isLandscape then + local rows = math.ceil(#skinColorList/SKIN_COLORS_PER_ROW) + + Utilities.create'ImageLabel' + { + Name = 'SimpleBackgroundTemplate'; + Position = UDim2.new(0, -4, 0, -3); + Size = UDim2.new(1, 8, 0, rows*buttonSize + (rows+1)*SKIN_COLOR_GRID_PADDING + 8); + ZIndex = 2; + BackgroundTransparency = 0; + BackgroundColor3 = Color3.new(1,1,1); + BorderSizePixel = 1; + Visible = true; + Parent = scrollingFrame + } + end + + local equippedFrame = Utilities.create'ImageLabel' + { + Name = "EquippedFrame"; + Position = UDim2.new(-.1, 0, -.1, 0); + Size = UDim2.new(1.2, 0, 1.2, 0); + BackgroundTransparency = 1; + SizeConstraint = 'RelativeYY'; + ZIndex = scrollingFrame.ZIndex + 2; + } + SpriteManager.equip(equippedFrame, "gr-ring-selector") + + scrollingFrame.CanvasSize = UDim2.new(0, 0, 0, + math.ceil(#skinColorList/SKIN_COLORS_PER_ROW)*(buttonSize+SKIN_COLOR_GRID_PADDING) + + SKIN_COLOR_GRID_PADDING + + SKIN_COLOR_EXTRA_VERTICAL_SHIFT) + + local sameBodyColor = this:getSameBodyColor() + for i, brickColor in pairs(skinColorList) do + local row = math.ceil(i/SKIN_COLORS_PER_ROW) + local column = ((i-1)%SKIN_COLORS_PER_ROW)+1 + local colorButton = Utilities.create'ImageButton' + { + Position = UDim2.new( + 0, + SKIN_COLOR_GRID_PADDING + (column-1)*(buttonSize+SKIN_COLOR_GRID_PADDING), + 0, + SKIN_COLOR_GRID_PADDING + (row-1)*(buttonSize+SKIN_COLOR_GRID_PADDING) + SKIN_COLOR_EXTRA_VERTICAL_SHIFT + ); + Size = UDim2.new(0, buttonSize, 0, buttonSize); + BackgroundTransparency = 1; + BorderSizePixel = 0; + ImageColor3 = brickColor.Color; + AutoButtonColor = false; + ZIndex = scrollingFrame.ZIndex + 1; + Parent = scrollingFrame; + } + SpriteManager.equip(colorButton, "gr-circle-white") + + local selectFunction = function() + equippedFrame.Parent = colorButton + local bodyColors = { + ["HeadColor"] = brickColor.Number, + ["LeftArmColor"] = brickColor.Number, + ["LeftLegColor"] = brickColor.Number, + ["RightArmColor"] = brickColor.Number, + ["RightLegColor"] = brickColor.Number, + ["TorsoColor"] = brickColor.Number, + } + AppState.Store:dispatch(SetBodyColors(bodyColors)) + end + GetButtonClickEvent(colorButton):connect(selectFunction) + + if LayoutInfo.isLandscape then + local _, s, v = Color3.toHSV(brickColor.Color) + + if s < 0.1 and v > 0.9 then + local outline = Utilities.create'ImageLabel' + { + Name = 'Outline'; + Position = UDim2.new(0, -1, 0, -1); + Size = UDim2.new(1, 2, 1, 2); + BackgroundTransparency = 1; + ImageColor3 = Color3.new(0, 0, 0); + ImageTransparency = 0.75; + ZIndex = colorButton.ZIndex - 1; + Parent = colorButton; + } + SpriteManager.equip(outline, 'gr-circle-white') + end + else + local colorButtonShadow = Utilities.create'ImageLabel' + { + Name = 'DropShadow'; + Position = UDim2.new(-.07, 0, -.07, 0); + Size = UDim2.new(1.13, 0, 1.13, 0); + BackgroundTransparency = 1; + ZIndex = colorButton.ZIndex - 1; + Parent = colorButton; + } + SpriteManager.equip(colorButtonShadow, "gr-circle-shadow") + end + + if sameBodyColor == brickColor.number then + equippedFrame.Parent = colorButton + end + end + + elseif currentPage.specialPageType == 'Scale' then + scrollingFrame.CanvasSize = UDim2.new(0, 0, 0, 260) + + if LayoutInfo.isLandscape then + scrollingFrame.ClipsDescendants = false + scrollingFrame.Position = UDim2.new(0, 116, 0, -1) + scrollingFrame.Size = UDim2.new(1, -127, 1, 0) + end + + local background = Utilities.create'ImageLabel' + { + Name = 'Background'; + Position = UDim2.new(0, 3, 0, EXTRA_VERTICAL_SHIFT + 5); + ZIndex = 2; + Visible = true; + BorderSizePixel = 1; + BackgroundColor3 = Color3.new(1,1,1); + + Parent = scrollingFrame; + } + + if AvatarEditorSliderUIAdjustments then + background.BorderSizePixel = 0; + end + + local scalesCount = this:getScalesInfoCount() + local sliderPositionY = LayoutInfo.SliderPositionY + local sliderSize = LayoutInfo.ScaleSliderSize + + for i = 1, scalesCount do + local slider +if AvatarEditorAnthroSliders then + slider = this:makeSlider(Slider, i, scrollingFrame) +else + slider = this:makeSlider(Slider, i, (i > 1) and scrollingFrame or nil) +end + slider.Position = LayoutInfo.isLandscape + and UDim2.new(0, 29, 0, sliderPositionY) + or UDim2.new(.1, 0, 0, sliderPositionY) + slider.Size = sliderSize + slider.Parent = scrollingFrame + sliderPositionY = sliderPositionY + LayoutInfo.SliderVeritcalOffset + end + + background.Size = UDim2.new(1, -6, 0, sliderPositionY); + + scrollingFrame.CanvasSize = + UDim2.new( 0, 0, 0, sliderPositionY + EXTRA_VERTICAL_SHIFT + 8) + end + + -- This is for rendering a generically layed out page + if thisLoadingContentCall == currentLoadingContentCall then + local buttonSize = cardGrid:getButtonSize() + + if not currentPage.special then + scrollingFrame.CanvasSize = + UDim2.new(0, 0, 0, + cardGrid:getAssetCardY(totalItems or #assetList) + + buttonSize + + (LayoutInfo.isLandscape and 15 or GRID_PADDING)) + + cardGrid:setRenderAssetCardCallback( function(...) return this:renderAssetCardByIndex(...) end ) + + -- No assets text + if #assetList == 0 and + (reachedBottom or (currentPage.specialPageType and string.find(currentPage.specialPageType, 'Recent'))) + and not renderedNoAssetsMessage then + + renderedNoAssetsMessage = true + Utilities.create'TextLabel' + { + Name = 'TextLabel'; + Position = UDim2.new(.5, 0, .5, -15); + Size = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Font = LayoutInfo.BackgroundTextFont; + FontSize = "Size18"; + Text = string.format(Strings:LocalizedString("NoAssetsPhrase"), currentPage.title); + TextColor3 = LayoutInfo.BackgroundTextColor; + TextXAlignment = "Center"; + ZIndex = 3; + Parent = scrollingFrame; + } + +if AvatarEditorCatalogRecommended then + if currentPage.shopUrl then + -- Create "shop in catalog" button + local shopInCatalogButton = Utilities.create'ImageButton' + { + Name = 'ShopInCatalogButton'; + AnchorPoint = Vector2.new(.5, 0); + Position = UDim2.new(.5, 0, .5, 0); + Size = UDim2.new(0, 160, 0, 36); + ZIndex = 5; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = 'rbxasset://textures/AvatarEditorImages/btn.png'; + ImageRectOffset = Vector2.new(0, 0); + ImageRectSize = Vector2.new(0, 0); + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(3, 3, 4, 4); + Visible = true; + Parent = scrollingFrame; + } + Utilities.create'TextLabel' + { + Name = 'TextLabel'; + Position = UDim2.new(0, 0, 0, -1); + Size = UDim2.new(1, 0, 1, 0); + ZIndex = 5; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Font = Enum.Font.SourceSans; + Text = 'Shop in Catalog'; + TextSize = 22; + TextColor3 = Color3.new(1,1,1); + TextScaled = false; + TextStrokeTransparency = 1; + Parent = shopInCatalogButton + } + + local pressFunction = makeShopPressFunction( currentPage.shopUrl ) + GetButtonClickEvent(shopInCatalogButton):connect( pressFunction ) + end +end + + scrollingFrame.CanvasSize = + UDim2.new(0, scrollingFrame.AbsoluteSize.X, 0, scrollingFrame.AbsoluteSize.Y) + elseif currentPage.typeName and not LayoutInfo.isLandscape then + local pageTitleLabel = scrollingFrame:FindFirstChild('PageTitleLabel') + if pageTitleLabel then + pageTitleLabel.Visible = true + end + end + end + +if AvatarEditorCatalogRecommended then + Utilities.fastSpawn(function() + if reachedBottom + and not renderedRecommended + and currentPage.shopUrl + and currentPage.typeName + and thisLoadingContentCall == currentLoadingContentCall + and not renderedNoAssetsMessage then + + -- Render recommended section + recommendedDebounceCounter = recommendedDebounceCounter + 1 + local thisRecommendedDebounceCounter = recommendedDebounceCounter + renderedRecommended = true + + local recommendedYPosition = scrollingFrame.CanvasSize.Y.Offset + 5 + + local bonusYPixels = 8 + if LayoutInfo.isLandscape then + bonusYPixels = bonusYPixels + 20 + end + + -- Tablet Text is on dark background, Phone text is on white background + Utilities.create'TextLabel' + { + Position = UDim2.new(0, 7, 0, recommendedYPosition - 2); + Size = UDim2.new(1, -14, 0, 25); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Font = LayoutInfo.isLandscape and "SourceSans" or "SourceSansLight"; + FontSize = "Size18"; + Text = string.upper(Strings:LocalizedString("RecommendedWord")); + TextXAlignment = "Left"; + TextColor3 = LayoutInfo.isLandscape and Color3.new(.9, .9, .9) or Color3.fromRGB(65, 78, 89); + ZIndex = 3; + Parent = scrollingFrame; + } + + -- Create "Shop Now" button + local shopNowButton = Utilities.create'ImageButton' + { + Name = 'ShopNowButtonTemplate'; + Position = LayoutInfo.isLandscape and + UDim2.new(1, -100, 0, recommendedYPosition - 3) or + UDim2.new(1, -88, 0, recommendedYPosition - 3); + Size = UDim2.new(0, 85, 0, 26); + ZIndex = 5; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = 'rbxasset://textures/AvatarEditorImages/btn.png'; + ImageRectOffset = Vector2.new(0, 0); + ImageRectSize = Vector2.new(0, 0); + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(3, 3, 4, 4); + Visible = true; + Parent = scrollingFrame; + } + Utilities.create'TextLabel' + { + Name = 'TextLabel'; + Position = UDim2.new(0, 0, 0, -1); + Size = UDim2.new(1, 0, 1, 0); + ZIndex = 5; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Font = Enum.Font.SourceSans; + Text = Strings:LocalizedString("ShopNowWord"); + TextSize = 18; + TextColor3 = Color3.new(1,1,1); + TextScaled = false; + TextStrokeTransparency = 1; + Parent = shopNowButton; + } + + local pressFunction = makeShopPressFunction( currentPage.shopUrl ) + GetButtonClickEvent(shopNowButton):connect( pressFunction ) + + local recommendedAssetListRequest = + Utilities.httpGet( + "https://www." + ..Urls.domainUrl + .."/assets/recommended-json?assetTypeId=" + ..AssetTypeNames[currentPage.typeName] + .."&numItems=4") + + recommendedAssetListRequest = Utilities.decodeJSON(recommendedAssetListRequest) + if recommendedAssetListRequest + and recommendedAssetListRequest.data + and recommendedAssetListRequest.data.Items + and thisRecommendedDebounceCounter == recommendedDebounceCounter + and thisLoadingContentCall == currentLoadingContentCall then + for i, itemData in pairs(recommendedAssetListRequest.data.Items) do + if itemData and itemData.Item then + -- Create card for recommended item + + local assetId = itemData.Item.AssetId + + local position = UDim2.new( + 0, + GRID_PADDING + (i-1)*(buttonSize+GRID_PADDING) -3, + 0, + recommendedYPosition + 30 + ) + local cardName = tostring(assetId) + local cardImage = Urls.assetImageUrl..tostring(assetId) + + local clickFunction = function() + if longPressMenu then + longPressMenu:openDetails(assetId) + end + end + + local card = cardGrid:makeAssetCard( + #assetList + i, + cardName, + cardImage, + clickFunction, + function() end, + false, + position) + + card.Parent = scrollingFrame + end + end + end + + scrollingFrame.CanvasSize = + UDim2.new( + 0, 0, 0, ( + math.ceil(#assetList/BUTTONS_PER_ROW) + 1)*(buttonSize + GRID_PADDING) + + GRID_PADDING + + 2 * EXTRA_VERTICAL_SHIFT + + bonusYPixels) + end + end) +end + end + + loadingContent = false + + return reachedBottom +end + + +local reachedBottomOfCurrentPage = false + +local function selectPage(categoryIndex, tabIndex) + local desiredPage = Categories[categoryIndex].pages[tabIndex] + if desiredPage ~= currentPage then + currentPage = desiredPage + + cardGrid:invalidateAllAssetCards() + scrollingFrame.CanvasPosition = Vector2.new(0, 0) + + if desiredPage.typeName then + loadPage(AssetTypeNames[desiredPage.typeName], '') + end + + -- Check again in case CategoryIndex or TabIndex has already been changed + if currentPage ~= desiredPage then + return + end + + if not LayoutInfo.isLandscape then + Utilities.create'TextLabel' + { + Name = 'PageTitleLabel'; + Position = UDim2.new(0, 7, 0, 3); + Size = UDim2.new(1, -14, 0, 25); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Font = "SourceSansLight"; + FontSize = "Size18"; + Text = string.upper(desiredPage.title); + TextColor3 = Color3.fromRGB(65, 78, 89); + TextXAlignment = "Left"; + ZIndex = 3; + Visible = not desiredPage.typeName; + Parent = scrollingFrame; + } + end + + if desiredPage.name == 'Skin Tone' then + if not LayoutInfo.isLandscape then + Utilities.create'ImageLabel' + { + Name = 'WhiteFrameTemplate'; + Position = UDim2.new(0, 2, 0, 27); + Size = UDim2.new(1, -4, 1, -29); + ZIndex = 3; + BackgroundTransparency = 0; + BorderSizePixel = 0; + BackgroundColor3 = Color3.new(1,1,1); + Visible = true; + Parent = scrollingFrame; + } + end + end + + loadingContent = false + renderedRecommended = false + renderedNoAssetsMessage = false + reachedBottomOfCurrentPage = false + assetList = {} + isAssetInList = {} + nextCursor = '' + + reachedBottomOfCurrentPage = loadMoreListContent() + + cardGrid:freshUpdateVisibleAssetCards() + end +end + + +function this:updateListContent(canvasPosition) + -- -100 pixels to give us a buffer to load more content so the user doesn't actually reach the end of the page + if canvasPosition >= + cardGrid:getAssetCardY(#assetList) + - 100 + - 2 * cardGrid:getButtonSize() + - scrollingFrame.CanvasSize.Y.Offset then + + if not loadingContent + and not renderedRecommended + and currentPage.infiniteScrolling + and not reachedBottomOfCurrentPage then + + reachedBottomOfCurrentPage = loadMoreListContent() + cardGrid:tryUpdateVisibleAssetCards() + end + end +end + +local function update(newState, oldState) + -- Either CategoryIndex changed (by selecting new category) or TabsInfo changed (by selecting new tab) + if newState.Category.CategoryIndex ~= oldState.Category.CategoryIndex or + newState.Category.TabsInfo ~= oldState.Category.TabsInfo then + + local categoryIndex = newState.Category.CategoryIndex + local tabInfo = categoryIndex and newState.Category.TabsInfo[categoryIndex] + local tabIndex = tabInfo and tabInfo.TabIndex or 1 + + Utilities.fastSpawn(function() + selectPage(categoryIndex, tabIndex) + end) + end +end + + + AppState.Store.Changed:Connect(update) + + return this +end + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/ParticleScreen.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/ParticleScreen.lua new file mode 100644 index 0000000..4749dc3 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/ParticleScreen.lua @@ -0,0 +1,82 @@ +local this = {} + +local function getModule(moduleName) + return script.Parent:FindFirstChild(moduleName) +end + +local utilities = require(getModule('Utilities')) + +function this.init() + local particleTransparency1 = NumberSequence.new( { + NumberSequenceKeypoint.new(0, 1, 0), + NumberSequenceKeypoint.new(0.195, 0, 0), + NumberSequenceKeypoint.new(0.783, 0.887, 0), + NumberSequenceKeypoint.new(1, 1, 0) } ) + + local ParticleEmitterContainer = utilities.create'Part' + { + Position = Vector3.new(14.197, 2.55, -18.644); + Reflectance = 0; + Transparency = 1; + Name = 'AvatarEditorParticleScreenPart'; + Orientation = Vector3.new(0, 15, 0); + Size = Vector3.new(4.2, 4.2, 1.8); + Material = 'Plastic'; + Anchored = true; + CanCollide = false; + + Parent = game.Workspace; + } + + local particleEmitter1 = utilities.create'ParticleEmitter' + { + Color = ColorSequence.new(Color3.fromRGB(225, 227, 213)); + LightEmission = 0.35; + LightInfluence = 0; + Size = NumberSequence.new(2); + Texture = 'rbxasset://textures/particles/smoke_main.dds'; + Transparency = particleTransparency1; + Speed = NumberRange.new(4, 8); + Acceleration = Vector3.new(0,30,0); + Lifetime = NumberRange.new(0.3, 0.4); + Rate = 100; + Enabled = false; + + Parent = ParticleEmitterContainer + } + + local particleEmitter2 = utilities.create'ParticleEmitter' + { + Color = ColorSequence.new(Color3.fromRGB(204, 209, 175)); + LightEmission = 0.35; + LightInfluence = 0; + Size = NumberSequence.new(2); + Texture = 'rbxasset://textures/particles/smoke_main.dds'; + Transparency = particleTransparency1; + Speed = NumberRange.new(3, 6); + Acceleration = Vector3.new(0,30,0); + Lifetime = NumberRange.new(0.3, 0.4); + Rate = 100; + Enabled = false; + ZOffset = 1; + + Parent = ParticleEmitterContainer + } + + local particleEmittionCount = 0 + function this.runParticleEmitter() + utilities.fastSpawn(function() + particleEmitter1.Enabled = true + particleEmitter2.Enabled = true + particleEmittionCount = particleEmittionCount + 1 + local thisParticleEmittionCount = particleEmittionCount + wait(.3) + if particleEmittionCount == thisParticleEmittionCount then + particleEmitter1.Enabled = false + particleEmitter2.Enabled = false + end + end) + end +end + +return this diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Slider.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Slider.lua new file mode 100644 index 0000000..0e87728 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Slider.lua @@ -0,0 +1,373 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local userInputService = game:GetService('UserInputService') + +local utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) + +local AvatarEditorHideSliderHighlightOnInit = Flags:GetFlag("AvatarEditorHideSliderHighlightOnInit") +local AvatarEditorSliderUIAdjustments = Flags:GetFlag("AvatarEditorSliderUIAdjustments") + +local GetButtonDownEventConnected = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonDownEventConnected) + +local function makeSlider() + local SliderFrameTemplate = utilities.create'ImageButton' + { + Name = 'SliderFrameTemplate'; + Position = UDim2.new(0.1, 0, 0, 70); + Size = UDim2.new(0.8, 0, 0, 30); + ZIndex = 5; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = ''; + ImageRectOffset = Vector2.new(0, 0); + ImageRectSize = Vector2.new(0, 0); + ScaleType = Enum.ScaleType.Stretch; + SliceCenter = Rect.new(0, 0, 0, 0); + } + + local BackgroundBar = utilities.create'ImageButton' + { + Name = 'BackgroundBar'; + Position = UDim2.new(0, 0, 0.5, -3); + Size = UDim2.new(1, 0, 0, 6); + ZIndex = 3; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = 'rbxasset://textures/AvatarEditorImages/Sheet.png'; + ImageRectOffset = Vector2.new(6, 1); + ImageRectSize = Vector2.new(7, 6); + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(3, 2, 4, 4); + Parent = SliderFrameTemplate; + } + + utilities.create'StringValue' + { + Name = 'SpriteName'; + Value = 'slider bar'; + Parent = BackgroundBar; + } + + local Dragger = utilities.create'ImageButton' + { + Name = 'Dragger'; + Position = UDim2.new(0.5, -16, 0.5, -16); + Size = UDim2.new(0, 32, 0, 32); + ZIndex = 5; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = 'rbxasset://textures/AvatarEditorImages/Sheet.png'; + ImageRectOffset = Vector2.new(1934, 31); + ImageRectSize = Vector2.new(32, 32); + ScaleType = Enum.ScaleType.Stretch; + SliceCenter = Rect.new(0, 0, 0, 0); + Parent = SliderFrameTemplate; + } + + local Highlight = utilities.create'ImageButton' + { + Name = 'Highlight'; + Position = UDim2.new(0.5, -24, 0.5, -24); + Size = UDim2.new(0, 48, 0, 48); + ZIndex = 4; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = 'rbxasset://textures/AvatarEditorImages/Sheet.png'; + ImageRectOffset = Vector2.new(627, 69); + ImageRectSize = Vector2.new(48, 48); + ScaleType = Enum.ScaleType.Stretch; + SliceCenter = Rect.new(0, 0, 0, 0); + Parent = Dragger; + } + + if AvatarEditorHideSliderHighlightOnInit then + Highlight.Visible = false + end + + utilities.create'StringValue' + { + Name = 'SpriteName'; + Value = 'dragger-highlight'; + Parent = Highlight; + } + + utilities.create'StringValue' + { + Name = 'SpriteName'; + Value = 'btn-slider'; + Parent = Dragger; + } + + utilities.create'ImageButton' + { + Name = 'DraggerButton'; + Position = UDim2.new(0.5, 0, 0.5, 0); + Size = UDim2.new(1, 32, 1, 32); + ZIndex = 5; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = ''; + ImageRectOffset = Vector2.new(0, 0); + ImageRectSize = Vector2.new(0, 0); + ScaleType = Enum.ScaleType.Stretch; + SliceCenter = Rect.new(0, 0, 0, 0); + AnchorPoint = Vector2.new(0.5, 0.5); + Parent = Dragger; + } + + local FillBar = utilities.create'ImageButton' + { + Name = 'FillBar'; + Position = UDim2.new(0, -5, 0.5, -6); + Size = UDim2.new(0.5, 0, 0, 14); + ZIndex = 3; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = 'rbxasset://textures/AvatarEditorImages/Sheet.png'; + ImageRectOffset = Vector2.new(134, 1); + ImageRectSize = Vector2.new(15, 14); + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(7, 5, 8, 7); + Parent = SliderFrameTemplate; + } + + utilities.create'StringValue' + { + Name = 'SpriteName'; + Value = 'slider bar-on'; + Parent = FillBar; + } + + utilities.create'ImageButton' + { + Name = 'DefaultLocationIndicator'; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0, 0, 0.5, 0); + Size = UDim2.new(0, 12, 0, 12); + ZIndex = 3; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = 'rbxasset://textures/AvatarEditorImages/circle_gray4.png'; + ScaleType = Enum.ScaleType.Stretch; + Parent = SliderFrameTemplate; + } + +if AvatarEditorSliderUIAdjustments then + utilities.create'TextLabel' + { + Name = 'TextLabel'; + Position = UDim2.new(0, 0, 0.15, -32); + Size = UDim2.new(0, 0, 0, 25); + TextXAlignment = 'Left'; + TextYAlignment = 'Center'; + ZIndex = 3; + BackgroundTransparency = 1; + BorderSizePixel = 1; + Parent = SliderFrameTemplate; + } +else + utilities.create'TextLabel' + { + Name = 'TextLabel'; + Position = UDim2.new(0, 0, 0.5, -40); + Size = UDim2.new(0.5, 0, 0, 25); + ZIndex = 3; + BackgroundTransparency = 1; + BorderSizePixel = 1; + Parent = SliderFrameTemplate; + } +end + + utilities.create'ImageButton' + { + Name = 'SliderButton'; + Position = UDim2.new(0.5, 0, 0.5, 0); + Size = UDim2.new(1, 10, 1, 0); + ZIndex = 5; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = ''; + ImageRectOffset = Vector2.new(0, 0); + ImageRectSize = Vector2.new(0, 0); + ScaleType = Enum.ScaleType.Stretch; + SliceCenter = Rect.new(0, 0, 0, 0); + AnchorPoint = Vector2.new(0.5, 0.5); + Parent = SliderFrameTemplate; + } + + return SliderFrameTemplate +end + +local this = {} + + +function this.renderSlider(name, title, changedFunction, currentPercent, intervals, defaultValue, scrollingFrame) + local slider = makeSlider() + slider.TextLabel.Text = title + slider.Name = 'Slider'..name + + local dragger = slider:WaitForChild('Dragger') + local draggerButton = dragger:WaitForChild('DraggerButton') -- Specific buttons for larger or custom hitboxes. + local sliderButton = slider:WaitForChild('SliderButton') + local lastValue = currentPercent + if intervals then + intervals = intervals - 1 + lastValue = math.floor((currentPercent * intervals) + .5) + end + + if intervals and intervals > 0 and defaultValue then + slider.DefaultLocationIndicator.Position = UDim2.new(defaultValue/intervals, 0, 0.5, 0); + else + slider.DefaultLocationIndicator.Visible = false + end + + local function updateSlider() + local percent = lastValue + if intervals then + percent = lastValue/intervals + end + if intervals and intervals > 0 and defaultValue then + if lastValue >= defaultValue then + slider.DefaultLocationIndicator.Image = 'rbxasset://textures/AvatarEditorImages/circle_blue.png' + else + slider.DefaultLocationIndicator.Image = 'rbxasset://textures/AvatarEditorImages/circle_gray4.png' + end + end + dragger.Position = UDim2.new(percent, -16, .5, -16) + slider.FillBar.Size = UDim2.new(percent, 8, 0, 15) + if changedFunction then + changedFunction(name, lastValue) + end + end + updateSlider() + + local function handle(x) + if slider then + local percent = math.max(0, math.min(1, (x - slider.AbsolutePosition.x) / slider.AbsoluteSize.x)) + local thisInterval = percent + if intervals then + thisInterval = math.floor((percent*intervals)+.5) + end + if thisInterval ~= lastValue then + lastValue = thisInterval + updateSlider() + end + end + end + + local function sliderDown(x, y) +if not AvatarEditorSliderUIAdjustments then + handle(x) +end + local upListen = nil + local moveListen = nil + local highlight + local firstX = x + local firstY = y + +if not AvatarEditorSliderUIAdjustments then + if dragger and dragger.Parent then + highlight = dragger:FindFirstChild('Highlight') + if highlight then + highlight.Visible = true + end + end + if scrollingFrame then -- If there is a scroll frame, let's stop it from scroll'n while we slide'n + scrollingFrame.ScrollingEnabled = false + end +end + + local function inputChanged(input, gameProcessedEvent) + if input.UserInputState == Enum.UserInputState.Change + and (input.UserInputType == Enum.UserInputType.MouseMovement + or input.UserInputType == Enum.UserInputType.Touch) then + -- Update slider + if input.Position then + handle(input.Position.x) + end + elseif input.UserInputState == Enum.UserInputState.End + and (input.UserInputType == Enum.UserInputType.MouseButton1 + or input.UserInputType == Enum.UserInputType.Touch) then + -- End of slider interaction + if moveListen then + moveListen:Disconnect() + moveListen = nil + end + if upListen then + upListen:Disconnect() + upListen = nil + end + if scrollingFrame then + scrollingFrame.ScrollingEnabled = true + end + local dragger = slider:FindFirstChild('Dragger') + if dragger then + local highlight = dragger:FindFirstChild('Highlight') + if highlight then + highlight.Visible = false + end + end + end + end + + local function inputStarted(input, gameProcessedEvent) + if input.UserInputState == Enum.UserInputState.Change + and (input.UserInputType == Enum.UserInputType.MouseMovement + or input.UserInputType == Enum.UserInputType.Touch) + then + -- Determine if the first drag motion is mostly horizontal. If it is, disable (vertical) scrolling, + -- and allow subsequent events to move the slider + local w = math.abs( input.Position.X - firstX ) + local h = math.abs( input.Position.Y - firstY ) + if w == 0 and h == 0 then return end + + if w > h then + highlight = dragger:FindFirstChild('Highlight') + if highlight then + highlight.Visible = true + end + if scrollingFrame then -- If there is a scroll frame, let's stop it from scroll'n while we slide'n + scrollingFrame.ScrollingEnabled = false + end + + if input.Position then + handle(input.Position.x) + end + + moveListen:Disconnect() + upListen:Disconnect() + + moveListen = userInputService.InputChanged:connect( inputChanged ) + upListen = userInputService.InputEnded:connect( inputChanged ) + else + -- If the user is dragging vertically, disconnect event handlers to let scrolling happen. + if moveListen then + moveListen:Disconnect() + moveListen = nil + end + if upListen then + upListen:Disconnect() + upListen = nil + end + end + end + end + +if AvatarEditorSliderUIAdjustments then + moveListen = userInputService.InputChanged:connect( inputStarted ) + upListen = userInputService.InputEnded:connect( inputStarted ) +else + moveListen = userInputService.InputChanged:connect( inputChanged ) + upListen = userInputService.InputEnded:connect( inputChanged ) +end + end + + GetButtonDownEventConnected(sliderButton, sliderDown) + GetButtonDownEventConnected(draggerButton, sliderDown) + + slider.Visible = true + return slider + +end + +return this diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/SliderConsole.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/SliderConsole.lua new file mode 100644 index 0000000..fe235bc --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/SliderConsole.lua @@ -0,0 +1,366 @@ +local UserInputService = game:GetService('UserInputService') +local GuiService = game:GetService("GuiService") + +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfoConsole) +local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) + + +local THUMBSTICK_MOVE_DEADZONE = 0.6 +local THUMBSTICK_MOVE_INITIAL_REPEAT_TIME = 0.5 +local THUMBSTICK_MOVE_REPEAT_TIME = 0.12 +local THUMBSTICK_MOVE_SLIDER_PERCENT = 10 + +local function makeSlider() + local SliderFrameTemplate = Utilities.create'ImageLabel' + { + Name = 'SliderFrameTemplate'; + Position = UDim2.new(0, 0, 0, 0); + Size = UDim2.new(1, 0, 0, 48); + ZIndex = 5; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = ''; + ImageRectOffset = Vector2.new(0, 0); + ImageRectSize = Vector2.new(0, 0); + ScaleType = Enum.ScaleType.Stretch; + SliceCenter = Rect.new(0, 0, 0, 0); + } + + local BackgroundBar = Utilities.create'ImageLabel' + { + Name = 'BackgroundBar'; + AnchorPoint = Vector2.new(0, 0.5); + Position = UDim2.new(0, 0, 0.5, 0); + Size = UDim2.new(1, 0, 0, 12); + ZIndex = 3; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = 'rbxasset://textures/ui/Shell/AvatarEditor/scale/slide bar.png'; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(7, 5, 8, 7); + Parent = SliderFrameTemplate; + } + + local Dragger = Utilities.create'ImageLabel' + { + Name = 'Dragger'; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + Size = UDim2.new(0, 48, 0, 48); + ZIndex = 5; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = 'rbxasset://textures/ui/Shell/AvatarEditor/scale/slider.png'; + ScaleType = Enum.ScaleType.Stretch; + Parent = SliderFrameTemplate; + } + + local Highlight = Utilities.create'ImageLabel' + { + Name = 'Highlight'; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0.5, 0, 0.5, 0); + Size = UDim2.new(0, 96, 0, 96); + ZIndex = 4; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = 'rbxasset://textures/ui/Shell/AvatarEditor/scale/slider-hover.png'; + ScaleType = Enum.ScaleType.Stretch; + Parent = Dragger; + Visible = false; + } + + local DraggerSelector = Utilities.create'ImageLabel' + { + Name = "Selector"; + Image = "rbxasset://textures/ui/Shell/AvatarEditor/scale/slider-select.png"; + BackgroundTransparency = 1; + Size = UDim2.new(1, 14, 1, 14); + Position = UDim2.new(0, -7, 0, -7); + ZIndex = Dragger.ZIndex + 1; + } + + Utilities.create'ImageButton' + { + Name = 'DraggerButton'; + Position = UDim2.new(0.5, 0, 0.5, 0); + Size = UDim2.new(1, 0, 1, 0); + ZIndex = 5; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = ''; + ImageRectOffset = Vector2.new(0, 0); + ImageRectSize = Vector2.new(0, 0); + ScaleType = Enum.ScaleType.Stretch; + SliceCenter = Rect.new(0, 0, 0, 0); + AnchorPoint = Vector2.new(0.5, 0.5); + Parent = Dragger; + SelectionImageObject = DraggerSelector; + } + + local FillBar = Utilities.create'ImageLabel' + { + Name = 'FillBar'; + AnchorPoint = Vector2.new(0, 0.5); + Position = UDim2.new(0, -5, 0.5, 0); + Size = UDim2.new(0.5, 0, 0, 12); + ZIndex = 3; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = 'rbxasset://textures/ui/Shell/AvatarEditor/scale/slide bar-filled.png'; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(7, 5, 8, 7); + Parent = SliderFrameTemplate; + } + + Utilities.create'ImageLabel' + { + Name = 'DefaultLocationIndicator'; + AnchorPoint = Vector2.new(0.5, 0.5); + Position = UDim2.new(0, 0, 0.5, 0); + Size = UDim2.new(0, 24, 0, 24); + ZIndex = 3; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = 'rbxasset://textures/AvatarEditorImages/circle_gray4@2x.png'; + ScaleType = Enum.ScaleType.Stretch; + Parent = SliderFrameTemplate; + } + + local titleText = Utilities.create'TextLabel' + { + Name = 'TextLabel'; + Position = UDim2.new(0, 0, 0, -42); + Size = UDim2.new(1, 0, 0, 30); + ZIndex = 3; + BackgroundTransparency = 1; + BorderSizePixel = 1; + Parent = SliderFrameTemplate; + TextColor3 = Color3.new(1, 1, 1); + TextXAlignment = Enum.TextXAlignment.Left; + TextSize = LayoutInfo.SubHeaderFontSize; + Font = LayoutInfo.RegularFont; + } + + local dummySelection = Utilities.create'Frame' + { + BackgroundTransparency = 1; + } + + Utilities.create'ImageButton' + { + Name = 'SliderButton'; + Position = UDim2.new(0.5, 0, 0.5, 0); + Size = UDim2.new(1, 10, 1, 0); + ZIndex = 5; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = ''; + ImageRectOffset = Vector2.new(0, 0); + ImageRectSize = Vector2.new(0, 0); + ScaleType = Enum.ScaleType.Stretch; + SliceCenter = Rect.new(0, 0, 0, 0); + AnchorPoint = Vector2.new(0.5, 0.5); + Parent = SliderFrameTemplate; + SelectionImageObject = dummySelection; + } + + return SliderFrameTemplate +end + +local this = {} + + +function this.renderSlider(name, title, changedFunction, currentPercent, intervals, defaultValue, scrollingFrame) + local slider = makeSlider() + slider.TextLabel.Text = title + slider.Name = 'Slider'..name + + local dragger = slider:WaitForChild('Dragger') + local draggerButton = dragger:WaitForChild('DraggerButton') -- Specific buttons for larger or custom hitboxes. + local sliderButton = slider:WaitForChild('SliderButton') + local lastValue = currentPercent + if intervals then + intervals = intervals - 1 + lastValue = math.floor((currentPercent * intervals) + .5) + end + + if intervals and intervals > 0 and defaultValue then + slider.DefaultLocationIndicator.Position = UDim2.new(defaultValue/intervals, 0, 0.5, 0); + else + slider.DefaultLocationIndicator.Visible = false + end + + local function updateSlider() + local percent = lastValue + if intervals then + percent = lastValue/intervals + end + if intervals and intervals > 0 and defaultValue then + if lastValue >= defaultValue then + slider.DefaultLocationIndicator.Image = 'rbxasset://textures/AvatarEditorImages/circle_blue@2x.png' + else + slider.DefaultLocationIndicator.Image = 'rbxasset://textures/AvatarEditorImages/circle_gray4@2x.png' + end + end + dragger.Position = UDim2.new(percent, 0, .5, 0) + slider.FillBar.Size = UDim2.new(percent, 8, 0, 12) + if changedFunction then + changedFunction(name, lastValue) + end + end + updateSlider() + + -- Moves slider by an exact amount. + local function handle(dir) + if slider then + lastValue = math.max(0, math.min(intervals, lastValue + dir)) + updateSlider() + end + end + + -- Moves Slider by a percent of its overall range + local function handleByPercent(dir, percent) + handle(dir * math.ceil(intervals / percent)) + end + + local inputBeganListener, inputChangedListener, inputEndedListener, renderStepListener + local lastMoveDirection = 0 + local repeatMoveTimer = nil + local fastRepeatMoveTimer = nil + local dpadIsControlingSlider = false + local function reset() + inputBeganListener = Utilities.disconnectEvent(inputBeganListener) + inputChangedListener = Utilities.disconnectEvent(inputChangedListener) + inputEndedListener = Utilities.disconnectEvent(inputEndedListener) + renderStepListener = Utilities.disconnectEvent(renderStepListener) + lastMoveDirection = 0 + repeatMoveTimer = nil + fastRepeatMoveTimer = nil + dpadIsControlingSlider = false + end + + -- Check if scrollingFrame need to scroll up/down + local function checkScroll() + local newCanvasPositionY = scrollingFrame.CanvasPosition.Y + local bottomOffset = slider.TextLabel.AbsolutePosition.Y + LayoutInfo.SliderVeritcalOffset - 890 + if slider.TextLabel.AbsolutePosition.Y < LayoutInfo.SelectorTopMinDistance then + newCanvasPositionY = newCanvasPositionY - LayoutInfo.SliderVeritcalOffset + elseif bottomOffset > 0 then + newCanvasPositionY = newCanvasPositionY + bottomOffset + end + newCanvasPositionY = + math.max(0, + math.min( + newCanvasPositionY, + scrollingFrame.CanvasSize.Y.Offset - scrollingFrame.AbsoluteWindowSize.Y + ) + ) + + scrollingFrame.CanvasPosition = Vector2.new(scrollingFrame.CanvasPosition.X, newCanvasPositionY) + end + + draggerButton.SelectionGained:Connect(function() + local highlight + if dragger and dragger.Parent then + highlight = dragger:FindFirstChild('Highlight') + if highlight then + highlight.Visible = true + end + end + local function inputChanged(input, gameProcessedEvent) + if input.KeyCode == Enum.KeyCode.Thumbstick1 then + local newMoveDirection = input.Position.X + if math.abs(newMoveDirection) > THUMBSTICK_MOVE_DEADZONE then + local newMoveDirection = newMoveDirection > 0 and 1 or -1 + if lastMoveDirection ~= newMoveDirection then + repeatMoveTimer = tick() + fastRepeatMoveTimer = nil + lastMoveDirection = newMoveDirection + dpadIsControlingSlider = false + handleByPercent(lastMoveDirection, THUMBSTICK_MOVE_SLIDER_PERCENT) + + end + else --thumbstick is not pressed(under THUMBSTICK_MOVE_DEADZONE, reset timer and lastMoveDirection) + lastMoveDirection = 0 + repeatMoveTimer = nil + fastRepeatMoveTimer = nil + end + end + end + + local function inputEnded(input, gameProcessedEvent) + if input.KeyCode == Enum.KeyCode.DPadLeft or input.KeyCode == Enum.KeyCode.DPadRight then + lastMoveDirection = 0 + repeatMoveTimer = nil + fastRepeatMoveTimer = nil + end + end + + local function inputBegan(input, gameProcessedEvent) + if input.KeyCode == Enum.KeyCode.DPadLeft or input.KeyCode == Enum.KeyCode.DPadRight then + local newMoveDirection = input.KeyCode == Enum.KeyCode.DPadLeft and -1 or 1 + if lastMoveDirection ~= newMoveDirection then + repeatMoveTimer = tick() + fastRepeatMoveTimer = nil + lastMoveDirection = newMoveDirection + dpadIsControlingSlider = true + handle(lastMoveDirection) + end + end + end + + reset() + checkScroll() + + inputBeganListener = UserInputService.InputBegan:connect(inputBegan) + inputEndedListener = UserInputService.InputEnded:connect(inputEnded) + inputChangedListener = UserInputService.InputChanged:connect(inputChanged) + renderStepListener = game:GetService("RunService").RenderStepped:connect(function() + if lastMoveDirection == 0 or repeatMoveTimer == nil then + return + end + local curTime = tick() + if curTime - repeatMoveTimer >= THUMBSTICK_MOVE_INITIAL_REPEAT_TIME then + if not fastRepeatMoveTimer or curTime - fastRepeatMoveTimer >= THUMBSTICK_MOVE_REPEAT_TIME then + fastRepeatMoveTimer = curTime + if not dpadIsControlingSlider then + handleByPercent(lastMoveDirection, THUMBSTICK_MOVE_SLIDER_PERCENT) + else + handle(lastMoveDirection) + end + end + end + end) + end) + + draggerButton.SelectionLost:Connect(function() + local dragger = slider:FindFirstChild('Dragger') + if dragger then + local highlight = dragger:FindFirstChild('Highlight') + if highlight then + highlight.Visible = false + end + end + reset() + end) + + --sliderButton transfers the selection to draggerButton + sliderButton.SelectionGained:Connect(function() + GuiService.SelectedCoreObject = draggerButton + end) + + draggerButton.NextSelectionLeft = draggerButton + draggerButton.NextSelectionRight = draggerButton + sliderButton.NextSelectionLeft = sliderButton + sliderButton.NextSelectionRight = sliderButton + sliderButton.NextSelectionUp = sliderButton + sliderButton.NextSelectionDown = sliderButton + + slider.Visible = true + return slider +end + +return this diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/SpriteSheetManager.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/SpriteSheetManager.lua new file mode 100644 index 0000000..00985bf --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/SpriteSheetManager.lua @@ -0,0 +1,2819 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) + +local SIMULATE_RESOLUTION_SCALE = nil + +local sheetIds = { + [1] = 'rbxasset://textures/AvatarEditorImages/Sheet.png' +} + +local sprites = { + ["bar-empty-head"] = { + name = "bar-empty-head", + filename = "1x/bar-empty-head.png", + imageRectSize = Vector2.new(3, 6), + imageRectOffset = Vector2.new(18, 1), + imageId = sheetIds[1], + }, + ["bar-empty-mid"] = { + name = "bar-empty-mid", + filename = "1x/bar-empty-mid.png", + imageRectSize = Vector2.new(1, 6), + imageRectOffset = Vector2.new(15, 1), + imageId = sheetIds[1], + }, + ["bar-empty-tail"] = { + name = "bar-empty-tail", + filename = "1x/bar-empty-tail.png", + imageRectSize = Vector2.new(3, 6), + imageRectOffset = Vector2.new(1, 1), + imageId = sheetIds[1], + }, + ["bar-full-head"] = { + name = "bar-full-head", + filename = "1x/bar-full-head.png", + imageRectSize = Vector2.new(7, 14), + imageRectOffset = Vector2.new(100, 1), + imageId = sheetIds[1], + }, + ["bar-full-mid"] = { + name = "bar-full-mid", + filename = "1x/bar-full-mid.png", + imageRectSize = Vector2.new(1, 14), + imageRectOffset = Vector2.new(97, 1), + imageId = sheetIds[1], + }, + ["bar-full-tail"] = { + name = "bar-full-tail", + filename = "1x/bar-full-tail.png", + imageRectSize = Vector2.new(7, 14), + imageRectOffset = Vector2.new(109, 1), + imageId = sheetIds[1], + }, + ["btn-expand"] = { + name = "btn-expand", + filename = "1x/btn-expand.png", + imageRectSize = Vector2.new(60, 24), + imageRectOffset = Vector2.new(403, 1), + imageId = sheetIds[1], + }, + ["dragger-highlight"] = { + name = "dragger-highlight", + filename = "1x/dragger-highlight.png", + imageRectSize = Vector2.new(48, 48), + imageRectOffset = Vector2.new(627, 69), + imageId = sheetIds[1], + }, + ["dragger"] = { + name = "dragger", + filename = "1x/dragger.png", + imageRectSize = Vector2.new(32, 32), + imageRectOffset = Vector2.new(1968, 31), + imageId = sheetIds[1], + }, + ["gr-box-shadow"] = { + name = "gr-box-shadow", + filename = "1x/gr-box-shadow.png", + imageRectSize = Vector2.new(12, 12), + imageRectOffset = Vector2.new(32, 1), + imageId = sheetIds[1], + }, + ["gr-card corner"] = { + name = "gr-card corner", + filename = "1x/gr-card corner.png", + imageRectSize = Vector2.new(13, 13), + imageRectOffset = Vector2.new(82, 1), + imageId = sheetIds[1], + }, + ["gr-category-selector"] = { + name = "gr-category-selector", + filename = "1x/gr-category-selector.png", + imageRectSize = Vector2.new(48, 48), + imageRectOffset = Vector2.new(913, 69), + imageId = sheetIds[1], + }, + ["gr-circle-shadow"] = { + name = "gr-circle-shadow", + filename = "1x/gr-circle-shadow.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1351, 185), + imageId = sheetIds[1], + }, + ["gr-circle-white"] = { + name = "gr-circle-white", + filename = "1x/gr-circle-white.png", + imageRectSize = Vector2.new(48, 48), + imageRectOffset = Vector2.new(477, 69), + imageId = sheetIds[1], + }, + ["gr-corner"] = { + name = "gr-corner", + filename = "1x/gr-corner.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1467, 185), + imageId = sheetIds[1], + }, + ["gr-half-circle"] = { + name = "gr-half-circle", + filename = "1x/gr-half-circle.png", + imageRectSize = Vector2.new(34, 70), + imageRectOffset = Vector2.new(1954, 243), + imageId = sheetIds[1], + }, + ["gr-orange-circle"] = { + name = "gr-orange-circle", + filename = "1x/gr-orange-circle.png", + imageRectSize = Vector2.new(52, 52), + imageRectOffset = Vector2.new(1015, 69), + imageId = sheetIds[1], + }, + ["gr-ring-selector"] = { + name = "gr-ring-selector", + filename = "1x/gr-ring-selector.png", + imageRectSize = Vector2.new(54, 54), + imageRectOffset = Vector2.new(1069, 69), + imageId = sheetIds[1], + }, + ["gr-ring01"] = { + name = "gr-ring01", + filename = "1x/gr-ring01.png", + imageRectSize = Vector2.new(48, 48), + imageRectOffset = Vector2.new(527, 69), + imageId = sheetIds[1], + }, + ["gr-tail"] = { + name = "gr-tail", + filename = "1x/gr-tail.png", + imageRectSize = Vector2.new(1, 70), + imageRectOffset = Vector2.new(1951, 243), + imageId = sheetIds[1], + }, + ["gra-toggle-button"] = { + name = "gra-toggle-button", + filename = "1x/gra-toggle-button.png", + imageRectSize = Vector2.new(30, 24), + imageRectOffset = Vector2.new(344, 1), + imageId = sheetIds[1], + }, + ["gra-toggle-frame"] = { + name = "gra-toggle-frame", + filename = "1x/gra-toggle-frame.png", + imageRectSize = Vector2.new(64, 28), + imageRectOffset = Vector2.new(571, 31), + imageId = sheetIds[1], + }, + ["ic-avatar-animation-on"] = { + name = "ic-avatar-animation-on", + filename = "1x/ic-avatar-animation-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(728, 31), + imageId = sheetIds[1], + }, + ["ic-avatar-animation"] = { + name = "ic-avatar-animation", + filename = "1x/ic-avatar-animation.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(818, 31), + imageId = sheetIds[1], + }, + ["ic-back-white"] = { + name = "ic-back-white", + filename = "1x/ic-back-white.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(878, 31), + imageId = sheetIds[1], + }, + ["ic-body-part-on"] = { + name = "ic-body-part-on", + filename = "1x/ic-body-part-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(848, 31), + imageId = sheetIds[1], + }, + ["ic-body-part"] = { + name = "ic-body-part", + filename = "1x/ic-body-part.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(788, 31), + imageId = sheetIds[1], + }, + ["ic-bundle-on"] = { + name = "ic-bundle-on", + filename = "1x/ic-bundle-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(758, 31), + imageId = sheetIds[1], + }, + ["ic-bundle"] = { + name = "ic-bundle", + filename = "1x/ic-bundle.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(541, 31), + imageId = sheetIds[1], + }, + ["ic-color-on"] = { + name = "ic-color-on", + filename = "1x/ic-color-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(211, 31), + imageId = sheetIds[1], + }, + ["ic-color"] = { + name = "ic-color", + filename = "1x/ic-color.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(301, 31), + imageId = sheetIds[1], + }, + ["ic-left-arm-on"] = { + name = "ic-left-arm-on", + filename = "1x/ic-left-arm-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(998, 31), + imageId = sheetIds[1], + }, + ["ic-left-arm"] = { + name = "ic-left-arm", + filename = "1x/ic-left-arm.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1238, 31), + imageId = sheetIds[1], + }, + ["ic-left-leg-on"] = { + name = "ic-left-leg-on", + filename = "1x/ic-left-leg-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1268, 31), + imageId = sheetIds[1], + }, + ["ic-left-leg"] = { + name = "ic-left-leg", + filename = "1x/ic-left-leg.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1448, 31), + imageId = sheetIds[1], + }, + ["ic-right-arm-on"] = { + name = "ic-right-arm-on", + filename = "1x/ic-right-arm-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1328, 31), + imageId = sheetIds[1], + }, + ["ic-right-arm"] = { + name = "ic-right-arm", + filename = "1x/ic-right-arm.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(968, 31), + imageId = sheetIds[1], + }, + ["ic-right-leg-on"] = { + name = "ic-right-leg-on", + filename = "1x/ic-right-leg-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1791, 1), + imageId = sheetIds[1], + }, + ["ic-right-leg"] = { + name = "ic-right-leg", + filename = "1x/ic-right-leg.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1183, 1), + imageId = sheetIds[1], + }, + ["ic-scaling-on"] = { + name = "ic-scaling-on", + filename = "1x/ic-scaling-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(943, 1), + imageId = sheetIds[1], + }, + ["ic-scaling"] = { + name = "ic-scaling", + filename = "1x/ic-scaling.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(853, 1), + imageId = sheetIds[1], + }, + ["ic-shirt-on"] = { + name = "ic-shirt-on", + filename = "1x/ic-shirt-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(793, 1), + imageId = sheetIds[1], + }, + ["ic-shirt"] = { + name = "ic-shirt", + filename = "1x/ic-shirt.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(763, 1), + imageId = sheetIds[1], + }, + ["ic-shoulder-on"] = { + name = "ic-shoulder-on", + filename = "1x/ic-shoulder-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(973, 1), + imageId = sheetIds[1], + }, + ["ic-shoulder"] = { + name = "ic-shoulder", + filename = "1x/ic-shoulder.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1063, 1), + imageId = sheetIds[1], + }, + ["ic-tshirt-on"] = { + name = "ic-tshirt-on", + filename = "1x/ic-tshirt-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1033, 1), + imageId = sheetIds[1], + }, + ["ic-tshirt"] = { + name = "ic-tshirt", + filename = "1x/ic-tshirt.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1003, 1), + imageId = sheetIds[1], + }, + ["ic-up-black"] = { + name = "ic-up-black", + filename = "1x/ic-up-black.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(523, 1), + imageId = sheetIds[1], + }, + ["img-selector-box"] = { + name = "img-selector-box", + filename = "1x/img-selector-box.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1409, 185), + imageId = sheetIds[1], + }, + ["bar-empty-head@2x"] = { + name = "bar-empty-head@2x", + filename = "2x/bar-empty-head@2x.png", + imageRectSize = Vector2.new(6, 12), + imageRectOffset = Vector2.new(46, 1), + imageId = sheetIds[1], + }, + ["bar-empty-mid@2x"] = { + name = "bar-empty-mid@2x", + filename = "2x/bar-empty-mid@2x.png", + imageRectSize = Vector2.new(2, 12), + imageRectOffset = Vector2.new(78, 1), + imageId = sheetIds[1], + }, + ["bar-empty-tail@2x"] = { + name = "bar-empty-tail@2x", + filename = "2x/bar-empty-tail@2x.png", + imageRectSize = Vector2.new(6, 12), + imageRectOffset = Vector2.new(54, 1), + imageId = sheetIds[1], + }, + ["bar-full-head@2x"] = { + name = "bar-full-head@2x", + filename = "2x/bar-full-head@2x.png", + imageRectSize = Vector2.new(14, 28), + imageRectOffset = Vector2.new(1273, 1), + imageId = sheetIds[1], + }, + ["bar-full-mid@2x"] = { + name = "bar-full-mid@2x", + filename = "2x/bar-full-mid@2x.png", + imageRectSize = Vector2.new(2, 28), + imageRectOffset = Vector2.new(1757, 1), + imageId = sheetIds[1], + }, + ["bar-full-tail@2x"] = { + name = "bar-full-tail@2x", + filename = "2x/bar-full-tail@2x.png", + imageRectSize = Vector2.new(14, 28), + imageRectOffset = Vector2.new(1741, 1), + imageId = sheetIds[1], + }, + ["btn-expand@2x"] = { + name = "btn-expand@2x", + filename = "2x/btn-expand@2x.png", + imageRectSize = Vector2.new(120, 48), + imageRectOffset = Vector2.new(677, 69), + imageId = sheetIds[1], + }, + ["dragger-highlight@2x"] = { + name = "dragger-highlight@2x", + filename = "2x/dragger-highlight@2x.png", + imageRectSize = Vector2.new(96, 96), + imageRectOffset = Vector2.new(799, 573), + imageId = sheetIds[1], + }, + ["dragger@2x"] = { + name = "dragger@2x", + filename = "2x/dragger@2x.png", + imageRectSize = Vector2.new(64, 64), + imageRectOffset = Vector2.new(1885, 243), + imageId = sheetIds[1], + }, + ["gr-box-shadow@2x"] = { + name = "gr-box-shadow@2x", + filename = "2x/gr-box-shadow@2x.png", + imageRectSize = Vector2.new(24, 24), + imageRectOffset = Vector2.new(318, 1), + imageId = sheetIds[1], + }, + ["gr-card corner@2x"] = { + name = "gr-card corner@2x", + filename = "2x/gr-card corner@2x.png", + imageRectSize = Vector2.new(26, 26), + imageRectOffset = Vector2.new(465, 1), + imageId = sheetIds[1], + }, + ["gr-category-selector@2x"] = { + name = "gr-category-selector@2x", + filename = "2x/gr-category-selector@2x.png", + imageRectSize = Vector2.new(96, 96), + imageRectOffset = Vector2.new(603, 573), + imageId = sheetIds[1], + }, + ["gr-circle-shadow@2x"] = { + name = "gr-circle-shadow@2x", + filename = "2x/gr-circle-shadow@2x.png", + imageRectSize = Vector2.new(112, 112), + imageRectOffset = Vector2.new(1903, 573), + imageId = sheetIds[1], + }, + ["gr-circle-white@2x"] = { + name = "gr-circle-white@2x", + filename = "2x/gr-circle-white@2x.png", + imageRectSize = Vector2.new(96, 96), + imageRectOffset = Vector2.new(897, 573), + imageId = sheetIds[1], + }, + ["gr-corner@2x"] = { + name = "gr-corner@2x", + filename = "2x/gr-corner@2x.png", + imageRectSize = Vector2.new(112, 112), + imageRectOffset = Vector2.new(1, 687), + imageId = sheetIds[1], + }, + ["gr-half-circle@2x"] = { + name = "gr-half-circle@2x", + filename = "2x/gr-half-circle@2x.png", + imageRectSize = Vector2.new(68, 140), + imageRectOffset = Vector2.new(839, 687), + imageId = sheetIds[1], + }, + ["gr-orange-circle@2x"] = { + name = "gr-orange-circle@2x", + filename = "2x/gr-orange-circle@2x.png", + imageRectSize = Vector2.new(104, 104), + imageRectOffset = Vector2.new(1687, 573), + imageId = sheetIds[1], + }, + ["gr-ring-selector@2x"] = { + name = "gr-ring-selector@2x", + filename = "2x/gr-ring-selector@2x.png", + imageRectSize = Vector2.new(108, 108), + imageRectOffset = Vector2.new(1793, 573), + imageId = sheetIds[1], + }, + ["gr-ring01@2x"] = { + name = "gr-ring01@2x", + filename = "2x/gr-ring01@2x.png", + imageRectSize = Vector2.new(96, 96), + imageRectOffset = Vector2.new(701, 573), + imageId = sheetIds[1], + }, + ["gr-tail@2x"] = { + name = "gr-tail@2x", + filename = "2x/gr-tail@2x.png", + imageRectSize = Vector2.new(2, 140), + imageRectOffset = Vector2.new(909, 687), + imageId = sheetIds[1], + }, + ["gra-toggle-button@2x"] = { + name = "gra-toggle-button@2x", + filename = "2x/gra-toggle-button@2x.png", + imageRectSize = Vector2.new(60, 48), + imageRectOffset = Vector2.new(799, 69), + imageId = sheetIds[1], + }, + ["gra-toggle-frame@2x"] = { + name = "gra-toggle-frame@2x", + filename = "2x/gra-toggle-frame@2x.png", + imageRectSize = Vector2.new(128, 56), + imageRectOffset = Vector2.new(1161, 185), + imageId = sheetIds[1], + }, + ["ic-avatar-animation-on@2x"] = { + name = "ic-avatar-animation-on@2x", + filename = "2x/ic-avatar-animation-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1045, 185), + imageId = sheetIds[1], + }, + ["ic-avatar-animation@2x"] = { + name = "ic-avatar-animation@2x", + filename = "2x/ic-avatar-animation@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1525, 185), + imageId = sheetIds[1], + }, + ["ic-back-white@2x"] = { + name = "ic-back-white@2x", + filename = "2x/ic-back-white@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1873, 185), + imageId = sheetIds[1], + }, + ["ic-body-part-on@2x"] = { + name = "ic-body-part-on@2x", + filename = "2x/ic-body-part-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(59, 243), + imageId = sheetIds[1], + }, + ["ic-body-part@2x"] = { + name = "ic-body-part@2x", + filename = "2x/ic-body-part@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1, 243), + imageId = sheetIds[1], + }, + ["ic-bundle-on@2x"] = { + name = "ic-bundle-on@2x", + filename = "2x/ic-bundle-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1931, 185), + imageId = sheetIds[1], + }, + ["ic-bundle@2x"] = { + name = "ic-bundle@2x", + filename = "2x/ic-bundle@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1815, 185), + imageId = sheetIds[1], + }, + ["ic-color-on@2x"] = { + name = "ic-color-on@2x", + filename = "2x/ic-color-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(175, 185), + imageId = sheetIds[1], + }, + ["ic-color@2x"] = { + name = "ic-color@2x", + filename = "2x/ic-color@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(117, 185), + imageId = sheetIds[1], + }, + ["ic-left-arm-on@2x"] = { + name = "ic-left-arm-on@2x", + filename = "2x/ic-left-arm-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(233, 243), + imageId = sheetIds[1], + }, + ["ic-left-arm@2x"] = { + name = "ic-left-arm@2x", + filename = "2x/ic-left-arm@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(581, 243), + imageId = sheetIds[1], + }, + ["ic-left-leg-on@2x"] = { + name = "ic-left-leg-on@2x", + filename = "2x/ic-left-leg-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1161, 243), + imageId = sheetIds[1], + }, + ["ic-left-leg@2x"] = { + name = "ic-left-leg@2x", + filename = "2x/ic-left-leg@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(755, 243), + imageId = sheetIds[1], + }, + ["ic-right-arm-on@2x"] = { + name = "ic-right-arm-on@2x", + filename = "2x/ic-right-arm-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(291, 243), + imageId = sheetIds[1], + }, + ["ic-right-arm@2x"] = { + name = "ic-right-arm@2x", + filename = "2x/ic-right-arm@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(465, 243), + imageId = sheetIds[1], + }, + ["ic-right-leg-on@2x"] = { + name = "ic-right-leg-on@2x", + filename = "2x/ic-right-leg-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(349, 243), + imageId = sheetIds[1], + }, + ["ic-right-leg@2x"] = { + name = "ic-right-leg@2x", + filename = "2x/ic-right-leg@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(407, 243), + imageId = sheetIds[1], + }, + ["ic-scaling-on@2x"] = { + name = "ic-scaling-on@2x", + filename = "2x/ic-scaling-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1683, 127), + imageId = sheetIds[1], + }, + ["ic-scaling@2x"] = { + name = "ic-scaling@2x", + filename = "2x/ic-scaling@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1219, 243), + imageId = sheetIds[1], + }, + ["ic-shirt-on@2x"] = { + name = "ic-shirt-on@2x", + filename = "2x/ic-shirt-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1045, 243), + imageId = sheetIds[1], + }, + ["ic-shirt@2x"] = { + name = "ic-shirt@2x", + filename = "2x/ic-shirt@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1393, 243), + imageId = sheetIds[1], + }, + ["ic-shoulder-on@2x"] = { + name = "ic-shoulder-on@2x", + filename = "2x/ic-shoulder-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(117, 243), + imageId = sheetIds[1], + }, + ["ic-shoulder@2x"] = { + name = "ic-shoulder@2x", + filename = "2x/ic-shoulder@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1589, 69), + imageId = sheetIds[1], + }, + ["ic-tshirt-on@2x"] = { + name = "ic-tshirt-on@2x", + filename = "2x/ic-tshirt-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(117, 127), + imageId = sheetIds[1], + }, + ["ic-tshirt@2x"] = { + name = "ic-tshirt@2x", + filename = "2x/ic-tshirt@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(233, 127), + imageId = sheetIds[1], + }, + ["ic-up-black@2x"] = { + name = "ic-up-black@2x", + filename = "2x/ic-up-black@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(59, 127), + imageId = sheetIds[1], + }, + ["img-selector-box@2x"] = { + name = "img-selector-box@2x", + filename = "2x/img-selector-box@2x.png", + imageRectSize = Vector2.new(112, 112), + imageRectOffset = Vector2.new(115, 687), + imageId = sheetIds[1], + }, + ["bar-empty-head@3x"] = { + name = "bar-empty-head@3x", + filename = "3x/bar-empty-head@3x.png", + imageRectSize = Vector2.new(9, 18), + imageRectOffset = Vector2.new(284, 1), + imageId = sheetIds[1], + }, + ["bar-empty-mid@3x"] = { + name = "bar-empty-mid@3x", + filename = "3x/bar-empty-mid@3x.png", + imageRectSize = Vector2.new(3, 18), + imageRectOffset = Vector2.new(279, 1), + imageId = sheetIds[1], + }, + ["bar-empty-tail@3x"] = { + name = "bar-empty-tail@3x", + filename = "3x/bar-empty-tail@3x.png", + imageRectSize = Vector2.new(9, 18), + imageRectOffset = Vector2.new(245, 1), + imageId = sheetIds[1], + }, + ["bar-full-head@3x"] = { + name = "bar-full-head@3x", + filename = "3x/bar-full-head@3x.png", + imageRectSize = Vector2.new(21, 42), + imageRectOffset = Vector2.new(29, 69), + imageId = sheetIds[1], + }, + ["bar-full-mid@3x"] = { + name = "bar-full-mid@3x", + filename = "3x/bar-full-mid@3x.png", + imageRectSize = Vector2.new(3, 42), + imageRectOffset = Vector2.new(24, 69), + imageId = sheetIds[1], + }, + ["bar-full-tail@3x"] = { + name = "bar-full-tail@3x", + filename = "3x/bar-full-tail@3x.png", + imageRectSize = Vector2.new(21, 42), + imageRectOffset = Vector2.new(1, 69), + imageId = sheetIds[1], + }, + ["dragger-highlight@3x"] = { + name = "dragger-highlight@3x", + filename = "3x/dragger-highlight@3x.png", + imageRectSize = Vector2.new(144, 144), + imageRectOffset = Vector2.new(1497, 687), + imageId = sheetIds[1], + }, + ["dragger@3x"] = { + name = "dragger@3x", + filename = "3x/dragger@3x.png", + imageRectSize = Vector2.new(96, 96), + imageRectOffset = Vector2.new(1387, 573), + imageId = sheetIds[1], + }, + ["gr-box-shadow@3x"] = { + name = "gr-box-shadow@3x", + filename = "3x/gr-box-shadow@3x.png", + imageRectSize = Vector2.new(36, 36), + imageRectOffset = Vector2.new(2002, 31), + imageId = sheetIds[1], + }, + ["gr-circle-shadow@3x"] = { + name = "gr-circle-shadow@3x", + filename = "3x/gr-circle-shadow@3x.png", + imageRectSize = Vector2.new(168, 168), + imageRectOffset = Vector2.new(331, 839), + imageId = sheetIds[1], + }, + ["gr-circle-white@3x"] = { + name = "gr-circle-white@3x", + filename = "3x/gr-circle-white@3x.png", + imageRectSize = Vector2.new(144, 144), + imageRectOffset = Vector2.new(1351, 687), + imageId = sheetIds[1], + }, + ["gr-ring-selector@3x"] = { + name = "gr-ring-selector@3x", + filename = "3x/gr-ring-selector@3x.png", + imageRectSize = Vector2.new(162, 162), + imageRectOffset = Vector2.new(1, 839), + imageId = sheetIds[1], + }, + ["ic-color-on@3x"] = { + name = "ic-color-on@3x", + filename = "3x/ic-color-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1, 487), + imageId = sheetIds[1], + }, + ["ic-color@3x"] = { + name = "ic-color@3x", + filename = "3x/ic-color@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1463, 401), + imageId = sheetIds[1], + }, + ["ctn-primary nav"] = { + name = "ctn-primary nav", + filename = "graphic/navigation/ctn-primary nav.png", + imageRectSize = Vector2.new(98, 99), + imageRectOffset = Vector2.new(1485, 573), + imageId = sheetIds[1], + }, + ["ctn-primary nav@2x"] = { + name = "ctn-primary nav@2x", + filename = "graphic/navigation/ctn-primary nav@2x.png", + imageRectSize = Vector2.new(196, 198), + imageRectOffset = Vector2.new(1411, 839), + imageId = sheetIds[1], + }, + ["ctn-primary nav@3x"] = { + name = "ctn-primary nav@3x", + filename = "graphic/navigation/ctn-primary nav@3x.png", + imageRectSize = Vector2.new(294, 297), + imageRectOffset = Vector2.new(249, 1057), + imageId = sheetIds[1], + }, + ["ctn-secondary nav"] = { + name = "ctn-secondary nav", + filename = "graphic/navigation/ctn-secondary nav.png", + imageRectSize = Vector2.new(92, 15), + imageRectOffset = Vector2.new(151, 1), + imageId = sheetIds[1], + }, + ["ctn-secondary nav@2x"] = { + name = "ctn-secondary nav@2x", + filename = "graphic/navigation/ctn-secondary nav@2x.png", + imageRectSize = Vector2.new(184, 30), + imageRectOffset = Vector2.new(1748, 31), + imageId = sheetIds[1], + }, + ["ctn-secondary nav@3x"] = { + name = "ctn-secondary nav@3x", + filename = "graphic/navigation/ctn-secondary nav@3x.png", + imageRectSize = Vector2.new(276, 45), + imageRectOffset = Vector2.new(99, 69), + imageId = sheetIds[1], + }, + ["ring1"] = { + name = "ring1", + filename = "graphic/ring/ring1.png", + imageRectSize = Vector2.new(60, 60), + imageRectOffset = Vector2.new(1509, 243), + imageId = sheetIds[1], + }, + ["ring1@2x"] = { + name = "ring1@2x", + filename = "graphic/ring/ring1@2x.png", + imageRectSize = Vector2.new(120, 120), + imageRectOffset = Vector2.new(473, 687), + imageId = sheetIds[1], + }, + ["ring1@3x"] = { + name = "ring1@3x", + filename = "graphic/ring/ring1@3x.png", + imageRectSize = Vector2.new(180, 180), + imageRectOffset = Vector2.new(683, 839), + imageId = sheetIds[1], + }, + ["ring2"] = { + name = "ring2", + filename = "graphic/ring/ring2.png", + imageRectSize = Vector2.new(60, 60), + imageRectOffset = Vector2.new(1571, 243), + imageId = sheetIds[1], + }, + ["ring2@2x"] = { + name = "ring2@2x", + filename = "graphic/ring/ring2@2x.png", + imageRectSize = Vector2.new(120, 120), + imageRectOffset = Vector2.new(351, 687), + imageId = sheetIds[1], + }, + ["ring2@3x"] = { + name = "ring2@3x", + filename = "graphic/ring/ring2@3x.png", + imageRectSize = Vector2.new(180, 180), + imageRectOffset = Vector2.new(501, 839), + imageId = sheetIds[1], + }, + ["ring3"] = { + name = "ring3", + filename = "graphic/ring/ring3.png", + imageRectSize = Vector2.new(60, 60), + imageRectOffset = Vector2.new(1757, 243), + imageId = sheetIds[1], + }, + ["ring3@2x"] = { + name = "ring3@2x", + filename = "graphic/ring/ring3@2x.png", + imageRectSize = Vector2.new(120, 120), + imageRectOffset = Vector2.new(229, 687), + imageId = sheetIds[1], + }, + ["ring3@3x"] = { + name = "ring3@3x", + filename = "graphic/ring/ring3@3x.png", + imageRectSize = Vector2.new(180, 180), + imageRectOffset = Vector2.new(1229, 839), + imageId = sheetIds[1], + }, + ["ring4"] = { + name = "ring4", + filename = "graphic/ring/ring4.png", + imageRectSize = Vector2.new(60, 60), + imageRectOffset = Vector2.new(1633, 243), + imageId = sheetIds[1], + }, + ["ring4@2x"] = { + name = "ring4@2x", + filename = "graphic/ring/ring4@2x.png", + imageRectSize = Vector2.new(120, 120), + imageRectOffset = Vector2.new(717, 687), + imageId = sheetIds[1], + }, + ["ring4@3x"] = { + name = "ring4@3x", + filename = "graphic/ring/ring4@3x.png", + imageRectSize = Vector2.new(180, 180), + imageRectOffset = Vector2.new(1047, 839), + imageId = sheetIds[1], + }, + ["ring5"] = { + name = "ring5", + filename = "graphic/ring/ring5.png", + imageRectSize = Vector2.new(60, 60), + imageRectOffset = Vector2.new(1695, 243), + imageId = sheetIds[1], + }, + ["ring5@2x"] = { + name = "ring5@2x", + filename = "graphic/ring/ring5@2x.png", + imageRectSize = Vector2.new(120, 120), + imageRectOffset = Vector2.new(595, 687), + imageId = sheetIds[1], + }, + ["ring5@3x"] = { + name = "ring5@3x", + filename = "graphic/ring/ring5@3x.png", + imageRectSize = Vector2.new(180, 180), + imageRectOffset = Vector2.new(865, 839), + imageId = sheetIds[1], + }, + ["color circle-default"] = { + name = "color circle-default", + filename = "graphic/selector/color circle-default.png", + imageRectSize = Vector2.new(72, 72), + imageRectOffset = Vector2.new(78, 315), + imageId = sheetIds[1], + }, + ["color circle-default@2x"] = { + name = "color circle-default@2x", + filename = "graphic/selector/color circle-default@2x.png", + imageRectSize = Vector2.new(144, 144), + imageRectOffset = Vector2.new(913, 687), + imageId = sheetIds[1], + }, + ["color circle-default@3x"] = { + name = "color circle-default@3x", + filename = "graphic/selector/color circle-default@3x.png", + imageRectSize = Vector2.new(216, 216), + imageRectOffset = Vector2.new(1609, 839), + imageId = sheetIds[1], + }, + ["corner"] = { + name = "corner", + filename = "graphic/selector/corner.png", + imageRectSize = Vector2.new(50, 50), + imageRectOffset = Vector2.new(963, 69), + imageId = sheetIds[1], + }, + ["corner@2x"] = { + name = "corner@2x", + filename = "graphic/selector/corner@2x.png", + imageRectSize = Vector2.new(100, 100), + imageRectOffset = Vector2.new(1585, 573), + imageId = sheetIds[1], + }, + ["corner@3x"] = { + name = "corner@3x", + filename = "graphic/selector/corner@3x.png", + imageRectSize = Vector2.new(150, 150), + imageRectOffset = Vector2.new(1789, 687), + imageId = sheetIds[1], + }, + ["icon-border-on"] = { + name = "icon-border-on", + filename = "graphic/selector/icon-border-on.png", + imageRectSize = Vector2.new(48, 48), + imageRectOffset = Vector2.new(427, 69), + imageId = sheetIds[1], + }, + ["icon-border-on@2x"] = { + name = "icon-border-on@2x", + filename = "graphic/selector/icon-border-on@2x.png", + imageRectSize = Vector2.new(96, 96), + imageRectOffset = Vector2.new(1191, 573), + imageId = sheetIds[1], + }, + ["icon-border-on@3x"] = { + name = "icon-border-on@3x", + filename = "graphic/selector/icon-border-on@3x.png", + imageRectSize = Vector2.new(144, 144), + imageRectOffset = Vector2.new(1059, 687), + imageId = sheetIds[1], + }, + ["icon-border"] = { + name = "icon-border", + filename = "graphic/selector/icon-border.png", + imageRectSize = Vector2.new(48, 48), + imageRectOffset = Vector2.new(577, 69), + imageId = sheetIds[1], + }, + ["icon-border@2x"] = { + name = "icon-border@2x", + filename = "graphic/selector/icon-border@2x.png", + imageRectSize = Vector2.new(96, 96), + imageRectOffset = Vector2.new(1289, 573), + imageId = sheetIds[1], + }, + ["icon-border@3x"] = { + name = "icon-border@3x", + filename = "graphic/selector/icon-border@3x.png", + imageRectSize = Vector2.new(144, 144), + imageRectOffset = Vector2.new(1205, 687), + imageId = sheetIds[1], + }, + ["selector-border"] = { + name = "selector-border", + filename = "graphic/selector/selector-border.png", + imageRectSize = Vector2.new(7, 7), + imageRectOffset = Vector2.new(23, 1), + imageId = sheetIds[1], + }, + ["selector-border@2x"] = { + name = "selector-border@2x", + filename = "graphic/selector/selector-border@2x.png", + imageRectSize = Vector2.new(14, 14), + imageRectOffset = Vector2.new(118, 1), + imageId = sheetIds[1], + }, + ["selector-border@3x"] = { + name = "selector-border@3x", + filename = "graphic/selector/selector-border@3x.png", + imageRectSize = Vector2.new(21, 21), + imageRectOffset = Vector2.new(295, 1), + imageId = sheetIds[1], + }, + ["selector-color"] = { + name = "selector-color", + filename = "graphic/selector/selector-color.png", + imageRectSize = Vector2.new(82, 82), + imageRectOffset = Vector2.new(152, 315), + imageId = sheetIds[1], + }, + ["selector-color@2x"] = { + name = "selector-color@2x", + filename = "graphic/selector/selector-color@2x.png", + imageRectSize = Vector2.new(164, 164), + imageRectOffset = Vector2.new(165, 839), + imageId = sheetIds[1], + }, + ["selector-color@3x"] = { + name = "selector-color@3x", + filename = "graphic/selector/selector-color@3x.png", + imageRectSize = Vector2.new(246, 246), + imageRectOffset = Vector2.new(1, 1057), + imageId = sheetIds[1], + }, + ["btn-slide pressed"] = { + name = "btn-slide pressed", + filename = "graphic/slider/btn-slide pressed.png", + imageRectSize = Vector2.new(48, 48), + imageRectOffset = Vector2.new(627, 69), + imageId = sheetIds[1], + }, + ["btn-slide pressed@2x"] = { + name = "btn-slide pressed@2x", + filename = "graphic/slider/btn-slide pressed@2x.png", + imageRectSize = Vector2.new(96, 96), + imageRectOffset = Vector2.new(799, 573), + imageId = sheetIds[1], + }, + ["btn-slide pressed@3x"] = { + name = "btn-slide pressed@3x", + filename = "graphic/slider/btn-slide pressed@3x.png", + imageRectSize = Vector2.new(144, 144), + imageRectOffset = Vector2.new(1497, 687), + imageId = sheetIds[1], + }, + ["btn-slider"] = { + name = "btn-slider", + filename = "graphic/slider/btn-slider.png", + imageRectSize = Vector2.new(32, 32), + imageRectOffset = Vector2.new(1934, 31), + imageId = sheetIds[1], + }, + ["btn-slider@2x"] = { + name = "btn-slider@2x", + filename = "graphic/slider/btn-slider@2x.png", + imageRectSize = Vector2.new(64, 64), + imageRectOffset = Vector2.new(1819, 243), + imageId = sheetIds[1], + }, + ["btn-slider@3x"] = { + name = "btn-slider@3x", + filename = "graphic/slider/btn-slider@3x.png", + imageRectSize = Vector2.new(96, 96), + imageRectOffset = Vector2.new(995, 573), + imageId = sheetIds[1], + }, + ["slider bar-on"] = { + name = "slider bar-on", + filename = "graphic/slider/slider bar-on.png", + imageRectSize = Vector2.new(15, 14), + imageRectOffset = Vector2.new(134, 1), + imageId = sheetIds[1], + }, + ["slider bar-on@2x"] = { + name = "slider bar-on@2x", + filename = "graphic/slider/slider bar-on@2x.png", + imageRectSize = Vector2.new(30, 28), + imageRectOffset = Vector2.new(1439, 1), + imageId = sheetIds[1], + }, + ["slider bar-on@3x"] = { + name = "slider bar-on@3x", + filename = "graphic/slider/slider bar-on@3x.png", + imageRectSize = Vector2.new(45, 42), + imageRectOffset = Vector2.new(52, 69), + imageId = sheetIds[1], + }, + ["slider bar"] = { + name = "slider bar", + filename = "graphic/slider/slider bar.png", + imageRectSize = Vector2.new(7, 6), + imageRectOffset = Vector2.new(6, 1), + imageId = sheetIds[1], + }, + ["slider bar@2x"] = { + name = "slider bar@2x", + filename = "graphic/slider/slider bar@2x.png", + imageRectSize = Vector2.new(14, 12), + imageRectOffset = Vector2.new(62, 1), + imageId = sheetIds[1], + }, + ["slider bar@3x"] = { + name = "slider bar@3x", + filename = "graphic/slider/slider bar@3x.png", + imageRectSize = Vector2.new(21, 18), + imageRectOffset = Vector2.new(256, 1), + imageId = sheetIds[1], + }, + ["btn-toggle"] = { + name = "btn-toggle", + filename = "graphic/switch/btn-toggle.png", + imageRectSize = Vector2.new(25, 24), + imageRectOffset = Vector2.new(376, 1), + imageId = sheetIds[1], + }, + ["btn-toggle@2x"] = { + name = "btn-toggle@2x", + filename = "graphic/switch/btn-toggle@2x.png", + imageRectSize = Vector2.new(50, 48), + imageRectOffset = Vector2.new(861, 69), + imageId = sheetIds[1], + }, + ["btn-toggle@3x"] = { + name = "btn-toggle@3x", + filename = "graphic/switch/btn-toggle@3x.png", + imageRectSize = Vector2.new(75, 72), + imageRectOffset = Vector2.new(1, 315), + imageId = sheetIds[1], + }, + ["ctn-toggle"] = { + name = "ctn-toggle", + filename = "graphic/switch/ctn-toggle.png", + imageRectSize = Vector2.new(29, 28), + imageRectOffset = Vector2.new(667, 31), + imageId = sheetIds[1], + }, + ["ctn-toggle@2x"] = { + name = "ctn-toggle@2x", + filename = "graphic/switch/ctn-toggle@2x.png", + imageRectSize = Vector2.new(58, 56), + imageRectOffset = Vector2.new(1291, 185), + imageId = sheetIds[1], + }, + ["ctn-toggle@3x"] = { + name = "ctn-toggle@3x", + filename = "graphic/switch/ctn-toggle@3x.png", + imageRectSize = Vector2.new(87, 84), + imageRectOffset = Vector2.new(1440, 315), + imageId = sheetIds[1], + }, + ["ic-climb-on"] = { + name = "ic-climb-on", + filename = "Icon/Navigation/animations/ic-climb-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(451, 31), + imageId = sheetIds[1], + }, + ["ic-climb-on@2x"] = { + name = "ic-climb-on@2x", + filename = "Icon/Navigation/animations/ic-climb-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1641, 185), + imageId = sheetIds[1], + }, + ["ic-climb-on@3x"] = { + name = "ic-climb-on@3x", + filename = "Icon/Navigation/animations/ic-climb-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(173, 487), + imageId = sheetIds[1], + }, + ["ic-climb"] = { + name = "ic-climb", + filename = "Icon/Navigation/animations/ic-climb.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(151, 31), + imageId = sheetIds[1], + }, + ["ic-climb@2x"] = { + name = "ic-climb@2x", + filename = "Icon/Navigation/animations/ic-climb@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1757, 185), + imageId = sheetIds[1], + }, + ["ic-climb@3x"] = { + name = "ic-climb@3x", + filename = "Icon/Navigation/animations/ic-climb@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(431, 487), + imageId = sheetIds[1], + }, + ["ic-fall-on"] = { + name = "ic-fall-on", + filename = "Icon/Navigation/animations/ic-fall-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(241, 31), + imageId = sheetIds[1], + }, + ["ic-fall-on@2x"] = { + name = "ic-fall-on@2x", + filename = "Icon/Navigation/animations/ic-fall-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(233, 185), + imageId = sheetIds[1], + }, + ["ic-fall-on@3x"] = { + name = "ic-fall-on@3x", + filename = "Icon/Navigation/animations/ic-fall-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(517, 401), + imageId = sheetIds[1], + }, + ["ic-fall"] = { + name = "ic-fall", + filename = "Icon/Navigation/animations/ic-fall.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(61, 31), + imageId = sheetIds[1], + }, + ["ic-fall@2x"] = { + name = "ic-fall@2x", + filename = "Icon/Navigation/animations/ic-fall@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(291, 185), + imageId = sheetIds[1], + }, + ["ic-fall@3x"] = { + name = "ic-fall@3x", + filename = "Icon/Navigation/animations/ic-fall@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(775, 401), + imageId = sheetIds[1], + }, + ["ic-idle-on"] = { + name = "ic-idle-on", + filename = "Icon/Navigation/animations/ic-idle-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1501, 1), + imageId = sheetIds[1], + }, + ["ic-idle-on@2x"] = { + name = "ic-idle-on@2x", + filename = "Icon/Navigation/animations/ic-idle-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1567, 127), + imageId = sheetIds[1], + }, + ["ic-idle-on@3x"] = { + name = "ic-idle-on@3x", + filename = "Icon/Navigation/animations/ic-idle-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(259, 573), + imageId = sheetIds[1], + }, + ["ic-idle"] = { + name = "ic-idle", + filename = "Icon/Navigation/animations/ic-idle.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1591, 1), + imageId = sheetIds[1], + }, + ["ic-idle@2x"] = { + name = "ic-idle@2x", + filename = "Icon/Navigation/animations/ic-idle@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1509, 127), + imageId = sheetIds[1], + }, + ["ic-idle@3x"] = { + name = "ic-idle@3x", + filename = "Icon/Navigation/animations/ic-idle@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(345, 573), + imageId = sheetIds[1], + }, + ["ic-jump-on"] = { + name = "ic-jump-on", + filename = "Icon/Navigation/animations/ic-jump-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1088, 31), + imageId = sheetIds[1], + }, + ["ic-jump-on@2x"] = { + name = "ic-jump-on@2x", + filename = "Icon/Navigation/animations/ic-jump-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1335, 243), + imageId = sheetIds[1], + }, + ["ic-jump-on@3x"] = { + name = "ic-jump-on@3x", + filename = "Icon/Navigation/animations/ic-jump-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(87, 573), + imageId = sheetIds[1], + }, + ["ic-jump"] = { + name = "ic-jump", + filename = "Icon/Navigation/animations/ic-jump.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1028, 31), + imageId = sheetIds[1], + }, + ["ic-jump@2x"] = { + name = "ic-jump@2x", + filename = "Icon/Navigation/animations/ic-jump@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(639, 243), + imageId = sheetIds[1], + }, + ["ic-jump@3x"] = { + name = "ic-jump@3x", + filename = "Icon/Navigation/animations/ic-jump@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(861, 487), + imageId = sheetIds[1], + }, + ["ic-run-on"] = { + name = "ic-run-on", + filename = "Icon/Navigation/animations/ic-run-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(913, 1), + imageId = sheetIds[1], + }, + ["ic-run-on@2x"] = { + name = "ic-run-on@2x", + filename = "Icon/Navigation/animations/ic-run-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1393, 127), + imageId = sheetIds[1], + }, + ["ic-run-on@3x"] = { + name = "ic-run-on@3x", + filename = "Icon/Navigation/animations/ic-run-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1377, 487), + imageId = sheetIds[1], + }, + ["ic-run"] = { + name = "ic-run", + filename = "Icon/Navigation/animations/ic-run.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(883, 1), + imageId = sheetIds[1], + }, + ["ic-run@2x"] = { + name = "ic-run@2x", + filename = "Icon/Navigation/animations/ic-run@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1277, 243), + imageId = sheetIds[1], + }, + ["ic-run@3x"] = { + name = "ic-run@3x", + filename = "Icon/Navigation/animations/ic-run@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1807, 401), + imageId = sheetIds[1], + }, + ["ic-swim-on"] = { + name = "ic-swim-on", + filename = "Icon/Navigation/animations/ic-swim-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1531, 1), + imageId = sheetIds[1], + }, + ["ic-swim-on@2x"] = { + name = "ic-swim-on@2x", + filename = "Icon/Navigation/animations/ic-swim-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(987, 127), + imageId = sheetIds[1], + }, + ["ic-swim-on@3x"] = { + name = "ic-swim-on@3x", + filename = "Icon/Navigation/animations/ic-swim-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(322, 315), + imageId = sheetIds[1], + }, + ["ic-swim"] = { + name = "ic-swim", + filename = "Icon/Navigation/animations/ic-swim.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1561, 1), + imageId = sheetIds[1], + }, + ["ic-swim@2x"] = { + name = "ic-swim@2x", + filename = "Icon/Navigation/animations/ic-swim@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(581, 127), + imageId = sheetIds[1], + }, + ["ic-swim@3x"] = { + name = "ic-swim@3x", + filename = "Icon/Navigation/animations/ic-swim@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(408, 315), + imageId = sheetIds[1], + }, + ["ic-walk-on"] = { + name = "ic-walk-on", + filename = "Icon/Navigation/animations/ic-walk-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1471, 1), + imageId = sheetIds[1], + }, + ["ic-walk-on@2x"] = { + name = "ic-walk-on@2x", + filename = "Icon/Navigation/animations/ic-walk-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(465, 127), + imageId = sheetIds[1], + }, + ["ic-walk-on@3x"] = { + name = "ic-walk-on@3x", + filename = "Icon/Navigation/animations/ic-walk-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1701, 315), + imageId = sheetIds[1], + }, + ["ic-walk"] = { + name = "ic-walk", + filename = "Icon/Navigation/animations/ic-walk.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1409, 1), + imageId = sheetIds[1], + }, + ["ic-walk@2x"] = { + name = "ic-walk@2x", + filename = "Icon/Navigation/animations/ic-walk@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(755, 127), + imageId = sheetIds[1], + }, + ["ic-walk@3x"] = { + name = "ic-walk@3x", + filename = "Icon/Navigation/animations/ic-walk@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1873, 315), + imageId = sheetIds[1], + }, + ["ic-head-on"] = { + name = "ic-head-on", + filename = "Icon/Navigation/body/ic-head-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1243, 1), + imageId = sheetIds[1], + }, + ["ic-head-on@2x"] = { + name = "ic-head-on@2x", + filename = "Icon/Navigation/body/ic-head-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(929, 127), + imageId = sheetIds[1], + }, + ["ic-head-on@3x"] = { + name = "ic-head-on@3x", + filename = "Icon/Navigation/body/ic-head-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(431, 573), + imageId = sheetIds[1], + }, + ["ic-head"] = { + name = "ic-head", + filename = "Icon/Navigation/body/ic-head.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1208, 31), + imageId = sheetIds[1], + }, + ["ic-head@2x"] = { + name = "ic-head@2x", + filename = "Icon/Navigation/body/ic-head@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1741, 127), + imageId = sheetIds[1], + }, + ["ic-head@3x"] = { + name = "ic-head@3x", + filename = "Icon/Navigation/body/ic-head@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(173, 573), + imageId = sheetIds[1], + }, + ["ic-left-arms-on"] = { + name = "ic-left-arms-on", + filename = "Icon/Navigation/body/ic-left-arms-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1289, 1), + imageId = sheetIds[1], + }, + ["ic-left-arms-on@2x"] = { + name = "ic-left-arms-on@2x", + filename = "Icon/Navigation/body/ic-left-arms-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(697, 127), + imageId = sheetIds[1], + }, + ["ic-left-arms-on@3x"] = { + name = "ic-left-arms-on@3x", + filename = "Icon/Navigation/body/ic-left-arms-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1119, 487), + imageId = sheetIds[1], + }, + ["ic-left-arms"] = { + name = "ic-left-arms", + filename = "Icon/Navigation/body/ic-left-arms.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1379, 1), + imageId = sheetIds[1], + }, + ["ic-left-arms@2x"] = { + name = "ic-left-arms@2x", + filename = "Icon/Navigation/body/ic-left-arms@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(407, 127), + imageId = sheetIds[1], + }, + ["ic-left-arms@3x"] = { + name = "ic-left-arms@3x", + filename = "Icon/Navigation/body/ic-left-arms@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1, 573), + imageId = sheetIds[1], + }, + ["ic-left-legs-on"] = { + name = "ic-left-legs-on", + filename = "Icon/Navigation/body/ic-left-legs-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1319, 1), + imageId = sheetIds[1], + }, + ["ic-left-legs-on@2x"] = { + name = "ic-left-legs-on@2x", + filename = "Icon/Navigation/body/ic-left-legs-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(639, 127), + imageId = sheetIds[1], + }, + ["ic-left-legs-on@3x"] = { + name = "ic-left-legs-on@3x", + filename = "Icon/Navigation/body/ic-left-legs-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1033, 487), + imageId = sheetIds[1], + }, + ["ic-left-legs"] = { + name = "ic-left-legs", + filename = "Icon/Navigation/body/ic-left-legs.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1448, 31), + imageId = sheetIds[1], + }, + ["ic-left-legs@2x"] = { + name = "ic-left-legs@2x", + filename = "Icon/Navigation/body/ic-left-legs@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(755, 243), + imageId = sheetIds[1], + }, + ["ic-left-legs@3x"] = { + name = "ic-left-legs@3x", + filename = "Icon/Navigation/body/ic-left-legs@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1205, 487), + imageId = sheetIds[1], + }, + ["ic-right-arms-on"] = { + name = "ic-right-arms-on", + filename = "Icon/Navigation/body/ic-right-arms-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1349, 1), + imageId = sheetIds[1], + }, + ["ic-right-arms-on@2x"] = { + name = "ic-right-arms-on@2x", + filename = "Icon/Navigation/body/ic-right-arms-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1451, 127), + imageId = sheetIds[1], + }, + ["ic-right-arms-on@3x"] = { + name = "ic-right-arms-on@3x", + filename = "Icon/Navigation/body/ic-right-arms-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1721, 487), + imageId = sheetIds[1], + }, + ["ic-right-arms"] = { + name = "ic-right-arms", + filename = "Icon/Navigation/body/ic-right-arms.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1621, 1), + imageId = sheetIds[1], + }, + ["ic-right-arms@2x"] = { + name = "ic-right-arms@2x", + filename = "Icon/Navigation/body/ic-right-arms@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1045, 127), + imageId = sheetIds[1], + }, + ["ic-right-arms@3x"] = { + name = "ic-right-arms@3x", + filename = "Icon/Navigation/body/ic-right-arms@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1893, 487), + imageId = sheetIds[1], + }, + ["ic-right-legs-on"] = { + name = "ic-right-legs-on", + filename = "Icon/Navigation/body/ic-right-legs-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1681, 1), + imageId = sheetIds[1], + }, + ["ic-right-legs-on@2x"] = { + name = "ic-right-legs-on@2x", + filename = "Icon/Navigation/body/ic-right-legs-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1161, 127), + imageId = sheetIds[1], + }, + ["ic-right-legs-on@3x"] = { + name = "ic-right-legs-on@3x", + filename = "Icon/Navigation/body/ic-right-legs-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1635, 487), + imageId = sheetIds[1], + }, + ["ic-right-legs"] = { + name = "ic-right-legs", + filename = "Icon/Navigation/body/ic-right-legs.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1183, 1), + imageId = sheetIds[1], + }, + ["ic-right-legs@2x"] = { + name = "ic-right-legs@2x", + filename = "Icon/Navigation/body/ic-right-legs@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(407, 243), + imageId = sheetIds[1], + }, + ["ic-right-legs@3x"] = { + name = "ic-right-legs@3x", + filename = "Icon/Navigation/body/ic-right-legs@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1463, 487), + imageId = sheetIds[1], + }, + ["ic-scale-on"] = { + name = "ic-scale-on", + filename = "Icon/Navigation/body/ic-scale-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1821, 1), + imageId = sheetIds[1], + }, + ["ic-scale-on@2x"] = { + name = "ic-scale-on@2x", + filename = "Icon/Navigation/body/ic-scale-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1219, 127), + imageId = sheetIds[1], + }, + ["ic-scale-on@3x"] = { + name = "ic-scale-on@3x", + filename = "Icon/Navigation/body/ic-scale-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(603, 487), + imageId = sheetIds[1], + }, + ["ic-scale"] = { + name = "ic-scale", + filename = "Icon/Navigation/body/ic-scale.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(853, 1), + imageId = sheetIds[1], + }, + ["ic-scale@2x"] = { + name = "ic-scale@2x", + filename = "Icon/Navigation/body/ic-scale@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1219, 243), + imageId = sheetIds[1], + }, + ["ic-scale@3x"] = { + name = "ic-scale@3x", + filename = "Icon/Navigation/body/ic-scale@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1615, 315), + imageId = sheetIds[1], + }, + ["ic-skintone-on"] = { + name = "ic-skintone-on", + filename = "Icon/Navigation/body/ic-skintone-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(211, 31), + imageId = sheetIds[1], + }, + ["ic-skintone-on@2x"] = { + name = "ic-skintone-on@2x", + filename = "Icon/Navigation/body/ic-skintone-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(175, 185), + imageId = sheetIds[1], + }, + ["ic-skintone-on@3x"] = { + name = "ic-skintone-on@3x", + filename = "Icon/Navigation/body/ic-skintone-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1, 487), + imageId = sheetIds[1], + }, + ["ic-skintone"] = { + name = "ic-skintone", + filename = "Icon/Navigation/body/ic-skintone.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(301, 31), + imageId = sheetIds[1], + }, + ["ic-skintone@2x"] = { + name = "ic-skintone@2x", + filename = "Icon/Navigation/body/ic-skintone@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(117, 185), + imageId = sheetIds[1], + }, + ["ic-skintone@3x"] = { + name = "ic-skintone@3x", + filename = "Icon/Navigation/body/ic-skintone@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1463, 401), + imageId = sheetIds[1], + }, + ["ic-torso-on"] = { + name = "ic-torso-on", + filename = "Icon/Navigation/body/ic-torso-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1851, 1), + imageId = sheetIds[1], + }, + ["ic-torso-on@2x"] = { + name = "ic-torso-on@2x", + filename = "Icon/Navigation/body/ic-torso-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(349, 127), + imageId = sheetIds[1], + }, + ["ic-torso-on@3x"] = { + name = "ic-torso-on@3x", + filename = "Icon/Navigation/body/ic-torso-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(236, 315), + imageId = sheetIds[1], + }, + ["ic-torso"] = { + name = "ic-torso", + filename = "Icon/Navigation/body/ic-torso.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1093, 1), + imageId = sheetIds[1], + }, + ["ic-torso@2x"] = { + name = "ic-torso@2x", + filename = "Icon/Navigation/body/ic-torso@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1, 127), + imageId = sheetIds[1], + }, + ["ic-torso@3x"] = { + name = "ic-torso@3x", + filename = "Icon/Navigation/body/ic-torso@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(494, 315), + imageId = sheetIds[1], + }, + ["ic-back-on"] = { + name = "ic-back-on", + filename = "Icon/Navigation/clothing/ic-back-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1911, 1), + imageId = sheetIds[1], + }, + ["ic-back-on@2x"] = { + name = "ic-back-on@2x", + filename = "Icon/Navigation/clothing/ic-back-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1299, 69), + imageId = sheetIds[1], + }, + ["ic-back-on@3x"] = { + name = "ic-back-on@3x", + filename = "Icon/Navigation/clothing/ic-back-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1549, 401), + imageId = sheetIds[1], + }, + ["ic-back"] = { + name = "ic-back", + filename = "Icon/Navigation/clothing/ic-back.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(938, 31), + imageId = sheetIds[1], + }, + ["ic-back@2x"] = { + name = "ic-back@2x", + filename = "Icon/Navigation/clothing/ic-back@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1989, 185), + imageId = sheetIds[1], + }, + ["ic-back@3x"] = { + name = "ic-back@3x", + filename = "Icon/Navigation/clothing/ic-back@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1377, 401), + imageId = sheetIds[1], + }, + ["ic-face-accessories-on"] = { + name = "ic-face-accessories-on", + filename = "Icon/Navigation/clothing/ic-face-accessories-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(2001, 1), + imageId = sheetIds[1], + }, + ["ic-face-accessories-on@2x"] = { + name = "ic-face-accessories-on@2x", + filename = "Icon/Navigation/clothing/ic-face-accessories-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1531, 69), + imageId = sheetIds[1], + }, + ["ic-face-accessories-on@3x"] = { + name = "ic-face-accessories-on@3x", + filename = "Icon/Navigation/clothing/ic-face-accessories-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1268, 315), + imageId = sheetIds[1], + }, + ["ic-face-accessories"] = { + name = "ic-face-accessories", + filename = "Icon/Navigation/clothing/ic-face-accessories.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1941, 1), + imageId = sheetIds[1], + }, + ["ic-face-accessories@2x"] = { + name = "ic-face-accessories@2x", + filename = "Icon/Navigation/clothing/ic-face-accessories@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1647, 69), + imageId = sheetIds[1], + }, + ["ic-face-accessories@3x"] = { + name = "ic-face-accessories@3x", + filename = "Icon/Navigation/clothing/ic-face-accessories@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1354, 315), + imageId = sheetIds[1], + }, + ["ic-face-on"] = { + name = "ic-face-on", + filename = "Icon/Navigation/clothing/ic-face-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(331, 31), + imageId = sheetIds[1], + }, + ["ic-face-on@2x"] = { + name = "ic-face-on@2x", + filename = "Icon/Navigation/clothing/ic-face-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1915, 127), + imageId = sheetIds[1], + }, + ["ic-face-on@3x"] = { + name = "ic-face-on@3x", + filename = "Icon/Navigation/clothing/ic-face-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1, 401), + imageId = sheetIds[1], + }, + ["ic-face"] = { + name = "ic-face", + filename = "Icon/Navigation/clothing/ic-face.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(271, 31), + imageId = sheetIds[1], + }, + ["ic-face@2x"] = { + name = "ic-face@2x", + filename = "Icon/Navigation/clothing/ic-face@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1857, 127), + imageId = sheetIds[1], + }, + ["ic-face@3x"] = { + name = "ic-face@3x", + filename = "Icon/Navigation/clothing/ic-face@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1959, 315), + imageId = sheetIds[1], + }, + ["ic-front-on"] = { + name = "ic-front-on", + filename = "Icon/Navigation/clothing/ic-front-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1971, 1), + imageId = sheetIds[1], + }, + ["ic-front-on@2x"] = { + name = "ic-front-on@2x", + filename = "Icon/Navigation/clothing/ic-front-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1473, 69), + imageId = sheetIds[1], + }, + ["ic-front-on@3x"] = { + name = "ic-front-on@3x", + filename = "Icon/Navigation/clothing/ic-front-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1033, 401), + imageId = sheetIds[1], + }, + ["ic-front"] = { + name = "ic-front", + filename = "Icon/Navigation/clothing/ic-front.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1628, 31), + imageId = sheetIds[1], + }, + ["ic-front@2x"] = { + name = "ic-front@2x", + filename = "Icon/Navigation/clothing/ic-front@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(697, 185), + imageId = sheetIds[1], + }, + ["ic-front@3x"] = { + name = "ic-front@3x", + filename = "Icon/Navigation/clothing/ic-front@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(947, 401), + imageId = sheetIds[1], + }, + ["ic-gear-on"] = { + name = "ic-gear-on", + filename = "Icon/Navigation/clothing/ic-gear-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1881, 1), + imageId = sheetIds[1], + }, + ["ic-gear-on@2x"] = { + name = "ic-gear-on@2x", + filename = "Icon/Navigation/clothing/ic-gear-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1125, 69), + imageId = sheetIds[1], + }, + ["ic-gear-on@3x"] = { + name = "ic-gear-on@3x", + filename = "Icon/Navigation/clothing/ic-gear-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1119, 401), + imageId = sheetIds[1], + }, + ["ic-gear"] = { + name = "ic-gear", + filename = "Icon/Navigation/clothing/ic-gear.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1658, 31), + imageId = sheetIds[1], + }, + ["ic-gear@2x"] = { + name = "ic-gear@2x", + filename = "Icon/Navigation/clothing/ic-gear@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(755, 185), + imageId = sheetIds[1], + }, + ["ic-gear@3x"] = { + name = "ic-gear@3x", + filename = "Icon/Navigation/clothing/ic-gear@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(861, 401), + imageId = sheetIds[1], + }, + ["ic-hair-on"] = { + name = "ic-hair-on", + filename = "Icon/Navigation/clothing/ic-hair-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1213, 1), + imageId = sheetIds[1], + }, + ["ic-hair-on@2x"] = { + name = "ic-hair-on@2x", + filename = "Icon/Navigation/clothing/ic-hair-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(523, 127), + imageId = sheetIds[1], + }, + ["ic-hair-on@3x"] = { + name = "ic-hair-on@3x", + filename = "Icon/Navigation/clothing/ic-hair-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(689, 401), + imageId = sheetIds[1], + }, + ["ic-hair"] = { + name = "ic-hair", + filename = "Icon/Navigation/clothing/ic-hair.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1688, 31), + imageId = sheetIds[1], + }, + ["ic-hair@2x"] = { + name = "ic-hair@2x", + filename = "Icon/Navigation/clothing/ic-hair@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(523, 185), + imageId = sheetIds[1], + }, + ["ic-hair@3x"] = { + name = "ic-hair@3x", + filename = "Icon/Navigation/clothing/ic-hair@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(603, 401), + imageId = sheetIds[1], + }, + ["ic-hat-on"] = { + name = "ic-hat-on", + filename = "Icon/Navigation/clothing/ic-hat-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1718, 31), + imageId = sheetIds[1], + }, + ["ic-hat-on@2x"] = { + name = "ic-hat-on@2x", + filename = "Icon/Navigation/clothing/ic-hat-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(349, 185), + imageId = sheetIds[1], + }, + ["ic-hat-on@3x"] = { + name = "ic-hat-on@3x", + filename = "Icon/Navigation/clothing/ic-hat-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(87, 401), + imageId = sheetIds[1], + }, + ["ic-hat"] = { + name = "ic-hat", + filename = "Icon/Navigation/clothing/ic-hat.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1058, 31), + imageId = sheetIds[1], + }, + ["ic-hat@2x"] = { + name = "ic-hat@2x", + filename = "Icon/Navigation/clothing/ic-hat@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(465, 185), + imageId = sheetIds[1], + }, + ["ic-hat@3x"] = { + name = "ic-hat@3x", + filename = "Icon/Navigation/clothing/ic-hat@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(517, 573), + imageId = sheetIds[1], + }, + ["ic-neck-on"] = { + name = "ic-neck-on", + filename = "Icon/Navigation/clothing/ic-neck-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1651, 1), + imageId = sheetIds[1], + }, + ["ic-neck-on@2x"] = { + name = "ic-neck-on@2x", + filename = "Icon/Navigation/clothing/ic-neck-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1357, 69), + imageId = sheetIds[1], + }, + ["ic-neck-on@3x"] = { + name = "ic-neck-on@3x", + filename = "Icon/Navigation/clothing/ic-neck-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(947, 487), + imageId = sheetIds[1], + }, + ["ic-neck"] = { + name = "ic-neck", + filename = "Icon/Navigation/clothing/ic-neck.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1478, 31), + imageId = sheetIds[1], + }, + ["ic-neck@2x"] = { + name = "ic-neck@2x", + filename = "Icon/Navigation/clothing/ic-neck@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(813, 243), + imageId = sheetIds[1], + }, + ["ic-neck@3x"] = { + name = "ic-neck@3x", + filename = "Icon/Navigation/clothing/ic-neck@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1182, 315), + imageId = sheetIds[1], + }, + ["ic-pants-on"] = { + name = "ic-pants-on", + filename = "Icon/Navigation/clothing/ic-pants-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1711, 1), + imageId = sheetIds[1], + }, + ["ic-pants-on@2x"] = { + name = "ic-pants-on@2x", + filename = "Icon/Navigation/clothing/ic-pants-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1415, 69), + imageId = sheetIds[1], + }, + ["ic-pants-on@3x"] = { + name = "ic-pants-on@3x", + filename = "Icon/Navigation/clothing/ic-pants-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(689, 487), + imageId = sheetIds[1], + }, + ["ic-pants"] = { + name = "ic-pants", + filename = "Icon/Navigation/clothing/ic-pants.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1388, 31), + imageId = sheetIds[1], + }, + ["ic-pants@2x"] = { + name = "ic-pants@2x", + filename = "Icon/Navigation/clothing/ic-pants@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(697, 243), + imageId = sheetIds[1], + }, + ["ic-pants@3x"] = { + name = "ic-pants@3x", + filename = "Icon/Navigation/clothing/ic-pants@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1291, 487), + imageId = sheetIds[1], + }, + ["ic-shirts-on"] = { + name = "ic-shirts-on", + filename = "Icon/Navigation/clothing/ic-shirts-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(793, 1), + imageId = sheetIds[1], + }, + ["ic-shirts-on@2x"] = { + name = "ic-shirts-on@2x", + filename = "Icon/Navigation/clothing/ic-shirts-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1045, 243), + imageId = sheetIds[1], + }, + ["ic-shirts-on@3x"] = { + name = "ic-shirts-on@3x", + filename = "Icon/Navigation/clothing/ic-shirts-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(580, 315), + imageId = sheetIds[1], + }, + ["ic-shirts"] = { + name = "ic-shirts", + filename = "Icon/Navigation/clothing/ic-shirts.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(763, 1), + imageId = sheetIds[1], + }, + ["ic-shirts@2x"] = { + name = "ic-shirts@2x", + filename = "Icon/Navigation/clothing/ic-shirts@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1393, 243), + imageId = sheetIds[1], + }, + ["ic-shirts@3x"] = { + name = "ic-shirts@3x", + filename = "Icon/Navigation/clothing/ic-shirts@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(752, 315), + imageId = sheetIds[1], + }, + ["ic-shoulders-on"] = { + name = "ic-shoulders-on", + filename = "Icon/Navigation/clothing/ic-shoulders-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1761, 1), + imageId = sheetIds[1], + }, + ["ic-shoulders-on@2x"] = { + name = "ic-shoulders-on@2x", + filename = "Icon/Navigation/clothing/ic-shoulders-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1277, 127), + imageId = sheetIds[1], + }, + ["ic-shoulders-on@3x"] = { + name = "ic-shoulders-on@3x", + filename = "Icon/Navigation/clothing/ic-shoulders-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(924, 315), + imageId = sheetIds[1], + }, + ["ic-shoulders"] = { + name = "ic-shoulders", + filename = "Icon/Navigation/clothing/ic-shoulders.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1063, 1), + imageId = sheetIds[1], + }, + ["ic-shoulders@2x"] = { + name = "ic-shoulders@2x", + filename = "Icon/Navigation/clothing/ic-shoulders@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1589, 69), + imageId = sheetIds[1], + }, + ["ic-shoulders@3x"] = { + name = "ic-shoulders@3x", + filename = "Icon/Navigation/clothing/ic-shoulders@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(838, 315), + imageId = sheetIds[1], + }, + ["ic-tshirts-on"] = { + name = "ic-tshirts-on", + filename = "Icon/Navigation/clothing/ic-tshirts-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(823, 1), + imageId = sheetIds[1], + }, + ["ic-tshirts-on@2x"] = { + name = "ic-tshirts-on@2x", + filename = "Icon/Navigation/clothing/ic-tshirts-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(291, 127), + imageId = sheetIds[1], + }, + ["ic-tshirts-on@3x"] = { + name = "ic-tshirts-on@3x", + filename = "Icon/Navigation/clothing/ic-tshirts-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(666, 315), + imageId = sheetIds[1], + }, + ["ic-tshirts"] = { + name = "ic-tshirts", + filename = "Icon/Navigation/clothing/ic-tshirts.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(703, 1), + imageId = sheetIds[1], + }, + ["ic-tshirts@2x"] = { + name = "ic-tshirts@2x", + filename = "Icon/Navigation/clothing/ic-tshirts@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(175, 127), + imageId = sheetIds[1], + }, + ["ic-tshirts@3x"] = { + name = "ic-tshirts@3x", + filename = "Icon/Navigation/clothing/ic-tshirts@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1096, 315), + imageId = sheetIds[1], + }, + ["ic-waist-on"] = { + name = "ic-waist-on", + filename = "Icon/Navigation/clothing/ic-waist-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(613, 1), + imageId = sheetIds[1], + }, + ["ic-waist-on@2x"] = { + name = "ic-waist-on@2x", + filename = "Icon/Navigation/clothing/ic-waist-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1625, 127), + imageId = sheetIds[1], + }, + ["ic-waist-on@3x"] = { + name = "ic-waist-on@3x", + filename = "Icon/Navigation/clothing/ic-waist-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1010, 315), + imageId = sheetIds[1], + }, + ["ic-waist"] = { + name = "ic-waist", + filename = "Icon/Navigation/clothing/ic-waist.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(643, 1), + imageId = sheetIds[1], + }, + ["ic-waist@2x"] = { + name = "ic-waist@2x", + filename = "Icon/Navigation/clothing/ic-waist@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(813, 127), + imageId = sheetIds[1], + }, + ["ic-waist@3x"] = { + name = "ic-waist@3x", + filename = "Icon/Navigation/clothing/ic-waist@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1787, 315), + imageId = sheetIds[1], + }, + ["ic-all-on"] = { + name = "ic-all-on", + filename = "Icon/Navigation/primary/ic-all-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(673, 1), + imageId = sheetIds[1], + }, + ["ic-all-on@2x"] = { + name = "ic-all-on@2x", + filename = "Icon/Navigation/primary/ic-all-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(871, 127), + imageId = sheetIds[1], + }, + ["ic-all-on@3x"] = { + name = "ic-all-on@3x", + filename = "Icon/Navigation/primary/ic-all-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1529, 315), + imageId = sheetIds[1], + }, + ["ic-all"] = { + name = "ic-all", + filename = "Icon/Navigation/primary/ic-all.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(481, 31), + imageId = sheetIds[1], + }, + ["ic-all@2x"] = { + name = "ic-all@2x", + filename = "Icon/Navigation/primary/ic-all@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1103, 185), + imageId = sheetIds[1], + }, + ["ic-all@3x"] = { + name = "ic-all@3x", + filename = "Icon/Navigation/primary/ic-all@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1721, 401), + imageId = sheetIds[1], + }, + ["ic-animations-on"] = { + name = "ic-animations-on", + filename = "Icon/Navigation/primary/ic-animations-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(913, 1), + imageId = sheetIds[1], + }, + ["ic-animations-on@2x"] = { + name = "ic-animations-on@2x", + filename = "Icon/Navigation/primary/ic-animations-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(987, 243), + imageId = sheetIds[1], + }, + ["ic-animations-on@3x"] = { + name = "ic-animations-on@3x", + filename = "Icon/Navigation/primary/ic-animations-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1635, 401), + imageId = sheetIds[1], + }, + ["ic-animations"] = { + name = "ic-animations", + filename = "Icon/Navigation/primary/ic-animations.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(883, 1), + imageId = sheetIds[1], + }, + ["ic-animations@2x"] = { + name = "ic-animations@2x", + filename = "Icon/Navigation/primary/ic-animations@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1277, 243), + imageId = sheetIds[1], + }, + ["ic-animations@3x"] = { + name = "ic-animations@3x", + filename = "Icon/Navigation/primary/ic-animations@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1807, 401), + imageId = sheetIds[1], + }, + ["ic-body-on"] = { + name = "ic-body-on", + filename = "Icon/Navigation/primary/ic-body-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(848, 31), + imageId = sheetIds[1], + }, + ["ic-body-on@2x"] = { + name = "ic-body-on@2x", + filename = "Icon/Navigation/primary/ic-body-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(59, 243), + imageId = sheetIds[1], + }, + ["ic-body-on@3x"] = { + name = "ic-body-on@3x", + filename = "Icon/Navigation/primary/ic-body-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1291, 401), + imageId = sheetIds[1], + }, + ["ic-body"] = { + name = "ic-body", + filename = "Icon/Navigation/primary/ic-body.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(788, 31), + imageId = sheetIds[1], + }, + ["ic-body@2x"] = { + name = "ic-body@2x", + filename = "Icon/Navigation/primary/ic-body@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1, 243), + imageId = sheetIds[1], + }, + ["ic-body@3x"] = { + name = "ic-body@3x", + filename = "Icon/Navigation/primary/ic-body@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1893, 401), + imageId = sheetIds[1], + }, + ["ic-clothing-on"] = { + name = "ic-clothing-on", + filename = "Icon/Navigation/primary/ic-clothing-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(698, 31), + imageId = sheetIds[1], + }, + ["ic-clothing-on@2x"] = { + name = "ic-clothing-on@2x", + filename = "Icon/Navigation/primary/ic-clothing-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1103, 127), + imageId = sheetIds[1], + }, + ["ic-clothing-on@3x"] = { + name = "ic-clothing-on@3x", + filename = "Icon/Navigation/primary/ic-clothing-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(517, 487), + imageId = sheetIds[1], + }, + ["ic-clothing"] = { + name = "ic-clothing", + filename = "Icon/Navigation/primary/ic-clothing.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(91, 31), + imageId = sheetIds[1], + }, + ["ic-clothing@2x"] = { + name = "ic-clothing@2x", + filename = "Icon/Navigation/primary/ic-clothing@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(871, 185), + imageId = sheetIds[1], + }, + ["ic-clothing@3x"] = { + name = "ic-clothing@3x", + filename = "Icon/Navigation/primary/ic-clothing@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(259, 487), + imageId = sheetIds[1], + }, + ["ic-costumes-on"] = { + name = "ic-costumes-on", + filename = "Icon/Navigation/primary/ic-costumes-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(637, 31), + imageId = sheetIds[1], + }, + ["ic-costumes-on@2x"] = { + name = "ic-costumes-on@2x", + filename = "Icon/Navigation/primary/ic-costumes-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1335, 127), + imageId = sheetIds[1], + }, + ["ic-costumes-on@3x"] = { + name = "ic-costumes-on@3x", + filename = "Icon/Navigation/primary/ic-costumes-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1205, 401), + imageId = sheetIds[1], + }, + ["ic-costumes"] = { + name = "ic-costumes", + filename = "Icon/Navigation/primary/ic-costumes.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(541, 31), + imageId = sheetIds[1], + }, + ["ic-costumes@2x"] = { + name = "ic-costumes@2x", + filename = "Icon/Navigation/primary/ic-costumes@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1815, 185), + imageId = sheetIds[1], + }, + ["ic-costumes@3x"] = { + name = "ic-costumes@3x", + filename = "Icon/Navigation/primary/ic-costumes@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(345, 401), + imageId = sheetIds[1], + }, + ["ic-recent-on"] = { + name = "ic-recent-on", + filename = "Icon/Navigation/primary/ic-recent-on.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1298, 31), + imageId = sheetIds[1], + }, + ["ic-recent-on@2x"] = { + name = "ic-recent-on@2x", + filename = "Icon/Navigation/primary/ic-recent-on@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(523, 243), + imageId = sheetIds[1], + }, + ["ic-recent-on@3x"] = { + name = "ic-recent-on@3x", + filename = "Icon/Navigation/primary/ic-recent-on@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1549, 487), + imageId = sheetIds[1], + }, + ["ic-recent"] = { + name = "ic-recent", + filename = "Icon/Navigation/primary/ic-recent.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(1358, 31), + imageId = sheetIds[1], + }, + ["ic-recent@2x"] = { + name = "ic-recent@2x", + filename = "Icon/Navigation/primary/ic-recent@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(175, 243), + imageId = sheetIds[1], + }, + ["ic-recent@3x"] = { + name = "ic-recent@3x", + filename = "Icon/Navigation/primary/ic-recent@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(1807, 487), + imageId = sheetIds[1], + }, + ["ic-close"] = { + name = "ic-close", + filename = "Icon/others/ic-close.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(121, 31), + imageId = sheetIds[1], + }, + ["ic-close@2x"] = { + name = "ic-close@2x", + filename = "Icon/others/ic-close@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1699, 185), + imageId = sheetIds[1], + }, + ["ic-close@3x"] = { + name = "ic-close@3x", + filename = "Icon/others/ic-close@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(345, 487), + imageId = sheetIds[1], + }, + ["ic-collapse"] = { + name = "ic-collapse", + filename = "Icon/others/ic-collapse.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(31, 31), + imageId = sheetIds[1], + }, + ["ic-collapse@2x"] = { + name = "ic-collapse@2x", + filename = "Icon/others/ic-collapse@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(59, 185), + imageId = sheetIds[1], + }, + ["ic-collapse@3x"] = { + name = "ic-collapse@3x", + filename = "Icon/others/ic-collapse@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(87, 487), + imageId = sheetIds[1], + }, + ["ic-down-arrow-off"] = { + name = "ic-down-arrow-off", + filename = "Icon/others/ic-down-arrow-off.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(391, 31), + imageId = sheetIds[1], + }, + ["ic-down-arrow-off@2x"] = { + name = "ic-down-arrow-off@2x", + filename = "Icon/others/ic-down-arrow-off@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1, 185), + imageId = sheetIds[1], + }, + ["ic-down-arrow-off@3x"] = { + name = "ic-down-arrow-off@3x", + filename = "Icon/others/ic-down-arrow-off@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(259, 401), + imageId = sheetIds[1], + }, + ["ic-down-arrow"] = { + name = "ic-down-arrow", + filename = "Icon/others/ic-down-arrow.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(361, 31), + imageId = sheetIds[1], + }, + ["ic-down-arrow@2x"] = { + name = "ic-down-arrow@2x", + filename = "Icon/others/ic-down-arrow@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1973, 127), + imageId = sheetIds[1], + }, + ["ic-down-arrow@3x"] = { + name = "ic-down-arrow@3x", + filename = "Icon/others/ic-down-arrow@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(431, 401), + imageId = sheetIds[1], + }, + ["ic-expand"] = { + name = "ic-expand", + filename = "Icon/others/ic-expand.png", + imageRectSize = Vector2.new(28, 28), + imageRectOffset = Vector2.new(421, 31), + imageId = sheetIds[1], + }, + ["ic-expand@2x"] = { + name = "ic-expand@2x", + filename = "Icon/others/ic-expand@2x.png", + imageRectSize = Vector2.new(56, 56), + imageRectOffset = Vector2.new(1799, 127), + imageId = sheetIds[1], + }, + ["ic-expand@3x"] = { + name = "ic-expand@3x", + filename = "Icon/others/ic-expand@3x.png", + imageRectSize = Vector2.new(84, 84), + imageRectOffset = Vector2.new(173, 401), + imageId = sheetIds[1], + }, + +} + +if Flags:GetFlag("AvatarEditorWhiteLineFix") then + sprites["btn-toggle@3x"] = { + name = "btn-toggle@3x", + filename = "graphic/switch/btn-toggle@3x.png", + imageRectSize = Vector2.new(74.9, 72), + imageRectOffset = Vector2.new(1, 315), + imageId = sheetIds[1], + } +end + +-- +-- api +local guiService = game:GetService'GuiService' + +local function getDescendants(obj, t) + t = t or {} + for _, v in next, obj:GetChildren() do + table.insert(t, v) + getDescendants(v, t) + end + return t +end + +local this = {} +this.enabled = true +this.all = function() + return sprites +end +this.info = function(name) + return sprites[name] +end +this.getScale = function() + return SIMULATE_RESOLUTION_SCALE or guiService:GetResolutionScale() +end +this.equipDescendants = function(gui, name) + for _, obj in next, getDescendants(gui) do + if obj.Name == 'SpriteName' + and obj.ClassName == 'StringValue' + and obj.Parent + and not obj.Parent:FindFirstChild'DontMagicallyAssignToSpriteSheet' then + this.equip(obj.Parent, obj.Value) + + if obj.Parent:FindFirstChild'SpriteIsReversed' then + obj.Parent.ImageRectOffset = obj.Parent.ImageRectOffset + Vector2.new(obj.Parent.ImageRectSize.x, 0) + obj.Parent.ImageRectSize = obj.Parent.ImageRectSize * Vector2.new(-1, 1) + end + end + end +end +this.equip = function(gui, name, doNotMutateSprite) + if not doNotMutateSprite and not gui:FindFirstChild'DoNotMutateSprite' then + pcall(function() + local tryMutation = function(n) + local scaleName = name..'@'..n..'x' + if sprites[scaleName] then + if gui.SliceCenter ~= Rect.new() then + gui.SliceCenter = Rect.new( + gui.SliceCenter.Min.x*n, gui.SliceCenter.Min.y*n, + gui.SliceCenter.Max.x*n, gui.SliceCenter.Max.y*n + ) + end + + return scaleName + end + end + + local resScale = this.getScale() + if resScale > 2 then + name = tryMutation(3) or tryMutation(2) or name + elseif resScale > 1 then + name = tryMutation(2) or name + end + end) + end + + if sprites[name] then + local content = sprites[name].imageId + + gui.ImageRectSize = sprites[name].imageRectSize + gui.ImageRectOffset = sprites[name].imageRectOffset + gui.Image = content + else + warn('BAD SPRITE NAME: "'..tostring(name)..'". Tried to equip to '..gui:GetFullName()) + gui.ImageRectSize = Vector2.new(0, 0) + gui.ImageRectOffset = Vector2.new(0, 0) + gui.Image = 'rbxasset://textures/ui/ErrorIcon.png' + end +end + +return this \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/TabList.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/TabList.lua new file mode 100644 index 0000000..fc1b713 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/TabList.lua @@ -0,0 +1,433 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) +local SelectCategoryTab = require(Modules.LuaApp.Actions.SelectCategoryTab) + +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfo) +local SpriteManager = require(Modules.LuaApp.Legacy.AvatarEditor.SpriteSheetManager) +local Categories = require(Modules.LuaApp.Legacy.AvatarEditor.Categories) +local TweenController = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController) +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) + +local AvatarEditorTabResizing = Flags:GetFlag("AvatarEditorTabResizing") +local AvatarEditorDefaultTabSelection = Flags:GetFlag("AvatarEditorDefaultTabSelection") +local GetButtonClickEvent = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonClickEvent) + +--Constants +local TAB_WIDTH = LayoutInfo.TabWidth +local TAB_HEIGHT = LayoutInfo.TabHeight +local FIRST_TAB_BONUS_WIDTH = LayoutInfo.FirstTabBonusWidth + +--Mutables +local this = {} +local tabList = nil +local tabListContainer = nil +local tabButtons = {} +local currentTabButton = nil +local currentPage = nil + +local function initTabList() + if LayoutInfo.isLandscape then + tabListContainer.Visible = true + +if AvatarEditorTabResizing then + -- When cleaning up FFlagAvatarEditorTabResizing, consider removing the scroll button from the place. + tabListContainer.ScrollButtonDown.Parent = nil + tabList.ScrollingEnabled = true +end + + if SpriteManager.getScale() >= 2 then + tabListContainer.TabListBackground.Size = UDim2.new(1, 12, 1, 0) + tabListContainer.TabListBackground.Position = UDim2.new(0, -6, 0, 0) +if not AvatarEditorTabResizing then + tabListContainer.ScrollButtonDown.BackgroundClipper.Background.Size = UDim2.new(1, 12, 2, 0) + tabListContainer.ScrollButtonDown.BackgroundClipper.Background.Position = UDim2.new(0, -6, -1, 0) +end + else + tabListContainer.TabListBackground.Size = UDim2.new(1, 6, 1, 3) + tabListContainer.TabListBackground.Position = UDim2.new(0, -3, 0, 0) + end + + Flags:GetFlag("AvatarEditorTabResizing") + + tabList.Position = UDim2.new(0, 0, 0, 0) + tabList.Size = UDim2.new(1, 0, 1, -6) + tabList.BackgroundTransparency = 1 + tabList.ClipsDescendants = true + tabList.Parent = tabListContainer + +if not AvatarEditorTabResizing then + tabListContainer.ScrollButtonDown.Visible = false + + GetButtonClickEvent(tabListContainer.ScrollButtonDown.Button):connect(function() + local current = tabList.CanvasPosition.y + local target = current + (TAB_HEIGHT+1) + local max = tabList.CanvasSize.Y.Offset - tabList.AbsoluteWindowSize.y + local newPos = Vector2.new(0, math.min(max, target)) + local tweenInfo = TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + TweenController(tabList, tweenInfo, { CanvasPosition = newPos }) + end) + + local currentAtBottom = nil + tabList.Changed:connect(function(prop) + if prop == 'CanvasSize' or prop == 'CanvasPosition' or prop == 'AbsoluteSize' then + local atBottom = tabList.CanvasPosition.y + tabList.AbsoluteSize.y >= tabList.CanvasSize.Y.Offset + + if currentAtBottom ~= atBottom then + tabListContainer.ScrollButtonDown.Button.Visible = not atBottom + local tweenInfo = TweenInfo.new(0.2, Enum.EasingStyle.Quad) + local propGoals = { ImageTransparency = atBottom and 1 or 0 } + TweenController(tabListContainer.ScrollButtonDown.Gradient, tweenInfo, propGoals) + TweenController(tabListContainer.ScrollButtonDown.Arrow, tweenInfo, propGoals) + TweenController(tabListContainer.ScrollButtonDown.Button, tweenInfo, propGoals) + TweenController(tabListContainer.ScrollButtonDown.BackgroundClipper.Background, tweenInfo, propGoals) + + currentAtBottom = atBottom + end + end + end) + end +end + +end + + +local function setScrollButtonVisible(visible) +if not AvatarEditorTabResizing then + if LayoutInfo.isLandscape then + tabListContainer.ScrollButtonDown.Visible = visible + end +end +end + + +local function selectTab(index, desiredPage) + local desiredTabButton = tabButtons[index] + desiredTabButton.BackgroundColor3 = Color3.fromRGB(246, 136, 2) + + local avatarType = AppState.Store:getState().Character.AvatarType + if desiredPage.specialPageType == 'Scale' and avatarType == 'R6' then + desiredTabButton.BackgroundColor3 = Color3.fromRGB(246*.66, 136*.66, 2*.66) + end + local imageLabel = desiredTabButton:FindFirstChild('ImageLabel') + if imageLabel then + SpriteManager.equip(imageLabel, desiredPage.iconImageSelectedName) + end + local textLabel = desiredTabButton:FindFirstChild('TextLabel') + if textLabel then + textLabel.TextColor3 = Color3.fromRGB(255, 255, 255) + end + + if LayoutInfo.isLandscape then + desiredTabButton.BackgroundTransparency = 0 + end + + if currentTabButton then + currentTabButton.BackgroundColor3 = Color3.fromRGB(255, 255, 255) + if currentPage.specialPageType == 'Scale' and avatarType == 'R6' then + currentTabButton.BackgroundColor3 = Color3.fromRGB(255*.66, 255*.66, 255*.66) + end + local imageLabel = currentTabButton:FindFirstChild('ImageLabel') + if imageLabel then + SpriteManager.equip(imageLabel, currentPage.iconImageName) + end + local textLabel = currentTabButton:FindFirstChild('TextLabel') + if textLabel then + textLabel.TextColor3 = Color3.fromRGB(137, 137, 137) + end + + if LayoutInfo.isLandscape then + currentTabButton.BackgroundTransparency = 1 + end + end + + currentTabButton = desiredTabButton + currentPage = desiredPage +end + + +local function renderTabButton(index, page, categoryIndex) + local tabButton = Instance.new('ImageButton') + tabButton.Name = 'Tab'..page.name + tabButton.Image = '' + tabButton.BackgroundColor3 = Color3.fromRGB(255, 255, 255) + if LayoutInfo.isLandscape then + tabButton.BackgroundTransparency = 1 + end + + local avatarType = AppState.Store:getState().Character.AvatarType + + if page.specialPageType == 'Scale' and avatarType == 'R6' then + tabButton.BackgroundColor3 = Color3.fromRGB(255*.66, 255*.66, 255*.66) + end + tabButton.BorderSizePixel = 0 + tabButton.AutoButtonColor = false + +if AvatarEditorTabResizing then + local MIN_TAB_HEIGHT = 80 + local MAX_TAB_WIDTH = 72 + + -- Compute a hight/width to allow a half-integer number of tabs to be visible + -- at first. The idea is to indicate to the user that the view scrolls. + if LayoutInfo.isLandscape then + local space = tabList.Contents.Parent.AbsoluteSize.y - FIRST_TAB_BONUS_WIDTH + local tabNum = space / MIN_TAB_HEIGHT + TAB_HEIGHT = space / ((2 * math.floor(tabNum + 0.5) - 1) / 2) + else + local space = tabList.Contents.Parent.AbsoluteSize.x - FIRST_TAB_BONUS_WIDTH + local tabNum = space / MAX_TAB_WIDTH + TAB_WIDTH = space / ((2 * math.ceil(tabNum - 0.5) + 1) / 2) + end +end + + if LayoutInfo.isLandscape then + tabButton.Size = UDim2.new(1, 0, 0, TAB_HEIGHT) + tabButton.Position = UDim2.new(0, 0, 0, (index-1) * (TAB_HEIGHT+1) + FIRST_TAB_BONUS_WIDTH) + else + tabButton.Size = UDim2.new(0, TAB_WIDTH, 0, TAB_HEIGHT) + tabButton.Position = UDim2.new(0, (index-1) * (TAB_WIDTH+1) + FIRST_TAB_BONUS_WIDTH, 0, 0) + end + + if index == 1 then + if LayoutInfo.isLandscape then + tabButton.Size = UDim2.new(1, 0, 0, TAB_HEIGHT + FIRST_TAB_BONUS_WIDTH) + tabButton.Position = UDim2.new(0, 0, 0, (index-1) * (TAB_HEIGHT+1)) + else + tabButton.Size = UDim2.new(0, TAB_WIDTH + FIRST_TAB_BONUS_WIDTH, 0, TAB_HEIGHT) + tabButton.Position = UDim2.new(0, (index-1) * (TAB_WIDTH+1), 0, 0) + end + end + + tabButton.ZIndex = tabList.ZIndex + tabButton.Parent = tabList.Contents + + if page.iconImage == '' then + --If tab has no image, then use placeholder text + local nameLabel = Instance.new('TextLabel') + nameLabel.BackgroundTransparency = 1 + nameLabel.Size = UDim2.new(1, 0, 1, 0) + nameLabel.Text = page.title + nameLabel.Font = Enum.Font.SourceSansBold + nameLabel.FontSize = 'Size14' + nameLabel.TextColor3 = Color3.fromRGB(25, 25, 25) + nameLabel.ZIndex = tabButton.ZIndex + nameLabel.Parent = tabButton + else + local imageFrame = Instance.new('ImageLabel') + imageFrame.BackgroundTransparency = 1 + imageFrame.Size = UDim2.new(0, 28, 0, 28) + imageFrame.Position = UDim2.new(.5, -14, .5, -14) + if index == 1 then + if LayoutInfo.isLandscape then + imageFrame.Position = imageFrame.Position + UDim2.new(0, 0, 0, FIRST_TAB_BONUS_WIDTH * 0.5) + else + imageFrame.Position = imageFrame.Position + UDim2.new(0, FIRST_TAB_BONUS_WIDTH * .5, 0, 0) + end + end + SpriteManager.equip(imageFrame, page.iconImageName) + imageFrame.ZIndex = tabButton.ZIndex + imageFrame.Parent = tabButton + + if LayoutInfo.isLandscape then + imageFrame.Position = imageFrame.Position + UDim2.new(0, 0, 0, -6) + + local nameLabel = Instance.new('TextLabel') + nameLabel.BackgroundTransparency = 1 + nameLabel.Position = UDim2.new( + 0, + 0, + imageFrame.Position.Y.Scale, + imageFrame.Position.Y.Offset+imageFrame.Size.Y.Offset+4) + + nameLabel.Size = UDim2.new(1,0,1,0) + nameLabel.Text = page.titleLandscape or page.title + nameLabel.Font = Enum.Font.SourceSans + nameLabel.FontSize = 'Size14' + nameLabel.TextColor3 = Color3.fromRGB(137, 137, 137) + nameLabel.TextXAlignment = 'Center' + nameLabel.TextYAlignment = 'Top' + nameLabel.ZIndex = tabButton.ZIndex + nameLabel.Parent = tabButton + end + end + + if index > 1 then + local divider = Instance.new('Frame') + divider.Name = 'Divider' + divider.BackgroundColor3 = Color3.fromRGB(227, 227, 227) + divider.BorderSizePixel = 0 + if LayoutInfo.isLandscape then + divider.AnchorPoint = Vector2.new(0.5, 0) + divider.Size = UDim2.new(1, -12, 0, 1) + divider.Position = UDim2.new(0.5, 0, 0, (index-1) * (TAB_HEIGHT+1) - 1 + FIRST_TAB_BONUS_WIDTH) + else + divider.Size = UDim2.new(0, 1, .6, 0) + divider.Position = UDim2.new(0, (index-1) * (TAB_WIDTH+1) - 1 + FIRST_TAB_BONUS_WIDTH, .2, 0) + end + divider.ZIndex = tabButton.ZIndex + 1 + divider.Parent = tabList.Contents + end + + GetButtonClickEvent(tabButton):connect(function() + if currentTabButton ~= tabButton then + selectTab(index, page) + AppState.Store:dispatch(SelectCategoryTab(categoryIndex, index, tabList.CanvasPosition)) + end + end) + + return tabButton +end + +local function changeCategory(categoryIndex) + local category = Categories[categoryIndex] + tabList.Contents:ClearAllChildren() + + if LayoutInfo.isLandscape then + tabList.CanvasSize = UDim2.new(0, 0, 0, #(category.pages) * (TAB_HEIGHT+1) + FIRST_TAB_BONUS_WIDTH - 1) + else + tabList.CanvasSize = UDim2.new(0, #(category.pages) * (TAB_WIDTH+1) + FIRST_TAB_BONUS_WIDTH, 0, 0) + end + tabList.CanvasPosition = Vector2.new(0,0) + + tabButtons = {} + for index, page in pairs(category.pages) do + local tabButton = renderTabButton(index, page, categoryIndex) + table.insert(tabButtons, tabButton) + end + + if #category.pages > 0 then + local tabInfo = AppState.Store:getState().Category.TabsInfo[categoryIndex] + if tabInfo and tabInfo.TabIndex and tabInfo.Position then + selectTab(tabInfo.TabIndex, category.pages[tabInfo.TabIndex]) + tabList.CanvasPosition = tabInfo.Position + else + selectTab(1, category.pages[1]) + end + end + +if not AvatarEditorTabResizing then + if LayoutInfo.isLandscape then + setScrollButtonVisible(#category.pages > 6) + end +end + +end + +local function getPageName() + local categoryIndex = AppState.Store:getState().Category.CategoryIndex + local tabsInfo = categoryIndex and AppState.Store:getState().Category.TabsInfo[categoryIndex] + local tabIndex = tabsInfo and tabsInfo.TabIndex + local defaultSelection = "" + + --[[ AvatarEditorDefaultTabSelection fixes issue where tab 1 is initially selected as a fallback when changeCategory() calls selectCategory() due to + AppState.Store:getState().Category.TabsInfo[categoryIndex] being nil. so this function also needs to use a fallack. currentPage being a + good option as it is set in selectCategory() --]] + if AvatarEditorDefaultTabSelection then + defaultSelection = currentPage and currentPage.name or defaultSelection + end + + local pageName = (categoryIndex and tabIndex) and Categories[categoryIndex].pages[tabIndex].name or defaultSelection + return pageName +end + + +return function(CategoryMenu, inTabList, inTabListContainer) + tabList = inTabList + tabListContainer = inTabListContainer + + initTabList() + + local tweenInfo = TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + local tweenInfoFast = TweenInfo.new(0.1, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + + local imageInvisible = { ImageTransparency = 1 } + local backgroundInvisible = { BackgroundTransparency = 1 } + local textInvisible = { TextTransparency = 1 } + + local imageVisible = { ImageTransparency = 0 } + local backgroundVisible = { BackgroundTransparency = 0 } + local textVisible = { TextTransparency = 0 } + + CategoryMenu.openCategoryMenuEvent:Connect(function() + if LayoutInfo.isLandscape then + TweenController(tabListContainer.TabListBackground, tweenInfo, imageInvisible) +if not AvatarEditorTabResizing then + TweenController(tabListContainer.ScrollButtonDown.Arrow, tweenInfo, imageInvisible) +end + + for _, obj in next, tabList.Contents:GetChildren() do + if obj.ClassName == 'ImageButton' then + -- tab button + if obj.Name == 'Tab'..getPageName() then + local objTweenInfo = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + TweenController(obj, objTweenInfo, backgroundInvisible) + else + local objTweenInfo = TweenInfo.new(0) + TweenController(obj, objTweenInfo, backgroundInvisible) + end + + TweenController(obj.ImageLabel, tweenInfo, imageInvisible) + TweenController(obj.TextLabel, tweenInfo, textInvisible) + else + TweenController(obj, tweenInfo, backgroundInvisible) + end + end + +if not AvatarEditorTabResizing then + tabListContainer.ScrollButtonDown.Button.Visible = false + + TweenController(tabListContainer.ScrollButtonDown.Gradient, tweenInfoFast, imageInvisible) + TweenController(tabListContainer.ScrollButtonDown.BackgroundClipper.Background, tweenInfoFast, imageInvisible) +end + end + end) + + CategoryMenu.closeCategoryMenuEvent:Connect(function() + if LayoutInfo.isLandscape then + for _, obj in next, tabList.Contents:GetChildren() do + if obj.ClassName == 'ImageButton' then + if obj.Name == 'Tab'..getPageName() then + TweenController(obj, tweenInfo, backgroundVisible, backgroundInvisible) + end + TweenController(obj.ImageLabel, tweenInfo, imageVisible, imageInvisible) + TweenController(obj.TextLabel, tweenInfo, textVisible, textInvisible) + else + TweenController(obj, tweenInfo, backgroundVisible, backgroundInvisible) + end + end + +if not AvatarEditorTabResizing then + local atBottom = tabList.CanvasPosition.y + tabList.AbsoluteSize.y >= tabList.CanvasSize.Y.Offset + if not atBottom then + tabListContainer.ScrollButtonDown.Button.Visible = true + TweenController(tabListContainer.ScrollButtonDown.Gradient, tweenInfo, imageVisible) + TweenController(tabListContainer.ScrollButtonDown.BackgroundClipper.Background, tweenInfo, imageVisible) + TweenController(tabListContainer.ScrollButtonDown.Arrow, tweenInfo, imageVisible) + end +end + + TweenController( + tabListContainer.TabListBackground, tweenInfo, imageVisible, imageInvisible).Completed:Connect( + function() + if not LayoutInfo.isLandscape then + local objTweenInfo = TweenInfo.new(0) + for _, obj in next, tabList.Contents:GetChildren() do + if obj.ClassName == 'ImageButton' then + TweenController(obj, objTweenInfo, backgroundVisible, backgroundInvisible) + end + end + end + end + ) + end + end) + + AppState.Store.Changed:Connect(function(newState, oldState) + if newState.Category.CategoryIndex ~= oldState.Category.CategoryIndex then + changeCategory(newState.Category.CategoryIndex) + end + end) + + + return this +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/TabListConsole.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/TabListConsole.lua new file mode 100644 index 0000000..41c75db --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/TabListConsole.lua @@ -0,0 +1,345 @@ +local GuiService = game:GetService("GuiService") + +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local ShellModules = Modules:FindFirstChild("Shell") + +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) +local SetConsoleMenuLevel = require(Modules.LuaApp.Actions.SetConsoleMenuLevel) +local SelectCategoryTab = require(Modules.LuaApp.Actions.SelectCategoryTab) + +local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfoConsole) +local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local Categories = require(Modules.LuaApp.Legacy.AvatarEditor.Categories) +local TweenController = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController) +local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) +local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) +local GetButtonClickEvent = require(Modules.LuaApp.Legacy.AvatarEditor.GetButtonClickEvent) + +local BUTTON_INTERVAL = LayoutInfo.CategoryButtonDefaultSize.Y.Offset + LayoutInfo.CategoryButtonsPadding + +local function createTabList(container, pageManager) + local this = {} + + local tabButtons = {} + local selectedTabIndex = 1 + + local SELECTOR_TOP_MIN_DISTANCE = LayoutInfo.SelectorTopMinDistance + local SELECTOR_BOTTOM_MIN_DISTANCE = LayoutInfo.SelectorBottomMinDistance + + local storeChangedCn = nil + + local TabList = Utilities.create'ScrollingFrame' + { + Name = 'TabList'; + Position = LayoutInfo.TabListPosition; + Size = UDim2.new(0, 360, 1, 0); + CanvasSize = UDim2.new(1, 0, 0, 0); + CanvasPosition = Vector2.new(0, 0); + BackgroundTransparency = 1; + ScrollingEnabled = false; + Selectable = false; + BorderSizePixel = 0; + ScrollBarThickness = 0; + Parent = container; + Visible = false; + } + + local buttonSelector = Utilities.create'ImageLabel' + { + Name = 'TabSelector'; + Image = LayoutInfo.CategoryButtonSelectorImage; + Position = UDim2.new(0, -7, 0, -7); + Size = UDim2.new(1, 14, 1, 14); + BackgroundTransparency = 1; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(31, 31, 63, 63); + } + + local function getCategoryIndex() + return AppState.Store:getState().Category.CategoryIndex + end + + local function tweenTabListState(isFullView) + local tweenInfo = TweenInfo.new(LayoutInfo.DefaultTweenTime, + Enum.EasingStyle.Quad, + Enum.EasingDirection.InOut, + 0, false, 0) + + if isFullView then + TweenController(TabList, tweenInfo, { Position = LayoutInfo.TabListFullviewPosition }) + else + TweenController(TabList, tweenInfo, { Position = LayoutInfo.TabListPosition }) + end + end + + -- Default tabButton style + local function defaultTabButton(tabButton, tweenTransparency) + tabButton.Image = LayoutInfo.CategoryButtonImageDefault + tabButton.TabText.TextColor3 = LayoutInfo.WhiteTextColor + + if tweenTransparency then + local tweenInfo = TweenInfo.new( + LayoutInfo.DefaultTweenTime, + Enum.EasingStyle.Quad, + Enum.EasingDirection.InOut, 0, false, 0) + + TweenController(tabButton, tweenInfo, { ImageTransparency = 0 }, { ImageTransparency = 1 }) + TweenController(tabButton.TabText, tweenInfo, { TextTransparency = 0 }, { TextTransparency = 1 }) + else + tabButton.TabText.TextTransparency = 0 + end + end + + -- Navigate on tabButton to highlight tabButton + local function highlightTabButton(tabButton, tweenTransparency) + tabButton.Image = LayoutInfo.CategoryButtonImageSelected + tabButton.TabText.TextColor3 = LayoutInfo.GrayTextColor + + if tweenTransparency then + local tweenInfo = TweenInfo.new( + LayoutInfo.DefaultTweenTime, + Enum.EasingStyle.Quad, + Enum.EasingDirection.InOut, 0, false, 0) + + TweenController(tabButton, tweenInfo, { ImageTransparency = 0 }, { ImageTransparency = 1 }) + TweenController(tabButton.TabText, tweenInfo, { TextTransparency = 0 }, { TextTransparency = 1 }) + else + tabButton.TabText.TextTransparency = 0 + end + end + + -- Inactive tabButton style + local function inactiveTabButton(tabButton) + tabButton.TabText.TextTransparency = 0.5 + end + + -- Active TabList style + local function activeTabList(tweenTransparency) + for i = 1, #tabButtons do + if selectedTabIndex ~= i then + defaultTabButton(tabButtons[i], tweenTransparency) + else + highlightTabButton(tabButtons[i], tweenTransparency) + end + end + end + + -- Inactive Tablist style + local function inactiveTabList() + for i = 1, #tabButtons do + if selectedTabIndex ~= i then + inactiveTabButton(tabButtons[i]) + end + end + end + + -- Open one of the tabButtons + local function openTab() + inactiveTabList() + GuiService.SelectedCoreObject = nil + + -- Select first gui in pageManager + pageManager:focusOnCard() + end + + -- Close tab and back to navigation between all tabButtons + local function closeTab(tweenTransparency) + activeTabList(tweenTransparency) + GuiService.SelectedCoreObject = tabButtons[selectedTabIndex] + end + + local function getCanvasPositionGoal(tabButton, tabIndex) + local topDistance = tabButton.AbsolutePosition.Y + local bottomDistance = container.AbsoluteSize.Y - topDistance - tabButton.AbsoluteSize.Y + local canvasPositionGoal = TabList.CanvasPosition + + if bottomDistance < SELECTOR_BOTTOM_MIN_DISTANCE then + canvasPositionGoal = Vector2.new(0, (tabIndex - 6) * BUTTON_INTERVAL) + elseif topDistance < SELECTOR_TOP_MIN_DISTANCE then + canvasPositionGoal = Vector2.new(0, (tabIndex - 1) * BUTTON_INTERVAL) + end + return canvasPositionGoal + end + + -- Check if TabList needs to scroll up/down + local function tweenCanvas() + local canvasPositionGoal = getCanvasPositionGoal(GuiService.SelectedCoreObject, selectedTabIndex) + if canvasPositionGoal ~= TabList.CanvasPosition then + local tweenInfo = + TweenInfo.new( + LayoutInfo.DefaultTweenTime, + Enum.EasingStyle.Quad, + Enum.EasingDirection.InOut, 0, false, 0) + TweenController(TabList, tweenInfo, { CanvasPosition = canvasPositionGoal }) + end + end + + local function selectTab(newTabButton, oldTabButton) + if oldTabButton then + defaultTabButton(oldTabButton) + end + highlightTabButton(newTabButton) + tweenCanvas() + end + + local function createTabButton(index, title) + local TabButton = Utilities.create'ImageButton' + { + Name = 'Tab'..index; + Position = UDim2.new(0, 0, 0, index * BUTTON_INTERVAL); + Size = LayoutInfo.CategoryButtonDefaultSize; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = LayoutInfo.CategoryButtonImageDefault; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(8, 8, 9, 9); + Selectable = true; + SelectionImageObject = buttonSelector; + Parent = TabList; + } + + local MoveSelectionSound = SoundManager:CreateSound('MoveSelection') + MoveSelectionSound.Parent = TabButton + + Utilities.create'TextLabel' + { + Name = 'TabText'; + Position = UDim2.new(0, 20, 0, 0); + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Text = title; + TextXAlignment = 'Left'; + TextColor3 = LayoutInfo.WhiteTextColor; + TextSize = LayoutInfo.ButtonFontSize; + Font = LayoutInfo.RegularFont; + Parent = TabButton; + } + + TabButton.SelectionGained:connect(function() + if selectedTabIndex ~= index then + local categoryIndex = getCategoryIndex() + local canvasPosition = getCanvasPositionGoal(TabButton, index) + AppState.Store:dispatch(SelectCategoryTab(categoryIndex, index, canvasPosition)) + end + end) + + GetButtonClickEvent(TabButton):connect(function() + if pageManager:hasAssets() then + SoundManager:Play('OverlayOpen') + AppState.Store:dispatch(SetConsoleMenuLevel(LayoutInfo.ConsoleMenuLevel.AssetsPage)) + end + end) + + return TabButton + end + + local function createTabButtons(categoryIndex) + local tabPages = Categories[categoryIndex].pages + + -- Remove redundant tabButtons + while #tabButtons > #tabPages do + tabButtons[#tabButtons]:Destroy() + table.remove(tabButtons) + end + + if #tabPages < 1 then + return + end + + -- Update existing tabButtons or create new tabButtons + for i = 1, #tabPages do + if tabButtons[i] then + tabButtons[i].TabText.Text = tabPages[i].titleConsole or tabPages[i].title + else + local tabButton = createTabButton(i, tabPages[i].titleConsole or tabPages[i].title) + table.insert(tabButtons, tabButton) + end + end + end + + local function openCategoryMenu() + TabList.Visible = true + closeTab(true) + end + + local function closeCategoryMenu() + TabList.Visible = false + end + + local function update(newState, oldState) + if newState.ConsoleMenuLevel ~= oldState.ConsoleMenuLevel then + if newState.ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.CategoryMenu then + closeCategoryMenu() + elseif newState.ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.TabList then + if oldState.ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.CategoryMenu then + openCategoryMenu() + elseif oldState.ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.AssetsPage then + closeTab() + end + elseif newState.ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.AssetsPage then + openTab() + end + end + + if newState.Category.CategoryIndex ~= oldState.Category.CategoryIndex then + local categoryIndex = newState.Category.CategoryIndex + if categoryIndex then + createTabButtons(categoryIndex) + + local tabsNum = #(Categories[categoryIndex].pages) + local canvasSizeY = math.max(TabList.AbsoluteWindowSize.Y, + (tabsNum - 6) * BUTTON_INTERVAL + TabList.AbsoluteWindowSize.Y) + TabList.CanvasSize = UDim2.new(1, 0, 0, canvasSizeY); + + local tabInfo = newState.Category.TabsInfo[categoryIndex] + if tabInfo and tabInfo.TabIndex and tabInfo.Position then + selectedTabIndex = tabInfo.TabIndex + TabList.CanvasPosition = tabInfo.Position + else + selectedTabIndex = 1 + TabList.CanvasPosition = Vector2.new(0, 0) + end + end + end + + if newState.Category.TabsInfo ~= oldState.Category.TabsInfo then + local oldTabIndex = selectedTabIndex + local tabInfo = newState.Category.TabsInfo[newState.Category.CategoryIndex] + if tabInfo and tabInfo.TabIndex and tabInfo.Position then + selectedTabIndex = tabInfo.TabIndex + else + selectedTabIndex = 1 + end + selectTab(tabButtons[selectedTabIndex], tabButtons[oldTabIndex]) + end + + if newState.FullView ~= oldState.FullView then + tweenTabListState(newState.FullView) + end + end + + function this:SelectNextPage() + local nextIndex = math.min(selectedTabIndex + LayoutInfo.CategoryButtonRowsPerPage, #tabButtons) + GuiService.SelectedCoreObject = tabButtons[nextIndex] + end + + function this:SelectPreviousPage() + local prevIndex = math.max(selectedTabIndex - LayoutInfo.CategoryButtonRowsPerPage, 1) + GuiService.SelectedCoreObject = tabButtons[prevIndex] + end + + function this:Focus() + storeChangedCn = AppState.Store.Changed:Connect(update) + GuiService:AddSelectionParent("Tablist", TabList) + end + + function this:RemoveFocus() + storeChangedCn = Utilities.disconnectEvent(storeChangedCn) + GuiService:RemoveSelectionGroup("Tablist") + end + + return this +end + +return createTabList diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/TweenInstanceController.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/TweenInstanceController.lua new file mode 100644 index 0000000..dac3b09 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/TweenInstanceController.lua @@ -0,0 +1,26 @@ +local TweenService = game:GetService('TweenService') + +local function createTween(obj, tweenInfo, propGoals, propStarts) + if obj and obj:IsA("Instance") and tweenInfo and next(propGoals) ~= nil then + local tweenGoals = {} + for prop, propGoal in pairs(propGoals) do + tweenGoals[prop] = propGoal + end + + if propStarts and next(propStarts) ~= nil then + for prop, propStart in pairs(propStarts) do + obj[prop] = propStart + end + end + + local tween = TweenService:Create(obj, tweenInfo, tweenGoals) + tween.Completed:Connect(function() + tween = nil + end) + tween:Play() + + return tween + end +end + +return createTween diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/TweenPropertyController.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/TweenPropertyController.lua new file mode 100644 index 0000000..4f20565 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/TweenPropertyController.lua @@ -0,0 +1,183 @@ +local module = {} +local runService = game:GetService("RunService") +local SAFE_MODE = false -- debug + +local interpolators = { + Number = function(a, b, t) + return (b-a)*t + a + end, + Vector3 = Vector3.new().lerp, + Vector2 = Vector2.new().lerp, + Color3 = Color3.new().lerp, + UDim2 = UDim2.new().lerp, + CFrame = CFrame.new().lerp, + BrickColor = function(a, b, t) + return BrickColor.new(a.Color:lerp(b.Color, t)) + end, + NumberRange = function(a, b, t) + return NumberRange.new( + (b.Min - a.Min)*t + a.Min, + (b.Max - a.Max)*t + a.Max + ) + end, + Ray = function(a, b, t) + return Ray.new( + a.Origin:lerp(b, t), + a.Direction:lerp(b, t) + ) + end, + UDim = function(a, b, t) + return UDim.new( + (b.Scale - a.Scale)*t + a.Scale, + (b.Offset - a.Offset)*t + a.Offset + ) + end, + Vector3int16 = function(a, b, t) + return (b-a)*t + a + end, + Vector2int16 = function(a, b, t) + return (b-a)*t + a + end +} + +local active = {} + +local stepAll +if SAFE_MODE then + stepAll = function() + for _, props in next, active do + for _, job in next, props do + if type(job) == 'table' then + if job.step then + local success, err = pcall(job.step) + + if not success then + warn('FAILED TO STEP',err,'\n///',job.traceback) + end + else + warn('JOB MISSING .STEP',job,job.traceback) + end + else + warn('BAD JOB',type(job),job) + end + end + end + end +else + stepAll = function() + for _, props in next, active do + for _, job in next, props do + job.step() + end + end + end +end + +local tween = function(obj, prop, vtype, start, finish, length, style, direction) + -- vtype should start with an uppercase letter + -- If you don't supply 'finish': Cancels existing tween on the property, doesn't start a new one + -- If you don't supply 'finish' or 'prop': Cancels all existing tweens on the object + -- If you supply a function instead of an object as 'obj': It is called in the following pattern: + -- Getting the start value: obj('Get', prop) + -- Setting the value: obj('Set', prop, value, alpha) + -- Returns: RBXScriptSignal(Bool completed) + -- This function is async + + if prop == nil then + -- cancel existing tweens on the obj + if active[obj] then + for _, job in next, active[obj] do + job.interrupt() + end + active[obj] = nil + end + elseif finish == nil then + -- cancel existing tween on the prop + if active[obj] and active[obj][prop] then + active[obj][prop].interrupt() + end + else + -- start a new job + local job = {} + if SAFE_MODE then + job.traceback = debug.traceback() + end + + local fmode = type(obj) == 'function' + local event = Instance.new'BindableEvent' + local signal = event.Event + local started = tick() + local interpolator = interpolators[vtype] + local running = true + if start == nil then + if fmode then + start = obj('Get', prop) + else + start = obj[prop] + end + end + + assert(interpolator ~= nil, 'No interpolator for type "'..tostring(vtype)..'"') + + local activeObj = active[obj] + if activeObj ~= nil then + local activeProp = activeObj[prop] + if activeProp ~= nil then + activeProp.interrupt() + end + end + if active[obj] == nil then -- activeObj may have been dereferenced during the interrupt + activeObj = {} + active[obj] = activeObj + end + + activeObj[prop] = job + + job.step = function() + local alphaRaw = (tick() - started)/length + if alphaRaw > 1 then alphaRaw = 1 end + local alpha = direction and direction(alphaRaw, style) or style and style(alphaRaw) or alphaRaw + local value = interpolator(start, finish, alpha) + + if fmode then + obj('Set', prop, value, alpha) + else + obj[prop] = value + end + + if alphaRaw == 1 then + running = false + event:Fire(true, obj) + + activeObj[prop] = nil + if next(activeObj) == nil then + active[obj] = nil + end + end + end + job.interrupt = function() + running = false + event:Fire(false, obj) + + activeObj[prop] = nil + if next(activeObj) == nil then + active[obj] = nil + end + end + + coroutine.wrap(function() + coroutine.yield() + if running then + job.step() + end + end)() + + return signal + end +end + +runService:BindToRenderStep('TweenPropertyStep', 0, stepAll) + +module.tween = tween + +return module \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Urls.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Urls.lua new file mode 100644 index 0000000..ecabe8a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Urls.lua @@ -0,0 +1,35 @@ +local domainUrl = game:GetService('ContentProvider').BaseUrl + +for _, word in pairs({"/", "www.", "https:", "http:" }) do + domainUrl = string.gsub(domainUrl, word, "") +end + + +return { + domainUrl = + domainUrl; + + api = "https://api."..domainUrl; + + avatarUrlPrefix = + "https://avatar." + ..domainUrl; + + assetImageUrl = + "https://www." + ..domainUrl + .."/Thumbs/Asset.ashx?width=110&height=110&assetId="; + + assetImageUrl150 = + "https://www." + ..domainUrl + .."/Thumbs/Asset.ashx?width=150&height=150&assetId="; + + outfitImageUrlPrefix = + "https://www." + ..domainUrl + .."/outfit-thumbnail/image?userOutfitId="; + + catalogUrlBase = + "https://www."..domainUrl.."/catalog/"; +} diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Utilities.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Utilities.lua new file mode 100644 index 0000000..0709dad --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/Utilities.lua @@ -0,0 +1,355 @@ +local this = {} + +local runService = game:GetService('RunService') +local httpService = game:GetService('HttpService') +local guiService = game:GetService('GuiService') + +function this.getDescendants(parent, t) + t = t or {} + for _, v in next, parent:GetChildren() do + table.insert(t, v) + this.getDescendants(v, t) + end + return t +end + +function this.fastSpawn(func, ...) + return coroutine.wrap(func)(...) +end + + +function this.renderWait(a) -- Waits a single render frame if wait time is small enough + local s = tick() + if a and a>.0333 then + wait(a) + else + runService.RenderStepped:wait() + end + return tick()-s +end + +function this.copyTable(originalTable) + local copy = {} + for index,value in pairs(originalTable) do + copy[index] = value + end + return copy +end + +function this.addTables(table1,table2) + for _, v in ipairs(table2) do + table.insert(table1,#table1+1,v) + end +end + +function this.findFirstChildOfType(parent, typeName) + if parent then + for _,child in pairs(parent:GetChildren()) do + if child:IsA(typeName) then + return child + end + end + end +end + +function this.recursiveDisable(parent) + if parent then + if parent:IsA('Script') then + parent.Disabled = true + end + for _,child in pairs(parent:GetChildren()) do + this.recursiveDisable(child) + end + end +end + +function this.decodeJSON(json) + if json == nil or #json == 0 then + return nil + end + + local success, result = pcall(function() + return httpService:JSONDecode(json) + end) + if not success then + if UserSettings().GameSettings:InStudioMode() then + print("decodeJSON() failed because", result, "Input:", json) + end + return nil + end + + return result +end + +function this.httpGet(...) + local tuple = {...} + local url = tuple[1] + local v = {pcall(function() + return game:HttpGetAsync(url) + end)} + if not v[1] then + return "[]" + end + return select(2, unpack(v)) +end + + +function this.httpPost(...) + local tuple = {...} + local v = {pcall(function() + return game:HttpPostAsync(unpack(tuple)) + end)} + if not v[1] then + return false + end + + if v[2] then + local response = this.decodeJSON(v[2]) + if response then + return response.success + end + end + + return false +end + + +function this.disconnectEvent(conn) + if conn then + if conn.disconnect then + conn:disconnect() + end + + if conn.Disconnect then + conn:Disconnect() + end + end + return nil +end + + +function this.disconnectEvents(conns) + if conns and type(conns) == 'table' then + for _, conn in pairs(conns) do + if conn.disconnect then + conn:disconnect() + end + if conn.Disconnect then + conn:Disconnect() + end + end + end +end + +function this.create(instanceType) + return function(data) + local obj = Instance.new(instanceType) + for k, v in pairs(data) do + if type(k) == 'number' then + v.Parent = obj + else + obj[k] = v + end + end + return obj + end +end + +function this.setSelectedCoreObject(obj) + guiService.SelectedCoreObject = obj +end + +function this.getSelectedCoreObject() + return guiService.SelectedCoreObject +end + +local function pivot_rgb(n) + if n > 0.04045 then + n = math.pow((n + 0.055) / 1.055, 2.4) + else + n = n / 12.92 + end + return n * 100 +end + +local function rgb_to_xyz(c) + local var_R = pivot_rgb(c.r) + local var_G = pivot_rgb(c.g) + local var_B = pivot_rgb(c.b) + + -- For Observer = 2 degrees, Illuminant = D65 + local xyz = {} + xyz.x = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805 + xyz.y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722 + xyz.z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505 + + return xyz +end + +local function pivot_xyz(n) + if (n > 0.008856) then + n = math.pow(n, 1.0/3.0) + else + n = (7.787 * n) + (16.0 / 116.0) + end + return n +end + +local function xyz_to_Lab(xyz) + local ReferenceX = 95.047 + local ReferenceY = 100.0 + local ReferenceZ = 108.883 + + local var_X = pivot_xyz(xyz.x / ReferenceX) + local var_Y = pivot_xyz(xyz.y / ReferenceY) + local var_Z = pivot_xyz(xyz.z / ReferenceZ) + + local CIELab = {} + CIELab.L = math.max(0, ( 116 * var_Y ) - 16) + CIELab.a = 500 * ( var_X - var_Y ) + CIELab.b = 200 * ( var_Y - var_Z ) + + return CIELab +end + +local function rgb_to_Lab(c) + local xyz = rgb_to_xyz(c) + local Lab = xyz_to_Lab(xyz) + return Lab +end + +local function deg2Rad(deg) + return deg * math.pi / 180.0; +end + +function this.delta_CIEDE2000(c1, c2) + local lab1 = rgb_to_Lab(c1) + local lab2 = rgb_to_Lab(c2) + + local k_L = 1.0 -- lightness + local k_C = 1.0 -- chroma + local k_H = 1.0 -- hue + local deg360InRad = deg2Rad(360.0) + local deg180InRad = deg2Rad(180.0) + local pow25To7 = 6103515625.0 -- ; /* pow(25, 7) */ + + -- Step 1 + -- /* Equation 2 */ + local C1 = math.sqrt((lab1.a * lab1.a) + (lab1.b * lab1.b)) + local C2 = math.sqrt((lab2.a * lab2.a) + (lab2.b * lab2.b)) + -- /* Equation 3 */ + local barC = (C1 + C2) / 2.0 + -- /* Equation 4 */ + local G = 0.5 * (1 - math.sqrt(math.pow(barC, 7) / (math.pow(barC, 7) + pow25To7))) + -- /* Equation 5 */ + local a1Prime = (1.0 + G) * lab1.a + local a2Prime = (1.0 + G) * lab2.a + -- /* Equation 6 */ + local CPrime1 = math.sqrt((a1Prime * a1Prime) + (lab1.b * lab1.b)) + local CPrime2 = math.sqrt((a2Prime * a2Prime) + (lab2.b * lab2.b)) + -- /* Equation 7 */ + local hPrime1 + if (lab1.b == 0 and a1Prime == 0) then + hPrime1 = 0.0 + else + hPrime1 = math.atan2(lab1.b, a1Prime) + --/* + --* This must be converted to a hue angle in degrees between 0 + --* and 360 by addition of 2􏰏 to negative hue angles. + --*/ + if (hPrime1 < 0) then + hPrime1 = hPrime1 + deg360InRad + end + end + + local hPrime2 + if (lab2.b == 0 and a2Prime == 0) then + hPrime2 = 0.0 + else + hPrime2 = math.atan2(lab2.b, a2Prime) + --/* + --* This must be converted to a hue angle in degrees between 0 + --* and 360 by addition of 2􏰏 to negative hue angles. + --*/ + if (hPrime2 < 0) then + hPrime2 = hPrime2 + deg360InRad + end + end + + -- * Step 2 + -- /* Equation 8 */ + local deltaLPrime = lab2.L - lab1.L + -- /* Equation 9 */ + local deltaCPrime = CPrime2 - CPrime1 + -- /* Equation 10 */ + local deltahPrime + local CPrimeProduct = CPrime1 * CPrime2 + if (CPrimeProduct == 0) then + deltahPrime = 0 + else + --/* Avoid the fabs() call */ + deltahPrime = hPrime2 - hPrime1 + if (deltahPrime < -deg180InRad) then + deltahPrime = deltahPrime + deg360InRad + elseif (deltahPrime > deg180InRad) then + deltahPrime = deltahPrime - deg360InRad + end + end + + --/* Equation 11 */ + local deltaHPrime = 2.0 * math.sqrt(CPrimeProduct) * math.sin(deltahPrime / 2.0) + + -- * Step 3 + -- /* Equation 12 */ + local barLPrime = (lab1.L + lab2.L) / 2.0 + -- /* Equation 13 */ + local barCPrime = (CPrime1 + CPrime2) / 2.0 + -- /* Equation 14 */ + local barhPrime + local hPrimeSum = hPrime1 + hPrime2 + if (CPrime1 * CPrime2 == 0) then + barhPrime = hPrimeSum + else + if (math.abs(hPrime1 - hPrime2) <= deg180InRad) then + barhPrime = hPrimeSum / 2.0 + else + if (hPrimeSum < deg360InRad) then + barhPrime = (hPrimeSum + deg360InRad) / 2.0 + else + barhPrime = (hPrimeSum - deg360InRad) / 2.0 + end + end + end + + -- /* Equation 15 */ + local T = 1.0 - (0.17 * math.cos(barhPrime - deg2Rad(30.0))) + + (0.24 * math.cos(2.0 * barhPrime)) + + (0.32 * math.cos((3.0 * barhPrime) + deg2Rad(6.0))) - + (0.20 * math.cos((4.0 * barhPrime) - deg2Rad(63.0))) + -- /* Equation 16 */ + local deltaTheta = deg2Rad(30.0) * + math.exp(-math.pow((barhPrime - deg2Rad(275.0)) / deg2Rad(25.0), 2.0)) + -- /* Equation 17 */ + local R_C = 2.0 * math.sqrt(math.pow(barCPrime, 7.0) / + (math.pow(barCPrime, 7.0) + pow25To7)) + -- /* Equation 18 */ + local S_L = 1 + ((0.015 * math.pow(barLPrime - 50.0, 2.0)) / + math.sqrt(20 + math.pow(barLPrime - 50.0, 2.0))) + -- /* Equation 19 */ + local S_C = 1 + (0.045 * barCPrime) + -- /* Equation 20 */ + local S_H = 1 + (0.015 * barCPrime * T) + -- /* Equation 21 */ + local R_T = (-math.sin(2.0 * deltaTheta)) * R_C + + -- /* Equation 22 */ + local deltaE = math.sqrt( + math.pow(deltaLPrime / (k_L * S_L), 2.0) + + math.pow(deltaCPrime / (k_C * S_C), 2.0) + + math.pow(deltaHPrime / (k_H * S_H), 2.0) + + (R_T * (deltaCPrime / (k_C * S_C)) * (deltaHPrime / (k_H * S_H)))) + + return deltaE +end + +return this diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/WarningWidget.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/WarningWidget.lua new file mode 100644 index 0000000..b7c2559 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Legacy/AvatarEditor/WarningWidget.lua @@ -0,0 +1,582 @@ +-------------- SERVICES -------------- +local CoreGui = game:GetService('CoreGui') + +------------ MODULES ------------------- +local Modules = CoreGui:FindFirstChild("RobloxGui").Modules + +local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) +local TweenController = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController) +local Categories = require(Modules.LuaApp.Legacy.AvatarEditor.Categories) +local Strings = require(Modules.LuaApp.Legacy.AvatarEditor.LocalizedStrings) +local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) +local LayoutInfoConsole = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfoConsole) + +-------------- CONSTANTS -------------- +local MAX_IMAGE_LENGTH = LayoutInfoConsole.WarningMaxLength +local TEXT_PADDING = LayoutInfoConsole.WarningTextPadding +local MAX_TEXT_LENGTH = MAX_IMAGE_LENGTH - TEXT_PADDING * 2 +local DEFAULT_IMAGE_HEIGHT = LayoutInfoConsole.ButtonFontSize + TEXT_PADDING * 2 +local WARNING_TYPE = { + ANIMATION = "Animation"; + SCALE = "Scale"; + DEFAULT_CLOTHING = "Default Clothing"; +} +local WARNING_STATE = { + OPEN = "Open"; + CLOSED = "Closed"; +} + +------------ VARIABLES ------------------- +local characterManager + +local currentWarningCnt = 0 +local currentWarningType = nil +local currentWarningState = WARNING_STATE.CLOSED +local warningsStack = {} -- For warnings need to display later + +local hasAnimation = false +local inScalePage = false +local hasDefaultShirt = false +local hasDefaultPants = false +local defaultClothingWarningCnt = 0 +local defaultWarningTiming = 5 + +local storeChangedCn = nil +local warningGUI = nil +local isConsole = false +local isFocusing = false +local this = {} + +local function tweenMobileWarning(text, thisWarningCnt) + if currentWarningState == WARNING_STATE.OPEN then + local t = 0.3 + + warningGUI.WarningText.TextTransparency = 1 + + local tweenInfo = TweenInfo.new(t) + + local warningGoals = { + Size = UDim2.new(0, 266, 0, 70); + Position = UDim2.new(0.5, -133, 0.5, -35); + } + TweenController(warningGUI, tweenInfo, warningGoals) + + local warningIconGoals = { + Rotation = -360; + ImageTransparency = 0; + Position = UDim2.new(0, 12, 0.5, -24); + } + local warningIconStarts = { + Rotation = 0; + ImageTransparency = 1; + } + TweenController(warningGUI.WarningIcon, tweenInfo, warningIconGoals, warningIconStarts) + + local backgroundGoals = { + ImageTransparency = 0.25 + } + local backgroundStarts = { + ImageTransparency = 1 + } + TweenController(warningGUI.BackgroundFill, tweenInfo, backgroundGoals, backgroundStarts) + TweenController(warningGUI.RoundedEnd, tweenInfo, backgroundGoals, backgroundStarts) + TweenController(warningGUI.RoundedStart, tweenInfo, backgroundGoals, backgroundStarts) + + warningGUI.WarningText.Text = text + + wait(t) + + if thisWarningCnt ~= currentWarningCnt then return end + + local textGoals = { + TextTransparency = 0 + } + local textStarts = { + TextTransparency = 1 + } + TweenController(warningGUI.WarningText, tweenInfo, textGoals, textStarts) + else + local tweenInfo = TweenInfo.new(0.3) + local textTweenInfo = TweenInfo.new(0.1) + + TweenController(warningGUI.WarningText, textTweenInfo, { TextTransparency = 1 }) + + TweenController(warningGUI, tweenInfo, { + Size = UDim2.new(0, 70, 0, 70); + Position = UDim2.new(0.5, -35, 0.5, -35); + }) + TweenController(warningGUI.WarningIcon, tweenInfo, { Position = UDim2.new(0.5, -24, 0.5, -24) }) + TweenController(warningGUI.WarningIcon, tweenInfo, { ImageTransparency = 1 }) + TweenController(warningGUI.BackgroundFill, tweenInfo, { ImageTransparency = 1 }) + TweenController(warningGUI.RoundedEnd, tweenInfo, { ImageTransparency = 1 }) + TweenController(warningGUI.RoundedStart, tweenInfo, { ImageTransparency = 1 }) + end +end + +local function tweenConsoleWarning(warningLabel) + local tweenInfo = TweenInfo.new(LayoutInfoConsole.DefaultTweenTime) + if currentWarningType == WARNING_TYPE.DEFAULT_CLOTHING then + local backgroundGoals = { + ImageTransparency = 0 + } + local backgroundStarts = { + ImageTransparency = 1 + } + TweenController(warningLabel, tweenInfo, backgroundGoals, backgroundStarts) + + local textGoals = { + TextTransparency = 0 + } + local textStarts = { + TextTransparency = 1 + } + TweenController(warningLabel.ToastText, tweenInfo, textGoals, textStarts) + end +end + +local function adjustWarningHeight(textLabel, warningImage) + -- In case of stack overflow + if not isFocusing then + return + end + if textLabel.TextFits then + textLabel.Size = UDim2.new(textLabel.Size.X.Scale, textLabel.TextBounds.X, + textLabel.Size.Y.Scale, textLabel.TextBounds.Y) + + local warningImageLength = textLabel.TextBounds.X + TEXT_PADDING * 2 + local warningImageHeight = textLabel.TextBounds.Y + TEXT_PADDING * 2 + warningImage.Size = UDim2.new(warningImage.Size.X.Scale, warningImageLength, + warningImage.Size.Y.Scale, warningImageHeight) + else + textLabel.Size = UDim2.new(textLabel.Size.X.Scale, textLabel.Size.X.Offset, + textLabel.Size.Y.Scale, textLabel.Size.Y.Offset + LayoutInfoConsole.ButtonFontSize) + + warningImage.Size = UDim2.new(warningImage.Size.X.Scale, warningImage.Size.X.Offset, + warningImage.Size.Y.Scale, warningImage.Size.Y.Offset + LayoutInfoConsole.ButtonFontSize) + + adjustWarningHeight(textLabel, warningImage) + end +end + +local function createTooltipsWarning(text) + local indicatorPositionX = warningGUI.Parent.SwitchTypeContainer.AbsolutePosition.X + local tooltipsWarningPositionX = indicatorPositionX - warningGUI.AbsolutePosition.X + local tooltipsTipPositionX = tooltipsWarningPositionX + LayoutInfoConsole.TooltilsTipOffset + + local maxImageSizeX = MAX_IMAGE_LENGTH - tooltipsWarningPositionX + local maxTextSizeX = maxImageSizeX - TEXT_PADDING * 2 + + local tooltipsWarning = Utilities.create'ImageLabel' + { + Name = "Tooltips"; + AnchorPoint = Vector2.new(0, 1); + Position = UDim2.new(0, tooltipsWarningPositionX, 1, -171); + Size = UDim2.new(0, maxImageSizeX, 0, DEFAULT_IMAGE_HEIGHT); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = 'rbxasset://textures/ui/Shell/AvatarEditor/graphic/gr-tooltips.png'; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(8, 8, 9, 9); + ZIndex = LayoutInfoConsole.IndicatorLayer; + Parent = warningGUI; + } + local tooltipsText = Utilities.create'TextLabel' + { + Name = "TooltipsText"; + AnchorPoint = Vector2.new(0, 0.5); + Position = UDim2.new(0, TEXT_PADDING, 0.5, 0); + Size = UDim2.new(0, maxTextSizeX, 0, LayoutInfoConsole.ButtonFontSize); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Font = LayoutInfoConsole.RegularFont; + TextSize = LayoutInfoConsole.ButtonFontSize; + TextColor3 = LayoutInfoConsole.GrayTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Center; + Text = text; + TextWrapped = true; + ZIndex = LayoutInfoConsole.IndicatorLayer; + Parent = tooltipsWarning; + } + Utilities.create'ImageLabel' + { + Name = "Tip"; + AnchorPoint = Vector2.new(0, 1); + Position = UDim2.new(0, tooltipsTipPositionX, 1, -155); + Size = UDim2.new(0, 32, 0, 16); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = 'rbxasset://textures/ui/Shell/AvatarEditor/graphic/gr-tip.png'; + ZIndex = LayoutInfoConsole.IndicatorLayer; + Parent = warningGUI; + } + + adjustWarningHeight(tooltipsText, tooltipsWarning) + + tweenConsoleWarning(tooltipsWarning) +end + +local function createToastWarning(text) + local toastWarning = Utilities.create'ImageLabel' + { + Name = "Toast"; + AnchorPoint = Vector2.new(0.5, 1); + Position = UDim2.new(0.5, 0, 1, -191); + Size = UDim2.new(0, MAX_IMAGE_LENGTH, 0, DEFAULT_IMAGE_HEIGHT); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Image = 'rbxasset://textures/ui/Shell/AvatarEditor/graphic/gr-toast.png'; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(10, 10, 11, 11); + ZIndex = LayoutInfoConsole.IndicatorLayer; + Parent = warningGUI; + } + local toastText = Utilities.create'TextLabel' + { + Name = "ToastText"; + AnchorPoint = Vector2.new(0, 0.5); + Position = UDim2.new(0, TEXT_PADDING, 0.5, 0); + Size = UDim2.new(0, MAX_TEXT_LENGTH, 0, LayoutInfoConsole.ButtonFontSize); + BackgroundTransparency = 1; + BorderSizePixel = 0; + Font = LayoutInfoConsole.RegularFont; + TextSize = LayoutInfoConsole.ButtonFontSize; + TextColor3 = LayoutInfoConsole.WhiteTextColor; + TextXAlignment = Enum.TextXAlignment.Center; + TextYAlignment = Enum.TextYAlignment.Center; + Text = text; + TextWrapped = true; + ZIndex = LayoutInfoConsole.IndicatorLayer; + Parent = toastWarning; + } + + adjustWarningHeight(toastText, toastWarning) + + tweenConsoleWarning(toastWarning) +end + +local function clearStack() + warningsStack = {} +end + +local function removeFromStack(warningType) + for i = #warningsStack, 1, -1 do + if warningsStack[i] == warningType then + table.remove(warningsStack, i) + end + end +end + +local function addToStack(warningType) + table.insert(warningsStack, warningType) +end + +local function moveWarningToTop(warningType) + removeFromStack(warningType) + addToStack(warningType) +end + +local function findInStack(warningType) + for i = 1, #warningsStack do + if warningsStack[i] == warningType then + return true + end + end + return false +end + +local function popWarningsTop() + if #warningsStack > 0 then + local top = warningsStack[#warningsStack] + table.remove(warningsStack) + return top + end + return nil +end + +local function resetWarning() + if isConsole then + warningGUI:ClearAllChildren() + else + warningGUI.WarningText.TextTransparency = 1 + warningGUI.Size = UDim2.new(0, 70, 0, 70) + warningGUI.Position = UDim2.new(0.5, -35, 0.5, -35) + warningGUI.WarningIcon.Position = UDim2.new(0.5, -24, 0.5, -24) + warningGUI.WarningIcon.ImageTransparency = 1 + warningGUI.BackgroundFill.ImageTransparency = 1 + warningGUI.RoundedEnd.ImageTransparency = 1 + warningGUI.RoundedStart.ImageTransparency = 1 + end +end + +local displayWarning = function() end + +local function closeWarning(warningType) + if warningType then + if warningType == WARNING_TYPE.DEFAULT_CLOTHING then + defaultClothingWarningCnt = defaultClothingWarningCnt + 1 + end + removeFromStack(warningType) + if currentWarningType ~= warningType then return end + else + if findInStack(WARNING_TYPE.DEFAULT_CLOTHING) then + defaultClothingWarningCnt = defaultClothingWarningCnt + 1 + end + clearStack() + end + + if currentWarningState == WARNING_STATE.CLOSED then return end + + currentWarningState = WARNING_STATE.CLOSED + currentWarningCnt = currentWarningCnt + 1 + currentWarningType = nil + + Utilities.fastSpawn(function() + if isConsole then + warningGUI:ClearAllChildren() + else + tweenMobileWarning() + end + if warningType then + local warningsTop = popWarningsTop() + if warningsTop then + displayWarning(warningsTop, true) + end + end + end) +end + +displayWarning = function (warningType, fromStack) + removeFromStack(warningType) + if currentWarningType == warningType and + currentWarningType ~= WARNING_TYPE.DEFAULT_CLOTHING then + return + end + + currentWarningState = WARNING_STATE.OPEN + resetWarning() + + currentWarningCnt = currentWarningCnt + 1 + + if currentWarningType ~= nil and + currentWarningType ~= WARNING_TYPE.SCALE and + currentWarningType ~= warningType then + moveWarningToTop(currentWarningType) + end + currentWarningType = warningType + + local thisDefaultClothingWarningCnt = defaultClothingWarningCnt + local warningText = "" + + if warningType == WARNING_TYPE.DEFAULT_CLOTHING then + if not fromStack then + defaultClothingWarningCnt = defaultClothingWarningCnt + 1 + thisDefaultClothingWarningCnt = defaultClothingWarningCnt + end + warningText = Strings:LocalizedString("DefaultClothingAppliedPhrase") + elseif warningType == WARNING_TYPE.ANIMATION then + warningText = isConsole and + Strings:LocalizedString("AnimationsForR15ConsolePhrase") or + Strings:LocalizedString("AnimationsForR15Phrase") + elseif warningType == WARNING_TYPE.SCALE then + warningText = isConsole and + Strings:LocalizedString("ScalingForR15ConsolePhrase") or + Strings:LocalizedString("ScalingForR15Phrase") + end + + Utilities.fastSpawn(function() + if isConsole then + if currentWarningType == WARNING_TYPE.DEFAULT_CLOTHING then + createToastWarning(warningText) + else + createTooltipsWarning(warningText) + end + else + tweenMobileWarning(warningText, currentWarningCnt) + end + + if warningType == WARNING_TYPE.DEFAULT_CLOTHING and + not fromStack and + thisDefaultClothingWarningCnt == defaultClothingWarningCnt then + wait(defaultWarningTiming) + if thisDefaultClothingWarningCnt == defaultClothingWarningCnt then + closeWarning(WARNING_TYPE.DEFAULT_CLOTHING) + end + end + end) +end + + +local function updateState(newState, oldState) + + warningGUI.Visible = not newState.FullView + + local avatarTypeChanged = false + if newState.Character.AvatarType ~= oldState.Character.AvatarType then + avatarTypeChanged = true + end + + local hasAnimationChanged = false + if newState.Character.Assets ~= oldState.Character.Assets then + local newHasAnimation = false + + for assetType, _ in pairs(newState.Character.Assets) do + if next(newState.Character.Assets[assetType]) ~= nil and + string.find(assetType, 'Animation') then + newHasAnimation = true + break + end + end + + if newHasAnimation ~= hasAnimation then + hasAnimationChanged = true + hasAnimation = newHasAnimation + end + end + + local inScalePageChanged = false + if newState.Category.CategoryIndex ~= oldState.Category.CategoryIndex + or newState.Category.TabsInfo ~= oldState.Category.TabsInfo then + + local categoryIndex = newState.Category.CategoryIndex or 1 + local tabInfo = categoryIndex and newState.Category.TabsInfo[categoryIndex] + local tabIndex = tabInfo and tabInfo.TabIndex or 1 + local currentPageName = Categories[categoryIndex].pages[tabIndex].name + + local newInScalePage = (currentPageName == 'Scale') + + if newInScalePage ~= inScalePage then + inScalePageChanged = true + inScalePage = newInScalePage + end + end + + local addedDefaultClothes = false + local destroyedDefaultClothes = false + + if newState.Character ~= oldState.Character then + local newHasDefaultShirt = characterManager.hasDefaultShirt() + local newHasDefaultPants = characterManager.hasDefaultPants() + + if (newHasDefaultShirt ~= hasDefaultShirt and newHasDefaultShirt) or + (newHasDefaultPants ~= hasDefaultPants and newHasDefaultPants) then + addedDefaultClothes = true + end + + if (newHasDefaultShirt ~= hasDefaultShirt and not newHasDefaultShirt) or + (newHasDefaultPants ~= hasDefaultPants and not newHasDefaultPants) then + destroyedDefaultClothes = true + end + + hasDefaultShirt = newHasDefaultShirt + hasDefaultPants = newHasDefaultPants + end + + if avatarTypeChanged then + if newState.Character.AvatarType == 'R15' then + closeWarning(WARNING_TYPE.ANIMATION) + closeWarning(WARNING_TYPE.SCALE) + else + if hasAnimation then + displayWarning(WARNING_TYPE.ANIMATION) + end + if inScalePage then + displayWarning(WARNING_TYPE.SCALE) + end + end + elseif newState.Character.AvatarType == 'R6' then + if hasAnimationChanged then + if hasAnimation then + displayWarning(WARNING_TYPE.ANIMATION) + else + closeWarning(WARNING_TYPE.ANIMATION) + end + end + if inScalePageChanged then + if inScalePage then + displayWarning(WARNING_TYPE.SCALE) + else + closeWarning(WARNING_TYPE.SCALE) + end + end + end + + if addedDefaultClothes then + displayWarning(WARNING_TYPE.DEFAULT_CLOTHING) + elseif destroyedDefaultClothes then + closeWarning(WARNING_TYPE.DEFAULT_CLOTHING) + end +end + +local function initWarning() + local state = AppState.Store:getState() + + local categoryIndex = state.Category.CategoryIndex or 1 + local tabInfo = categoryIndex and state.Category.TabsInfo[categoryIndex] + local tabIndex = tabInfo and tabInfo.TabIndex or 1 + local currentPageName = Categories[categoryIndex].pages[tabIndex].name + inScalePage = (currentPageName == 'Scale') + + for assetType, _ in pairs(state.Character.Assets) do + if next(state.Character.Assets[assetType]) ~= nil and + string.find(assetType, 'Animation') then + hasAnimation = true + break + end + end + + if state.Character.AvatarType == 'R6' then + if hasAnimation then + displayWarning(WARNING_TYPE.ANIMATION) + else + closeWarning(WARNING_TYPE.ANIMATION) + end + if inScalePage then + displayWarning(WARNING_TYPE.SCALE) + else + closeWarning(WARNING_TYPE.SCALE) + end + end + + hasDefaultShirt = characterManager.hasDefaultShirt() + hasDefaultPants = characterManager.hasDefaultPants() + + if hasDefaultShirt or hasDefaultPants then + displayWarning(WARNING_TYPE.DEFAULT_CLOTHING) + else + closeWarning(WARNING_TYPE.DEFAULT_CLOTHING) + end +end + + +function this:Focus() + isFocusing = true + storeChangedCn = AppState.Store.Changed:Connect(updateState) + initWarning() +end + +function this:RemoveFocus() + isFocusing = false + storeChangedCn = Utilities.disconnectEvent(storeChangedCn) +end + +function this:Hide() + closeWarning() +end + +return function(inWarningGUI, inCharacterManager, inIsConsole) + warningGUI = inWarningGUI + characterManager = inCharacterManager + isConsole = inIsConsole + + defaultWarningTiming = isConsole and 10 or 5 + hasAnimation = false + inScalePage = false + hasDefaultShirt = false + hasDefaultPants = false + + resetWarning() + + return this +end + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/de-de.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/de-de.lua new file mode 100644 index 0000000..b7efbe9 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/de-de.lua @@ -0,0 +1,194 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Application.Logout.Action.Logout"] = [[Abmelden]], + ["Common.Presence.Label.Online"] = [[Online]], + ["Common.Presence.Label.Offline"] = [[Offline]], + ["Common.VisitGame.Label.Play"] = [[Spielen]], + ["CommonUI.Features.Label.Avatar"] = [[Avatar]], + ["CommonUI.Features.Label.Blog"] = [[Blog]], + ["CommonUI.Features.Label.BuildersClub"] = [[Builders Club]], + ["CommonUI.Features.Label.Catalog"] = [[Katalog]], + ["CommonUI.Features.Label.Chat"] = [[Chat]], + ["CommonUI.Features.Label.Events"] = [[Events]], + ["CommonUI.Features.Label.Friends"] = [[Freunde]], + ["CommonUI.Features.Label.Game"] = [[Spiele]], + ["CommonUI.Features.Label.Groups"] = [[Gruppen]], + ["CommonUI.Features.Label.Help"] = [[Hilfe]], + ["CommonUI.Features.Label.Home"] = [[Hauptmenü]], + ["CommonUI.Features.Label.Inventory"] = [[Inventar]], + ["CommonUI.Features.Label.Messages"] = [[Nachrichten]], + ["CommonUI.Features.Label.More"] = [[Mehr]], + ["CommonUI.Features.Label.Profile"] = [[Profil]], + ["CommonUI.Features.Label.Settings"] = [[Einstellungen]], + ["CommonUI.Features.Label.About"] = [[Info]], + ["CommonUI.Features.Label.Players"] = [[Spieler]], + ["CommonUI.Features.Label.Create"] = [[Erstellen]], + ["CommonUI.Features.Label.CreateGames"] = [[Spiele erstellen]], + ["Feature.Chat.Response.ChatNameFullyModerated"] = [[Name der Chatgruppe wurde von einem Moderator angepasst.]], + ["Feature.Chat.Heading.LeaveGroup"] = [[Gruppe verlassen]], + ["Feature.Chat.Label.SearchWord"] = [[Suchen]], + ["Feature.Chat.Action.Add"] = [[Hinzufügen]], + ["Feature.Chat.Action.Leave"] = [[Verlassen]], + ["Feature.Chat.Action.Remove"] = [[Entfernen]], + ["Feature.Chat.Action.Cancel"] = [[Abbrechen]], + ["Feature.Chat.Description.NameGroupChat"] = [[Benenne deine Chatgruppe]], + ["Feature.Chat.Heading.NewChatGroup"] = [[Neue Chatgruppe]], + ["Feature.Chat.Label.SeeLess"] = [[Weniger ansehen]], + ["Feature.Chat.Label.InputPlaceHolder.SearchForFriends"] = [[Suche nach Freunden]], + ["Feature.Chat.Label.AddFriends"] = [[Freunde hinzufügen]], + ["Feature.Chat.Label.ChatDetails"] = [[Chatinfos]], + ["Feature.Chat.Label.General"] = [[Allgemein]], + ["Feature.Chat.Label.Members"] = [[Mitglieder]], + ["Feature.Chat.Label.ViewProfile"] = [[Profil ansehen]], + ["Feature.Chat.Label.ChatGroupName"] = [[Name der Chatgruppe]], + ["Feature.Chat.Action.Stay"] = [[Bleiben]], + ["Feature.Chat.Action.Create"] = [[Erstellen]], + ["Feature.Chat.Action.Send"] = [[Senden]], + ["Feature.Chat.Action.Save"] = [[Speichern]], + ["Feature.Chat.Message.ToastText"] = [[Du kannst bis zu {friendNum} Freunde in deiner Chatgruppe haben.]], + ["Feature.Chat.Message.NoConnectionMsg"] = [[Verbindung ...]], + ["Feature.Chat.Message.Default"] = [[Nicht jeder Chatteilnehmer kann deine Nachricht sehen.]], + ["Feature.Chat.Message.MessageFilterForReceivers"] = [[Nicht jeder Chatteilnehmer kann deine Nachricht sehen.]], + ["Feature.Chat.Message.MessageContentModerated"] = [[Deine Nachricht wurde von einem Moderator angepasst und nicht gesendet.]], + ["Feature.Chat.Label.PrivacySettings"] = [[Datenschutzeinstellungen]], + ["Feature.Chat.Label.ChatInputPlaceholder"] = [[Sag etwas]], + ["Feature.Chat.Action.Confirm"] = [[Okay]], + ["Feature.Chat.Heading.FailedToLeaveGroup"] = [[Gruppe konnte nicht verlassen werden.]], + ["Feature.Chat.Message.FailedToLeaveGroup"] = [[Du konntest nicht aus der Konversation „{CONVERSATION_TITLE}“ entfernt werden.]], + ["Feature.Chat.Heading.FailedToRemoveUser"] = [[Benutzer konnte nicht entfernt werden.]], + ["Feature.Chat.Message.FailedToRemoveUser"] = [[Benutzer {USERNAME} konnte nicht aus der Konversation „{CONVERSATION_TITLE}“ entfernt werden.]], + ["Feature.Chat.Heading.FailedToRenameConversation"] = [[Konversation konnte nicht umbenannt werden.]], + ["Feature.Chat.Message.FailedToRenameConversation"] = [[Die Konversation „{EXISTING_NAME}“ konnte nicht zu „{NEW_NAME}“ umbenannt werden.]], + ["Feature.Chat.Message.LeaveGroup"] = [[Du wirst nicht mehr mit dieser Gruppe chatten können.]], + ["Feature.Chat.Message.MakeFriendsToChat"] = [[Finde Freunde in Spielen, um euch zu unterhalten und gemeinsam zu spielen.]], + ["Feature.Chat.Label.NotSet"] = [[Nicht festgelegt]], + ["Feature.Chat.Heading.Option"] = [[Option]], + ["Feature.Chat.Action.RemoveFromGroup"] = [[Aus Gruppe entfernen]], + ["Feature.Chat.Action.RemoveUser"] = [[Benutzer entfernen]], + ["Feature.Chat.Message.RemoveUser"] = [[Möchtest du {USERNAME} wirklich aus dieser Chatgruppe entfernen?]], + ["Feature.Chat.Message.RemovedFromConversation"] = [[Du wurdest aus der Gruppe entfernt.]], + ["Feature.Chat.Action.ReportUser"] = [[Benutzer melden]], + ["Feature.Chat.Label.SearchForFriendsAndChat"] = [[Suche nach Freunden und Chatgruppen]], + ["Feature.Chat.Action.SeeMoreFriends"] = [[Mehr ansehen ({NUMBER_OF_FRIENDS})]], + ["Feature.Chat.Label.Sent"] = [[Gesendet]], + ["Feature.Chat.Heading.ShareGameToChat"] = [[Teile das Spiel, um zu chatten]], + ["Feature.Chat.Message.TurnOnChat"] = [[Um dich mit deinen Freunden zu unterhalten, aktiviere den Chat in deinen Datenschutzeinstellungen.]], + ["Feature.Chat.Action.ViewAssetDetails"] = [[Infos anzeigen]], + ["Feature.Chat.Label.ByBuilder"] = [[Von {USERNAME}]], + ["Feature.Chat.Message.AlreadyPinnedGame"] = [[Dieses Spiel wurde bereits angeheftet. Bitte versuche es mit einem anderen.]], + ["Feature.Chat.Message.PinFailed"] = [[Das Spiel konnte nicht angeheftet werden. Bitte versuche es später erneut.]], + ["Feature.Chat.Message.UnpinFailed"] = [[Das Anheften des Spiels konnte nicht rückgängig gemacht werden. Bitte versuche es später erneut.]], + ["Feature.Chat.Drawer.ShowMore"] = [[Mehr anzeigen]], + ["Feature.Chat.Drawer.ShowLess"] = [[Weniger anzeigen]], + ["Feature.Chat.Drawer.NoGames"] = [[Es sind keine Spiele angeheftet oder im Gange.]], + ["Feature.Chat.Drawer.Recommended"] = [[Empfohlen]], + ["Feature.Chat.Drawer.Loading"] = [[Wird geladen]], + ["Feature.Chat.Drawer.PinnedGame"] = [[Angeheftetes Spiel]], + ["Feature.Chat.Drawer.Play"] = [[Spielen]], + ["Feature.Chat.Drawer.Join"] = [[Beitreten]], + ["Feature.Chat.Drawer.PlayGame"] = [[Spiel spielen]], + ["Feature.Chat.Drawer.PinGame"] = [[Spiel anheften]], + ["Feature.Chat.Drawer.UnpinGame"] = [[Spiel nicht mehr anheften]], + ["Feature.Chat.Drawer.ViewDetails"] = [[Infos anzeigen]], + ["Feature.Chat.Label.NoDescriptionYet"] = [[Noch keine Beschreibung.]], + ["Feature.Chat.Message.GameLinkWasModerated"] = [[Dieser Spiel-Link wurde von einem Moderator angepasst und nicht gesendet.]], + ["Feature.Chat.ShareGameToChat.BrowseGames"] = [[Spiele durchsuchen]], + ["Feature.Chat.ShareGameToChat.By"] = [[von {creatorName}]], + ["Feature.Chat.ShareGameToChat.Popular"] = [[Beliebt]], + ["Feature.Chat.ShareGameToChat.Recent"] = [[Vor Kurzem gespielt]], + ["Feature.Chat.ShareGameToChat.Favorites"] = [[Lieblingsspiele]], + ["Feature.Chat.ShareGameToChat.FriendActivity"] = [[Freundesaktivität]], + ["Feature.Chat.ShareGameToChat.NoPopularGames"] = [[Keine beliebten Spiele verfügbar.]], + ["Feature.Chat.ShareGameToChat.NoRecentGames"] = [[Du hast keine vor Kurzem gespielten Spiele.]], + ["Feature.Chat.ShareGameToChat.NoFavoriteGames"] = [[Du hast keine Lieblingsspiele.]], + ["Feature.Chat.ShareGameToChat.NoFriendActivity"] = [[Du hast keine Freundesaktivität.]], + ["Feature.Chat.ShareGameToChat.GameNotAvailable"] = [[Nicht verfügbar]], + ["Feature.Chat.ShareGameToChat.FailedToShareTheGame"] = [[Spiel kann nicht geteilt werden. Bitte versuche es später erneut.]], + ["Feature.Chat.Heading.ChatWithFriends"] = [[Mit Freunden chatten]], + ["Feature.Chat.Action.StartChatWithFriends"] = [[Chatten]], + ["Feature.Friends.Label.SearchFriends"] = [[Suche nach Freunden]], + ["Feature.GameBadges.HeadingGameBadges"] = [[Spielabzeichen]], + ["Feature.GameDetails.Label.By"] = [[Von]], + ["Feature.GameDetails.Label.About"] = [[Info]], + ["Feature.GameDetails.Label.Store"] = [[Shop]], + ["Feature.GameDetails.Label.Leaderboards"] = [[Bestenlisten]], + ["Feature.GameDetails.Label.Servers"] = [[Server]], + ["Feature.GameDetails.Heading.Description"] = [[Beschreibung]], + ["Feature.GameDetails.Label.Playing"] = [[Spieler]], + ["Feature.GameDetails.Label.Visits"] = [[Besuche]], + ["Feature.GameDetails.Label.Created"] = [[Erstellt]], + ["Feature.GameDetails.Label.Updated"] = [[Aktualisiert]], + ["Feature.GameDetails.Label.MaxPlayers"] = [[Max. Spieler]], + ["Feature.GameDetails.Label.Genre"] = [[Genre]], + ["Feature.GameDetails.Label.GameCopyLocked"] = [[Dieses Spiel ist kopiergeschützt.]], + ["Feature.GameDetails.Label.ReportAbuse"] = [[Verstoß melden]], + ["Feature.GameDetails.Heading.RecommendedGames"] = [[Empfohlene Spiele]], + ["Feature.GameGear.Heading.GearForThisGame"] = [[Ausrüstung für dieses Spiel]], + ["Feature.GameLeaderboard.Label.Clans"] = [[Klans]], + ["Feature.GameLeaderboard.Heading.Clans"] = [[Klans]], + ["Feature.GameLeaderboard.Heading.Players"] = [[Spieler]], + ["Feature.GameLeaderboard.Label.NoResults"] = [[Keine Ergebnisse gefunden]], + ["Feature.GamePage.LabelPlayingPhrase"] = [[Von {playerCount} gespielt]], + ["Feature.GamePage.ActionSeeAll"] = [[Alle ansehen]], + ["Feature.GamePage.LabelNoSearchResults"] = [[Keine Suchergebnisse]], + ["Feature.GamePage.LabelCancelField"] = [[Abbrechen]], + ["Feature.GamePage.LabelShowingResultsFor"] = [[Zeige Treffer für]], + ["Feature.GamePage.LabelSearchInsteadFor"] = [[Suche stattdessen nach]], + ["Feature.GamePage.LabelSearchYouMightMean"] = [[Meintest du:]], + ["Feature.GamePage.Label.Sponsored"] = [[Gesponsert]], + ["Feature.GamePage.Message.EndOfList"] = [[PUH! Du hast es bis zum Ende geschafft!]], + ["Feature.GamePage.Action.BackToTop"] = [[Zurück zum Seitenanfang]], + ["Feature.GamePass.Heading.PassesForThisGame"] = [[Pässe für dieses Spiel]], + ["Feature.Home.HeadingFriends"] = [[Freunde ({friendCount})]], + ["Feature.Home.ActionSeeAll"] = [[Alle ansehen]], + ["Feature.Home.Action.ViewMyFeed"] = [[Meinen Feed ansehen]], + ["Feature.PlayerSearchResults.Heading.PlayerResultsFor"] = [[Spielerergebnisse für {startSpan}{keyword}{endSpan}]], + ["Feature.PlayerSearchResults.Label.NoMatchesAvailable"] = [[Es gibt keine Treffer für „{keyword}“.]], + ["Feature.PlayerSearchResults.Label.EnterMinCharacters"] = [[Gib bitte mindestens {keywordMinLength} Zeichen ein.]], + ["Feature.PlayerSearchResults.Label.UnsafeInput"] = [[Du hast einen unangemessenen Suchbegriff eingegeben. Bitte führe eine neue Suche durch.]], + ["Feature.PlayerSearchResults.Label.ShowingCountOfResults"] = [[{countStartSpan}{resultsStart} – {resultsInPage} von {countEndSpan}{totalStartSpan}{totalResults}{totalEndSpan}]], + ["Feature.PlayerSearchResults.Label.AlsoKnownAsAbbreviation"] = [[alias]], + ["Feature.PlayerSearchResults.Label.ThisIsYou"] = [[Das bist du]], + ["Feature.PlayerSearchResults.Label.YouAreFriends"] = [[Ihr seid befreundet]], + ["Feature.PlayerSearchResults.Label.YouAreFollowing"] = [[Du folgst]], + ["Feature.PlayerSearchResults.Action.AddFriend"] = [[Freund hinzufügen]], + ["Feature.PlayerSearchResults.Action.AcceptRequest"] = [[Anfrage annehmen]], + ["Feature.PlayerSearchResults.Action.RequestSent"] = [[Anfrage gesendet]], + ["Feature.PlayerSearchResults.Action.Chat"] = [[Chatten]], + ["Feature.PlayerSearchResults.Action.JoinGame"] = [[Spiel beitreten]], + ["Feature.PrivateServers.Heading.VipServers"] = [[VIP-Server]], + ["Feature.Profile.Label.About"] = [[Info]], + ["Feature.ServerList.Heading.OtherServers"] = [[Andere Server]], + ["Feature.ServerList.Heading.ServersMyFriendsAreIn"] = [[Server, auf denen meine Freunde sind]], + ["Game.Launch.IDS_ROBLOX_INSTALLED"] = [[ROBLOX WURDE ERFOLGREICH INSTALLIERT!]], + ["Game.Launch.IDS_ROBLOX_TOPLAY"] = [[Klicke auf 'Spielen' und stürze dich in die Action!]], + ["Game.Launch.IDS_STUDIO_SUCCESS1"] = [[ROBLOX STUDIO WURDE ERFOLGREICH INSTALLIERT!]], + ["Game.Launch.IDS_STUDIO_SUCCESS2"] = [[Klicke auf "Studio starten" und erschaffe dein neues Spiel!]], + ["Game.Launch.IDS_ROBLOX_DOWNLOAD"] = [[Roblox herunterladen und installieren]], + ["Game.Launch.IDS_CHECKING_FILE"] = [[Dateien werden geprüft ...]], + ["Game.Launch.IDS_FILE_CHECKED"] = [[Dateiprüfung abgeschlossen]], + ["Game.Launch.IDS_ROBLOX_STARTING"] = [[%s wird gestartet ...]], + ["Game.Launch.IDS_ROBLOX_UPTODATE"] = [[%s ist auf dem neuesten Stand]], + ["Game.Launch.IDS_ROBLOX_UPGRADING"] = [[%s wird aufgewertet ...]], + ["Game.Launch.IDS_ROBLOX_INSTALLING"] = [[%s wird installiert ...]], + ["Game.Launch.IDS_ROBLOX_CONNECTING"] = [[Verbindung zu %s wird hergestellt ...]], + ["Game.Launch.IDS_BOOTS_ASK_DOWNLOAD"] = [[Neuesten Bootstrapper herunterladen?]], + ["Game.Launch.IDS_BOOTS_GET_LATEST"] = [[Neueste Version von %s wird beschafft ...]], + ["Game.Launch.IDS_PLEASE_WAIT"] = [[Bitte warten ...]], + ["Game.Launch.IDS_BOOTS_SHUTDOWN"] = [[%s wird abgeschaltet]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLING"] = [[%s wird deinstalliert ...]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLED"] = [[%s wurde deinstalliert]], + ["Game.Launch.IDS_BOOTS_CONFIGURING"] = [[%s wird konfiguriert ...]], + ["Search.GlobalSearch.Example.SearchGames"] = [[Spiele suchen]], +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/en-us.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/en-us.lua new file mode 100644 index 0000000..5f72dc9 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/en-us.lua @@ -0,0 +1,195 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Application.Logout.Action.Logout"] = [[Log Out]], + ["Common.Presence.Label.Online"] = [[Online]], + ["Common.Presence.Label.Offline"] = [[Offline]], + ["Common.VisitGame.Label.Play"] = [[Play]], + ["CommonUI.Features.Label.Avatar"] = [[Avatar]], + ["CommonUI.Features.Label.Blog"] = [[Blog]], + ["CommonUI.Features.Label.BuildersClub"] = [[Builders Club]], + ["CommonUI.Features.Label.Catalog"] = [[Catalog]], + ["CommonUI.Features.Label.Chat"] = [[Chat]], + ["CommonUI.Features.Label.Events"] = [[Events]], + ["CommonUI.Features.Label.Friends"] = [[Friends]], + ["CommonUI.Features.Label.Game"] = [[Games]], + ["CommonUI.Features.Label.Groups"] = [[Groups]], + ["CommonUI.Features.Label.Help"] = [[Help]], + ["CommonUI.Features.Label.Home"] = [[Home]], + ["CommonUI.Features.Label.Inventory"] = [[Inventory]], + ["CommonUI.Features.Label.Messages"] = [[Messages]], + ["CommonUI.Features.Label.More"] = [[More]], + ["CommonUI.Features.Label.Profile"] = [[Profile]], + ["CommonUI.Features.Label.Settings"] = [[Settings]], + ["CommonUI.Features.Label.About"] = [[About]], + ["CommonUI.Features.Label.Players"] = [[Players]], + ["CommonUI.Features.Label.Create"] = [[Create]], + ["CommonUI.Features.Label.CreateGames"] = [[Create Games]], + ["CommonUI.Features.Label.Nil"] = [[]], -- Used for None page, which is to be deprecated soon. SOC-1810 + ["Feature.Chat.Response.ChatNameFullyModerated"] = [[Chat group name was moderated.]], + ["Feature.Chat.Heading.LeaveGroup"] = [[Leave Group]], + ["Feature.Chat.Label.SearchWord"] = [[Search]], + ["Feature.Chat.Action.Add"] = [[Add]], + ["Feature.Chat.Action.Leave"] = [[Leave]], + ["Feature.Chat.Action.Remove"] = [[Remove]], + ["Feature.Chat.Action.Cancel"] = [[Cancel]], + ["Feature.Chat.Description.NameGroupChat"] = [[Name your chat group]], + ["Feature.Chat.Heading.NewChatGroup"] = [[New Chat Group]], + ["Feature.Chat.Label.SeeLess"] = [[See Less]], + ["Feature.Chat.Label.InputPlaceHolder.SearchForFriends"] = [[Search for friends]], + ["Feature.Chat.Label.AddFriends"] = [[Add Friends]], + ["Feature.Chat.Label.ChatDetails"] = [[Chat Details]], + ["Feature.Chat.Label.General"] = [[General]], + ["Feature.Chat.Label.Members"] = [[Members]], + ["Feature.Chat.Label.ViewProfile"] = [[View Profile]], + ["Feature.Chat.Label.ChatGroupName"] = [[Chat Group Name]], + ["Feature.Chat.Action.Stay"] = [[Stay]], + ["Feature.Chat.Action.Create"] = [[Create]], + ["Feature.Chat.Action.Send"] = [[Send]], + ["Feature.Chat.Action.Save"] = [[Save]], + ["Feature.Chat.Message.ToastText"] = [[You can have up to {friendNum} friends in chat group.]], + ["Feature.Chat.Message.NoConnectionMsg"] = [[Connecting...]], + ["Feature.Chat.Message.Default"] = [[Not everyone in this chat can see your message.]], + ["Feature.Chat.Message.MessageFilterForReceivers"] = [[Not everyone in this chat can see your message.]], + ["Feature.Chat.Message.MessageContentModerated"] = [[Your message was moderated and not sent.]], + ["Feature.Chat.Label.PrivacySettings"] = [[Privacy Settings]], + ["Feature.Chat.Label.ChatInputPlaceholder"] = [[Say something]], + ["Feature.Chat.Action.Confirm"] = [[Okay]], + ["Feature.Chat.Heading.FailedToLeaveGroup"] = [[Failed to Leave Group]], + ["Feature.Chat.Message.FailedToLeaveGroup"] = [[You could not be removed from the conversation {CONVERSATION_TITLE}.]], + ["Feature.Chat.Heading.FailedToRemoveUser"] = [[Failed to Remove User]], + ["Feature.Chat.Message.FailedToRemoveUser"] = [[The user {USERNAME} could not be removed from the conversation {CONVERSATION_TITLE}.]], + ["Feature.Chat.Heading.FailedToRenameConversation"] = [[Failed to Rename Conversation]], + ["Feature.Chat.Message.FailedToRenameConversation"] = [[The conversation {EXISTING_NAME} could not be renamed to {NEW_NAME}.]], + ["Feature.Chat.Message.LeaveGroup"] = [[You won't be able to keep chatting with this group.]], + ["Feature.Chat.Message.MakeFriendsToChat"] = [[Make friends in games to start chatting and playing together.]], + ["Feature.Chat.Label.NotSet"] = [[Not Set]], + ["Feature.Chat.Heading.Option"] = [[Option]], + ["Feature.Chat.Action.RemoveFromGroup"] = [[Remove from Group]], + ["Feature.Chat.Action.RemoveUser"] = [[Remove User]], + ["Feature.Chat.Message.RemoveUser"] = [[Are you sure you want to remove {USERNAME} from this chat group?]], + ["Feature.Chat.Message.RemovedFromConversation"] = [[You have been removed from the group.]], + ["Feature.Chat.Action.ReportUser"] = [[Report User]], + ["Feature.Chat.Label.SearchForFriendsAndChat"] = [[Search for friends and chat groups]], + ["Feature.Chat.Action.SeeMoreFriends"] = [[See More ({NUMBER_OF_FRIENDS})]], + ["Feature.Chat.Label.Sent"] = [[Sent]], + ["Feature.Chat.Heading.ShareGameToChat"] = [[Share game to chat]], + ["Feature.Chat.Message.TurnOnChat"] = [[To chat with friends, turn on chat in your Privacy Settings.]], + ["Feature.Chat.Action.ViewAssetDetails"] = [[View Details]], + ["Feature.Chat.Label.ByBuilder"] = [[By {USERNAME}]], + ["Feature.Chat.Message.AlreadyPinnedGame"] = [[This game has already been pinned. Please try another one.]], + ["Feature.Chat.Message.PinFailed"] = [[The game could not be pinned. Please try again later.]], + ["Feature.Chat.Message.UnpinFailed"] = [[The game could not be unpinned. Please try again later.]], + ["Feature.Chat.Drawer.ShowMore"] = [[Show More]], + ["Feature.Chat.Drawer.ShowLess"] = [[Show Less]], + ["Feature.Chat.Drawer.NoGames"] = [[No games are pinned or in progress.]], + ["Feature.Chat.Drawer.Recommended"] = [[Recommended]], + ["Feature.Chat.Drawer.Loading"] = [[Loading]], + ["Feature.Chat.Drawer.PinnedGame"] = [[Pinned Game]], + ["Feature.Chat.Drawer.Play"] = [[Play]], + ["Feature.Chat.Drawer.Join"] = [[Join]], + ["Feature.Chat.Drawer.PlayGame"] = [[Play Game]], + ["Feature.Chat.Drawer.PinGame"] = [[Pin Game]], + ["Feature.Chat.Drawer.UnpinGame"] = [[Unpin Game]], + ["Feature.Chat.Drawer.ViewDetails"] = [[View Details]], + ["Feature.Chat.Label.NoDescriptionYet"] = [[No description yet.]], + ["Feature.Chat.Message.GameLinkWasModerated"] = [[This game link was moderated and not sent.]], + ["Feature.Chat.ShareGameToChat.BrowseGames"] = [[Browse Games]], + ["Feature.Chat.ShareGameToChat.By"] = [[by {creatorName}]], + ["Feature.Chat.ShareGameToChat.Popular"] = [[Popular]], + ["Feature.Chat.ShareGameToChat.Recent"] = [[Recent]], + ["Feature.Chat.ShareGameToChat.Favorites"] = [[Favorites]], + ["Feature.Chat.ShareGameToChat.FriendActivity"] = [[Friend Activity]], + ["Feature.Chat.ShareGameToChat.NoPopularGames"] = [[No popular games available]], + ["Feature.Chat.ShareGameToChat.NoRecentGames"] = [[You have no recent games]], + ["Feature.Chat.ShareGameToChat.NoFavoriteGames"] = [[You have no favorite games]], + ["Feature.Chat.ShareGameToChat.NoFriendActivity"] = [[You have no friend activity]], + ["Feature.Chat.ShareGameToChat.GameNotAvailable"] = [[Not available]], + ["Feature.Chat.ShareGameToChat.FailedToShareTheGame"] = [[Cannot share game. Please try again later.]], + ["Feature.Chat.Heading.ChatWithFriends"] = [[Chat with Friends]], + ["Feature.Chat.Action.StartChatWithFriends"] = [[Chat]], + ["Feature.Friends.Label.SearchFriends"] = [[Search for Friends]], + ["Feature.GameBadges.HeadingGameBadges"] = [[Game Badges]], + ["Feature.GameDetails.Label.By"] = [[By]], + ["Feature.GameDetails.Label.About"] = [[About]], + ["Feature.GameDetails.Label.Store"] = [[Store]], + ["Feature.GameDetails.Label.Leaderboards"] = [[Leaderboards]], + ["Feature.GameDetails.Label.Servers"] = [[Servers]], + ["Feature.GameDetails.Heading.Description"] = [[Description]], + ["Feature.GameDetails.Label.Playing"] = [[Playing]], + ["Feature.GameDetails.Label.Visits"] = [[Visits]], + ["Feature.GameDetails.Label.Created"] = [[Created]], + ["Feature.GameDetails.Label.Updated"] = [[Updated]], + ["Feature.GameDetails.Label.MaxPlayers"] = [[Max Players]], + ["Feature.GameDetails.Label.Genre"] = [[Genre]], + ["Feature.GameDetails.Label.GameCopyLocked"] = [[This game is copylocked]], + ["Feature.GameDetails.Label.ReportAbuse"] = [[Report Abuse]], + ["Feature.GameDetails.Heading.RecommendedGames"] = [[Recommended Games]], + ["Feature.GameGear.Heading.GearForThisGame"] = [[Gear for this game]], + ["Feature.GameLeaderboard.Label.Clans"] = [[Clans]], + ["Feature.GameLeaderboard.Heading.Clans"] = [[Clans]], + ["Feature.GameLeaderboard.Heading.Players"] = [[Players]], + ["Feature.GameLeaderboard.Label.NoResults"] = [[No results found]], + ["Feature.GamePage.LabelPlayingPhrase"] = [[{playerCount} Playing]], + ["Feature.GamePage.ActionSeeAll"] = [[See All]], + ["Feature.GamePage.LabelNoSearchResults"] = [[No Search Results Found]], + ["Feature.GamePage.LabelCancelField"] = [[Cancel]], + ["Feature.GamePage.LabelShowingResultsFor"] = [[Showing results for]], + ["Feature.GamePage.LabelSearchInsteadFor"] = [[Search instead for]], + ["Feature.GamePage.LabelSearchYouMightMean"] = [[Did you mean:]], + ["Feature.GamePage.Label.Sponsored"] = [[Sponsored]], + ["Feature.GamePage.Message.EndOfList"] = [[OOF! You've reached the end!]], + ["Feature.GamePage.Action.BackToTop"] = [[Back to top]], + ["Feature.GamePass.Heading.PassesForThisGame"] = [[Passes for this game]], + ["Feature.Home.HeadingFriends"] = [[Friends ({friendCount})]], + ["Feature.Home.ActionSeeAll"] = [[See All]], + ["Feature.Home.Action.ViewMyFeed"] = [[View My Feed]], + ["Feature.PlayerSearchResults.Heading.PlayerResultsFor"] = [[Player Results for {startSpan}{keyword}{endSpan}]], + ["Feature.PlayerSearchResults.Label.NoMatchesAvailable"] = [[There are no matches available for "{keyword}"]], + ["Feature.PlayerSearchResults.Label.EnterMinCharacters"] = [[Please enter at least {keywordMinLength} characters.]], + ["Feature.PlayerSearchResults.Label.UnsafeInput"] = [[You have entered unsafe input. Please try your search again.]], + ["Feature.PlayerSearchResults.Label.ShowingCountOfResults"] = [[{countStartSpan}{resultsStart} - {resultsInPage} of {countEndSpan}{totalStartSpan}{totalResults}{totalEndSpan}]], + ["Feature.PlayerSearchResults.Label.AlsoKnownAsAbbreviation"] = [[aka.]], + ["Feature.PlayerSearchResults.Label.ThisIsYou"] = [[This is you]], + ["Feature.PlayerSearchResults.Label.YouAreFriends"] = [[You are friends]], + ["Feature.PlayerSearchResults.Label.YouAreFollowing"] = [[You are following]], + ["Feature.PlayerSearchResults.Action.AddFriend"] = [[Add Friend]], + ["Feature.PlayerSearchResults.Action.AcceptRequest"] = [[Accept Request]], + ["Feature.PlayerSearchResults.Action.RequestSent"] = [[Request Sent]], + ["Feature.PlayerSearchResults.Action.Chat"] = [[Chat]], + ["Feature.PlayerSearchResults.Action.JoinGame"] = [[Join Game]], + ["Feature.PrivateServers.Heading.VipServers"] = [[VIP Servers]], + ["Feature.Profile.Label.About"] = [[About]], + ["Feature.ServerList.Heading.OtherServers"] = [[Other Servers]], + ["Feature.ServerList.Heading.ServersMyFriendsAreIn"] = [[Servers My Friends Are In]], + ["Game.Launch.IDS_ROBLOX_INSTALLED"] = [[ROBLOX IS SUCCESSFULLY INSTALLED!]], + ["Game.Launch.IDS_ROBLOX_TOPLAY"] = [[Click the 'Play' button on any game to join the action!]], + ["Game.Launch.IDS_STUDIO_SUCCESS1"] = [[ROBLOX STUDIO IS SUCCESSFULLY INSTALLED!]], + ["Game.Launch.IDS_STUDIO_SUCCESS2"] = [[Click "Launch Studio" to make your new game!]], + ["Game.Launch.IDS_ROBLOX_DOWNLOAD"] = [[Download and install roblox]], + ["Game.Launch.IDS_CHECKING_FILE"] = [[Performing file check ...]], + ["Game.Launch.IDS_FILE_CHECKED"] = [[File check complete]], + ["Game.Launch.IDS_ROBLOX_STARTING"] = [[Starting %s ...]], + ["Game.Launch.IDS_ROBLOX_UPTODATE"] = [[%s is up-to-date]], + ["Game.Launch.IDS_ROBLOX_UPGRADING"] = [[Upgrading %s ...]], + ["Game.Launch.IDS_ROBLOX_INSTALLING"] = [[Installing %s ...]], + ["Game.Launch.IDS_ROBLOX_CONNECTING"] = [[Connecting to %s ...]], + ["Game.Launch.IDS_BOOTS_ASK_DOWNLOAD"] = [[Download the latest bootstrapper?]], + ["Game.Launch.IDS_BOOTS_GET_LATEST"] = [[Getting the latest %s ...]], + ["Game.Launch.IDS_PLEASE_WAIT"] = [[Please Wait ...]], + ["Game.Launch.IDS_BOOTS_SHUTDOWN"] = [[Shutting down %s]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLING"] = [[Uninstalling %s ...]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLED"] = [[%s has been uninstalled]], + ["Game.Launch.IDS_BOOTS_CONFIGURING"] = [[Configuring %s ...]], + ["Search.GlobalSearch.Example.SearchGames"] = [[Search games]], +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/es-es.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/es-es.lua new file mode 100644 index 0000000..bfeabd3 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/es-es.lua @@ -0,0 +1,194 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Application.Logout.Action.Logout"] = [[Cerrar sesión]], + ["Common.Presence.Label.Online"] = [[Conectado]], + ["Common.Presence.Label.Offline"] = [[Sin conexión]], + ["Common.VisitGame.Label.Play"] = [[Jugar]], + ["CommonUI.Features.Label.Avatar"] = [[Avatar]], + ["CommonUI.Features.Label.Blog"] = [[Blog]], + ["CommonUI.Features.Label.BuildersClub"] = [[Builders Club]], + ["CommonUI.Features.Label.Catalog"] = [[Catálogo]], + ["CommonUI.Features.Label.Chat"] = [[Chat]], + ["CommonUI.Features.Label.Events"] = [[Eventos]], + ["CommonUI.Features.Label.Friends"] = [[Amigos]], + ["CommonUI.Features.Label.Game"] = [[Juegos]], + ["CommonUI.Features.Label.Groups"] = [[Grupos]], + ["CommonUI.Features.Label.Help"] = [[Ayuda]], + ["CommonUI.Features.Label.Home"] = [[Inicio]], + ["CommonUI.Features.Label.Inventory"] = [[Inventario]], + ["CommonUI.Features.Label.Messages"] = [[Mensajes]], + ["CommonUI.Features.Label.More"] = [[Más]], + ["CommonUI.Features.Label.Profile"] = [[Perfil]], + ["CommonUI.Features.Label.Settings"] = [[Configuración]], + ["CommonUI.Features.Label.About"] = [[Acerca de Roblox]], + ["CommonUI.Features.Label.Players"] = [[Jugadores]], + ["CommonUI.Features.Label.Create"] = [[Crear]], + ["CommonUI.Features.Label.CreateGames"] = [[Crear juegos]], + ["Feature.Chat.Response.ChatNameFullyModerated"] = [[Se ha moderado el nombre del grupo de chat.]], + ["Feature.Chat.Heading.LeaveGroup"] = [[Salir del grupo]], + ["Feature.Chat.Label.SearchWord"] = [[Buscar]], + ["Feature.Chat.Action.Add"] = [[Añadir]], + ["Feature.Chat.Action.Leave"] = [[Salir]], + ["Feature.Chat.Action.Remove"] = [[Eliminar]], + ["Feature.Chat.Action.Cancel"] = [[Cancelar]], + ["Feature.Chat.Description.NameGroupChat"] = [[Poner nombre a tu grupo de chat]], + ["Feature.Chat.Heading.NewChatGroup"] = [[Nuevo grupo de chat]], + ["Feature.Chat.Label.SeeLess"] = [[Ver menos]], + ["Feature.Chat.Label.InputPlaceHolder.SearchForFriends"] = [[Buscar amigos]], + ["Feature.Chat.Label.AddFriends"] = [[Añadir amigos]], + ["Feature.Chat.Label.ChatDetails"] = [[Detalles del chat]], + ["Feature.Chat.Label.General"] = [[General]], + ["Feature.Chat.Label.Members"] = [[Miembros]], + ["Feature.Chat.Label.ViewProfile"] = [[Ver perfil]], + ["Feature.Chat.Label.ChatGroupName"] = [[Nombre del grupo de chat]], + ["Feature.Chat.Action.Stay"] = [[Quedarse]], + ["Feature.Chat.Action.Create"] = [[Crear]], + ["Feature.Chat.Action.Send"] = [[Enviar]], + ["Feature.Chat.Action.Save"] = [[Guardar]], + ["Feature.Chat.Message.ToastText"] = [[Puedes tener hasta {friendNum} amigos en un grupo de chat.]], + ["Feature.Chat.Message.NoConnectionMsg"] = [[Conectando...]], + ["Feature.Chat.Message.Default"] = [[No todos los miembros del chat pueden ver tu mensaje.]], + ["Feature.Chat.Message.MessageFilterForReceivers"] = [[No todos los miembros del chat pueden ver tu mensaje.]], + ["Feature.Chat.Message.MessageContentModerated"] = [[Tu mensaje ha sido moderado y no se ha enviado.]], + ["Feature.Chat.Label.PrivacySettings"] = [[Configuración de privacidad]], + ["Feature.Chat.Label.ChatInputPlaceholder"] = [[Di algo]], + ["Feature.Chat.Action.Confirm"] = [[Aceptar]], + ["Feature.Chat.Heading.FailedToLeaveGroup"] = [[Error al salir del grupo]], + ["Feature.Chat.Message.FailedToLeaveGroup"] = [[No se te ha podido eliminar de la conversación {CONVERSATION_TITLE}.]], + ["Feature.Chat.Heading.FailedToRemoveUser"] = [[Error al eliminar usuario]], + ["Feature.Chat.Message.FailedToRemoveUser"] = [[No se ha podido eliminar al usuario {USERNAME} de la conversación {CONVERSATION_TITLE}.]], + ["Feature.Chat.Heading.FailedToRenameConversation"] = [[Error al cambiar el nombre de la conversación]], + ["Feature.Chat.Message.FailedToRenameConversation"] = [[No se ha podido cambiar el nombre de la conversación de {EXISTING_NAME} a {NEW_NAME}.]], + ["Feature.Chat.Message.LeaveGroup"] = [[No podrás seguir chateando en este grupo.]], + ["Feature.Chat.Message.MakeFriendsToChat"] = [[Haz amigos en los juegos para empezar a chatear y jugar juntos.]], + ["Feature.Chat.Label.NotSet"] = [[Sin configurar]], + ["Feature.Chat.Heading.Option"] = [[Opción]], + ["Feature.Chat.Action.RemoveFromGroup"] = [[Eliminar del grupo]], + ["Feature.Chat.Action.RemoveUser"] = [[Eliminar usuario]], + ["Feature.Chat.Message.RemoveUser"] = [[¿Seguro que quieres eliminar a {USERNAME} de este grupo de chat?]], + ["Feature.Chat.Message.RemovedFromConversation"] = [[Has sido eliminado del grupo.]], + ["Feature.Chat.Action.ReportUser"] = [[Denunciar usuario]], + ["Feature.Chat.Label.SearchForFriendsAndChat"] = [[Buscar amigos y grupos de chat]], + ["Feature.Chat.Action.SeeMoreFriends"] = [[Ver más ({NUMBER_OF_FRIENDS})]], + ["Feature.Chat.Label.Sent"] = [[Enviado]], + ["Feature.Chat.Heading.ShareGameToChat"] = [[Compartir en el chat]], + ["Feature.Chat.Message.TurnOnChat"] = [[Para chatear con tus amigos, activa el chat en la configuración de privacidad.]], + ["Feature.Chat.Action.ViewAssetDetails"] = [[Ver detalles]], + ["Feature.Chat.Label.ByBuilder"] = [[De {USERNAME}]], + ["Feature.Chat.Message.AlreadyPinnedGame"] = [[Este juego ya ha sido anclado. Intenta con otro.]], + ["Feature.Chat.Message.PinFailed"] = [[El juego no se ha podido anclar. Inténtalo de nuevo más tarde.]], + ["Feature.Chat.Message.UnpinFailed"] = [[Este juego no se ha podido desanclar. Inténtalo de nuevo más tarde.]], + ["Feature.Chat.Drawer.ShowMore"] = [[Mostrar más]], + ["Feature.Chat.Drawer.ShowLess"] = [[Mostrar menos]], + ["Feature.Chat.Drawer.NoGames"] = [[No hay juegos anclados o en curso.]], + ["Feature.Chat.Drawer.Recommended"] = [[Recomendados]], + ["Feature.Chat.Drawer.Loading"] = [[Cargando]], + ["Feature.Chat.Drawer.PinnedGame"] = [[Juego anclado]], + ["Feature.Chat.Drawer.Play"] = [[Jugar]], + ["Feature.Chat.Drawer.Join"] = [[Unirse]], + ["Feature.Chat.Drawer.PlayGame"] = [[Jugar]], + ["Feature.Chat.Drawer.PinGame"] = [[Anclar juego]], + ["Feature.Chat.Drawer.UnpinGame"] = [[Desanclar juego]], + ["Feature.Chat.Drawer.ViewDetails"] = [[Ver detalles]], + ["Feature.Chat.Label.NoDescriptionYet"] = [[Aún sin descripción.]], + ["Feature.Chat.Message.GameLinkWasModerated"] = [[Este enlace del juego ha sido moderado y no se ha enviado.]], + ["Feature.Chat.ShareGameToChat.BrowseGames"] = [[Buscar juegos]], + ["Feature.Chat.ShareGameToChat.By"] = [[de {creatorName}]], + ["Feature.Chat.ShareGameToChat.Popular"] = [[Populares]], + ["Feature.Chat.ShareGameToChat.Recent"] = [[Recientes]], + ["Feature.Chat.ShareGameToChat.Favorites"] = [[Favoritos]], + ["Feature.Chat.ShareGameToChat.FriendActivity"] = [[Actividad de tus amigos]], + ["Feature.Chat.ShareGameToChat.NoPopularGames"] = [[No hay juegos populares disponibles]], + ["Feature.Chat.ShareGameToChat.NoRecentGames"] = [[No tienes ningún juego reciente]], + ["Feature.Chat.ShareGameToChat.NoFavoriteGames"] = [[No tienes ningún juego favorito]], + ["Feature.Chat.ShareGameToChat.NoFriendActivity"] = [[No tienes ninguna actividad de amigo]], + ["Feature.Chat.ShareGameToChat.GameNotAvailable"] = [[No disponible]], + ["Feature.Chat.ShareGameToChat.FailedToShareTheGame"] = [[No se puede compartir el juego. Inténtalo de nuevo más tarde.]], + ["Feature.Chat.Heading.ChatWithFriends"] = [[Chatear con amigos]], + ["Feature.Chat.Action.StartChatWithFriends"] = [[Chatear]], + ["Feature.Friends.Label.SearchFriends"] = [[Buscar amigos]], + ["Feature.GameBadges.HeadingGameBadges"] = [[Emblemas del juego]], + ["Feature.GameDetails.Label.By"] = [[De]], + ["Feature.GameDetails.Label.About"] = [[Info.]], + ["Feature.GameDetails.Label.Store"] = [[Tienda]], + ["Feature.GameDetails.Label.Leaderboards"] = [[Clasificación]], + ["Feature.GameDetails.Label.Servers"] = [[Servidores]], + ["Feature.GameDetails.Heading.Description"] = [[Descripción]], + ["Feature.GameDetails.Label.Playing"] = [[Jugando]], + ["Feature.GameDetails.Label.Visits"] = [[Visitas]], + ["Feature.GameDetails.Label.Created"] = [[Creado]], + ["Feature.GameDetails.Label.Updated"] = [[Actualizado]], + ["Feature.GameDetails.Label.MaxPlayers"] = [[Máximo de jugadores]], + ["Feature.GameDetails.Label.Genre"] = [[Género]], + ["Feature.GameDetails.Label.GameCopyLocked"] = [[Este juego tiene bloqueo anticopia]], + ["Feature.GameDetails.Label.ReportAbuse"] = [[Denunciar abuso]], + ["Feature.GameDetails.Heading.RecommendedGames"] = [[Juegos recomendados]], + ["Feature.GameGear.Heading.GearForThisGame"] = [[Equipamiento]], + ["Feature.GameLeaderboard.Label.Clans"] = [[Clanes]], + ["Feature.GameLeaderboard.Heading.Clans"] = [[Clanes]], + ["Feature.GameLeaderboard.Heading.Players"] = [[Jugadores]], + ["Feature.GameLeaderboard.Label.NoResults"] = [[Sin resultados]], + ["Feature.GamePage.LabelPlayingPhrase"] = [[{playerCount} jugando]], + ["Feature.GamePage.ActionSeeAll"] = [[Ver todo]], + ["Feature.GamePage.LabelNoSearchResults"] = [[La búsqueda no ha dado resultados.]], + ["Feature.GamePage.LabelCancelField"] = [[Cancelar]], + ["Feature.GamePage.LabelShowingResultsFor"] = [[Resultados de]], + ["Feature.GamePage.LabelSearchInsteadFor"] = [[Buscar, en cambio,]], + ["Feature.GamePage.LabelSearchYouMightMean"] = [[Búsqueda alternativa:]], + ["Feature.GamePage.Label.Sponsored"] = [[Patrocinado]], + ["Feature.GamePage.Message.EndOfList"] = [[¡UF! ¡Has llegado al final!]], + ["Feature.GamePage.Action.BackToTop"] = [[Volver arriba]], + ["Feature.GamePass.Heading.PassesForThisGame"] = [[Pases para este juego]], + ["Feature.Home.HeadingFriends"] = [[Amigos ({friendCount})]], + ["Feature.Home.ActionSeeAll"] = [[Ver todo]], + ["Feature.Home.Action.ViewMyFeed"] = [[Ver mis noticias]], + ["Feature.PlayerSearchResults.Heading.PlayerResultsFor"] = [[Resultados de jugadores para {startSpan}{keyword}{endSpan}]], + ["Feature.PlayerSearchResults.Label.NoMatchesAvailable"] = [[No hay resultados disponibles para «{keyword}».]], + ["Feature.PlayerSearchResults.Label.EnterMinCharacters"] = [[Introduce al menos {keywordMinLength} caracteres.]], + ["Feature.PlayerSearchResults.Label.UnsafeInput"] = [[Has introducido una cadena no segura. Intenta hacer otra búsqueda.]], + ["Feature.PlayerSearchResults.Label.ShowingCountOfResults"] = [[{countStartSpan}{resultsStart}: {resultsInPage} de {countEndSpan}{totalStartSpan}{totalResults}{totalEndSpan}]], + ["Feature.PlayerSearchResults.Label.AlsoKnownAsAbbreviation"] = [[alias]], + ["Feature.PlayerSearchResults.Label.ThisIsYou"] = [[Este eres tú]], + ["Feature.PlayerSearchResults.Label.YouAreFriends"] = [[Sois amigos]], + ["Feature.PlayerSearchResults.Label.YouAreFollowing"] = [[Lo sigues]], + ["Feature.PlayerSearchResults.Action.AddFriend"] = [[Añadir amigo]], + ["Feature.PlayerSearchResults.Action.AcceptRequest"] = [[Aceptar solicitud]], + ["Feature.PlayerSearchResults.Action.RequestSent"] = [[Solicitud enviada]], + ["Feature.PlayerSearchResults.Action.Chat"] = [[Chat]], + ["Feature.PlayerSearchResults.Action.JoinGame"] = [[Unirse al juego]], + ["Feature.PrivateServers.Heading.VipServers"] = [[Servidores VIP]], + ["Feature.Profile.Label.About"] = [[Información]], + ["Feature.ServerList.Heading.OtherServers"] = [[Otros servidores]], + ["Feature.ServerList.Heading.ServersMyFriendsAreIn"] = [[Servidores en los que están mis amigos]], + ["Game.Launch.IDS_ROBLOX_INSTALLED"] = [[¡ROBLOX SE HA INSTALADO CORRECTAMENTE!]], + ["Game.Launch.IDS_ROBLOX_TOPLAY"] = [[¡Haz clic en el botón Jugar en cualquier juego para unirte a la acción!]], + ["Game.Launch.IDS_STUDIO_SUCCESS1"] = [[¡ROBLOX STUDIO SE HA INSTALADO CORRECTAMENTE!]], + ["Game.Launch.IDS_STUDIO_SUCCESS2"] = [[¡Haz clic en Lanzar Studio para crear tu nuevo juego!]], + ["Game.Launch.IDS_ROBLOX_DOWNLOAD"] = [[Descargar e instalar Roblox]], + ["Game.Launch.IDS_CHECKING_FILE"] = [[Realizando verificación de archivo...]], + ["Game.Launch.IDS_FILE_CHECKED"] = [[Verificación de archivo finalizada]], + ["Game.Launch.IDS_ROBLOX_STARTING"] = [[Inicializando %s...]], + ["Game.Launch.IDS_ROBLOX_UPTODATE"] = [[%s está actualizado]], + ["Game.Launch.IDS_ROBLOX_UPGRADING"] = [[Actualizando %s...]], + ["Game.Launch.IDS_ROBLOX_INSTALLING"] = [[Instalando %s...]], + ["Game.Launch.IDS_ROBLOX_CONNECTING"] = [[Conectando a %s...]], + ["Game.Launch.IDS_BOOTS_ASK_DOWNLOAD"] = [[¿Descargar la versión más reciente del programa de arranque?]], + ["Game.Launch.IDS_BOOTS_GET_LATEST"] = [[Obteniendo la versión más reciente de %s...]], + ["Game.Launch.IDS_PLEASE_WAIT"] = [[Espera...]], + ["Game.Launch.IDS_BOOTS_SHUTDOWN"] = [[Cerrando %s]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLING"] = [[Desinstalando %s...]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLED"] = [[%s ha sido desinstalado]], + ["Game.Launch.IDS_BOOTS_CONFIGURING"] = [[Configurando %s...]], + ["Search.GlobalSearch.Example.SearchGames"] = [[Buscar juegos]], +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/fr-fr.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/fr-fr.lua new file mode 100644 index 0000000..8ef0f76 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/fr-fr.lua @@ -0,0 +1,194 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Application.Logout.Action.Logout"] = [[Déconnexion]], + ["Common.Presence.Label.Online"] = [[Connecté]], + ["Common.Presence.Label.Offline"] = [[Déconnecté]], + ["Common.VisitGame.Label.Play"] = [[Jouer]], + ["CommonUI.Features.Label.Avatar"] = [[Avatar]], + ["CommonUI.Features.Label.Blog"] = [[Blog]], + ["CommonUI.Features.Label.BuildersClub"] = [[Builders Club]], + ["CommonUI.Features.Label.Catalog"] = [[Catalogue]], + ["CommonUI.Features.Label.Chat"] = [[Chat]], + ["CommonUI.Features.Label.Events"] = [[Événements]], + ["CommonUI.Features.Label.Friends"] = [[Amis]], + ["CommonUI.Features.Label.Game"] = [[Jeux]], + ["CommonUI.Features.Label.Groups"] = [[Groupes]], + ["CommonUI.Features.Label.Help"] = [[Aide]], + ["CommonUI.Features.Label.Home"] = [[Accueil]], + ["CommonUI.Features.Label.Inventory"] = [[Inventaire]], + ["CommonUI.Features.Label.Messages"] = [[Messages]], + ["CommonUI.Features.Label.More"] = [[Plus]], + ["CommonUI.Features.Label.Profile"] = [[Profil]], + ["CommonUI.Features.Label.Settings"] = [[Paramètres]], + ["CommonUI.Features.Label.About"] = [[À propos de]], + ["CommonUI.Features.Label.Players"] = [[Joueurs]], + ["CommonUI.Features.Label.Create"] = [[Créer]], + ["CommonUI.Features.Label.CreateGames"] = [[Créer des parties]], + ["Feature.Chat.Response.ChatNameFullyModerated"] = [[Le nom du groupe de chat a été modéré.]], + ["Feature.Chat.Heading.LeaveGroup"] = [[Quitter le groupe]], + ["Feature.Chat.Label.SearchWord"] = [[Rechercher]], + ["Feature.Chat.Action.Add"] = [[Ajouter]], + ["Feature.Chat.Action.Leave"] = [[Quitter]], + ["Feature.Chat.Action.Remove"] = [[Retirer]], + ["Feature.Chat.Action.Cancel"] = [[Annuler]], + ["Feature.Chat.Description.NameGroupChat"] = [[Nommez votre groupe de chat]], + ["Feature.Chat.Heading.NewChatGroup"] = [[Nouveau groupe de chat]], + ["Feature.Chat.Label.SeeLess"] = [[Afficher moins]], + ["Feature.Chat.Label.InputPlaceHolder.SearchForFriends"] = [[Rechercher des amis]], + ["Feature.Chat.Label.AddFriends"] = [[Ajouter des amis]], + ["Feature.Chat.Label.ChatDetails"] = [[Détails du chat]], + ["Feature.Chat.Label.General"] = [[Général]], + ["Feature.Chat.Label.Members"] = [[Membres]], + ["Feature.Chat.Label.ViewProfile"] = [[Voir le profil]], + ["Feature.Chat.Label.ChatGroupName"] = [[Nom du groupe de chat]], + ["Feature.Chat.Action.Stay"] = [[Rester]], + ["Feature.Chat.Action.Create"] = [[Créer]], + ["Feature.Chat.Action.Send"] = [[Envoyer]], + ["Feature.Chat.Action.Save"] = [[Enregistrer]], + ["Feature.Chat.Message.ToastText"] = [[Un groupe de chat peut compter jusqu'à {friendNum} amis.]], + ["Feature.Chat.Message.NoConnectionMsg"] = [[Connexion...]], + ["Feature.Chat.Message.Default"] = [[Certains membres de ce chat ne peuvent pas lire votre message.]], + ["Feature.Chat.Message.MessageFilterForReceivers"] = [[Certains membres de ce chat ne peuvent pas lire votre message.]], + ["Feature.Chat.Message.MessageContentModerated"] = [[Votre message a été modéré et n'a pas été envoyé.]], + ["Feature.Chat.Label.PrivacySettings"] = [[Paramètres de confidentialité]], + ["Feature.Chat.Label.ChatInputPlaceholder"] = [[Dites quelque chose]], + ["Feature.Chat.Action.Confirm"] = [[D'accord]], + ["Feature.Chat.Heading.FailedToLeaveGroup"] = [[Impossible de quitter le groupe]], + ["Feature.Chat.Message.FailedToLeaveGroup"] = [[Vous n'avez pas pu être retiré(e) de la conversation : {CONVERSATION_TITLE}.]], + ["Feature.Chat.Heading.FailedToRemoveUser"] = [[Impossible de retirer l'utilisateur]], + ["Feature.Chat.Message.FailedToRemoveUser"] = [[L'utilisateur {USERNAME} n'a pas pu être retiré de la conversation : {CONVERSATION_TITLE}.]], + ["Feature.Chat.Heading.FailedToRenameConversation"] = [[Impossible de renommer la conversation]], + ["Feature.Chat.Message.FailedToRenameConversation"] = [[La conversation {EXISTING_NAME} n'a pas pu être renommée en : {NEW_NAME}.]], + ["Feature.Chat.Message.LeaveGroup"] = [[Vous ne pourrez plus continuer à discuter avec ce groupe.]], + ["Feature.Chat.Message.MakeFriendsToChat"] = [[Faites-vous des amis en jeu pour commencer à discuter et jouer ensemble.]], + ["Feature.Chat.Label.NotSet"] = [[Non défini]], + ["Feature.Chat.Heading.Option"] = [[Option]], + ["Feature.Chat.Action.RemoveFromGroup"] = [[Retirer du groupe]], + ["Feature.Chat.Action.RemoveUser"] = [[Retirer l'utilisateur]], + ["Feature.Chat.Message.RemoveUser"] = [[Voulez-vous vraiment retirer {USERNAME} de ce groupe de chat ?]], + ["Feature.Chat.Message.RemovedFromConversation"] = [[Vous avez été retiré(e) du groupe.]], + ["Feature.Chat.Action.ReportUser"] = [[Signaler l'utilisateur]], + ["Feature.Chat.Label.SearchForFriendsAndChat"] = [[Rechercher des amis et des groupes de chat]], + ["Feature.Chat.Action.SeeMoreFriends"] = [[Afficher plus ({NUMBER_OF_FRIENDS})]], + ["Feature.Chat.Label.Sent"] = [[Envoyé]], + ["Feature.Chat.Heading.ShareGameToChat"] = [[Partager le jeu dans le chat]], + ["Feature.Chat.Message.TurnOnChat"] = [[Pour discuter avec vos amis, activez le chat dans vos paramètres de confidentialité.]], + ["Feature.Chat.Action.ViewAssetDetails"] = [[Voir les détails]], + ["Feature.Chat.Label.ByBuilder"] = [[Par {USERNAME}]], + ["Feature.Chat.Message.AlreadyPinnedGame"] = [[Ce jeu a déjà été épinglé, veuillez essayer avec un autre.]], + ["Feature.Chat.Message.PinFailed"] = [[Impossible d'épingler ce jeu, veuillez réessayer plus tard.]], + ["Feature.Chat.Message.UnpinFailed"] = [[Impossible de retirer l'épingle de ce jeu, veuillez réessayer plus tard.]], + ["Feature.Chat.Drawer.ShowMore"] = [[Afficher plus]], + ["Feature.Chat.Drawer.ShowLess"] = [[Afficher moins]], + ["Feature.Chat.Drawer.NoGames"] = [[Aucun jeu épinglé ou en cours.]], + ["Feature.Chat.Drawer.Recommended"] = [[Recommandés]], + ["Feature.Chat.Drawer.Loading"] = [[Chargement]], + ["Feature.Chat.Drawer.PinnedGame"] = [[Jeu épinglé]], + ["Feature.Chat.Drawer.Play"] = [[Jouer]], + ["Feature.Chat.Drawer.Join"] = [[Rejoindre]], + ["Feature.Chat.Drawer.PlayGame"] = [[Jouer]], + ["Feature.Chat.Drawer.PinGame"] = [[Épingler le jeu]], + ["Feature.Chat.Drawer.UnpinGame"] = [[Ne plus épingler le jeu]], + ["Feature.Chat.Drawer.ViewDetails"] = [[Afficher les détails]], + ["Feature.Chat.Label.NoDescriptionYet"] = [[Aucune description pour le moment.]], + ["Feature.Chat.Message.GameLinkWasModerated"] = [[Ce lien a été modéré et n'a pas été envoyé.]], + ["Feature.Chat.ShareGameToChat.BrowseGames"] = [[Parcourir les jeux]], + ["Feature.Chat.ShareGameToChat.By"] = [[par {creatorName}]], + ["Feature.Chat.ShareGameToChat.Popular"] = [[Populaire]], + ["Feature.Chat.ShareGameToChat.Recent"] = [[Récents]], + ["Feature.Chat.ShareGameToChat.Favorites"] = [[Favoris]], + ["Feature.Chat.ShareGameToChat.FriendActivity"] = [[Activité des amis]], + ["Feature.Chat.ShareGameToChat.NoPopularGames"] = [[Aucun jeu populaire disponible]], + ["Feature.Chat.ShareGameToChat.NoRecentGames"] = [[Aucun jeu récent]], + ["Feature.Chat.ShareGameToChat.NoFavoriteGames"] = [[Aucun jeu favori]], + ["Feature.Chat.ShareGameToChat.NoFriendActivity"] = [[Aucune activité des amis]], + ["Feature.Chat.ShareGameToChat.GameNotAvailable"] = [[Non disponible]], + ["Feature.Chat.ShareGameToChat.FailedToShareTheGame"] = [[Impossible de partager le jeu, veuillez réessayer plus tard.]], + ["Feature.Chat.Heading.ChatWithFriends"] = [[Discuter avec des amis]], + ["Feature.Chat.Action.StartChatWithFriends"] = [[Discuter]], + ["Feature.Friends.Label.SearchFriends"] = [[Rechercher des amis]], + ["Feature.GameBadges.HeadingGameBadges"] = [[Badges du jeu]], + ["Feature.GameDetails.Label.By"] = [[Par]], + ["Feature.GameDetails.Label.About"] = [[À propos]], + ["Feature.GameDetails.Label.Store"] = [[Boutique]], + ["Feature.GameDetails.Label.Leaderboards"] = [[Classements]], + ["Feature.GameDetails.Label.Servers"] = [[Serveurs]], + ["Feature.GameDetails.Heading.Description"] = [[Description]], + ["Feature.GameDetails.Label.Playing"] = [[En jeu]], + ["Feature.GameDetails.Label.Visits"] = [[Visites]], + ["Feature.GameDetails.Label.Created"] = [[Créé]], + ["Feature.GameDetails.Label.Updated"] = [[Mis à jour]], + ["Feature.GameDetails.Label.MaxPlayers"] = [[Joueurs max.]], + ["Feature.GameDetails.Label.Genre"] = [[Genre]], + ["Feature.GameDetails.Label.GameCopyLocked"] = [[Impossible de copier ce jeu]], + ["Feature.GameDetails.Label.ReportAbuse"] = [[Signaler un comportement abusif]], + ["Feature.GameDetails.Heading.RecommendedGames"] = [[Jeux recommandés]], + ["Feature.GameGear.Heading.GearForThisGame"] = [[Équipement pour ce jeu]], + ["Feature.GameLeaderboard.Label.Clans"] = [[Clans]], + ["Feature.GameLeaderboard.Heading.Clans"] = [[Clans]], + ["Feature.GameLeaderboard.Heading.Players"] = [[Joueurs]], + ["Feature.GameLeaderboard.Label.NoResults"] = [[Aucun résultat trouvé]], + ["Feature.GamePage.LabelPlayingPhrase"] = [[{playerCount} en jeu]], + ["Feature.GamePage.ActionSeeAll"] = [[Afficher tout]], + ["Feature.GamePage.LabelNoSearchResults"] = [[Aucun résultat trouvé]], + ["Feature.GamePage.LabelCancelField"] = [[Annuler]], + ["Feature.GamePage.LabelShowingResultsFor"] = [[Afficher les résultats pour]], + ["Feature.GamePage.LabelSearchInsteadFor"] = [[Rechercher plutôt]], + ["Feature.GamePage.LabelSearchYouMightMean"] = [[Voulez-vous dire :]], + ["Feature.GamePage.Label.Sponsored"] = [[Parrainé]], + ["Feature.GamePage.Message.EndOfList"] = [[Vous avez atteint la fin !]], + ["Feature.GamePage.Action.BackToTop"] = [[Haut de la page]], + ["Feature.GamePass.Heading.PassesForThisGame"] = [[Passes pour ce jeu]], + ["Feature.Home.HeadingFriends"] = [[Amis ({friendCount})]], + ["Feature.Home.ActionSeeAll"] = [[Afficher tout]], + ["Feature.Home.Action.ViewMyFeed"] = [[Voir mes actus]], + ["Feature.PlayerSearchResults.Heading.PlayerResultsFor"] = [[Résultats de joueurs pour {startSpan}{keyword}{endSpan}]], + ["Feature.PlayerSearchResults.Label.NoMatchesAvailable"] = [[Aucun résultat disponible pour « {keyword} ».]], + ["Feature.PlayerSearchResults.Label.EnterMinCharacters"] = [[Veuillez saisir au moins {keywordMinLength} caractères.]], + ["Feature.PlayerSearchResults.Label.UnsafeInput"] = [[Saisie non conforme au règlement. Veuillez recommencer votre recherche.]], + ["Feature.PlayerSearchResults.Label.ShowingCountOfResults"] = [[{countStartSpan}{resultsStart} - {resultsInPage} sur {countEndSpan}{totalStartSpan}{totalResults}{totalEndSpan}]], + ["Feature.PlayerSearchResults.Label.AlsoKnownAsAbbreviation"] = [[alias]], + ["Feature.PlayerSearchResults.Label.ThisIsYou"] = [[C'est vous]], + ["Feature.PlayerSearchResults.Label.YouAreFriends"] = [[Vous êtes amis]], + ["Feature.PlayerSearchResults.Label.YouAreFollowing"] = [[Vous suivez]], + ["Feature.PlayerSearchResults.Action.AddFriend"] = [[Ajouter ami]], + ["Feature.PlayerSearchResults.Action.AcceptRequest"] = [[Accepter la demande]], + ["Feature.PlayerSearchResults.Action.RequestSent"] = [[Demande envoyée]], + ["Feature.PlayerSearchResults.Action.Chat"] = [[Chat]], + ["Feature.PlayerSearchResults.Action.JoinGame"] = [[Rejoindre la partie]], + ["Feature.PrivateServers.Heading.VipServers"] = [[Serveurs VIP]], + ["Feature.Profile.Label.About"] = [[À propos]], + ["Feature.ServerList.Heading.OtherServers"] = [[Autres serveurs]], + ["Feature.ServerList.Heading.ServersMyFriendsAreIn"] = [[Serveurs où se trouvent mes amis]], + ["Game.Launch.IDS_ROBLOX_INSTALLED"] = [[ROBLOX A ÉTÉ INSTALLÉ AVEC SUCCÈS !]], + ["Game.Launch.IDS_ROBLOX_TOPLAY"] = [[Cliquez sur « Jouer » sur n'importe quel jeu pour plonger dans l'action !]], + ["Game.Launch.IDS_STUDIO_SUCCESS1"] = [[ROBLOX STUDIO A ÉTÉ INSTALLÉ AVEC SUCCÈS !]], + ["Game.Launch.IDS_STUDIO_SUCCESS2"] = [[Cliquez sur « Lancer Studio » pour créer votre nouveau jeu !]], + ["Game.Launch.IDS_ROBLOX_DOWNLOAD"] = [[Télécharger et installer Roblox]], + ["Game.Launch.IDS_CHECKING_FILE"] = [[Vérification du fichier...]], + ["Game.Launch.IDS_FILE_CHECKED"] = [[Vérification du fichier terminée]], + ["Game.Launch.IDS_ROBLOX_STARTING"] = [[Lancement de : %s...]], + ["Game.Launch.IDS_ROBLOX_UPTODATE"] = [[%s est à jour]], + ["Game.Launch.IDS_ROBLOX_UPGRADING"] = [[Amélioration de : %s...]], + ["Game.Launch.IDS_ROBLOX_INSTALLING"] = [[Installation de : %s...]], + ["Game.Launch.IDS_ROBLOX_CONNECTING"] = [[Connexion à : %s...]], + ["Game.Launch.IDS_BOOTS_ASK_DOWNLOAD"] = [[Télécharger le plus récent bootstrapper ?]], + ["Game.Launch.IDS_BOOTS_GET_LATEST"] = [[Récupération de : %s…]], + ["Game.Launch.IDS_PLEASE_WAIT"] = [[Veuillez patienter...]], + ["Game.Launch.IDS_BOOTS_SHUTDOWN"] = [[Fermeture de : %s]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLING"] = [[Désinstallation de : %s...]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLED"] = [[%s a été désinstallé]], + ["Game.Launch.IDS_BOOTS_CONFIGURING"] = [[Configuration de : %s...]], + ["Search.GlobalSearch.Example.SearchGames"] = [[Rechercher des jeux]], +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/id-id.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/id-id.lua new file mode 100644 index 0000000..d5f78e4 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/id-id.lua @@ -0,0 +1,34 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["CommonUI.Features.Label.Avatar"] = [[Avatar]], + ["CommonUI.Features.Label.Blog"] = [[Blog]], + ["CommonUI.Features.Label.BuildersClub"] = [[Klub Perakit]], + ["CommonUI.Features.Label.Catalog"] = [[Katalog]], + ["CommonUI.Features.Label.Chat"] = [[Obrolan]], + ["CommonUI.Features.Label.Events"] = [[Event]], + ["CommonUI.Features.Label.Friends"] = [[Teman]], + ["CommonUI.Features.Label.Game"] = [[Game]], + ["CommonUI.Features.Label.Groups"] = [[Grup]], + ["CommonUI.Features.Label.Help"] = [[Bantuan]], + ["CommonUI.Features.Label.Home"] = [[Beranda]], + ["CommonUI.Features.Label.Inventory"] = [[Inventaris]], + ["CommonUI.Features.Label.Messages"] = [[Pesan]], + ["CommonUI.Features.Label.More"] = [[Lainnya]], + ["CommonUI.Features.Label.Profile"] = [[Profil]], + ["CommonUI.Features.Label.Settings"] = [[Pengaturan]], + ["CommonUI.Features.Label.About"] = [[Tentang]], + ["CommonUI.Features.Label.Players"] = [[Pemain]], + ["CommonUI.Features.Label.Create"] = [[Buat]], + ["CommonUI.Features.Label.CreateGames"] = [[Buat Game]], +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/it-it.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/it-it.lua new file mode 100644 index 0000000..6011e16 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/it-it.lua @@ -0,0 +1,34 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["CommonUI.Features.Label.Avatar"] = [[Avatar]], + ["CommonUI.Features.Label.Blog"] = [[Blog]], + ["CommonUI.Features.Label.BuildersClub"] = [[Builders Club]], + ["CommonUI.Features.Label.Catalog"] = [[Catalogo]], + ["CommonUI.Features.Label.Chat"] = [[Chat]], + ["CommonUI.Features.Label.Events"] = [[Eventi]], + ["CommonUI.Features.Label.Friends"] = [[Amici]], + ["CommonUI.Features.Label.Game"] = [[Giochi]], + ["CommonUI.Features.Label.Groups"] = [[Gruppi]], + ["CommonUI.Features.Label.Help"] = [[Guida]], + ["CommonUI.Features.Label.Home"] = [[Home]], + ["CommonUI.Features.Label.Inventory"] = [[Inventario]], + ["CommonUI.Features.Label.Messages"] = [[Messaggi]], + ["CommonUI.Features.Label.More"] = [[Altro]], + ["CommonUI.Features.Label.Profile"] = [[Profilo]], + ["CommonUI.Features.Label.Settings"] = [[Impostazioni]], + ["CommonUI.Features.Label.About"] = [[Info]], + ["CommonUI.Features.Label.Players"] = [[Giocatori]], + ["CommonUI.Features.Label.Create"] = [[Crea]], + ["CommonUI.Features.Label.CreateGames"] = [[Crea partite]], +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/ja-jp.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/ja-jp.lua new file mode 100644 index 0000000..98318f0 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/ja-jp.lua @@ -0,0 +1,34 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["CommonUI.Features.Label.Avatar"] = [[アバター]], + ["CommonUI.Features.Label.Blog"] = [[ブログ]], + ["CommonUI.Features.Label.BuildersClub"] = [[ビルダーズ ・ クラブ]], + ["CommonUI.Features.Label.Catalog"] = [[カタログ]], + ["CommonUI.Features.Label.Chat"] = [[チャット]], + ["CommonUI.Features.Label.Events"] = [[イベント]], + ["CommonUI.Features.Label.Friends"] = [[友達]], + ["CommonUI.Features.Label.Game"] = [[ゲーム]], + ["CommonUI.Features.Label.Groups"] = [[グループ]], + ["CommonUI.Features.Label.Help"] = [[ヘルプ]], + ["CommonUI.Features.Label.Home"] = [[ホーム]], + ["CommonUI.Features.Label.Inventory"] = [[インベントリ]], + ["CommonUI.Features.Label.Messages"] = [[メッセージ]], + ["CommonUI.Features.Label.More"] = [[その他]], + ["CommonUI.Features.Label.Profile"] = [[プロフィール]], + ["CommonUI.Features.Label.Settings"] = [[設定]], + ["CommonUI.Features.Label.About"] = [[情報]], + ["CommonUI.Features.Label.Players"] = [[プレイヤー]], + ["CommonUI.Features.Label.Create"] = [[作成する]], + ["CommonUI.Features.Label.CreateGames"] = [[ゲームを作成する]], +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/ko-kr.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/ko-kr.lua new file mode 100644 index 0000000..bd6551c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/ko-kr.lua @@ -0,0 +1,194 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Application.Logout.Action.Logout"] = [[로그아웃]], + ["Common.Presence.Label.Online"] = [[온라인]], + ["Common.Presence.Label.Offline"] = [[오프라인]], + ["Common.VisitGame.Label.Play"] = [[플레이]], + ["CommonUI.Features.Label.Avatar"] = [[아바타]], + ["CommonUI.Features.Label.Blog"] = [[블로그]], + ["CommonUI.Features.Label.BuildersClub"] = [[빌더 클럽]], + ["CommonUI.Features.Label.Catalog"] = [[카탈로그]], + ["CommonUI.Features.Label.Chat"] = [[채팅]], + ["CommonUI.Features.Label.Events"] = [[이벤트]], + ["CommonUI.Features.Label.Friends"] = [[친구]], + ["CommonUI.Features.Label.Game"] = [[게임]], + ["CommonUI.Features.Label.Groups"] = [[그룹]], + ["CommonUI.Features.Label.Help"] = [[도움말]], + ["CommonUI.Features.Label.Home"] = [[홈]], + ["CommonUI.Features.Label.Inventory"] = [[인벤토리]], + ["CommonUI.Features.Label.Messages"] = [[메시지]], + ["CommonUI.Features.Label.More"] = [[더 보기]], + ["CommonUI.Features.Label.Profile"] = [[프로필]], + ["CommonUI.Features.Label.Settings"] = [[설정]], + ["CommonUI.Features.Label.About"] = [[소개]], + ["CommonUI.Features.Label.Players"] = [[플레이어]], + ["CommonUI.Features.Label.Create"] = [[생성]], + ["CommonUI.Features.Label.CreateGames"] = [[게임 생성]], + ["Feature.Chat.Response.ChatNameFullyModerated"] = [[채팅 그룹 이름이 검토 요청을 받았어요.]], + ["Feature.Chat.Heading.LeaveGroup"] = [[그룹 나가기]], + ["Feature.Chat.Label.SearchWord"] = [[검색]], + ["Feature.Chat.Action.Add"] = [[추가]], + ["Feature.Chat.Action.Leave"] = [[나가기]], + ["Feature.Chat.Action.Remove"] = [[제거]], + ["Feature.Chat.Action.Cancel"] = [[취소]], + ["Feature.Chat.Description.NameGroupChat"] = [[채팅 그룹에 이름을 붙이세요]], + ["Feature.Chat.Heading.NewChatGroup"] = [[새 채팅 그룹]], + ["Feature.Chat.Label.SeeLess"] = [[감추기]], + ["Feature.Chat.Label.InputPlaceHolder.SearchForFriends"] = [[친구 찾아보기]], + ["Feature.Chat.Label.AddFriends"] = [[친구 추가]], + ["Feature.Chat.Label.ChatDetails"] = [[채팅 정보]], + ["Feature.Chat.Label.General"] = [[일반]], + ["Feature.Chat.Label.Members"] = [[멤버]], + ["Feature.Chat.Label.ViewProfile"] = [[프로필 보기]], + ["Feature.Chat.Label.ChatGroupName"] = [[채팅 그룹 이름]], + ["Feature.Chat.Action.Stay"] = [[유지]], + ["Feature.Chat.Action.Create"] = [[만들기]], + ["Feature.Chat.Action.Send"] = [[보내기]], + ["Feature.Chat.Action.Save"] = [[저장]], + ["Feature.Chat.Message.ToastText"] = [[채팅 그룹의 친구 정원은 {friendNum}명이에요.]], + ["Feature.Chat.Message.NoConnectionMsg"] = [[연결 중...]], + ["Feature.Chat.Message.Default"] = [[이 채팅 참가자 중 일부는 메시지를 볼 수 없어요.]], + ["Feature.Chat.Message.MessageFilterForReceivers"] = [[이 채팅 참가자 중 일부는 메시지를 볼 수 없어요.]], + ["Feature.Chat.Message.MessageContentModerated"] = [[메시지가 검토 요청을 받아 전송되지 않았어요.]], + ["Feature.Chat.Label.PrivacySettings"] = [[개인정보 설정]], + ["Feature.Chat.Label.ChatInputPlaceholder"] = [[무엇이든 말씀하세요]], + ["Feature.Chat.Action.Confirm"] = [[확인]], + ["Feature.Chat.Heading.FailedToLeaveGroup"] = [[그룹 나가기 실패]], + ["Feature.Chat.Message.FailedToLeaveGroup"] = [[{CONVERSATION_TITLE} 대화에서 회원님을 제거할 수 없어요.]], + ["Feature.Chat.Heading.FailedToRemoveUser"] = [[사용자 제거 실패]], + ["Feature.Chat.Message.FailedToRemoveUser"] = [[{CONVERSATION_TITLE} 대화에서 {USERNAME}님을 제거할 수 없어요.]], + ["Feature.Chat.Heading.FailedToRenameConversation"] = [[대화 이름 변경 실패]], + ["Feature.Chat.Message.FailedToRenameConversation"] = [[대화 이름 {EXISTING_NAME}을(를) {NEW_NAME}(으)로 변경할 수 없어요.]], + ["Feature.Chat.Message.LeaveGroup"] = [[회원님은 이 그룹과 채팅을 유지할 수 없게 돼요.]], + ["Feature.Chat.Message.MakeFriendsToChat"] = [[함께 이야기를 나누고 놀려면 게임에서 친구를 사귀세요.]], + ["Feature.Chat.Label.NotSet"] = [[설정 안 됨]], + ["Feature.Chat.Heading.Option"] = [[옵션]], + ["Feature.Chat.Action.RemoveFromGroup"] = [[그룹에서 제거]], + ["Feature.Chat.Action.RemoveUser"] = [[사용자 제거]], + ["Feature.Chat.Message.RemoveUser"] = [[이 채팅 그룹에서 {USERNAME}님을 정말로 제거할까요?]], + ["Feature.Chat.Message.RemovedFromConversation"] = [[회원님은 그룹에서 제거되었어요.]], + ["Feature.Chat.Action.ReportUser"] = [[사용자 신고]], + ["Feature.Chat.Label.SearchForFriendsAndChat"] = [[친구와 채팅 그룹을 찾아보세요]], + ["Feature.Chat.Action.SeeMoreFriends"] = [[더 보기 ({NUMBER_OF_FRIENDS})]], + ["Feature.Chat.Label.Sent"] = [[보냄]], + ["Feature.Chat.Heading.ShareGameToChat"] = [[게임을 공유해 채팅하세요]], + ["Feature.Chat.Message.TurnOnChat"] = [[친구와 이야기를 나누려면 개인정보 설정에서 채팅을 켜세요.]], + ["Feature.Chat.Action.ViewAssetDetails"] = [[자세히 보기]], + ["Feature.Chat.Label.ByBuilder"] = [[제작: {USERNAME}]], + ["Feature.Chat.Message.AlreadyPinnedGame"] = [[이 게임은 이미 고정되어 있어요. 다른 게임을 시도해 보세요.]], + ["Feature.Chat.Message.PinFailed"] = [[게임을 고정할 수 없어요. 나중에 다시 시도하세요.]], + ["Feature.Chat.Message.UnpinFailed"] = [[게임을 고정 해제할 수 없어요. 나중에 다시 시도하세요.]], + ["Feature.Chat.Drawer.ShowMore"] = [[자세히 표시]], + ["Feature.Chat.Drawer.ShowLess"] = [[간단히 표시]], + ["Feature.Chat.Drawer.NoGames"] = [[고정되거나 진행 중인 게임이 없습니다.]], + ["Feature.Chat.Drawer.Recommended"] = [[추천]], + ["Feature.Chat.Drawer.Loading"] = [[로드 중]], + ["Feature.Chat.Drawer.PinnedGame"] = [[고정한 게임]], + ["Feature.Chat.Drawer.Play"] = [[플레이]], + ["Feature.Chat.Drawer.Join"] = [[참가]], + ["Feature.Chat.Drawer.PlayGame"] = [[게임 플레이]], + ["Feature.Chat.Drawer.PinGame"] = [[게임 고정]], + ["Feature.Chat.Drawer.UnpinGame"] = [[고정 해제]], + ["Feature.Chat.Drawer.ViewDetails"] = [[자세히 보기]], + ["Feature.Chat.Label.NoDescriptionYet"] = [[설명 없어요.]], + ["Feature.Chat.Message.GameLinkWasModerated"] = [[게임 링크가 검토 요청을 받아 전송되지 않았어요.]], + ["Feature.Chat.ShareGameToChat.BrowseGames"] = [[게임 살펴보기]], + ["Feature.Chat.ShareGameToChat.By"] = [[기준: {creatorName}]], + ["Feature.Chat.ShareGameToChat.Popular"] = [[인기]], + ["Feature.Chat.ShareGameToChat.Recent"] = [[최근]], + ["Feature.Chat.ShareGameToChat.Favorites"] = [[즐겨찾기]], + ["Feature.Chat.ShareGameToChat.FriendActivity"] = [[친구의 활동]], + ["Feature.Chat.ShareGameToChat.NoPopularGames"] = [[사용 가능한 인기 게임이 없어요]], + ["Feature.Chat.ShareGameToChat.NoRecentGames"] = [[최근 게임이 없어요]], + ["Feature.Chat.ShareGameToChat.NoFavoriteGames"] = [[즐겨찾기한 게임이 없어요]], + ["Feature.Chat.ShareGameToChat.NoFriendActivity"] = [[친구의 활동이 없어요]], + ["Feature.Chat.ShareGameToChat.GameNotAvailable"] = [[이용 불가]], + ["Feature.Chat.ShareGameToChat.FailedToShareTheGame"] = [[게임을 공유할 수 없어요. 나중에 다시 시도하세요.]], + ["Feature.Chat.Heading.ChatWithFriends"] = [[친구와 채팅하기]], + ["Feature.Chat.Action.StartChatWithFriends"] = [[채팅]], + ["Feature.Friends.Label.SearchFriends"] = [[친구 찾아보기]], + ["Feature.GameBadges.HeadingGameBadges"] = [[게임 배지]], + ["Feature.GameDetails.Label.By"] = [[제작:]], + ["Feature.GameDetails.Label.About"] = [[소개]], + ["Feature.GameDetails.Label.Store"] = [[상점]], + ["Feature.GameDetails.Label.Leaderboards"] = [[순위표]], + ["Feature.GameDetails.Label.Servers"] = [[서버]], + ["Feature.GameDetails.Heading.Description"] = [[설명]], + ["Feature.GameDetails.Label.Playing"] = [[플레이 중]], + ["Feature.GameDetails.Label.Visits"] = [[방문]], + ["Feature.GameDetails.Label.Created"] = [[생성]], + ["Feature.GameDetails.Label.Updated"] = [[업데이트]], + ["Feature.GameDetails.Label.MaxPlayers"] = [[최대 인원]], + ["Feature.GameDetails.Label.Genre"] = [[장르]], + ["Feature.GameDetails.Label.GameCopyLocked"] = [[이 게임은 보호되어 있어요]], + ["Feature.GameDetails.Label.ReportAbuse"] = [[신고하기]], + ["Feature.GameDetails.Heading.RecommendedGames"] = [[추천 게임]], + ["Feature.GameGear.Heading.GearForThisGame"] = [[사용 가능한 기어]], + ["Feature.GameLeaderboard.Label.Clans"] = [[클랜]], + ["Feature.GameLeaderboard.Heading.Clans"] = [[클랜]], + ["Feature.GameLeaderboard.Heading.Players"] = [[플레이어]], + ["Feature.GameLeaderboard.Label.NoResults"] = [[결과 없음]], + ["Feature.GamePage.LabelPlayingPhrase"] = [[{playerCount}명 플레이 중]], + ["Feature.GamePage.ActionSeeAll"] = [[모두 보기]], + ["Feature.GamePage.LabelNoSearchResults"] = [[검색 결과 없음]], + ["Feature.GamePage.LabelCancelField"] = [[취소]], + ["Feature.GamePage.LabelShowingResultsFor"] = [[검색 결과 - 검색어:]], + ["Feature.GamePage.LabelSearchInsteadFor"] = [[다른 검색 결과 - 검색어:]], + ["Feature.GamePage.LabelSearchYouMightMean"] = [[이것을 찾으셨나요?]], + ["Feature.GamePage.Label.Sponsored"] = [[스폰서]], + ["Feature.GamePage.Message.EndOfList"] = [[에구구! 이게 끝이에요!]], + ["Feature.GamePage.Action.BackToTop"] = [[맨 위로 돌아가기]], + ["Feature.GamePass.Heading.PassesForThisGame"] = [[사용 가능한 패스]], + ["Feature.Home.HeadingFriends"] = [[친구 ({friendCount}명)]], + ["Feature.Home.ActionSeeAll"] = [[모두 보기]], + ["Feature.Home.Action.ViewMyFeed"] = [[내 피드 보기]], + ["Feature.PlayerSearchResults.Heading.PlayerResultsFor"] = [[다음 플레이어 검색 결과: {startSpan}{keyword}{endSpan}]], + ["Feature.PlayerSearchResults.Label.NoMatchesAvailable"] = [["{keyword}"와(과) 일치되는 항목이 없어요]], + ["Feature.PlayerSearchResults.Label.EnterMinCharacters"] = [[{keywordMinLength}자 이상 입력하세요.]], + ["Feature.PlayerSearchResults.Label.UnsafeInput"] = [[입력한 내용이 불확실함. 다시 검색해보세요.]], + ["Feature.PlayerSearchResults.Label.ShowingCountOfResults"] = [[{countStartSpan}{resultsStart} - {resultsInPage} / {countEndSpan}{totalStartSpan}{totalResults}{totalEndSpan}]], + ["Feature.PlayerSearchResults.Label.AlsoKnownAsAbbreviation"] = [[일명.]], + ["Feature.PlayerSearchResults.Label.ThisIsYou"] = [[이건 회원님이에요]], + ["Feature.PlayerSearchResults.Label.YouAreFriends"] = [[여러분은 친구예요]], + ["Feature.PlayerSearchResults.Label.YouAreFollowing"] = [[팔로우 중이에요]], + ["Feature.PlayerSearchResults.Action.AddFriend"] = [[친구 추가]], + ["Feature.PlayerSearchResults.Action.AcceptRequest"] = [[요청 수락]], + ["Feature.PlayerSearchResults.Action.RequestSent"] = [[요청 보냄]], + ["Feature.PlayerSearchResults.Action.Chat"] = [[채팅]], + ["Feature.PlayerSearchResults.Action.JoinGame"] = [[게임 참가]], + ["Feature.PrivateServers.Heading.VipServers"] = [[VIP 서버]], + ["Feature.Profile.Label.About"] = [[소개]], + ["Feature.ServerList.Heading.OtherServers"] = [[다른 서버]], + ["Feature.ServerList.Heading.ServersMyFriendsAreIn"] = [[내 친구가 있는 서버]], + ["Game.Launch.IDS_ROBLOX_INSTALLED"] = [[ROBLOX 설치에 성공했어요!]], + ["Game.Launch.IDS_ROBLOX_TOPLAY"] = [[원하는 게임의 '플레이' 버튼을 클릭하여 시작하세요!]], + ["Game.Launch.IDS_STUDIO_SUCCESS1"] = [[ROBLOX STUDIO 설치에 성공했어요!]], + ["Game.Launch.IDS_STUDIO_SUCCESS2"] = [[새 게임을 만들려면 "Studio 시작"을 클릭하세요!]], + ["Game.Launch.IDS_ROBLOX_DOWNLOAD"] = [[Roblox 다운로드 및 설치]], + ["Game.Launch.IDS_CHECKING_FILE"] = [[파일 확인 중...]], + ["Game.Launch.IDS_FILE_CHECKED"] = [[파일 확인 중...]], + ["Game.Launch.IDS_ROBLOX_STARTING"] = [[%s 시작 중...]], + ["Game.Launch.IDS_ROBLOX_UPTODATE"] = [[%s이(가) 최신 상태네요]], + ["Game.Launch.IDS_ROBLOX_UPGRADING"] = [[%s 업그레이드 중...]], + ["Game.Launch.IDS_ROBLOX_INSTALLING"] = [[%s 설치 중...]], + ["Game.Launch.IDS_ROBLOX_CONNECTING"] = [[%s에 연결 중...]], + ["Game.Launch.IDS_BOOTS_ASK_DOWNLOAD"] = [[최신 bootstrapper를 다운로드할까요?]], + ["Game.Launch.IDS_BOOTS_GET_LATEST"] = [[최신 %s을(를) 가져오는 중...]], + ["Game.Launch.IDS_PLEASE_WAIT"] = [[잠시 기다려주세요...]], + ["Game.Launch.IDS_BOOTS_SHUTDOWN"] = [[%s 종료 중]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLING"] = [[%s 삭제 중...]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLED"] = [[%s이(가) 삭제되었어요]], + ["Game.Launch.IDS_BOOTS_CONFIGURING"] = [[%s 구성 중...]], + ["Search.GlobalSearch.Example.SearchGames"] = [[게임 검색]], +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/pt-br.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/pt-br.lua new file mode 100644 index 0000000..52e8666 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/pt-br.lua @@ -0,0 +1,194 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Application.Logout.Action.Logout"] = [[Desconectar-se]], + ["Common.Presence.Label.Online"] = [[Online]], + ["Common.Presence.Label.Offline"] = [[Offline]], + ["Common.VisitGame.Label.Play"] = [[Jogar]], + ["CommonUI.Features.Label.Avatar"] = [[Avatar]], + ["CommonUI.Features.Label.Blog"] = [[Blog]], + ["CommonUI.Features.Label.BuildersClub"] = [[Clube dos construtores]], + ["CommonUI.Features.Label.Catalog"] = [[Catálogo]], + ["CommonUI.Features.Label.Chat"] = [[Chat]], + ["CommonUI.Features.Label.Events"] = [[Eventos]], + ["CommonUI.Features.Label.Friends"] = [[Amigos]], + ["CommonUI.Features.Label.Game"] = [[Jogos]], + ["CommonUI.Features.Label.Groups"] = [[Grupos]], + ["CommonUI.Features.Label.Help"] = [[Ajuda]], + ["CommonUI.Features.Label.Home"] = [[Início]], + ["CommonUI.Features.Label.Inventory"] = [[Inventário]], + ["CommonUI.Features.Label.Messages"] = [[Mensagens]], + ["CommonUI.Features.Label.More"] = [[Mais]], + ["CommonUI.Features.Label.Profile"] = [[Perfil]], + ["CommonUI.Features.Label.Settings"] = [[Configurações]], + ["CommonUI.Features.Label.About"] = [[Sobre]], + ["CommonUI.Features.Label.Players"] = [[Jogadores]], + ["CommonUI.Features.Label.Create"] = [[Criar]], + ["CommonUI.Features.Label.CreateGames"] = [[Criar jogos]], + ["Feature.Chat.Response.ChatNameFullyModerated"] = [[Nome do grupo de chat foi moderado.]], + ["Feature.Chat.Heading.LeaveGroup"] = [[Sair do grupo]], + ["Feature.Chat.Label.SearchWord"] = [[Pesquisar]], + ["Feature.Chat.Action.Add"] = [[Adicionar]], + ["Feature.Chat.Action.Leave"] = [[Sair]], + ["Feature.Chat.Action.Remove"] = [[Remover]], + ["Feature.Chat.Action.Cancel"] = [[Cancelar]], + ["Feature.Chat.Description.NameGroupChat"] = [[Dê um nome para seu grupo de chat]], + ["Feature.Chat.Heading.NewChatGroup"] = [[Novo grupo de chat]], + ["Feature.Chat.Label.SeeLess"] = [[Ver menos]], + ["Feature.Chat.Label.InputPlaceHolder.SearchForFriends"] = [[Pesquisar amigos]], + ["Feature.Chat.Label.AddFriends"] = [[Adicionar amigos]], + ["Feature.Chat.Label.ChatDetails"] = [[Detalhes do chat]], + ["Feature.Chat.Label.General"] = [[Geral]], + ["Feature.Chat.Label.Members"] = [[Membros]], + ["Feature.Chat.Label.ViewProfile"] = [[Ver perfil]], + ["Feature.Chat.Label.ChatGroupName"] = [[Nome do grupo de chat]], + ["Feature.Chat.Action.Stay"] = [[Ficar]], + ["Feature.Chat.Action.Create"] = [[Criar]], + ["Feature.Chat.Action.Send"] = [[Enviar]], + ["Feature.Chat.Action.Save"] = [[Salvar]], + ["Feature.Chat.Message.ToastText"] = [[Você pode ter até {friendNum} amigos no grupo de chat.]], + ["Feature.Chat.Message.NoConnectionMsg"] = [[Conectando...]], + ["Feature.Chat.Message.Default"] = [[Nem todo mundo neste chat pode ver sua mensagem.]], + ["Feature.Chat.Message.MessageFilterForReceivers"] = [[Nem todo mundo neste chat pode ver sua mensagem.]], + ["Feature.Chat.Message.MessageContentModerated"] = [[Sua mensagem foi moderada e não foi enviada.]], + ["Feature.Chat.Label.PrivacySettings"] = [[Configurações de privacidade]], + ["Feature.Chat.Label.ChatInputPlaceholder"] = [[Diga algo]], + ["Feature.Chat.Action.Confirm"] = [[Ok]], + ["Feature.Chat.Heading.FailedToLeaveGroup"] = [[Falha ao sair do grupo]], + ["Feature.Chat.Message.FailedToLeaveGroup"] = [[Você não conseguiu sair da conversa {CONVERSATION_TITLE}.]], + ["Feature.Chat.Heading.FailedToRemoveUser"] = [[Falha ao remover usuário]], + ["Feature.Chat.Message.FailedToRemoveUser"] = [[O usuário {USERNAME} não pôde ser removido da conversa {CONVERSATION_TITLE}.]], + ["Feature.Chat.Heading.FailedToRenameConversation"] = [[Falha ao renomear conversa]], + ["Feature.Chat.Message.FailedToRenameConversation"] = [[A conversa {EXISTING_NAME} não pôde ser renomeada para {NEW_NAME}.]], + ["Feature.Chat.Message.LeaveGroup"] = [[Você não poderá continuar a falar no chat com este grupo.]], + ["Feature.Chat.Message.MakeFriendsToChat"] = [[Faça amigos nos jogos para conversar no chat e jogar juntos.]], + ["Feature.Chat.Label.NotSet"] = [[Não configurado]], + ["Feature.Chat.Heading.Option"] = [[Opção]], + ["Feature.Chat.Action.RemoveFromGroup"] = [[Remover do grupo]], + ["Feature.Chat.Action.RemoveUser"] = [[Remover usuário]], + ["Feature.Chat.Message.RemoveUser"] = [[Quer mesmo remover {USERNAME} do grupo de chat?]], + ["Feature.Chat.Message.RemovedFromConversation"] = [[Você foi removido do grupo de chat.]], + ["Feature.Chat.Action.ReportUser"] = [[Denunciar usuário]], + ["Feature.Chat.Label.SearchForFriendsAndChat"] = [[Pesquise por amigos e grupos de chat]], + ["Feature.Chat.Action.SeeMoreFriends"] = [[Ver mais ({NUMBER_OF_FRIENDS})]], + ["Feature.Chat.Label.Sent"] = [[Enviado]], + ["Feature.Chat.Heading.ShareGameToChat"] = [[Compartilhe o jogo para falar no chat]], + ["Feature.Chat.Message.TurnOnChat"] = [[Para conversar no chat com amigos, ative o chat em suas Configurações de Privacidade.]], + ["Feature.Chat.Action.ViewAssetDetails"] = [[Ver detalhes]], + ["Feature.Chat.Label.ByBuilder"] = [[De {USERNAME}]], + ["Feature.Chat.Message.AlreadyPinnedGame"] = [[Este jogo já foi marcado. Tente outro.]], + ["Feature.Chat.Message.PinFailed"] = [[O jogo não pôde ser marcado. Tente de novo mais tarde.]], + ["Feature.Chat.Message.UnpinFailed"] = [[O jogo não pôde ser desmarcado. Tente de novo mais tarde.]], + ["Feature.Chat.Drawer.ShowMore"] = [[Mostrar mais]], + ["Feature.Chat.Drawer.ShowLess"] = [[Mostrar menos]], + ["Feature.Chat.Drawer.NoGames"] = [[Nenhum jogo marcado ou em andamento.]], + ["Feature.Chat.Drawer.Recommended"] = [[Recomendado]], + ["Feature.Chat.Drawer.Loading"] = [[Carregando]], + ["Feature.Chat.Drawer.PinnedGame"] = [[Jogo marcado]], + ["Feature.Chat.Drawer.Play"] = [[Jogar]], + ["Feature.Chat.Drawer.Join"] = [[Entrar]], + ["Feature.Chat.Drawer.PlayGame"] = [[Jogar]], + ["Feature.Chat.Drawer.PinGame"] = [[Marcar jogo]], + ["Feature.Chat.Drawer.UnpinGame"] = [[Desmarcar jogo]], + ["Feature.Chat.Drawer.ViewDetails"] = [[Ver detalhes]], + ["Feature.Chat.Label.NoDescriptionYet"] = [[Ainda sem descrição.]], + ["Feature.Chat.Message.GameLinkWasModerated"] = [[Esse link foi moderado e não foi enviado.]], + ["Feature.Chat.ShareGameToChat.BrowseGames"] = [[Consultar jogos]], + ["Feature.Chat.ShareGameToChat.By"] = [[de {creatorName}]], + ["Feature.Chat.ShareGameToChat.Popular"] = [[Populares]], + ["Feature.Chat.ShareGameToChat.Recent"] = [[Recentes]], + ["Feature.Chat.ShareGameToChat.Favorites"] = [[Favoritos]], + ["Feature.Chat.ShareGameToChat.FriendActivity"] = [[Atividade de amigo]], + ["Feature.Chat.ShareGameToChat.NoPopularGames"] = [[Nenhum jogo popular disponível]], + ["Feature.Chat.ShareGameToChat.NoRecentGames"] = [[Você não possui jogos recentes]], + ["Feature.Chat.ShareGameToChat.NoFavoriteGames"] = [[Você não possui jogos favoritos]], + ["Feature.Chat.ShareGameToChat.NoFriendActivity"] = [[Você não possui atividades de amigo]], + ["Feature.Chat.ShareGameToChat.GameNotAvailable"] = [[Não disponível]], + ["Feature.Chat.ShareGameToChat.FailedToShareTheGame"] = [[Impossível compartilhar jogo. Tente de novo mais tarde.]], + ["Feature.Chat.Heading.ChatWithFriends"] = [[Fale no chat com amigos]], + ["Feature.Chat.Action.StartChatWithFriends"] = [[Chat]], + ["Feature.Friends.Label.SearchFriends"] = [[Pesquisar amigos]], + ["Feature.GameBadges.HeadingGameBadges"] = [[Emblemas do jogo]], + ["Feature.GameDetails.Label.By"] = [[De ]], + ["Feature.GameDetails.Label.About"] = [[Sobre]], + ["Feature.GameDetails.Label.Store"] = [[Loja]], + ["Feature.GameDetails.Label.Leaderboards"] = [[Classificação]], + ["Feature.GameDetails.Label.Servers"] = [[Servidores]], + ["Feature.GameDetails.Heading.Description"] = [[Descrição]], + ["Feature.GameDetails.Label.Playing"] = [[Jogando]], + ["Feature.GameDetails.Label.Visits"] = [[Visitas]], + ["Feature.GameDetails.Label.Created"] = [[Criado]], + ["Feature.GameDetails.Label.Updated"] = [[Atualizado]], + ["Feature.GameDetails.Label.MaxPlayers"] = [[Máximo de jogadores]], + ["Feature.GameDetails.Label.Genre"] = [[Gênero]], + ["Feature.GameDetails.Label.GameCopyLocked"] = [[Este jogo possui bloqueio de cópia.]], + ["Feature.GameDetails.Label.ReportAbuse"] = [[Denunciar abuso]], + ["Feature.GameDetails.Heading.RecommendedGames"] = [[Jogos recomendados]], + ["Feature.GameGear.Heading.GearForThisGame"] = [[Equipamento para este jogo]], + ["Feature.GameLeaderboard.Label.Clans"] = [[Clãs]], + ["Feature.GameLeaderboard.Heading.Clans"] = [[Clãs]], + ["Feature.GameLeaderboard.Heading.Players"] = [[Jogadores]], + ["Feature.GameLeaderboard.Label.NoResults"] = [[Nenhum resultado encontrado]], + ["Feature.GamePage.LabelPlayingPhrase"] = [[{playerCount} jogando]], + ["Feature.GamePage.ActionSeeAll"] = [[Ver todos]], + ["Feature.GamePage.LabelNoSearchResults"] = [[Nenhum resultado de pesquisa encontrado]], + ["Feature.GamePage.LabelCancelField"] = [[Cancelar]], + ["Feature.GamePage.LabelShowingResultsFor"] = [[Exibindo resultados para]], + ["Feature.GamePage.LabelSearchInsteadFor"] = [[Em vez disso, pesquisar por]], + ["Feature.GamePage.LabelSearchYouMightMean"] = [[Você quis dizer:]], + ["Feature.GamePage.Label.Sponsored"] = [[Patrocinado]], + ["Feature.GamePage.Message.EndOfList"] = [[UFA! Você chegou no final!]], + ["Feature.GamePage.Action.BackToTop"] = [[Voltar para o topo]], + ["Feature.GamePass.Heading.PassesForThisGame"] = [[Passes para este jogo]], + ["Feature.Home.HeadingFriends"] = [[Amigos ({friendCount})]], + ["Feature.Home.ActionSeeAll"] = [[Ver todos]], + ["Feature.Home.Action.ViewMyFeed"] = [[Ver meu feed]], + ["Feature.PlayerSearchResults.Heading.PlayerResultsFor"] = [[Resultados de jogador para {startSpan}{keyword}{endSpan}]], + ["Feature.PlayerSearchResults.Label.NoMatchesAvailable"] = [[Nenhuma partida disponível para "{keyword}"]], + ["Feature.PlayerSearchResults.Label.EnterMinCharacters"] = [[Insira pelo menos {keywordMinLength} caracteres.]], + ["Feature.PlayerSearchResults.Label.UnsafeInput"] = [[Você inseriu dados não seguros. Tente pesquisar de novo.]], + ["Feature.PlayerSearchResults.Label.ShowingCountOfResults"] = [[{countStartSpan}{resultsStart} - {resultsInPage} de {countEndSpan}{totalStartSpan}{totalResults}{totalEndSpan}]], + ["Feature.PlayerSearchResults.Label.AlsoKnownAsAbbreviation"] = [[também conhecido como]], + ["Feature.PlayerSearchResults.Label.ThisIsYou"] = [[Este é você]], + ["Feature.PlayerSearchResults.Label.YouAreFriends"] = [[Vocês são amigos]], + ["Feature.PlayerSearchResults.Label.YouAreFollowing"] = [[Você está seguindo]], + ["Feature.PlayerSearchResults.Action.AddFriend"] = [[Adicionar amigo]], + ["Feature.PlayerSearchResults.Action.AcceptRequest"] = [[Aceitar pedido]], + ["Feature.PlayerSearchResults.Action.RequestSent"] = [[Pedido enviado]], + ["Feature.PlayerSearchResults.Action.Chat"] = [[Chat]], + ["Feature.PlayerSearchResults.Action.JoinGame"] = [[Entrar no jogo]], + ["Feature.PrivateServers.Heading.VipServers"] = [[Servidores VIP]], + ["Feature.Profile.Label.About"] = [[Sobre]], + ["Feature.ServerList.Heading.OtherServers"] = [[Outros servidores]], + ["Feature.ServerList.Heading.ServersMyFriendsAreIn"] = [[Servidores onde meus amigos estão]], + ["Game.Launch.IDS_ROBLOX_INSTALLED"] = [[ROBLOX FOI INSTALADO COM SUCESSO!]], + ["Game.Launch.IDS_ROBLOX_TOPLAY"] = [[Clique no botão Jogar em qualquer jogo para entrar na ação!]], + ["Game.Launch.IDS_STUDIO_SUCCESS1"] = [[ROBLOX STUDIO FOI INSTALADO COM SUCESSO!]], + ["Game.Launch.IDS_STUDIO_SUCCESS2"] = [[Clique em Iniciar Studio para criar seu novo jogo!]], + ["Game.Launch.IDS_ROBLOX_DOWNLOAD"] = [[Baixe e instale Roblox]], + ["Game.Launch.IDS_CHECKING_FILE"] = [[Verificando o arquivo...]], + ["Game.Launch.IDS_FILE_CHECKED"] = [[Verificação concluída]], + ["Game.Launch.IDS_ROBLOX_STARTING"] = [[Iniciando %s...]], + ["Game.Launch.IDS_ROBLOX_UPTODATE"] = [[%s está atualizado]], + ["Game.Launch.IDS_ROBLOX_UPGRADING"] = [[Fazendo upgrade %s...]], + ["Game.Launch.IDS_ROBLOX_INSTALLING"] = [[Instalando %s...]], + ["Game.Launch.IDS_ROBLOX_CONNECTING"] = [[Conectando-se ao %s...]], + ["Game.Launch.IDS_BOOTS_ASK_DOWNLOAD"] = [[Baixar a versão mais recente do bootstrapper?]], + ["Game.Launch.IDS_BOOTS_GET_LATEST"] = [[Obtendo a versão mais recente do %s...]], + ["Game.Launch.IDS_PLEASE_WAIT"] = [[Aguarde...]], + ["Game.Launch.IDS_BOOTS_SHUTDOWN"] = [[Desligando %s]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLING"] = [[Desinstalando %s...]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLED"] = [[%s foi desinstalado]], + ["Game.Launch.IDS_BOOTS_CONFIGURING"] = [[Configurando %s...]], + ["Search.GlobalSearch.Example.SearchGames"] = [[Pesquisar jogos]], +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/ru-ru.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/ru-ru.lua new file mode 100644 index 0000000..172c017 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/ru-ru.lua @@ -0,0 +1,34 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["CommonUI.Features.Label.Avatar"] = [[Аватар]], + ["CommonUI.Features.Label.Blog"] = [[Блог]], + ["CommonUI.Features.Label.BuildersClub"] = [[Клуб создателей]], + ["CommonUI.Features.Label.Catalog"] = [[Каталог]], + ["CommonUI.Features.Label.Chat"] = [[Чат]], + ["CommonUI.Features.Label.Events"] = [[Акции]], + ["CommonUI.Features.Label.Friends"] = [[Друзья]], + ["CommonUI.Features.Label.Game"] = [[Игры]], + ["CommonUI.Features.Label.Groups"] = [[Группы]], + ["CommonUI.Features.Label.Help"] = [[Справка]], + ["CommonUI.Features.Label.Home"] = [[Главная]], + ["CommonUI.Features.Label.Inventory"] = [[Инвентарь]], + ["CommonUI.Features.Label.Messages"] = [[Сообщения]], + ["CommonUI.Features.Label.More"] = [[Больше]], + ["CommonUI.Features.Label.Profile"] = [[Профиль]], + ["CommonUI.Features.Label.Settings"] = [[Настройки]], + ["CommonUI.Features.Label.About"] = [[Сведения]], + ["CommonUI.Features.Label.Players"] = [[Игроки]], + ["CommonUI.Features.Label.Create"] = [[Создать]], + ["CommonUI.Features.Label.CreateGames"] = [[Создать игры]], +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/th-th.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/th-th.lua new file mode 100644 index 0000000..7c9c680 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/th-th.lua @@ -0,0 +1,34 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["CommonUI.Features.Label.Avatar"] = [[อวตาร]], + ["CommonUI.Features.Label.Blog"] = [[บล็อก]], + ["CommonUI.Features.Label.BuildersClub"] = [[ชมรมนักสร้าง]], + ["CommonUI.Features.Label.Catalog"] = [[แค็ตตาล็อก]], + ["CommonUI.Features.Label.Chat"] = [[แชต]], + ["CommonUI.Features.Label.Events"] = [[อีเวนต์]], + ["CommonUI.Features.Label.Friends"] = [[เพื่อน]], + ["CommonUI.Features.Label.Game"] = [[เกม]], + ["CommonUI.Features.Label.Groups"] = [[กลุ่ม]], + ["CommonUI.Features.Label.Help"] = [[ช่วยเหลือ]], + ["CommonUI.Features.Label.Home"] = [[หน้าหลัก]], + ["CommonUI.Features.Label.Inventory"] = [[คลังไอเทม]], + ["CommonUI.Features.Label.Messages"] = [[ข้อความ]], + ["CommonUI.Features.Label.More"] = [[เพิ่มเติม]], + ["CommonUI.Features.Label.Profile"] = [[ประวัติ]], + ["CommonUI.Features.Label.Settings"] = [[การตั้งค่า]], + ["CommonUI.Features.Label.About"] = [[เกี่ยวกับ]], + ["CommonUI.Features.Label.Players"] = [[ผู้เล่น]], + ["CommonUI.Features.Label.Create"] = [[สร้าง]], + ["CommonUI.Features.Label.CreateGames"] = [[สร้างเกม]], +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/tr-tr.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/tr-tr.lua new file mode 100644 index 0000000..271c40d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/tr-tr.lua @@ -0,0 +1,34 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["CommonUI.Features.Label.Avatar"] = [[Avatar]], + ["CommonUI.Features.Label.Blog"] = [[Blog]], + ["CommonUI.Features.Label.BuildersClub"] = [[Yapım Kulübü]], + ["CommonUI.Features.Label.Catalog"] = [[Katalog]], + ["CommonUI.Features.Label.Chat"] = [[Sohbet]], + ["CommonUI.Features.Label.Events"] = [[Etkinlikler]], + ["CommonUI.Features.Label.Friends"] = [[Arkadaşlar]], + ["CommonUI.Features.Label.Game"] = [[Oyunlar]], + ["CommonUI.Features.Label.Groups"] = [[Gruplar]], + ["CommonUI.Features.Label.Help"] = [[Yardım]], + ["CommonUI.Features.Label.Home"] = [[Ana Sayfa]], + ["CommonUI.Features.Label.Inventory"] = [[Envanter]], + ["CommonUI.Features.Label.Messages"] = [[Mesajlar]], + ["CommonUI.Features.Label.More"] = [[Daha Fazla]], + ["CommonUI.Features.Label.Profile"] = [[Profil]], + ["CommonUI.Features.Label.Settings"] = [[Ayarlar]], + ["CommonUI.Features.Label.About"] = [[Hakkında]], + ["CommonUI.Features.Label.Players"] = [[Oyuncular]], + ["CommonUI.Features.Label.Create"] = [[Oluştur]], + ["CommonUI.Features.Label.CreateGames"] = [[Oyun Oluştur]], +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/vi-vn.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/vi-vn.lua new file mode 100644 index 0000000..5b93cf4 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/vi-vn.lua @@ -0,0 +1,34 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["CommonUI.Features.Label.Avatar"] = [[Hình đại diện]], + ["CommonUI.Features.Label.Blog"] = [[Blog]], + ["CommonUI.Features.Label.BuildersClub"] = [[Câu lạc bộ Thợ xây]], + ["CommonUI.Features.Label.Catalog"] = [[Danh mục]], + ["CommonUI.Features.Label.Chat"] = [[Trò chuyện]], + ["CommonUI.Features.Label.Events"] = [[Sự kiện]], + ["CommonUI.Features.Label.Friends"] = [[Bạn bè]], + ["CommonUI.Features.Label.Game"] = [[Trò chơi]], + ["CommonUI.Features.Label.Groups"] = [[Nhóm]], + ["CommonUI.Features.Label.Help"] = [[Trợ giúp]], + ["CommonUI.Features.Label.Home"] = [[Trang chủ]], + ["CommonUI.Features.Label.Inventory"] = [[Kho]], + ["CommonUI.Features.Label.Messages"] = [[Tin nhắn]], + ["CommonUI.Features.Label.More"] = [[Thêm]], + ["CommonUI.Features.Label.Profile"] = [[Hồ sơ]], + ["CommonUI.Features.Label.Settings"] = [[Cài đặt]], + ["CommonUI.Features.Label.About"] = [[Về sản phẩm]], + ["CommonUI.Features.Label.Players"] = [[Người chơi]], + ["CommonUI.Features.Label.Create"] = [[Tạo]], + ["CommonUI.Features.Label.CreateGames"] = [[Tạo trò chơi]], +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/zh-cn.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/zh-cn.lua new file mode 100644 index 0000000..0079d2d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/zh-cn.lua @@ -0,0 +1,194 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Application.Logout.Action.Logout"] = [[注销]], + ["Common.Presence.Label.Online"] = [[在线]], + ["Common.Presence.Label.Offline"] = [[离线]], + ["Common.VisitGame.Label.Play"] = [[开始游戏]], + ["CommonUI.Features.Label.Avatar"] = [[虚拟形象]], + ["CommonUI.Features.Label.Blog"] = [[博客]], + ["CommonUI.Features.Label.BuildersClub"] = [[建造者社团]], + ["CommonUI.Features.Label.Catalog"] = [[目录]], + ["CommonUI.Features.Label.Chat"] = [[聊天]], + ["CommonUI.Features.Label.Events"] = [[活动]], + ["CommonUI.Features.Label.Friends"] = [[好友]], + ["CommonUI.Features.Label.Game"] = [[游戏]], + ["CommonUI.Features.Label.Groups"] = [[群组]], + ["CommonUI.Features.Label.Help"] = [[帮助]], + ["CommonUI.Features.Label.Home"] = [[首页]], + ["CommonUI.Features.Label.Inventory"] = [[道具]], + ["CommonUI.Features.Label.Messages"] = [[消息]], + ["CommonUI.Features.Label.More"] = [[更多]], + ["CommonUI.Features.Label.Profile"] = [[个人资料]], + ["CommonUI.Features.Label.Settings"] = [[设置]], + ["CommonUI.Features.Label.About"] = [[关于]], + ["CommonUI.Features.Label.Players"] = [[玩家]], + ["CommonUI.Features.Label.Create"] = [[创建]], + ["CommonUI.Features.Label.CreateGames"] = [[创建游戏]], + ["Feature.Chat.Response.ChatNameFullyModerated"] = [[群聊名称已被过滤。]], + ["Feature.Chat.Heading.LeaveGroup"] = [[离开群]], + ["Feature.Chat.Label.SearchWord"] = [[搜索]], + ["Feature.Chat.Action.Add"] = [[添加]], + ["Feature.Chat.Action.Leave"] = [[离开]], + ["Feature.Chat.Action.Remove"] = [[移除]], + ["Feature.Chat.Action.Cancel"] = [[取消]], + ["Feature.Chat.Description.NameGroupChat"] = [[命名你的群聊]], + ["Feature.Chat.Heading.NewChatGroup"] = [[新群聊]], + ["Feature.Chat.Label.SeeLess"] = [[收起]], + ["Feature.Chat.Label.InputPlaceHolder.SearchForFriends"] = [[搜索好友]], + ["Feature.Chat.Label.AddFriends"] = [[添加好友]], + ["Feature.Chat.Label.ChatDetails"] = [[聊天详情]], + ["Feature.Chat.Label.General"] = [[通用]], + ["Feature.Chat.Label.Members"] = [[成员]], + ["Feature.Chat.Label.ViewProfile"] = [[查看个人资料]], + ["Feature.Chat.Label.ChatGroupName"] = [[群聊名称]], + ["Feature.Chat.Action.Stay"] = [[留下]], + ["Feature.Chat.Action.Create"] = [[创建]], + ["Feature.Chat.Action.Send"] = [[发送]], + ["Feature.Chat.Action.Save"] = [[保存]], + ["Feature.Chat.Message.ToastText"] = [[你的群聊中最多可以有 {friendNum} 位好友。]], + ["Feature.Chat.Message.NoConnectionMsg"] = [[正在连接...]], + ["Feature.Chat.Message.Default"] = [[此聊天中不是所有人都能看到你的消息。]], + ["Feature.Chat.Message.MessageFilterForReceivers"] = [[此聊天中不是所有人都能看到你的消息。]], + ["Feature.Chat.Message.MessageContentModerated"] = [[你的消息已被过滤,未能发送。]], + ["Feature.Chat.Label.PrivacySettings"] = [[隐私设置]], + ["Feature.Chat.Label.ChatInputPlaceholder"] = [[说点什么]], + ["Feature.Chat.Action.Confirm"] = [[好的]], + ["Feature.Chat.Heading.FailedToLeaveGroup"] = [[离开群失败]], + ["Feature.Chat.Message.FailedToLeaveGroup"] = [[你无法从 {CONVERSATION_TITLE} 中移除。]], + ["Feature.Chat.Heading.FailedToRemoveUser"] = [[移除用户失败]], + ["Feature.Chat.Message.FailedToRemoveUser"] = [[用户 {USERNAME} 无法从对话 {CONVERSATION_TITLE} 中移除。]], + ["Feature.Chat.Heading.FailedToRenameConversation"] = [[重命名对话失败]], + ["Feature.Chat.Message.FailedToRenameConversation"] = [[对话 {EXISTING_NAME} 无法重命名为 {NEW_NAME}。]], + ["Feature.Chat.Message.LeaveGroup"] = [[你将无法与此群继续聊天。]], + ["Feature.Chat.Message.MakeFriendsToChat"] = [[在游戏中结交好友以开始聊天、玩游戏。]], + ["Feature.Chat.Label.NotSet"] = [[未设置]], + ["Feature.Chat.Heading.Option"] = [[选项]], + ["Feature.Chat.Action.RemoveFromGroup"] = [[移出群]], + ["Feature.Chat.Action.RemoveUser"] = [[移除用户]], + ["Feature.Chat.Message.RemoveUser"] = [[是否确定从此群聊中移除 {USERNAME}?]], + ["Feature.Chat.Message.RemovedFromConversation"] = [[你已被移出群。]], + ["Feature.Chat.Action.ReportUser"] = [[举报用户]], + ["Feature.Chat.Label.SearchForFriendsAndChat"] = [[搜索好友和群聊]], + ["Feature.Chat.Action.SeeMoreFriends"] = [[查看更多 ({NUMBER_OF_FRIENDS})]], + ["Feature.Chat.Label.Sent"] = [[已发送]], + ["Feature.Chat.Heading.ShareGameToChat"] = [[分享游戏至聊天]], + ["Feature.Chat.Message.TurnOnChat"] = [[要与好友聊天,请在你的隐私设置中打开聊天]], + ["Feature.Chat.Action.ViewAssetDetails"] = [[查看详情]], + ["Feature.Chat.Label.ByBuilder"] = [[自 {USERNAME}]], + ["Feature.Chat.Message.AlreadyPinnedGame"] = [[此游戏已置顶。请尝试另一个游戏。]], + ["Feature.Chat.Message.PinFailed"] = [[游戏无法置顶。请稍后重试。]], + ["Feature.Chat.Message.UnpinFailed"] = [[游戏无法取消置顶。请稍后重试。]], + ["Feature.Chat.Drawer.ShowMore"] = [[显示更多]], + ["Feature.Chat.Drawer.ShowLess"] = [[显示更少]], + ["Feature.Chat.Drawer.NoGames"] = [[无游戏置顶或进行中。]], + ["Feature.Chat.Drawer.Recommended"] = [[推荐]], + ["Feature.Chat.Drawer.Loading"] = [[正在加载]], + ["Feature.Chat.Drawer.PinnedGame"] = [[置顶游戏]], + ["Feature.Chat.Drawer.Play"] = [[开始游戏]], + ["Feature.Chat.Drawer.Join"] = [[加入]], + ["Feature.Chat.Drawer.PlayGame"] = [[开始游戏]], + ["Feature.Chat.Drawer.PinGame"] = [[置顶游戏]], + ["Feature.Chat.Drawer.UnpinGame"] = [[取消置顶游戏]], + ["Feature.Chat.Drawer.ViewDetails"] = [[查看详情]], + ["Feature.Chat.Label.NoDescriptionYet"] = [[尚无描述。]], + ["Feature.Chat.Message.GameLinkWasModerated"] = [[此游戏链接已被过滤,未能发送。]], + ["Feature.Chat.ShareGameToChat.BrowseGames"] = [[浏览游戏]], + ["Feature.Chat.ShareGameToChat.By"] = [[作者:{creatorName}]], + ["Feature.Chat.ShareGameToChat.Popular"] = [[热门]], + ["Feature.Chat.ShareGameToChat.Recent"] = [[最近]], + ["Feature.Chat.ShareGameToChat.Favorites"] = [[收藏]], + ["Feature.Chat.ShareGameToChat.FriendActivity"] = [[好友活动]], + ["Feature.Chat.ShareGameToChat.NoPopularGames"] = [[无可用热门游戏]], + ["Feature.Chat.ShareGameToChat.NoRecentGames"] = [[你没有最近游戏]], + ["Feature.Chat.ShareGameToChat.NoFavoriteGames"] = [[你没有收藏游戏]], + ["Feature.Chat.ShareGameToChat.NoFriendActivity"] = [[你没有好友活动]], + ["Feature.Chat.ShareGameToChat.GameNotAvailable"] = [[不可用]], + ["Feature.Chat.ShareGameToChat.FailedToShareTheGame"] = [[无法共享游戏。请稍后重试。]], + ["Feature.Chat.Heading.ChatWithFriends"] = [[与好友聊天]], + ["Feature.Chat.Action.StartChatWithFriends"] = [[聊天]], + ["Feature.Friends.Label.SearchFriends"] = [[搜索好友]], + ["Feature.GameBadges.HeadingGameBadges"] = [[游戏徽章]], + ["Feature.GameDetails.Label.By"] = [[作者]], + ["Feature.GameDetails.Label.About"] = [[关于]], + ["Feature.GameDetails.Label.Store"] = [[商店]], + ["Feature.GameDetails.Label.Leaderboards"] = [[排行榜]], + ["Feature.GameDetails.Label.Servers"] = [[服务器]], + ["Feature.GameDetails.Heading.Description"] = [[描述]], + ["Feature.GameDetails.Label.Playing"] = [[正在游戏]], + ["Feature.GameDetails.Label.Visits"] = [[访问量]], + ["Feature.GameDetails.Label.Created"] = [[创建时间]], + ["Feature.GameDetails.Label.Updated"] = [[更新时间]], + ["Feature.GameDetails.Label.MaxPlayers"] = [[玩家人数上限]], + ["Feature.GameDetails.Label.Genre"] = [[类别]], + ["Feature.GameDetails.Label.GameCopyLocked"] = [[此游戏受复制锁定。]], + ["Feature.GameDetails.Label.ReportAbuse"] = [[报告滥用行为]], + ["Feature.GameDetails.Heading.RecommendedGames"] = [[推荐游戏]], + ["Feature.GameGear.Heading.GearForThisGame"] = [[此游戏的装备]], + ["Feature.GameLeaderboard.Label.Clans"] = [[部落]], + ["Feature.GameLeaderboard.Heading.Clans"] = [[部落]], + ["Feature.GameLeaderboard.Heading.Players"] = [[玩家]], + ["Feature.GameLeaderboard.Label.NoResults"] = [[未找到结果]], + ["Feature.GamePage.LabelPlayingPhrase"] = [[{playerCount} 人正在玩]], + ["Feature.GamePage.ActionSeeAll"] = [[查看全部]], + ["Feature.GamePage.LabelNoSearchResults"] = [[未找到搜索结果]], + ["Feature.GamePage.LabelCancelField"] = [[取消]], + ["Feature.GamePage.LabelShowingResultsFor"] = [[显示以下结果]], + ["Feature.GamePage.LabelSearchInsteadFor"] = [[改为搜索]], + ["Feature.GamePage.LabelSearchYouMightMean"] = [[你是否指:]], + ["Feature.GamePage.Label.Sponsored"] = [[已赞助]], + ["Feature.GamePage.Message.EndOfList"] = [[OOF!你已到达列表底部!]], + ["Feature.GamePage.Action.BackToTop"] = [[返回顶部]], + ["Feature.GamePass.Heading.PassesForThisGame"] = [[此游戏的通行证]], + ["Feature.Home.HeadingFriends"] = [[好友 ({friendCount})]], + ["Feature.Home.ActionSeeAll"] = [[查看全部]], + ["Feature.Home.Action.ViewMyFeed"] = [[查看我的推送]], + ["Feature.PlayerSearchResults.Heading.PlayerResultsFor"] = [[{startSpan}{keyword}{endSpan} 的玩家结果]], + ["Feature.PlayerSearchResults.Label.NoMatchesAvailable"] = [[没有“{keyword}”的匹配项]], + ["Feature.PlayerSearchResults.Label.EnterMinCharacters"] = [[请输入至少 {keywordMinLength} 个字符。]], + ["Feature.PlayerSearchResults.Label.UnsafeInput"] = [[你输入的内容不安全。请重试搜索。]], + ["Feature.PlayerSearchResults.Label.ShowingCountOfResults"] = [[{countStartSpan}{resultsStart} - {resultsInPage}/{countEndSpan}{totalStartSpan}{totalResults}{totalEndSpan}]], + ["Feature.PlayerSearchResults.Label.AlsoKnownAsAbbreviation"] = [[又称]], + ["Feature.PlayerSearchResults.Label.ThisIsYou"] = [[这是你]], + ["Feature.PlayerSearchResults.Label.YouAreFriends"] = [[你们是好友]], + ["Feature.PlayerSearchResults.Label.YouAreFollowing"] = [[你正关注]], + ["Feature.PlayerSearchResults.Action.AddFriend"] = [[添加好友]], + ["Feature.PlayerSearchResults.Action.AcceptRequest"] = [[接受请求]], + ["Feature.PlayerSearchResults.Action.RequestSent"] = [[请求已发送]], + ["Feature.PlayerSearchResults.Action.Chat"] = [[聊天]], + ["Feature.PlayerSearchResults.Action.JoinGame"] = [[加入游戏]], + ["Feature.PrivateServers.Heading.VipServers"] = [[VIP 服务器]], + ["Feature.Profile.Label.About"] = [[关于]], + ["Feature.ServerList.Heading.OtherServers"] = [[其他服务器]], + ["Feature.ServerList.Heading.ServersMyFriendsAreIn"] = [[我的好友所在的服务器]], + ["Game.Launch.IDS_ROBLOX_INSTALLED"] = [[ROBLOX 已成功安装!]], + ["Game.Launch.IDS_ROBLOX_TOPLAY"] = [[点按“开始游戏”按钮即可加入!]], + ["Game.Launch.IDS_STUDIO_SUCCESS1"] = [[ROBLOX STUDIO 已成功安装!]], + ["Game.Launch.IDS_STUDIO_SUCCESS2"] = [[点按“启动 Studio“ 以开始制作你的新游戏!]], + ["Game.Launch.IDS_ROBLOX_DOWNLOAD"] = [[下载并安装 Roblox]], + ["Game.Launch.IDS_CHECKING_FILE"] = [[正在运行文件检查...]], + ["Game.Launch.IDS_FILE_CHECKED"] = [[文件检查完成]], + ["Game.Launch.IDS_ROBLOX_STARTING"] = [[正在开始 %s...]], + ["Game.Launch.IDS_ROBLOX_UPTODATE"] = [[%s 已是最新版本]], + ["Game.Launch.IDS_ROBLOX_UPGRADING"] = [[正在升级 %s...]], + ["Game.Launch.IDS_ROBLOX_INSTALLING"] = [[正在安装 %s...]], + ["Game.Launch.IDS_ROBLOX_CONNECTING"] = [[正在连接至 %s...]], + ["Game.Launch.IDS_BOOTS_ASK_DOWNLOAD"] = [[下载最新版引导程序?]], + ["Game.Launch.IDS_BOOTS_GET_LATEST"] = [[正在获取最新版本的 %s...]], + ["Game.Launch.IDS_PLEASE_WAIT"] = [[请稍候...]], + ["Game.Launch.IDS_BOOTS_SHUTDOWN"] = [[正在关闭 %s]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLING"] = [[正在卸载 %s...]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLED"] = [[%s 已卸载]], + ["Game.Launch.IDS_BOOTS_CONFIGURING"] = [[正在配置 %s...]], + ["Search.GlobalSearch.Example.SearchGames"] = [[搜索游戏]], +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/zh-tw.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/zh-tw.lua new file mode 100644 index 0000000..980ee09 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Locales/zh-tw.lua @@ -0,0 +1,194 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Application.Logout.Action.Logout"] = [[登出]], + ["Common.Presence.Label.Online"] = [[在線上]], + ["Common.Presence.Label.Offline"] = [[離線]], + ["Common.VisitGame.Label.Play"] = [[玩]], + ["CommonUI.Features.Label.Avatar"] = [[虛擬人偶]], + ["CommonUI.Features.Label.Blog"] = [[部落格]], + ["CommonUI.Features.Label.BuildersClub"] = [[建置者社團]], + ["CommonUI.Features.Label.Catalog"] = [[型錄]], + ["CommonUI.Features.Label.Chat"] = [[聊天]], + ["CommonUI.Features.Label.Events"] = [[活動]], + ["CommonUI.Features.Label.Friends"] = [[朋友]], + ["CommonUI.Features.Label.Game"] = [[遊戲]], + ["CommonUI.Features.Label.Groups"] = [[群組]], + ["CommonUI.Features.Label.Help"] = [[說明]], + ["CommonUI.Features.Label.Home"] = [[首頁]], + ["CommonUI.Features.Label.Inventory"] = [[物品庫]], + ["CommonUI.Features.Label.Messages"] = [[訊息]], + ["CommonUI.Features.Label.More"] = [[其他]], + ["CommonUI.Features.Label.Profile"] = [[個人檔]], + ["CommonUI.Features.Label.Settings"] = [[設定]], + ["CommonUI.Features.Label.About"] = [[關於]], + ["CommonUI.Features.Label.Players"] = [[玩家]], + ["CommonUI.Features.Label.Create"] = [[建立]], + ["CommonUI.Features.Label.CreateGames"] = [[建立遊戲]], + ["Feature.Chat.Response.ChatNameFullyModerated"] = [[聊天群組名稱受到仲裁。]], + ["Feature.Chat.Heading.LeaveGroup"] = [[離開群組]], + ["Feature.Chat.Label.SearchWord"] = [[搜尋]], + ["Feature.Chat.Action.Add"] = [[新增]], + ["Feature.Chat.Action.Leave"] = [[離開]], + ["Feature.Chat.Action.Remove"] = [[移除]], + ["Feature.Chat.Action.Cancel"] = [[取消]], + ["Feature.Chat.Description.NameGroupChat"] = [[為您的聊天群組命名]], + ["Feature.Chat.Heading.NewChatGroup"] = [[新增聊天群組]], + ["Feature.Chat.Label.SeeLess"] = [[簡閱]], + ["Feature.Chat.Label.InputPlaceHolder.SearchForFriends"] = [[搜尋朋友]], + ["Feature.Chat.Label.AddFriends"] = [[新增朋友]], + ["Feature.Chat.Label.ChatDetails"] = [[聊天詳情]], + ["Feature.Chat.Label.General"] = [[一般]], + ["Feature.Chat.Label.Members"] = [[會員]], + ["Feature.Chat.Label.ViewProfile"] = [[檢視個人檔]], + ["Feature.Chat.Label.ChatGroupName"] = [[聊天群組名稱]], + ["Feature.Chat.Action.Stay"] = [[留下]], + ["Feature.Chat.Action.Create"] = [[建立]], + ["Feature.Chat.Action.Send"] = [[傳送]], + ["Feature.Chat.Action.Save"] = [[儲存]], + ["Feature.Chat.Message.ToastText"] = [[您在聊天群組中至多可有 {friendNum} 位朋友。]], + ["Feature.Chat.Message.NoConnectionMsg"] = [[正在連線…]], + ["Feature.Chat.Message.Default"] = [[此聊天中並非每人都能看見您的訊息。]], + ["Feature.Chat.Message.MessageFilterForReceivers"] = [[此聊天中並非每人都能看見您的訊息。]], + ["Feature.Chat.Message.MessageContentModerated"] = [[您的訊息受到仲裁而未送出。]], + ["Feature.Chat.Label.PrivacySettings"] = [[隱私權設定]], + ["Feature.Chat.Label.ChatInputPlaceholder"] = [[說點什麼]], + ["Feature.Chat.Action.Confirm"] = [[好]], + ["Feature.Chat.Heading.FailedToLeaveGroup"] = [[未能離開群組]], + ["Feature.Chat.Message.FailedToLeaveGroup"] = [[無法將您自此次「{CONVERSATION_TITLE}」會話中移除。]], + ["Feature.Chat.Heading.FailedToRemoveUser"] = [[未能移除使用者]], + ["Feature.Chat.Message.FailedToRemoveUser"] = [[無法將使用者{USERNAME}自此次「{CONVERSATION_TITLE}」會話中移除。]], + ["Feature.Chat.Heading.FailedToRenameConversation"] = [[未能將此會話更名]], + ["Feature.Chat.Message.FailedToRenameConversation"] = [[此次「{EXISTING_NAME}」會話無法更名為「{NEW_NAME}」。]], + ["Feature.Chat.Message.LeaveGroup"] = [[您將無法繼續與此群組聊天。]], + ["Feature.Chat.Message.MakeFriendsToChat"] = [[在遊戲中交朋友以開始聊天和同樂。]], + ["Feature.Chat.Label.NotSet"] = [[未設定]], + ["Feature.Chat.Heading.Option"] = [[選項]], + ["Feature.Chat.Action.RemoveFromGroup"] = [[自群組移除]], + ["Feature.Chat.Action.RemoveUser"] = [[移除使用者]], + ["Feature.Chat.Message.RemoveUser"] = [[您是否確定要將{USERNAME}自此聊天群組移除?]], + ["Feature.Chat.Message.RemovedFromConversation"] = [[您已被從此群組中移除。]], + ["Feature.Chat.Action.ReportUser"] = [[舉報使用者]], + ["Feature.Chat.Label.SearchForFriendsAndChat"] = [[搜尋朋友和聊天群組]], + ["Feature.Chat.Action.SeeMoreFriends"] = [[看更多 ({NUMBER_OF_FRIENDS})]], + ["Feature.Chat.Label.Sent"] = [[已傳送]], + ["Feature.Chat.Heading.ShareGameToChat"] = [[在聊天中分享遊戲]], + ["Feature.Chat.Message.TurnOnChat"] = [[若要與朋友聊天,請在您的隱私權設定中開啟聊天功能]], + ["Feature.Chat.Action.ViewAssetDetails"] = [[檢視詳情]], + ["Feature.Chat.Label.ByBuilder"] = [[自{USERNAME}]], + ["Feature.Chat.Message.AlreadyPinnedGame"] = [[此遊戲已經釘選,請嘗試另一個遊戲。]], + ["Feature.Chat.Message.PinFailed"] = [[此遊戲無法釘選,請稍後再試。]], + ["Feature.Chat.Message.UnpinFailed"] = [[此遊戲無法取消釘選,請稍後再試。]], + ["Feature.Chat.Drawer.ShowMore"] = [[顯示更多]], + ["Feature.Chat.Drawer.ShowLess"] = [[顯示較少]], + ["Feature.Chat.Drawer.NoGames"] = [[無遊戲釘選或進行中。]], + ["Feature.Chat.Drawer.Recommended"] = [[推薦]], + ["Feature.Chat.Drawer.Loading"] = [[正在載入]], + ["Feature.Chat.Drawer.PinnedGame"] = [[已釘選的遊戲]], + ["Feature.Chat.Drawer.Play"] = [[玩]], + ["Feature.Chat.Drawer.Join"] = [[加入]], + ["Feature.Chat.Drawer.PlayGame"] = [[玩遊戲]], + ["Feature.Chat.Drawer.PinGame"] = [[釘選遊戲]], + ["Feature.Chat.Drawer.UnpinGame"] = [[取消釘選遊戲]], + ["Feature.Chat.Drawer.ViewDetails"] = [[檢視詳情]], + ["Feature.Chat.Label.NoDescriptionYet"] = [[尚無說明。]], + ["Feature.Chat.Message.GameLinkWasModerated"] = [[此遊戲連結受到仲裁而未送出。]], + ["Feature.Chat.ShareGameToChat.BrowseGames"] = [[瀏覽遊戲]], + ["Feature.Chat.ShareGameToChat.By"] = [[{creatorName}創作]], + ["Feature.Chat.ShareGameToChat.Popular"] = [[熱門]], + ["Feature.Chat.ShareGameToChat.Recent"] = [[近期使用]], + ["Feature.Chat.ShareGameToChat.Favorites"] = [[最愛]], + ["Feature.Chat.ShareGameToChat.FriendActivity"] = [[朋友活動]], + ["Feature.Chat.ShareGameToChat.NoPopularGames"] = [[無熱門遊戲可用]], + ["Feature.Chat.ShareGameToChat.NoRecentGames"] = [[您無近期玩過的遊戲]], + ["Feature.Chat.ShareGameToChat.NoFavoriteGames"] = [[您無最愛的遊戲]], + ["Feature.Chat.ShareGameToChat.NoFriendActivity"] = [[您無朋友活動]], + ["Feature.Chat.ShareGameToChat.GameNotAvailable"] = [[無法使用]], + ["Feature.Chat.ShareGameToChat.FailedToShareTheGame"] = [[無法分享遊戲,請稍後再試。]], + ["Feature.Chat.Heading.ChatWithFriends"] = [[與朋友聊天]], + ["Feature.Chat.Action.StartChatWithFriends"] = [[聊天]], + ["Feature.Friends.Label.SearchFriends"] = [[搜尋朋友]], + ["Feature.GameBadges.HeadingGameBadges"] = [[遊戲徽章]], + ["Feature.GameDetails.Label.By"] = [[自]], + ["Feature.GameDetails.Label.About"] = [[介紹]], + ["Feature.GameDetails.Label.Store"] = [[商店]], + ["Feature.GameDetails.Label.Leaderboards"] = [[排行榜]], + ["Feature.GameDetails.Label.Servers"] = [[伺服器]], + ["Feature.GameDetails.Heading.Description"] = [[說明]], + ["Feature.GameDetails.Label.Playing"] = [[正在玩]], + ["Feature.GameDetails.Label.Visits"] = [[造訪次數]], + ["Feature.GameDetails.Label.Created"] = [[建立時間]], + ["Feature.GameDetails.Label.Updated"] = [[更新時間]], + ["Feature.GameDetails.Label.MaxPlayers"] = [[玩家上限]], + ["Feature.GameDetails.Label.Genre"] = [[大類]], + ["Feature.GameDetails.Label.GameCopyLocked"] = [[此遊戲受保護而禁止複製]], + ["Feature.GameDetails.Label.ReportAbuse"] = [[提報濫行]], + ["Feature.GameDetails.Heading.RecommendedGames"] = [[推薦的遊戲]], + ["Feature.GameGear.Heading.GearForThisGame"] = [[此遊戲的裝備]], + ["Feature.GameLeaderboard.Label.Clans"] = [[部族]], + ["Feature.GameLeaderboard.Heading.Clans"] = [[部族]], + ["Feature.GameLeaderboard.Heading.Players"] = [[玩家]], + ["Feature.GameLeaderboard.Label.NoResults"] = [[未找到結果]], + ["Feature.GamePage.LabelPlayingPhrase"] = [[{playerCount} 人正在玩]], + ["Feature.GamePage.ActionSeeAll"] = [[查看全部]], + ["Feature.GamePage.LabelNoSearchResults"] = [[找不到搜尋結果]], + ["Feature.GamePage.LabelCancelField"] = [[取消]], + ["Feature.GamePage.LabelShowingResultsFor"] = [[顯示搜尋以下字串的結果:]], + ["Feature.GamePage.LabelSearchInsteadFor"] = [[改搜尋]], + ["Feature.GamePage.LabelSearchYouMightMean"] = [[您是否指:]], + ["Feature.GamePage.Label.Sponsored"] = [[經贊助]], + ["Feature.GamePage.Message.EndOfList"] = [[啊!您已來到結尾!]], + ["Feature.GamePage.Action.BackToTop"] = [[回到頂端]], + ["Feature.GamePass.Heading.PassesForThisGame"] = [[此遊戲的通行證]], + ["Feature.Home.HeadingFriends"] = [[朋友 ({friendCount})]], + ["Feature.Home.ActionSeeAll"] = [[查看全部]], + ["Feature.Home.Action.ViewMyFeed"] = [[檢視我的訊息饋送]], + ["Feature.PlayerSearchResults.Heading.PlayerResultsFor"] = [[搜尋玩家{startSpan}{keyword}{endSpan}的結果]], + ["Feature.PlayerSearchResults.Label.NoMatchesAvailable"] = [[無「{keyword}」的相符項]], + ["Feature.PlayerSearchResults.Label.EnterMinCharacters"] = [[請輸入至少 {keywordMinLength} 個字元。]], + ["Feature.PlayerSearchResults.Label.UnsafeInput"] = [[您輸入的內容不安全。請重試搜尋。]], + ["Feature.PlayerSearchResults.Label.ShowingCountOfResults"] = [[{countStartSpan}{resultsStart} - {resultsInPage} of {countEndSpan}{totalStartSpan}{totalResults}{totalEndSpan}]], + ["Feature.PlayerSearchResults.Label.AlsoKnownAsAbbreviation"] = [[又名]], + ["Feature.PlayerSearchResults.Label.ThisIsYou"] = [[這是您]], + ["Feature.PlayerSearchResults.Label.YouAreFriends"] = [[你們是朋友]], + ["Feature.PlayerSearchResults.Label.YouAreFollowing"] = [[您在關注]], + ["Feature.PlayerSearchResults.Action.AddFriend"] = [[新增朋友]], + ["Feature.PlayerSearchResults.Action.AcceptRequest"] = [[接受請求]], + ["Feature.PlayerSearchResults.Action.RequestSent"] = [[請求已送出]], + ["Feature.PlayerSearchResults.Action.Chat"] = [[聊天]], + ["Feature.PlayerSearchResults.Action.JoinGame"] = [[加入遊戲]], + ["Feature.PrivateServers.Heading.VipServers"] = [[VIP 伺服器]], + ["Feature.Profile.Label.About"] = [[介紹]], + ["Feature.ServerList.Heading.OtherServers"] = [[其他伺服器]], + ["Feature.ServerList.Heading.ServersMyFriendsAreIn"] = [[我朋友在的伺服器]], + ["Game.Launch.IDS_ROBLOX_INSTALLED"] = [[ROBLOX 已成功安裝!]], + ["Game.Launch.IDS_ROBLOX_TOPLAY"] = [[在任何遊戲按一下「開始遊戲」按鈕以加入行動!]], + ["Game.Launch.IDS_STUDIO_SUCCESS1"] = [[ROBLOX STUDIO 已成功安裝!]], + ["Game.Launch.IDS_STUDIO_SUCCESS2"] = [[按一下「啟動 Studio 」來製作您的新遊戲!]], + ["Game.Launch.IDS_ROBLOX_DOWNLOAD"] = [[下載並安裝 Roblox]], + ["Game.Launch.IDS_CHECKING_FILE"] = [[檔案檢查進行中…]], + ["Game.Launch.IDS_FILE_CHECKED"] = [[檔案檢查完成]], + ["Game.Launch.IDS_ROBLOX_STARTING"] = [[正在啟動 %s …]], + ["Game.Launch.IDS_ROBLOX_UPTODATE"] = [[%s 已是最新版本]], + ["Game.Launch.IDS_ROBLOX_UPGRADING"] = [[正在升級 %s …]], + ["Game.Launch.IDS_ROBLOX_INSTALLING"] = [[正在安裝 %s …]], + ["Game.Launch.IDS_ROBLOX_CONNECTING"] = [[正在連線到 %s …]], + ["Game.Launch.IDS_BOOTS_ASK_DOWNLOAD"] = [[下載最新的bootstrapper?]], + ["Game.Launch.IDS_BOOTS_GET_LATEST"] = [[正在擷取最新的 %s …]], + ["Game.Launch.IDS_PLEASE_WAIT"] = [[請稍後 …]], + ["Game.Launch.IDS_BOOTS_SHUTDOWN"] = [[正在關閉 %s]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLING"] = [[正在卸載 %s …]], + ["Game.Launch.IDS_ROBLOX_UNINSTALLED"] = [[%s 已安裝]], + ["Game.Launch.IDS_BOOTS_CONFIGURING"] = [[正在設定 %s …]], + ["Search.GlobalSearch.Example.SearchGames"] = [[搜尋遊戲]], +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Localization.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Localization.lua new file mode 100644 index 0000000..3e6104f --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Localization.lua @@ -0,0 +1,56 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local LocaleTables = Modules.LuaApp.Locales +local LocalizationContext = require(Modules.LuaApp.LocalizationContext) + + +local function loadTables(locale) + local relevantLanguages = LocalizationContext.getRelevantLanguages(locale) + local translations = {} + for _,language in ipairs(relevantLanguages) do + local languageTable = LocaleTables:FindFirstChild(language) + if languageTable then + translations[language] = require(languageTable) + end + end + return translations +end + +local Localization = {} +Localization.__index = Localization + +function Localization.new(locale) + + local self = { + locale = locale, + } + setmetatable(self, { + __index = Localization, + }) + + local translations = loadTables(locale) + self.localizationContext = LocalizationContext.new(translations) + return self +end + +function Localization.mock() + -- when running tests, use a mock object to get off the ground quickly + return Localization.new("en-us") +end + +function Localization:SetLocale(locale) + self.locale = locale + local translations = loadTables(locale) + self.localizationContext:addTranslations(translations) +end + +function Localization:Format(key, arguments) + if not key then + error("ERROR: NO STRING FOR KEY") + end + + local string = self.localizationContext:getString(self.locale, key, arguments) + return string +end + +return Localization \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Localization.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Localization.spec.lua new file mode 100644 index 0000000..492a294 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Localization.spec.lua @@ -0,0 +1,21 @@ +return function() + local Localization = require(script.Parent.Localization) + + describe("SetLocale", function() + it("should change the locale", function() + local localization = Localization.new("en-us") + local translation = localization:Format("Common.Presence.Label.Online") + expect(translation).to.equal("Online") + localization:SetLocale("es-es") + translation = localization:Format("Common.Presence.Label.Online") + expect(translation).to.equal("Conectado") + end) + end) + + describe("mock", function() + it("should create a localization without a problem", function() + local fakeLocalization = Localization.mock() + expect(fakeLocalization).to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/LocalizationContext.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/LocalizationContext.lua new file mode 100644 index 0000000..e78ef17 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/LocalizationContext.lua @@ -0,0 +1,142 @@ +--[[ + Contains all of the loaded translations and provides methods to translate + keys and parameters to strings. + + LocalizationContext doesn't handle loading of specific languages, but does + recommend what languages should be loaded (if available). + + To create a new LocalizationContext: + + local currentLanguage = LocalizationService.RobloxLocaleId + local languages = LocalizationContext.getRelevantLanguages(currentLanguage) + + local translations = {} + + -- Use the list of languages to load a set of translation tables here. + -- A translation table is just a map from key to the translated string. + -- `translations` is a map from language to translation tables. + + local context = LocalizationContext.new(translations) + + -- Get a string that doesn't require parameters + context:getString(currentLanguage, "SOME_KEY") + + -- Passing parameters: + context:getString(currentLanguage, "FANCY_KEY", { + apples = 5, + }) + + Additional languages can be added after the LocalizationContext is created + by calling `addTranslations`. Whenever the user's language changes, call + `getRelevantLanguages` to get a new list of languages to load, load them, + then call `addTranslations` to merge them in with the existing tables. +]] + +--[[ + Finds the base language code for the given language, if there is one. + + We assume: + * Language codes are of the form LANGUAGE or LANGUAGE_COUNTRY + * LANGUAGE_COUNTRY is more specific than LANGUAGE +]] + +local function getBaseLanguage(languageName) + return languageName:match("^(%w+)[-_]") +end + +local LocalizationContext = {} +LocalizationContext.__index = LocalizationContext + +function LocalizationContext.new(translations) + local self = { + _translations = translations, + } + + setmetatable(self, LocalizationContext) + + return self +end + +--[[ + Add translations to an existing LocalizationContext, such as when a user + switches languages while the app is running. +]] +function LocalizationContext:addTranslations(translations) + self._translations = translations +end + +--[[ + Yields a list of languages relevant to the current user. + + When the user's language changes, query this value, load those translations, + and add them to the LocalizationContext using addTranslations. +]] +function LocalizationContext.getRelevantLanguages(primaryLanguage) + local languages = {} + + -- Load the language itself if available. + table.insert(languages, primaryLanguage) + + -- If there's a fallback for our current language, load that as well. + local fallbackLanguage = getBaseLanguage(primaryLanguage) + if fallbackLanguage then + table.insert(languages, fallbackLanguage) + end + + -- We should always load English, as it should contain every valid key. + table.insert(languages, "en-us") + return languages +end + +function LocalizationContext:_getSourceString(language, key) + local translationTable = self._translations[language] + + if not translationTable then + return nil + end + + return translationTable[key] +end + +--[[ + Translate a key with a set of arguments into the given language. + + `language` must be explicitly provided +]] +function LocalizationContext:getString(language, key, parameters) + local exactValue = self:_getSourceString(language, key) + + local baseLanguage = getBaseLanguage(language) + + local baseLanguageValue + if baseLanguage then + baseLanguageValue = self:_getSourceString(baseLanguage, key) + end + + local englishValue = self:_getSourceString("en-us", key) + + -- We try to find source strings in descending priority here: + local sourceString = exactValue or baseLanguageValue or englishValue + + -- Missing translations are considered a developer error, so we throw here. + if not sourceString then + local message = ( + "Couldn't find value for translation key %q!\n" .. + "Tried these languages: %s, %s, %s" + ):format( + key, + language, baseLanguage, "en-us" + ) + error(message, 2) + end + + -- If we have parameters to insert into the string, put them in! + -- We don't check for missing parameters, should we in the future? + if parameters then + return (sourceString:gsub("{(.-)}", parameters)) + else + return sourceString + end +end + +return LocalizationContext \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/LocalizationContext.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/LocalizationContext.spec.lua new file mode 100644 index 0000000..8c7b8c0 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/LocalizationContext.spec.lua @@ -0,0 +1,66 @@ +return function() + local LocalizationContext = require(script.Parent.LocalizationContext) + + it("should pull from the correct language if available", function() + local context = LocalizationContext.new({ + ["es-mx"] = { + ["SomeKey"] = "Foo", + }, + ["es"] = { + ["SomeKey"] = "Bar", + }, + ["en-us"] = { + ["SomeKey"] = "Baz", + }, + }) + + expect(context:getString("es-mx", "SomeKey")).to.equal("Foo") + expect(context:getString("es", "SomeKey")).to.equal("Bar") + expect(context:getString("en", "SomeKey")).to.equal("Baz") + end) + + it("should fall through to a language's base language", function() + local context = LocalizationContext.new({ + ["es-mx"] = {}, + ["es"] = { + ["SomeKey"] = "Bar", + }, + ["en"] = { + ["SomeKey"] = "Baz", + }, + }) + + expect(context:getString("es-mx", "SomeKey")).to.equal("Bar") + expect(context:getString("es", "SomeKey")).to.equal("Bar") + expect(context:getString("en", "SomeKey")).to.equal("Baz") + end) + + it("should fall through to English if keys are missing in each table", function() + local context = LocalizationContext.new({ + ["es-mx"] = {}, + ["es"] = {}, + ["en-us"] = { + ["SomeKey"] = "Baz", + }, + }) + + expect(context:getString("es-mx", "SomeKey")).to.equal("Baz") + expect(context:getString("es", "SomeKey")).to.equal("Baz") + expect(context:getString("en_us", "SomeKey")).to.equal("Baz") + end) + + it("should replace formatting identifiers of the form {name}", function() + local context = LocalizationContext.new({ + ["en-us"] = { + ["SomeKey"] = "{greeting}, {target}!", + }, + }) + + local value = context:getString("en-us", "SomeKey", { + greeting = "Hello", + target = "world", + }) + + expect(value).to.equal("Hello, world!") + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/MockId.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/MockId.lua new file mode 100644 index 0000000..456cea0 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/MockId.lua @@ -0,0 +1,10 @@ +--[[ + A function to return a fake ID, used for testing +]] + +local lastId = 0 + +return function() + lastId = lastId + 1 + return ("MOCK-%d"):format(lastId) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/Game.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/Game.lua new file mode 100644 index 0000000..95484b7 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/Game.lua @@ -0,0 +1,62 @@ +--[[ + { + creatorId : number , + creatorName : string , + creatorType : CretorTypeEnum , + placeId : number , + universeId : number , + imageToken : string , + name : string , + totalUpVotes : number , + totalDownVotes : number , + playerCount : number , + + } +]] + +local CreatorType = { + User = "User", + Group = "Group" +} + +local Game = {} + +function Game.new() + local self = {} + + return self +end + +function Game.mock() + local self = Game.new() + self.universeId = 149757 + self.imageToken = 70395446 + self.totalDownVotes = 2564 + self.placeId = 10395446 + self.name = "test" + self.totalUpVotes = 10970 + self.creatorId = 22915773 + self.creatorName = "Jaegerblox" + self.creatorType = CreatorType.User + self.playabilityStatus = "" + + return self +end + +function Game.fromJsonData(gameJson) + local self = Game.new() + self.creatorId = gameJson.creatorId + self.creatorName = gameJson.creatorName + self.creatorType = gameJson.creatorType + self.placeId = gameJson.placeId + self.universeId = gameJson.universeId + self.imageToken = gameJson.imageToken + self.name = gameJson.name + self.totalUpVotes = gameJson.totalUpVotes + self.totalDownVotes = gameJson.totalDownVotes + self.playabilityStatus = "" + + return self +end + +return Game \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/GameDetail.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/GameDetail.lua new file mode 100644 index 0000000..3c3e539 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/GameDetail.lua @@ -0,0 +1,47 @@ +--[[ + { -- Mock data. No data inflow exists yet + playing : number , + visits : number , + created = string , + updated = string , + maxPlayers = number , + genre = string , + allowedGear = string , + description = string , + } +]] + +local GameDetail = {} + +function GameDetail.new() + local self = {} + + return self +end + +function GameDetail.mock() + local self = GameDetail.new() + self.playing = 666 + self.visits = 999 + self.created = "1/1/1900" + self.updated = "1/1/2020" + self.maxPlayers = 10 + self.genre = "Fighting" + self.allowedGear = "None" + self.description = "This description decribes itself" + return self +end + +function GameDetail.fromJsonData(gameJson) + local self = GameDetail.new() + self.playing = gameJson.playing + self.visits = gameJson.visits + self.created = gameJson.created + self.updated = gameJson.updated + self.maxPlayers = gameJson.maxPlayers + self.genre = gameJson.genre + self.allowedGear = gameJson.allowedGear + return self +end + +return GameDetail \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/GameSort.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/GameSort.lua new file mode 100644 index 0000000..eb923f0 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/GameSort.lua @@ -0,0 +1,80 @@ +--[[ + { + "token": string, + "name": string, + "displayName" : string, + "timeOptionsAvailable": boolean, + "genreOptionsAvailable": boolean, + "numberOfRows": number, + "isDefaultSort" : boolean + } +]] + +-- This is a static client-side mapping of icons that we want our sorts to use: +local ALL_GAME_ICONS = { + default = "rbxasset://textures/ui/LuaApp/category/ic-default.png", + + BuildersClub = "rbxasset://textures/ui/LuaApp/category/ic-bc.png", + Featured = "rbxasset://textures/ui/LuaApp/category/ic-featured.png", + FriendActivity = "rbxasset://textures/ui/LuaApp/category/ic-friend activity.png", + MyFavorite = "rbxasset://textures/ui/LuaApp/category/ic-my favorite.png", + MyRecent = "rbxasset://textures/ui/LuaApp/category/ic-my recent.png", + Popular = "rbxasset://textures/ui/LuaApp/category/ic-popular.png", + PopularInCountry = "rbxasset://textures/ui/LuaApp/category/ic-popular in country.png", + PopularInVr = "rbxasset://textures/ui/LuaApp/category/ic-popular in VR.png", + Purchased = "rbxasset://textures/ui/LuaApp/category/ic-purchased.png", + TopFavorite = "rbxasset://textures/ui/LuaApp/category/ic-top favorite.png", + TopGrossing = "rbxasset://textures/ui/LuaApp/category/ic-top earning.png", + TopPaid = "rbxasset://textures/ui/LuaApp/category/ic-top paid.png", + TopRated = "rbxasset://textures/ui/LuaApp/category/ic-top rated.png", + TopRetaining = "rbxasset://textures/ui/LuaApp/category/ic-recommended.png", +} + +local GameSort = {} + +function GameSort.new() + local self = {} + + return self +end + +function GameSort.mock() + local self = GameSort.new() + self.displayIcon = "" + self.displayName = "" + self.genreOptionsAvailable = false + self.isDefaultSort = true + self.name = "" + self.numberOfRows = 1 + self.timeOptionsAvailable = false + self.token = "" + self.contextUniverseId = "" + self.contextCountryRegionId = "" + + return self +end + +function GameSort.fromJsonData(gameSortJson) + local self = GameSort.new() + self.displayName = gameSortJson.displayName + self.genreOptionsAvailable = gameSortJson.genreOptionsAvailable + self.isDefaultSort = gameSortJson.isDefaultSort + self.name = gameSortJson.name + self.numberOfRows = gameSortJson.numberOfRows + self.timeOptionsAvailable = gameSortJson.timeOptionsAvailable + self.token = gameSortJson.token + self.contextUniverseId = gameSortJson.contextUniverseId + self.contextCountryRegionId = gameSortJson.contextCountryRegionId + + -- Assign the icon: + if self.name ~= nil then + self.displayIcon = ALL_GAME_ICONS[self.name] + end + if self.displayIcon == nil then + self.displayIcon = ALL_GAME_ICONS["default"] + end + + return self +end + +return GameSort \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/GameSortContents.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/GameSortContents.lua new file mode 100644 index 0000000..1dc05de --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/GameSortContents.lua @@ -0,0 +1,70 @@ +--[[ + { + -- A list of placeIds to indicate which games are in this sort. + entries: [ + [1] = entry1 : GameSortEntry, + [2] = entry2 : GameSortEntry, + ... + ] + + -- A value to remember how many games we've requested so far. + -- This value doens't necessarily equal #games. For more info, see + -- comments inside GamesGetList.lua. + "rowsRequested" : number, + + -- Indicating if we can request more games. + "hasMoreRows": boolean, + } +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local GameSortEntry = require(Modules.LuaApp.Models.GameSortEntry) +local TableUtilities = require(Modules.LuaApp.TableUtilities) + +local GameSortContents = {} + +function GameSortContents.new() + local self = {} + + self.entries = {} + self.rowsRequested = 0 + self.hasMoreRows = false + self.nextPageExclusiveStartId = 0 + + return self +end + +function GameSortContents.mock() + local self = GameSortContents.new() + + self.entries = { GameSortEntry.mock(), } + self.rowsRequested = 1 + self.hasMoreRows = false + self.nextPageExclusiveStartId = 0 + + return self +end + +function GameSortContents.fromData(gameSortContentsData) + local self = GameSortContents.new() + + self.entries = gameSortContentsData.entries + self.rowsRequested = gameSortContentsData.rowsRequested + self.hasMoreRows = gameSortContentsData.hasMoreRows + self.nextPageExclusiveStartId = gameSortContentsData.nextPageExclusiveStartId + + return self +end + +function GameSortContents.IsEqual(data1, data2) + -- Compare the entries + if not TableUtilities.ShallowEqual(data1.entries, data2.entries) then + return false + end + + -- Compare other things + local ignoreEntries = {["entries"] = true} + + return TableUtilities.ShallowEqual(data1, data2, ignoreEntries) +end + +return GameSortContents \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/GameSortEntry.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/GameSortEntry.lua new file mode 100644 index 0000000..b06754a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/GameSortEntry.lua @@ -0,0 +1,38 @@ +--[[ + { + universeId : number , + isSponsored : bool , + adId : number , + playerCount : number , + } +]] + +local GameSortEntry = {} + +function GameSortEntry.new() + local self = {} + + return self +end + +function GameSortEntry.mock(universeId) + local self = GameSortEntry.new() + self.universeId = universeId or 149757 + self.placeId = 384314 + self.isSponsored = false + self.adId = nil + self.playerCount = 150 + return self +end + +function GameSortEntry.fromJsonData(gameSortEntryJson) + local self = GameSortEntry.new() + self.universeId = gameSortEntryJson.universeId + self.placeId = gameSortEntryJson.placeId + self.isSponsored = gameSortEntryJson.isSponsored + self.adId = gameSortEntryJson.nativeAdData + self.playerCount = gameSortEntryJson.playerCount + return self +end + +return GameSortEntry \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/GameSortGroup.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/GameSortGroup.lua new file mode 100644 index 0000000..a5c24a2 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/GameSortGroup.lua @@ -0,0 +1,23 @@ +--[[ + { + "sorts" : array + } +]] + +local GameSortGroup = {} + +function GameSortGroup.new() + local self = {} + self.sorts = {} + + return self +end + +function GameSortGroup.mock() + local self = GameSortGroup.new() + self.sorts = {} + + return self +end + +return GameSortGroup \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/SearchInGames.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/SearchInGames.lua new file mode 100644 index 0000000..e6367b4 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/SearchInGames.lua @@ -0,0 +1,77 @@ +--[[ + { + keyword : string, + entries: [ + [1] = entry1 : GameSortEntry, + [2] = entry2 : GameSortEntry, + ... + ] + suggestedKeyword = string, + correctedKeyword = string, + filteredKeyword = string, + + -- A value to remember how many games we've requested so far. + -- This value doens't necessarily equal #games. For more info, see + -- comments inside GamesGetList.lua. + "rowsRequested" : number, + + -- Indicating if we can request more games. + hasMoreRows = bool, + + -- Indicating if we have enabled keywordSuggestion + isKeywordSuggestionEnabled = bool, + } +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local GameSortEntry = require(Modules.LuaApp.Models.GameSortEntry) +local TableUtilities = require(Modules.LuaApp.TableUtilities) + +local SearchInGames = {} + +function SearchInGames.new() + local self = {} + + return self +end + +function SearchInGames.mock() + local self = SearchInGames.new() + self.keyword = "Meepcity" + self.entries = { GameSortEntry.mock(), } + self.suggestedKeyword = nil + self.correctedKeyword = nil + self.filteredKeyword = nil + self.rowsRequested = 30 + self.hasMoreRows = false + self.isKeywordSuggestionEnabled = false + + return self +end + +function SearchInGames.fromJsonData(searchInGamesJson, keyword, entries, rowsRequested, isKeywordSuggestionEnabled) + local self = SearchInGames.new() + self.keyword = keyword + self.entries = entries + self.suggestedKeyword = searchInGamesJson.suggestedKeyword + self.correctedKeyword = searchInGamesJson.correctedKeyword + self.filteredKeyword = searchInGamesJson.filteredKeyword + self.rowsRequested = rowsRequested + self.hasMoreRows = searchInGamesJson.hasMoreRows + self.isKeywordSuggestionEnabled = isKeywordSuggestionEnabled + + return self +end + +function SearchInGames.IsEqual(data1, data2) + -- Compare the entries + if not TableUtilities.ShallowEqual(data1.entries, data2.entries) then + return false + end + + -- Compare other things + local ignoreEntries = {["entries"] = true} + + return TableUtilities.ShallowEqual(data1, data2, ignoreEntries) +end + +return SearchInGames \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/ThumbnailRequest.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/ThumbnailRequest.lua new file mode 100644 index 0000000..6df561f --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/ThumbnailRequest.lua @@ -0,0 +1,18 @@ +local ThumbnailRequest = {} + +function ThumbnailRequest.new() + local self = {} + + return self +end + +function ThumbnailRequest.fromData(thumbnailType, thumbnailSize) + local self = ThumbnailRequest.new() + + self.thumbnailType = thumbnailType + self.thumbnailSize = thumbnailSize + + return self +end + +return ThumbnailRequest \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/User.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/User.lua new file mode 100644 index 0000000..be2d87c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Models/User.lua @@ -0,0 +1,85 @@ +local Players = game:GetService("Players") +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules + +local MockId = require(Modules.LuaApp.MockId) +local FFlagFixUsersReducerDataLoss = settings():GetFFlag("FixUsersReducerDataLoss") + +local User = {} + +User.PresenceType = { + OFFLINE = "OFFLINE", + ONLINE = "ONLINE", + IN_GAME = "IN_GAME", + IN_STUDIO = "IN_STUDIO", +} + +function User.new() + local self = {} + + return self +end + +function User.mock() + local self = User.new() + + self.id = MockId() + + self.isFetching = false + self.isFriend = false + self.lastLocation = nil + self.name = "USER NAME" + self.placeId = nil + self.presence = User.PresenceType.OFFLINE + self.membership = nil + if not FFlagFixUsersReducerDataLoss then + self.thumbnails = {} + end + + return self +end + +function User.fromData(id, name, isFriend) + local self = User.new() + + self.id = tostring(id) + + self.isFetching = false + self.isFriend = isFriend + self.lastLocation = nil + self.name = name + self.placeId = nil + if FFlagFixUsersReducerDataLoss then + self.presence = (self.id == tostring(Players.LocalPlayer.UserId)) and User.PresenceType.ONLINE or nil + else + self.presence = (self.id == tostring(Players.LocalPlayer.UserId)) and User.PresenceType.ONLINE or + User.PresenceType.OFFLINE + self.thumbnails = {} + end + + return self +end + +function User.userPresenceToText(localization, user) + local presence = user.presence + local lastLocation = user.lastLocation + + if not presence then + return '' + end + + if presence == User.PresenceType.OFFLINE then + return localization:Format("Common.Presence.Label.Offline") + elseif presence == User.PresenceType.ONLINE then + return localization:Format("Common.Presence.Label.Online") + elseif (presence == User.PresenceType.IN_GAME) or (presence == User.PresenceType.IN_STUDIO) then + if lastLocation ~= nil then + return lastLocation + else + return localization:Format("Common.Presence.Label.Online") + end + end +end + +return User \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/PageIndex.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/PageIndex.lua new file mode 100644 index 0000000..4b225c1 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/PageIndex.lua @@ -0,0 +1,64 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local AppPage = require(Modules.LuaApp.AppPage) +local DeviceOrientationMode = require(Modules.LuaApp.DeviceOrientationMode) + +local PageIndexPortrait = { + [AppPage.Home] = 1, + [AppPage.Games] = 2, + [AppPage.AvatarEditor] = 3, + [AppPage.Chat] = 4, + [AppPage.More] = 5, +} + +local PageIndexLandscape = { + [AppPage.Home] = 1, + [AppPage.Games] = 2, + [AppPage.Catalog] = 3, + [AppPage.AvatarEditor] = 4, + [AppPage.Friends] = 5, + [AppPage.Chat] = 6, + [AppPage.More] = 7, +} + +local GetPageTypeByIndexPortrait = {} +for pageType, pageTypeIndex in pairs(PageIndexPortrait) do + GetPageTypeByIndexPortrait[pageTypeIndex] = pageType +end + +local GetPageTypeByIndexLandscape = {} +for pageType, pageTypeIndex in pairs(PageIndexLandscape) do + GetPageTypeByIndexLandscape[pageTypeIndex] = pageType +end + +local function GetIndexByPageType(pageType, deviceOrientation) + if deviceOrientation == DeviceOrientationMode.Portrait then + return PageIndexPortrait[pageType] + elseif deviceOrientation == DeviceOrientationMode.Landscape then + return PageIndexLandscape[pageType] + end + return nil +end + +local function GetPageTypeByIndex(pageIndex, deviceOrientation) + if deviceOrientation == DeviceOrientationMode.Portrait then + return GetPageTypeByIndexPortrait[pageIndex] + elseif deviceOrientation == DeviceOrientationMode.Landscape then + return GetPageTypeByIndexLandscape[pageIndex] + end + return nil +end + +local function GetTotalPages(deviceOrientation) + if deviceOrientation == DeviceOrientationMode.Portrait then + return #GetPageTypeByIndexPortrait + elseif deviceOrientation == DeviceOrientationMode.Landscape then + return #GetPageTypeByIndexLandscape + end + return 0 +end + +return { + GetIndexByPageType = GetIndexByPageType, + GetPageTypeByIndex = GetPageTypeByIndex, + GetTotalPages = GetTotalPages, +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/PerformanceTesting.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/PerformanceTesting.lua new file mode 100644 index 0000000..9cb1e5e --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/PerformanceTesting.lua @@ -0,0 +1,186 @@ + +local CoreGui = game:GetService("CoreGui") +local HttpService = game:GetService("HttpService") +local VirtualInputManager = game:GetService("VirtualInputManager") +local RunService = game:GetService("RunService") +local Stats = game:GetService("Stats") + +local LuaChat = CoreGui.RobloxGui.Modules.LuaChat +local LuaApp = CoreGui.RobloxGui.Modules.LuaApp + +local Config = require(LuaApp.Config) +local Constants = require(LuaChat.Constants) +local Create = require(LuaChat.Create) + +local PerformanceTesting = {} + +PerformanceTesting.Mode = { + None = "None", + Playing = "Playing", + Recording = "Recording", +} + +function PerformanceTesting:LuaState() + local frm = Stats:FindFirstChild("FrameRateManager") + if not frm then + warn("FrameRateManager not present in Stats") + return { + AverageFPS = 0, + FrameTimeVariance = 0, + } + end + return { + AverageFPS = frm:FindFirstChild("AverageFPS"):GetValue(), + FrameTimeVariance = frm:FindFirstChild("FrameTimeVariance"):GetValue(), + } +end + +function PerformanceTesting:ShowResults(expected) + local actual = PerformanceTesting:LuaState() + + local gui = Create.new "ScreenGui" { + Name = "TestResults", + DisplayOrder = 5, + + Create.new "Frame" { + + BackgroundTransparency = 0, + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Constants.Color.WHITE, + BorderSizePixel = 0, + + Create.new "UIListLayout" { + SortOrder = "LayoutOrder", + }, + + Create.new "TextLabel" { + + Name = "Title", + BackgroundTransparency = 1, + TextSize = Constants.Font.FONT_SIZE_14, + Size = UDim2.new(1, 0, 0, 100), + Text = "Test: " .. Config.General.PerformanceTestFilename + }, + + Create.new "TextLabel" { + + Name = "AverageFPS", + TextXAlignment = Enum.TextXAlignment.Left, + BackgroundTransparency = 1, + TextSize = Constants.Font.FONT_SIZE_14, + Size = UDim2.new(1, 0, 0, 50), + Text = "AverageFPS:", + }, + Create.new "TextLabel" { + + Name = "ExpectedAverageFPS", + TextXAlignment = Enum.TextXAlignment.Left, + BackgroundTransparency = 1, + TextSize = Constants.Font.FONT_SIZE_14, + Size = UDim2.new(1, 0, 0, 30), + Text = string.format("Expected: %g", expected.AverageFPS), + }, + Create.new "TextLabel" { + + Name = "ActualAverageFPS", + TextXAlignment = Enum.TextXAlignment.Left, + BackgroundTransparency = 1, + TextSize = Constants.Font.FONT_SIZE_14, + Size = UDim2.new(1, 0, 0, 30), + Text = string.format("Actual : %g", actual.AverageFPS), + }, + Create.new "TextLabel" { + + Name = "ActualAverageFPS", + TextXAlignment = Enum.TextXAlignment.Left, + BackgroundTransparency = 1, + TextSize = Constants.Font.FONT_SIZE_14, + Size = UDim2.new(1, 0, 0, 30), + Text = string.format("Delta : %g", actual.AverageFPS-expected.AverageFPS), + }, + + Create.new "TextLabel" { + + Name = "FrameTimeVariance", + TextXAlignment = Enum.TextXAlignment.Left, + BackgroundTransparency = 1, + TextSize = Constants.Font.FONT_SIZE_14, + Size = UDim2.new(1, 0, 0, 50), + Text = "FrameTimeVariance:", + }, + Create.new "TextLabel" { + + Name = "ExpectedAFrameTimeVariance", + TextXAlignment = Enum.TextXAlignment.Left, + BackgroundTransparency = 1, + TextSize = Constants.Font.FONT_SIZE_14, + Size = UDim2.new(1, 0, 0, 30), + Text = string.format("Expected: %g", expected.FrameTimeVariance), + }, + Create.new "TextLabel" { + + Name = "ActualFrameTimeVariance", + TextXAlignment = Enum.TextXAlignment.Left, + BackgroundTransparency = 1, + TextSize = Constants.Font.FONT_SIZE_14, + Size = UDim2.new(1, 0, 0, 30), + Text = string.format("Actual : %g", actual.FrameTimeVariance), + }, + Create.new "TextLabel" { + + Name = "ActualFrameTimeVariance", + TextXAlignment = Enum.TextXAlignment.Left, + BackgroundTransparency = 1, + TextSize = Constants.Font.FONT_SIZE_14, + Size = UDim2.new(1, 0, 0, 30), + Text = string.format("Delta : %g", actual.FrameTimeVariance-expected.FrameTimeVariance), + }, + } + } + gui.Parent = CoreGui +end + +function PerformanceTesting:Record() + RunService:setThrottleFramerateEnabled(false) + VirtualInputManager.RecordingCompleted:Connect(function(json) + print("*** Finished Recording Test ***") + VirtualInputManager.AdditionalLuaState = HttpService:JSONEncode(self:LuaState()) + print("~~~ DUMP ~~~") + VirtualInputManager:Dump() + end) + VirtualInputManager:StartRecording() +end + +function PerformanceTesting:Play(filename) + print("PerformanceTesting:Play") + RunService:setThrottleFramerateEnabled(false) + VirtualInputManager.PlaybackCompleted:Connect(function() + print("*** Finished Playing Test ***") + local expectedState = HttpService:JSONDecode(VirtualInputManager.AdditionalLuaState) + self:ShowResults(expectedState) + end) + VirtualInputManager:StartPlaying(filename) +end + +function PerformanceTesting:Stop() + if Config.General.PerformanceTestingMode == Enum.VirtualInputMode.Recording then + VirtualInputManager:StopRecording() + end +end + +function PerformanceTesting:Initialize(appState) + + if Config.General.PerformanceTestingMode == Enum.VirtualInputMode.None then + return + end + + self.appState = appState + + if Config.General.PerformanceTestingMode == Enum.VirtualInputMode.Recording then + self:Record() + elseif Config.General.PerformanceTestingMode == Enum.VirtualInputMode.Playing then + self:Play(Config.General.PerformanceTestFilename) + end +end + +return PerformanceTesting diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Promise.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Promise.lua new file mode 100644 index 0000000..6fcff90 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Promise.lua @@ -0,0 +1,343 @@ +--[[ + An implementation of Promises similar to Promise/A+. +]] +local TU = require(script.Parent.TableUtilities) + +local PROMISE_DEBUG = true + +-- If promise debugging is on, use a version of pcall that warns on failure. +-- This is useful for finding errors that happen within Promise itself. +local wpcall +if PROMISE_DEBUG then + wpcall = function(f, ...) + local result = { pcall(f, ...) } + + if not result[1] then + warn(result[2]) + end + + return unpack(result) + end +else + wpcall = pcall +end + +--[[ + Creates a function that invokes a callback with correct error handling and + resolution mechanisms. +]] +local function createAdvancer(callback, resolve, reject) + return function(...) + local result = { wpcall(callback, ...) } + local ok = table.remove(result, 1) + + if ok then + resolve(unpack(result)) + else + reject(unpack(result)) + end + end +end + +local function isEmpty(t) + return next(t) == nil +end + +local Promise = {} +Promise.__index = Promise + +Promise.Status = { + Started = "Started", + Resolved = "Resolved", + Rejected = "Rejected", +} + +--[[ + Constructs a new Promise with the given initializing callback. + + This is generally only called when directly wrapping a non-promise API into + a promise-based version. + + The callback will receive 'resolve' and 'reject' methods, used to start + invoking the promise chain. + + For example: + + local function get(url) + return Promise.new(function(resolve, reject) + spawn(function() + resolve(HttpService:GetAsync(url)) + end) + end) + end + + get("https://google.com") + :andThen(function(stuff) + print("Got some stuff!", stuff) + end) +]] +function Promise.new(callback) + local promise = { + -- Used to locate where a promise was created + _source = debug.traceback(), + + -- A tag to identify us as a promise + _type = "Promise", + + _status = Promise.Status.Started, + + -- A table containing a list of all results, whether success or failure. + -- Only valid if _status is set to something besides Started + _value = nil, + + -- Queues representing functions we should invoke when we update! + _queuedResolve = {}, + _queuedReject = {}, + } + + setmetatable(promise, Promise) + + local function resolve(...) + promise:_resolve(...) + end + + local function reject(...) + promise:_reject(...) + end + + local ok, err = wpcall(callback, resolve, reject) + + if not ok and promise._status == Promise.Status.Started then + reject(err) + end + + return promise +end + +--[[ + Create a promise that represents the immediately resolved value. +]] +function Promise.resolve(value) + return Promise.new(function(resolve) + resolve(value) + end) +end + +--[[ + Create a promise that represents the immediately rejected value. +]] +function Promise.reject(value) + return Promise.new(function(_, reject) + reject(value) + end) +end + +--[[ + Returns a new promise that: + * is resolved when all input promises resolve + * is rejected if ANY input promises reject +]] +function Promise.all(...) + local promises = {...} + + -- check if we've been given a list of promises, not just a variable number of promises + if type(promises[1]) == "table" and promises[1]._type ~= "Promise" then + -- we've been given a table of promises already + promises = promises[1] + end + + return Promise.new(function(resolve, reject) + local isResolved = false + local results = {} + local totalCompleted = 0 + local function promiseCompleted(index, result) + if isResolved then + return + end + + results[index] = result + totalCompleted = totalCompleted + 1 + + if totalCompleted == #promises then + resolve(results) + isResolved = true + end + end + + if #promises == 0 then + resolve(results) + isResolved = true + return + end + + for index, promise in ipairs(promises) do + -- if a promise isn't resolved yet, add listeners for when it does + if promise._status == Promise.Status.Started then + promise:andThen(function(result) + promiseCompleted(index, result) + end):catch(function(reason) + isResolved = true + reject(reason) + end) + + -- if a promise is already resolved, move on + elseif promise._status == Promise.Status.Resolved then + promiseCompleted(index, unpack(promise._value)) + + -- if a promise is rejected, reject the whole chain + else --if promise._status == Promise.Status.Rejected then + isResolved = true + reject(unpack(promise._value)) + end + end + end) +end + +--[[ + Is the given object a Promise instance? +]] +function Promise.is(object) + if type(object) ~= "table" then + return false + end + + return object._type == "Promise" +end + +--[[ + Creates a new promise that receives the result of this promise. + + The given callbacks are invoked depending on that result. +]] +function Promise:andThen(successHandler, failureHandler) + -- Create a new promise to follow this part of the chain + return Promise.new(function(resolve, reject) + -- Our default callbacks just pass values onto the next promise. + -- This lets success and failure cascade correctly! + + local successCallback = resolve + if successHandler then + successCallback = createAdvancer(successHandler, resolve, reject) + end + + local failureCallback = reject + if failureHandler then + failureCallback = createAdvancer(failureHandler, resolve, reject) + end + + if self._status == Promise.Status.Started then + -- If we haven't resolved yet, put ourselves into the queue + table.insert(self._queuedResolve, successCallback) + table.insert(self._queuedReject, failureCallback) + elseif self._status == Promise.Status.Resolved then + -- This promise has already resolved! Trigger success immediately. + successCallback(unpack(self._value)) + elseif self._status == Promise.Status.Rejected then + -- This promise died a terrible death! Trigger failure immediately. + failureCallback(unpack(self._value)) + end + end) +end + +--[[ + Used to catch any errors that may have occurred in the promise. +]] +function Promise:catch(failureCallback) + return self:andThen(nil, failureCallback) +end + +--[[ + Yield until the promise is completed. + + This matches the execution model of normal Roblox functions. +]] +function Promise:await() + if self._status == Promise.Status.Started then + local result + local bindable = Instance.new("BindableEvent") + + self:andThen(function(...) + result = {...} + bindable:Fire(true) + end, function(...) + result = {...} + bindable:Fire(false) + end) + + local ok = bindable.Event:Wait() + bindable:Destroy() + + if not ok then + error(tostring(result[1]), 2) + end + + return unpack(result) + elseif self._status == Promise.Status.Resolved then + return unpack(self._value) + elseif self._status == Promise.Status.Rejected then + error(tostring(self._value[1]), 2) + end +end + +function Promise:_resolve(...) + if self._status ~= Promise.Status.Started then + return + end + + -- If the resolved value was a Promise, we chain onto it! + if Promise.is((...)) then + -- Without this warning, arguments sometimes mysteriously disappear + if select("#", ...) > 1 then + local message = ("When returning a Promise from andThen, extra arguments are discarded! See:\n\n%s"):format( + self._source + ) + warn(message) + end + + (...):andThen(function(...) + self:_resolve(...) + end, function(...) + self:_reject(...) + end) + + return + end + + self._status = Promise.Status.Resolved + self._value = {...} + + -- We assume that these callbacks will not throw errors. + for _, callback in ipairs(self._queuedResolve) do + callback(...) + end +end + +function Promise:_reject(...) + if self._status ~= Promise.Status.Started then + return + end + + self._status = Promise.Status.Rejected + self._value = {...} + + -- If there are any rejection handlers, call those! + if not isEmpty(self._queuedReject) then + -- We assume that these callbacks will not throw errors. + for _, callback in ipairs(self._queuedReject) do + callback(...) + end + else + -- At this point, no one was able to observe the error. + -- An error handler might still be attached if the error occurred + -- synchronously. We'll wait one tick, and if there are still no + -- observers, then we should put a message in the console. + + local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format( + TU.RecursiveToString((...)), + self._source + ) + warn(message) + end +end + +return Promise diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Promise.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Promise.spec.lua new file mode 100644 index 0000000..2bf19c1 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Promise.spec.lua @@ -0,0 +1,331 @@ +return function() + local Promise = require(script.Parent.Promise) + + describe("Promise.new", function() + it("should instantiate with a callback", function() + local promise = Promise.new(function() end) + + expect(promise).to.be.ok() + end) + + it("should invoke the given callback with resolve and reject", function() + local callCount = 0 + local resolveArg + local rejectArg + + local promise = Promise.new(function(resolve, reject) + callCount = callCount + 1 + resolveArg = resolve + rejectArg = reject + end) + + expect(promise).to.be.ok() + + expect(callCount).to.equal(1) + expect(resolveArg).to.be.a("function") + expect(rejectArg).to.be.a("function") + expect(promise._status).to.equal(Promise.Status.Started) + end) + + it("should resolve promises on resolve()", function() + local callCount = 0 + + local promise = Promise.new(function(resolve) + callCount = callCount + 1 + resolve() + end) + + expect(promise).to.be.ok() + expect(callCount).to.equal(1) + expect(promise._status).to.equal(Promise.Status.Resolved) + end) + + it("should reject promises on reject()", function() + local callCount = 0 + + local promise = Promise.new(function(resolve, reject) + callCount = callCount + 1 + reject() + end) + + expect(promise).to.be.ok() + expect(callCount).to.equal(1) + expect(promise._status).to.equal(Promise.Status.Rejected) + end) + + it("should reject on error in callback", function() + local callCount = 0 + + local promise = Promise.new(function() + callCount = callCount + 1 + error("hahah") + end) + + expect(promise).to.be.ok() + expect(callCount).to.equal(1) + expect(promise._status).to.equal(Promise.Status.Rejected) + expect(promise._value[1]:find("hahah")).to.be.ok() + end) + end) + + describe("Promise.resolve", function() + it("should immediately resolve with a value", function() + local promise = Promise.resolve(5) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Resolved) + expect(promise._value[1]).to.equal(5) + end) + + it("should chain onto passed promises", function() + local promise = Promise.resolve(Promise.new(function(_, reject) + reject(7) + end)) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Rejected) + expect(promise._value[1]).to.equal(7) + end) + end) + + describe("Promise.reject", function() + it("should immediately reject with a value", function() + local promise = Promise.reject(6) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Rejected) + expect(promise._value[1]).to.equal(6) + end) + + it("should pass a promise as-is as an error", function() + local innerPromise = Promise.new(function(resolve) + resolve(6) + end) + + local promise = Promise.reject(innerPromise) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Rejected) + expect(promise._value[1]).to.equal(innerPromise) + end) + end) + + describe("Promise:andThen", function() + it("should chain onto resolved promises", function() + local args + local argsLength + local callCount = 0 + local badCallCount = 0 + + local promise = Promise.resolve(5) + + local chained = promise + :andThen(function(...) + args = {...} + argsLength = select("#", ...) + callCount = callCount + 1 + end, function() + badCallCount = badCallCount + 1 + end) + + expect(badCallCount).to.equal(0) + + expect(callCount).to.equal(1) + expect(argsLength).to.equal(1) + expect(args[1]).to.equal(5) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Resolved) + expect(promise._value[1]).to.equal(5) + + expect(chained).to.be.ok() + expect(chained).never.to.equal(promise) + expect(chained._status).to.equal(Promise.Status.Resolved) + expect(#chained._value).to.equal(0) + end) + + it("should chain onto rejected promises", function() + local args + local argsLength + local callCount = 0 + local badCallCount = 0 + + local promise = Promise.reject(5) + + local chained = promise + :andThen(function(...) + badCallCount = badCallCount + 1 + end, function(...) + args = {...} + argsLength = select("#", ...) + callCount = callCount + 1 + end) + + expect(badCallCount).to.equal(0) + + expect(callCount).to.equal(1) + expect(argsLength).to.equal(1) + expect(args[1]).to.equal(5) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Rejected) + expect(promise._value[1]).to.equal(5) + + expect(chained).to.be.ok() + expect(chained).never.to.equal(promise) + expect(chained._status).to.equal(Promise.Status.Resolved) + expect(#chained._value).to.equal(0) + end) + + it("should chain onto asynchronously resolved promises", function() + local args + local argsLength + local callCount = 0 + local badCallCount = 0 + + local startResolution + local promise = Promise.new(function(resolve) + startResolution = resolve + end) + + local chained = promise + :andThen(function(...) + args = {...} + argsLength = select("#", ...) + callCount = callCount + 1 + end, function() + badCallCount = badCallCount + 1 + end) + + expect(callCount).to.equal(0) + expect(badCallCount).to.equal(0) + + startResolution(6) + + expect(badCallCount).to.equal(0) + + expect(callCount).to.equal(1) + expect(argsLength).to.equal(1) + expect(args[1]).to.equal(6) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Resolved) + expect(promise._value[1]).to.equal(6) + + expect(chained).to.be.ok() + expect(chained).never.to.equal(promise) + expect(chained._status).to.equal(Promise.Status.Resolved) + expect(#chained._value).to.equal(0) + end) + + it("should chain onto asynchronously rejected promises", function() + local args + local argsLength + local callCount = 0 + local badCallCount = 0 + + local startResolution + local promise = Promise.new(function(_, reject) + startResolution = reject + end) + + local chained = promise + :andThen(function() + badCallCount = badCallCount + 1 + end, function(...) + args = {...} + argsLength = select("#", ...) + callCount = callCount + 1 + end) + + expect(callCount).to.equal(0) + expect(badCallCount).to.equal(0) + + startResolution(6) + + expect(badCallCount).to.equal(0) + + expect(callCount).to.equal(1) + expect(argsLength).to.equal(1) + expect(args[1]).to.equal(6) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Rejected) + expect(promise._value[1]).to.equal(6) + + expect(chained).to.be.ok() + expect(chained).never.to.equal(promise) + expect(chained._status).to.equal(Promise.Status.Resolved) + expect(#chained._value).to.equal(0) + end) + end) + + describe("Promise.all", function() + it("should resolve immediately with an empty table argument", function() + local promise = Promise.all({}) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Resolved) + + local result = unpack(promise._value) + expect(type(result)).to.equal("table") + expect(#result).to.equal(0) + end) + + it("should resolve immediately with no arguments", function() + local promise = Promise.all() + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Resolved) + + local result = unpack(promise._value) + expect(type(result)).to.equal("table") + expect(#result).to.equal(0) + end) + + it("should resolve with one result with one resolved argument", function() + local promise = Promise.all(Promise.resolve(7)) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Resolved) + + local result = unpack(promise._value) + expect(type(result)).to.equal("table") + expect(#result).to.equal(1) + expect(result[1]).to.equal(7) + end) + + it("should resolve with multiple resolved arguments", function() + local promise = Promise.all(Promise.resolve(7), Promise.resolve(4)) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Resolved) + + local result = unpack(promise._value) + expect(type(result)).to.equal("table") + expect(#result).to.equal(2) + expect(result[1]).to.equal(7) + expect(result[2]).to.equal(4) + end) + + it("should reject with one rejected argument", function() + local promise = Promise.all(Promise.reject(5)) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Rejected) + + local result = unpack(promise._value) + expect(result).to.equal(5) + end) + + it("should reject with one success followed by one rejected argument", function() + local promise = Promise.all(Promise.resolve(7), Promise.reject(4)) + + expect(promise).to.be.ok() + expect(promise._status).to.equal(Promise.Status.Rejected) + + local result = unpack(promise._value) + expect(result).to.equal(4) + end) + end) +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Assets.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Assets.lua new file mode 100644 index 0000000..c9c055b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Assets.lua @@ -0,0 +1,64 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Immutable = require(Modules.Common.Immutable) + +local AccessoryNames = { + "Hat", + "Hair Accessory", + "Face Accessory", + "Neck Accessory", + "Shoulder Accessory", + "Front Accessory", + "Back Accessory", + "Waist Accessory" +} + +local EquipAsset = require(Modules.LuaApp.Actions.EquipAsset) +local UnequipAsset = require(Modules.LuaApp.Actions.UnequipAsset) +local SetAssets = require(Modules.LuaApp.Actions.SetAssets) +local SetOutfit = require(Modules.LuaApp.Actions.SetOutfit) + +return function(state, action) + state = state or {} + + if action.type == EquipAsset.name then + if action.assetType == "Hat" then + local hats = state.Hat or {} + state = Immutable.Set(state, "Hat", {action.assetId, hats[1], hats[2]}) + for _, accessoryName in pairs(AccessoryNames) do + if string.find(accessoryName, 'Accessory') and state[accessoryName] then + state = Immutable.Set(state, accessoryName, {state[accessoryName][1]}) + end + end + return state + elseif string.find(action.assetType, 'Accessory') then + state = Immutable.Set(state, action.assetType, {action.assetId}) + local hats = state.Hat or {} + state = Immutable.Set(state, "Hat", {hats[1], hats[2], hats[3]}) + for _, accessoryName in pairs(AccessoryNames) do + if string.find(accessoryName, 'Accessory') and state[accessoryName] then + state = Immutable.Set(state, accessoryName, {state[accessoryName][1]}) + end + end + return state + end + return Immutable.Set(state, action.assetType, {action.assetId}) + elseif action.type == UnequipAsset.name then + local assets = state[action.assetType] or {} + return Immutable.Set(state, action.assetType, Immutable.RemoveValueFromList(assets, action.assetId)) + elseif action.type == SetAssets.name or action.type == SetOutfit.name then + local result = {} + for assetType, assetList in pairs(action.assets) do + local assetIdTable = {} + for _, assetId in pairs(assetList) do + table.insert(assetIdTable, assetId) + end + if next(assetIdTable) ~= nil then + result = Immutable.Set(result, assetType, assetIdTable) + end + end + return result + end + + return state +end + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Assets.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Assets.spec.lua new file mode 100644 index 0000000..f5cfe01 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Assets.spec.lua @@ -0,0 +1,187 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Assets = require(Modules.LuaApp.Reducers.AvatarEditor.Assets) + local EquipAsset = require(Modules.LuaApp.Actions.EquipAsset) + local UnequipAsset = require(Modules.LuaApp.Actions.UnequipAsset) + local SetAssets = require(Modules.LuaApp.Actions.SetAssets) + local SetOutfit = require(Modules.LuaApp.Actions.SetOutfit) + + it("should be an empty table by default", function() + local state = Assets(nil, {}) + + expect(state).to.be.a("table") + expect(next(state)).never.to.be.ok() + end) + + it("should equip the asset using EquipAsset", function() + local state = Assets(nil, EquipAsset("Hat", 1)) + + expect(state).to.be.a("table") + expect(state["Hat"]).to.be.a("table") + expect(state["Hat"][1]).to.equal(1) + + state = Assets(state, EquipAsset("Hair Accessory", 2)) + + expect(state["Hair Accessory"]).to.be.a("table") + expect(state["Hair Accessory"][1]).to.equal(2) + end) + + it("should unequip the asset using UnequipAsset", function() + local state = Assets(nil, EquipAsset("Hat", 1)) + state = Assets(state, UnequipAsset("Hat", 1)) + + expect(state).to.be.a("table") + expect(state["Hat"]).to.be.a("table") + expect(next(state["Hat"])).never.to.be.ok() + end) + + it("should equip the assets using SetAssets", function() + local state = Assets(nil, SetAssets({ + ["Hat"] = {1, 2, 3}, + ["Face Accessory"] = {4}, + ["Neck Accessory"] = {7}, + })) + + expect(state).to.be.a("table") + expect(state["Hat"]).to.be.a("table") + expect(state["Hat"][1]).to.equal(1) + expect(state["Hat"][2]).to.equal(2) + expect(state["Hat"][3]).to.equal(3) + + expect(state["Face Accessory"]).to.be.a("table") + expect(state["Face Accessory"][1]).to.equal(4) + + expect(state["Neck Accessory"]).to.be.a("table") + expect(state["Neck Accessory"][1]).to.equal(7) + end) + + it("should replace the old assets with new assets when SetAssets", function() + local state = Assets(nil, SetAssets({ + ["Hat"] = {1}, + ["Shirt"] = {2}, + })) + + expect(state).to.be.a("table") + expect(state["Hat"]).to.be.a("table") + expect(state["Hat"][1]).to.equal(1) + + expect(state["Shirt"]).to.be.a("table") + expect(state["Shirt"][1]).to.equal(2) + + state = Assets(state, SetAssets({ + ["Shirt"] = {3}, + ["Pants"] = {4}, + })) + + expect(state).to.be.a("table") + expect(state["Hat"]).never.to.be.ok() + + expect(state["Shirt"]).to.be.a("table") + expect(state["Shirt"][1]).to.equal(3) + + expect(state["Pants"]).to.be.a("table") + expect(state["Pants"][1]).to.equal(4) + end) + + it("should equip the assets using SetOutfit", function() + local state = Assets(nil, SetOutfit({ + ["Hat"] = {1, 2, 3}, + ["Face Accessory"] = {4}, + ["Neck Accessory"] = {7}, + })) + + expect(state).to.be.a("table") + expect(state["Hat"]).to.be.a("table") + expect(state["Hat"][1]).to.equal(1) + expect(state["Hat"][2]).to.equal(2) + expect(state["Hat"][3]).to.equal(3) + + expect(state["Face Accessory"]).to.be.a("table") + expect(state["Face Accessory"][1]).to.equal(4) + + expect(state["Neck Accessory"]).to.be.a("table") + expect(state["Neck Accessory"][1]).to.equal(7) + end) + + it("should equip advanced assets when SetOutfit", function() + local state = Assets(nil, SetOutfit({ + ["Hat"] = {1, 2, 3, 4, 5}, + ["Face Accessory"] = {6, 7}, + ["Neck Accessory"] = {8, 9}, + ["T-Shirt"] = {10}, + })) + + expect(state).to.be.a("table") + expect(state["Hat"]).to.be.a("table") + expect(state["Hat"][1]).to.equal(1) + expect(state["Hat"][2]).to.equal(2) + expect(state["Hat"][3]).to.equal(3) + expect(state["Hat"][4]).to.equal(4) + expect(state["Hat"][5]).to.equal(5) + + expect(state["Face Accessory"]).to.be.a("table") + expect(state["Face Accessory"][1]).to.equal(6) + expect(state["Face Accessory"][2]).to.equal(7) + + expect(state["Neck Accessory"]).to.be.a("table") + expect(state["Neck Accessory"][1]).to.equal(8) + expect(state["Neck Accessory"][2]).to.equal(9) + + expect(state["T-Shirt"]).to.be.a("table") + expect(state["T-Shirt"][1]).to.equal(10) + end) + + it("should replace the old assets with new assets when SetOutfit", function() + local state = Assets(nil, SetOutfit({ + ["Hat"] = {1, 2}, + ["Shirt"] = {3}, + })) + + expect(state).to.be.a("table") + expect(state["Hat"]).to.be.a("table") + expect(state["Hat"][1]).to.equal(1) + expect(state["Hat"][2]).to.equal(2) + + expect(state["Shirt"]).to.be.a("table") + expect(state["Shirt"][1]).to.equal(3) + + state = Assets(state, SetOutfit({ + ["Shirt"] = {4}, + ["Pants"] = {5}, + })) + + expect(state).to.be.a("table") + expect(state["Hat"]).never.to.be.ok() + + expect(state["Shirt"]).to.be.a("table") + expect(state["Shirt"][1]).to.equal(4) + + expect(state["Pants"]).to.be.a("table") + expect(state["Pants"][1]).to.equal(5) + end) + + it("should be unchanged by other actions", function() + local state = Assets(nil, {}) + + state = Assets(state, { + type = "Do the thing!", + assetType = "Hat", + assetId = 1, + }) + + expect(state).to.be.a("table") + expect(next(state)).never.to.be.ok() + + state = Assets(state, { + type = "Do the thing!", + assets = { + ["Hat"] = {1, 2, 3}, + ["Face Accessory"] = {4}, + ["Neck Accessory"] = {7}, + } + }) + + expect(state).to.be.a("table") + expect(next(state)).never.to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Avatar.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Avatar.lua new file mode 100644 index 0000000..95b8235 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Avatar.lua @@ -0,0 +1,17 @@ +local Reducers = script.Parent + +local FullView = require(Reducers.FullView) +local Character = require(Reducers.Character) +local Category = require(Reducers.Category) +local ConsoleMenuLevel = require(Reducers.ConsoleMenuLevel) + +return function(state, action) + state = state or {} + + return { + FullView = FullView(state.FullView, action); + Character = Character(state.Character, action); + Category = Category(state.Category, action); + ConsoleMenuLevel = ConsoleMenuLevel(state.ConsoleMenuLevel, action); + } +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/AvatarType.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/AvatarType.lua new file mode 100644 index 0000000..f5431db --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/AvatarType.lua @@ -0,0 +1,13 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local SetAvatarType = require(Modules.LuaApp.Actions.SetAvatarType) +local ToggleAvatarType = require(Modules.LuaApp.Actions.ToggleAvatarType) + +return function(state, action) + if action.type == SetAvatarType.name then + return action.avatarType + elseif action.type == ToggleAvatarType.name then + return state == "R6" and "R15" or "R6" + end + + return state +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/AvatarType.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/AvatarType.spec.lua new file mode 100644 index 0000000..c1991e8 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/AvatarType.spec.lua @@ -0,0 +1,40 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local AvatarType = require(Modules.LuaApp.Reducers.AvatarEditor.AvatarType) + local SetAvatarType = require(Modules.LuaApp.Actions.SetAvatarType) + local ToggleAvatarType = require(Modules.LuaApp.Actions.ToggleAvatarType) + + it("should be nil by default", function() + local state = AvatarType(nil, {}) + + expect(state).never.to.be.ok() + end) + + it("should be changed using SetAvatarType", function() + local state = AvatarType(nil, SetAvatarType("R6")) + + expect(state).to.equal("R6") + end) + + it("should be flipped using ToggleAvatarType", function() + local state = AvatarType(nil, SetAvatarType("R6")) + state = AvatarType(state, ToggleAvatarType()) + + expect(state).to.equal("R15") + + state = AvatarType(state, ToggleAvatarType()) + + expect(state).to.equal("R6") + end) + + it("should be unchanged by other actions", function() + local state = AvatarType(nil, {}) + + state = AvatarType(state, { + type = "Do the thing!", + avatarType = "R15" + }) + + expect(state).never.to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/BodyColors.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/BodyColors.lua new file mode 100644 index 0000000..c06e9ec --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/BodyColors.lua @@ -0,0 +1,24 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Immutable = require(Modules.Common.Immutable) +local SetBodyColors = require(Modules.LuaApp.Actions.SetBodyColors) +local SetOutfit = require(Modules.LuaApp.Actions.SetOutfit) + +return function(state, action) + state = state or { + HeadColor = 194, + LeftArmColor = 194, + LeftLegColor = 194, + RightArmColor = 194, + RightLegColor = 194, + TorsoColor = 194, + } + + if action.type == SetBodyColors.name or action.type == SetOutfit.name then + for key, value in pairs(action.bodyColors) do + state = Immutable.Set(state, key, value) + end + end + + return state +end + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/BodyColors.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/BodyColors.spec.lua new file mode 100644 index 0000000..5f47cc2 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/BodyColors.spec.lua @@ -0,0 +1,85 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local BodyColors = require(Modules.LuaApp.Reducers.AvatarEditor.BodyColors) + local SetBodyColors = require(Modules.LuaApp.Actions.SetBodyColors) + local SetOutfit = require(Modules.LuaApp.Actions.SetOutfit) + + it("all colors should be 194 by default", function() + local state = BodyColors(nil, {}) + + expect(state).to.be.a("table") + expect(state.HeadColor).to.equal(194) + expect(state.LeftArmColor).to.equal(194) + expect(state.LeftLegColor).to.equal(194) + expect(state.RightArmColor).to.equal(194) + expect(state.RightLegColor).to.equal(194) + expect(state.TorsoColor).to.equal(194) + end) + + it("should be updated using SetBodyColors", function() + local state = BodyColors(nil, {}) + + state = BodyColors(state, SetBodyColors({ + HeadColor = 100, + LeftArmColor = 120, + LeftLegColor = 160, + RightArmColor = 140, + RightLegColor = 180, + TorsoColor = 100 + })) + + expect(state).to.be.a("table") + expect(state.HeadColor).to.equal(100) + expect(state.LeftArmColor).to.equal(120) + expect(state.LeftLegColor).to.equal(160) + expect(state.RightArmColor).to.equal(140) + expect(state.RightLegColor).to.equal(180) + expect(state.TorsoColor).to.equal(100) + end) + + it("should be updated using SetOutfit", function() + local state = BodyColors(nil, {}) + + state = BodyColors(state, SetOutfit({}, { + HeadColor = 100, + LeftArmColor = 120, + LeftLegColor = 160, + RightArmColor = 140, + RightLegColor = 180, + TorsoColor = 100 + })) + + expect(state).to.be.a("table") + expect(state.HeadColor).to.equal(100) + expect(state.LeftArmColor).to.equal(120) + expect(state.LeftLegColor).to.equal(160) + expect(state.RightArmColor).to.equal(140) + expect(state.RightLegColor).to.equal(180) + expect(state.TorsoColor).to.equal(100) + end) + + it("should be unchanged by other actions", function() + local state = BodyColors(nil, {}) + + state = BodyColors(state, { + type = "Do the thing!", + bodyColors = + { + HeadColor = 100, + LeftArmColor = 120, + LeftLegColor = 160, + RightArmColor = 140, + RightLegColor = 180, + TorsoColor = 100 + } + }) + + expect(state).to.be.a("table") + expect(state.HeadColor).to.equal(194) + expect(state.LeftArmColor).to.equal(194) + expect(state.LeftLegColor).to.equal(194) + expect(state.RightArmColor).to.equal(194) + expect(state.RightLegColor).to.equal(194) + expect(state.TorsoColor).to.equal(194) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Category.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Category.lua new file mode 100644 index 0000000..7c6b07d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Category.lua @@ -0,0 +1,12 @@ +local Reducers = script.Parent +local CategoryIndex = require(Reducers.CategoryIndex) +local TabsInfo = require(Reducers.TabsInfo) + +return function(state, action) + state = state or {} + + return { + CategoryIndex = CategoryIndex(state.CategoryIndex, action); + TabsInfo = TabsInfo(state.TabsInfo, action); + } +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/CategoryIndex.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/CategoryIndex.lua new file mode 100644 index 0000000..49c377e --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/CategoryIndex.lua @@ -0,0 +1,13 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local SelectCategory = require(Modules.LuaApp.Actions.SelectCategory) +local ResetCategory = require(Modules.LuaApp.Actions.ResetCategory) + +return function(state, action) + if action.type == SelectCategory.name then + return action.categoryIndex + elseif action.type == ResetCategory.name then + return nil + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/CategoryIndex.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/CategoryIndex.spec.lua new file mode 100644 index 0000000..7588090 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/CategoryIndex.spec.lua @@ -0,0 +1,34 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local CategoryIndex = require(Modules.LuaApp.Reducers.AvatarEditor.CategoryIndex) + local SelectCategory = require(Modules.LuaApp.Actions.SelectCategory) + local ResetCategory = require(Modules.LuaApp.Actions.ResetCategory) + + it("should be nil by default", function() + local state = CategoryIndex(nil, {}) + + expect(state).never.to.be.ok() + end) + + it("should be changed using SelectCategory", function() + local state = CategoryIndex(nil, SelectCategory(2)) + + expect(state).to.equal(2) + end) + + it("should be reset using ResetCategory", function() + local state = CategoryIndex(nil, SelectCategory(2)) + state = CategoryIndex(state, ResetCategory()) + + expect(state).never.to.be.ok() + end) + + it("should be unchanged by other actions", function() + local state = CategoryIndex(nil, { + type = "Do the thing!", + categoryIndex = 1 + }) + + expect(state).never.to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Character.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Character.lua new file mode 100644 index 0000000..2aabf36 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Character.lua @@ -0,0 +1,16 @@ +local Reducers = script.Parent +local AvatarType = require(Reducers.AvatarType) +local Assets = require(Reducers.Assets) +local BodyColors = require(Reducers.BodyColors) +local Scales = require(Reducers.Scales) + +return function(state, action) + state = state or {} + + return { + AvatarType = AvatarType(state.AvatarType, action); + Assets = Assets(state.Assets, action); + BodyColors = BodyColors(state.BodyColors, action); + Scales = Scales(state.Scales, action); + } +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/ConsoleMenuLevel.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/ConsoleMenuLevel.lua new file mode 100644 index 0000000..d5884b5 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/ConsoleMenuLevel.lua @@ -0,0 +1,12 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local SetConsoleMenuLevel = require(Modules.LuaApp.Actions.SetConsoleMenuLevel) + +return function(state, action) + state = state or 0 + + if action.type == SetConsoleMenuLevel.name then + return action.menuLevel + end + + return state +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/ConsoleMenuLevel.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/ConsoleMenuLevel.spec.lua new file mode 100644 index 0000000..ed88a74 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/ConsoleMenuLevel.spec.lua @@ -0,0 +1,30 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local ConsoleMenuLevel = require(Modules.LuaApp.Reducers.AvatarEditor.ConsoleMenuLevel) + local SetConsoleMenuLevel = require(Modules.LuaApp.Actions.SetConsoleMenuLevel) + + it("should be 0 by default", function() + local state = ConsoleMenuLevel(nil, {}) + + expect(state).to.equal(0) + end) + + it("should be changed using SetConsoleMenuLevel", function() + local state = ConsoleMenuLevel(nil, {}) + + state = ConsoleMenuLevel(state, SetConsoleMenuLevel(2)) + + expect(state).to.equal(2) + end) + + it("should be unchanged by other actions", function() + local state = ConsoleMenuLevel(nil, {}) + + state = ConsoleMenuLevel(state, { + type = "Do the thing!", + menuLevel = 1 + }) + + expect(state).to.equal(0) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/FullView.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/FullView.lua new file mode 100644 index 0000000..6037996 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/FullView.lua @@ -0,0 +1,17 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local ToggleAvatarEditorFullView = require(Modules.LuaApp.Actions.ToggleAvatarEditorFullView) +local SetAvatarEditorFullView = require(Modules.LuaApp.Actions.SetAvatarEditorFullView) + +return function(state, action) + state = state or false + + if action.type == ToggleAvatarEditorFullView.name then + return not state + end + + if action.type == SetAvatarEditorFullView.name then + return action.fullView + end + + return state +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/FullView.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/FullView.spec.lua new file mode 100644 index 0000000..c84b4f0 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/FullView.spec.lua @@ -0,0 +1,47 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local FullView = require(Modules.LuaApp.Reducers.AvatarEditor.FullView) + local ToggleAvatarEditorFullView = require(Modules.LuaApp.Actions.ToggleAvatarEditorFullView) + local SetAvatarEditorFullView = require(Modules.LuaApp.Actions.SetAvatarEditorFullView) + + it("should be false by default", function() + local state = FullView(nil, {}) + + expect(state).to.equal(false) + end) + + it("should be flipped using ToggleAvatarEditorFullView", function() + local state = FullView(nil, {}) + + state = FullView(state, ToggleAvatarEditorFullView()) + + expect(state).to.equal(true) + + state = FullView(state, ToggleAvatarEditorFullView()) + + expect(state).to.equal(false) + end) + + it("should be changed using SetAvatarEditorFullView", function() + local state = FullView(nil, {}) + + state = FullView(state, SetAvatarEditorFullView(true)) + + expect(state).to.equal(true) + + state = FullView(state, SetAvatarEditorFullView(false)) + + expect(state).to.equal(false) + end) + + it("should be unchanged by other actions", function() + local state = FullView(nil, {}) + + state = FullView(state, { + type = "Do the thing!", + fullView = true + }) + + expect(state).to.equal(false) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Scales.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Scales.lua new file mode 100644 index 0000000..b5cb1de --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Scales.lua @@ -0,0 +1,39 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Immutable = require(Modules.Common.Immutable) +local SetScales = require(Modules.LuaApp.Actions.SetScales) +local SetAvatarHeight = require(Modules.LuaApp.Actions.SetAvatarHeight) +local SetAvatarWidth = require(Modules.LuaApp.Actions.SetAvatarWidth) +local SetAvatarHeadSize = require(Modules.LuaApp.Actions.SetAvatarHeadSize) +local SetAvatarBodyType = require(Modules.LuaApp.Actions.SetAvatarBodyType) +local SetAvatarProportion = require(Modules.LuaApp.Actions.SetAvatarProportion) + +return function(state, action) + state = state or { + Height = 1.00, + Width = 1.00, + Depth = 1.00, + Head = 1.00, + BodyType = 0.00, + Proportion = 0.00, + } + + if action.type == SetScales.name then + for key, value in pairs(action.scales) do + state = Immutable.Set(state, key, value) + end + elseif action.type == SetAvatarHeight.name then + return Immutable.Set(state, "Height", action.height) + elseif action.type == SetAvatarWidth.name then + state = Immutable.Set(state, "Width", action.width) + return Immutable.Set(state, "Depth", action.depth) + elseif action.type == SetAvatarHeadSize.name then + return Immutable.Set(state, "Head", action.head) + elseif action.type == SetAvatarBodyType.name then + return Immutable.Set(state, "BodyType", action.bodyType) + elseif action.type == SetAvatarProportion.name then + return Immutable.Set(state, "Proportion", action.proportion) + end + + return state +end + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Scales.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Scales.spec.lua new file mode 100644 index 0000000..dbf4cdb --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/Scales.spec.lua @@ -0,0 +1,138 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Scales = require(Modules.LuaApp.Reducers.AvatarEditor.Scales) + local SetScales = require(Modules.LuaApp.Actions.SetScales) + local SetAvatarHeight = require(Modules.LuaApp.Actions.SetAvatarHeight) + local SetAvatarWidth = require(Modules.LuaApp.Actions.SetAvatarWidth) + local SetAvatarHeadSize = require(Modules.LuaApp.Actions.SetAvatarHeadSize) + local SetAvatarBodyType = require(Modules.LuaApp.Actions.SetAvatarBodyType) + local SetAvatarProportion = require(Modules.LuaApp.Actions.SetAvatarProportion) + + it("all scales should be using default value", function() + local state = Scales(nil, {}) + + expect(state).to.be.a("table") + expect(state.Height).to.equal(1.00) + expect(state.Width).to.equal(1.00) + expect(state.Depth).to.equal(1.00) + expect(state.Head).to.equal(1.00) + expect(state.BodyType).to.equal(0.00) + expect(state.Proportion).to.equal(0.00) + end) + + it("should be updated using SetScales", function() + local state = Scales(nil, {}) + + state = Scales(state, SetScales({ + Height = 2.00, + Width = 1.50, + Depth = 1.00, + Head = 0.50, + BodyType = 0.30, + Proportion = 0.70 + })) + + expect(state).to.be.a("table") + expect(state.Height).to.equal(2.00) + expect(state.Width).to.equal(1.50) + expect(state.Depth).to.equal(1.00) + expect(state.Head).to.equal(0.50) + expect(state.BodyType).to.equal(0.30) + expect(state.Proportion).to.equal(0.70) + end) + + it("only height should be updated using SetAvatarHeight", function() + local state = Scales(nil, {}) + + state = Scales(state, SetAvatarHeight(1.20)) + + expect(state).to.be.a("table") + expect(state.Height).to.equal(1.20) + expect(state.Width).to.equal(1.00) + expect(state.Depth).to.equal(1.00) + expect(state.Head).to.equal(1.00) + expect(state.BodyType).to.equal(0.00) + expect(state.Proportion).to.equal(0.00) + end) + + it("only headsize should be updated using SetAvatarHeadSize", function() + local state = Scales(nil, {}) + + state = Scales(state, SetAvatarHeadSize(0.80)) + + expect(state).to.be.a("table") + expect(state.Height).to.equal(1.00) + expect(state.Width).to.equal(1.00) + expect(state.Depth).to.equal(1.00) + expect(state.Head).to.equal(0.80) + expect(state.BodyType).to.equal(0.00) + expect(state.Proportion).to.equal(0.00) + end) + + it("only width and depth should be updated using SetAvatarWidth", function() + local state = Scales(nil, {}) + + state = Scales(state, SetAvatarWidth(0.75, 1.05)) + + expect(state).to.be.a("table") + expect(state.Height).to.equal(1.00) + expect(state.Width).to.equal(0.75) + expect(state.Depth).to.equal(1.05) + expect(state.Head).to.equal(1.00) + expect(state.BodyType).to.equal(0.00) + expect(state.Proportion).to.equal(0.00) + end) + + it("only bodyType should be updated using SetAvatarBodyType", function() + local state = Scales(nil, {}) + + state = Scales(state, SetAvatarBodyType(0.30)) + + expect(state).to.be.a("table") + expect(state.Height).to.equal(1.00) + expect(state.Width).to.equal(1.00) + expect(state.Depth).to.equal(1.00) + expect(state.Head).to.equal(1.00) + expect(state.BodyType).to.equal(0.30) + expect(state.Proportion).to.equal(0.00) + end) + + it("only proportion should be updated using SetAvatarProportion", function() + local state = Scales(nil, {}) + + state = Scales(state, SetAvatarProportion(0.70)) + + expect(state).to.be.a("table") + expect(state.Height).to.equal(1.00) + expect(state.Width).to.equal(1.00) + expect(state.Depth).to.equal(1.00) + expect(state.Head).to.equal(1.00) + expect(state.BodyType).to.equal(0.00) + expect(state.Proportion).to.equal(0.70) + end) + + it("should be unchanged by other actions", function() + local state = Scales(nil, {}) + + state = Scales(state, { + type = "Do the thing!", + scales = + { + Height = 2.00, + Width = 1.50, + Depth = 1.25, + Head = 0.50, + BodyType = 0.30, + Proportion = 0.70 + } + }) + + expect(state).to.be.a("table") + expect(state.Height).to.equal(1.00) + expect(state.Width).to.equal(1.00) + expect(state.Depth).to.equal(1.00) + expect(state.Head).to.equal(1.00) + expect(state.BodyType).to.equal(0.00) + expect(state.Proportion).to.equal(0.00) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/TabsInfo.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/TabsInfo.lua new file mode 100644 index 0000000..183004c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/TabsInfo.lua @@ -0,0 +1,21 @@ +local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules +local Immutable = require(Modules.Common.Immutable) +local SelectCategoryTab = require(Modules.LuaApp.Actions.SelectCategoryTab) +local ResetCategory = require(Modules.LuaApp.Actions.ResetCategory) + +return function(state, action) + state = state or {} + + if action.type == SelectCategoryTab.name then + local categoryIndex = action.categoryIndex + local tabInfo = { + TabIndex = action.tabIndex; + Position = action.position; + } + return Immutable.Set(state, categoryIndex, tabInfo) + elseif action.type == ResetCategory.name then + return {} + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/TabsInfo.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/TabsInfo.spec.lua new file mode 100644 index 0000000..8af171d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/AvatarEditor/TabsInfo.spec.lua @@ -0,0 +1,42 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local TabsInfo = require(Modules.LuaApp.Reducers.AvatarEditor.TabsInfo) + local SelectCategoryTab = require(Modules.LuaApp.Actions.SelectCategoryTab) + local ResetCategory = require(Modules.LuaApp.Actions.ResetCategory) + + it("should be an empty table by default", function() + local state = TabsInfo(nil, {}) + + expect(state).to.be.a("table") + expect(next(state)).never.to.be.ok() + end) + + it("should be updated using SelectCategoryTab", function() + local state = TabsInfo(nil, SelectCategoryTab(3, 2, Vector2.new(0, 100))) + + expect(state).to.be.a("table") + expect(state[3]).to.be.a("table") + expect(state[3].TabIndex).to.equal(2) + expect(state[3].Position).to.equal(Vector2.new(0, 100)) + end) + + it("should be reset using ResetCategory", function() + local state = TabsInfo(nil, SelectCategoryTab(3, 2, Vector2.new(0, 100))) + state = TabsInfo(state, ResetCategory()) + + expect(state).to.be.a("table") + expect(next(state)).never.to.be.ok() + end) + + it("should be unchanged by other actions", function() + local state = TabsInfo(nil, { + type = "Do the thing!", + categoryIndex = 3, + tabIndex = 2, + position = Vector2.new(0, 100) + }) + + expect(state).to.be.a("table") + expect(next(state)).never.to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/DeviceOrientation.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/DeviceOrientation.lua new file mode 100644 index 0000000..3f4bb9c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/DeviceOrientation.lua @@ -0,0 +1,12 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local DeviceOrientationMode = require(Modules.LuaApp.DeviceOrientationMode) +local SetDeviceOrientation = require(Modules.LuaApp.Actions.SetDeviceOrientation) + +return function(state, action) + state = state or DeviceOrientationMode.Portrait + if action.type == SetDeviceOrientation.name then + return action.deviceOrientation + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/DeviceOrientation.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/DeviceOrientation.spec.lua new file mode 100644 index 0000000..a288501 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/DeviceOrientation.spec.lua @@ -0,0 +1,34 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetDeviceOrientation = require(Modules.LuaApp.Actions.SetDeviceOrientation) + local DeviceOrientationMode = require(Modules.LuaApp.DeviceOrientationMode) + + local DeviceOrientation = require(Modules.LuaApp.Reducers.DeviceOrientation) + + describe("DeviceOrientation", function() + it("should be portrait by default", function() + local state = DeviceOrientation(nil, {}) + + expect(state).to.equal(DeviceOrientationMode.Portrait) + end) + + it("should be unmodified by other actions", function() + local oldState = DeviceOrientation(nil, {}) + local newState = DeviceOrientation(oldState, { type = "not a real action" }) + + expect(oldState).to.equal(newState) + end) + + it("should be changed using SetDeviceOrientation", function() + local state = DeviceOrientation(nil, {}) + + state = DeviceOrientation(state, SetDeviceOrientation(DeviceOrientationMode.Landscape)) + expect(state).to.equal(DeviceOrientationMode.Landscape) + + state = DeviceOrientation(state, SetDeviceOrientation(DeviceOrientationMode.Portrait)) + expect(state).to.equal(DeviceOrientationMode.Portrait) + end) + end) + + +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/FormFactor.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/FormFactor.lua new file mode 100644 index 0000000..76cde66 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/FormFactor.lua @@ -0,0 +1,13 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local FormFactor = require(Modules.LuaApp.Enum.FormFactor) +local SetFormFactor = require(Modules.LuaApp.Actions.SetFormFactor) + +return function(state, action) + state = state or FormFactor.UNKNOWN + + if action.type == SetFormFactor.name then + return action.formFactor + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/FormFactor.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/FormFactor.spec.lua new file mode 100644 index 0000000..840da9f --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/FormFactor.spec.lua @@ -0,0 +1,31 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetFormFactor = require(Modules.LuaApp.Actions.SetFormFactor) + + local FormFactorReducer = require(Modules.LuaApp.Reducers.FormFactor) + local FormFactorEnum = require(Modules.LuaApp.Enum.FormFactor) + describe("FormFactor", function() + it("should be unknown by default", function() + local state = FormFactorReducer(nil, {}) + + expect(state).to.equal(FormFactorEnum.UNKNOWN) + end) + + it("should be unmodified by other actions", function() + local oldState = FormFactorReducer(nil, {}) + local newState = FormFactorReducer(oldState, { type = "not a real action" }) + + expect(oldState).to.equal(newState) + end) + + it("should be changed using SetFormFactor", function() + local state = FormFactorReducer(nil, {}) + + state = FormFactorReducer(state, SetFormFactor(FormFactorEnum.PHONE)) + expect(state).to.equal(FormFactorEnum.PHONE) + + state = FormFactorReducer(state, SetFormFactor(FormFactorEnum.TABLET)) + expect(state).to.equal(FormFactorEnum.TABLET) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortGroups.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortGroups.lua new file mode 100644 index 0000000..477d154 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortGroups.lua @@ -0,0 +1,23 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) +local GameSortGroup = require(Modules.LuaApp.Models.GameSortGroup) +local SetGameSortsInGroup = require(Modules.LuaApp.Actions.SetGameSortsInGroup) + +local defaultState = { + Games = GameSortGroup.new(), + GamesSeeAll = GameSortGroup.new(), + HomeGames = GameSortGroup.new() +} + +return function(state, action) + state = state or defaultState + + if action.type == SetGameSortsInGroup.name then + if state[action.groupId] then + local group = Immutable.Set(state[action.groupId], "sorts", action.sorts) + state = Immutable.Set(state, action.groupId, group) + end + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortGroups.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortGroups.spec.lua new file mode 100644 index 0000000..b58170a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortGroups.spec.lua @@ -0,0 +1,63 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local GameSortGroups = require(script.Parent.GameSortGroups) + local SetGameSortsInGroup = require(Modules.LuaApp.Actions.SetGameSortsInGroup) + + local function countChildObjects(aTable) + local numChildren = 0 + for _ in pairs(aTable) do + numChildren = numChildren + 1 + end + + return numChildren + end + + it("should have a few expected fields by default", function() + local defaultState = GameSortGroups(nil, {}) + + expect(defaultState).to.be.ok() + expect(defaultState.Games).to.be.ok() + expect(defaultState.GamesSeeAll).to.be.ok() + expect(defaultState.HomeGames).to.be.ok() + end) + + it("should be unmodified by other actions", function() + local oldState = GameSortGroups(nil, {}) + local newState = GameSortGroups(oldState, { type = "not a real action" }) + + expect(oldState).to.equal(newState) + end) + + describe("SetGameSortsInGroup", function() + it("should preserve purity", function() + local oldState = GameSortGroups(nil, {}) + local newState = GameSortGroups(oldState, SetGameSortsInGroup("Games", {})) + expect(oldState).to.never.equal(newState) + end) + + it("should set the game sorts in group", function() + local expectedModifiedGroup = "Games" + local testGameSorts = {"testSortToken1", "testSortToken2", "testSortToken3", "testSortToken4"} + local expectedAddedSorts = countChildObjects(testGameSorts) + + local defaultState = GameSortGroups(nil, {}) + + -- check that there are no sorts in the expected group to begin with + local totalSorts = countChildObjects(defaultState[expectedModifiedGroup].sorts) + expect(totalSorts).to.equal(0) + + -- modify the store + local action = SetGameSortsInGroup(expectedModifiedGroup, testGameSorts) + local modifiedState = GameSortGroups(defaultState, action) + + -- check that there are now sorts + totalSorts = countChildObjects(modifiedState[expectedModifiedGroup].sorts) + expect(totalSorts).to.equal(expectedAddedSorts) + + -- check that the sorts in testGameSorts have been added to the store + for index, sortId in pairs(modifiedState[expectedModifiedGroup].sorts) do + expect(sortId).to.equal(testGameSorts[index]) + end + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortTokenFetchingStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortTokenFetchingStatus.lua new file mode 100644 index 0000000..e47680c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortTokenFetchingStatus.lua @@ -0,0 +1,19 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) +local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) +local SetGameSortTokenFetchingStatus = require(Modules.LuaApp.Actions.SetGameSortTokenFetchingStatus) + +return function(state, action) + state = state or { + Games = RetrievalStatus.NotStarted, + HomeGames = RetrievalStatus.NotStarted, + + -- Not in-use now + GamesSeeAll = RetrievalStatus.NotStarted, + } + + if action.type == SetGameSortTokenFetchingStatus.name then + state = Immutable.Set(state, action.sortCategory, action.fetchStatus) + end + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortTokenFetchingStatus.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortTokenFetchingStatus.spec.lua new file mode 100644 index 0000000..07e302b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortTokenFetchingStatus.spec.lua @@ -0,0 +1,27 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local GameSortTokenFetchingStatus = require(script.parent.GameSortTokenFetchingStatus) + local SetGameSortTokenFetchingStatus = require(Modules.LuaApp.Actions.SetGameSortTokenFetchingStatus) + + it("Should not be mutated by other actions", function() + local oldState = GameSortTokenFetchingStatus(nil, {}) + local newState = GameSortTokenFetchingStatus(oldState, { type = "not a real action" }) + expect(oldState).to.equal(newState) + end) + + describe("SetGameSortTokenFetchingStatus", function() + it("should preserve purity", function() + local oldState = GameSortTokenFetchingStatus(nil, {}) + local newState = GameSortTokenFetchingStatus(oldState, SetGameSortTokenFetchingStatus("Games", "Failed")) + expect(oldState).to.never.equal(newState) + end) + + it("should correctly set the state of token fetching of various sort groups", function() + local oldState = GameSortTokenFetchingStatus(nil, {}) + local newState = GameSortTokenFetchingStatus(oldState, SetGameSortTokenFetchingStatus("Games", "Done")) + expect(newState["Games"]).to.equal("Done") + local newState2 = GameSortTokenFetchingStatus(oldState, SetGameSortTokenFetchingStatus("HomeGames", "Failed")) + expect(newState2["HomeGames"]).to.equal("Failed") + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSorts.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSorts.lua new file mode 100644 index 0000000..14b37f7 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSorts.lua @@ -0,0 +1,17 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) +local AddGameSorts = require(Modules.LuaApp.Actions.AddGameSorts) + +return function(state, action) + state = state or {} + + if action.type == AddGameSorts.name then + local newGames = {} + for _, sortData in pairs(action.sorts) do + newGames[sortData.name] = sortData + end + state = Immutable.JoinDictionaries(state, newGames) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSorts.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSorts.spec.lua new file mode 100644 index 0000000..638d8bc --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSorts.spec.lua @@ -0,0 +1,69 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local GameSorts = require(script.Parent.GameSorts) + local GameSort = require(Modules.LuaApp.Models.GameSort) + + local AddGameSorts = require(Modules.LuaApp.Actions.AddGameSorts) + + local function countChildObjects(aTable) + local numChildren = 0 + for _ in pairs(aTable) do + numChildren = numChildren + 1 + end + + return numChildren + end + local function createFakeGameSorts(numSorts) + local gameSorts = {} + for i = 1, numSorts do + local aSort = GameSort.mock() + aSort.token = tostring(i) + aSort.name = "A Test Sort" .. aSort.token + table.insert(gameSorts, aSort) + end + + return gameSorts + end + + it("should be empty by default", function() + local defaultState = GameSorts(nil, {}) + + expect(type(defaultState)).to.equal("table") + expect(countChildObjects(defaultState)).to.equal(0) + end) + + it("should be unchanged by other actions", function() + local oldState = GameSorts(nil, {}) + local newState = GameSorts(oldState, { type = "not a real action" }) + expect(oldState).to.equal(newState) + end) + + describe("AddGameSorts", function() + it("should preserve purity", function() + local oldState = GameSorts(nil, {}) + local newState = GameSorts(oldState, AddGameSorts(createFakeGameSorts(1))) + expect(newState).to.never.equal(oldState) + end) + + it("should add game sorts", function() + local expectedNumSorts = 5 + local someSorts = createFakeGameSorts(expectedNumSorts) + local action = AddGameSorts(someSorts) + + -- add some games to the game sorts store + local modifiedState = GameSorts(nil, action) + expect(countChildObjects(modifiedState)).to.equal(expectedNumSorts) + + -- check that the games have been added to the store + for _, sort in pairs(someSorts) do + local storedSort = modifiedState[sort.name] + expect(storedSort.token).to.equal(sort.token) + expect(storedSort.name).to.equal(sort.name) + expect(storedSort.timeOptionsAvailable).to.equal(sort.timeOptionsAvailable) + expect(storedSort.genreOptionsAvailable).to.equal(sort.genreOptionsAvailable) + expect(storedSort.numberOfRows).to.equal(sort.numberOfRows) + expect(storedSort.isDefaultSort).to.equal(sort.isDefaultSort) + end + end) + end) +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortsContents.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortsContents.lua new file mode 100644 index 0000000..e417627 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortsContents.lua @@ -0,0 +1,33 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) +local SetGameSortContents = require(Modules.LuaApp.Actions.SetGameSortContents) +local AddGameSortContents = require(Modules.LuaApp.Actions.AddGameSortContents) +local AddGameSorts = require(Modules.LuaApp.Actions.AddGameSorts) +local GameSortContents = require(Modules.LuaApp.Models.GameSortContents) + +return function(state, action) + state = state or {} + + if action.type == AddGameSorts.name then + local tmpTable = {} + for _, sortData in pairs(action.sorts) do + tmpTable[sortData.name] = state[sortData.name] or GameSortContents.new() + end + state = Immutable.JoinDictionaries(state, tmpTable) + + elseif action.type == SetGameSortContents.name then + -- store the universeIds associated with their sort + state = Immutable.Set(state, action.sort, action.gameSortContents) + + elseif action.type == AddGameSortContents.name then + local prevData = state[action.sort] + local newData = action.gameSortContents + + if prevData then + newData.entries = Immutable.JoinLists(prevData.entries, newData.entries) + end + + state = Immutable.Set(state, action.sort, newData) + end + return state +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortsContents.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortsContents.spec.lua new file mode 100644 index 0000000..bd6abcf --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortsContents.spec.lua @@ -0,0 +1,103 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local AddGameSorts = require(Modules.LuaApp.Actions.AddGameSorts) + local SetGameSortContents = require(Modules.LuaApp.Actions.SetGameSortContents) + local AddGameSortContents = require(Modules.LuaApp.Actions.AddGameSortContents) + local GameSortsContents = require(Modules.LuaApp.Reducers.GameSortsContents) + local GameSort = require(Modules.LuaApp.Models.GameSort) + local GameSortEntry = require(Modules.LuaApp.Models.GameSortEntry) + local GameSortContents = require(Modules.LuaApp.Models.GameSortContents) + + local testEntry1 = GameSortEntry.mock("testId1") + local testEntry2 = GameSortEntry.mock("testId2") + local testEntry3 = GameSortEntry.mock("testId3") + + it("should be unmodified by other actions", function() + local oldState = GameSortsContents(nil, {}) + local newState = GameSortsContents(oldState, { type = "not a real action" }) + + expect(oldState).to.equal(newState) + end) + + describe("AddGameSorts", function() + it("should update when empty", function() + local oldState = GameSortsContents(nil, {}) + local testSort = GameSort.mock() + testSort.name = "Popular" + local action = AddGameSorts({ testSort }) + + local newState = GameSortsContents(oldState, action) + expect(newState["Popular"]).to.never.equal(nil) + end) + end) + + describe("SetGameSortContents", function() + it("should preserve purity", function() + local oldState = GameSortsContents(nil, {}) + local newState = GameSortsContents(oldState, SetGameSortContents("Popular", GameSortContents.new())) + expect(oldState).to.never.equal(newState) + end) + + it("should set the games in sorts", function() + local expectedModifiedGroup = "Popular" + local testGameSortContents = GameSortContents.fromData({ + entries = {testEntry1, testEntry2, testEntry3}, + rowsRequested = 4, + hasMoreRows = false, + nextPageExclusiveStartId = 0 + }) + + local defaultState = GameSortsContents({ [expectedModifiedGroup] = GameSortContents.new() }, {}) + + -- check that there are no sorts in the expected group to begin with + local gameSortContents = #defaultState[expectedModifiedGroup] + expect(gameSortContents).to.equal(0) + + -- modify the store + local action = SetGameSortContents(expectedModifiedGroup, testGameSortContents) + local modifiedState = GameSortsContents(defaultState, action) + + -- check the store now contains the correct data + expect(GameSortContents.IsEqual(testGameSortContents, modifiedState[expectedModifiedGroup])).to.equal(true) + end) + end) + + describe("AddGameSortContents", function() + it("should preserve purity", function() + local oldState = GameSortsContents(nil, {}) + local newState = GameSortsContents(oldState, AddGameSortContents("Popular", GameSortContents.new())) + expect(oldState).to.never.equal(newState) + end) + + it("should add games in sorts", function() + local modifiedGroup = "Popular" + local testInitialGames = GameSortContents.fromData({ + entries = {testEntry1, testEntry2}, + rowsRequested = 4, + hasMoreRows = true, + nextPageExclusiveStartId = 0 + }) + local testAddedGames = GameSortContents.fromData({ + entries = {testEntry3}, + rowsRequested = 5, + hasMoreRows = false, + nextPageExclusiveStartId = 0 + }) + local testTotalGames = GameSortContents.fromData({ + entries = {testEntry1, testEntry2, testEntry3}, + rowsRequested = 5, + hasMoreRows = false, + nextPageExclusiveStartId = 0 + }) + + local defaultState = GameSortsContents({ [modifiedGroup] = testInitialGames }, {}) + + -- modify the store + local action = AddGameSortContents(modifiedGroup, testAddedGames) + local modifiedState = GameSortsContents(defaultState, action) + + -- check the store now contains the correct data + expect(GameSortContents.IsEqual(testTotalGames, modifiedState[modifiedGroup])).to.equal(true) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortsStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortsStatus.lua new file mode 100644 index 0000000..1077eff --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortsStatus.lua @@ -0,0 +1,15 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Immutable = require(Modules.Common.Immutable) + +local SetGameSortStatus = require(Modules.LuaApp.Actions.SetGameSortStatus) + +return function(state, action) + state = state or {} + + if action.type == SetGameSortStatus.name then + state = Immutable.Set(state, action.sortName, action.status) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortsStatus.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortsStatus.spec.lua new file mode 100644 index 0000000..90d7954 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameSortsStatus.spec.lua @@ -0,0 +1,29 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) + local GameSortsStatus = require(Modules.LuaApp.Reducers.GameSortsStatus) + local SetGameSortStatus = require(Modules.LuaApp.Actions.SetGameSortStatus) + + it("Should not be mutated by other actions", function() + local oldState = GameSortsStatus(nil, {}) + local newState = GameSortsStatus(oldState, { type = "not a real action" }) + expect(oldState).to.equal(newState) + end) + + describe("SetGameSortStatus", function() + it("should preserve purity", function() + local oldState = GameSortsStatus(nil, {}) + local newState = GameSortsStatus(oldState, SetGameSortStatus("Popular", RetrievalStatus.Failed)) + expect(oldState).to.never.equal(newState) + end) + + it("should correctly set and only set the state of a sort specified by id", function() + local oldState = GameSortsStatus(nil, {}) + local newState = GameSortsStatus(oldState, SetGameSortStatus("Popular", RetrievalStatus.Done)) + expect(newState["Popular"]).to.equal(RetrievalStatus.Done) + local newState2 = GameSortsStatus(newState, SetGameSortStatus("Featured", RetrievalStatus.Fetching)) + expect(newState2["Featured"]).to.equal(RetrievalStatus.Fetching) + expect(newState2["Popular"]).to.equal(RetrievalStatus.Done) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameThumbnails.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameThumbnails.lua new file mode 100644 index 0000000..6983648 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameThumbnails.lua @@ -0,0 +1,21 @@ +--[[ + A separate reducer for game thumbnails,setting + it to a separate table. We are using the universeId rightNow +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) +local SetGameThumbnails = require(Modules.LuaApp.Actions.SetGameThumbnails) + +return function(state, action) + state = state or {} + + if action.type == SetGameThumbnails.name then + local tmpTable = {} + for universeId, thumbnailData in pairs(action.thumbnails) do + tmpTable[universeId] = thumbnailData.url + end + state = Immutable.JoinDictionaries(state, tmpTable) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameThumbnails.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameThumbnails.spec.lua new file mode 100644 index 0000000..f39d485 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GameThumbnails.spec.lua @@ -0,0 +1,65 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local GameThumbnails = require(script.Parent.GameThumbnails) + local SetGameThumbnails = require(Modules.LuaApp.Actions.SetGameThumbnails) + local MockId = require(Modules.LuaApp.MockId) + + local function createFakeThumbnail(universeId) + return { + universeId = universeId, + placeId = MockId(), + url = "https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814", + final = true + } + end + + local function createFakeThumbnailTable(number) + local tmpTable = {} + for i = 1, number do + tmpTable[i] = createFakeThumbnail(i) + end + return tmpTable + end + + local function countKeys(t) + local count = 0 + for _ in pairs(t) do + count = count + 1 + end + return count + end + + it("should be empty by default", function() + local defaultState = GameThumbnails(nil, {}) + expect(type(defaultState)).to.equal("table") + expect(countKeys(defaultState)).to.equal(0) + end) + + it("should be unchanged by other actions", function() + local oldState = GameThumbnails(nil, {}) + local newState = GameThumbnails(oldState, { type = "not a real action" }) + expect(oldState).to.equal(newState) + end) + + describe("SetGameThumbnails", function() + it("should preserve purity", function() + local oldState = GameThumbnails(nil, {}) + local newState = GameThumbnails(oldState, SetGameThumbnails(createFakeThumbnailTable(1))) + expect(oldState).to.never.equal(newState) + end) + + it("should add thumbnails", function() + local expectedNum = 5 + local someGameThumbnails = createFakeThumbnailTable(expectedNum) + local action = SetGameThumbnails(someGameThumbnails) + + local modifiedState = GameThumbnails(nil, action) + expect(countKeys(modifiedState)).to.equal(expectedNum) + + for _, gameThumbnail in pairs(someGameThumbnails) do + local storedThumbnail = modifiedState[gameThumbnail.universeId] + expect(storedThumbnail).to.equal(gameThumbnail.url) + end + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Games.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Games.lua new file mode 100644 index 0000000..f25a584 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Games.lua @@ -0,0 +1,19 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) +local AddGames = require(Modules.LuaApp.Actions.AddGames) +local SetPlayabilityStatus = require(Modules.LuaApp.Actions.SetPlayabilityStatus) + +return function(state, action) + state = state or {} + + if action.type == AddGames.name then + -- store the data from the games + state = Immutable.JoinDictionaries(state, action.games) + elseif action.type == SetPlayabilityStatus.name then + if state[action.universeId] then + state[action.universeId] = Immutable.Set(state[action.universeId], "playabilityStatus", action.playabilityStatus) + end + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Games.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Games.spec.lua new file mode 100644 index 0000000..9ef3997 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Games.spec.lua @@ -0,0 +1,101 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Games = require(script.Parent.Games) + local Game = require(Modules.LuaApp.Models.Game) + local AddGames = require(Modules.LuaApp.Actions.AddGames) + local SetPlayabilityStatus = require(Modules.LuaApp.Actions.SetPlayabilityStatus) + local PlayabilityStatus = require(Modules.LuaApp.Enum.PlayabilityStatus) + local MockId = require(Modules.LuaApp.MockId) + + local function countChildObjects(aTable) + local numChildren = 0 + for _ in pairs(aTable) do + numChildren = numChildren + 1 + end + + return numChildren + end + + local function createFakeGame() + local game = Game.mock() + game.universeId = MockId() + game.placeId = MockId() + + return game + end + + local function createFakeGameTable(numGames) + local someGames = {} + for _ = 1, numGames do + local game = createFakeGame() + someGames[game.universeId] = game + end + + return someGames + end + + it("should be empty by default", function() + local defaultState = Games(nil, {}) + + expect(type(defaultState)).to.equal("table") + expect(countChildObjects(defaultState)).to.equal(0) + end) + + it("should be unchanged by other actions", function() + local oldState = Games(nil, {}) + local newState = Games(oldState, { type = "not a real action" }) + expect(oldState).to.equal(newState) + end) + + describe("AddGames", function() + it("should preserve purity", function() + local oldState = Games(nil, {}) + local newState = Games(oldState, AddGames(createFakeGameTable(1))) + expect(oldState).to.never.equal(newState) + end) + + it("should add games", function() + local expectedNumGames = 5 + local someGames = createFakeGameTable(expectedNumGames) + local action = AddGames(someGames) + + -- add some games to the store + local modifiedState = Games(nil, action) + expect(countChildObjects(modifiedState)).to.equal(expectedNumGames) + + -- check that the games have been added to the store + for _, game in pairs(someGames) do + local storedGame = modifiedState[game.universeId] + for key in pairs(storedGame) do + expect(storedGame[key]).to.equal(game[key]) + end + end + end) + end) + + describe("SetPlayabilityStatus", function() + it("should set playabilityStatus for game", function() + local expectedNumGames = 5 + local someGames = createFakeGameTable(expectedNumGames) + local oldState = Games(nil, {}) + local newState = Games(oldState, AddGames(someGames)) + + -- Default playabilityStatus is "" + for universeId, game in pairs(someGames) do + expect(newState[universeId].playabilityStatus).to.equal(game.playabilityStatus) + expect(newState[universeId].playabilityStatus).to.equal("") + end + + -- Set PlayabilityStatus + local playabilityStatus = PlayabilityStatus.Playable + for universeId, game in pairs(someGames) do + newState = Games(newState, SetPlayabilityStatus(universeId, playabilityStatus)) + end + + -- playabilityStatus should have been updated + for universeId, game in pairs(someGames) do + expect(newState[universeId].playabilityStatus).to.equal(playabilityStatus) + end + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GamesPageDataStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GamesPageDataStatus.lua new file mode 100644 index 0000000..81a7a92 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/GamesPageDataStatus.lua @@ -0,0 +1,13 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local SetGamesPageDataStatus = require(Modules.LuaApp.Actions.SetGamesPageDataStatus) +local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) + +return function(state, action) + state = state or RetrievalStatus.NotStarted + + if action.type == SetGamesPageDataStatus.name then + state = action.status + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/HomePageDataStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/HomePageDataStatus.lua new file mode 100644 index 0000000..8a90385 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/HomePageDataStatus.lua @@ -0,0 +1,13 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local SetHomePageDataStatus = require(Modules.LuaApp.Actions.SetHomePageDataStatus) +local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) + +return function(state, action) + state = state or RetrievalStatus.NotStarted + + if action.type == SetHomePageDataStatus.name then + state = action.status + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/LocalUserId.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/LocalUserId.lua new file mode 100644 index 0000000..be3b71d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/LocalUserId.lua @@ -0,0 +1,12 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local SetLocalUserId = require(Modules.LuaApp.Actions.SetLocalUserId) + +return function(state, action) + state = state or "" + + if action.type == SetLocalUserId.name then + return action.userId + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/LocalUserId.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/LocalUserId.spec.lua new file mode 100644 index 0000000..c0cbdf5 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/LocalUserId.spec.lua @@ -0,0 +1,21 @@ +return function() + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local LocalUserId = require(Modules.LuaApp.Reducers.LocalUserId) + local SetLocalUserId = require(Modules.LuaApp.Actions.SetLocalUserId) + + describe("SetLocalUserId", function() + it("should set the local userid with the given id", function() + + local state = nil + local existingId = state + local newId = "1337" + + local newState = LocalUserId(state, SetLocalUserId(newId)) + + expect(state).to.equal(existingId) + expect(newState).to.equal(newId) + end) + end) + +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Navigation.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Navigation.lua new file mode 100644 index 0000000..395ade6 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Navigation.lua @@ -0,0 +1,37 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Rodux = require(Modules.Common.Rodux) +local Immutable = require(Modules.Common.Immutable) +local AppPage = require(Modules.LuaApp.AppPage) +local ApplyNavigateToRoute = require(Modules.LuaApp.Actions.ApplyNavigateToRoute) +local ApplyNavigateBack = require(Modules.LuaApp.Actions.ApplyNavigateBack) + +local DEFAULT_PAGE = { name = AppPage.Home } +local DEFAULT_ROUTE = { DEFAULT_PAGE } + +local function newNavState(state, history, timeout) + return { + history = history, + lockTimer = math.max(state.lockTimer, timeout or 0), + } +end + +return Rodux.createReducer({ + history = { DEFAULT_ROUTE }, + lockTimer = 0, +}, { + [ApplyNavigateToRoute.name] = function(state, action) + if #action.route == 1 then + -- If we're navigating to a root page, clear the history. + return newNavState(state, { action.route }, action.timeout) + else + return newNavState(state, Immutable.Append(state.history, action.route), action.timeout) + end + end, + [ApplyNavigateBack.name] = function(state, action) + if #state.history > 1 then + return newNavState(state, Immutable.RemoveFromList(state.history, #state.history), action.timeout) + else + return state + end + end, +}) \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Navigation.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Navigation.spec.lua new file mode 100644 index 0000000..f1094e2 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Navigation.spec.lua @@ -0,0 +1,110 @@ +return function() + local Navigation = require(script.Parent.Navigation) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local AppPage = require(Modules.LuaApp.AppPage) + local ApplyNavigateToRoute = require(Modules.LuaApp.Actions.ApplyNavigateToRoute) + local ApplyNavigateBack = require(Modules.LuaApp.Actions.ApplyNavigateBack) + + it("should have a single route to the home page by default", function() + local navigation = Navigation(nil, {}) + + expect(type(navigation.history)).to.equal("table") + expect(#navigation.history).to.equal(1) + expect(type(navigation.history[1])).to.equal("table") + expect(#navigation.history[1]).to.equal(1) + expect(navigation.history[1][1].name).to.equal(AppPage.Home) + end) + + it("should be unchanged by other actions", function() + local state = Navigation(nil, {}) + state = Navigation(state, { type = "not SetAppPage" }) + + expect(#state.history).to.equal(1) + expect(#state.history[1]).to.equal(1) + expect(state.history[1][1].name).to.equal(AppPage.Home) + end) + + describe("ApplyNavigateToRoute", function() + it("should set the next route", function() + local state = Navigation(nil, {}) + state = Navigation(state, ApplyNavigateToRoute({ + { name = AppPage.Games }, + { name = AppPage.GamesList, detail = "Popular" } + })) + + expect(#state.history).to.equal(2) + expect(#state.history[1]).to.equal(1) + expect(state.history[1][1].name).to.equal(AppPage.Home) + expect(#state.history[2]).to.equal(2) + expect(state.history[2][1].name).to.equal(AppPage.Games) + expect(state.history[2][2].name).to.equal(AppPage.GamesList) + expect(state.history[2][2].detail).to.equal("Popular") + end) + + it("should clear history if the route is a root page", function() + local state = Navigation(nil, {}) + state = Navigation(state, ApplyNavigateToRoute({ { name = AppPage.Games } })) + + expect(#state.history).to.equal(1) + expect(#state.history[1]).to.equal(1) + expect(state.history[1][1].name).to.equal(AppPage.Games) + end) + + it("should store the timeout value", function() + local state = Navigation(nil, {}) + state = Navigation(state, ApplyNavigateToRoute({ { name = AppPage.Games } }, 12345)) + + expect(state.lockTimer).to.equal(12345) + end) + + it("should not set the timeout value if unspecified", function() + local state = Navigation(nil, {}) + state.lockTimer = 12345 + state = Navigation(state, ApplyNavigateToRoute({ { name = AppPage.Games } })) + + expect(state.lockTimer).to.equal(12345) + end) + end) + + describe("ApplyNavigateBack", function() + it("should go back to the previous route", function() + local state = { + history = { + { { name = AppPage.Home } }, + { { name = AppPage.Home }, { name = AppPage.GamesList, detail = "Popular" } }, + }, + lockTimer = 0, + } + state = Navigation(state, ApplyNavigateBack()) + + expect(#state.history).to.equal(1) + expect(#state.history[1]).to.equal(1) + expect(state.history[1][1].name).to.equal(AppPage.Home) + end) + + it("should do nothing if there's only one route in the history", function() + local state = Navigation(nil, {}) + state = Navigation(state, ApplyNavigateBack()) + + expect(#state.history).to.equal(1) + expect(#state.history[1]).to.equal(1) + expect(state.history[1][1].name).to.equal(AppPage.Home) + end) + + it("should store the timeout value", function() + local state = Navigation(nil, {}) + state = Navigation(state, ApplyNavigateBack(12345)) + + expect(state.lockTimer).to.equal(12345) + end) + + it("should not set the timeout value if unspecified", function() + local state = Navigation(nil, {}) + state.lockTimer = 12345 + state = Navigation(state, ApplyNavigateBack()) + + expect(state.lockTimer).to.equal(12345) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/NextTokenRefreshTime.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/NextTokenRefreshTime.lua new file mode 100644 index 0000000..19b219d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/NextTokenRefreshTime.lua @@ -0,0 +1,16 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) +local SetNextTokenRefreshTime = require(Modules.LuaApp.Actions.SetNextTokenRefreshTime) + +return function(state, action) + state = state or { + Games = -1, + HomeGames = -1, + GamesSeeAll = -1, + } + + if action.type == SetNextTokenRefreshTime.name then + state = Immutable.Set(state, action.sortCategory, action.nextRefreshTime) + end + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/NextTokenRefreshTime.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/NextTokenRefreshTime.spec.lua new file mode 100644 index 0000000..ff99756 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/NextTokenRefreshTime.spec.lua @@ -0,0 +1,25 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local NextTokenRefreshTime = require(script.parent.NextTokenRefreshTime) + local SetNextTokenRefreshTime = require(Modules.LuaApp.Actions.SetNextTokenRefreshTime) + + it("Should not be mutated by other actions", function() + local oldState = NextTokenRefreshTime(nil, {}) + local newState = NextTokenRefreshTime(oldState, { type = "not a real action" }) + expect(oldState).to.equal(newState) + end) + + describe("SetNextTokenRefreshTime", function() + it("should preserve purity", function() + local oldState = NextTokenRefreshTime(nil, {}) + local newState = NextTokenRefreshTime(oldState, SetNextTokenRefreshTime("Games", 10)) + expect(oldState).to.never.equal(newState) + end) + + it("should correctly update next token refresh time", function() + local oldState = NextTokenRefreshTime(nil, {}) + local newState = NextTokenRefreshTime(oldState, SetNextTokenRefreshTime("Games", 10)) + expect(newState["Games"]).to.equal(10) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/NotificationBadgeCounts.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/NotificationBadgeCounts.lua new file mode 100644 index 0000000..4b6fa87 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/NotificationBadgeCounts.lua @@ -0,0 +1,14 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local SetNotificationCount = require(Modules.LuaApp.Actions.SetNotificationCount) + +return function(state, action) + state = state or { + TopBarNotificationIcon = "0", + } + + if action.type == SetNotificationCount.name then + state.TopBarNotificationIcon = action.notificationCount + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/PlaceIdsToUniverseIds.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/PlaceIdsToUniverseIds.lua new file mode 100644 index 0000000..fd9d95a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/PlaceIdsToUniverseIds.lua @@ -0,0 +1,13 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) +local AddPlaceIdsToUniverseIds = require(Modules.LuaApp.Actions.AddPlaceIdsToUniverseIds) + +return function(state, action) + state = state or {} + + if action.type == AddPlaceIdsToUniverseIds.name then + state = Immutable.JoinDictionaries(state, action.placeIdsToUniverseIds) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/PlaceIdsToUniverseIds.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/PlaceIdsToUniverseIds.spec.lua new file mode 100644 index 0000000..067818d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/PlaceIdsToUniverseIds.spec.lua @@ -0,0 +1,41 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local PlaceIdsToUniverseIds = require(script.Parent.PlaceIdsToUniverseIds) + local AddPlaceIdsToUniverseIds = require(Modules.LuaApp.Actions.AddPlaceIdsToUniverseIds) + local TableUtilities = require(Modules.LuaApp.TableUtilities) + + it("should be empty by default", function() + local defaultState = PlaceIdsToUniverseIds(nil, {}) + + expect(type(defaultState)).to.equal("table") + expect(TableUtilities.FieldCount(defaultState)).to.equal(0) + end) + + it("should be unchanged by other actions", function() + local oldState = PlaceIdsToUniverseIds(nil, {}) + local newState = PlaceIdsToUniverseIds(oldState, { type = "not a real action" }) + expect(oldState).to.equal(newState) + end) + + describe("AddPlaceIdsToUniverseIds", function() + it("should preserve purity", function() + local oldState = PlaceIdsToUniverseIds(nil, {}) + local newState = PlaceIdsToUniverseIds(oldState, AddPlaceIdsToUniverseIds({})) + expect(oldState).to.never.equal(newState) + end) + + it("should add PlaceIdsToUniverseIds", function() + local somePlaceIdsToUniverseIds = {} + somePlaceIdsToUniverseIds[606849621] = 245662005 + somePlaceIdsToUniverseIds[824337654] = 343344299 + local action = AddPlaceIdsToUniverseIds(somePlaceIdsToUniverseIds) + + local modifiedState = PlaceIdsToUniverseIds(nil, action) + expect(TableUtilities.FieldCount(modifiedState)).to.equal(2) + + for placeId, universeId in pairs(somePlaceIdsToUniverseIds) do + expect(modifiedState[placeId]).to.equal(universeId) + end + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Platform.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Platform.lua new file mode 100644 index 0000000..b987936 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Platform.lua @@ -0,0 +1,12 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local SetPlatform = require(Modules.LuaApp.Actions.SetPlatform) + +return function(state, action) + state = state or Enum.Platform.None + + if action.type == SetPlatform.name then + return action.platform + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Platform.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Platform.spec.lua new file mode 100644 index 0000000..dd87689 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Platform.spec.lua @@ -0,0 +1,32 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SetPlatform = require(Modules.LuaApp.Actions.SetPlatform) + + local PlatformReducer = require(Modules.LuaApp.Reducers.Platform) + + describe("Platform", function() + it("should be none by default", function() + local state = PlatformReducer(nil, {}) + + expect(state).to.equal(Enum.Platform.None) + end) + + it("should be unmodified by other actions", function() + local oldState = PlatformReducer(nil, {}) + local newState = PlatformReducer(oldState, { type = "not a real action" }) + + expect(oldState).to.equal(newState) + end) + + it("should be changed using SetPlatform", function() + local state = PlatformReducer(nil, {}) + + state = PlatformReducer(state, SetPlatform(Enum.Platform.IOS)) + expect(state).to.equal(Enum.Platform.IOS) + + state = PlatformReducer(state, SetPlatform(Enum.Platform.Android)) + expect(state).to.equal(Enum.Platform.Android) + end) + end) + +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/RequestsStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/RequestsStatus.lua new file mode 100644 index 0000000..9dbf680 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/RequestsStatus.lua @@ -0,0 +1,15 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local GameSortTokenFetchingStatus = require(Modules.LuaApp.Reducers.GameSortTokenFetchingStatus) +local GameSortsStatus = require(Modules.LuaApp.Reducers.GameSortsStatus) +local SearchesInGamesStatus = require(Modules.LuaApp.Reducers.SearchesInGamesStatus) + +return function(state, action) + state = state or {} + + return { + GameSortTokenFetchingStatus = GameSortTokenFetchingStatus(state.GameSortTokenFetchingStatus, action), + GameSortsStatus = GameSortsStatus(state.GameSortsStatus, action), + SearchesInGamesStatus = SearchesInGamesStatus(state.SearchesInGamesStatus, action), + } +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/RequestsStatus.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/RequestsStatus.spec.lua new file mode 100644 index 0000000..ca82efb --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/RequestsStatus.spec.lua @@ -0,0 +1,21 @@ +return function() + local RequestsStatus = require(script.Parent.RequestsStatus) + + it("has the expected fields, and only the expected fields", function() + local state = RequestsStatus(nil, {}) + + local expectedKeys = { + GameSortTokenFetchingStatus = true, + GameSortsStatus = true, + SearchesInGamesStatus = true, + } + + for key in pairs(expectedKeys) do + assert(state[key] ~= nil, string.format("Expected field %q", key)) + end + + for key in pairs(state) do + assert(expectedKeys[key] ~= nil, string.format("Did not expect field %q", key)) + end + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/ScreenSize.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/ScreenSize.lua new file mode 100644 index 0000000..91ed881 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/ScreenSize.lua @@ -0,0 +1,12 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local SetScreenSize = require(Modules.LuaApp.Actions.SetScreenSize) + +return function(state, action) + state = state or Vector2.new(0, 0) + + if action.type == SetScreenSize.name then + return action.screenSize + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/ScreenSize.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/ScreenSize.spec.lua new file mode 100644 index 0000000..93a70ed --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/ScreenSize.spec.lua @@ -0,0 +1,24 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local ScreenSize = require(Modules.LuaApp.Reducers.ScreenSize) + local SetScreenSize = require(Modules.LuaApp.Actions.SetScreenSize) + local defaultScreenSize = Vector2.new(0, 0) + it("should return default value when init", function() + local state = ScreenSize(nil, {}) + expect(type(state)).to.equal("userdata") + expect(state).to.equal(defaultScreenSize) + end) + it("should be unchanged by other actions", function() + local oldState = ScreenSize(nil, {}) + local newState = ScreenSize(oldState, { type = "not SetScreenSize" }) + expect(newState).to.equal(oldState) + expect(newState).to.equal(defaultScreenSize) + end) + it("should update ScreenSize when dispatch SetScreenSize action", function() + local newScreenSize = Vector2.new(100, 100) + local oldState = ScreenSize(nil, {}) + local newState = ScreenSize(oldState, SetScreenSize(newScreenSize)) + expect(newState).never.to.equal(oldState) + expect(newState).to.equal(newScreenSize) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Search.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Search.lua new file mode 100644 index 0000000..52f5462 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Search.lua @@ -0,0 +1,19 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local SearchesInGames = require(Modules.LuaApp.Reducers.SearchesInGames) +-- local SearchesInGroups = require(Modules.LuaApp.Reducers.SearchesInGroups) +-- local SearchesInPlayers = require(Modules.LuaApp.Reducers.SearchesInPlayers) +-- local SearchesInCatalog = require(Modules.LuaApp.Reducers.SearchesInCatalog) +-- local SearchesInLibrary = require(Modules.LuaApp.Reducers.SearchesInLibrary) + +return function(state, action) + state = state or {} + + return { + SearchesInGames = SearchesInGames(state.SearchesInGames, action), + -- SearchesInGroups = SearchInGroups(state.SearchesInGroups, action), + -- SearchesInPlayers = SearchInPlayers(state.SearchesInPlayers, action), + -- SearchesInCatalog = SearchInCatalog(state.SearchesInCatalog, action), + -- SearchesInLibrary = SearchInLibrary(state.SearchesInLibrary, action), + } +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Search.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Search.spec.lua new file mode 100644 index 0000000..d874dc0 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Search.spec.lua @@ -0,0 +1,23 @@ +return function() + local Search = require(script.Parent.Search) + + it("has the expected fields, and only the expected fields", function() + local state = Search(nil, {}) + + local expectedKeys = { + SearchesInGames = true, + -- SearchesInGroups = true, + -- SearchesInPlayers = true, + -- SearchesInCatalog = true, + -- SearchesInLibrary = true, + } + + for key in pairs(expectedKeys) do + assert(state[key] ~= nil, string.format("Expected field %q", key)) + end + + for key in pairs(state) do + assert(expectedKeys[key] ~= nil, string.format("Did not expect field %q", key)) + end + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/SearchesInGames.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/SearchesInGames.lua new file mode 100644 index 0000000..ddd798b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/SearchesInGames.lua @@ -0,0 +1,38 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) +local SetSearchInGames = require(Modules.LuaApp.Actions.SetSearchInGames) +local AppendSearchInGames = require(Modules.LuaApp.Actions.AppendSearchInGames) +local RemoveSearchInGames = require(Modules.LuaApp.Actions.RemoveSearchInGames) +local ResetSearchesInGames = require(Modules.LuaApp.Actions.ResetSearchesInGames) + +return function(state, action) + state = state or {} + + if action.type == SetSearchInGames.name then + state = Immutable.Set(state, action.searchUuid, action.searchInGames) + + elseif action.type == AppendSearchInGames.name then + local searchUuid = action.searchUuid + local prevData = state[searchUuid] + local newData = action.searchInGames + + if prevData then + newData.entries = Immutable.JoinLists(prevData.entries, newData.entries) + newData.keyword = prevData.keyword + newData.suggestedKeyword = prevData.suggestedKeyword + newData.correctedKeyword = prevData.correctedKeyword + newData.filteredKeyword = prevData.filteredKeyword + newData.isKeywordSuggestionEnabled = prevData.isKeywordSuggestionEnabled + end + + state = Immutable.Set(state, searchUuid, newData) + + elseif action.type == RemoveSearchInGames.name then + state = Immutable.Set(state, action.searchUuid, nil) + + elseif action.type == ResetSearchesInGames.name then + state = {} + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/SearchesInGames.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/SearchesInGames.spec.lua new file mode 100644 index 0000000..53fbe04 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/SearchesInGames.spec.lua @@ -0,0 +1,145 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local SearchesInGames = require(script.Parent.SearchesInGames) + local SearchInGames = require(Modules.LuaApp.Models.SearchInGames) + local GameSortEntry = require(Modules.LuaApp.Models.GameSortEntry) + local SetSearchInGames = require(Modules.LuaApp.Actions.SetSearchInGames) + local AppendSearchInGames = require(Modules.LuaApp.Actions.AppendSearchInGames) + local RemoveSearchInGames = require(Modules.LuaApp.Actions.RemoveSearchInGames) + local ResetSearchesInGames = require(Modules.LuaApp.Actions.ResetSearchesInGames) + + local function countChildObjects(aTable) + local numChildren = 0 + for _ in pairs(aTable) do + numChildren = numChildren + 1 + end + + return numChildren + end + + local function createFakeSearches(numSearches) + local searches = {} + for i = 1, numSearches do + local search = SearchInGames.mock() + search.keyword = search.keyword..tostring(i) + table.insert(searches, search) + end + + return searches + end + + it("should be empty by default", function() + local defaultState = SearchesInGames(nil, {}) + + expect(type(defaultState)).to.equal("table") + expect(countChildObjects(defaultState)).to.equal(0) + end) + + it("should be unchanged by other actions", function() + local oldState = SearchesInGames(nil, {}) + local newState = SearchesInGames(oldState, { type = "not a real action" }) + expect(oldState).to.equal(newState) + end) + + describe("SetSearchInGames", function() + it("should preserve purity", function() + local oldState = SearchesInGames(nil, {}) + local newState = SearchesInGames(oldState, SetSearchInGames(1, SearchInGames.mock())) + expect(oldState).to.never.equal(newState) + end) + + it("should add searches", function() + local expectedNumSearches = 5 + local someSearches = createFakeSearches(expectedNumSearches) + someSearches[1].entries = { GameSortEntry.mock("testId1") } + + local state = nil + for i = 1, expectedNumSearches do + state = SearchesInGames(state, SetSearchInGames(i, someSearches[i])) + expect(countChildObjects(state)).to.equal(i) + expect(SearchInGames.IsEqual(state[i], someSearches[i])).to.equal(true) + end + end) + end) + + describe("AppendSearchInGames", function() + it("should preserve purity", function() + local oldState = SearchesInGames(nil, {}) + local newState = SearchesInGames(oldState, AppendSearchInGames(1, SearchInGames.mock())) + expect(oldState).to.never.equal(newState) + end) + + it("should append content to specified search", function() + local expectedNumSearches = 3 + local someSearches = createFakeSearches(expectedNumSearches) + local appendSearchIndex = 2 + someSearches[appendSearchIndex].entries = { GameSortEntry.mock("testId1") } + + local state = nil + for i = 1, expectedNumSearches do + state = SearchesInGames(state, SetSearchInGames(i, someSearches[i])) + end + + local newSearch = SearchInGames.mock() + newSearch.entries = { GameSortEntry.mock("testId2") } + someSearches[appendSearchIndex] = newSearch + someSearches[appendSearchIndex].entries = { GameSortEntry.mock("testId1"), GameSortEntry.mock("testId2") } + + state = SearchesInGames(state, AppendSearchInGames(appendSearchIndex, newSearch)) + + for i = 1, expectedNumSearches do + expect(SearchInGames.IsEqual(state[i], someSearches[i])).to.equal(true) + end + end) + end) + + describe("RemoveSearchInGames", function() + it("should preserve purity", function() + local oldState = SearchesInGames(nil, {}) + local newState = SearchesInGames(oldState, RemoveSearchInGames(1)) + expect(oldState).to.never.equal(newState) + end) + + it("should remove and only remove specified search", function() + local expectedNumSearches = 5 + local someSearches = createFakeSearches(expectedNumSearches) + + local state = nil + for i = 1, expectedNumSearches do + state = SearchesInGames(state, SetSearchInGames(i, someSearches[i])) + end + + local removeSearchIndex = 2 + someSearches[removeSearchIndex] = nil + state = SearchesInGames(state, RemoveSearchInGames(removeSearchIndex)) + + expect(countChildObjects(state)).to.equal(4) + expect(state[removeSearchIndex]).to.equal(nil) + for i = 1, expectedNumSearches do + if i ~= removeSearchIndex then + expect(SearchInGames.IsEqual(state[i], someSearches[i])).to.equal(true) + end + end + end) + end) + + describe("ResetSearchesInGames", function() + it("should preserve purity", function() + local oldState = SearchesInGames(nil, {}) + local newState = SearchesInGames(oldState, ResetSearchesInGames()) + expect(oldState).to.never.equal(newState) + end) + + it("should reset searches", function() + local expectedNumSearches = 3 + local someSearches = createFakeSearches(expectedNumSearches) + local state = nil + for i = 1, expectedNumSearches do + state = SearchesInGames(state, SetSearchInGames(i, someSearches[i])) + end + + state = SearchesInGames(state, ResetSearchesInGames()) + expect(countChildObjects(state)).to.equal(0) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/SearchesInGamesStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/SearchesInGamesStatus.lua new file mode 100644 index 0000000..f288dc1 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/SearchesInGamesStatus.lua @@ -0,0 +1,15 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Immutable = require(Modules.Common.Immutable) + +local SetSearchInGamesStatus = require(Modules.LuaApp.Actions.SetSearchInGamesStatus) + +return function(state, action) + state = state or {} + + if action.type == SetSearchInGamesStatus.name then + state = Immutable.Set(state, action.searchUuid, action.status) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/SearchesInGamesStatus.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/SearchesInGamesStatus.spec.lua new file mode 100644 index 0000000..55eb82c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/SearchesInGamesStatus.spec.lua @@ -0,0 +1,29 @@ +return function() + local Modules = game:GetService("CoreGui"):FindFirstChild("RobloxGui").Modules + local SearchRetrievalStatus = require(Modules.LuaApp.Enum.SearchRetrievalStatus) + local SearchesInGamesStatus = require(Modules.LuaApp.Reducers.SearchesInGamesStatus) + local SetSearchInGamesStatus = require(Modules.LuaApp.Actions.SetSearchInGamesStatus) + + it("Should not be mutated by other actions", function() + local oldState = SearchesInGamesStatus(nil, {}) + local newState = SearchesInGamesStatus(oldState, { type = "not a real action" }) + expect(oldState).to.equal(newState) + end) + + describe("SetSearchInGamesStatus", function() + it("should preserve purity", function() + local oldState = SearchesInGamesStatus(nil, {}) + local newState = SearchesInGamesStatus(oldState, SetSearchInGamesStatus(1, SearchRetrievalStatus.Failed)) + expect(oldState).to.never.equal(newState) + end) + + it("should correctly set and only set the state of a search specified by id", function() + local oldState = SearchesInGamesStatus(nil, {}) + local newState = SearchesInGamesStatus(oldState, SetSearchInGamesStatus(1, SearchRetrievalStatus.Done)) + expect(newState[1]).to.equal(SearchRetrievalStatus.Done) + local newState2 = SearchesInGamesStatus(newState, SetSearchInGamesStatus(2, SearchRetrievalStatus.Fetching)) + expect(newState2[2]).to.equal(SearchRetrievalStatus.Fetching) + expect(newState2[1]).to.equal(SearchRetrievalStatus.Done) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Startup.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Startup.lua new file mode 100644 index 0000000..1582933 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Startup.lua @@ -0,0 +1,20 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local GamesPageDataStatus = require(Modules.LuaApp.Reducers.GamesPageDataStatus) +local HomePageDataStatus = require(Modules.LuaApp.Reducers.HomePageDataStatus) +local SetPreloading = require(Modules.LuaApp.Actions.SetPreloading) + +return function(state, action) + state = state or {} + local Preloading = state.Preloading == nil and true or state.Preloading + + if action.type == SetPreloading.name then + Preloading = action.isPreloading + end + + return { + GamesPageDataStatus = GamesPageDataStatus(state.GamesPageDataStatus, action), + HomePageDataStatus = HomePageDataStatus(state.HomePageDataStatus, action), + Preloading = Preloading, + } +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/TabBarVisible.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/TabBarVisible.lua new file mode 100644 index 0000000..dc1af53 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/TabBarVisible.lua @@ -0,0 +1,14 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local SetTabBarVisible = require(Modules.LuaApp.Actions.SetTabBarVisible) + +return function(state, action) + if state == nil then + state = true + end + + if action.type == SetTabBarVisible.name then + state = action.isVisible + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/TabBarVisible.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/TabBarVisible.spec.lua new file mode 100644 index 0000000..969c53b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/TabBarVisible.spec.lua @@ -0,0 +1,23 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local LuaApp = Modules.LuaApp + + local TabBarVisible = require(LuaApp.Reducers.TabBarVisible) + local SetTabBarVisible = require(LuaApp.Actions.SetTabBarVisible) + + describe("Action TabBarVisible", function() + it("sets the TabBarVisible flag", function() + local state = TabBarVisible(nil, {}) + + expect(state).to.equal(true) + + state = TabBarVisible(state, SetTabBarVisible(false)) + + expect(state).to.equal(false) + + state = TabBarVisible(state, SetTabBarVisible(true)) + + expect(state).to.equal(true) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/TopBar.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/TopBar.lua new file mode 100644 index 0000000..ed84c00 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/TopBar.lua @@ -0,0 +1,23 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) + +local SetTopBarHeight = require(Modules.LuaApp.Actions.SetTopBarHeight) + +local Constants = require(Modules.LuaApp.Constants) + +return function(state, action) + state = state or { + topBarHeight = Constants.TOP_BAR_SIZE, + } + + if action.type == SetTopBarHeight.name then + if state.topBarHeight ~= action.topBarHeight then + local newProperties = { + topBarHeight = action.topBarHeight, + } + state = Immutable.JoinDictionaries(state, newProperties) + end + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/TopBar.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/TopBar.spec.lua new file mode 100644 index 0000000..d4f3531 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/TopBar.spec.lua @@ -0,0 +1,26 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Constants = require(Modules.LuaApp.Constants) + local TopBar = require(Modules.LuaApp.Reducers.TopBar) + + local SetTopBarHeight = require(Modules.LuaApp.Actions.SetTopBarHeight) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = TopBar(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("SetTopBarHeight", function() + it("should update topBarHeight", function() + local state = TopBar(nil, {}) + expect(state.topBarHeight).to.equal(Constants.TOP_BAR_SIZE) + + local newTopBarHeight = 100 + state = TopBar(state, SetTopBarHeight(newTopBarHeight)) + expect(state.topBarHeight).to.equal(newTopBarHeight) + end) + end) + +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/UserStatuses.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/UserStatuses.lua new file mode 100644 index 0000000..900b903 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/UserStatuses.lua @@ -0,0 +1,19 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Immutable = require(Modules.Common.Immutable) + +local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) +local FetchingUser = require(Modules.LuaChat.Actions.FetchingUser) +local AddUser = require(Modules.LuaApp.Actions.AddUser) + +return function(state, action) + state = state or {} + + if action.type == FetchingUser.name then + state = Immutable.Set(state, action.userId, action.status) + elseif action.type == AddUser.name then + state = Immutable.Set(state, action.user.id, RetrievalStatus.Done) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/UserStatuses.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/UserStatuses.spec.lua new file mode 100644 index 0000000..5d9598e --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/UserStatuses.spec.lua @@ -0,0 +1,52 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local UserStatuses = require(Modules.LuaApp.Reducers.UserStatuses) + local FetchingUser = require(Modules.LuaChat.Actions.FetchingUser) + local AddUser = require(Modules.LuaApp.Actions.AddUser) + local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) + local User = require(Modules.LuaApp.Models.User) + + describe("AddUser", function() + it("should not change by non-relevant actions", function() + local oldState = UserStatuses(nil, {}) + local newState = UserStatuses(oldState, { type = "not a real action" }) + expect(oldState).to.equal(newState) + end) + + it("should set is fetching to done when user is received", function() + local oldState = UserStatuses(nil, {}) + local user = User.mock() + + expect(oldState[user.id]).to.equal(nil) + + local newState = UserStatuses(oldState, AddUser(user)) + expect(newState[user.id]).to.equal(RetrievalStatus.Done) + end) + end) + + describe("FetchingUser", function() + it("should not change by non-relevant actions", function() + local oldState = UserStatuses(nil, {}) + local newState = UserStatuses(oldState, { type = "not a real action" }) + expect(oldState).to.equal(newState) + end) + + it("should set user status to NotStarted or nil by default", function() + local oldState = UserStatuses(nil, {}) + local user = User.mock() + expect(oldState[user.id]).to.equal(nil) + end) + + it("should set is fetching on new and existing users", function() + local oldState = UserStatuses(nil, {}) + local user = User.mock() + local newState = UserStatuses(oldState, FetchingUser(user.id)) + + expect(newState[user.id]).to.equal(RetrievalStatus.Fetching) + + local finalState = UserStatuses(newState, AddUser(user)) + + expect(finalState[user.id]).to.equal(RetrievalStatus.Done) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Users.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Users.lua new file mode 100644 index 0000000..39707c1 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Users.lua @@ -0,0 +1,113 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Immutable = require(Modules.Common.Immutable) + +local AddUser = require(Modules.LuaApp.Actions.AddUser) +local AddUsers = require(Modules.LuaApp.Actions.AddUsers) +local SetUserIsFriend = require(Modules.LuaApp.Actions.SetUserIsFriend) +local SetUserPresence = require(Modules.LuaApp.Actions.SetUserPresence) +local SetUserThumbnail = require(Modules.LuaApp.Actions.SetUserThumbnail) +local SetUserMembershipType = require(Modules.LuaApp.Actions.SetUserMembershipType) +local RemoveUser = require(Modules.LuaApp.Actions.RemoveUser) +local ReceivedUserPresence = require(Modules.LuaChat.Actions.ReceivedUserPresence) + +local FFlagFixUsersReducerDataLoss = settings():GetFFlag("FixUsersReducerDataLoss") + +return function(state, action) + state = state or {} + + if action.type == AddUser.name then + local user = action.user + state = Immutable.Set(state, user.id, user) + elseif action.type == AddUsers.name then + if FFlagFixUsersReducerDataLoss then + local addedUsers = action.users + local usersUpdate = {} + for userId, addedUser in pairs(addedUsers) do + local existingUser = state[userId] + if existingUser then + usersUpdate[userId] = Immutable.JoinDictionaries(existingUser, addedUser) + else + usersUpdate[userId] = addedUser + end + end + + state = Immutable.JoinDictionaries(state, usersUpdate) + else + local users = action.users + state = Immutable.JoinDictionaries(state, users) + end + + elseif action.type == SetUserIsFriend.name then + local user = state[action.userId] + if user then + local newUser = Immutable.Set(user, "isFriend", action.isFriend) + state = Immutable.Set(state, user.id, newUser) + else + warn("Setting isFriend on user", action.userId, "who doesn't exist yet") + end + elseif action.type == SetUserPresence.name then + local user = state[action.userId] + if user then + local newUser = Immutable.JoinDictionaries(user, { + presence = action.presence, + lastLocation = action.lastLocation, + }) + state = Immutable.Set(state, user.id, newUser) + else + warn("Setting presence on user", action.userId, "who doesn't exist yet") + end + elseif action.type == ReceivedUserPresence.name then + local user = state[action.userId] + if user then + state = Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(user, { + presence = action.presence, + lastLocation = action.lastLocation, + placeId = action.placeId, + }), + }) + end + elseif action.type == SetUserThumbnail.name then + local user = state[action.userId] + if user then + if FFlagFixUsersReducerDataLoss then + local thumbnails = user.thumbnails or {} + state = Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(user, { + thumbnails = Immutable.JoinDictionaries(thumbnails, { + [action.thumbnailType] = Immutable.JoinDictionaries(thumbnails[action.thumbnailType] or {}, { + [action.thumbnailSize] = action.image, + }), + }), + }), + }) + else + state = Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(user, { + thumbnails = Immutable.JoinDictionaries(user.thumbnails, { + [action.thumbnailType] = Immutable.JoinDictionaries(user.thumbnails[action.thumbnailType] or {}, { + [action.thumbnailSize] = action.image, + }), + }), + }), + }) + end + end + elseif action.type == SetUserMembershipType.name then + local user = state[action.userId] + if user then + state = Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(user, { + membership = action.membershipType, + }), + }) + end + elseif action.type == RemoveUser.name then + if state[action.userId] then + state = Immutable.RemoveFromDictionary(state, action.userId) + end + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Users.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Users.spec.lua new file mode 100644 index 0000000..6a33a46 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Reducers/Users.spec.lua @@ -0,0 +1,105 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Users = require(Modules.LuaApp.Reducers.Users) + local User = require(Modules.LuaApp.Models.User) + local AddUser = require(Modules.LuaApp.Actions.AddUser) + local SetUserIsFriend = require(Modules.LuaApp.Actions.SetUserIsFriend) + local SetUserPresence = require(Modules.LuaApp.Actions.SetUserPresence) + local SetUserMembershipType = require(Modules.LuaApp.Actions.SetUserMembershipType) + local Constants = require(Modules.LuaChat.Constants) + local ReceivedUserPresence = require(Modules.LuaChat.Actions.ReceivedUserPresence) + local MockId = require(Modules.LuaApp.MockId) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = Users(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("AddUser", function() + it("should add a user to the store", function() + local user = User.mock() + local state = {} + + state = Users(state, AddUser(user)) + + expect(state[user.id]).to.equal(user) + end) + end) + + describe("SetUserIsFriend", function() + it("should set isFriend on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + expect(state[user.id].isFriend).to.equal(false) + + state = Users(state, SetUserIsFriend(user.id, true)) + expect(state[user.id].isFriend).to.equal(true) + + state = Users(state, SetUserIsFriend(user.id, false)) + expect(state[user.id].isFriend).to.equal(false) + end) + end) + + describe("SetUserPresence", function() + it("should set presence on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + expect(state[user.id].presence).to.equal(User.PresenceType.OFFLINE) + + state = Users(state, SetUserPresence(user.id, User.PresenceType.ONLINE)) + expect(state[user.id].presence).to.equal(User.PresenceType.ONLINE) + + state = Users(state, SetUserPresence(user.id, User.PresenceType.IN_GAME)) + expect(state[user.id].presence).to.equal(User.PresenceType.IN_GAME) + + state = Users(state, SetUserPresence(user.id, User.PresenceType.IN_STUDIO)) + expect(state[user.id].presence).to.equal(User.PresenceType.IN_STUDIO) + end) + end) + + describe("ReceivedUserPresence", function() + it("should set presence on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + local existingPresence = user.presence + local newPresence = Constants.PresenceType.ONLINE + local lastLocation = MockId() + local newPlaceId = MockId() + + state = Users(state, ReceivedUserPresence(user.id, newPresence, lastLocation, newPlaceId)) + + expect(user.presence).to.equal(existingPresence) + expect(state[user.id].presence).to.equal(newPresence) + expect(state[user.id].lastLocation).to.equal(lastLocation) + expect(state[user.id].placeId).to.equal(newPlaceId) + end) + end) + + describe("SetUserMembershipType", function() + it("should set membership on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + local existingMembership = user.membership + local newMembership = Enum.MembershipType.BuildersClub + + state = Users(state, SetUserMembershipType(user.id, newMembership)) + + expect(user.membership).to.equal(existingMembership) + expect(state[user.id].membership).to.equal(newMembership) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotion.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotion.lua new file mode 100644 index 0000000..3e23722 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotion.lua @@ -0,0 +1,18 @@ +--[[ + The root for the implementation of RoactMotion. + + Loads up files out of RoactMotionImplementation and packages them into a + public API. +]] + +local Implementation = script.Parent.RoactMotionImplementation + +local MotionSpecifier = require(Implementation.MotionSpecifier) +local SimpleMotion = require(Implementation.SimpleMotion) + +local RoactMotion = {} + +RoactMotion.spring = MotionSpecifier.spring +RoactMotion.SimpleMotion = SimpleMotion + +return RoactMotion \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/Config.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/Config.lua new file mode 100644 index 0000000..303fced --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/Config.lua @@ -0,0 +1,23 @@ +return { + -- Update rate in seconds for all animations + UPDATE_RATE = 1 / 120, + + -- Percentage of normal speed to run animations at. + -- Less than 1 makes animations run more slowly. + -- More than 1 makes animations run more quickly. + TIME_FACTOR = 1, + + -- Max update in seconds that will be processed in one frame + -- This tries to prevent the classic 'spiral of death' + MAX_ACCUMULATION = 0.2, + + -- Default values for springs created with spring() + DEFAULT_STIFFNESS = 170, + DEFAULT_DAMPING = 26, + + -- Used to configure the spring resting mechanism + -- The spring will rest all of these are true: + -- * Velocity is less than SPRING_PRECISION + -- * Position is within SPRING_PRECISION of the target + SPRING_PRECISION = 0.01, +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/MotionSpecifier.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/MotionSpecifier.lua new file mode 100644 index 0000000..31a1cb2 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/MotionSpecifier.lua @@ -0,0 +1,54 @@ +--[[ + Motion specifiers define a value and how to move to it. + + A motion specifier is either: + * A table created by MotionSpecifier methods + * A number, which implies instant change +]] + +local Config = require(script.Parent.Config) +local MotionType = require(script.Parent.MotionType) + +local MotionSpecifier = {} + +--[[ + Creates a new spring specifier with the given target value, stiffness, + and damping. +]] +function MotionSpecifier.spring(value, stiffness, damping, precision) + stiffness = stiffness or Config.DEFAULT_STIFFNESS + damping = damping or Config.DEFAULT_DAMPING + precision = precision or Config.SPRING_PRECISION + + return { + type = MotionType.Spring, + value = value, + stiffness = stiffness, + damping = damping, + precision = precision, + } +end + +--[[ + Retrieves a numeric value for the given specifier. +]] +function MotionSpecifier.extractValue(specifier) + if type(specifier) == "table" then + return specifier.value + else + return specifier + end +end + +--[[ + Determines the MotionType specified by the given specifier. +]] +function MotionSpecifier.getType(specifier) + if type(specifier) == "table" then + return specifier.type + else + return MotionType.Instant + end +end + +return MotionSpecifier \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/MotionType.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/MotionType.lua new file mode 100644 index 0000000..2cc6842 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/MotionType.lua @@ -0,0 +1,11 @@ +--[[ + Defines all of the ways values can be transitioned. +]] + +return { + -- The new value should be applied instantly + Instant = "Instant", + + -- Use a physical simulation of a spring + Spring = "Spring", +} \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/SimpleMotion.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/SimpleMotion.lua new file mode 100644 index 0000000..0118204 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/SimpleMotion.lua @@ -0,0 +1,182 @@ +--[[ + Implements motion for a single set of values and a single child object. + + Example: + + Roact.createElement(RoactMotion.SimpleMotion, { + style = { + x = RoactMotion.spring(10), + }, + render = function(values) + return Roact.createElement(MyThing, { + x = x, + }) + end + }) + + When the value passed to `RoactMotion.spring` changes, the rendered children + will animate to the new value. +]] + +local RunService = game:GetService("RunService") + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Roact = require(Modules.Common.Roact) + +local stepSpring = require(script.Parent.stepSpring) +local merge = require(script.Parent.merge) +local Config = require(script.Parent.Config) +local MotionType = require(script.Parent.MotionType) +local MotionSpecifier = require(script.Parent.MotionSpecifier) + +local SimpleMotion = Roact.Component:extend("SimpleMotion") + +local renderSteppedCallbacks = {} + +RunService.RenderStepped:Connect(function(dt) + for callback in pairs(renderSteppedCallbacks) do + callback(dt) + end +end) + +function SimpleMotion:init() + -- Build up a list of initial values to use + -- First, we pull from 'style', which is a dictionary of specifiers + -- We also initialize starting velocities for all motion specifiers here + local startValues = {} + local velocities = {} + for key, value in pairs(self.props.style) do + velocities[key] = 0 + startValues[key] = MotionSpecifier.extractValue(value) + end + + -- ...and then we pull from defaultStyle, if it's given. + -- This is just a dictionary of numbers + if self.props.defaultStyle then + for key, value in pairs(self.props.defaultStyle) do + startValues[key] = value + end + end + + -- Setting resting to false will trigger the spring to re-evaluate its + -- position and velocity based on newest props. When the spring enters a + -- resting position, resting will be set to true. + self.resting = false + + -- When wasResting = false and resting = true, the callback function + -- onRested will be called. + -- Setting it to true here because we don't want to trigger onRested + -- when the spring is initialized and went to resting immediately. + self.wasResting = true + + self.accumulator = 0 + self.state = { + values = startValues, + velocities = velocities, + } +end + +function SimpleMotion:render() + return self.props.render(self.state.values) +end + +function SimpleMotion:update(dt) + if self.resting then + return + end + + local newValues = merge(self.state.values) + local newVelocities = merge(self.state.velocities) + local stateChanged = false + + -- We use a fixed update rate to make sure our springs are predictable. + self.accumulator = self.accumulator + dt % Config.MAX_ACCUMULATION + + while self.accumulator >= Config.UPDATE_RATE do + self.accumulator = self.accumulator - Config.UPDATE_RATE + + -- We should only rest if all values have almost reached their goals + local shouldRest = true + + for key, targetSpecifier in pairs(self.props.style) do + local targetType = MotionSpecifier.getType(targetSpecifier) + + local newPosition, newVelocity + + if targetType == MotionType.Instant then + newPosition = targetSpecifier + newVelocity = 0 + -- Instant MotionType should callback instantly after the move + self.wasResting = false + elseif targetType == MotionType.Spring then + newPosition, newVelocity = stepSpring( + Config.UPDATE_RATE * Config.TIME_FACTOR, + newValues[key], + newVelocities[key], + targetSpecifier.value, + targetSpecifier.stiffness, + targetSpecifier.damping, + targetSpecifier.precision + ) + else + error(("Unsupported MotionType %q"):format(targetType)) + end + + newValues[key] = newPosition + newVelocities[key] = newVelocity + + if newPosition ~= self.state.values[key] or + newVelocity ~= self.state.velocities[key] then + stateChanged = true + end + + -- Because 'stepSpring' does rounding for us, we don't have to + -- worry about floating point errors. + local realTargetValue = MotionSpecifier.extractValue(targetSpecifier) + if newPosition ~= realTargetValue or newVelocity ~= 0 then + shouldRest = false + end + end + + if shouldRest then + self.resting = true + self.accumulator = 0 + + break + end + end + + if stateChanged then + self:setState({ + values = newValues, + velocities = newVelocities, + }) + end + + if not self.wasResting and self.resting and self.props.onRested then + self.props.onRested() + end + + self.wasResting = self.resting +end + +function SimpleMotion:didMount() + self.renderCallback = function(dt) + self:update(dt) + end + renderSteppedCallbacks[self.renderCallback] = true +end + +function SimpleMotion:willUnmount() + renderSteppedCallbacks[self.renderCallback] = nil +end + +function SimpleMotion:willUpdate(newProps) + if newProps == self.props then + return + end + + self.resting = false +end + +return SimpleMotion \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/merge.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/merge.lua new file mode 100644 index 0000000..a60ce88 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/merge.lua @@ -0,0 +1,15 @@ +--[[ + Merges a list of tables into a new table. +]] + +return function(...) + local new = {} + + for i = 1, select("#", ...) do + for key, value in pairs(select(i, ...)) do + new[key] = value + end + end + + return new +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/stepSpring.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/stepSpring.lua new file mode 100644 index 0000000..de637d9 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactMotionImplementation/stepSpring.lua @@ -0,0 +1,30 @@ +--[[ + Steps forward a spring one step. Handles rounding errors for us. + + This function is similar to React Motion's 'stepper' function: + https://github.com/chenglou/react-motion/blob/167c1d19c02c47af64c8c07aeee760d9b3c7559a/src/stepper.js + + It's based off of the simple Damped Harmonic Motion equation: + + x" + cx' + kx = 0 + x" = -cx' - kx + + We simplify objects to have a mass of 1. +]] + +return function(deltaTime, position, velocity, destination, stiffness, damping, precision) + local displacement = position - destination + local springForce = -stiffness * displacement + local dampForce = -damping * velocity + + local acceleration = springForce + dampForce + local newVelocity = velocity + acceleration * deltaTime + local newPosition = position + velocity * deltaTime + + -- This allows us to put the simulation to sleep once it reaches its goal. + if math.abs(newVelocity) < precision and math.abs(destination - newPosition) < precision then + return destination, 0 + end + + return newPosition, newVelocity +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactServices.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactServices.lua new file mode 100644 index 0000000..11b3d24 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactServices.lua @@ -0,0 +1,95 @@ +--[[ + RoactServices is designed to provide a way to access global singletons. + + RoactServices operates by stashing objects into the ServiceProvider's _context, and retreiving them later. + The services are later exposed as props in wrapped component. + + _context cascades through all children components, so ServiceProvider is expected to be + initialized as one of the first components in the hierarchy. +]] + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Immutable = require(Modules.Common.Immutable) +local Roact = require(Modules.Common.Roact) +local Symbol = require(Modules.Common.Symbol) + + +local ServiceProvider = Roact.PureComponent:extend("ServiceProvider") +local RoactServices = { + ServiceProvider = ServiceProvider, +} + +-- props.services : (map) +function ServiceProvider:init(props) + if props and props.services then + -- STASH ALL THE STUFF INTO THE CONTEXT + assert(props.services, "Expected some services to connect, found none") + assert(type(props.services) == "table", "Expected the provided services to be a map") + for service, implementation in pairs(props.services) do + -- iterating over all of the props also exposes the Children, so only run setters on services + if type(service) == "table" then + service.set(self._context, implementation) + end + end +end +end + +function ServiceProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + + +-- createService : given a name for a service, return an object that knows where its stuff is stored +-- serviceName : (string) +function RoactServices.createService(serviceName) + assert(type(serviceName) == "string", "Expected serviceName to be a string") + local sharedSymbol = Symbol.named(serviceName) + + local service = {} + + function service.get(context) + return context[sharedSymbol] + end + + function service.set(context, value) + context[sharedSymbol] = value + end + + return service +end + + +-- connectServices : given an map of services, returns a component with the provided keys as props +-- services - (map) +function RoactServices.connect(serviceMap) + assert(serviceMap ~= nil, "Expected some services to connect, found none.") + return function(component) + assert(component ~= nil, "Expected a component to connect, found none.") + + local serviceProps = {} + local name = ("Services(%s)"):format(tostring(component)) + + local Connection = Roact.PureComponent:extend(name) + + function Connection:init(props) + for propName, service in pairs(serviceMap) do + -- pull the service out of stored context value + assert(type(propName) == "string", string.format("serviceMap must be indexed by strings, not %s", type(propName))) + assert(type(service) == "table", "serviceMap must have a service created by createService()") + assert(type(service.get) == "function", "serviceMap must have a service created by createService()") + assert(props[propName] == nil, "Naming conflict with prop : " .. propName) + serviceProps[propName] = service.get(self._context) + end + end + + function Connection:render() + local props = Immutable.JoinDictionaries(self.props, serviceProps) + return Roact.createElement(component, props) + end + + return Connection + end +end + +return RoactServices diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactServices.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactServices.spec.lua new file mode 100644 index 0000000..d82b083 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/RoactServices.spec.lua @@ -0,0 +1,227 @@ +return function() +--[[ + These tests will throw warnings into the console. These warnings include : + > "props[Children] was defined but was overriden by third parameter to createElement!" + This is because RoactServices and RoactServices.ServiceProvider are not meant to be initialized this often. + This is fine.s +]] + local RoactServices = require(script.parent.RoactServices) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Roact = require(Modules.Common.Roact) + + + describe("ServiceProvider", function() + it("should render the provided children", function() + local testComponentRendered = false + local testComponent = function() + testComponentRendered = true + end + + -- the whole point of ServiceProvider is that you would pass in service definitions as props, + -- but that functionality will be tested elsewhere. This test should just cover that it + -- can properly render components and its internal init() and render() functions don't throw. + local element = Roact.createElement(RoactServices.ServiceProvider, + nil, { + test = Roact.createElement(testComponent), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(testComponentRendered).to.equal(true) + end) + end) + + describe("createService()", function() + it("should create an object with a get and set function", function() + local service = RoactServices.createService("test") + expect(service).to.be.ok() + expect(type(service.get)).to.equal("function") + expect(type(service.set)).to.equal("function") + end) + end) + + describe("connect()", function() + it("should construct a component with the stored services exposed to the provided props", function() + -- This test needs to be read from the bottom upwards + local testService1 = RoactServices.createService("test1") + local testService2 = RoactServices.createService("test2") + local testService3 = RoactServices.createService("test3") + + local testService1Value = { + a = 1, + b = 2, + c = 3, + } + local testService2Value = "someKindOfString" + local testService3Value = 1 + + local testValue1Matches = false + local testValue2Matches = false + local testValue3Matches = false + + + -- create a component to read the test values + local testComponent = Roact.Component:extend("test") + + function testComponent:render() + testValue1Matches = self.props.value1 == testService1Value + testValue2Matches = self.props.value2 == testService2Value + testValue3Matches = self.props.value3 == testService3Value + return Roact.createElement("Frame") + end + + -- assign the values from the service into the props + testComponent = RoactServices.connect({ + value1 = testService1, + value2 = testService2, + value3 = testService3, + })(testComponent) + + -- initialize the heirarchy and the values the services should return + local element = Roact.createElement(RoactServices.ServiceProvider, { + services = { + [testService1] = testService1Value, + [testService2] = testService2Value, + [testService3] = testService3Value, + } + }, { + test = Roact.createElement(testComponent), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + + + expect(testValue1Matches).to.equal(true) + expect(testValue2Matches).to.equal(true) + expect(testValue3Matches).to.equal(true) + end) + + it("should throw if no services are provided to connect", function() + expect(function() + -- create a simple test component + local testComponent = Roact.Component:exend("test") + function testComponent.render() + return Roact.createElement("Frame") + end + + -- improperly hook it up to RoactServices + testComponent = RoactServices.connect()(testComponent) + + -- attempt to render this failure + local element = Roact.createElement(RoactServices.ServiceProvider, + nil, { + test = Roact.createElement(testComponent), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + end) + + it("should throw if no component is provided to connect", function() + expect(function() + -- create a service to hook up + local testService = RoactServices.createService("test") + + -- attempt to connect it to nothing + local testComponent = RoactServices.connect({ + testProp1 = testService, + })() + + -- attempt to render this failure + local element = Roact.createElement(RoactServices.ServiceProvider, { + services = { + [testService] = "foo", + } + }, { + test = Roact.createElement(testComponent), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + end) + + it("should pass props down through its connection", function() + -- This test needs to be read from the bottom upwards + local testService = RoactServices.createService("test") + local testPropNameValue = "testName" + + local testPropExists = false + local testPropMatches = false + + -- create a component to read the test values + local testComponent = Roact.Component:extend("test") + + function testComponent:render() + -- check that the props passed to the testComponent + testPropExists = self.props.name ~= nil + testPropMatches = self.props.name == testPropNameValue + + return Roact.createElement("Frame") + end + + -- wrap the component in a connection + testComponent = RoactServices.connect({ + testServiceProp = testService, + })(testComponent) + + -- initialize the heirarchy and the values the services should return + local element = Roact.createElement(RoactServices.ServiceProvider, { + services = { + -- this value doesn't matter for this test + [testService] = "" + } + }, { + -- add some props to the created component + testComponentWithProps = Roact.createElement(testComponent, + { + -- double check that these props exist on the created component + name = testPropNameValue, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + + + expect(testPropExists).to.equal(true) + expect(testPropMatches).to.equal(true) + end) + + it("should throw if there is a naming conflict in the component's props", function() + -- This test needs to be read from the bottom upwards + local testService = RoactServices.createService("test") + + + -- create a component to read the test values + local testComponent = Roact.Component:extend("test") + function testComponent:render() + return Roact.createElement("Frame") + end + + -- store the service into a prop that will conflict with a supplied prop + testComponent = RoactServices.connect({ + badPropName = testService, + })(testComponent) + + -- initialize the heirarchy + local element = Roact.createElement(RoactServices.ServiceProvider, { + services = { + -- this value doesn't matter for this test + [testService] = "" + } + }, { + -- add a prop that will conflict with the service value + testComponentWithProps = Roact.createElement(testComponent, + { + badPropName = 1, + }), + }) + + expect(function() + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/RobloxEventReceiver.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/RobloxEventReceiver.lua new file mode 100644 index 0000000..1e3b466 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/RobloxEventReceiver.lua @@ -0,0 +1,86 @@ +local CoreGui = game:GetService("CoreGui") + +local Modules = CoreGui.RobloxGui.Modules +local Signal = require(Modules.Common.Signal) + +local RobloxEventReceiver = {} +RobloxEventReceiver.__index = RobloxEventReceiver + +function RobloxEventReceiver:observeEvent(namespace, detailType, callback) + assert(type(namespace) == "string", "Expected namespace to be a string") + assert(type(detailType) == "string", "Expected detailtType to be a string") + assert(type(callback) == "function", "Expected callback to be a function") + + local detailTable = self.namespaceSingularTable[namespace] + if detailTable == nil then + detailTable = {} + self.namespaceSingularTable[namespace] = detailTable + end + -- Check and make sure that someone is observing this detailType under said namespace + local signal = detailTable[detailType] + if signal == nil then + signal = Signal.new() + detailTable[detailType] = signal + end + -- return the signal's connection function + return signal:Connect(callback) +end + +function RobloxEventReceiver:observeBulkEvent(namespace, callback) + assert(type(namespace) == "string", "Expected namespace to be a string") + assert(type(callback) == "function", "Expected callback to be a function") + + local signal = self.namespaceBulkTable[namespace] + if signal == nil then + signal = Signal.new() + self.namespaceBulkTable[namespace] = signal + end + + -- return the signal's connection function + return signal:Connect(callback) +end + +function RobloxEventReceiver.new(notificationService) + local self = { + namespaceSingularTable = {}, + namespaceBulkTable = {}, + } + setmetatable(self, RobloxEventReceiver) + + local notifySingularObservers = function(namespace, detailType, message) + -- Check and make sure that someone is observing this namespace + local detailTable = self.namespaceSingularTable[namespace] + if detailTable == nil then + return + end + -- Check and make sure that someone is observing this detailType under said namespace + local signal = detailTable[detailType] + if signal == nil then + return + end + signal:Fire(message) + end + + local notifyBulkObservers = function(namespace, messages) + -- Check and make sure that someone is observing this namespace + local signal = self.namespaceBulkTable[namespace] + if signal == nil then + return + end + signal:Fire(messages) + end + + self.connection = notificationService.RobloxEventReceived:Connect(function(event) + -- handle bulk and singular events + if event.detailType == nil or event.detailType == "" then + notifyBulkObservers(event.namespace, event.detail) + else + notifySingularObservers(event.namespace, event.detailType, event.detail) + end + + end) + return self +end + + +return RobloxEventReceiver \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/RobloxEventReceiver.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/RobloxEventReceiver.spec.lua new file mode 100644 index 0000000..01f3b0d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/RobloxEventReceiver.spec.lua @@ -0,0 +1,363 @@ +local mockNotificationService = {} +mockNotificationService.__index = mockNotificationService + +function mockNotificationService.new() + local self = {} + setmetatable(self, mockNotificationService) + self.RobloxEventReceived = {} + + function self.RobloxEventReceived:Connect(callback) + self.connection = callback + end + + function self.RobloxEventReceived:send(event) + self.connection(event) + end + return self +end + +return function() + local RobloxEventReceiver = require(game:GetService("CoreGui").RobloxGui.Modules.LuaApp.RobloxEventReceiver) + it("should be able to be created", function() + RobloxEventReceiver.new(mockNotificationService.new()) + end) + + describe("should have the correct api", function() + it("should require a notificationService", function() + expect(function() + RobloxEventReceiver.new(nil) + end).to.throw() + expect(function() + RobloxEventReceiver.new({}) + end).to.throw() + RobloxEventReceiver.new(mockNotificationService.new()) + end) + + it("should throw on bad arguments for observeEvent", function() + local eventReceiver = RobloxEventReceiver.new(mockNotificationService.new()) + expect(function() + eventReceiver:observeEvent() + end).to.throw() + expect(function() + eventReceiver:observeEvent({},"detailType", function()end) + end).to.throw() + expect(function() + eventReceiver:observeEvent("namespace", {}, function()end) + end).to.throw() + expect(function() + eventReceiver:observeEvent("namespace", "detailType", {}) + end).to.throw() + expect(function() + eventReceiver:observeEvent("namespace", function()end) + end).to.throw() + -- normal call + local connection = eventReceiver:observeEvent("namespace", "detailType", function()end) + connection:Disconnect() + end) + + it("should throw on bad arguments for observeBulkEvent", function() + local eventReceiver = RobloxEventReceiver.new(mockNotificationService.new()) + expect(function() + eventReceiver:observeBulkEvent() + end).to.throw() + expect(function() + eventReceiver:observeBulkEvent({}, function()end) + end).to.throw() + expect(function() + eventReceiver:observeBulkEvent("namespace", {}) + end).to.throw() + expect(function() + eventReceiver:observeBulkEvent("namespace","detailType", function() end) + end).to.throw() + -- normal call + local connection = eventReceiver:observeBulkEvent("namespace", function()end) + connection:Disconnect() + end) + end) + + describe("handle observer", function() + it("takes a singular observer", function() + local eventReceiver = RobloxEventReceiver.new(mockNotificationService.new()) + local connection = eventReceiver:observeEvent("namespace", "detail", function() + error("Should not call this callback") + end) + connection.Disconnect() + end) + + it("takes a bulk observer", function() + local eventReceiver = RobloxEventReceiver.new(mockNotificationService.new()) + local connection = eventReceiver:observeBulkEvent("namespaceBulk", function() + error("Should not call this callback") + end) + connection.Disconnect() + end) + + it("notifies and disconnects singular observer", function() + local mns = mockNotificationService.new() + local eventReceiver = RobloxEventReceiver.new(mns) + local count = 0 + local test_message = "Test Message" + local namespace = "namespaceSingular" + local detailType = "detail" + + local connection = eventReceiver:observeEvent(namespace, detailType, function(message) + count = count + 1 + expect(message).to.equal(test_message) + end) + mns.RobloxEventReceived:send({ + namespace = namespace, + detailType = detailType, + detail = test_message, + }) + + expect(count).to.equal(1) + connection.Disconnect() + + mns.RobloxEventReceived:send({ + namespace = namespace, + detailType = detailType, + detail = test_message, + }) + expect(count).to.equal(1) + end) + + it("notifies and disconnects bulk observer", function() + local mns = mockNotificationService.new() + local eventReceiver = RobloxEventReceiver.new(mns) + local count = 0 + local test_message = {"MESSAGE1", "MESSAGE2"} + local namespace = "namespaceBulk" + + local connection = eventReceiver:observeBulkEvent(namespace, function(message) + count = count + 1 + expect(message).to.equal(test_message) + end) + mns.RobloxEventReceived:send({ + namespace = namespace, + detail = test_message, + }) + + expect(count).to.equal(1) + connection.Disconnect() + + mns.RobloxEventReceived:send({ + namespace = namespace, + detail = test_message, + }) + expect(count).to.equal(1) + end) + end) + + describe("handle multiple observers", function() + it("notifies and disconnects singular observers", function() + local mns = mockNotificationService.new() + local eventReceiver = RobloxEventReceiver.new(mns) + local count = 0 + local test_message = "Test Message" + local namespace = "namespaceSingular" + local detailType = "detail" + + local connection = eventReceiver:observeEvent(namespace, detailType, function(message) + count = count + 1 + expect(message).to.equal(test_message) + end) + local connection2 = eventReceiver:observeEvent(namespace, detailType, function(message) + count = count + 1 + expect(message).to.equal(test_message) + end) + mns.RobloxEventReceived:send({ + namespace = namespace, + detailType = detailType, + detail = test_message, + }) + + expect(count).to.equal(2) + connection.Disconnect() + connection2.Disconnect() + + mns.RobloxEventReceived:send({ + namespace = namespace, + detailType = detailType, + detail = test_message, + }) + expect(count).to.equal(2) + end) + + it("notifies and disconnects bulk observers", function() + local mns = mockNotificationService.new() + local eventReceiver = RobloxEventReceiver.new(mns) + local count = 0 + local test_message = {"MESSAGE1", "MESSAGE2"} + local namespace = "namespaceBulk" + + local connection = eventReceiver:observeBulkEvent(namespace, function(message) + count = count + 1 + expect(message).to.equal(test_message) + end) + local connection2 = eventReceiver:observeBulkEvent(namespace, function(message) + count = count + 1 + expect(message).to.equal(test_message) + end) + + mns.RobloxEventReceived:send({ + namespace = namespace, + detail = test_message, + }) + + expect(count).to.equal(2) + connection.Disconnect() + connection2.Disconnect() + + mns.RobloxEventReceived:send({ + namespace = namespace, + detail = test_message, + }) + expect(count).to.equal(2) + end) + + it("notifies and disconnects singular and bulk observers", function() + local mns = mockNotificationService.new() + local eventReceiver = RobloxEventReceiver.new(mns) + local countSingular = 0 + local countBulk = 0 + local test_message = {"MESSAGE1", "MESSAGE2"} + local namespaceSingular = "test_message" + local detailType = "detailType" + local namespaceBulk = "namespaceBulk" + + local connection = eventReceiver:observeEvent(namespaceSingular, detailType, function(message) + countSingular = countSingular + 1 + expect(message).to.equal(test_message) + end) + local connection2 = eventReceiver:observeBulkEvent(namespaceBulk, function(message) + countBulk = countBulk + 1 + expect(message).to.equal(test_message) + end) + + mns.RobloxEventReceived:send({ + namespace = namespaceSingular, + detailType = detailType, + detail = test_message, + }) + + expect(countSingular).to.equal(1) + expect(countBulk).to.equal(0) + + mns.RobloxEventReceived:send({ + namespace = namespaceBulk, + detail = test_message, + }) + + expect(countSingular).to.equal(1) + expect(countBulk).to.equal(1) + connection.Disconnect() + connection2.Disconnect() + + mns.RobloxEventReceived:send({ + namespace = namespaceSingular, + detailType = detailType, + detail = test_message, + }) + mns.RobloxEventReceived:send({ + namespace = namespaceBulk, + detail = test_message, + }) + + expect(countSingular).to.equal(1) + expect(countBulk).to.equal(1) + end) + end) + + describe("should not call when", function() + it("deals with different namespace for singular", function() + local mns = mockNotificationService.new() + local eventReceiver = RobloxEventReceiver.new(mns) + local test_message = "Test Message" + local namespace = "namespaceSingular" + local detailType = "detail" + + local connection = eventReceiver:observeEvent("differentNameSpace", detailType, function(message) + error("Should not call this callback") + end) + mns.RobloxEventReceived:send({ + namespace = namespace, + detailType = detailType, + detail = test_message, + }) + + connection.Disconnect() + end) + + it("deals with different namespace for bulk", function() + local mns = mockNotificationService.new() + local eventReceiver = RobloxEventReceiver.new(mns) + local test_message = "Test Message" + local namespace = "namespaceSingular" + + local connection = eventReceiver:observeBulkEvent("differentNameSpace", function(message) + error("Should not call this callback") + end) + mns.RobloxEventReceived:send({ + namespace = namespace, + detail = test_message, + }) + + connection.Disconnect() + end) + + it("deals with different detailTypes for singular", function() + local mns = mockNotificationService.new() + local eventReceiver = RobloxEventReceiver.new(mns) + local test_message = "Test Message" + local namespace = "namespaceSingular" + local detailType = "detail" + + local connection = eventReceiver:observeEvent(namespace, "differentType", function(message) + error("Should not call this callback") + end) + mns.RobloxEventReceived:send({ + namespace = namespace, + detailType = detailType, + detail = test_message, + }) + + connection.Disconnect() + end) + + it("expects a singlular event", function() + local mns = mockNotificationService.new() + local eventReceiver = RobloxEventReceiver.new(mns) + local test_message = "Test Message" + local namespace = "namespaceSingular" + + local connection = eventReceiver:observeEvent(namespace, "differentType", function(message) + error("Should not call this callback") + end) + mns.RobloxEventReceived:send({ + namespace = namespace, + detail = test_message, + }) + + connection.Disconnect() + end) + + it("expects a bulk event", function() + local mns = mockNotificationService.new() + local eventReceiver = RobloxEventReceiver.new(mns) + local test_message = "Test Message" + local namespace = "namespaceSingular" + local detailType = "detail" + + local connection = eventReceiver:observeBulkEvent(namespace, function(message) + error("Should not call this callback") + end) + mns.RobloxEventReceived:send({ + namespace = namespace, + detailType = detailType, + detail = test_message, + }) + + connection.Disconnect() + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/SearchUuid.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/SearchUuid.lua new file mode 100644 index 0000000..c5630dc --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/SearchUuid.lua @@ -0,0 +1,10 @@ +--[[ + A function to return a unique ID, used for search results +]] + +local lastId = 0 + +return function() + lastId = lastId + 1 + return lastId +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/AppGuiService.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/AppGuiService.lua new file mode 100644 index 0000000..880a117 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/AppGuiService.lua @@ -0,0 +1,4 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local RoactServices = require(Modules.LuaApp.RoactServices) +local service = RoactServices.createService(script.Name) +return service \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/AppNotificationService.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/AppNotificationService.lua new file mode 100644 index 0000000..880a117 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/AppNotificationService.lua @@ -0,0 +1,4 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local RoactServices = require(Modules.LuaApp.RoactServices) +local service = RoactServices.createService(script.Name) +return service \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalytics.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalytics.lua new file mode 100644 index 0000000..880a117 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalytics.lua @@ -0,0 +1,4 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local RoactServices = require(Modules.LuaApp.RoactServices) +local service = RoactServices.createService(script.Name) +return service \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsAppRouter.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsAppRouter.lua new file mode 100644 index 0000000..9d2bfa4 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsAppRouter.lua @@ -0,0 +1,31 @@ +--[[ + Unlike RoactAnalytics, RoactAnalyticsAppRouter is merely a consumer of the analytics implementation. + It does not require its own setter to be called when the RoactServices ServiceProvider is initialized. +]] + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Events = Modules.LuaApp.Analytics.Events +local pageHeartbeat = require(Events.pageHeartbeat) +local userInteractions = require(Events.userInteractions) +local RoactAnalytics = require(Modules.LuaApp.Services.RoactAnalytics) + +local AppRouterAnalytics = {} +function AppRouterAnalytics.get(context) + local analyticsImpl = RoactAnalytics.get(context) + + local analyticsConsumer = {} + + function analyticsConsumer.reportPageHeartbeat(beatInterval, luaPage) + pageHeartbeat(analyticsImpl.EventStream, beatInterval, luaPage) + end + + function analyticsConsumer.reportPageChanged() + local contextName = "luaPage" + userInteractions(analyticsImpl.EventStream, contextName) + end + + return analyticsConsumer +end + +return AppRouterAnalytics \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsAppStageLoaded.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsAppStageLoaded.lua new file mode 100644 index 0000000..e88b589 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsAppStageLoaded.lua @@ -0,0 +1,25 @@ +--[[ + Unlike RoactAnalytics, RoactAnalyticsAppStageLoaded is merely a consumer of the analytics implementation. + It does not require its own setter to be called when the RoactServices ServiceProvider is initialized. +]] + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Events = Modules.LuaApp.Analytics.Events +local appStageLoaded = require(Events.appStageLoaded) +local RoactAnalytics = require(Modules.LuaApp.Services.RoactAnalytics) + +local AppStageLoadedAnalytics = {} +function AppStageLoadedAnalytics.get(context) + local analyticsImpl = RoactAnalytics.get(context) + + local analyticsConsumer = {} + + function analyticsConsumer.reportAppReady(eventContext) + appStageLoaded(analyticsImpl.EventStream, eventContext, "appReady") + end + + return analyticsConsumer +end + +return AppStageLoadedAnalytics \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsCommonGameEvents.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsCommonGameEvents.lua new file mode 100644 index 0000000..b21bd0f --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsCommonGameEvents.lua @@ -0,0 +1,93 @@ +--[[ + This object is designed to abstract all of the events fired by game components. + Since game carousels, game cards, game lists, and game grids are shared across multiple contexts, + these elements need a common reporting component. +]] +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local gameDetailReferral = require(Modules.LuaApp.Analytics.Events.gameDetailReferral) +local gamesPageInteraction = require(Modules.LuaApp.Analytics.Events.gamesPageInteraction) +local RoactAnalytics = require(Modules.LuaApp.Services.RoactAnalytics) +local Constants = require(Modules.LuaApp.Constants) + +local RoactAnalyticsCommonGameEvents = {} +function RoactAnalyticsCommonGameEvents.get(context, args) + assert(type(args.createReferralCtx) == "function", "Expected createReferralCtx to be a function") + assert(type(args.pageName) == "string", "Expected pageName to be a string") + + local analyticsImpl = RoactAnalytics.get(context) + + local createReferralCtx = args.createReferralCtx + local pageName = args.pageName + local pageNameSeeAll = pageName .. "SeeAll" + + local CGE = {} + + function CGE.reportSeeAll(sortName, indexOnPage) + local sortId = Constants.LEGACY_GAME_SORT_IDS[sortName] + if not sortId then + sortId = Constants.LEGACY_GAME_SORT_IDS.default + end + + local evtContext = "SeeAll" + local actionType = "touch" + local actionValue = tostring(sortId) + local selectedIndex = tonumber(indexOnPage) + + gamesPageInteraction(analyticsImpl.EventStream, evtContext, actionType, actionValue, selectedIndex) + end + + function CGE.reportFilterChange(sortName, indexOnPage) + local sortId = Constants.LEGACY_GAME_SORT_IDS[sortName] + if not sortId then + sortId = Constants.LEGACY_GAME_SORT_IDS.default + end + + local evtContext = "SFMenu" + local actionType = "touch" + local actionValue = tostring(sortId) + local selectedIndex = tonumber(indexOnPage) + + gamesPageInteraction(analyticsImpl.EventStream, evtContext, actionType, actionValue, selectedIndex) + end + + local function reportGameDetailReferral(referralPage, + placeId, + sortName, + indexInSort, + numItemsInSort, + isAd, + timeFilter, + genreFilter) + + -- handle optional values + if not timeFilter then + timeFilter = 1 + end + + if not genreFilter then + genreFilter = 1 + end + + -- lookup the legacy sortId based on the sortName + local sortId = Constants.LEGACY_GAME_SORT_IDS[sortName] + if not sortId then + sortId = Constants.LEGACY_GAME_SORT_IDS.default + end + + local referralContext = createReferralCtx(indexInSort, sortId, timeFilter, genreFilter) + gameDetailReferral(analyticsImpl.EventStream, referralContext, referralPage, numItemsInSort, placeId, isAd) + end + + function CGE.reportOpenGameDetail(placeId, sortName, indexInSort, itemsInSort, isAd, timeFilter, genreFilter) + reportGameDetailReferral(pageName, placeId, sortName, indexInSort, itemsInSort, isAd, timeFilter, genreFilter) + end + + function CGE.reportOpenGameDetailFromSeeAll(placeId, sortName, indexInSort, itemsInSort, isAd, timeFilter, genreFilter) + reportGameDetailReferral(pageNameSeeAll, placeId, sortName, indexInSort, itemsInSort, isAd, timeFilter, genreFilter) + end + + return CGE +end + +return RoactAnalyticsCommonGameEvents \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsGamesPage.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsGamesPage.lua new file mode 100644 index 0000000..c2257a5 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsGamesPage.lua @@ -0,0 +1,25 @@ +--[[ + Unlike RoactAnalytics, RoactAnalyticsGamesPage is merely a consumer of the analytics implementation. + It does not require its own setter to be called when the RoactServices ServiceProvider is initialized. +]] + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local RoactAnalyticsCommonGameEvents = require(Modules.LuaApp.Services.RoactAnalyticsCommonGameEvents) + +local GamesPageAnalytics = {} +function GamesPageAnalytics.get(context) + return RoactAnalyticsCommonGameEvents.get(context, { + pageName = "games", + createReferralCtx = function(indexInSort, sortId, timeFilter, genreFilter) + local context = string.format("gamesort_SortFilter<%d>_TimeFilter<%d>_GenreFilter<%d>_Position<%d>", + sortId, + timeFilter, + genreFilter, + indexInSort) + return context + end + }) +end + +return GamesPageAnalytics \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsHomePage.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsHomePage.lua new file mode 100644 index 0000000..9106ba2 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsHomePage.lua @@ -0,0 +1,23 @@ +--[[ + Unlike RoactAnalytics, RoactAnalyticsHomePage is merely a consumer of the analytics implementation. + It does not require its own setter to be called when the RoactServices ServiceProvider is initialized. +]] + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local RoactAnalyticsCommonGameEvents = require(Modules.LuaApp.Services.RoactAnalyticsCommonGameEvents) + +local HomePageAnalytics = {} +function HomePageAnalytics.get(context) + return RoactAnalyticsCommonGameEvents.get(context, { + pageName = "home", + createReferralCtx = function(indexInSort, sortId) + local context = string.format("home_SortFilter<%d>_Position<%d>", + sortId, + indexInSort) + return context + end + }) +end + +return HomePageAnalytics \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsSearchPage.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsSearchPage.lua new file mode 100644 index 0000000..b23212b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsSearchPage.lua @@ -0,0 +1,21 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local RoactAnalyticsCommonGameEvents = require(Modules.LuaApp.Services.RoactAnalyticsCommonGameEvents) + +local SearchPageAnalytics = {} + +function SearchPageAnalytics.get(context) + return RoactAnalyticsCommonGameEvents.get(context, { + pageName = "gameSearch", + createReferralCtx = function(indexInSort, sortId, timeFilter, genreFilter) + local context = string.format("gamesort_SortFilter<%d>_TimeFilter<%d>_GenreFilter<%d>_Position<%d>", + sortId, + timeFilter, + genreFilter, + indexInSort) + return context + end + }) +end + +return SearchPageAnalytics \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsTopBar.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsTopBar.lua new file mode 100644 index 0000000..54b7a49 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactAnalyticsTopBar.lua @@ -0,0 +1,46 @@ +--[[ + Unlike RoactAnalytics, RoactAnalyticsTopBar is merely a consumer of the analytics implementation. + It does not require its own setter to be called when the RoactServices ServiceProvider is initialized. +]] + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local search = require(Modules.LuaApp.Analytics.Events.search) +local nsOpenContent = require(Modules.LuaApp.Analytics.Events.nsOpenContent) +local buttonClick = require(Modules.LuaApp.Analytics.Events.buttonClick) +local RoactAnalytics = require(Modules.LuaApp.Services.RoactAnalytics) + +local TopBarAnalytics = {} +function TopBarAnalytics.get(context) + local analyticsImpl = RoactAnalytics.get(context) + + local TPA = {} + + local reportSearch = function(eventContext, act, keyword) + search(analyticsImpl.EventStream, eventContext, act, keyword) + end + + function TPA.reportSearchFocused(eventContext) + reportSearch(eventContext, "open", nil) + end + + function TPA.reportSearched(eventContext, keyword) + reportSearch(eventContext, "search", keyword) + end + + function TPA.reportSearchCanceled(eventContext) + reportSearch(eventContext, "cancel", nil) + end + + function TPA.reportNSButtonTouch(count) + nsOpenContent(analyticsImpl.EventStream, "touch", count) + end + + function TPA.reportRobuxButtonClick(eventContext) + buttonClick(analyticsImpl.EventStream, eventContext, "robux") + end + + return TPA +end + +return TopBarAnalytics diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactLocalization.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactLocalization.lua new file mode 100644 index 0000000..16202a5 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactLocalization.lua @@ -0,0 +1,108 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local LocalizationService = game:GetService("LocalizationService") + +local Immutable = require(Modules.Common.Immutable) +local Roact = require(Modules.Common.Roact) +local ExternalEventConnection = require(Modules.Common.RoactUtilities.ExternalEventConnection) +local RoactServices = require(Modules.LuaApp.RoactServices) +local TableUtilities = require(Modules.LuaApp.TableUtilities) + +local service = RoactServices.createService(script.Name) + +-- connect() : given an array of props, create a wrapper that localizes those props from the stored locale +-- propsToLocalize : (array) a list of props in the wrapped component to localize +function service.connect(propsToLocalizeList) + local propsToLocalize = {} + for _, propName in ipairs(propsToLocalizeList) do + propsToLocalize[propName] = true + end + + return function(component) + assert(component ~= nil, "Expected component to be passed to connection, got nil.") + + local name = ("Localize(%s)"):format(tostring(component)) + local connection = Roact.PureComponent:extend(name) + + function connection:init() + local localization = service.get(self._context) + + assert(localization ~= nil, table.concat({ + "Cannot initialize RoactLocalization component without being a descendent of ServiceProvider!", + ("Tried to wrap component %q"):format(tostring(component)), + "Make sure there is a ServiceProvider above this component in the tree." + }, "\n")) + + self.state = { + locale = LocalizationService.RobloxLocaleId + } + self.localization = localization + + self.updateLocalization = function(newLocale) + self:setState({ + locale = newLocale + }) + end + end + + function connection:render() + local localization = service.get(self._context) + + local localizedProps = {} + for propName in pairs(propsToLocalize) do + local stringInfo = self.props[propName] + assert(stringInfo ~= nil, + ("No localization string or table found for \"%s\""):format(propName)) + assert(type(stringInfo) == "table" or type(stringInfo) == "string", + ("Localized field \"%s\" is not a string or table."):format(propName)) + + if type(stringInfo) == "table" then + assert(type(stringInfo[1]) == "string", + ("Localization table \"%s\" requires a key of type string, got %s"):format(propName, type(stringInfo[1]))) + localizedProps[propName] = localization:Format(stringInfo[1], stringInfo) + else + localizedProps[propName] = localization:Format(stringInfo) + end + end + + local props = Immutable.JoinDictionaries(self.props, localizedProps) + if type(component) == "string" then + props.locale = nil + end + + return Roact.createElement(ExternalEventConnection, { + event = LocalizationService:GetPropertyChangedSignal("RobloxLocaleId"), + callback = self.updateLocalization, + }, { + Roact.createElement(component, props) + }) + end + + function connection:shouldUpdate(newProps, newState) + if newState ~= self.state then + return true + end + + for propName in pairs(propsToLocalize) do + local newProp = newProps[propName] + local oldProp = self.props[propName] + if newProp ~= nil and oldProp ~= nil then + if type(newProp) == "table" and type(oldProp) == "table" then + if not TableUtilities.ShallowEqual(newProp, oldProp) then + return true + end + elseif newProp ~= oldProp then + return true + end + elseif newProp ~= nil or oldProp ~= nil then + return true + end + end + + return not TableUtilities.ShallowEqual(newProps, self.props, propsToLocalize) + end + + return connection + end +end + +return service \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactLocalization.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactLocalization.spec.lua new file mode 100644 index 0000000..60b765d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactLocalization.spec.lua @@ -0,0 +1,135 @@ +return function() + local RoactLocalization = require(script.parent.RoactLocalization) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Localization = require(Modules.LuaApp.Localization) + local Roact = require(Modules.Common.Roact) + local RoactServices = require(Modules.LuaApp.RoactServices) + + local testServiceProps = { + services = { + [RoactLocalization] = Localization.new("en-us"), + }, + } + + describe("connect()", function() + it("should throw if instantiated without a ServiceProvider", function() + local testComponent = function() end + local localizedTestComponent = RoactLocalization.connect({})(testComponent) + local element = Roact.createElement(localizedTestComponent) + + expect(function() + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + end) + + it("should pass a localized string to the wrapped component", function() + local localizedString + local testComponent = function(props) + localizedString = props.Text + end + + local localizedTestComponent = RoactLocalization.connect({ "Text" })(testComponent) + local element = Roact.createElement(RoactServices.ServiceProvider, + testServiceProps, { + test = Roact.createElement(localizedTestComponent, { + Text = "Common.Presence.Label.Online", + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(localizedString).to.equal("Online") + end) + + it("should replace arguments when a localized table is used", function() + local localizedString + local testComponent = function(props) + localizedString = props.Text + end + + local localizedTestComponent = RoactLocalization.connect({ "Text" })(testComponent) + local element = Roact.createElement(RoactServices.ServiceProvider, + testServiceProps, { + test = Roact.createElement(localizedTestComponent, { + Text = { + "Feature.Home.HeadingFriends", + friendCount = 20, + } + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(localizedString).to.equal("Friends (20)") + end) + + it("should throw if localized field is not a table", function() + local testComponent = function() end + local localizedTestComponent = RoactLocalization.connect({ "Text" })(testComponent) + + local element = Roact.createElement(RoactServices.ServiceProvider, + testServiceProps, { + test = Roact.createElement(localizedTestComponent, { + Text = "WRONG THING!" + }) + }) + + expect(function() + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + end) + + it("should throw if localized field table's first element isn't a string", function() + local testComponent = function() end + local localizedTestComponent = RoactLocalization.connect({ "Text" })(testComponent) + + local element = Roact.createElement(RoactServices.ServiceProvider, + testServiceProps, { + test = Roact.createElement(localizedTestComponent, { + Text = { true } + }) + }) + + expect(function() + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + end) + + + it("should not re-render if passed a new but identical value for a localized field", function() + local rendered = 0 + local testComponent = function() rendered = rendered + 1 end + local connector = RoactLocalization.connect({ "Text" }) + local localizedTestComponent = connector(testComponent) + + local outerComponent = Roact.Component:extend("OuterComponent") + local update + function outerComponent:init() + update = function() + self:setState(self.state) + end + end + function outerComponent:render() + return Roact.createElement(localizedTestComponent, { + Text = "Common.Presence.Label.Online", + }) + end + + local element = Roact.createElement(RoactServices.ServiceProvider, testServiceProps, { + test = Roact.createElement(outerComponent) + }) + + Roact.mount(element, nil, "localization-test") + + expect(rendered).to.equal(1) + update() + expect(rendered).to.equal(1) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactNetworking.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactNetworking.lua new file mode 100644 index 0000000..880a117 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Services/RoactNetworking.lua @@ -0,0 +1,4 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local RoactServices = require(Modules.LuaApp.RoactServices) +local service = RoactServices.createService(script.Name) +return service \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/StringsLocale.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/StringsLocale.lua new file mode 100644 index 0000000..e90e0ed --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/StringsLocale.lua @@ -0,0 +1,865 @@ + +local GeneratedStrings = require(script.Parent.GeneratedStrings) + +local StringsLocale = {} + +StringsLocale.Keys = { + BY = "BY", + RECOMMENDED_GAMES = "RECOMMENDED_GAMES", + PLAYING = "PLAYING", + VISITS = "VISITS", + CREATED = "CREATED", + UPDATED = "UPDATED", + MAX_PLAYERS = "MAX_PLAYERS", + GENRE = "GENRE", + ALLOWED_GEAR = "ALLOWED_GEAR", + REPORT_ABUSE = "REPORT_ABUSE", + COPYLOCKED = "COPYLOCKED", + DETAILS = "DETAILS", + VIP_SERVERS = "VIP_SERVERS", + GAME_BADGES = "GAME_BADGES", + PASSES_FOR_THIS_GAME = "PASSES_FOR_THIS_GAME", + GEAR_FOR_THIS_GAME = "GEAR_FOR_THIS_GAME", + PLAYERS = "PLAYERS", + CLANS = "CLANS", + SERVERS_MY_FRIENDS_ARE_IN = "SERVERS_MY_FRIENDS_ARE_IN", + OTHER_SERVERS = "OTHER_SERVERS", + ABOUT = "ABOUT", + STORE = "STORE", + LEADERBOARDS = "LEADERBOARDS", + SERVERS = "SERVERS", + SEARCH = "SEARCH", + TURN_ON_CHAT = "TURN_ON_CHAT", + PRIVACY_SETTINGS = "PRIVACY_SETTINGS", + CANCEL = "CANCEL", + CONFIRM = "CONFIRM", + SEARCH_GAMES = "SEARCH_GAMES", + SEARCH_FOR_FRIENDS = "SEARCH_FOR_FRIENDS", + SEARCH_FOR_FRIENDS_AND_CHAT = "SEARCH_FOR_FRIENDS_AND_CHAT", + NO_RESULTS_FOUND = "NO_RESULTS_FOUND", + SEE_MORE_FRIENDS = "SEE_MORE_FRIENDS", + SEE_LESS_FRIENDS = "SEE_LESS_FRIENDS", + NO_NETWORK_CONNECTION = "NO_NETWORK_CONNECTION", + SHARE_GAME_TO_CHAT = "SHARE_GAME_TO_CHAT", + MAKE_FRIENDS_TO_CHAT = "MAKE_FRIENDS_TO_CHAT", + THIS_MESSAGE_WAS_MODERATED = "THIS_MESSAGE_WAS_MODERATED", + FILTERED_FOR_RECEIVERS = "FILTERED_FOR_RECEIVERS", + CHAT_GROUP_NAME = "CHAT_GROUP_NAME", + NOTIFICATIONS = "NOTIFICATIONS", + ADD_FRIENDS = "ADD_FRIENDS", + LEAVE_GROUP = "LEAVE_GROUP", + LEAVE_GROUP_MESSAGE = "LEAVE_GROUP_MESSAGE", + STAY = "STAY", + LEAVE = "LEAVE", + REMOVE_USER = "REMOVE_USER", + REMOVE_USER_CONFIRMATION_MESSAGE = "REMOVE_USER_CONFIRMATION_MESSAGE", + REMOVE = "REMOVE", + NOT_SET = "NOT_SET", + CHAT = "CHAT", + CHAT_DETAILS = "CHAT_DETAILS", + NEW_CHAT_GROUP = "NEW_CHAT_GROUP", + CHAT_INPUT_PLACEHOLDER = "CHAT_INPUT_PLACEHOLDER", + OPTION = "OPTION", + VIEW_PROFILE = "VIEW_PROFILE", + REPORT_USER = "REPORT_USER", + REMOVE_FROM_GROUP = "REMOVE_FROM_GROUP", + GENERAL = "GENERAL", + MEMBERS = "MEMBERS", + FAILED_TO_RENAME_TITLE = "FAILED_TO_RENAME_TITLE", + FAILED_TO_RENAME_MESSAGE = "FAILED_TO_RENAME_MESSAGE", + GROUP_NAME_MODERATED = "GROUP_NAME_MODERATED", + FAILED_TO_LEAVE_GROUP = "FAILED_TO_LEAVE_GROUP", + FAILED_TO_LEAVE_GROUP_MESSAGE = "FAILED_TO_LEAVE_GROUP_MESSAGE", + FAILED_TO_REMOVE_USER = "FAILED_TO_REMOVE_USER", + FAILED_TO_REMOVE_USER_MESSAGE = "FAILED_TO_REMOVE_USER_MESSAGE", + TOO_MANY_PEOPLE = "TOO_MANY_PEOPLE", + REMOVED_FROM_CONVERSATION = "REMOVED_FROM_CONVERSATION", + NAME_THIS_CHAT_GROUP = "NAME_THIS_CHAT_GROUP", + SAVE = "SAVE", + SAVE_NEW_GROUP = "SAVE_NEW_GROUP", + SAVE_ADDED_FRIENDS = "SAVE_ADDED_FRIENDS", + SEND = "SEND", + SENT = "SENT", + OFFLINE = "OFFLINE", + ONLINE = "ONLINE", + BY_BUILDER = "BY_BUILDER", + VIEW_ASSET_DETAILS = "VIEW_ASSET_DETAILS", + PLAY_GAME = "PLAY_GAME", + MY_FEED = "MY_FEED", + FRIENDS_COUNT = "FRIENDS_COUNT", + SEE_ALL = "SEE_ALL", + GAMES = "GAMES", + HOME = "HOME", + CATALOG = "CATALOG", + AVATAR = "AVATAR", + FRIENDS = "FRIENDS", + MORE = "MORE", + CURRENT_PLAYERS = "CURRENT_PLAYERS", +} + +StringsLocale.Languages = { + EN_US = "en-us", + AR = "ar", + ZH_CN = "zh-CN", + ZH_TW = "zh-TW", + CS = "cs", + DA = "da", + NL = "nl", + EN_GB = "en-GB", + FI = "fi", + FR_CA = "fr-CA", + ES = "es", + ES_XL = "es-XL", + SV = "sv", + TH = "th", + TR = "tr", + VI = "vi" +} + +StringsLocale.DefaultLanguage = StringsLocale.Languages.EN_US + +StringsLocale.Content = { + { + + key = StringsLocale.Keys.BY, + values = { + [StringsLocale.Languages.EN_US] = "By", + }, + comment = "The preposition after the game title to introduce the game creator.", + }, + { + + key = StringsLocale.Keys.RECOMMENDED_GAMES, + values = { + [StringsLocale.Languages.EN_US] = "Recommended Games", + }, + comment = "Recommended Games section header for the About Tab on the game details page.", + }, + { + + key = StringsLocale.Keys.COPYLOCKED, + values = { + [StringsLocale.Languages.EN_US] = "This place is copylocked", + }, + comment = "Notification that a game is locked.", + }, + { + + key = StringsLocale.Keys.REPORT_ABUSE, + values = { + [StringsLocale.Languages.EN_US] = "Report Abuse", + }, + comment = "Clickable text to report a game details page for bad content.", + }, + { + + key = StringsLocale.Keys.PLAYING, + values = { + [StringsLocale.Languages.EN_US] = "Playing", + }, + comment = "Label for the number of players playing a game.", + }, + { + + key = StringsLocale.Keys.CURRENT_PLAYERS, + values = { + [StringsLocale.Languages.EN_US] = "{NUMBER_OF_CURRENT_PLAYERS} Playing", + }, + comment = "Label for the number of players that are currently playing a game.", + }, + { + + key = StringsLocale.Keys.VISITS, + values = { + [StringsLocale.Languages.EN_US] = "Visits", + }, + comment = "Label for the number of visits a game has.", + }, + { + + key = StringsLocale.Keys.CREATED, + values = { + [StringsLocale.Languages.EN_US] = "Created", + }, + comment = "Label for the creation date of a game.", + }, + { + + key = StringsLocale.Keys.UPDATED, + values = { + [StringsLocale.Languages.EN_US] = "Updated", + }, + comment = "Label for the date of the last update to a game.", + }, + { + + key = StringsLocale.Keys.MAX_PLAYERS, + values = { + [StringsLocale.Languages.EN_US] = "Max Players", + }, + comment = "Label for the maximum number of players in a game.", + }, + { + + key = StringsLocale.Keys.GENRE, + values = { + [StringsLocale.Languages.EN_US] = "Genre", + }, + comment = "Label for the game genre.", + }, + { + + key = StringsLocale.Keys.ALLOWED_GEAR, + values = { + [StringsLocale.Languages.EN_US] = "Allowed Gear", + }, + comment = "Label for the allowed gear for a game.", + }, + { + + key = StringsLocale.Keys.OTHER_SERVERS, + values = { + [StringsLocale.Languages.EN_US] = "Other Servers", + }, + comment = "The other servers section header in the servers tab on the game details page.", + }, + { + + key = StringsLocale.Keys.SERVERS_MY_FRIENDS_ARE_IN, + values = { + [StringsLocale.Languages.EN_US] = "Servers my friends are in", + }, + comment = "The servers my friends are in section in the servers tab on the game details page.", + }, + { + + key = StringsLocale.Keys.CLANS, + values = { + [StringsLocale.Languages.EN_US] = "Clans", + }, + comment = "The group leaderboard header in the leaderboards tab on the game details page.", + }, + { + + key = StringsLocale.Keys.PLAYERS, + values = { + [StringsLocale.Languages.EN_US] = "Players", + }, + comment = "The players section header in the leaderboards tab on the game details page.", + }, + { + + key = StringsLocale.Keys.GEAR_FOR_THIS_GAME, + values = { + [StringsLocale.Languages.EN_US] = "Gear for this game", + }, + comment = "The gear section header in the store tab on the game details page.", + }, + { + + key = StringsLocale.Keys.PASSES_FOR_THIS_GAME, + values = { + [StringsLocale.Languages.EN_US] = "Passes for this game", + }, + comment = "The passes section header in the store tab on the game details page.", + }, + { + + key = StringsLocale.Keys.GAME_BADGES, + values = { + [StringsLocale.Languages.EN_US] = "Game Badges", + }, + comment = "The game badges section header on the game details page.", + }, + { + + key = StringsLocale.Keys.VIP_SERVERS, + values = { + [StringsLocale.Languages.EN_US] = "VIP Servers", + }, + comment = "The VIP servers section header in the about and servers tabs on the game details page.", + }, + { + + key = StringsLocale.Keys.DETAILS, + values = { + [StringsLocale.Languages.EN_US] = "Details", + }, + comment = "The game details section header on the game details page.", + }, + { + + key = StringsLocale.Keys.ABOUT, + values = { + [StringsLocale.Languages.EN_US] = "About", + }, + comment = "The About tab's text on the game details page.", + }, + { + + key = StringsLocale.Keys.STORE, + values = { + [StringsLocale.Languages.EN_US] = "Store", + }, + comment = "The Store tab's text on the game details page.", + }, + { + + key = StringsLocale.Keys.LEADERBOARDS, + values = { + [StringsLocale.Languages.EN_US] = "Leaderboards", + }, + comment = "The Leaderboards tab's text on the game details page.", + }, + { + + key = StringsLocale.Keys.SERVERS, + values = { + [StringsLocale.Languages.EN_US] = "Servers", + }, + comment = "The Servers tab's text on the game details page.", + }, + { + + key = StringsLocale.Keys.SEARCH, + values = { + [StringsLocale.Languages.EN_US] = "Search", + }, + comment = "Placeholder text for most search input text boxes. Explains the functionality of the text box.", + }, + { + + key = StringsLocale.Keys.TURN_ON_CHAT, + values = { + [StringsLocale.Languages.EN_US] = "To chat with friends, turn on chat in your Privacy Settings.", + }, + comment = "Explains how to enable chat.", + }, + { + + key = StringsLocale.Keys.PRIVACY_SETTINGS, + values = { + [StringsLocale.Languages.EN_US] = "Privacy Settings", + }, + comment = "Label for button that opens the user's privacy settings.", + }, + { + + key = StringsLocale.Keys.CANCEL, + values = { + [StringsLocale.Languages.EN_US] = "Cancel", + }, + comment = "Used for buttons that cancel an action.", + }, + { + + key = StringsLocale.Keys.CONFIRM, + values = { + [StringsLocale.Languages.EN_US] = "Okay", + }, + comment = "Used for buttons that confirm an action or dismiss an informational pop-up.", + }, + { + + key = StringsLocale.Keys.SEARCH_GAMES, + values = { + [StringsLocale.Languages.EN_US] = "Search games", + }, + comment = "Placeholder text when searching for games.", + }, + { + + key = StringsLocale.Keys.SEARCH_FOR_FRIENDS, + values = { + [StringsLocale.Languages.EN_US] = "Search for Friends", + }, + comment = "Placeholder text when searching for friends.", + }, + { + + key = StringsLocale.Keys.SEARCH_FOR_FRIENDS_AND_CHAT, + values = { + [StringsLocale.Languages.EN_US] = "Search for friends and chat groups", + }, + comment = "Placeholder text when searching for friends and chat groups.", + }, + { + + key = StringsLocale.Keys.NO_RESULTS_FOUND, + values = { + [StringsLocale.Languages.EN_US] = "No results found", + }, + comment = "When searching and not finding any results.", + }, + { + + key = StringsLocale.Keys.SEE_MORE_FRIENDS, + values = { + [StringsLocale.Languages.EN_US] = "See More ({NUMBER_OF_FRIENDS})", + }, + comment = "Labels a button that expands a list of friends. NUMBER_OF_FRIENDS" + .." is the number of additional friends that will be shown.", + }, + { + + key = StringsLocale.Keys.SEE_LESS_FRIENDS, + values = { + [StringsLocale.Languages.EN_US] = "See Less", + }, + comment = "Labels a button that expands a list of friends. " + .."NUMBER_OF_FRIENDS is the number of additional friends that will be shown.", + }, + { + + key = StringsLocale.Keys.NO_NETWORK_CONNECTION, + values = { + [StringsLocale.Languages.EN_US] = "Connecting...", + }, + comment = "Informs the user that there is no network connection and" + .." data (conversations, etc.) can not be sent or received.", + }, + { + + key = StringsLocale.Keys.SHARE_GAME_TO_CHAT, + values = { + [StringsLocale.Languages.EN_US] = "Share game to chat", + }, + comment = "Title for share game to chat feature.", + }, + { + + key = StringsLocale.Keys.MAKE_FRIENDS_TO_CHAT, + values = { + [StringsLocale.Languages.EN_US] = "Make friends in games to start chatting and playing together.", + }, + comment = "Unless a user has friends, they won't be able to chat with anyone. This message informs players" + .." they need friends to chat and suggests playing games as a way to find friends.", + }, + { + + key = StringsLocale.Keys.THIS_MESSAGE_WAS_MODERATED, + values = { + [StringsLocale.Languages.EN_US] = "This message was moderated and not sent.", + }, + comment = "Shown to the user if a message is fully moderated" + .." and not sent to the other users in a conversation.", + }, + { + + key = StringsLocale.Keys.FILTERED_FOR_RECEIVERS, + values = { + [StringsLocale.Languages.EN_US] = "Not everyone in this chat can see your message.", + }, + comment = "Shown to the user if a message is not shown to the minor people in a group chat.", + }, + { + + key = StringsLocale.Keys.CHAT_GROUP_NAME, + values = { + [StringsLocale.Languages.EN_US] = "Chat Group Name", + }, + comment = "Label for the name of a conversation", + }, + { + + key = StringsLocale.Keys.NOTIFICATIONS, + values = { + [StringsLocale.Languages.EN_US] = "Notifications", + }, + comment = "Label for button that allows you to change settings for which notifications you get.", + }, + { + + key = StringsLocale.Keys.ADD_FRIENDS, + values = { + [StringsLocale.Languages.EN_US] = "Add Friends", + }, + comment = "Labels a button that allows you to add friends to a conversation.", + }, + { + + key = StringsLocale.Keys.LEAVE_GROUP, + values = { + [StringsLocale.Languages.EN_US] = "Leave Group", + }, + comment = "Labels action that allows a user to remove themself from a conversation.", + }, + { + + key = StringsLocale.Keys.REMOVE_USER, + values = { + [StringsLocale.Languages.EN_US] = "Remove User", + }, + comment = "Labels an action which removes another user (not the local user) from a conversation.", + }, + { + + key = StringsLocale.Keys.REMOVE, + values = { + [StringsLocale.Languages.EN_US] = "Remove", + }, + comment = "Labels the confirmation button for removing a user from a conversation." + }, + { + + key = StringsLocale.Keys.LEAVE_GROUP_MESSAGE, + values = { + [StringsLocale.Languages.EN_US] = "You won't be able to keep chatting with this group.", + }, + comment = "Message explaining that if they leave a chat group they" + .." will no longer be able to participate in chat.", + }, + { + + key = StringsLocale.Keys.STAY, + values = { + [StringsLocale.Languages.EN_US] = "Stay", + }, + comment = "Cancels the leave group action.", + }, + { + + key = StringsLocale.Keys.LEAVE, + values = { + [StringsLocale.Languages.EN_US] = "Leave", + }, + comment = "Confirms the leave group action.", + }, + { + + key = StringsLocale.Keys.NOT_SET, + values = { + [StringsLocale.Languages.EN_US] = "Not Set", + }, + comment = "Informs the user the title of a group conversation has not been set.", + }, + { + + key = StringsLocale.Keys.CHAT, + values = { + [StringsLocale.Languages.EN_US] = "Chat", + }, + comment = "Title of screen that shows list of conversations, which" + .." is the first screen that appears when chat is open.", + }, + { + + key = StringsLocale.Keys.CHAT_DETAILS, + values = { + [StringsLocale.Languages.EN_US] = "Chat Details", + }, + comment = "Title of screen that informs the user of a conversation's" + .." participants and title and allows them to edit these." + }, + { + + key = StringsLocale.Keys.NEW_CHAT_GROUP, + values = { + [StringsLocale.Languages.EN_US] = "New Chat Group", + }, + comment = "Title of screen where user can create a new chat group.", + }, + { + + key = StringsLocale.Keys.CHAT_INPUT_PLACEHOLDER, + values = { + [StringsLocale.Languages.EN_US] = "Say something", + }, + comment = "Text shown in chat input bar before user types anything.", + }, + { + + key = StringsLocale.Keys.OPTION, + values = { + [StringsLocale.Languages.EN_US] = "Option", + }, + comment = "Title of pop-up that shows list of user actions.", + }, + { + + key = StringsLocale.Keys.VIEW_PROFILE, + values = { + [StringsLocale.Languages.EN_US] = "View Profile", + }, + comment = "Label for button that allows user to view another user's profile.", + }, + { + + key = StringsLocale.Keys.REPORT_USER, + values = { + [StringsLocale.Languages.EN_US] = "Report User", + }, + comment = "Label for button that allows user to report another user for inappropriate behavior.", + }, + { + + key = StringsLocale.Keys.REMOVE_FROM_GROUP, + values = { + [StringsLocale.Languages.EN_US] = "Remove from Group", + }, + comment = "Label for button that allows a user to remove another user from a group conversation.", + }, + { + + key = StringsLocale.Keys.GENERAL, + values = { + [StringsLocale.Languages.EN_US] = "General", + }, + comment = "Labels section of group details screen with general information like group name.", + }, + { + + key = StringsLocale.Keys.MEMBERS, + values = { + [StringsLocale.Languages.EN_US] = "Members", + }, + comment = "Labels section of group details screen that lists participants.", + }, + { + + key = StringsLocale.Keys.FAILED_TO_RENAME_TITLE, + values = { + [StringsLocale.Languages.EN_US] = "Failed to Rename Conversation", + }, + comment = "Informs the user that a conversation could not be renamed.", + }, + { + + key = StringsLocale.Keys.FAILED_TO_RENAME_MESSAGE, + values = { + [StringsLocale.Languages.EN_US] = "The conversation {EXISTING_NAME} could not be renamed to {NEW_NAME}.", + }, + comment = "Informs the user that a conversation could not be renamed.", + }, + { + + key = StringsLocale.Keys.GROUP_NAME_MODERATED, + values = { + [StringsLocale.Languages.EN_US] = "The chat group name you entered was moderated", + }, + comment = "Informs the user that the name they tried to give a group was deemed inappropriate and moderated.", + }, + { + + key = StringsLocale.Keys.FAILED_TO_LEAVE_GROUP, + values = { + [StringsLocale.Languages.EN_US] = "Failed to Leave Group", + }, + comment = "Title of message when a user tries to leave a group and the http request fails.", + }, + { + + key = StringsLocale.Keys.FAILED_TO_LEAVE_GROUP_MESSAGE, + values = { + [StringsLocale.Languages.EN_US] = "You could not be removed from the conversation {CONVERSATION_TITLE}.", + }, + comment = "Message when a user tries to leave a group and the http request fails.", + }, + { + + key = StringsLocale.Keys.FAILED_TO_REMOVE_USER, + values = { + [StringsLocale.Languages.EN_US] = "Failed to Remove User", + }, + comment = "Title of message when a user tries to remove another user from a group and the http request fails.", + }, + { + + key = StringsLocale.Keys.FAILED_TO_REMOVE_USER_MESSAGE, + values = { + [StringsLocale.Languages.EN_US] = + "The user {USERNAME} could not be removed from the conversation {CONVERSATION_TITLE}.", + }, + comment = "Title of message when a user tries to remove another user from a group and the http request fails.", + }, + { + + key = StringsLocale.Keys.TOO_MANY_PEOPLE, + values = { + [StringsLocale.Languages.EN_US] = "You can only have up to {MAX_GROUP_SIZE} players in a chat group.", + }, + comment = "Informs the user that they can't add any more friends to a conversation.", + }, + { + + key = StringsLocale.Keys.REMOVED_FROM_CONVERSATION, + values = { + [StringsLocale.Languages.EN_US] = "You have been removed from the group.", + }, + comment = "Informs the user that they have been removed from a conversation.", + }, + { + + key = StringsLocale.Keys.NAME_THIS_CHAT_GROUP, + values = { + [StringsLocale.Languages.EN_US] = "Name this chat group", + }, + comment = "Placeholder text for input field that allows user to name a new chat group", + }, + { + + key = StringsLocale.Keys.SAVE_NEW_GROUP, + values = { + [StringsLocale.Languages.EN_US] = "Create", + }, + comment = "Labels button that saves a new chat group.", + }, + { + + key = StringsLocale.Keys.SAVE_ADDED_FRIENDS, + values = { + [StringsLocale.Languages.EN_US] = "Add", + }, + comment = "Labels button that saves changes to chat group when adding new friends.", + }, + { + + key = StringsLocale.Keys.SEND, + values = { + [StringsLocale.Languages.EN_US] = "Send", + }, + comment = "A Send label that is used to send messages", + }, + { + + key = StringsLocale.Keys.SENT, + values = { + [StringsLocale.Languages.EN_US] = "Sent", + }, + comment = "A Sent label that is used after a message is sent.", + }, + { + + key = StringsLocale.Keys.SAVE, + values = { + [StringsLocale.Languages.EN_US] = "Save", + }, + comment = "Labels button that confirms and saves a change", + }, + { + + key = StringsLocale.Keys.REMOVE_USER_CONFIRMATION_MESSAGE, + values = { + [StringsLocale.Languages.EN_US] = "Are you sure you want to remove {USERNAME} from this chat group?", + }, + comment = "Message asking user for confirmation before removing another user from a group conversation", + }, + { + + key = StringsLocale.Keys.OFFLINE, + values = { + [StringsLocale.Languages.EN_US] = "Offline", + }, + comment = "Informs the local user that another user is not signed onto the platform", + }, + { + + key = StringsLocale.Keys.ONLINE, + values = { + [StringsLocale.Languages.EN_US] = "Online", + }, + comment = "Informs the local user that another user is signed onto the platform", + }, + { + + key = StringsLocale.Keys.BY_BUILDER, + values = { + [StringsLocale.Languages.EN_US] = "By {USERNAME}", + }, + comment = "Informs that a game is made by a certain builder.", + }, + + { + + key = StringsLocale.Keys.VIEW_ASSET_DETAILS, + values = { + [StringsLocale.Languages.EN_US] = "View Details", + }, + comment = "Labels a button that will show the user more information on an asset.", + }, + + { + + key = StringsLocale.Keys.PLAY_GAME, + values = { + [StringsLocale.Languages.EN_US] = "Play", + }, + comment = "Labels a button that will send the player into a game.", + }, + { + key = StringsLocale.Keys.MY_FEED, + values = { + [StringsLocale.Languages.EN_US] = "View My Feed", + }, + comment = "Header on the section of the home page that shows the user's feed", + }, + { + key = StringsLocale.Keys.FRIENDS_COUNT, + values = { + [StringsLocale.Languages.EN_US] = "Friends ({COUNT})", + }, + comment = "Header on the section of the home page that shows the user's friends", + }, + { + key = StringsLocale.Keys.SEE_ALL, + values = { + [StringsLocale.Languages.EN_US] = "See All", + }, + comment = "Button that goes on carousels of throughout the UI, which allows the user to go to a separate" + .." page which displays the entire list of things that would go in the carousel", + }, + { + key = StringsLocale.Keys.HOME, + values = { + [StringsLocale.Languages.EN_US] = "Home", + }, + comment = "Title text for application's home page", + }, + { + key = StringsLocale.Keys.GAMES, + values = { + [StringsLocale.Languages.EN_US] = "Games", + }, + comment = "Title text for page that contains a list of games", + }, + { + key = StringsLocale.Keys.CATALOG, + values = { + [StringsLocale.Languages.EN_US] = "Catalog", + }, + comment = "Title text for page that contains catalog", + }, + { + key = StringsLocale.Keys.AVATAR, + values = { + [StringsLocale.Languages.EN_US] = "Avatar", + }, + comment = "Title text for page that contains avatar editor", + }, + { + key = StringsLocale.Keys.FRIENDS, + values = { + [StringsLocale.Languages.EN_US] = "Friends", + }, + comment = "Title text for page that contains friends info", + }, + { + key = StringsLocale.Keys.MORE, + values = { + [StringsLocale.Languages.EN_US] = "More", + }, + comment = "Title text for page that contains a list of other pages", + }, +} + +-- We need to place the generated strings into our key string files +for _, stringData in pairs(StringsLocale.Content) do + local generatedStringData = GeneratedStrings[stringData.key] + if generatedStringData then + for language, translation in pairs(generatedStringData) do + stringData.values[language] = translation + end + end +end + +return StringsLocale \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/StringsLocale.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/StringsLocale.spec.lua new file mode 100644 index 0000000..32c112a --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/StringsLocale.spec.lua @@ -0,0 +1,35 @@ +return function() + local StringsLocale = require(script.Parent.StringsLocale) + + describe("Strings.Content", function() + it("should not contain any duplicate keys", function() + for i, entry1 in ipairs(StringsLocale.Content) do + for j, entry2 in ipairs(StringsLocale.Content) do + if i ~= j then + expect(entry1.key ~= entry2.key).to.equal(true) + end + end + end + end) + it("should contain an entry for every entry in StringsLocale.Keys", function() + for _, key in pairs(StringsLocale.Keys) do + + local keyFound = nil + for _, entry in ipairs(StringsLocale.Content) do + if entry.key == key then + keyFound = true + break + end + end + + expect(keyFound).to.be.ok() + end + end) + it("should have its keys contained in StringsLocale.Keys", function() + for _, entry in ipairs(StringsLocale.Content) do + expect(StringsLocale.Keys[entry.key]).to.be.ok() + end + end) + end) + +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/TableUtilities.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/TableUtilities.lua new file mode 100644 index 0000000..ff42df0 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/TableUtilities.lua @@ -0,0 +1,184 @@ +--[[ + Provides functions for comparing and printing lua tables. +]] + +local TableUtilities = {} +local defaultIgnore = {} + +--[[ + Takes two tables A and B, returns if they have the same key-value pairs + Except ignored keys +]] +function TableUtilities.ShallowEqual(A, B, ignore) + if not A or not B then + return false + elseif A == B then + return true + end + + if not ignore then + ignore = defaultIgnore + end + + for key, value in pairs(A) do + if B[key] ~= value and not ignore[key] then + return false + end + end + for key, value in pairs(B) do + if A[key] ~= value and not ignore[key] then + return false + end + end + + return true +end + +--[[ + Takes two tables A, B and a key, returns if two tables have the same value at key +]] +function TableUtilities.EqualKey(A, B, key) + if A and B and key and key ~= "" and A[key] and B[key] and A[key] == B[key] then + return true + end + return false +end + +--[[ + Takes two tables A and B, returns a new table with elements of A + which are either not keys in B or have a different value in B +]] +function TableUtilities.TableDifference(A, B) + local new = {} + + for key, value in pairs(A) do + if B[key] ~= A[key] then + new[key] = value + end + end + + return new +end + + +--[[ + Takes a list and returns a table whose + keys are elements of the list and whose + values are all true +]] +local function membershipTable(list) + local result = {} + for i = 1, #list do + result[list[i]] = true + end + return result +end + + +--[[ + Takes a table and returns a list of keys in that table +]] +local function listOfKeys(t) + local result = {} + for key,_ in pairs(t) do + table.insert(result, key) + end + return result +end + + +--[[ + Takes two lists A and B, returns a new list of elements of A + which are not in B +]] +function TableUtilities.ListDifference(A, B) + return listOfKeys(TableUtilities.TableDifference(membershipTable(A), membershipTable(B))) +end + + +--[[ + For debugging. Returns false if the given table has any of the following: + - a key that is neither a number or a string + - a mix of number and string keys + - number keys which are not exactly 1..#t +]] +function TableUtilities.CheckListConsistency(t) + local containsNumberKey = false + local containsStringKey = false + local numberConsistency = true + + local index = 1 + for x, _ in pairs(t) do + if type(x) == 'string' then + containsStringKey = true + elseif type(x) == 'number' then + if index ~= x then + numberConsistency = false + end + containsNumberKey = true + else + return false + end + + if containsStringKey and containsNumberKey then + return false + end + + index = index + 1 + end + + if containsNumberKey then + return numberConsistency + end + + return true +end + + +--[[ + For debugging, serializes the given table to a reasonable string that might even interpret as lua. +]] +function TableUtilities.RecursiveToString(t, indent) + indent = indent or '' + + if type(t) == 'table' then + local result = "" + if not TableUtilities.CheckListConsistency(t) then + result = result .. "-- WARNING: this table fails the list consistency test\n" + end + result = result .. "{\n" + for k,v in pairs(t) do + if type(k) == 'string' then + result = result + .. " " + .. indent + .. tostring(k) + .. " = " + .. TableUtilities.RecursiveToString(v, " "..indent) + ..";\n" + end + if type(k) == 'number' then + result = result .. " " .. indent .. TableUtilities.RecursiveToString(v, " "..indent)..",\n" + end + end + result = result .. indent .. "}" + return result + else + return tostring(t) + end +end + + +--[[ + Takes a table and returns the field count +]] +function TableUtilities.FieldCount(t) + local fieldCount = 0 + for _ in pairs(t) do + fieldCount = fieldCount + 1 + end + return fieldCount +end + +return TableUtilities + diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/TableUtilities.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/TableUtilities.spec.lua new file mode 100644 index 0000000..41ec737 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/TableUtilities.spec.lua @@ -0,0 +1,156 @@ +return function() + local TableUtilities = require(script.Parent.TableUtilities) + + it("should return whether tables are equal to each other", function() + local tableA = nil + local tableB = nil + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = nil + tableB = {} + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = {} + tableB = nil + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = {} + tableB = {} + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(true) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(true) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value2", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value1", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value2", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + key2 = "value2", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + end) + + it("should return whether tables are equal to each other at key", function() + local tableA = nil + local tableB = nil + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = nil + tableB = {} + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = {} + tableB = nil + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = {} + tableB = {} + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(true) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value2", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value1", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value2", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + key2 = "value2", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(true) + expect(TableUtilities.EqualKey(tableA, tableB, "key2")).to.equal(false) + end) + + it("should return table's field count", function() + local table = {} + expect(TableUtilities.FieldCount(table)).to.equal(0) + + table = { + key1 = "value1", + } + expect(TableUtilities.FieldCount(table)).to.equal(1) + + table = { + key1 = "value1", + key2 = "value2", + } + expect(TableUtilities.FieldCount(table)).to.equal(2) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/MockGuiService.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/MockGuiService.lua new file mode 100644 index 0000000..f363d5d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/MockGuiService.lua @@ -0,0 +1,16 @@ +local MockGuiService = {} + +function MockGuiService.BroadcastNotification(data, notification) +end + +function MockGuiService.GetNotificationTypeList() + return { VIEW_SUB_PAGE_IN_MORE = "VIEW_SUB_PAGE_IN_MORE", + ACTION_LOG_OUT = "ACTION_LOG_OUT", } +end +function MockGuiService.SetGlobalGuiInset(x1, y1, x2, y2) +end + +function MockGuiService.SafeZoneOffsetsChanged() +end + +return MockGuiService \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/MockNotificationService.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/MockNotificationService.lua new file mode 100644 index 0000000..8f47fc2 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/MockNotificationService.lua @@ -0,0 +1,39 @@ +--[[ + A fake notification service for faking Notifications for tests +]] + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Signal = require(Modules.Common.Signal) +local mockNotificationService = {} +mockNotificationService.__index = mockNotificationService + +function mockNotificationService.ScheduleNotification(userId, alertId, alertMsg, minutesToFire) +end + +function mockNotificationService.CancelNotification(userId, alertId) +end + +function mockNotificationService.CancelAllNotification(userId) +end + +function mockNotificationService.GetScheduledNotifications(userId) +end + +function mockNotificationService.ActionEnabled(actionType) +end + +function mockNotificationService.ActionTaken(actionType) +end + +function mockNotificationService.new() + local mns = {} + + mns.RobloxEventReceived = Signal.new() + mns.RobloxConnectionChanged = Signal.new() + + setmetatable(mns, mockNotificationService) + + return mns +end + +return mockNotificationService \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/MockRequest.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/MockRequest.lua new file mode 100644 index 0000000..2e3e3cb --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/MockRequest.lua @@ -0,0 +1,35 @@ +--[[ + A hub for faking networking responses for tests +]] + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Promise = require(Modules.LuaApp.Promise) +local HttpError = require(Modules.LuaApp.Http.HttpError) +local HttpResponse = require(Modules.LuaApp.Http.HttpResponse) +local StatusCodes = require(Modules.LuaApp.Http.StatusCodes) + + +local MockRequest = {} + +-- responseBody : (string) +function MockRequest.simpleSuccessRequest(responseBody) + assert(responseBody ~= nil, "Expected responseBody not to be nil") + + -- create a simple network handler that only needs a response body specified + return function(url, requestMethod, options) + return Promise.resolve(HttpResponse.new(url, responseBody, 0, StatusCodes.OK)) + end +end + +-- errMsg : (HttpError.Kind) +function MockRequest.simpleFailRequest(errKind) + assert(errKind ~= nil, "Expected errKind not to be nil") + + -- create a simple network handler that only needs an error kind specified + return function(url, requestMethod, options) + return Promise.reject(HttpError.new(url, errKind, "Fake request failed")) + end +end + +return MockRequest \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/MockRequest.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/MockRequest.spec.lua new file mode 100644 index 0000000..66711b7 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/MockRequest.spec.lua @@ -0,0 +1,50 @@ +return function() + local MockRequest = require(script.Parent.MockRequest) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local HttpError = require(Modules.LuaApp.Http.HttpError) + + describe("simpleSuccessRequest", function() + it("should return a function", function() + local request = MockRequest.simpleSuccessRequest("this is a test") + + expect(type(request)).to.equal("function") + end) + + it("should return the provided response as the resolution to a promise", function() + local testBodyMatches = false + + local testBody = "this is a test" + local request = MockRequest.simpleSuccessRequest(testBody) + + local httpPromise = request("testUrl", "GET") + httpPromise:andThen(function(httpResponse) + testBodyMatches = httpResponse.responseBody == testBody + end) + expect(testBodyMatches).to.equal(true) + end) + end) + + + describe("simpleFailRequest", function() + it("should return a function", function() + local request = MockRequest.simpleFailRequest(HttpError.Kind.Unknown) + + expect(type(request)).to.equal("function") + end) + + it("should return the provided error code as the rejection to a promise", function() + local testErrMatches = false + + local testErrKind = HttpError.Kind.Unknown + local request = MockRequest.simpleFailRequest(testErrKind) + + local httpPromise = request("testUrl", "GET") + httpPromise:catch(function(httpError) + testErrMatches = httpError.kind == testErrKind + end) + + expect(testErrMatches).to.equal(true) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/mockServices.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/mockServices.lua new file mode 100644 index 0000000..e4dd770 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/mockServices.lua @@ -0,0 +1,87 @@ +--[[ + Unit testing components tends to require a lot of boilerplate, + use this to easily hook up RoactServices with all the appropriate pieces. + + Any component that uses analytics, makes networking calls, or has localized children should use this in tests. +]] + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Analytics = require(Modules.Common.Analytics) +local AppReducer = require(Modules.LuaApp.AppReducer) +local Localization = require(Modules.LuaApp.Localization) +local MockRequest = require(Modules.LuaApp.TestHelpers.MockRequest) +local Roact = require(Modules.Common.Roact) +local RoactAnalytics = require(Modules.LuaApp.Services.RoactAnalytics) +local RoactLocalization = require(Modules.LuaApp.Services.RoactLocalization) +local RoactNetworking = require(Modules.LuaApp.Services.RoactNetworking) +local AppGuiService = require(Modules.LuaApp.Services.AppGuiService) +local MockGuiService = require(Modules.LuaApp.TestHelpers.MockGuiService) +local RoactRodux = require(Modules.Common.RoactRodux) +local RoactServices = require(Modules.LuaApp.RoactServices) +local Rodux = require(Modules.Common.Rodux) +local AppNotificationService = require(Modules.LuaApp.Services.AppNotificationService) +local MockNotificationService = require(Modules.LuaApp.TestHelpers.MockNotificationService) + +-- mockServices() : provides a test heirarchy for rendering a component that requires services +-- componentMap : (map) a map of elements to test render +-- extraArgs : (table, optional) +-- includeStoreProvider : (bool) when true, adds a StoreProvider in the heirarchy +-- store : (map) a populated table of data from a reducer to include with the StoreProvider +-- extraServices : (map) a map of services as keys that will be added to the services prop +local function mockServices(componentMap, extraArgs) + assert(componentMap, "Expected a map of components, recieved none") + + local includeStoreProvider = false + local store = Rodux.Store.new(AppReducer) + local fakeServiceProps = { + services = { + [RoactAnalytics] = Analytics.mock(), + [RoactLocalization] = Localization.mock(), + [RoactNetworking] = MockRequest.simpleSuccessRequest("{}"), + [AppNotificationService] = MockNotificationService.new(), + [AppGuiService] = MockGuiService, + } + } + + if extraArgs then + if extraArgs["includeStoreProvider"] ~= nil then + includeStoreProvider = extraArgs["includeStoreProvider"] + assert(type(includeStoreProvider) == "boolean", "Expected includeStoreProvider to be a bool") + end + + if extraArgs["store"] ~= nil then + store = extraArgs["store"] + assert(type(store) == "table", "Expected store to be a table") + end + + if extraArgs["extraServices"] ~= nil then + local extraServices = extraArgs["extraServices"] + assert(type(extraServices) == "table", "Expected extraServices to be a table") + for service, value in pairs(extraServices) do + assert(type(service) == "table", "Expected key to be a table") + fakeServiceProps.services[service] = value + end + end + end + + + local root + if includeStoreProvider then + root = Roact.createElement(RoactServices.ServiceProvider, + fakeServiceProps, { + StoreProvider = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, componentMap), + }) + else + root = Roact.createElement(RoactServices.ServiceProvider, + fakeServiceProps, + componentMap) + end + + return root +end + + +return mockServices diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/mockServices.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/mockServices.spec.lua new file mode 100644 index 0000000..1d05a7d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/TestHelpers/mockServices.spec.lua @@ -0,0 +1,216 @@ +return function() + local mockServices = require(script.Parent.mockServices) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + local Roact = require(Modules.Common.Roact) + local RoactServices = require(Modules.LuaApp.RoactServices) + + it("should construct a Roact element that contains an initialized RoactServices", function() + local testComponent = function() end + local element = mockServices({ + tc = Roact.createElement(testComponent) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should throw if no components are provided to render", function() + expect(function() + local element = mockServices() + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + end) + + describe("should accept a table of additional args...", function() + describe("extraArgs.includeStoreProvider", function() + it("should expect a boolean", function() + local function testValue(value) + local testComponent = function() end + local element = mockServices({ + tc = Roact.createElement(testComponent) + }, { + includeStoreProvider = value + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end + + expect(function() + testValue(false) + end).to.be.ok() + + expect(function() + testValue("hello world") + end).to.throw() + + expect(function() + testValue({}) + end).to.throw() + end) + + it("should add a StoreProvider into the returned Roact element", function() + local testComponent = function() end + local element = mockServices({ + tc = Roact.createElement(testComponent) + }, { + includeStoreProvider = true + }) + + local children = element.props[Roact.Children] + expect(children["StoreProvider"]).to.be.ok() + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) + + describe("extraArgs.store", function() + it("should do nothing if extraArgs.includeStoreProvider is false or not included", function() + local testComponent = function() end + local element = mockServices({ + tc = Roact.createElement(testComponent) + }, { + store = {} + }) + + local children = element.props[Roact.Children] + expect(children["StoreProvider"]).to.equal(nil) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should expect a table", function() + local function testValue(value) + local testComponent = function() end + local element = mockServices({ + tc = Roact.createElement(testComponent) + }, { + store = value + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end + + expect(function() + testValue({}) + end).to.be.ok() + + expect(function() + testValue("hello world") + end).to.throw() + + expect(function() + testValue(false) + end).to.throw() + end) + + it("should initialize the Rodux Store", function() + local testStore = { + testValue = "hello world" + } + + local testComponent = function() end + local element = mockServices({ + tc = Roact.createElement(testComponent) + }, { + includeStoreProvider = true, + store = testStore + }) + + local children = element.props[Roact.Children] + local storeProvider = children["StoreProvider"] + expect(storeProvider).to.be.ok() + + local elementStore = storeProvider.props["store"] + expect(elementStore).to.equal(testStore) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) + + describe("extraArgs.extraServices", function() + it("should expect a table", function() + local function testValue(value) + local testComponent = function() end + local element = mockServices({ + tc = Roact.createElement(testComponent) + }, { + extraServices = value + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end + + expect(function() + testValue({}) + end).to.be.ok() + + expect(function() + testValue("hello world") + end).to.throw() + + expect(function() + testValue(false) + end).to.throw() + end) + + it("should expect a valid extraServices map with table keys", function() + local fakeService = RoactServices.createService("test") + local validFakeService = { + [fakeService] = "test", + } + + local invalidFakeService = { + test1 = "haha", + } + + local function testValue(value) + local testComponent = function() end + local element = mockServices({ + tc = Roact.createElement(testComponent) + }, { + extraServices = value + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end + + expect(function() + testValue(invalidFakeService) + end).to.throw() + + expect(function() + testValue(validFakeService) + end).to.be.ok() + end) + + it("should map extraServices to the services prop", function() + local fakeService = RoactServices.createService("test") + local validFakeService = { + [fakeService] = {}, + } + + local function createElement(fakeServices) + local testComponent = function() end + local element = mockServices({ + tc = Roact.createElement(testComponent) + }, { + extraServices = fakeServices + }) + + return element + end + + local element = createElement(validFakeService) + local instance = Roact.mount(element) + + expect(element.props.services[fakeService]).to.equal(validFakeService[fakeService]) + + Roact.unmount(instance) + end) + end) + end) +end diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchGameThumbnails.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchGameThumbnails.lua new file mode 100644 index 0000000..e4a117c --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchGameThumbnails.lua @@ -0,0 +1,60 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Actions = Modules.LuaApp.Actions +local Requests = Modules.LuaApp.Http.Requests +local GamesGetThumbnails = require(Requests.GamesGetThumbnails) +local SetGameThumbnails = require(Actions.SetGameThumbnails) +local Functional = require(Modules.Common.Functional) +local Promise = require(Modules.LuaApp.Promise) + +local function subdiveThumbnailTokenArray(thumbnailTokens, tokenLimit) + local someTokens = {} + for i = 1, #thumbnailTokens, tokenLimit do + local subArray = Functional.Take(thumbnailTokens, tokenLimit, i) + table.insert(someTokens, subArray) + end + + return someTokens +end + +local function fetchThumbnails(networkImpl, thumbnailTokens) + return function(store) + -- NOTE : because the size of each thumbnail token, me must limit the number we can fetch at a time. + -- So break apart the array of tokens we get into smaller, more manageable pieces. + local fetchPromises = {} + local someTokens = subdiveThumbnailTokenArray(thumbnailTokens, 20) + for _, thumbsArr in ipairs(someTokens) do + + local promise = GamesGetThumbnails(networkImpl, thumbsArr, 150, 150):andThen(function(result) + local thumbnails = {} + local totalUnfinished = 0 + local unfinalizedThumbnails = {} + for _,image in pairs(result.responseBody) do + -- Will update this later when WEBAPP-64 has been fixed + local universeId = store:getState().PlaceIdsToUniverseIds[image.placeId] + if image.final == false then + totalUnfinished = totalUnfinished + 1 + unfinalizedThumbnails[universeId] = image.retryToken + else + -- index all of the thumbnails by universeId + thumbnails[universeId] = image + end + end + + store:dispatch(SetGameThumbnails(thumbnails)) + + -- refetch the subset of thumbnails that aren't finalized + if totalUnfinished > 0 then + print(string.format("%d thumbnails are not ready yet", totalUnfinished)) + return store:dispatch(fetchThumbnails(networkImpl, unfinalizedThumbnails)) + end + end) + + -- track all of the promises + table.insert(fetchPromises, promise) + end + + return Promise.all(fetchPromises) + end +end + +return fetchThumbnails \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchGamesData.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchGamesData.lua new file mode 100644 index 0000000..deda079 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchGamesData.lua @@ -0,0 +1,33 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local ApiFetchGamesInSort = require(Modules.LuaApp.Thunks.ApiFetchGamesInSort) +local Promise = require(Modules.LuaApp.Promise) +local Constants = require(Modules.LuaApp.Constants) + +-- create a thunk that fetches all the information we'll need for the games page +return function(networkImpl, sortCategory, targetSort) + + -- Default fetching for Games Page data + if not sortCategory then + sortCategory = Constants.GameSortGroups.Games + end + + return function(store) + local fetchPromises = {} + local state = store:getState() + if not targetSort then + for _, sortName in ipairs(state.GameSortGroups[sortCategory].sorts) do + local sort = state.GameSorts[sortName] + local promise = store:dispatch(ApiFetchGamesInSort(networkImpl, sort)) + table.insert(fetchPromises, promise) + end + else + if state.GameSorts[targetSort] then + local promise = store:dispatch(ApiFetchGamesInSort(networkImpl, state.GameSorts[targetSort])) + table.insert(fetchPromises, promise) + else + Promise.reject() + end + end + return Promise.all(fetchPromises) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchGamesInSort.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchGamesInSort.lua new file mode 100644 index 0000000..dd81e1d --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchGamesInSort.lua @@ -0,0 +1,115 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Actions = Modules.LuaApp.Actions +local Requests = Modules.LuaApp.Http.Requests +local GamesGetList = require(Requests.GamesGetList) +local AddGames = require(Actions.AddGames) +local AddPlaceIdsToUniverseIds = require(Actions.AddPlaceIdsToUniverseIds) +local SetGameSortContents = require(Actions.SetGameSortContents) +local AddGameSortContents = require(Actions.AddGameSortContents) +local SetGameSortStatus = require(Actions.SetGameSortStatus) +local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) +local ApiFetchGameThumbnails = require(Modules.LuaApp.Thunks.ApiFetchGameThumbnails) +local Game = require(Modules.LuaApp.Models.Game) +local GameSortEntry = require(Modules.LuaApp.Models.GameSortEntry) +local GameSortContents = require(Modules.LuaApp.Models.GameSortContents) +local Constants = require(Modules.LuaApp.Constants) +local Promise = require(Modules.LuaApp.Promise) +local TableUtilities = require(Modules.LuaApp.TableUtilities) + + +return function(networkImpl, sort, isAppend, optionalSettings) + return function(store) + local gameSortsStatus = store:getState().RequestsStatus.GameSortsStatus + local gameSortStatus = gameSortsStatus[sort.name] + + if gameSortStatus == RetrievalStatus.Fetching then + return Promise.resolve("Request for sort "..sort.name.." has been debounced") + end + + local argTable = optionalSettings or {} + + argTable.sortToken = sort.token + argTable.contextUniverseId = sort.contextUniverseId + argTable.contextCountryRegionId = sort.contextCountryRegionId + + argTable.startRows = argTable.startRows or 0 + argTable.maxRows = argTable.maxRows or Constants.DEFAULT_GAME_FETCH_COUNT + + store:dispatch(SetGameSortStatus(sort.name, RetrievalStatus.Fetching)) + + return GamesGetList(networkImpl, argTable):andThen( + function(result) + -- parse out the games and thumbnails + local entries = {} + local decodedGamesData = {} + local placeIdsToUniverseIds = {} + local thumbnailTokens = {} + local storedGames = store:GetState().Games + local data = result.responseBody + + if #data.games == 0 then + warn("Found no games in this sort:", sort.displayName) + end + + for index, game in pairs(data.games) do + local placeId = game.placeId + local universeId = game.universeId + local decodedGameData = Game.fromJsonData(game) + + if not TableUtilities.ShallowEqual(decodedGameData, storedGames[universeId]) then + decodedGamesData[universeId] = decodedGameData + + if storedGames[universeId] == nil or decodedGameData.imageToken ~= storedGames[universeId].imageToken then + table.insert(thumbnailTokens, decodedGameData.imageToken) + end + end + + entries[index] = GameSortEntry.fromJsonData(game) + + if placeId and universeId and store:getState().PlaceIdsToUniverseIds[placeId] ~= universeId then + placeIdsToUniverseIds[placeId] = universeId + end + end + + if next(decodedGamesData) then + -- write these games to the store + store:dispatch(AddGames(decodedGamesData)) + end + + if next(placeIdsToUniverseIds) then + store:dispatch(AddPlaceIdsToUniverseIds(placeIdsToUniverseIds)) + end + + local gameSortContentsData = { + entries = entries, + rowsRequested = argTable.startRows + argTable.maxRows, + hasMoreRows = data.hasMoreRows, + nextPageExclusiveStartId = data.nextPageExclusiveStartId, + } + + local gameSortContents = GameSortContents.fromData(gameSortContentsData) + + -- tell the sorts which games to show + if isAppend then + store:dispatch(AddGameSortContents(sort.name, gameSortContents)) + else + store:dispatch(SetGameSortContents(sort.name, gameSortContents)) + end + + store:dispatch(SetGameSortStatus(sort.name, RetrievalStatus.Done)) + + -- request the updated thumbnails for this sort + if #thumbnailTokens > 0 then + return store:dispatch(ApiFetchGameThumbnails(networkImpl, thumbnailTokens)) + else + return Promise.resolve() + end + end, + + function(err) + store:dispatch(SetGameSortStatus(sort.name, RetrievalStatus.Failed)) + return Promise.reject(err) + end + ) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchPlayabilityStatus.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchPlayabilityStatus.lua new file mode 100644 index 0000000..40a9212 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchPlayabilityStatus.lua @@ -0,0 +1,24 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Promise = require(Modules.LuaApp.Promise) +local GamesPlayabilityStatus = require(Modules.LuaApp.Http.Requests.GamesPlayabilityStatus) +local SetPlayabilityStatus = require(Modules.LuaApp.Actions.SetPlayabilityStatus) +local PlayabilityStatus = require(Modules.LuaApp.Enum.PlayabilityStatus) + +local function fetchPlayabilityStatus(networkImpl, universeId) + assert(type(universeId) == "string", "ApiFetchPlayabilityStatus thunk expects universeId to be a string") + + return function(store) + return GamesPlayabilityStatus(networkImpl, universeId):andThen(function(result) + store:dispatch(SetPlayabilityStatus(universeId, result.responseBody.playabilityStatus)) + return Promise.resolve() + end, + + -- failure handler for request 'GamesPlayabilityStatus' + function(err) + store:dispatch(SetPlayabilityStatus(universeId, PlayabilityStatus.RequestFailed)) + return Promise.reject(err) + end) + end +end + +return fetchPlayabilityStatus \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchSearchInGames.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchSearchInGames.lua new file mode 100644 index 0000000..a79b296 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchSearchInGames.lua @@ -0,0 +1,121 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Actions = Modules.LuaApp.Actions +local Requests = Modules.LuaApp.Http.Requests +local AddGames = require(Actions.AddGames) +local SetSearchInGames = require(Actions.SetSearchInGames) +local AppendSearchInGames = require(Actions.AppendSearchInGames) +local SetSearchInGamesStatus = require(Actions.SetSearchInGamesStatus) +local AddPlaceIdsToUniverseIds = require(Actions.AddPlaceIdsToUniverseIds) +local GamesGetList = require(Requests.GamesGetList) +local Immutable = require(Modules.Common.Immutable) +local SearchRetrievalStatus = require(Modules.LuaApp.Enum.SearchRetrievalStatus) +local ApiFetchGameThumbnails = require(Modules.LuaApp.Thunks.ApiFetchGameThumbnails) +local Game = require(Modules.LuaApp.Models.Game) +local GameSortEntry = require(Modules.LuaApp.Models.GameSortEntry) +local SearchInGames = require(Modules.LuaApp.Models.SearchInGames) +local Constants = require(Modules.LuaApp.Constants) +local Promise = require(Modules.LuaApp.Promise) +local TableUtilities = require(Modules.LuaApp.TableUtilities) + +return function(networkImpl, searchArguments, optionalSettings) + return function(store) + local searchKeyword = searchArguments.searchKeyword + local searchUuid = searchArguments.searchUuid + + if not searchUuid then + return Promise.reject("Must have a searchUuid.") + end + + if not searchKeyword then + return Promise.reject("Must have a searchKeyword to search with.") + end + + local searchesInGamesStatus = store:getState().RequestsStatus.SearchesInGamesStatus + local searchStatus = searchesInGamesStatus[searchUuid] + + if searchStatus == SearchRetrievalStatus.Fetching then + return Promise.resolve("Search with Uuid "..searchUuid.." has been debounced") + end + + local argTableSearch = Immutable.JoinDictionaries({ keyword = searchKeyword }, optionalSettings or {}) + + -- Some default values + argTableSearch.startRows = argTableSearch.startRows or 0 + argTableSearch.maxRows = argTableSearch.maxRows or Constants.DEFAULT_GAME_FETCH_COUNT + argTableSearch.isKeywordSuggestionEnabled = argTableSearch.isKeywordSuggestionEnabled or false + + store:dispatch(SetSearchInGamesStatus(searchUuid, SearchRetrievalStatus.Fetching)) + + return GamesGetList(networkImpl, argTableSearch):andThen( + function(result) + local entries = {} + local decodedGamesData = {} + local placeIdsToUniverseIds = {} + local thumbnailTokens = {} + local storedGames = store:getState().Games + local data = result.responseBody + + searchesInGamesStatus = store:getState().RequestsStatus.SearchesInGamesStatus + searchStatus = searchesInGamesStatus[searchUuid] + + if searchStatus == SearchRetrievalStatus.Removed then + return Promise.resolve("Search with Uuid "..searchUuid.." has been terminated") + end + + for index, game in pairs(data.games) do + local placeId = game.placeId + local universeId = game.universeId + local decodedGameData = Game.fromJsonData(game) + + if not TableUtilities.ShallowEqual(decodedGameData, storedGames[universeId]) then + decodedGamesData[universeId] = decodedGameData + + if storedGames[universeId] == nil or decodedGameData.imageToken ~= storedGames[universeId].imageToken then + table.insert(thumbnailTokens, decodedGameData.imageToken) + end + end + + -- convert universeId into entry so we keep some sponsor info + local entry = GameSortEntry.fromJsonData(game) + entries[index] = entry + + + if placeId and universeId and store:getState().PlaceIdsToUniverseIds[placeId] ~= universeId then + placeIdsToUniverseIds[placeId] = universeId + end + end + + if next(decodedGamesData) then + store:dispatch(AddGames(decodedGamesData)) + end + + if next(placeIdsToUniverseIds) then + store:dispatch(AddPlaceIdsToUniverseIds(placeIdsToUniverseIds)) + end + + local rowsRequested = argTableSearch.startRows + argTableSearch.maxRows + local searchInGamesData = SearchInGames.fromJsonData(result.responseBody, searchKeyword, entries, rowsRequested, + argTableSearch.isKeywordSuggestionEnabled) + + if searchArguments.isAppend then + store:dispatch(AppendSearchInGames(searchUuid, searchInGamesData)) + else + store:dispatch(SetSearchInGames(searchUuid, searchInGamesData)) + end + + store:dispatch(SetSearchInGamesStatus(searchUuid, SearchRetrievalStatus.Done)) + + if #thumbnailTokens > 0 then + return store:dispatch(ApiFetchGameThumbnails(networkImpl, thumbnailTokens)) + else + return Promise.resolve() + end + end, + + function(err) + store:dispatch(SetSearchInGamesStatus(searchUuid, SearchRetrievalStatus.Failed)) + return Promise.reject(err) + end + ) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchSearchResult.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchSearchResult.lua new file mode 100644 index 0000000..c033cfe --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchSearchResult.lua @@ -0,0 +1,61 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Constants = require(Modules.LuaApp.Constants) +local Promise = require(Modules.LuaApp.Promise) +local ApiFetchSearchInGames = require(Modules.LuaApp.Thunks.ApiFetchSearchInGames) +-- local ApiFetchSearchInGroups = require(Modules.LuaApp.Thunks.ApiFetchSearchInGroups) +-- local ApiFetchSearchInPlayers = require(Modules.LuaApp.Thunks.ApiFetchSearchInPlayers) +-- local ApiFetchSearchInCatalog = require(Modules.LuaApp.Thunks.ApiFetchSearchInCatalog) +-- local ApiFetchSearchInLibrary = require(Modules.LuaApp.Thunks.ApiFetchSearchInLibrary) + +local thunkMap = { + [Constants.SearchTypes.Games] = function(store, ...) + return store:Dispatch(ApiFetchSearchInGames(...)) + end, + -- [Constants.SearchTypes.Groups] = function(store, ...) + -- return store:Dispatch(ApiFetchSearchInGroups(...)) + -- end, + -- [Constants.SearchTypes.Players] = function(store, ...) + -- return store:Dispatch(ApiFetchSearchInPlayers(...)) + -- end, + -- [Constants.SearchTypes.Catalog] = function(store, ...) + -- return store:Dispatch(ApiFetchSearchInCatalog(...)) + -- end, + -- [Constants.SearchTypes.Library] = function(store, ...) + -- return store:Dispatch(ApiFetchSearchInLibrary(...)) + -- end, +} + +--[[ + searchArguments = { + searchKeyword : string, + searchUuid : number, + searchType : Constants.SearchTypes, + isAppend : boolean, + } +]] + +return function(networkImpl, searchArguments, optionalSettings) + if not searchArguments.searchUuid then + return function() + Promise.reject("Must have a searchUuid.") + end + end + + if not searchArguments.searchKeyword then + return function() + Promise.reject("Must have a searchKeyword to search with.") + end + end + -- Default search Games, will need to update when design is done + if not searchArguments.searchType then + searchArguments.searchType = Constants.SearchTypes.Games + end + return function(store) + if thunkMap[searchArguments.searchType] then + return thunkMap[searchArguments.searchType](store, networkImpl, searchArguments, optionalSettings) + else + Promise.reject("We don't support this searchType.") + end + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchSortTokens.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchSortTokens.lua new file mode 100644 index 0000000..c01e195 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchSortTokens.lua @@ -0,0 +1,81 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Actions = Modules.LuaApp.Actions +local Requests = Modules.LuaApp.Http.Requests +local GamesGetSorts = require(Requests.GamesGetSorts) +local AddGameSorts = require(Actions.AddGameSorts) +local SetGameSortsInGroup = require(Actions.SetGameSortsInGroup) +local GameSort = require(Modules.LuaApp.Models.GameSort) +local Promise = require(Modules.LuaApp.Promise) +local SetGameSortTokenFetchingStatus = require(Actions.SetGameSortTokenFetchingStatus) +local SetNextTokenRefreshTime = require(Modules.LuaApp.Actions.SetNextTokenRefreshTime) +local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) + +local min = math.min + +local function parseSortDataIntoStore(sortCategory, store, result) + local data = result.responseBody + if data.sorts then + local decodedDataSorts = {} + local gameSorts = {} + local minExpiryTime = nil + for _, gameSortJson in ipairs(data.sorts) do + local gameSort = GameSort.fromJsonData(gameSortJson) + decodedDataSorts[#decodedDataSorts + 1] = gameSort + gameSorts[#gameSorts + 1] = gameSortJson.name + + -- get minimum time for the next refresh + if not minExpiryTime then + minExpiryTime = gameSortJson.tokenExpiryInSeconds + else + minExpiryTime = min(minExpiryTime, gameSortJson.tokenExpiryInSeconds) + end + end + store:Dispatch(AddGameSorts(decodedDataSorts)) + store:Dispatch(SetGameSortsInGroup(sortCategory, gameSorts)) + return minExpiryTime + end + return -1 +end + +--[[ + This function will retry for MAX_RETRY_TIME before it + fails and reject with false. + retryTime -- How many time has this request retried +]] +local function fetchToken(networkImpl, store, sortCategory) + return GamesGetSorts(networkImpl, sortCategory):andThen(function(result) + local minExpiryTime = parseSortDataIntoStore(sortCategory, store, result) + + -- there is no data in fetching result + if minExpiryTime < 0 then + store:Dispatch(SetGameSortTokenFetchingStatus(sortCategory, RetrievalStatus.Failed)) + return Promise.reject("No sort data found in response.") + end + + store:Dispatch(SetNextTokenRefreshTime(sortCategory, tick() + minExpiryTime)) + store:Dispatch(SetGameSortTokenFetchingStatus(sortCategory, RetrievalStatus.Done)) + return Promise.resolve() + end, + + -- failure handler for request 'GamesGetSorts' + function() + store:Dispatch(SetGameSortTokenFetchingStatus(sortCategory, RetrievalStatus.Failed)) + return Promise.reject("Request failed.") + end) +end + +--[[ + A thunk fetches the tokens for sorts + networkImpl -- networking object + sortCategory -- HomeGames/Games +]] +return function(networkImpl, sortCategory) + return function(store) + if(store:getState().RequestsStatus.GameSortTokenFetchingStatus[sortCategory] == RetrievalStatus.Fetching) then + return Promise.reject("Data is fetching.") + else + store:Dispatch(SetGameSortTokenFetchingStatus(sortCategory, RetrievalStatus.Fetching)) + end + return fetchToken(networkImpl, store, sortCategory) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUnreadNotificationCount.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUnreadNotificationCount.lua new file mode 100644 index 0000000..9354625 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUnreadNotificationCount.lua @@ -0,0 +1,17 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local GetUnreadNotificationCount = require(Modules.LuaApp.Http.Requests.GetUnreadNotificationCount) +local SetNotificationCount = require(Modules.LuaApp.Actions.SetNotificationCount) + +return function(networkImpl) + return function(store) + return GetUnreadNotificationCount(networkImpl):andThen(function(response) + local responseBody = response.responseBody + local notificationCount = responseBody.unreadNotifications + + store:Dispatch(SetNotificationCount(notificationCount)) + + return notificationCount + end) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUsersData.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUsersData.lua new file mode 100644 index 0000000..4d66816 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUsersData.lua @@ -0,0 +1,17 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Promise = require(Modules.LuaApp.Promise) +local ApiFetchUsersPresences = require(Modules.LuaApp.Thunks.ApiFetchUsersPresences) +local ApiFetchUsersThumbnail = require(Modules.LuaApp.Thunks.ApiFetchUsersThumbnail) + +--this thunk will fill out users list with thumbnail and presence info +return function(networkImpl, userIds, thumbnailRequest) + return function(store) + local fetchedPromises = {} + + table.insert(fetchedPromises, store:Dispatch(ApiFetchUsersPresences(networkImpl, userIds))) + table.insert(fetchedPromises, store:Dispatch(ApiFetchUsersThumbnail(networkImpl, userIds, thumbnailRequest))) + + return Promise.all(fetchedPromises) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUsersFriendCount.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUsersFriendCount.lua new file mode 100644 index 0000000..2be6a8b --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUsersFriendCount.lua @@ -0,0 +1,19 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Actions = Modules.LuaApp.Actions +local Requests = Modules.LuaApp.Http.Requests +local UsersGetFriendCount = require(Requests.UsersGetFriendCount) +local SetFriendCount = require(Actions.SetFriendCount) + +return function(networkImpl) + return function(store) + return UsersGetFriendCount(networkImpl):andThen(function(result) + local data = result.responseBody + + if data.success and data.count then + store:Dispatch(SetFriendCount(data.count)) + end + + return data.count + end) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUsersFriends.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUsersFriends.lua new file mode 100644 index 0000000..85eecb3 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUsersFriends.lua @@ -0,0 +1,34 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Requests = Modules.LuaApp.Http.Requests + +local ApiFetchUsersFriendCount = require(Modules.LuaApp.Thunks.ApiFetchUsersFriendCount) +local ApiFetchUsersData = require(Modules.LuaApp.Thunks.ApiFetchUsersData) +local UsersGetFriends = require(Requests.UsersGetFriends) + +local UserModel = require(Modules.LuaApp.Models.User) +local AddUsers = require(Modules.LuaApp.Actions.AddUsers) + +return function(networkImpl, userId, thumbnailRequests) + return function(store) + return store:Dispatch(ApiFetchUsersFriendCount(networkImpl)):andThen(function() + return UsersGetFriends(networkImpl, userId):andThen(function(response) + local responseBody = response.responseBody + + local userIds = {} + local newUsers = {} + for _, userData in pairs(responseBody.data) do + local id = tostring(userData.id) + local newUser = UserModel.fromData(id, userData.name, true) + + table.insert(userIds, id) + newUsers[newUser.id] = newUser + end + store:Dispatch(AddUsers(newUsers)) + + return userIds + end):andThen(function(userIds) + store:Dispatch(ApiFetchUsersData(networkImpl, userIds, thumbnailRequests)) + end) + end) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUsersPresences.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUsersPresences.lua new file mode 100644 index 0000000..3384ca6 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUsersPresences.lua @@ -0,0 +1,30 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Requests = Modules.LuaApp.Http.Requests +local UsersGetPresence = require(Requests.UsersGetPresence) +local ReceivedUserPresence = require(Modules.LuaChat.Actions.ReceivedUserPresence) + +local User = require(Modules.LuaApp.Models.User) + +local webPresenceMap = { + [0] = User.PresenceType.OFFLINE, + [1] = User.PresenceType.ONLINE, + [2] = User.PresenceType.IN_GAME, + [3] = User.PresenceType.IN_STUDIO +} + +return function(networkImpl, userIds) + return function(store) + return UsersGetPresence(networkImpl, userIds):andThen(function(result) + local responseBody = result.responseBody + + for _, presenceModel in pairs(responseBody.userPresences) do + store:Dispatch(ReceivedUserPresence( + tostring(presenceModel.userId), + webPresenceMap[presenceModel.userPresenceType], + presenceModel.lastLocation, + presenceModel.placeId + )) + end + end) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUsersThumbnail.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUsersThumbnail.lua new file mode 100644 index 0000000..59d5bae --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/ApiFetchUsersThumbnail.lua @@ -0,0 +1,40 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Actions = Modules.LuaApp.Actions +local Requests = Modules.LuaApp.Http.Requests +local UsersGetThumbnail = require(Requests.UsersGetThumbnail) +local SetUserThumbnail = require(Actions.SetUserThumbnail) +local Promise = require(Modules.LuaApp.Promise) + +local function fetchThumbnailsBatch(networkImpl, userIds, thumbnailRequest) + local fetchedPromises = {} + + for _, userId in pairs(userIds) do + table.insert(fetchedPromises, + UsersGetThumbnail(userId, thumbnailRequest.thumbnailType, thumbnailRequest.thumbnailSize) + ) + end + + return Promise.all(fetchedPromises) +end + +return function(networkImpl, userIds, thumbnailRequests) + return function(store) + return Promise.new(function() + -- We currently cannot batch request user avatar thumbnails, + -- so each thumbnailRequest has to be processed individually. + + local fetchedPromises = {} + for _, thumbnailRequest in pairs(thumbnailRequests) do + table.insert(fetchedPromises, + fetchThumbnailsBatch(networkImpl, userIds, thumbnailRequest):andThen(function(result) + for _, data in pairs(result) do + store:Dispatch(SetUserThumbnail(data.id, data.image, data.thumbnailType, data.thumbnailSize)) + end + end) + ) + end + + return Promise.all(fetchedPromises) + end) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/FetchGamesPageData.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/FetchGamesPageData.lua new file mode 100644 index 0000000..dd9ea99 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/FetchGamesPageData.lua @@ -0,0 +1,33 @@ +local CoreGui = game:GetService("CoreGui") +local Modules = CoreGui.RobloxGui.Modules + +local diagCounterPageLoadTimes = settings():GetFVariable("LuaAppsDiagPageLoadTimeGames") +local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus) +local Constants = require(Modules.LuaApp.Constants) +local ApiFetchSortTokens = require(Modules.LuaApp.Thunks.ApiFetchSortTokens) +local SetGamesPageDataStatus = require(Modules.LuaApp.Actions.SetGamesPageDataStatus) +local ApiFetchGamesData = require(Modules.LuaApp.Thunks.ApiFetchGamesData) + +return function(networkImpl, analytics) + + return function(store) + local startTime = tick() + store:dispatch(SetGamesPageDataStatus(RetrievalStatus.Fetching)) + store:dispatch(ApiFetchSortTokens(networkImpl, Constants.GameSortGroups.Games)):andThen( + function() + return store:dispatch(ApiFetchGamesData(networkImpl, Constants.GameSortGroups.Games)) + end + ):andThen( + function(result) + local endTime = tick() + local deltaMs = (endTime - startTime) * 1000 + + analytics.Diag:reportStats(diagCounterPageLoadTimes, deltaMs) + store:dispatch(SetGamesPageDataStatus(RetrievalStatus.Done)) + end, + function(result) + store:dispatch(SetGamesPageDataStatus(RetrievalStatus.Failed)) + end + ) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/FriendshipCreated.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/FriendshipCreated.lua new file mode 100644 index 0000000..6c615df --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/FriendshipCreated.lua @@ -0,0 +1,54 @@ +local CoreGui = game:GetService("CoreGui") +local Players = game:GetService("Players") + +local Modules = CoreGui.RobloxGui.Modules + +local ApiFetchUsersFriendCount = require(Modules.LuaApp.Thunks.ApiFetchUsersFriendCount) +local ApiFetchUsersData = require(Modules.LuaApp.Thunks.ApiFetchUsersData) +local AddUser = require(Modules.LuaApp.Actions.AddUser) +local UserModel = require(Modules.LuaApp.Models.User) + + +local ConversationModel = require(Modules.LuaChat.Models.Conversation) +local ReceivedConversation = require(Modules.LuaChat.Actions.ReceivedConversation) + +local Constants = require(Modules.LuaApp.Constants) + +return function(networking, addedFriendUserId) + return function(store) + return store:Dispatch(ApiFetchUsersFriendCount(networking)):andThen(function() + -- Unfortunately this event does not pass in the username of the new friend. + local username = Players:GetNameFromUserIdAsync(tonumber(addedFriendUserId)) + + local newUser = UserModel.fromData(addedFriendUserId, username, true) + store:Dispatch(AddUser(newUser)) + store:Dispatch(ApiFetchUsersData( + networking, + {addedFriendUserId}, + Constants.AvatarThumbnailRequests.USER_CAROUSEL + )):andThen(function() + + -- LuaChat needs to create a mock 1:1 conversation for new friends + local state = store:GetState() + + local needsMockConversation = true + for _, conversation in pairs(state.ChatAppReducer.Conversations) do + if conversation.conversationType == ConversationModel.Type.ONE_TO_ONE_CONVERSATION then + for _, participantId in ipairs(conversation.participants) do + if participantId == addedFriendUserId then + needsMockConversation = false + break + end + end + end + end + + if needsMockConversation then + local conversation = ConversationModel.fromUser(newUser) + store:Dispatch(ReceivedConversation(conversation)) + end + end) + + end) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateBack.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateBack.lua new file mode 100644 index 0000000..e6de6e2 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateBack.lua @@ -0,0 +1,17 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local ApplyNavigateBack = require(Modules.LuaApp.Actions.ApplyNavigateBack) + +return function(navLockEndTime) + assert(type(navLockEndTime) == "nil" or type(navLockEndTime) == "number", + "NavigateBack thunk expects navLockEndTime to be nil or a number") + + return function(store) + local state = store:getState() + + if state.Navigation.lockTimer > tick() then + return + end + + store:dispatch(ApplyNavigateBack(navLockEndTime)) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateBack.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateBack.spec.lua new file mode 100644 index 0000000..3a2e620 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateBack.spec.lua @@ -0,0 +1,82 @@ +return function() + local NavigateBack = require(script.Parent.NavigateBack) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Rodux = require(Modules.Common.Rodux) + + local AppPage = require(Modules.LuaApp.AppPage) + local AppReducer = require(Modules.LuaApp.AppReducer) + + it("should do nothing if navigation is locked", function() + local store = Rodux.Store.new(AppReducer, { + Navigation = { + history = { + { + { name = AppPage.Games }, + }, + { + { name = AppPage.Games }, + { name = AppPage.GamesList, detail = "Popular" }, + }, + { + { name = AppPage.Games }, + { name = AppPage.GamesList, detail = "Popular" }, + { name = AppPage.GameDetails, detail = "12345" }, + }, + }, + lockTimer = 0, + }, + }) + store:dispatch(NavigateBack(tick() + 1)) + store:dispatch(NavigateBack()) + + local state = store:GetState().Navigation + expect(#state.history).to.equal(2) + expect(#state.history[1]).to.equal(1) + expect(state.history[1][1].name).to.equal(AppPage.Games) + expect(#state.history[2]).to.equal(2) + expect(state.history[2][1].name).to.equal(AppPage.Games) + expect(state.history[2][2].name).to.equal(AppPage.GamesList) + expect(state.history[2][2].detail).to.equal("Popular") + end) + + it("should remove the current route from the history", function() + local store = Rodux.Store.new(AppReducer, { + Navigation = { + history = { + { { name = AppPage.Games } }, + { { name = AppPage.Games }, { name = AppPage.GamesList, detail = "Popular" } }, + }, + lockTimer = 0, + }, + }) + store:dispatch(NavigateBack()) + + local state = store:GetState().Navigation + expect(#state.history).to.equal(1) + expect(#state.history[1]).to.equal(1) + expect(state.history[1][1].name).to.equal(AppPage.Games) + end) + + it("should assert if given a non-nil non-number for navLockEndTime", function() + NavigateBack(nil) + NavigateBack(0) + + expect(function() + NavigateBack("Blargle!") + end).to.throw() + + expect(function() + NavigateBack({}) + end).to.throw() + + expect(function() + NavigateBack(false) + end).to.throw() + + expect(function() + NavigateBack(function() end) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateDown.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateDown.lua new file mode 100644 index 0000000..8d9fcbe --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateDown.lua @@ -0,0 +1,17 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) +local NavigateToRoute = require(Modules.LuaApp.Thunks.NavigateToRoute) + +return function(page, navLockEndTime) + assert(type(page) == "table", "NavigateDown thunk expects page to be a table") + assert(type(navLockEndTime) == "nil" or type(navLockEndTime) == "number", + "NavigateDown thunk expects navLockEndTime to be nil or a number") + + return function(store) + local state = store:getState() + + local currentRoute = state.Navigation.history[#state.Navigation.history] + local newRoute = Immutable.Append(currentRoute, page) + store:dispatch(NavigateToRoute(newRoute, navLockEndTime)) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateDown.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateDown.spec.lua new file mode 100644 index 0000000..a526f89 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateDown.spec.lua @@ -0,0 +1,86 @@ +return function() + local NavigateDown = require(script.Parent.NavigateDown) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Rodux = require(Modules.Common.Rodux) + + local AppPage = require(Modules.LuaApp.AppPage) + local AppReducer = require(Modules.LuaApp.AppReducer) + + it("should do nothing if navigation is locked", function() + local store = Rodux.Store.new(AppReducer, { + Navigation = { + history = { + { { name = AppPage.Home } } + }, + lockTimer = tick() + 1, + }, + }) + store:dispatch(NavigateDown({ name = AppPage.GamesList, detail = "Popular" })) + + local state = store:GetState().Navigation + expect(#state.history).to.equal(1) + expect(#state.history[1]).to.equal(1) + expect(state.history[1][1].name).to.equal(AppPage.Home) + end) + + it("should navigate to new page by appending to the last route", function() + local store = Rodux.Store.new(AppReducer) + store:dispatch(NavigateDown({ name = AppPage.GamesList, detail = "Popular" })) + + local state = store:GetState().Navigation + expect(#state.history).to.equal(2) + expect(#state.history[1]).to.equal(1) + expect(state.history[1][1].name).to.equal(AppPage.Home) + expect(#state.history[2]).to.equal(2) + expect(state.history[2][1].name).to.equal(AppPage.Home) + expect(state.history[2][2].name).to.equal(AppPage.GamesList) + expect(state.history[2][2].detail).to.equal("Popular") + end) + + it("should assert if given a non-table for route", function() + NavigateDown({}) + + expect(function() + NavigateDown(nil) + end).to.throw() + + expect(function() + NavigateDown("Blargle!") + end).to.throw() + + expect(function() + NavigateDown(false) + end).to.throw() + + expect(function() + NavigateDown(0) + end).to.throw() + + expect(function() + NavigateDown(function() end) + end).to.throw() + end) + + it("should assert if given a non-nil non-number for navLockEndTime", function() + NavigateDown({}, nil) + NavigateDown({}, 0) + + expect(function() + NavigateDown({}, "Blargle!") + end).to.throw() + + expect(function() + NavigateDown({}, {}) + end).to.throw() + + expect(function() + NavigateDown({}, false) + end).to.throw() + + expect(function() + NavigateDown({}, function() end) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateSideways.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateSideways.lua new file mode 100644 index 0000000..5e1d872 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateSideways.lua @@ -0,0 +1,18 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) +local NavigateToRoute = require(Modules.LuaApp.Thunks.NavigateToRoute) + +return function(page, navLockEndTime) + assert(type(page) == "table", "NavigateSideways thunk expects page to be a table") + assert(type(navLockEndTime) == "nil" or type(navLockEndTime) == "number", + "NavigateSideways thunk expects navLockEndTime to be nil or a number") + + return function(store) + local state = store:getState() + + local oldRoute = state.Navigation.history[#state.Navigation.history] + local truncatedRoute = Immutable.RemoveFromList(oldRoute, #oldRoute) + local newRoute = Immutable.Append(truncatedRoute, page) + store:dispatch(NavigateToRoute(newRoute, navLockEndTime)) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateSideways.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateSideways.spec.lua new file mode 100644 index 0000000..13e6ba3 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateSideways.spec.lua @@ -0,0 +1,103 @@ +return function() + local NavigateSideways = require(script.Parent.NavigateSideways) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Rodux = require(Modules.Common.Rodux) + + local AppPage = require(Modules.LuaApp.AppPage) + local AppReducer = require(Modules.LuaApp.AppReducer) + + it("should do nothing if navigation is locked", function() + local store = Rodux.Store.new(AppReducer, { + Navigation = { + history = { + { { name = AppPage.Games } }, + { { name = AppPage.Games }, { name = AppPage.GamesList, detail = "Popular" } }, + }, + lockTimer = tick() + 1, + }, + }) + store:dispatch(NavigateSideways({ name = AppPage.GamesList, detail = "Featured" })) + + local state = store:GetState().Navigation + expect(#state.history).to.equal(2) + expect(#state.history[1]).to.equal(1) + expect(state.history[1][1].name).to.equal(AppPage.Games) + expect(#state.history[2]).to.equal(2) + expect(state.history[2][1].name).to.equal(AppPage.Games) + expect(state.history[2][2].name).to.equal(AppPage.GamesList) + expect(state.history[2][2].detail).to.equal("Popular") + end) + + it("should navigate to new page by removing the last element of the current route", function() + local store = Rodux.Store.new(AppReducer, { + Navigation = { + history = { + { { name = AppPage.Games } }, + { { name = AppPage.Games }, { name = AppPage.GamesList, detail = "Popular" } }, + }, + lockTimer = 0, + }, + }) + store:dispatch(NavigateSideways({ name = AppPage.GamesList, detail = "Featured" })) + + local state = store:GetState().Navigation + expect(#state.history).to.equal(3) + expect(#state.history[1]).to.equal(1) + expect(state.history[1][1].name).to.equal(AppPage.Games) + expect(#state.history[2]).to.equal(2) + expect(state.history[2][1].name).to.equal(AppPage.Games) + expect(state.history[2][2].name).to.equal(AppPage.GamesList) + expect(state.history[2][2].detail).to.equal("Popular") + expect(#state.history[3]).to.equal(2) + expect(state.history[3][1].name).to.equal(AppPage.Games) + expect(state.history[3][2].name).to.equal(AppPage.GamesList) + expect(state.history[3][2].detail).to.equal("Featured") + end) + + it("should assert if given a non-table for route", function() + NavigateSideways({}) + + expect(function() + NavigateSideways(nil) + end).to.throw() + + expect(function() + NavigateSideways("Blargle!") + end).to.throw() + + expect(function() + NavigateSideways(false) + end).to.throw() + + expect(function() + NavigateSideways(0) + end).to.throw() + + expect(function() + NavigateSideways(function() end) + end).to.throw() + end) + + it("should assert if given a non-nil non-number for navLockEndTime", function() + NavigateSideways({}, nil) + NavigateSideways({}, 0) + + expect(function() + NavigateSideways({}, "Blargle!") + end).to.throw() + + expect(function() + NavigateSideways({}, {}) + end).to.throw() + + expect(function() + NavigateSideways({}, false) + end).to.throw() + + expect(function() + NavigateSideways({}, function() end) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateToRoute.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateToRoute.lua new file mode 100644 index 0000000..5130f49 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateToRoute.lua @@ -0,0 +1,18 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local ApplyNavigateToRoute = require(Modules.LuaApp.Actions.ApplyNavigateToRoute) + +return function(route, navLockEndTime) + assert(type(route) == "table", "NavigateToRoute thunk expects route to be a table") + assert(type(navLockEndTime) == "nil" or type(navLockEndTime) == "number", + "NavigateToRoute thunk expects navLockEndTime to be nil or a number") + + return function(store) + local state = store:getState() + + if state.Navigation.lockTimer > tick() then + return + end + + store:dispatch(ApplyNavigateToRoute(route, navLockEndTime)) + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateToRoute.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateToRoute.spec.lua new file mode 100644 index 0000000..b74c964 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateToRoute.spec.lua @@ -0,0 +1,89 @@ +return function() + local NavigateToRoute = require(script.Parent.NavigateToRoute) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Rodux = require(Modules.Common.Rodux) + + local AppPage = require(Modules.LuaApp.AppPage) + local AppReducer = require(Modules.LuaApp.AppReducer) + + it("should do nothing if navigation is locked", function() + local store = Rodux.Store.new(AppReducer, { + Navigation = { + history = { + { { name = AppPage.Home } } + }, + lockTimer = tick() + 1, + }, + }) + store:dispatch(NavigateToRoute({ { name = AppPage.Games } })) + + local state = store:GetState().Navigation + expect(#state.history).to.equal(1) + expect(#state.history[1]).to.equal(1) + expect(state.history[1][1].name).to.equal(AppPage.Home) + end) + + it("should navigate to the new route", function() + local store = Rodux.Store.new(AppReducer) + store:dispatch(NavigateToRoute({ + { name = AppPage.Games }, + { name = AppPage.GamesList, detail = "Popular" }, + })) + + local state = store:GetState().Navigation + expect(#state.history).to.equal(2) + expect(#state.history[1]).to.equal(1) + expect(state.history[1][1].name).to.equal(AppPage.Home) + expect(#state.history[2]).to.equal(2) + expect(state.history[2][1].name).to.equal(AppPage.Games) + expect(state.history[2][2].name).to.equal(AppPage.GamesList) + expect(state.history[2][2].detail).to.equal("Popular") + end) + + it("should assert if given a non-table for route", function() + NavigateToRoute({}) + + expect(function() + NavigateToRoute(nil) + end).to.throw() + + expect(function() + NavigateToRoute("Blargle!") + end).to.throw() + + expect(function() + NavigateToRoute(false) + end).to.throw() + + expect(function() + NavigateToRoute(0) + end).to.throw() + + expect(function() + NavigateToRoute(function() end) + end).to.throw() + end) + + it("should assert if given a non-nil non-number for navLockEndTime", function() + NavigateToRoute({}, nil) + NavigateToRoute({}, 0) + + expect(function() + NavigateToRoute({}, "Blargle!") + end).to.throw() + + expect(function() + NavigateToRoute({}, {}) + end).to.throw() + + expect(function() + NavigateToRoute({}, false) + end).to.throw() + + expect(function() + NavigateToRoute({}, function() end) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateUp.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateUp.lua new file mode 100644 index 0000000..79ed545 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateUp.lua @@ -0,0 +1,21 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Immutable = require(Modules.Common.Immutable) +local AppPage = require(Modules.LuaApp.AppPage) +local NavigateToRoute = require(Modules.LuaApp.Thunks.NavigateToRoute) + +return function(navLockEndTime) + assert(type(navLockEndTime) == "nil" or type(navLockEndTime) == "number", + "NavigateUp thunk expects navLockEndTime to be nil or a number") + + return function(store) + local state = store:getState() + + local currentRoute = state.Navigation.history[#state.Navigation.history] + if #currentRoute == 1 then + store:dispatch(NavigateToRoute({ AppPage.Home }, navLockEndTime)) + else + local newRoute = Immutable.RemoveFromList(currentRoute, #currentRoute) + store:dispatch(NavigateToRoute(newRoute, navLockEndTime)) + end + end +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateUp.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateUp.spec.lua new file mode 100644 index 0000000..8bc75ba --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/Thunks/NavigateUp.spec.lua @@ -0,0 +1,82 @@ +return function() + local NavigateUp = require(script.Parent.NavigateUp) + + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local Rodux = require(Modules.Common.Rodux) + + local AppPage = require(Modules.LuaApp.AppPage) + local AppReducer = require(Modules.LuaApp.AppReducer) + + it("should do nothing if navigation is locked", function() + local store = Rodux.Store.new(AppReducer, { + Navigation = { + history = { + { { name = AppPage.Games } }, + { { name = AppPage.Games }, { name = AppPage.GamesList, detail = "Popular" } }, + }, + lockTimer = tick() + 1, + }, + }) + store:dispatch(NavigateUp()) + + local state = store:GetState().Navigation + expect(#state.history).to.equal(2) + expect(#state.history[1]).to.equal(1) + expect(state.history[1][1].name).to.equal(AppPage.Games) + expect(#state.history[2]).to.equal(2) + expect(state.history[2][1].name).to.equal(AppPage.Games) + expect(state.history[2][2].name).to.equal(AppPage.GamesList) + expect(state.history[2][2].detail).to.equal("Popular") + end) + + it("should navigate to new page by removing the last element of the current route", function() + local store = Rodux.Store.new(AppReducer, { + Navigation = { + history = { + { + { name = AppPage.Games }, + { name = AppPage.GamesList, detail = "Popular" }, + { name = AppPage.GameDetails, detail = "12345" }, + }, + }, + lockTimer = 0, + }, + }) + store:dispatch(NavigateUp()) + + local state = store:GetState().Navigation + expect(#state.history).to.equal(2) + expect(#state.history[1]).to.equal(3) + expect(state.history[1][1].name).to.equal(AppPage.Games) + expect(state.history[1][2].name).to.equal(AppPage.GamesList) + expect(state.history[1][2].detail).to.equal("Popular") + expect(state.history[1][3].name).to.equal(AppPage.GameDetails) + expect(state.history[1][3].detail).to.equal("12345") + expect(#state.history[2]).to.equal(2) + expect(state.history[2][1].name).to.equal(AppPage.Games) + expect(state.history[2][2].name).to.equal(AppPage.GamesList) + expect(state.history[2][2].detail).to.equal("Popular") + end) + + it("should assert if given a non-nil non-number for navLockEndTime", function() + NavigateUp(nil) + NavigateUp(0) + + expect(function() + NavigateUp("Blargle!") + end).to.throw() + + expect(function() + NavigateUp({}) + end).to.throw() + + expect(function() + NavigateUp(false) + end).to.throw() + + expect(function() + NavigateUp(function() end) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/getGameCardSize.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/getGameCardSize.lua new file mode 100644 index 0000000..6c2fd40 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/getGameCardSize.lua @@ -0,0 +1,41 @@ +local GAME_CARD_DETAIL_HEIGHT = 60 + +local function getMinCardCountForWidth(width) + if width < 513 then + return 3 + elseif width < 852 then + return 4 + elseif width < 1012 then + return 5 + elseif width < 1172 then + return 6 + elseif width < 1332 then + return 7 + else + return 8 + end +end + + +--[[ + Uses this document: + https://docs.google.com/spreadsheets/d/1CqzYHlCZxRUvY8_FuQJ7yc-H_gSNL0XWwEdDS2pNShA/edit#gid=15226408 + To lookup intended card count for screen size, then uses that to calculate the game card width. +]] +local function getGameCardSize(containerWidth, containerPadding, cardPadding, trailingCardFraction) + assert(type(containerWidth) == "number", "containerWidth (argument 1) must be a number") + assert(type(cardPadding) == "number", "cardPadding (argument 2) must be a number") + assert(type(trailingCardFraction) == "number", "trailingCardFraction (argument 3) must be a number") + + local cardCount = getMinCardCountForWidth(containerWidth + containerPadding) + trailingCardFraction + + -- for example, when we have 3.25 cards, there'll be 3 paddings + -- when we have 3 cards, there'll be 2 paddings + local paddingCount = math.ceil(cardCount) - 1 + local cardWidth = (containerWidth - cardPadding * paddingCount)/cardCount + local cardHeight = cardWidth + GAME_CARD_DETAIL_HEIGHT + + return Vector2.new(cardWidth, cardHeight), cardCount +end + +return getGameCardSize \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/getGameCardSize.spec.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/getGameCardSize.spec.lua new file mode 100644 index 0000000..eba7e13 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/getGameCardSize.spec.lua @@ -0,0 +1,59 @@ +return function() + local Modules = game:GetService("CoreGui").RobloxGui.Modules + + local getGameCardSize = require(Modules.LuaApp.getGameCardSize) + local Constants = require(Modules.LuaApp.Constants) + + describe("getGameCardSize", function() + it("should return accurate game card widths and counts", function() + local _, cardCount = getGameCardSize(320.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.25) + expect(cardCount).to.equal(3.25) + _, cardCount = getGameCardSize(320.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.0) + expect(cardCount).to.equal(3.0) + _, cardCount = getGameCardSize(360.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.25) + expect(cardCount).to.equal(3.25) + _, cardCount = getGameCardSize(375.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.25) + expect(cardCount).to.equal(3.25) + _, cardCount = getGameCardSize(414.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.25) + expect(cardCount).to.equal(3.25) + _, cardCount = getGameCardSize(510.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.25) + expect(cardCount).to.equal(3.25) + _, cardCount = getGameCardSize(513.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.25) + expect(cardCount).to.equal(4.25) + _, cardCount = getGameCardSize(513.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.0) + expect(cardCount).to.equal(4.0) + _, cardCount = getGameCardSize(600.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.25) + expect(cardCount).to.equal(4.25) + _, cardCount = getGameCardSize(692.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.25) + expect(cardCount).to.equal(4.25) + _, cardCount = getGameCardSize(768.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.25) + expect(cardCount).to.equal(4.25) + _, cardCount = getGameCardSize(852.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.25) + expect(cardCount).to.equal(5.25) + _, cardCount = getGameCardSize(1012.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.25) + expect(cardCount).to.equal(6.25) + _, cardCount = getGameCardSize(1024.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.25) + expect(cardCount).to.equal(6.25) + _, cardCount = getGameCardSize(1172.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.25) + expect(cardCount).to.equal(7.25) + _, cardCount = getGameCardSize(1332.0 - Constants.GAME_CAROUSEL_PADDING, + Constants.GAME_CAROUSEL_PADDING, Constants.GAME_CAROUSEL_CHILD_PADDING, 0.25) + expect(cardCount).to.equal(8.25) + end) + end) +end \ No newline at end of file diff --git a/Client2018/content/internal/LuaApp/Modules/LuaApp/getScreenBottomInset.lua b/Client2018/content/internal/LuaApp/Modules/LuaApp/getScreenBottomInset.lua new file mode 100644 index 0000000..b29df65 --- /dev/null +++ b/Client2018/content/internal/LuaApp/Modules/LuaApp/getScreenBottomInset.lua @@ -0,0 +1,15 @@ +local GuiService = game:GetService("GuiService") +local UserInputService = game:GetService("UserInputService") + +local function getScreenBottomInset() + if not _G.__TESTEZ_RUNNING_TEST__ then + local nativeBottomBarHeight = UserInputService.BottomBarSize.Y + local bottomSafeZoneHeight = GuiService:GetSafeZoneOffsets().bottom + -- When nativeBottomBar is hidden(nativeBottomBarHeight == 0), we need to fall back to bottomSafeZoneHeight + return nativeBottomBarHeight == 0 and bottomSafeZoneHeight or nativeBottomBarHeight + else + return 0 + end +end + +return getScreenBottomInset \ No newline at end of file diff --git a/Client2018/content/internal/Mobile/.luacheckrc b/Client2018/content/internal/Mobile/.luacheckrc new file mode 100644 index 0000000..f295c4b --- /dev/null +++ b/Client2018/content/internal/Mobile/.luacheckrc @@ -0,0 +1,67 @@ +stds.roblox = { + read_globals = { + game = { + other_fields = true, + }, + + -- Roblox globals + "script", + + -- Extra functions + "tick", "warn", "spawn", "delay", + "wait", "settings", "UserSettings", "typeof", + + -- Types + "Vector2", "Vector3", + "Color3", + "UDim", "UDim2", + "Ray", + "Rect", + "CFrame", + "Enum", + "Instance", + "TweenInfo", + "Random", + "NumberRange", + "NumberSequence", + "NumberSequenceKeypoint", + "ColorSequence", + "BrickColor", + } +} + +stds.testez = { + read_globals = { + "describe", + "it", "itFOCUS", "itSKIP", + "FOCUS", "SKIP", "HACK_NO_XPCALL", + "expect", + } +} + +ignore = { + "212", -- unused arguments + "421", -- shadowing local variable + "422", -- shadowing argument + "431", -- shadowing upvalue + "432", -- shadowing upvalue argument +} + +std = "lua51+roblox" + +files["**/*.spec.lua"] = { + std = "+testez", + ignore = { "631" }, --Line is too long +} + +files["**/*Locale.lua"] = { + ignore = { "631" }, --Line is too long +} + +files["**/Locales/*.lua"] = { + ignore = { "631" }, --Line is too long +} + +files["**/Legacy/AvatarEditor/*.lua"] = { + ignore = { "121", "122", "211", "213", "612", "614", "631", }, -- Bunch of warnings for legacy code +} diff --git a/Client2018/content/internal/Mobile/LuaAppStarterScript.lua b/Client2018/content/internal/Mobile/LuaAppStarterScript.lua new file mode 100644 index 0000000..c15d8c1 --- /dev/null +++ b/Client2018/content/internal/Mobile/LuaAppStarterScript.lua @@ -0,0 +1,57 @@ +local CoreGui = game:GetService("CoreGui") +local UserInputService = game:GetService("UserInputService") +local CorePackages = game:GetService("CorePackages") + +local Modules = CoreGui.RobloxGui.Modules + +local Roact = require(Modules.Common.Roact) +local App = require(Modules.LuaApp.Components.App) +local LuaErrorReporter = require(Modules.Common.LuaErrorReporter) +local FlagSettings = require(Modules.LuaApp.FlagSettings) + +if not UserSettings().GameSettings:InStudioMode() then + -- listen and report errors + local errorReporter = LuaErrorReporter.new() + errorReporter:setCurrentApp("Mobile") + errorReporter:startQueueTimers() +end + +-- Common Setup +if game.Players.LocalPlayer == nil then + game.Players.PlayerAdded:Wait() +end + +-- Reduce render quality to optimize performance +if settings():GetFFlag("AppShellManagementRefactor4") then + local renderSteppedConnection = nil + renderSteppedConnection = game:GetService("RunService").RenderStepped:connect(function() + if renderSteppedConnection then + renderSteppedConnection:Disconnect() + end + settings().Rendering.QualityLevel = 1 + end) +else + settings().Rendering.QualityLevel = 1 +end + +-- Update LuaApp.FlagSettings using the fact that this script is loaded. +FlagSettings:SetIsLuaAppStarterScriptEnabled(true) + +local root = Roact.createElement(App) +Roact.mount(root, CoreGui, "App") + +-- Run tests when shift+alt+ctrl+T is pressed +UserInputService.InputEnded:connect(function(input, gameProcessed) + if input.UserInputType == Enum.UserInputType.Keyboard and + input.KeyCode == Enum.KeyCode.T and + UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) and + UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) and + UserInputService:IsKeyDown(Enum.KeyCode.LeftAlt) + then + local TestEZ = require(CorePackages.TestEZ) + + TestEZ.run(Modules.LuaApp, function(results) + TestEZ.Reporters.TextReporter.report(results) + end) + end +end) diff --git a/Client2018/content/internal/Mobile/MobileMaster.lua b/Client2018/content/internal/Mobile/MobileMaster.lua new file mode 100644 index 0000000..e160873 --- /dev/null +++ b/Client2018/content/internal/Mobile/MobileMaster.lua @@ -0,0 +1,402 @@ +local CoreGui = game:GetService("CoreGui") +local GuiService = game:GetService("GuiService") +local UserInputService = game:GetService("UserInputService") + +local Modules = CoreGui.RobloxGui.Modules + +local Analytics = require(Modules.Common.Analytics) +local LuaErrorReporter = require(Modules.Common.LuaErrorReporter) +local Create = require(Modules.Mobile.Create) +local Constants = require(Modules.Mobile.Constants) +local MobileAppState = require(Modules.Mobile.AppState) +local AvatarEditorFlags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) +local AppGui = require(Modules.LuaApp.Legacy.AvatarEditor.AppGui) +local getScreenBottomInset = require(Modules.LuaApp.getScreenBottomInset) +local NotificationType = require(Modules.LuaApp.Enum.NotificationType) + +local RefactoringAvatarEditorSetup = AvatarEditorFlags:GetFlag("RefactoringAvatarEditorSetup") +local luaAppLegacyInputDisabledGlobally = settings():GetFFlag('LuaAppLegacyInputDisabledGlobally2') +local EnableLuaEventStreamRelease = settings():GetFFlag('EnableLuaEventStreamRelease') + +local ChatMaster = nil +local AvatarEditorMain = nil + + +local function reportAppReady(analyticsImpl, context) + analyticsImpl.EventStream:setRBXEventStream("appReady", context) +end + +local function notifyAppReady(appName) + spawn(function() + GuiService:BroadcastNotification(appName, NotificationType.APP_READY) + end) + local analyticsImpl = Analytics.new() + reportAppReady(analyticsImpl, appName) +end + +local AvatarEditorSetup = nil +local AppNameEnum = {} +if RefactoringAvatarEditorSetup then + AvatarEditorSetup = require(Modules.Mobile.AvatarEditorSetup) + AppNameEnum = require(Modules.Mobile.AppNameEnum) +else + local AppNames = { + "AvatarEditor", + "Chat", + "ShareGameToChat", + } + + for i = 1, #AppNames do + AppNameEnum[AppNames[i]] = AppNames[i] + end + + setmetatable(AppNameEnum, { + __index = function(self, key) + error(("Invalid AppNameEnum %q"):format(tostring(key))) + end + }) +end + +--This is to cover the sky while loading, and also prevent the sky from flashing in when the global gui inset changes +local screenGui + +if RefactoringAvatarEditorSetup then + AvatarEditorSetup:Initialize(notifyAppReady) +else + +if not UserSettings().GameSettings:InStudioMode() then +screenGui = Create.new "ScreenGui" { + Name = "SkyCoverGui", + DisplayOrder = 1, + + Create.new "Frame" { + Name = "HackHeader", + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 0, UserInputService.NavBarSize.Y+UserInputService.StatusBarSize.Y), + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.BLUE_PRESSED, + }, + Create.new "Frame" { + Name = "HackBody", + Position = UDim2.new(0, 0, 0, UserInputService.NavBarSize.Y+UserInputService.StatusBarSize.Y), + Size = UDim2.new(1, 0, 1, 200), + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.WHITE, + }, +} +else +screenGui = Create.new "ScreenGui" { + Name = "SkyCoverGui", + DisplayOrder = 1, + + Create.new "Frame" { + Name = "HackHeader", + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 0, 64), + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.BLUE_PRESSED, + }, + Create.new "Frame" { + Name = "HackBody", + Position = UDim2.new(0, 0, 0, 64), + Size = UDim2.new(1, 0, 1, 200), + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.WHITE, + }, +} +end +local function adjustScreenGuiLayout() + local headerHeight = UserInputService.NavBarSize.Y+UserInputService.StatusBarSize.Y + screenGui.HackHeader.Size = UDim2.new(1, 0, 0, headerHeight) + screenGui.HackBody.Position = UDim2.new(0, 0, 0, headerHeight) +end +local navBarChanged = UserInputService:GetPropertyChangedSignal("NavBarSize") +navBarChanged:Connect(function() + adjustScreenGuiLayout() +end) +local statusBarChanged = UserInputService:GetPropertyChangedSignal("StatusBarSize") +statusBarChanged:Connect(function() + adjustScreenGuiLayout() +end) + +screenGui.Parent = CoreGui + +--[[ + As long as initializing AvatarEditorMain requires a yield, it has to run in a + spawned task. It is then possible for the user to switch apps in the middle of + initialization. So, openAvatarEditor and closeAvatarEditor first check to see + if it's currently initializing, and if it is, they set a bool indicating whether + to call Start() when initialization is done. +]] +local startAvatarEditorAfterInitializing = false + +end + +local function openChat() + if ChatMaster == nil then + ChatMaster = require(Modules.ChatMaster).new() + end + + ChatMaster:Start() + notifyAppReady(AppNameEnum.Chat) +end + + +local function closeChat() + ChatMaster:Stop() +end + +local function openShareGameToChat(parameters) + if ChatMaster == nil then + ChatMaster = require(Modules.ChatMaster).new() + end + + ChatMaster:Start(ChatMaster.Type.GameShare, parameters) + notifyAppReady(AppNameEnum.ShareGameToChat) +end + + +local function closeShareGameToChat() + ChatMaster:Stop(ChatMaster.Type.GameShare) +end + + +local openAvatarEditor +if not RefactoringAvatarEditorSetup then + openAvatarEditor = function() + startAvatarEditorAfterInitializing = true + end +end + + +local closeAvatarEditor +if not RefactoringAvatarEditorSetup then + closeAvatarEditor = function() + startAvatarEditorAfterInitializing = false + end +end + +if not RefactoringAvatarEditorSetup then + +local function avatarEditorInitialization() + spawn(function() + + local header + local appGui + + if not UserSettings().GameSettings:InStudioMode() then + header = require(Modules.LuaApp.Legacy.AvatarEditor.Header).new("Avatar", + UserInputService.NavBarSize.Y, UserInputService.StatusBarSize.Y) + + local headerHeight = UserInputService.StatusBarSize.Y + UserInputService.NavBarSize.Y + appGui = AppGui( + UDim2.new(0, 0, 0, headerHeight), + UDim2.new(1, 0, 1, -headerHeight)) + + local function updateUIDimensions() + header:SetNavAndStatusBarHeight(UserInputService.NavBarSize.Y, UserInputService.StatusBarSize.Y) + local headerHeight = UserInputService.NavBarSize.Y + UserInputService.StatusBarSize.Y + appGui:setDimensions( + UDim2.new(0, 0, 0, headerHeight), + UDim2.new(1, 0, 1, -headerHeight)) + end + + UserInputService:GetPropertyChangedSignal("NavBarSize"):Connect( updateUIDimensions ) + UserInputService:GetPropertyChangedSignal("StatusBarSize"):Connect( updateUIDimensions ) + else + local navBarHeight = 44 + local statusBarHeight = 20 + + header = require(Modules.LuaApp.Legacy.AvatarEditor.Header).new("Avatar", + navBarHeight, statusBarHeight) + + local headerHeight = navBarHeight + statusBarHeight + appGui = AppGui( + UDim2.new(0, 0, 0, headerHeight), + UDim2.new(1, 0, 1, -headerHeight)) + end + + header.rbx.Parent = appGui.ScreenGui + + AvatarEditorMain = + require(Modules.LuaApp.Legacy.AvatarEditor.AvatarEditorMain) + .new(appGui) + + local function startAvatarEditor() + screenGui.HackBody.Visible = false + AvatarEditorMain:Start() + notifyAppReady(AppNameEnum.AvatarEditor) + end + + if startAvatarEditorAfterInitializing then + startAvatarEditor() + end + + openAvatarEditor = startAvatarEditor + + closeAvatarEditor = function() + screenGui.HackBody.Visible = true + AvatarEditorMain:Stop() + end + end) +end + +if settings():GetFFlag("AppShellManagementRefactor4") then + local hasRunInitialization = false + local renderSteppedConnection = nil + renderSteppedConnection = game:GetService("RunService").RenderStepped:connect(function() + if not hasRunInitialization then + hasRunInitialization = true + if renderSteppedConnection then + renderSteppedConnection:Disconnect() + end + avatarEditorInitialization() + end + end) +else + avatarEditorInitialization() +end + +end + +local function installStudioTestingHooks(store) + local ActionType = require(CoreGui.RobloxGui.Modules.Mobile.ActionType) + print("Testing in Studio") + print("") + print("Use number keys:") + print("1: AvatarEditor") + print("2: Chat") + print("") + + local function onKeyPress(inputObject, gameProcessedEvent) + local actionMap = { + [Enum.KeyCode.One] = function() + store:Dispatch( {type = ActionType.OpenApp, appName = AppNameEnum.AvatarEditor} ) + end; + + [Enum.KeyCode.Two] = function() + store:Dispatch( {type = ActionType.OpenApp, appName = AppNameEnum.Chat} ) + end; + } + + (actionMap[inputObject.KeyCode] or function()end)() + end + + UserInputService.InputBegan:connect(onKeyPress) +end + + +local initMobile + +if not UserSettings().GameSettings:InStudioMode() +then + initMobile = function() + local errorReporter = LuaErrorReporter.new() + errorReporter:setCurrentApp("Mobile") + errorReporter:startQueueTimers() + -- to do : observe app lifecycle changes to disable timers when in background + + if EnableLuaEventStreamRelease then + game:BindToClose(function() + -- there is currently a bug with the EventStream, where the stream is not released + -- by the game engine. This call is a temporary work around until a new api is available. + local analytics = Analytics.new() + analytics.EventStream:releaseRBXEventStream() + end) + end + + local appState = MobileAppState.new() + + local function setGlobalGuiInset() + GuiService:SetGlobalGuiInset(0, 0, 0, getScreenBottomInset()) + end + + setGlobalGuiInset() + + UserInputService:GetPropertyChangedSignal("BottomBarSize"):Connect(setGlobalGuiInset) + + GuiService.SafeZoneOffsetsChanged:Connect(setGlobalGuiInset) + + UserInputService.LegacyInputEventsEnabled = (not luaAppLegacyInputDisabledGlobally) + + appState.store.Changed:Connect( + function(newState, oldState) + if oldState.OpenApp ~= newState.OpenApp then + if newState.OpenApp == AppNameEnum.Chat then + openChat() + end + + if newState.OpenApp == AppNameEnum.AvatarEditor then + if RefactoringAvatarEditorSetup then + AvatarEditorSetup:Open() + else + openAvatarEditor() + end + end + + if newState.OpenApp == AppNameEnum.ShareGameToChat then + openShareGameToChat(newState.Parameters) + end + + if oldState.OpenApp == AppNameEnum.Chat then + closeChat() + end + + if oldState.OpenApp == AppNameEnum.AvatarEditor then + if RefactoringAvatarEditorSetup then + AvatarEditorSetup:Close() + else + closeAvatarEditor() + end + end + + if oldState.OpenApp == AppNameEnum.ShareGameToChat then + closeShareGameToChat() + end + end + + end + ) + end +else + initMobile = function() + local appState = MobileAppState.new() + + GuiService:SetGlobalGuiInset(0, 0, 0, 49) + + appState.store.Changed:Connect( + function(newState, oldState) + if oldState.OpenApp ~= newState.OpenApp then + if newState.OpenApp == AppNameEnum.Chat then + --openChat() + end + + if newState.OpenApp == AppNameEnum.AvatarEditor then + if RefactoringAvatarEditorSetup then + AvatarEditorSetup:Open() + else + openAvatarEditor() + end + end + + if oldState.OpenApp == AppNameEnum.Chat then + --closeChat() + end + + if oldState.OpenApp == AppNameEnum.AvatarEditor then + if RefactoringAvatarEditorSetup then + AvatarEditorSetup:Close() + else + closeAvatarEditor() + end + end + end + end + ) + + installStudioTestingHooks(appState.store) + end +end + +initMobile() + diff --git a/Client2018/content/internal/Mobile/MobileStarterScript.lua b/Client2018/content/internal/Mobile/MobileStarterScript.lua new file mode 100644 index 0000000..6419fe1 --- /dev/null +++ b/Client2018/content/internal/Mobile/MobileStarterScript.lua @@ -0,0 +1,5 @@ + +local scriptContext = game:GetService("ScriptContext") +local RobloxGui = game:GetService("CoreGui"):FindFirstChild("RobloxGui") + +scriptContext:AddCoreScriptLocal("MobileMaster", RobloxGui) diff --git a/Client2018/content/internal/Mobile/Modules/Mobile/ActionType.lua b/Client2018/content/internal/Mobile/Modules/Mobile/ActionType.lua new file mode 100644 index 0000000..85ad65e --- /dev/null +++ b/Client2018/content/internal/Mobile/Modules/Mobile/ActionType.lua @@ -0,0 +1,16 @@ +local ActionTypeNames = { + "OpenApp", +} + +local ActionType = {} +for i = 1, #ActionTypeNames do + ActionType[ActionTypeNames[i]] = ActionTypeNames[i] +end + +setmetatable(ActionType, { + __index = function(self, key) + error(("Invalid ActionType %q"):format(tostring(key))) + end +}) + +return ActionType diff --git a/Client2018/content/internal/Mobile/Modules/Mobile/AppNameEnum.lua b/Client2018/content/internal/Mobile/Modules/Mobile/AppNameEnum.lua new file mode 100644 index 0000000..0e2a86d --- /dev/null +++ b/Client2018/content/internal/Mobile/Modules/Mobile/AppNameEnum.lua @@ -0,0 +1,18 @@ +local AppNames = { + "AvatarEditor", + "Chat", + "ShareGameToChat", +} + +local AppNameEnum = {} +for i = 1, #AppNames do + AppNameEnum[AppNames[i]] = AppNames[i] +end + +setmetatable(AppNameEnum, { + __index = function(self, key) + error(("Invalid AppNameEnum %q"):format(tostring(key))) + end +}) + +return AppNameEnum \ No newline at end of file diff --git a/Client2018/content/internal/Mobile/Modules/Mobile/AppReducer.lua b/Client2018/content/internal/Mobile/Modules/Mobile/AppReducer.lua new file mode 100644 index 0000000..3a05d9e --- /dev/null +++ b/Client2018/content/internal/Mobile/Modules/Mobile/AppReducer.lua @@ -0,0 +1,14 @@ +local Modules = script.Parent.Parent +local Immutable = require(Modules.Common.Immutable) + +return function(state, action) + state = state or {OpenApp = "", Paramters = {}} + + if action.type == "OpenApp" then + state = Immutable.Set(state, "OpenApp", action.appName) + state = Immutable.Set(state, "Parameters", action.parameters) + end + + return state +end + diff --git a/Client2018/content/internal/Mobile/Modules/Mobile/AppState.lua b/Client2018/content/internal/Mobile/Modules/Mobile/AppState.lua new file mode 100644 index 0000000..0b08de3 --- /dev/null +++ b/Client2018/content/internal/Mobile/Modules/Mobile/AppState.lua @@ -0,0 +1,24 @@ +local Modules = script.Parent.Parent + +local AppReducer = require(Modules.Mobile.AppReducer) +local Store = require(Modules.Common.Rodux).Store +local NavigationEventReceiver = require(Modules.Mobile.NavigationEventReceiver) + +local AppState = {} + +function AppState.new() + local state = {} + + state.store = Store.new(AppReducer) + + state.NavigationEventReceiver = NavigationEventReceiver:init(state) + + return state +end + +function AppState:Destruct() + self.store:Destruct() +end + +return AppState + diff --git a/Client2018/content/internal/Mobile/Modules/Mobile/AvatarEditorSetup.lua b/Client2018/content/internal/Mobile/Modules/Mobile/AvatarEditorSetup.lua new file mode 100644 index 0000000..d19c870 --- /dev/null +++ b/Client2018/content/internal/Mobile/Modules/Mobile/AvatarEditorSetup.lua @@ -0,0 +1,217 @@ +local CoreGui = game:GetService("CoreGui") +local UserInputService = game:GetService("UserInputService") +local RunService = game:GetService("RunService") + +local Modules = CoreGui.RobloxGui.Modules + +local AppNameEnum = require(Modules.Mobile.AppNameEnum) +local Create = require(Modules.Mobile.Create) +local Constants = require(Modules.Mobile.Constants) +local AppGui = require(Modules.LuaApp.Legacy.AvatarEditor.AppGui) + +local LuaAppConstants = require(Modules.LuaApp.Constants) + +local AvatarEditorSetup = {} + +function AvatarEditorSetup:Initialize(notifyAppReady, useRoactLuaApp) + --This is to cover the sky while loading, and also prevent the sky from flashing in when the global gui inset changes + local screenGui + + if not UserSettings().GameSettings:InStudioMode() then + screenGui = Create.new "ScreenGui" { + Name = "SkyCoverGui", + DisplayOrder = 1, + + Create.new "Frame" { + Name = "HackHeader", + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 0, UserInputService.NavBarSize.Y+UserInputService.StatusBarSize.Y), + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.BLUE_PRESSED, + }, + Create.new "Frame" { + Name = "HackBody", + Position = UDim2.new(0, 0, 0, UserInputService.NavBarSize.Y+UserInputService.StatusBarSize.Y), + Size = UDim2.new(1, 0, 1, 200), + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.WHITE, + }, + } + else + screenGui = Create.new "ScreenGui" { + Name = "SkyCoverGui", + DisplayOrder = 1, + + Create.new "Frame" { + Name = "HackHeader", + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 0, 64), + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.BLUE_PRESSED, + }, + Create.new "Frame" { + Name = "HackBody", + Position = UDim2.new(0, 0, 0, 64), + Size = UDim2.new(1, 0, 1, 200), + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.WHITE, + }, + } + end + + local function adjustScreenGuiLayout() + local headerHeight = UserInputService.NavBarSize.Y+UserInputService.StatusBarSize.Y + screenGui.HackHeader.Size = UDim2.new(1, 0, 0, headerHeight) + screenGui.HackBody.Position = UDim2.new(0, 0, 0, headerHeight) + end + + local navBarChanged = UserInputService:GetPropertyChangedSignal("NavBarSize") + navBarChanged:Connect(function() + adjustScreenGuiLayout() + end) + + local statusBarChanged = UserInputService:GetPropertyChangedSignal("StatusBarSize") + statusBarChanged:Connect(function() + adjustScreenGuiLayout() + end) + + screenGui.Parent = CoreGui + + if useRoactLuaApp then + screenGui.Enabled = false + end + + --[[ + As long as initializing AvatarEditorMain requires a yield, it has to run in a + spawned task. It is then possible for the user to switch apps in the middle of + initialization. So, openAvatarEditor and closeAvatarEditor first check to see + if it's currently initializing, and if it is, they set a bool indicating whether + to call Start() when initialization is done. + ]] + local startAvatarEditorAfterInitializing = false + + self.openAvatarEditor = function() + startAvatarEditorAfterInitializing = true + end + + self.closeAvatarEditor = function() + startAvatarEditorAfterInitializing = false + end + + local function avatarEditorInitialization() + spawn(function() + self.AppGui = nil + + if not useRoactLuaApp then + local header + if not UserSettings().GameSettings:InStudioMode() then + header = require(Modules.LuaApp.Legacy.AvatarEditor.Header).new("Avatar", + UserInputService.NavBarSize.Y, UserInputService.StatusBarSize.Y) + + local headerHeight = UserInputService.StatusBarSize.Y + UserInputService.NavBarSize.Y + self.AppGui = AppGui( + UDim2.new(0, 0, 0, headerHeight), + UDim2.new(1, 0, 1, -headerHeight)) + + local function updateUIDimensions() + header:SetNavAndStatusBarHeight(UserInputService.NavBarSize.Y, UserInputService.StatusBarSize.Y) + local headerHeight = UserInputService.NavBarSize.Y + UserInputService.StatusBarSize.Y + self:UpdateTopBarHeight(headerHeight) + end + + UserInputService:GetPropertyChangedSignal("NavBarSize"):Connect( updateUIDimensions ) + UserInputService:GetPropertyChangedSignal("StatusBarSize"):Connect( updateUIDimensions ) + else + local navBarHeight = 44 + local statusBarHeight = 20 + + header = require(Modules.LuaApp.Legacy.AvatarEditor.Header).new("Avatar", + navBarHeight, statusBarHeight) + + local headerHeight = navBarHeight + statusBarHeight + self.AppGui = AppGui( + UDim2.new(0, 0, 0, headerHeight), + UDim2.new(1, 0, 1, -headerHeight)) + end + header.rbx.Parent = self.AppGui.ScreenGui + else + -- Sync with default value in LuaApp topbar reducer + local topBarHeight = LuaAppConstants.TOP_BAR_SIZE + self.AppGui = AppGui( + UDim2.new(0, 0, 0, topBarHeight), + UDim2.new(1, 0, 1, -topBarHeight)) + end + + AvatarEditorMain = + require(Modules.LuaApp.Legacy.AvatarEditor.AvatarEditorMain) + .new(self.AppGui) + + local function startAvatarEditor() + if useRoactLuaApp then + screenGui.Enabled = true + end + screenGui.HackBody.Visible = false + AvatarEditorMain:Start() + + -- Staging broadcasting of APP_READY to accomodate for unpredictable + -- delay on the native side. + -- Once Lua tab bar is integrated, there will be no use for this + notifyAppReady(AppNameEnum.AvatarEditor) + end + + if startAvatarEditorAfterInitializing then + startAvatarEditor() + end + + self.openAvatarEditor = startAvatarEditor + + self.closeAvatarEditor = function() + screenGui.HackBody.Visible = true + AvatarEditorMain:Stop() + if useRoactLuaApp then + screenGui.Enabled = false + end + end + end) + end + + if settings():GetFFlag("AppShellManagementRefactor4") then + local hasRunInitialization = false + local renderSteppedConnection = nil + renderSteppedConnection = RunService.RenderStepped:connect(function() + if not hasRunInitialization then + hasRunInitialization = true + if renderSteppedConnection then + renderSteppedConnection:Disconnect() + end + avatarEditorInitialization() + end + end) + else + avatarEditorInitialization() + end +end + +function AvatarEditorSetup:Open() + if not _G.__TESTEZ_RUNNING_TEST__ then + RunService:setThrottleFramerateEnabled(false) + end + self.openAvatarEditor() +end + +function AvatarEditorSetup:Close() + if not _G.__TESTEZ_RUNNING_TEST__ then + RunService:setThrottleFramerateEnabled(true) + end + self.closeAvatarEditor() +end + +function AvatarEditorSetup:UpdateTopBarHeight(topBarHeight) + if self.AppGui then + self.AppGui:setDimensions( + UDim2.new(0, 0, 0, topBarHeight), + UDim2.new(1, 0, 1, -topBarHeight)) + end +end + +return AvatarEditorSetup diff --git a/Client2018/content/internal/Mobile/Modules/Mobile/Constants.lua b/Client2018/content/internal/Mobile/Modules/Mobile/Constants.lua new file mode 100644 index 0000000..b526a27 --- /dev/null +++ b/Client2018/content/internal/Mobile/Modules/Mobile/Constants.lua @@ -0,0 +1,70 @@ +local Constants = { + Color = { + GRAY1 = Color3.fromRGB(25, 25, 25), + GRAY2 = Color3.fromRGB(117, 117, 117), + GRAY3 = Color3.fromRGB(184, 184, 184), + GRAY4 = Color3.fromRGB(227, 227, 227), + GRAY5 = Color3.fromRGB(242, 242, 242), + GRAY6 = Color3.fromRGB(245, 245, 245), + WHITE = Color3.fromRGB(255, 255, 255), + BLUE_PRIMARY = Color3.fromRGB(0, 162, 255), + BLUE_HOVER = Color3.fromRGB(50, 181, 255), + BLUE_PRESSED = Color3.fromRGB(0, 116, 189), + BLUE_DISABLED = Color3.fromRGB(153, 218, 255), + GREEN_PRIMARY = Color3.fromRGB(2, 183, 87), + GREEN_HOVER = Color3.fromRGB(63, 198, 121), + GREEN_PRESSED = Color3.fromRGB(17, 130, 55), + GREEN_DISABLED = Color3.fromRGB(163, 226, 189), + RED_PRIMARY = Color3.fromRGB(226, 35, 26), + RED_NEGATIVE = Color3.fromRGB(216, 104, 104), + RED_HOVER = Color3.fromRGB(226, 118, 118), + RED_PRESSED = Color3.fromRGB(172, 30, 45), + ORANGE_WARNING = Color3.fromRGB(246, 136, 2), + ORANGE_FAVORITE = Color3.fromRGB(246, 183, 2), + BROWN_TIX = Color3.fromRGB(204, 158, 113), + ALPHA_SHADOW_PRIMARY = 0.3, -- Used with Gray1 + ALPHA_SHADOW_HOVER = 0.75, -- Used with Gray1 + CONVERSATION_BACKGROUND = Color3.fromRGB(224, 224, 224), + }, + Header = { + HEIGHT = 64, + }, + Tween = { + PHONE_TWEEN_TIME = 0.25, + PHONE_TWEEN_STYLE = Enum.EasingStyle.Quad, + PHONE_TWEEN_DIRECTION = Enum.EasingDirection.Out, + }, + PresenceType = { + NONE = "NONE", + ONLINE = "ONLINE", + IN_GAME = "IN_GAME", + IN_STUDIO = "IN_STUDIO", + }, + ServerState = { + NONE = "NONE", + CREATING = "CREATING", + CREATED = "CREATED", + }, + ConversationLoadingState = { + NONE = "NONE", + LOADING = "LOADING", + DONE = "DONE" + }, + PresenceColors = { + NONE = nil, --Your code will crash if you render this. Intended. + ONLINE = Color3.fromRGB(0, 162, 255), + IN_GAME = Color3.fromRGB(2, 183, 87), + IN_STUDIO = Color3.fromRGB(246, 136, 2), + }, + Text = { + INPUT_PLACEHOLDER = Color3.fromRGB(189, 189, 189), + INPUT = Color3.fromRGB(25, 25, 25), + POST_TYPING_STATUS_INTERVAL = 3, --How frequently do we POST our typing status if we're still typing + }, + MAX_PARTICIPANT_COUNT = 5, + MIN_PARTICIPANT_COUNT = 2, + -- This value actually comes from iOS, but we are shortcutting actually getting the value from there. + TAB_BAR_SIZE = 49, +} + +return Constants diff --git a/Client2018/content/internal/Mobile/Modules/Mobile/Create.lua b/Client2018/content/internal/Mobile/Modules/Mobile/Create.lua new file mode 100644 index 0000000..3441cf1 --- /dev/null +++ b/Client2018/content/internal/Mobile/Modules/Mobile/Create.lua @@ -0,0 +1,82 @@ +local Create = { + events = {} +} + +--[[ + Merge a list of dictionary tables into one table +]] +function Create.merge(...) + if select("#", ...) == 1 then + return (...) + end + + local new = {} + + for i = 1, select("#", ...) do + for key, value in pairs(select(i, ...)) do + -- Push numeric keys as a list + if (type(key) == "number") then + table.insert(new, value) + else + new[key] = value + end + end + end + + return new +end + +--[[ + Create a new instance with the given type properties. + + Usage: + Create.new "Frame" { + Name = "MyFrame" + } + + -- OR -- + + Create "Frame" { + Name = "MyFrame" + } + + Makes no assumptions about the types of children added. The only requirement + is that the "Parent" property on them can be assigned. +]] +function Create.new(name) + return function(...) + local props = Create.merge(...) + local new = Instance.new(name) + + -- Add properties to this instance; all string keys are property names + for key, value in pairs(props) do + if type(key) == "string" then + assert(key ~= "Parent", "Don't set 'Parent' using Create!") + + new[key] = value + elseif type(key) == "table" then + -- Events use a special-case key + if key == Create.events then + for name, event in pairs(value) do + new[name]:connect(event) + end + end + end + end + + -- Add children after all the properties are set + for _, child in ipairs(props) do + child.Parent = new + end + + return new + end +end + +setmetatable(Create, { + __call = function(self, ...) + return Create.new(...) + end +}) + +return Create \ No newline at end of file diff --git a/Client2018/content/internal/Mobile/Modules/Mobile/NavigationEventReceiver.lua b/Client2018/content/internal/Mobile/Modules/Mobile/NavigationEventReceiver.lua new file mode 100644 index 0000000..77554fb --- /dev/null +++ b/Client2018/content/internal/Mobile/Modules/Mobile/NavigationEventReceiver.lua @@ -0,0 +1,33 @@ +local Modules = script.Parent + +local ActionType = require(Modules.ActionType) + +local NotificationService = game:GetService("NotificationService") +local HttpService = game:GetService("HttpService") + +local NavigationEventReceiver = {} + +function NavigationEventReceiver:init(appState) + + local function onNaviationNotifications(eventData) + local decodedDetail = HttpService:JSONDecode(eventData.detail) + local detailType = decodedDetail.Type or eventData.detailType + if detailType == "Destination" then + if decodedDetail.appName then + appState.store:Dispatch( {type = ActionType.OpenApp, appName = decodedDetail.appName, parameters = decodedDetail.parameters} ) + else + appState.store:Dispatch( {type = ActionType.OpenApp, appName = eventData.detail, parameters = {}} ) + end + end + end + + local function onRobloxEventReceived(eventData) + if eventData.namespace == "Navigations" then + onNaviationNotifications(eventData) + end + end + + NotificationService.RobloxEventReceived:connect(onRobloxEventReceived) +end + +return NavigationEventReceiver diff --git a/Client2018/content/models/AvatarContextMenu/AvatarContextArrow.rbxm b/Client2018/content/models/AvatarContextMenu/AvatarContextArrow.rbxm new file mode 100644 index 0000000..f8cc772 Binary files /dev/null and b/Client2018/content/models/AvatarContextMenu/AvatarContextArrow.rbxm differ diff --git a/Client2018/content/places/AvatarEditor.rbxl b/Client2018/content/places/AvatarEditor.rbxl new file mode 100644 index 0000000..d1fb289 Binary files /dev/null and b/Client2018/content/places/AvatarEditor.rbxl differ diff --git a/Client2018/content/places/ConsoleUnitTest.rbxl b/Client2018/content/places/ConsoleUnitTest.rbxl new file mode 100644 index 0000000..7265c92 Binary files /dev/null and b/Client2018/content/places/ConsoleUnitTest.rbxl differ diff --git a/Client2018/content/places/Lobby3D.rbxl b/Client2018/content/places/Lobby3D.rbxl new file mode 100644 index 0000000..0fea635 Binary files /dev/null and b/Client2018/content/places/Lobby3D.rbxl differ diff --git a/Client2018/content/places/Mobile.rbxl b/Client2018/content/places/Mobile.rbxl new file mode 100644 index 0000000..97849ef Binary files /dev/null and b/Client2018/content/places/Mobile.rbxl differ diff --git a/Client2018/content/places/MobileChatPlace.rbxl b/Client2018/content/places/MobileChatPlace.rbxl new file mode 100644 index 0000000..f087511 Binary files /dev/null and b/Client2018/content/places/MobileChatPlace.rbxl differ diff --git a/Client2018/content/places/MobileGamesPlace.rbxl b/Client2018/content/places/MobileGamesPlace.rbxl new file mode 100644 index 0000000..1cec762 Binary files /dev/null and b/Client2018/content/places/MobileGamesPlace.rbxl differ diff --git a/Client2018/content/places/RhodiumUnitTest.rbxl b/Client2018/content/places/RhodiumUnitTest.rbxl new file mode 100644 index 0000000..5a813eb Binary files /dev/null and b/Client2018/content/places/RhodiumUnitTest.rbxl differ diff --git a/Client2018/content/scripts/CoreScripts/CoreScripts/AvatarContextMenu.lua b/Client2018/content/scripts/CoreScripts/CoreScripts/AvatarContextMenu.lua new file mode 100644 index 0000000..5f0bac0 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/CoreScripts/AvatarContextMenu.lua @@ -0,0 +1,330 @@ +--[[ + // FileName: AvatarContextMenu.lua + // Written by: TheGamer101 + // Description: A context menu to allow users to click on avatars and then interact with that user. +]] + +-- OPTIONS +local DEBUG_MODE = game:GetService("RunService"):IsStudio() -- use this to run as a guest/use in games that don't have AvatarContextMenu. FOR TESTING ONLY! +local isAvatarContextMenuEnabled = false + +-- CONSTANTS +local MAX_CONTEXT_MENU_DISTANCE = 100 + +local OPEN_MENU_TIME = 0.2 +local OPEN_MENU_TWEEN = TweenInfo.new(OPEN_MENU_TIME, Enum.EasingStyle.Quad, Enum.EasingDirection.Out) + +local CLOSE_MENU_TIME = 0.2 +local CLOSE_MENU_TWEEN = TweenInfo.new(CLOSE_MENU_TIME, Enum.EasingStyle.Quad, Enum.EasingDirection.In) + +local LEAVE_MENU_ACTION_NAME = "EscapeAvatarContextMenu" + +local MAX_MOVEMENT_THRESHOLD = 20 + +-- SERVICES +local UserInputService = game:GetService("UserInputService") +local ContextActionService = game:GetService("ContextActionService") +local PlayersService = game:GetService("Players") +local TweenService = game:GetService("TweenService") +local CoreGuiService = game:GetService("CoreGui") +local StarterGui = game:GetService("StarterGui") +local GuiService = game:GetService("GuiService") +local AnalyticsService = game:GetService("AnalyticsService") + +--- SETCORE METHODS +-- These must be registered before we start requiring modules so they are available on the first frame. +-- This hack is ugly because it obscures the stack trace for these set core errors. We should one day just make the LocalPlayer +-- exist on the first frame. +local TempSetCoreQueue = {} + +function QueueSetCoreMethod(methodName, args) + if TempSetCoreQueue[methodName] == nil then + TempSetCoreQueue[methodName] = {} + end + table.insert(TempSetCoreQueue[methodName], args) +end + +StarterGui:RegisterSetCore("AddAvatarContextMenuOption", function(...) QueueSetCoreMethod("AddAvatarContextMenuOption", {...}) end) +StarterGui:RegisterSetCore("RemoveAvatarContextMenuOption", function(...) QueueSetCoreMethod("RemoveAvatarContextMenuOption", {...}) end) + +local hasTrackedAvatarContextMenu = false +StarterGui:RegisterSetCore("SetAvatarContextMenuEnabled", + function(enabled) isAvatarContextMenuEnabled = not not enabled + if isAvatarContextMenuEnabled and not hasTrackedAvatarContextMenu then + hasTrackedAvatarContextMenu = true + AnalyticsService:TrackEvent("Game", "AvatarContextMenuEnabled", "placeId: " .. tostring(game.PlaceId)) + end + end +) + +--- MODULES +local RobloxGui = CoreGuiService:WaitForChild("RobloxGui") +local CoreGuiModules = RobloxGui:WaitForChild("Modules") +local AvatarMenuModules = CoreGuiModules:WaitForChild("AvatarContextMenu") + +local ContextMenuGui = require(AvatarMenuModules:WaitForChild("ContextMenuGui")) +local ContextMenuItemsModule = require(AvatarMenuModules:WaitForChild("ContextMenuItems")) +local ContextMenuUtil = require(AvatarMenuModules:WaitForChild("ContextMenuUtil")) +local SelectedCharacterIndicator = require(AvatarMenuModules:WaitForChild("SelectedCharacterIndicator")) + +--- VARIABLES + +local LocalPlayer = PlayersService.LocalPlayer +while not LocalPlayer do + PlayersService.PlayerAdded:wait() + LocalPlayer = PlayersService.LocalPlayer +end + +-- no avatar context menu for guests +if LocalPlayer.UserId <= 0 and not DEBUG_MODE then return end + +local ContextMenuItems = nil +local ContextMenuFrame = nil + +local ContextMenuOpening = false + +local ContextMenuOpen = false +local SelectedPlayer = nil + +local lastInputObject = nil +local initialScreenPoint = nil + +local hasTouchSwipeInput = nil + +local contextMenuPlayerChangedConn = nil + +ContextMenuFrame = ContextMenuGui:CreateMenuFrame() +ContextMenuItems = ContextMenuItemsModule.new(ContextMenuFrame.Content.ContextActionList) + +-- SetCores have been registered, empty SetCoreQueue +for setCoreMethod, queue in pairs(TempSetCoreQueue) do + for i = 1, #queue do + StarterGui:SetCore(setCoreMethod, unpack(queue[i])) + end +end + +function SetSelectedPlayer(player, dontTween) + if SelectedPlayer == player then return end + SelectedPlayer = player + SelectedCharacterIndicator:ChangeSelectedPlayer(SelectedPlayer) + ContextMenuItems:BuildContextMenuItems(SelectedPlayer) + ContextMenuGui:SwitchToPlayerEntry(SelectedPlayer, dontTween) +end + +function OpenMenu() + ContextMenuOpening = true + + ContextMenuFrame.Visible = true + ContextMenuFrame.Content.ContextActionList.CanvasPosition = Vector2.new(0,0) + ContextMenuFrame.Position = UDim2.new(0.5, 0, 1, ContextMenuFrame.AbsoluteSize.Y) + + contextMenuPlayerChangedConn = ContextMenuGui.SelectedPlayerChanged:connect(function() + SetSelectedPlayer(ContextMenuGui:GetSelectedPlayer()) + end) + + local positionTween = TweenService:Create(ContextMenuFrame, OPEN_MENU_TWEEN, {Position = UDim2.new(0.5, 0, 1 - ContextMenuGui:GetBottomScreenPaddingConstant(), 0)}) + positionTween:Play() + positionTween.Completed:wait() + + ContextMenuOpening = false +end + +function BindMenuActions() + -- Close Menu actions + local closeMenuFunc = function(actionName, inputState, input) + if inputState ~= Enum.UserInputState.Begin then + return + end + ContextActionService:UnbindCoreAction(LEAVE_MENU_ACTION_NAME) + CloseContextMenu() + end + ContextActionService:BindCoreAction(LEAVE_MENU_ACTION_NAME, closeMenuFunc, false, Enum.KeyCode.Escape) + + local menuOpenedCon = nil + menuOpenedCon = GuiService.MenuOpened:connect(function() + menuOpenedCon:disconnect() + closeMenuFunc(nil, Enum.UserInputState.Begin, nil) + end) +end + +function BuildPlayerCarousel(selectedPlayer, worldPoint) + local playersByProximity = {} + local players = PlayersService:GetPlayers() + for i = 1, #players do + if players[i].UserId > 0 then + if players[i] ~= LocalPlayer then + local playerPosition = ContextMenuUtil:GetPlayerPosition(players[i]) + if playerPosition then + local distanceFromClicked = (worldPoint - playerPosition).magnitude + table.insert(playersByProximity, {players[i], distanceFromClicked}) + end + end + end + end + + local function closestPlayerComp(playerA, playerB) + return playerA[2] > playerB[2] + end + table.sort(playersByProximity, closestPlayerComp) + + ContextMenuGui:BuildPlayerCarousel(playersByProximity) +end + +function OpenContextMenu(player, worldPoint) + if ContextMenuOpening or ContextMenuOpen or not isAvatarContextMenuEnabled then + return + end + + ContextMenuOpen = true + BuildPlayerCarousel(player, worldPoint) + ContextMenuUtil:DisablePlayerMovement() + BindMenuActions() + SetSelectedPlayer(player, true) + OpenMenu() +end + +function CloseContextMenu() + GuiService.SelectedCoreObject = nil + ContextMenuUtil:EnablePlayerMovement() + if contextMenuPlayerChangedConn then + contextMenuPlayerChangedConn:disconnect() + end + + local positionTween = TweenService:Create(ContextMenuFrame, CLOSE_MENU_TWEEN, {Position = UDim2.new(0.5, 0, 1, ContextMenuFrame.AbsoluteSize.Y)}) + positionTween:Play() + positionTween.Completed:wait() + + ContextMenuFrame.Visible = false + SetSelectedPlayer(nil) + ContextMenuOpen = false +end +ContextMenuGui:SetCloseMenuFunc(CloseContextMenu) +ContextMenuItems:SetCloseMenuFunc(CloseContextMenu) + +local function isPointInside(point, topLeft, bottomRight) + return (point.X >= topLeft.X and + point.X <= bottomRight.X and + point.Y >= topLeft.Y and + point.Y <= bottomRight.Y) +end + +function PointInSwipeArea(screenPoint) + local topLeft = ContextMenuFrame.AbsolutePosition + + local nameTag = ContextMenuFrame.Content:FindFirstChild("NameTag") + local bottomRight = Vector2.new(topLeft.x + ContextMenuFrame.AbsoluteSize.x, nameTag.AbsolutePosition.y + nameTag.AbsoluteSize.y) + + return isPointInside(screenPoint, topLeft, bottomRight) +end + +function LocalPlayerHasToolEquipped() + if not LocalPlayer.Character then return false end + + for _, child in ipairs(LocalPlayer.Character:GetChildren()) do + if child:IsA("BackpackItem") then + return true + end + end + + return false +end + +function clickedOnPoint(screenPoint) + local camera = workspace.CurrentCamera + if not camera then return end + + if LocalPlayerHasToolEquipped() then return end + + local ray = camera:ScreenPointToRay(screenPoint.X, screenPoint.Y) + ray = Ray.new(ray.Origin, ray.Direction * MAX_CONTEXT_MENU_DISTANCE) + local hitPart, hitPoint = workspace:FindPartOnRay(ray, nil, false, true) + local player = ContextMenuUtil:FindPlayerFromPart(hitPart) + + if player and ((DEBUG_MODE and player ~= LocalPlayer) or (player ~= LocalPlayer and player.UserId > 0)) then + if ContextMenuOpen then + SetSelectedPlayer(player) + else + OpenContextMenu(player, hitPoint) + end + elseif not player and ContextMenuOpen then + CloseContextMenu() + end +end + +function OnUserInput(screenPoint, inputObject) + if inputObject.UserInputState == Enum.UserInputState.Begin and lastInputObject == nil then + lastInputObject = inputObject + initialScreenPoint = screenPoint + elseif lastInputObject == inputObject and inputObject.UserInputState == Enum.UserInputState.Change then + if (screenPoint - initialScreenPoint).magnitude > 5 then + lastInputObject = nil + initialScreenPoint = nil + end + elseif inputObject.UserInputState == Enum.UserInputState.End and lastInputObject == inputObject then + lastInputObject = nil + initialScreenPoint = nil + clickedOnPoint(screenPoint) + end +end + +function OnMouseMoved(screenPoint) + if not ContextMenuOpen and lastInputObject and (screenPoint - initialScreenPoint).magnitude > MAX_MOVEMENT_THRESHOLD then + lastInputObject = nil + initialScreenPoint = nil + end +end + +function trackTouchSwipeInput(inputObject) + if inputObject.UserInputType == Enum.UserInputType.Touch then + if not hasTouchSwipeInput and inputObject.UserInputState == Enum.UserInputState.Begin then + if PointInSwipeArea(inputObject.Position) then + hasTouchSwipeInput = inputObject + end + elseif hasTouchSwipeInput == inputObject and inputObject.UserInputState == Enum.UserInputState.End then + spawn(function() + hasTouchSwipeInput = nil + end) + end + end +end + +local function functionProcessInput(inputObject, gameProcessedEvent) + trackTouchSwipeInput(inputObject) + + if gameProcessedEvent then return end + + if inputObject.UserInputType == Enum.UserInputType.MouseButton1 or + inputObject.UserInputType == Enum.UserInputType.Touch then + OnUserInput(Vector2.new(inputObject.Position.X, inputObject.Position.Y), inputObject) + elseif inputObject.UserInputType == Enum.UserInputType.MouseMovement then + OnMouseMoved(Vector2.new(inputObject.Position.X, inputObject.Position.Y)) + end +end + +UserInputService.InputBegan:Connect(functionProcessInput) +UserInputService.InputChanged:Connect(functionProcessInput) +UserInputService.InputEnded:Connect(functionProcessInput) + +UserInputService.TouchSwipe:Connect(function(swipeDir, numOfTouches, gameProcessedEvent) + if not gameProcessedEvent then return end + if not ContextMenuOpen then return end + if not hasTouchSwipeInput then return end + + local offset = 0 + if swipeDir == Enum.SwipeDirection.Left then + offset = 1 + elseif swipeDir == Enum.SwipeDirection.Right then + offset = -1 + end + + if offset ~= 0 then + ContextMenuGui:OffsetPlayerEntry(offset) + SetSelectedPlayer(ContextMenuGui:GetSelectedPlayer()) + end +end) + +LocalPlayer.FriendStatusChanged:Connect(function(player, friendStatus) + if player and player == SelectedPlayer then + ContextMenuItems:UpdateFriendButton(friendStatus) + end +end) diff --git a/Client2018/content/scripts/CoreScripts/CoreScripts/BlockPlayerPrompt.lua b/Client2018/content/scripts/CoreScripts/CoreScripts/BlockPlayerPrompt.lua new file mode 100644 index 0000000..2a5a423 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/CoreScripts/BlockPlayerPrompt.lua @@ -0,0 +1,173 @@ +--[[ + // Filename: BlockPlayerPrompt.lua + // Version 1.0 + // Written by: TheGamer101 + // Description: Handles prompting the blocking and unblocking of Players. +]]-- + +local StarterGui = game:GetService("StarterGui") +local PlayersService = game:GetService("Players") +local CoreGuiService = game:GetService("CoreGui") + +local RobloxGui = CoreGuiService:WaitForChild("RobloxGui") +local LocalPlayer = PlayersService.LocalPlayer +while LocalPlayer == nil do + PlayersService.ChildAdded:wait() + LocalPlayer = PlayersService.LocalPlayer +end + +local CoreGuiModules = RobloxGui:WaitForChild("Modules") +local PromptCreator = require(CoreGuiModules:WaitForChild("PromptCreator")) +local SocialUtil = require(CoreGuiModules:WaitForChild("SocialUtil")) +local PlayerDropDownModule = require(CoreGuiModules:WaitForChild("PlayerDropDown")) +local BlockingUtility = PlayerDropDownModule:CreateBlockingUtility() + +local THUMBNAIL_URL = "https://www.roblox.com/Thumbs/Avatar.ashx?x=200&y=200&format=png&userId=" +local BUST_THUMBNAIL_URL = "https://www.roblox.com/bust-thumbnail/image?width=420&height=420&format=png&userId=" + +local REGULAR_THUMBNAIL_IMAGE_SIZE = Enum.ThumbnailSize.Size150x150 +local CONSOLE_THUMBNAIL_IMAGE_SIZE = Enum.ThumbnailSize.Size352x352 + +local REGULAR_THUMBNAIL_IMAGE_TYPE = Enum.ThumbnailType.HeadShot +local CONSOLE_THUMBNAIL_IMAGE_TYPE = Enum.ThumbnailType.AvatarThumbnail + +-- fetch and store player block list +PlayerDropDownModule:InitBlockListAsync() + +function createFetchImageFunction(...) + local args = {...} + return function(imageLabel) + spawn(function() + local imageUrl = SocialUtil.GetPlayerImage(unpack(args)) + if imageLabel and imageLabel.Parent then + imageLabel.Image = imageUrl + end + end) + end +end + +function DoPromptBlockPlayer(playerToBlock) + if BlockingUtility:IsPlayerBlockedByUserId(playerToBlock.UserId) then + return + end + + local function promptCompletedCallback(clickedConfirm) + if clickedConfirm then + local successfullyBlocked = BlockingUtility:BlockPlayerAsync(playerToBlock) + if not successfullyBlocked then + while PromptCreator:IsCurrentlyPrompting() do + wait() + end + + PromptCreator:CreatePrompt({ + WindowTitle = "Error Blocking Player", + MainText = string.format("An error occurred while blocking %s. Please try again later.", playerToBlock.Name), + ConfirmationText = "Okay", + CancelActive = false, + Image = BUST_THUMBNAIL_URL ..playerToBlock.UserId, + ImageConsoleVR = THUMBNAIL_URL ..playerToBlock.UserId, + FetchImageFunction = createFetchImageFunction(playerToBlock.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE), + FetchImageFunctionConsoleVR = createFetchImageFunction(playerToBlock.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE), + StripeColor = Color3.fromRGB(183, 34, 54), + }) + end + end + end + PromptCreator:CreatePrompt({ + WindowTitle = "Confirm Block", + MainText = string.format("Are you sure you want to block %s?", playerToBlock.Name), + ConfirmationText = "Block", + CancelText = "Cancel", + CancelActive = true, + Image = BUST_THUMBNAIL_URL ..playerToBlock.UserId, + ImageConsoleVR = THUMBNAIL_URL ..playerToBlock.UserId, + FetchImageFunction = createFetchImageFunction(playerToBlock.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE), + FetchImageFunctionConsoleVR = createFetchImageFunction(playerToBlock.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE), + PromptCompletedCallback = promptCompletedCallback, + }) +end + +function PromptBlockPlayer(player) + if LocalPlayer.UserId < 0 then + error("PromptBlockPlayer can not be called for guests!") + end + if typeof(player) == "Instance" and player:IsA("Player") then + if player.UserId < 0 then + error("PromptBlockPlayer can not be called on guests!") + end + if player == LocalPlayer then + error("PromptBlockPlayer: A user can not block themselves!") + end + DoPromptBlockPlayer(player) + else + error("Invalid argument to PromptBlockPlayer") + end +end + +function DoPromptUnblockPlayer(playerToUnblock) + if not BlockingUtility:IsPlayerBlockedByUserId(playerToUnblock.UserId) and false then + return + end + + local function promptCompletedCallback(clickedConfirm) + if clickedConfirm then + local successfullyUnblocked = BlockingUtility:UnblockPlayerAsync(playerToUnblock) + if not successfullyUnblocked then + while PromptCreator:IsCurrentlyPrompting() do + wait() + end + PromptCreator:CreatePrompt({ + WindowTitle = "Error Unblocking Player", + MainText = string.format("An error occurred while unblocking %s. Please try again later.", playerToUnblock.Name), + ConfirmationText = "Okay", + Image = BUST_THUMBNAIL_URL ..playerToUnblock.UserId, + ImageConsoleVR = THUMBNAIL_URL ..playerToUnblock.UserId, + FetchImageFunction = createFetchImageFunction(playerToUnblock.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE), + FetchImageFunctionConsoleVR = createFetchImageFunction(playerToUnblock.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE), + StripeColor = Color3.fromRGB(183, 34, 54), + }) + end + end + end + + PromptCreator:CreatePrompt({ + WindowTitle = "Confirm Unblock", + MainText = string.format("Would you like to unblock %s?", playerToUnblock.Name), + ConfirmationText = "Unblock", + CancelText = "Cancel", + CancelActive = true, + Image = BUST_THUMBNAIL_URL ..playerToUnblock.UserId, + ImageConsoleVR = THUMBNAIL_URL ..playerToUnblock.UserId, + FetchImageFunction = createFetchImageFunction(playerToUnblock.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE), + FetchImageFunctionConsoleVR = createFetchImageFunction(playerToUnblock.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE), + PromptCompletedCallback = promptCompletedCallback, + }) +end + +function PromptUnblockPlayer(player) + if LocalPlayer.UserId < 0 then + error("PromptUnblockPlayer can not be called for guests!") + end + if typeof(player) == "Instance" and player:IsA("Player") then + if player.UserId < 0 then + error("PromptUnblockPlayer can not be called on guests!") + end + if player == LocalPlayer then + error("PromptUnblockPlayer: A user can not block themselves!") + end + DoPromptUnblockPlayer(player) + else + error("Invalid argument to PromptUnblockPlayer") + end +end + +function GetBlockedUserIds() + if LocalPlayer.UserId < 0 then + error("GetBlockedUserIds can not be called for guests!") + end + return BlockingUtility:GetBlockedUserIdsAsync() +end + +StarterGui:RegisterSetCore("PromptBlockPlayer", PromptBlockPlayer) +StarterGui:RegisterSetCore("PromptUnblockPlayer", PromptUnblockPlayer) +StarterGui:RegisterGetCore("GetBlockedUserIds", GetBlockedUserIds) diff --git a/Client2018/content/scripts/CoreScripts/CoreScripts/ContextActionTouch.lua b/Client2018/content/scripts/CoreScripts/CoreScripts/ContextActionTouch.lua new file mode 100644 index 0000000..64dc5b4 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/CoreScripts/ContextActionTouch.lua @@ -0,0 +1,279 @@ +-- ContextActionTouch.lua +-- Copyright ROBLOX 2014, created by Ben Tkacheff +-- this script controls ui and firing of lua functions that are bound in ContextActionService for touch inputs +-- Essentially a user can bind a lua function to a key code, input type (mousebutton1 etc.) and this + +-- Variables +local contextActionService = game:GetService("ContextActionService") +local userInputService = game:GetService("UserInputService") +local playersService = game:GetService("Players") +local isTouchDevice = userInputService.TouchEnabled +local functionTable = {} +local buttonVector = {} +local buttonScreenGui = nil +local buttonFrame = nil + +local ContextDownImage = "https://www.roblox.com/asset/?id=97166756" +local ContextUpImage = "https://www.roblox.com/asset/?id=97166444" + +local oldTouches = {} + +local buttonPositionTable = { + [1] = UDim2.new(0,123,0,70), + [2] = UDim2.new(0,30,0,60), + [3] = UDim2.new(0,180,0,160), + [4] = UDim2.new(0,85,0,-25), + [5] = UDim2.new(0,185,0,-25), + [6] = UDim2.new(0,185,0,260), + [7] = UDim2.new(0,216,0,65) + } +local maxButtons = #buttonPositionTable + +-- Preload images +game:GetService("ContentProvider"):Preload(ContextDownImage) +game:GetService("ContentProvider"):Preload(ContextUpImage) + +local localPlayer = playersService.LocalPlayer +while not localPlayer do + playersService.ChildAdded:wait() + localPlayer = playersService.LocalPlayer +end + +function createContextActionGui() + if not buttonScreenGui and isTouchDevice then + buttonScreenGui = Instance.new("ScreenGui") + buttonScreenGui.Name = "ContextActionGui" + buttonScreenGui.AncestryChanged:connect(function(child, newParent) + if newParent == nil then + buttonScreenGui = nil + end + end) + + buttonFrame = Instance.new("Frame") + buttonFrame.BackgroundTransparency = 1 + buttonFrame.Size = UDim2.new(0.3,0,0.5,0) + buttonFrame.Position = UDim2.new(0.7,0,0.5,0) + buttonFrame.Name = "ContextButtonFrame" + buttonFrame.Parent = buttonScreenGui + + buttonFrame.Visible = not userInputService.ModalEnabled + userInputService.Changed:connect(function(property) + if property == "ModalEnabled" then + buttonFrame.Visible = not userInputService.ModalEnabled + end + end) + end +end + +-- functions +function setButtonSizeAndPosition(object) + local buttonSize = 55 + local xOffset = 10 + local yOffset = 95 + + -- todo: better way to determine mobile sized screens + local onSmallScreen = (game:GetService("CoreGui").RobloxGui.AbsoluteSize.X < 600) + if not onSmallScreen then + buttonSize = 85 + xOffset = 40 + end + + object.Size = UDim2.new(0,buttonSize,0,buttonSize) +end + +function contextButtonDown(button, inputObject, actionName) + if inputObject.UserInputType == Enum.UserInputType.Touch then + button.Image = ContextDownImage + contextActionService:CallFunction(actionName, Enum.UserInputState.Begin, inputObject) + end +end + +function contextButtonMoved(button, inputObject, actionName) + if inputObject.UserInputType == Enum.UserInputType.Touch then + button.Image = ContextDownImage + contextActionService:CallFunction(actionName, Enum.UserInputState.Change, inputObject) + end +end + +function contextButtonUp(button, inputObject, actionName) + button.Image = ContextUpImage + if inputObject.UserInputType == Enum.UserInputType.Touch and inputObject.UserInputState == Enum.UserInputState.End then + contextActionService:CallFunction(actionName, Enum.UserInputState.End, inputObject) + end +end + +function isSmallScreenDevice() + return game:GetService("GuiService"):GetScreenResolution().y <= 320 +end + + +function createNewButton(actionName, functionInfoTable) + local contextButton = Instance.new("ImageButton") + contextButton.Name = "ContextActionButton" + contextButton.BackgroundTransparency = 1 + contextButton.Size = UDim2.new(0,45,0,45) + contextButton.Active = true + if isSmallScreenDevice() then + contextButton.Size = UDim2.new(0,35,0,35) + end + contextButton.Image = ContextUpImage + contextButton.Parent = buttonFrame + + local currentButtonTouch = nil + + userInputService.InputEnded:connect(function ( inputObject ) + oldTouches[inputObject] = nil + end) + contextButton.InputBegan:connect(function(inputObject) + if oldTouches[inputObject] then return end + + if inputObject.UserInputState == Enum.UserInputState.Begin and currentButtonTouch == nil then + currentButtonTouch = inputObject + contextButtonDown(contextButton, inputObject, actionName) + end + end) + contextButton.InputChanged:connect(function(inputObject) + if oldTouches[inputObject] then return end + if currentButtonTouch ~= inputObject then return end + + contextButtonMoved(contextButton, inputObject, actionName) + end) + contextButton.InputEnded:connect(function(inputObject) + if oldTouches[inputObject] then return end + if currentButtonTouch ~= inputObject then return end + + currentButtonTouch = nil + oldTouches[inputObject] = true + contextButtonUp(contextButton, inputObject, actionName) + end) + + local actionIcon = Instance.new("ImageLabel") + actionIcon.Name = "ActionIcon" + actionIcon.Position = UDim2.new(0.175, 0, 0.175, 0) + actionIcon.Size = UDim2.new(0.65, 0, 0.65, 0) + actionIcon.BackgroundTransparency = 1 + if functionInfoTable["image"] and type(functionInfoTable["image"]) == "string" then + actionIcon.Image = functionInfoTable["image"] + end + actionIcon.Parent = contextButton + + local actionTitle = Instance.new("TextLabel") + actionTitle.Name = "ActionTitle" + actionTitle.Size = UDim2.new(1,0,1,0) + actionTitle.BackgroundTransparency = 1 + actionTitle.Font = Enum.Font.SourceSansBold + actionTitle.TextColor3 = Color3.new(1,1,1) + actionTitle.TextStrokeTransparency = 0 + actionTitle.FontSize = Enum.FontSize.Size18 + actionTitle.TextWrapped = true + actionTitle.Text = "" + if functionInfoTable["title"] and type(functionInfoTable["title"]) == "string" then + actionTitle.Text = functionInfoTable["title"] + end + actionTitle.Parent = contextButton + + return contextButton +end + +function createButton( actionName, functionInfoTable ) + local button = createNewButton(actionName, functionInfoTable) + + local position = nil + for i = 1,#buttonVector do + if buttonVector[i] == "empty" then + position = i + break + end + end + + if not position then + position = #buttonVector + 1 + end + + if position > maxButtons then + return -- todo: let user know we have too many buttons already? + end + + buttonVector[position] = button + functionTable[actionName]["button"] = button + + button.Position = buttonPositionTable[position] + button.Parent = buttonFrame + + if buttonScreenGui and buttonScreenGui.Parent == nil then + buttonScreenGui.Parent = localPlayer.PlayerGui + if not buttonFrame.Parent then + buttonFrame.Parent = buttonScreenGui + end + end +end + +function removeAction(actionName) + if not functionTable[actionName] then return end + + local actionButton = functionTable[actionName]["button"] + + if actionButton then + actionButton.Parent = nil + + for i = 1,#buttonVector do + if buttonVector[i] == actionButton then + buttonVector[i] = "empty" + break + end + end + + actionButton:Destroy() + end + + functionTable[actionName] = nil +end + +function addAction(actionName,createTouchButton,functionInfoTable) + if functionTable[actionName] then + removeAction(actionName) + end + functionTable[actionName] = {functionInfoTable} + if createTouchButton and isTouchDevice then + createContextActionGui() + createButton(actionName, functionInfoTable) + end +end + +-- Connections +contextActionService.BoundActionChanged:connect( function(actionName, changeName, changeTable) + if functionTable[actionName] and changeTable then + local button = functionTable[actionName]["button"] + if button then + if changeName == "image" then + button.ActionIcon.Image = changeTable[changeName] + elseif changeName == "title" then + button.ActionTitle.Text = changeTable[changeName] + elseif changeName == "description" then + -- todo: add description to menu + elseif changeName == "position" then + button.Position = changeTable[changeName] + end + end + end +end) + +contextActionService.BoundActionAdded:connect( function(actionName, createTouchButton, functionInfoTable) + addAction(actionName, createTouchButton, functionInfoTable) +end) + +contextActionService.BoundActionRemoved:connect( function(actionName, functionInfoTable) + removeAction(actionName) +end) + +contextActionService.GetActionButtonEvent:connect( function(actionName) + if functionTable[actionName] then + contextActionService:FireActionButtonFoundSignal(actionName, functionTable[actionName]["button"]) + end +end) + +-- make sure any bound data before we setup connections is handled +local boundActions = contextActionService:GetAllBoundActionInfo() +for actionName, actionData in pairs(boundActions) do + addAction(actionName,actionData["createTouchButton"],actionData) +end diff --git a/Client2018/content/scripts/CoreScripts/CoreScripts/FriendPlayerPrompt.lua b/Client2018/content/scripts/CoreScripts/CoreScripts/FriendPlayerPrompt.lua new file mode 100644 index 0000000..17023eb --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/CoreScripts/FriendPlayerPrompt.lua @@ -0,0 +1,286 @@ +--[[ + // Filename: FriendPlayerPrompt.lua + // Version 1.0 + // Written by: TheGamer101 + // Description: Can prompt a user to send a friend request or unfriend a player. +]]-- + +local StarterGui = game:GetService("StarterGui") +local PlayersService = game:GetService("Players") +local CoreGuiService = game:GetService("CoreGui") +local AnalyticsService = game:GetService("AnalyticsService") + +local RobloxGui = CoreGuiService:WaitForChild("RobloxGui") +local LocalPlayer = PlayersService.LocalPlayer +while LocalPlayer == nil do + PlayersService.ChildAdded:wait() + LocalPlayer = PlayersService.LocalPlayer +end + +local CoreGuiModules = RobloxGui:WaitForChild("Modules") +local PromptCreator = require(CoreGuiModules:WaitForChild("PromptCreator")) +local SocialUtil = require(CoreGuiModules:WaitForChild("SocialUtil")) +local PlayerDropDownModule = require(CoreGuiModules:WaitForChild("PlayerDropDown")) + +local FFlagCoreScriptsUseLocalizationModule = settings():GetFFlag('CoreScriptsUseLocalizationModule') +local FFlagFriendPlayerPromptUseFormatByKey = settings():GetFFlag('FriendPlayerPromptUseFormatByKey') + +local RobloxTranslator +if FFlagCoreScriptsUseLocalizationModule then + RobloxTranslator = require(CoreGuiModules:WaitForChild("RobloxTranslator")) +end + +local THUMBNAIL_URL = "https://www.roblox.com/Thumbs/Avatar.ashx?x=200&y=200&format=png&userId=" +local BUST_THUMBNAIL_URL = "https://www.roblox.com/bust-thumbnail/image?width=420&height=420&format=png&userId=" + +local REGULAR_THUMBNAIL_IMAGE_SIZE = Enum.ThumbnailSize.Size150x150 +local CONSOLE_THUMBNAIL_IMAGE_SIZE = Enum.ThumbnailSize.Size352x352 + +local REGULAR_THUMBNAIL_IMAGE_TYPE = Enum.ThumbnailType.HeadShot +local CONSOLE_THUMBNAIL_IMAGE_TYPE = Enum.ThumbnailType.AvatarThumbnail + +local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end) +local FFlagUseNotificationsLocalization = success and result + +local function LocalizedGetString(key, rtv) + pcall(function() + if FFlagCoreScriptsUseLocalizationModule then + rtv = RobloxTranslator:FormatByKey(key) + else + local LocalizationService = game:GetService("LocalizationService") + local CorescriptLocalization = LocalizationService:GetCorescriptLocalizations()[1] + rtv = CorescriptLocalization:GetString(LocalizationService.RobloxLocaleId, key) + end + end) + return rtv +end + +function createFetchImageFunction(...) + local args = {...} + return function(imageLabel) + spawn(function() + local imageUrl = SocialUtil.GetPlayerImage(unpack(args)) + if imageLabel and imageLabel.Parent then + imageLabel.Icon.Image = imageUrl + end + end) + end +end + +function SendFriendRequest(playerToFriend) + AnalyticsService:ReportCounter("FriendPlayerPrompt-RequestFriendship") + AnalyticsService:TrackEvent("Game", "RequestFriendship", "FriendPlayerPrompt") + + local success = pcall(function() + LocalPlayer:RequestFriendship(playerToFriend) + end) + return success +end + +function AtFriendLimit(player) + local friendCount = PlayerDropDownModule:GetFriendCountAsync(player) + if friendCount == nil then + return false + end + if friendCount >= PlayerDropDownModule:MaxFriendCount() then + return true + end + return false +end + +function DoPromptRequestFriendPlayer(playerToFriend) + if LocalPlayer:IsFriendsWith(playerToFriend.UserId) then + return + end + local function promptCompletedCallback(clickedConfirm) + if clickedConfirm then + if AtFriendLimit(LocalPlayer) then + while PromptCreator:IsCurrentlyPrompting() do + wait() + end + PromptCreator:CreatePrompt({ + WindowTitle = "Friend Limit Reached", + MainText = "You can not send a friend request because you are at the max friend limit.", + ConfirmationText = "Okay", + CancelActive = false, + Image = BUST_THUMBNAIL_URL ..playerToFriend.UserId, + ImageConsoleVR = THUMBNAIL_URL ..playerToFriend.UserId, + FetchImageFunction = createFetchImageFunction(playerToFriend.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE), + FetchImageFunctionConsoleVR = createFetchImageFunction(playerToFriend.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE), + StripeColor = Color3.fromRGB(183, 34, 54), + }) + else + if AtFriendLimit(playerToFriend) then + + local mainText = string.format("You can not send a friend request to %s because they are at the max friend limit.", playerToFriend.Name) + + if FFlagFriendPlayerPromptUseFormatByKey then + mainText = RobloxTranslator:FormatByKey("FriendPlayerPrompt.promptCompletedCallback.AtFriendLimit", {RBX_NAME = playerToFriend.Name}) + else + if FFlagUseNotificationsLocalization then + mainText = string.gsub(LocalizedGetString("FriendPlayerPrompt.promptCompletedCallback.AtFriendLimit",mainText),"{RBX_NAME}",playerToFriend.Name) + end + end + + PromptCreator:CreatePrompt({ + WindowTitle = "Error Sending Friend Request", + MainText = mainText, + ConfirmationText = "Okay", + CancelActive = false, + Image = BUST_THUMBNAIL_URL ..playerToFriend.UserId, + ImageConsoleVR = THUMBNAIL_URL ..playerToFriend.UserId, + FetchImageFunction = createFetchImageFunction(playerToFriend.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE), + FetchImageFunctionConsoleVR = createFetchImageFunction(playerToFriend.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE), + StripeColor = Color3.fromRGB(183, 34, 54), + }) + else + local successfullySentFriendRequest = SendFriendRequest(playerToFriend) + if not successfullySentFriendRequest then + while PromptCreator:IsCurrentlyPrompting() do + wait() + end + + local mainText = string.format("An error occurred while sending %s a friend request. Please try again later.", playerToFriend.Name) + if FFlagFriendPlayerPromptUseFormatByKey then + mainText = RobloxTranslator:FormatByKey("FriendPlayerPrompt.promptCompletedCallback.UnknownError", {RBX_NAME = playerToFriend.Name}) + else + if FFlagUseNotificationsLocalization then + mainText = string.gsub(LocalizedGetString("FriendPlayerPrompt.promptCompletedCallback.UnknownError",mainText),"{RBX_NAME}",playerToFriend.Name) + end + end + + PromptCreator:CreatePrompt({ + WindowTitle = "Error Sending Friend Request", + MainText = mainText, + ConfirmationText = "Okay", + CancelActive = false, + Image = BUST_THUMBNAIL_URL ..playerToFriend.UserId, + ImageConsoleVR = THUMBNAIL_URL ..playerToFriend.UserId, + FetchImageFunction = createFetchImageFunction(playerToFriend.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE), + FetchImageFunctionConsoleVR = createFetchImageFunction(playerToFriend.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE), + StripeColor = Color3.fromRGB(183, 34, 54), + }) + end + end + end + end + end + + local mainText = string.format("Would you like to send %s a Friend Request?", playerToFriend.Name) + + if FFlagFriendPlayerPromptUseFormatByKey then + mainText = RobloxTranslator:FormatByKey("FriendPlayerPrompt.DoPromptRequestFriendPlayer", {RBX_NAME = playerToFriend.Name}) + else + if FFlagUseNotificationsLocalization then + mainText = string.gsub(LocalizedGetString("FriendPlayerPrompt.DoPromptRequestFriendPlayer",mainText),"{RBX_NAME}",playerToFriend.Name) + end + end + + PromptCreator:CreatePrompt({ + WindowTitle = "Send Friend Request?", + MainText = mainText, + ConfirmationText = "Send Request", + CancelText = "Cancel", + CancelActive = true, + Image = BUST_THUMBNAIL_URL ..playerToFriend.UserId, + ImageConsoleVR = THUMBNAIL_URL ..playerToFriend.UserId, + FetchImageFunction = createFetchImageFunction(playerToFriend.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE), + FetchImageFunctionConsoleVR = createFetchImageFunction(playerToFriend.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE), + PromptCompletedCallback = promptCompletedCallback, + }) +end + +function PromptRequestFriendPlayer(player) + if LocalPlayer.UserId < 0 then + error("PromptSendFriendRequest can not be called for guests!") + end + if typeof(player) == "Instance" and player:IsA("Player") then + if player.UserId < 0 then + error("PromptSendFriendRequest can not be called on guests!") + end + if player == LocalPlayer then + error("PromptSendFriendRequest: A user can not friend themselves!") + end + DoPromptRequestFriendPlayer(player) + else + error("Invalid argument to PromptSendFriendRequest") + end +end + +function UnFriendPlayer(playerToUnfriend) + local success = pcall(function() + LocalPlayer:RevokeFriendship(playerToUnfriend) + end) + return success +end + +function DoPromptUnfriendPlayer(playerToUnfriend) + if not LocalPlayer:IsFriendsWith(playerToUnfriend.UserId) then + return + end + local function promptCompletedCallback(clickedConfirm) + if clickedConfirm then + local successfullyUnfriended = UnFriendPlayer(playerToUnfriend) + if not successfullyUnfriended then + while PromptCreator:IsCurrentlyPrompting() do + wait() + end + + local mainText = string.format("An error occurred while unfriending %s. Please try again later.", playerToUnfriend.Name) + if FFlagUseNotificationsLocalization then + mainText = string.gsub(LocalizedGetString("FriendPlayerPrompt.promptCompletedCallback.UnknownError",mainText),"{RBX_NAME}",playerToUnfriend.Name) + end + + PromptCreator:CreatePrompt({ + WindowTitle = "Error Unfriending Player", + MainText = mainText, + ConfirmationText = "Okay", + CancelActive = false, + Image = BUST_THUMBNAIL_URL ..playerToUnfriend.UserId, + ImageConsoleVR = THUMBNAIL_URL ..playerToUnfriend.UserId, + FetchImageFunction = createFetchImageFunction(playerToUnfriend.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE), + FetchImageFunctionConsoleVR = createFetchImageFunction(playerToUnfriend.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE), + StripeColor = Color3.fromRGB(183, 34, 54), + }) + end + end + end + + local mainText = string.format("Would you like to remove %s from your friends list?", playerToUnfriend.Name) + if FFlagUseNotificationsLocalization then + mainText = string.gsub(LocalizedGetString("FriendPlayerPrompt.DoPromptUnfriendPlayer",mainText),"{RBX_NAME}",playerToUnfriend.Name) + end + + PromptCreator:CreatePrompt({ + WindowTitle = "Unfriend Player?", + MainText = mainText, + ConfirmationText = "Unfriend", + CancelText = "Cancel", + CancelActive = true, + Image = BUST_THUMBNAIL_URL ..playerToUnfriend.UserId, + ImageConsoleVR = THUMBNAIL_URL ..playerToUnfriend.UserId, + FetchImageFunction = createFetchImageFunction(playerToUnfriend.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE), + FetchImageFunctionConsoleVR = createFetchImageFunction(playerToUnfriend.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE), + PromptCompletedCallback = promptCompletedCallback, + }) +end + +function PromptUnfriendPlayer(player) + if LocalPlayer.UserId < 0 then + error("PromptUnfriend can not be called for guests!") + end + if typeof(player) == "Instance" and player:IsA("Player") then + if player.UserId < 0 then + error("PromptUnfriend can not be called on guests!") + end + if player == LocalPlayer then + error("PromptUnfriend: A user can not unfriend themselves!") + end + DoPromptUnfriendPlayer(player) + else + error("Invalid argument to PromptUnfriend") + end +end + +StarterGui:RegisterSetCore("PromptSendFriendRequest", PromptRequestFriendPlayer) +StarterGui:RegisterSetCore("PromptUnfriend", PromptUnfriendPlayer) diff --git a/Client2018/content/scripts/CoreScripts/CoreScripts/GamepadMenu.lua b/Client2018/content/scripts/CoreScripts/CoreScripts/GamepadMenu.lua new file mode 100644 index 0000000..2e239b7 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/CoreScripts/GamepadMenu.lua @@ -0,0 +1,1012 @@ +--[[ + Filename: GamepadMenu.lua + Written by: jeditkacheff + Version 1.1 + Description: Controls the radial menu that appears when pressing menu button on gamepad +--]] + +--NOTICE: This file has been branched! If you're implementing changes in this file, please consider also implementing them in the other +--version. + +--[[ SERVICES ]] +local GuiService = game:GetService('GuiService') +local CoreGuiService = game:GetService('CoreGui') +local InputService = game:GetService('UserInputService') +local ContextActionService = game:GetService('ContextActionService') +local HttpService = game:GetService('HttpService') +local StarterGui = game:GetService('StarterGui') +local Players = game:GetService('Players') +local GuiRoot = CoreGuiService:WaitForChild('RobloxGui') +local TextService = game:GetService('TextService') +local VRService = game:GetService('VRService') +--[[ END OF SERVICES ]] + +--[[ MODULES ]] +local tenFootInterface = require(GuiRoot.Modules.TenFootInterface) +local utility = require(GuiRoot.Modules.Settings.Utility) +local recordPage = require(GuiRoot.Modules.Settings.Pages.Record) +local businessLogic = require(GuiRoot.Modules.BusinessLogic) +local Panel3D = require(GuiRoot.Modules.VR.Panel3D) + +--[[ VARIABLES ]] +local gamepadSettingsFrame = nil +local isVisible = false +local smallScreen = utility:IsSmallTouchScreen() +local isTenFootInterface = tenFootInterface:IsEnabled() +local radialButtons = {} +local radialButtonsByName = {} +local lastInputChangedCon = nil +local vrPanel = nil + +--[[ CONSTANTS ]] +local NON_VR_FRAME_HIDDEN_SIZE = UDim2.new(0, 102, 0, 102) +local NON_VR_FRAME_SIZE = UDim2.new(0, 408, 0, 408) + +local VR_FRAME_HIDDEN_SIZE = UDim2.new(0.125, 0, 0.125, 0) +local VR_FRAME_SIZE = UDim2.new(0.75, 0, 0.75, 0) + +local PANEL_SIZE_STUDS = 3 +local PANEL_RESOLUTION = 250 + + +--[[ Fast Flags ]]-- +local function getImagesForSlot(slot) + if slot == 1 then return "rbxasset://textures/ui/Settings/Radial/Top.png", "rbxasset://textures/ui/Settings/Radial/TopSelected.png", + "rbxasset://textures/ui/Settings/Radial/Menu.png", + UDim2.new(0.5,-26,0,18), UDim2.new(0,52,0,41), + UDim2.new(0,150,0,100), UDim2.new(0.5,-75,0,0) + elseif slot == 2 then return "rbxasset://textures/ui/Settings/Radial/TopRight.png", "rbxasset://textures/ui/Settings/Radial/TopRightSelected.png", + "rbxasset://textures/ui/Settings/Radial/PlayerList.png", + UDim2.new(1,-90,0,90), UDim2.new(0,52,0,52), + UDim2.new(0,108,0,150), UDim2.new(1,-110,0,50) + elseif slot == 3 then return "rbxasset://textures/ui/Settings/Radial/BottomRight.png", "rbxasset://textures/ui/Settings/Radial/BottomRightSelected.png", + "rbxasset://textures/ui/Settings/Radial/Alert.png", + UDim2.new(1,-85,1,-150), UDim2.new(0,42,0,58), + UDim2.new(0,120,0,150), UDim2.new(1,-120,1,-200) + elseif slot == 4 then return "rbxasset://textures/ui/Settings/Radial/Bottom.png", "rbxasset://textures/ui/Settings/Radial/BottomSelected.png", + "rbxasset://textures/ui/Settings/Radial/Leave.png", + UDim2.new(0.5,-20,1,-62), UDim2.new(0,55,0,46), + UDim2.new(0,150,0,100), UDim2.new(0.5,-75,1,-100) + elseif slot == 5 then return "rbxasset://textures/ui/Settings/Radial/BottomLeft.png", "rbxasset://textures/ui/Settings/Radial/BottomLeftSelected.png", + "rbxasset://textures/ui/Settings/Radial/Backpack.png", + UDim2.new(0,40,1,-150), UDim2.new(0,44,0,56), + UDim2.new(0,110,0,150), UDim2.new(0,0,0,205) + elseif slot == 6 then return "rbxasset://textures/ui/Settings/Radial/TopLeft.png", "rbxasset://textures/ui/Settings/Radial/TopLeftSelected.png", + "rbxasset://textures/ui/Settings/Radial/Chat.png", + UDim2.new(0,35,0,100), UDim2.new(0,56,0,53), + UDim2.new(0,110,0,150), UDim2.new(0,0,0,50) + end + + return "", "", "", UDim2.new(0,0,0,0), UDim2.new(0,0,0,0) +end + +local vrSlotImages = {} +local vrSlotBackgroundImage = "rbxasset://textures/ui/VR/Radial/SliceBackground.png" +local vrSlotActiveImage = "rbxasset://textures/ui/VR/Radial/SliceActive.png" +local vrSlotDisabledImage = "rbxasset://textures/ui/VR/Radial/SliceDisabled.png" +local vrNumSlots = 8 +for i = 1, vrNumSlots do + vrSlotImages[i] = { + background = vrSlotBackgroundImage, + active = vrSlotActiveImage, + disabled = vrSlotDisabledImage, + rotation = (360 / vrNumSlots) * (i - 1) + } +end +vrSlotImages[1].icon = "rbxasset://textures/ui/Settings/Radial/Menu.png" +vrSlotImages[1].iconPosition = UDim2.new(0.5,-26,0,18) +vrSlotImages[1].iconSize = UDim2.new(0,52,0,41) +vrSlotImages[2].icon = "rbxasset://textures/ui/Settings/Radial/PlayerList.png" +vrSlotImages[2].iconPosition = UDim2.new(0.71, 5, 0.29, -60) +vrSlotImages[2].iconSize = UDim2.new(0, 52, 0, 52) +vrSlotImages[3].icon = "rbxasset://textures/ui/VR/Radial/Icons/Recenter.png" +vrSlotImages[3].iconPosition = UDim2.new(1, -60, 0.5, -25) +vrSlotImages[3].iconSize = UDim2.new(0, 50, 0, 50) +vrSlotImages[4].icon = "rbxasset://textures/ui/Settings/Radial/Alert.png" +vrSlotImages[4].iconPosition = UDim2.new(0.71, 12, 0.71, 5) +vrSlotImages[4].iconSize = UDim2.new(0, 42, 0, 58) +vrSlotImages[5].icon = "rbxasset://textures/ui/Settings/Radial/Leave.png" +vrSlotImages[5].iconPosition = UDim2.new(0.5,-20,1,-58) +vrSlotImages[5].iconSize = UDim2.new(0,55,0,46) +vrSlotImages[6].icon = "rbxasset://textures/ui/VR/Radial/Icons/Backpack.png" +vrSlotImages[6].iconPosition = UDim2.new(0.29, -50, 0.71, 4) +vrSlotImages[6].iconSize = UDim2.new(0, 42, 0, 56) +vrSlotImages[7].icon = "rbxasset://textures/ui/VR/Radial/Icons/2DUI.png" +vrSlotImages[7].iconPosition = UDim2.new(0, 10, 0.5, -25) +vrSlotImages[7].iconSize = UDim2.new(0, 50, 0, 50) +vrSlotImages[8].icon = "rbxasset://textures/ui/Settings/Radial/Chat.png" +vrSlotImages[8].iconPosition = UDim2.new(0.29, -60, 0.29, -52) +vrSlotImages[8].iconSize = UDim2.new(0, 56, 0, 53) + +local radialButtonLayout = { + PlayerList = { Range = { Begin = 36, End = 96 } }, + Notifications = { Range = { Begin = 96, End = 156 } }, + LeaveGame = { Range = { Begin = 156, End = 216 } }, + Backpack = { Range = { Begin = 216, End = 276 } }, + Chat = { Range = { Begin = 276, End = 336 } }, + Settings = { Range = { Begin = 336, End = 36 } }, +} +local vrButtonLayout = { + PlayerList = { Range = { Begin = 22.5, End = 67.5 } }, + Recenter = { Range = { Begin = 67.5, End = 112.5 } }, + Notifications = { Range = { Begin = 112.5, End = 157.5 } }, + LeaveGame = { Range = { Begin = 157.5, End = 202.5 } }, + Backpack = { Range = { Begin = 202.5, End = 247.5 } }, + ToggleUI = { Range = { Begin = 247.5, End = 292.5 } }, + Chat = { Range = { Begin = 292.5, End = 337.5 } }, + Settings = { Range = { Begin = 337.5, End = 22.5 } } +} + +local freezeControllerActionName = "doNothingAction" +local radialSelectActionName = "RadialSelectAction" +local thumbstick2RadialActionName = "Thumbstick2RadialAction" +local radialCancelActionName = "RadialSelectCancel" +local radialAcceptActionName = "RadialSelectAccept" +local toggleMenuActionName = "RBXToggleMenuAction" + +local noOpFunc = function() end +local doGamepadMenuButton = nil +local toggleCoreGuiRadial = nil + +local function getSelectedObjectFromAngle(angle) + local closest = nil + local closestDistance = 30 -- threshold of 30 for selecting the closest radial button + local currentLayout = VRService.VREnabled and vrButtonLayout or radialButtonLayout + for radialKey, buttonLayout in pairs(currentLayout) do + if radialButtons[gamepadSettingsFrame[radialKey]]["Disabled"] == false then + --Check for exact match + if buttonLayout.Range.Begin < buttonLayout.Range.End then + if angle > buttonLayout.Range.Begin and angle <= buttonLayout.Range.End then + return gamepadSettingsFrame[radialKey] + end + else + if angle > buttonLayout.Range.Begin or angle <= buttonLayout.Range.End then + return gamepadSettingsFrame[radialKey] + end + end + --Check if this is the closest button so far + local distanceBegin = math.min(math.abs((buttonLayout.Range.Begin + 360) - angle), math.abs(buttonLayout.Range.Begin - angle)) + local distanceEnd = math.min(math.abs((buttonLayout.Range.End + 360) - angle), math.abs(buttonLayout.Range.End - angle)) + local distance = math.min(distanceBegin, distanceEnd) + if distance < closestDistance then + closestDistance = distance + closest = gamepadSettingsFrame[radialKey] + end + end + end + return closest +end + +local function setSelectedRadialButton(selectedObject) + for button, buttonTable in pairs(radialButtons) do + local isVisible = (button == selectedObject) + button:FindFirstChild("Selected").Visible = isVisible + button:FindFirstChild("RadialLabel").Visible = isVisible + + if VRService.VREnabled then + button.ImageTransparency = isVisible and 1 or 0 + end + end +end + +local function activateSelectedRadialButton() + for button, buttonTable in pairs(radialButtons) do + if button:FindFirstChild("Selected").Visible then + buttonTable["Function"]() + return true + end + end + + return false +end + +local function radialSelectAccept(name, state, input) + if gamepadSettingsFrame.Visible and state == Enum.UserInputState.Begin then + activateSelectedRadialButton() + end +end + +local function radialSelectCancel(name, state, input) + if gamepadSettingsFrame.Visible and state == Enum.UserInputState.Begin then + toggleCoreGuiRadial() + end +end + +local D_PAD_VR_DIRS = { + [Enum.KeyCode.DPadUp] = Vector2.new(0, 1), + [Enum.KeyCode.DPadDown] = Vector2.new(0, -1), + [Enum.KeyCode.DPadRight] = Vector2.new(1, 0), + [Enum.KeyCode.DPadLeft] = Vector2.new(-1, 0) +} + +local function radialSelect(name, state, input) + local inputVector = Vector2.new(0, 0) + + if input.KeyCode == Enum.KeyCode.Thumbstick1 then + inputVector = Vector2.new(input.Position.x, input.Position.y) + elseif input.KeyCode == Enum.KeyCode.DPadUp or input.KeyCode == Enum.KeyCode.DPadDown or input.KeyCode == Enum.KeyCode.DPadLeft or input.KeyCode == Enum.KeyCode.DPadRight then + local D_PAD_BUTTONS = { + [Enum.KeyCode.DPadUp] = false; + [Enum.KeyCode.DPadDown] = false; + [Enum.KeyCode.DPadLeft] = false; + [Enum.KeyCode.DPadRight] = false; + } + + --set D_PAD_BUTTONS status: button down->true, button up->false + local gamepadState = InputService:GetGamepadState(input.UserInputType) + for index, value in ipairs(gamepadState) do + if value.KeyCode == Enum.KeyCode.DPadUp or value.KeyCode == Enum.KeyCode.DPadDown or value.KeyCode == Enum.KeyCode.DPadLeft or value.KeyCode == Enum.KeyCode.DPadRight then + D_PAD_BUTTONS[value.KeyCode] = (value.UserInputState == Enum.UserInputState.Begin) + end + end + + if VRService.VREnabled then + for index, value in pairs(D_PAD_BUTTONS) do + if value then + inputVector = inputVector + D_PAD_VR_DIRS[index] + end + end + else + if D_PAD_BUTTONS[Enum.KeyCode.DPadUp] or D_PAD_BUTTONS[Enum.KeyCode.DPadDown] then + inputVector = D_PAD_BUTTONS[Enum.KeyCode.DPadUp] and Vector2.new(0, 1) or Vector2.new(0, -1) + if D_PAD_BUTTONS[Enum.KeyCode.DPadLeft] then + inputVector = Vector2.new(-1, inputVector.Y) + elseif D_PAD_BUTTONS[Enum.KeyCode.DPadRight] then + inputVector = Vector2.new(1, inputVector.Y) + end + end + end + + inputVector = inputVector.unit + end + + local selectedObject = nil + + if inputVector.magnitude > 0.8 then + + local angle = math.deg(math.atan2(inputVector.X, inputVector.Y)) + if angle < 0 then + angle = angle + 360 + end + + selectedObject = getSelectedObjectFromAngle(angle) + + setSelectedRadialButton(selectedObject) + end +end + +local function unbindAllRadialActions() + GuiService.CoreGuiNavigationEnabled = true + + ContextActionService:UnbindCoreAction(radialSelectActionName) + ContextActionService:UnbindCoreAction(radialCancelActionName) + ContextActionService:UnbindCoreAction(radialAcceptActionName) + ContextActionService:UnbindCoreAction(freezeControllerActionName) + ContextActionService:UnbindCoreAction(thumbstick2RadialActionName) + ContextActionService:UnbindCoreAction(radialAcceptActionName .. "VR") +end + +local function bindAllRadialActions() + GuiService.CoreGuiNavigationEnabled = false + + ContextActionService:BindCoreAction(freezeControllerActionName, noOpFunc, false, Enum.UserInputType.Gamepad1) + ContextActionService:BindCoreAction(radialAcceptActionName, radialSelectAccept, false, Enum.KeyCode.ButtonA) + ContextActionService:BindCoreAction(radialCancelActionName, radialSelectCancel, false, Enum.KeyCode.ButtonB) + ContextActionService:BindCoreAction(radialSelectActionName, radialSelect, false, Enum.KeyCode.Thumbstick1, Enum.KeyCode.DPadUp, Enum.KeyCode.DPadDown, Enum.KeyCode.DPadLeft, Enum.KeyCode.DPadRight) + ContextActionService:BindCoreAction(thumbstick2RadialActionName, noOpFunc, false, Enum.KeyCode.Thumbstick2) + ContextActionService:BindCoreAction(toggleMenuActionName, doGamepadMenuButton, false, Enum.KeyCode.ButtonStart) + + if VRService.VREnabled then + ContextActionService:BindCoreAction(radialAcceptActionName .. "VR", radialSelectAccept, false, Enum.KeyCode.ButtonR2) + end +end + +local function setOverrideMouseIconBehavior(override) + if override then + if InputService:GetLastInputType() == Enum.UserInputType.Gamepad1 then + InputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide + else + InputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceShow + end + else + InputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None + end +end + +toggleCoreGuiRadial = function(goingToSettings) + isVisible = not gamepadSettingsFrame.Visible + + updateGuiVisibility() + + if isVisible then + setOverrideMouseIconBehavior(true) + lastInputChangedCon = InputService.LastInputTypeChanged:connect(function() setOverrideMouseIconBehavior(true) end) + + gamepadSettingsFrame.Visible = isVisible + + local settingsChildren = gamepadSettingsFrame:GetChildren() + for i = 1, #settingsChildren do + if settingsChildren[i]:IsA("GuiButton") then + utility:TweenProperty(settingsChildren[i], "ImageTransparency", 1, 0, 0.1, utility:GetEaseOutQuad(), nil) + end + end + local desiredSize = VRService.VREnabled and VR_FRAME_SIZE or NON_VR_FRAME_SIZE + gamepadSettingsFrame:TweenSizeAndPosition(desiredSize, UDim2.new(0.5,0,0.5,0), + Enum.EasingDirection.Out, Enum.EasingStyle.Back, 0.18, true, + function() + updateGuiVisibility() + end) + else + if lastInputChangedCon ~= nil then + lastInputChangedCon:disconnect() + lastInputChangedCon = nil + end + + local settingsChildren = gamepadSettingsFrame:GetChildren() + for i = 1, #settingsChildren do + if settingsChildren[i]:IsA("GuiButton") then + utility:TweenProperty(settingsChildren[i], "ImageTransparency", 0, 1, 0.1, utility:GetEaseOutQuad(), nil) + end + end + local desiredSize = VRService.VREnabled and VR_FRAME_HIDDEN_SIZE or NON_VR_FRAME_HIDDEN_SIZE + gamepadSettingsFrame:TweenSizeAndPosition(desiredSize, UDim2.new(0.5,0,0.5,0), + Enum.EasingDirection.Out, Enum.EasingStyle.Sine, 0.1, true, + function() + if not VRService.VREnabled then + setOverrideMouseIconBehavior(false) + end + if not goingToSettings and not isVisible then GuiService:SetMenuIsOpen(false) end + gamepadSettingsFrame.Visible = isVisible + + if vrPanel then + vrPanel:SetVisible(false) + end + end) + end + + if isVisible then + setSelectedRadialButton(nil) + GuiService:SetMenuIsOpen(true) + bindAllRadialActions() + else + unbindAllRadialActions() + end + + return gamepadSettingsFrame.Visible +end + +local function setButtonEnabled(button, enabled) + if radialButtons[button]["Disabled"] == not enabled then return end + + if button:FindFirstChild("Selected").Visible == true then + setSelectedRadialButton(nil) + end + + local vrEnabled = VRService.VREnabled + + if enabled then + if vrEnabled then + button.Image = vrSlotBackgroundImage + else + button.Image = string.gsub(button.Image, "rbxasset://textures/ui/Settings/Radial/Empty", "rbxasset://textures/ui/Settings/Radial/") + end + button.ImageTransparency = 0 + button.RadialIcon.ImageTransparency = 0 + else + if vrEnabled then + button.Image = vrSlotDisabledImage + else + button.Image = string.gsub(button.Image, "rbxasset://textures/ui/Settings/Radial/", "rbxasset://textures/ui/Settings/Radial/Empty") + end + button.ImageTransparency = 0 + button.RadialIcon.ImageTransparency = 1 + end + + radialButtons[button]["Disabled"] = not enabled +end + +local function setButtonVisible(button, visible) + button.Visible = visible + if not visible then + setButtonEnabled(button, false) + end +end + +local kidSafeHint = nil; +local function getVRKidSafeHint() + if not kidSafeHint then + local text = businessLogic.GetVisibleAgeForPlayer(Players.LocalPlayer) + local textSize = TextService:GetTextSize(text, 24, Enum.Font.SourceSansBold, Vector2.new(800,800)) + + local bubble = utility:Create'ImageLabel' + { + Name = "AccountTypeBubble"; + Size = UDim2.new(0, textSize.x + 20, 0, 50); + Image = "rbxasset://textures/ui/TopBar/Round.png"; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(10, 10, 10, 10); + ImageTransparency = 0.3; + BackgroundTransparency = 1; + } + bubble.Position = UDim2.new(0.5, -bubble.Size.X.Offset/2, 1, 10); + + local accountTypeTextLabel = utility:Create'TextLabel'{ + Name = "AccountTypeText"; + Text = text; + Size = UDim2.new(1, -20, 1, -20); + Position = UDim2.new(0, 10, 0, 10); + Font = Enum.Font.SourceSansBold; + FontSize = Enum.FontSize.Size24; + BackgroundTransparency = 1; + TextColor3 = Color3.new(1,1,1); + TextYAlignment = Enum.TextYAlignment.Center; + TextXAlignment = Enum.TextXAlignment.Center; + Parent = bubble; + } + kidSafeHint = bubble + end + + return kidSafeHint +end + +local function toggleVR(vrEnabled) + if vrEnabled then + gamepadSettingsFrame.Size = VR_FRAME_SIZE + + vrPanel = Panel3D.Get("GamepadMenu") + vrPanel:SetEnabled(true) + vrPanel:SetVisible(false) + vrPanel:SetCanFade(false) + vrPanel:ResizeStuds(PANEL_SIZE_STUDS, PANEL_SIZE_STUDS, PANEL_RESOLUTION) + vrPanel:SetType(Panel3D.Type.Standard, { CFrame = CFrame.new(0, 0, 0.5) }) + gamepadSettingsFrame.Parent = vrPanel:GetGUI() + + function vrPanel:OnUpdate(dt) + if not vrPanel:IsVisible() then + return + end + + local lookAtPixel = vrPanel.lookAtPixel + local lookAtScale = lookAtPixel / vrPanel.gui.AbsoluteSize + local inputVector = (lookAtScale - Vector2.new(0.5, 0.5)) * 2 + + if inputVector.magnitude > 0.4 and inputVector.magnitude < 0.8 then + local angle = math.deg(math.atan2(inputVector.X, -inputVector.Y)) + if angle < 0 then + angle = angle + 360 + end + + local button = getSelectedObjectFromAngle(angle) + if button then + setSelectedRadialButton(button) + end + end + end + + for button, info in pairs(radialButtons) do + if info.VRSlot then + local slotImages = vrSlotImages[info.VRSlot] + + button.Parent = gamepadSettingsFrame + button.Image = info.Disabled and slotImages.disabled or slotImages.background + button.Rotation = slotImages.rotation + button.RadialIcon.Image = slotImages.icon + button.RadialIcon.Position = UDim2.new(0.5, 0, 0.09, 0) + button.RadialIcon.AnchorPoint = Vector2.new(0.5, 0.5) + button.RadialIcon.Size = slotImages.iconSize + button.RadialIcon.Rotation = -slotImages.rotation + button.RadialLabel.Rotation = -slotImages.rotation + button.RadialLabel.AnchorPoint = Vector2.new(0.5, 0.5) + button.RadialLabel.Position = UDim2.new(0.5, 0, 0.5, 0) + + local selectedImage = button:FindFirstChild("Selected") + if selectedImage then + selectedImage.Image = slotImages.active + end + + button.MouseFrame.Visible = false + end + end + + local healthbarFrame = utility:Create("Frame") { + Parent = gamepadSettingsFrame, + Position = UDim2.new(0.8, 0, 0, 0), + Size = UDim2.new(0, 192, 0, 32), + BackgroundTransparency = 1 + } + + local hint = getVRKidSafeHint() + hint.Parent = gamepadSettingsFrame + + local chatButton = radialButtonsByName.Chat + if chatButton then + setButtonEnabled(chatButton, false) + end + else + gamepadSettingsFrame.Size = NON_VR_FRAME_SIZE + if vrPanel then + vrPanel:SetEnabled(false) + end + vrPanel = nil + for button, info in pairs(radialButtons) do + if info.Slot then + local backgroundImage, activeImage, iconImage, iconPosition, iconSize = getImagesForSlot(info.Slot) + if info.Disabled then + backgroundImage = string.gsub(backgroundImage, "rbxasset://textures/ui/Settings/Radial/", "rbxasset://textures/ui/Settings/Radial/Empty") + end + + button.Image = backgroundImage + button.Rotation = 0 + button.RadialIcon.Position = iconPosition + button.RadialIcon.Size = iconSize + button.RadialIcon.Image = iconImage + button.RadialIcon.Rotation = 0 + button.RadialIcon.AnchorPoint = Vector2.new(0, 0) + + button.MouseFrame.Visible = true + else + button.Parent = nil + end + end + if kidSafeHint then + kidSafeHint.Parent = nil + end + + local chatButton = radialButtonsByName.Chat + if chatButton then + setButtonEnabled(chatButton, not isTenFootInterface) + end + end + + if gamepadSettingsFrame.Visible then + toggleCoreGuiRadial() + end +end + +local emptySelectedImageObject = utility:Create'ImageLabel' +{ + BackgroundTransparency = 1, + Size = UDim2.new(1,0,1,0), + Image = "" +}; + +local function createRadialButton(name, text, slot, vrSlot, disabled, coreGuiType, activateFunc) + local slotImage, selectedSlotImage, slotIcon, + slotIconPosition, slotIconSize, mouseFrameSize, mouseFramePos = getImagesForSlot(slot) + + local radialButton = utility:Create'ImageButton' + { + Name = name, + Position = UDim2.new(0.5,0,0.5,0), + Size = UDim2.new(1,0,1,0), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = slotImage, + ZIndex = 2, + SelectionImageObject = emptySelectedImageObject, + Selectable = false, + Parent = gamepadSettingsFrame + }; + if disabled then + radialButton.Image = string.gsub(radialButton.Image, "rbxasset://textures/ui/Settings/Radial/", "rbxasset://textures/ui/Settings/Radial/Empty") + end + + local selectedRadial = utility:Create'ImageLabel' + { + Name = "Selected", + Position = UDim2.new(0,0,0,0), + Size = UDim2.new(1,0,1,0), + BackgroundTransparency = 1, + Image = selectedSlotImage, + ZIndex = 2, + Visible = false, + Parent = radialButton + }; + + local radialIcon = utility:Create'ImageLabel' + { + Name = "RadialIcon", + Position = slotIconPosition, + Size = slotIconSize, + BackgroundTransparency = 1, + Image = slotIcon, + ZIndex = 3, + ImageTransparency = disabled and 1 or 0, + Parent = radialButton + }; + + local nameLabel = utility:Create'TextLabel' + { + + Size = UDim2.new(0,220,0,50), + Position = UDim2.new(0.5, -110, 0.5, -25), + BackgroundTransparency = 1, + Text = text, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size14, + TextColor3 = Color3.new(1,1,1), + Name = "RadialLabel", + Visible = false, + ZIndex = 3, + Parent = radialButton + }; + if not smallScreen then + nameLabel.FontSize = Enum.FontSize.Size36 + nameLabel.Size = UDim2.new(nameLabel.Size.X.Scale, nameLabel.Size.X.Offset, nameLabel.Size.Y.Scale, nameLabel.Size.Y.Offset + 4) + end + local nameBackgroundImage = utility:Create'ImageLabel' + { + Name = text .. "BackgroundImage", + Size = UDim2.new(1,0,1,0), + Position = UDim2.new(0,0,0,2), + BackgroundTransparency = 1, + Image = "rbxasset://textures/ui/Settings/Radial/RadialLabel@2x.png", + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(24,4,130,42), + ZIndex = 2, + Parent = nameLabel + }; + + local mouseFrame = utility:Create'ImageButton' + { + Name = "MouseFrame", + Position = mouseFramePos, + Size = mouseFrameSize, + ZIndex = 3, + BackgroundTransparency = 1, + SelectionImageObject = emptySelectedImageObject, + Parent = radialButton + }; + + mouseFrame.MouseEnter:connect(function() + if not radialButtons[radialButton]["Disabled"] then + setSelectedRadialButton(radialButton) + end + end) + mouseFrame.MouseLeave:connect(function() + setSelectedRadialButton(nil) + end) + + mouseFrame.MouseButton1Click:connect(function() + if selectedRadial.Visible then + activateFunc() + end + end) + + radialButtons[radialButton] = { ["Function"] = activateFunc, ["Disabled"] = disabled, ["CoreGuiType"] = coreGuiType, ["Slot"] = slot, ["VRSlot"] = vrSlot } + radialButtonsByName[name] = radialButton + return radialButton +end + +local function createGamepadMenuGui() + --If we've already created the gamepadSettingsFrame, don't + --do it again + if gamepadSettingsFrame then + return + end + + gamepadSettingsFrame = utility:Create'Frame' + { + Name = "GamepadSettingsFrame", + Position = UDim2.new(0.5,0,0.5,0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = NON_VR_FRAME_SIZE, + AnchorPoint = Vector2.new(0.5, 0.5), + Visible = false, + Parent = GuiRoot + }; + + --------------------------------- + -------- Settings Menu ---------- + local function settingsFunc() + toggleCoreGuiRadial(true) + local MenuModule = require(GuiRoot.Modules.Settings.SettingsHub) + MenuModule:SetVisibility(true, nil, MenuModule.Instance.GameSettingsPage, true) + end + local settingsRadial = createRadialButton("Settings", "Settings", 1, 1, false, nil, settingsFunc) + settingsRadial.Parent = gamepadSettingsFrame + + --------------------------------- + -------- Player List ------------ + local function playerListFunc() + if VRService.VREnabled then + toggleCoreGuiRadial(true) + local MenuModule = require(GuiRoot.Modules.Settings.SettingsHub) + MenuModule:SetVisibility(true, nil, MenuModule.Instance.PlayersPage, true) + else + local PlayerListModule = require(GuiRoot.Modules.PlayerlistModule) + if PlayerListModule and not PlayerListModule:IsOpen() then + toggleCoreGuiRadial(true) + PlayerListModule:ToggleVisibility() + else + toggleCoreGuiRadial() + end + end + end + local playerListRadial = createRadialButton("PlayerList", "Playerlist", 2, 2, not StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.PlayerList), Enum.CoreGuiType.PlayerList, playerListFunc) + playerListRadial.Parent = gamepadSettingsFrame + + --------------------------------- + -------- Notifications ---------- + local gamepadNotifications = Instance.new("BindableEvent") + gamepadNotifications.Name = "GamepadNotifications" + gamepadNotifications.Parent = script + local notificationsFunc = function() + toggleCoreGuiRadial() + if VRService.VREnabled then + local notificationHub = require(GuiRoot.Modules.VR.NotificationHub) + notificationHub:SetVisible(not notificationHub:IsVisible()) + else + gamepadNotifications:Fire(true) + end + end + local notificationsRadial = createRadialButton("Notifications", "Notifications", 3, 4, false, nil, notificationsFunc) + if isTenFootInterface then + setButtonEnabled(notificationsRadial, false) + end + notificationsRadial.Parent = gamepadSettingsFrame + + --------------------------------- + ---------- Leave Game ----------- + local function leaveGameFunc() + toggleCoreGuiRadial(true) + local MenuModule = require(GuiRoot.Modules.Settings.SettingsHub) + MenuModule:SetVisibility(true, false, require(GuiRoot.Modules.Settings.Pages.LeaveGame), true) + end + local leaveGameRadial = createRadialButton("LeaveGame", "Leave Game", 4, 5, false, nil, leaveGameFunc) + leaveGameRadial.Parent = gamepadSettingsFrame + + --------------------------------- + ---------- Backpack ------------- + local function backpackFunc() + toggleCoreGuiRadial(true) + local BackpackModule = require(GuiRoot.Modules.BackpackScript) + BackpackModule:OpenClose() + end + local backpackRadial = createRadialButton("Backpack", "Backpack", 5, 6, not StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Backpack), Enum.CoreGuiType.Backpack, backpackFunc) + backpackRadial.Parent = gamepadSettingsFrame + + --------------------------------- + ------------ Chat --------------- + local function chatFunc() + toggleCoreGuiRadial() + local ChatModule = require(GuiRoot.Modules.ChatSelector) + ChatModule:ToggleVisibility() + end + local chatRadial = createRadialButton("Chat", "Chat", 6, 8, not StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Chat), Enum.CoreGuiType.Chat, chatFunc) + if isTenFootInterface then + setButtonEnabled(chatRadial, false) + end + chatRadial.Parent = gamepadSettingsFrame + + -------------------------------- + ------ Recenter (VR ONLY) ------ + local function recenterFunc() + toggleCoreGuiRadial() + local RecenterModule = require(GuiRoot.Modules.VR.Recenter) + RecenterModule:SetVisible(not RecenterModule:IsVisible()) + end + local recenterRadial = createRadialButton("Recenter", "Recenter", nil, 3, false, nil, recenterFunc) + + -------------------------------- + ------- 2D UI (VR ONLY) -------- + local function toggleUIFunc() + toggleCoreGuiRadial() + local UserGuiModule = require(GuiRoot.Modules.VR.UserGui) + UserGuiModule:SetVisible(not UserGuiModule:IsVisible()) + end + local toggleUIRadial = createRadialButton("ToggleUI", "Toggle UI", nil, 7, false, nil, toggleUIFunc) + + + --------------------------------- + --------- Close Button ---------- + local closeHintFrame = utility:Create'Frame' + { + Name = "CloseHintFrame", + Position = UDim2.new(1,10,1,10), + Size = UDim2.new(0, 103, 0, 60), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Parent = gamepadSettingsFrame + } + local closeHintImage = utility:Create'ImageLabel' + { + Name = "CloseHint", + Position = UDim2.new(0,0,0.5,0), + Size = UDim2.new(1,0,1,0), + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + Image = "rbxasset://textures/ui/Settings/Help/BButtonDark.png", + Parent = closeHintFrame + } + utility:Create'UIAspectRatioConstraint' + { + AspectRatio = 1, + Parent = closeHintImage + } + if isTenFootInterface then + closeHintImage.Image = "rbxasset://textures/ui/Settings/Help/BButtonDark@2x.png" + closeHintFrame.Size = UDim2.new(0,133,0,90) + end + + local closeHintText = utility:Create'TextLabel' + { + Name = "closeHintText", + Position = UDim2.new(1,0,0.5,0), + Size = UDim2.new(0,43,0,24), + AnchorPoint = Vector2.new(1, 0.5), + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size24, + BackgroundTransparency = 1, + Text = "Back", + TextColor3 = Color3.new(1,1,1), + TextXAlignment = Enum.TextXAlignment.Left, + Parent = closeHintFrame + } + if isTenFootInterface then + closeHintText.FontSize = Enum.FontSize.Size36 + end + + ------------------------------------------ + --------- Stop Recording Button ---------- + --todo: enable this when recording is not a verb + --[[local stopRecordingImage = utility:Create'ImageLabel' + { + Name = "StopRecordingHint", + Position = UDim2.new(0,-100,1,10), + Size = UDim2.new(0,61,0,61), + BackgroundTransparency = 1, + Image = "rbxasset://textures/ui/Settings/Help/YButtonDark.png", + Visible = recordPage:IsRecording(), + Parent = gamepadSettingsFrame + } + local stopRecordingText = utility:Create'TextLabel' + { + Name = "stopRecordingHintText", + Position = UDim2.new(1,10,0.5,-12), + Size = UDim2.new(0,43,0,24), + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size24, + BackgroundTransparency = 1, + Text = "Stop Recording", + TextColor3 = Color3.new(1,1,1), + TextXAlignment = Enum.TextXAlignment.Left, + Parent = stopRecordingImage + } + + recordPage.RecordingChanged:connect(function(isRecording) + stopRecordingImage.Visible = isRecording + end)]] + + GuiService:AddSelectionParent(HttpService:GenerateGUID(false), gamepadSettingsFrame) + + gamepadSettingsFrame:GetPropertyChangedSignal("Visible"):connect(function() + if not gamepadSettingsFrame.Visible then + unbindAllRadialActions() + end + end) + + VRService:GetPropertyChangedSignal("VREnabled"):connect(function() toggleVR(VRService.VREnabled) end) + toggleVR(VRService.VREnabled) +end + +local function isCoreGuiDisabled() + for _, enumItem in pairs(Enum.CoreGuiType:GetEnumItems()) do + if StarterGui:GetCoreGuiEnabled(enumItem) then + return false + end + end + + return true +end + +function updateGuiVisibility() + if VRService.VREnabled and vrPanel and isVisible then + vrPanel:SetVisible(true, true) + end + + local children = gamepadSettingsFrame:GetChildren() + for i = 1, #children do + if children[i]:FindFirstChild("RadialIcon") then + children[i].RadialIcon.Visible = isVisible + end + if children[i]:FindFirstChild("RadialLabel") and not isVisible then + children[i].RadialLabel.Visible = isVisible + end + end +end + +doGamepadMenuButton = function(name, state, input) + if state ~= Enum.UserInputState.Begin then return end + + if game.IsLoaded then + if not toggleCoreGuiRadial() then + unbindAllRadialActions() + end + end +end + +if InputService:GetGamepadConnected(Enum.UserInputType.Gamepad1) then + createGamepadMenuGui() +else + InputService.GamepadConnected:connect(function(gamepadEnum) + if gamepadEnum == Enum.UserInputType.Gamepad1 then + createGamepadMenuGui() + end + end) +end + +local defaultLoadingGuiRemovedConnection = nil +local loadedConnection = nil +local isLoadingGuiRemoved = false +local isPlayerAdded = false + +local function updateRadialMenuActionBinding() + if isLoadingGuiRemoved and isPlayerAdded then + createGamepadMenuGui() + ContextActionService:BindCoreAction(toggleMenuActionName, doGamepadMenuButton, false, Enum.KeyCode.ButtonStart) + end +end + +local function handlePlayerAdded() + loadedConnection:disconnect() + isPlayerAdded = true + updateRadialMenuActionBinding() +end + +loadedConnection = Players.PlayerAdded:connect( + function(plr) + if Players.LocalPlayer and plr == Players.LocalPlayer then + handlePlayerAdded() + end + end +) + +if Players.LocalPlayer then + handlePlayerAdded() +end + +local function handleDefaultLoadingGuiRemoved() + if defaultLoadingGuiRemovedConnection then + defaultLoadingGuiRemovedConnection:disconnect() + end + isLoadingGuiRemoved = true + updateRadialMenuActionBinding() +end + +if game:GetService("ReplicatedFirst"):IsDefaultLoadingGuiRemoved() then + handleDefaultLoadingGuiRemoved() +else + defaultLoadingGuiRemovedConnection = game:GetService("ReplicatedFirst").DefaultLoadingGuiRemoved:connect(handleDefaultLoadingGuiRemoved) +end + +-- some buttons always show/hide depending on platform +local function canChangeButtonVisibleState(buttonType) + if isTenFootInterface then + if buttonType == Enum.CoreGuiType.Chat or buttonType == Enum.CoreGuiType.PlayerList then + return false + end + end + + if VRService.VREnabled then + if buttonType == Enum.CoreGuiType.Chat then + return false + end + end + + return true +end + +StarterGui.CoreGuiChangedSignal:connect(function(coreGuiType, enabled) + for button, buttonTable in pairs(radialButtons) do + local buttonType = buttonTable["CoreGuiType"] + if buttonType then + if coreGuiType == buttonType or coreGuiType == Enum.CoreGuiType.All then + if canChangeButtonVisibleState(buttonType) then + setButtonEnabled(button, enabled) + end + end + end + end +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/CoreScripts/HealthScript.lua b/Client2018/content/scripts/CoreScripts/CoreScripts/HealthScript.lua new file mode 100644 index 0000000..feb931d --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/CoreScripts/HealthScript.lua @@ -0,0 +1,310 @@ +--[[ + This script controls the gui the player sees in regards to his or her health. + Can be turned with Game.StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Health,false) + Copyright ROBLOX 2014. Written by Ben Tkacheff. +--]] + +--------------------------------------------------------------------- +-- Initialize/Variables +while not game do + wait(1/60) +end +while not game:GetService("Players") do + wait(1/60) +end + +local currentHumanoid = nil + +local HealthGui = nil +local lastHealth = 100 +local HealthPercentageForOverlay = 5 +local maxBarTweenTime = 0.3 +local greenColor = Color3.new(0.2, 1, 0.2) +local redColor = Color3.new(1, 0.2, 0.2) +local yellowColor = Color3.new(1, 1, 0.2) + +local guiEnabled = false +local healthChangedConnection = nil +local humanoidDiedConnection = nil +local characterAddedConnection = nil + +local greenBarImage = "rbxasset://textures/ui/Health-BKG-Center.png" +local greenBarImageLeft = "rbxasset://textures/ui/Health-BKG-Left-Cap.png" +local greenBarImageRight = "rbxasset://textures/ui/Health-BKG-Right-Cap.png" +local hurtOverlayImage = "https://www.roblox.com/asset/?id=34854607" + +game:GetService("ContentProvider"):Preload(greenBarImage) +game:GetService("ContentProvider"):Preload(hurtOverlayImage) + +while not game:GetService("Players").LocalPlayer do + wait(1/60) +end + +--------------------------------------------------------------------- +-- Functions + +local capHeight = 15 +local capWidth = 7 + +function CreateGui() + if HealthGui and #HealthGui:GetChildren() > 0 then + HealthGui.Parent = game:GetService("CoreGui").RobloxGui + return + end + + local hurtOverlay = Instance.new("ImageLabel") + hurtOverlay.Name = "HurtOverlay" + hurtOverlay.BackgroundTransparency = 1 + hurtOverlay.Image = hurtOverlayImage + hurtOverlay.Position = UDim2.new(-10,0,-10,0) + hurtOverlay.Size = UDim2.new(20,0,20,0) + hurtOverlay.Visible = false + hurtOverlay.Parent = HealthGui + + local healthFrame = Instance.new("Frame") + healthFrame.Name = "HealthFrame" + healthFrame.BackgroundTransparency = 1 + healthFrame.BackgroundColor3 = Color3.new(1,1,1) + healthFrame.BorderColor3 = Color3.new(0,0,0) + healthFrame.BorderSizePixel = 0 + healthFrame.Position = UDim2.new(0.5,-85,1,-20) + healthFrame.Size = UDim2.new(0,170,0,capHeight) + healthFrame.Parent = HealthGui + + + local healthBarBackCenter = Instance.new("ImageLabel") + healthBarBackCenter.Name = "healthBarBackCenter" + healthBarBackCenter.BackgroundTransparency = 1 + healthBarBackCenter.Image = greenBarImage + healthBarBackCenter.Size = UDim2.new(1,-capWidth*2,1,0) + healthBarBackCenter.Position = UDim2.new(0,capWidth,0,0) + healthBarBackCenter.Parent = healthFrame + healthBarBackCenter.ImageColor3 = Color3.new(1,1,1) + + local healthBarBackLeft = Instance.new("ImageLabel") + healthBarBackLeft.Name = "healthBarBackLeft" + healthBarBackLeft.BackgroundTransparency = 1 + healthBarBackLeft.Image = greenBarImageLeft + healthBarBackLeft.Size = UDim2.new(0,capWidth,1,0) + healthBarBackLeft.Position = UDim2.new(0,0,0,0) + healthBarBackLeft.Parent = healthFrame + healthBarBackLeft.ImageColor3 = Color3.new(1,1,1) + + local healthBarBackRight = Instance.new("ImageLabel") + healthBarBackRight.Name = "healthBarBackRight" + healthBarBackRight.BackgroundTransparency = 1 + healthBarBackRight.Image = greenBarImageRight + healthBarBackRight.Size = UDim2.new(0,capWidth,1,0) + healthBarBackRight.Position = UDim2.new(1,-capWidth,0,0) + healthBarBackRight.Parent = healthFrame + healthBarBackRight.ImageColor3 = Color3.new(1,1,1) + + + local healthBar = Instance.new("Frame") + healthBar.Name = "HealthBar" + healthBar.BackgroundTransparency = 1 + healthBar.BackgroundColor3 = Color3.new(1,1,1) + healthBar.BorderColor3 = Color3.new(0,0,0) + healthBar.BorderSizePixel = 0 + healthBar.ClipsDescendants = true + healthBar.Position = UDim2.new(0, 0, 0, 0) + healthBar.Size = UDim2.new(1,0,1,0) + healthBar.Parent = healthFrame + + + local healthBarCenter = Instance.new("ImageLabel") + healthBarCenter.Name = "healthBarCenter" + healthBarCenter.BackgroundTransparency = 1 + healthBarCenter.Image = greenBarImage + healthBarCenter.Size = UDim2.new(1,-capWidth*2,1,0) + healthBarCenter.Position = UDim2.new(0,capWidth,0,0) + healthBarCenter.Parent = healthBar + healthBarCenter.ImageColor3 = greenColor + + local healthBarLeft = Instance.new("ImageLabel") + healthBarLeft.Name = "healthBarLeft" + healthBarLeft.BackgroundTransparency = 1 + healthBarLeft.Image = greenBarImageLeft + healthBarLeft.Size = UDim2.new(0,capWidth,1,0) + healthBarLeft.Position = UDim2.new(0,0,0,0) + healthBarLeft.Parent = healthBar + healthBarLeft.ImageColor3 = greenColor + + local healthBarRight = Instance.new("ImageLabel") + healthBarRight.Name = "healthBarRight" + healthBarRight.BackgroundTransparency = 1 + healthBarRight.Image = greenBarImageRight + healthBarRight.Size = UDim2.new(0,capWidth,1,0) + healthBarRight.Position = UDim2.new(1,-capWidth,0,0) + healthBarRight.Parent = healthBar + healthBarRight.ImageColor3 = greenColor + + HealthGui.Parent = game:GetService("CoreGui").RobloxGui +end + +function UpdateGui(health) + if not HealthGui then return end + + local healthFrame = HealthGui:FindFirstChild("HealthFrame") + if not healthFrame then return end + + local healthBar = healthFrame:FindFirstChild("HealthBar") + if not healthBar then return end + + -- If more than 1/4 health, bar = green. Else, bar = red. + local percentHealth = (health/currentHumanoid.MaxHealth) + if percentHealth ~= percentHealth then + percentHealth = 1 + healthBar.healthBarCenter.ImageColor3 = yellowColor + healthBar.healthBarRight.ImageColor3 = yellowColor + healthBar.healthBarLeft.ImageColor3 = yellowColor + elseif percentHealth > 0.25 then + healthBar.healthBarCenter.ImageColor3 = greenColor + healthBar.healthBarRight.ImageColor3 = greenColor + healthBar.healthBarLeft.ImageColor3 = greenColor + else + healthBar.healthBarCenter.ImageColor3 = redColor + healthBar.healthBarRight.ImageColor3 = redColor + healthBar.healthBarLeft.ImageColor3 = redColor + end + + local width = (health / currentHumanoid.MaxHealth) + width = math.max(math.min(width,1),0) -- make sure width is between 0 and 1 + if width ~= width then width = 1 end + + local healthDelta = lastHealth - health + lastHealth = health + + local percentOfTotalHealth = math.abs(healthDelta/currentHumanoid.MaxHealth) + percentOfTotalHealth = math.max(math.min(percentOfTotalHealth,1),0) -- make sure percentOfTotalHealth is between 0 and 1 + if percentOfTotalHealth ~= percentOfTotalHealth then percentOfTotalHealth = 1 end + + local newHealthSize = UDim2.new(width,0,1,0) + + healthBar.Size = newHealthSize + + local sizeX = healthBar.AbsoluteSize.X + if sizeX < capWidth then + healthBar.healthBarCenter.Visible = false + healthBar.healthBarRight.Visible = false + elseif sizeX < (2*capWidth + 1) then + healthBar.healthBarCenter.Visible = true + healthBar.healthBarCenter.Size = UDim2.new(0,sizeX - capWidth,1,0) + healthBar.healthBarRight.Visible = false + else + healthBar.healthBarCenter.Visible = true + healthBar.healthBarCenter.Size = UDim2.new(1,-capWidth*2,1,0) + healthBar.healthBarRight.Visible = true + end + + local thresholdForHurtOverlay = currentHumanoid.MaxHealth * (HealthPercentageForOverlay/100) + + if healthDelta >= thresholdForHurtOverlay and guiEnabled then + AnimateHurtOverlay() + end + +end + +function AnimateHurtOverlay() + if not HealthGui then return end + + local overlay = HealthGui:FindFirstChild("HurtOverlay") + if not overlay then return end + + local newSize = UDim2.new(20, 0, 20, 0) + local newPos = UDim2.new(-10, 0, -10, 0) + + if overlay:IsDescendantOf(game) then + -- stop any tweens on overlay + overlay:TweenSizeAndPosition(newSize,newPos,Enum.EasingDirection.Out,Enum.EasingStyle.Linear,0,true,function() + + -- show the gui + overlay.Size = UDim2.new(1,0,1,0) + overlay.Position = UDim2.new(0,0,0,0) + overlay.Visible = true + + -- now tween the hide + if overlay:IsDescendantOf(game) then + overlay:TweenSizeAndPosition(newSize,newPos,Enum.EasingDirection.Out,Enum.EasingStyle.Quad,10,false,function() + overlay.Visible = false + end) + else + overlay.Size = newSize + overlay.Position = newPos + end + end) + else + overlay.Size = newSize + overlay.Position = newPos + end + +end + +function humanoidDied() + UpdateGui(0) +end + +function disconnectPlayerConnections() + if characterAddedConnection then characterAddedConnection:disconnect() end + if humanoidDiedConnection then humanoidDiedConnection:disconnect() end + if healthChangedConnection then healthChangedConnection:disconnect() end +end + +function newPlayerCharacter() + disconnectPlayerConnections() + startGui() +end + +function startGui() + characterAddedConnection = game:GetService("Players").LocalPlayer.CharacterAdded:connect(newPlayerCharacter) + + local character = game:GetService("Players").LocalPlayer.Character + if not character then + return + end + + currentHumanoid = character:WaitForChild("Humanoid") + if not currentHumanoid then + return + end + + if not game:GetService("StarterGui"):GetCoreGuiEnabled(Enum.CoreGuiType.Health) then + return + end + + healthChangedConnection = currentHumanoid.HealthChanged:connect(UpdateGui) + humanoidDiedConnection = currentHumanoid.Died:connect(humanoidDied) + UpdateGui(currentHumanoid.Health) + + CreateGui() +end + + + +--------------------------------------------------------------------- +-- Start Script + +HealthGui = Instance.new("Frame") +HealthGui.Name = "HealthGui" +HealthGui.BackgroundTransparency = 1 +HealthGui.Size = UDim2.new(1,0,1,0) + +game:GetService("StarterGui").CoreGuiChangedSignal:connect(function(coreGuiType,enabled) + if coreGuiType == Enum.CoreGuiType.Health or coreGuiType == Enum.CoreGuiType.All then + if guiEnabled and not enabled then + if HealthGui then + HealthGui.Parent = nil + end + disconnectPlayerConnections() + elseif not guiEnabled and enabled then + startGui() + end + + guiEnabled = enabled + end +end) + +if game:GetService("StarterGui"):GetCoreGuiEnabled(Enum.CoreGuiType.Health) then + guiEnabled = true + startGui() +end diff --git a/Client2018/content/scripts/CoreScripts/CoreScripts/MainBotChatScript2.lua b/Client2018/content/scripts/CoreScripts/CoreScripts/MainBotChatScript2.lua new file mode 100644 index 0000000..233e1c5 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/CoreScripts/MainBotChatScript2.lua @@ -0,0 +1,753 @@ +local PURPOSE_DATA = { + [Enum.DialogPurpose.Quest] = { + "rbxasset://textures/ui/dialog_purpose_quest.png", + Vector2.new(10, 34) + }, + [Enum.DialogPurpose.Help] = { + "rbxasset://textures/ui/dialog_purpose_help.png", + Vector2.new(20, 35) + }, + [Enum.DialogPurpose.Shop] = { + "rbxasset://textures/ui/dialog_purpose_shop.png", + Vector2.new(22, 43) + }, +} +local TEXT_HEIGHT = 24 -- Pixel height of one row +local FONT_SIZE = Enum.FontSize.Size24 +local BAR_THICKNESS = 6 +local STYLE_PADDING = 17 +local CHOICE_PADDING = 6 * 2 -- (Added to vertical height) +local PROMPT_SIZE = Vector2.new(80, 90) +local FRAME_WIDTH = 350 + +local WIDTH_BONUS = (STYLE_PADDING * 2) - BAR_THICKNESS +local XPOS_OFFSET = -(STYLE_PADDING - BAR_THICKNESS) + +local playerService = game:GetService("Players") +local contextActionService = game:GetService("ContextActionService") +local guiService = game:GetService("GuiService") +local YPOS_OFFSET = -math.floor(STYLE_PADDING / 2) +local usingGamepad = false + +local FlagHasReportedPlace = false + +local localPlayer = playerService.LocalPlayer +while localPlayer == nil do + playerService.PlayerAdded:wait() + localPlayer = playerService.LocalPlayer +end + +function setUsingGamepad(input, processed) + if input.UserInputType == Enum.UserInputType.Gamepad1 or input.UserInputType == Enum.UserInputType.Gamepad2 or + input.UserInputType == Enum.UserInputType.Gamepad3 or input.UserInputType == Enum.UserInputType.Gamepad4 then + usingGamepad = true + else + usingGamepad = false + end +end + +game:GetService("UserInputService").InputBegan:connect(setUsingGamepad) +game:GetService("UserInputService").InputChanged:connect(setUsingGamepad) + +function waitForProperty(instance, name) + while not instance[name] do + instance.Changed:wait() + end +end + +local goodbyeChoiceActiveFlagSuccess, goodbyeChoiceActiveFlagValue = pcall(function() + return settings():GetFFlag("GoodbyeChoiceActiveProperty") +end) +local goodbyeChoiceActiveFlag = (goodbyeChoiceActiveFlagSuccess and goodbyeChoiceActiveFlagValue) + +local mainFrame +local choices = {} +local lastChoice +local choiceMap = {} +local currentConversationDialog +local currentConversationPartner +local currentAbortDialogScript + +local coroutineMap = {} +local currentDialogTimeoutCoroutine = nil + +local tooFarAwayMessage = "You are too far away to chat!" +local tooFarAwaySize = 300 +local characterWanderedOffMessage = "Chat ended because you walked away" +local characterWanderedOffSize = 350 +local conversationTimedOut = "Chat ended because you didn't reply" +local conversationTimedOutSize = 350 + +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local RobloxReplicatedStorage = game:GetService('RobloxReplicatedStorage') +local setDialogInUseEvent = RobloxReplicatedStorage:WaitForChild("SetDialogInUse", 86400) + +local player +local screenGui +local chatNotificationGui +local messageDialog +local timeoutScript +local reenableDialogScript +local dialogMap = {} +local dialogConnections = {} +local touchControlGui = nil + +local gui = nil + +local isTenFootInterface = require(RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface")):IsEnabled() +local utility = require(RobloxGui.Modules.Settings.Utility) +local GameTranslator = require(RobloxGui.Modules.GameTranslator) +local isSmallTouchScreen = utility:IsSmallTouchScreen() + +if isTenFootInterface then + FONT_SIZE = Enum.FontSize.Size36 + TEXT_HEIGHT = 36 + FRAME_WIDTH = 500 +elseif isSmallTouchScreen then + FONT_SIZE = Enum.FontSize.Size14 + TEXT_HEIGHT = 14 + FRAME_WIDTH = 250 +end + +if RobloxGui:FindFirstChild("ControlFrame") then + gui = RobloxGui.ControlFrame +else + gui = RobloxGui +end +local touchEnabled = game:GetService("UserInputService").TouchEnabled + +local function isDialogMultiplePlayers(dialog) + local success, value = pcall(function() return dialog.BehaviorType == Enum.DialogBehaviorType.MultiplePlayers end) + return success and value or false +end + +function currentTone() + if currentConversationDialog then + return currentConversationDialog.Tone + else + return Enum.DialogTone.Neutral + end +end + + +function createChatNotificationGui() + chatNotificationGui = Instance.new("BillboardGui") + chatNotificationGui.Name = "ChatNotificationGui" + chatNotificationGui.ExtentsOffset = Vector3.new(0, 1, 0) + chatNotificationGui.Size = UDim2.new(PROMPT_SIZE.X / 31.5, 0, PROMPT_SIZE.Y / 31.5, 0) + chatNotificationGui.SizeOffset = Vector2.new(0, 0) + chatNotificationGui.StudsOffset = Vector3.new(0, 3.7, 0) + chatNotificationGui.Enabled = true + chatNotificationGui.RobloxLocked = true + chatNotificationGui.Active = true + + local button = Instance.new("ImageButton") + button.Name = "Background" + button.Active = false + button.BackgroundTransparency = 1 + button.Position = UDim2.new(0, 0, 0, 0) + button.Size = UDim2.new(1, 0, 1, 0) + button.Image = "" + button.Parent = chatNotificationGui + + local icon = Instance.new("ImageLabel") + icon.Name = "Icon" + icon.Position = UDim2.new(0, 0, 0, 0) + icon.Size = UDim2.new(1, 0, 1, 0) + icon.Image = "" + icon.BackgroundTransparency = 1 + icon.Parent = button + + local activationButton = Instance.new("ImageLabel") + activationButton.Name = "ActivationButton" + activationButton.Position = UDim2.new(-0.3, 0, -0.4, 0) + activationButton.Size = UDim2.new(.8, 0, .8 * (PROMPT_SIZE.X / PROMPT_SIZE.Y), 0) + activationButton.Image = "rbxasset://textures/ui/Settings/Help/XButtonDark.png" + activationButton.BackgroundTransparency = 1 + activationButton.Visible = false + activationButton.Parent = button +end + +function getChatColor(tone) + if tone == Enum.DialogTone.Neutral then + return Enum.ChatColor.Blue + elseif tone == Enum.DialogTone.Friendly then + return Enum.ChatColor.Green + elseif tone == Enum.DialogTone.Enemy then + return Enum.ChatColor.Red + end +end + +function styleChoices() + for _, obj in pairs(choices) do + obj.BackgroundTransparency = 1 + end + lastChoice.BackgroundTransparency = 1 +end + +function styleMainFrame(tone) + if tone == Enum.DialogTone.Neutral then + mainFrame.Style = Enum.FrameStyle.ChatBlue + elseif tone == Enum.DialogTone.Friendly then + mainFrame.Style = Enum.FrameStyle.ChatGreen + elseif tone == Enum.DialogTone.Enemy then + mainFrame.Style = Enum.FrameStyle.ChatRed + end + + styleChoices() +end +function setChatNotificationTone(gui, purpose, tone) + if tone == Enum.DialogTone.Neutral then + gui.Background.Image = "rbxasset://textures/ui/chatBubble_blue_notify_bkg.png" + elseif tone == Enum.DialogTone.Friendly then + gui.Background.Image = "rbxasset://textures/ui/chatBubble_green_notify_bkg.png" + elseif tone == Enum.DialogTone.Enemy then + gui.Background.Image = "rbxasset://textures/ui/chatBubble_red_notify_bkg.png" + end + + local newIcon, size = unpack(PURPOSE_DATA[purpose]) + local relativeSize = size / PROMPT_SIZE + gui.Background.Icon.Size = UDim2.new(relativeSize.X, 0, relativeSize.Y, 0) + gui.Background.Icon.Position = UDim2.new(0.5 - (relativeSize.X / 2), 0, 0.4 - (relativeSize.Y / 2), 0) + gui.Background.Icon.Image = newIcon +end + +function createMessageDialog() + messageDialog = Instance.new("Frame"); + messageDialog.Name = "DialogScriptMessage" + messageDialog.Style = Enum.FrameStyle.Custom + messageDialog.BackgroundTransparency = 0.5 + messageDialog.BackgroundColor3 = Color3.new(31 / 255, 31 / 255, 31 / 255) + messageDialog.Visible = false + messageDialog.RobloxLocked = true + + local text = Instance.new("TextLabel") + text.Name = "Text" + text.Position = UDim2.new(0, 0, 0, -1) + text.Size = UDim2.new(1, 0, 1, 0) + text.FontSize = Enum.FontSize.Size14 + text.BackgroundTransparency = 1 + text.TextColor3 = Color3.new(1, 1, 1) + text.Parent = messageDialog +end + +function showMessage(msg, size) + messageDialog.Text.Text = msg + messageDialog.Size = UDim2.new(0, size, 0, 40) + messageDialog.Position = UDim2.new(0.5, -size / 2, 0.5, -40) + messageDialog.Visible = true + wait(2) + messageDialog.Visible = false +end + +function variableDelay(str) + local length = math.min(string.len(str), 100) + wait(0.75 + ((length / 75) * 1.5)) +end + +function resetColor(frame) + frame.BackgroundTransparency = 1 +end + +function wanderDialog() + mainFrame.Visible = false + endDialog() + showMessage(characterWanderedOffMessage, characterWanderedOffSize) +end + +function timeoutDialog() + mainFrame.Visible = false + endDialog() + showMessage(conversationTimedOut, conversationTimedOutSize) +end + +function normalEndDialog() + endDialog() +end + +function endDialog() + if currentDialogTimeoutCoroutine then + coroutineMap[currentDialogTimeoutCoroutine] = false + currentDialogTimeoutCoroutine = nil + end + + local dialog = currentConversationDialog + currentConversationDialog = nil + if dialog and dialog.InUse then + -- Waits 5 seconds before setting InUse to false + setDialogInUseEvent:FireServer(dialog, false, 5) + delay(5, function() + dialog.InUse = false + end) + end + + for dialog, gui in pairs(dialogMap) do + if dialog and gui then + gui.Enabled = not dialog.InUse + end + end + + contextActionService:UnbindCoreAction("Nothing") + currentConversationPartner = nil + + if touchControlGui then + touchControlGui.Visible = true + end +end + +function sanitizeMessage(msg) + if string.len(msg) == 0 then + return "..." + else + return msg + end +end + +local function chatFunc(dialog, ...) + if isDialogMultiplePlayers(dialog) then + game:GetService("Chat"):ChatLocal(...) + else + game:GetService("Chat"):Chat(...) + end +end + +function selectChoice(choice) + renewKillswitch(currentConversationDialog) + + --First hide the Gui + mainFrame.Visible = false + if choice == lastChoice then + chatFunc(currentConversationDialog, localPlayer.Character, lastChoice.UserPrompt.Text, getChatColor(currentTone())) + + normalEndDialog() + else + local dialogChoice = choiceMap[choice] + + chatFunc(currentConversationDialog, localPlayer.Character, sanitizeMessage(dialogChoice.UserDialog), getChatColor(currentTone())) + wait(1) + currentConversationDialog:SignalDialogChoiceSelected(localPlayer, dialogChoice) + chatFunc(currentConversationDialog, currentConversationPartner, sanitizeMessage(dialogChoice.ResponseDialog), getChatColor(currentTone())) + + variableDelay(dialogChoice.ResponseDialog) + presentDialogChoices(currentConversationPartner, dialogChoice:GetChildren(), dialogChoice) + end +end + +function newChoice() + local dummyFrame = Instance.new("Frame") + dummyFrame.Visible = false + + local frame = Instance.new("TextButton") + frame.BackgroundColor3 = Color3.new(227 / 255, 227 / 255, 227 / 255) + frame.BackgroundTransparency = 1 + frame.AutoButtonColor = false + frame.BorderSizePixel = 0 + frame.Text = "" + frame.MouseEnter:connect(function() + frame.BackgroundTransparency = 0 + end) + frame.MouseLeave:connect(function() + frame.BackgroundTransparency = 1 + end) + frame.SelectionImageObject = dummyFrame + frame.MouseButton1Click:connect(function() + selectChoice(frame) + end) + frame.RobloxLocked = true + + local prompt = Instance.new("TextLabel") + prompt.Name = "UserPrompt" + prompt.BackgroundTransparency = 1 + prompt.Font = Enum.Font.SourceSans + prompt.FontSize = FONT_SIZE + prompt.Position = UDim2.new(0, 40, 0, 0) + prompt.Size = UDim2.new(1, -32 - 40, 1, 0) + prompt.TextXAlignment = Enum.TextXAlignment.Left + prompt.TextYAlignment = Enum.TextYAlignment.Center + prompt.TextWrap = true + prompt.Parent = frame + + local selectionButton = Instance.new("ImageLabel") + selectionButton.Name = "RBXchatDialogSelectionButton" + selectionButton.Position = UDim2.new(0, 0, 0.5, -33 / 2) + selectionButton.Size = UDim2.new(0, 33, 0, 33) + selectionButton.Image = "rbxasset://textures/ui/Settings/Help/AButtonLightSmall.png" + selectionButton.BackgroundTransparency = 1 + selectionButton.Visible = false + selectionButton.Parent = frame + + return frame +end +function initialize(parent) + choices[1] = newChoice() + choices[2] = newChoice() + choices[3] = newChoice() + choices[4] = newChoice() + + lastChoice = newChoice() + lastChoice.UserPrompt.Text = "Goodbye!" + lastChoice.Size = UDim2.new(1, WIDTH_BONUS, 0, TEXT_HEIGHT + CHOICE_PADDING) + + mainFrame = Instance.new("Frame") + mainFrame.Name = "UserDialogArea" + mainFrame.Size = UDim2.new(0, FRAME_WIDTH, 0, 200) + mainFrame.Style = Enum.FrameStyle.ChatBlue + mainFrame.Visible = false + + for n, obj in pairs(choices) do + obj.RobloxLocked = true + obj.Parent = mainFrame + end + + lastChoice.RobloxLocked = true + lastChoice.Parent = mainFrame + + mainFrame.RobloxLocked = true + mainFrame.Parent = parent +end + +function presentDialogChoices(talkingPart, dialogChoices, parentDialog) + if not currentConversationDialog then + return + end + + currentConversationPartner = talkingPart + local sortedDialogChoices = {} + for n, obj in pairs(dialogChoices) do + if obj:IsA("DialogChoice") then + table.insert(sortedDialogChoices, obj) + end + end + table.sort(sortedDialogChoices, function(a, b) + return a.Name < b.Name + end) + + if #sortedDialogChoices == 0 then + normalEndDialog() + return + end + + local pos = 1 + local yPosition = 0 + choiceMap = {} + for n, obj in pairs(choices) do + obj.Visible = false + end + + for n, obj in pairs(sortedDialogChoices) do + if pos <= #choices then + --3 lines is the maximum, set it to that temporarily + choices[pos].Size = UDim2.new(1, WIDTH_BONUS, 0, TEXT_HEIGHT * 3) + choices[pos].UserPrompt.Text = GameTranslator:TranslateGameText(obj, obj.UserDialog) + local height = (math.ceil(choices[pos].UserPrompt.TextBounds.Y / TEXT_HEIGHT) * TEXT_HEIGHT) + CHOICE_PADDING + + choices[pos].Position = UDim2.new(0, XPOS_OFFSET, 0, YPOS_OFFSET + yPosition) + choices[pos].Size = UDim2.new(1, WIDTH_BONUS, 0, height) + choices[pos].Visible = true + + choiceMap[choices[pos]] = obj + + yPosition = yPosition + height + 1 -- The +1 makes highlights not overlap + pos = pos + 1 + end + end + + lastChoice.Size = UDim2.new(1, WIDTH_BONUS, 0, TEXT_HEIGHT * 3) + lastChoice.UserPrompt.Text = parentDialog.GoodbyeDialog == "" and "Goodbye!" or parentDialog.GoodbyeDialog + local height = (math.ceil(lastChoice.UserPrompt.TextBounds.Y / TEXT_HEIGHT) * TEXT_HEIGHT) + CHOICE_PADDING + lastChoice.Size = UDim2.new(1, WIDTH_BONUS, 0, height) + lastChoice.Position = UDim2.new(0, XPOS_OFFSET, 0, YPOS_OFFSET + yPosition) + lastChoice.Visible = true + + if goodbyeChoiceActiveFlag and not parentDialog.GoodbyeChoiceActive then + lastChoice.Visible = false + mainFrame.Size = UDim2.new(0, FRAME_WIDTH, 0, yPosition + (STYLE_PADDING * 2) + (YPOS_OFFSET * 2)) + else + mainFrame.Size = UDim2.new(0, FRAME_WIDTH, 0, yPosition + lastChoice.AbsoluteSize.Y + (STYLE_PADDING * 2) + (YPOS_OFFSET * 2)) + end + + mainFrame.Position = UDim2.new(0, 20, 1.0, -mainFrame.Size.Y.Offset - 20) + if isSmallTouchScreen then + local touchScreenGui = localPlayer.PlayerGui:FindFirstChild("TouchGui") + if touchScreenGui then + touchControlGui = touchScreenGui:FindFirstChild("TouchControlFrame") + if touchControlGui then + touchControlGui.Visible = false + end + end + mainFrame.Position = UDim2.new(0, 10, 1.0, -mainFrame.Size.Y.Offset) + end + styleMainFrame(currentTone()) + mainFrame.Visible = true + + if usingGamepad then + game:GetService("GuiService").SelectedCoreObject = choices[1] + end +end + +function doDialog(dialog) + if dialog.InitialPrompt == "" then + warn("Can't start a dialog with an empty InitialPrompt") + return + end + + local isMultiplePlayers = isDialogMultiplePlayers(dialog) + + if dialog.InUse and not isMultiplePlayers then + return + else + currentConversationDialog = dialog + dialog.InUse = true + -- only bind if we actual enter the dialog + contextActionService:BindCoreAction("Nothing", function() + end, false, Enum.UserInputType.Gamepad1, Enum.UserInputType.Gamepad2, Enum.UserInputType.Gamepad3, Enum.UserInputType.Gamepad4) + -- Immediately sets InUse to true on the server + setDialogInUseEvent:FireServer(dialog, true, 0) + end + chatFunc(dialog, dialog.Parent, dialog.InitialPrompt, getChatColor(dialog.Tone)) + variableDelay(dialog.InitialPrompt) + + presentDialogChoices(dialog.Parent, dialog:GetChildren(), dialog) +end + +function renewKillswitch(dialog) + if currentDialogTimeoutCoroutine then + coroutineMap[currentDialogTimeoutCoroutine] = false + currentDialogTimeoutCoroutine = nil + end + + currentDialogTimeoutCoroutine = coroutine.create(function(thisCoroutine) + wait(15) + if thisCoroutine ~= nil then + if coroutineMap[thisCoroutine] == nil then + setDialogInUseEvent:FireServer(dialog, false, 0) + dialog.InUse = false + end + coroutineMap[thisCoroutine] = nil + end + end) + coroutine.resume(currentDialogTimeoutCoroutine, currentDialogTimeoutCoroutine) +end + +function checkForLeaveArea() + while currentConversationDialog do + if currentConversationDialog.Parent and (localPlayer:DistanceFromCharacter(currentConversationDialog.Parent.Position) >= currentConversationDialog.ConversationDistance) then + wanderDialog() + end + wait(1) + end +end + +function startDialog(dialog) + if dialog.Parent and dialog.Parent:IsA("BasePart") then + game:ReportInGoogleAnalytics("Dialogue", "Old Dialogue", "Conversation Initiated") + + if localPlayer:DistanceFromCharacter(dialog.Parent.Position) >= dialog.ConversationDistance then + showMessage(tooFarAwayMessage, tooFarAwaySize) + return + end + + for dialog, gui in pairs(dialogMap) do + if dialog and gui then + gui.Enabled = false + end + end + + renewKillswitch(dialog) + + delay(1, checkForLeaveArea) + doDialog(dialog) + end +end + +function removeDialog(dialog) + if dialogMap[dialog] then + dialogMap[dialog]:Destroy() + dialogMap[dialog] = nil + end + if dialogConnections[dialog] then + dialogConnections[dialog]:disconnect() + dialogConnections[dialog] = nil + end +end + +function addDialog(dialog) + if dialog.Parent then + if dialog.Parent:IsA("BasePart") and dialog:IsDescendantOf(game.Workspace) then + FlagHasReportedPlace = true + game:ReportInGoogleAnalytics("Dialogue", "Old Dialogue", "Used In Place", nil, game.PlaceId) + + local chatGui = chatNotificationGui:clone() + chatGui.Adornee = dialog.Parent + chatGui.RobloxLocked = true + chatGui.Enabled = not dialog.InUse or isDialogMultiplePlayers(dialog) + chatGui.Parent = CoreGui + + chatGui.Background.MouseButton1Click:connect(function() + startDialog(dialog) + end) + setChatNotificationTone(chatGui, dialog.Purpose, dialog.Tone) + + dialogMap[dialog] = chatGui + + dialogConnections[dialog] = dialog.Changed:connect(function(prop) + if prop == "Parent" and dialog.Parent then + --This handles the reparenting case, seperate from removal case + removeDialog(dialog) + addDialog(dialog) + elseif prop == "InUse" then + if not isDialogMultiplePlayers(dialog) then + chatGui.Enabled = (currentConversationDialog == nil) and not dialog.InUse + else + chatGui.Enabled = (currentConversationDialog ~= dialog) + end + + if not dialog.InUse and not isDialogMultiplePlayers(player) and dialog == currentConversationDialog then + timeoutDialog() + end + elseif prop == "Tone" or prop == "Purpose" then + setChatNotificationTone(chatGui, dialog.Purpose, dialog.Tone) + end + end) + else -- still need to listen to parent changes even if current parent is not a BasePart + dialogConnections[dialog] = dialog.Changed:connect(function(prop) + if prop == "Parent" and dialog.Parent then + --This handles the reparenting case, seperate from removal case + removeDialog(dialog) + addDialog(dialog) + end + end) + end + end +end + +function onLoad() + waitForProperty(localPlayer, "Character") + + createChatNotificationGui() + + createMessageDialog() + messageDialog.RobloxLocked = true + messageDialog.Parent = gui + + gui:WaitForChild("BottomLeftControl") + + local frame = Instance.new("Frame") + frame.Name = "DialogFrame" + frame.Position = UDim2.new(0, 0, 0, 0) + frame.Size = UDim2.new(0, 0, 0, 0) + frame.BackgroundTransparency = 1 + frame.RobloxLocked = true + game:GetService("GuiService"):AddSelectionParent("RBXDialogGroup", frame) + + if (touchEnabled and not isSmallTouchScreen) then + frame.Position = UDim2.new(0, 20, 0.5, 0) + frame.Size = UDim2.new(0.25, 0, 0.1, 0) + frame.Parent = gui + elseif isSmallTouchScreen then + frame.Position = UDim2.new(0, 0, .9, -10) + frame.Size = UDim2.new(0.25, 0, 0.1, 0) + frame.Parent = gui + else + frame.Parent = gui.BottomLeftControl + end + initialize(frame) + + game:GetService("CollectionService").ItemAdded:connect(function(obj) + if obj:IsA("Dialog") then + addDialog(obj) + end + end) + game:GetService("CollectionService").ItemRemoved:connect(function(obj) + if obj:IsA("Dialog") then + removeDialog(obj) + end + end) + for i, obj in pairs(game:GetService("CollectionService"):GetCollection("Dialog")) do + if obj:IsA("Dialog") then + addDialog(obj) + end + end +end + +function getLocalHumanoidRootPart() + if localPlayer.Character then + return localPlayer.Character:FindFirstChild("HumanoidRootPart") + end +end + +function dialogIsValid(dialog) + return dialog and dialog.Parent and dialog.Parent:IsA("BasePart") +end + +local lastClosestDialog = nil +local getClosestDialogToPosition = guiService.GetClosestDialogToPosition + +game:GetService("RunService").Heartbeat:connect(function() + local closestDistance = math.huge + local closestDialog = nil + + local humanoidRootPart = getLocalHumanoidRootPart() + if humanoidRootPart then + local characterPosition = humanoidRootPart.Position + closestDialog = getClosestDialogToPosition(guiService, characterPosition) + end + + if getLocalHumanoidRootPart() and dialogIsValid(closestDialog) and currentConversationDialog == nil then + + local dialogTriggerDistance = closestDialog.TriggerDistance + local dialogTriggerOffset = closestDialog.TriggerOffset + + local distanceFromCharacterWithOffset = localPlayer:DistanceFromCharacter( + closestDialog.Parent.Position + dialogTriggerOffset + ) + + if dialogTriggerDistance ~= 0 and + distanceFromCharacterWithOffset < closestDialog.ConversationDistance and + distanceFromCharacterWithOffset < dialogTriggerDistance then + + startDialog(closestDialog) + end + end + + if usingGamepad == true then + if closestDialog ~= lastClosestDialog then + if dialogMap[lastClosestDialog] then + dialogMap[lastClosestDialog].Background.ActivationButton.Visible = false + end + lastClosestDialog = closestDialog + contextActionService:UnbindCoreAction("StartDialogAction") + if closestDialog ~= nil then + contextActionService:BindCoreAction("StartDialogAction", function(actionName, userInputState, inputObject) + if userInputState == Enum.UserInputState.Begin then + if closestDialog and closestDialog.Parent then + startDialog(closestDialog) + end + end + end, false, Enum.KeyCode.ButtonX) + if dialogMap[closestDialog] then + dialogMap[closestDialog].Background.ActivationButton.Visible = true + end + end -- closestDialog ~= nil + end -- closestDialog ~= lastClosestDialog + end -- usingGamepad == true +end) + +local lastSelectedChoice = nil + +guiService.Changed:connect(function(property) + if property == "SelectedCoreObject" then + if lastSelectedChoice and lastSelectedChoice:FindFirstChild("RBXchatDialogSelectionButton") then + lastSelectedChoice:FindFirstChild("RBXchatDialogSelectionButton").Visible = false + lastSelectedChoice.BackgroundTransparency = 1 + end + lastSelectedChoice = guiService.SelectedCoreObject + if lastSelectedChoice and lastSelectedChoice:FindFirstChild("RBXchatDialogSelectionButton") then + lastSelectedChoice:FindFirstChild("RBXchatDialogSelectionButton").Visible = true + lastSelectedChoice.BackgroundTransparency = 0 + end + end +end) + +onLoad() diff --git a/Client2018/content/scripts/CoreScripts/CoreScripts/NotificationScript2.lua b/Client2018/content/scripts/CoreScripts/CoreScripts/NotificationScript2.lua new file mode 100644 index 0000000..13017b6 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/CoreScripts/NotificationScript2.lua @@ -0,0 +1,964 @@ +--[[ + Filename: NotificationScript2.lua + Version 1.1 + Written by: jmargh + Description: Handles notification gui for the following in game ROBLOX events + Badge Awarded + Player Points Awarded + Friend Request Recieved/New Friend + Graphics Quality Changed + Teleports + CreatePlaceInPlayerInventoryAsync +--]] + +local BadgeService = game:GetService('BadgeService') +local GuiService = game:GetService('GuiService') +local Players = game:GetService('Players') +local PointsService = game:GetService('PointsService') +local MarketplaceService = game:GetService('MarketplaceService') +local TeleportService = game:GetService('TeleportService') +local TextService = game:GetService("TextService") +local HttpService = game:GetService("HttpService") +local UserInputService = game:GetService("UserInputService") +local ContextActionService = game:GetService("ContextActionService") +local StarterGui = game:GetService("StarterGui") +local CoreGui = game:GetService("CoreGui") +local AnalyticsService = game:GetService("AnalyticsService") +local VRService = game:GetService("VRService") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local Settings = UserSettings() +local GameSettings = Settings.GameSettings + +local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end) +local FFlagUseNotificationsLocalization = success and result + +local FFlagCoreScriptsUseLocalizationModule = settings():GetFFlag('CoreScriptsUseLocalizationModule') +local FFlagNotificationScript2UseFormatByKey = settings():GetFFlag('NotificationScript2UseFormatByKey') + +local RobloxTranslator +if FFlagCoreScriptsUseLocalizationModule then + RobloxTranslator = require(RobloxGui:WaitForChild("Modules"):WaitForChild("RobloxTranslator")) +end + +local function LocalizedGetString(key, rtv) + pcall(function() + if FFlagCoreScriptsUseLocalizationModule then + rtv = RobloxTranslator:FormatByKey(key) + else + local LocalizationService = game:GetService("LocalizationService") + local CorescriptLocalization = LocalizationService:GetCorescriptLocalizations()[1] + rtv = CorescriptLocalization:GetString(LocalizationService.RobloxLocaleId, key) + end + end) + return rtv +end + +local LocalPlayer = nil +while not Players.LocalPlayer do + wait() +end +LocalPlayer = Players.LocalPlayer +local RbxGui = script.Parent +local NotificationQueue = {} +local OverflowQueue = {} +local FriendRequestBlacklist = {} +local BadgeBlacklist = {} +local CurrentGraphicsQualityLevel = GameSettings.SavedQualityLevel.Value +local BindableEvent_SendNotificationInfo = Instance.new('BindableEvent') +BindableEvent_SendNotificationInfo.Name = "SendNotificationInfo" +BindableEvent_SendNotificationInfo.Parent = RobloxGui +local isPaused = false +RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface") +local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled() + +local pointsNotificationsActive = true +local badgesNotificationsActive = true + +local SocialUtil = require(RobloxGui.Modules:WaitForChild("SocialUtil")) +local GameTranslator = require(RobloxGui.Modules.GameTranslator) + +local BG_TRANSPARENCY = 0.7 +local MAX_NOTIFICATIONS = 3 + +local NOTIFICATION_Y_OFFSET = isTenFootInterface and 145 or 64 +local NOTIFICATION_TITLE_Y_OFFSET = isTenFootInterface and 40 or 12 +local NOTIFICATION_TEXT_Y_OFFSET = isTenFootInterface and -16 or 1 +local NOTIFICATION_FRAME_WIDTH = isTenFootInterface and 450 or 200 +local NOTIFICATION_TEXT_HEIGHT = isTenFootInterface and 85 or 28 +local NOTIFICATION_TEXT_HEIGHT_MAX = isTenFootInterface and 170 or 56 +local NOTIFICATION_TITLE_FONT_SIZE = isTenFootInterface and Enum.FontSize.Size42 or Enum.FontSize.Size18 +local NOTIFICATION_TEXT_FONT_SIZE = isTenFootInterface and Enum.FontSize.Size36 or Enum.FontSize.Size14 + +local IMAGE_SIZE = isTenFootInterface and 72 or 48 + +local EASE_DIR = Enum.EasingDirection.InOut +local EASE_STYLE = Enum.EasingStyle.Sine +local TWEEN_TIME = 0.35 +local DEFAULT_NOTIFICATION_DURATION = 5 +local MAX_GET_FRIEND_IMAGE_YIELD_TIME = 5 +local FRIEND_REQUEST_NOTIFICATION_THROTTLE = 5 + +local friendRequestNotificationFIntSuccess, friendRequestNotificationFIntValue = pcall(function() return tonumber(settings():GetFVariable("FriendRequestNotificationThrottle")) end) +if friendRequestNotificationFIntSuccess and friendRequestNotificationFIntValue ~= nil then + FRIEND_REQUEST_NOTIFICATION_THROTTLE = friendRequestNotificationFIntValue +end + +local PLAYER_POINTS_IMG = 'https://www.roblox.com/asset?id=206410433' +local BADGE_IMG = 'https://www.roblox.com/asset?id=206410289' +local FRIEND_IMAGE = 'https://www.roblox.com/thumbs/avatar.ashx?userId=' + +local function createFrame(name, size, position, bgt) + local frame = Instance.new('Frame') + frame.Name = name + frame.Size = size + frame.Position = position + frame.BackgroundTransparency = bgt + + return frame +end + +local function createTextButton(name, text, position) + local button = Instance.new('TextButton') + button.Name = name + button.Size = UDim2.new(0.5, -2, 0.5, 0) + button.Position = position + button.BackgroundTransparency = BG_TRANSPARENCY + button.BackgroundColor3 = Color3.new(0, 0, 0) + button.Font = Enum.Font.SourceSansBold + button.FontSize = Enum.FontSize.Size18 + button.TextColor3 = Color3.new(1, 1, 1) + button.Text = text + + return button +end + +local NotificationFrame = createFrame("NotificationFrame", UDim2.new(0, NOTIFICATION_FRAME_WIDTH, 0.42, 0), UDim2.new(1, -NOTIFICATION_FRAME_WIDTH-4, 0.50, 0), 1.0) +NotificationFrame.Parent = RbxGui + +local DefaultNotification = createFrame("Notification", UDim2.new(1, 0, 0, NOTIFICATION_Y_OFFSET), UDim2.new(0, 0, 0, 0), BG_TRANSPARENCY) +DefaultNotification.BackgroundColor3 = Color3.new(0, 0, 0) +DefaultNotification.BorderSizePixel = 0 + +local NotificationTitle = Instance.new('TextLabel') +NotificationTitle.Name = "NotificationTitle" +NotificationTitle.Size = UDim2.new(0, 0, 0, 0) +NotificationTitle.Position = UDim2.new(0.5, 0, 0.5, -12) +NotificationTitle.BackgroundTransparency = 1 +NotificationTitle.Font = Enum.Font.SourceSansBold +NotificationTitle.FontSize = NOTIFICATION_TITLE_FONT_SIZE +NotificationTitle.TextColor3 = Color3.new(0.97, 0.97, 0.97) + +local NotificationText = Instance.new('TextLabel') +NotificationText.Name = "NotificationText" +NotificationText.Size = UDim2.new(1, -20, 0, 28) +NotificationText.Position = UDim2.new(0, 10, 0.5, 1) +NotificationText.BackgroundTransparency = 1 +NotificationText.Font = Enum.Font.SourceSans +NotificationText.FontSize = NOTIFICATION_TEXT_FONT_SIZE +NotificationText.TextColor3 = Color3.new(0.92, 0.92, 0.92) +NotificationText.TextWrap = true +NotificationText.TextYAlignment = Enum.TextYAlignment.Top + +local NotificationImage = Instance.new('ImageLabel') +NotificationImage.Name = "NotificationImage" +NotificationImage.Size = UDim2.new(0, IMAGE_SIZE, 0, IMAGE_SIZE) +NotificationImage.Position = UDim2.new(0, (1.0/6.0) * IMAGE_SIZE, 0, 0.5 * (NOTIFICATION_Y_OFFSET - IMAGE_SIZE)) +NotificationImage.BackgroundTransparency = 1 +NotificationImage.Image = "" + +-- Would really like to get rid of this but some events still require this +local PopupFrame = createFrame("PopupFrame", UDim2.new(0, 360, 0, 160), UDim2.new(0.5, -180, 0.5, -50), 0) +PopupFrame.Style = Enum.FrameStyle.DropShadow +PopupFrame.ZIndex = 4 +PopupFrame.Visible = false +PopupFrame.Parent = RbxGui + +local PopupAcceptButton = Instance.new('TextButton') +PopupAcceptButton.Name = "PopupAcceptButton" +PopupAcceptButton.Size = UDim2.new(0, 100, 0, 50) +PopupAcceptButton.Position = UDim2.new(0.5, -102, 1, -58) +PopupAcceptButton.Style = Enum.ButtonStyle.RobloxRoundButton +PopupAcceptButton.Font = Enum.Font.SourceSansBold +PopupAcceptButton.FontSize = Enum.FontSize.Size24 +PopupAcceptButton.TextColor3 = Color3.new(1, 1, 1) +PopupAcceptButton.Text = "Accept" +PopupAcceptButton.ZIndex = 5 +PopupAcceptButton.Parent = PopupFrame + +local PopupDeclineButton = PopupAcceptButton:Clone() +PopupDeclineButton.Name = "PopupDeclineButton" +PopupDeclineButton.Position = UDim2.new(0.5, 2, 1, -58) +PopupDeclineButton.Text = "Decline" +PopupDeclineButton.Parent = PopupFrame + +local PopupOKButton = PopupAcceptButton:Clone() +PopupOKButton.Name = "PopupOKButton" +PopupOKButton.Position = UDim2.new(0.5, -50, 1, -58) +PopupOKButton.Text = "OK" +PopupOKButton.Visible = false +PopupOKButton.Parent = PopupFrame + +local PopupText = Instance.new('TextLabel') +PopupText.Name = "PopupText" +PopupText.Size = UDim2.new(1, -16, 0.8, 0) +PopupText.Position = UDim2.new(0, 8, 0, 8) +PopupText.BackgroundTransparency = 1 +PopupText.Font = Enum.Font.SourceSansBold +PopupText.FontSize = Enum.FontSize.Size36 +PopupText.TextColor3 = Color3.new(0.97, 0.97, 0.97) +PopupText.TextWrap = true +PopupText.ZIndex = 5 +PopupText.TextYAlignment = Enum.TextYAlignment.Top +PopupText.Text = "This is a popup" +PopupText.Parent = PopupFrame + +local insertNotification +local removeNotification + +local function getFriendImage(playerId) + -- SocialUtil.GetPlayerImage can yield for up to MAX_GET_FRIEND_IMAGE_YIELD_TIME seconds while waiting for thumbnail to be final. + -- It will just return an invalid thumbnail if a valid one can not be generated in time. + return SocialUtil.GetPlayerImage(playerId, Enum.ThumbnailSize.Size48x48, Enum.ThumbnailType.HeadShot, --[[timeOut = ]] MAX_GET_FRIEND_IMAGE_YIELD_TIME) +end + +-- +local function createNotification(title, text, image) + local notificationFrame = DefaultNotification:Clone() + notificationFrame.Position = UDim2.new(1, 4, 1, -NOTIFICATION_Y_OFFSET - 4) + -- + local notificationTitle = NotificationTitle:Clone() + notificationTitle.Text = title + notificationTitle.Parent = notificationFrame + + local notificationText = NotificationText:Clone() + notificationText.Text = text + notificationText.Parent = notificationFrame + if (image == nil or image == "") then + notificationFrame.Parent = NotificationFrame + if not notificationText.TextFits then + local textSize = TextService:GetTextSize(notificationText.Text, notificationText.TextSize, notificationText.Font, Vector2.new(notificationText.AbsoluteSize.X, 1000)) + local addHeight = math.min(textSize.Y - notificationText.Size.Y.Offset, NOTIFICATION_TEXT_HEIGHT_MAX - notificationText.Size.Y.Offset) + notificationTitle.Position = notificationTitle.Position - UDim2.new(0, 0, 0, addHeight/2) + notificationText.Position = notificationText.Position - UDim2.new(0, 0, 0, addHeight/2) + notificationFrame.Size = notificationFrame.Size + UDim2.new(0, 0, 0, addHeight) + notificationText.Size = notificationText.Size + UDim2.new(0, 0, 0, addHeight) + end + notificationFrame.Parent = nil + end + + if image and image ~= "" then + local notificationImage = NotificationImage:Clone() + notificationImage.Image = image + notificationImage.Parent = notificationFrame + + notificationTitle.Position = UDim2.new(0, (4.0/3.0) * IMAGE_SIZE, 0.5, -NOTIFICATION_TITLE_Y_OFFSET) + notificationTitle.TextXAlignment = Enum.TextXAlignment.Left + + notificationFrame.Parent = NotificationFrame + notificationText.Size = UDim2.new(1, -IMAGE_SIZE - 16, 0, NOTIFICATION_TEXT_HEIGHT) + notificationText.Position = UDim2.new(0, (4.0/3.0) * IMAGE_SIZE, 0.5, NOTIFICATION_TEXT_Y_OFFSET) + notificationText.TextXAlignment = Enum.TextXAlignment.Left + if not notificationText.TextFits then + local extraText = nil + local text = notificationText.Text + for i = string.len(text) - 1, 2, -1 do + if string.sub(text, i, i) == " " then + notificationText.Text = string.sub(text, 1, i - 1) + if notificationText.TextFits then + extraText = string.sub(text, i + 1) + break + end + end + end + if extraText then + local notificationText2 = NotificationText:Clone() + notificationText2.TextXAlignment = Enum.TextXAlignment.Left + notificationText2.Text = extraText + notificationText2.Name = "ExtraText" + notificationText2.Parent = notificationFrame + + local textSize = TextService:GetTextSize(extraText, notificationText2.TextSize, notificationText2.Font, Vector2.new(notificationText2.AbsoluteSize.X, 1000)) + local addHeight = math.min(textSize.Y, NOTIFICATION_TEXT_HEIGHT_MAX - notificationText.Size.Y.Offset) + notificationTitle.Position = notificationTitle.Position - UDim2.new(0, 0, 0, addHeight/2) + notificationText.Position = notificationText.Position - UDim2.new(0, 0, 0, addHeight/2) + notificationFrame.Size = notificationFrame.Size + UDim2.new(0, 0, 0, addHeight) + + notificationText2.Size = UDim2.new(notificationText2.Size.X.Scale, notificationText2.Size.X.Offset, 0, addHeight) + notificationText2.AnchorPoint = Vector2.new(0.5, 0) + notificationText2.Position = UDim2.new(0.5, 0, notificationText.Position.Y.Scale, notificationText.Position.Y.Offset + notificationText.AbsoluteSize.Y) + else + notificationText.Text = text + end + end + notificationFrame.Parent = nil + end + + GuiService:AddSelectionParent(HttpService:GenerateGUID(false), notificationFrame) + + return notificationFrame +end + +local function findNotification(notification) + local index = nil + for i = 1, #NotificationQueue do + if NotificationQueue[i] == notification then + return i + end + end +end + +local function updateNotifications() + local pos = 1 + local yOffset = 0 + for i = #NotificationQueue, 1, -1 do + local currentNotification = NotificationQueue[i] + if currentNotification then + local frame = currentNotification.Frame + if frame and frame.Parent then + local thisOffset = currentNotification.IsFriend and (NOTIFICATION_Y_OFFSET + 2) * 1.5 or NOTIFICATION_Y_OFFSET + thisOffset = currentNotification.IsFriend and frame.Size.Y.Offset + ((NOTIFICATION_Y_OFFSET + 2) * 0.5) or frame.Size.Y.Offset + yOffset = yOffset + thisOffset + frame:TweenPosition(UDim2.new(0, 0, 1, -yOffset - (pos * 4)), EASE_DIR, EASE_STYLE, TWEEN_TIME, true, + function() + if currentNotification.TweenOutCallback then + currentNotification.TweenOutCallback() + end + end) + pos = pos + 1 + end + end + end +end + +local lastTimeInserted = 0 +insertNotification = function(notification) + spawn(function() + while isPaused do wait() end + notification.IsActive = true + local size = #NotificationQueue + if size == MAX_NOTIFICATIONS then + NotificationQueue[1].Duration = 0 + OverflowQueue[#OverflowQueue + 1] = notification + return + end + + NotificationQueue[size + 1] = notification + notification.Frame.Parent = NotificationFrame + + spawn(function() + wait(TWEEN_TIME * 2) + -- Wait for it to tween in, and then wait that same amount of time before calculating lifetime. + -- This is to have it not zoom out while half tweened in when the OverflowQueue forcibly + -- makes room for itself. + + while(notification.Duration > 0) do + wait(0.2) + notification.Duration = notification.Duration - 0.2 + end + + removeNotification(notification) + end) + + while tick() - lastTimeInserted < TWEEN_TIME do + wait() + end + lastTimeInserted = tick() + + updateNotifications() + end) +end + +removeNotification = function(notification) + if not notification then return end + -- + local index = findNotification(notification) + table.remove(NotificationQueue, index) + local frame = notification.Frame + if frame and frame.Parent then + notification.IsActive = false + spawn(function() + while isPaused do wait() end + + -- Tween out now, or set up to tween out immediately after current tween is finished, but don't interrupt. + local function doTweenOut() + return frame:TweenPosition(UDim2.new(1, 0, 1, frame.Position.Y.Offset), EASE_DIR, EASE_STYLE, TWEEN_TIME, false, + function() + frame:Destroy() + notification = nil + end) + end + + if (not doTweenOut()) then + notification.TweenOutCallback = doTweenOut + end + + end) + end + if #OverflowQueue > 0 then + local nextNotification = OverflowQueue[1] + table.remove(OverflowQueue, 1) + + insertNotification(nextNotification) + + if (#OverflowQueue > 0 and NotificationQueue[1]) then + NotificationQueue[1].Duration = 0 + end + else + updateNotifications() + end +end + +local function sendNotificationInfo(notificationInfo) + notificationInfo.Duration = notificationInfo.Duration or DEFAULT_NOTIFICATION_DURATION + BindableEvent_SendNotificationInfo:Fire(notificationInfo) +end + +local function onSendNotificationInfo(notificationInfo) + if VRService.VREnabled then + --If VR is enabled, notifications will be handled by Modules.VR.NotificationHub + return + end + local callback = notificationInfo.Callback + local button1Text = notificationInfo.Button1Text + local button2Text = notificationInfo.Button2Text + local localizedButton1Text = notificationInfo.Button1TextLocalized + local localizedButton2Text = notificationInfo.Button2TextLocalized + + local notification = {} + local notificationFrame = createNotification(notificationInfo.Title, notificationInfo.Text, notificationInfo.Image) + -- + + local button1 = nil + if button1Text and button1Text ~= "" then + notification.IsFriend = true -- Prevents other notifications overlapping the buttons + button1 = createTextButton("Button1", localizedButton1Text or button1Text, UDim2.new(0, 0, 1, 2)) + button1.Parent = notificationFrame + local button1ClickedConnection = nil + button1ClickedConnection = button1.MouseButton1Click:connect(function() + if button1ClickedConnection then + button1ClickedConnection:disconnect() + button1ClickedConnection = nil + removeNotification(notification) + if callback and type(callback) ~= "function" then -- callback should be a bindable + pcall(function() callback:Invoke(button1Text) end) + elseif type(callback) == "function" then + callback(button1Text) + end + end + end) + end + + if button2Text and button2Text ~= "" then + notification.IsFriend = true + local button2 = createTextButton("Button1", localizedButton2Text or button2Text, UDim2.new(0.5, 2, 1, 2)) + button2.Parent = notificationFrame + local button2ClickedConnection = nil + button2ClickedConnection = button2.MouseButton1Click:connect(function() + if button2ClickedConnection then + button2ClickedConnection:disconnect() + button2ClickedConnection = nil + removeNotification(notification) + if callback and type(callback) ~= "function" then -- callback should be a bindable + pcall(function() callback:Invoke(button2Text) end) + elseif type(callback) == "function" then + callback(notificationInfo.Button2Text) + end + end + end) + else + -- Resize button1 to take up all the space under the notification if button2 doesn't exist + if button1 then + button1.Size = UDim2.new(1, -2, .5, 0) + end + end + + notification.Frame = notificationFrame + notification.Duration = notificationInfo.Duration + insertNotification(notification) +end +BindableEvent_SendNotificationInfo.Event:connect(onSendNotificationInfo) + +-- New follower notification +spawn(function() + if isTenFootInterface then + --If on console, New follower notification should be blocked + return + end + + local RobloxReplicatedStorage = game:GetService('RobloxReplicatedStorage') + local RemoteEvent_NewFollower = RobloxReplicatedStorage:WaitForChild('NewFollower', 86400) or RobloxReplicatedStorage:WaitForChild('NewFollower') + + RemoteEvent_NewFollower.OnClientEvent:connect(function(followerRbxPlayer) + local message = ("%s is now following you"):format(followerRbxPlayer.Name) + if FFlagNotificationScript2UseFormatByKey then + message = RobloxTranslator:FormatByKey("NotificationScript2.NewFollower", {RBX_NAME = followerRbxPlayer.Name}) + else + if FFlagUseNotificationsLocalization then + message = string.gsub(LocalizedGetString("NotificationScript2.NewFollower",message),"{RBX_NAME}",followerRbxPlayer.Name) + end + end + + local image = getFriendImage(followerRbxPlayer.UserId) + sendNotificationInfo { + GroupName = "Friends", + Title = "New Follower", + Text = message, + DetailText = message, + Image = image, + Duration = 5 + } + end) +end) + +local checkFriendRequestIsThrottled; do + local friendRequestThrottlingMap = {} + + checkFriendRequestIsThrottled = function(fromPlayer) + local throttleFinishedTime = friendRequestThrottlingMap[fromPlayer] + + if throttleFinishedTime then + if tick() < throttleFinishedTime then + return true + end + end + + friendRequestThrottlingMap[fromPlayer] = tick() + FRIEND_REQUEST_NOTIFICATION_THROTTLE + return false + end +end + +local function sendFriendNotification(fromPlayer) + if checkFriendRequestIsThrottled(fromPlayer) then + return + end + + local acceptText = "Accept" + local declineText = "Decline" + sendNotificationInfo { + GroupName = "Friends", + Title = fromPlayer.Name, + Text = "Sent you a friend request!", + DetailText = fromPlayer.Name, + Image = getFriendImage(fromPlayer.UserId), + Duration = 8, + Callback = function(buttonChosen) + if buttonChosen == acceptText then + AnalyticsService:ReportCounter("NotificationScript-RequestFriendship") + AnalyticsService:TrackEvent("Game", "RequestFriendship", "NotificationScript") + + LocalPlayer:RequestFriendship(fromPlayer) + else + AnalyticsService:ReportCounter("NotificationScript-RevokeFriendship") + AnalyticsService:TrackEvent("Game", "RevokeFriendship", "NotificationScript") + + LocalPlayer:RevokeFriendship(fromPlayer) + FriendRequestBlacklist[fromPlayer] = true + end + end, + Button1Text = acceptText, + Button2Text = declineText + } +end + +local function onFriendRequestEvent(fromPlayer, toPlayer, event) + if fromPlayer ~= LocalPlayer and toPlayer ~= LocalPlayer then return end + -- + if fromPlayer == LocalPlayer then + if event == Enum.FriendRequestEvent.Accept then + local detailText = "You are now friends with " .. toPlayer.Name .. "!" + + if FFlagNotificationScript2UseFormatByKey then + detailText = RobloxTranslator:FormatByKey( + "NotificationScript2.FriendRequestEvent.Accept", + {RBX_NAME = toPlayer.Name}) + else + if FFlagUseNotificationsLocalization then + detailText = string.gsub(LocalizedGetString("NotificationScript2.FriendRequestEvent.Accept",detailText), "{RBX_NAME}", toPlayer.Name) + end + end + + sendNotificationInfo { + GroupName = "Friends", + Title = "New Friend", + Text = toPlayer.Name, + DetailText = detailText, + + Image = getFriendImage(toPlayer.UserId), + Duration = DEFAULT_NOTIFICATION_DURATION + } + + end + elseif toPlayer == LocalPlayer then + if event == Enum.FriendRequestEvent.Issue then + if FriendRequestBlacklist[fromPlayer] then return end + sendFriendNotification(fromPlayer) + elseif event == Enum.FriendRequestEvent.Accept then + local detailText = "You are now friends with " .. fromPlayer.Name .. "!" + + if FFlagNotificationScript2UseFormatByKey then + detailText = RobloxTranslator:FormatByKey("NotificationScript2.FriendRequestEvent.Accept", {RBX_NAME = fromPlayer.Name}) + + sendNotificationInfo { + GroupName = "Friends", + Title = "New Friend", + Text = fromPlayer.Name, + DetailText = detailText, + + Image = getFriendImage(fromPlayer.UserId), + Duration = DEFAULT_NOTIFICATION_DURATION + } + else + if FFlagUseNotificationsLocalization then + detailText = string.gsub(LocalizedGetString("NotificationScript2.FriendRequestEvent.Accept",detailText), "{RBX_NAME}", fromPlayer.Name) + end + + sendNotificationInfo { + GroupName = "Friends", + Title = "New Friend", + Text = fromPlayer.Name, + DetailText = "You are now friends with " .. fromPlayer.Name .. "!", + + Image = getFriendImage(fromPlayer.UserId), + Duration = DEFAULT_NOTIFICATION_DURATION + } + end + + end + end +end + +local function onPointsAwarded(userId, pointsAwarded, userBalanceInGame, userTotalBalance) + if pointsNotificationsActive and userId == LocalPlayer.UserId then + local title, text, detailText + if pointsAwarded == 1 then + title = "Point Awarded" + text = "You received 1 point!" + + if FFlagNotificationScript2UseFormatByKey then + text = RobloxTranslator:FormatByKey("NotificationScript2.onPointsAwarded.single", {RBX_NUMBER = tostring(pointsAwarded)}) + else + if FFlagUseNotificationsLocalization then + text = string.gsub(LocalizedGetString("NotificationScript2.onPointsAwarded.single",text),"{RBX_NUMBER}",pointsAwarded) + end + end + elseif pointsAwarded > 0 then + title = "Points Awarded" + text = ("You received %d points!"):format(pointsAwarded) + + if FFlagNotificationScript2UseFormatByKey then + text = RobloxTranslator:FormatByKey("NotificationScript2.onPointsAwarded.multiple", {RBX_NUMBER = tostring(pointsAwarded)}) + else + if FFlagUseNotificationsLocalization then + text = string.gsub(LocalizedGetString("NotificationScript2.onPointsAwarded.multiple",text),"{RBX_NUMBER}",pointsAwarded) + end + end + elseif pointsAwarded < 0 then + title = "Points Lost" + text = ("You lost %d points!"):format(math.abs(pointsAwarded)) + + if FFlagNotificationScript2UseFormatByKey then + text = RobloxTranslator:FormatByKey("NotificationScript2.onPointsAwarded.negative", {RBX_NUMBER = tostring(pointsAwarded)}) + else + if FFlagUseNotificationsLocalization then + text = string.gsub(LocalizedGetString("NotificationScript2.onPointsAwarded.negative",text),"{RBX_NUMBER}",math.abs(pointsAwarded)) + end + end + else + --don't notify for 0 points, shouldn't even happen + return + end + detailText = text + + sendNotificationInfo { + GroupName = "PlayerPoints", + Title = title, + Text = text, + DetailText = detailText, + Image = PLAYER_POINTS_IMG, + Duration = DEFAULT_NOTIFICATION_DURATION + } + end +end + +local function onBadgeAwarded(message, userId, badgeId) + if not BadgeBlacklist[badgeId] and badgesNotificationsActive and userId == LocalPlayer.UserId then + BadgeBlacklist[badgeId] = true + --SPTODO: Badge notifications are generated on the web and are not (for now) localized. + sendNotificationInfo { + GroupName = "BadgeAwards", + Title = "Badge Awarded", + Text = message, + DetailText = message, + Image = BADGE_IMG, + Duration = DEFAULT_NOTIFICATION_DURATION + } + end +end + +function onGameSettingsChanged(property, amount) + if property == "SavedQualityLevel" then + local level = GameSettings.SavedQualityLevel.Value + amount + if level > 10 then + level = 10 + elseif level < 1 then + level = 1 + end + -- value of 0 is automatic, we do not want to send a notification in that case + if level > 0 and level ~= CurrentGraphicsQualityLevel and GameSettings.SavedQualityLevel ~= Enum.SavedQualitySetting.Automatic then + local action = (level > CurrentGraphicsQualityLevel) and "Increased" or "Decreased" + local message = ("%s to (%d)"):format(action, level) + + if FFlagNotificationScript2UseFormatByKey then + if level > CurrentGraphicsQualityLevel then + message = RobloxTranslator:FormatByKey("NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Increased", {RBX_NUMBER = tostring(level)}) + else + message = RobloxTranslator:FormatByKey("NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Decreased", {RBX_NUMBER = tostring(level)}) + end + else + if FFlagUseNotificationsLocalization then + if level > CurrentGraphicsQualityLevel then + message = string.gsub(LocalizedGetString("NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Increased",message),"{RBX_NUMBER}",tostring(level)) + else + message = string.gsub(LocalizedGetString("NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Decreased",message),"{RBX_NUMBER}",tostring(level)) + end + end + end + + sendNotificationInfo { + GroupName = "Graphics", + Title = "Graphics Quality", + Text = message, + DetailText = message, + Image = "", + Duration = 2 + } + CurrentGraphicsQualityLevel = level + end + end +end + +BadgeService.BadgeAwarded:connect(onBadgeAwarded) +if not isTenFootInterface then + Players.FriendRequestEvent:connect(onFriendRequestEvent) + PointsService.PointsAwarded:connect(onPointsAwarded) + --GameSettings.Changed:connect(onGameSettingsChanged) + game.GraphicsQualityChangeRequest:connect(function(graphicsIncrease) --graphicsIncrease is a boolean + onGameSettingsChanged("SavedQualityLevel", graphicsIncrease == true and 1 or -1) + end) +end + +game.ScreenshotReady:Connect(function(path) + sendNotificationInfo { + Title = "Screenshot Taken", + Text = "Check out your screenshots folder to see it.", + Duration = 3.0, + Button1Text = "Open Folder", + Callback = function(text) + if text == "Open Folder" then + game:OpenScreenshotsFolder() + end + end + } +end) + +settings():GetService("GameSettings").VideoRecordingChangeRequest:Connect(function(value) + if not value then + sendNotificationInfo { + Title = "Video Recorded", + Text = "Check out your videos folder to see it.", + Duration = 3.0, + Button1Text = "Open Folder", + Callback = function(text) + if text == "Open Folder" then + game:OpenVideosFolder() + end + end + } + end +end) + +GuiService.SendCoreUiNotification = function(title, text) + local notification = createNotification(title, text, "") + notification.BackgroundTransparency = .5 + notification.Size = UDim2.new(.5, 0, .1, 0) + notification.Position = UDim2.new(.25, 0, -0.1, 0) + notification.NotificationTitle.FontSize = Enum.FontSize.Size36 + notification.NotificationText.FontSize = Enum.FontSize.Size24 + notification.Parent = RbxGui + notification:TweenPosition(UDim2.new(.25, 0, 0, 0), EASE_DIR, EASE_STYLE, TWEEN_TIME, true) + wait(5) + if notification then + notification:Destroy() + end +end + +-- This is used for when a player calls CreatePlaceInPlayerInventoryAsync +local function onClientLuaDialogRequested(msg, accept, decline) + PopupText.Text = msg + -- + local acceptCn, declineCn = nil, nil + local function disconnectCns() + if acceptCn then acceptCn:disconnect() end + if declineCn then declineCn:disconnect() end + -- + GuiService:RemoveCenterDialog(PopupFrame) + PopupFrame.Visible = false + end + + acceptCn = PopupAcceptButton.MouseButton1Click:connect(function() + disconnectCns() + MarketplaceService:SignalServerLuaDialogClosed(true) + end) + declineCn = PopupDeclineButton.MouseButton1Click:connect(function() + disconnectCns() + MarketplaceService:SignalServerLuaDialogClosed(false) + end) + + local centerDialogSuccess = pcall( + function() + GuiService:AddCenterDialog(PopupFrame, Enum.CenterDialogType.QuitDialog, + function() + PopupOKButton.Visible = false + PopupAcceptButton.Visible = true + PopupDeclineButton.Visible = true + PopupAcceptButton.Text = accept + PopupDeclineButton.Text = decline + PopupFrame.Visible = true + end, + function() + PopupFrame.Visible = false + end) + end) + + if not centerDialogSuccess then + PopupFrame.Visible = true + PopupAcceptButton.Text = accept + PopupDeclineButton.Text = decline + end + + return true +end +MarketplaceService.ClientLuaDialogRequested:connect(onClientLuaDialogRequested) + +local function createDeveloperNotification(notificationTable) + if type(notificationTable) == "table" then + if type(notificationTable.Title) == "string" and type(notificationTable.Text) == "string" then + local iconImage = (type(notificationTable.Icon) == "string" and notificationTable.Icon or "") + local duration = (type(notificationTable.Duration) == "number" and notificationTable.Duration or DEFAULT_NOTIFICATION_DURATION) + local bindable = (typeof(notificationTable.Callback) == "Instance" and notificationTable.Callback:IsA("BindableFunction") and notificationTable.Callback or nil) + local button1Text = (type(notificationTable.Button1) == "string" and notificationTable.Button1 or "") + local button2Text = (type(notificationTable.Button2) == "string" and notificationTable.Button2 or "") + -- AutoLocalize allows developers to disable automatic localization if they have pre-localized it. Defaults true. + local autoLocalize = notificationTable.AutoLocalize == nil or notificationTable.AutoLocalize == true + local title = autoLocalize and GameTranslator:TranslateGameText(CoreGui, notificationTable.Title) or notificationTable.Title + local text = autoLocalize and GameTranslator:TranslateGameText(CoreGui, notificationTable.Text) or notificationTable.Text + local localizedButton1Text = autoLocalize and GameTranslator:TranslateGameText(CoreGui, button1Text) or nil + local localizedButton2Text = autoLocalize and GameTranslator:TranslateGameText(CoreGui, button2Text) or nil + sendNotificationInfo { + GroupName = "Developer", + Title = title, + Text = text, + Image = iconImage, + Duration = duration, + Callback = bindable, + Button1Text = button1Text, + Button2Text = button2Text, + Button1TextLocalized = localizedButton1Text, + Button2TextLocalized = localizedButton2Text + } + end + end +end + +StarterGui:RegisterSetCore("PointsNotificationsActive", function(value) if type(value) == "boolean" then pointsNotificationsActive = value end end) +StarterGui:RegisterSetCore("BadgesNotificationsActive", function(value) if type(value) == "boolean" then badgesNotificationsActive = value end end) + +StarterGui:RegisterGetCore("PointsNotificationsActive", function() return pointsNotificationsActive end) +StarterGui:RegisterGetCore("BadgesNotificationsActive", function() return badgesNotificationsActive end) + +StarterGui:RegisterSetCore("SendNotification", createDeveloperNotification) + + +if not isTenFootInterface then + local gamepadMenu = RobloxGui:WaitForChild("CoreScripts/GamepadMenu") + local gamepadNotifications = gamepadMenu:FindFirstChild("GamepadNotifications") + while not gamepadNotifications do + wait() + gamepadNotifications = gamepadMenu:FindFirstChild("GamepadNotifications") + end + + local leaveNotificationFunc = function(name, state, inputObject) + if state ~= Enum.UserInputState.Begin then return end + + if GuiService.SelectedCoreObject:IsDescendantOf(NotificationFrame) then + GuiService.SelectedCoreObject = nil + end + + ContextActionService:UnbindCoreAction("LeaveNotificationSelection") + end + + gamepadNotifications.Event:connect(function(isSelected) + if not isSelected then return end + + isPaused = true + local notifications = NotificationFrame:GetChildren() + for i = 1, #notifications do + local noteComponents = notifications[i]:GetChildren() + for j = 1, #noteComponents do + if noteComponents[j]:IsA("GuiButton") and noteComponents[j].Visible then + GuiService.SelectedCoreObject = noteComponents[j] + break + end + end + end + + if GuiService.SelectedCoreObject then + ContextActionService:BindCoreAction("LeaveNotificationSelection", leaveNotificationFunc, false, Enum.KeyCode.ButtonB) + else + isPaused = false + local utility = require(RobloxGui.Modules.Settings.Utility) + local okPressedFunc = function() end + utility:ShowAlert("You have no notifications", "Ok", --[[settingsHub]] nil, okPressedFunc, true) + end + end) + + GuiService.Changed:connect(function(prop) + if prop == "SelectedCoreObject" then + if not GuiService.SelectedCoreObject or not GuiService.SelectedCoreObject:IsDescendantOf(NotificationFrame) then + isPaused = false + end + end + end) +end + +local UserInputService = game:GetService('UserInputService') +local Platform = UserInputService:GetPlatform() +local Modules = RobloxGui:FindFirstChild('Modules') +local CSMModule = Modules:FindFirstChild('ControllerStateManager') +if Modules and not CSMModule then + local ShellModules = Modules:FindFirstChild('Shell') + if ShellModules then + CSMModule = ShellModules:FindFirstChild('ControllerStateManager') + end +end + +if Platform == Enum.Platform.XBoxOne then + -- Platform hook for controller connection events + -- Displays overlay to user on controller connection lost + local PlatformService = nil + pcall(function() PlatformService = game:GetService('PlatformService') end) + if PlatformService and CSMModule then + local controllerStateManager = require(CSMModule) + if controllerStateManager then + controllerStateManager:Initialize() + + if not game:IsLoaded() then + game.Loaded:wait() + end + + -- retro check in case of controller disconnect while loading + -- for now, gamepad1 is always mapped to the active user + controllerStateManager:CheckUserConnected() + end + end +end diff --git a/Client2018/content/scripts/CoreScripts/CoreScripts/PerformanceStatsManagerScript.lua b/Client2018/content/scripts/CoreScripts/CoreScripts/PerformanceStatsManagerScript.lua new file mode 100644 index 0000000..d5a3164 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/CoreScripts/PerformanceStatsManagerScript.lua @@ -0,0 +1,229 @@ + +--[[ + Filename: PerformanceStatsManagerScript.lua + Written by: dbanks + Description: Handles performance stats gui. +--]] + +--[[ Services ]]-- +local PlayersService = game:GetService("Players") +local Settings = UserSettings() +local GameSettings = Settings.GameSettings +local CoreGuiService = game:GetService('CoreGui') + +--[[ Modules ]]-- +local GoogleAnalyticsUtils = require(CoreGuiService.RobloxGui.Modules.GoogleAnalyticsUtils) +local StatsAggregatorManagerClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsAggregatorManager) +local StatsButtonClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsButton) +local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils) +local StatsViewerClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsViewer) +local TopbarConstants = require(CoreGuiService.RobloxGui.Modules.TopbarConstants) + +--[[ Script Variables ]]-- +local masterFrame = Instance.new("Frame") +masterFrame.Name = "PerformanceStats" + +local FFlagStatUtilsUseFormatByKey = settings():GetFFlag('StatUtilsUseFormatByKey') + +local statsAggregatorManager = StatsAggregatorManagerClass.getSingleton() +local statsViewer = StatsViewerClass.new() +local statsButtonsByType ={} +local currentStatType = nil + +for i, statType in ipairs(StatsUtils.AllStatTypes) do + local button = StatsButtonClass.new(statType) + statsButtonsByType[statType] = button +end + +function ShowMasterFrame() + masterFrame.Visible = true + masterFrame.Parent = CoreGuiService.RobloxGui +end + +function HideMasterFrame() + masterFrame.Visible = false + masterFrame.Parent = nil +end + +--[[ Functions ]]-- +function ConfigureMasterFrame() + -- Set up the main frame that contains the whole PS GUI. + -- Avoid the top button bar. + masterFrame.Position = UDim2.new(0, 0, 0, 0) + masterFrame.Size = UDim2.new(1, 0, 1, 0) + masterFrame.Selectable = false + masterFrame.BackgroundTransparency = 1.0 + masterFrame.Active = false + masterFrame.ZIndex = 0 + + if FFlagStatUtilsUseFormatByKey then + HideMasterFrame() + end + + -- FIXME(dbanks) + -- Debug, can see the whole frame. + -- masterFrame.BackgroundColor3 = Color3.new(0, 0.5, 0.5) + -- masterFrame.BackgroundTransparency = 0.8 +end + +function ConfigureStatButtonsInMasterFrame() + -- Set up the row of buttons across the top and handler for button press. + for i, statType in ipairs(StatsUtils.AllStatTypes) do + AddButton(statType, i) + end +end + +function OnButtonToggled(toggledStatType) + local toggledButton = statsButtonsByType[toggledStatType] + local selectedState = toggledButton._isSelected + selectedState = not selectedState + + if (selectedState) then + currentStatType = toggledStatType + else + currentStatType = nil + end + + UpdateButtonSelectedStates() + UpdateViewerVisibility() +end + +function UpdateButtonSelectedStates() + for i, buttonType in ipairs(StatsUtils.AllStatTypes) do + local button = statsButtonsByType[buttonType] + button:SetIsSelected(buttonType == currentStatType) + end +end + +function UpdateViewerVisibility() + -- If a particular button/tab is on, show the Viewer. + -- + -- Don't bother if we're already there. + if (currentStatType == nil) then + if statsViewer:GetIsVisible() then + statsViewer:SetVisible(false) + statsViewer:SetStatsAggregator(nil) + end + else + local somethingChanged = false + if not statsViewer:GetIsVisible() then + somethingChanged = true + statsViewer:SetVisible(true) + end + + if currentStatType ~= statsViewer:GetStatType() then + statsViewer:SetStatType(currentStatType) + statsViewer:SetStatsAggregator(statsAggregatorManager:GetAggregator(currentStatType)) + somethingChanged = true + end + + if somethingChanged then + -- track it. + game:ReportInGoogleAnalytics(GoogleAnalyticsUtils.CA_CATEGORY_GAME, + "Enlarge PerfStat", + currentStatType, + 0) + end + end +end + +function AddButton(statType, index) + -- Configure size and position of button. + -- Configure callback behavior to toggle + -- button on or off and show/hide viewer. + -- Parent button in main screen. + local button = statsButtonsByType[statType] + + button:SetParent(masterFrame) + button:SetStatsAggregator( + statsAggregatorManager:GetAggregator(statType)) + + local fraction = 1.0/StatsUtils.NumButtonTypes + local size = UDim2.new(fraction, 0, 0, StatsUtils.ButtonHeight) + local position = UDim2.new(fraction * (index - 1), 0, 0, 0) + button:SetSizeAndPosition(size, position) + + button:SetToggleCallbackFunction(OnButtonToggled) +end + +function ConfigureStatViewerInMasterFrame() + -- Set up the widget that shows currently selected button. + statsViewer:SetParent(masterFrame) + + local size = UDim2.new(0, StatsUtils.ViewerWidth, 0, StatsUtils.ViewerHeight) + local position = UDim2.new(1, -StatsUtils.ViewerWidth, + 0, StatsUtils.ButtonHeight + StatsUtils.ViewerTopMargin) + + statsViewer:SetSizeAndPosition(size, position) +end + +function UpdatePerformanceStatsVisibility() + local shouldBeVisible = StatsUtils.PerformanceStatsShouldBeVisible() + + if (shouldBeVisible == masterFrame.Visible) then + return + end + + if FFlagStatUtilsUseFormatByKey then + if shouldBeVisible then + ShowMasterFrame() + else + HideMasterFrame() + end + else + if shouldBeVisible then + masterFrame.Visible = true + masterFrame.Parent = CoreGuiService.RobloxGui + else + masterFrame.Visible = false + masterFrame.Parent = nil + end + end + + -- Let the children respond to the transition that they are/are not visible. + statsViewer:OnPerformanceStatsShouldBeVisibleChanged() + for i, buttonType in ipairs(StatsUtils.AllStatTypes) do + local button = statsButtonsByType[buttonType] + button:OnPerformanceStatsShouldBeVisibleChanged() + end + + -- track it. + local actionName = "Hide PerfStats" + if shouldBeVisible then + actionName = "Show PerfStats" + end + game:ReportInGoogleAnalytics(GoogleAnalyticsUtils.CA_CATEGORY_GAME, + actionName, + "", + 0) +end + + +--[[ Top Level Code ]]-- +-- Set up our GUI. +ConfigureMasterFrame() +ConfigureStatButtonsInMasterFrame() +ConfigureStatViewerInMasterFrame() + + +-- Watch for changes in performance stats visibility. +GameSettings.PerformanceStatsVisibleChanged:connect( + UpdatePerformanceStatsVisibility) + +-- Make sure we're showing buttons and viewer based on current selection. +UpdateButtonSelectedStates() +UpdateViewerVisibility() + +-- Make sure stats are visible or not, as specified by current setting. +UpdatePerformanceStatsVisibility() + +-- This may change if Player shows up... +spawn(function() + local localPlayer = PlayersService.LocalPlayer + while not localPlayer do + PlayersService.PlayerAdded:wait() + localPlayer = PlayersService.LocalPlayer + end + UpdatePerformanceStatsVisibility() +end) + diff --git a/Client2018/content/scripts/CoreScripts/CoreScripts/PurchasePromptScript2.lua b/Client2018/content/scripts/CoreScripts/CoreScripts/PurchasePromptScript2.lua new file mode 100644 index 0000000..40ad021 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/CoreScripts/PurchasePromptScript2.lua @@ -0,0 +1,1862 @@ +--[[ + // Filename: PurchasePromptScript2.lua + // Version 1.0 + // Release 186 + // Written by: jeditkacheff/jmargh + // Description: Handles in game purchases +]]-- + +local success, result = pcall(function() return settings():GetFFlag('UsePurchasePromptLocalization') end) +local FFlagUsePurchasePromptLocalization = success and result + +local FFlagThwartPurchasePromptScams = settings():GetFFlag("ThwartPurchasePromptScams") +local FFlagThwartPurchasePromptScamsGamepad = settings():GetFFlag("ThwartPurchasePromptScamsGamepad") +local FFlagDelayPurchasePromptActivation = settings():GetFFlag("DelayPurchasePromptActivation") +local FFlagFixDesktopRobuxUpsell = settings():GetFFlag("FixDesktopRobuxUpsell") + +local AssetService = game:GetService('AssetService') +local GuiService = game:GetService('GuiService') +local HttpService = game:GetService('HttpService') +local HttpRbxApiService = game:GetService('HttpRbxApiService') +local InsertService = game:GetService('InsertService') +local MarketplaceService = game:GetService('MarketplaceService') +local Players = game:GetService('Players') +local UserInputService = game:GetService('UserInputService') +local RunService = game:GetService("RunService") + +local RobloxGui = script.Parent +local ThirdPartyProductName = nil + +local RobloxTranslator +local FFlagCoreScriptsUseLocalizationModule = settings():GetFFlag('CoreScriptsUseLocalizationModule') +if FFlagCoreScriptsUseLocalizationModule then + RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator) +end + +local function LocalizedGetString(key, rtv) + pcall(function() + if FFlagCoreScriptsUseLocalizationModule then + rtv = RobloxTranslator:FormatByKey(key) + else + local LocalizationService = game:GetService("LocalizationService") + local CorescriptLocalization = LocalizationService:GetCorescriptLocalizations()[1] + rtv = CorescriptLocalization:GetString(LocalizationService.RobloxLocaleId, key) + end + end) + return rtv +end + +local platform = UserInputService:GetPlatform() +local IsNativePurchasing = platform == Enum.Platform.XBoxOne or + platform == Enum.Platform.IOS or + platform == Enum.Platform.Android or + platform == Enum.Platform.UWP + +local IsCurrentlyPrompting = false +local IsCurrentlyPurchasing = false +local IsPurchasingConsumable = false +local IsPurchasingGamePass = false +local IsCheckingPlayerFunds = false +RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface") +local TenFootInterface = require(RobloxGui.Modules.TenFootInterface) +local isTenFootInterface = TenFootInterface:IsEnabled() +local freezeControllerActionName = "doNothingActionPrompt" +local freezeThumbstick1Name = "doNothingThumbstickPrompt" +local freezeThumbstick2Name = "doNothingThumbstickPrompt" +local _,largeFont = pcall(function() return Enum.FontSize.Size42 end) +largeFont = largeFont or Enum.FontSize.Size36 +local scaleFactor = 3 +local purchaseState = nil +local buttonsActive = false + +local PurchaseData = { + AssetId = nil, + ProductId = nil, + CurrencyType = nil, + EquipOnPurchase = nil, + ProductInfo = nil, + ItemDescription = nil, +} + +local BASE_URL = game:GetService('ContentProvider').BaseUrl:lower() +BASE_URL = string.gsub(BASE_URL, "/m.", "/www.") +local THUMBNAIL_URL = BASE_URL.."thumbs/asset.ashx?assetid=" +-- Images +local BG_IMAGE = 'rbxasset://textures/ui/Modal.png' +local PURCHASE_BG = 'rbxasset://textures/ui/LoadingBKG.png' +local BUTTON_LEFT = 'rbxasset://textures/ui/ButtonLeft.png' +local BUTTON_LEFT_DOWN = 'rbxasset://textures/ui/ButtonLeftDown.png' +local BUTTON_RIGHT = 'rbxasset://textures/ui/ButtonRight.png' +local BUTTON_RIGHT_DOWN = 'rbxasset://textures/ui/ButtonRightDown.png' +local BUTTON = 'rbxasset://textures/ui/SingleButton.png' +local BUTTON_DOWN = 'rbxasset://textures/ui/SingleButtonDown.png' +local ROBUX_ICON = 'rbxasset://textures/ui/RobuxIcon.png' +local TIX_ICON = 'rbxasset://textures/ui/TixIcon.png' +local ERROR_ICON = 'rbxasset://textures/ui/ErrorIcon.png' +local A_BUTTON = "rbxasset://textures/ui/Settings/Help/AButtonDark.png" +local B_BUTTON = "rbxasset://textures/ui/Settings/Help/BButtonDark.png" +local DEFAULT_XBOX_IMAGE = 'rbxasset://textures/ui/Shell/Icons/ROBUXIcon@1080.png' + +local CONTROLLER_CONFIRM_ACTION_NAME = "CoreScriptPurchasePromptControllerConfirm" +local CONTROLLER_CANCEL_ACTION_NAME = "CoreScriptPurchasePromptControllerCancel" +local GAMEPAD_BUTTONS = {} + +local ERROR_MSG = { + PURCHASE_DISABLED = "In-game purchases are temporarily disabled", + INVALID_FUNDS = "your account does not have enough ROBUX", + UNKNOWN = "ROBLOX is performing maintenance", + UNKNWON_FAILURE = "something went wrong" +} +local PURCHASE_MSG = { + SUCCEEDED = "Your purchase of itemName succeeded!", + FAILED = "Your purchase of itemName failed because errorReason. Your account has not been charged. Please try again later.", + PURCHASE = "Want to buy the assetType\nitemName for", + PURCHASE_TIX = "Want to buy the assetType\nitemName for", + FREE = "Would you like to take the assetType itemName for FREE?", + FREE_BALANCE = "Your account balance will not be affected by this transaction.", + BALANCE_FUTURE = "Your balance after this transaction will be ", + BALANCE_NOW = "Your balance is now ", + ALREADY_OWN = "You already own this item. Your account has not been charged.", + MOCK_PURCHASE = "This is a test purchase; your account will not be charged.", + MOCK_PURCHASE_SUCCESS = "This was a test purchase." +} +local PURCHASE_FAILED = { + DEFAULT_ERROR = 0, + IN_GAME_PURCHASE_DISABLED = 1, + CANNOT_GET_BALANCE = 2, + CANNOT_GET_ITEM_PRICE = 3, + NOT_FOR_SALE = 4, + NOT_ENOUGH_TIX = 5, + UNDER_13 = 6, + LIMITED = 7, + DID_NOT_BUY_ROBUX = 8, + PROMPT_PURCHASE_ON_GUEST = 9, + THIRD_PARTY_DISABLED = 10, +} +local PURCHASE_STATE = { + DEFAULT = 1, + FAILED = 2, + SUCCEEDED = 3, + BUYITEM = 4, + BUYROBUX = 5, + BUYINGROBUX = 6, + BUYBC = 7 +} + +local function studioMockPurchasesEnabled() + local result = false + pcall(function() result = game:GetService("RunService"):IsStudio() end) + return result +end + +local BC_LVL_TO_STRING = { + "Builders Club", + "Turbo Builders Club", + "Outrageous Builders Club", +} +local ASSET_TO_STRING = { + [1] = "Image"; + [2] = "T-Shirt"; + [3] = "Audio"; + [4] = "Mesh"; + [5] = "Lua"; + [6] = "HTML"; + [7] = "Text"; + [8] = "Hat"; + [9] = "Place"; + [10] = "Model"; + [11] = "Shirt"; + [12] = "Pants"; + [13] = "Decal"; + [16] = "Avatar"; + [17] = "Head"; + [18] = "Face"; + [19] = "Gear"; + [21] = "Badge"; + [22] = "Group Emblem"; + [24] = "Animation"; + [25] = "Arms"; + [26] = "Legs"; + [27] = "Torso"; + [28] = "Right Arm"; + [29] = "Left Arm"; + [30] = "Left Leg"; + [31] = "Right Leg"; + [32] = "Package"; + [33] = "YouTube Video"; + [34] = "Game Pass"; + [38] = "Plugin"; + [39] = "SolidModel"; + [40] = "MeshPart"; + [41] = "Hair Accessory"; + [42] = "Face Accessory"; + [43] = "Neck Accessory"; + [44] = "Shoulder Accessory"; + [45] = "Front Accessory"; + [46] = "Back Accessory"; + [47] = "Waist Accessory"; + [48] = "Climb Animation"; + [50] = "Fall Animation"; + [51] = "Idle Animation"; + [52] = "Jump Animation"; + [53] = "Run Animation"; + [54] = "Swim Animation"; + [55] = "Walk Animation"; + [56] = "Pose Animation"; + [57] = "Ear Accessory"; + [58] = "Eye Accessory"; + [0] = "Product"; + -- NOTE: GamePass and Plugin AssetTypeIds are now in sync on ST1, ST2 and ST3 +} + +local BC_ROBUX_PRODUCTS = { 90, 180, 270, 360, 450, 1000, 2750 } +local NON_BC_ROBUX_PRODUCTS = { 80, 160, 240, 320, 400, 800, 2000 } + +local DIALOG_SIZE = UDim2.new(0, 324, 0, 180) +local DIALOG_SIZE_TENFOOT = UDim2.new(0, 324*scaleFactor, 0, 180*scaleFactor) +local SHOW_POSITION = UDim2.new(0.5, -162, 0.5, -90) +local SHOW_POSITION_TENFOOT = UDim2.new(0.5, -162*scaleFactor, 0.5, -90*scaleFactor) +local HIDE_POSITION = UDim2.new(0.5, -162, 0, -181) +local HIDE_POSITION_TENFOOT = UDim2.new(0.5, -162*scaleFactor, 0, -180*scaleFactor - 1) +local BTN_SIZE = UDim2.new(0, 162, 0, 44) +local BTN_SIZE_TENFOOT = UDim2.new(0, 162*scaleFactor, 0, 44*scaleFactor) +local BODY_SIZE = UDim2.new(0, 324, 0, 136) +local BODY_SIZE_TENFOOT = UDim2.new(0, 324*scaleFactor, 0, 136*scaleFactor) +local TWEEN_TIME = 0.3 + +local BTN_L_POS = UDim2.new(0, 0, 0, 136) +local BTN_L_POS_TENFOOT = UDim2.new(0, 0, 0, 136*scaleFactor) +local BTN_R_POS = UDim2.new(0.5, 0, 0, 136) +local BTN_R_POS_TENFOOT = UDim2.new(0.5, 0, 0, 136*scaleFactor) + +local function lerp( start, finish, t) + return (1 - t) * start + t * finish +end + +local function formatNumber(value) + return tostring(tostring(value):reverse():gsub("%d%d%d", "%1,"):reverse():gsub("^,", "")) +end + +local function createFrame(name, size, position, bgTransparency, bgColor) + local frame = Instance.new('Frame') + frame.Name = name + frame.Size = size + frame.Position = position or UDim2.new(0, 0, 0, 0) + frame.BackgroundTransparency = bgTransparency + frame.BackgroundColor3 = bgColor or Color3.new() + frame.BorderSizePixel = 0 + frame.ZIndex = 8 + + return frame +end + +local function createTextLabel(name, size, position, font, fontSize, text) + local textLabel = Instance.new('TextLabel') + textLabel.Name = name + textLabel.Size = size or UDim2.new(0, 0, 0, 0) + textLabel.Position = position + textLabel.BackgroundTransparency = 1 + textLabel.Font = font + textLabel.FontSize = fontSize + textLabel.TextColor3 = Color3.new(1, 1, 1) + textLabel.Text = text + textLabel.ZIndex = 8 + + return textLabel +end + +local function createImageLabel(name, size, position, image) + local imageLabel = Instance.new('ImageLabel') + imageLabel.Name = name + imageLabel.Size = size + imageLabel.BackgroundTransparency = 1 + imageLabel.Position = position + imageLabel.Image = image + + return imageLabel +end + +local function createImageButtonWithText(name, position, image, imageDown, text, font) + local imageButton = Instance.new('ImageButton') + imageButton.Name = name + imageButton.Size = isTenFootInterface and BTN_SIZE_TENFOOT or BTN_SIZE + imageButton.Position = position + imageButton.Image = image + imageButton.BackgroundTransparency = 1 + imageButton.AutoButtonColor = false + imageButton.ZIndex = 8 + imageButton.Modal = true + + local textLabel = nil + if FFlagUsePurchasePromptLocalization then + textLabel = createTextLabel(name.."Text", UDim2.new(0.6, 0, 0.8, 0), UDim2.new(0.2, 0, 0.1, 0), font, isTenFootInterface and largeFont or Enum.FontSize.Size24, text) + textLabel.ZIndex = 9 + textLabel.Parent = imageButton + textLabel.TextScaled = true + textLabel.TextWrapped = true + local textSizeConstraint = Instance.new("UITextSizeConstraint",textLabel) + textSizeConstraint.MaxTextSize = textLabel.TextSize + else + textLabel = createTextLabel(name.."Text", UDim2.new(1, 0, 1, 0), UDim2.new(0, 0, 0, 0), font, isTenFootInterface and largeFont or Enum.FontSize.Size24, text) + textLabel.ZIndex = 9 + textLabel.Parent = imageButton + end + + imageButton.MouseEnter:connect(function() + imageButton.Image = imageDown + end) + imageButton.MouseLeave:connect(function() + imageButton.Image = image + end) + imageButton.MouseButton1Click:connect(function() + imageButton.Image = image + end) + + return imageButton +end + +local PurchaseDialog = isTenFootInterface and createFrame("PurchaseDialog", DIALOG_SIZE_TENFOOT, HIDE_POSITION_TENFOOT, 1, nil) or createFrame("PurchaseDialog", DIALOG_SIZE, HIDE_POSITION, 1, nil) +PurchaseDialog.Visible = false +PurchaseDialog.Parent = RobloxGui + + local ContainerFrame = createFrame("ContainerFrame", UDim2.new(1, 0, 1, 0), nil, 1, nil) + ContainerFrame.Parent = PurchaseDialog + + local ContainerImage = createImageLabel("ContainerImage", isTenFootInterface and BODY_SIZE_TENFOOT or BODY_SIZE, UDim2.new(0, 0, 0, 0), BG_IMAGE) + ContainerImage.ZIndex = 8 + ContainerImage.Parent = ContainerFrame + + local ItemPreviewImage + if isTenFootInterface then + ItemPreviewImage = createImageLabel("ItemPreviewImage", UDim2.new(0, 64*scaleFactor, 0, 64*scaleFactor), UDim2.new(0, 27*scaleFactor, 0, 20*scaleFactor), "") + else + ItemPreviewImage = createImageLabel("ItemPreviewImage", UDim2.new(0, 64, 0, 64), UDim2.new(0, 27, 0, 20), "") + end + ItemPreviewImage.ZIndex = 9 + ItemPreviewImage.Parent = ContainerFrame + + local ItemDescriptionText_str = PURCHASE_MSG.PURCHASE + if FFlagUsePurchasePromptLocalization then + ItemDescriptionText_str = LocalizedGetString("PurchasePromptScript.PURCHASE_MSG.PURCHASE",ItemDescriptionText_str) + end + + local ItemDescriptionText = createTextLabel( + "ItemDescriptionText", + isTenFootInterface and UDim2.new(0, 210*scaleFactor - 20, 0, 96*scaleFactor) or UDim2.new(0, 210, 0, 96), + isTenFootInterface and UDim2.new(0, 110*scaleFactor, 0, 18*scaleFactor) or UDim2.new(0, 110, 0, 18), + Enum.Font.SourceSans, + isTenFootInterface and Enum.FontSize.Size48 or Enum.FontSize.Size18, + ItemDescriptionText_str + ) + ItemDescriptionText.TextXAlignment = Enum.TextXAlignment.Left + ItemDescriptionText.TextYAlignment = Enum.TextYAlignment.Top + ItemDescriptionText.TextWrapped = true + ItemDescriptionText.Parent = ContainerFrame + + local RobuxIcon = createImageLabel("RobuxIcon", isTenFootInterface and UDim2.new(0, 20*scaleFactor, 0, 20*scaleFactor) or UDim2.new(0, 20, 0, 20), UDim2.new(0, 0, 0, 0), ROBUX_ICON) + RobuxIcon.ZIndex = 9 + RobuxIcon.Visible = false + RobuxIcon.Parent = ContainerFrame + + local TixIcon = createImageLabel("TixIcon", isTenFootInterface and UDim2.new(0, 20*scaleFactor, 0, 20*scaleFactor) or UDim2.new(0, 20, 0, 20), UDim2.new(0, 0, 0, 0), TIX_ICON) + TixIcon.ZIndex = 9 + TixIcon.Visible = false + TixIcon.Parent = ContainerFrame + + local CostText = createTextLabel("CostText", UDim2.new(0, 0, 0, 0), UDim2.new(0, 0, 0, 0), + Enum.Font.SourceSansBold, isTenFootInterface and largeFont or Enum.FontSize.Size18, "") + CostText.TextXAlignment = Enum.TextXAlignment.Left + CostText.Visible = false + CostText.Parent = ContainerFrame + + local PostBalanceText = createTextLabel("PostBalanceText", UDim2.new(1, -20, 0, 30), isTenFootInterface and UDim2.new(0, 10, 0, 100*scaleFactor) or UDim2.new(0, 10, 0, 100), Enum.Font.SourceSans, + isTenFootInterface and Enum.FontSize.Size36 or Enum.FontSize.Size14, "") + PostBalanceText.TextWrapped = true + PostBalanceText.Parent = ContainerFrame + + local BuyButton = createImageButtonWithText("BuyButton", isTenFootInterface and BTN_L_POS_TENFOOT or BTN_L_POS, BUTTON_LEFT, BUTTON_LEFT_DOWN, "Buy Now", Enum.Font.SourceSansBold) + BuyButton.Parent = ContainerFrame + local BuyButtonText = BuyButton:FindFirstChild("BuyButtonText") + + local gamepadButtonXLocation = (BuyButton.AbsoluteSize.X/2 - BuyButtonText.TextBounds.X/2)/2 + local buyButtonGamepadImage = Instance.new("ImageLabel") + if FFlagUsePurchasePromptLocalization then + buyButtonGamepadImage.BackgroundTransparency = 1 + buyButtonGamepadImage.Image = A_BUTTON + buyButtonGamepadImage.Size = UDim2.new(0.75, -8, 0.75, -8) + buyButtonGamepadImage.SizeConstraint = Enum.SizeConstraint.RelativeYY + buyButtonGamepadImage.Parent = BuyButton + buyButtonGamepadImage.Position = UDim2.new(0, buyButtonGamepadImage.AbsoluteSize.x * 0.65, 0.5, 0) + buyButtonGamepadImage.AnchorPoint = Vector2.new(0.5,0.5) + buyButtonGamepadImage.Visible = false + buyButtonGamepadImage.ZIndex = BuyButton.ZIndex + table.insert(GAMEPAD_BUTTONS, buyButtonGamepadImage) + else + buyButtonGamepadImage.BackgroundTransparency = 1 + buyButtonGamepadImage.Image = A_BUTTON + buyButtonGamepadImage.Size = UDim2.new(1, -8, 1, -8) + buyButtonGamepadImage.SizeConstraint = Enum.SizeConstraint.RelativeYY + buyButtonGamepadImage.Parent = BuyButton + buyButtonGamepadImage.Position = UDim2.new(0, gamepadButtonXLocation - buyButtonGamepadImage.AbsoluteSize.X/2, 0, 5) + buyButtonGamepadImage.Visible = false + buyButtonGamepadImage.ZIndex = BuyButton.ZIndex + table.insert(GAMEPAD_BUTTONS, buyButtonGamepadImage) + end + + local CancelButton = createImageButtonWithText("CancelButton", isTenFootInterface and BTN_R_POS_TENFOOT or BTN_R_POS, BUTTON_RIGHT, BUTTON_RIGHT_DOWN, "Cancel", Enum.Font.SourceSans) + CancelButton.Parent = ContainerFrame + + local cancelButtonGamepadImage = buyButtonGamepadImage:Clone() + cancelButtonGamepadImage.Image = B_BUTTON + cancelButtonGamepadImage.ZIndex = CancelButton.ZIndex + cancelButtonGamepadImage.Parent = CancelButton + table.insert(GAMEPAD_BUTTONS, cancelButtonGamepadImage) + + local BuyRobuxButton = createImageButtonWithText("BuyRobuxButton", isTenFootInterface and BTN_L_POS_TENFOOT or BTN_L_POS, BUTTON_LEFT, BUTTON_LEFT_DOWN, IsNativePurchasing and "Buy" or "Buy R$", + Enum.Font.SourceSansBold) + BuyRobuxButton.Visible = false + BuyRobuxButton.Parent = ContainerFrame + + local buyRobuxGamepadImage = buyButtonGamepadImage:Clone() + buyRobuxGamepadImage.ZIndex = BuyRobuxButton.ZIndex + buyRobuxGamepadImage.Parent = BuyRobuxButton + table.insert(GAMEPAD_BUTTONS, buyRobuxGamepadImage) + + local BuyBCButton = createImageButtonWithText("BuyBCButton", isTenFootInterface and BTN_L_POS_TENFOOT or BTN_L_POS, BUTTON_LEFT, BUTTON_LEFT_DOWN, "Upgrade", Enum.Font.SourceSansBold) + BuyBCButton.Visible = false + BuyBCButton.Parent = ContainerFrame + + local buyBCGamepadImage = buyButtonGamepadImage:Clone() + buyBCGamepadImage.ZIndex = BuyBCButton.ZIndex + buyBCGamepadImage.Parent = BuyBCButton + table.insert(GAMEPAD_BUTTONS, buyBCGamepadImage) + + local FreeButton = createImageButtonWithText("FreeButton", isTenFootInterface and BTN_L_POS_TENFOOT or BTN_L_POS, BUTTON_LEFT, BUTTON_LEFT_DOWN, "Take Free", Enum.Font.SourceSansBold) + FreeButton.Visible = false + FreeButton.Parent = ContainerFrame + + local OkButton = createImageButtonWithText("OkButton", isTenFootInterface and UDim2.new(0, 2, 0, 136*scaleFactor) or UDim2.new(0, 2, 0, 136), BUTTON, BUTTON_DOWN, "OK", Enum.Font.SourceSans) + OkButton.Size = isTenFootInterface and UDim2.new(0, 320*scaleFactor, 0, 44*scaleFactor) or UDim2.new(0, 320, 0, 44) + OkButton.Visible = false + OkButton.Parent = ContainerFrame + + local okButtonGamepadImage = buyButtonGamepadImage:Clone() + okButtonGamepadImage.ZIndex = OkButton.ZIndex + okButtonGamepadImage.Parent = OkButton + table.insert(GAMEPAD_BUTTONS, okButtonGamepadImage) + + local OkPurchasedButton = createImageButtonWithText("OkPurchasedButton", isTenFootInterface and UDim2.new(0, 2, 0, 136*scaleFactor) or UDim2.new(0, 2, 0, 136), BUTTON, BUTTON_DOWN, "OK", Enum.Font.SourceSans) + OkPurchasedButton.Size = isTenFootInterface and UDim2.new(0, 320*scaleFactor, 0, 44*scaleFactor) or UDim2.new(0, 320, 0, 44) + OkPurchasedButton.Visible = false + OkPurchasedButton.Parent = ContainerFrame + + local okPurchasedGamepadImage = buyButtonGamepadImage:Clone() + okPurchasedGamepadImage.ZIndex = OkPurchasedButton.ZIndex + okPurchasedGamepadImage.Parent = OkPurchasedButton + table.insert(GAMEPAD_BUTTONS, okPurchasedGamepadImage) + + local PurchaseFrame = createImageLabel("PurchaseFrame", UDim2.new(1, 0, 1, 0), UDim2.new(0, 0, 0, 0), PURCHASE_BG) + PurchaseFrame.ZIndex = 8 + PurchaseFrame.Visible = false + PurchaseFrame.Parent = PurchaseDialog + + local PurchaseText = createTextLabel("PurchaseText", nil, UDim2.new(0.5, 0, 0.5, -36), Enum.Font.SourceSans, + isTenFootInterface and largeFont or Enum.FontSize.Size36, "Purchasing") + PurchaseText.Parent = PurchaseFrame + + local LoadingFrames = {} + local xOffset = -40 + for i = 1, 3 do + local frame = createFrame("Loading", UDim2.new(0, 16, 0, 16), UDim2.new(0.5, xOffset, 0.5, 0), 0, Color3.new(132/255, 132/255, 132/255)) + table.insert(LoadingFrames, frame) + frame.Parent = PurchaseFrame + xOffset = xOffset + 32 + end + +local function noOpFunc() end + +local function enableControllerMovement() + game:GetService("ContextActionService"):UnbindCoreAction(freezeThumbstick1Name) + game:GetService("ContextActionService"):UnbindCoreAction(freezeThumbstick2Name) + game:GetService("ContextActionService"):UnbindCoreAction(freezeControllerActionName) +end + +local function disableControllerMovement() + game:GetService("ContextActionService"):BindCoreAction(freezeControllerActionName, noOpFunc, false, Enum.UserInputType.Gamepad1) + game:GetService("ContextActionService"):BindCoreAction(freezeThumbstick1Name, noOpFunc, false, Enum.KeyCode.Thumbstick1) + game:GetService("ContextActionService"):BindCoreAction(freezeThumbstick2Name, noOpFunc, false, Enum.KeyCode.Thumbstick2) +end + +-- isClickerScam() returns true if any of the following conditions are met: +-- 1. The user has pressed ButtonA or clicked on the buy button area `TRACKED_CLICKS` times in the past `REACTION_TIME` seconds +-- 2. Mouse is over the button and UserInputService.MouseBehavior has changed to LockCurrentPosition in the past `REACTION_TIME` seconds +local isClickerScam do + local REACTION_TIME = 1.1 -- Give the user this many seconds to stop clicking + local TRACKED_CLICKS = 3 -- How many clicks recorded in the last `REACTION_TIME` will count as a clicker scam + + local isOverBuyButton do + local btnPos0 = Vector2.new() -- Upper-left corner of BuyButton + local btnPos1 = Vector2.new() -- Lower-right corner of BuyButton + + do -- Update btnPos0 & btnPos1 + local function getOffsetAxes(ud2) + return Vector2.new(ud2.X.Offset, ud2.Y.Offset) + end + + local function getScaleAxes(ud2) + return Vector2.new(ud2.X.Scale, ud2.Y.Scale) + end + + -- simulatePixelBounds calculates screen-space AABB corners from a hierarchy of gui sizes and positions + local function simulatePixelBounds(absoluteSize, guis) + local absolutePosition = Vector2.new() + for _, elem in ipairs(guis) do + absolutePosition = absolutePosition + getOffsetAxes(elem.Position) + getScaleAxes(elem.Position)*absoluteSize + absoluteSize = getOffsetAxes(elem.Size) + getScaleAxes(elem.Size)*absoluteSize + end + return absolutePosition, absolutePosition + absoluteSize + end + + local function updateBuyButtonBounds() + btnPos0, btnPos1 = simulatePixelBounds(RobloxGui.AbsoluteSize, { + { -- PurchaseDialog + Position = SHOW_POSITION, + Size = DIALOG_SIZE, + }, + { -- BuyButton + Position = BTN_L_POS, + Size = BTN_SIZE, + }, + }) + end + + RobloxGui:GetPropertyChangedSignal("AbsoluteSize"):Connect(updateBuyButtonBounds) + updateBuyButtonBounds() + end + + function isOverBuyButton(mousePos) + local mouseX, mouseY = mousePos.X, mousePos.Y + return mouseY >= btnPos0.Y and mouseY < btnPos1.Y and mouseX >= btnPos0.X and mouseX < btnPos1.X + end + end + + do + local lastMouseBehaviorChange = 0 -- Timestamp of when the mouse was last locked into position over the buy button area + local clickStack = {} -- Fixed-size array of timestamps from recent clicks in the BuyButton area, ordered from earliest to latest + for i = 1, TRACKED_CLICKS do + clickStack[i] = 0 + end + + do -- Input capturing + local INPUT_MB1 = Enum.UserInputType.MouseButton1 + local INPUT_MMOVE = Enum.UserInputType.MouseMovement + local INPUT_TOUCH = Enum.UserInputType.Touch + local INPUT_BTN_A = Enum.KeyCode.ButtonA + + UserInputService.InputBegan:Connect(function(input, gpe) + if buttonsActive then return end -- Don't capture input if the purchase prompt is active + local inputType = input.UserInputType + + local isGamepad = FFlagThwartPurchasePromptScamsGamepad and input.KeyCode == INPUT_BTN_A + local isMouseOrTouch = inputType == INPUT_MB1 or inputType == INPUT_TOUCH + + if isGamepad or (isMouseOrTouch and isOverBuyButton(input.Position)) then + -- Push current timestamp to the click stack + for i = 2, TRACKED_CLICKS do + clickStack[i - 1] = clickStack[i] + end + clickStack[TRACKED_CLICKS] = tick() + end + end) + + UserInputService:GetPropertyChangedSignal("MouseBehavior"):Connect(function() + local lastInputType = UserInputService:GetLastInputType() + local isMouseInput = lastInputType == INPUT_MB1 or lastInputType == INPUT_MMOVE + local mousePos = UserInputService:GetMouseLocation() - GuiService:GetGuiInset() + if UserInputService.MouseBehavior == Enum.MouseBehavior.LockCurrentPosition and isMouseInput and isOverBuyButton(mousePos) then + lastMouseBehaviorChange = tick() + end + end) + end + + function isClickerScam() + if not FFlagThwartPurchasePromptScams then + return false + end + -- fastclick: User is told to click the buy button area rapidly. + -- The purchase prompt slides down and they unwittingly confirm the purchase. + local fastclick = tick() < REACTION_TIME + clickStack[1] + -- lockmouse: User is told to click different parts of the screen rapidly. + -- The mouse locks up over the buy button area, the purchase prompt slides down, and they unwittingly confirm the purchase. + local lockmouse = tick() < REACTION_TIME + lastMouseBehaviorChange + return fastclick or lockmouse + end + end +end + +local function getCurrencyString(currencyType) + return currencyType == Enum.CurrencyType.Tix and "Tix" or "R$" +end + +local function setInitialPurchaseData(assetId, productId, gamePassId, currencyType, equipOnPurchase) + PurchaseData.AssetId = assetId + PurchaseData.ProductId = productId + PurchaseData.GamePassId = gamePassId + PurchaseData.CurrencyType = currencyType + PurchaseData.EquipOnPurchase = equipOnPurchase + + IsPurchasingConsumable = productId ~= nil + IsPurchasingGamePass = gamePassId ~= nil +end + +local function setCurrencyData(playerBalance) + PurchaseData.CurrencyType = Enum.CurrencyType.Robux + PurchaseData.CurrencyAmount = tonumber(PurchaseData.ProductInfo['PriceInRobux']) + + if PurchaseData.CurrencyAmount == nil then + PurchaseData.CurrencyAmount = 0 + end +end + +local function setPreviewImageXbox(productInfo, assetId) + -- get the asset id we want + local id = nil + if IsPurchasingConsumable and productInfo and productInfo["IconImageAssetId"] then + id = productInfo["IconImageAssetId"] + elseif assetId then + id = assetId + else + ItemPreviewImage.Image = DEFAULT_XBOX_IMAGE + return + end + + spawn(function() + local imageUrl = nil + local isGenerated = false + local success, msg = pcall(function() + imageUrl, isGenerated = AssetService:GetAssetThumbnailAsync(id, Vector2.new(100, 100)) + end) + + if success and isGenerated == true and imageUrl then + ItemPreviewImage.Image = imageUrl + else + ItemPreviewImage.Image = DEFAULT_XBOX_IMAGE + end + end) +end + +local function setPreviewImage(productInfo, assetId) + -- For now let's only run this logic on Xbox + if platform == Enum.Platform.XBoxOne then + setPreviewImageXbox(productInfo, assetId) + return + end + if IsPurchasingConsumable then + if productInfo then + ItemPreviewImage.Image = THUMBNAIL_URL..tostring(productInfo["IconImageAssetId"].."&x=100&y=100&format=png") + end + else + if assetId then + ItemPreviewImage.Image = THUMBNAIL_URL..tostring(assetId).."&x=100&y=100&format=png" + end + end +end + +local function clearPurchaseData() + for k,v in pairs(PurchaseData) do + PurchaseData[k] = nil + end + RobuxIcon.Visible = false + TixIcon.Visible = false + CostText.Visible = false +end + +local function setButtonsVisible(...) + local args = {...} + local argCount = select('#', ...) + + for _,child in pairs(ContainerFrame:GetChildren()) do + if child:IsA('ImageButton') then + child.Visible = false + for i = 1, argCount do + if child == args[i] then + child.Visible = true + end + end + end + end +end + +local function tweenBackgroundColor(frame, endColor, duration) + local t = 0 + local prevTime = tick() + local startColor = frame.BackgroundColor3 + while t < duration do + local s = t / duration + local r = lerp(startColor.r, endColor.r, s) + local g = lerp(startColor.g, endColor.g, s) + local b = lerp(startColor.b, endColor.b, s) + frame.BackgroundColor3 = Color3.new(r, g, b) + -- + t = t + (tick() - prevTime) + prevTime = tick() + wait() + end + frame.BackgroundColor3 = endColor +end + +local isPurchaseAnimating = false +local function startPurchaseAnimation() + if PurchaseFrame.Visible then return end + -- + ContainerFrame.Visible = false + PurchaseFrame.Visible = true + -- + spawn(function() + isPurchaseAnimating = true + local i = 1 + while isPurchaseAnimating do + local frame = LoadingFrames[i] + local prevPosition = frame.Position + local newPosition = UDim2.new(prevPosition.X.Scale, prevPosition.X.Offset, prevPosition.Y.Scale, prevPosition.Y.Offset - 2) + spawn(function() + tweenBackgroundColor(frame, Color3.new(0, 162/255, 1), 0.25) + end) + frame:TweenSizeAndPosition(UDim2.new(0, 16, 0, 20), newPosition, Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, 0.25, true, function() + spawn(function() + tweenBackgroundColor(frame, Color3.new(132/255, 132/255, 132/255), 0.25) + end) + frame:TweenSizeAndPosition(UDim2.new(0, 16, 0, 16), prevPosition, Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, 0.25, true) + end) + i = i + 1 + if i > 3 then + i = 1 + wait(0.25) -- small pause when starting from 1 + end + wait(0.5) + end + end) +end + +local function stopPurchaseAnimation() + isPurchaseAnimating = false + PurchaseFrame.Visible = false + ContainerFrame.Visible = true +end + +local function setPurchaseDataInGui(isFree, invalidBC) + local descriptionText = PurchaseData.CurrencyType == Enum.CurrencyType.Tix and PURCHASE_MSG.PURCHASE_TIX or PURCHASE_MSG.PURCHASE + if FFlagUsePurchasePromptLocalization then + descriptionText = LocalizedGetString("PurchasePromptScript.PURCHASE_MSG.PURCHASE",descriptionText) + end + + if isFree then + descriptionText = PURCHASE_MSG.FREE + if FFlagUsePurchasePromptLocalization then + descriptionText = LocalizedGetString("PurchasePromptScript.PURCHASE_MSG.FREE",descriptionText) + end + + PostBalanceText.Text = PURCHASE_MSG.FREE_BALANCE + end + + local productInfo = PurchaseData.ProductInfo + if not productInfo then + return false + end + local itemDescription = "" + if FFlagUsePurchasePromptLocalization then + itemDescription = string.gsub(descriptionText, "{RBX_NAME2}", string.sub(productInfo["Name"], 1, 20)) + itemDescription = string.gsub(itemDescription, "{RBX_NAME1}", ASSET_TO_STRING[productInfo["AssetTypeId"]] or "Unknown") + else + itemDescription = string.gsub(descriptionText, "itemName", string.sub(productInfo["Name"], 1, 20)) + itemDescription = string.gsub(itemDescription, "assetType", ASSET_TO_STRING[productInfo["AssetTypeId"]] or "Unknown") + end + ItemDescriptionText.Text = itemDescription + + if not isFree then + if PurchaseData.CurrencyType == Enum.CurrencyType.Tix then + TixIcon.Visible = true + TixIcon.Position = UDim2.new(0, isTenFootInterface and 110*scaleFactor or 110, 0, ItemDescriptionText.Position.Y.Offset + ItemDescriptionText.TextBounds.y + (isTenFootInterface and 6*scaleFactor or 6)) + CostText.TextColor3 = Color3.new(204/255, 158/255, 113/255) + else + RobuxIcon.Visible = true + RobuxIcon.Position = UDim2.new(0, isTenFootInterface and 110*scaleFactor or 110, 0, ItemDescriptionText.Position.Y.Offset + ItemDescriptionText.TextBounds.y + (isTenFootInterface and 6*scaleFactor or 6)) + CostText.TextColor3 = Color3.new(2/255, 183/255, 87/255) + end + CostText.Text = formatNumber(PurchaseData.CurrencyAmount) + CostText.Position = UDim2.new(0, isTenFootInterface and 134*scaleFactor or 134, 0, ItemDescriptionText.Position.Y.Offset + ItemDescriptionText.TextBounds.y + (isTenFootInterface and 15*scaleFactor or 15)) + CostText.Visible = true + end + + setPreviewImage(productInfo, PurchaseData.AssetId) + purchaseState = PURCHASE_STATE.BUYITEM + setButtonsVisible(isFree and FreeButton or BuyButton, CancelButton) + PostBalanceText.Visible = true + + if invalidBC then + local neededBcLevel = PurchaseData.ProductInfo["MinimumMembershipLevel"] + PostBalanceText.Text = "This item requires "..BC_LVL_TO_STRING[neededBcLevel]..".\nClick 'Upgrade' to upgrade your Builders Club!" + if FFlagUsePurchasePromptLocalization then + PostBalanceText.Text = LocalizedGetString("PurchasePromptScript.setPurchaseDataInGui.invalidBC",PostBalanceText.Text) + PostBalanceText.Text = string.gsub(PostBalanceText.Text, "{RBX_NAME1}", BC_LVL_TO_STRING[neededBcLevel]) + end + purchaseState = PURCHASE_STATE.BUYBC + setButtonsVisible(BuyBCButton, CancelButton) + end + return true +end + +local function getRobuxProduct(amountNeeded, isBCMember) + local productArray = nil + + if platform == Enum.Platform.XBoxOne then + productArray = {} + local platformCatalogData = require(RobloxGui.Modules.Shell.PlatformCatalogData) + + local catalogInfo = platformCatalogData:GetCatalogInfoAsync() + if catalogInfo then + for _, productInfo in pairs(catalogInfo) do + local robuxValue = platformCatalogData:ParseRobuxValue(productInfo) + table.insert(productArray, robuxValue) + end + end + else + productArray = isBCMember and BC_ROBUX_PRODUCTS or NON_BC_ROBUX_PRODUCTS + end + + table.sort(productArray, function(a,b) return a < b end) + + for i = 1, #productArray do + if productArray[i] >= amountNeeded then + return productArray[i] + end + end + + return nil +end + +local function getRobuxProductToBuyItem(amountNeeded) + local isBCMember = Players.LocalPlayer.MembershipType ~= Enum.MembershipType.None + + local productCost = getRobuxProduct(amountNeeded, isBCMember) + if not productCost then + return nil + end + + --todo: we should clean all this up at some point so all the platforms have the + -- same product names, or at least names that are very similar + + local isUsingNewProductId = (platform == Enum.Platform.Android) or (platform == Enum.Platform.UWP) + + local prependStr, appendStr, appPrefix = "", "", "" + if isUsingNewProductId then + prependStr = "robux" + if isBCMember then + appendStr = "bc" + end + appPrefix = "com.roblox.client." + elseif platform == Enum.Platform.XBoxOne then + local platformCatalogData = require(RobloxGui.Modules.Shell.PlatformCatalogData) + + local catalogInfo = platformCatalogData:GetCatalogInfoAsync() + if catalogInfo then + for _, productInfo in pairs(catalogInfo) do + if platformCatalogData:ParseRobuxValue(productInfo) == productCost then + return productInfo.ProductId, productCost + end + end + end + elseif platform == Enum.Platform.IOS then + appendStr = isBCMember and "RobuxBC" or "RobuxNonBC" + appPrefix = "com.roblox.robloxmobile." + else + appendStr = isBCMember and "RobuxBCInvalid" or "RobuxNonBCInvalid" + appPrefix = "com.roblox.INVALIDPLATFORM." + end + + local productStr = appPrefix..prependStr..tostring(productCost)..appendStr + return productStr, productCost +end + +local function setBuyMoreRobuxDialog(playerBalance) + local playerBalanceInt = tonumber(playerBalance["robux"]) + local neededRobux = PurchaseData.CurrencyAmount - playerBalanceInt + local productInfo = PurchaseData.ProductInfo + + local descriptionText = "You need %s more ROBUX to buy the %s %s" + descriptionText = string.format(descriptionText, formatNumber(neededRobux), productInfo["Name"], ASSET_TO_STRING[productInfo["AssetTypeId"]] or "") + + purchaseState = PURCHASE_STATE.BUYROBUX + setButtonsVisible(BuyRobuxButton, CancelButton) + + if IsNativePurchasing then + local productCost = nil + ThirdPartyProductName, productCost = getRobuxProductToBuyItem(neededRobux) + -- + if not ThirdPartyProductName then + if isTenFootInterface then + -- don't direct them to roblox.com on consoles. + descriptionText = "This item cost more ROBUX than you have available. Please leave this game and go to the ROBUX screen to purchase more." + else + descriptionText = "This item cost more ROBUX than you can purchase. Please visit www.roblox.com to purchase more ROBUX." + end + purchaseState = PURCHASE_STATE.FAILED + setButtonsVisible(OkButton) + else + local remainder = playerBalanceInt + productCost - PurchaseData.CurrencyAmount + descriptionText = descriptionText..". Would you like to buy "..formatNumber(productCost).." ROBUX?" + if FFlagUsePurchasePromptLocalization then + descriptionText = LocalizedGetString("PurchasePromptScript.setBuyMoreRobuxDialog.descriptionText",descriptionText) + descriptionText = string.gsub(descriptionText, "{RBX_NUMBER}", formatNumber(neededRobux)) + descriptionText = string.gsub(descriptionText, "{RBX_NAME1}", productInfo["Name"]) + descriptionText = string.gsub(descriptionText, "{RBX_NAME2}", ASSET_TO_STRING[productInfo["AssetTypeId"]]) + end + + PostBalanceText.Text = "The remaining "..formatNumber(remainder).." ROBUX will be credited to your balance." + if FFlagUsePurchasePromptLocalization then + PostBalanceText.Text = LocalizedGetString("PurchasePromptScript.setBuyMoreRobuxDialog.PostBalanceText",PostBalanceText.Text) + PostBalanceText.Text = string.gsub(PostBalanceText.Text,"{RBX_NUMBER}",formatNumber(remainder)) + end + PostBalanceText.Visible = true + end + else + descriptionText = descriptionText..". Would you like to buy more ROBUX?" + if FFlagUsePurchasePromptLocalization then + descriptionText = LocalizedGetString("PurchasePromptScript.setBuyMoreRobuxDialog.descriptionText", descriptionText) + descriptionText = string.gsub(descriptionText, "{RBX_NUMBER}", formatNumber(neededRobux)) + descriptionText = string.gsub(descriptionText, "{RBX_NAME1}", productInfo["Name"]) + descriptionText = string.gsub(descriptionText, "{RBX_NAME2}", ASSET_TO_STRING[productInfo["AssetTypeId"]]) + end + end + ItemDescriptionText.Text = descriptionText + setPreviewImage(productInfo, PurchaseData.AssetId) +end + +local function showPurchasePrompt() + stopPurchaseAnimation() + PurchaseDialog.Visible = true + if isTenFootInterface then + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide + end + PurchaseDialog:TweenPosition(isTenFootInterface and SHOW_POSITION_TENFOOT or SHOW_POSITION, Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, TWEEN_TIME, true, function(tweenStatus) + if tweenStatus == Enum.TweenStatus.Completed then + buttonsActive = true + end + end) + disableControllerMovement() + enableControllerInput() +end + +local function onPurchaseFailed(failType) + setButtonsVisible(OkButton) + ItemPreviewImage.Image = ERROR_ICON + PostBalanceText.Text = "" + + local itemName = PurchaseData.ProductInfo and PurchaseData.ProductInfo["Name"] or "" + local failedText = "" + + if FFlagUsePurchasePromptLocalization then + failedText = string.gsub(LocalizedGetString("PurchasePromptScript.PURCHASE_MSG.FAILED",PURCHASE_MSG.FAILED), "{RBX_NAME1}", string.sub(itemName, 1, 20)) + if itemName == "" then + failedText = string.gsub(failedText, " of ", "") + end + + if failType == PURCHASE_FAILED.DEFAULT_ERROR then + failedText = string.gsub(failedText, "{RBX_NAME2}", LocalizedGetString("PurchasePromptScript.ERROR_MSG.UNKNOWN",ERROR_MSG.UNKNWON_FAILURE)) + elseif failType == PURCHASE_FAILED.IN_GAME_PURCHASE_DISABLED then + failedText = string.gsub(failedText, "{RBX_NAME2}", LocalizedGetString("PurchasePromptScript.ERROR_MSG.PURCHASE_DISABLED",ERROR_MSG.PURCHASE_DISABLED)) + elseif failType == PURCHASE_FAILED.CANNOT_GET_BALANCE then + failedText = LocalizedGetString( + "PurchasePromptScript.PURCHASE_FAILED.CANNOT_GET_BALANCE", + "Cannot retrieve your balance at this time. Your account has not been charged. Please try again later.") + elseif failType == PURCHASE_FAILED.CANNOT_GET_ITEM_PRICE then + failedText = LocalizedGetString( + "PurchasePromptScript.PURCHASE_FAILED.CANNOT_GET_ITEM_PRICE", + "We couldn't retrieve the price of the item at this time. Your account has not been charged. Please try again later.") + elseif failType == PURCHASE_FAILED.NOT_FOR_SALE then + failedText = LocalizedGetString( + "PurchasePromptScript.PURCHASE_FAILED.NOT_FOR_SALE", + "This item is not currently for sale. Your account has not been charged.") + setPreviewImage(PurchaseData.ProductInfo, PurchaseData.AssetId) + elseif failType == PURCHASE_FAILED.NOT_ENOUGH_TIX then + failedText = LocalizedGetString( + "PurchasePromptScript.PURCHASE_FAILED.NOT_ENOUGH_TIX", + "This item cost more tickets than you currently have. Try trading currency on www.roblox.com to get more tickets.") + setPreviewImage(PurchaseData.ProductInfo, PurchaseData.AssetId) + elseif failType == PURCHASE_FAILED.UNDER_13 then + failedText = LocalizedGetString( + "PurchasePromptScript.PURCHASE_FAILED.UNDER_13", + "Your account is under 13. Purchase of this item is not allowed. Your account has not been charged.") + elseif failType == PURCHASE_FAILED.LIMITED then + failedText = LocalizedGetString( + "PurchasePromptScript.PURCHASE_FAILED.LIMITED", + "This limited item has no more copies. Try buying from another user on www.roblox.com. Your account has not been charged.") + setPreviewImage(PurchaseData.ProductInfo, PurchaseData.AssetId) + elseif failType == PURCHASE_FAILED.DID_NOT_BUY_ROBUX then + failedText = string.gsub(failedText, "{RBX_NAME2}", LocalizedGetString("PurchasePromptScript.ERROR_MSG.INVALID_FUNDS",ERROR_MSG.INVALID_FUNDS)) + elseif failType == PURCHASE_FAILED.PROMPT_PURCHASE_ON_GUEST then + failedText = LocalizedGetString( + "PurchasePromptScript.PURCHASE_FAILED.PROMPT_PURCHASE_ON_GUEST", + "You need to create a ROBLOX account to buy items, visit www.roblox.com for more info.") + elseif failType == PURCHASE_FAILED.THIRD_PARTY_DISABLED then + failedText = LocalizedGetString( + "PurchasePromptScript.PURCHASE_FAILED.THIRD_PARTY_DISABLED", + "Third-party item sales have been disabled for this place. Your account has not been charged.") + setPreviewImage(PurchaseData.ProductInfo, PurchaseData.AssetId) + end + + else --FFlagUsePurchasePromptLocalization == false + + failedText = string.gsub(PURCHASE_MSG.FAILED, "itemName", string.sub(itemName, 1, 20)) + if itemName == "" then + failedText = string.gsub(failedText, " of ", "") + end + + if failType == PURCHASE_FAILED.DEFAULT_ERROR then + failedText = string.gsub(failedText, "errorReason", ERROR_MSG.UNKNWON_FAILURE) + elseif failType == PURCHASE_FAILED.IN_GAME_PURCHASE_DISABLED then + failedText = string.gsub(failedText, "errorReason", ERROR_MSG.PURCHASE_DISABLED) + elseif failType == PURCHASE_FAILED.CANNOT_GET_BALANCE then + failedText = "Cannot retrieve your balance at this time. Your account has not been charged. Please try again later." + elseif failType == PURCHASE_FAILED.CANNOT_GET_ITEM_PRICE then + failedText = "We couldn't retrieve the price of the item at this time. Your account has not been charged. Please try again later." + elseif failType == PURCHASE_FAILED.NOT_FOR_SALE then + failedText = "This item is not currently for sale. Your account has not been charged." + setPreviewImage(PurchaseData.ProductInfo, PurchaseData.AssetId) + elseif failType == PURCHASE_FAILED.NOT_ENOUGH_TIX then + failedText = "This item cost more tickets than you currently have. Try trading currency on www.roblox.com to get more tickets." + setPreviewImage(PurchaseData.ProductInfo, PurchaseData.AssetId) + elseif failType == PURCHASE_FAILED.UNDER_13 then + failedText = "Your account is under 13. Purchase of this item is not allowed. Your account has not been charged." + elseif failType == PURCHASE_FAILED.LIMITED then + failedText = "This limited item has no more copies. Try buying from another user on www.roblox.com. Your account has not been charged." + setPreviewImage(PurchaseData.ProductInfo, PurchaseData.AssetId) + elseif failType == PURCHASE_FAILED.DID_NOT_BUY_ROBUX then + failedText = string.gsub(failedText, "errorReason", ERROR_MSG.INVALID_FUNDS) + elseif failType == PURCHASE_FAILED.PROMPT_PURCHASE_ON_GUEST then + failedText = "You need to create a ROBLOX account to buy items, visit www.roblox.com for more info." + elseif failType == PURCHASE_FAILED.THIRD_PARTY_DISABLED then + failedText = "Third-party item sales have been disabled for this place. Your account has not been charged." + setPreviewImage(PurchaseData.ProductInfo, PurchaseData.AssetId) + end + end + + RobuxIcon.Visible = false + TixIcon.Visible = false + CostText.Visible = false + + purchaseState = PURCHASE_STATE.FAILED + + ItemDescriptionText.Text = failedText + showPurchasePrompt() +end + +local function closePurchaseDialog() + buttonsActive = false + purchaseState = PURCHASE_STATE.DEFAULT + if isTenFootInterface then + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None + end + PurchaseDialog:TweenPosition(isTenFootInterface and HIDE_POSITION_TENFOOT or HIDE_POSITION, Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, TWEEN_TIME, true, function(tweenStatus) + if tweenStatus == Enum.TweenStatus.Completed then + PurchaseDialog.Visible = false + IsCurrentlyPrompting = false + IsCurrentlyPurchasing = false + IsCheckingPlayerFunds = false + end + end) +end + +-- Main exit point +local function onPromptEnded(isSuccess) + local didPurchase = (purchaseState == PURCHASE_STATE.SUCCEEDED) + + closePurchaseDialog() + if IsPurchasingConsumable then + MarketplaceService:SignalPromptProductPurchaseFinished(Players.LocalPlayer.UserId, PurchaseData.ProductId, didPurchase) + elseif IsPurchasingGamePass then + MarketplaceService:SignalPromptGamePassPurchaseFinished(Players.LocalPlayer, PurchaseData.GamePassId, didPurchase) + else + MarketplaceService:SignalPromptPurchaseFinished(Players.LocalPlayer, PurchaseData.AssetId, didPurchase) + end + clearPurchaseData() + enableControllerMovement() + disableControllerInput() +end + +local function isMarketplaceDown() -- FFlag + local success, result = pcall(function() return settings():GetFFlag('Order66') end) + if not success then + print("PurchasePromptScript: isMarketplaceDown failed because", result) + return false + end + + return result +end + +local function checkMarketplaceAvailable() -- FFlag + local success, result = pcall(function() return settings():GetFFlag("CheckMarketplaceAvailable") end) + if not success then + print("PurchasePromptScript: checkMarketplaceAvailable failed because", result) + return false + end + + return result +end + +local function areThirdPartySalesRestricted() -- FFlag + return settings():GetFFlag("RestrictSales2") +end + + +-- return success and isAvailable +local function isMarketplaceAvailable() + local success, result = pcall(function() + return HttpRbxApiService:GetAsync("my/economy-status", + Enum.ThrottlingPriority.Extreme, + Enum.HttpRequestType.MarketplaceService) + end) + if not success then + print("PurchasePromptScript: isMarketplaceAvailable() failed because", result) + return false + end + result = HttpService:JSONDecode(result) + if result["isMarketplaceEnabled"] ~= nil then + if result["isMarketplaceEnabled"] == false then + return true, false + end + end + return true, true +end + +local function getProductInfo() + local success, result = nil, nil + if IsPurchasingConsumable then + success, result = pcall(function() + return MarketplaceService:GetProductInfo(PurchaseData.ProductId, Enum.InfoType.Product) + end) + elseif IsPurchasingGamePass then + success, result = pcall(function() + return MarketplaceService:GetProductInfo(PurchaseData.GamePassId, Enum.InfoType.GamePass) + end) + else + success, result = pcall(function() + return MarketplaceService:GetProductInfo(PurchaseData.AssetId) + end) + end + + if not success or not result then + warn("PurchasePromptScript: getProductInfo failed because", result, "Make sure a valid ID was specified") + return nil + end + + if type(result) ~= 'table' then + result = HttpService:JSONDecode(result) + end + + return result +end + +local function doesPlayerOwnGamePass() + if (not PurchaseData.GamePassId) or (PurchaseData.GamePassId <= 0) then + return false, nil + end + + local success, result = pcall(function() + return MarketplaceService:UserOwnsGamePassAsync(Players.LocalPlayer.UserId, PurchaseData.GamePassId) + end) + + if not success then + print("PurchasePromptScript: doesPlayerOwnGamePass() failed because", result) + return false, nil + end + + return true, (result == true) or (result == "true") +end + +-- returns success, doesOwnItem +local function doesPlayerOwnItem() + if not PurchaseData.AssetId or PurchaseData.AssetId <= 0 then + if PurchaseData.GamePassId then + return doesPlayerOwnGamePass() + else + return false, nil + end + end + + local success, result = pcall(function() + return MarketplaceService:PlayerOwnsAsset(Players.LocalPlayer, PurchaseData.AssetId) + end) + + if not success then + print("PurchasePromptScript: doesPlayerOwnItem() failed because", result) + return false, nil + end + + return true, result == true or result == 'true' +end + +local function isFreeItem() + return PurchaseData.ProductInfo and PurchaseData.ProductInfo["IsPublicDomain"] == true +end + +local getPlayerBalance + +getPlayerBalance = function() + local success, result = pcall(function() + return MarketplaceService:GetRobuxBalance() + end) + + if not success then + print("PurchasePromptScript: GetRobuxBalance() failed because", result) + return nil + end + + local balance = {} + balance.robux = result + balance.tickets = 0 + return balance +end + +local function isNotForSale() + return PurchaseData.ProductInfo['IsForSale'] == false and PurchaseData.ProductInfo["IsPublicDomain"] == false +end + +local function playerHasFundsForPurchase(playerBalance) + local currencyTypeStr = nil + if PurchaseData.CurrencyType == Enum.CurrencyType.Robux then + currencyTypeStr = "robux" + elseif PurchaseData.CurrencyType == Enum.CurrencyType.Tix then + currencyTypeStr = "tickets" + else + return false + end + + local playerBalanceInt = tonumber(playerBalance[currencyTypeStr]) + if not playerBalanceInt then + return false + end + + local afterBalanceAmount = playerBalanceInt - PurchaseData.CurrencyAmount + local currencyStr = getCurrencyString(PurchaseData.CurrencyType) + if afterBalanceAmount < 0 and PurchaseData.CurrencyType == Enum.CurrencyType.Robux then + PostBalanceText.Visible = false + return true, false + elseif afterBalanceAmount < 0 and PurchaseData.CurrencyType == Enum.CurrencyType.Tix then + PostBalanceText.Visible = true + PostBalanceText.Text = "You need "..formatNumber(-afterBalanceAmount).." more "..currencyStr.." to buy this item." + return true, false + end + + if PurchaseData.CurrencyType == Enum.CurrencyType.Tix then + PostBalanceText.Text = PURCHASE_MSG.BALANCE_FUTURE..formatNumber(afterBalanceAmount).." "..currencyStr.."." + else + PostBalanceText.Text = PURCHASE_MSG.BALANCE_FUTURE..currencyStr..formatNumber(afterBalanceAmount).."." + end + + if FFlagUsePurchasePromptLocalization then + PostBalanceText.Text = LocalizedGetString("PurchasePromptScript.PURCHASE_MSG.BALANCE_FUTURE",PostBalanceText.Text) + PostBalanceText.Text = string.gsub(PostBalanceText.Text, "{RBX_NUMBER}", currencyStr..formatNumber(afterBalanceAmount)) + end + + if studioMockPurchasesEnabled() then + PostBalanceText.Text = PURCHASE_MSG.MOCK_PURCHASE + end + + return true, true +end + +local function isUnder13() + if PurchaseData.ProductInfo["ContentRatingTypeId"] == 1 then + if Players.LocalPlayer:GetUnder13() then + return true + end + end + return false +end + +local function isLimitedUnique() + local productInfo = PurchaseData.ProductInfo + if productInfo then + if (productInfo["IsLimited"] or productInfo["IsLimitedUnique"]) and + (productInfo["Remaining"] == "" or productInfo["Remaining"] == 0 or productInfo["Remaining"] == nil or productInfo["Remaining"] == "null") then + return true + end + end + return false +end + +-- main validation function +local function canPurchase(disableUpsell) + if not MarketplaceService:PlayerCanMakePurchases(Players.LocalPlayer) then + onPurchaseFailed(PURCHASE_FAILED.PROMPT_PURCHASE_ON_GUEST) + return false + end + + if isMarketplaceDown() then -- FFlag + onPurchaseFailed(PURCHASE_FAILED.IN_GAME_PURCHASE_DISABLED) + return false + end + + if checkMarketplaceAvailable() then -- FFlag + local success, isAvailable = isMarketplaceAvailable() + if success then + if not isAvailable then + onPurchaseFailed(PURCHASE_FAILED.IN_GAME_PURCHASE_DISABLED) + return false + end + else + onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR) + return false + end + end + + PurchaseData.ProductInfo = getProductInfo() + if not PurchaseData.ProductInfo then + onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR) + return false + end + + if isNotForSale() then + onPurchaseFailed(PURCHASE_FAILED.NOT_FOR_SALE) + return false + end + + -- check if owned by player; dev products are not owned + local isRestrictedThirdParty = false + local thirdPartyRestrictions = areThirdPartySalesRestricted() + if not IsPurchasingConsumable then + local success, doesOwnItem = doesPlayerOwnItem() + if not success then + onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR) + return false + elseif doesOwnItem then + if not PurchaseData.ProductInfo then + onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR) + return false + end + purchaseState = PURCHASE_STATE.FAILED + setPreviewImage(PurchaseData.ProductInfo, PurchaseData.AssetId) + ItemDescriptionText.Text = PURCHASE_MSG.ALREADY_OWN + PostBalanceText.Visible = false + setButtonsVisible(OkButton) + return true + end + + -- most places will not need to sell third party assets. + if thirdPartyRestrictions and not game:GetService("Workspace").AllowThirdPartySales then + local isGroupGame = (game.CreatorType == Enum.CreatorType.Group) + local isGroupAsset = (PurchaseData.ProductInfo["Creator"]["CreatorType"] == "Group") + local RobloxCreator = 1 + local ProductCreator = tonumber(PurchaseData.ProductInfo["Creator"]["CreatorTargetId"]) + if (ProductCreator == RobloxCreator) then + isRestrictedThirdParty = false + elseif (isGroupGame == isGroupAsset) then + if (ProductCreator ~= game.CreatorId) then + isRestrictedThirdParty = true + warn(("AllowThirdPartySales has blocked the purchase prompt for " + .. PurchaseData.ProductInfo["AssetId"] .. " created by " .. ProductCreator + .. ". To sell this asset made by a different ") .. (isGroupGame and "group" or "user") + .. ", you will need to enable AllowThirdPartySales.") + end + else + isRestrictedThirdParty = true + warn(("AllowThirdPartySales has blocked the purchase prompt for " + .. PurchaseData.ProductInfo["AssetId"] .. " created by " .. ProductCreator + .. ". To sell this asset made by a different ") .. (isGroupGame and "group" or "user") + .. ", you will need to enable AllowThirdPartySales.") + end + end + end + + local isFree = isFreeItem() + + if isRestrictedThirdParty then + onPurchaseFailed(PURCHASE_FAILED.THIRD_PARTY_DISABLED) + return false + end + + local playerBalance = getPlayerBalance() + if not playerBalance then + onPurchaseFailed(PURCHASE_FAILED.CANNOT_GET_BALANCE) + return false + end + + -- validate item price + setCurrencyData(playerBalance) + if not PurchaseData.CurrencyAmount and not isFree then + onPurchaseFailed(PURCHASE_FAILED.CANNOT_GET_ITEM_PRICE) + return false + end + + -- check player funds + local hasFunds = nil + if not isFree then + local success = nil + success, hasFunds = playerHasFundsForPurchase(playerBalance) + if success then + if not hasFunds then + if PurchaseData.CurrencyType == Enum.CurrencyType.Tix then + onPurchaseFailed(PURCHASE_FAILED.NOT_ENOUGH_TIX) + return false + elseif not disableUpsell then + setBuyMoreRobuxDialog(playerBalance) + end + end + else + onPurchaseFailed(PURCHASE_FAILED.CANNOT_GET_BALANCE) + return false + end + end + + -- check membership type + local invalidBCLevel = PurchaseData.ProductInfo["MinimumMembershipLevel"] > Players.LocalPlayer.MembershipType.Value + + -- check under 13 + if isUnder13() then + onPurchaseFailed(PURCHASE_FAILED.UNDER_13) + return false + end + + if isLimitedUnique() then + onPurchaseFailed(PURCHASE_FAILED.LIMITED) + return false + end + + if (hasFunds or isFree or invalidBCLevel) then + if not setPurchaseDataInGui(isFree, invalidBCLevel) then + onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR) + return false + end + end + + return true +end + +local function getToolAsset(assetId) + local tool = InsertService:LoadAsset(assetId) + if not tool then return nil end + -- + if tool:IsA("Tool") then + return tool + end + + local children = tool:GetChildren() + for i = 1, #children do + if children[i]:IsA("Tool") then + return children[i] + end + end +end + +local function onPurchaseSuccess() + IsCheckingPlayerFunds = false + local descriptionText = PURCHASE_MSG.SUCCEEDED + if FFlagUsePurchasePromptLocalization then + descriptionText = LocalizedGetString("PurchasePromptScript.PURCHASE_MSG.SUCCEEDED", descriptionText) + descriptionText = string.gsub(descriptionText, "{RBX_NAME1}", string.sub(PurchaseData.ProductInfo["Name"], 1, 20)) + else + descriptionText = string.gsub(descriptionText, "itemName", string.sub(PurchaseData.ProductInfo["Name"], 1, 20)) + end + + ItemDescriptionText.Text = descriptionText + + local playerBalance = getPlayerBalance() + local currencyType = PurchaseData.CurrencyType == Enum.CurrencyType.Tix and "tickets" or "robux" + local newBalance = playerBalance[currencyType] + + if currencyType == "robux" then + PostBalanceText.Text = PURCHASE_MSG.BALANCE_NOW..getCurrencyString(PurchaseData.CurrencyType)..formatNumber(newBalance).."." + else + PostBalanceText.Text = PURCHASE_MSG.BALANCE_NOW..formatNumber(newBalance).." "..getCurrencyString(PurchaseData.CurrencyType).."." + end + if FFlagUsePurchasePromptLocalization then + PostBalanceText.Text = LocalizedGetString("PurchasePromptScript.PURCHASE_MSG.BALANCE_NOW",PostBalanceText.Text) + PostBalanceText.Text = string.gsub(PostBalanceText.Text, "{RBX_NUMBER}", getCurrencyString(PurchaseData.CurrencyType) .. formatNumber(newBalance)) + end + + if studioMockPurchasesEnabled() then + PostBalanceText.Text = PURCHASE_MSG.MOCK_PURCHASE_SUCCESS + elseif isFreeItem() then + PostBalanceText.Visible = false + end + + purchaseState = PURCHASE_STATE.SUCCEEDED + + setButtonsVisible(OkPurchasedButton) + stopPurchaseAnimation() +end + +local function onAcceptPurchase() + if not buttonsActive and FFlagDelayPurchasePromptActivation then return end + if IsCurrentlyPurchasing then return end + if isClickerScam() then return end + + if purchaseState ~= PURCHASE_STATE.BUYITEM then + return + end + + -- + disableControllerInput() + IsCurrentlyPurchasing = true + startPurchaseAnimation() + local startTime = tick() + local apiPath = nil + local params = nil + local currencyTypeInt = nil + if PurchaseData.CurrencyType == Enum.CurrencyType.Robux or PurchaseData.CurrencyType == Enum.CurrencyType.Default then + currencyTypeInt = 1 + elseif PurchaseData.CurrencyType == Enum.CurrencyType.Tix then + currencyTypeInt = 2 + end + + local productId = PurchaseData.ProductInfo["ProductId"] + if IsPurchasingConsumable then + apiPath = "marketplace/submitpurchase" + params = "productId="..tostring(productId).."¤cyTypeId="..tostring(currencyTypeInt).. + "&expectedUnitPrice="..tostring(PurchaseData.CurrencyAmount).."&placeId="..tostring(game.PlaceId) + params = params.."&requestId="..HttpService:UrlEncode(HttpService:GenerateGUID(false)) + else + apiPath = "marketplace/purchase" + params = "productId="..tostring(productId).."¤cyTypeId="..tostring(currencyTypeInt).. + "&purchasePrice="..tostring(PurchaseData.CurrencyAmount or 0).."&locationType=Game&locationId="..tostring(game.PlaceId) + end + + local submitPurchase + + local requestId = HttpService:GenerateGUID(false) + submitPurchase = function() + return game:GetService("MarketplaceService"):PerformPurchase(IsPurchasingConsumable and Enum.InfoType.Product or Enum.InfoType.Asset, productId, PurchaseData.CurrencyAmount or 0, requestId) + end + + local success, result = pcall(submitPurchase) + -- retry + if IsPurchasingConsumable then + local retries = 3 + local wasSuccess = success and result and result ~= '' + while retries > 0 and not wasSuccess do + wait(1) + retries = retries - 1 + success, result = pcall(submitPurchase) + wasSuccess = success and result and result ~= '' + end + -- + game:ReportInGoogleAnalytics("Developer Product", "Purchase", + wasSuccess and ("success. Retries = "..(3 - retries)) or ("failure: " .. tostring(result)), 1) + end + + if tick() - startTime < 1 then wait(1) end -- artifical delay to show spinner for at least 1 second + + enableControllerInput() + + if not success then + print("PurchasePromptScript: onAcceptPurchase() failed because", result) + onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR) + return + end + + if type(result) == "string" then + result = HttpService:JSONDecode(result) + end + if result then + if result["success"] == false then + if result["status"] ~= "AlreadyOwned" then + print("PurchasePromptScript: onAcceptPurchase() response failed because", tostring(result["status"])) + if result["status"] == "EconomyDisabled" then + onPurchaseFailed(PURCHASE_FAILED.IN_GAME_PURCHASE_DISABLED) + else + onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR) + end + return + end + end + else + print("PurchasePromptScript: onAcceptPurchase() failed to parse JSON of", productId) + onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR) + return + end + + if PurchaseData.EquipOnPurchase and PurchaseData.AssetId and tonumber(PurchaseData.ProductInfo["AssetTypeId"]) == 19 then + local tool = getToolAsset(tonumber(PurchaseData.AssetId)) + if tool then + tool.Parent = Players.LocalPlayer.Backpack + end + end + + if IsPurchasingConsumable then + if not result["receipt"] then + print("PurchasePromptScript: onAcceptPurchase() failed because no dev product receipt was returned for", tostring(productId)) + onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR) + return + end + MarketplaceService:SignalClientPurchaseSuccess(tostring(result["receipt"]), Players.LocalPlayer.UserId, productId) + elseif IsPurchasingGamePass then + onPurchaseSuccess() + MarketplaceService:ReportAssetSale(PurchaseData.GamePassId, PurchaseData.CurrencyAmount) + else + onPurchaseSuccess() + if PurchaseData.CurrencyType == Enum.CurrencyType.Robux then + MarketplaceService:ReportAssetSale(PurchaseData.AssetId, PurchaseData.CurrencyAmount) + end + end +end + +-- main entry point +local function onPurchasePrompt(player, assetId, equipIfPurchased, currencyType, productId, gamePassId) + if player == Players.LocalPlayer and not IsCurrentlyPrompting then + IsCurrentlyPrompting = true + setInitialPurchaseData(assetId, productId, gamePassId, currencyType, equipIfPurchased) + if canPurchase() then + showPurchasePrompt() + end + end +end + +function hasEnoughMoneyForPurchase() + local playerBalance = getPlayerBalance() + if playerBalance then + local success, hasFunds = nil + success, hasFunds = playerHasFundsForPurchase(playerBalance) + return success and hasFunds + end + + return false +end + +function retryPurchase(overrideRetries) + local canMakePurchase = canPurchase(true) and hasEnoughMoneyForPurchase() + if not canMakePurchase then + local retries = 40 + if overrideRetries then + retries = overrideRetries + end + while retries > 0 and not canMakePurchase do + wait(0.5) + canMakePurchase = canPurchase(true) and hasEnoughMoneyForPurchase() + retries = retries - 1 + end + end + + return canMakePurchase +end + +function nativePurchaseFinished(wasPurchased) + if wasPurchased then + local isPurchasing = retryPurchase() + if isPurchasing then + onAcceptPurchase() + else + onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR) + end + else + onPurchaseFailed(PURCHASE_FAILED.DID_NOT_BUY_ROBUX) + stopPurchaseAnimation() + end +end + +local function onBuyRobuxPrompt() + if not buttonsActive and FFlagDelayPurchasePromptActivation then return end + if purchaseState ~= PURCHASE_STATE.BUYROBUX then + return + end + if RunService:IsStudio() then + return + end + + purchaseState = PURCHASE_STATE.BUYINGROBUX + + startPurchaseAnimation() + if IsNativePurchasing then + if platform == Enum.Platform.XBoxOne then + spawn(function() + local PlatformService = nil + pcall(function() PlatformService = Game:GetService('PlatformService') end) + if PlatformService then + local platformPurchaseReturnInt = -1 + local purchaseCallSuccess, purchaseErrorMsg = pcall(function() + platformPurchaseReturnInt = PlatformService:BeginPlatformStorePurchase(ThirdPartyProductName) + end) + if purchaseCallSuccess then + nativePurchaseFinished(platformPurchaseReturnInt == 0) + else + nativePurchaseFinished(purchaseCallSuccess) + end + end + end) + else + MarketplaceService:PromptNativePurchase(Players.LocalPlayer, ThirdPartyProductName) + end + else + IsCheckingPlayerFunds = true + GuiService:OpenBrowserWindow(BASE_URL.."Upgrades/Robux.aspx") + end + MarketplaceService:ReportRobuxUpsellStarted() +end + +local function onUpgradeBCPrompt() + if not buttonsActive and FFlagDelayPurchasePromptActivation then return end + if purchaseState ~= PURCHASE_STATE.BUYBC then + return + end + + IsCheckingPlayerFunds = true + GuiService:OpenBrowserWindow(BASE_URL.."Upgrades/BuildersClubMemberships.aspx") +end + +function enableControllerInput() + local cas = game:GetService("ContextActionService") + + --accept the purchase when the user presses the a button + cas:BindCoreAction( + CONTROLLER_CONFIRM_ACTION_NAME, + function(actionName, inputState, inputObject) + if inputState ~= Enum.UserInputState.Begin then return end + + if purchaseState == PURCHASE_STATE.SUCCEEDED then + onPromptEnded() + elseif purchaseState == PURCHASE_STATE.FAILED then + onPromptEnded() + elseif purchaseState == PURCHASE_STATE.BUYITEM then + onAcceptPurchase() + elseif purchaseState == PURCHASE_STATE.BUYROBUX then + onBuyRobuxPrompt() + elseif purchaseState == PURCHASE_STATE.BUYBC then + onUpgradeBCPrompt() + end + end, + false, + Enum.KeyCode.ButtonA + ) + + --cancel the purchase when the user presses the b button + cas:BindCoreAction( + CONTROLLER_CANCEL_ACTION_NAME, + function(actionName, inputState, inputObject) + if inputState ~= Enum.UserInputState.Begin then return end + + if (OkPurchasedButton.Visible or OkButton.Visible or CancelButton.Visible) and (not PurchaseFrame.Visible) then + onPromptEnded(false) + end + end, + false, + Enum.KeyCode.ButtonB + ) +end + +function disableControllerInput() + local cas = game:GetService("ContextActionService") + cas:UnbindCoreAction(CONTROLLER_CONFIRM_ACTION_NAME) + cas:UnbindCoreAction(CONTROLLER_CANCEL_ACTION_NAME) +end + +function showGamepadButtons() + for _, button in pairs(GAMEPAD_BUTTONS) do + button.Visible = true + end +end + +function hideGamepadButtons() + for _, button in pairs(GAMEPAD_BUTTONS) do + button.Visible = false + end +end + +function valueInTable(val, tab) + for _, v in pairs(tab) do + if v == val then + return true + end + end + return false +end + +function onInputChanged(inputObject) + local input = inputObject.UserInputType + local inputs = Enum.UserInputType + if valueInTable(input, {inputs.Gamepad1, inputs.Gamepad2, inputs.Gamepad3, inputs.Gamepad4}) then + if inputObject.KeyCode == Enum.KeyCode.Thumbstick1 or inputObject.KeyCode == Enum.KeyCode.Thumbstick2 then + if math.abs(inputObject.Position.X) > 0.1 or math.abs(inputObject.Position.Z) > 0.1 or math.abs(inputObject.Position.Y) > 0.1 then + showGamepadButtons() + end + else + showGamepadButtons() + end + else + hideGamepadButtons() + end +end +UserInputService.InputChanged:connect(onInputChanged) +UserInputService.InputBegan:connect(onInputChanged) +hideGamepadButtons() + +CancelButton.MouseButton1Click:connect(function() + if not buttonsActive and FFlagDelayPurchasePromptActivation then return end + if IsCurrentlyPurchasing then return end + onPromptEnded(false) +end) +BuyButton.MouseButton1Click:connect(onAcceptPurchase) +FreeButton.MouseButton1Click:connect(onAcceptPurchase) +OkButton.MouseButton1Click:connect(function() + if not buttonsActive and FFlagDelayPurchasePromptActivation then return end + if purchaseState == PURCHASE_STATE.FAILED then + onPromptEnded(false) + end +end) +OkPurchasedButton.MouseButton1Click:connect(function() + if not buttonsActive and FFlagDelayPurchasePromptActivation then return end + if purchaseState == PURCHASE_STATE.SUCCEEDED then + onPromptEnded(true) + end +end) +BuyRobuxButton.MouseButton1Click:connect(onBuyRobuxPrompt) +BuyBCButton.MouseButton1Click:connect(onUpgradeBCPrompt) + +MarketplaceService.PromptProductPurchaseRequested:connect(function(player, productId, equipIfPurchased, currencyType) + onPurchasePrompt(player, nil, equipIfPurchased, currencyType, productId) +end) +MarketplaceService.PromptPurchaseRequested:connect(function(player, assetId, equipIfPurchased, currencyType) + onPurchasePrompt(player, assetId, equipIfPurchased, currencyType, nil) +end) +MarketplaceService.PromptGamePassPurchaseRequested:connect(function(player, gamePassId) + onPurchasePrompt(player, nil, false, Enum.CurrencyType.Default, nil, gamePassId) +end) +MarketplaceService.ServerPurchaseVerification:connect(function(serverResponseTable) + if not serverResponseTable then + onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR) + return + end + + if serverResponseTable["playerId"] and tonumber(serverResponseTable["playerId"]) == Players.LocalPlayer.UserId then + onPurchaseSuccess() + end +end) + +GuiService.BrowserWindowClosed:connect(function() + if FFlagFixDesktopRobuxUpsell then + if IsCheckingPlayerFunds then + local isPurchasing = retryPurchase(4) + if isPurchasing then + onAcceptPurchase() + else + onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR) + end + else + onPurchaseFailed(PURCHASE_FAILED.DID_NOT_BUY_ROBUX) + end + + stopPurchaseAnimation() + else + if IsCheckingPlayerFunds then + retryPurchase(4) + end + + onPurchaseFailed(PURCHASE_FAILED.DID_NOT_BUY_ROBUX) + stopPurchaseAnimation() + end +end) + +if IsNativePurchasing then + MarketplaceService.NativePurchaseFinished:connect(function(player, productId, wasPurchased) + nativePurchaseFinished(wasPurchased) + end) +end diff --git a/Client2018/content/scripts/CoreScripts/CoreScripts/Topbar.lua b/Client2018/content/scripts/CoreScripts/CoreScripts/Topbar.lua new file mode 100644 index 0000000..4227c96 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/CoreScripts/Topbar.lua @@ -0,0 +1,1515 @@ +--[[ + // FileName: Topbar.lua + // Written by: SolarCrane + // Description: Code for lua side Top Menu items in ROBLOX. +]] + +--[[ FFLAG VALUES ]] + +local FFlagSetGuiInsetInLoadingScript = settings():GetFFlag("SetGuiInsetInLoadingScript3") + +--[[ END OF FFLAG VALUES ]] + + +--[[ SERVICES ]] + +local CoreGuiService = game:GetService('CoreGui') +local PlayersService = game:GetService('Players') +local GuiService = game:GetService('GuiService') +local InputService = game:GetService('UserInputService') +local StarterGui = game:GetService('StarterGui') +local ContextActionService = game:GetService("ContextActionService") +local RunService = game:GetService('RunService') +local TextService = game:GetService('TextService') +local ChatService = game:GetService('Chat') +local VRService = game:GetService('VRService') + +--[[ END OF SERVICES ]] + +--[[ MODULES ]]-- +local GuiRoot = CoreGuiService:WaitForChild('RobloxGui') +local TopbarConstants = require(GuiRoot.Modules.TopbarConstants) +local Utility = require(GuiRoot.Modules.Settings.Utility) +local GameTranslator = require(GuiRoot.Modules.GameTranslator) +--[[ END OF MODULES ]] + +local topbarEnabled = true +local topbarEnabledChangedEvent = Instance.new('BindableEvent') + +local function isTopbarEnabled() + return topbarEnabled and not VRService.VREnabled +end + +StarterGui:RegisterSetCore("TopbarEnabled", function(enabled) -- registers a placeholder setcore function that keeps track of players enabling/disabling the topbar before it's ready. + if type(enabled) == "boolean" then + topbarEnabled = enabled + end +end) + +local settingsActive = false + +local GameSettings = UserSettings().GameSettings +local Player = PlayersService.LocalPlayer +while not Player do + PlayersService.ChildAdded:wait() + Player = PlayersService.LocalPlayer +end + +local canChat = true + +local accountTypeText = "Account: <13" +local accountTypeTextShort = "<13" + +function calculateAccountText() + accountTypeText = "Account: 13+" + accountTypeTextShort = "13+" + if Player:GetUnder13() then + accountTypeText = "Account: <13" + accountTypeTextShort = "<13" + end +end + +local TenFootInterface = require(GuiRoot.Modules.TenFootInterface) +local isTenFootInterface = TenFootInterface:IsEnabled() + +local Util = {} +do + -- Check if we are running on a touch device + function Util.IsTouchDevice() + return InputService.TouchEnabled + end + + function Util.IsSmallTouchScreen() + local screenResolution = GuiService:GetScreenResolution() + return InputService.TouchEnabled and (screenResolution.Y < 500 or screenResolution.X < 700) + end + + function Util.Create(instanceType) + return function(data) + local obj = Instance.new(instanceType) + for k, v in pairs(data) do + if type(k) == 'number' then + v.Parent = obj + else + obj[k] = v + end + end + return obj + end + end + + function Util.Clamp(low, high, input) + return math.max(low, math.min(high, input)) + end + + function Util.DisconnectEvent(conn) + if conn then + conn:disconnect() + end + return nil + end + + function Util.SetGUIInsetBounds(x1, y1, x2, y2) + GuiService:SetGlobalGuiInset(x1, y1, x2, y2) + if GuiRoot:FindFirstChild("GuiInsetChanged") then + GuiRoot.GuiInsetChanged:Fire(x1, y1, x2, y2) + end + end + + local humanoidCache = {} + function Util.FindPlayerHumanoid(player) + local character = player and player.Character + if character then + local resultHumanoid = humanoidCache[player] + if resultHumanoid and resultHumanoid.Parent == character then + return resultHumanoid + else + humanoidCache[player] = nil -- Bust Old Cache + for _, child in pairs(character:GetChildren()) do + if child:IsA('Humanoid') then + humanoidCache[player] = child + return child + end + end + end + end + end +end + +local function CreateTopBar() + local this = {} + + local playerGuiChangedConn = nil + + local topbarContainer = Util.Create'Frame'{ + Name = "TopBarContainer"; + Size = UDim2.new(1, 0, 0, TopbarConstants.TOPBAR_THICKNESS); + Position = UDim2.new(0, 0, 0, -TopbarConstants.TOPBAR_THICKNESS); + BackgroundTransparency = TopbarConstants.TOPBAR_OPAQUE_TRANSPARENCY; + BackgroundColor3 = TopbarConstants.TOPBAR_BACKGROUND_COLOR; + BorderSizePixel = 0; + Active = true; + Parent = GuiRoot; + }; + + local topbarShadow = Util.Create'ImageLabel'{ + Name = "TopBarShadow"; + Size = UDim2.new(1, 0, 0, 3); + Position = UDim2.new(0, 0, 1, 0); + Image = "rbxasset://textures/ui/TopBar/dropshadow.png"; + BackgroundTransparency = 1; + Active = false; + Visible = false; + Parent = topbarContainer; + }; + + local function ComputeTransparency() + if not isTopbarEnabled() then + return 1 + end + + local playerGui = Player:FindFirstChild('PlayerGui') + if playerGui then + return playerGui:GetTopbarTransparency() + end + + return TopbarConstants.TOPBAR_TRANSLUCENT_TRANSPARENCY + end + + function this:UpdateBackgroundTransparency() + if settingsActive and not VRService.VREnabled then + topbarContainer.BackgroundTransparency = TopbarConstants.TOPBAR_OPAQUE_TRANSPARENCY + topbarShadow.Visible = false + else + topbarContainer.BackgroundTransparency = ComputeTransparency() + topbarContainer.Active = (topbarContainer.BackgroundTransparency ~= 1) + topbarShadow.Visible = (topbarContainer.BackgroundTransparency == 0) + end + end + + function this:GetInstance() + return topbarContainer + end + + spawn(function() + local playerGui = Player:WaitForChild('PlayerGui', 86400) or Player:WaitForChild('PlayerGui') + playerGuiChangedConn = Util.DisconnectEvent(playerGuiChangedConn) + pcall(function() + playerGuiChangedConn = playerGui.TopbarTransparencyChangedSignal:connect(this.UpdateBackgroundTransparency) + end) + this:UpdateBackgroundTransparency() + end) + + return this +end + + +local BarAlignmentEnum = +{ + Right = 0; + Left = 1; + Middle = 2; +} + +local function CreateMenuBar(barAlignment) + local this = {} + local thickness = TopbarConstants.TOPBAR_THICKNESS + local alignment = barAlignment or BarAlignmentEnum.Right + local items = {} + local propertyChangedConnections = {} + local dock = nil + + function this:ArrangeItems() + local totalWidth = 0 + + local spacing = TopbarConstants.ITEM_SPACING + if InputService.VREnabled then + spacing = TopbarConstants.VR_ITEM_SPACING + end + + for i, item in ipairs(items) do + local width = item:GetWidth() + + if alignment == BarAlignmentEnum.Left then + item.Position = UDim2.new(0, totalWidth, 0, 0) + elseif alignment == BarAlignmentEnum.Right then + item.Position = UDim2.new(1, -totalWidth - width, 0, 0) + end + + if i ~= #items then + width = width + spacing + end + + totalWidth = totalWidth + width + end + + if alignment == BarAlignmentEnum.Middle then + local currentX = -totalWidth / 2 + for _, item in ipairs(items) do + item.Position = UDim2.new(0, currentX, 0, 0) + + currentX = currentX + item:GetWidth() + spacing + end + end + + return totalWidth + end + + function this:GetThickness() + return thickness + end + + function this:GetNumberOfItems() + return #items + end + + function this:SetDock(newDock) + dock = newDock + for _, item in pairs(items) do + item.Parent = dock + end + end + + function this:IndexOfItem(searchItem) + for index, item in pairs(items) do + if item == searchItem then + return index + end + end + return nil + end + + function this:ItemAtIndex(index) + return items[index] + end + + function this:GetItems() + return items + end + + function this:AddItem(item, index) + local numItems = self:GetNumberOfItems() + index = Util.Clamp(1, numItems + 1, (index or numItems + 1)) + + local alreadyFoundIndex = self:IndexOfItem(item) + if alreadyFoundIndex then + return item, index + end + + table.insert(items, index, item) + Util.DisconnectEvent(propertyChangedConnections[item]) + propertyChangedConnections[item] = item.Changed:connect(function(property) + if property == 'AbsoluteSize' then + self:ArrangeItems() + end + end) + self:ArrangeItems() + + if dock then + item.Parent = dock + end + + return item, index + end + + function this:RemoveItem(item) + local index = self:IndexOfItem(item) + if index then + local removedItem = table.remove(items, index) + + removedItem.Parent = nil + Util.DisconnectEvent(propertyChangedConnections[removedItem]) + + self:ArrangeItems() + return removedItem, index + end + end + + + return this +end + +local function CreateMenuChangedNotifier() + local this = {} + local notifier3D = require(GuiRoot.Modules.VR.NotifierHint3D) + + function this:PromptNotification() + -- Don't show the notification if we are looking down at the menubar already + notifier3D:BeginNotification(notifier3D.DEFAULT_DURATION) + end + + Player.FriendStatusChanged:connect(function(fromPlayer, friendStatus) + if friendStatus == Enum.FriendStatus.FriendRequestReceived then + this:PromptNotification() + end + end) + + local function findScreenGuiAncestor(object) + if not object then + return nil + end + local parent = object.Parent + if parent and parent:IsA('ScreenGui') then + return parent + end + return findScreenGuiAncestor(parent) + end + + InputService.TextBoxFocused:connect(function(textbox) + local myScreenGui = findScreenGuiAncestor(textbox) + local myScreenGuiParent = myScreenGui and myScreenGui.Parent + if myScreenGuiParent and myScreenGuiParent:IsA('PlayerGui') then + this:PromptNotification() + end + end) + + return this +end + + +local function CreateMenuItem(origInstance) + local this = {} + local instance = origInstance + + function this:SetInstance(newInstance) + if not instance then + instance = newInstance + else + print("Trying to set an Instance of a Menu Item that already has an instance; doing nothing.") + end + end + + function this:GetWidth() + return self.Size.X.Offset + end + + -- We are extending a regular instance. + do + local mt = + { + __index = function (t, k) + return instance[k] + end; + + __newindex = function (t, k, v) + --if instance[k] ~= nil then + instance[k] = v + --else + -- rawset(t, k, v) + --end + end; + } + setmetatable(this, mt) + end + + return this +end + +local function createNormalHealthBar() + local container = Util.Create'ImageButton' + { + Name = "NameHealthContainer"; + Size = UDim2.new(0, TopbarConstants.USERNAME_CONTAINER_WIDTH, 1, 0); + AutoButtonColor = false; + Image = ""; + BackgroundTransparency = 1; + } + + local username = Util.Create'TextLabel'{ + Name = "Username"; + Text = Player.Name; + Size = UDim2.new(1, -14, 0, 18); + Position = UDim2.new(0, 7, 0, 0); + Font = Enum.Font.SourceSansBold; + FontSize = Enum.FontSize.Size14; + BackgroundTransparency = 1; + TextColor3 = TopbarConstants.FONT_COLOR; + TextYAlignment = Enum.TextYAlignment.Bottom; + TextXAlignment = Enum.TextXAlignment.Left; + Parent = container; + }; + + + local accountType = Util.Create'TextLabel'{ + Name = "AccountType"; + Text = accountTypeText; + Size = UDim2.new(1, -14, 0, 9); + Position = UDim2.new(0, 7, 0, 20); + Font = Enum.Font.SourceSans; + TextSize = 11; + BackgroundTransparency = 1; + TextColor3 = TopbarConstants.FONT_COLOR; + TextYAlignment = Enum.TextYAlignment.Bottom; + TextXAlignment = Enum.TextXAlignment.Left; + Parent = container; + }; + + spawn(function() + wait() + calculateAccountText() + accountType.Text = accountTypeText + end) + + local healthContainer = Util.Create'Frame'{ + Name = "HealthContainer"; + Size = UDim2.new(1, -14, 0, 3); + Position = UDim2.new(0, 7, 1, -7); + BorderSizePixel = 0; + BackgroundColor3 = TopbarConstants.HEALTH_BACKGROUND_COLOR; + Parent = container; + }; + + local healthFill = Util.Create'Frame'{ + Name = "HealthFill"; + Size = UDim2.new(1, 0, 1, 0); + BorderSizePixel = 0; + BackgroundColor3 = TopbarConstants.HEALTH_GREEN_COLOR; + Parent = healthContainer; + }; + + local function onResized(viewportSize, isPortrait) + if isPortrait then + username.TextXAlignment = Enum.TextXAlignment.Right + accountType.TextXAlignment = Enum.TextXAlignment.Right + container.Size = UDim2.new(0.3, 0, 1, 0) + container.AnchorPoint = Vector2.new(1, 0) + else + username.TextXAlignment = Enum.TextXAlignment.Left + accountType.TextXAlignment = Enum.TextXAlignment.Left + container.Size = UDim2.new(0, TopbarConstants.USERNAME_CONTAINER_WIDTH, 1, 0) + container.AnchorPoint = Vector2.new(0, 0) + end + end + Utility:OnResized(container, onResized) + + return container, username, healthContainer, healthFill, accountType +end + +----- HEALTH ----- +local function CreateUsernameHealthMenuItem() + + local container, username, healthContainer, healthFill = nil + + if isTenFootInterface then + container, username, healthContainer, healthFill, accountType = TenFootInterface:CreateHealthBar() + else + container, username, healthContainer, healthFill, accountType = createNormalHealthBar() + end + + local hurtOverlay = Util.Create'ImageLabel' + { + Name = "HurtOverlay"; + BackgroundTransparency = 1; + Image = TopbarConstants.HURT_OVERLAY_IMAGE; + Position = UDim2.new(-10,0,-10,0); + Size = UDim2.new(20,0,20,0); + Visible = false; + Parent = GuiRoot; + }; + + local this = CreateMenuItem(container) + + --- EVENTS --- + local humanoidChangedConn, childAddedConn, childRemovedConn = nil + -------------- + + local HealthBarEnabled = true + local NameEnabled = true + local CurrentHumanoid = nil + + local function AnimateHurtOverlay() + if hurtOverlay and not VRService.VREnabled then + local newSize = UDim2.new(20, 0, 20, 0) + local newPos = UDim2.new(-10, 0, -10, 0) + + if hurtOverlay:IsDescendantOf(game) then + -- stop any tweens on overlay + hurtOverlay:TweenSizeAndPosition(newSize, newPos, Enum.EasingDirection.Out, Enum.EasingStyle.Linear, 0, true, function() + -- show the gui + hurtOverlay.Size = UDim2.new(1,0,1,0) + hurtOverlay.Position = UDim2.new(0,0,0,0) + hurtOverlay.Visible = true + -- now tween the hide + if hurtOverlay:IsDescendantOf(game) then + hurtOverlay:TweenSizeAndPosition(newSize, newPos, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 10, false, function() + hurtOverlay.Visible = false + end) + else + hurtOverlay.Size = newSize + hurtOverlay.Position = newPos + end + end) + end + end + end + + local healthColorToPosition = { + [Vector3.new(TopbarConstants.HEALTH_RED_COLOR.r, + TopbarConstants.HEALTH_RED_COLOR.g, + TopbarConstants.HEALTH_RED_COLOR.b)] = 0.1; + [Vector3.new(TopbarConstants.HEALTH_YELLOW_COLOR.r, + TopbarConstants.HEALTH_YELLOW_COLOR.g, + TopbarConstants.HEALTH_YELLOW_COLOR.b)] = 0.5; + [Vector3.new(TopbarConstants.HEALTH_GREEN_COLOR.r, + TopbarConstants.HEALTH_GREEN_COLOR.g, + TopbarConstants.HEALTH_GREEN_COLOR.b)] = 0.8; + } + local min = 0.1 + local minColor = TopbarConstants.HEALTH_RED_COLOR + local max = 0.8 + local maxColor = TopbarConstants.HEALTH_GREEN_COLOR + + local function HealthbarColorTransferFunction(healthPercent) + if healthPercent < min then + return minColor + elseif healthPercent > max then + return maxColor + end + + -- Shepard's Interpolation + local numeratorSum = Vector3.new(0,0,0) + local denominatorSum = 0 + for colorSampleValue, samplePoint in pairs(healthColorToPosition) do + local distance = healthPercent - samplePoint + if distance == 0 then + -- If we are exactly on an existing sample value then we don't need to interpolate + return Color3.new(colorSampleValue.x, colorSampleValue.y, colorSampleValue.z) + else + local wi = 1 / (distance*distance) + numeratorSum = numeratorSum + wi * colorSampleValue + denominatorSum = denominatorSum + wi + end + end + local result = numeratorSum / denominatorSum + return Color3.new(result.x, result.y, result.z) + end + + local function UpdateHealthVisible() + local isEnabled = HealthBarEnabled and CurrentHumanoid and CurrentHumanoid.Health ~= CurrentHumanoid.MaxHealth + healthContainer.Visible = isEnabled + end + + local function OnHumanoidAdded(humanoid) + CurrentHumanoid = humanoid + local lastHealth = humanoid.Health + local function OnHumanoidHealthChanged(health) + UpdateHealthVisible() + if humanoid then + local healthDelta = lastHealth - health + local healthPercent = health / humanoid.MaxHealth + if humanoid.MaxHealth <= 0 then + healthPercent = 0 + end + healthPercent = Util.Clamp(0, 1, healthPercent) + local healthColor = HealthbarColorTransferFunction(healthPercent) + local thresholdForHurtOverlay = + humanoid.MaxHealth * TopbarConstants.HEALTH_PERCANTAGE_FOR_OVERLAY + + if healthDelta >= thresholdForHurtOverlay and health ~= humanoid.MaxHealth and StarterGui:GetCoreGuiEnabled("Health") == true then + AnimateHurtOverlay() + end + + healthFill.Size = UDim2.new(healthPercent, 0, 1, 0) + healthFill.BackgroundColor3 = healthColor + + lastHealth = health + end + end + Util.DisconnectEvent(humanoidChangedConn) + humanoidChangedConn = humanoid.HealthChanged:connect(OnHumanoidHealthChanged) + OnHumanoidHealthChanged(lastHealth) + end + + local function OnCharacterAdded(character) + local humanoid = Util.FindPlayerHumanoid(Player) + if humanoid then + OnHumanoidAdded(humanoid) + end + + local function onChildAddedOrRemoved() + local tempHumanoid = Util.FindPlayerHumanoid(Player) + if tempHumanoid and tempHumanoid ~= humanoid then + humanoid = tempHumanoid + OnHumanoidAdded(humanoid) + end + end + Util.DisconnectEvent(childAddedConn) + Util.DisconnectEvent(childRemovedConn) + childAddedConn = character.ChildAdded:connect(onChildAddedOrRemoved) + childRemovedConn = character.ChildRemoved:connect(onChildAddedOrRemoved) + end + + local function UpdateContainerEnabled() + if HealthBarEnabled or NameEnabled then + container.Visible = true + container.Active = true + else + container.Visible = false + container.Active = false + end + end + + rawset(this, "SetHealthbarEnabled", + function(self, enabled) + HealthBarEnabled = enabled + UpdateHealthVisible() + UpdateContainerEnabled() + end) + + rawset(this, "SetNameVisible", + function(self, visible) + NameEnabled = visible + username.Visible = visible + accountType.Visible = visible + UpdateContainerEnabled() + end) + + -- Don't need to disconnect this one because we never reconnect it. + Player.CharacterAdded:connect(OnCharacterAdded) + if Player.Character then + OnCharacterAdded(Player.Character) + end + + local PlayerlistModule = require(GuiRoot.Modules.PlayerlistModule) + container.MouseButton1Click:connect(function() + if isTopbarEnabled() then + PlayerlistModule.ToggleVisibility() + end + end) + + return this +end +----- END OF HEALTH ----- + +----- LEADERSTATS ----- + +local function CreateLeaderstatsMenuItem() + local PlayerlistModule = require(GuiRoot.Modules.PlayerlistModule) + + local leaderstatsContainer = Util.Create'ImageButton' + { + Name = "LeaderstatsContainer"; + Size = UDim2.new(0, 0, 1, 0); + AutoButtonColor = false; + Image = ""; + BackgroundTransparency = 1; + }; + + local this = CreateMenuItem(leaderstatsContainer) + local columns = {} + + rawset(this, "SetColumns", + function(self, columnsList) + -- Should we handle is the screen dimensions change and it is no longer a small touch device after we set columns? + local isSmallTouchDevice = Util.IsSmallTouchScreen() + local numColumns = #columnsList + + -- Destroy old columns + for _, oldColumn in pairs(columns) do + oldColumn:Destroy() + end + columns = {} + -- End destroy old columns + local count = 0 + for index, columnData in pairs(columnsList) do -- i = 1, numColumns do + if not isSmallTouchDevice or index <= 1 then + local columnName = columnData.Name + local columnValue = columnData.Text + + local columnframe = Util.Create'Frame' + { + Name = "Column" .. tostring(index); + Size = UDim2.new(0, + TopbarConstants.COLUMN_WIDTH + (index == numColumns and 0 or TopbarConstants.NAME_LEADERBOARD_SEP_WIDTH), + 1, 0); + Position = UDim2.new(0, + TopbarConstants.NAME_LEADERBOARD_SEP_WIDTH + (TopbarConstants.COLUMN_WIDTH + TopbarConstants.NAME_LEADERBOARD_SEP_WIDTH) * (index-1), + 0, 0); + BackgroundTransparency = 1; + Parent = leaderstatsContainer; + + Util.Create'TextLabel' + { + Name = "ColumnName"; + Text = GameTranslator:TranslateGameText(CoreGuiService, columnName); + Size = UDim2.new(1, 0, 0, 10); + Position = UDim2.new(0, 0, 0, 4); + Font = Enum.Font.SourceSans; + FontSize = Enum.FontSize.Size14; + BorderSizePixel = 0; + BackgroundTransparency = 1; + TextColor3 = TopbarConstants.FONT_COLOR; + TextYAlignment = Enum.TextYAlignment.Center; + TextXAlignment = Enum.TextXAlignment.Center; + }; + + Util.Create'TextLabel' + { + Name = "ColumnValue"; + Text = columnValue; + Size = UDim2.new(1, 0, 0, 10); + Position = UDim2.new(0, 0, 0, 19); + Font = Enum.Font.SourceSansBold; + FontSize = Enum.FontSize.Size14; + BorderSizePixel = 0; + BackgroundTransparency = 1; + TextColor3 = TopbarConstants.FONT_COLOR; + TextYAlignment = Enum.TextYAlignment.Center; + TextXAlignment = Enum.TextXAlignment.Center; + }; + }; + columns[columnName] = columnframe + count = count + 1 + end + end + leaderstatsContainer.Size = UDim2.new(0, + TopbarConstants.COLUMN_WIDTH * count + TopbarConstants.NAME_LEADERBOARD_SEP_WIDTH * count, + 1, 0) + end) + + rawset(this, "UpdateColumnValue", + function(self, columnName, value) + local column = columns[columnName] + local columnValue = column and column:FindFirstChild('ColumnValue') + if columnValue then + columnValue.Text = tostring(value) + end + end) + + topbarEnabledChangedEvent.Event:connect(function(enabled) + PlayerlistModule.TopbarEnabledChanged(enabled and not VRService.VREnabled) --We don't show the playerlist at all in VR + end) + + this:SetColumns(PlayerlistModule.GetStats()) + PlayerlistModule.OnLeaderstatsChanged.Event:connect(function(newStatColumns) + if not Utility:IsPortrait() then + this:SetColumns(newStatColumns) + end + end) + + PlayerlistModule.OnStatChanged.Event:connect(function(statName, statValueAsString) + this:UpdateColumnValue(statName, statValueAsString) + end) + + leaderstatsContainer.MouseButton1Click:connect(function() + if isTopbarEnabled() then + PlayerlistModule.ToggleVisibility() + end + end) + + return this +end +----- END OF LEADERSTATS ----- + +--- SETTINGS --- +local function CreateSettingsIcon(topBarInstance) + local MenuModule = require(GuiRoot.Modules.Settings.SettingsHub) + + local settingsIconButton = Util.Create'ImageButton' + { + Name = "Settings"; + Size = UDim2.new(0, 50, 0, TopbarConstants.TOPBAR_THICKNESS); + Image = ""; + AutoButtonColor = false; + BackgroundTransparency = 1; + } + + local settingsIconImage = Util.Create'ImageLabel' + { + Name = "SettingsIcon"; + Size = UDim2.new(0, 32, 0, 25); + Position = UDim2.new(0.5, -16, 0.5, -12); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Menu/Hamburger.png"; + Parent = settingsIconButton; + }; + + local function UpdateHamburgerIcon() + if settingsActive then + settingsIconImage.Image = "rbxasset://textures/ui/Menu/HamburgerDown.png"; + else + settingsIconImage.Image = "rbxasset://textures/ui/Menu/Hamburger.png"; + end + end + + local function toggleSettings() + if settingsActive == false then + settingsActive = true + else + settingsActive = false + end + + MenuModule:ToggleVisibility() + UpdateHamburgerIcon() + + return settingsActive + end + + settingsIconButton.MouseButton1Click:connect(function() toggleSettings() end) + + MenuModule.SettingsShowSignal:connect(function(active) + settingsActive = active + topBarInstance:UpdateBackgroundTransparency() + UpdateHamburgerIcon() + end) + + local menuItem = CreateMenuItem(settingsIconButton) + + rawset(menuItem, "SetTransparency", function(self, transparency) + settingsIconImage.ImageTransparency = transparency + end) + rawset(menuItem, "SetImage", function(self, image) + settingsIconImage.Image = image + end) + rawset(menuItem, "SetSettingsActive", function(self, active) + settingsActive = active + MenuModule:ToggleVisibility(settingsActive) + UpdateHamburgerIcon() + + return settingsActive + end) + + return menuItem +end + + +------------ + +--- CHAT --- +local function CreateUnreadMessagesNotifier(ChatModule) + local chatActive = false + local lastMessageCount = 0 + + local chatCounter = Util.Create'ImageLabel' + { + Name = "ChatCounter"; + Size = UDim2.new(0, 18, 0, 18); + Position = UDim2.new(1, -12, 0, -4); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Chat/MessageCounter.png"; + Visible = false; + }; + + local chatCountText = Util.Create'TextLabel' + { + Name = "ChatCounterText"; + Text = ''; + Size = UDim2.new(0, 13, 0, 9); + Position = UDim2.new(0.5, -7, 0.5, -7); + Font = Enum.Font.SourceSansBold; + FontSize = Enum.FontSize.Size14; + BorderSizePixel = 0; + BackgroundTransparency = 1; + TextColor3 = TopbarConstants.FONT_COLOR; + TextYAlignment = Enum.TextYAlignment.Center; + TextXAlignment = Enum.TextXAlignment.Center; + Parent = chatCounter; + }; + + local function OnUnreadMessagesChanged(count) + if chatActive then + lastMessageCount = count + end + local unreadCount = count - lastMessageCount + + if unreadCount <= 0 then + chatCountText.Text = "" + chatCounter.Visible = false + else + if unreadCount < 100 then + chatCountText.Text = tostring(unreadCount) + else + chatCountText.Text = "!" + end + chatCounter.Visible = true + end + end + + local function onChatStateChanged(visible) + chatActive = visible + if ChatModule then + OnUnreadMessagesChanged(ChatModule:GetMessageCount()) + end + end + + + if ChatModule then + if ChatModule.VisibilityStateChanged then + ChatModule.VisibilityStateChanged:connect(onChatStateChanged) + end + if ChatModule.MessagesChanged then + ChatModule.MessagesChanged:connect(OnUnreadMessagesChanged) + end + + onChatStateChanged(ChatModule:GetVisibility()) + OnUnreadMessagesChanged(ChatModule:GetMessageCount()) + end + + return chatCounter +end + +local function GetChatIcon(chatIconName) + if Player:GetUnder13() then + return "rbxasset://textures/ui/Chat/" .. chatIconName .. "Flip.png" + else + return "rbxasset://textures/ui/Chat/" .. chatIconName .. ".png" + end +end + +local function CreateChatIcon() + local chatEnabled = game:GetService("UserInputService"):GetPlatform() ~= Enum.Platform.XBoxOne + if not chatEnabled then return end + + local ChatModule = require(GuiRoot.Modules.ChatSelector) + + local debounce = 0 + + local chatIconButton = Util.Create'ImageButton' + { + Name = "Chat"; + Size = UDim2.new(0, 50, 0, TopbarConstants.TOPBAR_THICKNESS); + Image = ""; + AutoButtonColor = false; + BackgroundTransparency = 1; + }; + + local chatIconImage = Util.Create'ImageLabel' + { + Name = "ChatIcon"; + Size = UDim2.new(0, 28, 0, 27); + Position = UDim2.new(0.5, -14, 0.5, -13); + BackgroundTransparency = 1; + Image = GetChatIcon("Chat"); + Parent = chatIconButton; + }; + if not Util.IsTouchDevice() then + local chatCounter = CreateUnreadMessagesNotifier(ChatModule) + chatCounter.Parent = chatIconImage; + end + + local function updateIcon(down) + if down then + chatIconImage.Image = GetChatIcon("ChatDown") + else + chatIconImage.Image = GetChatIcon("Chat") + end + end + + local function onChatStateChanged(visible) + if not Util.IsTouchDevice() then + updateIcon(visible) + GameSettings.ChatVisible = visible + end + end + + local function toggleChat() + if VRService.VREnabled then + ChatModule:ToggleVisibility() + elseif Util.IsTouchDevice() or ChatModule:IsBubbleChatOnly() then + if debounce + TopbarConstants.DEBOUNCE_TIME < tick() then + if Util.IsTouchDevice() then + ChatModule:SetVisible(true) + end + ChatModule:FocusChatBar() + end + else + ChatModule:ToggleVisibility() + end + end + + topbarEnabledChangedEvent.Event:connect(function(enabled) + ChatModule:TopbarEnabledChanged(enabled) + end) + + chatIconButton.MouseButton1Click:connect(function() + toggleChat() + end) + + if ChatModule.ChatBarFocusChanged then + ChatModule.ChatBarFocusChanged:connect(function(isFocused) + if Util.IsTouchDevice() or ChatModule:IsBubbleChatOnly() then + updateIcon(isFocused) + debounce = tick() + end + end) + end + + if Util.IsTouchDevice() or ChatModule:IsBubbleChatOnly() then + updateIcon(false) + end + + if ChatModule.BubbleChatOnlySet then + ChatModule.BubbleChatOnlySet:connect(function() + if ChatModule:IsBubbleChatOnly() then + updateIcon(false) + elseif not Util.IsTouchDevice() then + updateIcon(true) + end + end) + end + + if ChatModule.VisibilityStateChanged then + ChatModule.VisibilityStateChanged:connect(onChatStateChanged) + end + + if not VRService.VREnabled then + -- check to see if the chat was disabled + local willEnableChat = true + willEnableChat = GameSettings.ChatVisible + if Util.IsSmallTouchScreen() then + willEnableChat = false + end + ChatModule:SetVisible(willEnableChat) + end + + local menuItem = CreateMenuItem(chatIconButton) + + rawset(menuItem, "ToggleChat", function(self) + toggleChat() + end) + rawset(menuItem, "SetTransparency", function(self, transparency) + chatIconImage.ImageTransparency = transparency + end) + rawset(menuItem, "SetImage", function(self, newImage) + chatIconImage.Image = newImage + end) + + return menuItem +end + +local function CreateMobileHideChatIcon() + local ChatModule = require(GuiRoot.Modules.ChatSelector) + + local chatHideIconButton = Util.Create'ImageButton' + { + Name = "ChatVisible"; + Size = UDim2.new(0, 50, 0, TopbarConstants.TOPBAR_THICKNESS); + Image = ""; + AutoButtonColor = false; + BackgroundTransparency = 1; + }; + + local chatIconImage = Util.Create'ImageLabel' + { + Name = "ChatVisibleIcon"; + Size = UDim2.new(0, 28, 0, 27); + Position = UDim2.new(0.5, -14, 0.5, -13); + BackgroundTransparency = 1; + Image = GetChatIcon("ToggleChat"); + Parent = chatHideIconButton; + }; + + local unreadMessageNotifier = CreateUnreadMessagesNotifier(ChatModule) + unreadMessageNotifier.Parent = chatIconImage + + local function updateIcon(down) + if down then + chatIconImage.Image = GetChatIcon("ToggleChatDown") + else + chatIconImage.Image = GetChatIcon("ToggleChat") + end + end + + local function toggleChat() + ChatModule:ToggleVisibility() + end + + local function onChatStateChanged(visible) + updateIcon(visible) + end + + chatHideIconButton.MouseButton1Click:connect(function() + toggleChat() + GameSettings.ChatVisible = ChatModule:GetVisibility() + end) + + if ChatModule.VisibilityStateChanged then + ChatModule.VisibilityStateChanged:connect(onChatStateChanged) + end + onChatStateChanged(ChatModule:GetVisibility()) + + return CreateMenuItem(chatHideIconButton) +end + + + + +----------- + +--- Backpack --- +local function CreateBackpackIcon() + local BackpackModule = require(GuiRoot.Modules.BackpackScript) + + local backpackIconButton = Util.Create'ImageButton' + { + Name = "Backpack"; + Size = UDim2.new(0, 50, 0, TopbarConstants.TOPBAR_THICKNESS); + Image = ""; + AutoButtonColor = false; + BackgroundTransparency = 1; + }; + + local backpackIconImage = Util.Create'ImageLabel' + { + Name = "BackpackIcon"; + Size = UDim2.new(0, 22, 0, 28); + Position = UDim2.new(0.5, -11, 0.5, -14); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Backpack/Backpack.png"; + Parent = backpackIconButton; + }; + + local function onBackpackStateChanged(open) + if open then + backpackIconImage.Image = "rbxasset://textures/ui/Backpack/Backpack_Down.png"; + else + backpackIconImage.Image = "rbxasset://textures/ui/Backpack/Backpack.png"; + end + end + + BackpackModule.StateChanged.Event:connect(onBackpackStateChanged) + + local function toggleBackpack() + BackpackModule:OpenClose() + end + + topbarEnabledChangedEvent.Event:connect(function(enabled) + BackpackModule:TopbarEnabledChanged(enabled) + end) + + backpackIconButton.MouseButton1Click:connect(function() + BackpackModule:OpenClose() + end) + + return CreateMenuItem(backpackIconButton) +end +-------------- + +----- Stop Recording -- +local function CreateStopRecordIcon() + local stopRecordIconButton = Util.Create'ImageButton' + { + Name = "StopRecording"; + Size = UDim2.new(0, 50, 0, TopbarConstants.TOPBAR_THICKNESS); + Image = ""; + Visible = true; + BackgroundTransparency = 1; + }; + stopRecordIconButton:SetVerb("RecordToggle") + + local stopRecordIconLabel = Util.Create'ImageLabel' + { + Name = "StopRecordingIcon"; + Size = UDim2.new(0, 28, 0, 28); + Position = UDim2.new(0.5, -14, 0.5, -14); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/RecordDown.png"; + Parent = stopRecordIconButton; + }; + + return CreateMenuItem(stopRecordIconButton) +end +----------------------- + + + + +local function CreateNoTopBarAccountType() + local container = Util.Create'ImageButton' + { + Name = "AccountTypeContainer"; + Size = UDim2.new(0, 0, 0, 0); + AutoButtonColor = false; + Image = ""; + BackgroundTransparency = 1; + } + + local accountTypeTextLabel = Util.Create'TextLabel'{ + Name = "AccountTypeText"; + Text = accountTypeTextShort; + Size = UDim2.new(1, -12, 1, -12); + Position = UDim2.new(0, 0, 0, 6); + Font = Enum.Font.SourceSansBold; + FontSize = Enum.FontSize.Size14; + BackgroundTransparency = 1; + TextColor3 = TopbarConstants.FONT_COLOR; + TextYAlignment = Enum.TextYAlignment.Center; + TextXAlignment = Enum.TextXAlignment.Left; + Parent = container; + }; + + spawn(function() + wait() + calculateAccountText() + accountTypeTextLabel.Text = accountTypeTextShort + if container.Visible then + local textBounds = accountTypeTextLabel.TextBounds.X + local containerSize = textBounds + container.Size = UDim2.new(0, containerSize, 1, 0) + end + end) + + local function UpdateNoTopBarAccountType() + if isTopbarEnabled() or VRService.VREnabled then + container.Visible = false + container.Size = UDim2.new(0, 0, 0, 0) + else + container.Visible = true + local textBounds = accountTypeTextLabel.TextBounds.X + local containerSize = textBounds + container.Size = UDim2.new(0, containerSize, 1, 0) + end + end + + topbarEnabledChangedEvent.Event:connect(UpdateNoTopBarAccountType) + VRService:GetPropertyChangedSignal("VREnabled"):Connect(UpdateNoTopBarAccountType) + UpdateNoTopBarAccountType() + + local menuItem = CreateMenuItem(container) + + return menuItem +end + +------------------------ + +local TopBar = CreateTopBar() +local LeftMenubar = CreateMenuBar(BarAlignmentEnum.Left) +local RightMenubar = CreateMenuBar(BarAlignmentEnum.Right) + +local settingsIcon = CreateSettingsIcon(TopBar) +local noTopBarAccountType = nil + +if isTenFootInterface then + spawn(function() + wait() + calculateAccountText() + TenFootInterface:CreateAccountType(accountTypeTextShort) + end) +elseif not isTenFootInterface then + noTopBarAccountType = CreateNoTopBarAccountType() +end + +local mobileShowChatIcon = Util.IsTouchDevice() and CreateMobileHideChatIcon() or nil +local chatIcon = CreateChatIcon() +local backpackIcon = CreateBackpackIcon() +local stopRecordingIcon = CreateStopRecordIcon() + +local leaderstatsMenuItem = CreateLeaderstatsMenuItem() +local nameAndHealthMenuItem = CreateUsernameHealthMenuItem() + +local menuChangedNotifier3D = nil + +local LEFT_ITEM_ORDER = {} +local RIGHT_ITEM_ORDER = {} + + +-- Set Item Orders +if settingsIcon then + LEFT_ITEM_ORDER[settingsIcon] = 1 +end +if noTopBarAccountType then + LEFT_ITEM_ORDER[noTopBarAccountType] = 2 +end +if mobileShowChatIcon then + LEFT_ITEM_ORDER[mobileShowChatIcon] = 3 +end +if chatIcon then + LEFT_ITEM_ORDER[chatIcon] = 4 +end +if backpackIcon then + LEFT_ITEM_ORDER[backpackIcon] = 5 +end +if stopRecordingIcon then + LEFT_ITEM_ORDER[stopRecordingIcon] = 6 +end + +if leaderstatsMenuItem then + RIGHT_ITEM_ORDER[leaderstatsMenuItem] = 1 +end +if nameAndHealthMenuItem and not isTenFootInterface then + RIGHT_ITEM_ORDER[nameAndHealthMenuItem] = 2 +end + +------------------------- + + +local function AddItemInOrder(Bar, Item, ItemOrder) + local index = 1 + while ItemOrder[Bar:ItemAtIndex(index)] and ItemOrder[Bar:ItemAtIndex(index)] < ItemOrder[Item] do + index = index + 1 + end + Bar:AddItem(Item, index) +end + +local ChatModule = require(GuiRoot.Modules.ChatSelector) + +local function OnCoreGuiChanged(coreGuiType, coreGuiEnabled) + local enabled = coreGuiEnabled and topbarEnabled + if coreGuiType == Enum.CoreGuiType.PlayerList or coreGuiType == Enum.CoreGuiType.All then + if leaderstatsMenuItem then + if enabled then + AddItemInOrder(RightMenubar, leaderstatsMenuItem, RIGHT_ITEM_ORDER) + else + RightMenubar:RemoveItem(leaderstatsMenuItem) + end + end + end + if coreGuiType == Enum.CoreGuiType.Health or coreGuiType == Enum.CoreGuiType.All then + if nameAndHealthMenuItem then + nameAndHealthMenuItem:SetHealthbarEnabled(enabled) + end + end + if coreGuiType == Enum.CoreGuiType.Backpack or coreGuiType == Enum.CoreGuiType.All then + if backpackIcon then + if enabled then + AddItemInOrder(LeftMenubar, backpackIcon, LEFT_ITEM_ORDER) + else + LeftMenubar:RemoveItem(backpackIcon) + end + end + end + if coreGuiType == Enum.CoreGuiType.Chat or coreGuiType == Enum.CoreGuiType.All then + enabled = enabled and (not ChatModule:IsDisabled()) + local ChatSelector = require(GuiRoot.Modules.ChatSelector) + local showTopbarChatIcon = enabled + + if showTopbarChatIcon then + if Util.IsTouchDevice() or ChatModule:IsBubbleChatOnly() then + if chatIcon and canChat then + AddItemInOrder(LeftMenubar, chatIcon, LEFT_ITEM_ORDER) + end + else + if chatIcon then + AddItemInOrder(LeftMenubar, chatIcon, LEFT_ITEM_ORDER) + end + end + if mobileShowChatIcon and ChatModule:ClassicChatEnabled() then + AddItemInOrder(LeftMenubar, mobileShowChatIcon, LEFT_ITEM_ORDER) + end + else + if chatIcon then + LeftMenubar:RemoveItem(chatIcon) + end + if mobileShowChatIcon then + LeftMenubar:RemoveItem(mobileShowChatIcon) + end + end + end + + if nameAndHealthMenuItem then + local playerListOn = StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.PlayerList) + local healthbarOn = StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Health) + -- Left-align the player's name if either playerlist or healthbar is shown + nameAndHealthMenuItem:SetNameVisible(topbarEnabled) + end +end + +local function OnChatModuleDisabled() + if chatIcon then + LeftMenubar:RemoveItem(chatIcon) + end + if mobileShowChatIcon then + LeftMenubar:RemoveItem(mobileShowChatIcon) + end +end + +if ChatModule.ChatDisabled then + ChatModule.ChatDisabled:connect(function() + OnChatModuleDisabled() + end) +end + +TopBar:UpdateBackgroundTransparency() + +LeftMenubar:SetDock(TopBar:GetInstance()) +RightMenubar:SetDock(TopBar:GetInstance()) + +if not isTenFootInterface and not FFlagSetGuiInsetInLoadingScript then + Util.SetGUIInsetBounds(0, TopbarConstants.TOPBAR_THICKNESS, 0, 0) +end + +if settingsIcon then + AddItemInOrder(LeftMenubar, settingsIcon, LEFT_ITEM_ORDER) +end +if noTopBarAccountType and not isTenFootInterface then + AddItemInOrder(LeftMenubar, noTopBarAccountType, LEFT_ITEM_ORDER) +end +if nameAndHealthMenuItem and isTopbarEnabled() and not isTenFootInterface then + AddItemInOrder(RightMenubar, nameAndHealthMenuItem, RIGHT_ITEM_ORDER) +end + +local gameOptions = settings():FindFirstChild("Game Options") +if gameOptions and not isTenFootInterface then + local success, result = pcall(function() + gameOptions.VideoRecordingChangeRequest:connect(function(recording) + if recording and isTopbarEnabled() then + AddItemInOrder(LeftMenubar, stopRecordingIcon, LEFT_ITEM_ORDER) + else + LeftMenubar:RemoveItem(stopRecordingIcon) + end + end) + end) +end + +local function topbarEnabledChanged() + if VRService.VREnabled then + Util.SetGUIInsetBounds(0, 0, 0, 0) + else + if not isTenFootInterface and not FFlagSetGuiInsetInLoadingScript then + Util.SetGUIInsetBounds(0, TopbarConstants.TOPBAR_THICKNESS, 0, 0) + end + end + + + topbarEnabledChangedEvent:Fire(topbarEnabled) + TopBar:UpdateBackgroundTransparency() + for _, enumItem in pairs(Enum.CoreGuiType:GetEnumItems()) do + -- The All enum will be false if any of the coreguis are false + -- therefore by force updating it we are clobbering the previous sets + if enumItem ~= Enum.CoreGuiType.All then + OnCoreGuiChanged(enumItem, StarterGui:GetCoreGuiEnabled(enumItem)) + end + end +end + +--Temporarily disable the leaderstats while in portrait mode. +--Will come back to this when a new design is ready. +local PlayerlistModule = require(GuiRoot.Modules.PlayerlistModule) +local function onResized(viewportSize, isPortrait) + if isPortrait then + leaderstatsMenuItem:SetColumns({}) + else + leaderstatsMenuItem:SetColumns(PlayerlistModule.GetStats()) + end + RightMenubar:ArrangeItems() +end +Utility:OnResized(leaderstatsMenuItem, onResized) + +topbarEnabledChanged() -- if it was set before this point, enable/disable it now +StarterGui:RegisterSetCore("TopbarEnabled", function(enabled) + if type(enabled) == "boolean" then + topbarEnabled = enabled + topbarEnabledChanged() + end +end) + +spawn(function() + local success, localUserCanChat = pcall(function() + return ChatService:CanUserChatAsync(Player.UserId) + end) + canChat = RunService:IsStudio() or (success and localUserCanChat) + if canChat == false then + if Util.IsTouchDevice() or ChatModule:IsBubbleChatOnly() then + if chatIcon then + LeftMenubar:RemoveItem(chatIcon) + end + if ChatModule:IsBubbleChatOnly() and mobileShowChatIcon then + LeftMenubar:RemoveItem(mobileShowChatIcon) + end + end + ChatModule:SetVisible(false) + end +end) + +-- Hook-up coregui changing +StarterGui.CoreGuiChangedSignal:connect(OnCoreGuiChanged) diff --git a/Client2018/content/scripts/CoreScripts/CoreScripts/VehicleHud.lua b/Client2018/content/scripts/CoreScripts/CoreScripts/VehicleHud.lua new file mode 100644 index 0000000..b3b934f --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/CoreScripts/VehicleHud.lua @@ -0,0 +1,155 @@ +--[[ + // Filename: VehicleHud.lua + // Version 1.0 + // Written by: jmargh + // Description: Implementation of the VehicleSeat HUD + + // TODO: + Once this is live and stable, move to PlayerScripts as module +]] +local RunService = game:GetService('RunService') +local Players = game:GetService('Players') +while not Players.LocalPlayer do + wait() +end +local LocalPlayer = Players.LocalPlayer +local RobloxGui = script.Parent +local CurrentVehicleSeat = nil +local VehicleSeatHeartbeatCn = nil +local VehicleSeatHUDChangedCn = nil + +local RobloxGui = game:GetService("CoreGui"):WaitForChild("RobloxGui") +RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface") +local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled() + + +--[[ Images ]]-- +local VEHICLE_HUD_BG = 'rbxasset://textures/ui/Vehicle/SpeedBarBKG.png' +local SPEED_BAR_EMPTY = 'rbxasset://textures/ui/Vehicle/SpeedBarEmpty.png' +local SPEED_BAR = 'rbxasset://textures/ui/Vehicle/SpeedBar.png' + +--[[ Constants ]]-- +local BOTTOM_OFFSET = (isTenFootInterface and 100 or 70) + +--[[ Gui Creation ]]-- +local function createImageLabel(name, size, position, image, parent) + local imageLabel = Instance.new('ImageLabel') + imageLabel.Name = name + imageLabel.Size = size + imageLabel.Position = position + imageLabel.BackgroundTransparency = 1 + imageLabel.Image = image + imageLabel.Parent = parent + + return imageLabel +end + +local function createTextLabel(name, alignment, text, parent) + local textLabel = Instance.new('TextLabel') + textLabel.Name = name + textLabel.Size = UDim2.new(1, -4, 0, (isTenFootInterface and 50 or 20)) + textLabel.Position = UDim2.new(0, 2, 0, (isTenFootInterface and -50 or -20)) + textLabel.BackgroundTransparency = 1 + textLabel.TextXAlignment = alignment + textLabel.Font = Enum.Font.SourceSans + textLabel.FontSize = (isTenFootInterface and Enum.FontSize.Size48 or Enum.FontSize.Size18) + textLabel.TextColor3 = Color3.new(1, 1, 1) + textLabel.TextStrokeTransparency = 0.5 + textLabel.TextStrokeColor3 = Color3.new(49/255, 49/255, 49/255) + textLabel.Text = text + textLabel.Parent = parent + + return textLabel +end + +local VehicleHudFrame = Instance.new('Frame') +VehicleHudFrame.Name = "VehicleHudFrame" +VehicleHudFrame.Size = UDim2.new(0, (isTenFootInterface and 316 or 158), 0, (isTenFootInterface and 50 or 14)) +VehicleHudFrame.Position = UDim2.new(0.5, -(VehicleHudFrame.Size.X.Offset/2), 1, -BOTTOM_OFFSET - VehicleHudFrame.Size.Y.Offset) +VehicleHudFrame.BackgroundTransparency = 1 +VehicleHudFrame.Visible = false +VehicleHudFrame.Parent = RobloxGui + +local speedBarClippingFrame = Instance.new("Frame") +speedBarClippingFrame.Name = "SpeedBarClippingFrame" +speedBarClippingFrame.Size = UDim2.new(0, 0, 0, (isTenFootInterface and 24 or 4)) +speedBarClippingFrame.Position = UDim2.new(0.5, (isTenFootInterface and -142 or -71), 0.5, (isTenFootInterface and -13 or -2)) +speedBarClippingFrame.BackgroundTransparency = 1 +speedBarClippingFrame.ClipsDescendants = true +speedBarClippingFrame.Parent = VehicleHudFrame + +local HudBG = createImageLabel("HudBG", UDim2.new(1, 0, 1, 0), UDim2.new(0, 0, 0, 1), VEHICLE_HUD_BG, VehicleHudFrame) +local SpeedBG = createImageLabel("SpeedBG", UDim2.new(0, (isTenFootInterface and 284 or 142), 0, (isTenFootInterface and 24 or 4)), UDim2.new(0.5, (isTenFootInterface and -142 or -71), 0.5, (isTenFootInterface and -13 or -2)), SPEED_BAR_EMPTY, VehicleHudFrame) +local SpeedBarImage = createImageLabel("SpeedBarImage", UDim2.new(0, (isTenFootInterface and 284 or 142), 1, 0), UDim2.new(0, 0, 0, 0), SPEED_BAR, speedBarClippingFrame) +SpeedBarImage.ZIndex = 2 + +local SpeedLabel = createTextLabel("SpeedLabel", Enum.TextXAlignment.Left, "Speed", VehicleHudFrame) +local SpeedText = createTextLabel("SpeedText", Enum.TextXAlignment.Right, "0", VehicleHudFrame) + +--[[ Local Functions ]]-- +local function getHumanoid() + local character = LocalPlayer and LocalPlayer.Character + if character then + for _,child in pairs(character:GetChildren()) do + if child:IsA('Humanoid') then + return child + end + end + end +end + +local function onHeartbeat() + if CurrentVehicleSeat then + local speed = CurrentVehicleSeat.Velocity.magnitude + SpeedText.Text = tostring(math.min(math.floor(speed), 9999)) + local drawSize = math.floor((speed / CurrentVehicleSeat.MaxSpeed) * SpeedBG.Size.X.Offset) + drawSize = math.min(drawSize, SpeedBG.Size.X.Offset) + speedBarClippingFrame.Size = UDim2.new(0, drawSize, 0, (isTenFootInterface and 24 or 4)) + end +end + +local function onVehicleSeatChanged(property) + if property == "HeadsUpDisplay" then + VehicleHudFrame.Visible = not VehicleHudFrame.Visible + end +end + +local function onSeated(active, currentSeatPart) + if active then + if currentSeatPart and currentSeatPart:IsA('VehicleSeat') then + CurrentVehicleSeat = currentSeatPart + VehicleHudFrame.Visible = CurrentVehicleSeat.HeadsUpDisplay + VehicleSeatHeartbeatCn = RunService.Heartbeat:connect(onHeartbeat) + VehicleSeatHUDChangedCn = CurrentVehicleSeat.Changed:connect(onVehicleSeatChanged) + end + else + if CurrentVehicleSeat then + VehicleHudFrame.Visible = false + CurrentVehicleSeat = nil + if VehicleSeatHeartbeatCn then + VehicleSeatHeartbeatCn:disconnect() + VehicleSeatHeartbeatCn = nil + end + if VehicleSeatHUDChangedCn then + VehicleSeatHUDChangedCn:disconnect() + VehicleSeatHUDChangedCn = nil + end + end + end +end + +local function connectSeated() + local humanoid = getHumanoid() + while not humanoid do + wait() + humanoid = getHumanoid() + end + humanoid.Seated:connect(onSeated) +end +if LocalPlayer.Character then + connectSeated() +end +LocalPlayer.CharacterAdded:connect(function(character) + onSeated(false) + connectSeated() +end) diff --git a/Client2018/content/scripts/CoreScripts/Libraries/RbxGui.lua b/Client2018/content/scripts/CoreScripts/Libraries/RbxGui.lua new file mode 100644 index 0000000..73da517 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Libraries/RbxGui.lua @@ -0,0 +1,4206 @@ +local t = {} + +local function ScopedConnect(parentInstance, instance, event, signalFunc, syncFunc, removeFunc) + local eventConnection = nil + + --Connection on parentInstance is scoped by parentInstance (when destroyed, it goes away) + local tryConnect = function() + if game:IsAncestorOf(parentInstance) then + --Entering the world, make sure we are connected/synced + if not eventConnection then + eventConnection = instance[event]:connect(signalFunc) + if syncFunc then syncFunc() end + end + else + --Probably leaving the world, so disconnect for now + if eventConnection then + eventConnection:disconnect() + if removeFunc then removeFunc() end + end + end + end + + --Hook it up to ancestryChanged signal + local connection = parentInstance.AncestryChanged:connect(tryConnect) + + --Now connect us if we're already in the world + tryConnect() + + return connection +end + +local function getLayerCollectorAncestor(instance) + local localInstance = instance + while localInstance and not localInstance:IsA("LayerCollector") do + localInstance = localInstance.Parent + end + return localInstance +end + +local function CreateButtons(frame, buttons, yPos, ySize) + local buttonNum = 1 + local buttonObjs = {} + for i, obj in ipairs(buttons) do + local button = Instance.new("TextButton") + button.Name = "Button" .. buttonNum + button.Font = Enum.Font.Arial + button.FontSize = Enum.FontSize.Size18 + button.AutoButtonColor = true + button.Modal = true + if obj["Style"] then + button.Style = obj.Style + else + button.Style = Enum.ButtonStyle.RobloxButton + end + if obj["ZIndex"] then + button.ZIndex = obj.ZIndex + end + button.Text = obj.Text + button.TextColor3 = Color3.new(1,1,1) + button.MouseButton1Click:connect(obj.Function) + button.Parent = frame + buttonObjs[buttonNum] = button + + buttonNum = buttonNum + 1 + end + local numButtons = buttonNum-1 + + if numButtons == 1 then + frame.Button1.Position = UDim2.new(0.35, 0, yPos.Scale, yPos.Offset) + frame.Button1.Size = UDim2.new(.4,0,ySize.Scale, ySize.Offset) + elseif numButtons == 2 then + frame.Button1.Position = UDim2.new(0.1, 0, yPos.Scale, yPos.Offset) + frame.Button1.Size = UDim2.new(.8/3,0, ySize.Scale, ySize.Offset) + + frame.Button2.Position = UDim2.new(0.55, 0, yPos.Scale, yPos.Offset) + frame.Button2.Size = UDim2.new(.35,0, ySize.Scale, ySize.Offset) + elseif numButtons >= 3 then + local spacing = .1 / numButtons + local buttonSize = .9 / numButtons + + buttonNum = 1 + while buttonNum <= numButtons do + buttonObjs[buttonNum].Position = UDim2.new(spacing*buttonNum + (buttonNum-1) * buttonSize, 0, yPos.Scale, yPos.Offset) + buttonObjs[buttonNum].Size = UDim2.new(buttonSize, 0, ySize.Scale, ySize.Offset) + buttonNum = buttonNum + 1 + end + end +end + +local function setSliderPos(newAbsPosX,slider,sliderPosition,bar,steps) + + local newStep = steps - 1 --otherwise we really get one more step than we want + local relativePosX = math.min(1, math.max(0, (newAbsPosX - bar.AbsolutePosition.X) / bar.AbsoluteSize.X )) + local wholeNum, remainder = math.modf(relativePosX * newStep) + if remainder > 0.5 then + wholeNum = wholeNum + 1 + end + relativePosX = wholeNum/newStep + + local result = math.ceil(relativePosX * newStep) + if sliderPosition.Value ~= (result + 1) then --only update if we moved a step + sliderPosition.Value = result + 1 + slider.Position = UDim2.new(relativePosX,-slider.AbsoluteSize.X/2,slider.Position.Y.Scale,slider.Position.Y.Offset) + end + +end + +local function cancelSlide(areaSoak) + areaSoak.Visible = false +end + +t.CreateStyledMessageDialog = function(title, message, style, buttons) + local frame = Instance.new("Frame") + frame.Size = UDim2.new(0.5, 0, 0, 165) + frame.Position = UDim2.new(0.25, 0, 0.5, -72.5) + frame.Name = "MessageDialog" + frame.Active = true + frame.Style = Enum.FrameStyle.RobloxRound + + local styleImage = Instance.new("ImageLabel") + styleImage.Name = "StyleImage" + styleImage.BackgroundTransparency = 1 + styleImage.Position = UDim2.new(0,5,0,15) + if style == "error" or style == "Error" then + styleImage.Size = UDim2.new(0, 71, 0, 71) + styleImage.Image = "https://www.roblox.com/asset/?id=42565285" + elseif style == "notify" or style == "Notify" then + styleImage.Size = UDim2.new(0, 71, 0, 71) + styleImage.Image = "https://www.roblox.com/asset/?id=42604978" + elseif style == "confirm" or style == "Confirm" then + styleImage.Size = UDim2.new(0, 74, 0, 76) + styleImage.Image = "https://www.roblox.com/asset/?id=42557901" + else + return t.CreateMessageDialog(title,message,buttons) + end + styleImage.Parent = frame + + local titleLabel = Instance.new("TextLabel") + titleLabel.Name = "Title" + titleLabel.Text = title + titleLabel.TextStrokeTransparency = 0 + titleLabel.BackgroundTransparency = 1 + titleLabel.TextColor3 = Color3.new(221/255,221/255,221/255) + titleLabel.Position = UDim2.new(0, 80, 0, 0) + titleLabel.Size = UDim2.new(1, -80, 0, 40) + titleLabel.Font = Enum.Font.ArialBold + titleLabel.FontSize = Enum.FontSize.Size36 + titleLabel.TextXAlignment = Enum.TextXAlignment.Center + titleLabel.TextYAlignment = Enum.TextYAlignment.Center + titleLabel.Parent = frame + + local messageLabel = Instance.new("TextLabel") + messageLabel.Name = "Message" + messageLabel.Text = message + messageLabel.TextStrokeTransparency = 0 + messageLabel.TextColor3 = Color3.new(221/255,221/255,221/255) + messageLabel.Position = UDim2.new(0.025, 80, 0, 45) + messageLabel.Size = UDim2.new(0.95, -80, 0, 55) + messageLabel.BackgroundTransparency = 1 + messageLabel.Font = Enum.Font.Arial + messageLabel.FontSize = Enum.FontSize.Size18 + messageLabel.TextWrap = true + messageLabel.TextXAlignment = Enum.TextXAlignment.Left + messageLabel.TextYAlignment = Enum.TextYAlignment.Top + messageLabel.Parent = frame + + CreateButtons(frame, buttons, UDim.new(0, 105), UDim.new(0, 40) ) + + return frame +end + +t.CreateMessageDialog = function(title, message, buttons) + local frame = Instance.new("Frame") + frame.Size = UDim2.new(0.5, 0, 0.5, 0) + frame.Position = UDim2.new(0.25, 0, 0.25, 0) + frame.Name = "MessageDialog" + frame.Active = true + frame.Style = Enum.FrameStyle.RobloxRound + + local titleLabel = Instance.new("TextLabel") + titleLabel.Name = "Title" + titleLabel.Text = title + titleLabel.BackgroundTransparency = 1 + titleLabel.TextColor3 = Color3.new(221/255,221/255,221/255) + titleLabel.Position = UDim2.new(0, 0, 0, 0) + titleLabel.Size = UDim2.new(1, 0, 0.15, 0) + titleLabel.Font = Enum.Font.ArialBold + titleLabel.FontSize = Enum.FontSize.Size36 + titleLabel.TextXAlignment = Enum.TextXAlignment.Center + titleLabel.TextYAlignment = Enum.TextYAlignment.Center + titleLabel.Parent = frame + + local messageLabel = Instance.new("TextLabel") + messageLabel.Name = "Message" + messageLabel.Text = message + messageLabel.TextColor3 = Color3.new(221/255,221/255,221/255) + messageLabel.Position = UDim2.new(0.025, 0, 0.175, 0) + messageLabel.Size = UDim2.new(0.95, 0, .55, 0) + messageLabel.BackgroundTransparency = 1 + messageLabel.Font = Enum.Font.Arial + messageLabel.FontSize = Enum.FontSize.Size18 + messageLabel.TextWrap = true + messageLabel.TextXAlignment = Enum.TextXAlignment.Left + messageLabel.TextYAlignment = Enum.TextYAlignment.Top + messageLabel.Parent = frame + + CreateButtons(frame, buttons, UDim.new(0.8,0), UDim.new(0.15, 0)) + + return frame +end + +-- written by jmargh +-- to be used for the new settings menu +t.CreateScrollingDropDownMenu = function(onSelectedCallback, size, position, baseZ) + local maxVisibleList = 6 + local baseZIndex = 0 + if type(baseZ) == 'number' then + baseZIndex = baseZ + end + + local dropDownMenu = {} + local currentList = nil + + local updateFunc = nil + local frame = Instance.new('Frame') + frame.Name = "DropDownMenuFrame" + frame.Size = size + frame.Position = position + frame.BackgroundTransparency = 1 + dropDownMenu.Frame = frame + + local currentSelectionName = Instance.new('TextButton') + currentSelectionName.Name = "CurrentSelectionName" + currentSelectionName.Size = UDim2.new(1, 0, 1, 0) + currentSelectionName.BackgroundTransparency = 1 + currentSelectionName.Font = Enum.Font.SourceSansBold + currentSelectionName.FontSize = Enum.FontSize.Size18 + currentSelectionName.TextXAlignment = Enum.TextXAlignment.Left + currentSelectionName.TextYAlignment = Enum.TextYAlignment.Center + currentSelectionName.TextColor3 = Color3.new(0.5, 0.5, 0.5) + currentSelectionName.TextWrap = true + currentSelectionName.ZIndex = baseZIndex + currentSelectionName.Style = Enum.ButtonStyle.RobloxRoundDropdownButton + currentSelectionName.Text = "Choose One" + currentSelectionName.Parent = frame + dropDownMenu.CurrentSelectionButton = currentSelectionName + + local icon = Instance.new('ImageLabel') + icon.Name = "DropDownIcon" + icon.Size = UDim2.new(0, 16, 0, 12) + icon.Position = UDim2.new(1, -17, 0.5, -6) + icon.Image = 'rbxasset://textures/ui/dropdown_arrow.png' + icon.BackgroundTransparency = 1 + icon.ZIndex = baseZIndex + icon.Parent = currentSelectionName + + local listMenu = nil + local scrollingBackground = nil + local visibleCount = 0 + local isOpen = false + + local function onEntrySelected() + icon.Rotation = 0 + scrollingBackground:TweenSize(UDim2.new(1, 0, 0, currentSelectionName.AbsoluteSize.y), Enum.EasingDirection.InOut, Enum.EasingStyle.Sine, 0.15, true) + -- + listMenu.ScrollBarThickness = 0 + listMenu:TweenSize(UDim2.new(1, -16, 0, 24), Enum.EasingDirection.InOut, Enum.EasingStyle.Sine, 0.15, true, function() + if not isOpen then + listMenu.Visible = false + scrollingBackground.Visible = false + end + end) + isOpen = false + end + + currentSelectionName.MouseButton1Click:connect(function() + if not currentSelectionName.Active or #currentList == 0 then return end + if isOpen then + onEntrySelected() + return + end + -- + isOpen = true + icon.Rotation = 180 + if listMenu then listMenu.Visible = true end + if scrollingBackground then scrollingBackground.Visible = true end + -- + if scrollingBackground then + scrollingBackground:TweenSize(UDim2.new(1, 0, 0, visibleCount * 24 + 8), Enum.EasingDirection.InOut, Enum.EasingStyle.Sine, 0.15, true) + end + if listMenu then + listMenu:TweenSize(UDim2.new(1, -16, 0, visibleCount * 24), Enum.EasingDirection.InOut, Enum.EasingStyle.Sine, 0.15, true, function() + listMenu.ScrollBarThickness = 6 + end) + end + end) + + --[[ Public API ]]-- + dropDownMenu.IsOpen = function() + return isOpen + end + + dropDownMenu.Close = function() + onEntrySelected() + end + + dropDownMenu.Reset = function() + isOpen = false + icon.Rotation = 0 + listMenu.ScrollBarThickness = 0 + listMenu.Size = UDim2.new(1, -16, 0, 24) + listMenu.Visible = false + scrollingBackground.Visible = false + end + + dropDownMenu.SetVisible = function(isVisible) + if frame then + frame.Visible = isVisible + end + end + + dropDownMenu.UpdateZIndex = function(newZIndexBase) + currentSelectionName.ZIndex = newZIndexBase + icon.ZIndex = newZIndexBase + if scrollingBackground then scrollingBackground.ZIndex = newZIndexBase + 1 end + if listMenu then + listMenu.ZIndex = newZIndexBase + 2 + for _,child in pairs(listMenu:GetChildren()) do + child.ZIndex = newZIndexBase + 4 + end + end + end + + dropDownMenu.SetActive = function(isActive) + currentSelectionName.Active = isActive + end + + dropDownMenu.SetSelectionText = function(text) + currentSelectionName.Text = text + end + + dropDownMenu.CreateList = function(list) + currentSelectionName.Text = "Choose One" + if listMenu then listMenu:Destroy() end + if scrollingBackground then scrollingBackground:Destroy() end + -- + currentList = list + local length = #list + visibleCount = math.min(maxVisibleList, length) + local listMenuOffset = visibleCount * 24 + + listMenu = Instance.new('ScrollingFrame') + listMenu.Name = "ListMenu" + listMenu.Size = UDim2.new(1, -16, 0, 24) + listMenu.Position = UDim2.new(0, 12, 0, 32) + listMenu.CanvasSize = UDim2.new(0, 0, 0, length * 24) + listMenu.BackgroundTransparency = 1 + listMenu.BorderSizePixel = 0 + listMenu.ZIndex = baseZIndex + 2 + listMenu.Visible = false + listMenu.Active = true + listMenu.BottomImage = 'rbxasset://textures/ui/scroll-bottom.png' + listMenu.MidImage = 'rbxasset://textures/ui/scroll-middle.png' + listMenu.TopImage = 'rbxasset://textures/ui/scroll-top.png' + listMenu.ScrollBarThickness = 0 + listMenu.Parent = frame + + scrollingBackground = Instance.new('TextButton') + scrollingBackground.Name = "ScrollingBackground" + scrollingBackground.Size = UDim2.new(1, 0, 0, currentSelectionName.AbsoluteSize.y) + scrollingBackground.Position = UDim2.new(0, 0, 0, 28) + scrollingBackground.BackgroundColor3 = Color3.new(1, 1, 1) + scrollingBackground.Style = Enum.ButtonStyle.RobloxRoundDropdownButton + scrollingBackground.ZIndex = baseZIndex + 1 + scrollingBackground.Text = "" + scrollingBackground.Visible = false + scrollingBackground.AutoButtonColor = false + scrollingBackground.Parent = frame + + for i = 1, length do + local entry = list[i] + local btn = Instance.new('TextButton') + btn.Name = entry + btn.Size = UDim2.new(1, 0, 0, 24) + btn.Position = UDim2.new(0, 0, 0, (i - 1) * 24) + btn.BackgroundTransparency = 0 + btn.BackgroundColor3 = Color3.new(1, 1, 1) + btn.BorderSizePixel = 0 + btn.Font = Enum.Font.SourceSans + btn.FontSize = Enum.FontSize.Size18 + btn.TextColor3 = Color3.new(0.5, 0.5, 0.5) + btn.TextXAlignment = Enum.TextXAlignment.Left + btn.TextYAlignment = Enum.TextYAlignment.Center + btn.Text = entry + btn.ZIndex = baseZIndex + 4 + btn.AutoButtonColor = false + btn.Parent = listMenu + + btn.MouseButton1Click:connect(function() + currentSelectionName.Text = btn.Text + onEntrySelected() + btn.Font = Enum.Font.SourceSans + btn.TextColor3 = Color3.new(0.5, 0.5, 0.5) + btn.BackgroundColor3 = Color3.new(1, 1, 1) + onSelectedCallback(btn.Text) + end) + + btn.MouseEnter:connect(function() + btn.TextColor3 = Color3.new(1, 1, 1) + btn.BackgroundColor3 = Color3.new(0.75, 0.75, 0.75) + end) + btn.MouseLeave:connect(function() + btn.TextColor3 = Color3.new(0.5, 0.5, 0.5) + btn.BackgroundColor3 = Color3.new(1, 1, 1) + end) + end + end + + return dropDownMenu +end + +t.CreateDropDownMenu = function(items, onSelect, forRoblox, whiteSkin, baseZ) + local baseZIndex = 0 + if (type(baseZ) == "number") then + baseZIndex = baseZ + end + local width = UDim.new(0, 100) + local height = UDim.new(0, 32) + + local xPos = 0.055 + local frame = Instance.new("Frame") + local textColor = Color3.new(1,1,1) + if (whiteSkin) then + textColor = Color3.new(0.5, 0.5, 0.5) + end + frame.Name = "DropDownMenu" + frame.BackgroundTransparency = 1 + frame.Size = UDim2.new(width, height) + + local dropDownMenu = Instance.new("TextButton") + dropDownMenu.Name = "DropDownMenuButton" + dropDownMenu.TextWrap = true + dropDownMenu.TextColor3 = textColor + dropDownMenu.Text = "Choose One" + dropDownMenu.Font = Enum.Font.ArialBold + dropDownMenu.FontSize = Enum.FontSize.Size18 + dropDownMenu.TextXAlignment = Enum.TextXAlignment.Left + dropDownMenu.TextYAlignment = Enum.TextYAlignment.Center + dropDownMenu.BackgroundTransparency = 1 + dropDownMenu.AutoButtonColor = true + if (whiteSkin) then + dropDownMenu.Style = Enum.ButtonStyle.RobloxRoundDropdownButton + else + dropDownMenu.Style = Enum.ButtonStyle.RobloxButton + end + dropDownMenu.Size = UDim2.new(1,0,1,0) + dropDownMenu.Parent = frame + dropDownMenu.ZIndex = 2 + baseZIndex + + local dropDownIcon = Instance.new("ImageLabel") + dropDownIcon.Name = "Icon" + dropDownIcon.Active = false + if (whiteSkin) then + dropDownIcon.Image = "rbxasset://textures/ui/dropdown_arrow.png" + dropDownIcon.Size = UDim2.new(0,16,0,12) + dropDownIcon.Position = UDim2.new(1,-17,0.5, -6) + else + dropDownIcon.Image = "https://www.roblox.com/asset/?id=45732894" + dropDownIcon.Size = UDim2.new(0,11,0,6) + dropDownIcon.Position = UDim2.new(1,-11,0.5, -2) + end + dropDownIcon.BackgroundTransparency = 1 + dropDownIcon.Parent = dropDownMenu + dropDownIcon.ZIndex = 2 + baseZIndex + + local itemCount = #items + local dropDownItemCount = #items + local useScrollButtons = false + if dropDownItemCount > 6 then + useScrollButtons = true + dropDownItemCount = 6 + end + + local droppedDownMenu = Instance.new("TextButton") + droppedDownMenu.Name = "List" + droppedDownMenu.Text = "" + droppedDownMenu.BackgroundTransparency = 1 + --droppedDownMenu.AutoButtonColor = true + if (whiteSkin) then + droppedDownMenu.Style = Enum.ButtonStyle.RobloxRoundDropdownButton + else + droppedDownMenu.Style = Enum.ButtonStyle.RobloxButton + end + droppedDownMenu.Visible = false + droppedDownMenu.Active = true --Blocks clicks + droppedDownMenu.Position = UDim2.new(0,0,0,0) + droppedDownMenu.Size = UDim2.new(1,0, (1 + dropDownItemCount)*.8, 0) + droppedDownMenu.Parent = frame + droppedDownMenu.ZIndex = 2 + baseZIndex + + local choiceButton = Instance.new("TextButton") + choiceButton.Name = "ChoiceButton" + choiceButton.BackgroundTransparency = 1 + choiceButton.BorderSizePixel = 0 + choiceButton.Text = "ReplaceMe" + choiceButton.TextColor3 = textColor + choiceButton.TextXAlignment = Enum.TextXAlignment.Left + choiceButton.TextYAlignment = Enum.TextYAlignment.Center + choiceButton.BackgroundColor3 = Color3.new(1, 1, 1) + choiceButton.Font = Enum.Font.Arial + choiceButton.FontSize = Enum.FontSize.Size18 + if useScrollButtons then + choiceButton.Size = UDim2.new(1,-13, .8/((dropDownItemCount + 1)*.8),0) + else + choiceButton.Size = UDim2.new(1, 0, .8/((dropDownItemCount + 1)*.8),0) + end + choiceButton.TextWrap = true + choiceButton.ZIndex = 2 + baseZIndex + + local areaSoak = Instance.new("TextButton") + areaSoak.Name = "AreaSoak" + areaSoak.Text = "" + areaSoak.BackgroundTransparency = 1 + areaSoak.Active = true + areaSoak.Size = UDim2.new(1,0,1,0) + areaSoak.Visible = false + areaSoak.ZIndex = 3 + baseZIndex + + local dropDownSelected = false + + local scrollUpButton + local scrollDownButton + local scrollMouseCount = 0 + + local setZIndex = function(baseZIndex) + droppedDownMenu.ZIndex = baseZIndex +1 + if scrollUpButton then + scrollUpButton.ZIndex = baseZIndex + 3 + end + if scrollDownButton then + scrollDownButton.ZIndex = baseZIndex + 3 + end + + local children = droppedDownMenu:GetChildren() + if children then + for i, child in ipairs(children) do + if child.Name == "ChoiceButton" then + child.ZIndex = baseZIndex + 2 + elseif child.Name == "ClickCaptureButton" then + child.ZIndex = baseZIndex + end + end + end + end + + local scrollBarPosition = 1 + local updateScroll = function() + if scrollUpButton then + scrollUpButton.Active = scrollBarPosition > 1 + end + if scrollDownButton then + scrollDownButton.Active = scrollBarPosition + dropDownItemCount <= itemCount + end + + local children = droppedDownMenu:GetChildren() + if not children then return end + + local childNum = 1 + for i, obj in ipairs(children) do + if obj.Name == "ChoiceButton" then + if childNum < scrollBarPosition or childNum >= scrollBarPosition + dropDownItemCount then + obj.Visible = false + else + obj.Position = UDim2.new(0,0,((childNum-scrollBarPosition+1)*.8)/((dropDownItemCount+1)*.8),0) + obj.Visible = true + end + obj.TextColor3 = textColor + obj.BackgroundTransparency = 1 + + childNum = childNum + 1 + end + end + end + local toggleVisibility = function() + dropDownSelected = not dropDownSelected + + areaSoak.Visible = not areaSoak.Visible + dropDownMenu.Visible = not dropDownSelected + droppedDownMenu.Visible = dropDownSelected + if dropDownSelected then + setZIndex(4 + baseZIndex) + else + setZIndex(2 + baseZIndex) + end + if useScrollButtons then + updateScroll() + end + end + droppedDownMenu.MouseButton1Click:connect(toggleVisibility) + + local updateSelection = function(text) + local foundItem = false + local children = droppedDownMenu:GetChildren() + local childNum = 1 + if children then + for i, obj in ipairs(children) do + if obj.Name == "ChoiceButton" then + if obj.Text == text then + obj.Font = Enum.Font.ArialBold + foundItem = true + scrollBarPosition = childNum + if (whiteSkin) then + obj.TextColor3 = Color3.new(90/255,142/255,233/255) + end + else + obj.Font = Enum.Font.Arial + if (whiteSkin) then + obj.TextColor3 = textColor + end + end + childNum = childNum + 1 + end + end + end + if not text then + dropDownMenu.Text = "Choose One" + scrollBarPosition = 1 + else + if not foundItem then + error("Invalid Selection Update -- " .. text) + end + + if scrollBarPosition + dropDownItemCount > itemCount + 1 then + scrollBarPosition = itemCount - dropDownItemCount + 1 + end + + dropDownMenu.Text = text + end + end + + local function scrollDown() + if scrollBarPosition + dropDownItemCount <= itemCount then + scrollBarPosition = scrollBarPosition + 1 + updateScroll() + return true + end + return false + end + local function scrollUp() + if scrollBarPosition > 1 then + scrollBarPosition = scrollBarPosition - 1 + updateScroll() + return true + end + return false + end + + if useScrollButtons then + --Make some scroll buttons + scrollUpButton = Instance.new("ImageButton") + scrollUpButton.Name = "ScrollUpButton" + scrollUpButton.BackgroundTransparency = 1 + scrollUpButton.Image = "rbxasset://textures/ui/scrollbuttonUp.png" + scrollUpButton.Size = UDim2.new(0,17,0,17) + scrollUpButton.Position = UDim2.new(1,-11,(1*.8)/((dropDownItemCount+1)*.8),0) + scrollUpButton.MouseButton1Click:connect( + function() + scrollMouseCount = scrollMouseCount + 1 + end) + scrollUpButton.MouseLeave:connect( + function() + scrollMouseCount = scrollMouseCount + 1 + end) + scrollUpButton.MouseButton1Down:connect( + function() + scrollMouseCount = scrollMouseCount + 1 + + scrollUp() + local val = scrollMouseCount + wait(0.5) + while val == scrollMouseCount do + if scrollUp() == false then + break + end + wait(0.1) + end + end) + + scrollUpButton.Parent = droppedDownMenu + + scrollDownButton = Instance.new("ImageButton") + scrollDownButton.Name = "ScrollDownButton" + scrollDownButton.BackgroundTransparency = 1 + scrollDownButton.Image = "rbxasset://textures/ui/scrollbuttonDown.png" + scrollDownButton.Size = UDim2.new(0,17,0,17) + scrollDownButton.Position = UDim2.new(1,-11,1,-11) + scrollDownButton.Parent = droppedDownMenu + scrollDownButton.MouseButton1Click:connect( + function() + scrollMouseCount = scrollMouseCount + 1 + end) + scrollDownButton.MouseLeave:connect( + function() + scrollMouseCount = scrollMouseCount + 1 + end) + scrollDownButton.MouseButton1Down:connect( + function() + scrollMouseCount = scrollMouseCount + 1 + + scrollDown() + local val = scrollMouseCount + wait(0.5) + while val == scrollMouseCount do + if scrollDown() == false then + break + end + wait(0.1) + end + end) + + local scrollbar = Instance.new("ImageLabel") + scrollbar.Name = "ScrollBar" + scrollbar.Image = "rbxasset://textures/ui/scrollbar.png" + scrollbar.BackgroundTransparency = 1 + scrollbar.Size = UDim2.new(0, 18, (dropDownItemCount*.8)/((dropDownItemCount+1)*.8), -(17) - 11 - 4) + scrollbar.Position = UDim2.new(1,-11,(1*.8)/((dropDownItemCount+1)*.8),17+2) + scrollbar.Parent = droppedDownMenu + end + + for i,item in ipairs(items) do + -- needed to maintain local scope for items in event listeners below + local button = choiceButton:clone() + if forRoblox then + button.RobloxLocked = true + end + button.Text = item + button.Parent = droppedDownMenu + if (whiteSkin) then + button.TextColor3 = textColor + end + + button.MouseButton1Click:connect(function() + --Remove Highlight + if (not whiteSkin) then + button.TextColor3 = Color3.new(1,1,1) + end + button.BackgroundTransparency = 1 + + updateSelection(item) + onSelect(item) + + toggleVisibility() + end) + button.MouseEnter:connect(function() + --Add Highlight + if (not whiteSkin) then + button.TextColor3 = Color3.new(0,0,0) + end + button.BackgroundTransparency = 0 + end) + + button.MouseLeave:connect(function() + --Remove Highlight + if (not whiteSkin) then + button.TextColor3 = Color3.new(1,1,1) + end + button.BackgroundTransparency = 1 + end) + end + + --This does the initial layout of the buttons + updateScroll() + + frame.AncestryChanged:connect(function(child,parent) + if parent == nil then + areaSoak.Parent = nil + else + areaSoak.Parent = getLayerCollectorAncestor(frame) + end + end) + + dropDownMenu.MouseButton1Click:connect(toggleVisibility) + areaSoak.MouseButton1Click:connect(toggleVisibility) + return frame, updateSelection +end + +t.CreatePropertyDropDownMenu = function(instance, property, enum) + + local items = enum:GetEnumItems() + local names = {} + local nameToItem = {} + for i,obj in ipairs(items) do + names[i] = obj.Name + nameToItem[obj.Name] = obj + end + + local frame + local updateSelection + frame, updateSelection = t.CreateDropDownMenu(names, function(text) instance[property] = nameToItem[text] end) + + ScopedConnect(frame, instance, "Changed", + function(prop) + if prop == property then + updateSelection(instance[property].Name) + end + end, + function() + updateSelection(instance[property].Name) + end) + + return frame +end + +t.GetFontHeight = function(font, fontSize) + if font == nil or fontSize == nil then + error("Font and FontSize must be non-nil") + end + + local fontSizeInt = tonumber(fontSize.Name:match("%d+")) -- Clever hack to extract the size from the enum itself. + + if font == Enum.Font.Legacy then -- Legacy has a 50% bigger size. + return math.ceil(fontSizeInt*1.5) + else -- Size is literally just the fontSizeInt + return fontSizeInt + end +end + +local function layoutGuiObjectsHelper(frame, guiObjects, settingsTable) + local totalPixels = frame.AbsoluteSize.Y + local pixelsRemaining = frame.AbsoluteSize.Y + for i, child in ipairs(guiObjects) do + if child:IsA("TextLabel") or child:IsA("TextButton") then + local isLabel = child:IsA("TextLabel") + if isLabel then + pixelsRemaining = pixelsRemaining - settingsTable["TextLabelPositionPadY"] + else + pixelsRemaining = pixelsRemaining - settingsTable["TextButtonPositionPadY"] + end + child.Position = UDim2.new(child.Position.X.Scale, child.Position.X.Offset, 0, totalPixels - pixelsRemaining) + child.Size = UDim2.new(child.Size.X.Scale, child.Size.X.Offset, 0, pixelsRemaining) + + if child.TextFits and child.TextBounds.Y < pixelsRemaining then + child.Visible = true + if isLabel then + child.Size = UDim2.new(child.Size.X.Scale, child.Size.X.Offset, 0, child.TextBounds.Y + settingsTable["TextLabelSizePadY"]) + else + child.Size = UDim2.new(child.Size.X.Scale, child.Size.X.Offset, 0, child.TextBounds.Y + settingsTable["TextButtonSizePadY"]) + end + + while not child.TextFits do + child.Size = UDim2.new(child.Size.X.Scale, child.Size.X.Offset, 0, child.AbsoluteSize.Y + 1) + end + pixelsRemaining = pixelsRemaining - child.AbsoluteSize.Y + + if isLabel then + pixelsRemaining = pixelsRemaining - settingsTable["TextLabelPositionPadY"] + else + pixelsRemaining = pixelsRemaining - settingsTable["TextButtonPositionPadY"] + end + else + child.Visible = false + pixelsRemaining = -1 + end + + else + --GuiObject + child.Position = UDim2.new(child.Position.X.Scale, child.Position.X.Offset, 0, totalPixels - pixelsRemaining) + pixelsRemaining = pixelsRemaining - child.AbsoluteSize.Y + child.Visible = (pixelsRemaining >= 0) + end + end +end + +t.LayoutGuiObjects = function(frame, guiObjects, settingsTable) + if not frame:IsA("GuiObject") then + error("Frame must be a GuiObject") + end + for i, child in ipairs(guiObjects) do + if not child:IsA("GuiObject") then + error("All elements that are layed out must be of type GuiObject") + end + end + + if not settingsTable then + settingsTable = {} + end + + if not settingsTable["TextLabelSizePadY"] then + settingsTable["TextLabelSizePadY"] = 0 + end + if not settingsTable["TextLabelPositionPadY"] then + settingsTable["TextLabelPositionPadY"] = 0 + end + if not settingsTable["TextButtonSizePadY"] then + settingsTable["TextButtonSizePadY"] = 12 + end + if not settingsTable["TextButtonPositionPadY"] then + settingsTable["TextButtonPositionPadY"] = 2 + end + + --Wrapper frame takes care of styled objects + local wrapperFrame = Instance.new("Frame") + wrapperFrame.Name = "WrapperFrame" + wrapperFrame.BackgroundTransparency = 1 + wrapperFrame.Size = UDim2.new(1,0,1,0) + wrapperFrame.Parent = frame + + for i, child in ipairs(guiObjects) do + child.Parent = wrapperFrame + end + + local recalculate = function() + wait() + layoutGuiObjectsHelper(wrapperFrame, guiObjects, settingsTable) + end + + frame.Changed:connect( + function(prop) + if prop == "AbsoluteSize" then + --Wait a heartbeat for it to sync in + recalculate(nil) + end + end) + frame.AncestryChanged:connect(recalculate) + + layoutGuiObjectsHelper(wrapperFrame, guiObjects, settingsTable) +end + + +t.CreateSlider = function(steps,width,position) + local sliderGui = Instance.new("Frame") + sliderGui.Size = UDim2.new(1,0,1,0) + sliderGui.BackgroundTransparency = 1 + sliderGui.Name = "SliderGui" + + local sliderSteps = Instance.new("IntValue") + sliderSteps.Name = "SliderSteps" + sliderSteps.Value = steps + sliderSteps.Parent = sliderGui + + local areaSoak = Instance.new("TextButton") + areaSoak.Name = "AreaSoak" + areaSoak.Text = "" + areaSoak.BackgroundTransparency = 1 + areaSoak.Active = false + areaSoak.Size = UDim2.new(1,0,1,0) + areaSoak.Visible = false + areaSoak.ZIndex = 4 + + sliderGui.AncestryChanged:connect(function(child,parent) + if parent == nil then + areaSoak.Parent = nil + else + areaSoak.Parent = getLayerCollectorAncestor(sliderGui) + end + end) + + local sliderPosition = Instance.new("IntValue") + sliderPosition.Name = "SliderPosition" + sliderPosition.Value = 0 + sliderPosition.Parent = sliderGui + + local id = math.random(1,100) + + local bar = Instance.new("TextButton") + bar.Text = "" + bar.AutoButtonColor = false + bar.Name = "Bar" + bar.BackgroundColor3 = Color3.new(0,0,0) + if type(width) == "number" then + bar.Size = UDim2.new(0,width,0,5) + else + bar.Size = UDim2.new(0,200,0,5) + end + bar.BorderColor3 = Color3.new(95/255,95/255,95/255) + bar.ZIndex = 2 + bar.Parent = sliderGui + + if position["X"] and position["X"]["Scale"] and position["X"]["Offset"] and position["Y"] and position["Y"]["Scale"] and position["Y"]["Offset"] then + bar.Position = position + end + + local slider = Instance.new("ImageButton") + slider.Name = "Slider" + slider.BackgroundTransparency = 1 + slider.Image = "rbxasset://textures/ui/Slider.png" + slider.Position = UDim2.new(0,0,0.5,-10) + slider.Size = UDim2.new(0,20,0,20) + slider.ZIndex = 3 + slider.Parent = bar + + local areaSoakMouseMoveCon = nil + + areaSoak.MouseLeave:connect(function() + if areaSoak.Visible then + cancelSlide(areaSoak) + end + end) + areaSoak.MouseButton1Up:connect(function() + if areaSoak.Visible then + cancelSlide(areaSoak) + end + end) + + slider.MouseButton1Down:connect(function() + areaSoak.Visible = true + if areaSoakMouseMoveCon then areaSoakMouseMoveCon:disconnect() end + areaSoakMouseMoveCon = areaSoak.MouseMoved:connect(function(x,y) + setSliderPos(x,slider,sliderPosition,bar,steps) + end) + end) + + slider.MouseButton1Up:connect(function() cancelSlide(areaSoak) end) + + sliderPosition.Changed:connect(function(prop) + sliderPosition.Value = math.min(steps, math.max(1,sliderPosition.Value)) + local relativePosX = (sliderPosition.Value - 1) / (steps - 1) + slider.Position = UDim2.new(relativePosX,-slider.AbsoluteSize.X/2,slider.Position.Y.Scale,slider.Position.Y.Offset) + end) + + bar.MouseButton1Down:connect(function(x,y) + setSliderPos(x,slider,sliderPosition,bar,steps) + end) + + return sliderGui, sliderPosition, sliderSteps + +end + + + +t.CreateSliderNew = function(steps,width,position) + local sliderGui = Instance.new("Frame") + sliderGui.Size = UDim2.new(1,0,1,0) + sliderGui.BackgroundTransparency = 1 + sliderGui.Name = "SliderGui" + + local sliderSteps = Instance.new("IntValue") + sliderSteps.Name = "SliderSteps" + sliderSteps.Value = steps + sliderSteps.Parent = sliderGui + + local areaSoak = Instance.new("TextButton") + areaSoak.Name = "AreaSoak" + areaSoak.Text = "" + areaSoak.BackgroundTransparency = 1 + areaSoak.Active = false + areaSoak.Size = UDim2.new(1,0,1,0) + areaSoak.Visible = false + areaSoak.ZIndex = 6 + + sliderGui.AncestryChanged:connect(function(child,parent) + if parent == nil then + areaSoak.Parent = nil + else + areaSoak.Parent = getLayerCollectorAncestor(sliderGui) + end + end) + + local sliderPosition = Instance.new("IntValue") + sliderPosition.Name = "SliderPosition" + sliderPosition.Value = 0 + sliderPosition.Parent = sliderGui + + local id = math.random(1,100) + + local sliderBarImgHeight = 7 + local sliderBarCapImgWidth = 4 + + local bar = Instance.new("ImageButton") + bar.BackgroundTransparency = 1 + bar.Image = "rbxasset://textures/ui/Slider-BKG-Center.png" + bar.Name = "Bar" + local displayWidth = 200 + if type(width) == "number" then + bar.Size = UDim2.new(0,width - (sliderBarCapImgWidth * 2),0,sliderBarImgHeight) + displayWidth = width - (sliderBarCapImgWidth * 2) + else + bar.Size = UDim2.new(0,200,0,sliderBarImgHeight) + end + bar.ZIndex = 3 + bar.Parent = sliderGui + if position["X"] and position["X"]["Scale"] and position["X"]["Offset"] and position["Y"] and position["Y"]["Scale"] and position["Y"]["Offset"] then + bar.Position = position + end + + local barLeft = bar:clone() + barLeft.Name = "BarLeft" + barLeft.Image = "rbxasset://textures/ui/Slider-BKG-Left-Cap.png" + barLeft.Size = UDim2.new(0, sliderBarCapImgWidth, 0, sliderBarImgHeight) + barLeft.Position = UDim2.new(position.X.Scale, position.X.Offset - sliderBarCapImgWidth, position.Y.Scale, position.Y.Offset) + barLeft.Parent = sliderGui + barLeft.ZIndex = 3 + + local barRight = barLeft:clone() + barRight.Name = "BarRight" + barRight.Image = "rbxasset://textures/ui/Slider-BKG-Right-Cap.png" + barRight.Position = UDim2.new(position.X.Scale, position.X.Offset + displayWidth, position.Y.Scale, position.Y.Offset) + barRight.Parent = sliderGui + + local fillLeft = barLeft:clone() + fillLeft.Name = "FillLeft" + fillLeft.Image = "rbxasset://textures/ui/Slider-Fill-Left-Cap.png" + fillLeft.Parent = sliderGui + fillLeft.ZIndex = 4 + + local fill = fillLeft:clone() + fill.Name = "Fill" + fill.Image = "rbxasset://textures/ui/Slider-Fill-Center.png" + fill.Parent = bar + fill.ZIndex = 4 + fill.Position = UDim2.new(0, 0, 0, 0) + fill.Size = UDim2.new(0.5, 0, 1, 0) + + +-- bar.Visible = false + + local slider = Instance.new("ImageButton") + slider.Name = "Slider" + slider.BackgroundTransparency = 1 + slider.Image = "rbxasset://textures/ui/slider_new_tab.png" + slider.Position = UDim2.new(0,0,0.5,-14) + slider.Size = UDim2.new(0,28,0,28) + slider.ZIndex = 5 + slider.Parent = bar + + local areaSoakMouseMoveCon = nil + + areaSoak.MouseLeave:connect(function() + if areaSoak.Visible then + cancelSlide(areaSoak) + end + end) + areaSoak.MouseButton1Up:connect(function() + if areaSoak.Visible then + cancelSlide(areaSoak) + end + end) + + slider.MouseButton1Down:connect(function() + areaSoak.Visible = true + if areaSoakMouseMoveCon then areaSoakMouseMoveCon:disconnect() end + areaSoakMouseMoveCon = areaSoak.MouseMoved:connect(function(x,y) + setSliderPos(x,slider,sliderPosition,bar,steps) + end) + end) + + slider.MouseButton1Up:connect(function() cancelSlide(areaSoak) end) + + sliderPosition.Changed:connect(function(prop) + sliderPosition.Value = math.min(steps, math.max(1,sliderPosition.Value)) + local relativePosX = (sliderPosition.Value - 1) / (steps - 1) + slider.Position = UDim2.new(relativePosX,-slider.AbsoluteSize.X/2,slider.Position.Y.Scale,slider.Position.Y.Offset) + fill.Size = UDim2.new(relativePosX, 0, 1, 0) + end) + + bar.MouseButton1Down:connect(function(x,y) + setSliderPos(x,slider,sliderPosition,bar,steps) + end) + + fill.MouseButton1Down:connect(function(x,y) + setSliderPos(x,slider,sliderPosition,bar,steps) + end) + + fillLeft.MouseButton1Down:connect(function(x,y) + setSliderPos(x,slider,sliderPosition,bar,steps) + end) + + + return sliderGui, sliderPosition, sliderSteps + +end + + + + + +t.CreateTrueScrollingFrame = function() + local lowY = nil + local highY = nil + + local dragCon = nil + local upCon = nil + + local internalChange = false + + local descendantsChangeConMap = {} + + local scrollingFrame = Instance.new("Frame") + scrollingFrame.Name = "ScrollingFrame" + scrollingFrame.Active = true + scrollingFrame.Size = UDim2.new(1,0,1,0) + scrollingFrame.ClipsDescendants = true + + local controlFrame = Instance.new("Frame") + controlFrame.Name = "ControlFrame" + controlFrame.BackgroundTransparency = 1 + controlFrame.Size = UDim2.new(0,18,1,0) + controlFrame.Position = UDim2.new(1,-20,0,0) + controlFrame.Parent = scrollingFrame + + local scrollBottom = Instance.new("BoolValue") + scrollBottom.Value = false + scrollBottom.Name = "ScrollBottom" + scrollBottom.Parent = controlFrame + + local scrollUp = Instance.new("BoolValue") + scrollUp.Value = false + scrollUp.Name = "scrollUp" + scrollUp.Parent = controlFrame + + local scrollUpButton = Instance.new("TextButton") + scrollUpButton.Name = "ScrollUpButton" + scrollUpButton.Text = "" + scrollUpButton.AutoButtonColor = false + scrollUpButton.BackgroundColor3 = Color3.new(0,0,0) + scrollUpButton.BorderColor3 = Color3.new(1,1,1) + scrollUpButton.BackgroundTransparency = 0.5 + scrollUpButton.Size = UDim2.new(0,18,0,18) + scrollUpButton.ZIndex = 2 + scrollUpButton.Parent = controlFrame + for i = 1, 6 do + local triFrame = Instance.new("Frame") + triFrame.BorderColor3 = Color3.new(1,1,1) + triFrame.Name = "tri" .. tostring(i) + triFrame.ZIndex = 3 + triFrame.BackgroundTransparency = 0.5 + triFrame.Size = UDim2.new(0,12 - ((i -1) * 2),0,0) + triFrame.Position = UDim2.new(0,3 + (i -1),0.5,2 - (i -1)) + triFrame.Parent = scrollUpButton + end + scrollUpButton.MouseEnter:connect(function() + scrollUpButton.BackgroundTransparency = 0.1 + local upChildren = scrollUpButton:GetChildren() + for i = 1, #upChildren do + upChildren[i].BackgroundTransparency = 0.1 + end + end) + scrollUpButton.MouseLeave:connect(function() + scrollUpButton.BackgroundTransparency = 0.5 + local upChildren = scrollUpButton:GetChildren() + for i = 1, #upChildren do + upChildren[i].BackgroundTransparency = 0.5 + end + end) + + local scrollDownButton = scrollUpButton:clone() + scrollDownButton.Name = "ScrollDownButton" + scrollDownButton.Position = UDim2.new(0,0,1,-18) + local downChildren = scrollDownButton:GetChildren() + for i = 1, #downChildren do + downChildren[i].Position = UDim2.new(0,3 + (i -1),0.5,-2 + (i - 1)) + end + scrollDownButton.MouseEnter:connect(function() + scrollDownButton.BackgroundTransparency = 0.1 + local downChildren = scrollDownButton:GetChildren() + for i = 1, #downChildren do + downChildren[i].BackgroundTransparency = 0.1 + end + end) + scrollDownButton.MouseLeave:connect(function() + scrollDownButton.BackgroundTransparency = 0.5 + local downChildren = scrollDownButton:GetChildren() + for i = 1, #downChildren do + downChildren[i].BackgroundTransparency = 0.5 + end + end) + scrollDownButton.Parent = controlFrame + + local scrollTrack = Instance.new("Frame") + scrollTrack.Name = "ScrollTrack" + scrollTrack.BackgroundTransparency = 1 + scrollTrack.Size = UDim2.new(0,18,1,-38) + scrollTrack.Position = UDim2.new(0,0,0,19) + scrollTrack.Parent = controlFrame + + local scrollbar = Instance.new("TextButton") + scrollbar.BackgroundColor3 = Color3.new(0,0,0) + scrollbar.BorderColor3 = Color3.new(1,1,1) + scrollbar.BackgroundTransparency = 0.5 + scrollbar.AutoButtonColor = false + scrollbar.Text = "" + scrollbar.Active = true + scrollbar.Name = "ScrollBar" + scrollbar.ZIndex = 2 + scrollbar.BackgroundTransparency = 0.5 + scrollbar.Size = UDim2.new(0, 18, 0.1, 0) + scrollbar.Position = UDim2.new(0,0,0,0) + scrollbar.Parent = scrollTrack + + local scrollNub = Instance.new("Frame") + scrollNub.Name = "ScrollNub" + scrollNub.BorderColor3 = Color3.new(1,1,1) + scrollNub.Size = UDim2.new(0,10,0,0) + scrollNub.Position = UDim2.new(0.5,-5,0.5,0) + scrollNub.ZIndex = 2 + scrollNub.BackgroundTransparency = 0.5 + scrollNub.Parent = scrollbar + + local newNub = scrollNub:clone() + newNub.Position = UDim2.new(0.5,-5,0.5,-2) + newNub.Parent = scrollbar + + local lastNub = scrollNub:clone() + lastNub.Position = UDim2.new(0.5,-5,0.5,2) + lastNub.Parent = scrollbar + + scrollbar.MouseEnter:connect(function() + scrollbar.BackgroundTransparency = 0.1 + scrollNub.BackgroundTransparency = 0.1 + newNub.BackgroundTransparency = 0.1 + lastNub.BackgroundTransparency = 0.1 + end) + scrollbar.MouseLeave:connect(function() + scrollbar.BackgroundTransparency = 0.5 + scrollNub.BackgroundTransparency = 0.5 + newNub.BackgroundTransparency = 0.5 + lastNub.BackgroundTransparency = 0.5 + end) + + local mouseDrag = Instance.new("ImageButton") + mouseDrag.Active = false + mouseDrag.Size = UDim2.new(1.5, 0, 1.5, 0) + mouseDrag.AutoButtonColor = false + mouseDrag.BackgroundTransparency = 1 + mouseDrag.Name = "mouseDrag" + mouseDrag.Position = UDim2.new(-0.25, 0, -0.25, 0) + mouseDrag.ZIndex = 10 + + local function positionScrollBar(x,y,offset) + local oldPos = scrollbar.Position + + if y < scrollTrack.AbsolutePosition.y then + scrollbar.Position = UDim2.new(scrollbar.Position.X.Scale,scrollbar.Position.X.Offset,0,0) + return (oldPos ~= scrollbar.Position) + end + + local relativeSize = scrollbar.AbsoluteSize.Y/scrollTrack.AbsoluteSize.Y + + if y > (scrollTrack.AbsolutePosition.y + scrollTrack.AbsoluteSize.y) then + scrollbar.Position = UDim2.new(scrollbar.Position.X.Scale,scrollbar.Position.X.Offset,1 - relativeSize,0) + return (oldPos ~= scrollbar.Position) + end + local newScaleYPos = (y - scrollTrack.AbsolutePosition.y - offset)/scrollTrack.AbsoluteSize.y + if newScaleYPos + relativeSize > 1 then + newScaleYPos = 1 - relativeSize + scrollBottom.Value = true + scrollUp.Value = false + elseif newScaleYPos <= 0 then + newScaleYPos = 0 + scrollUp.Value = true + scrollBottom.Value = false + else + scrollUp.Value = false + scrollBottom.Value = false + end + scrollbar.Position = UDim2.new(scrollbar.Position.X.Scale,scrollbar.Position.X.Offset,newScaleYPos,0) + + return (oldPos ~= scrollbar.Position) + end + + local function drillDownSetHighLow(instance) + if not instance or not instance:IsA("GuiObject") then return end + if instance == controlFrame then return end + if instance:IsDescendantOf(controlFrame) then return end + if not instance.Visible then return end + + if lowY and lowY > instance.AbsolutePosition.Y then + lowY = instance.AbsolutePosition.Y + elseif not lowY then + lowY = instance.AbsolutePosition.Y + end + if highY and highY < (instance.AbsolutePosition.Y + instance.AbsoluteSize.Y) then + highY = instance.AbsolutePosition.Y + instance.AbsoluteSize.Y + elseif not highY then + highY = instance.AbsolutePosition.Y + instance.AbsoluteSize.Y + end + local children = instance:GetChildren() + for i = 1, #children do + drillDownSetHighLow(children[i]) + end + end + + local function resetHighLow() + local firstChildren = scrollingFrame:GetChildren() + + for i = 1, #firstChildren do + drillDownSetHighLow(firstChildren[i]) + end + end + + local function recalculate() + internalChange = true + + local percentFrame = 0 + if scrollbar.Position.Y.Scale > 0 then + if scrollbar.Visible then + percentFrame = scrollbar.Position.Y.Scale/((scrollTrack.AbsoluteSize.Y - scrollbar.AbsoluteSize.Y)/scrollTrack.AbsoluteSize.Y) + else + percentFrame = 0 + end + end + if percentFrame > 0.99 then percentFrame = 1 end + + local hiddenYAmount = (scrollingFrame.AbsoluteSize.Y - (highY - lowY)) * percentFrame + + local guiChildren = scrollingFrame:GetChildren() + for i = 1, #guiChildren do + if guiChildren[i] ~= controlFrame then + guiChildren[i].Position = UDim2.new(guiChildren[i].Position.X.Scale,guiChildren[i].Position.X.Offset, + 0, math.ceil(guiChildren[i].AbsolutePosition.Y) - math.ceil(lowY) + hiddenYAmount) + end + end + + lowY = nil + highY = nil + resetHighLow() + internalChange = false + end + + local function setSliderSizeAndPosition() + if not highY or not lowY then return end + + local totalYSpan = math.abs(highY - lowY) + if totalYSpan == 0 then + scrollbar.Visible = false + scrollDownButton.Visible = false + scrollUpButton.Visible = false + + if dragCon then dragCon:disconnect() dragCon = nil end + if upCon then upCon:disconnect() upCon = nil end + return + end + + local percentShown = scrollingFrame.AbsoluteSize.Y/totalYSpan + if percentShown >= 1 then + scrollbar.Visible = false + scrollDownButton.Visible = false + scrollUpButton.Visible = false + recalculate() + else + scrollbar.Visible = true + scrollDownButton.Visible = true + scrollUpButton.Visible = true + + scrollbar.Size = UDim2.new(scrollbar.Size.X.Scale,scrollbar.Size.X.Offset,percentShown,0) + end + + local percentPosition = (scrollingFrame.AbsolutePosition.Y - lowY)/totalYSpan + scrollbar.Position = UDim2.new(scrollbar.Position.X.Scale,scrollbar.Position.X.Offset,percentPosition,-scrollbar.AbsoluteSize.X/2) + + if scrollbar.AbsolutePosition.y < scrollTrack.AbsolutePosition.y then + scrollbar.Position = UDim2.new(scrollbar.Position.X.Scale,scrollbar.Position.X.Offset,0,0) + end + + if (scrollbar.AbsolutePosition.y + scrollbar.AbsoluteSize.Y) > (scrollTrack.AbsolutePosition.y + scrollTrack.AbsoluteSize.y) then + local relativeSize = scrollbar.AbsoluteSize.Y/scrollTrack.AbsoluteSize.Y + scrollbar.Position = UDim2.new(scrollbar.Position.X.Scale,scrollbar.Position.X.Offset,1 - relativeSize,0) + end + end + + local buttonScrollAmountPixels = 7 + local reentrancyGuardScrollUp = false + local function doScrollUp() + if reentrancyGuardScrollUp then return end + + reentrancyGuardScrollUp = true + if positionScrollBar(0,scrollbar.AbsolutePosition.Y - buttonScrollAmountPixels,0) then + recalculate() + end + reentrancyGuardScrollUp = false + end + + local reentrancyGuardScrollDown = false + local function doScrollDown() + if reentrancyGuardScrollDown then return end + + reentrancyGuardScrollDown = true + if positionScrollBar(0,scrollbar.AbsolutePosition.Y + buttonScrollAmountPixels,0) then + recalculate() + end + reentrancyGuardScrollDown = false + end + + local function scrollUp(mouseYPos) + if scrollUpButton.Active then + scrollStamp = tick() + local current = scrollStamp + local upCon + upCon = mouseDrag.MouseButton1Up:connect(function() + scrollStamp = tick() + mouseDrag.Parent = nil + upCon:disconnect() + end) + mouseDrag.Parent = getLayerCollectorAncestor(scrollbar) + doScrollUp() + wait(0.2) + local t = tick() + local w = 0.1 + while scrollStamp == current do + doScrollUp() + if mouseYPos and mouseYPos > scrollbar.AbsolutePosition.y then + break + end + if not scrollUpButton.Active then break end + if tick()-t > 5 then + w = 0 + elseif tick()-t > 2 then + w = 0.06 + end + wait(w) + end + end + end + + local function scrollDown(mouseYPos) + if scrollDownButton.Active then + scrollStamp = tick() + local current = scrollStamp + local downCon + downCon = mouseDrag.MouseButton1Up:connect(function() + scrollStamp = tick() + mouseDrag.Parent = nil + downCon:disconnect() + end) + mouseDrag.Parent = getLayerCollectorAncestor(scrollbar) + doScrollDown() + wait(0.2) + local t = tick() + local w = 0.1 + while scrollStamp == current do + doScrollDown() + if mouseYPos and mouseYPos < (scrollbar.AbsolutePosition.y + scrollbar.AbsoluteSize.x) then + break + end + if not scrollDownButton.Active then break end + if tick()-t > 5 then + w = 0 + elseif tick()-t > 2 then + w = 0.06 + end + wait(w) + end + end + end + + scrollbar.MouseButton1Down:connect(function(x,y) + if scrollbar.Active then + scrollStamp = tick() + local mouseOffset = y - scrollbar.AbsolutePosition.y + if dragCon then dragCon:disconnect() dragCon = nil end + if upCon then upCon:disconnect() upCon = nil end + local prevY = y + local reentrancyGuardMouseScroll = false + dragCon = mouseDrag.MouseMoved:connect(function(x,y) + if reentrancyGuardMouseScroll then return end + + reentrancyGuardMouseScroll = true + if positionScrollBar(x,y,mouseOffset) then + recalculate() + end + reentrancyGuardMouseScroll = false + + end) + upCon = mouseDrag.MouseButton1Up:connect(function() + scrollStamp = tick() + mouseDrag.Parent = nil + dragCon:disconnect(); dragCon = nil + upCon:disconnect(); drag = nil + end) + mouseDrag.Parent = getLayerCollectorAncestor(scrollbar) + end + end) + + local scrollMouseCount = 0 + + scrollUpButton.MouseButton1Down:connect(function() + scrollUp() + end) + scrollUpButton.MouseButton1Up:connect(function() + scrollStamp = tick() + end) + + scrollDownButton.MouseButton1Up:connect(function() + scrollStamp = tick() + end) + scrollDownButton.MouseButton1Down:connect(function() + scrollDown() + end) + + scrollbar.MouseButton1Up:connect(function() + scrollStamp = tick() + end) + + local function heightCheck(instance) + if highY and (instance.AbsolutePosition.Y + instance.AbsoluteSize.Y) > highY then + highY = instance.AbsolutePosition.Y + instance.AbsoluteSize.Y + elseif not highY then + highY = instance.AbsolutePosition.Y + instance.AbsoluteSize.Y + end + setSliderSizeAndPosition() + end + + local function highLowRecheck() + local oldLowY = lowY + local oldHighY = highY + lowY = nil + highY = nil + resetHighLow() + + if (lowY ~= oldLowY) or (highY ~= oldHighY) then + setSliderSizeAndPosition() + end + end + + local function descendantChanged(this, prop) + if internalChange then return end + if not this.Visible then return end + + if prop == "Size" or prop == "Position" then + wait() + highLowRecheck() + end + end + + scrollingFrame.DescendantAdded:connect(function(instance) + if not instance:IsA("GuiObject") then return end + + if instance.Visible then + wait() -- wait a heartbeat for sizes to reconfig + highLowRecheck() + end + + descendantsChangeConMap[instance] = instance.Changed:connect(function(prop) descendantChanged(instance, prop) end) + end) + + scrollingFrame.DescendantRemoving:connect(function(instance) + if not instance:IsA("GuiObject") then return end + if descendantsChangeConMap[instance] then + descendantsChangeConMap[instance]:disconnect() + descendantsChangeConMap[instance] = nil + end + wait() -- wait a heartbeat for sizes to reconfig + highLowRecheck() + end) + + scrollingFrame.Changed:connect(function(prop) + if prop == "AbsoluteSize" then + if not highY or not lowY then return end + + highLowRecheck() + setSliderSizeAndPosition() + end + end) + + return scrollingFrame, controlFrame +end + +t.CreateScrollingFrame = function(orderList,scrollStyle) + local frame = Instance.new("Frame") + frame.Name = "ScrollingFrame" + frame.BackgroundTransparency = 1 + frame.Size = UDim2.new(1,0,1,0) + + local scrollUpButton = Instance.new("ImageButton") + scrollUpButton.Name = "ScrollUpButton" + scrollUpButton.BackgroundTransparency = 1 + scrollUpButton.Image = "rbxasset://textures/ui/scrollbuttonUp.png" + scrollUpButton.Size = UDim2.new(0,17,0,17) + + + local scrollDownButton = Instance.new("ImageButton") + scrollDownButton.Name = "ScrollDownButton" + scrollDownButton.BackgroundTransparency = 1 + scrollDownButton.Image = "rbxasset://textures/ui/scrollbuttonDown.png" + scrollDownButton.Size = UDim2.new(0,17,0,17) + + local scrollbar = Instance.new("ImageButton") + scrollbar.Name = "ScrollBar" + scrollbar.Image = "rbxasset://textures/ui/scrollbar.png" + scrollbar.BackgroundTransparency = 1 + scrollbar.Size = UDim2.new(0, 18, 0, 150) + + local scrollStamp = 0 + + local scrollDrag = Instance.new("ImageButton") + scrollDrag.Image = "https://www.roblox.com/asset/?id=61367186" + scrollDrag.Size = UDim2.new(1, 0, 0, 16) + scrollDrag.BackgroundTransparency = 1 + scrollDrag.Name = "ScrollDrag" + scrollDrag.Active = true + scrollDrag.Parent = scrollbar + + local mouseDrag = Instance.new("ImageButton") + mouseDrag.Active = false + mouseDrag.Size = UDim2.new(1.5, 0, 1.5, 0) + mouseDrag.AutoButtonColor = false + mouseDrag.BackgroundTransparency = 1 + mouseDrag.Name = "mouseDrag" + mouseDrag.Position = UDim2.new(-0.25, 0, -0.25, 0) + mouseDrag.ZIndex = 10 + + local style = "simple" + if scrollStyle and tostring(scrollStyle) then + style = scrollStyle + end + + local scrollPosition = 1 + local rowSize = 0 + local howManyDisplayed = 0 + + local layoutGridScrollBar = function() + howManyDisplayed = 0 + local guiObjects = {} + if orderList then + for i, child in ipairs(orderList) do + if child.Parent == frame then + table.insert(guiObjects, child) + end + end + else + local children = frame:GetChildren() + if children then + for i, child in ipairs(children) do + if child:IsA("GuiObject") then + table.insert(guiObjects, child) + end + end + end + end + if #guiObjects == 0 then + scrollUpButton.Active = false + scrollDownButton.Active = false + scrollDrag.Active = false + scrollPosition = 1 + return + end + + if scrollPosition > #guiObjects then + scrollPosition = #guiObjects + end + + if scrollPosition < 1 then scrollPosition = 1 end + + local totalPixelsY = frame.AbsoluteSize.Y + local pixelsRemainingY = frame.AbsoluteSize.Y + + local totalPixelsX = frame.AbsoluteSize.X + + local xCounter = 0 + local rowSizeCounter = 0 + local setRowSize = true + + local pixelsBelowScrollbar = 0 + local pos = #guiObjects + + local currentRowY = 0 + + pos = scrollPosition + --count up from current scroll position to fill out grid + while pos <= #guiObjects and pixelsBelowScrollbar < totalPixelsY do + xCounter = xCounter + guiObjects[pos].AbsoluteSize.X + --previous pos was the end of a row + if xCounter >= totalPixelsX then + pixelsBelowScrollbar = pixelsBelowScrollbar + currentRowY + currentRowY = 0 + xCounter = guiObjects[pos].AbsoluteSize.X + end + if guiObjects[pos].AbsoluteSize.Y > currentRowY then + currentRowY = guiObjects[pos].AbsoluteSize.Y + end + pos = pos + 1 + end + --Count wherever current row left off + pixelsBelowScrollbar = pixelsBelowScrollbar + currentRowY + currentRowY = 0 + + pos = scrollPosition - 1 + xCounter = 0 + + --objects with varying X,Y dimensions can rarely cause minor errors + --rechecking every new scrollPosition is necessary to avoid 100% of errors + + --count backwards from current scrollPosition to see if we can add more rows + while pixelsBelowScrollbar + currentRowY < totalPixelsY and pos >= 1 do + xCounter = xCounter + guiObjects[pos].AbsoluteSize.X + rowSizeCounter = rowSizeCounter + 1 + if xCounter >= totalPixelsX then + rowSize = rowSizeCounter - 1 + rowSizeCounter = 0 + xCounter = guiObjects[pos].AbsoluteSize.X + if pixelsBelowScrollbar + currentRowY <= totalPixelsY then + --It fits, so back up our scroll position + pixelsBelowScrollbar = pixelsBelowScrollbar + currentRowY + if scrollPosition <= rowSize then + scrollPosition = 1 + break + else + scrollPosition = scrollPosition - rowSize + end + currentRowY = 0 + else + break + end + end + + if guiObjects[pos].AbsoluteSize.Y > currentRowY then + currentRowY = guiObjects[pos].AbsoluteSize.Y + end + + pos = pos - 1 + end + + --Do check last time if pos = 0 + if (pos == 0) and (pixelsBelowScrollbar + currentRowY <= totalPixelsY) then + scrollPosition = 1 + end + + xCounter = 0 + --pos = scrollPosition + rowSizeCounter = 0 + setRowSize = true + local lastChildSize = 0 + + local xOffset,yOffset = 0 + if guiObjects[1] then + yOffset = math.ceil(math.floor(math.fmod(totalPixelsY,guiObjects[1].AbsoluteSize.X))/2) + xOffset = math.ceil(math.floor(math.fmod(totalPixelsX,guiObjects[1].AbsoluteSize.Y))/2) + end + + for i, child in ipairs(guiObjects) do + if i < scrollPosition then + --print("Hiding " .. child.Name) + child.Visible = false + else + if pixelsRemainingY < 0 then + --print("Out of Space " .. child.Name) + child.Visible = false + else + --print("Laying out " .. child.Name) + --GuiObject + if setRowSize then rowSizeCounter = rowSizeCounter + 1 end + if xCounter + child.AbsoluteSize.X >= totalPixelsX then + if setRowSize then + rowSize = rowSizeCounter - 1 + setRowSize = false + end + xCounter = 0 + pixelsRemainingY = pixelsRemainingY - child.AbsoluteSize.Y + end + child.Position = UDim2.new(child.Position.X.Scale,xCounter + xOffset, 0, totalPixelsY - pixelsRemainingY + yOffset) + xCounter = xCounter + child.AbsoluteSize.X + child.Visible = ((pixelsRemainingY - child.AbsoluteSize.Y) >= 0) + if child.Visible then + howManyDisplayed = howManyDisplayed + 1 + end + lastChildSize = child.AbsoluteSize + end + end + end + + scrollUpButton.Active = (scrollPosition > 1) + if lastChildSize == 0 then + scrollDownButton.Active = false + else + scrollDownButton.Active = ((pixelsRemainingY - lastChildSize.Y) < 0) + end + scrollDrag.Active = #guiObjects > howManyDisplayed + scrollDrag.Visible = scrollDrag.Active + end + + + + local layoutSimpleScrollBar = function() + local guiObjects = {} + howManyDisplayed = 0 + + if orderList then + for i, child in ipairs(orderList) do + if child.Parent == frame then + table.insert(guiObjects, child) + end + end + else + local children = frame:GetChildren() + if children then + for i, child in ipairs(children) do + if child:IsA("GuiObject") then + table.insert(guiObjects, child) + end + end + end + end + if #guiObjects == 0 then + scrollUpButton.Active = false + scrollDownButton.Active = false + scrollDrag.Active = false + scrollPosition = 1 + return + end + + if scrollPosition > #guiObjects then + scrollPosition = #guiObjects + end + + local totalPixels = frame.AbsoluteSize.Y + local pixelsRemaining = frame.AbsoluteSize.Y + + local pixelsBelowScrollbar = 0 + local pos = #guiObjects + while pixelsBelowScrollbar < totalPixels and pos >= 1 do + if pos >= scrollPosition then + pixelsBelowScrollbar = pixelsBelowScrollbar + guiObjects[pos].AbsoluteSize.Y + else + if pixelsBelowScrollbar + guiObjects[pos].AbsoluteSize.Y <= totalPixels then + --It fits, so back up our scroll position + pixelsBelowScrollbar = pixelsBelowScrollbar + guiObjects[pos].AbsoluteSize.Y + if scrollPosition <= 1 then + scrollPosition = 1 + break + else + --local ("Backing up ScrollPosition from -- " ..scrollPosition) + scrollPosition = scrollPosition - 1 + end + else + break + end + end + pos = pos - 1 + end + + pos = scrollPosition + for i, child in ipairs(guiObjects) do + if i < scrollPosition then + --print("Hiding " .. child.Name) + child.Visible = false + else + if pixelsRemaining < 0 then + --print("Out of Space " .. child.Name) + child.Visible = false + else + --print("Laying out " .. child.Name) + --GuiObject + child.Position = UDim2.new(child.Position.X.Scale, child.Position.X.Offset, 0, totalPixels - pixelsRemaining) + pixelsRemaining = pixelsRemaining - child.AbsoluteSize.Y + if (pixelsRemaining >= 0) then + child.Visible = true + howManyDisplayed = howManyDisplayed + 1 + else + child.Visible = false + end + end + end + end + scrollUpButton.Active = (scrollPosition > 1) + scrollDownButton.Active = (pixelsRemaining < 0) + scrollDrag.Active = #guiObjects > howManyDisplayed + scrollDrag.Visible = scrollDrag.Active + end + + + local moveDragger = function() + local guiObjects = 0 + local children = frame:GetChildren() + if children then + for i, child in ipairs(children) do + if child:IsA("GuiObject") then + guiObjects = guiObjects + 1 + end + end + end + + if not scrollDrag.Parent then return end + + local dragSizeY = scrollDrag.Parent.AbsoluteSize.y * (1/(guiObjects - howManyDisplayed + 1)) + if dragSizeY < 16 then dragSizeY = 16 end + scrollDrag.Size = UDim2.new(scrollDrag.Size.X.Scale,scrollDrag.Size.X.Offset,scrollDrag.Size.Y.Scale,dragSizeY) + + local relativeYPos = (scrollPosition - 1)/(guiObjects - (howManyDisplayed)) + if relativeYPos > 1 then relativeYPos = 1 + elseif relativeYPos < 0 then relativeYPos = 0 end + local absYPos = 0 + + if relativeYPos ~= 0 then + absYPos = (relativeYPos * scrollbar.AbsoluteSize.y) - (relativeYPos * scrollDrag.AbsoluteSize.y) + end + + scrollDrag.Position = UDim2.new(scrollDrag.Position.X.Scale,scrollDrag.Position.X.Offset,scrollDrag.Position.Y.Scale,absYPos) + end + + local reentrancyGuard = false + local recalculate = function() + if reentrancyGuard then + return + end + reentrancyGuard = true + wait() + local success, err = nil + if style == "grid" then + success, err = pcall(function() layoutGridScrollBar() end) + elseif style == "simple" then + success, err = pcall(function() layoutSimpleScrollBar() end) + end + if not success then print(err) end + moveDragger() + reentrancyGuard = false + end + + local doScrollUp = function() + scrollPosition = (scrollPosition) - rowSize + if scrollPosition < 1 then scrollPosition = 1 end + recalculate(nil) + end + + local doScrollDown = function() + scrollPosition = (scrollPosition) + rowSize + recalculate(nil) + end + + local scrollUp = function(mouseYPos) + if scrollUpButton.Active then + scrollStamp = tick() + local current = scrollStamp + local upCon + upCon = mouseDrag.MouseButton1Up:connect(function() + scrollStamp = tick() + mouseDrag.Parent = nil + upCon:disconnect() + end) + mouseDrag.Parent = getLayerCollectorAncestor(scrollbar) + doScrollUp() + wait(0.2) + local t = tick() + local w = 0.1 + while scrollStamp == current do + doScrollUp() + if mouseYPos and mouseYPos > scrollDrag.AbsolutePosition.y then + break + end + if not scrollUpButton.Active then break end + if tick()-t > 5 then + w = 0 + elseif tick()-t > 2 then + w = 0.06 + end + wait(w) + end + end + end + + local scrollDown = function(mouseYPos) + if scrollDownButton.Active then + scrollStamp = tick() + local current = scrollStamp + local downCon + downCon = mouseDrag.MouseButton1Up:connect(function() + scrollStamp = tick() + mouseDrag.Parent = nil + downCon:disconnect() + end) + mouseDrag.Parent = getLayerCollectorAncestor(scrollbar) + doScrollDown() + wait(0.2) + local t = tick() + local w = 0.1 + while scrollStamp == current do + doScrollDown() + if mouseYPos and mouseYPos < (scrollDrag.AbsolutePosition.y + scrollDrag.AbsoluteSize.x) then + break + end + if not scrollDownButton.Active then break end + if tick()-t > 5 then + w = 0 + elseif tick()-t > 2 then + w = 0.06 + end + wait(w) + end + end + end + + local y = 0 + scrollDrag.MouseButton1Down:connect(function(x,y) + if scrollDrag.Active then + scrollStamp = tick() + local mouseOffset = y - scrollDrag.AbsolutePosition.y + local dragCon + local upCon + dragCon = mouseDrag.MouseMoved:connect(function(x,y) + local barAbsPos = scrollbar.AbsolutePosition.y + local barAbsSize = scrollbar.AbsoluteSize.y + + local dragAbsSize = scrollDrag.AbsoluteSize.y + local barAbsOne = barAbsPos + barAbsSize - dragAbsSize + y = y - mouseOffset + y = y < barAbsPos and barAbsPos or y > barAbsOne and barAbsOne or y + y = y - barAbsPos + + local guiObjects = 0 + local children = frame:GetChildren() + if children then + for i, child in ipairs(children) do + if child:IsA("GuiObject") then + guiObjects = guiObjects + 1 + end + end + end + + local doublePercent = y/(barAbsSize-dragAbsSize) + local rowDiff = rowSize + local totalScrollCount = guiObjects - (howManyDisplayed - 1) + local newScrollPosition = math.floor((doublePercent * totalScrollCount) + 0.5) + rowDiff + if newScrollPosition < scrollPosition then + rowDiff = -rowDiff + end + + if newScrollPosition < 1 then + newScrollPosition = 1 + end + + scrollPosition = newScrollPosition + recalculate(nil) + end) + upCon = mouseDrag.MouseButton1Up:connect(function() + scrollStamp = tick() + mouseDrag.Parent = nil + dragCon:disconnect(); dragCon = nil + upCon:disconnect(); drag = nil + end) + mouseDrag.Parent = getLayerCollectorAncestor(scrollbar) + end + end) + + local scrollMouseCount = 0 + + scrollUpButton.MouseButton1Down:connect( + function() + scrollUp() + end) + scrollUpButton.MouseButton1Up:connect(function() + scrollStamp = tick() + end) + + + scrollDownButton.MouseButton1Up:connect(function() + scrollStamp = tick() + end) + scrollDownButton.MouseButton1Down:connect( + function() + scrollDown() + end) + + scrollbar.MouseButton1Up:connect(function() + scrollStamp = tick() + end) + scrollbar.MouseButton1Down:connect( + function(x,y) + if y > (scrollDrag.AbsoluteSize.y + scrollDrag.AbsolutePosition.y) then + scrollDown(y) + elseif y < (scrollDrag.AbsolutePosition.y) then + scrollUp(y) + end + end) + + + frame.ChildAdded:connect(function() + recalculate(nil) + end) + + frame.ChildRemoved:connect(function() + recalculate(nil) + end) + + frame.Changed:connect( + function(prop) + if prop == "AbsoluteSize" then + --Wait a heartbeat for it to sync in + recalculate(nil) + end + end) + frame.AncestryChanged:connect(function() recalculate(nil) end) + + return frame, scrollUpButton, scrollDownButton, recalculate, scrollbar +end +local function binaryGrow(min, max, fits) + if min > max then + return min + end + local biggestLegal = min + + while min <= max do + local mid = min + math.floor((max - min) / 2) + if fits(mid) and (biggestLegal == nil or biggestLegal < mid) then + biggestLegal = mid + + --Try growing + min = mid + 1 + else + --Doesn't fit, shrink + max = mid - 1 + end + end + return biggestLegal +end + + +local function binaryShrink(min, max, fits) + if min > max then + return min + end + local smallestLegal = max + + while min <= max do + local mid = min + math.floor((max - min) / 2) + if fits(mid) and (smallestLegal == nil or smallestLegal > mid) then + smallestLegal = mid + + --It fits, shrink + max = mid - 1 + else + --Doesn't fit, grow + min = mid + 1 + end + end + return smallestLegal +end + + +local function getGuiOwner(instance) + while instance ~= nil do + if instance:IsA("ScreenGui") or instance:IsA("BillboardGui") then + return instance + end + instance = instance.Parent + end + return nil +end + +t.AutoTruncateTextObject = function(textLabel) + local text = textLabel.Text + + local fullLabel = textLabel:Clone() + fullLabel.Name = "Full" .. textLabel.Name + fullLabel.BorderSizePixel = 0 + fullLabel.BackgroundTransparency = 0 + fullLabel.Text = text + fullLabel.TextXAlignment = Enum.TextXAlignment.Center + fullLabel.Position = UDim2.new(0,-3,0,0) + fullLabel.Size = UDim2.new(0,100,1,0) + fullLabel.Visible = false + fullLabel.Parent = textLabel + + local shortText = nil + local mouseEnterConnection = nil + local mouseLeaveConnection= nil + + local checkForResize = function() + if getGuiOwner(textLabel) == nil then + return + end + textLabel.Text = text + if textLabel.TextFits then + --Tear down the rollover if it is active + if mouseEnterConnection then + mouseEnterConnection:disconnect() + mouseEnterConnection = nil + end + if mouseLeaveConnection then + mouseLeaveConnection:disconnect() + mouseLeaveConnection = nil + end + else + local len = string.len(text) + textLabel.Text = text .. "~" + + --Shrink the text + local textSize = binaryGrow(0, len, + function(pos) + if pos == 0 then + textLabel.Text = "~" + else + textLabel.Text = string.sub(text, 1, pos) .. "~" + end + return textLabel.TextFits + end) + shortText = string.sub(text, 1, textSize) .. "~" + textLabel.Text = shortText + + --Make sure the fullLabel fits + if not fullLabel.TextFits then + --Already too small, grow it really bit to start + fullLabel.Size = UDim2.new(0, 10000, 1, 0) + end + + --Okay, now try to binary shrink it back down + local fullLabelSize = binaryShrink(textLabel.AbsoluteSize.X,fullLabel.AbsoluteSize.X, + function(size) + fullLabel.Size = UDim2.new(0, size, 1, 0) + return fullLabel.TextFits + end) + fullLabel.Size = UDim2.new(0,fullLabelSize+6,1,0) + + --Now setup the rollover effects, if they are currently off + if mouseEnterConnection == nil then + mouseEnterConnection = textLabel.MouseEnter:connect( + function() + fullLabel.ZIndex = textLabel.ZIndex + 1 + fullLabel.Visible = true + --textLabel.Text = "" + end) + end + if mouseLeaveConnection == nil then + mouseLeaveConnection = textLabel.MouseLeave:connect( + function() + fullLabel.Visible = false + --textLabel.Text = shortText + end) + end + end + end + textLabel.AncestryChanged:connect(checkForResize) + textLabel.Changed:connect( + function(prop) + if prop == "AbsoluteSize" then + checkForResize() + end + end) + + checkForResize() + + local function changeText(newText) + text = newText + fullLabel.Text = text + checkForResize() + end + + return textLabel, changeText +end + +local function TransitionTutorialPages(fromPage, toPage, transitionFrame, currentPageValue) + if fromPage then + fromPage.Visible = false + if transitionFrame.Visible == false then + transitionFrame.Size = fromPage.Size + transitionFrame.Position = fromPage.Position + end + else + if transitionFrame.Visible == false then + transitionFrame.Size = UDim2.new(0.0,50,0.0,50) + transitionFrame.Position = UDim2.new(0.5,-25,0.5,-25) + end + end + transitionFrame.Visible = true + currentPageValue.Value = nil + + local newSize, newPosition + if toPage then + --Make it visible so it resizes + toPage.Visible = true + + newSize = toPage.Size + newPosition = toPage.Position + + toPage.Visible = false + else + newSize = UDim2.new(0.0,50,0.0,50) + newPosition = UDim2.new(0.5,-25,0.5,-25) + end + transitionFrame:TweenSizeAndPosition(newSize, newPosition, Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, 0.3, true, + function(state) + if state == Enum.TweenStatus.Completed then + transitionFrame.Visible = false + if toPage then + toPage.Visible = true + currentPageValue.Value = toPage + end + end + end) +end + +t.CreateTutorial = function(name, tutorialKey, createButtons) + local frame = Instance.new("Frame") + frame.Name = "Tutorial-" .. name + frame.BackgroundTransparency = 1 + frame.Size = UDim2.new(0.6, 0, 0.6, 0) + frame.Position = UDim2.new(0.2, 0, 0.2, 0) + + local transitionFrame = Instance.new("Frame") + transitionFrame.Name = "TransitionFrame" + transitionFrame.Style = Enum.FrameStyle.RobloxRound + transitionFrame.Size = UDim2.new(0.6, 0, 0.6, 0) + transitionFrame.Position = UDim2.new(0.2, 0, 0.2, 0) + transitionFrame.Visible = false + transitionFrame.Parent = frame + + local currentPageValue = Instance.new("ObjectValue") + currentPageValue.Name = "CurrentTutorialPage" + currentPageValue.Value = nil + currentPageValue.Parent = frame + + local boolValue = Instance.new("BoolValue") + boolValue.Name = "Buttons" + boolValue.Value = createButtons + boolValue.Parent = frame + + local pages = Instance.new("Frame") + pages.Name = "Pages" + pages.BackgroundTransparency = 1 + pages.Size = UDim2.new(1,0,1,0) + pages.Parent = frame + + local function getVisiblePageAndHideOthers() + local visiblePage = nil + local children = pages:GetChildren() + if children then + for i,child in ipairs(children) do + if child.Visible then + if visiblePage then + child.Visible = false + else + visiblePage = child + end + end + end + end + return visiblePage + end + + local showTutorial = function(alwaysShow) + if alwaysShow or UserSettings().GameSettings:GetTutorialState(tutorialKey) == false then + print("Showing tutorial-",tutorialKey) + local currentTutorialPage = getVisiblePageAndHideOthers() + + local firstPage = pages:FindFirstChild("TutorialPage1") + if firstPage then + TransitionTutorialPages(currentTutorialPage, firstPage, transitionFrame, currentPageValue) + else + error("Could not find TutorialPage1") + end + end + end + + local dismissTutorial = function() + local currentTutorialPage = getVisiblePageAndHideOthers() + + if currentTutorialPage then + TransitionTutorialPages(currentTutorialPage, nil, transitionFrame, currentPageValue) + end + + UserSettings().GameSettings:SetTutorialState(tutorialKey, true) + end + + local gotoPage = function(pageNum) + local page = pages:FindFirstChild("TutorialPage" .. pageNum) + local currentTutorialPage = getVisiblePageAndHideOthers() + TransitionTutorialPages(currentTutorialPage, page, transitionFrame, currentPageValue) + end + + return frame, showTutorial, dismissTutorial, gotoPage +end + +local function CreateBasicTutorialPage(name, handleResize, skipTutorial, giveDoneButton) + local frame = Instance.new("Frame") + frame.Name = "TutorialPage" + frame.Style = Enum.FrameStyle.RobloxRound + frame.Size = UDim2.new(0.6, 0, 0.6, 0) + frame.Position = UDim2.new(0.2, 0, 0.2, 0) + frame.Visible = false + + local frameHeader = Instance.new("TextLabel") + frameHeader.Name = "Header" + frameHeader.Text = name + frameHeader.BackgroundTransparency = 1 + frameHeader.FontSize = Enum.FontSize.Size24 + frameHeader.Font = Enum.Font.ArialBold + frameHeader.TextColor3 = Color3.new(1,1,1) + frameHeader.TextXAlignment = Enum.TextXAlignment.Center + frameHeader.TextWrap = true + frameHeader.Size = UDim2.new(1,-55, 0, 22) + frameHeader.Position = UDim2.new(0,0,0,0) + frameHeader.Parent = frame + + local skipButton = Instance.new("ImageButton") + skipButton.Name = "SkipButton" + skipButton.AutoButtonColor = false + skipButton.BackgroundTransparency = 1 + skipButton.Image = "rbxasset://textures/ui/closeButton.png" + skipButton.MouseButton1Click:connect(function() + skipTutorial() + end) + skipButton.MouseEnter:connect(function() + skipButton.Image = "rbxasset://textures/ui/closeButton_dn.png" + end) + skipButton.MouseLeave:connect(function() + skipButton.Image = "rbxasset://textures/ui/closeButton.png" + end) + skipButton.Size = UDim2.new(0, 25, 0, 25) + skipButton.Position = UDim2.new(1, -25, 0, 0) + skipButton.Parent = frame + + + if giveDoneButton then + local doneButton = Instance.new("TextButton") + doneButton.Name = "DoneButton" + doneButton.Style = Enum.ButtonStyle.RobloxButtonDefault + doneButton.Text = "Done" + doneButton.TextColor3 = Color3.new(1,1,1) + doneButton.Font = Enum.Font.ArialBold + doneButton.FontSize = Enum.FontSize.Size18 + doneButton.Size = UDim2.new(0,100,0,50) + doneButton.Position = UDim2.new(0.5,-50,1,-50) + + if skipTutorial then + doneButton.MouseButton1Click:connect(function() skipTutorial() end) + end + + doneButton.Parent = frame + end + + local innerFrame = Instance.new("Frame") + innerFrame.Name = "ContentFrame" + innerFrame.BackgroundTransparency = 1 + innerFrame.Position = UDim2.new(0,0,0,25) + innerFrame.Parent = frame + + local nextButton = Instance.new("TextButton") + nextButton.Name = "NextButton" + nextButton.Text = "Next" + nextButton.TextColor3 = Color3.new(1,1,1) + nextButton.Font = Enum.Font.Arial + nextButton.FontSize = Enum.FontSize.Size18 + nextButton.Style = Enum.ButtonStyle.RobloxButtonDefault + nextButton.Size = UDim2.new(0,80, 0, 32) + nextButton.Position = UDim2.new(0.5, 5, 1, -32) + nextButton.Active = false + nextButton.Visible = false + nextButton.Parent = frame + + local prevButton = Instance.new("TextButton") + prevButton.Name = "PrevButton" + prevButton.Text = "Previous" + prevButton.TextColor3 = Color3.new(1,1,1) + prevButton.Font = Enum.Font.Arial + prevButton.FontSize = Enum.FontSize.Size18 + prevButton.Style = Enum.ButtonStyle.RobloxButton + prevButton.Size = UDim2.new(0,80, 0, 32) + prevButton.Position = UDim2.new(0.5, -85, 1, -32) + prevButton.Active = false + prevButton.Visible = false + prevButton.Parent = frame + + if giveDoneButton then + innerFrame.Size = UDim2.new(1,0,1,-75) + else + innerFrame.Size = UDim2.new(1,0,1,-22) + end + + local parentConnection = nil + + local function basicHandleResize() + if frame.Visible and frame.Parent then + local maxSize = math.min(frame.Parent.AbsoluteSize.X, frame.Parent.AbsoluteSize.Y) + handleResize(200,maxSize) + end + end + + frame.Changed:connect( + function(prop) + if prop == "Parent" then + if parentConnection ~= nil then + parentConnection:disconnect() + parentConnection = nil + end + if frame.Parent and frame.Parent:IsA("GuiObject") then + parentConnection = frame.Parent.Changed:connect( + function(parentProp) + if parentProp == "AbsoluteSize" then + wait() + basicHandleResize() + end + end) + basicHandleResize() + end + end + + if prop == "Visible" then + basicHandleResize() + end + end) + + return frame, innerFrame +end + +t.CreateTextTutorialPage = function(name, text, skipTutorialFunc) + local frame = nil + local contentFrame = nil + + local textLabel = Instance.new("TextLabel") + textLabel.BackgroundTransparency = 1 + textLabel.TextColor3 = Color3.new(1,1,1) + textLabel.Text = text + textLabel.TextWrap = true + textLabel.TextXAlignment = Enum.TextXAlignment.Left + textLabel.TextYAlignment = Enum.TextYAlignment.Center + textLabel.Font = Enum.Font.Arial + textLabel.FontSize = Enum.FontSize.Size14 + textLabel.Size = UDim2.new(1,0,1,0) + + local function handleResize(minSize, maxSize) + size = binaryShrink(minSize, maxSize, + function(size) + frame.Size = UDim2.new(0, size, 0, size) + return textLabel.TextFits + end) + frame.Size = UDim2.new(0, size, 0, size) + frame.Position = UDim2.new(0.5, -size/2, 0.5, -size/2) + end + + frame, contentFrame = CreateBasicTutorialPage(name, handleResize, skipTutorialFunc) + textLabel.Parent = contentFrame + + return frame +end + +t.CreateImageTutorialPage = function(name, imageAsset, x, y, skipTutorialFunc, giveDoneButton) + local frame = nil + local contentFrame = nil + + local imageLabel = Instance.new("ImageLabel") + imageLabel.BackgroundTransparency = 1 + imageLabel.Image = imageAsset + imageLabel.Size = UDim2.new(0,x,0,y) + imageLabel.Position = UDim2.new(0.5,-x/2,0.5,-y/2) + + local function handleResize(minSize, maxSize) + size = binaryShrink(minSize, maxSize, + function(size) + return size >= x and size >= y + end) + if size >= x and size >= y then + imageLabel.Size = UDim2.new(0,x, 0,y) + imageLabel.Position = UDim2.new(0.5,-x/2, 0.5, -y/2) + else + if x > y then + --X is limiter, so + imageLabel.Size = UDim2.new(1,0,y/x,0) + imageLabel.Position = UDim2.new(0,0, 0.5 - (y/x)/2, 0) + else + --Y is limiter + imageLabel.Size = UDim2.new(x/y,0,1, 0) + imageLabel.Position = UDim2.new(0.5-(x/y)/2, 0, 0, 0) + end + end + size = size + 50 + frame.Size = UDim2.new(0, size, 0, size) + frame.Position = UDim2.new(0.5, -size/2, 0.5, -size/2) + end + + frame, contentFrame = CreateBasicTutorialPage(name, handleResize, skipTutorialFunc, giveDoneButton) + imageLabel.Parent = contentFrame + + return frame +end + +t.AddTutorialPage = function(tutorial, tutorialPage) + local transitionFrame = tutorial.TransitionFrame + local currentPageValue = tutorial.CurrentTutorialPage + + if not tutorial.Buttons.Value then + tutorialPage.NextButton.Parent = nil + tutorialPage.PrevButton.Parent = nil + end + + local children = tutorial.Pages:GetChildren() + if children and #children > 0 then + tutorialPage.Name = "TutorialPage" .. (#children+1) + local previousPage = children[#children] + if not previousPage:IsA("GuiObject") then + error("All elements under Pages must be GuiObjects") + end + + if tutorial.Buttons.Value then + if previousPage.NextButton.Active then + error("NextButton already Active on previousPage, please only add pages with RbxGui.AddTutorialPage function") + end + previousPage.NextButton.MouseButton1Click:connect( + function() + TransitionTutorialPages(previousPage, tutorialPage, transitionFrame, currentPageValue) + end) + previousPage.NextButton.Active = true + previousPage.NextButton.Visible = true + + if tutorialPage.PrevButton.Active then + error("PrevButton already Active on tutorialPage, please only add pages with RbxGui.AddTutorialPage function") + end + tutorialPage.PrevButton.MouseButton1Click:connect( + function() + TransitionTutorialPages(tutorialPage, previousPage, transitionFrame, currentPageValue) + end) + tutorialPage.PrevButton.Active = true + tutorialPage.PrevButton.Visible = true + end + + tutorialPage.Parent = tutorial.Pages + else + --First child + tutorialPage.Name = "TutorialPage1" + tutorialPage.Parent = tutorial.Pages + end +end + +t.CreateSetPanel = function(userIdsForSets, objectSelected, dialogClosed, size, position, showAdminCategories, useAssetVersionId) + + if not userIdsForSets then + error("CreateSetPanel: userIdsForSets (first arg) is nil, should be a table of number ids") + end + if type(userIdsForSets) ~= "table" and type(userIdsForSets) ~= "userdata" then + error("CreateSetPanel: userIdsForSets (first arg) is of type " ..type(userIdsForSets) .. ", should be of type table or userdata") + end + if not objectSelected then + error("CreateSetPanel: objectSelected (second arg) is nil, should be a callback function!") + end + if type(objectSelected) ~= "function" then + error("CreateSetPanel: objectSelected (second arg) is of type " .. type(objectSelected) .. ", should be of type function!") + end + if dialogClosed and type(dialogClosed) ~= "function" then + error("CreateSetPanel: dialogClosed (third arg) is of type " .. type(dialogClosed) .. ", should be of type function!") + end + + if showAdminCategories == nil then -- by default, don't show beta sets + showAdminCategories = false + end + + local arrayPosition = 1 + local insertButtons = {} + local insertButtonCons = {} + local contents = nil + local setGui = nil + + -- used for water selections + local waterForceDirection = "NegX" + local waterForce = "None" + local waterGui, waterTypeChangedEvent = nil + + local Data = {} + Data.CurrentCategory = nil + Data.Category = {} + local SetCache = {} + + local userCategoryButtons = nil + + local buttonWidth = 64 + local buttonHeight = buttonWidth + + local SmallThumbnailUrl = nil + local LargeThumbnailUrl = nil + local BaseUrl = game:GetService("ContentProvider").BaseUrl:lower() + local AssetGameUrl = string.gsub(BaseUrl, "www", "assetgame") + + if useAssetVersionId then + LargeThumbnailUrl = AssetGameUrl .. "Game/Tools/ThumbnailAsset.ashx?fmt=png&wd=420&ht=420&assetversionid=" + SmallThumbnailUrl = AssetGameUrl .. "Game/Tools/ThumbnailAsset.ashx?fmt=png&wd=75&ht=75&assetversionid=" + else + LargeThumbnailUrl = AssetGameUrl .. "Game/Tools/ThumbnailAsset.ashx?fmt=png&wd=420&ht=420&aid=" + SmallThumbnailUrl = AssetGameUrl .. "Game/Tools/ThumbnailAsset.ashx?fmt=png&wd=75&ht=75&aid=" + end + + local function drillDownSetZIndex(parent, index) + local children = parent:GetChildren() + for i = 1, #children do + if children[i]:IsA("GuiObject") then + children[i].ZIndex = index + end + drillDownSetZIndex(children[i], index) + end + end + + -- for terrain stamping + local currTerrainDropDownFrame = nil + local terrainShapes = {"Block","Vertical Ramp","Corner Wedge","Inverse Corner Wedge","Horizontal Ramp","Auto-Wedge"} + local terrainShapeMap = {} + for i = 1, #terrainShapes do + terrainShapeMap[terrainShapes[i]] = i - 1 + end + terrainShapeMap[terrainShapes[#terrainShapes]] = 6 + + local function createWaterGui() + local waterForceDirections = {"NegX","X","NegY","Y","NegZ","Z"} + local waterForces = {"None", "Small", "Medium", "Strong", "Max"} + + local waterFrame = Instance.new("Frame") + waterFrame.Name = "WaterFrame" + waterFrame.Style = Enum.FrameStyle.RobloxSquare + waterFrame.Size = UDim2.new(0,150,0,110) + waterFrame.Visible = false + + local waterForceLabel = Instance.new("TextLabel") + waterForceLabel.Name = "WaterForceLabel" + waterForceLabel.BackgroundTransparency = 1 + waterForceLabel.Size = UDim2.new(1,0,0,12) + waterForceLabel.Font = Enum.Font.ArialBold + waterForceLabel.FontSize = Enum.FontSize.Size12 + waterForceLabel.TextColor3 = Color3.new(1,1,1) + waterForceLabel.TextXAlignment = Enum.TextXAlignment.Left + waterForceLabel.Text = "Water Force" + waterForceLabel.Parent = waterFrame + + local waterForceDirLabel = waterForceLabel:Clone() + waterForceDirLabel.Name = "WaterForceDirectionLabel" + waterForceDirLabel.Text = "Water Force Direction" + waterForceDirLabel.Position = UDim2.new(0,0,0,50) + waterForceDirLabel.Parent = waterFrame + + local waterTypeChangedEvent = Instance.new("BindableEvent",waterFrame) + waterTypeChangedEvent.Name = "WaterTypeChangedEvent" + + local waterForceDirectionSelectedFunc = function(newForceDirection) + waterForceDirection = newForceDirection + waterTypeChangedEvent:Fire({waterForce, waterForceDirection}) + end + local waterForceSelectedFunc = function(newForce) + waterForce = newForce + waterTypeChangedEvent:Fire({waterForce, waterForceDirection}) + end + + local waterForceDirectionDropDown, forceWaterDirectionSelection = t.CreateDropDownMenu(waterForceDirections, waterForceDirectionSelectedFunc) + waterForceDirectionDropDown.Size = UDim2.new(1,0,0,25) + waterForceDirectionDropDown.Position = UDim2.new(0,0,1,3) + forceWaterDirectionSelection("NegX") + waterForceDirectionDropDown.Parent = waterForceDirLabel + + local waterForceDropDown, forceWaterForceSelection = t.CreateDropDownMenu(waterForces, waterForceSelectedFunc) + forceWaterForceSelection("None") + waterForceDropDown.Size = UDim2.new(1,0,0,25) + waterForceDropDown.Position = UDim2.new(0,0,1,3) + waterForceDropDown.Parent = waterForceLabel + + return waterFrame, waterTypeChangedEvent + end + + -- Helper Function that contructs gui elements + local function createSetGui() + + local setGui = Instance.new("ScreenGui") + setGui.Name = "SetGui" + + local setPanel = Instance.new("Frame") + setPanel.Name = "SetPanel" + setPanel.Active = true + setPanel.BackgroundTransparency = 1 + if position then + setPanel.Position = position + else + setPanel.Position = UDim2.new(0.2, 29, 0.1, 24) + end + if size then + setPanel.Size = size + else + setPanel.Size = UDim2.new(0.6, -58, 0.64, 0) + end + setPanel.Style = Enum.FrameStyle.RobloxRound + setPanel.ZIndex = 6 + setPanel.Parent = setGui + + -- Children of SetPanel + local itemPreview = Instance.new("Frame") + itemPreview.Name = "ItemPreview" + itemPreview.BackgroundTransparency = 1 + itemPreview.Position = UDim2.new(0.8,5,0.085,0) + itemPreview.Size = UDim2.new(0.21,0,0.9,0) + itemPreview.ZIndex = 6 + itemPreview.Parent = setPanel + + -- Children of ItemPreview + local textPanel = Instance.new("Frame") + textPanel.Name = "TextPanel" + textPanel.BackgroundTransparency = 1 + textPanel.Position = UDim2.new(0,0,0.45,0) + textPanel.Size = UDim2.new(1,0,0.55,0) + textPanel.ZIndex = 6 + textPanel.Parent = itemPreview + + -- Children of TextPanel + local rolloverText = Instance.new("TextLabel") + rolloverText.Name = "RolloverText" + rolloverText.BackgroundTransparency = 1 + rolloverText.Size = UDim2.new(1,0,0,48) + rolloverText.ZIndex = 6 + rolloverText.Font = Enum.Font.ArialBold + rolloverText.FontSize = Enum.FontSize.Size24 + rolloverText.Text = "" + rolloverText.TextColor3 = Color3.new(1,1,1) + rolloverText.TextWrap = true + rolloverText.TextXAlignment = Enum.TextXAlignment.Left + rolloverText.TextYAlignment = Enum.TextYAlignment.Top + rolloverText.Parent = textPanel + + local largePreview = Instance.new("ImageLabel") + largePreview.Name = "LargePreview" + largePreview.BackgroundTransparency = 1 + largePreview.Image = "" + largePreview.Size = UDim2.new(1,0,0,170) + largePreview.ZIndex = 6 + largePreview.Parent = itemPreview + + local sets = Instance.new("Frame") + sets.Name = "Sets" + sets.BackgroundTransparency = 1 + sets.Position = UDim2.new(0,0,0,5) + sets.Size = UDim2.new(0.23,0,1,-5) + sets.ZIndex = 6 + sets.Parent = setPanel + + -- Children of Sets + local line = Instance.new("Frame") + line.Name = "Line" + line.BackgroundColor3 = Color3.new(1,1,1) + line.BackgroundTransparency = 0.7 + line.BorderSizePixel = 0 + line.Position = UDim2.new(1,-3,0.06,0) + line.Size = UDim2.new(0,3,0.9,0) + line.ZIndex = 6 + line.Parent = sets + + local setsLists, controlFrame = t.CreateTrueScrollingFrame() + setsLists.Size = UDim2.new(1,-6,0.94,0) + setsLists.Position = UDim2.new(0,0,0.06,0) + setsLists.BackgroundTransparency = 1 + setsLists.Name = "SetsLists" + setsLists.ZIndex = 6 + setsLists.Parent = sets + drillDownSetZIndex(controlFrame, 7) + + local setsHeader = Instance.new("TextLabel") + setsHeader.Name = "SetsHeader" + setsHeader.BackgroundTransparency = 1 + setsHeader.Size = UDim2.new(0,47,0,24) + setsHeader.ZIndex = 6 + setsHeader.Font = Enum.Font.ArialBold + setsHeader.FontSize = Enum.FontSize.Size24 + setsHeader.Text = "Sets" + setsHeader.TextColor3 = Color3.new(1,1,1) + setsHeader.TextXAlignment = Enum.TextXAlignment.Left + setsHeader.TextYAlignment = Enum.TextYAlignment.Top + setsHeader.Parent = sets + + local cancelButton = Instance.new("TextButton") + cancelButton.Name = "CancelButton" + cancelButton.Position = UDim2.new(1,-32,0,-2) + cancelButton.Size = UDim2.new(0,34,0,34) + cancelButton.Style = Enum.ButtonStyle.RobloxButtonDefault + cancelButton.ZIndex = 6 + cancelButton.Text = "" + cancelButton.Modal = true + cancelButton.Parent = setPanel + + -- Children of Cancel Button + local cancelImage = Instance.new("ImageLabel") + cancelImage.Name = "CancelImage" + cancelImage.BackgroundTransparency = 1 + cancelImage.Image = "https://www.roblox.com/asset/?id=54135717" + cancelImage.Position = UDim2.new(0,-2,0,-2) + cancelImage.Size = UDim2.new(0,16,0,16) + cancelImage.ZIndex = 6 + cancelImage.Parent = cancelButton + + return setGui + end + + local function createSetButton(text) + local setButton = Instance.new("TextButton") + + if text then setButton.Text = text + else setButton.Text = "" end + + setButton.AutoButtonColor = false + setButton.BackgroundTransparency = 1 + setButton.BackgroundColor3 = Color3.new(1,1,1) + setButton.BorderSizePixel = 0 + setButton.Size = UDim2.new(1,-5,0,18) + setButton.ZIndex = 6 + setButton.Visible = false + setButton.Font = Enum.Font.Arial + setButton.FontSize = Enum.FontSize.Size18 + setButton.TextColor3 = Color3.new(1,1,1) + setButton.TextXAlignment = Enum.TextXAlignment.Left + + return setButton + end + + local function buildSetButton(name, setId, setImageId, i, count) + local button = createSetButton(name) + button.Text = name + button.Name = "SetButton" + button.Visible = true + + local setValue = Instance.new("IntValue") + setValue.Name = "SetId" + setValue.Value = setId + setValue.Parent = button + + local setName = Instance.new("StringValue") + setName.Name = "SetName" + setName.Value = name + setName.Parent = button + + return button + end + + local function processCategory(sets) + local setButtons = {} + local numSkipped = 0 + for i = 1, #sets do + if not showAdminCategories and sets[i].Name == "Beta" then + numSkipped = numSkipped + 1 + else + setButtons[i - numSkipped] = buildSetButton(sets[i].Name, sets[i].CategoryId, sets[i].ImageAssetId, i - numSkipped, #sets) + end + end + return setButtons + end + + local function handleResize() + wait() -- neccessary to insure heartbeat happened + + local itemPreview = setGui.SetPanel.ItemPreview + + itemPreview.LargePreview.Size = UDim2.new(1,0,0,itemPreview.AbsoluteSize.X) + itemPreview.LargePreview.Position = UDim2.new(0.5,-itemPreview.LargePreview.AbsoluteSize.X/2,0,0) + itemPreview.TextPanel.Position = UDim2.new(0,0,0,itemPreview.LargePreview.AbsoluteSize.Y) + itemPreview.TextPanel.Size = UDim2.new(1,0,0,itemPreview.AbsoluteSize.Y - itemPreview.LargePreview.AbsoluteSize.Y) + end + + local function makeInsertAssetButton() + local insertAssetButtonExample = Instance.new("Frame") + insertAssetButtonExample.Name = "InsertAssetButtonExample" + insertAssetButtonExample.Position = UDim2.new(0,128,0,64) + insertAssetButtonExample.Size = UDim2.new(0,64,0,64) + insertAssetButtonExample.BackgroundTransparency = 1 + insertAssetButtonExample.ZIndex = 6 + insertAssetButtonExample.Visible = false + + local assetId = Instance.new("IntValue") + assetId.Name = "AssetId" + assetId.Value = 0 + assetId.Parent = insertAssetButtonExample + + local assetName = Instance.new("StringValue") + assetName.Name = "AssetName" + assetName.Value = "" + assetName.Parent = insertAssetButtonExample + + local button = Instance.new("TextButton") + button.Name = "Button" + button.Text = "" + button.Style = Enum.ButtonStyle.RobloxButton + button.Position = UDim2.new(0.025,0,0.025,0) + button.Size = UDim2.new(0.95,0,0.95,0) + button.ZIndex = 6 + button.Parent = insertAssetButtonExample + + local buttonImage = Instance.new("ImageLabel") + buttonImage.Name = "ButtonImage" + buttonImage.Image = "" + buttonImage.Position = UDim2.new(0,-7,0,-7) + buttonImage.Size = UDim2.new(1,14,1,14) + buttonImage.BackgroundTransparency = 1 + buttonImage.ZIndex = 7 + buttonImage.Parent = button + + local configIcon = buttonImage:clone() + configIcon.Name = "ConfigIcon" + configIcon.Visible = false + configIcon.Position = UDim2.new(1,-23,1,-24) + configIcon.Size = UDim2.new(0,16,0,16) + configIcon.Image = "" + configIcon.ZIndex = 6 + configIcon.Parent = insertAssetButtonExample + + return insertAssetButtonExample + end + + local function showLargePreview(insertButton) + if insertButton:FindFirstChild("AssetId") then + delay(0,function() + game:GetService("ContentProvider"):Preload(LargeThumbnailUrl .. tostring(insertButton.AssetId.Value)) + setGui.SetPanel.ItemPreview.LargePreview.Image = LargeThumbnailUrl .. tostring(insertButton.AssetId.Value) + end) + end + if insertButton:FindFirstChild("AssetName") then + setGui.SetPanel.ItemPreview.TextPanel.RolloverText.Text = insertButton.AssetName.Value + end + end + + local function selectTerrainShape(shape) + if currTerrainDropDownFrame then + objectSelected(tostring(currTerrainDropDownFrame.AssetName.Value), tonumber(currTerrainDropDownFrame.AssetId.Value), shape) + end + end + + local function createTerrainTypeButton(name, parent) + local dropDownTextButton = Instance.new("TextButton") + dropDownTextButton.Name = name .. "Button" + dropDownTextButton.Font = Enum.Font.ArialBold + dropDownTextButton.FontSize = Enum.FontSize.Size14 + dropDownTextButton.BorderSizePixel = 0 + dropDownTextButton.TextColor3 = Color3.new(1,1,1) + dropDownTextButton.Text = name + dropDownTextButton.TextXAlignment = Enum.TextXAlignment.Left + dropDownTextButton.BackgroundTransparency = 1 + dropDownTextButton.ZIndex = parent.ZIndex + 1 + dropDownTextButton.Size = UDim2.new(0,parent.Size.X.Offset - 2,0,16) + dropDownTextButton.Position = UDim2.new(0,1,0,0) + + dropDownTextButton.MouseEnter:connect(function() + dropDownTextButton.BackgroundTransparency = 0 + dropDownTextButton.TextColor3 = Color3.new(0,0,0) + end) + + dropDownTextButton.MouseLeave:connect(function() + dropDownTextButton.BackgroundTransparency = 1 + dropDownTextButton.TextColor3 = Color3.new(1,1,1) + end) + + dropDownTextButton.MouseButton1Click:connect(function() + dropDownTextButton.BackgroundTransparency = 1 + dropDownTextButton.TextColor3 = Color3.new(1,1,1) + if dropDownTextButton.Parent and dropDownTextButton.Parent:IsA("GuiObject") then + dropDownTextButton.Parent.Visible = false + end + selectTerrainShape(terrainShapeMap[dropDownTextButton.Text]) + end) + + return dropDownTextButton + end + + local function createTerrainDropDownMenu(zIndex) + local dropDown = Instance.new("Frame") + dropDown.Name = "TerrainDropDown" + dropDown.BackgroundColor3 = Color3.new(0,0,0) + dropDown.BorderColor3 = Color3.new(1,0,0) + dropDown.Size = UDim2.new(0,200,0,0) + dropDown.Visible = false + dropDown.ZIndex = zIndex + dropDown.Parent = setGui + + for i = 1, #terrainShapes do + local shapeButton = createTerrainTypeButton(terrainShapes[i],dropDown) + shapeButton.Position = UDim2.new(0,1,0,(i - 1) * (shapeButton.Size.Y.Offset)) + shapeButton.Parent = dropDown + dropDown.Size = UDim2.new(0,200,0,dropDown.Size.Y.Offset + (shapeButton.Size.Y.Offset)) + end + + dropDown.MouseLeave:connect(function() + dropDown.Visible = false + end) + end + + + local function createDropDownMenuButton(parent) + local dropDownButton = Instance.new("ImageButton") + dropDownButton.Name = "DropDownButton" + dropDownButton.Image = "https://www.roblox.com/asset/?id=67581509" + dropDownButton.BackgroundTransparency = 1 + dropDownButton.Size = UDim2.new(0,16,0,16) + dropDownButton.Position = UDim2.new(1,-24,0,6) + dropDownButton.ZIndex = parent.ZIndex + 2 + dropDownButton.Parent = parent + + if not setGui:FindFirstChild("TerrainDropDown") then + createTerrainDropDownMenu(8) + end + + dropDownButton.MouseButton1Click:connect(function() + setGui.TerrainDropDown.Visible = true + setGui.TerrainDropDown.Position = UDim2.new(0,parent.AbsolutePosition.X,0,parent.AbsolutePosition.Y) + currTerrainDropDownFrame = parent + end) + end + + local function buildInsertButton() + local insertButton = makeInsertAssetButton() + insertButton.Name = "InsertAssetButton" + insertButton.Visible = true + + if Data.Category[Data.CurrentCategory].SetName == "High Scalability" then + createDropDownMenuButton(insertButton) + end + + local lastEnter = nil + local mouseEnterCon = insertButton.MouseEnter:connect(function() + lastEnter = insertButton + delay(0.1,function() + if lastEnter == insertButton then + showLargePreview(insertButton) + end + end) + end) + return insertButton, mouseEnterCon + end + + local function realignButtonGrid(columns) + local x = 0 + local y = 0 + for i = 1, #insertButtons do + insertButtons[i].Position = UDim2.new(0, buttonWidth * x, 0, buttonHeight * y) + x = x + 1 + if x >= columns then + x = 0 + y = y + 1 + end + end + end + + local function setInsertButtonImageBehavior(insertFrame, visible, name, assetId) + if visible then + insertFrame.AssetName.Value = name + insertFrame.AssetId.Value = assetId + local newImageUrl = SmallThumbnailUrl .. assetId + if newImageUrl ~= insertFrame.Button.ButtonImage.Image then + delay(0,function() + game:GetService("ContentProvider"):Preload(SmallThumbnailUrl .. assetId) + if insertFrame:findFirstChild("Button") then + insertFrame.Button.ButtonImage.Image = SmallThumbnailUrl .. assetId + end + end) + end + table.insert(insertButtonCons, + insertFrame.Button.MouseButton1Click:connect(function() + -- special case for water, show water selection gui + local isWaterSelected = (name == "Water") and (Data.Category[Data.CurrentCategory].SetName == "High Scalability") + waterGui.Visible = isWaterSelected + if isWaterSelected then + objectSelected(name, tonumber(assetId), nil) + else + objectSelected(name, tonumber(assetId)) + end + end) + ) + insertFrame.Visible = true + else + insertFrame.Visible = false + end + end + + local function loadSectionOfItems(setGui, rows, columns) + local pageSize = rows * columns + + if arrayPosition > #contents then return end + + local origArrayPos = arrayPosition + + local yCopy = 0 + for i = 1, pageSize + 1 do + if arrayPosition >= #contents + 1 then + break + end + + local buttonCon + insertButtons[arrayPosition], buttonCon = buildInsertButton() + table.insert(insertButtonCons,buttonCon) + insertButtons[arrayPosition].Parent = setGui.SetPanel.ItemsFrame + arrayPosition = arrayPosition + 1 + end + realignButtonGrid(columns) + + local indexCopy = origArrayPos + for index = origArrayPos, arrayPosition do + if insertButtons[index] then + if contents[index] then + + -- we don't want water to have a drop down button + if contents[index].Name == "Water" then + if Data.Category[Data.CurrentCategory].SetName == "High Scalability" then + insertButtons[index]:FindFirstChild("DropDownButton",true):Destroy() + end + end + + local assetId + if useAssetVersionId then + assetId = contents[index].AssetVersionId + else + assetId = contents[index].AssetId + end + setInsertButtonImageBehavior(insertButtons[index], true, contents[index].Name, assetId) + else + break + end + else + break + end + indexCopy = index + end + end + + local function setSetIndex() + Data.Category[Data.CurrentCategory].Index = 0 + + rows = 7 + columns = math.floor(setGui.SetPanel.ItemsFrame.AbsoluteSize.X/buttonWidth) + + contents = Data.Category[Data.CurrentCategory].Contents + if contents then + -- remove our buttons and their connections + for i = 1, #insertButtons do + insertButtons[i]:remove() + end + for i = 1, #insertButtonCons do + if insertButtonCons[i] then insertButtonCons[i]:disconnect() end + end + insertButtonCons = {} + insertButtons = {} + + arrayPosition = 1 + loadSectionOfItems(setGui, rows, columns) + end + end + + local function selectSet(button, setName, setId, setIndex) + if button and Data.Category[Data.CurrentCategory] ~= nil then + if button ~= Data.Category[Data.CurrentCategory].Button then + Data.Category[Data.CurrentCategory].Button = button + + if SetCache[setId] == nil then + SetCache[setId] = game:GetService("InsertService"):GetCollection(setId) + end + Data.Category[Data.CurrentCategory].Contents = SetCache[setId] + + Data.Category[Data.CurrentCategory].SetName = setName + Data.Category[Data.CurrentCategory].SetId = setId + end + setSetIndex() + end + end + + local function selectCategoryPage(buttons, page) + if buttons ~= Data.CurrentCategory then + if Data.CurrentCategory then + for key, button in pairs(Data.CurrentCategory) do + button.Visible = false + end + end + + Data.CurrentCategory = buttons + if Data.Category[Data.CurrentCategory] == nil then + Data.Category[Data.CurrentCategory] = {} + if #buttons > 0 then + selectSet(buttons[1], buttons[1].SetName.Value, buttons[1].SetId.Value, 0) + end + else + Data.Category[Data.CurrentCategory].Button = nil + selectSet(Data.Category[Data.CurrentCategory].ButtonFrame, Data.Category[Data.CurrentCategory].SetName, Data.Category[Data.CurrentCategory].SetId, Data.Category[Data.CurrentCategory].Index) + end + end + end + + local function selectCategory(category) + selectCategoryPage(category, 0) + end + + local function resetAllSetButtonSelection() + local setButtons = setGui.SetPanel.Sets.SetsLists:GetChildren() + for i = 1, #setButtons do + if setButtons[i]:IsA("TextButton") then + setButtons[i].Selected = false + setButtons[i].BackgroundTransparency = 1 + setButtons[i].TextColor3 = Color3.new(1,1,1) + setButtons[i].BackgroundColor3 = Color3.new(1,1,1) + end + end + end + + local function populateSetsFrame() + local currRow = 0 + for i = 1, #userCategoryButtons do + local button = userCategoryButtons[i] + button.Visible = true + button.Position = UDim2.new(0,5,0,currRow * button.Size.Y.Offset) + button.Parent = setGui.SetPanel.Sets.SetsLists + + if i == 1 then -- we will have this selected by default, so show it + button.Selected = true + button.BackgroundColor3 = Color3.new(0,204/255,0) + button.TextColor3 = Color3.new(0,0,0) + button.BackgroundTransparency = 0 + end + + button.MouseEnter:connect(function() + if not button.Selected then + button.BackgroundTransparency = 0 + button.TextColor3 = Color3.new(0,0,0) + end + end) + button.MouseLeave:connect(function() + if not button.Selected then + button.BackgroundTransparency = 1 + button.TextColor3 = Color3.new(1,1,1) + end + end) + button.MouseButton1Click:connect(function() + resetAllSetButtonSelection() + button.Selected = not button.Selected + button.BackgroundColor3 = Color3.new(0,204/255,0) + button.TextColor3 = Color3.new(0,0,0) + button.BackgroundTransparency = 0 + selectSet(button, button.Text, userCategoryButtons[i].SetId.Value, 0) + end) + + currRow = currRow + 1 + end + + local buttons = setGui.SetPanel.Sets.SetsLists:GetChildren() + + -- set first category as loaded for default + if buttons then + for i = 1, #buttons do + if buttons[i]:IsA("TextButton") then + selectSet(buttons[i], buttons[i].Text, userCategoryButtons[i].SetId.Value, 0) + selectCategory(userCategoryButtons) + break + end + end + end + end + + setGui = createSetGui() + waterGui, waterTypeChangedEvent = createWaterGui() + waterGui.Position = UDim2.new(0,55,0,0) + waterGui.Parent = setGui + setGui.Changed:connect(function(prop) -- this resizes the preview image to always be the right size + if prop == "AbsoluteSize" then + handleResize() + setSetIndex() + end + end) + + local scrollFrame, controlFrame = t.CreateTrueScrollingFrame() + scrollFrame.Size = UDim2.new(0.54,0,0.85,0) + scrollFrame.Position = UDim2.new(0.24,0,0.085,0) + scrollFrame.Name = "ItemsFrame" + scrollFrame.ZIndex = 6 + scrollFrame.Parent = setGui.SetPanel + scrollFrame.BackgroundTransparency = 1 + + drillDownSetZIndex(controlFrame,7) + + controlFrame.Parent = setGui.SetPanel + controlFrame.Position = UDim2.new(0.76, 5, 0, 0) + + local debounce = false + controlFrame.ScrollBottom.Changed:connect(function(prop) + if controlFrame.ScrollBottom.Value == true then + if debounce then return end + debounce = true + loadSectionOfItems(setGui, rows, columns) + debounce = false + end + end) + + local userData = {} + for id = 1, #userIdsForSets do + local newUserData = game:GetService("InsertService"):GetUserSets(userIdsForSets[id]) + if newUserData and #newUserData > 2 then + -- start at #3 to skip over My Decals and My Models for each account + for category = 3, #newUserData do + if newUserData[category].Name == "High Scalability" then -- we want high scalability parts to show first + table.insert(userData,1,newUserData[category]) + else + table.insert(userData, newUserData[category]) + end + end + end + + end + if userData then + userCategoryButtons = processCategory(userData) + end + + rows = math.floor(setGui.SetPanel.ItemsFrame.AbsoluteSize.Y/buttonHeight) + columns = math.floor(setGui.SetPanel.ItemsFrame.AbsoluteSize.X/buttonWidth) + + populateSetsFrame() + + setGui.SetPanel.CancelButton.MouseButton1Click:connect(function() + setGui.SetPanel.Visible = false + if dialogClosed then dialogClosed() end + end) + + local setVisibilityFunction = function(visible) + if visible then + setGui.SetPanel.Visible = true + else + setGui.SetPanel.Visible = false + end + end + + local getVisibilityFunction = function() + if setGui then + if setGui:FindFirstChild("SetPanel") then + return setGui.SetPanel.Visible + end + end + + return false + end + + return setGui, setVisibilityFunction, getVisibilityFunction, waterTypeChangedEvent +end + +t.CreateTerrainMaterialSelector = function(size,position) + local terrainMaterialSelectionChanged = Instance.new("BindableEvent") + terrainMaterialSelectionChanged.Name = "TerrainMaterialSelectionChanged" + + local selectedButton = nil + + local frame = Instance.new("Frame") + frame.Name = "TerrainMaterialSelector" + if size then + frame.Size = size + else + frame.Size = UDim2.new(0, 245, 0, 230) + end + if position then + frame.Position = position + end + frame.BorderSizePixel = 0 + frame.BackgroundColor3 = Color3.new(0,0,0) + frame.Active = true + + terrainMaterialSelectionChanged.Parent = frame + + local waterEnabled = true -- todo: turn this on when water is ready + + local materialToImageMap = {} + local materialNames = {"Grass", "Sand", "Brick", "Granite", "Asphalt", "Iron", "Aluminum", "Gold", "Plank", "Log", "Gravel", "Cinder Block", "Stone Wall", "Concrete", "Plastic (red)", "Plastic (blue)"} + if waterEnabled then + table.insert(materialNames,"Water") + end + local currentMaterial = 1 + + function getEnumFromName(choice) + if choice == "Grass" then return 1 end + if choice == "Sand" then return 2 end + if choice == "Erase" then return 0 end + if choice == "Brick" then return 3 end + if choice == "Granite" then return 4 end + if choice == "Asphalt" then return 5 end + if choice == "Iron" then return 6 end + if choice == "Aluminum" then return 7 end + if choice == "Gold" then return 8 end + if choice == "Plank" then return 9 end + if choice == "Log" then return 10 end + if choice == "Gravel" then return 11 end + if choice == "Cinder Block" then return 12 end + if choice == "Stone Wall" then return 13 end + if choice == "Concrete" then return 14 end + if choice == "Plastic (red)" then return 15 end + if choice == "Plastic (blue)" then return 16 end + if choice == "Water" then return 17 end + end + + function getNameFromEnum(choice) + if choice == Enum.CellMaterial.Grass or choice == 1 then return "Grass"end + if choice == Enum.CellMaterial.Sand or choice == 2 then return "Sand" end + if choice == Enum.CellMaterial.Empty or choice == 0 then return "Erase" end + if choice == Enum.CellMaterial.Brick or choice == 3 then return "Brick" end + if choice == Enum.CellMaterial.Granite or choice == 4 then return "Granite" end + if choice == Enum.CellMaterial.Asphalt or choice == 5 then return "Asphalt" end + if choice == Enum.CellMaterial.Iron or choice == 6 then return "Iron" end + if choice == Enum.CellMaterial.Aluminum or choice == 7 then return "Aluminum" end + if choice == Enum.CellMaterial.Gold or choice == 8 then return "Gold" end + if choice == Enum.CellMaterial.WoodPlank or choice == 9 then return "Plank" end + if choice == Enum.CellMaterial.WoodLog or choice == 10 then return "Log" end + if choice == Enum.CellMaterial.Gravel or choice == 11 then return "Gravel" end + if choice == Enum.CellMaterial.CinderBlock or choice == 12 then return "Cinder Block" end + if choice == Enum.CellMaterial.MossyStone or choice == 13 then return "Stone Wall" end + if choice == Enum.CellMaterial.Cement or choice == 14 then return "Concrete" end + if choice == Enum.CellMaterial.RedPlastic or choice == 15 then return "Plastic (red)" end + if choice == Enum.CellMaterial.BluePlastic or choice == 16 then return "Plastic (blue)" end + + if waterEnabled then + if choice == Enum.CellMaterial.Water or choice == 17 then return "Water" end + end + end + + + local function updateMaterialChoice(choice) + currentMaterial = getEnumFromName(choice) + terrainMaterialSelectionChanged:Fire(currentMaterial) + end + + -- we so need a better way to do this + for i,v in pairs(materialNames) do + materialToImageMap[v] = {} + if v == "Grass" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=56563112" + elseif v == "Sand" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=62356652" + elseif v == "Brick" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=65961537" + elseif v == "Granite" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=67532153" + elseif v == "Asphalt" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=67532038" + elseif v == "Iron" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=67532093" + elseif v == "Aluminum" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=67531995" + elseif v == "Gold" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=67532118" + elseif v == "Plastic (red)" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=67531848" + elseif v == "Plastic (blue)" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=67531924" + elseif v == "Plank" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=67532015" + elseif v == "Log" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=67532051" + elseif v == "Gravel" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=67532206" + elseif v == "Cinder Block" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=67532103" + elseif v == "Stone Wall" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=67531804" + elseif v == "Concrete" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=67532059" + elseif v == "Water" then materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=81407474" + else materialToImageMap[v].Regular = "https://www.roblox.com/asset/?id=66887593" -- fill in the rest here!! + end + end + + local scrollFrame, scrollUp, scrollDown, recalculateScroll = t.CreateScrollingFrame(nil,"grid") + scrollFrame.Size = UDim2.new(0.85,0,1,0) + scrollFrame.Position = UDim2.new(0,0,0,0) + scrollFrame.Parent = frame + + scrollUp.Parent = frame + scrollUp.Visible = true + scrollUp.Position = UDim2.new(1,-19,0,0) + + scrollDown.Parent = frame + scrollDown.Visible = true + scrollDown.Position = UDim2.new(1,-19,1,-17) + + local function goToNewMaterial(buttonWrap, materialName) + updateMaterialChoice(materialName) + buttonWrap.BackgroundTransparency = 0 + selectedButton.BackgroundTransparency = 1 + selectedButton = buttonWrap + end + + local function createMaterialButton(name) + local buttonWrap = Instance.new("TextButton") + buttonWrap.Text = "" + buttonWrap.Size = UDim2.new(0,32,0,32) + buttonWrap.BackgroundColor3 = Color3.new(1,1,1) + buttonWrap.BorderSizePixel = 0 + buttonWrap.BackgroundTransparency = 1 + buttonWrap.AutoButtonColor = false + buttonWrap.Name = tostring(name) + + local imageButton = Instance.new("ImageButton") + imageButton.AutoButtonColor = false + imageButton.BackgroundTransparency = 1 + imageButton.Size = UDim2.new(0,30,0,30) + imageButton.Position = UDim2.new(0,1,0,1) + imageButton.Name = tostring(name) + imageButton.Parent = buttonWrap + imageButton.Image = materialToImageMap[name].Regular + + local enumType = Instance.new("NumberValue") + enumType.Name = "EnumType" + enumType.Parent = buttonWrap + enumType.Value = 0 + + imageButton.MouseEnter:connect(function() + buttonWrap.BackgroundTransparency = 0 + end) + imageButton.MouseLeave:connect(function() + if selectedButton ~= buttonWrap then + buttonWrap.BackgroundTransparency = 1 + end + end) + imageButton.MouseButton1Click:connect(function() + if selectedButton ~= buttonWrap then + goToNewMaterial(buttonWrap, tostring(name)) + end + end) + + return buttonWrap + end + + for i = 1, #materialNames do + local imageButton = createMaterialButton(materialNames[i]) + + if materialNames[i] == "Grass" then -- always start with grass as the default + selectedButton = imageButton + imageButton.BackgroundTransparency = 0 + end + + imageButton.Parent = scrollFrame + end + + local forceTerrainMaterialSelection = function(newMaterialType) + if not newMaterialType then return end + if currentMaterial == newMaterialType then return end + + local matName = getNameFromEnum(newMaterialType) + local buttons = scrollFrame:GetChildren() + for i = 1, #buttons do + if buttons[i].Name == "Plastic (blue)" and matName == "Plastic (blue)" then goToNewMaterial(buttons[i],matName) return end + if buttons[i].Name == "Plastic (red)" and matName == "Plastic (red)" then goToNewMaterial(buttons[i],matName) return end + if string.find(buttons[i].Name, matName) then + goToNewMaterial(buttons[i],matName) + return + end + end + end + + frame.Changed:connect(function ( prop ) + if prop == "AbsoluteSize" then + recalculateScroll() + end + end) + + recalculateScroll() + return frame, terrainMaterialSelectionChanged, forceTerrainMaterialSelection +end + +t.CreateLoadingFrame = function(name,size,position) + game:GetService("ContentProvider"):Preload("https://www.roblox.com/asset/?id=35238053") + + local loadingFrame = Instance.new("Frame") + loadingFrame.Name = "LoadingFrame" + loadingFrame.Style = Enum.FrameStyle.RobloxRound + + if size then loadingFrame.Size = size + else loadingFrame.Size = UDim2.new(0,300,0,160) end + if position then loadingFrame.Position = position + else loadingFrame.Position = UDim2.new(0.5, -150, 0.5,-80) end + + local loadingBar = Instance.new("Frame") + loadingBar.Name = "LoadingBar" + loadingBar.BackgroundColor3 = Color3.new(0,0,0) + loadingBar.BorderColor3 = Color3.new(79/255,79/255,79/255) + loadingBar.Position = UDim2.new(0,0,0,41) + loadingBar.Size = UDim2.new(1,0,0,30) + loadingBar.Parent = loadingFrame + + local loadingGreenBar = Instance.new("ImageLabel") + loadingGreenBar.Name = "LoadingGreenBar" + loadingGreenBar.Image = "https://www.roblox.com/asset/?id=35238053" + loadingGreenBar.Position = UDim2.new(0,0,0,0) + loadingGreenBar.Size = UDim2.new(0,0,1,0) + loadingGreenBar.Visible = false + loadingGreenBar.Parent = loadingBar + + local loadingPercent = Instance.new("TextLabel") + loadingPercent.Name = "LoadingPercent" + loadingPercent.BackgroundTransparency = 1 + loadingPercent.Position = UDim2.new(0,0,1,0) + loadingPercent.Size = UDim2.new(1,0,0,14) + loadingPercent.Font = Enum.Font.Arial + loadingPercent.Text = "0%" + loadingPercent.FontSize = Enum.FontSize.Size14 + loadingPercent.TextColor3 = Color3.new(1,1,1) + loadingPercent.Parent = loadingBar + + local cancelButton = Instance.new("TextButton") + cancelButton.Name = "CancelButton" + cancelButton.Position = UDim2.new(0.5,-60,1,-40) + cancelButton.Size = UDim2.new(0,120,0,40) + cancelButton.Font = Enum.Font.Arial + cancelButton.FontSize = Enum.FontSize.Size18 + cancelButton.TextColor3 = Color3.new(1,1,1) + cancelButton.Text = "Cancel" + cancelButton.Style = Enum.ButtonStyle.RobloxButton + cancelButton.Parent = loadingFrame + + local loadingName = Instance.new("TextLabel") + loadingName.Name = "loadingName" + loadingName.BackgroundTransparency = 1 + loadingName.Size = UDim2.new(1,0,0,18) + loadingName.Position = UDim2.new(0,0,0,2) + loadingName.Font = Enum.Font.Arial + loadingName.Text = name + loadingName.TextColor3 = Color3.new(1,1,1) + loadingName.TextStrokeTransparency = 1 + loadingName.FontSize = Enum.FontSize.Size18 + loadingName.Parent = loadingFrame + + local cancelButtonClicked = Instance.new("BindableEvent") + cancelButtonClicked.Name = "CancelButtonClicked" + cancelButtonClicked.Parent = cancelButton + cancelButton.MouseButton1Click:connect(function() + cancelButtonClicked:Fire() + end) + + local updateLoadingGuiPercent = function(percent, tweenAction, tweenLength) + if percent and type(percent) ~= "number" then + error("updateLoadingGuiPercent expects number as argument, got",type(percent),"instead") + end + + local newSize = nil + if percent < 0 then + newSize = UDim2.new(0,0,1,0) + elseif percent > 1 then + newSize = UDim2.new(1,0,1,0) + else + newSize = UDim2.new(percent,0,1,0) + end + + if tweenAction then + if not tweenLength then + error("updateLoadingGuiPercent is set to tween new percentage, but got no tween time length! Please pass this in as third argument") + end + + if (newSize.X.Scale > 0) then + loadingGreenBar.Visible = true + loadingGreenBar:TweenSize( newSize, + Enum.EasingDirection.Out, + Enum.EasingStyle.Quad, + tweenLength, + true) + else + loadingGreenBar:TweenSize( newSize, + Enum.EasingDirection.Out, + Enum.EasingStyle.Quad, + tweenLength, + true, + function() + if (newSize.X.Scale < 0) then + loadingGreenBar.Visible = false + end + end) + end + + else + loadingGreenBar.Size = newSize + loadingGreenBar.Visible = (newSize.X.Scale > 0) + end + end + + loadingGreenBar.Changed:connect(function(prop) + if prop == "Size" then + loadingPercent.Text = tostring( math.ceil(loadingGreenBar.Size.X.Scale * 100) ) .. "%" + end + end) + + return loadingFrame, updateLoadingGuiPercent, cancelButtonClicked +end + +t.CreatePluginFrame = function (name,size,position,scrollable,parent) + local function createMenuButton(size,position,text,fontsize,name,parent) + local button = Instance.new("TextButton",parent) + button.AutoButtonColor = false + button.Name = name + button.BackgroundTransparency = 1 + button.Position = position + button.Size = size + button.Font = Enum.Font.ArialBold + button.FontSize = fontsize + button.Text = text + button.TextColor3 = Color3.new(1,1,1) + button.BorderSizePixel = 0 + button.BackgroundColor3 = Color3.new(20/255,20/255,20/255) + + button.MouseEnter:connect(function ( ) + if button.Selected then return end + button.BackgroundTransparency = 0 + end) + button.MouseLeave:connect(function ( ) + if button.Selected then return end + button.BackgroundTransparency = 1 + end) + + return button + + end + + local dragBar = Instance.new("Frame",parent) + dragBar.Name = tostring(name) .. "DragBar" + dragBar.BackgroundColor3 = Color3.new(39/255,39/255,39/255) + dragBar.BorderColor3 = Color3.new(0,0,0) + if size then + dragBar.Size = UDim2.new(size.X.Scale,size.X.Offset,0,20) + UDim2.new(0,20,0,0) + else + dragBar.Size = UDim2.new(0,183,0,20) + end + if position then + dragBar.Position = position + end + dragBar.Active = true + dragBar.Draggable = true + --dragBar.Visible = false + dragBar.MouseEnter:connect(function ( ) + dragBar.BackgroundColor3 = Color3.new(49/255,49/255,49/255) + end) + dragBar.MouseLeave:connect(function ( ) + dragBar.BackgroundColor3 = Color3.new(39/255,39/255,39/255) + end) + + -- plugin name label + local pluginNameLabel = Instance.new("TextLabel",dragBar) + pluginNameLabel.Name = "BarNameLabel" + pluginNameLabel.Text = " " .. tostring(name) + pluginNameLabel.TextColor3 = Color3.new(1,1,1) + pluginNameLabel.TextStrokeTransparency = 0 + pluginNameLabel.Size = UDim2.new(1,0,1,0) + pluginNameLabel.Font = Enum.Font.ArialBold + pluginNameLabel.FontSize = Enum.FontSize.Size18 + pluginNameLabel.TextXAlignment = Enum.TextXAlignment.Left + pluginNameLabel.BackgroundTransparency = 1 + + -- close button + local closeButton = createMenuButton(UDim2.new(0,15,0,17),UDim2.new(1,-16,0.5,-8),"X",Enum.FontSize.Size14,"CloseButton",dragBar) + local closeEvent = Instance.new("BindableEvent") + closeEvent.Name = "CloseEvent" + closeEvent.Parent = closeButton + closeButton.MouseButton1Click:connect(function () + closeEvent:Fire() + closeButton.BackgroundTransparency = 1 + end) + + -- help button + local helpButton = createMenuButton(UDim2.new(0,15,0,17),UDim2.new(1,-51,0.5,-8),"?",Enum.FontSize.Size14,"HelpButton",dragBar) + local helpFrame = Instance.new("Frame",dragBar) + helpFrame.Name = "HelpFrame" + helpFrame.BackgroundColor3 = Color3.new(0,0,0) + helpFrame.Size = UDim2.new(0,300,0,552) + helpFrame.Position = UDim2.new(1,5,0,0) + helpFrame.Active = true + helpFrame.BorderSizePixel = 0 + helpFrame.Visible = false + + helpButton.MouseButton1Click:connect(function( ) + helpFrame.Visible = not helpFrame.Visible + if helpFrame.Visible then + helpButton.Selected = true + helpButton.BackgroundTransparency = 0 + local screenGui = getLayerCollectorAncestor(helpFrame) + if screenGui then + if helpFrame.AbsolutePosition.X + helpFrame.AbsoluteSize.X > screenGui.AbsoluteSize.X then --position on left hand side + helpFrame.Position = UDim2.new(0,-5 - helpFrame.AbsoluteSize.X,0,0) + else -- position on right hand side + helpFrame.Position = UDim2.new(1,5,0,0) + end + else + helpFrame.Position = UDim2.new(1,5,0,0) + end + else + helpButton.Selected = false + helpButton.BackgroundTransparency = 1 + end + end) + + local minimizeButton = createMenuButton(UDim2.new(0,16,0,17),UDim2.new(1,-34,0.5,-8),"-",Enum.FontSize.Size14,"MinimizeButton",dragBar) + minimizeButton.TextYAlignment = Enum.TextYAlignment.Top + + local minimizeFrame = Instance.new("Frame",dragBar) + minimizeFrame.Name = "MinimizeFrame" + minimizeFrame.BackgroundColor3 = Color3.new(73/255,73/255,73/255) + minimizeFrame.BorderColor3 = Color3.new(0,0,0) + minimizeFrame.Position = UDim2.new(0,0,1,0) + if size then + minimizeFrame.Size = UDim2.new(size.X.Scale,size.X.Offset,0,50) + UDim2.new(0,20,0,0) + else + minimizeFrame.Size = UDim2.new(0,183,0,50) + end + minimizeFrame.Visible = false + + local minimizeBigButton = Instance.new("TextButton",minimizeFrame) + minimizeBigButton.Position = UDim2.new(0.5,-50,0.5,-20) + minimizeBigButton.Name = "MinimizeButton" + minimizeBigButton.Size = UDim2.new(0,100,0,40) + minimizeBigButton.Style = Enum.ButtonStyle.RobloxButton + minimizeBigButton.Font = Enum.Font.ArialBold + minimizeBigButton.FontSize = Enum.FontSize.Size18 + minimizeBigButton.TextColor3 = Color3.new(1,1,1) + minimizeBigButton.Text = "Show" + + local separatingLine = Instance.new("Frame",dragBar) + separatingLine.Name = "SeparatingLine" + separatingLine.BackgroundColor3 = Color3.new(115/255,115/255,115/255) + separatingLine.BorderSizePixel = 0 + separatingLine.Position = UDim2.new(1,-18,0.5,-7) + separatingLine.Size = UDim2.new(0,1,0,14) + + local otherSeparatingLine = separatingLine:clone() + otherSeparatingLine.Position = UDim2.new(1,-35,0.5,-7) + otherSeparatingLine.Parent = dragBar + + local widgetContainer = Instance.new("Frame",dragBar) + widgetContainer.Name = "WidgetContainer" + widgetContainer.BackgroundTransparency = 1 + widgetContainer.Position = UDim2.new(0,0,1,0) + widgetContainer.BorderColor3 = Color3.new(0,0,0) + if not scrollable then + widgetContainer.BackgroundTransparency = 0 + widgetContainer.BackgroundColor3 = Color3.new(72/255,72/255,72/255) + end + + if size then + if scrollable then + widgetContainer.Size = size + else + widgetContainer.Size = UDim2.new(0,dragBar.AbsoluteSize.X,size.Y.Scale,size.Y.Offset) + end + else + if scrollable then + widgetContainer.Size = UDim2.new(0,163,0,400) + else + widgetContainer.Size = UDim2.new(0,dragBar.AbsoluteSize.X,0,400) + end + end + if position then + widgetContainer.Position = position + UDim2.new(0,0,0,20) + end + + local frame,control,verticalDragger = nil + if scrollable then + --frame for widgets + frame,control = t.CreateTrueScrollingFrame() + frame.Size = UDim2.new(1, 0, 1, 0) + frame.BackgroundColor3 = Color3.new(72/255,72/255,72/255) + frame.BorderColor3 = Color3.new(0,0,0) + frame.Active = true + frame.Parent = widgetContainer + control.Parent = dragBar + control.BackgroundColor3 = Color3.new(72/255,72/255,72/255) + control.BorderSizePixel = 0 + control.BackgroundTransparency = 0 + control.Position = UDim2.new(1,-21,1,1) + if size then + control.Size = UDim2.new(0,21,size.Y.Scale,size.Y.Offset) + else + control.Size = UDim2.new(0,21,0,400) + end + control:FindFirstChild("ScrollDownButton").Position = UDim2.new(0,0,1,-20) + + local fakeLine = Instance.new("Frame",control) + fakeLine.Name = "FakeLine" + fakeLine.BorderSizePixel = 0 + fakeLine.BackgroundColor3 = Color3.new(0,0,0) + fakeLine.Size = UDim2.new(0,1,1,1) + fakeLine.Position = UDim2.new(1,0,0,0) + + verticalDragger = Instance.new("TextButton",widgetContainer) + verticalDragger.ZIndex = 2 + verticalDragger.AutoButtonColor = false + verticalDragger.Name = "VerticalDragger" + verticalDragger.BackgroundColor3 = Color3.new(50/255,50/255,50/255) + verticalDragger.BorderColor3 = Color3.new(0,0,0) + verticalDragger.Size = UDim2.new(1,20,0,20) + verticalDragger.Position = UDim2.new(0,0,1,0) + verticalDragger.Active = true + verticalDragger.Text = "" + + local scrubFrame = Instance.new("Frame",verticalDragger) + scrubFrame.Name = "ScrubFrame" + scrubFrame.BackgroundColor3 = Color3.new(1,1,1) + scrubFrame.BorderSizePixel = 0 + scrubFrame.Position = UDim2.new(0.5,-5,0.5,0) + scrubFrame.Size = UDim2.new(0,10,0,1) + scrubFrame.ZIndex = 5 + local scrubTwo = scrubFrame:clone() + scrubTwo.Position = UDim2.new(0.5,-5,0.5,-2) + scrubTwo.Parent = verticalDragger + local scrubThree = scrubFrame:clone() + scrubThree.Position = UDim2.new(0.5,-5,0.5,2) + scrubThree.Parent = verticalDragger + + local areaSoak = Instance.new("TextButton",getLayerCollectorAncestor(parent)) + areaSoak.Name = "AreaSoak" + areaSoak.Size = UDim2.new(1,0,1,0) + areaSoak.BackgroundTransparency = 1 + areaSoak.BorderSizePixel = 0 + areaSoak.Text = "" + areaSoak.ZIndex = 10 + areaSoak.Visible = false + areaSoak.Active = true + + local draggingVertical = false + local startYPos = nil + verticalDragger.MouseEnter:connect(function () + verticalDragger.BackgroundColor3 = Color3.new(60/255,60/255,60/255) + end) + verticalDragger.MouseLeave:connect(function () + verticalDragger.BackgroundColor3 = Color3.new(50/255,50/255,50/255) + end) + verticalDragger.MouseButton1Down:connect(function(x,y) + draggingVertical = true + areaSoak.Visible = true + startYPos = y + end) + areaSoak.MouseButton1Up:connect(function ( ) + draggingVertical = false + areaSoak.Visible = false + end) + areaSoak.MouseMoved:connect(function(x,y) + if not draggingVertical then return end + + local yDelta = y - startYPos + if not control.ScrollDownButton.Visible and yDelta > 0 then + return + end + + if (widgetContainer.Size.Y.Offset + yDelta) < 150 then + widgetContainer.Size = UDim2.new(widgetContainer.Size.X.Scale, widgetContainer.Size.X.Offset,widgetContainer.Size.Y.Scale,150) + control.Size = UDim2.new (0,21,0,150) + return + end + + startYPos = y + + if widgetContainer.Size.Y.Offset + yDelta >= 0 then + widgetContainer.Size = UDim2.new(widgetContainer.Size.X.Scale, widgetContainer.Size.X.Offset,widgetContainer.Size.Y.Scale,widgetContainer.Size.Y.Offset + yDelta) + control.Size = UDim2.new(0,21,0,control.Size.Y.Offset + yDelta ) + end + end) + end + + local function switchMinimize() + minimizeFrame.Visible = not minimizeFrame.Visible + if scrollable then + frame.Visible = not frame.Visible + verticalDragger.Visible = not verticalDragger.Visible + control.Visible = not control.Visible + else + widgetContainer.Visible = not widgetContainer.Visible + end + + if minimizeFrame.Visible then + minimizeButton.Text = "+" + else + minimizeButton.Text = "-" + end + end + + minimizeBigButton.MouseButton1Click:connect(function ( ) + switchMinimize() + end) + + minimizeButton.MouseButton1Click:connect(function( ) + switchMinimize() + end) + + if scrollable then + return dragBar, frame, helpFrame, closeEvent + else + return dragBar, widgetContainer, helpFrame, closeEvent + end +end + +t.Help = + function(funcNameOrFunc) + --input argument can be a string or a function. Should return a description (of arguments and expected side effects) + if funcNameOrFunc == "CreatePropertyDropDownMenu" or funcNameOrFunc == t.CreatePropertyDropDownMenu then + return "Function CreatePropertyDropDownMenu. " .. + "Arguments: (instance, propertyName, enumType). " .. + "Side effect: returns a container with a drop-down-box that is linked to the 'property' field of 'instance' which is of type 'enumType'" + end + if funcNameOrFunc == "CreateDropDownMenu" or funcNameOrFunc == t.CreateDropDownMenu then + return "Function CreateDropDownMenu. " .. + "Arguments: (items, onItemSelected). " .. + "Side effect: Returns 2 results, a container to the gui object and a 'updateSelection' function for external updating. The container is a drop-down-box created around a list of items" + end + if funcNameOrFunc == "CreateMessageDialog" or funcNameOrFunc == t.CreateMessageDialog then + return "Function CreateMessageDialog. " .. + "Arguments: (title, message, buttons). " .. + "Side effect: Returns a gui object of a message box with 'title' and 'message' as passed in. 'buttons' input is an array of Tables contains a 'Text' and 'Function' field for the text/callback of each button" + end + if funcNameOrFunc == "CreateStyledMessageDialog" or funcNameOrFunc == t.CreateStyledMessageDialog then + return "Function CreateStyledMessageDialog. " .. + "Arguments: (title, message, style, buttons). " .. + "Side effect: Returns a gui object of a message box with 'title' and 'message' as passed in. 'buttons' input is an array of Tables contains a 'Text' and 'Function' field for the text/callback of each button, 'style' is a string, either Error, Notify or Confirm" + end + if funcNameOrFunc == "GetFontHeight" or funcNameOrFunc == t.GetFontHeight then + return "Function GetFontHeight. " .. + "Arguments: (font, fontSize). " .. + "Side effect: returns the size in pixels of the given font + fontSize" + end + if funcNameOrFunc == "LayoutGuiObjects" or funcNameOrFunc == t.LayoutGuiObjects then + + end + if funcNameOrFunc == "CreateScrollingFrame" or funcNameOrFunc == t.CreateScrollingFrame then + return "Function CreateScrollingFrame. " .. + "Arguments: (orderList, style) " .. + "Side effect: returns 4 objects, (scrollFrame, scrollUpButton, scrollDownButton, recalculateFunction). 'scrollFrame' can be filled with GuiObjects. It will lay them out and allow scrollUpButton/scrollDownButton to interact with them. Orderlist is optional (and specifies the order to layout the children. Without orderlist, it uses the children order. style is also optional, and allows for a 'grid' styling if style is passed 'grid' as a string. recalculateFunction can be called when a relayout is needed (when orderList changes)" + end + if funcNameOrFunc == "CreateTrueScrollingFrame" or funcNameOrFunc == t.CreateTrueScrollingFrame then + return "Function CreateTrueScrollingFrame. " .. + "Arguments: (nil) " .. + "Side effect: returns 2 objects, (scrollFrame, controlFrame). 'scrollFrame' can be filled with GuiObjects, and they will be clipped if not inside the frame's bounds. controlFrame has children scrollup and scrolldown, as well as a slider. controlFrame can be parented to any guiobject and it will readjust itself to fit." + end + if funcNameOrFunc == "AutoTruncateTextObject" or funcNameOrFunc == t.AutoTruncateTextObject then + return "Function AutoTruncateTextObject. " .. + "Arguments: (textLabel) " .. + "Side effect: returns 2 objects, (textLabel, changeText). The 'textLabel' input is modified to automatically truncate text (with ellipsis), if it gets too small to fit. 'changeText' is a function that can be used to change the text, it takes 1 string as an argument" + end + if funcNameOrFunc == "CreateSlider" or funcNameOrFunc == t.CreateSlider then + return "Function CreateSlider. " .. + "Arguments: (steps, width, position) " .. + "Side effect: returns 2 objects, (sliderGui, sliderPosition). The 'steps' argument specifies how many different positions the slider can hold along the bar. 'width' specifies in pixels how wide the bar should be (modifiable afterwards if desired). 'position' argument should be a UDim2 for slider positioning. 'sliderPosition' is an IntValue whose current .Value specifies the specific step the slider is currently on." + end + if funcNameOrFunc == "CreateSliderNew" or funcNameOrFunc == t.CreateSliderNew then + return "Function CreateSliderNew. " .. + "Arguments: (steps, width, position) " .. + "Side effect: returns 2 objects, (sliderGui, sliderPosition). The 'steps' argument specifies how many different positions the slider can hold along the bar. 'width' specifies in pixels how wide the bar should be (modifiable afterwards if desired). 'position' argument should be a UDim2 for slider positioning. 'sliderPosition' is an IntValue whose current .Value specifies the specific step the slider is currently on." + end + if funcNameOrFunc == "CreateLoadingFrame" or funcNameOrFunc == t.CreateLoadingFrame then + return "Function CreateLoadingFrame. " .. + "Arguments: (name, size, position) " .. + "Side effect: Creates a gui that can be manipulated to show progress for a particular action. Name appears above the loading bar, and size and position are udim2 values (both size and position are optional arguments). Returns 3 arguments, the first being the gui created. The second being updateLoadingGuiPercent, which is a bindable function. This function takes one argument (two optionally), which should be a number between 0 and 1, representing the percentage the loading gui should be at. The second argument to this function is a boolean value that if set to true will tween the current percentage value to the new percentage value, therefore our third argument is how long this tween should take. Our third returned argument is a BindableEvent, that when fired means that someone clicked the cancel button on the dialog." + end + if funcNameOrFunc == "CreateTerrainMaterialSelector" or funcNameOrFunc == t.CreateTerrainMaterialSelector then + return "Function CreateTerrainMaterialSelector. " .. + "Arguments: (size, position) " .. + "Side effect: Size and position are UDim2 values that specifies the selector's size and position. Both size and position are optional arguments. This method returns 3 objects (terrainSelectorGui, terrainSelected, forceTerrainSelection). terrainSelectorGui is just the gui object that we generate with this function, parent it as you like. TerrainSelected is a BindableEvent that is fired whenever a new terrain type is selected in the gui. ForceTerrainSelection is a function that takes an argument of Enum.CellMaterial and will force the gui to show that material as currently selected." + end + end + +return t diff --git a/Client2018/content/scripts/CoreScripts/Libraries/RbxStamper.lua b/Client2018/content/scripts/CoreScripts/Libraries/RbxStamper.lua new file mode 100644 index 0000000..997c4e6 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Libraries/RbxStamper.lua @@ -0,0 +1,2195 @@ +local t = {} + +-- Do a line/plane intersection. The line starts at the camera. The plane is at y == 0, normal(0, 1, 0) +-- +-- vectorPos - End point of the line. +-- +-- Return: +-- cellPos - The terrain cell intersection point if there is one, vectorPos if there isn't. +-- hit - Whether there was a plane intersection. Value is true if there was, false if not. +function PlaneIntersection(vectorPos) + local hit = false + local currCamera = game:GetService("Workspace").CurrentCamera + local startPos = Vector3.new(currCamera.CoordinateFrame.p.X, currCamera.CoordinateFrame.p.Y, currCamera.CoordinateFrame.p.Z) + local endPos = Vector3.new(vectorPos.X, vectorPos.Y, vectorPos.Z) + local normal = Vector3.new(0, 1, 0) + local p3 = Vector3.new(0, 0, 0) + local startEndDot = normal:Dot(endPos - startPos) + local cellPos = vectorPos + if startEndDot ~= 0 then + local t = normal:Dot(p3 - startPos) / startEndDot + if(t >=0 and t <=1) then + local intersection = ((endPos - startPos) * t) + startPos + cellPos = game:GetService("Workspace").Terrain:WorldToCell(intersection) + hit = true + end + end + + return cellPos, hit +end + + +-- Purpose: +-- Checks for terrain touched by the mouse hit. +-- Will do a plane intersection if no terrain is touched. +-- +-- mouse - Mouse to check the .hit for. +-- +-- Return: +-- cellPos - Cell position hit. Nil if none. +function GetTerrainForMouse(mouse) + -- There was no target, so all it could be is a plane intersection. + -- Check for a plane intersection. If there isn't one then nothing will get hit. + local cell = game:GetService("Workspace").Terrain:WorldToCellPreferSolid(Vector3.new(mouse.hit.x, mouse.hit.y, mouse.hit.z)) + local planeLoc = nil + local hit = nil + -- If nothing was hit, do the plane intersection. + if 0 == game:GetService("Workspace").Terrain:GetCell(cell.X, cell.Y, cell.Z).Value then + cell = nil + planeLoc, hit = PlaneIntersection(Vector3.new(mouse.hit.x, mouse.hit.y, mouse.hit.z)) + if hit then + cell = planeLoc + end + end + return cell +end + +-- setup helper functions +local insertBoundingBoxOverlapVector = Vector3.new(.3, .3, .3) -- we can still stamp if our character extrudes into the target stamping space by .3 or fewer units + +-- rotates a model by yAngle radians about the global y-axis +local function rotatePartAndChildren(part, rotCF, offsetFromOrigin) + -- rotate this thing, if it's a part + if part:IsA("BasePart") then + part.CFrame = (rotCF * (part.CFrame - offsetFromOrigin)) + offsetFromOrigin + end + + -- recursively do the same to all children + local partChildren = part:GetChildren() + for c = 1, #partChildren do rotatePartAndChildren(partChildren[c], rotCF, offsetFromOrigin) end +end + +local function modelRotate(model, yAngle) + local rotCF = CFrame.Angles(0, yAngle, 0) + local offsetFromOrigin = model:GetModelCFrame().p + + rotatePartAndChildren(model, rotCF, offsetFromOrigin) +end + + +local function collectParts(object, baseParts, scripts, decals) + if object:IsA("BasePart") then + baseParts[#baseParts+1] = object + elseif object:IsA("Script") then + scripts[#scripts+1] = object + elseif object:IsA("Decal") then + decals[#decals+1] = object + end + + for index,child in pairs(object:GetChildren()) do + collectParts(child, baseParts, scripts, decals) + end +end + +local function clusterPartsInRegion(startVector, endVector) + local cluster = game:GetService("Workspace"):FindFirstChild("Terrain") + + local startCell = cluster:WorldToCell(startVector) + local endCell = cluster:WorldToCell(endVector) + + local startX = startCell.X + local startY = startCell.Y + local startZ = startCell.Z + + local endX = endCell.X + local endY = endCell.Y + local endZ = endCell.Z + + if startX < cluster.MaxExtents.Min.X then startX = cluster.MaxExtents.Min.X end + if startY < cluster.MaxExtents.Min.Y then startY = cluster.MaxExtents.Min.Y end + if startZ < cluster.MaxExtents.Min.Z then startZ = cluster.MaxExtents.Min.Z end + + if endX > cluster.MaxExtents.Max.X then endX = cluster.MaxExtents.Max.X end + if endY > cluster.MaxExtents.Max.Y then endY = cluster.MaxExtents.Max.Y end + if endZ > cluster.MaxExtents.Max.Z then endZ = cluster.MaxExtents.Max.Z end + + for x = startX, endX do + for y = startY, endY do + for z = startZ, endZ do + if (cluster:GetCell(x, y, z).Value) > 0 then return true end + end + end + end + + return false +end + +local function findSeatsInModel(parent, seatTable) + if not parent then return end + + if parent.className == "Seat" or parent.className == "VehicleSeat" then + table.insert(seatTable, parent) + end + local myChildren = parent:GetChildren() + for j = 1, #myChildren do + findSeatsInModel(myChildren[j], seatTable) + end +end + +local function setSeatEnabledStatus(model, isEnabled) + local seatList = {} + findSeatsInModel(model, seatList) + + if isEnabled then + -- remove any welds called "SeatWeld" in seats + for i = 1, #seatList do + local nextSeat = seatList[i]:FindFirstChild("SeatWeld") + while nextSeat do nextSeat:Remove() nextSeat = seatList[i]:FindFirstChild("SeatWeld") end + end + else + -- put a weld called "SeatWeld" in every seat + -- this tricks it into thinking there's already someone sitting there, and it won't make you sit XD + for i = 1, #seatList do + local fakeWeld = Instance.new("Weld") + fakeWeld.Name = "SeatWeld" + fakeWeld.Parent = seatList[i] + end + end +end + +local function autoAlignToFace(parts) + local aatf = parts:FindFirstChild("AutoAlignToFace") + if aatf then return aatf.Value else return false end +end + +local function getClosestAlignedWorldDirection(aVector3InWorld) + local xDir = Vector3.new(1,0,0) + local yDir = Vector3.new(0,1,0) + local zDir = Vector3.new(0,0,1) + local xDot = aVector3InWorld.x * xDir.x + aVector3InWorld.y * xDir.y + aVector3InWorld.z * xDir.z + local yDot = aVector3InWorld.x * yDir.x + aVector3InWorld.y * yDir.y + aVector3InWorld.z * yDir.z + local zDot = aVector3InWorld.x * zDir.x + aVector3InWorld.y * zDir.y + aVector3InWorld.z * zDir.z + + if math.abs(xDot) > math.abs(yDot) and math.abs(xDot) > math.abs(zDot) then + if xDot > 0 then + return 0 + else + return 3 + end + elseif math.abs(yDot) > math.abs(xDot) and math.abs(yDot) > math.abs(zDot) then + if yDot > 0 then + return 1 + else + return 4 + end + else + if zDot > 0 then + return 2 + else + return 5 + end + end +end + +local function positionPartsAtCFrame3(aCFrame, currentParts) + local insertCFrame = nil + if not currentParts then return currentParts end + if currentParts and (currentParts:IsA("Model") or currentParts:IsA("Tool")) then + insertCFrame = currentParts:GetModelCFrame() + currentParts:TranslateBy(aCFrame.p - insertCFrame.p) + else + currentParts.CFrame = aCFrame + end + return currentParts +end + +local function calcRayHitTime(rayStart, raySlope, intersectionPlane) + if math.abs(raySlope) < .01 then return 0 end -- 0 slope --> we just say intersection time is 0, and sidestep this dimension + return (intersectionPlane - rayStart) / raySlope +end + +local function modelTargetSurface(partOrModel, rayStart, rayEnd) + if not partOrModel then + return 0 + end + + local modelCFrame = nil + local modelSize = nil + if partOrModel:IsA("Model") then + modelCFrame = partOrModel:GetModelCFrame() + modelSize = partOrModel:GetModelSize() + else + modelCFrame = partOrModel.CFrame + modelSize = partOrModel.Size + end + + local mouseRayStart = modelCFrame:pointToObjectSpace(rayStart) + local mouseRayEnd = modelCFrame:pointToObjectSpace(rayEnd) + local mouseSlope = mouseRayEnd - mouseRayStart + + local xPositive = 1 + local yPositive = 1 + local zPositive = 1 + if mouseSlope.X > 0 then xPositive = -1 end + if mouseSlope.Y > 0 then yPositive = -1 end + if mouseSlope.Z > 0 then zPositive = -1 end + + -- find which surface the transformed mouse ray hits (using modelSize): + local xHitTime = calcRayHitTime(mouseRayStart.X, mouseSlope.X, modelSize.X/2 * xPositive) + local yHitTime = calcRayHitTime(mouseRayStart.Y, mouseSlope.Y, modelSize.Y/2 * yPositive) + local zHitTime = calcRayHitTime(mouseRayStart.Z, mouseSlope.Z, modelSize.Z/2 * zPositive) + + local hitFace = 0 + + --if xHitTime >= 0 and yHitTime >= 0 and zHitTime >= 0 then + if xHitTime > yHitTime then + if xHitTime > zHitTime then + -- xFace is hit + hitFace = 1*xPositive + else + -- zFace is hit + hitFace = 3*zPositive + end + else + if yHitTime > zHitTime then + -- yFace is hit + hitFace = 2*yPositive + else + -- zFace is hit + hitFace = 3*zPositive + end + end + + return hitFace +end + +local function getBoundingBox2(partOrModel) + + -- for models, the bounding box is defined as the minimum and maximum individual part bounding boxes + -- relative to the first part's coordinate frame. + local minVec = Vector3.new(math.huge, math.huge, math.huge) + local maxVec = Vector3.new(-math.huge, -math.huge, -math.huge) + + if partOrModel:IsA("Terrain") then + minVec = Vector3.new(-2, -2, -2) + maxVec = Vector3.new(2, 2, 2) + elseif partOrModel:IsA("BasePart") then + minVec = -0.5 * partOrModel.Size + maxVec = -minVec + else + maxVec = partOrModel:GetModelSize()*0.5 + minVec = -maxVec + end + + -- Adjust bounding box to reflect what the model or part author wants in terms of justification + local justifyValue = partOrModel:FindFirstChild("Justification") + if justifyValue ~= nil then + -- find the multiple of 4 that contains the model + local justify = justifyValue.Value + local two = Vector3.new(2, 2, 2) + local actualBox = maxVec - minVec - Vector3.new(0.01, 0.01, 0.01) + local containingGridBox = Vector3.new(4 * math.ceil(actualBox.x/4), 4 * math.ceil(actualBox.y/4), 4 * math.ceil(actualBox.z/4)) + local adjustment = containingGridBox - actualBox + minVec = minVec - 0.5 * adjustment * justify + maxVec = maxVec + 0.5 * adjustment * (two - justify) + end + + return minVec, maxVec +end + +local function getBoundingBoxInWorldCoordinates(partOrModel) + local minVec = Vector3.new(math.huge, math.huge, math.huge) + local maxVec = Vector3.new(-math.huge, -math.huge, -math.huge) + + if partOrModel:IsA("BasePart") and not partOrModel:IsA("Terrain") then + local vec1 = partOrModel.CFrame:pointToWorldSpace(-0.5 * partOrModel.Size) + local vec2 = partOrModel.CFrame:pointToWorldSpace(0.5 * partOrModel.Size) + minVec = Vector3.new(math.min(vec1.X, vec2.X), math.min(vec1.Y, vec2.Y), math.min(vec1.Z, vec2.Z)) + maxVec = Vector3.new(math.max(vec1.X, vec2.X), math.max(vec1.Y, vec2.Y), math.max(vec1.Z, vec2.Z)) + elseif partOrModel:IsA("Terrain") then + -- we shouldn't have to deal with this case + --minVec = Vector3.new(-2, -2, -2) + --maxVec = Vector3.new(2, 2, 2) + else + local vec1 = partOrModel:GetModelCFrame():pointToWorldSpace(-0.5 * partOrModel:GetModelSize()) + local vec2 = partOrModel:GetModelCFrame():pointToWorldSpace(0.5 * partOrModel:GetModelSize()) + minVec = Vector3.new(math.min(vec1.X, vec2.X), math.min(vec1.Y, vec2.Y), math.min(vec1.Z, vec2.Z)) + maxVec = Vector3.new(math.max(vec1.X, vec2.X), math.max(vec1.Y, vec2.Y), math.max(vec1.Z, vec2.Z)) + end + + return minVec, maxVec +end + +local function getTargetPartBoundingBox(targetPart) + if targetPart.Parent:FindFirstChild("RobloxModel") ~= nil then + return getBoundingBox2(targetPart.Parent) + else + return getBoundingBox2(targetPart) + end +end + +local function getMouseTargetCFrame(targetPart) + if targetPart.Parent:FindFirstChild("RobloxModel") ~= nil then + if targetPart.Parent:IsA("Tool") then return targetPart.Parent.Handle.CFrame + else return targetPart.Parent:GetModelCFrame() end + else + return targetPart.CFrame + end +end + +local function isBlocker(part) -- returns whether or not we want to cancel the stamp because we're blocked by this part + if not part then return false end + if not part.Parent then return false end + if part:FindFirstChild("Humanoid") then return false end + if part:FindFirstChild("RobloxStamper") or part:FindFirstChild("RobloxModel") then return true end + if part:IsA("Part") and not part.CanCollide then return false end + if part == game:GetService("Lighting") then return false end + return isBlocker(part.Parent) +end + +-- helper function to determine if a character can be pushed upwards by a certain amount +-- character is 5 studs tall, we'll check a 1.5 x 1.5 x 4.5 box around char, with center .5 studs below torsocenter +local function spaceAboveCharacter(charTorso, newTorsoY, stampData) + local partsAboveChar = game:GetService("Workspace"):FindPartsInRegion3( + Region3.new(Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) - Vector3.new(.75, 2.75, .75), + Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) + Vector3.new(.75, 1.75, .75)), + charTorso.Parent, + 100) + + for j = 1, #partsAboveChar do + if partsAboveChar[j].CanCollide and not partsAboveChar[j]:IsDescendantOf(stampData.CurrentParts) then return false end + end + + if clusterPartsInRegion(Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) - Vector3.new(.75, 2.75, .75), + Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) + Vector3.new(.75, 1.75, .75)) then + return false + end + + return true +end + + +local function findConfigAtMouseTarget(Mouse, stampData) + -- *Critical Assumption* : + -- This function assumes the target CF axes are orthogonal with the target bounding box faces + -- And, it assumes the insert CF axes are orthongonal with the insert bounding box faces + -- Therefore, insertion will not work with angled faces on wedges or other "non-block" parts, nor + -- will it work for parts in a model that are not orthogonally aligned with the model's CF. + + if not Mouse then return nil end -- This can happen sometimes, return if so + if not stampData then error("findConfigAtMouseTarget: stampData is nil") return nil end + if not stampData["CurrentParts"] then return nil end + + local grid = 4.0 + local admissibleConfig = false + local targetConfig = CFrame.new(0,0,0) + + local minBB, maxBB = getBoundingBox2(stampData.CurrentParts) + local diagBB = maxBB - minBB + + local insertCFrame + if stampData.CurrentParts:IsA("Model") or stampData.CurrentParts:IsA("Tool") then + insertCFrame = stampData.CurrentParts:GetModelCFrame() + else + insertCFrame = stampData.CurrentParts.CFrame + end + + if Mouse then + if stampData.CurrentParts:IsA("Tool") then + Mouse.TargetFilter = stampData.CurrentParts.Handle + else + Mouse.TargetFilter = stampData.CurrentParts + end + end + + local hitPlane = false + local targetPart = nil + local success = pcall(function() targetPart = Mouse.Target end) + + if not success then-- or targetPart == nil then + return admissibleConfig, targetConfig + end + + local mouseHitInWorld = Vector3.new(0, 0, 0) + if Mouse then + mouseHitInWorld = Vector3.new(Mouse.Hit.x, Mouse.Hit.y, Mouse.Hit.z) + end + + local cellPos = nil + + -- Nothing was hit, so check for the default plane. + if nil == targetPart then + cellPos = GetTerrainForMouse(Mouse) + if nil == cellPos then + hitPlane = false + return admissibleConfig, targetConfig + else + targetPart = game:GetService("Workspace").Terrain + hitPlane = true + -- Take into account error that will occur. + cellPos = Vector3.new(cellPos.X - 1, cellPos.Y, cellPos.Z) + mouseHitInWorld = game:GetService("Workspace").Terrain:CellCenterToWorld(cellPos.x, cellPos.y, cellPos.z) + end + end + + -- test mouse hit location + local minBBTarget, maxBBTarget = getTargetPartBoundingBox(targetPart) + local diagBBTarget = maxBBTarget - minBBTarget + local targetCFrame = getMouseTargetCFrame(targetPart) + + if targetPart:IsA("Terrain") then + local cluster = game:GetService("Workspace"):FindFirstChild("Terrain") + local cellID = cluster:WorldToCellPreferSolid(mouseHitInWorld) + if hitPlane then + cellID = cellPos + end + + targetCFrame = CFrame.new(game:GetService("Workspace").Terrain:CellCenterToWorld(cellID.x, cellID.y, cellID.z)) + end + + local mouseHitInTarget = targetCFrame:pointToObjectSpace(mouseHitInWorld) + local targetVectorInWorld = Vector3.new(0,0,0) + if Mouse then + -- DON'T WANT THIS IN TERMS OF THE MODEL CFRAME! (.TargetSurface is in terms of the part CFrame, so this would break, right? [HotThoth]) + -- (ideally, we would want to make the Mouse.TargetSurface a model-targetsurface instead, but for testing will be using the converse) + --targetVectorInWorld = targetCFrame:vectorToWorldSpace(Vector3.FromNormalId(Mouse.TargetSurface)) + targetVectorInWorld = targetPart.CFrame:vectorToWorldSpace(Vector3.FromNormalId(Mouse.TargetSurface)) -- better, but model cframe would be best + --[[if targetPart.Parent:IsA("Model") then + local hitFace = modelTargetSurface(targetPart.Parent, Mouse.Hit.p, game.Workspace.CurrentCamera.CoordinateFrame.p) -- best, if you get it right + local WORLD_AXES = {Vector3.new(1, 0, 0), Vector3.new(0, 1, 0), Vector3.new(0, 0, 1)} + if hitFace > 0 then + targetVectorInWorld = targetCFrame:vectorToWorldSpace(WORLD_AXES[hitFace]) + elseif hitFace < 0 then + targetVectorInWorld = targetCFrame:vectorToWorldSpace(-WORLD_AXES[-hitFace]) + end + end]] + end + + local targetRefPointInTarget + local clampToSurface + local insertRefPointInInsert + + if getClosestAlignedWorldDirection(targetVectorInWorld) == 0 then + targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(1, -1, 1)) + insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1)) + clampToSurface = Vector3.new(0,1,1) + elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 3 then + targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(-1, -1, -1)) + insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(1, -1, -1)) + clampToSurface = Vector3.new(0,1,1) + elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 1 then + targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(-1, 1, 1)) + insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1)) + clampToSurface = Vector3.new(1,0,1) + elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 4 then + targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1)) + insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(-1, 1, 1)) + clampToSurface = Vector3.new(1,0,1) + elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 2 then + targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1)) + insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(-1, -1, -1)) + clampToSurface = Vector3.new(1,1,0) + else + targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(1, -1, -1)) + insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(1, -1, 1)) + clampToSurface = Vector3.new(1,1,0) + end + + targetRefPointInTarget = targetRefPointInTarget * (0.5 * diagBBTarget) + 0.5 * (maxBBTarget + minBBTarget) + insertRefPointInInsert = insertRefPointInInsert * (0.5 * diagBB) + 0.5 * (maxBB + minBB) + + -- To Do: For cases that are not aligned to the world grid, account for the minimal rotation + -- needed to bring the Insert part(s) into alignment with the Target Part + -- Apply the rotation here + + local delta = mouseHitInTarget - targetRefPointInTarget + local deltaClamped = Vector3.new(grid * math.modf(delta.x/grid), grid * math.modf(delta.y/grid), grid * math.modf(delta.z/grid)) + deltaClamped = deltaClamped * clampToSurface + local targetTouchInTarget = deltaClamped + targetRefPointInTarget + + local TargetTouchRelToWorld = targetCFrame:pointToWorldSpace(targetTouchInTarget) + local InsertTouchInWorld = insertCFrame:vectorToWorldSpace(insertRefPointInInsert) + local posInsertOriginInWorld = TargetTouchRelToWorld - InsertTouchInWorld + + local x, y, z, R00, R01, R02, R10, R11, R12, R20, R21, R22 = insertCFrame:components() + targetConfig = CFrame.new(posInsertOriginInWorld.x, posInsertOriginInWorld.y, posInsertOriginInWorld.z, R00, R01, R02, R10, R11, R12, R20, R21, R22) + admissibleConfig = true + + return admissibleConfig, targetConfig, getClosestAlignedWorldDirection(targetVectorInWorld) +end + +local function truncateToCircleEighth(bigValue, littleValue) + local big = math.abs(bigValue) + local little = math.abs(littleValue) + local hypotenuse = math.sqrt(big*big + little*little) + local frac = little / hypotenuse + + local bigSign = 1 + local littleSign = 1 + if bigValue < 0 then bigSign = -1 end + if littleValue < 0 then littleSign = -1 end + + if frac > .382683432 then + -- between 22.5 and 45 degrees, so truncate to 45-degree tilt + return .707106781 * hypotenuse * bigSign, .707106781 * hypotenuse * littleSign + else + -- between 0 and 22.5 degrees, so truncate to 0-degree tilt + return hypotenuse * bigSign, 0 + end +end + + +local function saveTheWelds(object, manualWeldTable, manualWeldParentTable) + if object:IsA("ManualWeld") or object:IsA("Rotate") then + table.insert(manualWeldTable, object) + table.insert(manualWeldParentTable, object.Parent) + else + local children = object:GetChildren() + for i = 1, #children do + saveTheWelds(children[i], manualWeldTable, manualWeldParentTable) + end + end +end + +local function restoreTheWelds(manualWeldTable, manualWeldParentTable) + for i = 1, #manualWeldTable do + manualWeldTable[i].Parent = manualWeldParentTable[i] + end +end + +t.CanEditRegion = function(partOrModel, EditRegion) -- todo: use model and stamper metadata + if not EditRegion then return true, false end + + local minBB, maxBB = getBoundingBoxInWorldCoordinates(partOrModel) + + if minBB.X < EditRegion.CFrame.p.X - EditRegion.Size.X/2 or + minBB.Y < EditRegion.CFrame.p.Y - EditRegion.Size.Y/2 or + minBB.Z < EditRegion.CFrame.p.Z - EditRegion.Size.Z/2 then + return false, false + end + + if maxBB.X > EditRegion.CFrame.p.X + EditRegion.Size.X/2 or + maxBB.Y > EditRegion.CFrame.p.Y + EditRegion.Size.Y/2 or + maxBB.Z > EditRegion.CFrame.p.Z + EditRegion.Size.Z/2 then + return false, false + end + + return true, false +end + +t.GetStampModel = function(assetId, terrainShape, useAssetVersionId) + if assetId == 0 then + return nil, "No Asset" + end + if assetId < 0 then + return nil, "Negative Asset" + end + + local function UnlockInstances(object) + if object:IsA("BasePart") then + object.Locked = false + end + for index,child in pairs(object:GetChildren()) do + UnlockInstances(child) + end + end + + local function getClosestColorToTerrainMaterial(terrainValue) + if terrainValue == 1 then + return BrickColor.new("Bright green") + elseif terrainValue == 2 then + return BrickColor.new("Bright yellow") + elseif terrainValue == 3 then + return BrickColor.new("Bright red") + elseif terrainValue == 4 then + return BrickColor.new("Sand red") + elseif terrainValue == 5 then + return BrickColor.new("Black") + elseif terrainValue == 6 then + return BrickColor.new("Dark stone grey") + elseif terrainValue == 7 then + return BrickColor.new("Sand blue") + elseif terrainValue == 8 then + return BrickColor.new("Deep orange") + elseif terrainValue == 9 then + return BrickColor.new("Dark orange") + elseif terrainValue == 10 then + return BrickColor.new("Reddish brown") + elseif terrainValue == 11 then + return BrickColor.new("Light orange") + elseif terrainValue == 12 then + return BrickColor.new("Light stone grey") + elseif terrainValue == 13 then + return BrickColor.new("Sand green") + elseif terrainValue == 14 then + return BrickColor.new("Medium stone grey") + elseif terrainValue == 15 then + return BrickColor.new("Really red") + elseif terrainValue == 16 then + return BrickColor.new("Really blue") + elseif terrainValue == 17 then + return BrickColor.new("Bright blue") + else + return BrickColor.new("Bright green") + end + end + + local function setupFakeTerrainPart(cellMat, cellType, cellOrient) + local newTerrainPiece = nil + if (cellType == 1 or cellType == 4) then newTerrainPiece = Instance.new("WedgePart") + elseif (cellType == 2) then newTerrainPiece = Instance.new("CornerWedgePart") + else newTerrainPiece = Instance.new("Part") end + newTerrainPiece.Name = "MegaClusterCube" + newTerrainPiece.Size = Vector3.new(4, 4, 4) + newTerrainPiece.BottomSurface = "Smooth" + newTerrainPiece.TopSurface = "Smooth" + + -- can add decals or textures here if feeling particularly adventurous... for now, can make a table of look-up colors + newTerrainPiece.BrickColor = getClosestColorToTerrainMaterial(cellMat) + + local sideways = 0 + local flipped = math.pi + if cellType == 4 then sideways = -math.pi/2 end + if cellType == 2 or cellType == 3 then flipped = 0 end + newTerrainPiece.CFrame = CFrame.Angles(0, math.pi/2*cellOrient + flipped, sideways) + + if cellType == 3 then + local inverseCornerWedgeMesh = Instance.new("SpecialMesh") + inverseCornerWedgeMesh.MeshType = "FileMesh" + inverseCornerWedgeMesh.MeshId = "https://www.roblox.com/asset/?id=66832495" + inverseCornerWedgeMesh.Scale = Vector3.new(2, 2, 2) + inverseCornerWedgeMesh.Parent = newTerrainPiece + end + + local materialTag = Instance.new("Vector3Value") + materialTag.Value = Vector3.new(cellMat, cellType, cellOrient) + materialTag.Name = "ClusterMaterial" + materialTag.Parent = newTerrainPiece + + return newTerrainPiece + end + + -- This call will cause a "wait" until the data comes back + -- below we wait a max of 8 seconds before deciding to bail out on loading + local root + local loader + loading = true + if useAssetVersionId then + loader = coroutine.create(function() + root = game:GetService("InsertService"):LoadAssetVersion(assetId) + loading = false + end) + coroutine.resume(loader) + else + loader = coroutine.create(function() + root = game:GetService("InsertService"):LoadAsset(assetId) + loading = false + end) + coroutine.resume(loader) + end + + local lastGameTime = 0 + local totalTime = 0 + local maxWait = 8 + while loading and totalTime < maxWait do + lastGameTime = tick() + wait(1) + totalTime = totalTime + tick() - lastGameTime + end + loading = false + + if totalTime >= maxWait then + return nil, "Load Time Fail" + end + + + if root == nil then + return nil, "Load Asset Fail" + end + + if not root:IsA("Model") then + return nil, "Load Type Fail" + end + + local instances = root:GetChildren() + if #instances == 0 then + return nil, "Empty Model Fail" + end + + --Unlock all parts that are inserted, to make sure they are editable + UnlockInstances(root) + + --Continue the insert process + root = root:GetChildren()[1] + + --Examine the contents and decide what it looks like + for pos, instance in pairs(instances) do + if instance:IsA("Team") then + instance.Parent = game:GetService("Teams") + elseif instance:IsA("Sky") then + local lightingService = game:GetService("Lighting") + for index,child in pairs(lightingService:GetChildren()) do + if child:IsA("Sky") then + child:Remove(); + end + end + instance.Parent = lightingService + return + end + end + + -- ...and tag all inserted models for subsequent origin identification + -- if no RobloxModel tag already exists, then add it. + if root:FindFirstChild("RobloxModel") == nil then + local stringTag = Instance.new("BoolValue", root) + stringTag.Name = "RobloxModel" + + if root:FindFirstChild("RobloxStamper") == nil then + local stringTag2 = Instance.new("BoolValue", root) + stringTag2.Name = "RobloxStamper" + end + end + + if terrainShape then + if root.Name == "MegaClusterCube" then + if (terrainShape == 6) then -- insert an autowedging tag + local autowedgeTag = Instance.new("BoolValue") + autowedgeTag.Name = "AutoWedge" + autowedgeTag.Parent = root + else + local clusterTag = root:FindFirstChild("ClusterMaterial") + if clusterTag then + if clusterTag:IsA("Vector3Value") then + root = setupFakeTerrainPart(clusterTag.Value.X, terrainShape, clusterTag.Value.Z) + else + root = setupFakeTerrainPart(clusterTag.Value, terrainShape, 0) + end + else + root = setupFakeTerrainPart(1, terrainShape, 0) + end + end + end + end + + return root +end + + + +t.SetupStamperDragger = function(modelToStamp, Mouse, StampInModel, AllowedStampRegion, StampFailedFunc) + if not modelToStamp then + error("SetupStamperDragger: modelToStamp (first arg) is nil! Should be a stamper model") + return nil + end + if not modelToStamp:IsA("Model") and not modelToStamp:IsA("BasePart") then + error("SetupStamperDragger: modelToStamp (first arg) is neither a Model or Part!") + return nil + end + if not Mouse then + error("SetupStamperDragger: Mouse (second arg) is nil! Should be a mouse object") + return nil + end + if not Mouse:IsA("Mouse") then + error("SetupStamperDragger: Mouse (second arg) is not of type Mouse!") + return nil + end + + local stampInModel = nil + local allowedStampRegion = nil + local stampFailedFunc = nil + if StampInModel then + if not StampInModel:IsA("Model") then + error("SetupStamperDragger: StampInModel (optional third arg) is not of type 'Model'") + return nil + end + if not AllowedStampRegion then + error("SetupStamperDragger: AllowedStampRegion (optional fourth arg) is nil when StampInModel (optional third arg) is defined") + return nil + end + stampFailedFunc = StampFailedFunc + stampInModel = StampInModel + allowedStampRegion = AllowedStampRegion + end + + -- Init all state variables + local gInitial90DegreeRotations = 0 + local stampData = nil + local mouseTarget = nil + + local errorBox = Instance.new("SelectionBox") + errorBox.Color = BrickColor.new("Bright red") + errorBox.Transparency = 0 + errorBox.Archivable = false + + -- for megacluster MEGA STAMPING + local adornPart = Instance.new("Part") + adornPart.Parent = nil + adornPart.Size = Vector3.new(4, 4, 4) + adornPart.CFrame = CFrame.new() + adornPart.Archivable = false + + local adorn = Instance.new("SelectionBox") + adorn.Color = BrickColor.new("Toothpaste") + adorn.Adornee = adornPart + adorn.Visible = true + adorn.Transparency = 0 + adorn.Name = "HighScalabilityStamperLine" + adorn.Archivable = false + + local HighScalabilityLine = {} + HighScalabilityLine.Start = nil + HighScalabilityLine.End = nil + HighScalabilityLine.Adorn = adorn + HighScalabilityLine.AdornPart = adornPart + HighScalabilityLine.InternalLine = nil + HighScalabilityLine.NewHint = true + + HighScalabilityLine.MorePoints = {nil, nil} + HighScalabilityLine.MoreLines = {nil, nil} + HighScalabilityLine.Dimensions = 1 + + local control = {} + local movingLock = false + local stampUpLock = false + local unstampableSurface = false + local mouseCons = {} + local keyCon = nil + + local stamped = Instance.new("BoolValue") + stamped.Archivable = false + stamped.Value = false + + local lastTarget = {} + lastTarget.TerrainOrientation = 0 + lastTarget.CFrame = 0 + + local cellInfo = {} + cellInfo.Material = 1 + cellInfo.clusterType = 0 + cellInfo.clusterOrientation = 0 + + local function isMegaClusterPart() + if not stampData then return false end + if not stampData.CurrentParts then return false end + + return ( stampData.CurrentParts:FindFirstChild("ClusterMaterial",true) or (stampData.CurrentParts.Name == "MegaClusterCube") ) + end + + local function DoHighScalabilityRegionSelect() + local megaCube = stampData.CurrentParts:FindFirstChild("MegaClusterCube") + if not megaCube then + if not stampData.CurrentParts.Name == "MegaClusterCube" then + return + else + megaCube = stampData.CurrentParts + end + end + + HighScalabilityLine.End = megaCube.CFrame.p + local line = nil + local line2 = Vector3.new(0, 0, 0) + local line3 = Vector3.new(0, 0, 0) + + if HighScalabilityLine.Dimensions == 1 then + -- extract the line from these positions and limit to a 2D plane made from 2 of the world axes + -- then use dominating axis to limit line to be at 45-degree intervals + -- will use this internal representation of the line for the actual stamping + line = (HighScalabilityLine.End - HighScalabilityLine.Start) + + if math.abs(line.X) < math.abs(line.Y) then + if math.abs(line.X) < math.abs(line.Z) then + -- limit to Y/Z plane, domination unknown + local newY, newZ + if (math.abs(line.Y) > math.abs(line.Z)) then + newY, newZ = truncateToCircleEighth(line.Y, line.Z) + else + newZ, newY = truncateToCircleEighth(line.Z, line.Y) + end + line = Vector3.new(0, newY, newZ) + else + -- limit to X/Y plane, with Y dominating + local newY, newX = truncateToCircleEighth(line.Y, line.X) + line = Vector3.new(newX, newY, 0) + end + else + if math.abs(line.Y) < math.abs(line.Z) then + -- limit to X/Z plane, domination unknown + local newX, newZ + if math.abs(line.X) > math.abs(line.Z) then + newX, newZ = truncateToCircleEighth(line.X, line.Z) + else + newZ, newX = truncateToCircleEighth(line.Z, line.X) + end + line = Vector3.new(newX, 0, newZ) + else + -- limit to X/Y plane, with X dominating + local newX, newY = truncateToCircleEighth(line.X, line.Y) + line = Vector3.new(newX, newY, 0) + end + end + HighScalabilityLine.InternalLine = line + + elseif HighScalabilityLine.Dimensions == 2 then + line = HighScalabilityLine.MoreLines[1] + line2 = HighScalabilityLine.End - HighScalabilityLine.MorePoints[1] + + -- take out any component of line2 along line1, so you get perpendicular to line1 component + line2 = line2 - line.unit*line.unit:Dot(line2) + + local tempCFrame = CFrame.new(HighScalabilityLine.Start, HighScalabilityLine.Start + line) + + -- then zero out whichever is the smaller component + local yAxis = tempCFrame:vectorToWorldSpace(Vector3.new(0, 1, 0)) + local xAxis = tempCFrame:vectorToWorldSpace(Vector3.new(1, 0, 0)) + + local xComp = xAxis:Dot(line2) + local yComp = yAxis:Dot(line2) + + if math.abs(yComp) > math.abs(xComp) then + line2 = line2 - xAxis * xComp + else + line2 = line2 - yAxis * yComp + end + + HighScalabilityLine.InternalLine = line2 + + elseif HighScalabilityLine.Dimensions == 3 then + line = HighScalabilityLine.MoreLines[1] + line2 = HighScalabilityLine.MoreLines[2] + line3 = HighScalabilityLine.End - HighScalabilityLine.MorePoints[2] + + -- zero out all components of previous lines + line3 = line3 - line.unit * line.unit:Dot(line3) + line3 = line3 - line2.unit * line2.unit:Dot(line3) + + HighScalabilityLine.InternalLine = line3 + end + + -- resize the "line" graphic to be the correct size and orientation + local tempCFrame = CFrame.new(HighScalabilityLine.Start, HighScalabilityLine.Start + line) + + if HighScalabilityLine.Dimensions == 1 then -- faster calculation for line + HighScalabilityLine.AdornPart.Size = Vector3.new(4, 4, line.magnitude + 4) + HighScalabilityLine.AdornPart.CFrame = tempCFrame + tempCFrame:vectorToWorldSpace(Vector3.new(2, 2, 2) - HighScalabilityLine.AdornPart.Size/2) + else + local boxSize = tempCFrame:vectorToObjectSpace(line + line2 + line3) + HighScalabilityLine.AdornPart.Size = Vector3.new(4, 4, 4) + Vector3.new(math.abs(boxSize.X), math.abs(boxSize.Y), math.abs(boxSize.Z)) + HighScalabilityLine.AdornPart.CFrame = tempCFrame + tempCFrame:vectorToWorldSpace(boxSize/2) + end + + -- make player able to see this ish + + local gui = nil + if game:GetService("Players")["LocalPlayer"] then + gui = game:GetService("Players").LocalPlayer:FindFirstChild("PlayerGui") + if gui and gui:IsA("PlayerGui") then + if HighScalabilityLine.Dimensions == 1 and line.magnitude > 3 then -- don't show if mouse hasn't moved enough + HighScalabilityLine.Adorn.Parent = gui + elseif HighScalabilityLine.Dimensions > 1 then + HighScalabilityLine.Adorn.Parent = gui + end + end + end + + if gui == nil then -- we are in studio + gui = game:GetService("CoreGui") + if HighScalabilityLine.Dimensions == 1 and line.magnitude > 3 then -- don't show if mouse hasn't moved enough + HighScalabilityLine.Adorn.Parent = gui + elseif HighScalabilityLine.Dimensions > 1 then + HighScalabilityLine.Adorn.Parent = gui + end + end + end + + + local function DoStamperMouseMove(Mouse) + if not Mouse then + error("Error: RbxStamper.DoStamperMouseMove: Mouse is nil") + return + end + if not Mouse:IsA("Mouse") then + error("Error: RbxStamper.DoStamperMouseMove: Mouse is of type", Mouse.className,"should be of type Mouse") + return + end + + -- There wasn't a target (no part or terrain), so check for plane intersection. + if not Mouse.Target then + local cellPos = GetTerrainForMouse(Mouse) + if nil == cellPos then + return + end + end + + if not stampData then + return + end + + -- don't move with dragger - will move in one step on mouse down + -- draw ghost at acceptable positions + local configFound, targetCFrame, targetSurface = findConfigAtMouseTarget(Mouse, stampData) + if not configFound then + error("RbxStamper.DoStamperMouseMove No configFound, returning") + return + end + + local numRotations = 0 -- update this according to how many rotations you need to get it to target surface + if autoAlignToFace(stampData.CurrentParts) and targetSurface ~= 1 and targetSurface ~= 4 then -- pre-rotate the flag or portrait so it's aligned correctly + if targetSurface == 3 then numRotations = 0 - gInitial90DegreeRotations + autoAlignToFace(stampData.CurrentParts) + elseif targetSurface == 0 then numRotations = 2 - gInitial90DegreeRotations + autoAlignToFace(stampData.CurrentParts) + elseif targetSurface == 5 then numRotations = 3 - gInitial90DegreeRotations + autoAlignToFace(stampData.CurrentParts) + elseif targetSurface == 2 then numRotations = 1 - gInitial90DegreeRotations + autoAlignToFace(stampData.CurrentParts) + end + end + + local ry = math.pi/2 + gInitial90DegreeRotations = gInitial90DegreeRotations + numRotations + if stampData.CurrentParts:IsA("Model") or stampData.CurrentParts:IsA("Tool") then + --stampData.CurrentParts:Rotate(0, ry*numRotations, 0) + modelRotate(stampData.CurrentParts, ry*numRotations) + else + stampData.CurrentParts.CFrame = CFrame.fromEulerAnglesXYZ(0, ry*numRotations, 0) * stampData.CurrentParts.CFrame + end + + -- CODE TO CHECK FOR DRAGGING GHOST PART INTO A COLLIDING STATE + local minBB, maxBB = getBoundingBoxInWorldCoordinates(stampData.CurrentParts) + + -- need to offset by distance to be dragged + local currModelCFrame = nil + if stampData.CurrentParts:IsA("Model") then + currModelCFrame = stampData.CurrentParts:GetModelCFrame() + else + currModelCFrame = stampData.CurrentParts.CFrame + end + + minBB = minBB + targetCFrame.p - currModelCFrame.p + maxBB = maxBB + targetCFrame.p - currModelCFrame.p + + -- don't drag into terrain + if clusterPartsInRegion(minBB + insertBoundingBoxOverlapVector, maxBB - insertBoundingBoxOverlapVector) then + if lastTarget.CFrame then + if (stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)) then + local theClusterMaterial = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true) + if theClusterMaterial:IsA("Vector3Value") then + local stampClusterMaterial = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true) + if stampClusterMaterial then + stampClusterMaterial = theClusterMaterial + end + end + end + end + return + end + + -- if we are stamping a terrain part, make sure it goes on the grid! Otherwise preview block could be placed off grid, but stamped on grid + if isMegaClusterPart() then + local cellToStamp = game:GetService("Workspace").Terrain:WorldToCell(targetCFrame.p) + local newCFramePosition = game:GetService("Workspace").Terrain:CellCenterToWorld(cellToStamp.X, cellToStamp.Y, cellToStamp.Z) + local x, y, z, R00, R01, R02, R10, R11, R12, R20, R21, R22 = targetCFrame:components() + targetCFrame = CFrame.new(newCFramePosition.X,newCFramePosition.Y,newCFramePosition.Z,R00, R01, R02, R10, R11, R12, R20, R21, R22) + end + + positionPartsAtCFrame3(targetCFrame, stampData.CurrentParts) + lastTarget.CFrame = targetCFrame -- successful positioning, so update 'dat cframe + if stampData.CurrentParts:FindFirstChild("ClusterMaterial", true) then + local clusterMat = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true) + if clusterMat:IsA("Vector3Value") then + lastTarget.TerrainOrientation = clusterMat.Value.Z + end + end + + + -- auto break joints code + if Mouse and Mouse.Target and Mouse.Target.Parent then + local modelInfo = Mouse.Target:FindFirstChild("RobloxModel") + if not modelInfo then modelInfo = Mouse.Target.Parent:FindFirstChild("RobloxModel") end + + local myModelInfo = stampData.CurrentParts:FindFirstChild("UnstampableFaces") + + --if (modelInfo and modelInfo.Parent:FindFirstChild("UnstampableFaces")) or (modelInfo and myModelInfo) then -- need better targetSurface calcs + if (true) then + local breakingFaces = "" + local myBreakingFaces = "" + if modelInfo and modelInfo.Parent:FindFirstChild("UnstampableFaces") then breakingFaces = modelInfo.Parent.UnstampableFaces.Value end + if myModelInfo then myBreakingFaces = myModelInfo.Value end + local hitFace = 0 + + if modelInfo then hitFace = modelTargetSurface(modelInfo.Parent, game:GetService("Workspace").CurrentCamera.CoordinateFrame.p, Mouse.Hit.p) end + + -- are we stamping TO an unstampable surface? + for bf in string.gmatch(breakingFaces, "[^,]+") do + if hitFace == tonumber(bf) then + -- return before we hit the JointsService code below! + unstampableSurface = true + game:GetService("JointsService"):ClearJoinAfterMoveJoints() -- clear the JointsService cache + return + end + end + + -- now we have to cast the ray back in the other direction to find the surface we're stamping FROM + hitFace = modelTargetSurface(stampData.CurrentParts, Mouse.Hit.p, game:GetService("Workspace").CurrentCamera.CoordinateFrame.p) + + -- are we stamping WITH an unstampable surface? + for bf in string.gmatch(myBreakingFaces, "[^,]+") do + if hitFace == tonumber(bf) then + unstampableSurface = true + game:GetService("JointsService"):ClearJoinAfterMoveJoints() -- clear the JointsService cache + return + end + end + + -- just need to match breakingFace against targetSurface using rotation supplied by modelCFrame + -- targetSurface: 1 is top, 4 is bottom, + end + end + + -- to show joints during the mouse move + unstampableSurface = false + game:GetService("JointsService"):SetJoinAfterMoveInstance(stampData.CurrentParts) + + -- most common mouse inactive error occurs here, so check mouse active one more time in a pcall + if not pcall(function() + if Mouse and Mouse.Target and Mouse.Target.Parent:FindFirstChild("RobloxModel") == nil then + return + else + return + end + end) + then + error("Error: RbxStamper.DoStamperMouseMove Mouse is nil on second check") + game:GetService("JointsService"):ClearJoinAfterMoveJoints() + Mouse = nil + return + end + + if Mouse and Mouse.Target and Mouse.Target.Parent:FindFirstChild("RobloxModel") == nil then + game:GetService("JointsService"):SetJoinAfterMoveTarget(Mouse.Target) + else + game:GetService("JointsService"):SetJoinAfterMoveTarget(nil) + end + game:GetService("JointsService"):ShowPermissibleJoints() + + -- here we allow for a line of high-scalability parts + if isMegaClusterPart() and HighScalabilityLine and HighScalabilityLine.Start then + DoHighScalabilityRegionSelect() + end + end + + local function setupKeyListener(key, Mouse) + if control and control["Paused"] then return end -- don't do this if we have no stamp + + key = string.lower(key) + if key == 'r' and not autoAlignToFace(stampData.CurrentParts) then -- rotate the model + gInitial90DegreeRotations = gInitial90DegreeRotations + 1 + + -- Update orientation value if this is a fake terrain part + local clusterValues = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true) + if clusterValues and clusterValues:IsA("Vector3Value") then + clusterValues.Value = Vector3.new(clusterValues.Value.X, clusterValues.Value.Y, (clusterValues.Value.Z + 1) % 4) + end + + -- Rotate the parts or all the parts in the model + local ry = math.pi/2 + if stampData.CurrentParts:IsA("Model") or stampData.CurrentParts:IsA("Tool") then + --stampData.CurrentParts:Rotate(0, ry, 0) + modelRotate(stampData.CurrentParts, ry) + else + stampData.CurrentParts.CFrame = CFrame.fromEulerAnglesXYZ(0, ry, 0) * stampData.CurrentParts.CFrame + end + + -- After rotating, update the position + configFound, targetCFrame = findConfigAtMouseTarget(Mouse, stampData) + if configFound then + positionPartsAtCFrame3(targetCFrame, stampData.CurrentParts) + + -- update everything else in MouseMove + DoStamperMouseMove(Mouse) + end + elseif key == 'c' then -- try to expand our high scalability dragger dimension + if HighScalabilityLine.InternalLine and HighScalabilityLine.InternalLine.magnitude > 0 and HighScalabilityLine.Dimensions < 3 then + HighScalabilityLine.MorePoints[HighScalabilityLine.Dimensions] = HighScalabilityLine.End + HighScalabilityLine.MoreLines[HighScalabilityLine.Dimensions] = HighScalabilityLine.InternalLine + HighScalabilityLine.Dimensions = HighScalabilityLine.Dimensions + 1 + HighScalabilityLine.NewHint = true + end + end + end + + keyCon = Mouse.KeyDown:connect(function(key) -- init key connection (keeping code close to func) + setupKeyListener(key, Mouse) + end) + + local function resetHighScalabilityLine() + if HighScalabilityLine then + HighScalabilityLine.Start = nil + HighScalabilityLine.End = nil + HighScalabilityLine.InternalLine = nil + HighScalabilityLine.NewHint = true + end + end + + local function flashRedBox() + local gui = game:GetService("CoreGui") + if game:GetService("Players") then + if game:GetService("Players")["LocalPlayer"] then + if game:GetService("Players").LocalPlayer:FindFirstChild("PlayerGui") then + gui = game:GetService("Players").LocalPlayer.PlayerGui + end + end + end + if not stampData["ErrorBox"] then return end + + stampData.ErrorBox.Parent = gui + if stampData.CurrentParts:IsA("Tool") then + stampData.ErrorBox.Adornee = stampData.CurrentParts.Handle + else + stampData.ErrorBox.Adornee = stampData.CurrentParts + end + + delay(0,function() + for i = 1, 3 do + if stampData["ErrorBox"] then stampData.ErrorBox.Visible = true end + wait(0.13) + if stampData["ErrorBox"] then stampData.ErrorBox.Visible = false end + wait(0.13) + end + if stampData["ErrorBox"] then + stampData.ErrorBox.Adornee = nil + stampData.ErrorBox.Parent = nil + end + end) + end + + local function DoStamperMouseDown(Mouse) + if not Mouse then + error("Error: RbxStamper.DoStamperMouseDown: Mouse is nil") + return + end + if not Mouse:IsA("Mouse") then + error("Error: RbxStamper.DoStamperMouseDown: Mouse is of type", Mouse.className,"should be of type Mouse") + return + end + if not stampData then + return + end + + if isMegaClusterPart() then + if Mouse and HighScalabilityLine then + local megaCube = stampData.CurrentParts:FindFirstChild("MegaClusterCube", true) + local terrain = game:GetService("Workspace").Terrain + if megaCube then + HighScalabilityLine.Dimensions = 1 + local tempCell = terrain:WorldToCell(megaCube.CFrame.p) + HighScalabilityLine.Start = terrain:CellCenterToWorld(tempCell.X, tempCell.Y, tempCell.Z) + return + else + HighScalabilityLine.Dimensions = 1 + local tempCell = terrain:WorldToCell(stampData.CurrentParts.CFrame.p) + HighScalabilityLine.Start = terrain:CellCenterToWorld(tempCell.X, tempCell.Y, tempCell.Z) + return + end + end + end + end + + local function loadSurfaceTypes(part, surfaces) + part.TopSurface = surfaces[1] + part.BottomSurface = surfaces[2] + part.LeftSurface = surfaces[3] + part.RightSurface = surfaces[4] + part.FrontSurface = surfaces[5] + part.BackSurface = surfaces[6] + end + + local function saveSurfaceTypes(part, myTable) + local tempTable = {} + tempTable[1] = part.TopSurface + tempTable[2] = part.BottomSurface + tempTable[3] = part.LeftSurface + tempTable[4] = part.RightSurface + tempTable[5] = part.FrontSurface + tempTable[6] = part.BackSurface + + myTable[part] = tempTable + end + + local function makeSurfaceUnjoinable(part, surface) + -- TODO: FILL OUT! + end + + local function prepareModel(model) + if not model then return nil end + + local gDesiredTrans = 0.7 + local gStaticTrans = 1 + + local clone = model:Clone() + local scripts = {} + local parts = {} + local decals = {} + + stampData = {} + stampData.DisabledScripts = {} + stampData.TransparencyTable = {} + stampData.MaterialTable = {} + stampData.CanCollideTable = {} + stampData.AnchoredTable = {} + stampData.ArchivableTable = {} + stampData.DecalTransparencyTable = {} + stampData.SurfaceTypeTable = {} + + collectParts(clone, parts, scripts, decals) + + if #parts <= 0 then return nil, "no parts found in modelToStamp" end + + for index,script in pairs(scripts) do + if not(script.Disabled) then + script.Disabled = true + stampData.DisabledScripts[#stampData.DisabledScripts + 1] = script + end + end + for index, part in pairs(parts) do + stampData.TransparencyTable[part] = part.Transparency + part.Transparency = gStaticTrans + (1 - gStaticTrans) * part.Transparency + stampData.MaterialTable[part] = part.Material + part.Material = Enum.Material.Plastic + stampData.CanCollideTable[part] = part.CanCollide + part.CanCollide = false + stampData.AnchoredTable[part] = part.Anchored + part.Anchored = true + stampData.ArchivableTable[part] = part.Archivable + part.Archivable = false + + saveSurfaceTypes(part, stampData.SurfaceTypeTable) + + local fadeInDelayTime = 0.5 + local transFadeInTime = 0.5 + delay(0,function() + wait(fadeInDelayTime) -- give it some time to be completely transparent + + local begTime = tick() + local currTime = begTime + while (currTime - begTime) < transFadeInTime and part and part:IsA("BasePart") and part.Transparency > gDesiredTrans do + local newTrans = 1 - (((currTime - begTime)/transFadeInTime) * (gStaticTrans - gDesiredTrans)) + if stampData["TransparencyTable"] and stampData.TransparencyTable[part] then + part.Transparency = newTrans + (1 - newTrans) * stampData.TransparencyTable[part] + end + wait(0.03) + currTime = tick() + end + if part and part:IsA("BasePart") then + if stampData["TransparencyTable"] and stampData.TransparencyTable[part] then + part.Transparency = gDesiredTrans + (1 - gDesiredTrans) * stampData.TransparencyTable[part] + end + end + end) + end + + for index, decal in pairs(decals) do + stampData.DecalTransparencyTable[decal] = decal.Transparency + decal.Transparency = gDesiredTrans + (1 - gDesiredTrans) * decal.Transparency + end + + -- disable all seats + setSeatEnabledStatus(clone, true) + setSeatEnabledStatus(clone, false) + + stampData.CurrentParts = clone + + -- if auto-alignable, we enforce a pre-rotation to the canonical "0-frame" + if autoAlignToFace(clone) then + stampData.CurrentParts:ResetOrientationToIdentity() + gInitial90DegreeRotations = 0 + else -- pre-rotate if necessary + local ry = gInitial90DegreeRotations * math.pi/2 + if stampData.CurrentParts:IsA("Model") or stampData.CurrentParts:IsA("Tool") then + --stampData.CurrentParts:Rotate(0, ry, 0) + modelRotate(stampData.CurrentParts, ry) + else + stampData.CurrentParts.CFrame = CFrame.fromEulerAnglesXYZ(0, ry, 0) * stampData.CurrentParts.CFrame + end + end + + -- since we're cloning the old model instead of the new one, we will need to update the orientation based on the original value AND how many more + -- rotations we expect since then [either that or we need to store the just-stamped clusterMaterial.Value.Z somewhere]. This should fix the terrain rotation + -- issue (fingers crossed) [HotThoth] + + local clusterMaterial = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true) + if clusterMaterial and clusterMaterial:IsA("Vector3Value") then + clusterMaterial.Value = Vector3.new(clusterMaterial.Value.X, clusterMaterial.Value.Y, (clusterMaterial.Value.Z + gInitial90DegreeRotations) % 4) + end + + -- After rotating, update the position + local configFound, targetCFrame = findConfigAtMouseTarget(Mouse, stampData) + if configFound then + stampData.CurrentParts = positionPartsAtCFrame3(targetCFrame, stampData.CurrentParts) + end + + -- to show joints during the mouse move + game:GetService("JointsService"):SetJoinAfterMoveInstance(stampData.CurrentParts) + + return clone, parts + end + + local function checkTerrainBlockCollisions(cellPos, checkHighScalabilityStamp) + local cellCenterToWorld = game:GetService("Workspace").Terrain.CellCenterToWorld + local cellCenter = cellCenterToWorld(game:GetService("Workspace").Terrain, cellPos.X, cellPos.Y, cellPos.Z) + local cellBlockingParts = game:GetService("Workspace"):FindPartsInRegion3(Region3.new(cellCenter - Vector3.new(2, 2, 2) + insertBoundingBoxOverlapVector, cellCenter + Vector3.new(2, 2, 2) - insertBoundingBoxOverlapVector), stampData.CurrentParts, 100) + + local skipThisCell = false + + for b = 1, #cellBlockingParts do + if isBlocker(cellBlockingParts[b]) then skipThisCell = true break end + end + + if not skipThisCell then + -- pop players up above any set cells + local alreadyPushedUp = {} + -- if no blocking model below, then see if stamping on top of a character + for b = 1, #cellBlockingParts do + if cellBlockingParts[b].Parent and + not alreadyPushedUp[cellBlockingParts[b].Parent] and + cellBlockingParts[b].Parent:FindFirstChild("Humanoid") and + cellBlockingParts[b].Parent:FindFirstChild("Humanoid"):IsA("Humanoid") then + ----------------------------------------------------------------------------------- + local blockingPersonTorso = cellBlockingParts[b].Parent:FindFirstChild("Torso") + alreadyPushedUp[cellBlockingParts[b].Parent] = true + + if blockingPersonTorso then + -- if so, let's push the person upwards so they pop on top of the stamped model/part (but only if there's space above them) + local newY = cellCenter.Y + 5 + if spaceAboveCharacter(blockingPersonTorso, newY, stampData) then + blockingPersonTorso.CFrame = blockingPersonTorso.CFrame + Vector3.new(0, newY - blockingPersonTorso.CFrame.p.Y, 0) + else + -- if no space, we just skip this one + skipThisCell = true + break + end + end + ----------------------------------------------------------------------------------- + end + end + end + + if not skipThisCell then -- if we STILL aren't skipping... then we're good to go! + local canSetCell = true + + if checkHighScalabilityStamp then -- check to see if cell is in region, if not we'll skip set + if allowedStampRegion then + local cellPos = cellCenterToWorld(game:GetService("Workspace").Terrain, cellPos.X, cellPos.Y, cellPos.Z) + if cellPos.X + 2 > allowedStampRegion.CFrame.p.X + allowedStampRegion.Size.X/2 then + canSetCell = false + elseif cellPos.X - 2 < allowedStampRegion.CFrame.p.X - allowedStampRegion.Size.X/2 then + canSetCell = false + elseif cellPos.Y + 2 > allowedStampRegion.CFrame.p.Y + allowedStampRegion.Size.Y/2 then + canSetCell = false + elseif cellPos.Y - 2 < allowedStampRegion.CFrame.p.Y - allowedStampRegion.Size.Y/2 then + canSetCell = false + elseif cellPos.Z + 2 > allowedStampRegion.CFrame.p.Z + allowedStampRegion.Size.Z/2 then + canSetCell = false + elseif cellPos.Z - 2 < allowedStampRegion.CFrame.p.Z - allowedStampRegion.Size.Z/2 then + canSetCell = false + end + end + end + + return canSetCell + end + return false + end + + + local function ResolveMegaClusterStamp(checkHighScalabilityStamp) + local cellSet = false + + local cluser = game:GetService("Workspace").Terrain + + local line = HighScalabilityLine.InternalLine + local cMax = game:GetService("Workspace").Terrain.MaxExtents.Max + local cMin = game:GetService("Workspace").Terrain.MaxExtents.Min + + local clusterMaterial = 1 -- default is grass + local clusterType = 0 -- default is brick + local clusterOrientation = 0 -- default is 0 rotation + + local autoWedgeClusterParts = false + if stampData.CurrentParts:FindFirstChild("AutoWedge") then autoWedgeClusterParts = true end + + if stampData.CurrentParts:FindFirstChild("ClusterMaterial", true) then + clusterMaterial = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true) + if clusterMaterial:IsA("Vector3Value") then + clusterType = clusterMaterial.Value.Y + clusterOrientation = clusterMaterial.Value.Z + clusterMaterial = clusterMaterial.Value.X + elseif clusterMaterial:IsA("IntValue") then + clusterMaterial = clusterMaterial.Value + end + end + + if HighScalabilityLine.Adorn.Parent and HighScalabilityLine.Start and ((HighScalabilityLine.Dimensions > 1) or (line and line.magnitude > 0)) then + local startCell = game:GetService("Workspace").Terrain:WorldToCell(HighScalabilityLine.Start) + local xInc = {0,0,0} + local yInc = {0,0,0} + local zInc = {0,0,0} + + local cluster = game:GetService("Workspace").Terrain + + local incrementVect = {nil, nil, nil} + local stepVect = {Vector3.new(0, 0, 0), Vector3.new(0, 0, 0), Vector3.new(0, 0, 0)} + + local worldAxes = {Vector3.new(1, 0, 0), Vector3.new(0, 1, 0), Vector3.new(0, 0, 1)} + + local lines = {} + if HighScalabilityLine.Dimensions > 1 then table.insert(lines, HighScalabilityLine.MoreLines[1]) end + if line and line.magnitude > 0 then table.insert(lines, line) end + if HighScalabilityLine.Dimensions > 2 then table.insert(lines, HighScalabilityLine.MoreLines[2]) end + + for i = 1, #lines do + lines[i] = Vector3.new(math.floor(lines[i].X+.5), math.floor(lines[i].Y+.5), math.floor(lines[i].Z+.5)) -- round to integers + + if lines[i].X > 0 then xInc[i] = 1 elseif lines[i].X < 0 then xInc[i] = -1 end + if lines[i].Y > 0 then yInc[i] = 1 elseif lines[i].Y < 0 then yInc[i] = -1 end + if lines[i].Z > 0 then zInc[i] = 1 elseif lines[i].Z < 0 then zInc[i] = -1 end + + incrementVect[i] = Vector3.new(xInc[i], yInc[i], zInc[i]) + if incrementVect[i].magnitude < .9 then incrementVect[i] = nil end + end + + + if not lines[2] then lines[2] = Vector3.new(0, 0, 0) end + if not lines[3] then lines[3] = Vector3.new(0, 0, 0) end + + local waterForceTag = stampData.CurrentParts:FindFirstChild("WaterForceTag", true) + local waterForceDirectionTag = stampData.CurrentParts:FindFirstChild("WaterForceDirectionTag", true) + + while (stepVect[3].magnitude*4 <= lines[3].magnitude) do + local outerStepVectIndex = 1 + while outerStepVectIndex < 4 do + stepVect[2] = Vector3.new(0, 0, 0) + while (stepVect[2].magnitude*4 <= lines[2].magnitude) do + local innerStepVectIndex = 1 + while innerStepVectIndex < 4 do + stepVect[1] = Vector3.new(0, 0, 0) + while (stepVect[1].magnitude*4 <= lines[1].magnitude) do + local stepVectSum = stepVect[1] + stepVect[2] + stepVect[3] + local cellPos = Vector3int16.new(startCell.X + stepVectSum.X, startCell.Y + stepVectSum.Y, startCell.Z + stepVectSum.Z) + if cellPos.X >= cMin.X and cellPos.Y >= cMin.Y and cellPos.Z >= cMin.Z and cellPos.X < cMax.X and cellPos.Y < cMax.Y and cellPos.Z < cMax.Z then + -- check if overlaps player or part + local okToStampTerrainBlock = checkTerrainBlockCollisions(cellPos, checkHighScalabilityStamp) + + if okToStampTerrainBlock then + if waterForceTag then + cluster:SetWaterCell(cellPos.X, cellPos.Y, cellPos.Z, Enum.WaterForce[waterForceTag.Value], Enum.WaterDirection[waterForceDirectionTag.Value]) + else + cluster:SetCell(cellPos.X, cellPos.Y, cellPos.Z, clusterMaterial, clusterType, clusterOrientation) + end + cellSet = true + + -- auto-wedge it? + if (autoWedgeClusterParts) then + game:GetService("Workspace").Terrain:AutowedgeCells(Region3int16.new(Vector3int16.new(cellPos.x - 1, cellPos.y - 1, cellPos.z - 1), + Vector3int16.new(cellPos.x + 1, cellPos.y + 1, cellPos.z + 1))) + end + end + end + stepVect[1] = stepVect[1] + incrementVect[1] + end + if incrementVect[2] then + while innerStepVectIndex < 4 and worldAxes[innerStepVectIndex]:Dot(incrementVect[2]) == 0 do + innerStepVectIndex = innerStepVectIndex + 1 + end + if innerStepVectIndex < 4 then + stepVect[2] = stepVect[2] + worldAxes[innerStepVectIndex] * worldAxes[innerStepVectIndex]:Dot(incrementVect[2]) + end + innerStepVectIndex = innerStepVectIndex + 1 + else + stepVect[2] = Vector3.new(1, 0, 0) + innerStepVectIndex = 4 -- skip all remaining loops + end + if (stepVect[2].magnitude*4 > lines[2].magnitude) then innerStepVectIndex = 4 end + end + end + if incrementVect[3] then + while outerStepVectIndex < 4 and worldAxes[outerStepVectIndex]:Dot(incrementVect[3]) == 0 do + outerStepVectIndex = outerStepVectIndex + 1 + end + if outerStepVectIndex < 4 then + stepVect[3] = stepVect[3] + worldAxes[outerStepVectIndex] * worldAxes[outerStepVectIndex]:Dot(incrementVect[3]) + end + outerStepVectIndex = outerStepVectIndex + 1 + else -- skip all remaining loops + stepVect[3] = Vector3.new(1, 0, 0) outerStepVectIndex = 4 + end + if (stepVect[3].magnitude*4 > lines[3].magnitude) then outerStepVectIndex = 4 end + end + end + end + + -- and also get rid of any HighScalabilityLine stuff if it's there + HighScalabilityLine.Start = nil + HighScalabilityLine.Adorn.Parent = nil + + -- Mark for undo. + if cellSet then + stampData.CurrentParts.Parent = nil + pcall(function() game:GetService("ChangeHistoryService"): SetWaypoint("StamperMulti") end) + end + + return cellSet + end + + local function DoStamperMouseUp(Mouse) + if not Mouse then + error("Error: RbxStamper.DoStamperMouseUp: Mouse is nil") + return false + end + if not Mouse:IsA("Mouse") then + error("Error: RbxStamper.DoStamperMouseUp: Mouse is of type", Mouse.className,"should be of type Mouse") + return false + end + + if not stampData.Dragger then + error("Error: RbxStamper.DoStamperMouseUp: stampData.Dragger is nil") + return false + end + + if not HighScalabilityLine then + return false + end + + local checkHighScalabilityStamp = nil + if stampInModel then + local canStamp = nil + local isHSLPart = isMegaClusterPart() + + if isHSLPart and + HighScalabilityLine and + HighScalabilityLine.Start and + HighScalabilityLine.InternalLine and + HighScalabilityLine.InternalLine.magnitude > 0 then -- we have an HSL line, test later + canStamp = true + checkHighScalabilityStamp = true + else + canStamp, checkHighScalabilityStamp = t.CanEditRegion(stampData.CurrentParts, allowedStampRegion) + end + + if not canStamp then + if stampFailedFunc then + stampFailedFunc() + end + return false + end + end + + -- if unstampable face, then don't let us stamp there! + if unstampableSurface then + flashRedBox() + return false + end + + -- recheck if we can stamp, as we just moved part + local canStamp, checkHighScalabilityStamp = t.CanEditRegion(stampData.CurrentParts, allowedStampRegion) + if not canStamp then + if stampFailedFunc then + stampFailedFunc() + end + return false + end + + -- Prevent part from being stamped on top of a player + + local minBB, maxBB = getBoundingBoxInWorldCoordinates(stampData.CurrentParts) + + -- HotThoth's note: Now that above CurrentParts positioning has been commented out, to be truly correct, we would need to use the + -- value of configFound from the previous onStamperMouseMove call which moved the CurrentParts + -- Shouldn't this be true when lastTargetCFrame has been set and false otherwise? + configFound, targetCFrame = findConfigAtMouseTarget(Mouse, stampData) + + if configFound and not HighScalabilityLine.Adorn.Parent then + if clusterPartsInRegion(minBB + insertBoundingBoxOverlapVector, maxBB - insertBoundingBoxOverlapVector) then + flashRedBox() + return false + end + + local blockingParts = game:GetService("Workspace"):FindPartsInRegion3(Region3.new(minBB + insertBoundingBoxOverlapVector, + maxBB - insertBoundingBoxOverlapVector), + stampData.CurrentParts, + 100) + + + for b = 1, #blockingParts do + if isBlocker(blockingParts[b]) then + flashRedBox() + return false + end + end + + local alreadyPushedUp = {} + -- if no blocking model below, then see if stamping on top of a character + for b = 1, #blockingParts do + if blockingParts[b].Parent and + not alreadyPushedUp[blockingParts[b].Parent] and + blockingParts[b].Parent:FindFirstChild("Humanoid") and + blockingParts[b].Parent:FindFirstChild("Humanoid"):IsA("Humanoid") then + --------------------------------------------------------------------------- + local blockingPersonTorso = blockingParts[b].Parent:FindFirstChild("Torso") + alreadyPushedUp[blockingParts[b].Parent] = true + + if blockingPersonTorso then + -- if so, let's push the person upwards so they pop on top of the stamped model/part (but only if there's space above them) + local newY = maxBB.Y + 3 + if spaceAboveCharacter(blockingPersonTorso, newY, stampData) then + blockingPersonTorso.CFrame = blockingPersonTorso.CFrame + Vector3.new(0, newY - blockingPersonTorso.CFrame.p.Y, 0) + else + -- if no space, we just error + flashRedBox() + return false + end + end + --------------------------------------------------------------------------- + end + end + + elseif (not configFound) and not (HighScalabilityLine.Start and HighScalabilityLine.Adorn.Parent) then -- if no config then only stamp if it's a real HSL! + resetHighScalabilityLine() + return false + end + + -- something will be stamped! so set the "StampedSomething" toggle to true + if game:GetService("Players")["LocalPlayer"] then + if game:GetService("Players").LocalPlayer["Character"] then + local localChar = game:GetService("Players").LocalPlayer.Character + local stampTracker = localChar:FindFirstChild("StampTracker") + if stampTracker and not stampTracker.Value then + stampTracker.Value = true + end + end + end + + -- if we drew a line of mega parts, stamp them out + if HighScalabilityLine.Start and HighScalabilityLine.Adorn.Parent and isMegaClusterPart() then + if ResolveMegaClusterStamp(checkHighScalabilityStamp) or checkHighScalabilityStamp then + -- kill the ghost part + stampData.CurrentParts.Parent = nil + return true + end + end + + -- not High-Scalability-Line-Based, so behave normally [and get rid of any HSL stuff] + HighScalabilityLine.Start = nil + HighScalabilityLine.Adorn.Parent = nil + + local cluster = game:GetService("Workspace").Terrain + + -- if target point is in cluster, just use cluster:SetCell + if isMegaClusterPart() then + -- if targetCFrame is inside cluster, just set that cell to 1 and return + --local cellPos = cluster:WorldToCell(targetCFrame.p) + + local cellPos + if stampData.CurrentParts:IsA("Model") then cellPos = cluster:WorldToCell(stampData.CurrentParts:GetModelCFrame().p) + else cellPos = cluster:WorldToCell(stampData.CurrentParts.CFrame.p) end + + local cMax = game:GetService("Workspace").Terrain.MaxExtents.Max + local cMin = game:GetService("Workspace").Terrain.MaxExtents.Min + + if checkTerrainBlockCollisions(cellPos, false) then + + local clusterValues = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true) + local waterForceTag = stampData.CurrentParts:FindFirstChild("WaterForceTag", true) + local waterForceDirectionTag = stampData.CurrentParts:FindFirstChild("WaterForceDirectionTag", true) + + if cellPos.X >= cMin.X and cellPos.Y >= cMin.Y and cellPos.Z >= cMin.Z and cellPos.X < cMax.X and cellPos.Y < cMax.Y and cellPos.Z < cMax.Z then + + if waterForceTag then + cluster:SetWaterCell(cellPos.X, cellPos.Y, cellPos.Z, Enum.WaterForce[waterForceTag.Value], Enum.WaterDirection[waterForceDirectionTag.Value]) + elseif not clusterValues then + cluster:SetCell(cellPos.X, cellPos.Y, cellPos.Z, cellInfo.Material, cellInfo.clusterType, gInitial90DegreeRotations % 4) + elseif clusterValues:IsA("Vector3Value") then + cluster:SetCell(cellPos.X, cellPos.Y, cellPos.Z, clusterValues.Value.X, clusterValues.Value.Y, clusterValues.Value.Z) + else + cluster:SetCell(cellPos.X, cellPos.Y, cellPos.Z, clusterValues.Value, 0, 0) + end + + local autoWedgeClusterParts = false + if stampData.CurrentParts:FindFirstChild("AutoWedge") then autoWedgeClusterParts = true end + + -- auto-wedge it + if (autoWedgeClusterParts) then + game:GetService("Workspace").Terrain:AutowedgeCells( + Region3int16.new( + Vector3int16.new(cellPos.x - 1, cellPos.y - 1, cellPos.z - 1), + Vector3int16.new(cellPos.x + 1, cellPos.y + 1, cellPos.z + 1) + ) + ) + end + + -- kill the ghost part + stampData.CurrentParts.Parent = nil + + -- Mark for undo. It has to happen here or the selection display will come back also. + pcall(function() game:GetService("ChangeHistoryService"):SetWaypoint("StamperSingle") end) + return true + end + else + -- you tried to stamp a HSL-single part where one does not belong! + flashRedBox() + return false + end + end + + local function getPlayer() + if game:GetService("Players")["LocalPlayer"] then + return game:GetService("Players").LocalPlayer + end + return nil + end + + + -- Post process: after positioning the part or model, restore transparency, material, anchored and collide states and create joints + if stampData.CurrentParts:IsA("Model") or stampData.CurrentParts:IsA("Tool") then + if stampData.CurrentParts:IsA("Model") then + -- Tyler's magical hack-code for allowing/preserving clones of both Surface and Manual Welds... just don't ask X< + local manualWeldTable = {} + local manualWeldParentTable = {} + saveTheWelds(stampData.CurrentParts, manualWeldTable, manualWeldParentTable) + stampData.CurrentParts:BreakJoints() + stampData.CurrentParts:MakeJoints() + restoreTheWelds(manualWeldTable, manualWeldParentTable) + end + + -- if it's a model, we also want to fill in the playerID and playerName tags, if it has those (e.g. for the friend-only door) + local playerIdTag = stampData.CurrentParts:FindFirstChild("PlayerIdTag") + local playerNameTag = stampData.CurrentParts:FindFirstChild("PlayerNameTag") + if playerIdTag ~= nil then + local tempPlayerValue = getPlayer() + if tempPlayerValue ~= nil then playerIdTag.Value = tempPlayerValue.UserId end + end + if playerNameTag ~= nil then + if game:GetService("Players")["LocalPlayer"] then + local tempPlayerValue = game:GetService("Players").LocalPlayer + if tempPlayerValue ~= nil then playerNameTag.Value = tempPlayerValue.Name end + end + end + -- ...and tag all inserted models for subsequent origin identification + -- if no RobloxModel tag already exists, then add it. + if stampData.CurrentParts:FindFirstChild("RobloxModel") == nil then + local stringTag = Instance.new("BoolValue", stampData.CurrentParts) + stringTag.Name = "RobloxModel" + + if stampData.CurrentParts:FindFirstChild("RobloxStamper") == nil then + local stringTag2 = Instance.new("BoolValue", stampData.CurrentParts) + stringTag2.Name = "RobloxStamper" + end + end + + else + stampData.CurrentParts:BreakJoints() + if stampData.CurrentParts:FindFirstChild("RobloxStamper") == nil then + local stringTag2 = Instance.new("BoolValue", stampData.CurrentParts) + stringTag2.Name = "RobloxStamper" + end + end + + -- make sure all the joints are activated before restoring anchor states + game:GetService("JointsService"):CreateJoinAfterMoveJoints() + + -- Restore the original properties for all parts being stamped + for part, transparency in pairs(stampData.TransparencyTable) do + part.Transparency = transparency + end + for part, archivable in pairs(stampData.ArchivableTable) do + part.Archivable = archivable + end + for part, material in pairs(stampData.MaterialTable) do + part.Material = material + end + for part, collide in pairs(stampData.CanCollideTable) do + part.CanCollide = collide + end + for part, anchored in pairs(stampData.AnchoredTable) do + part.Anchored = anchored + end + for decal, transparency in pairs(stampData.DecalTransparencyTable) do + decal.Transparency = transparency + end + + for part, surfaces in pairs(stampData.SurfaceTypeTable) do + loadSurfaceTypes(part, surfaces) + end + + if isMegaClusterPart() then + stampData.CurrentParts.Transparency = 0 + end + + -- re-enable all seats + setSeatEnabledStatus(stampData.CurrentParts, true) + + stampData.TransparencyTable = nil + stampData.ArchivableTable = nil + stampData.MaterialTable = nil + stampData.CanCollideTable = nil + stampData.AnchoredTable = nil + stampData.SurfaceTypeTable = nil + + -- ...and tag all inserted models for subsequent origin identification + -- if no RobloxModel tag already exists, then add it. + if stampData.CurrentParts:FindFirstChild("RobloxModel") == nil then + local stringTag = Instance.new("BoolValue", stampData.CurrentParts) + stringTag.Name = "RobloxModel" + end + + --Re-enable the scripts + for index,script in pairs(stampData.DisabledScripts) do + script.Disabled = false + end + + --Now that they are all marked enabled, reinsert them into the world so they start running + for index,script in pairs(stampData.DisabledScripts) do + local oldParent = script.Parent + script.Parent = nil + script:Clone().Parent = oldParent + end + + -- clear out more data + stampData.DisabledScripts = nil + stampData.Dragger = nil + stampData.CurrentParts = nil + + pcall(function() game:GetService("ChangeHistoryService"): SetWaypoint("StampedObject") end) + return true + end + + local function pauseStamper() + for i = 1, #mouseCons do -- stop the mouse from doing anything + mouseCons[i]:disconnect() + mouseCons[i] = nil + end + mouseCons = {} + + if stampData and stampData.CurrentParts then -- remove our ghost part + stampData.CurrentParts.Parent = nil + stampData.CurrentParts:Remove() + end + + resetHighScalabilityLine() + + game:GetService("JointsService"):ClearJoinAfterMoveJoints() + end + + + local function prepareUnjoinableSurfaces(modelCFrame, parts, whichSurface) + local AXIS_VECTORS = {Vector3.new(1, 0, 0), Vector3.new(0, 1, 0), Vector3.new(0, 0, 1)} -- maybe last one is negative? TODO: check this! + local isPositive = 1 + if whichSurface < 0 then isPositive = isPositive * -1 whichSurface = whichSurface*-1 end + local surfaceNormal = isPositive * modelCFrame:vectorToWorldSpace(AXIS_VECTORS[whichSurface]) + + for i = 1, #parts do + local currPart = parts[i] + + -- now just need to find which surface of currPart most closely match surfaceNormal and then set that to Unjoinable + local surfaceNormalInLocalCoords = currPart.CFrame:vectorToObjectSpace(surfaceNormal) + if math.abs(surfaceNormalInLocalCoords.X) > math.abs(surfaceNormalInLocalCoords.Y) then + if math.abs(surfaceNormalInLocalCoords.X) > math.abs(surfaceNormalInLocalCoords.Z) then + if surfaceNormalInLocalCoords.X > 0 then currPart.RightSurface = "Unjoinable" else currPart.LeftSurface = "Unjoinable" end + else + if surfaceNormalInLocalCoords.Z > 0 then currPart.BackSurface = "Unjoinable" else currPart.FrontSurface = "Unjoinable" end + end + else + if math.abs(surfaceNormalInLocalCoords.Y) > math.abs(surfaceNormalInLocalCoords.Z) then + if surfaceNormalInLocalCoords.Y > 0 then currPart.TopSurface = "Unjoinable" else currPart.BottomSurface = "Unjoinable" end + else + if surfaceNormalInLocalCoords.Z > 0 then currPart.BackSurface = "Unjoinable" else currPart.FrontSurface = "Unjoinable" end + end + end + end + end + + local function resumeStamper() + local clone, parts = prepareModel(modelToStamp) + + if not clone or not parts then + return + end + + -- if we have unjoinable faces, then we want to change those surfaces to be Unjoinable + local unjoinableTag = clone:FindFirstChild("UnjoinableFaces", true) + if unjoinableTag then + for unjoinableSurface in string.gmatch(unjoinableTag.Value, "[^,]*") do + if tonumber(unjoinableSurface) then + if clone:IsA("Model") then + prepareUnjoinableSurfaces(clone:GetModelCFrame(), parts, tonumber(unjoinableSurface)) + else + prepareUnjoinableSurfaces(clone.CFrame, parts, tonumber(unjoinableSurface)) + end + end + end + end + + stampData.ErrorBox = errorBox + if stampInModel then + clone.Parent = stampInModel + else + clone.Parent = game:GetService("Workspace") + end + + if clone:FindFirstChild("ClusterMaterial", true) then -- extract all info from vector + local clusterMaterial = clone:FindFirstChild("ClusterMaterial", true) + if (clusterMaterial:IsA("Vector3Value")) then + cellInfo.Material = clusterMaterial.Value.X + cellInfo.clusterType = clusterMaterial.Value.Y + cellInfo.clusterOrientation = clusterMaterial.Value.Z + elseif clusterMaterial:IsA("IntValue") then + cellInfo.Material = clusterMaterial.Value + end + end + + pcall(function() mouseTarget = Mouse.Target end) + + if mouseTarget and mouseTarget.Parent:FindFirstChild("RobloxModel") == nil then + game:GetService("JointsService"):SetJoinAfterMoveTarget(mouseTarget) + else + game:GetService("JointsService"):SetJoinAfterMoveTarget(nil) + end + game:GetService("JointsService"):ShowPermissibleJoints() + + for index, object in pairs(stampData.DisabledScripts) do + if object.Name == "GhostRemovalScript" then + object.Parent = stampData.CurrentParts + end + end + + stampData.Dragger = Instance.new("Dragger") + + --Begin a movement by faking a MouseDown signal + stampData.Dragger:MouseDown(parts[1], Vector3.new(0,0,0), parts) + stampData.Dragger:MouseUp() + + DoStamperMouseMove(Mouse) + + table.insert(mouseCons,Mouse.Move:connect(function() + if movingLock or stampUpLock then return end + movingLock = true + DoStamperMouseMove(Mouse) + movingLock = false + end)) + + table.insert(mouseCons,Mouse.Button1Down:connect(function() + DoStamperMouseDown(Mouse) + end)) + + table.insert(mouseCons,Mouse.Button1Up:connect(function() + stampUpLock = true + while movingLock do wait() end + stamped.Value = DoStamperMouseUp(Mouse) + resetHighScalabilityLine() + stampUpLock = false + end)) + + stamped.Value = false + end + + local function resetStamperState(newModelToStamp) + + -- if we have a new model, swap it out + if newModelToStamp then + if not newModelToStamp:IsA("Model") and not newModelToStamp:IsA("BasePart") then + error("resetStamperState: newModelToStamp (first arg) is not nil, but not a model or part!") + end + modelToStamp = newModelToStamp + end + + -- first clear our state + pauseStamper() + -- now lets load in the new model + resumeStamper() + + end + + -- load the model initially + resetStamperState() + + + -- setup the control table we pass back to the user + control.Stamped = stamped -- BoolValue that fires when user stamps + control.Paused = false + + control.LoadNewModel = function(newStampModel) -- allows us to specify a new stamper model to be used with this stamper + if newStampModel and not newStampModel:IsA("Model") and not newStampModel:IsA("BasePart") then + error("Control.LoadNewModel: newStampModel (first arg) is not a Model or Part!") + return nil + end + resetStamperState(newStampModel) + end + + control.ReloadModel = function() -- will automatically set stamper to get a new model of current model and start stamping with new model + resetStamperState() + end + + control.Pause = function() -- temporarily stops stamping, use resume to start up again + if not control.Paused then + pauseStamper() + control.Paused = true + else + print("RbxStamper Warning: Tried to call Control.Pause() when already paused") + end + end + + control.Resume = function() -- resumes stamping, if currently paused + if control.Paused then + resumeStamper() + control.Paused = false + else + print("RbxStamper Warning: Tried to call Control.Resume() without Pausing First") + end + end + + control.ResetRotation = function() -- resets the model rotation so new models are at default orientation + -- gInitial90DegreeRotations = 0 + -- Note: This function will not always work quite the way we want it to; we will have to build this out further so it works with + -- High-Scalability and with the new model orientation setting methods (model:ResetOrientationToIdentity()) [HotThoth] + end + + control.Destroy = function() -- Stops current Stamp operation and destroys control construct + for i = 1, #mouseCons do + mouseCons[i]:disconnect() + mouseCons[i] = nil + end + + if keyCon then + keyCon:disconnect() + end + + game:GetService("JointsService"):ClearJoinAfterMoveJoints() + + if adorn then adorn:Destroy() end + if adornPart then adornPart:Destroy() end + if errorBox then errorBox:Destroy() end + if stampData then + if stampData["Dragger"] then + stampData.Dragger:Destroy() + end + if stampData.CurrentParts then + stampData.CurrentParts:Destroy() + end + end + if control and control["Stamped"] then + control.Stamped:Destroy() + end + control = nil + end + + return control +end + +t.Help = + function(funcNameOrFunc) + --input argument can be a string or a function. Should return a description (of arguments and expected side effects) + if funcNameOrFunc == "GetStampModel" or funcNameOrFunc == t.GetStampModel then + return "Function GetStampModel. Arguments: assetId, useAssetVersionId. assetId is the asset to load in, define useAssetVersionId as true if assetId is a version id instead of a relative assetId. Side effect: returns a model of the assetId, or a string with error message if something fails" + end + if funcNameOrFunc == "SetupStamperDragger" or funcNameOrFunc == t.SetupStamperDragger then + return "Function SetupStamperDragger. Side Effect: Creates 4x4 stamping mechanism for building out parts quickly. Arguments: ModelToStamp, Mouse, LegalStampCheckFunction. ModelToStamp should be a Model or Part, preferrably loaded from RbxStamper.GetStampModel and should have extents that are multiples of 4. Mouse should be a mouse object (obtained from things such as Tool.OnEquipped), used to drag parts around 'stamp' them out. LegalStampCheckFunction is optional, used as a callback with a table argument (table is full of instances about to be stamped). Function should return either true or false, false stopping the stamp action." + end + end + +return t diff --git a/Client2018/content/scripts/CoreScripts/Libraries/RbxUtility.lua b/Client2018/content/scripts/CoreScripts/Libraries/RbxUtility.lua new file mode 100644 index 0000000..3f239ac --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Libraries/RbxUtility.lua @@ -0,0 +1,1134 @@ +local t = {} + +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------JSON Functions Begin---------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + + --JSON Encoder and Parser for Lua 5.1 + -- + --Copyright 2007 Shaun Brown (http://www.chipmunkav.com) + --All Rights Reserved. + + --Permission is hereby granted, free of charge, to any person + --obtaining a copy of this software to deal in the Software without + --restriction, including without limitation the rights to use, + --copy, modify, merge, publish, distribute, sublicense, and/or + --sell copies of the Software, and to permit persons to whom the + --Software is furnished to do so, subject to the following conditions: + + --The above copyright notice and this permission notice shall be + --included in all copies or substantial portions of the Software. + --If you find this software useful please give www.chipmunkav.com a mention. + + --THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + --EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + --OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + --IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + --ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + --CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + --CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +local string = string +local math = math +local table = table +local error = error +local tonumber = tonumber +local tostring = tostring +local type = type +local setmetatable = setmetatable +local pairs = pairs +local ipairs = ipairs +local assert = assert + + +local StringBuilder = { + buffer = {} +} + +function StringBuilder:New() + local o = {} + setmetatable(o, self) + self.__index = self + o.buffer = {} + return o +end + +function StringBuilder:Append(s) + self.buffer[#self.buffer+1] = s +end + +function StringBuilder:ToString() + return table.concat(self.buffer) +end + +local JsonWriter = { + backslashes = { + ['\b'] = "\\b", + ['\t'] = "\\t", + ['\n'] = "\\n", + ['\f'] = "\\f", + ['\r'] = "\\r", + ['"'] = "\\\"", + ['\\'] = "\\\\", + ['/'] = "\\/" + } +} + +function JsonWriter:New() + local o = {} + o.writer = StringBuilder:New() + setmetatable(o, self) + self.__index = self + return o +end + +function JsonWriter:Append(s) + self.writer:Append(s) +end + +function JsonWriter:ToString() + return self.writer:ToString() +end + +function JsonWriter:Write(o) + local t = type(o) + if t == "nil" then + self:WriteNil() + elseif t == "boolean" then + self:WriteString(o) + elseif t == "number" then + self:WriteString(o) + elseif t == "string" then + self:ParseString(o) + elseif t == "table" then + self:WriteTable(o) + elseif t == "function" then + self:WriteFunction(o) + elseif t == "thread" then + self:WriteError(o) + elseif t == "userdata" then + self:WriteError(o) + end +end + +function JsonWriter:WriteNil() + self:Append("null") +end + +function JsonWriter:WriteString(o) + self:Append(tostring(o)) +end + +function JsonWriter:ParseString(s) + self:Append('"') + self:Append(string.gsub(s, "[%z%c\\\"/]", function(n) + local c = self.backslashes[n] + if c then return c end + return string.format("\\u%.4X", string.byte(n)) + end)) + self:Append('"') +end + +function JsonWriter:IsArray(t) + local count = 0 + local isindex = function(k) + if type(k) == "number" and k > 0 then + if math.floor(k) == k then + return true + end + end + return false + end + for k,v in pairs(t) do + if not isindex(k) then + return false, '{', '}' + else + count = math.max(count, k) + end + end + return true, '[', ']', count +end + +function JsonWriter:WriteTable(t) + local ba, st, et, n = self:IsArray(t) + self:Append(st) + if ba then + for i = 1, n do + self:Write(t[i]) + if i < n then + self:Append(',') + end + end + else + local first = true; + for k, v in pairs(t) do + if not first then + self:Append(',') + end + first = false; + self:ParseString(k) + self:Append(':') + self:Write(v) + end + end + self:Append(et) +end + +function JsonWriter:WriteError(o) + error(string.format( + "Encoding of %s unsupported", + tostring(o))) +end + +function JsonWriter:WriteFunction(o) + if o == Null then + self:WriteNil() + else + self:WriteError(o) + end +end + +local StringReader = { + s = "", + i = 0 +} + +function StringReader:New(s) + local o = {} + setmetatable(o, self) + self.__index = self + o.s = s or o.s + return o +end + +function StringReader:Peek() + local i = self.i + 1 + if i <= #self.s then + return string.sub(self.s, i, i) + end + return nil +end + +function StringReader:Next() + self.i = self.i+1 + if self.i <= #self.s then + return string.sub(self.s, self.i, self.i) + end + return nil +end + +function StringReader:All() + return self.s +end + +local JsonReader = { + escapes = { + ['t'] = '\t', + ['n'] = '\n', + ['f'] = '\f', + ['r'] = '\r', + ['b'] = '\b', + } +} + +function JsonReader:New(s) + local o = {} + o.reader = StringReader:New(s) + setmetatable(o, self) + self.__index = self + return o; +end + +function JsonReader:Read() + self:SkipWhiteSpace() + local peek = self:Peek() + if peek == nil then + error(string.format( + "Nil string: '%s'", + self:All())) + elseif peek == '{' then + return self:ReadObject() + elseif peek == '[' then + return self:ReadArray() + elseif peek == '"' then + return self:ReadString() + elseif string.find(peek, "[%+%-%d]") then + return self:ReadNumber() + elseif peek == 't' then + return self:ReadTrue() + elseif peek == 'f' then + return self:ReadFalse() + elseif peek == 'n' then + return self:ReadNull() + elseif peek == '/' then + self:ReadComment() + return self:Read() + else + return nil + end +end + +function JsonReader:ReadTrue() + self:TestReservedWord{'t','r','u','e'} + return true +end + +function JsonReader:ReadFalse() + self:TestReservedWord{'f','a','l','s','e'} + return false +end + +function JsonReader:ReadNull() + self:TestReservedWord{'n','u','l','l'} + return nil +end + +function JsonReader:TestReservedWord(t) + for i, v in ipairs(t) do + if self:Next() ~= v then + error(string.format( + "Error reading '%s': %s", + table.concat(t), + self:All())) + end + end +end + +function JsonReader:ReadNumber() + local result = self:Next() + local peek = self:Peek() + while peek ~= nil and string.find( + peek, + "[%+%-%d%.eE]") do + result = result .. self:Next() + peek = self:Peek() + end + result = tonumber(result) + if result == nil then + error(string.format( + "Invalid number: '%s'", + result)) + else + return result + end +end + +function JsonReader:ReadString() + local result = "" + assert(self:Next() == '"') + while self:Peek() ~= '"' do + local ch = self:Next() + if ch == '\\' then + ch = self:Next() + if self.escapes[ch] then + ch = self.escapes[ch] + end + end + result = result .. ch + end + assert(self:Next() == '"') + local fromunicode = function(m) + return string.char(tonumber(m, 16)) + end + return string.gsub( + result, + "u%x%x(%x%x)", + fromunicode) +end + +function JsonReader:ReadComment() + assert(self:Next() == '/') + local second = self:Next() + if second == '/' then + self:ReadSingleLineComment() + elseif second == '*' then + self:ReadBlockComment() + else + error(string.format( + "Invalid comment: %s", + self:All())) + end +end + +function JsonReader:ReadBlockComment() + local done = false + while not done do + local ch = self:Next() + if ch == '*' and self:Peek() == '/' then + done = true + end + if not done and + ch == '/' and + self:Peek() == "*" then + error(string.format( + "Invalid comment: %s, '/*' illegal.", + self:All())) + end + end + self:Next() +end + +function JsonReader:ReadSingleLineComment() + local ch = self:Next() + while ch ~= '\r' and ch ~= '\n' do + ch = self:Next() + end +end + +function JsonReader:ReadArray() + local result = {} + assert(self:Next() == '[') + local done = false + if self:Peek() == ']' then + done = true; + end + while not done do + local item = self:Read() + result[#result+1] = item + self:SkipWhiteSpace() + if self:Peek() == ']' then + done = true + end + if not done then + local ch = self:Next() + if ch ~= ',' then + error(string.format( + "Invalid array: '%s' due to: '%s'", + self:All(), ch)) + end + end + end + assert(']' == self:Next()) + return result +end + +function JsonReader:ReadObject() + local result = {} + assert(self:Next() == '{') + local done = false + if self:Peek() == '}' then + done = true + end + while not done do + local key = self:Read() + if type(key) ~= "string" then + error(string.format( + "Invalid non-string object key: %s", + key)) + end + self:SkipWhiteSpace() + local ch = self:Next() + if ch ~= ':' then + error(string.format( + "Invalid object: '%s' due to: '%s'", + self:All(), + ch)) + end + self:SkipWhiteSpace() + local val = self:Read() + result[key] = val + self:SkipWhiteSpace() + if self:Peek() == '}' then + done = true + end + if not done then + ch = self:Next() + if ch ~= ',' then + error(string.format( + "Invalid array: '%s' near: '%s'", + self:All(), + ch)) + end + end + end + assert(self:Next() == "}") + return result +end + +function JsonReader:SkipWhiteSpace() + local p = self:Peek() + while p ~= nil and string.find(p, "[%s/]") do + if p == '/' then + self:ReadComment() + else + self:Next() + end + p = self:Peek() + end +end + +function JsonReader:Peek() + return self.reader:Peek() +end + +function JsonReader:Next() + return self.reader:Next() +end + +function JsonReader:All() + return self.reader:All() +end + +function Encode(o) + local writer = JsonWriter:New() + writer:Write(o) + return writer:ToString() +end + +function Decode(s) + local reader = JsonReader:New(s) + return reader:Read() +end + +function Null() + return Null +end +-------------------- End JSON Parser ------------------------ + +t.DecodeJSON = function(jsonString) + pcall(function() warn("RbxUtility.DecodeJSON is deprecated, please use Game:GetService('HttpService'):JSONDecode() instead.") end) + + if type(jsonString) == "string" then + return Decode(jsonString) + end + print("RbxUtil.DecodeJSON expects string argument!") + return nil +end + +t.EncodeJSON = function(jsonTable) + pcall(function() warn("RbxUtility.EncodeJSON is deprecated, please use Game:GetService('HttpService'):JSONEncode() instead.") end) + return Encode(jsonTable) +end + + + + + + + + +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +--------------------------------------------Terrain Utilities Begin----------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +--makes a wedge at location x, y, z +--sets cell x, y, z to default material if parameter is provided, if not sets cell x, y, z to be whatever material it previously w +--returns true if made a wedge, false if the cell remains a block +t.MakeWedge = function(x, y, z, defaultmaterial) + return game:GetService("Terrain"):AutoWedgeCell(x,y,z) +end + +t.SelectTerrainRegion = function(regionToSelect, color, selectEmptyCells, selectionParent) + local terrain = game:GetService("Workspace"):FindFirstChild("Terrain") + if not terrain then return end + + assert(regionToSelect) + assert(color) + + if not type(regionToSelect) == "Region3" then + error("regionToSelect (first arg), should be of type Region3, but is type",type(regionToSelect)) + end + if not type(color) == "BrickColor" then + error("color (second arg), should be of type BrickColor, but is type",type(color)) + end + + -- frequently used terrain calls (speeds up call, no lookup necessary) + local GetCell = terrain.GetCell + local WorldToCellPreferSolid = terrain.WorldToCellPreferSolid + local CellCenterToWorld = terrain.CellCenterToWorld + local emptyMaterial = Enum.CellMaterial.Empty + + -- container for all adornments, passed back to user + local selectionContainer = Instance.new("Model") + selectionContainer.Name = "SelectionContainer" + selectionContainer.Archivable = false + if selectionParent then + selectionContainer.Parent = selectionParent + else + selectionContainer.Parent = game:GetService("Workspace") + end + + local updateSelection = nil -- function we return to allow user to update selection + local currentKeepAliveTag = nil -- a tag that determines whether adorns should be destroyed + local aliveCounter = 0 -- helper for currentKeepAliveTag + local lastRegion = nil -- used to stop updates that do nothing + local adornments = {} -- contains all adornments + local reusableAdorns = {} + + local selectionPart = Instance.new("Part") + selectionPart.Name = "SelectionPart" + selectionPart.Transparency = 1 + selectionPart.Anchored = true + selectionPart.Locked = true + selectionPart.CanCollide = false + selectionPart.Size = Vector3.new(4.2,4.2,4.2) + + local selectionBox = Instance.new("SelectionBox") + + -- srs translation from region3 to region3int16 + local function Region3ToRegion3int16(region3) + local theLowVec = region3.CFrame.p - (region3.Size/2) + Vector3.new(2,2,2) + local lowCell = WorldToCellPreferSolid(terrain,theLowVec) + + local theHighVec = region3.CFrame.p + (region3.Size/2) - Vector3.new(2,2,2) + local highCell = WorldToCellPreferSolid(terrain, theHighVec) + + local highIntVec = Vector3int16.new(highCell.x,highCell.y,highCell.z) + local lowIntVec = Vector3int16.new(lowCell.x,lowCell.y,lowCell.z) + + return Region3int16.new(lowIntVec,highIntVec) + end + + -- helper function that creates the basis for a selection box + function createAdornment(theColor) + local selectionPartClone = nil + local selectionBoxClone = nil + + if #reusableAdorns > 0 then + selectionPartClone = reusableAdorns[1]["part"] + selectionBoxClone = reusableAdorns[1]["box"] + table.remove(reusableAdorns,1) + + selectionBoxClone.Visible = true + else + selectionPartClone = selectionPart:Clone() + selectionPartClone.Archivable = false + + selectionBoxClone = selectionBox:Clone() + selectionBoxClone.Archivable = false + + selectionBoxClone.Adornee = selectionPartClone + selectionBoxClone.Parent = selectionContainer + + selectionBoxClone.Adornee = selectionPartClone + + selectionBoxClone.Parent = selectionContainer + end + + if theColor then + selectionBoxClone.Color = theColor + end + + return selectionPartClone, selectionBoxClone + end + + -- iterates through all current adornments and deletes any that don't have latest tag + function cleanUpAdornments() + for cellPos, adornTable in pairs(adornments) do + + if adornTable.KeepAlive ~= currentKeepAliveTag then -- old news, we should get rid of this + adornTable.SelectionBox.Visible = false + table.insert(reusableAdorns,{part = adornTable.SelectionPart, box = adornTable.SelectionBox}) + adornments[cellPos] = nil + end + end + end + + -- helper function to update tag + function incrementAliveCounter() + aliveCounter = aliveCounter + 1 + if aliveCounter > 1000000 then + aliveCounter = 0 + end + return aliveCounter + end + + -- finds full cells in region and adorns each cell with a box, with the argument color + function adornFullCellsInRegion(region, color) + local regionBegin = region.CFrame.p - (region.Size/2) + Vector3.new(2,2,2) + local regionEnd = region.CFrame.p + (region.Size/2) - Vector3.new(2,2,2) + + local cellPosBegin = WorldToCellPreferSolid(terrain, regionBegin) + local cellPosEnd = WorldToCellPreferSolid(terrain, regionEnd) + + currentKeepAliveTag = incrementAliveCounter() + for y = cellPosBegin.y, cellPosEnd.y do + for z = cellPosBegin.z, cellPosEnd.z do + for x = cellPosBegin.x, cellPosEnd.x do + local cellMaterial = GetCell(terrain, x, y, z) + + if cellMaterial ~= emptyMaterial then + local cframePos = CellCenterToWorld(terrain, x, y, z) + local cellPos = Vector3int16.new(x,y,z) + + local updated = false + for cellPosAdorn, adornTable in pairs(adornments) do + if cellPosAdorn == cellPos then + adornTable.KeepAlive = currentKeepAliveTag + if color then + adornTable.SelectionBox.Color = color + end + updated = true + break + end + end + + if not updated then + local selectionPart, selectionBox = createAdornment(color) + selectionPart.Size = Vector3.new(4,4,4) + selectionPart.CFrame = CFrame.new(cframePos) + local adornTable = {SelectionPart = selectionPart, SelectionBox = selectionBox, KeepAlive = currentKeepAliveTag} + adornments[cellPos] = adornTable + end + end + end + end + end + cleanUpAdornments() + end + + + ------------------------------------- setup code ------------------------------ + lastRegion = regionToSelect + + if selectEmptyCells then -- use one big selection to represent the area selected + local selectionPart, selectionBox = createAdornment(color) + + selectionPart.Size = regionToSelect.Size + selectionPart.CFrame = regionToSelect.CFrame + + adornments.SelectionPart = selectionPart + adornments.SelectionBox = selectionBox + + updateSelection = + function (newRegion, color) + if newRegion and newRegion ~= lastRegion then + lastRegion = newRegion + selectionPart.Size = newRegion.Size + selectionPart.CFrame = newRegion.CFrame + end + if color then + selectionBox.Color = color + end + end + else -- use individual cell adorns to represent the area selected + adornFullCellsInRegion(regionToSelect, color) + updateSelection = + function (newRegion, color) + if newRegion and newRegion ~= lastRegion then + lastRegion = newRegion + adornFullCellsInRegion(newRegion, color) + end + end + + end + + local destroyFunc = function() + updateSelection = nil + if selectionContainer then selectionContainer:Destroy() end + adornments = nil + end + + return updateSelection, destroyFunc +end + +-----------------------------Terrain Utilities End----------------------------- + + + + + + + +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------Signal class begin------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +--[[ +A 'Signal' object identical to the internal RBXScriptSignal object in it's public API and semantics. This function +can be used to create "custom events" for user-made code. +API: +Method :connect( function handler ) + Arguments: The function to connect to. + Returns: A new connection object which can be used to disconnect the connection + Description: Connects this signal to the function specified by |handler|. That is, when |fire( ... )| is called for + the signal the |handler| will be called with the arguments given to |fire( ... )|. Note, the functions + connected to a signal are called in NO PARTICULAR ORDER, so connecting one function after another does + NOT mean that the first will be called before the second as a result of a call to |fire|. + +Method :disconnect() + Arguments: None + Returns: None + Description: Disconnects all of the functions connected to this signal. + +Method :fire( ... ) + Arguments: Any arguments are accepted + Returns: None + Description: Calls all of the currently connected functions with the given arguments. + +Method :wait() + Arguments: None + Returns: The arguments given to fire + Description: This call blocks until +]] + +function t.CreateSignal() + local this = {} + + local mBindableEvent = Instance.new('BindableEvent') + local mAllCns = {} --all connection objects returned by mBindableEvent::connect + + --main functions + function this:connect(func) + if self ~= this then error("connect must be called with `:`, not `.`", 2) end + if type(func) ~= 'function' then + error("Argument #1 of connect must be a function, got a "..type(func), 2) + end + local cn = mBindableEvent.Event:Connect(func) + mAllCns[cn] = true + local pubCn = {} + function pubCn:disconnect() + cn:Disconnect() + mAllCns[cn] = nil + end + pubCn.Disconnect = pubCn.disconnect + + return pubCn + end + + function this:disconnect() + if self ~= this then error("disconnect must be called with `:`, not `.`", 2) end + for cn, _ in pairs(mAllCns) do + cn:Disconnect() + mAllCns[cn] = nil + end + end + + function this:wait() + if self ~= this then error("wait must be called with `:`, not `.`", 2) end + return mBindableEvent.Event:Wait() + end + + function this:fire(...) + if self ~= this then error("fire must be called with `:`, not `.`", 2) end + mBindableEvent:Fire(...) + end + + this.Connect = this.connect + this.Disconnect = this.disconnect + this.Wait = this.wait + this.Fire = this.fire + + return this +end + +------------------------------------------------- Sigal class End ------------------------------------------------------ + + + + +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +-----------------------------------------------Create Function Begins--------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +--[[ +A "Create" function for easy creation of Roblox instances. The function accepts a string which is the classname of +the object to be created. The function then returns another function which either accepts accepts no arguments, in +which case it simply creates an object of the given type, or a table argument that may contain several types of data, +in which case it mutates the object in varying ways depending on the nature of the aggregate data. These are the +type of data and what operation each will perform: +1) A string key mapping to some value: + Key-Value pairs in this form will be treated as properties of the object, and will be assigned in NO PARTICULAR + ORDER. If the order in which properties is assigned matter, then they must be assigned somewhere else than the + |Create| call's body. + +2) An integral key mapping to another Instance: + Normal numeric keys mapping to Instances will be treated as children if the object being created, and will be + parented to it. This allows nice recursive calls to Create to create a whole hierarchy of objects without a + need for temporary variables to store references to those objects. + +3) A key which is a value returned from Create.Event( eventname ), and a value which is a function function + The Create.E( string ) function provides a limited way to connect to signals inside of a Create hierarchy + for those who really want such a functionality. The name of the event whose name is passed to + Create.E( string ) + +4) A key which is the Create function itself, and a value which is a function + The function will be run with the argument of the object itself after all other initialization of the object is + done by create. This provides a way to do arbitrary things involving the object from withing the create + hierarchy. + Note: This function is called SYNCHRONOUSLY, that means that you should only so initialization in + it, not stuff which requires waiting, as the Create call will block until it returns. While waiting in the + constructor callback function is possible, it is probably not a good design choice. + Note: Since the constructor function is called after all other initialization, a Create block cannot have two + constructor functions, as it would not be possible to call both of them last, also, this would be unnecessary. + + +Some example usages: + +A simple example which uses the Create function to create a model object and assign two of it's properties. +local model = Create'Model'{ + Name = 'A New model', + Parent = game.Workspace, +} + + +An example where a larger hierarchy of object is made. After the call the hierarchy will look like this: +Model_Container + |-ObjectValue + | | + | `-BoolValueChild + `-IntValue + +local model = Create'Model'{ + Name = 'Model_Container', + Create'ObjectValue'{ + Create'BoolValue'{ + Name = 'BoolValueChild', + }, + }, + Create'IntValue'{}, +} + + +An example using the event syntax: + +local part = Create'Part'{ + [Create.E'Touched'] = function(part) + print("I was touched by "..part.Name) + end, +} + + +An example using the general constructor syntax: + +local model = Create'Part'{ + [Create] = function(this) + print("Constructor running!") + this.Name = GetGlobalFoosAndBars(this) + end, +} + + +Note: It is also perfectly legal to save a reference to the function returned by a call Create, this will not cause + any unexpected behavior. EG: + local partCreatingFunction = Create'Part' + local part = partCreatingFunction() +]] + +--the Create function need to be created as a functor, not a function, in order to support the Create.E syntax, so it +--will be created in several steps rather than as a single function declaration. +local function Create_PrivImpl(objectType) + if type(objectType) ~= 'string' then + error("Argument of Create must be a string", 2) + end + --return the proxy function that gives us the nice Create'string'{data} syntax + --The first function call is a function call using Lua's single-string-argument syntax + --The second function call is using Lua's single-table-argument syntax + --Both can be chained together for the nice effect. + return function(dat) + --default to nothing, to handle the no argument given case + dat = dat or {} + + --make the object to mutate + local obj = Instance.new(objectType) + local parent = nil + + --stored constructor function to be called after other initialization + local ctor = nil + + for k, v in pairs(dat) do + --add property + if type(k) == 'string' then + if k == 'Parent' then + -- Parent should always be set last, setting the Parent of a new object + -- immediately makes performance worse for all subsequent property updates. + parent = v + else + obj[k] = v + end + + + --add child + elseif type(k) == 'number' then + if type(v) ~= 'userdata' then + error("Bad entry in Create body: Numeric keys must be paired with children, got a: "..type(v), 2) + end + v.Parent = obj + + + --event connect + elseif type(k) == 'table' and k.__eventname then + if type(v) ~= 'function' then + error("Bad entry in Create body: Key `[Create.E\'"..k.__eventname.."\']` must have a function value\ + got: "..tostring(v), 2) + end + obj[k.__eventname]:connect(v) + + + --define constructor function + elseif k == t.Create then + if type(v) ~= 'function' then + error("Bad entry in Create body: Key `[Create]` should be paired with a constructor function, \ + got: "..tostring(v), 2) + elseif ctor then + --ctor already exists, only one allowed + error("Bad entry in Create body: Only one constructor function is allowed", 2) + end + ctor = v + + + else + error("Bad entry ("..tostring(k).." => "..tostring(v)..") in Create body", 2) + end + end + + --apply constructor function if it exists + if ctor then + ctor(obj) + end + + if parent then + obj.Parent = parent + end + + --return the completed object + return obj + end +end + +--now, create the functor: +t.Create = setmetatable({}, {__call = function(tb, ...) return Create_PrivImpl(...) end}) + +--and create the "Event.E" syntax stub. Really it's just a stub to construct a table which our Create +--function can recognize as special. +t.Create.E = function(eventName) + return {__eventname = eventName} +end + +-------------------------------------------------Create function End---------------------------------------------------- + + + + +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------Documentation Begin----------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + +t.Help = + function(funcNameOrFunc) + --input argument can be a string or a function. Should return a description (of arguments and expected side effects) + if funcNameOrFunc == "DecodeJSON" or funcNameOrFunc == t.DecodeJSON then + return "Function DecodeJSON. " .. + "Arguments: (string). " .. + "Side effect: returns a table with all parsed JSON values" + end + if funcNameOrFunc == "EncodeJSON" or funcNameOrFunc == t.EncodeJSON then + return "Function EncodeJSON. " .. + "Arguments: (table). " .. + "Side effect: returns a string composed of argument table in JSON data format" + end + if funcNameOrFunc == "MakeWedge" or funcNameOrFunc == t.MakeWedge then + return "Function MakeWedge. " .. + "Arguments: (x, y, z, [default material]). " .. + "Description: Makes a wedge at location x, y, z. Sets cell x, y, z to default material if ".. + "parameter is provided, if not sets cell x, y, z to be whatever material it previously was. ".. + "Returns true if made a wedge, false if the cell remains a block " + end + if funcNameOrFunc == "SelectTerrainRegion" or funcNameOrFunc == t.SelectTerrainRegion then + return "Function SelectTerrainRegion. " .. + "Arguments: (regionToSelect, color, selectEmptyCells, selectionParent). " .. + "Description: Selects all terrain via a series of selection boxes within the regionToSelect " .. + "(this should be a region3 value). The selection box color is detemined by the color argument " .. + "(should be a brickcolor value). SelectionParent is the parent that the selection model gets placed to (optional)." .. + "SelectEmptyCells is bool, when true will select all cells in the " .. + "region, otherwise we only select non-empty cells. Returns a function that can update the selection," .. + "arguments to said function are a new region3 to select, and the adornment color (color arg is optional). " .. + "Also returns a second function that takes no arguments and destroys the selection" + end + if funcNameOrFunc == "CreateSignal" or funcNameOrFunc == t.CreateSignal then + return "Function CreateSignal. ".. + "Arguments: None. ".. + "Returns: The newly created Signal object. This object is identical to the RBXScriptSignal class ".. + "used for events in Objects, but is a Lua-side object so it can be used to create custom events in".. + "Lua code. ".. + "Methods of the Signal object: :connect, :wait, :fire, :disconnect. ".. + "For more info you can pass the method name to the Help function, or view the wiki page ".. + "for this library. EG: Help('Signal:connect')." + end + if funcNameOrFunc == "Signal:connect" then + return "Method Signal:connect. ".. + "Arguments: (function handler). ".. + "Return: A connection object which can be used to disconnect the connection to this handler. ".. + "Description: Connectes a handler function to this Signal, so that when |fire| is called the ".. + "handler function will be called with the arguments passed to |fire|." + end + if funcNameOrFunc == "Signal:wait" then + return "Method Signal:wait. ".. + "Arguments: None. ".. + "Returns: The arguments passed to the next call to |fire|. ".. + "Description: This call does not return until the next call to |fire| is made, at which point it ".. + "will return the values which were passed as arguments to that |fire| call." + end + if funcNameOrFunc == "Signal:fire" then + return "Method Signal:fire. ".. + "Arguments: Any number of arguments of any type. ".. + "Returns: None. ".. + "Description: This call will invoke any connected handler functions, and notify any waiting code ".. + "attached to this Signal to continue, with the arguments passed to this function. Note: The calls ".. + "to handlers are made asynchronously, so this call will return immediately regardless of how long ".. + "it takes the connected handler functions to complete." + end + if funcNameOrFunc == "Signal:disconnect" then + return "Method Signal:disconnect. ".. + "Arguments: None. ".. + "Returns: None. ".. + "Description: This call disconnects all handlers attacched to this function, note however, it ".. + "does NOT make waiting code continue, as is the behavior of normal Roblox events. This method ".. + "can also be called on the connection object which is returned from Signal:connect to only ".. + "disconnect a single handler, as opposed to this method, which will disconnect all handlers." + end + if funcNameOrFunc == "Create" then + return "Function Create. ".. + "Arguments: A table containing information about how to construct a collection of objects. ".. + "Returns: The constructed objects. ".. + "Descrition: Create is a very powerfull function, whose description is too long to fit here, and ".. + "is best described via example, please see the wiki page for a description of how to use it." + end + end + +--------------------------------------------Documentation Ends---------------------------------------------------------- + +return t + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Client2018/content/scripts/CoreScripts/LoadingScript.lua b/Client2018/content/scripts/CoreScripts/LoadingScript.lua new file mode 100644 index 0000000..ff39bc5 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/LoadingScript.lua @@ -0,0 +1,993 @@ +-- Creates the generic "ROBLOX" loading screen on startup +-- Written by ArceusInator & Ben Tkacheff, 2014 +-- Updates by 0xBAADF00D, 2017 +local AssetService = game:GetService('AssetService') +local MarketplaceService = game:GetService("MarketplaceService") +local UserInputService = game:GetService("UserInputService") +local VRService = game:GetService("VRService") +local GuiService = game:GetService("GuiService") +local ContextActionService = game:GetService("ContextActionService") +local RunService = game:GetService("RunService") +local ContentProvider = game:GetService("ContentProvider") +local RobloxGui = game:GetService("CoreGui"):WaitForChild("RobloxGui") + +--FFlags +local FFlagLoadTheLoadingScreenFasterSuccess, FFlagLoadTheLoadingScreenFasterValue = pcall(function() return settings():GetFFlag("LoadTheLoadingScreenFaster") end) +local FFlagLoadTheLoadingScreenFaster = FFlagLoadTheLoadingScreenFasterSuccess and FFlagLoadTheLoadingScreenFasterValue + +-- Remove when remove PresetInGameGuiInset +local FFlagSetGuiInsetInLoadingScript = settings():GetFFlag("SetGuiInsetInLoadingScript3") +local FFlagFixLoadingScreenJankiness = settings():GetFFlag("FixLoadingScreenJankiness") +local FFlagLoadingScreenUseLocalizationTable = settings():GetFFlag("LoadingScreenUseLocalizationTable") +local FFlagPresetInGameGuiInset = settings():GetFFlag("PresetInGameGuiInset") +local FFlagLoadingScreenNewTextLayout = settings():GetFFlag("LoadingScreenNewTextLayout") + +-- Remove when removing PresetInGameGuiInset +if FFlagSetGuiInsetInLoadingScript then + coroutine.wrap(function() + local TopbarConstants = require(RobloxGui:WaitForChild("Modules"):WaitForChild("TopbarConstants")) + GuiService:SetGlobalGuiInset(0, TopbarConstants.TOPBAR_THICKNESS, 0, 0) + end)() +end + +local debugMode = false + +local startTime = tick() +local loadingImageInputBeganConn = nil + +local COLORS = { + BACKGROUND_COLOR = Color3.fromRGB(45, 45, 45), + TEXT_COLOR = Color3.fromRGB(255, 255, 255), + ERROR = Color3.fromRGB(253, 68, 72) +} +local spinnerImageId = "rbxasset://textures/loading/robloxTilt.png" + +local gameIconSubstitutionType = { + None = 0; + Unapproved = 1; + PendingReview = 2; + Broken = 3; + Unavailable = 4; + Unknown = 5; +} + +-- +-- Variables +local GameAssetInfo -- loaded by InfoProvider:LoadAssets() +local currScreenGui, renderSteppedConnection = nil, nil +local destroyingBackground, destroyedLoadingGui, hasReplicatedFirstElements = false, false, false +local isTenFootInterface = GuiService:IsTenFootInterface() +local platform = UserInputService:GetPlatform() + +local placeLabel, creatorLabel = nil, nil +local backgroundFadeStarted = false +local tweenPlaceIcon = nil +local layoutIsReady = false + +local connectionHealthShown = false +local connectionHealthCon + +local function IsConvertMyPlaceNameInXboxAppEnabled() + if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then + local success, flagValue = pcall(function() return settings():GetFFlag("ConvertMyPlaceNameInXboxApp") end) + return (success and flagValue == true) + end + return false +end + +-- +-- Utility functions +local create = function(className, defaultParent) + return function(propertyList) + local object = Instance.new(className) + local parent = nil + + for index, value in next, propertyList do + if typeof(index) == 'string' then + if index == 'Parent' then + parent = value + else + object[index] = value + end + else + local valueType = typeof(value) + if valueType == 'function' then + value(object) + elseif valueType == 'Instance' then + value.Parent = object + end + end + end + + if parent then + object.Parent = parent + end + + if object.Parent == nil then + object.Parent = defaultParent + end + + return object + end +end + +-- +-- Create objects + +local MainGui = {} +local InfoProvider = {} + +local function WaitForPlaceId() + local placeId = game.PlaceId + if placeId == 0 then + game:GetPropertyChangedSignal("PlaceId"):wait() + placeId = game.PlaceId + end + return placeId +end + +local function ExtractGeneratedUsername(gameName) + local tempUsername = string.match(gameName, "^([0-9a-fA-F]+)'s Place$") + if tempUsername and #tempUsername == 32 then + return tempUsername + end +end + +-- Fix places that have been made with incorrect temporary usernames +local function GetFilteredGameName(gameName, creatorName) + if gameName and type(gameName) == 'string' then + local tempUsername = ExtractGeneratedUsername(gameName) + if tempUsername then + local newGameName = string.gsub(gameName, tempUsername, creatorName, 1) + if newGameName then + return newGameName + end + end + end + return gameName +end + + +function InfoProvider:GetGameName() + if GameAssetInfo ~= nil then + if IsConvertMyPlaceNameInXboxAppEnabled() then + return GetFilteredGameName(GameAssetInfo.Name, self:GetCreatorName()) + else + return GameAssetInfo.Name + end + else + return '' + end +end + +function InfoProvider:GetCreatorName() + if GameAssetInfo ~= nil then + return GameAssetInfo.Creator.Name + else + return '' + end +end + +function InfoProvider:LoadAssets() + if FFlagLoadTheLoadingScreenFaster then + coroutine.wrap(function() + local placeId = WaitForPlaceId() + local success, result = pcall(function() + GameAssetInfo = MarketplaceService:GetProductInfo(placeId) + end) + if not success then + print("LoadingScript->InfoProvider:LoadAssets:", result) + end + end)() + else + --spawn() == slowpoke.jpg + spawn(function() + local PLACEID = game.PlaceId + if PLACEID <= 0 then + while game.PlaceId <= 0 do + wait() + end + PLACEID = game.PlaceId + end + + -- load game asset info + coroutine.resume(coroutine.create(function() + local success, result = pcall(function() + GameAssetInfo = MarketplaceService:GetProductInfo(PLACEID) + end) + if not success then + print("LoadingScript->InfoProvider:LoadAssets:", result) + end + end)) + end) + end +end + +-- create a cancel binding for console to be able to cancel anytime while loading +local function createTenfootCancelGui() + local cancelLabel = create'ImageLabel' + { + Name = "CancelLabel", + Size = UDim2.new(0, 83, 0, 83), + Position = UDim2.new(1, -32 - 83, 0, 32), + BackgroundTransparency = 1, + Image = 'rbxasset://textures/ui/Shell/ButtonIcons/BButton.png' + } + local cancelText = create'TextLabel' + { + Name = "CancelText", + Size = UDim2.new(0, 400, 0, 83), + Position = UDim2.new(1, -131, 0, 32), + AnchorPoint = Vector2.new(1, 0), + BackgroundTransparency = 1, + Font = Enum.Font.SourceSans, + TextSize = 48, + TextXAlignment = Enum.TextXAlignment.Right, + TextColor3 = COLORS.TEXT_COLOR, + Text = "Cancel" + } + + if not game:GetService("ReplicatedFirst"):IsFinishedReplicating() then + local seenBButtonBegin = false + ContextActionService:BindCoreAction("CancelGameLoad", + function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + seenBButtonBegin = true + elseif inputState == Enum.UserInputState.End and seenBButtonBegin then + cancelLabel:Destroy() + cancelText.Text = "Canceling..." + cancelText.Position = UDim2.new(1, -32, 0, 32) + ContextActionService:UnbindCoreAction('CancelGameLoad') + game:Shutdown() + end + end, + false, + Enum.KeyCode.ButtonB) + end + + while cancelLabel.Parent == nil do + if currScreenGui then + local blackFrame = currScreenGui:FindFirstChild('BlackFrame') + if blackFrame then + cancelLabel.Parent = blackFrame + cancelText.Parent = blackFrame + break + end + end + wait() + end +end + +-- +-- Declare member functions +function MainGui:GenerateMain() + local screenGui = create 'ScreenGui' { + Name = 'RobloxLoadingGui' + } + + -- + -- create descendant frames + local mainBackgroundContainer + if FFlagPresetInGameGuiInset then + local inGameGlobalGuiInset = settings():GetFVariable("InGameGlobalGuiInset") + mainBackgroundContainer = create 'Frame' { + Name = 'BlackFrame', + BackgroundColor3 = COLORS.BACKGROUND_COLOR, + BackgroundTransparency = 0, + Size = UDim2.new(1, 0, 1, inGameGlobalGuiInset), + Position = UDim2.new(0, 0, 0, -inGameGlobalGuiInset), + Active = true, + Parent = screenGui + } + else + mainBackgroundContainer = create 'Frame' { + Name = 'BlackFrame', + BackgroundColor3 = COLORS.BACKGROUND_COLOR, + BackgroundTransparency = 0, + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + Active = true, + Parent = screenGui + } + end + + -- Remove when remove FFlagPresetInGameGuiInset + if FFlagSetGuiInsetInLoadingScript then + coroutine.wrap(function() + local TopbarConstants = require(RobloxGui:WaitForChild("Modules"):WaitForChild("TopbarConstants")) + mainBackgroundContainer.Size = UDim2.new(1, 0, 1, TopbarConstants.TOPBAR_THICKNESS) + mainBackgroundContainer.Position = UDim2.new(0, 0, 0, -TopbarConstants.TOPBAR_THICKNESS) + end)() + end + + local closeButton = create 'ImageButton' { + Name = 'CloseButton', + Image = 'rbxasset://textures/loading/cancelButton.png', + ImageTransparency = 1, + BackgroundTransparency = 1, + Position = UDim2.new(1, -37, 0, 5), + Size = UDim2.new(0, 32, 0, 32), + Active = false, + ZIndex = 10, + Parent = mainBackgroundContainer + } + + closeButton.MouseButton1Click:connect(function() + game:Shutdown() + end) + + local graphicsFrame = create 'Frame' { + Name = 'GraphicsFrame', + BorderSizePixel = 0, + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(1, 1), + Position = UDim2.new(0.95, 0, 0.95, 0), + Size = UDim2.new(0.15, 0, 0.15, 0), + ZIndex = 2, + Parent = mainBackgroundContainer, + + create("UIAspectRatioConstraint") { + AspectRatio = 1 + }, + create("UISizeConstraint") { + MaxSize = Vector2.new(100, 100) + } + } + + local loadingImage = create 'ImageLabel' { + Name = 'LoadingImage', + BackgroundTransparency = 1, + Image = spinnerImageId, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 2, + Parent = graphicsFrame, + } + + local numberOfTaps = 0 + local lastTapTime = math.huge + local doubleTapTimeThreshold = 0.5 + + loadingImageInputBeganConn = loadingImage.InputBegan:connect(function() + if numberOfTaps == 0 then + numberOfTaps = 1 + lastTapTime = tick() + return + end + + if UserInputService.TouchEnabled == true and UserInputService.MouseEnabled == false then + if tick() - lastTapTime <= doubleTapTimeThreshold then + GuiService:ShowStatsBasedOnInputString("ConnectionHealth") + connectionHealthShown = not connectionHealthShown + end + end + + numberOfTaps = 0 + lastTapTime = math.huge + end) + + local infoFrame = create 'Frame' { + Name = 'InfoFrame', + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0.75, 0, 1, 0), + ZIndex = 2, + Parent = mainBackgroundContainer, + FFlagLoadingScreenNewTextLayout and create 'UIPadding' { + Name = 'UiMessagePadding', + PaddingBottom = UDim.new(0, 25), + } or nil + } + + + local uiMessageFrame = create 'Frame' { + Name = 'UiMessageFrame', + BackgroundTransparency = 1, + Position = UDim2.new(0.25, 0, 1, -120), + Size = UDim2.new(1, 0, 0, 35), + ZIndex = 2, + LayoutOrder = 5, + Parent = infoFrame, + + create 'TextLabel' { + Name = 'UiMessage', + BackgroundTransparency = 1, + Position = UDim2.new(0, 0, 0, FFlagLoadingScreenNewTextLayout and 5 or 10), + Size = UDim2.new(1, 0, 0, 25), + Font = Enum.Font.SourceSansLight, + FontSize = Enum.FontSize.Size18, + TextScaled = true, + TextWrapped = true, + TextColor3 = COLORS.TEXT_COLOR, + Text = "", + TextTransparency = 1, + ZIndex = 2, + } + } + + local infoFrameAspect = create("UIAspectRatioConstraint") { + AspectRatio = 3 / 2, + Parent = infoFrame + } + local infoFrameList = create("UIListLayout") { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + Padding = UDim.new(0.05, 0), + Parent = infoFrame + } + + local textContainer = create("Frame") { + BackgroundTransparency = 1, + Size = UDim2.new(2/3, 0, 1, 0), + LayoutOrder = 2, + Parent = nil, + + create("UIListLayout") { + FillDirection = Enum.FillDirection.Vertical, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + SortOrder = Enum.SortOrder.LayoutOrder + } + } + local placeIcon = create("ImageLabel") { + Name = "PlaceIcon", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(0.5, 0), + LayoutOrder = 1, + Parent = infoFrame, + + ImageTransparency = 1, + Image = "", + + create("UIAspectRatioConstraint") { + AspectRatio = 576 / 324, + AspectType = Enum.AspectType.ScaleWithParentSize, + DominantAxis = Enum.DominantAxis.Width + }, + create("UISizeConstraint") { + MaxSize = Vector2.new(400, 400) + } + } + + --Start trying to load the place icon image + --Web might not have this icon size generated, so we can poll asset-thumbnail/json and check + --the JSON result for thumbnailFinal/Final to see when it's done being generated so we never + --show a N/A image. This is how the console AppShell does it! + coroutine.wrap(function() + local httpService = game:GetService("HttpService") + local placeId = WaitForPlaceId() + + local function tryGetFinalAsync() + local imageUrl = nil + local isGenerated = false + local success, msg = pcall(function() + imageUrl, isGenerated = AssetService:GetAssetThumbnailAsync(placeId, Vector2.new(576, 324), 1) + end) + + if success and isGenerated == true and imageUrl then + ContentProvider:PreloadAsync { imageUrl } + placeIcon.Image = imageUrl + + if not backgroundFadeStarted then + placeIcon.ImageTransparency = 0 + end + + return true + end + + return false + end + + while not tryGetFinalAsync() do end + end)() + + + placeLabel = create 'TextLabel' { + Name = 'PlaceLabel', + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 80), + Position = UDim2.new(0, 0, 0, 0), + Font = Enum.Font.SourceSans, + FontSize = (isTenFootInterface and Enum.FontSize.Size48 or Enum.FontSize.Size24), + TextWrapped = true, + TextScaled = true, + TextColor3 = COLORS.TEXT_COLOR, + TextStrokeTransparency = 1, + TextTransparency = FFlagFixLoadingScreenJankiness and 1 or nil, --setting to nil means it's not in the table at all, so it uses the default value to ensure behavior is the same. It should be 0 either way. + Text = "", + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Bottom, + ZIndex = 2, + LayoutOrder = 2, + Parent = infoFrame + } + + local creatorContainer = create 'Frame' { + Name = "Creator", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 48), + LayoutOrder = 3, + + create 'UIListLayout' { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 5) + } + } + + local byLabel, creatorIcon = nil, nil + if isTenFootInterface then + byLabel = create'TextLabel' { + Name = "ByLabel", + BackgroundTransparency = 1, + Size = UDim2.new(0, 36, 0, 30), + Position = UDim2.new(0, 0, 0, 80), + Font = Enum.Font.SourceSansLight, + FontSize = Enum.FontSize.Size36, + TextScaled = true, + TextColor3 = COLORS.TEXT_COLOR, + TextStrokeTransparency = 1, + Text = "By", + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + ZIndex = 2, + Visible = true, + Parent = infoFrame, + LayoutOrder = 1 + } + creatorIcon = create'ImageLabel' { + Name = "CreatorIcon", + BackgroundTransparency = 1, + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(0, 38, 0, 80), + ImageTransparency = 0, + Image = 'rbxasset://textures/ui/Shell/Icons/RobloxIcon24.png', + ZIndex = 2, + Visible = true, + Parent = infoFrame, + LayoutOrder = 2 + } + end + + creatorLabel = create 'TextLabel' { + Name = 'CreatorLabel', + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 30), + Position = UDim2.new(0, 0, 0, 80), + Font = Enum.Font.SourceSansLight, + FontSize = (isTenFootInterface and Enum.FontSize.Size36 or Enum.FontSize.Size18), + TextWrapped = true, + TextScaled = true, + TextColor3 = COLORS.TEXT_COLOR, + TextStrokeTransparency = 1, + Text = "", + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + ZIndex = 2, + LayoutOrder = 4, + Parent = infoFrame + } + + if isTenFootInterface then + creatorContainer.Parent = infoFrame + + byLabel.TextScaled = false + byLabel.Parent = creatorContainer + byLabel.TextXAlignment = Enum.TextXAlignment.Center + byLabel.TextYAlignment = Enum.TextYAlignment.Center + + creatorIcon.Parent = creatorContainer + + creatorLabel.Parent = creatorContainer + creatorLabel.TextScaled = false + creatorLabel.Position = UDim2.new(0, 72, 0, 80) + creatorLabel.Size = UDim2.new(0, creatorLabel.TextBounds.X, 1, 0) + end + + if FFlagFixLoadingScreenJankiness then + coroutine.wrap(function() + RunService.RenderStepped:wait() + RunService.RenderStepped:wait() + layoutIsReady = true + + placeLabel.TextTransparency = 0 + + local uiMessage = uiMessageFrame.UiMessage + if uiMessage.Text ~= "" then + uiMessage.TextTransparency = 0 + end + end)() + end + + local errorFrame = create 'Frame' { + Name = 'ErrorFrame', + BackgroundColor3 = COLORS.ERROR, + BorderSizePixel = 0, + Position = UDim2.new(0.25,0,0,0), + Size = UDim2.new(0.5, 0, 0, 80), + ZIndex = 8, + Visible = false, + Parent = screenGui, + + create 'TextLabel' { + Name = "ErrorText", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size14, + TextWrapped = true, + TextColor3 = COLORS.TEXT_COLOR, + Text = "", + ZIndex = 8 + } + } + + while not game:GetService("CoreGui") do + if FFlagLoadTheLoadingScreenFaster then + RunService.RenderStepped:wait() + else + wait() + end + end + + local CoreGui = game:GetService("CoreGui"); + screenGui.Parent = CoreGui + + if FFlagLoadingScreenUseLocalizationTable then + infoFrame.RootLocalizationTable = CoreGui:FindFirstChild("CoreScriptLocalization") + end + + currScreenGui = screenGui + + local function onResized() + local isPortrait = screenGui.AbsoluteSize.X < screenGui.AbsoluteSize.Y + + infoFrame.Position = UDim2.new(0.5, 0, 0.5, 0) + infoFrame.AnchorPoint = Vector2.new(0.5, 0.5) + infoFrame.Size = UDim2.new(0.75, 0, 1, 0) + infoFrameAspect.AspectRatio = isPortrait and 2/3 or 3/2 + + placeLabel.Size = UDim2.new(1, 0, 0, isTenFootInterface and 120 or 80) + + if isTenFootInterface then + creatorLabel.Size = UDim2.new(0, creatorLabel.TextBounds.X, 1, 0) + else + creatorLabel.Size = UDim2.new(1, 0, 0, 30) + end + + infoFrameList.FillDirection = Enum.FillDirection.Vertical + infoFrameList.HorizontalAlignment = Enum.HorizontalAlignment.Center + infoFrameList.Padding = UDim.new(0, 0) + textContainer.Parent = nil + infoFrameList:ApplyLayout() + + placeLabel.TextXAlignment = Enum.TextXAlignment.Center + end + onResized() + screenGui:GetPropertyChangedSignal("AbsoluteSize"):connect(onResized) +end + +--------------------------------------------------------- +-- Main Script (show something now + setup connections) + +-- start loading assets asap +InfoProvider:LoadAssets() +MainGui:GenerateMain() +if isTenFootInterface then + createTenfootCancelGui() +end + +local fadeCycleTime = 1.7 +local turnCycleTime = 2 + +local function spinnerEasingFunc(a, b, t) + t = t * 2 + if t < 1 then + return b / 2 * t*t*t + a + else + t = t - 2 + return b / 2 * (t * t * t + 2) + b + end +end + +renderSteppedConnection = RunService.RenderStepped:connect(function(dt) + if not currScreenGui then return end + if not currScreenGui:FindFirstChild("BlackFrame") then return end + + local infoFrame = currScreenGui.BlackFrame:FindFirstChild('InfoFrame') + if infoFrame then + -- set place name + if placeLabel and placeLabel.Text == "" then + placeLabel.Text = InfoProvider:GetGameName() + end + + -- set creator name + if creatorLabel and creatorLabel.Text == "" then + local creatorName = InfoProvider:GetCreatorName() + if creatorName ~= "" then + if isTenFootInterface then + creatorLabel.Text = creatorName + creatorLabel.Size = UDim2.new(0, creatorLabel.TextBounds.X, 1, 0) + else + creatorLabel.Text = "By ".. creatorName + end + end + end + end + + local currentTime = tick() + local fadeAmount = dt * fadeCycleTime + + local spinnerImage = currScreenGui.BlackFrame.GraphicsFrame.LoadingImage + local timeInCycle = currentTime % turnCycleTime + local cycleAlpha = spinnerEasingFunc(0, 1, timeInCycle / turnCycleTime) + spinnerImage.Rotation = cycleAlpha * 360 + + + if not isTenFootInterface then + if currentTime - startTime > 5 and currScreenGui.BlackFrame.CloseButton.ImageTransparency > 0 then + currScreenGui.BlackFrame.CloseButton.ImageTransparency = currScreenGui.BlackFrame.CloseButton.ImageTransparency - fadeAmount + + if currScreenGui.BlackFrame.CloseButton.ImageTransparency <= 0 then + currScreenGui.BlackFrame.CloseButton.Active = true + end + end + end +end) + +--TODO: Evaluate whether or not this is still necessary +-- Remove when remove FFlagPresetInGameGuiInset +coroutine.wrap(function() + local RobloxGui = game:GetService("CoreGui"):WaitForChild("RobloxGui") + local guiInsetChangedEvent = Instance.new("BindableEvent") + guiInsetChangedEvent.Name = "GuiInsetChanged" + guiInsetChangedEvent.Event:connect(function(x1, y1, x2, y2) + if currScreenGui and currScreenGui:FindFirstChild("BlackFrame") then + currScreenGui.BlackFrame.Position = UDim2.new(0, -x1, 0, -y1) + currScreenGui.BlackFrame.Size = UDim2.new(1, x1 + x2, 1, y1 + y2) + end + end) + guiInsetChangedEvent.Parent = RobloxGui +end)() + +local leaveGameButton, leaveGameTextLabel, errorImage = nil + +GuiService.ErrorMessageChanged:connect(function() + if GuiService:GetErrorMessage() ~= '' then + --TODO: Remove this reference to Utility + local utility = require(RobloxGui.Modules.Settings.Utility) + if isTenFootInterface then + currScreenGui.ErrorFrame.Size = UDim2.new(1, 0, 0, 144) + currScreenGui.ErrorFrame.Position = UDim2.new(0, 0, 0, 0) + currScreenGui.ErrorFrame.BackgroundColor3 = COLORS.BACKGROUND_COLOR + currScreenGui.ErrorFrame.BackgroundTransparency = 0.5 + currScreenGui.ErrorFrame.ErrorText.FontSize = Enum.FontSize.Size36 + currScreenGui.ErrorFrame.ErrorText.Position = UDim2.new(.3, 0, 0, 0) + currScreenGui.ErrorFrame.ErrorText.Size = UDim2.new(.4, 0, 0, 144) + if errorImage == nil then + errorImage = Instance.new("ImageLabel") + errorImage.Image = "rbxasset://textures/ui/ErrorIconSmall.png" + errorImage.Size = UDim2.new(0, 96, 0, 79) + errorImage.Position = UDim2.new(0.228125, 0, 0, 32) + errorImage.ZIndex = 9 + errorImage.BackgroundTransparency = 1 + errorImage.Parent = currScreenGui.ErrorFrame + end + elseif utility:IsSmallTouchScreen() then + currScreenGui.ErrorFrame.Size = UDim2.new(0.5, 0, 0, 40) + end + currScreenGui.ErrorFrame.ErrorText.Text = GuiService:GetErrorMessage() + currScreenGui.ErrorFrame.Visible = true + local blackFrame = currScreenGui:FindFirstChild('BlackFrame') + if blackFrame then + blackFrame.CloseButton.ImageTransparency = 0 + blackFrame.CloseButton.Active = true + end + else + currScreenGui.ErrorFrame.Visible = false + end +end) + +GuiService.UiMessageChanged:connect(function(type, newMessage) + if type == Enum.UiMessageType.UiMessageInfo then + local blackFrame = currScreenGui and currScreenGui:FindFirstChild('BlackFrame') + if blackFrame then + local infoFrame = blackFrame:FindFirstChild("InfoFrame") + if FFlagFixLoadingScreenJankiness then + if infoFrame then + local uiMessage = infoFrame.UiMessageFrame.UiMessage + uiMessage.Text = newMessage + if newMessage ~= '' and layoutIsReady then + uiMessage.TextTransparency = 0 + else + uiMessage.TextTransparency = 1 + end + end + else + if infoFrame then + infoFrame.UiMessageFrame.UiMessage.Text = newMessage + if newMessage ~= '' then + infoFrame.UiMessageFrame.Visible = true + else + infoFrame.UiMessageFrame.Visible = false + end + else + blackFrame.UiMessageFrame.UiMessage.Text = newMessage + if newMessage ~= '' then + blackFrame.UiMessageFrame.Visible = true + else + blackFrame.UiMessageFrame.Visible = false + end + end + end + end + end +end) + +if GuiService:GetErrorMessage() ~= '' then + currScreenGui.ErrorFrame.ErrorText.Text = GuiService:GetErrorMessage() + currScreenGui.ErrorFrame.Visible = true +end + + +function stopListeningToRenderingStep() + if renderSteppedConnection then + renderSteppedConnection:disconnect() + renderSteppedConnection = nil + end +end + +function disconnectAndCloseHealthStat() + if connectionHealthCon then + connectionHealthCon:disconnect() + connectionHealthCon = nil + GuiService:CloseStatsBasedOnInputString("ConnectionHealth") + end +end + +function fadeAndDestroyBlackFrame(blackFrame) + if destroyingBackground then return end + destroyingBackground = true + spawn(function() + local infoFrame = blackFrame:FindFirstChild("InfoFrame") + local graphicsFrame = blackFrame:FindFirstChild("GraphicsFrame") + + local function getDescendants(root, children) + children = children or {} + for i, v in pairs(root:GetChildren()) do + children[#children + 1] = v + getDescendants(v, children) + end + return children + end + local infoFrameDescendants = getDescendants(infoFrame) + local transparency = 0 + local rateChange = 1.8 + local lastUpdateTime = nil + + --Notify everything else to stop messing with transparency to avoid ugly fighting effects + backgroundFadeStarted = true + if tweenPlaceIcon then + tweenPlaceIcon:Cancel() + tweenPlaceIcon = nil + end + + while transparency < 1 do + RunService.RenderStepped:wait() + if not lastUpdateTime then + lastUpdateTime = tick() + else + local newTime = tick() + transparency = transparency + rateChange * (newTime - lastUpdateTime) + for i = 1, #infoFrameDescendants do + local child = infoFrameDescendants[i] + if child:IsA('TextLabel') then + child.TextTransparency = transparency + elseif child:IsA('ImageLabel') then + child.ImageTransparency = transparency + end + end + graphicsFrame.LoadingImage.ImageTransparency = transparency + blackFrame.BackgroundTransparency = transparency + + lastUpdateTime = newTime + end + end + if blackFrame ~= nil then + stopListeningToRenderingStep() + blackFrame:Destroy() + end + + loadingImageInputBeganConn:disconnect() + if connectionHealthShown then + if UserInputService.TouchEnabled == true and UserInputService.MouseEnabled == false then + connectionHealthCon = game:GetService("UserInputService").InputBegan:connect(function() + disconnectAndCloseHealthStat() + end) + else + GuiService:CloseStatsBasedOnInputString("ConnectionHealth") + end + else + GuiService:CloseStatsBasedOnInputString("ConnectionHealth") + end + end) +end + +function destroyLoadingElements(instant) + if not currScreenGui then return end + if destroyedLoadingGui then return end + destroyedLoadingGui = true + + local guiChildren = currScreenGui:GetChildren() + for i=1, #guiChildren do + -- need to keep this around in case we get a connection error later + if guiChildren[i].Name ~= "ErrorFrame" then + if guiChildren[i].Name == "BlackFrame" and not instant then + fadeAndDestroyBlackFrame(guiChildren[i]) + else + guiChildren[i]:Destroy() + end + end + end +end + +function handleFinishedReplicating() + hasReplicatedFirstElements = (#game:GetService("ReplicatedFirst"):GetChildren() > 0) + + if not hasReplicatedFirstElements then + if game:IsLoaded() then + handleRemoveDefaultLoadingGui() + else + local gameLoadedCon = nil + gameLoadedCon = game.Loaded:connect(function() + gameLoadedCon:disconnect() + gameLoadedCon = nil + handleRemoveDefaultLoadingGui() + end) + end + else + wait(5) -- make sure after 5 seconds we remove the default gui, even if the user doesn't + handleRemoveDefaultLoadingGui() + end +end + +function handleRemoveDefaultLoadingGui(instant) + if isTenFootInterface then + ContextActionService:UnbindCoreAction('CancelGameLoad') + end + destroyLoadingElements(instant) + game:GetService("ReplicatedFirst"):SetDefaultLoadingGuiRemoved() +end + +if debugMode then + warn("Not destroying loading screen because debugMode is true") + return +end +game:GetService("ReplicatedFirst").FinishedReplicating:connect(handleFinishedReplicating) +if game:GetService("ReplicatedFirst"):IsFinishedReplicating() then + handleFinishedReplicating() +end + +game:GetService("ReplicatedFirst").RemoveDefaultLoadingGuiSignal:connect(handleRemoveDefaultLoadingGui) +if game:GetService("ReplicatedFirst"):IsDefaultLoadingGuiRemoved() then + handleRemoveDefaultLoadingGui() +end + +local VREnabledConn +local function onVREnabled() + if VRService.VREnabled then + handleRemoveDefaultLoadingGui(true) + require(RobloxGui.Modules.LoadingScreen3D) + end +end + +VREnabledConn = VRService:GetPropertyChangedSignal("VREnabled"):connect(onVREnabled) +onVREnabled() \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/MergeSort.lua b/Client2018/content/scripts/CoreScripts/MergeSort.lua new file mode 100644 index 0000000..6a1737a --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/MergeSort.lua @@ -0,0 +1,33 @@ +-- >= left < mid, >= mid <= right +function bottomupmerge(comp, a, b, left, mid, right) + local i, j = left, mid + for k = left, right do + if i < mid and (j > right or not comp(a[j], a[i])) then + b[k] = a[i] + i = i + 1 + else + b[k] = a[j] + j = j + 1 + end + end +end + +function mergesort(arr, comp) + local work = {} + for i = 1, #arr do + work[i] = arr[i] + end + local width = 1 + while width < #arr do + for i = 1, #arr, 2*width do + bottomupmerge(comp, arr, work, i, math.min(i+width, #arr), math.min(i+2*width-1, #arr)) + end + local temp = work + work = arr + arr = temp + width = width * 2 + end + return arr +end + +return mergesort(...) diff --git a/Client2018/content/scripts/CoreScripts/Modules/ActionBindingsTab.lua b/Client2018/content/scripts/CoreScripts/Modules/ActionBindingsTab.lua new file mode 100644 index 0000000..1aeabae --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/ActionBindingsTab.lua @@ -0,0 +1,460 @@ +local CoreGui = game:GetService("CoreGui") +local ContextActionService = game:GetService("ContextActionService") +local TweenService = game:GetService("TweenService") + +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local Utility = require(RobloxGui.Modules.Settings.Utility) + +local INPUT_TYPE_ROW_HEIGHT = 30 +local ACTION_ROW_HEIGHT = 20 +local ROW_PADDING = 5 +local COLUMN_PADDING = 5 + +local CORE_SECURITY_COLUMN_COLOR = Color3.new(0.1, 0, 0) +local DEV_SECURITY_COLUMN_COLOR = Color3.new(0, 0, 0) + +local EXPAND_ROTATE_IMAGE_TWEEN_OUT = TweenInfo.new(0.150, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) +local EXPAND_ROTATE_IMAGE_TWEEN_IN = TweenInfo.new(0.150, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + +local ROW_PULSE = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, true, 0) +local CONTAINER_SCROLL = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + +local container = nil +local boundInputTypeRows = {} +local boundInputTypesByRows = {} +local boundInputTypeActionRows = {} +local boundActionInfoByRows = {} +local inputTypesByActionRows = {} +local inputTypesByHeaders = {} +local headersByInputTypes = {} +local inputTypesExpanded = {} + +local layoutOrderDirty = true + +local rowTypePrecedence = { + BoundInputType = 3, + TableHeader = 2, + BoundAction = 1 +} + +local function sortInputTypeRows(a, b) + local inputTypeA = boundInputTypesByRows[a] + local inputTypeB = boundInputTypesByRows[b] + return tostring(inputTypeA) < tostring(inputTypeB) +end + +local function sortActionRows(a, b) + local actionA = boundActionInfoByRows[a] + local actionB = boundActionInfoByRows[b] + if actionA and actionB then + local rowInputTypeA = inputTypesByActionRows[a] + local rowInputTypeB = inputTypesByActionRows[b] + if rowInputTypeA ~= rowInputTypeB then + return tostring(rowInputTypeA) < tostring(rowInputTypeB) + end + if actionA.isCore and not actionB.isCore then + return true + elseif not actionA.isCore and actionB.isCore then + return false + end + local stackOrderA = actionA.stackOrder + local stackOrderB = actionB.stackOrder + if stackOrderA and stackOrderB then + return stackOrderA > stackOrderB --descending sort + else + return true + end + else + return true + end + return true + end + +local function createEmptyRow(name, height) + local row = Utility:Create("Frame") { + Name = name, + BackgroundTransparency = 1, + ZIndex = 6, + Size = UDim2.new(1, 0, 0, height or 0) + } + local columnList = Utility:Create("UIListLayout") { + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, COLUMN_PADDING), + Parent = row + } + return row +end + +local function createButtonRow(name, height) + local row = Utility:Create("TextButton") { + Name = name, + BackgroundTransparency = 1, + ZIndex = 6, + Text = "", + Size = UDim2.new(1, 0, 0, height or 0), + } + local columnList = Utility:Create("UIListLayout") { + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, COLUMN_PADDING), + Parent = row + } + return row +end + +local function createEmptyColumn(row, columnName) + local column = Utility:Create("Frame") { + Name = columnName, + BackgroundColor3 = Color3.new(0, 0, 0), + BackgroundTransparency = 0.75, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 6, + ClipsDescendants = true, + Parent = row + } + + return column +end + +local function createImageColumn(row, columnName, image, aspectRatio, imageSize) + local column = createEmptyColumn(row, columnName) + + local aspectRatioConstraint = Utility:Create("UIAspectRatioConstraint") { + AspectRatio = aspectRatio or 1, + Parent = column + } + + local imageLabel = Utility:Create("ImageLabel") { + Name = "ColumnImage", + BackgroundTransparency = 1, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(imageSize or 1, 0, imageSize or 1, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + ZIndex = 6, + Image = image, + Parent = column + } + + return column +end + +local function createTextColumn(row, columnName, text) + local column = createEmptyColumn(row, columnName) + + local textLabel = Utility:Create("TextLabel") { + Name = "ColumnText", + BackgroundTransparency = 1, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, -10, 1, -10), + AnchorPoint = Vector2.new(0.5, 0.5), + ZIndex = 6, + Text = text, + TextSize = 18, + TextColor3 = Color3.new(1, 1, 1), + TextXAlignment = Enum.TextXAlignment.Left, + Font = Enum.Font.SourceSans, + Parent = column + } + + return column +end + +local function createActionColumns(row, backgroundColor) + local x = 0 + + local insetWidth = ACTION_ROW_HEIGHT + (COLUMN_PADDING * 2) + local insetCol = createEmptyColumn(row, "Inset") + insetCol.LayoutOrder = 0 + insetCol.BackgroundTransparency = 1 + insetCol.Size = UDim2.new(0, insetWidth, 1, 0) + x = x + insetWidth + COLUMN_PADDING + + local priorityWidth = 80 + local priorityCol = createTextColumn(row, "Priority", "Priority") + priorityCol.LayoutOrder = 1 + priorityCol.BackgroundColor3 = backgroundColor + priorityCol.Size = UDim2.new(0, priorityWidth, 1, 0) + x = x + priorityWidth + COLUMN_PADDING + + local securityWidth = 80 + local securityCol = createTextColumn(row, "Security", "Security") + securityCol.LayoutOrder = 2 + securityCol.BackgroundColor3 = backgroundColor + securityCol.Size = UDim2.new(0, securityWidth, 1, 0) + x = x + securityWidth + COLUMN_PADDING + + local nameCol = createTextColumn(row, "ActionName", "Action Name") + nameCol.LayoutOrder = 3 + nameCol.BackgroundColor3 = backgroundColor + nameCol.Size = UDim2.new(1/4, 0, 1, 0) + + local inputTypesCol = createTextColumn(row, "InputTypes", "Input Types") + inputTypesCol.LayoutOrder = 4 + inputTypesCol.BackgroundColor3 = backgroundColor + inputTypesCol.Size = UDim2.new(3/4, -x - COLUMN_PADDING, 1, 0) + + return insetCol, priorityCol, securityCol, nameCol, inputTypesCol +end + +local function updateContainerCanvas() + debug.profilebegin("updateContainerCanvas") + + if layoutOrderDirty then + layoutOrderDirty = false + local idx = 1 + + local inputTypeRowList = {} + + for inputType, inputTypeRow in pairs(boundInputTypeRows) do + table.insert(inputTypeRowList, inputTypeRow) + end + + table.sort(inputTypeRowList, sortInputTypeRows) + + for i, inputTypeRow in pairs(inputTypeRowList) do + local inputType = boundInputTypesByRows[inputTypeRow] + inputTypeRow.LayoutOrder = idx; idx = idx + 1 + local headerRow = headersByInputTypes[inputType] + if headerRow then + headerRow.LayoutOrder = idx; idx = idx + 1 + end + + local actionRows = boundInputTypeActionRows[inputType] + if actionRows then + local actionRowList = {} + for actionName, actionRow in pairs(actionRows) do + table.insert(actionRowList, actionRow) + end + + table.sort(actionRowList, sortActionRows) + + for i, actionRow in pairs(actionRowList) do + actionRow.LayoutOrder = idx; idx = idx + 1 + end + end + end + end + + debug.profileend() +end + +local function scrollContainerToRow(row) + local scrollOffset = row.AbsolutePosition.Y - container.AbsolutePosition.Y + local newCanvasPosition = container.CanvasPosition + Vector2.new(0, scrollOffset) + TweenService:Create(container, CONTAINER_SCROLL, { CanvasPosition = newCanvasPosition }):Play() +end + +local ActionBindingsTab = {} + +function ActionBindingsTab.initializeGui(tabFrame) + local scrollingFrame = Utility:Create("ScrollingFrame") { + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, -10, 1, -10), + AnchorPoint = Vector2.new(0.5, 0.5), + BorderSizePixel = 0, + ScrollBarThickness = 4, + BackgroundTransparency = 1, + ZIndex = 6, + Parent = tabFrame + } + container = scrollingFrame + + local listLayout = Utility:Create("UIListLayout") { + Padding = UDim.new(0, ROW_PADDING), + Parent = scrollingFrame + } + listLayout.SortOrder = Enum.SortOrder.LayoutOrder + listLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function() container.CanvasSize = UDim2.new(0, 0, 0, listLayout.AbsoluteContentSize.Y) end) + + ActionBindingsTab.updateGuis() + + ContextActionService.BoundActionAdded:connect(function(actionName, createTouchButton, actionInfo, isCore) + actionInfo.isCore = isCore + ActionBindingsTab.updateActionRows(actionName, actionInfo) + + layoutOrderDirty = true + updateContainerCanvas() + end) + ContextActionService.BoundActionRemoved:connect(function(actionName, actionInfo, isCore) + actionInfo.isCore = isCore + ActionBindingsTab.removeActionRows(actionName, actionInfo) + + layoutOrderDirty = true + updateContainerCanvas() + end) +end + +function ActionBindingsTab.updateBoundInputTypeRow(inputType) + local existingRow = boundInputTypeRows[inputType] + if not existingRow then + local row = createButtonRow("BoundInputType", INPUT_TYPE_ROW_HEIGHT) + + local expandImageCol = createImageColumn(row, "ExpandImage", "rbxasset://textures/ui/ExpandArrowSheet.png", 1, 0.35) + expandImageCol.ColumnImage.ImageRectSize = Vector2.new(21, 21) + expandImageCol.ColumnImage.ImageRectOffset = Vector2.new(0, 0) + + local inputTypeCol = createTextColumn(row, "InputType", tostring(inputType)) + inputTypeCol.Size = UDim2.new(1, -INPUT_TYPE_ROW_HEIGHT - COLUMN_PADDING, 1, 0) + inputTypeCol.ColumnText.Font = Enum.Font.SourceSansBold + + local tableHeaderRow = createEmptyRow("TableHeader", ACTION_ROW_HEIGHT) + tableHeaderRow.Visible = false + local _, priorityCol, securityCol, nameCol, inputTypesCol = createActionColumns(tableHeaderRow, DEV_SECURITY_COLUMN_COLOR) + priorityCol.ColumnText.Font = Enum.Font.SourceSansBold + securityCol.ColumnText.Font = Enum.Font.SourceSansBold + nameCol.ColumnText.Font = Enum.Font.SourceSansBold + inputTypesCol.ColumnText.Font = Enum.Font.SourceSansBold + + boundInputTypeRows[inputType] = row + boundInputTypesByRows[row] = inputType + inputTypesByHeaders[tableHeaderRow] = inputType + headersByInputTypes[inputType] = tableHeaderRow + + tableHeaderRow.Parent = container + row.Parent = container + + TweenService:Create(inputTypeCol, ROW_PULSE, { BackgroundColor3 = Color3.new(0.5, 0.5, 0.5) }):Play() + + inputTypesExpanded[inputType] = false + row.MouseButton1Click:connect(function() + inputTypesExpanded[inputType] = not inputTypesExpanded[inputType] + + local inputTypeActionRows = boundInputTypeActionRows[inputType] + if not inputTypesExpanded[inputType] then + expandImageCol.ColumnImage.ImageRectOffset = Vector2.new(0, 0) + tableHeaderRow.Visible = false + if inputTypeActionRows then + for _, actionRow in pairs(inputTypeActionRows) do + actionRow.Visible = false + end + end + else + expandImageCol.ColumnImage.ImageRectOffset = Vector2.new(21, 0) + tableHeaderRow.Visible = true + if inputTypeActionRows then + for _, actionRow in pairs(inputTypeActionRows) do + actionRow.Visible = true + end + end + end + + updateContainerCanvas() + if inputTypesExpanded[inputType] then + TweenService:Create(inputTypeCol, ROW_PULSE, { BackgroundColor3 = Color3.new(0.5, 0.5, 0.5) }):Play() + scrollContainerToRow(row) + end + end) + end +end + +function ActionBindingsTab.updateActionRowForInputType(actionName, actionInfo, inputType) + local inputTypeActionRows = boundInputTypeActionRows[inputType] + if not inputTypeActionRows then + inputTypeActionRows = {} + boundInputTypeActionRows[inputType] = inputTypeActionRows + end + + local existingRow = inputTypeActionRows[actionName] + if not existingRow then + local row = createEmptyRow("BoundAction", ACTION_ROW_HEIGHT) + row.Visible = inputTypesExpanded[inputType] + + local inputTypeNames = {} + for i, inputType in pairs(actionInfo.inputTypes) do + inputTypeNames[i] = tostring(inputType) + end + + local insetCol, priorityCol, securityCol, nameCol, inputTypesCol = createActionColumns(row, actionInfo.isCore and CORE_SECURITY_COLUMN_COLOR or DEV_SECURITY_COLUMN_COLOR) + priorityCol.ColumnText.Text = actionInfo.priorityLevel or "Default" + securityCol.ColumnText.Text = actionInfo.isCore and "Core" or "Developer" + nameCol.ColumnText.Text = actionName + inputTypesCol.ColumnText.Text = table.concat(inputTypeNames, ", ") + + if actionInfo.isCore then + priorityCol.ColumnText.Font = Enum.Font.SourceSansItalic + securityCol.ColumnText.Font = Enum.Font.SourceSansItalic + nameCol.ColumnText.Font = Enum.Font.SourceSansItalic + inputTypesCol.ColumnText.Font = Enum.Font.SourceSansItalic + end + + inputTypeActionRows[actionName] = row + inputTypesByActionRows[row] = inputType + boundActionInfoByRows[row] = actionInfo + row.Parent = container + + if row.Visible then + TweenService:Create(nameCol, ROW_PULSE, { BackgroundColor3 = Color3.new(0.5, 0.5, 0.5) }):Play() + else + local inputTypeRow = boundInputTypeRows[inputType] + if inputTypeRow then + TweenService:Create(inputTypeRow.InputType, ROW_PULSE, { BackgroundColor3 = Color3.new(0.5, 0.5, 0.5) }):Play() + end + end + + existingRow = row + end +end + +function ActionBindingsTab.updateActionRows(actionName, actionInfo) + for _, inputType in pairs(actionInfo.inputTypes) do + ActionBindingsTab.updateBoundInputTypeRow(inputType) + ActionBindingsTab.updateActionRowForInputType(actionName, actionInfo, inputType) + end +end + +function ActionBindingsTab.removeActionRows(actionName, actionInfo) + for _, inputType in pairs(actionInfo.inputTypes) do + local inputTypeActionRows = boundInputTypeActionRows[inputType] + if inputTypeActionRows then + local row = inputTypeActionRows[actionName] + row:Destroy() + inputTypeActionRows[actionName] = nil + + --The following code looks weird. It's because Lua has no way to determine + --if a table is explicitly empty in both the array and dictionary parts. + --This does it though. + local isEmpty = true + for _, __ in pairs(inputTypeActionRows) do + isEmpty = false + break + end + + if isEmpty then + local inputTypeRow = boundInputTypeRows[inputType] + if inputTypeRow then + inputTypeRow:Destroy() + boundInputTypeRows[inputType] = nil + end + local tableHeaderRow = headersByInputTypes[inputType] + if tableHeaderRow then + headersByInputTypes[tableHeaderRow] = nil + tableHeaderRow:Destroy() + headersByInputTypes[inputType] = nil + end + end + end + end + + updateContainerCanvas() + container.UIListLayout:ApplyLayout() +end + +function ActionBindingsTab.updateGuis() + local boundCoreActions = ContextActionService:GetAllBoundCoreActionInfo() + for actionName, actionInfo in pairs(boundCoreActions) do + actionInfo.isCore = true + ActionBindingsTab.updateActionRows(actionName, actionInfo) + end + local boundActions = ContextActionService:GetAllBoundActionInfo() + for actionName, actionInfo in pairs(boundActions) do + actionInfo.isCore = false + ActionBindingsTab.updateActionRows(actionName, actionInfo) + end + + layoutOrderDirty = true + updateContainerCanvas() +end + +return ActionBindingsTab \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/AvatarContextMenu/ContextMenuGui.lua b/Client2018/content/scripts/CoreScripts/Modules/AvatarContextMenu/ContextMenuGui.lua new file mode 100644 index 0000000..2b58c64 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/AvatarContextMenu/ContextMenuGui.lua @@ -0,0 +1,269 @@ +--[[ + // FileName: ContextMenuGui.lua + // Written by: TheGamer101 + // Description: Module for creating the context GUI. +]] + +-- CONSTANTS + +local BG_TRANSPARENCY = 1 +local BG_COLOR = Color3.fromRGB(31, 31, 31) + +local BOTTOM_SCREEN_PADDING_PERCENT = 0.02 + +local MAX_WIDTH = 250 +local MAX_HEIGHT = 300 +local MAX_WIDTH_PERCENT = 0.7 +local MAX_HEIGHT_PERCENT = 0.6 + +local PLAYER_ICON_SIZE_Y = 0.3 + +-- SERVICES +local CoreGuiService = game:GetService("CoreGui") +local GuiService = game:GetService("GuiService") + +--- VARIABLES +local RobloxGui = CoreGuiService:WaitForChild("RobloxGui") +local CoreGuiModules = RobloxGui:WaitForChild("Modules") +local SettingsModules = CoreGuiModules:WaitForChild("Settings") +local AvatarMenuModules = CoreGuiModules:WaitForChild("AvatarContextMenu") +local PlayerCarousel = nil +local PlayerChangedEvent = Instance.new("BindableEvent") + +--- Modules +local ContextMenuUtil = require(AvatarMenuModules:WaitForChild("ContextMenuUtil")) +local Utility = require(SettingsModules:WaitForChild("Utility")) + +local ContextMenuGui = {} +ContextMenuGui.__index = ContextMenuGui + +-- PRIVATE METHODS + +function ContextMenuGui:CreateContextMenuHolder(player) + local contextMenuHolder = Instance.new("Frame") + contextMenuHolder.Name = "AvatarContextMenu" + contextMenuHolder.Position = UDim2.new(0, 0, 0, 0) + contextMenuHolder.Size = UDim2.new(1, 0, 1, 0) + contextMenuHolder.BackgroundTransparency = 1 + contextMenuHolder.Parent = RobloxGui + return contextMenuHolder +end + +function ContextMenuGui:CreateLeaveMenuButton(frame) + local function closeMenu() + self.CloseMenuFunc() + end + local closeMenuButton = Instance.new("ImageButton") + closeMenuButton.Name = "CloseMenuButton" + closeMenuButton.BackgroundTransparency = 1 + closeMenuButton.AnchorPoint = Vector2.new(1, 0) + closeMenuButton.Position = UDim2.new(1, -10, 0, 10) + closeMenuButton.Size = UDim2.new(0.05, 0, 0.1, 0) + closeMenuButton.Image = "rbxasset://textures/loading/cancelButton.png" + closeMenuButton.Selectable = false + closeMenuButton.Activated:Connect(closeMenu) + + local aspectConstraint = Instance.new("UIAspectRatioConstraint") + aspectConstraint.AspectType = Enum.AspectType.FitWithinMaxSize + aspectConstraint.DominantAxis = Enum.DominantAxis.Height + aspectConstraint.AspectRatio = 1 + aspectConstraint.Parent = closeMenuButton + + closeMenuButton.Parent = frame + + return closeMenuButton +end + +-- PUBLIC METHODS + +local function listenToViewportChange(functionToFire) + if functionToFire == nil then return end + + local viewportChangedConnection = nil + + local function updateCamera() + local newCamera = workspace.CurrentCamera + if viewportChangedConnection then + viewportChangedConnection:Disconnect() + end + viewportChangedConnection = newCamera:GetPropertyChangedSignal('ViewportSize'):Connect(functionToFire) + functionToFire() + end + + workspace:GetPropertyChangedSignal('CurrentCamera'):Connect(updateCamera) + updateCamera() +end + +function ContextMenuGui:CreateMenuFrame() + local contextMenuHolder = self:CreateContextMenuHolder() + + local menu = Instance.new("ImageButton") + menu.Name = "Menu" + menu.Size = UDim2.new(0.95, 0, 0.9, 0) + menu.Position = UDim2.new(0.5, 0, 1 - BOTTOM_SCREEN_PADDING_PERCENT, 0) + menu.AnchorPoint = Vector2.new(0.5, 1) + menu.BackgroundTransparency = 1 + menu.Selectable = false + menu.Image = "rbxasset://textures/blackBkg_round.png" + menu.ScaleType = Enum.ScaleType.Slice + menu.SliceCenter = Rect.new(12,12,12,12) + menu.Visible = false + menu.Active = true + menu.ClipsDescendants = true + + GuiService:AddSelectionParent("AvatarContextMenuGroup", menu) + + local aspectConstraint = Instance.new("UIAspectRatioConstraint") + aspectConstraint.AspectType = Enum.AspectType.ScaleWithParentSize + aspectConstraint.DominantAxis = Enum.DominantAxis.Height + aspectConstraint.AspectRatio = 1.15 + aspectConstraint.Parent = menu + + local function updateAspectRatioForViewport() + local viewportSize = workspace.CurrentCamera.ViewportSize + if viewportSize.x < viewportSize.y then + aspectConstraint.DominantAxis = Enum.DominantAxis.Width + else + aspectConstraint.DominantAxis = Enum.DominantAxis.Height + end + end + listenToViewportChange(updateAspectRatioForViewport) + + local sizeConstraint = Instance.new("UISizeConstraint") + sizeConstraint.MaxSize = Vector2.new(300,300) + sizeConstraint.MinSize = Vector2.new(200,200) + sizeConstraint.Parent = menu + + local contentFrame = Instance.new("Frame") + contentFrame.Name = "Content" + contentFrame.Size = UDim2.new(1,0,1,0) + contentFrame.BackgroundTransparency = 1 + contentFrame.Parent = menu + + local contentListLayout = Instance.new("UIListLayout") + contentListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center + contentListLayout.VerticalAlignment = Enum.VerticalAlignment.Top + contentListLayout.SortOrder = Enum.SortOrder.LayoutOrder + contentListLayout.Parent = contentFrame + + local contextActionList = Instance.new("ScrollingFrame") + contextActionList.Name = "ContextActionList" + contextActionList.AnchorPoint = Vector2.new(0.5,1) + contextActionList.BackgroundColor3 = Color3.fromRGB(79,79,79) + contextActionList.BorderSizePixel = 0 + contextActionList.LayoutOrder = 2 + contextActionList.Size = UDim2.new(1,-12,0.54,0) + contextActionList.CanvasSize = UDim2.new(0,0,0,208) + contextActionList.ScrollBarThickness = 4 + contextActionList.Selectable = false + contextActionList.Parent = contentFrame + + local contextActionListUIListLayout = Instance.new("UIListLayout") + contextActionListUIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center + contextActionListUIListLayout.SortOrder = Enum.SortOrder.LayoutOrder + contextActionListUIListLayout.VerticalAlignment = Enum.VerticalAlignment.Top + + contextActionListUIListLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function() + contextActionList.CanvasSize = UDim2.new(0,0,0,contextActionListUIListLayout.AbsoluteContentSize.Y) + end) + + contextActionListUIListLayout.Parent = contextActionList + + local nameTag = Instance.new("TextButton") + nameTag.Name = "NameTag" + nameTag.AnchorPoint = Vector2.new(0.5,1) + nameTag.BackgroundColor3 = Color3.fromRGB(79,79,79) + nameTag.AutoButtonColor = false + nameTag.BorderSizePixel = 0 + nameTag.LayoutOrder = 1 + nameTag.Size = UDim2.new(1,-12,0.16,0) + nameTag.Font = Enum.Font.SourceSansBold + nameTag.Text = "" + nameTag.TextColor3 = Color3.fromRGB(255,255,255) + nameTag.TextSize = 24 + nameTag.TextXAlignment = Enum.TextXAlignment.Center + nameTag.TextYAlignment = Enum.TextYAlignment.Center + nameTag.Selectable = false + nameTag.Parent = contentFrame + + local underline = Instance.new("Frame") + underline.Name = "Underline" + underline.BackgroundColor3 = Color3.fromRGB(255,255,255) + underline.AnchorPoint = Vector2.new(0,1) + underline.BorderSizePixel = 0 + underline.Position = UDim2.new(0,0,1,0) + underline.Size = UDim2.new(1,0,0,2) + underline.Parent = nameTag + + self:CreateLeaveMenuButton(menu) + + menu.Parent = contextMenuHolder + self.ContextMenuFrame = menu + + return menu +end + +function ContextMenuGui:BuildPlayerCarousel(playersByProximity) + if not PlayerCarousel then + PlayerCarousel = require(AvatarMenuModules:WaitForChild("PlayerCarousel")) + PlayerCarousel.rbxGui.Parent = self.ContextMenuFrame.Content + end + + PlayerCarousel:ClearPlayerEntries() + + for i = 1, #playersByProximity do + PlayerCarousel:CreatePlayerEntry(playersByProximity[i][1], playersByProximity[i][2]) + end + + if #playersByProximity > 0 then + self.ContextMenuFrame.Content.NameTag.Text = playersByProximity[1][1].Name + end + + PlayerCarousel.PlayerChanged:Connect(function(player) + if player then + self.ContextMenuFrame.Content.NameTag.Text = player.Name + else + self.ContextMenuFrame.Content.NameTag.Text = "" + end + PlayerChangedEvent:Fire(player) + end) + +end + +function ContextMenuGui:GetBottomScreenPaddingConstant() + return BOTTOM_SCREEN_PADDING_PERCENT +end + +function ContextMenuGui:SetCloseMenuFunc(closeMenuFunc) + self.CloseMenuFunc = closeMenuFunc +end + +function ContextMenuGui:SwitchToPlayerEntry(player, dontTween) + if not PlayerCarousel then return end + PlayerCarousel:SwitchToPlayerEntry(player, dontTween) +end + +function ContextMenuGui:OffsetPlayerEntry(offset) + if not PlayerCarousel then return end + PlayerCarousel:OffsetPlayerEntry(offset) +end + +function ContextMenuGui:GetSelectedPlayer() + if not PlayerCarousel then return nil end + return PlayerCarousel:GetSelectedPlayer() +end + +function ContextMenuGui.new() + local obj = setmetatable({}, ContextMenuGui) + + obj.CloseMenuFunc = nil + + obj.ContextMenuFrame = nil + obj.LastSetPlayerIcon = nil + + obj.SelectedPlayerChanged = PlayerChangedEvent.Event + + return obj +end + +return ContextMenuGui.new() diff --git a/Client2018/content/scripts/CoreScripts/Modules/AvatarContextMenu/ContextMenuItems.lua b/Client2018/content/scripts/CoreScripts/Modules/AvatarContextMenu/ContextMenuItems.lua new file mode 100644 index 0000000..dfa0f3c --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/AvatarContextMenu/ContextMenuItems.lua @@ -0,0 +1,314 @@ +--[[ + // FileName: ContextMenuItems.lua + // Written by: TheGamer101 + // Description: Module for creating the context menu items for the menu and doing the actions when they are clicked. +]] + +-- CONSTANTS +local FRIEND_LAYOUT_ORDER = 1 +local CHAT_LAYOUT_ORDER = 3 +local WAVE_LAYOUT_ORDER = 4 +local CUSTOM_LAYOUT_ORDER = 20 + +local MENU_ITEM_SIZE_X = 0.96 +local MENU_ITEM_SIZE_Y = 0 +local MENU_ITEM_SIZE_Y_OFFSET = 52 + +local THUMBNAIL_URL = "https://www.roblox.com/Thumbs/Avatar.ashx?x=200&y=200&format=png&userId=" +local BUST_THUMBNAIL_URL = "https://www.roblox.com/bust-thumbnail/image?width=420&height=420&format=png&userId=" + +--- SERVICES +local PlayersService = game:GetService("Players") +local CoreGuiService = game:GetService("CoreGui") +local StarterGui = game:GetService("StarterGui") +local Chat = game:GetService("Chat") +local RunService = game:GetService("RunService") +local AnalyticsService = game:GetService("AnalyticsService") + +-- MODULES +local RobloxGui = CoreGuiService:WaitForChild("RobloxGui") +local CoreGuiModules = RobloxGui:WaitForChild("Modules") +local SettingsModules = CoreGuiModules:WaitForChild("Settings") +local AvatarMenuModules = CoreGuiModules:WaitForChild("AvatarContextMenu") +local SettingsPages = SettingsModules:WaitForChild("Pages") + +local ContextMenuUtil = require(AvatarMenuModules:WaitForChild("ContextMenuUtil")) + +local PromptCreator = require(CoreGuiModules:WaitForChild("PromptCreator")) +local PlayerDropDownModule = require(CoreGuiModules:WaitForChild("PlayerDropDown")) +local ReportAbuseMenu = require(SettingsPages:WaitForChild("ReportAbuseMenu")) + +-- VARIABLES + +local LocalPlayer = PlayersService.LocalPlayer +while not LocalPlayer do + PlayersService.PlayerAdded:wait() + LocalPlayer = PlayersService.LocalPlayer +end + +local EnabledContextMenuItems = { + [Enum.AvatarContextMenuOption.Chat] = true, + [Enum.AvatarContextMenuOption.Friend] = true, + [Enum.AvatarContextMenuOption.Emote] = true +} +local CustomContextMenuItems = {} + +local BlockingUtility = PlayerDropDownModule:CreateBlockingUtility() + +local ContextMenuItems = {} +ContextMenuItems.__index = ContextMenuItems + +-- PRIVATE METHODS +function ContextMenuItems:ClearMenuItems() + local children = self.MenuItemFrame:GetChildren() + for i = 1, #children do + if children[i]:IsA("GuiObject") then + children[i]:Destroy() + end + end +end + +function ContextMenuItems:AddCustomAvatarMenuItem(menuOption, bindableEvent) + CustomContextMenuItems[menuOption] = bindableEvent +end + +function ContextMenuItems:RemoveCustomAvatarMenuItem(menuOption) + CustomContextMenuItems[menuOption] = nil +end + +function ContextMenuItems:IsContextAvatarEnumItem(enumItem) + local enumItems = Enum.AvatarContextMenuOption:GetEnumItems() + for i = 1, #enumItems do + if enumItem == enumItems[i] then + return true + end + end + return false +end + +function ContextMenuItems:EnableDefaultMenuItem(menuOption) + EnabledContextMenuItems[menuOption] = true +end + +function ContextMenuItems:RemoveDefaultMenuItem(menuOption) + EnabledContextMenuItems[menuOption] = false +end + +function ContextMenuItems:RegisterCoreMethods() + local function addMenuItemFunc(args) --[[ menuOption, bindableEvent]] + if type(args) == "table" then + local name = "" + if args[1] and type(args[1]) == "string" then + name = args[1] + else + error("AddAvatarContextMenuOption first argument must be a table or Enum.AvatarContextMenuOption") + end + + if args[2] and typeof(args[2]) == "Instance" and args[2].ClassName == "BindableEvent" then + self:AddCustomAvatarMenuItem(name, args[2]) + else + error("AddAvatarContextMenuOption second table entry must be a BindableEvent") + end + + elseif typeof(args) == "EnumItem" then + if self:IsContextAvatarEnumItem(args) then + self:EnableDefaultMenuItem(args) + else + error("AddAvatarContextMenuOption given EnumItem is not valid") + end + else + error("AddAvatarContextMenuOption first argument must be a table or Enum.AvatarContextMenuOption") + end + end + StarterGui:RegisterSetCore("AddAvatarContextMenuOption", addMenuItemFunc) + local function removeMenuItemFunc(menuOption) + if type(menuOption) == "string" then + self:RemoveCustomAvatarMenuItem(menuOption) + elseif typeof(menuOption) == "EnumItem" then + if self:IsContextAvatarEnumItem(menuOption) then + self:RemoveDefaultMenuItem(menuOption) + else + error("RemoveAvatarContextMenuOption given EnumItem is not valid") + end + else + error("RemoveAvatarContextMenuOption first argument must be a string or Enum.AvatarContextMenuOption") + end + end + StarterGui:RegisterSetCore("RemoveAvatarContextMenuOption", removeMenuItemFunc) +end + +function ContextMenuItems:CreateCustomMenuItems() + for buttonText, bindableEvent in pairs(CustomContextMenuItems) do + AnalyticsService:TrackEvent("Game", "AvatarContextMenuCustomButton", "name: " .. tostring(buttonText)) + local function customButtonFunc() + bindableEvent:Fire(self.SelectedPlayer) + end + local customButton = ContextMenuUtil:MakeStyledButton("CustomButton", buttonText, UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET), customButtonFunc) + customButton.Name = "CustomButton" + customButton.LayoutOrder = CUSTOM_LAYOUT_ORDER + customButton.Parent = self.MenuItemFrame + end +end + +-- PUBLIC METHODS + +local addFriendString = "Add Friend" +local friendsString = "Friends" +local friendRequestPendingString = "Friend Request Pending" +local acceptFriendRequestString = "Accept Friend Request" + +local addFriendDisabledTransparency = 0.75 +local friendStatusChangedConn = nil +function ContextMenuItems:CreateFriendButton(status) + local friendLabel = self.MenuItemFrame:FindFirstChild("FriendStatus") + if friendLabel then + friendLabel:Destroy() + friendLabel = nil + end + if friendStatusChangedConn then + friendStatusChangedConn:disconnect() + end + local friendLabelText = nil + + local addFriendFunc = function() + if friendLabelText and friendLabel.Selectable then + friendLabel.Selectable = false + friendLabelText.TextTransparency = addFriendDisabledTransparency + friendLabelText.Text = friendRequestPendingString + AnalyticsService:ReportCounter("AvatarContextMenu-RequestFriendship") + AnalyticsService:TrackEvent("Game", "RequestFriendship", "AvatarContextMenu") + LocalPlayer:RequestFriendship(self.SelectedPlayer) + end + end + + friendLabel, friendLabelText = ContextMenuUtil:MakeStyledButton("FriendStatus", addFriendString, UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET), addFriendFunc) + + if status ~= Enum.FriendStatus.Friend then + friendLabel.Selectable = true + friendLabelText.TextTransparency = 0 + else + friendLabel.Selectable = false + friendLabelText.TextTransparency = addFriendDisabledTransparency + friendLabelText.Text = friendsString + end + + friendStatusChangedConn = LocalPlayer.FriendStatusChanged:connect(function(player, friendStatus) + if player == self.SelectedPlayer and friendLabelText then + if not friendLabel.Selectable then + if friendStatus == Enum.FriendStatus.Friend then + friendLabelText.Text = friendsString + end + else + if friendStatus == Enum.FriendStatus.FriendRequestReceived then + friendLabelText.Text = acceptFriendRequestString + end + end + end + end) + + friendLabel.LayoutOrder = FRIEND_LAYOUT_ORDER + friendLabel.Parent = self.MenuItemFrame +end + +function ContextMenuItems:UpdateFriendButton(status) + local friendLabel = self.MenuItemFrame:FindFirstChild("FriendStatus") + if friendLabel then + self:CreateFriendButton(status) + end +end + +function ContextMenuItems:CreateEmoteButton() + local function wave() + if self.CloseMenuFunc then self:CloseMenuFunc() end + + AnalyticsService:ReportCounter("AvatarContextMenu-Wave") + AnalyticsService:TrackEvent("Game", "AvatarContextMenuWave", "placeId: " .. tostring(game.PlaceId)) + + PlayersService:Chat("/e wave") + end + + local waveButton = self.MenuItemFrame:FindFirstChild("Wave") + if not waveButton then + waveButton = ContextMenuUtil:MakeStyledButton("Wave", "Wave", UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET), wave) + waveButton.LayoutOrder = WAVE_LAYOUT_ORDER + waveButton.Parent = self.MenuItemFrame + end +end + + +function ContextMenuItems:CreateChatButton() + local function chatFunc() + if self.CloseMenuFunc then self:CloseMenuFunc() end + + AnalyticsService:ReportCounter("AvatarContextMenu-Chat") + AnalyticsService:TrackEvent("Game", "AvatarContextMenuChat", "placeId: " .. tostring(game.PlaceId)) + + -- todo: need a proper api to set up text in the chat bar + local ChatBar = nil + pcall(function() ChatBar = LocalPlayer.PlayerGui.Chat.Frame.ChatBarParentFrame.Frame.BoxFrame.Frame.ChatBar end) + if ChatBar then + ChatBar.Text = "/w " .. self.SelectedPlayer.Name + end + + local ChatModule = require(RobloxGui.Modules.ChatSelector) + ChatModule:SetVisible(true) + ChatModule:FocusChatBar() + end + + local chatButton = self.MenuItemFrame:FindFirstChild("ChatStatus") + if not chatButton then + chatButton = ContextMenuUtil:MakeStyledButton("ChatStatus", "Chat", UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET), chatFunc) + chatButton.LayoutOrder = CHAT_LAYOUT_ORDER + end + + local success, canLocalUserChat = pcall(function() return Chat:CanUserChatAsync(LocalPlayer.UserId) end) + local canChat = success and (RunService:IsStudio() or canLocalUserChat) + + if canChat then + chatButton.Parent = self.MenuItemFrame + else + chatButton.Parent = nil + end +end + +function ContextMenuItems:BuildContextMenuItems(player) + if not player then return end + + local friendStatus = ContextMenuUtil:GetFriendStatus(player) + local isBlocked = BlockingUtility:IsPlayerBlockedByUserId(player.UserId) + local isMuted = BlockingUtility:IsPlayerMutedByUserId(player.UserId) + self:ClearMenuItems() + self:SetSelectedPlayer(player) + if EnabledContextMenuItems[Enum.AvatarContextMenuOption.Friend] then + self:CreateFriendButton(friendStatus) + end + if EnabledContextMenuItems[Enum.AvatarContextMenuOption.Chat] then + self:CreateChatButton() + end + if EnabledContextMenuItems[Enum.AvatarContextMenuOption.Emote] then + self:CreateEmoteButton() + end + + self:CreateCustomMenuItems() +end + +function ContextMenuItems:SetSelectedPlayer(selectedPlayer) + self.SelectedPlayer = selectedPlayer +end + +function ContextMenuItems:SetCloseMenuFunc(closeMenuFunc) + self.CloseMenuFunc = closeMenuFunc +end + +function ContextMenuItems.new(menuItemFrame) + local obj = setmetatable({}, ContextMenuItems) + + obj.MenuItemFrame = menuItemFrame + obj.SelectedPlayer = nil + + obj:RegisterCoreMethods() + + return obj +end + +return ContextMenuItems diff --git a/Client2018/content/scripts/CoreScripts/Modules/AvatarContextMenu/ContextMenuUtil.lua b/Client2018/content/scripts/CoreScripts/Modules/AvatarContextMenu/ContextMenuUtil.lua new file mode 100644 index 0000000..a3084a3 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/AvatarContextMenu/ContextMenuUtil.lua @@ -0,0 +1,278 @@ +--[[ + // FileName: ContextMenuUtil.lua + // Written by: TheGamer101 + // Description: Module for utility funcitons of the avatar context menu. +]] + +--[[ + // FileName: ContextMenuGui.lua + // Written by: TheGamer101 + // Description: Module for creating the context GUI. +]] + +--- CONSTANTS + +local STOP_MOVEMENT_ACTION_NAME = "AvatarContextMenuStopInput" +local MAX_THUMBNAIL_WAIT_TIME = 2 +local MAX_THUMBNAIL_RETRIES = 4 + +--- SERVICES +local CoreGuiService = game:GetService("CoreGui") +local PlayersService = game:GetService("Players") +local ContextActionService = game:GetService("ContextActionService") +local GuiService = game:GetService("GuiService") +local UserInputService = game:GetService("UserInputService") + +--- VARIABLES +local RobloxGui = CoreGuiService:WaitForChild("RobloxGui") + +local LocalPlayer = PlayersService.LocalPlayer +while not LocalPlayer do + PlayersService.PlayerAdded:wait() + LocalPlayer = PlayersService.LocalPlayer +end + +local ContextMenuUtil = {} +ContextMenuUtil.__index = ContextMenuUtil + +-- PUBLIC METHODS + +function ContextMenuUtil:GetHeadshotForPlayer(player) + if self.HeadShotUrlCache[player] ~= nil and self.HeadShotUrlCache[player] ~= "" then + return self.HeadShotUrlCache[player] + end + if self.HeadShotUrlCache[player] == nil then + -- Mark that we are getting a headshot for this player. + self.HeadShotUrlCache[player] = "" + end + + local startTime = tick() + local headshotUrl, isFinal = PlayersService:GetUserThumbnailAsync(player.UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size180x180) + + if not isFinal then + for i = 0, MAX_THUMBNAIL_RETRIES do + headshotUrl, isFinal = PlayersService:GetUserThumbnailAsync(player.UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size180x180) + if isFinal then + break + end + wait(i ^ 2) + end + end + self.HeadShotUrlCache[player] = headshotUrl + + return headshotUrl +end + +function ContextMenuUtil:HasOrGettingHeadShot(player) + return self.HeadShotUrlCache[player] ~= nil +end + +function ContextMenuUtil:FindPlayerFromPart(part) + if part and part.Parent then + local possibleCharacter = part + while possibleCharacter and not possibleCharacter:IsA("Model") do + possibleCharacter = possibleCharacter.Parent + end + if possibleCharacter then + return PlayersService:GetPlayerFromCharacter(possibleCharacter) + end + end + return nil +end + +function ContextMenuUtil:GetPlayerPosition(player) + if player.Character then + local hrp = player.Character:FindFirstChild("HumanoidRootPart") + if hrp then + return hrp.Position + end + end + return nil +end + +local playerMovementEnabled = true + +function ContextMenuUtil:DisablePlayerMovement() + if not playerMovementEnabled then return end + playerMovementEnabled = false + + local noOpFunc = function(actionName, actionState) + if actionState == Enum.UserInputState.End then + return Enum.ContextActionResult.Pass + end + return Enum.ContextActionResult.Sink + end + + ContextActionService:BindCoreAction(STOP_MOVEMENT_ACTION_NAME, noOpFunc, false, + Enum.PlayerActions.CharacterForward, + Enum.PlayerActions.CharacterBackward, + Enum.PlayerActions.CharacterLeft, + Enum.PlayerActions.CharacterRight, + Enum.PlayerActions.CharacterJump, + Enum.UserInputType.Gamepad1, Enum.UserInputType.Gamepad2, Enum.UserInputType.Gamepad3, Enum.UserInputType.Gamepad4 + ) +end + +function ContextMenuUtil:EnablePlayerMovement() + if playerMovementEnabled then return end + playerMovementEnabled = true + + ContextActionService:UnbindCoreAction(STOP_MOVEMENT_ACTION_NAME) +end + +function ContextMenuUtil:GetFriendStatus(player) + local success, result = pcall(function() + -- NOTE: Core script only + return LocalPlayer:GetFriendStatus(player) + end) + if success then + return result + else + return Enum.FriendStatus.NotFriend + end +end + +local SelectionOverrideObject = Instance.new("ImageLabel") +SelectionOverrideObject.Image = "" +SelectionOverrideObject.BackgroundTransparency = 1 + +local function MakeDefaultButton(name, size, clickFunc) + + local button = Instance.new("ImageButton") + button.Name = name .. "Button" + button.Image = "" + button.ScaleType = Enum.ScaleType.Slice + button.SliceCenter = Rect.new(8,6,46,44) + button.AutoButtonColor = false + button.BackgroundTransparency = 1 + button.Size = size + button.ZIndex = 2 + button.SelectionImageObject = SelectionOverrideObject + button.BorderSizePixel = 0 + + local underline = Instance.new("Frame") + underline.Name = "Underline" + underline.BackgroundColor3 = Color3.fromRGB(137,137,137) + underline.AnchorPoint = Vector2.new(0.5,1) + underline.BorderSizePixel = 0 + underline.Position = UDim2.new(0.5,0,1,0) + underline.Size = UDim2.new(0.95,0,0,1) + underline.Parent = button + + if clickFunc then + button.MouseButton1Click:Connect(function() + clickFunc(UserInputService:GetLastInputType()) + end) + end + + local function isPointerInput(inputObject) + return inputObject.UserInputType == Enum.UserInputType.MouseMovement or inputObject.UserInputType == Enum.UserInputType.Touch + end + + local function selectButton() + button.BackgroundTransparency = 0.5 + end + + local function deselectButton() + button.BackgroundTransparency = 1 + end + + button.InputBegan:Connect(function(inputObject) + if button.Selectable and isPointerInput(inputObject) then + selectButton() + inputObject:GetPropertyChangedSignal("UserInputState"):connect(function() + if inputObject.UserInputState == Enum.UserInputState.End then + deselectButton() + end + end) + end + end) + button.InputEnded:Connect(function(inputObject) + if button.Selectable and GuiService.SelectedCoreObject ~= button and isPointerInput(inputObject) then + deselectButton() + end + end) + + button.SelectionGained:Connect(function() + selectButton() + end) + button.SelectionLost:Connect(function() + deselectButton() + end) + + local guiServiceCon = GuiService.Changed:Connect(function(prop) + if prop ~= "SelectedCoreObject" then return end + + if GuiService.SelectedCoreObject == nil or GuiService.SelectedCoreObject ~= button then + deselectButton() + return + end + + if button.Selectable then + selectButton() + end + end) + + return button +end + +local function getViewportSize() + while not workspace.CurrentCamera do + workspace.Changed:wait() + end + + -- ViewportSize is initally set to 1, 1 in Camera.cpp constructor. + -- Also check against 0, 0 incase this is changed in the future. + while workspace.CurrentCamera.ViewportSize == Vector2.new(0,0) or + workspace.CurrentCamera.ViewportSize == Vector2.new(1,1) do + workspace.CurrentCamera.Changed:wait() + end + + return workspace.CurrentCamera.ViewportSize +end + +local function isSmallTouchScreen() + local viewportSize = getViewportSize() + return UserInputService.TouchEnabled and (viewportSize.Y < 500 or viewportSize.X < 700) +end + +function ContextMenuUtil:MakeStyledButton(name, text, size, clickFunc) + local button = MakeDefaultButton(name, size, clickFunc) + + local textLabel = Instance.new("TextLabel") + textLabel.Name = name .. "TextLabel" + textLabel.BackgroundTransparency = 1 + textLabel.BorderSizePixel = 0 + textLabel.Size = UDim2.new(1, 0, 1, -8) + textLabel.Position = UDim2.new(0,0,0,0) + textLabel.TextColor3 = Color3.fromRGB(255,255,255) + textLabel.TextYAlignment = Enum.TextYAlignment.Center + textLabel.Font = Enum.Font.SourceSansLight + textLabel.TextSize = 24 + textLabel.Text = text + textLabel.TextScaled = true + textLabel.TextWrapped = true + textLabel.ZIndex = 2 + textLabel.Parent = button + + local constraint = Instance.new("UITextSizeConstraint",textLabel) + + if isSmallTouchScreen() then + textLabel.TextSize = 18 + elseif GuiService:IsTenFootInterface() then + textLabel.TextSize = 36 + end + constraint.MaxTextSize = textLabel.TextSize + + return button, textLabel +end + +function ContextMenuUtil.new() + local obj = setmetatable({}, ContextMenuUtil) + + obj.HeadShotUrlCache = {} + + return obj +end + +return ContextMenuUtil.new() diff --git a/Client2018/content/scripts/CoreScripts/Modules/AvatarContextMenu/PlayerCarousel.lua b/Client2018/content/scripts/CoreScripts/Modules/AvatarContextMenu/PlayerCarousel.lua new file mode 100644 index 0000000..a4bbbe4 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/AvatarContextMenu/PlayerCarousel.lua @@ -0,0 +1,226 @@ +--[[ + // FileName: PlayerCarousel.lua + // Written by: darthskrill + // Description: Module for building the UI for the player selection carousel +]] + +local PlayerCarousel = {} +PlayerCarousel.__index = PlayerCarousel + +-- SERVICES +local GuiService = game:GetService("GuiService") +local CoreGuiService = game:GetService("CoreGui") +local TweenService = game:GetService("TweenService") +local UserInputService = game:GetService("UserInputService") + +-- CONSTANTS +local BACKGROUND_SELECTED_COLOR = Color3.fromRGB(0,162,255) +local BACKGROUND_DEFAULT_COLOR = Color3.fromRGB(0,0,0) +local PAGE_LAYOUT_TWEEN_TIME = 0.25 + +-- VARIABLES +local RobloxGui = CoreGuiService:WaitForChild("RobloxGui") +local CoreGuiModules = RobloxGui:WaitForChild("Modules") +local AvatarMenuModules = CoreGuiModules:WaitForChild("AvatarContextMenu") +local selectedPlayer = nil +local uiPageLayout = nil +local playerChangedEvent = nil +local buttonToPlayerMap = {} +local playerToButtonMap = {} + +local function CreateMenuCarousel() + local playerSelection = Instance.new("Frame") + playerSelection.Name = "PlayerCarousel" + playerSelection.AnchorPoint = Vector2.new(0.5, 0.5) + playerSelection.BackgroundTransparency = 1 + playerSelection.Position = UDim2.new(0.5,0,0.5,0) + playerSelection.Size = UDim2.new(1, 0, 0.28, 0) + playerSelection.ClipsDescendants = true + + local innerFrame = Instance.new("Frame") + innerFrame.Name = "InnerFrame" + innerFrame.AnchorPoint = Vector2.new(0.5, 0.5) + innerFrame.BackgroundTransparency = 1 + innerFrame.Position = UDim2.new(0.5, 0, 0.5, 0) + innerFrame.Size = UDim2.new(0.8, 0, 1, 0) + innerFrame.ClipsDescendants = true + innerFrame.Active = true + innerFrame.Parent = playerSelection + + selectedPlayer = Instance.new("Frame") + selectedPlayer.Name = "SelectedPlayer" + selectedPlayer.AnchorPoint = Vector2.new(0.5, 0.5) + selectedPlayer.BackgroundTransparency = 1 + selectedPlayer.Position = UDim2.new(0.5, 0, 0.5, 0) + selectedPlayer.Size = UDim2.new(0, 100, 1, -10) + selectedPlayer.Parent = innerFrame + + uiPageLayout = Instance.new("UIPageLayout") + uiPageLayout.EasingDirection = Enum.EasingDirection.Out + uiPageLayout.EasingStyle = Enum.EasingStyle.Quad + uiPageLayout.Padding = UDim.new(0, 5) + uiPageLayout.TweenTime = PAGE_LAYOUT_TWEEN_TIME + uiPageLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center + uiPageLayout.VerticalAlignment = Enum.VerticalAlignment.Center + uiPageLayout.TouchInputEnabled = false + uiPageLayout.SortOrder = Enum.SortOrder.LayoutOrder + + local aspectRatioConstraint = Instance.new("UIAspectRatioConstraint") + aspectRatioConstraint.DominantAxis = Enum.DominantAxis.Height + aspectRatioConstraint.Parent = selectedPlayer + + playerChangedEvent = Instance.new("BindableEvent") + playerChangedEvent.Name = "PlayerChanged" + + uiPageLayout:GetPropertyChangedSignal("CurrentPage"):Connect(function() + if uiPageLayout.CurrentPage then uiPageLayout.CurrentPage.BackgroundColor3 = BACKGROUND_DEFAULT_COLOR end + if GuiService.SelectedCoreObject and GuiService.SelectedCoreObject.Parent == uiPageLayout.Parent then + GuiService.SelectedCoreObject.BackgroundColor3 = BACKGROUND_SELECTED_COLOR + end + GuiService.SelectedCoreObject = uiPageLayout.CurrentPage + if uiPageLayout.CurrentPage then playerChangedEvent:Fire(buttonToPlayerMap[uiPageLayout.CurrentPage]) end + end) + uiPageLayout.Parent = selectedPlayer + + local nextButton = Instance.new("ImageButton") + nextButton.Name = "NextButton" + nextButton.Image = "rbxassetid://471630112" + nextButton.BackgroundTransparency = 1 + nextButton.AnchorPoint = Vector2.new(1,0.5) + nextButton.Position = UDim2.new(1,-5,0.5,0) + nextButton.Size = UDim2.new(0.3,0,0.3,0) + nextButton.Selectable = false + nextButton.Parent = playerSelection + + local aspectRatioConstraint = Instance.new("UIAspectRatioConstraint") + aspectRatioConstraint.DominantAxis = Enum.DominantAxis.Width + aspectRatioConstraint.Parent = nextButton + + local prevButton = nextButton:Clone() + prevButton.Name = "PrevButton" + prevButton.AnchorPoint = Vector2.new(0, 0.5) + prevButton.Position = UDim2.new(0, 5, 0.5, 0) + prevButton.Rotation = 180 + prevButton.Selectable = false + prevButton.Parent = playerSelection + + local function moveChangePage(goToNext) + if goToNext then + uiPageLayout:Next() + else + uiPageLayout:Previous() + end + end + + nextButton.MouseButton1Click:Connect(function() moveChangePage(true) end) + prevButton.MouseButton1Click:Connect(function() moveChangePage(false) end) + + local defaultChildrenSize = 3 -- aspectRatioConstraint, PageLayout, first button in pagelayout + + local function checkButtonVisibility() + local lastInputIsTouch = UserInputService:GetLastInputType() == Enum.UserInputType.Touch + prevButton.Visible = not lastInputIsTouch and (#selectedPlayer:GetChildren() > defaultChildrenSize) + nextButton.Visible = not lastInputIsTouch and (#selectedPlayer:GetChildren() > defaultChildrenSize) + end + checkButtonVisibility() + UserInputService.LastInputTypeChanged:Connect(checkButtonVisibility) + + return playerSelection +end + +function PlayerCarousel:ClearPlayerEntries() + for button, player in pairs(buttonToPlayerMap) do + button:Destroy() + end + + buttonToPlayerMap = {} + playerToButtonMap = {} +end + +function PlayerCarousel:CreatePlayerEntry(player, distanceToLocalPlayer) + local playerButton = playerToButtonMap[player] + if playerButton then + playerButton.LayoutOrder = distanceToLocalPlayer + return + end + + local button = Instance.new("ImageButton") + button.Name = player.Name + button.BorderSizePixel = 0 + button.LayoutOrder = distanceToLocalPlayer + button.BackgroundColor3 = Color3.fromRGB(0,0,0) + button.BackgroundTransparency = 0 + button.Size = UDim2.new(1, 0, 1, 0) + + button.SelectionLost:connect(function() button.BackgroundColor3 = BACKGROUND_DEFAULT_COLOR end) + button.SelectionGained:connect(function() button.BackgroundColor3 = BACKGROUND_SELECTED_COLOR end) + + local tweenStyle = TweenInfo.new(1, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, -1, true) + local buttonLoadingTween = TweenService:Create(button, tweenStyle, {BackgroundTransparency = 1, BackgroundColor3 = Color3.fromRGB(255,255,255)}) + buttonLoadingTween:Play() + + buttonToPlayerMap[button] = player + playerToButtonMap[player] = button + + button.MouseButton1Click:Connect(function() + uiPageLayout:JumpTo(button) + end) + + button.Parent = selectedPlayer + + spawn(function() + local ContextMenuUtil = require(AvatarMenuModules:WaitForChild("ContextMenuUtil")) + button.Image = ContextMenuUtil:GetHeadshotForPlayer(player) + buttonLoadingTween:Cancel() + buttonLoadingTween = nil + button.BackgroundTransparency = 0 + if button == GuiService.SelectedCoreObject then + button.BackgroundColor3 = BACKGROUND_SELECTED_COLOR + else + button.BackgroundColor3 = BACKGROUND_DEFAULT_COLOR + end + end) +end + +function PlayerCarousel:SwitchToPlayerEntry(player, dontTween) + if not player then return end + + local button = playerToButtonMap[player] + if not button then + self:CreatePlayerEntry(player, 0) + button = playerToButtonMap[player] + end + + if dontTween then + uiPageLayout.TweenTime = 0 + end + uiPageLayout:JumpTo(button) + spawn(function() + uiPageLayout.TweenTime = PAGE_LAYOUT_TWEEN_TIME + end) +end + +function PlayerCarousel:OffsetPlayerEntry(offset) + if offset == 0 then return end + + if offset > 0 then + uiPageLayout:Next() + else + uiPageLayout:Previous() + end +end + +function PlayerCarousel:GetSelectedPlayer() + return buttonToPlayerMap[uiPageLayout.CurrentPage] +end + +function PlayerCarousel.new() + local obj = setmetatable({}, PlayerCarousel) + + obj.rbxGui = CreateMenuCarousel() + obj.PlayerChanged = playerChangedEvent.Event + + return obj +end + +return PlayerCarousel.new() diff --git a/Client2018/content/scripts/CoreScripts/Modules/AvatarContextMenu/SelectedCharacterIndicator.lua b/Client2018/content/scripts/CoreScripts/Modules/AvatarContextMenu/SelectedCharacterIndicator.lua new file mode 100644 index 0000000..52c4682 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/AvatarContextMenu/SelectedCharacterIndicator.lua @@ -0,0 +1,102 @@ +--[[ + // FileName: SelectedCharacterIndicator.lua + // Written by: TheGamer101 + // Description: Module for rendering an effect for the selected character . +]] + +local SelectedCharacterIndicator = {} +SelectedCharacterIndicator.__index = SelectedCharacterIndicator + +local RunService = game:GetService("RunService") + +local RENDER_ARROW_CONTEXT_ACTION = "ContextActionMenuRenderArrow" + +local CurrentCamera = workspace.CurrentCamera + +function ApplyArrow(character) + local baseModel = Instance.new("Model") + baseModel.Name = "ContextMenuArrow" + baseModel.Parent = CurrentCamera + + local humanoid = character:FindFirstChildOfClass("Humanoid") + if humanoid == nil then + humanoid = character:WaitForChild("Humanoid", 15) + if humanoid == nil then + return + end + end + + local torso = character:WaitForChild("HumanoidRootPart") + + local arrowPart = game:GetService("InsertService"):LoadLocalAsset("rbxasset://models/AvatarContextMenu/AvatarContextArrow.rbxm") + arrowPart.Anchored = true + arrowPart.Transparency = 0 + arrowPart.CanCollide = false + arrowPart.Parent = baseModel + + local arrowTween = game:GetService("TweenService"):Create(arrowPart, + TweenInfo.new(4, Enum.EasingStyle.Linear, Enum.EasingDirection.Out, -1, false), + {Orientation=Vector3.new(0,360,180)}) + arrowTween:Play() + + local function update() + arrowPart.Position = torso.Position + Vector3.new(0, 5, 0) + end + + local isKilled = false + local function kill() + if isKilled then + return + end + isKilled = true + baseModel:Destroy() + arrowTween:Destroy() + RunService:UnbindFromRenderStep(RENDER_ARROW_CONTEXT_ACTION) + end + + humanoid.Died:Connect(kill) + character.AncestryChanged:Connect(kill) + RunService:BindToRenderStep(RENDER_ARROW_CONTEXT_ACTION, Enum.RenderPriority.Camera.Value + 1 , update) + baseModel.Parent = CurrentCamera + + return kill +end + +function SelectedCharacterIndicator:ChangeSelectedPlayer(selectedPlayer) + coroutine.wrap(function() + if self.SelectedPlayer then + self.SelectedPlayer = nil + self.CharacterAddedConn:Disconnect() + self.CharacterAddedConn = nil + if self.KillOldRenderFunction then + self.KillOldRenderFunction() + self.KillOldRenderFunction = nil + end + end + + if selectedPlayer then + self.SelectedPlayer = selectedPlayer + self.CharacterAddedConn = selectedPlayer.CharacterAdded:Connect(function(character) + if self.KillOldRenderFunction then + self.KillOldRenderFunction() + end + self.KillOldRenderFunction = ApplyArrow(character) + end) + if selectedPlayer.Character then + self.KillOldRenderFunction = ApplyArrow(selectedPlayer.Character) + end + end + end)() +end + +function SelectedCharacterIndicator.new() + local obj = setmetatable({}, SelectedCharacterIndicator) + + obj.KillOldRenderFunction = nil + obj.SelectedPlayer = nil + obj.CharacterAddedConn = nil + + return obj +end + +return SelectedCharacterIndicator.new() diff --git a/Client2018/content/scripts/CoreScripts/Modules/BackpackScript.lua b/Client2018/content/scripts/CoreScripts/Modules/BackpackScript.lua new file mode 100644 index 0000000..9df2afb --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/BackpackScript.lua @@ -0,0 +1,2001 @@ +-- Backpack Version 5.1 +-- OnlyTwentyCharacters, SolarCrane + +local BackpackScript = {} +BackpackScript.OpenClose = nil -- Function to toggle open/close +BackpackScript.IsOpen = false +BackpackScript.StateChanged = Instance.new('BindableEvent') -- Fires after any open/close, passes IsNowOpen + +BackpackScript.ModuleName = "Backpack" +BackpackScript.KeepVRTopbarOpen = true +BackpackScript.VRIsExclusive = true +BackpackScript.VRClosesNonExclusive = true + +local ICON_SIZE = 60 +local FONT_SIZE = Enum.FontSize.Size14 +local ICON_BUFFER = 5 + +local BACKGROUND_FADE = 0.50 +local BACKGROUND_COLOR = Color3.new(31/255, 31/255, 31/255) + +local VR_FADE_TIME = 1 +local VR_PANEL_RESOLUTION = 100 + +local SLOT_DRAGGABLE_COLOR = Color3.new(49/255, 49/255, 49/255) +local SLOT_EQUIP_COLOR = Color3.new(90/255, 142/255, 233/255) +local SLOT_EQUIP_THICKNESS = 0.1 -- Relative +local SLOT_FADE_LOCKED = 0.50 -- Locked means undraggable +local SLOT_BORDER_COLOR = Color3.new(1, 1, 1) -- Appears when dragging + +local TOOLTIP_BUFFER = 6 +local TOOLTIP_HEIGHT = 16 +local TOOLTIP_OFFSET = -25 -- From top + +local ARROW_IMAGE_OPEN = 'rbxasset://textures/ui/Backpack_Open.png' +local ARROW_IMAGE_CLOSE = 'rbxasset://textures/ui/Backpack_Close.png' +local ARROW_SIZE = UDim2.new(0, 14, 0, 9) +local ARROW_HOTKEY = Enum.KeyCode.Backquote.Value --TODO: Hookup '~' too? +local ARROW_HOTKEY_STRING = '`' +local ARROW_HOVER_COLOR = Color3.new(0,162/255,1) + +local HOTBAR_SLOTS_FULL = 10 +local HOTBAR_SLOTS_VR = 6 +local HOTBAR_SLOTS_MINI = 3 +local HOTBAR_SLOTS_WIDTH_CUTOFF = 1024 -- Anything smaller is MINI +local HOTBAR_OFFSET_FROMBOTTOM = -30 -- Offset to make room for the Health GUI + +local INVENTORY_ROWS_FULL = 4 +local INVENTORY_ROWS_VR = 3 +local INVENTORY_ROWS_MINI = 2 +local INVENTORY_HEADER_SIZE = 40 +local INVENTORY_ARROWS_BUFFER_VR = 40 + +local SEARCH_BUFFER = 5 +local SEARCH_WIDTH = 200 +local SEARCH_TEXT = " Search" + +local SEARCH_TEXT_OFFSET_FROMLEFT = 0 +local SEARCH_BACKGROUND_COLOR = Color3.new(0.37, 0.37, 0.37) +local SEARCH_BACKGROUND_FADE = 0.15 + +local DOUBLE_CLICK_TIME = 0.5 + +local ZERO_KEY_VALUE = Enum.KeyCode.Zero.Value +local DROP_HOTKEY_VALUE = Enum.KeyCode.Backspace.Value + +local GAMEPAD_INPUT_TYPES = +{ + [Enum.UserInputType.Gamepad1] = true; + [Enum.UserInputType.Gamepad2] = true; + [Enum.UserInputType.Gamepad3] = true; + [Enum.UserInputType.Gamepad4] = true; + [Enum.UserInputType.Gamepad5] = true; + [Enum.UserInputType.Gamepad6] = true; + [Enum.UserInputType.Gamepad7] = true; + [Enum.UserInputType.Gamepad8] = true; +} + +local PlayersService = game:GetService('Players') +local UserInputService = game:GetService('UserInputService') +local StarterGui = game:GetService('StarterGui') +local GuiService = game:GetService('GuiService') +local CoreGui = game:GetService('CoreGui') +local ContextActionService = game:GetService('ContextActionService') +local VRService = game:GetService("VRService") +local RobloxGui = CoreGui:WaitForChild('RobloxGui') +RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface") +local IsTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled() +local Utility = require(RobloxGui.Modules.Settings.Utility) +local GameTranslator = require(RobloxGui.Modules.GameTranslator) + +local FFlagBackpackScriptUseFormatByKey = settings():GetFFlag('BackpackScriptUseFormatByKey') + +if FFlagBackpackScriptUseFormatByKey then + SEARCH_TEXT_OFFSET_FROMLEFT = 3 + local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator) + SEARCH_TEXT = RobloxTranslator:FormatByKey("BACKPACK_SEARCH") +else + pcall(function() + local LocalizationService = game:GetService("LocalizationService") + local CorescriptLocalization = LocalizationService:GetCorescriptLocalizations()[1] + SEARCH_TEXT = CorescriptLocalization:GetString( + LocalizationService.RobloxLocaleId, + "BACKPACK_SEARCH" + ) + end) +end + +local TopbarEnabled = true + +if IsTenFootInterface then + ICON_SIZE = 100 + FONT_SIZE = Enum.FontSize.Size24 +end + +local GamepadActionsBound = false + +local IS_PHONE = UserInputService.TouchEnabled and GuiService:GetScreenResolution().X < HOTBAR_SLOTS_WIDTH_CUTOFF + +local Player = PlayersService.LocalPlayer + +local MainFrame = nil +local HotbarFrame = nil +local OpenInventoryButton = nil +local CloseInventoryButton = nil +local InventoryFrame = nil +local VRInventorySelector = nil +local ScrollingFrame = nil +local UIGridFrame = nil +local UIGridLayout = nil +local ScrollUpInventoryButton = nil +local ScrollDownInventoryButton = nil + +local Character = nil +local Humanoid = nil +local Backpack = nil + +local Slots = {} -- List of all Slots by index +local LowestEmptySlot = nil +local SlotsByTool = {} -- Map of Tools to their assigned Slots +local HotkeyFns = {} -- Map of KeyCode values to their assigned behaviors +local Dragging = {} -- Only used to check if anything is being dragged, to disable other input +local FullHotbarSlots = 0 -- Now being used to also determine whether or not LB and RB on the gamepad are enabled. +local ActiveHopper = nil -- NOTE: HopperBin +local StarterToolFound = false -- Special handling is required for the gear currently equipped on the site +local WholeThingEnabled = false +local TextBoxFocused = false -- ANY TextBox, not just the search box +local ViewingSearchResults = false -- If the results of a search are currently being viewed +local HotkeyStrings = {} -- Used for eating/releasing hotkeys +local CharConns = {} -- Holds character connections to be cleared later +local GamepadEnabled = false -- determines if our gui needs to be gamepad friendly +local TimeOfLastToolChange = 0 + +local IsVR = VRService.VREnabled -- Are we currently using a VR device? +local NumberOfHotbarSlots = IsVR and HOTBAR_SLOTS_VR or (IS_PHONE and HOTBAR_SLOTS_MINI or HOTBAR_SLOTS_FULL) -- Number of slots shown at the bottom +local NumberOfInventoryRows = IsVR and INVENTORY_ROWS_VR or (IS_PHONE and INVENTORY_ROWS_MINI or INVENTORY_ROWS_FULL) -- How many rows in the popped-up inventory +local BackpackPanel = nil + +local lastEquippedSlot = nil + +local function EvaluateBackpackPanelVisibility(enabled) + return enabled and StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Backpack) and TopbarEnabled and VRService.VREnabled +end + +local function ShowVRBackpackPopup() + if BackpackPanel and EvaluateBackpackPanelVisibility(true) then + BackpackPanel:ForceShowForSeconds(2) + end +end + +local function NewGui(className, objectName) + local newGui = Instance.new(className) + newGui.Name = objectName + newGui.BackgroundColor3 = Color3.new(0, 0, 0) + newGui.BackgroundTransparency = 1 + newGui.BorderColor3 = Color3.new(0, 0, 0) + newGui.BorderSizePixel = 0 + newGui.Size = UDim2.new(1, 0, 1, 0) + if className:match('Text') then + newGui.TextColor3 = Color3.new(1, 1, 1) + newGui.Text = '' + newGui.Font = Enum.Font.SourceSans + newGui.FontSize = FONT_SIZE + newGui.TextWrapped = true + if className == 'TextButton' then + newGui.Font = Enum.Font.SourceSansBold + newGui.BorderSizePixel = 1 + end + end + return newGui +end + +local function FindLowestEmpty() + for i = 1, NumberOfHotbarSlots do + local slot = Slots[i] + if not slot.Tool then + return slot + end + end + return nil +end + +local function isInventoryEmpty() + for i = NumberOfHotbarSlots + 1, #Slots do + local slot = Slots[i] + if slot and slot.Tool then + return false + end + end + return true +end + +local function UseGazeSelection() + return UserInputService.VREnabled +end + +local function AdjustHotbarFrames() + local inventoryOpen = InventoryFrame.Visible -- (Show all) + local visualTotal = (inventoryOpen) and NumberOfHotbarSlots or FullHotbarSlots + local visualIndex = 0 + local hotbarIsVisible = (visualTotal >= 1) + + for i = 1, NumberOfHotbarSlots do + local slot = Slots[i] + if slot.Tool or inventoryOpen then + visualIndex = visualIndex + 1 + slot:Readjust(visualIndex, visualTotal) + slot.Frame.Visible = true + else + slot.Frame.Visible = false + end + end + + OpenInventoryButton.Visible = not inventoryOpen and (hotbarIsVisible or not isInventoryEmpty()) + OpenInventoryButton.Position = UDim2.new(0.5, -15, 1, hotbarIsVisible and -110 or -50) +end + +local function UpdateScrollingFrameCanvasSize() + local countX = math.floor(ScrollingFrame.AbsoluteSize.X/(ICON_SIZE + ICON_BUFFER)) + local maxRow = math.ceil((#UIGridFrame:GetChildren() - 1)/countX) + local canvasSizeY = maxRow*(ICON_SIZE + ICON_BUFFER) + ICON_BUFFER + ScrollingFrame.CanvasSize = UDim2.new(0, 0, 0, canvasSizeY) +end + +local function AdjustInventoryFrames() + for i = NumberOfHotbarSlots + 1, #Slots do + local slot = Slots[i] + slot.Frame.LayoutOrder = slot.Index + slot.Frame.Visible = (slot.Tool ~= nil) + end + UpdateScrollingFrameCanvasSize() +end + +local function UpdateBackpackLayout() + HotbarFrame.Size = UDim2.new(0, ICON_BUFFER + (NumberOfHotbarSlots * (ICON_SIZE + ICON_BUFFER)), 0, ICON_BUFFER + ICON_SIZE + ICON_BUFFER) + HotbarFrame.Position = UDim2.new(0.5, -HotbarFrame.Size.X.Offset / 2, 1, -HotbarFrame.Size.Y.Offset) + InventoryFrame.Size = UDim2.new(0, HotbarFrame.Size.X.Offset, 0, (HotbarFrame.Size.Y.Offset * NumberOfInventoryRows) + INVENTORY_HEADER_SIZE + (IsVR and 2*INVENTORY_ARROWS_BUFFER_VR or 0)) + InventoryFrame.Position = UDim2.new(0.5, -InventoryFrame.Size.X.Offset / 2, 1, HotbarFrame.Position.Y.Offset - InventoryFrame.Size.Y.Offset) + + ScrollingFrame.Size = UDim2.new(1, ScrollingFrame.ScrollBarThickness + 1, 1, -INVENTORY_HEADER_SIZE - (IsVR and 2*INVENTORY_ARROWS_BUFFER_VR or 0)) + ScrollingFrame.Position = UDim2.new(0, 0, 0, INVENTORY_HEADER_SIZE + (IsVR and INVENTORY_ARROWS_BUFFER_VR or 0)) + AdjustHotbarFrames() + AdjustInventoryFrames() +end + +local function Clamp(low, high, num) + return math.min(high, math.max(low, num)) +end + +local function CheckBounds(guiObject, x, y) + local pos = guiObject.AbsolutePosition + local size = guiObject.AbsoluteSize + return (x > pos.X and x <= pos.X + size.X and y > pos.Y and y <= pos.Y + size.Y) +end + +local function GetOffset(guiObject, point) + local centerPoint = guiObject.AbsolutePosition + (guiObject.AbsoluteSize / 2) + return (centerPoint - point).magnitude +end + +local function DisableActiveHopper() --NOTE: HopperBin + ActiveHopper:ToggleSelect() + SlotsByTool[ActiveHopper]:UpdateEquipView() + ActiveHopper = nil +end + +local function UnequipAllTools() --NOTE: HopperBin + if Humanoid then + Humanoid:UnequipTools() + if ActiveHopper then + DisableActiveHopper() + end + end +end + +local function EquipNewTool(tool) --NOTE: HopperBin + UnequipAllTools() + if tool:IsA('HopperBin') then + tool:ToggleSelect() + SlotsByTool[tool]:UpdateEquipView() + ActiveHopper = tool + else + --Humanoid:EquipTool(tool) --NOTE: This would also unequip current Tool + tool.Parent = Character --TODO: Switch back to above line after EquipTool is fixed! + end +end + +local function IsEquipped(tool) + return tool and ((tool:IsA('HopperBin') and tool.Active) or tool.Parent == Character) --NOTE: HopperBin +end + +local function MakeSlot(parent, index) + index = index or (#Slots + 1) + + -- Slot Definition -- + + local slot = {} + slot.Tool = nil + slot.Index = index + slot.Frame = nil + + local LocalizedName = nil + local LocalizedToolTip = nil + + local SlotFrameParent = nil + local SlotFrame = nil + local FakeSlotFrame = nil + local ToolIcon = nil + local ToolName = nil + local ToolChangeConn = nil + local HighlightFrame = nil + local SelectionObj = nil + + --NOTE: The following are only defined for Hotbar Slots + local ToolTip = nil + local SlotNumber = nil + + -- Slot Functions -- + + local function UpdateSlotFading() + if VRService.VREnabled and BackpackPanel then + local panelTransparency = BackpackPanel.transparency + local slotTransparency = SLOT_FADE_LOCKED + + -- This equation multiplies the two transparencies together. + local finalTransparency = panelTransparency + slotTransparency - panelTransparency * slotTransparency + + SlotFrame.BackgroundTransparency = finalTransparency + SlotFrame.TextTransparency = finalTransparency + if ToolIcon then + ToolIcon.ImageTransparency = InventoryFrame.Visible and 0 or panelTransparency + end + if HighlightFrame then + for _, child in pairs(HighlightFrame:GetChildren()) do + child.BackgroundTransparency = finalTransparency + end + end + + SlotFrame.SelectionImageObject = SelectionObj + else + SlotFrame.SelectionImageObject = nil + SlotFrame.BackgroundTransparency = (SlotFrame.Draggable) and 0 or SLOT_FADE_LOCKED + end + SlotFrame.BackgroundColor3 = (SlotFrame.Draggable) and SLOT_DRAGGABLE_COLOR or BACKGROUND_COLOR + end + + function slot:Readjust(visualIndex, visualTotal) --NOTE: Only used for Hotbar slots + local centered = HotbarFrame.Size.X.Offset / 2 + local sizePlus = ICON_BUFFER + ICON_SIZE + local midpointish = (visualTotal / 2) + 0.5 + local factor = visualIndex - midpointish + SlotFrame.Position = UDim2.new(0, centered - (ICON_SIZE / 2) + (sizePlus * factor), 0, ICON_BUFFER) + end + + function slot:Fill(tool) + if not tool then + return self:Clear() + end + + self.Tool = tool + + local function assignToolData() + LocalizedName = GameTranslator:TranslateGameText(tool, tool.Name) + LocalizedToolTip = nil + + local icon = tool.TextureId + ToolIcon.Image = icon + ToolName.Text = (icon == '') and LocalizedName or '' -- (Only show name if no icon) + if ToolTip and tool:IsA('Tool') then --NOTE: HopperBin + LocalizedToolTip = GameTranslator:TranslateGameText(tool, tool.ToolTip) + ToolTip.Text = LocalizedToolTip + local width = ToolTip.TextBounds.X + TOOLTIP_BUFFER + ToolTip.Size = UDim2.new(0, width, 0, TOOLTIP_HEIGHT) + ToolTip.Position = UDim2.new(0.5, -width / 2, 0, TOOLTIP_OFFSET) + end + end + assignToolData() + + if ToolChangeConn then + ToolChangeConn:disconnect() + ToolChangeConn = nil + end + + ToolChangeConn = tool.Changed:connect(function(property) + if property == 'TextureId' or property == 'Name' or property == 'ToolTip' then + assignToolData() + end + end) + + local hotbarSlot = (self.Index <= NumberOfHotbarSlots) + local inventoryOpen = InventoryFrame.Visible + + if (not hotbarSlot or inventoryOpen) and not UserInputService.VREnabled then + SlotFrame.Draggable = true + end + + self:UpdateEquipView() + + if hotbarSlot then + FullHotbarSlots = FullHotbarSlots + 1 + -- If using a controller, determine whether or not we can enable BindCoreAction("RBXHotbarEquip", etc) + if WholeThingEnabled then + if FullHotbarSlots >= 1 and not GamepadActionsBound then + -- Player added first item to a hotbar slot, enable BindCoreAction + GamepadActionsBound = true + ContextActionService:BindCoreAction("RBXHotbarEquip", changeToolFunc, false, Enum.KeyCode.ButtonL1, Enum.KeyCode.ButtonR1) + end + end + end + + SlotsByTool[tool] = self + LowestEmptySlot = FindLowestEmpty() + end + + function slot:Clear() + if not self.Tool then return end + + if ToolChangeConn then + ToolChangeConn:disconnect() + ToolChangeConn = nil + end + + ToolIcon.Image = '' + ToolName.Text = '' + if ToolTip then + ToolTip.Text = '' + ToolTip.Visible = false + end + SlotFrame.Draggable = false + + self:UpdateEquipView(true) -- Show as unequipped + + if self.Index <= NumberOfHotbarSlots then + FullHotbarSlots = FullHotbarSlots - 1 + if FullHotbarSlots < 1 then + -- Player removed last item from hotbar; UnbindCoreAction("RBXHotbarEquip"), allowing the developer to use LB and RB. + GamepadActionsBound = false + ContextActionService:UnbindCoreAction("RBXHotbarEquip") + end + end + + SlotsByTool[self.Tool] = nil + self.Tool = nil + LowestEmptySlot = FindLowestEmpty() + end + + function slot:UpdateEquipView(unequippedOverride) + if not unequippedOverride and IsEquipped(self.Tool) then -- Equipped + lastEquippedSlot = slot + if not HighlightFrame then + HighlightFrame = NewGui('Frame', 'Equipped') + HighlightFrame.ZIndex = SlotFrame.ZIndex + local t = SLOT_EQUIP_THICKNESS + local dataTable = { -- Relative sizes and positions + {t, 1, 0, 0}, + {1 - 2*t, t, t, 0}, + {t, 1, 1 - t, 0}, + {1 - 2*t, t, t, 1 - t}, + } + for _, data in pairs(dataTable) do + local edgeFrame = NewGui('Frame', 'Edge') + edgeFrame.BackgroundTransparency = 0 + edgeFrame.BackgroundColor3 = SLOT_EQUIP_COLOR + edgeFrame.Size = UDim2.new(data[1], 0, data[2], 0) + edgeFrame.Position = UDim2.new(data[3], 0, data[4], 0) + edgeFrame.ZIndex = HighlightFrame.ZIndex + edgeFrame.Parent = HighlightFrame + end + end + HighlightFrame.Parent = SlotFrame + else -- In the Backpack + if HighlightFrame then + HighlightFrame.Parent = nil + end + end + UpdateSlotFading() + end + + function slot:IsEquipped() + return IsEquipped(self.Tool) + end + + function slot:Delete() + SlotFrame:Destroy() --NOTE: Also clears connections + table.remove(Slots, self.Index) + local newSize = #Slots + + -- Now adjust the rest (both visually and representationally) + for i = self.Index, newSize do + Slots[i]:SlideBack() + end + + UpdateScrollingFrameCanvasSize() + end + + function slot:Swap(targetSlot) --NOTE: This slot (self) must not be empty! + local myTool, otherTool = self.Tool, targetSlot.Tool + self:Clear() + if otherTool then -- (Target slot might be empty) + targetSlot:Clear() + self:Fill(otherTool) + end + if myTool then + targetSlot:Fill(myTool) + else + targetSlot:Clear() + end + end + + function slot:SlideBack() -- For inventory slot shifting + self.Index = self.Index - 1 + SlotFrame.Name = self.Index + SlotFrame.LayoutOrder = self.Index + end + + function slot:TurnNumber(on) + if SlotNumber then + SlotNumber.Visible = on + end + end + + function slot:SetClickability(on) -- (Happens on open/close arrow) + if self.Tool then + if UserInputService.VREnabled then + SlotFrame.Draggable = false + else + SlotFrame.Draggable = not on + end + UpdateSlotFading() + end + end + + function slot:CheckTerms(terms) + local hits = 0 + local function checkEm(str, term) + local _, n = str:lower():gsub(term, '') + hits = hits + n + end + local tool = self.Tool + if tool then + for term in pairs(terms) do + checkEm(LocalizedName, term) + if tool:IsA('Tool') then --NOTE: HopperBin + checkEm(LocalizedToolTip, term) + end + end + end + return hits + end + + -- Slot select logic, activated by clicking or pressing hotkey + function slot:Select() + local tool = slot.Tool + if tool then + if IsEquipped(tool) then --NOTE: HopperBin + UnequipAllTools() + elseif tool.Parent == Backpack then + EquipNewTool(tool) + end + end + end + + -- Slot Init Logic -- + + SlotFrame = NewGui('TextButton', index) + SlotFrame.BackgroundColor3 = BACKGROUND_COLOR + SlotFrame.BorderColor3 = SLOT_BORDER_COLOR + SlotFrame.Text = "" + SlotFrame.AutoButtonColor = false + SlotFrame.BorderSizePixel = 0 + SlotFrame.Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE) + SlotFrame.Active = true + SlotFrame.Draggable = false + SlotFrame.BackgroundTransparency = SLOT_FADE_LOCKED + SlotFrame.MouseButton1Click:connect(function() changeSlot(slot) end) + slot.Frame = SlotFrame + + do + local selectionObjectClipper = NewGui('Frame', 'SelectionObjectClipper') + selectionObjectClipper.Visible = false + selectionObjectClipper.Parent = SlotFrame + + SelectionObj = NewGui('ImageLabel', 'Selector') + SelectionObj.Size = UDim2.new(1, 0, 1, 0) + SelectionObj.Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png" + SelectionObj.ScaleType = Enum.ScaleType.Slice + SelectionObj.SliceCenter = Rect.new(12,12,52,52) + SelectionObj.Parent = selectionObjectClipper + end + + + ToolIcon = NewGui('ImageLabel', 'Icon') + ToolIcon.Size = UDim2.new(0.8, 0, 0.8, 0) + ToolIcon.Position = UDim2.new(0.1, 0, 0.1, 0) + ToolIcon.Parent = SlotFrame + + ToolName = NewGui('TextLabel', 'ToolName') + ToolName.Size = UDim2.new(1, -2, 1, -2) + ToolName.Position = UDim2.new(0, 1, 0, 1) + ToolName.Parent = SlotFrame + + slot.Frame.LayoutOrder = slot.Index + + if index <= NumberOfHotbarSlots then -- Hotbar-Specific Slot Stuff + -- ToolTip stuff + ToolTip = NewGui('TextLabel', 'ToolTip') + ToolTip.TextWrapped = false + ToolTip.TextYAlignment = Enum.TextYAlignment.Top + ToolTip.BackgroundColor3 = Color3.new(0.4, 0.4, 0.4) + ToolTip.BackgroundTransparency = 0 + ToolTip.Visible = false + ToolTip.Parent = SlotFrame + SlotFrame.MouseEnter:connect(function() + if ToolTip.Text ~= '' then + ToolTip.Visible = true + end + end) + SlotFrame.MouseLeave:connect(function() ToolTip.Visible = false end) + + function slot:MoveToInventory() + if slot.Index <= NumberOfHotbarSlots then -- From a Hotbar slot + local tool = slot.Tool + self:Clear() --NOTE: Order matters here + local newSlot = MakeSlot(UIGridFrame) + newSlot:Fill(tool) + if IsEquipped(tool) then -- Also unequip it --NOTE: HopperBin + UnequipAllTools() + end + -- Also hide the inventory slot if we're showing results right now + if ViewingSearchResults then + newSlot.Frame.Visible = false + newSlot.Parent = InventoryFrame + end + end + end + + -- Show label and assign hotkeys for 1-9 and 0 (zero is always last slot when > 10 total) + if index < 10 or index == NumberOfHotbarSlots then -- NOTE: Hardcoded on purpose! + local slotNum = (index < 10) and index or 0 + SlotNumber = NewGui('TextLabel', 'Number') + SlotNumber.Text = slotNum + SlotNumber.Size = UDim2.new(0.15, 0, 0.15, 0) + SlotNumber.Visible = false + SlotNumber.Parent = SlotFrame + HotkeyFns[ZERO_KEY_VALUE + slotNum] = slot.Select + end + end + + do -- Dragging Logic + local startPoint = SlotFrame.Position + local lastUpTime = 0 + local startParent = nil + + SlotFrame.DragBegin:connect(function(dragPoint) + Dragging[SlotFrame] = true + startPoint = dragPoint + + SlotFrame.BorderSizePixel = 2 + + -- Raise above other slots + SlotFrame.ZIndex = 2 + ToolIcon.ZIndex = 2 + ToolName.ZIndex = 2 + if SlotNumber then + SlotNumber.ZIndex = 2 + end + if HighlightFrame then + HighlightFrame.ZIndex = 2 + for _, child in pairs(HighlightFrame:GetChildren()) do + child.ZIndex = 2 + end + end + + -- Circumvent the ScrollingFrame's ClipsDescendants property + startParent = SlotFrame.Parent + if startParent == UIGridFrame then + local oldAbsolutPos = SlotFrame.AbsolutePosition + local newPosition = UDim2.new(0, SlotFrame.AbsolutePosition.X - InventoryFrame.AbsolutePosition.X, 0, SlotFrame.AbsolutePosition.Y - InventoryFrame.AbsolutePosition.Y) + SlotFrame.Parent = InventoryFrame + SlotFrame.Position = newPosition + + FakeSlotFrame = NewGui('Frame', 'FakeSlot') + FakeSlotFrame.LayoutOrder = SlotFrame.LayoutOrder + FakeSlotFrame.Size = SlotFrame.Size + FakeSlotFrame.BackgroundTransparency = 1 + FakeSlotFrame.Parent = UIGridFrame + end + end) + + SlotFrame.DragStopped:connect(function(x, y) + if FakeSlotFrame then + FakeSlotFrame:Destroy() + end + + local now = tick() + SlotFrame.Position = startPoint + SlotFrame.Parent = startParent + + SlotFrame.BorderSizePixel = 0 + + -- Restore height + SlotFrame.ZIndex = 1 + ToolIcon.ZIndex = 1 + ToolName.ZIndex = 1 + if SlotNumber then + SlotNumber.ZIndex = 1 + end + if HighlightFrame then + HighlightFrame.ZIndex = 1 + for _, child in pairs(HighlightFrame:GetChildren()) do + child.ZIndex = 1 + end + end + + Dragging[SlotFrame] = nil + + -- Make sure the tool wasn't dropped + if not slot.Tool then + return + end + + -- Check where we were dropped + if CheckBounds(InventoryFrame, x, y) then + if slot.Index <= NumberOfHotbarSlots then + slot:MoveToInventory() + end + -- Check for double clicking on an inventory slot, to move into empty hotbar slot + if slot.Index > NumberOfHotbarSlots and now - lastUpTime < DOUBLE_CLICK_TIME then + if LowestEmptySlot then + local myTool = slot.Tool + slot:Clear() + LowestEmptySlot:Fill(myTool) + slot:Delete() + end + now = 0 -- Resets the timer + end + elseif CheckBounds(HotbarFrame, x, y) then + local closest = {math.huge, nil} + for i = 1, NumberOfHotbarSlots do + local otherSlot = Slots[i] + local offset = GetOffset(otherSlot.Frame, Vector2.new(x, y)) + if offset < closest[1] then + closest = {offset, otherSlot} + end + end + local closestSlot = closest[2] + if closestSlot ~= slot then + slot:Swap(closestSlot) + if slot.Index > NumberOfHotbarSlots then + local tool = slot.Tool + if not tool then -- Clean up after ourselves if we're an inventory slot that's now empty + slot:Delete() + else -- Moved inventory slot to hotbar slot, and gained a tool that needs to be unequipped + if IsEquipped(tool) then --NOTE: HopperBin + UnequipAllTools() + end + -- Also hide the inventory slot if we're showing results right now + if ViewingSearchResults then + slot.Frame.Visible = false + slot.Frame.Parent = InventoryFrame + end + end + end + end + else + -- local tool = slot.Tool + -- if tool.CanBeDropped then --TODO: HopperBins + -- tool.Parent = workspace + -- --TODO: Move away from character + -- end + if slot.Index <= NumberOfHotbarSlots then + slot:MoveToInventory() --NOTE: Temporary + end + end + + lastUpTime = now + end) + end + + -- All ready! + SlotFrame.Parent = parent + Slots[index] = slot + + if index > NumberOfHotbarSlots then + UpdateScrollingFrameCanvasSize() + -- Scroll to new inventory slot, if we're open and not viewing search results + if InventoryFrame.Visible and not ViewingSearchResults then + local offset = ScrollingFrame.CanvasSize.Y.Offset - ScrollingFrame.AbsoluteSize.Y + ScrollingFrame.CanvasPosition = Vector2.new(0, math.max(0, offset)) + end + end + + return slot +end + +-- NOTE: We should probably migrate to a 2 collection system: +-- One collection for the hotbar and another collection for the inventory +local function SetNumberOfHotbarSlots(numSlots) + if NumberOfHotbarSlots ~= numSlots then + local prevNumberOfSlots = NumberOfHotbarSlots + local newNumberOfSlots = numSlots + -- If we are shrinking the number of slots we need + -- to move around our tools to the right locations + if prevNumberOfSlots > newNumberOfSlots then + -- Delete the slots that are now no longer in the Hotbar + -- Iterate backwards as to not corrupt our iterator + for i = prevNumberOfSlots, newNumberOfSlots + 1, -1 do + local slot = Slots[i] + if slot then + slot:MoveToInventory() + slot:Delete() + end + end + -- Also need to slide-back the inventory slots now that they are indexed earlier + for i = prevNumberOfSlots, newNumberOfSlots + 1, -1 do + local slot = Slots[i] + if not slot.Tool then + slot:Delete() + end + end + else -- If we added more slots + for i = prevNumberOfSlots, newNumberOfSlots do + -- Incrementally add hotbar slots + NumberOfHotbarSlots = i + if Slots[i] then + -- Move old + local oldSlot = Slots[i] + local oldTool = Slots[i].Tool + + local newSlot = MakeSlot(HotbarFrame, i) + + if oldTool then + newSlot:Fill(oldTool) + elseif not LowestEmptySlot then + LowestEmptySlot = newSlot + end + else + local slot = MakeSlot(HotbarFrame, i) + + slot.Frame.Visible = false + + if not LowestEmptySlot then + LowestEmptySlot = slot + end + end + end + end + NumberOfHotbarSlots = numSlots + FullHotbarSlots = 0 + for i = 1, NumberOfHotbarSlots do + if Slots[i] and Slots[i].Tool then + FullHotbarSlots = FullHotbarSlots + 1 + end + end + + UpdateBackpackLayout() + end +end + +local function OnChildAdded(child) -- To Character or Backpack + if not child:IsA('Tool') and not child:IsA('HopperBin') then --NOTE: HopperBin + if child:IsA('Humanoid') and child.Parent == Character then + Humanoid = child + end + return + end + local tool = child + + if tool.Parent == Character then + ShowVRBackpackPopup() + TimeOfLastToolChange = tick() + end + + if ActiveHopper and tool.Parent == Character then --NOTE: HopperBin + DisableActiveHopper() + end + + --TODO: Optimize / refactor / do something else + if not StarterToolFound and tool.Parent == Character and not SlotsByTool[tool] then + local starterGear = Player:FindFirstChild('StarterGear') + if starterGear then + if starterGear:FindFirstChild(tool.Name) then + StarterToolFound = true + local slot = LowestEmptySlot or MakeSlot(UIGridFrame) + for i = slot.Index, 1, -1 do + local curr = Slots[i] -- An empty slot, because above + local pIndex = i - 1 + if pIndex > 0 then + local prev = Slots[pIndex] -- Guaranteed to be full, because above + prev:Swap(curr) + else + curr:Fill(tool) + end + end + -- Have to manually unequip a possibly equipped tool + for _, child in pairs(Character:GetChildren()) do + if child:IsA('Tool') and child ~= tool then + child.Parent = Backpack + end + end + AdjustHotbarFrames() + return -- We're done here + end + end + end + + -- The tool is either moving or new + local slot = SlotsByTool[tool] + if slot then + slot:UpdateEquipView() + else -- New! Put into lowest hotbar slot or new inventory slot + slot = LowestEmptySlot or MakeSlot(UIGridFrame) + slot:Fill(tool) + if slot.Index <= NumberOfHotbarSlots and not InventoryFrame.Visible then + AdjustHotbarFrames() + end + if tool:IsA('HopperBin') then --NOTE: HopperBin + if tool.Active then + UnequipAllTools() + ActiveHopper = tool + end + end + end +end + +local function OnChildRemoved(child) -- From Character or Backpack + if not child:IsA('Tool') and not child:IsA('HopperBin') then --NOTE: HopperBin + return + end + local tool = child + + ShowVRBackpackPopup() + TimeOfLastToolChange = tick() + + -- Ignore this event if we're just moving between the two + local newParent = tool.Parent + if newParent == Character or newParent == Backpack then + return + end + + local slot = SlotsByTool[tool] + if slot then + slot:Clear() + if slot.Index > NumberOfHotbarSlots then -- Inventory slot + slot:Delete() + elseif not InventoryFrame.Visible then + AdjustHotbarFrames() + end + end + + if tool == ActiveHopper then --NOTE: HopperBin + ActiveHopper = nil + end +end + +local function OnCharacterAdded(character) + -- First, clean up any old slots + for i = #Slots, 1, -1 do + local slot = Slots[i] + if slot.Tool then + slot:Clear() + end + if i > NumberOfHotbarSlots then + slot:Delete() + end + end + ActiveHopper = nil --NOTE: HopperBin + + -- And any old connections + for _, conn in pairs(CharConns) do + conn:disconnect() + end + CharConns = {} + + -- Hook up the new character + Character = character + table.insert(CharConns, character.ChildRemoved:connect(OnChildRemoved)) + table.insert(CharConns, character.ChildAdded:connect(OnChildAdded)) + for _, child in pairs(character:GetChildren()) do + OnChildAdded(child) + end + --NOTE: Humanoid is set inside OnChildAdded + + -- And the new backpack, when it gets here + Backpack = Player:WaitForChild('Backpack') + table.insert(CharConns, Backpack.ChildRemoved:connect(OnChildRemoved)) + table.insert(CharConns, Backpack.ChildAdded:connect(OnChildAdded)) + for _, child in pairs(Backpack:GetChildren()) do + OnChildAdded(child) + end + + AdjustHotbarFrames() +end + +local function OnInputBegan(input, isProcessed) + -- Pass through keyboard hotkeys when not typing into a TextBox and not disabled (except for the Drop key) + if input.UserInputType == Enum.UserInputType.Keyboard and not TextBoxFocused and (WholeThingEnabled or input.KeyCode.Value == DROP_HOTKEY_VALUE) then + local hotkeyBehavior = HotkeyFns[input.KeyCode.Value] + if hotkeyBehavior then + hotkeyBehavior(isProcessed) + end + end +end + +local function OnUISChanged(property) + if property == 'KeyboardEnabled' or property == "VREnabled" then + local on = UserInputService.KeyboardEnabled and not UserInputService.VREnabled + for i = 1, NumberOfHotbarSlots do + Slots[i]:TurnNumber(on) + end + end +end + +local lastChangeToolInputObject = nil +local lastChangeToolInputTime = nil +local maxEquipDeltaTime = 0.06 +local noOpFunc = function() end +local selectDirection = Vector2.new(0,0) +local hotbarVisible = false + +function unbindAllGamepadEquipActions() + ContextActionService:UnbindCoreAction("RBXBackpackHasGamepadFocus") + ContextActionService:UnbindCoreAction("RBXCloseInventory") +end + +local function setHotbarVisibility(visible, isInventoryScreen) + for i = 1, NumberOfHotbarSlots do + local hotbarSlot = Slots[i] + if hotbarSlot and hotbarSlot.Frame and (isInventoryScreen or hotbarSlot.Tool) then + hotbarSlot.Frame.Visible = visible + end + end +end + +local function getInputDirection(inputObject) + local buttonModifier = 1 + if inputObject.UserInputState == Enum.UserInputState.End then + buttonModifier = -1 + end + + if inputObject.KeyCode == Enum.KeyCode.Thumbstick1 then + + local magnitude = inputObject.Position.magnitude + + if magnitude > 0.98 then + local normalizedVector = Vector2.new(inputObject.Position.x / magnitude, -inputObject.Position.y / magnitude) + selectDirection = normalizedVector + else + selectDirection = Vector2.new(0,0) + end + elseif inputObject.KeyCode == Enum.KeyCode.DPadLeft then + selectDirection = Vector2.new(selectDirection.x - 1 * buttonModifier, selectDirection.y) + elseif inputObject.KeyCode == Enum.KeyCode.DPadRight then + selectDirection = Vector2.new(selectDirection.x + 1 * buttonModifier, selectDirection.y) + elseif inputObject.KeyCode == Enum.KeyCode.DPadUp then + selectDirection = Vector2.new(selectDirection.x, selectDirection.y - 1 * buttonModifier) + elseif inputObject.KeyCode == Enum.KeyCode.DPadDown then + selectDirection = Vector2.new(selectDirection.x, selectDirection.y + 1 * buttonModifier) + else + selectDirection = Vector2.new(0,0) + end + + return selectDirection +end + +local selectToolExperiment = function(actionName, inputState, inputObject) + local inputDirection = getInputDirection(inputObject) + + if inputDirection == Vector2.new(0,0) then + return + end + + local angle = math.atan2(inputDirection.y, inputDirection.x) - math.atan2(-1, 0) + if angle < 0 then + angle = angle + (math.pi * 2) + end + + local quarterPi = (math.pi * 0.25) + + local index = (angle/quarterPi) + 1 + index = math.floor(index + 0.5) -- round index to whole number + if index > NumberOfHotbarSlots then + index = 1 + end + + if index > 0 then + local selectedSlot = Slots[index] + if selectedSlot and selectedSlot.Tool and not selectedSlot:IsEquipped() then + selectedSlot:Select() + end + else + UnequipAllTools() + end +end + +changeToolFunc = function(actionName, inputState, inputObject) + if inputState ~= Enum.UserInputState.Begin then return end + + if lastChangeToolInputObject then + if (lastChangeToolInputObject.KeyCode == Enum.KeyCode.ButtonR1 and + inputObject.KeyCode == Enum.KeyCode.ButtonL1) or + (lastChangeToolInputObject.KeyCode == Enum.KeyCode.ButtonL1 and + inputObject.KeyCode == Enum.KeyCode.ButtonR1) then + if (tick() - lastChangeToolInputTime) <= maxEquipDeltaTime then + UnequipAllTools() + lastChangeToolInputObject = inputObject + lastChangeToolInputTime = tick() + return + end + end + end + + lastChangeToolInputObject = inputObject + lastChangeToolInputTime = tick() + + delay(maxEquipDeltaTime, function() + if lastChangeToolInputObject ~= inputObject then return end + + local moveDirection = 0 + if (inputObject.KeyCode == Enum.KeyCode.ButtonL1) then + moveDirection = -1 + else + moveDirection = 1 + end + + for i = 1, NumberOfHotbarSlots do + local hotbarSlot = Slots[i] + if hotbarSlot:IsEquipped() then + + local newSlotPosition = moveDirection + i + local hitEdge = false + if newSlotPosition > NumberOfHotbarSlots then + newSlotPosition = 1 + hitEdge = true + elseif newSlotPosition < 1 then + newSlotPosition = NumberOfHotbarSlots + hitEdge = true + end + + local origNewSlotPos = newSlotPosition + while not Slots[newSlotPosition].Tool do + newSlotPosition = newSlotPosition + moveDirection + if newSlotPosition == origNewSlotPos then return end + + if newSlotPosition > NumberOfHotbarSlots then + newSlotPosition = 1 + hitEdge = true + elseif newSlotPosition < 1 then + newSlotPosition = NumberOfHotbarSlots + hitEdge = true + end + end + + if hitEdge then + UnequipAllTools() + lastEquippedSlot = nil + else + Slots[newSlotPosition]:Select() + end + return + end + end + + if lastEquippedSlot and lastEquippedSlot.Tool then + lastEquippedSlot:Select() + return + end + + local startIndex = moveDirection == -1 and NumberOfHotbarSlots or 1 + local endIndex = moveDirection == -1 and 1 or NumberOfHotbarSlots + for i = startIndex, endIndex, moveDirection do + if Slots[i].Tool then + Slots[i]:Select() + return + end + end + end) +end + +function getGamepadSwapSlot() + for i = 1, #Slots do + if Slots[i].Frame.BorderSizePixel > 0 then + return Slots[i] + end + end +end + +function changeSlot(slot) + local swapInVr = not VRService.VREnabled or InventoryFrame.Visible + + if slot.Frame == GuiService.SelectedCoreObject and swapInVr then + local currentlySelectedSlot = getGamepadSwapSlot() + + if currentlySelectedSlot then + currentlySelectedSlot.Frame.BorderSizePixel = 0 + if currentlySelectedSlot ~= slot then + slot:Swap(currentlySelectedSlot) + VRInventorySelector.SelectionImageObject.Visible = false + + if slot.Index > NumberOfHotbarSlots and not slot.Tool then + if GuiService.SelectedCoreObject == slot.Frame then + GuiService.SelectedCoreObject = currentlySelectedSlot.Frame + end + slot:Delete() + end + + if currentlySelectedSlot.Index > NumberOfHotbarSlots and not currentlySelectedSlot.Tool then + if GuiService.SelectedCoreObject == currentlySelectedSlot.Frame then + GuiService.SelectedCoreObject = slot.Frame + end + currentlySelectedSlot:Delete() + end + end + else + local startSize = slot.Frame.Size + local startPosition = slot.Frame.Position + slot.Frame:TweenSizeAndPosition(startSize + UDim2.new(0, 10, 0, 10), startPosition - UDim2.new(0, 5, 0, 5), Enum.EasingDirection.Out, Enum.EasingStyle.Quad, .1, true, function() slot.Frame:TweenSizeAndPosition(startSize, startPosition, Enum.EasingDirection.In, Enum.EasingStyle.Quad, .1, true) end) + slot.Frame.BorderSizePixel = 3 + VRInventorySelector.SelectionImageObject.Visible = true + end + else + slot:Select() + VRInventorySelector.SelectionImageObject.Visible = false + end +end + +function vrMoveSlotToInventory() + if not VRService.VREnabled then + return + end + + local currentlySelectedSlot = getGamepadSwapSlot() + if currentlySelectedSlot and currentlySelectedSlot.Tool then + currentlySelectedSlot.Frame.BorderSizePixel = 0 + currentlySelectedSlot:MoveToInventory() + VRInventorySelector.SelectionImageObject.Visible = false + end +end + +function enableGamepadInventoryControl() + local goBackOneLevel = function(actionName, inputState, inputObject) + if inputState ~= Enum.UserInputState.Begin then return end + + local selectedSlot = getGamepadSwapSlot() + if selectedSlot then + local selectedSlot = getGamepadSwapSlot() + if selectedSlot then + selectedSlot.Frame.BorderSizePixel = 0 + return + end + elseif InventoryFrame.Visible then + BackpackScript.OpenClose() + spawn(function() GuiService:SetMenuIsOpen(false) end) + end + end + + ContextActionService:BindCoreAction("RBXBackpackHasGamepadFocus", noOpFunc, false, Enum.UserInputType.Gamepad1) + ContextActionService:BindCoreAction("RBXCloseInventory", goBackOneLevel, false, Enum.KeyCode.ButtonB, Enum.KeyCode.ButtonStart) + + -- Gaze select will automatically select the object for us! + if not UseGazeSelection() then + GuiService.SelectedCoreObject = HotbarFrame:FindFirstChild("1") + end +end + + +function disableGamepadInventoryControl() + unbindAllGamepadEquipActions() + + for i = 1, NumberOfHotbarSlots do + local hotbarSlot = Slots[i] + if hotbarSlot and hotbarSlot.Frame then + hotbarSlot.Frame.BorderSizePixel = 0 + end + end + + if GuiService.SelectedCoreObject and GuiService.SelectedCoreObject:IsDescendantOf(MainFrame) then + GuiService.SelectedCoreObject = nil + end +end + + +local function bindBackpackHotbarAction() + if WholeThingEnabled and not GamepadActionsBound then + GamepadActionsBound = true + ContextActionService:BindCoreAction("RBXHotbarEquip", changeToolFunc, false, Enum.KeyCode.ButtonL1, Enum.KeyCode.ButtonR1) + end +end + +local function unbindBackpackHotbarAction() + disableGamepadInventoryControl() + GamepadActionsBound = false + ContextActionService:UnbindCoreAction("RBXHotbarEquip") +end + +function gamepadDisconnected() + GamepadEnabled = false + disableGamepadInventoryControl() +end + +function gamepadConnected() + GamepadEnabled = true + GuiService:AddSelectionParent("RBXBackpackSelection", MainFrame) + + if FullHotbarSlots >= 1 then + bindBackpackHotbarAction() + end + + if InventoryFrame.Visible then + enableGamepadInventoryControl() + end +end + +local function OnCoreGuiChanged(coreGuiType, enabled) + -- Check for enabling/disabling the whole thing + if coreGuiType == Enum.CoreGuiType.Backpack or coreGuiType == Enum.CoreGuiType.All then + enabled = enabled and TopbarEnabled + WholeThingEnabled = enabled + MainFrame.Visible = enabled + + -- Eat/Release hotkeys (Doesn't affect UserInputService) + for _, keyString in pairs(HotkeyStrings) do + if enabled then + GuiService:AddKey(keyString) + else + GuiService:RemoveKey(keyString) + end + end + + if enabled then + if FullHotbarSlots >=1 then + bindBackpackHotbarAction() + end + else + unbindBackpackHotbarAction() + end + end +end + + +local function MakeVRRoundButton(name, image) + local newButton = NewGui('ImageButton', name) + newButton.Size = UDim2.new(0, 40, 0, 40) + newButton.Image = "rbxasset://textures/ui/Keyboard/close_button_background.png"; + + local buttonIcon = NewGui('ImageLabel', 'Icon') + buttonIcon.Size = UDim2.new(0.5,0,0.5,0); + buttonIcon.Position = UDim2.new(0.25,0,0.25,0); + buttonIcon.Image = image; + buttonIcon.Parent = newButton; + + local buttonSelectionObject = NewGui('ImageLabel', 'Selection') + buttonSelectionObject.Size = UDim2.new(0.9,0,0.9,0); + buttonSelectionObject.Position = UDim2.new(0.05,0,0.05,0); + buttonSelectionObject.Image = "rbxasset://textures/ui/Keyboard/close_button_selection.png"; + newButton.SelectionImageObject = buttonSelectionObject + + return newButton, buttonIcon, buttonSelectionObject +end + + +-- Make the main frame, which (mostly) covers the screen +MainFrame = NewGui('Frame', 'Backpack') +MainFrame.Visible = false +MainFrame.Parent = RobloxGui + +-- Make the HotbarFrame, which holds only the Hotbar Slots +HotbarFrame = NewGui('Frame', 'Hotbar') +HotbarFrame.Parent = MainFrame + +-- Make all the Hotbar Slots +for i = 1, NumberOfHotbarSlots do + local slot = MakeSlot(HotbarFrame, i) + slot.Frame.Visible = false + + if not LowestEmptySlot then + LowestEmptySlot = slot + end +end + +-- Up arrow to open the inventory +OpenInventoryButton = NewGui('ImageButton', 'OpenInventory') +do + OpenInventoryButton.Size = UDim2.new(0, 30, 0, 30) + OpenInventoryButton.Image = "rbxasset://textures/ui/Backpack/ScrollUpArrow.png"; + OpenInventoryButton.MouseButton1Click:connect(function() + BackpackScript.OpenClose() + end) + OpenInventoryButton.SelectionGained:connect(function() + OpenInventoryButton.ImageColor3 = ARROW_HOVER_COLOR + end) + OpenInventoryButton.SelectionLost:connect(function() + OpenInventoryButton.ImageColor3 = Color3.new(1,1,1) + end) + local openInventoryButtonSelectionObject = NewGui('Frame', 'Selection') + openInventoryButtonSelectionObject.Visible = false + OpenInventoryButton.SelectionImageObject = openInventoryButtonSelectionObject +end + +CloseInventoryButton = MakeVRRoundButton('CloseInventory', 'rbxasset://textures/ui/Keyboard/close_button_icon.png') +CloseInventoryButton.Position = UDim2.new(0, 0, 0, -50) +CloseInventoryButton.MouseButton1Click:connect(function() + if InventoryFrame.Visible then + BackpackScript.OpenClose() + spawn(function() GuiService:SetMenuIsOpen(false) end) + end +end) + +LeftBumperButton = NewGui('ImageLabel', 'LeftBumper') +LeftBumperButton.Size = UDim2.new(0, 40, 0, 40) +LeftBumperButton.Position = UDim2.new(0, -LeftBumperButton.Size.X.Offset, 0.5, -LeftBumperButton.Size.Y.Offset/2) + +RightBumperButton = NewGui('ImageLabel', 'RightBumper') +RightBumperButton.Size = UDim2.new(0, 40, 0, 40) +RightBumperButton.Position = UDim2.new(1, 0, 0.5, -RightBumperButton.Size.Y.Offset/2) + +-- Make the Inventory, which holds the ScrollingFrame, the header, and the search box +InventoryFrame = NewGui('Frame', 'Inventory') +InventoryFrame.BackgroundTransparency = BACKGROUND_FADE +InventoryFrame.BackgroundColor3 = BACKGROUND_COLOR +InventoryFrame.Active = true +InventoryFrame.Visible = false +InventoryFrame.Parent = MainFrame + +VRInventorySelector = NewGui('TextButton', 'VRInventorySelector') +VRInventorySelector.Position = UDim2.new(0, 0, 0, 0) +VRInventorySelector.Size = UDim2.new(1, 0, 1, 0) +VRInventorySelector.BackgroundTransparency = 1 +VRInventorySelector.Text = "" +VRInventorySelector.Parent = InventoryFrame + +local selectorImage = NewGui('ImageLabel', 'Selector') +selectorImage.Size = UDim2.new(1, 0, 1, 0) +selectorImage.Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png" +selectorImage.ScaleType = Enum.ScaleType.Slice +selectorImage.SliceCenter = Rect.new(12,12,52,52) +selectorImage.Visible = false +VRInventorySelector.SelectionImageObject = selectorImage + +VRInventorySelector.MouseButton1Click:connect(function() + vrMoveSlotToInventory() +end) + +-- Make the ScrollingFrame, which holds the rest of the Slots (however many) +ScrollingFrame = NewGui('ScrollingFrame', 'ScrollingFrame') +ScrollingFrame.Selectable = false +ScrollingFrame.CanvasSize = UDim2.new(0, 0, 0, 0) +ScrollingFrame.Parent = InventoryFrame + +UIGridFrame = NewGui('Frame', 'UIGridFrame') +UIGridFrame.Selectable = false +UIGridFrame.Size = UDim2.new(1, -(ICON_BUFFER*2), 1, 0) +UIGridFrame.Position = UDim2.new(0, ICON_BUFFER, 0, 0) +UIGridFrame.Parent = ScrollingFrame + +UIGridLayout = Instance.new("UIGridLayout") +UIGridLayout.SortOrder = Enum.SortOrder.LayoutOrder +UIGridLayout.CellSize = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE) +UIGridLayout.CellPadding = UDim2.new(0, ICON_BUFFER, 0, ICON_BUFFER) +UIGridLayout.Parent = UIGridFrame + +ScrollUpInventoryButton = MakeVRRoundButton('ScrollUpButton', 'rbxasset://textures/ui/Backpack/ScrollUpArrow.png') +ScrollUpInventoryButton.Size = UDim2.new(0, 34, 0, 34) +ScrollUpInventoryButton.Position = UDim2.new(0.5, -ScrollUpInventoryButton.Size.X.Offset/2, 0, INVENTORY_HEADER_SIZE + 3) +ScrollUpInventoryButton.Icon.Position = ScrollUpInventoryButton.Icon.Position - UDim2.new(0,0,0,2) +ScrollUpInventoryButton.MouseButton1Click:connect(function() + ScrollingFrame.CanvasPosition = Vector2.new( + ScrollingFrame.CanvasPosition.X, + Clamp(0, ScrollingFrame.CanvasSize.Y.Offset - ScrollingFrame.AbsoluteWindowSize.Y, ScrollingFrame.CanvasPosition.Y - (ICON_BUFFER + ICON_SIZE))) +end) + +ScrollDownInventoryButton = MakeVRRoundButton('ScrollDownButton', 'rbxasset://textures/ui/Backpack/ScrollUpArrow.png') +ScrollDownInventoryButton.Rotation = 180 +ScrollDownInventoryButton.Icon.Position = ScrollDownInventoryButton.Icon.Position - UDim2.new(0,0,0,2) +ScrollDownInventoryButton.Size = UDim2.new(0, 34, 0, 34) +ScrollDownInventoryButton.Position = UDim2.new(0.5, -ScrollDownInventoryButton.Size.X.Offset/2, 1, -ScrollDownInventoryButton.Size.Y.Offset - 3) +ScrollDownInventoryButton.MouseButton1Click:connect(function() + ScrollingFrame.CanvasPosition = Vector2.new( + ScrollingFrame.CanvasPosition.X, + Clamp(0, ScrollingFrame.CanvasSize.Y.Offset - ScrollingFrame.AbsoluteWindowSize.Y, ScrollingFrame.CanvasPosition.Y + (ICON_BUFFER + ICON_SIZE))) +end) + +ScrollingFrame.Changed:connect(function(prop) + if prop == 'AbsoluteWindowSize' or prop == 'CanvasPosition' or prop == 'CanvasSize' then + local canScrollUp = ScrollingFrame.CanvasPosition.Y ~= 0 + local canScrollDown = ScrollingFrame.CanvasPosition.Y < ScrollingFrame.CanvasSize.Y.Offset - ScrollingFrame.AbsoluteWindowSize.Y + + ScrollUpInventoryButton.Visible = canScrollUp + ScrollDownInventoryButton.Visible = canScrollDown + end +end) + +-- Position the frames and sizes for the Backpack GUI elements +UpdateBackpackLayout() + +--Make the gamepad hint frame +local gamepadHintsFrame = Utility:Create'Frame' +{ + Name = "GamepadHintsFrame", + Size = UDim2.new(0, HotbarFrame.Size.X.Offset, 0, (IsTenFootInterface and 95 or 60)), + BackgroundTransparency = 1, + Visible = false, + Parent = MainFrame +} + +local function addGamepadHint(hintImage, hintImageLarge, hintText) + local hintFrame = Utility:Create'Frame' + { + Name = "HintFrame", + Size = UDim2.new(1, 0, 1, -5), + Position = UDim2.new(0, 0, 0, 0), + BackgroundTransparency = 1, + Parent = gamepadHintsFrame + } + + local hintImage = Utility:Create'ImageLabel' + { + Name = "HintImage", + Size = (IsTenFootInterface and UDim2.new(0,90,0,90) or UDim2.new(0,60,0,60)), + BackgroundTransparency = 1, + Image = (IsTenFootInterface and hintImageLarge or hintImage), + Parent = hintFrame + } + + local hintText = Utility:Create'TextLabel' + { + Name = "HintText", + Position = UDim2.new(0, (IsTenFootInterface and 100 or 70), 0, 0), + Size = UDim2.new(1, -(IsTenFootInterface and 100 or 70), 1, 0), + Font = Enum.Font.SourceSansBold, + FontSize = (IsTenFootInterface and Enum.FontSize.Size36 or Enum.FontSize.Size24), + BackgroundTransparency = 1, + Text = hintText, + TextColor3 = Color3.new(1,1,1), + TextXAlignment = Enum.TextXAlignment.Left, + Parent = hintFrame + } + local textSizeConstraint = Instance.new("UITextSizeConstraint", hintText) + textSizeConstraint.MaxTextSize = hintText.TextSize +end + +local function resizeGamepadHintsFrame() + gamepadHintsFrame.Size = UDim2.new(HotbarFrame.Size.X.Scale, HotbarFrame.Size.X.Offset, 0, (IsTenFootInterface and 95 or 60)) + gamepadHintsFrame.Position = UDim2.new(HotbarFrame.Position.X.Scale, HotbarFrame.Position.X.Offset, InventoryFrame.Position.Y.Scale, InventoryFrame.Position.Y.Offset - gamepadHintsFrame.Size.Y.Offset) + + local spaceTaken = 0 + + local gamepadHints = gamepadHintsFrame:GetChildren() + --First get the total space taken by all the hints + for i = 1, #gamepadHints do + gamepadHints[i].Size = UDim2.new(1, 0, 1, -5) + gamepadHints[i].Position = UDim2.new(0, 0, 0, 0) + spaceTaken = spaceTaken + (gamepadHints[i].HintText.Position.X.Offset + gamepadHints[i].HintText.TextBounds.X) + end + + --The space between all the frames should be equal + local spaceBetweenElements = (gamepadHintsFrame.AbsoluteSize.X - spaceTaken)/(#gamepadHints - 1) + for i = 1, #gamepadHints do + gamepadHints[i].Position = (i == 1 and UDim2.new(0, 0, 0, 0) or UDim2.new(0, gamepadHints[i-1].Position.X.Offset + gamepadHints[i-1].Size.X.Offset + spaceBetweenElements, 0, 0)) + gamepadHints[i].Size = UDim2.new(0, (gamepadHints[i].HintText.Position.X.Offset + gamepadHints[i].HintText.TextBounds.X), 1, -5) + end +end + +addGamepadHint("rbxasset://textures/ui/Settings/Help/XButtonDark.png", "rbxasset://textures/ui/Settings/Help/XButtonDark@2x.png", "Remove From Hotbar") +addGamepadHint("rbxasset://textures/ui/Settings/Help/AButtonDark.png", "rbxasset://textures/ui/Settings/Help/AButtonDark@2x.png", "Select/Swap") +addGamepadHint("rbxasset://textures/ui/Settings/Help/BButtonDark.png", "rbxasset://textures/ui/Settings/Help/BButtonDark@2x.png", "Close Backpack") + +do -- Search stuff + local searchFrame = NewGui('Frame', 'Search') + searchFrame.BackgroundColor3 = SEARCH_BACKGROUND_COLOR + searchFrame.BackgroundTransparency = SEARCH_BACKGROUND_FADE + searchFrame.Size = UDim2.new(0, SEARCH_WIDTH - (SEARCH_BUFFER * 2), 0, INVENTORY_HEADER_SIZE - (SEARCH_BUFFER * 2)) + searchFrame.Position = UDim2.new(1, -searchFrame.Size.X.Offset - SEARCH_BUFFER, 0, SEARCH_BUFFER) + searchFrame.Parent = InventoryFrame + + local searchBox = NewGui('TextBox', 'TextBox') + searchBox.Text = SEARCH_TEXT + searchBox.ClearTextOnFocus = false + searchBox.FontSize = Enum.FontSize.Size24 + searchBox.TextXAlignment = Enum.TextXAlignment.Left + searchBox.Size = searchFrame.Size - UDim2.new(0, SEARCH_TEXT_OFFSET_FROMLEFT, 0, 0) + searchBox.Position = UDim2.new(0, SEARCH_TEXT_OFFSET_FROMLEFT, 0, 0) + searchBox.Parent = searchFrame + + local xButton = NewGui('TextButton', 'X') + xButton.Text = 'x' + xButton.TextColor3 = SLOT_EQUIP_COLOR + xButton.FontSize = Enum.FontSize.Size24 + xButton.TextYAlignment = Enum.TextYAlignment.Bottom + xButton.BackgroundColor3 = SEARCH_BACKGROUND_COLOR + xButton.BackgroundTransparency = 0 + xButton.Size = UDim2.new(0, searchFrame.Size.Y.Offset - (SEARCH_BUFFER * 2), 0, searchFrame.Size.Y.Offset - (SEARCH_BUFFER * 2)) + xButton.Position = UDim2.new(1, -xButton.Size.X.Offset - (SEARCH_BUFFER * 2), 0.5, -xButton.Size.Y.Offset / 2) + xButton.ZIndex = 0 + xButton.Visible = false + xButton.BorderSizePixel = 0 + xButton.Parent = searchFrame + + local function search() + local terms = {} + for word in searchBox.Text:gmatch('%S+') do + terms[word:lower()] = true + end + + local hitTable = {} + for i = NumberOfHotbarSlots + 1, #Slots do -- Only search inventory slots + local slot = Slots[i] + local hits = slot:CheckTerms(terms) + table.insert(hitTable, {slot, hits}) + slot.Frame.Visible = false + slot.Frame.Parent = InventoryFrame + end + + table.sort(hitTable, function(left, right) + return left[2] > right[2] + end) + ViewingSearchResults = true + + local hitCount = 0 + for i, data in ipairs(hitTable) do + local slot, hits = data[1], data[2] + if hits > 0 then + slot.Frame.Visible = true + slot.Frame.Parent = UIGridFrame + slot.Frame.LayoutOrder = NumberOfHotbarSlots + hitCount + hitCount = hitCount + 1 + end + end + + ScrollingFrame.CanvasPosition = Vector2.new(0, 0) + UpdateScrollingFrameCanvasSize() + + xButton.ZIndex = 3 + end + + local function clearResults() + if xButton.ZIndex > 0 then + ViewingSearchResults = false + for i = NumberOfHotbarSlots + 1, #Slots do + local slot = Slots[i] + slot.Frame.LayoutOrder = slot.Index + slot.Frame.Parent = UIGridFrame + slot.Frame.Visible = true + end + xButton.ZIndex = 0 + end + UpdateScrollingFrameCanvasSize() + end + + local function reset() + clearResults() + searchBox.Text = SEARCH_TEXT + end + + local function onChanged(property) + if property == 'Text' then + local text = searchBox.Text + if text == '' then + clearResults() + elseif text ~= SEARCH_TEXT then + search() + end + xButton.Visible = (text ~= '' and text ~= SEARCH_TEXT) + end + end + + local function onFocused() + if searchBox.Text == SEARCH_TEXT then + searchBox.Text = '' + end + end + + local function focusLost(enterPressed) + if enterPressed then + --TODO: Could optimize + search() + elseif searchBox.Text == '' then + searchBox.Text = SEARCH_TEXT + end + end + + searchBox.Focused:connect(onFocused) + xButton.MouseButton1Click:connect(reset) + searchBox.Changed:connect(onChanged) + searchBox.FocusLost:connect(focusLost) + + BackpackScript.StateChanged.Event:connect(function(isNowOpen) + xButton.Modal = isNowOpen -- Allows free mouse movement even in first person + if not isNowOpen then + reset() + end + end) + + HotkeyFns[Enum.KeyCode.Escape.Value] = function(isProcessed) + if isProcessed then -- Pressed from within a TextBox + reset() + elseif InventoryFrame.Visible then + BackpackScript.OpenClose() + end + end + + local function detectGamepad(lastInputType) + if lastInputType == Enum.UserInputType.Gamepad1 and not UserInputService.VREnabled then + searchFrame.Visible = false + else + searchFrame.Visible = true + end + end + UserInputService.LastInputTypeChanged:connect(detectGamepad) +end + +do -- Make the Inventory expand/collapse arrow (unless TopBar) + local removeHotBarSlot = function(name, state, input) + if state ~= Enum.UserInputState.Begin then return end + if not GuiService.SelectedCoreObject then return end + + for i = 1, NumberOfHotbarSlots do + if Slots[i].Frame == GuiService.SelectedCoreObject and Slots[i].Tool then + Slots[i]:MoveToInventory() + return + end + end + end + + local function openClose() + if not next(Dragging) then -- Only continue if nothing is being dragged + InventoryFrame.Visible = not InventoryFrame.Visible + local nowOpen = InventoryFrame.Visible + AdjustHotbarFrames() + HotbarFrame.Active = not HotbarFrame.Active + for i = 1, NumberOfHotbarSlots do + Slots[i]:SetClickability(not nowOpen) + end + end + + if InventoryFrame.Visible then + if GamepadEnabled then + if GAMEPAD_INPUT_TYPES[UserInputService:GetLastInputType()] then + resizeGamepadHintsFrame() + gamepadHintsFrame.Visible = not UserInputService.VREnabled + end + enableGamepadInventoryControl() + end + if BackpackPanel and VRService.VREnabled then + BackpackPanel:SetVisible(true) + BackpackPanel:RequestPositionUpdate() + end + else + if GamepadEnabled then + gamepadHintsFrame.Visible = false + end + disableGamepadInventoryControl() + end + + if InventoryFrame.Visible then + ContextActionService:BindCoreAction("RBXRemoveSlot", removeHotBarSlot, false, Enum.KeyCode.ButtonX) + else + ContextActionService:UnbindCoreAction("RBXRemoveSlot") + end + + BackpackScript.IsOpen = InventoryFrame.Visible + BackpackScript.StateChanged:Fire(InventoryFrame.Visible) + end + HotkeyFns[ARROW_HOTKEY] = openClose + BackpackScript.OpenClose = openClose -- Exposed +end + +-- Now that we're done building the GUI, we connect to all the major events + +-- Wait for the player if LocalPlayer wasn't ready earlier +while not Player do + wait() + Player = PlayersService.LocalPlayer +end + +-- Listen to current and all future characters of our player +Player.CharacterAdded:connect(OnCharacterAdded) +if Player.Character then + OnCharacterAdded(Player.Character) +end + +do -- Hotkey stuff + -- Init HotkeyStrings, used for eating hotkeys + for i = 0, 9 do + table.insert(HotkeyStrings, tostring(i)) + end + table.insert(HotkeyStrings, ARROW_HOTKEY_STRING) + + -- Listen to key down + UserInputService.InputBegan:connect(OnInputBegan) + + -- Listen to ANY TextBox gaining or losing focus, for disabling all hotkeys + UserInputService.TextBoxFocused:connect(function() TextBoxFocused = true end) + UserInputService.TextBoxFocusReleased:connect(function() TextBoxFocused = false end) + + -- Manual unequip for HopperBins on drop button pressed + HotkeyFns[DROP_HOTKEY_VALUE] = function() --NOTE: HopperBin + if ActiveHopper then + UnequipAllTools() + end + end + + -- Listen to keyboard status, for showing/hiding hotkey labels + UserInputService.Changed:connect(OnUISChanged) + OnUISChanged('KeyboardEnabled') + + -- Listen to gamepad status, for allowing gamepad style selection/equip + if UserInputService:GetGamepadConnected(Enum.UserInputType.Gamepad1) then + gamepadConnected() + end + UserInputService.GamepadConnected:connect(function(gamepadEnum) + if gamepadEnum == Enum.UserInputType.Gamepad1 then + gamepadConnected() + end + end) + UserInputService.GamepadDisconnected:connect(function(gamepadEnum) + if gamepadEnum == Enum.UserInputType.Gamepad1 then + gamepadDisconnected() + end + end) +end + +function BackpackScript:TopbarEnabledChanged(enabled) + TopbarEnabled = enabled + -- Update coregui to reflect new topbar status + OnCoreGuiChanged(Enum.CoreGuiType.Backpack, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Backpack)) +end + +-- Listen to enable/disable signals from the StarterGui +StarterGui.CoreGuiChangedSignal:connect(OnCoreGuiChanged) +local backpackType, healthType = Enum.CoreGuiType.Backpack, Enum.CoreGuiType.Health +OnCoreGuiChanged(backpackType, StarterGui:GetCoreGuiEnabled(backpackType)) +OnCoreGuiChanged(healthType, StarterGui:GetCoreGuiEnabled(healthType)) + + +local BackpackStateChangedInVRConn, VRModuleOpenedConn, VRModuleClosedConn = nil, nil, nil +local function OnVREnabled() + local Panel3D = require(RobloxGui.Modules.VR.Panel3D) + + IsVR = VRService.VREnabled + OnCoreGuiChanged(backpackType, StarterGui:GetCoreGuiEnabled(backpackType)) + OnCoreGuiChanged(healthType, StarterGui:GetCoreGuiEnabled(healthType)) + + VRInventorySelector.Visible = IsVR + + if IsVR then + local VRHub = require(RobloxGui.Modules.VR.VRHub) + + local slotsToStuds = (ICON_SIZE + ICON_BUFFER) / VR_PANEL_RESOLUTION + local inventoryOpenStudSize = Vector2.new(6.25, 7.2) * slotsToStuds + local inventoryClosedStudSize = Vector2.new(6.25, 2) * slotsToStuds -- Closed size is computed as numberOfHotbarSlots + 0.25 + local inventoryOpenPanelCF = CFrame.new(0, 0.6, 0) + local inventoryClosedPanelCF = CFrame.new(0, -1, 0) + local currentPanelLocalCF = inventoryClosedPanelCF + + VRHub:RegisterModule(BackpackScript) + + BackpackPanel = Panel3D.Get(BackpackScript.ModuleName) + BackpackPanel:ResizeStuds(inventoryClosedStudSize.x, inventoryClosedStudSize.y, VR_PANEL_RESOLUTION) + BackpackPanel:SetType(Panel3D.Type.Standard, { CFrame = currentPanelLocalCF }) + BackpackPanel:RequestPositionUpdate() + local panelOriginCF = CFrame.new() + function BackpackPanel:CalculateTransparency() + if InventoryFrame.Visible then + return 0 + end + + local now = tick() + local timeSinceToolChange = now - TimeOfLastToolChange + local transparency = math.clamp(timeSinceToolChange / VR_FADE_TIME, 0, 1) + + if transparency == 1 and BackpackPanel:IsVisible() and not InventoryFrame.Visible then + BackpackPanel:SetVisible(false) + end + + return transparency + end + + function BackpackPanel:PreUpdate() + local inventoryOpen = InventoryFrame.Visible + BackpackPanel.localCF = inventoryOpen and inventoryOpenPanelCF or inventoryClosedPanelCF + end + + function BackpackPanel:OnUpdate() + local inventoryOpen = InventoryFrame.Visible + if not inventoryOpen then + BackpackPanel:ResizeStuds((FullHotbarSlots + 0.25) * slotsToStuds, inventoryClosedStudSize.y, VR_PANEL_RESOLUTION) + end + + -- Update transparency + for i = 1, #Slots do + local slot = Slots[i] + if slot then + slot:UpdateEquipView() + end + end + OpenInventoryButton.ImageTransparency = BackpackPanel.transparency + CloseInventoryButton.ImageTransparency = BackpackPanel.transparency + end + + MainFrame.Parent = BackpackPanel:GetGUI() + OpenInventoryButton.Parent = MainFrame + CloseInventoryButton.Parent = InventoryFrame + + ScrollUpInventoryButton.Parent = InventoryFrame + ScrollDownInventoryButton.Parent = InventoryFrame + -- Stop the ScrollingFrame from automatically scrolling when you hover over items + ScrollingFrame.ScrollingEnabled = false + + BackpackStateChangedInVRConn = BackpackScript.StateChanged.Event:connect(function(isNowOpen) + if isNowOpen then + VRHub:FireModuleOpened(BackpackScript.ModuleName) + BackpackPanel:ResizeStuds(inventoryOpenStudSize.x, inventoryOpenStudSize.y, VR_PANEL_RESOLUTION) + BackpackPanel:SetCanFade(false) + else + VRHub:FireModuleClosed(BackpackScript.ModuleName) + BackpackPanel:ResizeStuds(inventoryClosedStudSize.x, inventoryClosedStudSize.y, VR_PANEL_RESOLUTION) + BackpackPanel:SetCanFade(true) + end + end) + + VRModuleOpenedConn = VRHub.ModuleOpened.Event:connect(function(moduleName) + local openedModule = VRHub:GetModule(moduleName) + if openedModule ~= BackpackScript and openedModule.VRIsExclusive then + BackpackPanel:SetVisible(EvaluateBackpackPanelVisibility(false)) + if InventoryFrame.Visible then + BackpackScript.OpenClose() + end + end + end) + VRModuleClosedConn = VRHub.ModuleClosed.Event:connect(function(moduleName) + local openedModule = VRHub:GetModule(moduleName) + if openedModule ~= BackpackScript then + BackpackPanel:SetVisible(EvaluateBackpackPanelVisibility(true)) + end + end) + + + -- Turn off dragging when in VR + for _, slot in pairs(Slots) do + slot:SetClickability(false) + end + else -- not IsVR (VR was turned off) + local BackpackPanel = Panel3D.Get(BackpackScript.ModuleName) + BackpackPanel:SetVisible(EvaluateBackpackPanelVisibility(false)) + BackpackPanel:LinkTo(nil) + + MainFrame.Parent = RobloxGui + OpenInventoryButton.Parent = nil + CloseInventoryButton.Parent = nil + + ScrollUpInventoryButton.Parent = nil + ScrollDownInventoryButton.Parent = nil + ScrollingFrame.ScrollingEnabled = true + + -- Turn draggin back on + for _, slot in pairs(Slots) do + slot:SetClickability(true) + end + + if BackpackStateChangedInVRConn then + BackpackStateChangedInVRConn:disconnect() + BackpackStateChangedInVRConn = nil + end + if VRModuleOpenedConn then + VRModuleOpenedConn:disconnect() + VRModuleOpenedConn = nil + end + if VRModuleClosedConn then + VRModuleClosedConn:disconnect() + VRModuleClosedConn = nil + end + end + + NumberOfInventoryRows = IsVR and INVENTORY_ROWS_VR or (IS_PHONE and INVENTORY_ROWS_MINI or INVENTORY_ROWS_FULL) + local newSlotTotal = IsVR and HOTBAR_SLOTS_VR or (IS_PHONE and HOTBAR_SLOTS_MINI or HOTBAR_SLOTS_FULL) + SetNumberOfHotbarSlots(newSlotTotal) +end +VRService:GetPropertyChangedSignal("VREnabled"):connect(OnVREnabled) +OnVREnabled() + +return BackpackScript diff --git a/Client2018/content/scripts/CoreScripts/Modules/BackpackScript3D.lua b/Client2018/content/scripts/CoreScripts/Modules/BackpackScript3D.lua new file mode 100644 index 0000000..1079739 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/BackpackScript3D.lua @@ -0,0 +1,521 @@ +--BackpackScript3D: VR port of backpack interface using a 3D panel +--written by 0xBAADF00D +local ICON_SIZE = 48 +local ICON_SPACING = 52 +local PIXELS_PER_STUD = 64 + +local SLOT_BORDER_SIZE = 0 +local SLOT_BORDER_SELECTED_SIZE = 4 +local SLOT_BORDER_COLOR = Color3.new(90/255, 142/255, 233/255) +local SLOT_BACKGROUND_COLOR = Color3.new(0.2, 0.2, 0.2) +local SLOT_HOVER_BACKGROUND_COLOR = Color3.new(90/255, 90/255, 90/255) + +local HOPPERBIN_ANGLE = math.rad(-45) +local HOPPERBIN_ROTATION = CFrame.Angles(HOPPERBIN_ANGLE, 0, 0) +local HOPPERBIN_OFFSET = Vector3.new(0, 0, -5) + +local HEALTHBAR_SPACE = 12 +local HEALTHBAR_WIDTH = 82 +local HEALTHBAR_HEIGHT = 5 + +local NAME_SPACE = 14 + +local Tools = {} +local ToolsList = {} +local slotIcons = {} + +local BackpackScript = {} +local topbarEnabled = false + +local player = game:GetService("Players").LocalPlayer +local currentHumanoid = nil +local CoreGui = game:GetService('CoreGui') +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local Panel3D = require(RobloxGui.Modules.VR.Panel3D) +local Util = require(RobloxGui.Modules.Settings.Utility) + +local ContextActionService = game:GetService("ContextActionService") + +local BackpackPanel = Panel3D.Get("Backpack") +BackpackPanel:ResizeStuds(5, 2) +BackpackPanel:SetType(Panel3D.Type.Fixed, { CFrame = CFrame.new(0, 0, -5) }) +BackpackPanel:SetVisible(true) + +local toolsFrame = Instance.new("TextButton", BackpackPanel:GetGUI()) --prevent clicks falling through in case you have a rocket launcher and blow yourself up +toolsFrame.Text = "" +toolsFrame.Size = UDim2.new(1, 0, 0, ICON_SIZE) +toolsFrame.BackgroundTransparency = 1 +toolsFrame.Selectable = false +local insetAdjustY = toolsFrame.AbsolutePosition.Y +toolsFrame.Position = UDim2.new(0, 0, 0, HEALTHBAR_SPACE + NAME_SPACE) + +--Healthbar color function stolen from Topbar.lua +local HEALTH_BACKGROUND_COLOR = Color3.new(228/255, 236/255, 246/255) +local HEALTH_RED_COLOR = Color3.new(255/255, 28/255, 0/255) +local HEALTH_YELLOW_COLOR = Color3.new(250/255, 235/255, 0) +local HEALTH_GREEN_COLOR = Color3.new(27/255, 252/255, 107/255) + +local healthbarBack = Instance.new("ImageLabel", BackpackPanel:GetGUI()) +healthbarBack.ImageColor3 = HEALTH_BACKGROUND_COLOR +healthbarBack.BackgroundTransparency = 1 +healthbarBack.ScaleType = Enum.ScaleType.Slice +healthbarBack.SliceCenter = Rect.new(10, 10, 10, 10) +healthbarBack.Name = "HealthbarContainer" +healthbarBack.Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png" +local healthbarFront = Instance.new("ImageLabel", healthbarBack) +healthbarFront.ImageColor3 = HEALTH_GREEN_COLOR +healthbarFront.BackgroundTransparency = 1 +healthbarFront.ScaleType = Enum.ScaleType.Slice +healthbarFront.SliceCenter = Rect.new(10, 10, 10, 10) +healthbarFront.Size = UDim2.new(1, 0, 1, 0) +healthbarFront.Position = UDim2.new(0, 0, 0, 0) +healthbarFront.Name = "HealthbarFill" +healthbarFront.Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png" + +local playerName = Instance.new("TextLabel", BackpackPanel:GetGUI()) +playerName.Name = "PlayerName" +playerName.BackgroundTransparency = 1 +playerName.TextColor3 = Color3.new(1, 1, 1) +playerName.Text = player.Name +playerName.Font = Enum.Font.SourceSansBold +playerName.FontSize = Enum.FontSize.Size12 +playerName.TextXAlignment = Enum.TextXAlignment.Left +playerName.Size = UDim2.new(1, 0, 0, NAME_SPACE) + + +BackpackScript.ToolAddedEvent = Instance.new("BindableEvent") + + +local healthColorToPosition = { + [Vector3.new(HEALTH_RED_COLOR.r, HEALTH_RED_COLOR.g, HEALTH_RED_COLOR.b)] = 0.1; + [Vector3.new(HEALTH_YELLOW_COLOR.r, HEALTH_YELLOW_COLOR.g, HEALTH_YELLOW_COLOR.b)] = 0.5; + [Vector3.new(HEALTH_GREEN_COLOR.r, HEALTH_GREEN_COLOR.g, HEALTH_GREEN_COLOR.b)] = 0.8; +} +local min = 0.1 +local minColor = HEALTH_RED_COLOR +local max = 0.8 +local maxColor = HEALTH_GREEN_COLOR + +local function HealthbarColorTransferFunction(healthPercent) + if healthPercent < min then + return minColor + elseif healthPercent > max then + return maxColor + end + + -- Shepard's Interpolation + local numeratorSum = Vector3.new(0,0,0) + local denominatorSum = 0 + for colorSampleValue, samplePoint in pairs(healthColorToPosition) do + local distance = healthPercent - samplePoint + if distance == 0 then + -- If we are exactly on an existing sample value then we don't need to interpolate + return Color3.new(colorSampleValue.x, colorSampleValue.y, colorSampleValue.z) + else + local wi = 1 / (distance*distance) + numeratorSum = numeratorSum + wi * colorSampleValue + denominatorSum = denominatorSum + wi + end + end + local result = numeratorSum / denominatorSum + return Color3.new(result.x, result.y, result.z) +end +--- + +local backpackEnabled = true +local healthbarEnabled = true + +local function UpdateLayout() + local width, height = 100, 100 + local borderSize = (ICON_SPACING - ICON_SIZE) / 2 + + local x = borderSize + local y = 0 + for _, tool in ipairs(ToolsList) do + local slot = Tools[tool] + if slot then + slot.icon.Position = UDim2.new(0, x, 0, y) + x = x + ICON_SPACING + end + end + + if #ToolsList == 0 then + width = HEALTHBAR_WIDTH + height = HEALTHBAR_SPACE + NAME_SPACE + BackpackPanel.showCursor = false + else + width = #ToolsList * ICON_SPACING + height = ICON_SIZE + HEALTHBAR_SPACE + NAME_SPACE + BackpackPanel.showCursor = true + end + + BackpackPanel:ResizePixels(width, height) + + playerName.Position = UDim2.new(0, borderSize, 0, 0) + + healthbarBack.Position = UDim2.new(0, borderSize, 0, NAME_SPACE + (HEALTHBAR_SPACE - HEALTHBAR_HEIGHT) / 2) + healthbarBack.Size = UDim2.new(0, HEALTHBAR_WIDTH, 0, HEALTHBAR_HEIGHT) +end + +local function UpdateHealth(humanoid) + local percentHealth = humanoid.Health / humanoid.MaxHealth + if percentHealth ~= percentHealth then + percentHealth = 1 + end + healthbarFront.BackgroundColor3 = HealthbarColorTransferFunction(percentHealth) + healthbarFront.Size = UDim2.new(percentHealth, 0, 1, 0) +end + +local function SetTransparency(transparency) + for i, v in pairs(Tools) do + v.bg.ImageTransparency = transparency + v.image.ImageTransparency = transparency + v.text.TextTransparency = transparency + end + + playerName.TextTransparency = transparency + healthbarBack.ImageTransparency = transparency + healthbarFront.ImageTransparency = transparency +end + +local function OnHotbarEquipPrimary(actionName, state, obj) + if state ~= Enum.UserInputState.Begin then + return + end + for tool, slot in pairs(Tools) do + if slot.hovered then + slot.OnClick() + return + end + end +end + +local function EnableHotbarInput(enable) + if not backpackEnabled then + enable = false + end + if not currentHumanoid then + return + end + if enable then + ContextActionService:BindCoreAction("HotbarEquipPrimary", OnHotbarEquipPrimary, false, Enum.KeyCode.ButtonA, Enum.KeyCode.ButtonR2, Enum.UserInputType.MouseButton1) + else + ContextActionService:UnbindCoreAction("HotbarEquipPrimary") + end +end + +local function AddTool(tool) + if Tools[tool] then + return + end + + local slot = {} + Tools[tool] = slot + table.insert(ToolsList, tool) + + slot.hovered = false + slot.tool = tool + + slot.icon = Instance.new("TextButton", toolsFrame) + slot.icon.Text = "" + slot.icon.Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE) + slot.icon.BackgroundColor3 = Color3.new(0, 0, 0) + slot.icon.Selectable = true + slot.icon.BackgroundTransparency = 1 + slotIcons[tool] = slot.icon + + slot.bg = Instance.new("ImageLabel", slot.icon) + slot.bg.Position = UDim2.new(0, -1, 0, -1) + slot.bg.Size = UDim2.new(1, 2, 1, 2) + slot.bg.Image = "rbxasset://textures/ui/VR/rectBackground.png" + slot.bg.ScaleType = Enum.ScaleType.Slice + slot.bg.SliceCenter = Rect.new(10, 10, 10, 10) + slot.bg.BackgroundTransparency = 1 + + slot.image = Instance.new("ImageLabel", slot.icon) + slot.image.Position = UDim2.new(0, 1, 0, 1) + slot.image.Size = UDim2.new(1, -2, 1, -2) + slot.image.BackgroundTransparency = 1 + slot.image.Selectable = false + + slot.text = Instance.new("TextLabel", slot.icon) + slot.text.Position = UDim2.new(0, 1, 0, 1) + slot.text.Size = UDim2.new(1, -2, 1, -2) + slot.text.BackgroundTransparency = 1 + slot.text.TextColor3 = Color3.new(1, 1, 1) + slot.text.Font = Enum.Font.SourceSans + slot.text.FontSize = Enum.FontSize.Size12 + slot.text.ClipsDescendants = true + slot.text.Selectable = false + + local selectionObject = Util:Create'ImageLabel' + { + Name = 'SelectionObject'; + Size = UDim2.new(1,0,1,0); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png"; + ImageTransparency = 0; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(12,12,52,52); + BorderSizePixel = 0; + } + slot.icon.SelectionImageObject = selectionObject + + local function updateToolData() + slot.image.Image = tool.TextureId + slot.text.Text = tool.TextureId == "" and tool.Name or "" + end + updateToolData() + + slot.OnClick = function() + if not player.Character then return end + local humanoid = player.Character:FindFirstChild("Humanoid") + if not humanoid then return end + + local inBackpack = tool.Parent == player.Backpack + humanoid:UnequipTools() + if inBackpack then + humanoid:EquipTool(tool) + end + end + + slot.icon.MouseButton1Click:connect(slot.OnClick) + slot.OnEnter = function() + slot.hovered = true + end + slot.OnLeave = function() + slot.hovered = false + end +-- slot.icon.MouseEnter:connect(slot.OnEnter) +-- slot.icon.MouseLeave:connect(slot.OnLeave) + + tool.Changed:connect(function(prop) + if prop == "Parent" then + if tool.Parent == player:FindFirstChild("Backpack") then + slot.bg.Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE) --temporary hold-over until new backpack design comes along (can't use border with this antialiased frame stand-in) + slot.bg.Position = UDim2.new(0, 0, 0, 0) + elseif tool.Parent == player.Character then + slot.bg.Size = UDim2.new(0, ICON_SIZE + 8, 0, ICON_SIZE + 8) + slot.bg.Position = UDim2.new(0, -4, 0, -4) + end + elseif prop == "TextureId" or prop == "Name" then + updateToolData() + end + end) + + UpdateLayout() + + BackpackScript.ToolAddedEvent:Fire(tool) +end + +local humanoidChangedEvent = nil +local humanoidAncestryChangedEvent = nil +local function RegisterHumanoid(humanoid) + currentHumanoid = humanoid + if humanoidChangedEvent then + humanoidChangedEvent:disconnect() + humanoidChangedEvent = nil + end + if humanoidAncestryChangedEvent then + humanoidAncestryChangedEvent:disconnect() + humanoidAncestryChangedEvent = nil + end + if humanoid then + humanoidChangedEvent = humanoid.HealthChanged:connect(function() UpdateHealth(humanoid) end) + humanoidAncestryChangedEvent = humanoid.AncestryChanged:connect(function(child, parent) + if child == humanoid and parent ~= player.Character then + RegisterHumanoid(nil) + end + end) + UpdateHealth(humanoid) + end +end + +local function OnChildAdded(child) + if child:IsA("Tool") or child:IsA("HopperBin") then + AddTool(child) + end + if child:IsA("Humanoid") and child.Parent == player.Character then + RegisterHumanoid(child) + end +end + +local function RemoveTool(tool) + if not Tools[tool] then + return + end + Tools[tool].icon:Destroy() + for i, v in ipairs(ToolsList) do + if v == tool then + table.remove(ToolsList, i) + break + end + end + Tools[tool] = nil + slotIcons[tool] = nil + UpdateLayout() +end + +local function OnChildRemoved(child) + if child:IsA("Tool") or child:IsA("HopperBin") then + if Tools[child] then + if child.Parent ~= player:FindFirstChild("Backpack") and child.Parent ~= player.Character then + RemoveTool(child) + end + end + end +end + +local function OnCharacterAdded(character) + local backpack = player:WaitForChild("Backpack") + + for i, v in ipairs(character:GetChildren()) do + if v:IsA("Humanoid") then + RegisterHumanoid(v) + break + end + end + + for tool, v in pairs(Tools) do + RemoveTool(tool) + end + Tools = {} + ToolsList = {} + + character.ChildAdded:connect(OnChildAdded) + character.ChildRemoved:connect(OnChildRemoved) + + for i, v in ipairs(backpack:GetChildren()) do + OnChildAdded(v) + end + + backpack.ChildAdded:connect(OnChildAdded) + backpack.ChildRemoved:connect(OnChildRemoved) +end + +player.CharacterAdded:connect(OnCharacterAdded) +if player.Character then + spawn(function() OnCharacterAdded(player.Character) end) +end + +local function OnHotbarEquip(actionName, state, obj) + if not backpackEnabled then + return + end + local character = player.Character + if not character then + return + end + if not currentHumanoid then + return + end + if state ~= Enum.UserInputState.Begin then + return + end + if #ToolsList == 0 then + return + end + local current = 0 + for i, v in pairs(ToolsList) do + if v.Parent == character then + current = i + end + end + currentHumanoid:UnequipTools() + if obj.KeyCode == Enum.KeyCode.ButtonR1 then + current = current + 1 + if current > #ToolsList then + current = 1 + end + else + current = current - 1 + if current < 1 then + current = #ToolsList + end + end + currentHumanoid:EquipTool(ToolsList[current]) +end + +local function OnCoreGuiChanged(coreGuiType, enabled) + -- Check for enabling/disabling the whole thing + if coreGuiType == Enum.CoreGuiType.Backpack or coreGuiType == Enum.CoreGuiType.All then + backpackEnabled = enabled + UpdateLayout() + if enabled then + ContextActionService:BindCoreAction("HotbarEquip2", OnHotbarEquip, false, Enum.KeyCode.ButtonL1, Enum.KeyCode.ButtonR1) + toolsFrame.Parent = BackpackPanel:GetGUI() + else + ContextActionService:UnbindCoreAction("HotbarEquip2") + toolsFrame.Parent = nil + end + end + + if coreGuiType == Enum.CoreGuiType.Health or coreGuiType == Enum.CoreGuiType.All then + healthbarEnabled = enabled + UpdateLayout() + if enabled then + healthbarBack.Parent = BackpackPanel:GetGUI() + else + healthbarBack.Parent = nil + end + end +end + +local StarterGui = game:GetService("StarterGui") +StarterGui.CoreGuiChangedSignal:connect(OnCoreGuiChanged) +OnCoreGuiChanged(Enum.CoreGuiType.Backpack, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Backpack)) +OnCoreGuiChanged(Enum.CoreGuiType.Backpack, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.All)) + +OnCoreGuiChanged(Enum.CoreGuiType.Health, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Health)) +OnCoreGuiChanged(Enum.CoreGuiType.Health, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.All)) + +local panelLocalCF = CFrame.Angles(math.rad(-5), 0, 0) * CFrame.new(0, 1.75, 0) * CFrame.Angles(math.rad(-5), 0, 0) + +function BackpackPanel:PreUpdate(cameraCF, cameraRenderCF, userHeadCF, lookRay) + --the backpack panel needs to go in front of the user when they look at it. + --if they aren't looking, we should be updating self.localCF + + local topbarPanel = Panel3D.Get("Topbar3D") + local panelOriginCF = topbarPanel.localCF or CFrame.new() + self.localCF = panelOriginCF * panelLocalCF +end + +function BackpackPanel:OnUpdate() + SetTransparency(self.transparency) + + local hovered, tool = BackpackPanel:FindHoveredGuiElement(slotIcons) + if hovered and tool then + local slot = Tools[tool] + if not slot.hovered then + slot.OnEnter() + end + for i, v in pairs(Tools) do + if v.hovered and v ~= slot then + v.OnLeave() + end + end + end +end + +function BackpackPanel:OnMouseEnter(x, y) + EnableHotbarInput(true) +end +function BackpackPanel:OnMouseLeave(x, y) + EnableHotbarInput(false) +end + +local VRHub = require(RobloxGui.Modules.VR.VRHub) +VRHub.ModuleOpened.Event:connect(function(moduleName) + local module = VRHub:GetModule(moduleName) + if module.VRIsExclusive then + BackpackPanel:SetVisible(false) + end +end) +VRHub.ModuleClosed.Event:connect(function(moduleName) + BackpackPanel:SetVisible(true) +end) + + +BackpackPanel:LinkTo("Topbar3D") + +return BackpackScript diff --git a/Client2018/content/scripts/CoreScripts/Modules/BusinessLogic.lua b/Client2018/content/scripts/CoreScripts/Modules/BusinessLogic.lua new file mode 100644 index 0000000..6d6c256 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/BusinessLogic.lua @@ -0,0 +1,19 @@ +local BusinessLogic = {} + + + + + +function BusinessLogic.GetVisibleAgeForPlayer(player) + local accountTypeText = "Account: <13" + if player and not player:GetUnder13() then + accountTypeText = "Account: 13+" + end + return accountTypeText +end + + + + + +return BusinessLogic diff --git a/Client2018/content/scripts/CoreScripts/Modules/Chat.lua b/Client2018/content/scripts/CoreScripts/Modules/Chat.lua new file mode 100644 index 0000000..099a6f5 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Chat.lua @@ -0,0 +1,2786 @@ +--[[ + // FileName: Chat.lua + // Written by: SolarCrane + // Description: Code for lua side chat on ROBLOX. +]] + +--[[ CONSTANTS ]] + +-- NOTE: IF YOU WANT TO USE THIS CHAT SCRIPT IN YOUR OWN GAME: +-- 1) COPY THE CONTENTS OF THIS FILE INTO A MODULE +-- 2) CREATE A LOCALSCRIPT AND PARENT IT TO StarterGui +-- 3) IN THE LOCALSCRIPT require() THE CHAT MODULE YOU MADE IN STEP 1 +-- 4) CONFIGURE YOUR PLACE ON THE WEBSITE TO USE BUBBLE-CHAT +-- 5) SET THE FOLLOWING TWO VARIABLES TO TRUE +local FORCE_CHAT_GUI = false +local NON_CORESCRIPT_MODE = false +-- 6) (OPTIONAL) PUT THE FOLLOWING LINE IN A SERVER SCRIPT TO MAKE CHAT PERSIST THROUGH RESPAWNING +-- game:GetService('StarterGui').ResetPlayerGuiOnSpawn = false +--------------------------------- + +local MESSAGES_FADE_OUT_TIME = 30 +local MAX_UDIM_SIZE = 2^15 - 1 + +local CHAT_WINDOW_Y_OFFSET = 2 + +local PHONE_SCREEN_WIDTH = 640 +local TABLET_SCREEN_WIDTH = 1024 + +local FLOOD_CHECK_MESSAGE_COUNT = 7 +local FLOOD_CHECK_MESSAGE_INTERVAL = 15 -- This is in seconds + +local VR_CHAT_CLICK_DEBOUNCE = 0.25 + +local SCROLLBAR_THICKNESS = 7 +local CHAT_COLORS = +{ + Color3.new(253/255, 41/255, 67/255), -- BrickColor.new("Bright red").Color, + Color3.new(1/255, 162/255, 255/255), -- BrickColor.new("Bright blue").Color, + Color3.new(2/255, 184/255, 87/255), -- BrickColor.new("Earth green").Color, + BrickColor.new("Bright violet").Color, + BrickColor.new("Bright orange").Color, + BrickColor.new("Bright yellow").Color, + BrickColor.new("Light reddish violet").Color, + BrickColor.new("Brick yellow").Color, +} + +local thisModuleName = "Chat" + +local emptySelectionImage = Instance.new("ImageLabel") +emptySelectionImage.ImageTransparency = 1 +emptySelectionImage.BackgroundTransparency = 1 + +--[[ END OF CONSTANTS ]] + +--[[ SERVICES ]] +local RunService = game:GetService('RunService') +local CoreGuiService = game:GetService('CoreGui') +local PlayersService = game:GetService('Players') +local DebrisService = game:GetService('Debris') +local GuiService = game:GetService('GuiService') +local InputService = game:GetService('UserInputService') +local StarterGui = game:GetService('StarterGui') +local ContextActionService = game:GetService('ContextActionService') +local Settings = UserSettings() +local GameSettings = Settings.GameSettings +--[[ END OF SERVICES ]] + +--[[ Fast Flags ]]-- +local FFlagEnableNewDevConsole = settings():GetFFlag("EnableNewDevConsole") + +--[[ SCRIPT VARIABLES ]] +local RobloxGui = CoreGuiService:WaitForChild("RobloxGui") +local VRHub = require(RobloxGui.Modules.VR.VRHub) +local PlayerPermissionsModule = require(RobloxGui.Modules.PlayerPermissionsModule) +local StatsUtils = require(RobloxGui.Modules.Stats.StatsUtils) + +-- I am not fond of waiting at the top of the script here... +while PlayersService.LocalPlayer == nil do PlayersService.ChildAdded:wait() end +local Player = PlayersService.LocalPlayer +-- GuiRoot will act as the top-node for parenting GUIs +local GuiRoot = Instance.new('Frame') +GuiRoot.Name = 'GuiRoot'; +GuiRoot.Size = UDim2.new(1,0,1,0); +GuiRoot.BackgroundTransparency = 1; + + + +local chatRepositioned = false +local chatBarDisabled = false + +local lastSelectedPlayer = nil +local lastSelectedButton = nil + +local blockingUtility = nil + +local topbarEnabled = true + +if not NON_CORESCRIPT_MODE and not InputService.VREnabled then + blockingUtility = require(RobloxGui.Modules:WaitForChild("PlayerDropDown")):CreateBlockingUtility() +end + +--[[ END OF SCRIPT VARIABLES ]] + +local function GetLuaChatFilteringFlag() + return true +end + +local Util = {} +do + -- Check if we are running on a touch device + function Util.IsTouchDevice() + local touchEnabled = false + pcall(function() touchEnabled = InputService.TouchEnabled end) + return touchEnabled + end + + function Util.IsSmallScreenSize() + return GuiRoot.AbsoluteSize.X <= PHONE_SCREEN_WIDTH + end + + function Util.Create(instanceType) + return function(data) + local obj = Instance.new(instanceType) + for k, v in pairs(data) do + if type(k) == 'number' then + v.Parent = obj + else + obj[k] = v + end + end + return obj + end + end + + function Util.Clamp(low, high, input) + return math.max(low, math.min(high, input)) + end + + function Util.Linear(t, b, c, d) + if t >= d then return b + c end + + return c*t/d + b + end + + function Util.EaseOutQuad(t, b, c, d) + if t >= d then return b + c end + + t = t/d; + return -c * t*(t-2) + b + end + + function Util.EaseInOutQuad(t, b, c, d) + if t >= d then + return b + c + end + + t = t / (d/2); + if (t < 1) then + return c/2*t*t + b + end; + t = t - 1; + return -c/2 * (t*(t-2) - 1) + b; + end + + function Util.PropertyTweener(instance, prop, start, final, duration, easingFunc, cbFunc) + local this = {} + this.StartTime = tick() + this.EndTime = this.StartTime + duration + this.Cancelled = false + + local finished = false + local percentComplete = 0 + spawn(function() + local now = tick() + while now < this.EndTime and instance do + if this.Cancelled then + return + end + instance[prop] = easingFunc(now - this.StartTime, start, final - start, duration) + percentComplete = Util.Clamp(0, 1, (now - this.StartTime) / duration) + RunService.RenderStepped:wait() + now = tick() + end + if this.Cancelled == false and instance then + instance[prop] = final + finished = true + percentComplete = 1 + if cbFunc then + cbFunc() + end + end + end) + + function this:GetPercentComplete() + return percentComplete + end + + function this:IsFinished() + return finished + end + + function this:Cancel() + this.Cancelled = true + end + + return this + end + + function Util.Signal() + local sig = {} + + local mSignaler = Instance.new('BindableEvent') + + local mArgData = nil + local mArgDataCount = nil + + function sig:fire(...) + mArgData = {...} + mArgDataCount = select('#', ...) + mSignaler:Fire() + end + + function sig:connect(f) + if not f then error("connect(nil)", 2) end + return mSignaler.Event:connect(function() + f(unpack(mArgData, 1, mArgDataCount)) + end) + end + + function sig:wait() + mSignaler.Event:wait() + assert(mArgData, "Missing arg data, likely due to :TweenSize/Position corrupting threadrefs.") + return unpack(mArgData, 1, mArgDataCount) + end + + return sig + end + + function Util.DisconnectEvent(conn) + if conn then + conn:disconnect() + end + return nil + end + + function Util.SetGUIInsetBounds(x, y) + local success, _ = pcall(function() GuiService:SetGlobalGuiInset(0, x, 0, y) end) + if not success then + pcall(function() GuiService:SetGlobalSizeOffsetPixel(-x, -y) end) -- Legacy GUI-offset function + end + end + + local baseUrl = game:GetService("ContentProvider").BaseUrl:lower() + baseUrl = string.gsub(baseUrl,"/m.","/www.") --mobile site does not work for this stuff! + function Util.GetSecureApiBaseUrl() + local secureApiUrl = baseUrl + secureApiUrl = string.gsub(secureApiUrl,"http://","https://") + secureApiUrl = string.gsub(secureApiUrl,"www","api") + return secureApiUrl + end + + function Util.GetPlayerByName(playerName) + -- O(n), may be faster if I store a reverse hash from the players list; can't trust FindFirstChild in PlayersService because anything can be parented to there. + local lowerName = string.lower(playerName) + for _, player in pairs(PlayersService:GetPlayers()) do + if string.lower(player.Name) == lowerName then + return player + end + end + return nil -- Found no player + end + + local function MakeIsInGroup(groupId, requiredRank) + assert(type(requiredRank) == "nil" or type(requiredRank) == "number", "requiredRank must be a number or nil") + + local inGroupCache = {} + return function(player) + if player and player.UserId then + local userId = player.UserId + + if inGroupCache[userId] == nil then + local inGroup = false + pcall(function() -- Many things can error is the IsInGroup check + if requiredRank then + inGroup = player:GetRankInGroup(groupId) > requiredRank + else + inGroup = player:IsInGroup(groupId) + end + end) + inGroupCache[userId] = inGroup + end + + return inGroupCache[userId] + end + + return false + end + end + Util.IsPlayerAdminAsync = MakeIsInGroup(1200769) + Util.IsPlayerInternAsync = MakeIsInGroup(2868472, 100) + + local function GetNameValue(pName) + local value = 0 + for index = 1, #pName do + local cValue = string.byte(string.sub(pName, index, index)) + local reverseIndex = #pName - index + 1 + if #pName%2 == 1 then + reverseIndex = reverseIndex - 1 + end + if reverseIndex%4 >= 2 then + cValue = -cValue + end + value = value + cValue + end + return value + end + + function Util.ComputeChatColor(pName) + return CHAT_COLORS[(GetNameValue(pName) % #CHAT_COLORS) + 1] + end + + -- This is a memo-izing function + local testLabel = Instance.new('TextLabel') + testLabel.TextWrapped = true; + testLabel.Position = UDim2.new(1,0,1,0) + testLabel.Parent = GuiRoot -- Note: We have to parent it to check TextBounds + -- The TextSizeCache table looks like this Text->Font->sizeBounds->FontSize + local TextSizeCache = {} + function Util.GetStringTextBounds(text, font, fontSize, sizeBounds) + -- If no sizeBounds are specified use some huge number + sizeBounds = sizeBounds or false + if not TextSizeCache[text] then + TextSizeCache[text] = {} + end + if not TextSizeCache[text][font] then + TextSizeCache[text][font] = {} + end + if not TextSizeCache[text][font][sizeBounds] then + TextSizeCache[text][font][sizeBounds] = {} + end + if not TextSizeCache[text][font][sizeBounds][fontSize] then + testLabel.Text = text + testLabel.Font = font + testLabel.FontSize = fontSize + if sizeBounds then + testLabel.TextWrapped = true; + testLabel.Size = sizeBounds + else + testLabel.TextWrapped = false; + end + TextSizeCache[text][font][sizeBounds][fontSize] = testLabel.TextBounds + end + return TextSizeCache[text][font][sizeBounds][fontSize] + end + + local PRINTABLE_CHARS = '[^' .. string.char(32) .. '-' .. string.char(126) .. ']' + local WHITESPACE_CHARS = '(' .. string.rep('%s', 7) .. ')%s+' + function Util.FilterUnprintableCharacters(str) + if not GetLuaChatFilteringFlag() then + return str + end + + local result = str:gsub(PRINTABLE_CHARS, ''); + result = str:gsub(WHITESPACE_CHARS, '%1'); + return result + end +end + +local SelectChatModeEvent = Util.Signal() +local SelectPlayerEvent = Util.Signal() + +local function CreateChatMessage() + local this = {} + this.FadeRoutines = {} + + function this:GetMessageFontSize(settings) + return Util.IsSmallScreenSize() and settings.SmallScreenFontSize or settings.FontSize + end + + function this:OnResize() + -- Nothing! + end + + function this:FadeIn() + local gui = this:GetGui() + if gui then + gui.Visible = true + end + end + + function this:FadeOut() + local gui = this:GetGui() + if gui then + gui.Visible = false + end + end + + function this:GetGui() + return this.Container + end + + function this:Destroy() + if this.Container ~= nil then + this.Container:Destroy() + this.Container = nil + end + if this.FadeRoutines then + for _, routine in pairs(this.FadeRoutines) do + routine:Cancel() + end + this.FadeRoutines = {} + end + end + + return this +end + +local function CreateSystemChatMessage(settings, chattedMessage) + local this = CreateChatMessage() + + this.Settings = settings + this.rawChatString = chattedMessage + + function this:OnResize(containerSize) + if this.Container and this.ChatMessage then + + if InputService.VREnabled then + this.ChatMessage.Position = UDim2.new(0, 4, 0, 0) + this.ChatMessage.Size = UDim2.new(1, 0, 1, 0) + end + + this.Container.Size = UDim2.new(1,0,0,1000) + local textHeight = this.ChatMessage.TextBounds.Y + + local newContainerHeight = textHeight + 5 + this.Container.Size = UDim2.new(1,0,0,newContainerHeight) + return newContainerHeight + end + end + + function this:FadeIn() + local gui = this:GetGui() + if gui then + gui.Visible = true + for _, routine in pairs(this.FadeRoutines) do + routine:Cancel() + end + this.FadeRoutines = {} + local tweenableObjects = { + this.ChatMessage; + } + for _, object in pairs(tweenableObjects) do + object.TextTransparency = 0; + object.TextStrokeTransparency = this.Settings.TextStrokeTransparency; + end + + if this.MessageBackgroundImage then + this.MessageBackgroundImage.Visible = InputService.VREnabled + end + end + end + + function this:FadeOut(instant) + local gui = this:GetGui() + if gui then + if instant then + gui.Visible = false + else + local tweenableObjects = { + this.ChatMessage; + } + for _, object in pairs(tweenableObjects) do + table.insert(this.FadeRoutines, Util.PropertyTweener(object, 'TextTransparency', object.TextTransparency, 1, 1, Util.Linear)) + table.insert(this.FadeRoutines, Util.PropertyTweener(object, 'TextStrokeTransparency', object.TextStrokeTransparency, 1, 0.85, Util.Linear)) + end + end + if this.MessageBackgroundImage then + this.MessageBackgroundImage.Visible = false + end + end + end + + local function CreateMessageGuiElement() + local fontSize = this:GetMessageFontSize(this.Settings) + + local systemMessageDisplayText = this.rawChatString or "" + local systemMessageSize = Util.GetStringTextBounds(systemMessageDisplayText, this.Settings.Font, fontSize, UDim2.new(0, 400, 0, 1000)) + + local container = Util.Create'Frame' + { + Name = 'MessageContainer'; + Position = UDim2.new(0, 0, 0, 0); + ZIndex = 1; + BackgroundColor3 = Color3.new(0, 0, 0); + BackgroundTransparency = 1; + }; + this.MessageBackgroundImage = Util.Create'ImageLabel' + { + Name = 'TextEntryBackground'; + Size = UDim2.new(1,0,1,-2); + Position = UDim2.new(0,0,0,1); + Image = 'rbxasset://textures/ui/Chat/VRChatBackground.png'; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(8,8,56,56); + BackgroundTransparency = 1; + ImageTransparency = 0.3; + BorderSizePixel = 0; + ZIndex = 1; + Visible = InputService.VREnabled; + Parent = container; + } + + local chatMessage = Util.Create'TextLabel' + { + Name = 'SystemChatMessage'; + Position = UDim2.new(0, 0, 0, 0); + Size = UDim2.new(1, 0, 1, 0); + Text = systemMessageDisplayText; + ZIndex = 1; + BackgroundColor3 = Color3.new(0, 0, 0); + BackgroundTransparency = 1; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + TextWrapped = true; + TextColor3 = this.Settings.DefaultMessageTextColor; + FontSize = fontSize; + Font = this.Settings.Font; + TextStrokeColor3 = this.Settings.TextStrokeColor; + TextStrokeTransparency = this.Settings.TextStrokeTransparency; + Parent = container; + }; + if InputService.VREnabled then + chatMessage.Position = UDim2.new(0, 4, 0, 0) + chatMessage.Size = UDim2.new(1, 0, 1, 0) + end + + container.Size = UDim2.new(1, 0, 0, systemMessageSize.Y + 1); + this.Container = container + this.ChatMessage = chatMessage + end + + CreateMessageGuiElement() + + return this +end + +--[[ Popup Handling ]]-- +function createPopupFrame(selectedPlayer, selectedButton) + if selectedPlayer and selectedPlayer.Parent == PlayersService then + if lastSelectedButton ~= selectedButton then + if lastSelectedButton ~= nil then + lastSelectedButton.BackgroundTransparency = 1 + lastSelectedButton = nil + end + lastSelectedButton = selectedButton + lastSelectedPlayer = selectedPlayer + selectedButton.BackgroundTransparency = 0.5 + else + lastSelectedPlayer = nil + end + end +end + +function popupHidden() + if lastSelectedButton then + lastSelectedPlayer = nil + lastSelectedButton.BackgroundTransparency = 1 + lastSelectedButton = nil + end +end + +InputService.InputBegan:connect(function(inputObject, isProcessed) + if isProcessed then return end + local inputType = inputObject.UserInputType + if ((inputType == Enum.UserInputType.Touch and + inputObject.UserInputState == Enum.UserInputState.Begin) or + inputType == Enum.UserInputType.MouseButton1) then + end + end) + +--[[ End of popup handling ]]-- + +local function CreatePlayerChatMessage(settings, playerChatType, sendingPlayer, chattedMessage, receivingPlayer) + local this = CreateChatMessage() + + this.Settings = settings + this.PlayerChatType = playerChatType + this.SendingPlayer = sendingPlayer + this.RawMessageContent = chattedMessage + this.ReceivingPlayer = receivingPlayer + this.ReceivedTime = tick() + + this.Neutral = this.SendingPlayer and this.SendingPlayer.Neutral or true + this.TeamColor = this.SendingPlayer and this.SendingPlayer.TeamColor or BrickColor.new("White") + + function this:OnResize(containerSize) + if this.Container and this.ChatMessage then + this.Container.Size = UDim2.new(1,0,0,1000) + local textHeight = this.ChatMessage.TextBounds.Y + local newContainerHeight = textHeight + 5 + this.Container.Size = UDim2.new(1,0,0,newContainerHeight) + return newContainerHeight + end + end + + function this:FormatMessage() + local result = "" + if this.RawMessageContent then + local message = this.RawMessageContent + result = message + end + return result + end + + function this:FormatChatType() + if this.PlayerChatType then + if this.PlayerChatType == Enum.PlayerChatType.All then + --return "[All]" + elseif this.PlayerChatType == Enum.PlayerChatType.Team then + return "[Team]" + elseif this.PlayerChatType == Enum.PlayerChatType.Whisper then + -- nothing! + end + end + end + + function this:FormatPlayerNameText() + local playerName = "" + -- If we are sending a whisper to someone, then we should show their name + if this.PlayerChatType == Enum.PlayerChatType.Whisper and this.SendingPlayer and this.SendingPlayer == Player then + playerName = (this.ReceivingPlayer and this.ReceivingPlayer.Name or "") + else + playerName = (this.SendingPlayer and this.SendingPlayer.Name or "") + end + return "[" .. playerName .. "]:" + end + + function this:FadeIn() + local gui = this:GetGui() + if gui then + gui.Visible = true + for _, routine in pairs(this.FadeRoutines) do + routine:Cancel() + end + this.FadeRoutines = {} + local tweenableObjects = { + this.WhisperToText; + this.WhisperFromText; + this.ChatModeButton; + this.UserNameButton; + this.ChatMessage; + } + for _, object in pairs(tweenableObjects) do + object.TextTransparency = 0; + object.TextStrokeTransparency = this.Settings.TextStrokeTransparency; + object.Active = true + end + if this.UserNameDot then + this.UserNameDot.ImageTransparency = 0 + end + + if this.MessageBackgroundImage then + this.MessageBackgroundImage.Visible = InputService.VREnabled + end + end + end + + function this:FadeOut(instant) + local gui = this:GetGui() + if gui then + if instant then + gui.Visible = false + else + local tweenableObjects = { + this.WhisperToText; + this.WhisperFromText; + this.ChatModeButton; + this.UserNameButton; + this.ChatMessage; + } + for _, object in pairs(tweenableObjects) do + table.insert(this.FadeRoutines, Util.PropertyTweener(object, 'TextTransparency', object.TextTransparency, 1, 1, Util.Linear)) + table.insert(this.FadeRoutines, Util.PropertyTweener(object, 'TextStrokeTransparency', object.TextStrokeTransparency, 1, 0.85, Util.Linear)) + object.Active = false + end + if this.UserNameDot then + table.insert(this.FadeRoutines, Util.PropertyTweener(this.UserNameDot, 'ImageTransparency', this.UserNameDot.ImageTransparency, 1, 1, Util.Linear)) + end + end + if this.MessageBackgroundImage then + this.MessageBackgroundImage.Visible = false + end + end + end + + function this:Destroy() + if this.Container ~= nil then + this.Container:Destroy() + this.Container = nil + end + this.ClickedOnModeConn = Util.DisconnectEvent(this.ClickedOnModeConn) + this.ClickedOnPlayerConn = Util.DisconnectEvent(this.ClickedOnPlayerConn) + end + + local function CreateMessageGuiElement() + local fontSize = this:GetMessageFontSize(this.Settings) + + local toMesasgeDisplayText = "To " + local toMessageSize = Util.GetStringTextBounds(toMesasgeDisplayText, this.Settings.Font, fontSize) + local fromMesasgeDisplayText = "From " + local fromMessageSize = Util.GetStringTextBounds(fromMesasgeDisplayText, this.Settings.Font, fontSize) + local chatTypeDisplayText = this:FormatChatType() + local chatTypeSize = chatTypeDisplayText and Util.GetStringTextBounds(chatTypeDisplayText, this.Settings.Font, fontSize) or Vector2.new(0,0) + local playerNameDisplayText = this:FormatPlayerNameText() + local playerNameSize = Util.GetStringTextBounds(playerNameDisplayText, this.Settings.Font, fontSize) + + local singleSpaceSize = Util.GetStringTextBounds(" ", this.Settings.Font, fontSize) + local numNeededSpaces = math.ceil(playerNameSize.X / singleSpaceSize.X) + 1 + local chatMessageDisplayText = string.rep(" ", numNeededSpaces) .. this:FormatMessage() + local chatMessageSize = Util.GetStringTextBounds(chatMessageDisplayText, this.Settings.Font, fontSize, UDim2.new(0, 400 - 5 - playerNameSize.X, 0, 1000)) + + + local playerColor = this.Settings.DefaultMessageTextColor + if this.SendingPlayer then + if this.PlayerChatType == Enum.PlayerChatType.Whisper then + if this.SendingPlayer == Player and this.ReceivingPlayer then + playerColor = Util.ComputeChatColor(this.ReceivingPlayer.Name) + else + playerColor = Util.ComputeChatColor(this.SendingPlayer.Name) + end + else + if this.SendingPlayer.Neutral then + playerColor = Util.ComputeChatColor(this.SendingPlayer.Name) + else + playerColor = this.SendingPlayer.TeamColor.Color + end + end + end + + local container = Util.Create'Frame' + { + Name = 'MessageContainer'; + Position = UDim2.new(0, 0, 0, 0); + ZIndex = 1; + BackgroundColor3 = Color3.new(0, 0, 0); + BackgroundTransparency = 1; + }; + this.MessageBackgroundImage = Util.Create'ImageLabel' + { + Name = 'TextEntryBackground'; + Size = UDim2.new(1,0,1,-2); + Position = UDim2.new(0,0,0,1); + Image = 'rbxasset://textures/ui/Chat/VRChatBackground.png'; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(8,8,56,56); + BackgroundTransparency = 1; + ImageTransparency = 0.3; + BorderSizePixel = 0; + ZIndex = 1; + Visible = InputService.VREnabled; + Parent = container; + } + + local xOffset = InputService.VREnabled and 4 or 0 + + if this.SendingPlayer and this.SendingPlayer == Player and this.PlayerChatType == Enum.PlayerChatType.Whisper then + local whisperToText = Util.Create'TextLabel' + { + Name = 'WhisperTo'; + Position = UDim2.new(0, 0, 0, 0); + Size = UDim2.new(0, toMessageSize.X, 0, toMessageSize.Y); + Text = toMesasgeDisplayText; + ZIndex = 1; + BackgroundColor3 = Color3.new(0, 0, 0); + BackgroundTransparency = 1; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + TextWrapped = true; + TextColor3 = this.Settings.DefaultMessageTextColor; + FontSize = fontSize; + Font = this.Settings.Font; + TextStrokeColor3 = this.Settings.TextStrokeColor; + TextStrokeTransparency = this.Settings.TextStrokeTransparency; + Parent = container; + }; + xOffset = xOffset + toMessageSize.X + this.WhisperToText = whisperToText + elseif this.SendingPlayer and this.SendingPlayer ~= Player and this.PlayerChatType == Enum.PlayerChatType.Whisper then + local whisperFromText = Util.Create'TextLabel' + { + Name = 'WhisperFromText'; + Position = UDim2.new(0, 0, 0, 0); + Size = UDim2.new(0, fromMessageSize.X, 0, fromMessageSize.Y); + Text = fromMesasgeDisplayText; + ZIndex = 1; + BackgroundColor3 = Color3.new(0, 0, 0); + BackgroundTransparency = 1; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + TextWrapped = true; + TextColor3 = this.Settings.DefaultMessageTextColor; + FontSize = fontSize; + Font = this.Settings.Font; + TextStrokeColor3 = this.Settings.TextStrokeColor; + TextStrokeTransparency = this.Settings.TextStrokeTransparency; + Parent = container; + }; + xOffset = xOffset + fromMessageSize.X + this.WhisperFromText = whisperFromText + end + if chatTypeDisplayText then + local chatModeButton = Util.Create(Util.IsTouchDevice() and 'TextLabel' or 'TextButton') + { + Name = 'ChatMode'; + BackgroundTransparency = 1; + ZIndex = 2; + Text = chatTypeDisplayText; + TextColor3 = this.Settings.DefaultMessageTextColor; + Position = UDim2.new(0, xOffset, 0, 0); + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + FontSize = fontSize; + Font = this.Settings.Font; + Size = UDim2.new(0, chatTypeSize.X, 0, chatTypeSize.Y); + TextStrokeColor3 = this.Settings.TextStrokeColor; + TextStrokeTransparency = this.Settings.TextStrokeTransparency; + Parent = container + } + if chatModeButton:IsA('TextButton') then + this.ClickedOnModeConn = chatModeButton.MouseButton1Click:connect(function() + SelectChatModeEvent:fire(this.PlayerChatType) + end) + end + if this.PlayerChatType == Enum.PlayerChatType.Team then + chatModeButton.TextColor3 = playerColor + end + xOffset = xOffset + chatTypeSize.X + 1 + this.ChatModeButton = chatModeButton + end + local userNameButton = Util.Create(Util.IsTouchDevice() and 'TextLabel' or 'TextButton') + { + Name = 'PlayerName'; + BackgroundTransparency = 1; + BackgroundColor3 = Color3.new(0, 1, 1); + BorderSizePixel = 0; + ZIndex = 2; + Text = playerNameDisplayText; + TextColor3 = playerColor; + Position = UDim2.new(0, xOffset, 0, 0); + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + FontSize = fontSize; + Font = this.Settings.Font; + Size = UDim2.new(0, playerNameSize.X, 0, playerNameSize.Y); + TextStrokeColor3 = this.Settings.TextStrokeColor; + TextStrokeTransparency = this.Settings.TextStrokeTransparency; + Parent = container + } + if userNameButton:IsA('TextButton') then + this.ClickedOnPlayerConn = userNameButton.MouseButton1Click:connect(function() + local gui = this:GetGui() + if gui and gui.Visible then + if this.PlayerChatType == Enum.PlayerChatType.Whisper and this.SendingPlayer == Player and this.ReceivingPlayer then + SelectPlayerEvent:fire(this.ReceivingPlayer) + else + SelectPlayerEvent:fire(this.SendingPlayer) + end + end + end) + + end + + local chatMessage = Util.Create'TextLabel' + { + Name = 'ChatMessage'; + Position = UDim2.new(0, xOffset, 0, 0); + Size = UDim2.new(1, -xOffset, 1, 0); + Text = chatMessageDisplayText; + ZIndex = 1; + BackgroundColor3 = Color3.new(0, 0, 0); + BackgroundTransparency = 1; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + TextWrapped = true; + TextColor3 = this.Settings.DefaultMessageTextColor; + FontSize = fontSize; + Font = this.Settings.Font; + TextStrokeColor3 = this.Settings.TextStrokeColor; + TextStrokeTransparency = this.Settings.TextStrokeTransparency; + Parent = container; + }; + if InputService.VREnabled then + chatMessage.Size = chatMessage.Size - UDim2.new(0,4,0,0) + end + -- Check if they got moderated and put up a real message instead of Label + if chatMessage.Text == 'Label' and chatMessageDisplayText ~= 'Label' then + chatMessage.Text = string.rep(" ", numNeededSpaces) .. '[Content Deleted]' + end + if this.SendingPlayer then + if PlayerPermissionsModule.IsPlayerAdminAsync(this.SendingPlayer) then + chatMessage.TextColor3 = this.Settings.AdminTextColor + elseif PlayerPermissionsModule.IsPlayerInternAsync(this.SendingPlayer) then + chatMessage.TextColor3 = this.Settings.InternTextColor + end + end + chatMessage.Size = chatMessage.Size + UDim2.new(0, 0, 0, chatMessage.TextBounds.Y); + + container.Size = UDim2.new(1, 0, 0, math.max(chatMessageSize.Y + 1, userNameButton.Size.Y.Offset + 1)); + this.Container = container + this.ChatMessage = chatMessage + this.UserNameButton = userNameButton + end + + CreateMessageGuiElement() + + return this +end + +local function CreateChatBarWidget(settings) + local this = {} + + -- MessageModes: {All, Team, Whisper} + this.MessageMode = 'All' + this.TargetWhisperPlayer = nil + this.Settings = settings + + this.WidgetVisible = false + this.FadedIn = true + + this.ChatBarGainedFocusEvent = Util.Signal() + this.ChatBarLostFocusEvent = Util.Signal() + this.ChatCommandEvent = Util.Signal() -- Signal Signatue: success, actionType, [captures] + this.ChatErrorEvent = Util.Signal() -- Signal Signatue: success, actionType, [captures] + this.ChatBarFloodEvent = Util.Signal() + + this.unfocusedAt = 0 + + local chatCoreGuiEnabled = true + + -- This function while lets string.find work case-insensitively without clobbering the case of the captures + local function nocase(s) + s = string.gsub(s, "%a", function (c) + return string.format("[%s%s]", string.lower(c), + string.upper(c)) + end) + return s + end + + this.ChatMatchingRegex = + { + [function(chatBarText) return string.find(chatBarText, nocase("^/w ") .. "(%w+_?%w+)") end] = "Whisper"; + [function(chatBarText) return string.find(chatBarText, nocase("^/whisper ") .. "(%w+_?%w+)") end] = "Whisper"; + + [function(chatBarText) return string.find(chatBarText, "^%%") end] = "Team"; + [function(chatBarText) return string.find(chatBarText, "^%(TEAM%)") end] = "Team"; + [function(chatBarText) return string.find(chatBarText, nocase("^/t")) end] = "Team"; + [function(chatBarText) return string.find(chatBarText, nocase("^/team")) end] = "Team"; + + [function(chatBarText) return string.find(chatBarText, nocase("^/a")) end] = "All"; + [function(chatBarText) return string.find(chatBarText, nocase("^/all")) end] = "All"; + [function(chatBarText) return string.find(chatBarText, nocase("^/s")) end] = "All"; + [function(chatBarText) return string.find(chatBarText, nocase("^/say")) end] = "All"; + + [function(chatBarText) return string.find(chatBarText, nocase("^/e")) end] = "Emote"; + [function(chatBarText) return string.find(chatBarText, nocase("^/emote")) end] = "Emote"; + + [function(chatBarText) return string.find(chatBarText, "^/%?") end] = "Help"; + [function(chatBarText) return string.find(chatBarText, nocase("^/help")) end] = "Help"; + + [function(chatBarText) return string.find(chatBarText, nocase("^/block ") .. "(%w+_?%w+)") end] = "Block"; + + [function(chatBarText) return string.find(chatBarText, nocase("^/unblock ") .. "(%w+_?%w+)") end] = "Unblock"; + + [function(chatBarText) return string.find(chatBarText, nocase("^/mute ") .. "(%w+_?%w+)") end] = "Mute"; + + [function(chatBarText) return string.find(chatBarText, nocase("^/unmute ") .. "(%w+_?%w+)") end] = "Unmute"; + } + + local ChatModesDict = + { + ['Whisper'] = 'Whisper'; + ['Team'] = 'Team'; + ['All'] = 'All'; + [Enum.PlayerChatType.Whisper] = 'Whisper'; + [Enum.PlayerChatType.Team] = 'Team'; + [Enum.PlayerChatType.All] = 'All'; + } + + local function TearDownEvents() + this.ClickToChatButtonConn = Util.DisconnectEvent(this.ClickToChatButtonConn) + this.ChatBarFocusLostConn = Util.DisconnectEvent(this.ChatBarFocusLostConn) + this.ChatBarLostFocusConn = Util.DisconnectEvent(this.ChatBarLostFocusConn) + this.SelectChatModeConn = Util.DisconnectEvent(this.SelectChatModeConn) + this.SelectPlayerConn = Util.DisconnectEvent(this.SelectPlayerConn) + this.FocusChatBarInputBeganConn = Util.DisconnectEvent(this.FocusChatBarInputBeganConn) + this.InputBeganConn = Util.DisconnectEvent(this.InputBeganConn) + this.ChatBarChangedConn = Util.DisconnectEvent(this.ChatBarChangedConn) + end + + local function HookUpEvents() + TearDownEvents() -- Cleanup old events + + if this.ClickToChatButton then this.ClickToChatButtonConn = this.ClickToChatButton.MouseButton1Click:connect(function() this:FocusChatBar() end) end + + if this.ChatBar then + -- Use a count to check for double backspace out of a chatmode + local count = 0 + if not Util.IsTouchDevice() then + this.FocusChatBarInputBeganConn = Util.DisconnectEvent(this.FocusChatBarInputBeganConn) + this.FocusChatBarInputBeganConn = InputService.InputBegan:connect(function(inputObj) + if inputObj.KeyCode == Enum.KeyCode.Backspace and this:GetChatBarText() == "" then + if count == 0 then + count = count + 1 + else + this:SetMessageMode('All') + end + else + count = 0 + end + end) + end + + this.ChatBarFocusLostConn = this.ChatBar.FocusLost:connect(function(...) + count = 0 + this.unfocusedAt = tick() + this.ChatBarLostFocusEvent:fire(...) + end) + this.ChatBarChangedConn = this.ChatBar.Changed:connect(function(prop) + if prop == "Text" then + this:OnChatBarTextChanged() + elseif prop == 'TextFits' or prop == 'TextBounds' or prop == 'Visible' then + this:OnChatBarBoundsChanged() + end + end) + end + + if this.ChatBarLostFocusEvent then this.ChatBarLostFocusConn = this.ChatBarLostFocusEvent:connect(function(...) this:OnChatBarFocusLost(...) end) end + + this.SelectChatModeConn = SelectChatModeEvent:connect(function(chatType) + this:SetMessageMode(chatType) + this:FocusChatBar() + end) + + this.SelectPlayerConn = SelectPlayerEvent:connect(function(chatPlayer) + this.TargetWhisperPlayer = chatPlayer + this:SetMessageMode("Whisper") + this:FocusChatBar() + end) + + this.InputBeganConn = InputService.InputBegan:connect(function(inputObject) + if inputObject.KeyCode == Enum.KeyCode.Escape then + -- Clear text when they press escape + this:SetChatBarText("") + end + end) + end + + function this:CalculateVisibility() + if this.ChatBarContainer then + local enabled = self.WidgetVisible and chatCoreGuiEnabled and not NON_CORESCRIPT_MODE + if enabled then + HookUpEvents() + else + TearDownEvents() + end + this.ChatBarContainer.Visible = enabled and self.FadedIn and (not chatBarDisabled) + end + end + + function this:ToggleVisibility(visible) + if visible ~= self.WidgetVisible then + self.WidgetVisible = visible + self:CalculateVisibility() + end + if NON_CORESCRIPT_MODE or chatBarDisabled then + this.ChatBarContainer.Visible = false + end + end + + function this:FadeIn() + self.FadedIn = true + self:CalculateVisibility() + end + + function this:FadeOut() + self.FadedIn = false + self:CalculateVisibility() + end + + function this:CoreGuiChanged(coreGuiType, enabled) + if coreGuiType == Enum.CoreGuiType.Chat or coreGuiType == Enum.CoreGuiType.All then + chatCoreGuiEnabled = enabled + self:CalculateVisibility() + end + end + + function this:IsAChatMode(mode) + return ChatModesDict[mode] ~= nil + end + + function this:ProcessChatBarModes(requireWhitespaceAfterChatMode) + local matchedAChatCommand = false + if this.ChatBar then + local chatBarText = this:SanitizeInput(this:GetChatBarText()) + for regexFunc, actionType in pairs(this.ChatMatchingRegex) do + local start, finish, capture = regexFunc(chatBarText) + if start and finish then + -- The following line is for whether or not to try setting the chatmode as-you-type + -- versus when you press enter. + local whitespaceAfterSlashCommand = string.find(string.sub(chatBarText, finish+1, finish+1), "%s") + if (not requireWhitespaceAfterChatMode and finish == #chatBarText) or whitespaceAfterSlashCommand then + if this:IsAChatMode(actionType) then + if actionType == "Whisper" then + local targetPlayer = capture and Util.GetPlayerByName(capture) + if targetPlayer then --and targetPlayer ~= Player then + this.TargetWhisperPlayer = targetPlayer + -- start from two over to eat the space or tab character after the slash command + this:SetChatBarText(string.sub(chatBarText, finish + 2)) + this:SetMessageMode(actionType) + this.ChatCommandEvent:fire(true, actionType, capture) + else + -- This is an indirect way of detecting if they used enter to close submit this chat + if not requireWhitespaceAfterChatMode then + this:SetChatBarText("") + this.ChatCommandEvent:fire(false, actionType, capture) + end + end + else + -- start from two over to eat the space or tab character after the slash command + this:SetChatBarText(string.sub(chatBarText, finish + 2)) + this:SetMessageMode(actionType) + this.ChatCommandEvent:fire(true, actionType, capture) + end + elseif actionType == "Emote" then + -- You can only emote to everyone. + this:SetMessageMode('All') + elseif not requireWhitespaceAfterChatMode then -- Some non-chat related command + if actionType == "Help" then + this:SetChatBarText("") -- Clear the chat so we don't send /? to everyone + end + this.ChatCommandEvent:fire(true, actionType, capture) + end + -- should we break here since we already matched a slash command or keep going? + matchedAChatCommand = true + end + end + end + end + return matchedAChatCommand + end + + local previousText = "" + function this:OnChatBarTextChanged() + if not Util.IsTouchDevice() then + this:ProcessChatBarModes(true) + local originalText = this:GetChatBarText() + local newText = Util.FilterUnprintableCharacters(originalText) + if newText ~= originalText then + previousText = newText + end + + local fixedText = newText + if #newText > this.Settings.MaxCharactersInMessage or originalText ~= newText then + -- This is a hack to deal with the bug that holding down a key for repeated input doesn't trigger the textChanged event + if #newText == #previousText + 1 then + fixedText = string.sub(previousText, 1, this.Settings.MaxCharactersInMessage) + else + fixedText = string.sub(newText, 1, this.Settings.MaxCharactersInMessage) + end + end + this:SetChatBarText(fixedText) + previousText = fixedText + end + end + + function this:OnChatBarBoundsChanged() + if this.ChatBarContainer and this.ChatBar then + local currSize = this.ChatBarContainer.Size + if this.ChatBar.Visible and not this.ChatBar.TextFits then + local textBounds = Util.GetStringTextBounds(this.ChatBar.Text, this.ChatBar.Font, this.ChatBar.FontSize, UDim2.new(0, this.ChatBar.AbsoluteSize.X, 0, 1000)) + if textBounds.Y <= 36 then + this.ChatBarContainer.Size = UDim2.new(currSize.X.Scale, currSize.X.Offset, currSize.Y.Scale, 58) + else --if currSize.Y.Offset <= 54 then + this.ChatBarContainer.Size = UDim2.new(currSize.X.Scale, currSize.X.Offset, currSize.Y.Scale, 76) + end + elseif this.ChatBar.Visible == false or this.ChatBar.TextBounds.Y <= 18 then + if currSize.Y.Offset ~= 40 then + this.ChatBarContainer.Size = UDim2.new(currSize.X.Scale, currSize.X.Offset, currSize.Y.Scale, 40) + end + elseif this.ChatBar.TextBounds.Y <= 36 then + this.ChatBarContainer.Size = UDim2.new(currSize.X.Scale, currSize.X.Offset, currSize.Y.Scale, 58) + end + end + end + + function this:GetChatBarText() + return this.ChatBar and this.ChatBar.Text or "" + end + + function this:SetChatBarText(newText) + if this.ChatBar and newText ~= this.ChatBar.Text then + this.ChatBar.Text = newText + end + end + + function this:GetMessageMode() + return this.MessageMode + end + + function this:SetMessageMode(newMessageMode) + newMessageMode = ChatModesDict[newMessageMode] + + local chatRecipientText = "[" .. (this.TargetWhisperPlayer and this.TargetWhisperPlayer.Name or "") .. "]" + if this.MessageMode ~= newMessageMode or (newMessageMode == 'Whisper' and this.ChatModeText and chatRecipientText ~= this.ChatModeText.Text) then + if this.ChatModeText then + this.MessageMode = newMessageMode + if newMessageMode == 'Whisper' then + local chatRecipientTextBounds = Util.GetStringTextBounds(chatRecipientText, this.ChatModeText.Font, this.ChatModeText.FontSize) + + this.ChatModeText.TextColor3 = this.Settings.WhisperTextColor + this.ChatModeText.Text = chatRecipientText + this.ChatModeText.Size = UDim2.new(0, chatRecipientTextBounds.X, 1, 0) + elseif newMessageMode == 'Team' then + local chatTeamText = '[Team]' + local chatTeamTextBounds = Util.GetStringTextBounds(chatTeamText, this.ChatModeText.Font, this.ChatModeText.FontSize) + + this.ChatModeText.TextColor3 = this.Settings.TeamTextColor + this.ChatModeText.Text = "[Team]" + this.ChatModeText.Size = UDim2.new(0, chatTeamTextBounds.X, 1, 0) + else + this.ChatModeText.Text = "" + this.ChatModeText.Size = UDim2.new(0, 0, 1, 0) + end + if this.ChatBar then + local offset = this.ChatModeText.Size.X.Offset + this.ChatModeText.Position.X.Offset + this.ChatBar.Size = UDim2.new(1, -14 - offset, 1, 0) + this.ChatBar.Position = UDim2.new(0, 7 + offset, 0, 0) + end + end + end + end + + function this:FocusChatBar() + if this.ChatBar and not chatBarDisabled then + this.ChatBar.Visible = true + this.ChatBar:CaptureFocus() + if self.ClickToChatButton then + self.ClickToChatButton.Visible = false + end + if this.ChatModeText then + this.ChatModeText.Visible = true + end + if Util.IsTouchDevice() or InputService.VREnabled then + this:SetMessageMode('All') -- Don't remember message mode on mobile devices or VR + end + -- Update chatbar properties when chatbar is focused + this:OnChatBarBoundsChanged() + if this.ChatBarContainer then + if self.ChatBarInnerBackground then + self.ChatBarInnerBackground.BackgroundTransparency = 0 + end + end + this.ChatBarGainedFocusEvent:fire() + end + end + + function this:RemoveFocus() + if self:IsFocused() then + self.ChatBar:ReleaseFocus() + end + end + + function this:IsFocused() + return self.ChatBar and self.ChatBar == InputService:GetFocusedTextBox() + end + + function this:WasFocused() + return (tick() - this.unfocusedAt) < VR_CHAT_CLICK_DEBOUNCE + end + + function this:SanitizeInput(input) + local sanitizedInput = input + -- Chomp the whitespace at the front and end of the string + -- TODO: maybe only chop off the front space if there are more than a few? + local _, _, capture = string.find(sanitizedInput, "^%s*(.*)%s*$") + sanitizedInput = capture or "" + + return sanitizedInput + end + + + local sentMessageTimeQueue = {} + function this:FloodCheck() + if not GetLuaChatFilteringFlag() then + return false + end + + while sentMessageTimeQueue[1] and tick() - sentMessageTimeQueue[1] > FLOOD_CHECK_MESSAGE_INTERVAL do + table.remove(sentMessageTimeQueue, 1) + end + if #sentMessageTimeQueue > FLOOD_CHECK_MESSAGE_COUNT then + return true + end + return false + end + + function this:OnChatBarFocusLost(enterPressed) + if self.ChatBar then + self.ChatBar.Visible = false + if enterPressed then + local didMatchSlashCommand = self:ProcessChatBarModes(false) + local cText = self:SanitizeInput(self:GetChatBarText()) + if cText ~= "" then + if self:FloodCheck() then -- and not didMatchSlashCommand then + self.ChatBarFloodEvent:fire() + else + -- For now we will let any slash command go through, NOTE: these will show up in bubble-chat + --if not didMatchSlashCommand and string.sub(cText,1,1) == "/" then + -- self.ChatCommandEvent:fire(false, "Unknown", cText) + --else + local currentMessageMode = self:GetMessageMode() + -- {All, Team, Whisper} + if currentMessageMode == 'Team' then + if Player and Player.Neutral == true then + self.ChatErrorEvent:fire("You're not on a team.") + else + pcall(function() PlayersService:TeamChat(cText) end) + end + elseif currentMessageMode == 'Whisper' then + if self.TargetWhisperPlayer then + if self.TargetWhisperPlayer == Player then + self.ChatErrorEvent:fire("You cannot send a whisper to yourself.") + else + pcall(function() PlayersService:WhisperChat(cText, self.TargetWhisperPlayer) end) + end + else + self.ChatErrorEvent:fire("Invalid whisper target.") + end + elseif currentMessageMode == 'All' then + pcall(function() PlayersService:Chat(cText) end) + else + spawn(function() error("ChatScript: Unknown Message Mode of " .. tostring(currentMessageMode)) end) + end + table.insert(sentMessageTimeQueue, tick()) + --end + self:SetChatBarText("") + end + end + end + end + if self.ClickToChatButton then + self.ClickToChatButton.Visible = true + -- Fade-back in the text so it doesn't abruptly appear + -- Normally I would like to cancel the old tween but it is so short that it doesn't matter + self.ClickToChatButton.TextTransparency = 1 + Util.PropertyTweener(self.ClickToChatButton, 'TextTransparency', 1, 0, 0.25, Util.Linear) + end + if self.ChatModeText then + self.ChatModeText.Visible = false + end + if this.ChatBarContainer then + local currSize = this.ChatBarContainer.Size + this.ChatBarContainer.Size = UDim2.new(currSize.X.Scale, currSize.X.Offset, currSize.Y.Scale, 32) + if self.ChatBarInnerBackground then + self.ChatBarInnerBackground.BackgroundTransparency = 0.5 + end + end + this.ChatBarChangedConn = Util.DisconnectEvent(this.ChatBarChangedConn) + this.FocusChatBarInputBeganConn = Util.DisconnectEvent(this.FocusChatBarInputBeganConn) + end + + local function CreateChatBar() + local chatBarContainer = Util.Create'Frame' + { + Name = 'ChatBarContainer'; + Position = UDim2.new(0, 0, 1, 0); + Size = UDim2.new(1, 0, 0, 20); + ZIndex = 1; + BackgroundColor3 = Color3.new(0, 0, 0); + BackgroundTransparency = 0.25; + BorderSizePixel = 0; + }; + chatBarContainer.BackgroundColor3 = Color3.new(31/255, 31/255, 31/255); + chatBarContainer.BackgroundTransparency = 0.5; + local chatBarInnerBackground = Util.Create'Frame' + { + Name = 'InnerBackground'; + Position = UDim2.new(0, 7, 0, 5); + Size = UDim2.new(1, -14, 1, -10); + ZIndex = 1; + BackgroundColor3 = Color3.new(209/255, 216/255, 221/255); + BackgroundTransparency = 0.5; + BorderSizePixel = 0; + }; + local clickToChatButton = Util.Create'TextButton' + { + Name = 'ClickToChat'; + Position = UDim2.new(0,9,0,0); + Size = UDim2.new(1, -9, 1, 0); + BackgroundTransparency = 1; + AutoButtonColor = false; + ZIndex = 3; + Text = 'To chat click here or press "/" key'; + TextColor3 = this.Settings.GlobalTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + Font = Enum.Font.SourceSansBold; + FontSize = Enum.FontSize.Size18; + Parent = chatBarContainer; + } + clickToChatButton.TextWrapped = true; + clickToChatButton.Position = UDim2.new(0, 7, 0, 0); + clickToChatButton.Size = UDim2.new(1, -14, 1, 0); + clickToChatButton.TextYAlignment = Enum.TextYAlignment.Center; + if Util.IsTouchDevice() then + clickToChatButton.Text = "Tap here to chat" + end + + local chatBar = Util.Create'TextBox' + { + Name = 'ChatBar'; + Position = UDim2.new(0, 9, 0, 0); + Size = UDim2.new(1, -9, 1, 0); + Text = ""; + ZIndex = 1; + BackgroundColor3 = Color3.new(0, 0, 0); + Active = false; + BackgroundTransparency = 1; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + TextColor3 = this.Settings.GlobalTextColor; + Font = Enum.Font.SourceSansBold; + FontSize = Enum.FontSize.Size18; + ClearTextOnFocus = false; + Visible = not Util.IsTouchDevice(); + Parent = chatBarContainer; + SelectionImageObject = emptySelectionImage; + } + chatBar.TextWrapped = true; + chatBar.Position = UDim2.new(0, 7, 0, 0); + chatBar.Size = UDim2.new(1, -14, 1, 0); + chatBar.TextYAlignment = Enum.TextYAlignment.Center; + chatBar.Visible = false; + + local chatModeText = Util.Create'TextButton' + { + Name = 'ChatModeText'; + Position = UDim2.new(0, 9, 0, 0); + Size = UDim2.new(1, -9, 1, 0); + AutoButtonColor = false; + BackgroundTransparency = 1; + ZIndex = 2; + Text = ''; + TextColor3 = this.Settings.WhisperTextColor; + TextXAlignment = Enum.TextXAlignment.Left; + TextYAlignment = Enum.TextYAlignment.Top; + Font = Enum.Font.SourceSansBold; + FontSize = Enum.FontSize.Size18; + Parent = chatBarContainer; + } + chatModeText.Position = UDim2.new(0, 7, 0, 0); + chatModeText.Size = UDim2.new(1, -14, 1, 0); + chatModeText.TextYAlignment = Enum.TextYAlignment.Center; + -- Create grey background for text + chatBarInnerBackground.Parent = chatBarContainer; + clickToChatButton.Parent = chatBarInnerBackground; + chatBar.Parent = chatBarInnerBackground; + chatModeText.Parent = chatBarInnerBackground; + + this.ChatBarContainer = chatBarContainer + this.ChatBarInnerBackground = chatBarInnerBackground + this.ClickToChatButton = clickToChatButton + this.ChatBar = chatBar + this.ChatModeText = chatModeText + this.ChatBarContainer.Parent = GuiRoot + + local function UpdateChatBarContainerLayout(newSize) + if chatBarContainer then + local chatbarVisible = this.ChatBar and this.ChatBar.Visible + local bubbleChatIsOn = not PlayersService.ClassicChat and PlayersService.BubbleChat + -- Phone + if newSize.X <= PHONE_SCREEN_WIDTH then + chatBarContainer.Size = UDim2.new(0.5, 0,0, chatbarVisible and 40 or 32) + if bubbleChatIsOn then + chatBarContainer.Position = UDim2.new(0, 0, 0, 2) + else + chatBarContainer.Position = UDim2.new(0, 0, 0.5, 2) + end + -- Tablet + elseif newSize.X <= TABLET_SCREEN_WIDTH then + chatBarContainer.Size = UDim2.new(0.4, 0,0, chatbarVisible and 40 or 32) + if bubbleChatIsOn then + chatBarContainer.Position = UDim2.new(0, 0, 0, 2) + else + chatBarContainer.Position = UDim2.new(0, 0, 0.3, 2) + end + -- Desktop + else + chatBarContainer.Size = UDim2.new(0.3, 0,0, chatbarVisible and 40 or 32) + if bubbleChatIsOn then + chatBarContainer.Position = UDim2.new(0, 0, 0, 2) + else + chatBarContainer.Position = UDim2.new(0,0,0.25, 2) + end + end + + if Util.IsTouchDevice() or InputService.VREnabled then + -- Hide the chatbar on mobile and in VR so they can't see it. + chatBarContainer.Position = UDim2.new(0,0,1,20); + end + end + end + + GuiRoot.Changed:connect(function(prop) + if (prop == "AbsoluteSize" and not chatRepositioned) then + UpdateChatBarContainerLayout(GuiRoot.AbsoluteSize) + end + end) + UpdateChatBarContainerLayout(GuiRoot.AbsoluteSize) + end + + + CreateChatBar() + return this +end + +local function CreateChatWindowWidget(settings) + local this = {} + this.Settings = settings + this.Chats = {} + this.BackgroundVisible = false + this.ChatsVisible = false + this.WidgetVisible = false + this.NewUnreadMessage = false + this.MessageCount = 0 + + this.MessageCountChanged = Util.Signal() + this.FadeInSignal = Util.Signal() + this.FadeOutSignal = Util.Signal() + + this.ChatWindowPagingConn = nil + + local lastMoveTime = tick() + local lastEnterTime = tick() + local lastLeaveTime = tick() + + local lastFadeOutTime = 0 + local lastFadeInTime = 0 + local lastChatActivity = 0 + + local FadeLock = false + + local chatCoreGuiEnabled = true + + local function PointInChatWindow(pt) + local point0 = this.ChatContainer.AbsolutePosition + local point1 = point0 + this.ChatContainer.AbsoluteSize + -- HACK, this is so the "ChatWindow" includes the chatbar box, TODO: refactor the fadeing code to include the chatbar + point1 = point1 + Vector2.new(0, 34) + return (point0.X <= pt.X and + point1.X >= pt.X and + point0.Y <= pt.Y and + point1.Y >= pt.Y) + end + + function this:IsHovering() + if this.ChatContainer and this.LastMousePosition and self:CalculateVisibility() then + return PointInChatWindow(this.LastMousePosition) + end + return false + end + + function this:SetFadeLock(lock) + FadeLock = lock + end + + function this:GetFadeLock() + return FadeLock + end + + function this:SetCanvasPosition(newCanvasPosition) + if this.ScrollingFrame then + local maxSize = Vector2.new(math.max(0, this.ScrollingFrame.CanvasSize.X.Offset - this.ScrollingFrame.AbsoluteWindowSize.X), + math.max(0, this.ScrollingFrame.CanvasSize.Y.Offset - this.ScrollingFrame.AbsoluteWindowSize.Y)) + this.ScrollingFrame.CanvasPosition = Vector2.new(Util.Clamp(0, maxSize.X, newCanvasPosition.X), + Util.Clamp(0, maxSize.Y, newCanvasPosition.Y)) + end + end + + function this:ScrollToBottom() + if this.ScrollingFrame then + this:SetCanvasPosition(Vector2.new(this.ScrollingFrame.CanvasPosition.X, this.ScrollingFrame.CanvasSize.Y.Offset)) + end + end + + function this:FadeIn(duration, lockFade) + if not FadeLock then + duration = duration or 0.75 + local backgroundTransparency = InputService.VREnabled and 1 or 0.5 + -- fade in + if this.BackgroundTweener then + this.BackgroundTweener:Cancel() + end + lastFadeInTime = tick() + lastChatActivity = tick() + this.ScrollingFrame.ScrollingEnabled = true + this.ScrollingFrame.ScrollBarThickness = SCROLLBAR_THICKNESS + this.BackgroundTweener = Util.PropertyTweener(this.ChatContainer, 'BackgroundTransparency', this.ChatContainer.BackgroundTransparency, backgroundTransparency, duration, Util.Linear) + this.BackgroundVisible = true + this:FadeInChats() + + this.ChatWindowPagingConn = Util.DisconnectEvent(this.ChatWindowPagingConn) + this.ChatWindowPagingConn = InputService.InputBegan:connect(function(inputObject) + local key = inputObject.KeyCode + if key == Enum.KeyCode.PageUp then + this:SetCanvasPosition(this.ScrollingFrame.CanvasPosition - Vector2.new(0, this.ScrollingFrame.AbsoluteWindowSize.Y)) + elseif key == Enum.KeyCode.PageDown then + this:SetCanvasPosition(this.ScrollingFrame.CanvasPosition + Vector2.new(0, this.ScrollingFrame.AbsoluteWindowSize.Y)) + elseif key == Enum.KeyCode.Home then + this:SetCanvasPosition(Vector2.new(0, 0)) + elseif key == Enum.KeyCode.End then + this:ScrollToBottom() + end + end) + if this.FadeInSignal then + this.FadeInSignal:fire() + end + end + end + + function this:FadeOut(duration, unlockFade) + if not FadeLock then + duration = duration or 0.75 + -- fade out + if this.BackgroundTweener then + this.BackgroundTweener:Cancel() + end + lastFadeOutTime = tick() + lastChatActivity = tick() + this.ScrollingFrame.ScrollingEnabled = false + this.ScrollingFrame.ScrollBarThickness = 0 + this.BackgroundTweener = Util.PropertyTweener(this.ChatContainer, 'BackgroundTransparency', this.ChatContainer.BackgroundTransparency, 1, duration, Util.Linear) + this.BackgroundVisible = false + + this.ChatWindowPagingConn = Util.DisconnectEvent(this.ChatWindowPagingConn) + if this.FadeOutSignal then + this.FadeOutSignal:fire() + end + end + end + + function this:FadeInChats() + if this.ChatsVisible == true then return end + this.ChatsVisible = true + for index, message in pairs(this.Chats) do + message:FadeIn() + end + end + + function this:FadeOutChats() + if InputService.VREnabled then return end + if this.ChatsVisible == false then return end + this.ChatsVisible = false + for index, message in pairs(this.Chats) do + local messageGui = message:GetGui() + local instant = false + if messageGui and this.ScrollingFrame then + -- If the chat is not in the visible frame then don't waste cpu cycles fading it out + if (messageGui.AbsolutePosition.Y > (this.ScrollingFrame.AbsolutePosition + this.ScrollingFrame.AbsoluteWindowSize).Y or + messageGui.AbsolutePosition.Y + messageGui.AbsoluteSize.Y < this.ScrollingFrame.AbsolutePosition.Y) then + instant = true + end + end + message:FadeOut(instant) + end + end + + local ResizeCount = 0 + function this:OnResize() + ResizeCount = ResizeCount + 1 + local currentResizeCount = ResizeCount + local isScrolledDown = this:IsScrolledDown() + -- Unfortunately there is a race condition so we need this wait here. + wait() + if this.ScrollingFrame then + if currentResizeCount ~= ResizeCount then return end + local scrollingFrameAbsoluteSize = this.ScrollingFrame.AbsoluteWindowSize + if scrollingFrameAbsoluteSize ~= nil and scrollingFrameAbsoluteSize.X > 0 and scrollingFrameAbsoluteSize.Y > 0 then + local ySize = 0 + + if this.ScrollingFrame then + for _, message in pairs(this.Chats) do + local newHeight = message:OnResize(scrollingFrameAbsoluteSize) + if newHeight then + local chatMessageElement = message:GetGui() + if chatMessageElement then + local chatMessageElementYSize = chatMessageElement.Size.Y.Offset + chatMessageElement.Position = UDim2.new(0, 0, 0, ySize) + ySize = ySize + chatMessageElementYSize + end + end + end + end + if this.MessageContainer and this.ScrollingFrame then + this.MessageContainer.Size = UDim2.new( + this.MessageContainer.Size.X.Scale, + this.MessageContainer.Size.X.Offset, + 0, + ySize) + this.MessageContainer.Position = UDim2.new(0, 0, 1, -this.MessageContainer.Size.Y.Offset) + this.ScrollingFrame.CanvasSize = UDim2.new(this.ScrollingFrame.CanvasSize.X.Scale, this.ScrollingFrame.CanvasSize.X.Offset, this.ScrollingFrame.CanvasSize.Y.Scale, ySize) + end + end + this:ScrollToBottom() + end + end + + function this:FilterMessage(playerChatType, sendingPlayer, chattedMessage, receivingPlayer) + if chattedMessage and string.sub(chattedMessage, 1, 1) ~= '/' then + return true + end + return false + end + + function this:PushMessageIntoQueue(chatMessage, silently) + table.insert(this.Chats, chatMessage) + + local isScrolledDown = this:IsScrolledDown() + + local chatMessageElement = chatMessage:GetGui() + + chatMessageElement.Parent = this.MessageContainer + local chatMessageHeight = chatMessage:OnResize() or 10 + local ySize = this.MessageContainer.Size.Y.Offset + local chatMessageElementYSize = UDim2.new(0, 0, 0, chatMessageHeight) + + if not silently then + this.MessageCount = this.MessageCount + 1 + end + + chatMessageElement.Position = chatMessageElement.Position + UDim2.new(0, 0, 0, ySize) + this.MessageContainer.Size = this.MessageContainer.Size + chatMessageElementYSize + this.ScrollingFrame.CanvasSize = this.ScrollingFrame.CanvasSize + chatMessageElementYSize + + if this.Settings.MaxWindowChatMessages < #this.Chats then + this:RemoveOldestMessage() + end + if isScrolledDown then + this:ScrollToBottom() + elseif not silently then + -- Raise unread message alert! + this.NewUnreadMessage = true + end + + if silently then + if this.ChatsVisible == false then + chatMessage:FadeOut(true) + end + else + this:FadeInChats() + lastChatActivity = tick() + this.MessageCountChanged:fire(this.MessageCount) + end + + -- NOTE: Sort of hacky, but if we are approaching the max 16 bit size + -- we need to rebase y back to 0 which can be done with the resize function + if ySize > (MAX_UDIM_SIZE / 2) then + self:OnResize() + end + end + + function this:AddSystemChatMessage(chattedMessage, silently) + local chatMessage = CreateSystemChatMessage(this.Settings, chattedMessage) + this:PushMessageIntoQueue(chatMessage, silently) + end + + local function checkEnum(enumItems, value) + for _, enum in pairs(enumItems) do + if enum.Value == value then + return enum + end + end + return nil + end + + -- We only need to copy the top level for the settings table + local function shallowCopy(tableToCopy) + local newTable = {} + for key, value in pairs(tableToCopy) do + newTable[key] = value + end + return newTable + end + + function this:AddDeveloperSystemChatMessage(informationTable) + local settings = shallowCopy(this.Settings) + + if informationTable["Text"] and type(informationTable["Text"]) == "string" then + if typeof(informationTable.Color) == "Color3" then + settings.DefaultMessageTextColor = informationTable.Color + end + if typeof(informationTable.Font) == "EnumItem" and informationTable.Font.EnumType == Enum.Font then + settings.Font = informationTable.Font + end + if typeof(informationTable.FontSize) == "EnumItem" and informationTable.FontSize.EnumType == Enum.FontSize then + settings.FontSize = informationTable.FontSize + end + local chatMessage = CreateSystemChatMessage(settings, informationTable["Text"]) + this:PushMessageIntoQueue(chatMessage, false) + end + end + + function this:AddChatMessage(playerChatType, sendingPlayer, chattedMessage, receivingPlayer, silently) + local fixedChattedMessage = Util.FilterUnprintableCharacters(chattedMessage) + if this:FilterMessage(playerChatType, sendingPlayer, fixedChattedMessage, receivingPlayer) then + local chatMessage = CreatePlayerChatMessage(this.Settings, playerChatType, sendingPlayer, fixedChattedMessage, receivingPlayer) + this:PushMessageIntoQueue(chatMessage, silently) + end + end + + function this:RemoveOldestMessage() + local oldestChat = this.Chats[1] + if oldestChat then + return this:RemoveChatMessage(oldestChat) + end + end + + function this:RemoveChatMessage(chatMessage) + if chatMessage then + for index, message in pairs(this.Chats) do + if chatMessage == message then + local guiObj = chatMessage:GetGui() + if guiObj then + local ySize = guiObj.Size.Y.Offset + this.ScrollingFrame.CanvasSize = this.ScrollingFrame.CanvasSize - UDim2.new(0,0,0,ySize) + -- Clamp the canvasposition + this:SetCanvasPosition(this.ScrollingFrame.CanvasPosition) + guiObj.Parent = nil + end + message:Destroy() + return table.remove(this.Chats, index) + end + end + end + end + + function this:IsScrolledDown() + if this.ScrollingFrame then + local yCanvasSize = this.ScrollingFrame.CanvasSize.Y.Offset + local yContainerSize = this.ScrollingFrame.AbsoluteWindowSize.Y + local yScrolledPosition = this.ScrollingFrame.CanvasPosition.Y + -- Check if the messages are at the bottom + return yCanvasSize < yContainerSize or + yCanvasSize - yScrolledPosition <= yContainerSize + 5 -- a little wiggle room + end + return false + end + + function this:GetMessageCount() + return this.MessageCount + end + + function this:CalculateVisibility() + return this.WidgetVisible and ((chatCoreGuiEnabled and PlayersService.ClassicChat) or NON_CORESCRIPT_MODE) + end + + function this:ToggleVisibility(visible) + if visible ~= self.WidgetVisible then + self.WidgetVisible = visible + if this.ChatContainer then + this.ChatContainer.Visible = self:CalculateVisibility() + end + end + if NON_CORESCRIPT_MODE then + this.ChatContainer.Visible = true + end + end + + function this:CoreGuiChanged(coreGuiType, enabled) + if coreGuiType == Enum.CoreGuiType.Chat or coreGuiType == Enum.CoreGuiType.All then + chatCoreGuiEnabled = enabled + if this.ChatContainer then + this.ChatContainer.Visible = self:CalculateVisibility() + end + end + end + + local function CreateChatWindow() + -- This really shouldn't be a button, but it is currently needed for VR. + local container = Util.Create 'TextButton' + { + Name = 'ChatWindowContainer'; + Size = UDim2.new(0.3, 0, 0.25, 0); + ZIndex = 1; + BackgroundColor3 = Color3.new(0, 0, 0); + BackgroundTransparency = 1; + BorderSizePixel = 0; + SelectionImageObject = emptySelectionImage; + Active = false; + Text = "" + }; + + container.BackgroundColor3 = Color3.new(31/255, 31/255, 31/255); + local scrollingFrame = Util.Create'ScrollingFrame' + { + Name = 'ChatWindow'; + Size = UDim2.new(1, -4 - 10, 1, -20); + CanvasSize = UDim2.new(1, -4 - 10, 0, 0); + Position = UDim2.new(0, 10, 0, 10); + ZIndex = 1; + BackgroundColor3 = Color3.new(0, 0, 0); + BackgroundTransparency = 1; + BottomImage = "rbxasset://textures/ui/scroll-bottom.png"; + MidImage = "rbxasset://textures/ui/scroll-middle.png"; + TopImage = "rbxasset://textures/ui/scroll-top.png"; + ScrollBarThickness = 0; + BorderSizePixel = 0; + ScrollingEnabled = false; + Parent = container; + }; + local messageContainer = Util.Create'Frame' + { + Name = 'MessageContainer'; + Size = UDim2.new(1, -SCROLLBAR_THICKNESS - 1, 0, 0); + Position = UDim2.new(0, 0, 1, 0); + ZIndex = 1; + BackgroundColor3 = Color3.new(0, 0, 0); + BackgroundTransparency = 1; + Parent = scrollingFrame + }; + + local function OnChatWindowResize(prop) + if prop == 'AbsoluteSize' then + messageContainer.Position = UDim2.new(0, 0, 1, -messageContainer.Size.Y.Offset) + end + if prop == 'CanvasPosition' then + if this.ScrollingFrame then + if this:IsScrolledDown() then + this.NewUnreadMessage = false + end + end + end + end + + container.Changed:connect(function(prop) + if prop == 'AbsoluteSize' then + this:OnResize() + end + end) + + local function UpdateChatWindowLayout(newSize) + -- A function to position the chat window in light of various factors + -- (platform, container window size, presence of performance stats). + if container == nil then + return + end + + -- Account for presence/absence of performance stats buttons. + local localPlayer = PlayersService.LocalPlayer + local isPerformanceStatsVisible = (GameSettings.PerformanceStatsVisible and localPlayer ~= nil) + local yOffset = CHAT_WINDOW_Y_OFFSET + if isPerformanceStatsVisible then + yOffset = yOffset + StatsUtils.ButtonHeight + end + container.Position = UDim2.new(0, 0, 0, yOffset); + + -- Account for new screen size, if applicable. + if (newSize == nil) then + return + end + + if InputService.VREnabled then + container.Size = UDim2.new(1,0,1,0) + -- Phone + elseif newSize.X <= 640 then + container.Size = UDim2.new(0.5,0,0.5,0) - container.Position + -- Tablet + elseif newSize.X <= 1024 then + container.Size = UDim2.new(0.4,0,0.3,0) - container.Position + -- Desktop + else + container.Size = UDim2.new(0.3,0,0.25,0) - container.Position + end + end + + -- When quick profiler button row visiblity changes, update position of chat window. + GameSettings.PerformanceStatsVisibleChanged:connect(function() + if not chatRepositioned then + UpdateChatWindowLayout(nil) + end + end) + + GuiRoot.Changed:connect(function(prop) + if (prop == "AbsoluteSize" and not chatRepositioned) then + UpdateChatWindowLayout(GuiRoot.AbsoluteSize) + end + end) + + UpdateChatWindowLayout() + + messageContainer.Changed:connect(OnChatWindowResize) + scrollingFrame.Changed:connect(OnChatWindowResize) + + this.ChatContainer = container + this.ScrollingFrame = scrollingFrame + this.MessageContainer = messageContainer + this.ChatContainer.Parent = GuiRoot + + + -- It is important to set this to true in NON_CORESCRIPT_MODE because normally the topbar sets + -- the chat window to visible + if NON_CORESCRIPT_MODE then + this:ToggleVisibility(true) + end + + --- BACKGROUND FADING CODE --- + -- This is so we don't accidentally fade out when we are scrolling and mess with the scrollbar. + local dontFadeOutOnMouseLeave = false + + if Util:IsTouchDevice() then + local touchCount = 0 + this.InputBeganConn = InputService.InputBegan:connect(function(inputObject) + if inputObject.UserInputType == Enum.UserInputType.Touch and inputObject.UserInputState == Enum.UserInputState.Begin then + if PointInChatWindow(Vector2.new(inputObject.Position.X, inputObject.Position.Y)) then + touchCount = touchCount + 1 + dontFadeOutOnMouseLeave = true + end + end + end) + + this.InputEndedConn = InputService.InputEnded:connect(function(inputObject) + if inputObject.UserInputType == Enum.UserInputType.Touch and inputObject.UserInputState == Enum.UserInputState.End then + local endedCount = touchCount + wait(2) + if touchCount == endedCount then + dontFadeOutOnMouseLeave = false + end + end + end) + + spawn(function() + local now = tick() + while true do + wait() + now = tick() + if this.BackgroundVisible then + if not dontFadeOutOnMouseLeave then + this:FadeOut(0.25) + end + -- If background is not visible/in-focus + elseif this.ChatsVisible and now > lastChatActivity + MESSAGES_FADE_OUT_TIME then + this:FadeOutChats() + end + end + end) + else + this.LastMousePosition = Vector2.new() + + this.MouseEnterFrameConn = this.ChatContainer.MouseEnter:connect(function() + lastEnterTime = tick() + if this.BackgroundTweener and not this.BackgroundTweener:IsFinished() and not this.BackgroundVisible then + this:FadeIn() + end + end) + + this.MouseMoveConn = InputService.InputChanged:connect(function(inputObject) + if inputObject.UserInputType == Enum.UserInputType.MouseMovement then + lastMoveTime = tick() + this.LastMousePosition = Vector2.new(inputObject.Position.X, inputObject.Position.Y) + if this.BackgroundTweener and this.BackgroundTweener:GetPercentComplete() < 0.5 and this.BackgroundVisible then + if not dontFadeOutOnMouseLeave then + this:FadeOut() + end + end + end + end) + + local clickCount = 0 + this.InputBeganConn = InputService.InputBegan:connect(function(inputObject) + if inputObject.UserInputType == Enum.UserInputType.MouseButton1 and inputObject.UserInputState == Enum.UserInputState.Begin then + if PointInChatWindow(Vector2.new(inputObject.Position.X, inputObject.Position.Y)) then + clickCount = clickCount + 1 + dontFadeOutOnMouseLeave = true + end + end + end) + + this.InputEndedConn = InputService.InputEnded:connect(function(inputObject) + if inputObject.UserInputType == Enum.UserInputType.MouseButton1 and inputObject.UserInputState == Enum.UserInputState.End then + local nowCount = clickCount + wait(1.3) + if nowCount == clickCount then + dontFadeOutOnMouseLeave = false + end + end + end) + + this.MouseLeaveFrameConn = this.ChatContainer.MouseLeave:connect(function() + lastLeaveTime = tick() + if this.BackgroundTweener and not this.BackgroundTweener:IsFinished() and this.BackgroundVisible then + if not dontFadeOutOnMouseLeave then + this:FadeOut() + end + end + end) + + spawn(function() + while true do + wait() + local now = tick() + if this:IsHovering() then + if now - lastMoveTime > 1.3 and not this.BackgroundVisible then + this:FadeIn() + end + else -- not this:IsHovering() + if this.BackgroundVisible then + if not dontFadeOutOnMouseLeave then + this:FadeOut(0.25) + end + -- If background is not visible/in-focus + elseif this.ChatsVisible and now > lastChatActivity + MESSAGES_FADE_OUT_TIME then + this:FadeOutChats() + end + end + end + end) + end + --- END OF BACKGROUND FADING CODE --- + end + + CreateChatWindow() + + return this +end + + +local function CreateChat() + local this = {} + + this.Settings = + { + GlobalTextColor = Color3.new(112/255, 110/255, 106/255); + WhisperTextColor = Color3.new(77/255, 139/255, 255/255); + TeamTextColor = Color3.new(230/255, 207/255, 0); + DefaultMessageTextColor = Color3.new(255/255, 255/255, 243/255); + AdminTextColor = Color3.new(1, 215/255, 0); + InternTextColor = Color3.new(175/255, 221/255, 1); + TextStrokeTransparency = 0.75; + TextStrokeColor = Color3.new(34/255,34/255,34/255); + Font = Enum.Font.SourceSansBold; + SmallScreenFontSize = Enum.FontSize.Size14; + FontSize = Enum.FontSize.Size18; + MaxWindowChatMessages = 50; + MaxCharactersInMessage = 140; + } + + this.CurrentWindowMessageCountChanged = nil + this.VisibilityStateChanged = Util.Signal() + this.ChatBarFocusChanged = Util.Signal() + this.Visible = false + + function this:CoreGuiChanged(coreGuiType, enabled) + enabled = enabled and (topbarEnabled or InputService.VREnabled) + if coreGuiType == Enum.CoreGuiType.Chat or coreGuiType == Enum.CoreGuiType.All then + if enabled then + pcall(function() + self.SpecialKeyPressedConn = Util.DisconnectEvent(self.SpecialKeyPressedConn) + GuiService:AddSpecialKey(Enum.SpecialKey.ChatHotkey) + self.SpecialKeyPressedConn = GuiService.SpecialKeyPressed:connect(function(key) + if key == Enum.SpecialKey.ChatHotkey then + if self.Visible == false then + self:ToggleVisibility() + end + if self.ChatBarWidget then + self.ChatBarWidget:FocusChatBar() + end + end + end) + end) + else + pcall(function() GuiService:RemoveSpecialKey(Enum.SpecialKey.ChatHotkey) end) + self.SpecialKeyPressedConn = Util.DisconnectEvent(self.SpecialKeyPressedConn) + end + if this.MobileChatButton then + if enabled == true then + this.MobileChatButton.Parent = GuiRoot + -- we need to set it to be visible in-case we missed a lost focus event while chat was turned off. + this.MobileChatButton.Visible = true + else + this.MobileChatButton.Parent = nil + end + end + end + if this.ChatWindowWidget then + this.ChatWindowWidget:CoreGuiChanged(coreGuiType, enabled) + end + if this.ChatBarWidget then + this.ChatBarWidget:CoreGuiChanged(coreGuiType, enabled) + end + end + + -- This event has 4 callback arguments + -- Enum.PlayerChatType.{All|Team|Whisper}, chatPlayer, message, targetPlayer + function this:OnPlayerChatted(playerChatType, sendingPlayer, chattedMessage, receivingPlayer) + if this.ChatWindowWidget then + -- Don't add messages from blocked players, don't show message if is a debug command + local isDebugCommand = false + pcall(function() + if not NON_CORESCRIPT_MODE and sendingPlayer == PlayersService.LocalPlayer then + isDebugCommand = game:GetService("GuiService"):ShowStatsBasedOnInputString(chattedMessage) + + -- allows dev console to be opened on mobile + -- NOTE: Removed ToggleDevConsole bindable event, so engine no longer handles this + if string.lower(chattedMessage) == "/console" then + if FFlagEnableNewDevConsole then + local devConsoleMaster = require(RobloxGui.Modules.DevConsoleMaster) + if devConsoleMaster then + devConsoleMaster.ToggleVisibility() + end + else + local devConsoleModule = require(RobloxGui.Modules.DeveloperConsoleModule) + if devConsoleModule then + local devConsoleVisible = devConsoleModule:GetVisibility() + devConsoleModule:SetVisibility(not devConsoleVisible) + end + end + elseif string.lower(chattedMessage) == "/newconsole" then + local devConsoleMaster = require(RobloxGui.Modules.DevConsoleMaster) + if devConsoleMaster then + devConsoleMaster.ToggleVisibility() + end + end + end + end) + if not (this:IsPlayerBlocked(sendingPlayer) or this:IsPlayerMuted(sendingPlayer) or isDebugCommand) then + this.ChatWindowWidget:AddChatMessage(playerChatType, sendingPlayer, chattedMessage, receivingPlayer) + end + end + end + + function this:OnPlayerAdded(newPlayer) + if newPlayer then + assert(coroutine.resume(coroutine.create(function() PlayerPermissionsModule.IsPlayerAdminAsync(newPlayer) end))) + end + if NON_CORESCRIPT_MODE then + newPlayer.Chatted:connect(function(msg, recipient) + this:OnPlayerChatted(Enum.PlayerChatType.All, newPlayer, msg, recipient) + end) + else + this.PlayerChattedConn = Util.DisconnectEvent(this.PlayerChattedConn) + this.PlayerChattedConn = PlayersService.PlayerChatted:connect(function(...) + this:OnPlayerChatted(...) + end) + end + end + + function this:IsPlayerBlocked(player) + if blockingUtility then + return player and blockingUtility:IsPlayerBlockedByUserId(player.UserId) + else + return false + end + end + + function this:BlockPlayerAsync(playerToBlock) + if playerToBlock and Player ~= playerToBlock then + local blockUserId = playerToBlock.UserId + local playerToBlockName = playerToBlock.Name + if blockUserId > 0 then + if not this:IsPlayerBlocked(playerToBlock) then + if blockingUtility then + blockingUtility:BlockPlayerAsync(playerToBlock) + this.ChatWindowWidget:AddSystemChatMessage(playerToBlockName .. " is now blocked.") + end + else + this.ChatWindowWidget:AddSystemChatMessage(playerToBlockName .. " is already blocked.") + end + else + this.ChatWindowWidget:AddSystemChatMessage("You cannot block guests.") + end + else + this.ChatWindowWidget:AddSystemChatMessage("You cannot block yourself.") + end + end + + function this:UnblockPlayerAsync(playerToUnblock) + if playerToUnblock then + local unblockUserId = playerToUnblock.UserId + local playerToUnblockName = playerToUnblock.Name + + if this:IsPlayerBlocked(playerToUnblock) then + if blockingUtility then + this.ChatWindowWidget:AddSystemChatMessage(playerToUnblockName .. " is no longer blocked.") + blockingUtility:UnblockPlayerAsync(playerToUnblock) + end + else + this.ChatWindowWidget:AddSystemChatMessage(playerToUnblockName .. " is not blocked.") + end + end + end + + function this:IsPlayerMuted(player) + if blockingUtility then + return player and blockingUtility:IsPlayerMutedByUserId(player.UserId) + else + return false + end + end + + function this:MutePlayer(playerToMute) + if playerToMute and playerToMute ~= Player then + if playerToMute.UserId > 0 then + if not this:IsPlayerMuted(playerToMute) then + if blockingUtility then + blockingUtility:MutePlayer(playerToMute) + this.ChatWindowWidget:AddSystemChatMessage(playerToMute.Name .. " is now muted.") + end + else + this.ChatWindowWidget:AddSystemChatMessage(playerToMute.Name .. " is already muted.") + end + else + this.ChatWindowWidget:AddSystemChatMessage("You cannot mute guests.") + end + else + this.ChatWindowWidget:AddSystemChatMessage("You cannot mute yourself.") + end + end + + function this:UnmutePlayer(playerToUnmute) + if playerToUnmute then + if this:IsPlayerMuted(playerToUnmute) then + if blockingUtility then + blockingUtility:UnmutePlayer(playerToUnmute) + this.ChatWindowWidget:AddSystemChatMessage(playerToUnmute.Name .. " is no longer muted.") + end + else + this.ChatWindowWidget:AddSystemChatMessage(playerToUnmute.Name .. " is not muted.") + end + end + end + + function this:CreateTouchDeviceChatButton() + return Util.Create'ImageButton' + { + Name = 'TouchDeviceChatButton'; + Size = UDim2.new(0, 128, 0, 32); + Position = UDim2.new(0, 88, 0, 0); + BackgroundTransparency = 1.0; + Image = 'https://www.roblox.com/asset/?id=97078724'; + }; + end + + function this:PrintWelcome() + if this.ChatWindowWidget then + if Util.IsTouchDevice() then + this.ChatWindowWidget:AddSystemChatMessage("Please press the '...' icon to chat", true) + end + this.ChatWindowWidget:AddSystemChatMessage("Please chat '/?' for a list of commands", true) + end + end + + local doOnceVRWelcome = false + function this:PrintVRWelcome() + if this.ChatWindowWidget and not doOnceVRWelcome then + if InputService.VREnabled then + this.ChatWindowWidget:AddSystemChatMessage("Press here to chat", true) + doOnceVRWelcome = true + end + end + end + + function this:PrintHelp() + if this.ChatWindowWidget then + this.ChatWindowWidget:AddSystemChatMessage("Help Menu") + this.ChatWindowWidget:AddSystemChatMessage("Chat Commands:") + this.ChatWindowWidget:AddSystemChatMessage("/w [PlayerName] or /whisper [PlayerName] - Whisper Chat") + this.ChatWindowWidget:AddSystemChatMessage("/t or /team - Team Chat") + this.ChatWindowWidget:AddSystemChatMessage("/a or /all - All Chat") + + this.ChatWindowWidget:AddSystemChatMessage("/block [PlayerName] - Block communications from Target Player") + this.ChatWindowWidget:AddSystemChatMessage("/unblock [PlayerName] - Restore communications with Target Player") + this.ChatWindowWidget:AddSystemChatMessage("/mute [PlayerName] - Mute in-game communications from Target Player") + this.ChatWindowWidget:AddSystemChatMessage("/unmute [PlayerName] - Restore in-game communications with Target Player") + end + end + + local focusCount = 0 + function this:CreateGUI() + if (FORCE_CHAT_GUI or + (Player.ChatMode == Enum.ChatMode.TextAndMenu or RunService:IsStudio()) and + game:GetService("UserInputService"):GetPlatform() ~= Enum.Platform.XBoxOne) then + if NON_CORESCRIPT_MODE then + local chatGui = Instance.new("ScreenGui") + chatGui.Name = "RobloxGui" + chatGui.Parent = Player:WaitForChild('PlayerGui') + GuiRoot.Parent = chatGui + end + + -- NOTE: eventually we will make multiple chat window frames + this.ChatWindowWidget = CreateChatWindowWidget(this.Settings) + this.ChatBarWidget = CreateChatBarWidget(this.Settings) + this.CurrentWindowMessageCountChanged = this.ChatWindowWidget.MessageCountChanged + + this.ChatWindowWidget.FadeInSignal:connect(function() + this.ChatBarWidget:FadeIn() + end) + this.ChatWindowWidget.FadeOutSignal:connect(function() + this.ChatBarWidget:FadeOut() + end) + + this.ChatWindowWidget:FadeOut(0) + this.ChatBarWidget.ChatBarGainedFocusEvent:connect(function() + focusCount = focusCount + 1 + this.ChatWindowWidget:FadeIn(0.25) + this.ChatWindowWidget:SetFadeLock(true) + this.ChatBarFocusChanged:fire(true) + end) + this.ChatBarWidget.ChatBarLostFocusEvent:connect(function() + local focusNow = focusCount + if Util:IsTouchDevice() then + delay(2, function() + if focusNow == focusCount then + this.ChatWindowWidget:SetFadeLock(false) + end + end) + else + this.ChatWindowWidget:SetFadeLock(false) + end + this.ChatBarFocusChanged:fire(false) + end) + this.ChatBarWidget.ChatBarFloodEvent:connect(function() + if this.ChatWindowWidget then + this.ChatWindowWidget:AddSystemChatMessage("Wait before sending another message.") + end + end) + + this.ChatBarWidget.ChatErrorEvent:connect(function(msg) + if msg then + this.ChatWindowWidget:AddSystemChatMessage(msg) + end + end) + + this.ChatBarWidget.ChatCommandEvent:connect(function(success, actionType, capture) + if actionType == "Help" then + this:PrintHelp() + elseif actionType == "Block" then + local blockPlayerName = capture and tostring(capture) or "" + local playerToBlock = Util.GetPlayerByName(blockPlayerName) + if playerToBlock then + spawn(function() this:BlockPlayerAsync(playerToBlock) end) + else + this.ChatWindowWidget:AddSystemChatMessage("Cannot block " .. blockPlayerName .. " because they are not in the game.") + end + elseif actionType == "Unblock" then + local unblockPlayerName = capture and tostring(capture) or "" + local playerToBlock = Util.GetPlayerByName(unblockPlayerName) + if playerToBlock then + spawn(function() this:UnblockPlayerAsync(playerToBlock) end) + else + this.ChatWindowWidget:AddSystemChatMessage("Cannot unblock " .. unblockPlayerName .. " because they are not in the game.") + end + elseif actionType == "Mute" then + local mutePlayerName = capture and tostring(capture) or "" + local playerToMute = Util.GetPlayerByName(mutePlayerName) + if playerToMute then + this:MutePlayer(playerToMute) + else + this.ChatWindowWidget:AddSystemChatMessage("Cannot mute " .. mutePlayerName .. " because they are not in the game.") + end + elseif actionType == "Unmute" then + local unmutePlayerName = capture and tostring(capture) or "" + local playerToUnmute = Util.GetPlayerByName(unmutePlayerName) + if playerToUnmute then + this:UnmutePlayer(playerToUnmute) + else + this.ChatWindowWidget:AddSystemChatMessage("Cannot unmute " .. unmutePlayerName .. " because they are not in the game.") + end + elseif actionType == "Whisper" then + if success == false then + local playerName = capture and tostring(capture) or "Unknown" + this.ChatWindowWidget:AddSystemChatMessage("Unable to Send a Whisper to Player: " .. playerName) + end + elseif actionType == "Unknown" then + if success == false then + local commandText = capture and tostring(capture) or "Unknown" + this.ChatWindowWidget:AddSystemChatMessage("Invalid Slash Command: " .. commandText) + end + end + end) + + if not NON_CORESCRIPT_MODE then + local function onVREnabled() + if InputService.VREnabled then + self.Settings.TextStrokeTransparency = 1 + self:PrintVRWelcome() + local Panel3D = require(RobloxGui.Modules.VR.Panel3D) + + local panel = Panel3D.Get(thisModuleName) + panel:LinkTo("Keyboard") + panel:SetType(Panel3D.Type.Fixed) + panel:ResizePixels(300, 125) + GuiRoot.Parent = panel:GetGUI() + + if this.ChatWindowWidget and this.ChatWindowWidget.ChatContainer then + this.ChatWindowWidget.ChatContainer.MouseButton1Click:connect(function() + if this.ChatBarWidget then + if this.ChatBarWidget:WasFocused() then + this.ChatBarWidget:RemoveFocus() + else + self:FocusChatBar() + end + end + end) + end + + function panel:CalculateTransparency() + return 0 + end + + VRHub.ModuleOpened.Event:connect(function(moduleName) + local module = VRHub:GetModule(moduleName) + if moduleName ~= thisModuleName and module.VRIsExclusive then + this:SetVisible(false) + end + end) + else + self.Settings.TextStrokeTransparency = 0.75 + GuiRoot.Parent = RobloxGui + end + end + onVREnabled() + InputService.Changed:connect(function(prop) + if prop == 'VREnabled' then + onVREnabled() + end + end) + end + end + end + + local toggleCount = 0 + local function SetVisbility(newVisibility) + this.Visible = newVisibility + if this.ChatWindowWidget then + this.ChatWindowWidget:ToggleVisibility(this.Visible) + if this.Visible then + toggleCount = toggleCount + 1 + local thisToggle = toggleCount + local thisFocusCount = focusCount + this.ChatWindowWidget:FadeIn() + this.ChatWindowWidget:SetFadeLock(true) + delay(5, function() + if thisToggle == toggleCount and thisFocusCount == focusCount then + this.ChatWindowWidget:SetFadeLock(false) + end + end) + end + end + if this.ChatBarWidget then + this.ChatBarWidget:ToggleVisibility(this.Visible) + if this.Visible then + this.ChatBarWidget:FadeIn() + end + if InputService.VREnabled and not this.Visible then + this.ChatBarWidget:RemoveFocus() + end + end + if InputService.VREnabled then + local Panel3D = require(RobloxGui.Modules.VR.Panel3D) + + local panel = Panel3D.Get(thisModuleName) + if this.Visible then + local topbarPanel = Panel3D.Get("Topbar3D") + panel.localCF = topbarPanel.localCF * CFrame.Angles(math.rad(-5), 0, 0) * CFrame.new(0, 4, 0) * CFrame.Angles(math.rad(-15), 0, 0) + panel:SetVisible(true) + panel:ForceShowUntilLookedAt() + + VRHub:FireModuleOpened(thisModuleName) + else + panel:SetVisible(this.Visible) + + VRHub:FireModuleClosed(thisModuleName) + end + end + this.VisibilityStateChanged:fire(this.Visible) + end + + function this:ToggleVisibility() + SetVisbility(not self.Visible) + end + + function this:SetVisible(visible) + SetVisbility(visible) + end + + function this:FocusChatBar() + if self.ChatBarWidget and this.Visible then + self.ChatBarWidget:FocusChatBar() + end + end + + function this:IsFocused(useWasFocused) + if not self.ChatBarWidget then return false end + return self.ChatBarWidget:IsFocused() or (useWasFocused and self.ChatBarWidget:WasFocused()) + end + + function this:GetCurrentWindowMessageCount() + if this.ChatWindowWidget then + return this.ChatWindowWidget:GetMessageCount() + end + return 0 + end + + function this:TopbarEnabledChanged(enabled) + topbarEnabled = enabled + -- Update coregui to reflect new topbar status + self:CoreGuiChanged(Enum.CoreGuiType.Chat, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Chat)) + end + + function this:Initialize() + --[[ Developer Customization API ]]-- + if not NON_CORESCRIPT_MODE then + StarterGui:RegisterSetCore("ChatMakeSystemMessage", function(informationTable) + if this.ChatWindowWidget then + this.ChatWindowWidget:AddDeveloperSystemChatMessage(informationTable) + end + end) + local function isUDim2Value(value) + return typeof(value) == "UDim2" and value or nil + end + + local function isBubbleChatOn() + return not PlayersService.ClassicChat and PlayersService.BubbleChat + end + + StarterGui:RegisterSetCore("ChatWindowPosition", function(value) + if this.ChatWindowWidget and this.ChatBarWidget then + value = isUDim2Value(value) + if value ~= nil and not isBubbleChatOn() then + chatRepositioned = true -- Prevent chat from moving back to the original position on screen resolution change + this.ChatWindowWidget.ChatContainer.Position = value + this.ChatBarWidget.ChatBarContainer.Position = value + UDim2.new(0, 0, this.ChatWindowWidget.ChatContainer.Size.Y.Scale, this.ChatWindowWidget.ChatContainer.Size.Y.Offset + 2) + end + end + end) + + StarterGui:RegisterSetCore("ChatWindowSize", function(value) + if this.ChatWindowWidget and this.ChatBarWidget then + value = isUDim2Value(value) + if value ~= nil and not isBubbleChatOn() then + chatRepositioned = true + this.ChatWindowWidget.ChatContainer.Size = value + this.ChatBarWidget.ChatBarContainer.Size = UDim2.new(this.ChatWindowWidget.ChatContainer.Size.X.Scale, this.ChatWindowWidget.ChatContainer.Size.X.Offset, this.ChatBarWidget.ChatBarContainer.Size.Y.Scale, this.ChatBarWidget.ChatBarContainer.Size.Y.Offset) + this.ChatBarWidget.ChatBarContainer.Position = this.ChatWindowWidget.ChatContainer.Position + UDim2.new(0, 0, this.ChatWindowWidget.ChatContainer.Size.Y.Scale, this.ChatWindowWidget.ChatContainer.Size.Y.Offset + 2) + end + end + end) + + StarterGui:RegisterGetCore("ChatWindowPosition", function() + if this.ChatWindowWidget then + return this.ChatWindowWidget.ChatContainer.Position + else + return nil + end + end) + + StarterGui:RegisterGetCore("ChatWindowSize", function() + if this.ChatWindowWidget then + return this.ChatWindowWidget.ChatContainer.Size + else + return nil + end + end) + + StarterGui:RegisterSetCore("ChatBarDisabled", function(value) + if this.ChatBarWidget then + if type(value) == "boolean" then + chatBarDisabled = value + if value == true then + this.ChatBarWidget:ToggleVisibility(false) + end + end + end + end) + + StarterGui:RegisterGetCore("ChatBarDisabled", function() return chatBarDisabled end) + end + + this:OnPlayerAdded(Player) + -- Upsettingly, it seems everytime a player is added, you have to redo the connection + -- NOTE: PlayerAdded only fires on the server, hence ChildAdded is used here + PlayersService.ChildAdded:connect(function(child) + if child:IsA('Player') then + this:OnPlayerAdded(child) + end + end) + this:CreateGUI() + + + this:CoreGuiChanged(Enum.CoreGuiType.Chat, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Chat)) + this.CoreGuiChangedConn = Util.DisconnectEvent(this.CoreGuiChangedConn) + pcall(function() + this.CoreGuiChangedConn = StarterGui.CoreGuiChangedSignal:connect( + function(coreGuiType,enabled) + this:CoreGuiChanged(coreGuiType, enabled) + end) + end) + + if not NON_CORESCRIPT_MODE then + this:PrintWelcome() + end + + --SetVisbility(true) + end + + return this +end + +local moduleApiTable = {} +-- Main Entry Point +do + moduleApiTable.ModuleName = thisModuleName + moduleApiTable.KeepVRTopbarOpen = true + moduleApiTable.VRIsExclusive = true + moduleApiTable.VRClosesNonExclusive = false + VRHub:RegisterModule(moduleApiTable) + + VRHub.ModuleOpened.Event:connect(function(moduleName) + if moduleName ~= thisModuleName then + local module = VRHub:GetModule(moduleName) + if module.VRIsExclusive then + moduleApiTable:SetVisible(false) + end + end + end) + + local ChatInstance = CreateChat() + ChatInstance:Initialize() + + function moduleApiTable:ToggleVisibility() + ChatInstance:ToggleVisibility() + end + + function moduleApiTable:SetVisible(visible) + ChatInstance:SetVisible(visible) + end + + function moduleApiTable:FocusChatBar() + ChatInstance:FocusChatBar() + end + + function moduleApiTable:GetVisibility() + return ChatInstance.Visible + end + + function moduleApiTable:GetMessageCount() + return ChatInstance:GetCurrentWindowMessageCount() + end + + function moduleApiTable:TopbarEnabledChanged(...) + return ChatInstance:TopbarEnabledChanged(...) + end + + function moduleApiTable:IsFocused(useWasFocused) + return ChatInstance:IsFocused(useWasFocused) + end + + function moduleApiTable:ClassicChatEnabled() + return PlayersService.ClassicChat + end + + function moduleApiTable:IsBubbleChatOnly() + return PlayersService.BubbleChat and not PlayersService.ClassicChat + end + + function moduleApiTable:IsDisabled() + return false + end + + moduleApiTable.ChatBarFocusChanged = ChatInstance.ChatBarFocusChanged + moduleApiTable.VisibilityStateChanged = ChatInstance.VisibilityStateChanged + moduleApiTable.MessagesChanged = ChatInstance.CurrentWindowMessageCountChanged + +end + +return moduleApiTable diff --git a/Client2018/content/scripts/CoreScripts/Modules/ChatSelector.lua b/Client2018/content/scripts/CoreScripts/Modules/ChatSelector.lua new file mode 100644 index 0000000..ada7efc --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/ChatSelector.lua @@ -0,0 +1,191 @@ +--[[ + // FileName: ChatSelector.lua + // Written by: Xsitsu + // Description: Code for determining which chat version to use in game. +]] + +local FORCE_IS_CONSOLE = false +local FORCE_IS_VR = false + +local CoreGuiService = game:GetService("CoreGui") +local RobloxGui = CoreGuiService:WaitForChild("RobloxGui") +local Modules = RobloxGui:WaitForChild("Modules") +local Common = Modules:WaitForChild("Common") + +local StarterGui = game:GetService("StarterGui") +local UserInputService = game:GetService("UserInputService") +local GuiService = game:GetService("GuiService") +local RunService = game:GetService("RunService") + +local Players = game:GetService("Players") + +local Util = require(RobloxGui.Modules.ChatUtil) + +local ClassicChatEnabled = Players.ClassicChat +local BubbleChatEnabled = Players.BubbleChat + +local useModule = nil + +local state = {Visible = true} +local interface = {} +do + function interface:ToggleVisibility() + if (useModule) then + useModule:ToggleVisibility() + else + state.Visible = not state.Visible + end + end + + function interface:SetVisible(visible) + if (useModule) then + useModule:SetVisible(visible) + else + state.Visible = visible + end + end + + function interface:FocusChatBar() + if (useModule) then + useModule:FocusChatBar() + end + end + + function interface:GetVisibility() + if (useModule) then + return useModule:GetVisibility() + else + return state.Visible + end + end + + function interface:GetMessageCount() + if (useModule) then + return useModule:GetMessageCount() + else + return 0 + end + end + + function interface:TopbarEnabledChanged(...) + if (useModule) then + return useModule:TopbarEnabledChanged(...) + end + end + + function interface:IsFocused(useWasFocused) + if (useModule) then + return useModule:IsFocused(useWasFocused) + else + return false + end + end + + function interface:ClassicChatEnabled() + if useModule then + return useModule:ClassicChatEnabled() + else + return ClassicChatEnabled + end + end + + function interface:IsBubbleChatOnly() + if useModule then + return useModule:IsBubbleChatOnly() + end + return BubbleChatEnabled and not ClassicChatEnabled + end + + function interface:IsDisabled() + if useModule then + return useModule:IsDisabled() + end + return not (BubbleChatEnabled or ClassicChatEnabled) + end + + interface.ChatBarFocusChanged = Util.Signal() + interface.VisibilityStateChanged = Util.Signal() + interface.MessagesChanged = Util.Signal() + + -- Signals that are called when we get information on if Bubble Chat and Classic chat are enabled from the chat. + interface.BubbleChatOnlySet = Util.Signal() + interface.ChatDisabled = Util.Signal() +end + +local StopQueueingSystemMessages = false +local MakeSystemMessageQueue = {} +local function MakeSystemMessageQueueingFunction(data) + if (StopQueueingSystemMessages) then return end + table.insert(MakeSystemMessageQueue, data) +end + +local function NonFunc() end +StarterGui:RegisterSetCore("ChatMakeSystemMessage", MakeSystemMessageQueueingFunction) +StarterGui:RegisterSetCore("ChatWindowPosition", NonFunc) +StarterGui:RegisterGetCore("ChatWindowPosition", NonFunc) +StarterGui:RegisterSetCore("ChatWindowSize", NonFunc) +StarterGui:RegisterGetCore("ChatWindowSize", NonFunc) +StarterGui:RegisterSetCore("ChatBarDisabled", NonFunc) +StarterGui:RegisterGetCore("ChatBarDisabled", NonFunc) + + +StarterGui:RegisterGetCore("ChatActive", function() + return interface:GetVisibility() +end) +StarterGui:RegisterSetCore("ChatActive", function(visible) + return interface:SetVisible(visible) +end) + + +local function ConnectSignals(useModule, interface, sigName) + --// "MessagesChanged" event is not created for Studio Start Server + if (useModule[sigName]) then + useModule[sigName]:connect(function(...) interface[sigName]:fire(...) end) + end +end + +local isConsole = GuiService:IsTenFootInterface() or FORCE_IS_CONSOLE +local isVR = UserInputService.VREnabled or FORCE_IS_VR + +if ( not isConsole and not isVR ) then + spawn(function() + useModule = require(RobloxGui.Modules.NewChat) + + ConnectSignals(useModule, interface, "ChatBarFocusChanged") + ConnectSignals(useModule, interface, "VisibilityStateChanged") + ConnectSignals(useModule, interface, "BubbleChatOnlySet") + ConnectSignals(useModule, interface, "ChatDisabled") + + while Players.LocalPlayer == nil do Players.ChildAdded:wait() end + local LocalPlayer = Players.LocalPlayer + + ConnectSignals(useModule, interface, "MessagesChanged") + -- Retained for legacy reasons, no longer used by the chat scripts. + StarterGui:RegisterGetCore("UseNewLuaChat", function() return true end) + + useModule:SetVisible(state.Visible) + StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Chat, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Chat)) + + StopQueueingSystemMessages = true + for i, messageData in pairs(MakeSystemMessageQueue) do + pcall(function() StarterGui:SetCore("ChatMakeSystemMessage", messageData) end) + end + end) +elseif not isConsole then + useModule = require(RobloxGui.Modules.Chat) + + ConnectSignals(useModule, interface, "ChatBarFocusChanged") + ConnectSignals(useModule, interface, "VisibilityStateChanged") + + while Players.LocalPlayer == nil do Players.ChildAdded:wait() end + local LocalPlayer = Players.LocalPlayer + + if (LocalPlayer.ChatMode == Enum.ChatMode.TextAndMenu or RunService:IsStudio()) then + ConnectSignals(useModule, interface, "MessagesChanged") + end + + StarterGui:RegisterGetCore("UseNewLuaChat", function() return false end) + +end + +return interface diff --git a/Client2018/content/scripts/CoreScripts/Modules/ChatUtil.lua b/Client2018/content/scripts/CoreScripts/Modules/ChatUtil.lua new file mode 100644 index 0000000..1be8dbd --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/ChatUtil.lua @@ -0,0 +1,34 @@ +local Util = {} +do + function Util.Signal() + local sig = {} + + local mSignaler = Instance.new('BindableEvent') + + local mArgData = nil + local mArgDataCount = nil + + function sig:fire(...) + mArgData = {...} + mArgDataCount = select('#', ...) + mSignaler:Fire() + end + + function sig:connect(f) + if not f then error("connect(nil)", 2) end + return mSignaler.Event:connect(function() + f(unpack(mArgData, 1, mArgDataCount)) + end) + end + + function sig:wait() + mSignaler.Event:wait() + assert(mArgData, "Missing arg data, likely due to :TweenSize/Position corrupting threadrefs.") + return unpack(mArgData, 1, mArgDataCount) + end + + return sig + end +end + +return Util \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Common/CommonUtil.lua b/Client2018/content/scripts/CoreScripts/Modules/Common/CommonUtil.lua new file mode 100644 index 0000000..73dfb12 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Common/CommonUtil.lua @@ -0,0 +1,61 @@ +--[[ + Filename: CommonUtil.lua + Written by: dbanks + Description: Common work. +--]] + + +--[[ Classes ]]-- +local CommonUtil = {} + +-- Concatenate these two tables, return result. +function CommonUtil.TableConcat(t1,t2) + for i=1,#t2 do + t1[#t1+1] = t2[i] + end + return t1 +end + +-- Instances have a "Name" field. Sort +-- by that name, +function CommonUtil.SortByName(items) + local function compareInstanceNames(i1, i2) + return (i1.Name < i2.Name) + end + table.sort(items, compareInstanceNames) + return items +end + +-- Provides a nice syntax for creating Roblox instances. +-- Example: +-- local newPart = Utility.Create("Part") { +-- Parent = workspace, +-- Anchored = true, +-- +-- --Create a SpecialMesh as a child of this part too +-- Utility.Create("SpecialMesh") { +-- MeshId = "rbxassetid://...", +-- Scale = Vector3.new(0.5, 0.2, 10) +-- } +-- } +function CommonUtil.Create(instanceType) + return function(data) + local obj = Instance.new(instanceType) + local parent = nil + for k, v in pairs(data) do + if type(k) == 'number' then + v.Parent = obj + elseif k == 'Parent' then + parent = v + else + obj[k] = v + end + end + if parent then + obj.Parent = parent + end + return obj + end +end + +return CommonUtil \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Common/ObjectPool.lua b/Client2018/content/scripts/CoreScripts/Modules/Common/ObjectPool.lua new file mode 100644 index 0000000..0834ef9 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Common/ObjectPool.lua @@ -0,0 +1,51 @@ +-- // FileName: ObjectPool.lua +-- // Written by: TheGamer101 +-- // Description: An object pool class used to avoid unnecessarily instantiating Instances. + +local module = {} +--////////////////////////////// Include +--////////////////////////////////////// +local modulesFolder = script.Parent + +--////////////////////////////// Methods +--////////////////////////////////////// +local methods = {} +methods.__index = methods + +function methods:GetInstance(className) + if self.InstancePoolsByClass[className] == nil then + self.InstancePoolsByClass[className] = {} + end + local availableInstances = #self.InstancePoolsByClass[className] + if availableInstances > 0 then + local instance = self.InstancePoolsByClass[className][availableInstances] + table.remove(self.InstancePoolsByClass[className]) + return instance + end + return Instance.new(className) +end + +function methods:ReturnInstance(instance) + if self.InstancePoolsByClass[instance.ClassName] == nil then + self.InstancePoolsByClass[instance.ClassName] = {} + end + if #self.InstancePoolsByClass[instance.ClassName] < self.PoolSizePerType then + table.insert(self.InstancePoolsByClass[instance.ClassName], instance) + else + instance:Destroy() + end +end + +--///////////////////////// Constructors +--////////////////////////////////////// + +function module.new(poolSizePerType) + local obj = setmetatable({}, methods) + obj.InstancePoolsByClass = {} + obj.Name = "ObjectPool" + obj.PoolSizePerType = poolSizePerType + + return obj +end + +return module diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Action.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Action.lua new file mode 100644 index 0000000..d3d1ff4 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Action.lua @@ -0,0 +1,68 @@ +--[[ + A helper function to define a Rodux action creator with an associated name. + + Normally when creating a Rodux action, you can just create a function: + + return function(value) + return { + type = "MyAction", + value = value, + } + end + + And then when you check for it in your reducer, you either use a constant, + or type out the string name: + + if action.type == "MyAction" then + -- change some state + end + + Typos here are a remarkably common bug. We also have the issue that there's + no link between reducers and the actions that they respond to! + + `Action` (this helper) provides a utility that makes this a bit cleaner. + + Instead, define your Rodux action like this: + + return Action("MyAction", function(value) + return { + value = value, + } + end) + + We no longer need to add the `type` field manually. + + Additionally, the returned action creator now has a 'name' property that can + be checked by your reducer: + + local MyAction = require(Reducers.MyAction) + + ... + + if action.type == MyAction.name then + -- change some state! + end + + Now we have a clear link between our reducers and the actions they use, and + if we ever typo a name, we'll get a warning in LuaCheck as well as an error + at runtime! +]] + +return function(name, fn) + assert(type(name) == "string", "A name must be provided to create an Action") + assert(type(fn) == "function", "A function must be provided to create an Action") + + return setmetatable({ + name = name, + }, { + __call = function(self, ...) + local result = fn(...) + + assert(type(result) == "table", "An action must return a table") + + result.type = name + + return result + end + }) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ActionBindingsUpdateSearchFilter.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ActionBindingsUpdateSearchFilter.lua new file mode 100644 index 0000000..dee088b --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ActionBindingsUpdateSearchFilter.lua @@ -0,0 +1,7 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("ActionBindingsUpdateSearchFilter", function(searchTerm) + return { + searchTerm = searchTerm + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ChangeDevConsoleSize.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ChangeDevConsoleSize.lua new file mode 100644 index 0000000..607885d --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ChangeDevConsoleSize.lua @@ -0,0 +1,7 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("ChangeDevConsoleSize", function(newSize) + return { + newSize = newSize + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ClientLogUpdateSearchFilter.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ClientLogUpdateSearchFilter.lua new file mode 100644 index 0000000..4cc7cdd --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ClientLogUpdateSearchFilter.lua @@ -0,0 +1,8 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("ClientLogUpdateSearchFilter", function(searchTerm, filterTypes) + return { + searchTerm = searchTerm, + filterTypes = filterTypes + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ClientMemoryUpdateSearchFilter.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ClientMemoryUpdateSearchFilter.lua new file mode 100644 index 0000000..b5f301b --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ClientMemoryUpdateSearchFilter.lua @@ -0,0 +1,8 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("ClientMemoryUpdateSearchFilter", function(searchTerm, filterTypes) + return { + searchTerm = searchTerm, + filterTypes = filterTypes + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ClientNetworkUpdateSearchFilter.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ClientNetworkUpdateSearchFilter.lua new file mode 100644 index 0000000..c7940ad --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ClientNetworkUpdateSearchFilter.lua @@ -0,0 +1,8 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("ClientNetworkUpdateSearchFilter", function(searchTerm, filterTypes) + return { + searchTerm = searchTerm, + filterTypes = filterTypes + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ClientScriptsUpdateSearchFilter.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ClientScriptsUpdateSearchFilter.lua new file mode 100644 index 0000000..547b82b --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ClientScriptsUpdateSearchFilter.lua @@ -0,0 +1,8 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("ClientScriptsUpdateSearchFilter", function(searchTerm, filterTypes) + return { + searchTerm = searchTerm, + filterTypes = filterTypes + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/DataStoresUpdateSearchFilter.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/DataStoresUpdateSearchFilter.lua new file mode 100644 index 0000000..32fb559 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/DataStoresUpdateSearchFilter.lua @@ -0,0 +1,8 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("DataStoresUpdateSearchFilter", function(searchTerm, filterTypes) + return { + searchTerm = searchTerm, + filterTypes = filterTypes + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerJobsUpdateSearchFilter.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerJobsUpdateSearchFilter.lua new file mode 100644 index 0000000..ce29ad8 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerJobsUpdateSearchFilter.lua @@ -0,0 +1,8 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("ServerJobsUpdateSearchFilter", function(searchTerm, filterTypes) + return { + searchTerm = searchTerm, + filterTypes = filterTypes + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerLogUpdateSearchFilter.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerLogUpdateSearchFilter.lua new file mode 100644 index 0000000..8db873b --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerLogUpdateSearchFilter.lua @@ -0,0 +1,8 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("ServerLogUpdateSearchFilter", function(searchTerm, filterTypes) + return { + searchTerm = searchTerm, + filterTypes = filterTypes + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerMemoryUpdateSearchFilter.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerMemoryUpdateSearchFilter.lua new file mode 100644 index 0000000..3c1fe2b --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerMemoryUpdateSearchFilter.lua @@ -0,0 +1,8 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("ServerMemoryUpdateSearchFilter", function(searchTerm, filterTypes) + return { + searchTerm = searchTerm, + filterTypes = filterTypes + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerNetworkUpdateSearchFilter.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerNetworkUpdateSearchFilter.lua new file mode 100644 index 0000000..a51a266 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerNetworkUpdateSearchFilter.lua @@ -0,0 +1,9 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("ServerNetworkUpdateSearchFilter", function(searchTerm, filterTypes) + + return { + searchTerm = searchTerm, + filterTypes = filterTypes + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerScriptsUpdateSearchFilter.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerScriptsUpdateSearchFilter.lua new file mode 100644 index 0000000..af4568a --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerScriptsUpdateSearchFilter.lua @@ -0,0 +1,8 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("ServerScriptsUpdateSearchFilter", function(searchTerm, filterTypes) + return { + searchTerm = searchTerm, + filterTypes = filterTypes + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerStatsUpdateSearchFilter.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerStatsUpdateSearchFilter.lua new file mode 100644 index 0000000..c2cc9bf --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/ServerStatsUpdateSearchFilter.lua @@ -0,0 +1,8 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("ServerStatsUpdateSearchFilter", function(searchTerm, filterTypes) + return { + searchTerm = searchTerm, + filterTypes = filterTypes + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/SetActiveTab.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/SetActiveTab.lua new file mode 100644 index 0000000..c205cf4 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/SetActiveTab.lua @@ -0,0 +1,8 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("SetActiveTab", function(tabListIndex, isClientView) + return { + newTabIndex = tabListIndex, + isClientView = isClientView + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/SetDevConsoleMinimized.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/SetDevConsoleMinimized.lua new file mode 100644 index 0000000..5c02da1 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/SetDevConsoleMinimized.lua @@ -0,0 +1,7 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("SetDevConsoleMinimized", function(minimize) + return { + isMinimized = minimize + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/SetDevConsolePosition.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/SetDevConsolePosition.lua new file mode 100644 index 0000000..ca13075 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/SetDevConsolePosition.lua @@ -0,0 +1,7 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("SetDevConsolePosition", function(pos) + return { + position = pos + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/SetDevConsoleVisibility.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/SetDevConsoleVisibility.lua new file mode 100644 index 0000000..892b306 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/SetDevConsoleVisibility.lua @@ -0,0 +1,7 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("SetDevConsoleVisibility", function(visibility) + return { + isVisible = visibility + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/UpdateAveragePing.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/UpdateAveragePing.lua new file mode 100644 index 0000000..32e5ba7 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Actions/UpdateAveragePing.lua @@ -0,0 +1,7 @@ +local Action = require(script.Parent.Parent.Action) + +return Action("UpdateAveragePing", function(newAveragePing) + return { + AveragePing = newAveragePing + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/CircularBuffer.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/CircularBuffer.lua new file mode 100644 index 0000000..7129a4b --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/CircularBuffer.lua @@ -0,0 +1,113 @@ +local CircularBuffer = {} +CircularBuffer.__index = CircularBuffer + +function CircularBuffer.new(size) + -- just dont do this + if size == 0 then + return nil + end + + local self = {} + setmetatable(self, CircularBuffer) + + self._data = {} + self._backIndex = 0 + self._maxSize = size + + return self +end + +function CircularBuffer:reset() + self._data = {} + self._backIndex = 0 +end + +function CircularBuffer:getSize() + return #self._data +end + +function CircularBuffer:setSize(newSize) + if newSize == self._maxSize or newSize == 0 then + return + end + + local sorted = self:getOrdered() + if newSize > self._maxSize then + self._data = sorted + self._backIndex = self._maxSize + else + local newDataSet = {} + for i = 1, newSize do + newDataSet[i] = sorted[i] + end + self._data = newDataSet + self._backIndex = newSize + end + + self._maxSize = newSize +end + +function CircularBuffer:getFrontIndex() + local front = self._backIndex + 1 + if not self._data[front] then + return 1 + end + return front +end + +function CircularBuffer:front() + local front = self:getFrontIndex() + return self._data[front].entry +end + +function CircularBuffer:iterator() + local front = self._data[self:getFrontIndex()] + + local iterator = { + data = front, + next = function (self) + local retVal = self.data + if retVal then + self.data = self.data._next + end + return retVal and retVal.entry + end + } + + return iterator +end + +function CircularBuffer:back() + return self._data[self._backIndex].entry +end + +function CircularBuffer:getData() + return self._data +end + +-- returns the ejected element if newData overwrites +-- the previous front element +function CircularBuffer:push_back(newData) + local currBackIndex = self._backIndex + local newBackIndex = self._backIndex + 1 + if newBackIndex > self._maxSize then + newBackIndex = 1 + end + + local overwrittenData = self._data[newBackIndex] + self._data[newBackIndex] = { + entry = newData + } + + if currBackIndex > 0 then + self._data[currBackIndex]._next = self._data[newBackIndex] + if overwrittenData then + overwrittenData._next = nil + end + end + + self._backIndex = newBackIndex + return overwrittenData and overwrittenData.entry +end + +return CircularBuffer \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ActionBindings/ActionBindingsChart.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ActionBindings/ActionBindingsChart.lua new file mode 100644 index 0000000..a9ca6fb --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ActionBindings/ActionBindingsChart.lua @@ -0,0 +1,338 @@ + +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Components = script.Parent.Parent.Parent.Components +local DataConsumer = require(Components.DataConsumer) +local HeaderButton = require(Components.HeaderButton) +local CellLabel = require(Components.CellLabel) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local GeneralFormatting = Constants.GeneralFormatting +local LINE_WIDTH = GeneralFormatting.LineWidth +local LINE_COLOR = GeneralFormatting.LineColor + +local ActionBindingsFormatting = Constants.ActionBindingsFormatting +local HEADER_NAMES = ActionBindingsFormatting.ChartHeaderNames +local CELL_WIDTHS = ActionBindingsFormatting.ChartCellWidths +local HEADER_HEIGHT = ActionBindingsFormatting.HeaderFrameHeight +local ENTRY_HEIGHT = ActionBindingsFormatting.EntryFrameHeight +local CELL_PADDING = ActionBindingsFormatting.CellPadding +local MIN_FRAME_WIDTH = ActionBindingsFormatting.MinFrameWidth + +local IS_CORE_STR = "Core" +local IS_DEVELOPER_STR = "Developer" + +-- create table of offsets and sizes for each cell +local totalCellWidth = 0 +for _, cellWidth in ipairs(CELL_WIDTHS) do + totalCellWidth = totalCellWidth + cellWidth +end + +local currOffset = -totalCellWidth +local cellOffset = {} +local headerCellSize = {} +local entryCellSize = {} + +currOffset = currOffset / 2 +table.insert(cellOffset, UDim2.new(0, CELL_PADDING, 0, 0)) +table.insert(headerCellSize, UDim2.new(.5, currOffset - CELL_PADDING, 0, HEADER_HEIGHT)) +table.insert(entryCellSize, UDim2.new(.5, currOffset - CELL_PADDING, 0, ENTRY_HEIGHT)) + +for _, cellWidth in ipairs(CELL_WIDTHS) do + table.insert(cellOffset,UDim2.new(.5, currOffset + CELL_PADDING, 0, 0)) + table.insert(headerCellSize, UDim2.new(0, cellWidth - CELL_PADDING, 0, HEADER_HEIGHT)) + table.insert(entryCellSize, UDim2.new(0, cellWidth - CELL_PADDING, 0, ENTRY_HEIGHT)) + currOffset = currOffset + cellWidth +end + +table.insert(cellOffset,UDim2.new(.5, currOffset + CELL_PADDING, 0, 0)) +table.insert(headerCellSize, UDim2.new(.5, (-totalCellWidth / 2) - CELL_PADDING, 0, HEADER_HEIGHT)) +table.insert(entryCellSize, UDim2.new(.5, (-totalCellWidth / 2) - CELL_PADDING, 0, ENTRY_HEIGHT)) + +local verticalOffsets = {} +for i, offset in ipairs(cellOffset) do + verticalOffsets[i] = UDim2.new( + offset.X.Scale, + offset.X.Offset - CELL_PADDING, + offset.Y.Scale, + offset.Y.Offset) +end + +local ActionBindingsChart = Roact.Component:extend("ActionBindingsChart") + +local function constructHeader(onSortChanged, width) + local header = {} + + for ind, name in ipairs(HEADER_NAMES) do + header[name] = Roact.createElement(HeaderButton, { + text = name, + size = headerCellSize[ind], + pos = cellOffset[ind], + sortfunction = onSortChanged, + }) + end + + header["upperHorizontalLine"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 0, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + + header["lowerHorizontalLine"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + + for ind = 2, #verticalOffsets do + local key = string.format("VerticalLine_%d",ind) + header[key] = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 1, 0), + Position = verticalOffsets[ind], + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + end + + return Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, 0, 0, HEADER_HEIGHT), + CanvasSize = UDim2.new(0, width, 0, HEADER_HEIGHT), + BackgroundTransparency = 1, + ScrollingEnabled = false, + ScrollBarThickness = 0, + }, header) +end + +local function constructEntry(entry, width, layoutOrder) + local name = entry.name + local actionInfo = entry.actionInfo + + -- the last element is special cased because the data in the + -- string is passed in as value in the table + -- use tostring to convert the enum into an actual string also because it's used twice + local enumStr = tostring(actionInfo["inputTypes"][1]) + + local isCoreString = IS_CORE_STR + if actionInfo["isCore"] then + isCoreString = IS_DEVELOPER_STR + end + + local row = {} + for i = 2,#verticalOffsets do + local key = string.format("line_%d",i) + row[key] = Roact.createElement("Frame", { + Size = UDim2.new(0,LINE_WIDTH,1,0), + Position = verticalOffsets[i], + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + end + + row[name] = Roact.createElement(CellLabel, { + text = enumStr, + size = entryCellSize[1], + pos = cellOffset[1], + }) + + row.priorityLevel = Roact.createElement(CellLabel, { + text = actionInfo["priorityLevel"], + size = entryCellSize[2], + pos = cellOffset[2], + }) + + row.isCore = Roact.createElement(CellLabel, { + text = isCoreString, + size = entryCellSize[3], + pos = cellOffset[3], + }) + + row.actionName = Roact.createElement(CellLabel, { + text = name, + size = entryCellSize[4], + pos = cellOffset[4], + }) + + row.inputTypes = Roact.createElement(CellLabel, { + text = enumStr, + size = entryCellSize[5], + pos = cellOffset[5], + }) + + row.upperHorizontalLine = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + + row.lowerHorizontalLine = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + + + return Roact.createElement("Frame", { + Size = UDim2.new(0, width, 0, ENTRY_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + },row) +end + +function ActionBindingsChart:init(props) + local initBindings = props.ActionBindingsData:getCurrentData() + + self.onSortChanged = function(sortType) + local currSortType = props.ActionBindingsData:getSortType() + if sortType == currSortType then + self:setState({ + reverseSort = not self.state.reverseSort + }) + else + props.ActionBindingsData:setSortType(sortType) + self:setState({ + reverseSort = false, + }) + end + end + + self.onCanvasPosChanged = function() + local canvasPos = self.scrollingRef.current.CanvasPosition + if self.state.canvasPos ~= canvasPos then + self:setState({ + canvasPos = canvasPos, + }) + end + end + + self.scrollingRef = Roact.createRef() + + self.state = { + actionBindingEntries = initBindings, + reverseSort = false, + } +end + +function ActionBindingsChart:willUpdate() + if self.canvasPosConnector then + self.canvasPosConnector:Disconnect() + end +end + +function ActionBindingsChart:didUpdate() + if self.scrollingRef.current then + local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition") + self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged) + + local absSize = self.scrollingRef.current.AbsoluteSize + local currAbsSize = self.state.absScrollSize + if absSize.X ~= currAbsSize.X or + absSize.Y ~= currAbsSize.Y then + self:setState({ + absScrollSize = absSize, + }) + end + end +end + +function ActionBindingsChart:didMount() + self.bindingsUpdated = self.props.ActionBindingsData:Signal():Connect(function(bindingsData) + self:setState({ + actionBindingEntries = bindingsData + }) + end) + + if self.scrollingRef.current then + local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition") + self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged) + + self:setState({ + absScrollSize = self.scrollingRef.current.AbsoluteSize, + canvasPos = self.scrollingRef.current.CanvasPosition, + }) + end +end + +function ActionBindingsChart:willUnmount() + self.bindingsUpdated:Disconnect() + self.bindingsUpdated = nil + self.canvasPosConnector:Disconnect() + self.canvasPosConnector = nil +end + +function ActionBindingsChart:render() + local entries = {} + local searchTerm = self.props.searchTerm + local size = self.props.size + local layoutOrder = self.props.layoutOrder + + local entryList = self.state.actionBindingEntries + local reverseSort = self.state.reverseSort + + local canvasPos = self.state.canvasPos + local absScrollSize = self.state.absScrollSize + local frameWidth = absScrollSize and math.max(absScrollSize.X, MIN_FRAME_WIDTH) or MIN_FRAME_WIDTH + + entries["UIListLayout"] = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + local totalEntries = #entryList + local canvasHeight = 0 + + if absScrollSize and canvasPos then + + local paddingHeight = -1 + local usedFrameSpace = 0 + + + for ind, entry in ipairs(entryList) do + if not searchTerm or string.find(entry.name:lower(), searchTerm:lower()) ~= nil then + if canvasHeight + ENTRY_HEIGHT >= canvasPos.Y then + if usedFrameSpace < absScrollSize.Y then + local entryLayoutOrder = reverseSort and (totalEntries - ind) or ind + entries[ind] = constructEntry(entry, frameWidth, entryLayoutOrder + 1) + end + if paddingHeight < 0 then + paddingHeight = canvasHeight + else + usedFrameSpace = usedFrameSpace + ENTRY_HEIGHT + end + end + + canvasHeight = canvasHeight + ENTRY_HEIGHT + end + end + + entries["WindowingPadding"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, paddingHeight), + BackgroundTransparency = 1, + LayoutOrder = 1, + }) + end + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + Header = constructHeader(self.onSortChanged, frameWidth), + MainChart = Roact.createElement("ScrollingFrame", { + Position = UDim2.new(0, 0, 0, HEADER_HEIGHT), + Size = UDim2.new(1, 0, 1, - HEADER_HEIGHT), + CanvasSize = UDim2.new(0, frameWidth, 0, canvasHeight), + ScrollBarThickness = 6, + BackgroundColor3 = Constants.Color.BaseGray, + BackgroundTransparency = 1, + + [Roact.Ref] = self.scrollingRef + }, entries), + }) +end + +return DataConsumer(ActionBindingsChart, "ActionBindingsData") \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ActionBindings/ActionBindingsChart.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ActionBindings/ActionBindingsChart.spec.lua new file mode 100644 index 0000000..ec9a91f --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ActionBindings/ActionBindingsChart.spec.lua @@ -0,0 +1,15 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local DataProvider = require(script.Parent.Parent.DataProvider) + local ActionBindingsChart = require(script.Parent.ActionBindingsChart) + + it("should create and destroy without errors", function() + local element = Roact.createElement(DataProvider, nil, { + ActionBindingsChart = Roact.createElement(ActionBindingsChart) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ActionBindings/ActionBindingsData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ActionBindings/ActionBindingsData.lua new file mode 100644 index 0000000..c20140a --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ActionBindings/ActionBindingsData.lua @@ -0,0 +1,145 @@ +local ContextActionService = game:GetService("ContextActionService") +local Signal = require(script.Parent.Parent.Parent.Signal) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local HEADER_NAMES = Constants.ActionBindingsFormatting.ChartHeaderNames + +local SORT_COMPARATOR = { + [HEADER_NAMES[1]] = function(a,b) -- "Name" + return a.counter < b.counter + end, + [HEADER_NAMES[2]] = function(a,b) -- "Priority" + if a.actionInfo.priorityLevel == b.actionInfo.priorityLevel then + return a.counter < b.counter + end + return a.actionInfo.priorityLevel < b.actionInfo.priorityLevel + end, + [HEADER_NAMES[3]] = function(a,b) -- "Security" + if a.actionInfo.isCore == b.actionInfo.isCore then + return a.counter < b.counter + else + return a.actionInfo.isCore + end + end, + [HEADER_NAMES[4]] = function(a,b) -- "Action Name" + return a.name:lower() < b.name:lower() + end, + [HEADER_NAMES[5]] = function(a,b) -- "Input Types" + return tostring(a.actionInfo.inputTypes[1]) < tostring(b.actionInfo.inputTypes[1]) + end, +} + +local ActionBindingsData = {} +ActionBindingsData.__index = ActionBindingsData + +function ActionBindingsData.new() + local self = {} + setmetatable(self, ActionBindingsData) + + self._bindingsUpdated = Signal.new() + self._bindingsData = {} + self._bindingCounter = 0 + self._sortedBindingData = {} + self._sortType = HEADER_NAMES[1] -- Name + return self +end + +function ActionBindingsData:setSortType(sortType) + if SORT_COMPARATOR[sortType] then + self._sortType = sortType + table.sort(self._sortedBindingData, SORT_COMPARATOR[self._sortType]) + self._bindingsUpdated:Fire(self._sortedBindingData) + else + error(string.format("attempted to pass invalid sortType: %s", tostring(sortType)), 2) + end +end + +function ActionBindingsData:getSortType() + return self._sortType +end + +function ActionBindingsData:Signal() + return self._bindingsUpdated +end + +function ActionBindingsData:getCurrentData() + return self._sortedBindingData +end + +-- this funciton will require some extra work to handle the +-- case a entry insert occurs during the end of the list +function ActionBindingsData:updateBindingDataEntry(name, info) + if info == nil then + --remove element and clean up sorted + self._bindingsData[name] = nil + + elseif not self._bindingsData[name] then + self._bindingCounter = self._bindingCounter + 1 + self._bindingsData[name] = info + local newEntry = { + name = name, + actionInfo = self._bindingsData[name], + counter = self._bindingCounter, + } + table.insert(self._sortedBindingData, newEntry) + else + self._bindingsData[name] = info + end +end + +function ActionBindingsData:start() + local boundActions = ContextActionService:GetAllBoundActionInfo() + for actionName, actionInfo in pairs(boundActions) do + actionInfo.isCore = false + self:updateBindingDataEntry(actionName, actionInfo) + end + + local boundCoreActions = ContextActionService:GetAllBoundCoreActionInfo() + for actionName, actionInfo in pairs(boundCoreActions) do + actionInfo.isCore = true + self:updateBindingDataEntry(actionName, actionInfo) + end + + if not self._actionChangedConnection then + self._actionChangedConnection = ContextActionService.BoundActionChanged:connect( + function(actionName, changeName, changeTable) + self:updateBindingDataEntry(actionName, nil) + self:updateBindingDataEntry(changeName, changeTable) + self._bindingsUpdated:Fire(self._sortedBindingData) + end) + end + + if not self._actionAddedConnection then + self._actionAddedConnection = ContextActionService.BoundActionAdded:connect( + function(actionName, createTouchButton, actionInfo, isCore) + actionInfo.isCore = isCore + self:updateBindingDataEntry(actionName, actionInfo) + self._bindingsUpdated:Fire(self._sortedBindingData) + end) + + end + if not self._actionRemovedConnection then + self._actionRemovedConnection = ContextActionService.BoundActionRemoved:connect( + function(actionName, actionInfo, isCore) + self:updateBindingDataEntry(actionName, nil) + self._bindingsUpdated:Fire(self._sortedBindingData) + end) + end +end + +function ActionBindingsData:stop() + if self.actionChangedConnector then + self.actionChangedConnector:Disconnect() + self.actionChangedConnector = nil + end + if self.actionAddedConnector then + self.actionAddedConnector:Disconnect() + self.actionAddedConnector = nil + end + if self.actionRemovedConnector then + self.actionRemovedConnector:Disconnect() + self.actionRemovedConnector = nil + end +end + +return ActionBindingsData \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ActionBindings/MainViewActionBindings.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ActionBindings/MainViewActionBindings.lua new file mode 100644 index 0000000..f909a10 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ActionBindings/MainViewActionBindings.lua @@ -0,0 +1,105 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) + +local Components = script.Parent.Parent.Parent.Components +local ActionBindingsChart = require(Components.ActionBindings.ActionBindingsChart) +local UtilAndTab = require(Components.UtilAndTab) + +local Actions = script.Parent.Parent.Parent.Actions +local ActionBindingsUpdateSearchFilter = require(Actions.ActionBindingsUpdateSearchFilter) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local PADDING = Constants.GeneralFormatting.MainRowPadding + +local MainViewActionBindings = Roact.Component:extend("MainViewActionBindings") + +function MainViewActionBindings:init() + self.onUtilTabHeightChanged = function(utilTabHeight) + self:setState({ + utilTabHeight = utilTabHeight + }) + end + + self.onSearchTermChanged = function(newSearchTerm) + self.props.dispatchActionBindingsUpdateSearchFilter(newSearchTerm, {}) + end + + self.utilRef = Roact.createRef() + + self.state = { + utilTabHeight = 0 + } +end + +function MainViewActionBindings:didMount() + local utilSize = self.utilRef.current.Size + self:setState({ + utilTabHeight = utilSize.Y.Offset + }) +end + +function MainViewActionBindings:didUpdate() + local utilSize = self.utilRef.current.Size + if utilSize.Y.Offset ~= self.state.utilTabHeight then + self:setState({ + utilTabHeight = utilSize.Y.Offset + }) + end +end + +function MainViewActionBindings:render() + local size = self.props.size + local formFactor = self.props.formFactor + local tabList = self.props.tabList + local searchTerm = self.props.bindingsSearchTerm + + local utilTabHeight = self.state.utilTabHeight + + return Roact.createElement("Frame",{ + Size = size, + BackgroundColor3 = Constants.Color.BaseGray, + BackgroundTransparency = 1, + LayoutOrder = 3, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + Padding = UDim.new(0, PADDING), + SortOrder = Enum.SortOrder.LayoutOrder, + }), + + UtilAndTab = Roact.createElement(UtilAndTab,{ + windowWidth = size.X.Offset, + formFactor = formFactor, + tabList = tabList, + searchTerm = searchTerm, + layoutOrder = 1, + + refForParent = self.utilRef, + + onHeightChanged = self.onUtilTabHeightChanged, + onSearchTermChanged = self.onSearchTermChanged, + }), + + ActionBindings = utilTabHeight > 0 and Roact.createElement(ActionBindingsChart, { + size = UDim2.new(1, 0, 1, -utilTabHeight), + searchTerm = searchTerm, + layoutOrder = 2, + }), + }) +end + +local function mapStateToProps(state, props) + return { + bindingsSearchTerm = state.ActionBindingsData.bindingsSearchTerm, + } +end + +local function mapDispatchToProps(dispatch) + return { + dispatchActionBindingsUpdateSearchFilter = function(searchTerm, filters) + dispatch(ActionBindingsUpdateSearchFilter(searchTerm, filters)) + end + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(MainViewActionBindings) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ActionBindings/MainViewActionBindings.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ActionBindings/MainViewActionBindings.spec.lua new file mode 100644 index 0000000..b17889f --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ActionBindings/MainViewActionBindings.spec.lua @@ -0,0 +1,36 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local RoactRodux = require(CorePackages.RoactRodux) + local Store = require(CorePackages.Rodux).Store + + local DataProvider = require(script.Parent.Parent.DataProvider) + local MainViewActionBindings = require(script.Parent.MainViewActionBindings) + + it("should create and destroy without errors", function() + local store = Store.new(function() + return { + MainView = { + currTabIndex = 0 + }, + ActionBindingsData = { + bindingsSearchTerm = "" + } + } + end) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + DataProvider = Roact.createElement(DataProvider, {},{ + MainViewActionBindings = Roact.createElement(MainViewActionBindings,{ + size = UDim2.new(), + tabList = {}, + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/BannerButton.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/BannerButton.lua new file mode 100644 index 0000000..c45f2d9 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/BannerButton.lua @@ -0,0 +1,56 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Immutable = require(script.Parent.Parent.Immutable) +local Constants = require(script.Parent.Parent.Constants) +local LINE_WIDTH = Constants.GeneralFormatting.LineWidth +local LINE_COLOR = Constants.GeneralFormatting.LineColor +local ARROW_WIDTH = Constants.GeneralFormatting.ArrowWidth +local CLOSE_ARROW = Constants.Image.RightArrow +local OPEN_ARROW = Constants.Image.DownArrow + +local BannerButton = Roact.Component:extend("BannerButton") + +function BannerButton:render() + local children = self.props[Roact.Children] or {} + + local size = self.props.size + local pos = self.props.pos + local isExpanded = self.props.isExpanded + local layoutOrder = self.props.layoutOrder + + local onButtonPress = self.props.onButtonPress + + local bannerElements = { + BannerButtonArrow = Roact.createElement("ImageLabel", { + Image = isExpanded and OPEN_ARROW or CLOSE_ARROW, + BackgroundTransparency = 1, + Size = UDim2.new(0, ARROW_WIDTH, 0, ARROW_WIDTH), + Position = UDim2.new(0, 0, .5, -ARROW_WIDTH / 2), + }), + + HorizontalLineTop = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + + HorizontalLineBottom = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 1, -LINE_WIDTH), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + } + + return Roact.createElement("ImageButton", { + Size = size, + Position = pos, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + + [Roact.Event.InputEnded] = onButtonPress, + }, Immutable.JoinDictionaries(bannerElements, children)) +end + +return BannerButton \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CellLabel.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CellLabel.lua new file mode 100644 index 0000000..980f4f0 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CellLabel.lua @@ -0,0 +1,32 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Constants) +local TEXT_SIZE = Constants.DefaultFontSize.MainWindow +local TEXT_COLOR = Constants.Color.Text +local MAIN_FONT = Constants.Font.MainWindow +local MAIN_FONT_BOLD = Constants.Font.MainWindowBold + +local function CellLabel(props) + local text = props.text + local size = props.size + local pos = props.pos + local bold = props.bold + local layoutOrder = props.layoutOrder + + return Roact.createElement("TextLabel", { + Text = text, + TextSize = TEXT_SIZE, + TextColor3 = TEXT_COLOR, + TextXAlignment = Enum.TextXAlignment.Left, + TextWrapped = true, + Font = bold and MAIN_FONT_BOLD or MAIN_FONT, + + Size = size, + Position = pos, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }) +end + +return CellLabel \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBox.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBox.lua new file mode 100644 index 0000000..e65ed6f --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBox.lua @@ -0,0 +1,70 @@ +local CorePackages = game:GetService("CorePackages") +local TextService = game:GetService("TextService") +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Constants) +local PADDING = Constants.UtilityBarFormatting.CheckBoxInnerPadding + +local CheckBox = Roact.Component:extend("CheckBox") + +function CheckBox:render() + local checkBoxHeight = self.props.checkBoxHeight + local frameHeight = self.props.frameHeight + local layoutOrder = self.props.layoutOrder + + local name = self.props.name + local font = self.props.font + local fontSize = self.props.fontSize + + local isSelected = self.props.isSelected + local selectedColor = self.props.selectedColor + local unselectedColor = self.props.unselectedColor + local onCheckBoxClicked = self.props.onCheckBoxClicked + + -- this can be replaced with default values once that releases + local image = "" + local borderSize = 1 + local backgroundColor = unselectedColor + + if isSelected then + image = Constants.Image.Check + borderSize = 0 + backgroundColor = selectedColor + end + + local textVector = TextService:GetTextSize(name, fontSize, font, Vector2.new(0, frameHeight)) + local textWidth = textVector.X + + return Roact.createElement("ImageButton",{ + Size = UDim2.new(0, checkBoxHeight + textWidth + (PADDING * 2), 0, frameHeight), + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + + [Roact.Event.Activated] = function(rbx) + onCheckBoxClicked(name, not isSelected) + end, + }, { + Icon = Roact.createElement("ImageLabel", { + Image = image, + Size = UDim2.new(0, checkBoxHeight, 0, checkBoxHeight), + Position = UDim2.new(0, 0, .5, -checkBoxHeight / 2), + BackgroundColor3 = backgroundColor, + BackgroundTransparency = 0, + BorderColor3 = Constants.Color.Text, + BorderSizePixel = borderSize, + }), + Text = Roact.createElement("TextLabel",{ + Text = name, + TextColor3 = Constants.Color.Text, + TextXAlignment = Enum.TextXAlignment.Left, + Font = font, + TextSize = fontSize, + + Size = UDim2.new(1, -frameHeight, 1, 0), + Position = UDim2.new(0, checkBoxHeight + PADDING, 0, 0), + BackgroundTransparency = 1, + }) + }) +end + +return CheckBox \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBox.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBox.spec.lua new file mode 100644 index 0000000..581bd48 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBox.spec.lua @@ -0,0 +1,18 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local CheckBox = require(script.Parent.CheckBox) + + it("should create and destroy without errors", function() + local element = Roact.createElement(CheckBox, { + name = "", + fontSize = 0, + font = 0, + frameHeight = 0, + checkBoxHeight = 0, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBoxContainer.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBoxContainer.lua new file mode 100644 index 0000000..ffec0bd --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBoxContainer.lua @@ -0,0 +1,169 @@ +local CorePackages = game:GetService("CorePackages") +local TextService = game:GetService("TextService") +local Roact = require(CorePackages.Roact) + +local Components = script.Parent.Parent.Components +local CheckBox = require(Components.CheckBox) +local CheckBoxDropDown = require(Components.CheckBoxDropDown) + +local Constants = require(script.Parent.Parent.Constants) +local CHECK_BOX_HEIGHT = Constants.UtilityBarFormatting.CheckBoxHeight +local CHECK_BOX_PADDING = Constants.UtilityBarFormatting.CheckBoxInnerPadding * 2 +local FILTER_ICON_UNFILLED = Constants.Image.FilterUnfilled +local FILTER_ICON_FILLED = Constants.Image.FilterFilled + +local DROP_DOWN_Y_ADJUST = 3 + +local CheckBoxContainer = Roact.PureComponent:extend("CheckBoxContainer") + +function CheckBoxContainer:init() + self.onCheckBoxClicked = function(field, newState) + local onCheckBoxesChanged = self.props.onCheckBoxesChanged + local currState = self.state.checkBoxStates + currState[field] = newState + + self:setState({ + checkBoxStates = currState, + }) + onCheckBoxesChanged(self.state.checkBoxStates) + end + + self.onCheckBoxExpanded = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or + (input.UserInputType == Enum.UserInputType.Touch and + input.UserInputState == Enum.UserInputState.End) then + self:setState({ + expanded = true, + }) + end + end + + self.onCloseCheckBox = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or + (input.UserInputType == Enum.UserInputType.Touch and + input.UserInputState == Enum.UserInputState.End) then + self:setState({ + expanded = false + }) + end + end + + if not self.props.boxNames then + warn("CheckBoxContainer must be passed a list of Box Names or else it only creates an empty frame") + end + + local boxState = {} + local textWidths = {} + local totalLength = 0 + local count = 0 + for ind, name in ipairs(self.props.boxNames) do + local textVector = TextService:GetTextSize( + name, + Constants.DefaultFontSize.UtilBar, + Constants.Font.UtilBar, + Vector2.new(0, 0) + ) + textWidths[ind] = textVector.X + totalLength = totalLength + textVector.X + CHECK_BOX_HEIGHT + CHECK_BOX_PADDING + boxState[name] = true + count = count + 1 + end + + self.ref = Roact.createRef() + + self.state = { + checkBoxStates = boxState, + expanded = false, + textWidths = textWidths, + numCheckBoxes = count, + minFullLength = totalLength, + } +end + +function CheckBoxContainer:render() + local elements = {} + local frameWidth = self.props.frameWidth + local frameHeight = self.props.frameHeight + local pos = self.props.pos + + local boxOrder = self.props.boxNames + local layoutOrder = self.props.layoutOrder + + local boxStates = self.state.checkBoxStates + local minFullLength = self.state.minFullLength + local expanded = self.state.expanded + local numCheckBoxes = self.state.numCheckBoxes + + local anySelected = false + for layoutOrder, name in ipairs(boxOrder) do + elements[name] = Roact.createElement(CheckBox, { + name = name, + font = Constants.Font.UtilBar, + fontSize = Constants.DefaultFontSize.UtilBar, + checkBoxHeight = CHECK_BOX_HEIGHT, + frameHeight = frameHeight, + layoutOrder = layoutOrder, + + isSelected = boxStates[name], + selectedColor = Constants.Color.SelectedBlue, + unselectedColor = Constants.Color.UnselectedGray, + + onCheckBoxClicked = self.onCheckBoxClicked, + }) + anySelected = anySelected or boxStates[name] + end + + if frameWidth < minFullLength then + elements["CheckBoxLayout"] = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Left, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Top, + FillDirection = Enum.FillDirection.Vertical, + }) + + local showDropDown = self.ref.current and expanded + local dropDownPos + if showDropDown then + local absPos = self.ref.current.AbsolutePosition + -- adding slight y offset to nudge dropdown enough to see button border + dropDownPos = UDim2.new(0, absPos.X, 0, absPos.Y + frameHeight + DROP_DOWN_Y_ADJUST) + end + + return Roact.createElement("ImageButton", { + Size = UDim2.new(0, frameHeight, 0, frameHeight), + LayoutOrder = layoutOrder, + + Image = showDropDown and FILTER_ICON_FILLED or FILTER_ICON_UNFILLED, + BackgroundTransparency = 1, + BorderColor3 = Constants.Color.Text, + + [Roact.Event.InputEnded] = self.onCheckBoxExpanded, + [Roact.Ref] = self.ref, + }, { + DropDown = showDropDown and Roact.createElement(CheckBoxDropDown, { + absolutePosition = dropDownPos, + frameWidth = frameWidth, + elementHeight = frameHeight, + numElements = numCheckBoxes, + + onCloseCheckBox = self.onCloseCheckBox + }, elements) + }) + else + elements["CheckBoxLayout"] = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Left, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Top, + FillDirection = Enum.FillDirection.Horizontal, + }) + + return Roact.createElement("Frame", { + Size = UDim2.new(0,frameWidth,0,frameHeight), + Position = pos, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, elements) + end +end + +return CheckBoxContainer \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBoxContainer.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBoxContainer.spec.lua new file mode 100644 index 0000000..6da792b --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBoxContainer.spec.lua @@ -0,0 +1,15 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local CheckBoxContainer = require(script.Parent.CheckBoxContainer) + + it("should create and destroy without errors", function() + local element = Roact.createElement(CheckBoxContainer,{ + boxNames = {}, + frameWidth = 0, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBoxDropDown.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBoxDropDown.lua new file mode 100644 index 0000000..2e322ad --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBoxDropDown.lua @@ -0,0 +1,52 @@ +local CorePackages = game:GetService("CorePackages") +local RobloxGui = game:GetService("CoreGui").RobloxGui +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Constants) +local INNER_FRAME_PADDING = 12 + +local CheckBoxDropDown = Roact.Component:extend("CheckBoxDropDown") + +function CheckBoxDropDown:render() + local children = self.props[Roact.Children] or {} + local absolutePosition = self.props.absolutePosition + local frameWidth = self.props.frameWidth + local elementHeight = self.props.elementHeight + local numElements = self.props.numElements + + local onCloseCheckBox = self.props.onCloseCheckBox + + local frameHeight = elementHeight * numElements + local outerFrameSize = UDim2.new( 0, frameWidth, 0, (2 * INNER_FRAME_PADDING) + frameHeight) + + return Roact.createElement(Roact.Portal, { + target = RobloxGui, + }, { + FullScreen = Roact.createElement("ScreenGui", { + }, { + InputCatcher = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + BackgroundTransparency = 1, + + [Roact.Event.InputEnded] = onCloseCheckBox, + },{ + OuterFrame = Roact.createElement("ImageButton", { + Size = outerFrameSize, + AutoButtonColor = false, + Position = absolutePosition, + BackgroundColor3 = Constants.Color.TextBoxGray, + BackgroundTransparency = 0, + }, { + innerFrame = Roact.createElement("Frame", { + Position = UDim2.new(0, INNER_FRAME_PADDING, 0 , INNER_FRAME_PADDING), + Size = UDim2.new(0,frameWidth, 0, frameHeight), + BackgroundTransparency = 1, + }, children) + }) + }) + }) + }) +end + +return CheckBoxDropDown \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBoxDropDown.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBoxDropDown.spec.lua new file mode 100644 index 0000000..597d132 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/CheckBoxDropDown.spec.lua @@ -0,0 +1,15 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local CheckBoxDropDown = require(script.Parent.CheckBoxDropDown) + + it("should create and destroy without errors", function() + local element = Roact.createElement(CheckBoxDropDown, { + elementHeight = 0, + numElements = 0, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ClientServerButton.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ClientServerButton.lua new file mode 100644 index 0000000..123cfa2 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ClientServerButton.lua @@ -0,0 +1,101 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Constants) +local FRAME_HEIGHT = Constants.UtilityBarFormatting.FrameHeight +local SMALL_FRAME_HEIGHT = Constants.UtilityBarFormatting.SmallFrameHeight +local CS_BUTTON_WIDTH = Constants.UtilityBarFormatting.ClientServerButtonWidth +local SMALL_CS_BUTTON_WIDTH = Constants.UtilityBarFormatting.ClientServerDropDownWidth +local FONT = Constants.Font.UtilBar +local FONT_SIZE = Constants.DefaultFontSize.UtilBar +local FONT_COLOR = Constants.Color.Text + +local FullScreenDropDownButton = require(script.Parent.FullScreenDropDownButton) +local DropDown = require(script.Parent.DropDown) + +local BUTTON_SIZE = UDim2.new(0, CS_BUTTON_WIDTH, 0, FRAME_HEIGHT) +local CLIENT_SERVER_NAMES = {"Client", "Server"} + +local ClientServerButton = Roact.Component:extend("ClientServerButton") + +function ClientServerButton:init() + self.dropDownCallback = function(index) + if index == 1 then + self.props.onClientButton() + elseif index == 2 then + self.props.onServerButton() + end + end +end + +function ClientServerButton:render() + local formFactor = self.props.formFactor + local useDropDown = self.props.useDropDown + local isClientView = self.props.isClientView + local layoutOrder = self.props.layoutOrder + local onServerButton = self.props.onServerButton + local onClientButton = self.props.onClientButton + + local serverButtonColor = Constants.Color.SelectedBlue + local clientButtonColor = Constants.Color.UnselectedGray + + if isClientView then + clientButtonColor = Constants.Color.SelectedBlue + serverButtonColor = Constants.Color.UnselectedGray + end + + if formFactor == Constants.FormFactor.Small then + return Roact.createElement(FullScreenDropDownButton, { + buttonSize = UDim2.new(0, SMALL_CS_BUTTON_WIDTH, 0, SMALL_FRAME_HEIGHT), + dropDownList = CLIENT_SERVER_NAMES, + selectedIndex = isClientView and 1 or 2, + onSelection = self.dropDownCallback, + layoutOrder = layoutOrder, + }) + + elseif useDropDown then + return Roact.createElement(DropDown, { + buttonSize = UDim2.new(0, SMALL_CS_BUTTON_WIDTH, 0, SMALL_FRAME_HEIGHT), + dropDownList = CLIENT_SERVER_NAMES, + selectedIndex = isClientView and 1 or 2, + onSelection = self.dropDownCallback, + layoutOrder = layoutOrder, + }) + else + return Roact.createElement("Frame", { + Size = UDim2.new(0, 2 * CS_BUTTON_WIDTH, 0, FRAME_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + ClientButton = Roact.createElement('TextButton', { + Text = CLIENT_SERVER_NAMES[1], + TextSize = FONT_SIZE, + TextColor3 = FONT_COLOR, + Font = FONT, + Size = BUTTON_SIZE, + AutoButtonColor = false, + BackgroundColor3 = clientButtonColor, + BackgroundTransparency = 0, + LayoutOrder = 1, + + [Roact.Event.Activated] = onClientButton, + }), + ServerButton = Roact.createElement('TextButton', { + Text = CLIENT_SERVER_NAMES[2], + TextSize = FONT_SIZE, + TextColor3 = FONT_COLOR, + Font = FONT, + Size = BUTTON_SIZE, + AutoButtonColor = false, + Position = UDim2.new(0, CS_BUTTON_WIDTH, 0, 0), + BackgroundColor3 = serverButtonColor, + BackgroundTransparency = 0, + LayoutOrder = 2, + + [Roact.Event.Activated] = onServerButton, + }) + }) + end +end + +return ClientServerButton \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ClientServerButton.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ClientServerButton.spec.lua new file mode 100644 index 0000000..599916b --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ClientServerButton.spec.lua @@ -0,0 +1,13 @@ +return function() +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local ClientServerButton = require(script.Parent.ClientServerButton) + +it("should create and destroy without errors", function() + local element = Roact.createElement(ClientServerButton + ) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataConsumer.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataConsumer.lua new file mode 100644 index 0000000..84e1c9e --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataConsumer.lua @@ -0,0 +1,41 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) +local Immutable = require(script.Parent.Parent.Immutable) + +return function(component, ...) + if not component then + error("Expected component to be passed to connection, got nil.") + end + + local targetList = {} + for i = 1, select("#", ...) do + targetList[i] = select(i, ...) + end + + local name = string.format("Consumer(%s)_DependsOn_%s",tostring(component), targetList[1] ) + local DataConsumer = Roact.Component:extend(name) + + function DataConsumer:init() + local contextTable = {} + for _,dataName in pairs(targetList) do + local contextualData = self._context.DevConsoleData[dataName] + if not contextualData then + local errorStr = string.format("%s %s",tostring(dataName), + "could not be found. Make sure DataProvider is above this consumer" + ) + error(errorStr) + return + end + contextTable[dataName] = contextualData + end + + self.state = contextTable + end + + function DataConsumer:render() + local props = Immutable.JoinDictionaries(self.props, self.state) + return Roact.createElement(component, props) + end + + return DataConsumer +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataProvider.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataProvider.lua new file mode 100644 index 0000000..2cd264f --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataProvider.lua @@ -0,0 +1,49 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Components = script.Parent.Parent.Components +local LogData = require(Components.Log.LogData) +local ClientMemoryData = require(Components.Memory.ClientMemoryData) +local ServerMemoryData = require(Components.Memory.ServerMemoryData) +local NetworkData = require(Components.Network.NetworkData) +local ServerScriptsData = require(Components.Scripts.ServerScriptsData) +local DataStoresData = require(Components.DataStores.DataStoresData) +local ServerStatsData = require(Components.ServerStats.ServerStatsData) +local ActionBindingsData = require(Components.ActionBindings.ActionBindingsData) +local ServerJobsData = require(Components.ServerJobs.ServerJobsData) + + +local DataProvider = Roact.Component:extend("DataProvider") + +function DataProvider:init() + self._context.DevConsoleData = { + ClientLogData = LogData.new(true), + ServerLogData = LogData.new(false), + ClientMemoryData = ClientMemoryData.new(), + ServerMemoryData = ServerMemoryData.new(), + ClientNetworkData = NetworkData.new(true), + ServerNetworkData = NetworkData.new(false), + ServerScriptsData = ServerScriptsData.new(), + DataStoresData = DataStoresData.new(), + ServerStatsData = ServerStatsData.new(), + ActionBindingsData = ActionBindingsData.new(), + ServerJobsData = ServerJobsData.new(), + } +end + +function DataProvider:didMount() + if self.props.isDeveloperView then + for _, dataProvider in pairs(self._context.DevConsoleData) do + dataProvider:start() + end + else + self._context.DevConsoleData.ClientLogData:start() + self._context.DevConsoleData.ClientMemoryData:start() + end +end + +function DataProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +return DataProvider \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataStores/DataStoresChart.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataStores/DataStoresChart.lua new file mode 100644 index 0000000..b14b6f7 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataStores/DataStoresChart.lua @@ -0,0 +1,229 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Components = script.Parent.Parent.Parent.Components +local DataConsumer = require(Components.DataConsumer) +local HeaderButton = require(Components.HeaderButton) +local CellLabel = require(Components.CellLabel) +local BannerButton = require(Components.BannerButton) +local LineGraph = require(Components.LineGraph) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local LINE_WIDTH = Constants.GeneralFormatting.LineWidth +local LINE_COLOR = Constants.GeneralFormatting.LineColor +local HEADER_NAMES = Constants.DataStoresFormatting.ChartHeaderNames +local VALUE_CELL_WIDTH = Constants.DataStoresFormatting.ValueCellWidth +local CELL_PADDING = Constants.DataStoresFormatting.CellPadding +local ARROW_PADDING = Constants.DataStoresFormatting.ExpandArrowPadding +local HEADER_HEIGHT = Constants.DataStoresFormatting.HeaderFrameHeight +local ENTRY_HEIGHT = Constants.DataStoresFormatting.EntryFrameHeight + +local GRAPH_HEIGHT = Constants.GeneralFormatting.LineGraphHeight + +local NO_DATA_MSG = "Initialize DataStoresService to view DataStore Budget." + +local convertTimeStamp = require(script.Parent.Parent.Parent.Util.convertTimeStamp) + +local DataStoresChart = Roact.Component:extend("DataStoresChart") + +local function getX(entry) + return entry.time +end + +local function getY(entry) + return entry.value +end + +local function stringFormatY(value) + return math.ceil(value) +end + +function DataStoresChart:init(props) + local currStoresData, currStoresDataCount = props.DataStoresData:getCurrentData() + + self.getOnButtonPress = function (name) + return function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or + (input.UserInputType == Enum.UserInputType.Touch and + input.UserInputState == Enum.UserInputState.End) then + self:setState({ + expandedEntry = self.state.expandedEntry ~= name and name + }) + end + end + end + + self.state = { + dataStoresData = currStoresData, + dataStoresDataCount = currStoresDataCount, + expandedEntry = nil + } +end + +function DataStoresChart:didMount() + self.statsConnector = self.props.DataStoresData:Signal():Connect(function(data, count) + self:setState({ + dataStoresData = data, + dataStoresDataCount = count, + }) + end) +end + +function DataStoresChart:willUnmount() + self.statsConnector:Disconnect() + self.statsConnector = nil +end + +function DataStoresChart:render() + local elements = {} + local searchTerm = self.props.searchTerm + local size = self.props.size + local layoutOrder = self.props.layoutOrder + + local expandedEntry = self.state.expandedEntry + + elements["UIListLayout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + local componentHeight = HEADER_HEIGHT + + local datastoreBudget = self.state.dataStoresData + local currLayoutOrder = 1 + if datastoreBudget then + for name, data in pairs(datastoreBudget) do + if not searchTerm or string.find(name:lower(), searchTerm:lower()) ~= nil then + currLayoutOrder = currLayoutOrder + 1 + + local showGraph = expandedEntry == name + local frameHeight = showGraph and ENTRY_HEIGHT + GRAPH_HEIGHT or ENTRY_HEIGHT + + elements[name] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, frameHeight), + BackgroundTransparency = 1, + LayoutOrder = currLayoutOrder, + }, { + + DataButton = Roact.createElement(BannerButton,{ + size = UDim2.new(1, 0, 0, ENTRY_HEIGHT), + pos = UDim2.new(), + isExpanded = showGraph, + + onButtonPress = self.getOnButtonPress(name), + }, { + [name] = Roact.createElement(CellLabel,{ + text = name, + size = UDim2.new(1,-VALUE_CELL_WIDTH - CELL_PADDING - ARROW_PADDING, 1, 0), + pos = UDim2.new(0, CELL_PADDING + ARROW_PADDING, 0, 0), + }), + + Data = Roact.createElement(CellLabel,{ + text = data.dataSet:back().value, + size = UDim2.new(0, VALUE_CELL_WIDTH - CELL_PADDING, 1, 0), + pos = UDim2.new(1, -VALUE_CELL_WIDTH + CELL_PADDING, 0, 0), + }), + + VerticalLine = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 0, ENTRY_HEIGHT), + Position = UDim2.new(1, -VALUE_CELL_WIDTH, 0, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + + lowerHorizontalLine = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + }), + + Graph = showGraph and Roact.createElement(LineGraph, { + pos = UDim2.new(0, 0, 0, ENTRY_HEIGHT), + size = UDim2.new(1, 0, 1, -ENTRY_HEIGHT), + graphData = data.dataSet, + maxY = data.max, + minY = data.min, + + getX = getX, + getY = getY, + + axisLabelX = "Timestamp", + axisLabelY = name, + + stringFormatX = convertTimeStamp, + stringFormatY = stringFormatY, + + }), + }) + componentHeight = componentHeight + ENTRY_HEIGHT + end + end + end + + if currLayoutOrder == 1 then + return Roact.createElement("TextLabel",{ + Size = size, + Text = NO_DATA_MSG, + TextColor3 = Constants.Color.Text, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }) + end + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + },{ + Header = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, HEADER_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = 1, + }, { + [HEADER_NAMES[1]] = Roact.createElement(CellLabel,{ + text = HEADER_NAMES[1], + size = UDim2.new(1, -VALUE_CELL_WIDTH - CELL_PADDING - ARROW_PADDING, 1, 0), + pos = UDim2.new(0, CELL_PADDING + ARROW_PADDING, 0, 0), + }), + + [HEADER_NAMES[2]] = Roact.createElement(CellLabel,{ + text = HEADER_NAMES[2], + size = UDim2.new(0, VALUE_CELL_WIDTH - CELL_PADDING, 1, 0), + pos = UDim2.new(1, -VALUE_CELL_WIDTH + CELL_PADDING, 0, 0), + }), + + upperHorizontalLine = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + + vertical = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 1, 0), + Position = UDim2.new(1, -VALUE_CELL_WIDTH, 0, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + + lowerHorizontalLine = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + }), + mainFrame = Roact.createElement("ScrollingFrame", { + Position = UDim2.new(0, 0, 0, HEADER_HEIGHT), + Size = UDim2.new(1, 0, 1, -HEADER_HEIGHT), + ScrollBarThickness = 5, + + BackgroundTransparency = 1, + }, elements), + }) +end + +return DataConsumer(DataStoresChart, "DataStoresData") \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataStores/DataStoresChart.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataStores/DataStoresChart.spec.lua new file mode 100644 index 0000000..1765125 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataStores/DataStoresChart.spec.lua @@ -0,0 +1,16 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local DataProvider = require(script.Parent.Parent.DataProvider) + local DataStoresChart = require(script.Parent.DataStoresChart) + + it("should create and destroy without errors", function() + local element = Roact.createElement(DataProvider, {},{ + DataStoresChart = Roact.createElement(DataStoresChart) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataStores/DataStoresData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataStores/DataStoresData.lua new file mode 100644 index 0000000..5543480 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataStores/DataStoresData.lua @@ -0,0 +1,108 @@ +local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer) +local Signal = require(script.Parent.Parent.Parent.Signal) + +local MAX_DATASET_COUNT = tonumber(settings():GetFVariable("NewDevConsoleMaxGraphCount")) + +local getClientReplicator = require(script.Parent.Parent.Parent.Util.getClientReplicator) + +local DataStoresData = {} +DataStoresData.__index = DataStoresData + +function DataStoresData.new() + local self = {} + setmetatable(self, DataStoresData) + + self._dataStoresUpdated = Signal.new() + self._dataStoresData = {} + self._dataStoresDataCount = 0 + self._lastUpdateTime = 0 + return self +end + +function DataStoresData:Signal() + return self._dataStoresUpdated +end + +function DataStoresData:getCurrentData() + return self._dataStoresData, self._dataStoresDataCount +end + +function DataStoresData:updateValue(key, value) + if not self._dataStoresData[key] then + local newBuffer = CircularBuffer.new(MAX_DATASET_COUNT) + newBuffer:push_back({ + value = value, + time = self._lastUpdateTime + }) + + self._dataStoresData[key] = { + max = value, + min = value, + dataSet = newBuffer, + } + else + local dataEntry = self._dataStoresData[key] + local currMax = dataEntry.max + local currMin = dataEntry.min + + local update = { + value = value, + time = self._lastUpdateTime + } + local overwrittenEntry = self._dataStoresData[key].dataSet:push_back(update) + + if overwrittenEntry then + local iter = self._dataStoresData[key].dataSet:iterator() + local dat = iter:next() + if currMax == overwrittenEntry.data then + currMax = currMin + while dat do + currMax = dat.value < currMax and currMax or dat.value + dat = iter:next() + end + end + if currMin == overwrittenEntry.data then + currMin = currMax + while dat do + currMin = currMin < dat.value and currMin or dat.value + dat = iter:next() + end + end + end + + self._dataStoresData[key].max = currMax < value and value or currMax + self._dataStoresData[key].min = currMin < value and currMin or value + end +end + +function DataStoresData:start() + local clientReplicator = getClientReplicator() + if clientReplicator and not self._statsListenerConnection then + self._statsListenerConnection = clientReplicator.StatsReceived:connect(function(stats) + local dataStoreBudget = stats.DataStoreBudget + self._lastUpdateTime = os.time() + if dataStoreBudget then + local count = 0 + for k, v in pairs(dataStoreBudget) do + if type(v) == 'number' then + self:updateValue(k,v) + count = count + 1 + end + end + self._dataStoresDataCount = count + self._dataStoresUpdated:Fire(self._dataStoresData, self._dataStoresDataCount) + end + end) + clientReplicator:RequestServerStats(true) + end +end + +function DataStoresData:stop() + -- listeners are responsible for disconnecting themselves + if self._statsListenerConnection then + self._statsListenerConnection:Disconnect() + self._statsListenerConnection = nil + end +end + +return DataStoresData \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataStores/MainViewDataStores.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataStores/MainViewDataStores.lua new file mode 100644 index 0000000..c5e62c7 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataStores/MainViewDataStores.lua @@ -0,0 +1,105 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) + +local Components = script.Parent.Parent.Parent.Components +local DataStoresChart = require(Components.DataStores.DataStoresChart) +local UtilAndTab = require(Components.UtilAndTab) + +local Actions = script.Parent.Parent.Parent.Actions +local DataStoresUpdateSearchFilter = require(Actions.DataStoresUpdateSearchFilter) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local PADDING = Constants.GeneralFormatting.MainRowPadding + +local MainViewDataStores = Roact.PureComponent:extend("MainViewDataStores") + +function MainViewDataStores:init() + self.onUtilTabHeightChanged = function(utilTabHeight) + self:setState({ + utilTabHeight = utilTabHeight + }) + end + + self.onSearchTermChanged = function(newSearchTerm) + self.props.dispatchDataStoresUpdateSearchFilter(newSearchTerm, {}) + end + + self.utilRef = Roact.createRef() + + self.state = { + utilTabHeight = 0 + } +end + +function MainViewDataStores:didMount() + local utilSize = self.utilRef.current.Size + self:setState({ + utilTabHeight = utilSize.Y.Offset + }) +end + +function MainViewDataStores:didUpdate() + local utilSize = self.utilRef.current.Size + if utilSize.Y.Offset ~= self.state.utilTabHeight then + self:setState({ + utilTabHeight = utilSize.Y.Offset + }) + end +end + +function MainViewDataStores:render() + local size = self.props.size + local formFactor = self.props.formFactor + local tabList = self.props.tabList + local searchTerm = self.props.storesSearchTerm + + local utilTabHeight = self.state.utilTabHeight + + return Roact.createElement("Frame",{ + Size = size, + BackgroundColor3 = Constants.Color.BaseGray, + BackgroundTransparency = 1, + LayoutOrder = 3, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + Padding = UDim.new(0, PADDING), + SortOrder = Enum.SortOrder.LayoutOrder, + }), + + UtilAndTab = Roact.createElement(UtilAndTab,{ + windowWidth = size.X.Offset, + formFactor = formFactor, + tabList = tabList, + searchTerm = searchTerm, + layoutOrder = 1, + + refForParent = self.utilRef, + + onHeightChanged = self.onUtilTabHeightChanged, + onSearchTermChanged = self.onSearchTermChanged, + }), + + DataStores = utilTabHeight > 0 and Roact.createElement(DataStoresChart, { + size = UDim2.new(1, 0, 1, -utilTabHeight), + searchTerm = searchTerm, + layoutOrder = 2, + }) + }) +end + +local function mapStateToProps(state, props) + return { + storesSearchTerm = state.DataStoresData.storesSearchTerm, + } +end + +local function mapDispatchToProps(dispatch) + return { + dispatchDataStoresUpdateSearchFilter = function(searchTerm, filters) + dispatch(DataStoresUpdateSearchFilter(searchTerm, filters)) + end, + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(MainViewDataStores) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataStores/MainViewDataStores.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataStores/MainViewDataStores.spec.lua new file mode 100644 index 0000000..c910153 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DataStores/MainViewDataStores.spec.lua @@ -0,0 +1,36 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local RoactRodux = require(CorePackages.RoactRodux) + local Store = require(CorePackages.Rodux).Store + + local DataProvider = require(script.Parent.Parent.DataProvider) + local MainViewDataStores = require(script.Parent.MainViewDataStores) + + it("should create and destroy without errors", function() + local store = Store.new(function() + return { + MainView = { + currTabIndex = 0 + }, + DataStoresData = { + storesSearchTerm = "" + } + } + end) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + DataProvider = Roact.createElement(DataProvider, {},{ + MainViewDataStores = Roact.createElement(MainViewDataStores,{ + size = UDim2.new(), + tabList = {}, + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DevConsoleTopBar.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DevConsoleTopBar.lua new file mode 100644 index 0000000..bf9534c --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DevConsoleTopBar.lua @@ -0,0 +1,180 @@ +local CorePackages = game:GetService("CorePackages") +local RobloxGui = game:GetService("CoreGui").RobloxGui +local TextService = game:GetService("TextService") +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) + +local Constants = require(script.Parent.Parent.Constants) +local FRAME_HEIGHT = Constants.TopBarFormatting.FrameHeight +local ICON_SIZE = .5 * FRAME_HEIGHT +local ICON_PADDING = (FRAME_HEIGHT - ICON_SIZE) / 2 + +local DEVCONSOLE_TEXT = "Developer Console" +local DEVCONSOLE_TEXT_FRAMESIZE = TextService:GetTextSize(DEVCONSOLE_TEXT, Constants.DefaultFontSize.TopBar, + Constants.Font.TopBar, Vector2.new(0, 0)) + +local LiveUpdateElement = require(script.Parent.Parent.Components.LiveUpdateElement) +local SetDevConsolePosition = require(script.Parent.Parent.Actions.SetDevConsolePosition) + +local DevConsoleTopBar = Roact.Component:extend("DevConsoleTopBar") + +function DevConsoleTopBar:init() + self.inputBegan = function(rbx,input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + local absPos = self.ref.current.AbsolutePosition + local startPos = Vector3.new(absPos.X, absPos.Y, 0) + self:setState({ + startPos = startPos, + startOffset = input.Position, + moving = true, + }) + end + end + self.inputChanged = function(rbx,input) + if self.state.moving then + + local offset = self.state.startPos - self.state.startOffset + offset = offset + input.Position + local position = UDim2.new(0, offset.X, 0, offset.Y) + self.props.dispatchSetDevConsolePosition(position) + end + end + self.inputEnded = function(rbx,input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self:setState({ + moving = false, + }) + end + end + + self.ref = Roact.createRef() +end + +function DevConsoleTopBar:render() + local isMinimized = self.props.isMinimized + local formFactor = self.props.formFactor + + local onMinimizeClicked = self.props.onMinimizeClicked + local onMaximizeClicked = self.props.onMaximizeClicked + local onCloseClicked = self.props.onCloseClicked + + local moving = self.state.moving + + local elements = {} + + + elements["WindowTitle"] = Roact.createElement("TextLabel", { + Text = DEVCONSOLE_TEXT, + TextSize = Constants.DefaultFontSize.TopBar, + TextColor3 = Color3.new(1, 1, 1), + Font = Constants.Font.TopBar, + Size = UDim2.new(0, DEVCONSOLE_TEXT_FRAMESIZE.X, 0, FRAME_HEIGHT), + Position = UDim2.new(0, 0, 0, 0), + BackgroundColor3 = Constants.Color.BaseGray, + BackgroundTransparency = 1, + TextXAlignment = Enum.TextXAlignment.Left, + }) + + local liveStatsModulePos = UDim2.new(0, DEVCONSOLE_TEXT_FRAMESIZE.X, 0, 0) + local liveStatsModuleSize = UDim2.new(1, -2 * DEVCONSOLE_TEXT_FRAMESIZE.X, 0, FRAME_HEIGHT) + + if isMinimized then + liveStatsModulePos = UDim2.new(0, 0, 1, 0) + liveStatsModuleSize = UDim2.new(1, 0, 1, 0) + + elseif self.ref.current then + liveStatsModuleSize = UDim2.new( + 0, + self.ref.current.AbsoluteSize.X - (2 * DEVCONSOLE_TEXT_FRAMESIZE.X), + 0, + FRAME_HEIGHT + ) + end + + local topBarLiveUpdate = self.props.topBarLiveUpdate + + elements["LiveStatsModule"] = Roact.createElement(LiveUpdateElement, { + topBarLiveUpdate = topBarLiveUpdate, + size = liveStatsModuleSize, + position = liveStatsModulePos, + }) + + -- minimize and maximize buttons should only appear on desktop + if formFactor == Constants.FormFactor.Large then + if not isMinimized then + elements["MinButton"] = Roact.createElement("ImageButton", { + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + Position = UDim2.new(1, -2 * FRAME_HEIGHT + ICON_PADDING, 0, ICON_PADDING), + BorderColor3 = Color3.new(1, 0, 0), + BackgroundColor3 = Constants.Color.BaseGray, + BackgroundTransparency = 1, + Image = Constants.Image.Minimize, + + [Roact.Event.Activated] = onMinimizeClicked, + }) + else + elements["MaxButton"] = Roact.createElement("ImageButton", { + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + Position = UDim2.new(1, -2 * FRAME_HEIGHT + ICON_PADDING, 0, ICON_PADDING), + BorderColor3 = Color3.new(0, 0, 1), + BackgroundColor3 = Constants.Color.BaseGray, + BackgroundTransparency = 1, + Image = Constants.Image.Maximize, + + [Roact.Event.Activated] = onMaximizeClicked, + }) + end + end + + elements["CloseButton"] = Roact.createElement("ImageButton", { + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + Position = UDim2.new(1, -FRAME_HEIGHT + ICON_PADDING, 0, ICON_PADDING), + BorderColor3 = Color3.new(0, 1, 0), + BackgroundColor3 = Constants.Color.BaseGray, + BackgroundTransparency = 1, + Image = Constants.Image.Close, + [Roact.Event.Activated] = onCloseClicked, + }) + + --[[ we do this to catch all inputchanged events + if we can handle LARGE distances of continuous MouseMovmement input events + for dragging then we might be able to remove the portal + ]]-- + elements["MovmentCatchAll"] = moving and Roact.createElement(Roact.Portal, { + target = RobloxGui, + }, { + InputCatcher = Roact.createElement("ScreenGui", {}, { + GreyOutFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Constants.Color.Black, + BackgroundTransparency = .99, + Active = true, + + [Roact.Event.InputChanged] = self.inputChanged, + [Roact.Event.InputEnded] = self.inputEnded, + }) + }) + }) + + return Roact.createElement("ImageButton", { + Size = UDim2.new(1, 0, 0, FRAME_HEIGHT), + BackgroundColor3 = Constants.Color.Black, + BackgroundTransparency = .5, + AutoButtonColor = false, + LayoutOrder = 1, + + [Roact.Ref] = self.ref, + + [Roact.Event.InputBegan] = self.inputBegan, + }, elements) +end + +local function mapDispatchToProps(dispatch) + return { + dispatchSetDevConsolePosition = function (size) + dispatch(SetDevConsolePosition(size)) + end + } +end + +return RoactRodux.UNSTABLE_connect2(nil, mapDispatchToProps)(DevConsoleTopBar) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DevConsoleTopBar.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DevConsoleTopBar.spec.lua new file mode 100644 index 0000000..dd17cf5 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DevConsoleTopBar.spec.lua @@ -0,0 +1,32 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local RoactRodux = require(CorePackages.RoactRodux) + local Store = require(CorePackages.Rodux).Store + + local DataProvider = require(script.Parent.DataProvider) + local DevConsoleTopBar = require(script.Parent.DevConsoleTopBar) + + it("should create and destroy without errors", function() + local store = Store.new(function() + return { + TopBarLiveUpdate = { + LogWarningCount = 0, + LogErrorCount = 0 + } + } + end) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + DataProvider = Roact.createElement(DataProvider, nil, { + DevConsoleTopBar = Roact.createElement(DevConsoleTopBar) + }) + + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DevConsoleWindow.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DevConsoleWindow.lua new file mode 100644 index 0000000..02129ff --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DevConsoleWindow.lua @@ -0,0 +1,264 @@ +local CorePackages = game:GetService("CorePackages") +local CoreGui = game:GetService("CoreGui").RobloxGui + +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) +local DevConsole = script.Parent.Parent + +local Constants = require(DevConsole.Constants) +local TOPBAR_HEIGHT = Constants.TopBarFormatting.FrameHeight +local ROW_PADDING = Constants.Padding.WindowPadding +local MIN_SIZE = Constants.MainWindowInit.MinSize + +local Components = DevConsole.Components +local DevConsoleTopBar = require(Components.DevConsoleTopBar) + +local Actions = script.Parent.Parent.Actions +local ChangeDevConsoleSize = require(Actions.ChangeDevConsoleSize) +local SetDevConsoleVisibility = require(Actions.SetDevConsoleVisibility) +local SetDevConsoleMinimized = require(Actions.SetDevConsoleMinimized) + +local BORDER_SIZE = 16 + +local DevConsoleWindow = Roact.PureComponent:extend("DevConsoleWindow") + +function DevConsoleWindow:onMinimizeClicked() + self.props.dispatchSetDevConsoleMinimized(true) +end + +function DevConsoleWindow:onMaximizeClicked() + self.props.dispatchSetDevConsoleMinimized(false) +end + +function DevConsoleWindow:onCloseClicked() + self.props.dispatchSetDevConsolVisibility(false) + +end + +function DevConsoleWindow:init() + self.setDevConsoleSize = function (self, topLeft, bottomRight) + local x = bottomRight.X - topLeft.X + local y = bottomRight.Y - topLeft.Y + + x = x < MIN_SIZE.X and MIN_SIZE.X or x + y = y < MIN_SIZE.Y and MIN_SIZE.Y or y + + self.props.dispatchChangeDevConsoleSize(UDim2.new(0, x, 0, y)) + end + + self.resizeInputBegan = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self:setState({ + resizing = true, + }) + end + end + + self.resizeInputChanged = function(rbx,input) + if self.state.resizing then + local currPosition = self.ref.current.AbsolutePosition + local cornerPos = input.Position + + self:setDevConsoleSize(currPosition, cornerPos) + end + end + + self.resizeInputEnded = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + --reset resize-dragger + self:setState({ + resizing = false + }) + end + end + + self.ref = Roact.createRef() + + self.state = { + resizing = false, + } +end + +function DevConsoleWindow:didMount() + -- we need to run this delay before grabbing the size of the frame + -- because the DevconsoleWindow is mounted before the ScreenGui + -- is resized to the correct screen size. + delay(0, function () + if self.ref.current then + local absPos1 = self.ref.current.AbsolutePosition + local absPos2 = self.ref.current.ResizeButton.AbsolutePosition + self:setDevConsoleSize(absPos1, absPos2) + end + end) +end + +function DevConsoleWindow:render() + local isVisible = self.props.isVisible + local formFactor = self.props.formFactor + local isdeveloperView = self.props.isdeveloperView + local currTabIndex = self.props.currTabIndex + local tabList = self.props.tabList + + local isMinimized = self.props.isMinimized + local pos = self.props.position + local size = self.props.size + + local resizing = self.state.resizing + + local windowSize = size + local windowPos = UDim2.new() + + local elements = {} + + local borderSizePixel = BORDER_SIZE + + if formFactor ~= Constants.FormFactor.Large then + -- none desktop/Large are full screen devconsoles + local absSize = CoreGui.AbsoluteSize + size = UDim2.new(0, absSize.X, 0, absSize.Y) + pos = UDim2.new(0, 0, 0, 0) + + windowPos = UDim2.new(0, 16, 0, 0) + windowSize = size + UDim2.new(0, -32, 0, 0) + + borderSizePixel = 0 + end + + elements["UIListLayout"] = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + Padding = UDim.new(0, ROW_PADDING), + }) + + if isMinimized then + elements["TopBar"] = Roact.createElement(DevConsoleTopBar, { + LayoutOrder = 1, + formFactor = formFactor, + isMinimized = true, + onMinimizeClicked = function() + self:onMinimizeClicked() + end, + onMaximizeClicked = function() + self:onMaximizeClicked() + end, + onCloseClicked = function() + self:onCloseClicked() + end, + }) + + return Roact.createElement("Frame", { + Position = UDim2.new(1, -500, 1, -2 * TOPBAR_HEIGHT), + Size = UDim2.new(0, 500, 0, 2 * TOPBAR_HEIGHT), + BackgroundColor3 = Color3.new(0, 0, 0), + Transparency = Constants.MainWindowInit.Transparency, + Active = true, + Visible = isVisible, + BorderColor3 = Constants.Color.BaseGray, + + [Roact.Ref] = self.ref, + }, elements) + else + elements["TopBar"] = Roact.createElement(DevConsoleTopBar, { + LayoutOrder = 1, + formFactor = formFactor, + isMinimized = false, + onMinimizeClicked = function() + self:onMinimizeClicked() + end, + onMaximizeClicked = function() + self:onMaximizeClicked() + end, + onCloseClicked = function() + self:onCloseClicked() + end, + }) + + local mainViewSize = windowSize + + local TopSectionHeight = TOPBAR_HEIGHT + 2 * ROW_PADDING + local mainViewSizeOffset = UDim2.new(0, 0, 0, TopSectionHeight) + mainViewSize = mainViewSize - mainViewSizeOffset + + if tabList and (currTabIndex > 0) and self.ref.current then + elements["MainView"] = Roact.createElement(tabList[currTabIndex].tab, { + size = mainViewSize, + formFactor = formFactor, + isdeveloperView = isdeveloperView, + tabList = tabList, + currTabIndex = currTabIndex, + }) + end + + return Roact.createElement("Frame", { + Position = pos, + Size = size, + BackgroundColor3 = Color3.new(0, 0, 0), + Transparency = Constants.MainWindowInit.Transparency, + Visible = isVisible, + BorderColor3 = Constants.Color.BaseGray, + BorderSizePixel = borderSizePixel, + + [Roact.Ref] = self.ref, + }, { + DevConsoleUI = Roact.createElement("Frame", { + Size = windowSize, + Position = windowPos, + BackgroundTransparency = 1, + },elements), + + ResizeButton = Roact.createElement("ImageButton", { + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(0, borderSizePixel, 0, borderSizePixel), + BackgroundColor3 = Color3.new(0, 0, 0), + + [Roact.Event.InputBegan] = self.resizeInputBegan, + }), + --[[ we do this to catch all inputchanged events + if we can handle LARGE distances of continuous MouseMovmement input events + for dragging then we might be able to remove the portal + ]]-- + ResizeCatchAll = resizing and Roact.createElement(Roact.Portal, { + target = CoreGui, + }, { + InputCatcher = Roact.createElement("ScreenGui", {}, { + GreyOutFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Constants.Color.Black, + BackgroundTransparency = .99, + Active = true, + + [Roact.Event.InputChanged] = self.resizeInputChanged, + [Roact.Event.InputEnded] = self.resizeInputEnded, + }) + }) + }) + }) + end +end + +local function mapStateToProps(state, props) + return { + isVisible = state.DisplayOptions.isVisible, + isMinimized = state.DisplayOptions.isMinimized, + position = state.DisplayOptions.position, + size = state.DisplayOptions.size, + currTabIndex = state.MainView.currTabIndex, + } +end + +local function mapDispatchToProps(dispatch) + return { + dispatchChangeDevConsoleSize = function (size) + dispatch(ChangeDevConsoleSize(size)) + end, + dispatchSetDevConsolVisibility = function (isVisible) + dispatch(SetDevConsoleVisibility(isVisible)) + end, + dispatchSetDevConsoleMinimized = function (isMinimized) + dispatch(SetDevConsoleMinimized(isMinimized)) + end, + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(DevConsoleWindow) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DevConsoleWindow.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DevConsoleWindow.spec.lua new file mode 100644 index 0000000..f9e1a7f --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DevConsoleWindow.spec.lua @@ -0,0 +1,51 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local RoactRodux = require(CorePackages.RoactRodux) + local Store = require(CorePackages.Rodux).Store + + local DataProvider = require(script.Parent.DataProvider) + local DevConsoleWindow = require(script.Parent.DevConsoleWindow) + + it("should create and destroy without errors", function() + local store = Store.new(function() + return { + DisplayOptions = { + isVisible = 0, + platform = 0, + isMinimized = false, + size = UDim2.new(1, 0, 1, 0), + }, + MainView = { + currTabIndex = 0 + }, + TopBarLiveUpdate = { + LogWarningCount = 0, + LogErrorCount = 0 + }, + LogData = { + clientData = { }, + clientDataFiltered = { }, + clientSearchTerm = "", + clientTypeFilters = { }, + + serverData = { }, + serverDataFiltered = { }, + serverSearchTerm = "", + serverTypeFilters = { }, + } + } + end) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + DataProvider = Roact.createElement(DataProvider,{},{ + DevConsoleWindow = Roact.createElement(DevConsoleWindow) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DropDown.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DropDown.lua new file mode 100644 index 0000000..f49e928 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DropDown.lua @@ -0,0 +1,154 @@ +local CorePackages = game:GetService("CorePackages") +local RobloxGui = game:GetService("CoreGui").RobloxGui +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Constants) +local FONT = Constants.Font.UtilBar +local FONT_SIZE = Constants.DefaultFontSize.UtilBar +local ARROW_SIZE = Constants.GeneralFormatting.DropDownArrowHeight +local ARROW_OFFSET = ARROW_SIZE / 2 +local OPEN_ARROW = Constants.Image.DownArrow +local INNER_FRAME_PADDING = 12 + +local DropDown = Roact.Component:extend("DropDown") + +function DropDown:init() + self.onMainButtonPressed = function(rbx, input) + self:setState({ + showDropDown = true, + }) + end + + self.nonDropDownSelection = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or + (input.UserInputType == Enum.UserInputType.Touch and + input.UserInputState == Enum.UserInputState.End) then + self:setState({ + showDropDown = false + }) + end + end + + self.state = { + showDropDown = false, + } + + self.ref = Roact.createRef() +end + +function DropDown:render() + local buttonSize = self.props.buttonSize + local dropDownList = self.props.dropDownList + local selectedIndex = self.props.selectedIndex + + local onSelection = self.props.onSelection + local layoutOrder = self.props.layoutOrder + + local showDropDown = self.ref.current and self.state.showDropDown + + local children = {} + local absolutePosition + local outerFrameSize + local frameHeight = 0 + local frameWidth = 0 + + if self.ref.current and showDropDown then + local absolutePos = self.ref.current.AbsolutePosition + local absoluteSize = self.ref.current.AbsoluteSize + + frameWidth = absoluteSize.X + + children["UIListLayout"] = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Left, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Top, + }) + + for ind, name in pairs(dropDownList) do + local color = (ind == selectedIndex) and Constants.Color.SelectedGray or Constants.Color.UnselectedGray + + children[name] = Roact.createElement("TextButton", { + Size = buttonSize, + Text = name, + TextColor3 = Constants.Color.Text, + TextSize = FONT_SIZE, + Font = FONT, + + AutoButtonColor = false, + BackgroundColor3 = color, + BackgroundTransparency = 0, + BorderSizePixel = 0, + + LayoutOrder = ind, + + [Roact.Event.Activated] = function() + onSelection(ind) + self:setState({ + showDropDown = false + }) + end + }) + frameHeight = frameHeight + absoluteSize.Y + end + + local padding = 2 * INNER_FRAME_PADDING + outerFrameSize = UDim2.new(0, frameWidth + padding, 0, frameHeight + padding) + absolutePosition = UDim2.new(0, absolutePos.X, 0, absolutePos.Y + absoluteSize.Y) + end + + return Roact.createElement("TextButton", { + Size = buttonSize, + Text = dropDownList[selectedIndex], + TextColor3 = Constants.Color.Text, + TextSize = FONT_SIZE, + Font = FONT, + + AutoButtonColor = false, + BackgroundColor3 = Constants.Color.UnselectedGray, + BackgroundTransparency = 0, + LayoutOrder = layoutOrder, + + [Roact.Event.Activated] = self.onMainButtonPressed, + + [Roact.Ref] = self.ref, + }, { + arrow = Roact.createElement("ImageLabel", { + Image = OPEN_ARROW, + BackgroundTransparency = 1, + Size = UDim2.new(0, ARROW_SIZE, 0, ARROW_SIZE), + Position = UDim2.new(1, -ARROW_SIZE - ARROW_OFFSET, .5, -ARROW_OFFSET), + }), + + DropDown = showDropDown and Roact.createElement(Roact.Portal, { + target = RobloxGui, + }, { + FullScreen = Roact.createElement("ScreenGui", { + }, { + InputCatcher = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + BackgroundTransparency = 1, + + [Roact.Event.InputEnded] = self.nonDropDownSelection, + }, { + OuterFrame = Roact.createElement("ImageButton", { + Size = outerFrameSize, + AutoButtonColor = false, + Position = absolutePosition, + BackgroundColor3 = Constants.Color.TextBoxGray, + BackgroundTransparency = 0, + }, { + innerFrame = Roact.createElement("Frame", { + Position = UDim2.new(0, INNER_FRAME_PADDING, 0 , INNER_FRAME_PADDING), + Size = UDim2.new(0, frameWidth, 0, frameHeight), + BackgroundTransparency = 1, + }, children) + }) + }) + }) + }) + }) + +end + +return DropDown \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DropDown.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DropDown.spec.lua new file mode 100644 index 0000000..071cb9d --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/DropDown.spec.lua @@ -0,0 +1,15 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local DropDown = require(script.Parent.DropDown) + + it("should create and destroy without errors", function() + local element = Roact.createElement(DropDown, { + elementHeight = 0, + numElements = 0, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/FullScreenDropDownButton.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/FullScreenDropDownButton.lua new file mode 100644 index 0000000..ceacafb --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/FullScreenDropDownButton.lua @@ -0,0 +1,153 @@ + +local CorePackages = game:GetService("CorePackages") +local CoreGui = game:GetService("CoreGui").RobloxGui +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Constants) +local ENTRY_HEIGHT = Constants.GeneralFormatting.DropDownEntryHeight +local ARROW_SIZE = Constants.GeneralFormatting.DropDownArrowHeight +local ARROW_OFFSET = ARROW_SIZE / 2 +local OPEN_ARROW = Constants.Image.DownArrow + +local FULL_SCREEN_WIDTH = 375 +local INNER_Y_OFFSET = 8 +local INNER_X_OFFSET = 15 + +local FullScreenDropDownButton = Roact.Component:extend("FullScreenDropDownButton") + +function FullScreenDropDownButton:init() + self.startDropDownView = function() + self:setState({ + selectionScreenExpanded = true + }) + end + + self.noSelection = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or + (input.UserInputType == Enum.UserInputType.Touch and + input.UserInputState == Enum.UserInputState.End) then + self:setState({ + selectionScreenExpanded = false + }) + end + end + + self.state = { + selectionScreenExpanded = false, + } +end + +function FullScreenDropDownButton:render() + local buttonSize = self.props.buttonSize + local dropDownList = self.props.dropDownList + local selectedIndex = self.props.selectedIndex + local onSelection = self.props.onSelection + local layoutOrder = self.props.layoutOrder + + local isSelecting = self.state.selectionScreenExpanded + + local dropDownItemList = {} + local scrollingFrameHeight = 2 * INNER_Y_OFFSET + + if isSelecting then + dropDownItemList["UIListLayout"] = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Top, + FillDirection = Enum.FillDirection.Vertical, + }) + + if dropDownList then + for ind,name in ipairs(dropDownList) do + local color = (ind == selectedIndex) and Constants.Color.SelectedGray or Constants.Color.UnselectedGray + + dropDownItemList[ind] = Roact.createElement("TextButton", { + Text = name, + Font = Constants.Font.TabBar, + TextSize = Constants.DefaultFontSize.DropDownTabBar, + TextColor3 = Constants.Color.Text, + AutoButtonColor = false, + + Size = UDim2.new(1, 0, 0, ENTRY_HEIGHT), + BackgroundColor3 = color, + LayoutOrder = ind, + BorderSizePixel = 0, + + [Roact.Event.Activated] = function(rbx) + self:setState({ + selectionScreenExpanded = false + }) + onSelection(ind) + end, + }) + + scrollingFrameHeight = scrollingFrameHeight + ENTRY_HEIGHT + end + end + end + + return Roact.createElement("TextButton", { + Size = buttonSize, + BackgroundColor3 = Constants.Color.UnselectedGray, + Text = "", + AutoButtonColor = false, + LayoutOrder = layoutOrder, + + [Roact.Event.Activated] = self.startDropDownView, + }, { + text = Roact.createElement("TextLabel", { + Size = UDim2.new(1, -ARROW_SIZE - ARROW_OFFSET, 1, 0), + Text = dropDownList[selectedIndex], + Font = Constants.Font.TabBar, + TextSize = Constants.DefaultFontSize.DropDownTabBar, + TextXAlignment = Enum.TextXAlignment.Center, + TextColor3 = Constants.Color.Text, + + BackgroundTransparency = 1, + }), + + arrow = Roact.createElement("ImageLabel", { + Image = OPEN_ARROW, + BackgroundTransparency = 1, + Size = UDim2.new(0, ARROW_SIZE, 0, ARROW_SIZE), + Position = UDim2.new(1, -ARROW_SIZE - ARROW_OFFSET, .5, -ARROW_OFFSET), + }), + + selectionView = isSelecting and Roact.createElement(Roact.Portal, { + target = CoreGui, + }, { + TempScreen = Roact.createElement("ScreenGui", {}, { + GreyOutFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Constants.Color.Black, + BackgroundTransparency = .36, + Active = true, + + [Roact.Event.InputEnded] = self.noSelection, + }, { + BorderFrame = Roact.createElement("Frame", { + Size = UDim2.new(0, FULL_SCREEN_WIDTH, 0, scrollingFrameHeight), + Position = UDim2.new(.5, -FULL_SCREEN_WIDTH / 2, 0, 0), + BackgroundColor3 = Constants.Color.UnselectedGray, + BorderSizePixel = 0, + }, { + SelectionFrame = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, -2 * INNER_X_OFFSET, 1, -2 * INNER_Y_OFFSET), + Position = UDim2.new(0, INNER_X_OFFSET, 0, INNER_Y_OFFSET), + BackgroundTransparency = 1, + + -- adding an extra entry's worth of height for easier access to last + -- child when trying to select last child + CanvasSize = UDim2.new(1, -2 * INNER_X_OFFSET, 1, ENTRY_HEIGHT), + BorderSizePixel = 0, + + ScrollBarThickness = 0, + }, dropDownItemList) + }) + }) + }) + }) + }) +end + +return FullScreenDropDownButton \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/FullScreenDropDownButton.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/FullScreenDropDownButton.spec.lua new file mode 100644 index 0000000..e153e48 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/FullScreenDropDownButton.spec.lua @@ -0,0 +1,14 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local FullScreenDropDownButton = require(script.Parent.FullScreenDropDownButton) + + it("should create and destroy without errors", function() + local element = Roact.createElement(FullScreenDropDownButton, { + dropDownList = {} + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/HeaderButton.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/HeaderButton.lua new file mode 100644 index 0000000..6898ab6 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/HeaderButton.lua @@ -0,0 +1,34 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Constants) +local FONT = Constants.Font.MainWindowHeader +local TEXT_SIZE = Constants.DefaultFontSize.MainWindowHeader +local TEXT_COLOR = Constants.Color.Text + +local function HeaderButton(props) + local text = props.text + local size = props.size + local pos = props.pos + local sortfunction = props.sortfunction + + return Roact.createElement("TextButton", { + Text = text, + TextSize = TEXT_SIZE, + TextColor3 = TEXT_COLOR, + Font = FONT, + TextXAlignment = Enum.TextXAlignment.Left, + + Size = size, + Position = pos, + BackgroundTransparency = 1, + + [Roact.Event.Activated] = function() + if sortfunction then + sortfunction(text) + end + end, + }) +end + +return HeaderButton \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/LineGraph.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/LineGraph.lua new file mode 100644 index 0000000..b2d1a9b --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/LineGraph.lua @@ -0,0 +1,287 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local LineGraphHoverDisplay = require(script.Parent.LineGraphHoverDisplay) + +local Constants = require(script.Parent.Parent.Constants) +local TEXT_COLOR = Constants.Color.Text +local MAIN_LINE_COLOR = Constants.Color.HighlightBlue +local LINE_WIDTH = Constants.GeneralFormatting.LineWidth +local LINE_COLOR = Constants.GeneralFormatting.LineColor + +local POINT_WIDTH = Constants.Graph.PointWidth +local POINT_OFFSET = Constants.Graph.PointOffset +local GRAPH_PADDING = Constants.Graph.Padding +local GRAPH_SCALE = Constants.Graph.Scale +local GRAPH_Y_INNER_PADDING = Constants.Graph.InnerPaddingY +local GRAPH_Y_INNER_SCALE = Constants.Graph.InnerScaleY +local TEXT_PADDING = Constants.Graph.TextPadding + +local LineGraph = Roact.Component:extend("LineGraph") + +function LineGraph:init() + self.onGraphInputChanged = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseMovement then + if not self.state.holdPos then + self:setState({ + inputPosition = input.Position, + }) + end + end + end + + self.onGraphInputEnded = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseMovement then + if not self.state.holdPos then + self:setState({ + inputPosition = false + }) + end + elseif input.UserInputType == Enum.UserInputType.MouseButton1 then + self:setState({ + holdPos = not self.state.holdPos + }) + end + end + + self.graphRef = Roact.createRef() + self.state = { + selectedTimeStamps = {} + } +end + +function LineGraph:didUpdate() + if self.state.absGraphSize ~= self.graphRef.current.AbsoluteSize then + local absSize = self.graphRef.current.AbsoluteSize + local absPos = self.graphRef.current.AbsolutePosition + + self:setState({ + absGraphSize = absSize, + absGraphPos = absPos, + }) + end +end + +function LineGraph:didMount() + local absSize = self.graphRef.current.AbsoluteSize + local absPos = self.graphRef.current.AbsolutePosition + + self:setState({ + absGraphSize = absSize, + absGraphPos = absPos, + }) +end + +function LineGraph:render() + local size = self.props.size + local pos = self.props.pos + + local graphData = self.props.graphData + + local getX = self.props.getX + local getY = self.props.getY + + local stringFormatX = self.props.stringFormatX + local stringFormatY = self.props.stringFormatY + + local axisLabelX = self.props.axisLabelX + local axisLabelY = self.props.axisLabelY + + local layoutOrder = self.props.layoutOrder + + local inputPosition = self.state.inputPosition + local absGraphSize = self.state.absGraphSize + local absGraphPos = self.state.absGraphPos + + + local maxX = getX(graphData:back()) + local minX = getX(graphData:front()) + local maxY = self.props.maxY + local minY = self.props.minY + + local elements = {} + if absGraphSize then + local dataPoints = {} + local dataIter = graphData:iterator() + local data = dataIter:next() + while data do + local datapoint = getY(data) + local time = getX(data) + + local xdivisor = maxX - minX + local xPosition = xdivisor > 0 and (time - minX) / xdivisor or 0 + + local ydivisor = maxY - minY + local yPosition = ydivisor > 0 and (datapoint - minY) / ydivisor or 1 + + local point = { + X = xPosition, + Y = yPosition, + data = data, + } + + table.insert(dataPoints, point) + + data = dataIter:next() + end + + for i = 2, #dataPoints do + local aX = dataPoints[i].X * absGraphSize.X + local aY = dataPoints[i].Y * absGraphSize.Y * GRAPH_Y_INNER_SCALE + local bX = dataPoints[i - 1].X * absGraphSize.X + local bY = dataPoints[i - 1].Y * absGraphSize.Y * GRAPH_Y_INNER_SCALE + + if aX ~= bX then + local vecPosX = (aX + bX) / 2 + local vecPosY = (aY + bY) / 2 + + local vecX = aX - bX + local vecY = aY - bY + + local length = math.sqrt((vecX * vecX) + (vecY * vecY)) + local rot = math.deg(math.atan2(vecY, vecX)) + + table.insert(elements, Roact.createElement("Frame", { + Size = UDim2.new(0, length, 0, LINE_WIDTH), + Position = UDim2.new(0, vecPosX - length / 2, 1 - GRAPH_Y_INNER_PADDING, -vecPosY), + BackgroundColor3 = MAIN_LINE_COLOR, + BorderSizePixel = 0, + Rotation = -rot, + })) + + table.insert(elements, Roact.createElement("Frame", { + Size = UDim2.new(0, POINT_WIDTH, 0, POINT_WIDTH), + Position = UDim2.new(0, aX, 1 - GRAPH_Y_INNER_PADDING, -aY - POINT_OFFSET), + BackgroundColor3 = MAIN_LINE_COLOR, + BorderSizePixel = 0, + })) + + if inputPosition then + local hoverLineX = inputPosition.X - absGraphPos.X + if hoverLineX < aX and bX < hoverLineX then + local aDataX = getX(dataPoints[i].data) + local bDataX = getX(dataPoints[i - 1].data) + local aDataY = getY(dataPoints[i].data) + local bDataY = getY(dataPoints[i - 1].data) + + local ratio = (hoverLineX - bX) / vecX + local hoverLineY = bY + (vecY * ratio) + + local hoverValX = (aDataX - bDataX) * ratio + bDataX + local hoverValY = (aDataY - bDataY) * ratio + bDataY + + elements["HoverDetails"] = Roact.createElement(LineGraphHoverDisplay, { + hoverLineX = hoverLineX, + hoverLineY = hoverLineY, + hoverValX = hoverValX, + hoverValY = hoverValY, + + stringFormatX = stringFormatX, + stringFormatY = stringFormatY, + }) + end + end + end + end + + if #dataPoints > 0 then + local lastEntryHeight = dataPoints[#dataPoints].Y * absGraphSize.Y * GRAPH_Y_INNER_SCALE + local currValue = getY(dataPoints[#dataPoints].data) + + elements["LatestEntryLine"] = Roact.createElement("Frame", { + Size = UDim2.new(1, TEXT_PADDING, 0, LINE_WIDTH), + Position = UDim2.new(0, -TEXT_PADDING, 1 - GRAPH_Y_INNER_PADDING, -lastEntryHeight), + BackgroundColor3 = LINE_COLOR, + BackgroundTransparency = .5, + BorderSizePixel = 0, + }) + + elements["LatestEntryText"] = Roact.createElement("TextLabel", { + Text = stringFormatY and stringFormatY(currValue) or currValue, + TextColor3 = TEXT_COLOR, + TextXAlignment = Enum.TextXAlignment.Right, + + Position = UDim2.new(0, -TEXT_PADDING - 2, 1 - GRAPH_Y_INNER_PADDING, -lastEntryHeight), + BackgroundTransparency = 1, + }) + + elements["AxisTextY0"] = Roact.createElement("TextLabel", { + Text = stringFormatY and stringFormatY(minY) or minY, + TextColor3 = TEXT_COLOR, + TextXAlignment = Enum.TextXAlignment.Right, + + Position = UDim2.new(0, -TEXT_PADDING - 2, 1 - GRAPH_Y_INNER_PADDING,0), + BackgroundTransparency = 1, + }) + + elements["AxisX"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + elements["AxisY"] = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + end + end + + return Roact.createElement("Frame", { + Size = size, + Position = pos, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + name = Roact.createElement("TextLabel", { + Text = axisLabelY, + TextColor3 = TEXT_COLOR, + TextXAlignment = Enum.TextXAlignment.Left, + + Position = UDim2.new(GRAPH_PADDING, 0, GRAPH_PADDING, -TEXT_PADDING), + BackgroundTransparency = 1, + }), + + minX = Roact.createElement("TextLabel", { + Text = stringFormatX and stringFormatX(minX) or minX, + TextColor3 = TEXT_COLOR, + TextXAlignment = Enum.TextXAlignment.Center, + + Position = UDim2.new(GRAPH_PADDING, 0, GRAPH_PADDING + GRAPH_SCALE, TEXT_PADDING), + BackgroundTransparency = 1, + }), + + maxX = Roact.createElement("TextLabel", { + Text = stringFormatX and stringFormatX(maxX) or maxX, + TextColor3 = TEXT_COLOR, + TextXAlignment = Enum.TextXAlignment.Center, + + Position = UDim2.new(GRAPH_PADDING + GRAPH_SCALE, 0, GRAPH_PADDING + GRAPH_SCALE, TEXT_PADDING), + BackgroundTransparency = 1, + }), + + axisLabelX = Roact.createElement("TextLabel", { + Text = axisLabelX, + TextColor3 = TEXT_COLOR, + TextXAlignment = Enum.TextXAlignment.Center, + + -- adding 2 to padding to push the label away from the axis line + Position = UDim2.new(.5, 0, GRAPH_PADDING + GRAPH_SCALE, 2 * TEXT_PADDING + 2), + BackgroundTransparency = 1, + }), + + graph = Roact.createElement("Frame", { + Position = UDim2.new(GRAPH_PADDING, 0, GRAPH_PADDING, 0), + Size = UDim2.new(GRAPH_SCALE, 0, GRAPH_SCALE, 0), + BackgroundTransparency = 1, + + [Roact.Ref] = self.graphRef, + + [Roact.Event.InputChanged] = self.onGraphInputChanged, + [Roact.Event.InputEnded] = self.onGraphInputEnded, + }, elements) + }) +end + +return LineGraph \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/LineGraphHoverDisplay.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/LineGraphHoverDisplay.lua new file mode 100644 index 0000000..8084fb7 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/LineGraphHoverDisplay.lua @@ -0,0 +1,56 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Constants) +local HOVER_LINE_COLOR = Constants.Color.HoverGreen +local LINE_WIDTH = Constants.GeneralFormatting.LineWidth +local TEXT_PADDING = Constants.Graph.TextPadding +local GRAPH_Y_INNER_PADDING = Constants.Graph.InnerPaddingY + +return function(props) + local hoverLineX = props.hoverLineX + local hoverLineY = props.hoverLineY + + local hoverValX = props.hoverValX + local hoverValY = props.hoverValY + + local stringFormatX = props.stringFormatX + local stringFormatY = props.stringFormatY + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + hoverLine = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 1, 0), + Position = UDim2.new(0, hoverLineX, 0, 0), + BackgroundColor3 = HOVER_LINE_COLOR, + BorderSizePixel = 0, + }), + + HoverHorizontal = Roact.createElement("Frame", { + Size = UDim2.new(0, hoverLineX + TEXT_PADDING, 0, LINE_WIDTH), + Position = UDim2.new(0, -TEXT_PADDING, 1 - GRAPH_Y_INNER_PADDING, -hoverLineY), + BackgroundColor3 = HOVER_LINE_COLOR, + BorderSizePixel = 0, + }), + + HoverTextY = Roact.createElement("TextLabel", { + Text = stringFormatY and stringFormatY(hoverValY) or hoverValY, + TextColor3 = HOVER_LINE_COLOR, + TextXAlignment = Enum.TextXAlignment.Right, + + Position = UDim2.new(0, -TEXT_PADDING - 2, 1 - GRAPH_Y_INNER_PADDING, -hoverLineY), + BackgroundTransparency = 1, + }), + + HoverTextX = Roact.createElement("TextLabel", { + Text = stringFormatX and stringFormatX(hoverValX) or hoverValX, + TextColor3 = HOVER_LINE_COLOR, + TextXAlignment = Enum.TextXAlignment.Center, + + Position = UDim2.new(0, hoverLineX, 1, TEXT_PADDING), + BackgroundTransparency = 1, + }), + }) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/LiveUpdateElement.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/LiveUpdateElement.lua new file mode 100644 index 0000000..789481a --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/LiveUpdateElement.lua @@ -0,0 +1,285 @@ +local CorePackages = game:GetService("CorePackages") +local TextService = game:GetService("TextService") +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) + +local DataConsumer = require(script.Parent.Parent.Components.DataConsumer) + +local Actions = script.Parent.Parent.Actions +local SetActiveTab = require(Actions.SetActiveTab) + +local Constants = require(script.Parent.Parent.Constants) +local TOP_BAR_FONT_SIZE = Constants.DefaultFontSize.TopBar +local TEXT_COLOR = Constants.Color.Text +local FONT = Constants.Font.TopBar + +local IMAGE_SIZE = UDim2.new(0, TOP_BAR_FONT_SIZE, 0, TOP_BAR_FONT_SIZE) + +local MEM_STAT_STR_SMALL = "Client Mem:" +local memStatStrSmallWidth = TextService:GetTextSize(MEM_STAT_STR_SMALL, TOP_BAR_FONT_SIZE, FONT, Vector2.new(0, 0)) +local MEM_STAT_STR = "Client Memory Usage:" +local memStatStrWidth = TextService:GetTextSize(MEM_STAT_STR, TOP_BAR_FONT_SIZE, FONT, Vector2.new(0, 0)) +local AVG_PING_STR = "Avg. Ping:" +local avgPingStrWidth = TextService:GetTextSize(AVG_PING_STR, TOP_BAR_FONT_SIZE, FONT, Vector2.new(0, 0)) + +-- supposed to be the calculated width of the frame, but +-- doing this for now due to time constraints. +local MIN_LARGE_FORMFACTOR_WIDTH = 380 +local INNER_PADDING = 6 + +local LiveUpdateElement = Roact.PureComponent:extend("LiveUpdateElement") + +function LiveUpdateElement:didMount() + local totalMemSignal = self.props.ClientMemoryData:totalMemSignal() + self.totalMemConnector = totalMemSignal:Connect(function(totalClientMemory) + self:setState({totalClientMemory = totalClientMemory}) + end) + + self.avgPingConnector = self.props.ServerStatsData:avgPing():Connect(function(averagePing) + self:setState({averagePing = averagePing}) + end) + + self.logWarningErrorConnector = self.props.ClientLogData:errorWarningSignal():Connect(function(error, warning) + self:setState({ + numErrors = error, + numWarnings = warning, + }) + end) +end + +function LiveUpdateElement:willUnmount() + self.totalMemConnector:Disconnect() + self.totalMemConnector = nil + + self.avgPingConnector:Disconnect() + self.avgPingConnector = nil + + self.logWarningErrorConnector:Disconnect() + self.logWarningErrorConnector = nil + +end + +function LiveUpdateElement:init() + self.onLogWarningButton = function() + local WarningFilters = { + Warning = true, + } + self.props.ClientLogData:setFilters(WarningFilters) + self.props.dispatchChangeTabClientLog() + end + + self.onLogErrorButton = function() + local ErrorFilters = { + Error = true, + } + self.props.ClientLogData:setFilters(ErrorFilters) + self.props.dispatchChangeTabClientLog() + end + + self.ref = Roact.createRef() + + self.state = { + numErrors = 0, + numWarnings = 0, + totalClientMemory = 0, + averagePing = 0, + formFactorThreshold = MIN_LARGE_FORMFACTOR_WIDTH, + } +end + +function LiveUpdateElement:render() + local size = self.props.size + local position = self.props.position + local formFactor = self.props.formFactor + + local numErrors = self.state.numErrors + local numWarnings = self.state.numWarnings + local clientMemoryUsage = self.state.totalClientMemory + local averagePing = self.state.averagePing + local formFactorThreshold = self.state.formFactorThreshold + + local useSmallForm = false + local currMemStrWidth = memStatStrWidth.X + local alignment = Enum.HorizontalAlignment.Center + + local sizeCheck + if self.ref.current then + sizeCheck = self.ref.current.AbsoluteSize.X < formFactorThreshold + end + + if formFactor == Constants.FormFactor.Small or sizeCheck then + position = position + UDim2.new(0, INNER_PADDING * 2, 0, 0) + currMemStrWidth = memStatStrSmallWidth.X + useSmallForm = true + alignment = Enum.HorizontalAlignment.Left + end + + local logErrorStat = string.format("%d", numErrors) + local logErrorStatVector = TextService:GetTextSize( + logErrorStat, + TOP_BAR_FONT_SIZE, + FONT, + Vector2.new(0, 0) + ) + + local logWarningStat = string.format("%d", numWarnings) + local logWarningStatVector = TextService:GetTextSize( + logWarningStat, + TOP_BAR_FONT_SIZE, + FONT, + Vector2.new(0, 0) + ) + + local memUsageString = string.format("%d MB", clientMemoryUsage) + local memUsageStringVector = TextService:GetTextSize( + memUsageString, + TOP_BAR_FONT_SIZE, + FONT, + Vector2.new(0, 0) + ) + + local avgPingString = string.format("%d ms", averagePing) + local avgPingStringVector = TextService:GetTextSize(avgPingString, + TOP_BAR_FONT_SIZE, + FONT, + Vector2.new(0, 0) + ) + + return Roact.createElement("Frame", { + Position = position, + Size = size, + BackgroundTransparency = 1, + + [Roact.Ref] = self.ref, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + Padding = UDim.new(0, INNER_PADDING), + HorizontalAlignment = alignment, + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + + LogErrorIcon = Roact.createElement("ImageButton", { + Image = Constants.Image.Error, + Size = IMAGE_SIZE, + BackgroundTransparency = 1, + LayoutOrder = 1, + + [Roact.Event.Activated] = self.onLogErrorButton, + }), + + LogErrorCount = Roact.createElement("TextButton", { + Text = logErrorStat, + TextSize = TOP_BAR_FONT_SIZE, + TextColor3 = TEXT_COLOR, + TextXAlignment = Enum.TextXAlignment.Left, + Font = FONT, + Size = UDim2.new(0, logErrorStatVector.X, 1, 0), + BackgroundTransparency = 1, + LayoutOrder = 2, + [Roact.Event.Activated] = self.onLogErrorButton, + }), + + ErrorWarningPad = Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = 3, + }), + + LogWarningIcon = Roact.createElement("ImageButton", { + Image = Constants.Image.Warning, + Size = IMAGE_SIZE, + BackgroundTransparency = 1, + LayoutOrder = 4, + [Roact.Event.Activated] = self.onLogWarningButton, + }), + + LogWarningCount = Roact.createElement("TextButton", { + Text = logWarningStat, + TextSize = TOP_BAR_FONT_SIZE, + TextColor3 = TEXT_COLOR, + TextXAlignment = Enum.TextXAlignment.Left, + Font = FONT, + Size = UDim2.new(0, logWarningStatVector.X, 1, 0), + BackgroundTransparency = 9, + LayoutOrder = 5, + [Roact.Event.Activated] = self.onLogWarningButton, + }), + + WarningMemoryPad = Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = 6, + }), + + MemoryUsage = Roact.createElement("TextButton", { + Text = useSmallForm and MEM_STAT_STR_SMALL or MEM_STAT_STR, + TextSize = TOP_BAR_FONT_SIZE, + TextColor3 = Constants.Color.WarningYellow, + TextXAlignment = Enum.TextXAlignment.Right, + Font = FONT, + Size = UDim2.new(0, currMemStrWidth, 1, 0), + BackgroundTransparency = 1, + LayoutOrder = 7, + [Roact.Event.Activated] = self.props.dispatchChangeTabClientMemory, + }), + + MemoryUsage_MB = Roact.createElement("TextButton", { + Text = memUsageString, + TextSize = TOP_BAR_FONT_SIZE, + TextColor3 = TEXT_COLOR, + TextXAlignment = Enum.TextXAlignment.Left, + Font = FONT, + Size = UDim2.new(0, memUsageStringVector.X, 1, 0), + BackgroundTransparency = 1, + LayoutOrder = 8, + [Roact.Event.Activated] = self.props.dispatchChangeTabClientMemory, + }), + + MemoryPingPad = Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = 9, + }), + + AvgPing = not useSmallForm and Roact.createElement("TextButton", { + Text = AVG_PING_STR, + TextSize = TOP_BAR_FONT_SIZE, + TextColor3 = Constants.Color.WarningYellow, + TextXAlignment = Enum.TextXAlignment.Right, + Font = FONT, + Size = UDim2.new(0, avgPingStrWidth.X, 1, 0), + BackgroundTransparency = 1, + LayoutOrder = 10, + [Roact.Event.Activated] = self.props.dispatchChangeTabNetworkPing, + }), + + AvgPing_ms = not useSmallForm and Roact.createElement("TextButton", { + Text = avgPingString, + TextSize = TOP_BAR_FONT_SIZE, + TextColor3 = TEXT_COLOR, + TextXAlignment = Enum.TextXAlignment.Left, + Font = FONT, + Size = UDim2.new(0, avgPingStringVector.X, 1, 0), + BackgroundTransparency = 1, + LayoutOrder = 11, + [Roact.Event.Activated] = self.props.dispatchChangeTabNetworkPing, + }) + }) +end + +local function mapDispatchToProps(dispatch) + return { + dispatchChangeTabClientLog = function() + dispatch(SetActiveTab(1, true)) + end, + dispatchChangeTabClientMemory = function() + dispatch(SetActiveTab(2, true)) + end, + dispatchChangeTabNetworkPing = function() + dispatch(SetActiveTab(6, true)) + end, + } +end + +return RoactRodux.UNSTABLE_connect2(nil, mapDispatchToProps)( + DataConsumer(LiveUpdateElement, "ServerStatsData", "ClientMemoryData", "ClientLogData" ) +) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/LiveUpdateElement.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/LiveUpdateElement.spec.lua new file mode 100644 index 0000000..d8d3705 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/LiveUpdateElement.spec.lua @@ -0,0 +1,31 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local RoactRodux = require(CorePackages.RoactRodux) + local Store = require(CorePackages.Rodux).Store + + local DataProvider = require(script.Parent.DataProvider) + local LiveUpdateElement = require(script.Parent.LiveUpdateElement) + + + it("should create and destroy without errors", function() + local store = Store.new(function() + return { + } + end) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + DataProvider = Roact.createElement(DataProvider, nil, { + LiveUpdateElement = Roact.createElement(LiveUpdateElement, { + size = UDim2.new(), + position = UDim2.new(), + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/ClientLog.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/ClientLog.lua new file mode 100644 index 0000000..1f1fa13 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/ClientLog.lua @@ -0,0 +1,23 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local DataConsumer = require(script.Parent.Parent.DataConsumer) +local LogOutput = require(script.Parent.LogOutput) + +local ClientLog = Roact.Component:extend("ClientLog") + +function ClientLog:init() + self.initClientLogData = function() + return self.props.ClientLogData:getLogData() + end +end +function ClientLog:render() + return Roact.createElement(LogOutput, { + layoutOrder = self.props.layoutOrder, + size = self.props.size, + initLogOutput = self.initClientLogData, + targetSignal = self.props.ClientLogData:Signal(), + }) +end + +return DataConsumer(ClientLog, "ClientLogData") \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/ClientLog.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/ClientLog.spec.lua new file mode 100644 index 0000000..19d5c77 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/ClientLog.spec.lua @@ -0,0 +1,16 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local DataProvider = require(script.Parent.Parent.DataProvider) + local ClientLog = require(script.Parent.ClientLog) + + it("should create and destroy without errors", function() + local element = Roact.createElement(DataProvider, {},{ + ClientLog = Roact.createElement(ClientLog) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/DevConsoleCommandLine.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/DevConsoleCommandLine.lua new file mode 100644 index 0000000..136b3a0 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/DevConsoleCommandLine.lua @@ -0,0 +1,59 @@ +local CorePackages = game:GetService("CorePackages") +local LogService = game:GetService("LogService") +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local COMMANDLINE_INDENT = Constants.LogFormatting.CommandLineIndent +local COMMANDLINE_FONTSIZE = Constants.DefaultFontSize.CommandLine +local FONT = Constants.Font.MainWindow + +local DevConsoleCommandLine = Roact.Component:extend("DevConsoleCommandLine") + +function DevConsoleCommandLine:render() + local height = self.props.height + local pos = self.props.pos + + return Roact.createElement("Frame", { + Position = pos, + Size = UDim2.new(1, 0, 0, height), + BackgroundTransparency = 0, + BackgroundColor3 = Constants.Color.TextBoxGray, + BorderColor3 = Constants.Color.BorderGray, + BorderSizePixel = 1, + }, { + Arrow = Roact.createElement("TextLabel", { + Size = UDim2.new(0, COMMANDLINE_INDENT, 1, 0), + BackgroundTransparency = 1, + TextSize = COMMANDLINE_FONTSIZE, + Font = FONT, + Text = "> ", + TextColor3 = Constants.Color.Text, + TextXAlignment = Enum.TextXAlignment.Right, + }), + + TextBox = Roact.createElement("TextBox", { + Position = UDim2.new(0, COMMANDLINE_INDENT, 0, 0), + Size = UDim2.new(1, -COMMANDLINE_INDENT, 0, height), + BackgroundTransparency = 1, + + ShowNativeInput = true, + TextColor3 = Constants.Color.Text, + TextXAlignment = 0, + TextSize = COMMANDLINE_FONTSIZE, + Text = "", + Font = FONT, + PlaceholderText = "command line", + + [Roact.Event.FocusLost] = function(rbx, enterPressed, inputThatCausedFocusLoss) + if enterPressed then + if #rbx.text > 0 then + LogService:ExecuteScript(rbx.text) + end + rbx.Text = "" + end + end, + }) + }) +end + +return DevConsoleCommandLine \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/DevConsoleCommandLine.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/DevConsoleCommandLine.spec.lua new file mode 100644 index 0000000..1853b16 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/DevConsoleCommandLine.spec.lua @@ -0,0 +1,13 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local DevConsoleCommandLine = require(script.Parent.DevConsoleCommandLine) + + it("should create and destroy without errors", function() + local element = Roact.createElement(DevConsoleCommandLine) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/LogData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/LogData.lua new file mode 100644 index 0000000..6919633 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/LogData.lua @@ -0,0 +1,266 @@ +local LogService = game:GetService("LogService") + +local Constants = require(script.Parent.Parent.Parent.Constants) +local MESSAGE_TO_TYPENAME = Constants.EnumToMsgTypeName + +local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer) +local Signal = require(script.Parent.Parent.Parent.Signal) + +-- 500 is max kept in history in C++ as of 7/2/2018 +local MAX_LOG_SIZE = tonumber(settings():GetFVariable("NewDevConsoleMaxLogCount")) +local WARNING_TO_FILTER = {"ClassDescriptor failed to learn", "EventDescriptor failed to learn", "Type failed to learn"} + +local convertTimeStamp = require(script.Parent.Parent.Parent.Util.convertTimeStamp) + +local LogData = {} +LogData.__index = LogData + +local function messageEntry(msg, timeAsStr, type) + return { + Message = msg, + Time = timeAsStr, + Type = type, + } +end + +-- MOST if not all of this code is copied from the +-- Filter "ClassDescriptor failed to learn" errors +local function ignoreWarningMessageOnAdd(message) + if message.Type ~= Enum.MessageType.MessageWarning.Value then + return false + end + local found = false + for _, filterString in ipairs(WARNING_TO_FILTER) do + if string.find(message.Message, filterString) ~= nil then + found = true + break + end + end + return found +end + +-- if a message if filtered that means we need to put it into the filtered messages +-- this is defined as the messages that we are searching for when the search +-- feature is being used +local function isMessageFiltered(message, filterTypes, filterTerm) + -- if any types are flagged on, then we need to check against it + if #filterTerm == 0 and not next(filterTypes) then + return false + end + + if next(filterTypes) then + if not filterTypes[MESSAGE_TO_TYPENAME[message.Type]] then + return false + end + end + + if #filterTerm > 0 then + if string.find(message.Message:lower(), filterTerm:lower()) == nil then + return false + end + end + + return true +end + +local function validActiveFilters(filterTypes) + local validFilters = false + for _, filterActive in pairs(filterTypes) do + validFilters = validFilters or filterActive + end + return validFilters +end + +local function filterMessages(buffer, msgIter, filterTypes, filterTerm) + buffer:reset() + + local validFilters = validActiveFilters(filterTypes) + + if #filterTerm == 0 and not validFilters then + return + end + + local counter = 0 + local msg = msgIter:next() + while msg do + if isMessageFiltered(msg, filterTypes, filterTerm) then + counter = counter + 1 + buffer:push_back(msg) + end + msg = msgIter:next() + end + + if counter == 0 then + if #filterTerm > 0 then + local errorMsg = messageEntry(string.format ("\"%s\" was not found", filterTerm), "", 0) + buffer:push_back(errorMsg) + else + local errorMsg = messageEntry("No Messages were found", "", 0) + buffer:push_back(errorMsg) + end + end +end + +function LogData:checkErrorWarningCounter(msgType) + if msgType == Enum.MessageType.MessageWarning.Value then + self._warningCount = self._warningCount + 1 + self._errorWarningSignal:Fire(self._errorCount, self._warningCount) + elseif msgType == Enum.MessageType.MessageError.Value then + self._errorCount = self._errorCount + 1 + self._errorWarningSignal:Fire(self._errorCount, self._warningCount) + end +end + +function LogData.new(isClient) + local self = {} + setmetatable(self, LogData) + + self._initialized = false + self._isClient = isClient + + self._logData = CircularBuffer.new(MAX_LOG_SIZE) + self._logDataSearched = CircularBuffer.new(MAX_LOG_SIZE) + self._searchTerm = "" + self._filters = {} + self._errorCount = isClient and 0 + self._warningCount = isClient and 0 + self._logDataUpdate = Signal.new() + self._errorWarningSignal = isClient and Signal.new() + + return self +end + +function LogData:Signal() + return self._logDataUpdate +end + +function LogData:errorWarningSignal() + return self._errorWarningSignal +end + +function LogData:setSearchTerm(targetSearchTerm) + if self._searchTerm ~= targetSearchTerm then + self._searchTerm = targetSearchTerm + + if self._searchTerm == "" then + self._logDataSearched:reset() + self._logDataUpdate:Fire(self._logData) + else + filterMessages( + self._logDataSearched, + self._logData:iterator(), + self._filters, + self._searchTerm + ) + self._logDataUpdate:Fire(self._logDataSearched) + end + end +end + +function LogData:getSearchTerm() + return self._searchTerm +end + +function LogData:setFilters(filters) + self._filters = filters + + if not validActiveFilters(filters) then + self._logDataSearched:reset() + self._logDataUpdate:Fire(self._logData) + return + end + + filterMessages( + self._logDataSearched, + self._logData:iterator(), + self._filters, + self._searchTerm + ) + self._logDataUpdate:Fire(self._logDataSearched) +end + +function LogData:getLogData() + return self._logData +end + +function LogData:getErrorWarningCount() + return self._errorCount, self._warningCount +end + +function LogData:start() + if self._isClient then + if not self._initialized then + self._initialized = true + local Messages = {} + if #Messages == 0 then + local history = LogService:GetLogHistory() + for _, msg in ipairs(history) do + local message = messageEntry( + msg.message or "[DevConsole Error 1]", + convertTimeStamp(msg.timestamp), + msg.messageType.Value + ) + if not ignoreWarningMessageOnAdd(message) then + self:checkErrorWarningCounter(msg.messageType.Value) + self._logData:push_back(message) + end + end + end + end + + self._connection = LogService.MessageOut:connect(function(text, messageType) + local message = messageEntry( + text or "[DevConsole Error 2]", + convertTimeStamp(os.time()), + messageType.Value + ) + + if not ignoreWarningMessageOnAdd(message) then + self:checkErrorWarningCounter(messageType.Value) + self._logData:push_back(message) + + if #self._logDataSearched:getData() > 0 then + if isMessageFiltered(message, self._filters, self._searchTerm) then + self._logDataSearched:push_back(message) + self._logDataUpdate:Fire(self._logDataSearched) + end + else + self._logDataUpdate:Fire(self._logData) + end + end + end) + else + self._connection = LogService.ServerMessageOut:connect(function(text, messageType, timestamp) + local message = messageEntry( + text or "[DevConsole Error 3]", + convertTimeStamp(timestamp), + messageType.Value + ) + + if not ignoreWarningMessageOnAdd(message) then + self._logData:push_back(message) + + if #self._logDataSearched:getData() > 0 then + if isMessageFiltered(message, self._Filters, self._SearchTerm) then + self._logDataSearched:push_back(message) + self._logDataUpdate:Fire(self._logDataSearched) + end + else + self._logDataUpdate:Fire(self._logData) + end + end + end) + + LogService:RequestServerOutput() + end +end + +function LogData:stop() + self._initialized = false + + if self._connection then + self._connection:Disconnect() + end +end + +return LogData \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/LogOutput.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/LogOutput.lua new file mode 100644 index 0000000..e3288b9 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/LogOutput.lua @@ -0,0 +1,197 @@ +local CorePackages = game:GetService("CorePackages") +local TextService = game:GetService("TextService") +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local FONT_SIZE = Constants.DefaultFontSize.MainWindow +local FONT = Constants.Font.MainWindow +local ICON_PADDING = Constants.LogFormatting.IconHeight +local FRAME_HEIGHT = Constants.LogFormatting.TextFrameHeight +local LINE_PADDING = Constants.LogFormatting.TextFramePadding + +local MAX_STRING_SIZE = 16384 +local MAX_STR_MSG = "Could not display entire %d character message because message exceeds max displayable length of %d" + +local LogOutput = Roact.Component:extend("LogOutput") + +function LogOutput:init(props) + local initLogOutput = props.initLogOutput and props.initLogOutput() + + self.onCanvasPosChanged = function() + local canvasPos = self.ref.current.CanvasPosition + if self.state.canvasPos ~= canvasPos then + self:setState({ + canvasPos = canvasPos, + }) + end + end + + self.ref = Roact.createRef() + + self.state = { + logData = initLogOutput, + } +end + +function LogOutput:willUpdate() + self._canvasSignal:Disconnect() +end + +function LogOutput:didUpdate() + self._canvasSignal = self.ref.current:GetPropertyChangedSignal("CanvasPosition"):Connect(self.onCanvasPosChanged) + + if self.state.absSize ~= self.ref.current.AbsoluteSize then + self:setState({ + absSize = self.ref.current.AbsoluteSize, + }) + end +end + +function LogOutput:didMount() + self.logConnector = self.props.targetSignal:Connect(function(data) + self:setState({ + logData = data + }) + end) + + + self._canvasSignal = self.ref.current:GetPropertyChangedSignal("CanvasPosition"):Connect(self.onCanvasPosChanged) + + self:setState({ + absSize = self.ref.current.AbsoluteSize, + canvasPos = self.ref.current.CanvasPosition, + }) +end + +function LogOutput:willUnmount() + self.logConnector:Disconnect() + self.logConnector = nil +end + +function LogOutput:render() + local layoutOrder = self.props.layoutOrder + local size = self.props.size + + local logData = self.state.logData + local absSize = self.state.absSize + local canvasPos = self.state.canvasPos + + if self.ref.current then + canvasPos = self.ref.current.CanvasPosition + end + + local elements = {} + + local messageCount = 1 + local scrollingFrameHeight = 0 + + if self.ref.current and logData then + -- FRAME_HEIGHT is used to offset the text for the icon + local maxSize = Vector2.new(absSize.X - FRAME_HEIGHT, 1000000) + local paddingHeight = -1 + local usedFrameSpace = 0 + + local msgIter = logData:iterator() + local message = msgIter:next() + while message do + local fmtMessage + if #message.Message < MAX_STRING_SIZE then + fmtMessage = string.format("%s -- %s", message.Time, message.Message) + + else + fmtMessage = string.format("%s -- %s", message.Time, string.sub(message.Message, 1, MAX_STRING_SIZE)) + end + + local msgDims = TextService:GetTextSize(fmtMessage, FONT_SIZE, FONT, maxSize) + messageCount = messageCount + 1 + + if scrollingFrameHeight + msgDims.Y >= canvasPos.Y then + if usedFrameSpace < absSize.Y then + local color = Constants.Color.Text + local image = "" + + if message.Type == Enum.MessageType.MessageOutput.Value then + color = Constants.Color.Text + elseif message.Type == Enum.MessageType.MessageInfo.Value then + color = Constants.Color.HighlightBlue + image = Constants.Image.Info + elseif message.Type == Enum.MessageType.MessageWarning.Value then + color = Constants.Color.WarningYellow + image = Constants.Image.Warning + elseif message.Type == Enum.MessageType.MessageError.Value then + color = Constants.Color.ErrorRed + image = Constants.Image.Errors + end + + elements[messageCount] = Roact.createElement("Frame",{ + Size = UDim2.new(1, 0, 0, msgDims.Y), + BackgroundTransparency = 1, + LayoutOrder = messageCount, + }, { + image = Roact.createElement("ImageLabel",{ + Image = image, + Size = UDim2.new(0, ICON_PADDING , 0, ICON_PADDING), + Position = UDim2.new(0, ICON_PADDING / 4, .5, -ICON_PADDING / 2), + BackgroundTransparency = 1, + }), + msg = Roact.createElement("TextLabel",{ + Text = fmtMessage, + TextColor3 = color, + TextSize = FONT_SIZE, + Font = FONT, + TextXAlignment = Enum.TextXAlignment.Left, + + TextWrapped = true, + + Size = UDim2.new(0, msgDims.X, 0, msgDims.Y), + Position = UDim2.new(0, FRAME_HEIGHT, 0, 0), + BackgroundTransparency = 1, + }) + }) + end + if paddingHeight < 0 then + paddingHeight = scrollingFrameHeight + else + usedFrameSpace = usedFrameSpace + msgDims.Y + LINE_PADDING + end + end + + scrollingFrameHeight = scrollingFrameHeight + msgDims.Y + LINE_PADDING + + if #message.Message < MAX_STRING_SIZE then + message = msgIter:next() + else + message = { + Message = string.format(MAX_STR_MSG, #message.Message, MAX_STRING_SIZE), + Time = "", + Type = message.Type, + } + end + end + elements["UIListLayout"] = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, LINE_PADDING), + }) + + elements["WindowingPadding"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, paddingHeight), + BackgroundTransparency = 1, + LayoutOrder = 1, + }) + end + + return Roact.createElement("ScrollingFrame",{ + Size = size, + BackgroundTransparency = 1, + VerticalScrollBarInset = 1, + ScrollBarThickness = 6, + CanvasSize = UDim2.new(1, 0, 0, scrollingFrameHeight), + LayoutOrder = layoutOrder, + + [Roact.Ref] = self.ref, + }, elements) +end + +return LogOutput \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/LogOutput.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/LogOutput.spec.lua new file mode 100644 index 0000000..84d46b3 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/LogOutput.spec.lua @@ -0,0 +1,16 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local Signal = require(script.Parent.Parent.Parent.Signal) + local LogOutput = require(script.Parent.LogOutput) + + it("should create and destroy without errors", function() + local element = Roact.createElement(LogOutput,{ + targetSignal = Signal.new() + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/MainViewLog.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/MainViewLog.lua new file mode 100644 index 0000000..7cbf571 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/MainViewLog.lua @@ -0,0 +1,154 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) + +local Components = script.Parent.Parent.Parent.Components +local ClientLog = require(Components.Log.ClientLog) +local ServerLog = require(Components.Log.ServerLog) +local UtilAndTab = require(Components.UtilAndTab) +local DataConsumer = require(Components.DataConsumer) + +local Actions = script.Parent.Parent.Parent.Actions +local ClientLogUpdateSearchFilter = require(Actions.ClientLogUpdateSearchFilter) +local ServerLogUpdateSearchFilter = require(Actions.ServerLogUpdateSearchFilter) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local PADDING = Constants.GeneralFormatting.MainRowPadding + +local MsgTypeNamesOrdered = Constants.MsgTypeNamesOrdered + +local MainViewLog = Roact.PureComponent:extend("MainViewLog") + +function MainViewLog:init() + self.onUtilTabHeightChanged = function(utilTabHeight) + self:setState({ + utilTabHeight = utilTabHeight + }) + end + + self.onClientButton = function() + self:setState({isClientView = true}) + end + + self.onServerButton = function() + self:setState({isClientView = false}) + end + + self.onCheckBoxesChanged = function(newFilters) + if self.state.isClientView then + self.props.ClientLogData:setFilters(newFilters) + else + self.props.ServerLogData:setFilters(newFilters) + end + end + + self.onSearchTermChanged = function(newSearchTerm) + if self.state.isClientView then + self.props.ClientLogData:setSearchTerm(newSearchTerm) + else + self.props.ServerLogData:setSearchTerm(newSearchTerm) + end + end + + self.utilRef = Roact.createRef() + + self.state = { + utilTabHeight = 0, + isClientView = true + } +end + +function MainViewLog:didMount() + local utilSize = self.utilRef.current.Size + self:setState({ + utilTabHeight = utilSize.Y.Offset + }) +end + +function MainViewLog:didUpdate() + local utilSize = self.utilRef.current.Size + if utilSize.Y.Offset ~= self.state.utilTabHeight then + self:setState({ + utilTabHeight = utilSize.Y.Offset + }) + end +end + +function MainViewLog:render() + local size = self.props.size + local formFactor = self.props.formFactor + local isdeveloperView = self.props.isdeveloperView + local tabList = self.props.tabList + + local utilTabHeight = self.state.utilTabHeight + local isClientView = self.state.isClientView + + local searchTerm + if isClientView then + searchTerm = self.props.ClientLogData:getSearchTerm() + else + searchTerm = self.props.ServerLogData:getSearchTerm() + end + + local elements = {} + + elements["UIListLayout"] = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, PADDING), + }) + + elements ["UtilAndTab"] = Roact.createElement(UtilAndTab,{ + windowWidth = size.X.Offset, + formFactor = formFactor, + tabList = tabList, + checkBoxNames = MsgTypeNamesOrdered, + isClientView = isClientView, + searchTerm = searchTerm, + layoutOrder = 1, + + refForParent = self.utilRef, + + onClientButton = isdeveloperView and self.onClientButton, + onServerButton = isdeveloperView and self.onServerButton, + onCheckBoxesChanged = self.onCheckBoxesChanged, + onSearchTermChanged = self.onSearchTermChanged, + }) + + if utilTabHeight > 0 then + if isClientView then + elements["ClientLog"] = Roact.createElement(ClientLog, { + size = UDim2.new(1, 0, 1, -utilTabHeight), + layoutOrder = 2, + }) + else + elements["ServerLog"] = Roact.createElement(ServerLog, { + size = UDim2.new(1, 0, 1, -utilTabHeight), + layoutOrder = 2, + }) + end + end + + return Roact.createElement("Frame",{ + Size = size, + BackgroundColor3 = Constants.Color.BaseGray, + BackgroundTransparency = 1, + LayoutOrder = 3, + + }, elements) +end + +local function mapDispatchToProps(dispatch) + return { + dispatchClientLogUpdateSearchFilter = function(searchTerm, filters) + dispatch(ClientLogUpdateSearchFilter(searchTerm, filters)) + end, + + dispatchServerLogUpdateSearchFilter = function(searchTerm, filters) + dispatch(ServerLogUpdateSearchFilter(searchTerm, filters)) + end, + } +end + +return RoactRodux.UNSTABLE_connect2(nil, mapDispatchToProps)( + DataConsumer(MainViewLog, "ClientLogData", "ServerLogData") +) diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/MainViewLog.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/MainViewLog.spec.lua new file mode 100644 index 0000000..b001a47 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/MainViewLog.spec.lua @@ -0,0 +1,40 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local RoactRodux = require(CorePackages.RoactRodux) + local Store = require(CorePackages.Rodux).Store + + local DataProvider = require(script.Parent.Parent.DataProvider) + + local MainViewLog = require(script.Parent.MainViewLog) + + it("should create and destroy without errors", function() + local store = Store.new(function() + return { + MainView = { + currTabIndex = 0 + }, + LogData = { + clientSearchTerm = "", + clientTypeFilters = {}, + serverSearchTerm = "", + serverTypeFilters = {}, + } + } + end) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + DataProvider = Roact.createElement(DataProvider, {},{ + MainViewLog = Roact.createElement(MainViewLog,{ + size = UDim2.new(), + tabList = {}, + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/ServerLog.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/ServerLog.lua new file mode 100644 index 0000000..0c6d0de --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/ServerLog.lua @@ -0,0 +1,32 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local DataConsumer = require(script.Parent.Parent.DataConsumer) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local COMMANDLINE_HEIGHT = Constants.LogFormatting.CommandLineHeight + +local DevConsoleCommandLine = require(script.Parent.DevConsoleCommandLine) +local LogOutput = require(script.Parent.LogOutput) + +local ServerLog = Roact.Component:extend("ServerLog") + +function ServerLog:render() + return Roact.createElement("Frame", { + Size = self.props.size, + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + }, { + Scroll = Roact.createElement(LogOutput, { + size = UDim2.new(1, 0 , 1, -COMMANDLINE_HEIGHT), + targetSignal = self.props.ServerLogData:Signal(), + }), + + CommandLine = Roact.createElement(DevConsoleCommandLine, { + pos = UDim2.new(0,0,1,-COMMANDLINE_HEIGHT), + height = COMMANDLINE_HEIGHT + }), + }) +end + +return DataConsumer(ServerLog, "ServerLogData") \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/ServerLog.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/ServerLog.spec.lua new file mode 100644 index 0000000..e958a18 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Log/ServerLog.spec.lua @@ -0,0 +1,16 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local DataProvider = require(script.Parent.Parent.DataProvider) + local ServerLog = require(script.Parent.ServerLog) + + it("should create and destroy without errors", function() + local element = Roact.createElement(DataProvider, {},{ + ServerLog = Roact.createElement(ServerLog) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ClientMemory.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ClientMemory.lua new file mode 100644 index 0000000..cafa5a5 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ClientMemory.lua @@ -0,0 +1,19 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Components = script.Parent.Parent.Parent.Components +local DataConsumer = require(Components.DataConsumer) +local MemoryView = require(Components.Memory.MemoryView) + +local ClientMemory = Roact.Component:extend("ClientMemory") + +function ClientMemory:render() + return Roact.createElement(MemoryView,{ + layoutOrder = self.props.layoutOrder, + size = self.props.size, + searchTerm = self.props.searchTerm, + targetMemoryData = self.props.ClientMemoryData, + }) +end + +return DataConsumer(ClientMemory, "ClientMemoryData") \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ClientMemory.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ClientMemory.spec.lua new file mode 100644 index 0000000..ef4663b --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ClientMemory.spec.lua @@ -0,0 +1,16 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local DataProvider = require(script.Parent.Parent.DataProvider) + local ClientMemory = require(script.Parent.ClientMemory) + + it("should create and destroy without errors", function() + local element = Roact.createElement(DataProvider, {},{ + ClientMemory = Roact.createElement(ClientMemory) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ClientMemoryData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ClientMemoryData.lua new file mode 100644 index 0000000..c42c0d7 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ClientMemoryData.lua @@ -0,0 +1,186 @@ +local Signal = require(script.Parent.Parent.Parent.Signal) + +local StatsService = game:GetService("Stats") +local StatsUtils = require(script.Parent.Parent.Parent.Parent.Stats.StatsUtils) + +local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer) +local Constants = require(script.Parent.Parent.Parent.Constants) +local HEADER_NAMES = Constants.MemoryFormatting.ChartHeaderNames + +local MAX_DATASET_COUNT = tonumber(settings():GetFVariable("NewDevConsoleMaxGraphCount")) +local CLIENT_POLLING_INTERVAL = 3 -- seconds + +local SORT_COMPARATOR = { + [HEADER_NAMES[1]] = function(a, b) + return a.name < b.name + end, + [HEADER_NAMES[2]] = function(a, b) + return a.dataStats.dataSet:back().data < b.dataStats.dataSet:back().data + end, +} + +local ClientMemoryData = {} +ClientMemoryData.__index = ClientMemoryData + +function ClientMemoryData.new() + local self = {} + setmetatable(self, ClientMemoryData) + + self._pollingId = 0 + self._totalMemory = 0 + self._memoryData = {} + self._memoryDataSorted = {} + self._treeViewUpdatedSignal = Signal.new() + self._totalMemoryUpdated = Signal.new() + self._sortType = HEADER_NAMES[1] + + return self +end + +local function GetMemoryPerformanceStatsItem() + local performanceStats = StatsService and StatsService:FindFirstChild("PerformanceStats") + if not performanceStats then + return nil + end + local memoryStats = performanceStats:FindFirstChild( + StatsUtils.StatNames[StatsUtils.StatType_Memory]) + return memoryStats +end + + +function ClientMemoryData:recursiveUpdateEntry(entryList, sortedList, statsItem) + local name = StatsUtils.GetMemoryAnalyzerStatName(statsItem.Name) + local data = statsItem:GetValue() + + local children = statsItem:GetChildren() + + if not entryList[name] then + local newBuffer = CircularBuffer.new(MAX_DATASET_COUNT) + + newBuffer:push_back({ + data = data, + time = self._lastUpdate + }) + + entryList[name] = { + min = data, + max = data, + dataSet = newBuffer, + children = #children > 0 and {}, + sortedChildren = #children > 0 and {}, + } + + local newEntry = { + name = name, + dataStats = entryList[name] + } + + table.insert(sortedList, newEntry) + else + local currMax = entryList[name].max + local currMin = entryList[name].min + + local update = { + data = data, + time = self._lastUpdate + } + + local overwrittenEntry = entryList[name].dataSet:push_back(update) + + if overwrittenEntry then + local iter = entryList[name].dataSet:iterator() + local dat = iter:next() + if currMax == overwrittenEntry.data then + currMax = currMin + while dat do + currMax = dat.data < currMax and currMax or dat.data + dat = iter:next() + end + end + if currMin == overwrittenEntry.data then + currMin = currMax + while dat do + currMin = currMin < dat.data and currMin or dat.data + dat = iter:next() + end + end + end + + entryList[name].max = currMax < data and data or currMax + entryList[name].min = currMin < data and currMin or data + end + + for _, childStatItem in ipairs(children) do + self:recursiveUpdateEntry( + entryList[name].children, + entryList[name].sortedChildren, + childStatItem + ) + end +end + +function ClientMemoryData:totalMemSignal() + return self._totalMemoryUpdated +end + +function ClientMemoryData:treeUpdatedSignal() + return self._treeViewUpdatedSignal +end + +function ClientMemoryData:getSortType() + return self._sortType +end + +local function recursiveSort(memoryDataSort, comparator) + table.sort(memoryDataSort, comparator) + for _, entry in pairs(memoryDataSort) do + if entry.dataStats.sortedChildren then + recursiveSort(entry.dataStats.sortedChildren, comparator) + end + end +end + +function ClientMemoryData:setSortType(sortType) + if SORT_COMPARATOR[sortType] then + self._sortType = sortType + -- do we need a mutex type thing here? + recursiveSort(self._memoryDataSorted, SORT_COMPARATOR[self._sortType]) + else + error(string.format("attempted to pass invalid sortType: %s", tostring(sortType)), 2) + end +end + +function ClientMemoryData:getMemoryData() + return self._memoryDataSorted +end + +function ClientMemoryData:start() + spawn(function() + self._pollingId = self._pollingId + 1 + local instanced_pollingId = self._pollingId + while instanced_pollingId == self._pollingId do + local statsItem = GetMemoryPerformanceStatsItem() + if not statsItem then + return nil + end + self._lastUpdate = os.time() + self:recursiveUpdateEntry(self._memoryData, self._memoryDataSorted, statsItem) + + if self._totalMemory ~= statsItem:getValue() then + self._totalMemory = statsItem:getValue() + self._totalMemoryUpdated:Fire(self._totalMemory) + end + + self._treeViewUpdatedSignal:Fire(self._memoryDataSorted) + + wait(CLIENT_POLLING_INTERVAL) + end + end) +end + +function ClientMemoryData:stop() + -- listeners are responsible for disconnecting themselves + self._pollingId = self._pollingId + 1 +end + +return ClientMemoryData \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MainViewMemory.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MainViewMemory.lua new file mode 100644 index 0000000..9715baf --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MainViewMemory.lua @@ -0,0 +1,146 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) + +local Components = script.Parent.Parent.Parent.Components +local ClientMemory = require(Components.Memory.ClientMemory) +local ServerMemory = require(Components.Memory.ServerMemory) +local UtilAndTab = require(Components.UtilAndTab) + +local Actions = script.Parent.Parent.Parent.Actions +local ClientMemoryUpdateSearchFilter = require(Actions.ClientMemoryUpdateSearchFilter) +local ServerMemoryUpdateSearchFilter = require(Actions.ServerMemoryUpdateSearchFilter) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local MAIN_ROW_PADDING = Constants.GeneralFormatting.MainRowPadding + +local MainViewMemory = Roact.Component:extend("MainViewMemory") + +function MainViewMemory:init() + self.onUtilTabHeightChanged = function(utilTabHeight) + self:setState({ + utilTabHeight = utilTabHeight + }) + end + + self.onClientButton = function() + self:setState({isClientView = true}) + end + + self.onServerButton = function() + self:setState({isClientView = false}) + end + + self.onSearchTermChanged = function(newSearchTerm) + if self.state.isClientView then + self.props.dispatchClientMemoryUpdateSearchFilter(newSearchTerm, {}) + else + self.props.dispatchServerMemoryUpdateSearchFilter(newSearchTerm, {}) + end + end + + self.utilRef = Roact.createRef() + + self.state = { + utilTabHeight = 0, + isClientView = true, + } +end + +function MainViewMemory:didMount() + local utilSize = self.utilRef.current.Size + self:setState({ + utilTabHeight = utilSize.Y.Offset, + }) +end + +function MainViewMemory:didUpdate() + local utilSize = self.utilRef.current.Size + local height = utilSize.Y.Offset + + if height ~= self.state.utilTabHeight then + self:setState({ + utilTabHeight = height, + }) + end +end + +function MainViewMemory:render() + local elements = {} + local size = self.props.size + local isdeveloperView = self.props.isdeveloperView + local formFactor = self.props.formFactor + local tabList = self.props.tabList + + local utilTabHeight = self.state.utilTabHeight + local isClientView = self.state.isClientView + local searchTerm = isClientView and self.props.clientSearchTerm or self.props.serverSearchTerm + + elements["UIListLayout"] = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, MAIN_ROW_PADDING), + }) + + elements ["UtilAndTab"] = Roact.createElement(UtilAndTab,{ + windowWidth = size.X.Offset, + formFactor = formFactor, + tabList = tabList, + isClientView = isClientView, + searchTerm = searchTerm, + layoutOrder = 1, + + refForParent = self.utilRef, + + onHeightChanged = self.onUtilTabHeightChanged, + onClientButton = isdeveloperView and self.onClientButton, + onServerButton = isdeveloperView and self.onServerButton, + onSearchTermChanged = self.onSearchTermChanged, + }) + + + if utilTabHeight > 0 then + if isClientView then + elements["ClientMemory"] = Roact.createElement(ClientMemory, { + size = UDim2.new(1, 0, 1, -utilTabHeight), + searchTerm = searchTerm, + layoutOrder = 2, + }) + else + elements["ServerMemory"] = Roact.createElement(ServerMemory, { + size = UDim2.new(1, 0, 1, -utilTabHeight), + searchTerm = searchTerm, + layoutOrder = 2, + }) + end + end + + return Roact.createElement("Frame",{ + Size = size, + BackgroundColor3 = Constants.Color.BaseGray, + BackgroundTransparency = 1, + LayoutOrder = 3, + }, elements) +end + +local function mapStateToProps(state, props) + return { + clientSearchTerm = state.MemoryData.clientSearchTerm, + clientTypeFilters = state.MemoryData.clientTypeFilters, + serverSearchTerm = state.MemoryData.serverSearchTerm, + serverTypeFilters = state.MemoryData.serverTypeFilters, + } +end + +local function mapDispatchToProps(dispatch) + return { + dispatchClientMemoryUpdateSearchFilter = function(searchTerm, filters) + dispatch(ClientMemoryUpdateSearchFilter(searchTerm, filters)) + end, + + dispatchServerMemoryUpdateSearchFilter = function(searchTerm, filters) + dispatch(ServerMemoryUpdateSearchFilter(searchTerm, filters)) + end, + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(MainViewMemory) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MainViewMemory.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MainViewMemory.spec.lua new file mode 100644 index 0000000..0ba078c --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MainViewMemory.spec.lua @@ -0,0 +1,39 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local RoactRodux = require(CorePackages.RoactRodux) + local Store = require(CorePackages.Rodux).Store + + local DataProvider = require(script.Parent.Parent.DataProvider) + local MainViewMemory = require(script.Parent.MainViewMemory) + + it("should create and destroy without errors", function() + local store = Store.new(function() + return { + MainView = { + currTabIndex = 0 + }, + MemoryData = { + clientSearchTerm = "", + clientTypeFilters = {}, + serverSearchTerm = "", + serverTypeFilters = {}, + } + } + end) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + DataProvider = Roact.createElement(DataProvider, {},{ + MainViewMemory = Roact.createElement(MainViewMemory,{ + size = UDim2.new(), + tabList = {}, + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MemoryView.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MemoryView.lua new file mode 100644 index 0000000..0e51a95 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MemoryView.lua @@ -0,0 +1,281 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Components = script.Parent.Parent.Parent.Components +local HeaderButton = require(Components.HeaderButton) +local MemoryViewEntry = require(script.Parent.MemoryViewEntry) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local LINE_WIDTH = Constants.GeneralFormatting.LineWidth +local LINE_COLOR = Constants.GeneralFormatting.LINE_COLOR + +local HEADER_NAMES = Constants.MemoryFormatting.ChartHeaderNames +local HEADER_HEIGHT = Constants.GeneralFormatting.HeaderFrameHeight +local VALUE_CELL_WIDTH = Constants.MemoryFormatting.ValueCellWidth +local CELL_PADDING = Constants.MemoryFormatting.CellPadding +local VALUE_PADDING = Constants.MemoryFormatting.ValuePadding + +local ENTRY_HEIGHT = Constants.GeneralFormatting.EntryFrameHeight +local GRAPH_HEIGHT = Constants.GeneralFormatting.LineGraphHeight + +local MemoryView = Roact.Component:extend("MemoryView") + +local function getX(entry) + return entry.time +end + +local function getY(entry) + return entry.data +end + +local function formatValueStr(value) + return string.format("%.3f", value) +end + +function MemoryView:init(props) + self.getOnButtonPress = function (name) + return function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or + (input.UserInputType == Enum.UserInputType.Touch and + input.UserInputState == Enum.UserInputState.End) then + + self:setState({ + expandIndex = self.state.expandIndex ~= name and name + }) + end + end + end + + self.onSortChanged = function(sortType) + local currSortType = props.targetMemoryData:getSortType() + if sortType == currSortType then + self:setState({ + reverseSort = not self.state.reverseSort + }) + else + props.targetMemoryData:setSortType(sortType) + self:setState({ + reverseSort = false, + }) + end + end + + self.onCanvasPosChanged = function() + local canvasPos = self.scrollingRef.current.CanvasPosition + if self.state.canvasPos ~= canvasPos then + self:setState({ + absScrollSize = self.scrollingRef.current.AbsoluteSize, + canvasPos = canvasPos, + }) + end + end + + self.scrollingRef = Roact.createRef() + + self.state = { + memoryData = props.targetMemoryData:getMemoryData(), + reverseSort = false, + expandIndex = false, + } +end + +function MemoryView:willUpdate() + if self.canvasPosConnector then + self.canvasPosConnector:Disconnect() + end +end + +function MemoryView:didUpdate() + if self.scrollingRef.current then + local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition") + self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged) + end +end + +function MemoryView:didMount() + local treeUpdatedSignal = self.props.targetMemoryData:treeUpdatedSignal() + self.treeViewItemConnector = treeUpdatedSignal:Connect(function(memoryData) + self:setState({ + memoryData = memoryData + }) + end) + + if self.scrollingRef.current then + local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition") + self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged) + + self:setState({ + absScrollSize = self.scrollingRef.current.AbsoluteSize, + canvasPos = self.scrollingRef.current.CanvasPosition, + }) + end +end + +function MemoryView:willUnmount() + self.treeViewItemConnector:Disconnect() +end + +function MemoryView:recursiveConstructEntries(elements, entry, depth, windowing) + assert(self.scrollingRef.current, "ScrollingFrame not initialized yet") + + local expandIndex = self.state.expandIndex + local searchTerm = self.props.searchTerm + local reverseSort = self.state.reverseSort + local canvasPos = self.scrollingRef.current.CanvasPosition + local absScrollSize = self.scrollingRef.current.AbsoluteSize + + local name = entry.name + + local found = string.find(name:lower(), searchTerm:lower()) + + if found then + local showGraph = expandIndex == name + local frameHeight = showGraph and ENTRY_HEIGHT + GRAPH_HEIGHT or ENTRY_HEIGHT + + if windowing.scrollingFrameHeight + frameHeight >= canvasPos.Y then + if windowing.usedFrameSpace < absScrollSize.Y then + windowing.layoutOrder = windowing.layoutOrder + 1 + + elements[name] = Roact.createElement(MemoryViewEntry, { + size = UDim2.new(1, 0, 0, frameHeight), + depth = depth, + entry = entry, + showGraph = showGraph, + + onButtonPress = self.getOnButtonPress(name), + formatValueStr = formatValueStr, + getX = getX, + getY = getY, + + layoutOrder = windowing.layoutOrder, + }) + end + if windowing.paddingHeight < 0 then + windowing.paddingHeight = windowing.scrollingFrameHeight + else + windowing.usedFrameSpace = windowing.usedFrameSpace + frameHeight + end + end + windowing.scrollingFrameHeight = windowing.scrollingFrameHeight + frameHeight + end + + local sortedChildren = entry.dataStats.sortedChildren + if sortedChildren then + if reverseSort then + local totalChildren = #sortedChildren + for i = 1, totalChildren do + self:recursiveConstructEntries(elements, sortedChildren[totalChildren - i + 1], depth + 1, windowing) + end + else + for _, entry in ipairs(sortedChildren) do + self:recursiveConstructEntries(elements, entry, depth + 1, windowing) + end + end + end +end + +function MemoryView:render() + local elements = {} + local layoutOrder = self.props.layoutOrder + local size = self.props.size + + -- we pass this table into the recursion to keep sum up + -- height totals for windowing + local windowingInfo = { + scrollingFrameHeight = 0, + paddingHeight = -1, + usedFrameSpace = 0, + layoutOrder = 1 + } + + if self.scrollingRef.current then + for _, entry in ipairs(self.state.memoryData) do + self:recursiveConstructEntries(elements, entry, 0, windowingInfo) + end + + elements["UIListLayout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + elements["WindowingPadding"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, windowingInfo.paddingHeight), + BackgroundTransparency = 1, + LayoutOrder = 1, + }) + end + + if layoutOrder == 1 then + return Roact.createElement("TextLabel",{ + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + Text = "Awaiting Memory Stats", + TextColor3 = Constants.Color.Text, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + },{ + Entries = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, 0, 1, -HEADER_HEIGHT), + BackgroundTransparency = 1, + + [Roact.Ref] = self.scrollingRef, + }), + }) + end + + return Roact.createElement("Frame",{ + Size = size, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + },{ + Header = Roact.createElement("Frame",{ + Size = UDim2.new(1, 0, 0, HEADER_HEIGHT), + BackgroundTransparency = 1, + },{ + Name = Roact.createElement(HeaderButton, { + text = HEADER_NAMES[1], + size = UDim2.new(VALUE_CELL_WIDTH, CELL_PADDING, 0, HEADER_HEIGHT), + pos = UDim2.new(0, CELL_PADDING, 0, 0), + sortfunction = self.onSortChanged, + }), + ValueMB = Roact.createElement(HeaderButton, { + text = HEADER_NAMES[2], + size = UDim2.new(1 - VALUE_CELL_WIDTH, -VALUE_PADDING, 0, HEADER_HEIGHT), + pos = UDim2.new(1 - VALUE_CELL_WIDTH, VALUE_PADDING, 0, 0), + sortfunction = self.onSortChanged, + }), + TopHorizontal = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 1), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + LowerHorizontal = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + vertical = Roact.createElement("Frame",{ + Size = UDim2.new(0, LINE_WIDTH, 1, 0), + Position = UDim2.new(1 - VALUE_CELL_WIDTH, 0, 0, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + }), + + Entries = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, 0, 1, -HEADER_HEIGHT), + Position = UDim2.new(0, 0, 0, HEADER_HEIGHT), + BackgroundTransparency = 1, + VerticalScrollBarInset = 1, + ScrollBarThickness = 5, + CanvasSize = UDim2.new(1, 0, 0, windowingInfo.scrollingFrameHeight), + + [Roact.Ref] = self.scrollingRef, + }, elements), + }) +end + +return MemoryView \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MemoryView.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MemoryView.spec.lua new file mode 100644 index 0000000..9579c7e --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MemoryView.spec.lua @@ -0,0 +1,30 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local Signal = require(script.Parent.Parent.Parent.Signal) + + local MemoryView = require(script.Parent.MemoryView) + + local dummmyMemoryData = { + getMemoryData = function () + return { + summaryTable = {}, + summaryCount = 0, + entryList = nil, + } + end, + treeUpdatedSignal = function () + return Signal.new() + end, + } + + it("should create and destroy without errors", function() + local element = Roact.createElement(MemoryView,{ + targetMemoryData = dummmyMemoryData, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MemoryViewEntry.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MemoryViewEntry.lua new file mode 100644 index 0000000..6fa1393 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MemoryViewEntry.lua @@ -0,0 +1,105 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Components = script.Parent.Parent.Parent.Components +local CellLabel = require(Components.CellLabel) +local BannerButton = require(Components.BannerButton) +local LineGraph = require(Components.LineGraph) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local LINE_WIDTH = Constants.GeneralFormatting.LineWidth +local LINE_COLOR = Constants.GeneralFormatting.LINE_COLOR +local VALUE_CELL_WIDTH = Constants.MemoryFormatting.ValueCellWidth +local CELL_PADDING = Constants.MemoryFormatting.CellPadding +local VALUE_PADDING = Constants.MemoryFormatting.ValuePadding + +local ENTRY_HEIGHT = Constants.GeneralFormatting.EntryFrameHeight +local DEPTH_INDENT = Constants.MemoryFormatting.DepthIndent + +local convertTimeStamp = require(script.Parent.Parent.Parent.Util.convertTimeStamp) + +return function(props) + local size = props.size + local depth = props.depth + local entry = props.entry + local showGraph = props.showGraph + local layoutOrder = props.layoutOrder + + local onButtonPress = props.onButtonPress + local formatValueStr = props.formatValueStr + local getX = props.getX + local getY = props.getY + + local offset = depth * DEPTH_INDENT + + local dataStats = entry.dataStats + + local name = entry.name + local value = dataStats.dataSet:back().data + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + button = Roact.createElement(BannerButton, { + size = UDim2.new(1, - offset, 0, ENTRY_HEIGHT), + pos = UDim2.new(0, offset, 0, 0), + isExpanded = showGraph, + + onButtonPress = onButtonPress, + }, { + name = Roact.createElement(CellLabel, { + text = name, + size = UDim2.new(1, 0, 1, 0), + pos = UDim2.new(0, CELL_PADDING, 0, 0), + }), + + horizonal1 = Roact.createElement("Frame", { + Size = UDim2.new(1, -offset , 0, LINE_WIDTH), + Position = UDim2.new(0, offset, 0, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + + horizonal2 = Roact.createElement("Frame", { + Size = UDim2.new(1, -offset, 0, LINE_WIDTH), + Position = UDim2.new(0, offset, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + }), + + value = Roact.createElement(CellLabel, { + text = formatValueStr(value), + size = UDim2.new(VALUE_CELL_WIDTH, -VALUE_PADDING, 0, ENTRY_HEIGHT), + pos = UDim2.new(1 - VALUE_CELL_WIDTH, VALUE_PADDING, 0, 0), + }), + + vertical = Roact.createElement("Frame",{ + Size = UDim2.new(0, 1, 0, ENTRY_HEIGHT), + Position = UDim2.new(1 - VALUE_CELL_WIDTH, 0, 0, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + + Graph = showGraph and Roact.createElement(LineGraph, { + pos = UDim2.new(0, 0, 0, ENTRY_HEIGHT), + size = UDim2.new(1, 0, 1, -ENTRY_HEIGHT), + graphData = dataStats.dataSet, + maxY = dataStats.max, + minY = dataStats.min, + + getX = getX, + getY = getY, + + stringFormatX = convertTimeStamp, + stringFormatY = formatValueStr, + + axisLabelX = "Timestamp", + axisLabelY = name, + }), + }) +end + +--return MemoryViewEntry \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MemoryViewEntry.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MemoryViewEntry.spec.lua new file mode 100644 index 0000000..76f51e0 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/MemoryViewEntry.spec.lua @@ -0,0 +1,41 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer) + + local DataProvider = require(script.Parent.Parent.DataProvider) + local MemoryViewEntry = require(script.Parent.MemoryViewEntry) + + it("should create and destroy without errors", function() + local dummyScriptEntry = { + time = 0, + data = {0, 0}, + } + + local dummyDataSet = CircularBuffer.new(1) + dummyDataSet:push_back(dummyScriptEntry) + + local formatValueStr = function() + return "" + end + + local dummyEntry = { + name = "", + dataStats = { + dataSet = dummyDataSet, + } + } + + local element = Roact.createElement(DataProvider, {},{ + MemoryViewEntry = Roact.createElement(MemoryViewEntry, { + depth = 0, + entry = dummyEntry, + formatValueStr = formatValueStr, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ServerMemory.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ServerMemory.lua new file mode 100644 index 0000000..4a86dc4 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ServerMemory.lua @@ -0,0 +1,19 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Components = script.Parent.Parent.Parent.Components +local DataConsumer = require(Components.DataConsumer) +local MemoryView = require(Components.Memory.MemoryView) + +local ServerMemory = Roact.Component:extend("ServerMemory") + +function ServerMemory:render() + return Roact.createElement(MemoryView,{ + layoutOrder = self.props.layoutOrder, + size = self.props.size, + searchTerm = self.props.searchTerm, + targetMemoryData = self.props.ServerMemoryData, + }) +end + +return DataConsumer(ServerMemory, "ServerMemoryData") \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ServerMemory.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ServerMemory.spec.lua new file mode 100644 index 0000000..34e6d68 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ServerMemory.spec.lua @@ -0,0 +1,16 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local DataProvider = require(script.Parent.Parent.DataProvider) + local ServerMemory = require(script.Parent.ServerMemory) + + it("should create and destroy without errors", function() + local element = Roact.createElement(DataProvider, nil, { + ServerMemory = Roact.createElement(ServerMemory) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ServerMemoryData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ServerMemoryData.lua new file mode 100644 index 0000000..c546456 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Memory/ServerMemoryData.lua @@ -0,0 +1,232 @@ +local Signal = require(script.Parent.Parent.Parent.Signal) +local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer) +local Constants = require(script.Parent.Parent.Parent.Constants) +local HEADER_NAMES = Constants.MemoryFormatting.ChartHeaderNames + +local MAX_DATASET_COUNT = tonumber(settings():GetFVariable("NewDevConsoleMaxGraphCount")) +local BYTES_PER_MB = 1048576.0 + +local SORT_COMPARATOR = { + [HEADER_NAMES[1]] = function(a, b) + return a.name < b.name + end, + [HEADER_NAMES[2]] = function(a, b) + return a.dataStats.dataSet:back().data < b.dataStats.dataSet:back().data + end, +} + +local getClientReplicator = require(script.Parent.Parent.Parent.Util.getClientReplicator) + +local ServerMemoryData = {} +ServerMemoryData.__index = ServerMemoryData + +function ServerMemoryData.new() + local self = {} + + setmetatable(self, ServerMemoryData) + self._init = false + self._totalMemory = 0 + + self._memoryData = {} + self._memoryDataSorted = {} + + self._coreTreeData = {} + self._coreTreeDataSorted = {} + + self._placeTreeData = {} + self._placeTreeDataSorted = {} + + self._treeViewUpdated = Signal.new() + self._sortType = HEADER_NAMES[1] + + return self +end + +function ServerMemoryData:updateEntry(entryList, sortedList, name, data) + if not entryList[name] then + local newBuffer = CircularBuffer.new(MAX_DATASET_COUNT) + + newBuffer:push_back({ + data = data, + time = self._lastUpdate + }) + + entryList[name] = { + min = data, + max = data, + dataSet = newBuffer + } + + local newEntry = { + name = name, + dataStats = entryList[name] + } + + table.insert(sortedList, newEntry) + else + local currMax = entryList[name].max + local currMin = entryList[name].min + + local update = { + data = data, + time = self._lastUpdate + } + + local overwrittenEntry = entryList[name].dataSet:push_back(update) + + if overwrittenEntry then + local iter = entryList[name].dataSet:iterator() + local dat = iter:next() + if currMax == overwrittenEntry.data then + currMax = currMin + while dat do + currMax = dat.data < currMax and currMax or dat.data + dat = iter:next() + end + end + if currMin == overwrittenEntry.data then + currMin = currMax + while dat do + currMin = currMin < dat.data and currMin or dat.data + dat = iter:next() + end + end + end + + entryList[name].max = currMax < data and data or currMax + entryList[name].min = currMin < data and currMin or data + end +end + +function ServerMemoryData:updateEntryList(entryList, sortedList, statsItems) + -- All values are in bytes. + -- Convert to MB ASAP. + local totalMB = 0 + for label, numBytes in pairs(statsItems) do + local value = numBytes / BYTES_PER_MB + totalMB = totalMB + value + + self:updateEntry(entryList, sortedList, label, value) + end + + return totalMB +end + +function ServerMemoryData:updateWithTreeStats(stats) + local update = { + PlaceMemory = 0, + CoreMemory = 0, + UntrackedMemory = 0, + } + + for key, value in pairs(stats) do + if key == "totalServerMemory" then + self._totalMemory = value / BYTES_PER_MB + elseif key == "developerTags" then + update.PlaceMemory = self:updateEntryList(self._placeTreeData, self._placeTreeDataSorted, value) + elseif key == "internalCategories" then + update.CoreMemory = self:updateEntryList(self._coreTreeData, self._coreTreeDataSorted, value) + end + end + + update.UntrackedMemory = self._totalMemory - update.PlaceMemory - update.CoreMemory + + if self._init then + for name, value in pairs(update) do + self:updateEntry( + self._memoryData["Memory"].children, + self._memoryData["Memory"].sortedChildren, + name, + value + ) + end + + self:updateEntry(self._memoryData, self._memoryDataSorted, "Memory", self._totalMemory) + else + local memChildren = {} + local memChildrenSorted = {} + + for name, value in pairs(update) do + self:updateEntry(memChildren, memChildrenSorted, name, value) + end + + self:updateEntry(self._memoryData, self._memoryDataSorted, "Memory", self._totalMemory) + + memChildren["PlaceMemory"].children = self._placeTreeData + memChildren["PlaceMemory"].sortedChildren = self._placeTreeDataSorted + + memChildren["CoreMemory"].children = self._coreTreeData + memChildren["CoreMemory"].sortedChildren = self._coreTreeDataSorted + + self._memoryData["Memory"].children = memChildren + self._memoryData["Memory"].sortedChildren = memChildrenSorted + + self._init = true + end + +end + +function ServerMemoryData:totalMemSignal() + return self._totalMemoryUpdated +end + +function ServerMemoryData:treeUpdatedSignal() + return self._treeViewUpdated +end + +function ServerMemoryData:getSortType() + return self._sortType +end + +local function recursiveSort(memoryDataSort, comparator) + table.sort(memoryDataSort, comparator) + for _, entry in pairs(memoryDataSort) do + if entry.dataStats.sortedChildren then + recursiveSort(entry.dataStats.sortedChildren, comparator) + end + end +end + +function ServerMemoryData:setSortType(sortType) + if SORT_COMPARATOR[sortType] then + self._sortType = sortType + recursiveSort(self._memoryDataSorted, SORT_COMPARATOR[self._sortType]) + else + error(string.format("attempted to pass invalid sortType: %s", tostring(sortType)), 2) + end +end + +function ServerMemoryData:getMemoryData() + return self._memoryDataSorted +end + +function ServerMemoryData:start() + local clientReplicator = getClientReplicator() + if clientReplicator and not self._statsListenerConnection then + self._statsListenerConnection = clientReplicator.StatsReceived:connect(function(stats) + if not stats.ServerMemoryTree then + return + end + self._lastUpdate = os.time() + + local serverMemoryTree = stats.ServerMemoryTree + if serverMemoryTree then + self:updateWithTreeStats(serverMemoryTree) + self._treeViewUpdated:Fire(self._memoryDataSorted) + end + + end) + clientReplicator:RequestServerStats(true) + end +end + +function ServerMemoryData:stop() + -- listeners are responsible for disconnecting themselves + local clientReplicator = getClientReplicator() + if clientReplicator then + clientReplicator:RequestServerStats(false) + self._statsListenerConnection:Disconnect() + end +end + +return ServerMemoryData \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/ClientNetwork.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/ClientNetwork.lua new file mode 100644 index 0000000..0aa2665 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/ClientNetwork.lua @@ -0,0 +1,29 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Components = script.Parent.Parent.Parent.Components +local DataConsumer = require(Components.DataConsumer) +local NetworkView = require(script.Parent.NetworkView) + +local ClientNetwork = Roact.Component:extend("ClientNetwork") + +function ClientNetwork:init(props) + self.state = { + targetNetworkData = self.props.ClientNetworkData + } +end + +function ClientNetwork:render() +local layoutOrder = self.props.layoutOrder + local searchTerm = self.props.searchTerm + local size = self.props.size + + return Roact.createElement(NetworkView, { + size = size, + searchTerm = searchTerm, + layoutOrder = layoutOrder, + targetNetworkData = self.state.targetNetworkData + }) +end + +return DataConsumer(ClientNetwork, "ClientNetworkData") \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/ClientNetwork.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/ClientNetwork.spec.lua new file mode 100644 index 0000000..c0b60f6 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/ClientNetwork.spec.lua @@ -0,0 +1,16 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local DataProvider = require(script.Parent.Parent.DataProvider) + local ClientNetwork = require(script.Parent.ClientNetwork) + + it("should create and destroy without errors", function() + local element = Roact.createElement(DataProvider, {},{ + ClientNetwork = Roact.createElement(ClientNetwork) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/MainViewNetwork.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/MainViewNetwork.lua new file mode 100644 index 0000000..b437487 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/MainViewNetwork.lua @@ -0,0 +1,141 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) + +local Components = script.Parent.Parent.Parent.Components +local ClientNetwork = require(Components.Network.ClientNetwork) +local ServerNetwork = require(Components.Network.ServerNetwork) +local UtilAndTab = require(Components.UtilAndTab) + +local Actions = script.Parent.Parent.Parent.Actions +local ClientNetworkUpdateSearchFilter = require(Actions.ClientNetworkUpdateSearchFilter) +local ServerNetworkUpdateSearchFilter = require(Actions.ServerNetworkUpdateSearchFilter) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local PADDING = Constants.GeneralFormatting.MainRowPadding + +local MainViewNetwork = Roact.Component:extend("MainViewNetwork") + +function MainViewNetwork:init() + self.onUtilTabHeightChanged = function(utilTabHeight) + self:setState({ + utilTabHeight = utilTabHeight + }) + end + + self.onClientButton = function() + self:setState({isClientView = true}) + end + + self.onServerButton = function() + self:setState({isClientView = false}) + end + + self.onSearchTermChanged = function(newSearchTerm) + if self.state.isClientView then + self.props.dispatchClientNetworkUpdateSearchFilter(newSearchTerm, {}) + else + self.props.dispatchServerNetworkUpdateSearchFilter(newSearchTerm, {}) + end + end + + self.utilRef = Roact.createRef() + + self.state = { + utilTabHeight = 0, + isClientView = true, + } +end + +function MainViewNetwork:didMount() + local utilSize = self.utilRef.current.Size + self:setState({ + utilTabHeight = utilSize.Y.Offset + }) +end + +function MainViewNetwork:didUpdate() + local utilSize = self.utilRef.current.Size + if utilSize.Y.Offset ~= self.state.utilTabHeight then + self:setState({ + utilTabHeight = utilSize.Y.Offset + }) + end +end + +function MainViewNetwork:render() + local elements = {} + local size = self.props.size + local formFactor = self.props.formFactor + local tabList = self.props.tabList + + local utilTabHeight = self.state.utilTabHeight + local isClientView = self.state.isClientView + local searchTerm = isClientView and self.props.clientSearchTerm or self.props.serverSearchTerm + + elements["UIListLayout"] = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, PADDING), + }) + + elements ["UtilAndTab"] = Roact.createElement(UtilAndTab,{ + windowWidth = size.X.Offset, + formFactor = formFactor, + tabList = tabList, + isClientView = isClientView, + searchTerm = searchTerm, + layoutOrder = 1, + + refForParent = self.utilRef, + + onHeightChanged = self.onUtilTabHeightChanged, + onClientButton = self.onClientButton, + onServerButton = self.onServerButton, + onSearchTermChanged = self.onSearchTermChanged, + }) + + if utilTabHeight > 0 then + if isClientView then + elements["ClientNetwork"] = Roact.createElement(ClientNetwork, { + size = UDim2.new(1, 0, 1, -utilTabHeight), + searchTerm = searchTerm, + layoutOrder = 2, + }) + else + elements["ServerNetwork"] = Roact.createElement(ServerNetwork, { + size = UDim2.new(1, 0, 1, -utilTabHeight), + searchTerm = searchTerm, + layoutOrder = 2, + }) + end + end + + return Roact.createElement("Frame",{ + Size = size, + BackgroundColor3 = Constants.Color.BaseGray, + BackgroundTransparency = 1, + LayoutOrder = 3, + + }, elements) +end + +local function mapStateToProps(state, props) + return { + clientSearchTerm = state.NetworkData.clientSearchTerm, + serverSearchTerm = state.NetworkData.serverSearchTerm, + } +end + +local function mapDispatchToProps(dispatch) + return { + dispatchClientNetworkUpdateSearchFilter = function(searchTerm, filters) + dispatch(ClientNetworkUpdateSearchFilter(searchTerm, filters)) + end, + + dispatchServerNetworkUpdateSearchFilter = function(searchTerm, filters) + dispatch(ServerNetworkUpdateSearchFilter(searchTerm, filters)) + end, + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(MainViewNetwork) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/MainViewNetwork.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/MainViewNetwork.spec.lua new file mode 100644 index 0000000..37c09e6 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/MainViewNetwork.spec.lua @@ -0,0 +1,38 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local RoactRodux = require(CorePackages.RoactRodux) + local Store = require(CorePackages.Rodux).Store + + local DataProvider = require(script.Parent.Parent.DataProvider) + + local MainViewNetwork = require(script.Parent.MainViewNetwork) + + it("should create and destroy without errors", function() + local store = Store.new(function() + return { + MainView = { + currTabIndex = 0 + }, + NetworkData = { + clientSearchTerm = "", + serverSearchTerm = "" + } + } + end) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + DataProvider = Roact.createElement(DataProvider, {},{ + MainViewNetwork = Roact.createElement(MainViewNetwork,{ + size = UDim2.new(), + tabList = {}, + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkChart.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkChart.lua new file mode 100644 index 0000000..e78000f --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkChart.lua @@ -0,0 +1,292 @@ +local CorePackages = game:GetService("CorePackages") +local TextService = game:GetService("TextService") +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Parent.Constants) + +local HEADER_NAMES = Constants.NetworkFormatting.ChartHeaderNames +local CELL_WIDTHS = Constants.NetworkFormatting.ChartCellWidths +local CELL_PADDING = Constants.NetworkFormatting.CellPadding +local HEADER_HEIGHT = Constants.NetworkFormatting.HeaderFrameHeight +local ENTRY_HEIGHT = Constants.NetworkFormatting.EntryFrameHeight +local LINE_WIDTH = Constants.GeneralFormatting.LineWidth +local LINE_COLOR = Constants.GeneralFormatting.LineColor + +local FONT_SIZE = Constants.DefaultFontSize.MainWindow +local MAIN_FONT = Constants.Font.MainWindow +local RESPONSE_STR_TEXT_HEIGHT = Constants.NetworkFormatting.ResponseStrHeight +local RESPONSE_WIDTH_RATIO = Constants.NetworkFormatting.ResponseWidthRatio + +local Components = script.Parent.Parent.Parent.Components +local HeaderButton = require(Components.HeaderButton) +local NetworkChartEntry = require(script.Parent.NetworkChartEntry) + +local totalEntryWidth = 0 +for _, cellWidth in pairs(CELL_WIDTHS) do + totalEntryWidth = totalEntryWidth + cellWidth +end + +-- create table of offsets and sizes for each cell +-- each of the first 5 cells has a fixed size +local currOffset = 0 +local cellOffset = {} +local headerCellSize = {} +local entryCellSize = {} + +for _, cellWidth in ipairs(CELL_WIDTHS) do + table.insert(cellOffset,UDim2.new(0, currOffset + CELL_PADDING, 0, 0)) + table.insert(headerCellSize, UDim2.new(0, cellWidth - CELL_PADDING, 0, HEADER_HEIGHT)) + table.insert(entryCellSize, UDim2.new(0, cellWidth - CELL_PADDING, 0, ENTRY_HEIGHT)) + currOffset = currOffset + cellWidth +end + +-- cell 1-5 are defined widths, +-- cell 6 pads out the remaining width in the row +table.insert(cellOffset,UDim2.new(0, currOffset + CELL_PADDING, 0, 0)) +table.insert(headerCellSize, UDim2.new(1, -totalEntryWidth - CELL_PADDING, 0, HEADER_HEIGHT)) +table.insert(entryCellSize, UDim2.new(1, -totalEntryWidth - CELL_PADDING, 0, ENTRY_HEIGHT)) + +local verticalOffsets = {} +for i, offset in ipairs(cellOffset) do + verticalOffsets[i] = UDim2.new(offset.X.Scale, offset.X.Offset - CELL_PADDING, + offset.Y.Scale, offset.Y.Offset) +end + +local NetworkChart = Roact.Component:extend("NetworkChart") + +function NetworkChart:init() + self.getOnExpandEntry = function (name) + return function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or + (input.UserInputType == Enum.UserInputType.Touch and + input.UserInputState == Enum.UserInputState.End) then + self:setState({ + expandIndex = self.state.expandIndex ~= name and name + }) + end + end + end + + self.onCanvasPosChanged = function() + local canvasPos = self.scrollingRef.current.CanvasPosition + if self.state.canvasPos ~= canvasPos then + self:setState({ + absScrollSize = self.scrollingRef.current.AbsoluteSize, + canvasPos = canvasPos, + }) + end + end + + self.ref = Roact.createRef() + self.scrollingRef = Roact.createRef() + + self.state = { + expandIndex = false, + } +end + +function NetworkChart:willUpdate() + if self.canvasPosConnector then + self.canvasPosConnector:Disconnect() + end +end + +function NetworkChart:didUpdate() + if self.scrollingRef.current then + local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition") + self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged) + end +end + +function NetworkChart:didMount() + if self.scrollingRef.current then + local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition") + self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged) + + self:setState({ + absScrollSize = self.scrollingRef.current.AbsoluteSize, + canvasPos = self.scrollingRef.current.CanvasPosition, + }) + end +end + +function NetworkChart:render() + local httpEntryList = self.props.httpEntryList or {} + local summaryHeight = self.props.summaryHeight + local width = self.props.width + local searchTerm = self.props.searchTerm + local layoutOrder = self.props.layoutOrder + local reverseSort = self.props.reverseSort + + local onSortChanged = self.props.onSortChanged + + local expandIndex = self.state.expandIndex + local absScrollSize = self.state.absScrollSize + local canvasPos = self.state.canvasPos + + local headerCells = {} + for ind, name in ipairs(HEADER_NAMES) do + headerCells[name] = Roact.createElement(HeaderButton, { + text = name, + size = headerCellSize[ind], + pos = cellOffset[ind], + + sortfunction = onSortChanged, + }) + end + + for i = 2, #verticalOffsets do + local key = string.format("VerticalLine_%d",i) + headerCells[key] = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 0, ENTRY_HEIGHT), + Position = verticalOffsets[i], + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + end + + local entries = {} + local canvasHeight = 0 + local paddingHeight = -1 + local usedFrameSpace = 0 + + local totalEntries = #httpEntryList + entries["UIListLayout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + if canvasPos and absScrollSize then + for ind, entry in ipairs(httpEntryList) do + local valid = true + if searchTerm ~= "" then + valid = string.find(entry.RequestType:lower(), searchTerm:lower()) ~= nil or + string.find(entry.Url:lower(), searchTerm:lower()) ~= nil + end + + if not (entry.RequestType == "Default") and valid then + -- insert header elements into a frame so that we can use the UIListLayout to keep everything in order + local showResponse = expandIndex == entry.Num + local frameHeight = ENTRY_HEIGHT + local responseBodyHeight = 0 + + if showResponse then + frameHeight = frameHeight + RESPONSE_STR_TEXT_HEIGHT + if self.ref.current then + local fSize = Vector2.new( + self.ref.current.AbsoluteSize.X * RESPONSE_WIDTH_RATIO, + 100000000) + local DisSize = TextService:GetTextSize(entry.Response, FONT_SIZE, MAIN_FONT, fSize) + + responseBodyHeight = RESPONSE_STR_TEXT_HEIGHT + DisSize.Y + frameHeight = frameHeight + responseBodyHeight + end + end + + if canvasHeight + frameHeight >= canvasPos.Y then + if usedFrameSpace < absScrollSize.Y then + local layoutOrder = reverseSort and totalEntries - ind or ind + + entries[ind] = Roact.createElement(NetworkChartEntry, { + size = UDim2.new(1, 0, 0, frameHeight), + entry = entry, + entryCellSize = entryCellSize, + cellOffset = cellOffset, + verticalOffsets = verticalOffsets, + showResponse = showResponse, + responseBodyHeight = responseBodyHeight, + + layoutOrder = layoutOrder + 1, + + onButtonPress = self.getOnExpandEntry(entry.Num), + }) + end + if paddingHeight < 0 then + paddingHeight = canvasHeight + else + usedFrameSpace = usedFrameSpace + frameHeight + end + end + canvasHeight = canvasHeight + frameHeight + end + entries["WindowingPadding"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, paddingHeight), + BackgroundTransparency = 1, + LayoutOrder = 1, + }) + end + end + + if totalEntries == 0 then + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, -summaryHeight), + Text = "No Network Entries Found", + TextColor3 = Constants.Color.Text, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Header = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, HEADER_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = 1, + },headerCells), + + HorizontalLine_1 = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + BackgroundTransparency = 0, + LayoutOrder = 2, + }), + }) + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, -summaryHeight), + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + + [Roact.Ref] = self.ref, + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + + Header = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, HEADER_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = 1, + },headerCells), + + HorizontalLine_1 = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + BackgroundTransparency = 0, + LayoutOrder = 2, + }), + + scrollingFrameEntries = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, 0, 1, -HEADER_HEIGHT), + CanvasSize = UDim2.new(0, width, 0, canvasHeight), + ScrollBarThickness = 6, + BackgroundTransparency = 1, + LayoutOrder = 3, + + [Roact.Ref] = self.scrollingRef + }, entries), + }) +end + +return NetworkChart \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkChart.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkChart.spec.lua new file mode 100644 index 0000000..e0eafda --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkChart.spec.lua @@ -0,0 +1,14 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local NetworkChart = require(script.Parent.NetworkChart) + + it("should create and destroy without errors", function() + local element = Roact.createElement(NetworkChart, { + summaryHeight = 0, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkChartEntry.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkChartEntry.lua new file mode 100644 index 0000000..b6a740d --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkChartEntry.lua @@ -0,0 +1,111 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local ENTRY_HEIGHT = Constants.NetworkFormatting.EntryFrameHeight +local LINE_WIDTH = Constants.GeneralFormatting.LineWidth +local LINE_COLOR = Constants.GeneralFormatting.LineColor + +local RESPONSE_STR_TEXT_HEIGHT = Constants.NetworkFormatting.ResponseStrHeight +local RESPONSE_WIDTH_RATIO = Constants.NetworkFormatting.ResponseWidthRatio +local RESPONSE_X_OFFSET = (1 - RESPONSE_WIDTH_RATIO) / 2 +local RESPONSE_STR_X_OFFSET = (1 - RESPONSE_WIDTH_RATIO) / 4 +local RESPONSE_STR_TEXT = "Response Body:" + +local Components = script.Parent.Parent.Parent.Components +local CellLabel = require(Components.CellLabel) +local HeaderButton = require(Components.HeaderButton) +local BannerButton = require(Components.BannerButton) + +return function(props) + local size = props.size + local entry = props.entry + local entryCellSize = props.entryCellSize + local cellOffset = props.cellOffset + local verticalOffsets = props.verticalOffsets + local layoutOrder = props.layoutOrder + local showResponse = props.showResponse + local responseBodyHeight = props.responseBodyHeight + + + local onButtonPress = props.onButtonPress + + local row = {} + row["Num"] = Roact.createElement(CellLabel, { + text = entry.Num, + size = entryCellSize[1], + pos = cellOffset[1], + }) + row["Method"] = Roact.createElement(CellLabel, { + text = entry.Method, + size = entryCellSize[2], + pos = cellOffset[2], + }) + row["Status"] = Roact.createElement(CellLabel, { + text = entry.Status, + size = entryCellSize[3], + pos = cellOffset[3], + }) + row["Time"] = Roact.createElement(CellLabel, { + text = string.format("%.3f", entry.Time), + size = entryCellSize[4], + pos = cellOffset[4], + }) + row["RequestType"] = Roact.createElement(CellLabel, { + text = entry.RequestType, + size = entryCellSize[5], + pos = cellOffset[5], + }) + row["Url"] = Roact.createElement(CellLabel, { + text = entry.Url, + size = entryCellSize[6], + pos = cellOffset[6], + }) + row["LowerHorizontalLine"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 1), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + + for i = 2, #verticalOffsets do + local key = string.format("VerticalLine_%d",i) + row[key] = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 0, ENTRY_HEIGHT), + Position = verticalOffsets[i], + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + end + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + -- to make room for the windowing padding + LayoutOrder = layoutOrder, + }, { + Button = Roact.createElement(BannerButton, { + size = UDim2.new(1, 0, 0, ENTRY_HEIGHT), + pos = UDim2.new(), + isExpanded = showResponse, + + onButtonPress = onButtonPress, + }, row), + + Response = showResponse and Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, responseBodyHeight), + Position = UDim2.new(RESPONSE_X_OFFSET, 0, 0, ENTRY_HEIGHT), + BackgroundTransparency = 1, + }, { + Text = Roact.createElement(HeaderButton, { + text = RESPONSE_STR_TEXT, + size = UDim2.new(1, 0, 0, RESPONSE_STR_TEXT_HEIGHT), + pos = UDim2.new(-RESPONSE_STR_X_OFFSET, 0, 0, 0), + }), + ResponseBody = Roact.createElement(CellLabel, { + text = entry.Response, + size = UDim2.new(.8, 0, 1, -RESPONSE_STR_TEXT_HEIGHT), + pos = UDim2.new(0, 0, 0, RESPONSE_STR_TEXT_HEIGHT), + }) + }) + }) +end diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkChartEntry.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkChartEntry.spec.lua new file mode 100644 index 0000000..52b05d6 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkChartEntry.spec.lua @@ -0,0 +1,35 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local NetworkChartEntry = require(script.Parent.NetworkChartEntry) + + it("should create and destroy without errors", function() + local dummyEntry = { + Num = 0, + Method = "", + Status = "", + Time = 0, + RequestType = "", + Url = "", + Reponse = "", + } + local dummyCellSizes = { + UDim2.new(), + UDim2.new(), + UDim2.new(), + UDim2.new(), + UDim2.new(), + UDim2.new(), + } + + local element = Roact.createElement(NetworkChartEntry,{ + entry = dummyEntry, + entryCellSize = dummyCellSizes, + cellOffset = dummyCellSizes, + verticalOffsets = dummyCellSizes, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkData.lua new file mode 100644 index 0000000..0cc8f56 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkData.lua @@ -0,0 +1,213 @@ +local LogService = game:GetService("LogService") +local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer) +local Signal = require(script.Parent.Parent.Parent.Signal) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local HEADER_NAMES = Constants.NetworkFormatting.ChartHeaderNames + +local MAX_NUM_ENTRIES = tonumber(settings():GetFVariable("NewDevConsoleMaxHttpCount")) + +local SORT_COMPARATOR = { + [HEADER_NAMES[1]] = function(a, b) + return a.Num < b.Num + end, + [HEADER_NAMES[2]] = function(a, b) + return a.Method < b.Method + end, + [HEADER_NAMES[3]] = function(a, b) + return a.Status < b.Status + end, + [HEADER_NAMES[4]] = function(a, b) + return a.Time < b.Time + end, + [HEADER_NAMES[5]] = function(a, b) + return a.RequestType < b.RequestType + end, + [HEADER_NAMES[6]] = function(a, b) + return a.Url < b.Url + end, +} + +local function newHttpEntry(num, method, status, time, requestType, url, response) + return { + Num = num, + Method = method, + Status = status, + Time = time, + RequestType = requestType, + Url = url, + Response = response + } +end + +local NetworkData = {} +NetworkData.__index = NetworkData + +function NetworkData.new( isClient ) + local self = {} + setmetatable(self, NetworkData) + + self._isClient = isClient + self._httpResultSignal = isClient and LogService.HttpResultOut or LogService.ServerHttpResultOut + + self._httpEntryAdded = Signal.new() + self._httpSummaryTable = {} + self._httpSummaryCount = 0 + self._httpEntryBuffer = CircularBuffer.new(MAX_NUM_ENTRIES) + + self._httpLifeTimeEntryCount = 0 + self._sortType = HEADER_NAMES[1] -- Num + return self +end + +function NetworkData:addHttpEntry(httpResult) + -- append new entry to the back of the list + self._httpLifeTimeEntryCount = self._httpLifeTimeEntryCount + 1 + local entry = newHttpEntry( + self._httpLifeTimeEntryCount, + httpResult.Method, + httpResult.Status, + httpResult.Time, + httpResult.RequestType, + httpResult.URL, + httpResult.Response + ) + + self._httpEntryBuffer:push_back(entry) + + -- update Summary Data + local requestType = httpResult.RequestType + local summaryTable = self._httpSummaryTable[requestType] + + if not summaryTable then + self._httpSummaryCount = self._httpSummaryCount + 1 + + self._httpSummaryTable[requestType] = { + RequestType = requestType, + RequestCount = 1, + FailedCount = 0, + AverageTime = httpResult.Time, + MinTime = httpResult.Time, + MaxTime = httpResult.Time, + } + + if httpResult.Status >= 400 then + self._httpSummaryTable[httpResult.RequestType].FailedCount = 1 + end + else + summaryTable.RequestCount = summaryTable.RequestCount + 1 + if httpResult.Status >= 400 then + summaryTable.FailedCount = summaryTable.FailedCount + 1 + end + + summaryTable.AverageTime = ((summaryTable.AverageTime * summaryTable.RequestCount) + + httpResult.Time - summaryTable.AverageTime) / summaryTable.RequestCount + + if httpResult.Time < summaryTable.MinTime then + summaryTable.MinTime = httpResult.Time + end + if httpResult.Time > summaryTable.MaxTime then + summaryTable.MaxTime = httpResult.Time + end + end +end + +function NetworkData:setSortType(sortType) + if SORT_COMPARATOR[sortType] then + self._sortType = sortType + self._httpEntryAdded:Fire( + self._httpSummaryTable, + self._httpSummaryCount, + self:getSortedEntries() + ) + else + error(string.format("attempted to pass invalid sortType: %s", tostring(sortType)), 2) + end +end + +function NetworkData:getSortType() + return self._sortType +end + +function NetworkData:resetHttpEntryList() + self._httpSummaryTable = {} + self._httpEntryBuffer:reset() + self._httpLifeTimeEntryCount = 0 + if self._isClient then + local history = LogService:GetHttpResultHistory() + + if history then + for _,httpResult in pairs(history) do + self:addHttpEntry(httpResult) + end + end + end +end + +function NetworkData:Signal() + return self._httpEntryAdded +end + +function NetworkData:getSortedEntries() + local iter = self._httpEntryBuffer:iterator() + local data = iter:next() + local entryList = {} + local count = 1 + + while data do + entryList[count] = data + count = count + 1 + data = iter:next() + end + + table.sort(entryList, SORT_COMPARATOR[self._sortType]) + + return entryList +end + +function NetworkData:getCurrentData() + return { + summaryTable = self._httpSummaryTable, + summaryCount = self._httpSummaryCount, + entryList = self:getSortedEntries(), + } +end + +function NetworkData:start() + if not self._httpResultConnection then + if self._additionHttpSetup then + self._additionHttpSetup() + end + + self._httpResultConnection = self._httpResultSignal:connect(function (httpResult) + self:addHttpEntry(httpResult) + self._httpEntryAdded:Fire( + self._httpSummaryTable, + self._httpSummaryCount, + self:getSortedEntries() + ) + end) + + self:resetHttpEntryList() + + if not self._isClient then + self._onHttpResultApprovedConnection = LogService.OnHttpResultApproved:connect( function (isApproved) + LogService:RequestHttpResultApproved() + if isApproved then + self._onHttpResultApprovedConnection:Disconnect() + self._onHttpResultApprovedConnection = nil + end + end) + LogService:RequestServerHttpResult() + end + end +end + +function NetworkData:stop() + if self._httpResultConnection then + self._httpResultConnection:Disconnect() + self._httpResultConnection = nil + end +end + +return NetworkData \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkSummary.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkSummary.lua new file mode 100644 index 0000000..e0828fe --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkSummary.lua @@ -0,0 +1,193 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Parent.Constants) + +local HEADER_NAMES = Constants.NetworkFormatting.SummaryHeaderNames +local CELL_WIDTH = Constants.NetworkFormatting.SummaryCellWidths +local HEADER_HEIGHT = Constants.NetworkFormatting.HeaderFrameHeight +local ENTRY_HEIGHT = Constants.NetworkFormatting.EntryFrameHeight + +local LINE_WIDTH = Constants.GeneralFormatting.LineWidth +local LINE_COLOR = Constants.GeneralFormatting.LineColor +local CELL_PADDING = Constants.NetworkFormatting.CellPadding +local TEXT_COLOR = Constants.Color.Text +local NON_DATA_STR = "No Summary Data Found" + +local CellLabel = require(script.Parent.Parent.Parent.Components.CellLabel) +local HeaderButton = require(script.Parent.Parent.Parent.Components.HeaderButton) + +local totalSummaryWidth = 0 +for _, v in pairs(CELL_WIDTH) do + totalSummaryWidth = totalSummaryWidth + v +end + +-- create table of offsets and sizes for each cell +local currOffset = -totalSummaryWidth +local cellOffset = {} +local headerCellSize = {} +local entryCellSize = {} + +table.insert(cellOffset, UDim2.new(0, CELL_PADDING, 0, 0)) +table.insert(headerCellSize, UDim2.new(1, -totalSummaryWidth - CELL_PADDING, 0, HEADER_HEIGHT)) +table.insert(entryCellSize, UDim2.new(1, -totalSummaryWidth - CELL_PADDING, 0, ENTRY_HEIGHT)) + +for _, width in ipairs(CELL_WIDTH) do + table.insert(cellOffset,UDim2.new(1, currOffset + CELL_PADDING, 0, 0)) + table.insert(headerCellSize, UDim2.new(0, width - CELL_PADDING, 0, HEADER_HEIGHT)) + table.insert(entryCellSize, UDim2.new(0, width - CELL_PADDING, 0, ENTRY_HEIGHT)) + currOffset = currOffset + width +end + +local verticalOffsets = {} +for i, offset in ipairs(cellOffset) do + verticalOffsets[i] = UDim2.new( + offset.X.Scale, + offset.X.Offset - CELL_PADDING, + offset.Y.Scale, + offset.Y.Offset + ) +end + +local NetworkSummary = Roact.Component:extend("NetworkSummary") + +function NetworkSummary:render() + local width = self.props.width + local httpSummaryTable = self.props.httpSummaryTable or {} + local layoutOrder = self.props.layoutOrder + + local elements = {} + elements["UIListLayout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + local summaryHeader = {} + summaryHeader["UpperHorizontalLine"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + summaryHeader["LowerHorizontalLine"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + + for i = 2, #verticalOffsets do + local key = string.format("VerticalLine_%d",i) + summaryHeader[key] = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 0, ENTRY_HEIGHT), + Position = verticalOffsets[i], + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + end + + for ind, name in ipairs(HEADER_NAMES) do + summaryHeader[name] = Roact.createElement(HeaderButton, { + text = name, + size = headerCellSize[ind], + pos = cellOffset[ind], + }) + end + elements["Header"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, HEADER_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = 1, + }, summaryHeader) + + local entryCount = 0 + for _, dataEntry in pairs(httpSummaryTable) do + entryCount = entryCount + 1 + + local row = {} + + row.RequestType = Roact.createElement(CellLabel, { + text = dataEntry.RequestType, + size = headerCellSize[1], + pos = cellOffset[1], + }) + + row.RequestCount = Roact.createElement(CellLabel, { + text = dataEntry.RequestCount, + size = headerCellSize[2], + pos = cellOffset[2], + }) + + row.FailedCount = Roact.createElement(CellLabel, { + text = dataEntry.FailedCount, + size = headerCellSize[3], + pos = cellOffset[3], + }) + + row.AverageTime = Roact.createElement(CellLabel, { + text = string.format("%.3f", dataEntry.AverageTime), + size = headerCellSize[4], + pos = cellOffset[4], + }) + + row.MinTime = Roact.createElement(CellLabel, { + text = string.format("%.3f", dataEntry.MinTime), + size = headerCellSize[5], + pos = cellOffset[5], + }) + + row.MaxTime = Roact.createElement(CellLabel, { + text = string.format("%.3f", dataEntry.MaxTime), + size = headerCellSize[6], + pos = cellOffset[6], + }) + + row.LowerHorizontalLine = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + + for ind = 2, #verticalOffsets do + local key = string.format("VerticalLine_%d",ind) + row[key] = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 1, 0), + Position = verticalOffsets[ind], + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + end + + elements[dataEntry.RequestType] = Roact.createElement("Frame", { + Size = UDim2.new(0, width, 0, ENTRY_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = entryCount + 1 + },row) + end + + if entryCount == 0 then + elements["Padding"] = Roact.createElement("TextLabel", { + Size = UDim2.new(0, width, 0, ENTRY_HEIGHT), + Text = NON_DATA_STR, + TextColor3 = TEXT_COLOR, + BackgroundTransparency = 1, + LayoutOrder = 2 + }) + entryCount = 1 + end + + local summaryHeight = entryCount * ENTRY_HEIGHT + HEADER_HEIGHT + + -- update offsets to remove padding so we can use for vertical line + return Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, 0, 0, summaryHeight), + CanvasSize = UDim2.new(0, width, 0, summaryHeight), + ScrollBarThickness = 6, + BackgroundTransparency = 1, + + LayoutOrder = layoutOrder, + }, elements) +end + +return NetworkSummary \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkSummary.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkSummary.spec.lua new file mode 100644 index 0000000..e3685d9 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkSummary.spec.lua @@ -0,0 +1,12 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local NetworkSummary = require(script.Parent.NetworkSummary) + + it("should create and destroy without errors", function() + local element = Roact.createElement(NetworkSummary) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkView.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkView.lua new file mode 100644 index 0000000..509cc0c --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkView.lua @@ -0,0 +1,196 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local HEADER_HEIGHT = Constants.NetworkFormatting.HeaderFrameHeight +local ENTRY_HEIGHT = Constants.NetworkFormatting.EntryFrameHeight +local SUMMARY_HEIGHT = Constants.NetworkFormatting.SummaryButtonHeight +local MIN_WIDTH = Constants.NetworkFormatting.MinFrameWidth + +local BANNER_FONT_SIZE = Constants.DefaultFontSize.MainWindow +local BANNER_FONT = Constants.Font.MainWindowHeader + +local INDENT = 30 + +local Components = script.Parent.Parent.Parent.Components +local BannerButton = require(Components.BannerButton) +local NetworkSummary = require(Components.Network.NetworkSummary) +local NetworkChart = require(Components.Network.NetworkChart) + +local NetworkView = Roact.Component:extend("NetworkView") + +function NetworkView:init(props) + assert(props.targetNetworkData, "Make sure the NetworkData is assigned for this NetworkView") + + local currentData = props.targetNetworkData:getCurrentData() + + self.onSortChanged = function(sortType) + local currSortType = props.targetNetworkData:getSortType() + if sortType == currSortType then + self:setState({ + reverseSort = not self.state.reverseSort + }) + else + props.targetNetworkData:setSortType(sortType) + self:setState({ + reverseSort = false, + }) + end + end + + self.onSummaryButton = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or + (input.UserInputType == Enum.UserInputType.Touch and + input.UserInputState == Enum.UserInputState.End) then + self:setState({ + summaryExpanded = not self.state.summaryExpanded, + }) + end + end + + self.onDetailButton = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or + (input.UserInputType == Enum.UserInputType.Touch and + input.UserInputState == Enum.UserInputState.End) then + self:setState({ + entriesExpanded = not self.state.entriesExpanded, + }) + end + end + + self.ref = Roact.createRef() + + self.state = { + httpSummaryTable = currentData.summaryTable, + httpSummaryCount = currentData.summaryCount, + httpEntryList = currentData.entryList, + summaryExpanded = true, + entriesExpanded = true, + indexOfEntryExpanded = 0, + reverseSort = false, + absWidth = 0, + } +end + +function NetworkView:didUpdate() + if self.ref.current then + if self.state.absWidth ~= self.ref.current.AbsoluteSize.X then + self:setState({ + absWidth = self.ref.current.AbsoluteSize.X, + }) + end + end +end + +function NetworkView:didMount() + local networkDataSignal = self.props.targetNetworkData:Signal() + self.httpEntryAddedConnector = networkDataSignal:Connect(function(summaryTable, summaryCount, entryList) + self:setState({ + httpSummaryTable = summaryTable, + httpSummaryCount = summaryCount, + httpEntryList = entryList, + }) + end) + + if self.ref.current then + self:setState({ + absWidth = self.ref.current.AbsoluteSize.X, + }) + end +end + +function NetworkView:willUnmount() + self.httpEntryAddedConnector:Disconnect() + self.httpEntryAddedConnector = nil +end + +function NetworkView:render() + local layoutOrder = self.props.layoutOrder + local searchTerm = self.props.searchTerm + local size = self.props.size + + local summaryExpanded = self.state.summaryExpanded + local entriesExpanded = self.state.entriesExpanded + local reverseSort = self.state.reverseSort + local httpSummaryTable = self.state.httpSummaryTable + local httpEntryList = self.state.httpEntryList + local httpSummaryCount = self.state.httpSummaryCount + + local absWidth = math.max(MIN_WIDTH, self.state.absWidth) + + -- for both the detail button and summary button + local summaryHeight = SUMMARY_HEIGHT * 2 + if summaryExpanded then + summaryHeight = summaryHeight + httpSummaryCount * ENTRY_HEIGHT + HEADER_HEIGHT + end + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + + [Roact.Ref] = self.ref, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + SummaryButton = Roact.createElement(BannerButton, { + size = UDim2.new(1, 0, 0, SUMMARY_HEIGHT), + pos = UDim2.new(), + isExpanded = summaryExpanded, + onButtonPress = self.onSummaryButton, + layoutOrder = 1, + }, { + Text = Roact.createElement("TextLabel", { + Text = "Summary", + TextColor3 = Constants.Color.Text, + TextXAlignment = Enum.TextXAlignment.Left, + TextSize = BANNER_FONT_SIZE, + Font = BANNER_FONT, + + Size = UDim2.new(1,-INDENT,0,SUMMARY_HEIGHT), + Position = UDim2.new(0, INDENT, 0, 0), + BackgroundTransparency = 1, + }) + }), + Summary = summaryExpanded and Roact.createElement(NetworkSummary, { + width = absWidth, + httpSummaryTable = httpSummaryTable, + layoutOrder = 2, + }), + DetailsButton = Roact.createElement(BannerButton, { + size = UDim2.new(1, 0, 0, SUMMARY_HEIGHT), + pos = UDim2.new(), + isExpanded = entriesExpanded, + onButtonPress = self.onDetailButton, + layoutOrder = 3, + }, { + Text = Roact.createElement("TextLabel", { + Text = "Details", + TextColor3 = Constants.Color.Text, + TextXAlignment = Enum.TextXAlignment.Left, + TextSize = BANNER_FONT_SIZE, + Font = BANNER_FONT, + + Size = UDim2.new(1,-INDENT,0,SUMMARY_HEIGHT), + Position = UDim2.new(0, INDENT, 0, 0), + BackgroundTransparency = 1, + }) + }), + Entries = entriesExpanded and Roact.createElement(NetworkChart, { + httpEntryList = httpEntryList, + summaryHeight = summaryHeight, + width = absWidth, + searchTerm = searchTerm, + reverseSort = reverseSort, + layoutOrder = 4, + + onSortChanged = self.onSortChanged + }) + }) +end + +return NetworkView \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkView.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkView.spec.lua new file mode 100644 index 0000000..526762a --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/NetworkView.spec.lua @@ -0,0 +1,31 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local Signal = require(script.Parent.Parent.Parent.Signal) + + local NetworkView = require(script.Parent.NetworkView) + + local dummmyNetworkData = { + getCurrentData = function () + return { + summaryTable = {}, + summaryCount = 0, + entryList = nil, + } + end, + Signal = function () + return Signal.new() + end, + } + + it("should create and destroy without errors", function() + + local element = Roact.createElement(NetworkView,{ + targetNetworkData = dummmyNetworkData, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/ServerNetwork.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/ServerNetwork.lua new file mode 100644 index 0000000..e75cbc4 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/ServerNetwork.lua @@ -0,0 +1,29 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Components = script.Parent.Parent.Parent.Components +local DataConsumer = require(Components.DataConsumer) +local NetworkView = require(script.Parent.NetworkView) + +local ServerNetwork = Roact.Component:extend("ServerNetwork") + +function ServerNetwork:init(props) + self.state = { + targetNetworkData = self.props.ServerNetworkData + } +end + +function ServerNetwork:render() +local layoutOrder = self.props.layoutOrder + local searchTerm = self.props.searchTerm + local size = self.props.size + + return Roact.createElement(NetworkView, { + size = size, + searchTerm = searchTerm, + layoutOrder = layoutOrder, + targetNetworkData = self.state.targetNetworkData + }) +end + +return DataConsumer(ServerNetwork, "ServerNetworkData") \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/ServerNetwork.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/ServerNetwork.spec.lua new file mode 100644 index 0000000..7baab42 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Network/ServerNetwork.spec.lua @@ -0,0 +1,16 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local DataProvider = require(script.Parent.Parent.DataProvider) + local ServerNetwork = require(script.Parent.ServerNetwork) + + it("should create and destroy without errors", function() + local element = Roact.createElement(DataProvider, {},{ + ServerNetwork = Roact.createElement(ServerNetwork) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/MainViewScripts.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/MainViewScripts.lua new file mode 100644 index 0000000..277306f --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/MainViewScripts.lua @@ -0,0 +1,121 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) + +local Components = script.Parent.Parent.Parent.Components +local ServerScripts = require(Components.Scripts.ServerScripts) +local UtilAndTab = require(Components.UtilAndTab) + +local Actions = script.Parent.Parent.Parent.Actions +local ServerScriptsUpdateSearchFilter = require(Actions.ServerScriptsUpdateSearchFilter) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local PADDING = Constants.GeneralFormatting.MainRowPadding + +local BOX_NAMES = { + "Active", + "Inactive" +} + +local MainViewScripts = Roact.PureComponent:extend("MainViewScripts") + +function MainViewScripts:init() + self.onUtilTabHeightChanged = function(utilTabHeight) + self:setState({ + utilTabHeight = utilTabHeight + }) + end + + self.onCheckBoxesChanged = function(newFilters) + self.props.dispatchServerScriptsUpdateSearchFilter(nil, newFilters) + end + + self.onSearchTermChanged = function(newSearchTerm) + self.props.dispatchServerScriptsUpdateSearchFilter(newSearchTerm, {}) + end + + self.utilRef = Roact.createRef() + + self.state = { + utilTabHeight = 0, + isClientView = true, + } +end + +function MainViewScripts:didMount() + local utilSize = self.utilRef.current.Size + self:setState({ + utilTabHeight = utilSize.Y.Offset + }) +end + +function MainViewScripts:didUpdate() + local utilSize = self.utilRef.current.Size + if utilSize.Y.Offset ~= self.state.utilTabHeight then + self:setState({ + utilTabHeight = utilSize.Y.Offset + }) + end +end + +function MainViewScripts:render() + local size = self.props.size + local formFactor = self.props.formFactor + local tabList = self.props.tabList + local scriptFilters = self.props.serverTypeFilters + + local utilTabHeight = self.state.utilTabHeight + + local searchTerm = self.props.serverSearchTerm + + return Roact.createElement("Frame",{ + Size = size, + BackgroundColor3 = Constants.Color.BaseGray, + BackgroundTransparency = 1, + LayoutOrder = 3, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + Padding = UDim.new(0, PADDING), + SortOrder = Enum.SortOrder.LayoutOrder, + }), + + UtilAndTab = Roact.createElement(UtilAndTab,{ + windowWidth = size.X.Offset, + formFactor = formFactor, + tabList = tabList, + checkBoxNames = BOX_NAMES, + searchTerm = searchTerm, + layoutOrder = 1, + + refForParent = self.utilRef, + + onHeightChanged = self.onUtilTabHeightChanged, + onCheckBoxesChanged = self.onCheckBoxesChanged, + onSearchTermChanged = self.onSearchTermChanged, + }), + + ServerScripts = utilTabHeight > 0 and Roact.createElement(ServerScripts, { + size = UDim2.new(1, 0, 1, -utilTabHeight), + searchTerm = searchTerm, + scriptFilters = scriptFilters, + layoutOrder = 2, + }), + }) +end + +local function mapStateToProps(state, props) + return { + serverSearchTerm = state.ScriptsData.serverSearchTerm, + serverTypeFilters = state.ScriptsData.serverTypeFilters, + } +end + +local function mapDispatchToProps(dispatch) + return { + dispatchServerScriptsUpdateSearchFilter = function(searchTerm, filters) + dispatch(ServerScriptsUpdateSearchFilter(searchTerm, filters)) + end, + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(MainViewScripts) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/MainViewScripts.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/MainViewScripts.spec.lua new file mode 100644 index 0000000..24120c2 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/MainViewScripts.spec.lua @@ -0,0 +1,37 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local RoactRodux = require(CorePackages.RoactRodux) + local Store = require(CorePackages.Rodux).Store + + local DataProvider = require(script.Parent.Parent.DataProvider) + + local MainViewScripts = require(script.Parent.MainViewScripts) + + it("should create and destroy without errors", function() + local store = Store.new(function() + return { + MainView = { + currTabIndex = 0 + }, + ScriptsData = { + scriptsSearchTerm = "" + } + } + end) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + DataProvider = Roact.createElement(DataProvider, {},{ + MainViewScripts = Roact.createElement(MainViewScripts,{ + size = UDim2.new(), + tabList = {}, + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/ServerScripts.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/ServerScripts.lua new file mode 100644 index 0000000..2a57793 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/ServerScripts.lua @@ -0,0 +1,327 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Components = script.Parent.Parent.Parent.Components +local DataConsumer = require(script.Parent.Parent.Parent.Components.DataConsumer) +local HeaderButton = require(Components.HeaderButton) + +local ServerScriptsEntry = require(script.Parent.ServerScriptsEntry) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local LINE_WIDTH = Constants.GeneralFormatting.LineWidth +local LINE_COLOR = Constants.GeneralFormatting.LineColor +local CELL_WIDTHS = Constants.ServerScriptsFormatting.ChartCellWidths +local HEADER_NAMES = Constants.ServerScriptsFormatting.ChartHeaderNames +local HEADER_HEIGHT = Constants.ServerScriptsFormatting.HeaderFrameHeight +local ENTRY_HEIGHT = Constants.ServerScriptsFormatting.EntryFrameHeight +local CELL_PADDING = Constants.ServerScriptsFormatting.CellPadding +local ACTIVITYBOX_PADDING = Constants.ServerScriptsFormatting.ActivityBoxPadding + +local GRAPH_HEIGHT = Constants.GeneralFormatting.LineGraphHeight + +local NO_DATA_MSG = "Awaiting Server Scripts Information." + +-- create table of offsets and sizes for each cell +local totalCellWidth = 0 +for _, cellWidth in ipairs(CELL_WIDTHS) do + totalCellWidth = totalCellWidth + cellWidth +end + +local currOffset = -totalCellWidth +local cellOffset = {} +local headerCellSize = {} +local entryCellSize = {} + +table.insert(cellOffset, UDim2.new(0, CELL_PADDING + ACTIVITYBOX_PADDING, 0, 0)) +table.insert(headerCellSize, UDim2.new(1, -totalCellWidth - CELL_PADDING - ACTIVITYBOX_PADDING, 0, HEADER_HEIGHT)) +table.insert(entryCellSize, UDim2.new(1, -totalCellWidth - CELL_PADDING - ACTIVITYBOX_PADDING, 0, ENTRY_HEIGHT)) + +for _, cellWidth in ipairs(CELL_WIDTHS) do + table.insert(cellOffset,UDim2.new(1, currOffset + CELL_PADDING, 0, 0)) + table.insert(headerCellSize, UDim2.new(0, cellWidth - CELL_PADDING, 0, HEADER_HEIGHT)) + table.insert(entryCellSize, UDim2.new(0, cellWidth - CELL_PADDING, 0, ENTRY_HEIGHT)) + currOffset = currOffset + cellWidth +end + +local verticalOffsets = {} +for i, offset in ipairs(cellOffset) do + verticalOffsets[i] = UDim2.new(offset.X.Scale, offset.X.Offset - CELL_PADDING, + offset.Y.Scale, offset.Y.Offset) +end + +local ServerScripts = Roact.Component:extend("ServerScripts") + +local function constructHeader(onSortChanged) + local header = {} + -- NameButton + for i = 1, #HEADER_NAMES do + header[HEADER_NAMES[i]] = Roact.createElement(HeaderButton,{ + text = HEADER_NAMES[i], + size = headerCellSize[i], + pos = cellOffset[i], + sortfunction = onSortChanged, + }) + end + + header["upperHorizontalLine"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 0, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + + header["verticalLine1"] = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 1, 0), + Position = verticalOffsets[2], + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + + header["verticalLine2"] = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 1, 0), + Position = verticalOffsets[3], + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + + header["lowerHorizontalLine"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, HEADER_HEIGHT), + BackgroundTransparency = 1, + }, header) +end + +local function getX(entry) + return entry.time +end + +local function getActivity(entry) + return entry.data[1] +end + +local function getRate(entry) + return entry.data[2] +end + +local function formatScriptsData(data) + return string.format("%.3f", data) +end + +function ServerScripts:init(props) + local currScriptsData = props.ServerScriptsData:getCurrentData() + + self.onSortChanged = function(sortType) + local currSortType = props.ServerScriptsData:getSortType() + if sortType == currSortType then + self:setState({ + reverseSort = not self.state.reverseSort + }) + else + props.ServerScriptsData:setSortType(sortType) + self:setState({ + reverseSort = false, + }) + end + end + + self.getOnButtonPress = function (name) + return function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or + (input.UserInputType == Enum.UserInputType.Touch and + input.UserInputState == Enum.UserInputState.End) then + self:setState({ + expandIndex = self.state.expandIndex ~= name and name + }) + end + end + end + + self.onCanvasPosChanged = function() + local canvasPos = self.scrollingRef.current.CanvasPosition + if self.state.canvasPos ~= canvasPos then + self:setState({ + absScrollSize = self.scrollingRef.current.AbsoluteSize, + canvasPos = canvasPos, + }) + end + end + + self.scrollingRef = Roact.createRef() + + self.state = { + serverScriptsData = currScriptsData, + expandIndex = nil, + } +end + +function ServerScripts:willUpdate() + if self.canvasPosConnector then + self.canvasPosConnector:Disconnect() + end +end + +function ServerScripts:didUpdate() + if self.scrollingRef.current then + local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition") + self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged) + end +end + +function ServerScripts:didMount() + self.statsConnector = self.props.ServerScriptsData:Signal():Connect(function(data) + self:setState({ + serverScriptsData = data, + }) + end) + + if self.scrollingRef.current then + local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition") + self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged) + + self:setState({ + absScrollSize = self.scrollingRef.current.AbsoluteSize, + canvasPos = self.scrollingRef.current.CanvasPosition, + }) + end +end + +function ServerScripts:willUnmount() + self.statsConnector:Disconnect() + self.statsConnector = nil +end + +function ServerScripts:render() + local searchTerm = self.props.searchTerm + local scriptFilters = self.props.scriptFilters + local layoutOrder = self.props.layoutOrder + local size = self.props.size + + local scriptData = self.state.serverScriptsData + local reverseSort = self.state.reverseSort + local absScrollSize = self.state.absScrollSize + local canvasPos = self.state.canvasPos + + local entries = {} + local totalEntries = #scriptData + + if totalEntries == 0 then + return Roact.createElement("TextLabel",{ + Size = size, + Position = UDim2.new(0, 0, 0, 0), + Text = NO_DATA_MSG, + TextColor3 = Constants.Color.Text, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }) + end + + local canvasHeight = 0 + local paddingHeight = -1 + local usedFrameSpace = 0 + + if canvasPos and absScrollSize then + for ind, scriptData in pairs(scriptData) do + + if not searchTerm or string.find(scriptData.name:lower(), searchTerm:lower()) ~= nil then + local includeEntry = true + local isActiveChecked = scriptFilters["Active"] + local isInactiveChecked = scriptFilters["Inactive"] + local currEntry = scriptData.dataStats.dataSet:back() + --[[ + this conditional determines if the script is rendered or not depending on the + the state of the active/inactive checkboxes. + if neither are checked, render all + if Active is checked, only render active scripts + if Inactive is check, only render inactive scripts + if Both are checked, render all + ]]-- + + if isActiveChecked ~= isInactiveChecked then + if currEntry.data[1] > 0 and isInactiveChecked then + includeEntry = false + elseif currEntry.data[1] == 0 and isActiveChecked then + includeEntry = false + end + end + + if includeEntry then + local showGraph = self.state.expandIndex == scriptData.name + local frameHeight = showGraph and ENTRY_HEIGHT + (2 * GRAPH_HEIGHT) or ENTRY_HEIGHT + + if canvasHeight + frameHeight >= canvasPos.Y then + if usedFrameSpace < absScrollSize.Y then + local activityBoxColor = currEntry.data[1] > 0 and Constants.Color.ActiveBox or Constants.Color.InactiveBox + local layoutOrder = reverseSort and totalEntries - ind or ind + local newElement = Roact.createElement(ServerScriptsEntry, { + scriptData = scriptData, + frameHeight = frameHeight, + showGraph = showGraph, + layoutOrder = layoutOrder, + activityBoxColor = activityBoxColor, + + entryCellSize = entryCellSize, + cellOffset = cellOffset, + verticalOffsets = verticalOffsets, + + onButtonPress = self.getOnButtonPress(scriptData.name), + + formatScriptsData = formatScriptsData, + getX = getX, + getActivity = getActivity, + getRate = getRate, + stringFormatY = formatScriptsData, + }) + entries[ind] = newElement + end + + if paddingHeight < 0 then + paddingHeight = canvasHeight + else + usedFrameSpace = usedFrameSpace + frameHeight + end + end + canvasHeight = canvasHeight + frameHeight + end + end + end + entries["WindowingPadding"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, paddingHeight), + BackgroundTransparency = 1, + LayoutOrder = 1, + }) + end + + entries["UIListLayout"] = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + + Header = constructHeader(self.onSortChanged), + + Entries = Roact.createElement("ScrollingFrame", { + Position = UDim2.new(0, 0, 0, HEADER_HEIGHT), + Size = UDim2.new(1, 0, 1, -HEADER_HEIGHT), + BackgroundTransparency = 1, + VerticalScrollBarInset = 1, + ScrollBarThickness = 5, + CanvasSize = UDim2.new(1, 0, 0, canvasHeight), + + [Roact.Ref] = self.scrollingRef, + }, entries), + }) +end + +return DataConsumer(ServerScripts, "ServerScriptsData") \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/ServerScripts.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/ServerScripts.spec.lua new file mode 100644 index 0000000..9aee626 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/ServerScripts.spec.lua @@ -0,0 +1,16 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local DataProvider = require(script.Parent.Parent.DataProvider) + local ServerScripts = require(script.Parent.ServerScripts) + + it("should create and destroy without errors", function() + local element = Roact.createElement(DataProvider, {},{ + ServerScripts = Roact.createElement(ServerScripts) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/ServerScriptsData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/ServerScriptsData.lua new file mode 100644 index 0000000..08f4e98 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/ServerScriptsData.lua @@ -0,0 +1,148 @@ +local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer) +local Signal = require(script.Parent.Parent.Parent.Signal) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local MAX_DATASET_COUNT = tonumber(settings():GetFVariable("NewDevConsoleMaxGraphCount")) + +local HEADER_NAMES = Constants.ServerScriptsFormatting.ChartHeaderNames + +local SORT_COMPARATOR = { + [HEADER_NAMES[1]] = function(a, b) + return a.dataStats.dataSet:back().name < b.dataStats.dataSet:back().name + end, + [HEADER_NAMES[2]] = function(a, b) + return a.dataStats.dataSet:back().data[1] < b.dataStats.dataSet:back().data[1] + end, + [HEADER_NAMES[3]] = function(a, b) + return a.dataStats.dataSet:back().data[2] < b.dataStats.dataSet:back().data[2] + end, +} + +local minOfTable = require(script.Parent.Parent.Parent.Util.minOfTable) +local maxOfTable = require(script.Parent.Parent.Parent.Util.maxOfTable) +local getClientReplicator = require(script.Parent.Parent.Parent.Util.getClientReplicator) + +local ServerScriptsData = {} +ServerScriptsData.__index = ServerScriptsData + +function ServerScriptsData.new() + local self = {} + setmetatable(self, ServerScriptsData) + + self._serverScriptsUpdated = Signal.new() + self._serverScriptsData = {} + self._lastUpdate = 0 + self._sortedScriptsData = {} + self._sortType = HEADER_NAMES[1] -- Name + return self +end + +function ServerScriptsData:setSortType(sortType) + if SORT_COMPARATOR[sortType] then + self._sortType = sortType + -- do we need a mutex type thing here? + table.sort(self._sortedScriptsData, SORT_COMPARATOR[self._sortType]) + else + error(string.format("attempted to pass invalid sortType: %s", tostring(sortType)), 2) + end +end + +function ServerScriptsData:getSortType() + return self._sortType +end + +function ServerScriptsData:Signal() + return self._serverScriptsUpdated +end + +function ServerScriptsData:getCurrentData() + return self._sortedScriptsData +end + + +function ServerScriptsData:updateScriptsData(scriptsStats) + self._lastUpdate = os.time() + for key, data in pairs(scriptsStats) do + if not self._serverScriptsData[key] then + local newBuffer = CircularBuffer.new(MAX_DATASET_COUNT) + newBuffer:push_back({ + data = data, + time = self._lastUpdate, + }) + + self._serverScriptsData[key] = { + max = data, + min = data, + dataSet = newBuffer, + } + + local newEntry = { + name = key, + dataStats = self._serverScriptsData[key], + } + + table.insert(self._sortedScriptsData, newEntry) + else + local currMax = self._serverScriptsData[key].max + local currMin = self._serverScriptsData[key].min + + local update = { + data = data, + time = self._lastUpdate + } + + local overwrittenEntry = self._serverScriptsData[key].dataSet:push_back(update) + + if overwrittenEntry then + local iter = self._serverScriptsData[key].dataSet:iterator() + local dat = iter:next() + if currMax == overwrittenEntry.data then + currMax = currMin + while dat do + currMax = maxOfTable(dat, currMax) + dat = iter:next() + end + end + if currMin == overwrittenEntry.data then + currMin = currMax + while dat do + currMin = minOfTable(dat, currMin) + dat = iter:next() + end + end + end + + self._serverScriptsData[key].max = maxOfTable(currMax, data) + self._serverScriptsData[key].min = minOfTable(currMin, data) + end + end +end + +function ServerScriptsData:start() + local clientReplicator = getClientReplicator() + if clientReplicator and not self._statsListenerConnection then + self._statsListenerConnection = clientReplicator.StatsReceived:connect(function(stats) + if stats then + self._lastUpdate = os.time() + + local statsScripts = stats.Scripts + if statsScripts then + self:updateScriptsData(statsScripts) + end + + self._serverScriptsUpdated:Fire(self._sortedScriptsData) + end + end) + clientReplicator:RequestServerStats(true) + end +end + +function ServerScriptsData:stop() + -- listeners are responsible for disconnecting themselves + if self._statsListenerConnection then + self._statsListenerConnection:Disconnect() + self._statsListenerConnection = nil + end +end + +return ServerScriptsData \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/ServerScriptsEntry.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/ServerScriptsEntry.lua new file mode 100644 index 0000000..078fdb1 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/ServerScriptsEntry.lua @@ -0,0 +1,137 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Components = script.Parent.Parent.Parent.Components +local CellLabel = require(Components.CellLabel) +local BannerButton = require(Components.BannerButton) +local LineGraph = require(Components.LineGraph) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local LINE_WIDTH = Constants.GeneralFormatting.LineWidth +local LINE_COLOR = Constants.GeneralFormatting.LineColor +local HEADER_NAMES = Constants.ServerScriptsFormatting.ChartHeaderNames +local ENTRY_HEIGHT = Constants.ServerScriptsFormatting.EntryFrameHeight +local ACTIVITYBOX_PADDING = Constants.ServerScriptsFormatting.ActivityBoxPadding +local ACTIVITYBOX_WIDTH = Constants.ServerScriptsFormatting.ActivityBoxWidth + +local GRAPH_HEIGHT = Constants.GeneralFormatting.LineGraphHeight + +local convertTimeStamp = require(script.Parent.Parent.Parent.Util.convertTimeStamp) + +return function(props) + local scriptData = props.scriptData + + local frameHeight = props.frameHeight + local showGraph = props.showGraph + local layoutOrder = props.layoutOrder + local activityBoxColor = props.activityBoxColor + + local entryCellSize = props.entryCellSize + local cellOffset = props.cellOffset + local verticalOffsets = props.verticalOffsets + + local onButtonPress = props.onButtonPress + local getX = props.getX + local getActivity = props.getActivity + local getRate = props.getRate + local formatScriptsData = props.formatScriptsData + + local scriptDataStats = scriptData.dataStats + local currEntry = scriptDataStats.dataSet:back() + + return Roact.createElement("Frame",{ + Size = UDim2.new(1, 0, 0, frameHeight), + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + button = Roact.createElement(BannerButton, { + size = UDim2.new(1, 0, 0, ENTRY_HEIGHT), + pos = UDim2.new(), + isExpanded = showGraph, + + onButtonPress = onButtonPress, + }, { + ActivityBox = Roact.createElement("Frame", { + Size = UDim2.new(0, ACTIVITYBOX_WIDTH, 0, ACTIVITYBOX_WIDTH), + Position = UDim2.new(0, ACTIVITYBOX_PADDING, 0, (ENTRY_HEIGHT - ACTIVITYBOX_WIDTH) / 2), + BackgroundColor3 = activityBoxColor, + }), + ScriptName = Roact.createElement(CellLabel,{ + text = scriptData.name, + size = entryCellSize[1], + pos = cellOffset[1], + }), + ScriptAcitivity = Roact.createElement(CellLabel,{ + text = formatScriptsData(currEntry.data[1]), + size = entryCellSize[2], + pos = cellOffset[2], + }), + scriptFreqStr = Roact.createElement(CellLabel,{ + text = formatScriptsData(currEntry.data[2]), + size = entryCellSize[3], + pos = cellOffset[3], + }), + + upperHorizontalLine = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + + verticalLine1 = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 1, 0), + Position = verticalOffsets[2], + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + + verticalLine2 = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 1, 0), + Position = verticalOffsets[3], + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + + lowerHorizontalLine = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + }), + + ActivityGraph = showGraph and Roact.createElement(LineGraph,{ + pos = UDim2.new(0, 0, 0, ENTRY_HEIGHT), + size = UDim2.new(1, 0, 0, GRAPH_HEIGHT), + graphData = scriptDataStats.dataSet, + maxY = scriptDataStats.max[1], + minY = scriptDataStats.min[1], + + getX = getX, + getY = getActivity, + + stringFormatX = convertTimeStamp, + stringFormatY = formatScriptsData, + + axisLabelX = "Timestamp", + axisLabelY = HEADER_NAMES[2], + }), + + RateGraph = showGraph and Roact.createElement(LineGraph,{ + pos = UDim2.new(0, 0, 0, ENTRY_HEIGHT + GRAPH_HEIGHT), + size = UDim2.new(1, 0, 0, GRAPH_HEIGHT), + graphData = scriptDataStats.dataSet, + maxY = scriptDataStats.max[2], + minY = scriptDataStats.min[2], + + getX = getX, + getY = getRate, + + stringFormatX = convertTimeStamp, + stringFormatY = formatScriptsData, + + axisLabelX = "Timestamp", + axisLabelY = HEADER_NAMES[3], + }), + }) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/ServerScriptsEntry.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/ServerScriptsEntry.spec.lua new file mode 100644 index 0000000..2934378 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/Scripts/ServerScriptsEntry.spec.lua @@ -0,0 +1,48 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer) + + local DataProvider = require(script.Parent.Parent.DataProvider) + local ServerScriptsEntry = require(script.Parent.ServerScriptsEntry) + + it("should create and destroy without errors", function() + local dummyScriptEntry = { + time = 0, + data = {0, 0}, + } + local dataSet = CircularBuffer.new(1) + dataSet:push_back(dummyScriptEntry) + + local formatScriptsData = function() + return "" + end + + local scriptDummyData = { + dataStats = { + min = 0, + max = 0, + dataSet = dataSet, + } + } + + local dummyCellSizes = { + UDim2.new(), + UDim2.new(), + UDim2.new(), + } + local element = Roact.createElement(DataProvider, {},{ + ServerScriptsEntry = Roact.createElement(ServerScriptsEntry,{ + scriptData = scriptDummyData, + entryCellSize = dummyCellSizes, + cellOffset = dummyCellSizes, + verticalOffsets = dummyCellSizes, + formatScriptsData = formatScriptsData, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/SearchBar.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/SearchBar.lua new file mode 100644 index 0000000..e2f5420 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/SearchBar.lua @@ -0,0 +1,76 @@ +--[[ + The Search Bar is reused by every tab. + When changing tabs, the SearchBar is updated with the last known search term + of the new target tab. It is also updated with the appropriate callback to + start a search +]]-- +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Constants) +local ICON_OFFSET = 10 + +local SearchBar = Roact.Component:extend("SearchBar") + +function SearchBar:render() + local size = self.props.size + local pos = self.props.pos + local frameHeight = self.props.frameHeight + + local textSize = self.props.textSize + local font = self.props.font + local searchTerm = self.props.searchTerm + local layoutOrder = self.props.layoutOrder + + local visible = self.props.visible + local showClear = self.props.showClear + + local iconHeight = frameHeight * .6 + + return Roact.createElement("Frame", { + Size = size, + Position = pos, + BackgroundColor3 = Constants.Color.UnselectedGray, + BorderColor3 = Constants.Color.BorderGray, + Visible = visible, + LayoutOrder = layoutOrder, + }, { + SearchImage = Roact.createElement("ImageLabel", { + Name = "SearchIcon", + Size = UDim2.new(0, iconHeight, 0, iconHeight), + Position = UDim2.new(0, ICON_OFFSET, .5, -iconHeight / 2), + BackgroundTransparency = 1, + Image = Constants.Image.Search, + }), + + ClearButton = showClear and Roact.createElement("ImageButton", { + Size = UDim2.new(0, -iconHeight, 0, iconHeight), + Position = UDim2.new(1, -ICON_OFFSET, .5, -iconHeight / 2), + BackgroundTransparency = 1, + Image = Constants.Image.Clear, + + [Roact.Event.InputEnded] = self.props.cancelInput, + }), + + TextBox = Roact.createElement("TextBox", { + Text = searchTerm and searchTerm or "", + TextSize = textSize, + Font = font, + Size = UDim2.new(1, - (2 * frameHeight), 1, 0), + Position = UDim2.new(0, ICON_OFFSET * 2 + iconHeight, 0, 0), + ShowNativeInput = true, + TextColor3 = Constants.Color.Text, + BackgroundTransparency = 1, + TextXAlignment = 0, + Active = false, + + ClearTextOnFocus = false, + PlaceholderText = "Search", + [Roact.Event.FocusLost] = self.props.focusLost, + + [Roact.Ref] = self.props.refForParent, + }), + }) +end + +return SearchBar \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/SearchBar.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/SearchBar.spec.lua new file mode 100644 index 0000000..63a09f1 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/SearchBar.spec.lua @@ -0,0 +1,14 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local SearchBar = require(script.Parent.SearchBar) + + it("should create and destroy without errors", function() + local element = Roact.createElement(SearchBar,{ + frameHeight = 0, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerJobs/MainViewServerJobs.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerJobs/MainViewServerJobs.lua new file mode 100644 index 0000000..6cd36c9 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerJobs/MainViewServerJobs.lua @@ -0,0 +1,106 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) + +local Components = script.Parent.Parent.Parent.Components +local ServerJobsChart = require(Components.ServerJobs.ServerJobsChart) +local UtilAndTab = require(Components.UtilAndTab) + +local Actions = script.Parent.Parent.Parent.Actions +local ServerJobsUpdateSearchFilter = require(Actions.ServerJobsUpdateSearchFilter) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local PADDING = Constants.GeneralFormatting.MainRowPadding + +local MainViewServerJobs = Roact.Component:extend("MainViewServerJobs") + +function MainViewServerJobs:init() + self.onUtilTabHeightChanged = function(utilTabHeight) + self:setState({ + utilTabHeight = utilTabHeight + }) + end + + self.onSearchTermChanged = function(newSearchTerm) + self.props.dispatchServerJobsUpdateSearchFilter(newSearchTerm, {}) + end + + self.utilRef = Roact.createRef() + + self.state = { + utilTabHeight = 0 + } +end + +function MainViewServerJobs:didMount() + local utilSize = self.utilRef.current.Size + self:setState({ + utilTabHeight = utilSize.Y.Offset + }) +end + +function MainViewServerJobs:didUpdate() + local utilSize = self.utilRef.current.Size + if utilSize.Y.Offset ~= self.state.utilTabHeight then + self:setState({ + utilTabHeight = utilSize.Y.Offset + }) + end +end + + +function MainViewServerJobs:render() + local size = self.props.size + local formFactor = self.props.formFactor + local tabList = self.props.tabList + local searchTerm = self.props.jobsSearchTerm + + local utilTabHeight = self.state.utilTabHeight + + return Roact.createElement("Frame",{ + Size = size, + BackgroundColor3 = Constants.Color.BaseGray, + BackgroundTransparency = 1, + LayoutOrder = 3 + }, { + UIListLayout = Roact.createElement("UIListLayout", { + Padding = UDim.new(0, PADDING), + SortOrder = Enum.SortOrder.LayoutOrder, + }), + + UtilAndTab = Roact.createElement(UtilAndTab,{ + windowWidth = size.X.Offset, + formFactor = formFactor, + tabList = tabList, + searchTerm = searchTerm, + layoutOrder = 1, + + refForParent = self.utilRef, + + onHeightChanged = self.onUtilTabHeightChanged, + onSearchTermChanged = self.onSearchTermChanged, + }), + + ServerJobs = utilTabHeight > 0 and Roact.createElement(ServerJobsChart, { + size = UDim2.new(1, 0, 1, -utilTabHeight), + searchTerm = searchTerm, + layoutOrder = 2, + }) + }) +end + +local function mapStateToProps(state, props) + return { + jobsSearchTerm = state.ServerJobsData.jobsSearchTerm, + } +end + +local function mapDispatchToProps(dispatch) + return { + dispatchServerJobsUpdateSearchFilter = function(searchTerm, filters) + dispatch(ServerJobsUpdateSearchFilter(searchTerm, filters)) + end + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(MainViewServerJobs) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerJobs/MainViewServerJobs.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerJobs/MainViewServerJobs.spec.lua new file mode 100644 index 0000000..ef4dc54 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerJobs/MainViewServerJobs.spec.lua @@ -0,0 +1,35 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local RoactRodux = require(CorePackages.RoactRodux) + local Store = require(CorePackages.Rodux).Store + + local DataProvider = require(script.Parent.Parent.DataProvider) + local MainViewServerJobs = require(script.Parent.MainViewServerJobs) + + it("should create and destroy without errors", function() + local store = Store.new(function() + return { + MainView = { + currTabIndex = 0 + }, + ServerJobsData = { + jobsSearchTerm = "" + } + } + end) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + DataProvider = Roact.createElement(DataProvider, {},{ + MainViewServerJobs = Roact.createElement(MainViewServerJobs,{ + size = UDim2.new(), + tabList = {}, + }) + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerJobs/ServerJobsChart.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerJobs/ServerJobsChart.lua new file mode 100644 index 0000000..a328e07 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerJobs/ServerJobsChart.lua @@ -0,0 +1,408 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Components = script.Parent.Parent.Parent.Components +local DataConsumer = require(Components.DataConsumer) +local HeaderButton = require(Components.HeaderButton) +local CellLabel = require(Components.CellLabel) +local BannerButton = require(Components.BannerButton) +local LineGraph = require(Components.LineGraph) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local LINE_WIDTH = Constants.GeneralFormatting.LineWidth +local LINE_COLOR = Constants.GeneralFormatting.LineColor +local HEADER_NAMES = Constants.ServerJobsFormatting.ChartHeaderNames +local CELL_WIDTHS = Constants.ServerJobsFormatting.ValueCellWidth +local ENTRY_HEIGHT = Constants.ServerJobsFormatting.EntryFrameHeight +local HEADER_HEIGHT = Constants.ServerJobsFormatting.HeaderFrameHeight +local CELL_PADDING = Constants.ServerJobsFormatting.CellPadding +local MIN_FRAME_WIDTH = Constants.ServerJobsFormatting.MinFrameWidth + +local GRAPH_HEIGHT = Constants.GeneralFormatting.LineGraphHeight + +local convertTimeStamp = require(script.Parent.Parent.Parent.Util.convertTimeStamp) + +local COLUMN_TRANSFORM_FUNC = { + function(point) + return point and math.floor(point * 10000000 + 0.5) / 100000 .. "%" or "" + end, + function(point) + return point and (math.floor(point * 10000 + 0.5) / 10000) .. "/s" or "" + end, + function(point) + return point and (math.floor(point * 10000000 + 0.5) / 10000) .. "ms" or "" + end, +} + +local NO_DATA_MSG = "Awaiting Server Jobs Information" + +-- create table of offsets and sizes for each cell +local currOffset = 0 +local cellOffset = {} +local headerCellSize = {} +local entryCellSize = {} + +local entryCellHeight = ENTRY_HEIGHT - LINE_WIDTH -- to account for border height and to use UIListLayout + +for _, cellWidth in ipairs(CELL_WIDTHS) do + table.insert(cellOffset,UDim2.new(currOffset, CELL_PADDING, 0, 0)) + table.insert(headerCellSize, UDim2.new(cellWidth, -CELL_PADDING, 0, HEADER_HEIGHT)) + table.insert(entryCellSize, UDim2.new(cellWidth, -CELL_PADDING, 0, entryCellHeight)) + currOffset = currOffset + cellWidth +end + +local verticalOffsets = {} +for i, offset in ipairs(cellOffset) do + verticalOffsets[i] = UDim2.new( + offset.X.Scale, + offset.X.Offset - CELL_PADDING, + offset.Y.Scale, + offset.Y.Offset + ) +end + +local ServerJobsChart = Roact.Component:extend("ServerJobsChart") + +local function getX(entry) + return entry.time +end + +local function getDutyCycle(entry) + return entry.data[1] +end + +local function getStepsPerSec (entry) + return entry.data[2] +end + +local function getStepTime (entry) + return entry.data[3] +end + +function ServerJobsChart:init(props) + local currJobsList = props.ServerJobsData:getCurrentData() + + self.getOnButtonPress = function (name) + return function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or + (input.UserInputType == Enum.UserInputType.Touch and + input.UserInputState == Enum.UserInputState.End) then + self:setState({ + expandIndex = self.state.expandIndex ~= name and name + }) + end + end + end + + self.onSortChanged = function(sortType) + local currSortType = props.ServerJobsData:getSortType() + if sortType == currSortType then + self:setState({ + reverseSort = not self.state.reverseSort + }) + else + props.ServerJobsData:setSortType(sortType) + self:setState({ + reverseSort = false, + }) + end + end + + + self.onCanvasPosChanged = function() + local canvasPos = self.scrollingRef.current.CanvasPosition + if self.state.canvasPos ~= canvasPos then + self:setState({ + canvasPos = canvasPos, + }) + end + end + + self.scrollingRef = Roact.createRef() + + self.state = { + serverJobsList = currJobsList, + reverseSort = false, + expandIndex = nil, + } +end + +function ServerJobsChart:willUpdate() + if self.canvasPosConnector then + self.canvasPosConnector:Disconnect() + end +end + +function ServerJobsChart:didUpdate() + if self.scrollingRef.current then + local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition") + self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged) + + local absSize = self.scrollingRef.current.AbsoluteSize + local currAbsSize = self.state.absScrollSize + if absSize.X ~= currAbsSize.X or + absSize.Y ~= currAbsSize.Y then + self:setState({ + absScrollSize = absSize, + }) + end + end +end + +function ServerJobsChart:didMount() + self.statsConnector = self.props.ServerJobsData:Signal():Connect(function(data) + self:setState({ + serverJobsList = data, + }) + end) + + if self.scrollingRef.current then + local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition") + self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged) + + self:setState({ + absScrollSize = self.scrollingRef.current.AbsoluteSize, + canvasPos = self.scrollingRef.current.CanvasPosition, + }) + end +end + +function ServerJobsChart:willUnmount() + self.statsConnector:Disconnect() + self.statsConnector = nil + if self.canvasPosConnector then + self.canvasPosConnector:Disconnect() + self.canvasPosConnector = nil + end +end + +function ServerJobsChart:render() + local elements = {} + local searchTerm = self.props.searchTerm + local size = self.props.size + local layoutOrder = self.props.layoutOrder + + local serverJobsList = self.state.serverJobsList + local reverseSort = self.state.reverseSort + local expandIndex = self.state.expandIndex + local canvasPos = self.state.canvasPos + local absScrollSize = self.state.absScrollSize + + local absWidth = absScrollSize and math.max(absScrollSize.X, MIN_FRAME_WIDTH) or MIN_FRAME_WIDTH + + local totalEntries = #serverJobsList + + if totalEntries == 0 then + return Roact.createElement("TextLabel", { + Size = size, + Position = UDim2.new(0, 0, 0, 0), + Text = NO_DATA_MSG, + TextColor3 = Constants.Color.Text, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }) + end + + local scrollingFrameHeight = 0 + if absScrollSize then + local paddingHeight = -1 + local usedFrameSpace = 0 + + for ind, job in pairs(serverJobsList) do + local name = job.name + if not searchTerm or string.find(name:lower(), searchTerm:lower()) ~= nil then + + local jobData = job.dataStats + local currEntry = jobData.dataSet:back() + + local showGraph = expandIndex == name + local frameHeight = showGraph and ENTRY_HEIGHT + (3 * GRAPH_HEIGHT / 2) + CELL_PADDING * 4 or ENTRY_HEIGHT + + if scrollingFrameHeight + frameHeight >= canvasPos.Y then + if usedFrameSpace < absScrollSize.Y then + local entryLayoutOrder = reverseSort and (totalEntries - ind) or ind + + local entry = {} + entry[name] = Roact.createElement(CellLabel, { + text = name, + size = headerCellSize[1], + pos = cellOffset[1], + }) + + for i, v in pairs(currEntry.data) do + entry[HEADER_NAMES[i] ] = Roact.createElement(CellLabel, { + text = COLUMN_TRANSFORM_FUNC[i](v), + size = headerCellSize[i + 1], + pos = cellOffset[i + 1], + }) + end + + -- add vertical lines over components + for offsetInd = 2, #verticalOffsets do + local key = string.format("VerticalLine_%d",offsetInd) + entry[key] = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 1, 0), + Position = verticalOffsets[offsetInd], + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + end + + entry["lowerHorizontalLine"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + + elements[name] = Roact.createElement("Frame", { + Size = UDim2.new(0, absWidth, 0, frameHeight), + BackgroundTransparency = 1, + -- we add 1 to the layoutorder to make room for the windowing padding + LayoutOrder = entryLayoutOrder + 1, + }, { + Button = Roact.createElement(BannerButton, { + size = UDim2.new(1, 0, 0, ENTRY_HEIGHT), + pos = UDim2.new(), + isExpanded = showGraph, + + onButtonPress = self.getOnButtonPress(name) + }, entry), + + DutyCycleGraph = showGraph and Roact.createElement(LineGraph, { + pos = UDim2.new(0, 0, 0, ENTRY_HEIGHT + CELL_PADDING), + size = UDim2.new(0, absWidth, 0, GRAPH_HEIGHT / 2), + graphData = jobData.dataSet, + maxY = jobData.max[1], + minY = jobData.min[1], + + getX = getX, + getY = getDutyCycle, + + stringFormatX = convertTimeStamp, + stringFormatY = COLUMN_TRANSFORM_FUNC[1], + + axisLabelX = "Timestamp", + axisLabelY = name .. " " .. HEADER_NAMES[2], + }), + + StepsPerSecGraph = showGraph and Roact.createElement(LineGraph, { + pos = UDim2.new(0, 0, 0, ENTRY_HEIGHT + (GRAPH_HEIGHT / 2) + CELL_PADDING * 2), + size = UDim2.new(0, absWidth, 0, GRAPH_HEIGHT / 2), + graphData = jobData.dataSet, + maxY = jobData.max[2], + minY = jobData.min[2], + + getX = getX, + getY = getStepsPerSec, + + stringFormatX = convertTimeStamp, + stringFormatY = COLUMN_TRANSFORM_FUNC[2], + + axisLabelX = "Timestamp", + axisLabelY = name .. " " .. HEADER_NAMES[3], + }), + + StepTimeGraph = showGraph and Roact.createElement(LineGraph, { + pos = UDim2.new(0, 0, 0, ENTRY_HEIGHT + GRAPH_HEIGHT + CELL_PADDING * 3), + size = UDim2.new(0, absWidth, 0, GRAPH_HEIGHT / 2), + graphData = jobData.dataSet, + maxY = jobData.max[3], + minY = jobData.min[3], + + getX = getX, + getY = getStepTime, + + stringFormatX = convertTimeStamp, + stringFormatY = COLUMN_TRANSFORM_FUNC[3], + + axisLabelX = "Timestamp", + axisLabelY = name .. " " .. HEADER_NAMES[4], + }), + }) + end + if paddingHeight < 0 then + paddingHeight = scrollingFrameHeight + else + usedFrameSpace = usedFrameSpace + frameHeight + end + end + scrollingFrameHeight = scrollingFrameHeight + frameHeight + end + end + + elements["WindowingPadding"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, paddingHeight), + BackgroundTransparency = 1, + LayoutOrder = 1, + }) + end + + local header = {} + for i = 1, #HEADER_NAMES do + header[HEADER_NAMES[i]] = Roact.createElement(HeaderButton, { + text = HEADER_NAMES[i], + size = headerCellSize[i], + pos = cellOffset[i], + sortfunction = self.onSortChanged, + }) + end + + header["upperHorizontalLine"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 0, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + + header["lowerHorizontalLine"] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, LINE_WIDTH), + Position = UDim2.new(0, 0, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + + for ind = 2, #verticalOffsets do + local key = string.format("VerticalLine_%d",ind) + header[key] = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 1, 0), + Position = verticalOffsets[ind], + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }) + end + + elements["UIListLayout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + Header = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, 0, 0, HEADER_HEIGHT), + CanvasSize = UDim2.new(0, absWidth, 1, 0), + ScrollingEnabled = false, + ScrollBarThickness = 0, + BackgroundTransparency = 1, + }, header), + + MainChart = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, 0, 1, -HEADER_HEIGHT), + Position = UDim2.new(0, 0, 0, HEADER_HEIGHT), + BackgroundTransparency = 1, + VerticalScrollBarInset = 1, + ScrollBarThickness = 5, + CanvasSize = UDim2.new(0, absWidth, 0, scrollingFrameHeight), + + [Roact.Ref] = self.scrollingRef, + }, elements), + }) +end + +return DataConsumer(ServerJobsChart, "ServerJobsData") \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerJobs/ServerJobsChart.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerJobs/ServerJobsChart.spec.lua new file mode 100644 index 0000000..1445230 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerJobs/ServerJobsChart.spec.lua @@ -0,0 +1,16 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local DataProvider = require(script.Parent.Parent.DataProvider) + local ServerJobsChart = require(script.Parent.ServerJobsChart) + + it("should create and destroy without errors", function() + local element = Roact.createElement(DataProvider, {},{ + ServerJobsChart = Roact.createElement(ServerJobsChart) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerJobs/ServerJobsData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerJobs/ServerJobsData.lua new file mode 100644 index 0000000..b6344a6 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerJobs/ServerJobsData.lua @@ -0,0 +1,146 @@ +local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer) +local Signal = require(script.Parent.Parent.Parent.Signal) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local HEADER_NAMES = Constants.ServerJobsFormatting.ChartHeaderNames + +local MAX_DATASET_COUNT = tonumber(settings():GetFVariable("NewDevConsoleMaxGraphCount")) + +local SORT_COMPARATOR = { + [HEADER_NAMES[1]] = function(a, b) -- "Name" + return a.name < b.name + end, + [HEADER_NAMES[2]] = function(a, b) -- "DutyCycle(%)" + return a.dataStats.dataSet:back().data[1] < b.dataStats.dataSet:back().data[1] + end, + [HEADER_NAMES[3]] = function(a, b) -- "Steps Per Sec (/s)" + return a.dataStats.dataSet:back().data[2] < b.dataStats.dataSet:back().data[2] + end, + [HEADER_NAMES[4]] = function(a, b) -- "Step Time (ms)" + return a.dataStats.dataSet:back().data[3] < b.dataStats.dataSet:back().data[3] + end, +} + +local minOfTable = require(script.Parent.Parent.Parent.Util.minOfTable) +local maxOfTable = require(script.Parent.Parent.Parent.Util.maxOfTable) +local getClientReplicator = require(script.Parent.Parent.Parent.Util.getClientReplicator) + +local ServerJobsData = {} +ServerJobsData.__index = ServerJobsData + +function ServerJobsData.new() + local self = {} + setmetatable(self, ServerJobsData) + + self._serverJobsUpdated = Signal.new() + self._serverJobsData = {} + self._sortedJobsData = {} + self._sortType = HEADER_NAMES[1] -- Name + self._lastUpdate = 0 + + return self +end + +function ServerJobsData:setSortType(sortType) + if SORT_COMPARATOR[sortType] then + self._sortType = sortType + -- do we need a mutex type thing here? + table.sort(self._sortedJobsData, SORT_COMPARATOR[self._sortType]) + else + error(string.format("attempted to pass invalid sortType: %s", tostring(sortType)), 2) + end +end + +function ServerJobsData:getSortType() + return self._sortType +end + +function ServerJobsData:Signal() + return self._serverJobsUpdated +end + +function ServerJobsData:getCurrentData() + return self._sortedJobsData +end + +function ServerJobsData:updateServerJobsData(updatedJobs) + self._lastUpdate = os.time() + for key, data in pairs(updatedJobs) do + if not self._serverJobsData[key] then + local newBuffer = CircularBuffer.new(MAX_DATASET_COUNT) + newBuffer:push_back({ + data = data, + time = self._lastUpdate, + }) + + self._serverJobsData[key] = { + max = data, + min = data, + dataSet = newBuffer, + } + + local newEntry = { + name = key, + dataStats = self._serverJobsData[key], + } + + table.insert(self._sortedJobsData, newEntry) + else + local currMax = self._serverJobsData[key].max + local currMin = self._serverJobsData[key].min + + local update = { + data = data, + time = self._lastUpdate + } + + local overwrittenEntry = self._serverJobsData[key].dataSet:push_back(update) + + if overwrittenEntry then + local iter = self._serverJobsData[key].dataSet:iterator() + local dat = iter:next() + if currMax == overwrittenEntry.data then + currMax = currMin + while dat do + currMax = maxOfTable(dat, currMax) + dat = iter:next() + end + end + if currMin == overwrittenEntry.data then + currMin = currMax + while dat do + currMin = minOfTable(dat, currMin) + dat = iter:next() + end + end + end + + self._serverJobsData[key].max = maxOfTable(currMax, data) + self._serverJobsData[key].min = minOfTable(currMin, data) + end + end +end + +function ServerJobsData:start() + local clientReplicator = getClientReplicator() + if clientReplicator and not self._statsListenerConnection then + self._statsListenerConnection = clientReplicator.StatsReceived:connect(function(stats) + local serverJobsList = stats.Jobs + + if serverJobsList then + self:updateServerJobsData(serverJobsList) + self._serverJobsUpdated:Fire(self._sortedJobsData) + end + end) + clientReplicator:RequestServerStats(true) + end +end + +function ServerJobsData:stop() + if self._statsListenerConnection then + self._statsListenerConnection:Disconnect() + self._statsListenerConnection = nil + end +end + +return ServerJobsData \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerStats/MainViewServerStats.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerStats/MainViewServerStats.lua new file mode 100644 index 0000000..3fdd2aa --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerStats/MainViewServerStats.lua @@ -0,0 +1,105 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) + +local Components = script.Parent.Parent.Parent.Components +local ServerStatsChart = require(Components.ServerStats.ServerStatsChart) +local UtilAndTab = require(Components.UtilAndTab) + +local Actions = script.Parent.Parent.Parent.Actions +local ServerStatsUpdateSearchFilter = require(Actions.ServerStatsUpdateSearchFilter) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local PADDING = Constants.GeneralFormatting.MainRowPadding + +local MainViewServerStats = Roact.Component:extend("MainViewServerStats") + +function MainViewServerStats:init() + self.onUtilTabHeightChanged = function(utilTabHeight) + self:setState({ + utilTabHeight = utilTabHeight + }) + end + + self.onSearchTermChanged = function(newSearchTerm) + self.props.dispatchServerStatsUpdateSearchFilter(newSearchTerm, {}) + end + + self.utilRef = Roact.createRef() + + self.state = { + utilTabHeight = 0 + } +end + +function MainViewServerStats:didMount() + local utilSize = self.utilRef.current.Size + self:setState({ + utilTabHeight = utilSize.Y.Offset + }) +end + +function MainViewServerStats:didUpdate() + local utilSize = self.utilRef.current.Size + if utilSize.Y.Offset ~= self.state.utilTabHeight then + self:setState({ + utilTabHeight = utilSize.Y.Offset + }) + end +end + +function MainViewServerStats:render() + local size = self.props.size + local formFactor = self.props.formFactor + local tabList = self.props.tabList + local searchTerm = self.props.statsSearchTerm + + local utilTabHeight = self.state.utilTabHeight + + return Roact.createElement("Frame",{ + Size = size, + BackgroundColor3 = Constants.Color.BaseGray, + BackgroundTransparency = 1, + LayoutOrder = 3, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + Padding = UDim.new(0, PADDING), + SortOrder = Enum.SortOrder.LayoutOrder, + }), + + UtilAndTab = Roact.createElement(UtilAndTab,{ + windowWidth = size.X.Offset, + formFactor = formFactor, + tabList = tabList, + searchTerm = searchTerm, + layoutOrder = 1, + + refForParent = self.utilRef, + + onHeightChanged = self.onUtilTabHeightChanged, + onSearchTermChanged = self.onSearchTermChanged, + }), + + ServerStats = utilTabHeight > 0 and Roact.createElement(ServerStatsChart, { + size = UDim2.new(1, 0, 1, -utilTabHeight), + searchTerm = searchTerm, + layoutOrder = 2, + }) + }) +end + +local function mapStateToProps(state, props) + return { + statsSearchTerm = state.ServerStatsData.statsSearchTerm, + } +end + +local function mapDispatchToProps(dispatch) + return { + dispatchServerStatsUpdateSearchFilter = function(searchTerm, filters) + dispatch(ServerStatsUpdateSearchFilter(searchTerm, filters)) + end, + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(MainViewServerStats) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerStats/MainViewServerStats.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerStats/MainViewServerStats.spec.lua new file mode 100644 index 0000000..b02e197 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerStats/MainViewServerStats.spec.lua @@ -0,0 +1,37 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local RoactRodux = require(CorePackages.RoactRodux) + local Store = require(CorePackages.Rodux).Store + + local DataProvider = require(script.Parent.Parent.DataProvider) + + local MainViewServerStats = require(script.Parent.MainViewServerStats) + + it("should create and destroy without errors", function() + local store = Store.new(function() + return { + MainView = { + currTabIndex = 0 + }, + ServerStatsData = { + statsSearchTerm = "" + } + } + end) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + DataProvider = Roact.createElement(DataProvider, {},{ + MainViewServerStats = Roact.createElement(MainViewServerStats,{ + size = UDim2.new(), + tabList = {}, + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerStats/ServerStatsChart.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerStats/ServerStatsChart.lua new file mode 100644 index 0000000..5f0eecf --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerStats/ServerStatsChart.lua @@ -0,0 +1,228 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Components = script.Parent.Parent.Parent.Components +local DataConsumer = require(Components.DataConsumer) +local CellLabel = require(Components.CellLabel) +local BannerButton = require(Components.BannerButton) +local LineGraph = require(Components.LineGraph) + +local Constants = require(script.Parent.Parent.Parent.Constants) +local LINE_WIDTH = Constants.GeneralFormatting.LineWidth +local LINE_COLOR = Constants.GeneralFormatting.LineColor +local HEADER_NAMES = Constants.ServerStatsFormatting.ChartHeaderNames +local VALUE_CELL_WIDTH = Constants.ServerStatsFormatting.ValueCellWidth +local CELL_PADDING = Constants.ServerStatsFormatting.CellPadding +local ARROW_PADDING = Constants.ServerStatsFormatting.ExpandArrowPadding +local HEADER_HEIGHT = Constants.ServerStatsFormatting.HeaderFrameHeight +local ENTRY_HEIGHT = Constants.ServerStatsFormatting.EntryFrameHeight + +local GRAPH_HEIGHT = Constants.GeneralFormatting.LineGraphHeight +local NO_DATA_MSG = "Awaiting Server Stats" + +local convertTimeStamp = require(script.Parent.Parent.Parent.Util.convertTimeStamp) + +local ServerStats = Roact.Component:extend("ServerStats") + +local function formatData(data) + return string.format("%.3f", data) +end +local function getX(entry) + return entry.time +end + +local function getY(entry) + return entry.value +end + +function ServerStats:init() + local currStatsData = self.props.ServerStatsData:getCurrentData() + + self.getOnButtonPress = function (name) + return function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or + (input.UserInputType == Enum.UserInputType.Touch and + input.UserInputState == Enum.UserInputState.End) then + self:setState({ + expandedEntry = self.state.expandedEntry ~= name and name + }) + end + end + end + + self.state = { + serverStatsData = currStatsData, + } +end + +function ServerStats:didMount() + self.statsConnector = self.props.ServerStatsData:Signal():Connect(function(data) + self:setState({ + serverStatsData = data, + }) + end) +end + +function ServerStats:willUnmount() + self.statsConnector:Disconnect() + self.statsConnector = nil +end + +function ServerStats:render() + local elements = {} + local searchTerm = self.props.searchTerm + local layoutOrder = self.props.layoutOrder + local size = self.props.size + + local expandedEntry = self.state.expandedEntry + local serverStatsData = self.state.serverStatsData + + -- insert all stats entries + local currLayoutIndex = 1 + local entryCount = 0 + local graphCount = 0 + if serverStatsData then + for name, data in pairs(serverStatsData) do + if not searchTerm or string.find(name:lower(), searchTerm:lower()) ~= nil then + currLayoutIndex = currLayoutIndex + 1 + + local showGraph = expandedEntry == name + local frameHeight = showGraph and ENTRY_HEIGHT + GRAPH_HEIGHT or ENTRY_HEIGHT + + entryCount = entryCount + 1 + graphCount = showGraph and graphCount + 1 or graphCount + + elements[name] = Roact.createElement("Frame",{ + Size = UDim2.new(1, 0, 0, frameHeight), + BackgroundTransparency = 1, + LayoutOrder = currLayoutIndex, + }, { + DataButton = Roact.createElement(BannerButton,{ + size = UDim2.new(1, 0, 0, ENTRY_HEIGHT), + pos = UDim2.new(), + isExpanded = showGraph, + + onButtonPress = self.getOnButtonPress(name), + }, { + [name] = Roact.createElement(CellLabel,{ + text = name, + size = UDim2.new(1 - VALUE_CELL_WIDTH, -CELL_PADDING - ARROW_PADDING, 1, 0), + pos = UDim2.new(0, CELL_PADDING + ARROW_PADDING, 0, 0), + }), + + Data = Roact.createElement(CellLabel,{ + text = formatData(data.dataSet:back().value), + size = UDim2.new(VALUE_CELL_WIDTH, -CELL_PADDING, 1, 0), + pos = UDim2.new(1 - VALUE_CELL_WIDTH, CELL_PADDING, 0, 0), + }), + + VerticalLine = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 1, 0), + Position = UDim2.new(1 - VALUE_CELL_WIDTH, 0, 0, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + + HorizontalAlignment = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + }), + + Graph = showGraph and Roact.createElement(LineGraph, { + pos = UDim2.new(0, 0, 0, ENTRY_HEIGHT), + size = UDim2.new(1, 0, 1, -ENTRY_HEIGHT), + graphData = data.dataSet, + minY = data.min, + maxY = data.max, + + getX = getX, + getY = getY, + + stringFormatX = convertTimeStamp, + stringFormatY = formatData, + + axisLabelX = "Timestamp", + axisLabelY = name, + }), + }) + end + end + end + + if currLayoutIndex == 1 then + return Roact.createElement("TextLabel",{ + Size = size, + Position = UDim2.new(0, 0, 0, 0), + Text = NO_DATA_MSG, + TextColor3 = Constants.Color.Text, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }) + end + + elements["UIListLayout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + local canvasHeight = entryCount * ENTRY_HEIGHT + graphCount * GRAPH_HEIGHT + -- layer over a vertical line to visually separate name and data + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + },{ + Header = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, HEADER_HEIGHT), + BackgroundTransparency = 1, + }, { + [HEADER_NAMES[1]] = Roact.createElement(CellLabel,{ + text = HEADER_NAMES[1], + size = UDim2.new(1 - VALUE_CELL_WIDTH, -CELL_PADDING - ARROW_PADDING, 1, 0), + pos = UDim2.new(0, CELL_PADDING + ARROW_PADDING, 0, 0), + }), + + [HEADER_NAMES[2]] = Roact.createElement(CellLabel,{ + text = HEADER_NAMES[2], + size = UDim2.new(VALUE_CELL_WIDTH , -CELL_PADDING, 1, 0), + pos = UDim2.new(1 - VALUE_CELL_WIDTH, CELL_PADDING, 0, 0), + }), + + upperHorizontalLine = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + + VerticalLine = Roact.createElement("Frame", { + Size = UDim2.new(0, LINE_WIDTH, 1, 0), + Position = UDim2.new(1 - VALUE_CELL_WIDTH, 0, 0, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + + lowerHorizontalLine = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, LINE_WIDTH), + Position = UDim2.new(0, 0, 1, 0), + BackgroundColor3 = LINE_COLOR, + BorderSizePixel = 0, + }), + }), + + mainFrame = Roact.createElement("ScrollingFrame", { + Position = UDim2.new(0, 0, 0, HEADER_HEIGHT), + Size = UDim2.new(1, 0, 1, -HEADER_HEIGHT), + CanvasSize = UDim2.new(1, 0, 0, canvasHeight), + ScrollBarThickness = 5, + + BackgroundTransparency = 1, + }, elements), + }) +end + +return DataConsumer(ServerStats, "ServerStatsData") \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerStats/ServerStatsChart.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerStats/ServerStatsChart.spec.lua new file mode 100644 index 0000000..4212543 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerStats/ServerStatsChart.spec.lua @@ -0,0 +1,16 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local DataProvider = require(script.Parent.Parent.DataProvider) + local ServerStatsChart = require(script.Parent.ServerStatsChart) + + it("should create and destroy without errors", function() + local element = Roact.createElement(DataProvider, {},{ + ServerStatsChart = Roact.createElement(ServerStatsChart) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerStats/ServerStatsData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerStats/ServerStatsData.lua new file mode 100644 index 0000000..063d018 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/ServerStats/ServerStatsData.lua @@ -0,0 +1,117 @@ +local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer) +local Signal = require(script.Parent.Parent.Parent.Signal) + +local MAX_DATASET_COUNT = tonumber(settings():GetFVariable("NewDevConsoleMaxGraphCount")) +local AVG_PING_MS = "Avg Ping ms" + +local getClientReplicator = require(script.Parent.Parent.Parent.Util.getClientReplicator) + +local ServerStatsData = {} +ServerStatsData.__index = ServerStatsData + +function ServerStatsData.new() + local self = {} + setmetatable(self, ServerStatsData) + + self._serverStatsUpdated = Signal.new() + self._serverStatsPing = Signal.new() + self._serverStatsData = {} + self._serverStatsDataCount = 0 + self._lastUpdateTime = 0 + return self +end + +function ServerStatsData:avgPing() + return self._serverStatsPing +end + +function ServerStatsData:Signal() + return self._serverStatsUpdated +end + +function ServerStatsData:getCurrentData() + return self._serverStatsData +end + +function ServerStatsData:updateValue(key, value) + if not self._serverStatsData[key] then + local newBuffer = CircularBuffer.new(MAX_DATASET_COUNT) + newBuffer:push_back({ + value = value, + time = self._lastUpdateTime + }) + self._serverStatsData[key] = { + max = value, + min = value, + dataSet = newBuffer, + } + else + local dataEntry = self._serverStatsData[key] + local currMax = dataEntry.max + local currMin = dataEntry.min + + local update = { + value = value, + time = self._lastUpdateTime + } + + local overwrittenEntry = self._serverStatsData[key].dataSet:push_back(update) + + if overwrittenEntry then + local iter = self._serverStatsData[key].dataSet:iterator() + local dat = iter:next() + if currMax == overwrittenEntry.data then + currMax = currMin + while dat do + currMax = dat.value < currMax and currMax or dat.value + dat = iter:next() + end + end + if currMin == overwrittenEntry.data then + currMin = currMax + while dat do + currMin = currMin < dat.value and currMin or dat.value + dat = iter:next() + end + end + end + + self._serverStatsData[key].max = currMax < value and value or currMax + self._serverStatsData[key].min = currMin < value and currMin or value + end +end + +function ServerStatsData:start() + local clientReplicator = getClientReplicator() + if clientReplicator and not self._statsListenerConnection then + self._statsListenerConnection = clientReplicator.StatsReceived:connect(function(stats) + if stats then + self._lastUpdateTime = os.time() + local count = 0 + for k, v in pairs(stats) do + if type(v) == 'number' then + self:updateValue(k,v) + count = count + 1 + end + end + + self._serverStatsDataCount = count + self._serverStatsUpdated:Fire(self._serverStatsData) + if self._serverStatsData[AVG_PING_MS] then + self._serverStatsPing:Fire(self._serverStatsData[AVG_PING_MS].dataSet:back().value) + end + end + end) + clientReplicator:RequestServerStats(true) + end + +end + +function ServerStatsData:stop() + if self._statsListenerConnection then + self._statsListenerConnection:Disconnect() + self._statsListenerConnection = nil + end +end + +return ServerStatsData \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/TabRowButton.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/TabRowButton.lua new file mode 100644 index 0000000..d7393c1 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/TabRowButton.lua @@ -0,0 +1,47 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Constants) + +local HIGHLIGHT_HEIGHT = Constants.TabRowFormatting.HighlightHeight +local FRAME_HEIGHT = Constants.TabRowFormatting.FrameHeight + +local function TabRowButton(props) + local index = props.index + local name = props.name + local textWidth = props.textWidth + local padding = props.padding + local isSelected = props.isSelected + local onTabButtonClicked = props.onTabButtonClicked + + textWidth = textWidth + padding + local textTransparency = Constants.TabRowFormatting.UnselectedTextTransparency + if isSelected then + textTransparency = Constants.TabRowFormatting.SelectedTextTransparency + end + + return Roact.createElement("TextButton", { + Text = name, + TextSize = Constants.DefaultFontSize.TabBar, + Font = Constants.Font.TabBar, + TextScaled = false, + TextTransparency = textTransparency, + Size = UDim2.new(0, textWidth, 0, FRAME_HEIGHT), + AutoButtonColor = false, + TextColor3 = Constants.Color.Text, + BackgroundColor3 = Constants.Color.UnselectedGray, + BorderSizePixel = 0, + [Roact.Event.Activated] = function(rbx) + onTabButtonClicked(index) + end, + }, { + BlueHighlight = isSelected and Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, -HIGHLIGHT_HEIGHT), + Position = UDim2.new(0, 0, 1, 0), + BorderSizePixel = 0, + BackgroundColor3 = Constants.Color.HighlightBlue, + }) + }) +end + +return TabRowButton \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/TabRowButton.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/TabRowButton.spec.lua new file mode 100644 index 0000000..bed7ce7 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/TabRowButton.spec.lua @@ -0,0 +1,15 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + + local TabRowButton = require(script.Parent.TabRowButton) + + it("should create and destroy without errors", function() + local element = Roact.createElement(TabRowButton, { + textWidth = 0, + padding = 0, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/TabRowContainer.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/TabRowContainer.lua new file mode 100644 index 0000000..910664b --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/TabRowContainer.lua @@ -0,0 +1,140 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) +local TextService = game:GetService("TextService") + +local SetActiveTab = require(script.Parent.Parent.Actions.SetActiveTab) + +local TabRowButton = require(script.Parent.TabRowButton) +local DropDown = require(script.Parent.DropDown) +local FullScreenDropDownButton = require(script.Parent.FullScreenDropDownButton) + +local Constants = require(script.Parent.Parent.Constants) +local DROP_DOWN_WIDTH = Constants.TabRowFormatting.TabDropDownWidth + +local TabRowContainer = Roact.Component:extend("TabRowContainer") + +function TabRowContainer:init() + local tabs = self.props.tabList + local textWidths = {} + local totalLength = 0 + local count = 0 + + if tabs then + for ind, tab in ipairs(tabs) do + local textVector = TextService:GetTextSize( + tab.label, + Constants.DefaultFontSize.TabBar, + Constants.Font.TabBar, + Vector2.new(0, 0) + ) + textWidths[ind] = textVector.X + totalLength = totalLength + textVector.X + count = count + 1 + end + end + + self.state = { + textWidths = textWidths, + totalTextLength = totalLength, + totalTabCount = count, + currContainerWidth = 0 + } + + self.onTabButtonClicked = function(tabIndex) + self.props.dispatchSetActiveTab(tabIndex) + end +end + +function TabRowContainer:render() + local tabs = self.props.tabList + local currTabIndex = self.props.currTabIndex + local formFactor = self.props.formFactor + local currWindowWidth = self.props.windowWidth + local frameHeight = self.props.frameHeight + local layoutOrder = self.props.layoutOrder + + local textWidths = self.state.textWidths + local totalTextLength = self.state.totalTextLength + local totalTabCount = self.state.totalTabCount + + local nodes = {} + + local padding = (currWindowWidth - totalTextLength) / totalTabCount + + -- the remainder is used to center the row of tabs so the + -- snap when crossing integer boundaries is less noticeable + local remainder = (padding % 1) * totalTabCount / 2 + + local useDropDown = padding < 0 and currWindowWidth > 0 + local useFullScreenDropDown = formFactor == Constants.FormFactor.Small + + if useDropDown or useFullScreenDropDown then + local names = {} + for ind,tab in ipairs(tabs) do + names[ind] = tab.label + end + + if useFullScreenDropDown then + return Roact.createElement(FullScreenDropDownButton, { + buttonSize = UDim2.new(0, DROP_DOWN_WIDTH, 0, frameHeight), + dropDownList = names, + selectedIndex = currTabIndex, + onSelection = self.onTabButtonClicked, + layoutOrder = layoutOrder, + }) + elseif useDropDown then + return Roact.createElement(DropDown, { + buttonSize = UDim2.new(0, DROP_DOWN_WIDTH, 0, frameHeight), + dropDownList = names, + selectedIndex = currTabIndex, + onSelection = self.onTabButtonClicked, + }) + end + end + + if tabs then + for ind,tab in ipairs(tabs) do + nodes[ind] = Roact.createElement(TabRowButton, { + index = ind, + name = tab.label, + padding = padding, + textWidth = useDropDown and DROP_DOWN_WIDTH or textWidths[ind], + isSelected = (ind == currTabIndex), + LayoutOrder = ind, + + onTabButtonClicked = self.onTabButtonClicked, + }) + end + end + + nodes["UIListLayout"] = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Left, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Top, + FillDirection = Enum.FillDirection.Horizontal, + }) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, frameHeight), + Position = UDim2.new(0, remainder, 0, 0), + Transparency = 1, + LayoutOrder = layoutOrder, + }, nodes) +end + +local function mapStateToProps(state, props) + return { + currTabIndex = state.MainView.currTabIndex, + } +end + +local function mapDispatchToProps(dispatch) + return { + dispatchSetActiveTab = function(index) + dispatch(SetActiveTab(index)) + end + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(TabRowContainer) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/TabRowContainer.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/TabRowContainer.spec.lua new file mode 100644 index 0000000..8ef3b35 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/TabRowContainer.spec.lua @@ -0,0 +1,30 @@ +return function() +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) +local Store = require(CorePackages.Rodux).Store + +local TabRowContainer = require(script.Parent.TabRowContainer) + + it("should create and destroy without errors", function() + local store = Store.new(function() + return { + MainView = { + currTabIndex = 0 + } + } + end) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + TabRowContainer = Roact.createElement(TabRowContainer,{ + tabList = {}, + windowWidth = 0, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/UtilAndTab.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/UtilAndTab.lua new file mode 100644 index 0000000..af15ee0 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/UtilAndTab.lua @@ -0,0 +1,321 @@ +local CorePackages = game:GetService("CorePackages") +local TextService = game:GetService("TextService") +local Roact = require(CorePackages.Roact) + +local Constants = require(script.Parent.Parent.Constants) +local TAB_HEIGHT = Constants.TabRowFormatting.FrameHeight +local DROP_DOWN_WIDTH = Constants.TabRowFormatting.TabDropDownWidth + +local UTIL_HEIGHT = Constants.UtilityBarFormatting.FrameHeight +local SMALL_UTIL_HEIGHT = Constants.UtilityBarFormatting.SmallFrameHeight +local SMALL_PADDING = Constants.UtilityBarFormatting.SmallUtilPadding +local CS_BUTTON_WIDTH = Constants.UtilityBarFormatting.ClientServerButtonWidth +local SMALL_CS_BUTTON_WIDTH = Constants.UtilityBarFormatting.ClientServerDropDownWidth + +local PADDING = Constants.GeneralFormatting.MainRowPadding + +local CANCEL_BUTTON_TEXT = "Cancel" +local CANCEL_BUTTON_PADDING = 6 + +local Components = script.Parent +local ClientServerButton = require(Components.ClientServerButton) +local CheckBoxContainer = require(Components.CheckBoxContainer) +local TabRowContainer = require(Components.TabRowContainer) +local SearchBar = require(Components.SearchBar) + +local UtilAndTab = Roact.Component:extend("UtilAndTab") + +function UtilAndTab:init() + local tabList = self.props.tabList + local totalTabWidth = 0 + local tabCount = 0 + + for _, tab in ipairs(tabList) do + local textVector = TextService:GetTextSize( + tab.label, + Constants.DefaultFontSize.TabBar, + Constants.Font.TabBar, + Vector2.new(0, 0) + ) + totalTabWidth = totalTabWidth + textVector.X + tabCount = tabCount + 1 + end + + self.showSearchBar = function() + self:setState({ + activeSearchTerm = true, + }) + end + + self.cancelInput = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or + (input.UserInputType == Enum.UserInputType.Touch and + input.UserInputState == Enum.UserInputState.End) then + if self.searchRef.current then + self.searchRef.current.Text = "" + end + + local onSearchTermChanged = self.props.onSearchTermChanged + if onSearchTermChanged then + onSearchTermChanged("") + end + + -- the clear button displays based on if the + self:setState({ + activeSearchTerm = false + }) + end + end + + self.focusLost = function(rbx, enterPressed, inputThatCausedFocusLoss) + if enterPressed then + local searchTerm = rbx.text + local onSearchTermChanged = self.props.onSearchTermChanged + if onSearchTermChanged then + onSearchTermChanged(searchTerm) + end + local hasSearchTerm = searchTerm ~= "" + if self.state.activeSearchTerm ~= hasSearchTerm then + self:setState({ + activeSearchTerm = hasSearchTerm, + }) + end + end + end + + self.state = { + totalTabWidth = totalTabWidth, + totalTabCount = tabCount, + activeSearchTerm = false, + } + + self.utilRef = Roact.createRef() + self.searchRef = Roact.createRef() +end + +function UtilAndTab:render() + local windowWidth = self.props.windowWidth + local formFactor = self.props.formFactor + local tabList = self.props.tabList + local checkBoxNames = self.props.checkBoxNames + local layoutOrder = self.props.layoutOrder + local isClientView = self.props.isClientView + local searchTerm = self.props.searchTerm + + local onClientButton = self.props.onClientButton + local onServerButton = self.props.onServerButton + local onCheckBoxesChanged = self.props.onCheckBoxesChanged + local onSearchTermChanged = self.props.onSearchTermChanged + + local totalTabWidth = self.state.totalTabWidth + local totalTabCount = self.state.totalTabCount + local activeSearchTerm = self.state.activeSearchTerm + + local tabOverLap = (windowWidth - totalTabWidth) / totalTabCount + + local useDropDown = tabOverLap < 0 and windowWidth > 0 + + if (formFactor == Constants.FormFactor.Small) or + useDropDown then + local frameHeight = SMALL_UTIL_HEIGHT + SMALL_PADDING + if activeSearchTerm then + frameHeight = frameHeight + SMALL_UTIL_HEIGHT + SMALL_PADDING + end + + local useCSButton = onClientButton and onServerButton + + local endFrameWidth = windowWidth - SMALL_UTIL_HEIGHT - DROP_DOWN_WIDTH + + if useCSButton then + endFrameWidth = endFrameWidth - SMALL_CS_BUTTON_WIDTH + end + + local cancelButtonWidth = TextService:GetTextSize( + CANCEL_BUTTON_TEXT, + Constants.DefaultFontSize.UtilBar, + Constants.Font.UtilBar, + Vector2.new(0, 0) + ).X + + cancelButtonWidth = cancelButtonWidth + (2 * CANCEL_BUTTON_PADDING) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, frameHeight), + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + + [Roact.Ref] = self.props.refForParent + }, { + MainFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, SMALL_UTIL_HEIGHT), + BackgroundTransparency = 1, + [Roact.Ref] = self.utilRef, + }, { + MainRow = Roact.createElement("Frame", { + Size = UDim2.new(1,0,1,0), + BackgroundTransparency = 1, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Left, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Top, + FillDirection = Enum.FillDirection.Horizontal, + Padding = UDim.new(0,SMALL_PADDING), + }), + + Tabs = Roact.createElement(TabRowContainer, { + tabList = tabList, + windowWidth = windowWidth, + frameHeight = SMALL_UTIL_HEIGHT, + formFactor = formFactor, + layoutOrder = 1, + }), + + ClientServerButton = useCSButton and Roact.createElement(ClientServerButton, { + frameHeight = SMALL_UTIL_HEIGHT, + formFactor = formFactor, + useDropDown = useDropDown, + isClientView = isClientView, + layoutOrder = 2, + onClientButton = onClientButton, + onServerButton = onServerButton, + }), + + FilterCheckBoxes = onCheckBoxesChanged and Roact.createElement(CheckBoxContainer, { + boxNames = checkBoxNames, + frameWidth = endFrameWidth, + frameHeight = SMALL_UTIL_HEIGHT, + pos = UDim2.new(0, 2 * (CS_BUTTON_WIDTH) + PADDING, 0, 0), + layoutOrder = 3, + onCheckBoxesChanged = onCheckBoxesChanged, + }), + + }), + + SearchButton = Roact.createElement("ImageButton", { + Size = UDim2.new(0, SMALL_UTIL_HEIGHT, 0, SMALL_UTIL_HEIGHT), + Position = UDim2.new(1,-SMALL_UTIL_HEIGHT, 0, 0), + BackgroundTransparency = 1, + Image = Constants.Image.Search, + Visible = not activeSearchTerm, + + [Roact.Event.Activated] = self.showSearchBar, + }), + }), + + -- the searchBar is only visible when there is an active searchterm in the textbox + SearchBarFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, SMALL_UTIL_HEIGHT), + Position = UDim2.new(0, 0, 0, SMALL_UTIL_HEIGHT + SMALL_PADDING), + Visible = activeSearchTerm, + BorderSizePixel = 0, + BackgroundTransparency = 1, + + }, { + SearchBar = Roact.createElement(SearchBar, { + size = UDim2.new(1, -cancelButtonWidth , 0, SMALL_UTIL_HEIGHT), + searchTerm = searchTerm, + showClear = activeSearchTerm, + textSize = Constants.DefaultFontSize.UtilBar, + font = Constants.Font.UtilBar, + frameHeight = SMALL_UTIL_HEIGHT, + + refForParent = self.searchRef, + cancelInput = self.cancelInput, + focusLost = self.focusLost, + }), + + CancelButton = Roact.createElement("TextButton", { + Size = UDim2.new(0, cancelButtonWidth, 1, 0), + Position = UDim2.new(1, -cancelButtonWidth, 0, 0), + Text = CANCEL_BUTTON_TEXT, + TextSize = Constants.DefaultFontSize.UtilBar, + TextColor3 = Constants.Color.Text, + Font = Constants.Font.UtilBar, + BorderSizePixel = 0, + BackgroundTransparency = 1, + + [Roact.Event.Activated] = self.cancelInput, + }) + }) + }) + else + local useCSButton = onClientButton and onServerButton + + local endFrameWidth = windowWidth - (SMALL_PADDING * 3) - (3 * CS_BUTTON_WIDTH) + + if useCSButton then + endFrameWidth = endFrameWidth - CS_BUTTON_WIDTH + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, TAB_HEIGHT + UTIL_HEIGHT + PADDING), + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + + [Roact.Ref] = self.props.refForParent, + }, { + Tabs = Roact.createElement(TabRowContainer, { + tabList = tabList, + windowWidth = windowWidth, + frameHeight = TAB_HEIGHT, + formFactor = formFactor, + }), + + UtilBar = Roact.createElement("Frame", { + Position = UDim2.new(0, 0, 0, TAB_HEIGHT + PADDING), + Size = UDim2.new(1, 0, 0, UTIL_HEIGHT), + BackgroundTransparency = 1, + + [Roact.Ref] = self.utilRef, + }, { + MainRow = Roact.createElement("Frame", { + Size = UDim2.new(1,0,1,0), + BackgroundTransparency = 1, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Left, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Top, + FillDirection = Enum.FillDirection.Horizontal, + Padding = UDim.new(0,SMALL_PADDING), + }), + + ClientServerButton = useCSButton and Roact.createElement(ClientServerButton, { + formFactor = formFactor, + isClientView = isClientView, + onClientButton = onClientButton, + onServerButton = onServerButton, + }), + + FilterCheckBoxes = onCheckBoxesChanged and Roact.createElement(CheckBoxContainer, { + boxNames = checkBoxNames, + frameWidth = endFrameWidth, + frameHeight = UTIL_HEIGHT, + pos = UDim2.new(0, 2 * (CS_BUTTON_WIDTH) + PADDING, 0, 0), + onCheckBoxesChanged = onCheckBoxesChanged, + }), + + }), + + SearchBar = onSearchTermChanged and Roact.createElement(SearchBar, { + size = UDim2.new(0, 2 * CS_BUTTON_WIDTH, 0, UTIL_HEIGHT), + pos = UDim2.new(1, -2 * CS_BUTTON_WIDTH, 0, 0), + searchTerm = searchTerm, + showClear = activeSearchTerm, + textSize = Constants.DefaultFontSize.UtilBar, + font = Constants.Font.UtilBar, + frameHeight = Constants.UtilityBarFormatting.FrameHeight, + + refForParent = self.searchRef, + cancelInput = self.cancelInput, + focusLost = self.focusLost, + }), + }), + }) + end + + +end + +return UtilAndTab diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/UtilAndTab.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/UtilAndTab.spec.lua new file mode 100644 index 0000000..06ed1c8 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Components/UtilAndTab.spec.lua @@ -0,0 +1,31 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local RoactRodux = require(CorePackages.RoactRodux) + local Store = require(CorePackages.Rodux).Store + + local UtilAndTab = require(script.Parent.UtilAndTab) + + it("should create and destroy without errors", function() + local store = Store.new(function() + return { + MainView = { + currTabIndex = 1, + } + } + end) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + utilAndTab = Roact.createElement(UtilAndTab, { + tabList = {}, + windowWidth = 0, + }) + + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Constants.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Constants.lua new file mode 100644 index 0000000..7ec8833 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Constants.lua @@ -0,0 +1,221 @@ +local Constants = { + MainWindowInit = { + Position = UDim2.new(.5, -486, .02, 16), + Size = UDim2.new(0, 992, .96, -32), + Transparency = .36, + Padding = UDim.new(0, 5), + MinSize = Vector2.new(400, 250), + }, + FormFactor = { + Large = 0, + Middle = 1, + Small = 2, + Console = 3, + }, + Color = { + Black = Color3.fromRGB(0, 0, 0), + BaseGray = Color3.fromRGB(30, 30, 30), + TextBoxGray = Color3.fromRGB(45, 45, 45), + ErrorRed = Color3.fromRGB(215, 90, 74), + HighlightBlue = Color3.fromRGB(0, 162, 255), + WarningYellow = Color3.fromRGB(255, 218, 68), + SelectedBlue = Color3.fromRGB(50, 181, 255), + UnselectedGray = Color3.fromRGB(78, 84, 96), + SelectedGray = Color3.fromRGB(102, 108, 119), + HoverGreen = Color3.fromRGB(70, 197, 124), + TabUnselectedGray = Color3.fromRGB(102, 108, 119), + BorderGray = Color3.fromRGB(184, 184, 184), + Text = Color3.fromRGB(255, 255, 255), + ActiveBox = Color3.fromRGB(63, 198, 121), + InactiveBox = Color3.fromRGB(184, 184, 184), + }, + Icon = { + -- These values appear differently because of the discrepancy between design sizes and + -- the engine sizes + Info = "Info.png", + Error = "Error.png", + Warning = "Warning.png", + Close = "Close.png", + Sort = "Sort.png", + Search = "Search.png", + Maximize = "Maximize.png", + Minimize = "Minimize.png", + }, + Image = { + Minimize = "rbxasset://textures/DevConsole/Minimize.png", + Maximize = "rbxasset://textures/DevConsole/Maximize.png", + Clear = "rbxasset://textures/DevConsole/Clear.png", + Close = "rbxasset://textures/DevConsole/Close.png", + Search = "rbxasset://textures/DevConsole/Search.png", + Error = "rbxasset://textures/DevConsole/Error.png", + Warning = "rbxasset://textures/DevConsole/Warning.png", + Info = "rbxasset://textures/DevConsole/Info.png", + Check = "rbxasset://textures/ui/LuaChat/icons/ic-check.png", + FilterUnfilled = "rbxasset://textures/DevConsole/Filter-stroke.png", + FilterFilled = "rbxasset://textures/DevConsole/Filter-filled.png", + RightArrow = "rbxasset://textures/DevConsole/Arrow.png", -- we want rotate this for the over effects + DownArrow = "rbxasset://textures/TerrainTools/button_arrow_down.png", -- current use a down arrow because we can't rotate + }, + Padding = { + WindowPadding = 8, + TabRow = 24, + LinePadding = 2, + MemoryIndent = 24, + }, + -- the commented numbers here are the font sizes given by the design spec + -- they were changed because the sizing did not match the the visuals in the design spec + DefaultFontSize = { + TopBar = 18, + TabBar = 20, + DropDownTabBar = 18, + UtilBar = 18, + MainWindowHeader = 12, + MainWindow = 15, + CommandLine = 15, + }, + Font = { + TopBar = Enum.Font.SourceSans, + TabBar = Enum.Font.SourceSansBold, + UtilBar = Enum.Font.SourceSans, + MainWindowHeader = Enum.Font.SourceSansBold, + MainWindow = Enum.Font.SourceSans, + }, + GeneralFormatting = { + LineWidth = 1, + LineColor = Color3.new(1, 1, 1), + ArrowWidth = 8, + MainRowPadding = 8, + LineGraphHeight = 200, + HeaderFrameHeight = 20, + EntryFrameHeight = 30, + + DropDownEntryHeight = 40, + DropDownEntryWidth = 375, + DropDownArrowHeight = 12, + }, + TopBarFormatting = { + BarTransparency = .64, + FrameHeight = 30, + }, + TabRowFormatting = { + TabDropDownWidth = 144, + FrameHeight = 40, + HighlightHeight = 6, + SelectedTextTransparency = 0, + UnselectedTextTransparency = .5, + }, + UtilityBarFormatting = { + FrameHeight = 30, + SmallFrameHeight = 24, + SmallUtilPadding = 6, -- horizontal padding + ClientServerButtonWidth = 100, + ClientServerDropDownWidth = 84, + CheckBoxHeight = 16, + CheckBoxInnerPadding = 6 + }, + LogFormatting = { + IconHeight = 14, + TextFrameHeight = 20, + TextFramePadding = 2, + CommandLineHeight = 30, + CommandLineIndent = 30, + }, + EnumToMsgTypeName = { + [Enum.MessageType.MessageOutput.Value] = "Output", + [Enum.MessageType.MessageInfo.Value] = "Information", + [Enum.MessageType.MessageWarning.Value] = "Warning", + [Enum.MessageType.MessageError.Value] = "Error" + }, + + MsgTypeNamesOrdered = { + "Output", + "Information", + "Warning", + "Error" + }, + + MemoryFormatting = { + ChartHeaderNames = {"Name", "Value MB"}, + ValueCellWidth = .2, + DepthIndent = 24, + CellPadding = 24, + ValuePadding = 12, + HeaderFrameHeight = 20, + EntryFrameHeight = 30, + }, + + NetworkFormatting = { + SummaryHeaderNames = {"RequestType", "RequestCount", "FailedCount", "AvgTime(ms)", "MinTime(ms)", "MaxTime(ms)"}, + HttpAnalyticsKeys = {"RequestType", "RequestCount", "FailedCount", "AverageTime", "MinTime", "MaxTime"}, + ChartHeaderNames = {"No.", "Method", "Status", "Time(ms).", "RequestType", "URL"}, + SummaryCellWidths = {120, 120, 120, 120, 120}, -- width of cells 2-6; cell 1 fills remainder + ChartCellWidths = {72, 72, 72, 84, 140}, -- widths of cells 1-5; cell 6 is the filler + CellPadding = 16, + SummaryButtonHeight = 30, + HeaderFrameHeight = 20, + EntryFrameHeight = 30, + ResponseWidthRatio = .8, + ResponseStrHeight = 15, + MinFrameWidth = 750, + }, + + ServerScriptsFormatting = { + ChartHeaderNames = {"Name", "Activity (%)", "Rate (/s)"}, + ChartCellWidths = {200, 200}, -- width of cells 2-3; cell 1 fills remainder + HeaderFrameHeight = 20, + EntryFrameHeight = 30, + CellPadding = 16, + ActivityBoxWidth = 12, + ActivityBoxPadding = 14, + }, + + DataStoresFormatting = { + ChartHeaderNames = {"Name", "Value"}, + ValueCellWidth = 200, + CellPadding = 16, + ExpandArrowPadding = 12, + HeaderFrameHeight = 20, + EntryFrameHeight = 30, + }, + + ServerStatsFormatting = { + ChartHeaderNames = {"Name", "Value"}, + ValueCellWidth = .2, + CellPadding = 14, + ExpandArrowPadding = 12, + HeaderFrameHeight = 20, + EntryFrameHeight = 30, + }, + + ActionBindingsFormatting = { + ChartHeaderNames = {"Name", "Priority", "Security", "Action Name", "Input Types"}, + ChartCellWidths = {80, 100, 185}, -- width of cells 2-4; cell 1 fills remainder + CellPadding = 16, + ExpandArrowPadding = 12, + HeaderFrameHeight = 20, + EntryFrameHeight = 30, + MinFrameWidth = 654, + }, + + ServerJobsFormatting = { + ChartHeaderNames = {"Name", "DutyCycle(%)", "Steps Per Sec (/s)", "Step Time (ms)"}, + ValueCellWidth = {.31, .23, .23, .23}, -- width of cells 2-4; cell 1 fills remainder + CellPadding = 16, + ExpandArrowPadding = 12, + HeaderFrameHeight = 20, + EntryFrameHeight = 30, + MinFrameWidth = 654, + }, + + Graph = { + PointWidth = 4, + PointOffset = 2, -- should be 1/2 pointwidth + Padding = 0.15, + Scale = 0.7, -- should be 1 - (2 * Padding) + InnerPaddingY = 0.1, + InnerScaleY = 0.8, -- should be 1 - (2 * innerPaddingY) + TextPadding = 10, + } +} + +return Constants \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Immutable.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Immutable.lua new file mode 100644 index 0000000..a73d203 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Immutable.lua @@ -0,0 +1,141 @@ +--[[ + Provides functions for manipulating immutable data structures. +]] + +local Immutable = {} + +--[[ + Merges dictionary-like tables together. +]] +function Immutable.JoinDictionaries(...) + local result = {} + + for i = 1, select("#", ...) do + local dictionary = select(i, ...) + for key, value in pairs(dictionary) do + result[key] = value + end + end + + return result +end + +--[[ + Joins any number of lists together into a new list +]] +function Immutable.JoinLists(...) + local new = {} + + for listKey = 1, select("#", ...) do + local list = select(listKey, ...) + local len = #new + + for itemKey = 1, #list do + new[len + itemKey] = list[itemKey] + end + end + + return new +end + +--[[ + Creates a new copy of the dictionary and sets a value inside it. +]] +function Immutable.Set(dictionary, key, value) + local new = {} + + for key, value in pairs(dictionary) do + new[key] = value + end + + new[key] = value + + return new +end + +--[[ + Creates a new copy of the list with the given elements appended to it. +]] +function Immutable.Append(list, ...) + local new = {} + local len = #list + + for key = 1, len do + new[key] = list[key] + end + + for i = 1, select("#", ...) do + new[len + i] = select(i, ...) + end + + return new +end + +--[[ + Remove elements from a dictionary +]] +function Immutable.RemoveFromDictionary(dictionary, ...) + local result = {} + + for key, value in pairs(dictionary) do + local found = false + for listKey = 1, select("#", ...) do + if key == select(listKey, ...) then + found = true + break + end + end + if not found then + result[key] = value + end + end + + return result +end + +--[[ + Remove the given key from the list. +]] +function Immutable.RemoveFromList(list, removeIndex) + local new = {} + + for i = 1, #list do + if i ~= removeIndex then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Remove the range from the list starting from the index. +]] +function Immutable.RemoveRangeFromList(list, index, count) + local new = {} + + for i = 1, #list do + if i < index or i >= index + count then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Creates a new list that has no occurrences of the given value. +]] +function Immutable.RemoveValueFromList(list, removeValue) + local new = {} + + for i = 1, #list do + if list[i] ~= removeValue then + table.insert(new, list[i]) + end + end + + return new +end + +return Immutable diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ActionBindingsData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ActionBindingsData.lua new file mode 100644 index 0000000..7a59681 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ActionBindingsData.lua @@ -0,0 +1,14 @@ +local Immutable = require(script.Parent.Parent.Immutable) + +local ActionBindingsUpdateSearchFilter = require(script.Parent.Parent.Actions.ActionBindingsUpdateSearchFilter) + +return function(state, action) + local actionBindingsData = state or { + bindingsSearchTerm = "", + } + + if action.type == ActionBindingsUpdateSearchFilter.name then + return Immutable.Set(actionBindingsData, "bindingsSearchTerm", action.searchTerm) + end + return actionBindingsData +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ActionBindingsData.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ActionBindingsData.spec.lua new file mode 100644 index 0000000..5c897e0 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ActionBindingsData.spec.lua @@ -0,0 +1,19 @@ +return function() + local ActionBindingsData = require(script.Parent.Parent.Reducers.ActionBindingsData) + + it("has the expected fields, and only the expected fields", function() + local state = ActionBindingsData(nil, {}) + + local expectedKeys = { + bindingsSearchTerm = "", + } + + for key in pairs(expectedKeys) do + assert(state[key] ~= nil, string.format("Expected field %q", key)) + end + + for key in pairs(state) do + assert(expectedKeys[key] ~= nil, string.format("Did not expect field %q", key)) + end + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DataStoresData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DataStoresData.lua new file mode 100644 index 0000000..6ac11fa --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DataStoresData.lua @@ -0,0 +1,15 @@ +local Immutable = require(script.Parent.Parent.Immutable) + +local DataStoresUpdateSearchFilter = require(script.Parent.Parent.Actions.DataStoresUpdateSearchFilter) + +return function(state, action) + local dataStoresData = state or { + storesSearchTerm = "", + } + + if action.type == DataStoresUpdateSearchFilter.name then + return Immutable.Set(dataStoresData, "storesSearchTerm", action.searchTerm) + end + + return dataStoresData +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DataStoresData.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DataStoresData.spec.lua new file mode 100644 index 0000000..3190942 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DataStoresData.spec.lua @@ -0,0 +1,19 @@ +return function() + local DataStoresData = require(script.Parent.Parent.Reducers.DataStoresData) + + it("has the expected fields, and only the expected fields", function() + local state = DataStoresData(nil, {}) + + local expectedKeys = { + storesSearchTerm = "", + } + + for key in pairs(expectedKeys) do + assert(state[key] ~= nil, string.format("Expected field %q", key)) + end + + for key in pairs(state) do + assert(expectedKeys[key] ~= nil, string.format("Did not expect field %q", key)) + end + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DevConsoleDisplayOptions.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DevConsoleDisplayOptions.lua new file mode 100644 index 0000000..08eff60 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DevConsoleDisplayOptions.lua @@ -0,0 +1,42 @@ +local Immutable = require(script.Parent.Parent.Immutable) +local Constants = require(script.Parent.Parent.Constants) + +local SetDevConsoleVisibility = require(script.Parent.Parent.Actions.SetDevConsoleVisibility) +local SetDevConsoleMinimized = require(script.Parent.Parent.Actions.SetDevConsoleMinimized) +local ChangeDevConsoleSize = require(script.Parent.Parent.Actions.ChangeDevConsoleSize) +local SetDevConsolePosition = require(script.Parent.Parent.Actions.SetDevConsolePosition) +local SetActiveTab = require(script.Parent.Parent.Actions.SetActiveTab) + +return function(DisplayOptions, action) + local displayOptions = DisplayOptions or { + formFactor = Constants.FormFactor.Large, -- masterrace + isVisible = false, + isMinimized = false, -- false means windowed, otherwise shows up as a minimized bar + position = Constants.MainWindowInit.Position, + size = Constants.MainWindowInit.Size + } + + if action.type == SetDevConsoleVisibility.name then + local update = { + isVisible = action.isVisible + } + if not update.isVisible then + update.isMinimized = false + end + return Immutable.JoinDictionaries(displayOptions, update) + elseif action.type == SetDevConsolePosition.name then + return Immutable.Set(displayOptions, "position", action.position) + elseif action.type == SetDevConsoleMinimized.name then + return Immutable.Set(displayOptions, "isMinimized", action.isMinimized) + + elseif action.type == ChangeDevConsoleSize.name then + -- Desktop should be the only one that can changes the devconsole Size + if displayOptions.formFactor == Constants.FormFactor.Large then + return Immutable.Set(displayOptions, "size", action.newSize) + end + elseif action.type == SetActiveTab.name then + return Immutable.Set(displayOptions, "isMinimized", false) + end + + return displayOptions +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DevConsoleDisplayOptions.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DevConsoleDisplayOptions.spec.lua new file mode 100644 index 0000000..f9c5e05 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DevConsoleDisplayOptions.spec.lua @@ -0,0 +1,24 @@ +return function() + local Constants = require(script.Parent.Parent.Constants) + local DevConsoleDisplayOptions = require(script.Parent.Parent.Reducers.DevConsoleDisplayOptions) + + it("has the expected fields, and only the expected fields", function() + local state = DevConsoleDisplayOptions(nil, {}) + + local expectedKeys = { + formFactor = Constants.FormFactor.Large, + isVisible = false, + isMinimized = false, + position = Constants.MainWindowInit.Position, + size = Constants.MainWindowInit.Size, + } + + for key in pairs(expectedKeys) do + assert(state[key] ~= nil, string.format("Expected field %q", key)) + end + + for key in pairs(state) do + assert(expectedKeys[key] ~= nil, string.format("Did not expect field %q", key)) + end + end) +end diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DevConsoleReducer.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DevConsoleReducer.lua new file mode 100644 index 0000000..7f22392 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DevConsoleReducer.lua @@ -0,0 +1,27 @@ +local Reducers = script.Parent +local DevConsoleDisplayOptions = require(Reducers.DevConsoleDisplayOptions) +local MainView = require(Reducers.MainView) + +local MemoryData = require(Reducers.MemoryData) +local NetworkData = require(Reducers.NetworkData) +local ScriptsData = require(Reducers.ScriptsData) +local DataStoresData = require(Reducers.DataStoresData) +local ServerStatsData = require(Reducers.ServerStatsData) +local ServerJobsData = require(Reducers.ServerJobsData) +local ActionBindingsData = require(Reducers.ActionBindingsData) + +return function(state, action) + local devConsoleState = state or {} + return { + DisplayOptions = DevConsoleDisplayOptions(devConsoleState.DisplayOptions, action), + MainView = MainView(devConsoleState.MainView, action), + + MemoryData = MemoryData(devConsoleState.MemoryData, action), + NetworkData = NetworkData(devConsoleState.NetworkData, action), + ScriptsData = ScriptsData(devConsoleState.ScriptsData, action), + DataStoresData = DataStoresData(devConsoleState.DataStoresData, action), + ServerStatsData = ServerStatsData(devConsoleState.ServerStatsData, action), + ServerJobsData = ServerJobsData(devConsoleState.ServerJobsData, action), + ActionBindingsData = ActionBindingsData(devConsoleState.ActionBindingsData, action), + } +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DevConsoleReducer.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DevConsoleReducer.spec.lua new file mode 100644 index 0000000..96f1176 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/DevConsoleReducer.spec.lua @@ -0,0 +1,40 @@ +return function() + local Reducers = script.Parent.Parent.Reducers + + local DevConsoleReducer = require(Reducers.DevConsoleReducer) + local DevConsoleDisplayOptions = require(Reducers.DevConsoleDisplayOptions) + local MainView = require(Reducers.MainView) + + local MemoryData = require(Reducers.MemoryData) + local NetworkData = require(Reducers.NetworkData) + local ScriptsData = require(Reducers.ScriptsData) + local DataStoresData = require(Reducers.DataStoresData) + local ServerStatsData = require(Reducers.ServerStatsData) + local ServerJobsData = require(Reducers.ServerJobsData) + local ActionBindingsData = require(Reducers.ActionBindingsData) + + it("has the expected fields, and only the expected fields", function() + local state = DevConsoleReducer(nil, {}) + + local expectedKeys = { + DisplayOptions = DevConsoleDisplayOptions(nil,{}), + MainView = MainView(nil,{}), + + MemoryData = MemoryData(nil,{}), + NetworkData = NetworkData(nil,{}), + ScriptsData = ScriptsData(nil,{}), + DataStoresData = DataStoresData(nil,{}), + ServerStatsData = ServerStatsData(nil,{}), + ServerJobsData = ServerJobsData(nil,{}), + ActionBindingsData = ActionBindingsData(nil,{}) + } + + for key in pairs(expectedKeys) do + assert(state[key] ~= nil, string.format("Expected field %q", key)) + end + + for key in pairs(state) do + assert(expectedKeys[key] ~= nil, string.format("Did not expect field %q", key)) + end + end) +end diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/LogData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/LogData.lua new file mode 100644 index 0000000..e60c2e4 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/LogData.lua @@ -0,0 +1,66 @@ +local CorePackages = game:GetService("CorePackages") +local Rodux = require(CorePackages.Rodux) +local Immutable = require(script.Parent.Parent.Immutable) + +return function(state, action) + return Rodux.createReducer( { + clientSearchTerm = "", + clientTypeFilters = {}, + + serverSearchTerm = "", + serverTypeFilters = {}, + }, { + ClientLogAppendMessage = function(logData, action) + return Immutable.JoinDictionaries(logData, {clientData = Immutable.Append(logData.clientData, action.newMessage) }) + end, + ServerLogAppendMessage = function(logData, action) + return Immutable.JoinDictionaries(logData, {serverData = Immutable.Append(logData.serverData, action.newMessage) }) + end, + ClientLogAppendFilteredMessage = function(logData, action) + local update = { + clientData = Immutable.Append(logData.clientData, action.newMessage), + clientDataFiltered = Immutable.Append(logData.clientDataFiltered, action.newMessage) + } + return Immutable.JoinDictionaries(logData, update) + end, + ServerLogAppendFilteredMessage = function(logData, action) + local update = { + serverData = Immutable.Append(logData.serverData, action.newMessage), + serverDataFiltered = Immutable.Append(logData.serverDataFiltered, action.newMessage) + } + return Immutable.JoinDictionaries(logData, update) + + end, + ClientLogSetData = function(logData, action) + local update = { + clientData = action.newData, + clientDataFiltered = action.newDataFiltered + } + return Immutable.JoinDictionaries(logData, update) + + end, + ServerLogSetData = function(logData, action) + local update = { + serverData = action.newData, + serverDataFiltered = action.newDataFiltered + } + return Immutable.JoinDictionaries(logData, update) + + end, + ClientLogUpdateSearchFilter = function(logData, action) + local update = { + clientSearchTerm = action.searchTerm, + clientTypeFilters = Immutable.JoinDictionaries(logData.clientTypeFilters, action.filterTypes) + } + return Immutable.JoinDictionaries(logData, update) + + end, + ServerLogUpdateSearchFilter = function(logData, action) + local update = { + serverSearchTerm = action.searchTerm, + serverTypeFilters = Immutable.JoinDictionaries(logData.serverTypeFilters, action.filterTypes) + } + return Immutable.JoinDictionaries(logData, update) + end + })(state, action) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/LogData.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/LogData.spec.lua new file mode 100644 index 0000000..ee4a94b --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/LogData.spec.lua @@ -0,0 +1,23 @@ +return function() + local LogData = require(script.Parent.Parent.Reducers.LogData) + + it("has the expected fields, and only the expected fields", function() + local state = LogData(nil, {}) + + local expectedKeys = { + clientSearchTerm = "", + clientTypeFilters = {}, + + serverSearchTerm = "", + serverTypeFilters = {}, + } + + for key in pairs(expectedKeys) do + assert(state[key] ~= nil, string.format("Expected field %q", key)) + end + + for key in pairs(state) do + assert(expectedKeys[key] ~= nil, string.format("Did not expect field %q", key)) + end + end) +end diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/MainView.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/MainView.lua new file mode 100644 index 0000000..0137837 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/MainView.lua @@ -0,0 +1,21 @@ +local Immutable = require(script.Parent.Parent.Immutable) + +local SetActiveTab = require(script.Parent.Parent.Actions.SetActiveTab) + +return function(state, action) + local mainView = state or { + -- initializes to the first tab in the list of views which should be Log + isClientView = true, + currTabIndex = 1, + } + + if action.type == SetActiveTab.name then + local update = { + currTabIndex = action.newTabIndex, + isClientView = action.isClientView + } + return Immutable.JoinDictionaries(mainView, update) + end + + return mainView +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/MainView.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/MainView.spec.lua new file mode 100644 index 0000000..10cb7a5 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/MainView.spec.lua @@ -0,0 +1,23 @@ +return function() + local MainView = require(script.Parent.Parent.Reducers.MainView) + + it("has the expected fields, and only the expected fields", function() + local state = MainView(nil, {}) + + local expectedKeys = { + isClientView = true, + activeSearchTerm = nil, + currSearchFilter = nil, + contextualSearchAction = nil, + currTabIndex = 1, + } + + for key in pairs(expectedKeys) do + assert(state[key] ~= nil, string.format("Expected field %q", key)) + end + + for key in pairs(state) do + assert(expectedKeys[key] ~= nil, string.format("Did not expect field %q", key)) + end + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/MemoryData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/MemoryData.lua new file mode 100644 index 0000000..e846aec --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/MemoryData.lua @@ -0,0 +1,20 @@ +local Immutable = require(script.Parent.Parent.Immutable) + +local ClientMemoryUpdateSearchFilter = require(script.Parent.Parent.Actions.ClientMemoryUpdateSearchFilter) +local ServerMemoryUpdateSearchFilter = require(script.Parent.Parent.Actions.ServerMemoryUpdateSearchFilter) + +return function(state, action) + local memoryData = state or { + clientSearchTerm = "", + serverSearchTerm = "", + } + + if action.type == ClientMemoryUpdateSearchFilter.name then + return Immutable.Set(memoryData, "clientSearchTerm", action.searchTerm) + + elseif action.type == ServerMemoryUpdateSearchFilter.name then + return Immutable.Set(memoryData, "serverSearchTerm", action.searchTerm) + end + + return memoryData +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/MemoryData.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/MemoryData.spec.lua new file mode 100644 index 0000000..af189aa --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/MemoryData.spec.lua @@ -0,0 +1,20 @@ +return function() + local MemoryData = require(script.Parent.Parent.Reducers.MemoryData) + + it("has the expected fields, and only the expected fields", function() + local state = MemoryData(nil, {}) + + local expectedKeys = { + clientSearchTerm = "", + serverSearchTerm = "", + } + + for key in pairs(expectedKeys) do + assert(state[key] ~= nil, string.format("Expected field %q", key)) + end + + for key in pairs(state) do + assert(expectedKeys[key] ~= nil, string.format("Did not expect field %q", key)) + end + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/NetworkData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/NetworkData.lua new file mode 100644 index 0000000..13c4d67 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/NetworkData.lua @@ -0,0 +1,20 @@ +local Immutable = require(script.Parent.Parent.Immutable) + +local ClientNetworkUpdateSearchFilter = require(script.Parent.Parent.Actions.ClientNetworkUpdateSearchFilter) +local ServerNetworkUpdateSearchFilter = require(script.Parent.Parent.Actions.ServerNetworkUpdateSearchFilter) + +return function(state, action) + local networkData = state or { + clientSearchTerm = "", + serverSearchTerm = "", + } + + if action.type == ClientNetworkUpdateSearchFilter.name then + return Immutable.Set(networkData, "clientSearchTerm", action.searchTerm) + + elseif action.type == ServerNetworkUpdateSearchFilter.name then + return Immutable.Set(networkData, "serverSearchTerm", action.searchTerm) + end + + return networkData +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/NetworkData.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/NetworkData.spec.lua new file mode 100644 index 0000000..c550746 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/NetworkData.spec.lua @@ -0,0 +1,20 @@ +return function() + local NetworkData = require(script.Parent.Parent.Reducers.NetworkData) + + it("has the expected fields, and only the expected fields", function() + local state = NetworkData(nil, {}) + + local expectedKeys = { + clientSearchTerm = "", + serverSearchTerm = "", + } + + for key in pairs(expectedKeys) do + assert(state[key] ~= nil, string.format("Expected field %q", key)) + end + + for key in pairs(state) do + assert(expectedKeys[key] ~= nil, string.format("Did not expect field %q", key)) + end + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ScriptsData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ScriptsData.lua new file mode 100644 index 0000000..e5ee562 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ScriptsData.lua @@ -0,0 +1,32 @@ +local Immutable = require(script.Parent.Parent.Immutable) + +local ClientScriptsUpdateSearchFilter = require(script.Parent.Parent.Actions.ClientScriptsUpdateSearchFilter) +local ServerScriptsUpdateSearchFilter = require(script.Parent.Parent.Actions.ServerScriptsUpdateSearchFilter) + +return function(state, action) + local scriptsData = state or { + clientSearchTerm = "", + clientTypeFilters = {}, + serverSearchTerm = "", + serverTypeFilters = {}, + } + + if action.type == ClientScriptsUpdateSearchFilter.name then + local update = { + clientSearchTerm = action.searchTerm, + clientTypeFilters = Immutable.JoinDictionaries(scriptsData.clientTypeFilters, action.filterTypes) + } + return Immutable.JoinDictionaries(scriptsData, update) + + elseif action.type == ServerScriptsUpdateSearchFilter.name then + + local update = { + serverSearchTerm = action.searchTerm, + serverTypeFilters = Immutable.JoinDictionaries(scriptsData.serverTypeFilters, action.filterTypes) + } + return Immutable.JoinDictionaries(scriptsData, update) + + end + + return scriptsData +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ScriptsData.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ScriptsData.spec.lua new file mode 100644 index 0000000..3464be4 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ScriptsData.spec.lua @@ -0,0 +1,22 @@ +return function() + local ScriptsData = require(script.Parent.Parent.Reducers.ScriptsData) + + it("has the expected fields, and only the expected fields", function() + local state = ScriptsData(nil, {}) + + local expectedKeys = { + clientSearchTerm = "", + clientTypeFilters = {}, + serverSearchTerm = "", + serverTypeFilters = {}, + } + + for key in pairs(expectedKeys) do + assert(state[key] ~= nil, string.format("Expected field %q", key)) + end + + for key in pairs(state) do + assert(expectedKeys[key] ~= nil, string.format("Did not expect field %q", key)) + end + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ServerJobsData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ServerJobsData.lua new file mode 100644 index 0000000..196e94b --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ServerJobsData.lua @@ -0,0 +1,15 @@ +local Immutable = require(script.Parent.Parent.Immutable) + +local ServerJobsUpdateSearchFilter = require(script.Parent.Parent.Actions.ServerJobsUpdateSearchFilter) + +return function(state, action) + local serverJobsData = state or { + jobsSearchTerm = "", + } + + if action.type == ServerJobsUpdateSearchFilter.name then + return Immutable.Set(serverJobsData, "jobsSearchTerm", action.searchTerm) + end + + return serverJobsData +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ServerJobsData.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ServerJobsData.spec.lua new file mode 100644 index 0000000..1350791 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ServerJobsData.spec.lua @@ -0,0 +1,19 @@ +return function() + local ServerJobsData = require(script.Parent.Parent.Reducers.ServerJobsData) + + it("has the expected fields, and only the expected fields", function() + local state = ServerJobsData(nil, {}) + + local expectedKeys = { + jobsSearchTerm = "", + } + + for key in pairs(expectedKeys) do + assert(state[key] ~= nil, string.format("Expected field %q", key)) + end + + for key in pairs(state) do + assert(expectedKeys[key] ~= nil, string.format("Did not expect field %q", key)) + end + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ServerStatsData.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ServerStatsData.lua new file mode 100644 index 0000000..a4f0cdb --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ServerStatsData.lua @@ -0,0 +1,15 @@ +local Immutable = require(script.Parent.Parent.Immutable) + +local ServerStatsUpdateSearchFilter = require(script.Parent.Parent.Actions.ServerStatsUpdateSearchFilter) + +return function(state, action) + local serverStatsData = state or { + statsSearchTerm = "", + } + + if action.type == ServerStatsUpdateSearchFilter.name then + return Immutable.Set(serverStatsData, "statsSearchTerm", action.searchTerm) + end + + return serverStatsData +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ServerStatsData.spec.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ServerStatsData.spec.lua new file mode 100644 index 0000000..76332a9 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Reducers/ServerStatsData.spec.lua @@ -0,0 +1,19 @@ +return function() + local ServerStatsData = require(script.Parent.Parent.Reducers.ServerStatsData) + + it("has the expected fields, and only the expected fields", function() + local state = ServerStatsData(nil, {}) + + local expectedKeys = { + statsSearchTerm = "", + } + + for key in pairs(expectedKeys) do + assert(state[key] ~= nil, string.format("Expected field %q", key)) + end + + for key in pairs(state) do + assert(expectedKeys[key] ~= nil, string.format("Did not expect field %q", key)) + end + end) +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Signal.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Signal.lua new file mode 100644 index 0000000..8292822 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Signal.lua @@ -0,0 +1,51 @@ +--[[ + A limited, simple implementation of a Signal. + + Handlers are fired in order, and (dis)connections are properly handled when + executing an event. + + Signal uses Immutable to avoid invalidating the 'Fire' loop iteration. +]] + +local Immutable = require(script.Parent.Immutable) + +local Signal = {} + +Signal.__index = Signal + +function Signal.new() + local self = { + _listeners = {} + } + + setmetatable(self, Signal) + + return self +end + +function Signal:Connect(callback) + local listener = { + callback = callback, + isConnected = true, + } + self._listeners = Immutable.Append(self._listeners, listener) + + local function disconnect() + listener.isConnected = false + self._listeners = Immutable.RemoveValueFromList(self._listeners, listener) + end + + return { + Disconnect = disconnect + } +end + +function Signal:Fire(...) + for _, listener in ipairs(self._listeners) do + if listener.isConnected then + listener.callback(...) + end + end +end + +return Signal \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Util/convertTimeStamp.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Util/convertTimeStamp.lua new file mode 100644 index 0000000..23f46d3 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Util/convertTimeStamp.lua @@ -0,0 +1,23 @@ +local function numberWithZero(num) + return (num < 10 and "0" or "") .. num +end + +local function convertTimeStamp(timeStamp) + local localTime = math.floor(timeStamp - os.time() + tick()) + local dayTime = localTime % 86400 + + local hour = math.floor(dayTime / 3600) + + dayTime = dayTime - (hour * 3600) + local minute = math.floor(dayTime / 60) + + dayTime = dayTime - (minute * 60) + + local h = numberWithZero(hour) + local m = numberWithZero(minute) + local s = numberWithZero(dayTime) + + return string.format("%s:%s:%s", h, m, s) +end + +return convertTimeStamp \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Util/getClientReplicator.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Util/getClientReplicator.lua new file mode 100644 index 0000000..dc2c288 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Util/getClientReplicator.lua @@ -0,0 +1,12 @@ +local clientReplicator +local function getClientReplicator() + if clientReplicator == nil then + local networkClient = game:FindService("NetworkClient") + if networkClient then + clientReplicator = networkClient:FindFirstChildOfClass("ClientReplicator") + end + end + return clientReplicator +end + +return getClientReplicator \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Util/maxOfTable.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Util/maxOfTable.lua new file mode 100644 index 0000000..3c2bc0a --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Util/maxOfTable.lua @@ -0,0 +1,12 @@ +return function (tableA, tableB) + local maxTable = {} + for ind, valA in pairs(tableA) do + if tableB[ind] then + maxTable[ind] = valA < tableB[ind] and tableB[ind] or valA + else + error("tables did not have matching indices") + return + end + end + return maxTable +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Util/minOfTable.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Util/minOfTable.lua new file mode 100644 index 0000000..19e6094 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsole/Util/minOfTable.lua @@ -0,0 +1,12 @@ +return function (tableA, tableB) + local minTable = {} + for ind, valA in pairs(tableA) do + if tableB[ind] then + minTable[ind] = valA < tableB[ind] and valA or tableB[ind] + else + error("tables did not have matching indices") + return + end + end + return minTable +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DevConsoleMaster.lua b/Client2018/content/scripts/CoreScripts/Modules/DevConsoleMaster.lua new file mode 100644 index 0000000..8b44adc --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DevConsoleMaster.lua @@ -0,0 +1,201 @@ +local CoreGui = game:GetService("CoreGui") +local StarterGui = game:GetService("StarterGui") +local UserInputService = game:GetService("UserInputService") +local GuiService = game:GetService('GuiService') +local HttpRbxApiService = game:GetService('HttpRbxApiService') +local HttpService = game:GetService('HttpService') +local RunService = game:GetService('RunService') +local CorePackages = game:GetService("CorePackages") + +local Roact = require(CorePackages.Roact) +local Rodux = require(CorePackages.Rodux) +local RoactRodux = require(CorePackages.RoactRodux) + +local DevConsole = script.Parent.DevConsole +local Constants = require(DevConsole.Constants) + +local Components = DevConsole.Components +local DevConsoleWindow = require(Components.DevConsoleWindow) +local DataProvider = require(Components.DataProvider) +local Log = require(Components.Log.MainViewLog) +local Memory = require(Components.Memory.MainViewMemory) +local Network = require(Components.Network.MainViewNetwork) +local Scripts = require(Components.Scripts.MainViewScripts) +local DataStores = require(Components.DataStores.MainViewDataStores) +local ServerStats = require(Components.ServerStats.MainViewServerStats) +local ActionBindings = require(Components.ActionBindings.MainViewActionBindings) +local ServerJobs = require(Components.ServerJobs.MainViewServerJobs) + +local DevConsoleReducer = require(DevConsole.Reducers.DevConsoleReducer) + +local SetDevConsoleVisibility = require(DevConsole.Actions.SetDevConsoleVisibility) + +local DEV_TAB_LIST = { + { + label = "Log", + tab = Log, + }, { + label = "Memory", + tab = Memory, + }, { + label = "Network", + tab = Network, + }, { + label = "Scripts", + tab = Scripts, + }, { + label = "DataStores", + tab = DataStores, + }, { + label = "ServerStats", + tab = ServerStats, + }, { + label = "ActionBindings", + tab = ActionBindings, + }, { + label = "ServerJobs", + tab = ServerJobs, + } +} + +local PLAYER_TAB_LIST = { + { + label = "Log", + tab = Log, + }, { + label = "Memory", + tab = Memory, + }, +} + +local DevConsoleMaster = {} +DevConsoleMaster.__index = DevConsoleMaster + +local platformConversion = { + [Enum.Platform.Windows] = Constants.FormFactor.Large, + [Enum.Platform.OSX] = Constants.FormFactor.Large, + [Enum.Platform.IOS] = Constants.FormFactor.Small, + [Enum.Platform.Android] = Constants.FormFactor.Small, + [Enum.Platform.XBoxOne] = Constants.FormFactor.Console, + [Enum.Platform.PS4] = Constants.FormFactor.Console, + [Enum.Platform.PS3] = Constants.FormFactor.Console, + [Enum.Platform.XBox360] = Constants.FormFactor.Console, + [Enum.Platform.WiiU] = Constants.FormFactor.Console, + [Enum.Platform.NX] = Constants.FormFactor.Console, + [Enum.Platform.Ouya] = Constants.FormFactor.Console, + [Enum.Platform.AndroidTV] = Constants.FormFactor.Console, + [Enum.Platform.Chromecast] = Constants.FormFactor.Console, + [Enum.Platform.Linux] = Constants.FormFactor.Large, + [Enum.Platform.SteamOS] = Constants.FormFactor.Console, + [Enum.Platform.WebOS] = Constants.FormFactor.Large, + [Enum.Platform.DOS] = Constants.FormFactor.Large, + [Enum.Platform.BeOS] = Constants.FormFactor.Large, + [Enum.Platform.UWP] = Constants.FormFactor.Large, + [Enum.Platform.None] = Constants.FormFactor.Large, +} + +local function isDeveloper() + if RunService:IsStudio() then + return true + end + + local canManageSuccess, canManageResult = pcall(function() + local url = string.format("/users/%d/canmanage/%d", game:GetService("Players").LocalPlayer.UserId, game.PlaceId) + return HttpRbxApiService:GetAsync(url, Enum.ThrottlingPriority.Default, Enum.HttpRequestType.Default, true) + end) + if canManageSuccess and type(canManageResult) == "string" then + -- API returns: {"Success":BOOLEAN,"CanManage":BOOLEAN} + -- Convert from JSON to a table + -- pcall in case of invalid JSON + local success, result = pcall(function() + return HttpService:JSONDecode(canManageResult) + end) + if success and result.CanManage == true then + return true + end + end + return false +end + +function DevConsoleMaster.new() + local self = {} + setmetatable(self, DevConsoleMaster) + + -- will need to decide on whether to use DPI and screensize or + -- to use Platform to distinguish between the different form factors + local platformEnum = UserInputService:GetPlatform() + local formFactor = platformConversion[platformEnum] + local screenSizePixel = GuiService:GetScreenResolution() + + local developerConsoleView = isDeveloper() + + -- create store + self.store = Rodux.Store.new(DevConsoleReducer) + self.init = false + self.isVisible = false + + -- use connector to wrap store and root together + self.root = Roact.createElement(RoactRodux.StoreProvider, { + store = self.store, + }, { + DataProvider = Roact.createElement(DataProvider, { + isDeveloperView = developerConsoleView, + }, { + App = Roact.createElement("ScreenGui", {}, { + DevConsoleWindow = Roact.createElement(DevConsoleWindow, { + formFactor = formFactor, + isdeveloperView = developerConsoleView, + isVisible = false, -- determines if visible or not + isMinimized = false, -- false means windowed, otherwise shows up as a minimized bar + position = Constants.MainWindowInit.Position, + size = Constants.MainWindowInit.Size, + tabList = developerConsoleView and DEV_TAB_LIST or PLAYER_TAB_LIST + }) + }), + }) + }) + return self +end + +function DevConsoleMaster:Start() + if not self.init then + self.init = true + self.element = Roact.mount(self.root, CoreGui, "DevConsoleMaster") + end +end + +function DevConsoleMaster:Stop() + +end + +function DevConsoleMaster:ToggleVisibility() + self.isVisible = not self.store:getState().DisplayOptions.isVisible + self.store:dispatch(SetDevConsoleVisibility(self.isVisible)) +end + +function DevConsoleMaster:GetVisibility() + return self.isVisible +end + +function DevConsoleMaster:SetVisibility(value) + if type(value) == "boolean" then + self.isVisible = value + self.store:dispatch(SetDevConsoleVisibility(self.isVisible)) + end +end + +local master = DevConsoleMaster.new() +master:Start() + +StarterGui:RegisterGetCore("DevConsoleVisible", function() + return master:GetVisibility() +end) + +StarterGui:RegisterSetCore("DevConsoleVisible", function(visible) + if (type(visible) ~= "boolean") then + error("DevConsoleVisible must be given a boolean value.") + end + master:SetVisibility(visible) +end) + +return master \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/DeveloperConsoleModule.lua b/Client2018/content/scripts/CoreScripts/Modules/DeveloperConsoleModule.lua new file mode 100644 index 0000000..44ae3a7 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/DeveloperConsoleModule.lua @@ -0,0 +1,3362 @@ +-- Made by Tomarty (talk to me if you have questions) + +--[[ Globals ]]-- +-- Quick optimizations +local Instance_new = Instance.new +local UDim2_new = UDim2.new +local Color3_new = Color3.new +local math_max = math.max +local tick = tick +local pairs = pairs +local os_time = os.time + +local DEBUG = false + +local AnalyticsCategory_Game = "Game" +local AnalyticsAction_InitialOpenTab = "DeveloperConsole_InitialOpenTab" +local AnalyticsAction_ClickToOpenOpenTab = "DeveloperConsole_ClickToOpenOpenTab" + +--[[ Services ]]-- +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:FindFirstChild("RobloxGui") +local Modules = RobloxGui:FindFirstChild("Modules") + +local ContextActionService = game:GetService("ContextActionService") +local TextService = game:GetService("TextService") +local GuiService = game:GetService("GuiService") +local VRService = game:GetService("VRService") +local isTenFootInterface = GuiService:IsTenFootInterface() + +--[[ Modules ]]-- +local ClientMemoryAnalyzerClass = require(CoreGui.RobloxGui.Modules.Stats.ClientMemoryAnalyzer) +local ServerMemoryAnalyzerClass = require(CoreGui.RobloxGui.Modules.Stats.ServerMemoryAnalyzer) + +local StatsUtils = require(CoreGui.RobloxGui.Modules.Stats.StatsUtils) +local Style = require(CoreGui.RobloxGui.Modules.Stats.DeveloperConsoleStyle) +local Primitives = require(CoreGui.RobloxGui.Modules.Stats.DeveloperConsolePrimitives) + +--[[ Flags ]]-- +local function checkFFlag(flagName) + local flagSuccess, flagValue = pcall(function() + return settings():GetFFlag(flagName) + end) + return (flagSuccess and flagValue) +end + + +-- Eye candy uses RenderStepped +local EYECANDY_ENABLED = true + +local AUTO_TAB_WIDTH = -1 +local TAB_TEXT_SIZE = 14 +local TAB_TEXT_PADDING = 8 + + +local function CreateSignal() + local this = {} + + local mBindableEvent = Instance.new('BindableEvent') + local mAllCns = {} --all connection objects returned by mBindableEvent::connect + + --main functions + function this:connect(func) + if self ~= this then error("connect must be called with `:`, not `.`", 2) end + if type(func) ~= 'function' then + error("Argument #1 of connect must be a function, got a "..type(func), 2) + end + local cn = mBindableEvent.Event:Connect(func) + mAllCns[cn] = true + local pubCn = {} + function pubCn:disconnect() + cn:Disconnect() + mAllCns[cn] = nil + end + pubCn.Disconnect = pubCn.disconnect + + return pubCn + end + + function this:disconnect() + if self ~= this then error("disconnect must be called with `:`, not `.`", 2) end + for cn, _ in pairs(mAllCns) do + cn:Disconnect() + mAllCns[cn] = nil + end + end + + function this:wait() + if self ~= this then error("wait must be called with `:`, not `.`", 2) end + return mBindableEvent.Event:Wait() + end + + function this:fire(...) + if self ~= this then error("fire must be called with `:`, not `.`", 2) end + mBindableEvent:Fire(...) + end + + this.Connect = this.connect + this.Disconnect = this.disconnect + this.Wait = this.wait + this.Fire = this.fire + + return this +end + +-- This is a Signal that only calls once, then forgets about the function. It also accepts event listeners as functions +local CreateDisconnectSignal; do + local Methods = {} + local Metatable = {__index = Methods} + function Methods.fire(this, ...) + return this.Signal:fire(...) + end + function Methods.wait(this, ...) + return this.Signal:wait(...) + end + function Methods.connect(this, func) + local t = type(func) + if t == 'table' or t == 'userdata' then + -- Got event listener + local listener = func + function func() + listener:disconnect() + end + elseif t ~= 'function' then + error('Invalid disconnect method type: ' .. t, 2) + end + + local listener; + listener = this.Signal:connect(function(...) + if listener then + listener:disconnect() + listener = nil + func(...) + end + end) + return listener + end + function CreateDisconnectSignal() + return setmetatable({ + Signal = CreateSignal(); + }, Metatable) + end +end + +-- Services +local UserInputService = game:GetService('UserInputService') +local RunService = game:GetService('RunService') +local TouchEnabled = UserInputService.TouchEnabled + +local DeveloperConsole = {} + +local Methods = {} +local Metatable = {__index = Methods} + +------------------------- +-- Listener management -- +------------------------- +function Methods.ConnectSetVisible(devConsole, func) + -- This is used mainly for pausing rendering and stuff when the console isn't visible + func(devConsole.Visible) + return devConsole.VisibleChanged:connect(function(visible) + func(visible) + end) +end +function Methods.ConnectObjectSetVisible(devConsole, object, func) + -- Same as above, but used for calling methods like object:SetVisible() + func(object, devConsole.Visible) + return devConsole.VisibleChanged:connect(function(visible) + func(object, visible) + end) +end + +----------------------------- +-- Frame/Window Dimensions -- +----------------------------- + +local function connectPropertyChanged(object, property, callback) + return object.Changed:connect(function(propertyChanged) + if propertyChanged == property then + callback(object[property]) + end + end) +end + +function Methods.ResetFrameDimensions(devConsole) + devConsole.Frame.Size = UDim2_new(0.5, 20, 0.5, 20); + + local abSize = devConsole.Frame.AbsoluteSize + devConsole:SetFrameSize(abSize.x, abSize.y) + local newSize = devConsole.Frame.Size + devConsole.Frame.Position = UDim2_new(0.5, -newSize.X.Offset/2, 0.5, -newSize.Y.Offset/2) +end +function Methods.BoundFrameSize(devConsole, x, y) + -- Minimum frame size + return math_max(x, 400), math_max(y, 200) +end +function Methods.SetFrameSize(devConsole, x, y) + x, y = devConsole:BoundFrameSize(x, y) + devConsole.Frame.Size = UDim2_new(0, x, 0, y) +end +function Methods.BoundFramePosition(devConsole, x, y) + -- Make sure the frame doesn't go somewhere where the bar can't be clicked + return x, math_max(y, 0) +end +function Methods.SetFramePosition(devConsole, x, y) + x, y = devConsole:BoundFramePosition(x, y) + devConsole.Frame.Position = UDim2_new(0, x, 0, y) +end + +-- Open/Close the console +function Methods.SetVisible(devConsole, visible, animate) + if devConsole.Visible == visible then + return + end + devConsole.Visible = visible + devConsole.VisibleChanged:fire(visible) + if devConsole.Frame then + devConsole.Frame.Visible = visible + end + if visible then -- Open the console + devConsole:ResetFrameDimensions() + + local tab = devConsole:GetCurrentOpenTab() + if (tab ~= nil) then + tab:RecordInitialOpen() + end + end + if VRService.VREnabled then + if visible then + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceShow + else + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide + end + end +end + +----------------- +-- Constructor -- +----------------- +function DeveloperConsole.new(screenGui, permissions, messagesAndStats) + + local visibleChanged = CreateSignal() + + local devConsole = { + ScreenGui = screenGui; + Permissions = permissions; + MessagesAndStats = messagesAndStats; + Initialized = false; + Visible = false; + Tabs = {}; + CurrentOpenedTab = nil; -- save last tab opened to set SelectedCoreObject for TenFootInterfaces + VisibleChanged = visibleChanged; -- Created by :Initialize(); It's used to stop and disconnect things when the window is hidden + } + + setmetatable(devConsole, Metatable) + + devConsole:EnableGUIMouse() + + -- It's a button so it catches mouse events + local frame = Primitives.Button(screenGui, 'DeveloperConsole') + frame.AutoButtonColor = false + --frame.ClipsDescendants = true + frame.Visible = devConsole.Visible + frame.Selectable = not isTenFootInterface + + local function onVREnabled() + frame.Modal = VRService.VREnabled + end + onVREnabled() + VRService:GetPropertyChangedSignal("VREnabled"):connect(onVREnabled) + + devConsole.Frame = frame + devConsole:ResetFrameDimensions() + + -- The bar at the top that you can drag around + local handle = Primitives.Button(frame, 'Handle') + handle.Size = UDim2_new(1, -(Style.HandleHeight + Style.BorderSize), 0, Style.HandleHeight) + handle.Selectable = not isTenFootInterface + handle.Modal = true -- Unlocks mouse + handle.AutoButtonColor = false + + + do -- Title + local title = Primitives.InvisibleTextLabel(handle, 'Title', "Roblox Developer Console") + title.Size = UDim2_new(1, -5, 1, 0) + title.Position = UDim2_new(0, 5, 0, 0) + title.FontSize = Enum.FontSize.Size18 + title.TextXAlignment = Enum.TextXAlignment.Left + end + + local function setCornerButtonImageSize(buttonImage, buttonImageSize) + buttonImage.Size = UDim2_new(buttonImageSize, 0, buttonImageSize, 0) + buttonImage.Position = UDim2_new((1 - buttonImageSize) / 2, 0, (1 - buttonImageSize) / 2, 0) + end + + -- This is used for creating the square exit button and the square window resize button + local function createCornerButton(name, x, y, image, buttonImageSize) + -- Corners (x, y): + -- (0, 0) (1, 0) + -- (0, 1) (1, 1) + + local button = Primitives.Button(frame, name) + button.Size = UDim2_new(0, Style.HandleHeight, 0, Style.HandleHeight) + button.Position = UDim2_new(x, -x * Style.HandleHeight, y, -y * Style.HandleHeight) + + local buttonImage = Primitives.InvisibleImageLabel(button, 'Image', image) + setCornerButtonImageSize(buttonImage, buttonImageSize) + + return button, buttonImage + end + + do -- Create top right exit button + local exitButton, exitButtonImage = createCornerButton('Exit', 1, 0, 'https://www.roblox.com/asset/?id=261878266', 2/3) + exitButton.AutoButtonColor = false + exitButton.Visible = not isTenFootInterface + exitButton.Selectable = not isTenFootInterface + + local buttonEffectFunction = devConsole:CreateButtonEffectFunction(exitButton) + + devConsole:ConnectButtonHover(exitButton, function(clicking, hovering) + if hovering and not clicking then + setCornerButtonImageSize(exitButtonImage, 3/4) + else + setCornerButtonImageSize(exitButtonImage, 2/3) + end + buttonEffectFunction(clicking, hovering) + end) + + exitButton.MouseButton1Click:connect(function() + devConsole:SetVisible(false, true) + end) + end + + do -- Repositioning and Resizing + + do -- Create bottom right window resize button and activate resize dragging + local resizeButton, resizeButtonImage = createCornerButton('Resize', 1, 1, 'https://www.roblox.com/asset/?id=261880743', 1) + resizeButtonImage.Position = UDim2_new(0, 0, 0, 0) + resizeButtonImage.Size = UDim2_new(1, 0, 1, 0) + resizeButton.Selectable = not isTenFootInterface + + local dragging = false + + local buttonEffectFunction = devConsole:CreateButtonEffectFunction(resizeButton) + + devConsole:ConnectButtonDragging(resizeButton, function() + local x0, y0 = frame.AbsoluteSize.X, frame.AbsoluteSize.Y + return function(dx, dy) + devConsole:SetFrameSize(x0 + dx, y0 + dy) + end + end, function(clicking, hovering) + dragging = clicking + buttonEffectFunction(clicking, hovering) + end) + + end + + do -- Activate top handle dragging + local frame = devConsole.Frame + local handle = frame.Handle + + local buttonEffectFunction = devConsole:CreateButtonEffectFunction(handle) + + devConsole:ConnectButtonDragging(handle, function() + local x, y = frame.AbsolutePosition.X, frame.AbsolutePosition.Y + return function(dx, dy) + devConsole:SetFramePosition(x + dx, y + dy) + end + --deltaCallback_Resize(-dx, -dy) -- Used if they are grabbing both at the same time + end, buttonEffectFunction) + end + end + + -- interiorFrame contains tabContainer and window + local interiorFrame = Primitives.FolderFrame(frame, 'Interior') + interiorFrame.Position = UDim2_new(0, 0, 0, Style.HandleHeight) + interiorFrame.Size = UDim2_new(1, -(Style.HandleHeight + Style.BorderSize * 2), 1, -(Style.HandleHeight + Style.BorderSize)) + + local windowContainer = Primitives.FolderFrame(interiorFrame, 'WindowContainer') + windowContainer.Size = UDim2_new(1, 0, 1, -(Style.TabHeight)) + windowContainer.Position = UDim2_new(0, Style.BorderSize, 0, Style.TabHeight) + + -- This is what applies ClipsDescendants to tab contents + local window = Primitives.Frame(windowContainer, 'Window') + window.Size = UDim2_new(1, 0, 1, 0) -- The tab open/close methods, and the consoles also set this + window.Position = UDim2_new(0, 0, 0, 0) + window.ClipsDescendants = true + + -- This is the frame that moves around with the scroll bar + local body = Primitives.FolderFrame(window, 'Body') + + do -- Scrollbars + local scrollbar = devConsole:CreateScrollbar() + devConsole.WindowScrollbar = scrollbar + local scrollbarFrame = scrollbar.Frame + scrollbarFrame.Parent = frame + scrollbarFrame.Size = UDim2_new(0, Style.HandleHeight, 1, -(Style.HandleHeight + Style.BorderSize) * 2) + scrollbarFrame.Position = UDim2_new(1, -Style.HandleHeight, 0, Style.HandleHeight + Style.BorderSize) + + devConsole:ApplyScrollbarToFrame(scrollbar, window, body, frame) + end + + local tabContainer = Primitives.FolderFrame(interiorFrame, 'Tabs') -- Shouldn't this be named 'tabFrame'? + tabContainer.Size = UDim2_new(1, -(Style.GearSize + Style.BorderSize), 0, Style.TabHeight) + tabContainer.Position = UDim2_new(0, 0, 0, 0) + tabContainer.ClipsDescendants = true + + -- Options button + local optionsButton = Primitives.InvisibleButton(frame, 'OptionsButton') + + local optionsClippingFrame = Primitives.FolderFrame(interiorFrame, 'OptionsClippingFrame') + optionsClippingFrame.ClipsDescendants = true + optionsClippingFrame.Position = UDim2_new(0, 0, 0, 0) + optionsClippingFrame.Size = UDim2_new(1, 0, 0, 0) + local optionsFrame = Primitives.FolderFrame(optionsClippingFrame, 'OptionsFrame') + optionsFrame.Size = UDim2_new(1, 0, 0, Style.OptionAreaHeight) + optionsFrame.Position = UDim2_new(0, 0, 0, Style.OptionAreaHeight) + --optionsFrame.BackgroundColor3 = Style.OptionsFrameColor + do -- Options animation + + local gearSize = Style.GearSize + local tabHeight = Style.TabHeight + local offset = (tabHeight - gearSize) / 2 + optionsButton.Size = UDim2_new(0, Style.GearSize, 0, Style.GearSize) + optionsButton.Position = UDim2_new(1, -(Style.GearSize + offset + Style.HandleHeight), 0, Style.HandleHeight + offset) + local gear = Primitives.InvisibleImageLabel(optionsButton, 'Image', 'https://www.roblox.com/asset/?id=261882463') + --gear.ZIndex = ZINDEX + 1 + local animationToggle = devConsole:GenerateOptionButtonAnimationToggle(interiorFrame, optionsButton, gear, tabContainer, optionsClippingFrame, optionsFrame) + local open = false + optionsButton.MouseButton1Click:connect(function() + open = not open + animationToggle(open) + end) + + end + + -- Console/Log and Stats options + local setShownOptionTypes; -- Toggles what options to show: setOptionType({Log = true}) + + local textFilter, scriptStatFilter; + local textFilterChanged, scriptStatFilterChanged; + + local messageFilter; + local messageFilterChanged, messageTextWrappedChanged; + do -- Options contents/filters + + local function createCheckbox(color, callback) + local this = { + Value = true; + } + + local frame = Primitives.FolderFrame(nil, 'Checkbox') + this.Frame = frame + frame.Size = UDim2_new(0, Style.CheckboxSize, 0, Style.CheckboxSize) + frame.BackgroundColor3 = color + + local padding = 2 + + local function f(xs, xp, yp) -- quick way to get an opaque border around a transparent center + local ys = 1 - xs + local f = Primitives.Frame(frame, 'Border') + f.BackgroundColor3 = color + f.BackgroundTransparency = 0 + f.Size = UDim2_new(xs, ys * padding, ys, xs * padding) + f.Position = UDim2_new(xp, -xp * padding, yp, -yp * padding) + end + f(1, 0, 0) + f(1, 0, 1) + f(0, 0, 0) + f(0, 1, 0) + + local button = Primitives.Button(frame, 'Button') + button.Size = UDim2_new(1, -padding * 2, 1, -padding * 2) + button.Position = UDim2_new(0, padding, 0, padding) + + local buttonEffectFunction = devConsole:CreateButtonEffectFunction(button) + + local check = Primitives.Frame(button, 'Check') + + local padding = 4 + check.Size = UDim2_new(1, -padding * 2, 1, -padding * 2) + check.Position = UDim2_new(0, padding, 0, padding) + check.BackgroundColor3 = color + check.BackgroundTransparency = 0 + + devConsole:ConnectButtonHover(button, buttonEffectFunction) + + function this.SetValue(this, value) + if value == this.Value then + return + end + this.Value = value + check.Visible = value + this.Value = value + callback(value) + end + + button.MouseButton1Click:connect(function() + this:SetValue(not this.Value) + end) + + return this + end + + local string_find = string.find + local containsString; -- the text typed into the search textBox, nil if equal to "" + + function textFilter(text) + return not containsString or string_find(text:lower(), containsString) + end + + local filterLookup = {} -- filterLookup[Enum.MessageType.x.Value] = true or false + function messageFilter(message) + return filterLookup[message.Type] and (not containsString or string_find(message.Message:lower(), containsString)) + end + + -- Events + textFilterChanged = CreateSignal() + scriptStatFilterChanged = CreateSignal() + + messageFilterChanged = CreateSignal() + messageTextWrappedChanged = CreateSignal() + + local optionTypeContainers = { + --[OptionType] = Frame + --Log = Frame; + --Scripts = Frame; + } + function setShownOptionTypes(shownOptionTypes) + -- Example showOptionTypes: + -- {Log = true} + for optionType, container in pairs(optionTypeContainers) do + container.Visible = shownOptionTypes[optionType] or false + end + end + + do -- Log options + local container = Primitives.FolderFrame(optionsFrame, 'Log') + container.Visible = false + optionTypeContainers.Log = container + + local label = Primitives.InvisibleTextLabel(container, 'FilterLabel', "Filters") + label.FontSize = 'Size18' + label.TextXAlignment = 'Left' + label.Size = UDim2_new(0, 54, 0, Style.CheckboxSize) + label.Position = UDim2_new(0, 4, 0, 2) + + do + local x = label.Size.X.Offset + local messageColors = Style.MessageColors + for i = 0, #messageColors do -- 0, 3 initially + local checkbox = createCheckbox(messageColors[i], function(value) + filterLookup[i] = value + messageFilterChanged:fire() + end) + filterLookup[i] = checkbox.Value + checkbox.Frame.Parent = container + checkbox.Frame.Position = UDim2_new(0, x, 0, 4) + x = x + Style.CheckboxSize + 4 + end + + do -- Word wrap + x = x + 8 + + local label = Primitives.InvisibleTextLabel(container, 'WrapLabel', "Word Wrap") + label.FontSize = 'Size18' + label.TextXAlignment = 'Left' + label.Size = UDim2_new(0, 54 + Style.CheckboxSize, 0, Style.CheckboxSize) + label.Position = UDim2_new(0, x + 4, 0, 2) + + local checkbox = createCheckbox(Color3.new(0.65, 0.65, 0.65), function(value) + messageTextWrappedChanged:fire(value) -- an event isn't ideal here + end) + checkbox:SetValue(false) + checkbox.Frame.Parent = container + checkbox.Frame.Position = UDim2_new(0, x + label.Size.X.Offset, 0, 4) + end + end + end + + do -- Scripts options + local container = Primitives.FolderFrame(optionsFrame, 'Stats') + container.Visible = false + optionTypeContainers.Scripts = container + + do + local x = 0 + + do -- Show inactive + x = x + 4 + local label = Primitives.InvisibleTextLabel(container, 'FilterLabel', "Show inactive") + label.FontSize = 'Size18' + label.TextXAlignment = 'Left' + label.Size = UDim2_new(0, label.TextBounds.X + 6, 0, Style.CheckboxSize) + label.Position = UDim2_new(0, x, 0, 2) + x = x + label.Size.X.Offset + + local showInactive; + local function getScriptCurrentlyActive(chartStat) + local stats = chartStat.Stats + if stats then + local stat = stats[#stats] + if stat then + return stat[1] > 0.000001 or stat[2] > 0.000001 + end + end + return false + end + function scriptStatFilter(chartStat) + return (showInactive or getScriptCurrentlyActive(chartStat)) + and (not containsString or string_find(chartStat.Name:lower(), containsString)) + end + + local checkbox = createCheckbox(Color3_new(1, 1, 1), function(value) + showInactive = value + scriptStatFilterChanged:fire() + end) + showInactive = checkbox.Value + checkbox.Frame.Parent = container + checkbox.Frame.Position = UDim2_new(0, x, 0, 4) + x = x + Style.CheckboxSize + 4 + + end + + x = x + 8 + + --[[ + local label = Primitives.InvisibleTextLabel(container, 'WrapLabel', "Word Wrap") + label.FontSize = 'Size18' + label.TextXAlignment = 'Left' + label.Size = UDim2_new(0, 54 + Style.CheckboxSize, 0, Style.CheckboxSize) + label.Position = UDim2_new(0, x + 4, 0, 2) + + local checkbox = createCheckbox(Color3.new(0.65, 0.65, 0.65), function(value) + messageTextWrappedChanged:fire(value) + end) + checkbox:SetValue(false) + checkbox.Frame.Parent = container + checkbox.Frame.Position = UDim2_new(0, x + label.Size.X.Offset, 0, 4) + --]] + end + end + + do -- Search/filter/contains textbox + + local container = Primitives.FolderFrame(optionsFrame, 'Search') + container.Visible = false + optionTypeContainers.Search = container + + local label = Primitives.InvisibleTextLabel(container, 'FilterLabel', "Contains:") + label.FontSize = 'Size18' + label.TextXAlignment = 'Left' + label.Size = UDim2_new(0, 60, 0, Style.CheckboxSize) + label.Position = UDim2_new(0, 4, 0, 4 + Style.CheckboxSize + 4) + + local textBox = Primitives.TextBox(container, 'ContainsFilter') + textBox.ClearTextOnFocus = true + textBox.FontSize = 'Size18' + textBox.TextXAlignment = 'Left' + textBox.Size = UDim2_new(0, 150, 0, Style.CheckboxSize) + textBox.Position = UDim2_new(0, label.Position.X.Offset + label.Size.X.Offset + 4, 0, 4 + Style.CheckboxSize + 4) + textBox.Text = "" + + local runningColor = Color3.new(0, 0.5, 0) + local normalColor = textBox.BackgroundColor3 + + connectPropertyChanged(textBox, 'Text', function(text) + text = text:lower() + if text == "" then + text = nil + end + if text == containsString then + return + end + textBox.BackgroundColor3 = text and runningColor or normalColor + containsString = text + messageFilterChanged:fire() + textFilterChanged:fire() + end) + + connectPropertyChanged(textBox, 'TextBounds', function(textBounds) + textBox.Size = UDim2_new(0, math.max(textBounds.X, 150), 0, Style.CheckboxSize) + end) + end + end + + ---------- + -- Tabs -- + ---------- + do -- Console/Log tabs + + -- Wrapper for :AddTab + local function createConsoleTab(name, + text, + outputMessageSync, + commandLineVisible, + commandInputtedCallback, + openCallback) + local tabBody = Primitives.FolderFrame(body, name) + local output, commandLine; + local disconnector = CreateDisconnectSignal() + + local tab = devConsole:AddTab(text, tabBody, function(open) + if commandLine then + commandLine.Frame.Visible = open + end + + if open then + setShownOptionTypes({ + Log = true; + Search = true; + }) + + if not output then + output = devConsole:CreateOutput(outputMessageSync:GetMessages(), messageFilter) + output.Frame.Parent = tabBody + end + + output:SetVisible(true) + + if commandLineVisible then + if open and not commandLine then + commandLine = devConsole:CreateCommandLine() + commandLine.Frame.Parent = frame + commandLine.Frame.Size = UDim2_new(1, + -(Style.HandleHeight + Style.BorderSize * 2), + 0, + Style.CommandLineHeight) + commandLine.Frame.Position = UDim2_new(0, + Style.BorderSize, + 1, + -(Style.CommandLineHeight + Style.BorderSize)) + commandLine.CommandInputted:connect(commandInputtedCallback) + end + end + + window.Size = commandLineVisible + and UDim2_new(1, 0, 1, -(Style.HandleHeight)) + or UDim2_new(1, 0, 1, 0) + + + local messages = outputMessageSync:GetMessages() + + local height = output:RefreshMessages() + body.Size = UDim2_new(1, 0, 0, height) + + disconnector:connect(output.HeightChanged:connect(function(height) + body.Size = UDim2_new(1, 0, 0, height) + end)) + body.Size = UDim2_new(1, 0, 0, output.Height) + + disconnector:connect(outputMessageSync.MessageAdded:connect(function(message) + output:RefreshMessages(#messages) + end)) + + disconnector:connect(messageFilterChanged:connect(function() + output:RefreshMessages() + end)) + disconnector:connect(messageTextWrappedChanged:connect(function(enabled) + output:SetTextWrappedEnabled(enabled) + end)) + else + if output then + output:SetVisible(false) + end + window.Size = UDim2_new(1, 0, 1, 0) + disconnector:fire() + end + if openCallback then + openCallback(open) + end + end) + + return tab + end + + -- Client Log tab -- + if permissions.MayViewClientLog then + local tab = createConsoleTab( + 'ClientLog', "Client Log", + devConsole.MessagesAndStats.OutputMessageSyncLocal, + permissions.ClientCodeExecutionEnabled + ) + tab:SetVisible(true) + tab:SetOpen(true) + end + + -- Server Log tab -- + if permissions.MayViewServerLog then + local LogService = game:GetService('LogService') + local tab = createConsoleTab( + 'ServerLog', "Server Log", + devConsole.MessagesAndStats.OutputMessageSyncServer, + permissions.ServerCodeExecutionEnabled, + function(text) + if #text <= 1 then + return + end + if permissions.ServerCodeExecutionEnabled then + -- print("Server Loadstring:", text) + LogService:ExecuteScript(text) + end + end + ) + tab:SetVisible(true) + end + end + + do -- Stats tabs + + local function generateGreenYellowRedColor(unit) -- 0 <= unit <= 1 + --[[ + 0 -> 0, 223, 0 + 0.5 -> 223, 233, 0 + 1 -> 233, 0, 0 + --]] + local brightness = 0.9 + if not unit then + return Color3.new(0, 0, 0) + elseif unit <= 0 then + return Color3.new(1, 1, 1) + elseif unit <= 0.5 then + unit = unit * 2 + return Color3.new(unit * brightness, brightness, 0) + elseif unit <= 1 then + unit = unit * 2 - 1 + return Color3.new(brightness, (1 - unit) * brightness, 0) + else + return Color3.new(1, 0, 0) + end + end + + -- Wrapper for :AddTab + local function createStatsTab(name, text, + config, openCallback, + filterStats, shownOptionTypes) + + local statsSyncServer = devConsole.MessagesAndStats.StatsSyncServer + + local open = false + + local statList = devConsole:CreateChartList(config) + + local tabBody = statList.Frame + tabBody.Parent = body + tabBody.Name = name + tabBody.BackgroundTransparency = 1 + tabBody.Size = UDim2_new(1, 0, 1, 0) + statList.SideMenu.Parent = windowContainer -- so the left side menu doesn't resize with the contents on right + + statsSyncServer:GetStats() + statsSyncServer.StatsReceived:connect(function(stats) + local statsFiltered = filterStats(stats) + if statsFiltered then + statList:UpdateStats(statsFiltered) + end + end) + + local tab = devConsole:AddTab(text, tabBody, function(openNew) + open = openNew + if open then + devConsole.WindowScrollbar:SetValue(0) + setShownOptionTypes(shownOptionTypes) + end + statList:SetVisible(open) + if openCallback then + openCallback(open) + end + end) + tab:SetVisible(true) + + return tab, statList + end + + + do -- Client Memory tab + + local tabBody = Primitives.FolderFrame(body, 'ClientMemory') + local clientTab = nil + local clientMemoryAnalyzer = nil + + clientMemoryAnalyzer = ClientMemoryAnalyzerClass.new(tabBody) + + -- When memory analyzer decides it's new size, we get notified. + clientMemoryAnalyzer:setHeightChangedCallback(function(newHeight) + body.Size = UDim2.new(1, 0, 0, newHeight) + end) + + -- Considering all state (is dev console even showing, which tab is showing), + -- do I need to update the memory stats tab right now, and should I be listening + -- for regular updates? + local function syncClientMemoryAnalyzerVisibility() + if (clientTab.Open and clientTab.Visible and devConsole.Visible) then + clientMemoryAnalyzer:refreshMemoryUsageTree() + clientMemoryAnalyzer:renderUpdates() + clientMemoryAnalyzer:startListeningForUpdates() + body.Size = UDim2.new(1, 0, 0, clientMemoryAnalyzer:getHeightInPix()) + else + clientMemoryAnalyzer:stopListeningForUpdates() + end + end + + -- Every time 'open' state changes, call syncVisibility. + local tabName = "Client Memory" + + clientTab = devConsole:AddTab(tabName, tabBody, function(open) + if (open) then + devConsole.WindowScrollbar:SetValue(0) + setShownOptionTypes({}) + end + + syncClientMemoryAnalyzerVisibility() + end) + clientTab:SetVisible(true) + + -- Every time dev console's open state changes, call syncVisibility. + devConsole.VisibleChanged:connect(function(visible) + syncClientMemoryAnalyzerVisibility() + end) + end + + -- Server Scripts -- + if permissions.MayViewServerScripts then + + local open = false + + local config = { + GetNotifyColor = function(chartButton) + local chartStat = chartButton.ChartStat + local point; + local stat = chartStat.Stats[#chartStat.Stats] + if stat then + point = stat[1] + local freq = stat[2] + if point and freq then + point = (point < 0 and 0) or (point > 1 and 1) or point -- clamp between 0 and 1 + point = math.max(freq > 0 and 0.000001 or 0, point ^ (1/4)) + end + end + return generateGreenYellowRedColor(point) + end; + CreateChartPage = function(chartButton, statsBody) + local chartStat = chartButton.ChartStat + local chart1 = devConsole:CreateChart(chartStat.Stats, "Script Activity", 1, function(point) + return point and math.ceil(point * 100000 * 100) / 100000 .. "%" or "" + end) + local chart2 = devConsole:CreateChart(chartStat.Stats, "Script Rate", 2, function(point) + return point and (math.floor(point * 100000) / 100000) .. "/s" or "" + end) + + local y = 16 + chart1.Frame.Parent = statsBody + chart1.Frame.Position = UDim2_new(0, 16, 0, y) + y = y + 16 + chart1.Frame.Size.Y.Offset + + chart2.Frame.Parent = statsBody + chart2.Frame.Position = UDim2_new(0, 16, 0, y) + y = y + 16 + chart2.Frame.Size.Y.Offset + + local this = {} + function this.OnPointAdded(this) + chart1:OnPointAdded() + chart2:OnPointAdded() + end + function this.SetVisible(this, visible) + chart1:SetVisible(visible) + chart2:SetVisible(visible) + body.Size = open and UDim2_new(1, 0, 0, y) or UDim2_new(1, 0, 1, 0) + end + function this.Dispose(this) + this:SetVisible(false) + end + return this + end; + FilterButton = function(chartButton) + return scriptStatFilter(chartButton.ChartStat) + end; + } + + local function filterStats(stats) + -- return stats.Scripts + if stats.Scripts then + local statsFiltered = {} + for k, v in pairs(stats.Scripts) do + statsFiltered[k] = {v[1]/100, v[2]} + end + return statsFiltered + end + end + + local function openCallback(openNew) + open = openNew + end + + local tab, statList = createStatsTab('ServerScripts', "Server Scripts", config, openCallback, filterStats, + { + Scripts = true; + Search = true; + }) + + textFilterChanged:connect(function() + statList:Refresh() + end) + scriptStatFilterChanged:connect(function() + statList:Refresh() + end) + + tab:SetVisible(true) + end + + -- Server Stats -- + if permissions.MayViewServerStats then + + local open = false + + local config = { + GetNotifyColor = function(chartButton) + return Color3.new(0.5, 0.5, 0.5) + end; + CreateChartPage = function(chartButton, statsBody) + local chartStat = chartButton.ChartStat + local chart1 = devConsole:CreateChart(chartStat.Stats, chartStat.Name, 1) + + local y = 16 + chart1.Frame.Parent = statsBody + chart1.Frame.Position = UDim2_new(0, 16, 0, y) + y = y + 16 + chart1.Frame.Size.Y.Offset + + local this = {} + function this.OnPointAdded(this) + chart1:OnPointAdded() + end + function this.SetVisible(this, visible) + chart1:SetVisible(visible) + body.Size = open and UDim2_new(1, 0, 0, y) or UDim2_new(1, 0, 1, 0) + end + function this.Dispose(this) + this:SetVisible(false) + end + return this + end; + FilterButton = function(chartButton) + return textFilter(chartButton.ChartStat.Name) + end; + } + + local function filterStats(stats) + local statsFiltered = {} + for k, v in pairs(stats) do + if type(v) == 'number' then + statsFiltered[k] = {v} + end + end + return statsFiltered + end + + local function openCallback(openNew) + open = openNew + end + + local tab, statList = createStatsTab('ServerStats', "Server Stats", config, openCallback, filterStats, + { + Stats = true; + Search = true; + }) + + textFilterChanged:connect(function() + statList:Refresh() + end) + + tab:SetVisible(true) + end + + -- Server Memory -- + if (permissions.MayViewServerMemory) then + local tabBody = Primitives.FolderFrame(body, "ServerMemory") + + local serverTab = nil + local serverMemoryAnalyzer = nil + + serverMemoryAnalyzer = ServerMemoryAnalyzerClass.new(tabBody) + + -- When memory analyzer decides its new size, we get notified. + serverMemoryAnalyzer:setHeightChangedCallback(function(newHeight) + body.Size = UDim2.new(1, 0, 0, newHeight) + end) + + -- Considering all state (is dev console even showing, which tab is showing), + -- do I need to update the memory stats tab right now, and should I be listening + -- for regular update? + local function syncServerMemoryAnalyzerVisibility() + if (serverTab.Open and serverTab.Visible and devConsole.Visible) then + serverMemoryAnalyzer:setVisible(true) + body.Size = UDim2.new(1, 0, 0, serverMemoryAnalyzer:getHeightInPix()) + else + serverMemoryAnalyzer:setVisible(false) + end + end + + -- Every time 'open' state changes, call syncVisibility. + local tabName = "Server Memory" + + serverTab = devConsole:AddTab(tabName, tabBody, function(open) + if (open) then + devConsole.WindowScrollbar:SetValue(0) + setShownOptionTypes({}) + end + syncServerMemoryAnalyzerVisibility() + end) + serverTab:SetVisible(true) + + -- Every time dev console's open state changes, call syncVisibility. + devConsole.VisibleChanged:connect(function(visible) + syncServerMemoryAnalyzerVisibility() + end) + + -- Ensure server stats are being collected. + local statsSyncServer = devConsole.MessagesAndStats.StatsSyncServer + statsSyncServer:GetStats() + -- Listen to the "new server stats" event. + statsSyncServer.StatsReceived:connect(function(stats) + local filteredTreeStats = serverMemoryAnalyzer:filterServerMemoryTreeStats(stats) + if filteredTreeStats then + serverMemoryAnalyzer:updateWithTreeStats(filteredTreeStats) + end + end) + end + + + -- DataStoreBudget -- + if permissions.MayViewDataStoreBudget then + local open = false + + local config = { + GetNotifyColor = function(chartButton) + return Color3.new(0.5, 0.5, 0.5) + end; + CreateChartPage = function(chartButton, statsBody) + local chartStat = chartButton.ChartStat + local chart1 = devConsole:CreateChart(chartStat.Stats, chartStat.Name, 1) + + local y = 16 + chart1.Frame.Parent = statsBody + chart1.Frame.Position = UDim2_new(0, 16, 0, y) + y = y + 16 + chart1.Frame.Size.Y.Offset + + local this = {} + function this.OnPointAdded(this) + chart1:OnPointAdded() + end + function this.SetVisible(this, visible) + chart1:SetVisible(visible) + body.Size = open and UDim2_new(1, 0, 0, y) or UDim2_new(1, 0, 1, 0) + end + function this.Dispose(this) + this:SetVisible(false) + end + return this + end; + FilterButton = function(chartButton) + return textFilter(chartButton.ChartStat.Name) + end; + } + + local function filterStats(stats) + local statsFiltered = {} + for k, v in pairs(stats.DataStoreBudget) do + if type(v) == 'number' then + statsFiltered[k] = {v} + end + end + return statsFiltered + end + + local function openCallback(openNew) + open = openNew + end + + local tab, statList = createStatsTab('DataStoreBudget', "DataStore Budget", config, openCallback, filterStats, + { + Stats = true; + Search = true; + }) + + textFilterChanged:connect(function() + statList:Refresh() + end) + + tab:SetVisible(true) + end + + -- Server Jobs -- + if permissions.MayViewServerJobs then + + local open = false + + local config = { + GetNotifyColor = function(chartButton) + return Color3.new(0.5, 0.5, 0.5) + end; + CreateChartPage = function(chartButton, statsBody) + local chartStat = chartButton.ChartStat + local chart1 = devConsole:CreateChart(chartStat.Stats, "Duty Cycle", 1, function(point) + return point and math.floor(point * 10000000 + 0.5) / 100000 .. "%" or "" + end) + local chart2 = devConsole:CreateChart(chartStat.Stats, "Steps Per Sec", 2, function(point) + return point and (math.floor(point * 10000 + 0.5) / 10000) .. "/s" or "" + end) + local chart3 = devConsole:CreateChart(chartStat.Stats, "Step Time", 3, function(point) + return point and (math.floor(point * 10000000 + 0.5) / 10000) .. "ms" or "" + end) + + local y = 16 + chart1.Frame.Parent = statsBody + chart1.Frame.Position = UDim2_new(0, 16, 0, y) + y = y + 16 + chart1.Frame.Size.Y.Offset + + chart2.Frame.Parent = statsBody + chart2.Frame.Position = UDim2_new(0, 16, 0, y) + y = y + 16 + chart2.Frame.Size.Y.Offset + + chart3.Frame.Parent = statsBody + chart3.Frame.Position = UDim2_new(0, 16, 0, y) + y = y + 16 + chart3.Frame.Size.Y.Offset + + local this = {} + function this.OnPointAdded(this) + chart1:OnPointAdded() + chart2:OnPointAdded() + chart3:OnPointAdded() + end + function this.SetVisible(this, visible) + chart1:SetVisible(visible) + chart2:SetVisible(visible) + chart3:SetVisible(visible) + body.Size = open and UDim2_new(1, 0, 0, y) or UDim2_new(1, 0, 1, 0) + end + function this.Dispose(this) + this:SetVisible(false) + end + return this + end; + FilterButton = function(chartButton) + return textFilter(chartButton.ChartStat.Name) + end; + } + + local function filterStats(stats) + return stats.Jobs + end + + local function openCallback(openNew) + open = openNew + end + + local tab, statList = createStatsTab('ServerJobs', "Server Jobs", config, openCallback, filterStats, + { + Stats = true; + Search = true; + }) + + textFilterChanged:connect(function() + statList:Refresh() + end) + + tab:SetVisible(true) + end + end + + do -- Client Http Results tab + if permissions.MayViewHttpResultClient then + local logService = game:GetService('LogService') + local tabBody = Primitives.FolderFrame(body, 'HttpResult') + local tabOpen = false + local httpAnalyzerClass = require(CoreGui.RobloxGui.Modules.HttpAnalyticsTab) + local httpAnalyzer = httpAnalyzerClass.new(tabBody, function ( newHeight ) + -- update the body.Size only when tab is open so it won't disturb other tab + if tabOpen then + if newHeight < window.AbsoluteSize.Y then + newHeight = window.AbsoluteSize.Y + end + body.Size = UDim2.new(1, 0, 0, newHeight) + end + end) + + -- add http result when client got a http result + logService.HttpResultOut:connect(function (httpResult) + httpAnalyzer:addHttpResult(httpResult) + end) + -- add http results that client got before console was opened + local history = logService:GetHttpResultHistory() + for i = 1, #history do + httpAnalyzer:addHttpResult(history[i]) + end + + local tab = devConsole:AddTab('Client Http', tabBody, function(open) + tabOpen = open + -- update the 'body.Size', so the scrollbar will work + if open then + local newHeight = httpAnalyzer:getHeightInPix() + if newHeight < window.AbsoluteSize.Y then + newHeight = window.AbsoluteSize.Y + end + body.Size = UDim2.new(1, 0, 0, newHeight) + end + end) + tab:SetVisible(true) + end + end + + do -- Server Http Results tab + local logService = game:GetService('LogService') + local showServerHttp = function () + if permissions.MayViewHttpResultServer then + local tabBody = Primitives.FolderFrame(body, 'Server Http Result') + local tabOpen = false + local httpAnalyzerClass = require(CoreGui.RobloxGui.Modules.HttpAnalyticsTab) + local httpAnalyzer = httpAnalyzerClass.new(tabBody, function ( newHeight ) + -- update the body.Size only when tab is open so it won't disturb other tab + if tabOpen then + if newHeight < window.AbsoluteSize.Y then + newHeight = window.AbsoluteSize.Y + end + body.Size = UDim2.new(1, 0, 0, newHeight) + end + end) + + -- add http result when got a http result from server + logService.ServerHttpResultOut:connect(function (httpResult) + httpAnalyzer:addHttpResult(httpResult) + end) + logService:RequestServerHttpResult() + + local tab = devConsole:AddTab('Server Http', tabBody, function(open) + tabOpen = open + -- update the 'body.Size', so the scrollbar will work + if open then + local newHeight = httpAnalyzer:getHeightInPix() + if newHeight < window.AbsoluteSize.Y then + newHeight = window.AbsoluteSize.Y + end + body.Size = UDim2.new(1, 0, 0, httpAnalyzer:getHeightInPix()) + end + end) + + tab:SetVisible(true) + end + end + -- show server http results with user in the DFStringHttpResultsApprovedUserIDs + if not permissions.MayViewHttpResultServer then + logService.OnHttpResultApproved:connect(function (isApproved) + permissions.MayViewHttpResultServer = isApproved + showServerHttp() + end) + logService:RequestHttpResultApproved() + else + showServerHttp() + end + end + + do -- ContextActionService debugging + if permissions.MayViewContextActionBindings then + local tabBody = Primitives.FolderFrame(body, "ActionBindings") + local tab = devConsole:AddTab("Action Bindings", tabBody) + + + local success, result = pcall(function() + local ActionBindingsTab = require(RobloxGui.Modules.ActionBindingsTab) + ActionBindingsTab.initializeGui(tabBody) + end) + if not success then + warn(result) + warn("Action Bindings tab was hidden") + else + tab:SetVisible(true) + end + end + end + + --[[ + do -- Sample tab + local tabBody = Primitives.FolderFrame(body, 'TabName') + + local tab = devConsole:AddTab("Tab Name", tabBody) + tab:SetVisible(true) + --tab:SetOpen(true) + end + --]] + + return devConsole + +end + +---------------------- +-- Backup GUI Mouse -- +---------------------- +do -- This doesn't support multiple windows very well + function Methods.EnableGUIMouse(devConsole) + local label = Instance.new("ImageLabel") + label.BackgroundTransparency = 1 + label.BorderSizePixel = 0 + label.Size = UDim2.new(0, 64, 0, 64) + label.Image = "rbxasset://Textures/ArrowFarCursor.png" + label.Name = "BackupMouse" + label.ZIndex = Style.ZINDEX + 2 + + local disconnector = CreateDisconnectSignal() + + local enabled = false + + local mouse = game:GetService("Players").LocalPlayer:GetMouse() + + local function Refresh() + local enabledNew = devConsole.Visible and not UserInputService.MouseIconEnabled + if enabledNew == enabled then + return + end + enabled = enabledNew + label.Visible = enabled + label.Parent = enabled and devConsole.ScreenGui or nil + disconnector:fire() + if enabled then + label.Position = UDim2.new(0, mouse.X - 32, 0, mouse.Y - 32) + disconnector:connect(UserInputService.InputChanged:connect(function(input) + if input.UserInputType == Enum.UserInputType.MouseMovement then + --local p = input.Position + --if p then + label.Position = UDim2.new(0, mouse.X - 32, 0, mouse.Y - 32) + --end + end + end)) + end + end + + Refresh() + local userInputServiceListener; + devConsole.VisibleChanged:connect(function(visible) + if userInputServiceListener then + userInputServiceListener:disconnect() + userInputServiceListener = nil + end + + userInputServiceListener = UserInputService.Changed:connect(Refresh) + + Refresh() + end) + + end +end + + +---------------------- +-- Charts and Stats -- +---------------------- + + +do -- Script performance/Chart list + + --[[ + local chartStatExample = { + Name = "RoundScript"; + Stats = { + -- {Activity, InvocationCount} + {0, 0}; + {0, 0}; + } + } + --]] + + -- this manages the button and the chartPage + local function createChartButton(devConsole, chartList, chartStat, config) + + local this = { + ChartList = chartList; + ChartStat = chartStat; + Open = false; + } + + local button = Primitives.Button(nil, 'Button') + this.Frame = button + this.Button = button + + button.AutoButtonColor = false + local size0 = UDim2_new(1, -12 - chartList.ScrollingFrame.ScrollBarThickness, 0, 16) -- Size when script is closed + local size1 = UDim2_new(1, -2 - chartList.ScrollingFrame.ScrollBarThickness, 0, 16) -- Size when script is open + button.Size = size0 + button.Name = (chartStat.Name or "[no name]") + if not chartStat.Name then + button.TextColor3 = Color3.new(1, 0.5, 0.5) + end + button.BackgroundColor3 = Style.ScriptButtonColor + button.BackgroundTransparency = Style.ScriptBackgroundTransparency + + local notifyFrame = Primitives.Frame(button, 'NotifyFrame') + notifyFrame.BackgroundTransparency = 0 + notifyFrame.Size = UDim2.new(0, 8, 1, 0) + notifyFrame.BackgroundColor3 = Color3.new(0, 0.75, 0) + + local label = Primitives.InvisibleTextLabel(button, 'Label', chartStat.Name) + label.Size = UDim2_new(1, -notifyFrame.Size.X.Offset - 4 - 1, 1, 0) + label.Position = UDim2_new(0, notifyFrame.Size.X.Offset + 4, 0, 0) + label.FontSize = 'Size14' + label.TextXAlignment = 'Left' -- Enum.TextXAlignment.Left + --label.TextWrap = true + + local buttonEffectFunction = devConsole:CreateButtonEffectFunction(button) + devConsole:ConnectButtonHover(button, function(clicking, hovering) + buttonEffectFunction(clicking, hovering) + end) + + button.MouseButton1Down:connect(function() + -- not ideal + for i, that in pairs(chartList.ChartButtons) do + if this ~= that and that.Open then + that:SetOpen(false) + end + end + this:SetOpen(true) + end) + + -- This fires when the button opens/closes + local disconnector = CreateDisconnectSignal() + -- This fires when the button disposes + local disconnector2 = CreateDisconnectSignal() -- (Best variable name ever) + + local function refreshNotifyFrame() + notifyFrame.BackgroundColor3 = config.GetNotifyColor(this) + end + refreshNotifyFrame() + disconnector2:connect(chartList.OnStatUpdate:connect(refreshNotifyFrame)) + + function this.SetOpen(this, open) + if this.Open == open then + return + end + this.Open = open + + button:TweenSize(open and size1 or size0, "Out", "Sine", 0.25, true) + + disconnector:fire() + + if open then + + -- The chart page is initialized directly from the button, (this is not ideal, but it works) + local statsBody = Primitives.FolderFrame(chartList.Body, 'StatsBody') -- Button container + + local chartPage = config.CreateChartPage(this, statsBody) + + chartPage:SetVisible(true) + + disconnector:connect(chartList.OnStatUpdate:connect(function() + chartPage:OnPointAdded() + end)) + + disconnector:connect(function() + chartPage:Dispose() + statsBody:Destroy() + end) + + end + + end + + function this.Dispose(this) + button:Destroy() + disconnector:fire() + disconnector2:fire() + end + + return this + end + + + local function defaultSorter(a, b) -- this sorts chartButtons + return a.ChartStat.Name < b.ChartStat.Name + end + + function Methods.CreateChartList(devConsole, config) + + local this = { + Config = config; + Visible = false; + OnStatUpdate = CreateSignal(); + ChartButtons = {}; -- usage: chartButtons[position] = scriptButton + ChartStats = {}; -- usage: chartStats[chartStat.Name] = chartStat + } + + local frame = Primitives.FolderFrame(nil, 'ScriptList') + this.Frame = frame + frame.Visible = false + + local sideMenu = Primitives.Frame(frame, 'SideMenu') -- not necessarily parented to frame! + sideMenu.Size = UDim2_new(0, 196, 1, 0) + this.SideMenu = sideMenu + sideMenu.Visible = false + + local body = Primitives.FolderFrame(frame, 'Body') + this.Body = body + body.Size = UDim2_new(1, -sideMenu.Size.X.Offset, 1, 0) + body.Position = UDim2_new(0, sideMenu.Size.X.Offset, 0, 0) + + local scrollingFrame = Instance.new("ScrollingFrame", sideMenu) + scrollingFrame.BorderSizePixel = 0 + scrollingFrame.ZIndex = Style.ZINDEX + scrollingFrame.ScrollBarThickness = 12 + scrollingFrame.Selectable = false + this.ScrollingFrame = scrollingFrame + do + local y = 1 -- if we want to add a label above it later + scrollingFrame.Size = UDim2_new(1, 0, 1, -y) + scrollingFrame.Position = UDim2_new(0, 0, 0, y) + scrollingFrame.BackgroundTransparency = 1 + end + + local chartButtons = this.ChartButtons + local chartStats = this.ChartStats + + local sorter = defaultSorter + + function this.SetChartButtonSorter(this, sorterNew) + if sorter == sorterNew then + return + end + sorter = sorterNew + table.sort(chartButtons, sorter) + this:Refresh() + end + + function this.GetChartButton(this, name) -- not used? + for i = #chartButtons, 1, -1 do + local chartButton = chartButtons[i] + if chartButton.ChartStat.Name == name then + return chartButton, i + end + end + end + + function this.RemoveChart(this, name) + chartStats[name] = nil + for i = #chartButtons, 1, -1 do + local chartButton = chartButtons[i] + if chartButton.ChartStat.Name == name then + chartButton:Dispose() + table.remove(chartButtons, i) + return + end + end + end + + function this.UpdateStats(this, newStats) + + local timeStamp = os_time() -- Should it use tick instead? + + local scriptAddedOrRemoved = false + + for name, stat in pairs(chartStats) do + if not newStats[name] then + scriptAddedOrRemoved = true + this:RemoveChart(name) + end + end + + for name, newStat in pairs(newStats) do + local chartStat = chartStats[name] + if not chartStats[name] then + chartStat = { + Name = name; + Stats = {}; -- this could be loaded + } + chartStats[name] = chartStat + if this.Visible then + local chartButton = createChartButton(devConsole, this, chartStat, config) + chartButton.Frame.Parent = scrollingFrame + chartButtons[#chartButtons + 1] = chartButton + end + end + local stats = chartStat.Stats + stats[#stats + 1] = newStat + end + + table.sort(chartButtons, sorter) + + this:Refresh() + + this.OnStatUpdate:fire() + + end + + function this.Refresh(this) + + if not this.Visible then + for i = #chartButtons, 1, -1 do + chartButtons[i]:Dispose() + chartButtons[i] = nil + end + return + end + + table.sort(chartButtons, sorter) + + local y = 0 + for i = 1, #chartButtons do + local chartButton = chartButtons[i] + local visible = config.FilterButton(chartButton) + local button = chartButton.Button + button.Visible = visible + if visible then + button.Position = UDim2_new(0, 1, 0, y) -- Should it lerp to position if animating? + y = y + button.AbsoluteSize.Y + 1 + end + end + + scrollingFrame.CanvasSize = UDim2_new(0, 0, 0, y) + + end + this:Refresh() + + function this.SetVisible(this, visible) + if visible == this.Visible then + return + end + this.Visible = visible + + frame.Visible = visible + sideMenu.Visible = visible + + if visible then + for name, chartStat in pairs(chartStats) do + local chartButton = createChartButton(devConsole, this, chartStat, config) + chartButton.Button.Parent = scrollingFrame + chartButtons[#chartButtons + 1] = chartButton + end + this:Refresh() + else + for i = #chartButtons, 1, -1 do + chartButtons[i]:Dispose() + chartButtons[i] = nil + end + end + end + + return this + end +end + +do -- Chart + + local barWidth = 4 + local numBars = math.ceil((Style.ChartWidth - Style.BorderSize * 2) / (barWidth + 1)) + + local function round(x) + return math.floor(x * 1000 + 0.5) / 1000 + end + + local function CreateBar() + local bar = Primitives.Frame() + bar.BackgroundTransparency = 0 + bar.BackgroundColor3 = Color3_new(0, 0.5, 1) + return bar + end + + local function CreateGraph(points, statIndex, autoScale) + -- point = points[i][statIndex] + + local this = {} + + local direction = Style.ChartGraphDirection -- -1 means coming from right, 1 means coming from left + + local frame = Primitives.Frame(nil, 'Graph') + this.Frame = frame + frame.ClipsDescendants = true + + local scaleFrame = Primitives.FolderFrame(frame, 'ScaleFrame') + + local body = Primitives.FolderFrame(scaleFrame, 'Body') + body.Size = UDim2_new(0, barWidth, 1, 0) + + local bars = {} + local barHeights = {} + local barPositions = {} + + do -- reference notches + local function getReferenceHeight(position) + if position % 60 == 0 then + return 24 + elseif position % 30 == 0 then + return 12 + elseif position % 15 == 0 then + return 4 + elseif position % 5 == 0 then + return 1 + else + return 0 + end + end + for position = 0, numBars do + local height = getReferenceHeight(position) + if height ~= 0 then + local notch = Instance_new('Frame', frame) + notch.ZIndex = Style.ZINDEX + notch.BorderSizePixel = 0 + notch.BackgroundColor3 = Color3_new(1, 1, 1) + notch.Size = UDim2_new(0, 1, 0, height) + notch.Position = UDim2_new(0, position * (barWidth + 1), 1, -height) + end + end + end + local scale = 1 + + local position = 0 + + local visible = false + + local function generateSizeAndPosition(height, position) + height = height * scale + return + UDim2_new(0, barWidth, height, 0), + UDim2_new(0, (barWidth + 1) * position * -direction + 1, 1 - height, 0) + end + + local function RefreshScale(animate) + if not autoScale then + return + end + local heightMax; + for i = math_max(#points - numBars + 1, 1), #points do + local height = points[i][statIndex] + if not heightMax or height > heightMax then + heightMax = height + end + end + + if not heightMax or heightMax <= 0 then + local size, position = UDim2_new(1, 0, 1, 0), UDim2_new(0, 0, 0, 0) + if animate then + scaleFrame:TweenSizeAndPosition(size, position, 'Out', 'Sine', 0.25, true) + else + scaleFrame.Size, scaleFrame.Position = UDim2_new(1, 0, 1, 0), UDim2_new(0, 0, 0, 0) + end + return + end + + local scaleNew = 1 / heightMax * 0.95 + if math.abs(scale - scaleNew) < 0.0000001 then + return + end + -- Possible performance boost Todo: if the scale isn't significantly different (within like 0.5-4), just adjust scaleFrame's size + scale = scaleNew + + for i = 1, #bars do + local bar = bars[i] + local height = barHeights[i] + local barSize, barPosition = generateSizeAndPosition(height, barPositions[i]) + if animate then + bar:TweenSizeAndPosition(barSize, barPosition, 'Out', 'Sine', 0.25, true) + else + bar.Size, bar.Position = barSize, barPosition + end + end + + --local scale = 1 / heightMax * 0.95 + --scaleFrame:TweenSizeAndPosition(UDim2_new(1, 0, scale, 0), UDim2_new(0, 0, 1 - scale, 0), 'Out', 'Sine', 0.25, true) + + end + + function this.OnPointAdded(this) + if not visible then + return + end + local bar; + + -- possible game crasher + while #bars > numBars do + if bar then + bar:Destroy() + end + bar = bars[1] + table.remove(bars, 1) + table.remove(barHeights, 1) + table.remove(barPositions, 1) + end + + local point = points[#points] and points[#points][statIndex] + assert(point) + if not bar then + bar = CreateBar() + bar.Parent = body + end + + local height = point + + bars[#bars + 1] = bar + barHeights[#barHeights + 1] = height + barPositions[#barPositions + 1] = position + + bar.Size, bar.Position = generateSizeAndPosition(height, position) + + body:TweenPosition(UDim2_new(1 - (direction * 0.5 + 0.5), (barWidth + 1) * position * direction, 0, 0), 'Out', 'Sine', 0.25, true) + + position = position + 1 + + RefreshScale(true) + end + + function this.SetVisible(this, visibleNew) + + body.Position = UDim2_new(1 - (direction * 0.5 + 0.5), 0, 0, 0) + if visibleNew == visible then + return + end + visible = visibleNew + if not visible then + for i = #bars, 1, -1 do + bars[i]:Destroy() + bars[i] = nil + end + return + end + + position = 0 + for i = math_max(#points - numBars + 1, 1), #points do + local bar = bars[position + 1] + if not bar then + bar = CreateBar() + bar.Parent = body + bars[position + 1] = bar + end + + local point = points[i][statIndex] + local height = point + + barHeights[position + 1] = height + barPositions[position + 1] = position + + bar.Size, bar.Position = generateSizeAndPosition(height, position) + + position = position + 1 + end + + body.Position = UDim2_new(1 - (direction * 0.5 + 0.5), (barWidth + 1) * (position - 1) * direction, 0, 0) + + RefreshScale(false) + + end + + return this + end + + local function createLabel(...) + local n = Primitives.InvisibleTextLabel(...) + n.TextXAlignment = 'Left' + n.FontSize = 'Size14' + return n + end + + function Methods.CreateChart(devConsole, points, title, statIndex, pointToString) + + pointToString = pointToString or function(point) + if point then + local precision = 10000 + local v = point * precision + if v < 1 and v > 0 then + return "<" .. (1 / precision) + else + return math.floor(v + 0.5) / precision .. "" + end + else + return "" + end + end + + local chart = {} + + local frame = Primitives.Frame(nil, 'Chart') + chart.Frame = frame + frame.Size = UDim2_new(0, Style.ChartWidth, 0, Style.ChartHeight) + + local labelCurrent = createLabel(frame, 'Current', "Current: " .. pointToString(points[#points] and points[#points][statIndex])) + labelCurrent.Size = UDim2_new(0, 0.5, 0, Style.ChartTitleHeight) + labelCurrent.Position = UDim2_new(0, 4, 0, Style.ChartTitleHeight + 1) + + local graph = CreateGraph(points, statIndex, true) + graph.Frame.Parent = frame + graph.Frame.Size = UDim2_new(0, Style.ChartWidth - Style.BorderSize * 2, 0, Style.ChartGraphHeight) + graph.Frame.Position = UDim2_new(0, Style.BorderSize, 0, Style.ChartTitleHeight + Style.ChartDataHeight) + + do + local bar = Primitives.Frame(frame, 'Bar') + bar.Size = UDim2_new(1, 0, 0, Style.ChartTitleHeight) + + local label = Primitives.InvisibleTextLabel(bar, 'Title', title) + label.TextXAlignment = 'Left' -- Enum.TextXAlignment.Left + label.Size = UDim2_new(1, -4, 1, 0) + label.Position = UDim2_new(0, 4, 0, 0) + label.FontSize = 'Size18' + end + + local visible = false + function chart.SetVisible(chart, visibleNew) + if visibleNew == visible then + return + end + visible = visibleNew + graph:SetVisible(visible) + end + + function chart.OnPointAdded(chart) + local point = points[#points] + + if not point then + return + end + + labelCurrent.Text = "Current: " .. pointToString(point and point[statIndex]) + + graph:OnPointAdded() + end + + return chart + end +end + +-------------------- +-- Output console -- +-------------------- +do + function Methods.CreateCommandLine(devConsole) + local this = { + CommandInputted = CreateSignal(); + } + + local frame = Primitives.FolderFrame(nil, 'CommandLine') + this.Frame = frame + frame.Size = UDim2_new(1, 0, 0, Style.CommandLineHeight) + + local textBoxFrame = Primitives.Frame(frame, 'TextBoxFrame') + textBoxFrame.Size = UDim2_new(1, 0, 0, Style.CommandLineHeight) + textBoxFrame.Position = UDim2_new(0, 0, 0, 0) + textBoxFrame.ClipsDescendants = true + + local label = Primitives.InvisibleTextLabel(textBoxFrame, 'Label', ">") + label.Position = UDim2_new(0, 4, 0, 0) + label.Size = UDim2_new(0, 12, 1, -1) + label.FontSize = 'Size14' + + local DEFAULT_COMMAND_BAR_TEXT = "Type command here" + + local textBox = Primitives.TextBox(textBoxFrame, 'TextBox') + --textBox.TextWrapped = true -- This needs to auto-resize + textBox.BackgroundTransparency = 1 + textBox.Text = DEFAULT_COMMAND_BAR_TEXT + textBox.ClearTextOnFocus = false + local padding = 2 + textBox.Size = UDim2_new(1, -(padding * 2) - 4 - 12, 0, 500) + textBox.Position = UDim2_new(0, 4 + 12 + padding, 0, 0) + textBox.TextXAlignment = 'Left' + textBox.TextYAlignment = 'Top' + textBox.FontSize = 'Size18' + textBox.TextWrapped = true + + -- override SelectionImageObject to better fit + if isTenFootInterface then + local selectionImage = Instance.new('ImageLabel') + selectionImage.Name = "SelectionImage" + selectionImage.Size = UDim2.new(1, textBoxFrame.AbsoluteSize.x + 36, 0, Style.CommandLineHeight + 24) + selectionImage.Position = UDim2.new(0, -18, 0, -12) + selectionImage.Image = 'rbxasset://textures/ui/SelectionBox.png' + selectionImage.ScaleType = Enum.ScaleType.Slice + selectionImage.SliceCenter = Rect.new(21,21,41,41) + selectionImage.BackgroundTransparency = 1 + + textBox.SelectionImageObject = selectionImage + end + + do + local defaultSize = UDim2_new(1, 0, 0, Style.CommandLineHeight) + local first = true + + textBox.Changed:connect(function(property) + if property == 'TextBounds' or property == 'AbsoluteSize' then + if first then -- There's a glitch that only occurs on the first change + first = false + return + end + local textBounds = textBox.TextBounds + if textBounds.Y > Style.CommandLineHeight then + textBoxFrame.Size = UDim2_new(1, 0, 0, textBounds.Y + 2) + else + textBoxFrame.Size = defaultSize + end + end + end) + end + + local disconnector = CreateDisconnectSignal() + + local backtrackPosition = 0 + local inputtedText = {} + local isLastWeak = false + local function addInputtedText(text, weak) + -- weak means it gets overwritten by the next text that's inputted + if isLastWeak then + table.remove(inputtedText, 1) + end + if inputtedText[1] == text then + isLastWeak = isLastWeak and weak + return + end + isLastWeak = weak + if not weak then + for i = #inputtedText, 1, -1 do + if inputtedText[i] == text then + table.remove(inputtedText, i) + end + end + end + table.insert(inputtedText, 1, text) + end + local function backtrack(direction) + backtrackPosition = backtrackPosition + direction + if backtrackPosition < 1 then + backtrackPosition = 1 + elseif backtrackPosition > #inputtedText then + backtrackPosition = #inputtedText + end + if inputtedText[backtrackPosition] then + -- Setting the text doesn't always work, especially after losing focus without pressing enter, then clicking back + textBox.Text = inputtedText[backtrackPosition] + end + end + + local focusLostWithoutEnter = false + + textBox.Focused:connect(function() + if textBox.Text == DEFAULT_COMMAND_BAR_TEXT then + textBox.Text = "" + end + disconnector:fire() + backtrackPosition = 0 + disconnector:connect(UserInputService.InputBegan:connect(function(input) + if input.KeyCode == Enum.KeyCode.Up then + if backtrackPosition == 0 and not focusLostWithoutEnter then + -- They typed something, then pressed up. They might want what they typed back, so we store it + -- after they input the next thing, we know they meant to discard this, which is why it's "weak" (second arg is true) + addInputtedText(textBox.Text, true) + backtrackPosition = 1 + end + backtrack(1) + elseif input.KeyCode == Enum.KeyCode.Down then + backtrack(-1) + end + end)) + end) + + textBox.FocusLost:connect(function(enterPressed) + disconnector:fire() + if enterPressed then + focusLostWithoutEnter = false + + local text = textBox.Text + addInputtedText(text, false) + this.CommandInputted:fire(text) + textBox.Text = "" + + -- let's not spam the popup keyboard after text is entered + if not isTenFootInterface then + textBox:CaptureFocus() + end + else + backtrackPosition = 0 + focusLostWithoutEnter = true + addInputtedText(textBox.Text, true) + if textBox.Text == "" then + textBox.Text = DEFAULT_COMMAND_BAR_TEXT + end + end + end) + + return this + end +end +do + local padding = 5 + local LabelSize = UDim2_new(1, -padding, 0, 2048) + + local TextColors = Style.MessageColors + local TextColorUnknown = Color3_new(0.5, 0, 1) + + local function isHidden(message) + return false + end + + function Methods.CreateOutput(devConsole, messages, messageFilter) + + -- AKA 'Log' + local heightChanged = CreateSignal() + local output = { + Visible = false; + Height = 0; + HeightChanged = heightChanged; + MessagesDirty = false; + MessagesDirtyPosition = 1; + } + + local function setHeight(height) + height = height + 4 + output.Height = height + heightChanged:fire(height) + end + + -- The label container + local frame = Primitives.FolderFrame(nil, 'Output') + frame.ClipsDescendants = true + output.Frame = frame + + local textWrappedEnabled = false + + do + local lastX = 0 + connectPropertyChanged(frame, 'AbsoluteSize', function(size) + local currentX = size.X + --currentY = currentY - currentY + if currentX ~= lastX then + lastX = currentX + output:RefreshMessages() + end + end) + end + + local labels = {} + local labelPositions = {} + + local function RefreshTextWrapped() + if not output.Visible then + return + end + local y = 1 + for i = 1, #labels do + local label = labels[i] + label.TextWrapped = textWrappedEnabled + local height = label.TextBounds.Y + label.Size = LabelSize -- UDim2_new(1, 0, 0, height) + label.Position = UDim2_new(0, padding, 0, y) + y = y + height + if height > 16 then + y = y + 4 + end + end + setHeight(y) + end + local MAX_LINES = 2048 + + local function RefreshMessagesForReal(messageStartPosition) + if not output.Visible then + return + end + + local y = 1 + local labelPosition = 0 -- position of last used label + + -- Failed optimization: + messageStartPosition = nil + if messageStartPosition then + local labelPositionLast; + for i = messageStartPosition, math_max(1, #messages - MAX_LINES), -1 do + if labelPositions[i] then + labelPositionLast = labelPositions[i] + break + end + end + if labels[labelPositionLast] then + labelPosition = labelPositionLast + local label = labels[labelPositionLast] + y = label.Position.Y.Offset + label.Size.Y.Offset + else + messageStartPosition = nil + end + end + + for i = messageStartPosition or math_max(1, #messages - MAX_LINES), #messages do + local message = messages[i] + if messageFilter(message) then + labelPosition = labelPosition + 1 + labelPositions[i] = labelPosition + local label = labels[labelPosition] + if not label then + label = Instance_new('TextLabel', frame) + label.ZIndex = Style.ZINDEX + label.BackgroundTransparency = 1 + --label.Font = Style.Font + label.FontSize = 'Size10' + label.TextXAlignment = 'Left' + label.TextYAlignment = 'Top' + labels[labelPosition] = label + end + label.TextWrapped = textWrappedEnabled + label.Size = LabelSize + label.TextColor3 = TextColors[message.Type] or TextColorUnknown + label.Text = message.Time .. " -- " .. message.Message + + local height = label.TextBounds.Y + label.Size = LabelSize -- UDim2_new(1, -padding, 0, height) + label.Position = UDim2_new(0, padding, 0, y) + + y = y + height + + if height > 16 then + y = y + 4 + end + else + labelPositions[i] = false + end + end + + -- Destroy extra labels + for i = #labels, labelPosition + 1, -1 do + labels[i]:Destroy() + labels[i] = nil + end + + setHeight(y) + end + + function output.SetMessagesDirty(output, messageStartPosition) + if output.MessagesDirty then + return + end + output.MessagesDirty = true + output.MessagesDirtyPosition = messageStartPosition + end + + local refreshHandle; + function output.RefreshMessages(output, messageStartPosition) + if not output.Visible then + return + end + if not refreshHandle then + refreshHandle = true + coroutine.wrap(function() -- Not ideal + wait() + refreshHandle = false + RefreshMessagesForReal() + end)() + end + end + + function output.SetTextWrappedEnabled(output, textWrappedEnabledNew) + if textWrappedEnabledNew == textWrappedEnabled then + return + end + textWrappedEnabled = textWrappedEnabledNew + RefreshTextWrapped() + end + + function output.SetVisible(output, visible) + if visible == output.Visible then + return + end + output.Visible = visible + if visible then + RefreshMessagesForReal() + else + for i = #labels, 1, -1 do + labels[i]:Destroy() + labels[i] = nil + end + end + end + + return output + end +end + +---------- +-- Tabs -- +---------- +function Methods.GetCurrentOpenTab(devConsole) + local tabs = devConsole.Tabs + if tabs == nil then + return nil + end + + for i = 1, #tabs do + local tab = tabs[i] + if tab.Open then + return tab + end + end + return nil +end + + +function Methods.RefreshTabs(devConsole) + -- Go through and reposition them + local x = Style.BorderSize + local tabs = devConsole.Tabs + for i = 1, #tabs do + local tab = tabs[i] + if tab.ButtonFrame.Visible then + x = x + 3 + tab.ButtonFrame.Position = UDim2_new(0, x, 0, 0) + x = x + tab.ButtonFrame.AbsoluteSize.X + 3 + end + end +end + +function Methods.AddTab(devConsole, text, body, openCallback, visibleCallback) + -- Body is a frame that contains the tab contents + body.Visible = false + + local tab = { + Open = false; -- If the tab is open + Visible = false; -- If the tab is shown + OpenCallback = openCallback; + VisibleCallback = visibleCallback; + Body = body; + } + + local nominalSize = TextService:GetTextSize(text, TAB_TEXT_SIZE, Enum.Font.SourceSans, Vector2.new(1e3, 1e3)) + local width = nominalSize.x + (TAB_TEXT_PADDING * 2) + + local buttonFrame = Primitives.InvisibleButton(devConsole.Frame.Interior.Tabs, 'Tab_' .. text) + tab.ButtonFrame = buttonFrame + buttonFrame.Size = UDim2_new(0, width, 0, Style.TabHeight) + buttonFrame.Visible = false + + local textLabel = Primitives.TextLabel(buttonFrame, 'Label', text) + textLabel.TextSize = TAB_TEXT_SIZE + --textLabel.TextYAlignment = Enum.TextYAlignment.Top + + devConsole:ConnectButtonHover(buttonFrame, devConsole:CreateButtonEffectFunction(textLabel)) + + -- These are the dimensions when the tab is closed + local size0 = UDim2_new(1, 0, 1, -7) + local position0 = UDim2_new(0, 0, 0, 4) + -- There are the dimensions when the tab is open + local size1 = UDim2_new(1, 0, 1, -4) + local position1 = UDim2_new(0, 0, 0, 4) + -- It starts closed + textLabel.Size = size0 + textLabel.Position = position0 + + function tab.SetVisible(tab, visible) + if visible == tab.Visible then + return + end + tab.Visible = visible + tab:SetOpen(false) + if tab.VisibleCallback then + tab.VisibleCallback(visible) + end + buttonFrame.Visible = visible + devConsole:RefreshTabs() + if not visible then + tab.SetOpen(false) + end + end + + function tab.RecordInitialOpen(tab) + local analyticsService = game:GetService("AnalyticsService") + analyticsService:trackEvent(AnalyticsCategory_Game, + AnalyticsAction_InitialOpenTab, + tab.Body.Name) + end + + function tab.RecordClickToOpen(tab) + local analyticsService = game:GetService("AnalyticsService") + analyticsService:trackEvent(AnalyticsCategory_Game, + AnalyticsAction_ClickToOpenOpenTab, + tab.Body.Name) + end + + function tab.SetOpen(tab, open) + if open == tab.Open then + return + end + tab.Open = open + + if open then + if tab.SavedScrollbarValue then + devConsole.WindowScrollbar:SetValue(tab.SavedScrollbarValue) -- This doesn't load correctly? + end + local tabs = devConsole.Tabs + for i = 1, #tabs do + if tabs[i] ~= tab then + tabs[i]:SetOpen(false) + end + end + if body then + body.Visible = true + end + devConsole:RefreshTabs() + -- Set dimensions for folder effect + textLabel.Size = size1 + textLabel.Position = position1 + devConsole.CurrentOpenedTab = buttonFrame + else + tab.SavedScrollbarValue = devConsole.WindowScrollbar:GetValue() -- This doesn't save correctly + + if body then + body.Visible = false + -- todo: (not essential) these 2 lines should instead exist during open (above block) after going through tabs + devConsole.Frame.Interior.WindowContainer.Window.Body.Size = UDim2_new(1, 0, 1, 0) + devConsole.Frame.Interior.WindowContainer.Window.Body.Position = UDim2_new(0, 0, 0, 0) + end + + -- Set dimensions for folder effect + textLabel.Size = size0 + textLabel.Position = position0 + end + + if tab.OpenCallback then + tab.OpenCallback(open) + end + + end + + buttonFrame.MouseButton1Click:connect(function() + if tab.Visible then + tab:RecordClickToOpen() + tab:SetOpen(true) + end + end) + + table.insert(devConsole.Tabs, tab) + + return tab + +end + +---------------- +-- Scroll bar -- +---------------- +function Methods.ApplyScrollbarToFrame(devConsole, + scrollbar, + window, + body, + frame) + local windowHeight, bodyHeight + local height = scrollbar:GetHeight() + local value = scrollbar:GetValue() + local function getHeights() + return window.AbsoluteSize.Y, body.AbsoluteSize.Y + end + local function refreshDimension() + local windowHeightNew, bodyHeightNew = getHeights() + + if bodyHeight ~= bodyHeightNew or windowHeight ~= windowHeightNew then + bodyHeight, windowHeight = bodyHeightNew, windowHeightNew + height = windowHeight / bodyHeight + scrollbar:SetHeight(height) + + local yOffset = (bodyHeight - windowHeight) * value + -- Never let yOffset go negative. + -- Without this line, things that are smaller than the containing scroll + -- window start at the bottom and grow up. + -- It's a better UX to have things start at top and grow down. + if (yOffset < 0) then + yOffset = 0 + end + + local x = body.Position.X + local y = body.Position.Y + + body.Position = UDim2_new(x.Scale, x.Offset, y.Scale, -math.floor(yOffset)) + end + + end + + local function setValue(valueNew) + value = valueNew + refreshDimension() + local yOffset = (bodyHeight - windowHeight) * value + local x = body.Position.X + local y = body.Position.Y + body.Position = UDim2_new(x.Scale, x.Offset, y.Scale, -math.floor(yOffset)) + end + scrollbar.ValueChanged:connect(setValue) + setValue(scrollbar:GetValue()) + + local scrollDistance = 120 + + window.Active = true + + scrollbar.ButtonUp.MouseButton1Click:connect(function() + scrollbar:Scroll(-scrollDistance, getHeights()) + end) + scrollbar.ButtonDown.MouseButton1Click:connect(function() + scrollbar:Scroll(scrollDistance, getHeights()) + end) + + connectPropertyChanged(window, 'AbsoluteSize', refreshDimension) + connectPropertyChanged(body, 'AbsoluteSize', function() + local windowHeight, bodyHeight = getHeights() + local value = scrollbar:GetValue() + if value ~= 1 and value ~= 0 then + local value = -body.Position.Y.Offset / (bodyHeight - windowHeight) + scrollbar:SetValue(value) + end + refreshDimension() + end) + + window.MouseWheelForward:connect(function() + scrollbar:Scroll(-scrollDistance, getHeights()) + end) + window.MouseWheelBackward:connect(function() + scrollbar:Scroll(scrollDistance, getHeights()) + end) + window.TouchPan:connect(function(positions, delta, velocity, userInputState) + scrollbar:Scroll(-delta.y, getHeights()) + end) +end + +function Methods.CreateScrollbar(devConsole, rotation) + local scrollbar = {} + + local main = nil + main = Primitives.FolderFrame(main, 'Scrollbar') + scrollbar.Frame = main + + local frame = Primitives.Button(main, 'Frame') + frame.AutoButtonColor = false + frame.Size = UDim2_new(1, 0, 1, -(Style.HandleHeight) * 2 - 2) + frame.Position = UDim2_new(0, 0, 0, Style.HandleHeight + 1) + -- frame.BackgroundTransparency = 0.75 + + -- This replaces the scrollbar when it's not being used + local frame2 = Primitives.Frame(main, 'Frame') + frame2.Size = UDim2_new(1, 0, 1, 0) + frame2.Position = UDim2_new(0, 0, 0, 0) + + function scrollbar.SetVisible(scrollbar, visible) + frame.Visible = visible + frame2.Visible = not visible + end + + local buttonUp = Primitives.ImageButton(frame, 'Up', 'https://www.roblox.com/asset/?id=261880783') + scrollbar.ButtonUp = buttonUp + buttonUp.Size = UDim2_new(1, 0, 0, Style.HandleHeight) + buttonUp.Position = UDim2_new(0, 0, 0, -Style.HandleHeight - 1) + buttonUp.AutoButtonColor = false + devConsole:ConnectButtonHover(buttonUp, devConsole:CreateButtonEffectFunction(buttonUp)) + + local buttonDown = Primitives.ImageButton(frame, 'Down', 'https://www.roblox.com/asset/?id=261880783') + scrollbar.ButtonDown = buttonDown + buttonDown.Size = UDim2_new(1, 0, 0, Style.HandleHeight) + buttonDown.Position = UDim2_new(0, 0, 1, 1) + buttonDown.Rotation = 180 + buttonDown.AutoButtonColor = false + devConsole:ConnectButtonHover(buttonDown, devConsole:CreateButtonEffectFunction(buttonDown)) + + local bar = Primitives.Button(frame, 'Bar') + bar.Size = UDim2_new(1, 0, 0.5, 0) + bar.Position = UDim2_new(0, 0, 0.25, 0) + + bar.AutoButtonColor = false + + local grip = Primitives.InvisibleImageLabel(bar, 'Image', 'https://www.roblox.com/asset/?id=261904959') + grip.Size = UDim2_new(0, 16, 0, 16) + grip.Position = UDim2_new(0.5, -8, 0.5, -8) + + local buttonEffectFunction = devConsole:CreateButtonEffectFunction(bar, nil, bar.BackgroundColor3, bar.BackgroundColor3) + + -- Inertial scrolling would be added around here + + local value = 1 + local valueChanged = CreateSignal() + scrollbar.ValueChanged = valueChanged + -- value = 0: at very top + -- value = 1: at very bottom + + local height = 0.25 + local heightChanged = CreateSignal() + scrollbar.HeightChanged = heightChanged + -- height = 0: infinite page size + -- height = 1: bar fills frame completely, no need to scroll + + local function getValueAtPosition(pos) + return ((pos - main.AbsolutePosition.Y) / main.AbsoluteSize.Y) / (1 - height) + end + + -- Refreshes the position and size of the scrollbar + local function refresh() + local y = height + bar.Size = UDim2_new(1, 0, y, 0) + bar.Position = UDim2_new(0, 0, value * (1 - y), 0) + end + refresh() + + function scrollbar.SetValue(scrollbar, valueNew) + if valueNew < 0 then + valueNew = 0 + elseif valueNew > 1 then + valueNew = 1 + end + if valueNew ~= value then + value = valueNew + refresh() + valueChanged:fire(valueNew) + end + end + function scrollbar.GetValue(scrollbar) + return value + end + + function scrollbar.Scroll(scrollbar, direction, windowHeight, bodyHeight) + scrollbar:SetValue(value + direction / bodyHeight) -- needs to be adjusted + end + + function scrollbar.SetHeight(scrollbar, heightNew) + if heightNew < 0 then + heightNew = 0 -- this is still an awkward case of divide-by-zero that shouldn't happen + elseif heightNew > 1 then + heightNew = 1 + end + heightNew = math.max(heightNew, 0.1) -- Minimum scroll bar size, from that point on it is not the actual ratio + if heightNew ~= height then + height = heightNew + scrollbar:SetVisible(heightNew < 1) + refresh() + heightChanged:fire(heightNew) + end + end + function scrollbar.GetHeight(scrollbar) + return height + end + + devConsole:ConnectButtonDragging(bar, function() + local value0 = value -- starting value + return function(dx, dy) + local dposition = dy -- net position change relative to the bar's axis (could support rotated scroll bars) + local dvalue = (dposition / frame.AbsoluteSize.Y) / (1 - height) -- net value change + scrollbar:SetValue(value0 + dvalue) + end + end, buttonEffectFunction) + + return scrollbar +end + +---------------------- +-- Fancy color lerp -- +---------------------- +local RenderLerpAnimation; do + local math_cos = math.cos + local math_pi = math.pi + function RenderLerpAnimation(disconnectSignal, length, callback) + disconnectSignal:fire() + local timeStamp = tick() + local listener = RunService.RenderStepped:connect(function() + local t = (tick() - timeStamp) / length + if t >= 1 then + t = 1 + disconnectSignal:fire() + else + t = (1 - math_cos(t * math_pi)) / 2 -- cosine interpolation aka 'Sine' in :TweenSizeAndPosition + end + callback(t) + end) + disconnectSignal:connect(listener) + return listener + end +end + +if EYECANDY_ENABLED then + -- This is the pretty version + function Methods.CreateButtonEffectFunction(devConsole, button, normalColor, clickingColor, hoveringColor) + normalColor = normalColor or button.BackgroundColor3 + clickingColor = clickingColor or Style.GetButtonDownColor(normalColor) + hoveringColor = hoveringColor or Style.GetButtonHoverColor(normalColor) + local disconnectSignal = CreateDisconnectSignal() + return function(clicking, hovering) + local color0 = button.BackgroundColor3 + local color1 = clicking and clickingColor or (hovering and hoveringColor or normalColor) + local r0, g0, b0 = color0.r, color0.g, color0.b + local r1, g1, b1 = color1.r, color1.g, color1.b + local r2, g2, b2 = r1 - r0, g1 - g0, b1 - b0 + RenderLerpAnimation(disconnectSignal, clicking and 0.125 or 0.25, function(t) + button.BackgroundColor3 = Color3_new(r0 + r2 * t, g0 + g2 * t, b0 + b2 * t) + end) + end + end +else + -- This is the simple version + function Methods.CreateButtonEffectFunction(devConsole, button, normalColor, clickingColor, hoveringColor) + normalColor = normalColor or button.BackgroundColor3 + clickingColor = clickingColor or Style.GetButtonDownColor(normalColor) + hoveringColor = hoveringColor or Style.GetButtonHoverColor(normalColor) + return function(clicking, hovering) + button.BackgroundColor3 = clicking and clickingColor or (hovering and hoveringColor or normalColor) + end + end +end + +function Methods.GenerateOptionButtonAnimationToggle(devConsole, interior, button, gear, tabContainer, optionsClippingFrame, optionsFrame) + + local tabContainerSize0 = tabContainer.Size + local tabContainerSize1 = UDim2_new( + tabContainerSize0.X.Scale, tabContainerSize0.X.Offset + (Style.GearSize + 2) + Style.BorderSize, + tabContainerSize0.Y.Scale, tabContainerSize0.Y.Offset) + + local gearRotation0 = gear.Rotation + local gearRotation1 = gear.Rotation - 90 + local interiorSize0 = interior.Size + local interiorSize1 = UDim2_new(interiorSize0.X.Scale, interiorSize0.X.Offset, interiorSize0.Y.Scale, interiorSize0.Y.Offset - Style.OptionAreaHeight) + local interiorPosition0 = interior.Position + local interiorPosition1 = UDim2_new(interiorPosition0.X.Scale, interiorPosition0.X.Offset, interiorPosition0.Y.Scale, interiorPosition0.Y.Offset + Style.OptionAreaHeight) + + local length = 0.5 + local disconnector = CreateDisconnectSignal() + return function(open) + if open then + interior:TweenSizeAndPosition(interiorSize1, interiorPosition1, 'Out', 'Sine', length, true) + tabContainer:TweenSize(tabContainerSize1, 'Out', 'Sine', length, true) + optionsClippingFrame:TweenSizeAndPosition( + UDim2_new(1, 0, 0, Style.OptionAreaHeight), + UDim2_new(0, 0, 0, -Style.OptionAreaHeight), + 'Out', 'Sine', length, true + ) + optionsFrame:TweenPosition( + UDim2_new(0, 0, 0, 0),-- -Style.OptionAreaHeight), + 'Out', 'Sine', length, true + ) + local gearRotation = gear.Rotation + RenderLerpAnimation(disconnector, length, function(t) + gear.Rotation = gearRotation1 * t + gearRotation * (1 - t) + end) + else + interior:TweenSizeAndPosition(interiorSize0, interiorPosition0, 'Out', 'Sine', length, true) + tabContainer:TweenSize(tabContainerSize0, 'Out', 'Sine', length, true) + optionsClippingFrame:TweenSizeAndPosition( + UDim2_new(1, 0, 0, 0), + UDim2_new(0, 0, 0, 0), + 'Out', 'Sine', length, true + ) + optionsFrame:TweenPosition( + UDim2_new(0, 0, 0, Style.OptionAreaHeight), + 'Out', 'Sine', length, true + ) + local gearRotation = gear.Rotation + RenderLerpAnimation(disconnector, length, function(t) + gear.Rotation = gearRotation0 * t + gearRotation * (1 - t) + end) + end + end +end + +------------------------------ +-- Events for color effects -- +------------------------------ +do + local globalInteractEvent = CreateSignal() + function Methods.ConnectButtonHover(devConsole, button, mouseInteractCallback) + -- void mouseInteractCallback(bool clicking, bool hovering) + + local this = {} + + local clicking = false + local hovering = false + local function set(clickingNew, hoveringNew) + if hoveringNew and TouchEnabled then + hoveringNew = false -- Touch screens don't hover + end + if clickingNew ~= clicking or hoveringNew ~= hovering then + clicking, hovering = clickingNew, hoveringNew + mouseInteractCallback(clicking, hovering) + end + end + + button.MouseButton1Down:connect(function() + set(true, true) + end) + button.MouseButton1Up:connect(function() + set(false, true) + end) + button.MouseEnter:connect(function() + set(clicking, true) + end) + button.MouseLeave:connect(function() + set(false, false) + end) + --[[ these might cause memory leakes (when creating temporary buttons) + -- This solves the case in which the user presses F9 while hovering over a button + devConsole.VisibleChanged:connect(function() + set(false, false) + end) + + globalInteractEvent:connect(function() + set(false, false) + end) + --]] + end +end + +------------------------- +-- Events for draggers -- (for the window's top handle, the resize button, and scrollbars) +------------------------- +function Methods.ConnectButtonDragging(devConsole, button, dragCallback, mouseInteractCallback) + + -- How dragCallback is called: local deltaCallback = dragCallback(xPositionAtMouseDown, yPositionAtMouseDown) + -- How deltaCallback is called: deltaCallback(netChangeInAbsoluteXPositionSinceMouseDown, netChangeInAbsoluteYPositionSinceMouseDown) + + local dragging = false -- AKA 'clicking' + local hovering = false + + local listeners = {} + + local disconnectCallback; + + local function stopDragging() + if not dragging then + return + end + dragging = false + mouseInteractCallback(dragging, hovering) + for i = #listeners, 1, -1 do + listeners[i]:disconnect() + listeners[i] = nil + end + end + + local ButtonUserInputTypes = { + [Enum.UserInputType.MouseButton1] = true; + [Enum.UserInputType.Touch] = true; -- I'm not sure if touch actually works here + } + + local mouse = game:GetService("Players").LocalPlayer:GetMouse() + + local function startDragging(startP) + if dragging then + return + end + dragging = true + + mouseInteractCallback(dragging, hovering) + local deltaCallback; + + local x0, y0 = startP.X, startP.Y + --[[ + listeners[#listeners + 1] = UserInputService.InputBegan:connect(function(input) + if ButtonUserInputTypes[input.UserInputType] then + local position = input.Position + if position and not x0 then + x0, y0 = position.X, position.Y -- The same click + end + end + end) + --]] + listeners[#listeners + 1] = UserInputService.InputEnded:connect(function(input) + if ButtonUserInputTypes[input.UserInputType] then + stopDragging() + end + end) + listeners[#listeners + 1] = UserInputService.InputChanged:connect(function(input) + + if not (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch)then -- added in a touch check + return + end + + local p1 = input.Position + + if not p1 then + return + end + local x1, y1 = p1.X, p1.Y + if not deltaCallback then + deltaCallback, disconnectCallback = dragCallback(x0 or x1, y0 or y1) + end + if x0 then + deltaCallback(x1 - x0, y1 - y0) + end + end) + end + + --button.MouseButton1Down:connect(startDragging) + --button.MouseButton1Up:connect(stopDragging) + + button.InputBegan:connect(function(iobj) + if iobj.UserInputType == Enum.UserInputType.Touch or iobj.UserInputType == Enum.UserInputType.MouseButton1 then + startDragging(iobj.Position) + end + end) + + button.InputEnded:connect(function(iobj) + if iobj.UserInputType == Enum.UserInputType.Touch or iobj.UserInputType == Enum.UserInputType.MouseButton1 then + stopDragging() + end + end) + + button.MouseEnter:connect(function() + if not hovering then + hovering = true + mouseInteractCallback(dragging, hovering) + end + end) + button.MouseLeave:connect(function() + if hovering then + hovering = false + mouseInteractCallback(dragging, hovering) + end + end) + + devConsole.VisibleChanged:connect(stopDragging) +end + +----------------- +-- Permissions -- +----------------- +do + local permissionsLoading, permissions = false; + function DeveloperConsole.GetPermissions() + while permissionsLoading do wait() end + + if permissions then + return permissions + end + + permissions = {} + permissionsLoading = true + permissions.IsCreator = false + + local success, result = pcall(function() + local url = string.format("/users/%d/canmanage/%d", game:GetService("Players").LocalPlayer.UserId, game.PlaceId) + return game:GetService('HttpRbxApiService'):GetAsync(url, Enum.ThrottlingPriority.Default, Enum.HttpRequestType.Default, true) + end) + if success and type(result) == "string" then + -- API returns: {"Success":BOOLEAN,"CanManage":BOOLEAN} + -- Convert from JSON to a table + -- pcall in case of invalid JSON + success, result = pcall(function() + return game:GetService('HttpService'):JSONDecode(result) + end) + if success and result.CanManage == true then + permissions.IsCreator = result.CanManage + end + end + + permissions.ClientCodeExecutionEnabled = false + pcall(function() + permissions.ServerCodeExecutionEnabled = permissions.IsCreator and (not settings():GetFFlag("DebugDisableLogServiceExecuteScript")) + end) + + if DEBUG or (RunService:IsStudio()) then + permissions.IsCreator = true + permissions.ServerCodeExecutionEnabled = true + end + + permissions.MayViewServerLog = permissions.IsCreator + permissions.MayViewClientLog = true + + permissions.MayViewServerStats = permissions.IsCreator + permissions.MayViewServerMemory = permissions.IsCreator + permissions.MayViewServerScripts = permissions.IsCreator + permissions.MayViewServerJobs = permissions.IsCreator + + permissions.MayViewDataStoreBudget = false + pcall(function() + permissions.MayViewDataStoreBudget = permissions.IsCreator + end) + permissions.MayViewHttpResultClient = false + permissions.MayViewHttpResultClient = permissions.IsCreator + permissions.MayViewHttpResultServer = false + permissions.MayViewHttpResultServer = permissions.IsCreator + + permissions.MayViewContextActionBindings = permissions.IsCreator + + permissionsLoading = false + + return permissions + end +end + +---------------------- +-- Output interface -- +---------------------- +do + local messagesAndStats; + function DeveloperConsole.GetMessagesAndStats(permissions) + + if messagesAndStats then + return messagesAndStats + end + + local function NewOutputMessageSync(getMessages) + local this; + this = { + Messages = nil; -- Private member, DeveloperConsole should use :GetMessages() + MessageAdded = CreateSignal(); + GetMessages = function() + local messages = this.Messages + if not messages then + -- If it errors while getting messages, it skip it next time + if this.Attempted then + messages = {} + else + this.Attempted = true + messages = getMessages(this) + this.Messages = messages + end + + end + return messages + end; + } + return this + end + + local ConvertTimeStamp; do + -- Easy, fast, and working nicely + local function numberWithZero(num) + return (num < 10 and "0" or "") .. num + end + local string_format = string.format -- optimization + function ConvertTimeStamp(timeStamp) + local localTime = timeStamp - os_time() + math.floor(tick()) + local dayTime = localTime % 86400 + + local hour = math.floor(dayTime/3600) + + dayTime = dayTime - (hour * 3600) + local minute = math.floor(dayTime/60) + + dayTime = dayTime - (minute * 60) + local second = dayTime + + local h = numberWithZero(hour) + local m = numberWithZero(minute) + local s = numberWithZero(dayTime) + + return string_format("%s:%s:%s", h, m, s) + end + end + + local warningsToFilter = {"ClassDescriptor failed to learn", "EventDescriptor failed to learn", "Type failed to learn"} + + -- Filter "ClassDescriptor failed to learn" errors + local function filterMessageOnAdd(message) + if message.Type ~= Enum.MessageType.MessageWarning.Value then + return false + end + local found = false + for _, filterString in ipairs(warningsToFilter) do + if string.find(message.Message, filterString) ~= nil then + found = true + break + end + end + return found + end + + local outputMessageSyncLocal; + if permissions.MayViewClientLog then + outputMessageSyncLocal = NewOutputMessageSync(function(this) + local messages = {} + + local LogService = game:GetService("LogService") + do -- This do block keeps history from sticking around in memory + local history = LogService:GetLogHistory() + for i = 1, #history do + local msg = history[i] + local message = { + Message = msg.message or "[DevConsole Error 1]"; + Time = ConvertTimeStamp(msg.timestamp); + Type = msg.messageType.Value; + } + if not filterMessageOnAdd(message) then + messages[#messages + 1] = message + end + end + end + + LogService.MessageOut:connect(function(text, messageType) + local message = { + Message = text or "[DevConsole Error 2]"; + Time = ConvertTimeStamp(os_time()); + Type = messageType.Value; + } + if not filterMessageOnAdd(message) then + messages[#messages + 1] = message + this.MessageAdded:fire(message) + end + end) + + return messages + end) + end + + local outputMessageSyncServer; + if permissions.MayViewServerLog then + outputMessageSyncServer = NewOutputMessageSync(function(this) + local messages = {} + + local LogService = game:GetService("LogService") + + LogService.ServerMessageOut:connect(function(text, messageType, timestamp) + local message = { + Message = text or "[DevConsole Error 3]"; + Time = ConvertTimeStamp(timestamp); + Type = messageType.Value; + } + if not filterMessageOnAdd(message) then + messages[#messages + 1] = message + this.MessageAdded:fire(message) + end + end) + LogService:RequestServerOutput() + + return messages + end) + end + + local statsSyncServer; + if (permissions.MayViewServerStats or + permissions.MayViewServerScripts or + permissions.MayViewServerMemory) then + + statsSyncServer = { + Stats = nil; -- Private member, use GetStats instead + StatsReceived = CreateSignal(); + } + local statsListenerConnection; + function statsSyncServer.GetStats(statsSyncServer) + local stats = statsSyncServer.Stats + if not stats then + stats = {} + pcall(function() + local clientReplicator = game:FindService("NetworkClient"):GetChildren()[1] + if clientReplicator then + statsListenerConnection = clientReplicator.StatsReceived:connect(function(stat) + statsSyncServer.StatsReceived:fire(stat) + end) + clientReplicator:RequestServerStats(true) + end + end) + statsSyncServer.Stats = stats + end + return stats + end + + end + --]] + + messagesAndStats = { + OutputMessageSyncLocal = outputMessageSyncLocal; + OutputMessageSyncServer = outputMessageSyncServer; + StatsSyncServer = statsSyncServer; + } + + return messagesAndStats + end +end + +--[[ Module Table ]]-- +-- We only create the dev console if we need it; user toggles visibility. + +local DevConsoleModuleTable = {} +local myDeveloperConsole = nil + +-- Tenfoot Interface set up +local function onDevConsoleVisibilityChanged(isVisible) + local blockMenuActionName = "blockMenuAction" + local closeDevConsoleActionName = "closeDevConsoleAction" + local selectionParentName = "devConsoleSelectionGroup" + + local function closeDevConsole(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + myDeveloperConsole:SetVisible(false) + end + end + + if isVisible then + -- block menu open input while dev console is open + ContextActionService:BindCoreAction(blockMenuActionName, function() end, false, Enum.KeyCode.ButtonStart) + + local menuModule = require(Modules.Settings.SettingsHub) + menuModule:SetVisibility(false, true) + ContextActionService:BindCoreAction(closeDevConsoleActionName, closeDevConsole, false, Enum.KeyCode.ButtonB) + + GuiService:AddSelectionParent(selectionParentName, myDeveloperConsole.Frame) + GuiService.SelectedCoreObject = myDeveloperConsole.CurrentOpenedTab + else + ContextActionService:UnbindCoreAction(closeDevConsoleActionName) + ContextActionService:UnbindCoreAction(blockMenuActionName) + + GuiService:RemoveSelectionGroup(selectionParentName) + GuiService.SelectedCoreObject = nil + end +end + +local devConsoleCreating = false +local function getDeveloperConsole() + if (not myDeveloperConsole and not devConsoleCreating) then + devConsoleCreating = true + local permissions = DeveloperConsole.GetPermissions() + local messagesAndStats = DeveloperConsole.GetMessagesAndStats(permissions) + + myDeveloperConsole = DeveloperConsole.new(RobloxGui, permissions, messagesAndStats) + + if isTenFootInterface then + myDeveloperConsole.VisibleChanged:connect(onDevConsoleVisibilityChanged) + end + devConsoleCreating = false + end + return myDeveloperConsole +end + +function DevConsoleModuleTable:GetVisibility() + local devConsole = getDeveloperConsole() + if devConsole then + return devConsole.Visible + else + return false + end +end + +function DevConsoleModuleTable:SetVisibility(value) + local devConsole = getDeveloperConsole() + if devConsole then + devConsole:SetVisible(value) + end +end + + +local creatingLock = false +local creatingVisibleValueToSet = false + +local function SetCoreConsoleCreation() + if (creatingLock) then return end + creatingLock = true + + spawn(function() + --// Keep GetVisibility call before SetVisibility because the first call will yield for some time and + --// there is the possibility that during the yield time the value of 'creatingVisibleValueToSet' may + --// change. + DevConsoleModuleTable:GetVisibility() + DevConsoleModuleTable:SetVisibility(creatingVisibleValueToSet) + + creatingLock = false + end) +end + +local StarterGui = game:GetService("StarterGui") +StarterGui:RegisterGetCore("DeveloperConsoleVisible", function() + if (not myDeveloperConsole) then + SetCoreConsoleCreation() + return creatingVisibleValueToSet; + else + return DevConsoleModuleTable:GetVisibility() + end +end) +StarterGui:RegisterSetCore("DeveloperConsoleVisible", function(visible) + if (type(visible) ~= "boolean") then + error("DeveloperConsoleVisible must be given a boolean value.") + end + + if (not myDeveloperConsole) then + creatingVisibleValueToSet = visible + SetCoreConsoleCreation() + else + DevConsoleModuleTable:SetVisibility(visible) + end +end) + +return DevConsoleModuleTable diff --git a/Client2018/content/scripts/CoreScripts/Modules/GameTranslator.lua b/Client2018/content/scripts/CoreScripts/Modules/GameTranslator.lua new file mode 100644 index 0000000..0eb2338 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/GameTranslator.lua @@ -0,0 +1,76 @@ +local LocalizationService = game:GetService("LocalizationService") +local Players = game:GetService("Players") + +local CoreScriptTranslateGameText = settings():GetFFlag("CoreScriptTranslateGameText") + +local playerTranslator = nil +local player = nil +local didWarn = false +local localeId = nil +local localeIdConnection = nil +local localeChangedEvent = Instance.new("BindableEvent") + +local function handlePlayerOrLocaleChanged() + if player and player.LocaleId ~= localeId then + localeId = player.LocaleId + localeChangedEvent:Fire(localeId) + end +end + +local function reset() + playerTranslator = nil + player = nil + didWarn = false + + if localeIdConnection then + localeIdConnection:Disconnect() + localeIdConnection = nil + end +end + +local function getTranslator() + if not playerTranslator then + player = Players.LocalPlayer + if player then + playerTranslator = LocalizationService:GetTranslatorForPlayer(player) + + handlePlayerOrLocaleChanged() + localeIdConnection = player:GetPropertyChangedSignal("LocaleId"):Connect(handlePlayerOrLocaleChanged) + end + end + return playerTranslator +end + +if CoreScriptTranslateGameText then + Players:GetPropertyChangedSignal("LocalPlayer"):Connect(function() + -- LocalPlayer changed + reset() + getTranslator() + end) +end + +local GameTranslator = {} + +GameTranslator.LocaleChanged = localeChangedEvent.Event + +-- This is meant for translating user game text that appears under CoreGui. +-- It uses Player.LocaleId and the LocalizationTables under LocalizationService. +-- This includes team names, score names, tool names, and notification toasts. +-- DO NOT USE THIS TO TRANSLATE ROBLOX TEXT IN ROBLOX GUIS!!! +-- Text from Roblox in Roblox guis should use LocalizationService.RobloxLocaleId +-- and the CoreScriptLocalization table, NOT user tables with the game locale ID. +function GameTranslator:TranslateGameText(context, text) + if CoreScriptTranslateGameText then + local translator = getTranslator() + if translator then + return translator:RobloxOnlyTranslate(context, text) + elseif not didWarn then + warn("CoreScript failed to translate text. Translator not ready.") + didWarn = true + end + end + + return text +end + +return GameTranslator \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/GoogleAnalyticsUtils.lua b/Client2018/content/scripts/CoreScripts/Modules/GoogleAnalyticsUtils.lua new file mode 100644 index 0000000..5c8e4b4 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/GoogleAnalyticsUtils.lua @@ -0,0 +1,19 @@ +--[[ + Filename: GoogleAnalyticsUtils.lua + Written by: dbanks + Description: Shared variables/work in reporting analytics. +--]] + +local GoogleAnalyticsUtils = {} + +-- Make sure these stay in sync with values in Analytics.h +GoogleAnalyticsUtils.CA_CATEGORY_GAME = "Game" +GoogleAnalyticsUtils.GA_CATEGORY_ACTION = "Action" +GoogleAnalyticsUtils.GA_CATEGORY_ERROR = "Error" +GoogleAnalyticsUtils.GA_CATEGORY_STUDIO = "Studio" +GoogleAnalyticsUtils.GA_CATEGORY_COUNTERS = "Counters" +GoogleAnalyticsUtils.GA_CATEGORY_RIBBONBAR = "RibbonBar" +GoogleAnalyticsUtils.GA_CATEGORY_SECURITY = "Security" +GoogleAnalyticsUtils.GA_CATEGORY_STUDIO_SETTINGS = "StudioSettings" + +return GoogleAnalyticsUtils \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/HttpAnalyticsTab.lua b/Client2018/content/scripts/CoreScripts/Modules/HttpAnalyticsTab.lua new file mode 100644 index 0000000..075d299 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/HttpAnalyticsTab.lua @@ -0,0 +1,610 @@ +local ROW_HEIGHT = 20 +local COL_WIDTH = 100 +local BORDER_WIDTH = 1 +local BORDER_COLOR = Color3.new(1,1,1) +local STD_BG_COLOR = Color3.new(0.25, 0.75, 0.75) +local ALT_BG_COLOR = Color3.new(0.25, 0.25, 0.75) +local TEXT_COLOR = Color3.new(1, 1, 1) +local SORTED_TITLE_COLOR = Color3.new(1,0,1) +local BG_TRANSPARENCY = 0.9 + +local COL_WIDTHS = {50,50,50,65,120,-335} +local COL_KEYS = { "Method", "Status", "Time", "RequestType", "URL"} +local COL_TITLES = {"No.", "Method", "Status", "Time(ms)", "RequestType", "URL"} +local MAX_ROW = 256 + +pcall(function () + MAX_ROW = tonumber(settings():GetFVariable("HttpAnalyticsMaxHistory")) +end) + +--//////////////////////////////////// +-- +-- UIListGridItemClass +-- a single grid in the list +-- it has a 'Frame' to show background and 'TextLabel' to show text +-- +--//////////////////////////////////// +local UIListGridItemClass = {} +UIListGridItemClass.__index = UIListGridItemClass + +function UIListGridItemClass.new(parentFrame, pos, size, bgColor) + local self = {} + setmetatable(self, UIListGridItemClass) + + self._parent = parentFrame + + self._frame = Instance.new("Frame") + self._frame.Position = pos + self._frame.Size = size + self._frame.BorderColor3 = BORDER_COLOR + self._frame.BorderSizePixel = BORDER_WIDTH + self._frame.BackgroundTransparency = BG_TRANSPARENCY + self._frame.ZIndex = self._parent.ZIndex + 1 + self._frame.BackgroundColor3 = bgColor + self._frame.Parent = self._parent + + self._label = Instance.new("TextLabel") + self._label.TextColor3 = TEXT_COLOR + self._label.Position = UDim2.new(0, 5, 0, 0) + self._label.Size = UDim2.new(1, -10, 1, 0) + self._label.BackgroundTransparency = 1 + self._label.TextXAlignment = Enum.TextXAlignment.Left + self._label.ZIndex = self._parent.ZIndex + 1 + self._label.Parent = self._frame + + return self +end + +function UIListGridItemClass:setValue( value ) + if self._label == nil then + return + end + + local valueType = type(value) + local text + if valueType == "string" then + text = value + elseif valueType == "number" then + if value - math.floor(value) > 0 then + text = string.format("%.02f", value) + else + text = tostring(value) + end + else + text = tostring(value) + end + + self._label.Text = text +end + +function UIListGridItemClass:getFrame() + return self._frame +end + +--//////////////////////////////////// +-- +-- UIListClass +-- to show lua tables in UI +-- creates and manager UIListGridItemClass instance +-- +--//////////////////////////////////// +local UIListClass = {} +UIListClass.__index = UIListClass + +-- callbackClicked(row, col, frame) 'frame' is the 'Frame' instance be clicked +function UIListClass.new(parentFrame, callbackClicked) + local self = {} + setmetatable(self, UIListClass) + + self._parent = parentFrame + self._frame = Instance.new("Frame") + self._frame.Name = "UIListClass" + self._frame.Position = UDim2.new(0,1,0,1) + self._frame.Size = UDim2.new(1,0,0,0) + self._frame.ZIndex = self._parent.ZIndex + self._frame.BorderSizePixel = 0 + self._frame.Parent = self._parent + + self._columnWidths = {COL_WIDTH} + self._rowHeight = ROW_HEIGHT + + self._rows = {} + + self._callbackClicked = callbackClicked + return self +end + +function UIListClass:setColumns(names, widths) + self:setColumnWidths(widths) + self:setRow(1, names) +end + +function UIListClass:setColumnWidths( widths ) + self._columnWidths = widths +end + +function UIListClass:setRow( index, values ) + if #self._rows + 1 < index then + return + elseif #self._rows < index then + self:addRow(values) + else + local row = self._rows[index] + for i,v in ipairs(row) do + v:setValue(values[i]) + end + end +end + +function UIListClass:addRow( values ) + local index = #self._rows+1 + local row = {} + for i = 1, #values do + if i > #self._columnWidths then + break + end + + local pos = self:getItemPosition(index, i) + local size = self:getItemSize(index,i) + local bgColor + if (index % 2 == 1) then + bgColor = STD_BG_COLOR + else + bgColor = ALT_BG_COLOR + end + local item = UIListGridItemClass.new(self._frame, pos, size, bgColor) + + -- set click callback + local itemFrame = item:getFrame() + itemFrame.InputBegan:connect(function(input) + + -- set the frame's background color and call _callbackClicked + if input.UserInputType == Enum.UserInputType.MouseButton1 then + if self._callbackClicked ~= nil then + self._callbackClicked(index, i, itemFrame) + end + end + end) + + item:setValue(values[i]) + table.insert(row, item) + end + table.insert(self._rows, row) +end + +function UIListClass:getItemPosition( row, col ) + local offsetX = 0 + local offsetY = 0 + for i = 1, col-1 do + offsetX = offsetX + self._columnWidths[i] + end + for i = 1, row-1 do + offsetY = offsetY + self._rowHeight + end + + return UDim2.new(0, offsetX, 0, offsetY) +end + +function UIListClass:getItemSize( row, col ) + if self._columnWidths[col] < 0 then + return UDim2.new(1, -self._columnWidths[col], 0, self._rowHeight) + else + return UDim2.new(0, self._columnWidths[col], 0, self._rowHeight) + end +end + +function UIListClass:getHeightInPix() + return self._rowHeight * #self._rows +end + +function UIListClass:getFrame() + return self._frame +end + +--//////////////////////////////////// +-- +-- HttpResultList +-- manage http results data and set data to UIListClass then the http results will show in UI +-- call HttpResultListClass:addHttpResult to add a row of http result to UI +-- +--//////////////////////////////////// +local HttpResultListClass = {} +HttpResultListClass.__index = HttpResultListClass + +function HttpResultListClass.new( parentFrame, heightChange, callbackClicked) + local self = {} + setmetatable(self, HttpResultListClass) + + self._sortedColumn = 1 + self._list = UIListClass.new(parentFrame, callbackClicked) + + self._heightChangedCallback = heightChange + self._rowValues = {} + self._counter = 0 + self._sortedTitleLastColor = nil + self._sortedTitleFrame = nil + return self +end + +function HttpResultListClass:sort(col,frame) + if col == self._sortedColumn then + return + end + if col < 1 or + #self._rowValues < 2 or + col > #self._rowValues[1] then + return + end + self._sortedColumn = col + self._sortedTitleLastColor = frame.BackgroundColor3 + if self._sortedTitleFrame ~= nil then + self._sortedTitleFrame.BackgroundColor3 = self._sortedTitleLastColor + end + self._sortedTitleFrame = frame + self._sortedTitleFrame.BackgroundColor3 = SORTED_TITLE_COLOR + table.sort(self._rowValues, function (row1, row2) + return row1[self._sortedColumn] < row2[self._sortedColumn] + end) + for i,values in ipairs(self._rowValues) do + self._list:setRow(i+1, values) + end +end + +-- insert 'valuse'. If '_rowValues' is ordered, will keep the order +function HttpResultListClass:insert(values) + if self._sortedColumn == 0 or #self._rowValues == 0 then + table.insert(self._rowValues, values) + return + else + -- insert to an ordered table + -- find a right position in the ordered table + local posInsert = #self._rowValues + 1 + for i=1,#self._rowValues do + if self._rowValues[i][self._sortedColumn] > values[self._sortedColumn] then + posInsert = i + break + end + end + + -- inser the 'values' at posInsert + -- move the all items after posInsert back off one position + table.insert(self._rowValues, posInsert, values) + + end +end + +-- remove the the oldest item +function HttpResultListClass:remove() + local removeIndex = 1 + if self._sortedColumn > 1 then + local removeCounterValue = self._counter - MAX_ROW + 1 + for i,v in ipairs(self._rowValues) do + if v[1] == removeCounterValue then + removeIndex = i + break + end + end -- end for + end + table.remove(self._rowValues, removeIndex) +end + +function HttpResultListClass:addRow(values) + local isHeightUpdate = false + if #self._rowValues < MAX_ROW then + self:insert(values) + isHeightUpdate = true + else + self:remove() + self:insert(values) + end + for i,values in ipairs(self._rowValues) do + self._list:setRow(i+1, values) + end + if isHeightUpdate and self._heightChangedCallback ~= nil then + self._heightChangedCallback(self._list:getHeightInPix()) + end +end + +-- add a row of http result to list +function HttpResultListClass:addHttpResult(httpResult) + if httpResult["Hiden"] == true then + return + end + if #self._rowValues == 0 then + self._list:setColumns(COL_TITLES, COL_WIDTHS) + end + + local values = {self._counter+1} + for _,v in ipairs(COL_KEYS) do + table.insert(values, httpResult[v]) + end + table.insert(values, httpResult["Response"]) + self:addRow(values) + self._counter = self._counter+1 +end + +function HttpResultListClass:getHeightInPix() + return self._list:getHeightInPix() +end + +function HttpResultListClass:getFrame() + return self._list:getFrame() +end + +function HttpResultListClass:getValues(row) + return self._rowValues[row] +end + +--//////////////////////////////////// +-- +-- HttpAnalyticsTable +-- how many requests have been made, how many requests have failed, +-- average, min, and max response time for per developer facing RequestType +-- +--//////////////////////////////////// +local HttpAnalyticsTableClass = {} +HttpAnalyticsTableClass.__index = HttpAnalyticsTableClass + +local HTTP_ANALYTICS_TITTLES = {"RequestType","RequestCount","FailedCount", + "AverageTime(ms)","MinTime(ms)","MaxTime(ms)"} +local HTTP_ANALYTICS_KEYS = {"RequestType","RequestCount","FailedCount", + "AverageTime","MinTime","MaxTime"} +local HTTP_ANALYTICS_WIDTHS = {120, 90, 90, 110, 90, 90} +function HttpAnalyticsTableClass.new( parentFrame,callback ) + local self = {} + setmetatable(self, HttpAnalyticsTableClass) + + self._list = UIListClass.new(parentFrame) + self._tableData = {} + self._tableData.size = 0 + self._heightChangedCallback = callback + return self +end + +-- http analyzer data +-- show how many requests have been made, how many requests have failed, +-- average, min, and max response time for per developer facing RequestType +function HttpAnalyticsTableClass:addHttpResult( httpResult ) + if self._tableData.size == 0 then + self._list:setColumns(HTTP_ANALYTICS_TITTLES, HTTP_ANALYTICS_WIDTHS) + end + local record = self._tableData[httpResult.RequestType] + local isHeightUpdate = false + if record == nil then + self._tableData[httpResult.RequestType] = { + RequestType = httpResult.RequestType, + RequestCount = 1, + FailedCount = 0, + AverageTime = httpResult.Time, + MinTime = httpResult.Time, + MaxTime = httpResult.Time, + index = self._tableData.size + 1 + } + self._tableData.size = self._tableData.size + 1 + if httpResult.Status >= 400 then + self._tableData[httpResult.RequestType].FailedCount = 1 + end + + record = self._tableData[httpResult.RequestType] + + isHeightUpdate = true + else + record.RequestCount = record.RequestCount + 1 + if httpResult.Status >= 400 then + record.FailedCount = record.FailedCount + 1 + end + record.AverageTime = (record.AverageTime*record.RequestCount + + httpResult.Time - record.AverageTime)/record.RequestCount + if httpResult.Time < record.MinTime then + record.MinTime = httpResult.Time + end + if httpResult.Time > record.MaxTime then + record.MaxTime = httpResult.Time + end + end + + local values = {} + for _,v in ipairs(HTTP_ANALYTICS_KEYS) do + table.insert(values, record[v]) + end + self._list:setRow(record.index+1, values) + if isHeightUpdate and self._heightChangedCallback ~= nil then + self._heightChangedCallback(self:getHeightInPix()) + end +end + +function HttpAnalyticsTableClass:getHeightInPix() + return self._list:getHeightInPix() +end + +function HttpAnalyticsTableClass:getFrame() + return self._list:getFrame() +end + +--//////////////////////////////////// +-- +-- HttpResponseViewClass +-- show response header and body +-- +--//////////////////////////////////// +local RESPONSE_TITLE_SIZE = 12 +local RESPONSE_TITLE_COLOR = Color3.new(0.8,0.8,1) +local RESPONSE_TITLE_INDENTATION = 4 +local RESPONSE_TEXT_INDENTATION = 10 +local CANT_RENDER_MSG = "\n Some content can't be rendered as text." +local BODY_TITTLE = "Response Body:" +local HttpResponseViewClass = {} +HttpResponseViewClass.__index = HttpResponseViewClass + +function HttpResponseViewClass.new( parentFrame,callback ) + local self = {} + setmetatable(self, HttpResponseViewClass) + + self._frame = Instance.new("Frame") + self._frame.Position = UDim2.new(0, 2, 0, 2) + self._frame.Size = UDim2.new(1, -4, 10, 0) + self._frame.BackgroundTransparency = 1 + self._frame.ZIndex = parentFrame.ZIndex+1 + self._frame.Visible = false + self._frame.InputBegan:connect(function(input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self._bodyTitle.Visible = false + self._bodyLabel.Visible = false + self._frame.Visible = false + callback(false) + end + end) + self._frame.Parent = parentFrame + + self._bodyTitle = Instance.new("TextLabel") + self._bodyTitle.Name = "self._bodyTitle" + self._bodyTitle.Position = UDim2.new(0, RESPONSE_TITLE_INDENTATION, 0, RESPONSE_TITLE_INDENTATION) + self._bodyTitle.Size = UDim2.new(1, 0, 0, 50) + self._bodyTitle.TextSize = RESPONSE_TITLE_SIZE + self._bodyTitle.TextColor3 = RESPONSE_TITLE_COLOR + self._bodyTitle.TextXAlignment = Enum.TextXAlignment.Left + self._bodyTitle.TextYAlignment = Enum.TextYAlignment.Top + self._bodyTitle.ZIndex = self._frame.ZIndex + self._bodyTitle.BackgroundTransparency = 1 + self._bodyTitle.Visible = false + self._bodyTitle.TextWrap = true + self._bodyTitle.Text = BODY_TITTLE + self._bodyTitle.Parent = self._frame + + self._bodyLabel = Instance.new("TextLabel") + self._bodyLabel.Name = "self._bodyLabel" + self._bodyLabel.Position = UDim2.new(0, RESPONSE_TEXT_INDENTATION, 0, 29) + self._bodyLabel.Size = UDim2.new(1, -2*RESPONSE_TEXT_INDENTATION, 1, 0) + self._bodyLabel.TextWrap = true + self._bodyLabel.TextXAlignment = Enum.TextXAlignment.Left + self._bodyLabel.TextYAlignment = Enum.TextYAlignment.Top + self._bodyLabel.TextColor3 = TEXT_COLOR + self._bodyLabel.ZIndex = self._frame.ZIndex + self._bodyLabel.BackgroundTransparency = 1 + self._bodyLabel.Visible = false + self._bodyLabel.Parent = self._frame + + return self +end + +-- show labels and update the postion +function HttpResponseViewClass:show(body) + local utf8Length,pos= utf8.len(body) + if utf8Length == nil then + if pos ~= nil then + body = body:sub(1, pos-1) + end + -- tell developers something can't be rendered as text + self._bodyTitle.Text = BODY_TITTLE .. CANT_RENDER_MSG + else + self._bodyTitle.Text = BODY_TITTLE + end + + self._bodyLabel.Text = body + self._bodyTitle.Position = UDim2.new(0, RESPONSE_TITLE_INDENTATION, 0, RESPONSE_TITLE_INDENTATION) + self._bodyLabel.Position = UDim2.new(0, RESPONSE_TEXT_INDENTATION, 0, + self._bodyTitle.Position.Y.Offset + self._bodyTitle.TextBounds.Y) + + self._bodyTitle.Visible = true + self._bodyLabel.Visible = true + self._frame.Visible = true +end + +function HttpResponseViewClass:getHeightInPix() + return RESPONSE_TITLE_INDENTATION + self._bodyTitle.TextBounds.Y + + self._bodyLabel.TextBounds.Y +end + +function HttpResponseViewClass:isVisible() + return self._frame.Visible +end + +--//////////////////////////////////// +-- +-- HttpAnalyzer +-- contain the HttpAnalyticsTable and HttpResultList +-- get data from uper layer, update the position of HttpResultList +-- +--//////////////////////////////////// +local SPACE_TABLE_LIST = 3 +local HttpAnalyzerClass = {} +HttpAnalyzerClass.__index = HttpAnalyzerClass + +function HttpAnalyzerClass.new( parentFrame,callback ) + local self = {} + setmetatable(self, HttpAnalyzerClass) + + self._responseView = HttpResponseViewClass.new(parentFrame, function (isShowed, height) + if not isShowed then + self._httpAnalyticsFrame.Visible = true + self._httpResultListFrame.Visible = true + end + self:heightChange() + end) + + local analyticsTableHeightChange = function (newHeight) + self:updatePosition() + end + + local resultListHeightChange = function (newHeight) + self:heightChange() + end + + local resultListClicked = function(row, col, frame) + if self._responseView:isVisible() then + return + end + if row == 1 then + self._httpResultList:sort(col,frame) + else + local values = self._httpResultList:getValues(row-1) + self:showResponse(values) + end + end + + self._httpAnalyticsTable = HttpAnalyticsTableClass.new(parentFrame, analyticsTableHeightChange) + self._httpResultList = HttpResultListClass.new(parentFrame, resultListHeightChange, resultListClicked) + self._httpAnalyticsFrame = self._httpAnalyticsTable:getFrame() + self._httpResultListFrame = self._httpResultList:getFrame() + self._heightChangedCallback = callback + + return self +end + +function HttpAnalyzerClass:updatePosition() + local tableHeight = self._httpAnalyticsTable:getHeightInPix() + self._httpResultListFrame.Position = UDim2.new(0,1,0,tableHeight+SPACE_TABLE_LIST) + self:heightChange() +end + +function HttpAnalyzerClass:heightChange() + if self._heightChangedCallback ~= nil then + self._heightChangedCallback(self:getHeightInPix()) + end +end + +-- add a row of http result to list +-- and update the analyzer data on table +function HttpAnalyzerClass:addHttpResult( httpResult ) + self._httpAnalyticsTable:addHttpResult(httpResult) + self._httpResultList:addHttpResult(httpResult) +end + +function HttpAnalyzerClass:getHeightInPix() + if self._responseView:isVisible() then + return self._responseView:getHeightInPix() + else + return self._httpAnalyticsTable:getHeightInPix() + + self._httpResultList:getHeightInPix() + SPACE_TABLE_LIST + end +end + +function HttpAnalyzerClass:showResponse(values) + self._httpAnalyticsFrame.Visible = false + self._httpResultListFrame.Visible = false + + self._responseView:show(values[#values]) + self:heightChange() +end + +return HttpAnalyzerClass \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/LoadingScreen3D.lua b/Client2018/content/scripts/CoreScripts/Modules/LoadingScreen3D.lua new file mode 100644 index 0000000..b1b3b85 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/LoadingScreen3D.lua @@ -0,0 +1,372 @@ +-- LoadingScreen3D.lua -- +-- Written by Kip Turner, copyright ROBLOX 2016 -- + +local GUI_DISTANCE_FROM_CAMERA = 6 +local VERTICAL_SCREEN_PERCENT = 1/3 +local HORIZONTAL_SCREEN_PERCENT = 1/3 +local SECOND_TO_FADE = 2.5 +local ROTATIONS_PER_SECOND = 0.5 +local TEXT_SCROLL_SPEED = 25 + +local CoreGui = game:GetService('CoreGui') +local RunService = game:GetService('RunService') +local MarketPlaceService = game:GetService('MarketplaceService') +local UserInputService = game:GetService('UserInputService') +local ReplicatedFirst = game:GetService('ReplicatedFirst') +local GuiService = game:GetService('GuiService') + +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local Util = require(RobloxGui.Modules.Settings.Utility) + +local function FadeElements(element, newValue, duration) + duration = duration or 0.5 + if element == nil then return end + if element:IsA('ImageLabel') or element:IsA('ImageButton') then + Util:TweenProperty(element, 'ImageTransparency', element.ImageTransparency, newValue, duration, Util:GetEaseInOutQuad()) + end + if element:IsA('GuiObject') then + Util:TweenProperty(element, 'BackgroundTransparency', element.BackgroundTransparency, newValue, duration, Util:GetEaseInOutQuad()) + end + if element:IsA('TextLabel') or element:IsA('TextBox') or element:IsA('TextButton') then + Util:TweenProperty(element, 'TextTransparency', element.TextTransparency, newValue, duration, Util:GetEaseInOutQuad()) + end + for _, child in pairs(element:GetChildren()) do + FadeElements(child, newValue, duration) + end +end + + +local GameInfoProvider = {} +do + local LoadingFinishedSignal = Instance.new('BindableEvent') + GameInfoProvider.Finished = false + GameInfoProvider.GameAssetInfo = nil + GameInfoProvider.LoadingFinishedEvent = LoadingFinishedSignal.Event + + function GameInfoProvider:GetGameName() + if self.GameAssetInfo ~= nil then + return self.GameAssetInfo.Name + else + return '' + end + end + + function GameInfoProvider:GetCreatorName() + if self.GameAssetInfo ~= nil then + return self.GameAssetInfo.Creator.Name + else + return '' + end + end + + function GameInfoProvider:IsReady() + return self.Finished + end + + function GameInfoProvider:LoadAssetsAsync() + spawn(function() + while game.PlaceId <= 0 do + wait() + end + + -- load game asset info + local success, result = pcall(function() + self.GameAssetInfo = MarketPlaceService:GetProductInfo(game.PlaceId) + end) + if not success then + print("LoadingScript->GameInfoProvider:LoadAssets:", result) + end + self.Finished = true + LoadingFinishedSignal:Fire() + end) + end +end + + + + +local LoadingScreen = {} + + +local surfaceGuiAdorn = Util:Create'Part' +{ + Name = "LoadingGui"; + Transparency = 1; + CanCollide = false; + Anchored = true; + Archivable = false; + FormFactor = Enum.FormFactor.Custom; + RobloxLocked = true; + Parent = RobloxGui; +} + +local loadingSurfaceGui = Util:Create'SurfaceGui' +{ + Name = "LoadingSurfaceGui"; + Adornee = surfaceGuiAdorn; + ToolPunchThroughDistance = 1000; + CanvasSize = Vector2.new(500, 500); + Archivable = false; + Parent = CoreGui; +} + +local backgroundImage = Util:Create'ImageLabel' +{ + Name = 'LoadingBackground'; + Size = UDim2.new(1,0,1,0); + Image = 'rbxasset://textures/ui/LoadingScreen/BackgroundLight.png'; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(70,70,110,110); + BackgroundTransparency = 1; + Parent = loadingSurfaceGui; +} + +local spinnerRotation = 0 +local spinnerImage = Util:Create'ImageLabel' +{ + Name = 'Spinner'; + Size = UDim2.new(0.25,0,0.25,0); + Position = UDim2.new(0.5 - (0.25/2), 0, 0.45 - (0.25/2), 0); + Image = 'rbxasset://textures/ui/LoadingScreen/LoadingSpinner.png'; + BackgroundTransparency = 1; + Parent = backgroundImage; +} + + +local loadingText = Util:Create'TextLabel' +{ + Name = 'LoadingText'; + Text = 'Loading...'; + BackgroundTransparency = 1; + Font = Enum.Font.SourceSans; + FontSize = Enum.FontSize.Size60; + Position = UDim2.new(0.5,0,0.2,0); + Parent = backgroundImage; +} + +local gameNameText = Util:Create'TextLabel' +{ + Name = 'GameNameText'; + Text = ''; + BackgroundTransparency = 1; + Font = Enum.Font.SourceSans; + FontSize = Enum.FontSize.Size60; + Size = UDim2.new(0.9, 0, 0.1, 0); + Position = UDim2.new(0.05,0,0.65,0); + ClipsDescendants = true; + Parent = backgroundImage; +} + + +local creatorTextContainer = Util:Create'Frame' +{ + Name = 'CreatorTextContainer'; + Size = UDim2.new(0.9, 0, 0.1, 0); + Position = UDim2.new(0.05,0,0.77,0); + BackgroundTransparency = 1; + ClipsDescendants = true; + Parent = backgroundImage; +} + +local creatorTextPosition = 0 +local creatorText = Util:Create'TextLabel' +{ + Name = 'CreatorText'; + Text = ''; + BackgroundTransparency = 1; + Font = Enum.Font.SourceSans; + FontSize = Enum.FontSize.Size42; + Size = UDim2.new(1, 0, 1, 0); + Parent = creatorTextContainer; +} + + +local function ScreenDimsAtDepth(depth) + local camera = workspace.CurrentCamera + if camera then + local aspectRatio = camera.ViewportSize.x / camera.ViewportSize.y + local studHeight = 2 * depth * math.tan(math.rad(camera.FieldOfView/2)) + local studWidth = studHeight * aspectRatio + + return Vector2.new(studWidth, studHeight) + end + return Vector2.new(0,0) +end + +local CleanedUp = false +local freeze = true +delay(2.5, function() + freeze = false +end) +local function UpdateLayout(delta) + local screenDims = ScreenDimsAtDepth(GUI_DISTANCE_FROM_CAMERA) + + surfaceGuiAdorn.Size = Vector3.new(screenDims.x * HORIZONTAL_SCREEN_PERCENT, screenDims.y * VERTICAL_SCREEN_PERCENT, 1) + local camera = workspace.CurrentCamera + if camera then + surfaceGuiAdorn.Parent = camera + end + + + if creatorText.TextBounds.X < creatorTextContainer.AbsoluteSize.X then + creatorText.Position = UDim2.new(0, 0, 0, 0) + creatorText.Size = UDim2.new(1, 0, 1, 0) + elseif delta ~= nil then + creatorText.Size = UDim2.new(0, creatorText.TextBounds.X, 1, 0) + if not freeze then + local newX = (creatorTextPosition - delta * TEXT_SCROLL_SPEED) + if newX + creatorText.AbsoluteSize.X < creatorTextContainer.AbsoluteSize.X then + freeze = true + spawn(function() + Util:TweenProperty(creatorText, 'TextTransparency', creatorText.TextTransparency, 1, 1, Util:GetEaseInOutQuad()) + wait(1.5) + if CleanedUp then return end + creatorTextPosition = 0 + Util:TweenProperty(creatorText, 'TextTransparency', creatorText.TextTransparency, 0, 1, Util:GetEaseInOutQuad()) + wait(1.5) + freeze = false + end) + else + creatorTextPosition = newX + end + end + creatorText.Position = UDim2.new(0, creatorTextPosition, 0, 0) + end + + if not gameNameText.TextFits then + gameNameText.Size = UDim2.new(0.9, 0, 0.3, 0) + gameNameText.Position = UDim2.new(0.05,0,0.5,0) + gameNameText.TextScaled = true + gameNameText.TextWrapped = true + + spinnerImage.Position = UDim2.new(0.5 - (0.25/2), 0, 0.225, 0) + loadingText.Position = UDim2.new(0.5,0,0.15,0) + end + +end + +local CameraChangedConn = nil +local WorkspaceChangedConn = nil + +local function CleanUp() + if CleanedUp then return end + CleanedUp = true + + FadeElements(loadingSurfaceGui, 1, SECOND_TO_FADE) + wait(SECOND_TO_FADE) + + RunService:UnbindFromRenderStep("LoadingGui3D") + surfaceGuiAdorn.Parent = nil + if CameraChangedConn then + CameraChangedConn:disconnect() + CameraChangedConn = nil + end + if WorkspaceChangedConn then + WorkspaceChangedConn:disconnect() + WorkspaceChangedConn = nil + end +end + +local function OnGameInfoLoaded() + local creatorName = GameInfoProvider:GetCreatorName() + if creatorName and creatorName ~= '' then + creatorName = string.format("By %s", tostring(creatorName)) + end + gameNameText.Text = GameInfoProvider:GetGameName() + creatorText.Text = creatorName +end + +local function OnReplicatingFinishedAsync() + local function OnGameLoaded() + CleanUp() + end + + if game:IsLoaded() then + OnGameLoaded() + else + game.Loaded:wait() + OnGameLoaded() + end +end + +local function OnDefaultLoadingGuiRemoved() + CleanUp() +end + + +local function UpdateSurfaceGuiPosition() + local camera = workspace.CurrentCamera + if camera then + local cameraCFrame = camera.CFrame + local cameraLook = cameraCFrame.lookVector + + surfaceGuiAdorn.CFrame = (cameraCFrame * CFrame.Angles(0,math.pi,0)) + cameraLook * GUI_DISTANCE_FROM_CAMERA + end +end + +do + local lastUpdate = tick() + RunService:BindToRenderStep("LoadingGui3D", Enum.RenderPriority.Last.Value, function() + local now = tick() + local delta = now - lastUpdate + + UpdateSurfaceGuiPosition() + UpdateLayout(delta) + + local rotation = delta * ROTATIONS_PER_SECOND * 360 + spinnerRotation = spinnerRotation + rotation + spinnerImage.Rotation = spinnerRotation + + lastUpdate = now + end) + UpdateSurfaceGuiPosition() + UpdateLayout() + + local function connectCameraEvent() + if workspace.CurrentCamera then + if CameraChangedConn then + CameraChangedConn:disconnect() + end + CameraChangedConn = workspace.CurrentCamera:GetPropertyChangedSignal("CFrame"):connect(function() + UpdateSurfaceGuiPosition() + end) + end + end + WorkspaceChangedConn = workspace.Changed:connect(function(prop) + if prop == 'CurrentCamera' then + connectCameraEvent() + end + end) + + connectCameraEvent() +end + +GameInfoProvider:LoadAssetsAsync() +if GameInfoProvider:IsReady() then + OnGameInfoLoaded() +end +GameInfoProvider.LoadingFinishedEvent:connect(OnGameInfoLoaded) + + +if ReplicatedFirst:IsFinishedReplicating() then + spawn(OnReplicatingFinishedAsync) +else + ReplicatedFirst.FinishedReplicating:connect(OnReplicatingFinishedAsync) +end + +if ReplicatedFirst:IsDefaultLoadingGuiRemoved() then + OnDefaultLoadingGuiRemoved() +else + ReplicatedFirst.RemoveDefaultLoadingGuiSignal:connect(OnDefaultLoadingGuiRemoved) +end + +GuiService.ErrorMessageChanged:connect(function() + -- TODO +end) + +GuiService.UiMessageChanged:connect(function(type, newMessage) + -- TODO +end) + +return LoadingScreen + diff --git a/Client2018/content/scripts/CoreScripts/Modules/NewChat.lua b/Client2018/content/scripts/CoreScripts/Modules/NewChat.lua new file mode 100644 index 0000000..7350710 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/NewChat.lua @@ -0,0 +1,294 @@ +--[[ + // FileName: NewChat.lua + // Written by: Xsitsu + // Description: Bridges the topbar in corescripts to any chat system running in the non-corescripts environment. +]] +local CoreGuiService = game:GetService("CoreGui") +local RobloxGui = CoreGuiService:WaitForChild("RobloxGui") + +local StarterGui = game:GetService("StarterGui") +local GuiService = game:GetService("GuiService") +local PlayersService = game:GetService("Players") + +local ChatTypesSet = false +local ClassicChatEnabled = PlayersService.ClassicChat +local BubbleChatEnabled = PlayersService.BubbleChat + +local Util = require(RobloxGui.Modules.ChatUtil) + +local moduleApiTable = {} +do + local ChatWindowState = + { + Visible = true, + MessageCount = 0, + TopbarEnabled = true, + CoreGuiEnabled = true, + } + + local communicationsConnections = {} + local eventConnections = {} + + local SetCoreCache = {} + + local function FindInCollectionByKeyAndType(collection, indexName, type) + if (collection and collection[indexName] and collection[indexName]:IsA(type)) then + return collection[indexName] + end + return nil + end + + local function DispatchEvent(eventName, ...) + local event = FindInCollectionByKeyAndType(communicationsConnections.ChatWindow, eventName, "BindableEvent") + if (event) then + event:Fire(...) + return true + end + return false + end + + local function AttemptInvokeFunction(functionName, ...) + local func = FindInCollectionByKeyAndType(communicationsConnections.ChatWindow, functionName, "BindableFunction") + if (func) then + return true, func:Invoke() + end + return false, nil + end + + local function DoConnectGetCore(connectionName) + StarterGui:RegisterGetCore(connectionName, function(data) + local func = FindInCollectionByKeyAndType(communicationsConnections.GetCore, connectionName, "BindableFunction") + local rVal = nil + if (func) then rVal = func:Invoke(data) end + return rVal + end) + end + + function moduleApiTable:ToggleVisibility() + ChatWindowState.Visible = not ChatWindowState.Visible + local didFire = DispatchEvent("ToggleVisibility") + if (not didFire) then + moduleApiTable.VisibilityStateChanged:fire(ChatWindowState.Visible) + end + end + + function moduleApiTable:SetVisible(visible) + ChatWindowState.Visible = visible + local didFire = DispatchEvent("SetVisible", ChatWindowState.Visible) + if (not didFire) then + moduleApiTable.VisibilityStateChanged:fire(ChatWindowState.Visible) + end + end + + function moduleApiTable:FocusChatBar() + DispatchEvent("FocusChatBar") + end + + function moduleApiTable:GetVisibility() + local success, retVal = AttemptInvokeFunction("GetVisibility") + if (success) then + return retVal + else + return ChatWindowState.Visible + end + end + + function moduleApiTable:GetMessageCount() + local success, retVal = AttemptInvokeFunction("GetMessageCount") + if (success) then + return retVal + else + return ChatWindowState.MessageCount + end + end + + function moduleApiTable:TopbarEnabledChanged(enabled) + ChatWindowState.TopbarEnabled = enabled + DispatchEvent("TopbarEnabledChanged", ChatWindowState.TopbarEnabled) + end + + function moduleApiTable:IsFocused(useWasFocused) + local success, retVal = AttemptInvokeFunction("IsFocused", useWasFocused) + if (success) then + return retVal + else + return false + end + end + + function moduleApiTable:ClassicChatEnabled() + return ClassicChatEnabled + end + + function moduleApiTable:IsBubbleChatOnly() + return BubbleChatEnabled and not ClassicChatEnabled + end + + function moduleApiTable:IsDisabled() + return not (BubbleChatEnabled or ClassicChatEnabled) + end + + function SetInitialChatTypes(chatTypesTable) + if ChatTypesSet then + return + end + ChatTypesSet = true + + local bubbleChat = chatTypesTable.BubbleChatEnabled + local classicChat = chatTypesTable.ClassicChatEnabled + if type(bubbleChat) == "boolean" then + BubbleChatEnabled = bubbleChat + end + if type(classicChat) == "boolean" then + ClassicChatEnabled = classicChat + end + + if not (ClassicChatEnabled or BubbleChatEnabled) then + moduleApiTable.ChatDisabled:fire() + end + if BubbleChatEnabled and not ClassicChatEnabled then + moduleApiTable.BubbleChatOnlySet:fire() + end + end + + moduleApiTable.ChatBarFocusChanged = Util.Signal() + moduleApiTable.VisibilityStateChanged = Util.Signal() + moduleApiTable.MessagesChanged = Util.Signal() + + -- Signals that are called when we get information on if Bubble Chat and Classic chat are enabled from the chat. + moduleApiTable.BubbleChatOnlySet = Util.Signal() + moduleApiTable.ChatDisabled = Util.Signal() + + StarterGui.CoreGuiChangedSignal:connect(function(coreGuiType, enabled) + if (coreGuiType == Enum.CoreGuiType.All or coreGuiType == Enum.CoreGuiType.Chat) then + ChatWindowState.CoreGuiEnabled = enabled + DispatchEvent("CoreGuiEnabled", ChatWindowState.CoreGuiEnabled) + end + end) + + GuiService:AddSpecialKey(Enum.SpecialKey.ChatHotkey) + GuiService.SpecialKeyPressed:connect(function(key, modifiers) + DispatchEvent("SpecialKeyPressed", key, modifiers) + end) + + function DoConnectSetCore(setCoreName) + StarterGui:RegisterSetCore(setCoreName, function(data) + local event = FindInCollectionByKeyAndType(communicationsConnections.SetCore, setCoreName, "BindableEvent") + if (event) then + event:Fire(data) + else + if SetCoreCache[setCoreName] == nil then + SetCoreCache[setCoreName] = {} + end + table.insert(SetCoreCache[setCoreName], data) + end + end) + end + + DoConnectSetCore("ChatMakeSystemMessage") + DoConnectSetCore("ChatWindowPosition") + DoConnectSetCore("ChatWindowSize") + DoConnectSetCore("ChatBarDisabled") + + DoConnectGetCore("ChatWindowPosition") + DoConnectGetCore("ChatWindowSize") + DoConnectGetCore("ChatBarDisabled") + + local function RegisterCoreGuiConnections(containerTable) + if (type(containerTable) == "table") then + local chatWindowCollection = containerTable.ChatWindow + local setCoreCollection = containerTable.SetCore + local getCoreCollection = containerTable.GetCore + + if (type(chatWindowCollection) == "table") then + for i, v in pairs(eventConnections) do + v:disconnect() + end + + if type(chatWindowCollection.ChatTypes) == "table" then + SetInitialChatTypes(chatWindowCollection.ChatTypes) + end + + eventConnections = {} + communicationsConnections.ChatWindow = {} + + communicationsConnections.ChatWindow.ToggleVisibility = FindInCollectionByKeyAndType(chatWindowCollection, "ToggleVisibility", "BindableEvent") + communicationsConnections.ChatWindow.SetVisible = FindInCollectionByKeyAndType(chatWindowCollection, "SetVisible", "BindableEvent") + communicationsConnections.ChatWindow.FocusChatBar = FindInCollectionByKeyAndType(chatWindowCollection, "FocusChatBar", "BindableEvent") + communicationsConnections.ChatWindow.TopbarEnabledChanged = FindInCollectionByKeyAndType(chatWindowCollection, "TopbarEnabledChanged", "BindableEvent") + communicationsConnections.ChatWindow.IsFocused = FindInCollectionByKeyAndType(chatWindowCollection, "IsFocused", "BindableFunction") + communicationsConnections.ChatWindow.SpecialKeyPressed = FindInCollectionByKeyAndType(chatWindowCollection, "SpecialKeyPressed", "BindableEvent") + + + local function DoConnect(index) + communicationsConnections.ChatWindow[index] = FindInCollectionByKeyAndType(chatWindowCollection, index, "BindableEvent") + if (communicationsConnections.ChatWindow[index]) then + local con = communicationsConnections.ChatWindow[index].Event:connect(function(...) moduleApiTable[index]:fire(...) end) + table.insert(eventConnections, con) + end + end + + DoConnect("ChatBarFocusChanged") + DoConnect("VisibilityStateChanged") + DoConnect("MessagesChanged") + + local index = "MessagePosted" + communicationsConnections.ChatWindow[index] = FindInCollectionByKeyAndType(chatWindowCollection, index, "BindableEvent") + if (communicationsConnections.ChatWindow[index]) then + local con = communicationsConnections.ChatWindow[index].Event:connect(function(message) game:GetService("Players"):Chat(message) end) + table.insert(eventConnections, con) + end + + moduleApiTable:SetVisible(ChatWindowState.Visible) + moduleApiTable:TopbarEnabledChanged(ChatWindowState.TopbarEnabled) + + local event = FindInCollectionByKeyAndType(chatWindowCollection, "CoreGuiEnabled", "BindableEvent") + if (event) then + communicationsConnections.ChatWindow.CoreGuiEnabled = event + event:Fire(ChatWindowState.CoreGuiEnabled) + end + + else + error("Table 'ChatWindow' must be provided!") + + end + + if (type(setCoreCollection) == "table" and type(getCoreCollection) == "table") then + communicationsConnections.SetCore = {} + communicationsConnections.GetCore = {} + + local function addSetCore(setCoreName) + local event = FindInCollectionByKeyAndType(setCoreCollection, setCoreName, "BindableEvent") + if (event) then + communicationsConnections.SetCore[setCoreName] = event + if SetCoreCache[setCoreName] then + for i, data in pairs(SetCoreCache[setCoreName]) do + event:Fire(data) + end + SetCoreCache[setCoreName] = nil + end + end + end + + addSetCore("ChatMakeSystemMessage") + addSetCore("ChatWindowPosition") + addSetCore("ChatWindowSize") + addSetCore("ChatBarDisabled") + + communicationsConnections.GetCore.ChatWindowPosition = FindInCollectionByKeyAndType(getCoreCollection, "ChatWindowPosition", "BindableFunction") + communicationsConnections.GetCore.ChatWindowSize = FindInCollectionByKeyAndType(getCoreCollection, "ChatWindowSize", "BindableFunction") + communicationsConnections.GetCore.ChatBarDisabled = FindInCollectionByKeyAndType(getCoreCollection, "ChatBarDisabled", "BindableFunction") + + elseif (type(setCoreCollection) ~= "nil" or type(getCoreCollection) ~= "nil") then + error("Both 'SetCore' and 'GetCore' must be tables if provided!") + + end + + end + end + + StarterGui:RegisterSetCore("CoreGuiChatConnections", RegisterCoreGuiConnections) + +end + +return moduleApiTable diff --git a/Client2018/content/scripts/CoreScripts/Modules/PlayerDropDown.lua b/Client2018/content/scripts/CoreScripts/Modules/PlayerDropDown.lua new file mode 100644 index 0000000..4691d0c --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/PlayerDropDown.lua @@ -0,0 +1,766 @@ +--[[ + // FileName: PlayerDropDown.lua + // Written by: TheGamer101 + // Description: Code for the player drop down in the PlayerList and Chat +]] +local moduleApiTable = {} + +local CoreGui = game:GetService('CoreGui') +local HttpService = game:GetService('HttpService') +local HttpRbxApiService = game:GetService('HttpRbxApiService') +local PlayersService = game:GetService('Players') +local StarterGui = game:GetService("StarterGui") +local AnalyticsService = game:GetService("AnalyticsService") +local RobloxReplicatedStorage = game:GetService('RobloxReplicatedStorage') + +local fixPlayerlistFollowingSuccess, fixPlayerlistFollowingFlagValue = pcall(function() return settings():GetFFlag("FixPlayerlistFollowing") end) +local fixPlayerlistFollowingEnabled = fixPlayerlistFollowingSuccess and fixPlayerlistFollowingFlagValue + +local LocalPlayer = PlayersService.LocalPlayer +while not LocalPlayer do + PlayersService.PlayerAdded:wait() + LocalPlayer = PlayersService.LocalPlayer +end + +local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end) +local FFlagUseNotificationsLocalization = success and result +local FFlagHandlePlayerBlockListsInternalPermissive = settings():GetFFlag('HandlePlayerBlockListsInternalPermissive') +local FFlagCoreScriptsUseLocalizationModule = settings():GetFFlag('CoreScriptsUseLocalizationModule') + +local recentApiRequests = -- stores requests for target players by userId +{ + Following = {}; +} + +local POPUP_ENTRY_SIZE_Y = 24 +local ENTRY_PAD = 2 +local BG_TRANSPARENCY = 0.5 +local BG_COLOR = Color3.new(31/255, 31/255, 31/255) +local TEXT_STROKE_TRANSPARENCY = 0.75 +local TEXT_COLOR = Color3.new(1, 1, 243/255) +local TEXT_STROKE_COLOR = Color3.new(34/255, 34/255, 34/255) +local MAX_FRIEND_COUNT = 200 +local FRIEND_IMAGE = 'https://www.roblox.com/thumbs/avatar.ashx?userId=' + +local GET_BLOCKED_USERIDS_TIMEOUT = 5 + +local RobloxGui = CoreGui:WaitForChild('RobloxGui') +local reportAbuseMenu = require(RobloxGui.Modules.Settings.Pages.ReportAbuseMenu) + +local RobloxTranslator +if FFlagCoreScriptsUseLocalizationModule then + RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator) +end + +local function LocalizedGetString(key, rtv) + pcall(function() + if FFlagCoreScriptsUseLocalizationModule then + rtv = RobloxTranslator:FormatByKey(key) + else + local LocalizationService = game:GetService("LocalizationService") + local CorescriptLocalization = LocalizationService:GetCorescriptLocalizations()[1] + rtv = CorescriptLocalization:GetString(LocalizationService.RobloxLocaleId, key) + end + end) + return rtv +end + + +local BindableEvent_SendNotificationInfo = nil +spawn(function() + BindableEvent_SendNotificationInfo = RobloxGui:WaitForChild("SendNotificationInfo") +end) + +local RemoteEvent_NewFollower = nil +local RemoteEvent_UpdatePlayerBlockList = nil + +spawn(function() + RemoteEvent_NewFollower = RobloxReplicatedStorage:WaitForChild('NewFollower', 86400) or RobloxReplicatedStorage:WaitForChild('NewFollower') + if FFlagHandlePlayerBlockListsInternalPermissive then + RemoteEvent_UpdatePlayerBlockList = RobloxReplicatedStorage:WaitForChild('UpdatePlayerBlockList') + end +end) + + +local function createSignal() + local sig = {} + + local mSignaler = Instance.new('BindableEvent') + + local mArgData = nil + local mArgDataCount = nil + + function sig:fire(...) + mArgData = {...} + mArgDataCount = select('#', ...) + mSignaler:Fire() + end + + function sig:connect(f) + if not f then error("connect(nil)", 2) end + return mSignaler.Event:connect(function() + f(unpack(mArgData, 1, mArgDataCount)) + end) + end + + function sig:wait() + mSignaler.Event:wait() + assert(mArgData, "Missing arg data, likely due to :TweenSize/Position corrupting threadrefs.") + return unpack(mArgData, 1, mArgDataCount) + end + + return sig +end + +local BlockStatusChanged = createSignal() +local MuteStatusChanged = createSignal() + +local function sendNotification(title, text, image, duration, callback) + if BindableEvent_SendNotificationInfo then + BindableEvent_SendNotificationInfo:Fire { Title = title, Text = text, Image = image, Duration = duration, Callback = callback } + end +end + +local function getFriendStatus(selectedPlayer) + if selectedPlayer == LocalPlayer then + return Enum.FriendStatus.NotFriend + else + local success, result = pcall(function() + -- NOTE: Core script only + return LocalPlayer:GetFriendStatus(selectedPlayer) + end) + if success then + return result + else + return Enum.FriendStatus.NotFriend + end + end +end + +-- if userId = nil, then it will get count for local player +local function getFriendCountAsync(userId) + local friendCount = nil + local wasSuccess, result = pcall(function() + local str = 'user/get-friendship-count' + if userId then + str = str..'?userId='..tostring(userId) + end + return HttpRbxApiService:GetAsync(str, Enum.ThrottlingPriority.Default, + Enum.HttpRequestType.Players) + end) + if not wasSuccess then + print("getFriendCountAsync() failed because", result) + return nil + end + result = HttpService:JSONDecode(result) + + if result["success"] and result["count"] then + friendCount = result["count"] + end + + return friendCount +end + +-- checks if we can send a friend request. Right now the only way we +-- can't is if one of the players is at the max friend limit +local function canSendFriendRequestAsync(otherPlayer) + local theirFriendCount = getFriendCountAsync(otherPlayer.UserId) + local myFriendCount = getFriendCountAsync() + + -- assume max friends if web call fails + if not myFriendCount or not theirFriendCount then + return false + end + if myFriendCount < MAX_FRIEND_COUNT and theirFriendCount < MAX_FRIEND_COUNT then + return true + elseif myFriendCount >= MAX_FRIEND_COUNT then + sendNotification("Cannot send friend request", "You are at the max friends limit.", "", 5, function() end) + return false + elseif theirFriendCount >= MAX_FRIEND_COUNT then + sendNotification("Cannot send friend request", otherPlayer.Name.." is at the max friends limit.", "", 5, function() end) + return false + end +end + +-- Returns whether followerUserId is following userId +local function isFollowing(userId, followerUserId) + local apiPath = "user/following-exists?userId=" + local params = userId.."&followerUserId="..followerUserId + local success, result = pcall(function() + return HttpRbxApiService:GetAsync(apiPath..params, + Enum.ThrottlingPriority.Default, Enum.HttpRequestType.Players) + end) + if not success then + print("isFollowing() failed because", result) + return false + else + if fixPlayerlistFollowingEnabled then + -- check to make sure the result isn't cached by checking the most recent response + if followerUserId == LocalPlayer.UserId then + if recentApiRequests["Following"][tostring(userId)] ~= nil then + return recentApiRequests["Following"][tostring(userId)] + end + end + end + end + + -- can now parse web response + result = HttpService:JSONDecode(result) + return result["success"] and result["isFollowing"] +end + +local BlockedList = {} +local MutedList = {} + +local function GetBlockedPlayersAsync() + local userId = LocalPlayer.UserId + local apiPath = "userblock/getblockedusers" .. "?" .. "userId=" .. tostring(userId) .. "&" .. "page=" .. "1" + if userId > 0 then + local blockList = nil + local success, msg = pcall(function() + local request = HttpRbxApiService:GetAsync(apiPath, + Enum.ThrottlingPriority.Default, Enum.HttpRequestType.Players) + blockList = request and game:GetService('HttpService'):JSONDecode(request) + end) + if blockList and blockList['success'] == true and blockList['userList'] then + local returnList = {} + for i, v in pairs(blockList['userList']) do + returnList[v] = true + end + return returnList + end + end + return {} +end + +if FFlagHandlePlayerBlockListsInternalPermissive == false then + spawn(function() + BlockedList = GetBlockedPlayersAsync() + GetBlockedPlayersCompleted = true + end) +end + +local function getBlockedUserIdsFromBlockedList() + local userIdList = {} + for userId, _ in pairs(BlockedList) do + table.insert(userIdList, userId) + end + return userIdList +end + +local function getBlockedUserIds() + if LocalPlayer.UserId > 0 then + local timeWaited = 0 + while true do + if GetBlockedPlayersCompleted then + return getBlockedUserIdsFromBlockedList() + end + timeWaited = timeWaited + wait() + if timeWaited > GET_BLOCKED_USERIDS_TIMEOUT then + return {} + end + end + end + return {} +end + +local function initializeBlockList() + if FFlagHandlePlayerBlockListsInternalPermissive then + spawn(function() + BlockedList = GetBlockedPlayersAsync() + GetBlockedPlayersCompleted = true + + local RemoteEvent_SetPlayerBlockList = RobloxReplicatedStorage:WaitForChild('SetPlayerBlockList') + local blockedUserIds = getBlockedUserIds() + RemoteEvent_SetPlayerBlockList:FireServer(blockedUserIds) + end) + end +end + +local function isBlocked(userId) + if (BlockedList[userId]) then + return true + end + return false +end + +local function isMuted(userId) + if (MutedList[userId] ~= nil and MutedList[userId] == true) then + return true + end + return false +end + +local function BlockPlayerAsync(playerToBlock) + if playerToBlock and LocalPlayer ~= playerToBlock then + local blockUserId = playerToBlock.UserId + if blockUserId > 0 then + if not isBlocked(blockUserId) then + BlockedList[blockUserId] = true + BlockStatusChanged:fire(blockUserId, true) + + if FFlagHandlePlayerBlockListsInternalPermissive then + if RemoteEvent_UpdatePlayerBlockList then + RemoteEvent_UpdatePlayerBlockList:FireServer(blockUserId, true) + end + end + + local success, wasBlocked = pcall(function() + local apiPath = "userblock/block" + local params = "userId=" ..tostring(playerToBlock.UserId) + local request = HttpRbxApiService:PostAsync(apiPath, params, Enum.ThrottlingPriority.Default, Enum.HttpContentType.ApplicationUrlEncoded) + response = request and game:GetService('HttpService'):JSONDecode(request) + return response and response.success + end) + return success and wasBlocked + else + return true + end + end + end + return false +end + +local function UnblockPlayerAsync(playerToUnblock) + if playerToUnblock then + local unblockUserId = playerToUnblock.UserId + + if isBlocked(unblockUserId) then + BlockedList[unblockUserId] = nil + BlockStatusChanged:fire(unblockUserId, false) + + if FFlagHandlePlayerBlockListsInternalPermissive then + if RemoteEvent_UpdatePlayerBlockList then + RemoteEvent_UpdatePlayerBlockList:FireServer(unblockUserId, false) + end + end + + local success, wasUnBlocked = pcall(function() + local apiPath = "userblock/unblock" + local params = "userId=" ..tostring(playerToUnblock.UserId) + local request = HttpRbxApiService:PostAsync(apiPath, params, Enum.ThrottlingPriority.Default, Enum.HttpContentType.ApplicationUrlEncoded) + response = request and game:GetService('HttpService'):JSONDecode(request) + return response and response.success + end) + return success and wasUnBlocked + else + return true + end + end + return false +end + +local function MutePlayer(playerToMute) + if playerToMute and LocalPlayer ~= playerToMute then + local muteUserId = playerToMute.UserId + if muteUserId > 0 then + if not isMuted(muteUserId) then + MutedList[muteUserId] = true + MuteStatusChanged:fire(muteUserId, true) + end + end + end +end + +local function UnmutePlayer(playerToUnmute) + if playerToUnmute then + local unmuteUserId = playerToUnmute.UserId + MutedList[unmuteUserId] = nil + MuteStatusChanged:fire(unmuteUserId, false) + end +end + +function createPlayerDropDown() + local playerDropDown = {} + playerDropDown.Player = nil + playerDropDown.PopupFrame = nil + playerDropDown.HidePopupImmediately = false + playerDropDown.PopupFrameOffScreenPosition = nil -- if this is set the popup frame tweens to a different offscreen position than the default + + playerDropDown.HiddenSignal = createSignal() + + --[[ Functions for when options in the dropdown are pressed ]]-- + local function onFriendButtonPressed() + if playerDropDown.Player then + local status = getFriendStatus(playerDropDown.Player) + if status == Enum.FriendStatus.Friend then + LocalPlayer:RevokeFriendship(playerDropDown.Player) + elseif status == Enum.FriendStatus.Unknown or status == Enum.FriendStatus.NotFriend then + -- cache and spawn + local cachedLastSelectedPlayer = playerDropDown.Player + spawn(function() + -- check for max friends before letting them send the request + if canSendFriendRequestAsync(cachedLastSelectedPlayer) then -- Yields + if cachedLastSelectedPlayer and cachedLastSelectedPlayer.Parent == PlayersService then + AnalyticsService:ReportCounter("PlayerDropDown-RequestFriendship") + AnalyticsService:TrackEvent("Game", "RequestFriendship", "PlayerDropDown") + + LocalPlayer:RequestFriendship(cachedLastSelectedPlayer) + end + end + end) + elseif status == Enum.FriendStatus.FriendRequestSent then + AnalyticsService:ReportCounter("PlayerDropDown-RevokeFriendship") + AnalyticsService:TrackEvent("Game", "RevokeFriendship", "PlayerDropDown") + + LocalPlayer:RevokeFriendship(playerDropDown.Player) + elseif status == Enum.FriendStatus.FriendRequestReceived then + AnalyticsService:ReportCounter("PlayerDropDown-RequestFriendship") + AnalyticsService:TrackEvent("Game", "RequestFriendship", "PlayerDropDown") + + LocalPlayer:RequestFriendship(playerDropDown.Player) + end + + playerDropDown:Hide() + end + end + + local function onDeclineFriendButonPressed() + if playerDropDown.Player then + LocalPlayer:RevokeFriendship(playerDropDown.Player) + playerDropDown:Hide() + end + end + + -- Client unfollows followedUserId + local function onUnfollowButtonPressed() + if not playerDropDown.Player then return end + + local followedUserId = tostring(playerDropDown.Player.UserId) + local apiPath = "user/unfollow" + local params = "followedUserId="..followedUserId + local success, result = pcall(function() + return HttpRbxApiService:PostAsync(apiPath, params, Enum.ThrottlingPriority.Default, + Enum.HttpContentType.ApplicationUrlEncoded, Enum.HttpRequestType.Players) + end) + if not success then + print("unfollowPlayer() failed because", result) + playerDropDown:Hide() + return + end + + result = HttpService:JSONDecode(result) + if result["success"] then + if fixPlayerlistFollowingEnabled then + recentApiRequests["Following"][followedUserId] = false + local text = "no longer following "..playerDropDown.Player.Name + if FFlagUseNotificationsLocalization then + text = string.gsub(LocalizedGetString("PlayerDropDown.onUnfollowButtonPress.success",text),"{RBX_NAME}",playerDropDown.Player.Name) + end + sendNotification("You are", text, FRIEND_IMAGE..followedUserId.."&x=48&y=48", 5, function() end) + end + if RemoteEvent_NewFollower then + RemoteEvent_NewFollower:FireServer(playerDropDown.Player, false) + end + moduleApiTable.FollowerStatusChanged:fire() + end + + playerDropDown:Hide() + end + + local function onBlockButtonPressed() + if playerDropDown.Player then + local cachedPlayer = playerDropDown.Player + spawn(function() + BlockPlayerAsync(cachedPlayer) + end) + playerDropDown:Hide() + end + end + + local function onUnblockButtonPressed() + if playerDropDown.Player then + local cachedPlayer = playerDropDown.Player + spawn(function() + UnblockPlayerAsync(cachedPlayer) + end) + playerDropDown:Hide() + end + end + + local function onReportButtonPressed() + if playerDropDown.Player then + reportAbuseMenu:ReportPlayer(playerDropDown.Player) + playerDropDown:Hide() + end + end + + -- Client follows followedUserId + local function onFollowButtonPressed() + if not playerDropDown.Player then return end + -- + local followedUserId = tostring(playerDropDown.Player.UserId) + local apiPath = "user/follow" + local params = "followedUserId="..followedUserId + local success, result = pcall(function() + return HttpRbxApiService:PostAsync(apiPath, params, Enum.ThrottlingPriority.Default, + Enum.HttpContentType.ApplicationUrlEncoded, Enum.HttpRequestType.Players) + end) + if not success then + print("followPlayer() failed because", result) + playerDropDown:Hide() + return + end + + result = HttpService:JSONDecode(result) + if result["success"] then + if fixPlayerlistFollowingEnabled then + recentApiRequests["Following"][followedUserId] = true + end + local text = "now following "..playerDropDown.Player.Name + if FFlagUseNotificationsLocalization then + text = string.gsub(LocalizedGetString("PlayerDropDown.onFollowButtonPress.success",text),"{RBX_NAME}",playerDropDown.Player.Name) + end + sendNotification("You are", text, FRIEND_IMAGE..followedUserId.."&x=48&y=48", 5, function() end) + if RemoteEvent_NewFollower then + RemoteEvent_NewFollower:FireServer(playerDropDown.Player, true) + end + moduleApiTable.FollowerStatusChanged:fire() + end + + playerDropDown:Hide() + end + + local function createPopupFrame(buttons) + local frame = Instance.new('Frame') + frame.Name = "PopupFrame" + frame.Size = UDim2.new(1, 0, 0, (POPUP_ENTRY_SIZE_Y * #buttons) + (#buttons - ENTRY_PAD)) + frame.Position = UDim2.new(1, 1, 0, 0) + frame.BackgroundTransparency = 1 + + for i,button in ipairs(buttons) do + local btn = Instance.new('TextButton') + btn.Name = button.Name + btn.Size = UDim2.new(1, 0, 0, POPUP_ENTRY_SIZE_Y) + btn.Position = UDim2.new(0, 0, 0, POPUP_ENTRY_SIZE_Y * (i - 1) + ((i - 1) * ENTRY_PAD)) + btn.BackgroundTransparency = BG_TRANSPARENCY + btn.BackgroundColor3 = BG_COLOR + btn.BorderSizePixel = 0 + btn.Text = button.Text + btn.Font = Enum.Font.SourceSans + btn.FontSize = Enum.FontSize.Size14 + btn.TextColor3 = TEXT_COLOR + btn.TextStrokeTransparency = TEXT_STROKE_TRANSPARENCY + btn.TextStrokeColor3 = TEXT_STROKE_COLOR + btn.AutoButtonColor = true + btn.Parent = frame + + btn.MouseButton1Click:connect(button.OnPress) + end + + return frame + end + + local TWEEN_TIME = 0.25 + + function playerDropDown:Hide() + if playerDropDown.PopupFrame then + local offscreenPosition = (playerDropDown.PopupFrameOffScreenPosition ~= nil and playerDropDown.PopupFrameOffScreenPosition or UDim2.new(1, 1, 0, playerDropDown.PopupFrame.Position.Y.Offset)) + if not playerDropDown.HidePopupImmediately then + playerDropDown.PopupFrame:TweenPosition(offscreenPosition, Enum.EasingDirection.InOut, + Enum.EasingStyle.Quad, TWEEN_TIME, true, function() + if playerDropDown.PopupFrame then + playerDropDown.PopupFrame:Destroy() + playerDropDown.PopupFrame = nil + end + end) + else + playerDropDown.PopupFrame:Destroy() + playerDropDown.PopupFrame = nil + end + end + if playerDropDown.Player then + playerDropDown.Player = nil + end + playerDropDown.HiddenSignal:fire() + end + + function playerDropDown:CreatePopup(Player) + playerDropDown.Player = Player + + local buttons = {} + + local status = getFriendStatus(playerDropDown.Player) + local friendText = "" + local canDeclineFriend = false + if status == Enum.FriendStatus.Friend then + friendText = "Unfriend Player" + elseif status == Enum.FriendStatus.Unknown or status == Enum.FriendStatus.NotFriend then + friendText = "Send Friend Request" + elseif status == Enum.FriendStatus.FriendRequestSent then + friendText = "Revoke Friend Request" + elseif status == Enum.FriendStatus.FriendRequestReceived then + friendText = "Accept Friend Request" + canDeclineFriend = true + end + + local blocked = isBlocked(playerDropDown.Player.UserId) + + if not blocked then + table.insert(buttons, { + Name = "FriendButton", + Text = friendText, + OnPress = onFriendButtonPressed, + }) + end + + if canDeclineFriend and not blocked then + table.insert(buttons, { + Name = "DeclineFriend", + Text = "Decline Friend Request", + OnPress = onDeclineFriendButonPressed, + }) + end + -- following status + local following = isFollowing(playerDropDown.Player.UserId, LocalPlayer.UserId) + local followerText = following and "Unfollow Player" or "Follow Player" + + if not blocked then + table.insert(buttons, { + Name = "FollowerButton", + Text = followerText, + OnPress = following and onUnfollowButtonPressed or onFollowButtonPressed, + }) + end + + local blockedText = blocked and "Unblock Player" or "Block Player" + table.insert(buttons, { + Name = "BlockButton", + Text = blockedText, + OnPress = blocked and onUnblockButtonPressed or onBlockButtonPressed, + }) + table.insert(buttons, { + Name = "ReportButton", + Text = "Report Abuse", + OnPress = onReportButtonPressed, + }) + + if playerDropDown.PopupFrame then + playerDropDown.PopupFrame:Destroy() + end + playerDropDown.PopupFrame = createPopupFrame(buttons) + return playerDropDown.PopupFrame + end + + PlayersService.PlayerRemoving:connect(function(leavingPlayer) + if playerDropDown.Player == leavingPlayer then + playerDropDown:Hide() + end + end) + + return playerDropDown +end + +--- GetCore Blocked/Muted/Friended events. + +local PlayerBlockedEvent = Instance.new("BindableEvent") +local PlayerUnblockedEvent = Instance.new("BindableEvent") +local PlayerMutedEvent = Instance.new("BindableEvent") +local PlayerUnMutedEvent = Instance.new("BindableEvent") +local PlayerFriendedEvent = Instance.new("BindableEvent") +local PlayerUnFriendedEvent = Instance.new("BindableEvent") + +BlockStatusChanged:connect(function(userId, isBlocked) + local player = PlayersService:GetPlayerByUserId(userId) + if player then + if isBlocked then + PlayerBlockedEvent:Fire(player) + else + PlayerUnblockedEvent:Fire(player) + end + end +end) + +MuteStatusChanged:connect(function(userId, isMuted) + local player = PlayersService:GetPlayerByUserId(userId) + if player then + if isMuted then + PlayerMutedEvent:Fire(player) + else + PlayerUnMutedEvent:Fire(player) + end + end +end) + +LocalPlayer.FriendStatusChanged:connect(function(player, friendStatus) + if friendStatus == Enum.FriendStatus.Friend then + PlayerFriendedEvent:Fire(player) + elseif friendStatus == Enum.FriendStatus.NotFriend then + PlayerUnFriendedEvent:Fire(player) + end +end) + +StarterGui:RegisterGetCore("PlayerBlockedEvent", function() return PlayerBlockedEvent end) +StarterGui:RegisterGetCore("PlayerUnblockedEvent", function() return PlayerUnblockedEvent end) +StarterGui:RegisterGetCore("PlayerMutedEvent", function() return PlayerMutedEvent end) +StarterGui:RegisterGetCore("PlayerUnmutedEvent", function() return PlayerUnMutedEvent end) +StarterGui:RegisterGetCore("PlayerFriendedEvent", function() return PlayerFriendedEvent end) +StarterGui:RegisterGetCore("PlayerUnfriendedEvent", function() return PlayerUnFriendedEvent end) + +do + moduleApiTable.FollowerStatusChanged = createSignal() + + function moduleApiTable:CreatePlayerDropDown() + return createPlayerDropDown() + end + + function moduleApiTable:GetFriendCountAsync(player) + return getFriendCountAsync(player.UserId) + end + + function moduleApiTable:InitBlockListAsync() + initializeBlockList() + end + + function moduleApiTable:MaxFriendCount() + return MAX_FRIEND_COUNT + end + + function moduleApiTable:GetFriendStatus() + return getFriendStatus() + end + + function moduleApiTable:CreateBlockingUtility() + local blockingUtility = {} + + function blockingUtility:BlockPlayerAsync(player) + return BlockPlayerAsync(player) + end + + function blockingUtility:UnblockPlayerAsync(player) + return UnblockPlayerAsync(player) + end + + function blockingUtility:MutePlayer(player) + return MutePlayer(player) + end + + function blockingUtility:UnmutePlayer(player) + return UnmutePlayer(player) + end + + function blockingUtility:IsPlayerBlockedByUserId(userId) + return isBlocked(userId) + end + + function blockingUtility:GetBlockedStatusChangedEvent() + return BlockStatusChanged + end + + function blockingUtility:GetMutedStatusChangedEvent() + return MuteStatusChanged + end + + function blockingUtility:IsPlayerMutedByUserId(userId) + return isMuted(userId) + end + + function blockingUtility:GetBlockedUserIdsAsync() + return getBlockedUserIds() + end + + return blockingUtility + end +end + +return moduleApiTable diff --git a/Client2018/content/scripts/CoreScripts/Modules/PlayerPermissionsModule.lua b/Client2018/content/scripts/CoreScripts/Modules/PlayerPermissionsModule.lua new file mode 100644 index 0000000..ae2212f --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/PlayerPermissionsModule.lua @@ -0,0 +1,42 @@ +local PlayerPermissionsModule = {} + +local function HasRankInGroupFunctionFactory(groupId, requiredRank) + local hasRankCache = {} + assert(type(requiredRank) == "number", "requiredRank must be a number") + return function(player) + if player and player.UserId > 0 then + if hasRankCache[player.UserId] == nil then + local hasRank = false + pcall(function() + hasRank = player:GetRankInGroup(groupId) >= requiredRank + end) + hasRankCache[player.UserId] = hasRank + end + return hasRankCache[player.UserId] + end + return false + end +end + +local function IsInGroupFunctionFactory(groupId) + local inGroupCache = {} + return function(player) + if player and player.UserId > 0 then + if inGroupCache[player.UserId] == nil then + local inGroup = false + pcall(function() + inGroup = player:IsInGroup(groupId) + end) + inGroupCache[player.UserId] = inGroup + end + return inGroupCache[player.UserId] + end + return false + end +end + +PlayerPermissionsModule.IsPlayerAdminAsync = IsInGroupFunctionFactory(1200769) +PlayerPermissionsModule.IsPlayerInternAsync = HasRankInGroupFunctionFactory(2868472, 100) +PlayerPermissionsModule.IsPlayerStarAsync = IsInGroupFunctionFactory(4199740) + +return PlayerPermissionsModule diff --git a/Client2018/content/scripts/CoreScripts/Modules/PlayerlistModule.lua b/Client2018/content/scripts/CoreScripts/Modules/PlayerlistModule.lua new file mode 100644 index 0000000..a9f1602 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/PlayerlistModule.lua @@ -0,0 +1,2047 @@ +--[[ + // FileName: PlayerlistModule.lua + // Version 1.3 + // Written by: jmargh + // Description: Implementation of in game player list and leaderboard +]] +local CoreGui = game:GetService('CoreGui') +local GuiService = game:GetService('GuiService') -- NOTE: Can only use in core scripts +local UserInputService = game:GetService('UserInputService') +local TeamsService = game:FindService('Teams') +local ContextActionService = game:GetService('ContextActionService') +local StarterGui = game:GetService('StarterGui') +local PlayersService = game:GetService('Players') +local AnalyticsService = game:GetService("AnalyticsService") +local Settings = UserSettings() +local GameSettings = Settings.GameSettings + +local fixPlayerlistFollowingSuccess, fixPlayerlistFollowingFlagValue = pcall(function() return settings():GetFFlag("FixPlayerlistFollowing") end) +local fixPlayerlistFollowingEnabled = fixPlayerlistFollowingSuccess and fixPlayerlistFollowingFlagValue + +local FFlagTheStarsAreBright = settings():GetFFlag("TheStarsAreBright") + +while not PlayersService.LocalPlayer do + -- This does not follow the usual pattern of PlayersService:PlayerAdded:Wait() + -- because it caused a bug where the local players name would show as Player in game. + -- The local players name is not yet set when the PlayerAdded event fires. + wait() +end + +local Player = PlayersService.LocalPlayer +local RobloxGui = CoreGui:WaitForChild('RobloxGui') + +local Utility = require(RobloxGui.Modules.Settings.Utility) +local StatsUtils = require(RobloxGui.Modules.Stats.StatsUtils) + +local TenFootInterface = require(RobloxGui.Modules.TenFootInterface) +local isTenFootInterface = TenFootInterface:IsEnabled() + +local playerDropDownModule = require(RobloxGui.Modules.PlayerDropDown) +local blockingUtility = playerDropDownModule:CreateBlockingUtility() +local playerDropDown = playerDropDownModule:CreatePlayerDropDown() + +local PlayerPermissionsModule = require(RobloxGui.Modules.PlayerPermissionsModule) + +local GameTranslator = require(RobloxGui.Modules.GameTranslator) + +local RemoveEvent_OnFollowRelationshipChanged = nil +local RemoteFunc_GetFollowRelationships = nil + +local Playerlist = {} + +-- Parameters: Sorted Array - see GameStats below +Playerlist.OnLeaderstatsChanged = Instance.new('BindableEvent') +-- Parameters: nameOfStat(string), formatedStringOfStat(string) +Playerlist.OnStatChanged = Instance.new('BindableEvent') + +-- Sorted Array of tables +local GameStats = {} +-- Fields +-- Name: String the developer has given the stat +-- Text: Formated string of the stat value +-- AddId: Child add order id +-- IsPrimary: Is this the primary stat +-- Priority: Sorting priority +-- NOTE: IsPrimary and Priority are unofficially supported. They are left over legacy from the old player list. +-- They can be un-supported at anytime. You should prefer using child add order to order your stats in the leader board. + +local topbarEnabled = true +local playerlistCoreGuiEnabled = true +local MyPlayerEntryTopFrame = nil +local PlayerEntries = {} +local StatAddId = 0 +local TeamEntries = {} +local TeamAddId = 0 +local NeutralTeam = nil +local IsShowingNeutralFrame = false +local LastSelectedFrame = nil +local LastSelectedPlayer = nil +local MinContainerSize = UDim2.new(0, 165, 0.5, 0) +if isTenFootInterface then + MinContainerSize = UDim2.new(0, 1000, 0, 720) +end +local TempHideKeys = {} + +local PlayerEntrySizeY = 24 +if isTenFootInterface then + PlayerEntrySizeY = 100 +end + +local TeamEntrySizeY = 18 + +if isTenFootInterface then + TeamEntrySizeY = 32 +end + +local NameEntrySizeX = 170 +if isTenFootInterface then + NameEntrySizeX = 350 +end + +local StatEntrySizeX = 75 +if isTenFootInterface then + StatEntrySizeX = 250 +end + +local IsSmallScreenDevice = Utility:IsSmallTouchScreen() + +local BaseUrl = game:GetService('ContentProvider').BaseUrl:lower() +BaseUrl = string.gsub(BaseUrl, "/m.", "/www.") +AssetGameUrl = string.gsub(BaseUrl, 'www', 'assetgame') + +--Make SideBar if on Console +local SideBar = nil +local reportAbuseMenu = nil + +--Set Visible Func +local setVisible = nil + +--Whether the playerlist is still open (isOpen is true if the playerlist is hidden in the background) +local isOpen = not isTenFootInterface + +local ENTRY_PAD = 2 +local BG_TRANSPARENCY = 0.5 +local BG_COLOR = Color3.new(31/255, 31/255, 31/255) +local BG_COLOR_TOP = Color3.new(106/255, 106/255, 106/255) +local TEXT_STROKE_TRANSPARENCY = 0.75 +local TEXT_COLOR = Color3.new(1, 1, 243/255) +local TEXT_STROKE_COLOR = Color3.new(34/255, 34/255, 34/255) +local TWEEN_TIME = 0.15 +local MAX_LEADERSTATS = 4 +local MAX_STR_LEN = 12 +local TILE_SPACING = 2 +if isTenFootInterface then + BG_COLOR_TOP = Color3.new(25/255, 25/255, 25/255) + BG_COLOR = Color3.new(60/255, 60/255, 60/255) + BG_TRANSPARENCY = 0.25 + TEXT_STROKE_TRANSPARENCY = 1 + TILE_SPACING = 5 +end +local SHADOW_IMAGE = 'rbxasset://textures/ui/PlayerList/TileShadowMissingTop.png'--'http://www.roblox.com/asset?id=286965900' +local SHADOW_SLICE_SIZE = 5 +local SHADOW_SLICE_RECT = Rect.new(SHADOW_SLICE_SIZE+1, SHADOW_SLICE_SIZE+1, SHADOW_SLICE_SIZE*2-1, SHADOW_SLICE_SIZE*2-1) + +local CUSTOM_ICONS = { -- Admins with special icons + ['7210880'] = 'rbxassetid://134032333', -- Jeditkacheff + ['13268404'] = 'rbxassetid://113059239', -- Sorcus + ['261'] = 'rbxassetid://105897927', -- shedlestky + ['20396599'] = 'rbxassetid://161078086', -- Robloxsai +} + +local ABUSES = { + "Swearing", + "Bullying", + "Scamming", + "Dating", + "Cheating/Exploiting", + "Personal Questions", + "Offsite Links", + "Bad Username", +} + +local CHAT_ICON = 'rbxasset://textures/ui/chat_teamButton.png' +local ADMIN_ICON = 'rbxasset://textures/ui/icon_admin-16.png' +local INTERN_ICON = 'rbxasset://textures/ui/icon_intern-16.png' +local STAR_ICON = 'rbxasset://textures/ui/icon_star-16.png' +local PLACE_OWNER_ICON = 'rbxasset://textures/ui/icon_placeowner.png' +local BC_ICON = 'rbxasset://textures/ui/icon_BC-16.png' +local TBC_ICON = 'rbxasset://textures/ui/icon_TBC-16.png' +local OBC_ICON = 'rbxasset://textures/ui/icon_OBC-16.png' +local BLOCKED_ICON = 'rbxasset://textures/ui/PlayerList/BlockedIcon.png' +local FRIEND_ICON = 'rbxasset://textures/ui/icon_friends_16.png' +local FRIEND_REQUEST_ICON = 'rbxasset://textures/ui/icon_friendrequestsent_16.png' +local FRIEND_RECEIVED_ICON = 'rbxasset://textures/ui/icon_friendrequestrecieved-16.png' + +local FOLLOWER_ICON = 'rbxasset://textures/ui/icon_follower-16.png' +local FOLLOWING_ICON = 'rbxasset://textures/ui/icon_following-16.png' +local MUTUAL_FOLLOWING_ICON = 'rbxasset://textures/ui/icon_mutualfollowing-16.png' + +local CHARACTER_BACKGROUND_IMAGE = 'rbxasset://textures/ui/PlayerList/CharacterImageBackground.png' + +local RobloxTranslator +local FFlagCoreScriptsUseLocalizationModule = settings():GetFFlag('CoreScriptsUseLocalizationModule') +if FFlagCoreScriptsUseLocalizationModule then + RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator) +end + +local function LocalizedGetString(key, rtv) + if FFlagCoreScriptsUseLocalizationModule then + return RobloxTranslator:FormatByKey(key) + else + pcall(function() + local LocalizationService = game:GetService("LocalizationService") + local CorescriptLocalization = LocalizationService:GetCorescriptLocalizations()[1] + rtv = CorescriptLocalization:GetString(LocalizationService.RobloxLocaleId, key) + end) + return rtv + end +end + +local function rbx_profilebegin(name) + debug.profilebegin(name) +end + +local function rbx_profileend() + debug.profileend() +end + +local function clamp(value, min, max) + if value < min then + value = min + elseif value > max then + value = max + end + + return value +end + +local function getFriendStatusIcon(friendStatus) + if friendStatus == Enum.FriendStatus.Unknown or friendStatus == Enum.FriendStatus.NotFriend then + return nil + elseif friendStatus == Enum.FriendStatus.Friend then + return FRIEND_ICON + elseif friendStatus == Enum.FriendStatus.FriendRequestSent then + return FRIEND_REQUEST_ICON + elseif friendStatus == Enum.FriendStatus.FriendRequestReceived then + return FRIEND_RECEIVED_ICON + else + error("PlayerList: Unknown value for friendStatus: "..tostring(friendStatus)) + end +end + +local function getCustomPlayerIcon(player) + local userIdStr = tostring(player.UserId) + if CUSTOM_ICONS[userIdStr] then return nil end + -- + + if PlayerPermissionsModule.IsPlayerAdminAsync(player) then + return ADMIN_ICON + elseif PlayerPermissionsModule.IsPlayerInternAsync(player) then + return INTERN_ICON + elseif FFlagTheStarsAreBright and PlayerPermissionsModule.IsPlayerStarAsync(player) then + return STAR_ICON + end +end + +local function setAvatarIconAsync(player, iconImage) + -- this function is only used on Xbox + -- we require here with pcall because the module is only loaded on Xbox + local thumbnailLoader = nil + pcall(function() + thumbnailLoader = require(RobloxGui.Modules.Shell.ThumbnailLoader) + end) + + local isFinalSuccess = false + if thumbnailLoader then + local loader = thumbnailLoader:LoadAvatarThumbnailAsync(iconImage, math.max(1, player.UserId), + Enum.ThumbnailType.AvatarThumbnail, Enum.ThumbnailSize.Size100x100, true) + isFinalSuccess = loader:LoadAsync(false, true, nil) + end + + if not isFinalSuccess then + iconImage.Image = 'rbxasset://textures/ui/Shell/Icons/DefaultProfileIcon.png' + end +end + +local function getMembershipIcon(player) + if isTenFootInterface then + -- return nothing, we need to spawn off setAvatarIconAsync() as a later time to not block + return "" + else + if blockingUtility:IsPlayerBlockedByUserId(player.UserId) then + return BLOCKED_ICON + else + local userIdStr = tostring(player.UserId) + local membershipType = player.MembershipType + if CUSTOM_ICONS[userIdStr] then + return CUSTOM_ICONS[userIdStr] + elseif player.UserId == game.CreatorId and game.CreatorType == Enum.CreatorType.User then + return PLACE_OWNER_ICON + elseif membershipType == Enum.MembershipType.None then + return "" + elseif membershipType == Enum.MembershipType.BuildersClub then + return BC_ICON + elseif membershipType == Enum.MembershipType.TurboBuildersClub then + return TBC_ICON + elseif membershipType == Enum.MembershipType.OutrageousBuildersClub then + return OBC_ICON + else + return "" + end + end + end + + return "" +end + +local function isValidStat(obj) + return obj:IsA('StringValue') or obj:IsA('IntValue') or obj:IsA('BoolValue') or obj:IsA('NumberValue') or + obj:IsA('DoubleConstrainedValue') or obj:IsA('IntConstrainedValue') +end + +local function sortPlayerEntries(a, b) + if a.PrimaryStat == b.PrimaryStat then + return a.Player.Name:upper() < b.Player.Name:upper() + end + if not a.PrimaryStat then return false end + if not b.PrimaryStat then return true end + local statA = a.PrimaryStat + local statB = b.PrimaryStat + statA = tonumber(statA) or statA + statB = tonumber(statB) or statB + if type(statA) ~= type(statB) then + statA = tostring(statA) + statB = tostring(statB) + end + return statA > statB +end + +local function sortLeaderStats(a, b) + if a.IsPrimary ~= b.IsPrimary then + return a.IsPrimary + end + if a.Priority == b.Priority then + return a.AddId < b.AddId + end + return a.Priority < b.Priority +end + +local function sortTeams(a, b) + if a.TeamScore == b.TeamScore then + return a.Id < b.Id + end + if not a.TeamScore then return false end + if not b.TeamScore then return true end + return a.TeamScore < b.TeamScore +end + +-- Start of Gui Creation +local Container = Instance.new('Frame') +Container.Name = "PlayerListContainer" +Container.Size = MinContainerSize + +if isTenFootInterface then + Container.Position = UDim2.new(0.5, -MinContainerSize.X.Offset/2, 0.25, 0) +else + Container.Position = UDim2.new(1, -167, 0, 2) +end + +-- Every time Performance Stats toggles on/off we need to +-- reposition the main Container, so things don't overlap. +-- Optimally I could just call an "UpdateContainerPosition" function +-- that takes into account everything that affects Container position +-- and recalculate things. +-- +-- Unfortunately, the position of Container may be kind of hard to re-calculate +-- on the fly when it's been shaped based on current leader board state. +-- +-- So instead we do this: +-- We always track where we'd be putting the widget if there were no +-- position stats in targetContainerYOffset. +-- Whenever we reposition Container, we first move it to the ignoring-stats +-- location, (updating targetContainerYOffset), then call the +-- AdjustContainerPosition function to derive final position. +local targetContainerYOffset = Container.Position.Y.Offset + +Container.BackgroundTransparency = 1 +Container.Visible = false +Container.Parent = RobloxGui + +local function AdjustContainerPosition() + -- A function to position the Container in light of presence of performance stats. + if Container == nil then + return + end + + -- Account for presence/absence of performance stats buttons. + local localPlayer = PlayersService.LocalPlayer + local isPerformanceStatsVisible = (GameSettings.PerformanceStatsVisible and localPlayer ~= nil) + local yOffset = targetContainerYOffset + if isPerformanceStatsVisible then + yOffset = yOffset + StatsUtils.ButtonHeight + end + + Container.Position = UDim2.new(Container.Position.X.Scale, + Container.Position.X.Offset, + Container.Position.Y.Scale, + yOffset) +end + +-- When quick profiler button row visiblity changes, update position of +-- Container. +GameSettings.PerformanceStatsVisibleChanged:connect(AdjustContainerPosition) +AdjustContainerPosition() + +-- Scrolling Frame +local noSelectionObject = Instance.new("Frame") +noSelectionObject.BackgroundTransparency = 1 +noSelectionObject.BorderSizePixel = 0 + +local ScrollList = Instance.new('ScrollingFrame') +ScrollList.Name = "ScrollList" +ScrollList.Size = UDim2.new(1, -1, 0, 0) +if isTenFootInterface then + ScrollList.Position = UDim2.new(0, 0, 0, PlayerEntrySizeY + TILE_SPACING) + ScrollList.Size = UDim2.new(1, 19, 0, 0) +end +ScrollList.BackgroundTransparency = 1 +ScrollList.BackgroundColor3 = Color3.new() +ScrollList.BorderSizePixel = 0 +ScrollList.CanvasSize = UDim2.new(0, 0, 0, 0) -- NOTE: Look into if x needs to be set to anything +ScrollList.ScrollBarThickness = 6 +ScrollList.BottomImage = 'rbxasset://textures/ui/scroll-bottom.png' +ScrollList.MidImage = 'rbxasset://textures/ui/scroll-middle.png' +ScrollList.TopImage = 'rbxasset://textures/ui/scroll-top.png' +ScrollList.SelectionImageObject = noSelectionObject +ScrollList.Selectable = false +ScrollList.Parent = Container + +-- PlayerDropDown clipping frame +local PopupClipFrame = Instance.new('Frame') +PopupClipFrame.Name = "PopupClipFrame" +PopupClipFrame.Size = UDim2.new(0, 150, 1.5, 0) +PopupClipFrame.Position = UDim2.new(0, -150 - ENTRY_PAD, 0, 0) +PopupClipFrame.BackgroundTransparency = 1 +PopupClipFrame.ClipsDescendants = true +PopupClipFrame.Parent = Container + +-- Xbox exclusive functions/variables, need to declare here so they can be used elsewhere in file +local xboxSetShieldVisibility = nil +local xboxEnableHotkeys = nil +local xboxDisableHotkeys = nil + +local hasPermissionToVoiceChat = false +if isTenFootInterface then + pcall(function() + local platformService = game:GetService('PlatformService') + hasPermissionToVoiceChat = platformService:BeginCheckXboxPrivilege(252).PrivilegeCheckResult == "NoIssue" + end) +end + +-- Area to set up Xbox disable voice chat +if hasPermissionToVoiceChat then + local CreateHintActionView = require(RobloxGui.Modules.Shell.HintActionView) + local voiceChatService = game:GetService('VoiceChatService') + + -- Player shield + local xboxPlayerlistShield = Instance.new("Frame", RobloxGui) + xboxPlayerlistShield.AutoLocalize = false + xboxPlayerlistShield.Size = UDim2.new(1, 0, 1, 0) + xboxPlayerlistShield.BackgroundColor3 = Color3.fromRGB(41, 41, 41) -- Copied from: SETTINGS_SHIELD_COLOR in SettingsHub.lua + xboxPlayerlistShield.BackgroundTransparency = 0.2 -- Copied from: SETTINGS_SHIELD_TRANSPARENCY in SettingsHub.lua + xboxPlayerlistShield.Visible = false + + local xboxMuteAllState = false + local seenYButtonPressed = false + + local EnableVoicePhrase = "Enable Voice Chat" + local DisableVoicePhrase = "Disable Voice Chat" + EnableVoicePhrase = LocalizedGetString("EnableVoiceKey", EnableVoicePhrase) + DisableVoicePhrase = LocalizedGetString("DisableVoiceKey", DisableVoicePhrase) + + local function getVoiceEnabledString() + return xboxMuteAllState and EnableVoicePhrase or DisableVoicePhrase + end + + -- Set up "toggle voice" hotkey using HintActionView module from the xbox AppShell + local xboxToggleVoiceHotkey = CreateHintActionView(xboxPlayerlistShield, "ToggleVoiceChat", UDim2.new(0.96, -1, 0.96, -1)) + xboxToggleVoiceHotkey:SetText(getVoiceEnabledString()) + xboxToggleVoiceHotkey:SetImage('rbxasset://textures/ui/Shell/ButtonIcons/YButton.png') + + -- Callback for when the "toggle voice" hotkey is activated + local function onToggleVoice(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + seenYButtonPressed = true + elseif inputState == Enum.UserInputState.End and seenYButtonPressed then + xboxMuteAllState = not xboxMuteAllState + voiceChatService:VoiceChatSetMuteAllState(xboxMuteAllState) + xboxToggleVoiceHotkey:SetText(getVoiceEnabledString()) + seenYButtonPressed = false + + -- Analytics + local eventName = xboxMuteAllState and "XboxDisableVoiceChat" or "XboxEnableVoiceChat" + AnalyticsService:ReportCounter(eventName, 1) + AnalyticsService:SetRBXEventStream("console", "XboxOne", eventName, {}) + end + end + + -- Define the Functions that can be used outside of this block + xboxSetShieldVisibility = function(state) + xboxPlayerlistShield.Visible = state + end + + xboxEnableHotkeys = function() + xboxToggleVoiceHotkey:BindAction(onToggleVoice, Enum.KeyCode.ButtonY) + end + + xboxDisableHotkeys = function() + xboxToggleVoiceHotkey:UnbindAction() + seenYButtonPressed = false + end +end + +local function createEntryFrame(name, sizeYOffset, isTopStat) + local containerFrame = Instance.new('Frame') + containerFrame.Name = name + containerFrame.Position = UDim2.new(0, 0, 0, 0) + containerFrame.Size = UDim2.new(1, 0, 0, sizeYOffset) + if isTenFootInterface then + containerFrame.Position = UDim2.new(0, 10, 0, 0) + containerFrame.Size = containerFrame.Size + UDim2.new(0, -20, 0, 0) + end + containerFrame.BackgroundTransparency = 1 + containerFrame.ZIndex = isTenFootInterface and 2 or 1 + + local nameFrame = Instance.new('TextButton') + nameFrame.Name = "BGFrame" + nameFrame.Position = UDim2.new(0, 0, 0, 0) + nameFrame.Size = UDim2.new(0, NameEntrySizeX, 0, sizeYOffset) + nameFrame.BackgroundTransparency = isTopStat and 0 or BG_TRANSPARENCY + nameFrame.BackgroundColor3 = isTopStat and BG_COLOR_TOP or BG_COLOR + nameFrame.BorderSizePixel = 0 + nameFrame.AutoButtonColor = false + nameFrame.Text = "" + nameFrame.Parent = containerFrame + nameFrame.ZIndex = isTenFootInterface and 2 or 1 + pcall(function() + nameFrame.Localize = false + end) + + return containerFrame, nameFrame +end + +local function createEntryNameText(name, text, position, size, fontSize) + local nameLabel = Instance.new('TextLabel') + nameLabel.Name = name + nameLabel.Position = position + nameLabel.Size = size + nameLabel.BackgroundTransparency = 1 + nameLabel.Font = Enum.Font.SourceSans + + if fontSize then + nameLabel.FontSize = fontSize + else + if isTenFootInterface then + nameLabel.FontSize = Enum.FontSize.Size32 + else + nameLabel.FontSize = Enum.FontSize.Size14 + end + end + + nameLabel.TextColor3 = TEXT_COLOR + nameLabel.TextStrokeTransparency = TEXT_STROKE_TRANSPARENCY + nameLabel.TextStrokeColor3 = TEXT_STROKE_COLOR + nameLabel.TextXAlignment = Enum.TextXAlignment.Left + nameLabel.ClipsDescendants = true + nameLabel.Text = text + nameLabel.ZIndex = isTenFootInterface and 2 or 1 + pcall(function() + nameLabel.Localize = false + end) + + return nameLabel +end + +local function createStatFrame(offset, parent, name, isTopStat) + local statFrame = Instance.new('Frame') + statFrame.Name = name + statFrame.Size = UDim2.new(0, StatEntrySizeX, 1, 0) + statFrame.Position = UDim2.new(0, offset + TILE_SPACING, 0, 0) + statFrame.BackgroundTransparency = isTopStat and 0 or BG_TRANSPARENCY + statFrame.BackgroundColor3 = isTopStat and BG_COLOR_TOP or BG_COLOR + statFrame.BorderSizePixel = 0 + statFrame.Parent = parent + pcall(function() + statFrame.Localize = false + end) + + if isTenFootInterface then + statFrame.ZIndex = 2 + + local shadow = Instance.new("ImageLabel") + shadow.BackgroundTransparency = 1 + shadow.Name = 'Shadow' + shadow.Image = SHADOW_IMAGE + shadow.Position = UDim2.new(0, -SHADOW_SLICE_SIZE, 0, 0) + shadow.Size = UDim2.new(1, SHADOW_SLICE_SIZE*2, 1, SHADOW_SLICE_SIZE) + shadow.ScaleType = 'Slice' + shadow.SliceCenter = SHADOW_SLICE_RECT + shadow.Parent = statFrame + end + + return statFrame +end + +local function createStatText(parent, text, isTopStat, isTeamStat) + local statText = Instance.new('TextLabel') + statText.Name = "StatText" + statText.Size = isTopStat and UDim2.new(1, 0, 0.5, 0) or UDim2.new(1, 0, 1, 0) + statText.Position = isTopStat and UDim2.new(0, 0, 0.5, 0) or UDim2.new(0, 0, 0, 0) + statText.BackgroundTransparency = 1 + statText.Font = isTopStat and Enum.Font.SourceSansBold or Enum.Font.SourceSans + if isTenFootInterface then + statText.FontSize = Enum.FontSize.Size32 + else + statText.FontSize = Enum.FontSize.Size14 + end + statText.TextColor3 = TEXT_COLOR + statText.TextStrokeColor3 = TEXT_STROKE_COLOR + statText.TextStrokeTransparency = TEXT_STROKE_TRANSPARENCY + statText.Text = text + statText.Active = true + statText.Parent = parent + pcall(function() + statText.Localize = false + end) + + if isTenFootInterface then + statText.ZIndex = 2 + end + + if isTopStat then + local statName = statText:Clone() + statName.Name = "StatName" + statName.Text = GameTranslator:TranslateGameText(CoreGui, parent.Name) + statName.Position = UDim2.new(0,0,0,0) + statName.Font = Enum.Font.SourceSans + statName.ClipsDescendants = true + statName.Parent = parent + if isTenFootInterface then + statName.ZIndex = 2 + end + end + + if isTeamStat then + statText.Font = 'SourceSansBold' + end + + return statText +end + +local function createImageIcon(image, name, xOffset, parent) + local imageLabel = Instance.new('ImageLabel') + imageLabel.Name = name + if isTenFootInterface then + local background = Instance.new("ImageLabel", parent) + background.Name = 'Background' + background.BackgroundTransparency = 1 + background.Image = CHARACTER_BACKGROUND_IMAGE + background.Size = UDim2.new(0, 66, 0, 66) + background.Position = UDim2.new(0.01, xOffset - 1, 0.5, -background.Size.Y.Offset/2) + background.ZIndex = 2 + + imageLabel.Size = UDim2.new(0, 64, 0, 64) + imageLabel.Position = UDim2.new(0.5, -64/2, 0.5, -64/2) + imageLabel.ZIndex = 2 + imageLabel.Parent = background + else + imageLabel.Size = UDim2.new(0, 16, 0, 16) + imageLabel.Position = UDim2.new(0.01, xOffset, 0.5, -imageLabel.Size.Y.Offset/2) + imageLabel.Parent = parent + end + imageLabel.BackgroundTransparency = 1 + imageLabel.Image = image + imageLabel.BorderSizePixel = 0 + + return imageLabel +end + +local function getScoreValue(statObject) + if statObject:IsA('DoubleConstrainedValue') or statObject:IsA('IntConstrainedValue') then + return statObject.ConstrainedValue + elseif statObject:IsA('BoolValue') then + if statObject.Value then return 1 else return 0 end + else + return statObject.Value + end +end + +local THIN_CHARS = "[^%[iIl\%.,']" +local function strWidth(str) + return string.len(str) - math.floor(string.len(string.gsub(str, THIN_CHARS, "")) / 2) +end + +local function formatNumber(value) + local _,_,minusSign, int, fraction = tostring(value):find('([-]?)(%d+)([.]?%d*)') + int = int:reverse():gsub("%d%d%d", "%1,") + return minusSign..int:reverse():gsub("^,", "")..fraction +end + +local function formatStatString(text) + local numberValue = tonumber(text) + if numberValue then + text = formatNumber(numberValue) + end + + if strWidth(text) <= MAX_STR_LEN then + return text + else + return string.sub(text, 1, MAX_STR_LEN - 3).."..." + end +end + +local LastMaxScrollSize = 0 +local function setScrollListSize() + local teamSize = #TeamEntries * TeamEntrySizeY + local playerSize = #PlayerEntries * PlayerEntrySizeY + local spacing = #PlayerEntries * ENTRY_PAD + #TeamEntries * ENTRY_PAD + local canvasSize = teamSize + playerSize + spacing + if #TeamEntries > 0 and NeutralTeam and IsShowingNeutralFrame then + canvasSize = canvasSize + TeamEntrySizeY + ENTRY_PAD + end + ScrollList.CanvasSize = UDim2.new(0, 0, 0, canvasSize) + local newScrollListSize = math.min(canvasSize, Container.AbsoluteSize.y) + if ScrollList.Size.Y.Offset == LastMaxScrollSize then + if isTenFootInterface then + ScrollList.Size = UDim2.new(1, 20, 0, newScrollListSize) + else + ScrollList.Size = UDim2.new(1, 0, 0, newScrollListSize) + end + end + LastMaxScrollSize = newScrollListSize +end + +local function setPlayerEntryPositions() + local position = 0 + for i = 1, #PlayerEntries do + if isTenFootInterface and PlayerEntries[i].Frame ~= MyPlayerEntryTopFrame then + PlayerEntries[i].Frame.Position = UDim2.new(0, 10, 0, position) + position = position + PlayerEntrySizeY + TILE_SPACING + elseif PlayerEntries[i].Frame ~= MyPlayerEntryTopFrame then + PlayerEntries[i].Frame.Position = UDim2.new(0, 0, 0, position) + position = position + PlayerEntrySizeY + TILE_SPACING + end + end +end + +local function setTeamEntryPositions() + local teams = {} + for _,teamEntry in ipairs(TeamEntries) do + local team = teamEntry.Team + teams[tostring(team.TeamColor)] = {} + end + if NeutralTeam then + teams.Neutral = {} + end + + for _,playerEntry in ipairs(PlayerEntries) do + if playerEntry.Frame ~= MyPlayerEntryTopFrame then + local player = playerEntry.Player + if player.Neutral then + table.insert(teams.Neutral, playerEntry) + elseif teams[tostring(player.TeamColor)] then + table.insert(teams[tostring(player.TeamColor)], playerEntry) + else + table.insert(teams.Neutral, playerEntry) + end + end + end + + local position = 0 + for _,teamEntry in ipairs(TeamEntries) do + local team = teamEntry.Team + teamEntry.Frame.Position = UDim2.new(0, isTenFootInterface and 10 or 0, 0, position) + position = position + TeamEntrySizeY + TILE_SPACING + local players = teams[tostring(team.TeamColor)] + for _,playerEntry in ipairs(players) do + playerEntry.Frame.Position = UDim2.new(0, isTenFootInterface and 10 or 0, 0, position) + position = position + PlayerEntrySizeY + TILE_SPACING + end + end + if NeutralTeam then + NeutralTeam.Frame.Position = UDim2.new(0, isTenFootInterface and 10 or 0, 0, position) + position = position + TeamEntrySizeY + TILE_SPACING + if #teams.Neutral > 0 then + IsShowingNeutralFrame = true + local players = teams.Neutral + for _,playerEntry in ipairs(players) do + playerEntry.Frame.Position = UDim2.new(0, isTenFootInterface and 10 or 0, 0, position) + position = position + PlayerEntrySizeY + TILE_SPACING + end + else + IsShowingNeutralFrame = false + end + end +end + +local function setEntryPositions() + table.sort(PlayerEntries, sortPlayerEntries) + if #TeamEntries > 0 then + setTeamEntryPositions() + else + setPlayerEntryPositions() + end +end + +local function updateSocialIcon(newIcon, bgFrame) + local socialIcon = bgFrame:FindFirstChild('SocialIcon') + local nameFrame = bgFrame:FindFirstChild('PlayerName') + local offset = 19 + if socialIcon then + if newIcon then + socialIcon.Image = newIcon + else + if nameFrame then + local newSize = nameFrame.Size.X.Offset + socialIcon.Size.X.Offset + 2 + nameFrame.Size = UDim2.new(-0.01, newSize, 0.5, 0) + nameFrame.Position = UDim2.new(0.01, offset, 0.245, 0) + end + socialIcon:Destroy() + end + elseif newIcon and bgFrame then + socialIcon = createImageIcon(newIcon, "SocialIcon", offset, bgFrame) + offset = offset + socialIcon.Size.X.Offset + 2 + if nameFrame then + local newSize = bgFrame.Size.X.Offset - offset + nameFrame.Size = UDim2.new(-0.01, newSize, 0.5, 0) + nameFrame.Position = UDim2.new(0.01, offset, 0.245, 0) + end + end +end + +local function getFriendStatus(selectedPlayer) + if selectedPlayer == Player then + return Enum.FriendStatus.NotFriend + else + local success, result = pcall(function() + -- NOTE: Core script only + return Player:GetFriendStatus(selectedPlayer) + end) + if success then + return result + else + return Enum.FriendStatus.NotFriend + end + end +end + +function popupHidden() + if LastSelectedFrame then + for _,childFrame in pairs(LastSelectedFrame:GetChildren()) do + if childFrame:IsA('TextButton') or childFrame:IsA('Frame') then + childFrame.BackgroundColor3 = BG_COLOR + end + end + end + ScrollList.ScrollingEnabled = true + LastSelectedFrame = nil + LastSelectedPlayer = nil +end +playerDropDown.HiddenSignal:connect(popupHidden) + +local function openPlatformProfileUI(rbxUid) + if not rbxUid or rbxUid < 1 then return end + pcall(function() + local platformService = game:GetService('PlatformService') + local platformId = platformService:GetPlatformId(rbxUid) + if platformId and #platformId > 0 then + platformService:PopupProfileUI(Enum.UserInputType.Gamepad1, platformId) + end + end) +end + +local function createPlayerSideBarOption(player) + --Make sure the player is valid and isn't a guest + if player and player.UserId and player.UserId >= 1 then + local platformId = nil + pcall(function() + local platformService = game:GetService('PlatformService') + platformId = platformService:GetPlatformId(player.UserId) + end) + local addReportItem = false + if player ~= PlayersService.LocalPlayer then + addReportItem = true + end + local addGamerCardItem = false + if platformId and #platformId > 0 then + addGamerCardItem = true + end + + --Add sidebar only if we have item(s) to add + if addReportItem or addGamerCardItem then + local savedSelectedGuiObject = GuiService.SelectedCoreObject + if not SideBar then + local sideBarModule = RobloxGui.Modules:FindFirstChild('SideBar') or RobloxGui.Modules.Shell.SideBar + local createSideBarFunc = require(sideBarModule) + SideBar = createSideBarFunc() + end + --Get modules + local screenManagerModule = RobloxGui.Modules:FindFirstChild('ScreenManager') or RobloxGui.Modules.Shell.ScreenManager + local ScreenManager = require(screenManagerModule) + local stringsModule = RobloxGui.Modules:FindFirstChild('LocalizedStrings') or RobloxGui.Modules.Shell.LocalizedStrings + local Strings = require(stringsModule) + + SideBar:RemoveAllItems() + if addGamerCardItem then + SideBar:AddItem(Strings:LocalizedString("ViewGamerCardWord"), function() + openPlatformProfileUI(player.UserId) + end) + end + + if not reportAbuseMenu then + reportAbuseMenu = require(RobloxGui.Modules.Settings.Pages.ReportAbuseMenu) + end + + --We can't report guests/localplayer + if addReportItem then + SideBar:AddItem(Strings:LocalizedString("Report Player"), function() + --Force closing player list before open the report tab + isOpen = false + setVisible(false) + GuiService.SelectedCoreObject = nil + reportAbuseMenu:ReportPlayer(player) + end) + end + + local closedCon = nil + --Will fire when sidebar closes, fires before the item callback + closedCon = SideBar.Closed:connect(function() + closedCon:disconnect() + if Container.Visible then + if savedSelectedGuiObject and savedSelectedGuiObject.Parent then + GuiService.SelectedCoreObject = savedSelectedGuiObject + else + --SavedSelectedGuiObject gets removed, selects the local player's frame + setVisible(true) + end + end + end) + + ScreenManager:OpenScreen(SideBar, false) + end + end +end + +local function onEntryFrameSelected(selectedFrame, selectedPlayer) + if isTenFootInterface then + -- open the profile UI for the selected user. On console we allow user to select themselves + -- they may want quick access to platform profile features + createPlayerSideBarOption(selectedPlayer) + return + end + + if selectedPlayer ~= Player and selectedPlayer.UserId > 1 and Player.UserId > 1 then + if LastSelectedFrame ~= selectedFrame then + if LastSelectedFrame then + for _,childFrame in pairs(LastSelectedFrame:GetChildren()) do + if childFrame:IsA('TextButton') or childFrame:IsA('Frame') then + childFrame.BackgroundColor3 = BG_COLOR + end + end + end + LastSelectedFrame = selectedFrame + LastSelectedPlayer = selectedPlayer + for _,childFrame in pairs(selectedFrame:GetChildren()) do + if childFrame:IsA('TextButton') or childFrame:IsA('Frame') then + childFrame.BackgroundColor3 = Color3.new(0, 1, 1) + end + end + -- NOTE: Core script only + ScrollList.ScrollingEnabled = false + + local PopupFrame = playerDropDown:CreatePopup(selectedPlayer) + PopupFrame.Position = UDim2.new(1, 1, 0, selectedFrame.Position.Y.Offset - ScrollList.CanvasPosition.y) + PopupFrame.Parent = PopupClipFrame + PopupFrame:TweenPosition(UDim2.new(0, 0, 0, selectedFrame.Position.Y.Offset - ScrollList.CanvasPosition.y), Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, TWEEN_TIME, true) + else + playerDropDown:Hide() + LastSelectedFrame = nil + LastSelectedPlayer = nil + end + end +end + +local function onFriendshipChanged(otherPlayer, newFriendStatus) + local entryToUpdate = nil + for _,entry in ipairs(PlayerEntries) do + if entry.Player == otherPlayer then + entryToUpdate = entry + break + end + end + if not entryToUpdate then + return + end + local newIcon = getFriendStatusIcon(newFriendStatus) + local frame = entryToUpdate.Frame + local bgFrame = frame:FindFirstChild('BGFrame') + if bgFrame then + --no longer friends, but might still be following + -- TODO: We need to get follow relationship here; we currently don't have a way + -- to get a single users result, so the server script will need to be updated + -- issue will be when unfriending a user, but still following them, the icon + -- will not show correctly. + updateSocialIcon(newIcon, bgFrame) + end +end + +-- NOTE: Core script only. This fires when a player joins the game. +-- Don't listen/show rbx friends status on xbox +if not isTenFootInterface then + Player.FriendStatusChanged:connect(onFriendshipChanged) +end + +local function setFollowRelationshipsView(relationshipTable) + if not relationshipTable then + return + end + + for i = 1, #PlayerEntries do + local entry = PlayerEntries[i] + local player = entry.Player + local userId = tostring(player.UserId) + + -- don't update icon if already friends + local friendStatus = getFriendStatus(player) + if friendStatus == Enum.FriendStatus.Friend then + return + end + + local icon = nil + if relationshipTable[userId] then + local relationship = relationshipTable[userId] + if relationship.IsMutual == true then + icon = MUTUAL_FOLLOWING_ICON + elseif relationship.IsFollowing == true then + icon = FOLLOWING_ICON + elseif relationship.IsFollower == true then + icon = FOLLOWER_ICON + end + end + + if icon or fixPlayerlistFollowingEnabled then + local frame = entry.Frame + local bgFrame = frame:FindFirstChild('BGFrame') + if bgFrame then + updateSocialIcon(icon, bgFrame) + end + end + end +end + +local function getFollowRelationships() + local result = nil + if RemoteFunc_GetFollowRelationships then + result = RemoteFunc_GetFollowRelationships:InvokeServer() + end + return result +end + +local function updateAllTeamScores() + local teamScores = {} + for _,playerEntry in ipairs(PlayerEntries) do + local player = playerEntry.Player + local leaderstats = player:FindFirstChild('leaderstats') + local team = player.Neutral and 'Neutral' or tostring(player.TeamColor) + local isInValidColor = true + if team ~= 'Neutral' then + for _,teamEntry in ipairs(TeamEntries) do + local color = teamEntry.Team.TeamColor + if team == tostring(color) then + isInValidColor = false + break + end + end + end + if isInValidColor then + team = 'Neutral' + end + if not teamScores[team] then + teamScores[team] = {} + end + if playerEntry.Frame ~= MyPlayerEntryTopFrame then + if leaderstats then + for _,stat in ipairs(GameStats) do + local statObject = leaderstats:FindFirstChild(stat.Name) + if statObject and not statObject:IsA('StringValue') then + if not teamScores[team][stat.Name] then + teamScores[team][stat.Name] = 0 + end + teamScores[team][stat.Name] = teamScores[team][stat.Name] + getScoreValue(statObject) + end + end + end + end + end + + for _,teamEntry in ipairs(TeamEntries) do + local team = teamEntry.Team + local frame = teamEntry.Frame + local color = tostring(team.TeamColor) + local stats = teamScores[color] + if stats then + for statName,statValue in pairs(stats) do + local statFrame = frame:FindFirstChild(statName) + if statFrame then + local statText = statFrame:FindFirstChild('StatText') + if statText then + statText.Text = formatStatString(tostring(statValue)) + end + end + end + else + for _,childFrame in pairs(frame:GetChildren()) do + local statText = childFrame:FindFirstChild('StatText') + if statText then + statText.Text = '' + end + end + end + end + if NeutralTeam then + local frame = NeutralTeam.Frame + local stats = teamScores['Neutral'] + if stats then + frame.Visible = true + for statName,statValue in pairs(stats) do + local statFrame = frame:FindFirstChild(statName) + if statFrame then + local statText = statFrame:FindFirstChild('StatText') + if statText then + statText.Text = formatStatString(tostring(statValue)) + end + end + end + else + frame.Visible = false + end + end +end + +local function updateTeamEntry(entry) + local frame = entry.Frame + local team = entry.Team + local color = team.TeamColor.Color + local offset = NameEntrySizeX + for _,stat in ipairs(GameStats) do + local statFrame = frame:FindFirstChild(stat.Name) + if not statFrame then + statFrame = createStatFrame(offset, frame, stat.Name) + statFrame.BackgroundColor3 = color + createStatText(statFrame, "", false, true) + end + statFrame.Position = UDim2.new(0, offset + TILE_SPACING, 0, 0) + offset = offset + statFrame.Size.X.Offset + TILE_SPACING + end +end + +local function updatePrimaryStats(statName) + for _,entry in ipairs(PlayerEntries) do + local player = entry.Player + local leaderstats = player:FindFirstChild('leaderstats') + entry.PrimaryStat = nil + if leaderstats then + local statObject = leaderstats:FindFirstChild(statName) + if statObject then + local scoreValue = getScoreValue(statObject) + entry.PrimaryStat = scoreValue + end + end + end +end + +local updateLeaderstatFrames = nil +-- TODO: fire event to top bar? +local function initializeStatText(stat, statObject, entry, statFrame, index, isTopStat) + local player = entry.Player + local statValue = getScoreValue(statObject) + if statObject.Name == GameStats[1].Name then + entry.PrimaryStat = statValue + end + local statText = createStatText(statFrame, formatStatString(tostring(statValue)), isTopStat) + -- Top Bar insertion + if player == Player then + stat.Text = statText.Text + end + + statObject.Changed:connect(function(newValue) + rbx_profilebegin("statObject.Changed") + local scoreValue = getScoreValue(statObject) + statText.Text = formatStatString(tostring(scoreValue)) + if statObject.Name == GameStats[1].Name then + entry.PrimaryStat = scoreValue + end + -- Top bar changed event + if player == Player then + stat.Text = statText.Text + Playerlist.OnStatChanged:Fire(stat.Name, stat.Text) + end + updateAllTeamScores() + setEntryPositions() + rbx_profileend() + end) + statObject.ChildAdded:connect(function(child) + rbx_profilebegin("statObject.ChildAdded") + if child.Name == "IsPrimary" then + GameStats[1].IsPrimary = false + stat.IsPrimary = true + updatePrimaryStats(stat.Name) + if updateLeaderstatFrames then updateLeaderstatFrames() end + Playerlist.OnLeaderstatsChanged:Fire(GameStats) + end + rbx_profileend() + end) +end + +updateLeaderstatFrames = function() + table.sort(GameStats, sortLeaderStats) + if #TeamEntries > 0 then + for _,entry in ipairs(TeamEntries) do + updateTeamEntry(entry) + end + if NeutralTeam then + updateTeamEntry(NeutralTeam) + end + end + + for index,entry in ipairs(PlayerEntries) do + local player = entry.Player + local mainFrame = entry.Frame + local offset = NameEntrySizeX + local leaderstats = player:FindFirstChild('leaderstats') + local isTopStat = (entry.Frame == MyPlayerEntryTopFrame) + + if leaderstats then + for _,stat in ipairs(GameStats) do + local statObject = leaderstats:FindFirstChild(stat.Name) + local statFrame = mainFrame:FindFirstChild(stat.Name) + + if not statFrame then + statFrame = createStatFrame(offset, mainFrame, stat.Name, isTopStat) + if statObject then + initializeStatText(stat, statObject, entry, statFrame, index, isTopStat) + end + elseif statObject then + local statText = statFrame:FindFirstChild('StatText') + if not statText then + initializeStatText(stat, statObject, entry, statFrame, index, isTopStat) + end + end + statFrame.Position = UDim2.new(0, offset + TILE_SPACING, 0, 0) + offset = offset + statFrame.Size.X.Offset + TILE_SPACING + end + else + for _,stat in ipairs(GameStats) do + local statFrame = mainFrame:FindFirstChild(stat.Name) + if not statFrame then + statFrame = createStatFrame(offset, mainFrame, stat.Name, isTopStat) + end + offset = offset + statFrame.Size.X.Offset + TILE_SPACING + end + end + + if entry.Frame ~= MyPlayerEntryTopFrame then + if isTenFootInterface then + Container.Position = UDim2.new(0.5, -offset/2, 0, 110) + Container.Size = UDim2.new(0, offset, 0.8, 0) + else + Container.Position = UDim2.new(1, -offset, 0, 2) + Container.Size = UDim2.new(0, offset, 0.5, 0) + end + targetContainerYOffset = Container.Position.Y.Offset + AdjustContainerPosition() + + local newMinContainerOffset = offset + MinContainerSize = UDim2.new(0, newMinContainerOffset, 0.5, 0) + end + end + updateAllTeamScores() + setEntryPositions() + Playerlist.OnLeaderstatsChanged:Fire(GameStats) +end + +local function addNewStats(leaderstats) + for i,stat in ipairs(leaderstats:GetChildren()) do + if isValidStat(stat) and #GameStats < MAX_LEADERSTATS then + local gameHasStat = false + for _,gStat in ipairs(GameStats) do + if stat.Name == gStat.Name then + gameHasStat = true + break + end + end + + if not gameHasStat then + local newStat = {} + newStat.Name = stat.Name + newStat.Text = "-" + newStat.Priority = 0 + local priority = stat:FindFirstChild('Priority') + if priority then newStat.Priority = priority end + newStat.IsPrimary = false + local isPrimary = stat:FindFirstChild('IsPrimary') + if isPrimary then + newStat.IsPrimary = true + end + newStat.AddId = StatAddId + StatAddId = StatAddId + 1 + table.insert(GameStats, newStat) + table.sort(GameStats, sortLeaderStats) + if #GameStats == 1 then + setScrollListSize() + setEntryPositions() + end + end + end + end +end + +local function removeStatFrameFromEntry(stat, frame) + local statFrame = frame:FindFirstChild(stat.Name) + if statFrame then + statFrame:Destroy() + end +end + +local function doesStatExists(stat) + local doesExists = false + for _,entry in ipairs(PlayerEntries) do + local player = entry.Player + if player then + local leaderstats = player:FindFirstChild('leaderstats') + if leaderstats and leaderstats:FindFirstChild(stat.Name) then + doesExists = true + break + end + end + end + + return doesExists +end + +local function onStatRemoved(oldStat, entry) + if isValidStat(oldStat) then + removeStatFrameFromEntry(oldStat, entry.Frame) + local statExists = doesStatExists(oldStat) + -- + local toRemove = nil + for i, stat in ipairs(GameStats) do + if stat.Name == oldStat.Name then + toRemove = i + break + end + end + -- removed from player but not from game; another player still has this stat + if statExists then + if toRemove and entry.Player == Player then + GameStats[toRemove].Text = "-" + Playerlist.OnStatChanged:Fire(GameStats[toRemove].Name, GameStats[toRemove].Text) + end + -- removed from game + else + for _,playerEntry in ipairs(PlayerEntries) do + removeStatFrameFromEntry(oldStat, playerEntry.Frame) + end + for _,teamEntry in ipairs(TeamEntries) do + removeStatFrameFromEntry(oldStat, teamEntry.Frame) + end + if toRemove then + table.remove(GameStats, toRemove) + table.sort(GameStats, sortLeaderStats) + end + end + if GameStats[1] then + updatePrimaryStats(GameStats[1].Name) + end + updateLeaderstatFrames() + end +end + +local function onStatAdded(leaderstats, entry) + leaderstats.ChildAdded:connect(function(newStat) + if isValidStat(newStat) then + addNewStats(newStat.Parent) + updateLeaderstatFrames() + end + end) + leaderstats.ChildRemoved:connect(function(child) + onStatRemoved(child, entry) + end) + addNewStats(leaderstats) + updateLeaderstatFrames() +end + +local function setLeaderStats(entry) + local player = entry.Player + local leaderstats = player:FindFirstChild('leaderstats') + + if leaderstats then + onStatAdded(leaderstats, entry) + end + + local function onPlayerChildChanged(property, child) + if property == 'Name' and child.Name == 'leaderstats' then + onStatAdded(child, entry) + end + end + + player.ChildAdded:connect(function(child) + rbx_profilebegin("player.ChildAdded") + if child.Name == 'leaderstats' then + onStatAdded(child, entry) + end + rbx_profileend() + child.Changed:connect(function(property) + rbx_profilebegin("child.Changed-1") + onPlayerChildChanged(property, child) + rbx_profileend() + end) + end) + for _,child in pairs(player:GetChildren()) do + child.Changed:connect(function(property) + rbx_profilebegin("child.Changed-2") + onPlayerChildChanged(property, child) + rbx_profileend() + end) + end + + player.ChildRemoved:connect(function(child) + rbx_profilebegin("player.ChildRemoved-1") + if child.Name == 'leaderstats' then + for i,stat in ipairs(child:GetChildren()) do + onStatRemoved(stat, entry) + end + updateLeaderstatFrames() + end + rbx_profileend() + end) +end + +local offsetSize = 18 +if isTenFootInterface then offsetSize = 32 end + +local function createPlayerEntry(player, isTopStat) + local playerEntry = {} + local name = player.Name + local hasXboxGamertag = isTenFootInterface and player.DisplayName ~= "" + + local containerFrame, entryFrame = createEntryFrame(name, PlayerEntrySizeY, isTopStat) + entryFrame.Active = true + + entryFrame.MouseButton1Click:connect(function() + onEntryFrameSelected(containerFrame, player) + end) + + local currentXOffset = isTenFootInterface and 14 or 1 + + -- check membership + local membershipIconImage = getMembershipIcon(player) + local membershipIcon = nil + + if membershipIconImage then + membershipIcon = createImageIcon(membershipIconImage, "MembershipIcon", currentXOffset, entryFrame) + currentXOffset = currentXOffset + membershipIcon.Size.X.Offset + (isTenFootInterface and 4 or 2) + else + currentXOffset = currentXOffset + offsetSize + end + + spawn(function() + if isTenFootInterface and membershipIcon then + setAvatarIconAsync(player, membershipIcon) + end + end) + + -- Some functions yield, so we need to spawn off in order to not cause a race condition with other events like PlayersService.ChildRemoved + spawn(function() + -- don't make rank/grp calls on console + if isTenFootInterface then return end + + local success, result = pcall(function() + return player:GetRankInGroup(game.CreatorId) == 255 + end) + if success then + if game.CreatorType == Enum.CreatorType.Group and result then + membershipIconImage = PLACE_OWNER_ICON + if not membershipIcon then + membershipIcon = createImageIcon(membershipIconImage, "MembershipIcon", 1, entryFrame) + else + membershipIcon.Image = membershipIconImage + end + end + else + print("PlayerList: GetRankInGroup failed because", result) + end + local iconImage = getCustomPlayerIcon(player) + if iconImage then + if not membershipIcon then + membershipIcon = createImageIcon(iconImage, "MembershipIcon", 1, entryFrame) + else + membershipIcon.Image = iconImage + end + end + -- Friendship and Follower status is checked by onFriendshipChanged, which is called by the FriendStatusChanged + -- event. This event is fired when any player joins the game. onFriendshipChanged will check Follower status in + -- the case that we are not friends with the new player who is joining. + end) + + local playerName + local playerPlatformName + local robloxIcon + + -- Only show new layout if... + -- 1) It's TenFootInterface + -- 2) Our client has a DisplayName (this implies we have a gamertag and backend cross play is enabled) + if game:GetService('UserInputService'):GetPlatform() == Enum.Platform.XBoxOne and Player.DisplayName ~= "" then + local playerNameXSize = entryFrame.Size.X.Offset - currentXOffset + + if hasXboxGamertag then + playerPlatformName = createEntryNameText("PlayerPlatformName", player.DisplayName, + UDim2.new(0.01, currentXOffset, -0.20, 0), + UDim2.new(-0.01, playerNameXSize, 1, 0)) + playerPlatformName.Parent = entryFrame + end + + robloxIcon = Instance.new('ImageButton') + if hasXboxGamertag then + robloxIcon.Position = UDim2.new(0.01, currentXOffset, 0.21, 30) + else + robloxIcon.Position = UDim2.new(0.01, currentXOffset, 0.5, -12) + end + robloxIcon.Size = UDim2.new(0, 24, 0, 24) + robloxIcon.Image = "rbxasset://textures/ui/Shell/Icons/RobloxIcon24.png" + robloxIcon.BackgroundTransparency = 1 + robloxIcon.ImageColor3 = Color3.new(1,1,1) + robloxIcon.Selectable = false + robloxIcon.ZIndex = 2 + robloxIcon.Parent = entryFrame + + playerName = createEntryNameText("PlayerName", name, + UDim2.new(0.01, robloxIcon.Size.X.Offset + 6, 0, 0), + UDim2.new(0, playerNameXSize, 1, 0)) + playerName.ClipsDescendants = false + playerName.Parent = robloxIcon + else + playerName = createEntryNameText("PlayerName", name, + UDim2.new(0.01, currentXOffset, 0, 0), + UDim2.new(-0.01, entryFrame.Size.X.Offset - currentXOffset, 1, 0)) + + playerName.Parent = entryFrame + end + + local ColorConstants = { + SelectedButtonColor = Color3.new(50/255, 181/255, 1); + TextSelectedColor = Color3.new(19/255, 19/255, 19/255); + IconSelectedColor = Color3.new(19/255, 19/255, 19/255); + TextUnselectedColor = Color3.new(1,1,1); + IconUnselectedColor = Color3.new(1,1,1); + } + + -- update selection for consoles + if isTenFootInterface then + entryFrame.SelectionGained:connect(function() + entryFrame.BackgroundColor3 = ColorConstants.SelectedButtonColor + playerName.TextColor3 = ColorConstants.TextSelectedColor + if playerPlatformName then + playerPlatformName.TextColor3 = ColorConstants.TextSelectedColor + end + if robloxIcon then + robloxIcon.ImageColor3 = ColorConstants.IconSelectedColor + end + end) + entryFrame.SelectionLost:connect(function() + entryFrame.BackgroundColor3 = BG_COLOR + playerName.TextColor3 = ColorConstants.TextUnselectedColor + if playerPlatformName then + playerPlatformName.TextColor3 = ColorConstants.TextUnselectedColor + end + if robloxIcon then + robloxIcon.ImageColor3 = ColorConstants.IconUnselectedColor + end + end) + end + + playerEntry.Player = player + playerEntry.Frame = containerFrame + + if isTenFootInterface then + local shadow = Instance.new("ImageLabel") + shadow.BackgroundTransparency = 1 + shadow.Name = 'Shadow' + shadow.Image = SHADOW_IMAGE + shadow.Position = UDim2.new(0, -SHADOW_SLICE_SIZE, 0, 0) + shadow.Size = UDim2.new(1, SHADOW_SLICE_SIZE*2, 1, SHADOW_SLICE_SIZE) + shadow.ScaleType = 'Slice' + shadow.SliceCenter = SHADOW_SLICE_RECT + shadow.Parent = entryFrame + end + + if isTopStat then + playerName.Font = 'SourceSansBold' + end + + return playerEntry +end + +local function createTeamEntry(team) + local teamEntry = {} + teamEntry.Team = team + teamEntry.TeamScore = 0 + + local containerFrame, entryFrame = createEntryFrame(team.Name, TeamEntrySizeY) + entryFrame.Selectable = false -- dont allow gamepad selection of team frames + entryFrame.BackgroundColor3 = team.TeamColor.Color + + local teamName = createEntryNameText( + "TeamName", + GameTranslator:TranslateGameText(team, team.Name), + UDim2.new(0.01, 1, 0, 0), + UDim2.new(-0.01, entryFrame.AbsoluteSize.x, 1, 0)) + + teamName.Parent = entryFrame + + teamEntry.Frame = containerFrame + + if isTenFootInterface then + local shadow = Instance.new("ImageLabel") + shadow.BackgroundTransparency = 1 + shadow.Name = 'Shadow' + shadow.Image = SHADOW_IMAGE + shadow.Position = UDim2.new(0, -SHADOW_SLICE_SIZE, 0, 0) + shadow.Size = UDim2.new(1, SHADOW_SLICE_SIZE*2, 1, SHADOW_SLICE_SIZE) + shadow.ScaleType = 'Slice' + shadow.SliceCenter = SHADOW_SLICE_RECT + shadow.Parent = entryFrame + end + + -- connections + team.Changed:connect(function(property) + rbx_profilebegin("team.Changed") + if property == 'Name' then + teamName.Text = GameTranslator:TranslateGameText(team, team.Name) + elseif property == 'TeamColor' then + for _,childFrame in pairs(containerFrame:GetChildren()) do + if childFrame:IsA('GuiObject') then + childFrame.BackgroundColor3 = team.TeamColor.Color + end + end + + setTeamEntryPositions() + updateAllTeamScores() + setEntryPositions() + setScrollListSize() + end + rbx_profileend() + end) + + return teamEntry +end + +local function createNeutralTeam() + if not NeutralTeam then + local team = Instance.new('Team') + team.Name = 'Neutral' + team.TeamColor = BrickColor.new('White') + NeutralTeam = createTeamEntry(team) + NeutralTeam.Frame.Parent = ScrollList + end +end + +local function setupEntry(player, newEntry, isTopStat) + setLeaderStats(newEntry) + + if isTopStat then + newEntry.Frame.Parent = Container + table.insert(PlayerEntries, newEntry) + else + newEntry.Frame.Parent = ScrollList + table.insert(PlayerEntries, newEntry) + setScrollListSize() + end + + updateLeaderstatFrames() + + player.Changed:connect(function(property) + rbx_profilebegin("player.Changed-4") + if #TeamEntries > 0 and (property == 'Neutral' or property == 'TeamColor') then + setTeamEntryPositions() + updateAllTeamScores() + setEntryPositions() + setScrollListSize() + end + rbx_profileend() + end) +end + +local function insertPlayerEntry(player) + local entry = createPlayerEntry(player) + setupEntry(player, entry) + + -- create an entry on the top of the playerlist + if player == Player and isTenFootInterface then + local localEntry = createPlayerEntry(player, true) + MyPlayerEntryTopFrame = localEntry.Frame + MyPlayerEntryTopFrame.BackgroundTransparency = 1 + MyPlayerEntryTopFrame.BorderSizePixel = 0 + setupEntry(player, localEntry, true) + end +end + +local function removePlayerEntry(player) + for i = 1, #PlayerEntries do + if PlayerEntries[i].Player == player then + local hadSelectedObject = GuiService.SelectedCoreObject and GuiService.SelectedCoreObject.Parent + PlayerEntries[i].Frame:Destroy() + --Fix lose selection + if Container.Visible then + --previous SelectedCoreObject get removed, reset selection + if hadSelectedObject and (not GuiService.SelectedCoreObject or not GuiService.SelectedCoreObject.Parent) then + --SelectedCoreObject gets removed, selects the first frame + setVisible(true) + end + end + table.remove(PlayerEntries, i) + break + end + end + updateAllTeamScores() + setEntryPositions() + setScrollListSize() +end + +local function onTeamAdded(team) + for i = 1, #TeamEntries do + if TeamEntries[i].Team.TeamColor == team.TeamColor then + TeamEntries[i].Frame:Destroy() + table.remove(TeamEntries, i) + break + end + end + local entry = createTeamEntry(team) + entry.Id = TeamAddId + TeamAddId = TeamAddId + 1 + if not NeutralTeam then + createNeutralTeam() + end + table.insert(TeamEntries, entry) + table.sort(TeamEntries, sortTeams) + setTeamEntryPositions() + updateLeaderstatFrames() + setScrollListSize() + entry.Frame.Parent = ScrollList +end + +local function onTeamRemoved(removedTeam) + for i = 1, #TeamEntries do + local team = TeamEntries[i].Team + if team.Name == removedTeam.Name then + TeamEntries[i].Frame:Destroy() + table.remove(TeamEntries, i) + break + end + end + if #TeamEntries == 0 then + if NeutralTeam then + NeutralTeam.Frame:Destroy() + NeutralTeam.Team:Destroy() + NeutralTeam = nil + IsShowingNeutralFrame = false + end + end + setEntryPositions() + updateLeaderstatFrames() + setScrollListSize() +end + +local function clampCanvasPosition() + local maxCanvasPosition = ScrollList.CanvasSize.Y.Offset - ScrollList.Size.Y.Offset + if maxCanvasPosition >= 0 and ScrollList.CanvasPosition.y > maxCanvasPosition then + ScrollList.CanvasPosition = Vector2.new(0, maxCanvasPosition) + end +end + +local function resizePlayerList() + setScrollListSize() + clampCanvasPosition() +end + +RobloxGui.Changed:connect(function(property) + rbx_profilebegin("RobloxGui.Changed") + if property == 'AbsoluteSize' then + spawn(function() -- must spawn because F11 delays when abs size is set + resizePlayerList() + end) + end + rbx_profileend() + end) + +UserInputService.InputBegan:connect(function(inputObject, isProcessed) + rbx_profilebegin("UserInputService.InputBegan") + if isProcessed then return end + local inputType = inputObject.UserInputType + if (inputType == Enum.UserInputType.Touch and inputObject.UserInputState == Enum.UserInputState.Begin) or + inputType == Enum.UserInputType.MouseButton1 then + if LastSelectedFrame then + playerDropDown:Hide() + end + end + rbx_profileend() + end) + +-- NOTE: Core script only + +PlayersService.PlayerAdded:connect(function(child) + rbx_profilebegin("PlayersService.PlayerAdded") + insertPlayerEntry(child) + rbx_profileend() +end) + +for _, player in ipairs(PlayersService:GetPlayers()) do + insertPlayerEntry(player) +end + +-- Don't listen/show rbx followers status on console +if not isTenFootInterface then + -- spawn so we don't block script + spawn(function() + local RobloxReplicatedStorage = game:GetService('RobloxReplicatedStorage') + RemoveEvent_OnFollowRelationshipChanged = RobloxReplicatedStorage:WaitForChild('FollowRelationshipChanged', 86400) or RobloxReplicatedStorage:WaitForChild('FollowRelationshipChanged') + RemoteFunc_GetFollowRelationships = RobloxReplicatedStorage:WaitForChild('GetFollowRelationships') + + RemoveEvent_OnFollowRelationshipChanged.OnClientEvent:connect(function(result) + rbx_profilebegin("RemoveEvent_OnFollowRelationshipChanged.OnClientEvent") + setFollowRelationshipsView(result) + rbx_profileend() + end) + + local result = getFollowRelationships() + setFollowRelationshipsView(result) + end) +end + +PlayersService.ChildRemoved:connect(function(child) + rbx_profilebegin("PlayersService.ChildRemoved") + if child:IsA("Player") then + if LastSelectedPlayer and child == LastSelectedPlayer then + playerDropDown:Hide() + end + removePlayerEntry(child) + end + rbx_profileend() +end) + +local function initializeTeams(teams) + for _,team in pairs(teams:GetTeams()) do + onTeamAdded(team) + end + + teams.ChildAdded:connect(function(team) + if team:IsA('Team') then + onTeamAdded(team) + end + end) + + teams.ChildRemoved:connect(function(team) + if team:IsA('Team') then + onTeamRemoved(team) + end + end) +end + +TeamsService = game:FindService('Teams') +if TeamsService then + initializeTeams(TeamsService) +end + +game.ChildAdded:connect(function(child) + rbx_profilebegin("game.ChildAdded") + if child:IsA('Teams') then + initializeTeams(child) + end + rbx_profileend() + end) + +Playerlist.GetStats = function() + return GameStats +end + +local noOpFunc = function ( ) +end + + +local closeListFunc = function(name, state, input) + if state ~= Enum.UserInputState.Begin then return end + + isOpen = false + Container.Visible = false + if hasPermissionToVoiceChat then + xboxSetShieldVisibility(false) + xboxDisableHotkeys() + end + spawn(function() GuiService:SetMenuIsOpen(false) end) + ContextActionService:UnbindCoreAction("CloseList") + ContextActionService:UnbindCoreAction("StopAction") + GuiService:RemoveSelectionGroup("PlayerlistGuiSelection") + GuiService.SelectedCoreObject = nil + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None +end + +setVisible = function(state) + Container.Visible = state + if hasPermissionToVoiceChat then + xboxSetShieldVisibility(state) + end + local lastInputType = UserInputService:GetLastInputType() + local isUsingGamepad = (lastInputType == Enum.UserInputType.Gamepad1 or lastInputType == Enum.UserInputType.Gamepad2 or + lastInputType == Enum.UserInputType.Gamepad3 or lastInputType == Enum.UserInputType.Gamepad4) + + if state then + local children = ScrollList:GetChildren() + if children and #children > 0 then + local frame = children[1] + local frameChildren = frame:GetChildren() + for i = 1, #frameChildren do + if frameChildren[i]:IsA("TextButton") then + if isUsingGamepad then + GuiService.SelectedCoreObject = frameChildren[i] + GuiService:AddSelectionParent("PlayerlistGuiSelection", ScrollList) + end + break + end + end + end + --We need to OverrideMouseIcon and rebind core action even if the ScrollList is empty + if isUsingGamepad then + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide + ContextActionService:UnbindCoreAction("CloseList") + ContextActionService:UnbindCoreAction("StopAction") + ContextActionService:BindCoreAction("StopAction", noOpFunc, false, Enum.UserInputType.Gamepad1) + ContextActionService:BindCoreAction("CloseList", closeListFunc, false, Enum.KeyCode.ButtonB, Enum.KeyCode.ButtonStart) + end + if hasPermissionToVoiceChat then + xboxEnableHotkeys() + end + else + if isUsingGamepad then + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None + end + + ContextActionService:UnbindCoreAction("CloseList") + ContextActionService:UnbindCoreAction("StopAction") + if hasPermissionToVoiceChat then + xboxDisableHotkeys() + end + + if GuiService.SelectedCoreObject and GuiService.SelectedCoreObject:IsDescendantOf(Container) then + GuiService.SelectedCoreObject = nil + GuiService:RemoveSelectionGroup("PlayerlistGuiSelection") + end + end +end + +Playerlist.ToggleVisibility = function(name, inputState, inputObject) + if inputState and inputState ~= Enum.UserInputState.Begin then return end + if IsSmallScreenDevice then return end + if not playerlistCoreGuiEnabled then return end + + isOpen = not isOpen + + if next(TempHideKeys) == nil then + setVisible(isOpen) + end +end + +Playerlist.IsOpen = function() + return isOpen +end + +Playerlist.HideTemp = function(self, key, hidden) + if not playerlistCoreGuiEnabled then return end + if IsSmallScreenDevice then return end + + TempHideKeys[key] = hidden and true or nil + + if next(TempHideKeys) == nil then + if isOpen then + setVisible(true) + end + else + if isOpen then + setVisible(false) + end + end +end +local topStat = nil +if isTenFootInterface then + topStat = TenFootInterface:SetupTopStat() +end + +-- NOTE: Core script only +local function onCoreGuiChanged(coreGuiType, enabled) + rbx_profilebegin("onCoreGuiChanged") + if coreGuiType == Enum.CoreGuiType.All or coreGuiType == Enum.CoreGuiType.PlayerList then + -- on console we can always toggle on/off, ignore change + if isTenFootInterface then + playerlistCoreGuiEnabled = true + return + end + + playerlistCoreGuiEnabled = enabled and topbarEnabled + + -- not visible on small screen devices + if IsSmallScreenDevice then + Container.Visible = false + return + end + + setVisible(playerlistCoreGuiEnabled and isOpen and next(TempHideKeys) == nil, true) + + if isTenFootInterface and topStat then + topStat:SetTopStatEnabled(playerlistCoreGuiEnabled) + end + + if playerlistCoreGuiEnabled then + ContextActionService:BindCoreAction("RbxPlayerListToggle", Playerlist.ToggleVisibility, false, Enum.KeyCode.Tab) + else + ContextActionService:UnbindCoreAction("RbxPlayerListToggle") + end + end + rbx_profileend() +end + +Playerlist.TopbarEnabledChanged = function(enabled) + topbarEnabled = enabled + -- Update coregui to reflect new topbar status + onCoreGuiChanged(Enum.CoreGuiType.PlayerList, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.PlayerList)) +end + +onCoreGuiChanged(Enum.CoreGuiType.PlayerList, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.PlayerList)) +StarterGui.CoreGuiChangedSignal:connect(onCoreGuiChanged) + +resizePlayerList() + +local blockStatusChanged = function(userId, isBlocked) + if userId < 0 then return end + + for _,playerEntry in ipairs(PlayerEntries) do + if playerEntry.Player.UserId == userId then + local membershipIcon = getMembershipIcon(playerEntry.Player) + local iconImage = getCustomPlayerIcon(playerEntry.Player) + playerEntry.Frame.BGFrame.MembershipIcon.Image = iconImage and iconImage or membershipIcon + return + end + end +end + +blockingUtility:GetBlockedStatusChangedEvent():connect(blockStatusChanged) + +return Playerlist diff --git a/Client2018/content/scripts/CoreScripts/Modules/PromptCreator.lua b/Client2018/content/scripts/CoreScripts/Modules/PromptCreator.lua new file mode 100644 index 0000000..675b595 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/PromptCreator.lua @@ -0,0 +1,720 @@ +--[[ + // Filename: PromptCreator.lua + // Version 1.0 + // Written by: TheGamer101 + // Description: General module for prompting players to confirm or reject something. + // For usage example see the BlockPlayerPrompt module. +]]-- + +local moduleApiTable = {} + +local CoreGuiService = game:GetService("CoreGui") +local UserInputService = game:GetService("UserInputService") +local ContextActionService = game:GetService("ContextActionService") +local TextService = game:GetService("TextService") +local GuiService = game:GetService("GuiService") + +local RobloxGui = CoreGuiService:WaitForChild("RobloxGui") +local CoreGuiModules = RobloxGui:WaitForChild("Modules") +local TenFootInterface = require(CoreGuiModules:WaitForChild("TenFootInterface")) +local VRModules = CoreGuiModules:WaitForChild("VR") +local VRDialogModule = require(VRModules:WaitForChild("Dialog")) + +function getViewportSize() + while not game.Workspace.CurrentCamera do + game.Workspace.Changed:wait() + end + + -- ViewportSize is initally set to 1, 1 in Camera.cpp constructor. + -- Also check against 0, 0 incase this is changed in the future. + while game.Workspace.CurrentCamera.ViewportSize == Vector2.new(0,0) or + game.Workspace.CurrentCamera.ViewportSize == Vector2.new(1,1) do + game.Workspace.CurrentCamera.Changed:wait() + end + + return game.Workspace.CurrentCamera.ViewportSize +end + +local IsTenFootInterface = TenFootInterface:IsEnabled() +local IsVRMode = false +local IsMobile = UserInputService.TouchEnabled == true and UserInputService.MouseEnabled == false +local IsPhone = IsMobile and (getViewportSize().Y <= 370) +local IsTablet = IsMobile and not IsPhone + +local IsCurrentlyPrompting = false + +local LastInputWasGamepad = false +local WasCoreGuiNavigationEnabled = false +local WasGuiNavigationEnabled = false +local WasAutoSelectGuiEnabled = false + +-- Inital prompt options. These are passed to CreatePrompt. +local DefaultPromptOptions = { + WindowTitle = "Confirm", + MainText = "Is this okay?", + ConfirmationText = "Confirm", + CancelText = "Cancel", + CancelActive = true, + StripeColor = Color3.new(0.01, 0.72, 0.34), + Image = nil, + ImageConsoleVR = nil, + PromptCompletedCallback = nil, +} + +local PromptCallback = nil +local LastPromptOptions = nil + +--[[ Constants ]]-- +-- Images +local BUTTON = 'rbxasset://textures/ui/VR/button.png' +local BUTTON_DOWN = 'rbxasset://textures/ui/VR/buttonSelected.png' + +-- Context Actions +local FREEZE_THUMBSTICK2_ACTION_NAME = "doNothingThumbstick2PromptCreator" +local FREEZE_ABUTTON_ACTION_NAME = "doNothingAButtonPromptCreator" +local CONTROLLER_CANCEL_ACTION_NAME = "CoreScriptPromptCreatorCancel" +local CONTROLLER_SELECT_ACTION_NAME = "CoreScriptPromptCreatorSelect" + +-- GUI constants +local TWEEN_TIME = 0.3 + +local DIALOG_SIZE = UDim2.new(0, 438, 0, 300) +local HIDE_POSITION = UDim2.new(0.5, -219, 0, -300) +local SHOW_POSITION = UDim2.new(0.5, -219, 0.5, -150) + +if IsTenFootInterface or IsVRMode then + DIALOG_SIZE = UDim2.new(1, 0, 0, 690) + HIDE_POSITION = UDim2.new(0, 0, 0, -690) + SHOW_POSITION = UDim2.new(0, 0, 0.5, -345) +elseif IsPhone then + DIALOG_SIZE = UDim2.new(0.9, 0, 0.9, 0) + HIDE_POSITION = UDim2.new(0.05, 0, -0.9, 0) + SHOW_POSITION = UDim2.new(0.05, 0, 0.05, 0) +elseif IsTablet then + DIALOG_SIZE = UDim2.new(0, 400, 0, 305) + HIDE_POSITION = UDim2.new(0.5, -200, 0, -305) + SHOW_POSITION = UDim2.new(0.5, -200, 0.5, -152) +end + +local TITLE_HEIGHT = 52 +local TITLE_TEXTSIZE = 24 + +if IsPhone then + TITLE_HEIGHT = 44 +end + +local BUTTON_TEXTSIZE = 24 + +if IsTenFootInterface or IsVRMode then + BUTTON_TEXTSIZE = 42 +end + +--[[ Gui Creation Functions ]]-- +local function createFrame(name, size, position, bgTransparency, bgColor) + local frame = Instance.new('Frame') + frame.Name = name + frame.Size = size + frame.Position = position or UDim2.new(0, 0, 0, 0) + frame.BackgroundTransparency = bgTransparency + frame.BackgroundColor3 = bgColor or Color3.new() + frame.BorderSizePixel = 0 + frame.ZIndex = 8 + + return frame +end + +local function createTextLabel(name, size, position, font, textSize, text) + local textLabel = Instance.new('TextLabel') + textLabel.Name = name + textLabel.Size = size or UDim2.new(0, 0, 0, 0) + textLabel.Position = position + textLabel.BackgroundTransparency = 1 + textLabel.Font = font + textLabel.TextSize = textSize + textLabel.TextColor3 = Color3.new(1, 1, 1) + textLabel.Text = text + textLabel.ZIndex = 8 + + return textLabel +end + +local function createScrollingTextLabel(name, size, position, font, textSize, text, scrollBarThickness) + local textLabel = createTextLabel(name, size, position, font, textSize, text) + textLabel.TextXAlignment = Enum.TextXAlignment.Left + textLabel.TextYAlignment = Enum.TextYAlignment.Top + textLabel.TextWrapped = true + + local oldTextBounds = TextService:GetTextSize(text, textSize, font, Vector2.new(size.X.Offset, 10000)) + + if oldTextBounds.Y > size.Y.Offset then + local sizeOffset = Vector2.new(size.X.Offset - (scrollBarThickness + 20), 10000) + local textBounds = TextService:GetTextSize(text, textSize, font, sizeOffset) + -- Create scrolling frame. + local parentFrame = Instance.new("Frame") + parentFrame.Name = "ScrollingTextParent" + parentFrame.BackgroundTransparency = 1 + parentFrame.BackgroundColor3 = Color3.new(1, 1, 1) + parentFrame.BorderSizePixel = 0 + parentFrame.Position = position + parentFrame.Size = size - UDim2.new(0, scrollBarThickness*2, 0, 0) + + local scrollingFrame = Instance.new('ScrollingFrame') + scrollingFrame.Selectable = true + scrollingFrame.Name = "ScrollingFrame" + scrollingFrame.BackgroundTransparency = 1 + scrollingFrame.BackgroundColor3 = Color3.new(1, 1, 1) + scrollingFrame.BorderSizePixel = 0 + scrollingFrame.Position = UDim2.new(0, 0, 0, 0) + scrollingFrame.Size = size + scrollingFrame.CanvasSize = UDim2.new(0, sizeOffset.X - scrollBarThickness*2, 0, textBounds.Y) + scrollingFrame.ScrollBarThickness = scrollBarThickness + scrollingFrame.SelectionImageObject = Instance.new("ImageLabel") + scrollingFrame.SelectionImageObject.Name = "EmptySelectionImage" + scrollingFrame.SelectionImageObject.BackgroundTransparency = 1 + scrollingFrame.SelectionImageObject.Image = "" + scrollingFrame.Active = false + + scrollingFrame.SelectionGained:connect(function() + parentFrame.BackgroundTransparency = 0.15 + end) + + scrollingFrame.SelectionLost:connect(function() + parentFrame.BackgroundTransparency = 1 + end) + + textLabel.Position = UDim2.new(0, 10, 0, 0) + textLabel.Size = UDim2.new(0, sizeOffset.X, 0, textBounds.Y) + textLabel.Parent = scrollingFrame + + scrollingFrame.Parent = parentFrame + + return parentFrame + end + + return textLabel +end + + +local function createImageLabel(name, size, position, image, fetchImageFunction) + local imageLabel = Instance.new('ImageLabel') + imageLabel.Name = name + imageLabel.Size = size + imageLabel.BackgroundTransparency = 1 + imageLabel.Position = position + imageLabel.Image = image + + if fetchImageFunction then + fetchImageFunction(imageLabel) + end + + return imageLabel +end + +local function createImageButtonWithText(name, size, position, image, imageDown, text, font) + local imageButton = Instance.new('ImageButton') + imageButton.Name = name + imageButton.Size = size + imageButton.Position = position + imageButton.Image = image + imageButton.BackgroundTransparency = 1 + imageButton.AutoButtonColor = false + imageButton.ZIndex = 8 + imageButton.Modal = true + imageButton.Selectable = true + imageButton.SelectionImageObject = Instance.new("ImageLabel") + imageButton.SelectionImageObject.Name = "EmptySelectionImage" + imageButton.SelectionImageObject.BackgroundTransparency = 1 + imageButton.SelectionImageObject.Image = "" + + local textLabel = createTextLabel(name.."Text", UDim2.new(1, 0, 1, 0), UDim2.new(0, 0, 0, 0), font, BUTTON_TEXTSIZE, text) + textLabel.ZIndex = 9 + textLabel.Parent = imageButton + + imageButton.MouseEnter:connect(function() + imageButton.Image = imageDown + end) + imageButton.MouseLeave:connect(function() + imageButton.Image = image + end) + imageButton.MouseButton1Click:connect(function() + imageButton.Image = image + end) + imageButton.SelectionGained:connect(function() + imageButton.Image = imageDown + end) + imageButton.SelectionLost:connect(function() + imageButton.Image = image + end) + + return imageButton +end + +--[[ Begin Gui Creation ]]-- +local PromptDialog = createFrame("PromptDialog", DIALOG_SIZE, HIDE_POSITION, 1, nil) +PromptDialog.Visible = false +PromptDialog.Parent = RobloxGui +PromptDialog.Active = true + +local ContainerFrame = createFrame("ContainerFrame", UDim2.new(1, 0, 1, 0), nil, 0.36, Color3.new(0, 0, 0)) +ContainerFrame.ZIndex = 8 +ContainerFrame.Parent = PromptDialog + +function AddDefaultsToPromptOptions(promptOptions, defaultPromptOptions) + for key, value in pairs(defaultPromptOptions) do + if promptOptions[key] == nil then + promptOptions[key] = value + end + end +end + +--- Creates a prompt for VR or console. +function CreatePromptVRorConsole(promptOptions) + local xOffset = 90 + + if promptOptions.ImageConsoleVR then + local image = createImageLabel("Image", UDim2.new(0, 600, 0, 600), UDim2.new(0, 100, 0, 45), promptOptions.ImageConsoleVR, promptOptions.FetchImageFunctionConsoleVR) + image.ZIndex = 9 + image.Parent = ContainerFrame + + xOffset = 800 + end + + local windowTitle = createTextLabel("WindowTitle", UDim2.new(0, 800, 0, 60), + UDim2.new(0, xOffset, 0, 60), Enum.Font.SourceSansBold, 48, + promptOptions.WindowTitle) + windowTitle.TextXAlignment = Enum.TextXAlignment.Left + windowTitle.TextYAlignment = Enum.TextYAlignment.Center + windowTitle.Parent = ContainerFrame + windowTitle.ZIndex = 9 + + local colorStripe = createFrame("ColorStripe", UDim2.new(0, 832, 0, 4), UDim2.new(0, xOffset, 0, 120), 0, promptOptions.StripeColor) + colorStripe.ZIndex = 9 + colorStripe.Parent = ContainerFrame + + local mainText = createScrollingTextLabel("MainText", UDim2.new(0, 800, 0, 450), UDim2.new(0, xOffset, 0, 152), + Enum.Font.SourceSansBold, 44, promptOptions.MainText, 16) + mainText.Parent = ContainerFrame + + local buttonSliceCenter = Rect.new(8, 8, 64 - 8, 64 - 8) + local buttonScaleType = Enum.ScaleType.Slice + + local confirmButton = createImageButtonWithText("ConfirmButton", UDim2.new(0, 320, 0, 80), UDim2.new(0, xOffset, 1, -125), BUTTON, + BUTTON_DOWN, promptOptions.ConfirmationText, Enum.Font.SourceSans) + + confirmButton.Parent = ContainerFrame + confirmButton.ScaleType = buttonScaleType + confirmButton.SliceCenter = buttonSliceCenter + + confirmButton.MouseButton1Click:connect(function() + OnPromptEnded(true) + end) + + if promptOptions.CancelActive then + local cancelButton = createImageButtonWithText("CancelButton", UDim2.new(0, 320, 0, 80), UDim2.new(0, xOffset + 340, 1, -125), BUTTON, BUTTON_DOWN, + promptOptions.CancelText, Enum.Font.SourceSans) + + cancelButton.Parent = ContainerFrame + cancelButton.ScaleType = buttonScaleType + cancelButton.SliceCenter = buttonSliceCenter + + cancelButton.MouseButton1Click:connect(function() + OnPromptEnded(false) + end) + end +end + +function CreatePromptPCorTablet(promptOptions) + local windowTitle = createTextLabel("WindowTitle", UDim2.new(1, 0, 0, TITLE_HEIGHT), + UDim2.new(0, 0, 0, 0), Enum.Font.SourceSansBold, TITLE_TEXTSIZE, + promptOptions.WindowTitle) + windowTitle.Parent = ContainerFrame + windowTitle.ZIndex = 9 + + local colorStripe = createFrame("ColorStripe", UDim2.new(1, 0, 0, 2), nil, 0, promptOptions.StripeColor) + colorStripe.Position = UDim2.new(0, 0, 0, TITLE_HEIGHT) + colorStripe.ZIndex = 9 + colorStripe.Parent = ContainerFrame + + local mainText = nil + local image = nil + + if promptOptions.Image then + image = createImageLabel("Image", UDim2.new(0, 150, 0, 150), UDim2.new(0, 15, 0, TITLE_HEIGHT + 17), promptOptions.Image, promptOptions.FetchImageFunction) + image.ZIndex = 9 + image.Parent = ContainerFrame + + if IsTablet then + mainText = createScrollingTextLabel("MainText", UDim2.new(0, 195, 0, 150), UDim2.new(0, 185, 0, TITLE_HEIGHT + 17), + Enum.Font.SourceSansBold, 22, promptOptions.MainText, 8) + else + mainText = createScrollingTextLabel("MainText", UDim2.new(0, 233, 0, 150), UDim2.new(0, 185, 0, TITLE_HEIGHT + 17), + Enum.Font.SourceSansBold, 22, promptOptions.MainText, 8) + end + else + if IsTablet then + mainText = createScrollingTextLabel("MainText", UDim2.new(0, 360, 0, 150), UDim2.new(0, 20, 0, TITLE_HEIGHT + 17), + Enum.Font.SourceSansBold, 22, promptOptions.MainText, 8) + else + mainText = createScrollingTextLabel("MainText", UDim2.new(0, 398, 0, 150), UDim2.new(0, 20, 0, TITLE_HEIGHT + 17), + Enum.Font.SourceSansBold, 22, promptOptions.MainText, 8) + end + end + + mainText.Parent = ContainerFrame + + local buttonSliceCenter = Rect.new(8, 8, 64 - 8, 64 - 8) + local buttonScaleType = Enum.ScaleType.Slice + + local confirmButton = nil + + if IsTablet then + if promptOptions.CancelActive then + confirmButton = createImageButtonWithText("ConfirmButton", + UDim2.new(0, 128, 0, 44), UDim2.new(0.5, -138, 1, -59), BUTTON, BUTTON_DOWN, promptOptions.ConfirmationText, Enum.Font.SourceSansBold) + else + confirmButton = createImageButtonWithText("ConfirmButton", UDim2.new(0, 128, 0, 44), UDim2.new(0.5, -64, 1, -59), BUTTON, + BUTTON_DOWN, promptOptions.ConfirmationText, Enum.Font.SourceSans) + end + else + if promptOptions.CancelActive then + confirmButton = createImageButtonWithText("ConfirmButton", + UDim2.new(0, 128, 0, 38), UDim2.new(0.5, -138, 1, -53), BUTTON, BUTTON_DOWN, promptOptions.ConfirmationText, Enum.Font.SourceSansBold) + else + confirmButton = createImageButtonWithText("ConfirmButton", UDim2.new(0, 128, 0, 38), UDim2.new(0.5, -64, 1, -53), BUTTON, + BUTTON_DOWN, promptOptions.ConfirmationText, Enum.Font.SourceSans) + end + end + + confirmButton.Parent = ContainerFrame + confirmButton.ScaleType = buttonScaleType + confirmButton.SliceCenter = buttonSliceCenter + + confirmButton.MouseButton1Click:connect(function() + OnPromptEnded(true) + end) + + if promptOptions.CancelActive then + local cancelButton = nil + if IsTablet then + cancelButton = createImageButtonWithText("CancelButton", UDim2.new(0, 128, 0, 44), UDim2.new(0.5, 10, 1, -59), BUTTON, BUTTON_DOWN, + promptOptions.CancelText, Enum.Font.SourceSans) + else + cancelButton = createImageButtonWithText("CancelButton", UDim2.new(0, 128, 0, 38), UDim2.new(0.5, 10, 1, -53), BUTTON, BUTTON_DOWN, + promptOptions.CancelText, Enum.Font.SourceSans) + end + + cancelButton.Parent = ContainerFrame + cancelButton.ScaleType = buttonScaleType + cancelButton.SliceCenter = buttonSliceCenter + + cancelButton.MouseButton1Click:connect(function() + OnPromptEnded(false) + end) + + cancelButton.NextSelectionLeft = confirmButton + confirmButton.NextSelectionRight = cancelButton + end +end + +function CreatePromptPhone(promptOptions) + local windowTitle = createTextLabel("WindowTitle", UDim2.new(1, 0, 0, TITLE_HEIGHT), + UDim2.new(0, 0, 0, 0), Enum.Font.SourceSansBold, TITLE_TEXTSIZE, + promptOptions.WindowTitle) + windowTitle.Parent = ContainerFrame + windowTitle.ZIndex = 9 + + local colorStripe = createFrame("ColorStripe", UDim2.new(1, 0, 0, 2), nil, 0, promptOptions.StripeColor) + colorStripe.Position = UDim2.new(0, 0, 0, TITLE_HEIGHT) + colorStripe.ZIndex = 9 + colorStripe.Parent = ContainerFrame + + local mainText = nil + local image = nil + + if promptOptions.Image then + image = createImageLabel("Image", UDim2.new(0, 120, 0, 120), UDim2.new(0, 15, 0, TITLE_HEIGHT + 17), promptOptions.Image, promptOptions.FetchImageFunction) + image.ZIndex = 9 + image.Parent = ContainerFrame + + mainText = createScrollingTextLabel("MainText", UDim2.new(0, ContainerFrame.AbsoluteSize.X - 175, 0, 120), UDim2.new(0, 155, 0, TITLE_HEIGHT + 17), + Enum.Font.SourceSansBold, 20, promptOptions.MainText, 8) + else + mainText = createScrollingTextLabel("MainText", UDim2.new(1, ContainerFrame.AbsoluteSize.X - 40, 0, 120), UDim2.new(0, 20, 0, TITLE_HEIGHT + 17), + Enum.Font.SourceSansBold, 20, promptOptions.MainText, 8) + end + + mainText.Parent = ContainerFrame + + local buttonSliceCenter = Rect.new(8, 8, 64 - 8, 64 - 8) + local buttonScaleType = Enum.ScaleType.Slice + + local confirmButton = nil + + if promptOptions.CancelActive then + confirmButton = createImageButtonWithText("ConfirmButton", + UDim2.new(0, 128, 0, 44), UDim2.new(0.5, -138, 1, -59), BUTTON, BUTTON_DOWN, promptOptions.ConfirmationText, Enum.Font.SourceSansBold) + else + confirmButton = createImageButtonWithText("ConfirmButton", UDim2.new(0, 128, 0, 44), UDim2.new(0.5, -64, 1, -59), BUTTON, + BUTTON_DOWN, promptOptions.ConfirmationText, Enum.Font.SourceSans) + end + + confirmButton.Parent = ContainerFrame + confirmButton.ScaleType = buttonScaleType + confirmButton.SliceCenter = buttonSliceCenter + + confirmButton.MouseButton1Click:connect(function() + OnPromptEnded(true) + end) + + if promptOptions.CancelActive then + local cancelButton = createImageButtonWithText("CancelButton", UDim2.new(0, 128, 0, 44), UDim2.new(0.5, 10, 1, -59), BUTTON, BUTTON_DOWN, + promptOptions.CancelText, Enum.Font.SourceSans) + cancelButton.Parent = ContainerFrame + cancelButton.ScaleType = buttonScaleType + cancelButton.SliceCenter = buttonSliceCenter + + cancelButton.MouseButton1Click:connect(function() + OnPromptEnded(false) + end) + end +end + +function CreatePromptFromOptions(promptOptions) + ContainerFrame:ClearAllChildren() + + if IsVRMode or IsTenFootInterface then + CreatePromptVRorConsole(promptOptions) + elseif IsPhone then + CreatePromptPhone(promptOptions) + else + CreatePromptPCorTablet(promptOptions) + end +end + +function SetSelectedObject() + local cancelButton = ContainerFrame:FindFirstChild("CancelButton") + if cancelButton then + GuiService.SelectedCoreObject = cancelButton + else + local confirmButton = ContainerFrame:FindFirstChild("ConfirmButton") + if confirmButton then + GuiService.SelectedCoreObject = confirmButton + end + end +end + +function OnTweenInFinished() + if LastInputWasGamepad or IsTenFootInterface then + SetSelectedObject() + end +end + +function ShowPrompt() + PromptDialog.Visible = true + if IsTenFootInterface then + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide + end + if IsVRMode then + PromptDialog.Position = SHOW_POSITION + PromptDialogVR:SetContent(PromptDialog) + PromptDialogVR:Show(true) + DisableControllerMovement() + else + PromptDialog:TweenPosition(SHOW_POSITION, Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, TWEEN_TIME, true, OnTweenInFinished) + DisableControllerMovement() + EnableControllerInput() + end +end + +function HidePrompt() + local function onClosed() + PromptDialog.Visible = false + IsCurrentlyPrompting = false + GuiService.CoreGuiNavigationEnabled = WasCoreGuiNavigationEnabled + GuiService.GuiNavigationEnabled = WasGuiNavigationEnabled + GuiService.AutoSelectGuiEnabled = WasAutoSelectGuiEnabled + GuiService.SelectedCoreObject = nil + if IsTenFootInterface then + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None + end + end + if IsVRMode then + PromptDialog.Position = HIDE_POSITION + PromptDialogVR:Close() + onClosed() + else + PromptDialog:TweenPosition(HIDE_POSITION, Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, TWEEN_TIME, true, onClosed) + end +end + +function DoCreatePrompt(promptOptions) + PromptCallback = promptOptions.PromptCompletedCallback + AddDefaultsToPromptOptions(promptOptions, DefaultPromptOptions) + LastPromptOptions = promptOptions + CreatePromptFromOptions(promptOptions) + ShowPrompt() +end + +function OnPromptEnded(okayButtonPressed) + if PromptCallback then + if LastPromptOptions.CancelActive then + spawn(function() PromptCallback(okayButtonPressed) end) + else + spawn(function() PromptCallback(true) end) + end + end + HidePrompt() + EnableControllerMovement() + DisableControllerInput() +end + +--[[ Controller input handling ]] + +function NoOpFunc() end + +function EnableControllerMovement() + ContextActionService:UnbindCoreAction(FREEZE_THUMBSTICK2_ACTION_NAME) + ContextActionService:UnbindCoreAction(FREEZE_ABUTTON_ACTION_NAME) +end + +function DisableControllerMovement() + ContextActionService:BindCoreAction(FREEZE_THUMBSTICK2_ACTION_NAME, NoOpFunc, false, Enum.KeyCode.Thumbstick2) + ContextActionService:BindCoreAction(FREEZE_ABUTTON_ACTION_NAME, NoOpFunc, false, Enum.KeyCode.ButtonA) +end + +function EnableControllerInput() + --cancel the prompt when the user pressed the b button. + ContextActionService:BindCoreAction( + CONTROLLER_CANCEL_ACTION_NAME, + function(actionName, inputState, inputObject) + if inputState ~= Enum.UserInputState.Begin then return end + + if LastPromptOptions.CancelActive then + OnPromptEnded(false) + end + end, + false, + Enum.KeyCode.ButtonB + ) + + ContextActionService:BindCoreAction( + CONTROLLER_SELECT_ACTION_NAME, + function(actionName, inputState, inputObject) + if inputState ~= Enum.UserInputState.Begin then return end + + if GuiService.SelectedCoreObject == nil then + SetSelectedObject() + end + end, + false, + Enum.KeyCode.ButtonSelect + ) +end + +function DisableControllerInput() + ContextActionService:UnbindCoreAction(CONTROLLER_CANCEL_ACTION_NAME) + ContextActionService:UnbindCoreAction(CONTROLLER_SELECT_ACTION_NAME) +end + +function valueInTable(val, tab) + for _, v in pairs(tab) do + if v == val then + return true + end + end + return false +end + +function OnInputChanged(inputObject) + local inputType = inputObject.UserInputType + local inputTypes = Enum.UserInputType + if not IsVRMode and valueInTable(inputType, {inputTypes.Gamepad1, inputTypes.Gamepad2, inputTypes.Gamepad3, inputTypes.Gamepad4}) then + if inputObject.KeyCode == Enum.KeyCode.Thumbstick1 or inputObject.KeyCode == Enum.KeyCode.Thumbstick2 then + if math.abs(inputObject.Position.X) > 0.1 or math.abs(inputObject.Position.Z) > 0.1 or math.abs(inputObject.Position.Y) > 0.1 then + LastInputWasGamepad = true + end + else + LastInputWasGamepad = true + end + else + LastInputWasGamepad = false + end +end +UserInputService.InputChanged:connect(OnInputChanged) +UserInputService.InputBegan:connect(OnInputChanged) + +--[[ VR changed handling ]] +function OnVREnabled(vrEnabled) + if vrEnabled then + if not PromptDialogVR then + PromptDialogVR = VRDialogModule.new() + end + PromptDialogVR:SetContent(PromptDialog) + IsVRMode = true + else + IsVRMode = false + if PromptDialogVR then + PromptDialogVR:SetContent(nil) + end + PromptDialog.Parent = RobloxGui + end +end + +spawn(function() + OnVREnabled(UserInputService.VREnabled) +end) + +UserInputService.Changed:connect(function(prop) + if prop == "VREnabled" then + OnVREnabled(UserInputService.VREnabled) + end +end) + +GuiService.Changed:connect(function(prop) + if IsCurrentlyPrompting then + if prop == "CoreGuiNavigationEnabled" then + if GuiService.CoreGuiNavigationEnabled ~= true then + WasCoreGuiNavigationEnabled = GuiService.CoreGuiNavigationEnabled + GuiService.CoreGuiNavigationEnabled = true + end + elseif prop == "GuiNavigationEnabled" then + if GuiService.GuiNavigationEnabled ~= false then + WasGuiNavigationEnabled = GuiService.GuiNavigationEnabled + GuiService.GuiNavigationEnabled = false + end + elseif prop == "AutoSelectGuiEnabled" then + if GuiService.AutoSelectGuiEnabled ~= false then + WasAutoSelectGuiEnabled = GuiService.AutoSelectGuiEnabled + GuiService.AutoSelectGuiEnabled = false + end + end + end +end) + +function SetupGamepadSelection() + WasCoreGuiNavigationEnabled = GuiService.CoreGuiNavigationEnabled + WasGuiNavigationEnabled = GuiService.GuiNavigationEnabled + WasAutoSelectGuiEnabled = GuiService.AutoSelectGuiEnabled + + GuiService.SelectedCoreObject = nil + GuiService.CoreGuiNavigationEnabled = true + GuiService.GuiNavigationEnabled = false + GuiService.AutoSelectGuiEnabled = false +end + +-- [[ Public Methods ]] +function moduleApiTable:CreatePrompt(promptOptions) + if IsCurrentlyPrompting then + return false + end + IsCurrentlyPrompting = true + SetupGamepadSelection() + DoCreatePrompt(promptOptions) + return true +end + +function moduleApiTable:IsCurrentlyPrompting() + return IsCurrentlyPrompting +end + +return moduleApiTable diff --git a/Client2018/content/scripts/CoreScripts/Modules/RobloxTranslator.lua b/Client2018/content/scripts/CoreScripts/Modules/RobloxTranslator.lua new file mode 100644 index 0000000..42160e2 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/RobloxTranslator.lua @@ -0,0 +1,25 @@ +local LocalizationService = game:GetService("LocalizationService") +local CoreGui = game:GetService('CoreGui') +local Players = game:GetService("Players") + +-- Waiting for the player ensures that the RobloxLocaleId has been set. +if Players.LocalPlayer == nil then + Players:GetPropertyChangedSignal("LocalPlayer"):Wait() +end + +local coreScriptTableTranslator +local function getTranslator() + if coreScriptTableTranslator == nil then + coreScriptTableTranslator = CoreGui.CoreScriptLocalization:GetTranslator( + LocalizationService.RobloxLocaleId) + end + return coreScriptTableTranslator +end + +local RobloxTranslator = {} + +function RobloxTranslator:FormatByKey(key, args) + return getTranslator():FormatByKey(key, args) +end + +return RobloxTranslator diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/BubbleChat/BubbleChat.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/BubbleChat/BubbleChat.lua new file mode 100644 index 0000000..818391f --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/BubbleChat/BubbleChat.lua @@ -0,0 +1,711 @@ +--[[ + // FileName: BubbleChat.lua + // Written by: jeditkacheff, TheGamer101 + // Description: Code for rendering bubble chat +]] + +--[[ SERVICES ]] +local PlayersService = game:GetService('Players') +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local ChatService = game:GetService("Chat") +local TextService = game:GetService("TextService") +--[[ END OF SERVICES ]] + +local LocalPlayer = PlayersService.LocalPlayer +while LocalPlayer == nil do + PlayersService.ChildAdded:wait() + LocalPlayer = PlayersService.LocalPlayer +end + +local PlayerGui = LocalPlayer:WaitForChild("PlayerGui") + +local okShouldClipInGameChat, valueShouldClipInGameChat = pcall(function() return UserSettings():IsUserFeatureEnabled("UserShouldClipInGameChat") end) +local shouldClipInGameChat = okShouldClipInGameChat and valueShouldClipInGameChat + +--[[ SCRIPT VARIABLES ]] +local CHAT_BUBBLE_FONT = Enum.Font.SourceSans +local CHAT_BUBBLE_FONT_SIZE = Enum.FontSize.Size24 -- if you change CHAT_BUBBLE_FONT_SIZE_INT please change this to match +local CHAT_BUBBLE_FONT_SIZE_INT = 24 -- if you change CHAT_BUBBLE_FONT_SIZE please change this to match +local CHAT_BUBBLE_LINE_HEIGHT = CHAT_BUBBLE_FONT_SIZE_INT + 10 +local CHAT_BUBBLE_TAIL_HEIGHT = 14 +local CHAT_BUBBLE_WIDTH_PADDING = 30 +local CHAT_BUBBLE_FADE_SPEED = 1.5 + +local BILLBOARD_MAX_WIDTH = 400 +local BILLBOARD_MAX_HEIGHT = 250 --This limits the number of bubble chats that you see above characters + +local ELIPSES = "..." +local MaxChatMessageLength = 128 -- max chat message length, including null terminator and elipses. +local MaxChatMessageLengthExclusive = MaxChatMessageLength - string.len(ELIPSES) - 1 + +local NEAR_BUBBLE_DISTANCE = 65 --previously 45 +local MAX_BUBBLE_DISTANCE = 100 --previously 80 + +--[[ END OF SCRIPT VARIABLES ]] + + +-- [[ SCRIPT ENUMS ]] +local BubbleColor = { WHITE = "dub", + BLUE = "blu", + GREEN = "gre", + RED = "red" } + +--[[ END OF SCRIPT ENUMS ]] + +-- This screenGui exists so that the billboardGui is not deleted when the PlayerGui is reset. +local BubbleChatScreenGui = Instance.new("ScreenGui") +BubbleChatScreenGui.Name = "BubbleChat" +BubbleChatScreenGui.ResetOnSpawn = false +BubbleChatScreenGui.Parent = PlayerGui + +--[[ FUNCTIONS ]] + +local function lerpLength(msg, min, max) + return min + (max-min) * math.min(string.len(msg)/75.0, 1.0) +end + +local function createFifo() + local this = {} + this.data = {} + + local emptyEvent = Instance.new("BindableEvent") + this.Emptied = emptyEvent.Event + + function this:Size() + return #this.data + end + + function this:Empty() + return this:Size() <= 0 + end + + function this:PopFront() + table.remove(this.data, 1) + if this:Empty() then emptyEvent:Fire() end + end + + function this:Front() + return this.data[1] + end + + function this:Get(index) + return this.data[index] + end + + function this:PushBack(value) + table.insert(this.data, value) + end + + function this:GetData() + return this.data + end + + return this +end + +local function createCharacterChats() + local this = {} + + this.Fifo = createFifo() + this.BillboardGui = nil + + return this +end + +local function createMap() + local this = {} + this.data = {} + local count = 0 + + function this:Size() + return count + end + + function this:Erase(key) + if this.data[key] then count = count - 1 end + this.data[key] = nil + end + + function this:Set(key, value) + this.data[key] = value + if value then count = count + 1 end + end + + function this:Get(key) + if not key then return end + if not this.data[key] then + this.data[key] = createCharacterChats() + local emptiedCon = nil + emptiedCon = this.data[key].Fifo.Emptied:connect(function() + emptiedCon:disconnect() + this:Erase(key) + end) + end + return this.data[key] + end + + function this:GetData() + return this.data + end + + return this +end + +local function createChatLine(message, bubbleColor, isLocalPlayer) + local this = {} + + function this:ComputeBubbleLifetime(msg, isSelf) + if isSelf then + return lerpLength(msg,8,15) + else + return lerpLength(msg,12,20) + end + end + + this.Origin = nil + this.RenderBubble = nil + this.Message = message + this.BubbleDieDelay = this:ComputeBubbleLifetime(message, isLocalPlayer) + this.BubbleColor = bubbleColor + this.IsLocalPlayer = isLocalPlayer + + return this +end + +local function createPlayerChatLine(player, message, isLocalPlayer) + local this = createChatLine(message, BubbleColor.WHITE, isLocalPlayer) + + if player then + this.User = player.Name + this.Origin = player.Character + end + + return this +end + +local function createGameChatLine(origin, message, isLocalPlayer, bubbleColor) + local this = createChatLine(message, bubbleColor, isLocalPlayer) + this.Origin = origin + + return this +end + +function createChatBubbleMain(filePrefix, sliceRect) + local chatBubbleMain = Instance.new("ImageLabel") + chatBubbleMain.Name = "ChatBubble" + chatBubbleMain.ScaleType = Enum.ScaleType.Slice + chatBubbleMain.SliceCenter = sliceRect + chatBubbleMain.Image = "rbxasset://textures/" .. tostring(filePrefix) .. ".png" + chatBubbleMain.BackgroundTransparency = 1 + chatBubbleMain.BorderSizePixel = 0 + chatBubbleMain.Size = UDim2.new(1.0, 0, 1.0, 0) + chatBubbleMain.Position = UDim2.new(0,0,0,0) + + return chatBubbleMain +end + +function createChatBubbleTail(position, size) + local chatBubbleTail = Instance.new("ImageLabel") + chatBubbleTail.Name = "ChatBubbleTail" + chatBubbleTail.Image = "rbxasset://textures/ui/dialog_tail.png" + chatBubbleTail.BackgroundTransparency = 1 + chatBubbleTail.BorderSizePixel = 0 + chatBubbleTail.Position = position + chatBubbleTail.Size = size + + return chatBubbleTail +end + +function createChatBubbleWithTail(filePrefix, position, size, sliceRect) + local chatBubbleMain = createChatBubbleMain(filePrefix, sliceRect) + + local chatBubbleTail = createChatBubbleTail(position, size) + chatBubbleTail.Parent = chatBubbleMain + + return chatBubbleMain +end + +function createScaledChatBubbleWithTail(filePrefix, frameScaleSize, position, sliceRect) + local chatBubbleMain = createChatBubbleMain(filePrefix, sliceRect) + + local frame = Instance.new("Frame") + frame.Name = "ChatBubbleTailFrame" + frame.BackgroundTransparency = 1 + frame.SizeConstraint = Enum.SizeConstraint.RelativeXX + frame.Position = UDim2.new(0.5, 0, 1, 0) + frame.Size = UDim2.new(frameScaleSize, 0, frameScaleSize, 0) + frame.Parent = chatBubbleMain + + local chatBubbleTail = createChatBubbleTail(position, UDim2.new(1,0,0.5,0)) + chatBubbleTail.Parent = frame + + return chatBubbleMain +end + +function createChatImposter(filePrefix, dotDotDot, yOffset) + local result = Instance.new("ImageLabel") + result.Name = "DialogPlaceholder" + result.Image = "rbxasset://textures/" .. tostring(filePrefix) .. ".png" + result.BackgroundTransparency = 1 + result.BorderSizePixel = 0 + result.Position = UDim2.new(0, 0, -1.25, 0) + result.Size = UDim2.new(1, 0, 1, 0) + + local image = Instance.new("ImageLabel") + image.Name = "DotDotDot" + image.Image = "rbxasset://textures/" .. tostring(dotDotDot) .. ".png" + image.BackgroundTransparency = 1 + image.BorderSizePixel = 0 + image.Position = UDim2.new(0.001, 0, yOffset, 0) + image.Size = UDim2.new(1, 0, 0.7, 0) + image.Parent = result + + return result +end + + +local this = {} +this.ChatBubble = {} +this.ChatBubbleWithTail = {} +this.ScalingChatBubbleWithTail = {} +this.CharacterSortedMsg = createMap() + +-- init chat bubble tables +local function initChatBubbleType(chatBubbleType, fileName, imposterFileName, isInset, sliceRect) + this.ChatBubble[chatBubbleType] = createChatBubbleMain(fileName, sliceRect) + this.ChatBubbleWithTail[chatBubbleType] = createChatBubbleWithTail(fileName, UDim2.new(0.5, -CHAT_BUBBLE_TAIL_HEIGHT, 1, isInset and -1 or 0), UDim2.new(0, 30, 0, CHAT_BUBBLE_TAIL_HEIGHT), sliceRect) + this.ScalingChatBubbleWithTail[chatBubbleType] = createScaledChatBubbleWithTail(fileName, 0.5, UDim2.new(-0.5, 0, 0, isInset and -1 or 0), sliceRect) +end + +initChatBubbleType(BubbleColor.WHITE, "ui/dialog_white", "ui/chatBubble_white_notify_bkg", false, Rect.new(5,5,15,15)) +initChatBubbleType(BubbleColor.BLUE, "ui/dialog_blue", "ui/chatBubble_blue_notify_bkg", true, Rect.new(7,7,33,33)) +initChatBubbleType(BubbleColor.RED, "ui/dialog_red", "ui/chatBubble_red_notify_bkg", true, Rect.new(7,7,33,33)) +initChatBubbleType(BubbleColor.GREEN, "ui/dialog_green", "ui/chatBubble_green_notify_bkg", true, Rect.new(7,7,33,33)) + +function this:SanitizeChatLine(msg) + if string.len(msg) > MaxChatMessageLengthExclusive then + return string.sub(msg, 1, MaxChatMessageLengthExclusive + string.len(ELIPSES)) + else + return msg + end +end + +local function createBillboardInstance(adornee) + local billboardGui = Instance.new("BillboardGui") + billboardGui.Adornee = adornee + billboardGui.Size = UDim2.new(0,BILLBOARD_MAX_WIDTH,0,BILLBOARD_MAX_HEIGHT) + billboardGui.StudsOffset = Vector3.new(0, 1.5, 2) + billboardGui.Parent = BubbleChatScreenGui + + local billboardFrame = Instance.new("Frame") + billboardFrame.Name = "BillboardFrame" + billboardFrame.Size = UDim2.new(1,0,1,0) + billboardFrame.Position = UDim2.new(0,0,-0.5,0) + billboardFrame.BackgroundTransparency = 1 + billboardFrame.Parent = billboardGui + + local billboardChildRemovedCon = nil + billboardChildRemovedCon = billboardFrame.ChildRemoved:connect(function() + if #billboardFrame:GetChildren() <= 1 then + billboardChildRemovedCon:disconnect() + billboardGui:Destroy() + end + end) + + this:CreateSmallTalkBubble(BubbleColor.WHITE).Parent = billboardFrame + + return billboardGui +end + +function this:CreateBillboardGuiHelper(instance, onlyCharacter) + if instance and not this.CharacterSortedMsg:Get(instance)["BillboardGui"] then + if not onlyCharacter then + if instance:IsA("BasePart") then + -- Create a new billboardGui object attached to this player + local billboardGui = createBillboardInstance(instance) + this.CharacterSortedMsg:Get(instance)["BillboardGui"] = billboardGui + return + end + end + + if instance:IsA("Model") then + local head = instance:FindFirstChild("Head") + if head and head:IsA("BasePart") then + -- Create a new billboardGui object attached to this player + local billboardGui = createBillboardInstance(head) + this.CharacterSortedMsg:Get(instance)["BillboardGui"] = billboardGui + end + end + end +end + +local function distanceToBubbleOrigin(origin) + if not origin then return 100000 end + + return (origin.Position - game.Workspace.CurrentCamera.CoordinateFrame.p).magnitude +end + +local function isPartOfLocalPlayer(adornee) + if adornee and PlayersService.LocalPlayer.Character then + return adornee:IsDescendantOf(PlayersService.LocalPlayer.Character) + end +end + +function this:SetBillboardLODNear(billboardGui) + local isLocalPlayer = isPartOfLocalPlayer(billboardGui.Adornee) + billboardGui.Size = UDim2.new(0, BILLBOARD_MAX_WIDTH, 0, BILLBOARD_MAX_HEIGHT) + billboardGui.StudsOffset = Vector3.new(0, isLocalPlayer and 1.5 or 2.5, isLocalPlayer and 2 or 0.1) + billboardGui.Enabled = true + local billChildren = billboardGui.BillboardFrame:GetChildren() + for i = 1, #billChildren do + billChildren[i].Visible = true + end + billboardGui.BillboardFrame.SmallTalkBubble.Visible = false +end + +function this:SetBillboardLODDistant(billboardGui) + local isLocalPlayer = isPartOfLocalPlayer(billboardGui.Adornee) + billboardGui.Size = UDim2.new(4,0,3,0) + billboardGui.StudsOffset = Vector3.new(0, 3, isLocalPlayer and 2 or 0.1) + billboardGui.Enabled = true + local billChildren = billboardGui.BillboardFrame:GetChildren() + for i = 1, #billChildren do + billChildren[i].Visible = false + end + billboardGui.BillboardFrame.SmallTalkBubble.Visible = true +end + +function this:SetBillboardLODVeryFar(billboardGui) + billboardGui.Enabled = false +end + +function this:SetBillboardGuiLOD(billboardGui, origin) + if not origin then return end + + if origin:IsA("Model") then + local head = origin:FindFirstChild("Head") + if not head then origin = origin.PrimaryPart + else origin = head end + end + + local bubbleDistance = distanceToBubbleOrigin(origin) + + if bubbleDistance < NEAR_BUBBLE_DISTANCE then + this:SetBillboardLODNear(billboardGui) + elseif bubbleDistance >= NEAR_BUBBLE_DISTANCE and bubbleDistance < MAX_BUBBLE_DISTANCE then + this:SetBillboardLODDistant(billboardGui) + else + this:SetBillboardLODVeryFar(billboardGui) + end +end + +function this:CameraCFrameChanged() + for index, value in pairs(this.CharacterSortedMsg:GetData()) do + local playerBillboardGui = value["BillboardGui"] + if playerBillboardGui then this:SetBillboardGuiLOD(playerBillboardGui, index) end + end +end + +function this:CreateBubbleText(message) + local bubbleText = Instance.new("TextLabel") + bubbleText.Name = "BubbleText" + bubbleText.BackgroundTransparency = 1 + bubbleText.Position = UDim2.new(0,CHAT_BUBBLE_WIDTH_PADDING/2,0,0) + bubbleText.Size = UDim2.new(1,-CHAT_BUBBLE_WIDTH_PADDING,1,0) + bubbleText.Font = CHAT_BUBBLE_FONT + if shouldClipInGameChat then + bubbleText.ClipsDescendants = true + end + bubbleText.TextWrapped = true + bubbleText.FontSize = CHAT_BUBBLE_FONT_SIZE + bubbleText.Text = message + bubbleText.Visible = false + + return bubbleText +end + +function this:CreateSmallTalkBubble(chatBubbleType) + local smallTalkBubble = this.ScalingChatBubbleWithTail[chatBubbleType]:Clone() + smallTalkBubble.Name = "SmallTalkBubble" + smallTalkBubble.AnchorPoint = Vector2.new(0, 0.5) + smallTalkBubble.Position = UDim2.new(0,0,0.5,0) + smallTalkBubble.Visible = false + local text = this:CreateBubbleText("...") + text.TextScaled = true + text.TextWrapped = false + text.Visible = true + text.Parent = smallTalkBubble + + return smallTalkBubble +end + +function this:UpdateChatLinesForOrigin(origin, currentBubbleYPos) + local bubbleQueue = this.CharacterSortedMsg:Get(origin).Fifo + local bubbleQueueSize = bubbleQueue:Size() + local bubbleQueueData = bubbleQueue:GetData() + if #bubbleQueueData <= 1 then return end + + for index = (#bubbleQueueData - 1), 1, -1 do + local value = bubbleQueueData[index] + local bubble = value.RenderBubble + if not bubble then return end + local bubblePos = bubbleQueueSize - index + 1 + + if bubblePos > 1 then + local tail = bubble:FindFirstChild("ChatBubbleTail") + if tail then tail:Destroy() end + local bubbleText = bubble:FindFirstChild("BubbleText") + if bubbleText then bubbleText.TextTransparency = 0.5 end + end + + local udimValue = UDim2.new( bubble.Position.X.Scale, bubble.Position.X.Offset, + 1, currentBubbleYPos - bubble.Size.Y.Offset - CHAT_BUBBLE_TAIL_HEIGHT ) + bubble:TweenPosition(udimValue, Enum.EasingDirection.Out, Enum.EasingStyle.Bounce, 0.1, true) + currentBubbleYPos = currentBubbleYPos - bubble.Size.Y.Offset - CHAT_BUBBLE_TAIL_HEIGHT + end +end + +function this:DestroyBubble(bubbleQueue, bubbleToDestroy) + if not bubbleQueue then return end + if bubbleQueue:Empty() then return end + + local bubble = bubbleQueue:Front().RenderBubble + if not bubble then + bubbleQueue:PopFront() + return + end + + spawn(function() + while bubbleQueue:Front().RenderBubble ~= bubbleToDestroy do + wait() + end + + bubble = bubbleQueue:Front().RenderBubble + + local timeBetween = 0 + local bubbleText = bubble:FindFirstChild("BubbleText") + local bubbleTail = bubble:FindFirstChild("ChatBubbleTail") + + while bubble and bubble.ImageTransparency < 1 do + timeBetween = wait() + if bubble then + local fadeAmount = timeBetween * CHAT_BUBBLE_FADE_SPEED + bubble.ImageTransparency = bubble.ImageTransparency + fadeAmount + if bubbleText then bubbleText.TextTransparency = bubbleText.TextTransparency + fadeAmount end + if bubbleTail then bubbleTail.ImageTransparency = bubbleTail.ImageTransparency + fadeAmount end + end + end + + if bubble then + bubble:Destroy() + bubbleQueue:PopFront() + end + end) +end + +function this:CreateChatLineRender(instance, line, onlyCharacter, fifo) + if not instance then return end + + if not this.CharacterSortedMsg:Get(instance)["BillboardGui"] then + this:CreateBillboardGuiHelper(instance, onlyCharacter) + end + + local billboardGui = this.CharacterSortedMsg:Get(instance)["BillboardGui"] + if billboardGui then + local chatBubbleRender = this.ChatBubbleWithTail[line.BubbleColor]:Clone() + chatBubbleRender.Visible = false + local bubbleText = this:CreateBubbleText(line.Message) + + bubbleText.Parent = chatBubbleRender + chatBubbleRender.Parent = billboardGui.BillboardFrame + + line.RenderBubble = chatBubbleRender + + local currentTextBounds = TextService:GetTextSize( + bubbleText.Text, CHAT_BUBBLE_FONT_SIZE_INT, CHAT_BUBBLE_FONT, + Vector2.new(BILLBOARD_MAX_WIDTH, BILLBOARD_MAX_HEIGHT)) + local bubbleWidthScale = math.max((currentTextBounds.X + CHAT_BUBBLE_WIDTH_PADDING)/BILLBOARD_MAX_WIDTH, 0.1) + local numOflines = (currentTextBounds.Y/CHAT_BUBBLE_FONT_SIZE_INT) + + -- prep chat bubble for tween + chatBubbleRender.Size = UDim2.new(0,0,0,0) + chatBubbleRender.Position = UDim2.new(0.5,0,1,0) + + local newChatBubbleOffsetSizeY = numOflines * CHAT_BUBBLE_LINE_HEIGHT + + chatBubbleRender:TweenSizeAndPosition(UDim2.new(bubbleWidthScale, 0, 0, newChatBubbleOffsetSizeY), + UDim2.new( (1-bubbleWidthScale)/2, 0, 1, -newChatBubbleOffsetSizeY), + Enum.EasingDirection.Out, Enum.EasingStyle.Elastic, 0.1, true, + function() bubbleText.Visible = true end) + + -- todo: remove when over max bubbles + this:SetBillboardGuiLOD(billboardGui, line.Origin) + this:UpdateChatLinesForOrigin(line.Origin, -newChatBubbleOffsetSizeY) + + delay(line.BubbleDieDelay, function() + this:DestroyBubble(fifo, chatBubbleRender) + end) + end +end + +function this:OnPlayerChatMessage(sourcePlayer, message, targetPlayer) + if not this:BubbleChatEnabled() then return end + + local localPlayer = PlayersService.LocalPlayer + local fromOthers = localPlayer ~= nil and sourcePlayer ~= localPlayer + + local safeMessage = this:SanitizeChatLine(message) + + local line = createPlayerChatLine(sourcePlayer, safeMessage, not fromOthers) + + if sourcePlayer and line.Origin then + local fifo = this.CharacterSortedMsg:Get(line.Origin).Fifo + fifo:PushBack(line) + --Game chat (badges) won't show up here + this:CreateChatLineRender(sourcePlayer.Character, line, true, fifo) + end +end + +function this:OnGameChatMessage(origin, message, color) + local localPlayer = PlayersService.LocalPlayer + local fromOthers = localPlayer ~= nil and (localPlayer.Character ~= origin) + + local bubbleColor = BubbleColor.WHITE + + if color == Enum.ChatColor.Blue then bubbleColor = BubbleColor.BLUE + elseif color == Enum.ChatColor.Green then bubbleColor = BubbleColor.GREEN + elseif color == Enum.ChatColor.Red then bubbleColor = BubbleColor.RED end + + local safeMessage = this:SanitizeChatLine(message) + local line = createGameChatLine(origin, safeMessage, not fromOthers, bubbleColor) + + this.CharacterSortedMsg:Get(line.Origin).Fifo:PushBack(line) + this:CreateChatLineRender(origin, line, false, this.CharacterSortedMsg:Get(line.Origin).Fifo) +end + +function this:BubbleChatEnabled() + local clientChatModules = ChatService:FindFirstChild("ClientChatModules") + if clientChatModules then + local chatSettings = clientChatModules:FindFirstChild("ChatSettings") + if chatSettings then + local chatSettings = require(chatSettings) + if chatSettings.BubbleChatEnabled ~= nil then + return chatSettings.BubbleChatEnabled + end + end + end + return PlayersService.BubbleChat +end + +function this:ShowOwnFilteredMessage() + local clientChatModules = ChatService:FindFirstChild("ClientChatModules") + if clientChatModules then + local chatSettings = clientChatModules:FindFirstChild("ChatSettings") + if chatSettings then + chatSettings = require(chatSettings) + return chatSettings.ShowUserOwnFilteredMessage + end + end + return false +end + +function findPlayer(playerName) + for i,v in pairs(PlayersService:GetPlayers()) do + if v.Name == playerName then + return v + end + end +end + +ChatService.Chatted:connect(function(origin, message, color) this:OnGameChatMessage(origin, message, color) end) + +local cameraChangedCon = nil +if game.Workspace.CurrentCamera then + cameraChangedCon = game.Workspace.CurrentCamera:GetPropertyChangedSignal("CFrame"):connect(function(prop) this:CameraCFrameChanged() end) +end + +game.Workspace.Changed:connect(function(prop) + if prop == "CurrentCamera" then + if cameraChangedCon then cameraChangedCon:disconnect() end + if game.Workspace.CurrentCamera then + cameraChangedCon = game.Workspace.CurrentCamera:GetPropertyChangedSignal("CFrame"):connect(function(prop) this:CameraCFrameChanged() end) + end + end +end) + + +local AllowedMessageTypes = nil + +function getAllowedMessageTypes() + if AllowedMessageTypes then + return AllowedMessageTypes + end + local clientChatModules = ChatService:FindFirstChild("ClientChatModules") + if clientChatModules then + local chatSettings = clientChatModules:FindFirstChild("ChatSettings") + if chatSettings then + chatSettings = require(chatSettings) + if chatSettings.BubbleChatMessageTypes then + AllowedMessageTypes = chatSettings.BubbleChatMessageTypes + return AllowedMessageTypes + end + end + local chatConstants = clientChatModules:FindFirstChild("ChatConstants") + if chatConstants then + chatConstants = require(chatConstants) + AllowedMessageTypes = {chatConstants.MessageTypeDefault, chatConstants.MessageTypeWhisper} + end + return AllowedMessageTypes + end + return {"Message", "Whisper"} +end + +function checkAllowedMessageType(messageData) + local allowedMessageTypes = getAllowedMessageTypes() + for i = 1, #allowedMessageTypes do + if allowedMessageTypes[i] == messageData.MessageType then + return true + end + end + return false +end + +local ChatEvents = ReplicatedStorage:WaitForChild("DefaultChatSystemChatEvents") +local OnMessageDoneFiltering = ChatEvents:WaitForChild("OnMessageDoneFiltering") +local OnNewMessage = ChatEvents:WaitForChild("OnNewMessage") + +OnNewMessage.OnClientEvent:connect(function(messageData, channelName) + if not checkAllowedMessageType(messageData) then + return + end + + local sender = findPlayer(messageData.FromSpeaker) + if not sender then + return + end + + if not messageData.IsFiltered or messageData.FromSpeaker == LocalPlayer.Name then + if messageData.FromSpeaker ~= LocalPlayer.Name or this:ShowOwnFilteredMessage() then + return + end + end + + this:OnPlayerChatMessage(sender, messageData.Message, nil) +end) + +OnMessageDoneFiltering.OnClientEvent:connect(function(messageData, channelName) + if not checkAllowedMessageType(messageData) then + return + end + + local sender = findPlayer(messageData.FromSpeaker) + if not sender then + return + end + + if messageData.FromSpeaker == LocalPlayer.Name and not this:ShowOwnFilteredMessage() then + return + end + + this:OnPlayerChatMessage(sender, messageData.Message, nil) +end) diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChannelsBar.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChannelsBar.lua new file mode 100644 index 0000000..ddddd33 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChannelsBar.lua @@ -0,0 +1,400 @@ +-- // FileName: ChannelsBar.lua +-- // Written by: Xsitsu +-- // Description: Manages creating, destroying, and displaying ChannelTabs. + +local module = {} + +local PlayerGui = game:GetService("Players").LocalPlayer:WaitForChild("PlayerGui") + +--////////////////////////////// Include +--////////////////////////////////////// +local Chat = game:GetService("Chat") +local clientChatModules = Chat:WaitForChild("ClientChatModules") +local modulesFolder = script.Parent +local moduleChannelsTab = require(modulesFolder:WaitForChild("ChannelsTab")) +local MessageSender = require(modulesFolder:WaitForChild("MessageSender")) +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) +local CurveUtil = require(modulesFolder:WaitForChild("CurveUtil")) + +--////////////////////////////// Methods +--////////////////////////////////////// +local methods = {} +methods.__index = methods + +function methods:CreateGuiObjects(targetParent) + local BaseFrame = Instance.new("Frame") + BaseFrame.Selectable = false + BaseFrame.Size = UDim2.new(1, 0, 1, 0) + BaseFrame.BackgroundTransparency = 1 + BaseFrame.Parent = targetParent + + local ScrollingBase = Instance.new("Frame") + ScrollingBase.Selectable = false + ScrollingBase.Name = "ScrollingBase" + ScrollingBase.BackgroundTransparency = 1 + ScrollingBase.ClipsDescendants = true + ScrollingBase.Size = UDim2.new(1, 0, 1, 0) + ScrollingBase.Position = UDim2.new(0, 0, 0, 0) + ScrollingBase.Parent = BaseFrame + + local ScrollerSizer = Instance.new("Frame") + ScrollerSizer.Selectable = false + ScrollerSizer.Name = "ScrollerSizer" + ScrollerSizer.BackgroundTransparency = 1 + ScrollerSizer.Size = UDim2.new(1, 0, 1, 0) + ScrollerSizer.Position = UDim2.new(0, 0, 0, 0) + ScrollerSizer.Parent = ScrollingBase + + local ScrollerFrame = Instance.new("Frame") + ScrollerFrame.Selectable = false + ScrollerFrame.Name = "ScrollerFrame" + ScrollerFrame.BackgroundTransparency = 1 + ScrollerFrame.Size = UDim2.new(1, 0, 1, 0) + ScrollerFrame.Position = UDim2.new(0, 0, 0, 0) + ScrollerFrame.Parent = ScrollerSizer + + local LeaveConfirmationFrameBase = Instance.new("Frame") + LeaveConfirmationFrameBase.Selectable = false + LeaveConfirmationFrameBase.Size = UDim2.new(1, 0, 1, 0) + LeaveConfirmationFrameBase.Position = UDim2.new(0, 0, 0, 0) + LeaveConfirmationFrameBase.ClipsDescendants = true + LeaveConfirmationFrameBase.BackgroundTransparency = 1 + LeaveConfirmationFrameBase.Parent = BaseFrame + + local LeaveConfirmationFrame = Instance.new("Frame") + LeaveConfirmationFrame.Selectable = false + LeaveConfirmationFrame.Name = "LeaveConfirmationFrame" + LeaveConfirmationFrame.Size = UDim2.new(1, 0, 1, 0) + LeaveConfirmationFrame.Position = UDim2.new(0, 0, 1, 0) + LeaveConfirmationFrame.BackgroundTransparency = 0.6 + LeaveConfirmationFrame.BorderSizePixel = 0 + LeaveConfirmationFrame.BackgroundColor3 = Color3.new(0, 0, 0) + LeaveConfirmationFrame.Parent = LeaveConfirmationFrameBase + + local InputBlocker = Instance.new("TextButton") + InputBlocker.Selectable = false + InputBlocker.Size = UDim2.new(1, 0, 1, 0) + InputBlocker.BackgroundTransparency = 1 + InputBlocker.Text = "" + InputBlocker.Parent = LeaveConfirmationFrame + + local LeaveConfirmationButtonYes = Instance.new("TextButton") + LeaveConfirmationButtonYes.Selectable = false + LeaveConfirmationButtonYes.Size = UDim2.new(0.25, 0, 1, 0) + LeaveConfirmationButtonYes.BackgroundTransparency = 1 + LeaveConfirmationButtonYes.Font = ChatSettings.DefaultFont + LeaveConfirmationButtonYes.TextSize = 18 + LeaveConfirmationButtonYes.TextStrokeTransparency = 0.75 + LeaveConfirmationButtonYes.Position = UDim2.new(0, 0, 0, 0) + LeaveConfirmationButtonYes.TextColor3 = Color3.new(0, 1, 0) + LeaveConfirmationButtonYes.Text = "Confirm" + LeaveConfirmationButtonYes.Parent = LeaveConfirmationFrame + + local LeaveConfirmationButtonNo = LeaveConfirmationButtonYes:Clone() + LeaveConfirmationButtonNo.Parent = LeaveConfirmationFrame + LeaveConfirmationButtonNo.Position = UDim2.new(0.75, 0, 0, 0) + LeaveConfirmationButtonNo.TextColor3 = Color3.new(1, 0, 0) + LeaveConfirmationButtonNo.Text = "Cancel" + + local LeaveConfirmationNotice = Instance.new("TextLabel") + LeaveConfirmationNotice.Selectable = false + LeaveConfirmationNotice.Size = UDim2.new(0.5, 0, 1, 0) + LeaveConfirmationNotice.Position = UDim2.new(0.25, 0, 0, 0) + LeaveConfirmationNotice.BackgroundTransparency = 1 + LeaveConfirmationNotice.TextColor3 = Color3.new(1, 1, 1) + LeaveConfirmationNotice.TextStrokeTransparency = 0.75 + LeaveConfirmationNotice.Text = "Leave channel ?" + LeaveConfirmationNotice.Font = ChatSettings.DefaultFont + LeaveConfirmationNotice.TextSize = 18 + LeaveConfirmationNotice.Parent = LeaveConfirmationFrame + + local LeaveTarget = Instance.new("StringValue") + LeaveTarget.Name = "LeaveTarget" + LeaveTarget.Parent = LeaveConfirmationFrame + + local outPos = LeaveConfirmationFrame.Position + LeaveConfirmationButtonYes.MouseButton1Click:connect(function() + MessageSender:SendMessage(string.format("/leave %s", LeaveTarget.Value), nil) + LeaveConfirmationFrame:TweenPosition(outPos, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.2, true) + end) + LeaveConfirmationButtonNo.MouseButton1Click:connect(function() + LeaveConfirmationFrame:TweenPosition(outPos, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.2, true) + end) + + + + local scale = 0.7 + local scaleOther = (1 - scale) / 2 + local pageButtonImage = "rbxasset://textures/ui/Chat/TabArrowBackground.png" + local pageButtonArrowImage = "rbxasset://textures/ui/Chat/TabArrow.png" + + --// ToDo: Remove these lines when the assets are put into trunk. + --// These grab unchanging versions hosted on the site, and not from the content folder. + pageButtonImage = "rbxassetid://471630199" + pageButtonArrowImage = "rbxassetid://471630112" + + + local PageLeftButton = Instance.new("ImageButton", BaseFrame) + PageLeftButton.Selectable = ChatSettings.GamepadNavigationEnabled + PageLeftButton.Name = "PageLeftButton" + PageLeftButton.SizeConstraint = Enum.SizeConstraint.RelativeYY + PageLeftButton.Size = UDim2.new(scale, 0, scale, 0) + PageLeftButton.BackgroundTransparency = 1 + PageLeftButton.Position = UDim2.new(0, 4, scaleOther, 0) + PageLeftButton.Visible = false + PageLeftButton.Image = pageButtonImage + local ArrowLabel = Instance.new("ImageLabel", PageLeftButton) + ArrowLabel.Name = "ArrowLabel" + ArrowLabel.BackgroundTransparency = 1 + ArrowLabel.Size = UDim2.new(0.4, 0, 0.4, 0) + ArrowLabel.Image = pageButtonArrowImage + + local PageRightButtonPositionalHelper = Instance.new("Frame", BaseFrame) + PageRightButtonPositionalHelper.Selectable = false + PageRightButtonPositionalHelper.BackgroundTransparency = 1 + PageRightButtonPositionalHelper.Name = "PositionalHelper" + PageRightButtonPositionalHelper.Size = PageLeftButton.Size + PageRightButtonPositionalHelper.SizeConstraint = PageLeftButton.SizeConstraint + PageRightButtonPositionalHelper.Position = UDim2.new(1, 0, scaleOther, 0) + + local PageRightButton = PageLeftButton:Clone() + PageRightButton.Parent = PageRightButtonPositionalHelper + PageRightButton.Name = "PageRightButton" + PageRightButton.Size = UDim2.new(1, 0, 1, 0) + PageRightButton.SizeConstraint = Enum.SizeConstraint.RelativeXY + PageRightButton.Position = UDim2.new(-1, -4, 0, 0) + + local positionOffset = UDim2.new(0.05, 0, 0, 0) + + PageRightButton.ArrowLabel.Position = UDim2.new(0.3, 0, 0.3, 0) + positionOffset + PageLeftButton.ArrowLabel.Position = UDim2.new(0.3, 0, 0.3, 0) - positionOffset + PageLeftButton.ArrowLabel.Rotation = 180 + + + self.GuiObject = BaseFrame + + self.GuiObjects.BaseFrame = BaseFrame + self.GuiObjects.ScrollerSizer = ScrollerSizer + self.GuiObjects.ScrollerFrame = ScrollerFrame + self.GuiObjects.PageLeftButton = PageLeftButton + self.GuiObjects.PageRightButton = PageRightButton + self.GuiObjects.LeaveConfirmationFrame = LeaveConfirmationFrame + self.GuiObjects.LeaveConfirmationNotice = LeaveConfirmationNotice + + self.GuiObjects.PageLeftButtonArrow = PageLeftButton.ArrowLabel + self.GuiObjects.PageRightButtonArrow = PageRightButton.ArrowLabel + self:AnimGuiObjects() + + PageLeftButton.MouseButton1Click:connect(function() self:ScrollChannelsFrame(-1) end) + PageRightButton.MouseButton1Click:connect(function() self:ScrollChannelsFrame(1) end) + + self:ScrollChannelsFrame(0) +end + + +function methods:UpdateMessagePostedInChannel(channelName) + local tab = self:GetChannelTab(channelName) + if (tab) then + tab:UpdateMessagePostedInChannel() + else + warn("ChannelsTab '" .. channelName .. "' does not exist!") + end +end + +function methods:AddChannelTab(channelName) + if (self:GetChannelTab(channelName)) then + error("Channel tab '" .. channelName .. "'already exists!") + end + + local tab = moduleChannelsTab.new(channelName) + tab.GuiObject.Parent = self.GuiObjects.ScrollerFrame + self.ChannelTabs[channelName:lower()] = tab + + self.NumTabs = self.NumTabs + 1 + self:OrganizeChannelTabs() + + if (ChatSettings.RightClickToLeaveChannelEnabled) then + tab.NameTag.MouseButton2Click:connect(function() + self.LeaveConfirmationNotice.Text = string.format("Leave channel %s?", tab.ChannelName) + self.LeaveConfirmationFrame.LeaveTarget.Value = tab.ChannelName + self.LeaveConfirmationFrame:TweenPosition(UDim2.new(0, 0, 0, 0), Enum.EasingDirection.In, Enum.EasingStyle.Quad, 0.2, true) + end) + end + + return tab +end + +function methods:RemoveChannelTab(channelName) + if (not self:GetChannelTab(channelName)) then + error("Channel tab '" .. channelName .. "'does not exist!") + end + + local indexName = channelName:lower() + self.ChannelTabs[indexName]:Destroy() + self.ChannelTabs[indexName] = nil + + self.NumTabs = self.NumTabs - 1 + self:OrganizeChannelTabs() +end + +function methods:GetChannelTab(channelName) + return self.ChannelTabs[channelName:lower()] +end + +function methods:OrganizeChannelTabs() + local order = {} + + table.insert(order, self:GetChannelTab(ChatSettings.GeneralChannelName)) + table.insert(order, self:GetChannelTab("System")) + + for tabIndexName, tab in pairs(self.ChannelTabs) do + if (tab.ChannelName ~= ChatSettings.GeneralChannelName and tab.ChannelName ~= "System") then + table.insert(order, tab) + end + end + + for index, tab in pairs(order) do + tab.GuiObject.Position = UDim2.new(index - 1, 0, 0, 0) + end + + --// Dynamic tab resizing + self.GuiObjects.ScrollerSizer.Size = UDim2.new(1 / math.max(1, math.min(ChatSettings.ChannelsBarFullTabSize, self.NumTabs)), 0, 1, 0) + + self:ScrollChannelsFrame(0) +end + +function methods:ResizeChannelTabText(textSize) + for i, tab in pairs(self.ChannelTabs) do + tab:SetTextSize(textSize) + end +end + +function methods:ScrollChannelsFrame(dir) + if (self.ScrollChannelsFrameLock) then return end + self.ScrollChannelsFrameLock = true + + local tabNumber = ChatSettings.ChannelsBarFullTabSize + + local newPageNum = self.CurPageNum + dir + if (newPageNum < 0) then + newPageNum = 0 + elseif (newPageNum > 0 and newPageNum + tabNumber > self.NumTabs) then + newPageNum = self.NumTabs - tabNumber + end + + self.CurPageNum = newPageNum + + local tweenTime = 0.15 + local endPos = UDim2.new(-self.CurPageNum, 0, 0, 0) + + self.GuiObjects.PageLeftButton.Visible = (self.CurPageNum > 0) + self.GuiObjects.PageRightButton.Visible = (self.CurPageNum + tabNumber < self.NumTabs) + + if dir == 0 then + self.ScrollChannelsFrameLock = false + return + end + + local function UnlockFunc() + self.ScrollChannelsFrameLock = false + end + + self:WaitUntilParentedCorrectly() + + self.GuiObjects.ScrollerFrame:TweenPosition(endPos, Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, tweenTime, true, UnlockFunc) +end + +function methods:FadeOutBackground(duration) + for channelName, channelObj in pairs(self.ChannelTabs) do + channelObj:FadeOutBackground(duration) + end + + self.AnimParams.Background_TargetTransparency = 1 + self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) +end + +function methods:FadeInBackground(duration) + for channelName, channelObj in pairs(self.ChannelTabs) do + channelObj:FadeInBackground(duration) + end + + self.AnimParams.Background_TargetTransparency = 0.6 + self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) +end + +function methods:FadeOutText(duration) + for channelName, channelObj in pairs(self.ChannelTabs) do + channelObj:FadeOutText(duration) + end +end + +function methods:FadeInText(duration) + for channelName, channelObj in pairs(self.ChannelTabs) do + channelObj:FadeInText(duration) + end +end + +function methods:AnimGuiObjects() + self.GuiObjects.PageLeftButton.ImageTransparency = self.AnimParams.Background_CurrentTransparency + self.GuiObjects.PageRightButton.ImageTransparency = self.AnimParams.Background_CurrentTransparency + self.GuiObjects.PageLeftButtonArrow.ImageTransparency = self.AnimParams.Background_CurrentTransparency + self.GuiObjects.PageRightButtonArrow.ImageTransparency = self.AnimParams.Background_CurrentTransparency +end + +function methods:InitializeAnimParams() + self.AnimParams.Background_TargetTransparency = 0.6 + self.AnimParams.Background_CurrentTransparency = 0.6 + self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(0) +end + +function methods:Update(dtScale) + for channelName, channelObj in pairs(self.ChannelTabs) do + channelObj:Update(dtScale) + end + + self.AnimParams.Background_CurrentTransparency = CurveUtil:Expt( + self.AnimParams.Background_CurrentTransparency, + self.AnimParams.Background_TargetTransparency, + self.AnimParams.Background_NormalizedExptValue, + dtScale + ) + + self:AnimGuiObjects() +end + +--// ToDo: Move to common modules +function methods:WaitUntilParentedCorrectly() + while (not self.GuiObject:IsDescendantOf(game:GetService("Players").LocalPlayer)) do + self.GuiObject.AncestryChanged:wait() + end +end + +--///////////////////////// Constructors +--////////////////////////////////////// + +function module.new() + local obj = setmetatable({}, methods) + + obj.GuiObject = nil + obj.GuiObjects = {} + + obj.ChannelTabs = {} + obj.NumTabs = 0 + obj.CurPageNum = 0 + + obj.ScrollChannelsFrameLock = false + + obj.AnimParams = {} + + obj:InitializeAnimParams() + + ChatSettings.SettingsChanged:connect(function(setting, value) + if (setting == "ChatChannelsTabTextSize") then + obj:ResizeChannelTabText(value) + end + end) + + return obj +end + +return module diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChannelsTab.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChannelsTab.lua new file mode 100644 index 0000000..b374ad1 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChannelsTab.lua @@ -0,0 +1,310 @@ +-- // FileName: ChannelsTab.lua +-- // Written by: Xsitsu +-- // Description: Channel tab button for selecting current channel and also displaying if currently selected. + +local module = {} +--////////////////////////////// Include +--////////////////////////////////////// +local Chat = game:GetService("Chat") +local clientChatModules = Chat:WaitForChild("ClientChatModules") +local modulesFolder = script.Parent +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) +local CurveUtil = require(modulesFolder:WaitForChild("CurveUtil")) + +--////////////////////////////// Methods +--////////////////////////////////////// +local methods = {} +methods.__index = methods + +local function CreateGuiObjects() + local BaseFrame = Instance.new("Frame") + BaseFrame.Selectable = false + BaseFrame.Size = UDim2.new(1, 0, 1, 0) + BaseFrame.BackgroundTransparency = 1 + + local gapOffsetX = 1 + local gapOffsetY = 1 + + local BackgroundFrame = Instance.new("Frame") + BackgroundFrame.Selectable = false + BackgroundFrame.Name = "BackgroundFrame" + BackgroundFrame.Size = UDim2.new(1, -gapOffsetX * 2, 1, -gapOffsetY * 2) + BackgroundFrame.Position = UDim2.new(0, gapOffsetX, 0, gapOffsetY) + BackgroundFrame.BackgroundTransparency = 1 + BackgroundFrame.Parent = BaseFrame + + local UnselectedFrame = Instance.new("Frame") + UnselectedFrame.Selectable = false + UnselectedFrame.Name = "UnselectedFrame" + UnselectedFrame.Size = UDim2.new(1, 0, 1, 0) + UnselectedFrame.Position = UDim2.new(0, 0, 0, 0) + UnselectedFrame.BorderSizePixel = 0 + UnselectedFrame.BackgroundColor3 = ChatSettings.ChannelsTabUnselectedColor + UnselectedFrame.BackgroundTransparency = 0.6 + UnselectedFrame.Parent = BackgroundFrame + + local SelectedFrame = Instance.new("Frame") + SelectedFrame.Selectable = false + SelectedFrame.Name = "SelectedFrame" + SelectedFrame.Size = UDim2.new(1, 0, 1, 0) + SelectedFrame.Position = UDim2.new(0, 0, 0, 0) + SelectedFrame.BorderSizePixel = 0 + SelectedFrame.BackgroundColor3 = ChatSettings.ChannelsTabSelectedColor + SelectedFrame.BackgroundTransparency = 1 + SelectedFrame.Parent = BackgroundFrame + + local SelectedFrameBackgroundImage = Instance.new("ImageLabel") + SelectedFrameBackgroundImage.Selectable = false + SelectedFrameBackgroundImage.Name = "BackgroundImage" + SelectedFrameBackgroundImage.BackgroundTransparency = 1 + SelectedFrameBackgroundImage.BorderSizePixel = 0 + SelectedFrameBackgroundImage.Size = UDim2.new(1, 0, 1, 0) + SelectedFrameBackgroundImage.Position = UDim2.new(0, 0, 0, 0) + SelectedFrameBackgroundImage.ScaleType = Enum.ScaleType.Slice + SelectedFrameBackgroundImage.Parent = SelectedFrame + + SelectedFrameBackgroundImage.BackgroundTransparency = 0.6 - 1 + local rate = 1.2 * 1 + SelectedFrameBackgroundImage.BackgroundColor3 = Color3.fromRGB(78 * rate, 84 * rate, 96 * rate) + + local borderXOffset = 2 + local blueBarYSize = 4 + local BlueBarLeft = Instance.new("ImageLabel") + BlueBarLeft.Selectable = false + BlueBarLeft.Size = UDim2.new(0.5, -borderXOffset, 0, blueBarYSize) + BlueBarLeft.BackgroundTransparency = 1 + BlueBarLeft.ScaleType = Enum.ScaleType.Slice + BlueBarLeft.SliceCenter = Rect.new(3,3,32,21) + BlueBarLeft.Parent = SelectedFrame + + local BlueBarRight = BlueBarLeft:Clone() + BlueBarRight.Parent = SelectedFrame + + BlueBarLeft.Position = UDim2.new(0, borderXOffset, 1, -blueBarYSize) + BlueBarRight.Position = UDim2.new(0.5, 0, 1, -blueBarYSize) + BlueBarLeft.Image = "rbxasset://textures/ui/Settings/Slider/SelectedBarLeft.png" + BlueBarRight.Image = "rbxasset://textures/ui/Settings/Slider/SelectedBarRight.png" + + BlueBarLeft.Name = "BlueBarLeft" + BlueBarRight.Name = "BlueBarRight" + + local NameTag = Instance.new("TextButton") + NameTag.Selectable = ChatSettings.GamepadNavigationEnabled + NameTag.Size = UDim2.new(1, 0, 1, 0) + NameTag.Position = UDim2.new(0, 0, 0, 0) + NameTag.BackgroundTransparency = 1 + NameTag.Font = ChatSettings.DefaultFont + NameTag.TextSize = ChatSettings.ChatChannelsTabTextSize + NameTag.TextColor3 = Color3.new(1, 1, 1) + NameTag.TextStrokeTransparency = 0.75 + NameTag.Parent = BackgroundFrame + + local NameTagNonSelect = NameTag:Clone() + local NameTagSelect = NameTag:Clone() + NameTagNonSelect.Parent = UnselectedFrame + NameTagSelect.Parent = SelectedFrame + NameTagNonSelect.Font = Enum.Font.SourceSans + NameTagNonSelect.Active = false + NameTagSelect.Active = false + + local NewMessageIconFrame = Instance.new("Frame") + NewMessageIconFrame.Selectable = false + NewMessageIconFrame.Size = UDim2.new(0, 18, 0, 18) + NewMessageIconFrame.Position = UDim2.new(0.8, -9, 0.5, -9) + NewMessageIconFrame.BackgroundTransparency = 1 + NewMessageIconFrame.Parent = BackgroundFrame + + local NewMessageIcon = Instance.new("ImageLabel") + NewMessageIcon.Selectable = false + NewMessageIcon.Size = UDim2.new(1, 0, 1, 0) + NewMessageIcon.BackgroundTransparency = 1 + NewMessageIcon.Image = "rbxasset://textures/ui/Chat/MessageCounter.png" + NewMessageIcon.Visible = false + NewMessageIcon.Parent = NewMessageIconFrame + + local NewMessageIconText = Instance.new("TextLabel") + NewMessageIconText.Selectable = false + NewMessageIconText.BackgroundTransparency = 1 + NewMessageIconText.Size = UDim2.new(0, 13, 0, 9) + NewMessageIconText.Position = UDim2.new(0.5, -7, 0.5, -7) + NewMessageIconText.Font = ChatSettings.DefaultFont + NewMessageIconText.TextSize = 14 + NewMessageIconText.TextColor3 = Color3.new(1, 1, 1) + NewMessageIconText.Text = "" + NewMessageIconText.Parent = NewMessageIcon + + return BaseFrame, NameTag, NameTagNonSelect, NameTagSelect, NewMessageIcon, UnselectedFrame, SelectedFrame +end + +function methods:Destroy() + self.GuiObject:Destroy() +end + +function methods:UpdateMessagePostedInChannel(ignoreActive) + if (self.Active and (ignoreActive ~= true)) then return end + + local count = self.UnreadMessageCount + 1 + self.UnreadMessageCount = count + + local label = self.NewMessageIcon + label.Visible = true + label.TextLabel.Text = (count < 100) and tostring(count) or "!" + + local tweenTime = 0.15 + local tweenPosOffset = UDim2.new(0, 0, -0.1, 0) + + local curPos = label.Position + local outPos = curPos + tweenPosOffset + local easingDirection = Enum.EasingDirection.Out + local easingStyle = Enum.EasingStyle.Quad + + label.Position = UDim2.new(0, 0, -0.15, 0) + label:TweenPosition(UDim2.new(0, 0, 0, 0), easingDirection, easingStyle, tweenTime, true) + +end + +function methods:SetActive(active) + self.Active = active + self.UnselectedFrame.Visible = not active + self.SelectedFrame.Visible = active + + if (active) then + self.UnreadMessageCount = 0 + self.NewMessageIcon.Visible = false + + self.NameTag.Font = Enum.Font.SourceSansBold + else + self.NameTag.Font = Enum.Font.SourceSans + + end +end + +function methods:SetTextSize(textSize) + self.NameTag.TextSize = textSize +end + +function methods:FadeOutBackground(duration) + self.AnimParams.Background_TargetTransparency = 1 + self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) +end + +function methods:FadeInBackground(duration) + self.AnimParams.Background_TargetTransparency = 0.6 + self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) +end + +function methods:FadeOutText(duration) + self.AnimParams.Text_TargetTransparency = 1 + self.AnimParams.Text_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) + self.AnimParams.TextStroke_TargetTransparency = 1 + self.AnimParams.TextStroke_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) +end + +function methods:FadeInText(duration) + self.AnimParams.Text_TargetTransparency = 0 + self.AnimParams.Text_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) + self.AnimParams.TextStroke_TargetTransparency = 0.75 + self.AnimParams.TextStroke_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) +end + +function methods:AnimGuiObjects() + self.UnselectedFrame.BackgroundTransparency = self.AnimParams.Background_CurrentTransparency + self.SelectedFrame.BackgroundImage.BackgroundTransparency = self.AnimParams.Background_CurrentTransparency + self.SelectedFrame.BlueBarLeft.ImageTransparency = self.AnimParams.Background_CurrentTransparency + self.SelectedFrame.BlueBarRight.ImageTransparency = self.AnimParams.Background_CurrentTransparency + self.NameTagNonSelect.TextTransparency = self.AnimParams.Background_CurrentTransparency + self.NameTagNonSelect.TextStrokeTransparency = self.AnimParams.Background_CurrentTransparency + + self.NameTag.TextTransparency = self.AnimParams.Text_CurrentTransparency + self.NewMessageIcon.ImageTransparency = self.AnimParams.Text_CurrentTransparency + self.WhiteTextNewMessageNotification.TextTransparency = self.AnimParams.Text_CurrentTransparency + self.NameTagSelect.TextTransparency = self.AnimParams.Text_CurrentTransparency + + self.NameTag.TextStrokeTransparency = self.AnimParams.TextStroke_CurrentTransparency + self.WhiteTextNewMessageNotification.TextStrokeTransparency = self.AnimParams.TextStroke_CurrentTransparency + self.NameTagSelect.TextStrokeTransparency = self.AnimParams.TextStroke_CurrentTransparency +end + +function methods:InitializeAnimParams() + self.AnimParams.Text_TargetTransparency = 0 + self.AnimParams.Text_CurrentTransparency = 0 + self.AnimParams.Text_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(0) + + self.AnimParams.TextStroke_TargetTransparency = 0.75 + self.AnimParams.TextStroke_CurrentTransparency = 0.75 + self.AnimParams.TextStroke_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(0) + + self.AnimParams.Background_TargetTransparency = 0.6 + self.AnimParams.Background_CurrentTransparency = 0.6 + self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(0) +end + +function methods:Update(dtScale) + self.AnimParams.Background_CurrentTransparency = CurveUtil:Expt( + self.AnimParams.Background_CurrentTransparency, + self.AnimParams.Background_TargetTransparency, + self.AnimParams.Background_NormalizedExptValue, + dtScale + ) + self.AnimParams.Text_CurrentTransparency = CurveUtil:Expt( + self.AnimParams.Text_CurrentTransparency, + self.AnimParams.Text_TargetTransparency, + self.AnimParams.Text_NormalizedExptValue, + dtScale + ) + self.AnimParams.TextStroke_CurrentTransparency = CurveUtil:Expt( + self.AnimParams.TextStroke_CurrentTransparency, + self.AnimParams.TextStroke_TargetTransparency, + self.AnimParams.TextStroke_NormalizedExptValue, + dtScale + ) + + self:AnimGuiObjects() +end + +--///////////////////////// Constructors +--////////////////////////////////////// + +function module.new(channelName) + local obj = setmetatable({}, methods) + + local BaseFrame, NameTag, NameTagNonSelect, NameTagSelect, NewMessageIcon, UnselectedFrame, SelectedFrame = CreateGuiObjects() + obj.GuiObject = BaseFrame + obj.NameTag = NameTag + obj.NameTagNonSelect = NameTagNonSelect + obj.NameTagSelect = NameTagSelect + obj.NewMessageIcon = NewMessageIcon + obj.UnselectedFrame = UnselectedFrame + obj.SelectedFrame = SelectedFrame + + obj.BlueBarLeft = SelectedFrame.BlueBarLeft + obj.BlueBarRight = SelectedFrame.BlueBarRight + obj.BackgroundImage = SelectedFrame.BackgroundImage + obj.WhiteTextNewMessageNotification = obj.NewMessageIcon.TextLabel + + obj.ChannelName = channelName + obj.UnreadMessageCount = 0 + obj.Active = false + + obj.GuiObject.Name = "Frame_" .. obj.ChannelName + + if (string.len(channelName) > ChatSettings.MaxChannelNameLength) then + channelName = string.sub(channelName, 1, ChatSettings.MaxChannelNameLength - 3) .. "..." + end + + --obj.NameTag.Text = channelName + + obj.NameTag.Text = "" + obj.NameTagNonSelect.Text = channelName + obj.NameTagSelect.Text = channelName + + obj.AnimParams = {} + + obj:InitializeAnimParams() + obj:AnimGuiObjects() + obj:SetActive(false) + + return obj +end + +return module diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatBar.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatBar.lua new file mode 100644 index 0000000..9477dcd --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatBar.lua @@ -0,0 +1,571 @@ +-- // FileName: ChatBar.lua +-- // Written by: Xsitsu +-- // Description: Manages text typing and typing state. + +local module = {} + +local UserInputService = game:GetService("UserInputService") +local RunService = game:GetService("RunService") +local Players = game:GetService("Players") +local LocalPlayer = Players.LocalPlayer + +while not LocalPlayer do + Players.PlayerAdded:wait() + LocalPlayer = Players.LocalPlayer +end + +--////////////////////////////// Include +--////////////////////////////////////// +local Chat = game:GetService("Chat") +local clientChatModules = Chat:WaitForChild("ClientChatModules") +local modulesFolder = script.Parent +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) +local CurveUtil = require(modulesFolder:WaitForChild("CurveUtil")) + +local MessageSender = require(modulesFolder:WaitForChild("MessageSender")) + +local ChatLocalization = nil +pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end) +if ChatLocalization == nil then ChatLocalization = {} function ChatLocalization:Get(key,default) return default end end + +--////////////////////////////// Methods +--////////////////////////////////////// +local methods = {} +methods.__index = methods + +function methods:CreateGuiObjects(targetParent) + self.ChatBarParentFrame = targetParent + + local backgroundImagePixelOffset = 7 + local textBoxPixelOffset = 5 + + local BaseFrame = Instance.new("Frame") + BaseFrame.Selectable = false + BaseFrame.Size = UDim2.new(1, 0, 1, 0) + BaseFrame.BackgroundTransparency = 0.6 + BaseFrame.BorderSizePixel = 0 + BaseFrame.BackgroundColor3 = ChatSettings.ChatBarBackGroundColor + BaseFrame.Parent = targetParent + + local BoxFrame = Instance.new("Frame") + BoxFrame.Selectable = false + BoxFrame.Name = "BoxFrame" + BoxFrame.BackgroundTransparency = 0.6 + BoxFrame.BorderSizePixel = 0 + BoxFrame.BackgroundColor3 = ChatSettings.ChatBarBoxColor + BoxFrame.Size = UDim2.new(1, -backgroundImagePixelOffset * 2, 1, -backgroundImagePixelOffset * 2) + BoxFrame.Position = UDim2.new(0, backgroundImagePixelOffset, 0, backgroundImagePixelOffset) + BoxFrame.Parent = BaseFrame + + local TextBoxHolderFrame = Instance.new("Frame") + TextBoxHolderFrame.BackgroundTransparency = 1 + TextBoxHolderFrame.Size = UDim2.new(1, -textBoxPixelOffset * 2, 1, -textBoxPixelOffset * 2) + TextBoxHolderFrame.Position = UDim2.new(0, textBoxPixelOffset, 0, textBoxPixelOffset) + TextBoxHolderFrame.Parent = BoxFrame + + local TextBox = Instance.new("TextBox") + TextBox.Selectable = ChatSettings.GamepadNavigationEnabled + TextBox.Name = "ChatBar" + TextBox.BackgroundTransparency = 1 + TextBox.Size = UDim2.new(1, 0, 1, 0) + TextBox.Position = UDim2.new(0, 0, 0, 0) + TextBox.TextSize = ChatSettings.ChatBarTextSize + TextBox.Font = ChatSettings.ChatBarFont + TextBox.TextColor3 = ChatSettings.ChatBarTextColor + TextBox.TextTransparency = 0.4 + TextBox.TextStrokeTransparency = 1 + TextBox.ClearTextOnFocus = false + TextBox.TextXAlignment = Enum.TextXAlignment.Left + TextBox.TextYAlignment = Enum.TextYAlignment.Top + TextBox.TextWrapped = true + TextBox.Text = "" + TextBox.Parent = TextBoxHolderFrame + + local MessageModeTextButton = Instance.new("TextButton") + MessageModeTextButton.Selectable = false + MessageModeTextButton.Name = "MessageMode" + MessageModeTextButton.BackgroundTransparency = 1 + MessageModeTextButton.Position = UDim2.new(0, 0, 0, 0) + MessageModeTextButton.TextSize = ChatSettings.ChatBarTextSize + MessageModeTextButton.Font = ChatSettings.ChatBarFont + MessageModeTextButton.TextXAlignment = Enum.TextXAlignment.Left + MessageModeTextButton.TextWrapped = true + MessageModeTextButton.Text = "" + MessageModeTextButton.Size = UDim2.new(0, 0, 0, 0) + MessageModeTextButton.TextYAlignment = Enum.TextYAlignment.Center + MessageModeTextButton.TextColor3 = self:GetDefaultChannelNameColor() + MessageModeTextButton.Visible = true + MessageModeTextButton.Parent = TextBoxHolderFrame + + local TextLabel = Instance.new("TextLabel") + TextLabel.Selectable = false + TextLabel.TextWrapped = true + TextLabel.BackgroundTransparency = 1 + TextLabel.Size = TextBox.Size + TextLabel.Position = TextBox.Position + TextLabel.TextSize = TextBox.TextSize + TextLabel.Font = TextBox.Font + TextLabel.TextColor3 = TextBox.TextColor3 + TextLabel.TextTransparency = TextBox.TextTransparency + TextLabel.TextStrokeTransparency = TextBox.TextStrokeTransparency + TextLabel.TextXAlignment = TextBox.TextXAlignment + TextLabel.TextYAlignment = TextBox.TextYAlignment + TextLabel.Text = "..." + TextLabel.Parent = TextBoxHolderFrame + + self.GuiObject = BaseFrame + self.TextBox = TextBox + self.TextLabel = TextLabel + + self.GuiObjects.BaseFrame = BaseFrame + self.GuiObjects.TextBoxFrame = BoxFrame + self.GuiObjects.TextBox = TextBox + self.GuiObjects.TextLabel = TextLabel + self.GuiObjects.MessageModeTextButton = MessageModeTextButton + + self:AnimGuiObjects() + self:SetUpTextBoxEvents(TextBox, TextLabel, MessageModeTextButton) + if self.UserHasChatOff then + self:DoLockChatBar() + end + self.eGuiObjectsChanged:Fire() +end + +-- Used to lock the chat bar when the user has chat turned off. +function methods:DoLockChatBar() + if self.TextLabel then + if LocalPlayer.UserId > 0 then + self.TextLabel.Text = ChatLocalization:Get( + "GameChat_ChatMessageValidator_SettingsError", + "To chat in game, turn on chat in your Privacy Settings." + ) + else + self.TextLabel.Text = ChatLocalization:Get( + "GameChat_SwallowGuestChat_Message", + "Sign up to chat in game." + ) + end + self:CalculateSize() + end + if self.TextBox then + self.TextBox.Active = false + self.TextBox.Focused:connect(function() + self.TextBox:ReleaseFocus() + end) + end +end + +function methods:SetUpTextBoxEvents(TextBox, TextLabel, MessageModeTextButton) + -- Clean up events from a previous setup. + for name, conn in pairs(self.TextBoxConnections) do + conn:disconnect() + self.TextBoxConnections[name] = nil + end + + --// Code for getting back into general channel from other target channel when pressing backspace. + self.TextBoxConnections.UserInputBegan = UserInputService.InputBegan:connect(function(inputObj, gpe) + if (inputObj.KeyCode == Enum.KeyCode.Backspace) then + if (self:IsFocused() and TextBox.Text == "") then + self:SetChannelTarget(ChatSettings.GeneralChannelName) + end + end + end) + + self.TextBoxConnections.TextBoxChanged = TextBox.Changed:connect(function(prop) + if prop == "AbsoluteSize" then + self:CalculateSize() + return + end + + if prop ~= "Text" then + return + end + + self:CalculateSize() + + if (string.len(TextBox.Text) > ChatSettings.MaximumMessageLength) then + TextBox.Text = string.sub(TextBox.Text, 1, ChatSettings.MaximumMessageLength) + return + end + + if not self.InCustomState then + local customState = self.CommandProcessor:ProcessInProgressChatMessage(TextBox.Text, self.ChatWindow, self) + if customState then + self.InCustomState = true + self.CustomState = customState + end + else + self.CustomState:TextUpdated() + end + end) + + local function UpdateOnFocusStatusChanged(isFocused) + if isFocused or TextBox.Text ~= "" then + TextLabel.Visible = false + else + TextLabel.Visible = true + end + end + + self.TextBoxConnections.MessageModeClick = MessageModeTextButton.MouseButton1Click:connect(function() + if MessageModeTextButton.Text ~= "" then + self:SetChannelTarget(ChatSettings.GeneralChannelName) + end + end) + + self.TextBoxConnections.TextBoxFocused = TextBox.Focused:connect(function() + if not self.UserHasChatOff then + self:CalculateSize() + UpdateOnFocusStatusChanged(true) + end + end) + + self.TextBoxConnections.TextBoxFocusLost = TextBox.FocusLost:connect(function(enterPressed, inputObject) + self:CalculateSize() + if (inputObject and inputObject.KeyCode == Enum.KeyCode.Escape) then + TextBox.Text = "" + end + UpdateOnFocusStatusChanged(false) + end) +end + +function methods:GetTextBox() + return self.TextBox +end + +function methods:GetMessageModeTextButton() + return self.GuiObjects.MessageModeTextButton +end + +-- Deprecated in favour of GetMessageModeTextButton +-- Retained for compatibility reasons. +function methods:GetMessageModeTextLabel() + return self:GetMessageModeTextButton() +end + +function methods:IsFocused() + if self.UserHasChatOff then + return false + end + + return self:GetTextBox():IsFocused() +end + +function methods:GetVisible() + return self.GuiObject.Visible +end + +function methods:CaptureFocus() + if not self.UserHasChatOff then + self:GetTextBox():CaptureFocus() + end +end + +function methods:ReleaseFocus(didRelease) + self:GetTextBox():ReleaseFocus(didRelease) +end + +function methods:ResetText() + self:GetTextBox().Text = "" +end + +function methods:SetText(text) + self:GetTextBox().Text = text +end + +function methods:GetEnabled() + return self.GuiObject.Visible +end + +function methods:SetEnabled(enabled) + if self.UserHasChatOff then + -- The chat bar can not be removed if a user has chat turned off so that + -- the chat bar can display a message explaining that chat is turned off. + self.GuiObject.Visible = true + else + self.GuiObject.Visible = enabled + end +end + +function methods:SetTextLabelText(text) + if not self.UserHasChatOff then + self.TextLabel.Text = text + end +end + +function methods:SetTextBoxText(text) + self.TextBox.Text = text +end + +function methods:GetTextBoxText() + return self.TextBox.Text +end + +function methods:ResetSize() + self.TargetYSize = 0 + self:TweenToTargetYSize() +end + +function methods:CalculateSize() + if self.CalculatingSizeLock then + return + end + self.CalculatingSizeLock = true + + local lastPos = self.GuiObject.Size + self.GuiObject.Size = UDim2.new(1, 0, 0, 1000) + + local textSize = nil + local bounds = nil + + if self:IsFocused() or self.TextBox.Text ~= "" then + textSize = self.TextBox.textSize + bounds = self.TextBox.TextBounds.Y + else + textSize = self.TextLabel.textSize + bounds = self.TextLabel.TextBounds.Y + end + + self.GuiObject.Size = lastPos + + local newTargetYSize = bounds - textSize + if (self.TargetYSize ~= newTargetYSize) then + self.TargetYSize = newTargetYSize + self:TweenToTargetYSize() + end + + self.CalculatingSizeLock = false +end + +function methods:TweenToTargetYSize() + local endSize = UDim2.new(1, 0, 1, self.TargetYSize) + local curSize = self.GuiObject.Size + + local curAbsoluteSizeY = self.GuiObject.AbsoluteSize.Y + self.GuiObject.Size = endSize + local endAbsoluteSizeY = self.GuiObject.AbsoluteSize.Y + self.GuiObject.Size = curSize + + local pixelDistance = math.abs(endAbsoluteSizeY - curAbsoluteSizeY) + local tweeningTime = math.min(1, (pixelDistance * (1 / self.TweenPixelsPerSecond))) -- pixelDistance * (seconds per pixels) + + local success = pcall(function() self.GuiObject:TweenSize(endSize, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, tweeningTime, true) end) + if (not success) then + self.GuiObject.Size = endSize + end +end + +function methods:SetTextSize(textSize) + if not self:IsInCustomState() then + if self.TextBox then + self.TextBox.TextSize = textSize + end + if self.TextLabel then + self.TextLabel.TextSize = textSize + end + end +end + +function methods:GetDefaultChannelNameColor() + if ChatSettings.DefaultChannelNameColor then + return ChatSettings.DefaultChannelNameColor + end + return Color3.fromRGB(35, 76, 142) +end + +function methods:SetChannelTarget(targetChannel) + local messageModeTextButton = self.GuiObjects.MessageModeTextButton + local textBox = self.TextBox + local textLabel = self.TextLabel + + self.TargetChannel = targetChannel + + if not self:IsInCustomState() then + if targetChannel ~= ChatSettings.GeneralChannelName then + messageModeTextButton.Size = UDim2.new(0, 1000, 1, 0) + messageModeTextButton.Text = string.format("[%s] ", targetChannel) + + local channelNameColor = self:GetChannelNameColor(targetChannel) + if channelNameColor then + messageModeTextButton.TextColor3 = channelNameColor + else + messageModeTextButton.TextColor3 = self:GetDefaultChannelNameColor() + end + + local xSize = messageModeTextButton.TextBounds.X + messageModeTextButton.Size = UDim2.new(0, xSize, 1, 0) + textBox.Size = UDim2.new(1, -xSize, 1, 0) + textBox.Position = UDim2.new(0, xSize, 0, 0) + textLabel.Size = UDim2.new(1, -xSize, 1, 0) + textLabel.Position = UDim2.new(0, xSize, 0, 0) + else + messageModeTextButton.Text = "" + messageModeTextButton.Size = UDim2.new(0, 0, 0, 0) + textBox.Size = UDim2.new(1, 0, 1, 0) + textBox.Position = UDim2.new(0, 0, 0, 0) + textLabel.Size = UDim2.new(1, 0, 1, 0) + textLabel.Position = UDim2.new(0, 0, 0, 0) + end + end +end + +function methods:IsInCustomState() + return self.InCustomState +end + +function methods:ResetCustomState() + if self.InCustomState then + self.CustomState:Destroy() + self.CustomState = nil + self.InCustomState = false + + self.ChatBarParentFrame:ClearAllChildren() + self:CreateGuiObjects(self.ChatBarParentFrame) + self:SetTextLabelText( + ChatLocalization:Get( + "GameChat_ChatMain_ChatBarText", + 'To chat click here or press "/" key' + ) + ) + end +end + +function methods:GetCustomMessage() + if self.InCustomState then + return self.CustomState:GetMessage() + end + return nil +end + +function methods:CustomStateProcessCompletedMessage(message) + if self.InCustomState then + return self.CustomState:ProcessCompletedMessage() + end + return false +end + +function methods:FadeOutBackground(duration) + self.AnimParams.Background_TargetTransparency = 1 + self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) + self:FadeOutText(duration) +end + +function methods:FadeInBackground(duration) + self.AnimParams.Background_TargetTransparency = 0.6 + self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) + self:FadeInText(duration) +end + +function methods:FadeOutText(duration) + self.AnimParams.Text_TargetTransparency = 1 + self.AnimParams.Text_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) +end + +function methods:FadeInText(duration) + self.AnimParams.Text_TargetTransparency = 0.4 + self.AnimParams.Text_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) +end + +function methods:AnimGuiObjects() + self.GuiObject.BackgroundTransparency = self.AnimParams.Background_CurrentTransparency + self.GuiObjects.TextBoxFrame.BackgroundTransparency = self.AnimParams.Background_CurrentTransparency + + self.GuiObjects.TextLabel.TextTransparency = self.AnimParams.Text_CurrentTransparency + self.GuiObjects.TextBox.TextTransparency = self.AnimParams.Text_CurrentTransparency + self.GuiObjects.MessageModeTextButton.TextTransparency = self.AnimParams.Text_CurrentTransparency +end + +function methods:InitializeAnimParams() + self.AnimParams.Text_TargetTransparency = 0.4 + self.AnimParams.Text_CurrentTransparency = 0.4 + self.AnimParams.Text_NormalizedExptValue = 1 + + self.AnimParams.Background_TargetTransparency = 0.6 + self.AnimParams.Background_CurrentTransparency = 0.6 + self.AnimParams.Background_NormalizedExptValue = 1 +end + +function methods:Update(dtScale) + self.AnimParams.Text_CurrentTransparency = CurveUtil:Expt( + self.AnimParams.Text_CurrentTransparency, + self.AnimParams.Text_TargetTransparency, + self.AnimParams.Text_NormalizedExptValue, + dtScale + ) + self.AnimParams.Background_CurrentTransparency = CurveUtil:Expt( + self.AnimParams.Background_CurrentTransparency, + self.AnimParams.Background_TargetTransparency, + self.AnimParams.Background_NormalizedExptValue, + dtScale + ) + + self:AnimGuiObjects() +end + +function methods:SetChannelNameColor(channelName, channelNameColor) + self.ChannelNameColors[channelName] = channelNameColor + if self.GuiObjects.MessageModeTextButton.Text == channelName then + self.GuiObjects.MessageModeTextButton.TextColor3 = channelNameColor + end +end + +function methods:GetChannelNameColor(channelName) + return self.ChannelNameColors[channelName] +end + +--///////////////////////// Constructors +--////////////////////////////////////// + +function module.new(CommandProcessor, ChatWindow) + local obj = setmetatable({}, methods) + + obj.GuiObject = nil + obj.ChatBarParentFrame = nil + obj.TextBox = nil + obj.TextLabel = nil + obj.GuiObjects = {} + obj.eGuiObjectsChanged = Instance.new("BindableEvent") + obj.GuiObjectsChanged = obj.eGuiObjectsChanged.Event + obj.TextBoxConnections = {} + + obj.InCustomState = false + obj.CustomState = nil + + obj.TargetChannel = nil + obj.CommandProcessor = CommandProcessor + obj.ChatWindow = ChatWindow + + obj.TweenPixelsPerSecond = 500 + obj.TargetYSize = 0 + + obj.AnimParams = {} + obj.CalculatingSizeLock = false + + obj.ChannelNameColors = {} + + obj.UserHasChatOff = false + + obj:InitializeAnimParams() + + ChatSettings.SettingsChanged:connect(function(setting, value) + if (setting == "ChatBarTextSize") then + obj:SetTextSize(value) + end + end) + + coroutine.wrap(function() + local success, canLocalUserChat = pcall(function() + return Chat:CanUserChatAsync(LocalPlayer.UserId) + end) + local canChat = success and (RunService:IsStudio() or canLocalUserChat) + if canChat == false then + obj.UserHasChatOff = true + obj:DoLockChatBar() + end + end)() + + + return obj +end + +return module diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatChannel.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatChannel.lua new file mode 100644 index 0000000..5c9aa10 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatChannel.lua @@ -0,0 +1,166 @@ +-- // FileName: ChatChannel.lua +-- // Written by: Xsitsu +-- // Description: ChatChannel class for handling messages being added and removed from the chat channel. + +local module = {} +--////////////////////////////// Include +--////////////////////////////////////// +local Chat = game:GetService("Chat") +local clientChatModules = Chat:WaitForChild("ClientChatModules") +local modulesFolder = script.Parent + +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) + +--////////////////////////////// Methods +--////////////////////////////////////// +local methods = {} +methods.__index = methods + +function methods:Destroy() + self.Destroyed = true +end + +function methods:SetActive(active) + if active == self.Active then + return + end + if active == false then + self.MessageLogDisplay:Clear() + else + self.MessageLogDisplay:SetCurrentChannelName(self.Name) + for i = 1, #self.MessageLog do + self.MessageLogDisplay:AddMessage(self.MessageLog[i]) + end + end + self.Active = active +end + +function methods:UpdateMessageFiltered(messageData) + local searchIndex = 1 + local searchTable = self.MessageLog + local messageObj = nil + while (#searchTable >= searchIndex) do + local obj = searchTable[searchIndex] + + if (obj.ID == messageData.ID) then + messageObj = obj + break + end + + searchIndex = searchIndex + 1 + end + + if messageObj then + messageObj.Message = messageData.Message + messageObj.IsFiltered = true + if self.Active then + self.MessageLogDisplay:UpdateMessageFiltered(messageObj) + end + else + -- We have not seen this filtered message before, but we should still add it to our log. + self:AddMessageToChannelByTimeStamp(messageData) + end +end + +function methods:AddMessageToChannel(messageData) + table.insert(self.MessageLog, messageData) + if self.Active then + self.MessageLogDisplay:AddMessage(messageData) + end + if #self.MessageLog > ChatSettings.MessageHistoryLengthPerChannel then + self:RemoveLastMessageFromChannel() + end +end + +function methods:InternalAddMessageAtTimeStamp(messageData) + for i = 1, #self.MessageLog do + if messageData.Time < self.MessageLog[i].Time then + table.insert(self.MessageLog, i, messageData) + return + end + end + table.insert(self.MessageLog, messageData) +end + +function methods:AddMessagesToChannelByTimeStamp(messageLog, startIndex) + for i = startIndex, #messageLog do + self:InternalAddMessageAtTimeStamp(messageLog[i]) + end + while #self.MessageLog > ChatSettings.MessageHistoryLengthPerChannel do + table.remove(self.MessageLog, 1) + end + if self.Active then + self.MessageLogDisplay:Clear() + for i = 1, #self.MessageLog do + self.MessageLogDisplay:AddMessage(self.MessageLog[i]) + end + end +end + +function methods:AddMessageToChannelByTimeStamp(messageData) + if #self.MessageLog >= 1 then + -- These are the fast cases to evalutate. + if self.MessageLog[1].Time > messageData.Time then + return + elseif messageData.Time >= self.MessageLog[#self.MessageLog].Time then + self:AddMessageToChannel(messageData) + return + end + + for i = 1, #self.MessageLog do + if messageData.Time < self.MessageLog[i].Time then + table.insert(self.MessageLog, i, messageData) + + if #self.MessageLog > ChatSettings.MessageHistoryLengthPerChannel then + self:RemoveLastMessageFromChannel() + end + + if self.Active then + self.MessageLogDisplay:AddMessageAtIndex(messageData, i) + end + + return + end + end + else + self:AddMessageToChannel(messageData) + end +end + +function methods:RemoveLastMessageFromChannel() + table.remove(self.MessageLog, 1) + + if self.Active then + self.MessageLogDisplay:RemoveLastMessage() + end +end + +function methods:ClearMessageLog() + self.MessageLog = {} + + if self.Active then + self.MessageLogDisplay:Clear() + end +end + +function methods:RegisterChannelTab(tab) + self.ChannelTab = tab +end + +--///////////////////////// Constructors +--////////////////////////////////////// + +function module.new(channelName, messageLogDisplay) + local obj = setmetatable({}, methods) + obj.Destroyed = false + obj.Active = false + + obj.MessageLog = {} + obj.MessageLogDisplay = messageLogDisplay + obj.ChannelTab = nil + obj.Name = channelName + + return obj +end + +return module diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatMain.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatMain.lua new file mode 100644 index 0000000..50fb830 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatMain.lua @@ -0,0 +1,1041 @@ +-- // FileName: ChatMain.lua +-- // Written by: Xsitsu +-- // Description: Main module to handle initializing chat window UI and hooking up events to individual UI pieces. + +local moduleApiTable = {} + +--// This section of code waits until all of the necessary RemoteEvents are found in EventFolder. +--// I have to do some weird stuff since people could potentially already have pre-existing +--// things in a folder with the same name, and they may have different class types. +--// I do the useEvents thing and set EventFolder to useEvents so I can have a pseudo folder that +--// the rest of the code can interface with and have the guarantee that the RemoteEvents they want +--// exist with their desired names. + +local FILTER_MESSAGE_TIMEOUT = 60 + +local RunService = game:GetService("RunService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Chat = game:GetService("Chat") +local StarterGui = game:GetService("StarterGui") + +local DefaultChatSystemChatEvents = ReplicatedStorage:WaitForChild("DefaultChatSystemChatEvents") +local EventFolder = ReplicatedStorage:WaitForChild("DefaultChatSystemChatEvents") +local clientChatModules = Chat:WaitForChild("ClientChatModules") +local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants")) +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) +local messageCreatorModules = clientChatModules:WaitForChild("MessageCreatorModules") +local MessageCreatorUtil = require(messageCreatorModules:WaitForChild("Util")) + +local ChatLocalization = nil +pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end) +if ChatLocalization == nil then ChatLocalization = {} function ChatLocalization:Get(key,default) return default end end + +local numChildrenRemaining = 10 -- #waitChildren returns 0 because it's a dictionary +local waitChildren = +{ + OnNewMessage = "RemoteEvent", + OnMessageDoneFiltering = "RemoteEvent", + OnNewSystemMessage = "RemoteEvent", + OnChannelJoined = "RemoteEvent", + OnChannelLeft = "RemoteEvent", + OnMuted = "RemoteEvent", + OnUnmuted = "RemoteEvent", + OnMainChannelSet = "RemoteEvent", + + SayMessageRequest = "RemoteEvent", + GetInitDataRequest = "RemoteFunction", +} +-- waitChildren/EventFolder does not contain all the remote events, because the server version could be older than the client version. +-- In that case it would not create the new events. +-- These events are accessed directly from DefaultChatSystemChatEvents + +local useEvents = {} + +local FoundAllEventsEvent = Instance.new("BindableEvent") + +function TryRemoveChildWithVerifyingIsCorrectType(child) + if (waitChildren[child.Name] and child:IsA(waitChildren[child.Name])) then + waitChildren[child.Name] = nil + useEvents[child.Name] = child + numChildrenRemaining = numChildrenRemaining - 1 + end +end + +for i, child in pairs(EventFolder:GetChildren()) do + TryRemoveChildWithVerifyingIsCorrectType(child) +end + +if (numChildrenRemaining > 0) then + local con = EventFolder.ChildAdded:connect(function(child) + TryRemoveChildWithVerifyingIsCorrectType(child) + if (numChildrenRemaining < 1) then + FoundAllEventsEvent:Fire() + end + end) + + FoundAllEventsEvent.Event:wait() + con:disconnect() + + FoundAllEventsEvent:Destroy() +end + +EventFolder = useEvents + + + +--// Rest of code after waiting for correct events. + +local UserInputService = game:GetService("UserInputService") +local RunService = game:GetService("RunService") + +local Players = game:GetService("Players") +local LocalPlayer = Players.LocalPlayer + +while not LocalPlayer do + Players.ChildAdded:wait() + LocalPlayer = Players.LocalPlayer +end + +local canChat = true + +local ChatDisplayOrder = 6 +if ChatSettings.ScreenGuiDisplayOrder ~= nil then + ChatDisplayOrder = ChatSettings.ScreenGuiDisplayOrder +end + +local PlayerGui = LocalPlayer:WaitForChild("PlayerGui") +local GuiParent = Instance.new("ScreenGui") +GuiParent.Name = "Chat" +GuiParent.ResetOnSpawn = false +GuiParent.DisplayOrder = ChatDisplayOrder +GuiParent.Parent = PlayerGui + +local DidFirstChannelsLoads = false + +local modulesFolder = script + +local moduleChatWindow = require(modulesFolder:WaitForChild("ChatWindow")) +local moduleChatBar = require(modulesFolder:WaitForChild("ChatBar")) +local moduleChannelsBar = require(modulesFolder:WaitForChild("ChannelsBar")) +local moduleMessageLabelCreator = require(modulesFolder:WaitForChild("MessageLabelCreator")) +local moduleMessageLogDisplay = require(modulesFolder:WaitForChild("MessageLogDisplay")) +local moduleChatChannel = require(modulesFolder:WaitForChild("ChatChannel")) +local moduleCommandProcessor = require(modulesFolder:WaitForChild("CommandProcessor")) + +local ChatWindow = moduleChatWindow.new() +local ChannelsBar = moduleChannelsBar.new() +local MessageLogDisplay = moduleMessageLogDisplay.new() +local CommandProcessor = moduleCommandProcessor.new() +local ChatBar = moduleChatBar.new(CommandProcessor, ChatWindow) + +ChatWindow:CreateGuiObjects(GuiParent) + +ChatWindow:RegisterChatBar(ChatBar) +ChatWindow:RegisterChannelsBar(ChannelsBar) +ChatWindow:RegisterMessageLogDisplay(MessageLogDisplay) + +MessageCreatorUtil:RegisterChatWindow(ChatWindow) + +local MessageSender = require(modulesFolder:WaitForChild("MessageSender")) +MessageSender:RegisterSayMessageFunction(EventFolder.SayMessageRequest) + + + +if (UserInputService.TouchEnabled) then + ChatBar:SetTextLabelText(ChatLocalization:Get("GameChat_ChatMain_ChatBarText",'Tap here to chat')) +else + ChatBar:SetTextLabelText(ChatLocalization:Get("GameChat_ChatMain_ChatBarTextTouch",'To chat click here or press "/" key')) +end + +spawn(function() + local CurveUtil = require(modulesFolder:WaitForChild("CurveUtil")) + local animationFps = ChatSettings.ChatAnimationFPS or 20.0 + + local updateWaitTime = 1.0 / animationFps + local lastTick = tick() + while true do + local currentTick = tick() + local tickDelta = currentTick - lastTick + local dtScale = CurveUtil:DeltaTimeToTimescale(tickDelta) + + if dtScale ~= 0 then + ChatWindow:Update(dtScale) + end + + lastTick = currentTick + wait(updateWaitTime) + end +end) + + + + +--//////////////////////////////////////////////////////////////////////////////////////////// +--////////////////////////////////////////////////////////////// Code to do chat window fading +--//////////////////////////////////////////////////////////////////////////////////////////// +function CheckIfPointIsInSquare(checkPos, topLeft, bottomRight) + return (topLeft.X <= checkPos.X and checkPos.X <= bottomRight.X and + topLeft.Y <= checkPos.Y and checkPos.Y <= bottomRight.Y) +end + +local backgroundIsFaded = false +local textIsFaded = false +local lastTextFadeTime = 0 +local lastBackgroundFadeTime = 0 + +local fadedChanged = Instance.new("BindableEvent") +local mouseStateChanged = Instance.new("BindableEvent") +local chatBarFocusChanged = Instance.new("BindableEvent") + +function DoBackgroundFadeIn(setFadingTime) + lastBackgroundFadeTime = tick() + backgroundIsFaded = false + fadedChanged:Fire() + ChatWindow:FadeInBackground((setFadingTime or ChatSettings.ChatDefaultFadeDuration)) + + local currentChannelObject = ChatWindow:GetCurrentChannel() + if (currentChannelObject) then + + local Scroller = MessageLogDisplay.Scroller + Scroller.ScrollingEnabled = true + Scroller.ScrollBarThickness = moduleMessageLogDisplay.ScrollBarThickness + end +end + +function DoBackgroundFadeOut(setFadingTime) + lastBackgroundFadeTime = tick() + backgroundIsFaded = true + fadedChanged:Fire() + ChatWindow:FadeOutBackground((setFadingTime or ChatSettings.ChatDefaultFadeDuration)) + + local currentChannelObject = ChatWindow:GetCurrentChannel() + if (currentChannelObject) then + + local Scroller = MessageLogDisplay.Scroller + Scroller.ScrollingEnabled = false + Scroller.ScrollBarThickness = 0 + end +end + +function DoTextFadeIn(setFadingTime) + lastTextFadeTime = tick() + textIsFaded = false + fadedChanged:Fire() + ChatWindow:FadeInText((setFadingTime or ChatSettings.ChatDefaultFadeDuration) * 0) +end + +function DoTextFadeOut(setFadingTime) + lastTextFadeTime = tick() + textIsFaded = true + fadedChanged:Fire() + ChatWindow:FadeOutText((setFadingTime or ChatSettings.ChatDefaultFadeDuration)) +end + +function DoFadeInFromNewInformation() + DoTextFadeIn() + if ChatSettings.ChatShouldFadeInFromNewInformation then + DoBackgroundFadeIn() + end +end + +function InstantFadeIn() + DoBackgroundFadeIn(0) + DoTextFadeIn(0) +end + +function InstantFadeOut() + DoBackgroundFadeOut(0) + DoTextFadeOut(0) +end + +local mouseIsInWindow = nil +function UpdateFadingForMouseState(mouseState) + mouseIsInWindow = mouseState + + mouseStateChanged:Fire() + + if (ChatBar:IsFocused()) then return end + + if (mouseState) then + DoBackgroundFadeIn() + DoTextFadeIn() + else + DoBackgroundFadeIn() + end +end + + +spawn(function() + while true do + RunService.RenderStepped:wait() + + while (mouseIsInWindow or ChatBar:IsFocused()) do + if (mouseIsInWindow) then + mouseStateChanged.Event:wait() + end + if (ChatBar:IsFocused()) then + chatBarFocusChanged.Event:wait() + end + end + + if (not backgroundIsFaded) then + local timeDiff = tick() - lastBackgroundFadeTime + if (timeDiff > ChatSettings.ChatWindowBackgroundFadeOutTime) then + DoBackgroundFadeOut() + end + + elseif (not textIsFaded) then + local timeDiff = tick() - lastTextFadeTime + if (timeDiff > ChatSettings.ChatWindowTextFadeOutTime) then + DoTextFadeOut() + end + + else + fadedChanged.Event:wait() + + end + + end +end) + +function getClassicChatEnabled() + if ChatSettings.ClassicChatEnabled ~= nil then + return ChatSettings.ClassicChatEnabled + end + return Players.ClassicChat +end + +function getBubbleChatEnabled() + if ChatSettings.BubbleChatEnabled ~= nil then + return ChatSettings.BubbleChatEnabled + end + return Players.BubbleChat +end + +function bubbleChatOnly() + return not getClassicChatEnabled() and getBubbleChatEnabled() +end + +function UpdateMousePosition(mousePos) + if not (moduleApiTable.Visible and moduleApiTable.IsCoreGuiEnabled and (moduleApiTable.TopbarEnabled or ChatSettings.ChatOnWithTopBarOff)) then return end + + if bubbleChatOnly() then + return + end + + local windowPos = ChatWindow.GuiObject.AbsolutePosition + local windowSize = ChatWindow.GuiObject.AbsoluteSize + + local newMouseState = CheckIfPointIsInSquare(mousePos, windowPos, windowPos + windowSize) + if (newMouseState ~= mouseIsInWindow) then + UpdateFadingForMouseState(newMouseState) + end +end + +UserInputService.InputChanged:connect(function(inputObject) + if (inputObject.UserInputType == Enum.UserInputType.MouseMovement) then + local mousePos = Vector2.new(inputObject.Position.X, inputObject.Position.Y) + UpdateMousePosition(mousePos) + end +end) + +UserInputService.TouchTap:connect(function(tapPos, gameProcessedEvent) + UpdateMousePosition(tapPos[1]) +end) + +UserInputService.TouchMoved:connect(function(inputObject, gameProcessedEvent) + local tapPos = Vector2.new(inputObject.Position.X, inputObject.Position.Y) + UpdateMousePosition(tapPos) +end) + +UserInputService.Changed:connect(function(prop) + if prop == "MouseBehavior" then + if UserInputService.MouseBehavior == Enum.MouseBehavior.LockCenter then + local windowPos = ChatWindow.GuiObject.AbsolutePosition + local windowSize = ChatWindow.GuiObject.AbsoluteSize + local screenSize = GuiParent.AbsoluteSize + + local centerScreenIsInWindow = CheckIfPointIsInSquare(screenSize/2, windowPos, windowPos + windowSize) + if centerScreenIsInWindow then + UserInputService.MouseBehavior = Enum.MouseBehavior.Default + end + end + end +end) + +--// Start and stop fading sequences / timers +UpdateFadingForMouseState(true) +UpdateFadingForMouseState(false) + + +--//////////////////////////////////////////////////////////////////////////////////////////// +--///////////// Code to talk to topbar and maintain set/get core backwards compatibility stuff +--//////////////////////////////////////////////////////////////////////////////////////////// +local Util = {} +do + function Util.Signal() + local sig = {} + + local mSignaler = Instance.new('BindableEvent') + + local mArgData = nil + local mArgDataCount = nil + + function sig:fire(...) + mArgData = {...} + mArgDataCount = select('#', ...) + mSignaler:Fire() + end + + function sig:connect(f) + if not f then error("connect(nil)", 2) end + return mSignaler.Event:connect(function() + f(unpack(mArgData, 1, mArgDataCount)) + end) + end + + function sig:wait() + mSignaler.Event:wait() + assert(mArgData, "Missing arg data, likely due to :TweenSize/Position corrupting threadrefs.") + return unpack(mArgData, 1, mArgDataCount) + end + + return sig + end +end + + +function SetVisibility(val) + ChatWindow:SetVisible(val) + moduleApiTable.VisibilityStateChanged:fire(val) + moduleApiTable.Visible = val + + if (moduleApiTable.IsCoreGuiEnabled) then + if (val) then + InstantFadeIn() + else + InstantFadeOut() + end + end +end + +do + moduleApiTable.TopbarEnabled = true + moduleApiTable.MessageCount = 0 + moduleApiTable.Visible = true + moduleApiTable.IsCoreGuiEnabled = true + + function moduleApiTable:ToggleVisibility() + SetVisibility(not ChatWindow:GetVisible()) + end + + function moduleApiTable:SetVisible(visible) + if (ChatWindow:GetVisible() ~= visible) then + SetVisibility(visible) + end + end + + function moduleApiTable:FocusChatBar() + ChatBar:CaptureFocus() + end + + function moduleApiTable:GetVisibility() + return ChatWindow:GetVisible() + end + + function moduleApiTable:GetMessageCount() + return self.MessageCount + end + + function moduleApiTable:TopbarEnabledChanged(enabled) + self.TopbarEnabled = enabled + self.CoreGuiEnabled:fire(game:GetService("StarterGui"):GetCoreGuiEnabled(Enum.CoreGuiType.Chat)) + end + + function moduleApiTable:IsFocused(useWasFocused) + return ChatBar:IsFocused() + end + + moduleApiTable.ChatBarFocusChanged = Util.Signal() + moduleApiTable.VisibilityStateChanged = Util.Signal() + moduleApiTable.MessagesChanged = Util.Signal() + + + moduleApiTable.MessagePosted = Util.Signal() + moduleApiTable.CoreGuiEnabled = Util.Signal() + + moduleApiTable.ChatMakeSystemMessageEvent = Util.Signal() + moduleApiTable.ChatWindowPositionEvent = Util.Signal() + moduleApiTable.ChatWindowSizeEvent = Util.Signal() + moduleApiTable.ChatBarDisabledEvent = Util.Signal() + + + function moduleApiTable:fChatWindowPosition() + return ChatWindow.GuiObject.Position + end + + function moduleApiTable:fChatWindowSize() + return ChatWindow.GuiObject.Size + end + + function moduleApiTable:fChatBarDisabled() + return not ChatBar:GetEnabled() + end + + + + function moduleApiTable:SpecialKeyPressed(key, modifiers) + if (key == Enum.SpecialKey.ChatHotkey) then + if canChat then + DoChatBarFocus() + end + end + end +end + +moduleApiTable.CoreGuiEnabled:connect(function(enabled) + moduleApiTable.IsCoreGuiEnabled = enabled + + enabled = enabled and (moduleApiTable.TopbarEnabled or ChatSettings.ChatOnWithTopBarOff) + + ChatWindow:SetCoreGuiEnabled(enabled) + + if (not enabled) then + ChatBar:ReleaseFocus() + InstantFadeOut() + else + InstantFadeIn() + end +end) + +function trimTrailingSpaces(str) + local lastSpace = #str + while lastSpace > 0 do + --- The pattern ^%s matches whitespace at the start of the string. (Starting from lastSpace) + if str:find("^%s", lastSpace) then + lastSpace = lastSpace - 1 + else + break + end + end + return str:sub(1, lastSpace) +end + +moduleApiTable.ChatMakeSystemMessageEvent:connect(function(valueTable) + if (valueTable["Text"] and type(valueTable["Text"]) == "string") then + while (not DidFirstChannelsLoads) do wait() end + + local channel = ChatSettings.GeneralChannelName + local channelObj = ChatWindow:GetChannel(channel) + + if (channelObj) then + local messageObject = { + ID = -1, + FromSpeaker = nil, + SpeakerUserId = 0, + OriginalChannel = channel, + IsFiltered = true, + MessageLength = string.len(valueTable.Text), + Message = trimTrailingSpaces(valueTable.Text), + MessageType = ChatConstants.MessageTypeSetCore, + Time = os.time(), + ExtraData = valueTable, + } + channelObj:AddMessageToChannel(messageObject) + ChannelsBar:UpdateMessagePostedInChannel(channel) + + moduleApiTable.MessageCount = moduleApiTable.MessageCount + 1 + moduleApiTable.MessagesChanged:fire(moduleApiTable.MessageCount) + end + end +end) + +moduleApiTable.ChatBarDisabledEvent:connect(function(disabled) + if canChat then + ChatBar:SetEnabled(not disabled) + if (disabled) then + ChatBar:ReleaseFocus() + end + end +end) + +moduleApiTable.ChatWindowSizeEvent:connect(function(size) + ChatWindow.GuiObject.Size = size +end) + +moduleApiTable.ChatWindowPositionEvent:connect(function(position) + ChatWindow.GuiObject.Position = position +end) + +--//////////////////////////////////////////////////////////////////////////////////////////// +--///////////////////////////////////////////////// Code to hook client UI up to server events +--//////////////////////////////////////////////////////////////////////////////////////////// + +function DoChatBarFocus() + if (not ChatWindow:GetCoreGuiEnabled()) then return end + if (not ChatBar:GetEnabled()) then return end + + if (not ChatBar:IsFocused() and ChatBar:GetVisible()) then + moduleApiTable:SetVisible(true) + InstantFadeIn() + ChatBar:CaptureFocus() + moduleApiTable.ChatBarFocusChanged:fire(true) + end +end + +chatBarFocusChanged.Event:connect(function(focused) + moduleApiTable.ChatBarFocusChanged:fire(focused) +end) + +function DoSwitchCurrentChannel(targetChannel) + if (ChatWindow:GetChannel(targetChannel)) then + ChatWindow:SwitchCurrentChannel(targetChannel) + end +end + +function SendMessageToSelfInTargetChannel(message, channelName, extraData) + local channelObj = ChatWindow:GetChannel(channelName) + if (channelObj) then + local messageData = + { + ID = -1, + FromSpeaker = nil, + SpeakerUserId = 0, + OriginalChannel = channelName, + IsFiltered = true, + MessageLength = string.len(message), + Message = trimTrailingSpaces(message), + MessageType = ChatConstants.MessageTypeSystem, + Time = os.time(), + ExtraData = extraData, + } + + channelObj:AddMessageToChannel(messageData) + end +end + +function chatBarFocused() + if (not mouseIsInWindow) then + DoBackgroundFadeIn() + if (textIsFaded) then + DoTextFadeIn() + end + end + + chatBarFocusChanged:Fire(true) +end + +--// Event for making player say chat message. +function chatBarFocusLost(enterPressed, inputObject) + DoBackgroundFadeIn() + chatBarFocusChanged:Fire(false) + + if (enterPressed) then + local message = ChatBar:GetTextBox().Text + + if ChatBar:IsInCustomState() then + local customMessage = ChatBar:GetCustomMessage() + if customMessage then + message = customMessage + end + local messageSunk = ChatBar:CustomStateProcessCompletedMessage(message) + ChatBar:ResetCustomState() + if messageSunk then + return + end + end + + message = string.sub(message, 1, ChatSettings.MaximumMessageLength) + + ChatBar:GetTextBox().Text = "" + + if message ~= "" then + --// Sends signal to eventually call Player:Chat() to handle C++ side legacy stuff. + moduleApiTable.MessagePosted:fire(message) + + if not CommandProcessor:ProcessCompletedChatMessage(message, ChatWindow) then + if ChatSettings.DisallowedWhiteSpace then + for i = 1, #ChatSettings.DisallowedWhiteSpace do + if ChatSettings.DisallowedWhiteSpace[i] == "\t" then + message = string.gsub(message, ChatSettings.DisallowedWhiteSpace[i], " ") + else + message = string.gsub(message, ChatSettings.DisallowedWhiteSpace[i], "") + end + end + end + message = string.gsub(message, "\n", "") + message = string.gsub(message, "[ ]+", " ") + + local targetChannel = ChatWindow:GetTargetMessageChannel() + if targetChannel then + MessageSender:SendMessage(message, targetChannel) + else + MessageSender:SendMessage(message, nil) + end + end + end + + end +end + +local ChatBarConnections = {} +function setupChatBarConnections() + for i = 1, #ChatBarConnections do + ChatBarConnections[i]:Disconnect() + end + ChatBarConnections = {} + + local focusLostConnection = ChatBar:GetTextBox().FocusLost:connect(chatBarFocusLost) + table.insert(ChatBarConnections, focusLostConnection) + + local focusGainedConnection = ChatBar:GetTextBox().Focused:connect(chatBarFocused) + table.insert(ChatBarConnections, focusGainedConnection) +end + +setupChatBarConnections() +ChatBar.GuiObjectsChanged:connect(setupChatBarConnections) + +function getEchoMessagesInGeneral() + if ChatSettings.EchoMessagesInGeneralChannel == nil then + return true + end + return ChatSettings.EchoMessagesInGeneralChannel +end + +EventFolder.OnMessageDoneFiltering.OnClientEvent:connect(function(messageData) + if not ChatSettings.ShowUserOwnFilteredMessage then + if messageData.FromSpeaker == LocalPlayer.Name then + return + end + end + + local channelName = messageData.OriginalChannel + local channelObj = ChatWindow:GetChannel(channelName) + if channelObj then + channelObj:UpdateMessageFiltered(messageData) + end + + if getEchoMessagesInGeneral() and ChatSettings.GeneralChannelName and channelName ~= ChatSettings.GeneralChannelName then + local generalChannel = ChatWindow:GetChannel(ChatSettings.GeneralChannelName) + if generalChannel then + generalChannel:UpdateMessageFiltered(messageData) + end + end +end) + +EventFolder.OnNewMessage.OnClientEvent:connect(function(messageData, channelName) + local channelObj = ChatWindow:GetChannel(channelName) + if (channelObj) then + channelObj:AddMessageToChannel(messageData) + + if (messageData.FromSpeaker ~= LocalPlayer.Name) then + ChannelsBar:UpdateMessagePostedInChannel(channelName) + end + + if getEchoMessagesInGeneral() and ChatSettings.GeneralChannelName and channelName ~= ChatSettings.GeneralChannelName then + local generalChannel = ChatWindow:GetChannel(ChatSettings.GeneralChannelName) + if generalChannel then + generalChannel:AddMessageToChannel(messageData) + end + end + + moduleApiTable.MessageCount = moduleApiTable.MessageCount + 1 + moduleApiTable.MessagesChanged:fire(moduleApiTable.MessageCount) + + DoFadeInFromNewInformation() + end +end) + +EventFolder.OnNewSystemMessage.OnClientEvent:connect(function(messageData, channelName) + channelName = channelName or "System" + + local channelObj = ChatWindow:GetChannel(channelName) + if (channelObj) then + channelObj:AddMessageToChannel(messageData) + + ChannelsBar:UpdateMessagePostedInChannel(channelName) + + moduleApiTable.MessageCount = moduleApiTable.MessageCount + 1 + moduleApiTable.MessagesChanged:fire(moduleApiTable.MessageCount) + + DoFadeInFromNewInformation() + + if getEchoMessagesInGeneral() and ChatSettings.GeneralChannelName and channelName ~= ChatSettings.GeneralChannelName then + local generalChannel = ChatWindow:GetChannel(ChatSettings.GeneralChannelName) + if generalChannel then + generalChannel:AddMessageToChannel(messageData) + end + end + else + warn(string.format("Just received system message for channel I'm not in [%s]", channelName)) + end +end) + + +function HandleChannelJoined(channel, welcomeMessage, messageLog, channelNameColor, addHistoryToGeneralChannel, + addWelcomeMessageToGeneralChannel) + if ChatWindow:GetChannel(channel) then + --- If the channel has already been added, remove it first. + ChatWindow:RemoveChannel(channel) + end + + if (channel == ChatSettings.GeneralChannelName) then + DidFirstChannelsLoads = true + end + + if channelNameColor then + ChatBar:SetChannelNameColor(channel, channelNameColor) + end + + local channelObj = ChatWindow:AddChannel(channel) + + if (channelObj) then + if (channel == ChatSettings.GeneralChannelName) then + DoSwitchCurrentChannel(channel) + end + + if (messageLog) then + local startIndex = 1 + if #messageLog > ChatSettings.MessageHistoryLengthPerChannel then + startIndex = #messageLog - ChatSettings.MessageHistoryLengthPerChannel + end + + for i = startIndex, #messageLog do + channelObj:AddMessageToChannel(messageLog[i]) + end + + if getEchoMessagesInGeneral() and addHistoryToGeneralChannel then + if ChatSettings.GeneralChannelName and channel ~= ChatSettings.GeneralChannelName then + local generalChannel = ChatWindow:GetChannel(ChatSettings.GeneralChannelName) + if generalChannel then + generalChannel:AddMessagesToChannelByTimeStamp(messageLog, startIndex) + end + end + end + end + + if (welcomeMessage ~= "") then + local welcomeMessageObject = { + ID = -1, + FromSpeaker = nil, + SpeakerUserId = 0, + OriginalChannel = channel, + IsFiltered = true, + MessageLength = string.len(welcomeMessage), + Message = trimTrailingSpaces(welcomeMessage), + MessageType = ChatConstants.MessageTypeWelcome, + Time = os.time(), + ExtraData = nil, + } + channelObj:AddMessageToChannel(welcomeMessageObject) + + if getEchoMessagesInGeneral() and addWelcomeMessageToGeneralChannel and not ChatSettings.ShowChannelsBar then + if channel ~= ChatSettings.GeneralChannelName then + local generalChannel = ChatWindow:GetChannel(ChatSettings.GeneralChannelName) + if generalChannel then + generalChannel:AddMessageToChannel(welcomeMessageObject) + end + end + end + end + + DoFadeInFromNewInformation() + end + +end + +EventFolder.OnChannelJoined.OnClientEvent:connect(function(channel, welcomeMessage, messageLog, channelNameColor) + HandleChannelJoined(channel, welcomeMessage, messageLog, channelNameColor, false, true) +end) + +EventFolder.OnChannelLeft.OnClientEvent:connect(function(channel) + ChatWindow:RemoveChannel(channel) + + DoFadeInFromNewInformation() +end) + +EventFolder.OnMuted.OnClientEvent:connect(function(channel) + --// Do something eventually maybe? + --// This used to take away the chat bar in channels the player was muted in. + --// We found out this behavior was inconvenient for doing chat commands though. +end) + +EventFolder.OnUnmuted.OnClientEvent:connect(function(channel) + --// Same as above. +end) + +EventFolder.OnMainChannelSet.OnClientEvent:connect(function(channel) + DoSwitchCurrentChannel(channel) +end) + +coroutine.wrap(function() + -- ChannelNameColorUpdated may not exist if the client version is older than the server version. + local ChannelNameColorUpdated = DefaultChatSystemChatEvents:WaitForChild("ChannelNameColorUpdated", 5) + if ChannelNameColorUpdated then + ChannelNameColorUpdated.OnClientEvent:connect(function(channelName, channelNameColor) + ChatBar:SetChannelNameColor(channelName, channelNameColor) + end) + end +end)() + + +--- Interaction with SetCore Player events. + +local PlayerBlockedEvent = nil +local PlayerMutedEvent = nil +local PlayerUnBlockedEvent = nil +local PlayerUnMutedEvent = nil + + +-- This is pcalled because the SetCore methods may not be released yet. +pcall(function() + PlayerBlockedEvent = StarterGui:GetCore("PlayerBlockedEvent") + PlayerMutedEvent = StarterGui:GetCore("PlayerMutedEvent") + PlayerUnBlockedEvent = StarterGui:GetCore("PlayerUnblockedEvent") + PlayerUnMutedEvent = StarterGui:GetCore("PlayerUnmutedEvent") +end) + +function SendSystemMessageToSelf(message) + local currentChannel = ChatWindow:GetCurrentChannel() + + if currentChannel then + local messageData = + { + ID = -1, + FromSpeaker = nil, + SpeakerUserId = 0, + OriginalChannel = currentChannel.Name, + IsFiltered = true, + MessageLength = string.len(message), + Message = trimTrailingSpaces(message), + MessageType = ChatConstants.MessageTypeSystem, + Time = os.time(), + ExtraData = nil, + } + + currentChannel:AddMessageToChannel(messageData) + end +end + +function MutePlayer(player) + local mutePlayerRequest = DefaultChatSystemChatEvents:FindFirstChild("MutePlayerRequest") + if mutePlayerRequest then + return mutePlayerRequest:InvokeServer(player.Name) + end + return false +end + +if PlayerBlockedEvent then + PlayerBlockedEvent.Event:connect(function(player) + if MutePlayer(player) then + SendSystemMessageToSelf( + string.gsub( + ChatLocalization:Get( + "GameChat_ChatMain_SpeakerHasBeenBlocked", + string.format("Speaker '%s' has been blocked.", player.Name) + ), + "{RBX_NAME}",player.Name + ) + ) + end + end) +end + +if PlayerMutedEvent then + PlayerMutedEvent.Event:connect(function(player) + if MutePlayer(player) then + SendSystemMessageToSelf( + string.gsub( + ChatLocalization:Get( + "GameChat_ChatMain_SpeakerHasBeenMuted", + string.format("Speaker '%s' has been muted.", player.Name) + ), + "{RBX_NAME}", player.Name + ) + ) + end + end) +end + +function UnmutePlayer(player) + local unmutePlayerRequest = DefaultChatSystemChatEvents:FindFirstChild("UnMutePlayerRequest") + if unmutePlayerRequest then + return unmutePlayerRequest:InvokeServer(player.Name) + end + return false +end + +if PlayerUnBlockedEvent then + PlayerUnBlockedEvent.Event:connect(function(player) + if UnmutePlayer(player) then + SendSystemMessageToSelf( + string.gsub( + ChatLocalization:Get( + "GameChat_ChatMain_SpeakerHasBeenUnBlocked", + string.format("Speaker '%s' has been unblocked.", player.Name) + ), + "{RBX_NAME}",player.Name + ) + ) + end + end) +end + +if PlayerUnMutedEvent then + PlayerUnMutedEvent.Event:connect(function(player) + if UnmutePlayer(player) then + SendSystemMessageToSelf( + string.gsub( + ChatLocalization:Get( + "GameChat_ChatMain_SpeakerHasBeenUnMuted", + string.format("Speaker '%s' has been unmuted.", player.Name) + ), + "{RBX_NAME}",player.Name + ) + ) + end + end) +end + +-- Get a list of blocked users from the corescripts. +-- Spawned because this method can yeild. +spawn(function() + -- Pcalled because this method is not released on all platforms yet. + if LocalPlayer.UserId > 0 then + pcall(function() + local blockedUserIds = StarterGui:GetCore("GetBlockedUserIds") + if #blockedUserIds > 0 then + local setInitalBlockedUserIds = DefaultChatSystemChatEvents:FindFirstChild("SetBlockedUserIdsRequest") + if setInitalBlockedUserIds then + setInitalBlockedUserIds:FireServer(blockedUserIds) + end + end + end) + end +end) + +spawn(function() + local success, canLocalUserChat = pcall(function() + return Chat:CanUserChatAsync(LocalPlayer.UserId) + end) + if success then + canChat = RunService:IsStudio() or canLocalUserChat + end +end) + +local initData = EventFolder.GetInitDataRequest:InvokeServer() + +-- Handle joining general channel first. +for i, channelData in pairs(initData.Channels) do + if channelData[1] == ChatSettings.GeneralChannelName then + HandleChannelJoined(channelData[1], channelData[2], channelData[3], channelData[4], true, false) + end +end + +for i, channelData in pairs(initData.Channels) do + if channelData[1] ~= ChatSettings.GeneralChannelName then + HandleChannelJoined(channelData[1], channelData[2], channelData[3], channelData[4], true, false) + end +end + +return moduleApiTable diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatScript.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatScript.lua new file mode 100644 index 0000000..eab4378 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatScript.lua @@ -0,0 +1,140 @@ +-- // FileName: ChatScript.lua +-- // Written by: Xsitsu +-- // Description: Hooks main chat module up to Topbar in corescripts. + +local StarterGui = game:GetService("StarterGui") +local GuiService = game:GetService("GuiService") +local ChatService = game:GetService("Chat") + +local MAX_COREGUI_CONNECTION_ATTEMPTS = 10 + +local ClientChatModules = ChatService:WaitForChild("ClientChatModules") +local ChatSettings = require(ClientChatModules:WaitForChild("ChatSettings")) + +local function DoEverything() + local Chat = require(script:WaitForChild("ChatMain")) + + local containerTable = {} + containerTable.ChatWindow = {} + containerTable.SetCore = {} + containerTable.GetCore = {} + + containerTable.ChatWindow.ChatTypes = {} + containerTable.ChatWindow.ChatTypes.BubbleChatEnabled = ChatSettings.BubbleChatEnabled + containerTable.ChatWindow.ChatTypes.ClassicChatEnabled = ChatSettings.ClassicChatEnabled + + --// Connection functions + local function ConnectEvent(name) + local event = Instance.new("BindableEvent") + event.Name = name + containerTable.ChatWindow[name] = event + + event.Event:connect(function(...) Chat[name](Chat, ...) end) + end + + local function ConnectFunction(name) + local func = Instance.new("BindableFunction") + func.Name = name + containerTable.ChatWindow[name] = func + + func.OnInvoke = function(...) return Chat[name](Chat, ...) end + end + + local function ReverseConnectEvent(name) + local event = Instance.new("BindableEvent") + event.Name = name + containerTable.ChatWindow[name] = event + + Chat[name]:connect(function(...) event:Fire(...) end) + end + + local function ConnectSignal(name) + local event = Instance.new("BindableEvent") + event.Name = name + containerTable.ChatWindow[name] = event + + event.Event:connect(function(...) Chat[name]:fire(...) end) + end + + local function ConnectSetCore(name) + local event = Instance.new("BindableEvent") + event.Name = name + containerTable.SetCore[name] = event + + event.Event:connect(function(...) Chat[name.."Event"]:fire(...) end) + end + + local function ConnectGetCore(name) + local func = Instance.new("BindableFunction") + func.Name = name + containerTable.GetCore[name] = func + + func.OnInvoke = function(...) return Chat["f"..name](...) end + end + + --// Do connections + ConnectEvent("ToggleVisibility") + ConnectEvent("SetVisible") + ConnectEvent("FocusChatBar") + ConnectFunction("GetVisibility") + ConnectFunction("GetMessageCount") + ConnectEvent("TopbarEnabledChanged") + ConnectFunction("IsFocused") + + ReverseConnectEvent("ChatBarFocusChanged") + ReverseConnectEvent("VisibilityStateChanged") + ReverseConnectEvent("MessagesChanged") + ReverseConnectEvent("MessagePosted") + + ConnectSignal("CoreGuiEnabled") + + ConnectSetCore("ChatMakeSystemMessage") + ConnectSetCore("ChatWindowPosition") + ConnectSetCore("ChatWindowSize") + ConnectGetCore("ChatWindowPosition") + ConnectGetCore("ChatWindowSize") + ConnectSetCore("ChatBarDisabled") + ConnectGetCore("ChatBarDisabled") + + ConnectEvent("SpecialKeyPressed") + + SetCoreGuiChatConnections(containerTable) +end + +function SetCoreGuiChatConnections(containerTable) + local tries = 0 + while tries < MAX_COREGUI_CONNECTION_ATTEMPTS do + tries = tries + 1 + local success, ret = pcall(function() StarterGui:SetCore("CoreGuiChatConnections", containerTable) end) + if success then + break + end + if not success and tries == MAX_COREGUI_CONNECTION_ATTEMPTS then + error("Error calling SetCore CoreGuiChatConnections: " .. ret) + end + wait() + end +end + +function checkBothChatTypesDisabled() + if ChatSettings.BubbleChatEnabled ~= nil then + if ChatSettings.ClassicChatEnabled ~= nil then + return not (ChatSettings.BubbleChatEnabled or ChatSettings.ClassicChatEnabled) + end + end + return false +end + +if (not GuiService:IsTenFootInterface()) and (not game:GetService('UserInputService').VREnabled) then + if not checkBothChatTypesDisabled() then + DoEverything() + else + local containerTable = {} + containerTable.ChatWindow = {} + + containerTable.ChatWindow.ChatTypes = {} + containerTable.ChatWindow.ChatTypes.BubbleChatEnabled = false + containerTable.ChatWindow.ChatTypes.ClassicChatEnabled = false + SetCoreGuiChatConnections(containerTable) + end +end diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatWindow.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatWindow.lua new file mode 100644 index 0000000..de2c905 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatWindow.lua @@ -0,0 +1,653 @@ +-- // FileName: ChatWindow.lua +-- // Written by: Xsitsu +-- // Description: Main GUI window piece. Manages ChatBar, ChannelsBar, and ChatChannels. + +local module = {} + +local Players = game:GetService("Players") +local LocalPlayer = Players.LocalPlayer +local PlayerGui = LocalPlayer:WaitForChild("PlayerGui") + +local PHONE_SCREEN_WIDTH = 640 +local TABLET_SCREEN_WIDTH = 1024 + +local DEVICE_PHONE = 1 +local DEVICE_TABLET = 2 +local DEVICE_DESKTOP = 3 + +--////////////////////////////// Include +--////////////////////////////////////// +local Chat = game:GetService("Chat") +local clientChatModules = Chat:WaitForChild("ClientChatModules") +local modulesFolder = script.Parent +local moduleChatChannel = require(modulesFolder:WaitForChild("ChatChannel")) +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) +local CurveUtil = require(modulesFolder:WaitForChild("CurveUtil")) + +--////////////////////////////// Methods +--////////////////////////////////////// +local methods = {} +methods.__index = methods + +function getClassicChatEnabled() + if ChatSettings.ClassicChatEnabled ~= nil then + return ChatSettings.ClassicChatEnabled + end + return Players.ClassicChat +end + +function getBubbleChatEnabled() + if ChatSettings.BubbleChatEnabled ~= nil then + return ChatSettings.BubbleChatEnabled + end + return Players.BubbleChat +end + +function bubbleChatOnly() + return not getClassicChatEnabled() and getBubbleChatEnabled() +end + +function methods:CreateGuiObjects(targetParent) + local BaseFrame = Instance.new("Frame") + BaseFrame.BackgroundTransparency = 1 + BaseFrame.Active = ChatSettings.WindowDraggable + BaseFrame.Parent = targetParent + + local ChatBarParentFrame = Instance.new("Frame") + ChatBarParentFrame.Selectable = false + ChatBarParentFrame.Name = "ChatBarParentFrame" + ChatBarParentFrame.BackgroundTransparency = 1 + ChatBarParentFrame.Parent = BaseFrame + + local ChannelsBarParentFrame = Instance.new("Frame") + ChannelsBarParentFrame.Selectable = false + ChannelsBarParentFrame.Name = "ChannelsBarParentFrame" + ChannelsBarParentFrame.BackgroundTransparency = 1 + ChannelsBarParentFrame.Position = UDim2.new(0, 0, 0, 0) + ChannelsBarParentFrame.Parent = BaseFrame + + local ChatChannelParentFrame = Instance.new("Frame") + ChatChannelParentFrame.Selectable = false + ChatChannelParentFrame.Name = "ChatChannelParentFrame" + ChatChannelParentFrame.BackgroundTransparency = 1 + ChatChannelParentFrame.BackgroundColor3 = ChatSettings.BackGroundColor + ChatChannelParentFrame.BackgroundTransparency = 0.6 + ChatChannelParentFrame.BorderSizePixel = 0 + ChatChannelParentFrame.Parent = BaseFrame + + local ChatResizerFrame = Instance.new("ImageButton") + ChatResizerFrame.Selectable = false + ChatResizerFrame.Image = "" + ChatResizerFrame.BackgroundTransparency = 0.6 + ChatResizerFrame.BorderSizePixel = 0 + ChatResizerFrame.Visible = false + ChatResizerFrame.BackgroundColor3 = ChatSettings.BackGroundColor + ChatResizerFrame.Active = true + if bubbleChatOnly() then + ChatResizerFrame.Position = UDim2.new(1, -ChatResizerFrame.AbsoluteSize.X, 0, 0) + else + ChatResizerFrame.Position = UDim2.new(1, -ChatResizerFrame.AbsoluteSize.X, 1, -ChatResizerFrame.AbsoluteSize.Y) + end + ChatResizerFrame.Parent = BaseFrame + + local ResizeIcon = Instance.new("ImageLabel") + ResizeIcon.Selectable = false + ResizeIcon.Size = UDim2.new(0.8, 0, 0.8, 0) + ResizeIcon.Position = UDim2.new(0.2, 0, 0.2, 0) + ResizeIcon.BackgroundTransparency = 1 + ResizeIcon.Image = "rbxassetid://261880743" + ResizeIcon.Parent = ChatResizerFrame + + local function GetScreenGuiParent() + --// Travel up parent list until you find the ScreenGui that the chat window is parented to + local screenGuiParent = BaseFrame + while (screenGuiParent and not screenGuiParent:IsA("ScreenGui")) do + screenGuiParent = screenGuiParent.Parent + end + + return screenGuiParent + end + + + local deviceType = DEVICE_DESKTOP + + local screenGuiParent = GetScreenGuiParent() + if (screenGuiParent.AbsoluteSize.X <= PHONE_SCREEN_WIDTH) then + deviceType = DEVICE_PHONE + + elseif (screenGuiParent.AbsoluteSize.X <= TABLET_SCREEN_WIDTH) then + deviceType = DEVICE_TABLET + + end + + local checkSizeLock = false + local function doCheckSizeBounds() + if (checkSizeLock) then return end + checkSizeLock = true + + if (not BaseFrame:IsDescendantOf(PlayerGui)) then return end + + local screenGuiParent = GetScreenGuiParent() + + local minWinSize = ChatSettings.MinimumWindowSize + local maxWinSize = ChatSettings.MaximumWindowSize + + local forceMinY = ChannelsBarParentFrame.AbsoluteSize.Y + ChatBarParentFrame.AbsoluteSize.Y + + local minSizePixelX = (minWinSize.X.Scale * screenGuiParent.AbsoluteSize.X) + minWinSize.X.Offset + local minSizePixelY = math.max((minWinSize.Y.Scale * screenGuiParent.AbsoluteSize.Y) + minWinSize.Y.Offset, forceMinY) + + local maxSizePixelX = (maxWinSize.X.Scale * screenGuiParent.AbsoluteSize.X) + maxWinSize.X.Offset + local maxSizePixelY = (maxWinSize.Y.Scale * screenGuiParent.AbsoluteSize.Y) + maxWinSize.Y.Offset + + local absSizeX = BaseFrame.AbsoluteSize.X + local absSizeY = BaseFrame.AbsoluteSize.Y + + if (absSizeX < minSizePixelX) then + local offset = UDim2.new(0, minSizePixelX - absSizeX, 0, 0) + BaseFrame.Size = BaseFrame.Size + offset + + elseif (absSizeX > maxSizePixelX) then + local offset = UDim2.new(0, maxSizePixelX - absSizeX, 0, 0) + BaseFrame.Size = BaseFrame.Size + offset + + end + + if (absSizeY < minSizePixelY) then + local offset = UDim2.new(0, 0, 0, minSizePixelY - absSizeY) + BaseFrame.Size = BaseFrame.Size + offset + + elseif (absSizeY > maxSizePixelY) then + local offset = UDim2.new(0, 0, 0, maxSizePixelY - absSizeY) + BaseFrame.Size = BaseFrame.Size + offset + + end + + local xScale = BaseFrame.AbsoluteSize.X / screenGuiParent.AbsoluteSize.X + local yScale = BaseFrame.AbsoluteSize.Y / screenGuiParent.AbsoluteSize.Y + BaseFrame.Size = UDim2.new(xScale, 0, yScale, 0) + + checkSizeLock = false + end + + + BaseFrame.Changed:connect(function(prop) + if (prop == "AbsoluteSize") then + doCheckSizeBounds() + end + end) + + + + ChatResizerFrame.DragBegin:connect(function(startUdim) + BaseFrame.Draggable = false + end) + + local function UpdatePositionFromDrag(atPos) + if ChatSettings.WindowDraggable == false and ChatSettings.WindowResizable == false then + return + end + local newSize = atPos - BaseFrame.AbsolutePosition + ChatResizerFrame.AbsoluteSize + BaseFrame.Size = UDim2.new(0, newSize.X, 0, newSize.Y) + if bubbleChatOnly() then + ChatResizerFrame.Position = UDim2.new(1, -ChatResizerFrame.AbsoluteSize.X, 0, 0) + else + ChatResizerFrame.Position = UDim2.new(1, -ChatResizerFrame.AbsoluteSize.X, 1, -ChatResizerFrame.AbsoluteSize.Y) + end + end + + ChatResizerFrame.DragStopped:connect(function(endX, endY) + BaseFrame.Draggable = ChatSettings.WindowDraggable + --UpdatePositionFromDrag(Vector2.new(endX, endY)) + end) + + local resizeLock = false + ChatResizerFrame.Changed:connect(function(prop) + if (prop == "AbsolutePosition" and not BaseFrame.Draggable) then + if (resizeLock) then return end + resizeLock = true + + UpdatePositionFromDrag(ChatResizerFrame.AbsolutePosition) + + resizeLock = false + end + end) + + local function CalculateChannelsBarPixelSize(textSize) + if (deviceType == DEVICE_PHONE) then + textSize = textSize or ChatSettings.ChatChannelsTabTextSizePhone + else + textSize = textSize or ChatSettings.ChatChannelsTabTextSize + end + + local channelsBarTextYSize = textSize + local chatChannelYSize = math.max(32, channelsBarTextYSize + 8) + 2 + + return chatChannelYSize + end + + local function CalculateChatBarPixelSize(textSize) + if (deviceType == DEVICE_PHONE) then + textSize = textSize or ChatSettings.ChatBarTextSizePhone + else + textSize = textSize or ChatSettings.ChatBarTextSize + end + + local chatBarTextSizeY = textSize + local chatBarYSize = chatBarTextSizeY + (7 * 2) + (5 * 2) + + return chatBarYSize + end + + if bubbleChatOnly() then + ChatBarParentFrame.Position = UDim2.new(0, 0, 0, 0) + ChannelsBarParentFrame.Visible = false + ChannelsBarParentFrame.Active = false + ChatChannelParentFrame.Visible = false + ChatChannelParentFrame.Active = false + + local useXScale = 0 + local useXOffset = 0 + + local screenGuiParent = GetScreenGuiParent() + + if (deviceType == DEVICE_PHONE) then + useXScale = ChatSettings.DefaultWindowSizePhone.X.Scale + useXOffset = ChatSettings.DefaultWindowSizePhone.X.Offset + + elseif (deviceType == DEVICE_TABLET) then + useXScale = ChatSettings.DefaultWindowSizeTablet.X.Scale + useXOffset = ChatSettings.DefaultWindowSizeTablet.X.Offset + + else + useXScale = ChatSettings.DefaultWindowSizeTablet.X.Scale + useXOffset = ChatSettings.DefaultWindowSizeTablet.X.Offset + + end + + local chatBarYSize = CalculateChatBarPixelSize() + + BaseFrame.Size = UDim2.new(useXScale, useXOffset, 0, chatBarYSize) + BaseFrame.Position = ChatSettings.DefaultWindowPosition + + else + + local screenGuiParent = GetScreenGuiParent() + + if (deviceType == DEVICE_PHONE) then + BaseFrame.Size = ChatSettings.DefaultWindowSizePhone + + elseif (deviceType == DEVICE_TABLET) then + BaseFrame.Size = ChatSettings.DefaultWindowSizeTablet + + else + BaseFrame.Size = ChatSettings.DefaultWindowSizeDesktop + + end + + BaseFrame.Position = ChatSettings.DefaultWindowPosition + + end + + if (deviceType == DEVICE_PHONE) then + ChatSettings.ChatWindowTextSize = ChatSettings.ChatWindowTextSizePhone + ChatSettings.ChatChannelsTabTextSize = ChatSettings.ChatChannelsTabTextSizePhone + ChatSettings.ChatBarTextSize = ChatSettings.ChatBarTextSizePhone + end + + local function UpdateDraggable(enabled) + BaseFrame.Active = enabled + BaseFrame.Draggable = enabled + end + + local function UpdateResizable(enabled) + ChatResizerFrame.Visible = enabled + ChatResizerFrame.Draggable = enabled + + local frameSizeY = ChatBarParentFrame.Size.Y.Offset + + if (enabled) then + ChatBarParentFrame.Size = UDim2.new(1, -frameSizeY - 2, 0, frameSizeY) + if not bubbleChatOnly() then + ChatBarParentFrame.Position = UDim2.new(0, 0, 1, -frameSizeY) + end + else + ChatBarParentFrame.Size = UDim2.new(1, 0, 0, frameSizeY) + if not bubbleChatOnly() then + ChatBarParentFrame.Position = UDim2.new(0, 0, 1, -frameSizeY) + end + end + end + + local function UpdateChatChannelParentFrameSize() + local channelsBarSize = CalculateChannelsBarPixelSize() + local chatBarSize = CalculateChatBarPixelSize() + + if (ChatSettings.ShowChannelsBar) then + ChatChannelParentFrame.Size = UDim2.new(1, 0, 1, -(channelsBarSize + chatBarSize + 2 + 2)) + ChatChannelParentFrame.Position = UDim2.new(0, 0, 0, channelsBarSize + 2) + + else + ChatChannelParentFrame.Size = UDim2.new(1, 0, 1, -(chatBarSize + 2 + 2)) + ChatChannelParentFrame.Position = UDim2.new(0, 0, 0, 2) + + end + end + + local function UpdateChatChannelsTabTextSize(size) + local channelsBarSize = CalculateChannelsBarPixelSize(size) + ChannelsBarParentFrame.Size = UDim2.new(1, 0, 0, channelsBarSize) + + UpdateChatChannelParentFrameSize() + end + + local function UpdateChatBarTextSize(size) + local chatBarSize = CalculateChatBarPixelSize(size) + + ChatBarParentFrame.Size = UDim2.new(1, 0, 0, chatBarSize) + if not bubbleChatOnly() then + ChatBarParentFrame.Position = UDim2.new(0, 0, 1, -chatBarSize) + end + + ChatResizerFrame.Size = UDim2.new(0, chatBarSize, 0, chatBarSize) + ChatResizerFrame.Position = UDim2.new(1, -chatBarSize, 1, -chatBarSize) + + UpdateChatChannelParentFrameSize() + UpdateResizable(ChatSettings.WindowResizable) + end + + local function UpdateShowChannelsBar(enabled) + ChannelsBarParentFrame.Visible = enabled + UpdateChatChannelParentFrameSize() + end + + UpdateChatChannelsTabTextSize(ChatSettings.ChatChannelsTabTextSize) + UpdateChatBarTextSize(ChatSettings.ChatBarTextSize) + UpdateDraggable(ChatSettings.WindowDraggable) + UpdateResizable(ChatSettings.WindowResizable) + UpdateShowChannelsBar(ChatSettings.ShowChannelsBar) + + ChatSettings.SettingsChanged:connect(function(setting, value) + if (setting == "WindowDraggable") then + UpdateDraggable(value) + + elseif (setting == "WindowResizable") then + UpdateResizable(value) + + elseif (setting == "ChatChannelsTabTextSize") then + UpdateChatChannelsTabTextSize(value) + + elseif (setting == "ChatBarTextSize") then + UpdateChatBarTextSize(value) + + elseif (setting == "ShowChannelsBar") then + UpdateShowChannelsBar(value) + + end + end) + + self.GuiObject = BaseFrame + + self.GuiObjects.BaseFrame = BaseFrame + self.GuiObjects.ChatBarParentFrame = ChatBarParentFrame + self.GuiObjects.ChannelsBarParentFrame = ChannelsBarParentFrame + self.GuiObjects.ChatChannelParentFrame = ChatChannelParentFrame + self.GuiObjects.ChatResizerFrame = ChatResizerFrame + self.GuiObjects.ResizeIcon = ResizeIcon + self:AnimGuiObjects() +end + +function methods:GetChatBar() + return self.ChatBar +end + +function methods:RegisterChatBar(ChatBar) + self.ChatBar = ChatBar + self.ChatBar:CreateGuiObjects(self.GuiObjects.ChatBarParentFrame) +end + +function methods:RegisterChannelsBar(ChannelsBar) + self.ChannelsBar = ChannelsBar + self.ChannelsBar:CreateGuiObjects(self.GuiObjects.ChannelsBarParentFrame) +end + +function methods:RegisterMessageLogDisplay(MessageLogDisplay) + self.MessageLogDisplay = MessageLogDisplay + self.MessageLogDisplay.GuiObject.Parent = self.GuiObjects.ChatChannelParentFrame +end + +function methods:AddChannel(channelName) + if (self:GetChannel(channelName)) then + error("Channel '" .. channelName .. "' already exists!") + return + end + + local channel = moduleChatChannel.new(channelName, self.MessageLogDisplay) + self.Channels[channelName:lower()] = channel + + channel:SetActive(false) + + local tab = self.ChannelsBar:AddChannelTab(channelName) + tab.NameTag.MouseButton1Click:connect(function() + self:SwitchCurrentChannel(channelName) + end) + + channel:RegisterChannelTab(tab) + + return channel +end + +function methods:GetFirstChannel() + --// Channels are not indexed numerically, so this function is necessary. + --// Grabs and returns the first channel it happens to, or nil if none exist. + for i, v in pairs(self.Channels) do + return v + end + return nil +end + +function methods:RemoveChannel(channelName) + if (not self:GetChannel(channelName)) then + error("Channel '" .. channelName .. "' does not exist!") + end + + local indexName = channelName:lower() + + local needsChannelSwitch = false + if (self.Channels[indexName] == self:GetCurrentChannel()) then + needsChannelSwitch = true + + self:SwitchCurrentChannel(nil) + end + + self.Channels[indexName]:Destroy() + self.Channels[indexName] = nil + + self.ChannelsBar:RemoveChannelTab(channelName) + + if (needsChannelSwitch) then + local generalChannelExists = (self:GetChannel(ChatSettings.GeneralChannelName) ~= nil) + local removingGeneralChannel = (indexName == ChatSettings.GeneralChannelName:lower()) + + local targetSwitchChannel = nil + + if (generalChannelExists and not removingGeneralChannel) then + targetSwitchChannel = ChatSettings.GeneralChannelName + else + local firstChannel = self:GetFirstChannel() + targetSwitchChannel = (firstChannel and firstChannel.Name or nil) + end + + self:SwitchCurrentChannel(targetSwitchChannel) + end + + if not ChatSettings.ShowChannelsBar then + if self.ChatBar.TargetChannel == channelName then + self.ChatBar:SetChannelTarget(ChatSettings.GeneralChannelName) + end + end +end + +function methods:GetChannel(channelName) + return channelName and self.Channels[channelName:lower()] or nil +end + +function methods:GetTargetMessageChannel() + if (not ChatSettings.ShowChannelsBar) then + return self.ChatBar.TargetChannel + else + local curChannel = self:GetCurrentChannel() + return curChannel and curChannel.Name + end +end + +function methods:GetCurrentChannel() + return self.CurrentChannel +end + +function methods:SwitchCurrentChannel(channelName) + if (not ChatSettings.ShowChannelsBar) then + local targ = self:GetChannel(channelName) + if (targ) then + self.ChatBar:SetChannelTarget(targ.Name) + end + + channelName = ChatSettings.GeneralChannelName + end + + local cur = self:GetCurrentChannel() + local new = self:GetChannel(channelName) + if new == nil then + error(string.format("Channel '%s' does not exist.", channelName)) + end + + if (new ~= cur) then + if (cur) then + cur:SetActive(false) + local tab = self.ChannelsBar:GetChannelTab(cur.Name) + tab:SetActive(false) + end + + if (new) then + new:SetActive(true) + local tab = self.ChannelsBar:GetChannelTab(new.Name) + tab:SetActive(true) + end + + self.CurrentChannel = new + end + +end + +function methods:UpdateFrameVisibility() + self.GuiObject.Visible = (self.Visible and self.CoreGuiEnabled) +end + +function methods:GetVisible() + return self.Visible +end + +function methods:SetVisible(visible) + self.Visible = visible + self:UpdateFrameVisibility() +end + +function methods:GetCoreGuiEnabled() + return self.CoreGuiEnabled +end + +function methods:SetCoreGuiEnabled(enabled) + self.CoreGuiEnabled = enabled + self:UpdateFrameVisibility() +end + +function methods:EnableResizable() + self.GuiObjects.ChatResizerFrame.Active = true +end + +function methods:DisableResizable() + self.GuiObjects.ChatResizerFrame.Active = false +end + +function methods:FadeOutBackground(duration) + self.ChannelsBar:FadeOutBackground(duration) + self.MessageLogDisplay:FadeOutBackground(duration) + self.ChatBar:FadeOutBackground(duration) + + self.AnimParams.Background_TargetTransparency = 1 + self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) +end + +function methods:FadeInBackground(duration) + self.ChannelsBar:FadeInBackground(duration) + self.MessageLogDisplay:FadeInBackground(duration) + self.ChatBar:FadeInBackground(duration) + + self.AnimParams.Background_TargetTransparency = 0.6 + self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) +end + +function methods:FadeOutText(duration) + self.MessageLogDisplay:FadeOutText(duration) + self.ChannelsBar:FadeOutText(duration) +end + +function methods:FadeInText(duration) + self.MessageLogDisplay:FadeInText(duration) + self.ChannelsBar:FadeInText(duration) +end + +function methods:AnimGuiObjects() + self.GuiObjects.ChatChannelParentFrame.BackgroundTransparency = self.AnimParams.Background_CurrentTransparency + self.GuiObjects.ChatResizerFrame.BackgroundTransparency = self.AnimParams.Background_CurrentTransparency + self.GuiObjects.ResizeIcon.ImageTransparency = self.AnimParams.Background_CurrentTransparency +end + +function methods:InitializeAnimParams() + self.AnimParams.Background_TargetTransparency = 0.6 + self.AnimParams.Background_CurrentTransparency = 0.6 + self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(0) +end + +function methods:Update(dtScale) + self.ChatBar:Update(dtScale) + self.ChannelsBar:Update(dtScale) + self.MessageLogDisplay:Update(dtScale) + + self.AnimParams.Background_CurrentTransparency = CurveUtil:Expt( + self.AnimParams.Background_CurrentTransparency, + self.AnimParams.Background_TargetTransparency, + self.AnimParams.Background_NormalizedExptValue, + dtScale + ) + + self:AnimGuiObjects() +end + +--///////////////////////// Constructors +--////////////////////////////////////// + +function module.new() + local obj = setmetatable({}, methods) + + obj.GuiObject = nil + obj.GuiObjects = {} + + obj.ChatBar = nil + obj.ChannelsBar = nil + obj.MessageLogDisplay = nil + + obj.Channels = {} + obj.CurrentChannel = nil + + obj.Visible = true + obj.CoreGuiEnabled = true + + obj.AnimParams = {} + + obj:InitializeAnimParams() + + return obj +end + +return module diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatWindowInstaller.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatWindowInstaller.lua new file mode 100644 index 0000000..3c33159 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/ChatWindowInstaller.lua @@ -0,0 +1,173 @@ +local runnerScriptName = "ChatScript" +local bubbleChatScriptName = "BubbleChat" +local installDirectory = game:GetService("Chat") + +local PlayersService = game:GetService("Players") +local StarterPlayerScripts = game:GetService("StarterPlayer"):WaitForChild("StarterPlayerScripts") + +local function LoadLocalScript(location, name, parent) + local originalModule = location:WaitForChild(name) + local script = Instance.new("LocalScript") + script.Name = name + script.Source = originalModule.Source + script.Parent = parent + return script +end + +local function LoadModule(location, name, parent) + local originalModule = location:WaitForChild(name) + local module = Instance.new("ModuleScript") + module.Name = name + module.Source = originalModule.Source + module.Parent = parent + return module +end + +local function GetBoolValue(parent, name, defaultValue) + local boolValue = parent:FindFirstChild(name) + if boolValue then + if boolValue:IsA("BoolValue") then + return boolValue.Value + end + end + return defaultValue +end + +local function Install() + local chatScriptArchivable = true + local ChatScript = installDirectory:FindFirstChild(runnerScriptName) + if not ChatScript then + chatScriptArchivable = false + ChatScript = LoadLocalScript(script.Parent, runnerScriptName, installDirectory) + local ChatMain = LoadModule(script.Parent, "ChatMain", ChatScript) + + LoadModule(script.Parent, "ChannelsBar", ChatMain) + LoadModule(script.Parent, "ChatBar", ChatMain) + LoadModule(script.Parent, "ChatChannel", ChatMain) + LoadModule(script.Parent, "MessageLogDisplay", ChatMain) + LoadModule(script.Parent, "ChatWindow", ChatMain) + LoadModule(script.Parent, "MessageLabelCreator", ChatMain) + LoadModule(script.Parent, "CommandProcessor", ChatMain) + LoadModule(script.Parent, "ChannelsTab", ChatMain) + LoadModule(script.Parent.Parent.Parent.Common, "ObjectPool", ChatMain) + LoadModule(script.Parent, "MessageSender", ChatMain) + LoadModule(script.Parent, "CurveUtil", ChatMain) + end + + local bubbleChatScriptArchivable = true + local BubbleChatScript = installDirectory:FindFirstChild(bubbleChatScriptName) + if not BubbleChatScript then + bubbleChatScriptArchivable = false + BubbleChatScript = LoadLocalScript(script.Parent.BubbleChat, bubbleChatScriptName, installDirectory) + end + + local clientChatModules = installDirectory:FindFirstChild("ClientChatModules") + if not clientChatModules then + clientChatModules = Instance.new("Folder") + clientChatModules.Name = "ClientChatModules" + clientChatModules.Archivable = false + + clientChatModules.Parent = installDirectory + end + + local chatSettings = clientChatModules:FindFirstChild("ChatSettings") + if not chatSettings then + LoadModule(script.Parent.DefaultClientChatModules, "ChatSettings", clientChatModules) + end + + local chatConstants = clientChatModules:FindFirstChild("ChatConstants") + if not chatConstants then + LoadModule(script.Parent.DefaultClientChatModules, "ChatConstants", clientChatModules) + end + + local ChatLocalization = clientChatModules:FindFirstChild("ChatLocalization") + if not ChatLocalization then + LoadModule(script.Parent.DefaultClientChatModules, "ChatLocalization", clientChatModules) + end + + local MessageCreatorModules = clientChatModules:FindFirstChild("MessageCreatorModules") + if not MessageCreatorModules then + MessageCreatorModules = Instance.new("Folder") + MessageCreatorModules.Name = "MessageCreatorModules" + MessageCreatorModules.Archivable = false + + local InsertDefaults = Instance.new("BoolValue") + InsertDefaults.Name = "InsertDefaultModules" + InsertDefaults.Value = true + InsertDefaults.Parent = MessageCreatorModules + + MessageCreatorModules.Parent = clientChatModules + end + + local insertDefaultMessageCreators = GetBoolValue(MessageCreatorModules, "InsertDefaultModules", false) + + if insertDefaultMessageCreators then + local creatorModules = script.Parent.DefaultClientChatModules.MessageCreatorModules:GetChildren() + for i = 1, #creatorModules do + if not MessageCreatorModules:FindFirstChild(creatorModules[i].Name) then + LoadModule(script.Parent.DefaultClientChatModules.MessageCreatorModules, creatorModules[i].Name, MessageCreatorModules) + end + end + end + + local CommandModules = clientChatModules:FindFirstChild("CommandModules") + if not CommandModules then + CommandModules = Instance.new("Folder") + CommandModules.Name = "CommandModules" + CommandModules.Archivable = false + + local InsertDefaults = Instance.new("BoolValue") + InsertDefaults.Name = "InsertDefaultModules" + InsertDefaults.Value = true + InsertDefaults.Parent = CommandModules + + CommandModules.Parent = clientChatModules + end + + local insertDefaultCommands = GetBoolValue(CommandModules, "InsertDefaultModules", false) + + if insertDefaultCommands then + local commandModules = script.Parent.DefaultClientChatModules.CommandModules:GetChildren() + for i = 1, #commandModules do + if not CommandModules:FindFirstChild(commandModules[i].Name) then + LoadModule(script.Parent.DefaultClientChatModules.CommandModules, commandModules[i].Name, CommandModules) + end + end + end + + if not StarterPlayerScripts:FindFirstChild(runnerScriptName) then + local ChatScriptCopy = ChatScript:Clone() + ChatScriptCopy.Parent = StarterPlayerScripts + ChatScriptCopy.Archivable = false + + local currentPlayers = PlayersService:GetPlayers() + for i, player in pairs(currentPlayers) do + if (player:IsA("Player") and player:FindFirstChild("PlayerScripts") and not player.PlayerScripts:FindFirstChild(runnerScriptName)) then + ChatScriptCopy = ChatScript:Clone() + ChatScriptCopy.Parent = player.PlayerScripts + ChatScriptCopy.Archivable = false + end + end + end + + ChatScript.Archivable = chatScriptArchivable + + if not StarterPlayerScripts:FindFirstChild(bubbleChatScriptName) then + local BubbleChatScriptCopy = BubbleChatScript:Clone() + BubbleChatScriptCopy.Parent = StarterPlayerScripts + BubbleChatScriptCopy.Archivable = false + + local currentPlayers = PlayersService:GetPlayers() + for i, player in pairs(currentPlayers) do + if (player:IsA("Player") and player:FindFirstChild("PlayerScripts") and not player.PlayerScripts:FindFirstChild(bubbleChatScriptName)) then + BubbleChatScriptCopy = BubbleChatScript:Clone() + BubbleChatScriptCopy.Parent = player.PlayerScripts + BubbleChatScriptCopy.Archivable = false + end + end + end + + BubbleChatScript.Archivable = bubbleChatScriptArchivable +end + +return Install diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/CommandProcessor.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/CommandProcessor.lua new file mode 100644 index 0000000..a66c30e --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/CommandProcessor.lua @@ -0,0 +1,70 @@ +-- // FileName: ProcessCommands.lua +-- // Written by: TheGamer101 +-- // Description: Module for processing commands using the client CommandModules + +local module = {} +local methods = {} +methods.__index = methods + +--////////////////////////////// Include +--////////////////////////////////////// +local Chat = game:GetService("Chat") +local clientChatModules = Chat:WaitForChild("ClientChatModules") +local commandModules = clientChatModules:WaitForChild("CommandModules") +local commandUtil = require(commandModules:WaitForChild("Util")) +local modulesFolder = script.Parent +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) + +function methods:SetupCommandProcessors() + local commands = commandModules:GetChildren() + for i = 1, #commands do + if commands[i]:IsA("ModuleScript") then + if commands[i].Name ~= "Util" then + local commandProcessor = require(commands[i]) + local processorType = commandProcessor[commandUtil.KEY_COMMAND_PROCESSOR_TYPE] + local processorFunction = commandProcessor[commandUtil.KEY_PROCESSOR_FUNCTION] + if processorType == commandUtil.IN_PROGRESS_MESSAGE_PROCESSOR then + table.insert(self.InProgressMessageProcessors, processorFunction) + elseif processorType == commandUtil.COMPLETED_MESSAGE_PROCESSOR then + table.insert(self.CompletedMessageProcessors, processorFunction) + end + end + end + end +end + +function methods:ProcessCompletedChatMessage(message, ChatWindow) + for i = 1, #self.CompletedMessageProcessors do + local processedCommand = self.CompletedMessageProcessors[i](message, ChatWindow, ChatSettings) + if processedCommand then + return true + end + end + return false +end + +function methods:ProcessInProgressChatMessage(message, ChatWindow, ChatBar) + for i = 1, #self.InProgressMessageProcessors do + local customState = self.InProgressMessageProcessors[i](message, ChatWindow, ChatBar, ChatSettings) + if customState then + return customState + end + end + return nil +end + +--///////////////////////// Constructors +--////////////////////////////////////// + +function module.new() + local obj = setmetatable({}, methods) + + obj.CompletedMessageProcessors = {} + obj.InProgressMessageProcessors = {} + + obj:SetupCommandProcessors() + + return obj +end + +return module diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/CurveUtil.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/CurveUtil.lua new file mode 100644 index 0000000..b17bb60 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/CurveUtil.lua @@ -0,0 +1,78 @@ +local CurveUtil = { } +local DEFAULT_THRESHOLD = 0.01 + +function CurveUtil:Expt(start, to, pct, dt_scale) + if math.abs(to - start) < DEFAULT_THRESHOLD then + return to + end + + local y = CurveUtil:Expty(start,to,pct,dt_scale) + + --rtv = start + (to - start) * timescaled_friction-- + local delta = (to - start) * y + return start + delta +end + +function CurveUtil:Expty(start, to, pct, dt_scale) + --y = e ^ (-a * timescale)-- + local friction = 1 - pct + local a = -math.log(friction) + return 1 - math.exp(-a * dt_scale) +end + +function CurveUtil:Sign(val) + if val > 0 then + return 1 + elseif val < 0 then + return -1 + else + return 0 + end +end + +function CurveUtil:BezierValForT(p0, p1, p2, p3, t) + local cp0 = (1 - t) * (1 - t) * (1 - t) + local cp1 = 3 * t * (1-t)*(1-t) + local cp2 = 3 * t * t * (1 - t) + local cp3 = t * t * t + return cp0 * p0 + cp1 * p1 + cp2 * p2 + cp3 * p3 +end + +CurveUtil._BezierPt2ForT = { x = 0; y = 0 } +function CurveUtil:BezierPt2ForT( + p0x, p0y, + p1x, p1y, + p2x, p2y, + p3x, p3y, + t) + + CurveUtil._BezierPt2ForT.x = CurveUtil:BezierValForT(p0x,p1x,p2x,p3x,t) + CurveUtil._BezierPt2ForT.y = CurveUtil:BezierValForT(p0y,p1y,p2y,p3y,t) + return CurveUtil._BezierPt2ForT +end + +function CurveUtil:YForPointOf2PtLine(pt1, pt2, x) + --(y - y1)/(x - x1) = m-- + local m = (pt1.y - pt2.y) / (pt1.x - pt2.x) + --y - mx = b-- + local b = pt1.y - m * pt1.x + return m * x + b +end + +function CurveUtil:DeltaTimeToTimescale(s_frame_delta_time) + return s_frame_delta_time / (1.0 / 60.0) +end + +function CurveUtil:SecondsToTick(sec) + return (1 / 60.0) / sec +end + +function CurveUtil:ExptValueInSeconds(threshold, start, seconds) + return 1 - math.pow((threshold / start), 1 / (60.0 * seconds)) +end + +function CurveUtil:NormalizedDefaultExptValueInSeconds(seconds) + return self:ExptValueInSeconds(DEFAULT_THRESHOLD, 1, seconds) +end + +return CurveUtil diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/ChatConstants.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/ChatConstants.lua new file mode 100644 index 0000000..e0fd585 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/ChatConstants.lua @@ -0,0 +1,28 @@ +-- // FileName: ChatConstants.lua +-- // Written by: TheGamer101 +-- // Description: Module for creating chat constants shared between server and client. + +local module = {} + +---[[ Message Types ]] +module.MessageTypeDefault = "Message" +module.MessageTypeSystem = "System" +module.MessageTypeMeCommand = "MeCommand" +module.MessageTypeWelcome = "Welcome" +module.MessageTypeSetCore = "SetCore" +module.MessageTypeWhisper = "Whisper" + +--[[ Version ]] +module.MajorVersion = 0 +module.MinorVersion = 8 +module.BuildVersion = "2018.05.16" +---[[ Command/Filter Priorities ]] +module.VeryLowPriority = -5 +module.LowPriority = 0 +module.StandardPriority = 10 +module.HighPriority = 20 +module.VeryHighPriority = 25 + +module.WhisperChannelPrefix = "To " + +return module diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/ChatLocalization.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/ChatLocalization.lua new file mode 100644 index 0000000..4e9b3eb --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/ChatLocalization.lua @@ -0,0 +1,41 @@ +local LocalizationService = game:GetService("LocalizationService") +local ChatService = game:GetService("Chat") + +local ChatLocalization = { + _hasFetchedLocalization = false, +} + +function ChatLocalization:_getTranslator() + if not self._translator and not self._hasFetchedLocalization then + -- Don't keep retrying if this fails. + self._hasFetchedLocalization = true + + local localizationTable = ChatService:WaitForChild("ChatLocalization", 4) + if localizationTable then + self._translator = localizationTable:GetTranslator(LocalizationService.RobloxLocaleId) + LocalizationService:GetPropertyChangedSignal("RobloxLocaleId"):Connect(function() + -- If RobloxLocaleId changes invalidate the cached Translator. + self._hasFetchedLocalization = false + self._translator = nil + end) + else + warn("Missing ChatLocalization. Chat interface will not be localized.") + end + end + return self._translator +end + +function ChatLocalization:Get(key, default) + local rtv = default + pcall(function() + local translator = self:_getTranslator() + if translator then + rtv = translator:FormatByKey(key) + else + warn("Missing Translator. Used default for", key) + end + end) + return rtv +end + +return ChatLocalization \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/ChatSettings.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/ChatSettings.lua new file mode 100644 index 0000000..93ec6b6 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/ChatSettings.lua @@ -0,0 +1,107 @@ +-- // FileName: ChatSettings.lua +-- // Written by: Xsitsu +-- // Description: Settings module for configuring different aspects of the chat window. + +local PlayersService = game:GetService("Players") + +local clientChatModules = script.Parent +local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants")) + +local module = {} + +---[[ Chat Behaviour Settings ]] +module.WindowDraggable = false +module.WindowResizable = false +module.ShowChannelsBar = false +module.GamepadNavigationEnabled = false +module.ShowUserOwnFilteredMessage = true --Show a user the filtered version of their message rather than the original. +-- Make the chat work when the top bar is off +module.ChatOnWithTopBarOff = false +module.ScreenGuiDisplayOrder = 6 -- The DisplayOrder value for the ScreenGui containing the chat. + +module.ShowFriendJoinNotification = true -- Show a notification in the chat when a players friend joins the game. + +--- Replace with true/false to force the chat type. Otherwise this will default to the setting on the website. +module.BubbleChatEnabled = PlayersService.BubbleChat +module.ClassicChatEnabled = PlayersService.ClassicChat + +---[[ Chat Text Size Settings ]] +module.ChatWindowTextSize = 18 +module.ChatChannelsTabTextSize = 18 +module.ChatBarTextSize = 18 +module.ChatWindowTextSizePhone = 14 +module.ChatChannelsTabTextSizePhone = 18 +module.ChatBarTextSizePhone = 14 + +---[[ Font Settings ]] +module.DefaultFont = Enum.Font.SourceSansBold +module.ChatBarFont = Enum.Font.SourceSansBold + +----[[ Color Settings ]] +module.BackGroundColor = Color3.new(0, 0, 0) +module.DefaultMessageColor = Color3.new(1, 1, 1) +module.DefaultNameColor = Color3.new(1, 1, 1) +module.ChatBarBackGroundColor = Color3.new(0, 0, 0) +module.ChatBarBoxColor = Color3.new(1, 1, 1) +module.ChatBarTextColor = Color3.new(0, 0, 0) +module.ChannelsTabUnselectedColor = Color3.new(0, 0, 0) +module.ChannelsTabSelectedColor = Color3.new(30/255, 30/255, 30/255) +module.DefaultChannelNameColor = Color3.fromRGB(35, 76, 142) +module.WhisperChannelNameColor = Color3.fromRGB(102, 14, 102) +module.ErrorMessageTextColor = Color3.fromRGB(245, 50, 50) + +---[[ Window Settings ]] +module.MinimumWindowSize = UDim2.new(0.3, 0, 0.25, 0) +module.MaximumWindowSize = UDim2.new(1, 0, 1, 0) -- if you change this to be greater than full screen size, weird things start to happen with size/position bounds checking. +module.DefaultWindowPosition = UDim2.new(0, 0, 0, 0) +local extraOffset = (7 * 2) + (5 * 2) -- Extra chatbar vertical offset +module.DefaultWindowSizePhone = UDim2.new(0.5, 0, 0.5, extraOffset) +module.DefaultWindowSizeTablet = UDim2.new(0.4, 0, 0.3, extraOffset) +module.DefaultWindowSizeDesktop = UDim2.new(0.3, 0, 0.25, extraOffset) + +---[[ Fade Out and In Settings ]] +module.ChatWindowBackgroundFadeOutTime = 0.5 --Chat background will fade out after this many seconds. +module.ChatWindowTextFadeOutTime = 30 --Chat text will fade out after this many seconds. +module.ChatDefaultFadeDuration = 0.8 +module.ChatShouldFadeInFromNewInformation = false +module.ChatAnimationFPS = 20.0 + +---[[ Channel Settings ]] +module.GeneralChannelName = "All" -- You can set to nil to turn off echoing to a general channel. +module.EchoMessagesInGeneralChannel = true -- Should messages to channels other than general be echoed into the general channel. +-- Setting this to false should be used with ShowChannelsBar +module.ChannelsBarFullTabSize = 4 -- number of tabs in bar before it starts to scroll +module.MaxChannelNameLength = 12 +--// Although this feature is pretty much ready, it needs some UI design still. +module.RightClickToLeaveChannelEnabled = false +module.MessageHistoryLengthPerChannel = 50 +-- Show the help text for joining and leaving channels. This is not useful unless custom channels have been added. +-- So it is turned off by default. +module.ShowJoinAndLeaveHelpText = false + +---[[ Message Settings ]] +module.MaximumMessageLength = 200 +module.DisallowedWhiteSpace = {"\n", "\r", "\t", "\v", "\f"} +module.ClickOnPlayerNameToWhisper = true +module.ClickOnChannelNameToSetMainChannel = true +module.BubbleChatMessageTypes = {ChatConstants.MessageTypeDefault, ChatConstants.MessageTypeWhisper} + +---[[ Misc Settings ]] +module.WhisperCommandAutoCompletePlayerNames = true + +local ChangedEvent = Instance.new("BindableEvent") + +local proxyTable = setmetatable({}, +{ + __index = function(tbl, index) + return module[index] + end, + __newindex = function(tbl, index, value) + module[index] = value + ChangedEvent:Fire(index, value) + end, +}) + +rawset(proxyTable, "SettingsChanged", ChangedEvent.Event) + +return proxyTable diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/ClearMessages.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/ClearMessages.lua new file mode 100644 index 0000000..b921cd9 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/ClearMessages.lua @@ -0,0 +1,21 @@ +-- // FileName: ClearMessages.lua +-- // Written by: TheGamer101 +-- // Description: Command to clear the message log of the current channel. + +local util = require(script.Parent:WaitForChild("Util")) + +function ProcessMessage(message, ChatWindow, ChatSettings) + if string.sub(message, 1, 4):lower() == "/cls" or string.sub(message, 1, 6):lower() == "/clear" then + local currentChannel = ChatWindow:GetCurrentChannel() + if (currentChannel) then + currentChannel:ClearMessageLog() + end + return true + end + return false +end + +return { + [util.KEY_COMMAND_PROCESSOR_TYPE] = util.COMPLETED_MESSAGE_PROCESSOR, + [util.KEY_PROCESSOR_FUNCTION] = ProcessMessage +} diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/DeveloperConsole.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/DeveloperConsole.lua new file mode 100644 index 0000000..9b1be64 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/DeveloperConsole.lua @@ -0,0 +1,35 @@ +-- // FileName: DeveloperConsole.lua +-- // Written by: TheGamer101 +-- // Description: Command to open or close the developer console. + +local StarterGui = game:GetService("StarterGui") +local util = require(script.Parent:WaitForChild("Util")) + +function ProcessMessage(message, ChatWindow, ChatSettings) + if string.sub(message, 1, 11):lower() == "/newconsole" then + local success, developerConsoleVisible = pcall(function() return StarterGui:GetCore("DevConsoleVisible") end) + if success then + local success, err = pcall(function() StarterGui:SetCore("DevConsoleVisible", not developerConsoleVisible) end) + if not success and err then + print("Error making developer console visible: " ..err) + end + end + return true + elseif string.sub(message, 1, 8):lower() == "/console" then + local success, developerConsoleVisible = pcall(function() return StarterGui:GetCore("DeveloperConsoleVisible") end) + if success then + local success, err = pcall(function() StarterGui:SetCore("DeveloperConsoleVisible", not developerConsoleVisible) end) + if not success and err then + print("Error making developer console visible: " ..err) + end + end + return true + end + return false +end + +return { + [util.KEY_COMMAND_PROCESSOR_TYPE] = util.COMPLETED_MESSAGE_PROCESSOR, + [util.KEY_PROCESSOR_FUNCTION] = ProcessMessage +} + diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/GetVersion.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/GetVersion.lua new file mode 100644 index 0000000..06c2a11 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/GetVersion.lua @@ -0,0 +1,25 @@ +-- // FileName: GetVersion.lua +-- // Written by: spotco +-- // Description: Command to print the chat version. + +local util = require(script.Parent:WaitForChild("Util")) +local ChatConstants = require(script.Parent.Parent:WaitForChild("ChatConstants")) + +local function ProcessMessage(message, ChatWindow, _) + if string.sub(message, 1, 8):lower() == "/version" or string.sub(message, 1, 9):lower() == "/version " then + util:SendSystemMessageToSelf( + string.format("This game is running chat version [%d.%d.%s].", + ChatConstants.MajorVersion, + ChatConstants.MinorVersion, + ChatConstants.BuildVersion), + ChatWindow:GetCurrentChannel(), + {}) + return true + end + return false +end + +return { + [util.KEY_COMMAND_PROCESSOR_TYPE] = util.COMPLETED_MESSAGE_PROCESSOR, + [util.KEY_PROCESSOR_FUNCTION] = ProcessMessage +} \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/SwallowGuestChat.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/SwallowGuestChat.lua new file mode 100644 index 0000000..111bb56 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/SwallowGuestChat.lua @@ -0,0 +1,34 @@ +-- // FileName: SwallowGuestChat.lua +-- // Written by: TheGamer101 +-- // Description: Stop Guests from chatting and give them a message telling them to sign up. +-- // Guests are generally not allowed to chat, so please do not remove this. + +local util = require(script.Parent:WaitForChild("Util")) +local RunService = game:GetService("RunService") + +local ChatLocalization = nil +pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end) +if ChatLocalization == nil then ChatLocalization = {} function ChatLocalization:Get(key,default) return default end end + +function ProcessMessage(message, ChatWindow, ChatSettings) + local LocalPlayer = game:GetService("Players").LocalPlayer + if LocalPlayer and LocalPlayer.UserId < 0 and not RunService:IsStudio() then + + local channelObj = ChatWindow:GetCurrentChannel() + if channelObj then + util:SendSystemMessageToSelf( + ChatLocalization:Get("GameChat_SwallowGuestChat_Message","Create a free account to get access to chat permissions!"), + channelObj, + {} + ) + end + + return true + end + return false +end + +return { + [util.KEY_COMMAND_PROCESSOR_TYPE] = util.COMPLETED_MESSAGE_PROCESSOR, + [util.KEY_PROCESSOR_FUNCTION] = ProcessMessage +} diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/SwitchChannel.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/SwitchChannel.lua new file mode 100644 index 0000000..a0e1c48 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/SwitchChannel.lua @@ -0,0 +1,52 @@ +-- // FileName: ClearMessages.lua +-- // Written by: TheGamer101 +-- // Description: Command to switch channel. + +local util = require(script.Parent:WaitForChild("Util")) + +local ChatLocalization = nil +pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end) +if ChatLocalization == nil then ChatLocalization = { Get = function(key,default) return default end } end + +function ProcessMessage(message, ChatWindow, ChatSettings) + if string.sub(message, 1, 3):lower() ~= "/c " then + return false + end + + local channelName = string.sub(message, 4) + + local targetChannel = ChatWindow:GetChannel(channelName) + if targetChannel then + ChatWindow:SwitchCurrentChannel(channelName) + if not ChatSettings.ShowChannelsBar then + local currentChannel = ChatWindow:GetCurrentChannel() + if currentChannel then + util:SendSystemMessageToSelf( + string.gsub(ChatLocalization:Get( + "GameChat_SwitchChannel_NowInChannel", + string.format("You are now chatting in channel: '%s'", channelName) + ),"{RBX_NAME}",channelName), + targetChannel, {} + ) + end + end + else + local currentChannel = ChatWindow:GetCurrentChannel() + if currentChannel then + util:SendSystemMessageToSelf( + string.gsub(ChatLocalization:Get( + "GameChat_SwitchChannel_NotInChannel", + string.format("You are not in channel: '%s'", channelName) + ),"{RBX_NAME}",channelName), + currentChannel, {ChatColor = Color3.fromRGB(245, 50, 50)} + ) + end + end + + return true +end + +return { + [util.KEY_COMMAND_PROCESSOR_TYPE] = util.COMPLETED_MESSAGE_PROCESSOR, + [util.KEY_PROCESSOR_FUNCTION] = ProcessMessage +} diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/Team.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/Team.lua new file mode 100644 index 0000000..ee1ba26 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/Team.lua @@ -0,0 +1,130 @@ +-- // FileName: Team.lua +-- // Written by: Partixel/TheGamer101 +-- // Description: Team chat bar manipulation. + +local PlayersService = game:GetService("Players") + +local TEAM_COMMANDS = {"/team ", "/t ", "% "} + +function IsTeamCommand(message) + for i = 1, #TEAM_COMMANDS do + local teamCommand = TEAM_COMMANDS[i] + if string.sub(message, 1, teamCommand:len()):lower() == teamCommand then + return true + end + end + return false +end + +local teamStateMethods = {} +teamStateMethods.__index = teamStateMethods + +local util = require(script.Parent:WaitForChild("Util")) + +local TeamCustomState = {} + +function teamStateMethods:EnterTeamChat() + self.TeamChatEntered = true + self.MessageModeButton.Size = UDim2.new(0, 1000, 1, 0) + self.MessageModeButton.Text = "[Team]" + self.MessageModeButton.TextColor3 = self:GetTeamChatColor() + + local xSize = self.MessageModeButton.TextBounds.X + self.MessageModeButton.Size = UDim2.new(0, xSize, 1, 0) + self.TextBox.Size = UDim2.new(1, -xSize, 1, 0) + self.TextBox.Position = UDim2.new(0, xSize, 0, 0) + self.OriginalTeamText = self.TextBox.Text + self.TextBox.Text = " " +end + +function teamStateMethods:TextUpdated() + local newText = self.TextBox.Text + if not self.TeamChatEntered then + if IsTeamCommand(newText) then + self:EnterTeamChat() + end + else + if newText == "" then + self.MessageModeButton.Text = "" + self.MessageModeButton.Size = UDim2.new(0, 0, 0, 0) + self.TextBox.Size = UDim2.new(1, 0, 1, 0) + self.TextBox.Position = UDim2.new(0, 0, 0, 0) + self.TextBox.Text = "" + ---Implement this when setting cursor positon is a thing. + ---self.TextBox.Text = self.OriginalTeamText + self.TeamChatEntered = false + ---Temporary until setting cursor position... + self.ChatBar:ResetCustomState() + self.ChatBar:CaptureFocus() + end + end +end + +function teamStateMethods:GetMessage() + if self.TeamChatEntered then + return "/t " ..self.TextBox.Text + end + return self.TextBox.Text +end + +function teamStateMethods:ProcessCompletedMessage() + return false +end + +function teamStateMethods:Destroy() + self.MessageModeConnection:disconnect() + self.Destroyed = true +end + +function teamStateMethods:GetTeamChatColor() + local LocalPlayer = PlayersService.LocalPlayer + if LocalPlayer.Team then + return LocalPlayer.Team.TeamColor.Color + end + if self.ChatSettings.DefaultChannelNameColor then + return self.ChatSettings.DefaultChannelNameColor + end + return Color3.fromRGB(35, 76, 142) +end + +function TeamCustomState.new(ChatWindow, ChatBar, ChatSettings) + local obj = setmetatable({}, teamStateMethods) + obj.Destroyed = false + obj.ChatWindow = ChatWindow + obj.ChatBar = ChatBar + obj.ChatSettings = ChatSettings + obj.TextBox = ChatBar:GetTextBox() + obj.MessageModeButton = ChatBar:GetMessageModeTextButton() + obj.OriginalTeamText = "" + obj.TeamChatEntered = false + + obj.MessageModeConnection = obj.MessageModeButton.MouseButton1Click:connect(function() + local chatBarText = obj.TextBox.Text + if string.sub(chatBarText, 1, 1) == " " then + chatBarText = string.sub(chatBarText, 2) + end + obj.ChatBar:ResetCustomState() + obj.ChatBar:SetTextBoxText(chatBarText) + obj.ChatBar:CaptureFocus() + end) + + obj:EnterTeamChat() + + return obj +end + +function ProcessMessage(message, ChatWindow, ChatBar, ChatSettings) + if ChatBar.TargetChannel == "Team" then + return + end + + if IsTeamCommand(message) then + return TeamCustomState.new(ChatWindow, ChatBar, ChatSettings) + end + return nil +end + +return { + [util.KEY_COMMAND_PROCESSOR_TYPE] = util.IN_PROGRESS_MESSAGE_PROCESSOR, + [util.KEY_PROCESSOR_FUNCTION] = ProcessMessage +} diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/Util.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/Util.lua new file mode 100644 index 0000000..a24e7b0 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/Util.lua @@ -0,0 +1,63 @@ +-- // FileName: Util.lua +-- // Written by: TheGamer101 +-- // Description: Module for shared code between CommandModules. + +--[[ +Creating a command module: +1) Create a new module inside the CommandModules folder. +2) Create a function that takes a message, the ChatWindow object and the ChatSettings and returns +a bool command processed. +3) Return this function from the module. +--]] + +local clientChatModules = script.Parent.Parent +local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants")) + +local COMMAND_MODULES_VERSION = 1 + +local KEY_COMMAND_PROCESSOR_TYPE = "ProcessorType" +local KEY_PROCESSOR_FUNCTION = "ProcessorFunction" + +---Command types. +---Process a command as it is being typed. This allows for manipulation of the chat bar. +local IN_PROGRESS_MESSAGE_PROCESSOR = 0 +---Simply process a completed message. +local COMPLETED_MESSAGE_PROCESSOR = 1 + +local module = {} +local methods = {} +methods.__index = methods + +function methods:SendSystemMessageToSelf(message, channelObj, extraData) + local messageData = + { + ID = -1, + FromSpeaker = nil, + SpeakerUserId = 0, + OriginalChannel = channelObj.Name, + IsFiltered = true, + MessageLength = string.len(message), + Message = message, + MessageType = ChatConstants.MessageTypeSystem, + Time = os.time(), + ExtraData = extraData, + } + + channelObj:AddMessageToChannel(messageData) +end + +function module.new() + local obj = setmetatable({}, methods) + + obj.COMMAND_MODULES_VERSION = COMMAND_MODULES_VERSION + + obj.KEY_COMMAND_PROCESSOR_TYPE = KEY_COMMAND_PROCESSOR_TYPE + obj.KEY_PROCESSOR_FUNCTION = KEY_PROCESSOR_FUNCTION + + obj.IN_PROGRESS_MESSAGE_PROCESSOR = IN_PROGRESS_MESSAGE_PROCESSOR + obj.COMPLETED_MESSAGE_PROCESSOR = COMPLETED_MESSAGE_PROCESSOR + + return obj +end + +return module.new() diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/Whisper.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/Whisper.lua new file mode 100644 index 0000000..ce40e55 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/CommandModules/Whisper.lua @@ -0,0 +1,174 @@ +-- // FileName: Whisper.lua +-- // Written by: TheGamer101 +-- // Description: Whisper chat bar manipulation. + +local util = require(script.Parent:WaitForChild("Util")) +local ChatSettings = require(script.Parent.Parent:WaitForChild("ChatSettings")) + +local PlayersService = game:GetService("Players") + +local LocalPlayer = PlayersService.LocalPlayer +while LocalPlayer == nil do + PlayersService.ChildAdded:wait() + LocalPlayer = PlayersService.LocalPlayer +end + +local whisperStateMethods = {} +whisperStateMethods.__index = whisperStateMethods + +local WhisperCustomState = {} + +function whisperStateMethods:TrimWhisperCommand(text) + if string.sub(text, 1, 3):lower() == "/w " then + return string.sub(text, 4) + elseif string.sub(text, 1, 9):lower() == "/whisper " then + return string.sub(text, 10) + end + return nil +end + +function whisperStateMethods:TrimWhiteSpace(text) + local newText = string.gsub(text, "%s+", "") + local wasWhitespaceTrimmed = text[#text] == " " + return newText, wasWhitespaceTrimmed +end + +function whisperStateMethods:ShouldAutoCompleteNames() + if ChatSettings.WhisperCommandAutoCompletePlayerNames ~= nil then + return ChatSettings.WhisperCommandAutoCompletePlayerNames + end + return true +end + +function whisperStateMethods:GetWhisperingPlayer(enteredText) + enteredText = enteredText:lower() + local trimmedText = self:TrimWhisperCommand(enteredText) + if trimmedText then + local possiblePlayerName, whitespaceTrimmed = self:TrimWhiteSpace(trimmedText) + local possibleMatches = {} + local players = PlayersService:GetPlayers() + for i = 1, #players do + if players[i] ~= LocalPlayer then + local lowerPlayerName = players[i].Name:lower() + if string.sub(lowerPlayerName, 1, string.len(possiblePlayerName)) == possiblePlayerName then + possibleMatches[players[i]] = players[i].Name:lower() + end + end + end + local matchCount = 0 + local lastMatch = nil + local lastMatchName = nil + for player, playerName in pairs(possibleMatches) do + matchCount = matchCount + 1 + lastMatch = player + lastMatchName = playerName + if playerName == possiblePlayerName and whitespaceTrimmed then + return player + end + end + if matchCount == 1 then + if self:ShouldAutoCompleteNames() then + return lastMatch + elseif lastMatchName == possiblePlayerName then + return lastMatch + end + end + end + return nil +end + +function whisperStateMethods:GetWhisperChanneNameColor() + if self.ChatSettings.WhisperChannelNameColor then + return self.ChatSettings.WhisperChannelNameColor + end + return Color3.fromRGB(102, 14, 102) +end + +function whisperStateMethods:TextUpdated() + local newText = self.TextBox.Text + if not self.PlayerNameEntered then + local player = self:GetWhisperingPlayer(newText) + if player then + self.PlayerNameEntered = true + self.PlayerName = player.Name + + self.MessageModeButton.Size = UDim2.new(0, 1000, 1, 0) + self.MessageModeButton.Text = string.format("[To %s]", player.Name) + self.MessageModeButton.TextColor3 = self:GetWhisperChanneNameColor() + + local xSize = self.MessageModeButton.TextBounds.X + self.MessageModeButton.Size = UDim2.new(0, xSize, 1, 0) + self.TextBox.Size = UDim2.new(1, -xSize, 1, 0) + self.TextBox.Position = UDim2.new(0, xSize, 0, 0) + self.TextBox.Text = " " + end + else + if newText == "" then + self.MessageModeButton.Text = "" + self.MessageModeButton.Size = UDim2.new(0, 0, 0, 0) + self.TextBox.Size = UDim2.new(1, 0, 1, 0) + self.TextBox.Position = UDim2.new(0, 0, 0, 0) + self.TextBox.Text = "" + ---Implement this when setting cursor positon is a thing. + ---self.TextBox.Text = self.OriginalText .. " " .. self.PlayerName + self.PlayerNameEntered = false + ---Temporary until setting cursor position... + self.ChatBar:ResetCustomState() + self.ChatBar:CaptureFocus() + end + end +end + +function whisperStateMethods:GetMessage() + if self.PlayerNameEntered then + return "/w " ..self.PlayerName.. " " ..self.TextBox.Text + end + return self.TextBox.Text +end + +function whisperStateMethods:ProcessCompletedMessage() + return false +end + +function whisperStateMethods:Destroy() + self.MessageModeConnection:disconnect() + self.Destroyed = true +end + +function WhisperCustomState.new(ChatWindow, ChatBar, ChatSettings) + local obj = setmetatable({}, whisperStateMethods) + obj.Destroyed = false + obj.ChatWindow = ChatWindow + obj.ChatBar = ChatBar + obj.ChatSettings = ChatSettings + obj.TextBox = ChatBar:GetTextBox() + obj.MessageModeButton = ChatBar:GetMessageModeTextButton() + obj.OriginalWhisperText = "" + obj.PlayerNameEntered = false + + obj.MessageModeConnection = obj.MessageModeButton.MouseButton1Click:connect(function() + local chatBarText = obj.TextBox.Text + if string.sub(chatBarText, 1, 1) == " " then + chatBarText = string.sub(chatBarText, 2) + end + obj.ChatBar:ResetCustomState() + obj.ChatBar:SetTextBoxText(chatBarText) + obj.ChatBar:CaptureFocus() + end) + + obj:TextUpdated() + + return obj +end + +function ProcessMessage(message, ChatWindow, ChatBar, ChatSettings) + if string.sub(message, 1, 3):lower() == "/w " or string.sub(message, 1, 9):lower() == "/whisper " then + return WhisperCustomState.new(ChatWindow, ChatBar, ChatSettings) + end + return nil +end + +return { + [util.KEY_COMMAND_PROCESSOR_TYPE] = util.IN_PROGRESS_MESSAGE_PROCESSOR, + [util.KEY_PROCESSOR_FUNCTION] = ProcessMessage +} diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/DefaultChatMessage.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/DefaultChatMessage.lua new file mode 100644 index 0000000..bf80609 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/DefaultChatMessage.lua @@ -0,0 +1,112 @@ +-- // FileName: DefaultChatMessage.lua +-- // Written by: TheGamer101 +-- // Description: Create a message label for a standard chat message. + +local clientChatModules = script.Parent.Parent +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) +local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants")) +local util = require(script.Parent:WaitForChild("Util")) + +function CreateMessageLabel(messageData, channelName) + + local fromSpeaker = messageData.FromSpeaker + local message = messageData.Message + + local extraData = messageData.ExtraData or {} + local useFont = extraData.Font or ChatSettings.DefaultFont + local useTextSize = extraData.TextSize or ChatSettings.ChatWindowTextSize + local useNameColor = extraData.NameColor or ChatSettings.DefaultNameColor + local useChatColor = extraData.ChatColor or ChatSettings.DefaultMessageColor + local useChannelColor = extraData.ChannelColor or useChatColor + local tags = extraData.Tags or {} + + local formatUseName = string.format("[%s]:", fromSpeaker) + local speakerNameSize = util:GetStringTextBounds(formatUseName, useFont, useTextSize) + local numNeededSpaces = util:GetNumberOfSpaces(formatUseName, useFont, useTextSize) + 1 + + local BaseFrame, BaseMessage = util:CreateBaseMessage("", useFont, useTextSize, useChatColor) + local NameButton = util:AddNameButtonToBaseMessage(BaseMessage, useNameColor, formatUseName, fromSpeaker) + local ChannelButton = nil + + local guiObjectSpacing = UDim2.new(0, 0, 0, 0) + + if channelName ~= messageData.OriginalChannel then + local formatChannelName = string.format("{%s}", messageData.OriginalChannel) + ChannelButton = util:AddChannelButtonToBaseMessage(BaseMessage, useChannelColor, formatChannelName, messageData.OriginalChannel) + guiObjectSpacing = UDim2.new(0, ChannelButton.Size.X.Offset + util:GetStringTextBounds(" ", useFont, useTextSize).X, 0, 0) + numNeededSpaces = numNeededSpaces + util:GetNumberOfSpaces(formatChannelName, useFont, useTextSize) + 1 + end + + local tagLabels = {} + for i, tag in pairs(tags) do + local tagColor = tag.TagColor or Color3.fromRGB(255, 0, 255) + local tagText = tag.TagText or "???" + local formatTagText = string.format("[%s] ", tagText) + local label = util:AddTagLabelToBaseMessage(BaseMessage, tagColor, formatTagText) + label.Position = guiObjectSpacing + + numNeededSpaces = numNeededSpaces + util:GetNumberOfSpaces(formatTagText, useFont, useTextSize) + guiObjectSpacing = guiObjectSpacing + UDim2.new(0, label.Size.X.Offset, 0, 0) + + table.insert(tagLabels, label) + end + + NameButton.Position = guiObjectSpacing + + local function UpdateTextFunction(messageObject) + if messageData.IsFiltered then + BaseMessage.Text = string.rep(" ", numNeededSpaces) .. messageObject.Message + else + BaseMessage.Text = string.rep(" ", numNeededSpaces) .. string.rep("_", messageObject.MessageLength) + end + end + + UpdateTextFunction(messageData) + + local function GetHeightFunction(xSize) + return util:GetMessageHeight(BaseMessage, BaseFrame, xSize) + end + + local FadeParmaters = {} + FadeParmaters[NameButton] = { + TextTransparency = {FadedIn = 0, FadedOut = 1}, + TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1} + } + + FadeParmaters[BaseMessage] = { + TextTransparency = {FadedIn = 0, FadedOut = 1}, + TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1} + } + + for i, tagLabel in pairs(tagLabels) do + local index = string.format("Tag%d", i) + FadeParmaters[tagLabel] = { + TextTransparency = {FadedIn = 0, FadedOut = 1}, + TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1} + } + end + + if ChannelButton then + FadeParmaters[ChannelButton] = { + TextTransparency = {FadedIn = 0, FadedOut = 1}, + TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1} + } + end + + local FadeInFunction, FadeOutFunction, UpdateAnimFunction = util:CreateFadeFunctions(FadeParmaters) + + return { + [util.KEY_BASE_FRAME] = BaseFrame, + [util.KEY_BASE_MESSAGE] = BaseMessage, + [util.KEY_UPDATE_TEXT_FUNC] = UpdateTextFunction, + [util.KEY_GET_HEIGHT] = GetHeightFunction, + [util.KEY_FADE_IN] = FadeInFunction, + [util.KEY_FADE_OUT] = FadeOutFunction, + [util.KEY_UPDATE_ANIMATION] = UpdateAnimFunction + } +end + +return { + [util.KEY_MESSAGE_TYPE] = ChatConstants.MessageTypeDefault, + [util.KEY_CREATOR_FUNCTION] = CreateMessageLabel +} diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/MeCommandMessage.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/MeCommandMessage.lua new file mode 100644 index 0000000..fde4cbc --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/MeCommandMessage.lua @@ -0,0 +1,72 @@ +-- // FileName: MeCommandMessage.lua +-- // Written by: TheGamer101 +-- // Description: Create a message label for a me command message. + +local clientChatModules = script.Parent.Parent +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) +local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants")) +local util = require(script.Parent:WaitForChild("Util")) + +function CreateMeCommandMessageLabel(messageData, channelName) + local message = messageData.Message + local extraData = messageData.ExtraData or {} + local useFont = extraData.Font or Enum.Font.SourceSansItalic + local useTextSize = extraData.TextSize or ChatSettings.ChatWindowTextSize + local useChatColor = Color3.new(1, 1, 1) + local useChannelColor = extraData.ChannelColor or useChatColor + local numNeededSpaces = 0 + + local BaseFrame, BaseMessage = util:CreateBaseMessage("", useFont, useTextSize, useChatColor) + local ChannelButton = nil + + if channelName ~= messageData.OriginalChannel then + local formatChannelName = string.format("{%s}", messageData.OriginalChannel) + ChannelButton = util:AddChannelButtonToBaseMessage(BaseMessage, useChannelColor, formatChannelName, messageData.OriginalChannel) + numNeededSpaces = util:GetNumberOfSpaces(formatChannelName, useFont, useTextSize) + 1 + end + + local function UpdateTextFunction(messageObject) + if messageData.IsFiltered then + BaseMessage.Text = string.rep(" ", numNeededSpaces) .. messageObject.FromSpeaker .. " " .. string.sub(messageObject.Message, 5) + else + local messageLength = string.len(messageObject.FromSpeaker) + messageObject.MessageLength - 4 + BaseMessage.Text = string.rep(" ", numNeededSpaces) .. string.rep("_", messageLength) + end + end + + UpdateTextFunction(messageData) + + local function GetHeightFunction(xSize) + return util:GetMessageHeight(BaseMessage, BaseFrame, xSize) + end + + local FadeParmaters = {} + FadeParmaters[BaseMessage] = { + TextTransparency = {FadedIn = 0, FadedOut = 1}, + TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1} + } + + if ChannelButton then + FadeParmaters[ChannelButton] = { + TextTransparency = {FadedIn = 0, FadedOut = 1}, + TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1} + } + end + + local FadeInFunction, FadeOutFunction, UpdateAnimFunction = util:CreateFadeFunctions(FadeParmaters) + + return { + [util.KEY_BASE_FRAME] = BaseFrame, + [util.KEY_BASE_MESSAGE] = BaseMessage, + [util.KEY_UPDATE_TEXT_FUNC] = UpdateTextFunction, + [util.KEY_GET_HEIGHT] = GetHeightFunction, + [util.KEY_FADE_IN] = FadeInFunction, + [util.KEY_FADE_OUT] = FadeOutFunction, + [util.KEY_UPDATE_ANIMATION] = UpdateAnimFunction + } +end + +return { + [util.KEY_MESSAGE_TYPE] = ChatConstants.MessageTypeMeCommand, + [util.KEY_CREATOR_FUNCTION] = CreateMeCommandMessageLabel +} diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/SetCoreMessage.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/SetCoreMessage.lua new file mode 100644 index 0000000..2e9421a --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/SetCoreMessage.lua @@ -0,0 +1,45 @@ +-- // FileName: SetCoreMessage.lua +-- // Written by: TheGamer101 +-- // Description: Create a message label for a message created with SetCore(ChatMakeSystemMessage). + +local clientChatModules = script.Parent.Parent +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) +local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants")) +local util = require(script.Parent:WaitForChild("Util")) + +function CreateSetCoreMessageLabel(messageData, channelName) + local message = messageData.Message + local extraData = messageData.ExtraData or {} + local useFont = extraData.Font or ChatSettings.DefaultFont + local useTextSize = extraData.TextSize or ChatSettings.ChatWindowTextSize + local useColor = extraData.Color or ChatSettings.DefaultMessageColor + + local BaseFrame, BaseMessage = util:CreateBaseMessage(message, useFont, useTextSize, useColor) + + local function GetHeightFunction(xSize) + return util:GetMessageHeight(BaseMessage, BaseFrame, xSize) + end + + local FadeParmaters = {} + FadeParmaters[BaseMessage] = { + TextTransparency = {FadedIn = 0, FadedOut = 1}, + TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1} + } + + local FadeInFunction, FadeOutFunction, UpdateAnimFunction = util:CreateFadeFunctions(FadeParmaters) + + return { + [util.KEY_BASE_FRAME] = BaseFrame, + [util.KEY_BASE_MESSAGE] = BaseMessage, + [util.KEY_UPDATE_TEXT_FUNC] = nil, + [util.KEY_GET_HEIGHT] = GetHeightFunction, + [util.KEY_FADE_IN] = FadeInFunction, + [util.KEY_FADE_OUT] = FadeOutFunction, + [util.KEY_UPDATE_ANIMATION] = UpdateAnimFunction + } +end + +return { + [util.KEY_MESSAGE_TYPE] = ChatConstants.MessageTypeSetCore, + [util.KEY_CREATOR_FUNCTION] = CreateSetCoreMessageLabel +} diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/SystemMessage.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/SystemMessage.lua new file mode 100644 index 0000000..504c4c2 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/SystemMessage.lua @@ -0,0 +1,61 @@ +-- // FileName: SystemMessage.lua +-- // Written by: TheGamer101 +-- // Description: Create a message label for a system message. + +local clientChatModules = script.Parent.Parent +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) +local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants")) +local util = require(script.Parent:WaitForChild("Util")) + +function CreateSystemMessageLabel(messageData, channelName) + local message = messageData.Message + local extraData = messageData.ExtraData or {} + local useFont = extraData.Font or ChatSettings.DefaultFont + local useTextSize = extraData.TextSize or ChatSettings.ChatWindowTextSize + local useChatColor = extraData.ChatColor or ChatSettings.DefaultMessageColor + local useChannelColor = extraData.ChannelColor or useChatColor + + local BaseFrame, BaseMessage = util:CreateBaseMessage(message, useFont, useTextSize, useChatColor) + local ChannelButton = nil + + if channelName ~= messageData.OriginalChannel then + local formatChannelName = string.format("{%s}", messageData.OriginalChannel) + ChannelButton = util:AddChannelButtonToBaseMessage(BaseMessage, useChannelColor, formatChannelName, messageData.OriginalChannel) + local numNeededSpaces = util:GetNumberOfSpaces(formatChannelName, useFont, useTextSize) + 1 + BaseMessage.Text = string.rep(" ", numNeededSpaces) .. message + end + + local function GetHeightFunction(xSize) + return util:GetMessageHeight(BaseMessage, BaseFrame, xSize) + end + + local FadeParmaters = {} + FadeParmaters[BaseMessage] = { + TextTransparency = {FadedIn = 0, FadedOut = 1}, + TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1} + } + + if ChannelButton then + FadeParmaters[ChannelButton] = { + TextTransparency = {FadedIn = 0, FadedOut = 1}, + TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1} + } + end + + local FadeInFunction, FadeOutFunction, UpdateAnimFunction = util:CreateFadeFunctions(FadeParmaters) + + return { + [util.KEY_BASE_FRAME] = BaseFrame, + [util.KEY_BASE_MESSAGE] = BaseMessage, + [util.KEY_UPDATE_TEXT_FUNC] = nil, + [util.KEY_GET_HEIGHT] = GetHeightFunction, + [util.KEY_FADE_IN] = FadeInFunction, + [util.KEY_FADE_OUT] = FadeOutFunction, + [util.KEY_UPDATE_ANIMATION] = UpdateAnimFunction + } +end + +return { + [util.KEY_MESSAGE_TYPE] = ChatConstants.MessageTypeSystem, + [util.KEY_CREATOR_FUNCTION] = CreateSystemMessageLabel +} diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/UnknownMessage.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/UnknownMessage.lua new file mode 100644 index 0000000..1867a8d --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/UnknownMessage.lua @@ -0,0 +1,19 @@ +-- // FileName: UnknownMessage.lua +-- // Written by: TheGamer101 +-- // Description: Default handler for message types with no other creator registered. +-- // Just print that there was a message with no creator for now. + +local MESSAGE_TYPE = "UnknownMessage" + +local clientChatModules = script.Parent.Parent +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) +local util = require(script.Parent:WaitForChild("Util")) + +function CreateUnknownMessageLabel(messageData) + print("No message creator for message: " ..messageData.Message) +end + +return { + [util.KEY_MESSAGE_TYPE] = MESSAGE_TYPE, + [util.KEY_CREATOR_FUNCTION] = CreateUnknownMessageLabel +} diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/Util.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/Util.lua new file mode 100644 index 0000000..5a8926b --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/Util.lua @@ -0,0 +1,375 @@ +-- // FileName: Util.lua +-- // Written by: Xsitsu, TheGamer101 +-- // Description: Module for shared code between MessageCreatorModules. + +--[[ +Creating a message creator module: +1) Create a new module inside the MessageCreatorModules folder. +2) Create a function that takes a messageData object and returns: +{ + KEY_BASE_FRAME = BaseFrame, + KEY_BASE_MESSAGE = BaseMessage, + KEY_UPDATE_TEXT_FUNC = function(newMessageObject) ---Function to update the text of the message. + KEY_GET_HEIGHT = function() ---Function to get the height of the message in absolute pixels, + KEY_FADE_IN = function(duration, CurveUtil) ---Function to tell the message to start fading in. + KEY_FADE_OUT = function(duration, CurveUtil) ---Function to tell the message to start fading out. + KEY_UPDATE_ANIMATION = function(dtScale, CurveUtil) ---Update animation function. +} +3) return the following format from the module: +{ + KEY_MESSAGE_TYPE = "Message type this module creates messages for." + KEY_CREATOR_FUNCTION = YourFunctionHere +} +--]] + +local DEFAULT_MESSAGE_CREATOR = "UnknownMessage" +local MESSAGE_CREATOR_MODULES_VERSION = 1 +---Creator Module Object Keys +local KEY_MESSAGE_TYPE = "MessageType" +local KEY_CREATOR_FUNCTION = "MessageCreatorFunc" +---Creator function return object keys +local KEY_BASE_FRAME = "BaseFrame" +local KEY_BASE_MESSAGE = "BaseMessage" +local KEY_UPDATE_TEXT_FUNC = "UpdateTextFunction" +local KEY_GET_HEIGHT = "GetHeightFunction" +local KEY_FADE_IN = "FadeInFunction" +local KEY_FADE_OUT = "FadeOutFunction" +local KEY_UPDATE_ANIMATION = "UpdateAnimFunction" + +local TextService = game:GetService("TextService") + +local Players = game:GetService("Players") +local LocalPlayer = Players.LocalPlayer +while not LocalPlayer do + Players.ChildAdded:wait() + LocalPlayer = Players.LocalPlayer +end + +local clientChatModules = script.Parent.Parent +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) +local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants")) + +local okShouldClipInGameChat, valueShouldClipInGameChat = pcall(function() return UserSettings():IsUserFeatureEnabled("UserShouldClipInGameChat") end) +local shouldClipInGameChat = okShouldClipInGameChat and valueShouldClipInGameChat + +local module = {} +local methods = {} +methods.__index = methods + +function methods:GetStringTextBounds(text, font, textSize, sizeBounds) + sizeBounds = sizeBounds or Vector2.new(10000, 10000) + return TextService:GetTextSize(text, textSize, font, sizeBounds) +end +--// Above was taken directly from Util.GetStringTextBounds() in the old chat corescripts. + +function methods:GetMessageHeight(BaseMessage, BaseFrame, xSize) + xSize = xSize or BaseFrame.AbsoluteSize.X + local textBoundsSize = self:GetStringTextBounds(BaseMessage.Text, BaseMessage.Font, BaseMessage.TextSize, Vector2.new(xSize, 1000)) + return textBoundsSize.Y +end + +function methods:GetNumberOfSpaces(str, font, textSize) + local strSize = self:GetStringTextBounds(str, font, textSize) + local singleSpaceSize = self:GetStringTextBounds(" ", font, textSize) + return math.ceil(strSize.X / singleSpaceSize.X) +end + +function methods:CreateBaseMessage(message, font, textSize, chatColor) + local BaseFrame = self:GetFromObjectPool("Frame") + BaseFrame.Selectable = false + BaseFrame.Size = UDim2.new(1, 0, 0, 18) + BaseFrame.Visible = true + BaseFrame.BackgroundTransparency = 1 + + local messageBorder = 8 + + local BaseMessage = self:GetFromObjectPool("TextLabel") + BaseMessage.Selectable = false + BaseMessage.Size = UDim2.new(1, -(messageBorder + 6), 1, 0) + BaseMessage.Position = UDim2.new(0, messageBorder, 0, 0) + BaseMessage.BackgroundTransparency = 1 + BaseMessage.Font = font + BaseMessage.TextSize = textSize + BaseMessage.TextXAlignment = Enum.TextXAlignment.Left + BaseMessage.TextYAlignment = Enum.TextYAlignment.Top + BaseMessage.TextTransparency = 0 + BaseMessage.TextStrokeTransparency = 0.75 + BaseMessage.TextColor3 = chatColor + BaseMessage.TextWrapped = true + if shouldClipInGameChat then + BaseMessage.ClipsDescendants = true + end + BaseMessage.Text = message + BaseMessage.Visible = true + BaseMessage.Parent = BaseFrame + + return BaseFrame, BaseMessage +end + +function methods:AddNameButtonToBaseMessage(BaseMessage, nameColor, formatName, playerName) + local speakerNameSize = self:GetStringTextBounds(formatName, BaseMessage.Font, BaseMessage.TextSize) + local NameButton = self:GetFromObjectPool("TextButton") + NameButton.Selectable = false + NameButton.Size = UDim2.new(0, speakerNameSize.X, 0, speakerNameSize.Y) + NameButton.Position = UDim2.new(0, 0, 0, 0) + NameButton.BackgroundTransparency = 1 + NameButton.Font = BaseMessage.Font + NameButton.TextSize = BaseMessage.TextSize + NameButton.TextXAlignment = BaseMessage.TextXAlignment + NameButton.TextYAlignment = BaseMessage.TextYAlignment + NameButton.TextTransparency = BaseMessage.TextTransparency + NameButton.TextStrokeTransparency = BaseMessage.TextStrokeTransparency + NameButton.TextColor3 = nameColor + NameButton.Text = formatName + NameButton.Visible = true + NameButton.Parent = BaseMessage + + local clickedConn = NameButton.MouseButton1Click:connect(function() + self:NameButtonClicked(NameButton, playerName) + end) + + local changedConn = nil + changedConn = NameButton.Changed:connect(function(prop) + if prop == "Parent" then + clickedConn:Disconnect() + changedConn:Disconnect() + end + end) + + return NameButton +end + +function methods:AddChannelButtonToBaseMessage(BaseMessage, channelColor, formatChannelName, channelName) + local channelNameSize = self:GetStringTextBounds(formatChannelName, BaseMessage.Font, BaseMessage.TextSize) + local ChannelButton = self:GetFromObjectPool("TextButton") + ChannelButton.Selectable = false + ChannelButton.Size = UDim2.new(0, channelNameSize.X, 0, channelNameSize.Y) + ChannelButton.Position = UDim2.new(0, 0, 0, 0) + ChannelButton.BackgroundTransparency = 1 + ChannelButton.Font = BaseMessage.Font + ChannelButton.TextSize = BaseMessage.TextSize + ChannelButton.TextXAlignment = BaseMessage.TextXAlignment + ChannelButton.TextYAlignment = BaseMessage.TextYAlignment + ChannelButton.TextTransparency = BaseMessage.TextTransparency + ChannelButton.TextStrokeTransparency = BaseMessage.TextStrokeTransparency + ChannelButton.TextColor3 = channelColor + ChannelButton.Text = formatChannelName + ChannelButton.Visible = true + ChannelButton.Parent = BaseMessage + + local clickedConn = ChannelButton.MouseButton1Click:connect(function() + self:ChannelButtonClicked(ChannelButton, channelName) + end) + + local changedConn = nil + changedConn = ChannelButton.Changed:connect(function(prop) + if prop == "Parent" then + clickedConn:Disconnect() + changedConn:Disconnect() + end + end) + + return ChannelButton +end + +function methods:AddTagLabelToBaseMessage(BaseMessage, tagColor, formatTagText) + local tagNameSize = self:GetStringTextBounds(formatTagText, BaseMessage.Font, BaseMessage.TextSize) + local TagLabel = self:GetFromObjectPool("TextLabel") + TagLabel.Selectable = false + TagLabel.Size = UDim2.new(0, tagNameSize.X, 0, tagNameSize.Y) + TagLabel.Position = UDim2.new(0, 0, 0, 0) + TagLabel.BackgroundTransparency = 1 + TagLabel.Font = BaseMessage.Font + TagLabel.TextSize = BaseMessage.TextSize + TagLabel.TextXAlignment = BaseMessage.TextXAlignment + TagLabel.TextYAlignment = BaseMessage.TextYAlignment + TagLabel.TextTransparency = BaseMessage.TextTransparency + TagLabel.TextStrokeTransparency = BaseMessage.TextStrokeTransparency + TagLabel.TextColor3 = tagColor + TagLabel.Text = formatTagText + TagLabel.Visible = true + TagLabel.Parent = BaseMessage + + return TagLabel +end + +function GetWhisperChannelPrefix() + if ChatConstants.WhisperChannelPrefix then + return ChatConstants.WhisperChannelPrefix + end + return "To " +end + +function methods:NameButtonClicked(nameButton, playerName) + if not self.ChatWindow then + return + end + + if ChatSettings.ClickOnPlayerNameToWhisper then + local player = Players:FindFirstChild(playerName) + if player and player ~= LocalPlayer then + local whisperChannel = GetWhisperChannelPrefix() ..playerName + if self.ChatWindow:GetChannel(whisperChannel) then + self.ChatBar:ResetCustomState() + local targetChannelName = self.ChatWindow:GetTargetMessageChannel() + if targetChannelName ~= whisperChannel then + self.ChatWindow:SwitchCurrentChannel(whisperChannel) + end + + local whisperMessage = "/w " ..playerName + self.ChatBar:SetText(whisperMessage) + + self.ChatBar:CaptureFocus() + elseif not self.ChatBar:IsInCustomState() then + local whisperMessage = "/w " ..playerName + self.ChatBar:SetText(whisperMessage) + + self.ChatBar:CaptureFocus() + end + end + end +end + +function methods:ChannelButtonClicked(channelButton, channelName) + if not self.ChatWindow then + return + end + + if ChatSettings.ClickOnChannelNameToSetMainChannel then + if self.ChatWindow:GetChannel(channelName) then + self.ChatBar:ResetCustomState() + local targetChannelName = self.ChatWindow:GetTargetMessageChannel() + if targetChannelName ~= channelName then + self.ChatWindow:SwitchCurrentChannel(channelName) + end + self.ChatBar:ResetText() + self.ChatBar:CaptureFocus() + end + end +end + +function methods:RegisterChatWindow(chatWindow) + self.ChatWindow = chatWindow + self.ChatBar = chatWindow:GetChatBar() +end + +function methods:GetFromObjectPool(className) + if self.ObjectPool == nil then + return Instance.new(className) + end + return self.ObjectPool:GetInstance(className) +end + +function methods:RegisterObjectPool(objectPool) + self.ObjectPool = objectPool +end + +-- CreateFadeFunctions usage: +-- fadeObjects is a map of text labels and button to start and end values for a given property. +-- e.g { +-- NameButton = { +-- TextTransparency = { +-- FadedIn = 0.5, +-- FadedOut = 1, +-- } +-- }, +-- ImageOne = { +-- ImageTransparency = { +-- FadedIn = 0, +-- FadedOut = 0.5, +-- } +-- } +-- } +function methods:CreateFadeFunctions(fadeObjects) + local AnimParams = {} + for object, properties in pairs(fadeObjects) do + AnimParams[object] = {} + for property, values in pairs(properties) do + AnimParams[object][property] = { + Target = values.FadedIn, + Current = object[property], + NormalizedExptValue = 1, + } + end + end + + local function FadeInFunction(duration, CurveUtil) + for object, properties in pairs(AnimParams) do + for property, values in pairs(properties) do + values.Target = fadeObjects[object][property].FadedIn + values.NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) + end + end + end + + local function FadeOutFunction(duration, CurveUtil) + for object, properties in pairs(AnimParams) do + for property, values in pairs(properties) do + values.Target = fadeObjects[object][property].FadedOut + values.NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration) + end + end + end + + local function AnimGuiObjects() + for object, properties in pairs(AnimParams) do + for property, values in pairs(properties) do + object[property] = values.Current + end + end + end + + local function UpdateAnimFunction(dtScale, CurveUtil) + for object, properties in pairs(AnimParams) do + for property, values in pairs(properties) do + values.Current = CurveUtil:Expt( + values.Current, + values.Target, + values.NormalizedExptValue, + dtScale + ) + end + end + + AnimGuiObjects() + end + + return FadeInFunction, FadeOutFunction, UpdateAnimFunction +end + +function methods:NewBindableEvent(name) + local bindable = Instance.new("BindableEvent") + bindable.Name = name + return bindable +end + +--- DEPRECATED METHODS: +function methods:RegisterGuiRoot() + -- This is left here for compatibility with ChatScript versions lower than 0.5 +end +--- End of Deprecated methods. + +function module.new() + local obj = setmetatable({}, methods) + + obj.ObjectPool = nil + obj.ChatWindow = nil + + obj.DEFAULT_MESSAGE_CREATOR = DEFAULT_MESSAGE_CREATOR + obj.MESSAGE_CREATOR_MODULES_VERSION = MESSAGE_CREATOR_MODULES_VERSION + + obj.KEY_MESSAGE_TYPE = KEY_MESSAGE_TYPE + obj.KEY_CREATOR_FUNCTION = KEY_CREATOR_FUNCTION + + obj.KEY_BASE_FRAME = KEY_BASE_FRAME + obj.KEY_BASE_MESSAGE = KEY_BASE_MESSAGE + obj.KEY_UPDATE_TEXT_FUNC = KEY_UPDATE_TEXT_FUNC + obj.KEY_GET_HEIGHT = KEY_GET_HEIGHT + obj.KEY_FADE_IN = KEY_FADE_IN + obj.KEY_FADE_OUT = KEY_FADE_OUT + obj.KEY_UPDATE_ANIMATION = KEY_UPDATE_ANIMATION + + return obj +end + +return module.new() diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/WelcomeMessage.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/WelcomeMessage.lua new file mode 100644 index 0000000..188dc11 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/WelcomeMessage.lua @@ -0,0 +1,61 @@ +-- // FileName: WelcomeMessage.lua +-- // Written by: TheGamer101 +-- // Description: Create a message label for a welcome message. + +local clientChatModules = script.Parent.Parent +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) +local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants")) +local util = require(script.Parent:WaitForChild("Util")) + +function CreateWelcomeMessageLabel(messageData, channelName) + local message = messageData.Message + local extraData = messageData.ExtraData or {} + local useFont = extraData.Font or ChatSettings.DefaultFont + local useTextSize = extraData.FontSize or ChatSettings.ChatWindowTextSize + local useChatColor = extraData.ChatColor or ChatSettings.DefaultMessageColor + local useChannelColor = extraData.ChannelColor or useChatColor + + local BaseFrame, BaseMessage = util:CreateBaseMessage(message, useFont, useTextSize, useChatColor) + local ChannelButton = nil + + if channelName ~= messageData.OriginalChannel then + local formatChannelName = string.format("{%s}", messageData.OriginalChannel) + ChannelButton = util:AddChannelButtonToBaseMessage(BaseMessage, useChannelColor, formatChannelName, messageData.OriginalChannel) + local numNeededSpaces = util:GetNumberOfSpaces(formatChannelName, useFont, useTextSize) + 1 + BaseMessage.Text = string.rep(" ", numNeededSpaces) .. message + end + + local function GetHeightFunction(xSize) + return util:GetMessageHeight(BaseMessage, BaseFrame, xSize) + end + + local FadeParmaters = {} + FadeParmaters[BaseMessage] = { + TextTransparency = {FadedIn = 0, FadedOut = 1}, + TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1} + } + + if ChannelButton then + FadeParmaters[ChannelButton] = { + TextTransparency = {FadedIn = 0, FadedOut = 1}, + TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1} + } + end + + local FadeInFunction, FadeOutFunction, UpdateAnimFunction = util:CreateFadeFunctions(FadeParmaters) + + return { + [util.KEY_BASE_FRAME] = BaseFrame, + [util.KEY_BASE_MESSAGE] = BaseMessage, + [util.KEY_UPDATE_TEXT_FUNC] = nil, + [util.KEY_GET_HEIGHT] = GetHeightFunction, + [util.KEY_FADE_IN] = FadeInFunction, + [util.KEY_FADE_OUT] = FadeOutFunction, + [util.KEY_UPDATE_ANIMATION] = UpdateAnimFunction + } +end + +return { + [util.KEY_MESSAGE_TYPE] = ChatConstants.MessageTypeWelcome, + [util.KEY_CREATOR_FUNCTION] = CreateWelcomeMessageLabel +} diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/WhisperMessage.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/WhisperMessage.lua new file mode 100644 index 0000000..5f930a2 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/DefaultClientChatModules/MessageCreatorModules/WhisperMessage.lua @@ -0,0 +1,97 @@ +-- // FileName: WhisperMessage.lua +-- // Written by: TheGamer101 +-- // Description: Create a message label for a whisper chat message. + +local PlayersService = game:GetService("Players") +local LocalPlayer = PlayersService.LocalPlayer +while not LocalPlayer do + PlayersService.ChildAdded:wait() + LocalPlayer = PlayersService.LocalPlayer +end + +local clientChatModules = script.Parent.Parent +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) +local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants")) +local util = require(script.Parent:WaitForChild("Util")) + +function CreateMessageLabel(messageData, channelName) + + local fromSpeaker = messageData.FromSpeaker + local message = messageData.Message + + local extraData = messageData.ExtraData or {} + local useFont = extraData.Font or ChatSettings.DefaultFont + local useTextSize = extraData.TextSize or ChatSettings.ChatWindowTextSize + local useNameColor = extraData.NameColor or ChatSettings.DefaultNameColor + local useChatColor = extraData.ChatColor or ChatSettings.DefaultMessageColor + local useChannelColor = extraData.ChannelColor or useChatColor + + local formatUseName = string.format("[%s]:", fromSpeaker) + local speakerNameSize = util:GetStringTextBounds(formatUseName, useFont, useTextSize) + local numNeededSpaces = util:GetNumberOfSpaces(formatUseName, useFont, useTextSize) + 1 + + local BaseFrame, BaseMessage = util:CreateBaseMessage("", useFont, useTextSize, useChatColor) + local NameButton = util:AddNameButtonToBaseMessage(BaseMessage, useNameColor, formatUseName, fromSpeaker) + local ChannelButton = nil + + if channelName ~= messageData.OriginalChannel then + local whisperString = messageData.OriginalChannel + if messageData.FromSpeaker ~= LocalPlayer.Name then + whisperString = string.format("From %s", messageData.FromSpeaker) + end + + local formatChannelName = string.format("{%s}", whisperString) + ChannelButton = util:AddChannelButtonToBaseMessage(BaseMessage, useChannelColor, formatChannelName, messageData.OriginalChannel) + NameButton.Position = UDim2.new(0, ChannelButton.Size.X.Offset + util:GetStringTextBounds(" ", useFont, useTextSize).X, 0, 0) + numNeededSpaces = numNeededSpaces + util:GetNumberOfSpaces(formatChannelName, useFont, useTextSize) + 1 + end + + local function UpdateTextFunction(messageObject) + if messageData.IsFiltered then + BaseMessage.Text = string.rep(" ", numNeededSpaces) .. messageObject.Message + else + BaseMessage.Text = string.rep(" ", numNeededSpaces) .. string.rep("_", messageObject.MessageLength) + end + end + + UpdateTextFunction(messageData) + + local function GetHeightFunction(xSize) + return util:GetMessageHeight(BaseMessage, BaseFrame, xSize) + end + + local FadeParmaters = {} + FadeParmaters[NameButton] = { + TextTransparency = {FadedIn = 0, FadedOut = 1}, + TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1} + } + + FadeParmaters[BaseMessage] = { + TextTransparency = {FadedIn = 0, FadedOut = 1}, + TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1} + } + + if ChannelButton then + FadeParmaters[ChannelButton] = { + TextTransparency = {FadedIn = 0, FadedOut = 1}, + TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1} + } + end + + local FadeInFunction, FadeOutFunction, UpdateAnimFunction = util:CreateFadeFunctions(FadeParmaters) + + return { + [util.KEY_BASE_FRAME] = BaseFrame, + [util.KEY_BASE_MESSAGE] = BaseMessage, + [util.KEY_UPDATE_TEXT_FUNC] = UpdateTextFunction, + [util.KEY_GET_HEIGHT] = GetHeightFunction, + [util.KEY_FADE_IN] = FadeInFunction, + [util.KEY_FADE_OUT] = FadeOutFunction, + [util.KEY_UPDATE_ANIMATION] = UpdateAnimFunction + } +end + +return { + [util.KEY_MESSAGE_TYPE] = ChatConstants.MessageTypeWhisper, + [util.KEY_CREATOR_FUNCTION] = CreateMessageLabel +} diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/MessageLabelCreator.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/MessageLabelCreator.lua new file mode 100644 index 0000000..936b9ce --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/MessageLabelCreator.lua @@ -0,0 +1,116 @@ +-- // FileName: MessageLabelCreator.lua +-- // Written by: Xsitsu +-- // Description: Module to handle taking text and creating stylized GUI objects for display in ChatWindow. + +local OBJECT_POOL_SIZE = 50 + +local module = {} +--////////////////////////////// Include +--////////////////////////////////////// +local Chat = game:GetService("Chat") +local clientChatModules = Chat:WaitForChild("ClientChatModules") +local messageCreatorModules = clientChatModules:WaitForChild("MessageCreatorModules") +local messageCreatorUtil = require(messageCreatorModules:WaitForChild("Util")) +local modulesFolder = script.Parent +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) +local moduleObjectPool = require(modulesFolder:WaitForChild("ObjectPool")) +local MessageSender = require(modulesFolder:WaitForChild("MessageSender")) + +--////////////////////////////// Methods +--////////////////////////////////////// +local methods = {} +methods.__index = methods + +function ReturnToObjectPoolRecursive(instance, objectPool) + local children = instance:GetChildren() + for i = 1, #children do + ReturnToObjectPoolRecursive(children[i], objectPool) + end + instance.Parent = nil + objectPool:ReturnInstance(instance) +end + +function GetMessageCreators() + local typeToFunction = {} + local creators = messageCreatorModules:GetChildren() + for i = 1, #creators do + if creators[i]:IsA("ModuleScript") then + if creators[i].Name ~= "Util" then + local creator = require(creators[i]) + typeToFunction[creator[messageCreatorUtil.KEY_MESSAGE_TYPE]] = creator[messageCreatorUtil.KEY_CREATOR_FUNCTION] + end + end + end + return typeToFunction +end + +function methods:WrapIntoMessageObject(messageData, createdMessageObject) + local BaseFrame = createdMessageObject[messageCreatorUtil.KEY_BASE_FRAME] + local BaseMessage = nil + if messageCreatorUtil.KEY_BASE_MESSAGE then + BaseMessage = createdMessageObject[messageCreatorUtil.KEY_BASE_MESSAGE] + end + local UpdateTextFunction = createdMessageObject[messageCreatorUtil.KEY_UPDATE_TEXT_FUNC] + local GetHeightFunction = createdMessageObject[messageCreatorUtil.KEY_GET_HEIGHT] + local FadeInFunction = createdMessageObject[messageCreatorUtil.KEY_FADE_IN] + local FadeOutFunction = createdMessageObject[messageCreatorUtil.KEY_FADE_OUT] + local UpdateAnimFunction = createdMessageObject[messageCreatorUtil.KEY_UPDATE_ANIMATION] + + local obj = {} + + obj.ID = messageData.ID + obj.BaseFrame = BaseFrame + obj.BaseMessage = BaseMessage + obj.UpdateTextFunction = UpdateTextFunction or function() warn("NO MESSAGE RESIZE FUNCTION") end + obj.GetHeightFunction = GetHeightFunction + obj.FadeInFunction = FadeInFunction + obj.FadeOutFunction = FadeOutFunction + obj.UpdateAnimFunction = UpdateAnimFunction + obj.ObjectPool = self.ObjectPool + obj.Destroyed = false + + function obj:Destroy() + ReturnToObjectPoolRecursive(self.BaseFrame, self.ObjectPool) + self.Destroyed = true + end + + return obj +end + +function methods:CreateMessageLabel(messageData, currentChannelName) + local messageType = messageData.MessageType + if self.MessageCreators[messageType] then + local createdMessageObject = self.MessageCreators[messageType](messageData, currentChannelName) + if createdMessageObject then + return self:WrapIntoMessageObject(messageData, createdMessageObject) + end + elseif self.DefaultCreatorType then + local createdMessageObject = self.MessageCreators[self.DefaultCreatorType](messageData, currentChannelName) + if createdMessageObject then + return self:WrapIntoMessageObject(messageData, createdMessageObject) + end + else + error("No message creator available for message type: " ..messageType) + end +end + +--///////////////////////// Constructors +--////////////////////////////////////// + +function module.new() + local obj = setmetatable({}, methods) + + obj.ObjectPool = moduleObjectPool.new(OBJECT_POOL_SIZE) + obj.MessageCreators = GetMessageCreators() + obj.DefaultCreatorType = messageCreatorUtil.DEFAULT_MESSAGE_CREATOR + + messageCreatorUtil:RegisterObjectPool(obj.ObjectPool) + + return obj +end + +function module:GetStringTextBounds(text, font, textSize, sizeBounds) + return messageCreatorUtil:GetStringTextBounds(text, font, textSize, sizeBounds) +end + +return module diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/MessageLogDisplay.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/MessageLogDisplay.lua new file mode 100644 index 0000000..4e6f3c1 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/MessageLogDisplay.lua @@ -0,0 +1,263 @@ +-- // FileName: MessageLogDisplay.lua +-- // Written by: Xsitsu, TheGamer101 +-- // Description: ChatChannel window for displaying messages. + +local module = {} +module.ScrollBarThickness = 4 + +--////////////////////////////// Include +--////////////////////////////////////// +local Chat = game:GetService("Chat") +local clientChatModules = Chat:WaitForChild("ClientChatModules") +local modulesFolder = script.Parent +local moduleMessageLabelCreator = require(modulesFolder:WaitForChild("MessageLabelCreator")) +local CurveUtil = require(modulesFolder:WaitForChild("CurveUtil")) + +local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings")) + +local MessageLabelCreator = moduleMessageLabelCreator.new() + +--////////////////////////////// Methods +--////////////////////////////////////// +local methods = {} +methods.__index = methods + +local function CreateGuiObjects() + local BaseFrame = Instance.new("Frame") + BaseFrame.Selectable = false + BaseFrame.Size = UDim2.new(1, 0, 1, 0) + BaseFrame.BackgroundTransparency = 1 + + local Scroller = Instance.new("ScrollingFrame") + Scroller.Selectable = ChatSettings.GamepadNavigationEnabled + Scroller.Name = "Scroller" + Scroller.BackgroundTransparency = 1 + Scroller.BorderSizePixel = 0 + Scroller.Position = UDim2.new(0, 0, 0, 3) + Scroller.Size = UDim2.new(1, -4, 1, -6) + Scroller.CanvasSize = UDim2.new(0, 0, 0, 0) + Scroller.ScrollBarThickness = module.ScrollBarThickness + Scroller.Active = false + Scroller.Parent = BaseFrame + + return BaseFrame, Scroller +end + +function methods:Destroy() + self.GuiObject:Destroy() + self.Destroyed = true +end + +function methods:SetActive(active) + self.GuiObject.Visible = active +end + +function methods:UpdateMessageFiltered(messageData) + local messageObject = nil + local searchIndex = 1 + local searchTable = self.MessageObjectLog + + while (#searchTable >= searchIndex) do + local obj = searchTable[searchIndex] + + if (obj.ID == messageData.ID) then + messageObject = obj + break + end + + searchIndex = searchIndex + 1 + end + + if (messageObject) then + messageObject.UpdateTextFunction(messageData) + self:ReorderAllMessages() + end +end + +function methods:AddMessage(messageData) + self:WaitUntilParentedCorrectly() + + local messageObject = MessageLabelCreator:CreateMessageLabel(messageData, self.CurrentChannelName) + if messageObject == nil then + return + end + + table.insert(self.MessageObjectLog, messageObject) + self:PositionMessageLabelInWindow(messageObject) +end + +function methods:AddMessageAtIndex(messageData, index) + local messageObject = MessageLabelCreator:CreateMessageLabel(messageData, self.CurrentChannelName) + if messageObject == nil then + return + end + + table.insert(self.MessageObjectLog, index, messageObject) + + local wasScrolledToBottom = self:IsScrolledDown() + self:ReorderAllMessages() + if wasScrolledToBottom then + self.Scroller.CanvasPosition = Vector2.new(0, math.max(0, self.Scroller.CanvasSize.Y.Offset - self.Scroller.AbsoluteSize.Y)) + end +end + +function methods:RemoveLastMessage() + self:WaitUntilParentedCorrectly() + + local lastMessage = self.MessageObjectLog[1] + local posOffset = UDim2.new(0, 0, 0, lastMessage.BaseFrame.AbsoluteSize.Y) + + lastMessage:Destroy() + table.remove(self.MessageObjectLog, 1) + + for i, messageObject in pairs(self.MessageObjectLog) do + messageObject.BaseFrame.Position = messageObject.BaseFrame.Position - posOffset + end + + self.Scroller.CanvasSize = self.Scroller.CanvasSize - posOffset +end + +function methods:IsScrolledDown() + local yCanvasSize = self.Scroller.CanvasSize.Y.Offset + local yContainerSize = self.Scroller.AbsoluteWindowSize.Y + local yScrolledPosition = self.Scroller.CanvasPosition.Y + + return (yCanvasSize < yContainerSize or + yCanvasSize - yScrolledPosition <= yContainerSize + 5) +end + +function min(x, y) + return x < y and x or y +end + +function methods:PositionMessageLabelInWindow(messageObject) + self:WaitUntilParentedCorrectly() + + local baseFrame = messageObject.BaseFrame + + baseFrame.Parent = self.Scroller + baseFrame.Position = UDim2.new(0, 0, 0, self.Scroller.CanvasSize.Y.Offset) + + baseFrame.Size = UDim2.new(1, 0, 0, messageObject.GetHeightFunction(self.Scroller.AbsoluteSize.X)) + + if messageObject.BaseMessage then + local trySize = self.Scroller.AbsoluteSize.X + local minTrySize = min(self.Scroller.AbsoluteSize.X - 10, 0) + while not messageObject.BaseMessage.TextFits do + trySize = trySize - 1 + if trySize < minTrySize then + break + end + baseFrame.Size = UDim2.new(1, 0, 0, messageObject.GetHeightFunction(trySize)) + end + end + + local isScrolledDown = self:IsScrolledDown() + + local add = UDim2.new(0, 0, 0, baseFrame.Size.Y.Offset) + self.Scroller.CanvasSize = self.Scroller.CanvasSize + add + + if isScrolledDown then + self.Scroller.CanvasPosition = Vector2.new(0, math.max(0, self.Scroller.CanvasSize.Y.Offset - self.Scroller.AbsoluteSize.Y)) + end +end + +function methods:ReorderAllMessages() + self:WaitUntilParentedCorrectly() + + --// Reordering / reparenting with a size less than 1 causes weird glitches to happen with scrolling as repositioning happens. + if (self.GuiObject.AbsoluteSize.Y < 1) then return end + + local oldCanvasPositon = self.Scroller.CanvasPosition + local wasScrolledDown = self:IsScrolledDown() + + self.Scroller.CanvasSize = UDim2.new(0, 0, 0, 0) + for i, messageObject in pairs(self.MessageObjectLog) do + self:PositionMessageLabelInWindow(messageObject) + end + + if not wasScrolledDown then + self.Scroller.CanvasPosition = oldCanvasPositon + end +end + +function methods:Clear() + for i, v in pairs(self.MessageObjectLog) do + v:Destroy() + end + self.MessageObjectLog = {} + + self.Scroller.CanvasSize = UDim2.new(0, 0, 0, 0) +end + +function methods:SetCurrentChannelName(name) + self.CurrentChannelName = name +end + +function methods:FadeOutBackground(duration) + --// Do nothing +end + +function methods:FadeInBackground(duration) + --// Do nothing +end + +function methods:FadeOutText(duration) + for i = 1, #self.MessageObjectLog do + if self.MessageObjectLog[i].FadeOutFunction then + self.MessageObjectLog[i].FadeOutFunction(duration, CurveUtil) + end + end +end + +function methods:FadeInText(duration) + for i = 1, #self.MessageObjectLog do + if self.MessageObjectLog[i].FadeInFunction then + self.MessageObjectLog[i].FadeInFunction(duration, CurveUtil) + end + end +end + +function methods:Update(dtScale) + for i = 1, #self.MessageObjectLog do + if self.MessageObjectLog[i].UpdateAnimFunction then + self.MessageObjectLog[i].UpdateAnimFunction(dtScale, CurveUtil) + end + end +end + +--// ToDo: Move to common modules +function methods:WaitUntilParentedCorrectly() + while (not self.GuiObject:IsDescendantOf(game:GetService("Players").LocalPlayer)) do + self.GuiObject.AncestryChanged:wait() + end +end + +--///////////////////////// Constructors +--////////////////////////////////////// + +function module.new() + local obj = setmetatable({}, methods) + obj.Destroyed = false + + local BaseFrame, Scroller = CreateGuiObjects() + obj.GuiObject = BaseFrame + obj.Scroller = Scroller + + obj.MessageObjectLog = {} + + obj.Name = "MessageLogDisplay" + obj.GuiObject.Name = "Frame_" .. obj.Name + + obj.CurrentChannelName = "" + + obj.GuiObject.Changed:connect(function(prop) + if (prop == "AbsoluteSize") then + spawn(function() obj:ReorderAllMessages() end) + end + end) + + return obj +end + +return module diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/MessageSender.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/MessageSender.lua new file mode 100644 index 0000000..59a1ac5 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/MessageSender.lua @@ -0,0 +1,33 @@ +-- // FileName: MessageSender.lua +-- // Written by: Xsitsu +-- // Description: Module to centralize sending message functionality. + +local module = {} +--////////////////////////////// Include +--////////////////////////////////////// +local modulesFolder = script.Parent + +--////////////////////////////// Methods +--////////////////////////////////////// +local methods = {} +methods.__index = methods + +function methods:SendMessage(message, toChannel) + self.SayMessageRequest:FireServer(message, toChannel) +end + +function methods:RegisterSayMessageFunction(func) + self.SayMessageRequest = func +end + +--///////////////////////// Constructors +--////////////////////////////////////// + +function module.new() + local obj = setmetatable({}, methods) + obj.SayMessageRequest = nil + + return obj +end + +return module.new() diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/README.md b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/README.md new file mode 100644 index 0000000..ca685c3 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/README.md @@ -0,0 +1,80 @@ +## Documentation +### ChatWindow +This is the main client side chat object. + +### Methods + AddChannel(string channelName) + RemoveChannel(string channelName) + ChatChannel GetChannel(string channelName) + ChatChannel GetCurrentChannel() + SwitchCurrentChannel(string channelName) + + bool GetVisible() + SetVisible(bool visible) + + EnableResizable() + DisableResizable() + + FadeOutBackground(float duration) + FadeInBackground(float duration) + FadeOutText(float duration) + FadeInText(float duration) + +### Properties + ChatBar ChatBar + ChannelsBar ChannelsBar + MessageLogDisplay MessageLogDisplay + +### ChatChannel +A client side chat channel object to manage the chat. + +### Methods + AddMessageToChannel(table messageData) + RemoveLastMessageFromChannel() + ClearMessageLog() + +### MessageLogDisplay +This object handles displaying the messages of the current channel. + +### Methods + AddMessage(table messageData) + RemoveLastMessage() + ReorderAllMessages() + Clear() + + bool IsScrolledDown() + + FadeOutBackground(float duration) + FadeInBackground(float duration) + FadeOutText(float duration) + FadeInText(float duration) + +### ChatBar +The chat bar object handles text entry. + +### Methods + TextBox GetTextBox() + TextButton GetMessageModeTextButton + + bool IsFocused() + CaptureFocus() + ReleaseFocus(bool submitted) + + ResetText() + SetText(text) + + bool GetEnabled() + SetEnabled(bool enabled) + + SetTextLabelText(string text) + SetTextBoxText(string text) + string GetTextBoxText() + + ResetSize() + SetTextSize(int textSize) + SetChannelTarget(string channelName) + + FadeOutBackground(float duration) + FadeInBackground(float duration) + FadeOutText(float duration) + FadeInText(float duration) diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/TransparencyTweener.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/TransparencyTweener.lua new file mode 100644 index 0000000..d5eb854 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ClientChat/TransparencyTweener.lua @@ -0,0 +1,195 @@ +-- // FileName: TransparencyTweener.lua +-- // Written by: Xsitsu +-- // Description: Data structure for tweening transparency of a group of objects as one unit. + +local module = {} + +local RunService = game:GetService("RunService") +--////////////////////////////// Include +--////////////////////////////////////// +local modulesFolder = script.Parent + +--// Can't use ClassMaker in it's current state since this uses custom __index and __newindex. +--// Maybe in the future I'll expand that to be more powerful, but it's alright like this for now. + +--////////////////////////////// Details +--////////////////////////////////////// +local metatable = {} +metatable.__ClassName = "TransparencyTweener" + +metatable.__tostring = function(tbl) + return tbl.__ClassName .. ": " .. tbl.MemoryLocation +end + +metatable.__metatable = "The metatable is locked" +metatable.__index = function(tbl, index, value) + if rawget(tbl, index) then return rawget(tbl, index) end + if rawget(metatable, index) then return rawget(metatable, index) end + + if (index == "Transparency") then + return rawget(tbl, "InternalLastTweenPercentage") + end + + error(index .. " is not a valid member of " .. tbl.__ClassName) +end +metatable.__newindex = function(tbl, index, value) + if (index == "Transparency") then + tbl.InternalLastTweenPercentage = value + tbl:SetPropertiesToTweenPercentage(value) + return + end + + error(index .. " is not a valid member of " .. tbl.__ClassName) +end + + +--////////////////////////////// Methods +--////////////////////////////////////// +function metatable:Dump() + local str = tostring(self) + + for tweenObject, objectProperties in pairs(self.TweenObjects) do + local addStr = " | " + if (type(tweenObject) == "table") then + addStr = addStr .. "{{" .. tweenObject:Dump() .. "}}" + elseif (type(tweenObject) == "userdata") then + addStr = addStr .. tweenObject.Name .. "/" .. (tweenObject.Parent and tweenObject.Parent.Name or nil) + end + + for propertyName, baseValue in pairs(objectProperties) do + addStr = addStr .. " [" .. propertyName .. "=" .. string.sub(tostring(baseValue), 1, 4) .. "]" + end + + str = str .. addStr + end + + return str +end + +function metatable:OutputTest() + print("Test Output for {={" .. self:Dump() .. "}=}") + for tweenObject, objectProperties in pairs(self.TweenObjects) do + print("TweenObject:", tweenObject) + for propertyName, baseValue in pairs(objectProperties) do + print("\t[" .. propertyName .. "=" .. string.sub(tostring(baseValue), 1, 4) .. "] Actual:" .. tweenObject[propertyName]) + end + end +end + +function metatable:RegisterTweenObjectProperty(objectValue, propertyName, baseValue) + baseValue = baseValue or objectValue[propertyName] + + if (not self.TweenObjects[objectValue]) then + self.TweenObjects[objectValue] = {} + end + + self.TweenObjects[objectValue][propertyName] = baseValue + + self:SetObjectPropertyToPercentValue(objectValue, propertyName, baseValue, self.InternalLastTweenPercentage) +end + +function metatable:UnregisterTweenObject(objectValue) + self.TweenObjects[objectValue] = nil +end + +function metatable:SetObjectPropertyToPercentValue(objectValue, propertyName, baseValue, percentValue) + local tweenOverValue = 1 - baseValue + local actualValue = baseValue + (tweenOverValue * percentValue) + objectValue[propertyName] = actualValue +end + +function metatable:SetPropertiesToTweenPercentage(percentValue) + for tweenObject, objectProperties in pairs(self.TweenObjects) do + for propertyName, baseValue in pairs(objectProperties) do + self:SetObjectPropertyToPercentValue(tweenObject, propertyName, baseValue, percentValue) + end + end +end + +function metatable:Tween(duration, targetPercentage, startingPercentage) + if (self.Tweening) then + self.QueuedTween = {duration, targetPercentage, startingPercentage} + self.TweenIsQueued = true + self:CancelTween() + return + end + self.Tweening = true + + local vStartingPercentage = startingPercentage + + if (vStartingPercentage) then + self:SetPropertiesToTweenPercentage(vStartingPercentage) + else + vStartingPercentage = self.InternalLastTweenPercentage + end + + local startTime = tick() + local endTime = startTime + duration + local tweeningOverPercentage = targetPercentage - vStartingPercentage + + local percentComplete = 0 + spawn(function() + local now = tick() + while(now < endTime and not self.Canceled) do + percentComplete = math.min(math.max((now - startTime) / duration, 0), 1) + local percentValue = vStartingPercentage + (tweeningOverPercentage * percentComplete) + self.InternalLastTweenPercentage = percentValue + self:SetPropertiesToTweenPercentage(percentValue) + + RunService.RenderStepped:wait() + now = tick() + end + + if (not self.Canceled) then + self.InternalLastTweenPercentage = targetPercentage + self:SetPropertiesToTweenPercentage(targetPercentage) + + else + self.Canceled = false + + if (self.TweenIsQueued) then + self.TweenIsQueued = false + self.Tweening = false + self:Tween(unpack(self.QueuedTween)) + return + end + end + + self.Tweening = false + end) +end + +function metatable:CancelTween() + self.Canceled = true +end + +--///////////////////////// Constructors +--////////////////////////////////////// +function module.new() + local obj = {} + obj.MemoryLocation = tostring(obj):match("[0123456789ABCDEF]+") + + --// We do not want to hold strong references to objects. + obj.TweenObjects = setmetatable({}, {__mode = "k"}) + + --// Transparency is a property that doesn't actually exist. + --// The index of 'Transparency' is used for reading from and + --// writing to InternalLastTweenPercentage. This needs to be + --// done through metatables so we can also get the behavior + --// of calling the method 'SetPropertiesToTweenPercentage' + --// automatically when a new value is set. + + obj.Transparency = nil + obj.InternalLastTweenPercentage = 0 + obj.Tweening = false + obj.Canceled = false + + obj.TweenIsQueued = false + obj.QueuedTween = {} + + obj = setmetatable(obj, metatable) + + return obj +end + +return module diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/FreeCamera/FreeCamera.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/FreeCamera/FreeCamera.lua new file mode 100644 index 0000000..56b712e --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/FreeCamera/FreeCamera.lua @@ -0,0 +1,349 @@ +------------------------------------------------ +-- +-- Freecam.lua +-- Written by: Fractality +-- Edited by: TheGamer101, to make it work without the screen gui, remove the Class library +-- and add alternative key bindings. +------------------------------------------------ + +-- To exit and enter free camera, use key shortcut Left Shift + P + +local player = game:GetService("Players") +while not player.LocalPlayer do player.Changed:wait() end +player = player.LocalPlayer +local camera = workspace.CurrentCamera + +local RS = game:GetService("RunService") +local UIS = game:GetService("UserInputService") +local StarterGui = game:GetService("StarterGui") + +local Spring = require(script:WaitForChild("Spring")) +local Maid = require(script:WaitForChild("Maid")) + +local WasGuiVisible = {} +function ToggleGui(on) + if not on then + WasGuiVisible["PointsNotificationsActive"] = StarterGui:GetCore("PointsNotificationsActive") + WasGuiVisible["BadgesNotificationsActive"] = StarterGui:GetCore("BadgesNotificationsActive") + WasGuiVisible["Health"] = StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Health) + WasGuiVisible["Backpack"] = StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Backpack) + WasGuiVisible["PlayerList"] = StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.PlayerList) + WasGuiVisible["Chat"] = StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Chat) + end + + local function GuiOn(name) + if on == false then + return false + end + if WasGuiVisible[name] ~= nil then + return WasGuiVisible[name] + end + return true + end + + StarterGui:SetCore("PointsNotificationsActive", GuiOn("PointsNotificationsActive")) + StarterGui:SetCore("BadgesNotificationsActive", GuiOn("BadgesNotificationsActive")) + + StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Health, GuiOn("Health")) + StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, GuiOn("Backpack")) + StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.PlayerList, GuiOn("PlayerList")) + StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Chat, GuiOn("Chat")) +end + +------------------------------------------------ + +local DEF_FOV = 70 +local NM_ZOOM = math.tan(DEF_FOV * math.pi/360) +local LVEL_GAIN = Vector3.new(1, 0.75, 1) +local RVEL_GAIN = Vector2.new(0.85, 1)/128 +local FVEL_GAIN = -330 +local DEADZONE = 0.125 +local FOCUS_OFFSET = CFrame.new(0, 0, -16) + +local DIRECTION_LEFT = 1 +local DIRECTION_RIGHT = 2 +local DIRECTION_FORWARD = 3 +local DIRECTION_BACKWARD = 4 +local DIRECTION_UP = 5 +local DIRECTION_DOWN = 6 + +local KEY_MAPPINGS = { + [DIRECTION_LEFT] = {Enum.KeyCode.A, Enum.KeyCode.H}, + [DIRECTION_RIGHT] = {Enum.KeyCode.D, Enum.KeyCode.K}, + [DIRECTION_FORWARD] = {Enum.KeyCode.W, Enum.KeyCode.U}, + [DIRECTION_BACKWARD] = {Enum.KeyCode.S, Enum.KeyCode.J}, + [DIRECTION_UP] = {Enum.KeyCode.E, Enum.KeyCode.I}, + [DIRECTION_DOWN] = {Enum.KeyCode.Q, Enum.KeyCode.Y}, +} + +function CreateLetterBox() + local topBar = Instance.new("Frame") + topBar.Name = "TopBar" + topBar.Position = UDim2.new(0, 0, 0, -36) + topBar.Size = UDim2.new(1, 0, 0.128, 0) + topBar.ZIndex = 10 + topBar.BackgroundColor3 = Color3.new(0, 0, 0) + topBar.BorderSizePixel = 0 + topBar.Parent = script.Parent + + local bottomBar = topBar:Clone() + bottomBar.Name = "BottomBar" + bottomBar.Position = UDim2.new(0, 0, 1, 0) + bottomBar.AnchorPoint = Vector2.new(0, 1) + bottomBar.Parent = script.Parent + return script.Parent +end + +------------------------------------------------ + +local screenGuis = {} +local freeCamEnabled = false +local letterBoxEnabled = true + +local stateRot = Vector2.new() +local panDeltaGamepad = Vector2.new() +local panDeltaMouse = Vector2.new() + +local velSpring = Spring.new(7/9, 1/3, 1, Vector3.new()) +local rotSpring = Spring.new(7/9, 1/3, 1, Vector2.new()) +local fovSpring = Spring.new(2, 1/3, 1, 0) + +local letterbox = CreateLetterBox() + +local gp_x = 0 +local gp_z = 0 +local gp_l1 = 0 +local gp_r1 = 0 +local rate_fov = 0 + +local SpeedModifier = 1 + +------------------------------------------------ + +local function Clamp(x, min, max) + return x < min and min or x > max and max or x +end + +local function GetChar() + local character = player.Character + if character then + return character:FindFirstChildOfClass("Humanoid"), character:FindFirstChild("HumanoidRootPart") + end +end + +local function InputCurve(x) + local s = math.abs(x) + if s > DEADZONE then + s = 0.255000975*(2^(2.299113817*s) - 1) + return x > 0 and (s > 1 and 1 or s) or (s > 1 and -1 or -s) + end + return 0 +end + +------------------------------------------------ + +local function ProcessInput(input, processed) + local userInputType = input.UserInputType + if userInputType == Enum.UserInputType.Gamepad1 then + local keycode = input.KeyCode + if keycode == Enum.KeyCode.Thumbstick2 then + local pos = input.Position + panDeltaGamepad = Vector2.new(InputCurve(pos.y), InputCurve(-pos.x))*7 + elseif keycode == Enum.KeyCode.Thumbstick1 then + local pos = input.Position + gp_x = InputCurve(pos.x) + gp_z = InputCurve(-pos.y) + elseif keycode == Enum.KeyCode.ButtonL2 then + gp_l1 = input.Position.z + elseif keycode == Enum.KeyCode.ButtonR2 then + gp_r1 = input.Position.z + end + elseif userInputType == Enum.UserInputType.MouseWheel then + rate_fov = input.Position.Z + end +end + +UIS.InputChanged:Connect(ProcessInput) +UIS.InputEnded:Connect(ProcessInput) +UIS.InputBegan:Connect(ProcessInput) + +------------------------------------------------ + +local function IsDirectionDown(direction) + for i = 1, #KEY_MAPPINGS[direction] do + if UIS:IsKeyDown(KEY_MAPPINGS[direction][i]) then + return true + end + end + return false +end + +local UpdateFreecam do + local dt = 1/60 + RS.RenderStepped:Connect(function(_dt) + dt = _dt + end) + + function UpdateFreecam() + local camCFrame = camera.CFrame + + local kx = (IsDirectionDown(DIRECTION_RIGHT) and 1 or 0) - (IsDirectionDown(DIRECTION_LEFT) and 1 or 0) + local ky = (IsDirectionDown(DIRECTION_UP) and 1 or 0) - (IsDirectionDown(DIRECTION_DOWN) and 1 or 0) + local kz = (IsDirectionDown(DIRECTION_BACKWARD) and 1 or 0) - (IsDirectionDown(DIRECTION_FORWARD) and 1 or 0) + local km = (kx * kx) + (ky * ky) + (kz * kz) + if km > 1e-15 then + km = ((UIS:IsKeyDown(Enum.KeyCode.LeftShift) or UIS:IsKeyDown(Enum.KeyCode.RightShift)) and 1/4 or 1)/math.sqrt(km) + kx = kx * km + ky = ky * km + kz = kz * km + end + + local dx = kx + gp_x + local dy = ky + gp_r1 - gp_l1 + local dz = kz + gp_z + + velSpring.t = Vector3.new(dx, dy, dz) * SpeedModifier + rotSpring.t = panDeltaMouse + panDeltaGamepad + fovSpring.t = Clamp(fovSpring.t + dt * rate_fov*FVEL_GAIN, 5, 120) + + local fov = fovSpring:Update(dt) + local dPos = velSpring:Update(dt) * LVEL_GAIN + local dRot = rotSpring:Update(dt) * (RVEL_GAIN * math.tan(fov * math.pi/360) * NM_ZOOM) + + rate_fov = 0 + panDeltaMouse = Vector2.new() + + stateRot = stateRot + dRot + stateRot = Vector2.new(Clamp(stateRot.x, -3/2, 3/2), stateRot.y) + + local c = CFrame.new(camCFrame.p) * CFrame.Angles(0, stateRot.y, 0) * CFrame.Angles(stateRot.x, 0, 0) * CFrame.new(dPos) + + camera.CFrame = c + camera.Focus = c*FOCUS_OFFSET + camera.FieldOfView = fov + end +end + +------------------------------------------------ + +local function Panned(input, processed) + if not processed and input.UserInputType == Enum.UserInputType.MouseMovement then + local delta = input.Delta + panDeltaMouse = Vector2.new(-delta.y, -delta.x) + end +end + +------------------------------------------------ + +local function EnterFreecam() + ToggleGui(false) + UIS.MouseIconEnabled = false + Maid:Mark(UIS.InputBegan:Connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.MouseButton2 then + UIS.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition + local conn = UIS.InputChanged:Connect(Panned) + repeat + input = UIS.InputEnded:wait() + until input.UserInputType == Enum.UserInputType.MouseButton2 or not freeCamEnabled + panDeltaMouse = Vector2.new() + panDeltaGamepad = Vector2.new() + conn:Disconnect() + if freeCamEnabled then + UIS.MouseBehavior = Enum.MouseBehavior.Default + end + elseif input.KeyCode == Enum.KeyCode.LeftShift or input.KeyCode == Enum.KeyCode.RightShift then + SpeedModifier = 0.5 + end + end)) + + Maid:Mark(UIS.InputEnded:Connect(function(input, processed) + if input.KeyCode == Enum.KeyCode.LeftShift or input.KeyCode == Enum.KeyCode.RightShift then + SpeedModifier = 1 + end + end)) + + camera.CameraType = Enum.CameraType.Scriptable + + local hum, hrp = GetChar() + if hrp then + hrp.Anchored = true + end + if hum then + hum.WalkSpeed = 0 + Maid:Mark(hum.Jumping:Connect(function(active) + if active then + hum.Jumping = false + end + end)) + end + + velSpring.t, velSpring.v, velSpring.x = Vector3.new(), Vector3.new(), Vector3.new() + rotSpring.t, rotSpring.v, rotSpring.x = Vector2.new(), Vector2.new(), Vector2.new() + fovSpring.t, fovSpring.v, fovSpring.x = camera.FieldOfView, 0, camera.FieldOfView + + local camCFrame = camera.CFrame + local lookVector = camCFrame.lookVector.unit + + stateRot = Vector2.new( + math.asin(lookVector.y), + math.atan2(-lookVector.z, lookVector.x) - math.pi/2 + ) + panDeltaMouse = Vector2.new() + + local playerGui = player:WaitForChild("PlayerGui") + for _, obj in next, playerGui:GetChildren() do + if obj:IsA("ScreenGui") and obj.Enabled then + obj.Enabled = false + screenGuis[obj] = true + end + end + if letterBoxEnabled then + letterbox.Enabled = true + end + RS:BindToRenderStep("Freecam", Enum.RenderPriority.Camera.Value, UpdateFreecam) + freeCamEnabled = true +end + +local function ExitFreecam() + freeCamEnabled = false + if letterBoxEnabled then + letterbox.Enabled = false + end + UIS.MouseIconEnabled = true + UIS.MouseBehavior = Enum.MouseBehavior.Default + Maid:Sweep() + RS:UnbindFromRenderStep("Freecam") + local hum, hrp = GetChar() + if hum then + hum.WalkSpeed = 16 + end + if hrp then + hrp.Anchored = false + end + camera.FieldOfView = DEF_FOV + camera.CameraType = Enum.CameraType.Custom + for obj in next, screenGuis do + obj.Enabled = true + end + screenGuis = {} + ToggleGui(true) +end + +------------------------------------------------ + +UIS.InputBegan:Connect(function(input, processed) + if not processed then + if input.KeyCode == Enum.KeyCode.P then + if UIS:IsKeyDown(Enum.KeyCode.LeftShift) then + if freeCamEnabled then + ExitFreecam() + else + EnterFreecam() + end + end + elseif input.KeyCode == Enum.KeyCode.L and freeCamEnabled and UIS:IsKeyDown(Enum.KeyCode.LeftShift) then + letterBoxEnabled = not letterBoxEnabled + letterbox.Enabled = letterBoxEnabled + end + end +end) diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/FreeCamera/FreeCameraInstaller.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/FreeCamera/FreeCameraInstaller.lua new file mode 100644 index 0000000..de9fc53 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/FreeCamera/FreeCameraInstaller.lua @@ -0,0 +1,92 @@ +-- // FileName: FreeCameraInstaller.lua +-- // Written by: TheGamer101 +-- // Description: Installs the free camera ability for members of the Roblox Admin Group. +-- This is used by the marketing team to record video in game. +-- The code is kept on the server and only replicated to the client of members of the Roblox admin group +-- to minimize security problems. + +local FREECAM_GROUP_IDS = {1200769, 3013794} -- Admin Group, Group Requested by RobloxsaurusRex for contractors who need this power. +local PlayerService = game:GetService("Players") +local HttpRbxApiService = game:GetService("HttpRbxApiService") + +local freeCameraDevelopersFlagSuccess, freeCameraDevelopersFlagValue = pcall(function() return settings():GetFFlag("FreeCameraForDevelopers") end) +local freeCameraDevelopersFlag = (freeCameraDevelopersFlagSuccess and freeCameraDevelopersFlagValue) + +local function LoadLocalScript(name, parent) + local originalModule = script.Parent:WaitForChild(name) + local script = Instance.new("LocalScript") + script.Name = name + script.Source = originalModule.Source + script.Parent = parent + return script +end + +local function LoadModule(name, parent) + local originalModule = script.Parent:WaitForChild(name) + local module = Instance.new("ModuleScript") + module.Name = name + module.Source = originalModule.Source + module.Parent = parent + return module +end + +local Install = function () + + local function AddFreeCamera(player) + local playerGui = player:WaitForChild("PlayerGui") + local freeCameraScreenGui = Instance.new("ScreenGui") + freeCameraScreenGui.Name = "FreeCamera" + freeCameraScreenGui.ResetOnSpawn = false + freeCameraScreenGui.DisplayOrder = 10 + freeCameraScreenGui.Enabled = false + freeCameraScreenGui.Parent = playerGui + local freeCamera = LoadLocalScript("FreeCamera", freeCameraScreenGui) + LoadModule("Maid", freeCamera) + LoadModule("Spring", freeCamera) + end + + local function ShouldAddFreeCam(player) + if freeCameraDevelopersFlag then + local success, result = pcall(function() + local url = string.format("/users/%d/canmanage/%d", player.UserId, game.PlaceId) + return HttpRbxApiService:GetAsync(url, Enum.ThrottlingPriority.Default, Enum.HttpRequestType.Default, true) + end) + if success and type(result) == "string" then + -- API returns: {"Success":BOOLEAN,"CanManage":BOOLEAN} + -- Convert from JSON to a table + -- pcall in case of invalid JSON + success, result = pcall(function() + return game:GetService('HttpService'):JSONDecode(result) + end) + if success and result.CanManage == true then + return true + end + end + end + for i = 1, #FREECAM_GROUP_IDS do + local success, inGroup = pcall(function() + return player:IsInGroup(FREECAM_GROUP_IDS[i]) + end) + if success and inGroup then + return true + end + end + return false + end + + local function PlayerAdded(player) + if player.UserId > 0 then + if ShouldAddFreeCam(player) then + AddFreeCamera(player) + end + end + end + PlayerService.PlayerAdded:connect(PlayerAdded) + local players = PlayerService:GetPlayers() + for i = 1, #players do + PlayerAdded(players[i]) + end + +end + +return Install diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/FreeCamera/Maid.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/FreeCamera/Maid.lua new file mode 100644 index 0000000..f46eacd --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/FreeCamera/Maid.lua @@ -0,0 +1,55 @@ +-- Maid class + +local destructors = { + ['function'] = function(item) + item() + end; + ['RBXScriptConnection'] = function(item) + item:Disconnect() + end; + ['Instance'] = function(item) + item:Destroy() + end; +} + +local Maid = {} +Maid.__index = Maid + +function Maid:Mark(item) + if destructors[typeof(item)] then + self.trash[#self.trash + 1] = item + else + error(('Maid does not support type "%s"'):format(typeof(item)), 2) + end +end + +function Maid:Unmark(item) + if item then + local trash = self.trash + for i = 1, #trash do + if trash[i] == item then + table.remove(trash, i) + break + end + end + else + self.trash = {} + end +end + +function Maid:Sweep() + local trash = self.trash + for i = 1, #trash do + local item = trash[i] + destructors[typeof(item)](item) + end + self.trash = {} +end + +function Maid.new() + local self = setmetatable({}, Maid) + self.trash = {} + return self +end + +return Maid.new() diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/FreeCamera/Spring.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/FreeCamera/Spring.lua new file mode 100644 index 0000000..38ce6b9 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/FreeCamera/Spring.lua @@ -0,0 +1,32 @@ + +local Spring = {} +Spring.__index = Spring + +function Spring:Update(dt) + local t, k, d, x0, v0 = self.t, self.k, self.d, self.x, self.v + local a0 = k*(t - x0) + v0*d + local v1 = v0 + a0*(dt/2) + local a1 = k*(t - (x0 + v0*(dt/2))) + v1*d + local v2 = v0 + a1*(dt/2) + local a2 = k*(t - (x0 + v1*(dt/2))) + v2*d + local v3 = v0 + a2*dt + local x4 = x0 + (v0 + 2*(v1 + v2) + v3)*(dt/6) + self.x, self.v = x4, v0 + (a0 + 2*(a1 + a2) + k*(t - (x0 + v2*dt)) + v3*d)*(dt/6) + return x4 +end + +function Spring.new(stiffness, dampingCoeff, dampingRatio, initialPos) + local self = setmetatable({}, Spring) + + dampingRatio = dampingRatio or 1 + local m = dampingCoeff*dampingCoeff/(4*stiffness*dampingRatio*dampingRatio) + self.k = stiffness/m + self.d = -dampingCoeff/m + self.x = initialPos + self.t = initialPos + self.v = initialPos*0 + + return self +end + +return Spring diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/ChatChannel.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/ChatChannel.lua new file mode 100644 index 0000000..39471f7 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/ChatChannel.lua @@ -0,0 +1,701 @@ +-- // FileName: ChatChannel.lua +-- // Written by: Xsitsu +-- // Description: A representation of one channel that speakers can chat in. + +local forceNewFilterAPI = false +local IN_GAME_CHAT_USE_NEW_FILTER_API +do + local textServiceExists = (game:GetService("TextService") ~= nil) + local success, enabled = pcall(function() return UserSettings():IsUserFeatureEnabled("UserInGameChatUseNewFilterAPIV2") end) + local flagEnabled = (success and enabled) + IN_GAME_CHAT_USE_NEW_FILTER_API = (forceNewFilterAPI or flagEnabled) and textServiceExists +end + +local module = {} + +local modulesFolder = script.Parent +local Chat = game:GetService("Chat") +local RunService = game:GetService("RunService") +local replicatedModules = Chat:WaitForChild("ClientChatModules") + +--////////////////////////////// Include +--////////////////////////////////////// +local ChatConstants = require(replicatedModules:WaitForChild("ChatConstants")) +local Util = require(modulesFolder:WaitForChild("Util")) + +local ChatLocalization = nil +pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end) +if ChatLocalization == nil then ChatLocalization = { Get = function(key,default) return default end } end + +--////////////////////////////// Methods +--////////////////////////////////////// + +local methods = {} +methods.__index = methods + +function methods:SendSystemMessage(message, extraData) + local messageObj = self:InternalCreateMessageObject(message, nil, true, extraData) + + local success, err = pcall(function() self.eMessagePosted:Fire(messageObj) end) + if not success and err then + print("Error posting message: " ..err) + end + + self:InternalAddMessageToHistoryLog(messageObj) + + for i, speaker in pairs(self.Speakers) do + speaker:InternalSendSystemMessage(messageObj, self.Name) + end + + return messageObj +end + +function methods:SendSystemMessageToSpeaker(message, speakerName, extraData) + local speaker = self.Speakers[speakerName] + if (speaker) then + local messageObj = self:InternalCreateMessageObject(message, nil, true, extraData) + speaker:InternalSendSystemMessage(messageObj, self.Name) + else + warn(string.format("Speaker '%s' is not in channel '%s' and cannot be sent a system message", speakerName, self.Name)) + end +end + +function methods:SendMessageObjToFilters(message, messageObj, fromSpeaker) + local oldMessage = messageObj.Message + messageObj.Message = message + self:InternalDoMessageFilter(fromSpeaker.Name, messageObj, self.Name) + self.ChatService:InternalDoMessageFilter(fromSpeaker.Name, messageObj, self.Name) + local newMessage = messageObj.Message + messageObj.Message = oldMessage + return newMessage +end + +function methods:CanCommunicateByUserId(userId1, userId2) + if RunService:IsStudio() then + return true + end + -- UserId is set as 0 for non player speakers. + if userId1 == 0 or userId2 == 0 then + return true + end + local success, canCommunicate = pcall(function() + return Chat:CanUsersChatAsync(userId1, userId2) + end) + return success and canCommunicate +end + +function methods:CanCommunicate(speakerObj1, speakerObj2) + local player1 = speakerObj1:GetPlayer() + local player2 = speakerObj2:GetPlayer() + if player1 and player2 then + return self:CanCommunicateByUserId(player1.UserId, player2.UserId) + end + return true +end + +function methods:SendMessageToSpeaker(message, speakerName, fromSpeakerName, extraData) + local speakerTo = self.Speakers[speakerName] + local speakerFrom = self.ChatService:GetSpeaker(fromSpeakerName) + if speakerTo and speakerFrom then + local isMuted = speakerTo:IsSpeakerMuted(fromSpeakerName) + if isMuted then + return + end + + if not self:CanCommunicate(speakerTo, speakerFrom) then + return + end + + -- We need to claim the message is filtered even if it not in this case for compatibility with legacy client side code. + local isFiltered = speakerName == fromSpeakerName + local messageObj = self:InternalCreateMessageObject(message, fromSpeakerName, isFiltered, extraData) + message = self:SendMessageObjToFilters(message, messageObj, fromSpeakerName) + speakerTo:InternalSendMessage(messageObj, self.Name) + + --// START FFLAG + if (not IN_GAME_CHAT_USE_NEW_FILTER_API) then --// USES FFLAG + --// OLD BEHAVIOR + local filteredMessage = self.ChatService:InternalApplyRobloxFilter(messageObj.FromSpeaker, message, speakerName) + if filteredMessage then + messageObj.Message = filteredMessage + messageObj.IsFiltered = true + speakerTo:InternalSendFilteredMessage(messageObj, self.Name) + end + --// OLD BEHAVIOR + else + --// NEW BEHAVIOR + local textContext = self.Private and Enum.TextFilterContext.PrivateChat or Enum.TextFilterContext.PublicChat + local filterSuccess, isFilterResult, filteredMessage = self.ChatService:InternalApplyRobloxFilterNewAPI( + messageObj.FromSpeaker, + message, + textContext + ) + if (filterSuccess) then + messageObj.FilterResult = filteredMessage + messageObj.IsFilterResult = isFilterResult + messageObj.IsFiltered = true + speakerTo:InternalSendFilteredMessageWithFilterResult(messageObj, self.Name) + end + --// NEW BEHAVIOR + end + --// END FFLAG + else + warn(string.format("Speaker '%s' is not in channel '%s' and cannot be sent a message", speakerName, self.Name)) + end +end + +function methods:KickSpeaker(speakerName, reason) + local speaker = self.ChatService:GetSpeaker(speakerName) + if (not speaker) then + error("Speaker \"" .. speakerName .. "\" does not exist!") + end + + local messageToSpeaker = "" + local messageToChannel = "" + + if (reason) then + messageToSpeaker = string.format("You were kicked from '%s' for the following reason(s): %s", self.Name, reason) + messageToChannel = string.format("%s was kicked for the following reason(s): %s", speakerName, reason) + else + messageToSpeaker = string.format("You were kicked from '%s'", self.Name) + messageToChannel = string.format("%s was kicked", speakerName) + end + + self:SendSystemMessageToSpeaker(messageToSpeaker, speakerName) + speaker:LeaveChannel(self.Name) + self:SendSystemMessage(messageToChannel) +end + +function methods:MuteSpeaker(speakerName, reason, length) + local speaker = self.ChatService:GetSpeaker(speakerName) + if (not speaker) then + error("Speaker \"" .. speakerName .. "\" does not exist!") + end + + self.Mutes[speakerName:lower()] = (length == 0 or length == nil) and 0 or (os.time() + length) + + if (reason) then + self:SendSystemMessage(string.format("%s was muted for the following reason(s): %s", speakerName, reason)) + end + + local success, err = pcall(function() self.eSpeakerMuted:Fire(speakerName, reason, length) end) + if not success and err then + print("Error mutting speaker: " ..err) + end + + local spkr = self.ChatService:GetSpeaker(speakerName) + if (spkr) then + local success, err = pcall(function() spkr.eMuted:Fire(self.Name, reason, length) end) + if not success and err then + print("Error mutting speaker: " ..err) + end + end + +end + +function methods:UnmuteSpeaker(speakerName) + local speaker = self.ChatService:GetSpeaker(speakerName) + if (not speaker) then + error("Speaker \"" .. speakerName .. "\" does not exist!") + end + + self.Mutes[speakerName:lower()] = nil + + local success, err = pcall(function() self.eSpeakerUnmuted:Fire(speakerName) end) + if not success and err then + print("Error unmuting speaker: " ..err) + end + + local spkr = self.ChatService:GetSpeaker(speakerName) + if (spkr) then + local success, err = pcall(function() spkr.eUnmuted:Fire(self.Name) end) + if not success and err then + print("Error unmuting speaker: " ..err) + end + end +end + +function methods:IsSpeakerMuted(speakerName) + return (self.Mutes[speakerName:lower()] ~= nil) +end + +function methods:GetSpeakerList() + local list = {} + for i, speaker in pairs(self.Speakers) do + table.insert(list, speaker.Name) + end + return list +end + +function methods:RegisterFilterMessageFunction(funcId, func, priority) + if self.FilterMessageFunctions:HasFunction(funcId) then + error(string.format("FilterMessageFunction '%s' already exists", funcId)) + end + self.FilterMessageFunctions:AddFunction(funcId, func, priority) +end + +function methods:FilterMessageFunctionExists(funcId) + return self.FilterMessageFunctions:HasFunction(funcId) +end + +function methods:UnregisterFilterMessageFunction(funcId) + if not self.FilterMessageFunctions:HasFunction(funcId) then + error(string.format("FilterMessageFunction '%s' does not exists", funcId)) + end + self.FilterMessageFunctions:RemoveFunction(funcId) +end + +function methods:RegisterProcessCommandsFunction(funcId, func, priority) + if self.ProcessCommandsFunctions:HasFunction(funcId) then + error(string.format("ProcessCommandsFunction '%s' already exists", funcId)) + end + self.ProcessCommandsFunctions:AddFunction(funcId, func, priority) +end + +function methods:ProcessCommandsFunctionExists(funcId) + return self.ProcessCommandsFunctions:HasFunction(funcId) +end + +function methods:UnregisterProcessCommandsFunction(funcId) + if not self.ProcessCommandsFunctions:HasFunction(funcId) then + error(string.format("ProcessCommandsFunction '%s' does not exist", funcId)) + end + self.ProcessCommandsFunctions:RemoveFunction(funcId) +end + +local function ShallowCopy(table) + local copy = {} + for i, v in pairs(table) do + copy[i] = v + end + return copy +end + +function methods:GetHistoryLog() + return ShallowCopy(self.ChatHistory) +end + +function methods:GetHistoryLogForSpeaker(speaker) + local userId = -1 + local player = speaker:GetPlayer() + if player then + userId = player.UserId + end + local chatlog = {} + --// START FFLAG + if (not IN_GAME_CHAT_USE_NEW_FILTER_API) then --// USES FFLAG + --// OLD BEHAVIOR + for i = 1, #self.ChatHistory do + local logUserId = self.ChatHistory[i].SpeakerUserId + if self:CanCommunicateByUserId(userId, logUserId) then + table.insert(chatlog, ShallowCopy(self.ChatHistory[i])) + end + end + --// OLD BEHAVIOR + else + --// NEW BEHAVIOR + for i = 1, #self.ChatHistory do + local logUserId = self.ChatHistory[i].SpeakerUserId + if self:CanCommunicateByUserId(userId, logUserId) then + local messageObj = ShallowCopy(self.ChatHistory[i]) + + --// Since we're using the new filter API, we need to convert the stored filter result + --// into an actual string message to send to players for their chat history. + --// System messages aren't filtered the same way, so they just have a regular + --// text value in the Message field. + if (messageObj.MessageType == ChatConstants.MessageTypeDefault or messageObj.MessageType == ChatConstants.MessageTypeMeCommand) then + local filterResult = messageObj.FilterResult + if (messageObj.IsFilterResult) then + if (player) then + messageObj.Message = filterResult:GetChatForUserAsync(player.UserId) + else + messageObj.Message = filterResult:GetNonChatStringForBroadcastAsync() + end + else + messageObj.Message = filterResult + end + end + + table.insert(chatlog, messageObj) + end + end + --// NEW BEHAVIOR + end + --// END FFLAG + return chatlog +end + +--///////////////// Internal-Use Methods +--////////////////////////////////////// +function methods:InternalDestroy() + for i, speaker in pairs(self.Speakers) do + speaker:LeaveChannel(self.Name) + end + + self.eDestroyed:Fire() + + self.eDestroyed:Destroy() + self.eMessagePosted:Destroy() + self.eSpeakerJoined:Destroy() + self.eSpeakerLeft:Destroy() + self.eSpeakerMuted:Destroy() + self.eSpeakerUnmuted:Destroy() +end + +function methods:InternalDoMessageFilter(speakerName, messageObj, channel) + local filtersIterator = self.FilterMessageFunctions:GetIterator() + for funcId, func, priority in filtersIterator do + local success, errorMessage = pcall(function() + func(speakerName, messageObj, channel) + end) + + if not success then + warn(string.format("DoMessageFilter Function '%s' failed for reason: %s", funcId, errorMessage)) + end + end +end + +function methods:InternalDoProcessCommands(speakerName, message, channel) + local commandsIterator = self.ProcessCommandsFunctions:GetIterator() + for funcId, func, priority in commandsIterator do + local success, returnValue = pcall(function() + local ret = func(speakerName, message, channel) + if type(ret) ~= "boolean" then + error("Process command functions must return a bool") + end + return ret + end) + + if not success then + warn(string.format("DoProcessCommands Function '%s' failed for reason: %s", funcId, returnValue)) + elseif returnValue then + return true + end + end + + return false +end + +function methods:InternalPostMessage(fromSpeaker, message, extraData) + if (self:InternalDoProcessCommands(fromSpeaker.Name, message, self.Name)) then return false end + + if (self.Mutes[fromSpeaker.Name:lower()] ~= nil) then + local t = self.Mutes[fromSpeaker.Name:lower()] + if (t > 0 and os.time() > t) then + self:UnmuteSpeaker(fromSpeaker.Name) + else + self:SendSystemMessageToSpeaker(ChatLocalization:Get("GameChat_ChatChannel_MutedInChannel","You are muted and cannot talk in this channel"), fromSpeaker.Name) + return false + end + end + + local messageObj = self:InternalCreateMessageObject(message, fromSpeaker.Name, false, extraData) + message = self:SendMessageObjToFilters(message, messageObj, fromSpeaker) + + local sentToList = {} + for i, speaker in pairs(self.Speakers) do + local isMuted = speaker:IsSpeakerMuted(fromSpeaker.Name) + if not isMuted and self:CanCommunicate(fromSpeaker, speaker) then + table.insert(sentToList, speaker.Name) + if speaker.Name == fromSpeaker.Name then + -- Send unfiltered message to speaker who sent the message. + local cMessageObj = ShallowCopy(messageObj) + cMessageObj.Message = message + cMessageObj.IsFiltered = true + -- We need to claim the message is filtered even if it not in this case for compatibility with legacy client side code. + speaker:InternalSendMessage(cMessageObj, self.Name) + else + speaker:InternalSendMessage(messageObj, self.Name) + end + end + end + + local success, err = pcall(function() self.eMessagePosted:Fire(messageObj) end) + if not success and err then + print("Error posting message: " ..err) + end + + --// START FFLAG + if (not IN_GAME_CHAT_USE_NEW_FILTER_API) then --// USES FFLAG + --// OLD BEHAVIOR + local filteredMessages = {} + for i, speakerName in pairs(sentToList) do + local filteredMessage = self.ChatService:InternalApplyRobloxFilter(messageObj.FromSpeaker, message, speakerName) + if filteredMessage then + filteredMessages[speakerName] = filteredMessage + else + return false + end + end + + for i, speakerName in pairs(sentToList) do + local speaker = self.Speakers[speakerName] + if (speaker) then + local cMessageObj = ShallowCopy(messageObj) + cMessageObj.Message = filteredMessages[speakerName] + cMessageObj.IsFiltered = true + speaker:InternalSendFilteredMessage(cMessageObj, self.Name) + end + end + + local filteredMessage = self.ChatService:InternalApplyRobloxFilter(messageObj.FromSpeaker, message) + if filteredMessage then + messageObj.Message = filteredMessage + else + return false + end + messageObj.IsFiltered = true + self:InternalAddMessageToHistoryLog(messageObj) + --// OLD BEHAVIOR + else + --// NEW BEHAVIOR + local textFilterContext = self.Private and Enum.TextFilterContext.PrivateChat or Enum.TextFilterContext.PublicChat + local filterSuccess, isFilterResult, filteredMessage = self.ChatService:InternalApplyRobloxFilterNewAPI( + messageObj.FromSpeaker, + message, + textFilterContext + ) + if (filterSuccess) then + messageObj.FilterResult = filteredMessage + messageObj.IsFilterResult = isFilterResult + else + return false + end + messageObj.IsFiltered = true + self:InternalAddMessageToHistoryLog(messageObj) + + for _, speakerName in pairs(sentToList) do + local speaker = self.Speakers[speakerName] + if (speaker) then + speaker:InternalSendFilteredMessageWithFilterResult(messageObj, self.Name) + end + end + --// NEW BEHAVIOR + end + --// END FFLAG + + -- One more pass is needed to ensure that no speakers do not recieve the message. + -- Otherwise a user could join while the message is being filtered who had not originally been sent the message. + local speakersMissingMessage = {} + for _, speaker in pairs(self.Speakers) do + local isMuted = speaker:IsSpeakerMuted(fromSpeaker.Name) + if not isMuted and self:CanCommunicate(fromSpeaker, speaker) then + local wasSentMessage = false + for _, sentSpeakerName in pairs(sentToList) do + if speaker.Name == sentSpeakerName then + wasSentMessage = true + break + end + end + if not wasSentMessage then + table.insert(speakersMissingMessage, speaker.Name) + end + end + end + + --// START FFLAG + if (not IN_GAME_CHAT_USE_NEW_FILTER_API) then --// USES FFLAG + --// OLD BEHAVIOR + for _, speakerName in pairs(speakersMissingMessage) do + local speaker = self.Speakers[speakerName] + if speaker then + local filteredMessage = self.ChatService:InternalApplyRobloxFilter(messageObj.FromSpeaker, message, speakerName) + if filteredMessage == nil then + return false + end + local cMessageObj = ShallowCopy(messageObj) + cMessageObj.Message = filteredMessage + cMessageObj.IsFiltered = true + speaker:InternalSendFilteredMessage(cMessageObj, self.Name) + end + end + --// OLD BEHAVIOR + else + --// NEW BEHAVIOR + for _, speakerName in pairs(speakersMissingMessage) do + local speaker = self.Speakers[speakerName] + if speaker then + speaker:InternalSendFilteredMessageWithFilterResult(messageObj, self.Name) + end + end + --// NEW BEHAVIOR + end + --// END FFLAG + + return messageObj +end + +function methods:InternalAddSpeaker(speaker) + if (self.Speakers[speaker.Name]) then + warn("Speaker \"" .. speaker.name .. "\" is already in the channel!") + return + end + + self.Speakers[speaker.Name] = speaker + local success, err = pcall(function() self.eSpeakerJoined:Fire(speaker.Name) end) + if not success and err then + print("Error removing channel: " ..err) + end +end + +function methods:InternalRemoveSpeaker(speaker) + if (not self.Speakers[speaker.Name]) then + warn("Speaker \"" .. speaker.name .. "\" is not in the channel!") + return + end + + self.Speakers[speaker.Name] = nil + local success, err = pcall(function() self.eSpeakerLeft:Fire(speaker.Name) end) + if not success and err then + print("Error removing speaker: " ..err) + end +end + +function methods:InternalRemoveExcessMessagesFromLog() + local remove = table.remove + while (#self.ChatHistory > self.MaxHistory) do + remove(self.ChatHistory, 1) + end +end + +function methods:InternalAddMessageToHistoryLog(messageObj) + table.insert(self.ChatHistory, messageObj) + + self:InternalRemoveExcessMessagesFromLog() +end + +function methods:GetMessageType(message, fromSpeaker) + if fromSpeaker == nil then + return ChatConstants.MessageTypeSystem + end + return ChatConstants.MessageTypeDefault +end + +function methods:InternalCreateMessageObject(message, fromSpeaker, isFiltered, extraData) + local messageType = self:GetMessageType(message, fromSpeaker) + + local speakerUserId = -1 + local speaker = nil + + if fromSpeaker then + speaker = self.Speakers[fromSpeaker] + if speaker then + local player = speaker:GetPlayer() + if player then + speakerUserId = player.UserId + else + speakerUserId = 0 + end + end + end + + local messageObj = + { + ID = self.ChatService:InternalGetUniqueMessageId(), + FromSpeaker = fromSpeaker, + SpeakerUserId = speakerUserId, + OriginalChannel = self.Name, + MessageLength = string.len(message), + MessageType = messageType, + IsFiltered = isFiltered, + Message = isFiltered and message or nil, + --// These two get set by the new API. The comments are just here + --// to remind readers that they will exist so it's not super + --// confusing if they find them in the code but cannot find them + --// here. + --FilterResult = nil, + --IsFilterResult = false, + Time = os.time(), + ExtraData = {}, + } + + if speaker then + for k, v in pairs(speaker.ExtraData) do + messageObj.ExtraData[k] = v + end + end + + if (extraData) then + for k, v in pairs(extraData) do + messageObj.ExtraData[k] = v + end + end + + return messageObj +end + +function methods:SetChannelNameColor(color) + self.ChannelNameColor = color + for i, speaker in pairs(self.Speakers) do + speaker:UpdateChannelNameColor(self.Name, color) + end +end + +function methods:GetWelcomeMessageForSpeaker(speaker) + if self.GetWelcomeMessageFunction then + local welcomeMessage = self.GetWelcomeMessageFunction(speaker) + if welcomeMessage then + return welcomeMessage + end + end + return self.WelcomeMessage +end + +function methods:RegisterGetWelcomeMessageFunction(func) + if type(func) ~= "function" then + error("RegisterGetWelcomeMessageFunction must be called with a function.") + end + self.GetWelcomeMessageFunction = func +end + +function methods:UnRegisterGetWelcomeMessageFunction() + self.GetWelcomeMessageFunction = nil +end + +--///////////////////////// Constructors +--////////////////////////////////////// + +function module.new(vChatService, name, welcomeMessage, channelNameColor) + local obj = setmetatable({}, methods) + + obj.ChatService = vChatService + + obj.Name = name + obj.WelcomeMessage = welcomeMessage or "" + obj.GetWelcomeMessageFunction = nil + obj.ChannelNameColor = channelNameColor + + obj.Joinable = true + obj.Leavable = true + obj.AutoJoin = false + obj.Private = false + + obj.Speakers = {} + obj.Mutes = {} + + obj.MaxHistory = 200 + obj.HistoryIndex = 0 + obj.ChatHistory = {} + + obj.FilterMessageFunctions = Util:NewSortedFunctionContainer() + obj.ProcessCommandsFunctions = Util:NewSortedFunctionContainer() + + -- Make sure to destroy added binadable events in the InternalDestroy method. + obj.eDestroyed = Instance.new("BindableEvent") + obj.eMessagePosted = Instance.new("BindableEvent") + obj.eSpeakerJoined = Instance.new("BindableEvent") + obj.eSpeakerLeft = Instance.new("BindableEvent") + obj.eSpeakerMuted = Instance.new("BindableEvent") + obj.eSpeakerUnmuted = Instance.new("BindableEvent") + + obj.MessagePosted = obj.eMessagePosted.Event + obj.SpeakerJoined = obj.eSpeakerJoined.Event + obj.SpeakerLeft = obj.eSpeakerLeft.Event + obj.SpeakerMuted = obj.eSpeakerMuted.Event + obj.SpeakerUnmuted = obj.eSpeakerUnmuted.Event + obj.Destroyed = obj.eDestroyed.Event + + return obj +end + +return module diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/ChatService.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/ChatService.lua new file mode 100644 index 0000000..4683d6d --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/ChatService.lua @@ -0,0 +1,453 @@ +-- // FileName: ChatService.lua +-- // Written by: Xsitsu +-- // Description: Manages creating and destroying ChatChannels and Speakers. + +local MAX_FILTER_RETRIES = 3 +local FILTER_BACKOFF_INTERVALS = {50/1000, 100/1000, 200/1000} +local MAX_FILTER_DURATION = 60 + +--- Constants used to decide when to notify that the chat filter is having issues filtering messages. +local FILTER_NOTIFCATION_THRESHOLD = 3 --Number of notifcation failures before an error message is output. +local FILTER_NOTIFCATION_INTERVAL = 60 --Time between error messages. +local FILTER_THRESHOLD_TIME = 60 --If there has not been an issue in this many seconds, the count of issues resets. + +local module = {} + +local RunService = game:GetService("RunService") +local Chat = game:GetService("Chat") +local ReplicatedModules = Chat:WaitForChild("ClientChatModules") + +local modulesFolder = script.Parent +local ReplicatedModules = Chat:WaitForChild("ClientChatModules") +local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings")) + +local errorTextColor = ChatSettings.ErrorMessageTextColor or Color3.fromRGB(245, 50, 50) +local errorExtraData = {ChatColor = errorTextColor} + +--////////////////////////////// Include +--////////////////////////////////////// +local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants")) + +local ChatChannel = require(modulesFolder:WaitForChild("ChatChannel")) +local Speaker = require(modulesFolder:WaitForChild("Speaker")) +local Util = require(modulesFolder:WaitForChild("Util")) + +local ChatLocalization = nil +pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end) +if ChatLocalization == nil then ChatLocalization = {} function ChatLocalization:Get(key,default) return default end end + +--////////////////////////////// Methods +--////////////////////////////////////// +local methods = {} +methods.__index = methods + +function methods:AddChannel(channelName, autoJoin) + if (self.ChatChannels[channelName:lower()]) then + error(string.format("Channel %q alrady exists.", channelName)) + end + + local function DefaultChannelCommands(fromSpeaker, message) + if (message:lower() == "/leave") then + local channel = self:GetChannel(channelName) + local speaker = self:GetSpeaker(fromSpeaker) + if (channel and speaker) then + if (channel.Leavable) then + speaker:LeaveChannel(channelName) + speaker:SendSystemMessage( + string.gsub( + ChatLocalization:Get( + "GameChat_ChatService_YouHaveLeftChannel", + string.format("You have left channel '%s'", channelName) + ), + "{RBX_NAME}",channelName), + "System" + ) + else + speaker:SendSystemMessage(ChatLocalization:Get("GameChat_ChatService_CannotLeaveChannel","You cannot leave this channel."), channelName) + end + end + + return true + end + return false + end + + + local channel = ChatChannel.new(self, channelName) + self.ChatChannels[channelName:lower()] = channel + + channel:RegisterProcessCommandsFunction("default_commands", DefaultChannelCommands, ChatConstants.HighPriority) + + local success, err = pcall(function() self.eChannelAdded:Fire(channelName) end) + if not success and err then + print("Error addding channel: " ..err) + end + + if autoJoin ~= nil then + channel.AutoJoin = autoJoin + if autoJoin then + for _, speaker in pairs(self.Speakers) do + speaker:JoinChannel(channelName) + end + end + end + + return channel +end + +function methods:RemoveChannel(channelName) + if (self.ChatChannels[channelName:lower()]) then + local n = self.ChatChannels[channelName:lower()].Name + + self.ChatChannels[channelName:lower()]:InternalDestroy() + self.ChatChannels[channelName:lower()] = nil + + local success, err = pcall(function() self.eChannelRemoved:Fire(n) end) + if not success and err then + print("Error removing channel: " ..err) + end + else + warn(string.format("Channel %q does not exist.", channelName)) + end +end + +function methods:GetChannel(channelName) + return self.ChatChannels[channelName:lower()] +end + + +function methods:AddSpeaker(speakerName) + if (self.Speakers[speakerName:lower()]) then + error("Speaker \"" .. speakerName .. "\" already exists!") + end + + local speaker = Speaker.new(self, speakerName) + self.Speakers[speakerName:lower()] = speaker + + local success, err = pcall(function() self.eSpeakerAdded:Fire(speakerName) end) + if not success and err then + print("Error adding speaker: " ..err) + end + + return speaker +end + +function methods:InternalUnmuteSpeaker(speakerName) + for channelName, channel in pairs(self.ChatChannels) do + if channel:IsSpeakerMuted(speakerName) then + channel:UnmuteSpeaker(speakerName) + end + end +end + +function methods:RemoveSpeaker(speakerName) + if (self.Speakers[speakerName:lower()]) then + local n = self.Speakers[speakerName:lower()].Name + self:InternalUnmuteSpeaker(n) + + self.Speakers[speakerName:lower()]:InternalDestroy() + self.Speakers[speakerName:lower()] = nil + + local success, err = pcall(function() self.eSpeakerRemoved:Fire(n) end) + if not success and err then + print("Error removing speaker: " ..err) + end + + else + warn("Speaker \"" .. speakerName .. "\" does not exist!") + end +end + +function methods:GetSpeaker(speakerName) + return self.Speakers[speakerName:lower()] +end + +function methods:GetChannelList() + local list = {} + for i, channel in pairs(self.ChatChannels) do + if (not channel.Private) then + table.insert(list, channel.Name) + end + end + return list +end + +function methods:GetAutoJoinChannelList() + local list = {} + for i, channel in pairs(self.ChatChannels) do + if channel.AutoJoin then + table.insert(list, channel) + end + end + return list +end + +function methods:GetSpeakerList() + local list = {} + for i, speaker in pairs(self.Speakers) do + table.insert(list, speaker.Name) + end + return list +end + +function methods:SendGlobalSystemMessage(message) + for i, speaker in pairs(self.Speakers) do + speaker:SendSystemMessage(message, nil) + end +end + +function methods:RegisterFilterMessageFunction(funcId, func, priority) + if self.FilterMessageFunctions:HasFunction(funcId) then + error(string.format("FilterMessageFunction '%s' already exists", funcId)) + end + self.FilterMessageFunctions:AddFunction(funcId, func, priority) +end + +function methods:FilterMessageFunctionExists(funcId) + return self.FilterMessageFunctions:HasFunction(funcId) +end + +function methods:UnregisterFilterMessageFunction(funcId) + if not self.FilterMessageFunctions:HasFunction(funcId) then + error(string.format("FilterMessageFunction '%s' does not exists", funcId)) + end + self.FilterMessageFunctions:RemoveFunction(funcId) +end + +function methods:RegisterProcessCommandsFunction(funcId, func, priority) + if self.ProcessCommandsFunctions:HasFunction(funcId) then + error(string.format("ProcessCommandsFunction '%s' already exists", funcId)) + end + self.ProcessCommandsFunctions:AddFunction(funcId, func, priority) +end + +function methods:ProcessCommandsFunctionExists(funcId) + return self.ProcessCommandsFunctions:HasFunction(funcId) +end + +function methods:UnregisterProcessCommandsFunction(funcId) + if not self.ProcessCommandsFunctions:HasFunction(funcId) then + error(string.format("ProcessCommandsFunction '%s' does not exist", funcId)) + end + self.ProcessCommandsFunctions:RemoveFunction(funcId) +end + +local LastFilterNoficationTime = 0 +local LastFilterIssueTime = 0 +local FilterIssueCount = 0 +function methods:InternalNotifyFilterIssue() + if (tick() - LastFilterIssueTime) > FILTER_THRESHOLD_TIME then + FilterIssueCount = 0 + end + FilterIssueCount = FilterIssueCount + 1 + LastFilterIssueTime = tick() + if FilterIssueCount >= FILTER_NOTIFCATION_THRESHOLD then + if (tick() - LastFilterNoficationTime) > FILTER_NOTIFCATION_INTERVAL then + LastFilterNoficationTime = tick() + local systemChannel = self:GetChannel("System") + if systemChannel then + systemChannel:SendSystemMessage( + ChatLocalization:Get( + "GameChat_ChatService_ChatFilterIssues", + "The chat filter is currently experiencing issues and messages may be slow to appear." + ), + errorExtraData + ) + end + end + end +end + +local StudioMessageFilteredCache = {} + +--///////////////// Internal-Use Methods +--////////////////////////////////////// +--DO NOT REMOVE THIS. Chat must be filtered or your game will face +--moderation. +function methods:InternalApplyRobloxFilter(speakerName, message, toSpeakerName) --// USES FFLAG + if (RunService:IsServer() and not RunService:IsStudio()) then + local fromSpeaker = self:GetSpeaker(speakerName) + local toSpeaker = toSpeakerName and self:GetSpeaker(toSpeakerName) + + if fromSpeaker == nil then + return nil + end + + local fromPlayerObj = fromSpeaker:GetPlayer() + local toPlayerObj = toSpeaker and toSpeaker:GetPlayer() + + if fromPlayerObj == nil then + return message + end + + local filterStartTime = tick() + local filterRetries = 0 + while true do + local success, message = pcall(function() + if toPlayerObj then + return Chat:FilterStringAsync(message, fromPlayerObj, toPlayerObj) + else + return Chat:FilterStringForBroadcast(message, fromPlayerObj) + end + end) + if success then + return message + else + warn("Error filtering message:", message) + end + filterRetries = filterRetries + 1 + if filterRetries > MAX_FILTER_RETRIES or (tick() - filterStartTime) > MAX_FILTER_DURATION then + self:InternalNotifyFilterIssue() + return nil + end + local backoffInterval = FILTER_BACKOFF_INTERVALS[math.min(#FILTER_BACKOFF_INTERVALS, filterRetries)] + -- backoffWait = backoffInterval +/- (0 -> backoffInterval) + local backoffWait = backoffInterval + ((math.random()*2 - 1) * backoffInterval) + wait(backoffWait) + end + else + --// Simulate filtering latency. + --// There is only latency the first time the message is filtered, all following calls will be instant. + if not StudioMessageFilteredCache[message] then + StudioMessageFilteredCache[message] = true + wait() + end + return message + end + + return nil +end + +--// Return values: bool filterSuccess, bool resultIsFilterObject, variant result +function methods:InternalApplyRobloxFilterNewAPI(speakerName, message, textFilterContext) --// USES FFLAG + local alwaysRunFilter = false + local runFilter = RunService:IsServer() and not RunService:IsStudio() + if (alwaysRunFilter or runFilter) then + local fromSpeaker = self:GetSpeaker(speakerName) + if fromSpeaker == nil then + return false, nil, nil + end + + local fromPlayerObj = fromSpeaker:GetPlayer() + if fromPlayerObj == nil then + return true, false, message + end + + local success, filterResult = pcall(function() + local ts = game:GetService("TextService") + local result = ts:FilterStringAsync(message, fromPlayerObj.UserId, textFilterContext) + return result + end) + if (success) then + return true, true, filterResult + else + warn("Error filtering message:", message, filterResult) + self:InternalNotifyFilterIssue() + return false, nil, nil + end + end + + --// Simulate filtering latency. + wait() + return true, false, message +end + +function methods:InternalDoMessageFilter(speakerName, messageObj, channel) + local filtersIterator = self.FilterMessageFunctions:GetIterator() + + for funcId, func, priority in filtersIterator do + local success, errorMessage = pcall(function() + func(speakerName, messageObj, channel) + end) + + if not success then + warn(string.format("DoMessageFilter Function '%s' failed for reason: %s", funcId, errorMessage)) + end + end +end + +function methods:InternalDoProcessCommands(speakerName, message, channel) + local commandsIterator = self.ProcessCommandsFunctions:GetIterator() + + for funcId, func, priority in commandsIterator do + local success, returnValue = pcall(function() + local ret = func(speakerName, message, channel) + if type(ret) ~= "boolean" then + error("Process command functions must return a bool") + end + return ret + end) + + if not success then + warn(string.format("DoProcessCommands Function '%s' failed for reason: %s", funcId, returnValue)) + elseif returnValue then + return true + end + end + + return false +end + +function methods:InternalGetUniqueMessageId() + local id = self.MessageIdCounter + self.MessageIdCounter = id + 1 + return id +end + +function methods:InternalAddSpeakerWithPlayerObject(speakerName, playerObj, fireSpeakerAdded) + if (self.Speakers[speakerName:lower()]) then + error("Speaker \"" .. speakerName .. "\" already exists!") + end + + local speaker = Speaker.new(self, speakerName) + speaker:InternalAssignPlayerObject(playerObj) + self.Speakers[speakerName:lower()] = speaker + + if fireSpeakerAdded then + local success, err = pcall(function() self.eSpeakerAdded:Fire(speakerName) end) + if not success and err then + print("Error adding speaker: " ..err) + end + end + + return speaker +end + +function methods:InternalFireSpeakerAdded(speakerName) + local success, err = pcall(function() self.eSpeakerAdded:Fire(speakerName) end) + if not success and err then + print("Error firing speaker added: " ..err) + end +end + +--///////////////////////// Constructors +--////////////////////////////////////// + +function module.new() + local obj = setmetatable({}, methods) + + obj.MessageIdCounter = 0 + + obj.ChatChannels = {} + obj.Speakers = {} + + obj.FilterMessageFunctions = Util:NewSortedFunctionContainer() + obj.ProcessCommandsFunctions = Util:NewSortedFunctionContainer() + + obj.eChannelAdded = Instance.new("BindableEvent") + obj.eChannelRemoved = Instance.new("BindableEvent") + obj.eSpeakerAdded = Instance.new("BindableEvent") + obj.eSpeakerRemoved = Instance.new("BindableEvent") + + obj.ChannelAdded = obj.eChannelAdded.Event + obj.ChannelRemoved = obj.eChannelRemoved.Event + obj.SpeakerAdded = obj.eSpeakerAdded.Event + obj.SpeakerRemoved = obj.eSpeakerRemoved.Event + + obj.ChatServiceMajorVersion = 0 + obj.ChatServiceMinorVersion = 5 + + return obj +end + +return module.new() diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/ChatServiceInstaller.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/ChatServiceInstaller.lua new file mode 100644 index 0000000..9ba1e5d --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/ChatServiceInstaller.lua @@ -0,0 +1,115 @@ +local runnerScriptName = "ChatServiceRunner" + +local installDirectory = game:GetService("Chat") +local ServerScriptService = game:GetService("ServerScriptService") + +local ChatServiceInstallerExemptLocalizationFromAnalytics = settings():GetFFlag("ChatServiceInstallerExemptLocalizationFromAnalytics") + +local function LoadScript(name, parent) + local originalModule = script.Parent:WaitForChild(name) + local script = Instance.new("Script") + script.Name = name + script.Source = originalModule.Source + script.Parent = parent + return script +end + +local function LoadModule(location, name, parent) + local originalModule = location:WaitForChild(name) + local module = Instance.new("ModuleScript") + module.Name = name + module.Source = originalModule.Source + module.Parent = parent + return module +end + +local function GetBoolValue(parent, name, defaultValue) + local boolValue = parent:FindFirstChild(name) + if boolValue then + if boolValue:IsA("BoolValue") then + return boolValue.Value + end + end + return defaultValue +end + +local function makeDefaultLocalizationTable(parent) + local defaultChatLocalization = Instance.new("LocalizationTable") + defaultChatLocalization.Name = "ChatLocalization" + defaultChatLocalization.Archivable = false + defaultChatLocalization.SourceLocaleId = "en-us" + defaultChatLocalization:SetEntries(require(script.Parent:WaitForChild("DefaultChatLocalization"))) + defaultChatLocalization:SetIsExemptFromUGCAnalytics(true) + defaultChatLocalization.Parent = parent; +end + +local function Install() + local existingChatLocalization + + if ChatServiceInstallerExemptLocalizationFromAnalytics then + existingChatLocalization = installDirectory:FindFirstChild("ChatLocalization") + if existingChatLocalization then + if existingChatLocalization:IsA("LocalizationTable" ) then + existingChatLocalization:SetIsExemptFromUGCAnalytics(true) + end + else + makeDefaultLocalizationTable(installDirectory) + end + else + if not installDirectory:FindFirstChild("ChatLocalization") then + local defaultChatLocalization = Instance.new("LocalizationTable") + defaultChatLocalization.Name = "ChatLocalization" + defaultChatLocalization.Archivable = false + defaultChatLocalization.SourceLocaleId = "en-us" + defaultChatLocalization:SetEntries(require(script.Parent:WaitForChild("DefaultChatLocalization"))) + defaultChatLocalization.Parent = installDirectory + end + end + + local chatServiceRunnerArchivable = true + local ChatServiceRunner = installDirectory:FindFirstChild(runnerScriptName) + if not ChatServiceRunner then + chatServiceRunnerArchivable = false + ChatServiceRunner = LoadScript(runnerScriptName, installDirectory) + + LoadModule(script.Parent, "ChatService", ChatServiceRunner) + LoadModule(script.Parent, "ChatChannel", ChatServiceRunner) + LoadModule(script.Parent, "Speaker", ChatServiceRunner) + LoadModule(script.Parent, "Util", ChatServiceRunner) + end + + local ChatModules = installDirectory:FindFirstChild("ChatModules") + if not ChatModules then + ChatModules = Instance.new("Folder") + ChatModules.Name = "ChatModules" + ChatModules.Archivable = false + + local InsertDefaults = Instance.new("BoolValue") + InsertDefaults.Name = "InsertDefaultModules" + InsertDefaults.Value = true + InsertDefaults.Parent = ChatModules + + ChatModules.Parent = installDirectory + end + + local shouldInsertDefaultModules = GetBoolValue(ChatModules, "InsertDefaultModules", false) + + if shouldInsertDefaultModules then + local defaultChatModules = script.Parent.DefaultChatModules:GetChildren() + for i = 1, #defaultChatModules do + if not ChatModules:FindFirstChild(defaultChatModules[i].Name) then + LoadModule(script.Parent.DefaultChatModules, defaultChatModules[i].Name, ChatModules) + end + end + end + + if not ServerScriptService:FindFirstChild(runnerScriptName) then + local ChatServiceRunnerCopy = ChatServiceRunner:Clone() + ChatServiceRunnerCopy.Archivable = false + ChatServiceRunnerCopy.Parent = ServerScriptService + end + + ChatServiceRunner.Archivable = chatServiceRunnerArchivable +end + +return Install diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/ChatServiceRunner.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/ChatServiceRunner.lua new file mode 100644 index 0000000..5659d31 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/ChatServiceRunner.lua @@ -0,0 +1,443 @@ +-- // FileName: ChatServiceRunner.lua +-- // Written by: Xsitsu +-- // Description: Main script to initialize ChatService and run ChatModules. + +local EventFolderName = "DefaultChatSystemChatEvents" +local EventFolderParent = game:GetService("ReplicatedStorage") +local modulesFolder = script + +local PlayersService = game:GetService("Players") +local RunService = game:GetService("RunService") +local Chat = game:GetService("Chat") + +local ChatService = require(modulesFolder:WaitForChild("ChatService")) + +local ReplicatedModules = Chat:WaitForChild("ClientChatModules") +local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings")) + +local ChatLocalization = nil +pcall(function() ChatLocalization = require(Chat.ClientChatModules.ChatLocalization) end) +if ChatLocalization == nil then ChatLocalization = { Get = function(key,default) return default end } end + +local useEvents = {} + +local EventFolder = EventFolderParent:FindFirstChild(EventFolderName) +if (not EventFolder) then + EventFolder = Instance.new("Folder") + EventFolder.Name = EventFolderName + EventFolder.Archivable = false + EventFolder.Parent = EventFolderParent +end + +--// No-opt connect Server>Client RemoteEvents to ensure they cannot be called +--// to fill the remote event queue. +local function emptyFunction() + --intentially empty +end + +local function GetObjectWithNameAndType(parentObject, objectName, objectType) + for _, child in pairs(parentObject:GetChildren()) do + if (child:IsA(objectType) and child.Name == objectName) then + return child + end + end + + return nil +end + +local function CreateIfDoesntExist(parentObject, objectName, objectType) + local obj = GetObjectWithNameAndType(parentObject, objectName, objectType) + if (not obj) then + obj = Instance.new(objectType) + obj.Name = objectName + obj.Parent = parentObject + end + useEvents[objectName] = obj + + return obj +end + +--// All remote events will have a no-opt OnServerEvent connecdted on construction +local function CreateEventIfItDoesntExist(parentObject, objectName) + local obj = CreateIfDoesntExist(parentObject, objectName, "RemoteEvent") + obj.OnServerEvent:Connect(emptyFunction) + return obj +end + +CreateEventIfItDoesntExist(EventFolder, "OnNewMessage") +CreateEventIfItDoesntExist(EventFolder, "OnMessageDoneFiltering") +CreateEventIfItDoesntExist(EventFolder, "OnNewSystemMessage") +CreateEventIfItDoesntExist(EventFolder, "OnChannelJoined") +CreateEventIfItDoesntExist(EventFolder, "OnChannelLeft") +CreateEventIfItDoesntExist(EventFolder, "OnMuted") +CreateEventIfItDoesntExist(EventFolder, "OnUnmuted") +CreateEventIfItDoesntExist(EventFolder, "OnMainChannelSet") +CreateEventIfItDoesntExist(EventFolder, "ChannelNameColorUpdated") + +CreateEventIfItDoesntExist(EventFolder, "SayMessageRequest") +CreateEventIfItDoesntExist(EventFolder, "SetBlockedUserIdsRequest") +CreateIfDoesntExist(EventFolder, "GetInitDataRequest", "RemoteFunction") +CreateIfDoesntExist(EventFolder, "MutePlayerRequest", "RemoteFunction") +CreateIfDoesntExist(EventFolder, "UnMutePlayerRequest", "RemoteFunction") + +EventFolder = useEvents + +local function CreatePlayerSpeakerObject(playerObj) + --// If a developer already created a speaker object with the + --// name of a player and then a player joins and tries to + --// take that name, we first need to remove the old speaker object + local speaker = ChatService:GetSpeaker(playerObj.Name) + if (speaker) then + ChatService:RemoveSpeaker(playerObj.Name) + end + + speaker = ChatService:InternalAddSpeakerWithPlayerObject(playerObj.Name, playerObj, false) + + for _, channel in pairs(ChatService:GetAutoJoinChannelList()) do + speaker:JoinChannel(channel.Name) + end + + speaker.ReceivedUnfilteredMessage:connect(function(messageObj, channel) + EventFolder.OnNewMessage:FireClient(playerObj, messageObj, channel) + end) + + speaker.MessageDoneFiltering:connect(function(messageObj, channel) + EventFolder.OnMessageDoneFiltering:FireClient(playerObj, messageObj, channel) + end) + + speaker.ReceivedSystemMessage:connect(function(messageObj, channel) + EventFolder.OnNewSystemMessage:FireClient(playerObj, messageObj, channel) + end) + + speaker.ChannelJoined:connect(function(channel, welcomeMessage) + local log = nil + local channelNameColor = nil + + local channelObject = ChatService:GetChannel(channel) + if (channelObject) then + log = channelObject:GetHistoryLogForSpeaker(speaker) + channelNameColor = channelObject.ChannelNameColor + end + EventFolder.OnChannelJoined:FireClient(playerObj, channel, welcomeMessage, log, channelNameColor) + end) + + speaker.ChannelLeft:connect(function(channel) + EventFolder.OnChannelLeft:FireClient(playerObj, channel) + end) + + speaker.Muted:connect(function(channel, reason, length) + EventFolder.OnMuted:FireClient(playerObj, channel, reason, length) + end) + + speaker.Unmuted:connect(function(channel) + EventFolder.OnUnmuted:FireClient(playerObj, channel) + end) + + speaker.MainChannelSet:connect(function(channel) + EventFolder.OnMainChannelSet:FireClient(playerObj, channel) + end) + + speaker.ChannelNameColorUpdated:connect(function(channelName, channelNameColor) + EventFolder.ChannelNameColorUpdated:FireClient(playerObj, channelName, channelNameColor) + end) + + ChatService:InternalFireSpeakerAdded(speaker.Name) +end + +EventFolder.SayMessageRequest.OnServerEvent:connect(function(playerObj, message, channel) + if type(message) ~= "string" then + return + end + if type(channel) ~= "string" then + return + end + + local speaker = ChatService:GetSpeaker(playerObj.Name) + if (speaker) then + return speaker:SayMessage(message, channel) + end + + return nil +end) + +EventFolder.MutePlayerRequest.OnServerInvoke = function(playerObj, muteSpeakerName) + if type(muteSpeakerName) ~= "string" then + return + end + + local speaker = ChatService:GetSpeaker(playerObj.Name) + if speaker then + local muteSpeaker = ChatService:GetSpeaker(muteSpeakerName) + if muteSpeaker then + speaker:AddMutedSpeaker(muteSpeaker.Name) + return true + end + end + return false +end + +EventFolder.UnMutePlayerRequest.OnServerInvoke = function(playerObj, unmuteSpeakerName) + if type(unmuteSpeakerName) ~= "string" then + return + end + + local speaker = ChatService:GetSpeaker(playerObj.Name) + if speaker then + local unmuteSpeaker = ChatService:GetSpeaker(unmuteSpeakerName) + if unmuteSpeaker then + speaker:RemoveMutedSpeaker(unmuteSpeaker.Name) + return true + end + end + return false +end + +-- Map storing Player -> Blocked user Ids. +local BlockedUserIdsMap = {} + +PlayersService.PlayerAdded:connect(function(newPlayer) + for player, blockedUsers in pairs(BlockedUserIdsMap) do + local speaker = ChatService:GetSpeaker(player.Name) + if speaker then + for i = 1, #blockedUsers do + local blockedUserId = blockedUsers[i] + if blockedUserId == newPlayer.UserId then + speaker:AddMutedSpeaker(newPlayer.Name) + end + end + end + end +end) + +PlayersService.PlayerRemoving:connect(function(removingPlayer) + BlockedUserIdsMap[removingPlayer] = nil +end) + +EventFolder.SetBlockedUserIdsRequest.OnServerEvent:connect(function(player, blockedUserIdsList) + if type(blockedUserIdsList) ~= "table" then + return + end + + BlockedUserIdsMap[player] = blockedUserIdsList + local speaker = ChatService:GetSpeaker(player.Name) + if speaker then + for i = 1, #blockedUserIdsList do + if type(blockedUserIdsList[i]) == "number" then + local blockedPlayer = PlayersService:GetPlayerByUserId(blockedUserIdsList[i]) + if blockedPlayer then + speaker:AddMutedSpeaker(blockedPlayer.Name) + end + end + end + end +end) + +EventFolder.GetInitDataRequest.OnServerInvoke = (function(playerObj) + local speaker = ChatService:GetSpeaker(playerObj.Name) + if not (speaker and speaker:GetPlayer()) then + CreatePlayerSpeakerObject(playerObj) + speaker = ChatService:GetSpeaker(playerObj.Name) + end + + local data = {} + data.Channels = {} + data.SpeakerExtraData = {} + + for _, channelName in pairs(speaker:GetChannelList()) do + local channelObj = ChatService:GetChannel(channelName) + if (channelObj) then + local channelData = + { + channelName, + channelObj:GetWelcomeMessageForSpeaker(speaker), + channelObj:GetHistoryLogForSpeaker(speaker), + channelObj.ChannelNameColor, + } + + table.insert(data.Channels, channelData) + end + end + + for _, oSpeakerName in pairs(ChatService:GetSpeakerList()) do + local oSpeaker = ChatService:GetSpeaker(oSpeakerName) + data.SpeakerExtraData[oSpeakerName] = oSpeaker.ExtraData + end + + return data +end) + +local function DoJoinCommand(speakerName, channelName, fromChannelName) + local speaker = ChatService:GetSpeaker(speakerName) + local channel = ChatService:GetChannel(channelName) + + if (speaker) then + if (channel) then + if (channel.Joinable) then + if (not speaker:IsInChannel(channel.Name)) then + speaker:JoinChannel(channel.Name) + else + speaker:SetMainChannel(channel.Name) + speaker:SendSystemMessage( + string.gsub( + ChatLocalization:Get( + "GameChat_SwitchChannel_NowInChannel", + string.format("You are now chatting in channel: '%s'", channel.Name) + ), + "{RBX_NAME}",channel.Name), + channel.Name + ) + end + else + speaker:SendSystemMessage( + string.gsub( + ChatLocalization:Get( + "GameChat_ChatServiceRunner_YouCannotJoinChannel", + ("You cannot join channel '" .. channelName .. "'.") + ), + "{RBX_NAME}",channelName), + fromChannelName + ) + end + else + speaker:SendSystemMessage( + string.gsub( + ChatLocalization:Get( + "GameChat_ChatServiceRunner_ChannelDoesNotExist", + ("Channel '" .. channelName .. "' does not exist.") + ), + "{RBX_NAME}",channelName), + fromChannelName + ) + end + end +end + +local function DoLeaveCommand(speakerName, channelName, fromChannelName) + local speaker = ChatService:GetSpeaker(speakerName) + local channel = ChatService:GetChannel(channelName) + + if (speaker) then + if (speaker:IsInChannel(channelName)) then + if (channel.Leavable) then + speaker:LeaveChannel(channel.Name) + speaker:SendSystemMessage( + string.gsub( + ChatLocalization:Get( + "GameChat_ChatService_YouHaveLeftChannel", + string.format("You have left channel '%s'", channelName) + ), + "{RBX_NAME}",channel.Name), + "System" + ) + else + speaker:SendSystemMessage( + string.gsub( + ChatLocalization:Get( + "GameChat_ChatServiceRunner_YouCannotLeaveChannel", + ("You cannot leave channel '" .. channelName .. "'.") + ), + "{RBX_NAME}",channelName), + fromChannelName + ) + end + else + speaker:SendSystemMessage( + string.gsub( + ChatLocalization:Get( + "GameChat_ChatServiceRunner_YouAreNotInChannel", + ("You are not in channel '" .. channelName .. "'.") + ), + "{RBX_NAME}",channelName), + fromChannelName + ) + end + end +end + +ChatService:RegisterProcessCommandsFunction("default_commands", function(fromSpeaker, message, channel) + if (string.sub(message, 1, 6):lower() == "/join ") then + DoJoinCommand(fromSpeaker, string.sub(message, 7), channel) + return true + elseif (string.sub(message, 1, 3):lower() == "/j ") then + DoJoinCommand(fromSpeaker, string.sub(message, 4), channel) + return true + + elseif (string.sub(message, 1, 7):lower() == "/leave ") then + DoLeaveCommand(fromSpeaker, string.sub(message, 8), channel) + return true + elseif (string.sub(message, 1, 3):lower() == "/l ") then + DoLeaveCommand(fromSpeaker, string.sub(message, 4), channel) + return true + + elseif (string.sub(message, 1, 3) == "/e " or string.sub(message, 1, 7) == "/emote ") then + -- Just don't show these in the chatlog. The animation script listens on these. + return true + + end + + return false +end) + +if ChatSettings.GeneralChannelName and ChatSettings.GeneralChannelName ~= "" then + local allChannel = ChatService:AddChannel(ChatSettings.GeneralChannelName) + + allChannel.Leavable = false + allChannel.AutoJoin = true + + allChannel:RegisterGetWelcomeMessageFunction(function(speaker) + if RunService:IsStudio() then + return nil + end + local player = speaker:GetPlayer() + if player then + local success, canChat = pcall(function() + return Chat:CanUserChatAsync(player.UserId) + end) + if success and not canChat then + return "" + end + end + end) +end + +local systemChannel = ChatService:AddChannel("System") +systemChannel.Leavable = false +systemChannel.AutoJoin = true +systemChannel.WelcomeMessage = ChatLocalization:Get( + "GameChat_ChatServiceRunner_SystemChannelWelcomeMessage", "This channel is for system and game notifications." +) + +systemChannel.SpeakerJoined:connect(function(speakerName) + systemChannel:MuteSpeaker(speakerName) +end) + + +local function TryRunModule(module) + if module:IsA("ModuleScript") then + local ret = require(module) + if (type(ret) == "function") then + ret(ChatService) + end + end +end + +local modules = Chat:WaitForChild("ChatModules") +modules.ChildAdded:connect(function(child) + local success, returnval = pcall(TryRunModule, child) + if not success and returnval then + print("Error running module " ..child.Name.. ": " ..returnval) + end +end) + +for _, module in pairs(modules:GetChildren()) do + local success, returnval = pcall(TryRunModule, module) + if not success and returnval then + print("Error running module " ..module.Name.. ": " ..returnval) + end +end + +PlayersService.PlayerRemoving:connect(function(playerObj) + if (ChatService:GetSpeaker(playerObj.Name)) then + ChatService:RemoveSpeaker(playerObj.Name) + end +end) diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatLocalization.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatLocalization.lua new file mode 100644 index 0000000..7a17a47 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatLocalization.lua @@ -0,0 +1,699 @@ +return { + { + Key = "GameChat_ChatCommandsTeller_AllChannelWelcomeMessage", + Values = { + ["ru"] = "Введите «/?» или «/help», чтобы увидеть список команд чата.", + ["fr"] = "Dans le chat, « /? » or « /help » pour la liste des commandes du chat.", + ["en-us"] = "Chat '/?' or '/help' for a list of chat commands.", + ["de"] = "Gib /? oder /help im Chat ein, um eine Liste der Chatbefehle zu erhalten.", + ["it"] = "Scrivi \"/?\" o \"/help\" per avere l'elenco dei comandi della chat.", + ["pt"] = "Digite '/?' ou '/help' no chat para ver uma lista de comandos.", + ["ja"] = "チャットで '/?' または '/help' を入力するとチャットコマンドの一覧を表示します。", + ["es"] = "Envía \"/?\" o \"/help\" para obtener la lista de comandos del chat.", + ["pt-br"] = "Digite '/?' ou '/help' no chat para ver uma lista de comandos.", + ["ko"] = "채팅창에 '/?' 또는 '/도움말'을 입력하면 채팅 명령어 목록을 볼 수 있어요.", + ["zh-tw"] = "Chat '/?' 或 '/help' 可取得聊天指令清單。", + ["zh-cn"] = "Chat '/?' 或 '/help' 可获取聊天指令清单。", + } + }, + { + Key = "GameChat_SwallowGuestChat_Message", + Values = { + ["ru"] = "Создайте бесплатную учетную запись, чтобы настроить права доступа в чате!", + ["fr"] = "Créez un compte gratuit pour accéder aux permissions de chat !", + ["en-us"] = "Create a free account to get access to chat permissions!", + ["de"] = "Erstelle ein kostenloses Konto, um Zugriff auf Chatberechtigungen zu erhalten!", + ["it"] = "Crea un account gratuito per avere accesso ai permessi della chat!", + ["pt"] = "Crie uma conta grátis para ter acesso a permissões de chat!", + ["ja"] = "フリーアカウントを作ってチャットを始めましょう!", + ["es"] = "¡Crea una cuenta gratuita para obtener los permisos de acceso al chat!", + ["pt-br"] = "Crie uma conta grátis para ter acesso a permissões de chat!", + ["ko"] = "무료 계정을 생성해 채팅 권한을 이용하세요!", + ["zh-tw"] = "請建立免費帳戶,以取得聊天權限!", + ["zh-cn"] = "创建免费帐户以获取聊天权限!", + } + }, + { + Key = "GameChat_ChatCommandsTeller_SwitchChannelCommand", + Values = { + ["ru"] = "/c <канал> : переключить вкладку в меню каналов.", + ["fr"] = "/c  : échanger les onglets du menu Canal.", + ["en-us"] = "/c : switch channel menu tabs.", + ["de"] = "/c : Zum Wechseln zwischen Kanalmenüreitern.", + ["it"] = "/c : cambia scheda nel menu dei canali.", + ["pt"] = "/c : trocar abas de menu de canal.", + ["ja"] = "/c : チャンネルメニュータブを切り替える。", + ["es"] = "/c : alternar pestañas del menú del chat.", + ["pt-br"] = "/c : trocar abas de menu de canal.", + ["ko"] = "/c <채널> : 채널 메뉴 탭 전환.", + ["zh-tw"] = "/c : 切換頻道選單標籤。", + ["zh-cn"] = "/c : 切换频道菜单标签。", + } + }, + { + Key = "GameChat_ChatCommandsTeller_MeCommand", + Values = { + ["ru"] = "/me <текст> : сообщить о выполнении какого-либо действия.", + ["fr"] = "/me  : commande jeu de rôle pour accomplir des actions.", + ["en-us"] = "/me : roleplaying command for doing actions.", + ["de"] = "/me : Rollenspielbefehl für Aktionen.", + ["it"] = "/me : comando per descrivere azioni e giocare di ruolo.", + ["pt"] = "/me : comando de roleplaying para realizar ações.", + ["ja"] = "/me : アクションのためのロールプレイングコマンド。", + ["es"] = "/me : comando de rol para realizar acciones.", + ["pt-br"] = "/me : comando de roleplaying para realizar ações.", + ["ko"] = "/me <텍스트> : 작업 수행을 위한 역할 놀이 명령어.", + ["zh-tw"] = "/me : 做動作的角色扮演指令。", + ["zh-cn"] = "/me : 做动作的角色扮演指令。", + } + }, + { + Key = "GameChat_ChatCommandsTeller_MuteCommand", + Values = { + ["ru"] = "/mute <пользователь> : игнорировать пользователя.", + ["fr"] = "/mute  : bâillonne un interlocuteur.", + ["en-us"] = "/mute : mute a speaker.", + ["de"] = "/mute : Schaltet einen Teilnehmer stumm.", + ["it"] = "/mute : togli la parola a un giocatore.", + ["pt"] = "/mute : silenciar uma pessoa.", + ["ja"] = "/mute :相手をミュート。", + ["es"] = "/mute : silenciar a un usuario.", + ["pt-br"] = "/mute : silenciar uma pessoa.", + ["ko"] = "/mute <스피커> : 스피커 음소거.", + ["zh-tw"] = "/mute : 將使用者靜音。", + ["zh-cn"] = "/mute :将发言者静音。", + } + }, + { + Key = "GameChat_ChatCommandsTeller_UnMuteCommand", + Values = { + ["ru"] = "/unmute <пользователь> : перестать игнорировать пользователя.", + ["fr"] = "/unmute  : retire le bâillon d'un interlocuteur.", + ["en-us"] = "/unmute : unmute a speaker.", + ["de"] = "/unmute : Hebt Stummschaltung eines Teilnehmers auf.", + ["it"] = "/unmute : restituisci la parola a un giocatore.", + ["pt"] = "/unmute : remover silêncio de uma pessoa.", + ["ja"] = "/unmute : 相手のミュートを解除.", + ["es"] = "/unmute : cancelar silencio de un usuario.", + ["pt-br"] = "/unmute : remover silêncio de uma pessoa.", + ["ko"] = "/unmute <스피커> : 스피커 음소거 해제.", + ["zh-tw"] = "/unmute : 取消靜音講者。", + ["zh-cn"] = "/unmute : 取消发言者静音。", + } + }, + { + Key = "GameChat_ChatCommandsTeller_WhisperCommand", + Values = { + ["ru"] = "/whisper <пользователь> или /w <пользователь> : открыть канал для личной переписки с пользователем.", + ["fr"] = "/whisper ou /w  : ouvre un canal de discussion privé avec l'interlocuteur.", + ["en-us"] = "/whisper or /w : open private message channel with speaker.", + ["de"] = "/whisper oder /w : Öffnet privaten Nachrichtenkanal mit Teilnehmer.", + ["it"] = "/whisper o /w : apri un canale privato con un giocatore.", + ["pt"] = "/whisper ou /w : abrir um canal de mensagem privada com uma pessoa.", + ["ja"] = "/whisper または /w : プライベートメッセージチャネルを開く", + ["es"] = "/whisper o /w : abrir canal de mensajes privados con un usuario.", + ["pt-br"] = "/whisper ou /w : abrir um canal de mensagem privada com uma pessoa.", + ["ko"] = "/whisper <스피커> 또는 /w <스피커> : 스피커 채널에서 비공개 메시지 열기.", + ["zh-tw"] = "/whisper 或 /w : 開啟與講者的私訊頻道。", + ["zh-cn"] = "/whisper 或 /w : 打开与发言者的私人消息频道。", + } + }, + { + Key = "GameChat_ChatMain_SpeakerHasBeenMuted", + Values = { + ["ru"] = "Пользователь {RBX_NAME} добавлен в список игнорируемых.", + ["fr"] = "L'interlocuteur {RBX_NAME} a été bâillonné.", + ["en-us"] = "Speaker '{RBX_NAME}' has been muted.", + ["de"] = "Teilnehmer „{RBX_NAME}“ wurde stummgeschaltet.", + ["it"] = "Hai tolto la parola al giocatore \"{RBX_NAME}\".", + ["pt"] = "{RBX_NAME}' foi silenciado(a).", + ["ja"] = "{RBX_NAME}' をミュートしました。", + ["es"] = "Se ha silenciado al usuario \"{RBX_NAME}\".", + ["pt-br"] = "{RBX_NAME}' foi silenciado(a).", + ["ko"] = "스피커 '{RBX_NAME}'이(가) 음소거되었어요.", + ["zh-tw"] = "講者「{RBX_NAME}」已遭靜音。", + ["zh-cn"] = "发言者“{RBX_NAME}”已被静音。", + } + }, + { + Key = "GameChat_ChatMain_SpeakerHasBeenUnMuted", + Values = { + ["ru"] = "Пользователь {RBX_NAME} удален из списка игнорируемых.", + ["fr"] = "L'interlocuteur {RBX_NAME} n'est plus bâillonné.", + ["en-us"] = "Speaker '{RBX_NAME}' has been unmuted.", + ["de"] = "Stummschaltung von Teilnehmer „{RBX_NAME}“ wurde aufgehoben.", + ["it"] = "Hai restituito la parola al giocatore \"{RBX_NAME}\".", + ["pt"] = "O silêncio de '{RBX_NAME}' foi removido.", + ["ja"] = "{RBX_NAME}' のミュートを解除しました。", + ["es"] = "Se ha cancelado el silencio del usuario \"{RBX_NAME}\".", + ["pt-br"] = "O silêncio de '{RBX_NAME}' foi removido.", + ["ko"] = "스피커 '{RBX_NAME}'의 음소거가 해제되었어요.", + ["zh-tw"] = "講者「{RBX_NAME}」已被取消靜音。", + ["zh-cn"] = "发言者“{RBX_NAME}”已被取消静音。", + } + }, + { + Key = "GameChat_ChatCommandsTeller_Desc", + Values = { + ["ru"] = "Это основные команды чата.", + ["fr"] = "Voici les commandes de chat basiques.", + ["en-us"] = "These are the basic chat commands.", + ["de"] = "Das sind die grundlegenden Chatbefehle.", + ["it"] = "Questi sono i comandi base della chat.", + ["pt"] = "Esses são os comandos de chat básicos.", + ["ja"] = "これらは基本的なチャットコマンドです。", + ["es"] = "Estos son los comandos básicos del chat.", + ["pt-br"] = "Esses são os comandos de chat básicos.", + ["ko"] = "기본 채팅 명령어에요.", + ["zh-tw"] = "這些是基本聊天指令。", + ["zh-cn"] = "这些是基本聊天指令。", + } + }, + { + Key = "GameChat_GetVersion_Message", + Values = { + ["ru"] = "Эта игра поддерживает чат версии [{RBX_NUMBER}.{RBX_NUMBER}].", + ["fr"] = "Le jeu utilise la version de chat [{RBX_NUMBER} {RBX_NUMBER}].", + ["en-us"] = "This game is running chat version [{RBX_NUMBER}.{RBX_NUMBER}].", + ["de"] = "Die Chatversion dieses Spiels ist [{RBX_NUMBER}.{RBX_NUMBER}].", + ["it"] = "Questo gioco usa la versione [{RBX_NUMBER}.{RBX_NUMBER}] della chat.", + ["pt"] = "Este jogo está rodando a versão de chat [{RBX_NUMBER}.{RBX_NUMBER}].", + ["ja"] = "このゲームはチャットバージョン [{RBX_NUMBER}.{RBX_NUMBER}]を実行しています。", + ["es"] = "Este juego utiliza la versión del chat [{RBX_NUMBER} {RBX_NUMBER}].", + ["pt-br"] = "Este jogo está rodando a versão de chat [{RBX_NUMBER}.{RBX_NUMBER}].", + ["ko"] = "이 게임은 채팅 버전 [{RBX_NUMBER}.{RBX_NUMBER}]을(를) 실행합니다.", + ["zh-tw"] = "此遊戲正執行聊天版本 [{RBX_NUMBER}.{RBX_NUMBER}]。", + ["zh-cn"] = "此游戏正在运行聊天版本 [{RBX_NUMBER}.{RBX_NUMBER}]。", + } + }, + { + Key = "GameChat_SwitchChannel_NowInChannel", + Values = { + ["ru"] = "Вы общаетесь на канале «{RBX_NAME}»", + ["fr"] = "Maintenant, vous discutez sur le canal : {RBX_NAME}", + ["en-us"] = "You are now chatting in channel: '{RBX_NAME}'", + ["de"] = "Du chattest jetzt in Kanal „{RBX_NAME}“.", + ["it"] = "Ora stai parlando nel canale: \"{RBX_NAME}\"", + ["pt"] = "Você agora está no canal de chat: '{RBX_NAME}'", + ["ja"] = "あなたの現在のチャットチャンネルは: '{RBX_NAME}' です。", + ["es"] = "Estás chateando en el canal \"{RBX_NAME}\".", + ["pt-br"] = "Você agora está no canal de chat: '{RBX_NAME}'", + ["ko"] = "{RBX_NAME}' 채널에서 채팅 중이에요", + ["zh-tw"] = "您此刻聊天的頻道在:「{RBX_NAME}」", + ["zh-cn"] = "你当前的聊天频道为:“{RBX_NAME}”", + } + }, + { + Key = "GameChat_SwitchChannel_NotInChannel", + Values = { + ["ru"] = "Вы не на канале «{RBX_NAME}»", + ["fr"] = "Vous n'êtes pas sur le canal : {RBX_NAME}", + ["en-us"] = "You are not in channel: '{RBX_NAME}'", + ["de"] = "Du befindest dich nicht in Kanal „{RBX_NAME}“.", + ["it"] = "Non ti trovi nel canale: \"{RBX_NAME}\"", + ["pt"] = "Você não está no canal: '{RBX_NAME}'", + ["ja"] = "あなたはチャネル: '{RBX_NAME}' にいません。", + ["es"] = "No estás en el canal \"{RBX_NAME}\".", + ["pt-br"] = "Você não está no canal: '{RBX_NAME}'", + ["ko"] = "{RBX_NAME}' 채널에 있지 않아요", + ["zh-tw"] = "您未在頻道:「{RBX_NAME}」", + ["zh-cn"] = "你不在频道:“{RBX_NAME}”", + } + }, + { + Key = "GameChat_ChatMain_ChatBarText", + Values = { + ["ru"] = "Чтобы общаться в чате, нажмите здесь или на клавишу «/»", + ["fr"] = "Pour discuter, cliquez ici ou appuyez sur la touche !", + ["en-us"] = "To chat click here or press \"/\" key", + ["de"] = "Klicke zum Chatten hier oder drücke die /-Taste.", + ["it"] = "Per chattare, clicca qui o premi il tasto \"/\"", + ["pt"] = "Para escrever clique aqui ou aperte a tecla \"/\"", + ["ja"] = "チャットするにはここをクリックするか \"/\" キーを押します。", + ["es"] = "Para chatear, haz clic aquí o pulsa la tecla \"/\".", + ["pt-br"] = "Para escrever clique aqui ou aperte a tecla \"/\"", + ["ko"] = "채팅하려면 여기를 클릭하거나 \"/\" 키를 누르세요", + ["zh-tw"] = "若要聊天,按一下此處或按 \"/\" 鍵", + ["zh-cn"] = "若要聊天,请点按这里或按住“/”键", + } + }, + { + Key = "GameChat_ChatMain_ChatBarTextTouch", + Values = { + ["ru"] = "Коснитесь здесь, чтобы общаться в чате", + ["fr"] = "Touchez ici pour discuter", + ["en-us"] = "Tap here to chat", + ["de"] = "Tippe zum Chatten hier.", + ["it"] = "Tocca qui per chattare", + ["pt"] = "Toque aqui para escrever", + ["ja"] = "タップしてチャットする", + ["es"] = "Toca aquí para chatear", + ["pt-br"] = "Toque aqui para escrever", + ["ko"] = "채팅하려면 여기를 누르세요", + ["zh-tw"] = "輕觸此處以聊天", + ["zh-cn"] = "轻点此处以聊天", + } + }, + { + Key = "GameChat_ChatMain_SpeakerHasBeenBlocked", + Values = { + ["ru"] = "Пользователь {RBX_NAME} заблокирован.", + ["fr"] = "L'interlocuteur {RBX_NAME} a été bloqué.", + ["en-us"] = "Speaker '{RBX_NAME}' has been blocked.", + ["de"] = "Teilnehmer „{RBX_NAME}“ wurde gesperrt.", + ["it"] = "Hai bloccato il giocatore \"{RBX_NAME}\".", + ["pt"] = "{RBX_NAME}' foi bloqueado.", + ["ja"] = "{RBX_NAME}' はブロック中です。", + ["es"] = "Se ha bloqueado al usuario \"{RBX_NAME}\".", + ["pt-br"] = "{RBX_NAME}' foi bloqueado.", + ["ko"] = "스피커 '{RBX_NAME}' 님을 차단했어요.", + ["zh-tw"] = "講者「{RBX_NAME}」已遭封鎖。", + ["zh-cn"] = "发言者“{RBX_NAME}”已被屏蔽。", + } + }, + { + Key = "GameChat_ChatMain_SpeakerHasBeenUnBlocked", + Values = { + ["ru"] = "Пользователь {RBX_NAME} разблокирован.", + ["fr"] = "L'interlocuteur {RBX_NAME} n'est plus bloqué.", + ["en-us"] = "Speaker '{RBX_NAME}' has been unblocked.", + ["de"] = "Sperrung von Teilnehmer „{RBX_NAME}“ wurde aufgehoben.", + ["it"] = "Hai sbloccato il giocatore \"{RBX_NAME}\".", + ["pt"] = "{RBX_NAME}' foi desbloqueado.", + ["ja"] = "{RBX_NAME}' のブロックが解除されました。", + ["es"] = "Se ha desbloqueado al usuario \"{RBX_NAME}\".", + ["pt-br"] = "{RBX_NAME}' foi desbloqueado.", + ["ko"] = "스피커 '{RBX_NAME}' 님 차단을 해제했어요.", + ["zh-tw"] = "講者「{RBX_NAME}」已被解除封鎖。", + ["zh-cn"] = "发言者“{RBX_NAME}”已被取消屏蔽。", + } + }, + { + Key = "GameChat_ChatCommandsTeller_TeamCommand", + Values = { + ["ru"] = "/team <сообщение> или /t <сообщение> : отправить сообщение игрокам из вашей команды.", + ["fr"] = "/team ou /t  : envoyer un message aux joueurs de votre équipe.", + ["en-us"] = "/team or /t : send a team chat to players on your team.", + ["de"] = "/team oder /t : Sendet eine Teamnachricht an Spieler deines Teams.", + ["it"] = "/team o /t : invia un messaggio a tutti i giocatori della tua squadra.", + ["pt"] = "/team ou /t : enviar um chat de equipe aos jogadores da sua equipe.", + ["ja"] = "/team または /t : 自分のチームメンバーにチームチャットを送る。", + ["es"] = "/team o /t : enviar un mensaje de chat de equipo a los jugadores de tu equipo.", + ["pt-br"] = "/team ou /t : enviar um chat de equipe aos jogadores da sua equipe.", + ["ko"] = "/team <메시지> 또는 /t <메시지> : 팀 내 플레이어에게 팀 채팅 전송.", + ["zh-tw"] = "/team 或 /t : 傳送團隊聊天給隊伍中的玩家。", + ["zh-cn"] = "/team 或 /t : 向你团队的玩家发送团队聊天。", + } + }, + { + Key = "GameChat_ChatFloodDetector_MessageDisplaySeconds", + Values = { + ["ru"] = "Вы сможете отправить новое сообщение только через {RBX_NUMBER} сек.!", + ["fr"] = "Vous devez attendre {RBX_NUMBER} secondes avant d'envoyer un autre message !", + ["en-us"] = "You must wait {RBX_NUMBER} seconds before sending another message!", + ["de"] = "Du musst {RBX_NUMBER} Sekunden lang warten, bevor du eine weitere Nachricht senden kannst!", + ["it"] = "Devi aspettare {RBX_NUMBER} secondi prima di inviare un altro messaggio!", + ["pt"] = "Você precisa esperar {RBX_NUMBER} segundos antes de enviar outra mensagem!", + ["ja"] = "{RBX_NUMBER} 秒待ってから次のメッセージを送ってください!", + ["es"] = "¡Debes esperar {RBX_NUMBER} segundos antes de enviar otro mensaje!", + ["pt-br"] = "Você precisa esperar {RBX_NUMBER} segundos antes de enviar outra mensagem!", + ["ko"] = "추가 메시지를 보내기 전에 {RBX_NUMBER}초 동안 기다려야 해요!", + ["zh-tw"] = "傳送另一則訊息之前您必須等候 {RBX_NUMBER} 秒!", + ["zh-cn"] = "发送另一条消息前你必须等待 {RBX_NUMBER} 秒!", + } + }, + { + Key = "GameChat_ChatFloodDetector_Message", + Values = { + ["ru"] = "Необходимо подождать, прежде чем отправлять новое сообщение!", + ["fr"] = "Vous devez attendre avant d'envoyer un autre message !", + ["en-us"] = "You must wait before sending another message!", + ["de"] = "Du musst warten, bevor du eine weitere Nachricht senden kannst!", + ["it"] = "Devi aspettare prima di inviare un altro messaggio!", + ["pt"] = "Você precisa esperar antes de enviar outra mensagem!", + ["ja"] = "少し待ってから次のメッセージを送ってください!", + ["es"] = "¡Debes esperar antes de enviar otro mensaje!", + ["pt-br"] = "Você precisa esperar antes de enviar outra mensagem!", + ["ko"] = "추가 메시지를 보내기 전에 기다려야 해요!", + ["zh-tw"] = "傳送另一則訊息之前您必須等候!", + ["zh-cn"] = "发送另一条消息前你必须等待!", + } + }, + { + Key = "GameChat_ChatMessageValidator_SettingsError", + Values = { + ["ru"] = "В ваших настройках чата заблокирована возможность отправлять сообщения.", + ["fr"] = "Vos paramètres de chat vous empêchent d'envoyer des messages.", + ["en-us"] = "Your chat settings prevent you from sending messages.", + ["de"] = "Aufgrund deiner Chateinstellungen kannst du keine Nachrichten senden.", + ["it"] = "Non puoi inviare messaggi per le impostazioni della tua chat.", + ["pt"] = "Suas configurações de chat impedem que você envie mensagens.", + ["ja"] = "メッセージが送れないチャット設定です。", + ["es"] = "Tu configuración de chat te impide enviar mensajes.", + ["pt-br"] = "Suas configurações de chat impedem que você envie mensagens.", + ["ko"] = "채팅 설정 때문에 메시지를 보낼 수 없어요.", + ["zh-tw"] = "您的聊天設定禁止您送出訊息。", + ["zh-cn"] = "你的聊天设置禁止你发送消息。", + } + }, + { + Key = "GameChat_ChatMessageValidator_MaxLengthError", + Values = { + ["ru"] = "Превышено максимально допустимое количество символов в сообщении.", + ["fr"] = "Votre message dépasse la longueur maximale.", + ["en-us"] = "Your message exceeds the maximum message length.", + ["de"] = "Deine Nachricht überschreitet die zulässige Nachrichtenlänge.", + ["it"] = "Il tuo messaggio supera la lunghezza massima consentita.", + ["pt"] = "Sua mensagem ultrapassa o tamanho máximo de mensagem.", + ["ja"] = "メッセージが最大文字数を超えています。", + ["es"] = "Tu mensaje supera la longitud máxima permitida.", + ["pt-br"] = "Sua mensagem ultrapassa o tamanho máximo de mensagem.", + ["ko"] = "메시지 길이 한도를 초과했어요.", + ["zh-tw"] = "您的訊息超過最大訊息長度。", + ["zh-cn"] = "你的消息已超过最大长度限制。", + } + }, + { + Key = "GameChat_ChatMessageValidator_WhitespaceError", + Values = { + ["ru"] = "Ваше сообщение содержит недопустимый пробел.", + ["fr"] = "Votre message contient des espaces blancs qui sont interdits.", + ["en-us"] = "Your message contains whitespace that is not allowed.", + ["de"] = "Deine Nachricht enthält unzulässige Leerräume.", + ["it"] = "Il tuo messaggio contiene spazi vuoti non consentiti.", + ["pt"] = "Sua mensagem contém um espaço em branco, que não é permitido.", + ["ja"] = "メッセージに許可されていないスペースが含まれています。", + ["es"] = "Tu mensaje contiene espacios vacíos que no se permiten.", + ["pt-br"] = "Sua mensagem contém um espaço em branco, que não é permitido.", + ["ko"] = "메시지에 허용되지 않는 여백이 있어요.", + ["zh-tw"] = "您的訊息含有不允許的空格。", + ["zh-cn"] = "你的消息包含不被允许的空格。", + } + }, + { + Key = "GameChat_DoMuteCommand_CannotMuteSelf", + Values = { + ["ru"] = "Невозможно добавить себя в список игнорируемых.", + ["fr"] = "Vous ne pouvez pas vous bâillonner.", + ["en-us"] = "You cannot mute yourself.", + ["de"] = "Du kannst dich nicht selbst stummschalten.", + ["it"] = "Non puoi togliere la parola a te stesso.", + ["pt"] = "Você não pode silenciar a si mesmo.", + ["ja"] = "自分をミュートすることは出来ません。", + ["es"] = "No puedes silenciarte a ti mismo.", + ["pt-br"] = "Você não pode silenciar a si mesmo.", + ["ko"] = "자신을 음소거할 수 없어요.", + ["zh-tw"] = "您無法將自己靜音。", + ["zh-cn"] = "你无法将自己静音。", + } + }, + { + Key = "GameChat_MuteSpeaker_SpeakerDoesNotExist", + Values = { + ["ru"] = "Пользователя {RBX_NAME} не существует.", + ["fr"] = "L'interlocuteur {RBX_NAME} n'existe pas.", + ["en-us"] = "Speaker '{RBX_NAME}' does not exist.", + ["de"] = "Teilnehmer „{RBX_NAME}“ existiert nicht.", + ["it"] = "Il giocatore \"{RBX_NAME}\" non esiste.", + ["pt"] = "{RBX_NAME}' não existe.", + ["ja"] = "{RBX_NAME}' は存在しません。", + ["es"] = "El usuario \"{RBX_NAME}\" no existe.", + ["pt-br"] = "{RBX_NAME}' não existe.", + ["ko"] = "스피커 '{RBX_NAME}'이(가) 없어요.", + ["zh-tw"] = "講者「{RBX_NAME}」不存在。", + ["zh-cn"] = "发言者“{RBX_NAME}”不存在。", + } + }, + { + Key = "GameChat_PrivateMessaging_CannotChat", + Values = { + ["ru"] = "Вы не можете общаться в чате с этим игроком.", + ["fr"] = "Vous ne pouvez pas discuter avec ce joueur.", + ["en-us"] = "You are not able to chat with this player.", + ["de"] = "Du kannst mit diesem Spieler nicht chatten.", + ["it"] = "Non puoi chattare con questo giocatore.", + ["pt"] = "Você não pode participar de chat com este jogador.", + ["ja"] = "このプレーヤーとチャットすることは出来ません。", + ["es"] = "No puedes chatear con este jugador.", + ["pt-br"] = "Você não pode participar de chat com este jogador.", + ["ko"] = "이 플레이어와 채팅할 수 없어요.", + ["zh-tw"] = "您無法與此玩家聊天。", + ["zh-cn"] = "你无法与此玩家聊天。", + } + }, + { + Key = "GameChat_PrivateMessaging_CannotWhisperToSelf", + Values = { + ["ru"] = "Нельзя отправлять себе личные сообщения.", + ["fr"] = "Vous ne pouvez pas murmurer à votre propre oreille.", + ["en-us"] = "You cannot whisper to yourself.", + ["de"] = "Du kannst dir nicht selbst etwas zuflüstern.", + ["it"] = "Non puoi aprire un canale privato con te stesso.", + ["pt"] = "Você não pode sussurrar para si mesmo.", + ["ja"] = "自分自身に話しかけることは出来ません。", + ["es"] = "No puedes enviarte mensajes privados a ti mismo.", + ["pt-br"] = "Você não pode sussurrar para si mesmo.", + ["ko"] = "자신에게 귓속말할 수 없어요.", + ["zh-tw"] = "您無法對自己耳語。", + ["zh-cn"] = "你无法向自己秘密发送消息。", + } + }, + { + Key = "GameChat_PrivateMessaging_NowChattingWith", + Values = { + ["ru"] = "Открыт канал для личного общения с пользователем {RBX_NAME}", + ["fr"] = "Maintenant, vous discutez en privé avec {RBX_NAME}", + ["en-us"] = "You are now privately chatting with {RBX_NAME}", + ["de"] = "Du unterhältst dich nun privat mit {RBX_NAME}.", + ["it"] = "Ora stai chattando in privato con {RBX_NAME}", + ["pt"] = "Você agora está em um chat privado com {RBX_NAME}", + ["ja"] = "あなたは現在{RBX_NAME}とプライベートチャット中です。", + ["es"] = "Estás chateando en privado con {RBX_NAME}.", + ["pt-br"] = "Você agora está em um chat privado com {RBX_NAME}", + ["ko"] = "현재 {RBX_NAME} 님과 비공개 채팅 중이에요", + ["zh-tw"] = "您正在與{RBX_NAME}私聊", + ["zh-cn"] = "你正在与“{RBX_NAME}”私聊", + } + }, + { + Key = "GameChat_TeamChat_WelcomeMessage", + Values = { + ["ru"] = "Это канал для личного общения участников вашей команды.", + ["fr"] = "Ceci est un canal privé entre les membres de votre équipe et vous.", + ["en-us"] = "This is a private channel between you and your team members.", + ["de"] = "Dies ist ein privater Kanal für dich und deine Teammitglieder.", + ["it"] = "Questo è un canale privato tra te e i membri della tua squadra.", + ["pt"] = "Este é um canal privado entre você e os membros da sua equipe.", + ["ja"] = "これはあなたとあなたのチームメンバーとのプライベートチャンネルです。", + ["es"] = "Este es un canal privado entre tú y los miembros de tu equipo.", + ["pt-br"] = "Este é um canal privado entre você e os membros da sua equipe.", + ["ko"] = "회원님과 팀원 간의 비공개 채널이에요.", + ["zh-tw"] = "這是您與隊伍成員之間的私人頻道。", + ["zh-cn"] = "这是你与团队成员之间的私人频道。", + } + }, + { + Key = "GameChat_TeamChat_CannotTeamChatIfNotInTeam", + Values = { + ["ru"] = "Вы не можете общаться в командном чате, если не состоите в команде!", + ["fr"] = "Vous ne pouvez pas avoir de discussion d'équipe si vous n'appartenez pas à une équipe !", + ["en-us"] = "You cannot team chat if you are not on a team!", + ["de"] = "Teamchat ist nur verfügbar, wenn du Mitglied eines Teams bist!", + ["it"] = "Non puoi chattare con la squadra se non sei in una squadra!", + ["pt"] = "Você não pode participar de chat de equipe se não estiver em uma!", + ["ja"] = "チームに所属していなければチームチャットは出来ません。", + ["es"] = "¡No puedes chatear con tu equipo si no formas parte de un equipo!", + ["pt-br"] = "Você não pode participar de chat de equipe se não estiver em uma!", + ["ko"] = "팀에 속하지 않으면 팀 채팅을 이용할 수 없어요!", + ["zh-tw"] = "若您不屬於隊伍,無法群聊!", + ["zh-cn"] = "如果你不在该团队,则无法进行团队聊天。", + } + }, + { + Key = "GameChat_TeamChat_NowInTeam", + Values = { + ["ru"] = "Вы вступили в команду {RBX_NAME}.", + ["fr"] = "Vous êtes désormais dans l'équipe {RBX_NAME}.", + ["en-us"] = "You are now on the '{RBX_NAME}' team.", + ["de"] = "Du bist nun Mitglied im Team „{RBX_NAME}“.", + ["it"] = "Ora sei nella squadra \"{RBX_NAME}\".", + ["pt"] = "Você agora está na equipe '{RBX_NAME}'.", + ["ja"] = "あなたは現在 '{RBX_NAME}' チームに所属中です。", + ["es"] = "Ahora formas parte del equipo \"{RBX_NAME}\".", + ["pt-br"] = "Você agora está na equipe '{RBX_NAME}'.", + ["ko"] = "현재 '{RBX_NAME}'팀에 속해 있어요.", + ["zh-tw"] = "您目前在「{RBX_NAME}」隊。", + ["zh-cn"] = "你正在团队“{RBX_NAME}”中。", + } + }, + { + Key = "GameChat_ChatChannel_MutedInChannel", + Values = { + ["ru"] = "Вы добавлены в список игнорируемых и не можете общаться на этом канале", + ["fr"] = "Vous êtes bâillonné et ne pouvez pas parler sur ce canal", + ["en-us"] = "You are muted and cannot talk in this channel", + ["de"] = "Du wurdest stummgeschaltet und kannst in diesem Kanal nicht kommunizieren.", + ["it"] = "Non hai più la parola e non puoi chattare in questo canale", + ["pt"] = "Você está silenciado(a) e não pode falar neste canal", + ["ja"] = "あなたはミュートされこのチャンネルで話すことは出来ません。", + ["es"] = "Se te ha silenciado y no puedes hablar en este canal.", + ["pt-br"] = "Você está silenciado(a) e não pode falar neste canal", + ["ko"] = "이 채널에서 음소거되어 이야기할 수 없어요", + ["zh-tw"] = "您遭靜音,無法在此頻道聊天", + ["zh-cn"] = "你已被静音,无法在此频道聊天", + } + }, + { + Key = "GameChat_ChatService_ChatFilterIssues", + Values = { + ["ru"] = "Могут возникать задержки в передаче сообщений из-за проблем с фильтром чата.", + ["fr"] = "Le filtre de chat connaît actuellement des problèmes et les messages pourraient mettre du temps à apparaître.", + ["en-us"] = "The chat filter is currently experiencing issues and messages may be slow to appear.", + ["de"] = "Es gibt derzeit Probleme mit dem Chatfilter. Nachrichten können deshalb mit Verzögerung angezeigt werden.", + ["it"] = "Il filtro della chat sta riscontrando dei problemi e i messaggi potrebbero apparire in ritardo.", + ["pt"] = "O filtro de chat está com problemas no momento e as mensagens podem demorar para aparecer.", + ["ja"] = "現在チャットフィルターに問題があるためメッセージの表示が遅れています。", + ["es"] = "El filtro del chat sufre problemas en este momento y es posible que los mensajes tarden un poco en aparecer.", + ["pt-br"] = "O filtro de chat está com problemas no momento e as mensagens podem demorar para aparecer.", + ["ko"] = "현재 채팅 필터에 문제가 있어 메시지 표시가 느릴 수 있어요.", + ["zh-tw"] = "此聊天篩選條件目前遇到問題,訊息可能較慢顯示。", + ["zh-cn"] = "聊天过滤器当前遇到问题,消息显示可能出现延迟。", + } + }, + { + Key = "GameChat_ChatService_YouHaveLeftChannel", + Values = { + ["ru"] = "Вы покинули канал «{RBX_NAME}»", + ["fr"] = "Vous avez quitté le canal {RBX_NAME}", + ["en-us"] = "You have left channel '{RBX_NAME}'", + ["de"] = "Du hast Kanal „{RBX_NAME}“ verlassen.", + ["it"] = "Hai lasciato il canale \"{RBX_NAME}\"", + ["pt"] = "Você saiu do canal '{RBX_NAME}'", + ["ja"] = "チャンネル '{RBX_NAME}' を退出しました。", + ["es"] = "Has salido del canal \"{RBX_NAME}\".", + ["pt-br"] = "Você saiu do canal '{RBX_NAME}'", + ["ko"] = "{RBX_NAME}' 채널에서 나왔어요", + ["zh-tw"] = "您已離開頻道「{RBX_NAME}」", + ["zh-cn"] = "你已离开频道“{RBX_NAME}”", + } + }, + { + Key = "GameChat_ChatService_CannotLeaveChannel", + Values = { + ["ru"] = "Вы не можете покинуть этот канал.", + ["fr"] = "Vous ne pouvez pas quitter ce canal.", + ["en-us"] = "You cannot leave this channel.", + ["de"] = "Du kannst diesen Kanal nicht verlassen.", + ["it"] = "Non puoi lasciare questo canale.", + ["pt"] = "Você não pode sair deste canal.", + ["ja"] = "このチャンネルを退出することは出来ません。", + ["es"] = "No puedes salir de este canal.", + ["pt-br"] = "Você não pode sair deste canal.", + ["ko"] = "이 채널에서 나갈 수 없어요.", + ["zh-tw"] = "您無法離開此頻道。", + ["zh-cn"] = "你无法离开此频道。", + } + }, + { + Key = "GameChat_ChatServiceRunner_YouCannotJoinChannel", + Values = { + ["ru"] = "Вы не можете подключиться к каналу «{RBX_NAME}»", + ["fr"] = "Vous ne pouvez pas rejoindre le canal {RBX_NAME}", + ["en-us"] = "You cannot join channel {RBX_NAME}", + ["de"] = "Du kannst Kanal „{RBX_NAME}“ nicht beitreten.", + ["it"] = "Non puoi accedere al canale {RBX_NAME}", + ["pt"] = "Você não pode entrar no canal {RBX_NAME}", + ["ja"] = "チャンネル {RBX_NAME} に参加することは出来ません。", + ["es"] = "No puedes unirte al canal {RBX_NAME}.", + ["pt-br"] = "Você não pode entrar no canal {RBX_NAME}", + ["ko"] = "{RBX_NAME} 채널에 가입할 수 없어요", + ["zh-tw"] = "您無法加入頻道{RBX_NAME}", + ["zh-cn"] = "你无法加入频道“{RBX_NAME}”", + } + }, + { + Key = "GameChat_ChatServiceRunner_ChannelDoesNotExist", + Values = { + ["ru"] = "Канала «{RBX_NAME}» не существует.", + ["fr"] = "Le canal {RBX_NAME} n'existe pas.", + ["en-us"] = "Channel {RBX_NAME} does not exist.", + ["de"] = "Kanal „{RBX_NAME}“ existiert nicht.", + ["it"] = "Il canale {RBX_NAME} non esiste.", + ["pt"] = "O canal {RBX_NAME} não existe.", + ["ja"] = "チャンネル {RBX_NAME} は存在しません。", + ["es"] = "El canal {RBX_NAME} no existe.", + ["pt-br"] = "O canal {RBX_NAME} não existe.", + ["ko"] = "{RBX_NAME} 채널이 없어요.", + ["zh-tw"] = "頻道{RBX_NAME}不存在。", + ["zh-cn"] = "频道“{RBX_NAME}”不存在。", + } + }, + { + Key = "GameChat_ChatServiceRunner_YouCannotLeaveChannel", + Values = { + ["ru"] = "Вы не можете покинуть канал «{RBX_NAME}».", + ["fr"] = "Vous ne pouvez pas quitter le canal {RBX_NAME}", + ["en-us"] = "You cannot leave channel {RBX_NAME}", + ["de"] = "Du kannst Kanal „{RBX_NAME}“ nicht verlassen.", + ["it"] = "Non puoi lasciare il canale {RBX_NAME}", + ["pt"] = "Você não pode sair do canal {RBX_NAME}", + ["ja"] = "チャンネル {RBX_NAME}を退出することは出来ません。", + ["es"] = "No puedes salir del canal {RBX_NAME}.", + ["pt-br"] = "Você não pode sair do canal {RBX_NAME}", + ["ko"] = "{RBX_NAME} 채널에서 나갈 수 없어요", + ["zh-tw"] = "您無法離開頻道{RBX_NAME}", + ["zh-cn"] = "你无法离开频道“{RBX_NAME}”", + } + }, + { + Key = "GameChat_ChatServiceRunner_YouAreNotInChannel", + Values = { + ["ru"] = "Вы не на канале «{RBX_NAME}»", + ["fr"] = "Vous n'êtes pas sur le canal {RBX_NAME}", + ["en-us"] = "You are not in channel {RBX_NAME}", + ["de"] = "Du befindest dich nicht in Kanal „{RBX_NAME}“.", + ["it"] = "Non ti trovi nel canale {RBX_NAME}", + ["pt"] = "Você não está no canal {RBX_NAME}", + ["ja"] = "あなたはチャンネル {RBX_NAME} にいません。", + ["es"] = "No estás en el canal {RBX_NAME}.", + ["pt-br"] = "Você não está no canal {RBX_NAME}", + ["ko"] = "{RBX_NAME} 채널에 있지 않아요", + ["zh-tw"] = "您未在頻道{RBX_NAME}", + ["zh-cn"] = "你不在频道“{RBX_NAME}”", + } + }, + { + Key = "GameChat_ChatServiceRunner_SystemChannelWelcomeMessage", + Values = { + ["ru"] = "Этот канал предназначен для системных и игровых уведомлений.", + ["fr"] = "Ce canal est réservé aux notifications système et de jeu.", + ["en-us"] = "This channel is for system and game notifications.", + ["de"] = "Dieser Kanal ist für System- und Spielbenachrichtigungen.", + ["it"] = "Questo canale è per le notifiche di gioco e del sistema.", + ["pt"] = "Este canal é destinado a notificações do sistema e jogo.", + ["ja"] = "このチャンネルはシステムとゲーム通知のためのものです。", + ["es"] = "Este canal es para notificaciones del sistema y del juego.", + ["pt-br"] = "Este canal é destinado a notificações do sistema e jogo.", + ["ko"] = "이 채널은 시스템 및 게임 알림용이에요.", + ["zh-tw"] = "此頻道是供系統及遊戲通知用。", + ["zh-cn"] = "此频道用于发送系统及游戏通知。", + } + }, + { + Key = "GameChat_FriendChatNotifier_JoinMessage", + Values = { + ["ru"] = "Ваш друг {RBX_NAME} присоединился к игре.", + ["fr"] = "Votre ami {RBX_NAME} a rejoint le jeu.", + ["en-us"] = "Your friend {RBX_NAME} has joined the game.", + ["de"] = "Dein Freund {RBX_NAME} ist dem Spiel beigetreten.", + ["it"] = "Il tuo amico {RBX_NAME} è entrato nel gioco.", + ["pt"] = "Seu amigo {RBX_NAME} juntou-se ao jogo.", + ["ja"] = "あなたの友人{RBX_NAME}がゲームに参加しました。", + ["es"] = "Tu amigo {RBX_NAME} se ha unido al juego.", + ["pt-br"] = "Seu amigo {RBX_NAME} juntou-se ao jogo.", + ["ko"] = "친구 {RBX_NAME} 님이 게임에 가입했어요.", + ["zh-tw"] = "您的朋友{RBX_NAME}已加入遊戲。", + ["zh-cn"] = "你的朋友“{RBX_NAME}”已加入游戏。", + } + } +} diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/ChatCommandsTeller.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/ChatCommandsTeller.lua new file mode 100644 index 0000000..4071b06 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/ChatCommandsTeller.lua @@ -0,0 +1,58 @@ +-- // FileName: ChatCommandsTeller.lua +-- // Written by: Xsitsu +-- // Description: Module that provides information on default chat commands to players. + +local Chat = game:GetService("Chat") +local ReplicatedModules = Chat:WaitForChild("ClientChatModules") +local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings")) +local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants")) + +local ChatLocalization = nil +pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end) +if ChatLocalization == nil then ChatLocalization = { Get = function(key,default) return default end } end + +local function Run(ChatService) + + local function ShowJoinAndLeaveCommands() + if ChatSettings.ShowJoinAndLeaveHelpText ~= nil then + return ChatSettings.ShowJoinAndLeaveHelpText + end + return false + end + + local function ProcessCommandsFunction(fromSpeaker, message, channel) + if (message:lower() == "/?" or message:lower() == "/help") then + local speaker = ChatService:GetSpeaker(fromSpeaker) + speaker:SendSystemMessage(ChatLocalization:Get("GameChat_ChatCommandsTeller_Desc","These are the basic chat commands."), channel) + speaker:SendSystemMessage(ChatLocalization:Get("GameChat_ChatCommandsTeller_MeCommand","/me : roleplaying command for doing actions."), channel) + speaker:SendSystemMessage(ChatLocalization:Get("GameChat_ChatCommandsTeller_SwitchChannelCommand","/c : switch channel menu tabs."), channel) + if ShowJoinAndLeaveCommands() then + speaker:SendSystemMessage(ChatLocalization:Get("GameChat_ChatCommandsTeller_JoinChannelCommand","/join or /j : join channel."), channel) + speaker:SendSystemMessage(ChatLocalization:Get("GameChat_ChatCommandsTeller_LeaveChannelCommand","/leave or /l : leave channel. (leaves current if none specified)"), channel) + end + speaker:SendSystemMessage(ChatLocalization:Get("GameChat_ChatCommandsTeller_WhisperCommand","/whisper or /w : open private message channel with speaker."), channel) + speaker:SendSystemMessage(ChatLocalization:Get("GameChat_ChatCommandsTeller_MuteCommand","/mute : mute a speaker."), channel) + speaker:SendSystemMessage(ChatLocalization:Get("GameChat_ChatCommandsTeller_UnMuteCommand","/unmute : unmute a speaker."), channel) + + local player = speaker:GetPlayer() + if player and player.Team then + speaker:SendSystemMessage(ChatLocalization:Get("GameChat_ChatCommandsTeller_TeamCommand","/team or /t : send a team chat to players on your team."), channel) + end + + return true + end + + return false + end + + ChatService:RegisterProcessCommandsFunction("chat_commands_inquiry", ProcessCommandsFunction, ChatConstants.StandardPriority) + + if ChatSettings.GeneralChannelName then + local allChannel = ChatService:GetChannel(ChatSettings.GeneralChannelName) + if (allChannel) then + allChannel.WelcomeMessage = ChatLocalization:Get("GameChat_ChatCommandsTeller_AllChannelWelcomeMessage","Chat '/?' or '/help' for a list of chat commands.") + end + end +end + +return Run diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/ChatFloodDetector.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/ChatFloodDetector.lua new file mode 100644 index 0000000..5769fde --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/ChatFloodDetector.lua @@ -0,0 +1,92 @@ +-- // FileName: ChatFloodDetector.lua +-- // Written by: Xsitsu +-- // Description: Module that limits the number of messages a speaker can send in a given period of time. + +local Chat = game:GetService("Chat") +local ReplicatedModules = Chat:WaitForChild("ClientChatModules") +local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants")) + +local doFloodCheckByChannel = true +local informSpeakersOfWaitTimes = true +local chatBotsBypassFloodCheck = true +local numberMessagesAllowed = 7 +local decayTimePeriod = 15 + +local floodCheckTable = {} +local whitelistedSpeakers = {} + +local ChatLocalization = nil +pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end) +if ChatLocalization == nil then ChatLocalization = {} function ChatLocalization:Get(key,default) return default end end + +local function EnterTimeIntoLog(tbl) + table.insert(tbl, tick() + decayTimePeriod) +end + +local function Run(ChatService) + local function FloodDetectionProcessCommandsFunction(speakerName, message, channel) + if (whitelistedSpeakers[speakerName]) then return false end + + local speakerObj = ChatService:GetSpeaker(speakerName) + if (not speakerObj) then return false end + if (chatBotsBypassFloodCheck and not speakerObj:GetPlayer()) then return false end + + if (not floodCheckTable[speakerName]) then + floodCheckTable[speakerName] = {} + end + + local t = nil + + if (doFloodCheckByChannel) then + if (not floodCheckTable[speakerName][channel]) then + floodCheckTable[speakerName][channel] = {} + end + + t = floodCheckTable[speakerName][channel] + else + t = floodCheckTable[speakerName] + end + + local now = tick() + while (#t > 0 and t[1] < now) do + table.remove(t, 1) + end + + if (#t < numberMessagesAllowed) then + EnterTimeIntoLog(t) + return false + else + + local timeDiff = math.ceil(t[1] - now) + + if (informSpeakersOfWaitTimes) then + local msg = string.gsub( + ChatLocalization:Get( + "GameChat_ChatFloodDetector_MessageDisplaySeconds", + string.format("You must wait %d %s before sending another message!", timeDiff, (timeDiff > 1) and "seconds" or "second") + ), + "{RBX_NUMBER}", + tostring(timeDiff) + ) + speakerObj:SendSystemMessage(msg, channel) + else + speakerObj:SendSystemMessage( + ChatLocalization:Get( + "GameChat_ChatFloodDetector_Message", + "You must wait before sending another message!" + ) + ,channel) + end + + return true + end + end + + ChatService:RegisterProcessCommandsFunction("flood_detection", FloodDetectionProcessCommandsFunction, ChatConstants.LowPriority) + + ChatService.SpeakerRemoved:connect(function(speakerName) + floodCheckTable[speakerName] = nil + end) +end + +return Run diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/ChatMessageValidator.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/ChatMessageValidator.lua new file mode 100644 index 0000000..40a2d00 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/ChatMessageValidator.lua @@ -0,0 +1,65 @@ +-- // FileName: ChatMessageValidator.lua +-- // Written by: TheGamer101 +-- // Description: Validate things such as no disallowed whitespace and chat message length on the server. + +local Chat = game:GetService("Chat") +local RunService = game:GetService("RunService") +local ReplicatedModules = Chat:WaitForChild("ClientChatModules") +local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings")) +local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants")) + +local ChatLocalization = nil +pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end) +if ChatLocalization == nil then ChatLocalization = {} function ChatLocalization:Get(key,default) return default end end + +local DISALLOWED_WHITESPACE = {"\n", "\r", "\t", "\v", "\f"} + +if ChatSettings.DisallowedWhiteSpace then + DISALLOWED_WHITESPACE = ChatSettings.DisallowedWhiteSpace +end + +local function Run(ChatService) + + local function CanUserChat(playerObj) + if RunService:IsStudio() then + return true + end + local success, canChat = pcall(function() + return Chat:CanUserChatAsync(playerObj.UserId) + end) + return success and canChat + end + + local function ValidateChatFunction(speakerName, message, channel) + local speakerObj = ChatService:GetSpeaker(speakerName) + local playerObj = speakerObj:GetPlayer() + if not speakerObj then return false end + if not playerObj then return false end + + if not RunService:IsStudio() and playerObj.UserId < 1 then + return true + end + + if not CanUserChat(playerObj) then + speakerObj:SendSystemMessage(ChatLocalization:Get("GameChat_ChatMessageValidator_SettingsError","Your chat settings prevent you from sending messages."), channel) + return true + end + + if message:len() > ChatSettings.MaximumMessageLength + 1 then + speakerObj:SendSystemMessage(ChatLocalization:Get("GameChat_ChatMessageValidator_MaxLengthError","Your message exceeds the maximum message length."), channel) + return true + end + + for i = 1, #DISALLOWED_WHITESPACE do + if string.find(message, DISALLOWED_WHITESPACE[i]) then + speakerObj:SendSystemMessage(ChatLocalization:Get("GameChat_ChatMessageValidator_WhitespaceError","Your message contains whitespace that is not allowed."), channel) + return true + end + end + return false + end + + ChatService:RegisterProcessCommandsFunction("message_validation", ValidateChatFunction, ChatSettings.LowPriority) +end + +return Run diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/ExtraDataInitializer.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/ExtraDataInitializer.lua new file mode 100644 index 0000000..b08f987 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/ExtraDataInitializer.lua @@ -0,0 +1,187 @@ +-- // FileName: ExtraDataInitializer.lua +-- // Written by: Xsitsu +-- // Description: Module that sets some basic ExtraData such as name color, and chat color. + +local SpecialChatColors = { + Groups = { + { + --- ROBLOX Interns group + GroupId = 2868472, + Rank = 100, + ChatColor = Color3.new(175/255, 221/255, 1), + }, + { + --- ROBLOX Admins group + GroupId = 1200769, + ChatColor = Color3.new(1, 215/255, 0), + }, + }, + Players = { + { + --- Left as an example + -- UserId = 2231221, + -- ChatColor = Color3.new(205/255, 0, 0) + } + } +} + +local function MakeIsInGroup(groupId, requiredRank) + assert(type(requiredRank) == "nil" or type(requiredRank) == "number", "requiredRank must be a number or nil") + + return function(player) + if player and player.UserId then + local userId = player.UserId + + local inGroup = false + local success, err = pcall(function() -- Many things can error is the IsInGroup check + if requiredRank then + inGroup = player:GetRankInGroup(groupId) > requiredRank + else + inGroup = player:IsInGroup(groupId) + end + end) + if not success and err then + print("Error checking in group: " ..err) + end + + return inGroup + end + + return false + end +end + +local function ConstructIsInGroups() + if SpecialChatColors.Groups then + for _, group in pairs(SpecialChatColors.Groups) do + group.IsInGroup = MakeIsInGroup(group.GroupId, group.Rank) + end + end +end +ConstructIsInGroups() + +local Players = game:GetService("Players") + +local function GetSpecialChatColor(speakerName) + if SpecialChatColors.Players then + local playerFromSpeaker = Players:FindFirstChild(speakerName) + if playerFromSpeaker then + for _, player in pairs(SpecialChatColors.Players) do + if playerFromSpeaker.UserId == player.UserId then + return player.ChatColor + end + end + end + end + if SpecialChatColors.Groups then + for _, group in pairs(SpecialChatColors.Groups) do + if group.IsInGroup(Players:FindFirstChild(speakerName)) then + return group.ChatColor + end + end + end +end + +local function Run(ChatService) + local NAME_COLORS = + { + Color3.new(253/255, 41/255, 67/255), -- BrickColor.new("Bright red").Color, + Color3.new(1/255, 162/255, 255/255), -- BrickColor.new("Bright blue").Color, + Color3.new(2/255, 184/255, 87/255), -- BrickColor.new("Earth green").Color, + BrickColor.new("Bright violet").Color, + BrickColor.new("Bright orange").Color, + BrickColor.new("Bright yellow").Color, + BrickColor.new("Light reddish violet").Color, + BrickColor.new("Brick yellow").Color, + } + + local function GetNameValue(pName) + local value = 0 + for index = 1, #pName do + local cValue = string.byte(string.sub(pName, index, index)) + local reverseIndex = #pName - index + 1 + if #pName%2 == 1 then + reverseIndex = reverseIndex - 1 + end + if reverseIndex%4 >= 2 then + cValue = -cValue + end + value = value + cValue + end + return value + end + + local color_offset = 0 + local function ComputeNameColor(pName) + return NAME_COLORS[((GetNameValue(pName) + color_offset) % #NAME_COLORS) + 1] + end + + local function GetNameColor(speaker) + local player = speaker:GetPlayer() + if player then + if player.Team ~= nil then + return player.TeamColor.Color + end + end + return ComputeNameColor(speaker.Name) + end + + local function onNewSpeaker(speakerName) + local speaker = ChatService:GetSpeaker(speakerName) + if not speaker:GetExtraData("NameColor") then + speaker:SetExtraData("NameColor", GetNameColor(speaker)) + end + if not speaker:GetExtraData("ChatColor") then + local specialChatColor = GetSpecialChatColor(speakerName) + if specialChatColor then + speaker:SetExtraData("ChatColor", specialChatColor) + end + end + if not speaker:GetExtraData("Tags") then + --// Example of how you would set tags + --[[ + local tags = { + { + TagText = "VIP", + TagColor = Color3.new(1, 215/255, 0) + }, + { + TagText = "Alpha Tester", + TagColor = Color3.new(205/255, 0, 0) + } + } + speaker:SetExtraData("Tags", tags) + ]] + speaker:SetExtraData("Tags", {}) + end + end + + ChatService.SpeakerAdded:connect(onNewSpeaker) + + for _, speakerName in pairs(ChatService:GetSpeakerList()) do + onNewSpeaker(speakerName) + end + + local PlayerChangedConnections = {} + Players.PlayerAdded:connect(function(player) + local changedConn = player.Changed:connect(function(property) + local speaker = ChatService:GetSpeaker(player.Name) + if speaker then + if property == "TeamColor" or property == "Neutral" or property == "Team" then + speaker:SetExtraData("NameColor", GetNameColor(speaker)) + end + end + end) + PlayerChangedConnections[player] = changedConn + end) + + Players.PlayerRemoving:connect(function(player) + local changedConn = PlayerChangedConnections[player] + if changedConn then + changedConn:Disconnect() + end + PlayerChangedConnections[player] = nil + end) +end + +return Run diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/FriendJoinNotifier.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/FriendJoinNotifier.lua new file mode 100644 index 0000000..b5a941b --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/FriendJoinNotifier.lua @@ -0,0 +1,67 @@ +-- // FileName: FriendJoinNotifer.lua +-- // Written by: TheGamer101 +-- // Description: Module that adds a message to the chat whenever a friend joins the game. + +local Chat = game:GetService("Chat") +local Players = game:GetService("Players") +local FriendService = game:GetService("FriendService") + +local ReplicatedModules = Chat:WaitForChild("ClientChatModules") +local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings")) +local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants")) + +local ChatLocalization = nil +pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end) +if ChatLocalization == nil then ChatLocalization = {} function ChatLocalization:Get(key,default) return default end end + +local FriendMessageTextColor = Color3.fromRGB(255, 255, 255) +local FriendMessageExtraData = {ChatColor = FriendMessageTextColor} + +local function Run(ChatService) + + local function ShowFriendJoinNotification() + if ChatSettings.ShowFriendJoinNotification ~= nil then + return ChatSettings.ShowFriendJoinNotification + end + return false + end + + local function SendFriendJoinNotification(player, joinedFriend) + local speakerObj = ChatService:GetSpeaker(player.Name) + if speakerObj then + speakerObj:SendSystemMessage( + string.gsub( + ChatLocalization:Get( + "GameChat_FriendChatNotifier_JoinMessage", + string.format("Your friend %s has joined the game.", joinedFriend.Name) + ), + "{RBX_NAME}", + joinedFriend.Name + ), + "System", + FriendMessageExtraData + ) + end + end + + local function TrySendFriendNotification(player, joinedPlayer) + if player ~= joinedPlayer then + coroutine.wrap(function() + if player:IsFriendsWith(joinedPlayer.UserId) then + SendFriendJoinNotification(player, joinedPlayer) + end + end)() + end + end + + if ShowFriendJoinNotification() then + Players.PlayerAdded:connect(function(player) + local possibleFriends = Players:GetPlayers() + for i = 1, #possibleFriends do + TrySendFriendNotification(possibleFriends[i], player) + end + end) + end +end + +return Run diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/MeCommand.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/MeCommand.lua new file mode 100644 index 0000000..7bb1f55 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/MeCommand.lua @@ -0,0 +1,22 @@ +-- // FileName: MeCommand.lua +-- // Written by: TheGamer101 +-- // Description: Sets the type of /me messages. + +local Chat = game:GetService("Chat") +local ReplicatedModules = Chat:WaitForChild("ClientChatModules") +local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants")) + +local function Run(ChatService) + + local function MeCommandFilterFunction(speakerName, messageObj, channelName) + local message = messageObj.Message + if message and string.sub(message, 1, 4):lower() == "/me " then + -- Set a different message type so that clients can render the message differently. + messageObj.MessageType = ChatConstants.MessageTypeMeCommand + end + end + + ChatService:RegisterFilterMessageFunction("me_command", MeCommandFilterFunction) +end + +return Run diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/MuteSpeaker.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/MuteSpeaker.lua new file mode 100644 index 0000000..7d59da9 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/MuteSpeaker.lua @@ -0,0 +1,127 @@ +-- // FileName: MuteSpeaker.lua +-- // Written by: TheGamer101 +-- // Description: Module that handles all the mute and unmute commands. + +local Chat = game:GetService("Chat") +local ReplicatedModules = Chat:WaitForChild("ClientChatModules") +local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants")) +local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings")) + +local ChatLocalization = nil +pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end) +if ChatLocalization == nil then ChatLocalization = {} function ChatLocalization:Get(key,default) return default end end + +local errorTextColor = ChatSettings.ErrorMessageTextColor or Color3.fromRGB(245, 50, 50) +local errorExtraData = {ChatColor = errorTextColor} + +local function Run(ChatService) + + local function GetSpeakerNameFromMessage(message) + local speakerName = message + if string.sub(message, 1, 1) == "\"" then + local pos = string.find(message, "\"", 2) + if pos then + speakerName = string.sub(message, 2, pos - 1) + end + else + local first = string.match(message, "^[^%s]+") + if first then + speakerName = first + end + end + return speakerName + end + + local function DoMuteCommand(speakerName, message, channel) + local muteSpeakerName = GetSpeakerNameFromMessage(message) + local speaker = ChatService:GetSpeaker(speakerName) + if speaker then + if muteSpeakerName:lower() == speakerName:lower() then + speaker:SendSystemMessage(ChatLocalization:Get("GameChat_DoMuteCommand_CannotMuteSelf","You cannot mute yourself."), channel, errorExtraData) + return + end + + local muteSpeaker = ChatService:GetSpeaker(muteSpeakerName) + if muteSpeaker then + speaker:AddMutedSpeaker(muteSpeaker.Name) + speaker:SendSystemMessage( + string.gsub( + ChatLocalization:Get( + "GameChat_ChatMain_SpeakerHasBeenMuted", + string.format("Speaker '%s' has been muted.", muteSpeaker.Name) + ), + "{RBX_NAME}",muteSpeaker.Name + ), + channel + ) + else + speaker:SendSystemMessage( + string.gsub( + ChatLocalization:Get( + "GameChat_MuteSpeaker_SpeakerDoesNotExist", + string.format("Speaker '%s' does not exist.", tostring(muteSpeakerName)) + ), + "{RBX_NAME}",tostring(muteSpeakerName) + ), + channel, + errorExtraData + ) + end + end + end + + local function DoUnmuteCommand(speakerName, message, channel) + local unmuteSpeakerName = GetSpeakerNameFromMessage(message) + local speaker = ChatService:GetSpeaker(speakerName) + if speaker then + if unmuteSpeakerName:lower() == speakerName:lower() then + speaker:SendSystemMessage(ChatLocalization:Get("GameChat_DoMuteCommand_CannotMuteSelf","You cannot mute yourself."), channel, errorExtraData) + return + end + + local unmuteSpeaker = ChatService:GetSpeaker(unmuteSpeakerName) + if unmuteSpeaker then + speaker:RemoveMutedSpeaker(unmuteSpeaker.Name) + speaker:SendSystemMessage( + string.gsub( + ChatLocalization:Get( + "GameChat_ChatMain_SpeakerHasBeenUnMuted", + string.format("Speaker '%s' has been unmuted.", unmuteSpeaker.Name) + ), + "{RBX_NAME}",unmuteSpeaker.Name + ), + channel + ) + else + speaker:SendSystemMessage( + string.gsub( + ChatLocalization:Get( + "GameChat_MuteSpeaker_SpeakerDoesNotExist", + string.format("Speaker '%s' does not exist.", tostring(unmuteSpeakerName)) + ), + "{RBX_NAME}",tostring(unmuteSpeakerName) + ), + channel, + errorExtraData + ) + end + end + end + + local function MuteCommandsFunction(fromSpeaker, message, channel) + local processedCommand = false + + if string.sub(message, 1, 6):lower() == "/mute " then + DoMuteCommand(fromSpeaker, string.sub(message, 7), channel) + processedCommand = true + elseif string.sub(message, 1, 8):lower() == "/unmute " then + DoUnmuteCommand(fromSpeaker, string.sub(message, 9), channel) + processedCommand = true + end + return processedCommand + end + + ChatService:RegisterProcessCommandsFunction("mute_commands", MuteCommandsFunction, ChatConstants.StandardPriority) +end + +return Run diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/PrivateMessaging.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/PrivateMessaging.lua new file mode 100644 index 0000000..fdbe516 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/PrivateMessaging.lua @@ -0,0 +1,177 @@ +-- // FileName: PrivateMessaging.lua +-- // Written by: Xsitsu +-- // Description: Module that handles all private messaging. + +local Chat = game:GetService("Chat") +local RunService = game:GetService("RunService") +local ReplicatedModules = Chat:WaitForChild("ClientChatModules") +local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants")) +local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings")) + +local ChatLocalization = nil +pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end) +if ChatLocalization == nil then ChatLocalization = {} function ChatLocalization:Get(key,default) return default end end + +local errorTextColor = ChatSettings.ErrorMessageTextColor or Color3.fromRGB(245, 50, 50) +local errorExtraData = {ChatColor = errorTextColor} + +function GetWhisperChannelPrefix() + if ChatConstants.WhisperChannelPrefix then + return ChatConstants.WhisperChannelPrefix + end + return "To " +end + +local function Run(ChatService) + + local function CanCommunicate(fromSpeaker, toSpeaker) + if RunService:IsStudio() then + return true + end + local fromPlayer = fromSpeaker:GetPlayer() + local toPlayer = toSpeaker:GetPlayer() + if fromPlayer and toPlayer then + local success, canChat = pcall(function() + return Chat:CanUsersChatAsync(fromPlayer.UserId, toPlayer.UserId) + end) + return success and canChat + end + return false + end + + local function DoWhisperCommand(fromSpeaker, message, channel) + local otherSpeakerName = message + local sendMessage = nil + + if (string.sub(message, 1, 1) == "\"") then + local pos = string.find(message, "\"", 2) + if (pos) then + otherSpeakerName = string.sub(message, 2, pos - 1) + sendMessage = string.sub(message, pos + 2) + end + else + local first = string.match(message, "^[^%s]+") + if (first) then + otherSpeakerName = first + sendMessage = string.sub(message, string.len(otherSpeakerName) + 2) + end + end + + local speaker = ChatService:GetSpeaker(fromSpeaker) + local otherSpeaker = ChatService:GetSpeaker(otherSpeakerName) + local channelObj = ChatService:GetChannel(GetWhisperChannelPrefix() .. otherSpeakerName) + if channelObj and otherSpeaker then + if not CanCommunicate(speaker, otherSpeaker) then + speaker:SendSystemMessage(ChatLocalization:Get("GameChat_PrivateMessaging_CannotChat","You are not able to chat with this player."), channel, errorExtraData) + return + end + + if (channelObj.Name == GetWhisperChannelPrefix() .. speaker.Name) then + speaker:SendSystemMessage(ChatLocalization:Get("GameChat_PrivateMessaging_CannotWhisperToSelf","You cannot whisper to yourself."), channel, errorExtraData) + else + if (not speaker:IsInChannel(channelObj.Name)) then + speaker:JoinChannel(channelObj.Name) + end + + if (sendMessage and (string.len(sendMessage) > 0) ) then + speaker:SayMessage(sendMessage, channelObj.Name) + end + + speaker:SetMainChannel(channelObj.Name) + + end + + else + speaker:SendSystemMessage( + string.gsub( + ChatLocalization:Get( + "GameChat_MuteSpeaker_SpeakerDoesNotExist", + string.format("Speaker '%s' does not exist.", tostring(otherSpeakerName)) + ), + "{RBX_NAME}",tostring(otherSpeakerName) + ), + channel, + errorExtraData + ) + end + end + + local function WhisperCommandsFunction(fromSpeaker, message, channel) + local processedCommand = false + + if (string.sub(message, 1, 3):lower() == "/w ") then + DoWhisperCommand(fromSpeaker, string.sub(message, 4), channel) + processedCommand = true + + elseif (string.sub(message, 1, 9):lower() == "/whisper ") then + DoWhisperCommand(fromSpeaker, string.sub(message, 10), channel) + processedCommand = true + + end + + return processedCommand + end + + local function PrivateMessageReplicationFunction(fromSpeaker, message, channelName) + local sendingSpeaker = ChatService:GetSpeaker(fromSpeaker) + local extraData = sendingSpeaker.ExtraData + sendingSpeaker:SendMessage(message, channelName, fromSpeaker, extraData) + + local toSpeaker = ChatService:GetSpeaker(string.sub(channelName, 4)) + if (toSpeaker) then + if (not toSpeaker:IsInChannel(GetWhisperChannelPrefix() .. fromSpeaker)) then + toSpeaker:JoinChannel(GetWhisperChannelPrefix() .. fromSpeaker) + end + toSpeaker:SendMessage(message, GetWhisperChannelPrefix() .. fromSpeaker, fromSpeaker, extraData) + end + + return true + end + + local function PrivateMessageAddTypeFunction(speakerName, messageObj, channelName) + if ChatConstants.MessageTypeWhisper then + messageObj.MessageType = ChatConstants.MessageTypeWhisper + end + end + + ChatService:RegisterProcessCommandsFunction("whisper_commands", WhisperCommandsFunction, ChatConstants.StandardPriority) + + local function GetWhisperChanneNameColor() + if ChatSettings.WhisperChannelNameColor then + return ChatSettings.WhisperChannelNameColor + end + return Color3.fromRGB(102, 14, 102) + end + + ChatService.SpeakerAdded:connect(function(speakerName) + if (ChatService:GetChannel(GetWhisperChannelPrefix() .. speakerName)) then + ChatService:RemoveChannel(GetWhisperChannelPrefix() .. speakerName) + end + + local channel = ChatService:AddChannel(GetWhisperChannelPrefix() .. speakerName) + channel.Joinable = false + channel.Leavable = true + channel.AutoJoin = false + channel.Private = true + + channel.WelcomeMessage = string.gsub( + ChatLocalization:Get( + "GameChat_PrivateMessaging_NowChattingWith", + "You are now privately chatting with " .. speakerName .. "." + ), + "{RBX_NAME}",tostring(speakerName) + ) + channel.ChannelNameColor = GetWhisperChanneNameColor() + + channel:RegisterProcessCommandsFunction("replication_function", PrivateMessageReplicationFunction, ChatConstants.LowPriority) + channel:RegisterFilterMessageFunction("message_type_function", PrivateMessageAddTypeFunction) + end) + + ChatService.SpeakerRemoved:connect(function(speakerName) + if (ChatService:GetChannel(GetWhisperChannelPrefix() .. speakerName)) then + ChatService:RemoveChannel(GetWhisperChannelPrefix() .. speakerName) + end + end) +end + +return Run diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/TeamChat.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/TeamChat.lua new file mode 100644 index 0000000..e6917ca --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/DefaultChatModules/TeamChat.lua @@ -0,0 +1,190 @@ +-- // FileName: TeamChat.lua +-- // Written by: Xsitsu +-- // Description: Module that handles all team chat. + +local Chat = game:GetService("Chat") +local ReplicatedModules = Chat:WaitForChild("ClientChatModules") +local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings")) +local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants")) + +local ChatLocalization = nil +pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end) +if ChatLocalization == nil then ChatLocalization = {} function ChatLocalization:Get(key,default) return default end end + +local errorTextColor = ChatSettings.ErrorMessageTextColor or Color3.fromRGB(245, 50, 50) +local errorExtraData = {ChatColor = errorTextColor} + +local function Run(ChatService) + + local Players = game:GetService("Players") + + local channel = ChatService:AddChannel("Team") + channel.WelcomeMessage = ChatLocalization:Get("GameChat_TeamChat_WelcomeMessage","This is a private channel between you and your team members.") + channel.Joinable = false + channel.Leavable = false + channel.AutoJoin = false + channel.Private = true + + local function TeamChatReplicationFunction(fromSpeaker, message, channelName) + local speakerObj = ChatService:GetSpeaker(fromSpeaker) + local channelObj = ChatService:GetChannel(channelName) + if (speakerObj and channelObj) then + local player = speakerObj:GetPlayer() + if (player) then + + for i, speakerName in pairs(channelObj:GetSpeakerList()) do + local otherSpeaker = ChatService:GetSpeaker(speakerName) + if (otherSpeaker) then + local otherPlayer = otherSpeaker:GetPlayer() + if (otherPlayer) then + + if (player.Team == otherPlayer.Team) then + local extraData = { + NameColor = player.TeamColor.Color, + ChatColor = player.TeamColor.Color, + ChannelColor = player.TeamColor.Color + } + otherSpeaker:SendMessage(message, channelName, fromSpeaker, extraData) + else + --// Could use this line to obfuscate message for cool effects + --otherSpeaker:SendMessage(message, channelName, fromSpeaker) + end + + end + end + end + + end + end + + return true + end + + channel:RegisterProcessCommandsFunction("replication_function", TeamChatReplicationFunction, ChatConstants.LowPriority) + + local function DoTeamCommand(fromSpeaker, message, channel) + if message == nil then + message = "" + end + + local speaker = ChatService:GetSpeaker(fromSpeaker) + if speaker then + local player = speaker:GetPlayer() + + if player then + if player.Team == nil then + speaker:SendSystemMessage(ChatLocalization:Get("GameChat_TeamChat_CannotTeamChatIfNotInTeam","You cannot team chat if you are not on a team!"), channel, errorExtraData) + return + end + + local channelObj = ChatService:GetChannel("Team") + if channelObj then + if not speaker:IsInChannel(channelObj.Name) then + speaker:JoinChannel(channelObj.Name) + end + if message and string.len(message) > 0 then + speaker:SayMessage(message, channelObj.Name) + end + speaker:SetMainChannel(channelObj.Name) + end + end + end + end + + local function TeamCommandsFunction(fromSpeaker, message, channel) + local processedCommand = false + + if message == nil then + error("Message is nil") + end + + if channel == "Team" then + return false + end + + if string.sub(message, 1, 6):lower() == "/team " or message:lower() == "/team" then + DoTeamCommand(fromSpeaker, string.sub(message, 7), channel) + processedCommand = true + elseif string.sub(message, 1, 3):lower() == "/t " or message:lower() == "/t" then + DoTeamCommand(fromSpeaker, string.sub(message, 4), channel) + processedCommand = true + elseif string.sub(message, 1, 2):lower() == "% " or message:lower() == "%" then + DoTeamCommand(fromSpeaker, string.sub(message, 3), channel) + processedCommand = true + end + + return processedCommand + end + + ChatService:RegisterProcessCommandsFunction("team_commands", TeamCommandsFunction, ChatConstants.StandardPriority) + + local function GetDefaultChannelNameColor() + if ChatSettings.DefaultChannelNameColor then + return ChatSettings.DefaultChannelNameColor + end + return Color3.fromRGB(35, 76, 142) + end + + local function PutSpeakerInCorrectTeamChatState(speakerObj, playerObj) + if playerObj.Neutral or playerObj.Team == nil then + speakerObj:UpdateChannelNameColor(channel.Name, GetDefaultChannelNameColor()) + + if speakerObj:IsInChannel(channel.Name) then + speakerObj:LeaveChannel(channel.Name) + end + elseif not playerObj.Neutral and playerObj.Team then + speakerObj:UpdateChannelNameColor(channel.Name, playerObj.Team.TeamColor.Color) + + if not speakerObj:IsInChannel(channel.Name) then + speakerObj:JoinChannel(channel.Name) + end + end + end + + ChatService.SpeakerAdded:connect(function(speakerName) + local speakerObj = ChatService:GetSpeaker(speakerName) + if speakerObj then + local player = speakerObj:GetPlayer() + if player then + PutSpeakerInCorrectTeamChatState(speakerObj, player) + end + end + end) + + local PlayerChangedConnections = {} + Players.PlayerAdded:connect(function(player) + local changedConn = player.Changed:connect(function(property) + local speakerObj = ChatService:GetSpeaker(player.Name) + if speakerObj then + if property == "Neutral" then + PutSpeakerInCorrectTeamChatState(speakerObj, player) + elseif property == "Team" then + PutSpeakerInCorrectTeamChatState(speakerObj, player) + if speakerObj:IsInChannel(channel.Name) then + speakerObj:SendSystemMessage( + string.gsub( + ChatLocalization:Get( + "GameChat_TeamChat_NowInTeam", + string.format("You are now on the '%s' team.", player.Team.Name) + ), + "{RBX_NAME}",player.Team.Name + ), + channel.Name + ) + end + end + end + end) + PlayerChangedConnections[player] = changedConn + end) + + Players.PlayerRemoving:connect(function(player) + local changedConn = PlayerChangedConnections[player] + if changedConn then + changedConn:Disconnect() + end + PlayerChangedConnections[player] = nil + end) +end + +return Run diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/README.md b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/README.md new file mode 100644 index 0000000..3d11d11 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/README.md @@ -0,0 +1,134 @@ +## Documentation +### ChatService +This is the main singleton object that runs the server sided chat service. It manages both ChatChannels and Speakers. + +#### Methods + ChatChannel AddChannel(string channelName, bool autoJoin) + void RemoveChannel(string channelName) + ChatChannel GetChannel(string channelName) + + Speaker AddSpeaker(string speakerName) + void RemoveSpeaker(string speakerName) + Speaker GetSpeaker(string speakerName) + + string[] GetChannelList() + string[] GetAutoJoinChannelList() + + void RegisterFilterMessageFunction(string functionId, function func, int priority) + bool FilterMessageFunctionExists(string functionId) + void UnregisterFilterMessageFunction(string functionId) + + void RegisterProcessCommandsFunction(string functionId, function func, int priority) + bool ProcessCommandsFunctionExists(string functionId) + void UnregisterProcessCommandsFunction(string functionId) + + +#### Events + ChannelAdded(string channelName) + ChannelRemoved(string channelName) + SpeakerAdded(string speakerName) + SpeakerRemoved(string speakerName) + + +### ChatChannel +A ChatChannel is an object that stores data about a single channel that Speakers can chat in. The ChatChannel manages a list of Speakers currently in the channel for relaying messages between them, and also comes with some access permission properties. + +#### Properties + string Name (read-only) + string WelcomeMessage + + bool Joinable + bool Leavable + bool AutoJoin + bool Private + +#### Methods + void RegisterFilterMessageFunction(string functionId, function func, int priority) + bool FilterMessageFunctionExists(string functionId) + void UnregisterFilterMessageFunction(string functionId) + + void RegisterProcessCommandsFunction(string functionId, function func, int priority) + bool ProcessCommandsFunctionExists(string functionId) + void UnregisterProcessCommandsFunction(string functionId) + + void KickSpeaker(string speakerName, string reason) + void MuteSpeaker(string speakerName, string reason, int length) + void UnmuteSpeaker(string speakerName) + bool IsSpeakerMuted(string speakerName) + + string[] GetSpeakerList() + + void SendSystemMessage(string message) + + MessageObject[] GetHistoryLogForSpeaker(Speaker speaker) + + void RegisterGetWelcomeMessageFunction(function func) + void UnRegisterGetWelcomeMessageFunction() + string GetWelcomeMessageForSpeaker(Speaker speaker) + +#### Events + MessagePosted(table messageObj) + SpeakerJoined(string speakerName) + SpeakerLeft(string speakerName) + SpeakerMuted(string speakerName, string reason, int length) + SpeakerUnmuted(string speakerName) + +### Speaker +A Speaker object is a representation of one entity that can speak in a ChatChannel. A Speaker can be a Player, or it can be a chat bot that is run and managed by code. + +#### Properties + string Name (read-only) + +#### Methods + void SayMessage(string message, string channelName) + + void JoinChannel(string channelName) + void LeaveChannel(string channelName) + + string[] GetChannelList() + bool IsInChannel(string channelName) + + void SendMessage(string fromSpeaker, string channel, string message) + void SendSystemMessage(string message, string channel) + + Player GetPlayer() (returns nil for non-player speakers) + + void SetExtraData(string key, Variant data) + Variant GetExtraData(string key) + +#### Events + SaidMessage(table messageObject, string channelName) + ReceivedMessage(table messageObject, string channelName) + ReceivedSystemMessage(table messageObject, string channelName) + ChannelJoined(string channelName, string channelWelcomeMessage) + ChannelLeft(string channelName) + Muted(string channelName, string reason, int length) + Unmuted(string channelName) + ExtraDataUpdated(string key, Variant value) + MainChannelSet(string channelName) + +___ + +#### Message Object format +``` +{ + int ID + string FromSpeaker + int SpeakerUserId + string OriginalChannel + bool IsFiltered + int MessageLength + string Message + string MessageType + int Time + table ExtraData { + Color3 ChatColor + Color3 NameColor + Enum.Font Font + int TextSize + table Tags + } +} +``` + Note: Message will not exist on the client if IsFiltered is False + SpeakerUserId will be 0 if the speaker is not a player. diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/Speaker.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/Speaker.lua new file mode 100644 index 0000000..7551750 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/Speaker.lua @@ -0,0 +1,292 @@ +-- // FileName: Speaker.lua +-- // Written by: Xsitsu +-- // Description: A representation of one entity that can chat in different ChatChannels. + +local module = {} + +local modulesFolder = script.Parent + +--////////////////////////////// Methods +--////////////////////////////////////// +local function ShallowCopy(table) + local copy = {} + for i, v in pairs(table) do + copy[i] = v + end + return copy +end + +local methods = {} +methods.__index = methods + +function methods:SayMessage(message, channelName, extraData) + if (self.ChatService:InternalDoProcessCommands(self.Name, message, channelName)) then return end + if (not channelName) then return end + + local channel = self.Channels[channelName:lower()] + if (not channel) then + error("Speaker is not in channel \"" .. channelName .. "\"") + end + + local messageObj = channel:InternalPostMessage(self, message, extraData) + if (messageObj) then + local success, err = pcall(function() self.eSaidMessage:Fire(messageObj, channelName) end) + if not success and err then + print("Error saying message: " ..err) + end + end + + return messageObj +end + +function methods:JoinChannel(channelName) + if (self.Channels[channelName:lower()]) then + warn("Speaker is already in channel \"" .. channelName .. "\"") + return + end + + local channel = self.ChatService:GetChannel(channelName) + if (not channel) then + error("Channel \"" .. channelName .. "\" does not exist!") + end + + self.Channels[channelName:lower()] = channel + channel:InternalAddSpeaker(self) + local success, err = pcall(function() + self.eChannelJoined:Fire(channel.Name, channel:GetWelcomeMessageForSpeaker(self)) + end) + if not success and err then + print("Error joining channel: " ..err) + end +end + +function methods:LeaveChannel(channelName) + if (not self.Channels[channelName:lower()]) then + warn("Speaker is not in channel \"" .. channelName .. "\"") + return + end + + local channel = self.Channels[channelName:lower()] + + self.Channels[channelName:lower()] = nil + channel:InternalRemoveSpeaker(self) + local success, err = pcall(function() + self.eChannelLeft:Fire(channel.Name) + end) + if not success and err then + print("Error leaving channel: " ..err) + end +end + +function methods:IsInChannel(channelName) + return (self.Channels[channelName:lower()] ~= nil) +end + +function methods:GetChannelList() + local list = {} + for i, channel in pairs(self.Channels) do + table.insert(list, channel.Name) + end + return list +end + +function methods:SendMessage(message, channelName, fromSpeaker, extraData) + local channel = self.Channels[channelName:lower()] + if (channel) then + channel:SendMessageToSpeaker(message, self.Name, fromSpeaker, extraData) + + else + warn(string.format("Speaker '%s' is not in channel '%s' and cannot receive a message in it.", self.Name, channelName)) + + end +end + +function methods:SendSystemMessage(message, channelName, extraData) + local channel = self.Channels[channelName:lower()] + if (channel) then + channel:SendSystemMessageToSpeaker(message, self.Name, extraData) + + else + warn(string.format("Speaker '%s' is not in channel '%s' and cannot receive a system message in it.", self.Name, channelName)) + + end +end + +function methods:GetPlayer() + return self.PlayerObj +end + +function methods:SetExtraData(key, value) + self.ExtraData[key] = value + self.eExtraDataUpdated:Fire(key, value) +end + +function methods:GetExtraData(key) + return self.ExtraData[key] +end + +function methods:SetMainChannel(channelName) + local success, err = pcall(function() self.eMainChannelSet:Fire(channelName) end) + if not success and err then + print("Error setting main channel: " ..err) + end +end + +--- Used to mute a speaker so that this speaker does not see their messages. +function methods:AddMutedSpeaker(speakerName) + self.MutedSpeakers[speakerName:lower()] = true +end + +function methods:RemoveMutedSpeaker(speakerName) + self.MutedSpeakers[speakerName:lower()] = false +end + +function methods:IsSpeakerMuted(speakerName) + return self.MutedSpeakers[speakerName:lower()] +end + +--///////////////// Internal-Use Methods +--////////////////////////////////////// +function methods:InternalDestroy() + for i, channel in pairs(self.Channels) do + channel:InternalRemoveSpeaker(self) + end + + self.eDestroyed:Fire() + + self.eDestroyed:Destroy() + self.eSaidMessage:Destroy() + self.eReceivedMessage:Destroy() + self.eReceivedUnfilteredMessage:Destroy() + self.eMessageDoneFiltering:Destroy() + self.eReceivedSystemMessage:Destroy() + self.eChannelJoined:Destroy() + self.eChannelLeft:Destroy() + self.eMuted:Destroy() + self.eUnmuted:Destroy() + self.eExtraDataUpdated:Destroy() + self.eMainChannelSet:Destroy() + self.eChannelNameColorUpdated:Destroy() +end + +function methods:InternalAssignPlayerObject(playerObj) + self.PlayerObj = playerObj +end + +function methods:InternalSendMessage(messageObj, channelName) + local success, err = pcall(function() + self.eReceivedUnfilteredMessage:Fire(messageObj, channelName) + end) + if not success and err then + print("Error sending internal message: " ..err) + end +end + +function methods:InternalSendFilteredMessage(messageObj, channelName) + local success, err = pcall(function() + self.eReceivedMessage:Fire(messageObj, channelName) + self.eMessageDoneFiltering:Fire(messageObj, channelName) + end) + if not success and err then + print("Error sending internal filtered message: " ..err) + end +end + +--// This method is to be used with the new filter API. This method takes the +--// TextFilterResult objects and converts them into the appropriate string +--// messages for each player. +function methods:InternalSendFilteredMessageWithFilterResult(inMessageObj, channelName) + local messageObj = ShallowCopy(inMessageObj) + + local oldFilterResult = messageObj.FilterResult + local player = self:GetPlayer() + + local msg = "" + pcall(function() + if (messageObj.IsFilterResult) then + if (player) then + msg = oldFilterResult:GetChatForUserAsync(player.UserId) + else + msg = oldFilterResult:GetNonChatStringForBroadcastAsync() + end + else + msg = oldFilterResult + end + end) + + --// Messages of 0 length are the result of two users not being allowed + --// to chat, or GetChatForUserAsync() failing. In both of these situations, + --// messages with length of 0 should not be sent. + if (#msg > 0) then + messageObj.Message = msg + messageObj.FilterResult = nil + self:InternalSendFilteredMessage(messageObj, channelName) + end +end + +function methods:InternalSendSystemMessage(messageObj, channelName) + local success, err = pcall(function() + self.eReceivedSystemMessage:Fire(messageObj, channelName) + end) + if not success and err then + print("Error sending internal system message: " ..err) + end +end + +function methods:UpdateChannelNameColor(channelName, channelNameColor) + self.eChannelNameColorUpdated:Fire(channelName, channelNameColor) +end + +--///////////////////////// Constructors +--////////////////////////////////////// + +function module.new(vChatService, name) + local obj = setmetatable({}, methods) + + obj.ChatService = vChatService + + obj.PlayerObj = nil + + obj.Name = name + obj.ExtraData = {} + + obj.Channels = {} + obj.MutedSpeakers = {} + + -- Make sure to destroy added binadable events in the InternalDestroy method. + obj.eDestroyed = Instance.new("BindableEvent") + obj.eSaidMessage = Instance.new("BindableEvent") + obj.eReceivedMessage = Instance.new("BindableEvent") + obj.eReceivedUnfilteredMessage = Instance.new("BindableEvent") + obj.eMessageDoneFiltering = Instance.new("BindableEvent") + obj.eReceivedSystemMessage = Instance.new("BindableEvent") + obj.eChannelJoined = Instance.new("BindableEvent") + obj.eChannelLeft = Instance.new("BindableEvent") + obj.eMuted = Instance.new("BindableEvent") + obj.eUnmuted = Instance.new("BindableEvent") + obj.eExtraDataUpdated = Instance.new("BindableEvent") + obj.eMainChannelSet = Instance.new("BindableEvent") + obj.eChannelNameColorUpdated = Instance.new("BindableEvent") + + obj.Destroyed = obj.eDestroyed.Event + obj.SaidMessage = obj.eSaidMessage.Event + obj.ReceivedMessage = obj.eReceivedMessage.Event + obj.ReceivedUnfilteredMessage = obj.eReceivedUnfilteredMessage.Event + obj.MessageDoneFiltering = obj.eMessageDoneFiltering.Event + obj.ReceivedSystemMessage = obj.eReceivedSystemMessage.Event + obj.ChannelJoined = obj.eChannelJoined.Event + obj.ChannelLeft = obj.eChannelLeft.Event + obj.Muted = obj.eMuted.Event + obj.Unmuted = obj.eUnmuted.Event + obj.ExtraDataUpdated = obj.eExtraDataUpdated.Event + obj.MainChannelSet = obj.eMainChannelSet.Event + obj.ChannelNameColorUpdated = obj.eChannelNameColorUpdated.Event + + --- DEPRECATED: + --- Mispelled version of ReceivedUnfilteredMessage, retained for compatibility with legacy versions. + obj.RecievedUnfilteredMessage = obj.eReceivedUnfilteredMessage.Event + + return obj +end + +return module diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/Util.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/Util.lua new file mode 100644 index 0000000..6fb9da9 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerChat/Util.lua @@ -0,0 +1,110 @@ +-- // FileName: Util.lua +-- // Written by: TheGamer101 +-- // Description: Utility code used by the server side chat implementation. + +local Chat = game:GetService("Chat") +local ReplicatedModules = Chat:WaitForChild("ClientChatModules") +local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants")) + +local DEFAULT_PRIORITY = ChatConstants.StandardPriority +if DEFAULT_PRIORITY == nil then + DEFAULT_PRIORITY = 10 +end + +local Util = {} +Util.__index = Util + +local SortedFunctionContainer = {}; do + -- This sorted function container is used to handle the logic around storing filter functions and + -- command processors by priority. + + local methods = {} + methods.__index = methods + + function methods:RebuildProcessCommandsPriorities() + self.RegisteredPriorites = {} + for priority, functions in pairs(self.FunctionMap) do + local functionsEmpty = true + for funcId, funciton in pairs(functions) do + functionsEmpty = false + break + end + if not functionsEmpty then + table.insert(self.RegisteredPriorites, priority) + end + end + table.sort(self.RegisteredPriorites, function(a, b) + return a > b + end) + end + + function methods:HasFunction(funcId) + if self.RegisteredFunctions[funcId] == nil then + return false + end + return true + end + + function methods:RemoveFunction(funcId) + local functionPriority = self.RegisteredFunctions[funcId] + self.RegisteredFunctions[funcId] = nil + self.FunctionMap[functionPriority][funcId] = nil + self:RebuildProcessCommandsPriorities() + end + + function methods:AddFunction(funcId, func, priority) + if priority == nil then + priority = DEFAULT_PRIORITY + end + + if self.RegisteredFunctions[funcId] then + error(funcId .. " is already in use!") + end + + self.RegisteredFunctions[funcId] = priority + + if self.FunctionMap[priority] == nil then + self.FunctionMap[priority] = {} + end + + self.FunctionMap[priority][funcId] = func + self:RebuildProcessCommandsPriorities() + end + + function methods:GetIterator() + local priorityIndex = 1 + local funcId = nil + local func = nil + + return function() + while true do + if priorityIndex > #self.RegisteredPriorites then + return + end + local priority = self.RegisteredPriorites[priorityIndex] + funcId, func = next(self.FunctionMap[priority], funcId) + if funcId == nil then + priorityIndex = priorityIndex + 1 + else + return funcId, func, priority + end + end + end + end + + function SortedFunctionContainer.new() + local obj = setmetatable({}, methods) + + obj.RegisteredFunctions = {} + obj.RegisteredPriorites = {} + obj.FunctionMap = {} + + return obj + end +end + +function Util:NewSortedFunctionContainer() + return SortedFunctionContainer.new() +end + +return Util diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerPlayer/DefaultServerPlayerModules/PlayerSettings.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerPlayer/DefaultServerPlayerModules/PlayerSettings.lua new file mode 100644 index 0000000..a507607 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerPlayer/DefaultServerPlayerModules/PlayerSettings.lua @@ -0,0 +1,8 @@ +-- If you want to create your own custom player settings, +-- make a copy of this script and add it to your StarterPlayer object + +local module = {} + +module.UseDefaultAnimations = false -- force incoming players to use default avatar animations, not custom animations + +return module \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerPlayer/ServerPlayerInstaller.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerPlayer/ServerPlayerInstaller.lua new file mode 100644 index 0000000..cb9b07c --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerPlayer/ServerPlayerInstaller.lua @@ -0,0 +1,17 @@ + +local Install = function () + + local playerConfig = game:GetService("StarterPlayer"):FindFirstChild("PlayerSettings") + if (playerConfig == nil) then + playerConfig = Instance.new("ModuleScript") + playerConfig.Name = "PlayerSettings" + playerConfig.Source = script.Parent.DefaultServerPlayerModules:WaitForChild("PlayerSettings").Source + playerConfig.Parent = game:GetService("StarterPlayer") + playerConfig.Archivable = false + end + +end + +return Install + + diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerSound/SoundDispatcher.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerSound/SoundDispatcher.lua new file mode 100644 index 0000000..94b018f --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerSound/SoundDispatcher.lua @@ -0,0 +1,60 @@ +--[[ + The sound dispatcher will fire sound events to properly loaded characters. This script manages a list of + characters currently loaded in the game. When a character fires a sound event, this dispatcher will + check to make sure the event only fires on characters who have loaded in. +--]] +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local SOUND_EVENT_FOLDER_NAME = "DefaultSoundEvents" + +if UserSettings():IsUserFeatureEnabled("UserUseSoundDispatcher") then + local loadedCharacters = {} + + local EventFolder = ReplicatedStorage:FindFirstChild(SOUND_EVENT_FOLDER_NAME) + if not EventFolder then + EventFolder = Instance.new("Folder") + EventFolder.Name = SOUND_EVENT_FOLDER_NAME + EventFolder.Archivable = false + EventFolder.Parent = ReplicatedStorage + end + + local function createEvent(name, instanceType) + local newEvent = EventFolder:FindFirstChild(name) + if not newEvent then + newEvent = Instance.new(instanceType) + newEvent.Name = name + newEvent.Parent = EventFolder + end + + return newEvent + end + + local DefaultServerSoundEvent = createEvent("DefaultServerSoundEvent", "RemoteEvent") + local AddCharacterLoadedEvent = createEvent("AddCharacterLoadedEvent", "RemoteEvent") + local RemoveCharacterEvent = createEvent("RemoveCharacterEvent", "RemoteEvent") + + -- Fire the sound event to all clients connected + local function fireDefaultServerSoundEventToClient(player, sound, playing, resetPosition) + if loadedCharacters[player] then + DefaultServerSoundEvent:FireClient(player, sound, playing, resetPosition) + end + end + + -- Add a character to the list of clients ready to receive sounds + local function addCharacterLoaded(player) + loadedCharacters[player] = true + end + + -- Remove a character from the table + local function removeCharacter(player) + loadedCharacters[player] = nil + end + + local soundDispatcher = createEvent("SoundDispatcher", "BindableEvent") + soundDispatcher.Event:Connect(fireDefaultServerSoundEventToClient) + + --no op function to prevent rogue client from filling RemoteEvent queue + DefaultServerSoundEvent.OnServerEvent:Connect(function() end) + AddCharacterLoadedEvent.OnServerEvent:Connect(addCharacterLoaded) + RemoveCharacterEvent.OnServerEvent:Connect(removeCharacter) +end diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerSound/SoundDispatcherInstaller.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerSound/SoundDispatcherInstaller.lua new file mode 100644 index 0000000..04a1878 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerSound/SoundDispatcherInstaller.lua @@ -0,0 +1,31 @@ +-- Load the SoundDispatcher script into Studio +local soundDispatcherName = "SoundDispatcher" +local ServerScriptService = game:GetService("ServerScriptService") + +local function LoadScript(name, parent) + local originalModule = script.Parent:WaitForChild(name) + local script = Instance.new("Script") + script.Name = name + script.Source = originalModule.Source + script.Parent = parent + return script +end + +local function Install() + local soundDispatcherArchivable = true + local SoundDispatcher = ServerScriptService:FindFirstChild(soundDispatcherName) + if not SoundDispatcher then + soundDispatcherArchivable = false + SoundDispatcher = LoadScript(soundDispatcherName, ServerScriptService) + end + + if not ServerScriptService:FindFirstChild(soundDispatcherName) then + local SoundDispatcherCopy = SoundDispatcher:Clone() + SoundDispatcherCopy.Archivable = false + SoundDispatcherCopy.Parent = ServerScriptService + end + + SoundDispatcher.Archivable = soundDispatcherArchivable +end + +return Install diff --git a/Client2018/content/scripts/CoreScripts/Modules/Server/ServerUtil.lua b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerUtil.lua new file mode 100644 index 0000000..3941af5 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Server/ServerUtil.lua @@ -0,0 +1 @@ +return {} \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/GameSettings.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/GameSettings.lua new file mode 100644 index 0000000..40f0ee2 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/GameSettings.lua @@ -0,0 +1,1475 @@ +--[[ + Filename: GameSettings.lua + Written by: jeditkacheff + Version 1.1 + Description: Takes care of the Game Settings Tab in Settings Menu +--]] +-------------- SERVICES -------------- +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local GuiService = game:GetService("GuiService") +local UserInputService = game:GetService("UserInputService") +local RunService = game:GetService("RunService") +local PlatformService = nil +pcall( + function() + PlatformService = game:GetService("PlatformService") + end +) +local ContextActionService = game:GetService("ContextActionService") +local StarterGui = game:GetService("StarterGui") +local Players = game:GetService("Players") +local VRService = game:GetService("VRService") +local Settings = UserSettings() +local GameSettings = Settings.GameSettings + +-------------- CONSTANTS -------------- +local GRAPHICS_QUALITY_LEVELS = 10 +local GRAPHICS_QUALITY_TO_INT = { + ["Enum.SavedQualitySetting.Automatic"] = 0, + ["Enum.SavedQualitySetting.QualityLevel1"] = 1, + ["Enum.SavedQualitySetting.QualityLevel2"] = 2, + ["Enum.SavedQualitySetting.QualityLevel3"] = 3, + ["Enum.SavedQualitySetting.QualityLevel4"] = 4, + ["Enum.SavedQualitySetting.QualityLevel5"] = 5, + ["Enum.SavedQualitySetting.QualityLevel6"] = 6, + ["Enum.SavedQualitySetting.QualityLevel7"] = 7, + ["Enum.SavedQualitySetting.QualityLevel8"] = 8, + ["Enum.SavedQualitySetting.QualityLevel9"] = 9, + ["Enum.SavedQualitySetting.QualityLevel10"] = 10 +} +local PC_CHANGED_PROPS = { + DevComputerMovementMode = true, + DevComputerCameraMode = true, + DevEnableMouseLock = true +} +local TOUCH_CHANGED_PROPS = { + DevTouchMovementMode = true, + DevTouchCameraMode = true +} +local CAMERA_MODE_DEFAULT_STRING = UserInputService.TouchEnabled and "Default (Follow)" or "Default (Classic)" + +local MOVEMENT_MODE_DEFAULT_STRING = UserInputService.TouchEnabled and "Default (Thumbstick)" or "Default (Keyboard)" +local MOVEMENT_MODE_KEYBOARDMOUSE_STRING = "Keyboard + Mouse" +local MOVEMENT_MODE_CLICKTOMOVE_STRING = UserInputService.TouchEnabled and "Tap to Move" or "Click to Move" +local MOVEMENT_MODE_DYNAMICTHUMBSTICK_STRING = "Dynamic Thumbstick" + +----------- UTILITIES -------------- +local utility = require(RobloxGui.Modules.Settings.Utility) + +------------ Variables ------------------- +RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface") +RobloxGui:WaitForChild("Modules"):WaitForChild("Settings"):WaitForChild("SettingsHub") +local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled() +local PageInstance = nil +local LocalPlayer = Players.LocalPlayer +local platform = UserInputService:GetPlatform() + +local success, result = + pcall( + function() + return settings():GetFFlag("UseNotificationsLocalization") + end +) +local FFlagUseNotificationsLocalization = success and result + +local FFlagEnableNewDevConsole = settings():GetFFlag("EnableNewDevConsole") + +local UseMicroProfiler = false +local isDesktopClient = (platform == Enum.Platform.Windows) or (platform == Enum.Platform.OSX) or (platform == Enum.Platform.UWP) +local isMobileClient = (platform == Enum.Platform.IOS) or (platform == Enum.Platform.Android) +if isMobileClient then + UseMicroProfiler = settings():GetFFlag("EnableMobileMicroProfilerWebServerApi") +elseif isDesktopClient then + UseMicroProfiler = settings():GetFFlag("EnableDesktopMicroProfilerApi") +end +local FixCameraControlSetting = settings():GetFFlag("FixCameraControlSetting") +local EnableWebServerOnStart = settings():GetFFlag("EnableWebServerOnStart") + +--------------- FLAGS ---------------- + +----------- CLASS DECLARATION -------------- + +local function Initialize() + local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory) + local this = settingsPageFactory:CreateNewPage() + + local allSettingsCreated = false + local settingsDisabledInVR = {} + local function onVRSettingsReady() + local vrEnabled = VRService.VREnabled + for settingFrame, _ in pairs(settingsDisabledInVR) do + settingFrame.Visible = not vrEnabled + end + end + + local function onVREnabled() + if VRService.VREnabled and allSettingsCreated then + --Only call this if all settings have been created. + --If they aren't ready by the time VR is enabled, this + --will be called later when they are. + onVRSettingsReady() + end + end + VRService:GetPropertyChangedSignal("VREnabled"):connect(onVREnabled) + onVREnabled() + + ----------- FUNCTIONS --------------- + local function createGraphicsOptions() + ------------------ Fullscreen Selection GUI Setup ------------------ + local fullScreenInit = 1 + if not GameSettings:InFullScreen() then + fullScreenInit = 2 + end + + this.FullscreenFrame, this.FullscreenLabel, this.FullscreenEnabler = + utility:AddNewRow(this, "Fullscreen", "Selector", {"On", "Off"}, fullScreenInit) + this.FullscreenFrame.LayoutOrder = 6 + + settingsDisabledInVR[this.FullscreenFrame] = true + + this.FullscreenEnabler.IndexChanged:connect( + function(newIndex) + if newIndex == 1 then + if not GameSettings:InFullScreen() then + GuiService:ToggleFullscreen() + this.FullscreenEnabler:SetSelectionIndex(1) + end + elseif newIndex == 2 then + if GameSettings:InFullScreen() then + GuiService:ToggleFullscreen() + this.FullscreenEnabler:SetSelectionIndex(2) + end + end + end + ) + + GameSettings.FullscreenChanged:connect( + function(isFullScreen) + if isFullScreen then + if this.FullscreenEnabler:GetSelectedIndex() ~= 1 then + this.FullscreenEnabler:SetSelectionIndex(1) + end + else + if this.FullscreenEnabler:GetSelectedIndex() ~= 2 then + this.FullscreenEnabler:SetSelectionIndex(2) + end + end + end + ) + + ------------------ Gfx Enabler Selection GUI Setup ------------------ + local graphicsEnablerStart = 1 + if GameSettings.SavedQualityLevel ~= Enum.SavedQualitySetting.Automatic then + graphicsEnablerStart = 2 + end + + this.GraphicsEnablerFrame, this.GraphicsEnablerLabel, this.GraphicsQualityEnabler = + utility:AddNewRow(this, "Graphics Mode", "Selector", {"Automatic", "Manual"}, graphicsEnablerStart) + this.GraphicsEnablerFrame.LayoutOrder = 7 + + ------------------ Gfx Slider GUI Setup ------------------ + this.GraphicsQualityFrame, this.GraphicsQualityLabel, this.GraphicsQualitySlider = + utility:AddNewRow(this, "Graphics Quality", "Slider", GRAPHICS_QUALITY_LEVELS, 1) + this.GraphicsQualityFrame.LayoutOrder = 8 + this.GraphicsQualitySlider:SetMinStep(1) + + ------------------------------------------------------ + ------------------------- Connection Setup ---------------------------- + settings().Rendering.EnableFRM = true + + function SetGraphicsQuality(newValue, automaticSettingAllowed) + local percentage = newValue / GRAPHICS_QUALITY_LEVELS + local newQualityLevel = math.floor((settings().Rendering:GetMaxQualityLevel() - 1) * percentage) + if newQualityLevel == 20 then + newQualityLevel = 21 + elseif newValue == 1 then + newQualityLevel = 1 + elseif newValue < 1 and not automaticSettingAllowed then + newValue = 1 + newQualityLevel = 1 + elseif newQualityLevel > settings().Rendering:GetMaxQualityLevel() then + newQualityLevel = settings().Rendering:GetMaxQualityLevel() - 1 + end + + GameSettings.SavedQualityLevel = newValue + settings().Rendering.QualityLevel = newQualityLevel + end + + local function setGraphicsToAuto() + this.GraphicsQualitySlider:SetZIndex(1) + this.GraphicsQualityLabel.ZIndex = 1 + this.GraphicsQualitySlider:SetInteractable(false) + + SetGraphicsQuality(Enum.QualityLevel.Automatic.Value, true) + end + + local function setGraphicsToManual(level) + this.GraphicsQualitySlider:SetZIndex(2) + this.GraphicsQualityLabel.ZIndex = 2 + this.GraphicsQualitySlider:SetInteractable(true) + + -- need to force the quality change if slider is already at this position + if this.GraphicsQualitySlider:GetValue() == level then + SetGraphicsQuality(level) + else + this.GraphicsQualitySlider:SetValue(level) + end + end + + game.GraphicsQualityChangeRequest:connect( + function(isIncrease) + -- was using settings().Rendering.Quality level, which was wrongly saying it was automatic. + if GameSettings.SavedQualityLevel == Enum.SavedQualitySetting.Automatic then + return + end + local currentGraphicsSliderValue = this.GraphicsQualitySlider:GetValue() + if isIncrease then + currentGraphicsSliderValue = currentGraphicsSliderValue + 1 + else + currentGraphicsSliderValue = currentGraphicsSliderValue - 1 + end + + this.GraphicsQualitySlider:SetValue(currentGraphicsSliderValue) + end + ) + + this.GraphicsQualitySlider.ValueChanged:connect( + function(newValue) + SetGraphicsQuality(newValue) + end + ) + + this.GraphicsQualityEnabler.IndexChanged:connect( + function(newIndex) + if newIndex == 1 then + setGraphicsToAuto() + elseif newIndex == 2 then + setGraphicsToManual(this.GraphicsQualitySlider:GetValue()) + end + end + ) + + -- initialize the slider position + if GameSettings.SavedQualityLevel == Enum.SavedQualitySetting.Automatic then + this.GraphicsQualitySlider:SetValue(5) + setGraphicsToAuto() + else + local graphicsLevel = tostring(GameSettings.SavedQualityLevel) + if GRAPHICS_QUALITY_TO_INT[graphicsLevel] then + graphicsLevel = GRAPHICS_QUALITY_TO_INT[graphicsLevel] + else + graphicsLevel = GRAPHICS_QUALITY_LEVELS + end + SetGraphicsQuality(graphicsLevel) + spawn( + function() + this.GraphicsQualitySlider:SetValue(graphicsLevel) + end + ) + end + end -- of createGraphicsOptions + + local function createPerformanceStatsOptions() + ------------------ + ------------------ Performance Stats ----------------- + this.PerformanceStatsFrame, this.PerformanceStatsLabel, this.PerformanceStatsMode, this.PerformanceStatsOverrideText = + nil + + function GetDesiredPerformanceStatsIndex() + if GameSettings.PerformanceStatsVisible then + return 1 + else + return 2 + end + end + + local startIndex = GetDesiredPerformanceStatsIndex() + + this.PerformanceStatsFrame, this.PerformanceStatsLabel, this.PerformanceStatsMode = + utility:AddNewRow(this, "Performance Stats", "Selector", {"On", "Off"}, startIndex) + this.PerformanceStatsFrame.LayoutOrder = 9 + + this.PerformanceStatsOverrideText = + utility:Create "TextLabel" { + Name = "PerformanceStatsLabel", + Text = "Set by Developer", + TextColor3 = Color3.new(1, 1, 1), + Font = Enum.Font.SourceSans, + FontSize = Enum.FontSize.Size24, + BackgroundTransparency = 1, + Size = UDim2.new(0, 200, 1, 0), + Position = UDim2.new(1, -350, 0, 0), + Visible = false, + ZIndex = 2, + Parent = this.PerformanceStatsFrame + } + + this.PerformanceStatsMode.IndexChanged:connect( + function(newIndex) + if newIndex == 1 then + GameSettings.PerformanceStatsVisible = true + else + GameSettings.PerformanceStatsVisible = false + end + end + ) + + GameSettings.PerformanceStatsVisibleChanged:connect( + function() + local desiredIndex = GetDesiredPerformanceStatsIndex() + if desiredIndex ~= this.PerformanceStatsMode.CurrentIndex then + this.PerformanceStatsMode:SetSelectionIndex(desiredIndex) + end + end + ) + end -- of createPerformanceStats + + -- Create UI element to show IPs and port a player need to access the + -- web server for micro profiler + local function createWebServerInformationRow() + this.InformationFrame, this.InformationLabel, this.InformationTextBox = + utility:AddNewRow(this, "MicroProfiler Information", "TextBox", nil, nil, 5) + this.InformationFrame.LayoutOrder = 99 -- I want this always to be the last shown + + -- Override the default position + -- todo replace this with TextX and TextYAlignment to centerlise the text + this.InformationFrame.Position = UDim2.new(0.5, 0, 0.5, 0) + + this.InformationText = + utility:Create "TextLabel" { + Name = "InformationLabel", + Text = "Information Loading", + Font = Enum.Font.SourceSans, + FontSize = Enum.FontSize.Size14, + BackgroundTransparency = 1, + Size = UDim2.new(0, 800, 1, 0), + Position = UDim2.new(1, -650, 0, 20), + Visible = true, + ZIndex = 2, + Parent = this.InformationFrame + } + return this.InformationFrame, this.InformationText + end + + local function createMicroProfilerOptions() + ------------------ + ------------------ Micro Profiler Web Server ----------------- + this.MicroProfilerFrame, this.MicroProfilerLabel, this.MicroProfilerMode, this.MicroProfilerOverrideText = nil + + local function tryContentLabel() + local port = GameSettings.MicroProfilerWebServerPort + if port ~= 0 then + -- Need to create this each time. + this.InformationFrame, this.InformationText = createWebServerInformationRow() + this.InformationText.Text = GameSettings.MicroProfilerWebServerIP .. port + return true + else + return false + end + end + + local function setMicroProfilerIndex(newIndex) + local function hideContentLabel() + GameSettings.MicroProfilerWebServerEnabled = false + + if this.InformationFrame or this.InformationText then + this.InformationFrame.Visible = false + this.InformationFrame.Parent = nil + this.InformationText.Parent = nil + this.InformationFrame = nil + this.InformationText = nil + end + end + + if isMobileClient then + if newIndex == 1 then -- Show Web Server Content Label + GameSettings.MicroProfilerWebServerEnabled = true + + -- Try poll every 0.1 seconds until 3 seconds passed + local tryPollCount = 30 + while(tryPollCount >= 1) do + if tryContentLabel() then + break + end + + tryPollCount = tryPollCount - 1 + wait(0.1) + end + + if tryPollCount <= 0 then + -- if the web server has not been started, we will just set the switch and try to stop the + -- web server + this.MicroProfilerMode:SetSelectionIndex(2) + hideContentLabel() + end + else -- Hide Web Server Content Label + hideContentLabel() + end + elseif isDesktopClient then + GameSettings.OnScreenProfilerEnabled = (newIndex == 1) + end + end + + -- This should be off default. + local function GetDesiredWebServerIndex() + if isMobileClient then + if GameSettings.MicroProfilerWebServerEnabled then + return 1 + else + return 2 + end + elseif isDesktopClient then + if GameSettings.OnScreenProfilerEnabled then + return 1 + else + return 2 + end + end + end + + local webServerIndex = GetDesiredWebServerIndex() + + this.MicroProfilerFrame, this.MicroProfilerLabel, this.MicroProfilerMode = + utility:AddNewRow(this, "Micro Profiler", "Selector", {"On", "Off"}, webServerIndex) -- This can be set to override defualt micro profiler state + this.MicroProfilerFrame.LayoutOrder = 10 + + if EnableWebServerOnStart then + tryContentLabel() + end + + this.MicroProfilerMode.IndexChanged:connect( + setMicroProfilerIndex + ) + end -- of create Micro Profiler Web Server + + local function createCameraModeOptions(movementModeEnabled) + ------------------------------------------------------ + ------------------ + ------------------ Shift Lock Switch ----------------- + if UserInputService.MouseEnabled and not isTenFootInterface then + this.ShiftLockFrame, this.ShiftLockLabel, this.ShiftLockMode, this.ShiftLockOverrideText = nil + + if UserInputService.MouseEnabled and UserInputService.KeyboardEnabled then + local startIndex = 2 + if GameSettings.ControlMode == Enum.ControlMode.MouseLockSwitch then + startIndex = 1 + end + + this.ShiftLockFrame, this.ShiftLockLabel, this.ShiftLockMode = + utility:AddNewRow(this, "Shift Lock Switch", "Selector", {"On", "Off"}, startIndex) + this.ShiftLockFrame.LayoutOrder = 1 + + settingsDisabledInVR[this.ShiftLockFrame] = true + + this.ShiftLockOverrideText = + utility:Create "TextLabel" { + Name = "ShiftLockOverrideLabel", + Text = "Set by Developer", + TextColor3 = Color3.new(1, 1, 1), + Font = Enum.Font.SourceSans, + FontSize = Enum.FontSize.Size24, + BackgroundTransparency = 1, + Size = UDim2.new(0, 200, 1, 0), + Position = UDim2.new(1, -350, 0, 0), + Visible = false, + ZIndex = 2, + Parent = this.ShiftLockFrame + } + + this.ShiftLockMode.IndexChanged:connect( + function(newIndex) + if newIndex == 1 then + GameSettings.ControlMode = Enum.ControlMode.MouseLockSwitch + else + GameSettings.ControlMode = Enum.ControlMode.Classic + end + end + ) + end + end + + ------------------------------------------------------ + ------------------ + ------------------ Camera Mode ----------------------- + local enumItems = {} + + function setCameraModeVisible(visible) + if this.CameraMode then + this.CameraMode.SelectorFrame.Visible = visible + this.CameraMode:SetInteractable(visible) + this.CameraModeOverrideText.Visible = not visible + end + end + + do + local startingCameraEnumItem = 1 --todo: remove with FixCameraControlSetting + + local PlayerScripts = LocalPlayer:WaitForChild("PlayerScripts") + + local cameraEnumNames = {} + local cameraEnumNameToItem = {} + + local function updateCurrentCameraMovementIndex(index) + local newEnumSetting = nil + local success = + pcall( + function() + newEnumSetting = cameraEnumNameToItem[cameraEnumNames[index]] + end + ) + if not success or newEnumSetting == nil then + return + end + + if UserInputService.TouchEnabled then + GameSettings.TouchCameraMovementMode = newEnumSetting + else + GameSettings.ComputerCameraMovementMode = newEnumSetting + end + end + + local function updateCameraMovementModes() + local enumsToAdd = nil + + if UserInputService.TouchEnabled then + enumsToAdd = PlayerScripts:GetRegisteredTouchCameraMovementModes() + else + enumsToAdd = PlayerScripts:GetRegisteredComputerCameraMovementModes() + end + + if FixCameraControlSetting then + cameraEnumNames = {} + cameraEnumNameToItem = {} + end + + if #enumsToAdd <= 0 then + if not FixCameraControlSetting then + cameraEnumNames = {} + cameraEnumNameToItem = {} + end + setCameraModeVisible(false) + return + end + + setCameraModeVisible(true) + + for i = 1, #enumsToAdd do + local newCameraMode = enumsToAdd[i] + local displayName = newCameraMode.Name + if displayName == "Default" then + displayName = CAMERA_MODE_DEFAULT_STRING + end + + if not FixCameraControlSetting then + if UserInputService.TouchEnabled then + if GameSettings.TouchCameraMovementMode == newCameraMode then + startingCameraEnumItem = i + end + else + if GameSettings.ComputerCameraMovementMode == newCameraMode then + startingCameraEnumItem = i + end + end + end + + cameraEnumNames[#cameraEnumNames + 1] = displayName + cameraEnumNameToItem[displayName] = newCameraMode.Value + end + + if this.CameraMode then + this.CameraMode:UpdateOptions(cameraEnumNames) + end + + if FixCameraControlSetting then + local currentSavedMode = -1 + + if UserInputService.TouchEnabled then + currentSavedMode = GameSettings.TouchCameraMovementMode.Value + else + currentSavedMode = GameSettings.ComputerCameraMovementMode.Value + end + + if currentSavedMode > -1 then + currentSavedMode = currentSavedMode + 1 + local savedEnum = nil + local exists = + pcall( + function() + savedEnum = enumsToAdd[currentSavedMode] + end + ) + if exists and savedEnum then + updateCurrentCameraMovementIndex(savedEnum.Value + 1) + this.CameraMode:SetSelectionIndex(savedEnum.Value + 1) + end + end + else + updateCurrentCameraMovementIndex(this.CameraMode.CurrentIndex) + end + end + + if FixCameraControlSetting then + this.CameraModeFrame, this.CameraModeLabel, this.CameraMode = + utility:AddNewRow(this, "Camera Mode", "Selector", cameraEnumNames, 1) + else + this.CameraModeFrame, this.CameraModeLabel, this.CameraMode = + utility:AddNewRow(this, "Camera Mode", "Selector", cameraEnumNames, startingCameraEnumItem) + end + this.CameraModeFrame.LayoutOrder = 2 + + settingsDisabledInVR[this.CameraMode] = true + + this.CameraModeOverrideText = + utility:Create "TextLabel" { + Name = "CameraDevOverrideLabel", + Text = "Set by Developer", + TextColor3 = Color3.new(1, 1, 1), + Font = Enum.Font.SourceSans, + FontSize = Enum.FontSize.Size24, + BackgroundTransparency = 1, + Size = UDim2.new(0.6, 0, 1, 0), + AnchorPoint = Vector2.new(1, 0.5), + Position = UDim2.new(1, 0, 0.5, 0), + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + Visible = false, + ZIndex = 2, + Parent = this.CameraModeFrame + } + + PlayerScripts.TouchCameraMovementModeRegistered:connect( + function(registeredMode) + if UserInputService.TouchEnabled then + updateCameraMovementModes() + end + end + ) + + PlayerScripts.ComputerCameraMovementModeRegistered:connect( + function(registeredMode) + if UserInputService.MouseEnabled then + updateCameraMovementModes() + end + end + ) + + this.CameraMode.IndexChanged:connect( + function(newIndex) + updateCurrentCameraMovementIndex(newIndex) + end + ) + + updateCameraMovementModes() + end + + ------------------------------------------------------ + ------------------ + ------------------ VR Mode ----------------------- + local createdVROption = false + local function createVROption() + if not createdVROption then + createdVROption = true + + local optionNames + if GameSettings.VREnabled then + optionNames = {"On", "Off (restart pending)"} + else + optionNames = {"On (restart pending)", "Off"} + end + + this.VREnabledFrame, this.VREnabledLabel, this.VREnabledSelector = + utility:AddNewRow(this, "VR", "Selector", optionNames, GameSettings.VREnabled and 1 or 2) + this.VREnabledFrame.LayoutOrder = 12 + + this.VREnabledSelector.IndexChanged:connect( + function(newIndex) + local vrEnabledSetting = (newIndex == 1) + if GameSettings.VREnabled ~= vrEnabledSetting then + GameSettings.VREnabled = vrEnabledSetting + end + end + ) + end + end + + local function onVREnabledChanged() + if VRService.VREnabled then + GameSettings.HasEverUsedVR = true + createVROption() + else + if GameSettings.HasEverUsedVR then + createVROption() + end + end + end + onVREnabledChanged() + VRService:GetPropertyChangedSignal("VREnabled"):connect(onVREnabledChanged) + + ------------------------------------------------------ + ------------------ + ------------------ Movement Mode --------------------- + local movementModes = {} + + function setMovementModeVisible(visible) + if this.MovementMode then + local shouldBeVisible = visible and (#movementModes > 0) + this.MovementMode.SelectorFrame.Visible = shouldBeVisible + this.MovementMode:SetInteractable(shouldBeVisible) + this.MovementModeOverrideText.Visible = not shouldBeVisible + end + end + + if movementModeEnabled then + local startingMovementEnumItem = 1 --todo: remove with FixCameraControlSetting + local movementEnumNames = {} + local movementEnumNameToItem = {} + + local PlayerScripts = LocalPlayer:WaitForChild("PlayerScripts") + + local function getDisplayName(name) + local displayName = name + if name == "Default" then + displayName = MOVEMENT_MODE_DEFAULT_STRING + elseif name == "KeyboardMouse" then + displayName = MOVEMENT_MODE_KEYBOARDMOUSE_STRING + elseif name == "ClickToMove" then + displayName = MOVEMENT_MODE_CLICKTOMOVE_STRING + elseif name == "DynamicThumbstick" then + displayName = MOVEMENT_MODE_DYNAMICTHUMBSTICK_STRING + end + + return displayName + end + + if FixCameraControlSetting then + this.MovementModeFrame, this.MovementModeLabel, this.MovementMode = + utility:AddNewRow(this, "Movement Mode", "Selector", movementEnumNames, 1) + else + this.MovementModeFrame, this.MovementModeLabel, this.MovementMode = + utility:AddNewRow(this, "Movement Mode", "Selector", movementEnumNames, startingMovementEnumItem) + end + this.MovementModeFrame.LayoutOrder = 3 + + settingsDisabledInVR[this.MovementMode] = true + + this.MovementModeOverrideText = + utility:Create "TextLabel" { + Name = "MovementDevOverrideLabel", + Text = "Set by Developer", + TextColor3 = Color3.new(1, 1, 1), + Font = Enum.Font.SourceSans, + FontSize = Enum.FontSize.Size24, + BackgroundTransparency = 1, + Size = UDim2.new(0.6, 0, 1, 0), + AnchorPoint = Vector2.new(1, 0.5), + Position = UDim2.new(1, 0, 0.5, 0), + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + Visible = false, + ZIndex = 2, + Parent = this.MovementModeFrame + } + + local function setMovementModeToIndex(index) + local newEnumSetting = nil + local success = + pcall( + function() + newEnumSetting = movementEnumNameToItem[movementEnumNames[index]] + end + ) + if not success or newEnumSetting == nil then + return + end + + if UserInputService.TouchEnabled then + GameSettings.TouchMovementMode = newEnumSetting + else + GameSettings.ComputerMovementMode = newEnumSetting + end + end + + local function updateMovementModes() + if UserInputService.TouchEnabled then + movementModes = PlayerScripts:GetRegisteredTouchMovementModes() + else + movementModes = PlayerScripts:GetRegisteredComputerMovementModes() + end + + movementEnumNames = {} + movementEnumNameToItem = {} + + if #movementModes <= 0 then + setMovementModeVisible(false) + return + end + + setMovementModeVisible(true) + + for i = 1, #movementModes do + local movementMode = movementModes[i] + + local displayName = getDisplayName(movementMode.Name) + + if not FixCameraControlSetting then + if UserInputService.TouchEnabled then + if GameSettings.TouchMovementMode == movementMode then + startingMovementEnumItem = movementMode.Value + 1 + end + else + if GameSettings.ComputerMovementMode == movementModes[i] then + startingMovementEnumItem = movementMode.Value + 1 + end + end + end + + movementEnumNames[#movementEnumNames + 1] = displayName + movementEnumNameToItem[displayName] = movementMode + end + + if this.MovementMode then + this.MovementMode:UpdateOptions(movementEnumNames) + end + + if FixCameraControlSetting then + local currentSavedMode = -1 + + if UserInputService.TouchEnabled then + currentSavedMode = GameSettings.TouchMovementMode.Value + else + currentSavedMode = GameSettings.ComputerMovementMode.Value + end + + if currentSavedMode > -1 then + currentSavedMode = currentSavedMode + 1 + local savedEnum = nil + local exists = + pcall( + function() + savedEnum = movementEnumNameToItem[movementEnumNames[currentSavedMode]] + end + ) + if exists and savedEnum then + setMovementModeToIndex(savedEnum.Value + 1) + this.MovementMode:SetSelectionIndex(savedEnum.Value + 1) + end + end + else + setMovementModeToIndex(this.MovementMode.CurrentIndex) + end + end + + updateMovementModes() + + PlayerScripts.TouchMovementModeRegistered:connect( + function(registeredMode) + if UserInputService.TouchEnabled then + updateMovementModes() + end + end + ) + + PlayerScripts.ComputerMovementModeRegistered:connect( + function(registeredMode) + if UserInputService.MouseEnabled then + updateMovementModes() + end + end + ) + + this.MovementMode.IndexChanged:connect( + function(newIndex) + setMovementModeToIndex(newIndex) + end + ) + end + + ------------------------------------------------------ + ------------------ + ------------------------- Connection Setup ----------- + function setShiftLockVisible(visible) + if this.ShiftLockMode then + this.ShiftLockMode.SelectorFrame.Visible = visible + this.ShiftLockMode:SetInteractable(visible) + end + end + + do -- initial set of dev vs user choice for guis + local isUserChoiceCamera = false + if UserInputService.TouchEnabled then + isUserChoiceCamera = LocalPlayer.DevTouchCameraMode == Enum.DevTouchCameraMovementMode.UserChoice + else + isUserChoiceCamera = LocalPlayer.DevComputerCameraMode == Enum.DevComputerCameraMovementMode.UserChoice + end + + if not isUserChoiceCamera then + this.CameraModeOverrideText.Visible = true + setCameraModeVisible(false) + else + this.CameraModeOverrideText.Visible = false + setCameraModeVisible(true) + end + + local isUserChoiceMovement = false + if UserInputService.TouchEnabled then + isUserChoiceMovement = LocalPlayer.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice + else + isUserChoiceMovement = LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.UserChoice + end + + if this.MovementModeOverrideText then + if not isUserChoiceMovement then + this.MovementModeOverrideText.Visible = true + setMovementModeVisible(false) + else + this.MovementModeOverrideText.Visible = false + setMovementModeVisible(true) + end + end + + if this.ShiftLockOverrideText then + this.ShiftLockOverrideText.Visible = not LocalPlayer.DevEnableMouseLock + setShiftLockVisible(LocalPlayer.DevEnableMouseLock) + end + end + + local function updateUserSettingsMenu(property) + if this.ShiftLockOverrideText and property == "DevEnableMouseLock" then + this.ShiftLockOverrideText.Visible = not LocalPlayer.DevEnableMouseLock + setShiftLockVisible(LocalPlayer.DevEnableMouseLock) + elseif property == "DevComputerCameraMode" then + local isUserChoice = LocalPlayer.DevComputerCameraMode == Enum.DevComputerCameraMovementMode.UserChoice + setCameraModeVisible(isUserChoice) + this.CameraModeOverrideText.Visible = not isUserChoice + elseif property == "DevComputerMovementMode" then + -- TOUCH + local isUserChoice = LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.UserChoice + setMovementModeVisible(isUserChoice) + if this.MovementModeOverrideText then + this.MovementModeOverrideText.Visible = not isUserChoice + end + elseif property == "DevTouchMovementMode" then + local isUserChoice = LocalPlayer.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice + setMovementModeVisible(isUserChoice) + if this.MovementModeOverrideText then + this.MovementModeOverrideText.Visible = not isUserChoice + end + elseif property == "DevTouchCameraMode" then + local isUserChoice = LocalPlayer.DevTouchCameraMode == Enum.DevTouchCameraMovementMode.UserChoice + setCameraModeVisible(isUserChoice) + this.CameraModeOverrideText.Visible = not isUserChoice + end + end + + LocalPlayer.Changed:connect( + function(property) + if UserInputService.TouchEnabled then + if TOUCH_CHANGED_PROPS[property] then + updateUserSettingsMenu(property) + end + end + if UserInputService.KeyboardEnabled then + if PC_CHANGED_PROPS[property] then + updateUserSettingsMenu(property) + end + end + end + ) + end + + local function createVolumeOptions() + local startVolumeLevel = math.floor(GameSettings.MasterVolume * 10) + this.VolumeFrame, this.VolumeLabel, this.VolumeSlider = + utility:AddNewRow(this, "Volume", "Slider", 10, startVolumeLevel) + this.VolumeFrame.LayoutOrder = 5 + + local volumeSound = Instance.new("Sound", game:GetService("CoreGui").RobloxGui.Sounds) + volumeSound.Name = "VolumeChangeSound" + volumeSound.SoundId = "rbxasset://sounds/uuhhh.mp3" + + this.VolumeSlider.ValueChanged:connect( + function(newValue) + local soundPercent = newValue / 10 + volumeSound.Volume = soundPercent + volumeSound:Play() + GameSettings.MasterVolume = soundPercent + end + ) + end + + local function createCameraInvertedOptions() + local initialIndex = 1 + local success = + pcall( + function() + if GameSettings.CameraYInverted == true then + initialIndex = 2 + end + end + ) + + if success == false then + return + end + + this.CameraInvertedFrame, _, this.CameraInvertedSelector = + utility:AddNewRow(this, "Camera Inverted", "Selector", {"Off", "On"}, initialIndex) + this.CameraInvertedFrame.LayoutOrder = 11 + settingsDisabledInVR[this.CameraInvertedFrame] = true + + this.CameraInvertedSelector.IndexChanged:connect( + function(newIndex) + if newIndex == 2 then + GameSettings.CameraYInverted = true + else + GameSettings.CameraYInverted = false + end + end + ) + end + + -- TODO: remove "advancedEnabled" when clean up FFlagAdvancedMouseSensitivityEnabled + local function setCameraSensitivity(newValue, advancedEnabled) + if UserInputService.GamepadEnabled and GameSettings.IsUsingGamepadCameraSensitivity then + GameSettings.GamepadCameraSensitivity = newValue + end + if UserInputService.MouseEnabled then + if not advancedEnabled then + GameSettings.MouseSensitivity = newValue + else + local newVectorValue = Vector2.new(newValue, newValue) + GameSettings.MouseSensitivityFirstPerson = newVectorValue + GameSettings.MouseSensitivityThirdPerson = newVectorValue + end + end + end + + local function createMouseOptions() + local MouseSteps = 10 + local MinMouseSensitivity = 0.2 + local AdvancedSuccess, AdvancedValue = + pcall( + function() + return settings():GetFFlag("AdvancedMouseSensitivityEnabled") + end + ) + local AdvancedEnabled = AdvancedSuccess and AdvancedValue + + -- equations below map a function to include points (0, 0.2) (5, 1) (10, 4) + -- where x is the slider position, y is the mouse sensitivity + local function translateEngineMouseSensitivityToGui(engineSensitivity) + -- 0 <= y <= 1: x = (y - 0.2) / 0.16 + -- 1 <= y <= 4: x = (y + 2) / 0.6 + local guiSensitivity = + (engineSensitivity <= 1) and math.floor((engineSensitivity - 0.2) / 0.16 + 0.5) or + math.floor((engineSensitivity + 2) / 0.6 + 0.5) + return (engineSensitivity <= MinMouseSensitivity) and 0 or guiSensitivity + end + + local function translateGuiMouseSensitivityToEngine(guiSensitivity) + -- 0 <= x <= 5: y = 0.16 * x + 0.2 + -- 5 <= x <= 10: y = 0.6 * x - 2 + local engineSensitivity = (guiSensitivity <= 5) and (0.16 * guiSensitivity + 0.2) or (0.6 * guiSensitivity - 2) + return (engineSensitivity <= MinMouseSensitivity) and MinMouseSensitivity or engineSensitivity + end + + local startMouseLevel = translateEngineMouseSensitivityToGui(GameSettings.MouseSensitivity) + + if not AdvancedEnabled then + ------------------ Basic Mouse Sensitivity Slider ------------------ + -- basic quantized sensitivity with a weird number of settings. + local SliderLabel = "Camera Sensitivity" + + this.MouseSensitivityFrame, this.MouseSensitivityLabel, this.MouseSensitivitySlider = + utility:AddNewRow(this, SliderLabel, "Slider", MouseSteps, startMouseLevel) + this.MouseSensitivityFrame.LayoutOrder = 4 + + this.MouseSensitivitySlider.ValueChanged:connect( + function(newValue) + setCameraSensitivity(translateGuiMouseSensitivityToEngine(newValue)) + end + ) + else + ------------------ 3D Sensitivity ------------------ + -- affects both first and third person. + local AdvancedMouseSteps = 10 + local textBoxWidth = 60 + local canSetSensitivity = true + local MouseAdvancedStart = tostring(GameSettings.MouseSensitivityFirstPerson.X) + + this.MouseAdvancedFrame, this.MouseAdvancedLabel, this.MouseAdvancedEntry = + utility:AddNewRow(this, "Camera Sensitivity", "Slider", AdvancedMouseSteps, startMouseLevel) + this.MouseAdvancedFrame.LayoutOrder = 4 + settingsDisabledInVR[this.MouseAdvancedFrame] = true + + this.MouseAdvancedEntry.SliderFrame.Size = + UDim2.new( + this.MouseAdvancedEntry.SliderFrame.Size.X.Scale, + this.MouseAdvancedEntry.SliderFrame.Size.X.Offset - textBoxWidth, + this.MouseAdvancedEntry.SliderFrame.Size.Y.Scale, + this.MouseAdvancedEntry.SliderFrame.Size.Y.Offset - 6 + ) + this.MouseAdvancedEntry.SliderFrame.Position = + UDim2.new( + this.MouseAdvancedEntry.SliderFrame.Position.X.Scale, + this.MouseAdvancedEntry.SliderFrame.Position.X.Offset - textBoxWidth, + this.MouseAdvancedEntry.SliderFrame.Position.Y.Scale, + this.MouseAdvancedEntry.SliderFrame.Position.Y.Offset + ) + this.MouseAdvancedLabel.ZIndex = 2 + this.MouseAdvancedEntry:SetInteractable(true) + + local textBox = + utility:Create "TextBox" { + Name = "CameraSensitivityTextBox", + TextColor3 = Color3.new(1, 1, 1), + BorderColor3 = Color3.new(0.8, 0.8, 0.8), + BackgroundColor3 = Color3.new(0.2, 0.2, 0.2), + Font = Enum.Font.SourceSans, + TextSize = 18, + Size = UDim2.new(0, textBoxWidth, 0.8, 0), + Position = UDim2.new(1, -2, 0.5, 0), + AnchorPoint = Vector2.new(0, 0.5), + ZIndex = 3, + Selectable = false, + Parent = this.MouseAdvancedEntry.SliderFrame + } + + local maxTextBoxStringLength = 7 + local function setTextboxText(newText) + if string.len(newText) > maxTextBoxStringLength then + newText = string.sub(newText, 1, maxTextBoxStringLength) + end + textBox.Text = newText + end + + setTextboxText(tostring(GameSettings.MouseSensitivityFirstPerson.X)) + this.MouseAdvancedEntry:SetValue(translateEngineMouseSensitivityToGui(GameSettings.MouseSensitivityFirstPerson.X)) + + function clampMouseSensitivity(value) + if value < 0.0 then + value = -value + end + + -- * assume a minimum that allows a 16000 dpi mouse a full 800mm travel for 360deg + -- ~0.0029: min of 0.001 seems ok. + -- * assume a max that allows a 400 dpi mouse a 360deg travel in 10mm + -- ~9.2: max of 10 seems ok, but users will want to have a bit of fun with crazy settings. + if value > 100.0 then + value = 100.0 + elseif value < 0.001 then + value = 0.001 + end + + return value + end + + function setMouseSensitivity(newValue, widgetOrigin) + if not canSetSensitivity then + return + end + + setCameraSensitivity(newValue, true) + + canSetSensitivity = false + do + if widgetOrigin ~= this.MouseAdvancedEntry then + this.MouseAdvancedEntry:SetValue(translateEngineMouseSensitivityToGui(newValue)) + end + + setTextboxText(tostring(newValue)) + end + canSetSensitivity = true + end + + textBox.FocusLost:connect( + function() + this.MouseAdvancedEntry:SetInteractable(true) + + local num = tonumber(string.match(textBox.Text, "([%d%.]+)")) + + if num then + setMouseSensitivity(clampMouseSensitivity(num), textBox) + else + setMouseSensitivity(GameSettings.MouseSensitivityFirstPerson.X, textBox) + end + end + ) + + textBox.Focused:connect( + function() + this.MouseAdvancedEntry:SetInteractable(false) + end + ) + + this.MouseAdvancedEntry.ValueChanged:connect( + function(newValue) + newValue = clampMouseSensitivity(newValue) + newValue = translateGuiMouseSensitivityToEngine(newValue) + setMouseSensitivity(newValue, this.MouseAdvancedEntry) + end + ) + end + end + + local function createGamepadOptions() + local GamepadSteps = 10 + local MinGamepadCameraSensitivity = 0.2 + -- equations below map a function to include points (0, 0.2) (5, 1) (10, 4) + -- where x is the slider position, y is the mouse sensitivity + local function translateEngineGamepadSensitivityToGui(engineSensitivity) + -- 0 <= y <= 1: x = (y - 0.2) / 0.16 + -- 1 <= y <= 4: x = (y + 2) / 0.6 + local guiSensitivity = + (engineSensitivity <= 1) and math.floor((engineSensitivity - 0.2) / 0.16 + 0.5) or + math.floor((engineSensitivity + 2) / 0.6 + 0.5) + return (engineSensitivity <= MinGamepadCameraSensitivity) and 0 or guiSensitivity + end + local function translateGuiGamepadSensitivityToEngine(guiSensitivity) + -- 0 <= x <= 5: y = 0.16 * x + 0.2 + -- 5 <= x <= 10: y = 0.6 * x - 2 + local engineSensitivity = (guiSensitivity <= 5) and (0.16 * guiSensitivity + 0.2) or (0.6 * guiSensitivity - 2) + return (engineSensitivity <= MinGamepadCameraSensitivity) and MinGamepadCameraSensitivity or engineSensitivity + end + local startGamepadLevel = translateEngineGamepadSensitivityToGui(GameSettings.GamepadCameraSensitivity) + ------------------ Basic Gamepad Sensitivity Slider ------------------ + -- basic quantized sensitivity with a weird number of settings. + local SliderLabel = "Camera Sensitivity" + this.GamepadSensitivityFrame, this.GamepadSensitivityLabel, this.GamepadSensitivitySlider = + utility:AddNewRow(this, SliderLabel, "Slider", GamepadSteps, startGamepadLevel) + this.GamepadSensitivityFrame.LayoutOrder = 4 + this.GamepadSensitivitySlider.ValueChanged:connect( + function(newValue) + setCameraSensitivity(translateGuiGamepadSensitivityToEngine(newValue)) + end + ) + end + + local function createOverscanOption() + local showOverscanScreen = function() + local MenuModule = require(RobloxGui.Modules.Settings.SettingsHub) + local overscan = require(RobloxGui.Modules.Shell.Components.Overscan.Overscan) + local roact = require(RobloxGui.Modules.Common.Roact) + local overscanComponent = nil + + local props = {} + props.onUnmount = function() + if overscanComponent then + roact.teardown(overscanComponent) + -- show settings menu and give back movement + ContextActionService:UnbindCoreAction("RbxStopOverscanMovement") + MenuModule:SetVisibility(true, true) + end + end + props.ImageVisible = false + props.BackgroundTransparency = 0.2 + + -- hide settings menu + MenuModule:SetVisibility(false, true) + + -- override all bindings for movement + local noOpFunc = function() + end + ContextActionService:BindCoreAction( + "RbxStopOverscanMovement", + noOpFunc, + false, + Enum.UserInputType.Gamepad1, + Enum.UserInputType.Gamepad2, + Enum.UserInputType.Gamepad3, + Enum.UserInputType.Gamepad4 + ) + + local overscanElement = roact.createElement(overscan, props) + overscanComponent = roact.reify(overscanElement, RobloxGui, tostring(overscan)) + end + + local adjustButton, adjustText, setButtonRowRef = + utility:MakeStyledButton("AdjustButton", "Adjust", UDim2.new(0, 300, 1, -20), showOverscanScreen, this) + adjustText.Font = Enum.Font.SourceSans + adjustButton.Position = UDim2.new(1, -400, 0, 12) + + if RunService:IsStudio() then + adjustButton.Selectable = false + adjustButton.Active = false + adjustButton.Enabled.Value = false + adjustText.TextColor3 = Color3.fromRGB(100, 100, 100) + end + + local row = utility:AddNewRowObject(this, "Safe Zone", adjustButton) + setButtonRowRef(row) + end + + local function createDeveloperConsoleOption() + -- makes button in settings menu to open dev console + local function makeDevConsoleOption() + local function onOpenDevConsole() + if FFlagEnableNewDevConsole then + local devConsoleMaster = require(script.Parent.Parent.Parent.DevConsoleMaster) + if devConsoleMaster then + devConsoleMaster:SetVisibility(true) + local MenuModule = require(script.Parent.Parent.SettingsHub) + if MenuModule then + MenuModule:SetVisibility(false) + end + end + else + local devConsoleModule = require(RobloxGui.Modules.DeveloperConsoleModule) + if devConsoleModule then + devConsoleModule:SetVisibility(true) + local MenuModule = require(RobloxGui.Modules.Settings.SettingsHub) + if MenuModule then + MenuModule:SetVisibility(false) + end + end + end + end + + local devConsoleButton, devConsoleText, setButtonRowRef = + utility:MakeStyledButton("DevConsoleButton", "Open", UDim2.new(0, 300, 1, -20), onOpenDevConsole, this) + devConsoleText.Font = Enum.Font.SourceSans + devConsoleButton.Position = UDim2.new(1, -400, 0, 12) + local row = utility:AddNewRowObject(this, "Developer Console", devConsoleButton) + row.LayoutOrder = 13 + setButtonRowRef(row) + end + + -- Only show option if we are place/group owner + if game.CreatorType == Enum.CreatorType.Group then + spawn( + function() + -- spawn since GetRankInGroup is async + local success, result = + pcall( + function() + return LocalPlayer:GetRankInGroup(game.CreatorId) == 255 + end + ) + if success then + if result == true then + makeDevConsoleOption() + end + else + print("DeveloperConsoleModule: GetRankInGroup failed because", result) + end + end + ) + elseif LocalPlayer.UserId == game.CreatorId and game.CreatorType == Enum.CreatorType.User then + makeDevConsoleOption() + end + end + + createCameraModeOptions( + not isTenFootInterface and + (UserInputService.TouchEnabled or UserInputService.MouseEnabled or UserInputService.KeyboardEnabled) + ) + + local checkGamepadOptions = function() + if GameSettings.IsUsingGamepadCameraSensitivity then + createGamepadOptions() + else + local camerasettingsConn = nil + camerasettingsConn = + GameSettings:GetPropertyChangedSignal("IsUsingGamepadCameraSensitivity"):connect( + function() + if GameSettings.IsUsingGamepadCameraSensitivity then + if camerasettingsConn then + camerasettingsConn:disconnect() + end + createGamepadOptions() + end + end + ) + end + end + + if UserInputService.MouseEnabled then + createMouseOptions() + else + if UserInputService.GamepadEnabled then + checkGamepadOptions() + else + local gamepadConnectedConn = nil + gamepadConnectedConn = + UserInputService.GamepadConnected:connect( + function() + if gamepadConnectedConn then + gamepadConnectedConn:disconnect() + end + checkGamepadOptions() + end + ) + end + end + + if GameSettings.IsUsingCameraYInverted then + createCameraInvertedOptions() + else + local gamesettingsConn = nil + gamesettingsConn = + GameSettings.Changed:connect( + function(prop) + if prop == "IsUsingCameraYInverted" then + if GameSettings.IsUsingCameraYInverted then + gamesettingsConn:disconnect() + createCameraInvertedOptions() + end + end + end + ) + end + + createVolumeOptions() + + -- we disable quality slider on Xbox since it has FRM disabled and forced to max quality level so the slider is useless + if platform ~= Enum.Platform.XBoxOne then + createGraphicsOptions() + end + + createPerformanceStatsOptions() + + -- create micro profiler option in the end, so the ip and port can be shown next to the row + if UseMicroProfiler then + createMicroProfilerOptions() + end + + if isTenFootInterface then + createOverscanOption() + end + + -- dev console option only shows for place/group place owners + createDeveloperConsoleOption() + + allSettingsCreated = true + if VRService.VREnabled then + onVRSettingsReady() + end + + ------ TAB CUSTOMIZATION ------- + this.TabHeader.Name = "GameSettingsTab" + this.TabHeader.Icon.Image = + isTenFootInterface and "rbxasset://textures/ui/Settings/MenuBarIcons/GameSettingsTab@2x.png" or + "rbxasset://textures/ui/Settings/MenuBarIcons/GameSettingsTab.png" + + if FFlagUseNotificationsLocalization then + this.TabHeader.Title.Text = "Settings" + else + this.TabHeader.Icon.Title.Text = "Settings" + end + + ------ PAGE CUSTOMIZATION ------- + this.Page.ZIndex = 5 + + if this.PageListLayout then + this.PageListLayout.Padding = UDim.new(0, 0) + end + + return this +end + +----------- Page Instantiation -------------- + +PageInstance = Initialize() + +return PageInstance diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/Help.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/Help.lua new file mode 100644 index 0000000..a2772b4 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/Help.lua @@ -0,0 +1,654 @@ +--[[ + Filename: Help.lua + Written by: jeditkacheff + Version 1.0 + Description: Takes care of the help page in Settings Menu +--]] + +------------ FFLAGS ------------------- +local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end) +local FFlagUseNotificationsLocalization = success and result + +-------------- CONSTANTS -------------- +local KEYBOARD_MOUSE_TAG = "KeyboardMouse" +local TOUCH_TAG = "Touch" +local GAMEPAD_TAG = "Gamepad" +local PC_TABLE_SPACING = 4 +local XBOX_CONTROLLER_IMAGE_OFFSET = 30 +local TEXT_EDGE_DISTANCE = 20 + +-------------- SERVICES -------------- +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local UserInputService = game:GetService("UserInputService") +local GuiService = game:GetService("GuiService") +local TextService = game:GetService("TextService") +local Settings = UserSettings() +local GameSettings = Settings.GameSettings + +----------- UTILITIES -------------- +local utility = require(RobloxGui.Modules.Settings.Utility) + +------------ Variables ------------------- +local PageInstance = nil +RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface") +local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled() + +----------- CLASS DECLARATION -------------- + +local function Initialize() + local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory) + local this = settingsPageFactory:CreateNewPage() + this.HelpPages = {} + this.HelpPageContents = {} + this.ActiveHelpScheme = nil + + local lastInputType = nil + + function this:GetCurrentInputType() + if lastInputType == nil then -- we don't know what controls the user has, just use reasonable defaults + local platform = UserInputService:GetPlatform() + if platform == Enum.Platform.XBoxOne or platform == Enum.Platform.WiiU then + + return GAMEPAD_TAG + elseif platform == Enum.Platform.Windows or platform == Enum.Platform.OSX then + return KEYBOARD_MOUSE_TAG + else + return TOUCH_TAG + end + end + + if lastInputType == Enum.UserInputType.Keyboard or lastInputType == Enum.UserInputType.MouseMovement or + lastInputType == Enum.UserInputType.MouseButton1 or lastInputType == Enum.UserInputType.MouseButton2 or + lastInputType == Enum.UserInputType.MouseButton3 or lastInputType == Enum.UserInputType.MouseWheel then + return KEYBOARD_MOUSE_TAG + elseif lastInputType == Enum.UserInputType.Touch then + return TOUCH_TAG + elseif lastInputType == Enum.UserInputType.Gamepad1 or lastInputType == Enum.UserInputType.Gamepad2 or + lastInputType == Enum.UserInputType.Gamepad3 or lastInputType == Enum.UserInputType.Gamepad4 then + return GAMEPAD_TAG + end + + return KEYBOARD_MOUSE_TAG + end + + + local function createPCHelp(parentFrame) + local function createPCGroup(title, actionInputBindings) + local textIndent = 9 + + local pcGroupFrame = utility:Create'Frame' + { + Size = UDim2.new(1/3,-PC_TABLE_SPACING,1,0), + BackgroundTransparency = 1, + Name = "PCGroupFrame" .. tostring(title) + }; + local pcGroupTitle = utility:Create'TextLabel' + { + Position = UDim2.new(0,textIndent,0,0), + Size = UDim2.new(1,-textIndent,0,30), + BackgroundTransparency = 1, + Text = title, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size18, + TextColor3 = Color3.new(1,1,1), + TextXAlignment = Enum.TextXAlignment.Left, + Name = "PCGroupTitle" .. tostring(title), + ZIndex = 2, + Parent = pcGroupFrame + }; + + local count = 0 + local frameHeight = 42 + local spacing = 2 + local offset = pcGroupTitle.Size.Y.Offset + for i = 1, #actionInputBindings do + for actionName, inputName in pairs(actionInputBindings[i]) do + local actionInputFrame = utility:Create'Frame' + { + Size = UDim2.new(1,0,0,frameHeight), + Position = UDim2.new(0,0,0, offset + ((frameHeight + spacing) * count)), + BackgroundTransparency = 0.65, + BorderSizePixel = 0, + ZIndex = 2, + Name = "ActionInputBinding" .. tostring(actionName), + Parent = pcGroupFrame + }; + + local nameLabel = utility:Create'TextLabel' + { + Size = UDim2.new(0.4,-textIndent,0,frameHeight), + Position = UDim2.new(0,textIndent,0,0), + BackgroundTransparency = 1, + Text = actionName, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size18, + TextColor3 = Color3.new(1,1,1), + TextXAlignment = Enum.TextXAlignment.Left, + Name = actionName .. "Label", + ZIndex = 2, + Parent = actionInputFrame, + TextWrapped = true, + TextScaled = true + }; + do + local textSizeConstraint = Instance.new("UITextSizeConstraint",nameLabel) + textSizeConstraint.MaxTextSize = 18 + end + + + local inputLabel = utility:Create'TextLabel' + { + Size = UDim2.new(0.5,0,0,frameHeight), + Position = UDim2.new(0.5,-4,0,0), + BackgroundTransparency = 1, + Text = inputName, + Font = Enum.Font.SourceSans, + FontSize = Enum.FontSize.Size18, + TextColor3 = Color3.new(1,1,1), + TextXAlignment = Enum.TextXAlignment.Left, + Name = inputName .. "Label", + ZIndex = 2, + Parent = actionInputFrame, + TextWrapped = true, + TextScaled = true + }; + do + local textSizeConstraint = Instance.new("UITextSizeConstraint",inputLabel) + textSizeConstraint.MaxTextSize = 18 + end + + count = count + 1 + end + end + + pcGroupFrame.Size = UDim2.new(pcGroupFrame.Size.X.Scale,pcGroupFrame.Size.X.Offset, + 0, offset + ((frameHeight + spacing) * count)) + + return pcGroupFrame + end + + local rowOffset = 50 + local isOSX = UserInputService:GetPlatform() == Enum.Platform.OSX + + local charMoveFrame = createPCGroup( "Character Movement", {[1] = {["Move Forward"] = "W/Up Arrow"}, + [2] = {["Move Backward"] = "S/Down Arrow"}, + [3] = {["Move Left"] = "A/Left Arrow"}, + [4] = {["Move Right"] = "D/Right Arrow"}, + [5] = {["Jump"] = "Space"}} ) + charMoveFrame.Parent = parentFrame + + local accessoriesFrame = createPCGroup("Accessories", { + [1] = {["Equip Tools"] = "1,2,3..."}, + [2] = {["Unequip Tools"] = "1,2,3..."}, + [3] = {["Drop Tool"] = "Backspace"}, + [4] = {["Use Tool"] = "Left Mouse Button"} }) + accessoriesFrame.Position = UDim2.new(1/3,PC_TABLE_SPACING,0,0) + accessoriesFrame.Parent = parentFrame + + local miscFrame = nil + miscFrame = createPCGroup("Misc", { + [1] = {["Screenshot"] = "Print Screen"}, + [2] = {["Record Video"] = isOSX and "F12/fn + F12" or "F12"}, + [3] = {["Dev Console"] = isOSX and "F9/fn + F9" or "F9"}, + [4] = {["Mouselock"] = "Shift"}, + [5] = {["Graphics Level"] = isOSX and "F10/fn + F10" or "F10"}, + [6] = {["Fullscreen"] = isOSX and "F11/fn + F11" or "F11"}, + [7] = {["Perf. Stats"] = isOSX and "Fn+Opt+Cmd+F7" or "Ctrl + Shift + F7"}, + } + ) + + miscFrame.Position = UDim2.new(2/3,PC_TABLE_SPACING * 2,0,0) + miscFrame.Parent = parentFrame + + local camFrame = createPCGroup("Camera Movement", { [1] = {["Rotate"] = "Right Mouse Button"}, + [2] = {["Zoom In/Out"] = "Mouse Wheel"}, + [3] = {["Zoom In"] = "I"}, + [4] = {["Zoom Out"] = "O"} }) + camFrame.Position = UDim2.new(0,0,charMoveFrame.Size.Y.Scale,charMoveFrame.Size.Y.Offset + rowOffset) + camFrame.Parent = parentFrame + + local menuFrame = createPCGroup("Menu Items", { [1] = {["Roblox Menu"] = "ESC"}, + [2] = {["Backpack"] = "~"}, + [3] = {["Playerlist"] = "TAB"}, + [4] = {["Chat"] = "/"} }) + menuFrame.Position = UDim2.new(1/3,PC_TABLE_SPACING,charMoveFrame.Size.Y.Scale,charMoveFrame.Size.Y.Offset + rowOffset) + menuFrame.Parent = parentFrame + + parentFrame.Size = UDim2.new(parentFrame.Size.X.Scale, parentFrame.Size.X.Offset, 0, + menuFrame.Size.Y.Offset + menuFrame.Position.Y.Offset) + end + + local function createGamepadHelp(parentFrame) + local gamepadImage = nil + local imageSize = nil + + gamepadImage = "rbxasset://textures/ui/Settings/Help/GenericController.png" + imageSize = UDim2.new(0, 473, 0, 287) + + local imagePosition = UDim2.new(0.5, -imageSize.X.Offset/2, 0.5, -imageSize.Y.Offset/2) + + if UserInputService:GetPlatform() == Enum.Platform.XBoxOne or UserInputService:GetPlatform() == Enum.Platform.XBox360 then + gamepadImage = "rbxasset://textures/ui/Settings/Help/XboxController.png" + imageSize = UDim2.new(0, 745, 0, 452) + imagePosition = UDim2.new(0.5, (-imageSize.X.Offset/2) + XBOX_CONTROLLER_IMAGE_OFFSET, 0.5, -imageSize.Y.Offset/2 + 7) + elseif UserInputService:GetPlatform() == Enum.Platform.PS4 or UserInputService:GetPlatform() == Enum.Platform.PS3 then + gamepadImage = "rbxasset://textures/ui/Settings/Help/PSController.png" + end + + local gamepadImageLabel = utility:Create'ImageLabel' + { + Name = "GamepadImage", + Size = imageSize, + Position = imagePosition, + Image = gamepadImage, + BackgroundTransparency = 1, + ZIndex = 2, + Parent = parentFrame + }; + parentFrame.Size = UDim2.new(parentFrame.Size.X.Scale, parentFrame.Size.X.Offset, 0, gamepadImageLabel.Size.Y.Offset + 100) + + local gamepadFontSize = isTenFootInterface and Enum.FontSize.Size36 or Enum.FontSize.Size24 + local textVerticalSize = (gamepadFontSize == Enum.FontSize.Size36) and 36 or 24 + local function createGamepadLabel(text, position, size, rightAligned) + local nameLabel = nil + if FFlagUseNotificationsLocalization == true then + nameLabel = utility:Create'TextLabel'{ + Position = position, + Size = size, + BackgroundTransparency = 1, + Text = text, + TextXAlignment = rightAligned and Enum.TextXAlignment.Right or Enum.TextXAlignment.Left, + AnchorPoint = rightAligned and Vector2.new(1, 0.5) or Vector2.new(0, 0.5), + Font = Enum.Font.SourceSansBold, + FontSize = gamepadFontSize, + TextColor3 = Color3.new(1,1,1), + Name = text .. "Label", + ZIndex = 2, + Parent = gamepadImageLabel, + TextScaled = true, + TextWrapped = true + }; + else + nameLabel = utility:Create'TextLabel'{ + Position = position, + Size = size, + BackgroundTransparency = 1, + Text = text, + TextXAlignment = rightAligned and Enum.TextXAlignment.Right or Enum.TextXAlignment.Left, + AnchorPoint = rightAligned and Vector2.new(1, 0.5) or Vector2.new(0, 0.5), + Font = Enum.Font.SourceSansBold, + FontSize = gamepadFontSize, + TextColor3 = Color3.new(1,1,1), + Name = text .. "Label", + ZIndex = 2, + Parent = gamepadImageLabel + }; + end + + nameLabel.TextWrapped = true + + local textSize = TextService:GetTextSize(text, textVerticalSize, Enum.Font.SourceSansBold, Vector2.new(0, 0)) + local minSizeXOffset = textSize.X + local distanceToCenter = math.abs(position.X.Offset) + local parentGui = (gamepadImage == "rbxasset://textures/ui/Settings/Help/XboxController.png") and RobloxGui or parentFrame + + local function updateNameLabelSize() + local nameLabelSizeXOffset = nameLabel.Size.X.Offset + if gamepadImage == "rbxasset://textures/ui/Settings/Help/XboxController.png" then + nameLabelSizeXOffset = rightAligned and + RobloxGui.AbsoluteSize.X/2 + XBOX_CONTROLLER_IMAGE_OFFSET - distanceToCenter - TEXT_EDGE_DISTANCE or + RobloxGui.AbsoluteSize.X/2 - XBOX_CONTROLLER_IMAGE_OFFSET - distanceToCenter - TEXT_EDGE_DISTANCE + else + nameLabelSizeXOffset = parentFrame.AbsoluteSize.X/2 - distanceToCenter + end + + if nameLabelSizeXOffset < minSizeXOffset then + nameLabel.Size = UDim2.new(nameLabel.Size.X.Scale, nameLabelSizeXOffset, nameLabel.Size.Y.Scale, textVerticalSize * 2) + nameLabel.TextScaled = true + else + nameLabel.Size = UDim2.new(nameLabel.Size.X.Scale, nameLabelSizeXOffset, nameLabel.Size.Y.Scale, textVerticalSize) + nameLabel.FontSize = gamepadFontSize + nameLabel.TextScaled = false + end + end + + local nameLabelChangeCn = parentGui:GetPropertyChangedSignal('AbsoluteSize'):connect(function() + updateNameLabelSize() + end) + + updateNameLabelSize() + end + + if gamepadImage == "rbxasset://textures/ui/Settings/Help/XboxController.png" then + createGamepadLabel("Switch Tool", UDim2.new(0.5, -390, 0, 0), UDim2.new(0, 100, 0, textVerticalSize), true) + createGamepadLabel("Game Menu Toggle", UDim2.new(0.5, -390, 0.15, 0), UDim2.new(0, 164, 0, textVerticalSize), true) + createGamepadLabel("Move", UDim2.new(0.5, -390, 0.31, 0), UDim2.new(0, 46, 0, textVerticalSize), true) + createGamepadLabel("Menu Navigation", UDim2.new(0.5, -390, 0.46, 0), UDim2.new(0, 164, 0, textVerticalSize), true) + + createGamepadLabel("Use Tool", UDim2.new(0.5, 330, 0, 0), UDim2.new(0, 73, 0, textVerticalSize)) + createGamepadLabel("Roblox Menu", UDim2.new(0.5, 330, 0.15, 0), UDim2.new(0, 122, 0, textVerticalSize)) + createGamepadLabel("Back", UDim2.new(0.5, 330, 0.31, 0), UDim2.new(0, 43, 0, textVerticalSize)) + createGamepadLabel("Jump", UDim2.new(0.5, 330, 0.46, 0), UDim2.new(0, 49, 0, textVerticalSize)) + createGamepadLabel("Rotate Camera", UDim2.new(0.5, 380, 0.62, 0), UDim2.new(0, 132, 0, textVerticalSize)) + createGamepadLabel("Camera Zoom", UDim2.new(0.5, 380, 0.77, 0), UDim2.new(0, 122, 0, textVerticalSize)) + else + createGamepadLabel("Switch Tool", UDim2.new(0.5, -250, 0, 0), UDim2.new(0, 100, 0, textVerticalSize), true) + createGamepadLabel("Game Menu Toggle", UDim2.new(0.5, -250, 0.15, 0), UDim2.new(0, 164, 0, textVerticalSize), true) + createGamepadLabel("Move", UDim2.new(0.5, -250, 0.31, 0), UDim2.new(0, 46, 0, textVerticalSize), true) + createGamepadLabel("Menu Navigation", UDim2.new(0.5, -250, 0.46, 0), UDim2.new(0, 143, 0, textVerticalSize), true) + + createGamepadLabel("Use Tool", UDim2.new(0.5, 215, 0, 0), UDim2.new(0, 73, 0, textVerticalSize)) + createGamepadLabel("Roblox Menu", UDim2.new(0.5, 215, 0.15, 0), UDim2.new(0, 122, 0, textVerticalSize)) + createGamepadLabel("Back", UDim2.new(0.5, 215, 0.31, 0), UDim2.new(0, 43, 0, textVerticalSize)) + createGamepadLabel("Jump", UDim2.new(0.5, 215, 0.46, 0), UDim2.new(0, 49, 0, textVerticalSize)) + createGamepadLabel("Rotate Camera", UDim2.new(0.5, 255, 0.62, 0), UDim2.new(0, 132, 0, textVerticalSize)) + createGamepadLabel("Camera Zoom", UDim2.new(0.5, 255, 0.77, 0), UDim2.new(0, 122, 0, textVerticalSize)) + end + -- NOTE: On consoles we put the dev console in the settings menu. Only place + -- owners can see this for now. + end + + local function updateTouchLayout(scheme) -- adjust layout to work well with the various touch control schemes + this.ActiveHelpScheme = scheme + local isPortrait = utility:IsPortrait() + local helpFrame = this.HelpPages[TOUCH_TAG] + + if helpFrame then + local helpElements = this.HelpPageContents[TOUCH_TAG] + + local function hideUneeded(list) + if list then + for name, item in pairs(helpElements) do + item.Visible = not list[name] + end + end + end + + local hidden + if scheme == Enum.TouchMovementMode.DynamicThumbstick then + -- show that movement is done by dragging + -- show that tapping on bottom of the screen is to jump + -- show that tapping on the top of the screen is to use tools + -- show that dragging on the top of the screen is to pan camera + hidden = {MoveImageCTM = true} + helpElements["MoveLabel"].Position = isPortrait and UDim2.new(0.25,-helpElements["MoveLabel"].AbsoluteSize.x/2,0.75,-50) or UDim2.new(0.15,-helpElements["MoveLabel"].AbsoluteSize.x/2,0.85,-helpElements["MoveLabel"].AbsoluteSize.y) + helpElements["JumpLabel"].Position = isPortrait and UDim2.new(0.75,-helpElements["JumpLabel"].AbsoluteSize.x/2,0.75,-50) or UDim2.new(0.85,-60,0.85,-helpElements["JumpLabel"].AbsoluteSize.y) + helpElements["RotateLabel"].Position = isPortrait and UDim2.new(1,-helpElements["RotateLabel"].AbsoluteSize.x-20,0.02,0) or UDim2.new(0.85,-helpElements["RotateLabel"].AbsoluteSize.x/2,0.02,0) + helpElements["UseToolLabel"].Position = isPortrait and UDim2.new(0.5,-helpElements["UseToolLabel"].AbsoluteSize.x/2,0.5,-100) or UDim2.new(0.5,-helpElements["UseToolLabel"].AbsoluteSize.x/2,0.5,-60) + + helpElements["EquipLabel"].Position = isPortrait and UDim2.new(0.5,-60,0.75,50) or UDim2.new(0.5,-60,0.64,0) + helpElements["ZoomLabel"].Position = isPortrait and UDim2.new(0,20,0.02,0) or UDim2.new(0.15,-60,0.02,0) + + elseif scheme == Enum.TouchMovementMode.ClickToMove then + -- show that dragging on the screen is to pan the camera + -- show that tapping is to move + hidden = {BottomHalfDisplay = true, MoveImageDTS = true, JumpLabel = true} -- cant manually jump on ctm + helpElements["MoveLabel"].Position = isPortrait and UDim2.new(0.25,-helpElements["MoveLabel"].AbsoluteSize.x/2,0.5,0) or UDim2.new(0.25,-helpElements["MoveLabel"].AbsoluteSize.x/2,0.5,40) + helpElements["RotateLabel"].Position = isPortrait and UDim2.new(1,-helpElements["RotateLabel"].AbsoluteSize.x-20,0.02,0) or UDim2.new(0.5,-60,0.02,0) + helpElements["UseToolLabel"].Position = isPortrait and UDim2.new(0.75,-helpElements["UseToolLabel"].AbsoluteSize.x/2,0.5,0) or UDim2.new(0.85,-60,0.02,0) + + helpElements["EquipLabel"].Position = isPortrait and UDim2.new(0.5,-60,0.75,50) or UDim2.new(0.5,-60,0.64,0) + helpElements["ZoomLabel"].Position = isPortrait and UDim2.new(0,20,0.02,0) or UDim2.new(0.15,-60,0.02,0) + else + -- keep the default style, but take portrait mode into account + hidden = {BottomHalfDisplay = true, MoveImageDTS = true, MoveImageCTM = true, JumpImage = true} -- if theres a jump button we don't need to do touch gestures, same with thumbstick + helpElements["MoveLabel"].Position = isPortrait and UDim2.new(0.06,0,1,-120) or UDim2.new(0.06,0,0.58,0) + helpElements["JumpLabel"].Position = isPortrait and UDim2.new(0.94,-helpElements["JumpLabel"].AbsoluteSize.x,1,-120) or UDim2.new(0.8,0,0.58,0) + helpElements["RotateLabel"].Position = isPortrait and UDim2.new(1,-helpElements["RotateLabel"].AbsoluteSize.x-20,0.02,0) or UDim2.new(0.5,-60,0.02,0) + helpElements["UseToolLabel"].Position = isPortrait and UDim2.new(.5,-helpElements["UseToolLabel"].AbsoluteSize.x/2,0.25,50) or UDim2.new(0.85,-60,0.02,0) + + helpElements["EquipLabel"].Position = isPortrait and UDim2.new(0.5,-60,0.75,50) or UDim2.new(0.5,-60,0.64,0) + helpElements["ZoomLabel"].Position = isPortrait and UDim2.new(0,20,0.02,0) or UDim2.new(0.15,-60,0.02,0) + end + hideUneeded(hidden) + end + end + + local function createTouchHelp(parentFrame) + local createdElements = {} -- dictionary of buttons created + local smallScreen = utility:IsSmallTouchScreen() + local ySize = GuiService:GetScreenResolution().y - 350 + if smallScreen then + ySize = GuiService:GetScreenResolution().y - 100 + end + parentFrame.Size = UDim2.new(1,0,0,ySize) + + local function createDisplayFrame(name, position, size, transparency, color, parent) + local frame = utility:Create'Frame' + { + BackgroundColor3 = color, + Position = position, + Size = size, + BackgroundTransparency = transparency, + Name = name, + ZIndex = 1, + Parent = parent + }; + return frame + end + + local function createTouchLabel(text, position, size, parent) + local nameLabel = utility:Create'TextLabel' + { + Position = position, + Size = size, + BackgroundTransparency = 1, + Text = text, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size14, + TextColor3 = Color3.new(1,1,1), + Name = text .. "Label", + ZIndex = 3, + Parent = parent, + TextScaled = true, + TextWrapped = true + }; + + if not smallScreen then + nameLabel.FontSize = Enum.FontSize.Size18 + nameLabel.Size = UDim2.new(nameLabel.Size.X.Scale, nameLabel.Size.X.Offset, nameLabel.Size.Y.Scale, nameLabel.Size.Y.Offset + 4) + end + + local nameBackgroundImage = utility:Create'ImageLabel' + { + Name = text .. "BackgroundImage", + Size = UDim2.new(1.25,0,1.25,0), + Position = UDim2.new(-0.125,0,-0.065,0), + BackgroundTransparency = 1, + Image = "rbxasset://textures/ui/Settings/Radial/RadialLabel.png", + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(12,2,65,21), + ZIndex = 2, + Parent = nameLabel + }; + + local textSizeConstraint = Instance.new("UITextSizeConstraint",nameLabel) + textSizeConstraint.MaxTextSize = 18 + + return nameLabel + end + + local function createTouchGestureImage(name, image, position, size, parent) + local gestureImage = utility:Create'ImageLabel' + { + Name = name, + Size = size, + Position = position, + BackgroundTransparency = 1, + Image = image, + ZIndex = 2, + Parent = parent + }; + + return gestureImage + end + + local xSizeOffset = 30 + local ySize = 25 + if smallScreen then xSizeOffset = 0 end + + createdElements["BottomHalfDisplay"] = createDisplayFrame("BottomHalfFrame", UDim2.new(0,0,0.5,-16), UDim2.new(1,0,1,0), 0.35, Color3.new(0,0,0), parentFrame) + + -- movement stuff + createdElements["MoveLabel"] = createTouchLabel("Move", UDim2.new(0.06,0,0.58,0), UDim2.new(0,77 + xSizeOffset,0,ySize), parentFrame) + createdElements["MoveImageDTS"] = createTouchGestureImage("MoveImageDTS", "rbxasset://textures/ui/Settings/Help/RotateCameraGesture.png", UDim2.new(0.5,-32,1,3), UDim2.new(0,65,0,48), createdElements["MoveLabel"]) + createdElements["MoveImageCTM"] = createTouchGestureImage("MoveImageCTM", "rbxasset://textures/ui/Settings/Help/UseToolGesture.png", UDim2.new(0.5,-19,1,3), UDim2.new(0,38,0,52), createdElements["MoveLabel"]) + + -- jumping stuff + createdElements["JumpLabel"] = createTouchLabel("Jump", UDim2.new(0.8,0,0.58,0), UDim2.new(0,77 + xSizeOffset,0,ySize), parentFrame) + createdElements["JumpImage"] = createTouchGestureImage("JumpImage", "rbxasset://textures/ui/Settings/Help/UseToolGesture.png", UDim2.new(0.5,-19,1,3), UDim2.new(0,38,0,52), createdElements["JumpLabel"]) + + createdElements["EquipLabel"] = createTouchLabel("Equip/Unequip Tools", UDim2.new(0.5,-60,0.64,0), UDim2.new(0,120 + xSizeOffset,0,ySize), parentFrame) + + createdElements["ZoomLabel"] = createTouchLabel("Zoom In/Out", UDim2.new(0.15,-60,0.02,0), UDim2.new(0,120,0,ySize), parentFrame) + createdElements["ZoomImage"] = createTouchGestureImage("ZoomImage", "rbxasset://textures/ui/Settings/Help/ZoomGesture.png", UDim2.new(0.5,-26,1,3), UDim2.new(0,53,0,59), createdElements["ZoomLabel"]) + createdElements["RotateLabel"] = createTouchLabel("Rotate Camera", UDim2.new(0.5,-60,0.02,0), UDim2.new(0,120,0,ySize), parentFrame) + createdElements["RotateImage"] = createTouchGestureImage("RotateImage", "rbxasset://textures/ui/Settings/Help/RotateCameraGesture.png", UDim2.new(0.5,-32,1,3), UDim2.new(0,65,0,48), createdElements["RotateLabel"]) + createdElements["UseToolLabel"] = createTouchLabel("Use Tool", UDim2.new(0.85,-60,0.02,0), UDim2.new(0,120,0,ySize), parentFrame) + createdElements["ToolImage"] = createTouchGestureImage("ToolImage", "rbxasset://textures/ui/Settings/Help/UseToolGesture.png", UDim2.new(0.5,-19,1,3), UDim2.new(0,38,0,52), createdElements["UseToolLabel"]) + + return createdElements + end + + local function createHelpDisplay(typeOfHelp) + local helpContents = nil + local helpFrame = utility:Create'Frame' + { + Size = UDim2.new(1,0,1,0), + BackgroundTransparency = 1, + Name = "HelpFrame" .. tostring(typeOfHelp) + }; + + + if typeOfHelp == KEYBOARD_MOUSE_TAG then + createPCHelp(helpFrame) + elseif typeOfHelp == GAMEPAD_TAG then + createGamepadHelp(helpFrame) + elseif typeOfHelp == TOUCH_TAG then + helpContents = createTouchHelp(helpFrame) + end + + return helpFrame, helpContents + end + + local function displayHelp(currentPage) + for i, helpPage in pairs(this.HelpPages) do + if helpPage == currentPage then + helpPage.Parent = this.Page + this.Page.Size = helpPage.Size + if isTenFootInterface then + this.Page.Size = UDim2.new(helpPage.Size.X.Scale, helpPage.Size.X.Offset, helpPage.Size.Y.Scale, 0) + end + else + helpPage.Parent = nil + end + end + if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then + this.HubRef.PageViewClipper.ClipsDescendants = false + this.HubRef.PageView.ClipsDescendants = false + end + end + + local function switchToHelp(typeOfHelp) + local helpPage = this.HelpPages[typeOfHelp] + if helpPage then + displayHelp(helpPage) + if typeOfHelp == TOUCH_TAG then + -- update for the control scheme + local scheme = GameSettings.TouchMovementMode + if this.ActiveHelpScheme ~= scheme then + updateTouchLayout(scheme) + end + end + else + this.HelpPages[typeOfHelp], this.HelpPageContents[typeOfHelp] = createHelpDisplay(typeOfHelp) + switchToHelp(typeOfHelp) + end + end + + local function showTypeOfHelp() + switchToHelp(this:GetCurrentInputType()) + end + + local function adjustForScreenLayout(givenSize) -- portrait mode was causing the help frame to be either too tall or short when changed between landscape mode and portrait. + if this:GetCurrentInputType() == TOUCH_TAG then + local scheme = GameSettings.TouchMovementMode + local smallScreen = utility:IsSmallTouchScreen() + local size = givenSize or GuiService:GetScreenResolution() + local ySize = size.y - 350 + if smallScreen then + ySize = size.y - 100 + end + this.HelpPages[TOUCH_TAG].Size = UDim2.new(1,0,0,ySize) + updateTouchLayout(scheme) + end + end + + ------ TAB CUSTOMIZATION ------- + this.TabHeader.Name = "HelpTab" + + this.TabHeader.Icon.Image = "rbxasset://textures/ui/Settings/MenuBarIcons/HelpTab.png" + + if FFlagUseNotificationsLocalization then + this.TabHeader.Title.Text = "Help" + else + this.TabHeader.Icon.Title.Text = "Help" + end + ------ PAGE CUSTOMIZATION ------- + this.Page.Name = "Help" + + UserInputService.InputBegan:connect(function(inputObject) + local inputType = inputObject.UserInputType + if inputType ~= Enum.UserInputType.Focus and inputType ~= Enum.UserInputType.None then + lastInputType = inputObject.UserInputType + showTypeOfHelp() + end + end) + + utility:OnResized(this, function(newSize, isPortrait) + if this.HelpPages[TOUCH_TAG] then + adjustForScreenLayout(newSize) + end + end) + + return this +end + + +----------- Public Facing API Additions -------------- +do + PageInstance = Initialize() + + PageInstance.Displayed.Event:connect(function() + local isPortrait = utility:IsPortrait() + if PageInstance:GetCurrentInputType() == TOUCH_TAG then + if PageInstance.HubRef.BottomButtonFrame and not utility:IsSmallTouchScreen() and not isPortrait then + PageInstance.HubRef.BottomButtonFrame.Visible = false + end + end + if PageInstance.HubRef.VersionContainer then + PageInstance.HubRef.VersionContainer.Visible = true + end + end) + + PageInstance.Hidden.Event:connect(function() + PageInstance.HubRef.PageViewClipper.ClipsDescendants = true + PageInstance.HubRef.PageView.ClipsDescendants = true + + PageInstance.HubRef:ShowShield() + + local isPortrait = utility:IsPortrait() + if PageInstance:GetCurrentInputType() == TOUCH_TAG then + if PageInstance.HubRef.BottomButtonFrame and not utility:IsSmallTouchScreen() and not isPortrait then + PageInstance.HubRef.BottomButtonFrame.Visible = true + end + end + if PageInstance.HubRef.VersionContainer then + PageInstance.HubRef.VersionContainer.Visible = false + end + end) +end + + +return PageInstance \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/Home.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/Home.lua new file mode 100644 index 0000000..88d93c5 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/Home.lua @@ -0,0 +1,90 @@ +--[[ + Filename: Home.lua + Written by: jeditkacheff + Version 1.0 + Description: Takes care of the home page in Settings Menu +--]] + +local BUTTON_OFFSET = 20 +local BUTTON_SPACING = 10 + +-------------- SERVICES -------------- +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local GuiService = game:GetService("GuiService") + +----------- UTILITIES -------------- +local utility = require(RobloxGui.Modules.Settings.Utility) + +------------ Variables ------------------- +local PageInstance = nil + +local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end) +local FFlagUseNotificationsLocalization = success and result + +----------- CLASS DECLARATION -------------- + +local function Initialize() + local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory) + local this = settingsPageFactory:CreateNewPage() + + ------ TAB CUSTOMIZATION ------- + this.TabHeader.Name = "HomeTab" + + this.TabHeader.Icon.Image = "rbxasset://textures/ui/Settings/MenuBarIcons/HomeTab.png" + this.TabHeader.Icon.Size = UDim2.new(0,32,0,30) + this.TabHeader.Icon.Position = UDim2.new(0,5,0.5,-15) + + if FFlagUseNotificationsLocalization then + this.TabHeader.Title.Text = "Home" + else + this.TabHeader.Icon.Title.Text = "Home" + end + + this.TabHeader.Size = UDim2.new(0,100,1,0) + + ------ PAGE CUSTOMIZATION ------- + this.Page.Name = "Home" + local resumeGameFunc = function() + this.HubRef:SetVisibility(false) + end + + this.ResumeButton = utility:MakeStyledButton("ResumeButton", "Resume Game", UDim2.new(0, 200, 0, 50), resumeGameFunc) + this.ResumeButton.Position = UDim2.new(0.5,-100,0,BUTTON_OFFSET) + this.ResumeButton.Parent = this.Page + + local resetFunc = function() + this.HubRef:SwitchToPage(this.HubRef.ResetCharacterPage, false, 1) + end + + local resetButton = utility:MakeStyledButton("ResetButton", "Reset Character", UDim2.new(0, 200, 0, 50), resetFunc) + resetButton.Position = UDim2.new(0.5,-100,0,this.ResumeButton.AbsolutePosition.Y + this.ResumeButton.AbsoluteSize.Y + BUTTON_SPACING) + resetButton.Parent = this.Page + + local leaveGameFunc = function() + this.HubRef:SwitchToPage(this.HubRef.LeaveGamePage, false, 1) + end + + local leaveButton = utility:MakeStyledButton("LeaveButton", "Leave Game", UDim2.new(0, 200, 0, 50), leaveGameFunc) + leaveButton.Position = UDim2.new(0.5,-100,0,resetButton.AbsolutePosition.Y + resetButton.AbsoluteSize.Y + BUTTON_SPACING) + leaveButton.Parent = this.Page + + this.Page.Size = UDim2.new(1,0,0,leaveButton.AbsolutePosition.Y + leaveButton.AbsoluteSize.Y) + + return this +end + + +----------- Public Facing API Additions -------------- +do + PageInstance = Initialize() + + PageInstance.Displayed.Event:connect(function() + if not utility:UsesSelectedObject() then return end + + GuiService.SelectedCoreObject = PageInstance.ResumeButton + end) +end + + +return PageInstance \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/LeaveGame.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/LeaveGame.lua new file mode 100644 index 0000000..54a06fe --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/LeaveGame.lua @@ -0,0 +1,142 @@ +--[[ + Filename: LeaveGame.lua + Written by: jeditkacheff + Version 1.0 + Description: Takes care of the leave game in Settings Menu +--]] + + +-------------- CONSTANTS ------------- +local LEAVE_GAME_ACTION = "LeaveGameCancelAction" +local LEAVE_GAME_FRAME_WAITS = 2 + +-------------- SERVICES -------------- +local CoreGui = game:GetService("CoreGui") +local ContextActionService = game:GetService("ContextActionService") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local GuiService = game:GetService("GuiService") +local RunService = game:GetService("RunService") + +----------- UTILITIES -------------- +local utility = require(RobloxGui.Modules.Settings.Utility) + +------------ Variables ------------------- +local PageInstance = nil +RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface") +local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled() + +----------- CLASS DECLARATION -------------- + +local function Initialize() + local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory) + local this = settingsPageFactory:CreateNewPage() + + this.LeaveFunc = function() + GuiService.SelectedCoreObject = nil -- deselects the button and prevents spamming the popup to save in studio when using gamepad + + -- need to wait for render frames so on slower devices the leave button highlight will update + -- otherwise, since on slow devices it takes so long to leave you are left wondering if you pressed the button + for i = 1, LEAVE_GAME_FRAME_WAITS do + RunService.RenderStepped:wait() + end + + game:Shutdown() + end + this.DontLeaveFunc = function(isUsingGamepad) + if this.HubRef then + this.HubRef:PopMenu(isUsingGamepad, true) + end + end + this.DontLeaveFromHotkey = function(name, state, input) + if state == Enum.UserInputState.Begin then + local isUsingGamepad = input.UserInputType == Enum.UserInputType.Gamepad1 or input.UserInputType == Enum.UserInputType.Gamepad2 + or input.UserInputType == Enum.UserInputType.Gamepad3 or input.UserInputType == Enum.UserInputType.Gamepad4 + + this.DontLeaveFunc(isUsingGamepad) + end + end + this.DontLeaveFromButton = function(isUsingGamepad) + this.DontLeaveFunc(isUsingGamepad) + end + + ------ TAB CUSTOMIZATION ------- + this.TabHeader = nil -- no tab for this page + + ------ PAGE CUSTOMIZATION ------- + this.Page.Name = "LeaveGamePage" + this.ShouldShowBottomBar = false + this.ShouldShowHubBar = false + + local leaveGameText = utility:Create'TextLabel' + { + Name = "LeaveGameText", + Text = "Are you sure you want to leave the game?", + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size36, + TextColor3 = Color3.new(1,1,1), + BackgroundTransparency = 1, + Size = UDim2.new(1,0,0,200), + TextWrapped = true, + ZIndex = 2, + Parent = this.Page, + Position = isTenFootInterface and UDim2.new(0,0,0,100) or UDim2.new(0,0,0,0) + }; + + local leaveButtonContainer = utility:Create"Frame" + { + Name = "LeaveButtonContainer", + Parent = leaveGameText, + Size = UDim2.new(1,0,0,400), + BackgroundTransparency = 1, + Position = UDim2.new(0,0,1,0) + }; + + local leaveButtonLayout = utility:Create'UIGridLayout' + { + Name = "LeavetButtonsLayout", + CellSize = isTenFootInterface and UDim2.new(0, 300, 0, 80) or UDim2.new(0, 200, 0, 50), + CellPadding = UDim2.new(0,20,0,20), + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Top, + Parent = leaveButtonContainer + }; + + if utility:IsSmallTouchScreen() then + leaveGameText.FontSize = Enum.FontSize.Size24 + leaveGameText.Size = UDim2.new(1,0,0,100) + elseif isTenFootInterface then + leaveGameText.FontSize = Enum.FontSize.Size48 + end + + this.LeaveGameButton = utility:MakeStyledButton("LeaveGame", "Leave", nil, this.LeaveFunc) + this.LeaveGameButton.NextSelectionRight = nil + this.LeaveGameButton.Parent = leaveButtonContainer + + ------------- Init ---------------------------------- + + local dontleaveGameButton = utility:MakeStyledButton("DontLeaveGame", "Don't Leave", nil, this.DontLeaveFromButton) + dontleaveGameButton.NextSelectionLeft = nil + dontleaveGameButton.Parent = leaveButtonContainer + + this.Page.Size = UDim2.new(1,0,0,dontleaveGameButton.AbsolutePosition.Y + dontleaveGameButton.AbsoluteSize.Y) + + return this +end + + +----------- Public Facing API Additions -------------- +PageInstance = Initialize() + +PageInstance.Displayed.Event:connect(function() + GuiService.SelectedCoreObject = PageInstance.LeaveGameButton + ContextActionService:BindCoreAction(LEAVE_GAME_ACTION, PageInstance.DontLeaveFromHotkey, false, Enum.KeyCode.ButtonB) +end) + +PageInstance.Hidden.Event:connect(function() + ContextActionService:UnbindCoreAction(LEAVE_GAME_ACTION) +end) + + +return PageInstance \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/Players.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/Players.lua new file mode 100644 index 0000000..ba676d1 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/Players.lua @@ -0,0 +1,792 @@ + --[[ + Filename: Players.lua + Written by: Stickmasterluke + Version 1.0 + Description: Player list inside escape menu, with friend adding functionality. +--]] +-------------- SERVICES -------------- +local CoreGui = game:GetService("CoreGui") +local CorePackages = game:GetService("CorePackages") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local GuiService = game:GetService("GuiService") +local PlayersService = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local AnalyticsService = game:GetService("AnalyticsService") +local RunService = game:GetService("RunService") + +----------- UTILITIES -------------- +RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface") +local utility = require(RobloxGui.Modules.Settings.Utility) +local reportAbuseMenu = require(RobloxGui.Modules.Settings.Pages.ReportAbuseMenu) +local SocialUtil = require(RobloxGui.Modules:WaitForChild("SocialUtil")) +local EventStream = require(CorePackages.AppTempCommon.Temp.EventStream) +local ShareGameIcons = require(CoreGui.RobloxGui.Modules.Settings.Pages.ShareGame.Spritesheets.ShareGameIcons) +local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled() + +local preventFriendingRemovedPlayers = settings():GetFFlag("PreventFriendingRemovedPlayers2") +local fixPlayerRowLayout = settings():GetFFlag("FixPlayerRowLayout") + +------------ Constants ------------------- +local FRAME_DEFAULT_TRANSPARENCY = .85 +local FRAME_SELECTED_TRANSPARENCY = .65 +local REPORT_PLAYER_IMAGE = isTenFootInterface and "rbxasset://textures/ui/Settings/Players/ReportFlagIcon@2x.png" or "rbxasset://textures/ui/Settings/Players/ReportFlagIcon.png" +local ADD_FRIEND_IMAGE = isTenFootInterface and "rbxasset://textures/ui/Settings/Players/AddFriendIcon@2x.png" or "rbxasset://textures/ui/Settings/Players/AddFriendIcon.png" +local FRIEND_IMAGE = isTenFootInterface and "rbxasset://textures/ui/Settings/Players/FriendIcon@2x.png" or "rbxasset://textures/ui/Settings/Players/FriendIcon.png" + +local PLAYER_ROW_HEIGHT = 62 +local PLAYER_ROW_SPACING = 80 + +local INVITE_FRIENDS_TEXT = "Invite friends to play" + +------------ Variables ------------------- +local platform = UserInputService:GetPlatform() +local PageInstance = nil +local localPlayer = PlayersService.LocalPlayer +while not localPlayer do + PlayersService.ChildAdded:wait() + localPlayer = PlayersService.LocalPlayer +end + + +------------ FAST FLAGS ------------------- +local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end) +local FFlagUseNotificationsLocalization = success and result + +local FFlagSettingsHubInviteToGame2 = settings():GetFFlag('SettingsHubInviteToGame2') +local FFlagSettingsHubInviteToGameInStudio2 = settings():GetFFlag('SettingsHubInviteToGameInStudio2') +local FFlagSettingsHubBarsRefactor2 = settings():GetFFlag('SettingsHubBarsRefactor2') + +----------- CLASS DECLARATION -------------- +local function Initialize() + local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory) + local this = settingsPageFactory:CreateNewPage() + + this.PageListLayout.Padding = UDim.new(0, PLAYER_ROW_SPACING - PLAYER_ROW_HEIGHT) + + ------ TAB CUSTOMIZATION ------- + this.TabHeader.Name = "PlayersTab" + this.TabHeader.Icon.Image = isTenFootInterface and "rbxasset://textures/ui/Settings/MenuBarIcons/PlayersTabIcon@2x.png" or "rbxasset://textures/ui/Settings/MenuBarIcons/PlayersTabIcon.png" + + if FFlagUseNotificationsLocalization then + this.TabHeader.Title.Text = "Players" + else + this.TabHeader.Icon.Title.Text = "Players" + end + + ----- FRIENDSHIP FUNCTIONS ------ + local function getFriendStatus(selectedPlayer) + local success, result = pcall(function() + -- NOTE: Core script only + return localPlayer:GetFriendStatus(selectedPlayer) + end) + if success then + return result + else + return Enum.FriendStatus.NotFriend + end + end + + ------ PAGE CUSTOMIZATION ------- + this.Page.Name = "Players" + + local function showRightSideButtons(player) + return player and player ~= localPlayer and player.UserId > 1 and localPlayer.UserId > 1 + end + + local function createFriendStatusTextLabel(status, player) + if status == nil then + return nil + end + + local fakeSelection = Instance.new("Frame") + fakeSelection.BackgroundTransparency = 1 + + local friendLabel = nil + local friendLabelText = nil + if status == Enum.FriendStatus.Friend or status == Enum.FriendStatus.FriendRequestSent then + friendLabel = Instance.new("TextButton") + friendLabel.BackgroundTransparency = 1 + friendLabel.FontSize = Enum.FontSize.Size24 + friendLabel.Font = Enum.Font.SourceSans + friendLabel.TextColor3 = Color3.new(1,1,1) + friendLabel.SelectionImageObject = fakeSelection + if status == Enum.FriendStatus.Friend then + friendLabel.Text = "Friend" + else + friendLabel.Text = "Request Sent" + end + elseif status == Enum.FriendStatus.Unknown or status == Enum.FriendStatus.NotFriend or status == Enum.FriendStatus.FriendRequestReceived then + local addFriendFunc = function() + if friendLabel and friendLabelText and friendLabelText.Text ~= "" then + friendLabel.ImageTransparency = 1 + friendLabelText.Text = "" + if localPlayer and player then + AnalyticsService:ReportCounter("PlayersMenu-RequestFriendship") + AnalyticsService:TrackEvent("Game", "RequestFriendship", "PlayersMenu") + + localPlayer:RequestFriendship(player) + end + end + end + friendLabel, friendLabelText = utility:MakeStyledButton("FriendStatus", "Add Friend", UDim2.new(0, 182, 0, 46), addFriendFunc) + friendLabelText.ZIndex = 3 + friendLabelText.Position = friendLabelText.Position + UDim2.new(0,0,0,1) + end + + if friendLabel then + friendLabel.Name = "FriendStatus" + friendLabel.Size = UDim2.new(0,182,0,46) + friendLabel.Position = UDim2.new(1,-198,0,7) + friendLabel.ZIndex = 3 + end + return friendLabel + end + + local function createFriendStatusImageLabel(status, player) + if status == Enum.FriendStatus.Friend or status == Enum.FriendStatus.FriendRequestSent then + local friendLabel = Instance.new("ImageButton") + friendLabel.Name = "FriendStatus" + friendLabel.Size = UDim2.new(0, 46, 0, 46) + friendLabel.Image = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButton.png" + friendLabel.ScaleType = Enum.ScaleType.Slice + friendLabel.SliceCenter = Rect.new(8,6,46,44) + friendLabel.AutoButtonColor = false + friendLabel.BackgroundTransparency = 1 + friendLabel.ZIndex = 2 + local friendImage = Instance.new("ImageLabel") + friendImage.BackgroundTransparency = 1 + friendImage.Position = UDim2.new(0.5, 0, 0.5, 0) + friendImage.Size = UDim2.new(0, 28, 0, 28) + friendImage.AnchorPoint = Vector2.new(0.5, 0.5) + friendImage.ZIndex = 3 + friendImage.Image = FRIEND_IMAGE + if status == Enum.FriendStatus.Friend then + friendImage.ImageTransparency = 0 + else + friendImage.Image = ADD_FRIEND_IMAGE + friendImage.ImageTransparency = 0.5 + end + friendImage.Parent = friendLabel + return friendLabel + elseif status == Enum.FriendStatus.Unknown or status == Enum.FriendStatus.NotFriend or status == Enum.FriendStatus.FriendRequestReceived then + local addFriendButton, addFriendImage = nil + local addFriendFunc = function() + if addFriendButton and addFriendImage and addFriendButton.ImageTransparency ~= 1 then + addFriendButton.ImageTransparency = 1 + addFriendImage.ImageTransparency = 1 + if localPlayer and player then + AnalyticsService:ReportCounter("PlayersMenu-RequestFriendship") + AnalyticsService:TrackEvent("Game", "RequestFriendship", "PlayersMenu") + + localPlayer:RequestFriendship(player) + end + end + end + addFriendButton, addFriendImage = utility:MakeStyledImageButton("FriendStatus", ADD_FRIEND_IMAGE, + UDim2.new(0, 46, 0, 46), UDim2.new(0, 28, 0, 28), addFriendFunc) + addFriendButton.Name = "FriendStatus" + addFriendButton.Selectable = true + return addFriendButton + end + return nil + end + + --- Ideally we want to select the first add friend button, but select the first report button instead if none are available. + local reportSelectionFound = nil + local friendSelectionFound = nil + local function friendStatusCreate(playerLabel, player) + local friendLabelParent = nil + if playerLabel then + friendLabelParent = playerLabel:FindFirstChild("RightSideButtons") + end + + if friendLabelParent then + -- remove any previous friend status labels + for _, item in pairs(friendLabelParent:GetChildren()) do + if item and item.Name == "FriendStatus" then + if GuiService.SelectedCoreObject == item then + friendSelectionFound = nil + GuiService.SelectedCoreObject = nil + end + item:Destroy() + end + end + + -- create new friend status label + local status = nil + if showRightSideButtons(player) then + status = getFriendStatus(player) + end + + local friendLabel = nil + local wasIsPortrait = nil + utility:OnResized(playerLabel, function(newSize, isPortrait) + if friendLabel and isPortrait == wasIsPortrait then + return + end + wasIsPortrait = isPortrait + if friendLabel then + friendLabel:Destroy() + end + if isPortrait then + friendLabel = createFriendStatusImageLabel(status, player) + else + friendLabel = createFriendStatusTextLabel(status, player) + end + + if friendLabel then + friendLabel.Name = "FriendStatus" + friendLabel.LayoutOrder = 2 + friendLabel.Selectable = true + friendLabel.Parent = friendLabelParent + + if UserInputService.GamepadEnabled and not friendSelectionFound then + friendSelectionFound = true + GuiService.SelectedCoreObject = friendLabel + end + end + end) + end + end + + localPlayer.FriendStatusChanged:connect(function(player, friendStatus) + if player then + local playerLabel = this.Page:FindFirstChild("PlayerLabel"..player.Name) + if playerLabel then + friendStatusCreate(playerLabel, player) + end + end + end) + + local buttonsContainer = utility:Create("Frame") { + Name = "ButtonsContainer", + Size = UDim2.new(1, 0, 0, 62), + BackgroundTransparency = 1, + Parent = this.Page, + + Visible = false + } + + local leaveGameFunc = function() + this.HubRef:SwitchToPage(this.HubRef.LeaveGamePage, false, 1) + end + local leaveButton, leaveLabel = utility:MakeStyledButton("LeaveButton", "Leave Game", UDim2.new(1 / 3, -5, 1, 0), leaveGameFunc) + leaveButton.AnchorPoint = Vector2.new(0, 0) + leaveButton.Position = UDim2.new(0, 0, 0, 0) + leaveLabel.Size = UDim2.new(1, 0, 1, -6) + leaveButton.Parent = buttonsContainer + + local resetFunc = function() + this.HubRef:SwitchToPage(this.HubRef.ResetCharacterPage, false, 1) + end + local resetButton, resetLabel = utility:MakeStyledButton("ResetButton", "Reset Character", UDim2.new(1 / 3, -5, 1, 0), resetFunc) + resetButton.AnchorPoint = Vector2.new(0.5, 0) + resetButton.Position = UDim2.new(0.5, 0, 0, 0) + resetLabel.Size = UDim2.new(1, 0, 1, -6) + resetButton.Parent = buttonsContainer + + local resumeGameFunc = function() + this.HubRef:SetVisibility(false) + end + local resumeButton, resumeLabel = utility:MakeStyledButton("ResumeButton", "Resume Game", UDim2.new(1 / 3, -5, 1, 0), resumeGameFunc) + resumeButton.AnchorPoint = Vector2.new(1, 0) + resumeButton.Position = UDim2.new(1, 0, 0, 0) + resumeLabel.Size = UDim2.new(1, 0, 1, -6) + resumeButton.Parent = buttonsContainer + + utility:OnResized(buttonsContainer, function(newSize, isPortrait) + if isPortrait or utility:IsSmallTouchScreen() then + local buttonsFontSize = isPortrait and 18 or 24 + buttonsContainer.Visible = true + buttonsContainer.Size = UDim2.new(1, 0, 0, isPortrait and 50 or 62) + resetLabel.TextSize = buttonsFontSize + leaveLabel.TextSize = buttonsFontSize + resumeLabel.TextSize = buttonsFontSize + else + buttonsContainer.Visible = false + buttonsContainer.Size = UDim2.new(1, 0, 0, 0) + end + end) + + + if FFlagUseNotificationsLocalization then + local function ApplyLocalizeTextSettingsToLabel(label) + label.AnchorPoint = Vector2.new(0.5,0.5) + label.Position = UDim2.new(0.5, 0, 0.5, -3) + label.Size = UDim2.new(0.75, 0, 0.5, 0) + end + ApplyLocalizeTextSettingsToLabel(leaveLabel) + ApplyLocalizeTextSettingsToLabel(resetLabel) + ApplyLocalizeTextSettingsToLabel(resetLabel) + end + + local function reportAbuseButtonCreate(playerLabel, player) + local rightSideButtons = playerLabel:FindFirstChild("RightSideButtons") + if rightSideButtons then + local oldReportButton = rightSideButtons:FindFirstChild("ReportPlayer") + if oldReportButton then + if oldReportButton == GuiService.SelectedCoreObject then + reportSelectionFound = nil + end + oldReportButton:Destroy() + end + + if showRightSideButtons(player) then + local reportPlayerFunction = function() + reportAbuseMenu:ReportPlayer(player) + end + + local reportButton = utility:MakeStyledImageButton("ReportPlayer", REPORT_PLAYER_IMAGE, + UDim2.new(0, 46, 0, 46), UDim2.new(0, 28, 0, 28), reportPlayerFunction) + reportButton.Name = "ReportPlayer" + reportButton.Position = UDim2.new(1, -260, 0, 7) + reportButton.LayoutOrder = 1 + reportButton.Selectable = true + reportButton.Parent = rightSideButtons + + if not reportSelectionFound and not friendSelectionFound and UserInputService.GamepadEnabled then + reportSelectionFound = true + GuiService.SelectedCoreObject = reportButton + end + end + end + end + + local createShareGameButton = nil + local createPlayerRow = nil + if FFlagSettingsHubBarsRefactor2 then + local function createRow(frameClassName) + local frame = Instance.new(frameClassName) + frame.Image = "rbxasset://textures/ui/dialog_white.png" + frame.ScaleType = "Slice" + frame.SliceCenter = Rect.new(10, 10, 10, 10) + frame.Size = UDim2.new(1, 0, 0, PLAYER_ROW_HEIGHT) + frame.Position = UDim2.new(0, 0, 0, 0) + frame.BackgroundTransparency = 1 + frame.ZIndex = 2 + frame.ImageTransparency = FRAME_DEFAULT_TRANSPARENCY + + local icon = Instance.new("ImageLabel") + icon.Name = "Icon" + icon.BackgroundTransparency = 1 + icon.Size = UDim2.new(0, 36, 0, 36) + icon.Position = UDim2.new(0, 12, 0, 12) + icon.ZIndex = 3 + icon.Parent = frame + + local textLabel = Instance.new("TextLabel") + textLabel.TextXAlignment = Enum.TextXAlignment.Left + textLabel.Font = Enum.Font.SourceSans + textLabel.FontSize = Enum.FontSize.Size24 + textLabel.TextColor3 = Color3.new(1, 1, 1) + textLabel.BackgroundTransparency = 1 + textLabel.Position = UDim2.new(0, 60, .5, 0) + textLabel.Size = UDim2.new(0, 0, 0, 0) + textLabel.ZIndex = 3 + textLabel.Parent = frame + + return frame + end + + createShareGameButton = function() + local frame = createRow("ImageButton") + local textLabel = frame.TextLabel + local icon = frame.Icon + + textLabel.Font = Enum.Font.SourceSansSemibold + textLabel.Text = INVITE_FRIENDS_TEXT + + icon.Size = UDim2.new(0, 24, 0, 24) + icon.Position = UDim2.new(0, 18, 0, 18) + ShareGameIcons:ApplyImage(icon, "invite") + + return frame + end + + createPlayerRow = function() + local frame = createRow("ImageLabel") + frame.TextLabel.Name = "NameLabel" + + local rightSideButtons = Instance.new("Frame") + rightSideButtons.Name = "RightSideButtons" + rightSideButtons.BackgroundTransparency = 1 + rightSideButtons.ZIndex = 2 + rightSideButtons.Position = UDim2.new(0, 0, 0, 0) + rightSideButtons.Size = UDim2.new(1, -10, 1, 0) + rightSideButtons.Parent = frame + + -- Selection Highlighting logic: + local updateHighlight = function(lostSelectionObject) + if frame then + if GuiService.SelectedCoreObject and GuiService.SelectedCoreObject ~= lostSelectionObject and GuiService.SelectedCoreObject.Parent == rightSideButtons then + frame.ImageTransparency = FRAME_SELECTED_TRANSPARENCY + else + frame.ImageTransparency = FRAME_DEFAULT_TRANSPARENCY + end + end + end + + local fakeSelectionObject = nil + rightSideButtons.ChildAdded:connect(function(child) + if child:IsA("GuiObject") then + if fakeSelectionObject and child ~= fakeSelectionObject then + fakeSelectionObject:Destroy() + fakeSelectionObject = nil + end + child.SelectionGained:connect(function() updateHighlight(nil) end) + child.SelectionLost:connect(function() updateHighlight(child) end) + end + end) + + fakeSelectionObject = Instance.new("Frame") + fakeSelectionObject.Selectable = true + fakeSelectionObject.Size = UDim2.new(0, 100, 0, 100) + fakeSelectionObject.BackgroundTransparency = 1 + fakeSelectionObject.SelectionImageObject = fakeSelectionObject:Clone() + fakeSelectionObject.Parent = rightSideButtons + + local rightSideListLayout = Instance.new("UIListLayout") + rightSideListLayout.Name = "RightSideListLayout" + rightSideListLayout.FillDirection = Enum.FillDirection.Horizontal + rightSideListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Right + rightSideListLayout.VerticalAlignment = Enum.VerticalAlignment.Center + rightSideListLayout.SortOrder = Enum.SortOrder.LayoutOrder + rightSideListLayout.Padding = UDim.new(0, 20) + rightSideListLayout.Parent = rightSideButtons + + pcall(function() + frame.NameLabel.Localize = false + end) + + return frame + end + else + createPlayerRow = function(yPosition) + local frame = Instance.new("ImageLabel") + frame.Image = "rbxasset://textures/ui/dialog_white.png" + frame.ScaleType = "Slice" + frame.SliceCenter = Rect.new(10, 10, 10, 10) + frame.Size = UDim2.new(1, 0, 0, PLAYER_ROW_HEIGHT) + frame.Position = UDim2.new(0, 0, 0, yPosition) + frame.BackgroundTransparency = 1 + frame.ZIndex = 2 + + local rightSideButtons = Instance.new("Frame") + rightSideButtons.Name = "RightSideButtons" + rightSideButtons.BackgroundTransparency = 1 + rightSideButtons.ZIndex = 2 + rightSideButtons.Position = UDim2.new(0, 0, 0, 0) + rightSideButtons.Size = UDim2.new(1, -10, 1, 0) + rightSideButtons.Parent = frame + + -- Selection Highlighting logic: + local updateHighlight = function(lostSelectionObject) + if frame then + if GuiService.SelectedCoreObject and GuiService.SelectedCoreObject ~= lostSelectionObject and GuiService.SelectedCoreObject.Parent == rightSideButtons then + frame.ImageTransparency = FRAME_SELECTED_TRANSPARENCY + else + frame.ImageTransparency = FRAME_DEFAULT_TRANSPARENCY + end + end + end + + local fakeSelectionObject = nil + rightSideButtons.ChildAdded:connect(function(child) + if child:IsA("GuiObject") then + if fakeSelectionObject and child ~= fakeSelectionObject then + fakeSelectionObject:Destroy() + fakeSelectionObject = nil + end + child.SelectionGained:connect(function() updateHighlight(nil) end) + child.SelectionLost:connect(function() updateHighlight(child) end) + end + end) + + fakeSelectionObject = Instance.new("Frame") + fakeSelectionObject.Selectable = true + fakeSelectionObject.Size = UDim2.new(0, 100, 0, 100) + fakeSelectionObject.BackgroundTransparency = 1 + fakeSelectionObject.SelectionImageObject = fakeSelectionObject:Clone() + fakeSelectionObject.Parent = rightSideButtons + + local rightSideListLayout = Instance.new("UIListLayout") + rightSideListLayout.Name = "RightSideListLayout" + rightSideListLayout.FillDirection = Enum.FillDirection.Horizontal + rightSideListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Right + rightSideListLayout.VerticalAlignment = Enum.VerticalAlignment.Center + rightSideListLayout.SortOrder = Enum.SortOrder.LayoutOrder + rightSideListLayout.Padding = UDim.new(0, 20) + rightSideListLayout.Parent = rightSideButtons + + local icon = Instance.new("ImageLabel") + icon.Name = "Icon" + icon.BackgroundTransparency = 1 + icon.Size = UDim2.new(0, 36, 0, 36) + icon.Position = UDim2.new(0, 12, 0, 12) + icon.ZIndex = 3 + icon.Parent = frame + + local nameLabel = Instance.new("TextLabel") + nameLabel.Name = "NameLabel" + pcall(function() + nameLabel.Localize = false + end) + nameLabel.TextXAlignment = Enum.TextXAlignment.Left + nameLabel.Font = Enum.Font.SourceSans + nameLabel.FontSize = Enum.FontSize.Size24 + nameLabel.TextColor3 = Color3.new(1, 1, 1) + nameLabel.BackgroundTransparency = 1 + nameLabel.Position = UDim2.new(0, 60, .5, 0) + nameLabel.Size = UDim2.new(0, 0, 0, 0) + nameLabel.ZIndex = 3 + nameLabel.Parent = frame + pcall(function() + nameLabel.Localize = false + end) + + return frame + end + end + + -- Manage cutting off a players name if it is too long when switching into portrait mode. + local function managePlayerNameCutoff(frame, player) + local wasIsPortrait = nil + local reportFlagAddedConnection = nil + local reportFlagChangedConnection = nil + local function reportFlagChanged(reportFlag, prop) + if prop == "AbsolutePosition" and wasIsPortrait then + local maxPlayerNameSize = reportFlag.AbsolutePosition.X - 20 - frame.NameLabel.AbsolutePosition.X + frame.NameLabel.Text = player.Name + local newNameLength = string.len(player.Name) + while frame.NameLabel.TextBounds.X > maxPlayerNameSize and newNameLength > 0 do + frame.NameLabel.Text = string.sub(player.Name, 1, newNameLength) .. "..." + newNameLength = newNameLength - 1 + end + end + end + utility:OnResized(frame.NameLabel, function(newSize, isPortrait) + if wasIsPortrait ~= nil and wasIsPortrait == isPortrait then + return + end + wasIsPortrait = isPortrait + if isPortrait then + if reportFlagAddedConnection == nil then + reportFlagAddedConnection = frame.RightSideButtons.ChildAdded:connect(function(child) + if child.Name == "ReportPlayer" then + reportFlagChangedConnection = child.Changed:connect(function(prop) reportFlagChanged(child, prop) end) + reportFlagChanged(child, "AbsolutePosition") + end + end) + end + local reportFlag = frame.RightSideButtons:FindFirstChild("ReportPlayer") + if reportFlag then + reportFlagChangedConnection = reportFlag.Changed:connect(function(prop) reportFlagChanged(reportFlag, prop) end) + reportFlagChanged(reportFlag, "AbsolutePosition") + end + else + frame.NameLabel.Text = player.Name + end + end) + end + + local function canShareCurrentGame() + return this.HubRef.ShareGamePage ~= nil + end + + local shareGameButton + local sortedPlayers + local existingPlayerLabels = {} + + -- fixPlayerRowLayout + local livePlayers = {} + + this.Displayed.Event:connect(function(switchedFromGamepadInput) + sortedPlayers = PlayersService:GetPlayers() + table.sort(sortedPlayers, function(item1,item2) + return item1.Name:lower() < item2.Name:lower() + end) + + local extraOffset = 20 + if utility:IsSmallTouchScreen() or utility:IsPortrait() then + extraOffset = 85 + end + + if FFlagSettingsHubInviteToGame2 then + -- Create "invite friends" button if it doesn't exist yet + -- We shouldn't create this button if we're not in a live game + if canShareCurrentGame() and not shareGameButton + and (not RunService:IsStudio() or FFlagSettingsHubInviteToGameInStudio2) then + local eventStream = EventStream.new() + shareGameButton = createShareGameButton() + shareGameButton.MouseButton1Click:connect(function() + local eventContext = "inGame" + local eventName = "inputShareGameEntryPoint" + local additionalArgs = { + buttonName = "settingsHub", + } + eventStream:setRBXEventStream(eventContext, eventName, additionalArgs) + + this.HubRef:AddToMenuStack(this.HubRef.Pages.CurrentPage) + this.HubRef:SwitchToPage(this.HubRef.ShareGamePage, nil, 1, true) + end) + + -- Ensure the button is always at the top of the list + shareGameButton.LayoutOrder = 1 + shareGameButton.Parent = this.Page + end + end + + friendSelectionFound = nil + reportSelectionFound = nil + + -- iterate through players to reuse or create labels for players + for index=1, #sortedPlayers do + local player = sortedPlayers[index] + local frame + if fixPlayerRowLayout then + frame = existingPlayerLabels[player.Name] + else + frame = existingPlayerLabels[index] + end + + if player then + livePlayers[player.Name] = true + -- create label (frame) for this player index if one does not exist + if not frame or not frame.Parent then + frame = createPlayerRow((index - 1)*PLAYER_ROW_SPACING + extraOffset) + frame.Parent = this.Page + if fixPlayerRowLayout then + existingPlayerLabels[player.Name] = frame + else + table.insert(existingPlayerLabels, index, frame) + end + end + frame.Name = "PlayerLabel" ..player.Name + + -- Immediately assign the image to an image that isn't guaranteed to be generated + frame.Icon.Image = SocialUtil.GetFallbackPlayerImageUrl(math.max(1, player.UserId), Enum.ThumbnailSize.Size100x100, Enum.ThumbnailType.AvatarThumbnail) + -- Spawn a function to get the generated image + spawn(function() + local imageUrl = SocialUtil.GetPlayerImage(math.max(1, player.UserId), Enum.ThumbnailSize.Size100x100, Enum.ThumbnailType.AvatarThumbnail) + if frame and frame.Parent and frame.Parent == this.Page then + frame.Icon.Image = imageUrl + end + end) + + frame.NameLabel.Text = player.Name + frame.ImageTransparency = FRAME_DEFAULT_TRANSPARENCY + if fixPlayerRowLayout then + if FFlagSettingsHubInviteToGame2 then + -- extra index room for shareGameButton + frame.LayoutOrder = index + 1 + else + frame.LayoutOrder = index + end + end + + managePlayerNameCutoff(frame, player) + + friendStatusCreate(frame, player) + if platform ~= Enum.Platform.XBoxOne and platform ~= Enum.Platform.PS4 then + reportAbuseButtonCreate(frame, player) + end + end + end + + if fixPlayerRowLayout then + for playerName, frame in pairs(existingPlayerLabels) do + if not livePlayers[playerName] then + frame:Destroy() + existingPlayerLabels[playerName] = nil + end + end + else + -- iterate through existing labels in reverse to destroy and remove unused labels + for index=#existingPlayerLabels, 1, -1 do + local player = sortedPlayers[index] + local frame = existingPlayerLabels[index] + if frame and not player then + table.remove(existingPlayerLabels, index) + frame:Destroy() + end + end + end + + utility:OnResized("MenuPlayerListExtraPageSize", function(newSize, isPortrait) + local extraOffset = 20 + if utility:IsSmallTouchScreen() or utility:IsPortrait() then + extraOffset = 85 + end + + if FFlagSettingsHubInviteToGame2 then + local inviteToGameRow = 1 + local playerListRowsCount = #sortedPlayers + inviteToGameRow + + this.Page.Size = UDim2.new(1,0,0, extraOffset + PLAYER_ROW_SPACING * playerListRowsCount - 5) + else + this.Page.Size = UDim2.new(1,0,0, extraOffset + PLAYER_ROW_SPACING * #sortedPlayers - 5) + end + + end) + end) + + if fixPlayerRowLayout or preventFriendingRemovedPlayers then + PlayersService.PlayerRemoving:Connect(function (player) + if fixPlayerRowLayout then + livePlayers[player.Name] = nil + end + + if preventFriendingRemovedPlayers then + local playerLabel + if fixPlayerRowLayout then + playerLabel = existingPlayerLabels[player.Name] + else + -- iterate through sorted list to search for removed player + if not sortedPlayers then + return + end + for index = 1, #sortedPlayers do + if sortedPlayers[index] == player then + playerLabel = existingPlayerLabels[index] + break + end + end + end + + if not playerLabel then + return + end + + local buttons = playerLabel:FindFirstChild("RightSideButtons") + if not buttons then + return + end + + local friendStatus = buttons:FindFirstChild("FriendStatus") + if friendStatus then + if GuiService.SelectedCoreObject == friendStatus then + friendSelectionFound = nil + GuiService.SelectedCoreObject = nil + end + friendStatus:Destroy() + end + + local reportPlayer = buttons:FindFirstChild("ReportPlayer") + if reportPlayer then + if GuiService.SelectedCoreObject == reportPlayer then + reportSelectionFound = nil + GuiService.SelectedCoreObject = nil + end + reportPlayer:Destroy() + end + end + end) + end + + return this +end + +----------- Public Facing API Additions -------------- +PageInstance = Initialize() + +return PageInstance diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/Record.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/Record.lua new file mode 100644 index 0000000..b83e925 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/Record.lua @@ -0,0 +1,164 @@ +--[[r + Filename: Record.lua + Written by: jeditkacheff + Version 1.0 + Description: Takes care of the Record Tab in Settings Menu +--]] +-------------- SERVICES -------------- +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local GuiService = game:GetService("GuiService") +local TextService = game:GetService("TextService") +local VRService = game:GetService("VRService") +local Settings = UserSettings() +local GameSettings = Settings.GameSettings + +----------- UTILITIES -------------- +RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface") +local utility = require(RobloxGui.Modules.Settings.Utility) +local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled() + +------------ Variables ------------------- +local PageInstance = nil + +local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end) +local FFlagUseNotificationsLocalization = success and result + +----------- CLASS DECLARATION -------------- + +local function Initialize() + local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory) + local this = settingsPageFactory:CreateNewPage() + local isRecordingVideo = false + + local recordingEvent = Instance.new("BindableEvent") + recordingEvent.Name = "RecordingEvent" + this.RecordingChanged = recordingEvent.Event + function this:IsRecording() + return isRecordingVideo + end + + ------ TAB CUSTOMIZATION ------- + this.TabHeader.Name = "RecordTab" + + this.TabHeader.Icon.Image = "rbxasset://textures/ui/Settings/MenuBarIcons/RecordTab.png" + this.TabHeader.Icon.AspectRatioConstraint.AspectRatio = 41 / 40 + + if FFlagUseNotificationsLocalization then + this.TabHeader.Title.Text = "Record" + else + this.TabHeader.Icon.Title.Text = "Record" + end + + local function onVREnabled() + this.TabHeader.Visible = not VRService.VREnabled + end + onVREnabled() + VRService:GetPropertyChangedSignal("VREnabled"):connect(onVREnabled) + + ------ PAGE CUSTOMIZATION ------- + this.Page.Name = "Record" + + local function makeTextLabel(name, text, isTitle, parent, layoutOrder) + local leftPadding, rightPadding, bottomPadding, textSize, font = 10, 0, 10, 24, Enum.Font.SourceSans + + if isTitle then + leftPadding, rightPadding, bottomPadding, textSize, font = 10, 0, 0, 36, Enum.Font.SourceSansBold + end + + local container = utility:Create'Frame' + { + Name = name .. "Container", + BackgroundTransparency = 1, + ZIndex = 2, + LayoutOrder = layoutOrder, + Parent = parent + }; + local textLabel = utility:Create'TextLabel' + { + Name = name, + BackgroundTransparency = 1, + Text = text, + TextWrapped = true, + Font = font, + TextSize = textSize, + TextColor3 = Color3.new(1,1,1), + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + Position = UDim2.new(0, leftPadding, 0, 0), + Size = UDim2.new(1, -(leftPadding + rightPadding), 1, 0), + ZIndex = 2, + Parent = container + }; + + local function onResized(prop) + if prop == "AbsoluteSize" then + local textSize = TextService:GetTextSize(text, textLabel.TextSize, textLabel.Font, Vector2.new(parent.AbsoluteSize.X - leftPadding - rightPadding, 1e4)) + container.Size = UDim2.new(1, 0, 0, textSize.Y + bottomPadding) + end + end + onResized("AbsoluteSize") + parent.Changed:connect(onResized) + + return textLabel, container + end + + -- need to override this function from SettingsPageFactory + -- DropDown menus require hub to to be set when they are initialized + function this:SetHub(newHubRef) + this.HubRef = newHubRef + + ---------------------------------- SCREENSHOT ------------------------------------- + local closeSettingsFunc = function() + this.HubRef:SetVisibility(false, true) + end + + local screenshotTitle = makeTextLabel("ScreenshotTitle", "Screenshot", true, this.Page, 1) + local screenshotBody = makeTextLabel("ScreenshotBody", "By clicking the 'Take Screenshot' button, the menu will close and take a screenshot and save it to your computer.", false, this.Page, 2) + + this.ScreenshotButtonRow, this.ScreenshotButton = utility:AddButtonRow(this, "ScreenshotButton", "Take Screenshot", UDim2.new(0, 300, 0, 44), closeSettingsFunc) + this.ScreenshotButtonRow.LayoutOrder = 3 + + ---------------------------------- VIDEO ------------------------------------- + local videoTitle = makeTextLabel("VideoTitle", "Video", true, this.Page, 4) + local videoBody = makeTextLabel("VideoBody", "By clicking the 'Record Video' button, the menu will close and start recording your screen.", false, this.Page, 5) + + local recordButtonRow, recordButton = utility:AddButtonRow(this, "RecordButton", "Record Video", UDim2.new(0, 300, 0, 44), closeSettingsFunc) + recordButtonRow.LayoutOrder = 6 + recordButton.MouseButton1Click:connect(function() + recordingEvent:Fire(not isRecordingVideo) + end) + + local gameOptions = settings():FindFirstChild("Game Options") + if gameOptions then + gameOptions.VideoRecordingChangeRequest:connect(function(recording) + isRecordingVideo = recording + if recording then + recordButton.RecordButtonTextLabel.Text = "Stop Recording" + else + recordButton.RecordButtonTextLabel.Text = "Record Video" + end + end) + end + + recordButton:SetVerb("RecordToggle") + this.ScreenshotButton:SetVerb("Screenshot") + + this.Page.Size = UDim2.new(1,0,0,400) + end + + return this +end + + +----------- Public Facing API Additions -------------- +PageInstance = Initialize() + +PageInstance.Displayed.Event:connect(function(switchedFromGamepadInput) + if switchedFromGamepadInput then + GuiService.SelectedCoreObject = PageInstance.ScreenshotButton + end +end) + + +return PageInstance diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ReportAbuseMenu.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ReportAbuseMenu.lua new file mode 100644 index 0000000..d9b1949 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ReportAbuseMenu.lua @@ -0,0 +1,385 @@ +--[[ + Filename: ReportAbuseMenu.lua + Written by: jeditkacheff + Version 1.0 + Description: Takes care of the report abuse page in Settings Menu +--]] + +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local GuiService = game:GetService("GuiService") +local PlayersService = game:GetService("Players") +local MarketplaceService = game:GetService("MarketplaceService") +local LocalizationService = game:GetService("LocalizationService") + +local utility = require(RobloxGui.Modules.Settings.Utility) +local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator) + +local ABUSE_TYPES_PLAYER = { + "Swearing", + "Inappropriate Username", + "Bullying", + "Scamming", + "Dating", + "Cheating/Exploiting", + "Personal Question", + "Offsite Links", +} + +local FFlagReportAbuseMenuDescriptionFix = settings():GetFFlag('ReportAbuseMenuDescriptionFix') + +local ABUSE_TYPES_GAME = { + "Inappropriate Content" +} + +local DEFAULT_ABUSE_DESC_TEXT = " Short Description (Optional)" +if utility:IsSmallTouchScreen() then + DEFAULT_ABUSE_DESC_TEXT = " (Optional)" +end + +pcall(function() + local LocalizationService = game:GetService("LocalizationService") + local CorescriptLocalization = LocalizationService:GetCorescriptLocalizations()[1] + + if FFlagReportAbuseMenuDescriptionFix then + if utility:IsSmallTouchScreen() then + DEFAULT_ABUSE_DESC_TEXT = RobloxTranslator:FormatByKey("KEY_DESCRIPTION_OPTIONAL") + else + DEFAULT_ABUSE_DESC_TEXT = RobloxTranslator:FormatByKey("KEY_DESCRIPTION_SHORT_DECRIPTION_OPTIONAL") + end + else + DEFAULT_ABUSE_DESC_TEXT = CorescriptLocalization:GetString( + LocalizationService.RobloxLocaleId, + "KEY_DESCRIPTION_OPTIONAL") + end +end) + +local PageInstance = nil + +local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end) +local FFlagUseNotificationsLocalization = success and result + +----------- CLASS DECLARATION -------------- +local function Initialize() + local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory) + local this = settingsPageFactory:CreateNewPage() + + local playerNames = {} + local nameToRbxPlayer = {} + local nextPlayerToReport = nil + + function this:GetPlayerFromIndex(index) + local playerName = playerNames[index] + if playerName then + return nameToRbxPlayer[playerName] + end + + return nil + end + + function this:SetNextPlayerToReport(player) + nextPlayerToReport = player + end + + function this:UpdatePlayerDropDown() + playerNames = {} + nameToRbxPlayer = {} + + local players = PlayersService:GetPlayers() + local index = 1 + for i = 1, #players do + local player = players[i] + if player ~= PlayersService.LocalPlayer and player.UserId > 0 then + playerNames[index] = player.Name + nameToRbxPlayer[player.Name] = player + index = index + 1 + end + end + + table.sort(playerNames, function(a, b) + return a:lower() < b:lower() + end) + + this.WhichPlayerMode:UpdateDropDownList(playerNames) + + --Reset GameOrPlayerMode to Game if no other players + if index == 1 then + this.GameOrPlayerMode:SetSelectionIndex(1) + end + + this.GameOrPlayerMode:SetInteractable(index > 1) + + if this.GameOrPlayerMode.CurrentIndex == 1 then + this.WhichPlayerMode:SetInteractable(false) + this.TypeOfAbuseMode:UpdateDropDownList(ABUSE_TYPES_GAME) + this.TypeOfAbuseMode:SetInteractable(#ABUSE_TYPES_GAME > 1) + else + this.WhichPlayerLabel.ZIndex = 2 + this.WhichPlayerMode:SetInteractable(index > 1) + this.TypeOfAbuseMode:UpdateDropDownList(ABUSE_TYPES_PLAYER) + this.TypeOfAbuseMode:SetInteractable(#ABUSE_TYPES_PLAYER > 1) + end + + if nextPlayerToReport then + local playerSelected = this.WhichPlayerMode:SetSelectionByValue(nextPlayerToReport.Name) + nextPlayerToReport = nil + + if this.GameOrPlayerMode.CurrentIndex == 2 then + if playerSelected then --if the reported player is still in game + --Auto select type of abuse when report a player + GuiService.SelectedCoreObject = this.TypeOfAbuseMode.DropDownFrame + else + GuiService.SelectedCoreObject = this.WhichPlayerMode.DropDownFrame + end + end + end + end + + ------ TAB CUSTOMIZATION ------- + this.TabHeader.Name = "ReportAbuseTab" + this.TabHeader.Icon.Image = "rbxasset://textures/ui/Settings/MenuBarIcons/ReportAbuseTab.png" + if FFlagUseNotificationsLocalization then + this.TabHeader.Title.Text = "Report" + else + this.TabHeader.Icon.Title.Text = "Report" + end + + ------ PAGE CUSTOMIZATION ------- + this.Page.Name = "ReportAbusePage" + + -- need to override this function from SettingsPageFactory + -- DropDown menus require hub to to be set when they are initialized + function this:SetHub(newHubRef) + this.HubRef = newHubRef + + if utility:IsSmallTouchScreen() then + this.GameOrPlayerFrame, + this.GameOrPlayerLabel, + this.GameOrPlayerMode = utility:AddNewRow(this, "Game or Player?", "Selector", {"Game", "Player"}, 2) + else + this.GameOrPlayerFrame, + this.GameOrPlayerLabel, + this.GameOrPlayerMode = utility:AddNewRow(this, "Game or Player?", "Selector", {"Game", "Player"}, 2, 3) + end + this.GameOrPlayerMode.Selection.LayoutOrder = 1 + + this.WhichPlayerFrame, + this.WhichPlayerLabel, + this.WhichPlayerMode = utility:AddNewRow(this, "Which Player?", "DropDown", {"update me"}) + this.WhichPlayerMode:SetInteractable(false) + this.WhichPlayerLabel.ZIndex = 1 + this.WhichPlayerFrame.LayoutOrder = 2 + + this.TypeOfAbuseFrame, + this.TypeOfAbuseLabel, + this.TypeOfAbuseMode = utility:AddNewRow(this, "Type Of Abuse", "DropDown", ABUSE_TYPES_GAME, 1) + this.TypeOfAbuseFrame.LayoutOrder = 3 + + if utility:IsSmallTouchScreen() then + this.AbuseDescriptionFrame, + this.AbuseDescriptionLabel, + this.AbuseDescription = utility:AddNewRow(this, DEFAULT_ABUSE_DESC_TEXT, "TextBox", nil, nil, 5) + + this.AbuseDescriptionLabel.Text = "Abuse Description" + else + this.AbuseDescriptionFrame, + this.AbuseDescriptionLabel, + this.AbuseDescription = utility:AddNewRow(this, DEFAULT_ABUSE_DESC_TEXT, "TextBox", nil, nil, 5) + + this.AbuseDescriptionFrame.Size = UDim2.new(1, -10, 0, 100) + this.AbuseDescription.Selection.Size = UDim2.new(1, 0, 1, 0) + end + + this.AbuseDescriptionFrame.LayoutOrder = 4 + + this.AbuseDescription.Selection.FocusLost:connect(function() + if this.AbuseDescription.Selection.Text == "" then + this.AbuseDescription.Selection.Text = DEFAULT_ABUSE_DESC_TEXT + end + end) + + local SelectionOverrideObject = utility:Create'ImageLabel' + { + Image = "", + BackgroundTransparency = 1 + }; + + local submitButton, submitText = nil, nil + + local function makeSubmitButtonActive() + submitButton.ZIndex = 2 + submitButton.Selectable = true + submitText.ZIndex = 2 + end + + local function makeSubmitButtonInactive() + submitButton.ZIndex = 1 + submitButton.Selectable = false + submitText.ZIndex = 1 + end + + local function updateAbuseDropDown() + this.WhichPlayerMode:ResetSelectionIndex() + this.TypeOfAbuseMode:ResetSelectionIndex() + + if this.GameOrPlayerMode.CurrentIndex == 1 then + this.TypeOfAbuseMode:UpdateDropDownList(ABUSE_TYPES_GAME) + + this.TypeOfAbuseMode:SetInteractable(#ABUSE_TYPES_GAME > 1) + this.TypeOfAbuseLabel.ZIndex = (#ABUSE_TYPES_GAME > 1 and 2 or 1) + + this.WhichPlayerMode:SetInteractable(false) + this.WhichPlayerLabel.ZIndex = 1 + makeSubmitButtonActive() + else + this.TypeOfAbuseMode:UpdateDropDownList(ABUSE_TYPES_PLAYER) + this.TypeOfAbuseMode:SetInteractable(#ABUSE_TYPES_PLAYER > 1) + this.TypeOfAbuseLabel.ZIndex = (#ABUSE_TYPES_PLAYER > 1 and 2 or 1) + + if #playerNames > 0 then + this.WhichPlayerMode:SetInteractable(true) + this.WhichPlayerLabel.ZIndex = 2 + else + this.WhichPlayerMode:SetInteractable(false) + this.WhichPlayerLabel.ZIndex = 1 + end + makeSubmitButtonInactive() + end + end + + local function cleanupReportAbuseMenu() + updateAbuseDropDown() + this.AbuseDescription.Selection.Text = DEFAULT_ABUSE_DESC_TEXT + this.HubRef:SetVisibility(false, true) + end + + local function onReportSubmitted() + local abuseReason = nil + local reportSucceeded = false + if this.GameOrPlayerMode.CurrentIndex == 2 then + abuseReason = ABUSE_TYPES_PLAYER[this.TypeOfAbuseMode.CurrentIndex] + + local currentAbusingPlayer = this:GetPlayerFromIndex(this.WhichPlayerMode.CurrentIndex) + if currentAbusingPlayer and abuseReason then + reportSucceeded = true + spawn(function() + PlayersService:ReportAbuse(currentAbusingPlayer, abuseReason, this.AbuseDescription.Selection.Text) + end) + end + else + abuseReason = ABUSE_TYPES_GAME[this.TypeOfAbuseMode.CurrentIndex] + if abuseReason then + reportSucceeded = true + spawn(function() + local placeId,placeName,placeDescription = tostring(game.PlaceId), "N/A", "N/A" + local abuseDescription = this.AbuseDescription.Selection.Text + pcall(function() + local productInfo = MarketplaceService:GetProductInfo(game.PlaceId, Enum.InfoType.Asset) + placeName = productInfo.Name + placeDescription = productInfo.Description + end) + local formattedText = string.format("User Report: \n %s \n".."Place Title: \n %s \n".."PlaceId: \n %s \n".."Place Description: \n %s \n",abuseDescription, placeName, placeId, placeDescription) + + PlayersService:ReportAbuse(nil, abuseReason, formattedText) + end) + end + end + + if reportSucceeded then + local alertText = "Thanks for your report! Our moderators will review the chat logs and evaluate what happened." + + if abuseReason == 'Cheating/Exploiting' then + alertText = "Thanks for your report! We've recorded your report for evaluation." + elseif abuseReason == 'Inappropriate Username' then + alertText = "Thanks for your report! Our moderators will evaluate the username." + elseif abuseReason == "Bad Model or Script" or abuseReason == "Inappropriate Content" or abuseReason == "Offsite Link" or abuseReason == "Offsite Links" then + alertText = "Thanks for your report! Our moderators will review the place and make a determination." + end + + utility:ShowAlert(alertText, "Ok", this.HubRef, cleanupReportAbuseMenu) + + this.LastSelectedObject = nil + end + end + + submitButton, submitText = utility:MakeStyledButton("SubmitButton", "Submit", UDim2.new(0,198,0,50), onReportSubmitted, this) + + submitButton.AnchorPoint = Vector2.new(0.5,0) + submitButton.Position = UDim2.new(0.5,0,1,5) + + if this.GameOrPlayerMode.CurrentIndex == 1 then + makeSubmitButtonActive() + else + makeSubmitButtonInactive() + end + submitButton.Parent = this.AbuseDescription.Selection + + local function playerSelectionChanged(newIndex) + if newIndex ~= nil and this.TypeOfAbuseMode:GetSelectedIndex() ~= nil then + makeSubmitButtonActive() + else + makeSubmitButtonInactive() + end + end + this.WhichPlayerMode.IndexChanged:connect(playerSelectionChanged) + + local function typeOfAbuseChanged(newIndex) + if newIndex ~= nil then + if this.GameOrPlayerMode.CurrentIndex == 1 then -- 1 is Report Game + makeSubmitButtonActive() + else -- 2 is Report Player + if this.WhichPlayerMode:GetSelectedIndex() then + makeSubmitButtonActive() + else + makeSubmitButtonInactive() + end + end + else + makeSubmitButtonInactive() + end + end + this.TypeOfAbuseMode.IndexChanged:connect(typeOfAbuseChanged) + + this.GameOrPlayerMode.IndexChanged:connect(updateAbuseDropDown) + + this:AddRow(nil, nil, this.AbuseDescription) + + this.Page.Size = UDim2.new(1,0,0,submitButton.AbsolutePosition.Y + submitButton.AbsoluteSize.Y) + end + + return this +end + + +----------- Public Facing API Additions -------------- +do + PageInstance = Initialize() + + PageInstance.Displayed.Event:connect(function() + PageInstance:UpdatePlayerDropDown() + end) + + function PageInstance:ReportPlayer(player) + if player then + local setReportPlayerConnection = nil + PageInstance:SetNextPlayerToReport(player) + setReportPlayerConnection = PageInstance.Displayed.Event:connect(function() + PageInstance.GameOrPlayerMode:SetSelectionIndex(2) + + if setReportPlayerConnection then + setReportPlayerConnection:disconnect() + setReportPlayerConnection = nil + end + end) + + if not PageInstance.HubRef:GetVisibility() then + PageInstance.HubRef:SetVisibility(true, false, PageInstance) + else + PageInstance.HubRef:SwitchToPage(PageInstance, false) + end + end + end +end + + +return PageInstance diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ResetCharacter.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ResetCharacter.lua new file mode 100644 index 0000000..a9ecde9 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ResetCharacter.lua @@ -0,0 +1,165 @@ +--[[ + Filename: ResetCharacter.lua + Written by: jeditkacheff + Version 1.0 + Description: Takes care of the reseting the character in Settings Menu +--]] + +-------------- CONSTANTS ------------- +local RESET_CHARACTER_GAME_ACTION = "ResetCharacterAction" + +-------------- SERVICES -------------- +local CoreGui = game:GetService("CoreGui") +local ContextActionService = game:GetService("ContextActionService") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local GuiService = game:GetService("GuiService") +local PlayersService = game:GetService("Players") + +----------- UTILITIES -------------- +local utility = require(RobloxGui.Modules.Settings.Utility) + +------------ Variables ------------------- +local PageInstance = nil +RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface") +local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled() + +----------- CLASS DECLARATION -------------- + +local function Initialize() + local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory) + local this = settingsPageFactory:CreateNewPage() + + this.DontResetCharFunc = function(isUsingGamepad) + if this.HubRef then + this.HubRef:PopMenu(isUsingGamepad, true) + end + end + this.DontResetCharFromHotkey = function(name, state, input) + if state == Enum.UserInputState.Begin then + local isUsingGamepad = input.UserInputType == Enum.UserInputType.Gamepad1 or input.UserInputType == Enum.UserInputType.Gamepad2 + or input.UserInputType == Enum.UserInputType.Gamepad3 or input.UserInputType == Enum.UserInputType.Gamepad4 + + this.DontResetCharFunc(isUsingGamepad) + end + end + this.DontResetCharFromButton = function(isUsingGamepad) + this.DontResetCharFunc(isUsingGamepad) + end + + ------ TAB CUSTOMIZATION ------- + this.TabHeader = nil -- no tab for this page + + ------ PAGE CUSTOMIZATION ------- + this.Page.Name = "ResetCharacter" + this.ShouldShowBottomBar = false + this.ShouldShowHubBar = false + + local resetCharacterText = utility:Create'TextLabel' + { + Name = "ResetCharacterText", + Text = "Are you sure you want to reset your character?", + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size36, + TextColor3 = Color3.new(1,1,1), + BackgroundTransparency = 1, + Size = UDim2.new(1,0,0,200), + TextWrapped = true, + ZIndex = 2, + Parent = this.Page, + Position = isTenFootInterface and UDim2.new(0,0,0,100) or UDim2.new(0,0,0,0) + }; + + local resetButtonContainer = utility:Create"Frame" + { + Name = "ResetButtonContainer", + Parent = resetCharacterText, + Size = UDim2.new(1,0,0,400), + BackgroundTransparency = 1, + Position = UDim2.new(0,0,1,0) + }; + + local resetButtonLayout = utility:Create'UIGridLayout' + { + Name = "ResetButtonsLayout", + CellSize = isTenFootInterface and UDim2.new(0, 300, 0, 80) or UDim2.new(0, 200, 0, 50), + CellPadding = UDim2.new(0,20,0,20), + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Top, + Parent = resetButtonContainer + }; + + if utility:IsSmallTouchScreen() then + resetCharacterText.FontSize = Enum.FontSize.Size24 + resetCharacterText.Size = UDim2.new(1,0,0,100) + elseif isTenFootInterface then + resetCharacterText.FontSize = Enum.FontSize.Size48 + end + + ------ Init ------- + local resetCharFunc = function() + local player = PlayersService.LocalPlayer + if player then + local character = player.Character + if character then + local humanoid = character:FindFirstChild('Humanoid') + if humanoid then + humanoid.Health = 0 + end + end + end + end + + this.ResetBindable = true + + local onResetFunction = function() + if this.ResetBindable == true then + resetCharFunc() + elseif this.ResetBindable then + this.ResetBindable:Fire() + end + if this.HubRef then + this.HubRef:SetVisibility(false, true) + end + end + + this.ResetCharacterButton = utility:MakeStyledButton("ResetCharacter", "Reset", nil, onResetFunction) + this.ResetCharacterButton.NextSelectionRight = nil + this.ResetCharacterButton.Parent = resetButtonContainer + + + local dontResetCharacterButton = utility:MakeStyledButton("DontResetCharacter", "Don't Reset", nil, this.DontResetCharFromButton) + dontResetCharacterButton.NextSelectionLeft = nil + dontResetCharacterButton.Parent = resetButtonContainer + + this.Page.Size = UDim2.new(1,0,0,dontResetCharacterButton.AbsolutePosition.Y + dontResetCharacterButton.AbsoluteSize.Y) + + return this +end + + +----------- Public Facing API Additions -------------- +PageInstance = Initialize() +local isOpen = false + +PageInstance.Displayed.Event:connect(function() + isOpen = true + GuiService.SelectedCoreObject = PageInstance.ResetCharacterButton + ContextActionService:BindCoreAction(RESET_CHARACTER_GAME_ACTION, PageInstance.DontResetCharFromHotkey, false, Enum.KeyCode.ButtonB) +end) + +PageInstance.Hidden.Event:connect(function() + isOpen = false + ContextActionService:UnbindCoreAction(RESET_CHARACTER_GAME_ACTION) +end) + +function PageInstance:SetResetCallback(bindableEvent) + if bindableEvent == false and isOpen then + -- We need to close this page if reseting was just disabled and the page is already open + PageInstance.HubRef:PopMenu(nil, true) + end + PageInstance.ResetBindable = bindableEvent +end + +return PageInstance diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/ClosePage.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/ClosePage.lua new file mode 100644 index 0000000..5c33352 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/ClosePage.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CorePackages").AppTempCommon +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(route) + return { + route = route, + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/OpenPage.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/OpenPage.lua new file mode 100644 index 0000000..5c33352 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/OpenPage.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CorePackages").AppTempCommon +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(route) + return { + route = route, + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/SetDeviceLayout.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/SetDeviceLayout.lua new file mode 100644 index 0000000..01338f8 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/SetDeviceLayout.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CorePackages").AppTempCommon +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(deviceLayout) + return { + deviceLayout = deviceLayout, + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/SetIsSmallTouchScreen.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/SetIsSmallTouchScreen.lua new file mode 100644 index 0000000..3d3502d --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/SetIsSmallTouchScreen.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CorePackages").AppTempCommon +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(isSmallTouchScreen) + return { + isSmallTouchScreen = isSmallTouchScreen, + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/SetSearchAreaActive.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/SetSearchAreaActive.lua new file mode 100644 index 0000000..453ef2a --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/SetSearchAreaActive.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CorePackages").AppTempCommon +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(isActive) + return { + isActive = isActive, + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/SetSearchText.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/SetSearchText.lua new file mode 100644 index 0000000..de350ad --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/SetSearchText.lua @@ -0,0 +1,8 @@ +local Modules = game:GetService("CorePackages").AppTempCommon +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(searchText) + return { + searchText = searchText, + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/SetUserInvited.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/SetUserInvited.lua new file mode 100644 index 0000000..fcbed9e --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Actions/SetUserInvited.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CorePackages").AppTempCommon +local Action = require(Modules.Common.Action) + +return Action(script.Name, function(userId, isInvited) + return { + userId = tostring(userId), + isInvited = isInvited, + } +end) \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/AppReducer.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/AppReducer.lua new file mode 100644 index 0000000..69cdcf7 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/AppReducer.lua @@ -0,0 +1,27 @@ +local CorePackages = game:GetService("CorePackages") +local AppTempCommon = CorePackages.AppTempCommon + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local ShareGame = Modules.Settings.Pages.ShareGame + +local PlaceInfos = require(AppTempCommon.LuaChat.Reducers.PlaceInfos) +local Users = require(AppTempCommon.LuaApp.Reducers.Users) + +local ConversationsSearch = require(ShareGame.Reducers.ConversationsSearch) +local DeviceInfo = require(ShareGame.Reducers.DeviceInfo) +local Page = require(ShareGame.Reducers.Page) + +local Invites = require(ShareGame.Reducers.Invites) + +return function(state, action) + state = state or {} + + return { + ConversationsSearch = ConversationsSearch(state.ConversationsSearch, action), + DeviceInfo = DeviceInfo(state.DeviceInfo, action), + Invites = Invites(state.Invites, action), + Page = Page(state.Page, action), + PlaceInfos = PlaceInfos(state.PlaceInfos, action), + Users = Users(state.Users, action), + } +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/App.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/App.lua new file mode 100644 index 0000000..09a6d84 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/App.lua @@ -0,0 +1,73 @@ +local CoreGui = game:GetService("CoreGui") +local CorePackages = game:GetService("CorePackages") +local HttpRbxApiService = game:GetService("HttpRbxApiService") +local Players = game:GetService("Players") + + +local AppTempCommon = CorePackages.AppTempCommon +local Modules = CoreGui.RobloxGui.Modules + +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) + +local ShareGame = Modules.Settings.Pages.ShareGame +local Constants = require(ShareGame.Constants) + +local EventStream = require(AppTempCommon.Temp.EventStream) +local Promise = require(AppTempCommon.LuaApp.Promise) +local httpRequest = require(AppTempCommon.Temp.httpRequest) +local ApiFetchUsersFriends = require(AppTempCommon.LuaApp.Thunks.ApiFetchUsersFriends) + +local LayoutProvider = require(ShareGame.Components.LayoutProvider) +local ShareGamePageFrame = require(ShareGame.Components.ShareGamePageFrame) + +local ShareGameApp = Roact.PureComponent:extend("App") + +function ShareGameApp:render() + local pageTarget = self.props.pageTarget + + local pageFrame = nil + if self.props.isPageOpen then + pageFrame = Roact.createElement(ShareGamePageFrame, { + zIndex = Constants.SHARE_GAME_Z_INDEX, + }) + end + + return Roact.createElement(LayoutProvider, nil, { + Roact.createElement(Roact.Portal, { + target = pageTarget, + }, { + ShareGamePageFrame = pageFrame + }) + }) +end + +function ShareGameApp:didMount() + self._networkImpl = httpRequest(HttpRbxApiService) + self.props.initialFetch(self._networkImpl) + self.eventStream = EventStream.new() +end + +function ShareGameApp:willUnmount() + self.eventStream:releaseRBXEventStream() +end + +local connector = RoactRodux.connect(function(store) + local userId = tostring(Players.LocalPlayer.UserId) + + return { + isPageOpen = store:getState().Page.IsOpen, + initialFetch = function(networkImpl) + Promise.all({ + store:dispatch(ApiFetchUsersFriends( + networkImpl, userId, Constants.ThumbnailRequest.InviteToGameHeadshot + )), + -- V2: Add a fetch for conversations in this promise list + }):andThen(function(result) + -- TODO: This: self.state.store:dispatch(SetFetchedShareGameData(true)) + end) + end, + } +end) + +return connector(ShareGameApp) diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/BackButton.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/BackButton.lua new file mode 100644 index 0000000..de743b9 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/BackButton.lua @@ -0,0 +1,55 @@ +local CorePackages = game:GetService("CorePackages") + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local ShareGame = Modules.Settings.Pages.ShareGame +local RectangleButton = require(ShareGame.Components.RectangleButton) +local IconButton = require(ShareGame.Components.IconButton) +local Roact = require(CorePackages.Roact) + +local Constants = require(ShareGame.Constants) +local ShareGameIcons = require(Modules.Settings.Pages.ShareGame.Spritesheets.ShareGameIcons) + +local BACK_IMAGE_SPRITE_PATH = ShareGameIcons:GetImagePath() +local BACK_IMAGE_SPRITE_FRAME = ShareGameIcons:GetFrame("back") +local BACK_BUTTON_TEXT = "Back" +local BACK_BUTTON_TEXT_SIZE = 24 + +local BackButton = Roact.PureComponent:extend("BackButton") + +function BackButton:render() + local isArrow = self.props.isArrow + local visible = self.props.visible + local anchorPoint = self.props.anchorPoint + local position = self.props.position + local size = self.props.size + local zIndex = self.props.zIndex + local onClick = self.props.onClick + + if isArrow then + return Roact.createElement(IconButton, { + visible = visible, + anchorPoint = anchorPoint, + position = position, + size = size, + zIndex = zIndex, + onClick = onClick, + iconHorizontalAlignment = Enum.HorizontalAlignment.Left, + iconSpritePath = BACK_IMAGE_SPRITE_PATH, + iconSpriteFrame = BACK_IMAGE_SPRITE_FRAME, + }) + else + return Roact.createElement(RectangleButton, self.props, { + TextLabel = Roact.createElement("TextLabel", { + Position = UDim2.new(0.5, 0, 0.5, 0), + Text = BACK_BUTTON_TEXT, + TextSize = BACK_BUTTON_TEXT_SIZE, + TextColor3 = Constants.Color.WHITE, + Font = Enum.Font.SourceSansSemibold, + ZIndex = zIndex, + }), + }) + end +end + +return BackButton diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationDetails.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationDetails.lua new file mode 100644 index 0000000..d03e9ea --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationDetails.lua @@ -0,0 +1,75 @@ +local CorePackages = game:GetService("CorePackages") +local AppTempCommon = CorePackages.AppTempCommon + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(CorePackages.Roact) + +local ShareGame = Modules.Settings.Pages.ShareGame +local Constants = require(ShareGame.Constants) +local User = require(AppTempCommon.LuaApp.Models.User) + +local LIST_PADDING = 2 + +local TITLE_FONT = Enum.Font.SourceSans +local TITLE_COLOR = Constants.Color.WHITE +local TITLE_TEXT_SIZE = 19 + +local PRESENCE_FONT = Enum.Font.SourceSans +local PRESENCE_TEXT_SIZE = 16 + +local ConversationDetails = Roact.PureComponent:extend("ConversationDetails") + +function ConversationDetails:render() + local title = self.props.title + local presence = self.props.presence + local size = self.props.size + local layoutOrder = self.props.layoutOrder + local zIndex = self.props.zIndex + + -- Show user presence if it was passed in + local presenceTextComponent + if presence ~= nil and presence ~= User.PresenceType.OFFLINE then + presenceTextComponent = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, PRESENCE_TEXT_SIZE), + Text = Constants.PresenceText[presence], + Font = PRESENCE_FONT, + TextColor3 = Constants.PresenceColors[presence], + TextSize = PRESENCE_TEXT_SIZE, + TextXAlignment = Enum.TextXAlignment.Left, + LayoutOrder = 1, + ZIndex = zIndex, + }) + end + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = size, + LayoutOrder = layoutOrder, + ZIndex = zIndex, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, LIST_PADDING) + }), + Title = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, TITLE_TEXT_SIZE), + Text = title or "", + Font = TITLE_FONT, + TextColor3 = TITLE_COLOR, + TextSize = TITLE_TEXT_SIZE, + TextXAlignment = Enum.TextXAlignment.Left, + TextWrapped = true, + LayoutOrder = 0, + ZIndex = zIndex, + }), + Presence = presenceTextComponent, + }) +end + +return ConversationDetails diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationEntry.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationEntry.lua new file mode 100644 index 0000000..7411b82 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationEntry.lua @@ -0,0 +1,126 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(CorePackages.Roact) + +local ShareGame = Modules.Settings.Pages.ShareGame +local ConversationThumbnail = require(ShareGame.Components.ConversationThumbnail) +local ConversationDetails = require(ShareGame.Components.ConversationDetails) +local InviteButton = require(ShareGame.Components.InviteButton) +local EventStream = require(CorePackages.AppTempCommon.Temp.EventStream) + +local ENTRY_BG_IMAGE = "rbxasset://textures/ui/dialog_white.png" +local ENTRY_BG_SLICE = Rect.new(10, 10, 10, 10) +local ENTRY_BG_TRANSPARENCY = 0.85 + +local THUMBNAIL_SIZE = 32 +local INVITE_BUTTON_WIDTH = 69 +local CONTENTS_PADDING = 12 + +local ConversationEntry = Roact.PureComponent:extend("ConversationEntry") + +function ConversationEntry:init() + self.eventStream = EventStream.new() +end + +function ConversationEntry:render() + local visible = self.props.visible + local layoutOrder = self.props.layoutOrder + local zIndex = self.props.zIndex + local size = self.props.size + local title = self.props.title + local users = self.props.users + local inviteUser = self.props.inviteUser + local alreadyInvited = self.props.alreadyInvited + + -- Presence gets passed in if there's only one user + local presence = self.props.presence + + return Roact.createElement("ImageLabel", { + Visible = visible, + BackgroundTransparency = 1, + Image = ENTRY_BG_IMAGE, + ImageTransparency = ENTRY_BG_TRANSPARENCY, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = ENTRY_BG_SLICE, + Size = size, + LayoutOrder = layoutOrder, + ZIndex = zIndex, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, CONTENTS_PADDING), + PaddingRight = UDim.new(0, CONTENTS_PADDING), + PaddingTop = UDim.new(0, CONTENTS_PADDING), + PaddingBottom = UDim.new(0, CONTENTS_PADDING), + }), + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 12), + }), + + Thumbnail = Roact.createElement(ConversationThumbnail, { + users = users, + size = UDim2.new(0, THUMBNAIL_SIZE, 0, THUMBNAIL_SIZE), + layoutOrder = 0, + zIndex = zIndex, + }), + Details = Roact.createElement(ConversationDetails, { + title = title, + presence = presence, + size = UDim2.new( + -- Make details fullwidth and subtract the width of its siblings + 1, -(THUMBNAIL_SIZE + INVITE_BUTTON_WIDTH + CONTENTS_PADDING * 2), + 1, 0 + ), + layoutOrder = 1, + zIndex = zIndex, + }), + InviteButton = Roact.createElement(InviteButton, { + size = UDim2.new(0, INVITE_BUTTON_WIDTH, 1, 0), + layoutOrder = 2, + zIndex = zIndex, + onInvite = function() + -- Check if this is a one-on-one convo + if #users == 1 then + inviteUser(users[1].id):andThen(function(results) + if not results then + return + end + + -- Pluck the userIds out of the user list + local participants = {} + for _, user in pairs(users) do + table.insert(participants, user.id) + end + + local localPlayer = Players.LocalPlayer + local senderId = tostring(localPlayer.UserId) + + local eventContext = "inGame" + local eventName = "clickShareGameInviteSent" + local participantsString = table.concat(participants, ",") + local additionalArgs = { + btn = "settingsHub", + placeId = tostring(results.placeId), + senderId = senderId, + conversationId = tostring(results.conversationId), + participants = participantsString, + wasModerated = results.wasModerated, + } + + self.eventStream:setRBXEventStream(eventContext, eventName, additionalArgs) + -- TODO: SOC-722 Show error if link is moderated + end) + end + end, + alreadyInvited = alreadyInvited, + }), + }) +end + +return ConversationEntry diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationList.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationList.lua new file mode 100644 index 0000000..ccd0b6c --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationList.lua @@ -0,0 +1,185 @@ +local CorePackages = game:GetService("CorePackages") +local HttpRbxApiService = game:GetService("HttpRbxApiService") +local AppTempCommon = CorePackages.AppTempCommon + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) + +local ShareGame = Modules.Settings.Pages.ShareGame +local ConversationEntry = require(ShareGame.Components.ConversationEntry) +local NoFriendsPage = require(ShareGame.Components.NoFriendsPage) +local Constants = require(ShareGame.Constants) +local httpRequest = require(AppTempCommon.Temp.httpRequest) + +local User = require(AppTempCommon.LuaApp.Models.User) +local SetUserInvited = require(ShareGame.Actions.SetUserInvited) +local memoize = require(AppTempCommon.LuaApp.memoize) +local Promise = require(AppTempCommon.LuaApp.Promise) + +local ApiSendGameInvite = require(AppTempCommon.LuaApp.Thunks.ApiSendGameInvite) +local ApiFetchPlaceInfos = require(AppTempCommon.LuaApp.Thunks.ApiFetchPlaceInfos) + +local ENTRY_HEIGHT = 62 +local ENTRY_PADDING = 18 + +local NO_RESULTS_FONT = Enum.Font.SourceSans +local NO_RESULTS_TEXTCOLOR = Constants.Color.GRAY3 +local NO_RESULTS_TEXTSIZE = 19 +local NO_RESULTS_TRANSPRENCY = 0.22 + +local PRESENCE_WEIGHTS = { + [User.PresenceType.ONLINE] = 3, + [User.PresenceType.IN_GAME] = 2, + [User.PresenceType.IN_STUDIO] = 1, + [User.PresenceType.OFFLINE] = 0, +} + +local ConversationList = Roact.PureComponent:extend("ConversationList") + +local function searchFilterPredicate(query, other) + if query == "" then + return true + end + return string.find(string.lower(other), query:lower(), 1, true) ~= nil +end + +function ConversationList:render() + local children = self.props[Roact.Children] or {} + local friends = self.props.friends + local layoutOrder = self.props.layoutOrder + local size = self.props.size + local zIndex = self.props.zIndex + local topPadding = self.props.topPadding + + local invites = self.props.invites + local inviteUser = self.props.inviteUser + local searchText = self.props.searchText + + children["RowListLayout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, ENTRY_PADDING), + }) + + local numEntries = 0 + -- Populate list of conversations with friends + for i, user in ipairs(friends) do + local isEntryShown = searchFilterPredicate(searchText, user.name) + + children["User-"..user.name] = Roact.createElement(ConversationEntry, { + visible = isEntryShown, + size = UDim2.new(1, 0, 0, ENTRY_HEIGHT), + layoutOrder = i, + zIndex = zIndex, + title = user.name, + presence = user.presence, + users = {user}, + inviteUser = inviteUser, + alreadyInvited = invites[user.id] == true, + }) + + if isEntryShown then + numEntries = numEntries + 1 + end + end + + if #friends == 0 then + return Roact.createElement(NoFriendsPage, { + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + Position = UDim2.new(0, 0, 0, topPadding), + ZIndex = zIndex, + }) + else + if numEntries == 0 then + return Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + Font = NO_RESULTS_FONT, + Size = UDim2.new(1, 0, 0, ENTRY_HEIGHT), + Text = "No results found", + TextColor3 = NO_RESULTS_TEXTCOLOR, + TextSize = NO_RESULTS_TEXTSIZE, + TextTransparency = NO_RESULTS_TRANSPRENCY, + ZIndex = zIndex, + }) + end + end + + return Roact.createElement("ScrollingFrame", { + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + Size = size, + CanvasSize = UDim2.new(0, 0, 0, numEntries * (ENTRY_HEIGHT + ENTRY_PADDING)), + ScrollBarThickness = 0, + ZIndex = zIndex, + }, children) +end + +local selectFriends = memoize(function(users) + local friends = {} + local function friendPreference(friend1, friend2) + local friend1Weight = PRESENCE_WEIGHTS[friend1.presence] + local friend2Weight = PRESENCE_WEIGHTS[friend2.presence] + + if friend1Weight == friend2Weight then + return friend1.name:lower() < friend2.name:lower() + else + return friend1Weight > friend2Weight + end + end + + for _, user in pairs(users) do + if user.isFriend then + friends[#friends + 1] = user + end + end + + table.sort(friends, friendPreference) + + return friends +end) + +local connector = RoactRodux.connect(function(store, props) + local state = store:getState() + return { + friends = selectFriends( + state.Users + ), + invites = state.Invites, + + inviteUser = function(userId) + return Promise.new(function(resolve, reject) + local latestState = store:getState() + local placeId = tostring(game.PlaceId) + -- Check that we haven't already invited this user + if latestState.Invites[tostring(userId)] == true then + reject() + return + end + local requestImpl = httpRequest(HttpRbxApiService) + local placeInfo = latestState.PlaceInfos[placeId] + -- Log that we've tried inviting this user + store:dispatch(SetUserInvited(userId, true)) + -- Send invite if we already have the current game's place info + if placeInfo then + store:dispatch(ApiSendGameInvite(requestImpl, userId, placeInfo)):andThen(resolve) + else + -- Fetch place info of current game if we don't have it, then + -- send the invite + store:dispatch(ApiFetchPlaceInfos(requestImpl, {placeId})):andThen(function(placeInfos) + if placeInfos[1] ~= nil then + store:dispatch(ApiSendGameInvite(requestImpl, userId, placeInfos[1])):andThen(resolve) + end + end) + end + end) + end + } +end) + +return connector(ConversationList) diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationThumbnail.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationThumbnail.lua new file mode 100644 index 0000000..8f07e70 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationThumbnail.lua @@ -0,0 +1,173 @@ +local CorePackages = game:GetService("CorePackages") + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(CorePackages.Roact) + +local Constants = require(Modules.Settings.Pages.ShareGame.Constants) + +local BORDER_SIZE = 1 +local BORDER_COLOR = Constants.Color.GRAY3 + +local THUMBNAIL_IMAGE_SIZE = Constants.AvatarThumbnailSizes.Size100x100 +local THUMBNAIL_IMAGE_TYPE = Constants.AvatarThumbnailTypes.HeadShot +local DEFAULT_THUMBNAIL_ICON = "rbxasset://textures/ui/LuaApp/graphic/ph-avatar-portrait.png" + +-- (Borrowed values from LuaChat) +-- This lets us determine how to build the group thumbnail. +-- Index represents how many people are in the thumbnail! +local IMAGE_LAYOUT = { + [1] = { + { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + FrameSize = UDim2.new(1, 0, 1, 0), + FramePosition = UDim2.new(0, 0, 0, 0), + }, + }, + [2] = { + { + Size = UDim2.new(2, 0, 1, 0), + Position = UDim2.new(-0.5, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 1, 0), + FramePosition = UDim2.new(0, 0, 0, 0), + Border = { + BorderPosition = UDim2.new(0.5, -1, 0, 0), + BorderSize = UDim2.new(0, 1, 1, 0), + }, + }, + { + Size = UDim2.new(2, 0, 1, 0), + Position = UDim2.new(-0.5, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 1, 0), + FramePosition = UDim2.new(0.5, 1, 0, 0), + }, + }, + [3] = { + { + Size = UDim2.new(2, 0, 1, 0), + Position = UDim2.new(-0.5, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 1, 0), + FramePosition = UDim2.new(0, 0, 0, 0), + Border = { + BorderPosition = UDim2.new(0.5, -1, 0, 0), + BorderSize = UDim2.new(0, 1, 1, 0), + }, + }, + { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 0.5, -1), + FramePosition = UDim2.new(0.5, 1, 0, 0), + Border = { + BorderPosition = UDim2.new(0.5, 0, 0.5, -1), + BorderSize = UDim2.new(0.5, 0, 0, 1), + }, + }, + { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 0.5, -1), + FramePosition = UDim2.new(0.5, 1, 0.5, 1), + }, + }, + [4] = { + { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 0.5, -1), + FramePosition = UDim2.new(0, 0, 0, 0), + Border = { + BorderPosition = UDim2.new(0, 0, 0.5, -1), + BorderSize = UDim2.new(1, 0, 0, 1), + }, + }, + { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 0.5, -1), + FramePosition = UDim2.new(0.5, 1, 0, 0), + }, + { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 0.5, -1), + FramePosition = UDim2.new(0, 0, 0.5, 1), + Border = { + BorderPosition = UDim2.new(0.5, -1, 0, 0), + BorderSize = UDim2.new(0, 1, 1, 0), + }, + }, + { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + FrameSize = UDim2.new(0.5, -1, 0.5, -1), + FramePosition = UDim2.new(0.5, 1, 0.5, 1), + }, + }, +} + +local ConversationThumbnail = Roact.PureComponent:extend("ConversationThumbnail") + +function ConversationThumbnail:render() + local size = self.props.size + local layoutOrder = self.props.layoutOrder + local zIndex = self.props.zIndex + local users = self.props.users + local numUsers = #users + + -- Render the thumbnail contents + local thumbnailContents = {} + for i, user in ipairs(users) do + local layoutData = IMAGE_LAYOUT[numUsers][i] + + -- Render the avatar inside a clipping container + thumbnailContents["AvatarHolder-"..i] = Roact.createElement("Frame", { + BackgroundTransparency = 1, + ClipsDescendants = true, + Size = layoutData.FrameSize, + Position = layoutData.FramePosition, + ZIndex = zIndex, + }, { + Avatar = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Size = layoutData.Size, + Position = layoutData.Position, + Image = user and user.thumbnails and user.thumbnails[THUMBNAIL_IMAGE_TYPE] + and user.thumbnails[THUMBNAIL_IMAGE_TYPE][THUMBNAIL_IMAGE_SIZE] or DEFAULT_THUMBNAIL_ICON, + ZIndex = zIndex, + }) + }) + + -- Render any borders between avatars + if layoutData.Border then + thumbnailContents["Border-"..i] = Roact.createElement("Frame", { + Size = layoutData.Border.BorderSize, + Position = layoutData.Border.BorderPosition, + BorderSizePixel = 0, + BackgroundColor3 = BORDER_COLOR, + ZIndex = zIndex, + }) + end + end + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = size, + LayoutOrder = layoutOrder, + ZIndex = zIndex, + }, { + ContentsContainer = Roact.createElement("Frame", { + -- Render the border inwards instead of outwards + Size = UDim2.new(1, -BORDER_SIZE, 1, -BORDER_SIZE), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundColor3 = Constants.Color.WHITE, + BorderColor3 = BORDER_COLOR, + BorderSizePixel = BORDER_SIZE, + ZIndex = zIndex, + }, thumbnailContents), + }) +end + +return ConversationThumbnail diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/Header.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/Header.lua new file mode 100644 index 0000000..1e9a1e9 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/Header.lua @@ -0,0 +1,67 @@ +local CorePackages = game:GetService("CorePackages") + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(CorePackages.Roact) + +local ShareGame = Modules.Settings.Pages.ShareGame +local Constants = require(ShareGame.Constants) +local BackButton = require(ShareGame.Components.BackButton) +local SearchArea = require(ShareGame.Components.SearchArea) + +local PAGE_TITLE = "Invite Friends" + +local Header = Roact.PureComponent:extend("Header") + +function Header:render() + local deviceLayout = self.props.deviceLayout + local size = self.props.size + local layoutOrder = self.props.layoutOrder + local zIndex = self.props.zIndex + local closePage = self.props.closePage + local searchAreaActive = self.props.searchAreaActive + + local layoutSpecific = Constants.LayoutSpecific[deviceLayout] + local isDesktop = deviceLayout == Constants.DeviceLayout.DESKTOP + local isSearchingOnMobile = (not isDesktop) and searchAreaActive + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = size, + AnchorPoint = Vector2.new(0, 1), + LayoutOrder = layoutOrder, + ZIndex = zIndex, + }, { + Title = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Visible = not isSearchingOnMobile, + Position = UDim2.new(0.5, 0, 0.5, 0), + Text = PAGE_TITLE, + TextSize = layoutSpecific.PAGE_TITLE_TEXT_SIZE, + TextColor3 = Constants.Color.WHITE, + Font = Enum.Font.SourceSansSemibold, + ZIndex = self.props.zIndex, + }), + BackButton = Roact.createElement(BackButton, { + isArrow = not isDesktop, + visible = not isSearchingOnMobile, + position = UDim2.new(0, 0, 0.5, 0), + size = UDim2.new( + 0, layoutSpecific.BACK_BUTTON_WIDTH, + 0, layoutSpecific.BACK_BUTTON_HEIGHT + ), + anchorPoint = Vector2.new(0, 0.5), + zIndex = zIndex, + onClick = closePage, + }), + SearchArea = Roact.createElement(SearchArea, { + fullWidthSearchBar = not isDesktop, + searchBoxMargin = layoutSpecific.SEARCH_BOX_MARGIN, + anchorPoint = Vector2.new(1, 0.5), + position = UDim2.new(1, 0, 0.5, 0), + zIndex = zIndex, + }) + }) +end + +return Header diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/IconButton.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/IconButton.lua new file mode 100644 index 0000000..c718cfe --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/IconButton.lua @@ -0,0 +1,75 @@ + +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) + + +local IconButton = Roact.PureComponent:extend("IconButton") + +function IconButton:render() + local visible = self.props.visible + local anchorPoint = self.props.anchorPoint + local position = self.props.position + local size = self.props.size + local zIndex = self.props.zIndex + local onClick = self.props.onClick + + local iconHorizontalAlignment = self.props.iconHorizontalAlignment or Enum.HorizontalAlignment.Center + local iconVerticalAlignment = self.props.iconVerticalAlignment or Enum.VerticalAlignment.Center + local iconSpritePath = self.props.iconSpritePath + local iconSpriteFrame = self.props.iconSpriteFrame + + local horizontalInset = self.props.horizontalInset or 12 + local offsetLayoutOrder = 0 + local iconLayoutOrder = 1 + local iconSize = UDim2.new(0, iconSpriteFrame.size.X, 0, iconSpriteFrame.size.Y) + + if iconHorizontalAlignment == Enum.HorizontalAlignment.Center then + horizontalInset = 0 + elseif iconHorizontalAlignment == Enum.HorizontalAlignment.Left then + position = UDim2.new(position.X.Scale, position.X.Offset - horizontalInset, position.Y.Scale, position.Y.Offset) + elseif iconHorizontalAlignment == Enum.HorizontalAlignment.Right then + position = UDim2.new(position.X.Scale, position.X.Offset + horizontalInset, position.Y.Scale, position.Y.Offset) + + offsetLayoutOrder = 1 + iconLayoutOrder = 0 + end + + size = UDim2.new(size.X.Scale, size.X.Offset + horizontalInset, size.Y.Scale, size.Y.Offset) + + return Roact.createElement("ImageButton", { + Visible = visible, + BackgroundTransparency = 1, + BorderSizePixel = 0, + AnchorPoint = anchorPoint, + Position = position, + Size = size, + ZIndex = zIndex, + [Roact.Event.Activated] = onClick, + }, { + IconLayout = Roact.createElement("UIListLayout", { + HorizontalAlignment = iconHorizontalAlignment, + VerticalAlignment = iconVerticalAlignment, + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Offset = Roact.createElement("Frame", { + BorderSizePixel = 0, + + LayoutOrder = offsetLayoutOrder, + Size = UDim2.new(0, horizontalInset, 0, 0), + }), + BackIcon = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, iconSpriteFrame.size.X, 0, iconSpriteFrame.size.Y), + + Image = iconSpritePath, + ImageRectOffset = iconSpriteFrame.offset, + ImageRectSize = iconSpriteFrame.size, + ZIndex = zIndex, + LayoutOrder = iconLayoutOrder, + }), + }) +end + +return IconButton diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/InviteButton.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/InviteButton.lua new file mode 100644 index 0000000..08e6bc8 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/InviteButton.lua @@ -0,0 +1,63 @@ +local CorePackages = game:GetService("CorePackages") +local AppTempCommon = CorePackages.AppTempCommon + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(CorePackages.Roact) + +local ShareGame = Modules.Settings.Pages.ShareGame +local Immutable = require(AppTempCommon.Common.Immutable) +local Constants = require(ShareGame.Constants) +local RectangleButton = require(ShareGame.Components.RectangleButton) + +local INVITE_TEXT_FONT = Enum.Font.SourceSansSemibold +local INVITE_TEXT_SIZE = 19 +local INVITE_TEXT = "Invite" +local INVITE_SENT_TEXT = "Invited" + +local InviteButton = Roact.PureComponent:extend("InviteButton") + +function InviteButton:render() + local anchorPoint = self.props.anchorPoint + local position = self.props.position + local size = self.props.size + local layoutOrder = self.props.layoutOrder + local zIndex = self.props.zIndex + local onInvite = self.props.onInvite + + local alreadyInvited = self.props.alreadyInvited + local inviteText = alreadyInvited and INVITE_SENT_TEXT or INVITE_TEXT + + if not alreadyInvited then + local buttonProps = Immutable.Set(self.props, "onClick", function() + onInvite() + end) + + return Roact.createElement(RectangleButton, buttonProps, { + InviteText = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Position = UDim2.new(0.5, 0, 0.5, 0), + Font = INVITE_TEXT_FONT, + Text = inviteText, + TextSize = INVITE_TEXT_SIZE, + TextColor3 = Constants.Color.WHITE, + ZIndex = zIndex, + }) + }) + else + return Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + AnchorPoint = anchorPoint, + Position = position, + Size = size, + Font = INVITE_TEXT_FONT, + Text = inviteText, + TextSize = INVITE_TEXT_SIZE, + TextColor3 = Constants.Color.WHITE, + LayoutOrder = layoutOrder, + ZIndex = zIndex, + }) + end +end + +return InviteButton diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/LayoutProvider.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/LayoutProvider.lua new file mode 100644 index 0000000..2619567 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/LayoutProvider.lua @@ -0,0 +1,133 @@ +local CorePackages = game:GetService("CorePackages") +local AppTempCommon = CorePackages.AppTempCommon + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local UserInputService = game:GetService("UserInputService") + +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) + +local ShareGame = Modules.Settings.Pages.ShareGame +local Constants = require(ShareGame.Constants) + +local SetDeviceOrientation = require(AppTempCommon.LuaApp.Actions.SetDeviceOrientation) +local SetIsSmallTouchScreen = require(ShareGame.Actions.SetIsSmallTouchScreen) +local SetDeviceLayout = require(ShareGame.Actions.SetDeviceLayout) + +-- Magic values derived from CoreScripts/Modules/Settings/Utility.lua +local SMALL_DEVICE_HEIGHT = 500 +local SMALL_DEVICE_WIDTH = 700 + +local LayoutProvider = Roact.Component:extend("LayoutProvider") + +function LayoutProvider:didMount() + if workspace.CurrentCamera then + self:setObservedCamera(workspace.CurrentCamera) + end + + self.cameraChangedListener = workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function() + if workspace.CurrentCamera then + self:setObservedCamera(workspace.CurrentCamera) + end + end) +end + +function LayoutProvider:willUnmount() + if self.cameraChangedListener then + self.cameraChangedListener:Disconnect() + end + + if self.viewportSizeListener then + self.viewportSizeListener:Disconnect() + end +end + +function LayoutProvider:setObservedCamera(camera) + if self.viewportSizeListener then + self.viewportSizeListener:Disconnect() + end + + -- Listen for changes to ViewportSize and update DeviceInfo accordingly + self:checkAllDeviceInfo(camera.ViewportSize) + self.viewportSizeListener = camera:GetPropertyChangedSignal("ViewportSize"):Connect(function() + -- Hacky code awaits underlying mechanism fix. + -- Viewport will get a 0,0,1,1 rect before it is properly set. + local viewportSize = camera.ViewportSize + if viewportSize.X <= 1 or viewportSize.Y <= 1 then + return + end + + self:checkAllDeviceInfo(viewportSize) + end) +end + +function LayoutProvider:checkDeviceOrientation(viewportSize) + local deviceOrientation = viewportSize.X > viewportSize.Y and + Constants.DeviceOrientation.LANDSCAPE or Constants.DeviceOrientation.PORTRAIT + + if self._deviceOrientation ~= deviceOrientation then + self._deviceOrientation = deviceOrientation + self.props.setDeviceOrientation(deviceOrientation) + end +end + +function LayoutProvider:checkDeviceIsSmallTouchScreen(viewportSize) + local isSmallTouchScreen = UserInputService.TouchEnabled and + (viewportSize.X < SMALL_DEVICE_WIDTH or viewportSize.Y < SMALL_DEVICE_HEIGHT) + + if self._isSmallTouchScreen ~= isSmallTouchScreen then + self._isSmallTouchScreen = isSmallTouchScreen + self.props.setIsSmallTouchScreen(self._isSmallTouchScreen) + end +end + +function LayoutProvider:checkDeviceLayout() + local deviceLayout + + if self._isSmallTouchScreen then + if self._deviceOrientation == Constants.DeviceOrientation.LANDSCAPE then + deviceLayout = Constants.DeviceLayout.PHONE_LANDSCAPE + else + deviceLayout = Constants.DeviceLayout.PHONE_PORTRAIT + end + elseif UserInputService.TouchEnabled then + if self._deviceOrientation == Constants.DeviceOrientation.LANDSCAPE then + deviceLayout = Constants.DeviceLayout.TABLET_LANDSCAPE + else + deviceLayout = Constants.DeviceLayout.TABLET_PORTRAIT + end + else + deviceLayout = Constants.DeviceLayout.DESKTOP + end + + if self._deviceLayout ~= deviceLayout then + self._deviceLayout = deviceLayout + self.props.setDeviceLayout(self._deviceLayout) + end +end + +function LayoutProvider:checkAllDeviceInfo(viewportSize) + self:checkDeviceOrientation(viewportSize) + self:checkDeviceIsSmallTouchScreen(viewportSize) + self:checkDeviceLayout() +end + +function LayoutProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +local connector = RoactRodux.connect(function(store) + return { + setDeviceOrientation = function(orientation) + return store:dispatch(SetDeviceOrientation(orientation)) + end, + setIsSmallTouchScreen = function(isSmall) + return store:dispatch(SetIsSmallTouchScreen(isSmall)) + end, + setDeviceLayout = function(deviceLayout) + return store:dispatch(SetDeviceLayout(deviceLayout)) + end, + } +end) + +return connector(LayoutProvider) diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/NoFriendsPage.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/NoFriendsPage.lua new file mode 100644 index 0000000..18ee66e --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/NoFriendsPage.lua @@ -0,0 +1,76 @@ +local CorePackages = game:GetService("CorePackages") + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local ShareGame = Modules.Settings.Pages.ShareGame + +local Roact = require(CorePackages.Roact) +local Constants = require(ShareGame.Constants) + +local ShareGameIcons = require(ShareGame.Spritesheets.ShareGameIcons) +local FRIENDS_ICON_FRAME = ShareGameIcons:GetFrame("friends") +local SHARE_GAME_ICONS_IMAGE = ShareGameIcons:GetImagePath() + +local ICON_TO_SUBTITLE_PADDING = 34 + +local NoFriendsPage = Roact.PureComponent:extend("NoFriendsPage") + +function NoFriendsPage:render() + local layoutOrder = self.props.LayoutOrder + local zIndex = self.props.ZIndex + + local incrementingLayoutOrder = 0 + local function incrementLayoutOrder() + incrementingLayoutOrder = incrementingLayoutOrder + 1 + return incrementingLayoutOrder + end + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + LayoutOrder = layoutOrder, + ZIndex = zIndex, + }, { + listLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + friendsIcon = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Image = SHARE_GAME_ICONS_IMAGE, + ImageRectOffset = FRIENDS_ICON_FRAME.offset, + ImageRectSize = FRIENDS_ICON_FRAME.size, + Size = UDim2.new(0, 72, 0, 72), + LayoutOrder = incrementLayoutOrder(), + ZIndex = zIndex, + }), + iconToSubtitleSpacer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 0, 0, ICON_TO_SUBTITLE_PADDING), + LayoutOrder = incrementLayoutOrder(), + ZIndex = zIndex, + }), + subtitle = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Text = "Make friends so you can invite them to play with you!", + TextColor3 = Constants.Color.GRAY5, + TextTransparency = 0.22, + TextSize = 21, + TextWrapped = true, + Font = Enum.Font.SourceSans, + LayoutOrder = incrementLayoutOrder(), + Size = UDim2.new(0, 280, 0, 42), + TextYAlignment = Enum.TextYAlignment.Top, + ZIndex = zIndex, + }), + bottomSpacer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 0, 0, 120), + LayoutOrder = incrementLayoutOrder(), + ZIndex = zIndex, + }), + }) +end + +return NoFriendsPage diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/RectangleButton.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/RectangleButton.lua new file mode 100644 index 0000000..c35b7f1 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/RectangleButton.lua @@ -0,0 +1,88 @@ +local CorePackages = game:GetService("CorePackages") + +local UserInputService = game:GetService("UserInputService") + +local Roact = require(CorePackages.Roact) + +local BUTTON_IMAGE = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButton.png" +local BUTTON_IMAGE_ACTIVE = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png" +local BUTTON_SLICE = Rect.new(8, 6, 46, 44) + +local DROPSHADOW_SIZE = { + Left = 4, Right = 4, + Top = 2, Bottom = 6, +} + +local RectangleButton = Roact.PureComponent:extend("RectangleButton") + +function RectangleButton:init() + self.state = { + isHovering = false, + } +end + +function RectangleButton:render() + local size = self.props.size + local position = self.props.position + local anchorPoint = self.props.anchorPoint + local layoutOrder = self.props.layoutOrder + local zIndex = self.props.zIndex + local onClick = self.props.onClick + local children = self.props[Roact.Children] or {} + + local buttonImage = self.state.isHovering and BUTTON_IMAGE_ACTIVE or BUTTON_IMAGE + + -- Insert padding so that child elements of this component are positioned + -- inside the button as expected. This is to offset the dropshadow + -- extending outside the button bounds. + children["UIPadding"] = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, DROPSHADOW_SIZE.Left), + PaddingRight = UDim.new(0, DROPSHADOW_SIZE.Right), + PaddingTop = UDim.new(0, DROPSHADOW_SIZE.Top), + PaddingBottom = UDim.new(0, DROPSHADOW_SIZE.Bottom), + }) + + return Roact.createElement("ImageButton", { + BackgroundTransparency = 1, + Image = "", + Size = size, + Position = position, + AnchorPoint = anchorPoint, + LayoutOrder = layoutOrder, + ZIndex = zIndex, + [Roact.Event.MouseEnter] = function() + if not UserInputService.TouchEnabled then + self:setState({isHovering = true}) + end + end, + [Roact.Event.MouseLeave] = function() + if not UserInputService.TouchEnabled then + self:setState({isHovering = false}) + end + end, + [Roact.Event.Activated] = function() + if onClick then + self:setState({isHovering = false}) + onClick() + end + end, + }, { + ButtonBackground = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Position = UDim2.new( + 0, -DROPSHADOW_SIZE.Left, + 0, -DROPSHADOW_SIZE.Top + ), + Size = UDim2.new( + 1, DROPSHADOW_SIZE.Left + DROPSHADOW_SIZE.Right, + 1, DROPSHADOW_SIZE.Top + DROPSHADOW_SIZE.Bottom + ), + Image = buttonImage, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = BUTTON_SLICE, + ZIndex = zIndex, + }, children), + }) +end + +return RectangleButton diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/SearchArea.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/SearchArea.lua new file mode 100644 index 0000000..6aa9767 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/SearchArea.lua @@ -0,0 +1,197 @@ +local CorePackages = game:GetService("CorePackages") +local AppTempCommon = CorePackages.AppTempCommon + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) + +local ShareGame = Modules.Settings.Pages.ShareGame +local SearchBox = require(ShareGame.Components.SearchBox) +local IconButton = require(ShareGame.Components.IconButton) +local Constants = require(ShareGame.Constants) +local Text = require(AppTempCommon.Common.Text) + +local SetSearchAreaActive = require(ShareGame.Actions.SetSearchAreaActive) +local SetSearchText = require(ShareGame.Actions.SetSearchText) +local ShareGameIcons = require(Modules.Settings.Pages.ShareGame.Spritesheets.ShareGameIcons) + +local SEARCH_ICON_SPRITE_PATH = ShareGameIcons:GetImagePath() +local SEARCH_ICON_SPRITE_FRAME = ShareGameIcons:GetFrame("search_large") +local SEARCH_ICON_SIZE = 44 + +local SEARCH_BOX_WIDTH = 177 +local SEARCH_BOX_HEIGHT = 28 + +local CANCEL_TEXT_FONT = Enum.Font.SourceSans +local CANCEL_TEXT = "Cancel" +local CANCEL_TEXT_COLOR = Constants.Color.GRAY3 +local CANCEL_TEXT_SIZE = 20 + +local SearchArea = Roact.PureComponent:extend("SearchArea") + +function SearchArea:init() + self.state = { + cancelText = CANCEL_TEXT, + } + self.searchField = nil +end + +function SearchArea:render() + local fullWidthSearchBar = self.props.fullWidthSearchBar + local searchBoxMargin = self.props.searchBoxMargin + local anchorPoint = self.props.anchorPoint + local position = self.props.position + local zIndex = self.props.zIndex + local searchAreaActive = self.props.searchAreaActive + + local cancelText = self.state.cancelText + local cancelTextWidth = Text.GetTextWidth(cancelText, CANCEL_TEXT_FONT, CANCEL_TEXT_SIZE) + local searchBoxSizeOffset = searchBoxMargin + cancelTextWidth + + local setSearchText = self.props.setSearchText + + if fullWidthSearchBar then + -- When full-width is true, the search box spans the entire width of the + -- parent frame, and it becomes hidden behind a search button. + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + Position = position, + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ZIndex = zIndex, + }, { + -- Render the search button if the search area hasn't been activated + -- yet. + SearchButton = Roact.createElement(IconButton, { + visible = not searchAreaActive, + anchorPoint = anchorPoint, + position = position, + size = UDim2.new(0, SEARCH_ICON_SIZE, 0, SEARCH_ICON_SIZE), + zIndex = zIndex, + + iconHorizontalAlignment = Enum.HorizontalAlignment.Right, + iconSpritePath = SEARCH_ICON_SPRITE_PATH, + iconSpriteFrame = SEARCH_ICON_SPRITE_FRAME, + onClick = function(rbx) + self.props.setSearchAreaActive(true) + end, + }), + + -- Show search box with cancel button if the search area is active + SearchBox = Roact.createElement(SearchBox, { + anchorPoint = Vector2.new(0, 0.5), + position = UDim2.new(0, 0, 0.5, 0), + size = UDim2.new(1, -searchBoxSizeOffset, 0, SEARCH_BOX_HEIGHT), + zIndex = zIndex, + visible = searchAreaActive, + modalFocused = searchAreaActive, + onTextChanged = setSearchText, + onTextBoxFocusLost = function(text) + if text == "" then + self.props.setSearchAreaActive(false) + end + end, + searchFieldRef = function(rbx) + self.searchField = rbx + end, + }), + Cancel = Roact.createElement("TextButton", { + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(1, 0.5), + Position = UDim2.new(1, 0, 0.5, 0), + Size = UDim2.new(0, cancelTextWidth, 1, 0), + TextSize = CANCEL_TEXT_SIZE, + TextColor3 = CANCEL_TEXT_COLOR, + Font = CANCEL_TEXT_FONT, + ZIndex = zIndex, + Visible = searchAreaActive, + + [Roact.Ref] = function(rbx) + if rbx then + -- Set initial text on this TextLabel + rbx.Text = cancelText + + -- Listen to Text changed events in case of localization + -- (this is so we can trigger a resize) + if not self.textConnection then + local textChangedSignal = rbx:GetPropertyChangedSignal("Text") + + self.textConnection = textChangedSignal:connect(function() + self:setState({ + cancelText = rbx.Text + }) + end) + end + end + end, + [Roact.Event.Activated] = function(rbx) + self.props.setSearchAreaActive(false) + end, + }), + }) + else + -- Show a search box of a fixed size without any added behavior. + -- We don't bother setting SearchAreaActive here since we're not working + -- with a modal-style search box (i.e. nothing has to be shown or + -- hidden). + return Roact.createElement(SearchBox, { + anchorPoint = Vector2.new(1, 0.5), + position = UDim2.new(1, 0, 0.5, 0), + size = UDim2.new(0, SEARCH_BOX_WIDTH, 0, SEARCH_BOX_HEIGHT), + zIndex = zIndex, + onTextChanged = setSearchText, + searchFieldRef = function(rbx) + self.searchField = rbx + end, + }) + end +end + +function SearchArea:didUpdate(prevProps) + local fullWidthSearchBar = self.props.fullWidthSearchBar + + if self.searchField then + -- Check if the page was closed so we can reset the search query + if not self.props.isPageOpen and prevProps.isPageOpen then + -- Spawn new thread so we don't trigger state updates from this + -- function + spawn(function() + -- Reset the entire search area + if fullWidthSearchBar then + self.props.setSearchAreaActive(false) + else + self.searchField.Text = "" + end + end) + end + + -- Check if the search area has become active/inactive + if fullWidthSearchBar then + local searchAreaActive = self.props.searchAreaActive + local wasActive = prevProps.searchAreaActive + + if searchAreaActive and not wasActive then + self.searchField:CaptureFocus() + elseif not searchAreaActive and wasActive then + self.searchField.Text = "" + end + end + end +end + +SearchArea = RoactRodux.connect(function(store) + local state = store:getState() + return { + isPageOpen = state.Page.IsOpen, + searchAreaActive = state.ConversationsSearch.SearchAreaActive, + setSearchAreaActive = function(isActive) + store:dispatch(SetSearchAreaActive(isActive)) + end, + setSearchText = function(text) + store:dispatch(SetSearchText(text)) + end, + } +end)(SearchArea) + +return SearchArea diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/SearchBox.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/SearchBox.lua new file mode 100644 index 0000000..4643da5 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/SearchBox.lua @@ -0,0 +1,154 @@ +local CorePackages = game:GetService("CorePackages") + +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(CorePackages.Roact) +local Constants = require(Modules.Settings.Pages.ShareGame.Constants) + +local SearchBox = Roact.Component:extend("SearchBox") + +local ShareGameIcons = require(Modules.Settings.Pages.ShareGame.Spritesheets.ShareGameIcons) +local SHARE_GAME_SPRITE_PATH = ShareGameIcons:GetImagePath() + +local SEARCH_BORDER_SPRITE_IMAGE = SHARE_GAME_SPRITE_PATH +local SEARCH_BORDER_SPRITE_FRAME = ShareGameIcons:GetFrame("search_border") +local SEARCH_BORDER_SLICE = Rect.new(3, 3, 4, 4) + +local SEARCH_ICON_SPRITE_IMAGE = SHARE_GAME_SPRITE_PATH +local SEARCH_ICON_SPRITE_FRAME = ShareGameIcons:GetFrame("search_small") +local SEARCH_ICON_SIZE = 16 + +local CLEAR_ICON_SPRITE_IMAGE = SHARE_GAME_SPRITE_PATH +local CLEAR_ICON_SPRITE_FRAME = ShareGameIcons:GetFrame("clear") +local CLEAR_ICON_SIZE = 16 + +local SEARCH_MARGINS_HORIZONTAL = 8 +local SEARCH_MARGINS_VERTICAL = 6 +local SEARCH_FIELD_MARGINS = 12 + +local SEARCH_BOX_TEXT_SIZE = 16 +local SEARCH_PLACEHOLDER_TEXT = "Search for friends" + +function SearchBox:init() + self.state = { + isTextWritten = false, + } + self.searchField = nil +end + +function SearchBox:render() + local anchorPoint = self.props.anchorPoint + local onTextBoxFocusLost = self.props.onTextBoxFocusLost + local position = self.props.position + local searchFieldRef = self.props.searchFieldRef + local size = self.props.size + local visible = self.props.visible + local zIndex = self.props.zIndex + + + local isTextWritten = self.state.isTextWritten + return Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + AnchorPoint = anchorPoint, + Position = position, + Size = size, + Visible = visible, + Image = SEARCH_BORDER_SPRITE_IMAGE, + ImageRectOffset = SEARCH_BORDER_SPRITE_FRAME.offset, + ImageRectSize = SEARCH_BORDER_SPRITE_FRAME.size, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = SEARCH_BORDER_SLICE, + ZIndex = zIndex, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, SEARCH_MARGINS_HORIZONTAL), + PaddingRight = UDim.new(0, SEARCH_MARGINS_HORIZONTAL), + PaddingTop = UDim.new(0, SEARCH_MARGINS_VERTICAL), + PaddingBottom = UDim.new(0, SEARCH_MARGINS_VERTICAL), + }), + SearchIcon = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Image = SEARCH_ICON_SPRITE_IMAGE, + ImageRectOffset = SEARCH_ICON_SPRITE_FRAME.offset, + ImageRectSize = SEARCH_ICON_SPRITE_FRAME.size, + Size = UDim2.new(0, SEARCH_ICON_SIZE, 0, SEARCH_ICON_SIZE), + ZIndex = zIndex + }), + SearchField = Roact.createElement("TextBox", { + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(0, 0.5), + Position = UDim2.new(0, SEARCH_ICON_SIZE + SEARCH_FIELD_MARGINS, 0.5, 0), + Size = UDim2.new( + 1, -(SEARCH_ICON_SIZE + CLEAR_ICON_SIZE + SEARCH_FIELD_MARGINS * 2), + 1, 0 + ), + ClearTextOnFocus = false, + PlaceholderColor3 = Constants.Color.GREY3, + PlaceholderText = SEARCH_PLACEHOLDER_TEXT, + Text = "", + TextColor3 = Constants.Color.WHITE, + TextWrapped = true, + TextXAlignment = Enum.TextXAlignment.Left, + TextSize = SEARCH_BOX_TEXT_SIZE, + Font = Enum.Font.SourceSans, + ZIndex = zIndex, + [Roact.Ref] = function(rbx) + self.searchField = rbx + + -- Check if a higher reference function has been passed in and + -- then call it. + if searchFieldRef then + searchFieldRef(rbx) + end + end, + [Roact.Event.FocusLost] = function(rbx) + if onTextBoxFocusLost then + onTextBoxFocusLost(rbx.Text) + end + end + }), + ClearButton = Roact.createElement("ImageButton", { + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(1, 0), + Position = UDim2.new(1, 0, 0, 0), + Size = UDim2.new(0, CLEAR_ICON_SIZE, 0, SEARCH_ICON_SIZE), + Image = CLEAR_ICON_SPRITE_IMAGE, + ImageRectOffset = CLEAR_ICON_SPRITE_FRAME.offset, + ImageRectSize = CLEAR_ICON_SPRITE_FRAME.size, + ZIndex = zIndex, + Visible = isTextWritten, + [Roact.Event.Activated] = function() + if self.searchField then + self.searchField.Text = "" + + -- We lost focus when clicking this button so we have to + -- recapture it. + self.searchField:CaptureFocus() + end + end + }) + }) +end + +function SearchBox:didMount() + local searchField = self.searchField + local onTextChanged = self.props.onTextChanged + + if searchField then + self.textBoxChangedConnection = searchField:GetPropertyChangedSignal("Text"):Connect(function() + local text = searchField.Text + self:setState({ + isTextWritten = text:len() > 0 + }) + onTextChanged(text) + end) + end +end + +function SearchBox:willUnmount() + if self.textBoxChangedConnection then + self.textBoxChangedConnection:Disconnect() + end +end + +return SearchBox diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ShareGamePageFrame.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ShareGamePageFrame.lua new file mode 100644 index 0000000..a497466 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ShareGamePageFrame.lua @@ -0,0 +1,92 @@ +--[[ + Container for both the Share Game Page contents and header +]] + +local CorePackages = game:GetService("CorePackages") +local HttpRbxApiService = game:GetService("HttpRbxApiService") +local AppTempCommon = CorePackages.AppTempCommon + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local Players = game:GetService("Players") + +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) +local httpRequest = require(AppTempCommon.Temp.httpRequest) +local ApiFetchUsersFriends = require(AppTempCommon.LuaApp.Thunks.ApiFetchUsersFriends) + +local ShareGame = Modules.Settings.Pages.ShareGame + +local Header = require(ShareGame.Components.Header) +local ConversationList = require(ShareGame.Components.ConversationList) +local Constants = require(ShareGame.Constants) + +local ClosePage = require(ShareGame.Actions.ClosePage) + +local USER_LIST_PADDING = 10 + +local ShareGamePageFrame = Roact.PureComponent:extend("ShareGamePageFrame") + +function ShareGamePageFrame:didMount() + self.props.reFetch() +end + +function ShareGamePageFrame:render() + local isSmallTouchScreen = self.props.isSmallTouchScreen + local deviceOrientation = self.props.deviceOrientation + local deviceLayout = self.props.deviceLayout + local zIndex = self.props.zIndex + local closePage = self.props.closePage + local searchAreaActive = self.props.searchAreaActive + local searchText = self.props.searchText + + local layoutSpecific = Constants.LayoutSpecific[deviceLayout] + local headerHeight = layoutSpecific.HEADER_HEIGHT + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + ZIndex = zIndex, + }, { + Header = Roact.createElement(Header, { + deviceLayout = deviceLayout, + size = UDim2.new(1, 0, 0, headerHeight), + position = UDim2.new(0, 0, 0, -headerHeight), + layoutOrder = 0, + zIndex = zIndex, + closePage = closePage, + searchAreaActive = searchAreaActive, + }), + ConversationList = Roact.createElement(ConversationList, { + size = UDim2.new(1, 0, 1, layoutSpecific.EXTEND_BOTTOM_SIZE - USER_LIST_PADDING), + topPadding = USER_LIST_PADDING, + layoutOrder = 1, + zIndex = zIndex, + searchText = searchText, + }), + }) +end + +ShareGamePageFrame = RoactRodux.connect(function(store) + local state = store:getState() + return { + isSmallTouchScreen = state.DeviceInfo.IsSmallTouchScreen, + deviceOrientation = state.DeviceInfo.DeviceOrientation, + deviceLayout = state.DeviceInfo.DeviceLayout, + + searchAreaActive = state.ConversationsSearch.SearchAreaActive, + searchText = state.ConversationsSearch.SearchText, + + closePage = function() + store:dispatch(ClosePage(Constants.PageRoute.SHARE_GAME)) + end, + + reFetch = function() + local userId = tostring(Players.LocalPlayer.UserId) + local networkImpl = httpRequest(HttpRbxApiService) + store:dispatch(ApiFetchUsersFriends(networkImpl, userId, Constants.ThumbnailRequest.InviteToGameHeadshot)) + end + } +end)(ShareGamePageFrame) + +return ShareGamePageFrame diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Constants.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Constants.lua new file mode 100644 index 0000000..96a76ea --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Constants.lua @@ -0,0 +1,118 @@ +local CorePackages = game:GetService("CorePackages") +local AppTempCommon = CorePackages.AppTempCommon + +local User = require(AppTempCommon.LuaApp.Models.User) +local ThumbnailRequest = require(AppTempCommon.LuaApp.Models.ThumbnailRequest) + +local DeviceLayout = { + PHONE_PORTRAIT = "PHONE_PORTRAIT", + PHONE_LANDSCAPE = "PHONE_LANDSCAPE", + TABLET_PORTRAIT = "TABLET_PORTRAIT", + TABLET_LANDSCAPE = "TABLET_LANDSCAPE", + DESKTOP = "DESKTOP", +} + +local Constants = { + Color = { + WHITE = Color3.fromRGB(255, 255, 255), + GRAY1 = Color3.fromRGB(25, 25, 25), + GRAY2 = Color3.fromRGB(117, 117, 117), + GRAY3 = Color3.fromRGB(184, 184, 184), + GRAY4 = Color3.fromRGB(227, 227, 227), + GRAY5 = Color3.fromRGB(242, 242, 242), + GRAY6 = Color3.fromRGB(245, 245, 245), + }, + PresenceColors = { + [User.PresenceType.ONLINE] = Color3.fromRGB(0, 162, 255), + [User.PresenceType.IN_GAME] = Color3.fromRGB(2, 183, 87), + [User.PresenceType.IN_STUDIO] = Color3.fromRGB(246, 136, 2), + [User.PresenceType.OFFLINE] = 0, + }, + PresenceText = { + [User.PresenceType.ONLINE] = "Online", + [User.PresenceType.IN_GAME] = "In game", + [User.PresenceType.IN_STUDIO] = "In studio", + }, + DeviceOrientation = { + PORTRAIT = "PORTRAIT", + LANDSCAPE = "LANDSCAPE", + INVALID = "INVALID", + }, + PageRoute = { + NONE = "NONE", + SETTINGS_HUB = "SETTINGS_HUB", + SHARE_GAME = "SHARE_GAME", + }, + + AvatarThumbnailTypes = { + AvatarThumbnail = "AvatarThumbnail", + HeadShot = "HeadShot", + }, + AvatarThumbnailSizes = { + Size150x150 = "Size150x150", + Size100x100 = "Size100x100", + }, + + SHARE_GAME_Z_INDEX = 2, + + --[[ + Used for determining how the ShareGame page will be rendered across + devices. + ]] + DeviceLayout = DeviceLayout, + LayoutSpecific = { + [DeviceLayout.PHONE_LANDSCAPE] = { + HEADER_HEIGHT = 40, + PAGE_TITLE_TEXT_SIZE = 23, + SEARCH_BOX_MARGIN = 12, + PAGE_SIDE_MARGINS = 7, + BACK_BUTTON_HEIGHT = 44, + BACK_BUTTON_WIDTH = 44, + EXTEND_BOTTOM_SIZE = 0, + }, + [DeviceLayout.PHONE_PORTRAIT] = { + HEADER_HEIGHT = 40, + PAGE_TITLE_TEXT_SIZE = 23, + SEARCH_BOX_MARGIN = 15, + PAGE_SIDE_MARGINS = 5, + BACK_BUTTON_HEIGHT = 44, + BACK_BUTTON_WIDTH = 44, + EXTEND_BOTTOM_SIZE = 0, + }, + [DeviceLayout.TABLET_PORTRAIT] = { + HEADER_HEIGHT = 40, + PAGE_TITLE_TEXT_SIZE = 23, + SEARCH_BOX_MARGIN = 15, + PAGE_SIDE_MARGINS = 15, + BACK_BUTTON_HEIGHT = 44, + BACK_BUTTON_WIDTH = 44, + EXTEND_BOTTOM_SIZE = 0, + }, + [DeviceLayout.TABLET_LANDSCAPE] = { + HEADER_HEIGHT = 60, + PAGE_TITLE_TEXT_SIZE = 23, + SEARCH_BOX_MARGIN = 15, + PAGE_SIDE_MARGINS = 5, + BACK_BUTTON_HEIGHT = 44, + BACK_BUTTON_WIDTH = 44, + EXTEND_BOTTOM_SIZE = 68, + }, + [DeviceLayout.DESKTOP] = { + HEADER_HEIGHT = 60, + PAGE_TITLE_TEXT_SIZE = 29, + SEARCH_BOX_MARGIN = 0, + PAGE_SIDE_MARGINS = 0, + BACK_BUTTON_HEIGHT = 44, + BACK_BUTTON_WIDTH = 154, + EXTEND_BOTTOM_SIZE = 68, + }, + }, +} + +Constants.ThumbnailRequest = { + InviteToGameHeadshot = {ThumbnailRequest.fromData( + Constants.AvatarThumbnailTypes.HeadShot, Constants.AvatarThumbnailSizes.Size100x100 + )}, +} + +return Constants \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Reducers/ConversationsSearch.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Reducers/ConversationsSearch.lua new file mode 100644 index 0000000..bb98806 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Reducers/ConversationsSearch.lua @@ -0,0 +1,25 @@ +local CoreGui = game:GetService("CoreGui") +local CorePackages = game:GetService("CorePackages") +local AppTempCommon = CorePackages.AppTempCommon + +local Modules = CoreGui.RobloxGui.Modules +local ShareGame = Modules.Settings.Pages.ShareGame + +local Immutable = require(AppTempCommon.Common.Immutable) +local SetSearchAreaActive = require(ShareGame.Actions.SetSearchAreaActive) +local SetSearchText = require(ShareGame.Actions.SetSearchText) + +return function(state, action) + state = state or { + SearchAreaActive = false, + SearchText = "", + } + + if action.type == SetSearchAreaActive.name then + state = Immutable.Set(state, "SearchAreaActive", action.isActive) + elseif action.type == SetSearchText.name then + state = Immutable.Set(state, "SearchText", action.searchText) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Reducers/DeviceInfo.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Reducers/DeviceInfo.lua new file mode 100644 index 0000000..ececd70 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Reducers/DeviceInfo.lua @@ -0,0 +1,30 @@ +local CorePackages = game:GetService("CorePackages") +local AppTempCommon = CorePackages.AppTempCommon + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local ShareGame = Modules.Settings.Pages.ShareGame + +local Immutable = require(AppTempCommon.Common.Immutable) +local Constants = require(ShareGame.Constants) + +local SetDeviceLayout = require(ShareGame.Actions.SetDeviceLayout) +local SetDeviceOrientation = require(AppTempCommon.LuaApp.Actions.SetDeviceOrientation) +local SetIsSmallTouchScreen = require(ShareGame.Actions.SetIsSmallTouchScreen) + +return function(state, action) + state = state or { + DeviceLayout = Constants.DeviceLayout.DESKTOP, + DeviceOrientation = Constants.DeviceOrientation.INVALID, + IsSmallTouchScreen = false, + } + + if action.type == SetDeviceOrientation.name then + state = Immutable.Set(state, "DeviceOrientation", action.deviceOrientation) + elseif action.type == SetDeviceLayout.name then + state = Immutable.Set(state, "DeviceLayout", action.deviceLayout) + elseif action.type == SetIsSmallTouchScreen.name then + state = Immutable.Set(state, "IsSmallTouchScreen", action.isSmallTouchScreen) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Reducers/Invites.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Reducers/Invites.lua new file mode 100644 index 0000000..5a5ad28 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Reducers/Invites.lua @@ -0,0 +1,18 @@ +local CorePackages = game:GetService("CorePackages") +local AppTempCommon = CorePackages.AppTempCommon + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local ShareGame = Modules.Settings.Pages.ShareGame + +local Immutable = require(AppTempCommon.Common.Immutable) +local SetUserInvited = require(ShareGame.Actions.SetUserInvited) + +return function(state, action) + state = state or {} + + if action.type == SetUserInvited.name then + state = Immutable.Set(state, action.userId, action.isInvited) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Reducers/Page.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Reducers/Page.lua new file mode 100644 index 0000000..fee357e --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Reducers/Page.lua @@ -0,0 +1,31 @@ +local CorePackages = game:GetService("CorePackages") +local AppTempCommon = CorePackages.AppTempCommon + +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local ShareGame = Modules.Settings.Pages.ShareGame + +local Immutable = require(AppTempCommon.Common.Immutable) +local Constants = require(ShareGame.Constants) +local OpenPage = require(ShareGame.Actions.OpenPage) +local ClosePage = require(ShareGame.Actions.ClosePage) + +return function(state, action) + state = state or { + IsOpen = false, + Route = Constants.PageRoute.NONE, + } + + if action.type == OpenPage.name then + state = Immutable.JoinDictionaries(state, { + IsOpen = true, + Route = action.route or Constants.PageRoute.NONE, + }) + elseif action.type == ClosePage.name then + state = Immutable.JoinDictionaries(state, { + IsOpen = false, + Route = action.route or Constants.PageRoute.NONE, + }) + end + + return state +end \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Spritesheets/ShareGameIcons.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Spritesheets/ShareGameIcons.lua new file mode 100644 index 0000000..9319b85 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Spritesheets/ShareGameIcons.lua @@ -0,0 +1,40 @@ +local IMAGE_PATH = "rbxasset://textures/ui/Settings/ShareGame/icons.png" + +local function createFrameModel(offset, size) + return { + offset = offset, + size = size, + } +end + +local SHEET_MODEL = { + frames = { + back = createFrameModel(Vector2.new(2, 30), Vector2.new(24, 24)), + clear = createFrameModel(Vector2.new(6, 62), Vector2.new(16, 16)), + invite = createFrameModel(Vector2.new(2, 86), Vector2.new(24, 24)), + search_border = createFrameModel(Vector2.new(11, 11), Vector2.new(7, 7)), + search_large = createFrameModel(Vector2.new(3, 143), Vector2.new(22, 22)), + search_small = createFrameModel(Vector2.new(6, 117), Vector2.new(16, 16)), + friends = createFrameModel(Vector2.new(0, 170), Vector2.new(72, 72)), + }, +} + +local ShareGameIcons = {} + +function ShareGameIcons:GetFrame(key) + return SHEET_MODEL.frames[key] +end + +function ShareGameIcons:GetImagePath() + return IMAGE_PATH +end + +function ShareGameIcons:ApplyImage(guiObject, key) + local frameModel = self:GetFrame(key) + + guiObject.Image = IMAGE_PATH + guiObject.ImageRectOffset = frameModel.offset + guiObject.ImageRectSize = frameModel.size +end + +return ShareGameIcons diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGamePlaceholderPage.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGamePlaceholderPage.lua new file mode 100644 index 0000000..95cfd3e --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Pages/ShareGamePlaceholderPage.lua @@ -0,0 +1,64 @@ +--[[ + Placeholder page for SettingsHub that manages top level Roact view + + This page is a container and controller for the Roact app +--]] + +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") + +local ShareGame = RobloxGui.Modules.Settings.Pages.ShareGame +local Constants = require(ShareGame.Constants) +local OpenPage = require(ShareGame.Actions.OpenPage) +local ClosePage = require(ShareGame.Actions.ClosePage) + +local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory) +local this = settingsPageFactory:CreateNewPage() + +this.TabHeader = nil -- no tab for this page +this.PageListLayout.Parent = nil -- no list layout for this page +this.ShouldShowBottomBar = false +this.ShouldShowHubBar = false +this.IsPageClipped = false + +this.Page.Name = "ShareGameDummy" +this.Page.Size = UDim2.new(1, 0, 0, 0) + + +function this:ConnectHubToApp(settingsHub, shareGameApp) + this:SetHub(settingsHub) + + shareGameApp.store.changed:connect(function(state, prevState) + + local page = state.Page + local wasOpen = prevState.Page.IsOpen + + -- Check if the user closed the page via the Roact app. + if page.Route == Constants.PageRoute.SHARE_GAME and (wasOpen and not page.IsOpen) then + -- Close the page to sync up Settings Hub with the state change + this.HubRef:PopMenu(nil, true) + end + end) + + this.Displayed.Event:Connect(function() + local state = shareGameApp.store:getState() + if not state.Page.IsOpen then + -- Tell Roact app that the page was opened via Settings Hub + shareGameApp.store:dispatch(OpenPage(Constants.PageRoute.SETTINGS_HUB)) + end + end) + + this.Hidden.Event:Connect(function() + -- The user closed the page via the Settings Hub (instead of + -- pressing back on the page), so we have to sync the app state up + -- with the Settings Hub action. + local state = shareGameApp.store:getState() + if state.Page.IsOpen then + shareGameApp.store:dispatch(ClosePage(Constants.PageRoute.SETTINGS_HUB)) + end + end) + + shareGameApp.store:dispatch(ClosePage(Constants.PageRoute.SETTINGS_HUB)) +end + +return this \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/SettingsHub.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/SettingsHub.lua new file mode 100644 index 0000000..dfe67dc --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/SettingsHub.lua @@ -0,0 +1,1614 @@ +--[[ + Filename: SettingsHub.lua + Written by: jeditkacheff + Version 1.0 + Description: Controls the settings menu navigation and contains the settings pages +--]] + +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled() + +--[[ UTILITIES ]] +local utility = require(RobloxGui.Modules.Settings.Utility) +local VRHub = require(RobloxGui.Modules.VR.VRHub) + +--[[ CONSTANTS ]] +local SETTINGS_SHIELD_COLOR = Color3.new(41/255,41/255,41/255) +local SETTINGS_SHIELD_TRANSPARENCY = 0.2 +local SETTINGS_SHIELD_VR_TRANSPARENCY = 1 +local SETTINGS_SHIELD_SIZE = UDim2.new(1, 0, 1, 0) +local SETTINGS_SHIELD_INACTIVE_POSITION = UDim2.new(0,0,-1,-36) +local SETTINGS_SHIELD_ACTIVE_POSITION = UDim2.new(0, 0, 0, 0) +local SETTINGS_BASE_ZINDEX = 2 +local DEV_CONSOLE_ACTION_NAME = "Open Dev Console" +local QUICK_PROFILER_ACTION_NAME = "Show Quick Profiler" + +local VERSION_BAR_HEIGHT = isTenFootInterface and 32 or (utility:IsSmallTouchScreen() and 24 or 26) + +-- [[ FAST FLAGS ]] +local FFlagUseNotificationsLocalization = settings():GetFFlag('UseNotificationsLocalization') +local FFlagSettingsHubInviteToGame2 = settings():GetFFlag('SettingsHubInviteToGame2') +local FFlagSettingsHubBarsRefactor2 = settings():GetFFlag('SettingsHubBarsRefactor2') +local FFlagEnableNewDevConsole = settings():GetFFlag("EnableNewDevConsole") +local FFlagHelpMenuShowPlaceVersion = settings():GetFFlag("HelpMenuShowPlaceVersion") +local FFlagSettingsHubPlayersHorizontalScroll = settings():GetFFlag("SettingsHubPlayersHorizontalScroll") + +local enableResponsiveUIFixSuccess, enableResponsiveUIFixValue = pcall(function() return settings():GetFFlag("EnableResponsiveUIFix") end) +local FFlagEnableResponsiveUIFix = enableResponsiveUIFixSuccess and enableResponsiveUIFixValue + +--[[ SERVICES ]] +local RobloxReplicatedStorage = game:GetService("RobloxReplicatedStorage") +local ContentProvider = game:GetService("ContentProvider") +local StarterGui = game:GetService("StarterGui") +local ContextActionService = game:GetService("ContextActionService") +local GuiService = game:GetService("GuiService") +local UserInputService = game:GetService("UserInputService") +local RunService = game:GetService("RunService") +local VRService = game:GetService("VRService") +local HttpRbxApiService = game:GetService("HttpRbxApiService") +local HttpService = game:GetService("HttpService") +local Settings = UserSettings() +local GameSettings = Settings.GameSettings + + +--[[ REMOTES ]] +local GetServerVersionRemote = nil +spawn(function() + GetServerVersionRemote = RobloxReplicatedStorage:WaitForChild("GetServerVersion") +end) + +--[[ VARIABLES ]] +local isTouchDevice = UserInputService.TouchEnabled +RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface") +local platform = UserInputService:GetPlatform() + +local baseUrl = ContentProvider.BaseUrl +local isTestEnvironment = not string.find(baseUrl, "www.roblox.com") +local DeveloperConsoleModule = require(RobloxGui.Modules.DeveloperConsoleModule) +local DevConsoleMaster = require(RobloxGui.Modules.DevConsoleMaster) + +local lastInputChangedCon = nil +local chatWasVisible = false + +local connectedServerVersion = nil + +--[[ CORE MODULES ]] +local chat = require(RobloxGui.Modules.ChatSelector) + +if utility:IsSmallTouchScreen() or isTenFootInterface then + SETTINGS_SHIELD_ACTIVE_POSITION = UDim2.new(0,0,0,0) + SETTINGS_SHIELD_SIZE = UDim2.new(1,0,1,0) +end + +local function GetCorePackagesLoaded(packageList) + local CorePackages = game:GetService("CorePackages") + for _, moduleName in pairs(packageList) do + if not CorePackages:FindFirstChild(moduleName) then + return false + end + end + return true +end + +local function GetServerVersionBlocking() + if connectedServerVersion then + return connectedServerVersion + end + if not GetServerVersionRemote then + repeat + wait() + until GetServerVersionRemote + end + connectedServerVersion = GetServerVersionRemote:InvokeServer() + return connectedServerVersion +end + +local function GetPlaceVersionText() + local text = game.PlaceVersion + + pcall(function() + local json = HttpRbxApiService:GetAsync(string.format("assets/%d/versions", game.PlaceId)) + local versionData = HttpService:JSONDecode(json) + local latestVersion = versionData[1].VersionNumber + text = string.format("%s (Latest: %d)", text, latestVersion) + end) + + return text +end + +local function CreateSettingsHub() + local this = {} + this.Visible = false + this.Active = true + this.Pages = {CurrentPage = nil, PageTable = {}} + this.MenuStack = {} + this.TabHeaders = {} + this.BottomBarButtons = {} + this.ResizedConnection = nil + this.TabConnection = nil + this.LeaveGamePage = require(RobloxGui.Modules.Settings.Pages.LeaveGame) + this.ResetCharacterPage = require(RobloxGui.Modules.Settings.Pages.ResetCharacter) + this.SettingsShowSignal = utility:CreateSignal() + this.OpenStateChangedCount = 0 + + local pageChangeCon = nil + + local PoppedMenuEvent = Instance.new("BindableEvent") + PoppedMenuEvent.Name = "PoppedMenu" + this.PoppedMenu = PoppedMenuEvent.Event + + local function shouldShowHubBar(whichPage) + whichPage = whichPage or this.Pages.CurrentPage + return whichPage.ShouldShowBottomBar == true + end + + local function shouldShowBottomBar(whichPage) + whichPage = whichPage or this.Pages.CurrentPage + + if not FFlagSettingsHubBarsRefactor2 then + if whichPage == this.LeaveGamePage or whichPage == this.ResetCharacterPage then + return false + end + end + + if utility:IsPortrait() or utility:IsSmallTouchScreen() then + return false + end + + if FFlagSettingsHubBarsRefactor2 then + return whichPage.ShouldShowBottomBar == true + else + return true + end + end + + local function setBottomBarBindings() + if not this.Visible then + return + end + for i = 1, #this.BottomBarButtons do + local buttonTable = this.BottomBarButtons[i] + local buttonName = buttonTable[1] + local hotKeyTable = buttonTable[2] + ContextActionService:BindCoreAction(buttonName, hotKeyTable[1], false, unpack(hotKeyTable[2])) + end + + if this.BottomButtonFrame then + this.BottomButtonFrame.Visible = true + end + end + + local function removeBottomBarBindings(delayBeforeRemoving) + for _, hotKeyTable in pairs(this.BottomBarButtons) do + ContextActionService:UnbindCoreAction(hotKeyTable[1]) + end + + local myOpenStateChangedCount = this.OpenStateChangedCount + local removeBottomButtonFrame = function() + if this.OpenStateChangedCount == myOpenStateChangedCount and this.BottomButtonFrame then + this.BottomButtonFrame.Visible = false + end + end + + if delayBeforeRemoving then + delay(delayBeforeRemoving, removeBottomButtonFrame) + else + removeBottomButtonFrame() + end + end + + local function addBottomBarButton(name, text, gamepadImage, keyboardImage, position, clickFunc, hotkeys) + local buttonName = name .. "Button" + local textName = name .. "Text" + + local size = UDim2.new(0,260,0,70) + if isTenFootInterface then + size = UDim2.new(0,320,0,120) + end + + this[buttonName], this[textName] = utility:MakeStyledButton(name .. "Button", text, size, clickFunc, nil, this) + + this[buttonName].Position = position + this[buttonName].Parent = this.BottomButtonFrame + if isTenFootInterface then + this[buttonName].ImageTransparency = 1 + end + + this[textName].FontSize = Enum.FontSize.Size24 + local hintLabel = nil + + if not isTouchDevice then + if FFlagUseNotificationsLocalization then + this[textName].Size = UDim2.new(0.675,0,0.67,0) + this[textName].Position = UDim2.new(0.275,0,0.125,0) + else + this[textName].Size = UDim2.new(0.75,0,0.9,0) + this[textName].Position = UDim2.new(0.25,0,0,0) + end + local hintNameText = name .. "HintText" + local hintName = name .. "Hint" + local image = "" + if UserInputService:GetGamepadConnected(Enum.UserInputType.Gamepad1) or platform == Enum.Platform.XBoxOne then + image = gamepadImage + else + image = keyboardImage + end + + hintLabel = utility:Create'ImageLabel' + { + Name = hintName, + ZIndex = this.Shield.ZIndex + 2, + BackgroundTransparency = 1, + Image = image, + Parent = this[buttonName] + }; + + hintLabel.AnchorPoint = Vector2.new(0.5,0.5) + hintLabel.Size = UDim2.new(0,50,0,50) + hintLabel.Position = UDim2.new(0.15,0,0.475,0) + + end + + if isTenFootInterface then + this[textName].FontSize = Enum.FontSize.Size36 + end + + UserInputService.InputBegan:connect(function(inputObject) + + if inputObject.UserInputType == Enum.UserInputType.Gamepad1 or inputObject.UserInputType == Enum.UserInputType.Gamepad2 or + inputObject.UserInputType == Enum.UserInputType.Gamepad3 or inputObject.UserInputType == Enum.UserInputType.Gamepad4 then + if hintLabel then + hintLabel.Image = gamepadImage + -- if isTenFootInterface then + -- hintLabel.Size = UDim2.new(0,90,0,90) + -- hintLabel.Position = UDim2.new(0,10,0.5,-45) + -- else + -- hintLabel.Size = UDim2.new(0,60,0,60) + -- hintLabel.Position = UDim2.new(0,10,0,5) + -- end + end + elseif inputObject.UserInputType == Enum.UserInputType.Keyboard then + if hintLabel then + hintLabel.Image = keyboardImage + -- hintLabel.Size = UDim2.new(0,48,0,48) + -- hintLabel.Position = UDim2.new(0,10,0,8) + end + end + end) + + local hotKeyFunc = function(contextName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + clickFunc() + end + end + + local hotKeyTable = {hotKeyFunc, hotkeys} + this.BottomBarButtons[#this.BottomBarButtons + 1] = {buttonName, hotKeyTable} + end + + local resetEnabled = true + local function setResetEnabled(value) + resetEnabled = value + if this.ResetCharacterButton then + this.ResetCharacterButton.Selectable = value + this.ResetCharacterButton.Active = value + this.ResetCharacterButton.Enabled.Value = value + local resetHint = this.ResetCharacterButton:FindFirstChild("ResetCharacterHint") + if resetHint then + resetHint.ImageColor3 = (value and Color3.fromRGB(255, 255, 255) or Color3.fromRGB(100, 100, 100)) + end + local resetButtonText = this.ResetCharacterButton:FindFirstChild("ResetCharacterButtonTextLabel") + if resetButtonText then + resetButtonText.TextColor3 = (value and Color3.fromRGB(255, 255, 255) or Color3.fromRGB(100, 100, 100)) + end + end + end + + StarterGui:RegisterSetCore("ResetButtonCallback", function(callback) + local isBindableEvent = typeof(callback) == "Instance" and callback:IsA("BindableEvent") + if isBindableEvent or type(callback) == "boolean" then + this.ResetCharacterPage:SetResetCallback(callback) + else + warn("ResetButtonCallback must be set to a BindableEvent or a boolean") + end + if callback == false then + setResetEnabled(false) + elseif not resetEnabled and (isBindableEvent or callback == true) then + setResetEnabled(true) + end + end) + + local function createGui() + local PageViewSizeReducer = 0 + if utility:IsSmallTouchScreen() then + PageViewSizeReducer = 5 + end + + this.ClippingShield = utility:Create'Frame' + { + Name = "SettingsShield", + Size = SETTINGS_SHIELD_SIZE, + Position = SETTINGS_SHIELD_ACTIVE_POSITION, + BorderSizePixel = 0, + ClipsDescendants = true, + BackgroundTransparency = 1, + Visible = true, + ZIndex = SETTINGS_BASE_ZINDEX, + Parent = RobloxGui + }; + + this.Shield = utility:Create'Frame' + { + Name = "SettingsShield", + Size = UDim2.new(1,0,1,0), + Position = SETTINGS_SHIELD_INACTIVE_POSITION, + BackgroundTransparency = SETTINGS_SHIELD_TRANSPARENCY, + BackgroundColor3 = SETTINGS_SHIELD_COLOR, + BorderSizePixel = 0, + Visible = false, + Active = true, + ZIndex = SETTINGS_BASE_ZINDEX, + Parent = this.ClippingShield + }; + this.VRShield = utility:Create("Frame") { + Name = "VRBackground", + Parent = this.Shield, + + BackgroundColor3 = SETTINGS_SHIELD_COLOR, + BackgroundTransparency = SETTINGS_SHIELD_TRANSPARENCY, + Position = UDim2.new(0, -4, 0, 24), + Size = UDim2.new(1, 8, 1, -40), + BorderSizePixel = 0, + + Visible = false + } + + this.VersionContainer = utility:Create("Frame") { + Name = "VersionContainer", + Parent = this.Shield, + + BackgroundColor3 = SETTINGS_SHIELD_COLOR, + BackgroundTransparency = SETTINGS_SHIELD_TRANSPARENCY, + Position = UDim2.new(0, 0, 1, 0), + Size = UDim2.new(1, 0, 0, VERSION_BAR_HEIGHT), + AnchorPoint = Vector2.new(0,1), + BorderSizePixel = 0, + + ZIndex = 5, + + Visible = false + } + + local size = UDim2.new(0.5, -6, 1, -6) + local clientPosition = UDim2.new(0.5, 3, 0, 3) + local clientTextAlignment = Enum.TextXAlignment.Right + + if FFlagHelpMenuShowPlaceVersion then + size = UDim2.new(0.333, -6, 1, -6) + clientPosition = UDim2.new(0.333, 3, 0, 3) + clientTextAlignment = Enum.TextXAlignment.Center + end + + this.ServerVersionLabel = utility:Create("TextLabel") { + Name = "ServerVersionLabel", + Parent = this.VersionContainer, + Position = UDim2.new(0,3,0,3), + BackgroundTransparency = 1, + TextColor3 = Color3.new(1,1,1), + TextSize = isTenFootInterface and 28 or (utility:IsSmallTouchScreen() and 14 or 20), + Text = "Server Version: ...", + Size = size, + Font = Enum.Font.SourceSans, + TextXAlignment = Enum.TextXAlignment.Left, + ZIndex = 5 + } + spawn(function() + this.ServerVersionLabel.Text = "Server Version: "..GetServerVersionBlocking() + end) + + this.ClientVersionLabel = utility:Create("TextLabel") { + Name = "ClientVersionLabel", + Parent = this.VersionContainer, + Position = clientPosition, + BackgroundTransparency = 1, + TextColor3 = Color3.new(1,1,1), + TextSize = isTenFootInterface and 28 or (utility:IsSmallTouchScreen() and 14 or 20), + Text = "Client Version: "..RunService:GetRobloxVersion(), + Size = size, + Font = Enum.Font.SourceSans, + TextXAlignment = clientTextAlignment, + ZIndex = 5 + } + + if FFlagHelpMenuShowPlaceVersion then + this.PlaceVersionLabel = utility:Create("TextLabel") { + Name = "PlaceVersionLabel", + Parent = this.VersionContainer, + Position = UDim2.new(0.666, 3, 0, 3), + BackgroundTransparency = 1, + TextColor3 = Color3.new(1, 1, 1), + TextSize = isTenFootInterface and 28 or (utility:IsSmallTouchScreen() and 14 or 20), + Text = "Place Version: ...", + Size = UDim2.new(.333, -6, 1, -6), + Font = Enum.Font.SourceSans, + TextXAlignment = Enum.TextXAlignment.Right, + ZIndex = 5, + } + local function setPlaceVersionText() + this.PlaceVersionLabel.Text = "Place Version: "..GetPlaceVersionText() + end + game:GetPropertyChangedSignal("PlaceVersion"):Connect(setPlaceVersionText) + spawn(setPlaceVersionText) + end + + this.EnvironmentLabel = utility:Create("TextLabel") { + Name = "EnvironmentLabel", + Parent = this.VersionContainer, + Position = UDim2.new(0.5,0,0,3), + AnchorPoint = Vector2.new(0.5,0), + BackgroundTransparency = 1, + TextColor3 = Color3.new(1,1,1), + TextSize = isTenFootInterface and 28 or (utility:IsSmallTouchScreen() and 14 or 20), + Text = baseUrl, + Size = UDim2.new(.5,-6,1,-6), + Font = Enum.Font.SourceSans, + TextXAlignment = Enum.TextXAlignment.Center, + ZIndex = 5, + Visible = isTestEnvironment + } + + this.Modal = utility:Create'TextButton' -- Force unlocks the mouse, really need a way to do this via UIS + { + Name = 'Modal', + BackgroundTransparency = 1, + Position = UDim2.new(0, 0, 1, -1), + Size = UDim2.new(1, 0, 1, 0), + Modal = true, + Text = '', + Parent = this.Shield, + Selectable = false + } + + this.MenuContainer = utility:Create'Frame' + { + Name = 'MenuContainer', + ZIndex = this.Shield.ZIndex, + BackgroundTransparency = 1, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0.95, 0, 0.95, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Parent = this.Shield + } + if FFlagEnableResponsiveUIFix then + this.MenuListLayout = utility:Create'UIListLayout' + { + Name = "MenuListLayout", + FillDirection = Enum.FillDirection.Vertical, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Parent = this.MenuContainer + } + end + this.MenuAspectRatio = utility:Create'UIAspectRatioConstraint' + { + Name = 'MenuAspectRatio', + AspectRatio = 800 / 600, + AspectType = Enum.AspectType.ScaleWithParentSize, + Parent = this.MenuContainer + } + + this.HubBar = utility:Create'ImageLabel' + { + Name = "HubBar", + ZIndex = this.Shield.ZIndex + 1, + BorderSizePixel = 0, + BackgroundColor3 = Color3.new(78/255, 84/255, 96/255), + BackgroundTransparency = 1, + Image = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuBackground.png", + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(4,4,6,6), + AnchorPoint = Vector2.new(0.5, 0), + LayoutOrder = 0, + Parent = this.MenuContainer + } + this.HubBarListLayout = utility:Create'UIListLayout' + { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Parent = this.HubBar + } + + if utility:IsSmallTouchScreen() then + this.HubBar.Size = UDim2.new(1,-10,0,40) + this.HubBar.Position = UDim2.new(0.5,0,0,6) + elseif isTenFootInterface then + this.HubBar.Size = UDim2.new(0,1200,0,100) + this.HubBar.Position = UDim2.new(0.5,0,0.1,0) + else + this.HubBar.Size = UDim2.new(0,800,0,60) + this.HubBar.Position = UDim2.new(0.5,0,0.1,0) + end + + this.PageViewClipper = utility:Create'Frame' + { + Name = 'PageViewClipper', + BackgroundTransparency = 1, + Size = UDim2.new(this.HubBar.Size.X.Scale,this.HubBar.Size.X.Offset, + 1, -this.HubBar.Size.Y.Offset - this.HubBar.Position.Y.Offset - PageViewSizeReducer), + Position = UDim2.new(this.HubBar.Position.X.Scale, this.HubBar.Position.X.Offset, + this.HubBar.Position.Y.Scale, this.HubBar.Position.Y.Offset + this.HubBar.Size.Y.Offset + 1), + AnchorPoint = Vector2.new(0.5, 0), + ClipsDescendants = true, + LayoutOrder = 1, + Parent = this.MenuContainer, + + utility:Create'ImageButton'{ + Name = 'InputCapture', + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + Image = '' + } + } + + this.PageView = utility:Create'ScrollingFrame' + { + Name = "PageView", + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, 0, 1, -20), + ZIndex = this.Shield.ZIndex, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Selectable = false, + Parent = this.PageViewClipper, + }; + if FFlagSettingsHubPlayersHorizontalScroll then + this.PageView.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar + end + + this.PageViewInnerFrame = utility:Create'Frame' + { + Name = "PageViewInnerFrame", + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + ZIndex = this.Shield.ZIndex, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Selectable = false, + Parent = this.PageView, + }; + + if UserInputService.MouseEnabled then + this.PageViewClipper.Size = UDim2.new(this.HubBar.Size.X.Scale,this.HubBar.Size.X.Offset, + 0.5, -(this.HubBar.Position.Y.Offset - this.HubBar.Size.Y.Offset)) + end + + local bottomOffset = 0 + if isTouchDevice and not UserInputService.MouseEnabled then + bottomOffset = 80 + end + this.BottomButtonFrame = utility:Create'Frame' + { + Name = "BottomButtonFrame", + Size = this.HubBar.Size, + Position = UDim2.new(0.5, -this.HubBar.Size.X.Offset/2, 1-this.HubBar.Position.Y.Scale-this.HubBar.Size.Y.Scale, -this.HubBar.Position.Y.Offset-this.HubBar.Size.Y.Offset), + ZIndex = this.Shield.ZIndex + 1, + BackgroundTransparency = 1, + LayoutOrder = 2, + Parent = this.MenuContainer + }; + + local leaveGameFunc = function() + this:AddToMenuStack(this.Pages.CurrentPage) + this.HubBar.Visible = false + removeBottomBarBindings() + this:SwitchToPage(this.LeaveGamePage, nil, 1, true) + end + + -- Xbox Only + local inviteToGameFunc = function() + if not RunService:IsStudio() then + local platformService = game:GetService('PlatformService') + if platformService then + platformService:PopupGameInviteUI() + end + end + end + + local resumeFunc = function() + setVisibilityInternal(false) + end + + local buttonImageAppend = "" + + if isTenFootInterface then + buttonImageAppend = "@2x" + end + + if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then + local function createInviteButton() + addBottomBarButton("InviteToGame", "Send Game Invites", "rbxasset://textures/ui/Settings/Help/XButtonLight" .. buttonImageAppend .. ".png", + "", UDim2.new(0.5,isTenFootInterface and -160 or -130,0.5,-25), + inviteToGameFunc, {Enum.KeyCode.ButtonX} + ) + if RunService:IsStudio() then + this.InviteToGameButton.Selectable = false + this.InviteToGameButton.Active = false + this.InviteToGameButton.Enabled.Value = false + local inviteHint = this.InviteToGameButton:FindFirstChild("InviteToGameHint") + if inviteHint then + inviteHint.ImageColor3 = Color3.fromRGB(100, 100, 100) + end + local inviteButtonText = this.InviteToGameText + if inviteButtonText then + inviteButtonText.TextColor3 = Color3.fromRGB(100, 100, 100) + end + end + end + + -- only show invite button on non-PMP games. Some users games may not be enabled for console, so inviting to + -- to the game session will not work. + spawn(function() + local PlatformService = nil + pcall(function() PlatformService = game:GetService('PlatformService') end) + if not PlatformService then return end + + pcall(function() + local pmpCreatorId = PlatformService:BeginGetPMPCreatorId() + if pmpCreatorId == 0 then + createInviteButton() + end + end) + end) + else + addBottomBarButton("LeaveGame", "Leave Game", "rbxasset://textures/ui/Settings/Help/XButtonLight" .. buttonImageAppend .. ".png", + "rbxasset://textures/ui/Settings/Help/LeaveIcon.png", UDim2.new(0.5,isTenFootInterface and -160 or -130,0.5,-25), + leaveGameFunc, {Enum.KeyCode.L, Enum.KeyCode.ButtonX} + ) + end + + local resetCharFunc = function() + if resetEnabled then + this:AddToMenuStack(this.Pages.CurrentPage) + this.HubBar.Visible = false + removeBottomBarBindings() + this:SwitchToPage(this.ResetCharacterPage, nil, 1, true) + end + end + + addBottomBarButton("ResetCharacter", "Reset Character", "rbxasset://textures/ui/Settings/Help/YButtonLight" .. buttonImageAppend .. ".png", + "rbxasset://textures/ui/Settings/Help/ResetIcon.png", UDim2.new(0.5,isTenFootInterface and -550 or -400,0.5,-25), + resetCharFunc, {Enum.KeyCode.R, Enum.KeyCode.ButtonY} + ) + addBottomBarButton("Resume", "Resume Game", "rbxasset://textures/ui/Settings/Help/BButtonLight" .. buttonImageAppend .. ".png", + "rbxasset://textures/ui/Settings/Help/EscapeIcon.png", UDim2.new(0.5,isTenFootInterface and 200 or 140,0.5,-25), + resumeFunc, {Enum.KeyCode.ButtonB, Enum.KeyCode.ButtonStart} + ) + + local function cameraViewportChanged() + utility:FireOnResized() + end + + local viewportSizeChangedConn = nil + local function onWorkspaceChanged(prop) + if prop == "CurrentCamera" then + cameraViewportChanged() + if viewportSizeChangedConn then viewportSizeChangedConn:disconnect() end + viewportSizeChangedConn = workspace.CurrentCamera:GetPropertyChangedSignal("ViewportSize"):connect(cameraViewportChanged) + end + end + onWorkspaceChanged("CurrentCamera") + workspace.Changed:connect(onWorkspaceChanged) + end + + local function onScreenSizeChanged() + + local largestPageSize = 600 + local fullScreenSize = RobloxGui.AbsoluteSize.y + local bufferSize = (1-0.95) * fullScreenSize + local isPortrait = utility:IsPortrait() + + if isTenFootInterface then + largestPageSize = 800 + bufferSize = 0.07 * fullScreenSize + this.MenuContainer.Size = UDim2.new(0.95, 0, 0.95, 0) + elseif utility:IsSmallTouchScreen() then + bufferSize = math.min(10, (1-0.99) * fullScreenSize) + this.MenuContainer.Size = UDim2.new(1, 0, 0.99, 0) + else + this.MenuContainer.Size = UDim2.new(0.95, 0, 0.95, 0) + end + local barSize = this.HubBar.Size.Y.Offset + local extraSpace = bufferSize*2+barSize*2 + + if isPortrait then + this.MenuContainer.Size = UDim2.new(1, 0, 1, 0) + this.MenuAspectRatio.Parent = nil + this.HubBar.Position = UDim2.new(0.5, 0, 0, 10) + this.HubBar.Size = UDim2.new(1, -20, 0, 40) + else + if isTenFootInterface then + this.HubBar.Size = UDim2.new(0, 1200, 0, 100) + this.MenuAspectRatio.Parent = this.MenuContainer + elseif utility:IsSmallTouchScreen() then + this.HubBar.Size = UDim2.new(1, -10, 0, 40) + this.MenuAspectRatio.Parent = nil + else + this.HubBar.Size = UDim2.new(0, 800, 0, 60) + this.MenuAspectRatio.Parent = this.MenuContainer + end + end + + --We need to wait and let the HubBar AbsoluteSize actually update. + --This is in the same frame, so the delay should be very minimal. + --Maybe in the future we need to have a way to force AbsoluteSize + --to update, or we can just avoid using it so soon. + RunService.Heartbeat:wait() + + if shouldShowBottomBar() then + setBottomBarBindings() + else + removeBottomBarBindings() + end + + local usableScreenHeight = fullScreenSize - extraSpace + local minimumPageSize = 150 + local usePageSize = nil + + if not isPortrait then + if largestPageSize < usableScreenHeight then + usePageSize = largestPageSize + this.HubBar.Position = UDim2.new( + this.HubBar.Position.X.Scale, + this.HubBar.Position.X.Offset, + 0.5, + -largestPageSize/2 - this.HubBar.Size.Y.Offset + ) + if this.BottomButtonFrame then + this.BottomButtonFrame.Position = UDim2.new( + this.BottomButtonFrame.Position.X.Scale, + this.BottomButtonFrame.Position.X.Offset, + 0.5, + largestPageSize/2 + ) + end + elseif usableScreenHeight < minimumPageSize then + usePageSize = minimumPageSize + this.HubBar.Position = UDim2.new( + this.HubBar.Position.X.Scale, + this.HubBar.Position.X.Offset, + 0.5, + -minimumPageSize/2 - this.HubBar.Size.Y.Offset + ) + if this.BottomButtonFrame then + this.BottomButtonFrame.Position = UDim2.new( + this.BottomButtonFrame.Position.X.Scale, + this.BottomButtonFrame.Position.X.Offset, + 0.5, + minimumPageSize/2 + ) + end + else + usePageSize = usableScreenHeight + this.HubBar.Position = UDim2.new( + this.HubBar.Position.X.Scale, + this.HubBar.Position.X.Offset, + 0, + bufferSize + ) + if this.BottomButtonFrame then + this.BottomButtonFrame.Position = UDim2.new( + this.BottomButtonFrame.Position.X.Scale, + this.BottomButtonFrame.Position.X.Offset, + 1, + -(bufferSize + barSize) + ) + end + end + else + usePageSize = usableScreenHeight + end + + if not isTenFootInterface then + if utility:IsSmallTouchScreen() then + this.PageViewClipper.Size = UDim2.new( + 0, + this.HubBar.AbsoluteSize.X, + 0, + usePageSize + 44 + ) + else + this.PageViewClipper.Size = UDim2.new( + 0, + this.HubBar.AbsoluteSize.X, + 0, + usePageSize + ) + end + else + this.PageViewClipper.Size = UDim2.new( + 0, + this.HubBar.AbsoluteSize.X, + 0, + usePageSize + ) + end + if not isPortrait then + this.PageViewClipper.Position = UDim2.new( + this.PageViewClipper.Position.X.Scale, + this.PageViewClipper.Position.X.Offset, + 0.5, + -usePageSize/2 + ) + else + this.PageViewClipper.Position = UDim2.new(0.5, 0, 0, this.HubBar.Position.Y.Offset + this.HubBar.AbsoluteSize.Y) + end + end + + local function toggleQuickProfilerFromHotkey(actionName, inputState, inputObject) + -- Make sure it's Ctrl-F7. + -- NOTE: This will only work if FFlagDontSwallowInputForStudioShortcuts is True. + -- Otherwise, we never get the "Begin" input state when Ctrl key is down. + if (not (UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) or + UserInputService:IsKeyDown(Enum.KeyCode.RightControl))) then + return + end + + if actionName ==QUICK_PROFILER_ACTION_NAME then + if inputState and inputState == Enum.UserInputState.Begin then + GameSettings.PerformanceStatsVisible = not GameSettings.PerformanceStatsVisible + end + end + end + + local function toggleDevConsole(actionName, inputState, inputObject) + if actionName == DEV_CONSOLE_ACTION_NAME then -- ContextActionService->F9 + if inputState and inputState == Enum.UserInputState.Begin then + if FFlagEnableNewDevConsole then + DevConsoleMaster:ToggleVisibility() + else + local devConsoleVisible = DeveloperConsoleModule:GetVisibility() + DeveloperConsoleModule:SetVisibility(not devConsoleVisible) + end + end + end + end + + local lastInputUsedToSelectGui = isTenFootInterface + + -- Map indicating if a KeyCode or UserInputType should toggle the lastInputUsedToSelectGui variable. + local inputUsedToSelectGui = { + [Enum.UserInputType.Gamepad1] = true, + [Enum.UserInputType.Gamepad2] = true, + [Enum.UserInputType.Gamepad3] = true, + [Enum.UserInputType.Gamepad4] = true, + [Enum.KeyCode.Left] = true, + [Enum.KeyCode.Right] = true, + [Enum.KeyCode.Up] = true, + [Enum.KeyCode.Down] = true, + [Enum.KeyCode.Tab] = true, + [Enum.UserInputType.Touch] = false, + [Enum.UserInputType.MouseButton1] = false, + [Enum.UserInputType.MouseButton2] = false + } + + UserInputService.InputBegan:connect(function(input) + if input.UserInputType and inputUsedToSelectGui[input.UserInputType] ~= nil then + lastInputUsedToSelectGui = inputUsedToSelectGui[input.UserInputType] + elseif input.KeyCode and inputUsedToSelectGui[input.KeyCode] then + lastInputUsedToSelectGui = inputUsedToSelectGui[input.KeyCode] + end + end) + UserInputService.InputChanged:connect(function(input) + if input.KeyCode == Enum.KeyCode.Thumbstick1 or input.KeyCode == Enum.KeyCode.Thumbstick2 then + if input.Position.magnitude >= 0.25 then + lastInputUsedToSelectGui = true + end + elseif input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseMovement then + lastInputUsedToSelectGui = false + end + end) + + + local switchTab = function(direction, cycle) + local currentTabPosition = GetHeaderPosition(this.Pages.CurrentPage) + if currentTabPosition < 0 then return end + + local newTabPosition = currentTabPosition + direction + if cycle then + if newTabPosition > #this.TabHeaders then + newTabPosition = 1 + elseif newTabPosition < 1 then + newTabPosition = #this.TabHeaders + end + end + local newHeader = this.TabHeaders[newTabPosition] + + if newHeader then + for pager,v in pairs(this.Pages.PageTable) do + if pager:GetTabHeader() == newHeader then + this:SwitchToPage(pager, true, direction) + break + end + end + end + end + + local switchTabFromBumpers = function(actionName, inputState, inputObject) + if inputState ~= Enum.UserInputState.Begin then return end + + local direction = 0 + if inputObject.KeyCode == Enum.KeyCode.ButtonR1 then + direction = 1 + elseif inputObject.KeyCode == Enum.KeyCode.ButtonL1 then + direction = -1 + end + + switchTab(direction, true, true) + end + + local switchTabFromKeyboard = function(input) + if input.KeyCode == Enum.KeyCode.Tab then + local direction = 0 + if UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) or UserInputService:IsKeyDown(Enum.KeyCode.RightShift) then + direction = -1 + else + direction = 1 + end + + switchTab(direction, true, true) + end + end + + local scrollHotkeyFunc = function(actionName, inputState, inputObject) + if inputState ~= Enum.UserInputState.Begin then return end + + local direction = 0 + if inputObject.KeyCode == Enum.KeyCode.PageUp then + direction = -100 + elseif inputObject.KeyCode == Enum.KeyCode.PageDown then + direction = 100 + end + + this:ScrollPixels(direction) + end + + -- need some stuff for functions below so init here + createGui() + + function GetHeaderPosition(page) + local header = page:GetTabHeader() + if not header then return -1 end + + for i,v in pairs(this.TabHeaders) do + if v == header then + return i + end + end + + return -1 + end + + local setZIndex = nil + setZIndex = function(newZIndex, object) + if object:IsA("GuiObject") then + object.ZIndex = newZIndex + local children = object:GetChildren() + for i = 1, #children do + setZIndex(newZIndex, children[i]) + end + end + end + + local function AddHeader(newHeader, headerPage) + if not newHeader then return end + + table.insert(this.TabHeaders, newHeader) + headerPage.TabPosition = #this.TabHeaders + + local sizeOfTab = 1/#this.TabHeaders + for i = 1, #this.TabHeaders do + local tab = this.TabHeaders[i] + tab.Size = UDim2.new(sizeOfTab, 0, 1, 0) + end + + setZIndex(SETTINGS_BASE_ZINDEX + 1, newHeader) + newHeader.Parent = this.HubBar + end + + local function RemoveHeader(oldHeader) + local removedPos = nil + + for i = 1, #this.TabHeaders do + if this.TabHeaders[i] == oldHeader then + removedPos = i + table.remove(this.TabHeaders, i) + break + end + end + + if removedPos then + for i = removedPos, #this.TabHeaders do + local currentTab = this.TabHeaders[i] + currentTab.Position = UDim2.new(currentTab.Position.X.Scale, currentTab.Position.X.Offset - oldHeader.AbsoluteSize.X, + currentTab.Position.Y.Scale, currentTab.Position.Y.Offset) + end + end + + oldHeader.Parent = nil + end + + -- Page APIs + function this:AddPage(pageToAdd) + this.Pages.PageTable[pageToAdd] = true + AddHeader(pageToAdd:GetTabHeader(), pageToAdd) + pageToAdd.Page.Position = UDim2.new(pageToAdd.TabPosition - 1,0,0,0) + end + + function this:RemovePage(pageToRemove) + this.Pages.PageTable[pageToRemove] = nil + RemoveHeader(pageToRemove:GetTabHeader()) + end + + function this:HideBar() + this.HubBar.Visible = false + this.PageViewClipper.Visible = false + if this.BottomButtonFrame then + removeBottomBarBindings() + end + end + + function this:ShowBar() + this.HubBar.Visible = true + this.PageViewClipper.Visible = true + if this.BottomButtonFrame and shouldShowBottomBar() then + setBottomBarBindings() + end + end + + function this:ScrollPixels(pixels) + -- Only Y + local oldY = this.PageView.CanvasPosition.Y + local maxY = this.PageView.CanvasSize.Y.Offset - this.PageViewClipper.AbsoluteSize.y + local newY = math.max(0, math.min(oldY+pixels, maxY)) -- i.e. clamp + this.PageView.CanvasPosition = Vector2.new(0, newY) + end + + function this:ScrollToFrame(frame, forced) + if lastInputUsedToSelectGui or forced then + local ay = frame.AbsolutePosition.y - this.Pages.CurrentPage.Page.AbsolutePosition.y + local by = ay + frame.AbsoluteSize.y + + if ay < this.PageView.CanvasPosition.y then -- Scroll up to fit top + this.PageView.CanvasPosition = Vector2.new(0, ay) + elseif by - this.PageView.CanvasPosition.y > this.PageViewClipper.Size.Y.Offset then -- Scroll down to fit bottom + this.PageView.CanvasPosition = Vector2.new(0, by - this.PageViewClipper.Size.Y.Offset) + end + end + end + + function this:SwitchToPage(pageToSwitchTo, ignoreStack, direction, skipAnimation) + if this.Pages.PageTable[pageToSwitchTo] == nil then return end + + -- detect direction + if direction == nil then + if this.Pages.CurrentPage and this.Pages.CurrentPage.TabHeader and pageToSwitchTo and pageToSwitchTo.TabHeader then + direction = this.Pages.CurrentPage.TabHeader.AbsolutePosition.x < pageToSwitchTo.TabHeader.AbsolutePosition.x and 1 or -1 + end + end + if direction == nil then + direction = 1 + end + + -- if we have a page we need to let it know to go away + if this.Pages.CurrentPage then + pageChangeCon:disconnect() + this.Pages.CurrentPage.Active = false + end + + -- make sure all pages are in right position + local newPagePos = pageToSwitchTo.TabPosition + for page, _ in pairs(this.Pages.PageTable) do + if page ~= pageToSwitchTo then + page:Hide(-direction, newPagePos, skipAnimation) + end + end + + -- set top & bottom bar visibility + if this.BottomButtonFrame then + if shouldShowBottomBar(pageToSwitchTo) then + setBottomBarBindings() + else + this.BottomButtonFrame.Visible = false + end + + if FFlagSettingsHubBarsRefactor2 then + this.HubBar.Visible = shouldShowHubBar(pageToSwitchTo) + else + this.HubBar.Visible = not (pageToSwitchTo == this.LeaveGamePage or pageToSwitchTo == this.ResetCharacterPage) + end + end + + if FFlagSettingsHubBarsRefactor2 then + -- set whether the page should be clipped + local isClipped = pageToSwitchTo.IsPageClipped == true + this.PageViewClipper.ClipsDescendants = isClipped + this.PageView.ClipsDescendants = isClipped + this.PageViewInnerFrame.ClipsDescendants = isClipped + end + + -- make sure page is visible + this.Pages.CurrentPage = pageToSwitchTo + this.Pages.CurrentPage:Display(this.PageViewInnerFrame, skipAnimation) + this.Pages.CurrentPage.Active = true + + local pageSize = this.Pages.CurrentPage:GetSize() + this.PageView.CanvasSize = UDim2.new(0,pageSize.X,0,pageSize.Y) + if not FFlagSettingsHubPlayersHorizontalScroll then + if this.PageView.CanvasSize.Y.Offset > this.PageView.AbsoluteSize.Y then + this.PageViewInnerFrame.Size = UDim2.new(1, -this.PageView.ScrollBarThickness, 1, 0) + else + this.PageViewInnerFrame.Size = UDim2.new(1, 0, 1, 0) + end + end + + pageChangeCon = this.Pages.CurrentPage.Page.Changed:connect(function(prop) + if prop == "AbsoluteSize" then + local pageSize = this.Pages.CurrentPage:GetSize() + this.PageView.CanvasSize = UDim2.new(0,pageSize.X,0,pageSize.Y) + if not FFlagSettingsHubPlayersHorizontalScroll then + if this.PageView.CanvasSize.Y.Offset > this.PageView.AbsoluteSize.Y then + this.PageViewInnerFrame.Size = UDim2.new(1, -this.PageView.ScrollBarThickness, 1, 0) + else + this.PageViewInnerFrame.Size = UDim2.new(1, 0, 1, 0) + end + end + end + end) + + if this.MenuStack[#this.MenuStack] ~= this.Pages.CurrentPage and not ignoreStack then + this.MenuStack[#this.MenuStack + 1] = this.Pages.CurrentPage + end + end + + function this:SetActive(active) + this.Active = active + + if this.Pages.CurrentPage then + this.Pages.CurrentPage.Active = active + end + end + + function clearMenuStack() + while this.MenuStack and #this.MenuStack > 0 do + this:PopMenu() + end + end + + function setOverrideMouseIconBehavior() + if UserInputService:GetLastInputType() == Enum.UserInputType.Gamepad1 or VRService.VREnabled then + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide + else + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceShow + end + end + + function setVisibilityInternal(visible, noAnimation, customStartPage, switchedFromGamepadInput) + this.OpenStateChangedCount = this.OpenStateChangedCount + 1 + local switchedFromGamepadInput = switchedFromGamepadInput or isTenFootInterface + this.Visible = visible + + if this.ResizedConnection then + this.ResizedConnection:disconnect() + this.ResizedConnection = nil + end + + this.Modal.Visible = this.Visible + + if this.TabConnection then + this.TabConnection:disconnect() + this.TabConnection = nil + end + + local playerList = require(RobloxGui.Modules.PlayerlistModule) + + if this.Visible then + this.ResizedConnection = RobloxGui.Changed:connect(function(prop) + if prop == "AbsoluteSize" then + onScreenSizeChanged() + end + end) + onScreenSizeChanged() + + this.SettingsShowSignal:fire(this.Visible) + + pcall(function() GuiService:SetMenuIsOpen(true) end) + this.Shield.Visible = this.Visible + if noAnimation or not this.Shield:IsDescendantOf(game) then + this.Shield.Position = SETTINGS_SHIELD_ACTIVE_POSITION + else + this.Shield:TweenPosition(SETTINGS_SHIELD_ACTIVE_POSITION, Enum.EasingDirection.InOut, Enum.EasingStyle.Quart, 0.5, true) + end + + local noOpFunc = function() end + ContextActionService:BindCoreAction("RbxSettingsHubStopCharacter", noOpFunc, false, + Enum.PlayerActions.CharacterForward, + Enum.PlayerActions.CharacterBackward, + Enum.PlayerActions.CharacterLeft, + Enum.PlayerActions.CharacterRight, + Enum.PlayerActions.CharacterJump, + Enum.KeyCode.LeftShift, + Enum.KeyCode.RightShift, + Enum.KeyCode.Tab, + Enum.UserInputType.Gamepad1, Enum.UserInputType.Gamepad2, Enum.UserInputType.Gamepad3, Enum.UserInputType.Gamepad4 + ) + + ContextActionService:BindCoreAction("RbxSettingsHubSwitchTab", switchTabFromBumpers, false, Enum.KeyCode.ButtonR1, Enum.KeyCode.ButtonL1) + ContextActionService:BindCoreAction("RbxSettingsScrollHotkey", scrollHotkeyFunc, false, Enum.KeyCode.PageUp, Enum.KeyCode.PageDown) + if shouldShowBottomBar() then + setBottomBarBindings() + end + + this.TabConnection = UserInputService.InputBegan:connect(switchTabFromKeyboard) + + + setOverrideMouseIconBehavior() + lastInputChangedCon = UserInputService.LastInputTypeChanged:connect(setOverrideMouseIconBehavior) + if UserInputService.MouseEnabled and not VRService.VREnabled then + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceShow + end + + if customStartPage then + removeBottomBarBindings() + this:SwitchToPage(customStartPage, nil, 1, true) + else + if not isTenFootInterface then + this:SwitchToPage(this.PlayersPage, nil, 1, true) + else + if this.HomePage then + this:SwitchToPage(this.HomePage, nil, 1, true) + else + this:SwitchToPage(this.GameSettingsPage, nil, 1, true) + end + end + end + + playerList:HideTemp('SettingsMenu', true) + + if chat:GetVisibility() then + chatWasVisible = true + chat:ToggleVisibility() + end + + local backpack = require(RobloxGui.Modules.BackpackScript) + if backpack.IsOpen then + backpack:OpenClose() + end + else + if noAnimation then + this.Shield.Position = SETTINGS_SHIELD_INACTIVE_POSITION + this.Shield.Visible = this.Visible + this.SettingsShowSignal:fire(this.Visible) + pcall(function() GuiService:SetMenuIsOpen(false) end) + else + this.Shield:TweenPosition(SETTINGS_SHIELD_INACTIVE_POSITION, Enum.EasingDirection.In, Enum.EasingStyle.Quad, 0.4, true, function() + this.Shield.Visible = this.Visible + this.SettingsShowSignal:fire(this.Visible) + if not this.Visible then pcall(function() GuiService:SetMenuIsOpen(false) end) end + end) + end + + if lastInputChangedCon then + lastInputChangedCon:disconnect() + end + + playerList:HideTemp('SettingsMenu', false) + + if chatWasVisible then + chat:ToggleVisibility() + chatWasVisible = false + end + + if not VRService.VREnabled then + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None + end + + clearMenuStack() + ContextActionService:UnbindCoreAction("RbxSettingsHubSwitchTab") + ContextActionService:UnbindCoreAction("RbxSettingsHubStopCharacter") + ContextActionService:UnbindCoreAction("RbxSettingsScrollHotkey") + removeBottomBarBindings(0.4) + + GuiService.SelectedCoreObject = nil + end + end + + function this:SetVisibility(visible, noAnimation, customStartPage, switchedFromGamepadInput) + if this.Visible == visible then return end + + setVisibilityInternal(visible, noAnimation, customStartPage, switchedFromGamepadInput) + end + + function this:GetVisibility() + return this.Visible + end + + function this:ToggleVisibility(switchedFromGamepadInput) + setVisibilityInternal(not this.Visible, nil, nil, switchedFromGamepadInput) + end + + function this:AddToMenuStack(newItem) + if this.MenuStack[#this.MenuStack] ~= newItem then + this.MenuStack[#this.MenuStack + 1] = newItem + end + end + + + function this:PopMenu(switchedFromGamepadInput, skipAnimation) + if this.MenuStack and #this.MenuStack > 0 then + local lastStackItem = this.MenuStack[#this.MenuStack] + + if type(lastStackItem) ~= "table" then + PoppedMenuEvent:Fire(lastStackItem) + end + + table.remove(this.MenuStack, #this.MenuStack) + this:SwitchToPage(this.MenuStack[#this.MenuStack], true, 1, skipAnimation) + if #this.MenuStack == 0 then + this:SetVisibility(false) + + this.Pages.CurrentPage:Hide(0, 0) + end + else + this.MenuStack = {} + PoppedMenuEvent:Fire() + this:ToggleVisibility() + end + end + + function this:ShowShield() + this.Shield.BackgroundTransparency = UserInputService.VREnabled and SETTINGS_SHIELD_VR_TRANSPARENCY or SETTINGS_SHIELD_TRANSPARENCY + end + function this:HideShield() + this.Shield.BackgroundTransparency = 1 + end + + local thisModuleName = "SettingsMenu" + local vrMenuOpened, vrMenuClosed = nil, nil + local function enableVR() + local VRHub = require(RobloxGui.Modules.VR.VRHub) + local Panel3D = require(RobloxGui.Modules.VR.Panel3D) + local panel = Panel3D.Get(thisModuleName) + panel:ResizeStuds(4, 4, 250) + panel:SetType(Panel3D.Type.Standard) + panel:SetVisible(false) + panel:SetCanFade(false) + + this.ClippingShield.Parent = panel:GetGUI() + this.Shield.Parent.ClipsDescendants = false + this.VRShield.Visible = true + this:HideShield() + + vrMenuOpened = this.SettingsShowSignal:connect(function(visible) + if visible then + panel:SetVisible(true) + + VRHub:FireModuleOpened(thisModuleName) + else + panel:SetVisible(false) + + VRHub:FireModuleClosed(thisModuleName) + end + end) + + VRHub.ModuleOpened.Event:connect(function(moduleName) + if moduleName ~= thisModuleName then + this:SetVisibility(false) + end + end) + end + local function disableVR() + this.ClippingShield.Parent = RobloxGui + this.Shield.Parent.ClipsDescendants = true + this.VRShield.Visible = false + this:ShowShield() + + if vrMenuOpened then + vrMenuOpened:disconnect() + vrMenuOpened = nil + end + if vrMenuClosed then + vrMenuClosed:disconnect() + vrMenuClosed = nil + end + + local Panel3D = require(RobloxGui.Modules.VR.Panel3D) + local panel = Panel3D.Get(thisModuleName) + panel:SetVisible(false) + end + + local UISChanged; + local function OnVREnabled(prop) + if prop == "VREnabled" then + if UserInputService.VREnabled then + enableVR() + else + disableVR() + end + end + end + UISChanged = UserInputService.Changed:connect(OnVREnabled) + OnVREnabled("VREnabled") + + + local closeMenuFunc = function(name, inputState, input) + if inputState ~= Enum.UserInputState.Begin then return end + this:PopMenu(false, true) + end + ContextActionService:BindCoreAction("RBXEscapeMainMenu", closeMenuFunc, false, Enum.KeyCode.Escape) + + this.ResetCharacterPage:SetHub(this) + this.LeaveGamePage:SetHub(this) + + -- full page initialization + this.GameSettingsPage = require(RobloxGui.Modules.Settings.Pages.GameSettings) + this.GameSettingsPage:SetHub(this) + + this.ReportAbusePage = require(RobloxGui.Modules.Settings.Pages.ReportAbuseMenu) + this.ReportAbusePage:SetHub(this) + + this.HelpPage = require(RobloxGui.Modules.Settings.Pages.Help) + this.HelpPage:SetHub(this) + + if platform == Enum.Platform.Windows then + this.RecordPage = require(RobloxGui.Modules.Settings.Pages.Record) + this.RecordPage:SetHub(this) + end + + if not isTenFootInterface then + this.PlayersPage = require(RobloxGui.Modules.Settings.Pages.Players) + this.PlayersPage:SetHub(this) + end + + if FFlagSettingsHubInviteToGame2 then + local shareGameCorePackages = { + "Roact", + "Rodux", + "RoactRodux", + } + if GetCorePackagesLoaded(shareGameCorePackages) then + -- Create the embedded Roact app for the ShareGame page + -- This is accomplished via a Roact Portal into the ShareGame page frame + local ShareGameMaster = require(RobloxGui.Modules.Settings.ShareGameMaster) + this.ShareGameApp = ShareGameMaster.createApp(this.PageViewClipper) + + + this.ShareGamePage = require(RobloxGui.Modules.Settings.Pages.ShareGamePlaceholderPage) + this.ShareGamePage:ConnectHubToApp(this, this.ShareGameApp) + + this:AddPage(this.ShareGamePage) + end + end + + -- page registration + if not isTenFootInterface then + this:AddPage(this.PlayersPage) + end + this:AddPage(this.ResetCharacterPage) + this:AddPage(this.LeaveGamePage) + this:AddPage(this.GameSettingsPage) + if this.ReportAbusePage then + this:AddPage(this.ReportAbusePage) + end + this:AddPage(this.HelpPage) + if this.RecordPage then + this:AddPage(this.RecordPage) + end + + if not isTenFootInterface then + this:SwitchToPage(this.PlayersPage, true, 1) + else + if this.HomePage then + this:SwitchToPage(this.HomePage, true, 1) + else + this:SwitchToPage(this.GameSettingsPage, true, 1) + end + end + -- hook up to necessary signals + + -- connect back button on android + GuiService.ShowLeaveConfirmation:connect(function() + if #this.MenuStack == 0 then + this:SetVisibility(true) + this:SwitchToPage(this.PlayerPage, nil, 1) + else + this:PopMenu(false, true) + end + end) + + -- Dev Console Connections + ContextActionService:BindCoreAction(DEV_CONSOLE_ACTION_NAME, + toggleDevConsole, + false, + Enum.KeyCode.F9 + ) + + -- Quick Profiler connections + -- Note: it's actually Ctrl-F7. We don't have a nice way of + -- making that explicit here, so we check it inside toggleQuickProfilerFromHotkey. + ContextActionService:BindCoreAction(QUICK_PROFILER_ACTION_NAME, + toggleQuickProfilerFromHotkey, + false, + Enum.KeyCode.F7 + ) + + -- Keyboard control + UserInputService.InputBegan:connect(function(input) + if input.KeyCode == Enum.KeyCode.Left or input.KeyCode == Enum.KeyCode.Right or input.KeyCode == Enum.KeyCode.Up or input.KeyCode == Enum.KeyCode.Down then + if this.Visible and this.Active then + if this.Pages.CurrentPage then + if GuiService.SelectedCoreObject == nil then + this.Pages.CurrentPage:SelectARow() + end + end + end + end + end) + + return this +end + + +-- Main Entry Point + +local moduleApiTable = {} + +moduleApiTable.ModuleName = "SettingsMenu" +moduleApiTable.KeepVRTopbarOpen = true +moduleApiTable.VRIsExclusive = true +moduleApiTable.VRClosesNonExclusive = true +VRHub:RegisterModule(moduleApiTable) + +VRHub.ModuleOpened.Event:connect(function(moduleName) + if moduleName ~= moduleApiTable.ModuleName then + local module = VRHub:GetModule(moduleName) + if module.VRIsExclusive then + moduleApiTable:SetVisibility(false) + end + end +end) + +local SettingsHubInstance = CreateSettingsHub() + +function moduleApiTable:SetVisibility(visible, noAnimation, customStartPage, switchedFromGamepadInput) + SettingsHubInstance:SetVisibility(visible, noAnimation, customStartPage, switchedFromGamepadInput) +end + +function moduleApiTable:ToggleVisibility(switchedFromGamepadInput) + SettingsHubInstance:ToggleVisibility(switchedFromGamepadInput) +end + +function moduleApiTable:SwitchToPage(pageToSwitchTo, ignoreStack) + SettingsHubInstance:SwitchToPage(pageToSwitchTo, ignoreStack, 1) +end + +function moduleApiTable:GetVisibility() + return SettingsHubInstance.Visible +end + +function moduleApiTable:ShowShield() + SettingsHubInstance:ShowShield() +end + +function moduleApiTable:HideShield() + SettingsHubInstance:HideShield() +end + +moduleApiTable.SettingsShowSignal = SettingsHubInstance.SettingsShowSignal + +moduleApiTable.Instance = SettingsHubInstance + +return moduleApiTable diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/SettingsPageFactory.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/SettingsPageFactory.lua new file mode 100644 index 0000000..d11aaa5 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/SettingsPageFactory.lua @@ -0,0 +1,370 @@ +--[[ + Filename: SettingsPageFactory.lua + Written by: jeditkacheff + Version 1.0 + Description: Base Page Functionality for all Settings Pages +--]] +----------------- SERVICES ------------------------------ +local GuiService = game:GetService("GuiService") +local HttpService = game:GetService("HttpService") +local UserInputService = game:GetService("UserInputService") + +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") + +----------- UTILITIES -------------- +local utility = require(RobloxGui.Modules.Settings.Utility) +local StyleWidgets = require(RobloxGui.Modules.StyleWidgets) + + +----------- VARIABLES -------------- +RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface") +local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled() + +local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end) +local FFlagUseNotificationsLocalization = success and result + +----------- CONSTANTS -------------- +local HEADER_SPACING = 5 +if utility:IsSmallTouchScreen() then + HEADER_SPACING = 0 +end + +----------- CLASS DECLARATION -------------- +local function Initialize() + local this = {} + this.HubRef = nil + this.LastSelectedObject = nil + this.TabPosition = 0 + this.Active = false + this.OpenStateChangedCount = 0 + this.ShouldShowBottomBar = true + this.ShouldShowHubBar = true + this.IsPageClipped = true + local rows = {} + local displayed = false + + ------ TAB CREATION ------- + this.TabHeader = utility:Create'TextButton' + { + Name = "Header", + Text = "", + BackgroundTransparency = 1, + Size = UDim2.new(1/5, 0,1,0), + Position = UDim2.new(0,0,0,0) + }; + if utility:IsSmallTouchScreen() then + this.TabHeader.Size = UDim2.new(0,84,1,0) + elseif isTenFootInterface then + this.TabHeader.Size = UDim2.new(0,220,1,0) + end + this.TabHeader.MouseButton1Click:connect(function() + if this.HubRef then + this.HubRef:SwitchToPage(this, true) + end + end) + + local icon = utility:Create'ImageLabel' + { + Name = "Icon", + BackgroundTransparency = 1, + Size = UDim2.new(0.75, 0, 0.75, 0), + Position = UDim2.new(0,10,0.5,-18), + Image = "", + ImageTransparency = 0.5, + Parent = this.TabHeader + }; + local iconAspectRatio = utility:Create'UIAspectRatioConstraint' + { + Name = "AspectRatioConstraint", + AspectRatio = 1, + Parent = icon + }; + + local title = utility:Create'TextLabel' + { + Name = "Title", + Text = "", + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size24, + TextColor3 = Color3.new(1,1,1), + BackgroundTransparency = 1, + Size = UDim2.new(1.05,0,1,0), --overwritten + Position = UDim2.new(1.2,0,0,0), --overwritten + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = 0.5 + }; + + local titleTextSizeConstraint = Instance.new("UITextSizeConstraint") + titleTextSizeConstraint.MaxTextSize = 24 + if FFlagUseNotificationsLocalization then + title.Parent = this.TabHeader + title.TextScaled = true + title.TextWrapped = true + titleTextSizeConstraint.Parent = title + else + title.Parent = icon + end + + if utility:IsSmallTouchScreen() then + title.FontSize = Enum.FontSize.Size18 + titleTextSizeConstraint.MaxTextSize = 18 + elseif isTenFootInterface then + title.FontSize = Enum.FontSize.Size48 + titleTextSizeConstraint.MaxTextSize = 48 + end + + local tabSelection = StyleWidgets.MakeTabSelectionWidget(this.TabHeader) + local titleScaleInitial = Vector2.new(title.Size.X.Scale, title.Size.Y.Scale) + local function onResized() + if not this.TabHeader then + return + end + + if utility:IsSmallTouchScreen() then + this.TabHeader.Icon.Size = UDim2.new(0,34,0,28) + this.TabHeader.Icon.Position = UDim2.new(this.TabHeader.Icon.Position.X.Scale,this.TabHeader.Icon.Position.X.Offset,0.5,-14) + this.TabHeader.Icon.AnchorPoint = Vector2.new(0, 0) + elseif isTenFootInterface then + this.TabHeader.Icon.Size = UDim2.new(0,88,0,74) + this.TabHeader.Icon.Position = UDim2.new(0,0,0.5,0) + this.TabHeader.Icon.AnchorPoint = Vector2.new(0, 0.5) + else + this.TabHeader.Icon.Size = UDim2.new(0,44,0,37) + this.TabHeader.Icon.Position = UDim2.new(0,15,0.5,-18) + this.TabHeader.Icon.AnchorPoint = Vector2.new(0, 0) + end + + local isPortrait = utility:IsPortrait() + if isPortrait then + this.TabHeader.Icon.Position = UDim2.new(0.5, 0, 0.5, 0) + this.TabHeader.Icon.AnchorPoint = Vector2.new(0.5, 0.5) + this.TabHeader.Icon.Size = UDim2.new(0.5, 0, 0.5, 0) + if FFlagUseNotificationsLocalization then + this.TabHeader.Title.Visible = false + else + this.TabHeader.Icon.Title.Visible = false + end + else + if FFlagUseNotificationsLocalization then + this.TabHeader.Title.Visible = true + else + this.TabHeader.Icon.Title.Visible = true + end + end + + if FFlagUseNotificationsLocalization then + local iconSize = this.TabHeader.Icon.AbsoluteSize + local paddingLeft = 0.125 + local paddingRight = 0.025 + + title.Position = UDim2.new( + paddingLeft, + iconSize.X, + 0.225, + 0 + ) + title.Size = UDim2.new( + titleScaleInitial.X - paddingLeft - paddingRight, + -iconSize.X, + 0.5, + 0 + ) + end + end --end local function onResized() + + utility:OnResized(this.TabHeader, onResized) + + ------ PAGE CREATION ------- + this.Page = utility:Create'Frame' + { + Name = "Page", + BackgroundTransparency = 1, + Size = UDim2.new(1,0,1,0) + }; + + this.PageListLayout = utility:Create'UIListLayout' + { + Name = "RowListLayout", + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Top, + Padding = UDim.new(0, 3), + SortOrder = Enum.SortOrder.LayoutOrder, + Parent = this.Page + }; + + + -- make sure each page has a unique selection group (for gamepad selection) + GuiService:AddSelectionParent(HttpService:GenerateGUID(false), this.Page) + + ----------------- Events ------------------------ + + this.Displayed = Instance.new("BindableEvent") + this.Displayed.Name = "Displayed" + + this.Displayed.Event:connect(function() + if not this.HubRef.Shield.Visible then return end + + this:SelectARow() + end) + + this.Hidden = Instance.new("BindableEvent") + this.Hidden.Event:connect(function() + if GuiService.SelectedCoreObject and GuiService.SelectedCoreObject:IsDescendantOf(this.Page) then + GuiService.SelectedCoreObject = nil + end + end) + this.Hidden.Name = "Hidden" + + ----------------- FUNCTIONS ------------------------ + function this:SelectARow(forced) -- Selects the first row or the most recently selected row + if forced or not GuiService.SelectedCoreObject or not GuiService.SelectedCoreObject:IsDescendantOf(this.Page) then + if this.LastSelectedObject then + GuiService.SelectedCoreObject = this.LastSelectedObject + else + if rows and #rows > 0 then + local valueChangerFrame = nil + + if type(rows[1].ValueChanger) ~= "table" then + valueChangerFrame = rows[1].ValueChanger + else + valueChangerFrame = rows[1].ValueChanger.SliderFrame and + rows[1].ValueChanger.SliderFrame or rows[1].ValueChanger.SelectorFrame + end + GuiService.SelectedCoreObject = valueChangerFrame + end + end + end + end + + function this:Display(pageParent, skipAnimation) + this.OpenStateChangedCount = this.OpenStateChangedCount + 1 + + if this.TabHeader then + this.TabHeader.TabSelection.Visible = true + this.TabHeader.Icon.ImageTransparency = 0 + if FFlagUseNotificationsLocalization then + this.TabHeader.Title.TextTransparency = 0 + else + this.TabHeader.Icon.Title.TextTransparency = 0 + end + end + + this.Page.Parent = pageParent + this.Page.Visible = true + + local endPos = UDim2.new(0,0,0,0) + local animationComplete = function() + this.Page.Visible = true + displayed = true + this.Displayed:Fire() + end + if skipAnimation then + this.Page.Position = endPos + animationComplete() + else + this.Page:TweenPosition(endPos, Enum.EasingDirection.In, Enum.EasingStyle.Quad, 0.1, true, animationComplete) + end + end + function this:Hide(direction, newPagePos, skipAnimation, delayBeforeHiding) + this.OpenStateChangedCount = this.OpenStateChangedCount + 1 + + if this.TabHeader then + this.TabHeader.TabSelection.Visible = false + this.TabHeader.Icon.ImageTransparency = 0.5 + if FFlagUseNotificationsLocalization then + this.TabHeader.Title.TextTransparency = 0.5 + else + this.TabHeader.Icon.Title.TextTransparency = 0.5 + end + end + + if this.Page.Parent then + local endPos = UDim2.new(1 * direction,0,0,0) + local animationComplete = function() + this.Page.Visible = false + this.Page.Position = UDim2.new(this.TabPosition - newPagePos,0,0,0) + displayed = false + this.Hidden:Fire() + end + + local remove = function() + if skipAnimation then + this.Page.Position = endPos + animationComplete() + else + this.Page:TweenPosition(endPos, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.1, true, animationComplete) + end + end + + if delayBeforeHiding then + local myOpenStateChangedCount = this.OpenStateChangedCount + delay(delayBeforeHiding, function() + if myOpenStateChangedCount == this.OpenStateChangedCount then + remove() + end + end) + else + remove() + end + end + end + + function this:GetDisplayed() + return displayed + end + + function this:GetVisibility() + return this.Page.Parent + end + + function this:GetTabHeader() + return this.TabHeader + end + + function this:SetHub(hubRef) + this.HubRef = hubRef + + for i, row in next, rows do + if type(row.ValueChanger) == 'table' then + row.ValueChanger.HubRef = this.HubRef + end + end + end + + function this:GetSize() + return this.Page.AbsoluteSize + end + + function this:AddRow(RowFrame, RowLabel, ValueChangerInstance, ExtraRowSpacing) + rows[#rows + 1] = {SelectionFrame = RowFrame, Label = RowLabel, ValueChanger = ValueChangerInstance} + + local rowFrameYSize = 0 + if RowFrame then + rowFrameYSize = RowFrame.Size.Y.Offset + end + + if ExtraRowSpacing then + this.Page.Size = UDim2.new(1, 0, 0, this.Page.Size.Y.Offset + rowFrameYSize + ExtraRowSpacing) + else + this.Page.Size = UDim2.new(1, 0, 0, this.Page.Size.Y.Offset + rowFrameYSize) + end + + if this.HubRef and type(ValueChangerInstance) == 'table' then + ValueChangerInstance.HubRef = this.HubRef + end + end + + return this +end + + +-------- public facing API ---------------- +local moduleApiTable = {} + +function moduleApiTable:CreateNewPage() + return Initialize() +end + +return moduleApiTable \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/ShareGameMaster.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/ShareGameMaster.lua new file mode 100644 index 0000000..bfb2388 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/ShareGameMaster.lua @@ -0,0 +1,30 @@ +local CorePackages = game:GetService("CorePackages") +local Modules = game:GetService("CoreGui").RobloxGui.Modules + +local Roact = require(CorePackages.Roact) +local Rodux = require(CorePackages.Rodux) +local RoactRodux = require(CorePackages.RoactRodux) + +local ShareGame = Modules.Settings.Pages.ShareGame +local App = require(ShareGame.Components.App) +local AppReducer = require(ShareGame.AppReducer) + +local ShareGameMaster = {} + +function ShareGameMaster.createApp(parentGui) + local self = {} + self.store = Rodux.Store.new(AppReducer, nil, { Rodux.thunkMiddleware }) + + self._instanceHandle = Roact.mount( + Roact.createElement(RoactRodux.StoreProvider, { + store = self.store + }, { + Roact.createElement(App, { + pageTarget = parentGui, + }) + }) + ) + return self +end + +return ShareGameMaster diff --git a/Client2018/content/scripts/CoreScripts/Modules/Settings/Utility.lua b/Client2018/content/scripts/CoreScripts/Modules/Settings/Utility.lua new file mode 100644 index 0000000..c8baf00 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Settings/Utility.lua @@ -0,0 +1,2649 @@ +--[[ + Filename: Utility.lua + Written by: jeditkacheff + Version 1.0 + Description: Utility module for CoreScripts +--]] + +------------------ CONSTANTS -------------------- +local SELECTED_COLOR = Color3.fromRGB(0,162,255) +local NON_SELECTED_COLOR = Color3.fromRGB(78,84,96) + +local ARROW_COLOR = Color3.fromRGB(204, 204, 204) +local ARROW_COLOR_HOVER = Color3.fromRGB(255, 255, 255) +local ARROW_COLOR_TOUCH = ARROW_COLOR_HOVER +local ARROW_COLOR_INACTIVE = Color3.fromRGB(150, 150, 150) + +local SELECTED_LEFT_IMAGE = "rbxasset://textures/ui/Settings/Slider/SelectedBarLeft.png" +local NON_SELECTED_LEFT_IMAGE = "rbxasset://textures/ui/Settings/Slider/BarLeft.png" +local SELECTED_RIGHT_IMAGE = "rbxasset://textures/ui/Settings/Slider/SelectedBarRight.png" +local NON_SELECTED_RIGHT_IMAGE= "rbxasset://textures/ui/Settings/Slider/BarRight.png" + +local CONTROLLER_SCROLL_DELTA = 0.2 +local CONTROLLER_THUMBSTICK_DEADZONE = 0.8 + +local DROPDOWN_BG_TRANSPARENCY = 0.2 + +------------- SERVICES ---------------- +local HttpService = game:GetService("HttpService") +local UserInputService = game:GetService("UserInputService") +local GuiService = game:GetService("GuiService") +local RunService = game:GetService("RunService") +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:FindFirstChild("RobloxGui") +local ContextActionService = game:GetService("ContextActionService") +local VRService = game:GetService("VRService") + +--------------- FLAGS ---------------- + +local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end) +local FFlagUseNotificationsLocalization = success and result + +local FFlagFixInactiveSelectorArrowsSuccess, FFlagFixInactiveSelectorArrowsResult = pcall(function() return settings():GetFFlag("FixInactiveSelectorArrows") end) +local FFlagFixInactiveSelectorArrows = FFlagFixInactiveSelectorArrowsSuccess and FFlagFixInactiveSelectorArrowsResult + +------------------ VARIABLES -------------------- +local tenFootInterfaceEnabled = require(RobloxGui.Modules:WaitForChild("TenFootInterface")):IsEnabled() + +----------- UTILITIES -------------- +local Util = {} +do + function Util.Create(instanceType) + return function(data) + local obj = Instance.new(instanceType) + local parent = nil + for k, v in pairs(data) do + if type(k) == 'number' then + v.Parent = obj + elseif k == 'Parent' then + parent = v + else + obj[k] = v + end + end + if parent then + obj.Parent = parent + end + return obj + end + end +end + +local onResizedCallbacks = {} +setmetatable(onResizedCallbacks, { __mode = 'k' }) + +-- used by several guis to show no selection adorn +local noSelectionObject = Util.Create'ImageLabel' +{ + Image = "", + BackgroundTransparency = 1 +}; + + +-- MATH -- +function clamp(low, high, input) + return math.max(low, math.min(high, input)) +end + +function ClampVector2(low, high, input) + return Vector2.new(clamp(low.x, high.x, input.x), clamp(low.y, high.y, input.y)) +end + +---- TWEENZ ---- +local function Linear(t, b, c, d) + if t >= d then + return b + c + end + + return c*t/d + b +end + +local function EaseOutQuad(t, b, c, d) + if t >= d then + return b + c + end + + t = t/d + return b - c*t*(t - 2) +end + +local function EaseInOutQuad(t, b, c, d) + if t >= d then + return b + c + end + + t = t/d + if t < 1/2 then + return 2*c*t*t + b + end + return b + c*(2*(2 - t)*t - 1) +end + +function PropertyTweener(instance, prop, start, final, duration, easingFunc, cbFunc) + local this = {} + this.StartTime = tick() + this.EndTime = this.StartTime + duration + this.Cancelled = false + + local finished = false + local percentComplete = 0 + + local function finalize() + if instance then + instance[prop] = easingFunc(1, start, final - start, 1) + end + finished = true + percentComplete = 1 + if cbFunc then + cbFunc() + end + end + + -- Initial set + instance[prop] = easingFunc(0, start, final - start, duration) + coroutine.wrap(function() + local now = tick() + while now < this.EndTime and instance do + if this.Cancelled then + return + end + instance[prop] = easingFunc(now - this.StartTime, start, final - start, duration) + percentComplete = clamp(0, 1, (now - this.StartTime) / duration) + RunService.RenderStepped:wait() + now = tick() + end + if this.Cancelled == false and instance then + finalize() + end + end)() + + function this:GetFinal() + return final + end + + function this:GetPercentComplete() + return percentComplete + end + + function this:IsFinished() + return finished + end + + function this:Finish() + if not finished then + self:Cancel() + finalize() + end + end + + function this:Cancel() + this.Cancelled = true + end + + return this +end + +----------- CLASS DECLARATION -------------- + +local function CreateSignal() + local sig = {} + + local mSignaler = Instance.new('BindableEvent') + + local mArgData = nil + local mArgDataCount = nil + + function sig:fire(...) + mArgData = {...} + mArgDataCount = select('#', ...) + mSignaler:Fire() + end + + function sig:connect(f) + if not f then error("connect(nil)", 2) end + return mSignaler.Event:Connect(function() + f(unpack(mArgData, 1, mArgDataCount)) + end) + end + + function sig:wait() + mSignaler.Event:wait() + if not mArgData then + error("Missing arg data, likely due to :TweenSize/Position corrupting threadrefs.") + end + return unpack(mArgData, 1, mArgDataCount) + end + + return sig +end + +local function getViewportSize() + while not workspace.CurrentCamera do + workspace.Changed:wait() + end + + -- ViewportSize is initally set to 1, 1 in Camera.cpp constructor. + -- Also check against 0, 0 incase this is changed in the future. + while workspace.CurrentCamera.ViewportSize == Vector2.new(0,0) or + workspace.CurrentCamera.ViewportSize == Vector2.new(1,1) do + workspace.CurrentCamera.Changed:wait() + end + + return workspace.CurrentCamera.ViewportSize +end + +local function isSmallTouchScreen() + local viewportSize = getViewportSize() + return UserInputService.TouchEnabled and (viewportSize.Y < 500 or viewportSize.X < 700) +end + +local function isPortrait() + local viewport = getViewportSize() + return viewport.Y > viewport.X +end + +local function isTenFootInterface() + return tenFootInterfaceEnabled +end + +local function usesSelectedObject() + --VR does not use selected objects (in the same way as gamepad) + if VRService.VREnabled then return false end + --Touch does not use selected objects unless there's also a gamepad + if UserInputService.TouchEnabled and not UserInputService.GamepadEnabled then return false end + --PC with gamepad, console... does use selected objects + return true +end + +local function isPosOverGui(pos, gui, debug) -- does not account for rotation + local ax, ay = gui.AbsolutePosition.x, gui.AbsolutePosition.y + local sx, sy = gui.AbsoluteSize.x, gui.AbsoluteSize.y + local bx, by = ax+sx, ay+sy + + return pos.x > ax and pos.x < bx and pos.y > ay and pos.y < by +end + +local function isPosOverGuiWithClipping(pos, gui) -- isPosOverGui, accounts for clipping and visibility, does not account for rotation + if not isPosOverGui(pos, gui) then + return false + end + + local clipping = false + local check = gui + while true do + if check == nil or (not check:IsA'GuiObject' and not check:IsA'LayerCollector') then + clipping = true + if check and check:IsA'CoreGui' then + clipping = false + end + break + end + + if check:IsA'GuiObject' and not check.Visible then + clipping = true + break + end + if check:IsA'LayerCollector' or check.ClipsDescendants then + if not isPosOverGui(pos, check) then + clipping = true + break + end + end + + check = check.Parent + end + + return not clipping +end + +local function areGuisIntersecting(a, b) -- does not account for rotation + local aax, aay = a.AbsolutePosition.x, a.AbsolutePosition.y + local asx, asy = a.AbsoluteSize.x, a.AbsoluteSize.y + local abx, aby = aax+asx, aay+asy + local bax, bay = b.AbsolutePosition.x, b.AbsolutePosition.y + local bsx, bsy = b.AbsoluteSize.x, b.AbsoluteSize.y + local bbx, bby = bax+bsx, bay+bsy + + local intersectingX = aax < bbx and abx > bax + local intersectingY = aay < bby and aby > bay + local intersecting = intersectingX and intersectingY + + return intersecting +end + +local function isGuiVisible(gui, debug) -- true if any part of the gui is visible on the screen, considers clipping, does not account for rotation + local clipping = false + local check = gui + while true do + if check == nil or not check:IsA'GuiObject' and not check:IsA'LayerCollector' then + clipping = true + if check and check:IsA'CoreGui' then + clipping = false + end + break + end + + if check:IsA'GuiObject' and not check.Visible then + clipping = true + break + end + if check:IsA'LayerCollector' or check.ClipsDescendants then + if not areGuisIntersecting(check, gui) then + clipping = true + break + end + end + + check = check.Parent + end + + return not clipping +end + +local function addHoverState(button, instance, onNormalButtonState, onHoverButtonState) + local function onNormalButtonStateCallback() + if FFlagFixInactiveSelectorArrows then + if button.Active then + onNormalButtonState(instance) + end + else + onNormalButtonState(instance) + end + end + local function onHoverButtonStateCallback() + if FFlagFixInactiveSelectorArrows then + if button.Active then + onHoverButtonState(instance) + end + else + onHoverButtonState(instance) + end + end + + button.MouseEnter:Connect(onHoverButtonStateCallback) + button.SelectionGained:Connect(onHoverButtonStateCallback) + button.MouseLeave:Connect(onNormalButtonStateCallback) + button.SelectionLost:Connect(onNormalButtonStateCallback) + + onNormalButtonState(instance) +end + +local function addOnResizedCallback(key, callback) + onResizedCallbacks[key] = callback + callback(getViewportSize(), isPortrait()) +end + +local gamepadSet = { + [Enum.UserInputType.Gamepad1] = true; + [Enum.UserInputType.Gamepad2] = true; + [Enum.UserInputType.Gamepad3] = true; + [Enum.UserInputType.Gamepad4] = true; + [Enum.UserInputType.Gamepad5] = true; + [Enum.UserInputType.Gamepad6] = true; + [Enum.UserInputType.Gamepad7] = true; + [Enum.UserInputType.Gamepad8] = true; +} + +local function MakeDefaultButton(name, size, clickFunc, pageRef, hubRef) + local SelectionOverrideObject = Util.Create'ImageLabel' + { + Image = "", + BackgroundTransparency = 1, + }; + + local button = Util.Create'ImageButton' + { + Name = name .. "Button", + Image = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButton.png", + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8,6,46,44), + AutoButtonColor = false, + BackgroundTransparency = 1, + Size = size, + ZIndex = 2, + SelectionImageObject = SelectionOverrideObject + }; + + local enabled = Util.Create'BoolValue' + { + Name = 'Enabled', + Parent = button, + Value = true + } + + if clickFunc then + button.MouseButton1Click:Connect(function() + clickFunc(gamepadSet[UserInputService:GetLastInputType()] or false) + end) + end + + local function isPointerInput(inputObject) + return inputObject.UserInputType == Enum.UserInputType.MouseMovement or inputObject.UserInputType == Enum.UserInputType.Touch + end + + local rowRef = nil + local function setRowRef(ref) + rowRef = ref + end + + local function selectButton() + local hub = hubRef + if hub == nil then + if pageRef then + hub = pageRef.HubRef + end + end + + if (hub and hub.Active) or hub == nil then + button.Image = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png" + + local scrollTo = button + if rowRef then + scrollTo = rowRef + end + if hub then + hub:ScrollToFrame(scrollTo) + end + end + end + + local function deselectButton() + button.Image = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButton.png" + end + + button.InputBegan:Connect(function(inputObject) + if button.Selectable and isPointerInput(inputObject) then + selectButton() + end + end) + button.InputEnded:Connect(function(inputObject) + if button.Selectable and GuiService.SelectedCoreObject ~= button and isPointerInput(inputObject) then + deselectButton() + end + end) + + + button.SelectionGained:Connect(function() + selectButton() + end) + button.SelectionLost:Connect(function() + deselectButton() + end) + + local guiServiceCon = GuiService.Changed:Connect(function(prop) + if prop ~= "SelectedCoreObject" then return end + if not usesSelectedObject() then return end + + if GuiService.SelectedCoreObject == nil or GuiService.SelectedCoreObject ~= button then + deselectButton() + return + end + + if button.Selectable then + selectButton() + end + end) + + return button, setRowRef +end + +local function MakeButton(name, text, size, clickFunc, pageRef, hubRef) + local button, setRowRef = MakeDefaultButton(name, size, clickFunc, pageRef, hubRef) + + local textLabel = Util.Create'TextLabel' + { + Name = name .. "TextLabel", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, -8), + Position = UDim2.new(0,0,0,0), + TextColor3 = Color3.fromRGB(255,255,255), + TextYAlignment = Enum.TextYAlignment.Center, + Font = Enum.Font.SourceSansBold, + TextSize = 24, + Text = text, + TextScaled = true, + TextWrapped = true, + ZIndex = 2, + Parent = button + }; + local constraint = Instance.new("UITextSizeConstraint",textLabel) + + if isSmallTouchScreen() then + textLabel.TextSize = 18 + elseif isTenFootInterface() then + textLabel.TextSize = 36 + end + constraint.MaxTextSize = textLabel.TextSize + + return button, textLabel, setRowRef +end + +local function MakeImageButton(name, image, size, imageSize, clickFunc, pageRef, hubRef) + local button, setRowRef = MakeDefaultButton(name, size, clickFunc, pageRef, hubRef) + + local imageLabel = Util.Create'ImageLabel' + { + Name = name .. "ImageLabel", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = imageSize, + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Image = image, + ZIndex = 2, + Parent = button + }; + + return button, imageLabel, setRowRef +end + +local function AddButtonRow(pageToAddTo, name, text, size, clickFunc, hubRef) + local button, textLabel, setRowRef = MakeButton(name, text, size, clickFunc, pageToAddTo, hubRef) + local row = Util.Create'Frame' + { + Name = name .. "Row", + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, size.Y.Scale, size.Y.Offset), + Parent = pageToAddTo.Page + } + button.Parent = row + button.AnchorPoint = Vector2.new(1, 0) + button.Position = UDim2.new(1, -20, 0, 0) + return row, button, textLabel, setRowRef +end + +local function CreateDropDown(dropDownStringTable, startPosition, settingsHub) + -------------------- CONSTANTS ------------------------ + local DEFAULT_DROPDOWN_TEXT = "Choose One" + local SCROLLING_FRAME_PIXEL_OFFSET = 25 + local SELECTION_TEXT_COLOR_NORMAL = Color3.fromRGB(178,178,178) + local SELECTION_TEXT_COLOR_NORMAL_VR = Color3.fromRGB(229,229,229) + local SELECTION_TEXT_COLOR_HIGHLIGHTED = Color3.fromRGB(255,255,255) + + -------------------- VARIABLES ------------------------ + local lastSelectedCoreObject = nil + + -------------------- SETUP ------------------------ + local this = {} + this.CurrentIndex = nil + + local indexChangedEvent = Instance.new("BindableEvent") + indexChangedEvent.Name = "IndexChanged" + + if type(dropDownStringTable) ~= "table" then + error("CreateDropDown dropDownStringTable (first arg) is not a table", 2) + return this + end + + local indexChangedEvent = Instance.new("BindableEvent") + indexChangedEvent.Name = "IndexChanged" + + local interactable = true + local guid = HttpService:GenerateGUID(false) + local dropDownButtonEnabled + local lastStringTable = dropDownStringTable + + this.CurrentIndex = 0 + + ----------------- GUI SETUP ------------------------ + local DropDownFullscreenFrame = Util.Create'ImageButton' + { + Name = "DropDownFullscreenFrame", + BackgroundTransparency = DROPDOWN_BG_TRANSPARENCY, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Color3.fromRGB(0,0,0), + ZIndex = 10, + Active = true, + Visible = false, + Selectable = false, + AutoButtonColor = false, + Parent = CoreGui.RobloxGui + }; + + local function onVREnabled(prop) + if prop ~= "VREnabled" then + return + end + if VRService.VREnabled then + local Panel3D = require(CoreGui.RobloxGui.Modules.VR.Panel3D) + DropDownFullscreenFrame.Parent = Panel3D.Get("SettingsMenu"):GetGUI() + DropDownFullscreenFrame.BackgroundTransparency = 1 + else + DropDownFullscreenFrame.Parent = CoreGui.RobloxGui + DropDownFullscreenFrame.BackgroundTransparency = DROPDOWN_BG_TRANSPARENCY + end + + --Force the gui to update, but only if onVREnabled is fired later on + if this.UpdateDropDownList then + this:UpdateDropDownList(lastStringTable) + end + end + VRService.Changed:Connect(onVREnabled) + onVREnabled("VREnabled") + + local DropDownSelectionFrame = Util.Create'ImageLabel' + { + Name = "DropDownSelectionFrame", + Image = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButton.png", + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8,6,46,44), + BackgroundTransparency = 1, + Size = UDim2.new(0.6, 0, 0.9, 0), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + ZIndex = 10, + Parent = DropDownFullscreenFrame + }; + + local DropDownScrollingFrame = Util.Create'ScrollingFrame' + { + Name = "DropDownScrollingFrame", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1, -20, 1, -SCROLLING_FRAME_PIXEL_OFFSET), + Position = UDim2.new(0, 10, 0, 10), + ZIndex = 10, + Parent = DropDownSelectionFrame + }; + + local guiServiceChangeCon = nil + local active = false + local hideDropDownSelection = function(name, inputState) + if name ~= nil and inputState ~= Enum.UserInputState.Begin then return end + this.DropDownFrame.Selectable = interactable + + --Make sure to set the hub to Active again so selecting the + --dropdown button will highlight it + settingsHub:SetActive(true) + + if DropDownFullscreenFrame.Visible and usesSelectedObject() then + GuiService.SelectedCoreObject = lastSelectedCoreObject + end + DropDownFullscreenFrame.Visible = false + if guiServiceChangeCon then guiServiceChangeCon:Disconnect() end + ContextActionService:UnbindCoreAction(guid .. "Action") + ContextActionService:UnbindCoreAction(guid .. "FreezeAction") + + dropDownButtonEnabled.Value = interactable + active = false + + if VRService.VREnabled then + local Panel3D = require(CoreGui.RobloxGui.Modules.VR.Panel3D) + Panel3D.Get("SettingsMenu"):SetSubpanelDepth(DropDownFullscreenFrame, 0) + end + end + local noOpFunc = function() end + + local DropDownFrameClicked = function() + if not interactable then return end + + this.DropDownFrame.Selectable = false + active = true + + DropDownFullscreenFrame.Visible = true + if VRService.VREnabled then + local Panel3D = require(CoreGui.RobloxGui.Modules.VR.Panel3D) + Panel3D.Get("SettingsMenu"):SetSubpanelDepth(DropDownFullscreenFrame, 0.5) + end + if not this.CurrentIndex then this.CurrentIndex = 1 end + if this.CurrentIndex <= 0 then this.CurrentIndex = 1 end + + lastSelectedCoreObject = this.DropDownFrame + GuiService.SelectedCoreObject = this.Selections[this.CurrentIndex] + + guiServiceChangeCon = GuiService:GetPropertyChangedSignal("SelectedCoreObject"):Connect(function() + for i = 1, #this.Selections do + if GuiService.SelectedCoreObject == this.Selections[i] then + this.Selections[i].TextColor3 = SELECTION_TEXT_COLOR_HIGHLIGHTED + else + this.Selections[i].TextColor3 = VRService.VREnabled and SELECTION_TEXT_COLOR_NORMAL_VR or SELECTION_TEXT_COLOR_NORMAL + end + end + end) + + ContextActionService:BindCoreAction(guid .. "FreezeAction", noOpFunc, false, Enum.UserInputType.Keyboard, Enum.UserInputType.Gamepad1) + ContextActionService:BindCoreAction(guid .. "Action", hideDropDownSelection, false, Enum.KeyCode.ButtonB, Enum.KeyCode.Escape) + + settingsHub:SetActive(false) + + dropDownButtonEnabled.Value = false + end + + local dropDownFrameSize = UDim2.new(0.6, 0, 0, 50) + this.DropDownFrame = MakeButton("DropDownFrame", DEFAULT_DROPDOWN_TEXT, dropDownFrameSize, DropDownFrameClicked, nil, settingsHub) + this.DropDownFrame.Position = UDim2.new(1, 0, 0.5, 0) + this.DropDownFrame.AnchorPoint = Vector2.new(1, 0.5) + + dropDownButtonEnabled = this.DropDownFrame.Enabled + local selectedTextLabel = this.DropDownFrame.DropDownFrameTextLabel + selectedTextLabel.Position = UDim2.new(0, 15, 0, 0) + selectedTextLabel.Size = UDim2.new(1, -50, 1, -8) + selectedTextLabel.ClipsDescendants = true + selectedTextLabel.TextXAlignment = Enum.TextXAlignment.Left + local dropDownImage = Util.Create'ImageLabel' + { + Name = "DropDownImage", + Image = "rbxasset://textures/ui/Settings/DropDown/DropDown.png", + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(1, 0.5), + Size = UDim2.new(0,15,0,10), + Position = UDim2.new(1,-12,0.5,0), + ZIndex = 2, + Parent = this.DropDownFrame + }; + this.DropDownImage = dropDownImage + + + ---------------------- FUNCTIONS ----------------------------------- + local function setSelection(index) + local shouldFireChanged = false + for i, selectionLabel in pairs(this.Selections) do + if i == index then + selectedTextLabel.Text = selectionLabel.Text + this.CurrentIndex = i + + shouldFireChanged = true + end + end + + if shouldFireChanged then + indexChangedEvent:Fire(index) + end + end + + local function setSelectionByValue(value) + local shouldFireChanged = false + for i, selectionLabel in pairs(this.Selections) do + if selectionLabel.Text == value then + selectedTextLabel.Text = selectionLabel.Text + this.CurrentIndex = i + + shouldFireChanged = true + end + end + + if shouldFireChanged then + indexChangedEvent:Fire(this.CurrentIndex) + end + return shouldFireChanged + end + + local enterIsDown = false + local function processInput(input) + if input.UserInputState == Enum.UserInputState.Begin then + if input.KeyCode == Enum.KeyCode.Return then + if GuiService.SelectedCoreObject == this.DropDownFrame or this.SelectionInfo and this.SelectionInfo[GuiService.SelectedCoreObject] then + enterIsDown = true + end + end + elseif input.UserInputState == Enum.UserInputState.End then + if input.KeyCode == Enum.KeyCode.Return and enterIsDown then + enterIsDown = false + if GuiService.SelectedCoreObject == this.DropDownFrame then + DropDownFrameClicked() + elseif this.SelectionInfo and this.SelectionInfo[GuiService.SelectedCoreObject] then + local info = this.SelectionInfo[GuiService.SelectedCoreObject] + info.Clicked() + end + end + end + end + + local function setIsFaded(isFaded) + if isFaded then + this.DropDownFrame.DropDownFrameTextLabel.TextTransparency = 0.5 + this.DropDownFrame.ImageTransparency = 0.5 + this.DropDownImage.ImageTransparency = 0.5 + else + this.DropDownFrame.DropDownFrameTextLabel.TextTransparency = 0 + this.DropDownFrame.ImageTransparency = 0 + this.DropDownImage.ImageTransparency = 0 + end + end + + + --------------------- PUBLIC FACING FUNCTIONS ----------------------- + this.IndexChanged = indexChangedEvent.Event + + function this:SetSelectionIndex(newIndex) + setSelection(newIndex) + end + + function this:SetSelectionByValue(value) + return setSelectionByValue(value) + end + + function this:ResetSelectionIndex() + this.CurrentIndex = nil + selectedTextLabel.Text = DEFAULT_DROPDOWN_TEXT + hideDropDownSelection() + end + + function this:GetSelectedIndex() + return this.CurrentIndex + end + + function this:SetZIndex(newZIndex) + this.DropDownFrame.ZIndex = newZIndex + dropDownImage.ZIndex = newZIndex + selectedTextLabel.ZIndex = newZIndex + end + + function this:SetInteractable(value) + interactable = value + this.DropDownFrame.Selectable = interactable + + if not interactable then + hideDropDownSelection() + setIsFaded(VRService.VREnabled) + if not VRService.VREnabled then + this:SetZIndex(1) + end + else + setIsFaded(false) + if not VRService.VREnabled then + this:SetZIndex(2) + end + end + + dropDownButtonEnabled.Value = value and not active + end + + + function this:UpdateDropDownList(dropDownStringTable) + lastStringTable = dropDownStringTable + + if this.Selections then + for i = 1, #this.Selections do + this.Selections[i]:Destroy() + end + end + + this.Selections = {} + this.SelectionInfo = {} + + local vrEnabled = VRService.VREnabled + local font = vrEnabled and Enum.Font.SourceSansBold or Enum.Font.SourceSans + local textSize = vrEnabled and 36 or 24 + + local itemHeight = vrEnabled and 70 or 50 + local itemSpacing = itemHeight + 1 + + local dropDownWidth = vrEnabled and 600 or 400 + + for i,v in pairs(dropDownStringTable) do + local SelectionOverrideObject = Util.Create'Frame' + { + BackgroundTransparency = 0.7, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0) + }; + + local nextSelection = Util.Create'TextButton' + { + Name = "Selection" .. tostring(i), + BackgroundTransparency = 1, + BorderSizePixel = 0, + AutoButtonColor = false, + Size = UDim2.new(1, -28, 0, itemHeight), + Position = UDim2.new(0,14,0, (i - 1) * itemSpacing), + TextColor3 = VRService.VREnabled and SELECTION_TEXT_COLOR_NORMAL_VR or SELECTION_TEXT_COLOR_NORMAL, + Font = font, + TextSize = textSize, + Text = v, + ZIndex = 10, + SelectionImageObject = SelectionOverrideObject, + Parent = DropDownScrollingFrame + }; + + if i == startPosition then + this.CurrentIndex = i + selectedTextLabel.Text = v + nextSelection.TextColor3 = SELECTION_TEXT_COLOR_HIGHLIGHTED + elseif not startPosition and i == 1 then + nextSelection.TextColor3 = SELECTION_TEXT_COLOR_HIGHLIGHTED + end + + local clicked = function() + selectedTextLabel.Text = nextSelection.Text + hideDropDownSelection() + this.CurrentIndex = i + indexChangedEvent:Fire(i) + end + + nextSelection.MouseButton1Click:Connect(clicked) + + nextSelection.MouseEnter:Connect(function() + if usesSelectedObject() then + GuiService.SelectedCoreObject = nextSelection + end + end) + + this.Selections[i] = nextSelection + this.SelectionInfo[nextSelection] = {Clicked = clicked} + end + + GuiService:RemoveSelectionGroup(guid) + GuiService:AddSelectionTuple(guid, unpack(this.Selections)) + + DropDownScrollingFrame.CanvasSize = UDim2.new(1,-20,0,#dropDownStringTable * itemSpacing) + + local function updateDropDownSize() + if DropDownScrollingFrame.CanvasSize.Y.Offset < (DropDownFullscreenFrame.AbsoluteSize.Y - 10) then + DropDownSelectionFrame.Size = UDim2.new(0, dropDownWidth, + 0,DropDownScrollingFrame.CanvasSize.Y.Offset + SCROLLING_FRAME_PIXEL_OFFSET) + else + DropDownSelectionFrame.Size = UDim2.new(0, dropDownWidth, 0.9, 0) + end + end + + DropDownFullscreenFrame.Changed:Connect(function(prop) + if prop ~= "AbsoluteSize" then return end + updateDropDownSize() + end) + + updateDropDownSize() + end + + ----------------------- CONNECTIONS/SETUP -------------------------------- + this:UpdateDropDownList(dropDownStringTable) + + DropDownFullscreenFrame.MouseButton1Click:Connect(hideDropDownSelection) + + settingsHub.PoppedMenu:Connect(function(poppedMenu) + if poppedMenu == DropDownFullscreenFrame then + hideDropDownSelection() + end + end) + + return this +end + + +local function CreateSelector(selectionStringTable, startPosition) + + -------------------- VARIABLES ------------------------ + local lastInputDirection = 0 + local TweenTime = 0.15 + + -------------------- SETUP ------------------------ + local this = {} + this.HubRef = nil + + if type(selectionStringTable) ~= "table" then + error("CreateSelector selectionStringTable (first arg) is not a table", 2) + return this + end + + local indexChangedEvent = Instance.new("BindableEvent") + indexChangedEvent.Name = "IndexChanged" + + local interactable = true + + this.CurrentIndex = 0 + + ----------------- GUI SETUP ------------------------ + this.SelectorFrame = Util.Create'ImageButton' + { + Name = "Selector", + Image = "", + AutoButtonColor = false, + NextSelectionLeft = this.SelectorFrame, + NextSelectionRight = this.SelectorFrame, + BackgroundTransparency = 1, + Size = UDim2.new(0.6,0,0,50), + Position = UDim2.new(1, 0, 0.5, 0), + AnchorPoint = Vector2.new(1, 0.5), + ZIndex = 2, + SelectionImageObject = noSelectionObject + }; + + local leftButton = Util.Create'ImageButton' + { + Name = "LeftButton", + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(0, 0.5), + Position = UDim2.new(0,0,0.5,0), + Size = UDim2.new(0,50,0,50), + Image = "", + ZIndex = 3, + Selectable = false, + SelectionImageObject = noSelectionObject, + Parent = this.SelectorFrame + }; + local rightButton = Util.Create'ImageButton' + { + Name = "RightButton", + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(1, 0.5), + Position = UDim2.new(1,0,0.5,0), + Size = UDim2.new(0,50,0,50), + Image = "", + ZIndex = 3, + Selectable = false, + SelectionImageObject = noSelectionObject, + Parent = this.SelectorFrame + }; + + local leftButtonImage = Util.Create'ImageLabel' + { + Name = "LeftButton", + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5,0,0.5,0), + Size = UDim2.new(0,18,0,30), + Image = "rbxasset://textures/ui/Settings/Slider/Left.png", + ImageColor3 = ARROW_COLOR, + ZIndex = 4, + Parent = leftButton + }; + local rightButtonImage = Util.Create'ImageLabel' + { + Name = "RightButton", + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5,0,0.5,0), + Size = UDim2.new(0,18,0,30), + Image = "rbxasset://textures/ui/Settings/Slider/Right.png", + ImageColor3 = ARROW_COLOR, + ZIndex = 4, + Parent = rightButton + }; + if not UserInputService.TouchEnabled then + local applyNormal, applyHover = + function(instance) instance.ImageColor3 = ARROW_COLOR end, + function(instance) instance.ImageColor3 = ARROW_COLOR_HOVER end + addHoverState(leftButton, leftButtonImage, applyNormal, applyHover) + addHoverState(rightButton, rightButtonImage, applyNormal, applyHover) + end + + + this.Selections = {} + local isSelectionLabelVisible = {} + local isAutoSelectButton = {} + + local autoSelectButton = Util.Create'ImageButton'{ + Name = 'AutoSelectButton', + BackgroundTransparency = 1, + Image = '', + Position = UDim2.new(0, leftButton.Size.X.Offset, 0, 0), + Size = UDim2.new(1, leftButton.Size.X.Offset * -2, 1, 0), + Parent = this.SelectorFrame, + ZIndex = 2, + SelectionImageObject = noSelectionObject + } + autoSelectButton.MouseButton1Click:Connect(function() + if not interactable then return end + if #this.Selections <= 1 then return end + local newIndex = this.CurrentIndex + 1 + if newIndex > #this.Selections then + newIndex = 1 + end + this:SetSelectionIndex(newIndex) + if usesSelectedObject() then + GuiService.SelectedCoreObject = this.SelectorFrame + end + end) + isAutoSelectButton[autoSelectButton] = true + + ---------------------- FUNCTIONS ----------------------------------- + local function setSelection(index, direction) + for i, selectionLabel in pairs(this.Selections) do + local isSelected = (i == index) + + local leftButtonUDim = UDim2.new(0,leftButton.Size.X.Offset,0,0) + local tweenPos = UDim2.new(0,leftButton.Size.X.Offset * direction * 3,0,0) + + if isSelectionLabelVisible[selectionLabel] then + tweenPos = UDim2.new(0,leftButton.Size.X.Offset * -direction * 3,0,0) + end + + if tweenPos.X.Offset < 0 then + tweenPos = UDim2.new(0,tweenPos.X.Offset + (selectionLabel.AbsoluteSize.X/4),0,0) + end + + if isSelected then + isSelectionLabelVisible[selectionLabel] = true + selectionLabel.Position = tweenPos + selectionLabel.Visible = true + PropertyTweener(selectionLabel, "TextTransparency", 1, 0, TweenTime * 1.1, EaseOutQuad) + if selectionLabel:IsDescendantOf(game) then + selectionLabel:TweenPosition(leftButtonUDim, Enum.EasingDirection.In, Enum.EasingStyle.Quad, TweenTime, true) + else + selectionLabel.Position = leftButtonUDim + end + this.CurrentIndex = i + indexChangedEvent:Fire(index) + elseif isSelectionLabelVisible[selectionLabel] then + isSelectionLabelVisible[selectionLabel] = false + PropertyTweener(selectionLabel, "TextTransparency", 0, 1, TweenTime * 1.1, EaseOutQuad) + if selectionLabel:IsDescendantOf(game) then + selectionLabel:TweenPosition(tweenPos, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, TweenTime * 0.9, true) + else + selectionLabel.Position = UDim2.new(tweenPos) + end + end + end + end + + local function stepFunc(inputObject, step) + if not interactable then return end + + if inputObject ~= nil and inputObject.UserInputType ~= Enum.UserInputType.MouseButton1 and + inputObject.UserInputType ~= Enum.UserInputType.Gamepad1 and inputObject.UserInputType ~= Enum.UserInputType.Gamepad2 and + inputObject.UserInputType ~= Enum.UserInputType.Gamepad3 and inputObject.UserInputType ~= Enum.UserInputType.Gamepad4 and + inputObject.UserInputType ~= Enum.UserInputType.Keyboard then return end + + if usesSelectedObject() then + GuiService.SelectedCoreObject = this.SelectorFrame + end + + local newIndex = step + this.CurrentIndex + + local direction = 0 + if newIndex > this.CurrentIndex then + direction = 1 + else + direction = -1 + end + + if newIndex > #this.Selections then + newIndex = 1 + elseif newIndex < 1 then + newIndex = #this.Selections + end + + setSelection(newIndex, direction) + end + + local guiServiceCon = nil + local function connectToGuiService() + guiServiceCon = GuiService:GetPropertyChangedSignal("SelectedCoreObject"):Connect(function() + if #this.Selections <= 0 then + return + end + + if GuiService.SelectedCoreObject == this.SelectorFrame then + this.Selections[this.CurrentIndex].TextTransparency = 0 + else + if GuiService.SelectedCoreObject ~= nil and isAutoSelectButton[GuiService.SelectedCoreObject] then + if VRService.VREnabled then + this.Selections[this.CurrentIndex].TextTransparency = 0 + else + GuiService.SelectedCoreObject = this.SelectorFrame + end + else + this.Selections[this.CurrentIndex].TextTransparency = 0.5 + end + end + end) + end + + --------------------- PUBLIC FACING FUNCTIONS ----------------------- + this.IndexChanged = indexChangedEvent.Event + + function this:SetSelectionIndex(newIndex) + setSelection(newIndex, 1) + end + + function this:GetSelectedIndex() + return this.CurrentIndex + end + + function this:SetZIndex(newZIndex) + leftButton.ZIndex = newZIndex + rightButton.ZIndex = newZIndex + leftButtonImage.ZIndex = newZIndex + rightButtonImage.ZIndex = newZIndex + + for i = 1, #this.Selections do + this.Selections[i].ZIndex = newZIndex + end + end + + function this:SetInteractable(value) + interactable = value + this.SelectorFrame.Selectable = interactable + + if FFlagFixInactiveSelectorArrows then + leftButton.Active = interactable + rightButton.Active = interactable + end + + if not interactable then + for i, selectionLabel in pairs(this.Selections) do + selectionLabel.TextColor3 = Color3.fromRGB(49, 49, 49) + end + if FFlagFixInactiveSelectorArrows then + leftButtonImage.ImageColor3 = ARROW_COLOR_INACTIVE + rightButtonImage.ImageColor3 = ARROW_COLOR_INACTIVE + end + else + for i, selectionLabel in pairs(this.Selections) do + selectionLabel.TextColor3 = Color3.fromRGB(255, 255, 255) + end + if FFlagFixInactiveSelectorArrows then + leftButtonImage.ImageColor3 = ARROW_COLOR + rightButtonImage.ImageColor3 = ARROW_COLOR + end + end + end + + function this:UpdateOptions(selectionStringTable) + for i,v in pairs(this.Selections) do + v:Destroy() + end + + isSelectionLabelVisible = {} + this.Selections = {} + + for i,v in pairs(selectionStringTable) do + local nextSelection = Util.Create'TextLabel' + { + Name = "Selection" .. tostring(i), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1,leftButton.Size.X.Offset * -2, 1, 0), + Position = UDim2.new(1,0,0,0), + TextColor3 = Color3.fromRGB(255, 255, 255), + TextYAlignment = Enum.TextYAlignment.Center, + TextTransparency = 0.5, + Font = Enum.Font.SourceSans, + TextSize = 24, + Text = v, + ZIndex = 2, + Visible = false, + Parent = this.SelectorFrame + }; + if isTenFootInterface() then + nextSelection.TextSize = 36 + end + + if i == startPosition then + this.CurrentIndex = i + nextSelection.Position = UDim2.new(0,leftButton.Size.X.Offset,0,0) + nextSelection.Visible = true + + isSelectionLabelVisible[nextSelection] = true + else + isSelectionLabelVisible[nextSelection] = false + end + + this.Selections[i] = nextSelection + end + + local hasMoreThanOneSelection = #this.Selections > 1 + leftButton.Visible = hasMoreThanOneSelection + rightButton.Visible = hasMoreThanOneSelection + end + + --------------------- SETUP ----------------------- + local function onVREnabled(prop) + if prop ~= "VREnabled" then + return + end + local vrEnabled = VRService.VREnabled + leftButton.Selectable = vrEnabled + rightButton.Selectable = vrEnabled + autoSelectButton.Selectable = vrEnabled + end + VRService.Changed:Connect(onVREnabled) + onVREnabled("VREnabled") + + leftButton.InputBegan:Connect(function(inputObject) + if inputObject.UserInputType == Enum.UserInputType.Touch then + stepFunc(nil, -1) + end + end) + leftButton.MouseButton1Click:Connect(function() + if not UserInputService.TouchEnabled then + stepFunc(nil, -1) + end + end) + rightButton.InputBegan:Connect(function(inputObject) + if inputObject.UserInputType == Enum.UserInputType.Touch then + stepFunc(nil, 1) + end + end) + rightButton.MouseButton1Click:Connect(function() + if not UserInputService.TouchEnabled then + stepFunc(nil, 1) + end + end) + + local isInTree = true + this:UpdateOptions(selectionStringTable) + + UserInputService.InputBegan:Connect(function(inputObject) + if not interactable then return end + if not isInTree then return end + + if inputObject.UserInputType ~= Enum.UserInputType.Gamepad1 and inputObject.UserInputType ~= Enum.UserInputType.Keyboard then return end + if GuiService.SelectedCoreObject ~= this.SelectorFrame then return end + + if inputObject.KeyCode == Enum.KeyCode.DPadLeft or inputObject.KeyCode == Enum.KeyCode.Left or inputObject.KeyCode == Enum.KeyCode.A then + stepFunc(inputObject, -1) + elseif inputObject.KeyCode == Enum.KeyCode.DPadRight or inputObject.KeyCode == Enum.KeyCode.Right or inputObject.KeyCode == Enum.KeyCode.D then + stepFunc(inputObject, 1) + end + end) + + UserInputService.InputChanged:Connect(function(inputObject) + if not interactable then return end + if not isInTree then lastInputDirection = 0 return end + + if inputObject.UserInputType ~= Enum.UserInputType.Gamepad1 then return end + + local selected = GuiService.SelectedCoreObject + if not selected or not selected:IsDescendantOf(this.SelectorFrame.Parent) then return end + + if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end + + + if inputObject.Position.X > CONTROLLER_THUMBSTICK_DEADZONE and inputObject.Delta.X > 0 and lastInputDirection ~= 1 then + lastInputDirection = 1 + stepFunc(inputObject, lastInputDirection) + elseif inputObject.Position.X < -CONTROLLER_THUMBSTICK_DEADZONE and inputObject.Delta.X < 0 and lastInputDirection ~= -1 then + lastInputDirection = -1 + stepFunc(inputObject, lastInputDirection) + elseif math.abs(inputObject.Position.X) < CONTROLLER_THUMBSTICK_DEADZONE then + lastInputDirection = 0 + end + end) + + this.SelectorFrame.AncestryChanged:Connect(function(child, parent) + isInTree = parent + if not isInTree then + if guiServiceCon then guiServiceCon:Disconnect() end + else + connectToGuiService() + end + end) + + local function onResized(viewportSize, portrait) + local textSize = 0 + if portrait then + textSize = 16 + else + textSize = isTenFootInterface() and 36 or 24 + end + + for i, selection in pairs(this.Selections) do + selection.TextSize = textSize + end + end + addOnResizedCallback(this.SelectorFrame, onResized) + + connectToGuiService() + + return this +end + +local function ShowAlert(alertMessage, okButtonText, settingsHub, okPressedFunc, hasBackground) + local parent = CoreGui.RobloxGui + if parent:FindFirstChild("AlertViewFullScreen") then return end + + --Declare AlertViewBacking so onVREnabled can take it as an upvalue + local AlertViewBacking = nil + + --Handle VR toggle while alert is open + --Future consideration: maybe rebuild gui when VR toggles mid-game; right now only subpaneling is handled rather than visual style + local function onVREnabled(prop) + if prop ~= "VREnabled" then return end + local Panel3D, settingsPanel = nil, nil + if VRService.VREnabled then + Panel3D = require(CoreGui.RobloxGui.Modules.VR.Panel3D) + settingsPanel = Panel3D.Get("SettingsMenu") + parent = settingsPanel:GetGUI() + else + parent = CoreGui.RobloxGui + end + if AlertViewBacking and AlertViewBacking.Parent ~= nil then + AlertViewBacking.Parent = parent + if VRService.VREnabled then + settingsPanel:SetSubpanelDepth(AlertViewBacking, 0.5) + end + end + end + local vrEnabledConn = VRService.Changed:Connect(onVREnabled) + + local NON_SELECTED_TEXT_COLOR = Color3.fromRGB(59, 166, 241) + local SELECTED_TEXT_COLOR = Color3.fromRGB(255, 255, 255) + + AlertViewBacking = Util.Create'ImageLabel' + { + Name = "AlertViewBacking", + Image = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButton.png", + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8,6,46,44), + BackgroundTransparency = 1, + + ImageTransparency = 1, + Size = UDim2.new(0, 400, 0, 350), + Position = UDim2.new(0.5, -200, 0.5, -175), + ZIndex = 9, + Parent = parent + }; + onVREnabled("VREnabled") + if hasBackground or VRService.VREnabled then + AlertViewBacking.ImageTransparency = 0 + else + AlertViewBacking.Size = UDim2.new(0.8, 0, 0, 350) + AlertViewBacking.Position = UDim2.new(0.1, 0, 0.1, 0) + end + + if CoreGui.RobloxGui.AbsoluteSize.Y <= AlertViewBacking.Size.Y.Offset then + AlertViewBacking.Size = UDim2.new(AlertViewBacking.Size.X.Scale, AlertViewBacking.Size.X.Offset, + AlertViewBacking.Size.Y.Scale, CoreGui.RobloxGui.AbsoluteSize.Y) + AlertViewBacking.Position = UDim2.new(AlertViewBacking.Position.X.Scale, -AlertViewBacking.Size.X.Offset/2, 0.5, -AlertViewBacking.Size.Y.Offset/2) + end + + local AlertViewText = Util.Create'TextLabel' + { + Name = "AlertViewText", + BackgroundTransparency = 1, + Size = UDim2.new(0.95, 0, 0.6, 0), + Position = UDim2.new(0.025, 0, 0.05, 0), + Font = Enum.Font.SourceSansBold, + TextSize = 36, + Text = alertMessage, + TextWrapped = true, + TextColor3 = Color3.fromRGB(255, 255, 255), + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + ZIndex = 10, + Parent = AlertViewBacking + }; + + local SelectionOverrideObject = Util.Create'ImageLabel' + { + Image = "", + BackgroundTransparency = 1 + }; + + local removeId = HttpService:GenerateGUID(false) + + local destroyAlert = function(actionName, inputState) + if VRService.VREnabled and (inputState == Enum.UserInputState.Begin or inputState == Enum.UserInputState.Cancel) then + return + end + if not AlertViewBacking then + return + end + if VRService.VREnabled then + local Panel3D = require(CoreGui.RobloxGui.Modules.VR.Panel3D) + Panel3D.Get("SettingsMenu"):SetSubpanelDepth(AlertViewBacking, 0) + end + AlertViewBacking:Destroy() + AlertViewBacking = nil + if okPressedFunc then + okPressedFunc() + end + ContextActionService:UnbindCoreAction(removeId) + GuiService.SelectedCoreObject = nil + if settingsHub then + settingsHub:ShowBar() + end + if vrEnabledConn then + vrEnabledConn:Disconnect() + end + end + + local AlertViewButtonSize = UDim2.new(1, -20, 0, 60) + local AlertViewButtonPosition = UDim2.new(0, 10, 0.65, 0) + if not hasBackground then + AlertViewButtonSize = UDim2.new(0, 200, 0, 50) + AlertViewButtonPosition = UDim2.new(0.5, -100, 0.65, 0) + end + + local AlertViewButton, AlertViewText = MakeButton("AlertViewButton", okButtonText, AlertViewButtonSize, destroyAlert) + AlertViewButton.Position = AlertViewButtonPosition + AlertViewButton.NextSelectionLeft = AlertViewButton + AlertViewButton.NextSelectionRight = AlertViewButton + AlertViewButton.NextSelectionUp = AlertViewButton + AlertViewButton.NextSelectionDown = AlertViewButton + AlertViewButton.ZIndex = 9 + AlertViewText.ZIndex = AlertViewButton.ZIndex + AlertViewButton.Parent = AlertViewBacking + + if usesSelectedObject() then + GuiService.SelectedCoreObject = AlertViewButton + end + + GuiService.SelectedCoreObject = AlertViewButton + + ContextActionService:BindCoreAction(removeId, destroyAlert, false, Enum.KeyCode.Escape, Enum.KeyCode.ButtonB, Enum.KeyCode.ButtonA) + + if settingsHub and not VRService.VREnabled then + settingsHub:HideBar() + settingsHub.Pages.CurrentPage:Hide(1, 1) + end +end + +local function CreateNewSlider(numOfSteps, startStep, minStep) + -------------------- SETUP ------------------------ + local this = {} + + local spacing = 4 + local initialSpacing = 8 + local steps = tonumber(numOfSteps) + local currentStep = startStep + + local lastInputDirection = 0 + local timeAtLastInput = nil + + local interactable = true + + local renderStepBindName = HttpService:GenerateGUID(false) + + -- this is done to prevent using these values below (trying to keep the variables consistent) + numOfSteps = "" + startStep = "" + + if steps <= 0 then + error("CreateNewSlider failed because numOfSteps (first arg) is 0 or negative, please supply a positive integer", 2) + return + end + + local valueChangedEvent = Instance.new("BindableEvent") + valueChangedEvent.Name = "ValueChanged" + + ----------------- GUI SETUP ------------------------ + this.SliderFrame = Util.Create'ImageButton' + { + Name = "Slider", + Image = "", + AutoButtonColor = false, + NextSelectionLeft = this.SliderFrame, + NextSelectionRight = this.SliderFrame, + BackgroundTransparency = 1, + Size = UDim2.new(0.6, 0, 0, 50), + Position = UDim2.new(1, 0, 0.5, 0), + AnchorPoint = Vector2.new(1, 0.5), + SelectionImageObject = noSelectionObject, + ZIndex = 2 + }; + + this.StepsContainer = Util.Create "Frame" + { + Name = "StepsContainer", + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, -100, 1, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Parent = this.SliderFrame, + } + + local leftButton = Util.Create'ImageButton' + { + Name = "LeftButton", + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(0, 0.5), + Position = UDim2.new(0,0,0.5,0), + Size = UDim2.new(0,50,0,50), + Image = "", + ZIndex = 3, + Selectable = false, + SelectionImageObject = noSelectionObject, + Active = true, + Parent = this.SliderFrame + }; + local rightButton = Util.Create'ImageButton' + { + Name = "RightButton", + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(1, 0.5), + Position = UDim2.new(1,0,0.5,0), + Size = UDim2.new(0,50,0,50), + Image = "", + ZIndex = 3, + Selectable = false, + SelectionImageObject = noSelectionObject, + Active = true, + Parent = this.SliderFrame + }; + + local leftButtonImage = Util.Create'ImageLabel' + { + Name = "LeftButton", + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5,0,0.5,0), + Size = UDim2.new(0,30,0,30), + Image = "rbxasset://textures/ui/Settings/Slider/Less.png", + ZIndex = 4, + Parent = leftButton, + ImageColor3 = UserInputService.TouchEnabled and ARROW_COLOR_TOUCH or ARROW_COLOR + }; + local rightButtonImage = Util.Create'ImageLabel' + { + Name = "RightButton", + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5,0,0.5,0), + Size = UDim2.new(0,30,0,30), + Image = "rbxasset://textures/ui/Settings/Slider/More.png", + ZIndex = 4, + Parent = rightButton, + ImageColor3 = UserInputService.TouchEnabled and ARROW_COLOR_TOUCH or ARROW_COLOR + }; + if not UserInputService.TouchEnabled then + local onNormalButtonState, onHoverButtonState = + function(instance) instance.ImageColor3 = ARROW_COLOR end, + function(instance) instance.ImageColor3 = ARROW_COLOR_HOVER end + + addHoverState(leftButton, leftButtonImage, onNormalButtonState, onHoverButtonState) + addHoverState(rightButton, rightButtonImage, onNormalButtonState, onHoverButtonState) + end + + this.Steps = {} + local stepXSize = 35 + if isSmallTouchScreen() then + stepXSize = 25 + end + + local stepXScale = 1 / steps + stepXSize = 0 + + for i = 1, steps do + local nextStep = Util.Create'ImageButton' + { + Name = "Step" .. tostring(i), + BackgroundColor3 = SELECTED_COLOR, + BackgroundTransparency = 0.36, + BorderSizePixel = 0, + AutoButtonColor = false, + Active = false, + AnchorPoint = Vector2.new(0, 0.5), + Position = UDim2.new((i - 1) * stepXScale, spacing / 2, 0.5, 0), + Size = UDim2.new(stepXScale,-spacing, 24 / 50, 0), + Image = "", + ZIndex = 3, + Selectable = false, + ImageTransparency = 0.36, + Parent = this.StepsContainer, + SelectionImageObject = noSelectionObject + }; + + if i > currentStep then + nextStep.BackgroundColor3 = NON_SELECTED_COLOR + end + + if i == 1 or i == steps then + nextStep.BackgroundTransparency = 1 + nextStep.ScaleType = Enum.ScaleType.Slice + nextStep.SliceCenter = Rect.new(3,3,32,21) + + if i <= currentStep then + if i == 1 then + nextStep.Image = SELECTED_LEFT_IMAGE + else + nextStep.Image = SELECTED_RIGHT_IMAGE + end + else + if i == 1 then + nextStep.Image = NON_SELECTED_LEFT_IMAGE + else + nextStep.Image = NON_SELECTED_RIGHT_IMAGE + end + end + end + + this.Steps[#this.Steps + 1] = nextStep + end + + + ------------------- FUNCTIONS --------------------- + local function hideSelection() + for i = 1, steps do + this.Steps[i].BackgroundColor3 = NON_SELECTED_COLOR + if i == 1 then + this.Steps[i].Image = NON_SELECTED_LEFT_IMAGE + elseif i == steps then + this.Steps[i].Image = NON_SELECTED_RIGHT_IMAGE + end + end + end + local function showSelection() + for i = 1, steps do + if i > currentStep then break end + this.Steps[i].BackgroundColor3 = SELECTED_COLOR + if i == 1 then + this.Steps[i].Image = SELECTED_LEFT_IMAGE + elseif i == steps then + this.Steps[i].Image = SELECTED_RIGHT_IMAGE + end + end + end + local function modifySelection(alpha) + for i = 1, steps do + if i == 1 or i == steps then + this.Steps[i].ImageTransparency = alpha + else + this.Steps[i].BackgroundTransparency = alpha + end + end + end + + local function setCurrentStep(newStepPosition) + if not minStep then minStep = 0 end + + leftButton.Visible = true + rightButton.Visible = true + + if newStepPosition <= minStep then + newStepPosition = minStep + leftButton.Visible = false + end + if newStepPosition >= steps then + newStepPosition = steps + rightButton.Visible = false + end + + if currentStep == newStepPosition then return end + + currentStep = newStepPosition + + hideSelection() + showSelection() + + timeAtLastInput = tick() + valueChangedEvent:Fire(currentStep) + end + + local function isActivateEvent(inputObject) + if not inputObject then return false end + return inputObject.UserInputType == Enum.UserInputType.MouseButton1 or inputObject.UserInputType == Enum.UserInputType.Touch or (inputObject.UserInputType == Enum.UserInputType.Gamepad1 and inputObject.KeyCode == Enum.KeyCode.ButtonA) + end + local function mouseDownFunc(inputObject, newStepPos, repeatAction) + if not interactable then return end + + if inputObject == nil then return end + + if not isActivateEvent(inputObject) then return end + + if usesSelectedObject() and not VRService.VREnabled then + GuiService.SelectedCoreObject = this.SliderFrame + end + + if not VRService.VREnabled then + if repeatAction then + lastInputDirection = newStepPos - currentStep + else + lastInputDirection = 0 + + local mouseInputMovedCon = nil + local mouseInputEndedCon = nil + + mouseInputMovedCon = UserInputService.InputChanged:Connect(function(inputObject) + if inputObject.UserInputType ~= Enum.UserInputType.MouseMovement then return end + + local mousePos = inputObject.Position.X + for i = 1, steps do + local stepPosition = this.Steps[i].AbsolutePosition.X + local stepSize = this.Steps[i].AbsoluteSize.X + if mousePos >= stepPosition and mousePos <= stepPosition + stepSize then + setCurrentStep(i) + break + elseif i == 1 and mousePos < stepPosition then + setCurrentStep(0) + break + elseif i == steps and mousePos >= stepPosition then + setCurrentStep(i) + break + end + end + end) + mouseInputEndedCon = UserInputService.InputEnded:Connect(function(inputObject) + if not isActivateEvent(inputObject) then return end + + lastInputDirection = 0 + mouseInputEndedCon:Disconnect() + mouseInputMovedCon:Disconnect() + end) + end + else + lastInputDirection = 0 + end + + setCurrentStep(newStepPos) + end + + local function mouseUpFunc(inputObject) + if not interactable then return end + if not isActivateEvent(inputObject) then return end + + lastInputDirection = 0 + end + + local function touchClickFunc(inputObject, newStepPos, repeatAction) + mouseDownFunc(inputObject, newStepPos, repeatAction) + end + + --------------------- PUBLIC FACING FUNCTIONS ----------------------- + this.ValueChanged = valueChangedEvent.Event + + function this:SetValue(newValue) + setCurrentStep(newValue) + end + + function this:GetValue() + return currentStep + end + + function this:SetInteractable(value) + lastInputDirection = 0 + interactable = value + this.SliderFrame.Selectable = value + if not interactable then + hideSelection() + else + showSelection() + end + end + + function this:SetZIndex(newZIndex) + leftButton.ZIndex = newZIndex + rightButton.ZIndex = newZIndex + leftButtonImage.ZIndex = newZIndex + rightButtonImage.ZIndex = newZIndex + + for i = 1, #this.Steps do + this.Steps[i].ZIndex = newZIndex + end + end + + function this:SetMinStep(newMinStep) + if newMinStep >= 0 and newMinStep <= steps then + minStep = newMinStep + end + + if currentStep <= minStep then + currentStep = minStep + leftButton.Visible = false + end + if currentStep >= steps then + currentStep = steps + rightButton.Visible = false + end + end + + --------------------- SETUP ----------------------- + + leftButton.InputBegan:Connect(function(inputObject) mouseDownFunc(inputObject, currentStep - 1, true) end) + leftButton.InputEnded:Connect(function(inputObject) mouseUpFunc(inputObject) end) + rightButton.InputBegan:Connect(function(inputObject) mouseDownFunc(inputObject, currentStep + 1, true) end) + rightButton.InputEnded:Connect(function(inputObject) mouseUpFunc(inputObject) end) + + local function onVREnabled(prop) + if prop ~= "VREnabled" then + return + end + if VRService.VREnabled then + leftButton.Selectable = interactable + rightButton.Selectable = interactable + this.SliderFrame.Selectable = interactable + + for i = 1, steps do + this.Steps[i].Selectable = interactable + this.Steps[i].Active = interactable + end + else + leftButton.Selectable = false + rightButton.Selectable = false + this.SliderFrame.Selectable = interactable + for i = 1, steps do + this.Steps[i].Selectable = false + this.Steps[i].Active = false + end + end + end + VRService.Changed:Connect(onVREnabled) + onVREnabled("VREnabled") + + for i = 1, steps do + this.Steps[i].InputBegan:Connect(function(inputObject) + mouseDownFunc(inputObject, i) + end) + this.Steps[i].InputEnded:Connect(function(inputObject) + mouseUpFunc(inputObject) end) + end + + this.SliderFrame.InputBegan:Connect(function(inputObject) + if VRService.VREnabled then + local selected = GuiService.SelectedCoreObject + if not selected or not selected:IsDescendantOf(this.SliderFrame.Parent) then return end + end + mouseDownFunc(inputObject, currentStep) + end) + this.SliderFrame.InputEnded:Connect(function(inputObject) + if VRService.VREnabled then + local selected = GuiService.SelectedCoreObject + if not selected or not selected:IsDescendantOf(this.SliderFrame.Parent) then return end + end + mouseUpFunc(inputObject) + end) + + + local stepSliderFunc = function() + if timeAtLastInput == nil then return end + + local currentTime = tick() + local timeSinceLastInput = currentTime - timeAtLastInput + if timeSinceLastInput >= CONTROLLER_SCROLL_DELTA then + setCurrentStep(currentStep + lastInputDirection) + end + end + + local isInTree = true + + local navigateLeft = -1 --these are just for differentiation, the actual value isn't important as long as they coerce to boolean true (all numbers do in Lua) + local navigateRight = 1 + local navigationKeyCodes = { + [Enum.KeyCode.Thumbstick1] = true, --thumbstick can be either direction + [Enum.KeyCode.DPadLeft] = navigateLeft, + [Enum.KeyCode.DPadRight] = navigateRight, + [Enum.KeyCode.Left] = navigateLeft, + [Enum.KeyCode.Right] = navigateRight, + [Enum.KeyCode.A] = navigateLeft, + [Enum.KeyCode.D] = navigateRight, + [Enum.KeyCode.ButtonA] = true --buttonA can be either direction + } + UserInputService.InputBegan:Connect(function(inputObject) + if not interactable then return end + if not isInTree then return end + + if inputObject.UserInputType ~= Enum.UserInputType.Gamepad1 and inputObject.UserInputType ~= Enum.UserInputType.Keyboard then return end + local selected = GuiService.SelectedCoreObject + if not selected or not selected:IsDescendantOf(this.SliderFrame.Parent) then return end + + if navigationKeyCodes[inputObject.KeyCode] == navigateLeft then + lastInputDirection = -1 + setCurrentStep(currentStep - 1) + elseif navigationKeyCodes[inputObject.KeyCode] == navigateRight then + lastInputDirection = 1 + setCurrentStep(currentStep + 1) + end + end) + + UserInputService.InputEnded:Connect(function(inputObject) + if not interactable then return end + + if inputObject.UserInputType ~= Enum.UserInputType.Gamepad1 and inputObject.UserInputType ~= Enum.UserInputType.Keyboard then return end + local selected = GuiService.SelectedCoreObject + if not selected or not selected:IsDescendantOf(this.SliderFrame.Parent) then return end + + if navigationKeyCodes[inputObject.KeyCode] then --detect any keycode considered a navigation key + lastInputDirection = 0 + end + end) + + UserInputService.InputChanged:Connect(function(inputObject) + if not interactable then + lastInputDirection = 0 + return + end + if not isInTree then + lastInputDirection = 0 + return + end + + if inputObject.UserInputType ~= Enum.UserInputType.Gamepad1 then return end + local selected = GuiService.SelectedCoreObject + if not selected or not selected:IsDescendantOf(this.SliderFrame.Parent) then return end + if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end + + if inputObject.Position.X > CONTROLLER_THUMBSTICK_DEADZONE and inputObject.Delta.X > 0 and lastInputDirection ~= 1 then + lastInputDirection = 1 + setCurrentStep(currentStep + 1) + elseif inputObject.Position.X < -CONTROLLER_THUMBSTICK_DEADZONE and inputObject.Delta.X < 0 and lastInputDirection ~= -1 then + lastInputDirection = -1 + setCurrentStep(currentStep - 1) + elseif math.abs(inputObject.Position.X) < CONTROLLER_THUMBSTICK_DEADZONE then + lastInputDirection = 0 + end + end) + + local isBound = false + GuiService.Changed:Connect(function(prop) + if prop ~= "SelectedCoreObject" then return end + + local selected = GuiService.SelectedCoreObject + local isThisSelected = selected and selected:IsDescendantOf(this.SliderFrame.Parent) + if isThisSelected then + modifySelection(0) + if not isBound then + isBound = true + timeAtLastInput = tick() + RunService:BindToRenderStep(renderStepBindName, Enum.RenderPriority.Input.Value + 1, stepSliderFunc) + end + else + modifySelection(0.36) + if isBound then + isBound = false + RunService:UnbindFromRenderStep(renderStepBindName) + end + end + end) + + this.SliderFrame.AncestryChanged:Connect(function(child, parent) + isInTree = parent + end) + + setCurrentStep(currentStep) + + return this +end + +local ROW_HEIGHT = 50 +if isTenFootInterface() then ROW_HEIGHT = 90 end + +local nextPosTable = {} +local function AddNewRow(pageToAddTo, rowDisplayName, selectionType, rowValues, rowDefault, extraSpacing) + local nextRowPositionY = 0 + local isARealRow = selectionType ~= 'TextBox' -- Textboxes are constructed in this function - they don't have an associated class. + + if nextPosTable[pageToAddTo] then + nextRowPositionY = nextPosTable[pageToAddTo] + end + + local RowFrame = nil + RowFrame = Util.Create'ImageButton' + { + Name = rowDisplayName .. "Frame", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png", + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(2, 2, 18, 18), + ImageTransparency = 1, + Active = false, + AutoButtonColor = false, + Size = UDim2.new(1,0,0,ROW_HEIGHT), + Position = UDim2.new(0,0,0,nextRowPositionY), + ZIndex = 2, + Selectable = false, + SelectionImageObject = noSelectionObject, + Parent = pageToAddTo.Page + }; + RowFrame.ImageColor3 = RowFrame.BackgroundColor3 + + if RowFrame and extraSpacing then + RowFrame.Position = UDim2.new(RowFrame.Position.X.Scale,RowFrame.Position.X.Offset, + RowFrame.Position.Y.Scale,RowFrame.Position.Y.Offset + extraSpacing) + end + + local RowLabel = nil + RowLabel = Util.Create'TextLabel' + { + Name = rowDisplayName .. "Label", + Text = rowDisplayName, + Font = Enum.Font.SourceSansBold, + TextSize = 16, + TextColor3 = Color3.fromRGB(255,255,255), + TextXAlignment = Enum.TextXAlignment.Left, + BackgroundTransparency = 1, + Size = UDim2.new(0,200,1,0), + Position = UDim2.new(0,10,0,0), + ZIndex = 2, + Parent = RowFrame + }; + + local RowLabelTextSizeConstraint = Instance.new("UITextSizeConstraint") + if FFlagUseNotificationsLocalization then + RowLabel.Size = UDim2.new(0.35,0,1,0) + RowLabel.TextScaled = true + RowLabel.TextWrapped = true + RowLabelTextSizeConstraint.Parent = RowLabel + RowLabelTextSizeConstraint.MaxTextSize = 16 + end + + if not isARealRow then + RowLabel.Text = '' + end + + local function onResized(viewportSize, portrait) + if portrait then + RowLabel.TextSize = 16 + else + RowLabel.TextSize = isTenFootInterface() and 36 or 24 + end + RowLabelTextSizeConstraint.MaxTextSize = RowLabel.TextSize + end + onResized(getViewportSize(), isPortrait()) + addOnResizedCallback(RowFrame, onResized) + + local ValueChangerSelection = nil + local ValueChangerInstance = nil + if selectionType == "Slider" then + ValueChangerInstance = CreateNewSlider(rowValues, rowDefault) + ValueChangerInstance.SliderFrame.Parent = RowFrame + ValueChangerSelection = ValueChangerInstance.SliderFrame + elseif selectionType == "Selector" then + ValueChangerInstance = CreateSelector(rowValues, rowDefault) + ValueChangerInstance.SelectorFrame.Parent = RowFrame + ValueChangerSelection = ValueChangerInstance.SelectorFrame + elseif selectionType == "DropDown" then + ValueChangerInstance = CreateDropDown(rowValues, rowDefault, pageToAddTo.HubRef) + ValueChangerInstance.DropDownFrame.Parent = RowFrame + ValueChangerSelection = ValueChangerInstance.DropDownFrame + elseif selectionType == "TextBox" then + local isMouseOverRow = false + local forceReturnSelectionOnFocusLost = false + local SelectionOverrideObject = Util.Create'ImageLabel' + { + Image = "", + BackgroundTransparency = 1, + }; + + ValueChangerInstance = {} + ValueChangerInstance.HubRef = nil + + local box = Util.Create'TextBox' + { + AnchorPoint = Vector2.new(1, 0.5), + Size = UDim2.new(0.6,0,1,0), + Position = UDim2.new(1,0,0.5,0), + Text = rowDisplayName, + TextColor3 = Color3.fromRGB(49, 49, 49), + BackgroundTransparency = 0.5, + BorderSizePixel = 0, + TextYAlignment = Enum.TextYAlignment.Top, + TextXAlignment = Enum.TextXAlignment.Left, + TextWrapped = true, + Font = Enum.Font.SourceSans, + TextSize = 24, + ZIndex = 2, + SelectionImageObject = SelectionOverrideObject, + ClearTextOnFocus = false, + Parent = RowFrame + }; + ValueChangerSelection = box + + box.Focused:Connect(function() + if usesSelectedObject() then + GuiService.SelectedCoreObject = box + end + + if box.Text == rowDisplayName then + box.Text = "" + end + end) + box.FocusLost:Connect(function(enterPressed, inputObject) + forceReturnSelectionOnFocusLost = false + end) + if extraSpacing then + box.Position = UDim2.new(box.Position.X.Scale,box.Position.X.Offset, + box.Position.Y.Scale,box.Position.Y.Offset + extraSpacing) + end + + ValueChangerSelection.SelectionGained:Connect(function() + if usesSelectedObject() then + box.BackgroundTransparency = 0.1 + + if ValueChangerInstance.HubRef then + ValueChangerInstance.HubRef:ScrollToFrame(ValueChangerSelection) + end + end + end) + ValueChangerSelection.SelectionLost:Connect(function() + if usesSelectedObject() then + box.BackgroundTransparency = 0.5 + end + end) + + local setRowSelection = function() + local fullscreenDropDown = CoreGui.RobloxGui:FindFirstChild("DropDownFullscreenFrame") + if fullscreenDropDown and fullscreenDropDown.Visible then return end + + local valueFrame = ValueChangerSelection + + if valueFrame and valueFrame.Visible and valueFrame.ZIndex > 1 and usesSelectedObject() and pageToAddTo.Active then + GuiService.SelectedCoreObject = valueFrame + isMouseOverRow = true + end + end + local function processInput(input) + if input.UserInputState == Enum.UserInputState.Begin then + if input.KeyCode == Enum.KeyCode.Return then + if GuiService.SelectedCoreObject == ValueChangerSelection then + forceReturnSelectionOnFocusLost = true + box:CaptureFocus() + end + end + end + end + box.MouseEnter:Connect(setRowSelection) + + UserInputService.InputBegan:Connect(processInput) + + elseif selectionType == "TextEntry" then + local isMouseOverRow = false + local forceReturnSelectionOnFocusLost = false + local SelectionOverrideObject = Util.Create'ImageLabel' + { + Image = "", + BackgroundTransparency = 1, + }; + + ValueChangerInstance = {} + ValueChangerInstance.HubRef = nil + + local box = Util.Create'TextBox' + { + AnchorPoint = Vector2.new(1, 0.5), + Size = UDim2.new(0.4,-10,0,40), + Position = UDim2.new(1,0,0.5,0), + Text = rowDisplayName, + TextColor3 = Color3.fromRGB(178, 178, 178), + BackgroundTransparency = 1.0, + BorderSizePixel = 0, + TextYAlignment = Enum.TextYAlignment.Center, + TextXAlignment = Enum.TextXAlignment.Center, + TextWrapped = false, + Font = Enum.Font.SourceSans, + TextSize = 24, + ZIndex = 2, + SelectionImageObject = SelectionOverrideObject, + ClearTextOnFocus = false, + Parent = RowFrame + }; + ValueChangerSelection = box + + box.Focused:Connect(function() + if usesSelectedObject() then + GuiService.SelectedCoreObject = box + end + + if box.Text == rowDisplayName then + box.Text = "" + end + end) + box.FocusLost:Connect(function(enterPressed, inputObject) + forceReturnSelectionOnFocusLost = false + end) + if extraSpacing then + box.Position = UDim2.new(box.Position.X.Scale,box.Position.X.Offset, + box.Position.Y.Scale,box.Position.Y.Offset + extraSpacing) + end + + ValueChangerSelection.SelectionGained:Connect(function() + if usesSelectedObject() then + box.BackgroundTransparency = 0.8 + + if ValueChangerInstance.HubRef then + ValueChangerInstance.HubRef:ScrollToFrame(ValueChangerSelection) + end + end + end) + ValueChangerSelection.SelectionLost:Connect(function() + if usesSelectedObject() then + box.BackgroundTransparency = 1.0 + end + end) + + local setRowSelection = function() + local fullscreenDropDown = CoreGui.RobloxGui:FindFirstChild("DropDownFullscreenFrame") + if fullscreenDropDown and fullscreenDropDown.Visible then return end + + local valueFrame = ValueChangerSelection + + if valueFrame and valueFrame.Visible and valueFrame.ZIndex > 1 and usesSelectedObject() and pageToAddTo.Active then + GuiService.SelectedCoreObject = valueFrame + isMouseOverRow = true + end + end + local function processInput(input) + if input.UserInputState == Enum.UserInputState.Begin then + if input.KeyCode == Enum.KeyCode.Return then + if GuiService.SelectedCoreObject == ValueChangerSelection then + forceReturnSelectionOnFocusLost = true + box:CaptureFocus() + end + end + end + end + RowFrame.MouseEnter:Connect(setRowSelection) + + function ValueChangerInstance:SetZIndex(newZIndex) + box.ZIndex = newZIndex + end + + function ValueChangerInstance:SetInteractable(interactable) + box.Selectable = interactable + if not interactable then + box.TextColor3 = Color3.fromRGB(49,49,49) + box.ZIndex = 1 + else + box.TextColor3 = Color3.fromRGB(178,178,178) + box.ZIndex = 2 + end + end + + function ValueChangerInstance:SetValue(value) -- should this do more? + box.Text = value + end + + local valueChangedEvent = Instance.new("BindableEvent") + valueChangedEvent.Name = "ValueChanged" + + box.FocusLost:Connect(function() + valueChangedEvent:Fire(box.Text) + end) + + ValueChangerInstance.ValueChanged = valueChangedEvent.Event + + UserInputService.InputBegan:Connect(processInput) + end + + ValueChangerInstance.Name = rowDisplayName .. "ValueChanger" + + nextRowPositionY = nextRowPositionY + ROW_HEIGHT + if extraSpacing then + nextRowPositionY = nextRowPositionY + extraSpacing + end + + nextPosTable[pageToAddTo] = nextRowPositionY + + if isARealRow then + local setRowSelection = function() + local fullscreenDropDown = CoreGui.RobloxGui:FindFirstChild("DropDownFullscreenFrame") + if fullscreenDropDown and fullscreenDropDown.Visible then return end + + local valueFrame = ValueChangerInstance.SliderFrame + if not valueFrame then + valueFrame = ValueChangerInstance.SliderFrame + end + if not valueFrame then + valueFrame = ValueChangerInstance.DropDownFrame + end + if not valueFrame then + valueFrame = ValueChangerInstance.SelectorFrame + end + + if valueFrame and valueFrame.Visible and valueFrame.ZIndex > 1 and usesSelectedObject() and pageToAddTo.Active then + GuiService.SelectedCoreObject = valueFrame + end + end + RowFrame.MouseEnter:Connect(setRowSelection) + + --Could this be cleaned up even more? + local function onVREnabled(prop) + if prop == "VREnabled" then + if VRService.VREnabled then + RowFrame.Selectable = true + RowFrame.Active = true + ValueChangerSelection.Active = true + GuiService.Changed:Connect(function(prop) + if prop == "SelectedCoreObject" then + local selected = GuiService.SelectedCoreObject + if selected and (selected == RowFrame or selected:IsDescendantOf(RowFrame)) then + RowFrame.ImageTransparency = 0.5 + RowFrame.BackgroundTransparency = 1 + else + RowFrame.ImageTransparency = 1 + RowFrame.BackgroundTransparency = 1 + end + end + end) + else + RowFrame.Selectable = false + RowFrame.Active = false + end + end + end + VRService.Changed:Connect(onVREnabled) + onVREnabled("VREnabled") + + ValueChangerSelection.SelectionGained:Connect(function() + if usesSelectedObject() then + if VRService.VREnabled then + RowFrame.ImageTransparency = 0.5 + RowFrame.BackgroundTransparency = 1 + else + RowFrame.ImageTransparency = 1 + RowFrame.BackgroundTransparency = 0.5 + end + + if ValueChangerInstance.HubRef then + ValueChangerInstance.HubRef:ScrollToFrame(RowFrame) + end + end + end) + ValueChangerSelection.SelectionLost:Connect(function() + if usesSelectedObject() then + RowFrame.ImageTransparency = 1 + RowFrame.BackgroundTransparency = 1 + end + end) + end + + pageToAddTo:AddRow(RowFrame, RowLabel, ValueChangerInstance, extraSpacing, false) + + ValueChangerInstance.Selection = ValueChangerSelection + + return RowFrame, RowLabel, ValueChangerInstance +end + +local function AddNewRowObject(pageToAddTo, rowDisplayName, rowObject, extraSpacing) + local nextRowPositionY = 0 + + if nextPosTable[pageToAddTo] then + nextRowPositionY = nextPosTable[pageToAddTo] + end + + local RowFrame = Util.Create'ImageButton' + { + Name = rowDisplayName .. "Frame", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png", + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(10,10,10,10), + ImageTransparency = 1, + Active = false, + AutoButtonColor = false, + Size = UDim2.new(1,0,0,ROW_HEIGHT), + Position = UDim2.new(0,0,0,nextRowPositionY), + ZIndex = 2, + Selectable = false, + SelectionImageObject = noSelectionObject, + Parent = pageToAddTo.Page + }; + RowFrame.ImageColor3 = RowFrame.BackgroundColor3 + RowFrame.SelectionGained:Connect(function() + RowFrame.BackgroundTransparency = 0.5 + end) + RowFrame.SelectionLost:Connect(function() + RowFrame.BackgroundTransparency = 1 + end) + + local RowLabel = Util.Create'TextLabel' + { + Name = rowDisplayName .. "Label", + Text = rowDisplayName, + Font = Enum.Font.SourceSansBold, + TextSize = 16, + TextColor3 = Color3.fromRGB(255,255,255), + TextXAlignment = Enum.TextXAlignment.Left, + BackgroundTransparency = 1, + Size = UDim2.new(0,200,1,0), + Position = UDim2.new(0,10,0,0), + ZIndex = 2, + Parent = RowFrame + }; + local function onResized(viewportSize, portrait) + if portrait then + RowLabel.TextSize = 16 + else + RowLabel.TextSize = isTenFootInterface() and 36 or 24 + end + end + addOnResizedCallback(RowFrame, onResized) + + if extraSpacing then + RowFrame.Position = UDim2.new(RowFrame.Position.X.Scale,RowFrame.Position.X.Offset, + RowFrame.Position.Y.Scale,RowFrame.Position.Y.Offset + extraSpacing) + end + + nextRowPositionY = nextRowPositionY + ROW_HEIGHT + if extraSpacing then + nextRowPositionY = nextRowPositionY + extraSpacing + end + + nextPosTable[pageToAddTo] = nextRowPositionY + + local setRowSelection = function() + if RowFrame.Visible then + GuiService.SelectedCoreObject = RowFrame + end + end + RowFrame.MouseEnter:Connect(setRowSelection) + + rowObject.SelectionImageObject = noSelectionObject + + rowObject.SelectionGained:Connect(function() + if VRService.VREnabled then + RowFrame.ImageTransparency = 0.5 + RowFrame.BackgroundTransparency = 1 + else + RowFrame.ImageTransparency = 1 + RowFrame.BackgroundTransparency = 0.5 + end + end) + rowObject.SelectionLost:Connect(function() + RowFrame.ImageTransparency = 1 + RowFrame.BackgroundTransparency = 1 + end) + + rowObject.Parent = RowFrame + + pageToAddTo:AddRow(RowFrame, RowLabel, rowObject, extraSpacing, true) + return RowFrame +end + +-------- public facing API ---------------- +local moduleApiTable = {} + +function moduleApiTable:Create(instanceType) + return function(data) + local obj = Instance.new(instanceType) + local parent = nil + for k, v in pairs(data) do + if type(k) == 'number' then + v.Parent = obj + elseif k == 'Parent' then + parent = v + else + obj[k] = v + end + end + if parent then + obj.Parent = parent + end + return obj + end +end + +-- RayPlaneIntersection (shortened) +-- http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm +function moduleApiTable:RayPlaneIntersection(ray, planeNormal, pointOnPlane) + planeNormal = planeNormal.unit + ray = ray.Unit + + local Vd = planeNormal:Dot(ray.Direction) + if Vd == 0 then -- parallel, no intersection + return nil + end + + local V0 = planeNormal:Dot(pointOnPlane - ray.Origin) + local t = V0 / Vd + if t < 0 then --plane is behind ray origin, and thus there is no intersection + return nil + end + + return ray.Origin + ray.Direction * t +end + +function moduleApiTable:GetEaseLinear() + return Linear +end +function moduleApiTable:GetEaseOutQuad() + return EaseOutQuad +end +function moduleApiTable:GetEaseInOutQuad() + return EaseInOutQuad +end + +function moduleApiTable:CreateNewSlider(numOfSteps, startStep, minStep) + return CreateNewSlider(numOfSteps, startStep, minStep) +end + +function moduleApiTable:CreateNewSelector(selectionStringTable, startPosition) + return CreateSelector(selectionStringTable, startPosition) +end + +function moduleApiTable:CreateNewDropDown(dropDownStringTable, startPosition) + return CreateDropDown(dropDownStringTable, startPosition, nil) +end + +function moduleApiTable:AddNewRow(pageToAddTo, rowDisplayName, selectionType, rowValues, rowDefault, extraSpacing) + return AddNewRow(pageToAddTo, rowDisplayName, selectionType, rowValues, rowDefault, extraSpacing) +end + +function moduleApiTable:AddNewRowObject(pageToAddTo, rowDisplayName, rowObject, extraSpacing) + return AddNewRowObject(pageToAddTo, rowDisplayName, rowObject, extraSpacing) +end + +function moduleApiTable:ShowAlert(alertMessage, okButtonText, settingsHub, okPressedFunc, hasBackground) + ShowAlert(alertMessage, okButtonText, settingsHub, okPressedFunc, hasBackground) +end + +function moduleApiTable:IsSmallTouchScreen() + return isSmallTouchScreen() +end + +function moduleApiTable:IsPortrait() + return isPortrait() +end + +function moduleApiTable:MakeStyledButton(name, text, size, clickFunc, pageRef, hubRef) + return MakeButton(name, text, size, clickFunc, pageRef, hubRef) +end + +function moduleApiTable:MakeStyledImageButton(name, image, size, imageSize, clickFunc, pageRef, hubRef) + return MakeImageButton(name, image, size, imageSize, clickFunc, pageRef, hubRef) +end + +function moduleApiTable:AddButtonRow(pageToAddTo, name, text, size, clickFunc, hubRef) + return AddButtonRow(pageToAddTo, name, text, size, clickFunc, hubRef) +end + +function moduleApiTable:CreateSignal() + return CreateSignal() +end + +function moduleApiTable:UsesSelectedObject() + return usesSelectedObject() +end + +function moduleApiTable:TweenProperty(instance, prop, start, final, duration, easingFunc, cbFunc) + return PropertyTweener(instance, prop, start, final, duration, easingFunc, cbFunc) +end + +function moduleApiTable:OnResized(key, callback) + return addOnResizedCallback(key, callback) +end + +function moduleApiTable:FireOnResized() + local newSize = getViewportSize() + local portrait = moduleApiTable:IsPortrait() + + for key, callback in pairs(onResizedCallbacks) do + callback(newSize, portrait) + end +end + +return moduleApiTable diff --git a/Client2018/content/scripts/CoreScripts/Modules/SocialUtil.lua b/Client2018/content/scripts/CoreScripts/Modules/SocialUtil.lua new file mode 100644 index 0000000..267e4d9 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/SocialUtil.lua @@ -0,0 +1,112 @@ +--[[ + // FileName: SocialUtil.lua + // Written by: TheGamer101 + // Description: Utility code related to social features. +]] +local SocialUtil = {} + +--[[ Services ]]-- +local Players = game:GetService("Players") + +--[[ Constants ]]-- +local THUMBNAIL_SIZE_MAP = { + [Enum.ThumbnailSize.Size48x48] = 48, + [Enum.ThumbnailSize.Size180x180] = 180, + [Enum.ThumbnailSize.Size420x420] = 420, + [Enum.ThumbnailSize.Size60x60] = 60, + [Enum.ThumbnailSize.Size100x100] = 100, + [Enum.ThumbnailSize.Size150x150] = 150, + [Enum.ThumbnailSize.Size352x352] = 352 +} + +local THUMBNAIL_FALLBACK_URLS = { + [Enum.ThumbnailType.HeadShot] = "https://www.roblox.com/headshot-thumbnail/image?width=%d&height=%d&format=png&userId=%d", + [Enum.ThumbnailType.AvatarBust] = "https://www.roblox.com/bust-thumbnail/image?width=%d&height=%d&format=png&userId=%d", + [Enum.ThumbnailType.AvatarThumbnail] = "https://www.roblox.com/avatar-thumbnail/image?width=%d&height=%d&format=png&userId=%d" +} + +local GET_PLAYER_IMAGE_DEFAULT_TIMEOUT = 5 +local DEFAULT_THUMBNAIL_SIZE = Enum.ThumbnailSize.Size100x100 +local DEFAULT_THUMBNAIL_TYPE = Enum.ThumbnailType.AvatarThumbnail +local GET_USER_THUMBNAIL_ASYNC_RETRY_TIME = 1 + +local gutartSuccess,gutart = pcall(function() return tonumber(settings():GetFVariable("GetUserThumbnailAsyncRetryTime")) end) +local gpidtSuccess,gpidt = pcall(function() return tonumber(settings():GetFVariable("GetPlayerImageDefaultTimeout")) end) + +if gutartSuccess then + GET_USER_THUMBNAIL_ASYNC_RETRY_TIME = gutart +end + +if gpidtSuccess then + GET_PLAYER_IMAGE_DEFAULT_TIMEOUT = gpidt +end + +--[[ Functions ]]-- + +-- The thumbanil isn't guaranteed to be generated, this will just create the url using string.format and immediately return it. +function SocialUtil.GetFallbackPlayerImageUrl(userId, thumbnailSize, thumbnailType) + local sizeNumber = THUMBNAIL_SIZE_MAP[thumbnailSize] + if not sizeNumber then + if thumbnailSize then + warn("SocialUtil.GetPlayerImage: No thumbnail size in map for " ..tostring(thumbnailSize)) + end + + sizeNumber = THUMBNAIL_SIZE_MAP[DEFAULT_THUMBNAIL_SIZE] + end + + local thumbnailFallbackUrl = THUMBNAIL_FALLBACK_URLS[thumbnailType] + if not thumbnailFallbackUrl then + if thumbnailType then + warn("SocialUtil.GetPlayerImage: No thumbnail fallback url in map for " ..tostring(thumbnailType)) + end + + thumbnailFallbackUrl = THUMBNAIL_FALLBACK_URLS[DEFAULT_THUMBNAIL_TYPE] + end + + return thumbnailFallbackUrl:format(sizeNumber, sizeNumber, userId) +end + +-- This function will wait for up to timeOut seconds for the thumbnail to be generated. +-- It will just return a fallback (probably N/A) url if it's not generated in time. +function SocialUtil.GetPlayerImage(userId, thumbnailSize, thumbnailType, timeOut) + if not thumbnailSize then thumbnailSize = DEFAULT_THUMBNAIL_SIZE end + if not thumbnailType then thumbnailType = DEFAULT_THUMBNAIL_TYPE end + if not timeOut then timeOut = GET_PLAYER_IMAGE_DEFAULT_TIMEOUT end + + local finished = false + local finishedBindable = Instance.new("BindableEvent") -- fired with one parameter: imageUrl + + delay(timeOut, function() + if not finished then + finished = true + finishedBindable:Fire(SocialUtil.GetFallbackPlayerImageUrl(userId, thumbnailSize, thumbnailType)) + end + end) + + spawn(function() + while true do + if finished then + break + end + + local thumbnailUrl, isFinal = Players:GetUserThumbnailAsync(userId, thumbnailType, thumbnailSize) + + if finished then + break + end + + if isFinal then + finished = true + finishedBindable:Fire(thumbnailUrl) + break + end + + wait(GET_USER_THUMBNAIL_ASYNC_RETRY_TIME) + end + end) + + local imageUrl = finishedBindable.Event:Wait() + return imageUrl +end + +return SocialUtil diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/BarGraph.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/BarGraph.lua new file mode 100644 index 0000000..3ddde74 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/BarGraph.lua @@ -0,0 +1,218 @@ +--[[ + Filename: BarGraph.lua + Written by: dbanks + Description: A simple bar graph. +--]] + +--[[ Services ]]-- +local CoreGuiService = game:GetService('CoreGui') + +--[[ Globals ]]-- +local BarZIndex = 1 +local LineZIndex = 2 + + +--[[ Modules ]]-- +local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils) + +--[[ Classes ]]-- +local BarGraphClass = {} +BarGraphClass.__index = BarGraphClass + +function BarGraphClass.new(showExtras) + local self = {} + setmetatable(self, BarGraphClass) + + self._barFrame = Instance.new("Frame") + self._barFrame.Name = "PS_BarFrame" + self._barFrame.BackgroundTransparency = 1.0 + + self._lineFrame = Instance.new("Frame") + self._lineFrame.Name = "PS_LineFrame" + self._lineFrame.BackgroundTransparency = 1.0 + + self._showExtras = showExtras + + -- All of the values we are showing in the bar graph, in order. + self._values = {} + self._bars = {} + -- Average of these values. + self._average = 0 + -- Suggested max for these values. + self._target = 0 + + if self._showExtras then + self:_addGraphTarget() + self:_addGraphAverage() + end + + return self +end + +function BarGraphClass:SetZIndex(zIndex) + self._barFrame.ZIndex = zIndex + self._lineFrame.ZIndex = zIndex + 1 + if self._showExtras then + self._targetLine.ZIndex = self._lineFrame.ZIndex + self._averageLine.ZIndex = self._lineFrame.ZIndex + end +end + +function BarGraphClass:PlaceInParent(parent, size, position) + self._barFrame.Position = position + self._barFrame.Size = size + self._barFrame.Parent = parent + self._lineFrame.Position = position + self._lineFrame.Size = size + self._lineFrame.Parent = parent +end + +function BarGraphClass:SetAxisMax(axisMax) + self._axisMax = axisMax +end + +function BarGraphClass:SetValues(values) + self._values = values +end + +function BarGraphClass:SetAverage(average) + self._average = average +end + +function BarGraphClass:SetTarget(target) + -- Set the target value, move corresponding graph line + -- accordingly (if present). + self._target = target + self:_moveGraphTarget() +end + +function BarGraphClass:_updateBarCount(newBarCount) + -- Make sure we have exactly this number of bars in _bars. + -- Reuse old ones if possible. + -- If we have more old ones than we need, delete them. + local newBars = {} + local currentBarCount = table.getn(self._bars) + + for i = 1, currentBarCount, 1 do + if (i <= currentBarCount) then + table.insert(newBars, self._bars[i]) + else + self._bars[i].Destroy() + end + end + + for i = currentBarCount + 1, newBarCount, 1 do + table.insert(newBars, self:_makeNthBar(i)) + end + + self._bars = newBars +end + + +function BarGraphClass:Render() + local numValues = table.getn(self._values) + + self:_updateBarCount(numValues) + + for i, value in ipairs(self._values) do + self:_updateBar(i, value, numValues) + end + + if self._showExtras then + self:_moveGraphAverage() + end +end + + +function BarGraphClass:_addGraphTarget() + -- Add the line used to mark target value. + local line = Instance.new("ImageLabel") + line.Name = "TargetLine" + line.Size = UDim2.new(1, 0, 0, StatsUtils.GraphTargetLineInnerThickness) + + line.Image = 'rbxasset://textures/ui/PerformanceStats/TargetLine.png' + line.BackgroundTransparency = 1 + line.Parent = self._lineFrame + line.ZIndex = self._lineFrame.ZIndex + line.BorderSizePixel = 0 + + line.Changed:connect(function() + self:_updateTargetLineImageSize() + end) + + self._targetLine = line + self:_updateTargetLineImageSize() +end + +function BarGraphClass:_updateTargetLineImageSize() + self._targetLine.ImageRectSize = self._targetLine.AbsoluteSize +end + +function BarGraphClass:_addGraphAverage() + -- Add the line used to mark average of current values. + local line = Instance.new("Frame") + line.Name = "AverageLine" + line.Size = UDim2.new(1, 0, 0, StatsUtils.GraphAverageLineInnerThickness) + + line.Parent = self._lineFrame + line.ZIndex = self._lineFrame.ZIndex + + StatsUtils.StyleAverageLine(line) + + self._averageLine = line +end + +function BarGraphClass:_moveGraphTarget() + -- Update position of graph target line, if present. + if self._targetLine == nil then + return + end + self._targetLine.Position = UDim2.new(0, + 0, ( + self._axisMax - self._target)/self._axisMax, + -StatsUtils.GraphTargetLineInnerThickness/2) +end + +function BarGraphClass:_moveGraphAverage() + -- Update position of graph average line, if present. + if self._averageLine == nil then + return + end + + -- Never let it go above axis max. + local adjustedAverage = math.min(self._average, self._axisMax) + + self._averageLine.Position = UDim2.new(0, + 0, + (self._axisMax - adjustedAverage)/self._axisMax, + -StatsUtils.GraphAverageLineTotalThickness/2) +end + +function BarGraphClass:_makeNthBar(index) + -- Make the nth bar in the bar graph. + local realIndex = index-1 + local bar = Instance.new("Frame") + bar.Name = string.format("Bar_%d", realIndex) + bar.Parent = self._barFrame + bar.ZIndex = self._barFrame.ZIndex + bar.BorderSizePixel = 0 + return bar +end + +function BarGraphClass:_updateBar(i, value, numValues) + -- Update nth bar in graph: size, position, and color. + local bar = self._bars[i] + local realIndex = i-1 + + -- Don't let it go off the chart. + local clampedValue = math.max(0, math.min(value, self._axisMax)) + + bar.Position = UDim2.new(realIndex/numValues, 0, + (self._axisMax - clampedValue)/self._axisMax, 0) + bar.Size = UDim2.new(1/numValues, 0, + clampedValue/self._axisMax, 0) + + bar.BackgroundColor3 = StatsUtils.GetColorForValue(value, self._target) +end + +return BarGraphClass \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/BaseMemoryAnalyzer.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/BaseMemoryAnalyzer.lua new file mode 100644 index 0000000..0704365 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/BaseMemoryAnalyzer.lua @@ -0,0 +1,346 @@ +--[[ + Filename: BaseMemoryAnalyzer.lua + Written by: dbanks + Description: Base class for a widget that displays data about memory usage. +--]] + + +--[[ Globals ]]-- +local RowHeight = 20 +local ValueFrameWidth = 100 +local StdRowColor3 = Color3.new(0.35, 0.55, 0.35) +local AltRowColor3 = Color3.new(0.15, 0.35, 0.15) +local RowLabelTextColor3 = Color3.new(1, 1, 1) +local RowLabelBorderColor3 = Color3.new(1, 1, 1) +local ButtonBorderColor3 = Color3.new(1, 1, 1) + +local RowLabelBorderWidth = 1 +local ButtonBorderWidth = 1 + +local IndentSize = RowHeight +local ButtonSize = IndentSize - 4 + +local ButtonFrameUDim2Position = UDim2.new(0, 0, 0, 0) + +local ButtonUDim2Position = UDim2.new(1, -(IndentSize + ButtonSize)/2, 0, (IndentSize - ButtonSize)/2) +local ButtonUDim2Size = UDim2.new(0, ButtonSize, 0, ButtonSize) + +local ValueUDim2Position = UDim2.new(1, -ValueFrameWidth, 0, 0) +local ValueUDim2Size = UDim2.new(0, ValueFrameWidth, 0, RowHeight) + +--[[ Services ]]-- +local CoreGuiService = game:GetService('CoreGui') + +--[[ Helper functions ]]-- +local function __StyleAndSizeButton(button) + button.TextXAlignment = Enum.TextXAlignment.Center + button.TextColor3 = RowLabelTextColor3 + button.BackgroundTransparency = 1 + button.BorderColor3 = ButtonBorderColor3 + button.BorderSizePixel = ButtonBorderWidth + + button.Position = ButtonUDim2Position + button.Size = ButtonUDim2Size +end + +local function __StyleAndSizeLabel(label) + label.TextXAlignment = Enum.TextXAlignment.Left + label.TextColor3 = RowLabelTextColor3 + label.BackgroundTransparency = 1 + + label.Position = UDim2.new(0, 5, 0, 0) + label.Size = UDim2.new(1, -10, 1, 0) +end + +local function __StyleRowCellFrame(labelFrame) + labelFrame.BorderColor3 = RowLabelBorderColor3 + labelFrame.BorderSizePixel = RowLabelBorderWidth + labelFrame.BackgroundTransparency = 0.6 +end + +--[[ Classes ]]-- + +--//////////////////////////////////// +-- +-- MemoryAnalyzerRowClass +-- A single row in the table. +-- +--//////////////////////////////////// +local MemoryAnalyzerRowClass = {} +MemoryAnalyzerRowClass.__index = MemoryAnalyzerRowClass + + +function MemoryAnalyzerRowClass.new(treeViewItem) + local self = {} + setmetatable(self, MemoryAnalyzerRowClass) + + self._treeViewItem = treeViewItem + + self._expanded = true + self._expansionToggleCallback = nil + + -- The gui widget for the row + self._frame = Instance.new("Frame") + self._frame.Name = "MemoryAnalyzerRowClassFrame" + self._frame.BackgroundTransparency = 1 + + -- The button + self._buttonFrame = Instance.new("Frame") + self._buttonFrame.Name = "ButtonFrame" + self._buttonFrame.Parent = self._frame + self._buttonFrame.Position = ButtonFrameUDim2Position + local buttonFrameWidth = (1 + treeViewItem:getStackDepth()) * IndentSize + self._buttonFrame.Size = UDim2.new(0, buttonFrameWidth, 0, RowHeight) + __StyleRowCellFrame(self._buttonFrame) + + self._button = Instance.new("TextButton") + self._button.Name = "Button" + self._button.Parent = self._buttonFrame + __StyleAndSizeButton(self._button) + + self._button.MouseButton1Click:connect(function() + self:__toggleExpansion() + end) + + self:__updateButtonState() + + -- The label + self._labelFrame = Instance.new("Frame") + self._labelFrame.Name = "LabelFrame" + self._labelFrame.Parent = self._frame + -- From the left edge of button frame to right edge of value frame. + self._labelFrame.Position = UDim2.new(0, buttonFrameWidth, 0, 0) + self._labelFrame.Size = UDim2.new(1, -buttonFrameWidth - ValueFrameWidth, 0, RowHeight) + __StyleRowCellFrame(self._labelFrame) + + self._labelTextLabel = Instance.new("TextLabel") + self._labelTextLabel.Name = "Label" + self._labelTextLabel.Parent = self._labelFrame + __StyleAndSizeLabel(self._labelTextLabel) + + self._labelTextLabel.Text = treeViewItem:getLabel() + + -- The value + self._valueFrame = Instance.new("Frame") + self._valueFrame.Name = "ValueFrame" + self._valueFrame.Parent = self._frame + self._valueFrame.Position = ValueUDim2Position + self._valueFrame.Size = ValueUDim2Size + __StyleRowCellFrame(self._valueFrame) + + self._valueTextLabel = Instance.new("TextLabel") + self._valueTextLabel.Name = "Value" + self._valueTextLabel.Parent = self._valueFrame + __StyleAndSizeLabel(self._valueTextLabel) + + self._valueHasBeenNonZero = false + self:updateValue() + + return self +end + +function MemoryAnalyzerRowClass:isExpanded() + return self._expanded +end + +function MemoryAnalyzerRowClass:valueHasBeenNonZero() + return self._valueHasBeenNonZero +end + +function MemoryAnalyzerRowClass:setExpansionToggleCallback(callback) + self._expansionToggleCallback = callback +end + +function MemoryAnalyzerRowClass:__toggleExpansion() + self._expanded = not self._expanded + self:__updateButtonState() + if (self._expansionToggleCallback ~= nil) then + self._expansionToggleCallback() + end +end + + +function MemoryAnalyzerRowClass:__updateButtonState() + local children = self._treeViewItem:getChildren() + if #children == 0 then + self._button.Visible = false + else + self._button.Visible = true + if (self._expanded) then + self._button.Text = "-" + else + self._button.Text = "+" + end + end +end + +function MemoryAnalyzerRowClass:updateValue() + local value = self._treeViewItem:getValue() + -- We know we're only dealing with positive numbers. + -- And if I'm only going to display 3 significant digits (see below) I'm really + -- asking if this is >= 0.001 + if (value >= 0.001) then + self._valueHasBeenNonZero = true + end + + self._valueTextLabel.Text = string.format("%.3f", self._treeViewItem:getValue()) +end + +function MemoryAnalyzerRowClass:setZIndex(zIndex) + self._frame.ZIndex = zIndex + self._labelTextLabel.ZIndex = zIndex + self._valueTextLabel.ZIndex = zIndex +end + +function MemoryAnalyzerRowClass:setRowValue(value) + self._valueTextLabel.Text = string.format("%.3f", value) +end + +function MemoryAnalyzerRowClass:getFrame() + return self._frame +end + +function MemoryAnalyzerRowClass:setRowNumber(rowNumber) + if (rowNumber % 2 == 1) then + self._buttonFrame.BackgroundColor3 = StdRowColor3 + self._labelFrame.BackgroundColor3 = StdRowColor3 + self._valueFrame.BackgroundColor3 = StdRowColor3 + else + self._buttonFrame.BackgroundColor3 = AltRowColor3 + self._labelFrame.BackgroundColor3 = AltRowColor3 + self._valueFrame.BackgroundColor3 = AltRowColor3 + end +end + +--//////////////////////////////////// +-- +-- BaseMemoryAnalyzerClass +-- The whole table. +-- +--//////////////////////////////////// +local BaseMemoryAnalyzerClass = {} +BaseMemoryAnalyzerClass.__index = BaseMemoryAnalyzerClass + +BaseMemoryAnalyzerClass.Indent = string.rep(" ", 4) + +function BaseMemoryAnalyzerClass.new(parentFrame) + local self = {} + setmetatable(self, BaseMemoryAnalyzerClass) + + -- The gui widget containing the whole thing. + self._frame = Instance.new("Frame") + self._frame.Name = "MemoryAnalyzerClassFrame" + self._frame.ZIndex = parentFrame.ZIndex + self._frame.BackgroundTransparency = 1 + + -- a map from treeViewItem to the Row used to display the treeViewItem. + self._rowsByTreeViewItem = {} + + -- things need to be laid out, either because I have new content or + -- the size of my parent container changed. + -- Starts out as 'false' because there's no rows -> nothing to lay out. + self._layoutDirty = false + + self._heightChangedCallback = nil + self._heightInPix = 0 + + self._frame.Parent = parentFrame + + return self +end + +-- This needs to be overridden in derived classes. +function BaseMemoryAnalyzerClass:getMemoryUsageTree() + return nil +end + +function BaseMemoryAnalyzerClass:setHeightChangedCallback(callback) + self._heightChangedCallback = callback +end + +function BaseMemoryAnalyzerClass:__getOrMakeRowForTreeViewItem(treeViewItem) + if (self._rowsByTreeViewItem[treeViewItem] == nil) then + local row = MemoryAnalyzerRowClass.new(treeViewItem) + row:setExpansionToggleCallback(function() + self:__layoutRows() + end) + local frame = row:getFrame() + frame.Parent = self._frame + + self._rowsByTreeViewItem[treeViewItem] = row + self._layoutDirty = true + end + return self._rowsByTreeViewItem[treeViewItem] +end + +function BaseMemoryAnalyzerClass:__recursiveUpdateStatValue(treeViewItem) + local row = self:__getOrMakeRowForTreeViewItem(treeViewItem) + row:updateValue() + + local children = treeViewItem:getChildren() + for i, child in ipairs(children) do + self:__recursiveUpdateStatValue(child) + end +end + +-- Write latest stat values into each row. +function BaseMemoryAnalyzerClass:renderUpdates() + local treeViewItemRoot = self:getMemoryUsageTree() + if (treeViewItemRoot ~= nil) then + self:__recursiveUpdateStatValue(treeViewItemRoot) + end + + if self._layoutDirty then + self:__layoutRows() + end +end + + +function BaseMemoryAnalyzerClass:__layoutRows() + self._layoutDirty = false + self._heightInPix = 0 + + local treeViewItemRoot = self:getMemoryUsageTree() + if (treeViewItemRoot ~= nil) then + self:__recursiveLayoutTreeViewItem(self:getMemoryUsageTree(), true) + end + + self._frame.Size = UDim2.new(1, 0, 0, self._heightInPix) + self._frame.Position = UDim2.new(0, 0, 0, 0) + + if (self._heightChangedCallback) then + self._heightChangedCallback(self._heightInPix) + end +end + +function BaseMemoryAnalyzerClass:__recursiveLayoutTreeViewItem(treeViewItem, isVisible) + local row = self:__getOrMakeRowForTreeViewItem(treeViewItem) + local frame = row:getFrame() + + -- caller has something to say about whether it's visible (ancestor may be collapsed). + -- I also may be invisible if I have never been non-zero. + isVisible = (isVisible and row:valueHasBeenNonZero()) + + if (isVisible) then + frame.Visible = true + frame.Size = UDim2.new(1, 0, 0, RowHeight) + frame.Position = UDim2.new(0, 0, 0, self._heightInPix) + row:setZIndex(self._frame.ZIndex) + row:setRowNumber(self._heightInPix/RowHeight) + + self._heightInPix = self._heightInPix + RowHeight + else + frame.Visible = false + end + + local children = treeViewItem:getChildren() + for i, child in ipairs(children) do + self:__recursiveLayoutTreeViewItem(child, row:isExpanded() and isVisible) + end +end + +function BaseMemoryAnalyzerClass:getHeightInPix() + return self._heightInPix +end + +return BaseMemoryAnalyzerClass \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/ClientMemoryAnalyzer.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/ClientMemoryAnalyzer.lua new file mode 100644 index 0000000..6636305 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/ClientMemoryAnalyzer.lua @@ -0,0 +1,128 @@ +--[[ + Filename: ClientMemoryAnalyzer.lua + Written by: dbanks + Description: Widget to display client memory usage. +--]] + +--[[ Services ]]-- +local StatsService = game:GetService("Stats") +local CoreGuiService = game:GetService('CoreGui') + +--[[ Modules ]]-- +local BaseMemoryAnalyzerClass = require(CoreGuiService.RobloxGui.Modules.Stats.BaseMemoryAnalyzer) +local CommonUtils = require(CoreGuiService.RobloxGui.Modules.Common.CommonUtil) +local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils) +local TreeViewItem = require(CoreGuiService.RobloxGui.Modules.Stats.TreeViewItem) + +--[[ Helper functions ]]-- +local function __GetMemoryPerformanceStatsItem() + local performanceStats = StatsService and StatsService:FindFirstChild("PerformanceStats") + if performanceStats == nil then + return nil + end + + local memoryStats = performanceStats:FindFirstChild( + StatsUtils.StatNames[StatsUtils.StatType_Memory]) + return memoryStats +end + +local function __FillInMemoryUsageTreeRecursive(treeViewItem, statsItem) + local statId = statsItem.Name + local statLabel = StatsUtils.GetMemoryAnalyzerStatName(statId) + local statValue = statsItem:GetValue() + + treeViewItem:setLabelAndValue(statLabel, statValue) + + local rawChildren = statsItem:GetChildren() + -- sort children by name. + local sortedChildren = CommonUtils.SortByName(rawChildren) + + for i, childStatItem in ipairs(sortedChildren) do + local childStatId = childStatItem.Name + local childTreeItem = treeViewItem:getOrMakeChildById(childStatId) + __FillInMemoryUsageTreeRecursive(childTreeItem, childStatItem) + end +end + +--[[ Classes ]]-- + +--//////////////////////////////////// +-- +-- ClientMemoryAnalyzerClass +-- The whole table customized for Client memory. +-- +--//////////////////////////////////// +local ClientMemoryAnalyzerClass = {} +-- Derive from BaseMemoryAnalyzerClass +setmetatable(ClientMemoryAnalyzerClass, BaseMemoryAnalyzerClass) +ClientMemoryAnalyzerClass.__index = ClientMemoryAnalyzerClass + +function ClientMemoryAnalyzerClass.new(parentFrame) + local self = BaseMemoryAnalyzerClass.new(parentFrame) + setmetatable(self, ClientMemoryAnalyzerClass) + + self._rootTreeViewItem = nil + + -- am I currently listening for updates? + self._shouldListenForUpdates = false + + -- management of loop that does the listening. + -- Note: it is not enough to just track _shouldListenForUpdates. + -- If we called + -- startListeningForUpdates + -- stopListeningForUpdates + -- startListeningForUpdates + -- in quick succession, then in the second startListeningForUpdates, + -- _shouldListenForUpdates is false, but we still have a loop spawned from + -- the earlier call to startListeningForUpdates. + self._spawnedLoopScheduled = false + + return self +end + +-- Start a thread that wakes up every n seconds +-- and updates contents of stats widget. +function ClientMemoryAnalyzerClass:startListeningForUpdates() + self._shouldListenForUpdates = true + + -- If we have already scheduled a loop to start, we're done. + if (self._spawnedLoopScheduled) then + return + end + self._spawnedLoopScheduled = true + + spawn(function() + while(self._shouldListenForUpdates) do + self:refreshMemoryUsageTree() + self:renderUpdates() + wait(1) + end + -- Note that scheduled loop is now dead. + self._spawnedLoopScheduled = false + end) +end + +-- Stop the thread that does the updates. +function ClientMemoryAnalyzerClass:stopListeningForUpdates() + self._shouldListenForUpdates = false +end + +-- Generate the memory usage tree. +function ClientMemoryAnalyzerClass:refreshMemoryUsageTree() + if (self._rootTreeViewItem == nil) then + self._rootTreeViewItem = TreeViewItem.new("root", nil) + end + + local statsItem = __GetMemoryPerformanceStatsItem() + if statsItem == nil then + return nil + end + + __FillInMemoryUsageTreeRecursive(self._rootTreeViewItem, statsItem) +end + +function ClientMemoryAnalyzerClass:getMemoryUsageTree() + return self._rootTreeViewItem +end + +return ClientMemoryAnalyzerClass diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/DecoratedValueLabel.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/DecoratedValueLabel.lua new file mode 100644 index 0000000..9f6cba7 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/DecoratedValueLabel.lua @@ -0,0 +1,103 @@ +--[[ + Filename: DecoratedValueLabel.lua + Written by: dbanks + Description: Icon, text label, numeric value. + Icon is set by creator/caller. +--]] + +local CoreGuiService = game:GetService('CoreGui') + +local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils) + +local RobloxTranslator +local FFlagCoreScriptsUseLocalizationModule = settings():GetFFlag('CoreScriptsUseLocalizationModule') +if FFlagCoreScriptsUseLocalizationModule then + RobloxTranslator = require(CoreGuiService.RobloxGui.Modules.RobloxTranslator) +end + +function LocalizedGetKey(key) + local rtv = key + pcall(function() + if FFlagCoreScriptsUseLocalizationModule then + rtv = RobloxTranslator:FormatByKey(key) + else + local LocalizationService = game:GetService("LocalizationService") + local CorescriptLocalization = LocalizationService:GetCorescriptLocalizations()[1] + rtv = CorescriptLocalization:GetString(LocalizationService.RobloxLocaleId, key) + end + end) + + return rtv +end + +local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end) +local FFlagUseNotificationsLocalization = success and result + +local DecoratedValueLabelClass = {} +DecoratedValueLabelClass.__index = DecoratedValueLabelClass + +function DecoratedValueLabelClass.new(statType, valueName) + local self = {} + setmetatable(self, DecoratedValueLabelClass) + + self._frame = Instance.new("Frame") + self._frame.Name = "PS_DecoratedValueLabel" + self._frame.BackgroundTransparency = 1.0 + + if FFlagUseNotificationsLocalization == true then + self._valueName = LocalizedGetKey(valueName) + else + self._valueName = valueName + end + + self._statType = statType + + self._decorationFrame = Instance.new("Frame") + self._decorationFrame.Name = "PS_Decoration" + self._decorationFrame.Parent = self._frame + self._decorationFrame.Position = UDim2.new(0, 0, 0.5, -StatsUtils.DecorationSize/2) + self._decorationFrame.Size = UDim2.new(0, StatsUtils.DecorationSize, + 0, StatsUtils.DecorationSize) + self._decorationFrame.BackgroundTransparency = 1.0 + + self._label =Instance.new("TextLabel") + self._label.Name = "Label" + self._label.Parent = self._frame + self._label.Position = UDim2.new(0, StatsUtils.DecorationSize + StatsUtils.DecorationMargin, + 0, 0) + self._label.Size = UDim2.new(1, -(StatsUtils.DecorationSize + StatsUtils.DecorationMargin), + 1, 0) + self._label.FontSize = StatsUtils.PanelValueFontSize + self._label.TextXAlignment = Enum.TextXAlignment.Left + self._label.TextYAlignment = Enum.TextYAlignment.Center + + StatsUtils.StyleTextWidget(self._label) + + return self +end + +function DecoratedValueLabelClass:SetZIndex(zIndex) + self._frame.ZIndex = zIndex + self._decorationFrame.ZIndex = zIndex + self._label.ZIndex = zIndex +end + + +function DecoratedValueLabelClass:PlaceInParent(parent, size, position) + self._frame.Parent = parent + self._frame.Size = size + self._frame.Position = position +end + + +function DecoratedValueLabelClass:GetDecorationFrame() + return self._decorationFrame +end + +function DecoratedValueLabelClass:SetValue(value) + local formattedValue = StatsUtils.FormatTypedValue(value, self._statType) + self._label.Text = string.format("%s: %s", self._valueName, formattedValue) +end + + +return DecoratedValueLabelClass \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/DeveloperConsolePrimitives.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/DeveloperConsolePrimitives.lua new file mode 100644 index 0000000..32e7196 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/DeveloperConsolePrimitives.lua @@ -0,0 +1,103 @@ +--[[ + Filename: DeveloperConsolePrimitives.lua + Written by: dbanks + Description: Refactoring code from DeveloperConsoleModule. +--]] + + +--[[ Services ]]-- +local CoreGui = game:GetService('CoreGui') + +--[[ Modules ]]-- +local Style = require(CoreGui.RobloxGui.Modules.Stats.DeveloperConsoleStyle) + + +local Primitives = {}; +do + local function new(className, parent, name) + local n = Instance.new(className, parent) + n.ZIndex = 0 + if name then + n.Name = name + end + return n + end + local unitSize = UDim2.new(1, 0, 1, 0) + + local function setupFrame(n) + n.BackgroundColor3 = Style.FrameColor + n.BackgroundTransparency = Style.FrameTransparency + n.BorderSizePixel = 0 + end + local function setupText(n, text) + n.Font = Style.Font + n.TextColor3 = Style.TextColor + n.Text = text or n.Text + end + + function Primitives.Frame(parent, name) + local n = new('Frame', parent, name) + setupFrame(n) + return n + end + function Primitives.TextLabel(parent, name, text) + local n = new('TextLabel', parent, name) + setupFrame(n) + setupText(n, text) + return n + end + function Primitives.TextBox(parent, name, text) + local n = new('TextBox', parent, name) + setupFrame(n) + setupText(n, text) + return n + end + function Primitives.TextButton(parent, name, text) + local n = new('TextButton', parent, name) + setupFrame(n) + setupText(n, text) + return n + end + function Primitives.Button(parent, name) + local n = new('TextButton', parent, name) + setupFrame(n) + n.Text = "" + return n + end + function Primitives.ImageButton(parent, name, image) + local n = new('ImageButton', parent, name) + setupFrame(n) + n.Image = image or "" + n.Size = unitSize + return n + end + + -- An invisible frame of size (1, 0, 1, 0) + function Primitives.FolderFrame(parent, name) -- Should this be called InvisibleFrame? lol + local n = new('Frame', parent, name) + n.BackgroundTransparency = 1 + n.Size = unitSize + return n + end + function Primitives.InvisibleTextLabel(parent, name, text) + local n = new('TextLabel', parent, name) + setupText(n, text) + n.BackgroundTransparency = 1 + return n + end + function Primitives.InvisibleButton(parent, name, text) + local n = new('TextButton', parent, name) + n.BackgroundTransparency = 1 + n.Text = "" + return n + end + function Primitives.InvisibleImageLabel(parent, name, image) + local n = new('ImageLabel', parent, name) + n.BackgroundTransparency = 1 + n.Image = image or "" + n.Size = unitSize + return n + end +end + +return Primitives \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/DeveloperConsoleStyle.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/DeveloperConsoleStyle.lua new file mode 100644 index 0000000..b76474d --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/DeveloperConsoleStyle.lua @@ -0,0 +1,94 @@ +--[[ + Filename: DeveloperConsolePrimitives.lua + Written by: dbanks + Description: Refactoring code from DeveloperConsoleModule. +--]] + +local Style; + +do + local function c3(r, g, b) + return Color3.new(r / 255, g / 255, b / 255) + end + local frameColor = Color3.new(0.1, 0.1, 0.1) + local textColor = Color3.new(1, 1, 1) + local optionsFrameColor = Color3.new(1, 1, 1) + + pcall(function() -- Fun window colors for cool people + local Players = game:GetService("Players") + if not Players or not Players.LocalPlayer then + return + end + local FunColors = { + [56449] = {c3(255, 63, 127)}; -- ReeseMcBlox + [6949935] = {c3(255, 63, 127)}; -- NobleDragon + } + local funColor = FunColors[Players.LocalPlayer.UserId] + if funColor then + frameColor = funColor[1] or frameColor + textColor = funColor[2] or textColor + end + end) + + Style = { + ZINDEX = 6; + Font = Enum.Font.SourceSans; + FontBold = Enum.Font.SourceSansBold; + + HandleHeight = 24; -- How tall the top window handle is, as well as the width of the scroll bar + TabHeight = 28; + GearSize = 24; + BorderSize = 2; + CommandLineHeight = 22; + + OptionAreaHeight = 56; + + FrameColor = frameColor; -- Applies to pretty much everything, including buttons + FrameTransparency = 0.5; + OptionsFrameColor = optionsFrameColor; + + TextColor = textColor; + + MessageColors = { + [0] = Color3.new(1, 1, 1); -- Enum.MessageType.MessageOutput + [1] = Color3.new(0.4, 0.5, 1); -- Enum.MessageType.MessageInfo + [2] = Color3.new(1, 0.6, 0.4); -- Enum.MessageType.MessageWarning + [3] = Color3.new(1, 0, 0); -- Enum.MessageType.MessageError + }; + + ScrollbarFrameColor = frameColor; + ScrollbarBarColor = frameColor; + + ScriptButtonHeight = 32; + ScriptButtonColor = Color3.new(0, 1/3, 2/3); + ScriptButtonTransparency = 0.5; + + CheckboxSize = 24; + + ChartTitleHeight = 20; + ChartGraphHeight = 64; + ChartDataHeight = 24; + ChartHeight = 0; -- This gets added up at end and set at end of block + ChartWidth = 620; + + -- (-1) means right to left + -- (1) means left to right + ChartGraphDirection = 1; -- the direction the bars move + + + GetButtonDownColor = function(normalColor) + local r, g, b = normalColor.r, normalColor.g, normalColor.b + return Color3.new(1 - 0.75 * (1 - r), 1 - 0.75 * (1 - g), 1 - 0.75 * (1 - b)) + end; + GetButtonHoverColor = function(normalColor) + local r, g, b = normalColor.r, normalColor.g, normalColor.b + return Color3.new(1 - 0.875 * (1 - r), 1 - 0.875 * (1 - g), 1 - 0.875 * (1 - b)) + end; + + } + + Style.ChartHeight = Style.ChartTitleHeight + Style.ChartGraphHeight + Style.ChartDataHeight + Style.BorderSize + +end + +return Style \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/ServerMemoryAnalyzer.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/ServerMemoryAnalyzer.lua new file mode 100644 index 0000000..a7c135d --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/ServerMemoryAnalyzer.lua @@ -0,0 +1,133 @@ +--[[ + Filename: ServerMemoryAnalyzer.lua + Written by: dbanks + Description: Widget to display server memory usage. +--]] + +--[[ Services ]]-- +local CoreGuiService = game:GetService('CoreGui') + +--[[ Modules ]]-- +local BaseMemoryAnalyzerClass = require(CoreGuiService.RobloxGui.Modules.Stats.BaseMemoryAnalyzer) +local TreeViewItem = require(CoreGuiService.RobloxGui.Modules.Stats.TreeViewItem) +local CommonUtils = require(CoreGuiService.RobloxGui.Modules.Common.CommonUtil) + +--[[ Globals ]]-- +local BYTES_PER_MB = 1048576.0; + +-- labelToBytesUsedMap maps string to "num bytes used". +-- for each item in the map: +-- Make sure there's a child in the tree for that string. +-- Update that child to have latest value. +-- Add value to a sum total. +-- Return the sum total. +local function __ReadAndSumValues(treeViewItem, labelToBytesUsedMap) + local totalMB = 0; + + for label, numBytes in pairs(labelToBytesUsedMap) do + -- Convert to MB. + local numMB = numBytes / BYTES_PER_MB + totalMB = totalMB + numMB + local childTreeViewItem = treeViewItem:getOrMakeChildById(label) + childTreeViewItem:setLabelAndValue(label, numMB) + end + return totalMB +end + + +--[[ Classes ]]-- + +--//////////////////////////////////// +-- +-- ServerMemoryAnalyzerClass +-- The whole table customized for Server memory. +-- +--//////////////////////////////////// +local ServerMemoryAnalyzerClass = {} +-- Derive from BaseMemoryAnalyzerClass +setmetatable(ServerMemoryAnalyzerClass, BaseMemoryAnalyzerClass) +ServerMemoryAnalyzerClass.__index = ServerMemoryAnalyzerClass + +function ServerMemoryAnalyzerClass.new(parentFrame) + local self = BaseMemoryAnalyzerClass.new(parentFrame) + setmetatable(self, ServerMemoryAnalyzerClass) + + self._cachedRootTreeViewItem = nil + self._coreTreeViewItem = nil + self._placeTreeViewItem = nil + self._untrackedTreeViewItem = nil + + self._isVisible = false + + return self +end + +-- 'static' function. +-- 'stats' is a value table from server. +-- One top-level key is "ServerMemoryTree". +-- That contains a table that looks like this: +-- "totalServerMemory": +-- "developerTags": +-- : +-- (for all developer tags). +-- "internalCategories":
+-- : +-- (for all categories associated with the "internal" developer tag.) +-- We want to 'filter' this so that we return just the "ServerMemoryTree" value. +function ServerMemoryAnalyzerClass:filterServerMemoryTreeStats(stats) + if (stats.ServerMemoryTree == nil) then + return {} + else + return stats.ServerMemoryTree + end +end + +function ServerMemoryAnalyzerClass:updateWithTreeStats(stats) + local totalServerMemory = 0 + + if (self._cachedRootTreeViewItem == nil) then + self._cachedRootTreeViewItem = TreeViewItem.new("root", nil) + -- make sure the childen are in the order I want them in. + self._coreTreeViewItem = self._cachedRootTreeViewItem:getOrMakeChildById("CoreMemory") + self._placeTreeViewItem = self._cachedRootTreeViewItem:getOrMakeChildById("PlaceMemory") + self._untrackedTreeViewItem = self._cachedRootTreeViewItem:getOrMakeChildById("UntrackedMemory") + end + + + -- All values are in bytes. + -- Convert to MB ASAP. + for key, value in pairs(stats) do + if key == "totalServerMemory" then + totalServerMemory = value / BYTES_PER_MB + elseif key == "developerTags" then + local sum = __ReadAndSumValues(self._placeTreeViewItem, value) + self._placeTreeViewItem:setLabelAndValue("PlaceMemory", sum) + elseif key == "internalCategories" then + local sum = __ReadAndSumValues(self._coreTreeViewItem, value) + self._coreTreeViewItem:setLabelAndValue("CoreMemory", sum) + end + end + + -- Update total. + self._cachedRootTreeViewItem:setLabelAndValue("Memory", totalServerMemory) + + -- Update untracked. + local untrackedMemory = totalServerMemory - + (self._coreTreeViewItem:getValue() + self._placeTreeViewItem:getValue()) + self._untrackedTreeViewItem:setLabelAndValue("UntrackedMemory", untrackedMemory) + + self:renderUpdates(); +end + +function ServerMemoryAnalyzerClass:getMemoryUsageTree() + return self._cachedRootTreeViewItem +end + +function ServerMemoryAnalyzerClass:setVisible(isVisible) + self._isVisible = isVisible + if (self._isVisible) then + self:renderUpdates() + end +end + +return ServerMemoryAnalyzerClass diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsAggregator.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsAggregator.lua new file mode 100644 index 0000000..de46715 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsAggregator.lua @@ -0,0 +1,156 @@ +--[[ + Filename: StatsAggregator.lua + Written by: dbanks + Description: Gather and store stats on regular heartbeat. +--]] + +--[[ Services ]]-- +local CoreGuiService = game:GetService('CoreGui') + +--[[ Modules ]]-- +local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils) + +--[[ Classes ]]-- +local StatsAggregatorClass = {} +StatsAggregatorClass.__index = StatsAggregatorClass + +function StatsAggregatorClass.new(statType, numSamples, pauseBetweenSamples) + local self = {} + setmetatable(self, StatsAggregatorClass) + + self._statType = statType + self._numSamples = numSamples + self._pauseBetweenSamples = pauseBetweenSamples + + self._statName = StatsUtils.StatNames[self._statType] + self._statMaxName = StatsUtils.StatMaxNames[self._statType] + + -- init our circular buffer. + self._samples = {} + for i = 0, numSamples-1, 1 do + self._samples[i] = 0 + end + self._oldestIndex = 0 + + self._listeners = {} + + -- FIXME(dbanks) + -- Just want to be real clear this is a key, not an array index. + self._nextListenerId = 1001 + + return self +end + +function StatsAggregatorClass:AddListener(callbackFunction) + local id = self._nextListenerId + self._nextListenerId = self._nextListenerId+1 + self._listeners[id] = callbackFunction + return id +end + +function StatsAggregatorClass:RemoveListener(listenerId) + self._listeners[listenerId] = nil +end + +function StatsAggregatorClass:_notifyAllListeners() + for listenerId, listenerCallback in pairs(self._listeners) do + listenerCallback() + end +end + +function StatsAggregatorClass:StartListening() + -- On a regular heartbeat, wake up and read the latest + -- value into circular buffer. + -- Don't bother if we're already listening. + if (self._listening == true) then + return + end + + spawn(function() + self._listening = true + while(self._listening) do + local statValue = self:_getStatValue() + self:_storeStatValue(statValue) + self:_notifyAllListeners() + wait(self._pauseBetweenSamples) + end + end) +end + +function StatsAggregatorClass:StopListening() + self._listening = false +end + +function StatsAggregatorClass:GetValues() + -- Get the past N values, from oldest to newest. + local retval = {} + local actualIndex + for i = 0, self._numSamples-1, 1 do + actualIndex = (self._oldestIndex + i) % self._numSamples + retval[i+1] = self._samples[actualIndex] + end + return retval +end + +function StatsAggregatorClass:GetAverage() + -- Get average of past N values. + local retval = 0.0 + for i = 0, self._numSamples-1, 1 do + retval = retval + self._samples[i] + end + return retval / self._numSamples +end + +function StatsAggregatorClass:GetLatestValue() + -- Get latest value. + local index = (self._oldestIndex + self._numSamples -1) % self._numSamples + return self._samples[index] +end + +function StatsAggregatorClass:_storeStatValue(value) + -- Store this as the latest value in our circular buffer. + self._samples[self._oldestIndex] = value + self._oldestIndex = (self._oldestIndex + 1) % self._numSamples +end + +function StatsAggregatorClass:_getStatValue() + -- Look up and return the statistic we care about. + local statsService = game:GetService("Stats") + if statsService == nil then + return 0 + end + + local performanceStats = statsService:FindFirstChild("PerformanceStats") + if performanceStats == nil then + return 0 + end + + local itemStats = performanceStats:FindFirstChild(self._statName) + if itemStats == nil then + return 0 + end + + return itemStats:GetValue() +end + +function StatsAggregatorClass:GetTarget() + -- Look up and return the statistic we care about. + local statsService = game:GetService("Stats") + if statsService == nil then + return 0 + end + + local performanceStats = statsService:FindFirstChild("PerformanceStats") + if performanceStats == nil then + return 0 + end + + local itemStats = performanceStats:FindFirstChild(self._statMaxName) + if itemStats == nil then + return 0 + end + + return itemStats:GetValue() +end + +return StatsAggregatorClass diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsAggregatorManager.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsAggregatorManager.lua new file mode 100644 index 0000000..8e24f16 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsAggregatorManager.lua @@ -0,0 +1,67 @@ + +--[[ + Filename: StatsAggregatorManager.lua + Written by: dbanks + Description: Indexed array of stats aggregators, one for each stat. +--]] + +--[[ Services ]]-- +local CoreGuiService = game:GetService('CoreGui') + +--[[ Modules ]]-- +local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils) +local StatsAggregatorClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsAggregator) + + +--[[ Classes ]]-- +local StatsAggregatorManagerClass = {} +StatsAggregatorManagerClass.__index = StatsAggregatorManagerClass + +StatsAggregatorManagerClass.SecondsBetweenUpdate = 1.0 +StatsAggregatorManagerClass.NumSamplesToKeep = 20 + +local statsAggregatorManagerSingleton = nil + +function StatsAggregatorManagerClass.getSingleton() + if (statsAggregatorManagerSingleton == nil) then + statsAggregatorManagerSingleton = StatsAggregatorManagerClass.__new() + -- Start listening for updates in stats. + statsAggregatorManagerSingleton:StartListening() + end + return statsAggregatorManagerSingleton +end + +function StatsAggregatorManagerClass.__new() + local self = {} + setmetatable(self, StatsAggregatorManagerClass) + + self._statsAggregators = {} + + for i, statType in ipairs(StatsUtils.AllStatTypes) do + local statsAggregator = StatsAggregatorClass.new(statType, + StatsAggregatorManagerClass.NumSamplesToKeep, + StatsAggregatorManagerClass.SecondsBetweenUpdate) + self._statsAggregators[statType] = statsAggregator + end + + return self +end + + +function StatsAggregatorManagerClass:StartListening() + for i, statsAggregator in pairs(self._statsAggregators) do + statsAggregator:StartListening() + end +end + +function StatsAggregatorManagerClass:StopListening() + for i, statsAggregator in pairs(self._statsAggregators) do + statsAggregator:StopListening() + end +end + +function StatsAggregatorManagerClass:GetAggregator(statsType) + return self._statsAggregators[statsType] +end + +return StatsAggregatorManagerClass \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsAnnotatedGraph.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsAnnotatedGraph.lua new file mode 100644 index 0000000..5f93267 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsAnnotatedGraph.lua @@ -0,0 +1,240 @@ +--[[ + Filename: StatsAnnotatedGraph.lua + Written by: dbanks + Description: A graph plus extra annotations like axis markings, + target lines, etc. +--]] + +--[[ Services ]]-- +local CoreGuiService = game:GetService('CoreGui') + +--[[ Globals ]]-- +local Margin = 10 +local LabelXWidth = 30 + +--[[ Modules ]]-- +local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils) +local BarGraphClass = require(CoreGuiService.RobloxGui.Modules.Stats.BarGraph) + +--[[ Classes ]]-- +local StatsAnnotatedGraphClass = {} +StatsAnnotatedGraphClass.__index = StatsAnnotatedGraphClass + +function StatsAnnotatedGraphClass.new(statType, isMaximized) + local self = {} + setmetatable(self, StatsAnnotatedGraphClass) + + self._statType = statType + self._statMaxName = StatsUtils.StatMaxNames[statType] + self._isMaximized = isMaximized + + self._values = {} + + -- Average value of all bars in the graph. + self._average = 0 + -- Suggested max value for the stat being measured. + self._target = 0 + -- Max value we display on the y-axis. Values higher than this are truncated. + self._axisMax = 0 + + self._frame = Instance.new("Frame") + self._frame.Name = "PS_AnnotatedGraph" + self._frame.BackgroundTransparency = 1.0 + self._frame.ZIndex = StatsUtils.GraphZIndex + + self._topLabel = Instance.new("TextLabel") + self._topLabel.Name = "PS_TopAxisLabel" + self._topLabel.Parent = self._frame + self._topLabel.TextXAlignment = Enum.TextXAlignment.Left + self._topLabel.TextYAlignment = Enum.TextYAlignment.Top + self._topLabel.FontSize = StatsUtils.PanelGraphFontSize + + self._bottomLabel = Instance.new("TextLabel") + self._bottomLabel.Name = "PS_BottomAxisLabel" + self._bottomLabel.Parent = self._frame + self._bottomLabel.TextXAlignment = Enum.TextXAlignment.Left + self._bottomLabel.TextYAlignment = Enum.TextYAlignment.Bottom + self._bottomLabel.FontSize = StatsUtils.PanelGraphFontSize + + local showExtras = isMaximized + self._graph = BarGraphClass.new(showExtras) + + StatsUtils.StyleTextWidget(self._topLabel) + StatsUtils.StyleTextWidget(self._bottomLabel) + + self:_layoutElements() + + return self +end + +function StatsAnnotatedGraphClass:SetZIndex(zIndex) + self._frame.ZIndex = zIndex + self._topLabel.ZIndex = zIndex + self._bottomLabel.ZIndex = zIndex + self._graph:SetZIndex(zIndex) +end + +function StatsAnnotatedGraphClass:_layoutElements() + local labelWidth + if (self._isMaximized) then + labelWidth = LabelXWidth + + self._topLabel.Visible = true + self._bottomLabel.Visible = true + else + labelWidth = 0 + + self._topLabel.Visible = false + self._bottomLabel.Visible = false + end + + local GraphFramePosition = UDim2.new(0, Margin, 0, Margin) + local GraphFrameSize = UDim2.new(1, -(2 * Margin + labelWidth), 1, -2 * Margin) + + local TopLabelFramePosition = UDim2.new(1, -(Margin + labelWidth), 0, Margin) + local TopLabelFrameSize = UDim2.new(0, labelWidth, 0.333, -2 * Margin) + local BottomLabelFramePosition = UDim2.new(1, -(Margin + labelWidth), 0.666, Margin) + local BottomLabelFrameSize = UDim2.new(0, labelWidth, 0.333, -2 * Margin) + + self._topLabel.Size = TopLabelFrameSize + self._topLabel.Position = TopLabelFramePosition + self._bottomLabel.Size = BottomLabelFrameSize + self._bottomLabel.Position = BottomLabelFramePosition + + self._graph:PlaceInParent(self._frame, GraphFrameSize, GraphFramePosition) +end + +function StatsAnnotatedGraphClass:PlaceInParent(parent, size, position) + self._frame.Position = position + self._frame.Size = size + self._frame.Parent = parent +end + +function StatsAnnotatedGraphClass:_getTarget() + -- Get the current target value for the graphed stat. + if self._performanceStats == nil then + return 0 + end + + local maxItemStats = self._performanceStats:FindFirstChild(self._statMaxName) + if maxItemStats == nil then + return 0 + end + + return maxItemStats:GetValue() +end + +function StatsAnnotatedGraphClass:_render() + self._graph:SetAxisMax(self._axisMax) + self._graph:SetValues(self._values) + + self._graph:SetAverage(self._average) + self._graph:SetTarget(self._target) + self._graph:Render() + + self._topLabel.Text = string.format("%.2f", self._axisMax) + self._bottomLabel.Text = string.format("%.2f", 0,.0) +end + +function StatsAnnotatedGraphClass:_calculateAxisMax() + -- Calculate an optimal max axis label for this graph, given this 'target' value. + -- We want target to be roughly in the middle. + -- Say, roughly twice the target. + local max = self._target * 2 + local orderOfMagnitude = self:_recursiveGetOrderOfMagnitude(1.0, max) + local div = math.floor(0.5 + max/orderOfMagnitude) + self._axisMax = div * orderOfMagnitude +end + +function StatsAnnotatedGraphClass:SetStatsAggregator(aggregator) + if (self._aggregator) then + self._aggregator:RemoveListener(self._listenerId) + self._listenerId = nil + self._aggregator = nil + end + + self._aggregator = aggregator + + self:_refreshVisibility() +end + +function StatsAnnotatedGraphClass:_stopListening() + if (self._aggregator == nil) then + return + end + + if (self._listenerId == nil) then + return + end + + self._aggregator:RemoveListener(self._listenerId) + self._listenerId = nil +end + +function StatsAnnotatedGraphClass:_startListening() + if (self._aggregator == nil) then + return + end + + if (self._listenerId ~= nil) then + return + end + + self._listenerId = self._aggregator:AddListener(function() + self:_updateValuesAndRender() + end) +end + +function StatsAnnotatedGraphClass:_shouldBeVisible() + if StatsUtils:PerformanceStatsShouldBeVisible() then + return self._frame.Visible + else + return false + end +end + +function StatsAnnotatedGraphClass:SetVisible(visible) + self._frame.Visible = visible + self:_refreshVisibility() +end + +function StatsAnnotatedGraphClass:_refreshVisibility() + if self:_shouldBeVisible() then + self:_startListening() + self:_updateValuesAndRender() + else + self:_stopListening() + end +end + +function StatsAnnotatedGraphClass:OnPerformanceStatsShouldBeVisibleChanged() + self:_refreshVisibility() +end + +function StatsAnnotatedGraphClass:_recursiveGetOrderOfMagnitude(estimate, target) + if (estimate > target) then + return self:_recursiveGetOrderOfMagnitude(estimate/10.0, target) + end + + if (estimate * 10 >= target) then + return estimate + end + + return self:_recursiveGetOrderOfMagnitude(estimate*10.0, target) +end + +function StatsAnnotatedGraphClass:_updateValuesAndRender() + self._values = {} + self._average = 0 + self._target = 0 + if self._aggregator ~= nil then + self._values = self._aggregator:GetValues() + self._average = self._aggregator:GetAverage() + self._target = self._aggregator:GetTarget() + end + + self:_calculateAxisMax() + self:_render() +end + +return StatsAnnotatedGraphClass diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsButton.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsButton.lua new file mode 100644 index 0000000..da695f1 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsButton.lua @@ -0,0 +1,102 @@ +--[[ + Filename: StatsButton.lua + Written by: dbanks + Description: Button that displays latest deets of one or two + particular stats. +--]] + +--[[ Services ]]-- +local CoreGuiService = game:GetService('CoreGui') + +--[[ Modules ]]-- +local StyleWidgets = require(CoreGuiService.RobloxGui.Modules.StyleWidgets) +local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils) +local StatsMiniTextPanelClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsMiniTextPanel) +local StatsAnnotatedGraphClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsAnnotatedGraph) + +--[[ Globals ]]-- +local TextPanelXFraction = 0.5 +local GraphXFraction = 1 - TextPanelXFraction + +local TextPanelPosition = UDim2.new(0, 0, 0, 0) +local TextPanelSize = UDim2.new(TextPanelXFraction, 0, 1, -StyleWidgets.TabSelectionHeight) +local GraphPosition = UDim2.new(TextPanelXFraction, 0, 0, 0) +local GraphSize = UDim2.new(GraphXFraction, 0, 1, -StyleWidgets.TabSelectionHeight) + +--[[ Classes ]]-- +local StatsButtonClass = {} +StatsButtonClass.__index = StatsButtonClass + +function StatsButtonClass.new(statType) + local self = {} + setmetatable(self, StatsButtonClass) + + self._statType = statType + self._button = Instance.new("TextButton") + self._button.Name = "PS_Button" + self._button.Text = "" + + StatsUtils.StyleButton(self._button) + + self._textPanel = StatsMiniTextPanelClass.new(statType) + self._textPanel:PlaceInParent(self._button, + TextPanelSize, + TextPanelPosition) + + self._graph = StatsAnnotatedGraphClass.new(statType, false) + self._graph:PlaceInParent(self._button, + GraphSize, + GraphPosition) + + self._textPanel:SetZIndex(StatsUtils.TextZIndex) + self._graph:SetZIndex(StatsUtils.GraphZIndex) + + self._tabSelection = StyleWidgets.MakeTabSelectionWidget(self._button) + + self._isSelected = false + + self:_updateTabSelectionState(); + + return self +end + +function StatsButtonClass:OnPerformanceStatsShouldBeVisibleChanged() + if self._graph then + self._graph:OnPerformanceStatsShouldBeVisibleChanged() + end + + if self._textPanel then + self._textPanel:OnPerformanceStatsShouldBeVisibleChanged() + end +end + +function StatsButtonClass:SetToggleCallbackFunction(callbackFunction) + self._button.MouseButton1Click:connect(function() + callbackFunction(self._statType) + end) +end + +function StatsButtonClass:SetSizeAndPosition(size, position) + self._button.Size = size; + self._button.Position = position; +end + +function StatsButtonClass:SetIsSelected(isSelected) + self._isSelected = isSelected + self:_updateTabSelectionState(); +end + +function StatsButtonClass:_updateTabSelectionState() + self._tabSelection.Visible = self._isSelected +end + +function StatsButtonClass:SetParent(parent) + self._button.Parent = parent +end + +function StatsButtonClass:SetStatsAggregator(aggregator) + self._textPanel:SetStatsAggregator(aggregator) + self._graph:SetStatsAggregator(aggregator) +end + +return StatsButtonClass \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsMiniTextPanel.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsMiniTextPanel.lua new file mode 100644 index 0000000..44be46a --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsMiniTextPanel.lua @@ -0,0 +1,175 @@ +--[[ + Filename: StatsMiniTextPanel.lua + Written by: dbanks + Description: Panel that shows title and current value for a given stat. +--]] + +--[[ Globals ]]-- +local TitleHeightYFraction = 0.4 +local ValueHeightYFraction = 0.3 +local TitleTopYFraction = (1 - TitleHeightYFraction - + ValueHeightYFraction)/2 +local LeftMarginPix = 10 + +local TitlePosition = UDim2.new(0, + LeftMarginPix, + TitleTopYFraction, + 0) +local TitleSize = UDim2.new(1, + -LeftMarginPix * 2, + TitleHeightYFraction, + 0) +local ValuePosition = UDim2.new(0, + LeftMarginPix, + TitleTopYFraction + TitleHeightYFraction, + 0) +local ValueSize = UDim2.new(1, + -LeftMarginPix * 2, + ValueHeightYFraction, + 0) + +--[[ Services ]]-- +local CoreGuiService = game:GetService('CoreGui') + +--[[ Modules ]]-- +local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils) +local StatsAggregatorClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsAggregator) + +--[[ Classes ]]-- +local StatsMiniTextPanelClass = {} +StatsMiniTextPanelClass.__index = StatsMiniTextPanelClass + +function StatsMiniTextPanelClass.new(statType) + local self = {} + setmetatable(self, StatsMiniTextPanelClass) + + self._statType = statType + + self._frame = Instance.new("Frame") + self._frame.Name = "StatsMiniTextPanelClass" + + self._frame.BackgroundTransparency = 1.0 + self._frame.ZIndex = StatsUtils.TextPanelZIndex + + self._titleLabel = Instance.new("TextLabel") + self._titleLabel.Name = "TitleLabel" + self._valueLabel = Instance.new("TextLabel") + self._valueLabel.Name = "ValueLabel" + + StatsUtils.StyleTextWidget(self._titleLabel) + StatsUtils.StyleTextWidget(self._valueLabel) + + self._titleLabel.FontSize = StatsUtils.MiniPanelTitleFontSize + self._titleLabel.Text = self:_getTitle() + self._titleLabel.Parent = self._frame + self._titleLabel.Size = TitleSize + self._titleLabel.Position = TitlePosition + self._titleLabel.TextXAlignment = Enum.TextXAlignment.Left + + self._valueLabel.FontSize = StatsUtils.MiniPanelValueFontSize + self._valueLabel.Text = "0" + self._valueLabel.Parent = self._frame + self._valueLabel.Size = ValueSize + self._valueLabel.Position = ValuePosition + self._valueLabel.TextXAlignment = Enum.TextXAlignment.Left + + return self +end + +function StatsMiniTextPanelClass:SetZIndex(zIndex) + self._frame.ZIndex = zIndex + self._titleLabel.ZIndex = zIndex + self.ZIndex = zIndex +end + +function StatsMiniTextPanelClass:_getTitle() + return StatsUtils.TypeToShortName[self._statType] +end + +function StatsMiniTextPanelClass:PlaceInParent(parent, size, position) + self._frame.Position = position + self._frame.Size = size + self._frame.Parent = parent +end + +function StatsMiniTextPanelClass:SetStatsAggregator(aggregator) + if (self._aggregator) then + self._aggregator:RemoveListener(self._listenerId) + self._listenerId = nil + self._aggregator = nil + end + + self._aggregator = aggregator + + self:_refreshVisibility() +end + +function StatsMiniTextPanelClass:SetVisible(visible) + self.frame.Visible = visible + self:_refreshVisibility() +end + +function StatsMiniTextPanelClass:_shouldBeVisible() + if StatsUtils.PerformanceStatsShouldBeVisible() then + return self._frame.Visible + else + return false + end +end + + +function StatsMiniTextPanelClass:_refreshVisibility() + if self:_shouldBeVisible() then + self:_startListening() + self:_updateFromAggregator() + else + self:_stopListening() + end +end + +function StatsMiniTextPanelClass:_stopListening() + if (self._aggregator == nil) then + return + end + + if (self._listenerId == nil) then + return + end + + self._aggregator:RemoveListener(self._listenerId) + self._listenerId = nil +end + +function StatsMiniTextPanelClass:_startListening() + if (self._aggregator == nil) then + return + end + + if (self._listenerId ~= nil) then + return + end + + self._listenerId = self._aggregator:AddListener(function() + self:_updateFromAggregator() + end) +end + +function StatsMiniTextPanelClass:OnPerformanceStatsShouldBeVisibleChanged() + self:_refreshVisibility() +end + +function StatsMiniTextPanelClass:_updateFromAggregator() + local value + + if self._aggregator ~= nil then + value = self._aggregator:GetLatestValue() + else + value = 0 + end + + self._valueLabel.Text = StatsUtils.FormatTypedValue(value, self._statType) +end + + + +return StatsMiniTextPanelClass \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsTextPanel.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsTextPanel.lua new file mode 100644 index 0000000..1615f38 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsTextPanel.lua @@ -0,0 +1,265 @@ +--[[ + Filename: StatsTextPanel.lua + Written by: dbanks + Description: Panel that shows a "Legend" for a graph, including: + - name of stat being displayed. + - current value of stat. + - target (suggested max) value of stat. + - average value of stat over the whole graph. + particular stat. +--]] + +--[[ Services ]]-- +local CoreGuiService = game:GetService('CoreGui') + +--[[ Modules ]]-- +local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils) +local StatsAggregatorClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsAggregator) +local DecoratedValueLabelClass = require(CoreGuiService.RobloxGui.Modules.Stats.DecoratedValueLabel) + +--[[ Globals ]]-- +-- Positions +local top = StatsUtils.TextPanelTopMarginPix +local TitlePosition = UDim2.new(0, + StatsUtils.TextPanelLeftMarginPix, + 0, + top) +top = top + StatsUtils.TextPanelTitleHeightY +local CurrentValuePosition = UDim2.new(0, + StatsUtils.TextPanelLeftMarginPix, + 0, + top) +top = top + StatsUtils.TextPanelLegendItemHeightY +local TargetValuePosition = UDim2.new(0, + StatsUtils.TextPanelLeftMarginPix, + 0, + top) +top = top + StatsUtils.TextPanelLegendItemHeightY +local AverageValuePosition = UDim2.new(0, + StatsUtils.TextPanelLeftMarginPix, + 0, + top) + +-- Sizes +local TitleSize = UDim2.new(1, + -StatsUtils.TextPanelLeftMarginPix * 2, + 0, + StatsUtils.TextPanelTitleHeightY) +local LegendItemValueSize = UDim2.new(1, + -StatsUtils.TextPanelLeftMarginPix * 2, + 0, + StatsUtils.TextPanelLegendItemHeightY) + +--[[ Classes ]]-- +local StatsTextPanelClass = {} +StatsTextPanelClass.__index = StatsTextPanelClass + +function StatsTextPanelClass.new(statType) + local self = {} + setmetatable(self, StatsTextPanelClass) + + self._statType = statType + + self._frame = Instance.new("Frame") + self._frame.BackgroundTransparency = 1.0 + self._frame.ZIndex = StatsUtils.TextPanelZIndex + + self._titleLabel = Instance.new("TextLabel") + + StatsUtils.StyleTextWidget(self._titleLabel) + + self._titleLabel.FontSize = StatsUtils.PanelTitleFontSize + self._titleLabel.Text = self:_getTitle() + + self._titleLabel.Parent = self._frame + self._titleLabel.Size = TitleSize + self._titleLabel.Position = TitlePosition + self._titleLabel.TextXAlignment = Enum.TextXAlignment.Left + self._titleLabel.TextYAlignment = Enum.TextYAlignment.Top + + -- Icon + text widgets to show the current value of this stat. + self:_addCurrentValueWidget() + -- Icon + text widgets to show the suggested max value of this stat. + self:_addTargetValueWidget() + -- Icon + text widgets to show the average value of this stat. + self:_addAverageValueWidget() + + return self +end + +function StatsTextPanelClass:_getTarget() + -- Get the current target value for the graphed stat. + if self._performanceStats == nil then + return 0 + end + + local maxItemStats = self._performanceStats:FindFirstChild(self._statMaxName) + if maxItemStats == nil then + return 0 + end + + return maxItemStats:GetValue() +end + + +function StatsTextPanelClass:_addCurrentValueWidget() + self._currentValueWidget = DecoratedValueLabelClass.new(self._statType, + "Current") + + self._currentValueWidget:PlaceInParent(self._frame, + LegendItemValueSize, + CurrentValuePosition) + + local decorationFrame = self._currentValueWidget:GetDecorationFrame() + local decoration = Instance.new("ImageLabel") + decoration.Position = UDim2.new(0.5, -StatsUtils.OvalKeySize/2, + 0.5, -StatsUtils.OvalKeySize/2) + decoration.Size = UDim2.new(0, StatsUtils.OvalKeySize, + 0, StatsUtils.OvalKeySize) + + decoration.Parent = decorationFrame + decoration.BackgroundTransparency = 1 + decoration.Image = 'rbxasset://textures/ui/PerformanceStats/OvalKey.png' + decoration.BorderSizePixel = 0 + self._currentValueDecoration = decoration +end + +function StatsTextPanelClass:_addTargetValueWidget() + self._targetValueWidget = DecoratedValueLabelClass.new(self._statType, + "Target") + + self._targetValueWidget:PlaceInParent(self._frame, + LegendItemValueSize, + TargetValuePosition) + + local decorationFrame = self._targetValueWidget:GetDecorationFrame() + local decoration = Instance.new("ImageLabel") + decoration.Position = UDim2.new(0.5, -StatsUtils.TargetKeyWidth/2, + 0.5, -StatsUtils.TargetKeyHeight/2) + decoration.Size = UDim2.new(0, StatsUtils.TargetKeyWidth, + 0, StatsUtils.TargetKeyHeight) + + decoration.Parent = decorationFrame + decoration.BackgroundTransparency = 1 + decoration.Image = 'rbxasset://textures/ui/PerformanceStats/TargetKey.png' +end + +function StatsTextPanelClass:_addAverageValueWidget() + self._averageValueWidget = DecoratedValueLabelClass.new(self._statType, + "Average") + + self._averageValueWidget:PlaceInParent(self._frame, + LegendItemValueSize, + AverageValuePosition) + + local decorationFrame = self._averageValueWidget:GetDecorationFrame() + + local decoration = Instance.new("Frame") + decoration.Position = UDim2.new(0, 0, 0.5, -StatsUtils.GraphAverageLineTotalThickness/2) + decoration.Size = UDim2.new(1, 0, 0, StatsUtils.GraphAverageLineInnerThickness) + decoration.Parent = decorationFrame + + StatsUtils.StyleAverageLine(decoration) +end + +function StatsTextPanelClass:_getTitle() + return StatsUtils.TypeToName[self._statType] +end + +function StatsTextPanelClass:PlaceInParent(parent, size, position) + self._frame.Position = position + self._frame.Size = size + self._frame.Parent = parent +end + + +function StatsTextPanelClass:SetStatsAggregator(aggregator) + if (self._aggregator) then + self._aggregator:RemoveListener(self._listenerId) + self._listenerId = nil + self._aggregator = nil + end + + self._aggregator = aggregator + + self:_refreshVisibility() +end + +function StatsTextPanelClass:SetVisible(visible) + self._frame.Visible = visible + self:_refreshVisibility() +end + +function StatsTextPanelClass:_shouldBeVisible() + if StatsUtils.PerformanceStatsShouldBeVisible() then + return self._frame.Visible + else + return false + end +end + + +function StatsTextPanelClass:_refreshVisibility() + if self:_shouldBeVisible() then + self:_startListening() + self:_updateFromAggregator() + else + self:_stopListening() + end +end + +function StatsTextPanelClass:_stopListening() + if (self._aggregator == nil) then + return + end + + if (self._listenerId == nil) then + return + end + + self._aggregator:RemoveListener(self._listenerId) + self._listenerId = nil +end + +function StatsTextPanelClass:_startListening() + if (self._aggregator == nil) then + return + end + + if (self._listenerId ~= nil) then + return + end + + self._listenerId = self._aggregator:AddListener(function() + self:_updateFromAggregator() + end) +end + +function StatsTextPanelClass:_updateFromAggregator() + local value = 0 + local average = 0 + local target = 0 + + if self._aggregator ~= nil then + value = self._aggregator:GetLatestValue() + average = self._aggregator:GetAverage() + target = self._aggregator:GetTarget() + end + + self._currentValueWidget:SetValue(value) + self._targetValueWidget:SetValue(target) + self._averageValueWidget:SetValue(average) + + self._currentValueDecoration.ImageColor3 = StatsUtils.GetColorForValue(value, target) + +end + +function StatsTextPanelClass:SetZIndex(zIndex) + -- Pass through to all children. + self._frame.ZIndex = zIndex + self._titleLabel.ZIndex = zIndex + self._currentValueWidget:SetZIndex(zIndex) + self._averageValueWidget:SetZIndex(zIndex) +end + +return StatsTextPanelClass \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsUtils.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsUtils.lua new file mode 100644 index 0000000..7262655 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsUtils.lua @@ -0,0 +1,265 @@ +--[[ + Filename: StatsUtils.lua + Written by: dbanks + Description: Common work in the performance stats world. +--]] + +local CoreGuiService = game:GetService('CoreGui') +local PlayersService = game:GetService("Players") +local Settings = UserSettings() +local GameSettings = Settings.GameSettings + +local TopbarConstants = require(CoreGuiService.RobloxGui.Modules.TopbarConstants) +local StyleWidgets = require(CoreGuiService.RobloxGui.Modules.StyleWidgets) + +local FFlagStatUtilsUseFormatByKey = settings():GetFFlag('StatUtilsUseFormatByKey') +local RobloxTranslator +if FFlagStatUtilsUseFormatByKey then + RobloxTranslator = require(CoreGuiService.RobloxGui.Modules.RobloxTranslator) +end + +function LocalizedGetKey(key) + local rtv = key + pcall(function() + local LocalizationService = game:GetService("LocalizationService") + local CorescriptLocalization = LocalizationService:GetCorescriptLocalizations()[1] + + if FFlagStatUtilsUseFormatByKey then + rtv = RobloxTranslator:FormatByKey(key) + else + pcall(function() + local LocalizationService = game:GetService("LocalizationService") + local CorescriptLocalization = LocalizationService:GetCorescriptLocalizations()[1] + rtv = CorescriptLocalization:GetString(LocalizationService.RobloxLocaleId, key) + end) + end + end) + return rtv +end + +local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end) +local FFlagUseNotificationsLocalization = success and result + +--[[ Classes ]]-- +local StatsUtils = {} + +-- Colors +StatsUtils.SelectedBackgroundColor = Color3.new(0.4, 0.4, 0.4) +StatsUtils.FontColor = Color3.new(1, 1, 1) +StatsUtils.GraphBarGreenColor = Color3.new(126/255.0, 211/255.0, 33/255.0) +StatsUtils.GraphBarYellowColor = Color3.new(209/255.0, 211/255.0, 33/255.0) +StatsUtils.GraphBarRedColor = Color3.new(211/255.0, 88/255.0, 33/255.0) +StatsUtils.GraphAverageLineColor = Color3.new(208/255.0, 1/255.0, 27/255.0) +StatsUtils.GraphAverageLineBorderColor = Color3.new(1, 1, 1) +StatsUtils.NormalColor = TopbarConstants.TOPBAR_BACKGROUND_COLOR +StatsUtils.Transparency = TopbarConstants.TOPBAR_TRANSLUCENT_TRANSPARENCY; + +-- Font Sizes +StatsUtils.MiniPanelTitleFontSize = Enum.FontSize.Size12 +StatsUtils.MiniPanelValueFontSize = Enum.FontSize.Size10 +StatsUtils.PanelTitleFontSize = Enum.FontSize.Size24 +StatsUtils.PanelValueFontSize = Enum.FontSize.Size14 +StatsUtils.PanelGraphFontSize = Enum.FontSize.Size10 + +-- Layout +-- Layout: Buttons +StatsUtils.ButtonHeight = 36 + StyleWidgets.TabSelectionHeight + +-- Layout: Viewer +StatsUtils.ViewerTopMargin = 10 +StatsUtils.ViewerHeight = 144 +StatsUtils.ViewerWidth = 306 + +StatsUtils.TextZIndex = 5 +StatsUtils.GraphZIndex = 2 + +-- Layout: Graph +StatsUtils.GraphTargetLineInnerThickness = 2 +StatsUtils.GraphAverageLineInnerThickness = 2 +StatsUtils.GraphAverageLineBorderThickness = 1 +StatsUtils.GraphAverageLineTotalThickness = (StatsUtils.GraphAverageLineInnerThickness + + 2 * StatsUtils.GraphAverageLineBorderThickness) + +-- Layout: Main Text Panel +StatsUtils.TextPanelTitleHeightY = 52 +StatsUtils.TextPanelLegendItemHeightY = 20 + +StatsUtils.TextPanelLeftMarginPix = 10 +StatsUtils.TextPanelTopMarginPix = 10 + +-- Layout: Graph Legend +StatsUtils.DecorationSize = 12 +StatsUtils.OvalKeySize = 8 +StatsUtils.TargetKeyWidth = 11 +StatsUtils.TargetKeyHeight = 2 +StatsUtils.DecorationMargin = 6 + + +-- Enums +StatsUtils.StatType_Memory = "st_Memory" +StatsUtils.StatType_CPU = "st_CPU" +StatsUtils.StatType_GPU = "st_GPU" +StatsUtils.StatType_NetworkSent = "st_NetworkSent" +StatsUtils.StatType_NetworkReceived = "st_NetworkReceived" +StatsUtils.StatType_Physics = "st_Physics" + +StatsUtils.AllStatTypes = { + StatsUtils.StatType_Memory, + StatsUtils.StatType_CPU, + StatsUtils.StatType_GPU, + StatsUtils.StatType_NetworkSent, + StatsUtils.StatType_NetworkReceived, + StatsUtils.StatType_Physics, +} + +StatsUtils.StatNames = { + [StatsUtils.StatType_Memory] = "Memory", + [StatsUtils.StatType_CPU] = "CPU", + [StatsUtils.StatType_GPU] = "GPU", + [StatsUtils.StatType_NetworkSent] = "NetworkSent", + [StatsUtils.StatType_NetworkReceived] = "NetworkReceived", + [StatsUtils.StatType_Physics] = "Physics", +} + +StatsUtils.StatMaxNames = { + [StatsUtils.StatType_Memory] = "MaxMemory", + [StatsUtils.StatType_CPU] = "MaxCPU", + [StatsUtils.StatType_GPU] = "MaxGPU", + [StatsUtils.StatType_NetworkSent] = "MaxNetworkSent", + [StatsUtils.StatType_NetworkReceived] = "MaxNetworkReceived", + [StatsUtils.StatType_Physics] = "MaxPhysics", +} + +StatsUtils.NumButtonTypes = table.getn(StatsUtils.AllStatTypes) + +local strSentNetwork = "" +local strReceivedNetwork = "" +if FFlagUseNotificationsLocalization or FFlagStatUtilsUseFormatByKey then + strSentNetwork = LocalizedGetKey("Sent") .. "\n" .. LocalizedGetKey("Network") + strReceivedNetwork = LocalizedGetKey("Received") .. "\n" .. LocalizedGetKey("Network") +else + strSentNetwork = "Sent\n(Network)" + strReceivedNetwork = "Received\n(Network)" +end + +StatsUtils.TypeToName = { + [StatsUtils.StatType_Memory] = "Memory", + [StatsUtils.StatType_CPU] = "CPU", + [StatsUtils.StatType_GPU] = "GPU", + [StatsUtils.StatType_NetworkSent] = strSentNetwork, + [StatsUtils.StatType_NetworkReceived] = strReceivedNetwork, + [StatsUtils.StatType_Physics] = "Physics", +} + +StatsUtils.TypeToShortName = { + [StatsUtils.StatType_Memory] = "Mem", + [StatsUtils.StatType_CPU] = "CPU", + [StatsUtils.StatType_GPU] = "GPU", + [StatsUtils.StatType_NetworkSent] = "Sent", + [StatsUtils.StatType_NetworkReceived] = "Recv", + [StatsUtils.StatType_Physics] = "Phys", +} + +StatsUtils.MemoryAnalyzerTypeToName = { + ["HumanoidTexture"] = "Humanoid Textures", + ["HumanoidTextureOrphan"] = "Humanoid Textures (Unused)", + ["OtherTexture"] = "Other Textures", + ["OtherTextureOrphan"] = "Other Textures (Unused)", + ["CoreScript"] = "Core Scripts", + ["UserScript"] = "User Scripts", + ["Sounds"] = "Sounds", + ["CSG"] = "Solid Models", + ["Meshes"] = "Meshes", +} + +function StatsUtils.GetMemoryAnalyzerStatName(memoryAnalyzerStatType) + if (StatsUtils.MemoryAnalyzerTypeToName[memoryAnalyzerStatType] == nil) then + return memoryAnalyzerStatType + else + return StatsUtils.MemoryAnalyzerTypeToName[memoryAnalyzerStatType] + end +end + +function StatsUtils.StyleFrame(frame) + frame.BackgroundColor3 = StatsUtils.NormalColor + frame.BackgroundTransparency = StatsUtils.Transparency +end + +function StatsUtils.StyleButton(button) + button.BackgroundColor3 = StatsUtils.NormalColor + button.BackgroundTransparency = StatsUtils.Transparency +end + +function StatsUtils.StyleTextWidget(textLabel) + textLabel.BackgroundTransparency = 1.0 + textLabel.TextColor3 = StatsUtils.FontColor + textLabel.Font = Enum.Font.SourceSansBold +end + +function StatsUtils.StyleButtonSelected(frame, isSelected) + StatsUtils.StyleButton(frame) + if (isSelected) then + frame.BackgroundColor3 = StatsUtils.SelectedBackgroundColor + end +end + +local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end) +local FFlagUseNotificationsLocalization = success and result +local function LocalizedGetString(key, rtv) + pcall(function() + local LocalizationService = game:GetService("LocalizationService") + local CorescriptLocalization = LocalizationService:GetCorescriptLocalizations()[1] + rtv = CorescriptLocalization:GetString(LocalizationService.RobloxLocaleId, key) + end) + + return rtv +end + +function StatsUtils.FormatTypedValue(value, statType) + if FFlagUseNotificationsLocalization then + if statType == StatsUtils.StatType_CPU or statType == StatsUtils.StatType_GPU then + return string.gsub(LocalizedGetString("StatsUtil.ms",string.format("%.2f MB", value)),"{RBX_NUMBER}",string.format("%.2f",value)) + elseif statType == StatsUtils.StatType_PlaceMemory then + return string.gsub(LocalizedGetString("StatsUtil.MB",string.format("%.2f ms", value)),"{RBX_NUMBER}",string.format("%.2f",value)) + elseif statType == StatsUtils.StatType_NetworkSent or statType == StatsUtils.StatType_NetworkReceived then + return string.gsub(LocalizedGetString("StatsUtil.KBps",string.format("%.2f KB/s", value)),"{RBX_NUMBER}",string.format("%.2f",value)) + end + end + + if statType == StatsUtils.StatType_Memory then + return string.format("%.2f MB", value) + elseif statType == StatsUtils.StatType_CPU then + return string.format("%.2f ms", value) + elseif statType == StatsUtils.StatType_GPU then + return string.format("%.2f ms", value) + elseif statType == StatsUtils.StatType_NetworkSent then + return string.format("%.2f KB/s", value) + elseif statType == StatsUtils.StatType_NetworkReceived then + return string.format("%.2f KB/s", value) + elseif statType == StatsUtils.StatType_Physics then + return string.format("%.2f ms", value) + end +end + +function StatsUtils.StyleAverageLine(frame) + frame.BackgroundColor3 = StatsUtils.GraphAverageLineColor + frame.BorderSizePixel = StatsUtils.GraphAverageLineBorderThickness + frame.BorderColor3 = StatsUtils.GraphAverageLineBorderColor +end + +function StatsUtils.GetColorForValue(value, target) + if value < 0.666 * target then + return StatsUtils.GraphBarGreenColor + elseif value < 1.333 * target then + return StatsUtils.GraphBarYellowColor + else + return StatsUtils.GraphBarRedColor + end +end + +function StatsUtils.PerformanceStatsShouldBeVisible() + local localPlayer = PlayersService.LocalPlayer + return (GameSettings.PerformanceStatsVisible and localPlayer ~= nil) +end + +return StatsUtils diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsViewer.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsViewer.lua new file mode 100644 index 0000000..107e9ca --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/StatsViewer.lua @@ -0,0 +1,139 @@ +--[[ + Filename: StatsButtonViewer.lua + Written by: dbanks + Description: Widget that displays one or more stats in closeup view: + text and graphics. +--]] + +--[[ Services ]]-- +local CoreGuiService = game:GetService('CoreGui') + +--[[ Modules ]]-- +local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils) +local StatsTextPanelClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsTextPanel) +local StatsAnnotatedGraphClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsAnnotatedGraph) + +--[[ Globals ]]-- +local TextPanelXFraction = 0.4 +local GraphXFraction = 1 - TextPanelXFraction + +local TextPanelPosition = UDim2.new(0, 0, 0, 0) +local TextPanelSize = UDim2.new(TextPanelXFraction, 0, 1, 0) +local GraphPosition = UDim2.new(TextPanelXFraction, 0, 0, 0) +local GraphSize = UDim2.new(GraphXFraction, 0, 1, 0) + +--[[ Classes ]]-- +local StatsViewerClass = {} +StatsViewerClass.__index = StatsViewerClass + + +function StatsViewerClass.new() + local self = {} + setmetatable(self, StatsViewerClass) + + self._frameImageLabel = Instance.new("ImageLabel") + self._frameImageLabel.Name = "PS_Viewer" + self._frameImageLabel.Image = 'rbxasset://textures/ui/PerformanceStats/BackgroundRounded.png' + self._frameImageLabel.ScaleType = Enum.ScaleType.Slice + self._frameImageLabel.SliceCenter = Rect.new(10, 10, 22, 22) + self._frameImageLabel.BackgroundTransparency = 1 + self._frameImageLabel.ImageColor3 = StatsUtils.NormalColor + self._frameImageLabel.ImageTransparency = StatsUtils.Transparency + + self._textPanel = nil + self._statType = nil + self._graph = nil + + return self +end + +function StatsViewerClass:OnPerformanceStatsShouldBeVisibleChanged() + if self._graph then + self._graph:OnPerformanceStatsShouldBeVisibleChanged() + end + if self._textPanel then + self._textPanel:OnPerformanceStatsShouldBeVisibleChanged() + end + if self._textPanel then + self._textPanel:OnVisibilityChanged() + end +end + +function StatsViewerClass:GetIsVisible() + return self._frameImageLabel.Visible +end + +function StatsViewerClass:GetStatType() + return self._statType +end + +function StatsViewerClass:SetSizeAndPosition(size, position) + self._frameImageLabel.Size = size + self._frameImageLabel.Position = position +end + +function StatsViewerClass:SetParent(parent) + self._frameImageLabel.Parent = parent +end + +function StatsViewerClass:SetVisible(visible) + self._frameImageLabel.Visible = visible + + if (self._graph) then + self._graph:SetVisible(visible) + end + + if (self._textPanel) then + self._textPanel:SetVisible(visible) + end +end + +function StatsViewerClass:SetStatType(statType) + self._statType = statType + self._frameImageLabel:ClearAllChildren() + -- If there's already a text panel, make it clear it is no longer visible. + if (self._textPanel) then + self._textPanel:SetVisible(false) + self._textPanel = nil + end + + self._textPanel = StatsTextPanelClass.new(statType, true) + self._textPanel:PlaceInParent(self._frameImageLabel, + TextPanelSize, + TextPanelPosition) + + self._graph = StatsAnnotatedGraphClass.new(statType, true) + self._graph:PlaceInParent(self._frameImageLabel, + GraphSize, + GraphPosition) + + self._textPanel:SetZIndex(StatsUtils.TextZIndex) + self._graph:SetZIndex(StatsUtils.GraphZIndex) + + self._graph:SetVisible(self._frameImageLabel.Visible) + self._textPanel:SetVisible(self._frameImageLabel.Visible) + + + self:_applyStatsAggregator(); +end + +function StatsViewerClass:_applyStatsAggregator() + if (self._aggregator == nil) then + return + end + + if (self._textPanel) then + self._textPanel:SetStatsAggregator(self._aggregator) + end + if (self._graph) then + self._graph:SetStatsAggregator(self._aggregator) + end +end + + +function StatsViewerClass:SetStatsAggregator(aggregator) + self._aggregator = aggregator + self:_applyStatsAggregator(); + end + +return StatsViewerClass \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/Stats/TreeViewItem.lua b/Client2018/content/scripts/CoreScripts/Modules/Stats/TreeViewItem.lua new file mode 100644 index 0000000..5941231 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/Stats/TreeViewItem.lua @@ -0,0 +1,90 @@ +--[[ + Filename: TreeViewItem.lua + Written by: dbanks + Description: Generic tree view data stucture. +--]] + + +--[[ Globals ]]-- + +--[[ Helper functions ]]-- + +--[[ Classes ]]-- + +local TreeViewItem = {} +TreeViewItem.__index = TreeViewItem + + +function TreeViewItem.new(id, parent) + local self = {} + setmetatable(self, TreeViewItem) + + self._id = id + self._parent = parent + self._children = {} + return self +end + +function TreeViewItem:getValue() + return self._value +end + +function TreeViewItem:getLabel() + return self._label +end + +function TreeViewItem:getChildren() + return self._children +end + +function TreeViewItem:getId() + return self._id +end + +function TreeViewItem:getStackDepth() + if (self._parent == nil) then + return 0 + else + return 1 + self._parent:getStackDepth() + end +end + +function TreeViewItem:setLabelAndValue(label, value) + self._label = label + self._value = value +end + +function TreeViewItem:getOrMakeChildById(id) + for i, childTreeViewItem in ipairs(self._children) do + if (childTreeViewItem:getId() == id) then + return childTreeViewItem + end + end + + local newChild = TreeViewItem.new(id, self) + table.insert(self._children, newChild) + return newChild +end + +function TreeViewItem:removeChildren() + if not self._children then return end + for i, childTreeViewItem in ipairs(self._children) do + childTreeViewItem:removeChildren() + childTreeViewItem = nil + end + + self._id = nil + self._parent = nil + self._children = nil +end + +function TreeViewItem:removeChild(id) + for i, childTreeViewItem in ipairs(self._children) do + if (childTreeViewItem:getId() == id) then + childTreeViewItem:removeChildren() + return + end + end +end + +return TreeViewItem \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/StyleWidgets.lua b/Client2018/content/scripts/CoreScripts/Modules/StyleWidgets.lua new file mode 100644 index 0000000..7965825 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/StyleWidgets.lua @@ -0,0 +1,40 @@ +--[[ + Filename: StyleWidgets.lua + Written by: dbanks + Version 1.0 + Description: Widgets with common style elements that can be shared across + different GUIS. +--]] +----------------- SERVICES ------------------------------ +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") + +----------- UTILITIES -------------- +local utility = require(RobloxGui.Modules.Settings.Utility) + +----------- CLASS DECLARATION -------------- +--[[ Classes ]]-- +local StyleWidgets = {} + +----------- CONSTANTS -------------- +StyleWidgets.TabSelectionHeight = 6 + +function StyleWidgets.MakeTabSelectionWidget(parent) + local tabSelection = utility:Create'ImageLabel' + { + Name = "TabSelection", + Image = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuSelection.png", + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(3,1,4,5), + Visible = false, + BackgroundTransparency = 1, + Size = UDim2.new(1,0,0,StyleWidgets.TabSelectionHeight ), + Position = UDim2.new(0,0,1,-StyleWidgets.TabSelectionHeight ), + Parent = parent + }; + + return tabSelection +end + + +return StyleWidgets \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/TenFootInterface.lua b/Client2018/content/scripts/CoreScripts/Modules/TenFootInterface.lua new file mode 100644 index 0000000..6abf729 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/TenFootInterface.lua @@ -0,0 +1,389 @@ +--[[ + Filename: TenFootInterface.lua + Written by: jeditkacheff + Version 1.0 + Description: Setups up some special UI for ROBLOX TV gaming +--]] +-------------- CONSTANTS -------------- +local HEALTH_GREEN_COLOR = Color3.new(27/255, 252/255, 107/255) +local DISPLAY_POS_INIT_INSET = 0 +local DISPLAY_ITEM_OFFSET = 4 +local FORCE_TEN_FOOT_INTERFACE = false + +-------------- SERVICES -------------- +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local UserInputService = game:GetService("UserInputService") +local GuiService = game:GetService("GuiService") +local Players = game:GetService("Players") + +------------------ VARIABLES -------------------- +local tenFootInterfaceEnabled = false +do + local platform = UserInputService:GetPlatform() + + tenFootInterfaceEnabled = (platform == Enum.Platform.XBoxOne or platform == Enum.Platform.WiiU or platform == Enum.Platform.PS4 or + platform == Enum.Platform.AndroidTV or platform == Enum.Platform.XBox360 or platform == Enum.Platform.PS3 or + platform == Enum.Platform.Ouya or platform == Enum.Platform.SteamOS) +end + +if FORCE_TEN_FOOT_INTERFACE then + tenFootInterfaceEnabled = true +end + +local Util = {} +do + function Util.Create(instanceType) + return function(data) + local obj = Instance.new(instanceType) + for k, v in pairs(data) do + if type(k) == 'number' then + v.Parent = obj + else + obj[k] = v + end + end + return obj + end + end +end + +local function CreateModule() + local this = {} + local nextObjectDisplayYPos = DISPLAY_POS_INIT_INSET + local displayStack = {} + local displayStackChanged = Instance.new("BindableEvent") + local healthContainerPropertyChanged = Instance.new("BindableEvent") + + -- setup base gui + local function createContainer() + if not this.Container then + this.Container = Util.Create'ImageButton' + { + Name = "TopRightContainer"; + Size = UDim2.new(0, 350, 0, 100); + Position = UDim2.new(1,-415,0,10); + AutoButtonColor = false; + Image = ""; + Active = false; + BackgroundTransparency = 1; + Parent = RobloxGui; + }; + end + end + + function removeFromDisplayStack(displayObject) + local moveUpFromHere = nil + + for i = 1, #displayStack do + if displayStack[i] == displayObject then + moveUpFromHere = i + 1 + break + end + end + + local prevObject = displayObject + for i = moveUpFromHere, #displayStack do + local objectToMoveUp = displayStack[i] + objectToMoveUp.Position = UDim2.new(objectToMoveUp.Position.X.Scale, objectToMoveUp.Position.X.Offset, + objectToMoveUp.Position.Y.Scale, prevObject.AbsolutePosition.Y) + prevObject = objectToMoveUp + end + displayStackChanged:Fire() + end + + function addBackToDisplayStack(displayObject) + local moveDownFromHere = 0 + + for i = 1, #displayStack do + if displayStack[i] == displayObject then + moveDownFromHere = i + 1 + break + end + end + + local prevObject = displayObject + for i = moveDownFromHere, #displayStack do + local objectToMoveDown = displayStack[i] + local nextDisplayPos = prevObject.AbsolutePosition.Y + prevObject.AbsoluteSize.Y + DISPLAY_ITEM_OFFSET + objectToMoveDown.Position = UDim2.new(objectToMoveDown.Position.X.Scale, objectToMoveDown.Position.X.Offset, + objectToMoveDown.Position.Y.Scale, nextDisplayPos) + prevObject = objectToMoveDown + end + displayStackChanged:Fire() + end + + function addToDisplayStack(displayObject) + local lastDisplayed = nil + if #displayStack > 0 then + lastDisplayed = displayStack[#displayStack] + end + displayStack[#displayStack + 1] = displayObject + + local nextDisplayPos = DISPLAY_POS_INIT_INSET + if lastDisplayed then + nextDisplayPos = lastDisplayed.AbsolutePosition.Y + lastDisplayed.AbsoluteSize.Y + DISPLAY_ITEM_OFFSET + end + + displayObject.Position = UDim2.new(displayObject.Position.X.Scale, displayObject.Position.X.Offset, + displayObject.Position.Y.Scale, nextDisplayPos) + + createContainer() + displayObject.Parent = this.Container + + displayObject.Changed:connect(function(prop) + if prop == "Visible" then + if not displayObject.Visible then + removeFromDisplayStack(displayObject) + else + addBackToDisplayStack(displayObject) + end + end + end) + displayStackChanged:Fire() + end + + function this:CreateHealthBar() + this.HealthContainer = Util.Create'Frame'{ + Name = "HealthContainer"; + Size = UDim2.new(1, -86, 0, 50); + Position = UDim2.new(0, 92, 0, 0); + BorderSizePixel = 0; + BackgroundColor3 = Color3.new(0,0,0); + BackgroundTransparency = 0.5; + }; + + local healthFillHolder = Util.Create'Frame'{ + Name = "HealthFillHolder"; + Size = UDim2.new(1, -10, 1, -10); + Position = UDim2.new(0, 5, 0, 5); + BorderSizePixel = 0; + BackgroundColor3 = Color3.new(1,1,1); + BackgroundTransparency = 1.0; + Parent = this.HealthContainer; + }; + + local healthFill = Util.Create'Frame'{ + Name = "HealthFill"; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 0, 0); + BorderSizePixel = 0; + BackgroundTransparency = 0.0; + BackgroundColor3 = HEALTH_GREEN_COLOR; + Parent = healthFillHolder; + }; + + local healthText = Util.Create'TextLabel'{ + Name = "HealthText"; + Size = UDim2.new(0, 98, 0, 50); + Position = UDim2.new(0, -100, 0, 0); + BackgroundTransparency = 0.5; + BackgroundColor3 = Color3.new(0,0,0); + Font = Enum.Font.SourceSans; + FontSize = Enum.FontSize.Size36; + Text = "Health"; + TextColor3 = Color3.new(1,1,1); + BorderSizePixel = 0; + Parent = this.HealthContainer; + }; + + local username = Util.Create'TextLabel'{ + Visible = false + } + + local accountType = Util.Create'TextLabel'{ + Visible = false + } + + addToDisplayStack(this.HealthContainer) + createContainer() + + this.HealthContainer.Changed:connect(function() + healthContainerPropertyChanged:Fire() + end) + + return this.Container, username, this.HealthContainer, healthFill, accountType + end + + function this:CreateAccountType(accountTypeTextShort) + this.AccountTypeContainer = Util.Create'Frame'{ + Name = "AccountTypeContainer"; + Size = UDim2.new(0, 50, 0, 50); + Position = UDim2.new(1, -55, 0, 10); + BorderSizePixel = 0; + BackgroundColor3 = Color3.new(0,0,0); + BackgroundTransparency = 0.5; + Parent = RobloxGui; + }; + + local accountTypeTextLabel = Util.Create'TextLabel'{ + Name = "AccountTypeText"; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + BackgroundColor3 = Color3.new(0,0,0); + Font = Enum.Font.SourceSans; + FontSize = Enum.FontSize.Size36; + Text = accountTypeTextShort; + TextColor3 = Color3.new(1,1,1); + BorderSizePixel = 0; + Parent = this.AccountTypeContainer; + TextXAlignment = Enum.TextXAlignment.Center; + TextYAlignment = Enum.TextYAlignment.Center; + }; + end + + function this:SetupTopStat() + local topStatEnabled = true + local displayedStat = nil + local displayedStatChangedCon = nil + local displayedStatParentedCon = nil + local leaderstatsChildAddedCon = nil + local tenFootInterfaceStat = nil + + local function makeTenFootInterfaceStat() + if tenFootInterfaceStat then return end + + tenFootInterfaceStat = Util.Create'Frame'{ + Name = "OneStatFrame"; + Size = UDim2.new(1, 0, 0, 36); + Position = UDim2.new(0, 0, 0, 0); + BorderSizePixel = 0; + BackgroundTransparency = 1; + }; + local statName = Util.Create'TextLabel'{ + Name = "StatName"; + Size = UDim2.new(0.5,0,0,36); + BackgroundTransparency = 1; + Font = Enum.Font.SourceSans; + FontSize = Enum.FontSize.Size36; + TextStrokeColor3 = Color3.new(104/255, 104/255, 104/255); + TextStrokeTransparency = 0; + Text = " StatName:"; + TextColor3 = Color3.new(1,1,1); + TextXAlignment = Enum.TextXAlignment.Left; + BorderSizePixel = 0; + ClipsDescendants = true; + Parent = tenFootInterfaceStat; + }; + local statValue = statName:clone() + statValue.Position = UDim2.new(0.5,0,0,0) + statValue.Name = "StatValue" + statValue.Text = "123,643,231" + statValue.TextXAlignment = Enum.TextXAlignment.Right + statValue.Parent = tenFootInterfaceStat + + addToDisplayStack(tenFootInterfaceStat) + end + + local function setDisplayedStat(newStat) + if displayedStatChangedCon then displayedStatChangedCon:disconnect() displayedStatChangedCon = nil end + if displayedStatParentedCon then displayedStatParentedCon:disconnect() displayedStatParentedCon = nil end + + displayedStat = newStat + + if displayedStat then + makeTenFootInterfaceStat() + updateTenFootStat(displayedStat) + displayedStatParentedCon = displayedStat.AncestryChanged:connect(function() updateTenFootStat(displayedStat, "Parent") end) + displayedStatChangedCon = displayedStat.Changed:connect(function(prop) updateTenFootStat(displayedStat, prop) end) + end + end + + function updateTenFootStat(statObj, property) + if property and property == "Parent" then + tenFootInterfaceStat.StatName.Text = "" + tenFootInterfaceStat.StatValue.Text = "" + setDisplayedStat(nil) + + tenFootInterfaceChanged() + else + if topStatEnabled then + tenFootInterfaceStat.StatName.Text = " " .. tostring(statObj.Name) .. ":" + tenFootInterfaceStat.StatValue.Text = tostring(statObj.Value) + else + tenFootInterfaceStat.StatName.Text = "" + tenFootInterfaceStat.StatValue.Text = "" + end + end + end + + local function isValidStat(obj) + return obj:IsA('StringValue') or obj:IsA('IntValue') or obj:IsA('BoolValue') or obj:IsA('NumberValue') or + obj:IsA('DoubleConstrainedValue') or obj:IsA('IntConstrainedValue') + end + + local function tenFootInterfaceNewStat( newStat ) + if not displayedStat and isValidStat(newStat) then + setDisplayedStat(newStat) + end + end + + local localPlayer = Players.LocalPlayer + while not localPlayer do + Players.PlayerAdded:wait() + localPlayer = Players.LocalPlayer + end + + function tenFootInterfaceChanged() + local leaderstats = localPlayer:FindFirstChild('leaderstats') + if leaderstats then + local statChildren = leaderstats:GetChildren() + for i = 1, #statChildren do + tenFootInterfaceNewStat(statChildren[i]) + end + if leaderstatsChildAddedCon then leaderstatsChildAddedCon:disconnect() end + leaderstatsChildAddedCon = leaderstats.ChildAdded:connect(function(newStat) + tenFootInterfaceNewStat(newStat) + end) + end + end + + local leaderstats = localPlayer:FindFirstChild('leaderstats') + if leaderstats then + tenFootInterfaceChanged() + else + localPlayer.ChildAdded:connect(tenFootInterfaceChanged) + end + + --Top Stat Public API + + local topStatApiTable = {} + + function topStatApiTable:SetTopStatEnabled(value) + topStatEnabled = value + if displayedStat then + updateTenFootStat(displayedStat, "") + end + end + + return topStatApiTable + end + + return this +end + + +-- Public API + +local moduleApiTable = {} + + local TenFootInterfaceModule = CreateModule() + + function moduleApiTable:IsEnabled() + return tenFootInterfaceEnabled + end + + function moduleApiTable:CreateHealthBar() + return TenFootInterfaceModule:CreateHealthBar() + end + + function moduleApiTable:CreateAccountType(accountTypeText) + return TenFootInterfaceModule:CreateAccountType(accountTypeText) + end + + function moduleApiTable:SetupTopStat() + return TenFootInterfaceModule:SetupTopStat() + end + +return moduleApiTable diff --git a/Client2018/content/scripts/CoreScripts/Modules/TopbarConstants.lua b/Client2018/content/scripts/CoreScripts/Modules/TopbarConstants.lua new file mode 100644 index 0000000..e8e9a28 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/TopbarConstants.lua @@ -0,0 +1,38 @@ +--[[ + // FileName: Topbar.lua + // Written by: SolarCrane + // Description: Code for lua side Top Menu items in ROBLOX. +]] + + +--[[ CONSTANTS ]] + +local TopbarConstants = {} + +TopbarConstants.TOPBAR_THICKNESS = 36 +TopbarConstants.USERNAME_CONTAINER_WIDTH = 170 +TopbarConstants.COLUMN_WIDTH = 75 +TopbarConstants.NAME_LEADERBOARD_SEP_WIDTH = 2 + +TopbarConstants.ITEM_SPACING = 0 +TopbarConstants.VR_ITEM_SPACING = 3 + +TopbarConstants.FONT_COLOR = Color3.new(1,1,1) +TopbarConstants.TOPBAR_BACKGROUND_COLOR = Color3.fromRGB(31,31,31) +TopbarConstants.TOPBAR_OPAQUE_TRANSPARENCY = 0 +TopbarConstants.TOPBAR_TRANSLUCENT_TRANSPARENCY = 0.5 + +TopbarConstants.HEALTH_BACKGROUND_COLOR = Color3.fromRGB(228, 236, 246) +TopbarConstants.HEALTH_RED_COLOR = Color3.fromRGB(255, 28, 0) +TopbarConstants.HEALTH_YELLOW_COLOR = Color3.fromRGB(250, 235, 0) +TopbarConstants.HEALTH_GREEN_COLOR = Color3.fromRGB(27, 252, 107) + +TopbarConstants.HEALTH_PERCANTAGE_FOR_OVERLAY = 5 / 100 + +TopbarConstants.HURT_OVERLAY_IMAGE = "https://www.roblox.com/asset/?id=34854607" + +TopbarConstants.DEBOUNCE_TIME = 0.25 + +TopbarConstants.TOPBAR_LOCAL_CFRAME_3D = CFrame.new(0, -5, 5) * CFrame.Angles(math.rad(25), 0, 0) + +return TopbarConstants \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/VR/Controllers/ViveController.lua b/Client2018/content/scripts/CoreScripts/Modules/VR/Controllers/ViveController.lua new file mode 100644 index 0000000..da29969 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/VR/Controllers/ViveController.lua @@ -0,0 +1,346 @@ +local VRService = game:GetService("VRService") +local CoreGui = game:GetService("CoreGui") + +local RobloxGui = CoreGui.RobloxGui +local CommonUtil = require(RobloxGui.Modules.Common.CommonUtil) + +local PARTS_INFO = { + Body = { + size = Vector3.new(0.391, 0.277, 0.731), + meshId = "rbxassetid://433286977", + textureId = "rbxassetid://433287000", + offset = CFrame.new(0, -0.115, 0.213) + }, + RightGrip = { + meshId = "rbxassetid://433290663", + textureId = "rbxassetid://433290688", + offset = CFrame.new(0.057, -0.047, 0.29), + moveOffset = CFrame.new(-0.005, 0, 0) + }, + LeftGrip = { + meshId = "rbxassetid://433289821", + textureId = "rbxassetid://433289832", + offset = CFrame.new(-0.057, -0.047, 0.29), + moveOffset = CFrame.new(0.005, 0, 0) + }, + SystemButton = { + meshId = "rbxassetid://433291265", + textureId = "rbxassetid://433291278", + offset = CFrame.new(0, 0.005, 0.294), + moveOffset = CFrame.new(Vector3.new(0, 0.997420907, 0.071774438) * -0.001) + }, + MenuButton = { + meshId = "rbxassetid://433288124", + textureId = "rbxassetid://433288134", + offset = CFrame.new(0, 0.016, 0.068), + moveOffset = CFrame.new(Vector3.new(0, 0.999877751, -0.0156304892) * -0.001) + }, + Trackpad = { + meshId = "rbxassetid://433288821", + textureId = "rbxassetid://433288836", + offset = CFrame.new(0, 0.000, 0.163), + moveOffset = CFrame.new(Vector3.new(0, 0.993506849, 0.113772281) * -0.001) + }, + Trigger = { + meshId = "rbxassetid://433288767", + textureId = "rbxassetid://433288775", + offset = CFrame.new(0, -0.093, 0.163), + }, + LED = { + meshId = "rbxassetid://433293218", + textureId = "rbxassetid://433293231", + offset = CFrame.new(0, 0.007, 0.32) + }, + ButtonA = { + meshId = "rbxassetid://922444331", + textureId = "rbxasset://textures/ui/VR/ButtonA.png", + offset = CFrame.new(-0, 0.0115499999, 0.197307006) * CFrame.Angles(0, math.pi, 0), + moveOffset = CFrame.new(0, -0.005, 0) + }, + ButtonB = { + meshId = "rbxassetid://922454202", + textureId = "rbxasset://textures/ui/VR/ButtonB.png", + offset = CFrame.new(0.0391042456, 0.0154935224, 0.161779419) * CFrame.Angles(0, math.pi, 0), + moveOffset = CFrame.new(0, -0.005, 0) + }, + ButtonX = { + meshId = "rbxassetid://922453111", + textureId = "rbxasset://textures/ui/VR/ButtonX.png", + offset = CFrame.new(-0.0390719995, 0.0154770007, 0.161733001) * CFrame.Angles(0, math.pi, 0), + moveOffset = CFrame.new(0, -0.005, 0) + }, + ButtonY = { + meshId = "rbxassetid://922455502", + textureId = "rbxasset://textures/ui/VR/ButtonY.png", + offset = CFrame.new(0, 0.0197229274, 0.126810834) * CFrame.Angles(0, math.pi, 0), + moveOffset = CFrame.new(0, -0.005, 0) + } +} +local MATERIAL = Enum.Material.Granite +local SCALE = Vector3.new(0.033, 0.033, 0.033) + +local ViveController = {} +ViveController.__index = ViveController + +local touchpadForUserCFrame = { + [Enum.UserCFrame.LeftHand] = Enum.VRTouchpad.Left, + [Enum.UserCFrame.RightHand] = Enum.VRTouchpad.Right +} + +function ViveController.new(userCFrame) + local self = setmetatable({}, ViveController) + self.userCFrame = userCFrame + self.touchpad = touchpadForUserCFrame[self.userCFrame] + self.touchpadMode = VRService:GetTouchpadMode(self.touchpad) + self.onTouchpadModeChangedConn = VRService.TouchpadModeChanged:connect(function(...) + self:onTouchpadModeChanged(...) + end) + + self.model = CommonUtil.Create("Model") { + Name = "ViveController", + Archivable = false + } + + self.origin = CommonUtil.Create("Part") { + Parent = self.model, + Name = "Origin", + Anchored = false, + Transparency = 1, + Size = Vector3.new(0.05, 0.05, 0.05), + CanCollide = false + } + + self.parts = {} + + for partName, partInfo in pairs(PARTS_INFO) do + local partScale = SCALE + if partInfo.scale then + partScale = partScale * partInfo.scale + end + + local part = CommonUtil.Create("Part") { + Parent = self.model, + Name = partName, + Anchored = false, + CanCollide = false, + Material = MATERIAL, + Size = partInfo.size or Vector3.new(0.05, 0.05, 0.05), + CFrame = self.origin.CFrame * partInfo.offset + } + local mesh = CommonUtil.Create("SpecialMesh") { + Parent = part, + Name = "Mesh", + MeshId = partInfo.meshId, + TextureId = partInfo.textureId, + Scale = partScale + } + local weld = CommonUtil.Create("Weld") { + Parent = part, + Name = "Weld", + Part0 = self.origin, + Part1 = part, + C0 = partInfo.offset + } + + self.parts[partName] = part + end + + local trackpadIndicator = CommonUtil.Create("Part") { + Parent = self.model, + Name = "TrackpadIndicator", + Material = Enum.Material.Neon, + BrickColor = BrickColor.new("Institutional white"), + Shape = Enum.PartType.Ball, + Anchored = false, + Transparency = 1, + Size = Vector3.new(0.05, 0.05, 0.05), + CanCollide = false + } + CommonUtil.Create("Weld") { + Parent = trackpadIndicator, + Name = "Weld", + Part0 = self.origin, + Part1 = trackpadIndicator, + C0 = PARTS_INFO.Trackpad.offset + } + self.parts.TrackpadIndicator = trackpadIndicator + self.model.PrimaryPart = self.origin + + self:onTouchpadModeChanged(self.touchpad, VRService:GetTouchpadMode(self.touchpad)) + + return self +end + +function ViveController:setPartVisible(partName, visible) + local part = self.parts[partName] + if part then + part.Transparency = visible and 0 or 1 + end +end + +function ViveController:setABXYEnabled(enabled) + self:setPartVisible("ButtonA", enabled) + self:setPartVisible("ButtonB", enabled) + self:setPartVisible("ButtonX", enabled) + self:setPartVisible("ButtonY", enabled) +end + +function ViveController:setCFrame(cframe) + self.model:SetPrimaryPartCFrame(cframe) +end + +function ViveController:setButtonState(partName, depressed) + local partInfo = PARTS_INFO[partName] + if not partInfo then return end + + local offset = partInfo.offset + local moveOffset = partInfo.moveOffset + + if offset and moveOffset then + local part = self.parts[partName] + if part then + local mesh = part:FindFirstChild("Mesh") + local weld = part:FindFirstChild("Weld") + if weld then + if depressed then + part.CFrame = self.origin.CFrame * offset * moveOffset + weld.C0 = offset * moveOffset + else + part.CFrame = self.origin.CFrame * offset + weld.C0 = offset + end + end + if mesh then + if depressed then + mesh.VertexColor = Vector3.new(0.5, 0.5, 0.5) + else + mesh.VertexColor = Vector3.new(1, 1, 1) + end + end + end + end +end + +function ViveController:setTriggerState(state) + local partInfo = PARTS_INFO.Trigger + local offset = partInfo.offset + + local part = self.parts.Trigger + local weld = part:FindFirstChild("Weld") + if weld then + local angleMin, angleMax = math.rad(0), math.rad(-20) + local angleRange = angleMax - angleMin + local rotCenter = Vector3.new(0, 0.05, -0.025) + local angle = (state * angleRange) + angleMin + local newOffset = offset * CFrame.new(rotCenter) * CFrame.Angles(angle, 0, 0) * CFrame.new(-rotCenter) + part.CFrame = self.origin.CFrame * newOffset + weld.C0 = newOffset + end +end + +function ViveController:setTrackpadState(pos) + local part = self.parts.TrackpadIndicator + local weld = part:FindFirstChild("Weld") + if weld then + local pos3d = Vector3.new(pos.X, 0, -pos.Y) * 0.055 + local trackpadSpace = CFrame.Angles(math.rad(6.5), 0, 0) * CFrame.new(0, 0.002 * (pos.magnitude ^ 3), 0) + local newOffset = PARTS_INFO.Trackpad.offset * CFrame.new(0, 0.01, 0) * trackpadSpace:toWorldSpace(CFrame.new(pos3d)) + part.CFrame = self.origin.CFrame * newOffset + weld.C0 = newOffset + end +end + +function ViveController:onButtonInputChanged(inputObject, depressed) + --Trackpad is bound to L3 or R3 depending on controller + if (self.userCFrame == Enum.UserCFrame.RightHand and inputObject.KeyCode == Enum.KeyCode.ButtonR3) or + (self.userCFrame == Enum.UserCFrame.LeftHand and inputObject.KeyCode == Enum.KeyCode.ButtonL3) then + self:setButtonState("Trackpad", depressed) + return + end + + --Grips are bound to L2 or R2 depending on controller + if (self.userCFrame == Enum.UserCFrame.RightHand and inputObject.KeyCode == Enum.KeyCode.ButtonR1) or + (self.userCFrame == Enum.UserCFrame.LeftHand and inputObject.KeyCode == Enum.KeyCode.ButtonL1) then + self:setButtonState("LeftGrip", depressed) + self:setButtonState("RightGrip", depressed) + return + end + + --ButtonStart on the right, ButtonSelect on the left + if (self.userCFrame == Enum.UserCFrame.RightHand and inputObject.KeyCode == Enum.KeyCode.ButtonStart) or + (self.userCFrame == Enum.UserCFrame.LeftHand and inputObject.KeyCode == Enum.KeyCode.ButtonSelect) then + self:setButtonState("MenuButton", depressed) + return + end + + --If the touchpad mode is ABXY, then we have four buttons to deal with + if self.touchpadMode == Enum.VRTouchpadMode.ABXY then + if inputObject.KeyCode == Enum.KeyCode.ButtonA then + self:setButtonState("ButtonA", depressed) + return + end + if inputObject.KeyCode == Enum.KeyCode.ButtonB then + self:setButtonState("ButtonB", depressed) + return + end + if inputObject.KeyCode == Enum.KeyCode.ButtonX then + self:setButtonState("ButtonX", depressed) + return + end + if inputObject.KeyCode == Enum.KeyCode.ButtonY then + self:setButtonState("ButtonY", depressed) + return + end + end +end + +function ViveController:onInputBegan(inputObject) + self:onButtonInputChanged(inputObject, true) + if (self.userCFrame == Enum.UserCFrame.RightHand and inputObject.KeyCode == Enum.KeyCode.Thumbstick2) or + (self.userCFrame == Enum.UserCFrame.LeftHand and inputObject.KeyCode == Enum.KeyCode.Thumbstick1) then + self:setPartVisible("TrackpadIndicator", true) + end +end + +function ViveController:onInputChanged(inputObject) + if (self.userCFrame == Enum.UserCFrame.RightHand and inputObject.KeyCode == Enum.KeyCode.ButtonR2) or + (self.userCFrame == Enum.UserCFrame.LeftHand and inputObject.KeyCode == Enum.KeyCode.ButtonL2) then + self:setTriggerState(inputObject.Position.Z) + end + if (self.userCFrame == Enum.UserCFrame.RightHand and inputObject.KeyCode == Enum.KeyCode.Thumbstick2) or + (self.userCFrame == Enum.UserCFrame.LeftHand and inputObject.KeyCode == Enum.KeyCode.Thumbstick1) then + self:setTrackpadState(inputObject.Position) + end +end + +function ViveController:onInputEnded(inputObject) + self:onButtonInputChanged(inputObject, false) + if (self.userCFrame == Enum.UserCFrame.RightHand and inputObject.KeyCode == Enum.KeyCode.Thumbstick2) or + (self.userCFrame == Enum.UserCFrame.LeftHand and inputObject.KeyCode == Enum.KeyCode.Thumbstick1) then + self:setPartVisible("TrackpadIndicator", false) + end +end + +function ViveController:onTouchpadModeChanged(touchpad, touchpadMode) + if touchpad ~= self.touchpad then + return + end + + self.touchpadMode = touchpadMode + + if touchpadMode == Enum.VRTouchpadMode.ABXY then + self:setABXYEnabled(true) + else + self:setABXYEnabled(false) + end +end + +function ViveController:destroy() + if self.onTouchpadModeChangedConn then + self.onTouchpadModeChangedConn:disconnect() + self.onTouchpadModeChangedConn = nil + end + self.model:Destroy() +end + +return ViveController \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/VR/Dialog.lua b/Client2018/content/scripts/CoreScripts/Modules/VR/Dialog.lua new file mode 100644 index 0000000..e39adbe --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/VR/Dialog.lua @@ -0,0 +1,199 @@ +--Dialog: 3D dialogs for ROBLOX in VR +--written by 0xBAADF00D +--6/30/2016 + +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local InputService = game:GetService("UserInputService") +local RunService = game:GetService("RunService") +local Utility = require(RobloxGui.Modules.Settings.Utility) +local Panel3D = require(RobloxGui.Modules.VR.Panel3D) + +local DIALOG_BG_COLOR = Color3.new(0.2, 0.2, 0.2) +local DIALOG_BG_TRANSPARENCY = 0.3 +local DIALOG_TITLE_HEIGHT = 66 +local DIALOG_COLOR_HEIGHT = 8 +local DIALOG_TITLE_TEXT_SIZE = Enum.FontSize.Size36 +local DIALOG_CONTENT_PADDING = 48 +local TITLE_COLOR = Color3.new(1, 1, 1) + +local PANEL_OFFSET_CF = CFrame.new(0, 0, -7) * CFrame.Angles(0, math.pi, 0) + +local emptySelectionImage = Utility:Create "ImageLabel" { + Name = "EmptySelectionImage", + Image = "", + BackgroundTransparency = 1, + ImageTransparency = 1 +} + +local DialogPanel = Panel3D.Get("Dialog") +DialogPanel:SetType(Panel3D.Type.Fixed) +DialogPanel.localCF = PANEL_OFFSET_CF +DialogPanel:SetCanFade(true) + +local dialogPanelAngle = 0 +local opacityLookup = {} +local resetBaseAngle = false +local baseAngle = 0 + + +local PANEL_FADE_ANGLE_0, PANEL_FADE_ANGLE_1 = math.rad(37.5), math.rad(44) +local PANEL_FADE_RANGE = PANEL_FADE_ANGLE_1 - PANEL_FADE_ANGLE_0 +local PANEL_REAPPEAR_ANGLE = math.rad(90) + +local function positionDialogPanel(desiredAngle) + dialogPanelAngle = desiredAngle + local headCF = InputService:GetUserCFrame(Enum.UserCFrame.Head) + local headPos = headCF.p + DialogPanel.localCF = CFrame.new(headPos) * CFrame.Angles(0, desiredAngle, 0) * PANEL_OFFSET_CF +end + +function DialogPanel:CalculateTransparency() + local headCF = InputService:GetUserCFrame(Enum.UserCFrame.Head) + local headLook = headCF.lookVector * Vector3.new(1, 0, 1) + local vertAngle = math.asin(headCF.lookVector.Y) + local vectorToPanel = Vector3.new(math.cos(baseAngle + dialogPanelAngle + math.rad(90)), 0, -math.sin(baseAngle + dialogPanelAngle + math.rad(90))) + + if math.abs(vertAngle) > PANEL_FADE_ANGLE_1 then + resetBaseAngle = true + end + + local angleToPanel = math.acos(headLook:Dot(vectorToPanel)) + + return math.min(math.max(0, (angleToPanel - PANEL_FADE_ANGLE_0) / PANEL_FADE_RANGE), 1) +end + + +local function updatePanelPosition() + local headCF = InputService:GetUserCFrame(Enum.UserCFrame.Head) + local headLook = headCF.lookVector * Vector3.new(1, 0, 1) + local headAngle = (math.atan2(-headLook.Z, headLook.X) - math.rad(90)) % math.rad(360) + local newPanelAngle = baseAngle + math.floor((headAngle / PANEL_REAPPEAR_ANGLE) + 0.5) * PANEL_REAPPEAR_ANGLE + + if resetBaseAngle then + resetBaseAngle = false + baseAngle = headAngle + end + + positionDialogPanel(newPanelAngle) +end + +-- RenderStep update function for the DialogPanel +function dialogPanelRenderUpdate() + if DialogPanel.transparency == 1 or resetBaseAngle then + updatePanelPosition() + end + + --update the transparency of gui elements + local opacityMult = 1 - DialogPanel.transparency + for guiElement, baseOpacity in pairs(opacityLookup) do + local transparency = 1 - (baseOpacity * opacityMult) + if guiElement:IsA("TextLabel") or guiElement:IsA("TextButton") then + guiElement.TextTransparency = transparency + elseif guiElement:IsA("ImageLabel") or guiElement:IsA("ImageButton") then + guiElement.ImageTransparency = transparency + elseif guiElement:IsA("Frame") then + guiElement.BackgroundTransparency = transparency + end + end +end + +local DialogQueue = {} + +local currentDescendantConn = nil +local lastDialogShown = nil +local function updatePanel() + local currentDialog = DialogQueue[1] + if lastDialogShown and lastDialogShown.content then + lastDialogShown.content.Parent = nil + end + if not currentDialog or not currentDialog.content then + DialogPanel:SetVisible(false) + opacityLookup = {} + if currentDescendantConn then + currentDescendantConn:disconnect() + currentDescendantConn = nil + end + else + currentDialog.content.Parent = DialogPanel:GetGUI() + lastDialogShown = currentDialog + + local contentSize = currentDialog.content.AbsoluteSize + DialogPanel:ResizePixels(contentSize.X, contentSize.Y, 100) + + resetBaseAngle = true + updatePanelPosition() + DialogPanel:SetVisible(true) + + opacityLookup = {} + local function search(parent) + if parent:IsA("ImageLabel") or parent:IsA("ImageButton") then + opacityLookup[parent] = 1 - parent.ImageTransparency + elseif parent:IsA("TextLabel") or parent:IsA("TextButton") then + opacityLookup[parent] = 1 - parent.TextTransparency + elseif parent:IsA("Frame") then + opacityLookup[parent] = 1 - parent.BackgroundTransparency + end + for i, v in pairs(parent:GetChildren()) do + search(v) + end + end + search(DialogPanel:GetGUI()) + if currentDescendantConn then + currentDescendantConn:disconnect() + currentDescendantConn = nil + end + currentDescendantConn = DialogPanel:GetGUI().DescendantAdded:connect(function(descendant) + search(descendant) + end) + + + end +end + + + +local Dialog = {} +Dialog.__index = Dialog +function Dialog.new(content) + local self = setmetatable({}, Dialog) + self.content = content + return self +end + +function Dialog:SetContent(guiElement) + if not guiElement and self.content then + self.content.Parent = nil + end + self.content = guiElement +end + +local renderFuncBound = false +function Dialog:Show(shouldTakeover) + if not renderFuncBound then + renderFuncBound = true + RunService:BindToRenderStep("DialogPanel", Enum.RenderPriority.First.Value, dialogPanelRenderUpdate) + end + if shouldTakeover then + table.insert(DialogQueue, 1, self) + else + table.insert(DialogQueue, self) + end + updatePanel() +end + +function Dialog:Close() + for idx, dialog in pairs(DialogQueue) do + if dialog == self then + table.remove(DialogQueue, idx) + break + end + end + if renderFuncBound and #DialogQueue == 0 then + renderFuncBound = false + RunService:UnbindFromRenderStep("DialogPanel") + end + updatePanel() +end + +return Dialog \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/VR/Healthbar3D.lua b/Client2018/content/scripts/CoreScripts/Modules/VR/Healthbar3D.lua new file mode 100644 index 0000000..806c5e0 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/VR/Healthbar3D.lua @@ -0,0 +1,291 @@ + +-- Written by SolarCrane + +local HEALTH_BACKGROUND_COLOR = Color3.new(228/255, 236/255, 246/255) +local HEALTH_RED_COLOR = Color3.new(255/255, 28/255, 0/255) +local HEALTH_YELLOW_COLOR = Color3.new(250/255, 235/255, 0) +local HEALTH_GREEN_COLOR = Color3.new(27/255, 252/255, 107/255) + +local MIN_COLOR_POSITION = 0.1 +local MIN_COLOR = HEALTH_RED_COLOR +local MID_COLOR_POSITION = 0.5 +local MAX_COLOR_POSITION = 0.8 +local MAX_COLOR = HEALTH_GREEN_COLOR + +local NAME_SPACE = 14 + +local PlayersService = game:GetService('Players') +local CoreGui = game:GetService('CoreGui') +local StarterGui = game:GetService('StarterGui') +local UserInputService = game:GetService('UserInputService') + +local RobloxGui = CoreGui:WaitForChild("RobloxGui") + + + +local healthbarContainer = Instance.new('Frame') +healthbarContainer.Name = 'HealthbarContainer' +healthbarContainer.BackgroundTransparency = 1 +healthbarContainer.Size = UDim2.new(1,0,1,0) + +local healthbarBack = Instance.new('ImageLabel') +healthbarBack.ImageColor3 = HEALTH_BACKGROUND_COLOR +healthbarBack.BackgroundTransparency = 1 +healthbarBack.ScaleType = Enum.ScaleType.Slice +healthbarBack.SliceCenter = Rect.new(10, 10, 10, 10) +healthbarBack.Name = 'HealthbarBack' +healthbarBack.Image = 'rbxasset://textures/ui/VR/rectBackgroundWhite.png' +healthbarBack.Size = UDim2.new(1,0,0.3,0) +healthbarBack.Position = UDim2.new(0,0,0.7,0) +healthbarBack.Parent = healthbarContainer + +local healthbarFront = Instance.new('ImageLabel') +healthbarFront.ImageColor3 = HEALTH_GREEN_COLOR +healthbarFront.BackgroundTransparency = 1 +healthbarFront.ScaleType = Enum.ScaleType.Slice +healthbarFront.SliceCenter = Rect.new(10, 10, 10, 10) +healthbarFront.Size = UDim2.new(1, 0, 1, 0) +healthbarFront.Position = UDim2.new(0, 0, 0, 0) +healthbarFront.Name = 'HealthbarFill' +healthbarFront.Image = 'rbxasset://textures/ui/VR/rectBackgroundWhite.png' +healthbarFront.Parent = healthbarBack + +local playerName = Instance.new('TextLabel') +playerName.Name = 'PlayerName' +playerName.BackgroundTransparency = 1 +playerName.TextColor3 = Color3.new(1, 1, 1) +playerName.Text = '' +playerName.Font = Enum.Font.SourceSansBold +playerName.FontSize = Enum.FontSize.Size24 +playerName.TextXAlignment = Enum.TextXAlignment.Left +playerName.Size = UDim2.new(1, 0, 0, NAME_SPACE) +playerName.Parent = healthbarContainer + + +local function Color3ToVec3(color) + return Vector3.new(color.r, color.g, color.b) +end + +local function FindChildOfType(object, className) + for _, child in pairs(object:GetChildren()) do + if child:IsA(className) then + return child + end + end +end + + +local HEALTH_COLOR_TO_POSITION = { + [Color3ToVec3(MIN_COLOR)] = MIN_COLOR_POSITION; + [Color3ToVec3(HEALTH_YELLOW_COLOR)] = MID_COLOR_POSITION; + [Color3ToVec3(MAX_COLOR)] = MAX_COLOR_POSITION; +} + + +local function HealthbarColorTransferFunction(healthPercent) + if healthPercent <= MIN_COLOR_POSITION then + return MIN_COLOR + elseif healthPercent >= MAX_COLOR_POSITION then + return MAX_COLOR + end + + -- Shepard's Interpolation + local numeratorSum = Vector3.new(0,0,0) + local denominatorSum = 0 + for colorSampleValue, samplePoint in pairs(HEALTH_COLOR_TO_POSITION) do + local distance = healthPercent - samplePoint + if distance == 0 then + -- If we are exactly on an existing sample value then we don't need to interpolate + return Color3.new(colorSampleValue.x, colorSampleValue.y, colorSampleValue.z) + else + local wi = 1 / (distance*distance) + numeratorSum = numeratorSum + wi * colorSampleValue + denominatorSum = denominatorSum + wi + end + end + + local result = numeratorSum / denominatorSum + return Color3.new(result.x, result.y, result.z) +end + +local function UpdateHealth(humanoid) + local percentHealth = humanoid.Health / humanoid.MaxHealth + if percentHealth ~= percentHealth then + percentHealth = 1 + end + healthbarFront.ImageColor3 = HealthbarColorTransferFunction(percentHealth) + healthbarFront.Size = UDim2.new(percentHealth, 0, 1, 0) +end + +local HumanoidChangedConn = nil +local HumanoidAncestryChangedConn = nil +local function RegisterHumanoid(humanoid) + if HumanoidAncestryChangedConn then + HumanoidAncestryChangedConn:disconnect() + HumanoidAncestryChangedConn = nil + end + if HumanoidChangedConn then + HumanoidChangedConn:disconnect() + HumanoidChangedConn = nil + end + + if humanoid then + HumanoidAncestryChangedConn = humanoid.AncestryChanged:connect(function(child, parent) + local player = PlayersService.LocalPlayer + if child == humanoid and (not player or parent ~= player.Character) then + RegisterHumanoid(nil) + end + end) + HumanoidChangedConn = humanoid.HealthChanged:connect(function() UpdateHealth(humanoid) end) + UpdateHealth(humanoid) + end +end + +local function OnCharacterChildAdded(child) + local player = PlayersService.LocalPlayer + if player and child.Parent == player.Character and child:IsA('Humanoid') then + RegisterHumanoid(child) + end +end + +local CharacterChildAddedConn = nil +local function OnCharacterAdded(character) + local humanoid = FindChildOfType(character, 'Humanoid') + if humanoid then + RegisterHumanoid(humanoid) + end + + if CharacterChildAddedConn then + CharacterChildAddedConn:disconnect() + CharacterChildAddedConn = nil + end + CharacterChildAddedConn = character.ChildAdded:connect(OnCharacterChildAdded) +end + +local function OnPlayerAdded(player) + playerName.Text = player.Name + + player.CharacterAdded:connect(OnCharacterAdded) + if player.Character then + OnCharacterAdded(player.Character) + end +end + +if PlayersService.LocalPlayer then + OnPlayerAdded(PlayersService.LocalPlayer) +else + spawn(function() + while not PlayersService.LocalPlayer do + PlayersService.ChildAdded:wait() + end + OnPlayerAdded(PlayersService.LocalPlayer) + end) +end + +local Healthbar = {} + +Healthbar.ModuleName = "Healthbar" +Healthbar.KeepVRTopbarOpen = false +Healthbar.VRIsExclusive = false +Healthbar.VRClosesNonExclusive = false + +local CoreGuiChangedConn, VRModuleOpenedConn, VRModuleClosedConn; +local function OnVREnabled(prop) + if prop == "VREnabled" then + if UserInputService.VREnabled then + local VRHub = require(RobloxGui.Modules.VR.VRHub) + local Panel3D = require(RobloxGui.Modules.VR.Panel3D) + + local HealthbarPanel = Panel3D.Get("Healthbar") + HealthbarPanel:ResizeStuds(1.5, 0.25, 128) + HealthbarPanel:SetType(Panel3D.Type.Fixed, { CFrame = CFrame.new(0, 0, -5) }) + HealthbarPanel:SetVisible(true) + + + function HealthbarPanel:PreUpdate(cameraCF, cameraRenderCF, userHeadCF, lookRay) + local relativePanel = Panel3D.Get("Backpack") or Panel3D.Get("Topbar3D") + local topbarPanel = Panel3D.Get("Topbar3D") + if relativePanel and topbarPanel then + local panelOriginCF = relativePanel.localCF or CFrame.new() + -- Line up the Healthbar with the backpack icons, which are set 0.11 inwards + self.localCF = panelOriginCF * CFrame.new(math.max(topbarPanel.width, relativePanel.width)/2 - HealthbarPanel.width/2 - 0.11, 0.25, 0.1) + end + end + + function HealthbarPanel:CalculateTransparency() + local backpackPanel = Panel3D.Get("Backpack") + local topbarPanel = Panel3D.Get("Topbar3D") + local transparency = math.min( + backpackPanel and backpackPanel:IsVisible() and backpackPanel:CalculateTransparency() or 1, + topbarPanel and topbarPanel:IsVisible() and topbarPanel:CalculateTransparency() or 1) + + healthbarBack.ImageTransparency = transparency + healthbarFront.ImageTransparency = transparency + playerName.TextTransparency = transparency + + return transparency + end + + local OtherPanelOpen = false + local function UpdateExclusivePanelOpen() + for _, openModule in pairs(VRHub:GetOpenedModules()) do + if openModule.VRIsExclusive then + OtherPanelOpen = true + return + end + end + OtherPanelOpen = false + end + + local function CalculateVisibility() + return StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Health) and + not OtherPanelOpen + end + + + CoreGuiChangedConn = StarterGui.CoreGuiChangedSignal:connect(function() + HealthbarPanel:SetVisible(CalculateVisibility()) + end) + + VRModuleOpenedConn = VRHub.ModuleOpened.Event:connect(function(moduleName) + UpdateExclusivePanelOpen() + HealthbarPanel:SetVisible(CalculateVisibility()) + end) + VRModuleClosedConn = VRHub.ModuleClosed.Event:connect(function(moduleName) + UpdateExclusivePanelOpen() + HealthbarPanel:SetVisible(CalculateVisibility()) + end) + + -- Initialize OtherPanelOpen variable + UpdateExclusivePanelOpen() + HealthbarPanel:SetVisible(CalculateVisibility()) + healthbarContainer.Parent = HealthbarPanel:GetGUI() + + VRHub:RegisterModule(Healthbar) + else + if CoreGuiChangedConn then + CoreGuiChangedConn:disconnect() + CoreGuiChangedConn = nil + end + if VRModuleOpenedConn then + VRModuleOpenedConn:disconnect() + VRModuleOpenedConn = nil + end + if VRModuleClosedConn then + VRModuleClosedConn:disconnect() + VRModuleClosedConn = nil + end + healthbarContainer.Parent = nil + end + end +end + + + + +UserInputService.Changed:connect(OnVREnabled) +if UserInputService.VREnabled then + OnVREnabled("VREnabled") +end + +return Healthbar diff --git a/Client2018/content/scripts/CoreScripts/Modules/VR/LaserPointer.lua b/Client2018/content/scripts/CoreScripts/Modules/VR/LaserPointer.lua new file mode 100644 index 0000000..e2c892f --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/VR/LaserPointer.lua @@ -0,0 +1,668 @@ +--LaserPointer.lua +--Implements the visual part of the VR laser pointer +--Written by Kyle, September 2016 +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local ContextActionService = game:GetService("ContextActionService") +local UserInputService = game:GetService("UserInputService") +local Players = game:GetService("Players") +local GuiService = game:GetService("GuiService") +local VRService = game:GetService("VRService") +local Utility = require(RobloxGui.Modules.Settings.Utility) + +local LocalPlayer = Players.LocalPlayer +while not LocalPlayer do + Players.Changed:wait() + LocalPlayer = Players.LocalPlayer +end + +local gamma, invGamma = 2.2, 1/2.2 +local function fromLinearRGB(color) + return Color3.new(color.r ^ gamma, color.g ^ gamma, color.b ^ gamma) +end +local function toLinearRGB(color) + return Color3.new(color.r ^ invGamma, color.g ^ invGamma, color.b ^ invGamma) +end + +local function addPartsToGame(...) + local parts = {...} + local container = GuiService.CoreEffectFolder + + --The container might not be ready yet. + if not container then + coroutine.wrap(function() + --Wait until the container is ready, then add the parts to it. + while GuiService.Changed:wait() ~= "CoreEffectFolder" and GuiService.CoreEffectFolder == nil do end + for _, part in pairs(parts) do + part.Parent = container + end + end)() + else + --The container is ready, no waiting necessary. + for _, part in pairs(parts) do + part.Parent = container + end + end +end +local function removePartsFromGame(...) + local parts = {...} + for _, part in pairs(parts) do + part.Parent = nil + end +end + +local function getLocalHumanoid() + local character = LocalPlayer.Character + if not character then + return + end + + for _, child in pairs(character:GetChildren()) do + if child:IsA("Humanoid") then + return child + end + end +end + +local function applyExpCurve(x, exp) + local y = x ^ exp + if y ~= y then + y = math.abs(x) ^ exp + end + return y +end + +local HEAD_MOUNT_OFFSET = Vector3.new(0.5, 0.5, 0) +local HEAD_MOUNT_THICKNESS_MULTIPLIER = 0.25 + +local BUTTON_LONG_PRESS_TIME = 0.75 + +local LaserPointerMode = { + Disabled = 0, + Pointer = 1, + Navigation = 2, + Hidden = 3 +} + +--Teleport visual configuration +local TELEPORT = { + MODE_ENABLED = true, + + ARC_COLOR_GOOD = fromLinearRGB(Color3.fromRGB(0, 162, 255)), + ARC_COLOR_BAD = fromLinearRGB(Color3.fromRGB(253, 68, 72)), + ARC_THICKNESS = 0.025, + + PLOP_GOOD = "rbxasset://textures/ui/VR/VRPointerDiscBlue.png", + PLOP_BAD = "rbxasset://textures/ui/VR/VRPointerDiscRed.png", + PLOP_BALL_COLOR_GOOD = BrickColor.new("Bright green"), + PLOP_BALL_COLOR_BAD = BrickColor.new("Bright red"), + PLOP_BALL_SIZE = 0.5, + PLOP_SIZE = 2, + PLOP_PULSE_MIN_SIZE = 0, + PLOP_PULSE_MAX_SIZE = 2, + + MAX_VALID_DISTANCE = 100, + + BUTTON_DOWN_THRESHOLD = 0.95, + BUTTON_UP_THRESHOLD = 0.5, + + MIN_VELOCITY = 10, + RANGE_T_EXP = 2, + G = 10, -- Gravity constant for parabola + + PULSE_DURATION = 0.8, + PULSE_PERIOD = 1, + PULSE_EXP = 2, + PULSE_SIZE_0 = 0.25, + PULSE_SIZE_1 = 2, + + BALL_WAVE_PERIOD = 2, + BALL_WAVE_AMPLITUDE = 0.5, + BALL_WAVE_START = 0.25, + BALL_WAVE_EXP = 0.8, + + FLOOR_OFFSET = 4.5, + + FADE_OUT_DURATION = 0.125, + FADE_IN_DURATION = 0.125, + + TRANSITION_DURATION = 0.25, + TRANSITION_FUNC = Utility:GetEaseInOutQuad(), +} + +local LASER = { + MODE_ENABLED = true, + + ARC_COLOR_GOOD = TELEPORT.ARC_COLOR_GOOD, + ARC_COLOR_BAD = TELEPORT.ARC_COLOR_BAD, + ARC_THICKNESS = 0.02, + + MAX_DISTANCE = 500, + + G = 0, -- Gravity constant for parabola; in this case we want a laser/straight line + + --Couldn't figure out a good name for this. This is the maximum angle that the parabola's hit point + --can be from the laser's hit point when switching to laser pointer mode solely from the parabola hitting + --a gui part. + SWITCH_AIM_THRESHOLD = math.rad(15), + + TRANSITION_DURATION = 0.075, + TRANSITION_FUNC = Utility:GetEaseInOutQuad() +} + +local zeroVector2, identityVector2 = Vector2.new(0, 0), Vector2.new(1, 1) +local zeroVector3, identityVector3 = Vector3.new(0, 0, 0), Vector3.new(1, 1, 1) +local flattenMask = Vector3.new(1, 0, 1) --flattens a direction vector when multiplied by removing the vertical component +local minimumPartSize = Vector3.new(0.2, 0.2, 0.2) +local identity = CFrame.new() + +local aimableStates = { + [Enum.HumanoidStateType.Running] = true, + [Enum.HumanoidStateType.RunningNoPhysics] = true, + [Enum.HumanoidStateType.None] = true +} + +local LaserPointer = {} +LaserPointer.__index = LaserPointer +LaserPointer.Mode = LaserPointerMode + +function LaserPointer.new() + local self = setmetatable({}, LaserPointer) + + self.mode = LaserPointerMode.Disabled + self.lastMode = self.mode + + self.inputUserCFrame = Enum.UserCFrame.RightHand + self.equippedTool = false + + self.lastLaserModeHit = tick() + + self.guiMenuIsOpen = false + self.externalForcePointer = false + + self.navHitPoint = zeroVector3 + self.navHitNormal = Vector3.new(0, 1, 0) + self.navHitPart = nil + + self.navigationIsValid = false + self.lastNavigationValidityChangeTime = tick() + self.plopBallBounceStart = tick() + + self.buttonPressStart = 0 + + do --Create the instances that make up the Laser Pointer + self.parabola = Utility:Create("ParabolaAdornment") { + Name = "LaserPointerParabola", + Parent = CoreGui, + A = -1, + B = 2, + C = 0, + Color3 = TELEPORT.COLOR_GOOD, + Thickness = TELEPORT.ARC_THICKNESS, + Visible = false + } + + self.originPart = Utility:Create("Part") { + Name = "LaserPointerOrigin", + Anchored = true, + CanCollide = false, + TopSurface = Enum.SurfaceType.SmoothNoOutlines, + BottomSurface = Enum.SurfaceType.SmoothNoOutlines, + Material = Enum.Material.SmoothPlastic, + Size = minimumPartSize, + Transparency = 1 + } + self.parabola.Adornee = self.originPart + + self.plopPart = Utility:Create("Part") { + Name = "LaserPointerTeleportPlop", + Anchored = true, + CanCollide = false, + Size = minimumPartSize, + Transparency = 1 + } + self.plopBall = Utility:Create("Part") { + Name = "LaserPointerTeleportPlopBall", + Anchored = true, + CanCollide = false, + TopSurface = Enum.SurfaceType.SmoothNoOutlines, + BottomSurface = Enum.SurfaceType.SmoothNoOutlines, + Material = Enum.Material.Neon, + BrickColor = TELEPORT.PLOP_BALL_COLOR_GOOD, + Shape = Enum.PartType.Ball, + Size = identityVector3 * TELEPORT.PLOP_BALL_SIZE + } + self.plopAdorn = Utility:Create("ImageHandleAdornment") { + Name = "LaserPointerTeleportPlopAdorn", + Parent = self.plopPart, + Adornee = self.plopPart, + Size = identityVector2 * TELEPORT.PLOP_SIZE, + Image = TELEPORT.PLOP_GOOD + } + self.plopAdornPulse = Utility:Create("ImageHandleAdornment") { + Name = "LaserPointerTeleportPlopAdornPulse", + Parent = self.plopPart, + Adornee = self.plopPart, + Size = zeroVector2, + Image = TELEPORT.PLOP_GOOD, + Transparency = 0.5 + } + end + + do --Event connections and final setup + GuiService.MenuOpened:connect(function() + self.guiMenuIsOpen = true + end) + GuiService.MenuClosed:connect(function() + self.guiMenuIsOpen = false + end) + + self.inputUserCFrame = VRService.GuiInputUserCFrame + VRService.Changed:connect(function(prop) + if prop == "GuiInputUserCFrame" then + self.inputUserCFrame = VRService.GuiInputUserCFrame + end + end) + end + + self:onModeChanged(self.mode) + + self:updateInputUserCFrame() + + return self +end + +function LaserPointer.hasAnyHandController() + return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) or + VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand) +end + +function LaserPointer.getModeName(mode) + for name, value in pairs(LaserPointerMode) do + if mode == value then + return name + end + end + + return "unknown" +end + +function LaserPointer:updateInputUserCFrame() + if VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) then + VRService.GuiInputUserCFrame = Enum.UserCFrame.RightHand + elseif VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand) then + VRService.GuiInputUserCFrame = Enum.UserCFrame.LeftHand + else + VRService.GuiInputUserCFrame = Enum.UserCFrame.Head + end +end + +function LaserPointer:onModeChanged(newMode) + self:updateInputUserCFrame() + + --Disabled mode + if newMode == LaserPointerMode.Disabled or newMode == LaserPointerMode.Hidden then + removePartsFromGame(self.originPart, self.plopPart, self.plopBall) + self.parabola.Visible = false + self:setNavigationActionEnabled(false) + --Pointer mode + elseif newMode == LaserPointerMode.Pointer then + addPartsToGame(self.originPart) + removePartsFromGame(self.plopPart, self.plopBall) + self.parabola.Visible = true + self:setNavigationActionEnabled(false) + --Navigation mode + elseif newMode == LaserPointerMode.Navigation then + addPartsToGame(self.originPart, self.plopPart, self.plopBall) + self.parabola.Visible = true + self:setNavigationActionEnabled(true) + end +end + +function LaserPointer:setMode(mode) + if mode == self.mode then + return + end + + local oldMode = self.mode + self.mode = mode + self.lastMode = oldMode + self:onModeChanged(mode) +end + +function LaserPointer:getMode() + return self.mode +end + +local cosMax = math.cos(math.pi/4) +local sinMax = math.sin(math.pi/4) +function LaserPointer:calculateLaunchVelocity(gravity, desiredRange, height) + --return math.sqrt(desiredRange * gravity) / math.sqrt(math.sin(2*math.pi/4)) + --This calculates a launch velocity for an imaginary projectile that will start at y=height, travel desiredRange + --in the x direction until it hits y=0. This is only reached when the projectile is launched at the optimal + --launch angle, 45 degrees elevation. Anything above or below that will fall short, which is desired. + --See https://en.wikipedia.org/wiki/Range_of_a_projectile for a breakdown of this function. + return -(math.sqrt(gravity / cosMax)*desiredRange) / (math.sqrt(2*height*cosMax+2*desiredRange*sinMax)) +end + +function LaserPointer:isHeadMounted() + return self.inputUserCFrame == Enum.UserCFrame.Head +end + +function LaserPointer:shouldForcePointer() + return self.externalForcePointer or self.guiMenuIsOpen +end + +function LaserPointer:setForcePointer(force) + self.externalForcePointer = force +end + +function LaserPointer:getNavigationOrigin() + local humanoid = getLocalHumanoid() + if not humanoid then return end + + local rootPart = humanoid.Torso + if not rootPart then return end + + local hipHeight = humanoid.HipHeight + if humanoid.RigType == Enum.HumanoidRigType.R6 then + hipHeight = 2 + end + local rootPartOffset = rootPart.Size.Y / 2 + return rootPart.Position + Vector3.new(0, -rootPartOffset - hipHeight, 0) +end + +function LaserPointer:horzDistanceFromCharacter(point) + local character = LocalPlayer.Character + if not character then + return math.huge + end + local rootPart = character:FindFirstChild("HumanoidRootPart") + if not rootPart then + return math.huge + end + + return ((rootPart:GetRenderCFrame().p - point) * flattenMask).magnitude +end + + +function LaserPointer:onNavigateAction(actionName, inputState, inputObj) + if inputState == Enum.UserInputState.Begin then + VRService:RequestNavigation(CFrame.new(self.navHitPoint, self.navHitPoint + self.navHitNormal) * CFrame.Angles(math.pi/2, 0, 0), self.inputUserCFrame) + end +end + +function LaserPointer:setNavigationActionEnabled(enabled) + if enabled then + ContextActionService:BindCoreAction("LaserPointerNavigate", function(...) self:onNavigateAction(...) end, false, Enum.KeyCode.ButtonA) + else + ContextActionService:UnbindCoreAction("LaserPointerNavigate") + end +end + +function LaserPointer:setArcLaunchParams(launchAngle, launchVelocity, gravity, desiredRange) + local velocityX = math.cos(launchAngle) * launchVelocity + local velocityY = math.sin(launchAngle) * launchVelocity + + --don't let velocityX = 0 or we get a divide-by-zero and bad things happen + if velocityX == 0 then + velocityX = 1e-6 + end + + self.parabola.A = -(0.5 * gravity) * (1 / (velocityX ^ 2)) + self.parabola.B = velocityY / velocityX + self.parabola.C = 0 + self.parabola.Range = desiredRange * 1.5 +end + +function LaserPointer:renderAsParabola(origin, lookDir) + local lookFlat = lookDir * flattenMask + self.originPart.CFrame = CFrame.new(origin, origin + lookFlat) * CFrame.Angles(0, math.pi / 2, 0) +end + +function LaserPointer:renderAsLaser(laserOriginPos, laserEndpoint) + self.originPart.CFrame = CFrame.new(laserOriginPos, laserEndpoint) * CFrame.Angles(0, math.pi / 2, 0) + self.parabola.A = 0 + self.parabola.B = 1e-6 + self.parabola.C = 0 + self.parabola.Range = (laserEndpoint - laserOriginPos).magnitude +end + +function LaserPointer:getArcHit(pos, look, ignore) + if self.parabola.A == 0 then + --Just skip the parabola since this is effectively a line without the x^2 term + return self:getLaserHit(pos, look, ignore) + end + + self:renderAsParabola(pos, look) + + local parabHitPart, parabHitPoint, parabHitNormal, _, t = self.parabola:FindPartOnParabola(ignore) + return parabHitPart, parabHitPoint, parabHitNormal, t +end + +function LaserPointer:getLaserHit(pos, look, ignore) + local ray = Ray.new(pos, look * LASER.MAX_DISTANCE) + local laserHitPart, laserHitPoint, laserHitNormal, laserHitMaterial = workspace:FindPartOnRayWithIgnoreList(ray, ignore) + local t = (laserHitPoint - pos).magnitude / LASER.MAX_DISTANCE + return laserHitPart, laserHitPoint, laserHitNormal, t +end + +function LaserPointer:canNavigateTo(part, point, normal) + local character = LocalPlayer.Character + if not character then + return false + end + local humanoidRootPart = character:FindFirstChild("HumanoidRootPart") + if not humanoidRootPart then + return false + end + + --Check if a part was hit + if not part then + return false + end + + --Check if the surface hit is upside down or not + if normal.Y < -1e-6 then + return false + end + + return true +end + +function LaserPointer:checkHeadMountedMode(laserHitPart) + if self:shouldForcePointer() then + self:setMode(LaserPointerMode.Pointer) + return + end + + local coreGuiPartContainer = GuiService.CoreGuiFolder + if laserHitPart and laserHitPart:IsDescendantOf(coreGuiPartContainer) then + self:setMode(LaserPointerMode.Pointer) + else + self:setMode(LaserPointerMode.Navigation) + end +end + +function LaserPointer:checkMode(originPos, parabHitPart, parabHitPoint, laserHitPart, laserHitPoint) + if self:shouldForcePointer() then + self:setMode(LaserPointerMode.Pointer) + return + end + + local angleBetween = 0 + + --only check the angle between these two if the hit points aren't exactly equal + if parabHitPoint ~= laserHitPoint then + local toParabHit = (parabHitPoint - originPos).unit + local toLaserHit = (laserHitPoint - originPos).unit + angleBetween = math.acos(toParabHit:Dot(toLaserHit)) + end + + --todo: update this when we move the parts; also update it so that it can work with user surfaceguis + --we may need to be more creative about that since we can't easily tell if a part has a surfacegui from Lua + local coreGuiPartContainer = GuiService.CoreGuiFolder + local laserHitGui = laserHitPart and laserHitPart:IsDescendantOf(coreGuiPartContainer) + local parabHitGui = laserHitGui + --only check parab hit part if it's not the same as the laser hit part + if parabHitPart ~= laserHitPart then + parabHitGui = parabHitPart and parabHitPart:IsDescendantOf(coreGuiPartContainer) + end + + local newMode = self.mode + + if laserHitGui then + self.lastLaserModeHit = tick() + end + + --If we are navigating and the parabola hits a gui part, we switch to laser pointer if the laser pointer is close enough + --If we are navigating and the laser hits a gui part, we switch to laser pointer regardless of where the parabola is + if self.mode ~= LaserPointerMode.Pointer and laserHitGui then + if self:isHeadMounted() or ((parabHitGui and angleBetween < LASER.SWITCH_AIM_THRESHOLD) or laserHitGui) then + newMode = LaserPointerMode.Pointer + end + end + + --If we are in laser pointer mode but neither the parabola nor the laser hit any gui parts, we switch back to navigation or hidden mode. + if self.mode == LaserPointerMode.Pointer and not laserHitGui and not parabHitGui and tick() - self.lastLaserModeHit > 0.2 then + if self.lastMode == LaserPointerMode.Navigation or self.lastMode == LaserPointerMode.Hidden then + newMode = self.lastMode + end + end + + self:setMode(newMode) +end + +function LaserPointer:setNavigationValidState(valid) + if valid == self.navigationIsValid then return end + self.navigationIsValid = valid + self.lastNavigationValidityChangeTime = tick() + + if valid then + self.plopBallBounceStart = tick() + self.parabola.Color3 = TELEPORT.ARC_COLOR_GOOD + self.plopAdorn.Visible = true + self.plopAdorn.Image = TELEPORT.PLOP_GOOD + self.plopAdornPulse.Visible = true + self.plopAdornPulse.Image = TELEPORT.PLOP_GOOD + self.plopBall.BrickColor = TELEPORT.PLOP_BALL_COLOR_GOOD + else + self.parabola.Color3 = TELEPORT.ARC_COLOR_BAD + self.plopAdorn.Visible = false + self.plopAdorn.Image = TELEPORT.PLOP_BAD + self.plopAdornPulse.Visible = false + self.plopAdornPulse.Image = TELEPORT.PLOP_BAD + self.plopBall.BrickColor = TELEPORT.PLOP_BALL_COLOR_BAD + end +end + +function LaserPointer:updateNavPlop(parabHitPoint, parabHitNormal) + local now = tick() - self.plopBallBounceStart + + local plopCF = CFrame.new(parabHitPoint, parabHitPoint + parabHitNormal) + + local ballHeight = 0 + if self.navigationIsValid then + local ballWave = applyExpCurve(math.sin((now * 2 * math.pi) / TELEPORT.BALL_WAVE_PERIOD), TELEPORT.BALL_WAVE_EXP) + ballHeight = TELEPORT.BALL_WAVE_START + (ballWave * TELEPORT.BALL_WAVE_AMPLITUDE) + end + + self.plopPart.CFrame = plopCF + self.plopBall.CFrame = plopCF * CFrame.new(0, 0, -ballHeight) + + --Handle the pulse animation + --We're scheduling it to begin every TELEPORT.PULSE_PERIOD seconds + --and the animation runs for TELEPORT.PULSE_DURATION seconds. TELEPORT.PULSE_EXP + --affects the growth rate of the pulse size; ^2 is a good look, starts slow and accelerates. + local timeSincePulseStart = now % TELEPORT.PULSE_PERIOD + if timeSincePulseStart > 0 then + local pulseSize = timeSincePulseStart / TELEPORT.PULSE_DURATION + if pulseSize < 1 then + self.plopAdornPulse.Visible = true + self.plopAdornPulse.Size = identityVector2 * (TELEPORT.PULSE_SIZE_0 + applyExpCurve(pulseSize, TELEPORT.PULSE_EXP) * (TELEPORT.PULSE_SIZE_1 - TELEPORT.PULSE_SIZE_0)) + self.plopAdornPulse.Transparency = 0.5 + (pulseSize * 0.5) + else + self.plopAdornPulse.Visible = false + self.plopAdornPulse.Size = zeroVector2 + self.pulseStartTime = tick() + TELEPORT.PULSE_PERIOD + end + end +end + +function LaserPointer:updateNavigationMode(hitPoint, hitNormal, hitPart) + self.navHitPoint = hitPoint + self.navHitNormal = hitNormal + self.navHitPart = hitPart + + self:updateNavPlop(hitPoint, hitNormal) + self:setNavigationValidState(self:canNavigateTo(self.navHitPart, self.navHitPoint, self.navHitNormal)) +end + +function LaserPointer:update(dt) + if self.mode == LaserPointerMode.Disabled then + return + end + + local humanoid = getLocalHumanoid() + + local ignore = { LocalPlayer.Character or LocalPlayer, self.originPart, self.plopPart, self.plopBall, GuiService.CoreEffectFolder } + local cameraSpace = workspace.CurrentCamera.CFrame + local thickness0, thickness1 = LASER.ARC_THICKNESS, TELEPORT.ARC_THICKNESS + local gravity0, gravity1 = LASER.G, TELEPORT.G + + if self:isHeadMounted() then + self.parabola.Thickness = LASER.ARC_THICKNESS * HEAD_MOUNT_THICKNESS_MULTIPLIER + + --cast ray from center of camera, then render laser going from offset point to hit point + local originCFrame = cameraSpace * VRService:GetUserCFrame(Enum.UserCFrame.Head) + local originPos, originLook = originCFrame.p, originCFrame.lookVector + + local laserHitPart, laserHitPoint, laserHitNormal, laserHitT = self:getLaserHit(originPos, originLook, ignore) + self:checkHeadMountedMode(laserHitPart) + + --we actually want to render the laser from an offset from the head though + local offsetPosition = originCFrame:pointToWorldSpace(HEAD_MOUNT_OFFSET * workspace.CurrentCamera.HeadScale) + self:renderAsLaser(offsetPosition, laserHitPoint) + + if self.mode == LaserPointerMode.Navigation then + self:updateNavigationMode(laserHitPoint, laserHitNormal, laserHitPart, laserHitPoint) + else + self.parabola.Color3 = LASER.ARC_COLOR_GOOD + end + else + local originCFrame = cameraSpace * VRService:GetUserCFrame(self.inputUserCFrame) + local originPos, originLook = originCFrame.p, originCFrame.lookVector + + local gravity = TELEPORT.G + + local launchAngle = math.asin(originLook.Y) + local offsetHeight = humanoid and originPos.Y - self:getNavigationOrigin().Y or 0 + + local desiredRange = TELEPORT.MAX_VALID_DISTANCE + local launchVelocity = self:calculateLaunchVelocity(gravity, desiredRange, offsetHeight) + + self:setArcLaunchParams(launchAngle, launchVelocity, gravity, desiredRange) + + --Always check for both parabola and laser hits so we can use it to judge when to transition + ignore[6] = GuiService.CoreGuiFolder + local parabHitPart, parabHitPoint, parabHitNormal, parabHitT = self:getArcHit(originPos, originLook, ignore) + + --Clear the gui folder out of our ignore table and cast so we might hit SurfaceGuis with the laser + ignore[6] = nil + local laserHitPart, laserHitPoint, laserHitNormal, laserHitT = self:getLaserHit(originPos, originLook, ignore) + + self:checkMode(originPos, parabHitPart, parabHitPoint, laserHitPart, laserHitPoint) + + if self.mode == LaserPointerMode.Navigation then + self.parabola.Range = self.parabola.Range * parabHitT + self.parabola.Thickness = TELEPORT.ARC_THICKNESS + self:updateNavigationMode(parabHitPoint, parabHitNormal, parabHitPart) + else + self.parabola.Color3 = LASER.ARC_COLOR_GOOD + self.parabola.Thickness = LASER.ARC_THICKNESS + self:renderAsLaser(originPos, laserHitPoint) + end + end +end + +return LaserPointer \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/VR/NotificationHub.lua b/Client2018/content/scripts/CoreScripts/Modules/VR/NotificationHub.lua new file mode 100644 index 0000000..1a25778 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/VR/NotificationHub.lua @@ -0,0 +1,789 @@ +local ContextActionService = game:GetService("ContextActionService") +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local Util = require(RobloxGui.Modules.Settings.Utility) +local Panel3D = require(RobloxGui.Modules.VR.Panel3D) +local VRHub = require(RobloxGui.Modules.VR.VRHub) + +local PANEL_OFFSET_CFRAME = CFrame.Angles(math.rad(-5), 0, 0) * CFrame.new(0, 4, 0) * CFrame.Angles(math.rad(-15), 0, 0) + +local NO_TRANSITION_ANIMATIONS = false +local ANIMATE_OUT_DISTANCE = -100 +local ANIMATE_OUT_DURATION = 0.25 +local PIXELS_PER_STUD = 150 +local WINDOW_TITLEBAR_HEIGHT = 72 +local BLURRED_TITLEBAR_COLOR = Color3.new(78 / 255, 84 / 255, 96 / 255) +local FOCUSED_TITLEBAR_COLOR = Color3.new(82 / 255, 101 / 255, 141 / 255) +local WINDOW_BG_COLOR = Color3.new(20/255, 20/255, 20/255) +local WINDOW_BG_TRANSPARENCY = 0.5 +local POPOUT_DISTANCE = 0.25 +local POPOUT_DURATION = 0.25 + +local NOTIFICATION_WIDTH_SCALE = 0.85 +local NOTIFICATION_HEIGHT_OFFSET = 80 +local NOTIFICATION_PADDING_Y = 20 +local NOTIFICATION_PADDING_X_SCALE = (1 - NOTIFICATION_WIDTH_SCALE) / 2 +local NOTIFICATION_DEPTH_OFFSET = 0.25 +local NOTIFICATION_BG_COLOR = Color3.new(0.2, 0.2, 0.2) +local NOTIFICATION_BG_TRANSPARENCY = 0.1 +local MAX_NOTIFICATIONS_SHOWN = 3 +local MAX_DETAILS_SHOWN = 2 +local DETAILS_PADDING = 20 + +local BUTTON_1_POS = 0.07 +local BUTTON_2_POS = 0.511 +local BUTTON_SINGLE_SIZE = 0.86 +local BUTTON_DOUBLE_SIZE = 0.415 + +local BUTTON_Y_POS = 0.55 +local BUTTON_Y_SIZE = 0.29 + +local BUTTON_NORMAL_IMG = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButton.png" +local BUTTON_SELECTED_IMG = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png" + +local CLOSE_BUTTON_IMG = "rbxasset://textures/ui/Keyboard/close_button_icon.png" +local CLOSE_BUTTON_HOVER = "rbxasset://textures/ui/Keyboard/close_button_selection.png" +local CLOSE_BUTTON_SIZE = 32 +local CLOSE_BUTTON_OFFSET = 22 +local CLOSE_BUTTON_HOVER_OFFSET = 22 + +local emptySelectionImage = Util:Create "ImageLabel" { + BackgroundTransparency = 1, + Image = "" +} + + +local AVATAR_IMAGE_URL = 'http://www.roblox.com/thumbs/avatar.ashx?userId=%d&x=%d&y=%d' + +local TEXT_COLOR = Color3.new(1, 1, 1) + +local aspectRatio = 1.62666666 +local totalHeight = 3.5 +local totalWidth = totalHeight * aspectRatio +local leftPanelWidth = totalWidth * 0.4 +local rightPanelWidth = totalWidth * 0.6 + +local panelOffset = -totalWidth / 2 +local leftOffset = (panelOffset + (leftPanelWidth * 0.5)) +local rightOffset = (leftOffset + (leftPanelWidth * 0.5) + (rightPanelWidth * 0.5)) + +local NotificationHubModule = {} +NotificationHubModule.ModuleName = "Notifications" +NotificationHubModule.KeepVRTopbarOpen = true +NotificationHubModule.VRIsExclusive = true +NotificationHubModule.VRClosesNonExclusive = true +NotificationHubModule.UnreadCountChanged = function() end +VRHub:RegisterModule(NotificationHubModule) + +local notificationsPanel = Panel3D.Get("Notifications") +local notificationsWindow = nil +local detailsPanel = Panel3D.Get("NotificationDetails") +local detailsWindow = nil + +local function IsDeveloperGroupEnabled() + return false +end + +local WindowFrame = {} +do + local windows = {} + local WindowFrame_mt = { __index = WindowFrame } + function WindowFrame.new(panel, parent, title) + local instance = {} + table.insert(windows, instance) + instance.zeroCF = panel.localCF + instance.zOffset = 0 + instance.isPopping = false + instance.isAnimating = false + instance.tweener = nil + instance.panel = panel + instance.panel.OnMouseEnter = function() + for i, v in pairs(windows) do + if v ~= instance then + v:SetPopOut(false) + end + end + instance:SetPopOut(true) + end + + instance.titlebar = Util:Create "ImageLabel" { + Parent = parent, + Name = "TitlebarBackground", + + Position = UDim2.new(0, -1, 0, -1), + Size = UDim2.new(1, 2, 0, WINDOW_TITLEBAR_HEIGHT + 2), + + BackgroundTransparency = 1, + + Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png", + ImageColor3 = BLURRED_TITLEBAR_COLOR, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(10, 10, 10, 10) + } + instance.titleText = Util:Create "TextLabel" { + Parent = instance.titlebar, + Name = "TitleText", + + Position = UDim2.new(0, 1, 0, 1), + Size = UDim2.new(1, -2, 1, -2), + + Text = title, + TextColor3 = TEXT_COLOR, + Font = Enum.Font.SourceSans, + FontSize = Enum.FontSize.Size36, + + BackgroundTransparency = 1 + } + + instance.content = Util:Create "ImageLabel" { + Parent = parent, + Name = "ContentFrame", + + Position = UDim2.new(0, -1, 0, WINDOW_TITLEBAR_HEIGHT + 2), + Size = UDim2.new(1, 2, 1, -WINDOW_TITLEBAR_HEIGHT - 4), + + BackgroundTransparency = 1, + + Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png", + ImageColor3 = WINDOW_BG_COLOR, + ImageTransparency = WINDOW_BG_TRANSPARENCY, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(10, 10, 10, 10) + } + + return setmetatable(instance, WindowFrame_mt) + end + + function WindowFrame:SetTitle(title) + self.titleText.Text = title + end + + function WindowFrame:AddCloseButton(callback) + self.closeButton = Util:Create "ImageButton" { + Parent = self.titlebar, + Name = "CloseButton", + + Position = UDim2.new(0, CLOSE_BUTTON_OFFSET, 0, CLOSE_BUTTON_OFFSET), + Size = UDim2.new(0, CLOSE_BUTTON_SIZE, 0, CLOSE_BUTTON_SIZE), + + BackgroundTransparency = 1, + + Image = CLOSE_BUTTON_IMG, + SelectionImageObject = Util:Create "ImageButton" { + Name = "CloseButtonHover", + Position = UDim2.new(0, CLOSE_BUTTON_HOVER_OFFSET / -2, 0, CLOSE_BUTTON_HOVER_OFFSET / -2), + Size = UDim2.new(1, CLOSE_BUTTON_HOVER_OFFSET, 1, CLOSE_BUTTON_HOVER_OFFSET), + BackgroundTransparency = 1, + Image = CLOSE_BUTTON_HOVER + } + } + self.closeButton.MouseButton1Click:connect(callback) + end + + function WindowFrame:TweenZOffsetTo(zOffset, duration, easingFunc, callback) + if self.tweener and not self.tweener:IsFinished() then + self.tweener:Cancel() + end + self.tweener = Util:TweenProperty(self, "zOffset", self.zOffset, zOffset, duration, easingFunc, callback) + end + + function WindowFrame:AnimateOut(callback) + self.isAnimating = true + self:TweenZOffsetTo(ANIMATE_OUT_DISTANCE, ANIMATE_OUT_DURATION, Util:GetEaseInOutQuad(), function() + if callback then callback() end + self.isAnimating = false + end) + end + + function WindowFrame:AnimateIn(callback) + self.zOffset = ANIMATE_OUT_DISTANCE + self:OnUpdate(0) + self.isAnimating = true + self:TweenZOffsetTo(0, ANIMATE_OUT_DURATION, Util:GetEaseInOutQuad(), function() + if callback then callback() end + self.isAnimating = false + end) + end + + function WindowFrame:SetPopOut(popOut) + if self.isAnimating then + return + end + + if popOut then + self.isPopping = true + self:TweenZOffsetTo(POPOUT_DISTANCE, POPOUT_DURATION, Util:GetEaseInOutQuad(), function() + self.isPopping = false + end) + else + self.isPopping = true + self:TweenZOffsetTo(0, POPOUT_DURATION, Util:GetEaseInOutQuad(), function() + self.isPopping = false + end) + end + end + + function WindowFrame:OnUpdate(dt) + self.panel.localCF = self.zeroCF * CFrame.new(0, 0, self.zOffset) + self.panel.needsLocalPositionUpdate = self.isAnimating or self.isPopping + + if self.isPopping then + local alpha = math.max(0, math.min(1, self.zOffset / POPOUT_DISTANCE)) + self.titlebar.ImageColor3 = BLURRED_TITLEBAR_COLOR:lerp(FOCUSED_TITLEBAR_COLOR, alpha) + end + end +end + +--Notifications panel setup +do + notificationsPanel:SetType(Panel3D.Type.Standard) + notificationsPanel:SetVisible(false) + notificationsPanel:SetCanFade(false) + notificationsPanel:ResizeStuds(leftPanelWidth, totalHeight, PIXELS_PER_STUD) + local notificationsFrame = Util:Create "TextButton" { + Parent = notificationsPanel:GetGUI(), + Name = "NotificationsListFrame", + + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, -4, 1, 0), + + BackgroundTransparency = 1, + Text = "", + + Selectable = true, + SelectionImageObject = emptySelectionImage + } + notificationsWindow = WindowFrame.new(notificationsPanel, notificationsFrame, "Notifications") + notificationsWindow:AddCloseButton(function() + NotificationHubModule:SetVisible(false) + end) + function notificationsPanel:OnUpdate(dt) + notificationsWindow:OnUpdate(dt) + end +end + +--Details panel setup +do + detailsPanel:SetType(Panel3D.Type.Standard) + detailsPanel:SetVisible(false) + detailsPanel:SetCanFade(false) + detailsPanel:ResizeStuds(rightPanelWidth, totalHeight, PIXELS_PER_STUD) + local detailsFrame = Util:Create "TextButton" { + Parent = detailsPanel:GetGUI(), + Name = "NotificationsDetailFrame", + + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + + BackgroundTransparency = 1, + Text = "", + + Selectable = true, + SelectionImageObject = emptySelectionImage + } + detailsWindow = WindowFrame.new(detailsPanel, detailsFrame, "Friend Requests") + function detailsPanel:OnUpdate(dt) + detailsWindow:OnUpdate(dt) + end +end + +local notificationsGroups = {} +local notificationsGroupsList = {} +local function groupSort(a, b) + return a.order < b.order +end +local activeGroup = nil + +local function layoutNotificationsGroups() + local y = NOTIFICATION_PADDING_Y + for _, group in ipairs(notificationsGroupsList) do + if #group.notifications > 0 then + local height = NOTIFICATION_HEIGHT_OFFSET + (NOTIFICATION_PADDING_Y * (math.min(MAX_NOTIFICATIONS_SHOWN, #group.notifications) - 1)) + local widthOffset = -((MAX_NOTIFICATIONS_SHOWN - 1) * NOTIFICATION_PADDING_Y) + group.frame.Position = UDim2.new(NOTIFICATION_PADDING_X_SCALE, 0, 0, y) + group.frame.Size = UDim2.new(NOTIFICATION_WIDTH_SCALE, widthOffset, 0, height) + y = y + height + NOTIFICATION_PADDING_Y + + if group.notificationsDirty then + group.notificationsDirty = false + + local notificationOffset = 0 + local notificationDepth = 0 + + local notificationsEnd = #group.notifications + if notificationsEnd > 0 then + local notificationsStart = math.max(1, notificationsEnd - MAX_NOTIFICATIONS_SHOWN + 1) + for i = 1, notificationsEnd do + local notification = group.notifications[i] + if i >= notificationsStart then + notification.frame.Visible = true + notification.frame.Position = UDim2.new(0, notificationOffset, 0, notificationOffset) + notificationOffset = notificationOffset + NOTIFICATION_PADDING_Y + + local subpanel = notificationsPanel:SetSubpanelDepth(notification.frame, notificationDepth) + notificationDepth = notificationDepth + NOTIFICATION_DEPTH_OFFSET + else + notification.frame.Visible = false + end + end + + if activeGroup == group then + notificationsStart = math.max(1, notificationsEnd - MAX_DETAILS_SHOWN + 1) + local detailY = 0 + local fraction = 1 / MAX_DETAILS_SHOWN + for i = 1, notificationsEnd do + local notification = group.notifications[i] + if i >= notificationsStart then + notification.detailsFrame.Visible = true + notification.detailsFrame.Position = UDim2.new(0, DETAILS_PADDING, detailY, DETAILS_PADDING) + notification.detailsFrame.Size = UDim2.new(1, -DETAILS_PADDING * 2, fraction, -DETAILS_PADDING * 2) + + detailY = detailY + fraction + else + notification.detailsFrame.Visible = false + end + end + end + end + end + end + end +end + +local NotificationGroup = {} +do + local NotificationGroup_mt = { __index = NotificationGroup } + function NotificationGroup.new(key, title, order) + local self = setmetatable({}, NotificationGroup_mt) + + self.key = key + self.title = title + self.order = order + self.notifications = {} + self.notificationsDirty = false + self.frame = Util:Create "Frame" { + Parent = notificationsWindow.content, + Name = "NotificationGroup", + BackgroundTransparency = 1 + } + self.detailsFrame = Util:Create "Frame" { + Parent = nil, + BackgroundTransparency = 1, + + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0) + } + + notificationsGroups[key] = self + table.insert(notificationsGroupsList, self) + + return self + end + + function NotificationGroup:Deactivate() + self.detailsFrame.Parent = nil + for i, v in pairs(self.detailsFrame:GetChildren()) do + detailsPanel:RemoveSubpanel(v) + end + end + + function NotificationGroup:SwitchTo() + detailsWindow:SetTitle(self.title) + for i, v in pairs(notificationsGroups) do + if v ~= self then + v:Deactivate() + end + end + self.detailsFrame.Parent = detailsWindow.content + + activeGroup = self + self.notificationsDirty = true + end + + function NotificationGroup:BringNotificationToFront(notification) + if activeGroup ~= self then + self:SwitchTo() + end + + if #self.notifications ~= 0 and notification == self.notifications[#self.notifications] then + layoutNotificationsGroups() + return --already on top, no point + end + + for i, v in ipairs(self.notifications) do + if v == notification then + --take it out + table.remove(self.notifications, i) + break + end + end + --put it back on top + table.insert(self.notifications, notification) + self.notificationsDirty = true + layoutNotificationsGroups() + end + + function NotificationGroup:RemoveNotification(notification) + for i, v in ipairs(self.notifications) do + if v == notification then + table.remove(self.notifications, i) + notificationsPanel:RemoveSubpanel(notification.frame) + detailsPanel:RemoveSubpanel(notification.detailsFrame) + notification.detailsFrame:Destroy() + notification.frame:Destroy() + + self.notificationsDirty = true + layoutNotificationsGroups() + return + end + end + end + + function NotificationGroup:GetTopNotification() + local numNotifications = #self.notifications + if numNotifications <= 0 then + return nil + end + return self.notifications[numNotifications] + end +end + +NotificationGroup.new("Friends", "Friends", 1) +NotificationGroup.new("BadgeAwards", "Badges", 2) +NotificationGroup.new("PlayerPoints", "Points", 3) +if IsDeveloperGroupEnabled() then + NotificationGroup.new("Developer", "Other", 4) +end +table.sort(notificationsGroupsList, groupSort) + +local function doCallback(callback, ...) + if not callback then + return + end + + if type(callback) == "function" then + callback(...) + return + end + if callback:IsA("BindableEvent") then + callback:Fire(...) + return + end + if callback:IsA("BindableFunction") then + callback:Invoke(...) + return + end +end + +local Notification = {} +do + local Notification_mt = { __index = Notification } + function Notification.new(group, notificationInfo) + local self = setmetatable({}, Notification_mt) + self.group = group + self.frame = Util:Create "ImageButton" { + Parent = group.frame, + Size = UDim2.new(1, 0, 0, NOTIFICATION_HEIGHT_OFFSET), + + SelectionImageObject = emptySelectionImage, + + BackgroundTransparency = 1 --when we have proper frame rendering with AA, we can change this and remove the stand-in background + } + self.frame.MouseButton1Click:connect(function() + self:OnClicked() + end) + self.background = Util:Create "ImageLabel" { --this is the stand-in background for that smoooooooth edge rendering + Parent = self.frame, + Position = UDim2.new(0, -1, 0, -1), + Size = UDim2.new(1, 2, 1, 2), + + BackgroundTransparency = 1, + Image = "rbxasset://textures/ui/vr/rectBackgroundWhite.png", + ImageColor3 = NOTIFICATION_BG_COLOR, + ImageTransparency = NOTIFICATION_BG_TRANSPARENCY, + + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(10, 10, 10, 10) + } + self.imageBackground = Util:Create "ImageLabel" { + Parent = self.frame, + Position = UDim2.new(0, 5, 0, 5), + Size = UDim2.new(0, 70, 0, 70), + + BackgroundTransparency = 1, + + Image = "rbxasset://textures/ui/VR/circleWhite.png", + ImageColor3 = notificationInfo.imgBackgroundColor or Color3.new(1, 1, 1) + } + self.image = Util:Create "ImageLabel" { + Parent = self.imageBackground, + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + + BackgroundTransparency = 1, + + Image = notificationInfo.Image + } + + local text = notificationInfo.Text + if notificationInfo.Title and notificationInfo.Text then + text = ("%s\n%s"):format(notificationInfo.Title, notificationInfo.Text) + end + self.text = Util:Create "TextLabel" { + Parent = self.frame, + + Position = UDim2.new(0, NOTIFICATION_HEIGHT_OFFSET, 0, 0), + Size = UDim2.new(1, -NOTIFICATION_HEIGHT_OFFSET, 1, 0), + + BackgroundTransparency = 1, + + TextXAlignment = Enum.TextXAlignment.Left, + Text = text, + TextWrapped = true, + Font = Enum.Font.SourceSans, + FontSize = Enum.FontSize.Size18, + TextColor3 = TEXT_COLOR + } + + self.detailsFrame = Util:Create "Frame" { + Parent = group.detailsFrame, + BackgroundTransparency = 1 + } + self.detailsFrame.MouseEnter:connect(function() + detailsPanel:SetSubpanelDepth(self.detailsFrame, 0.25) + end) + self.detailsFrame.MouseLeave:connect(function() + detailsPanel:SetSubpanelDepth(self.detailsFrame, 0) + end) + self.detailsBackground = Util:Create "ImageLabel" { + Parent = self.detailsFrame, + Position = UDim2.new(0, -1, 0, -1), + Size = UDim2.new(1, 2, 1, 2), + + BackgroundTransparency = 1, + + Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png", + ImageColor3 = Color3.new(0.2, 0.2, 0.2), + ImageTransparency = 0.1, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(10,10,10,10) + } + + self.detailsIconBackground = Util:Create "ImageLabel" { + Parent = self.detailsFrame, + Position = UDim2.new(0, 20, 0, 10), + Size = UDim2.new(0, 80, 0, 80), + + BackgroundTransparency = 1, + + Image = "rbxasset://textures/ui/VR/circleWhite.png", + ImageColor3 = notificationInfo.imgBackgroundColor or Color3.new(1, 1, 1) + } + self.detailsIcon = Util:Create "ImageLabel" { + Parent = self.detailsIconBackground, + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + + BackgroundTransparency = 1, + + Image = notificationInfo.Image + } + + local detailText = notificationInfo.DetailText or notificationInfo.Title + self.detailsText = Util:Create "TextLabel" { + Parent = self.detailsFrame, + Position = UDim2.new(0, 110, 0, 10), + Size = UDim2.new(1, -120, 0, 90), + + BackgroundTransparency = 1, + + Text = detailText, + TextWrapped = true, + TextColor3 = TEXT_COLOR, + TextXAlignment = Enum.TextXAlignment.Left, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size36 + } + + local function createButton(xPosScale, xSizeScale, text) + local button, text = Util:Create "ImageButton" { + Parent = self.detailsFrame, + Position = UDim2.new(xPosScale, 0, BUTTON_Y_POS, 0), + Size = UDim2.new(xSizeScale, 0, BUTTON_Y_SIZE, 0), + + BackgroundTransparency = 1, + + Image = BUTTON_NORMAL_IMG, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8,6,46,44), + + SelectionImageObject = emptySelectionImage + }, Util:Create "TextLabel" { + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, -6), + + BackgroundTransparency = 1, + + Text = text, + TextColor3 = TEXT_COLOR, + Font = Enum.Font.SourceSansBold, + FontSize = Enum.FontSize.Size24 + } + text.Parent = button + + button.SelectionGained:connect(function() + button.Image = BUTTON_SELECTED_IMG + end) + button.SelectionLost:connect(function() + button.Image = BUTTON_NORMAL_IMG + end) + return button, text + end + + if notificationInfo.Button1Text and notificationInfo.Button2Text then + self.detailsButton1, self.detailsButton1Text = createButton(BUTTON_1_POS, BUTTON_DOUBLE_SIZE, notificationInfo.Button1Text) + self.detailsButton2, self.detailsButton2Text = createButton(BUTTON_2_POS, BUTTON_DOUBLE_SIZE, notificationInfo.Button2Text) + + self.detailsButton1.MouseButton1Click:connect(function() + doCallback(notificationInfo.Callback, notificationInfo.Button1Text) + self:Dismiss() + end) + self.detailsButton2.MouseButton1Click:connect(function() + doCallback(notificationInfo.Callback, notificationInfo.Button2Text) + self:Dismiss() + end) + elseif not notificationInfo.button2Text then + local text = notificationInfo.Button1Text or "Dismiss" + self.detailsButton1, self.detailsButton1Text = createButton(BUTTON_1_POS, BUTTON_SINGLE_SIZE, text) + self.detailsButton1.MouseButton1Click:connect(function() + doCallback(notificationInfo.Callback, notificationInfo.Button1Text) + self:Dismiss() + end) + end + + table.insert(group.notifications, self) + group.notificationsDirty = true + layoutNotificationsGroups() + + return self + end + + function Notification:OnClicked() + --We don't want this functionality anymore, but I'd like to keep this commented + --out for now since this design is still in a state of flux + --self.group:BringNotificationToFront(self) + self.group:SwitchTo() + layoutNotificationsGroups() + end + + function Notification:Dismiss() + self.group:RemoveNotification(self) + end +end + +--NotificationHubModule API and state management +do + local pendingNotifications = {} + local isVisible = false + local unreadCount = 0 + + local SendNotificationInfoEvent = RobloxGui:WaitForChild("SendNotificationInfo") + SendNotificationInfoEvent.Event:connect(function(notificationInfo) + local group = notificationInfo.GroupName and notificationsGroups[notificationInfo.GroupName] --avoid error by nil index + if not group then + if IsDeveloperGroupEnabled() then + group = notificationsGroups.Developer + else + return --ignore it, invalid group + end + end + + Notification.new(group, notificationInfo) + + if not isVisible then + unreadCount = unreadCount + 1 + NotificationHubModule.UnreadCountChanged(unreadCount) + end + end) + + local menuCloseShortcutBindName = "NotificationsMenuCloseShortcut" + local function onMenuCloseShortcut(actionName, inputState, inputObj) + if inputState == Enum.UserInputState.Begin then + NotificationHubModule:SetVisible(false) + end + end + + NotificationHubModule.VisibilityStateChanged = Util:Create "BindableEvent" { + Name = "VisibilityStateChanged" + } + + function NotificationHubModule:GetNumberOfPendingNotifications() + return #pendingNotifications + end + + function NotificationHubModule:IsVisible() + return isVisible + end + + function NotificationHubModule:SetVisible(visible) + if isVisible == visible then + return + end + isVisible = visible + + local topbarPanel = Panel3D.Get("Topbar3D") + topbarPanel:SetCanFade(not visible) + if visible then + unreadCount = 0 + NotificationHubModule.UnreadCountChanged(unreadCount) + + notificationsPanel.localCF = CFrame.new(leftOffset, 0, 0) + notificationsWindow.zeroCF = notificationsPanel.localCF + if not NO_TRANSITION_ANIMATIONS then + notificationsWindow:AnimateIn(nil) + end + + detailsPanel.localCF = CFrame.new(rightOffset, 0, 0) + detailsWindow.zeroCF = detailsPanel.localCF + if not NO_TRANSITION_ANIMATIONS then + detailsWindow:AnimateIn(nil) + end + + notificationsPanel:SetVisible(true) + detailsPanel:SetVisible(true) + + ContextActionService:BindCoreAction(menuCloseShortcutBindName, onMenuCloseShortcut, false, Enum.KeyCode.ButtonB, Enum.KeyCode.ButtonStart) + + VRHub:FireModuleOpened(NotificationHubModule.ModuleName) + else + if not NO_TRANSITION_ANIMATIONS then + spawn(function() + notificationsWindow:AnimateOut(function() + notificationsPanel:SetVisible(false) + end) + detailsWindow:AnimateOut(function() + detailsPanel:SetVisible(false) + end) + end) + else + notificationsPanel:SetVisible(false) + detailsPanel:SetVisible(false) + end + + ContextActionService:UnbindCoreAction(menuCloseShortcutBindName) + + VRHub:FireModuleClosed(NotificationHubModule.ModuleName) + end + + NotificationHubModule.VisibilityStateChanged:Fire(visible) + end + + + VRHub.ModuleOpened.Event:connect(function(moduleName, isExclusive, shouldCloseNonExclusive, shouldKeepTopbarOpen) + if moduleName ~= NotificationHubModule.ModuleName then + NotificationHubModule:SetVisible(false) + end + end) +end + +return NotificationHubModule \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/VR/NotifierHint3D.lua b/Client2018/content/scripts/CoreScripts/Modules/VR/NotifierHint3D.lua new file mode 100644 index 0000000..24af79e --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/VR/NotifierHint3D.lua @@ -0,0 +1,85 @@ +-- NotifierHint3D.lua -- +-- Written by Kip Turner, copyright ROBLOX 2016 -- + +local NotifierHint = {} + +local CoreGui = game:GetService('CoreGui') +local RunService = game:GetService('RunService') + +local RobloxGui = CoreGui:WaitForChild("RobloxGui") + +local Util = require(RobloxGui.Modules.Settings.Utility) + + +local NotificationObject = Util:Create'ImageLabel' +{ + Name = 'NotificationObject'; + -- These numbers are a bit funny to fit the screen of ROBLOX VR + Position = UDim2.new(0.5, -860, 1, -1 - 300); + Size = UDim2.new(0, 1700,0, 700 + 300); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/VR/notifier_glow.png"; + ImageTransparency = 0; + BorderSizePixel = 0; +} + +NotifierHint.DEFAULT_DURATION = 5 + +local function CreateNotificationEffect() + local this = {} + + local speed = 2.5 + local MAX_OPACITY = 0.5 + local WAVE_START_SHIFT = math.pi/2 + + local renderConn = nil + + function this:Init(duration) + local start = tick() + local endTime = start + duration + + if renderConn then + renderConn:disconnect() + renderConn = nil + end + renderConn = RunService.RenderStepped:connect(function() + local now = tick() + if now >= endTime then + self:Cancel() + return + end + NotificationObject.Parent = RobloxGui + local tweenPositionOnSineWave = math.sin((tick() - start) * speed + WAVE_START_SHIFT) + -- Restrict the sine wave to only positive values + tweenPositionOnSineWave = (tweenPositionOnSineWave + 1) / 2 + -- Keep the transparency in the range + NotificationObject.ImageTransparency = tweenPositionOnSineWave * (1 - MAX_OPACITY) + MAX_OPACITY + end) + end + + function this:Cancel() + if renderConn then + renderConn:disconnect() + renderConn = nil + end + NotificationObject.Parent = nil + end + + return this +end + +local NotifierEffect = CreateNotificationEffect() + +function NotifierHint:BeginNotification(duration) + self:CancelNotification() + NotifierEffect:Init(duration or self.DEFAULT_DURATION) +end + +function NotifierHint:CancelNotification() + NotifierEffect:Cancel() +end + + + + +return NotifierHint diff --git a/Client2018/content/scripts/CoreScripts/Modules/VR/Panel3D.lua b/Client2018/content/scripts/CoreScripts/Modules/VR/Panel3D.lua new file mode 100644 index 0000000..ce8bd29 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/VR/Panel3D.lua @@ -0,0 +1,1078 @@ +--Panel3D: 3D GUI panels for VR +--written by 0xBAADF00D +--revised/refactored 5/11/16 + +local UserInputService = game:GetService("UserInputService") +local VRService = game:GetService("VRService") +local RunService = game:GetService("RunService") +local GuiService = game:GetService("GuiService") +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local Players = game:GetService("Players") +local Utility = require(RobloxGui.Modules.Settings.Utility) + +--Panel3D State variables +local renderStepName = "Panel3DRenderStep-" .. game:GetService("HttpService"):GenerateGUID() +local defaultPixelsPerStud = 64 +local pointUpCF = CFrame.Angles(math.rad(-90), math.rad(180), 0) +local zeroVector = Vector3.new(0, 0, 0) +local zeroVector2 = Vector2.new(0, 0) +local turnAroundCF = CFrame.Angles(0, math.rad(180), 0) +local fullyOpaqueAtPixelsFromEdge = 10 +local fullyTransparentAtPixelsFromEdge = 80 +local partThickness = 0.2 + +--The default origin CFrame for all Standard type panels +local standardOriginCF = CFrame.new(0, -0.5, -5.5) + +--Compensates for the thickness of the panel part and rotates it so that +--the front face is pointing back at the camera +local panelAdjustCF = CFrame.new(0, 0, -0.5 * partThickness) * CFrame.Angles(0, math.pi, 0) + +local cursorHidden = false +local cursorHideTime = 2.5 + +local currentModal = nil +local lastModal = nil +local currentMaxDist = math.huge +local currentClosest = nil +local currentCursorParent = nil +local currentCursorPos = zeroVector2 +local lastClosest = nil +local currentHeadScale = 1 +local panels = {} +local floorRotation = CFrame.new() +local cursor = Utility:Create "ImageLabel" { + Image = "rbxasset://textures/Cursors/Gamepad/Pointer.png", + Size = UDim2.new(0, 8, 0, 8), + BackgroundTransparency = 1, + ZIndex = 1e9 +} +local partFolder = Utility:Create "Folder" { + Name = "VRCorePanelParts", + Archivable = false +} +local effectFolder = Utility:Create "Folder" { + Name = "VRCoreEffectParts", + Archivable = false +} +pcall(function() + GuiService.CoreGuiFolder = partFolder + GuiService.CoreEffectFolder = effectFolder +end) +--End of Panel3D State variables + + +--Panel3D Declaration and enumerations +local Panel3D = {} +Panel3D.Type = { + None = 0, + Standard = 1, + Fixed = 2, + HorizontalFollow = 3, + FixedToHead = 4 +} + +Panel3D.OnPanelClosed = Utility:Create 'BindableEvent' { + Name = 'OnPanelClosed' +} + +function Panel3D.GetHeadLookXZ(withTranslation) + local userHeadCF = VRService:GetUserCFrame(Enum.UserCFrame.Head) + local headLook = userHeadCF.lookVector + local headYaw = math.atan2(-headLook.Z, headLook.X) - math.rad(90) + local cf = CFrame.Angles(0, headYaw, 0) + + if withTranslation then + cf = cf + userHeadCF.p + end + return cf +end + +function Panel3D.FindContainerOf(element) + for _, panel in pairs(panels) do + if panel.gui and panel.gui:IsAncestorOf(element) then + return panel + end + for _, subpanel in pairs(panel.subpanels) do + if subpanel.gui and subpanel.gui:IsAncestorOf(element) then + return panel + end + end + end + return nil +end + +function Panel3D.SetModalPanel(panel) + if currentModal == panel then + return + end + if currentModal then + currentModal:OnModalChanged(false) + end + if panel then + panel:OnModalChanged(true) + end + lastModal = currentModal + currentModal = panel +end + +function Panel3D.RaycastOntoPanel(part, parentGui, gui, ray) + local partSize = part.Size + local partThickness = partSize.Z + local partWidth = partSize.X + local partHeight = partSize.Y + + local planeCF = part:GetRenderCFrame() + local planeNormal = planeCF.lookVector + local pointOnPlane = planeCF.p + (planeNormal * partThickness * 0.5) + + --Find where the view ray intersects with the plane in world space + local worldIntersectPoint = Utility:RayPlaneIntersection(ray, planeNormal, pointOnPlane) + if worldIntersectPoint then + local parentGuiWidth, parentGuiHeight = parentGui.AbsoluteSize.X, parentGui.AbsoluteSize.Y + --now figure out where that intersection point was in the panel's local space + --and then flip the X axis because the plane is looking back at you (panel's local +X is to the left of the camera) + --and then offset it by half of the panel's size in X and -Y to move 0,0 to the upper-left of the panel. + local localIntersectPoint = planeCF:pointToObjectSpace(worldIntersectPoint) * Vector3.new(-1, 1, 1) + Vector3.new(partWidth / 2, -partHeight / 2, 0) + --now scale it into the gui space on the panel's surface + local lookAtPixel = Vector2.new((localIntersectPoint.X / partWidth) * parentGuiWidth, (localIntersectPoint.Y / partHeight) * -parentGuiHeight) + + --fire mouse enter/leave events if necessary + local lookX, lookY = lookAtPixel.X, lookAtPixel.Y + local guiX, guiY = gui.AbsolutePosition.X, gui.AbsolutePosition.Y + local guiWidth, guiHeight = gui.AbsoluteSize.X, gui.AbsoluteSize.Y + local isOnGui = false + + if parentGui.Enabled then + if lookX >= guiX and lookX <= guiX + guiWidth and + lookY >= guiY and lookY <= guiY + guiHeight then + isOnGui = true + end + end + + return worldIntersectPoint, localIntersectPoint, lookAtPixel, isOnGui + else + return nil, nil, nil, false + end +end + +--End of Panel3D Declaration and enumerations + +--Panel class implementation +local Panel = {} +Panel.__index = Panel +function Panel.new(name) + local self = {} + self.name = name + + self.part = false + self.gui = false + + self.width = 1 + self.height = 1 + + self.isVisible = false + self.isEnabled = false + self.panelType = Panel3D.Type.None + self.pixelScale = 1 + self.showCursor = true + self.canFade = true + self.shouldFindLookAtGuiElement = false + self.ignoreModal = false + self.needsPositionUpdate = false + + self.linkedTo = false + self.subpanels = {} + + self.transparency = 1 + self.forceShowUntilLookedAt = false + self.forceShowUntilTick = 0 + self.isLookedAt = false + self.isOffscreen = true + self.lookAtPixel = Vector2.new(-1, -1) + self.cursorPos = Vector2.new(-1, -1) + self.lookAtDistance = math.huge + self.lookAtGuiElement = false + self.isClosest = true + + self.localCF = CFrame.new() + self.angleFromHorizon = false + self.angleFromForward = false + self.distance = false + + if panels[name] then + error("A panel by the name of " .. name .. " already exists.") + end + panels[name] = self + + return setmetatable(self, Panel) +end + +--Panel accessor methods +function Panel:GetPart() + if not self.part then + self.part = Utility:Create "Part" { + Name = self.name, + Parent = partFolder, + + Transparency = 1, + + CanCollide = false, + Anchored = true, + + Size = Vector3.new(1, 1, partThickness) + } + end + return self.part +end + +function Panel:GetGUI() + if not self.gui then + local part = self:GetPart() + self.gui = Utility:Create "SurfaceGui" { + Parent = CoreGui, + Name = self.name, + Archivable = false, + Adornee = part, + Active = true, + ToolPunchThroughDistance = 1000, + CanvasSize = self.CanvasSize or Vector2.new(0, 0), + Enabled = self.isEnabled, + AlwaysOnTop = true + } + end + return self.gui +end + +function Panel:FindHoveredGuiElement(elements) + local x, y = self.lookAtPixel.X, self.lookAtPixel.Y + for i, v in pairs(elements) do + local minPt = v.AbsolutePosition + local maxPt = v.AbsolutePosition + v.AbsoluteSize + if minPt.X <= x and maxPt.X >= x and + minPt.Y <= y and maxPt.Y >= y then + return v, i + end + end +end +--End of panel accessor methods + + +--Panel update methods +function Panel:SetPartCFrame(cframe) + self:GetPart().CFrame = cframe * panelAdjustCF +end + +function Panel:SetEnabled(enabled) + if self.isEnabled == enabled then + return + end + + self.isEnabled = enabled + if enabled then + self:GetPart().Parent = partFolder + self:GetGUI().Enabled = true + for i, v in pairs(self.subpanels) do + v:SetEnabled(v:GetEnabled()) + end + else + self:GetPart().Parent = nil + self:GetGUI().Enabled = false + for i, v in pairs(self.subpanels) do + v:SetEnabled(v:GetEnabled()) + end + end + + self:OnEnabled(enabled) +end + +function Panel:EvaluatePositioning(cameraCF, cameraRenderCF, userHeadCF) + if self.panelType == Panel3D.Type.Fixed then + --Places the panel in the camera's local space, but doesn't follow the user's head. + --Useful if you know what you're doing. localCF can be updated in PreUpdate for animation. + local cf = self.localCF - self.localCF.p + cf = cf + (self.localCF.p * currentHeadScale) + self:SetPartCFrame(cameraCF * cf) + elseif self.panelType == Panel3D.Type.HorizontalFollow then + local headLook = userHeadCF.lookVector + local headForwardCF = CFrame.new(userHeadCF.p, userHeadCF.p + (headLook * Vector3.new(1, 0, 1))) + local localCF = (headForwardCF * self.angleFromForward) * --Rotate about Y (left-right) + self.angleFromHorizon * --Rotate about X (up-down) + CFrame.new(0, 0, currentHeadScale * -self.distance) + self:SetPartCFrame(cameraCF * localCF) + elseif self.panelType == Panel3D.Type.FixedToHead then + --Places the panel in the user's head local space. localCF can be updated in PreUpdate for animation. + local cf = self.localCF - self.localCF.p + cf = cf + (self.localCF.p * currentHeadScale) + self:SetPartCFrame(cameraRenderCF * cf) + elseif self.panelType == Panel3D.Type.Standard then + if self.needsPositionUpdate then + self.needsPositionUpdate = false + local headLookXZ = Panel3D.GetHeadLookXZ(true) + self.originCF = headLookXZ * standardOriginCF + end + + self:SetPartCFrame(cameraCF * self.originCF * self.localCF) + end +end + +function Panel:SetLookedAt(lookedAt) + if not self.isLookedAt and lookedAt then + self.isLookedAt = true + self:OnMouseEnter(self.lookAtPixel.X, self.lookAtPixel.Y) + if self.forceShowUntilLookedAt then + self.forceShowUntilLookedAt = false + end + elseif self.isLookedAt and not lookedAt then + self.isLookedAt = false + self:OnMouseLeave(self.lookAtPixel.X, self.lookAtPixel.Y) + end +end + +function Panel:EvaluateGaze(cameraCF, cameraRenderCF, userHeadCF, lookRay, pointerRay) + --reset distance data + self.isClosest = false + self.lookAtPixel = zeroVector2 + self.lookAtDistance = math.huge + + --check all subpanels first, they're usually in front of the panel. + local highestSubpanel = nil + local highestSubpanelDepth = 0 + for guiElement, subpanel in pairs(self.subpanels) do + if subpanel.part and subpanel.guiElement then + --note that we're passing subpanel.guiElement and not subpanel.gui + --this is on purpose so we can fall through to the panels underneath since subpanels will rarely take up the whole + --panel size. + local worldIntersectPoint, localIntersectPoint, guiPixelHit, isOnGui = Panel3D.RaycastOntoPanel(subpanel.part, subpanel.gui, subpanel.guiElement, pointerRay) + if worldIntersectPoint then + subpanel.lookAtPixel = guiPixelHit + subpanel.cursorPos = guiPixelHit + + if isOnGui and subpanel.depthOffset > highestSubpanelDepth then + highestSubpanel = subpanel + highestSubpanelDepth = subpanel.depthOffset + end + end + end + end + + if highestSubpanel and highestSubpanel.depthOffset > 0 then + currentCursorParent = highestSubpanel.gui + currentCursorPos = highestSubpanel.cursorPos + currentClosest = highestSubpanel + + for _, subpanel in pairs(self.subpanels) do + if subpanel ~= highestSubpanel then + subpanel:SetLookedAt(false) + end + end + highestSubpanel:SetLookedAt(true) + end + + local gui = self:GetGUI() + local worldIntersectPoint, localIntersectPoint, guiPixelHit, isOnGui = Panel3D.RaycastOntoPanel(self:GetPart(), gui, gui, pointerRay) + if worldIntersectPoint then + self.isOffscreen = false + + --transform worldIntersectPoint to gui space + self.lookAtPixel = guiPixelHit + self.cursorPos = guiPixelHit + + --fire mouse enter/leave events if necessary + self:SetLookedAt(isOnGui) + + --evaluate distance + self.lookAtDistance = (worldIntersectPoint - cameraRenderCF.p).magnitude + if self.isLookedAt and self.lookAtDistance < currentMaxDist and self.showCursor then + currentMaxDist = self.lookAtDistance + currentClosest = self + if not highestSubpanel then + currentCursorParent = self.gui + currentCursorPos = self.cursorPos + end + end + else + self.isOffscreen = true + + --Not looking at the plane at all, so fire off mouseleave if necessary. + if self.isLookedAt then + self.isLookedAt = false + self:OnMouseLeave(self.lookAtPixel.X, self.lookAtPixel.Y) + end + end +end + +function Panel:EvaluateTransparency() + --Early exit if force shown + if self.forceShowUntilLookedAt or not self.canFade or self.forceShowUntilTick > tick() then + self.transparency = 0 + return + end + --Early exit if we're looking at the panel (no transparency!) + if self.isLookedAt then + self.transparency = 0 + return + end + --Similarly, exit if we can't possibly see the panel. + if self.isOffscreen then + self.transparency = 1 + return + end + --Otherwise, we'll want to calculate the transparency. + self.transparency = self:CalculateTransparency() +end + +function Panel:Update(cameraCF, cameraRenderCF, userHeadCF, lookRay, pointerRay, dt) + if (self.forceShowUntilLookedAt or self.forceShowUntilTick > tick()) and not self.part then + self:GetPart() + self:GetGUI() + end + if not self.part then + return + end + + local isModal = (currentModal == self) + if not isModal and self.linkedTo and self.linkedTo == currentModal then + isModal = true + end + if currentModal and not isModal then + self:SetEnabled(false) + return + end + + self:PreUpdate(cameraCF, cameraRenderCF, userHeadCF, lookRay, dt) + if self.isVisible then + self:EvaluatePositioning(cameraCF, cameraRenderCF, userHeadCF) + for i, v in pairs(self.subpanels) do + v:Update() + end + self:EvaluateGaze(cameraCF, cameraRenderCF, userHeadCF, lookRay, pointerRay) + + self:EvaluateTransparency(cameraCF, cameraRenderCF) + end +end +--End of Panel update methods + +--Panel virtual methods +function Panel:PreUpdate(cameraCF, cameraRenderCF, userHeadCF, lookRay, dt) --virtual: handle positioning here +end + +function Panel:OnUpdate(dt) --virtual: handle transparency here +end + +function Panel:OnMouseEnter(x, y) --virtual +end + +function Panel:OnMouseLeave(x, y) --virtual +end + +function Panel:OnEnabled(enabled) --virtual +end + +function Panel:OnModalChanged(isModal) --virtual +end + +function Panel:OnVisibilityChanged(visible) --virtual +end + +function Panel:CalculateTransparency() --virtual + if not self.canFade then + return 0 + end + + local guiWidth, guiHeight = self.gui.AbsoluteSize.X, self.gui.AbsoluteSize.Y + local lookX, lookY = self.lookAtPixel.X, self.lookAtPixel.Y + + --Determine the distance from the edge; + --if x is negative it's on the left side, meaning the distance is just absolute value + --if x is positive it's on the right side, meaning the distance is x minus the width + local xEdgeDist = lookX < 0 and -lookX or (lookX - guiWidth) + local yEdgeDist = lookY < 0 and -lookY or (lookY - guiHeight) + if lookX > 0 and lookX < guiWidth then + xEdgeDist = 0 + end + if lookY > 0 and lookY < guiHeight then + yEdgeDist = 0 + end + local edgeDist = math.sqrt(xEdgeDist ^ 2 + yEdgeDist ^ 2) + + --since transparency is 0-1, we know how many pixels will give us 0 and how many will give us 1. + local offset = fullyOpaqueAtPixelsFromEdge + local interval = fullyTransparentAtPixelsFromEdge + --then we just clamp between 0 and 1. + return math.max(0, math.min(1, (edgeDist - offset) / interval)) +end +--End of Panel virtual methods + + +--Panel configuration methods +function Panel:ResizeStuds(width, height, pixelsPerStud) + pixelsPerStud = pixelsPerStud or defaultPixelsPerStud + + self.width = width + self.height = height + + self.pixelScale = pixelsPerStud / defaultPixelsPerStud + + local part = self:GetPart() + part.Size = Vector3.new(self.width * currentHeadScale, self.height * currentHeadScale, partThickness) + local gui = self:GetGUI() + gui.CanvasSize = Vector2.new(pixelsPerStud * self.width, pixelsPerStud * self.height) + + for i, v in pairs(self.subpanels) do + if v.part then + v.part.Size = part.Size + end + if v.gui then + v.gui.CanvasSize = gui.CanvasSize + end + end +end + +function Panel:ResizePixels(width, height, pixelsPerStud) + pixelsPerStud = pixelsPerStud or defaultPixelsPerStud + + local widthInStuds = width / pixelsPerStud + local heightInStuds = height / pixelsPerStud + self:ResizeStuds(widthInStuds, heightInStuds, pixelsPerStud) +end + +function Panel:OnHeadScaleChanged(newHeadScale) + local pixelsPerStud = self.pixelScale * defaultPixelsPerStud + self:ResizeStuds(self.width, self.height, pixelsPerStud) +end + +function Panel:SetType(panelType, config) + self.panelType = panelType + + --clear out old type-specific members + + self.localCF = CFrame.new() + + self.angleFromHorizon = false + self.angleFromForward = false + self.distance = false + + if not config then + config = {} + end + + if panelType == Panel3D.Type.None then + --nothing to do + return + elseif panelType == Panel3D.Type.Standard then + self.localCF = config.CFrame or CFrame.new() + elseif panelType == Panel3D.Type.Fixed then + self.localCF = config.CFrame or CFrame.new() + elseif panelType == Panel3D.Type.HorizontalFollow then + self.angleFromHorizon = CFrame.Angles(config.angleFromHorizon or 0, 0, 0) + self.angleFromForward = CFrame.Angles(0, config.angleFromForward or 0, 0) + self.distance = config.distance or 5 + elseif panelType == Panel3D.Type.FixedToHead then + self.localCF = config.CFrame or CFrame.new() + else + error("Invalid Panel type") + end +end + +function Panel:SetVisible(visible, modal) + if visible ~= self.isVisible then + self:OnVisibilityChanged(visible) + if not visible then + Panel3D.OnPanelClosed:Fire(self.name) + else + self.needsPositionUpdate = true + end + end + + self.isVisible = visible + self:SetEnabled(visible) + if visible and modal then + Panel3D.SetModalPanel(self) + end + if not visible and currentModal == self then + if modal then + --restore last modal panel + Panel3D.SetModalPanel(lastModal) + else + Panel3D.SetModalPanel(nil) + + --if the coder explicitly wanted to hide this modal panel, + --it follows that they don't want it to be restored when the next + --modal panel is hidden. + if lastModal == self then + lastModal = nil + end + end + end + + if not visible and self.forceShowUntilLookedAt then + self.forceShowUntilLookedAt = false + end +end + +function Panel:IsVisible() + return self.isVisible +end + +function Panel:LinkTo(panelName) + if type(panelName) == "string" then + self.linkedTo = Panel3D.Get(panelName) + else + self.linkedTo = panelName + end +end + +function Panel:ForceShowUntilLookedAt(makeModal) + --ensure the part exists + self:GetPart() + self:GetGUI() + + self:SetVisible(true, makeModal) + self:RequestPositionUpdate() + self.forceShowUntilLookedAt = true +end + +function Panel:ForceShowForSeconds(seconds) + self:GetPart() + self:GetGUI() + + self:SetVisible(true) + if self.forceShowUntilTick < tick() then + self:RequestPositionUpdate() + end + self.forceShowUntilTick = tick() + seconds +end + +function Panel:SetCanFade(canFade) + self.canFade = canFade +end + +function Panel:RequestPositionUpdate() + self.needsPositionUpdate = true +end + +function Panel:GetGuiPositionInPanelSpace(guiPosition) + local partSize = Vector2.new(self.part.Size.X, self.part.Size.Y) + local guiSize = self.gui.AbsoluteSize + local guiCenter = guiSize / 2 + + local guiPositionFraction = (guiPosition - guiCenter) / guiSize + local positionInPartFace = guiPositionFraction * partSize + + return Vector3.new(positionInPartFace.X, positionInPartFace.Y, partThickness * 0.5) +end + +function Panel:GetCFrameInCameraSpace() + if self.panelType == Panel3D.Type.Standard then + return self.originCF * self.localCF + else + return self.localCF or CFrame.new() + end +end + +--Child class, Subpanel +local Subpanel = {} +Subpanel.__index = Subpanel +function Subpanel.new(parentPanel, guiElement) + local self = {} + self.parentPanel = parentPanel + self.guiElement = guiElement + self.lastParent = guiElement.Parent + self.ancestryConn = nil + self.changedConn = nil + + self.lookAtPixel = Vector2.new(-1, -1) + self.cursorPos = Vector2.new(-1, -1) + self.lookedAt = false + + self.isEnabled = true + + self.part = nil + self.gui = nil + self.guiSurrogate = nil + + self.depthOffset = 0 + + setmetatable(self, Subpanel) + + + self:GetGUI() + self:UpdateSurrogate() + self:WatchParent(self.lastParent) + + guiElement.Parent = self.guiSurrogate + + local function ancestryCallback(parent, child) + self:GetGUI().Enabled = self.parentPanel:GetGUI():IsAncestorOf(self.lastParent) + if not self:GetGUI().Enabled then + self:GetPart().Parent = nil + else + self:GetPart().Parent = workspace.CurrentCamera + end + if child == guiElement then + --disconnect the event because we're going to move this element + self.ancestryConn:disconnect() + + self.lastParent = guiElement.Parent + guiElement.Parent = self.guiSurrogate + self:WatchParent(self.lastParent) + + --reconnect it + self.ancestryConn = guiElement.AncestryChanged:connect(ancestryCallback) + end + end + self.ancestryConn = guiElement.AncestryChanged:connect(ancestryCallback) + + return self +end + +function Subpanel:Cleanup() + self.guiElement.Parent = self.lastParent + if self.part then + self.part:Destroy() + self.part = nil + end + spawn(function() + wait() --wait so anything that's in the gui that doesn't want to be has time to get out (panel cursor for example) + if self.gui then + self.gui:Destroy() + self.gui = nil + end + end) + if self.ancestryConn then + self.ancestryConn:disconnect() + self.ancestryConn = nil + end + if self.changedConn then + self.changedConn:disconnect() + self.changedConn = nil + end + self.lastParent = nil + self.parentPanel = nil + self.guiElement = nil + self.guiSurrogate = nil +end + +function Subpanel:OnMouseEnter(x, y) +end +function Subpanel:OnMouseLeave(x, y) +end + +function Subpanel:SetLookedAt(lookedAt) + if lookedAt and not self.lookedAt then + self:OnMouseEnter(self.lookAtPixel.X, self.lookAtPixel.Y) + elseif not lookedAt and self.lookedAt then + self:OnMouseLeave(self.lookAtPixel.X, self.lookAtPixel.Y) + end + self.lookedAt = lookedAt +end + +function Subpanel:WatchParent(parent) + if self.changedConn then + self.changedConn:disconnect() + end + self.changedConn = parent.Changed:connect(function(prop) + if prop == "AbsolutePosition" or prop == "AbsoluteSize" or prop == "Parent" then + self:UpdateSurrogate() + end + end) +end + +function Subpanel:UpdateSurrogate() + local lastParent = self.lastParent + self.guiSurrogate.Position = UDim2.new(0, lastParent.AbsolutePosition.X, 0, lastParent.AbsolutePosition.Y) + self.guiSurrogate.Size = UDim2.new(0, lastParent.AbsoluteSize.X, 0, lastParent.AbsoluteSize.Y) +end + +function Subpanel:GetPart() + if self.part then + return self.part + end + + self.part = self.parentPanel:GetPart():Clone() + self.part.Parent = partFolder + return self.part +end + +function Subpanel:GetGUI() + if self.gui then + return self.gui + end + + self.gui = Utility:Create "SurfaceGui" { + Parent = CoreGui, + Adornee = self:GetPart(), + Active = true, + ToolPunchThroughDistance = 1000, + CanvasSize = self.parentPanel:GetGUI().CanvasSize, + Enabled = self.parentPanel.isEnabled, + AlwaysOnTop = true + } + self.guiSurrogate = Utility:Create "Frame" { + Parent = self.gui, + + Active = false, + + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + + BackgroundTransparency = 1 + } + return self.gui +end + +function Subpanel:SetDepthOffset(offset) + self.depthOffset = offset +end + +function Subpanel:Update() + local part = self:GetPart() + local parentPart = self.parentPanel:GetPart() + + if part and parentPart then + part.CFrame = parentPart.CFrame * CFrame.new(0, 0, -self.depthOffset) + end +end + +function Subpanel:SetEnabled(enabled) + -- Don't change check here, parentPanel may try to refresh our enabled state + -- alternatively we could listen to an enabled changed event on our parent panel + self.isEnabled = enabled + if enabled and self.parentPanel.isEnabled then + self:GetPart().Parent = partFolder + self:GetGUI().Enabled = true + else + self:GetPart().Parent = nil + self:GetGUI().Enabled = false + end +end + +function Subpanel:GetEnabled() + return self.isEnabled +end + +function Subpanel:GetPixelScale() + return self.parentPanel:GetPixelScale() +end +function Panel:GetPixelScale() + return self.pixelScale +end + +function Panel:AddSubpanel(guiElement) + local subpanel = Subpanel.new(self, guiElement) + self.subpanels[guiElement] = subpanel + return subpanel +end + +function Panel:RemoveSubpanel(guiElement) + local subpanel = self.subpanels[guiElement] + if subpanel then + subpanel:Cleanup() + end + self.subpanels[guiElement] = nil +end + +function Panel:SetSubpanelDepth(guiElement, depth) + local subpanel = self.subpanels[guiElement] + + if depth == 0 then + if subpanel then + self:RemoveSubpanel(guiElement) + end + return nil + end + + if not subpanel then + subpanel = self:AddSubpanel(guiElement) + end + subpanel:SetDepthOffset(depth) + + return subpanel +end + +--End of Panel configuration methods +--End of Panel class implementation + + +--Panel3D API +function Panel3D.Get(name) + local panel = panels[name] + if not panels[name] then + panels[name] = Panel.new(name) + panel = panels[name] + end + return panel +end +--End of Panel3D API + + +--Panel3D Setup +local frameStart = tick() +local function onRenderStep() + if not VRService.VREnabled then + return + end + + local now = tick() + local dt = now - frameStart + frameStart = now + + + --reset distance info + currentClosest = nil + currentMaxDist = math.huge + + --figure out some useful stuff + local camera = workspace.CurrentCamera + local cameraCF = camera.CFrame + local cameraRenderCF = camera:GetRenderCFrame() + local userHeadCF = VRService:GetUserCFrame(Enum.UserCFrame.Head) + local lookRay = Ray.new(cameraRenderCF.p, cameraRenderCF.lookVector) + + local inputUserCFrame = VRService.GuiInputUserCFrame + local inputCF = cameraCF * VRService:GetUserCFrame(inputUserCFrame) + local pointerRay = Ray.new(inputCF.p, inputCF.lookVector) + + --allow all panels to run their own update code + for i, v in pairs(panels) do + v:Update(cameraCF, cameraRenderCF, userHeadCF, lookRay, pointerRay, dt) + end + + --evaluate linked panels + local processed = {} + for i, v in pairs(panels) do + if not processed[v] and v.linkedTo and v.isVisible and v.linkedTo.isVisible then + processed[v] = true + processed[v.linkedTo] = true + + local minTransparency = math.min(v.transparency, v.linkedTo.transparency) + v.transparency = minTransparency + v.linkedTo.transparency = minTransparency + end + end + + --run post update because the distance information hasn't been + --finalized until now. + for i, v in pairs(panels) do + --If the part is fully transparent, we don't want to keep it around in the workspace. + if v.part and v.gui then + --check if this panel is the current modal panel + local isModal = (currentModal == v) + --but also check if this panel is linked to the current modal panel + if not isModal and v.linkedTo and v.linkedTo == currentModal then + isModal = true + end + + local show = v.isVisible + if not isModal and currentModal then + show = false + end + if v.transparency >= 1 then + show = false + end + + if v.forceShowUntilLookedAt then + show = true + end + if not v.canFade and v.isVisible then + show = true + end + + v:SetEnabled(show) + end + + v:OnUpdate(dt) + end + + if currentClosest then + cursor.Parent = currentCursorParent + + local x, y = currentCursorPos.X, currentCursorPos.Y + local pixelScale = currentClosest:GetPixelScale() + cursor.Size = UDim2.new(0, 8 * pixelScale, 0, 8 * pixelScale) + cursor.Position = UDim2.new(0, x - cursor.AbsoluteSize.x * 0.5, 0, y - cursor.AbsoluteSize.y * 0.5) + else + cursor.Parent = nil + end + lastClosest = currentClosest +end + +local isCameraReady = false +local function putFoldersIn(parent) + partFolder.Parent = parent + effectFolder.Parent = parent +end + +local headscaleChangedConn = nil +local function onHeadScaleChanged() + local currentHeadScale = workspace.CurrentCamera.HeadScale + for i, v in pairs(panels) do + v:OnHeadScaleChanged(currentHeadScale) + end +end + +local function onCurrentCameraChanged() + onHeadScaleChanged() + if headscaleChangedConn then + headscaleChangedConn:disconnect() + end + headscaleChangedConn = workspace.CurrentCamera:GetPropertyChangedSignal("HeadScale"):connect(onHeadScaleChanged) + + if VRService.VREnabled and isCameraReady then + putFoldersIn(workspace.CurrentCamera) + end +end + +local currentCameraChangedConn = nil +local renderStepFuncBound = false +local function onVREnabledChanged() + if VRService.VREnabled then + while not isCameraReady do + wait() + end + + if workspace.CurrentCamera then + onCurrentCameraChanged() + end + currentCameraChangedConn = workspace:GetPropertyChangedSignal("CurrentCamera"):connect(onCurrentCameraChanged) + + putFoldersIn(workspace.CurrentCamera) + + if not renderStepFuncBound then + RunService:BindToRenderStep(renderStepName, Enum.RenderPriority.Last.Value, onRenderStep) + renderStepFuncBound = true + end + else + if currentCameraChangedConn then + currentCameraChangedConn:disconnect() + currentCameraChangedConn = nil + end + putFoldersIn(nil) + + if renderStepFuncBound then + RunService:UnbindFromRenderStep(renderStepName) + renderStepFuncBound = false + end + end +end +VRService:GetPropertyChangedSignal("VREnabled"):connect(onVREnabledChanged) +spawn(onVREnabledChanged) + +coroutine.wrap(function() + while true do + if workspace.CurrentCamera then + if workspace.CurrentCamera.CameraSubject ~= nil or workspace.CurrentCamera.CameraType == Enum.CameraType.Scriptable then + break + end + workspace.CurrentCamera.Changed:wait() + else + wait() + end + end + + isCameraReady = true +end)() + +return Panel3D \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/VR/Recenter.lua b/Client2018/content/scripts/CoreScripts/Modules/VR/Recenter.lua new file mode 100644 index 0000000..af4ee69 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/VR/Recenter.lua @@ -0,0 +1,118 @@ +local InputService = game:GetService("UserInputService") +local ContextActionService = game:GetService("ContextActionService") +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local Panel3D = require(RobloxGui.Modules.VR.Panel3D) +local VRHub = require(RobloxGui.Modules.VR.VRHub) +local Util = require(RobloxGui.Modules.Settings.Utility) + +local cancelShortcutName = "CancelRecenterShortcut" +local visible = false + +local RecenterModule = {} +RecenterModule.ModuleName = "Recenter" +RecenterModule.KeepVRTopbarOpen = true +RecenterModule.VRIsExclusive = true +RecenterModule.VRClosesNonExclusive = false +VRHub:RegisterModule(RecenterModule) + +local countdownPanel = Panel3D.Get("RecenterCountdown") +countdownPanel:SetType(Panel3D.Type.HorizontalFollow) +countdownPanel:ResizeStuds(5, 3, 128) +countdownPanel:SetCanFade(false) + +local countdown = Util:Create "TextLabel" { + Parent = countdownPanel:GetGUI(), + + Position = UDim2.new(0.5, -64, 0.5, -64), + Size = UDim2.new(0, 128, 0, 128), + + BackgroundTransparency = 0.9, + BackgroundColor3 = Color3.new(0.2, 0.2, 0.2), + + TextColor3 = Color3.new(1, 1, 1), + Text = "", + TextScaled = true, + Font = Enum.Font.SourceSansBold, + + Visible = true +} +local recenterFrame = Util:Create "ImageLabel" { + Parent = countdownPanel:GetGUI(), + + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + + BackgroundTransparency = 1, + + Image = "rbxasset://textures/ui/VR/recenterFrame.png" +} + +countdownPanel:SetVisible(false) + +local isCountingDown = false +local function cancelCountdown() + isCountingDown = false + countdownPanel:SetVisible(false) +end + +VRHub.ModuleOpened.Event:connect(function(moduleName) + if moduleName ~= RecenterModule.ModuleName then + local module = VRHub:GetModule(moduleName) + if module.VRIsExclusive then + cancelCountdown() + end + end +end) + +function RecenterModule:SetVisible(value) + visible = value + + if visible then + if isCountingDown then + cancelCountdown() + VRHub:FireModuleClosed(RecenterModule.ModuleName) + return + else + VRHub:FireModuleOpened(RecenterModule.ModuleName) + end + + spawn(function() + isCountingDown = true + countdownPanel:SetVisible(true) + + ContextActionService:BindCoreAction(cancelShortcutName, function(actionName, inputState, inputObj) + if inputState == Enum.UserInputState.Begin then + cancelCountdown() + end + end, false, Enum.KeyCode.ButtonB) + + for i = 3, 1, -1 do + if isCountingDown then + countdown.Text = tostring(i) + wait(1) + end + end + + if isCountingDown then + InputService:RecenterUserHeadCFrame() + end + + countdownPanel:SetVisible(false) + isCountingDown = false + + ContextActionService:UnbindCoreAction(cancelShortcutName) + + VRHub:FireModuleClosed(RecenterModule.ModuleName) + end) + else + cancelCountdown() + VRHub:FireModuleClosed(RecenterModule.ModuleName) + end +end + +function RecenterModule:IsVisible() + return visible +end + +return RecenterModule \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/VR/UserGui.lua b/Client2018/content/scripts/CoreScripts/Modules/VR/UserGui.lua new file mode 100644 index 0000000..f76ae34 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/VR/UserGui.lua @@ -0,0 +1,75 @@ +local VRService = game:GetService("VRService") +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui.RobloxGui +local Panel3D = require(RobloxGui.Modules.VR.Panel3D) +local VRHub = require(RobloxGui.Modules.VR.VRHub) +local VRKeyboard = require(RobloxGui.Modules.VR.VirtualKeyboard) + +local UserGuiModule = {} +UserGuiModule.ModuleName = "UserGui" +UserGuiModule.KeepVRTopbarOpen = false +UserGuiModule.VRIsExclusive = false +UserGuiModule.VRClosesNonExclusive = false +VRHub:RegisterModule(UserGuiModule) + +local userGuiPanel = Panel3D.Get(UserGuiModule.ModuleName) +userGuiPanel:SetType(Panel3D.Type.Standard) +userGuiPanel:ResizeStuds(4, 4, 128) +userGuiPanel:SetVisible(false) + +VRHub.ModuleOpened.Event:connect(function(moduleName) + if moduleName ~= UserGuiModule.ModuleName then + local module = VRHub:GetModule(moduleName) + if module.VRClosesNonExclusive and userGuiPanel:IsVisible() then + UserGuiModule:SetVisible(false) + end + end +end) + +local KeyboardOpen = false +local GuiVisible = false + +function UserGuiModule:SetVisible(visible) + GuiVisible = visible + userGuiPanel:SetVisible(GuiVisible) + if GuiVisible then + VRHub:FireModuleOpened(UserGuiModule.ModuleName) + else + VRHub:FireModuleClosed(UserGuiModule.ModuleName) + end + + -- We need to hide the UserGui when typing on the keyboard so that the textbox doesn't sink events from the keyboard + local showGui = GuiVisible and not KeyboardOpen + CoreGui:SetUserGuiRendering(true, showGui and userGuiPanel:GetPart() or nil, Enum.NormalId.Front) +end + +function UserGuiModule:IsVisible() + return GuiVisible +end + +function UserGuiModule:Update() + self:SetVisible(GuiVisible) +end + +local function OnVREnabledChanged() + if not VRService.VREnabled then + userGuiPanel:SetVisible(false) + CoreGui:SetUserGuiRendering(false, nil, Enum.NormalId.Front) + end +end +VRService:GetPropertyChangedSignal("VREnabled"):connect(OnVREnabledChanged) +OnVREnabledChanged() + +VRKeyboard.OpenedEvent:connect(function() + KeyboardOpen = true + UserGuiModule:Update() +end) + +VRKeyboard.ClosedEvent:connect(function() + KeyboardOpen = false + UserGuiModule:Update() +end) + +UserGuiModule:SetVisible(true) + +return UserGuiModule diff --git a/Client2018/content/scripts/CoreScripts/Modules/VR/VRControllerModel.lua b/Client2018/content/scripts/CoreScripts/Modules/VR/VRControllerModel.lua new file mode 100644 index 0000000..083b06d --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/VR/VRControllerModel.lua @@ -0,0 +1,159 @@ +local VRService = game:GetService("VRService") +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local CoreGui = game:GetService("CoreGui") + +local RobloxGui = CoreGui.RobloxGui +local ViveController = require(RobloxGui.Modules.VR.Controllers.ViveController) + +local LocalPlayer = Players.LocalPlayer +while not LocalPlayer do + Players.Changed:wait() + LocalPlayer = Players.LocalPlayer +end + +local VRControllerModel = {} +VRControllerModel.__index = VRControllerModel + +function VRControllerModel.new(userCFrame) + local self = setmetatable({}, VRControllerModel) + self.userCFrame = userCFrame + self.enabled = false + self.currentModel = nil + self.currentVRDeviceName = nil + self.modelIsInWorkspace = false + + self.onVRDeviceChangedConn = nil + self.onCurrentCameraChangedConn = nil + self.onUserCFrameEnabledChangedConn = nil + self.onInputBeganConn = nil + self.onInputChangedConn = nil + self.onInputEndedConn = nil + + return self +end + +function VRControllerModel:setModelType(vrDeviceName) + if vrDeviceName ~= self.currentVRDeviceName then + self.currentVRDeviceName = vrDeviceName + + if self.currentModel then + self.currentModel:destroy() + self.currentModel = nil + end + if self.currentVRDeviceName:match("Vive") then + self.currentModel = ViveController.new(self.userCFrame) + elseif self.currentVRDeviceName:match("Oculus") then + --todo: add an Oculus touch controller model + --self.currentModel = OculusTouchController.new(self.userCFrame) + end + + --If the controller is enabled, put the model into the workspace + if self.enabled then + self:setModelInWorkspace(VRService:GetUserCFrameEnabled(self.userCFrame)) + end + end +end + +function VRControllerModel:setModelInWorkspace(inWorkspace) + if not self.currentModel then + return + end + if inWorkspace ~= self.modelIsInWorkspace then + self.modelIsInWorkspace = inWorkspace + if self.modelIsInWorkspace then + local camera = workspace.CurrentCamera + local folder = camera:FindFirstChild("VRCoreEffectParts") + if folder then + self.currentModel.model.Parent = folder + end + else + self.currentModel.model.Parent = nil + end + end +end + +function VRControllerModel:setEnabled(enabled) + if enabled ~= self.enabled then + self.enabled = enabled + + if self.enabled then + --Connect events + self.onVRDeviceChangedConn = VRService:GetPropertyChangedSignal("VRDeviceName"):connect(function() + self:setModelType(VRService.VRDeviceName) + end) + self:setModelType(VRService.VRDeviceName) + + self.onCurrentCameraChangedConn = workspace:GetPropertyChangedSignal("CurrentCamera"):connect(function() + self:setModelInWorkspace(VRService:GetUserCFrameEnabled(self.userCFrame)) + end) + + self.onUserCFrameEnabledChangedConn = VRService.UserCFrameEnabled:connect(function(userCFrame, enabled) + if userCFrame == self.userCFrame then + self:setModelInWorkspace(enabled) + end + end) + + self.onInputBeganConn = UserInputService.InputBegan:connect(function(...) self:onInputBegan(...) end) + self.onInputChangedConn = UserInputService.InputChanged:connect(function(...) self:onInputChanged(...) end) + self.onInputEndedConn = UserInputService.InputEnded:connect(function(...) self:onInputEnded(...) end) + + --Put the model in the workspace + self:setModelInWorkspace(VRService:GetUserCFrameEnabled(self.userCFrame)) + else + --Disconnect events + if self.onVRDeviceChangedConn then self.onVRDeviceChangedConn:disconnect() self.onVRDeviceChangedConn = nil end + if self.onCurrentCameraChangedConn then self.onCurrentCameraChangedConn:disconnect() self.onCurrentCameraChangedConn = nil end + if self.onUserCFrameEnabledChangedConn then self.onUserCFrameEnabledChangedConn:disconnect() self.onUserCFrameEnabledChangedConn = nil end + if self.onInputBeganConn then self.onInputBeganConn:disconnect() self.onInputBeganConn = nil end + if self.onInputChangedConn then self.onInputChangedConn:disconnect() self.onInputChangedConn = nil end + if self.onInputEndedConn then self.onInputEndedConn:disconnect() self.onInputEndedConn = nil end + + --Remove the model from the workspace + if self.currentModel then + self:setModelInWorkspace(false) + end + end + end +end + +function VRControllerModel:update(dt) + if not self.enabled then + return + end + + if self.currentModel then + local camera = workspace.CurrentCamera + + local cameraCFrame = camera.CFrame + local controllerCFrame = VRService:GetUserCFrame(self.userCFrame) + local cframe = cameraCFrame * controllerCFrame + self.currentModel:setCFrame(cframe) + end +end + +function VRControllerModel:onInputBegan(inputObject, wasProcessed) + if not self.enabled or not self.modelIsInWorkspace then + return + end + if self.currentModel then + self.currentModel:onInputBegan(inputObject) + end +end + +function VRControllerModel:onInputChanged(inputObject, wasProcessed) + if not self.enabled or not self.modelIsInWorkspace then + return + end + if self.currentModel then + self.currentModel:onInputChanged(inputObject) + end +end + +function VRControllerModel:onInputEnded(inputObject, wasProcessed) + if self.currentModel then + self.currentModel:onInputEnded(inputObject) + end +end + +return VRControllerModel \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/VR/VRHub.lua b/Client2018/content/scripts/CoreScripts/Modules/VR/VRHub.lua new file mode 100644 index 0000000..d972ee1 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/VR/VRHub.lua @@ -0,0 +1,181 @@ +--Modules/VR/VRHub.lua +--Handles all global VR state that isn't built into a specific module. +--Written by 0xBAADF00D (Kyle) on 6/10/16 +local StarterGui = game:GetService("StarterGui") +local VRService = game:GetService("VRService") +local RunService = game:GetService("RunService") +local HttpService = game:GetService("HttpService") +local CoreGui = game:GetService("CoreGui") +local UserInputService = game:GetService("UserInputService") + +local RobloxGui = CoreGui.RobloxGui +local Util = require(RobloxGui.Modules.Settings.Utility) + +local LaserPointer = require(RobloxGui.Modules.VR.LaserPointer) +local VRControllerModel = require(RobloxGui.Modules.VR.VRControllerModel) + +local VRHub = {} +local RegisteredModules = {} +local OpenModules = {} + +--VR Setup +local vrUpdateRenderstepName = HttpService:GenerateGUID(true) + +VRHub.LaserPointer = nil + +VRHub.ControllerModelsEnabled = false +VRHub.LeftControllerModel = nil +VRHub.RightControllerModel = nil + +StarterGui:RegisterSetCore("VRLaserPointerMode", function(mode) + if not VRHub.LaserPointer then + return + end + if not mode or not tostring(mode) then + return + end + VRHub.LaserPointer:setMode(LaserPointer.Mode[tostring(mode)] or LaserPointer.Mode.Disabled) +end) + +local function enableControllerModels(enabled) + if enabled ~= VRHub.ControllerModelsEnabled then + VRHub.ControllerModelsEnabled = enabled + + if enabled then + if not VRHub.LeftControllerModel then + VRHub.LeftControllerModel = VRControllerModel.new(Enum.UserCFrame.LeftHand) + end + VRHub.LeftControllerModel:setEnabled(true) + + if not VRHub.RightControllerModel then + VRHub.RightControllerModel = VRControllerModel.new(Enum.UserCFrame.RightHand) + end + VRHub.RightControllerModel:setEnabled(true) + else + if VRHub.LeftControllerModel then + VRHub.LeftControllerModel:setEnabled(false) + end + if VRHub.RightControllerModel then + VRHub.RightControllerModel:setEnabled(false) + end + end + end +end +local enableControllerModelsSetByDeveloper = false +StarterGui:RegisterSetCore("VREnableControllerModels", function(enabled) + enableControllerModelsSetByDeveloper = true + enableControllerModels(enabled) +end) + +local start = tick() +local function onRenderSteppedLast() + local now = tick() + local dt = now - start + start = now + + if VRHub.LaserPointer then + VRHub.LaserPointer:update(dt) + end + + if VRHub.LeftControllerModel then + VRHub.LeftControllerModel:update(dt) + end + if VRHub.RightControllerModel then + VRHub.RightControllerModel:update(dt) + end +end + +local function onVREnabled(property) + if property ~= "VREnabled" then + return + end + + if VRService.VREnabled then + UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide + + if not VRHub.LaserPointer then + VRHub.LaserPointer = LaserPointer.new() + end + + --Check again in case creating the laser pointer gracefully failed + if VRHub.LaserPointer then + VRHub.LaserPointer:setMode(LaserPointer.Mode.Navigation) + end + if not enableControllerModelsSetByDeveloper then + enableControllerModels(true) + end + RunService:BindToRenderStep(vrUpdateRenderstepName, Enum.RenderPriority.Last.Value, onRenderSteppedLast) + else + if VRHub.LaserPointer then + VRHub.LaserPointer:setMode(LaserPointer.Mode.Disabled) + end + RunService:UnbindFromRenderStep(vrUpdateRenderstepName) + end +end +onVREnabled("VREnabled") +VRService.Changed:connect(onVREnabled) + +--VRHub API +function VRHub:RegisterModule(module) + RegisteredModules[module.ModuleName] = module +end + +function VRHub:GetModule(moduleName) + return RegisteredModules[moduleName] +end + +function VRHub:IsModuleOpened(moduleName) + return OpenModules[moduleName] ~= nil +end + +function VRHub:GetOpenedModules() + local result = {} + + for _, openModule in pairs(OpenModules) do + table.insert(result, openModule) + end + + return result +end + +VRHub.ModuleOpened = Util:Create "BindableEvent" { + Name = "VRModuleOpened" +} +--Wrapper function to document the arguments to the event +function VRHub:FireModuleOpened(moduleName) + if not RegisteredModules[moduleName] then + error("Tried to open module that is not registered: " .. moduleName) + end + + if OpenModules[moduleName] ~= RegisteredModules[moduleName] then + OpenModules[moduleName] = RegisteredModules[moduleName] + VRHub.ModuleOpened:Fire(moduleName) + end +end + +VRHub.ModuleClosed = Util:Create "BindableEvent" { + Name = "VRModuleClosed" +} +--Wrapper function to document the arguments to the event +function VRHub:FireModuleClosed(moduleName) + if not RegisteredModules[moduleName] then + error("Tried to close module that is not registered: " .. moduleName) + end + + if OpenModules[moduleName] ~= nil then + OpenModules[moduleName] = nil + VRHub.ModuleClosed:Fire(moduleName) + end +end + +function VRHub:KeepVRTopbarOpen() + for moduleName, openModule in pairs(OpenModules) do + if openModule.KeepVRTopbarOpen then + return true + end + end + return false +end + +return VRHub \ No newline at end of file diff --git a/Client2018/content/scripts/CoreScripts/Modules/VR/VirtualKeyboard.lua b/Client2018/content/scripts/CoreScripts/Modules/VR/VirtualKeyboard.lua new file mode 100644 index 0000000..2c7881f --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/Modules/VR/VirtualKeyboard.lua @@ -0,0 +1,1405 @@ +-- VirtualKeyboard.lua -- +-- Written by Kip Turner, copyright ROBLOX 2016 -- + + +local CoreGui = game:GetService('CoreGui') +local RunService = game:GetService('RunService') +local UserInputService = game:GetService('UserInputService') +local GuiService = game:GetService('GuiService') +local HttpService = game:GetService('HttpService') +local ContextActionService = game:GetService('ContextActionService') +local PlayersService = game:GetService('Players') +local SoundService = game:GetService('SoundService') +local TextService = game:GetService('TextService') + +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local Util = require(RobloxGui.Modules.Settings.Utility) + +local BACKGROUND_OPACITY = 0.3 +local NORMAL_KEY_COLOR = Color3.new(49/255,49/255,49/255) +local HOVER_KEY_COLOR = Color3.new(49/255,49/255,49/255) +local PRESSED_KEY_COLOR = Color3.new(0,162/255,1) +local SET_KEY_COLOR = Color3.new(0,162/255,1) + +local KEY_TEXT_COLOR = Color3.new(1,1,1) +---------------------------------------- KEYBOARD LAYOUT -------------------------------------- +local MINIMAL_KEYBOARD_LAYOUT = HttpService:JSONDecode([==[ +[ + [ + { + "a": 7, + "w": 0.8 + }, + "*", + "Q", + "W", + "E", + "R", + "T", + "Y", + "U", + "I", + "O", + "P", + { + "w": 1.8 + }, + "Delete" + ], + [ + { + "w": 1.6 + }, + "Caps", + "A", + "S", + "D", + "F", + "G", + "H", + "J", + "K", + "L", + "?", + { + "h": 2, + "w2": 2.4, + "h2": 1, + "x2": -1.4, + "y2": 1 + }, + "Enter" + ], + [ + { + "w": 2.2 + }, + "Shift", + "Z", + "X", + "C", + "V", + "B", + "N", + "M", + "." + ], + [ + { + "w": 2.2 + }, + "123/sym", + { + "w": 8 + }, + "", + { + "w": 2.4 + }, + "" + ] +] +]==]) + +local MINIMAL_KEYBOARD_LAYOUT_SYMBOLS = HttpService:JSONDecode([==[ +[ + [ + { + "a": 7, + "w": 0.8 + }, + "*", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + { + "w": 1.8 + }, + "Delete" + ], + [ + { + "w": 1.6 + }, + "!", + "@", + "#", + "$", + "%", + "^", + "&", + "(", + ")", + "=", + "?", + { + "h": 2, + "w2": 2.4, + "h2": 1, + "x2": -1.4, + "y2": 1 + }, + "Enter" + ], + [ + { + "w": 1.2 + }, + "/", + "-", + "+", + "_", + ":", + ";", + "'", + "\"", + ",", + "." + ], + [ + { + "w": 2.2 + }, + "abc", + { + "w": 8 + }, + "", + { + "w": 2.4 + }, + "" + ] +] +]==]) + + +---------------------------------------- END KEYBOARD LAYOUT -------------------------------------- + + +local VOICE_STATUS_CODE_ENUM = {} +do + local STATUS_CODES = + { + 'ASR_STATUS_OK', + 'ASR_STATUS_CANCELLED', + 'ASR_STATUS_UNKNOWN', + 'ASR_STATUS_INVALID_ARGUMENTS', + 'ASR_STATUS_DEADLINE_EXCEEDED', + 'ASR_STATUS_NOT_FOUND', + 'ASR_STATUS_ALREADY_EXISTS', + 'ASR_STATUS_PERMISSION_DENIED', + 'ASR_STATUS_UNAUTHENTICATED', + 'ASR_STATUS_RESOURCE_EXHAUSTED', + 'ASR_STATUS_FAILED_PRECONDITION', + 'ASR_STATUS_ABORTED', + 'ASR_STATUS_OUT_OF_RANGE', + 'ASR_STATUS_UNIMPLEMENTED', + 'ASR_STATUS_INTERNAL', + 'ASR_STATUS_UNAVAILABLE', + 'ASR_STATUS_DATA_LOSS', + -- last official google response + + -- Roblox statuses + 'ASR_STATUS_NOT_ENABLED', + 'ASR_STATUS_LOW_CONFIDENCE', + 'ASR_STATUS_INVALID_JSON' + }; + + for i, code in pairs(STATUS_CODES) do + VOICE_STATUS_CODE_ENUM[code] = i-1 + end +end + +local function tokenizeString(str, tokenChar) + local words = {} + for word in string.gmatch(str, '([^' .. tokenChar .. ']+)') do + table.insert(words, word) + end + return words +end + +local function ConvertFontSizeEnumToInt(fontSizeEnum) + local result = string.match(fontSizeEnum.Name, '%d+') + return (result and tostring(result)) or 12 +end + + +-- RayPlaneIntersection + +-- http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm +local function RayPlaneIntersection(ray, planeNormal, pointOnPlane) + planeNormal = planeNormal.unit + ray = ray.Unit + -- compute Pn (dot) Rd = Vd and check if Vd == 0 then we know ray is parallel to plane + local Vd = planeNormal:Dot(ray.Direction) + + -- could fuzzy equals this a little bit to account for imprecision or very close angles to zero + if Vd == 0 then -- parallel, no intersection + return nil + end + + local V0 = planeNormal:Dot(pointOnPlane - ray.Origin) + local t = V0 / Vd + + if t < 0 then --plane is behind ray origin, and thus there is no intersection + return nil + end + + return ray.Origin + ray.Direction * t +end + +function Clamp(low, high, input) + return math.max(low, math.min(high, input)) +end + +-- No rotation as of yet +local function PointInGuiObject(object, x, y) + local minPt = object.AbsolutePosition + local maxPt = object.AbsolutePosition + object.AbsoluteSize + if minPt.X <= x and maxPt.X >= x and minPt.Y <= y and maxPt.Y >= y then + return true + end + return false +end + +local function FindAncestorOfType(object, ancestorType) + if not object then return nil end + + local parent = object.Parent + if parent and parent:IsA(ancestorType) then + return parent + end + + return FindAncestorOfType(parent, ancestorType) +end + +local function ExtendedInstance(instance) + local this = {} + do + local mt = + { + __index = function (t, k) + return instance[k] + end; + + __newindex = function (t, k, v) + instance[k] = v + end; + } + setmetatable(this, mt) + end + return this +end + +local function CreateVRButton(instance) + local newButton = ExtendedInstance(instance) + + rawset(newButton, "OnEnter", function(self) + end) + rawset(newButton, "OnLeave", function(self) + end) + rawset(newButton, "OnDown", function(self) + end) + rawset(newButton, "OnUp", function(self) + end) + rawset(newButton, "ContainsPoint", function(self, x, y) + return PointInGuiObject(instance, x, y) + end) + rawset(newButton, "Update", function(self) + end) + + return newButton +end + +local selectionRing = Util:Create'ImageLabel' +{ + Name = 'SelectionRing'; + Size = UDim2.new(1, -6, 1, -6); + Position = UDim2.new(0, 4, 0, 3); + Image = 'rbxasset://textures/ui/menu/buttonHover.png'; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(94/2, 94/2, 94/2, 94/2); + BackgroundTransparency = 1; +} + +local KEY_ICONS = +{ + [""] = {Asset = "rbxasset://textures/ui/Keyboard/mic_icon.png", AspectRatio = 0.615}; +} + +local function CreateKeyboardKey(keyboard, layoutData, keyData) + local isSpecialShapeKey = layoutData['width2'] and layoutData['height2'] and layoutData['x2'] and layoutData['y2'] + + local newKeyElement = Util:Create'ImageButton' + { + Name = keyData[1]; + Position = UDim2.new(layoutData['x'], 0, layoutData['y'], 0); + Size = UDim2.new(layoutData['width'], 0, layoutData['height'], 0); + BorderSizePixel = 0; + Image = ""; + BackgroundTransparency = 1; + ZIndex = 1; + } + local keyText = Util:Create'TextLabel' + { + Name = "KeyText"; + Text = keyData[#keyData]; + Position = UDim2.new(0, -10, 0, -10); + Size = UDim2.new(1, 0, 1, 0); + Font = Enum.Font.SourceSansBold; + FontSize = Enum.FontSize.Size96; + TextColor3 = KEY_TEXT_COLOR; + BackgroundTransparency = 1; + Selectable = true; + ZIndex = 2; + Parent = newKeyElement; + } + local backgroundImage = Util:Create'Frame' + { + Name = 'KeyBackground'; + Size = UDim2.new(1,-10,1,-10); + Position = UDim2.new(0,-5,0,-5); + BackgroundColor3 = NORMAL_KEY_COLOR; + BackgroundTransparency = BACKGROUND_OPACITY; + BorderSizePixel = 0; + Parent = newKeyElement; + } + + local selectionObject = Util:Create'ImageLabel' + { + Name = 'SelectionObject'; + Size = UDim2.new(1,0,1,0); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png"; + ImageTransparency = 0; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(12,12,52,52); + BorderSizePixel = 0; + } + + newKeyElement.SelectionImageObject = Util:Create'ImageLabel' + { + Visible = false; + } + + -- Special silly enter key nonsense + local secondBackgroundImage = nil + local specialSelectionObject, specialSelectionObject2, specialSelectionObject3 = nil, nil, nil + if isSpecialShapeKey then + secondBackgroundImage = Util:Create'ImageButton' + { + Name = 'KeyBackground'; + Position = UDim2.new(layoutData['x2'] / layoutData['width'], -5, layoutData['y2'] / layoutData['height'], -5); + Size = UDim2.new(layoutData['width2'] / layoutData['width'], 0, layoutData['height2'] / layoutData['height'], -10); + BackgroundColor3 = NORMAL_KEY_COLOR; + BackgroundTransparency = BACKGROUND_OPACITY; + BorderSizePixel = 0; + AutoButtonColor = false; + SelectionImageObject = newKeyElement.SelectionImageObject; + Parent = newKeyElement; + } + if layoutData['x2'] <= 0 then + keyText.Size = secondBackgroundImage.Size - UDim2.new(0,10,0,0) + keyText.Position = secondBackgroundImage.Position + secondBackgroundImage.Size = secondBackgroundImage.Size - UDim2.new(1,0,0,0) + end + + do + specialSelectionObject = Util:Create'Frame' + { + Name = 'SpecialSelectionObject'; + Size = UDim2.new(1,0,0.5,0); + Position = UDim2.new(0,0,0.5,0); + BackgroundTransparency = 1; + ClipsDescendants = true; + Util:Create'ImageLabel' + { + Name = 'Borders'; + Position = UDim2.new(-1,0,-1,0); + Size = UDim2.new(2,0,2,0); + BackgroundTransparency = 1; + Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png"; + ImageTransparency = 0; + ScaleType = Enum.ScaleType.Slice; + SliceCenter = Rect.new(12,12,52,52); + }; + } + specialSelectionObject2 = specialSelectionObject:Clone() + specialSelectionObject2.Size = UDim2.new(1,0,0.5,5) + specialSelectionObject2.Position = UDim2.new(0,0,0,0) + specialSelectionObject2.Borders.Size = UDim2.new(1,0,1,30) + specialSelectionObject2.Borders.Position = UDim2.new(0,0,0,0) + + specialSelectionObject3 = specialSelectionObject:Clone() + specialSelectionObject3.Size = UDim2.new(1,5,1,0) + specialSelectionObject3.Position = UDim2.new(0,0,0,0) + specialSelectionObject3.Borders.Size = UDim2.new(1,30,1,0) + specialSelectionObject3.Borders.Position = UDim2.new(0,0,0,0) + end + -- End of nonsense + end + + local newKey = CreateVRButton(newKeyElement) + + local hovering = false + local pressed = false + local isAlpha = #keyData == 1 and type(keyData[1]) == 'string' and #keyData[1] == 1 and + string.byte(keyData[1]) >= string.byte("A") and string.byte(keyData[1]) <= string.byte("z") + + local icon = nil + if keyData[1] and KEY_ICONS[keyData[1]] then + keyText.Visible = false + icon = Util:Create'ImageLabel' + { + Name = 'KeyIcon'; + Size = UDim2.new(KEY_ICONS[keyData[1]].AspectRatio, -20, 1, -20); + SizeConstraint = Enum.SizeConstraint.RelativeYY; + BackgroundTransparency = 1; + Image = KEY_ICONS[keyData[1]].Asset; + Parent = backgroundImage; + } + + local function onChanged(prop) + if prop == 'AbsoluteSize' then + icon.Position = UDim2.new(0.5,-icon.AbsoluteSize.X/2,0.5,-icon.AbsoluteSize.Y/2); + end + end + icon.Changed:connect(onChanged) + onChanged('AbsoluteSize') + end + + local function onClicked() + local keyValue = nil + local currentKeySetting = newKey:GetCurrentKeyValue() + + if currentKeySetting == 'Shift' then + keyboard:SetShift(not keyboard:GetShift()) + elseif currentKeySetting == 'Caps' then + keyboard:SetCaps(not keyboard:GetCaps()) + elseif currentKeySetting == 'Enter' then + keyboard:SubmitText(true, true) + elseif currentKeySetting == 'Delete' then + keyboard:BackspaceAtCursor() + elseif currentKeySetting == "123/sym" then + keyboard:SetCurrentKeyset(2) + elseif currentKeySetting == "abc" then + keyboard:SetCurrentKeyset(1) + elseif currentKeySetting == "" then + keyboard:SetVoiceMode(true) + elseif currentKeySetting == 'Tab' then + keyValue = '\t' + else + keyValue = currentKeySetting + end + + if keyValue ~= nil then + keyboard:SubmitCharacter(keyValue, isAlpha) + end + end + + local function setKeyColor(newColor, hovering) + backgroundImage.BackgroundColor3 = newColor + if secondBackgroundImage then + secondBackgroundImage.BackgroundColor3 = newColor + end + if isSpecialShapeKey then + specialSelectionObject.Parent = hovering and backgroundImage or nil + specialSelectionObject2.Parent = hovering and backgroundImage or nil + specialSelectionObject3.Parent = hovering and secondBackgroundImage or nil + else + selectionObject.Parent = hovering and backgroundImage or nil + end + end + + local function update() + local currentKey = newKey:GetCurrentKeyValue() + + if pressed then + setKeyColor(PRESSED_KEY_COLOR, false) + elseif hovering then + setKeyColor(HOVER_KEY_COLOR, true) + elseif currentKey == 'Caps' and keyboard:GetCaps() then + setKeyColor(SET_KEY_COLOR, false) + elseif currentKey == 'Shift' and keyboard:GetShift() then + setKeyColor(SET_KEY_COLOR, false) + elseif currentKey == 'abc' then + setKeyColor(SET_KEY_COLOR, false) + else + setKeyColor(NORMAL_KEY_COLOR, false) + end + + if icon then + icon.ImageTransparency = 0.5 + end + + keyText.Text = newKey:GetCurrentKeyValue() + end + + local hoveringGuiElements = {} + + rawset(newKey, "OnEnter", function(self) + hovering = true + update() + end) + rawset(newKey, "OnLeave", function(self) + if not next(hoveringGuiElements) then + hovering = false + pressed = false + update() + end + end) + rawset(newKey, "OnDown", function(self) + pressed = true + update() + -- Fire the onclick when pressing down on the button; + -- pressing down and up on the same button is difficult + -- in VR because your head is constantly moving around + onClicked() + end) + rawset(newKey, "OnUp", function(self) + pressed = false + update() + end) + rawset(newKey, "GetCurrentKeyValue", function(self) + local shiftEnabled = keyboard:GetShift() + local capsEnabled = keyboard:GetCaps() + + if isAlpha then + if capsEnabled and shiftEnabled then + return string.lower(keyData[#keyData]) + elseif capsEnabled or shiftEnabled then + return keyData[1] + else + return string.lower(keyData[#keyData]) + end + end + + if shiftEnabled then + return keyData[1] + end + + return keyData[#keyData] + end) + rawset(newKey, "ContainsPoint", function(self, x, y) + return PointInGuiObject(backgroundImage, x, y) or + (secondBackgroundImage and PointInGuiObject(secondBackgroundImage, x, y)) + end) + rawset(newKey, "Update", function(self) + update() + end) + rawset(newKey, "GetInstance", function(self) + return newKeyElement + end) + + newKeyElement.MouseButton1Down:connect(function() newKey:OnDown() end) + newKeyElement.MouseButton1Up:connect(function() newKey:OnUp() end) + newKeyElement.SelectionGained:connect(function() hoveringGuiElements[newKeyElement] = true newKey:OnEnter() end) + newKeyElement.SelectionLost:connect(function() hoveringGuiElements[newKeyElement] = nil newKey:OnLeave() end) + -- For the time being, we will simulate onClick events in the OnDown() event + -- newKeyElement.MouseButton1Click:connect(function() onClicked() end) + if secondBackgroundImage then + -- For the time being, we will simulate onClick events in the OnDown() event + -- secondBackgroundImage.MouseButton1Click:connect(onClicked) + secondBackgroundImage.MouseButton1Down:connect(function() newKey:OnDown() end) + secondBackgroundImage.MouseButton1Up:connect(function() newKey:OnUp() end) + secondBackgroundImage.SelectionGained:connect(function() + hoveringGuiElements[secondBackgroundImage] = true + newKey:OnEnter() + end) + secondBackgroundImage.SelectionLost:connect(function() + hoveringGuiElements[secondBackgroundImage] = nil + newKey:OnLeave() + end) + end + + update() + + return newKey +end + +local function CreateBaseVoiceState() + local this = {} + this.Name = "Base" + + function this:TransitionFrom() + end + function this:TransitionTo() + end + + return this +end + +local function CreateListeningVoiceState() + local this = CreateBaseVoiceState() + + this.Name = "Listening" + + function this:TransitionTo() + pcall(function() SoundService:BeginRecording() end) + end + + return this +end + +local function CreateProcessingVoiceState() + local this = CreateBaseVoiceState() + + this.Name = "Processing" + + local finished = false + local result = nil + + function this:TransitionTo() + coroutine.wrap(function() + pcall(function() result = SoundService:EndRecording() end) + finished = true + end)() + end + + function this:GetResultAsync() + while not finished do + wait() + end + return result + end + + return this +end + +local function CreateWaitingVoiceState() + local this = CreateBaseVoiceState() + + this.Name = "Waiting" + + return this +end + +local VoiceTransitions = {Listening = {Processing = true}, Processing = {Waiting = true}, Waiting = {Listening = true}} + +local VoiceToTextFSM = {} +do + VoiceToTextFSM.CurrentState = CreateWaitingVoiceState() + + local stateTransitionedSignal = Instance.new('BindableEvent') + + function VoiceToTextFSM:TransitionState(newState) + -- If it is a new state then lets cleanup and activate it + if VoiceTransitions[self.CurrentState.Name][newState.Name] then + self.CurrentState:TransitionFrom() + self.CurrentState = newState + self.CurrentState:TransitionTo() + stateTransitionedSignal:Fire(self.CurrentState) + return true + end + return false + end + + function VoiceToTextFSM:GetCurrentState() + return self.CurrentState + end + + VoiceToTextFSM.StateTransitionedEvent = stateTransitionedSignal.Event +end + + + +local function ConstructKeyboardUI(keyboardLayoutDefinitions) + local Panel3D = require(RobloxGui.Modules.VR.Panel3D) + local panel = Panel3D.Get("Keyboard") + panel:SetVisible(false) + + local buttons = {} + + local keyboardContainer = Util:Create'Frame' + { + Name = 'VirtualKeyboard'; + Size = UDim2.new(1, 0, 1, 0); + Position = UDim2.new(0, 0, 0, 0); + BackgroundTransparency = 1; + Active = true; + Visible = false; + }; + + local textEntryBackground = Util:Create'ImageLabel' + { + Name = 'TextEntryBackground'; + Size = UDim2.new(0.5,0,0.125,0); + Position = UDim2.new(0.25,0,0,0); + Image = ""; + BackgroundTransparency = 0.5; + BackgroundColor3 = Color3.new(31/255,31/255,31/255); + BorderSizePixel = 0; + ClipsDescendants = true; + Parent = keyboardContainer; + } + local textfieldBackground = Util:Create'Frame' + { + Name = 'TextfieldBackground'; + Position = UDim2.new(0,2,0,2); + Size = UDim2.new(1, -4, 1, -4); + BackgroundTransparency = 0; + BackgroundColor3 = Color3.new(209/255,216/255,221/255); + BorderSizePixel = 0; + Visible = true; + Parent = textEntryBackground; + }; + local textEntryField = Util:Create'TextButton' + { + Name = "TextEntryField"; + Text = ""; + Position = UDim2.new(0,4,0,4); + Size = UDim2.new(1, -8, 1, -8); + Font = Enum.Font.SourceSans; + FontSize = Enum.FontSize.Size60; + TextXAlignment = Enum.TextXAlignment.Left; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Parent = textfieldBackground; + } + local textfieldCursor = Util:Create'Frame' + { + Name = 'TextfieldCursor'; + Size = UDim2.new(0, 5, 0.9, 0); + Position = UDim2.new(0, 0, 0.05, 0); + BackgroundTransparency = 0; + BackgroundColor3 = SET_KEY_COLOR; + BorderSizePixel = 0; + Visible = true; + ZIndex = 2; + Parent = textEntryField; + }; + + local closeButtonElement = Util:Create'ImageButton' + { + Name = 'CloseButton'; + Size = UDim2.new(0.075,-10,0.198,-10); + Position = UDim2.new(0,-5,0,-35); + Image = "rbxasset://textures/ui/Keyboard/close_button_background.png"; + BackgroundTransparency = 1; + AutoButtonColor = false; + Parent = keyboardContainer; + } + do + closeButtonElement.SelectionImageObject = Util:Create'ImageLabel' + { + Name = 'Selection'; + Size = UDim2.new(0.9,0,0.9,0); + Position = UDim2.new(0.05,0,0.05,0); + Image = "rbxasset://textures/ui/Keyboard/close_button_selection.png"; + BackgroundTransparency = 1; + } + Util:Create'ImageLabel' + { + Name = 'Icon'; + Size = UDim2.new(0.5,0,0.5,0); + Position = UDim2.new(0.25,0,0.25,0); + Image = "rbxasset://textures/ui/Keyboard/close_button_icon.png"; + BackgroundTransparency = 1; + Parent = closeButtonElement; + } + end + local closeButton = CreateVRButton(closeButtonElement) + table.insert(buttons, closeButton) + + local voiceRecognitionContainer = Util:Create'Frame' + { + Name = 'VoiceRecognitionContainer'; + Size = UDim2.new(1, 0, 0.85, 0); + Position = UDim2.new(0, 0, 0.15, 0); + BackgroundTransparency = 1; + Active = true; + Visible = false; + Parent = keyboardContainer; + }; + do + local voiceRecognitionBackground1 = Util:Create'Frame' + { + Name = 'voiceRecognitionBackground1'; + Size = UDim2.new(1, 0, 0.75, 0); + Position = UDim2.new(0, 0, 0, 0); + BackgroundColor3 = NORMAL_KEY_COLOR; + BackgroundTransparency = BACKGROUND_OPACITY; + BorderSizePixel = 0; + Active = true; + Parent = voiceRecognitionContainer; + }; + local voiceRecognitionBackground2 = voiceRecognitionBackground1:Clone() + voiceRecognitionBackground2.Size = UDim2.new(1 - 0.2, 0, 0.25, 0) + voiceRecognitionBackground2.Position = UDim2.new(0, 0, 0.75, 0) + voiceRecognitionBackground2.Parent = voiceRecognitionContainer + end + + local voiceDoneButton = CreateVRButton(Util:Create'TextButton' + { + Name = 'DoneButton'; + Size = UDim2.new(0.2, -5, 0.25, -5); + Position = UDim2.new(1 - 0.2, 5, 0.75, 5); + Text = "Done"; + BackgroundColor3 = SET_KEY_COLOR; + Font = Enum.Font.SourceSansBold; + FontSize = Enum.FontSize.Size96; + TextColor3 = KEY_TEXT_COLOR; + BackgroundTransparency = 0; + AutoButtonColor = false; + BorderSizePixel = 0; + Parent = voiceRecognitionContainer; + }) + table.insert(buttons, voiceDoneButton) + + local voiceProcessingStatus = Util:Create'TextLabel' + { + Name = 'VoiceProcessingStatus'; + Size = UDim2.new(0, 0, 0, 0); + Position = UDim2.new(0.5, 0, 0.33, 0); + Text = ""; + Font = Enum.Font.SourceSansBold; + FontSize = Enum.FontSize.Size96; + TextColor3 = KEY_TEXT_COLOR; + BackgroundTransparency = 1; + BorderSizePixel = 0; + Parent = voiceRecognitionContainer; + } + + local function CreateVoiceVisualizerWidget() + local this = {} + + local bars = {} + + local numOfBars = 50 + local numOfWaves = 4 + local waveSpeed = 2.5 + + local container = Util:Create'Frame' + { + Name = 'VoiceVisualizerContainer'; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + } + this.Container = container + + for i = 1, numOfBars do + local bar = Util:Create'Frame' + { + Name = 'Bar'; + Size = UDim2.new(1/numOfBars, -4, 1, 0); + Position = UDim2.new(i/numOfBars, 0, 0, 0); + BackgroundTransparency = 0; + BackgroundColor3 = KEY_TEXT_COLOR; + Parent = container; + } + table.insert(bars, bar) + end + + function this:StartAnimation() + RunService:UnbindFromRenderStep("VoiceVisualizerWidget") + RunService:BindToRenderStep("VoiceVisualizerWidget", Enum.RenderPriority.First.Value, + function() + local movementPerBar = (numOfWaves*2*math.pi) / numOfBars + for i, bar in pairs(bars) do + local height = math.abs(math.sin(tick() * waveSpeed + i * movementPerBar)) + math.abs(math.cos(tick() * waveSpeed + i * movementPerBar)) + height = ((height / 2) - 0.3) * (1/(1-0.3)) + bar.Size = UDim2.new(1/numOfBars, -4, height, 0) + bar.Position = UDim2.new(i/numOfBars, 0, (1-height) / 2, 0) + end + end) + end + + function this:StopAnimation() + RunService:UnbindFromRenderStep("VoiceVisualizerWidget") + end + + return this + end + + local voiceVisualizer = CreateVoiceVisualizerWidget() + voiceVisualizer.Container.Parent = voiceRecognitionContainer + voiceVisualizer.Container.Size = UDim2.new(0.5,0,0.4,0) + voiceVisualizer.Container.Position = UDim2.new(0.25,0,0.4,0) + + local newKeyboard = ExtendedInstance(keyboardContainer) + + local keyboardOptions = nil + local keysets = {} + + local capsLockEnabled = false + local shiftEnabled = false + + local textfieldCursorPosition = 0 + + local openedEvent = Instance.new('BindableEvent') + local closedEvent = Instance.new('BindableEvent') + local opened = false + + local function SetTextFieldCursorPosition(newPosition) + textfieldCursorPosition = Clamp(0, #textEntryField.Text, newPosition) + if not textEntryField.TextFits then + textfieldCursorPosition = #textEntryField.Text + end + + local textSize = TextService:GetTextSize( + string.sub(textEntryField.Text, 1, textfieldCursorPosition), + ConvertFontSizeEnumToInt(textEntryField.FontSize), + textEntryField.Font, + textEntryField.AbsoluteSize) + textfieldCursor.Position = UDim2.new(0, textSize.x, textfieldCursor.Position.Y.Scale, textfieldCursor.Position.Y.Offset) + end + + local function UpdateTextEntryFieldText(newText) + textEntryField:SetTextFromInput(newText) + SetTextFieldCursorPosition(textfieldCursorPosition) + end + + local buffer = "" + local function getBufferText() + if keyboardOptions and keyboardOptions.TextBox then + return keyboardOptions.TextBox.Text + end + return buffer + end + local function setBufferText(newBufferText) + if keyboardOptions and keyboardOptions.TextBox then + keyboardOptions.TextBox.Text = newBufferText + elseif buffer ~= newBufferText then + buffer = newBufferText + UpdateTextEntryFieldText(buffer) + end + end + + local function calculateTextCursorPosition(x, y) + x = x - textEntryField.AbsolutePosition.x + y = y - textEntryField.AbsolutePosition.y + + for i = 1, #textEntryField.Text do + local textSize = TextService:GetTextSize( + string.sub(textEntryField.Text, 1, i), + ConvertFontSizeEnumToInt(textEntryField.FontSize), + textEntryField.Font, + textEntryField.AbsoluteSize) + if textSize.x > x then + return i - 1 + end + end + + return #textEntryField.Text + end + + local currentKeyset = nil + + rawset(newKeyboard, "OpenedEvent", openedEvent.Event) + rawset(newKeyboard, "ClosedEvent", closedEvent.Event) + + rawset(newKeyboard, "GetCurrentKeyset", function(self) + return keysets[currentKeyset] + end) + + rawset(newKeyboard, "SetCurrentKeyset", function(self, newKeyset) + if newKeyset ~= currentKeyset and keysets[newKeyset] ~= nil then + if keysets[currentKeyset] and keysets[currentKeyset].container then + keysets[currentKeyset].container.Visible = false + end + + currentKeyset = newKeyset + + if keysets[currentKeyset] and keysets[currentKeyset].container then + keysets[currentKeyset].container.Visible = true + end + end + end) + + rawset(newKeyboard, "SetVoiceMode", function(self, inVoiceMode) + -- current Speech to Text solution is no longer enabled. If we find a new service provider we can hook it up through here + inVoiceMode = false + + local currentKeysetObject = self:GetCurrentKeyset() + if currentKeysetObject and currentKeysetObject.container then + currentKeysetObject.container.Visible = not inVoiceMode + end + + voiceRecognitionContainer.Visible = inVoiceMode + + if inVoiceMode then + VoiceToTextFSM:TransitionState(CreateListeningVoiceState()) + end + end) + + rawset(newKeyboard, "GetCaps", function(self) + return capsLockEnabled + end) + + rawset(newKeyboard, "SetCaps", function(self, newCaps) + capsLockEnabled = newCaps + for _, key in pairs(self:GetCurrentKeyset().keys) do + key:Update() + end + end) + + rawset(newKeyboard, "GetShift", function(self) + return shiftEnabled + end) + + rawset(newKeyboard, "SetShift", function(self, newShift) + shiftEnabled = newShift + for _, key in pairs(self:GetCurrentKeyset().keys) do + key:Update() + end + end) + + local ignoreFocusedLost = false + + local textChangedConn = nil + local textBoxFocusLostConn = nil + local panelClosedConn = nil + + local function disconnectKeyboardEvents() + if textChangedConn then textChangedConn:disconnect() end + textChangedConn = nil + if textBoxFocusLostConn then textBoxFocusLostConn:disconnect() end + textBoxFocusLostConn = nil + if panelClosedConn then panelClosedConn:disconnect() end + panelClosedConn = nil + end + + rawset(newKeyboard, "Open", function(self, options) + if opened then return end + opened = true + + keyboardOptions = options + + self:SetCurrentKeyset(1) + self:SetVoiceMode(false) + keyboardContainer.Visible = true + + panel:ResizeStuds(5.9, 2.25, 320) + + local localCF = CFrame.new() + + disconnectKeyboardEvents() + if options.TextBox then + textChangedConn = options.TextBox.Changed:connect(function(prop) + if prop == 'Text' then + UpdateTextEntryFieldText(options.TextBox.Text) + end + end) + textBoxFocusLostConn = options.TextBox.FocusLost:connect(function(submitted) + if not ignoreFocusedLost then + self:Close(submitted) + end + end) + if options.TextBox.ClearTextOnFocus then + setBufferText("") + else + UpdateTextEntryFieldText(options.TextBox.Text) + end + + -- Find panel for 2d ui? + local textboxPanel = Panel3D.FindContainerOf(options.TextBox) + if textboxPanel then + panelClosedConn = Panel3D.OnPanelClosed.Event:connect(function(closedPanelName) + if closedPanelName == textboxPanel.name then + self:Close(false) + end + end) + + local textboxPosition = options.TextBox.AbsolutePosition + (Vector2.new(0.5, 1) * options.TextBox.AbsoluteSize) + local panelCF = textboxPanel:GetCFrameInCameraSpace() + localCF = panelCF * CFrame.new(textboxPanel:GetGuiPositionInPanelSpace(textboxPosition)) * CFrame.new(0, -panel.height * 0.65, 0.5) * CFrame.Angles(math.rad(-22.5), 0, 0) + else -- no panel! + local headForwardCF = Panel3D.GetHeadLookXZ(true) + localCF = headForwardCF * CFrame.Angles(math.rad(22.5), 0, 0) * CFrame.new(0, -1, -5) + end + else + setBufferText("") + end + + ContextActionService:BindCoreAction("VirtualKeyboardControllerInput", + function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + if inputObject.KeyCode == Enum.KeyCode.ButtonL1 then + SetTextFieldCursorPosition(textfieldCursorPosition - 1) + elseif inputObject.KeyCode == Enum.KeyCode.ButtonR1 then + SetTextFieldCursorPosition(textfieldCursorPosition + 1) + elseif inputObject.KeyCode == Enum.KeyCode.ButtonX then + self:BackspaceAtCursor() + elseif inputObject.KeyCode == Enum.KeyCode.ButtonY then + self:SubmitCharacter(" ", false) + elseif inputObject.KeyCode == Enum.KeyCode.ButtonL2 then + if currentKeyset then + -- Go to the next keyset + self:SetCurrentKeyset((currentKeyset % #keysets) + 1) + end + elseif inputObject.KeyCode == Enum.KeyCode.ButtonL3 then + self:SetCaps(not self:GetCaps()) + elseif inputObject.KeyCode == Enum.KeyCode.ButtonB then + self:Close(false) + end + end + end, + false, + Enum.KeyCode.ButtonL1, Enum.KeyCode.ButtonR1, Enum.KeyCode.ButtonL2, Enum.KeyCode.ButtonL3, Enum.KeyCode.ButtonX, Enum.KeyCode.ButtonY, Enum.KeyCode.ButtonR2, Enum.KeyCode.ButtonB) + + self.Parent = panel:GetGUI() + + panel:SetType(Panel3D.Type.Fixed, { CFrame = localCF }) + panel:SetCanFade(false) + panel:SetVisible(true, true) + panel:ForceShowUntilLookedAt() + + function panel:OnUpdate() + end + + openedEvent:Fire() + end) + + rawset(newKeyboard, "Close", function(self, submit) + submit = (submit == true) + + if not opened then return end + opened = false + + disconnectKeyboardEvents() + + ContextActionService:UnbindCoreAction("VirtualKeyboardControllerInput") + -- Clean-up + panel:OnMouseLeave() + panel:SetVisible(false, true) + keyboardContainer.Visible = false + + Panel3D.Get("Topbar3D"):SetVisible(true) + + self:SubmitText(submit, false) + closedEvent:Fire() + end) + + rawset(newKeyboard, "SubmitText", function(self, submit, keepKeyboardOpen) + local keyboardTextbox = keyboardOptions and keyboardOptions.TextBox + if keyboardTextbox then + if submit then + keyboardTextbox.Text = getBufferText() + end + -- Only keep text boxes open for coreguis, such as chat + local textboxPanel = Panel3D.FindContainerOf(keyboardTextbox) + local reopenKeyboard = keepKeyboardOpen and textboxPanel and textboxPanel.linkedTo == panel + + if reopenKeyboard then + ignoreFocusedLost = true + end + + keyboardTextbox:ReleaseFocus(submit) + + if reopenKeyboard then + keyboardTextbox:CaptureFocus() + ignoreFocusedLost = false + end + end + end) + + rawset(newKeyboard, "GetCurrentOptions", function(self) + return keyboardOptions + end) + + rawset(newKeyboard, "BackspaceAtCursor", function(self) + if textfieldCursorPosition >= 1 then + local bufferText = getBufferText() + local newBufferText = string.sub(bufferText, 1, textfieldCursorPosition - 1) .. string.sub(bufferText, textfieldCursorPosition + 1, #bufferText) + local newCursorPosition = textfieldCursorPosition - 1 + setBufferText(newBufferText) + SetTextFieldCursorPosition(newCursorPosition) + end + end) + + rawset(newKeyboard, "SubmitCharacter", function(self, character, isAnAlphaKey) + local bufferText = getBufferText() + local newBufferText = string.sub(bufferText, 1, textfieldCursorPosition) .. character .. string.sub(bufferText, textfieldCursorPosition + 1, #bufferText) + setBufferText(newBufferText) + SetTextFieldCursorPosition(textfieldCursorPosition + #character) + + if isAnAlphaKey and self:GetShift() then + self:SetShift(false) + end + end) + + do -- Parse input definition + for _, keyboardKeyset in pairs(keyboardLayoutDefinitions) do + local keys = {} + local keyboardSizeConstrainer = Util:Create'Frame' + { + Name = 'KeyboardSizeConstrainer'; + Size = UDim2.new(1, 0, 1, -20); + Position = UDim2.new(0, 0, 0, 20); + BackgroundTransparency = 1; + Parent = keyboardContainer; + }; + + local maxWidth = 0 + local maxHeight = 0 + local y = 0 + for rowNum, rowData in pairs(keyboardKeyset) do + local x = 0 + local width = 1 + local height = 1 + local width2, height2, x2, y2; + for columnNum, columnData in pairs(rowData) do + if type(columnData) == 'table' then + if columnData['w'] then width = columnData['w'] end + if columnData['h'] then height = columnData['h'] end + if columnData['x'] then x = x + columnData['x'] end + if columnData['y'] then y = y + columnData['y'] end + if columnData['x2'] then x2 = columnData['x2'] end + if columnData['y2'] then y2 = columnData['y2'] end + if columnData['w2'] then width2 = columnData['w2'] end + if columnData['h2'] then height2 = columnData['h2'] end + elseif type(columnData) == 'string' then + if columnData == "" then + columnData = " " + end + -- put key + local key = CreateKeyboardKey( + newKeyboard, + {x = x, y = y, width = width, height = height, x2 = x2, y2 = y2, width2 = width2, height2 = height2}, + tokenizeString(columnData, '\n')) + table.insert(keys, key) + + x = x + width + maxWidth = math.max(maxWidth, x) + maxHeight = math.max(maxHeight, y + height) + -- reset for the next key + width = 1 + height = 1 + width2, height2, x2, y2 = nil, nil, nil, nil + end + end + y = y + 1 + end + + -- Fix the positions and sizes to fit in our KeyboardContainer + for _, element in pairs(keys) do + element.Position = UDim2.new(element.Position.X.Scale / maxWidth, 0, element.Position.Y.Scale / maxHeight, 0) + element.Size = UDim2.new(element.Size.X.Scale / maxWidth, 0, element.Size.Y.Scale / maxHeight, 0) + element.Parent = keyboardSizeConstrainer + end + + keyboardSizeConstrainer.SizeConstraint = Enum.SizeConstraint.RelativeXX + keyboardSizeConstrainer.Size = UDim2.new(1, 0, -maxHeight / maxWidth, 0) + keyboardSizeConstrainer.Position = UDim2.new(0, 0, 1, 0) + keyboardSizeConstrainer.Visible = false + + table.insert(keysets, {keys = keys, container = keyboardSizeConstrainer}) + end + newKeyboard:SetCurrentKeyset(1) + end + + textEntryField.MouseButton1Click:connect(function() + SetTextFieldCursorPosition(calculateTextCursorPosition(panel.lookAtPixel.X, panel.lookAtPixel.Y)) + end) + + closeButton.MouseButton1Click:connect(function() + newKeyboard:Close(false) + end) + + voiceDoneButton.MouseButton1Click:connect(function() + if VoiceToTextFSM:GetCurrentState().Name == "Listening" then + VoiceToTextFSM:TransitionState(CreateProcessingVoiceState()) + end + end) + + local function onVoiceProcessingStateChanged(newState) + if newState.Name == "Listening" then + voiceProcessingStatus.Text = "Listening..." + elseif newState.Name == "Processing" then + voiceProcessingStatus.Text = "Processing..." + elseif newState.Name == "Waiting" then + voiceProcessingStatus.Text = "Done" + end + + -- Get the result and put it into the textfield + if newState.Name == "Processing" then + coroutine.wrap(function() + voiceVisualizer:StopAnimation() + local result = newState:GetResultAsync() + if result and result["Status"] == VOICE_STATUS_CODE_ENUM.ASR_STATUS_OK then + setBufferText(result["Response"]) + else + voiceProcessingStatus.Text = "An error occurred, please try again." + wait(2) + end + VoiceToTextFSM:TransitionState(CreateWaitingVoiceState()) + end)() + elseif newState.Name == "Listening" then + voiceVisualizer:StartAnimation() + elseif newState.Name == "Waiting" then + newKeyboard:SetVoiceMode(false) + end + end + VoiceToTextFSM.StateTransitionedEvent:connect(onVoiceProcessingStateChanged) + onVoiceProcessingStateChanged(VoiceToTextFSM:GetCurrentState()) + + + return newKeyboard +end + + +local Keyboard = nil; +local function GetKeyboard() + if Keyboard == nil then + Keyboard = ConstructKeyboardUI({MINIMAL_KEYBOARD_LAYOUT, MINIMAL_KEYBOARD_LAYOUT_SYMBOLS}) + end + return Keyboard +end + + + +local VirtualKeyboardClass = {} + +function VirtualKeyboardClass:CreateVirtualKeyboardOptions(textbox) + local keyboardOptions = {} + + keyboardOptions.TextBox = textbox + + return keyboardOptions +end + +local VirtualKeyboardPlatform = false +do + -- iOS, Android and Xbox already have platform specific keyboards + local platform = UserInputService:GetPlatform() + VirtualKeyboardPlatform = platform == Enum.Platform.Windows or + platform == Enum.Platform.OSX or + platform == Enum.Platform.IOS or + platform == Enum.Platform.Android +end + + +function VirtualKeyboardClass:ShowVirtualKeyboard(virtualKeyboardOptions) + if VirtualKeyboardPlatform and UserInputService.VREnabled then + GetKeyboard():Open(virtualKeyboardOptions) + end +end + +function VirtualKeyboardClass:CloseVirtualKeyboard() + if VirtualKeyboardPlatform and UserInputService.VREnabled then + local currentKeyboard = GetKeyboard() + currentKeyboard:Close(false) + end +end + +VirtualKeyboardClass.OpenedEvent = GetKeyboard().OpenedEvent +VirtualKeyboardClass.ClosedEvent = GetKeyboard().ClosedEvent + + +if VirtualKeyboardPlatform then + UserInputService.TextBoxFocused:connect(function(textbox) + VirtualKeyboardClass:ShowVirtualKeyboard(VirtualKeyboardClass:CreateVirtualKeyboardOptions(textbox)) + end) + -- Don't have to hook up to TextBoxFocusReleased because we are already listening to that in keyboard +end + + +return VirtualKeyboardClass diff --git a/Client2018/content/scripts/CoreScripts/ServerCoreScripts/ServerSocialScript.lua b/Client2018/content/scripts/CoreScripts/ServerCoreScripts/ServerSocialScript.lua new file mode 100644 index 0000000..a5d0460 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/ServerCoreScripts/ServerSocialScript.lua @@ -0,0 +1,264 @@ +--[[ + // FileName: PlayerlistModule.lua + // Version 1.0 + // Written by: jmargh + // Description: Implements social features that need to be ran on the server + + // TODO + We need to get module script working on the server. When we get that working + This should be moved to a module, and http helper functions should be moved + to a utility module. +]] +local HttpService = game:GetService('HttpService') +local HttpRbxApiService = game:GetService('HttpRbxApiService') +local Players = game:GetService('Players') +local RobloxReplicatedStorage = game:GetService('RobloxReplicatedStorage') +local RunService = game:GetService('RunService') + +local GET_MULTI_FOLLOW = "user/multi-following-exists" + +-- Maximum amount of follow notifications that a player is allowed to send to another player. +local MAX_FOLLOW_NOTIFICATIONS_BETWEEN = 5 + +local PlayerToRelationshipMap = {} + +--[[ Remotes ]]-- +local RemoteEvent_FollowRelationshipChanged = Instance.new('RemoteEvent') +RemoteEvent_FollowRelationshipChanged.Name = "FollowRelationshipChanged" +RemoteEvent_FollowRelationshipChanged.Parent = RobloxReplicatedStorage + +local RemoteEvent_NewFollower = Instance.new("RemoteEvent") +RemoteEvent_NewFollower.Name = "NewFollower" +RemoteEvent_NewFollower.Parent = RobloxReplicatedStorage + +local RemoteFunc_GetFollowRelationships = Instance.new('RemoteFunction') +RemoteFunc_GetFollowRelationships.Name = "GetFollowRelationships" +RemoteFunc_GetFollowRelationships.Parent = RobloxReplicatedStorage + +local RemoteEvent_SetPlayerBlockList = Instance.new('RemoteEvent') +RemoteEvent_SetPlayerBlockList.Name = 'SetPlayerBlockList' +RemoteEvent_SetPlayerBlockList.Parent = RobloxReplicatedStorage + +local RemoteEvent_UpdatePlayerBlockList = Instance.new('RemoteEvent') +RemoteEvent_UpdatePlayerBlockList.Name = 'UpdatePlayerBlockList' +RemoteEvent_UpdatePlayerBlockList.Parent = RobloxReplicatedStorage + +--[[ Helper Functions ]]-- +local function decodeJSON(json) + local success, result = pcall(function() + return HttpService:JSONDecode(json) + end) + if not success then + print("decodeJSON() failed because", result, "Input:", json) + return nil + end + + return result +end + +local function rbxApiPostAsync(path, params, throttlePriority, contentType, httpType) + local success, result = pcall(function() + return HttpRbxApiService:PostAsync(path, params, throttlePriority, contentType, httpType) + end) + -- + if not success then + local label = string.format("%s: - path: %s, \njson: %s", tostring(result), tostring(path), tostring(params)) + return nil + end + + return decodeJSON(result) +end + +--[[ + // Return - table + Key: FollowingDetails + Value: Arrary of details + Key: UserId1 + Value: number - userId of new client + Key: UserId2 + Value: number - userId of other client + Key: User1FollowsUser2 + Value: boolean + Key: User2FollowsUser1 + Value: boolean +]] +local function getFollowRelationshipsAsync(uid) + if RunService:IsStudio() then + return + end + + local otherUserIdTable = {} + for _,player in pairs(Players:GetPlayers()) do + if player.UserId > 0 then + table.insert(otherUserIdTable, player.UserId) + end + end + + if #otherUserIdTable > 0 and uid and uid > 0 then + local jsonPostBody = { + userId = uid; + otherUserIds = otherUserIdTable; + } + jsonPostBody = HttpService:JSONEncode(jsonPostBody) + + if jsonPostBody then + return rbxApiPostAsync(GET_MULTI_FOLLOW, jsonPostBody, + Enum.ThrottlingPriority.Default, Enum.HttpContentType.ApplicationJson, + Enum.HttpRequestType.Players) + end + end +end + +local function createRelationshipObject(user1FollowsUser2, user2FollowsUser1) + local object = {} + object.IsFollower = user2FollowsUser1 + object.IsFollowing = user1FollowsUser2 + object.IsMutual = user1FollowsUser2 and user2FollowsUser1 + + return object +end + +local function updateAndNotifyClients(resultTable, newUserIdStr, newPlayer) + local followingDetails = resultTable["FollowingDetails"] + if followingDetails then + local relationshipTable = PlayerToRelationshipMap[newUserIdStr] or {} + + for i = 1, #followingDetails do + local detail = followingDetails[i] + local otherUserId = detail["UserId2"] + local otherUserIdStr = tostring(otherUserId) + + local followsOther = detail["User1FollowsUser2"] + local followsNewPlayer = detail["User2FollowsUser1"] + + relationshipTable[otherUserIdStr] = createRelationshipObject(followsOther, followsNewPlayer) + + -- update other use + local otherRelationshipTable = PlayerToRelationshipMap[otherUserIdStr] + if otherRelationshipTable then + local newRelationship = createRelationshipObject(followsNewPlayer, followsOther) + otherRelationshipTable[newUserIdStr] = newRelationship + + local otherPlayer = Players:GetPlayerByUserId(otherUserId) + if otherPlayer then + -- create single entry table (keep format same) and send to other client + local deltaTable = {} + deltaTable[newUserIdStr] = newRelationship + RemoteEvent_FollowRelationshipChanged:FireClient(otherPlayer, deltaTable) + end + end + end + + PlayerToRelationshipMap[newUserIdStr] = relationshipTable + RemoteEvent_FollowRelationshipChanged:FireClient(newPlayer, relationshipTable) + end +end + +--[[ Connections ]]-- +function RemoteFunc_GetFollowRelationships.OnServerInvoke(player) + local uid = player.UserId + local uidStr = tostring(player.UserId) + if uid and uid > 0 and PlayerToRelationshipMap[uidStr] then + return PlayerToRelationshipMap[uidStr] + else + return {} + end +end + +-- Map: { UserId -> { UserId -> NumberOfNotificationsSent } } +local FollowNotificationsBetweenMap = {} + +local function isPlayer(value) + return typeof(value) == "Instance" and value:IsA("Player") +end + +-- client fires event to server on new follow +RemoteEvent_NewFollower.OnServerEvent:connect(function(player1, player2, player1FollowsPlayer2) + if not isPlayer(player1) or not isPlayer(player2) or type(player1FollowsPlayer2) ~= "boolean" then + return + end + + local userId1 = tostring(player1.UserId) + local userId2 = tostring(player2.UserId) + + local user1map = PlayerToRelationshipMap[userId1] + local user2map = PlayerToRelationshipMap[userId2] + + local sentNotificationsMap = FollowNotificationsBetweenMap[userId1] + if sentNotificationsMap then + if sentNotificationsMap[userId2] then + sentNotificationsMap[userId2] = sentNotificationsMap[userId2] + 1 + if sentNotificationsMap[userId2] > MAX_FOLLOW_NOTIFICATIONS_BETWEEN then + -- This player is likely trying to spam the other player with notifications. + -- We won't send any more. + return + end + else + sentNotificationsMap[userId2] = 1 + end + end + + if user1map then + local relationTable = user1map[userId2] + if relationTable then + relationTable.IsFollowing = player1FollowsPlayer2 + relationTable.IsMutual = relationTable.IsFollowing and relationTable.IsFollower + + local delta = {} + delta[userId2] = relationTable + RemoteEvent_FollowRelationshipChanged:FireClient(player1, delta) + -- this should be updated, but current NotificationScript listens to this + if player1FollowsPlayer2 then + RemoteEvent_NewFollower:FireClient(player2, player1) + end + end + end + + if user2map then + local relationTable = user2map[userId1] + if relationTable then + relationTable.IsFollower = player1FollowsPlayer2 + relationTable.IsMutual = relationTable.IsFollowing and relationTable.IsFollower + + local delta = {} + delta[userId1] = relationTable + RemoteEvent_FollowRelationshipChanged:FireClient(player2, delta) + end + end +end) + +local function onPlayerAdded(newPlayer) + local uid = newPlayer.UserId + if uid > 0 then + local uidStr = tostring(uid) + FollowNotificationsBetweenMap[uidStr] = {} + local result = getFollowRelationshipsAsync(uid) + if result then + updateAndNotifyClients(result, uidStr, newPlayer) + end + end +end + +if settings():GetFFlag('HandlePlayerBlockListsInternalPermissive') == true then + RemoteEvent_SetPlayerBlockList.OnServerEvent:Connect(function(player, blockList) + player:AddToBlockList(blockList) + end) + + RemoteEvent_UpdatePlayerBlockList.OnServerEvent:Connect(function(player, userId, block) + player:UpdatePlayerBlocked(userId, block) + end) +end + +Players.PlayerAdded:connect(onPlayerAdded) +for _,player in pairs(Players:GetPlayers()) do + onPlayerAdded(player) +end + +Players.PlayerRemoving:connect(function(prevPlayer) + local uid = tostring(prevPlayer.UserId) + if PlayerToRelationshipMap[uid] then + PlayerToRelationshipMap[uid] = nil + FollowNotificationsBetweenMap[uid] = nil + end +end) + diff --git a/Client2018/content/scripts/CoreScripts/ServerStarterScript.lua b/Client2018/content/scripts/CoreScripts/ServerStarterScript.lua new file mode 100644 index 0000000..bab4305 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/ServerStarterScript.lua @@ -0,0 +1,99 @@ +--[[ + // Filename: ServerStarterScript.lua + // Version: 1.0 + // Description: Server core script that handles core script server side logic. +]]-- + +local runService = game:GetService('RunService') + +-- Prevent server script from running in Studio when not in run mode +while not runService:IsRunning() do + wait() +end + +--[[ Services ]]-- +local RobloxReplicatedStorage = game:GetService('RobloxReplicatedStorage') +local ScriptContext = game:GetService('ScriptContext') + +--[[ Add Server CoreScript ]]-- +ScriptContext:AddCoreScriptLocal("ServerCoreScripts/ServerSocialScript", script.Parent) + +--[[ Remote Events ]]-- +local RemoteEvent_SetDialogInUse = Instance.new("RemoteEvent") +RemoteEvent_SetDialogInUse.Name = "SetDialogInUse" +RemoteEvent_SetDialogInUse.Parent = RobloxReplicatedStorage + +local RemoteFunction_GetServerVersion = Instance.new("RemoteFunction") +RemoteFunction_GetServerVersion.Name = "GetServerVersion" +RemoteFunction_GetServerVersion.Parent = RobloxReplicatedStorage + +--[[ Event Connections ]]-- +local playerDialogMap = {} + +local freeCameraFlagSuccess, freeCameraFlagValue = pcall(function() return settings():GetFFlag("FreeCameraForAdmins") end) +local freeCameraFlag = (freeCameraFlagSuccess and freeCameraFlagValue) + +local function setDialogInUse(player, dialog, value, waitTime) + if typeof(dialog) ~= "Instance" or not dialog:IsA("Dialog") then + return + end + if type(value) ~= "boolean" then + return + end + if type(waitTime) ~= "number" and type(waitTime) ~= "nil" then + return + end + if typeof(player) ~= "Instance" or not player:IsA("Player") then + return + end + + if waitTime and waitTime ~= 0 then + wait(waitTime) + end + if dialog ~= nil then + dialog:SetPlayerIsUsing(player, value) + playerDialogMap[player] = value and dialog or nil + end +end +RemoteEvent_SetDialogInUse.OnServerEvent:connect(setDialogInUse) + +local function getServerVersion() + local rawVersion = runService:GetRobloxVersion() + local displayVersion + if rawVersion == "?" then + displayVersion = "DEBUG_SERVER" + else + if runService:IsStudio() then + displayVersion = "ROBLOX Studio" + else + displayVersion = rawVersion + end + end + return displayVersion +end + +RemoteFunction_GetServerVersion.OnServerInvoke = getServerVersion + +game:GetService("Players").PlayerRemoving:connect(function(player) + if player then + local dialog = playerDialogMap[player] + if dialog then + dialog:SetPlayerIsUsing(player, false) + playerDialogMap[player] = nil + end + end +end) + +if game:GetService("Chat").LoadDefaultChat then + require(game:GetService("CoreGui").RobloxGui.Modules.Server.ClientChat.ChatWindowInstaller)() + require(game:GetService("CoreGui").RobloxGui.Modules.Server.ServerChat.ChatServiceInstaller)() +end + +if freeCameraFlag then + require(game:GetService("CoreGui").RobloxGui.Modules.Server.FreeCamera.FreeCameraInstaller)() +end + +if UserSettings():IsUserFeatureEnabled("UserUseSoundDispatcher") then + require(game:GetService("CoreGui").RobloxGui.Modules.Server.ServerSound.SoundDispatcherInstaller)() +end + diff --git a/Client2018/content/scripts/CoreScripts/StarterScript.lua b/Client2018/content/scripts/CoreScripts/StarterScript.lua new file mode 100644 index 0000000..bfd9ad1 --- /dev/null +++ b/Client2018/content/scripts/CoreScripts/StarterScript.lua @@ -0,0 +1,99 @@ +-- Creates all neccessary scripts for the gui on initial load, everything except build tools +-- Created by Ben T. 10/29/10 +-- Please note that these are loaded in a specific order to diminish errors/perceived load time by user +local scriptContext = game:GetService("ScriptContext") +local touchEnabled = game:GetService("UserInputService").TouchEnabled + +local RobloxGui = game:GetService("CoreGui"):WaitForChild("RobloxGui") + +local soundFolder = Instance.new("Folder") +soundFolder.Name = "Sounds" +soundFolder.Parent = RobloxGui + +-- This can be useful in cases where a flag configuration issue causes requiring a CoreScript to fail +local function safeRequire(moduleScript) + local moduleReturnValue = nil + local success, err = pcall(function() moduleReturnValue = require(moduleScript) end) + if not success then + warn("Failure to Start CoreScript module" ..moduleScript.Name.. ".\n" ..err) + end + return moduleReturnValue +end + +-- TopBar +scriptContext:AddCoreScriptLocal("CoreScripts/Topbar", RobloxGui) + +-- MainBotChatScript (the Lua part of Dialogs) +scriptContext:AddCoreScriptLocal("CoreScripts/MainBotChatScript2", RobloxGui) + +-- In-game notifications script +scriptContext:AddCoreScriptLocal("CoreScripts/NotificationScript2", RobloxGui) + +-- Performance Stats Management +scriptContext:AddCoreScriptLocal("CoreScripts/PerformanceStatsManagerScript", + RobloxGui) + +-- Chat script +spawn(function() safeRequire(RobloxGui.Modules.ChatSelector) end) +spawn(function() safeRequire(RobloxGui.Modules.PlayerlistModule) end) + +-- Purchase Prompt Script +scriptContext:AddCoreScriptLocal("CoreScripts/PurchasePromptScript2", RobloxGui) + +-- Prompt Block Player Script +scriptContext:AddCoreScriptLocal("CoreScripts/BlockPlayerPrompt", RobloxGui) +scriptContext:AddCoreScriptLocal("CoreScripts/FriendPlayerPrompt", RobloxGui) + +-- Avatar Context Menu +scriptContext:AddCoreScriptLocal("CoreScripts/AvatarContextMenu", RobloxGui) + +-- Backpack! +spawn(function() safeRequire(RobloxGui.Modules.BackpackScript) end) + +scriptContext:AddCoreScriptLocal("CoreScripts/VehicleHud", RobloxGui) + +scriptContext:AddCoreScriptLocal("CoreScripts/GamepadMenu", RobloxGui) + +if touchEnabled then -- touch devices don't use same control frame + -- only used for touch device button generation + scriptContext:AddCoreScriptLocal("CoreScripts/ContextActionTouch", RobloxGui) + + RobloxGui:WaitForChild("ControlFrame") + RobloxGui.ControlFrame:WaitForChild("BottomLeftControl") + RobloxGui.ControlFrame.BottomLeftControl.Visible = false +end + +spawn(function() + local VRService = game:GetService('VRService') + local function onVREnabledChanged() + if VRService.VREnabled then + safeRequire(RobloxGui.Modules.VR.VirtualKeyboard) + safeRequire(RobloxGui.Modules.VR.UserGui) + end + end + onVREnabledChanged() + VRService:GetPropertyChangedSignal("VREnabled"):connect(onVREnabledChanged) +end) + +-- Boot up the VR App Shell +if UserSettings().GameSettings:InStudioMode() then + local VRService = game:GetService('VRService') + local function onVREnabledChanged() + if VRService.VREnabled then + local shellInVRSuccess, shellInVRFlagValue = pcall(function() return settings():GetFFlag("EnabledAppShell3D") end) + local shellInVR = (shellInVRSuccess and shellInVRFlagValue == true) + local modulesFolder = RobloxGui.Modules + local appHomeModule = modulesFolder:FindFirstChild('Shell') and modulesFolder:FindFirstChild('Shell'):FindFirstChild('AppHome') + if shellInVR and appHomeModule then + safeRequire(appHomeModule) + end + end + end + + spawn(function() + if VRService.VREnabled then + onVREnabledChanged() + end + VRService:GetPropertyChangedSignal("VREnabled"):connect(onVREnabledChanged) + end) +end diff --git a/Client2018/content/scripts/PlayerScripts/StarterCharacterScripts/Sound.server.lua b/Client2018/content/scripts/PlayerScripts/StarterCharacterScripts/Sound.server.lua new file mode 100644 index 0000000..4399486 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterCharacterScripts/Sound.server.lua @@ -0,0 +1,98 @@ +--[[ + Author: @spotco + This script creates sounds which are placed under the character head. + These sounds are used by the "LocalSound" script. + + To modify this script, copy it to your "StarterPlayer/StarterCharacterScripts" folder keeping the same script name ("Sound"). + The default Sound script loaded for every character will then be replaced with your copy of the script. +]]-- +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Players = game:GetService("Players") + +local SOUND_EVENT_FOLDER_NAME = "DefaultSoundEvents" +local DEFAULT_SERVER_SOUND_EVENT_NAME = "DefaultServerSoundEvent" + +local SoundEventFolder = ReplicatedStorage:FindFirstChild(SOUND_EVENT_FOLDER_NAME) +local DefaultServerSoundEvent = nil + +local useSoundDispatcher = UserSettings():IsUserFeatureEnabled("UserUseSoundDispatcher") +if useSoundDispatcher then + if not SoundEventFolder then + SoundEventFolder = Instance.new("Folder") + SoundEventFolder.Name = SOUND_EVENT_FOLDER_NAME + SoundEventFolder.Archivable = false + SoundEventFolder.Parent = ReplicatedStorage + end + + DefaultServerSoundEvent = SoundEventFolder:FindFirstChild(DEFAULT_SERVER_SOUND_EVENT_NAME) +else + DefaultServerSoundEvent = ReplicatedStorage:FindFirstChild(DEFAULT_SERVER_SOUND_EVENT_NAME) +end + +if not DefaultServerSoundEvent then + if useSoundDispatcher then + DefaultServerSoundEvent = Instance.new("RemoteEvent", SoundEventFolder) + else + DefaultServerSoundEvent = Instance.new("RemoteEvent", ReplicatedStorage) + end + + DefaultServerSoundEvent.Name = DEFAULT_SERVER_SOUND_EVENT_NAME + DefaultServerSoundEvent.OnServerEvent:Connect(function() end) +end + +local function CreateNewSound(name, id, looped, pitch, parent) + local sound = Instance.new("Sound") + sound.SoundId = id + sound.Name = name + sound.archivable = false + sound.Pitch = pitch + sound.Looped = looped + sound.MinDistance = 5 + sound.MaxDistance = 150 + sound.Volume = 0.65 + sound.Parent = parent + + if DefaultServerSoundEvent then + local CharacterSoundEvent = Instance.new("RemoteEvent", sound) + CharacterSoundEvent.Name = "CharacterSoundEvent" + CharacterSoundEvent.OnServerEvent:Connect(function(player, playing, resetPosition) + if type(playing) ~= "boolean" then + return + end + if type(resetPosition) ~= "boolean" then + return + end + + if player.Character ~= script.Parent then + return + end + for _, p in pairs(Players:GetPlayers()) do + if p ~= player then + -- Connect to the dispatcher to check if the player has loaded. + if useSoundDispatcher then + SoundEventFolder:FindFirstChild("SoundDispatcher"):Fire(p, sound, playing, resetPosition) + else + DefaultServerSoundEvent:FireClient(p, sound, playing, resetPosition) + end + end + end + end) + end + return sound +end + +local head = script.Parent:FindFirstChild("Head") +if not head then + error("Sound script parent has no child Head.") + return +end + +CreateNewSound("GettingUp", "rbxasset://sounds/action_get_up.mp3", false, 1, head) +CreateNewSound("Died", "rbxasset://sounds/uuhhh.mp3", false, 1, head) +CreateNewSound("FreeFalling", "rbxasset://sounds/action_falling.mp3", true, 1, head) +CreateNewSound("Jumping", "rbxasset://sounds/action_jump.mp3", false, 1, head) +CreateNewSound("Landing", "rbxasset://sounds/action_jump_land.mp3", false, 1, head) +CreateNewSound("Splash", "rbxasset://sounds/impact_water.mp3", false, 1, head) +CreateNewSound("Running", "rbxasset://sounds/action_footsteps_plastic.mp3", true, 1.85, head) +CreateNewSound("Swimming", "rbxasset://sounds/action_swim.mp3", true, 1.6, head) +CreateNewSound("Climbing", "rbxasset://sounds/action_footsteps_plastic.mp3", true, 1, head) \ No newline at end of file diff --git a/Client2018/content/scripts/PlayerScripts/StarterCharacterScripts/Sound/LocalSound.client.lua b/Client2018/content/scripts/PlayerScripts/StarterCharacterScripts/Sound/LocalSound.client.lua new file mode 100644 index 0000000..5fb95e3 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterCharacterScripts/Sound/LocalSound.client.lua @@ -0,0 +1,427 @@ +--[[ + Author: @spotco + This script runs locally for the player of the given humanoid. + This script triggers humanoid sound play/pause actions locally. + + The Playing/TimePosition properties of Sound objects bypass FilteringEnabled, so this triggers the sound + immediately for the player and is replicated to all other players. + + This script is optimized to reduce network traffic through minimizing the amount of property replication. +]]-- + +--All sounds are referenced by this ID +local SFX = { + Died = 0; + Running = 1; + Swimming = 2; + Climbing = 3, + Jumping = 4; + GettingUp = 5; + FreeFalling = 6; + FallingDown = 7; + Landing = 8; + Splash = 9; +} + +local useUpdatedLocalSoundFlag = UserSettings():IsUserFeatureEnabled("UserFixCharacterSoundIssues") + +local Humanoid = nil +local Head = nil +--SFX ID to Sound object +local Sounds = {} +local SoundService = game:GetService("SoundService") +local soundEventFolderName = "DefaultSoundEvents" +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local AddCharacterLoadedEvent = nil +local RemoveCharacterEvent = nil +local soundEventFolder = ReplicatedStorage:FindFirstChild(soundEventFolderName) +local useSoundDispatcher = UserSettings():IsUserFeatureEnabled("UserUseSoundDispatcher") + +if useSoundDispatcher then + if not soundEventFolder then + soundEventFolder = Instance.new("Folder", ReplicatedStorage) + soundEventFolder.Name = soundEventFolderName + soundEventFolder.Archivable = false + end + + -- Load the RemoveCharacterEvent + RemoveCharacterEvent = soundEventFolder:FindFirstChild("RemoveCharacterEvent") + if RemoveCharacterEvent == nil then + RemoveCharacterEvent = Instance.new("RemoteEvent", soundEventFolder) + RemoveCharacterEvent.Name = "RemoveCharacterEvent" + end + + AddCharacterLoadedEvent = soundEventFolder:FindFirstChild("AddCharacterLoadedEvent") + if AddCharacterLoadedEvent == nil then + AddCharacterLoadedEvent = Instance.new("RemoteEvent", soundEventFolder) + AddCharacterLoadedEvent.Name = "AddCharacterLoadedEvent" + end + + -- Notify the server a new character has been loaded + AddCharacterLoadedEvent:FireServer() + + -- Notify the sound dispatcher this character has left. + game.Players.LocalPlayer.CharacterRemoving:connect(function(character) + RemoveCharacterEvent:FireServer(game.Players.LocalPlayer) + end) +end + +do + local Figure = script.Parent.Parent + Head = Figure:WaitForChild("Head") + while not Humanoid do + for _,NewHumanoid in pairs(Figure:GetChildren()) do + if NewHumanoid:IsA("Humanoid") then + Humanoid = NewHumanoid + break + end + end + if Humanoid then break end + Figure.ChildAdded:wait() + end + Sounds[SFX.Died] = Head:WaitForChild("Died") + Sounds[SFX.Running] = Head:WaitForChild("Running") + Sounds[SFX.Swimming] = Head:WaitForChild("Swimming") + Sounds[SFX.Climbing] = Head:WaitForChild("Climbing") + Sounds[SFX.Jumping] = Head:WaitForChild("Jumping") + Sounds[SFX.GettingUp] = Head:WaitForChild("GettingUp") + Sounds[SFX.FreeFalling] = Head:WaitForChild("FreeFalling") + Sounds[SFX.Landing] = Head:WaitForChild("Landing") + Sounds[SFX.Splash] = Head:WaitForChild("Splash") + + local DefaultServerSoundEvent = nil + if useSoundDispatcher then + DefaultServerSoundEvent = soundEventFolder:FindFirstChild("DefaultServerSoundEvent") + else + DefaultServerSoundEvent = game:GetService("ReplicatedStorage"):FindFirstChild("DefaultServerSoundEvent") + end + + if DefaultServerSoundEvent then + DefaultServerSoundEvent.OnClientEvent:connect(function(sound, playing, resetPosition) + if resetPosition and sound.TimePosition ~= 0 then + sound.TimePosition = 0 + end + if sound.IsPlaying ~= playing then + sound.Playing = playing + end + end) + end +end + +local IsSoundFilteringEnabled = function() + return game.Workspace.FilteringEnabled and SoundService.RespectFilteringEnabled +end + +local Util +Util = { + + --Define linear relationship between (pt1x,pt2x) and (pt2x,pt2y). Evaluate this at x. + YForLineGivenXAndTwoPts = function(x,pt1x,pt1y,pt2x,pt2y) + --(y - y1)/(x - x1) = m + local m = (pt1y - pt2y) / (pt1x - pt2x) + --float b = pt1.y - m * pt1.x; + local b = (pt1y - m * pt1x) + return m * x + b + end; + + --Clamps the value of "val" between the "min" and "max" + Clamp = function(val,min,max) + return math.min(max,math.max(min,val)) + end; + + --Gets the horizontal (x,z) velocity magnitude of the given part + HorizontalSpeed = function(Head) + local hVel = Head.Velocity + Vector3.new(0,-Head.Velocity.Y,0) + return hVel.magnitude + end; + + --Gets the vertical (y) velocity magnitude of the given part + VerticalSpeed = function(Head) + return math.abs(Head.Velocity.Y) + end; + + --Setting Playing/TimePosition values directly result in less network traffic than Play/Pause/Resume/Stop + --If these properties are enabled, use them. + Play = function(sound) + if IsSoundFilteringEnabled() then + sound.CharacterSoundEvent:FireServer(true, true) + end + if sound.TimePosition ~= 0 then + sound.TimePosition = 0 + end + if not sound.IsPlaying then + sound.Playing = true + end + end; + + Pause = function(sound) + if IsSoundFilteringEnabled() then + sound.CharacterSoundEvent:FireServer(false, false) + end + if sound.IsPlaying then + sound.Playing = false + end + end; + + Resume = function(sound) + if IsSoundFilteringEnabled() then + sound.CharacterSoundEvent:FireServer(true, false) + end + if not sound.IsPlaying then + sound.Playing = true + end + end; + + Stop = function(sound) + if IsSoundFilteringEnabled() then + sound.CharacterSoundEvent:FireServer(false, true) + end + if sound.IsPlaying then + sound.Playing = false + end + if sound.TimePosition ~= 0 then + sound.TimePosition = 0 + end + end; +} + +do + -- List of all active Looped sounds + local playingLoopedSounds = {} + + -- Last seen Enum.HumanoidStateType + local activeState = nil + + local fallSpeed = 0 + + -- Verify and set that "sound" is in "playingLoopedSounds". + function setSoundInPlayingLoopedSounds(sound) + for i=1, #playingLoopedSounds do + if playingLoopedSounds[i] == sound then + return + end + end + table.insert(playingLoopedSounds,sound) + end + + -- Stop all active looped sounds except parameter "except". If "except" is not passed, all looped sounds will be stopped. + function stopPlayingLoopedSoundsExcept(except) + for i=#playingLoopedSounds,1,-1 do + if playingLoopedSounds[i] ~= except then + Util.Pause(playingLoopedSounds[i]) + table.remove(playingLoopedSounds,i) + end + end + end + + -- Table of Enum.HumanoidStateType to handling function + local stateUpdateHandler = { + [Enum.HumanoidStateType.Dead] = function() + stopPlayingLoopedSoundsExcept() + local sound = Sounds[SFX.Died] + Util.Play(sound) + end; + + [Enum.HumanoidStateType.RunningNoPhysics] = function(speed) + stateUpdated(Enum.HumanoidStateType.Running, speed) + end; + + [Enum.HumanoidStateType.Running] = function(speed) + local sound = Sounds[SFX.Running] + stopPlayingLoopedSoundsExcept(sound) + + if(useUpdatedLocalSoundFlag and activeState == Enum.HumanoidStateType.Freefall and fallSpeed > 0.1) then + -- Play a landing sound if the character dropped from a large distance + local vol = math.min(1.0, math.max(0.0, (fallSpeed - 50) / 110)) + local freeFallSound = Sounds[SFX.FreeFalling] + freeFallSound.Volume = vol + Util.Play(freeFallSound) + fallSpeed = 0 + end + if useUpdatedLocalSoundFlag then + if speed ~= nil and speed > 0.5 then + Util.Resume(sound) + setSoundInPlayingLoopedSounds(sound) + elseif speed ~= nil then + stopPlayingLoopedSoundsExcept() + end + else + if Util.HorizontalSpeed(Head) > 0.5 then + Util.Resume(sound) + setSoundInPlayingLoopedSounds(sound) + else + stopPlayingLoopedSoundsExcept() + end + end + end; + + [Enum.HumanoidStateType.Swimming] = function(speed) + local threshold + if useUpdatedLocalSoundFlag then threshold = speed else threshold = Util.VerticalSpeed(Head) end + if activeState ~= Enum.HumanoidStateType.Swimming and threshold > 0.1 then + local splashSound = Sounds[SFX.Splash] + splashSound.Volume = Util.Clamp( + Util.YForLineGivenXAndTwoPts( + Util.VerticalSpeed(Head), + 100, 0.28, + 350, 1), + 0,1) + Util.Play(splashSound) + end + + do + local sound = Sounds[SFX.Swimming] + stopPlayingLoopedSoundsExcept(sound) + Util.Resume(sound) + setSoundInPlayingLoopedSounds(sound) + end + end; + + [Enum.HumanoidStateType.Climbing] = function(speed) + local sound = Sounds[SFX.Climbing] + if useUpdatedLocalSoundFlag then + if speed ~= nil and math.abs(speed) > 0.1 then + Util.Resume(sound) + stopPlayingLoopedSoundsExcept(sound) + else + Util.Pause(sound) + stopPlayingLoopedSoundsExcept(sound) + end + else + if Util.VerticalSpeed(Head) > 0.1 then + Util.Resume(sound) + stopPlayingLoopedSoundsExcept(sound) + else + stopPlayingLoopedSoundsExcept() + end + end + + setSoundInPlayingLoopedSounds(sound) + end; + + [Enum.HumanoidStateType.Jumping] = function() + if activeState == Enum.HumanoidStateType.Jumping then + return + end + stopPlayingLoopedSoundsExcept() + local sound = Sounds[SFX.Jumping] + Util.Play(sound) + end; + + [Enum.HumanoidStateType.GettingUp] = function() + stopPlayingLoopedSoundsExcept() + local sound = Sounds[SFX.GettingUp] + Util.Play(sound) + end; + + [Enum.HumanoidStateType.Freefall] = function() + if activeState == Enum.HumanoidStateType.Freefall then + return + end + local sound = Sounds[SFX.FreeFalling] + sound.Volume = 0 + stopPlayingLoopedSoundsExcept() + + fallSpeed = math.max(fallSpeed, math.abs(Head.Velocity.y)) + end; + + [Enum.HumanoidStateType.FallingDown] = function() + stopPlayingLoopedSoundsExcept() + end; + + [Enum.HumanoidStateType.Landed] = function() + stopPlayingLoopedSoundsExcept() + if Util.VerticalSpeed(Head) > 75 then + local landingSound = Sounds[SFX.Landing] + landingSound.Volume = Util.Clamp( + Util.YForLineGivenXAndTwoPts( + Util.VerticalSpeed(Head), + 50, 0, + 100, 1), + 0,1) + Util.Play(landingSound) + end + end; + + [Enum.HumanoidStateType.Seated] = function() + stopPlayingLoopedSoundsExcept() + end; + } + + + + -- Handle state event fired or OnChange fired + function stateUpdated(state, speed) + if stateUpdateHandler[state] ~= nil then + if useUpdatedLocalSoundFlag and (state == Enum.HumanoidStateType.Running + or state == Enum.HumanoidStateType.Climbing + or state == Enum.HumanoidStateType.Swimming + or state == Enum.HumanoidStateType.RunningNoPhysics) then + stateUpdateHandler[state](speed) + else + stateUpdateHandler[state]() + end + end + activeState = state + end + + Humanoid.Died:connect( function() stateUpdated(Enum.HumanoidStateType.Dead) end) + Humanoid.Running:connect( function(speed) stateUpdated(Enum.HumanoidStateType.Running, speed) end) + Humanoid.Swimming:connect( function(speed) stateUpdated(Enum.HumanoidStateType.Swimming, speed) end) + Humanoid.Climbing:connect( function(speed) stateUpdated(Enum.HumanoidStateType.Climbing, speed) end) + Humanoid.Jumping:connect( function() stateUpdated(Enum.HumanoidStateType.Jumping) end) + Humanoid.GettingUp:connect( function() stateUpdated(Enum.HumanoidStateType.GettingUp) end) + Humanoid.FreeFalling:connect( function() stateUpdated(Enum.HumanoidStateType.Freefall) end) + Humanoid.FallingDown:connect( function() stateUpdated(Enum.HumanoidStateType.FallingDown) end) + + + + -- required for proper handling of Landed event + + Humanoid.StateChanged:connect(function(old, new) + stateUpdated(new) + end) + + + + function onUpdate(stepDeltaSeconds, tickSpeedSeconds) + local stepScale = stepDeltaSeconds / tickSpeedSeconds + do + local sound = Sounds[SFX.FreeFalling] + if activeState == Enum.HumanoidStateType.Freefall then + if Head.Velocity.Y < 0 and Util.VerticalSpeed(Head) > 75 then + Util.Resume(sound) + + --Volume takes 1.1 seconds to go from volume 0 to 1 + local ANIMATION_LENGTH_SECONDS = 1.1 + + local normalizedIncrement = tickSpeedSeconds / ANIMATION_LENGTH_SECONDS + sound.Volume = Util.Clamp(sound.Volume + normalizedIncrement * stepScale, 0, 1) + else + sound.Volume = 0 + end + else + Util.Pause(sound) + end + end + + do + local sound = Sounds[SFX.Running] + if activeState == Enum.HumanoidStateType.Running then + if Util.HorizontalSpeed(Head) < 0.5 then + Util.Pause(sound) + end + end + end + end + + + local lastTick = tick() + local TICK_SPEED_SECONDS = 0.25 + while true do + onUpdate(tick() - lastTick,TICK_SPEED_SECONDS) + lastTick = tick() + wait(TICK_SPEED_SECONDS) + end + +end diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript.lua new file mode 100644 index 0000000..8ae41e3 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript.lua @@ -0,0 +1,320 @@ +local RunService = game:GetService('RunService') +local UserInputService = game:GetService('UserInputService') +local PlayersService = game:GetService('Players') +local VRService = game:GetService("VRService") +local StarterPlayer = game:GetService('StarterPlayer') + + +local RootCamera = script:WaitForChild('RootCamera') + +local AttachCamera = require(RootCamera:WaitForChild('AttachCamera'))() +local FixedCamera = require(RootCamera:WaitForChild('FixedCamera'))() +local ScriptableCamera = require(RootCamera:WaitForChild('ScriptableCamera'))() +local TrackCamera = require(RootCamera:WaitForChild('TrackCamera'))() +local WatchCamera = require(RootCamera:WaitForChild('WatchCamera'))() + +local OrbitalCamera = require(RootCamera:WaitForChild('OrbitalCamera'))() +local ClassicCamera = require(RootCamera:WaitForChild('ClassicCamera'))() +local FollowCamera = require(RootCamera:WaitForChild('FollowCamera'))() +local PopperCam = require(script:WaitForChild('PopperCam')) +local Invisicam = require(script:WaitForChild('Invisicam')) +local TransparencyController = require(script:WaitForChild('TransparencyController'))() + +local VRCamera = require(RootCamera:WaitForChild("VRCamera"))() + +local GameSettings = UserSettings().GameSettings + +local AllCamerasInLua = false +local success, msg = pcall(function() + AllCamerasInLua = UserSettings():IsUserFeatureEnabled("UserAllCamerasInLua") +end) +if not success then + print("Couldn't get feature UserAllCamerasInLua because:" , msg) +end + +local FFlagUserNoCameraClickToMoveSuccess, FFlagUserNoCameraClickToMoveResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNoCameraClickToMove") end) +local FFlagUserNoCameraClickToMove = FFlagUserNoCameraClickToMoveSuccess and FFlagUserNoCameraClickToMoveResult +local ClickToMove = FFlagUserNoCameraClickToMove and nil or require(script:WaitForChild('ClickToMove'))() + +local isOrbitalCameraEnabled = pcall(function() local test = Enum.CameraType.Orbital end) + +-- register what camera scripts we are using +do + local PlayerScripts = PlayersService.LocalPlayer:WaitForChild("PlayerScripts") + local canRegisterCameras = pcall(function() PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Default) end) + + if canRegisterCameras then + PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Follow) + PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Classic) + + PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Default) + PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Follow) + PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Classic) + end +end + +local CameraTypeEnumMap = +{ + [Enum.CameraType.Attach] = AttachCamera; + [Enum.CameraType.Fixed] = FixedCamera; + [Enum.CameraType.Scriptable] = ScriptableCamera; + [Enum.CameraType.Track] = TrackCamera; + [Enum.CameraType.Watch] = WatchCamera; + [Enum.CameraType.Follow] = FollowCamera; +} + +if isOrbitalCameraEnabled then + CameraTypeEnumMap[Enum.CameraType.Orbital] = OrbitalCamera; +end + +local EnabledCamera = nil +local EnabledOcclusion = nil + +local cameraSubjectChangedConn = nil +local cameraTypeChangedConn = nil +local renderSteppedConn = nil + +local lastInputType = nil +local hasLastInput = false + +local function IsTouch() + return UserInputService.TouchEnabled +end + +local function shouldUsePlayerScriptsCamera() + local player = PlayersService.LocalPlayer + local currentCamera = workspace.CurrentCamera + if AllCamerasInLua then + return true + else + if player then + if currentCamera == nil or (currentCamera.CameraType == Enum.CameraType.Custom) + or (isOrbitalCameraEnabled and currentCamera.CameraType == Enum.CameraType.Orbital) then + return true + end + end + end + return false +end + +local function isClickToMoveOn() + local usePlayerScripts = shouldUsePlayerScriptsCamera() + local player = PlayersService.LocalPlayer + if usePlayerScripts and player then + if (hasLastInput and lastInputType == Enum.UserInputType.Touch) or IsTouch() then -- Touch + if player.DevTouchMovementMode == Enum.DevTouchMovementMode.ClickToMove or + (player.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice and GameSettings.TouchMovementMode == Enum.TouchMovementMode.ClickToMove) then + return true + end + else -- Computer + if player.DevComputerMovementMode == Enum.DevComputerMovementMode.ClickToMove or + (player.DevComputerMovementMode == Enum.DevComputerMovementMode.UserChoice and GameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove) then + return true + end + end + end + return false +end + +local function getCurrentCameraMode() + local usePlayerScripts = shouldUsePlayerScriptsCamera() + local player = PlayersService.LocalPlayer + if usePlayerScripts and player then + if (hasLastInput and lastInputType == Enum.UserInputType.Touch) or IsTouch() then -- Touch (iPad, etc...) + if not FFlagUserNoCameraClickToMove and isClickToMoveOn() then + return Enum.DevTouchMovementMode.ClickToMove.Name + elseif player.DevTouchCameraMode == Enum.DevTouchCameraMovementMode.UserChoice then + local touchMovementMode = GameSettings.TouchCameraMovementMode + if touchMovementMode == Enum.TouchCameraMovementMode.Default then + return Enum.TouchCameraMovementMode.Follow.Name + end + return touchMovementMode.Name + else + return player.DevTouchCameraMode.Name + end + else -- Computer + if not FFlagUserNoCameraClickToMove and isClickToMoveOn() then + return Enum.DevComputerMovementMode.ClickToMove.Name + elseif player.DevComputerCameraMode == Enum.DevComputerCameraMovementMode.UserChoice then + local computerMovementMode = GameSettings.ComputerCameraMovementMode + if computerMovementMode == Enum.ComputerCameraMovementMode.Default then + return Enum.ComputerCameraMovementMode.Classic.Name + end + return computerMovementMode.Name + else + return player.DevComputerCameraMode.Name + end + end + end +end + +local function getCameraOcclusionMode() + local usePlayerScripts = shouldUsePlayerScriptsCamera() + local player = PlayersService.LocalPlayer + if usePlayerScripts and player then + return player.DevCameraOcclusionMode + end +end + +-- New for AllCameraInLua support +local function shouldUseOcclusionModule() + local player = PlayersService.LocalPlayer + if player and game.Workspace.CurrentCamera and game.Workspace.CurrentCamera.CameraType == Enum.CameraType.Custom then + return true + end + return false +end + +local function Update() + if EnabledCamera then + EnabledCamera:Update() + end + if EnabledOcclusion and not VRService.VREnabled then + EnabledOcclusion:Update(EnabledCamera) + end + if shouldUsePlayerScriptsCamera() then + TransparencyController:Update() + end +end + +local function SetEnabledCamera(newCamera) + if EnabledCamera ~= newCamera then + if EnabledCamera then + EnabledCamera:SetEnabled(false) + end + EnabledCamera = newCamera + if EnabledCamera then + EnabledCamera:SetEnabled(true) + end + end +end + +local function OnCameraMovementModeChange(newCameraMode) + if newCameraMode == Enum.DevComputerMovementMode.ClickToMove.Name then + if FFlagUserNoCameraClickToMove then + --No longer responding to ClickToMove here! + return + end + ClickToMove:Start() + SetEnabledCamera(nil) + TransparencyController:SetEnabled(true) + else + local currentCameraType = workspace.CurrentCamera and workspace.CurrentCamera.CameraType + if VRService.VREnabled and currentCameraType ~= Enum.CameraType.Scriptable then + SetEnabledCamera(VRCamera) + TransparencyController:SetEnabled(false) + elseif (currentCameraType == Enum.CameraType.Custom or not AllCamerasInLua) and newCameraMode == Enum.ComputerCameraMovementMode.Classic.Name then + SetEnabledCamera(ClassicCamera) + TransparencyController:SetEnabled(true) + elseif (currentCameraType == Enum.CameraType.Custom or not AllCamerasInLua) and newCameraMode == Enum.ComputerCameraMovementMode.Follow.Name then + SetEnabledCamera(FollowCamera) + TransparencyController:SetEnabled(true) + elseif (currentCameraType == Enum.CameraType.Custom or not AllCamerasInLua) and (isOrbitalCameraEnabled and (newCameraMode == Enum.ComputerCameraMovementMode.Orbital.Name)) then + SetEnabledCamera(OrbitalCamera) + TransparencyController:SetEnabled(true) + elseif AllCamerasInLua and CameraTypeEnumMap[currentCameraType] then + SetEnabledCamera(CameraTypeEnumMap[currentCameraType]) + TransparencyController:SetEnabled(false) + else -- Our camera movement code was disabled by the developer + SetEnabledCamera(nil) + TransparencyController:SetEnabled(false) + end + ClickToMove:Stop() + end + + local useOcclusion = shouldUseOcclusionModule() + local newOcclusionMode = getCameraOcclusionMode() + if EnabledOcclusion == Invisicam and (newOcclusionMode ~= Enum.DevCameraOcclusionMode.Invisicam or (not useOcclusion)) then + Invisicam:Cleanup() + end + + -- PopperCam does not work with OrbitalCamera, as OrbitalCamera's distance can be fixed. + if useOcclusion then + if newOcclusionMode == Enum.DevCameraOcclusionMode.Zoom and ( isOrbitalCameraEnabled and newCameraMode ~= Enum.ComputerCameraMovementMode.Orbital.Name ) then + EnabledOcclusion = PopperCam + elseif newOcclusionMode == Enum.DevCameraOcclusionMode.Invisicam then + EnabledOcclusion = Invisicam + else + EnabledOcclusion = nil + end + else + EnabledOcclusion = nil + end +end + +local function OnCameraTypeChanged(newCameraType) + if newCameraType == Enum.CameraType.Scriptable then + UserInputService.MouseBehavior = Enum.MouseBehavior.Default + end +end + + +local function OnCameraSubjectChanged(newSubject) + TransparencyController:SetSubject(newSubject) +end + +local function OnNewCamera() + OnCameraMovementModeChange(getCurrentCameraMode()) + + local currentCamera = workspace.CurrentCamera + if currentCamera then + if cameraSubjectChangedConn then + cameraSubjectChangedConn:disconnect() + end + + if cameraTypeChangedConn then + cameraTypeChangedConn:disconnect() + end + + cameraSubjectChangedConn = currentCamera:GetPropertyChangedSignal("CameraSubject"):connect(function() + OnCameraSubjectChanged(currentCamera.CameraSubject) + end) + + cameraTypeChangedConn = currentCamera:GetPropertyChangedSignal("CameraType"):connect(function() + OnCameraMovementModeChange(getCurrentCameraMode()) + OnCameraTypeChanged(currentCamera.CameraType) + end) + + OnCameraSubjectChanged(currentCamera.CameraSubject) + OnCameraTypeChanged(currentCamera.CameraType) + end +end + + +local function OnPlayerAdded(player) + workspace.Changed:connect(function(prop) + if prop == 'CurrentCamera' then + OnNewCamera() + end + end) + + player.Changed:connect(function(prop) + OnCameraMovementModeChange(getCurrentCameraMode()) + end) + + GameSettings.Changed:connect(function(prop) + OnCameraMovementModeChange(getCurrentCameraMode()) + end) + + RunService:BindToRenderStep("cameraRenderUpdate", Enum.RenderPriority.Camera.Value, Update) + + OnNewCamera() + OnCameraMovementModeChange(getCurrentCameraMode()) +end + +do + while PlayersService.LocalPlayer == nil do PlayersService.PlayerAdded:wait() end + hasLastInput = pcall(function() + lastInputType = UserInputService:GetLastInputType() + UserInputService.LastInputTypeChanged:connect(function(newLastInputType) + lastInputType = newLastInputType + end) + end) + OnPlayerAdded(PlayersService.LocalPlayer) +end + +local function OnVREnabled() + OnCameraMovementModeChange(getCurrentCameraMode()) +end + +VRService:GetPropertyChangedSignal("VREnabled"):connect(OnVREnabled) diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/ClickToMove.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/ClickToMove.lua new file mode 100644 index 0000000..6fae4de --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/ClickToMove.lua @@ -0,0 +1,1587 @@ +-- Written By Kip Turner, Copyright Roblox 2014 + +local newClickToMove = script:FindFirstChild("NewClickToMove") +if newClickToMove then + local newClickToMoveFlagSuccess, newClickToMoveFlagEnabled = pcall(function() + return UserSettings():IsUserFeatureEnabled("UserUseNewClickToMove") + end) + local useNewClickToMove = newClickToMoveFlagSuccess and newClickToMoveFlagEnabled + if useNewClickToMove then + return require(newClickToMove) + end +end + +local UIS = game:GetService("UserInputService") +local PathfindingService = game:GetService("PathfindingService") +local PlayerService = game:GetService("Players") +local RunService = game:GetService("RunService") +local DebrisService = game:GetService('Debris') +local ReplicatedStorage = game:GetService('ReplicatedStorage') + +local CameraScript = script.Parent +local ClassicCameraModule = require(CameraScript:WaitForChild('RootCamera'):WaitForChild('ClassicCamera')) + +local Player = PlayerService.localPlayer +local MyMouse = Player:GetMouse() + + +local DirectPathEnabled = false +local SHOW_PATH = false + +local Y_VECTOR3 = Vector3.new(0, 1, 0) +local XZ_VECTOR3 = Vector3.new(1, 0, 1) +local ZERO_VECTOR3 = Vector3.new(0, 0, 0) +local ZERO_VECTOR2 = Vector2.new(0, 0) + +local RayCastIgnoreList = workspace.FindPartOnRayWithIgnoreList +local GetPartsTouchingExtents = workspace.FindPartsInRegion3 + +local math_min = math.min +local math_max = math.max +local math_pi = math.pi +local math_floor = math.floor +local math_abs = math.abs +local math_deg = math.deg +local math_acos = math.acos +local math_sin = math.sin +local math_atan2 = math.atan2 + +local Vector3_new = Vector3.new +local Vector2_new = Vector2.new +local CFrame_new = CFrame.new + +local CurrentSeatPart = nil +local DrivingTo = nil + +-- Bindable for when we want touch emergency controls +-- TODO: Click to move should probably have it's own gui touch controls +-- to manage this. +local BindableEvent_OnFailStateChanged = nil +local BindableEvent_EnableTouchJump = nil +if UIS.TouchEnabled then + BindableEvent_OnFailStateChanged = Instance.new('BindableEvent') + BindableEvent_OnFailStateChanged.Name = "OnClickToMoveFailStateChange" + BindableEvent_EnableTouchJump = Instance.new('BindableEvent') + BindableEvent_EnableTouchJump.Name = "EnableTouchJump" + local CameraScript = script.Parent + local PlayerScripts = CameraScript.Parent + BindableEvent_OnFailStateChanged.Parent = PlayerScripts + BindableEvent_EnableTouchJump.Parent = PlayerScripts +end + +local function clamp(low, high, num) + return (num > high and high or num < low and low or num) +end + +--------------------------UTIL LIBRARY------------------------------- +local Utility = {} +do + local Signal = {} + + function Signal.Create() + local sig = {} + + local mSignaler = Instance.new('BindableEvent') + + local mArgData = nil + local mArgDataCount = nil + + function sig:fire(...) + mArgData = {...} + mArgDataCount = select('#', ...) + mSignaler:Fire() + end + + function sig:connect(f) + if not f then error("connect(nil)", 2) end + return mSignaler.Event:connect(function() + f(unpack(mArgData, 1, mArgDataCount)) + end) + end + + function sig:wait() + mSignaler.Event:wait() + assert(mArgData, "Missing arg data, likely due to :TweenSize/Position corrupting threadrefs.") + return unpack(mArgData, 1, mArgDataCount) + end + + return sig + end + Utility.Signal = Signal + + function Utility.Create(instanceType) + return function(data) + local obj = Instance.new(instanceType) + for k, v in pairs(data) do + if type(k) == 'number' then + v.Parent = obj + else + obj[k] = v + end + end + return obj + end + end + + local function ViewSizeX() + local camera = workspace.CurrentCamera + local x = camera and camera.ViewportSize.X or 0 + local y = camera and camera.ViewportSize.Y or 0 + if x == 0 then + return 1024 + else + if x > y then + return x + else + return y + end + end + end + Utility.ViewSizeX = ViewSizeX + + local function ViewSizeY() + local camera = workspace.CurrentCamera + local x = camera and camera.ViewportSize.X or 0 + local y = camera and camera.ViewportSize.Y or 0 + if y == 0 then + return 768 + else + if x > y then + return y + else + return x + end + end + end + Utility.ViewSizeY = ViewSizeY + + local function AspectRatio() + return ViewSizeX() / ViewSizeY() + end + Utility.AspectRatio = AspectRatio + + local function FindChacterAncestor(part) + if part then + local humanoid = part:FindFirstChild("Humanoid") + if humanoid then + return part, humanoid + else + return FindChacterAncestor(part.Parent) + end + end + end + Utility.FindChacterAncestor = FindChacterAncestor + + + local function GetUnitRay(x, y, viewWidth, viewHeight, camera) + return camera:ScreenPointToRay(x, y) + end + Utility.GetUnitRay = GetUnitRay + + local function Raycast(ray, ignoreNonCollidable, ignoreList) + local ignoreList = ignoreList or {} + local hitPart, hitPos = RayCastIgnoreList(workspace, ray, ignoreList) + if hitPart then + if ignoreNonCollidable and hitPart.CanCollide == false then + table.insert(ignoreList, hitPart) + return Raycast(ray, ignoreNonCollidable, ignoreList) + end + return hitPart, hitPos + end + return nil, nil + end + Utility.Raycast = Raycast + + + Utility.Round = function(num, roundToNearest) + roundToNearest = roundToNearest or 1 + return math_floor((num + roundToNearest/2) / roundToNearest) * roundToNearest + end + + local function AveragePoints(positions) + local avgPos = ZERO_VECTOR2 + if #positions > 0 then + for i = 1, #positions do + avgPos = avgPos + positions[i] + end + avgPos = avgPos / #positions + end + return avgPos + end + Utility.AveragePoints = AveragePoints + + local function FuzzyEquals(numa, numb) + return numa + 0.1 > numb and numa - 0.1 < numb + end + Utility.FuzzyEquals = FuzzyEquals + + local LastInput = 0 + UIS.InputBegan:connect(function(inputObject, wasSunk) + if not wasSunk then + if inputObject.UserInputType == Enum.UserInputType.Touch or + inputObject.UserInputType == Enum.UserInputType.MouseButton1 or + inputObject.UserInputType == Enum.UserInputType.MouseButton2 then + LastInput = tick() + end + end + end) + Utility.GetLastInput = function() + return LastInput + end +end + +local humanoidCache = {} +local function findPlayerHumanoid(player) + local character = player and player.Character + if character then + local resultHumanoid = humanoidCache[player] + if resultHumanoid and resultHumanoid.Parent == character then + return resultHumanoid + else + humanoidCache[player] = nil -- Bust Old Cache + local humanoid = character:FindFirstChildOfClass("Humanoid") + if humanoid then + humanoidCache[player] = humanoid + end + return humanoid + end + end +end + +local GetThetaBetweenCFrames; do + local components = CFrame.new().components + local inverse = CFrame.new().inverse + local acos = math.acos + + GetThetaBetweenCFrames = function(c0, c1) -- (CFrame from, CFrame to) -> (float theta) + local _, _, _, xx, yx, zx, + xy, yy, zy, + xz, yz, zz = components(inverse(c0)*c1) + local cosTheta = (xx + yy + zz - 1)/2 + + if cosTheta >= 0.999 then + -- Same rotation + return 0 + elseif cosTheta <= -0.999 then + -- Oposite rotations + return math_pi + else + return acos(cosTheta) + end + end +end + +--------------------------------------------------------- + +local Signal = Utility.Signal +local Create = Utility.Create + +--------------------------CHARACTER CONTROL------------------------------- +local function CreateController() + local this = {} + + this.TorsoLookPoint = nil + + function this:SetTorsoLookPoint(point) + local humanoid = findPlayerHumanoid(Player) + if humanoid then + humanoid.AutoRotate = false + end + this.TorsoLookPoint = point + self:UpdateTorso() + delay(2, + function() + -- this isnt technically correct for detecting if this is the last issue to the setTorso function + if this.TorsoLookPoint == point then + this.TorsoLookPoint = nil + if humanoid then + humanoid.AutoRotate = true + end + end + end) + end + + function this:UpdateTorso(point) + if this.TorsoLookPoint then + point = this.TorsoLookPoint + else + return + end + + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + if torso then + local lookVec = (point - torso.CFrame.p).unit + local squashedLookVec = Vector3_new(lookVec.X, 0, lookVec.Z).unit + torso.CFrame = CFrame.new(torso.CFrame.p, torso.CFrame.p + squashedLookVec) + end + end + + return this +end + +local CharacterControl = CreateController() +----------------------------------------------------------------------- + +--------------------------PC AUTO JUMPER------------------------------- + +local function GetCharacter() + return Player and Player.Character +end + +local function GetTorso() + local humanoid = findPlayerHumanoid(Player) + return humanoid and humanoid.Torso +end + +local function IsPartAHumanoid(part) + return part and part.Parent and (part.Parent:FindFirstChild('Humanoid') ~= nil) +end + +local function doAutoJump() + local character = GetCharacter() + if (character == nil) then + return; + end + + local humanoid = findPlayerHumanoid(Player) + if (humanoid == nil) then + return; + end + + local rayLength = 1.5; + -- This is how high a ROBLOXian jumps from the mid point of his torso + local jumpHeight = 7.0; + + local torso = GetTorso() + if (torso == nil) then + return; + end + + local torsoCFrame = torso.CFrame; + local torsoLookVector = torsoCFrame.lookVector; + local torsoPos = torsoCFrame.p; + + local torsoRay = Ray.new(torsoPos + Vector3_new(0, -torso.Size.Y/2, 0), torsoLookVector * rayLength); + local jumpRay = Ray.new(torsoPos + Vector3_new(0, jumpHeight - torso.Size.Y, 0), torsoLookVector * rayLength); + + local hitPart, _ = RayCastIgnoreList(workspace, torsoRay, {character}, false) + local jumpHitPart, _ = RayCastIgnoreList(workspace, jumpRay, {character}, false) + + if (hitPart and jumpHitPart == nil and hitPart.CanCollide == true) then + -- NOTE: this follow line is not in the C++ impl, but an improvement in Click to Move + if not IsPartAHumanoid(hitPart) then + humanoid.Jump = true; + end + end +end + +local NO_JUMP_STATES = +{ + [Enum.HumanoidStateType.FallingDown] = false; + [Enum.HumanoidStateType.Flying] = false; + [Enum.HumanoidStateType.Freefall] = false; + [Enum.HumanoidStateType.GettingUp] = false; + [Enum.HumanoidStateType.Ragdoll] = false; + [Enum.HumanoidStateType.Running] = false; + [Enum.HumanoidStateType.Seated] = false; + [Enum.HumanoidStateType.Swimming] = false; + + -- Special case to detect if we are on a ladder + [Enum.HumanoidStateType.Climbing] = false; +} + +local function enableAutoJump() + local humanoid = findPlayerHumanoid(Player) + local currentState = humanoid and humanoid:GetState() + if currentState then + return NO_JUMP_STATES[currentState] == nil + end + return false +end + +local function getAutoJump() + return true +end + +local function vec3IsZero(vec3) + return vec3.magnitude < 0.05 +end + +-- NOTE: This function is radically different from the engine's implementation +local walkVelocityVector = Vector3_new(1,1,1) +local function calcDesiredWalkVelocity() + -- TEMP + return walkVelocityVector +end + +local function preStepSimulatorSide(dt) + if getAutoJump() and enableAutoJump() then + local desiredWalkVelocity = calcDesiredWalkVelocity(); + if (not vec3IsZero(desiredWalkVelocity)) then + doAutoJump(); + end + end +end + +local function AutoJumper() + local this = {} + local running = false + local runRoutine = nil + + function this:Run() + running = true + local thisRoutine = nil + thisRoutine = coroutine.create(function() + while running and thisRoutine == runRoutine do + this:Step() + wait() + end + end) + runRoutine = thisRoutine + coroutine.resume(thisRoutine) + end + + function this:Stop() + running = false + end + + function this:Step() + preStepSimulatorSide() + end + + return this +end + +----------------------------------------------------------------------------- + +-----------------------------------PATHER-------------------------------------- + +local function CreateDestinationIndicator(pos) + local destinationGlobe = Create'Part' + { + Name = 'PathGlobe'; + TopSurface = 'Smooth'; + BottomSurface = 'Smooth'; + Shape = 'Ball'; + CanCollide = false; + Size = Vector3_new(2,2,2); + BrickColor = BrickColor.new('Institutional white'); + Transparency = 0; + Anchored = true; + CFrame = CFrame.new(pos); + } + return destinationGlobe +end + +local function Pather(character, point) + local this = {} + + this.Cancelled = false + this.Started = false + + this.Finished = Signal.Create() + this.PathFailed = Signal.Create() + this.PathStarted = Signal.Create() + + this.PathComputed = false + + function this:YieldUntilPointReached(character, point, timeout) + timeout = timeout or 10000000 + + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + local start = tick() + local lastMoveTo = start + while torso and tick() - start < timeout and this.Cancelled == false do + local diffVector = (point - torso.CFrame.p) + local xzMagnitude = (diffVector * XZ_VECTOR3).magnitude + if xzMagnitude < 6 then + -- Jump if the path is telling is to go upwards + if diffVector.Y >= 2.2 then + humanoid.Jump = true + end + end + -- The hard-coded number 2 here is from the engine's MoveTo implementation + if xzMagnitude < 2 then + return true + end + -- Keep on issuing the move command because it will automatically quit every so often. + if tick() - lastMoveTo > 1.5 then + humanoid:MoveTo(point) + lastMoveTo = tick() + end + CharacterControl:UpdateTorso(point) + wait() + end + return false + end + + function this:Cancel() + this.Cancelled = true + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + if humanoid and torso then + humanoid:MoveTo(torso.CFrame.p) + end + end + + function this:CheckOcclusion(point1, point2, character, torsoRadius) + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + if torsoRadius == nil then + torsoRadius = torso and Vector3_new(torso.Size.X/2,0,torso.Size.Z/2) or XZ_VECTOR3 + end + + local diffVector = point2 - point1 + local directionVector = diffVector.unit + + local rightVector = Y_VECTOR3:Cross(directionVector) * torsoRadius + + local rightPart, _ = Utility.Raycast(Ray.new(point1 + rightVector, diffVector + rightVector), true, {character}) + local hitPart, _ = Utility.Raycast(Ray.new(point1, diffVector), true, {character}) + local leftPart, _ = Utility.Raycast(Ray.new(point1 - rightVector, diffVector - rightVector), true, {character}) + + if rightPart or hitPart or leftPart then + return false + end + + -- Make sure we have somewhere to stand on + local midPt = (point2 + point1) / 2 + local studsBetweenSamples = 2 + for i = 1, math_floor(diffVector.magnitude/studsBetweenSamples) do + local downPart, _ = Utility.Raycast(Ray.new(point1 + directionVector * i * studsBetweenSamples, Vector3_new(0,-7,0)), true, {character}) + if not downPart then + return false + end + end + + return true + end + + function this:SmoothPoints(pathToSmooth) + local result = {} + + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + for i = 1, #pathToSmooth do + table.insert(result, pathToSmooth[i]) + end + + -- Backwards for safe-deletion + for i = #result - 1, 1, -1 do + if i + 1 <= #result then + + local nextPoint = result[i+1] + local thisPoint = result[i] + + local lastPoint = result[i-1] + if lastPoint == nil then + lastPoint = torso and Vector3_new(torso.CFrame.p.X, thisPoint.Y, torso.CFrame.p.Z) + end + + if lastPoint and Utility.FuzzyEquals(thisPoint.Y, lastPoint.Y) and Utility.FuzzyEquals(thisPoint.Y, nextPoint.Y) then + if this:CheckOcclusion(lastPoint, nextPoint, character) then + table.remove(result, i) + -- Move i back one to recursively-smooth + i = i + 1 + end + end + end + end + + return result + end + + function this:CheckNeighboringCells(character) + local pathablePoints = {} + local humanoid = findPlayerHumanoid(Player) + local torso = character and humanoid and humanoid.Torso + if torso then + local torsoCFrame = torso.CFrame + local torsoPos = torsoCFrame.p + -- Minus and plus 2 is so we can get it into the cell-corner space and then translate it back into cell-center space + local roundedPos = Vector3_new(Utility.Round(torsoPos.X-2,4)+2, Utility.Round(torsoPos.Y-2,4)+2, Utility.Round(torsoPos.Z-2,4)+2) + local neighboringCells = {} + for x = -4, 4, 8 do + for z = -4, 4, 8 do + table.insert(neighboringCells, roundedPos + Vector3_new(x,0,z)) + end + end + for _, testPoint in pairs(neighboringCells) do + local pathable = this:CheckOcclusion(roundedPos, testPoint, character, ZERO_VECTOR3) + if pathable then + table.insert(pathablePoints, testPoint) + end + end + end + return pathablePoints + end + + function this:ComputeDirectPath() + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + if torso then + local startPt = torso.CFrame.p + local finishPt = point + if (finishPt - startPt).magnitude < 150 then + -- move back the destination by 2 studs or otherwise the pather will collide with the object we are trying to reach + finishPt = finishPt - (finishPt - startPt).unit * 2 + if this:CheckOcclusion(startPt, finishPt, character, ZERO_VECTOR3) then + local pathResult = {} + pathResult.Status = Enum.PathStatus.Success + function pathResult:GetPointCoordinates() + return {finishPt} + end + return pathResult + end + end + end + end + + local function AllAxisInThreshhold(targetPt, otherPt, threshold) + return math_abs(targetPt.X - otherPt.X) <= threshold and + math_abs(targetPt.Y - otherPt.Y) <= threshold and + math_abs(targetPt.Z - otherPt.Z) <= threshold + end + + function this:ComputePath() + local smoothed = false + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + if torso then + if this.PathComputed then return end + this.PathComputed = true + -- Will yield the script since it is an Async script (start, finish, maxDistance) + -- Try to use the smooth function, but it may not exist yet :( + local success = pcall(function() + -- 3 is height from torso cframe to ground + this.pathResult = PathfindingService:ComputeSmoothPathAsync(torso.CFrame.p - Vector3_new(0,3,0), point, 400) + smoothed = true + end) + if not success then + -- 3 is height from torso cframe to ground + this.pathResult = PathfindingService:ComputeRawPathAsync(torso.CFrame.p - Vector3_new(0,3,0), point, 400) + smoothed = false + end + this.pointList = this.pathResult and this.pathResult:GetPointCoordinates() + local pathFound = false + if this.pathResult.Status == Enum.PathStatus.FailFinishNotEmpty then + -- Lets try again with a slightly set back start point; it is ok to do this again so the FailFinishNotEmpty uses little computation + local diffVector = point - workspace.CurrentCamera.CoordinateFrame.p + if diffVector.magnitude > 2 then + local setBackPoint = point - (diffVector).unit * 2.1 + local success = pcall(function() + this.pathResult = PathfindingService:ComputeSmoothPathAsync(torso.CFrame.p, setBackPoint, 400) + smoothed = true + end) + if not success then + this.pathResult = PathfindingService:ComputeRawPathAsync(torso.CFrame.p, setBackPoint, 400) + smoothed = false + end + this.pointList = this.pathResult and this.pathResult:GetPointCoordinates() + pathFound = true + end + end + if this.pathResult.Status == Enum.PathStatus.ClosestNoPath and #this.pointList >= 1 and pathFound == false then + local otherPt = this.pointList[#this.pointList] + if AllAxisInThreshhold(point, otherPt, 4) and (torso.CFrame.p - point).magnitude > (otherPt - point).magnitude then + local pathResult = {} + pathResult.Status = Enum.PathStatus.Success + function pathResult:GetPointCoordinates() + return {this.pointList} + end + this.pathResult = pathResult + pathFound = true + end + end + if (this.pathResult.Status == Enum.PathStatus.FailStartNotEmpty or this.pathResult.Status == Enum.PathStatus.ClosestNoPath) and pathFound == false then + local pathablePoints = this:CheckNeighboringCells(character) + for _, otherStart in pairs(pathablePoints) do + local pathResult; + local success = pcall(function() + pathResult = PathfindingService:ComputeSmoothPathAsync(otherStart, point, 400) + smoothed = true + end) + if not success then + pathResult = PathfindingService:ComputeRawPathAsync(otherStart, point, 400) + smoothed = false + end + if pathResult and pathResult.Status == Enum.PathStatus.Success then + this.pathResult = pathResult + if this.pathResult then + this.pointList = this.pathResult:GetPointCoordinates() + table.insert(this.pointList, 1, otherStart) + end + break + end + end + end + if DirectPathEnabled then + if this.pathResult.Status ~= Enum.PathStatus.Success then + local directPathResult = this:ComputeDirectPath() + if directPathResult and directPathResult.Status == Enum.PathStatus.Success then + this.pathResult = directPathResult + this.pointList = directPathResult:GetPointCoordinates() + end + end + end + end + return smoothed + end + + function this:IsValidPath() + this:ComputePath() + local pathStatus = this.pathResult.Status + return pathStatus == Enum.PathStatus.Success + end + + function this:GetPathStatus() + this:ComputePath() + return this.pathResult.Status + end + + function this:Start() + if CurrentSeatPart then + return + end + spawn(function() + local humanoid = findPlayerHumanoid(Player) + --humanoid.AutoRotate = false + local torso = humanoid and humanoid.Torso + if torso then + if this.Started then return end + this.Started = true + -- Will yield the script since it is an Async function script (start, finish, maxDistance) + local smoothed = this:ComputePath() + if this:IsValidPath() then + this.PathStarted:fire() + -- smooth out zig-zaggy paths + local smoothPath = smoothed and this.pointList or this:SmoothPoints(this.pointList) + for i, point in pairs(smoothPath) do + if humanoid then + if this.Cancelled then + return + end + + local wayPoint = nil + if SHOW_PATH then + wayPoint = CreateDestinationIndicator(point) + wayPoint.BrickColor = BrickColor.new("New Yeller") + wayPoint.Parent = workspace + print(wayPoint.CFrame.p) + end + + humanoid:MoveTo(point) + + local distance = ((torso.CFrame.p - point) * XZ_VECTOR3).magnitude + local approxTime = 10 + if math_abs(humanoid.WalkSpeed) > 0 then + approxTime = distance / math_abs(humanoid.WalkSpeed) + end + + local yielding = true + + if i == 1 then + --local rotatedCFrame = CameraModule:LookAtPreserveHeight(point) + if CameraModule then + local rotatedCFrame = CameraModule:LookAtPreserveHeight(smoothPath[#smoothPath]) + local finishedSignal, duration = CameraModule:TweenCameraLook(rotatedCFrame) + end + --CharacterControl:SetTorsoLookPoint(point) + end + ---[[ + if (humanoid.Torso.CFrame.p - point).magnitude > 9 then + spawn(function() + while yielding and this.Cancelled == false do + if CameraModule then + local look = CameraModule:GetCameraLook() + local squashedLook = (look * XZ_VECTOR3).unit + local direction = ((point - CameraModule.cframe.p) * XZ_VECTOR3).unit + + local theta = math_deg(math_acos(squashedLook:Dot(direction))) + + if tick() - Utility.GetLastInput() > 2 and theta > (workspace.CurrentCamera.FieldOfView / 2) then + local rotatedCFrame = CameraModule:LookAtPreserveHeight(point) + local finishedSignal, duration = CameraModule:TweenCameraLook(rotatedCFrame) + --return + end + end + wait(0.1) + end + end) + end + --]] + local didReach = this:YieldUntilPointReached(character, point, approxTime * 3 + 1) + + yielding = false + + if SHOW_PATH then + wayPoint:Destroy() + end + + if not didReach then + this.PathFailed:fire() + return + end + end + end + + this.Finished:fire() + return + end + end + this.PathFailed:fire() + end) + end + + return this +end + +------------------------------------------------------------------------- + +local function FlashRed(object) + local origColor = object.BrickColor + local redColor = BrickColor.new("Really red") + local start = tick() + local duration = 4 + spawn(function() + while object and tick() - start < duration do + object.BrickColor = origColor + wait(0.13) + if object then + object.BrickColor = redColor + end + wait(0.13) + end + end) +end + +--local joystickWidth = 250 +--local joystickHeight = 250 +local function IsInBottomLeft(pt) + local joystickHeight = math.min(Utility.ViewSizeY() * 0.33, 250) + local joystickWidth = joystickHeight + return pt.X <= joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight +end + +local function IsInBottomRight(pt) + local joystickHeight = math.min(Utility.ViewSizeY() * 0.33, 250) + local joystickWidth = joystickHeight + return pt.X >= Utility.ViewSizeX() - joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight +end + +local function CheckAlive(character) + local humanoid = findPlayerHumanoid(Player) + return humanoid ~= nil and humanoid.Health > 0 +end + +local function GetEquippedTool(character) + if character ~= nil then + for _, child in pairs(character:GetChildren()) do + if child:IsA('Tool') then + return child + end + end + end +end + +local function ExploreWithRayCast(currentPoint, originDirection) + local TestDistance = 40 + local TestVectors = {} + do + local forwardVector = originDirection; + for i = 0, 15 do + table.insert(TestVectors, CFrame.Angles(0, math.pi / 8 * i, 0) * forwardVector) + end + end + + local testResults = {} + -- Heuristic should be something along the lines of distance and closeness to the traveling direction + local function ExploreHeuristic() + for _, testData in pairs(testResults) do + local walkDirection = -1 * originDirection + local directionCoeff = (walkDirection:Dot(testData['Vector']) + 1) / 2 + local distanceCoeff = testData['Distance'] / TestDistance + testData["Value"] = directionCoeff * distanceCoeff + end + end + + for i, vec in pairs(TestVectors) do + local hitPart, hitPos = Utility.Raycast(Ray.new(currentPoint, vec * TestDistance), true, {Player.Character}) + if hitPos then + table.insert(testResults, {Vector = vec; Distance = (hitPos - currentPoint).magnitude}) + else + table.insert(testResults, {Vector = vec; Distance = TestDistance}) + end + end + + ExploreHeuristic() + + table.sort(testResults, function(a,b) return a["Value"] > b["Value"] end) + + return testResults +end + +local TapId = 1 +local ExistingPather = nil +local ExistingIndicator = nil +local PathCompleteListener = nil +local PathFailedListener = nil + +local function CleanupPath() + DrivingTo = nil + if ExistingPather then + ExistingPather:Cancel() + end + if PathCompleteListener then + PathCompleteListener:disconnect() + PathCompleteListener = nil + end + if PathFailedListener then + PathFailedListener:disconnect() + PathFailedListener = nil + end + if ExistingIndicator then + DebrisService:AddItem(ExistingIndicator, 0) + ExistingIndicator = nil + end +end + +local function getExtentsSize(Parts) + local maxX = Parts[1].Position.X + local maxY = Parts[1].Position.Y + local maxZ = Parts[1].Position.Z + local minX = Parts[1].Position.X + local minY = Parts[1].Position.Y + local minZ = Parts[1].Position.Z + for i = 2, #Parts do + maxX = math_max(maxX, Parts[i].Position.X) + maxY = math_max(maxY, Parts[i].Position.Y) + maxZ = math_max(maxZ, Parts[i].Position.Z) + minX = math_min(minX, Parts[i].Position.X) + minY = math_min(minY, Parts[i].Position.Y) + minZ = math_min(minZ, Parts[i].Position.Z) + end + return Region3.new(Vector3_new(minX, minY, minZ), Vector3_new(maxX, maxY, maxZ)) +end + +local function inExtents(Extents, Position) + if Position.X < (Extents.CFrame.p.X - Extents.Size.X/2) or Position.X > (Extents.CFrame.p.X + Extents.Size.X/2) then + return false + end + if Position.Z < (Extents.CFrame.p.Z - Extents.Size.Z/2) or Position.Z > (Extents.CFrame.p.Z + Extents.Size.Z/2) then + return false + end + --ignoring Y for now + return true +end + +local AutoJumperInstance = nil +local ShootCount = 0 +local FailCount = 0 +local function OnTap(tapPositions, goToPoint) + -- Good to remember if this is the latest tap event + TapId = TapId + 1 + local thisTapId = TapId + + + local camera = workspace.CurrentCamera + local character = Player.Character + + + if not CheckAlive(character) then return end + + -- This is a path tap position + if #tapPositions == 1 or goToPoint then + if camera then + local unitRay = Utility.GetUnitRay(tapPositions[1].x, tapPositions[1].y, MyMouse.ViewSizeX, MyMouse.ViewSizeY, camera) + local ray = Ray.new(unitRay.Origin, unitRay.Direction*400) + local hitPart, hitPt = Utility.Raycast(ray, true, {character}) + + local hitChar, hitHumanoid = Utility.FindChacterAncestor(hitPart) + local torso = character and character:FindFirstChild("Humanoid") and character:FindFirstChild("Humanoid").Torso + local startPos = torso.CFrame.p + if goToPoint then + hitPt = goToPoint + hitChar = nil + end + if hitChar and hitHumanoid and hitHumanoid.Torso and (hitHumanoid.Torso.CFrame.p - torso.CFrame.p).magnitude < 7 then + CleanupPath() + + local myHumanoid = findPlayerHumanoid(Player) + if myHumanoid then + myHumanoid:MoveTo(hitPt) + end + + ShootCount = ShootCount + 1 + local thisShoot = ShootCount + -- Do shooot + local currentWeapon = GetEquippedTool(character) + if currentWeapon then + currentWeapon:Activate() + LastFired = tick() + end + elseif hitPt and character and not CurrentSeatPart then + local thisPather = Pather(character, hitPt) + if thisPather:IsValidPath() then + FailCount = 0 + -- TODO: Remove when bug in engine is fixed + Player:Move(Vector3_new(1, 0, 0)) + Player:Move(ZERO_VECTOR3) + thisPather:Start() + if BindableEvent_OnFailStateChanged then + BindableEvent_OnFailStateChanged:Fire(false) + end + CleanupPath() + + local destinationGlobe = CreateDestinationIndicator(hitPt) + destinationGlobe.Parent = camera + + ExistingPather = thisPather + ExistingIndicator = destinationGlobe + + if AutoJumperInstance then + AutoJumperInstance:Run() + end + + PathCompleteListener = thisPather.Finished:connect(function() + if AutoJumperInstance then + AutoJumperInstance:Stop() + end + if destinationGlobe then + if ExistingIndicator == destinationGlobe then + ExistingIndicator = nil + end + DebrisService:AddItem(destinationGlobe, 0) + destinationGlobe = nil + end + if hitChar then + local humanoid = findPlayerHumanoid(Player) + ShootCount = ShootCount + 1 + local thisShoot = ShootCount + -- Do shoot + local currentWeapon = GetEquippedTool(character) + if currentWeapon then + currentWeapon:Activate() + LastFired = tick() + end + if humanoid then + humanoid:MoveTo(hitPt) + end + end + local finishPos = torso and torso.CFrame.p --hitPt + if finishPos and startPos and tick() - Utility.GetLastInput() > 2 then + local exploreResults = ExploreWithRayCast(finishPos, ((startPos - finishPos) * XZ_VECTOR3).unit) + -- Check for Nans etc.. + if exploreResults[1] and exploreResults[1]["Vector"] and exploreResults[1]["Vector"].magnitude >= 0.5 and exploreResults[1]["Distance"] > 3 then + if CameraModule then + local rotatedCFrame = CameraModule:LookAtPreserveHeight(finishPos + exploreResults[1]["Vector"] * exploreResults[1]["Distance"]) + local finishedSignal, duration = CameraModule:TweenCameraLook(rotatedCFrame) + end + end + end + end) + PathFailedListener = thisPather.PathFailed:connect(function() + if AutoJumperInstance then + AutoJumperInstance:Stop() + end + if destinationGlobe then + FlashRed(destinationGlobe) + DebrisService:AddItem(destinationGlobe, 3) + end + end) + else + if hitPt then + -- Feedback here for when we don't have a good path + local failedGlobe = CreateDestinationIndicator(hitPt) + FlashRed(failedGlobe) + DebrisService:AddItem(failedGlobe, 1) + failedGlobe.Parent = camera + if ExistingIndicator == nil then + FailCount = FailCount + 1 + if FailCount >= 3 then + if BindableEvent_OnFailStateChanged then + BindableEvent_OnFailStateChanged:Fire(true) + end + CleanupPath() + end + end + end + end + elseif hitPt and character and CurrentSeatPart then + local destinationGlobe = CreateDestinationIndicator(hitPt) + destinationGlobe.Parent = camera + ExistingIndicator = destinationGlobe + DrivingTo = hitPt + local ConnectedParts = CurrentSeatPart:GetConnectedParts(true) + + while wait() do + if CurrentSeatPart and ExistingIndicator == destinationGlobe then + local ExtentsSize = getExtentsSize(ConnectedParts) + if inExtents(ExtentsSize, destinationGlobe.Position) then + DebrisService:AddItem(destinationGlobe, 0) + destinationGlobe = nil + DrivingTo = nil + break + end + else + DebrisService:AddItem(destinationGlobe, 0) + if CurrentSeatPart == nil and destinationGlobe == ExistingIndicator then + DrivingTo = nil + OnTap(tapPositions, hitPt) + end + destinationGlobe = nil + break + end + end + + else + -- no hit pt + end + end + elseif #tapPositions >= 2 then + if camera then + ShootCount = ShootCount + 1 + local thisShoot = ShootCount + -- Do shoot + local avgPoint = Utility.AveragePoints(tapPositions) + local unitRay = Utility.GetUnitRay(avgPoint.x, avgPoint.y, MyMouse.ViewSizeX, MyMouse.ViewSizeY, camera) + local currentWeapon = GetEquippedTool(character) + if currentWeapon then + currentWeapon:Activate() + LastFired = tick() + end + end + end +end + + +local function CreateClickToMoveModule() + local this = {} + + local LastStateChange = 0 + local LastState = Enum.HumanoidStateType.Running + local FingerTouches = {} + local NumUnsunkTouches = 0 + -- PC simulation + local mouse1Down = tick() + local mouse1DownPos = Vector2.new() + local mouse2Down = tick() + local mouse2DownPos = Vector2.new() + local mouse2Up = tick() + + local movementKeys = { + [Enum.KeyCode.W] = true; + [Enum.KeyCode.A] = true; + [Enum.KeyCode.S] = true; + [Enum.KeyCode.D] = true; + [Enum.KeyCode.Up] = true; + [Enum.KeyCode.Down] = true; + } + + local TapConn = nil + local InputBeganConn = nil + local InputChangedConn = nil + local InputEndedConn = nil + local HumanoidDiedConn = nil + local CharacterChildAddedConn = nil + local OnCharacterAddedConn = nil + local CharacterChildRemovedConn = nil + local RenderSteppedConn = nil + local HumanoidSeatedConn = nil + + local function disconnectEvent(event) + if event then + event:disconnect() + end + end + + local function DisconnectEvents() + disconnectEvent(TapConn) + disconnectEvent(InputBeganConn) + disconnectEvent(InputChangedConn) + disconnectEvent(InputEndedConn) + disconnectEvent(HumanoidDiedConn) + disconnectEvent(CharacterChildAddedConn) + disconnectEvent(OnCharacterAddedConn) + disconnectEvent(RenderSteppedConn) + disconnectEvent(CharacterChildRemovedConn) + pcall(function() RunService:UnbindFromRenderStep("ClickToMoveRenderUpdate") end) + disconnectEvent(HumanoidSeatedConn) + end + + + + local function IsFinite(num) + return num == num and num ~= 1/0 and num ~= -1/0 + end + + local function findAngleBetweenXZVectors(vec2, vec1) + return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z) + end + + -- Setup the camera + CameraModule = ClassicCameraModule() + + do + -- Extend The Camera Module Class + function CameraModule:LookAtPreserveHeight(newLookAtPt) + local camera = workspace.CurrentCamera + + local focus = camera.Focus.p + + local cameraCFrame = CameraModule.cframe + local mag = Vector3_new(cameraCFrame.lookVector.x, 0, cameraCFrame.lookVector.z).magnitude + local newLook = (Vector3_new(newLookAtPt.x, focus.y, newLookAtPt.z) - focus).unit * mag + local flippedLook = newLook + Vector3_new(0, cameraCFrame.lookVector.y, 0) + + local distance = (focus - cameraCFrame.p).magnitude + + local newCamPos = focus - flippedLook.unit * distance + return CFrame.new(newCamPos, newCamPos + flippedLook) + end + + local lerp = CFrame.new().lerp + function CameraModule:TweenCameraLook(desiredCFrame, speed) + local e = 2.718281828459 + + local function SCurve(t) + return 1/(1 + e^(-t*1.5)) + end + + local function easeOutSine(t, b, c, d) + if t >= d then return b + c end + return c * math_sin(t/d * (math_pi/2)) + b; + end + + local c0, c1 = CFrame_new(ZERO_VECTOR3, self:GetCameraLook()), desiredCFrame - desiredCFrame.p + local theta = GetThetaBetweenCFrames(c0, c1) + theta = clamp(0, math_pi, theta) + local duration = 0.65 * SCurve(theta - math_pi/4) + 0.15 + if speed then + duration = theta / speed + end + local start = tick() + local finish = start + duration + + self.UpdateTweenFunction = function() + local currTime = tick() - start + local alpha = clamp(0, 1, easeOutSine(currTime, 0, 1, duration)) + local newCFrame = lerp(c0, c1, alpha) + local y = findAngleBetweenXZVectors(newCFrame.lookVector, self:GetCameraLook()) + if IsFinite(y) and math_abs(y) > 0.0001 then + self.RotateInput = self.RotateInput + Vector2_new(y, 0) + end + return (currTime >= finish or alpha >= 1) + end + end + end + --- Done Extending + + + local function OnTouchBegan(input, processed) + if FingerTouches[input] == nil and not processed then + NumUnsunkTouches = NumUnsunkTouches + 1 + end + FingerTouches[input] = processed + end + + local function OnTouchChanged(input, processed) + if FingerTouches[input] == nil then + FingerTouches[input] = processed + if not processed then + NumUnsunkTouches = NumUnsunkTouches + 1 + end + end + end + + local function OnTouchEnded(input, processed) + --print("Touch tap fake:" , processed) + --if not processed then + -- OnTap({input.Position}) + --end + if FingerTouches[input] ~= nil and FingerTouches[input] == false then + NumUnsunkTouches = NumUnsunkTouches - 1 + end + FingerTouches[input] = nil + end + + + local function OnCharacterAdded(character) + DisconnectEvents() + + InputBeganConn = UIS.InputBegan:connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchBegan(input, processed) + + + -- Give back controls when they tap both sticks + local wasInBottomLeft = IsInBottomLeft(input.Position) + local wasInBottomRight = IsInBottomRight(input.Position) + if wasInBottomRight or wasInBottomLeft then + for otherInput, _ in pairs(FingerTouches) do + if otherInput ~= input then + local otherInputInLeft = IsInBottomLeft(otherInput.Position) + local otherInputInRight = IsInBottomRight(otherInput.Position) + if otherInput.UserInputState ~= Enum.UserInputState.End and ((wasInBottomLeft and otherInputInRight) or (wasInBottomRight and otherInputInLeft)) then + if BindableEvent_OnFailStateChanged then + BindableEvent_OnFailStateChanged:Fire(true) + end + return + end + end + end + end + end + + -- Cancel path when you use the keyboard controls. + if processed == false and input.UserInputType == Enum.UserInputType.Keyboard and movementKeys[input.KeyCode] then + CleanupPath() + end + if input.UserInputType == Enum.UserInputType.MouseButton1 then + mouse1Down = tick() + mouse1DownPos = input.Position + end + if input.UserInputType == Enum.UserInputType.MouseButton2 then + mouse2Down = tick() + mouse2DownPos = input.Position + end + end) + + InputChangedConn = UIS.InputChanged:connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchChanged(input, processed) + end + end) + + InputEndedConn = UIS.InputEnded:connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchEnded(input, processed) + end + + if input.UserInputType == Enum.UserInputType.MouseButton2 then + mouse2Up = tick() + local currPos = input.Position + if mouse2Up - mouse2Down < 0.25 and (currPos - mouse2DownPos).magnitude < 5 then + local positions = {currPos} + OnTap(positions) + end + end + end) + + TapConn = UIS.TouchTap:connect(function(touchPositions, processed) + if not processed then + OnTap(touchPositions) + end + end) + + if not UIS.TouchEnabled then -- PC + if AutoJumperInstance then + AutoJumperInstance:Stop() + AutoJumperInstance = nil + end + AutoJumperInstance = AutoJumper() + end + + local function getThrottleAndSteer(object, point) + local lookVector = (point - object.Position) + lookVector = Vector3_new(lookVector.X, 0, lookVector.Z).unit + local objectVector = Vector3_new(object.CFrame.lookVector.X, 0, object.CFrame.lookVector.Z).unit + local dirVector = lookVector - objectVector + local mag = dirVector.magnitude + local degrees = math_deg(math_acos(lookVector:Dot(objectVector))) + local side = (object.CFrame:pointToObjectSpace(point).X > 0) + local throttle = 0 + if mag < 0.25 then + throttle = 1 + end + if mag > 1.8 then + throttle = -1 + end + local distance = CurrentSeatPart.Position - DrivingTo + local velocity = CurrentSeatPart.Velocity + if velocity.magnitude*1.5 > distance.magnitude then + if velocity.magnitude*0.5 > distance.magnitude then + throttle = -throttle + else + throttle = 0 + end + end + local steer = 0 + if degrees > 5 and degrees < 175 then + if side then + steer = 1 + else + steer = -1 + end + end + local rotatingAt = math_deg(CurrentSeatPart.RotVelocity.magnitude) + local degreesAway = math_max(math_min(degrees, 180 - degrees), 10) + if (CurrentSeatPart.RotVelocity.X < 0)== (steer < 0) then + if rotatingAt*1.5 > degreesAway then + if rotatingAt*0.5 > degreesAway then + steer = -steer + else + steer = 0 + end + end + end + return throttle, steer + end + + local function Update() + if CameraModule then + if CameraModule.UserPanningTheCamera then + CameraModule.UpdateTweenFunction = nil + else + if CameraModule.UpdateTweenFunction then + local done = CameraModule.UpdateTweenFunction() + if done then + CameraModule.UpdateTweenFunction = nil + end + end + end + CameraModule:Update() + end + + if CurrentSeatPart then + if DrivingTo then + local throttle, steer = getThrottleAndSteer(CurrentSeatPart, DrivingTo) + CurrentSeatPart.Throttle = throttle + CurrentSeatPart.Steer = steer + end + end + end + + local success = pcall(function() RunService:BindToRenderStep("ClickToMoveRenderUpdate",Enum.RenderPriority.Camera.Value - 1,Update) end) + if not success then + if RenderSteppedConn then + RenderSteppedConn:disconnect() + end + RenderSteppedConn = RunService.RenderStepped:connect(Update) + end + + local WasAutoJumper = false + local WasAutoJumpMobile = false + local function onSeated(child, active, currentSeatPart) + if active then + if BindableEvent_EnableTouchJump then + BindableEvent_EnableTouchJump:Fire(true) + end + if currentSeatPart and currentSeatPart.ClassName == "VehicleSeat" then + CurrentSeatPart = currentSeatPart + if AutoJumperInstance then + AutoJumperInstance:Stop() + AutoJumperInstance = nil + WasAutoJumper = true + else + WasAutoJumper = false + end + if child.AutoJumpEnabled then + WasAutoJumpMobile = true + child.AutoJumpEnabled = false + end + end + else + CurrentSeatPart = nil + if BindableEvent_EnableTouchJump then + BindableEvent_EnableTouchJump:Fire(false) + end + if WasAutoJumper then + AutoJumperInstance = AutoJumper() + WasAutoJumper = false + end + if WasAutoJumpMobile then + child.AutoJumpEnabled = true + WasAutoJumpMobile = false + end + end + end + + local function OnCharacterChildAdded(child) + if UIS.TouchEnabled then + if child:IsA('Tool') then + child.ManualActivationOnly = true + end + end + if child:IsA('Humanoid') then + disconnectEvent(HumanoidDiedConn) + HumanoidDiedConn = child.Died:connect(function() + DebrisService:AddItem(ExistingIndicator, 1) + if AutoJumperInstance then + AutoJumperInstance:Stop() + AutoJumperInstance = nil + end + end) + local WasAutoJumper = false + local WasAutoJumpMobile = false + HumanoidSeatedConn = child.Seated:connect(function(active, seat) onSeated(child, active, seat) end) + if child.SeatPart then + onSeated(child, true, child.SeatPart) + end + end + end + + CharacterChildAddedConn = character.ChildAdded:connect(function(child) + OnCharacterChildAdded(child) + end) + CharacterChildRemovedConn = character.ChildRemoved:connect(function(child) + if UIS.TouchEnabled then + if child:IsA('Tool') then + child.ManualActivationOnly = false + end + end + end) + for _, child in pairs(character:GetChildren()) do + OnCharacterChildAdded(child) + end + end + + local Running = false + + function this:Stop() + if Running then + DisconnectEvents() + CleanupPath() + if AutoJumperInstance then + AutoJumperInstance:Stop() + AutoJumperInstance = nil + end + if CameraModule then + CameraModule.UpdateTweenFunction = nil + CameraModule:SetEnabled(false) + end + -- Restore tool activation on shutdown + if UIS.TouchEnabled then + local character = Player.Character + if character then + for _, child in pairs(character:GetChildren()) do + if child:IsA('Tool') then + child.ManualActivationOnly = false + end + end + end + end + DrivingTo = nil + Running = false + end + end + + function this:Start() + if not Running then + if Player.Character then -- retro-listen + OnCharacterAdded(Player.Character) + end + OnCharacterAddedConn = Player.CharacterAdded:connect(OnCharacterAdded) + if CameraModule then + CameraModule:SetEnabled(true) + end + Running = true + end + end + + return this +end + +return CreateClickToMoveModule diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/ClickToMove/NewClickToMove.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/ClickToMove/NewClickToMove.lua new file mode 100644 index 0000000..0c88557 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/ClickToMove/NewClickToMove.lua @@ -0,0 +1,1099 @@ +-- Written By Kip Turner, Copyright Roblox 2014 +-- Updated by Garnold to utilize the new PathfindingService API, 2017 + +local UIS = game:GetService("UserInputService") +local PathfindingService = game:GetService("PathfindingService") +local PlayerService = game:GetService("Players") +local RunService = game:GetService("RunService") +local DebrisService = game:GetService('Debris') +local ReplicatedStorage = game:GetService('ReplicatedStorage') +local tweenService = game:GetService("TweenService") + +local CameraScript = script:FindFirstAncestor("CameraScript") +local InvisicamModule = require(CameraScript:WaitForChild("Invisicam")) +local OrbitalCamModule = require(CameraScript:WaitForChild('RootCamera'):WaitForChild('OrbitalCamera')) + +local Player = PlayerService.LocalPlayer +local PlayerScripts = Player.PlayerScripts + +local ControlScript = PlayerScripts:FindFirstChild("ControlScript") +local MasterControl, TouchJump +if ControlScript then + MasterControl = ControlScript:FindFirstChild("MasterControl") + if MasterControl then + local TouchJumpModule = MasterControl:FindFirstChild("TouchJump") + if TouchJumpModule then + TouchJump = require(TouchJumpModule) + end + end +end + +local SHOW_PATH = true + +local RayCastIgnoreList = workspace.FindPartOnRayWithIgnoreList + +local math_min = math.min +local math_max = math.max +local math_pi = math.pi +local math_atan2 = math.atan2 + +local Vector3_new = Vector3.new +local Vector2_new = Vector2.new +local CFrame_new = CFrame.new + +local CurrentSeatPart = nil +local DrivingTo = nil + +local XZ_VECTOR3 = Vector3_new(1, 0, 1) +local ZERO_VECTOR3 = Vector3_new(0, 0, 0) +local ZERO_VECTOR2 = Vector2_new(0, 0) + +local lastFailedPosition = nil + +local BindableEvent_OnFailStateChanged = nil +if UIS.TouchEnabled then + BindableEvent_OnFailStateChanged = Instance.new('BindableEvent') + BindableEvent_OnFailStateChanged.Name = "OnClickToMoveFailStateChange" + BindableEvent_OnFailStateChanged.Parent = PlayerScripts +end + +--------------------------UTIL LIBRARY------------------------------- +local Utility = {} +do + local Signal = {} + function Signal.Create() + local sig = {} + + local mSignaler = Instance.new('BindableEvent') + + local mArgData = nil + local mArgDataCount = nil + + function sig:fire(...) + mArgData = {...} + mArgDataCount = select('#', ...) + mSignaler:Fire() + end + + function sig:connect(f) + if not f then error("connect(nil)", 2) end + return mSignaler.Event:connect(function() + f(unpack(mArgData, 1, mArgDataCount)) + end) + end + + function sig:wait() + mSignaler.Event:wait() + assert(mArgData, "Missing arg data, likely due to :TweenSize/Position corrupting threadrefs.") + return unpack(mArgData, 1, mArgDataCount) + end + + return sig + end + Utility.Signal = Signal + + function Utility.Create(instanceType) + return function(data) + local obj = Instance.new(instanceType) + for k, v in pairs(data) do + if type(k) == 'number' then + v.Parent = obj + else + obj[k] = v + end + end + return obj + end + end + + local function ViewSizeX() + local camera = workspace.CurrentCamera + local x = camera and camera.ViewportSize.X or 0 + local y = camera and camera.ViewportSize.Y or 0 + if x == 0 then + return 1024 + else + if x > y then + return x + else + return y + end + end + end + Utility.ViewSizeX = ViewSizeX + + local function ViewSizeY() + local camera = workspace.CurrentCamera + local x = camera and camera.ViewportSize.X or 0 + local y = camera and camera.ViewportSize.Y or 0 + if y == 0 then + return 768 + else + if x > y then + return y + else + return x + end + end + end + Utility.ViewSizeY = ViewSizeY + + local function FindChacterAncestor(part) + if part then + local humanoid = part:FindFirstChild("Humanoid") + if humanoid then + return part, humanoid + else + return FindChacterAncestor(part.Parent) + end + end + end + Utility.FindChacterAncestor = FindChacterAncestor + + local function Raycast(ray, ignoreNonCollidable, ignoreList) + local ignoreList = ignoreList or {} + local hitPart, hitPos, hitNorm, hitMat = RayCastIgnoreList(workspace, ray, ignoreList) + if hitPart then + if ignoreNonCollidable and hitPart.CanCollide == false then + table.insert(ignoreList, hitPart) + return Raycast(ray, ignoreNonCollidable, ignoreList) + end + return hitPart, hitPos, hitNorm, hitMat + end + return nil, nil + end + Utility.Raycast = Raycast + + local function AveragePoints(positions) + local avgPos = ZERO_VECTOR2 + if #positions > 0 then + for i = 1, #positions do + avgPos = avgPos + positions[i] + end + avgPos = avgPos / #positions + end + return avgPos + end + Utility.AveragePoints = AveragePoints + + local function FuzzyEquals(numa, numb) + return numa + 0.1 > numb and numa - 0.1 < numb + end + Utility.FuzzyEquals = FuzzyEquals + + local LastInput = 0 + UIS.InputBegan:connect(function(inputObject, wasSunk) + if not wasSunk then + if inputObject.UserInputType == Enum.UserInputType.Touch or + inputObject.UserInputType == Enum.UserInputType.MouseButton1 or + inputObject.UserInputType == Enum.UserInputType.MouseButton2 then + LastInput = tick() + end + end + end) + Utility.GetLastInput = function() + return LastInput + end +end + +local humanoidCache = {} +local function findPlayerHumanoid(player) + local character = player and player.Character + if character then + local resultHumanoid = humanoidCache[player] + if resultHumanoid and resultHumanoid.Parent == character then + return resultHumanoid + else + humanoidCache[player] = nil -- Bust Old Cache + local humanoid = character:FindFirstChildOfClass("Humanoid") + if humanoid then + humanoidCache[player] = humanoid + end + return humanoid + end + end +end + +--------------------------------------------------------- + +local Signal = Utility.Signal +local Create = Utility.Create + +--------------------------CHARACTER CONTROL------------------------------- +local CurrentIgnoreList + +local function GetCharacter() + return Player and Player.Character +end + +local function GetTorso() + local humanoid = findPlayerHumanoid(Player) + return humanoid and humanoid.Torso +end + +local function getIgnoreList() + if CurrentIgnoreList then + return CurrentIgnoreList + end + CurrentIgnoreList = {} + table.insert(CurrentIgnoreList, GetCharacter()) + return CurrentIgnoreList +end + +----------------------------------------------------------------------------- + +-----------------------------------PATHER-------------------------------------- + +local function createNewPopup(popupType) + + local newModel = Instance.new("ImageHandleAdornment") + + newModel.AlwaysOnTop = false + newModel.Image = "rbxasset://textures/Cursors/Gamepad/Pointer@2x.png" + newModel.ZIndex = 2 + + local size = ZERO_VECTOR2 + if popupType == "DestinationPopup" then + newModel.Color3 = Color3.fromRGB(0, 175, 255) + size = Vector2.new(4,4) + elseif popupType == "DirectWalkPopup" then + newModel.Color3 = Color3.fromRGB(255, 255, 100) + size = Vector2.new(4,4) + elseif popupType == "FailurePopup" then + newModel.Color3 = Color3.fromRGB(255, 100, 100) + size = Vector2.new(4,4) + elseif popupType == "PatherPopup" then + newModel.Color3 = Color3.fromRGB(255, 255, 255) + size = Vector2.new(3,3) + newModel.ZIndex = 1 + end + + local dataStructure = {} + dataStructure.Model = newModel + + function dataStructure:TweenIn() + local tween1 = tweenService:Create(self.Model, + TweenInfo.new( + 1, + Enum.EasingStyle.Elastic, + Enum.EasingDirection.Out, + 0, + false, + 0 + ),{ + Size = size + } + ) + tween1:Play() + return tween1 + end + + function dataStructure:TweenOut() + local tween1 = tweenService:Create(self.Model, + TweenInfo.new( + .25, + Enum.EasingStyle.Quad, + Enum.EasingDirection.In, + 0, + false, + 0 + ),{ + Size = ZERO_VECTOR2 + } + ) + tween1:Play() + return tween1 + end + + function dataStructure:Place(position, dest) + -- place the model at position + if not self.Model.Parent then + self.Model.Parent = workspace.Terrain + self.Model.Adornee = workspace.Terrain + self.Model.CFrame = CFrame.new(position,dest)*CFrame.Angles(math.pi/2,0,0)-Vector3.new(0,-.2,0) + end + end + + return dataStructure +end + +local function createPopupPath(points, numCircles) + -- creates a path with the provided points, using the path and number of circles provided + local popups = {} + local stopTraversing = false + + local function killPopup(i) + -- kill all popups before and at i + for iter, v in pairs(popups) do + if iter <= i then + local tween = v:TweenOut() + spawn(function() + tween.Completed:wait() + v.Model:Destroy() + end) + popups[iter] = nil + end + end + end + + local function stopFunction() + stopTraversing = true + killPopup(#points) + end + + spawn(function() + for i = 1, #points do + if stopTraversing then + break + end + + local includeWaypoint = i % numCircles == 0 + and i < #points + and (points[#points].Position - points[i].Position).magnitude > 4 + if includeWaypoint then + local popup = createNewPopup("PatherPopup") + popups[i] = popup + local nextPopup = points[i+1] + popup:Place(points[i].Position, nextPopup and nextPopup.Position or points[#points].Position) + local tween = popup:TweenIn() + wait(0.2) + end + end + end) + + return stopFunction, killPopup +end + +local function Pather(character, endPoint, surfaceNormal) + local this = {} + + this.Cancelled = false + this.Started = false + + this.Finished = Signal.Create() + this.PathFailed = Signal.Create() + this.PathStarted = Signal.Create() + + this.PathComputing = false + this.PathComputed = false + + this.TargetPoint = endPoint + this.TargetSurfaceNormal = surfaceNormal + + this.MoveToConn = nil + this.CurrentPoint = 0 + + function this:Cleanup() + if this.stopTraverseFunc then + this.stopTraverseFunc() + end + + if this.MoveToConn then + this.MoveToConn:disconnect() + this.MoveToConn = nil + this.humanoid = nil + end + + this.humanoid = nil + end + + function this:Cancel() + this.Cancelled = true + this:Cleanup() + end + + function this:ComputePath() + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + local success = false + if torso then + if this.PathComputed or this.PathComputing then return end + this.PathComputing = true + success = pcall(function() + this.pathResult = PathfindingService:FindPathAsync(torso.CFrame.p, this.TargetPoint) + end) + this.pointList = this.pathResult and this.pathResult:GetWaypoints() + this.PathComputing = false + this.PathComputed = this.pathResult and this.pathResult.Status == Enum.PathStatus.Success or false + end + return true + end + + function this:IsValidPath() + if not this.pathResult then + this:ComputePath() + end + return this.pathResult.Status == Enum.PathStatus.Success + end + + function this:OnPointReached(reached) + + if reached and not this.Cancelled then + + this.CurrentPoint = this.CurrentPoint + 1 + + if this.CurrentPoint > #this.pointList then + -- End of path reached + if this.stopTraverseFunc then + this.stopTraverseFunc() + end + this.Finished:fire() + this:Cleanup() + else + -- If next action == Jump, but the humanoid + -- is still jumping from a previous action + -- wait until it gets to the ground + if this.CurrentPoint + 1 <= #this.pointList then + local nextAction = this.pointList[this.CurrentPoint + 1].Action + if nextAction == Enum.PathWaypointAction.Jump then + local currentState = this.humanoid:GetState() + if currentState == Enum.HumanoidStateType.FallingDown or + currentState == Enum.HumanoidStateType.Freefall or + currentState == Enum.HumanoidStateType.Jumping then + + this.humanoid.FreeFalling:wait() + + -- Give time to the humanoid's state to change + -- Otherwise, the jump flag in Humanoid + -- will be reset by the state change + wait(0.1) + end + end + end + + -- Move to the next point + if this.setPointFunc then + this.setPointFunc(this.CurrentPoint) + end + + local nextWaypoint = this.pointList[this.CurrentPoint] + + if nextWaypoint.Action == Enum.PathWaypointAction.Jump then + this.humanoid.Jump = true + end + this.humanoid:MoveTo(nextWaypoint.Position) + end + else + this.PathFailed:fire() + this:Cleanup() + end + end + + function this:Start() + if CurrentSeatPart then + return + end + + this.humanoid = findPlayerHumanoid(Player) + if this.Started then return end + this.Started = true + + if SHOW_PATH then + -- choose whichever one Mike likes best + this.stopTraverseFunc, this.setPointFunc = createPopupPath(this.pointList, 4) + end + + if #this.pointList > 0 then + this.MoveToConn = this.humanoid.MoveToFinished:connect(function(reached) this:OnPointReached(reached) end) + this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it. + this:OnPointReached(true) -- Move to first point + else + this.PathFailed:fire() + if this.stopTraverseFunc then + this.stopTraverseFunc() + end + end + end + + this:ComputePath() + if not this.PathComputed then + -- set the end point towards the camera and raycasted towards the ground in case we hit a wall + local offsetPoint = this.TargetPoint + this.TargetSurfaceNormal*1.5 + local ray = Ray.new(offsetPoint, Vector3_new(0,-1,0)*50) + local newHitPart, newHitPos = RayCastIgnoreList(workspace, ray, getIgnoreList()) + if newHitPart then + this.TargetPoint = newHitPos + end + -- try again + this:ComputePath() + end + + return this +end + +------------------------------------------------------------------------- + +local function IsInBottomLeft(pt) + local joystickHeight = math_min(Utility.ViewSizeY() * 0.33, 250) + local joystickWidth = joystickHeight + return pt.X <= joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight +end + +local function IsInBottomRight(pt) + local joystickHeight = math_min(Utility.ViewSizeY() * 0.33, 250) + local joystickWidth = joystickHeight + return pt.X >= Utility.ViewSizeX() - joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight +end + +local function CheckAlive(character) + local humanoid = findPlayerHumanoid(Player) + return humanoid ~= nil and humanoid.Health > 0 +end + +local function GetEquippedTool(character) + if character ~= nil then + for _, child in pairs(character:GetChildren()) do + if child:IsA('Tool') then + return child + end + end + end +end + +local ExistingPather = nil +local ExistingIndicator = nil +local PathCompleteListener = nil +local PathFailedListener = nil + +local function CleanupPath() + DrivingTo = nil + if ExistingPather then + ExistingPather:Cancel() + end + if PathCompleteListener then + PathCompleteListener:disconnect() + PathCompleteListener = nil + end + if PathFailedListener then + PathFailedListener:disconnect() + PathFailedListener = nil + end + if ExistingIndicator then + local obj = ExistingIndicator + local tween = obj:TweenOut() + local tweenCompleteEvent = nil + tweenCompleteEvent = tween.Completed:connect(function() + tweenCompleteEvent:disconnect() + obj.Model:Destroy() + end) + ExistingIndicator = nil + end +end + +local function getExtentsSize(Parts) + local maxX,maxY,maxZ = -math.huge,-math.huge,-math.huge + local minX,minY,minZ = math.huge,math.huge,math.huge + for i = 1, #Parts do + maxX,maxY,maxZ = math_max(maxX, Parts[i].Position.X), math_max(maxY, Parts[i].Position.Y), math_max(maxZ, Parts[i].Position.Z) + minX,minY,minZ = math_min(minX, Parts[i].Position.X), math_min(minY, Parts[i].Position.Y), math_min(minZ, Parts[i].Position.Z) + end + return Region3.new(Vector3_new(minX, minY, minZ), Vector3_new(maxX, maxY, maxZ)) +end + +local function inExtents(Extents, Position) + if Position.X < (Extents.CFrame.p.X - Extents.Size.X/2) or Position.X > (Extents.CFrame.p.X + Extents.Size.X/2) then + return false + end + if Position.Z < (Extents.CFrame.p.Z - Extents.Size.Z/2) or Position.Z > (Extents.CFrame.p.Z + Extents.Size.Z/2) then + return false + end + --ignoring Y for now + return true +end + +local FailCount = 0 +local function OnTap(tapPositions, goToPoint) + -- Good to remember if this is the latest tap event + local camera = workspace.CurrentCamera + local character = Player.Character + + if not CheckAlive(character) then return end + + -- This is a path tap position + if #tapPositions == 1 or goToPoint then + if camera then + local unitRay = camera:ScreenPointToRay(tapPositions[1].x, tapPositions[1].y) + local ray = Ray.new(unitRay.Origin, unitRay.Direction*400) + + -- inivisicam stuff + local initIgnore = getIgnoreList() + local invisicamParts = InvisicamModule:GetObscuredParts() + local ignoreTab = {} + + -- add to the ignore list + for i, v in pairs(invisicamParts) do + ignoreTab[#ignoreTab+1] = i + end + for i = 1, #initIgnore do + ignoreTab[#ignoreTab+1] = initIgnore[i] + end + -- + local myHumanoid = findPlayerHumanoid(Player) + local hitPart, hitPt, hitNormal, hitMat = Utility.Raycast(ray, true, ignoreTab) + + local hitChar, hitHumanoid = Utility.FindChacterAncestor(hitPart) + local torso = GetTorso() + local startPos = torso.CFrame.p + if goToPoint then + hitPt = goToPoint + hitChar = nil + end + if hitChar and hitHumanoid and hitHumanoid.Torso and (hitHumanoid.Torso.CFrame.p - torso.CFrame.p).magnitude < 7 then + CleanupPath() + + if myHumanoid then + myHumanoid:MoveTo(hitPt) + end + -- Do shoot + local currentWeapon = GetEquippedTool(character) + if currentWeapon then + currentWeapon:Activate() + LastFired = tick() + end + elseif hitPt and character and not CurrentSeatPart then + local thisPather = Pather(character, hitPt, hitNormal) + if thisPather:IsValidPath() then + FailCount = 0 + + thisPather:Start() + if BindableEvent_OnFailStateChanged then + BindableEvent_OnFailStateChanged:Fire(false) + end + CleanupPath() + + local destinationPopup = createNewPopup("DestinationPopup") + destinationPopup:Place(hitPt, Vector3_new(0,hitPt.y,0)) + local failurePopup = createNewPopup("FailurePopup") + local currentTween = destinationPopup:TweenIn() + + + ExistingPather = thisPather + ExistingIndicator = destinationPopup + + PathCompleteListener = thisPather.Finished:connect(function() + if destinationPopup then + if ExistingIndicator == destinationPopup then + ExistingIndicator = nil + end + local tween = destinationPopup:TweenOut() + local tweenCompleteEvent = nil + tweenCompleteEvent = tween.Completed:connect(function() + tweenCompleteEvent:disconnect() + destinationPopup.Model:Destroy() + destinationPopup = nil + end) + end + if hitChar then + local humanoid = findPlayerHumanoid(Player) + local currentWeapon = GetEquippedTool(character) + if currentWeapon then + currentWeapon:Activate() + LastFired = tick() + end + if humanoid then + + humanoid:MoveTo(hitPt) + end + end + end) + PathFailedListener = thisPather.PathFailed:connect(function() + if failurePopup then + failurePopup:Place(hitPt, Vector3_new(0,hitPt.y,0)) + local failTweenIn = failurePopup:TweenIn() + failTweenIn.Completed:wait() + local failTweenOut = failurePopup:TweenOut() + failTweenOut.Completed:wait() + failurePopup.Model:Destroy() + failurePopup = nil + end + end) + else + if hitPt then + -- Feedback here for when we don't have a good path + local foundDirectPath = false + if (hitPt-startPos).Magnitude < 25 and (startPos.y-hitPt.y > -3) then + -- move directly here + if myHumanoid then + if myHumanoid.Sit then + myHumanoid.Jump = true + end + local currentPosition + myHumanoid:MoveTo(hitPt) + foundDirectPath = true + end + end + + spawn(function() + local directPopup = createNewPopup(foundDirectPath and "DirectWalkPopup" or "FailurePopup") + directPopup:Place(hitPt, Vector3_new(0,hitPt.y,0)) + local directTweenIn = directPopup:TweenIn() + directTweenIn.Completed:wait() + local directTweenOut = directPopup:TweenOut() + directTweenOut.Completed:wait() + directPopup.Model:Destroy() + directPopup = nil + end) + end + end + elseif hitPt and character and CurrentSeatPart then + local destinationPopup = createNewPopup("DestinationPopup") + ExistingIndicator = destinationPopup + destinationPopup:Place(hitPt, Vector3_new(0,hitPt.y,0)) + destinationPopup:TweenIn() + + DrivingTo = hitPt + local ConnectedParts = CurrentSeatPart:GetConnectedParts(true) + + while wait() do + if CurrentSeatPart and ExistingIndicator == destinationPopup then + local ExtentsSize = getExtentsSize(ConnectedParts) + if inExtents(ExtentsSize, hitPt) then + local popup = destinationPopup + spawn(function() + local tweenOut = popup:TweenOut() + tweenOut.Completed:wait() + popup.Model:Destroy() + end) + destinationPopup = nil + DrivingTo = nil + break + end + else + if CurrentSeatPart == nil and destinationPopup == ExistingIndicator then + DrivingTo = nil + OnTap(tapPositions, hitPt) + end + local popup = destinationPopup + spawn(function() + local tweenOut = popup:TweenOut() + tweenOut.Completed:wait() + popup.Model:Destroy() + end) + destinationPopup = nil + break + end + end + end + end + elseif #tapPositions >= 2 then + if camera then + -- Do shoot + local avgPoint = Utility.AveragePoints(tapPositions) + local unitRay = camera:ScreenPointToRay(avgPoint.x, avgPoint.y) + local currentWeapon = GetEquippedTool(character) + if currentWeapon then + currentWeapon:Activate() + LastFired = tick() + end + end + end +end + + +local function CreateClickToMoveModule() + local this = {} + + local LastStateChange = 0 + local LastState = Enum.HumanoidStateType.Running + local FingerTouches = {} + local NumUnsunkTouches = 0 + -- PC simulation + local mouse1Down = tick() + local mouse1DownPos = Vector2_new() + local mouse2Down = tick() + local mouse2DownPos = Vector2_new() + local mouse2Up = tick() + + local movementKeys = { + [Enum.KeyCode.W] = true; + [Enum.KeyCode.A] = true; + [Enum.KeyCode.S] = true; + [Enum.KeyCode.D] = true; + [Enum.KeyCode.Up] = true; + [Enum.KeyCode.Down] = true; + } + + local TapConn = nil + local InputBeganConn = nil + local InputChangedConn = nil + local InputEndedConn = nil + local HumanoidDiedConn = nil + local CharacterChildAddedConn = nil + local OnCharacterAddedConn = nil + local CharacterChildRemovedConn = nil + local RenderSteppedConn = nil + local HumanoidSeatedConn = nil + + local function disconnectEvent(event) + if event then + event:disconnect() + end + end + + local function DisconnectEvents() + disconnectEvent(TapConn) + disconnectEvent(InputBeganConn) + disconnectEvent(InputChangedConn) + disconnectEvent(InputEndedConn) + disconnectEvent(HumanoidDiedConn) + disconnectEvent(CharacterChildAddedConn) + disconnectEvent(OnCharacterAddedConn) + disconnectEvent(RenderSteppedConn) + disconnectEvent(CharacterChildRemovedConn) + pcall(function() RunService:UnbindFromRenderStep("ClickToMoveRenderUpdate") end) + disconnectEvent(HumanoidSeatedConn) + end + + + + local function IsFinite(num) + return num == num and num ~= 1/0 and num ~= -1/0 + end + + local function findAngleBetweenXZVectors(vec2, vec1) + return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z) + end + + -- Setup the camera + CameraModule = OrbitalCamModule() + + local function OnTouchBegan(input, processed) + if FingerTouches[input] == nil and not processed then + NumUnsunkTouches = NumUnsunkTouches + 1 + end + FingerTouches[input] = processed + end + + local function OnTouchChanged(input, processed) + if FingerTouches[input] == nil then + FingerTouches[input] = processed + if not processed then + NumUnsunkTouches = NumUnsunkTouches + 1 + end + end + end + + local function OnTouchEnded(input, processed) + if FingerTouches[input] ~= nil and FingerTouches[input] == false then + NumUnsunkTouches = NumUnsunkTouches - 1 + end + FingerTouches[input] = nil + end + + + local function OnCharacterAdded(character) + DisconnectEvents() + + InputBeganConn = UIS.InputBegan:connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchBegan(input, processed) + + -- Give back controls when they tap both sticks + local wasInBottomLeft = IsInBottomLeft(input.Position) + local wasInBottomRight = IsInBottomRight(input.Position) + if wasInBottomRight or wasInBottomLeft then + for otherInput, _ in pairs(FingerTouches) do + if otherInput ~= input then + local otherInputInLeft = IsInBottomLeft(otherInput.Position) + local otherInputInRight = IsInBottomRight(otherInput.Position) + if otherInput.UserInputState ~= Enum.UserInputState.End and ((wasInBottomLeft and otherInputInRight) or (wasInBottomRight and otherInputInLeft)) then + if BindableEvent_OnFailStateChanged then + BindableEvent_OnFailStateChanged:Fire(true) + end + return + end + end + end + end + end + + -- Cancel path when you use the keyboard controls. + if processed == false and input.UserInputType == Enum.UserInputType.Keyboard and movementKeys[input.KeyCode] then + CleanupPath() + end + if input.UserInputType == Enum.UserInputType.MouseButton1 then + mouse1Down = tick() + mouse1DownPos = input.Position + end + if input.UserInputType == Enum.UserInputType.MouseButton2 then + mouse2Down = tick() + mouse2DownPos = input.Position + end + end) + + InputChangedConn = UIS.InputChanged:connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchChanged(input, processed) + end + end) + + InputEndedConn = UIS.InputEnded:connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchEnded(input, processed) + end + + if input.UserInputType == Enum.UserInputType.MouseButton2 then + mouse2Up = tick() + local currPos = input.Position + if mouse2Up - mouse2Down < 0.25 and (currPos - mouse2DownPos).magnitude < 5 then + local positions = {currPos} + OnTap(positions) + end + end + end) + + TapConn = UIS.TouchTap:connect(function(touchPositions, processed) + if not processed then + OnTap(touchPositions) + end + end) + + local function computeThrottle(dist) + if dist > .2 then + return 0.5+(dist^2)/2 + else + return 0 + end + end + + local function getThrottleAndSteer(object, point) + local throttle, steer = 0, 0 + local oCF = object.CFrame + + local relativePosition = oCF:pointToObjectSpace(point) + local relativeZDirection = -relativePosition.z + local relativeDistance = relativePosition.magnitude + + -- throttle quadratically increases from 0-1 as distance from the selected point goes from 0-50, after 50, throttle is 1. + -- this allows shorter distance travel to have more fine-tuned control. + throttle = computeThrottle(math_min(1,relativeDistance/50))*math.sign(relativeZDirection) + + local steerAngle = -math_atan2(-relativePosition.x, -relativePosition.z) + steer = steerAngle/(math_pi/4) + return throttle, steer + end + + local function Update() + if CameraModule then + CameraModule.UserPanningTheCamera = true + if CameraModule.UserPanningTheCamera then + CameraModule.UpdateTweenFunction = nil + else + if CameraModule.UpdateTweenFunction then + local done = CameraModule.UpdateTweenFunction() + if done then + CameraModule.UpdateTweenFunction = nil + end + end + end + CameraModule:Update() + end + + if CurrentSeatPart then + if DrivingTo then + local throttle, steer = getThrottleAndSteer(CurrentSeatPart, DrivingTo) + CurrentSeatPart.ThrottleFloat = throttle + CurrentSeatPart.SteerFloat = steer + else + CurrentSeatPart.ThrottleFloat = 0 + CurrentSeatPart.SteerFloat = 0 + end + end + end + + RunService:BindToRenderStep("ClickToMoveRenderUpdate",Enum.RenderPriority.Camera.Value - 1,Update) + + local function onSeated(child, active, currentSeatPart) + if active then + if TouchJump and UIS.TouchEnabled then + TouchJump:Enable() + end + if currentSeatPart and currentSeatPart.ClassName == "VehicleSeat" then + CurrentSeatPart = currentSeatPart + end + else + CurrentSeatPart = nil + if TouchJump and UIS.TouchEnabled then + TouchJump:Disable() + end + end + end + + local function OnCharacterChildAdded(child) + if UIS.TouchEnabled then + if child:IsA('Tool') then + child.ManualActivationOnly = true + end + end + if child:IsA('Humanoid') then + disconnectEvent(HumanoidDiedConn) + HumanoidDiedConn = child.Died:connect(function() + if ExistingIndicator then + DebrisService:AddItem(ExistingIndicator.Model, 1) + end + end) + HumanoidSeatedConn = child.Seated:connect(function(active, seat) onSeated(child, active, seat) end) + if child.SeatPart then + onSeated(child, true, child.SeatPart) + end + end + end + + CharacterChildAddedConn = character.ChildAdded:connect(function(child) + OnCharacterChildAdded(child) + end) + CharacterChildRemovedConn = character.ChildRemoved:connect(function(child) + if UIS.TouchEnabled then + if child:IsA('Tool') then + child.ManualActivationOnly = false + end + end + end) + for _, child in pairs(character:GetChildren()) do + OnCharacterChildAdded(child) + end + end + + local Running = false + + function this:Stop() + if Running then + DisconnectEvents() + CleanupPath() + if CameraModule then + CameraModule.UpdateTweenFunction = nil + CameraModule:SetEnabled(false) + end + -- Restore tool activation on shutdown + if UIS.TouchEnabled then + local character = Player.Character + if character then + for _, child in pairs(character:GetChildren()) do + if child:IsA('Tool') then + child.ManualActivationOnly = false + end + end + end + end + DrivingTo = nil + Running = false + end + end + + function this:Start() + if not Running then + if Player.Character then -- retro-listen + OnCharacterAdded(Player.Character) + end + OnCharacterAddedConn = Player.CharacterAdded:connect(OnCharacterAdded) + if CameraModule then + CameraModule:SetEnabled(true) + end + Running = true + end + end + + return this +end + +return CreateClickToMoveModule diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/Invisicam.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/Invisicam.lua new file mode 100644 index 0000000..6d1d185 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/Invisicam.lua @@ -0,0 +1,584 @@ +--[[ + Invisicam + + Modified 5.16.2017 by AllYourBlox to consider combined transparency when looking through multiple parts, and to add + a mode that takes advantage of the reduced cost of ray casts from re-implementing GetPartsObscuringTarget + on the C++ side to be O(N) for N parts hit, rather than O(N^2), and to have optimizations specifically + improving performance of closely bundled rays. Fading is also removed, since it is a frame rate killer with high-poly models, + and on mobile. + + Based on Invisicam Version 2.5 by OnlyTwentyCharacters +--]] + +local Invisicam = {} +--------------- +-- Constants -- +--------------- +local USE_STACKING_TRANSPARENCY = true -- Multiple items between the subject and camera get transparency values that add up to TARGET_TRANSPARENCY +local TARGET_TRANSPARENCY = 0.75 -- Classic Invisicam's Value, also used by new invisicam for parts hit by head and torso rays +local TARGET_TRANSPARENCY_PERIPHERAL = 0.5 -- Used by new SMART_CIRCLE mode for items not hit by head and torso rays + +local MODE = { + CUSTOM = 1, -- Whatever you want! + LIMBS = 2, -- Track limbs + MOVEMENT = 3, -- Track movement + CORNERS = 4, -- Char model corners + CIRCLE1 = 5, -- Circle of casts around character + CIRCLE2 = 6, -- Circle of casts around character, camera relative + LIMBMOVE = 7, -- LIMBS mode + MOVEMENT mode + SMART_CIRCLE = 8, -- More sample points on and around character + CHAR_OUTLINE = 9, +} + +Invisicam.MODE = MODE +local STARTING_MODE = MODE.SMART_CIRCLE + +local LIMB_TRACKING_SET = { + -- Common to R6, R15 + ['Head'] = true, + + -- R6 Only + ['Left Arm'] = true, + ['Right Arm'] = true, + ['Left Leg'] = true, + ['Right Leg'] = true, + + -- R15 Only + ['LeftLowerArm'] = true, + ['RightLowerArm'] = true, + ['LeftUpperLeg'] = true, + ['RightUpperLeg'] = true +} + +local CORNER_FACTORS = { + Vector3.new(1,1,-1), + Vector3.new(1,-1,-1), + Vector3.new(-1,-1,-1), + Vector3.new(-1,1,-1) +} + +local CIRCLE_CASTS = 10 +local MOVE_CASTS = 3 +local SMART_CIRCLE_CASTS = 24 +local SMART_CIRCLE_INCREMENT = 2.0 * math.pi / SMART_CIRCLE_CASTS +local CHAR_OUTLINE_CASTS = 24 + +--------------- +-- Variables -- +--------------- + +local RunService = game:GetService('RunService') +local PlayersService = game:GetService('Players') +local Player = PlayersService.LocalPlayer + +local Camera = nil +local Character = nil +local HumanoidRootPart = nil +local TorsoPart = nil +local HeadPart = nil + +local Mode = nil +local BehaviorFunction = nil + +local childAddedConn = nil +local childRemovedConn = nil + +local Behaviors = {} -- Map of modes to behavior fns +local SavedHits = {} -- Objects currently being faded in/out +local TrackedLimbs = {} -- Used in limb-tracking casting modes + +--------------- +--| Utility |-- +--------------- + +local math_min = math.min +local math_max = math.max +local math_cos = math.cos +local math_sin = math.sin +local math_pi = math.pi + +local Vector3_new = Vector3.new +local ZERO_VECTOR3 = Vector3_new(0,0,0) + +local function AssertTypes(param, ...) + local allowedTypes = {} + local typeString = '' + for _, typeName in pairs({...}) do + allowedTypes[typeName] = true + typeString = typeString .. (typeString == '' and '' or ' or ') .. typeName + end + local theType = type(param) + assert(allowedTypes[theType], typeString .. " type expected, got: " .. theType) +end + +----------------------- +--| Local Functions |-- +----------------------- + +local function LimbBehavior(castPoints) + for limb, _ in pairs(TrackedLimbs) do + castPoints[#castPoints + 1] = limb.Position + end +end + +local function MoveBehavior(castPoints) + for i = 1, MOVE_CASTS do + local position, velocity = HumanoidRootPart.Position, HumanoidRootPart.Velocity + local horizontalSpeed = Vector3_new(velocity.X, 0, velocity.Z).Magnitude / 2 + local offsetVector = (i - 1) * HumanoidRootPart.CFrame.lookVector * horizontalSpeed + castPoints[#castPoints + 1] = position + offsetVector + end +end + +local function CornerBehavior(castPoints) + local cframe = HumanoidRootPart.CFrame + local centerPoint = cframe.p + local rotation = cframe - centerPoint + local halfSize = Character:GetExtentsSize() / 2 --NOTE: Doesn't update w/ limb animations + castPoints[#castPoints + 1] = centerPoint + for i = 1, #CORNER_FACTORS do + castPoints[#castPoints + 1] = centerPoint + (rotation * (halfSize * CORNER_FACTORS[i])) + end +end + +local function CircleBehavior(castPoints) + local cframe = nil + if Mode == MODE.CIRCLE1 then + cframe = HumanoidRootPart.CFrame + else + local camCFrame = Camera.CoordinateFrame + cframe = camCFrame - camCFrame.p + HumanoidRootPart.Position + end + castPoints[#castPoints + 1] = cframe.p + for i = 0, CIRCLE_CASTS - 1 do + local angle = (2 * math_pi / CIRCLE_CASTS) * i + local offset = 3 * Vector3_new(math_cos(angle), math_sin(angle), 0) + castPoints[#castPoints + 1] = cframe * offset + end +end + +local function LimbMoveBehavior(castPoints) + LimbBehavior(castPoints) + MoveBehavior(castPoints) +end + +local function CharacterOutlineBehavior(castPoints) + local torsoUp = TorsoPart.CFrame.upVector.unit + local torsoRight = TorsoPart.CFrame.rightVector.unit + + -- Torso cross of points for interior coverage + castPoints[#castPoints + 1] = TorsoPart.CFrame.p + castPoints[#castPoints + 1] = TorsoPart.CFrame.p + torsoUp + castPoints[#castPoints + 1] = TorsoPart.CFrame.p - torsoUp + castPoints[#castPoints + 1] = TorsoPart.CFrame.p + torsoRight + castPoints[#castPoints + 1] = TorsoPart.CFrame.p - torsoRight + if HeadPart then + castPoints[#castPoints + 1] = HeadPart.CFrame.p + end + + local cframe = CFrame.new(ZERO_VECTOR3,Vector3_new(Camera.CoordinateFrame.lookVector.X,0,Camera.CoordinateFrame.lookVector.Z)) + local centerPoint = (TorsoPart and TorsoPart.Position or HumanoidRootPart.Position) + + local partsWhitelist = {TorsoPart} + if HeadPart then + partsWhitelist[#partsWhitelist + 1] = HeadPart + end + + for i = 1, CHAR_OUTLINE_CASTS do + local angle = (2 * math_pi * i / CHAR_OUTLINE_CASTS) + local offset = cframe * (3 * Vector3_new(math_cos(angle), math_sin(angle), 0)) + + offset = Vector3_new(offset.X, math_max(offset.Y, -2.25), offset.Z) + + local ray = Ray.new(centerPoint + offset, -3 * offset) + local hit, hitPoint = game.Workspace:FindPartOnRayWithWhitelist(ray, partsWhitelist, false, false) + + if hit then + -- Use hit point as the cast point, but nudge it slightly inside the character so that bumping up against + -- walls is less likely to cause a transparency glitch + castPoints[#castPoints + 1] = hitPoint + 0.2 * (centerPoint - hitPoint).unit + end + end +end + +-- Helper function for Determinant of 3x3 +local function Det3x3(a,b,c,d,e,f,g,h,i) + return (a*(e*i-f*h)-b*(d*i-f*g)+c*(d*h-e*g)) +end + +-- Smart Circle mode needs the intersection of 2 rays that are known to be in the same plane +-- because they are generated from cross products with a common vector. This function is computing +-- that intersection, but it's actually the general solution for the point halfway between where +-- two skew lines come nearest to each other, which is more forgiving. +local function RayIntersection(p0, v0, p1, v1) + local v2 = v0:Cross(v1) + local d1 = p1.x - p0.x + local d2 = p1.y - p0.y + local d3 = p1.z - p0.z + local denom = Det3x3(v0.x,-v1.x,v2.x,v0.y,-v1.y,v2.y,v0.z,-v1.z,v2.z) + + if (denom == 0) then + return ZERO_VECTOR3 -- No solution (rays are parallel) + end + + local t0 = Det3x3(d1,-v1.x,v2.x,d2,-v1.y,v2.y,d3,-v1.z,v2.z) / denom + local t1 = Det3x3(v0.x,d1,v2.x,v0.y,d2,v2.y,v0.z,d3,v2.z) / denom + local s0 = p0 + t0 * v0 + local s1 = p1 + t1 * v1 + local s = s0 + 0.5 * ( s1 - s0 ) + + -- 0.25 studs is a threshold for deciding if the rays are + -- close enough to be considered intersecting, found through testing + if (s1-s0).Magnitude < 0.25 then + return s + else + return ZERO_VECTOR3 + end +end + +local function SmartCircleBehavior(castPoints) + local torsoUp = TorsoPart.CFrame.upVector.unit + local torsoRight = TorsoPart.CFrame.rightVector.unit + + -- SMART_CIRCLE mode includes rays to head and 5 to the torso. + -- Hands, arms, legs and feet are not included since they + -- are not canCollide and can therefore go inside of parts + castPoints[#castPoints + 1] = TorsoPart.CFrame.p + castPoints[#castPoints + 1] = TorsoPart.CFrame.p + torsoUp + castPoints[#castPoints + 1] = TorsoPart.CFrame.p - torsoUp + castPoints[#castPoints + 1] = TorsoPart.CFrame.p + torsoRight + castPoints[#castPoints + 1] = TorsoPart.CFrame.p - torsoRight + if HeadPart then + castPoints[#castPoints + 1] = HeadPart.CFrame.p + end + + local cameraOrientation = Camera.CFrame - Camera.CFrame.p + local torsoPoint = Vector3_new(0,0.5,0) + (TorsoPart and TorsoPart.Position or HumanoidRootPart.Position) + local radius = 2.5 + + -- This loop first calculates points in a circle of radius 2.5 around the torso of the character, in the + -- plane orthogonal to the camera's lookVector. Each point is then raycast to, to determine if it is within + -- the free space surrounding the player (not inside anything). Two iterations are done to adjust points that + -- are inside parts, to try to move them to valid locations that are still on their camera ray, so that the + -- circle remains circular from the camera's perspective, but does not cast rays into walls or parts that are + -- behind, below or beside the character and not really obstructing view of the character. This minimizes + -- the undesirable situation where the character walks up to an exterior wall and it is made invisible even + -- though it is behind the character. + for i = 1, SMART_CIRCLE_CASTS do + local angle = SMART_CIRCLE_INCREMENT * i - 0.5 * math.pi + local offset = radius * Vector3_new(math_cos(angle), math_sin(angle), 0) + local circlePoint = torsoPoint + cameraOrientation * offset + + -- Vector from camera to point on the circle being tested + local vp = circlePoint - Camera.CFrame.p + + local ray = Ray.new(torsoPoint, circlePoint - torsoPoint) + local hit, hp, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {Character}, false, false ) + local castPoint = circlePoint + + if hit then + local hprime = hp + 0.1 * hitNormal.unit -- Slightly offset hit point from the hit surface + local v0 = hprime - torsoPoint -- Vector from torso to offset hit point + local d0 = v0.magnitude + + local perp = (v0:Cross(vp)).unit + + -- Vector from the offset hit point, along the hit surface + local v1 = (perp:Cross(hitNormal)).unit + + -- Vector from camera to offset hit + local vprime = (hprime - Camera.CFrame.p).unit + + -- This dot product checks to see if the vector along the hit surface would hit the correct + -- side of the invisicam cone, or if it would cross the camera look vector and hit the wrong side + if ( v0.unit:Dot(-v1) < v0.unit:Dot(vprime)) then + castPoint = RayIntersection(hprime, v1, circlePoint, vp) + + if castPoint.Magnitude > 0 then + local ray = Ray.new(hprime, castPoint - hprime) + local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {Character}, false, false ) + + if hit then + local hprime2 = hitPoint + 0.1 * hitNormal.unit + castPoint = hprime2 + end + else + castPoint = hprime + end + else + castPoint = hprime + end + + local ray = Ray.new(torsoPoint, (castPoint - torsoPoint)) + local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {Character}, false, false ) + + if hit then + local castPoint2 = hitPoint - 0.1 * (castPoint - torsoPoint).unit + castPoint = castPoint2 + end + end + + castPoints[#castPoints + 1] = castPoint + end +end + +local function CheckTorsoReference() + if Character then + TorsoPart = Character:FindFirstChild("Torso") + if not TorsoPart then + TorsoPart = Character:FindFirstChild("UpperTorso") + if not TorsoPart then + TorsoPart = Character:FindFirstChild("HumanoidRootPart") + end + end + + HeadPart = Character:FindFirstChild("Head") + end +end + +local function OnCharacterAdded(character) + if childAddedConn then + childAddedConn:disconnect() + childAddedConn = nil + end + if childRemovedConn then + childRemovedConn:disconnect() + childRemovedConn = nil + end + + Character = character + + TrackedLimbs = {} + local function childAdded(child) + if child:IsA('BasePart') then + if LIMB_TRACKING_SET[child.Name] then + TrackedLimbs[child] = true + end + + if (child.Name == 'Torso' or child.Name == 'UpperTorso') then + TorsoPart = child + end + + if (child.Name == 'Head') then + HeadPart = child + end + end + end + + local function childRemoved(child) + TrackedLimbs[child] = nil + + -- If removed/replaced part is 'Torso' or 'UpperTorso' double check that we still have a TorsoPart to use + CheckTorsoReference() + end + + childAddedConn = character.ChildAdded:connect(childAdded) + childRemovedConn = character.ChildRemoved:connect(childRemoved) + + for _, child in pairs(Character:GetChildren()) do + childAdded(child) + end +end + +local function OnCurrentCameraChanged() + local newCamera = workspace.CurrentCamera + if newCamera then + Camera = newCamera + end +end + +----------------------- +-- Exposed Functions -- +----------------------- + +-- Update. Called every frame after the camera movement step +function Invisicam:Update() + + -- Bail if there is no Character + if not Character then return end + + -- Make sure we still have a HumanoidRootPart + if not HumanoidRootPart then + local humanoid = Character:FindFirstChildOfClass("Humanoid") + if humanoid and humanoid.Torso then + HumanoidRootPart = humanoid.Torso + else + -- Not set up with Humanoid? Try and see if there's one in the Character at all: + HumanoidRootPart = Character:FindFirstChild("HumanoidRootPart") + if not HumanoidRootPart then + -- Bail out, since we're relying on HumanoidRootPart existing + return + end + end + local ancestryChangedConn; + ancestryChangedConn = HumanoidRootPart.AncestryChanged:connect(function(child, parent) + if child == HumanoidRootPart and not parent then + HumanoidRootPart = nil + if ancestryChangedConn and ancestryChangedConn.Connected then + ancestryChangedConn:Disconnect() + ancestryChangedConn = nil + end + end + end) + end + + if not TorsoPart then + CheckTorsoReference() + if not TorsoPart then + -- Bail out, since we're relying on Torso existing, should never happen since we fall back to using HumanoidRootPart as torso + return + end + end + + -- Make a list of world points to raycast to + local castPoints = {} + BehaviorFunction(castPoints) + + -- Cast to get a list of objects between the camera and the cast points + local currentHits = {} + local ignoreList = {Character} + local function add(hit) + currentHits[hit] = true + if not SavedHits[hit] then + SavedHits[hit] = hit.LocalTransparencyModifier + end + end + + local hitParts + local hitPartCount = 0 + + -- Hash table to treat head-ray-hit parts differently than the rest of the hit parts hit by other rays + -- head/torso ray hit parts will be more transparent than peripheral parts when USE_STACKING_TRANSPARENCY is enabled + local headTorsoRayHitParts = {} + local partIsTouchingCamera = {} + + local perPartTransparencyHeadTorsoHits = TARGET_TRANSPARENCY + local perPartTransparencyOtherHits = TARGET_TRANSPARENCY + + if USE_STACKING_TRANSPARENCY then + + -- This first call uses head and torso rays to find out how many parts are stacked up + -- for the purpose of calculating required per-part transparency + local headPoint = HeadPart and HeadPart.CFrame.p or castPoints[1] + local torsoPoint = TorsoPart and TorsoPart.CFrame.p or castPoints[2] + hitParts = Camera:GetPartsObscuringTarget({headPoint, torsoPoint}, ignoreList) + + -- Count how many things the sample rays passed through, including decals. This should only + -- count decals facing the camera, but GetPartsObscuringTarget does not return surface normals, + -- so my compromise for now is to just let any decal increase the part count by 1. Only one + -- decal per part will be considered. + for i = 1, #hitParts do + local hitPart = hitParts[i] + hitPartCount = hitPartCount + 1 -- count the part itself + headTorsoRayHitParts[hitPart] = true + for _, child in pairs(hitPart:GetChildren()) do + if child:IsA('Decal') or child:IsA('Texture') then + hitPartCount = hitPartCount + 1 -- count first decal hit, then break + break + end + end + end + + if (hitPartCount > 0) then + perPartTransparencyHeadTorsoHits = math.pow( ((0.5 * TARGET_TRANSPARENCY) + (0.5 * TARGET_TRANSPARENCY / hitPartCount)), 1 / hitPartCount ) + perPartTransparencyOtherHits = math.pow( ((0.5 * TARGET_TRANSPARENCY_PERIPHERAL) + (0.5 * TARGET_TRANSPARENCY_PERIPHERAL / hitPartCount)), 1 / hitPartCount ) + end + end + + -- Now get all the parts hit by all the rays + hitParts = Camera:GetPartsObscuringTarget(castPoints, ignoreList) + + local partTargetTransparency = {} + + -- Include decals and textures + for i = 1, #hitParts do + local hitPart = hitParts[i] + + partTargetTransparency[hitPart] =headTorsoRayHitParts[hitPart] and perPartTransparencyHeadTorsoHits or perPartTransparencyOtherHits + + -- If the part is not already as transparent or more transparent than what invisicam requires, add it to the list of + -- parts to be modified by invisicam + if hitPart.Transparency < partTargetTransparency[hitPart] then + add(hitPart) + end + + -- Check all decals and textures on the part + for _, child in pairs(hitPart:GetChildren()) do + if child:IsA('Decal') or child:IsA('Texture') then + if (child.Transparency < partTargetTransparency[hitPart]) then + partTargetTransparency[child] = partTargetTransparency[hitPart] + add(child) + end + end + end + end + + -- Invisibilize objects that are in the way, restore those that aren't anymore + for hitPart, originalLTM in pairs(SavedHits) do + if currentHits[hitPart] then + -- LocalTransparencyModifier gets whatever value is required to print the part's total transparency to equal perPartTransparency + hitPart.LocalTransparencyModifier = (hitPart.Transparency < 1) and ((partTargetTransparency[hitPart] - hitPart.Transparency) / (1.0 - hitPart.Transparency)) or 0 + else -- Restore original pre-invisicam value of LTM + hitPart.LocalTransparencyModifier = originalLTM + SavedHits[hitPart] = nil + end + end +end + +function Invisicam:SetMode(newMode) + AssertTypes(newMode, 'number') + for modeName, modeNum in pairs(MODE) do + if modeNum == newMode then + Mode = newMode + BehaviorFunction = Behaviors[Mode] + return + end + end + error("Invalid mode number") +end + +function Invisicam:SetCustomBehavior(func) + AssertTypes(func, 'function') + Behaviors[MODE.CUSTOM] = func + if Mode == MODE.CUSTOM then + BehaviorFunction = func + end +end + +function Invisicam:GetObscuredParts() + return SavedHits +end + +-- Want to turn off Invisicam? Be sure to call this after. +function Invisicam:Cleanup() + for hit, originalFade in pairs(SavedHits) do + hit.LocalTransparencyModifier = originalFade + end +end + +--------------------- +--| Running Logic |-- +--------------------- + +-- Connect to the current and all future cameras +workspace:GetPropertyChangedSignal("CurrentCamera"):connect(OnCurrentCameraChanged) +OnCurrentCameraChanged() + +Player.CharacterAdded:connect(OnCharacterAdded) +if Player.Character then + OnCharacterAdded(Player.Character) +end + +Behaviors[MODE.CUSTOM] = function() end -- (Does nothing until SetCustomBehavior) +Behaviors[MODE.LIMBS] = LimbBehavior +Behaviors[MODE.MOVEMENT] = MoveBehavior +Behaviors[MODE.CORNERS] = CornerBehavior +Behaviors[MODE.CIRCLE1] = CircleBehavior +Behaviors[MODE.CIRCLE2] = CircleBehavior +Behaviors[MODE.LIMBMOVE] = LimbMoveBehavior +Behaviors[MODE.SMART_CIRCLE] = SmartCircleBehavior +Behaviors[MODE.CHAR_OUTLINE] = CharacterOutlineBehavior + +Invisicam:SetMode(STARTING_MODE) + +return Invisicam diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/PopperCam.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/PopperCam.lua new file mode 100644 index 0000000..7e325ea --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/PopperCam.lua @@ -0,0 +1,194 @@ +-- PopperCam Version 16 +-- OnlyTwentyCharacters + +local PopperCam = {} -- Guarantees your players won't see outside the bounds of your map! + +----------------- +--| Constants |-- +----------------- + +local POP_RESTORE_RATE = 0.3 +local MIN_CAMERA_ZOOM = 0.5 + +local VALID_SUBJECTS = { + 'Humanoid', + 'VehicleSeat', + 'SkateboardPlatform', +} + +local portraitPopperFixFlagExists, portraitPopperFixFlagEnabled = pcall(function() + return UserSettings():IsUserFeatureEnabled("UserPortraitPopperFix") +end) +local FFlagUserPortraitPopperFix = portraitPopperFixFlagExists and portraitPopperFixFlagEnabled + +----------------- +--| Variables |-- +----------------- + +local PlayersService = game:GetService('Players') + +local Camera = nil +local CameraSubjectChangeConn = nil + +local SubjectPart = nil + +local PlayerCharacters = {} -- For ignoring in raycasts +local VehicleParts = {} -- Also just for ignoring + +local LastPopAmount = 0 +local LastZoomLevel = 0 +local PopperEnabled = false + +local CFrame_new = CFrame.new + +----------------------- +--| Local Functions |-- +----------------------- + +local math_abs = math.abs + +local function OnCameraSubjectChanged() + VehicleParts = {} + + local newSubject = Camera.CameraSubject + if newSubject then + -- Determine if we should be popping at all + PopperEnabled = false + for _, subjectType in pairs(VALID_SUBJECTS) do + if newSubject:IsA(subjectType) then + PopperEnabled = true + break + end + end + + -- Get all parts of the vehicle the player is controlling + if newSubject:IsA('VehicleSeat') then + VehicleParts = newSubject:GetConnectedParts(true) + end + + if FFlagUserPortraitPopperFix then + if newSubject:IsA("BasePart") then + SubjectPart = newSubject + elseif newSubject:IsA("Model") then + SubjectPart = newSubject.PrimaryPart + elseif newSubject:IsA("Humanoid") then + SubjectPart = newSubject.Torso + end + end + end +end + +local function OnCharacterAdded(player, character) + PlayerCharacters[player] = character +end + +local function OnPlayersChildAdded(child) + if child:IsA('Player') then + child.CharacterAdded:connect(function(character) + OnCharacterAdded(child, character) + end) + if child.Character then + OnCharacterAdded(child, child.Character) + end + end +end + +local function OnPlayersChildRemoved(child) + if child:IsA('Player') then + PlayerCharacters[child] = nil + end +end + + local function OnWorkspaceChanged(property) + if property == 'CurrentCamera' then + local newCamera = workspace.CurrentCamera + if newCamera then + Camera = newCamera + + if CameraSubjectChangeConn then + CameraSubjectChangeConn:disconnect() + end + + CameraSubjectChangeConn = Camera:GetPropertyChangedSignal("CameraSubject"):connect(OnCameraSubjectChanged) + OnCameraSubjectChanged() + end + end +end + +------------------------- +--| Exposed Functions |-- +------------------------- + +function PopperCam:Update(EnabledCamera) + if PopperEnabled then + -- First, prep some intermediate vars + local cameraCFrame = Camera.CFrame + local focusPoint = Camera.Focus.p + + if FFlagUserPortraitPopperFix and SubjectPart then + focusPoint = SubjectPart.CFrame.p + end + + local ignoreList = {} + for _, character in pairs(PlayerCharacters) do + ignoreList[#ignoreList + 1] = character + end + for i = 1, #VehicleParts do + ignoreList[#ignoreList + 1] = VehicleParts[i] + end + + -- Get largest cutoff distance + local largest = Camera:GetLargestCutoffDistance(ignoreList) + + -- Then check if the player zoomed since the last frame, + -- and if so, reset our pop history so we stop tweening + local zoomLevel = (cameraCFrame.p - focusPoint).Magnitude + if math_abs(zoomLevel - LastZoomLevel) > 0.001 then + LastPopAmount = 0 + end + + -- Finally, zoom the camera in (pop) by that most-cut-off amount, or the last pop amount if that's more + local popAmount = largest + if LastPopAmount > popAmount then + popAmount = LastPopAmount + end + + if popAmount > 0 then + Camera.CFrame = cameraCFrame + (cameraCFrame.lookVector * popAmount) + LastPopAmount = popAmount - POP_RESTORE_RATE -- Shrink it for the next frame + if LastPopAmount < 0 then + LastPopAmount = 0 + end + end + + LastZoomLevel = zoomLevel + + -- Stop shift lock being able to see through walls by manipulating Camera focus inside the wall + if EnabledCamera and EnabledCamera:GetShiftLock() and not EnabledCamera:IsInFirstPerson() then + if EnabledCamera:GetCameraActualZoom() < 1 then + local subjectPosition = EnabledCamera.lastSubjectPosition + if subjectPosition then + Camera.Focus = CFrame_new(subjectPosition) + Camera.CFrame = CFrame_new(subjectPosition - MIN_CAMERA_ZOOM*EnabledCamera:GetCameraLook(), subjectPosition) + end + end + end + end +end + +-------------------- +--| Script Logic |-- +-------------------- + +-- Connect to the current and all future cameras +workspace.Changed:connect(OnWorkspaceChanged) +OnWorkspaceChanged('CurrentCamera') + +-- Connect to all Players so we can ignore their Characters +PlayersService.ChildRemoved:connect(OnPlayersChildRemoved) +PlayersService.ChildAdded:connect(OnPlayersChildAdded) +for _, player in pairs(PlayersService:GetPlayers()) do + OnPlayersChildAdded(player) +end + +return PopperCam diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera.lua new file mode 100644 index 0000000..124480d --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera.lua @@ -0,0 +1,1535 @@ + +local PlayersService = game:GetService('Players') +local UserInputService = game:GetService('UserInputService') +local StarterGui = game:GetService('StarterGui') +local GuiService = game:GetService('GuiService') +local ContextActionService = game:GetService('ContextActionService') +local VRService = game:GetService("VRService") + +local LocalPlayer = PlayersService.LocalPlayer +local PlayerGui = nil +if LocalPlayer then + PlayerGui = PlayersService.LocalPlayer:WaitForChild("PlayerGui") +end + +local PortraitMode = false + +local CameraScript = script.Parent +local ShiftLockController = require(CameraScript:WaitForChild('ShiftLockController')) + +local Settings = UserSettings() +local GameSettings = Settings.GameSettings + +local function clamp(low, high, num) + return (num > high and high or num < low and low or num) +end + +local math_atan2 = math.atan2 +local function findAngleBetweenXZVectors(vec2, vec1) + return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z) +end + +local function IsFinite(num) + return num == num and num ~= 1/0 and num ~= -1/0 +end + +local THUMBSTICK_DEADZONE = 0.2 + +local LANDSCAPE_DEFAULT_ZOOM = 12.5 +local PORTRAIT_DEFAULT_ZOOM = 25 + +local humanoidCache = {} +local function findPlayerHumanoid(player) + local character = player and player.Character + if character then + local resultHumanoid = humanoidCache[player] + if resultHumanoid and resultHumanoid.Parent == character then + return resultHumanoid + else + humanoidCache[player] = nil -- Bust Old Cache + local humanoid = character:FindFirstChildOfClass("Humanoid") + if humanoid then + humanoidCache[player] = humanoid + end + return humanoid + end + end +end + +local MIN_Y = math.rad(-80) +local MAX_Y = math.rad(80) + +local DEFAULT_CAMERA_ANGLE = 25 +local VR_ANGLE = math.rad(15) + +local VR_LOW_INTENSITY_ROTATION = Vector2.new(math.rad(15), 0) +local VR_HIGH_INTENSITY_ROTATION = Vector2.new(math.rad(45), 0) +local VR_LOW_INTENSITY_REPEAT = 0.1 +local VR_HIGH_INTENSITY_REPEAT = 0.4 + +local ZERO_VECTOR2 = Vector2.new(0, 0) +local ZERO_VECTOR3 = Vector3.new(0, 0, 0) + +local TOUCH_SENSITIVTY = Vector2.new(math.pi*2.25, math.pi*2) +local MOUSE_SENSITIVITY = Vector2.new(math.pi*4, math.pi*1.9) + +local MAX_TIME_FOR_DOUBLE_TAP = 1.5 +local MAX_TAP_POS_DELTA = 15 +local MAX_TAP_TIME_DELTA = 0.75 + +local SEAT_OFFSET = Vector3.new(0,5,0) +local VR_SEAT_OFFSET = Vector3.new(0, 4, 0) +local HEAD_OFFSET = Vector3.new(0, 1.5, 0) +local R15_HEAD_OFFSET = Vector3.new(0, 2.0, 0) + +local PORTRAIT_MODE_CAMERA_OFFSET = 2 + +-- Reset the camera look vector when the camera is enabled for the first time +local SetCameraOnSpawn = true + +local hasGameLoaded = false + +local GestureArea = nil +local GestureAreaManagedByControlScript = false + +local function layoutGestureArea(portraitMode) + if GestureArea and not GestureAreaManagedByControlScript then + if portraitMode then + GestureArea.Size = UDim2.new(1, 0, .6, 0) + GestureArea.Position = UDim2.new(0, 0, 0, 0) + else + GestureArea.Size = UDim2.new(1, 0, .5, -18) + GestureArea.Position = UDim2.new(0, 0, 0, 0) + end + end +end + +-- Setup gesture area that camera uses while DynamicThumbstick is enabled +local function OnCharacterAdded(character) + if UserInputService.TouchEnabled then + for _, child in ipairs(LocalPlayer.Character:GetChildren()) do + if child:IsA("Tool") then + IsAToolEquipped = true + end + end + character.ChildAdded:Connect(function(child) + if child:IsA("Tool") then + IsAToolEquipped = true + end + end) + character.ChildRemoved:Connect(function(child) + if child:IsA("Tool") then + IsAToolEquipped = false + end + end) + + if PlayerGui then + local TouchGui = PlayerGui:FindFirstChild("TouchGui") + if TouchGui and TouchGui:WaitForChild("GestureArea", 0.5) then + GestureArea = TouchGui.GestureArea + GestureAreaManagedByControlScript = true + else + GestureAreaManagedByControlScript = false + local ScreenGui = Instance.new("ScreenGui") + ScreenGui.Name = "GestureArea" + ScreenGui.Parent = PlayerGui + + GestureArea = Instance.new("Frame") + GestureArea.BackgroundTransparency = 1.0 + GestureArea.Visible = true + GestureArea.BackgroundColor3 = Color3.fromRGB(0, 0, 0) + layoutGestureArea(PortraitMode) + GestureArea.Parent = ScreenGui + end + end + end +end + +if LocalPlayer then + if LocalPlayer.Character ~= nil then + OnCharacterAdded(LocalPlayer.Character) + end + LocalPlayer.CharacterAdded:connect(function(character) + OnCharacterAdded(character) + end) +end + + +local function positionIntersectsGuiObject(position, guiObject) + if position.X < guiObject.AbsolutePosition.X + guiObject.AbsoluteSize.X + and position.X > guiObject.AbsolutePosition.X + and position.Y < guiObject.AbsolutePosition.Y + guiObject.AbsoluteSize.Y + and position.Y > guiObject.AbsolutePosition.Y then + return true + end + return false +end + +local function GetRenderCFrame(part) + return part:GetRenderCFrame() +end + +local function CreateCamera() + local this = {} + local R15HeadHeight = R15_HEAD_OFFSET + function this:GetActivateValue() + return 0.7 + end + + function this:IsPortraitMode() + return PortraitMode + end + + function this:GetRotateAmountValue(vrRotationIntensity) + vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity") + if vrRotationIntensity then + if vrRotationIntensity == "Low" then + return VR_LOW_INTENSITY_ROTATION + elseif vrRotationIntensity == "High" then + return VR_HIGH_INTENSITY_ROTATION + end + end + return ZERO_VECTOR2 + end + function this:GetRepeatDelayValue(vrRotationIntensity) + vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity") + if vrRotationIntensity then + if vrRotationIntensity == "Low" then + return VR_LOW_INTENSITY_REPEAT + elseif vrRotationIntensity == "High" then + return VR_HIGH_INTENSITY_REPEAT + end + end + return 0 + end + + this.ShiftLock = false + this.Enabled = false + local isFirstPerson = false + local isRightMouseDown = false + local isMiddleMouseDown = false + this.RotateInput = ZERO_VECTOR2 + this.DefaultZoom = LANDSCAPE_DEFAULT_ZOOM + this.activeGamepad = nil + + local tweens = {} + + this.lastSubject = nil + this.lastSubjectPosition = Vector3.new(0, 5, 0) + + local lastVRRotation = 0 + local vrRotateKeyCooldown = {} + + local isDynamicThumbstickEnabled = false + local dynamicThumbstickFrame = nil + + local function getDynamicThumbstickFrame() + if dynamicThumbstickFrame and dynamicThumbstickFrame:IsDescendantOf(game) then + return dynamicThumbstickFrame + else + local touchGui = PlayerGui:FindFirstChild("TouchGui") + if not touchGui then return nil end + + local touchControlFrame = touchGui:FindFirstChild("TouchControlFrame") + if not touchControlFrame then return nil end + + dynamicThumbstickFrame = touchControlFrame:FindFirstChild("DynamicThumbstickFrame") + return dynamicThumbstickFrame + end + end + + -- Check for changes in ViewportSize to decide if PortraitMode + local CameraChangedConn = nil + local workspaceCameraChangedConn = nil + local function onWorkspaceCameraChanged() + if UserInputService.TouchEnabled then + if CameraChangedConn then + CameraChangedConn:Disconnect() + CameraChangedConn = nil + end + local newCamera = workspace.CurrentCamera + if newCamera then + local size = newCamera.ViewportSize + PortraitMode = size.X < size.Y + layoutGestureArea(PortraitMode) + DefaultZoom = PortraitMode and PORTRAIT_DEFAULT_ZOOM or LANDSCAPE_DEFAULT_ZOOM + CameraChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(function() + size = newCamera.ViewportSize + PortraitMode = size.X < size.Y + layoutGestureArea(PortraitMode) + DefaultZoom = PortraitMode and PORTRAIT_DEFAULT_ZOOM or LANDSCAPE_DEFAULT_ZOOM + end) + end + end + end + workspaceCameraChangedConn = workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(onWorkspaceCameraChanged) + if workspace.CurrentCamera then + onWorkspaceCameraChanged() + end + + + function this:GetShiftLock() + return ShiftLockController:IsShiftLocked() + end + + function this:GetHumanoid() + local player = PlayersService.LocalPlayer + return findPlayerHumanoid(player) + end + + function this:GetHumanoidRootPart() + local humanoid = this:GetHumanoid() + return humanoid and humanoid.Torso + end + + function this:GetRenderCFrame(part) + GetRenderCFrame(part) + end + + local STATE_DEAD = Enum.HumanoidStateType.Dead + + -- HumanoidRootPart when alive, Head part when dead + local function getHumanoidPartToFollow(humanoid, humanoidStateType) + if humanoidStateType == STATE_DEAD then + local character = humanoid.Parent + if character then + return character:FindFirstChild("Head") or humanoid.Torso + else + return humanoid.Torso + end + else + return humanoid.Torso + end + end + + local HUMANOID_STATE_DEAD = Enum.HumanoidStateType.Dead + + function this:GetSubjectPosition() + local result = nil + local camera = workspace.CurrentCamera + local cameraSubject = camera and camera.CameraSubject + if cameraSubject then + if cameraSubject:IsA('Humanoid') then + local humanoidStateType = cameraSubject:GetState() + if VRService.VREnabled and humanoidStateType == STATE_DEAD and cameraSubject == this.lastSubject then + result = this.lastSubjectPosition + else + local humanoidRootPart = getHumanoidPartToFollow(cameraSubject, humanoidStateType) + if humanoidRootPart and humanoidRootPart:IsA('BasePart') then + local subjectCFrame = GetRenderCFrame(humanoidRootPart) + local heightOffset = ZERO_VECTOR3 + if humanoidStateType ~= STATE_DEAD then + heightOffset = cameraSubject.RigType == Enum.HumanoidRigType.R15 and R15HeadHeight or HEAD_OFFSET + end + + if PortraitMode then + heightOffset = heightOffset + Vector3.new(0, PORTRAIT_MODE_CAMERA_OFFSET, 0) + end + + result = subjectCFrame.p + + subjectCFrame:vectorToWorldSpace(heightOffset + cameraSubject.CameraOffset) + end + end + elseif cameraSubject:IsA('VehicleSeat') then + local subjectCFrame = GetRenderCFrame(cameraSubject) + local offset = SEAT_OFFSET + if VRService.VREnabled then + offset = VR_SEAT_OFFSET + end + result = subjectCFrame.p + subjectCFrame:vectorToWorldSpace(offset) + elseif cameraSubject:IsA('SkateboardPlatform') then + local subjectCFrame = GetRenderCFrame(cameraSubject) + result = subjectCFrame.p + SEAT_OFFSET + elseif cameraSubject:IsA('BasePart') then + local subjectCFrame = GetRenderCFrame(cameraSubject) + result = subjectCFrame.p + elseif cameraSubject:IsA('Model') then + result = cameraSubject:GetModelCFrame().p + end + end + this.lastSubject = cameraSubject + this.lastSubjectPosition = result + return result + end + + function this:ResetCameraLook() + end + + function this:GetCameraLook() + return workspace.CurrentCamera and workspace.CurrentCamera.CoordinateFrame.lookVector or Vector3.new(0,0,1) + end + + function this:GetCameraZoom() + if this.currentZoom == nil then + local player = PlayersService.LocalPlayer + this.currentZoom = player and clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, this.DefaultZoom) or this.DefaultZoom + end + return this.currentZoom + end + + function this:GetCameraActualZoom() + local camera = workspace.CurrentCamera + if camera then + return (camera.CoordinateFrame.p - camera.Focus.p).magnitude + end + end + + function this:GetCameraHeight() + if VRService.VREnabled and not this:IsInFirstPerson() then + local zoom = this:GetCameraZoom() + return math.sin(VR_ANGLE) * zoom + end + return 0 + end + + function this:ViewSizeX() + local result = 1024 + local camera = workspace.CurrentCamera + if camera then + result = camera.ViewportSize.X + end + return result + end + + function this:ViewSizeY() + local result = 768 + local camera = workspace.CurrentCamera + if camera then + result = camera.ViewportSize.Y + end + return result + end + + + local math_asin = math.asin + local math_atan2 = math.atan2 + local math_floor = math.floor + local math_max = math.max + local math_pi = math.pi + local Vector2_new = Vector2.new + local Vector3_new = Vector3.new + local CFrame_Angles = CFrame.Angles + local CFrame_new = CFrame.new + + function this:ScreenTranslationToAngle(translationVector) + local screenX = this:ViewSizeX() + local screenY = this:ViewSizeY() + local xTheta = (translationVector.x / screenX) + local yTheta = (translationVector.y / screenY) + return Vector2_new(xTheta, yTheta) + end + + function this:MouseTranslationToAngle(translationVector) + local xTheta = (translationVector.x / 1920) + local yTheta = (translationVector.y / 1200) + return Vector2_new(xTheta, yTheta) + end + + function this:RotateVector(startVector, xyRotateVector) + local startCFrame = CFrame_new(ZERO_VECTOR3, startVector) + local resultLookVector = (CFrame_Angles(0, -xyRotateVector.x, 0) * startCFrame * CFrame_Angles(-xyRotateVector.y,0,0)).lookVector + return resultLookVector, Vector2_new(xyRotateVector.x, xyRotateVector.y) + end + + function this:RotateCamera(startLook, xyRotateVector) + if VRService.VREnabled then + local yawRotatedVector, xyRotateVector = self:RotateVector(startLook, Vector2.new(xyRotateVector.x, 0)) + return Vector3_new(yawRotatedVector.x, 0, yawRotatedVector.z).unit, xyRotateVector + else + local startVertical = math_asin(startLook.y) + local yTheta = clamp(-MAX_Y + startVertical, -MIN_Y + startVertical, xyRotateVector.y) + return self:RotateVector(startLook, Vector2_new(xyRotateVector.x, yTheta)) + end + end + + function this:IsInFirstPerson() + return isFirstPerson + end + + -- there are several cases to consider based on the state of input and camera rotation mode + function this:UpdateMouseBehavior() + -- first time transition to first person mode or shiftlock + local camera = workspace.CurrentCamera + if camera.CameraType == Enum.CameraType.Scriptable then + return + end + + if isFirstPerson or self:GetShiftLock() then + pcall(function() GameSettings.RotationType = Enum.RotationType.CameraRelative end) + if UserInputService.MouseBehavior ~= Enum.MouseBehavior.LockCenter then + UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter + end + else + pcall(function() GameSettings.RotationType = Enum.RotationType.MovementRelative end) + if isRightMouseDown or isMiddleMouseDown then + UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition + else + UserInputService.MouseBehavior = Enum.MouseBehavior.Default + end + end + end + + function this:ZoomCamera(desiredZoom) + local player = PlayersService.LocalPlayer + if player then + if player.CameraMode == Enum.CameraMode.LockFirstPerson then + this.currentZoom = 0 + else + this.currentZoom = clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, desiredZoom) + end + end + + isFirstPerson = self:GetCameraZoom() < 2 + + ShiftLockController:SetIsInFirstPerson(isFirstPerson) + -- set mouse behavior + self:UpdateMouseBehavior() + return self:GetCameraZoom() + end + + function this:rk4Integrator(position, velocity, t) + local direction = velocity < 0 and -1 or 1 + local function acceleration(p, v) + local accel = direction * math_max(1, (p / 3.3) + 0.5) + return accel + end + + local p1 = position + local v1 = velocity + local a1 = acceleration(p1, v1) + local p2 = p1 + v1 * (t / 2) + local v2 = v1 + a1 * (t / 2) + local a2 = acceleration(p2, v2) + local p3 = p1 + v2 * (t / 2) + local v3 = v1 + a2 * (t / 2) + local a3 = acceleration(p3, v3) + local p4 = p1 + v3 * t + local v4 = v1 + a3 * t + local a4 = acceleration(p4, v4) + + local positionResult = position + (v1 + 2 * v2 + 2 * v3 + v4) * (t / 6) + local velocityResult = velocity + (a1 + 2 * a2 + 2 * a3 + a4) * (t / 6) + return positionResult, velocityResult + end + + function this:ZoomCameraBy(zoomScale) + local zoom = this:GetCameraActualZoom() + + if zoom then + -- Can break into more steps to get more accurate integration + zoom = self:rk4Integrator(zoom, zoomScale, 1) + self:ZoomCamera(zoom) + end + return self:GetCameraZoom() + end + + function this:ZoomCameraFixedBy(zoomIncrement) + return self:ZoomCamera(self:GetCameraZoom() + zoomIncrement) + end + + function this:Update() + end + + ----- VR STUFF ------ + + function this:ApplyVRTransform() + if not VRService.VREnabled then + return + end + --we only want this to happen in first person VR + local player = PlayersService.LocalPlayer + if not (player and player.Character + and player.Character:FindFirstChild("HumanoidRootPart") + and player.Character.HumanoidRootPart:FindFirstChild("RootJoint")) then + return + end + + local camera = workspace.CurrentCamera + local cameraSubject = camera.CameraSubject + local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat') + + if this:IsInFirstPerson() and not isInVehicle then + local vrFrame = VRService:GetUserCFrame(Enum.UserCFrame.Head) + local vrRotation = vrFrame - vrFrame.p + local rootJoint = player.Character.HumanoidRootPart.RootJoint + rootJoint.C0 = CFrame.new(vrRotation:vectorToObjectSpace(vrFrame.p)) * CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0) + else + local rootJoint = player.Character.HumanoidRootPart.RootJoint + rootJoint.C0 = CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0) + end + end + + local vrRotationIntensityExists = true + local lastVrRotationCheck = 0 + function this:ShouldUseVRRotation() + if not VRService.VREnabled then + return false + end + if not vrRotationIntensityExists and tick() - lastVrRotationCheck < 1 then return false end + + local success, vrRotationIntensity = pcall(function() return StarterGui:GetCore("VRRotationIntensity") end) + vrRotationIntensityExists = success and vrRotationIntensity ~= nil + lastVrRotationCheck = tick() + + return success and vrRotationIntensity ~= nil and vrRotationIntensity ~= "Smooth" + end + + function this:GetVRRotationInput() + local vrRotateSum = ZERO_VECTOR2 + + local vrRotationIntensity = StarterGui:GetCore("VRRotationIntensity") + + local vrGamepadRotation = self.GamepadPanningCamera or ZERO_VECTOR2 + local delayExpired = (tick() - lastVRRotation) >= self:GetRepeatDelayValue(vrRotationIntensity) + + if math.abs(vrGamepadRotation.x) >= self:GetActivateValue() then + if (delayExpired or not vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2]) then + local sign = 1 + if vrGamepadRotation.x < 0 then + sign = -1 + end + vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity) * sign + vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = true + end + elseif math.abs(vrGamepadRotation.x) < self:GetActivateValue() - 0.1 then + vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = nil + end + if self.TurningLeft then + if delayExpired or not vrRotateKeyCooldown[Enum.KeyCode.Left] then + vrRotateSum = vrRotateSum - self:GetRotateAmountValue(vrRotationIntensity) + vrRotateKeyCooldown[Enum.KeyCode.Left] = true + end + else + vrRotateKeyCooldown[Enum.KeyCode.Left] = nil + end + if self.TurningRight then + if (delayExpired or not vrRotateKeyCooldown[Enum.KeyCode.Right]) then + vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity) + vrRotateKeyCooldown[Enum.KeyCode.Right] = true + end + else + vrRotateKeyCooldown[Enum.KeyCode.Right] = nil + end + + if vrRotateSum ~= ZERO_VECTOR2 then + lastVRRotation = tick() + end + + return vrRotateSum + end + + local cameraTranslationConstraints = Vector3.new(1, 1, 1) + local humanoidJumpOrigin = nil + local trackingHumanoid = nil + local cameraFrozen = false + local subjectStateChangedConn = nil + local cameraSubjectChangedConn = nil + local workspaceChangedConn = nil + local humanoidChildAddedConn = nil + local humanoidChildRemovedConn = nil + + local function cancelCameraFreeze(keepConstraints) + if not keepConstraints then + cameraTranslationConstraints = Vector3.new(cameraTranslationConstraints.x, 1, cameraTranslationConstraints.z) + end + if cameraFrozen then + trackingHumanoid = nil + cameraFrozen = false + end + end + + local function startCameraFreeze(subjectPosition, humanoidToTrack) + if not cameraFrozen then + humanoidJumpOrigin = subjectPosition + trackingHumanoid = humanoidToTrack + cameraTranslationConstraints = Vector3.new(cameraTranslationConstraints.x, 0, cameraTranslationConstraints.z) + cameraFrozen = true + end + end + + local function rescaleCameraOffset(newScaleFactor) + R15HeadHeight = R15_HEAD_OFFSET*newScaleFactor + end + + local function onHumanoidSubjectChildAdded(child) + if child.Name == "BodyHeightScale" and child:IsA("NumberValue") then + if heightScaleChangedConn then + heightScaleChangedConn:disconnect() + end + heightScaleChangedConn = child.Changed:connect(rescaleCameraOffset) + rescaleCameraOffset(child.Value) + end + end + + local function onHumanoidSubjectChildRemoved(child) + if child.Name == "BodyHeightScale" then + rescaleCameraOffset(1) + if heightScaleChangedConn then + heightScaleChangedConn:disconnect() + heightScaleChangedConn = nil + end + end + end + + local function onNewCameraSubject() + if subjectStateChangedConn then + subjectStateChangedConn:disconnect() + subjectStateChangedConn = nil + end + if humanoidChildAddedConn then + humanoidChildAddedConn:disconnect() + humanoidChildAddedConn = nil + end + if humanoidChildRemovedConn then + humanoidChildRemovedConn:disconnect() + humanoidChildRemovedConn = nil + end + if heightScaleChangedConn then + heightScaleChangedConn:disconnect() + heightScaleChangedConn = nil + end + + local humanoid = workspace.CurrentCamera and workspace.CurrentCamera.CameraSubject + if trackingHumanoid ~= humanoid then + cancelCameraFreeze() + end + if humanoid and humanoid:IsA('Humanoid') then + humanoidChildAddedConn = humanoid.ChildAdded:connect(onHumanoidSubjectChildAdded) + humanoidChildRemovedConn = humanoid.ChildRemoved:connect(onHumanoidSubjectChildRemoved) + for _, child in pairs(humanoid:GetChildren()) do + onHumanoidSubjectChildAdded(child) + end + + subjectStateChangedConn = humanoid.StateChanged:connect(function(oldState, newState) + if VRService.VREnabled and newState == Enum.HumanoidStateType.Jumping and not this:IsInFirstPerson() then + startCameraFreeze(this:GetSubjectPosition(), humanoid) + elseif newState ~= Enum.HumanoidStateType.Jumping and newState ~= Enum.HumanoidStateType.Freefall then + cancelCameraFreeze(true) + end + end) + end + end + + local function onCurrentCameraChanged() + if cameraSubjectChangedConn then + cameraSubjectChangedConn:disconnect() + cameraSubjectChangedConn = nil + end + local camera = workspace.CurrentCamera + if camera then + cameraSubjectChangedConn = camera:GetPropertyChangedSignal("CameraSubject"):connect(onNewCameraSubject) + onNewCameraSubject() + end + end + + function this:GetVRFocus(subjectPosition, timeDelta) + local newFocus = nil + + local camera = workspace.CurrentCamera + local lastFocus = self.LastCameraFocus or subjectPosition + if not cameraFrozen then + cameraTranslationConstraints = Vector3.new(cameraTranslationConstraints.x, math.min(1, cameraTranslationConstraints.y + 0.42 * timeDelta), cameraTranslationConstraints.z) + end + if cameraFrozen and humanoidJumpOrigin and humanoidJumpOrigin.y > lastFocus.y then + newFocus = CFrame.new(Vector3.new(subjectPosition.x, math.min(humanoidJumpOrigin.y, lastFocus.y + 5 * timeDelta), subjectPosition.z)) + else + newFocus = CFrame.new(Vector3.new(subjectPosition.x, lastFocus.y, subjectPosition.z):lerp(subjectPosition, cameraTranslationConstraints.y)) + end + + if cameraFrozen then + -- No longer in 3rd person + if self:IsInFirstPerson() then -- not VRService.VREnabled + cancelCameraFreeze() + end + -- This case you jumped off a cliff and want to keep your character in view + -- 0.5 is to fix floating point error when not jumping off cliffs + if humanoidJumpOrigin and subjectPosition.y < (humanoidJumpOrigin.y - 0.5) then + cancelCameraFreeze() + end + end + + return newFocus + end + + ------------------------ + + ---- Input Events ---- + local startPos = nil + local lastPos = nil + local panBeginLook = nil + local lastTapTime = nil + + local fingerTouches = {} + local NumUnsunkTouches = 0 + + local inputStartPositions = {} + local inputStartTimes = {} + + local StartingDiff = nil + local pinchBeginZoom = nil + + this.ZoomEnabled = true + this.PanEnabled = true + this.KeyPanEnabled = true + + local function OnTouchBegan(input, processed) + --If isDynamicThumbstickEnabled, then only process TouchBegan event if it starts in GestureArea + + local dtFrame = getDynamicThumbstickFrame() + + local isDynamicThumbstickUsingThisInput = false + if isDynamicThumbstickEnabled then + local ControlScript = CameraScript.Parent:FindFirstChild("ControlScript") + if ControlScript then + local MasterControl = ControlScript:FindFirstChild("MasterControl") + if MasterControl then + local DynamicThumbstickModule = MasterControl:FindFirstChild("DynamicThumbstick") + if DynamicThumbstickModule then + DynamicThumbstickModule = require(DynamicThumbstickModule) + local dynamicInputObject = DynamicThumbstickModule:GetInputObject() + isDynamicThumbstickUsingThisInput = (dynamicInputObject == input) + end + end + end + end + + if not isDynamicThumbstickUsingThisInput then + fingerTouches[input] = processed + if not processed then + inputStartPositions[input] = input.Position + inputStartTimes[input] = tick() + NumUnsunkTouches = NumUnsunkTouches + 1 + end + end + end + + local function OnTouchChanged(input, processed) + + if fingerTouches[input] == nil then + if isDynamicThumbstickEnabled then + return + end + fingerTouches[input] = processed + if not processed then + NumUnsunkTouches = NumUnsunkTouches + 1 + end + end + + if NumUnsunkTouches == 1 then + if fingerTouches[input] == false then + panBeginLook = panBeginLook or this:GetCameraLook() + startPos = startPos or input.Position + lastPos = lastPos or startPos + this.UserPanningTheCamera = true + + local delta = input.Position - lastPos + + delta = Vector2.new(delta.X, delta.Y * GameSettings:GetCameraYInvertValue()) + + if this.PanEnabled then + local desiredXYVector = this:ScreenTranslationToAngle(delta) * TOUCH_SENSITIVTY + this.RotateInput = this.RotateInput + desiredXYVector + end + + lastPos = input.Position + end + else + panBeginLook = nil + startPos = nil + lastPos = nil + this.UserPanningTheCamera = false + end + if NumUnsunkTouches == 2 then + local unsunkTouches = {} + for touch, wasSunk in pairs(fingerTouches) do + if not wasSunk then + table.insert(unsunkTouches, touch) + end + end + if #unsunkTouches == 2 then + local difference = (unsunkTouches[1].Position - unsunkTouches[2].Position).magnitude + if StartingDiff and pinchBeginZoom then + local scale = difference / math_max(0.01, StartingDiff) + local clampedScale = clamp(0.1, 10, scale) + if this.ZoomEnabled then + this:ZoomCamera(pinchBeginZoom / clampedScale) + end + else + StartingDiff = difference + pinchBeginZoom = this:GetCameraActualZoom() + end + end + else + StartingDiff = nil + pinchBeginZoom = nil + end + end + + local function calcLookBehindRotateInput(torso) + if torso then + local newDesiredLook = (torso.CFrame.lookVector - Vector3.new(0, math.sin(math.rad(DEFAULT_CAMERA_ANGLE), 0))).unit + local horizontalShift = findAngleBetweenXZVectors(newDesiredLook, this:GetCameraLook()) + local vertShift = math.asin(this:GetCameraLook().y) - math.asin(newDesiredLook.y) + if not IsFinite(horizontalShift) then + horizontalShift = 0 + end + if not IsFinite(vertShift) then + vertShift = 0 + end + + return Vector2.new(horizontalShift, vertShift) + end + return nil + end + + local function IsTouchTap(input) + -- We can't make the assumption that the input exists in the inputStartPositions because we may have switched from a different camera type. + if inputStartPositions[input] then + local posDelta = (inputStartPositions[input] - input.Position).magnitude + if posDelta < MAX_TAP_POS_DELTA then + local timeDelta = inputStartTimes[input] - tick() + if timeDelta < MAX_TAP_TIME_DELTA then + return true + end + end + end + return false + end + + + local function OnTouchEnded(input, processed) + if fingerTouches[input] == false then + if NumUnsunkTouches == 1 then + panBeginLook = nil + startPos = nil + lastPos = nil + this.UserPanningTheCamera = false + elseif NumUnsunkTouches == 2 then + StartingDiff = nil + pinchBeginZoom = nil + end + end + + if fingerTouches[input] ~= nil and fingerTouches[input] == false then + NumUnsunkTouches = NumUnsunkTouches - 1 + end + fingerTouches[input] = nil + inputStartPositions[input] = nil + inputStartTimes[input] = nil + end + + local function OnMousePanButtonPressed(input, processed) + if processed then return end + this:UpdateMouseBehavior() + panBeginLook = panBeginLook or this:GetCameraLook() + startPos = startPos or input.Position + lastPos = lastPos or startPos + this.UserPanningTheCamera = true + end + + local function OnMousePanButtonReleased(input, processed) + this:UpdateMouseBehavior() + if not (isRightMouseDown or isMiddleMouseDown) then + panBeginLook = nil + startPos = nil + lastPos = nil + this.UserPanningTheCamera = false + end + end + + local function OnMouse2Down(input, processed) + if processed then return end + + isRightMouseDown = true + OnMousePanButtonPressed(input, processed) + end + + local function OnMouse2Up(input, processed) + isRightMouseDown = false + OnMousePanButtonReleased(input, processed) + end + + local function OnMouse3Down(input, processed) + if processed then return end + + isMiddleMouseDown = true + OnMousePanButtonPressed(input, processed) + end + + local function OnMouse3Up(input, processed) + isMiddleMouseDown = false + OnMousePanButtonReleased(input, processed) + end + + local function OnMouseMoved(input, processed) + if not hasGameLoaded and VRService.VREnabled then + return + end + + local inputDelta = input.Delta + inputDelta = Vector2.new(inputDelta.X, inputDelta.Y * GameSettings:GetCameraYInvertValue()) + + if startPos and lastPos and panBeginLook then + local currPos = lastPos + input.Delta + local totalTrans = currPos - startPos + if this.PanEnabled then + local desiredXYVector = this:MouseTranslationToAngle(inputDelta) * MOUSE_SENSITIVITY + this.RotateInput = this.RotateInput + desiredXYVector + end + lastPos = currPos + elseif this:IsInFirstPerson() or this:GetShiftLock() then + if this.PanEnabled then + local desiredXYVector = this:MouseTranslationToAngle(inputDelta) * MOUSE_SENSITIVITY + this.RotateInput = this.RotateInput + desiredXYVector + end + end + end + + local function OnMouseWheel(input, processed) + if not hasGameLoaded and VRService.VREnabled then + return + end + if not processed then + if this.ZoomEnabled then + this:ZoomCameraBy(clamp(-1, 1, -input.Position.Z) * 1.4) + end + end + end + + local function round(num) + return math_floor(num + 0.5) + end + + local eight2Pi = math_pi / 4 + + local function rotateVectorByAngleAndRound(camLook, rotateAngle, roundAmount) + if camLook ~= ZERO_VECTOR3 then + camLook = camLook.unit + local currAngle = math_atan2(camLook.z, camLook.x) + local newAngle = round((math_atan2(camLook.z, camLook.x) + rotateAngle) / roundAmount) * roundAmount + return newAngle - currAngle + end + return 0 + end + + local function OnKeyDown(input, processed) + if not hasGameLoaded and VRService.VREnabled then + return + end + if processed then return end + if this.ZoomEnabled then + if input.KeyCode == Enum.KeyCode.I then + this:ZoomCameraBy(-5) + elseif input.KeyCode == Enum.KeyCode.O then + this:ZoomCameraBy(5) + end + end + if panBeginLook == nil and this.KeyPanEnabled then + if input.KeyCode == Enum.KeyCode.Left then + this.TurningLeft = true + elseif input.KeyCode == Enum.KeyCode.Right then + this.TurningRight = true + elseif input.KeyCode == Enum.KeyCode.Comma then + local angle = rotateVectorByAngleAndRound(this:GetCameraLook() * Vector3.new(1,0,1), -eight2Pi * (3/4), eight2Pi) + if angle ~= 0 then + this.RotateInput = this.RotateInput + Vector2.new(angle, 0) + this.LastUserPanCamera = tick() + this.LastCameraTransform = nil + end + elseif input.KeyCode == Enum.KeyCode.Period then + local angle = rotateVectorByAngleAndRound(this:GetCameraLook() * Vector3.new(1,0,1), eight2Pi * (3/4), eight2Pi) + if angle ~= 0 then + this.RotateInput = this.RotateInput + Vector2.new(angle, 0) + this.LastUserPanCamera = tick() + this.LastCameraTransform = nil + end + elseif input.KeyCode == Enum.KeyCode.PageUp then + --elseif input.KeyCode == Enum.KeyCode.Home then + this.RotateInput = this.RotateInput + Vector2.new(0,math.rad(15)) + this.LastCameraTransform = nil + elseif input.KeyCode == Enum.KeyCode.PageDown then + --elseif input.KeyCode == Enum.KeyCode.End then + this.RotateInput = this.RotateInput + Vector2.new(0,math.rad(-15)) + this.LastCameraTransform = nil + end + end + end + + local function OnKeyUp(input, processed) + if input.KeyCode == Enum.KeyCode.Left then + this.TurningLeft = false + elseif input.KeyCode == Enum.KeyCode.Right then + this.TurningRight = false + end + end + + local function onWindowFocusReleased() + this:ResetInputStates() + end + + local lastThumbstickRotate = nil + local numOfSeconds = 0.7 + local currentSpeed = 0 + local maxSpeed = 6 + local vrMaxSpeed = 4 + local lastThumbstickPos = Vector2.new(0,0) + local ySensitivity = 0.65 + local lastVelocity = nil + + -- K is a tunable parameter that changes the shape of the S-curve + -- the larger K is the more straight/linear the curve gets + local k = 0.35 + local lowerK = 0.8 + local function SCurveTranform(t) + t = clamp(-1,1,t) + if t >= 0 then + return (k*t) / (k - t + 1) + end + return -((lowerK*-t) / (lowerK + t + 1)) + end + + -- DEADZONE + local DEADZONE = 0.1 + local function toSCurveSpace(t) + return (1 + DEADZONE) * (2*math.abs(t) - 1) - DEADZONE + end + + local function fromSCurveSpace(t) + return t/2 + 0.5 + end + + local function gamepadLinearToCurve(thumbstickPosition) + local function onAxis(axisValue) + local sign = 1 + if axisValue < 0 then + sign = -1 + end + local point = fromSCurveSpace(SCurveTranform(toSCurveSpace(math.abs(axisValue)))) + point = point * sign + return clamp(-1, 1, point) + end + return Vector2_new(onAxis(thumbstickPosition.x), onAxis(thumbstickPosition.y)) + end + + function this:UpdateGamepad() + local gamepadPan = this.GamepadPanningCamera + if gamepadPan and (hasGameLoaded or not VRService.VREnabled) then + gamepadPan = gamepadLinearToCurve(gamepadPan) + local currentTime = tick() + if gamepadPan.X ~= 0 or gamepadPan.Y ~= 0 then + this.userPanningTheCamera = true + elseif gamepadPan == ZERO_VECTOR2 then + lastThumbstickRotate = nil + if lastThumbstickPos == ZERO_VECTOR2 then + currentSpeed = 0 + end + end + + local finalConstant = 0 + + if lastThumbstickRotate then + if VRService.VREnabled then + currentSpeed = vrMaxSpeed + else + local elapsedTime = (currentTime - lastThumbstickRotate) * 10 + currentSpeed = currentSpeed + (maxSpeed * ((elapsedTime*elapsedTime)/numOfSeconds)) + + if currentSpeed > maxSpeed then currentSpeed = maxSpeed end + + if lastVelocity then + local velocity = (gamepadPan - lastThumbstickPos)/(currentTime - lastThumbstickRotate) + local velocityDeltaMag = (velocity - lastVelocity).magnitude + + if velocityDeltaMag > 12 then + currentSpeed = currentSpeed * (20/velocityDeltaMag) + if currentSpeed > maxSpeed then currentSpeed = maxSpeed end + end + end + end + + local success, gamepadCameraSensitivity = pcall(function() return GameSettings.GamepadCameraSensitivity end) + finalConstant = success and (gamepadCameraSensitivity * currentSpeed) or currentSpeed + lastVelocity = (gamepadPan - lastThumbstickPos)/(currentTime - lastThumbstickRotate) + end + + lastThumbstickPos = gamepadPan + lastThumbstickRotate = currentTime + + return Vector2_new( gamepadPan.X * finalConstant, gamepadPan.Y * finalConstant * ySensitivity * GameSettings:GetCameraYInvertValue()) + end + + return ZERO_VECTOR2 + end + + local InputBeganConn, InputChangedConn, InputEndedConn, WindowUnfocusConn, MenuOpenedConn, ShiftLockToggleConn, GamepadConnectedConn, GamepadDisconnectedConn, TouchActivateConn = nil, nil, nil, nil, nil, nil, nil, nil, nil + + function this:DisconnectInputEvents() + if InputBeganConn then + InputBeganConn:disconnect() + InputBeganConn = nil + end + if InputChangedConn then + InputChangedConn:disconnect() + InputChangedConn = nil + end + if InputEndedConn then + InputEndedConn:disconnect() + InputEndedConn = nil + end + if WindowUnfocusConn then + WindowUnfocusConn:disconnect() + WindowUnfocusConn = nil + end + if MenuOpenedConn then + MenuOpenedConn:disconnect() + MenuOpenedConn = nil + end + if ShiftLockToggleConn then + ShiftLockToggleConn:disconnect() + ShiftLockToggleConn = nil + end + if GamepadConnectedConn then + GamepadConnectedConn:disconnect() + GamepadConnectedConn = nil + end + if GamepadDisconnectedConn then + GamepadDisconnectedConn:disconnect() + GamepadDisconnectedConn = nil + end + if subjectStateChangedConn then + subjectStateChangedConn:disconnect() + subjectStateChangedConn = nil + end + if workspaceChangedConn then + workspaceChangedConn:disconnect() + workspaceChangedConn = nil + end + if TouchActivateConn then + TouchActivateConn:disconnect() + TouchActivateConn = nil + end + + this.TurningLeft = false + this.TurningRight = false + this.LastCameraTransform = nil + self.LastSubjectCFrame = nil + this.UserPanningTheCamera = false + this.RotateInput = Vector2.new() + this.GamepadPanningCamera = Vector2.new(0,0) + + -- Reset input states + startPos = nil + lastPos = nil + panBeginLook = nil + isRightMouseDown = false + isMiddleMouseDown = false + + fingerTouches = {} + NumUnsunkTouches = 0 + + StartingDiff = nil + pinchBeginZoom = nil + + -- Unlock mouse for example if right mouse button was being held down + if UserInputService.MouseBehavior ~= Enum.MouseBehavior.LockCenter then + UserInputService.MouseBehavior = Enum.MouseBehavior.Default + end + end + + function this:ResetInputStates() + isRightMouseDown = false + isMiddleMouseDown = false + this.TurningRight = false + this.TurningLeft = false + OnMousePanButtonReleased() -- this function doesn't seem to actually need parameters + + if UserInputService.TouchEnabled then + --[[menu opening was causing serious touch issues + this should disable all active touch events if + they're active when menu opens.]] + for inputObject, value in pairs(fingerTouches) do + fingerTouches[inputObject] = nil + end + panBeginLook = nil + startPos = nil + lastPos = nil + this.UserPanningTheCamera = false + StartingDiff = nil + pinchBeginZoom = nil + NumUnsunkTouches = 0 + end + end + + function this.getGamepadPan(name, state, input) + if state == Enum.UserInputState.Cancel then + this.GamepadPanningCamera = ZERO_VECTOR2 + return + end + + if input.UserInputType == this.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then + local inputVector = Vector2.new(input.Position.X, -input.Position.Y) + if inputVector.magnitude > THUMBSTICK_DEADZONE then + this.GamepadPanningCamera = Vector2_new(input.Position.X, -input.Position.Y) + else + this.GamepadPanningCamera = ZERO_VECTOR2 + end + end + end + + function this.doGamepadZoom(name, state, input) + if input.UserInputType == this.activeGamepad and input.KeyCode == Enum.KeyCode.ButtonR3 and state == Enum.UserInputState.Begin then + if this.ZoomEnabled then + if this:GetCameraZoom() > 0.5 then + this:ZoomCamera(0) + else + this:ZoomCamera(10) + end + end + end + end + + function this:BindGamepadInputActions() + ContextActionService:BindAction("RootCamGamepadPan", this.getGamepadPan, false, Enum.KeyCode.Thumbstick2) + ContextActionService:BindAction("RootCamGamepadZoom", this.doGamepadZoom, false, Enum.KeyCode.ButtonR3) + end + + function this:ConnectInputEvents() + InputBeganConn = UserInputService.InputBegan:connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchBegan(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseButton2 then + OnMouse2Down(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseButton3 then + OnMouse3Down(input, processed) + end + -- Keyboard + if input.UserInputType == Enum.UserInputType.Keyboard then + OnKeyDown(input, processed) + end + end) + + InputChangedConn = UserInputService.InputChanged:connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchChanged(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseMovement then + OnMouseMoved(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseWheel then + OnMouseWheel(input, processed) + end + end) + + InputEndedConn = UserInputService.InputEnded:connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchEnded(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseButton2 then + OnMouse2Up(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseButton3 then + OnMouse3Up(input, processed) + end + -- Keyboard + if input.UserInputType == Enum.UserInputType.Keyboard then + OnKeyUp(input, processed) + end + end) + + WindowUnfocusConn = UserInputService.WindowFocusReleased:connect(onWindowFocusReleased) + + MenuOpenedConn = GuiService.MenuOpened:connect(function() + this:ResetInputStates() + end) + + workspaceChangedConn = workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(onCurrentCameraChanged) + if workspace.CurrentCamera then + onCurrentCameraChanged() + end + + ShiftLockToggleConn = ShiftLockController.OnShiftLockToggled.Event:connect(function() + this:UpdateMouseBehavior() + end) + + this.RotateInput = Vector2.new() + + this.activeGamepad = nil + local function assignActivateGamepad() + local connectedGamepads = UserInputService:GetConnectedGamepads() + if #connectedGamepads > 0 then + for i = 1, #connectedGamepads do + if this.activeGamepad == nil then + this.activeGamepad = connectedGamepads[i] + elseif connectedGamepads[i].Value < this.activeGamepad.Value then + this.activeGamepad = connectedGamepads[i] + end + end + end + + if this.activeGamepad == nil then -- nothing is connected, at least set up for gamepad1 + this.activeGamepad = Enum.UserInputType.Gamepad1 + end + end + + GamepadConnectedConn = UserInputService.GamepadDisconnected:connect(function(gamepadEnum) + if this.activeGamepad ~= gamepadEnum then return end + this.activeGamepad = nil + assignActivateGamepad() + end) + + GamepadDisconnectedConn = UserInputService.GamepadConnected:connect(function(gamepadEnum) + if this.activeGamepad == nil then + assignActivateGamepad() + end + end) + + self:BindGamepadInputActions() + + assignActivateGamepad() + + -- set mouse behavior + self:UpdateMouseBehavior() + end + + --Process tweens related to tap-to-recenter and double-tap-to-zoom + --Needs to be called from specific cameras on each update + function this:ProcessTweens() + for name, tween in pairs(tweens) do + local alpha = math.min(1.0, (tick() - tween.start)/tween.duration) + tween.to = tween.func(tween.from, tween.to, alpha) + if math.abs(1 - alpha) < 0.0001 then + tweens[name] = nil + end + end + end + + function this:SetEnabled(newState) + if newState ~= self.Enabled then + self.Enabled = newState + if self.Enabled then + self:ConnectInputEvents() + self.cframe = workspace.CurrentCamera.CFrame + else + self:DisconnectInputEvents() + end + end + end + + local function OnPlayerAdded(player) + player.Changed:connect(function(prop) + if this.Enabled then + if prop == "CameraMode" or prop == "CameraMaxZoomDistance" or prop == "CameraMinZoomDistance" then + this:ZoomCameraFixedBy(0) + end + end + end) + + local function OnCharacterAdded(newCharacter) + + local humanoid = findPlayerHumanoid(player) + local start = tick() + while tick() - start < 0.3 and (humanoid == nil or humanoid.Torso == nil) do + wait() + humanoid = findPlayerHumanoid(player) + end + + if humanoid and humanoid.Torso and player.Character == newCharacter then + local newDesiredLook = (humanoid.Torso.CFrame.lookVector - Vector3.new(0, math.sin(math.rad(DEFAULT_CAMERA_ANGLE)), 0)).unit + local horizontalShift = findAngleBetweenXZVectors(newDesiredLook, this:GetCameraLook()) + local vertShift = math.asin(this:GetCameraLook().y) - math.asin(newDesiredLook.y) + if not IsFinite(horizontalShift) then + horizontalShift = 0 + end + if not IsFinite(vertShift) then + vertShift = 0 + end + this.RotateInput = Vector2.new(horizontalShift, vertShift) + + -- reset old camera info so follow cam doesn't rotate us + this.LastCameraTransform = nil + end + + -- Need to wait for camera cframe to update before we zoom in + -- Not waiting will force camera to original cframe + wait() + this:ZoomCamera(this.DefaultZoom) + end + + player.CharacterAdded:connect(function(character) + if this.Enabled or SetCameraOnSpawn then + OnCharacterAdded(character) + SetCameraOnSpawn = false + end + end) + if player.Character then + spawn(function() OnCharacterAdded(player.Character) end) + end + end + if PlayersService.LocalPlayer then + OnPlayerAdded(PlayersService.LocalPlayer) + end + PlayersService.ChildAdded:connect(function(child) + if child and PlayersService.LocalPlayer == child then + OnPlayerAdded(PlayersService.LocalPlayer) + end + end) + + local function OnGameLoaded() + hasGameLoaded = true + end + + spawn(function() + if game:IsLoaded() then + OnGameLoaded() + else + game.Loaded:wait() + OnGameLoaded() + end + end) + + local function OnDynamicThumbstickEnabled() + if UserInputService.TouchEnabled then + isDynamicThumbstickEnabled = true + end + end + + local function OnDynamicThumbstickDisabled() + isDynamicThumbstickEnabled = false + end + + local function OnGameSettingsTouchMovementModeChanged() + if LocalPlayer.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice then + if GameSettings.TouchMovementMode.Name == "DynamicThumbstick" then + OnDynamicThumbstickEnabled() + else + OnDynamicThumbstickDisabled() + end + end + end + + local function OnDevTouchMovementModeChanged() + if LocalPlayer.DevTouchMovementMode.Name == "DynamicThumbstick" then + OnDynamicThumbstickEnabled() + else + OnGameSettingsTouchMovementModeChanged() + end + end + + if PlayersService.LocalPlayer then + PlayersService.LocalPlayer.Changed:Connect(function(prop) + if prop == "DevTouchMovementMode" then + OnDevTouchMovementModeChanged() + end + end) + OnDevTouchMovementModeChanged() + end + + GameSettings.Changed:Connect(function(prop) + if prop == "TouchMovementMode" then + OnGameSettingsTouchMovementModeChanged() + end + end) + OnGameSettingsTouchMovementModeChanged() + GameSettings:SetCameraYInvertVisible() + pcall(function() GameSettings:SetGamepadCameraSensitivityVisible() end) + + return this +end + +return CreateCamera diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/AttachCamera.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/AttachCamera.lua new file mode 100644 index 0000000..f88837e --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/AttachCamera.lua @@ -0,0 +1,75 @@ +local PlayersService = game:GetService('Players') +local RootCameraCreator = require(script.Parent) + +local ZERO_VECTOR2 = Vector2.new(0, 0) +local XZ_VECTOR = Vector3.new(1,0,1) + +local Vector2_new = Vector2.new +local CFrame_new = CFrame.new +local math_atan2 = math.atan2 +local math_min = math.min + +local function IsFinite(num) + return num == num and num ~= 1/0 and num ~= -1/0 +end + +-- May return NaN or inf or -inf +-- This is a way of finding the angle between the two vectors: +local function findAngleBetweenXZVectors(vec2, vec1) + return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z) +end + +local function CreateAttachCamera() + local module = RootCameraCreator() + + local lastUpdate = tick() + function module:Update() + local now = tick() + + local camera = workspace.CurrentCamera + + if lastUpdate == nil or now - lastUpdate > 1 then + module:ResetCameraLook() + self.LastCameraTransform = nil + end + + local subjectPosition = self:GetSubjectPosition() + if subjectPosition and camera then + local zoom = self:GetCameraZoom() + if zoom <= 0 then + zoom = 0.1 + end + + + local humanoid = self:GetHumanoid() + if lastUpdate and humanoid and humanoid.Torso then + + -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from + local delta = math_min(0.1, now - lastUpdate) + local gamepadRotation = self:UpdateGamepad() + self.RotateInput = self.RotateInput + (gamepadRotation * delta) + + local forwardVector = humanoid.Torso.CFrame.lookVector + + local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook()) + if IsFinite(y) then + -- Preserve vertical rotation from user input + self.RotateInput = Vector2_new(y, self.RotateInput.Y) + end + end + + local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput) + self.RotateInput = ZERO_VECTOR2 + + camera.Focus = CFrame_new(subjectPosition) + local newCFrame = CFrame_new(subjectPosition - (zoom * newLookVector), subjectPosition) + camera.CFrame = newCFrame + self.LastCameraTransform = newCFrame + end + lastUpdate = now + end + + return module +end + +return CreateAttachCamera diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/ClassicCamera.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/ClassicCamera.lua new file mode 100644 index 0000000..bc2450f --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/ClassicCamera.lua @@ -0,0 +1,195 @@ + +local PlayersService = game:GetService('Players') +local VRService = game:GetService("VRService") + +local RootCameraCreator = require(script.Parent) + +local UP_VECTOR = Vector3.new(0, 1, 0) +local XZ_VECTOR = Vector3.new(1, 0, 1) +local ZERO_VECTOR2 = Vector2.new(0, 0) + +local VR_PITCH_FRACTION = 0.25 + +local Vector3_new = Vector3.new +local CFrame_new = CFrame.new +local math_min = math.min +local math_max = math.max +local math_atan2 = math.atan2 +local math_rad = math.rad +local math_abs = math.abs + +local function clamp(low, high, num) + return (num > high and high or num < low and low or num) +end + +local function IsFinite(num) + return num == num and num ~= 1/0 and num ~= -1/0 +end + +local function IsFiniteVector3(vec3) + return IsFinite(vec3.x) and IsFinite(vec3.y) and IsFinite(vec3.z) +end + +-- May return NaN or inf or -inf +-- This is a way of finding the angle between the two vectors: +local function findAngleBetweenXZVectors(vec2, vec1) + return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z) +end + +local function CreateClassicCamera() + local module = RootCameraCreator() + + local tweenAcceleration = math_rad(220) + local tweenSpeed = math_rad(0) + local tweenMaxSpeed = math_rad(250) + local timeBeforeAutoRotate = 2 + + local lastUpdate = tick() + module.LastUserPanCamera = tick() + function module:Update() + module:ProcessTweens() + local now = tick() + local timeDelta = (now - lastUpdate) + + local userPanningTheCamera = (self.UserPanningTheCamera == true) + local camera = workspace.CurrentCamera + local player = PlayersService.LocalPlayer + local humanoid = self:GetHumanoid() + local cameraSubject = camera and camera.CameraSubject + local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat') + local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform') + + if lastUpdate == nil or now - lastUpdate > 1 then + module:ResetCameraLook() + self.LastCameraTransform = nil + end + + if lastUpdate then + local gamepadRotation = self:UpdateGamepad() + + if self:ShouldUseVRRotation() then + self.RotateInput = self.RotateInput + self:GetVRRotationInput() + else + -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from + local delta = math_min(0.1, now - lastUpdate) + + if gamepadRotation ~= ZERO_VECTOR2 then + userPanningTheCamera = true + self.RotateInput = self.RotateInput + (gamepadRotation * delta) + end + + local angle = 0 + if not (isInVehicle or isOnASkateboard) then + angle = angle + (self.TurningLeft and -120 or 0) + angle = angle + (self.TurningRight and 120 or 0) + end + + if angle ~= 0 then + self.RotateInput = self.RotateInput + Vector2.new(math_rad(angle * delta), 0) + userPanningTheCamera = true + end + end + end + + -- Reset tween speed if user is panning + if userPanningTheCamera then + tweenSpeed = 0 + module.LastUserPanCamera = tick() + end + + local userRecentlyPannedCamera = now - module.LastUserPanCamera < timeBeforeAutoRotate + local subjectPosition = self:GetSubjectPosition() + + if subjectPosition and player and camera then + local zoom = self:GetCameraZoom() + if zoom < 0.5 then + zoom = 0.5 + end + + if self:GetShiftLock() and not self:IsInFirstPerson() then + -- We need to use the right vector of the camera after rotation, not before + local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput) + local offset = ((newLookVector * XZ_VECTOR):Cross(UP_VECTOR).unit * 1.75) + + if IsFiniteVector3(offset) then + subjectPosition = subjectPosition + offset + end + else + if not userPanningTheCamera and self.LastCameraTransform then + local isInFirstPerson = self:IsInFirstPerson() + if (isInVehicle or isOnASkateboard) and lastUpdate and humanoid and humanoid.Torso then + if isInFirstPerson then + if self.LastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then + local y = -findAngleBetweenXZVectors(self.LastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector) + if IsFinite(y) then + self.RotateInput = self.RotateInput + Vector2.new(y, 0) + end + tweenSpeed = 0 + end + elseif not userRecentlyPannedCamera then + local forwardVector = humanoid.Torso.CFrame.lookVector + if isOnASkateboard then + forwardVector = cameraSubject.CFrame.lookVector + end + + tweenSpeed = clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta) + + local percent = clamp(0, 1, tweenSpeed * timeDelta) + if self:IsInFirstPerson() then + percent = 1 + end + + local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook()) + if IsFinite(y) and math_abs(y) > 0.0001 then + self.RotateInput = self.RotateInput + Vector2.new(y * percent, 0) + end + end + end + end + end + + local VREnabled = VRService.VREnabled + camera.Focus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame_new(subjectPosition) + + local cameraFocusP = camera.Focus.p + if VREnabled and not self:IsInFirstPerson() then + local cameraHeight = self:GetCameraHeight() + local vecToSubject = (subjectPosition - camera.CFrame.p) + local distToSubject = vecToSubject.magnitude + + -- Only move the camera if it exceeded a maximum distance to the subject in VR + if distToSubject > zoom or self.RotateInput.x ~= 0 then + local desiredDist = math_min(distToSubject, zoom) + vecToSubject = self:RotateCamera(vecToSubject.unit * XZ_VECTOR, Vector2.new(self.RotateInput.x, 0)) * desiredDist + local newPos = cameraFocusP - vecToSubject + local desiredLookDir = camera.CFrame.lookVector + if self.RotateInput.x ~= 0 then + desiredLookDir = vecToSubject + end + local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z) + self.RotateInput = ZERO_VECTOR2 + + camera.CFrame = CFrame_new(newPos, lookAt) + Vector3_new(0, cameraHeight, 0) + end + else + local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput) + self.RotateInput = ZERO_VECTOR2 + camera.CFrame = CFrame_new(cameraFocusP - (zoom * newLookVector), cameraFocusP) + end + + self.LastCameraTransform = camera.CFrame + self.LastCameraFocus = camera.Focus + if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then + self.LastSubjectCFrame = cameraSubject.CFrame + else + self.LastSubjectCFrame = nil + end + end + + lastUpdate = now + end + + return module +end + +return CreateClassicCamera diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/FixedCamera.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/FixedCamera.lua new file mode 100644 index 0000000..17711d9 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/FixedCamera.lua @@ -0,0 +1,48 @@ +local PlayersService = game:GetService('Players') +local RootCameraCreator = require(script.Parent) + +local ZERO_VECTOR2 = Vector2.new(0, 0) + +local CFrame_new = CFrame.new +local math_min = math.min + +local function CreateFixedCamera() + local module = RootCameraCreator() + + local lastUpdate = tick() + function module:Update() + local now = tick() + + local camera = workspace.CurrentCamera + local player = PlayersService.LocalPlayer + if lastUpdate == nil or now - lastUpdate > 1 then + module:ResetCameraLook() + self.LastCameraTransform = nil + end + + if lastUpdate then + -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from + local delta = math_min(0.1, now - lastUpdate) + local gamepadRotation = self:UpdateGamepad() + self.RotateInput = self.RotateInput + (gamepadRotation * delta) + end + + local subjectPosition = self:GetSubjectPosition() + if subjectPosition and player and camera then + local zoom = self:GetCameraZoom() + if zoom <= 0 then + zoom = 0.1 + end + local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput) + self.RotateInput = ZERO_VECTOR2 + + camera.CFrame = CFrame_new(camera.CFrame.p, camera.CFrame.p + (zoom * newLookVector)) + self.LastCameraTransform = camera.CFrame + end + lastUpdate = now + end + + return module +end + +return CreateFixedCamera diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/FollowCamera.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/FollowCamera.lua new file mode 100644 index 0000000..11a374c --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/FollowCamera.lua @@ -0,0 +1,192 @@ +local PlayersService = game:GetService('Players') +local VRService = game:GetService("VRService") + +local RootCameraCreator = require(script.Parent) + +local CFrame_new = CFrame.new +local Vector2_new = Vector2.new +local Vector3_new = Vector3.new +local math_min = math.min +local math_max = math.max +local math_atan2 = math.atan2 +local math_rad = math.rad +local math_abs = math.abs + +local HUMANOIDSTATE_CLIMBING = Enum.HumanoidStateType.Climbing +local ZERO_VECTOR2 = Vector2.new(0, 0) +local UP_VECTOR = Vector3.new(0, 1, 0) +local XZ_VECTOR = Vector3.new(1, 0, 1) +local ZERO_VECTOR3 = Vector3.new(0, 0, 0) +local PORTRAIT_OFFSET = Vector3.new(0, -3, 0) + +local function clamp(low, high, num) + return num > high and high or num < low and low or num +end + +local function IsFinite(num) + return num == num and num ~= 1/0 and num ~= -1/0 +end + +local function IsFiniteVector3(vec3) + return IsFinite(vec3.x) and IsFinite(vec3.y) and IsFinite(vec3.z) +end + +-- May return NaN or inf or -inf +local function findAngleBetweenXZVectors(vec2, vec1) + -- This is a way of finding the angle between the two vectors: + return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z) +end + +local function CreateFollowCamera() + local module = RootCameraCreator() + + local tweenAcceleration = math_rad(220) + local tweenSpeed = math_rad(0) + local tweenMaxSpeed = math_rad(250) + local timeBeforeAutoRotate = 2 + + local lastUpdate = tick() + module.LastUserPanCamera = tick() + function module:Update() + module:ProcessTweens() + local now = tick() + local timeDelta = (now - lastUpdate) + + local userPanningTheCamera = (self.UserPanningTheCamera == true) + local camera = workspace.CurrentCamera + local player = PlayersService.LocalPlayer + local humanoid = self:GetHumanoid() + local cameraSubject = camera and camera.CameraSubject + local isClimbing = humanoid and humanoid:GetState() == HUMANOIDSTATE_CLIMBING + local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat') + local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform') + + if lastUpdate == nil or now - lastUpdate > 1 then + module:ResetCameraLook() + self.LastCameraTransform = nil + end + + if lastUpdate then + + if self:ShouldUseVRRotation() then + self.RotateInput = self.RotateInput + self:GetVRRotationInput() + else + -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from + local delta = math_min(0.1, now - lastUpdate) + local angle = 0 + -- NOTE: Traditional follow camera does not rotate with arrow keys + if not (isInVehicle or isOnASkateboard) then + angle = angle + (self.TurningLeft and -120 or 0) + angle = angle + (self.TurningRight and 120 or 0) + end + + local gamepadRotation = self:UpdateGamepad() + if gamepadRotation ~= Vector2.new(0,0) then + userPanningTheCamera = true + self.RotateInput = self.RotateInput + (gamepadRotation * delta) + end + + if angle ~= 0 then + userPanningTheCamera = true + self.RotateInput = self.RotateInput + Vector2_new(math_rad(angle * delta), 0) + end + end + end + + -- Reset tween speed if user is panning + if userPanningTheCamera then + tweenSpeed = 0 + module.LastUserPanCamera = tick() + end + + local userRecentlyPannedCamera = now - module.LastUserPanCamera < timeBeforeAutoRotate + + local subjectPosition = self:GetSubjectPosition() + if subjectPosition and player and camera then + local zoom = self:GetCameraZoom() + if zoom < 0.5 then + zoom = 0.5 + end + + if self:GetShiftLock() and not self:IsInFirstPerson() then + local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput) + local offset = ((newLookVector * XZ_VECTOR):Cross(UP_VECTOR).unit * 1.75) + if IsFiniteVector3(offset) then + subjectPosition = subjectPosition + offset + end + else + if self.LastCameraTransform and not userPanningTheCamera then + local isInFirstPerson = self:IsInFirstPerson() + if (isClimbing or isInVehicle or isOnASkateboard) and lastUpdate and humanoid and humanoid.Torso then + if isInFirstPerson then + if self.LastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then + local y = -findAngleBetweenXZVectors(self.LastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector) + if IsFinite(y) then + self.RotateInput = self.RotateInput + Vector2.new(y, 0) + end + tweenSpeed = 0 + end + elseif not userRecentlyPannedCamera then + local forwardVector = humanoid.Torso.CFrame.lookVector + if isOnASkateboard then + forwardVector = cameraSubject.CFrame.lookVector + end + + tweenSpeed = clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta) + + local percent = clamp(0, 1, tweenSpeed*timeDelta) + if not isClimbing and self:IsInFirstPerson() then + percent = 1 + end + local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook()) + -- Check for NaN + if IsFinite(y) and math_abs(y) > 0.0001 then + self.RotateInput = self.RotateInput + Vector2.new(y * percent, 0) + end + end + elseif not (isInFirstPerson or userRecentlyPannedCamera) and not VRService.VREnabled then + local lastVec = -(self.LastCameraTransform.p - subjectPosition) + + local y = findAngleBetweenXZVectors(lastVec, self:GetCameraLook()) + + -- This cutoff is to decide if the humanoid's angle of movement, + -- relative to the camera's look vector, is enough that + -- we want the camera to be following them. The point is to provide + -- a sizable deadzone to allow more precise forward movements. + local thetaCutoff = 0.4 + + -- Check for NaNs + if IsFinite(y) and math.abs(y) > 0.0001 and math_abs(y) > thetaCutoff*timeDelta then + self.RotateInput = self.RotateInput + Vector2.new(y, 0) + end + end + end + end + local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput) + self.RotateInput = ZERO_VECTOR2 + + if VRService.VREnabled then + camera.Focus = self:GetVRFocus(subjectPosition, timeDelta) + elseif self:IsPortraitMode() then + camera.Focus = CFrame_new(subjectPosition + PORTRAIT_OFFSET) + else + camera.Focus = CFrame_new(subjectPosition) + end + camera.CFrame = CFrame_new(camera.Focus.p - (zoom * newLookVector), camera.Focus.p) + Vector3.new(0, self:GetCameraHeight(), 0) + + self.LastCameraTransform = camera.CFrame + self.LastCameraFocus = camera.Focus + if isInVehicle or isOnASkateboard and cameraSubject:IsA('BasePart') then + self.LastSubjectCFrame = cameraSubject.CFrame + else + self.LastSubjectCFrame = nil + end + end + + lastUpdate = now + end + + return module +end + +return CreateFollowCamera diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/OrbitalCamera.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/OrbitalCamera.lua new file mode 100644 index 0000000..1ff6313 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/OrbitalCamera.lua @@ -0,0 +1,450 @@ +--[[ + Orbital Camera 1.0.0 + AllYourBlox + + Derived from ClassicCamera, adds camera angle constraints, and represents position values in spherical + coordinates (azimuth, elevation, radius), instead of Cartesian coordinates (x, y, z). Azimuth is the + angle of rotation about the Y axis, with the zero-angle reference point corresponding to offsetting + the camera in the +Z direction, from where it will be looking in the -Z direction by default. Elevation + is the angle up from the XZ plane, where zero degrees is on the plane, and +90 degrees is on the +Y + axis. Distance is the camera-to-subject distance, the spherical coordinates radius, specified in studs. +--]] + +-- Do not edit these values, they are not the developer-set limits, they are limits +-- to the values the camera system equations can correctly handle +local MIN_ALLOWED_ELEVATION_DEG = -80 +local MAX_ALLOWED_ELEVATION_DEG = 80 + +local externalProperties = {} +externalProperties["InitialDistance"] = 25 +externalProperties["MinDistance"] = 10 +externalProperties["MaxDistance"] = 100 +externalProperties["InitialElevation"] = 35 +externalProperties["MinElevation"] = 35 +externalProperties["MaxElevation"] = 35 +externalProperties["ReferenceAzimuth"] = -45 -- Angle around the Y axis where the camera starts. -45 offsets the camera in the -X and +Z directions equally +externalProperties["CWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CW as seen from above +externalProperties["CCWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CCW as seen from above +externalProperties["UseAzimuthLimits"] = false -- Full rotation around Y axis available by default + +local refAzimuthRad +local curAzimuthRad +local minAzimuthAbsoluteRad +local maxAzimuthAbsoluteRad +local useAzimuthLimits +local curElevationRad +local minElevationRad +local maxElevationRad +local curDistance +local minDistance +local maxDistance + +local UNIT_Z = Vector3.new(0,0,1) +local TAU = 2 * math.pi + +local changedSignalConnections = {} + +-- End of OrbitalCamera additions +local PlayersService = game:GetService('Players') +local VRService = game:GetService("VRService") + +local RootCameraCreator = require(script.Parent) + +local UP_VECTOR = Vector3.new(0, 1, 0) +local XZ_VECTOR = Vector3.new(1, 0, 1) +local ZERO_VECTOR2 = Vector2.new(0, 0) + +local VR_PITCH_FRACTION = 0.25 + +local Vector3_new = Vector3.new +local CFrame_new = CFrame.new +local math_min = math.min +local math_max = math.max +local math_atan2 = math.atan2 +local math_rad = math.rad +local math_abs = math.abs + +--[[ Gamepad Support ]]-- +local THUMBSTICK_DEADZONE = 0.2 +local r3ButtonDown = false +local l3ButtonDown = false +local currentZoomSpeed = 1 -- Multiplier, so 1 == no zooming + +local function clamp(value, minValue, maxValue) + if maxValue < minValue then + maxValue = minValue + end + return math.clamp(value, minValue, maxValue) +end + +local function IsFinite(num) + return num == num and num ~= 1/0 and num ~= -1/0 +end + +local function IsFiniteVector3(vec3) + return IsFinite(vec3.x) and IsFinite(vec3.y) and IsFinite(vec3.z) +end + +-- May return NaN or inf or -inf +-- This is a way of finding the angle between the two vectors: +local function findAngleBetweenXZVectors(vec2, vec1) + return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z) +end + +local function GetValueObject(name, defaultValue) + local valueObj = script:FindFirstChild(name) + if valueObj then + return valueObj.Value + end + return defaultValue +end + +local function LoadOrCreateNumberValueParameter(name, valueType, updateFunction) + local valueObj = script:FindFirstChild(name) + + if valueObj and valueObj:isA(valueType) then + -- Value object exists and is the correct type, use its value + externalProperties[name] = valueObj.Value + elseif externalProperties[name] ~= nil then + -- Create missing (or replace incorrectly-typed) valueObject with default value + valueObj = Instance.new(valueType) + valueObj.Name = name + valueObj.Parent = script + valueObj.Value = externalProperties[name] + else + print("externalProperties table has no entry for ",name) + return + end + + if updateFunction then + if changedSignalConnections[name] then + changedSignalConnections[name]:disconnect() + end + changedSignalConnections[name] = valueObj.Changed:connect(function(newValue) + externalProperties[name] = newValue + updateFunction() + end) + end +end + +local function SetAndBoundsCheckAzimuthValues() + minAzimuthAbsoluteRad = math.rad(externalProperties["ReferenceAzimuth"]) - math.abs(math.rad(externalProperties["CWAzimuthTravel"])) + maxAzimuthAbsoluteRad = math.rad(externalProperties["ReferenceAzimuth"]) + math.abs(math.rad(externalProperties["CCWAzimuthTravel"])) + useAzimuthLimits = externalProperties["UseAzimuthLimits"] + if useAzimuthLimits then + curAzimuthRad = math.max(curAzimuthRad, minAzimuthAbsoluteRad) + curAzimuthRad = math.min(curAzimuthRad, maxAzimuthAbsoluteRad) + end +end + +local function SetAndBoundsCheckElevationValues() + -- These degree values are the direct user input values. It is deliberate that they are + -- ranged checked only against the extremes, and not against each other. Any time one + -- is changed, both of the internal values in radians are recalculated. This allows for + -- A developer to change the values in any order and for the end results to be that the + -- internal values adjust to match intent as best as possible. + local minElevationDeg = math.max(externalProperties["MinElevation"], MIN_ALLOWED_ELEVATION_DEG) + local maxElevationDeg = math.min(externalProperties["MaxElevation"], MAX_ALLOWED_ELEVATION_DEG) + + -- Set internal values in radians + minElevationRad = math.rad(math.min(minElevationDeg, maxElevationDeg)) + maxElevationRad = math.rad(math.max(minElevationDeg, maxElevationDeg)) + curElevationRad = math.max(curElevationRad, minElevationRad) + curElevationRad = math.min(curElevationRad, maxElevationRad) +end + +local function SetAndBoundsCheckDistanceValues() + minDistance = externalProperties["MinDistance"] + maxDistance = externalProperties["MaxDistance"] + curDistance = math.max(curDistance, minDistance) + curDistance = math.min(curDistance, maxDistance) +end + +-- This loads from, or lazily creates, NumberValue objects for exposed parameters +local function LoadNumberValueParameters() + -- These initial values do not require change listeners since they are read only once + LoadOrCreateNumberValueParameter("InitialElevation", "NumberValue", nil) + LoadOrCreateNumberValueParameter("InitialDistance", "NumberValue", nil) + + -- Note: ReferenceAzimuth is also used as an initial value, but needs a change listener because it is used in the calculation of the limits + LoadOrCreateNumberValueParameter("ReferenceAzimuth", "NumberValue", SetAndBoundsCheckAzimuthValues) + LoadOrCreateNumberValueParameter("CWAzimuthTravel", "NumberValue", SetAndBoundsCheckAzimuthValues) + LoadOrCreateNumberValueParameter("CCWAzimuthTravel", "NumberValue", SetAndBoundsCheckAzimuthValues) + LoadOrCreateNumberValueParameter("MinElevation", "NumberValue", SetAndBoundsCheckElevationValues) + LoadOrCreateNumberValueParameter("MaxElevation", "NumberValue", SetAndBoundsCheckElevationValues) + LoadOrCreateNumberValueParameter("MinDistance", "NumberValue", SetAndBoundsCheckDistanceValues) + LoadOrCreateNumberValueParameter("MaxDistance", "NumberValue", SetAndBoundsCheckDistanceValues) + LoadOrCreateNumberValueParameter("UseAzimuthLimits", "BoolValue", SetAndBoundsCheckAzimuthValues) + + -- Internal values set (in radians, from degrees), plus sanitization + curAzimuthRad = math.rad(externalProperties["ReferenceAzimuth"]) + curElevationRad = math.rad(externalProperties["InitialElevation"]) + curDistance = externalProperties["InitialDistance"] + + SetAndBoundsCheckAzimuthValues() + SetAndBoundsCheckElevationValues() + SetAndBoundsCheckDistanceValues() +end + +local function CreateOrbitalCamera() + local module = RootCameraCreator() + + LoadNumberValueParameters() + + module.DefaultZoom = curDistance + + local tweenAcceleration = math_rad(220) + local tweenSpeed = math_rad(0) + local tweenMaxSpeed = math_rad(250) + local timeBeforeAutoRotate = 2 + + local lastUpdate = tick() + module.LastUserPanCamera = tick() + + function module:Update() + module:ProcessTweens() + local now = tick() + local timeDelta = (now - lastUpdate) + local userPanningTheCamera = (self.UserPanningTheCamera == true) + local camera = workspace.CurrentCamera + local player = PlayersService.LocalPlayer + local humanoid = self:GetHumanoid() + local cameraSubject = camera and camera.CameraSubject + local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat') + local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform') + + if lastUpdate == nil or now - lastUpdate > 1 then + module:ResetCameraLook() + self.LastCameraTransform = nil + end + + if lastUpdate then + local gamepadRotation = self:UpdateGamepad() + + if self:ShouldUseVRRotation() then + self.RotateInput = self.RotateInput + self:GetVRRotationInput() + else + -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from + local delta = math_min(0.1, now - lastUpdate) + + if gamepadRotation ~= ZERO_VECTOR2 then + userPanningTheCamera = true + self.RotateInput = self.RotateInput + (gamepadRotation * delta) + end + + local angle = 0 + if not (isInVehicle or isOnASkateboard) then + angle = angle + (self.TurningLeft and -120 or 0) + angle = angle + (self.TurningRight and 120 or 0) + end + + if angle ~= 0 then + self.RotateInput = self.RotateInput + Vector2.new(math_rad(angle * delta), 0) + userPanningTheCamera = true + end + end + end + + -- Reset tween speed if user is panning + if userPanningTheCamera then + tweenSpeed = 0 + module.LastUserPanCamera = tick() + end + + local userRecentlyPannedCamera = now - module.LastUserPanCamera < timeBeforeAutoRotate + local subjectPosition = self:GetSubjectPosition() + + if subjectPosition and player and camera then + + + local zoom = self:ZoomCamera(curDistance * currentZoomSpeed) + + + if not userPanningTheCamera and self.LastCameraTransform then + local isInFirstPerson = self:IsInFirstPerson() + if (isInVehicle or isOnASkateboard) and lastUpdate and humanoid and humanoid.Torso then + if isInFirstPerson then + if self.LastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then + local y = -findAngleBetweenXZVectors(self.LastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector) + if IsFinite(y) then + self.RotateInput = self.RotateInput + Vector2.new(y, 0) + end + tweenSpeed = 0 + end + elseif not userRecentlyPannedCamera then + local forwardVector = humanoid.Torso.CFrame.lookVector + if isOnASkateboard then + forwardVector = cameraSubject.CFrame.lookVector + end + + tweenSpeed = clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta) + + local percent = clamp(0, 1, tweenSpeed * timeDelta) + if self:IsInFirstPerson() then + percent = 1 + end + + local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook()) + if IsFinite(y) and math_abs(y) > 0.0001 then + self.RotateInput = self.RotateInput + Vector2.new(y * percent, 0) + end + end + end + end + + local VREnabled = VRService.VREnabled + camera.Focus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame_new(subjectPosition) + + local cameraFocusP = camera.Focus.p + if VREnabled and not self:IsInFirstPerson() then + local cameraHeight = self:GetCameraHeight() + local vecToSubject = (subjectPosition - camera.CFrame.p) + local distToSubject = vecToSubject.magnitude + + -- Only move the camera if it exceeded a maximum distance to the subject in VR + if distToSubject > zoom or self.RotateInput.x ~= 0 then + local desiredDist = math_min(distToSubject, zoom) + vecToSubject = self:RotateCamera(vecToSubject.unit * XZ_VECTOR, Vector2.new(self.RotateInput.x, 0)) * desiredDist + local newPos = cameraFocusP - vecToSubject + local desiredLookDir = camera.CFrame.lookVector + if self.RotateInput.x ~= 0 then + desiredLookDir = vecToSubject + end + local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z) + self.RotateInput = ZERO_VECTOR2 + + camera.CFrame = CFrame_new(newPos, lookAt) + Vector3_new(0, cameraHeight, 0) + end + else + -- self.RotateInput is a Vector2 of mouse movement deltas since last update + curAzimuthRad = curAzimuthRad - self.RotateInput.x + + if useAzimuthLimits then + curAzimuthRad = clamp(curAzimuthRad, minAzimuthAbsoluteRad, maxAzimuthAbsoluteRad) + else + curAzimuthRad = (curAzimuthRad ~= 0) and (math.sign(curAzimuthRad) * (math.abs(curAzimuthRad) % TAU)) or 0 + end + + curDistance = clamp(zoom, minDistance, maxDistance) + + curElevationRad = clamp(curElevationRad + self.RotateInput.y, minElevationRad,maxElevationRad) + + local cameraPosVector = curDistance * ( CFrame.fromEulerAnglesYXZ( -curElevationRad, curAzimuthRad, 0 ) * UNIT_Z ) + local camPos = subjectPosition + cameraPosVector + + camera.CFrame = CFrame.new(camPos, subjectPosition) + + self.RotateInput = ZERO_VECTOR2 + end + + self.LastCameraTransform = camera.CFrame + self.LastCameraFocus = camera.Focus + if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then + self.LastSubjectCFrame = cameraSubject.CFrame + else + self.LastSubjectCFrame = nil + end + end + + lastUpdate = now + end + + function module:GetCameraZoom() + return curDistance + end + + function module:ZoomCamera(desiredZoom) + local player = PlayersService.LocalPlayer + if player then + curDistance = clamp(desiredZoom, minDistance, maxDistance) + end + + local isFirstPerson = self:GetCameraZoom() < 2 + + --ShiftLockController:SetIsInFirstPerson(isFirstPerson) + -- set mouse behavior + self:UpdateMouseBehavior() + return self:GetCameraZoom() + end + + function module:ZoomCameraBy(zoomScale) + local newDist = curDistance + -- Can break into more steps to get more accurate integration + newDist = self:rk4Integrator(curDistance, zoomScale, 1) + self:ZoomCamera(newDist) + return self:GetCameraZoom() + end + + function module:ZoomCameraFixedBy(zoomIncrement) + return self:ZoomCamera(self:GetCameraZoom() + zoomIncrement) + end + + function module:SetInitialOrientation(humanoid) + local newDesiredLook = (humanoid.Torso.CFrame.lookVector - Vector3.new(0,0.23,0)).unit + local horizontalShift = findAngleBetweenXZVectors(newDesiredLook, self:GetCameraLook()) + local vertShift = math.asin(self:GetCameraLook().y) - math.asin(newDesiredLook.y) + if not IsFinite(horizontalShift) then + horizontalShift = 0 + end + if not IsFinite(vertShift) then + vertShift = 0 + end + self.RotateInput = Vector2.new(horizontalShift, vertShift) + end + + function module.getGamepadPan(name, state, input) + if input.UserInputType == module.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then + if r3ButtonDown or l3ButtonDown then + -- R3 or L3 Thumbstick is depressed, right stick controls dolly in/out + if (input.Position.Y > THUMBSTICK_DEADZONE) then + currentZoomSpeed = 0.96 + elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then + currentZoomSpeed = 1.04 + else + currentZoomSpeed = 1.00 + end + else + if state == Enum.UserInputState.Cancel then + module.GamepadPanningCamera = ZERO_VECTOR2 + return + end + + local inputVector = Vector2.new(input.Position.X, -input.Position.Y) + if inputVector.magnitude > THUMBSTICK_DEADZONE then + module.GamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y) + else + module.GamepadPanningCamera = ZERO_VECTOR2 + end + end + end + end + + function module.doGamepadZoom(name, state, input) + if input.UserInputType == module.activeGamepad and (input.KeyCode == Enum.KeyCode.ButtonR3 or input.KeyCode == Enum.KeyCode.ButtonL3) then + if (state == Enum.UserInputState.Begin) then + r3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonR3 + l3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonL3 + elseif (state == Enum.UserInputState.End) then + if (input.KeyCode == Enum.KeyCode.ButtonR3) then + r3ButtonDown = false + elseif (input.KeyCode == Enum.KeyCode.ButtonL3) then + l3ButtonDown = false + end + if (not r3ButtonDown) and (not l3ButtonDown) then + currentZoomSpeed = 1.00 + end + end + end + end + + function module:BindGamepadInputActions() + local ContextActionService = game:GetService('ContextActionService') + ContextActionService:BindAction("OrbitalCamGamepadPan", module.getGamepadPan, false, Enum.KeyCode.Thumbstick2) + ContextActionService:BindAction("OrbitalCamGamepadZoom", module.doGamepadZoom, false, Enum.KeyCode.ButtonR3) + ContextActionService:BindAction("OrbitalCamGamepadZoomAlt", module.doGamepadZoom, false, Enum.KeyCode.ButtonL3) + end + + return module +end + +return CreateOrbitalCamera diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/ScriptableCamera.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/ScriptableCamera.lua new file mode 100644 index 0000000..9526781 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/ScriptableCamera.lua @@ -0,0 +1,12 @@ +local RootCameraCreator = require(script.Parent) + +local function CreateScriptableCamera() + local module = RootCameraCreator() + + function module:Update() + end + + return module +end + +return CreateScriptableCamera diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/TrackCamera.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/TrackCamera.lua new file mode 100644 index 0000000..44ba394 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/TrackCamera.lua @@ -0,0 +1,54 @@ +local PlayersService = game:GetService('Players') +local RootCameraCreator = require(script.Parent) + +local ZERO_VECTOR2 = Vector2.new(0, 0) + +local CFrame_new = CFrame.new +local math_min = math.min + +local function CreateTrackCamera() + local module = RootCameraCreator() + + local lastUpdate = tick() + function module:Update() + local now = tick() + + local userPanningTheCamera = (self.UserPanningTheCamera == true) + local camera = workspace.CurrentCamera + local player = PlayersService.LocalPlayer + + if lastUpdate == nil or now - lastUpdate > 1 then + module:ResetCameraLook() + self.LastCameraTransform = nil + end + + if lastUpdate then + -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from + local delta = math_min(0.1, now - lastUpdate) + local gamepadRotation = self:UpdateGamepad() + if gamepadRotation ~= ZERO_VECTOR2 then + userPanningTheCamera = true + self.RotateInput = self.RotateInput + (gamepadRotation * delta) + end + end + + local subjectPosition = self:GetSubjectPosition() + if subjectPosition and player and camera then + local zoom = self:GetCameraZoom() + if zoom <= 0 then + zoom = 0.1 + end + local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput) + self.RotateInput = ZERO_VECTOR2 + + camera.Focus = CFrame_new(subjectPosition) + camera.CFrame = CFrame_new(subjectPosition - (zoom * newLookVector), subjectPosition) + self.LastCameraTransform = camera.CoordinateFrame + end + lastUpdate = now + end + + return module +end + +return CreateTrackCamera diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/VRCamera.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/VRCamera.lua new file mode 100644 index 0000000..7eb052c --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/VRCamera.lua @@ -0,0 +1,260 @@ +local PlayersService = game:GetService('Players') +local UserInputService = game:GetService('UserInputService') +local VRService = game:GetService("VRService") +local ContextActionService = game:GetService("ContextActionService") + +local LocalPlayer = PlayersService.LocalPlayer + +local RootCameraCreator = require(script.Parent) + +local XZ_VECTOR = Vector3.new(1, 0, 1) +local ZERO_VECTOR2 = Vector2.new(0, 0) + +local PRESNAP_TIME_OFFSET = 0.5 --seconds +local PRESNAP_TIME_LATCH = PRESNAP_TIME_OFFSET * 10 + +local HEIGHT_OFFSET = 3 +local HORZ_OFFSET = 4 + +local function CreateVRCamera() + local module = RootCameraCreator() + module.AllowOcclusion = false + + local lastUpdate = tick() + + local forceSnap = true + local forceSnapPoint = nil + local lastLook = Vector3.new(0, 0, -1) + + local movementUpdateEventConn = nil + + local lastSnapTimeEstimate = math.huge + local snapId = 0 + local waitingForSnap = false + local snappedHeadOffset = VRService:GetUserCFrame(Enum.UserCFrame.Head).p + local snapPendingWhileJumping = false + local isJumping = false + local autoSnapsPaused = false + + local currentDestination = nil + + local function forceSnapTo(snapPoint) + forceSnap = true + forceSnapPoint = snapPoint + snapId = snapId + 1 + waitingForSnap = false + end + + local function onMovementUpdateEvent(updateType, arg1, arg2) + if updateType == "targetPoint" then + currentDestination = arg1 + end + if updateType == "timing" then + local estimatedTimeRemaining = arg1 + local snapPoint = arg2 + + if waitingForSnap and estimatedTimeRemaining > lastSnapTimeEstimate then + --our estimate grew, so cancel this snap and potentially re-evaluate it + waitingForSnap = false + snapId = snapId + 1 + end + + if estimatedTimeRemaining < PRESNAP_TIME_LATCH and estimatedTimeRemaining > PRESNAP_TIME_OFFSET then + waitingForSnap = true + snapId = snapId + 1 + local thisSnapId = snapId + + local timeToWait = estimatedTimeRemaining - PRESNAP_TIME_OFFSET + coroutine.wrap(function() + wait(timeToWait) + if waitingForSnap and snapId == thisSnapId then + waitingForSnap = false + forceSnap = true + forceSnapPoint = snapPoint + end + end)() + end + elseif updateType == "shortPath" or updateType == "pathFailure" or updateType == "offtrack" and not autoSnapsPaused then + if isJumping then + snapPendingWhileJumping = true + return + end + local snapPoint = arg1 + forceSnapTo(snapPoint) + elseif updateType == "force" then + snapPendingWhileJumping = false + isJumping = false + forceSnapTo(nil) + end + end + + local function onResetCameraAction(actionName, inputState, inputObj) + if inputState == Enum.UserInputState.Begin then + autoSnapsPaused = true + onMovementUpdateEvent("force", nil, nil) + else + autoSnapsPaused = false + end + end + + local function bindActions(bind) + if bind then + ContextActionService:BindActionAtPriority("ResetCameraVR", onResetCameraAction, false, Enum.ContextActionPriority.Low.Value, Enum.KeyCode.ButtonL2) + else + autoSnapsPaused = false + ContextActionService:UnbindAction("ResetCameraVR") + end + end + + local lastKnownTorsoPosition = Vector3.new() + local function onCharacterAdded(character) + local humanoid = character:WaitForChild("Humanoid") + + humanoid.StateChanged:connect(function(oldState, newState) + local camera = workspace.CurrentCamera + if not camera or camera.CameraSubject ~= humanoid or not VRService.VREnabled then + return + end + if newState == Enum.HumanoidStateType.Jumping then + isJumping = true + elseif newState == Enum.HumanoidStateType.Landed then + if snapPendingWhileJumping then + forceSnapTo(nil) + snapPendingWhileJumping = false + end + isJumping = false + elseif newState == Enum.HumanoidStateType.Dead then + forceSnapTo(nil) + elseif newState == Enum.HumanoidStateType.Swimming then + if isJumping and snapPendingWhileJumping then + --Jumped into water and let go of controls during flight, treat as a normal landing + forceSnapTo(nil) + snapPendingWhileJumping = false + end + isJumping = false + end + end) + + local humanoidRootPart = humanoid.Torso + humanoidRootPart.Changed:connect(function(property) + local camera = workspace.CurrentCamera + local cameraSubject = camera.CameraSubject + if camera and cameraSubject == humanoid and property == "CFrame" or property == "Position" then + if (humanoidRootPart.Position - lastKnownTorsoPosition).magnitude > 5 then + forceSnapTo(nil) + end + end + end) + end + + if LocalPlayer.Character then + onCharacterAdded(LocalPlayer.Character) + end + LocalPlayer.CharacterAdded:connect(onCharacterAdded) + + spawn(function() + local rootCamera = script.Parent + if not rootCamera then return end + + local cameraScript = rootCamera.Parent + if not cameraScript then return end + + local playerScripts = cameraScript.Parent + if not playerScripts then return end + + local controlScript = playerScripts:WaitForChild("ControlScript") + local masterControlModule = controlScript:WaitForChild("MasterControl") + local vrNavigationModule = masterControlModule:WaitForChild("VRNavigation") + local movementUpdateEvent = vrNavigationModule:WaitForChild("MovementUpdate") + + movementUpdateEventConn = movementUpdateEvent.Event:connect(onMovementUpdateEvent) + end) + + local onCameraSubjectChangedConn = nil + local function onCameraSubjectChanged() + local camera = workspace.CurrentCamera + if camera and camera.CameraSubject then + delay(1, function () forceSnap = true end) + end + end + + local function onCurrentCameraChanged() + if onCameraSubjectChangedConn then + onCameraSubjectChangedConn:disconnect() + onCameraSubjectChangedConn = nil + end + if workspace.CurrentCamera then + onCameraSubjectChangedConn = workspace.CurrentCamera:GetPropertyChangedSignal("CameraSubject"):connect(onCameraSubjectChanged) + onCameraSubjectChanged() + end + end + workspace:GetPropertyChangedSignal("CurrentCamera"):connect(onCurrentCameraChanged) + onCurrentCameraChanged() + + local function onVREnabled() + bindActions(VRService.VREnabled) + end + VRService:GetPropertyChangedSignal("VREnabled"):connect(onVREnabled) + + function module:Update() + local now = tick() + local timeDelta = now - lastUpdate + + local camera = workspace.CurrentCamera + local cameraSubject = camera and camera.CameraSubject + local player = PlayersService.LocalPlayer + local subjectPosition = self:GetSubjectPosition() + local zoom = self:GetCameraZoom() + + local subjectPosition = self:GetSubjectPosition() + local gamepadRotation = self:UpdateGamepad() + + if subjectPosition and currentDestination then + local dist = (currentDestination - subjectPosition).magnitude + if dist < 10 then + forceSnap = true + forceSnapPoint = currentDestination + currentDestination = nil + end + end + + if cameraSubject and cameraSubject:IsA("Humanoid") then + local rootPart = cameraSubject.RootPart + if rootPart then + lastKnownTorsoPosition = rootPart.Position + end + end + + local look = lastLook + if subjectPosition and forceSnap then + forceSnap = false + + local newFocusPoint = subjectPosition + if forceSnapPoint then + newFocusPoint = Vector3.new(forceSnapPoint.X, subjectPosition.Y, forceSnapPoint.Z) + end + + camera.Focus = CFrame.new(newFocusPoint) + forceSnapPoint = nil + look = camera:GetRenderCFrame().lookVector + snappedHeadOffset = VRService:GetUserCFrame(Enum.UserCFrame.Head).p + end + if subjectPosition and player and camera then + local cameraFocusP = camera.Focus.p + self.RotateInput = ZERO_VECTOR2 + look = (look * XZ_VECTOR).unit + camera.CFrame = CFrame.new(cameraFocusP - (HORZ_OFFSET * look)) + Vector3.new(0, HEIGHT_OFFSET, 0) - snappedHeadOffset + + self.LastCameraTransform = camera.CFrame + self.LastCameraFocus = camera.Focus + end + lastLook = look + + lastUpdate = now + end + + return module +end + +return CreateVRCamera + diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/WatchCamera.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/WatchCamera.lua new file mode 100644 index 0000000..007a1aa --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/WatchCamera.lua @@ -0,0 +1,63 @@ +local PlayersService = game:GetService('Players') +local RootCameraCreator = require(script.Parent) + +local ZERO_VECTOR2 = Vector2.new(0, 0) + +local CFrame_new = CFrame.new + +local function CreateWatchCamera() + local module = RootCameraCreator() + module.PanEnabled = false + + local lastUpdate = tick() + function module:Update() + local now = tick() + + local camera = workspace.CurrentCamera + local player = PlayersService.LocalPlayer + + if lastUpdate == nil or now - lastUpdate > 1 then + module:ResetCameraLook() + self.LastZoom = nil + end + + + local subjectPosition = self:GetSubjectPosition() + if subjectPosition and player and camera then + local cameraLook = nil + + local humanoid = self:GetHumanoid() + if humanoid and humanoid.Torso then + -- TODO: let the paging buttons move the camera but not the mouse/touch + -- currently neither do + local diffVector = subjectPosition - camera.CFrame.p + cameraLook = diffVector.unit + + if self.LastZoom and self.LastZoom == self:GetCameraZoom() then + -- Don't clobber the zoom if they zoomed the camera + local zoom = diffVector.magnitude + self:ZoomCamera(zoom) + end + end + + local zoom = self:GetCameraZoom() + if zoom <= 0 then + zoom = 0.1 + end + + local newLookVector = self:RotateVector(cameraLook or self:GetCameraLook(), self.RotateInput) + self.RotateInput = ZERO_VECTOR2 + local newFocus = CFrame_new(subjectPosition) + local newCamCFrame = CFrame_new(newFocus.p - (zoom * newLookVector), subjectPosition) + + camera.Focus = newFocus + camera.CFrame = newCamCFrame + self.LastZoom = zoom + end + lastUpdate = now + end + + return module +end + +return CreateWatchCamera diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/ShiftLockController.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/ShiftLockController.lua new file mode 100644 index 0000000..4a36be9 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/ShiftLockController.lua @@ -0,0 +1,206 @@ +--[[ + // FileName: ShiftLockController + // Written by: jmargh + // Version 1.2 + // Description: Manages the state of shift lock mode + + // Required by: + RootCamera + + // Note: ContextActionService sinks keys, so until we allow binding to ContextActionService without sinking + // keys, this module will use UserInputService. +--]] + +local Players = game:GetService('Players') +local UserInputService = game:GetService('UserInputService') +-- Settings and GameSettings are read only +local Settings = UserSettings() -- ignore warning +local GameSettings = Settings.GameSettings + +local ShiftLockController = {} + +--[[ Script Variables ]]-- +while not Players.LocalPlayer do + Players.PlayerAdded:wait() +end + +local LocalPlayer = Players.LocalPlayer +local Mouse = LocalPlayer:GetMouse() +local PlayerGui = LocalPlayer:WaitForChild('PlayerGui') +local ScreenGui = nil +local ShiftLockIcon = nil +local InputCn = nil +local IsShiftLockMode = false +local IsShiftLocked = false +local IsActionBound = false +local IsInFirstPerson = false + +-- Toggle Event +ShiftLockController.OnShiftLockToggled = Instance.new('BindableEvent') + +-- wrapping long conditional in function +local function isShiftLockMode() + return LocalPlayer.DevEnableMouseLock and GameSettings.ControlMode == Enum.ControlMode.MouseLockSwitch and + LocalPlayer.DevComputerMovementMode ~= Enum.DevComputerMovementMode.ClickToMove and + GameSettings.ComputerMovementMode ~= Enum.ComputerMovementMode.ClickToMove and + LocalPlayer.DevComputerMovementMode ~= Enum.DevComputerMovementMode.Scriptable +end + +if not UserInputService.TouchEnabled then -- TODO: Remove when safe on mobile + IsShiftLockMode = isShiftLockMode() +end + +--[[ Constants ]]-- +local SHIFT_LOCK_OFF = 'rbxasset://textures/ui/mouseLock_off.png' +local SHIFT_LOCK_ON = 'rbxasset://textures/ui/mouseLock_on.png' +local SHIFT_LOCK_CURSOR = 'rbxasset://textures/MouseLockedCursor.png' + +--[[ Local Functions ]]-- +local function onShiftLockToggled() + IsShiftLocked = not IsShiftLocked + if IsShiftLocked then + ShiftLockIcon.Image = SHIFT_LOCK_ON + Mouse.Icon = SHIFT_LOCK_CURSOR + else + ShiftLockIcon.Image = SHIFT_LOCK_OFF + Mouse.Icon = "" + end + ShiftLockController.OnShiftLockToggled:Fire() +end + +local function initialize() + if ScreenGui then + ScreenGui:Destroy() + ScreenGui = nil + end + ScreenGui = Instance.new('ScreenGui') + ScreenGui.Name = "ControlGui" + + local frame = Instance.new('Frame') + frame.Name = "BottomLeftControl" + frame.Size = UDim2.new(0, 130, 0, 46) + frame.Position = UDim2.new(0, 0, 1, -46) + frame.BackgroundTransparency = 1 + frame.Parent = ScreenGui + + ShiftLockIcon = Instance.new('ImageButton') + ShiftLockIcon.Name = "MouseLockLabel" + ShiftLockIcon.Size = UDim2.new(0, 31, 0, 31) + ShiftLockIcon.Position = UDim2.new(0, 12, 0, 2) + ShiftLockIcon.BackgroundTransparency = 1 + ShiftLockIcon.Image = IsShiftLocked and SHIFT_LOCK_ON or SHIFT_LOCK_OFF + ShiftLockIcon.Visible = true + ShiftLockIcon.Parent = frame + + ShiftLockIcon.MouseButton1Click:connect(onShiftLockToggled) + + ScreenGui.Parent = IsShiftLockMode and PlayerGui or nil +end + +--[[ Public API ]]-- +function ShiftLockController:IsShiftLocked() + return IsShiftLockMode and IsShiftLocked +end + +function ShiftLockController:SetIsInFirstPerson(isInFirstPerson) + IsInFirstPerson = isInFirstPerson +end + +--[[ Input/Settings Changed Events ]]-- +local mouseLockSwitchFunc = function(actionName, inputState, inputObject) + if IsShiftLockMode then + onShiftLockToggled() + end +end + +local function disableShiftLock() + if ScreenGui then ScreenGui.Parent = nil end + IsShiftLockMode = false + Mouse.Icon = "" + if InputCn then + InputCn:disconnect() + InputCn = nil + end + IsActionBound = false + ShiftLockController.OnShiftLockToggled:Fire() +end + +-- TODO: Remove when we figure out ContextActionService without sinking keys +local function onShiftInputBegan(inputObject, isProcessed) + if isProcessed then return end + if inputObject.UserInputType == Enum.UserInputType.Keyboard and + (inputObject.KeyCode == Enum.KeyCode.LeftShift or inputObject.KeyCode == Enum.KeyCode.RightShift) then + -- + mouseLockSwitchFunc() + end +end + +local function enableShiftLock() + IsShiftLockMode = isShiftLockMode() + if IsShiftLockMode then + if ScreenGui then + ScreenGui.Parent = PlayerGui + end + if IsShiftLocked then + Mouse.Icon = SHIFT_LOCK_CURSOR + ShiftLockController.OnShiftLockToggled:Fire() + end + if not IsActionBound then + InputCn = UserInputService.InputBegan:connect(onShiftInputBegan) + IsActionBound = true + end + end +end + +GameSettings.Changed:connect(function(property) + if property == 'ControlMode' then + if GameSettings.ControlMode == Enum.ControlMode.MouseLockSwitch then + enableShiftLock() + else + disableShiftLock() + end + elseif property == 'ComputerMovementMode' then + if GameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove then + disableShiftLock() + else + enableShiftLock() + end + end +end) + +LocalPlayer.Changed:connect(function(property) + if property == 'DevEnableMouseLock' then + if LocalPlayer.DevEnableMouseLock then + enableShiftLock() + else + disableShiftLock() + end + elseif property == 'DevComputerMovementMode' then + if LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.ClickToMove or + LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.Scriptable then + -- + disableShiftLock() + else + enableShiftLock() + end + end +end) + +LocalPlayer.CharacterAdded:connect(function(character) + -- we need to recreate guis on character load + if not UserInputService.TouchEnabled then + initialize() + end +end) + +--[[ Initialization ]]-- + -- TODO: Remove when safe! ContextActionService crashes touch clients with tupele is 2 or more +if not UserInputService.TouchEnabled then + initialize() + if isShiftLockMode() then + InputCn = UserInputService.InputBegan:connect(onShiftInputBegan) + IsActionBound = true + end +end + +return ShiftLockController diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/TransparencyController.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/TransparencyController.lua new file mode 100644 index 0000000..4758b86 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/TransparencyController.lua @@ -0,0 +1,190 @@ +-- SolarCrane + +local MAX_TWEEN_RATE = 2.8 -- per second + +local function clamp(low, high, num) + return (num > high and high or num < low and low or num) +end + +local math_floor = math.floor + +local function Round(num, places) + local decimalPivot = 10^places + return math_floor(num * decimalPivot + 0.5) / decimalPivot +end + +local function CreateTransparencyController() + local module = {} + + + local LastUpdate = tick() + local TransparencyDirty = false + local Enabled = false + local LastTransparency = nil + + local DescendantAddedConn, DescendantRemovingConn = nil, nil + local ToolDescendantAddedConns = {} + local ToolDescendantRemovingConns = {} + local CachedParts = {} + + local function HasToolAncestor(object) + if object.Parent == nil then return false end + return object.Parent:IsA('Tool') or HasToolAncestor(object.Parent) + end + + local function IsValidPartToModify(part) + if part:IsA('BasePart') or part:IsA('Decal') then + return not HasToolAncestor(part) + end + return false + end + + local function CachePartsRecursive(object) + if object then + if IsValidPartToModify(object) then + CachedParts[object] = true + TransparencyDirty = true + end + for _, child in pairs(object:GetChildren()) do + CachePartsRecursive(child) + end + end + end + + local function TeardownTransparency() + for child, _ in pairs(CachedParts) do + child.LocalTransparencyModifier = 0 + end + CachedParts = {} + TransparencyDirty = true + LastTransparency = nil + + if DescendantAddedConn then + DescendantAddedConn:disconnect() + DescendantAddedConn = nil + end + if DescendantRemovingConn then + DescendantRemovingConn:disconnect() + DescendantRemovingConn = nil + end + for object, conn in pairs(ToolDescendantAddedConns) do + conn:disconnect() + ToolDescendantAddedConns[object] = nil + end + for object, conn in pairs(ToolDescendantRemovingConns) do + conn:disconnect() + ToolDescendantRemovingConns[object] = nil + end + end + + local function SetupTransparency(character) + TeardownTransparency() + + if DescendantAddedConn then DescendantAddedConn:disconnect() end + DescendantAddedConn = character.DescendantAdded:connect(function(object) + -- This is a part we want to invisify + if IsValidPartToModify(object) then + CachedParts[object] = true + TransparencyDirty = true + -- There is now a tool under the character + elseif object:IsA('Tool') then + if ToolDescendantAddedConns[object] then ToolDescendantAddedConns[object]:disconnect() end + ToolDescendantAddedConns[object] = object.DescendantAdded:connect(function(toolChild) + CachedParts[toolChild] = nil + if toolChild:IsA('BasePart') or toolChild:IsA('Decal') then + -- Reset the transparency + toolChild.LocalTransparencyModifier = 0 + end + end) + if ToolDescendantRemovingConns[object] then ToolDescendantRemovingConns[object]:disconnect() end + ToolDescendantRemovingConns[object] = object.DescendantRemoving:connect(function(formerToolChild) + wait() -- wait for new parent + if character and formerToolChild and formerToolChild:IsDescendantOf(character) then + if IsValidPartToModify(formerToolChild) then + CachedParts[formerToolChild] = true + TransparencyDirty = true + end + end + end) + end + end) + if DescendantRemovingConn then DescendantRemovingConn:disconnect() end + DescendantRemovingConn = character.DescendantRemoving:connect(function(object) + if CachedParts[object] then + CachedParts[object] = nil + -- Reset the transparency + object.LocalTransparencyModifier = 0 + end + end) + CachePartsRecursive(character) + end + + + function module:SetEnabled(newState) + if Enabled ~= newState then + Enabled = newState + self:Update() + end + end + + function module:SetSubject(subject) + local character = nil + if subject and subject:IsA("Humanoid") then + character = subject.Parent + end + if subject and subject:IsA("VehicleSeat") and subject.Occupant then + character = subject.Occupant.Parent + end + if character then + SetupTransparency(character) + else + TeardownTransparency() + end + end + + function module:Update() + local instant = false + local now = tick() + local currentCamera = workspace.CurrentCamera + + if currentCamera then + local transparency = 0 + if not Enabled then + instant = true + else + local distance = (currentCamera.Focus.p - currentCamera.CoordinateFrame.p).magnitude + transparency = (7 - distance) / 5 + if transparency < 0.5 then + transparency = 0 + end + + if LastTransparency then + local deltaTransparency = transparency - LastTransparency + -- Don't tween transparency if it is instant or your character was fully invisible last frame + if not instant and transparency < 1 and LastTransparency < 0.95 then + local maxDelta = MAX_TWEEN_RATE * (now - LastUpdate) + deltaTransparency = clamp(-maxDelta, maxDelta, deltaTransparency) + end + transparency = LastTransparency + deltaTransparency + else + TransparencyDirty = true + end + + transparency = clamp(0, 1, Round(transparency, 2)) + end + + if TransparencyDirty or LastTransparency ~= transparency then + for child, _ in pairs(CachedParts) do + child.LocalTransparencyModifier = transparency + end + TransparencyDirty = false + LastTransparency = transparency + end + end + LastUpdate = now + end + + return module +end + +return CreateTransparencyController diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript.lua new file mode 100644 index 0000000..eba4543 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript.lua @@ -0,0 +1,487 @@ +--[[ + // FileName: ControlScript.lua + // Version 1.1 + // Written by: jmargh and jeditkacheff + // Description: Manages in game controls for both touch and keyboard/mouse devices. + + // This script will be inserted into PlayerScripts under each player by default. If you want to + // create your own custom controls or modify these controls, you must place a script with this + // name, ControlScript, under StarterPlayer -> PlayerScripts. + + // Required Modules: + ClickToMove + DPad + KeyboardMovement + Thumbpad + Thumbstick + TouchJump + MasterControl + VehicleController +--]] + +--[[ Services ]]-- +local ContextActionService = game:GetService('ContextActionService') +local Players = game:GetService('Players') +local UserInputService = game:GetService('UserInputService') +local VRService = game:GetService('VRService') +-- Settings and GameSettings are read only +local Settings = UserSettings() +local GameSettings = Settings.GameSettings + +-- Issue with play solo? (F6) +while not UserInputService.KeyboardEnabled and not UserInputService.TouchEnabled and not UserInputService.GamepadEnabled do + wait() +end + +--[[ Script Variables ]]-- +while not Players.LocalPlayer do + wait() +end + +local lastInputType = nil +local LocalPlayer = Players.LocalPlayer +local PlayerGui = LocalPlayer:WaitForChild('PlayerGui') +local IsTouchDevice = UserInputService.TouchEnabled +local UserMovementMode = IsTouchDevice and GameSettings.TouchMovementMode or GameSettings.ComputerMovementMode +local DevMovementMode = IsTouchDevice and LocalPlayer.DevTouchMovementMode or LocalPlayer.DevComputerMovementMode +local IsUserChoice = (IsTouchDevice and DevMovementMode == Enum.DevTouchMovementMode.UserChoice) or (DevMovementMode == Enum.DevComputerMovementMode.UserChoice) +local TouchGui = nil +local TouchControlFrame = nil +local IsModalEnabled = UserInputService.ModalEnabled +local BindableEvent_OnFailStateChanged = nil +local isJumpEnabled = false + +-- register what control scripts we are using +do + local PlayerScripts = LocalPlayer:WaitForChild("PlayerScripts") + local canRegisterControls = pcall(function() PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.Default) end) + + if canRegisterControls then + PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.Thumbstick) + PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.DPad) + PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.Thumbpad) + PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.ClickToMove) + PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.DynamicThumbstick) + + PlayerScripts:RegisterComputerMovementMode(Enum.ComputerMovementMode.Default) + PlayerScripts:RegisterComputerMovementMode(Enum.ComputerMovementMode.KeyboardMouse) + PlayerScripts:RegisterComputerMovementMode(Enum.ComputerMovementMode.ClickToMove) + end +end + +local DynamicThumbstickAvailable = pcall(function() + return Enum.DevTouchMovementMode.DynamicThumbstick and Enum.TouchMovementMode.DynamicThumbstick +end) + +local FFlagUserNoCameraClickToMoveSuccess, FFlagUserNoCameraClickToMoveResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNoCameraClickToMove") end) +local FFlagUserNoCameraClickToMove = FFlagUserNoCameraClickToMoveSuccess and FFlagUserNoCameraClickToMoveResult + +--[[ Modules ]]-- +local ClickToMoveTouchControls = nil +local ControlModules = {} + +local ControlState = {} +ControlState.Current = nil +function ControlState:SwitchTo(newControl) + if ControlState.Current == newControl then return end + + if ControlState.Current then + ControlState.Current:Disable() + end + + ControlState.Current = newControl + + if ControlState.Current then + ControlState.Current:Enable() + end +end + +function ControlState:IsTouchJumpModuleUsed() + return isJumpEnabled +end + +local MasterControl = require(script:WaitForChild('MasterControl')) +--MasterControl needs access to ControlState in order to be able to fully enable and disable control +MasterControl.ControlState = ControlState + +local DynamicThumbstickModule = require(script.MasterControl:WaitForChild('DynamicThumbstick')) +local ThumbstickModule = require(script.MasterControl:WaitForChild('Thumbstick')) +local ThumbpadModule = require(script.MasterControl:WaitForChild('Thumbpad')) +local DPadModule = require(script.MasterControl:WaitForChild('DPad')) +local DefaultModule = ControlModules.Thumbstick +local TouchJumpModule = require(script.MasterControl:WaitForChild('TouchJump')) +local ClickToMoveModule = FFlagUserNoCameraClickToMove and require(script.MasterControl:WaitForChild('ClickToMoveController')) or nil + +MasterControl.TouchJumpModule = TouchJumpModule +local VRNavigationModule = require(script.MasterControl:WaitForChild('VRNavigation')) +local keyboardModule = require(script.MasterControl:WaitForChild('KeyboardMovement')) +ControlModules.Gamepad = require(script.MasterControl:WaitForChild('Gamepad')) + +function getTouchModule() + + local module = nil + if not IsUserChoice then + if DynamicThumbstickAvailable and DevMovementMode == Enum.DevTouchMovementMode.DynamicThumbstick then + module = DynamicThumbstickModule + isJumpEnabled = true + elseif DevMovementMode == Enum.DevTouchMovementMode.Thumbstick then + module = ThumbstickModule + isJumpEnabled = true + elseif DevMovementMode == Enum.DevTouchMovementMode.Thumbpad then + module = ThumbpadModule + isJumpEnabled = true + elseif DevMovementMode == Enum.DevTouchMovementMode.DPad then + module = DPadModule + isJumpEnabled = false + elseif DevMovementMode == Enum.DevTouchMovementMode.ClickToMove then + if FFlagUserNoCameraClickToMove then + module = ClickToMoveModule + isJumpEnabled = false -- TODO: What should this be, true or false? + else + module = nil + end + elseif DevMovementMode == Enum.DevTouchMovementMode.Scriptable then + module = nil + end + else + if DynamicThumbstickAvailable and UserMovementMode == Enum.TouchMovementMode.DynamicThumbstick then + module = DynamicThumbstickModule + isJumpEnabled = true + elseif UserMovementMode == Enum.TouchMovementMode.Default or UserMovementMode == Enum.TouchMovementMode.Thumbstick then + module = ThumbstickModule + isJumpEnabled = true + elseif UserMovementMode == Enum.TouchMovementMode.Thumbpad then + module = ThumbpadModule + isJumpEnabled = true + elseif UserMovementMode == Enum.TouchMovementMode.DPad then + module = DPadModule + isJumpEnabled = false + elseif UserMovementMode == Enum.TouchMovementMode.ClickToMove then + if FFlagUserNoCameraClickToMove then + module = ClickToMoveModule + isJumpEnabled = false -- TODO: What should this be, true or false? + else + module = nil + end + end + end + return module +end + +function setJumpModule(isEnabled) + if not isEnabled then + TouchJumpModule:Disable() + elseif ControlState.Current == ControlModules.Touch then + TouchJumpModule:Enable() + end +end + +function setClickToMove() + if DevMovementMode == Enum.DevTouchMovementMode.ClickToMove or DevMovementMode == Enum.DevComputerMovementMode.ClickToMove or + UserMovementMode == Enum.ComputerMovementMode.ClickToMove or UserMovementMode == Enum.TouchMovementMode.ClickToMove then + if lastInputType == Enum.UserInputType.Touch then + ClickToMoveTouchControls = ControlState.Current + end + elseif ClickToMoveTouchControls then + ClickToMoveTouchControls:Disable() + ClickToMoveTouchControls = nil + end +end + +ControlModules.Touch = {} +ControlModules.Touch.Current = nil +ControlModules.Touch.LocalPlayerChangedCon = nil +ControlModules.Touch.GameSettingsChangedCon = nil + +function ControlModules.Touch:RefreshControlStyle() + if ControlModules.Touch.Current then + ControlModules.Touch.Current:Disable() + end + setJumpModule(false) + TouchJumpModule:Disable() + ControlModules.Touch:Enable() +end +function ControlModules.Touch:DisconnectEvents() + if ControlModules.Touch.LocalPlayerChangedCon then + ControlModules.Touch.LocalPlayerChangedCon:disconnect() + ControlModules.Touch.LocalPlayerChangedCon = nil + end + if ControlModules.Touch.GameSettingsChangedCon then + ControlModules.Touch.GameSettingsChangedCon:disconnect() + ControlModules.Touch.GameSettingsChangedCon = nil + end +end +function ControlModules.Touch:Enable() + DevMovementMode = LocalPlayer.DevTouchMovementMode + IsUserChoice = DevMovementMode == Enum.DevTouchMovementMode.UserChoice + if IsUserChoice then + UserMovementMode = GameSettings.TouchMovementMode + end + + local newModuleToEnable = getTouchModule() + if newModuleToEnable then + setClickToMove() + setJumpModule(isJumpEnabled) + + newModuleToEnable:Enable() + ControlModules.Touch.Current = newModuleToEnable + + if isJumpEnabled then TouchJumpModule:Enable() end + end + + -- This being within the above if statement was causing issues with ClickToMove, which isn't a module within this script. + ControlModules.Touch:DisconnectEvents() + ControlModules.Touch.LocalPlayerChangedCon = LocalPlayer:GetPropertyChangedSignal("DevTouchMovementMode"):connect(function() + ControlModules.Touch:RefreshControlStyle() + end) + ControlModules.Touch.GameSettingsChangedCon = GameSettings:GetPropertyChangedSignal("TouchMovementMode"):connect(function() + ControlModules.Touch:RefreshControlStyle() + end) +end +function ControlModules.Touch:Disable() + ControlModules.Touch:DisconnectEvents() + + local newModuleToDisable = getTouchModule() + + if newModuleToDisable == ThumbstickModule or + newModuleToDisable == DPadModule or + newModuleToDisable == ThumbpadModule or + newModuleToDisable == DynamicThumbstickModule then + newModuleToDisable:Disable() + setJumpModule(false) + TouchJumpModule:Disable() + end + + -- UserMovementMode will still have the previous value at this point + if FFlagUserNoCameraClickToMove and UserMovementMode == Enum.ComputerMovementMode.ClickToMove then + ClickToMoveModule:Disable() + end +end + +local function getKeyboardModule() + -- NOTE: Click to move still uses keyboard. Leaving cases in case this ever changes. + local whichModule = nil + if not IsUserChoice then + if DevMovementMode == Enum.DevComputerMovementMode.KeyboardMouse then + whichModule = keyboardModule + elseif DevMovementMode == Enum.DevComputerMovementMode.ClickToMove then + whichModule = keyboardModule + end + else + if UserMovementMode == Enum.ComputerMovementMode.KeyboardMouse or UserMovementMode == Enum.ComputerMovementMode.Default then + whichModule = keyboardModule + elseif UserMovementMode == Enum.ComputerMovementMode.ClickToMove then + whichModule = keyboardModule + end + end + + return whichModule +end + +ControlModules.Keyboard = {} +function ControlModules.Keyboard:RefreshControlStyle() + ControlModules.Keyboard:Disable() + ControlModules.Keyboard:Enable() +end +function ControlModules.Keyboard:Enable() + DevMovementMode = LocalPlayer.DevComputerMovementMode + IsUserChoice = DevMovementMode == Enum.DevComputerMovementMode.UserChoice + if IsUserChoice then + UserMovementMode = GameSettings.ComputerMovementMode + end + + local newModuleToEnable = getKeyboardModule() + if newModuleToEnable then + newModuleToEnable:Enable() + end + + if FFlagUserNoCameraClickToMove and UserMovementMode == Enum.ComputerMovementMode.ClickToMove then + ClickToMoveModule:Enable() + end + + ControlModules.Keyboard:DisconnectEvents() + ControlModules.Keyboard.LocalPlayerChangedCon = LocalPlayer.Changed:connect(function(property) + if property == 'DevComputerMovementMode' then + ControlModules.Keyboard:RefreshControlStyle() + end + end) + + ControlModules.Keyboard.GameSettingsChangedCon = GameSettings.Changed:connect(function(property) + if property == 'ComputerMovementMode' then + ControlModules.Keyboard:RefreshControlStyle() + end + end) +end +function ControlModules.Keyboard:DisconnectEvents() + if ControlModules.Keyboard.LocalPlayerChangedCon then + ControlModules.Keyboard.LocalPlayerChangedCon:disconnect() + ControlModules.Keyboard.LocalPlayerChangedCon = nil + end + if ControlModules.Keyboard.GameSettingsChangedCon then + ControlModules.Keyboard.GameSettingsChangedCon:disconnect() + ControlModules.Keyboard.GameSettingsChangedCon = nil + end +end +function ControlModules.Keyboard:Disable() + ControlModules.Keyboard:DisconnectEvents() + local newModuleToDisable = getKeyboardModule() + if newModuleToDisable then + newModuleToDisable:Disable() + end + + -- UserMovementMode will still be set to previous movement type + if FFlagUserNoCameraClickToMove and UserMovementMode == Enum.ComputerMovementMode.ClickToMove then + ClickToMoveModule:Disable() + end +end + +ControlModules.VRNavigation = {} + +function ControlModules.VRNavigation:Enable() + VRNavigationModule:Enable() +end + +function ControlModules.VRNavigation:Disable() + VRNavigationModule:Disable() +end + +if not FFlagUserNoCameraClickToMove and IsTouchDevice then + BindableEvent_OnFailStateChanged = script.Parent:WaitForChild("OnClickToMoveFailStateChange") +end + +-- not used, but needs to be required +local VehicleController = require(script.MasterControl:WaitForChild('VehicleController')) + + +--[[ Initialization/Setup ]]-- +local function createTouchGuiContainer() + if TouchGui then TouchGui:Destroy() end + + -- Container for all touch device guis + TouchGui = Instance.new('ScreenGui') + TouchGui.Name = "TouchGui" + TouchGui.ResetOnSpawn = false + TouchGui.Parent = PlayerGui + + TouchControlFrame = Instance.new('Frame') + TouchControlFrame.Name = "TouchControlFrame" + TouchControlFrame.Size = UDim2.new(1, 0, 1, 0) + TouchControlFrame.BackgroundTransparency = 1 + TouchControlFrame.Parent = TouchGui + + ThumbstickModule:Create(TouchControlFrame) + DPadModule:Create(TouchControlFrame) + ThumbpadModule:Create(TouchControlFrame) + TouchJumpModule:Create(TouchControlFrame) + DynamicThumbstickModule:Create(TouchControlFrame) +end + +--[[ Settings Changed Connections ]]-- +LocalPlayer.Changed:connect(function(property) + if lastInputType == Enum.UserInputType.Touch and property == 'DevTouchMovementMode' then + ControlState:SwitchTo(ControlModules.Touch) + elseif UserInputService.KeyboardEnabled and property == 'DevComputerMovementMode' then + ControlState:SwitchTo(ControlModules.Keyboard) + end +end) + +GameSettings.Changed:connect(function(property) + if not IsUserChoice then return end + if property == 'TouchMovementMode' or property == 'ComputerMovementMode' then + UserMovementMode = GameSettings[property] + if property == 'TouchMovementMode' then + ControlState:SwitchTo(ControlModules.Touch) + elseif property == 'ComputerMovementMode' then + ControlState:SwitchTo(ControlModules.Keyboard) + end + end +end) + +--[[ Touch Events ]]-- +UserInputService.Changed:connect(function(property) + if property == 'ModalEnabled' then + IsModalEnabled = UserInputService.ModalEnabled + + if lastInputType == Enum.UserInputType.Touch then + if ControlState.Current == ControlModules.Touch and IsModalEnabled then + ControlState:SwitchTo(nil) + elseif ControlState.Current == nil and not IsModalEnabled then + ControlState:SwitchTo(ControlModules.Touch) + end + end + end +end) + +if FFlagUserNoCameraClickToMove then + BindableEvent_OnFailStateChanged = MasterControl:GetClickToMoveFailStateChanged() +end +if BindableEvent_OnFailStateChanged then + BindableEvent_OnFailStateChanged.Event:connect(function(isOn) + if lastInputType == Enum.UserInputType.Touch and ClickToMoveTouchControls then + if isOn then + ControlState:SwitchTo(ClickToMoveTouchControls) + else + ControlState:SwitchTo(nil) + end + end + end) +end + +local switchToInputType = function(newLastInputType) + lastInputType = newLastInputType + + if VRService.VREnabled then + ControlState:SwitchTo(ControlModules.VRNavigation) + return + end + + if lastInputType == Enum.UserInputType.Touch then + ControlState:SwitchTo(ControlModules.Touch) + elseif lastInputType == Enum.UserInputType.Keyboard or + lastInputType == Enum.UserInputType.MouseButton1 or + lastInputType == Enum.UserInputType.MouseButton2 or + lastInputType == Enum.UserInputType.MouseButton3 or + lastInputType == Enum.UserInputType.MouseWheel or + lastInputType == Enum.UserInputType.MouseMovement then + ControlState:SwitchTo(ControlModules.Keyboard) + elseif lastInputType == Enum.UserInputType.Gamepad1 or + lastInputType == Enum.UserInputType.Gamepad2 or + lastInputType == Enum.UserInputType.Gamepad3 or + lastInputType == Enum.UserInputType.Gamepad4 then + ControlState:SwitchTo(ControlModules.Gamepad) + end +end + +if IsTouchDevice then + createTouchGuiContainer() +end + +MasterControl:Init() + +UserInputService.GamepadDisconnected:connect(function(gamepadEnum) + local connectedGamepads = UserInputService:GetConnectedGamepads() + if #connectedGamepads > 0 then return end + + if not VRService.VREnabled then + if UserInputService.KeyboardEnabled then + ControlState:SwitchTo(ControlModules.Keyboard) + elseif IsTouchDevice then + ControlState:SwitchTo(ControlModules.Touch) + end + end +end) + +UserInputService.GamepadConnected:connect(function(gamepadEnum) + if not VRService.VREnabled then + ControlState:SwitchTo(ControlModules.Gamepad) + end +end) + +switchToInputType(UserInputService:GetLastInputType()) +UserInputService.LastInputTypeChanged:connect(switchToInputType) + +VRService:GetPropertyChangedSignal("VREnabled"):Connect(function() + if VRService.VREnabled then + ControlState:SwitchTo(ControlModules.VRNavigation) + end +end) diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl.lua new file mode 100644 index 0000000..d9eaa78 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl.lua @@ -0,0 +1,174 @@ +--[[ + // FileName: MasterControl + // Version 1.0 + // Written by: jeditkacheff + // Description: All character control scripts go thru this script, this script makes sure all actions are performed +--]] + +-- [[ Constants ]]-- +local ZERO_VECTOR3 = Vector3.new(0, 0, 0) +local STATE_JUMPING = Enum.HumanoidStateType.Jumping +local STATE_FREEFALL = Enum.HumanoidStateType.Freefall +local STATE_LANDED = Enum.HumanoidStateType.Landed + +--[[ Local Variables ]]-- +local MasterControl = {} + +local Players = game:GetService('Players') +local RunService = game:GetService('RunService') + +while not Players.LocalPlayer do + Players.PlayerAdded:wait() +end +local LocalPlayer = Players.LocalPlayer +local LocalCharacter = LocalPlayer.Character +local CachedHumanoid = nil + +local isJumping = false +local moveValue = Vector3.new(0, 0, 0) + +local isJumpEnabled = true +local areControlsEnabled = true + +local clickToMoveFailStateChanged = Instance.new("BindableEvent") +clickToMoveFailStateChanged.Name = "ClickToMoveFailStateChanged" + +--[[ Local Functions ]]-- +function MasterControl:GetHumanoid() + if LocalCharacter then + if CachedHumanoid then + return CachedHumanoid + else + CachedHumanoid = LocalCharacter:FindFirstChildOfClass("Humanoid") + return CachedHumanoid + end + end +end + +local characterAncestryChangedConn = nil +local characterChildRemovedConn = nil +local function characterAdded(character) + if characterAncestryChangedConn then + characterAncestryChangedConn:disconnect() + end + + if characterChildRemovedConn then + characterChildRemovedConn:disconnect() + end + + LocalCharacter = character + CachedHumanoid = LocalCharacter:FindFirstChildOfClass("Humanoid") + characterAncestryChangedConn = character.AncestryChanged:connect(function() + if character.Parent == nil then + LocalCharacter = nil + else + LocalCharacter = character + end + end) + + characterChildRemovedConn = character.ChildRemoved:connect(function(child) + if child == CachedHumanoid then + CachedHumanoid = nil + end + end) +end + +if LocalCharacter then + characterAdded(LocalCharacter) +end +LocalPlayer.CharacterAdded:connect(characterAdded) + + +local getHumanoid = MasterControl.GetHumanoid +local moveFunc = LocalPlayer.Move +local updateMovement = function() + + if not areControlsEnabled then return end + + local humanoid = getHumanoid() + if not humanoid then return end + + if isJumpEnabled and isJumping and not humanoid.PlatformStand then + local state = humanoid:GetState() + if state ~= STATE_JUMPING and state ~= STATE_FREEFALL and state ~= STATE_LANDED then + humanoid.Jump = isJumping + end + end + + moveFunc(LocalPlayer, moveValue, true) +end + +--[[ Public API ]]-- +function MasterControl:Init() + RunService:BindToRenderStep("MasterControlStep", Enum.RenderPriority.Input.Value, updateMovement) +end + +function MasterControl:Enable() + areControlsEnabled = true + isJumpEnabled = true + + if self.ControlState.Current then + self.ControlState.Current:Enable() + end +end + +function MasterControl:Disable() + + if self.ControlState.Current then + self.ControlState.Current:Disable() + end + + --After current control state is disabled, moveValue has been set to zero, + --Call updateMovement one last time to make sure this propagates to the engine - + --Otherwise if disabled while humanoid is moving, humanoid won't stop moving. + updateMovement() + + isJumping = false + areControlsEnabled = false +end + +function MasterControl:EnableJump() + isJumpEnabled = true + if areControlsEnabled and self.ControlState:IsTouchJumpModuleUsed() then + self.TouchJumpModule:Enable() + end +end + +function MasterControl:DisableJump() + isJumpEnabled = false + if self.ControlState:IsTouchJumpModuleUsed() then + self.TouchJumpModule:Disable() + end +end + +function MasterControl:AddToPlayerMovement(playerMoveVector) + moveValue = Vector3.new(moveValue.X + playerMoveVector.X, moveValue.Y + playerMoveVector.Y, moveValue.Z + playerMoveVector.Z) +end + +function MasterControl:GetMoveVector() + return moveValue +end + +function MasterControl:SetIsJumping(jumping) + if not isJumpEnabled then return end + isJumping = jumping + local humanoid = self:GetHumanoid() + if humanoid and not humanoid.PlatformStand then + humanoid.Jump = isJumping + end +end + +function MasterControl:DoJump() + if not isJumpEnabled then return end + local humanoid = self:GetHumanoid() + if humanoid then + humanoid.Jump = true + end +end + +function MasterControl:GetClickToMoveFailStateChanged() + return clickToMoveFailStateChanged +end + +return MasterControl + diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/ClickToMoveController.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/ClickToMoveController.lua new file mode 100644 index 0000000..1e2d29c --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/ClickToMoveController.lua @@ -0,0 +1,1164 @@ +-- Written By Kip Turner, Copyright Roblox 2014 +-- Updated by Garnold to utilize the new PathfindingService API, 2017 + +local FFlagUserNavigationFixClickToMoveInterruptionSuccess, FFlagUserNavigationFixClickToMoveInterruptionResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNavigationFixClickToMoveInterruption") end) +local FFlagUserNavigationFixClickToMoveInterruption = FFlagUserNavigationFixClickToMoveInterruptionSuccess and FFlagUserNavigationFixClickToMoveInterruptionResult + +local FFlagUserNavigationFixClickToMoveJumpSuccess, FFlagUserNavigationFixClickToMoveJumpResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNavigationFixClickToMoveJump") end) +local FFlagUserNavigationFixClickToMoveJump = FFlagUserNavigationFixClickToMoveJumpSuccess and FFlagUserNavigationFixClickToMoveJumpResult + +local DEBUG_NAME = "ClickToMoveController" + +local UIS = game:GetService("UserInputService") +local PathfindingService = game:GetService("PathfindingService") +local PlayerService = game:GetService("Players") +local RunService = game:GetService("RunService") +local DebrisService = game:GetService('Debris') +local ReplicatedStorage = game:GetService('ReplicatedStorage') +local TweenService = game:GetService("TweenService") + +local Player = PlayerService.LocalPlayer +local PlayerScripts = Player.PlayerScripts + +local CameraScript = script:FindFirstAncestor("CameraScript") +local InvisicamModule = nil +if CameraScript then + InvisicamModule = require(CameraScript:WaitForChild("Invisicam")) +end + +local MasterControlModule = script.Parent +local MasterControl = require(MasterControlModule) +local TouchJump = nil +if MasterControl then + local TouchJumpModule = MasterControlModule:FindFirstChild("TouchJump") + if TouchJumpModule then + TouchJump = require(TouchJumpModule) + end +end + +local SHOW_PATH = true + +local RayCastIgnoreList = workspace.FindPartOnRayWithIgnoreList + +local math_min = math.min +local math_max = math.max +local math_pi = math.pi +local math_atan2 = math.atan2 + +local Vector3_new = Vector3.new +local Vector2_new = Vector2.new +local CFrame_new = CFrame.new + +local CurrentSeatPart = nil +local DrivingTo = nil + +local XZ_VECTOR3 = Vector3_new(1, 0, 1) +local ZERO_VECTOR3 = Vector3_new(0, 0, 0) +local ZERO_VECTOR2 = Vector2_new(0, 0) + +local BindableEvent_OnFailStateChanged = nil +if UIS.TouchEnabled then + BindableEvent_OnFailStateChanged = MasterControl:GetClickToMoveFailStateChanged() +end + +--------------------------UTIL LIBRARY------------------------------- +local Utility = {} +do + local function ViewSizeX() + local camera = workspace.CurrentCamera + local x = camera and camera.ViewportSize.X or 0 + local y = camera and camera.ViewportSize.Y or 0 + if x == 0 then + return 1024 + else + if x > y then + return x + else + return y + end + end + end + Utility.ViewSizeX = ViewSizeX + + local function ViewSizeY() + local camera = workspace.CurrentCamera + local x = camera and camera.ViewportSize.X or 0 + local y = camera and camera.ViewportSize.Y or 0 + if y == 0 then + return 768 + else + if x > y then + return y + else + return x + end + end + end + Utility.ViewSizeY = ViewSizeY + + local function FindCharacterAncestor(part) + if part then + local humanoid = part:FindFirstChild("Humanoid") + if humanoid then + return part, humanoid + else + return FindCharacterAncestor(part.Parent) + end + end + end + Utility.FindCharacterAncestor = FindCharacterAncestor + + local function Raycast(ray, ignoreNonCollidable, ignoreList) + local ignoreList = ignoreList or {} + local hitPart, hitPos, hitNorm, hitMat = RayCastIgnoreList(workspace, ray, ignoreList) + if hitPart then + if ignoreNonCollidable and hitPart.CanCollide == false then + table.insert(ignoreList, hitPart) + return Raycast(ray, ignoreNonCollidable, ignoreList) + end + return hitPart, hitPos, hitNorm, hitMat + end + return nil, nil + end + Utility.Raycast = Raycast + + local function AveragePoints(positions) + local avgPos = ZERO_VECTOR2 + if #positions > 0 then + for i = 1, #positions do + avgPos = avgPos + positions[i] + end + avgPos = avgPos / #positions + end + return avgPos + end + Utility.AveragePoints = AveragePoints +end + +local humanoidCache = {} +local function findPlayerHumanoid(player) + local character = player and player.Character + if character then + local resultHumanoid = humanoidCache[player] + if resultHumanoid and resultHumanoid.Parent == character then + return resultHumanoid + else + humanoidCache[player] = nil -- Bust Old Cache + local humanoid = character:FindFirstChildOfClass("Humanoid") + if humanoid then + humanoidCache[player] = humanoid + end + return humanoid + end + end +end + +--------------------------------------------------------- + +--------------------------CHARACTER CONTROL------------------------------- +local CurrentIgnoreList + +local function GetCharacter() + return Player and Player.Character +end + +local function GetTorso() + local humanoid = findPlayerHumanoid(Player) + return humanoid and humanoid.RootPart +end + +local function getIgnoreList() + if CurrentIgnoreList then + return CurrentIgnoreList + end + CurrentIgnoreList = {} + table.insert(CurrentIgnoreList, GetCharacter()) + return CurrentIgnoreList +end + +----------------------------------------------------------------------------- + +-----------------------------------PATHER-------------------------------------- + +local popupAdornee +local function getPopupAdorneePart() + --Handle the case of the adornee part getting deleted (camera changed, maybe) + if popupAdornee and not popupAdornee.Parent then + popupAdornee = nil + end + + --If the adornee doesn't exist yet, create it + if not popupAdornee then + popupAdornee = Instance.new("Part") + popupAdornee.Name = "ClickToMovePopupAdornee" + popupAdornee.Transparency = 1 + popupAdornee.CanCollide = false + popupAdornee.Anchored = true + popupAdornee.Size = Vector3.new(2, 2, 2) + popupAdornee.CFrame = CFrame.new() + + popupAdornee.Parent = workspace.CurrentCamera + end + + return popupAdornee +end + +local activePopups = {} +local function createNewPopup(popupType) + local newModel = Instance.new("ImageHandleAdornment") + + newModel.AlwaysOnTop = false + newModel.Transparency = 1 + newModel.Size = ZERO_VECTOR2 + newModel.SizeRelativeOffset = ZERO_VECTOR3 + newModel.Image = "rbxasset://textures/ui/move.png" + newModel.ZIndex = 20 + + local radius = 0 + if popupType == "DestinationPopup" then + newModel.Color3 = Color3.fromRGB(0, 175, 255) + radius = 1.25 + elseif popupType == "DirectWalkPopup" then + newModel.Color3 = Color3.fromRGB(0, 175, 255) + radius = 1.25 + elseif popupType == "FailurePopup" then + newModel.Color3 = Color3.fromRGB(255, 100, 100) + radius = 1.25 + elseif popupType == "PatherPopup" then + newModel.Color3 = Color3.fromRGB(255, 255, 255) + radius = 1 + newModel.ZIndex = 10 + end + newModel.Size = Vector2.new(5, 0.1) * radius + + local dataStructure = {} + dataStructure.Model = newModel + + activePopups[#activePopups + 1] = newModel + + function dataStructure:TweenIn() + local tweenInfo = TweenInfo.new(1.5, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out) + local tween1 = TweenService:Create(newModel, tweenInfo, { Size = Vector2.new(2,2) * radius }) + tween1:Play() + TweenService:Create(newModel, TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, 0, false, 0.1), { Transparency = 0, SizeRelativeOffset = Vector3.new(0, radius * 1.5, 0) }):Play() + return tween1 + end + + function dataStructure:TweenOut() + local tweenInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.In) + local tween1 = TweenService:Create(newModel, tweenInfo, { Size = ZERO_VECTOR2 }) + tween1:Play() + + coroutine.wrap(function() + tween1.Completed:Wait() + + for i = 1, #activePopups do + if activePopups[i] == newModel then + table.remove(activePopups, i) + break + end + end + end)() + return tween1 + end + + function dataStructure:Place(position, dest) + -- place the model at position + if not self.Model.Parent then + local popupAdorneePart = getPopupAdorneePart() + self.Model.Parent = popupAdorneePart + self.Model.Adornee = popupAdorneePart + + --Start the 10-stud long ray 2.5 studs above where the tap happened and point straight down to try to find + --the actual ground position. + local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0)) + local hitPart, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(ray, { workspace.CurrentCamera, Player.Character }) + + self.Model.CFrame = CFrame.new(hitPoint) + Vector3.new(0, -radius,0) + end + end + + return dataStructure +end + +local function createPopupPath(points, numCircles) + -- creates a path with the provided points, using the path and number of circles provided + local popups = {} + local stopTraversing = false + + local function killPopup(i) + -- kill all popups before and at i + for iter, v in pairs(popups) do + if iter <= i then + local tween = v:TweenOut() + spawn(function() + tween.Completed:Wait() + v.Model:Destroy() + end) + popups[iter] = nil + end + end + end + + local function stopFunction() + stopTraversing = true + killPopup(#points) + end + + spawn(function() + for i = 1, #points do + if stopTraversing then + break + end + + local includeWaypoint = i % numCircles == 0 + and i < #points + and (points[#points].Position - points[i].Position).magnitude > 4 + if includeWaypoint then + local popup = createNewPopup("PatherPopup") + popups[i] = popup + local nextPopup = points[i+1] + popup:Place(points[i].Position, nextPopup and nextPopup.Position or points[#points].Position) + local tween = popup:TweenIn() + wait(0.2) + end + end + end) + + return stopFunction, killPopup +end + +local function Pather(character, endPoint, surfaceNormal) + local this = {} + + this.Cancelled = false + this.Started = false + + this.Finished = Instance.new("BindableEvent") + this.PathFailed = Instance.new("BindableEvent") + + this.PathComputing = false + this.PathComputed = false + + this.TargetPoint = endPoint + this.TargetSurfaceNormal = surfaceNormal + + this.DiedConn = nil + this.SeatedConn = nil + this.MoveToConn = nil + this.CurrentPoint = 0 + + function this:Cleanup() + if this.stopTraverseFunc then + this.stopTraverseFunc() + end + + if this.MoveToConn then + this.MoveToConn:Disconnect() + this.MoveToConn = nil + end + + if this.DiedConn then + this.DiedConn:Disconnect() + this.DiedConn = nil + end + + if this.SeatedConn then + this.SeatedConn:Disconnect() + this.SeatedConn = nil + end + + this.humanoid = nil + end + + function this:Cancel() + this.Cancelled = true + this:Cleanup() + end + + function this:OnPathInterrupted() + -- Stop moving + this.Cancelled = true + this:OnPointReached(false) + end + + function this:ComputePath() + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + local success = false + if torso then + if this.PathComputed or this.PathComputing then return end + this.PathComputing = true + success = pcall(function() + this.pathResult = PathfindingService:FindPathAsync(torso.CFrame.p, this.TargetPoint) + end) + this.pointList = this.pathResult and this.pathResult:GetWaypoints() + this.PathComputing = false + this.PathComputed = this.pathResult and this.pathResult.Status == Enum.PathStatus.Success or false + end + return true + end + + function this:IsValidPath() + if not this.pathResult then + this:ComputePath() + end + return this.pathResult.Status == Enum.PathStatus.Success + end + + function this:OnPointReached(reached) + + if reached and not this.Cancelled then + + this.CurrentPoint = this.CurrentPoint + 1 + + if this.CurrentPoint > #this.pointList then + -- End of path reached + if this.stopTraverseFunc then + this.stopTraverseFunc() + end + this.Finished:Fire() + this:Cleanup() + else + -- If next action == Jump, but the humanoid + -- is still jumping from a previous action + -- wait until it gets to the ground + if this.CurrentPoint + 1 <= #this.pointList then + local nextAction = this.pointList[this.CurrentPoint + 1].Action + if nextAction == Enum.PathWaypointAction.Jump then + local currentState = this.humanoid:GetState() + if currentState == Enum.HumanoidStateType.FallingDown or + currentState == Enum.HumanoidStateType.Freefall or + currentState == Enum.HumanoidStateType.Jumping then + + this.humanoid.FreeFalling:Wait() + + -- Give time to the humanoid's state to change + -- Otherwise, the jump flag in Humanoid + -- will be reset by the state change + wait(0.1) + end + end + end + + -- Move to the next point + if this.setPointFunc then + this.setPointFunc(this.CurrentPoint) + end + + local nextWaypoint = this.pointList[this.CurrentPoint] + + if nextWaypoint.Action == Enum.PathWaypointAction.Jump then + this.humanoid.Jump = true + end + this.humanoid:MoveTo(nextWaypoint.Position) + end + else + this.PathFailed:Fire() + this:Cleanup() + end + end + + function this:OnPointReachedFixJump(reached) + + if reached and not this.Cancelled then + + local nextWaypointIdx = this.CurrentPoint + 1 + + if nextWaypointIdx > #this.pointList then + -- End of path reached + if this.stopTraverseFunc then + this.stopTraverseFunc() + end + this.Finished:Fire() + this:Cleanup() + else + local currentWaypoint = this.pointList[this.CurrentPoint] + local nextWaypoint = this.pointList[nextWaypointIdx] + + -- If airborne, only allow to keep moving + -- if nextWaypoint.Action ~= Jump, or path mantains a direction + -- Otherwise, wait until the humanoid gets to the ground + local currentState = this.humanoid:GetState() + local isInAir = currentState == Enum.HumanoidStateType.FallingDown + or currentState == Enum.HumanoidStateType.Freefall + or currentState == Enum.HumanoidStateType.Jumping + + if isInAir then + local shouldWaitForGround = nextWaypoint.Action == Enum.PathWaypointAction.Jump + if not shouldWaitForGround and this.CurrentPoint > 1 then + local prevWaypoint = this.pointList[this.CurrentPoint - 1] + + local prevDir = currentWaypoint.Position - prevWaypoint.Position + local currDir = nextWaypoint.Position - currentWaypoint.Position + + local prevDirXZ = Vector2.new(prevDir.x, prevDir.z).Unit + local currDirXZ = Vector2.new(currDir.x, currDir.z).Unit + + local THRESHOLD_COS = 0.996 -- ~cos(5 degrees) + shouldWaitForGround = prevDirXZ:Dot(currDirXZ) < THRESHOLD_COS + end + + if shouldWaitForGround then + this.humanoid.FreeFalling:Wait() + + -- Give time to the humanoid's state to change + -- Otherwise, the jump flag in Humanoid + -- will be reset by the state change + wait(0.1) + end + end + + -- Move to the next point + if this.setPointFunc then + this.setPointFunc(nextWaypointIdx) + end + + if nextWaypoint.Action == Enum.PathWaypointAction.Jump then + this.humanoid.Jump = true + end + this.humanoid:MoveTo(nextWaypoint.Position) + + this.CurrentPoint = nextWaypointIdx + end + else + this.PathFailed:Fire() + this:Cleanup() + end + end + + function this:Start() + if CurrentSeatPart then + return + end + + this.humanoid = findPlayerHumanoid(Player) + if FFlagUserNavigationFixClickToMoveInterruption and not this.humanoid then + this.PathFailed:Fire() + return + end + + if this.Started then return end + this.Started = true + + if SHOW_PATH then + -- choose whichever one Mike likes best + this.stopTraverseFunc, this.setPointFunc = createPopupPath(this.pointList, 4) + end + + if #this.pointList > 0 then + if FFlagUserNavigationFixClickToMoveInterruption then + this.SeatedConn = this.humanoid.Seated:Connect(function(reached) this:OnPathInterrupted() end) + this.DiedConn = this.humanoid.Died:Connect(function(reached) this:OnPathInterrupted() end) + end + if FFlagUserNavigationFixClickToMoveJump then + this.MoveToConn = this.humanoid.MoveToFinished:Connect(function(reached) this:OnPointReachedFixJump(reached) end) + else + this.MoveToConn = this.humanoid.MoveToFinished:Connect(function(reached) this:OnPointReached(reached) end) + end + this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it. + this:OnPointReached(true) -- Move to first point + else + this.PathFailed:Fire() + if this.stopTraverseFunc then + this.stopTraverseFunc() + end + end + end + + this:ComputePath() + if not this.PathComputed then + -- set the end point towards the camera and raycasted towards the ground in case we hit a wall + local offsetPoint = this.TargetPoint + this.TargetSurfaceNormal*1.5 + local ray = Ray.new(offsetPoint, Vector3_new(0,-1,0)*50) + local newHitPart, newHitPos = RayCastIgnoreList(workspace, ray, getIgnoreList()) + if newHitPart then + this.TargetPoint = newHitPos + end + -- try again + this:ComputePath() + end + + return this +end + +------------------------------------------------------------------------- + +local function IsInBottomLeft(pt) + local joystickHeight = math_min(Utility.ViewSizeY() * 0.33, 250) + local joystickWidth = joystickHeight + return pt.X <= joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight +end + +local function IsInBottomRight(pt) + local joystickHeight = math_min(Utility.ViewSizeY() * 0.33, 250) + local joystickWidth = joystickHeight + return pt.X >= Utility.ViewSizeX() - joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight +end + +local function CheckAlive(character) + local humanoid = findPlayerHumanoid(Player) + return humanoid ~= nil and humanoid.Health > 0 +end + +local function GetEquippedTool(character) + if character ~= nil then + for _, child in pairs(character:GetChildren()) do + if child:IsA('Tool') then + return child + end + end + end +end + +local ExistingPather = nil +local ExistingIndicator = nil +local PathCompleteListener = nil +local PathFailedListener = nil + +local function CleanupPath() + DrivingTo = nil + if ExistingPather then + ExistingPather:Cancel() + end + if PathCompleteListener then + PathCompleteListener:Disconnect() + PathCompleteListener = nil + end + if PathFailedListener then + PathFailedListener:Disconnect() + PathFailedListener = nil + end + if ExistingIndicator then + local obj = ExistingIndicator + local tween = obj:TweenOut() + local tweenCompleteEvent = nil + tweenCompleteEvent = tween.Completed:connect(function() + tweenCompleteEvent:Disconnect() + obj.Model:Destroy() + end) + ExistingIndicator = nil + end +end + +local function getExtentsSize(Parts) + local maxX,maxY,maxZ = -math.huge,-math.huge,-math.huge + local minX,minY,minZ = math.huge,math.huge,math.huge + for i = 1, #Parts do + maxX,maxY,maxZ = math_max(maxX, Parts[i].Position.X), math_max(maxY, Parts[i].Position.Y), math_max(maxZ, Parts[i].Position.Z) + minX,minY,minZ = math_min(minX, Parts[i].Position.X), math_min(minY, Parts[i].Position.Y), math_min(minZ, Parts[i].Position.Z) + end + return Region3.new(Vector3_new(minX, minY, minZ), Vector3_new(maxX, maxY, maxZ)) +end + +local function inExtents(Extents, Position) + if Position.X < (Extents.CFrame.p.X - Extents.Size.X/2) or Position.X > (Extents.CFrame.p.X + Extents.Size.X/2) then + return false + end + if Position.Z < (Extents.CFrame.p.Z - Extents.Size.Z/2) or Position.Z > (Extents.CFrame.p.Z + Extents.Size.Z/2) then + return false + end + --ignoring Y for now + return true +end + +local function showQuickPopupAsync(position, popupType) + local popup = createNewPopup(popupType) + popup:Place(position, Vector3_new(0,position.y,0)) + local tweenIn = popup:TweenIn() + tweenIn.Completed:Wait() + local tweenOut = popup:TweenOut() + tweenOut.Completed:Wait() + popup.Model:Destroy() + popup = nil +end + +local FailCount = 0 +local function OnTap(tapPositions, goToPoint) + -- Good to remember if this is the latest tap event + local camera = workspace.CurrentCamera + local character = Player.Character + + if not CheckAlive(character) then return end + + -- This is a path tap position + if #tapPositions == 1 or goToPoint then + if camera then + local unitRay = camera:ScreenPointToRay(tapPositions[1].x, tapPositions[1].y) + local ray = Ray.new(unitRay.Origin, unitRay.Direction*1000) + + -- inivisicam stuff + local initIgnore = getIgnoreList() + local invisicamParts = InvisicamModule and InvisicamModule:GetObscuredParts() or {} + local ignoreTab = {} + + -- add to the ignore list + for i, v in pairs(invisicamParts) do + ignoreTab[#ignoreTab+1] = i + end + for i = 1, #initIgnore do + ignoreTab[#ignoreTab+1] = initIgnore[i] + end + -- + local myHumanoid = findPlayerHumanoid(Player) + local hitPart, hitPt, hitNormal, hitMat = Utility.Raycast(ray, true, ignoreTab) + + local hitChar, hitHumanoid = Utility.FindCharacterAncestor(hitPart) + local torso = GetTorso() + local startPos = torso.CFrame.p + if goToPoint then + hitPt = goToPoint + hitChar = nil + end + if hitChar and hitHumanoid and hitHumanoid.RootPart and (hitHumanoid.Torso.CFrame.p - torso.CFrame.p).magnitude < 7 then + CleanupPath() + + if myHumanoid then + myHumanoid:MoveTo(hitPt) + end + -- Do shoot + local currentWeapon = GetEquippedTool(character) + if currentWeapon then + currentWeapon:Activate() + LastFired = tick() + end + elseif hitPt and character and not CurrentSeatPart then + local thisPather = Pather(character, hitPt, hitNormal) + if thisPather:IsValidPath() then + FailCount = 0 + + thisPather:Start() + if BindableEvent_OnFailStateChanged then + BindableEvent_OnFailStateChanged:Fire(false) + end + CleanupPath() + + local destinationPopup = createNewPopup("DestinationPopup") + destinationPopup:Place(hitPt, Vector3_new(0,hitPt.y,0)) + local failurePopup = createNewPopup("FailurePopup") + local currentTween = destinationPopup:TweenIn() + + + ExistingPather = thisPather + ExistingIndicator = destinationPopup + + PathCompleteListener = thisPather.Finished.Event:Connect(function() + if destinationPopup then + if ExistingIndicator == destinationPopup then + ExistingIndicator = nil + end + local tween = destinationPopup:TweenOut() + local tweenCompleteEvent = nil + tweenCompleteEvent = tween.Completed:Connect(function() + tweenCompleteEvent:Disconnect() + destinationPopup.Model:Destroy() + destinationPopup = nil + end) + end + if hitChar then + local humanoid = findPlayerHumanoid(Player) + local currentWeapon = GetEquippedTool(character) + if currentWeapon then + currentWeapon:Activate() + LastFired = tick() + end + if humanoid then + humanoid:MoveTo(hitPt) + end + end + end) + PathFailedListener = thisPather.PathFailed.Event:Connect(function() + if FFlagUserNavigationFixClickToMoveInterruption then + CleanupPath() + end + if failurePopup then + failurePopup:Place(hitPt, Vector3_new(0,hitPt.y,0)) + local failTweenIn = failurePopup:TweenIn() + failTweenIn.Completed:Wait() + local failTweenOut = failurePopup:TweenOut() + failTweenOut.Completed:Wait() + failurePopup.Model:Destroy() + failurePopup = nil + end + end) + else + if hitPt then + -- Feedback here for when we don't have a good path + local foundDirectPath = false + if (hitPt-startPos).Magnitude < 25 and (startPos.y-hitPt.y > -3) then + -- move directly here + if myHumanoid then + if myHumanoid.Sit then + myHumanoid.Jump = true + end + myHumanoid:MoveTo(hitPt) + foundDirectPath = true + end + end + + coroutine.wrap(showQuickPopupAsync)(hitPt, foundDirectPath and "DirectWalkPopup" or "FailurePopup") + end + end + elseif hitPt and character and CurrentSeatPart then + local destinationPopup = createNewPopup("DestinationPopup") + ExistingIndicator = destinationPopup + destinationPopup:Place(hitPt, Vector3_new(0,hitPt.y,0)) + destinationPopup:TweenIn() + + DrivingTo = hitPt + local ConnectedParts = CurrentSeatPart:GetConnectedParts(true) + + while wait() do + if CurrentSeatPart and ExistingIndicator == destinationPopup then + local ExtentsSize = getExtentsSize(ConnectedParts) + if inExtents(ExtentsSize, hitPt) then + local popup = destinationPopup + spawn(function() + local tweenOut = popup:TweenOut() + tweenOut.Completed:Wait() + popup.Model:Destroy() + end) + destinationPopup = nil + DrivingTo = nil + break + end + else + if CurrentSeatPart == nil and destinationPopup == ExistingIndicator then + DrivingTo = nil + OnTap(tapPositions, hitPt) + end + local popup = destinationPopup + spawn(function() + local tweenOut = popup:TweenOut() + tweenOut.Completed:Wait() + popup.Model:Destroy() + end) + destinationPopup = nil + break + end + end + end + end + elseif #tapPositions >= 2 then + if camera then + -- Do shoot + local avgPoint = Utility.AveragePoints(tapPositions) + local unitRay = camera:ScreenPointToRay(avgPoint.x, avgPoint.y) + local currentWeapon = GetEquippedTool(character) + if currentWeapon then + currentWeapon:Activate() + LastFired = tick() + end + end + end +end + + +local function CreateClickToMoveModule() + local this = {} + + local LastStateChange = 0 + local LastState = Enum.HumanoidStateType.Running + local FingerTouches = {} + local NumUnsunkTouches = 0 + -- PC simulation + local mouse1Down = tick() + local mouse1DownPos = Vector2_new() + local mouse2Down = tick() + local mouse2DownPos = Vector2_new() + local mouse2Up = tick() + + local movementKeys = { + [Enum.KeyCode.W] = true; + [Enum.KeyCode.A] = true; + [Enum.KeyCode.S] = true; + [Enum.KeyCode.D] = true; + [Enum.KeyCode.Up] = true; + [Enum.KeyCode.Down] = true; + } + + local TapConn = nil + local InputBeganConn = nil + local InputChangedConn = nil + local InputEndedConn = nil + local HumanoidDiedConn = nil + local CharacterChildAddedConn = nil + local OnCharacterAddedConn = nil + local CharacterChildRemovedConn = nil + local RenderSteppedConn = nil + local HumanoidSeatedConn = nil + + local function disconnectEvent(event) + if event then + event:Disconnect() + end + end + + local function DisconnectEvents() + disconnectEvent(TapConn) + disconnectEvent(InputBeganConn) + disconnectEvent(InputChangedConn) + disconnectEvent(InputEndedConn) + disconnectEvent(HumanoidDiedConn) + disconnectEvent(CharacterChildAddedConn) + disconnectEvent(OnCharacterAddedConn) + disconnectEvent(RenderSteppedConn) + disconnectEvent(CharacterChildRemovedConn) + pcall(function() RunService:UnbindFromRenderStep("ClickToMoveRenderUpdate") end) + disconnectEvent(HumanoidSeatedConn) + end + + + + local function IsFinite(num) + return num == num and num ~= 1/0 and num ~= -1/0 + end + + local function findAngleBetweenXZVectors(vec2, vec1) + return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z) + end + + local function OnTouchBegan(input, processed) + if FingerTouches[input] == nil and not processed then + NumUnsunkTouches = NumUnsunkTouches + 1 + end + FingerTouches[input] = processed + end + + local function OnTouchChanged(input, processed) + if FingerTouches[input] == nil then + FingerTouches[input] = processed + if not processed then + NumUnsunkTouches = NumUnsunkTouches + 1 + end + end + end + + local function OnTouchEnded(input, processed) + if FingerTouches[input] ~= nil and FingerTouches[input] == false then + NumUnsunkTouches = NumUnsunkTouches - 1 + end + FingerTouches[input] = nil + end + + + local function OnCharacterAdded(character) + DisconnectEvents() + + InputBeganConn = UIS.InputBegan:Connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchBegan(input, processed) + + -- Give back controls when they tap both sticks + local wasInBottomLeft = IsInBottomLeft(input.Position) + local wasInBottomRight = IsInBottomRight(input.Position) + if wasInBottomRight or wasInBottomLeft then + for otherInput, _ in pairs(FingerTouches) do + if otherInput ~= input then + local otherInputInLeft = IsInBottomLeft(otherInput.Position) + local otherInputInRight = IsInBottomRight(otherInput.Position) + if otherInput.UserInputState ~= Enum.UserInputState.End and ((wasInBottomLeft and otherInputInRight) or (wasInBottomRight and otherInputInLeft)) then + if BindableEvent_OnFailStateChanged then + BindableEvent_OnFailStateChanged:Fire(true) + end + return + end + end + end + end + end + + -- Cancel path when you use the keyboard controls. + if processed == false and input.UserInputType == Enum.UserInputType.Keyboard and movementKeys[input.KeyCode] then + CleanupPath() + end + if input.UserInputType == Enum.UserInputType.MouseButton1 then + mouse1Down = tick() + mouse1DownPos = input.Position + end + if input.UserInputType == Enum.UserInputType.MouseButton2 then + mouse2Down = tick() + mouse2DownPos = input.Position + end + end) + + InputChangedConn = UIS.InputChanged:Connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchChanged(input, processed) + end + end) + + InputEndedConn = UIS.InputEnded:Connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + OnTouchEnded(input, processed) + end + + if input.UserInputType == Enum.UserInputType.MouseButton2 then + mouse2Up = tick() + local currPos = input.Position + if mouse2Up - mouse2Down < 0.25 and (currPos - mouse2DownPos).magnitude < 5 then + local positions = {currPos} + OnTap(positions) + end + end + end) + + TapConn = UIS.TouchTap:Connect(function(touchPositions, processed) + if not processed then + OnTap(touchPositions) + end + end) + + local function computeThrottle(dist) + if dist > .2 then + return 0.5+(dist^2)/2 + else + return 0 + end + end + + local lastSteer = 0 + + --kP = how much the steering corrects for the current error in driving angle + --kD = how much the steering corrects for how quickly the error in driving angle is changing + local kP = 1 + local kD = 0.5 + local function getThrottleAndSteer(object, point) + local throttle, steer = 0, 0 + local oCF = object.CFrame + + local relativePosition = oCF:pointToObjectSpace(point) + local relativeZDirection = -relativePosition.z + local relativeDistance = relativePosition.magnitude + + -- throttle quadratically increases from 0-1 as distance from the selected point goes from 0-50, after 50, throttle is 1. + -- this allows shorter distance travel to have more fine-tuned control. + throttle = computeThrottle(math_min(1,relativeDistance/50))*math.sign(relativeZDirection) + + local steerAngle = -math_atan2(-relativePosition.x, -relativePosition.z) + steer = steerAngle/(math_pi/4) + + local steerDelta = steer - lastSteer + lastSteer = steer + local pdSteer = kP * steer + kD * steer + return throttle, pdSteer + end + + local function Update() + if CurrentSeatPart then + if DrivingTo then + local throttle, steer = getThrottleAndSteer(CurrentSeatPart, DrivingTo) + CurrentSeatPart.ThrottleFloat = throttle + CurrentSeatPart.SteerFloat = steer + else + CurrentSeatPart.ThrottleFloat = 0 + CurrentSeatPart.SteerFloat = 0 + end + end + + local cameraPos = workspace.CurrentCamera.CFrame.p + for i = 1, #activePopups do + local popup = activePopups[i] + popup.CFrame = CFrame.new(popup.CFrame.p, cameraPos) + end + end + + RunService:BindToRenderStep("ClickToMoveRenderUpdate",Enum.RenderPriority.Camera.Value - 1,Update) + + local function onSeated(child, active, currentSeatPart) + if active then + if TouchJump and UIS.TouchEnabled then + TouchJump:Enable() + end + if currentSeatPart and currentSeatPart.ClassName == "VehicleSeat" then + CurrentSeatPart = currentSeatPart + end + else + CurrentSeatPart = nil + if TouchJump and UIS.TouchEnabled then + TouchJump:Disable() + end + end + end + + local function OnCharacterChildAdded(child) + if UIS.TouchEnabled then + if child:IsA('Tool') then + child.ManualActivationOnly = true + end + end + if child:IsA('Humanoid') then + disconnectEvent(HumanoidDiedConn) + HumanoidDiedConn = child.Died:Connect(function() + if ExistingIndicator then + DebrisService:AddItem(ExistingIndicator.Model, 1) + end + end) + HumanoidSeatedConn = child.Seated:Connect(function(active, seat) onSeated(child, active, seat) end) + if child.SeatPart then + onSeated(child, true, child.SeatPart) + end + end + end + + CharacterChildAddedConn = character.ChildAdded:Connect(function(child) + OnCharacterChildAdded(child) + end) + CharacterChildRemovedConn = character.ChildRemoved:Connect(function(child) + if UIS.TouchEnabled then + if child:IsA('Tool') then + child.ManualActivationOnly = false + end + end + end) + for _, child in pairs(character:GetChildren()) do + OnCharacterChildAdded(child) + end + end + + local Running = false + + function this:Disable() + if Running then + DisconnectEvents() + CleanupPath() + -- Restore tool activation on shutdown + if UIS.TouchEnabled then + local character = Player.Character + if character then + for _, child in pairs(character:GetChildren()) do + if child:IsA('Tool') then + child.ManualActivationOnly = false + end + end + end + end + DrivingTo = nil + Running = false + end + end + function this:Stop() + this:Disable() + end + + function this:Enable() + if not Running then + if Player.Character then -- retro-listen + OnCharacterAdded(Player.Character) + end + OnCharacterAddedConn = Player.CharacterAdded:Connect(OnCharacterAdded) + Running = true + end + end + function this:Start() + this:Enable() + end + + function this:GetName() + return DEBUG_NAME + end + + return this +end + +return CreateClickToMoveModule() \ No newline at end of file diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/DPad.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/DPad.lua new file mode 100644 index 0000000..4cf03f2 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/DPad.lua @@ -0,0 +1,170 @@ +--[[ + // FileName: DPad + // Version 1.0 + // Written by: jmargh + // Description: Implements DPad controls for touch devices +--]] + +local Players = game:GetService('Players') +local GuiService = game:GetService('GuiService') + +local DPad = {} + +local MasterControl = require(script.Parent) + +--[[ Script Variables ]]-- +while not Players.LocalPlayer do + wait() +end +local LocalPlayer = Players.LocalPlayer +local DPadFrame = nil +local TouchObject = nil +local OnInputEnded = nil -- defined in Create() + +--[[ Constants ]]-- +local DPAD_SHEET = "rbxasset://textures/ui/DPadSheet.png" +local COMPASS_DIR = { + Vector3.new(1, 0, 0), -- E + Vector3.new(1, 0, 1).unit, -- SE + Vector3.new(0, 0, 1), -- S + Vector3.new(-1, 0, 1).unit, -- SW + Vector3.new(-1, 0, 0), -- W + Vector3.new(-1, 0, -1).unit, -- NW + Vector3.new(0, 0, -1), -- N + Vector3.new(1, 0, -1).unit, -- NE +} + +--[[ lua Function Cache ]]-- +local ATAN2 = math.atan2 +local FLOOR = math.floor +local PI = math.pi + +--[[ Local Functions ]]-- +local function createArrowLabel(name, position, size, rectOffset, rectSize) + local image = Instance.new('ImageLabel') + image.Name = name + image.Image = DPAD_SHEET + image.ImageRectOffset = rectOffset + image.ImageRectSize = rectSize + image.BackgroundTransparency = 1 + image.Size = size + image.Position = position + image.Parent = DPadFrame + + return image +end + +local function getCenterPosition() + return Vector2.new(DPadFrame.AbsolutePosition.x + DPadFrame.AbsoluteSize.x/2, DPadFrame.AbsolutePosition.y + DPadFrame.AbsoluteSize.y/2) +end + +--[[ Public API ]]-- +function DPad:Enable() + DPadFrame.Visible = true +end + +function DPad:Disable() + DPadFrame.Visible = false + OnInputEnded() +end + +function DPad:Create(parentFrame) + if DPadFrame then + DPadFrame:Destroy() + DPadFrame = nil + end + + local position = UDim2.new(0, 10, 1, -230) + DPadFrame = Instance.new('Frame') + DPadFrame.Name = "DPadFrame" + DPadFrame.Active = true + DPadFrame.Visible = false + DPadFrame.Size = UDim2.new(0, 192, 0, 192) + DPadFrame.Position = position + DPadFrame.BackgroundTransparency = 1 + + local smArrowSize = UDim2.new(0, 23, 0, 23) + local lgArrowSize = UDim2.new(0, 64, 0, 64) + local smImgOffset = Vector2.new(46, 46) + local lgImgOffset = Vector2.new(128, 128) + + local bBtn = createArrowLabel("BackButton", UDim2.new(0.5, -32, 1, -64), lgArrowSize, Vector2.new(0, 0), lgImgOffset) + local fBtn = createArrowLabel("ForwardButton", UDim2.new(0.5, -32, 0, 0), lgArrowSize, Vector2.new(0, 258), lgImgOffset) + local lBtn = createArrowLabel("LeftButton", UDim2.new(0, 0, 0.5, -32), lgArrowSize, Vector2.new(129, 129), lgImgOffset) + local rBtn = createArrowLabel("RightButton", UDim2.new(1, -64, 0.5, -32), lgArrowSize, Vector2.new(0, 129), lgImgOffset) + local jumpBtn = createArrowLabel("JumpButton", UDim2.new(0.5, -32, 0.5, -32), lgArrowSize, Vector2.new(129, 0), lgImgOffset) + local flBtn = createArrowLabel("ForwardLeftButton", UDim2.new(0, 35, 0, 35), smArrowSize, Vector2.new(129, 258), smImgOffset) + local frBtn = createArrowLabel("ForwardRightButton", UDim2.new(1, -55, 0, 35), smArrowSize, Vector2.new(176, 258), smImgOffset) + flBtn.Visible = false + frBtn.Visible = false + + -- input connections + jumpBtn.InputBegan:connect(function(inputObject) + MasterControl:DoJump() + end) + + local movementVector = Vector3.new(0,0,0) + local function normalizeDirection(inputPosition) + local jumpRadius = jumpBtn.AbsoluteSize.x/2 + local centerPosition = getCenterPosition() + local direction = Vector2.new(inputPosition.x - centerPosition.x, inputPosition.y - centerPosition.y) + + if direction.magnitude > jumpRadius then + local angle = ATAN2(direction.y, direction.x) + local octant = (FLOOR(8 * angle / (2 * PI) + 8.5)%8) + 1 + movementVector = COMPASS_DIR[octant] + end + + if not flBtn.Visible and movementVector == COMPASS_DIR[7] then + flBtn.Visible = true + frBtn.Visible = true + end + end + + DPadFrame.InputBegan:connect(function(inputObject) + if TouchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch then + return + end + + MasterControl:AddToPlayerMovement(-movementVector) + + TouchObject = inputObject + normalizeDirection(TouchObject.Position) + + MasterControl:AddToPlayerMovement(movementVector) + end) + + DPadFrame.InputChanged:connect(function(inputObject) + if inputObject == TouchObject then + MasterControl:AddToPlayerMovement(-movementVector) + normalizeDirection(TouchObject.Position) + MasterControl:AddToPlayerMovement(movementVector) + MasterControl:SetIsJumping(false) + end + end) + + OnInputEnded = function() + TouchObject = nil + flBtn.Visible = false + frBtn.Visible = false + + MasterControl:AddToPlayerMovement(-movementVector) + movementVector = Vector3.new(0, 0, 0) + end + + DPadFrame.InputEnded:connect(function(inputObject) + if inputObject == TouchObject then + OnInputEnded() + end + end) + + GuiService.MenuOpened:connect(function() + if TouchObject then + OnInputEnded() + end + end) + + DPadFrame.Parent = parentFrame +end + +return DPad diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/DynamicThumbstick.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/DynamicThumbstick.lua new file mode 100644 index 0000000..bd9dbb4 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/DynamicThumbstick.lua @@ -0,0 +1,618 @@ +--[[ + // FileName: DynamicThumbstick + // Version 0.9 + // Written by: jhelms + // Description: Implements dynamic thumbstick controls for touch devices +--]] +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local GuiService = game:GetService("GuiService") +local RunService = game:GetService("RunService") +local TweenService = game:GetService("TweenService") +local Settings = UserSettings() +local GameSettings = Settings.GameSettings + +local MasterControl = require(script.Parent) + +local FFlagUserEnableDynamicThumbstickIntroSuccess, FFlagUserEnableDynamicThumbstickIntroResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserEnableDynamicThumbstickIntro") end) +local FFlagUserEnableDynamicThumbstickIntro = FFlagUserEnableDynamicThumbstickIntroSuccess and FFlagUserEnableDynamicThumbstickIntroResult +local Intro = FFlagUserEnableDynamicThumbstickIntro and require(script:WaitForChild("Intro")) or nil + +local Thumbstick = {} +local Enabled = false + +--[[ Script Variables ]]-- +while not Players.LocalPlayer do + Players.PlayerAdded:wait() +end +local LocalPlayer = Players.LocalPlayer + +local Tools = {} +local ToolEquipped = nil + +local RevertAutoJumpEnabledToFalse = false + +local ThumbstickFrame = nil +local GestureArea = nil +local StartImage = nil +local EndImage = nil +local MiddleImages = {} + +local MoveTouchObject = nil +local IsFollowStick = false +local ThumbstickFrame = nil +local OnMoveTouchEnded = nil -- defined in Create() +local OnTouchMovedCn = nil +local OnTouchEndedCn = nil +local TouchActivateCn = nil +local OnRenderSteppedCn = nil +local currentMoveVector = Vector3.new(0,0,0) +local IsFirstTouch = true + +--[[ Constants ]]-- + +local TOUCH_CONTROLS_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png" + +local MIDDLE_TRANSPARENCIES = { + 1 - 0.89, + 1 - 0.70, + 1 - 0.60, + 1 - 0.50, + 1 - 0.40, + 1 - 0.30, + 1 - 0.25 +} +local NUM_MIDDLE_IMAGES = #MIDDLE_TRANSPARENCIES + +local TOUCH_IS_TAP_TIME_THRESHOLD = 0.5 +local TOUCH_IS_TAP_DISTANCE_THRESHOLD = 25 + +local HasFadedBackgroundInPortrait = false +local HasFadedBackgroundInLandscape = false +local FadeInAndOutBackground = true +local FadeInAndOutMaxAlpha = 0.35 +local TweenInAlphaStart = nil +local TweenOutAlphaStart = nil + +local FADE_IN_OUT_HALF_DURATION_DEFAULT = 0.3 +local FADE_IN_OUT_HALF_DURATION_ORIENTATION_CHANGE = 2 +local FADE_IN_OUT_BALANCE_DEFAULT = 0.5 +local FadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT +local FadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT + +local ThumbstickFadeTweenInfo = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + +--[[ Local functionality ]]-- + +local function isDynamicThumbstickEnabled() + return ThumbstickFrame and ThumbstickFrame.Visible +end + +local function enableAutoJump(humanoid) + if humanoid and isDynamicThumbstickEnabled() then + local shouldRevert = humanoid.AutoJumpEnabled == false + shouldRevert = shouldRevert and LocalPlayer.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice + RevertAutoJumpEnabledToFalse = shouldRevert + humanoid.AutoJumpEnabled = true + end +end + +do + local function onCharacterAdded(character) + for _, child in ipairs(LocalPlayer.Character:GetChildren()) do + if child:IsA("Tool") then + ToolEquipped = child + end + end + character.ChildAdded:Connect(function(child) + if child:IsA("Tool") then + ToolEquipped = child + elseif child:IsA("Humanoid") then + enableAutoJump(child) + end + end) + character.ChildRemoved:Connect(function(child) + if child:IsA("Tool") then + if child == ToolEquipped then + ToolEquipped = nil + end + end + end) + + local humanoid = character:FindFirstChildOfClass("Humanoid") + if humanoid then + enableAutoJump(humanoid) + end + end + LocalPlayer.CharacterAdded:Connect(onCharacterAdded) + if LocalPlayer.Character then + onCharacterAdded(LocalPlayer.Character) + end +end + + + +--[[ Public API ]]-- +function Thumbstick:Enable() + Enabled = true + ThumbstickFrame.Visible = true + local humanoid = MasterControl:GetHumanoid() + enableAutoJump(humanoid) + + if FFlagUserEnableDynamicThumbstickIntro and Intro then + coroutine.wrap(function() + --TODO: Remove this pcall when API is stable + local success, shouldShowIntro = pcall(function() + local wasIntroShown = GameSettings:GetOnboardingCompleted("DynamicThumbstick") + return not wasIntroShown + end) + if success and not shouldShowIntro then return end + + --Give the game some time to initialize + wait(1) + --Wait to play the intro until the character can move + while true do + if not humanoid then + humanoid = MasterControl:GetHumanoid() + end + if humanoid and humanoid.WalkSpeed ~= 0 and not humanoid.Torso.Anchored then + break + else + wait() + end + end + Intro.play() + end)() + end +end + +function Thumbstick:Disable() + Enabled = false + if RevertAutoJumpEnabledToFalse then + local humanoid = MasterControl:GetHumanoid() + if humanoid then + humanoid.AutoJumpEnabled = false + end + end + ThumbstickFrame.Visible = false + OnMoveTouchEnded() +end + +function Thumbstick:GetInputObject() + return MoveTouchObject +end + +function Thumbstick:Create(parentFrame) + if ThumbstickFrame then + ThumbstickFrame:Destroy() + ThumbstickFrame = nil + if OnTouchMovedCn then + OnTouchMovedCn:disconnect() + OnTouchMovedCn = nil + end + if OnTouchEndedCn then + OnTouchEndedCn:disconnect() + OnTouchEndedCn = nil + end + if OnRenderSteppedCn then + OnRenderSteppedCn:disconnect() + OnRenderSteppedCn = nil + end + if TouchActivateCn then + TouchActivateCn:disconnect() + TouchActivateCn = nil + end + end + + local ThumbstickSize = 45 + local ThumbstickRingSize = 20 + local MiddleSize = 10 + local MiddleSpacing = MiddleSize + 4 + local RadiusOfDeadZone = 2 + local RadiusOfMaxSpeed = 50 + + local screenSize = parentFrame.AbsoluteSize + local isBigScreen = math.min(screenSize.x, screenSize.y) > 500 + if isBigScreen then + ThumbstickSize = ThumbstickSize * 2 + ThumbstickRingSize = ThumbstickRingSize * 2 + MiddleSize = MiddleSize * 2 + MiddleSpacing = MiddleSpacing * 2 + RadiusOfDeadZone = RadiusOfDeadZone * 2 + RadiusOfMaxSpeed = RadiusOfMaxSpeed * 2 + end + + local color = Color3.fromRGB(255, 255, 255) + + local function layoutThumbstickFrame(portraitMode) + if portraitMode then + ThumbstickFrame.Size = UDim2.new(1, 0, 0.4, 0) + ThumbstickFrame.Position = UDim2.new(0, 0, 0.6, 0) + GestureArea.Size = UDim2.new(1, 0, 0.6, 0) + GestureArea.Position = UDim2.new(0, 0, 0, 0) + else + ThumbstickFrame.Size = UDim2.new(0.4, 0, 2/3, 0) + ThumbstickFrame.Position = UDim2.new(0, 0, 1/3, 0) + GestureArea.Size = UDim2.new(1, 0, 1, 0) + GestureArea.Position = UDim2.new(0, 0, 0, 0) + end + end + + GestureArea = Instance.new("Frame") + GestureArea.Name = "GestureArea" + GestureArea.Active = false + GestureArea.Visible = true + GestureArea.BackgroundTransparency = 1 + GestureArea.BackgroundColor3 = Color3.fromRGB(0, 0, 0) + + ThumbstickFrame = Instance.new('Frame') + ThumbstickFrame.Name = "DynamicThumbstickFrame" + ThumbstickFrame.Active = false + ThumbstickFrame.Visible = false + ThumbstickFrame.BackgroundTransparency = 1.0 + ThumbstickFrame.BackgroundColor3 = Color3.fromRGB(0, 0, 0) + layoutThumbstickFrame() + + StartImage = Instance.new("ImageLabel") + StartImage.Name = "ThumbstickStart" + StartImage.Visible = true + StartImage.BackgroundTransparency = 1 + + StartImage.Image = TOUCH_CONTROLS_SHEET + StartImage.ImageRectOffset = Vector2.new(1,1) + StartImage.ImageRectSize = Vector2.new(144, 144) + StartImage.ImageColor3 = Color3.new(0, 0, 0) + StartImage.AnchorPoint = Vector2.new(0.5, 0.5) + StartImage.Position = UDim2.new(0, ThumbstickRingSize * 3.3, 1, -ThumbstickRingSize * 2.8) + StartImage.Size = UDim2.new(0, ThumbstickRingSize * 3.7, 0, ThumbstickRingSize * 3.7) + StartImage.ZIndex = 10 + StartImage.Parent = ThumbstickFrame + + EndImage = Instance.new("ImageLabel") + EndImage.Name = "ThumbstickEnd" + EndImage.Visible = true + EndImage.BackgroundTransparency = 1 + EndImage.Image = TOUCH_CONTROLS_SHEET + EndImage.ImageRectOffset = Vector2.new(1,1) + EndImage.ImageRectSize = Vector2.new(144, 144) + EndImage.AnchorPoint = Vector2.new(0.5, 0.5) + EndImage.Position = StartImage.Position + EndImage.Size = UDim2.new(0, ThumbstickSize * 0.8, 0, ThumbstickSize * 0.8) + EndImage.ZIndex = 10 + EndImage.Parent = ThumbstickFrame + + for i = 1, NUM_MIDDLE_IMAGES do + MiddleImages[i] = Instance.new("ImageLabel") + MiddleImages[i].Name = "ThumbstickMiddle" + MiddleImages[i].Visible = false + MiddleImages[i].BackgroundTransparency = 1 + MiddleImages[i].Image = TOUCH_CONTROLS_SHEET + MiddleImages[i].ImageRectOffset = Vector2.new(1,1) + MiddleImages[i].ImageRectSize = Vector2.new(144, 144) + MiddleImages[i].ImageTransparency = MIDDLE_TRANSPARENCIES[i] + MiddleImages[i].AnchorPoint = Vector2.new(0.5, 0.5) + MiddleImages[i].ZIndex = 9 + MiddleImages[i].Parent = ThumbstickFrame + end + + if FFlagUserEnableDynamicThumbstickIntro and Intro then + Intro.setup(isBigScreen, parentFrame, GestureArea, ThumbstickFrame, StartImage, EndImage, MiddleImages) + end + + local CameraChangedConn = nil + local function onCurrentCameraChanged() + if CameraChangedConn then + CameraChangedConn:Disconnect() + CameraChangedConn = nil + end + local newCamera = workspace.CurrentCamera + if newCamera then + local function onViewportSizeChanged() + local size = newCamera.ViewportSize + local portraitMode = size.X < size.Y + layoutThumbstickFrame(portraitMode) + + if FFlagUserEnableDynamicThumbstickIntro then + Intro.setPortraitMode(portraitMode) + end + end + CameraChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(onViewportSizeChanged) + onViewportSizeChanged() + end + end + workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(onCurrentCameraChanged) + if workspace.CurrentCamera then + onCurrentCameraChanged() + end + + MoveTouchObject = nil + local MoveTouchStartTime = nil + local MoveTouchStartPosition = nil + + local startImageFadeTween, endImageFadeTween, middleImageFadeTweens = nil, nil, {} + local function fadeThumbstick(visible) + if not visible and MoveTouchObject then + return + end + if IsFirstTouch then return end + + if startImageFadeTween then + startImageFadeTween:Cancel() + end + if endImageFadeTween then + endImageFadeTween:Cancel() + end + for i = 1, #MiddleImages do + if middleImageFadeTweens[i] then + middleImageFadeTweens[i]:Cancel() + end + end + + if visible then + startImageFadeTween = TweenService:Create(StartImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0 }) + startImageFadeTween:Play() + + endImageFadeTween = TweenService:Create(EndImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0.2 }) + endImageFadeTween:Play() + + for i = 1, #MiddleImages do + middleImageFadeTweens[i] = TweenService:Create(MiddleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = MIDDLE_TRANSPARENCIES[i] }) + middleImageFadeTweens[i]:Play() + end + else + startImageFadeTween = TweenService:Create(StartImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 }) + startImageFadeTween:Play() + + endImageFadeTween = TweenService:Create(EndImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 }) + endImageFadeTween:Play() + + for i = 1, #MiddleImages do + middleImageFadeTweens[i] = TweenService:Create(MiddleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = 1 }) + middleImageFadeTweens[i]:Play() + end + end + end + local function fadeThumbstickFrame(fadeDuration, fadeRatio) + FadeInAndOutHalfDuration = fadeDuration * 0.5 + FadeInAndOutBalance = fadeRatio + TweenInAlphaStart = tick() + end + + if FFlagUserEnableDynamicThumbstickIntro and Intro then + Intro.fadeThumbstick = fadeThumbstick + Intro.fadeThumbstickFrame = fadeThumbstickFrame + end + + local function doMove(direction) + MasterControl:AddToPlayerMovement(-currentMoveVector) + + currentMoveVector = direction + + -- Scaled Radial Dead Zone + local inputAxisMagnitude = currentMoveVector.magnitude + if inputAxisMagnitude < RadiusOfDeadZone then + currentMoveVector = Vector3.new() + else + currentMoveVector = currentMoveVector.unit*(1 - math.max(0, (RadiusOfMaxSpeed - currentMoveVector.magnitude)/RadiusOfMaxSpeed)) + currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y) + end + + MasterControl:AddToPlayerMovement(currentMoveVector) + end + + local function layoutMiddleImages(startPos, endPos) + local startDist = (ThumbstickSize / 2) + MiddleSize + local vector = endPos - startPos + local distAvailable = vector.magnitude - (ThumbstickRingSize / 2) - MiddleSize + local direction = vector.unit + + local distNeeded = MiddleSpacing * NUM_MIDDLE_IMAGES + local spacing = MiddleSpacing + + if distNeeded < distAvailable then + spacing = distAvailable / NUM_MIDDLE_IMAGES + end + + for i = 1, NUM_MIDDLE_IMAGES do + local image = MiddleImages[i] + local distWithout = startDist + (spacing * (i - 2)) + local currentDist = startDist + (spacing * (i - 1)) + + if distWithout < distAvailable then + local pos = endPos - direction * currentDist + local exposedFraction = math.clamp(1 - ((currentDist - distAvailable) / spacing), 0, 1) + + image.Visible = true + image.Position = UDim2.new(0, pos.X, 0, pos.Y) + image.Size = UDim2.new(0, MiddleSize * exposedFraction, 0, MiddleSize * exposedFraction) + else + image.Visible = false + end + end + end + + local function moveStick(pos) + local startPos = Vector2.new(MoveTouchStartPosition.X, MoveTouchStartPosition.Y) - ThumbstickFrame.AbsolutePosition + local endPos = Vector2.new(pos.X, pos.Y) - ThumbstickFrame.AbsolutePosition + local relativePosition = endPos - startPos + local length = relativePosition.magnitude + local maxLength = ThumbstickFrame.AbsoluteSize.X + + length = math.min(length, maxLength) + relativePosition = relativePosition*length + + EndImage.Position = UDim2.new(0, endPos.X, 0, endPos.Y) + + layoutMiddleImages(startPos, endPos) + end + + -- input connections + ThumbstickFrame.InputBegan:connect(function(inputObject) + if inputObject.UserInputType ~= Enum.UserInputType.Touch or inputObject.UserInputState ~= Enum.UserInputState.Begin then + return + end + if MoveTouchObject then + return + end + + if IsFirstTouch then + IsFirstTouch = false + local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out,0,false,0) + TweenService:Create(StartImage, tweenInfo, {Size = UDim2.new(0, 0, 0, 0)}):Play() + TweenService:Create(EndImage, tweenInfo, {Size = UDim2.new(0, ThumbstickSize, 0, ThumbstickSize), ImageColor3 = Color3.new(0,0,0)}):Play() + end + + if FFlagUserEnableDynamicThumbstickIntro and Intro then + Intro.onThumbstickMoveBegin() + end + + MoveTouchObject = inputObject + MoveTouchStartTime = tick() + MoveTouchStartPosition = inputObject.Position + local startPosVec2 = Vector2.new(inputObject.Position.X - ThumbstickFrame.AbsolutePosition.X, inputObject.Position.Y - ThumbstickFrame.AbsolutePosition.Y) + + StartImage.Visible = true + StartImage.Position = UDim2.new(0, startPosVec2.X, 0, startPosVec2.Y) + EndImage.Visible = true + EndImage.Position = StartImage.Position + + fadeThumbstick(true) + moveStick(inputObject.Position) + + if FadeInAndOutBackground then + local playerGui = LocalPlayer:FindFirstChildOfClass("PlayerGui") + local hasFadedBackgroundInOrientation = false + + -- only fade in/out the background once per orientation + if playerGui then + if playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or + playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight then + hasFadedBackgroundInOrientation = HasFadedBackgroundInLandscape + HasFadedBackgroundInLandscape = true + elseif playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait then + hasFadedBackgroundInOrientation = HasFadedBackgroundInPortrait + HasFadedBackgroundInPortrait = true + end + end + + if not hasFadedBackgroundInOrientation then + FadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT + FadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT + TweenInAlphaStart = tick() + end + end + end) + + OnTouchMovedCn = UserInputService.TouchMoved:connect(function(inputObject, isProcessed) + if inputObject == MoveTouchObject then + + local direction = Vector2.new(inputObject.Position.x - MoveTouchStartPosition.x, inputObject.Position.y - MoveTouchStartPosition.y) + if math.abs(direction.x) > 0 or math.abs(direction.y) > 0 then + doMove(direction) + moveStick(inputObject.Position) + end + end + end) + + OnMoveTouchEnded = function(inputObject) + if inputObject then + local direction = Vector2.new(inputObject.Position.x - MoveTouchStartPosition.x, inputObject.Position.y - MoveTouchStartPosition.y) + if FFlagUserEnableDynamicThumbstickIntro and Intro then + coroutine.wrap(function() Intro.onThumbstickMoved(direction.magnitude) end)() + end + end + + MoveTouchObject = nil + fadeThumbstick(false) + + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + end + + OnRenderSteppedCn = RunService.RenderStepped:Connect(function(step) + if TweenInAlphaStart ~= nil then + local delta = tick() - TweenInAlphaStart + local fadeInTime = (FadeInAndOutHalfDuration * 2 * FadeInAndOutBalance) + ThumbstickFrame.BackgroundTransparency = 1 - FadeInAndOutMaxAlpha*math.min(delta/fadeInTime, 1) + if delta > fadeInTime then + TweenOutAlphaStart = tick() + TweenInAlphaStart = nil + end + elseif TweenOutAlphaStart ~= nil then + local delta = tick() - TweenOutAlphaStart + local fadeOutTime = (FadeInAndOutHalfDuration * 2) - (FadeInAndOutHalfDuration * 2 * FadeInAndOutBalance) + ThumbstickFrame.BackgroundTransparency = 1 - FadeInAndOutMaxAlpha + FadeInAndOutMaxAlpha*math.min(delta/fadeOutTime, 1) + if delta > fadeOutTime then + TweenOutAlphaStart = nil + end + end + + if FFlagUserEnableDynamicThumbstickIntro and Intro then + if Intro.currentState == Intro.states.MoveThumbstick then + local startPos = StartImage.AbsolutePosition - ThumbstickFrame.AbsolutePosition + (StartImage.AbsoluteSize * 0.5) + local endPos = EndImage.AbsolutePosition - ThumbstickFrame.AbsolutePosition + (EndImage.AbsoluteSize * 0.5) + + layoutMiddleImages(startPos, endPos) + end + end + end) + + OnTouchEndedCn = UserInputService.TouchEnded:connect(function(inputObject, isProcessed) + if inputObject == MoveTouchObject then + OnMoveTouchEnded(inputObject) + end + end) + + + GuiService.MenuOpened:connect(function() + if MoveTouchObject then + OnMoveTouchEnded(nil) + end + end) + + local playerGui = LocalPlayer:FindFirstChildOfClass("PlayerGui") + while not playerGui do + LocalPlayer.ChildAdded:wait() + playerGui = LocalPlayer:FindFirstChildOfClass("PlayerGui") + end + + local playerGuiChangedConn = nil + local originalScreenOrientationWasLandscape = playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or + playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight + + local function longShowBackground() + FadeInAndOutHalfDuration = 2.5 + FadeInAndOutBalance = 0.05 + TweenInAlphaStart = tick() + end + + playerGuiChangedConn = playerGui.Changed:connect(function(prop) + if prop == "CurrentScreenOrientation" then + if (originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait) or + (not originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation ~= Enum.ScreenOrientation.Portrait) then + playerGuiChangedConn:disconnect() + longShowBackground() + + if originalScreenOrientationWasLandscape then + HasFadedBackgroundInPortrait = true + else + HasFadedBackgroundInLandscape = true + end + end + end + end) + + GestureArea.Parent = parentFrame.Parent + ThumbstickFrame.Parent = parentFrame + + spawn(function() + if game:IsLoaded() then + longShowBackground() + else + game.Loaded:wait() + longShowBackground() + end + end) +end + +return Thumbstick diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/DynamicThumbstick/Intro.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/DynamicThumbstick/Intro.lua new file mode 100644 index 0000000..a2b1d67 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/DynamicThumbstick/Intro.lua @@ -0,0 +1,522 @@ +local TweenService = game:GetService("TweenService") +local RunService = game:GetService("RunService") +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local Settings = UserSettings() +local GameSettings = Settings.GameSettings + +local LocalPlayer = Players.LocalPlayer + +local IMAGE_INTRO_MOVE = "rbxasset://textures/ui/Input/IntroMove.png" +local IMAGE_INTRO_CAMERA = "rbxasset://textures/ui/Input/IntroCamera.png" +local IMAGE_INTRO_CAMERA_PINCH = "rbxasset://textures/ui/Input/IntroCameraPinch.png" +local IMAGE_DASHED_LINE = "rbxasset://textures/ui/Input/DashedLine.png" +local IMAGE_DASHED_LINE_90 = "rbxasset://textures/ui/Input/DashedLine90.png" +local MIDDLE_TRANSPARENCIES = { + 1 - 0.69, + 1 - 0.50, + 1 - 0.40, + 1 - 0.30, + 1 - 0.20, + 1 - 0.10, + 1 - 0.05 +} +local MOVE_CAMERA_THRESHOLD = 50 + +local ParentFrame = nil +local GestureArea = nil +local ThumbstickFrame = nil +local StartImage, EndImage, MiddleImages = nil, nil, {} +local IntroMoveImage = nil +local IntroJumpImage = nil +local IntroJumpImageTap = nil +local IntroCameraImage = nil +local IntroCornerAnim = nil +local IntroDashedLineTop, IntroDashedLineSide = nil +local GenericFadeTweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + +local IntroMoveSpriteSize = 128 +local IntroMoveImageSize = 128 + +local noThumbstickFrameFade = true + +local function create(className) + return function(props) + local instance = Instance.new(className) + for key, val in pairs(props) do + if typeof(key) == "string" then + if key ~= "Parent" then + instance[key] = val + end + else + if typeof(val) == "Instance" then + val.Parent = instance + end + end + end + instance.Parent = props.Parent + return instance + end +end + +local Intro = { + running = false, + portraitMode = false, + + fadeThumbstick = nil, + fadeThumbstickFrame = nil, + + didMoveThumbstick = false, + didJump = false, + didMoveCamera = false, + + currentState = nil, + states = {}, +} + +function Intro.setup(isBigScreen, parentFrame, gestureArea, thumbstickFrame, startImage, endImage, middleImages) + ParentFrame = parentFrame + GestureArea = gestureArea + ThumbstickFrame = thumbstickFrame + StartImage = startImage + EndImage = endImage + MiddleImages = middleImages + + if isBigScreen then + IntroMoveImageSize = IntroMoveImageSize * 2 + end + + IntroMoveImage = create("ImageLabel") { + Name = "IntroMoveAnimation", + BackgroundTransparency = 1, + Image = IMAGE_INTRO_MOVE, + AnchorPoint = Vector2.new(102/256, 55/256), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, IntroMoveImageSize, 0, IntroMoveImageSize), + ImageRectOffset = Vector2.new(IntroMoveSpriteSize, IntroMoveSpriteSize), + ImageRectSize = Vector2.new(IntroMoveSpriteSize, IntroMoveSpriteSize), + ZIndex = 10, + Visible = false, + Parent = EndImage + } + + IntroJumpImage = create("ImageLabel") { + Name = "IntroJumpAnimation", + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(102/256, 55/256), + Position = UDim2.new(0.75, 0, 0.25, 0), + Image = IMAGE_INTRO_MOVE, + ImageRectOffset = Vector2.new(0, 0), + ImageRectSize = Vector2.new(IntroMoveSpriteSize, IntroMoveSpriteSize), + ZIndex = 10, + Visible = false, + Parent = ThumbstickFrame + } + + IntroJumpImageTap = create("ImageLabel") { + Name = "IntroJumpTapAnimation", + BackgroundTransparency = 1, + Image = IMAGE_INTRO_MOVE, + Position = UDim2.new(0, 0, 0, 0), + AnchorPoint = Vector2.new(0, 0), + Size = UDim2.new(1, 0, 1, 0), + ImageTransparency = 1, + ImageRectOffset = Vector2.new(IntroMoveSpriteSize, 0), + ImageRectSize = Vector2.new(IntroMoveSpriteSize, IntroMoveSpriteSize), + ZIndex = 10, + Visible = false, + Parent = IntroJumpImage + } + + IntroCameraImage = create("ImageLabel") { + Name = "IntroCameraDragAnimation", + BackgroundTransparency = 1, + Image = IMAGE_INTRO_MOVE, + ImageRectOffset = Vector2.new(0, IntroMoveSpriteSize), + ImageRectSize = Vector2.new(IntroMoveSpriteSize, IntroMoveSpriteSize), + Position = UDim2.new(0.75, 0, -0.6, 0), + Size = UDim2.new(0, IntroMoveImageSize, 0, IntroMoveImageSize), + AnchorPoint = Vector2.new(0.5, 0), + ZIndex = 10, + Visible = false, + Parent = GestureArea + } + + IntroPinchImage = create("ImageLabel") { + Name = "IntroCameraPinchAnimation", + BackgroundTransparency = 1, + Image = IMAGE_INTRO_CAMERA_PINCH, + ImageRectOffset = Vector2.new(0, 0), + ImageRectSize = Vector2.new(IntroMoveSpriteSize, IntroMoveSpriteSize), + Position = UDim2.new(0.95, 0, 0.15, 0), + Size = UDim2.new(0, IntroMoveImageSize, 0, IntroMoveImageSize), + AnchorPoint = Vector2.new(1, 0), + Rotation = 50, + ZIndex = 10, + Visible = false, + Parent = GestureArea + } + + IntroDashedLineTop = create("ImageLabel") { + Name = "DashedLine", + BackgroundTransparency = 1, + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, -5, 0, 10), + AnchorPoint = Vector2.new(0, 0.5), + Image = IMAGE_DASHED_LINE, + ImageTransparency = 0.5, + ScaleType = Enum.ScaleType.Tile, + TileSize = UDim2.new(0, 64, 0, 10), + Visible = false, + Parent = ThumbstickFrame + } + + IntroDashedLineSide = IntroDashedLineTop:Clone() + IntroDashedLineSide.Image = IMAGE_DASHED_LINE_90 + IntroDashedLineSide.Position = UDim2.new(1, 0, 0, -5) + IntroDashedLineSide.AnchorPoint = Vector2.new(0.5, 0) + IntroDashedLineSide.Size = UDim2.new(0, 10, 1, 0) + IntroDashedLineSide.TileSize = UDim2.new(0, 10, 0, 64) + IntroDashedLineSide.Parent = ThumbstickFrame +end + +function Intro.setPortraitMode(portraitMode) + if not IntroDashedLineSide or not Intro.running then return end + Intro.portraitMode = portraitMode + IntroDashedLineSide.Visible = not portraitMode +end + +function Intro.addState(stateName) + local state = {} + state.counter = 0 + + Intro.states[stateName] = state + return state +end + +function Intro.onCompleted() + --TODO: Remove this pcall when API is stable + pcall(function() GameSettings:SetOnboardingCompleted("DynamicThumbstick") end) + local fadeOutTop = TweenService:Create(IntroDashedLineTop, GenericFadeTweenInfo, { ImageTransparency = 1 }) + local fadeOutSide = TweenService:Create(IntroDashedLineSide, GenericFadeTweenInfo, { ImageTransparency = 1}) + fadeOutTop:Play() + fadeOutSide:Play() + fadeOutTop.Completed:wait() + IntroDashedLineTop.Visible = false + IntroDashedLineSide.Visible = false + + Intro.running = false +end + +function Intro.setState(newState) + if Intro.currentState == newState then return end + if Intro.currentState then + coroutine.wrap(function() Intro.currentState:stop() end)() + end + Intro.currentState = newState + Intro.currentState.counter = Intro.currentState.counter + 1 + Intro.currentState:start() +end + +function Intro.repeatState() + if not Intro.currentState then return end + coroutine.wrap(function() Intro.currentState:stop() end)() + Intro.currentState.counter = Intro.currentState.counter + 1 + Intro.currentState:start() +end + +function Intro.pause() + if not Intro.currentState then return end + Intro.currentState:stop() + Intro.currentState = nil +end + +function Intro.onThumbstickMoveBegin() + if Intro.currentState == Intro.states.MoveThumbstick then + Intro.states.MoveThumbstick.startedMoving = true + local moveTween = Intro.states.MoveThumbstick.moveTween + if moveTween then + moveTween:Cancel() + end + end +end + +function Intro.onThumbstickMoved(dist) + if Intro.currentState == Intro.states.MoveThumbstick then + if dist > 100 then + Intro.pause() + spawn(function() + wait(0.5) + while StartImage.ImageTransparency ~= 1 do + wait() + end + if Intro.states.MoveThumbstick.counter == 2 then + Intro.setState(Intro.states.MoveCamera) + else + Intro.setState(Intro.states.MoveThumbstick) + end + end) + else + Intro.states.MoveThumbstick.startedMoving = false + end + end +end + +function Intro.onJumped() + if Intro.currentState == Intro.states.Jump then + Intro.pause() + spawn(function() + wait(1) + if Intro.states.Jump.counter > 1 then + Intro.setState(Intro.states.MoveCamera) + else + Intro.setState(Intro.states.Jump) + end + end) + end +end + +function Intro.onCameraMoved() + if Intro.currentState == Intro.states.MoveCamera then + wait(1) + Intro.setState(Intro.states.ZoomCamera) + end +end + +function Intro.onCameraZoomed() + if Intro.currentState == Intro.states.ZoomCamera then + Intro.pause() + end +end + +function Intro.play() + for _, state in pairs(Intro.states) do + state.counter = 0 + end + Intro.running = true + Intro.currentState = nil + + local portraitMode = false + local currentCamera = workspace.CurrentCamera + if currentCamera then + portraitMode = currentCamera.ViewportSize.X < currentCamera.ViewportSize.Y + end + + Intro.setPortraitMode(portraitMode) + + Intro.setState(Intro.states.MoveThumbstick) +end + +Intro.addState("MoveThumbstick") do + function Intro.states.MoveThumbstick:start() + StartImage.Visible = true + EndImage.Visible = true + IntroMoveImage.Visible = true + + IntroDashedLineTop.Visible = true + + if not Intro.portraitMode then + IntroDashedLineSide.Visible = true + end + + self.startedMoving = false + + StartImage.Position = UDim2.new(0.5, 0, 0.6, 0) + EndImage.Position = UDim2.new(0.5, 0, 0.6, 0) + + local moveTweenTarget = UDim2.new(0.3, 0, -0.25, 0) + if Intro.states.MoveThumbstick.counter % 2 ~= 0 then + moveTweenTarget = UDim2.new(0.7, 0, -0.25, 0) + end + + local moveTweenInfo = TweenInfo.new(1.5, Enum.EasingStyle.Quart, Enum.EasingDirection.InOut) + self.moveTween = TweenService:Create(EndImage, moveTweenInfo, { Position = moveTweenTarget }) + + local moveFadeInfo = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + local fadeOutTween = TweenService:Create(IntroMoveImage, moveFadeInfo, { ImageTransparency = 1 }) + local fadeInTween = TweenService:Create(IntroMoveImage, moveFadeInfo, { ImageTransparency = 0 }) + + self.moveAnimActive = true + coroutine.wrap(function() + while Intro.currentState == self do + if not self.startedMoving then + if not noThumbstickFrameFade then + Intro.fadeThumbstickFrame(moveTweenInfo.Time, 0.05) + end + + StartImage.Position = UDim2.new(0.5, 0, 0.6, 0) + EndImage.Position = UDim2.new(0.5, 0, 0.6, 0) + + self.moveTween:Play() + Intro.fadeThumbstick(true) + fadeInTween:Play() + + self.moveTween.Completed:wait() + + if Intro.currentState == self then + Intro.fadeThumbstick(false) + fadeOutTween:Play() + + fadeOutTween.Completed:wait() + wait(0.5) + end + else + wait() + end + end + end)() + end + + function Intro.states.MoveThumbstick:stop() + self.moveTween:Cancel() + IntroMoveImage.Visible = false + + if self.iconOptConn then + self.iconOptConn:disconnect() + self.iconOptConn = nil + end + end +end + +Intro.addState("Jump") do + function Intro.states.Jump:start() + IntroJumpImage.Visible = true + IntroJumpImageTap.Visible = true + + local jumpImageBaseSize = IntroMoveImageSize + IntroJumpImage.Size = UDim2.new(0, jumpImageBaseSize * 1.5, 0, jumpImageBaseSize * 1.25) + IntroJumpImageTap.ImageTransparency = 1 + + local fadeInInfo = TweenInfo.new(0.15, Enum.EasingStyle.Quint, Enum.EasingDirection.InOut) + local fadeOutInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + self.fadeHandIn = TweenService:Create(IntroJumpImage, fadeInInfo, { ImageTransparency = 0, Size = UDim2.new(0, jumpImageBaseSize, 0, jumpImageBaseSize * 0.95) }) + self.fadeHandOut = TweenService:Create(IntroJumpImage, fadeOutInfo, { ImageTransparency = 0.25, Size = UDim2.new(0, jumpImageBaseSize * 1.25, 0, jumpImageBaseSize * 1.25) }) + self.fadeTapIn = TweenService:Create(IntroJumpImageTap, fadeInInfo, { ImageTransparency = 0 }) + self.fadeTapOut = TweenService:Create(IntroJumpImageTap, fadeOutInfo, { ImageTransparency = 1 }) + + coroutine.wrap(function() + while Intro.currentState == self do + self.fadeHandIn:Play() + IntroJumpImageTap.ImageTransparency = 1 + + if self.iconOptConn then + self.iconOptConn:disconnect() + self.iconOptConn = nil + end + + wait(self.fadeHandIn.Completed:wait()) + + if not noThumbstickFrameFade then + Intro.fadeThumbstickFrame(fadeOutInfo.Time, 0) + end + IntroJumpImageTap.ImageTransparency = 0 + self.fadeHandOut:Play() + self.fadeTapOut:Play() + + self.fadeHandOut.Completed:wait() + end + end)() + end + + function Intro.states.Jump:stop() + IntroJumpImage.Visible = false + IntroJumpImageTap.Visible = false + end +end + +Intro.addState("MoveCamera") do + function Intro.states.MoveCamera:start() + IntroCameraImage.Visible = true + + local swipeInfo = TweenInfo.new(1, Enum.EasingStyle.Quint, Enum.EasingDirection.InOut) + local swipeTweenLeft = TweenService:Create(IntroCameraImage, swipeInfo, { Position = UDim2.new(0.25, 0, 0.25, 0) }) + local swipeTweenRight = TweenService:Create(IntroCameraImage, swipeInfo, { Position = UDim2.new(0.75, 0, 0.25, 0) }) + + local camera = workspace.CurrentCamera + local cameraLookStart = camera.CFrame.lookVector + coroutine.wrap(function() + while Intro.currentState == self do + swipeTweenLeft:Play() + swipeTweenLeft.Completed:wait() + swipeTweenRight:Play() + swipeTweenRight.Completed:wait() + end + end)() + + local function isInCameraArea(touchPos) + return + touchPos.X >= GestureArea.AbsolutePosition.X and touchPos.Y >= GestureArea.AbsolutePosition.Y and + touchPos.X < GestureArea.AbsolutePosition.X + GestureArea.AbsoluteSize.X and + touchPos.Y < GestureArea.AbsolutePosition.Y + GestureArea.AbsoluteSize.Y + end + + local touchObj = nil + local recordedMovement = Vector3.new() + self.onInputBeganConn = UserInputService.InputBegan:Connect(function(inputObj, wasProcessed) + if inputObj.UserInputType == Enum.UserInputType.Touch and isInCameraArea(inputObj.Position) and not wasProcessed then + touchObj = inputObj + end + end) + self.onInputChangedConn = UserInputService.InputChanged:Connect(function(inputObj, wasProcessed) + if inputObj == touchObj then + recordedMovement = recordedMovement + inputObj.Delta + + if recordedMovement.magnitude > MOVE_CAMERA_THRESHOLD then + IntroCameraImage.Visible = false + end + end + end) + self.onInputEndedConn = UserInputService.InputEnded:Connect(function(inputObj, wasProcessed) + if inputObj == touchObj then + touchObj = nil + if recordedMovement.magnitude > MOVE_CAMERA_THRESHOLD then --todo: make this number a constant + Intro.onCameraMoved() + end + end + end) + end + + function Intro.states.MoveCamera:stop() + if self.iconOptConn then + self.iconOptConn:disconnect() + self.iconOptConn = nil + end + + IntroCameraImage.Visible = false + end +end + +Intro.addState("ZoomCamera") do + function Intro.states.ZoomCamera:start() + IntroPinchImage.Visible = true + + local camera = workspace.CurrentCamera + local zoomStart = (camera.Focus.p - camera.CFrame.p).magnitude + + coroutine.wrap(function() + while Intro.currentState == self do + IntroPinchImage.ImageRectOffset = Vector2.new(0, 0) + wait(0.75) + IntroPinchImage.ImageRectOffset = Vector2.new(128, 0) + wait(0.25) + end + end)() + coroutine.wrap(function() + while Intro.currentState == self do + local zoom = (camera.Focus.p - camera.CFrame.p).magnitude + if math.abs(zoom - zoomStart) > 3 then + Intro.onCameraZoomed() + end + wait() + end + end)() + end + + function Intro.states.ZoomCamera:stop() + IntroPinchImage.Visible = false + + Intro.onCompleted() + end +end + +return Intro \ No newline at end of file diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/Gamepad.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/Gamepad.lua new file mode 100644 index 0000000..49527fb --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/Gamepad.lua @@ -0,0 +1,198 @@ +--[[ + // FileName: Gamepad + // Written by: jeditkacheff + // Description: Implements movement controls for gamepad devices (XBox, PS4, MFi, etc.) +--]] +local Gamepad = {} + +local MasterControl = require(script.Parent) + +local Players = game:GetService('Players') +local RunService = game:GetService('RunService') +local UserInputService = game:GetService('UserInputService') +local ContextActionService = game:GetService('ContextActionService') +local StarterPlayer = game:GetService('StarterPlayer') +local Settings = UserSettings() +local GameSettings = Settings.GameSettings +local currentMoveVector = Vector3.new(0,0,0) +local activateGamepad = nil + +local gamepadConnectedCon = nil +local gamepadDisconnectedCon = nil + +while not Players.LocalPlayer do + wait() +end +local LocalPlayer = Players.LocalPlayer + +--[[ Constants ]]-- +local thumbstickDeadzone = 0.22 --raised from 14% on 3/1/16 to accommodate looser XB360 controllers + +function assignActivateGamepad() + local connectedGamepads = UserInputService:GetConnectedGamepads() + if #connectedGamepads > 0 then + for i = 1, #connectedGamepads do + if activateGamepad == nil then + activateGamepad = connectedGamepads[i] + elseif connectedGamepads[i].Value < activateGamepad.Value then + activateGamepad = connectedGamepads[i] + end + end + end + + if activateGamepad == nil then -- nothing is connected, at least set up for gamepad1 + activateGamepad = Enum.UserInputType.Gamepad1 + end +end + +--[[ Public API ]]-- +function Gamepad:Enable() + local forwardValue = 0 + local backwardValue = 0 + local leftValue = 0 + local rightValue = 0 + + local moveFunc = LocalPlayer.Move + local gamepadSupports = UserInputService.GamepadSupports + + local controlCharacterGamepad = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Cancel then + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + return + end + + if activateGamepad ~= inputObject.UserInputType then return end + if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end + + if inputObject.Position.magnitude > thumbstickDeadzone then + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y) + MasterControl:AddToPlayerMovement(currentMoveVector) + else + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + end + end + + local jumpCharacterGamepad = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Cancel then + MasterControl:SetIsJumping(false) + return + end + + if activateGamepad ~= inputObject.UserInputType then return end + if inputObject.KeyCode ~= Enum.KeyCode.ButtonA then return end + + MasterControl:SetIsJumping(inputObject.UserInputState == Enum.UserInputState.Begin) + end + + local doDpadMoveUpdate = function(userInputType) + if LocalPlayer and LocalPlayer.Character then + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(leftValue + rightValue,0,forwardValue + backwardValue) + MasterControl:AddToPlayerMovement(currentMoveVector) + end + end + + local moveForwardFunc = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + forwardValue = -1 + elseif inputState == Enum.UserInputState.Begin or inputState == Enum.UserInputState.Cancel then + forwardValue = 0 + end + + doDpadMoveUpdate(inputObject.UserInputType) + end + + local moveBackwardFunc = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + backwardValue = 1 + elseif inputState == Enum.UserInputState.Begin or inputState == Enum.UserInputState.Cancel then + backwardValue = 0 + end + + doDpadMoveUpdate(inputObject.UserInputType) + end + + local moveLeftFunc = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + leftValue = -1 + elseif inputState == Enum.UserInputState.Begin or inputState == Enum.UserInputState.Cancel then + leftValue = 0 + end + + doDpadMoveUpdate(inputObject.UserInputType) + end + + local moveRightFunc = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.End then + rightValue = 1 + elseif inputState == Enum.UserInputState.Begin or inputState == Enum.UserInputState.Cancel then + rightValue = 0 + end + + doDpadMoveUpdate(inputObject.UserInputType) + end + + local function setActivateGamepad() + if activateGamepad then + ContextActionService:UnbindActivate(activateGamepad, Enum.KeyCode.ButtonR2) + end + assignActivateGamepad() + if activateGamepad then + ContextActionService:BindActivate(activateGamepad, Enum.KeyCode.ButtonR2) + end + end + + ContextActionService:BindAction("JumpButton",jumpCharacterGamepad, false, Enum.KeyCode.ButtonA) + ContextActionService:BindAction("MoveThumbstick",controlCharacterGamepad, false, Enum.KeyCode.Thumbstick1) + + setActivateGamepad() + + if not gamepadSupports(UserInputService, activateGamepad, Enum.KeyCode.Thumbstick1) then + -- if the gamepad supports thumbsticks, theres no point in having the dpad buttons getting eaten up by these actions + ContextActionService:BindAction("forwardDpad", moveForwardFunc, false, Enum.KeyCode.DPadUp) + ContextActionService:BindAction("backwardDpad", moveBackwardFunc, false, Enum.KeyCode.DPadDown) + ContextActionService:BindAction("leftDpad", moveLeftFunc, false, Enum.KeyCode.DPadLeft) + ContextActionService:BindAction("rightDpad", moveRightFunc, false, Enum.KeyCode.DPadRight) + end + + gamepadConnectedCon = UserInputService.GamepadDisconnected:connect(function(gamepadEnum) + if activateGamepad ~= gamepadEnum then return end + + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + + activateGamepad = nil + setActivateGamepad() + end) + + gamepadDisconnectedCon = UserInputService.GamepadConnected:connect(function(gamepadEnum) + if activateGamepad == nil then + setActivateGamepad() + end + end) +end + +function Gamepad:Disable() + + ContextActionService:UnbindAction("forwardDpad") + ContextActionService:UnbindAction("backwardDpad") + ContextActionService:UnbindAction("leftDpad") + ContextActionService:UnbindAction("rightDpad") + + ContextActionService:UnbindAction("MoveThumbstick") + ContextActionService:UnbindAction("JumpButton") + ContextActionService:UnbindActivate(activateGamepad, Enum.KeyCode.ButtonR2) + + if gamepadConnectedCon then gamepadConnectedCon:disconnect() end + if gamepadDisconnectedCon then gamepadDisconnectedCon:disconnect() end + + activateGamepad = nil + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + MasterControl:SetIsJumping(false) +end + +return Gamepad diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/KeyboardMovement.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/KeyboardMovement.lua new file mode 100644 index 0000000..9371a28 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/KeyboardMovement.lua @@ -0,0 +1,175 @@ +--[[ + // FileName: ComputerMovementKeyboardMovement + // Version 1.2 + // Written by: jeditkacheff/jmargh + // Description: Implements movement controls for keyboard devices +--]] +local Players = game:GetService('Players') +local UserInputService = game:GetService('UserInputService') +local ContextActionService = game:GetService('ContextActionService') +local StarterPlayer = game:GetService('StarterPlayer') +local Settings = UserSettings() +local GameSettings = Settings.GameSettings + +local KeyboardMovement = {} + +while not Players.LocalPlayer do + wait() +end +local LocalPlayer = Players.LocalPlayer +local CachedHumanoid = nil +local SeatJumpCn = nil +local TextFocusReleasedCn = nil +local TextFocusGainedCn = nil +local WindowFocusReleasedCn = nil + +local MasterControl = require(script.Parent) +local currentMoveVector = Vector3.new(0,0,0) + +--[[ Local Functions ]]-- +local function getHumanoid() + local character = LocalPlayer and LocalPlayer.Character + if character then + if CachedHumanoid and CachedHumanoid.Parent == character then + return CachedHumanoid + else + CachedHumanoid = nil + for _,child in pairs(character:GetChildren()) do + if child:IsA('Humanoid') then + CachedHumanoid = child + return CachedHumanoid + end + end + end + end +end + +--[[ Public API ]]-- +function KeyboardMovement:Enable() + if not UserInputService.KeyboardEnabled then + return + end + + local forwardValue = 0 + local backwardValue = 0 + local leftValue = 0 + local rightValue = 0 + + local updateMovement = function(inputState) + if inputState == Enum.UserInputState.Cancel then + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0, 0, 0) + else + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(leftValue + rightValue,0,forwardValue + backwardValue) + MasterControl:AddToPlayerMovement(currentMoveVector) + end + end + + local moveForwardFunc = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + forwardValue = -1 + elseif inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then + forwardValue = 0 + end + updateMovement(inputState) + end + + local moveBackwardFunc = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + backwardValue = 1 + elseif inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then + backwardValue = 0 + end + updateMovement(inputState) + end + + local moveLeftFunc = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + leftValue = -1 + elseif inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then + leftValue = 0 + end + updateMovement(inputState) + end + + local moveRightFunc = function(actionName, inputState, inputObject) + if inputState == Enum.UserInputState.Begin then + rightValue = 1 + elseif inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then + rightValue = 0 + end + updateMovement(inputState) + end + + local jumpFunc = function(actionName, inputState, inputObject) + MasterControl:SetIsJumping(inputState == Enum.UserInputState.Begin) + end + + -- TODO: remove up and down arrows, these seem unnecessary + ContextActionService:BindActionToInputTypes("forwardMovement", moveForwardFunc, false, Enum.PlayerActions.CharacterForward) + ContextActionService:BindActionToInputTypes("backwardMovement", moveBackwardFunc, false, Enum.PlayerActions.CharacterBackward) + ContextActionService:BindActionToInputTypes("leftMovement", moveLeftFunc, false, Enum.PlayerActions.CharacterLeft) + ContextActionService:BindActionToInputTypes("rightMovement", moveRightFunc, false, Enum.PlayerActions.CharacterRight) + ContextActionService:BindActionToInputTypes("jumpAction", jumpFunc, false, Enum.PlayerActions.CharacterJump) + -- TODO: make sure we check key state before binding to check if key is already down + + local function onFocusReleased() + local humanoid = getHumanoid() + if humanoid then + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0, 0, 0) + forwardValue, backwardValue, leftValue, rightValue = 0, 0, 0, 0 + MasterControl:SetIsJumping(false) + end + end + + local function onTextFocusGained(textboxFocused) + MasterControl:SetIsJumping(false) + end + + SeatJumpCn = UserInputService.InputBegan:connect(function(inputObject, isProcessed) + if inputObject.KeyCode == Enum.KeyCode.Backspace and not isProcessed then + local humanoid = getHumanoid() + if humanoid and (humanoid.Sit or humanoid.PlatformStand) then + MasterControl:DoJump() + end + end + end) + + TextFocusReleasedCn = UserInputService.TextBoxFocusReleased:connect(onFocusReleased) + TextFocusGainedCn = UserInputService.TextBoxFocused:connect(onTextFocusGained) + -- TODO: remove pcall when API is live + WindowFocusReleasedCn = UserInputService.WindowFocusReleased:connect(onFocusReleased) +end + +function KeyboardMovement:Disable() + ContextActionService:UnbindAction("forwardMovement") + ContextActionService:UnbindAction("backwardMovement") + ContextActionService:UnbindAction("leftMovement") + ContextActionService:UnbindAction("rightMovement") + ContextActionService:UnbindAction("jumpAction") + + if SeatJumpCn then + SeatJumpCn:disconnect() + SeatJumpCn = nil + end + if TextFocusReleasedCn then + TextFocusReleasedCn:disconnect() + TextFocusReleasedCn = nil + end + if TextFocusGainedCn then + TextFocusGainedCn:disconnect() + TextFocusGainedCn = nil + end + if WindowFocusReleasedCn then + WindowFocusReleasedCn:disconnect() + WindowFocusReleasedCn = nil + end + + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + MasterControl:SetIsJumping(false) +end + +return KeyboardMovement diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/Thumbpad.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/Thumbpad.lua new file mode 100644 index 0000000..1e58857 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/Thumbpad.lua @@ -0,0 +1,228 @@ +--[[ + // FileName: Thumbpad + // Version 1.0 + // Written by: jmargh + // Description: Implements thumbpad controls for touch devices +--]] + +local Players = game:GetService('Players') +local UserInputService = game:GetService('UserInputService') +local GuiService = game:GetService('GuiService') + +local Thumbpad = {} + +local MasterControl = require(script.Parent) + +--[[ Script Variables ]]-- +while not Players.LocalPlayer do + wait() +end +local LocalPlayer = Players.LocalPlayer +local ThumbpadFrame = nil +local TouchObject = nil +local OnInputEnded = nil -- is defined in Create() +local OnTouchChangedCn = nil +local OnTouchEndedCn = nil +local currentMoveVector = Vector3.new(0,0,0) + +--[[ Constants ]]-- +local DPAD_SHEET = "rbxasset://textures/ui/DPadSheet.png" +local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/TouchControlsSheet.png" + +--[[ Local Functions ]]-- +local function createArrowLabel(name, parent, position, size, rectOffset, rectSize) + local image = Instance.new('ImageLabel') + image.Name = name + image.Image = DPAD_SHEET + image.ImageRectOffset = rectOffset + image.ImageRectSize = rectSize + image.BackgroundTransparency = 1 + image.ImageColor3 = Color3.new(190/255, 190/255, 190/255) + image.Size = size + image.Position = position + image.Parent = parent + + return image +end + +--[[ Public API ]]-- +function Thumbpad:Enable() + ThumbpadFrame.Visible = true +end + +function Thumbpad:Disable() + ThumbpadFrame.Visible = false + OnInputEnded() +end + +function Thumbpad:Create(parentFrame) + if ThumbpadFrame then + ThumbpadFrame:Destroy() + ThumbpadFrame = nil + if OnTouchChangedCn then + OnTouchChangedCn:disconnect() + OnTouchChangedCn = nil + end + if OnTouchEndedCn then + OnTouchEndedCn:disconnect() + OnTouchEndedCn = nil + end + end + + local minAxis = math.min(parentFrame.AbsoluteSize.x, parentFrame.AbsoluteSize.y) + local isSmallScreen = minAxis <= 500 + local thumbpadSize = isSmallScreen and 70 or 120 + local position = isSmallScreen and UDim2.new(0, thumbpadSize * 1.25, 1, -thumbpadSize - 20) or + UDim2.new(0, thumbpadSize/2 - 10, 1, -thumbpadSize * 1.75 - 10) + + ThumbpadFrame = Instance.new('Frame') + ThumbpadFrame.Name = "ThumbpadFrame" + ThumbpadFrame.Visible = false + ThumbpadFrame.Active = true + ThumbpadFrame.Size = UDim2.new(0, thumbpadSize + 20, 0, thumbpadSize + 20) + ThumbpadFrame.Position = position + ThumbpadFrame.BackgroundTransparency = 1 + + local outerImage = Instance.new('ImageLabel') + outerImage.Name = "OuterImage" + outerImage.Image = TOUCH_CONTROL_SHEET + outerImage.ImageRectOffset = Vector2.new(0, 0) + outerImage.ImageRectSize = Vector2.new(220, 220) + outerImage.BackgroundTransparency = 1 + outerImage.Size = UDim2.new(0, thumbpadSize, 0, thumbpadSize) + outerImage.Position = UDim2.new(0, 10, 0, 10) + outerImage.Parent = ThumbpadFrame + + local smArrowSize = isSmallScreen and UDim2.new(0, 32, 0, 32) or UDim2.new(0, 64, 0, 64) + local lgArrowSize = UDim2.new(0, smArrowSize.X.Offset * 2, 0, smArrowSize.Y.Offset * 2) + local imgRectSize = Vector2.new(110, 110) + local smImgOffset = isSmallScreen and -4 or -9 + local lgImgOffset = isSmallScreen and -28 or -55 + + local dArrow = createArrowLabel("DownArrow", outerImage, UDim2.new(0.5, -smArrowSize.X.Offset/2, 1, lgImgOffset), smArrowSize, Vector2.new(8, 8), imgRectSize) + local uArrow = createArrowLabel("UpArrow", outerImage, UDim2.new(0.5, -smArrowSize.X.Offset/2, 0, smImgOffset), smArrowSize, Vector2.new(8, 266), imgRectSize) + local lArrow = createArrowLabel("LeftArrow", outerImage, UDim2.new(0, smImgOffset, 0.5, -smArrowSize.Y.Offset/2), smArrowSize, Vector2.new(137, 137), imgRectSize) + local rArrow = createArrowLabel("RightArrow", outerImage, UDim2.new(1, lgImgOffset, 0.5, -smArrowSize.Y.Offset/2), smArrowSize, Vector2.new(8, 137), imgRectSize) + + local function doTween(guiObject, endSize, endPosition) + guiObject:TweenSizeAndPosition(endSize, endPosition, Enum.EasingDirection.InOut, Enum.EasingStyle.Linear, 0.15, true) + end + + local padOrigin = nil + local deadZone = 0.1 + local isRight, isLeft, isUp, isDown = false, false, false, false + local vForward = Vector3.new(0, 0, -1) + local vRight = Vector3.new(1, 0, 0) + local function doMove(pos) + MasterControl:AddToPlayerMovement(-currentMoveVector) + + local delta = Vector2.new(pos.x, pos.y) - padOrigin + currentMoveVector = delta / (thumbpadSize/2) + + -- Scaled Radial Dead Zone + local inputAxisMagnitude = currentMoveVector.magnitude + if inputAxisMagnitude < deadZone then + currentMoveVector = Vector3.new(0, 0, 0) + else + currentMoveVector = currentMoveVector.unit * ((inputAxisMagnitude - deadZone) / (1 - deadZone)) + -- catch possible NAN Vector + if currentMoveVector.magnitude == 0 then + currentMoveVector = Vector3.new(0, 0, 0) + else + currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y).unit + end + end + + MasterControl:AddToPlayerMovement(currentMoveVector) + + local forwardDot = currentMoveVector:Dot(vForward) + local rightDot = currentMoveVector:Dot(vRight) + if forwardDot > 0.5 then -- UP + if not isUp then + isUp, isDown = true, false + doTween(uArrow, lgArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset, 0, smImgOffset - smArrowSize.Y.Offset * 1.5)) + doTween(dArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 1, lgImgOffset)) + end + elseif forwardDot < -0.5 then -- DOWN + if not isDown then + isDown, isUp = true, false + doTween(dArrow, lgArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset, 1, lgImgOffset + smArrowSize.Y.Offset/2)) + doTween(uArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 0, smImgOffset)) + end + else + isUp, isDown = false, false + doTween(dArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 1, lgImgOffset)) + doTween(uArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 0, smImgOffset)) + end + + if rightDot > 0.5 then + if not isRight then + isRight, isLeft = true, false + doTween(rArrow, lgArrowSize, UDim2.new(1, lgImgOffset + smArrowSize.X.Offset/2, 0.5, -smArrowSize.Y.Offset)) + doTween(lArrow, smArrowSize, UDim2.new(0, smImgOffset, 0.5, -smArrowSize.Y.Offset/2)) + end + elseif rightDot < -0.5 then + if not isLeft then + isLeft, isRight = true, false + doTween(lArrow, lgArrowSize, UDim2.new(0, smImgOffset - smArrowSize.X.Offset * 1.5, 0.5, -smArrowSize.Y.Offset)) + doTween(rArrow, smArrowSize, UDim2.new(1, lgImgOffset, 0.5, -smArrowSize.Y.Offset/2)) + end + else + isRight, isLeft = false, false + doTween(lArrow, smArrowSize, UDim2.new(0, smImgOffset, 0.5, -smArrowSize.Y.Offset/2)) + doTween(rArrow, smArrowSize, UDim2.new(1, lgImgOffset, 0.5, -smArrowSize.Y.Offset/2)) + end + end + + --input connections + ThumbpadFrame.InputBegan:connect(function(inputObject) + --A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event + --if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin) + if TouchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch + or inputObject.UserInputState ~= Enum.UserInputState.Begin then + return + end + + ThumbpadFrame.Position = UDim2.new(0, inputObject.Position.x - ThumbpadFrame.AbsoluteSize.x/2, 0, inputObject.Position.y - ThumbpadFrame.Size.Y.Offset/2) + padOrigin = Vector2.new(ThumbpadFrame.AbsolutePosition.x + ThumbpadFrame.AbsoluteSize.x/2, + ThumbpadFrame.AbsolutePosition.y + ThumbpadFrame.AbsoluteSize.y/2) + doMove(inputObject.Position) + TouchObject = inputObject + end) + + OnTouchChangedCn = UserInputService.TouchMoved:connect(function(inputObject, isProcessed) + if inputObject == TouchObject then + doMove(TouchObject.Position) + end + end) + + OnInputEnded = function() + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + MasterControl:SetIsJumping(false) + + ThumbpadFrame.Position = position + TouchObject = nil + isUp, isDown, isLeft, isRight = false, false, false, false + doTween(dArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 1, lgImgOffset)) + doTween(uArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 0, smImgOffset)) + doTween(lArrow, smArrowSize, UDim2.new(0, smImgOffset, 0.5, -smArrowSize.Y.Offset/2)) + doTween(rArrow, smArrowSize, UDim2.new(1, lgImgOffset, 0.5, -smArrowSize.Y.Offset/2)) + end + + OnTouchEndedCn = UserInputService.TouchEnded:connect(function(inputObject) + if inputObject == TouchObject then + OnInputEnded() + end + end) + + GuiService.MenuOpened:connect(function() + if TouchObject then + OnInputEnded() + end + end) + + ThumbpadFrame.Parent = parentFrame +end + +return Thumbpad diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/Thumbstick.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/Thumbstick.lua new file mode 100644 index 0000000..bfe04bc --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/Thumbstick.lua @@ -0,0 +1,178 @@ +--[[ + // FileName: Thumbstick + // Version 1.0 + // Written by: jmargh + // Description: Implements thumbstick controls for touch devices +--]] +local Players = game:GetService('Players') +local UserInputService = game:GetService('UserInputService') +local GuiService = game:GetService('GuiService') + +local MasterControl = require(script.Parent) + +local Thumbstick = {} + +--[[ Script Variables ]]-- +while not Players.LocalPlayer do + wait() +end +local LocalPlayer = Players.LocalPlayer +local IsFollowStick = false +local ThumbstickFrame = nil +local MoveTouchObject = nil +local OnTouchEnded = nil -- defined in Create() +local OnTouchMovedCn = nil +local OnTouchEndedCn = nil +local currentMoveVector = Vector3.new(0,0,0) + +--[[ Constants ]]-- +local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/TouchControlsSheet.png" + +--[[ Public API ]]-- +function Thumbstick:Enable() + ThumbstickFrame.Visible = true +end + +function Thumbstick:Disable() + ThumbstickFrame.Visible = false + OnTouchEnded() +end + +function Thumbstick:Create(parentFrame) + if ThumbstickFrame then + ThumbstickFrame:Destroy() + ThumbstickFrame = nil + if OnTouchMovedCn then + OnTouchMovedCn:disconnect() + OnTouchMovedCn = nil + end + if OnTouchEndedCn then + OnTouchEndedCn:disconnect() + OnTouchEndedCn = nil + end + end + + local minAxis = math.min(parentFrame.AbsoluteSize.x, parentFrame.AbsoluteSize.y) + local isSmallScreen = minAxis <= 500 + local thumbstickSize = isSmallScreen and 70 or 120 + local position = isSmallScreen and UDim2.new(0, (thumbstickSize/2) - 10, 1, -thumbstickSize - 20) or + UDim2.new(0, thumbstickSize/2, 1, -thumbstickSize * 1.75) + + ThumbstickFrame = Instance.new('Frame') + ThumbstickFrame.Name = "ThumbstickFrame" + ThumbstickFrame.Active = true + ThumbstickFrame.Visible = false + ThumbstickFrame.Size = UDim2.new(0, thumbstickSize, 0, thumbstickSize) + ThumbstickFrame.Position = position + ThumbstickFrame.BackgroundTransparency = 1 + + local outerImage = Instance.new('ImageLabel') + outerImage.Name = "OuterImage" + outerImage.Image = TOUCH_CONTROL_SHEET + outerImage.ImageRectOffset = Vector2.new() + outerImage.ImageRectSize = Vector2.new(220, 220) + outerImage.BackgroundTransparency = 1 + outerImage.Size = UDim2.new(0, thumbstickSize, 0, thumbstickSize) + outerImage.Position = UDim2.new(0, 0, 0, 0) + outerImage.Parent = ThumbstickFrame + + StickImage = Instance.new('ImageLabel') + StickImage.Name = "StickImage" + StickImage.Image = TOUCH_CONTROL_SHEET + StickImage.ImageRectOffset = Vector2.new(220, 0) + StickImage.ImageRectSize = Vector2.new(111, 111) + StickImage.BackgroundTransparency = 1 + StickImage.Size = UDim2.new(0, thumbstickSize/2, 0, thumbstickSize/2) + StickImage.Position = UDim2.new(0, thumbstickSize/2 - thumbstickSize/4, 0, thumbstickSize/2 - thumbstickSize/4) + StickImage.ZIndex = 2 + StickImage.Parent = ThumbstickFrame + + local centerPosition = nil + local deadZone = 0.05 + local function doMove(direction) + MasterControl:AddToPlayerMovement(-currentMoveVector) + + currentMoveVector = direction / (thumbstickSize/2) + + -- Scaled Radial Dead Zone + local inputAxisMagnitude = currentMoveVector.magnitude + if inputAxisMagnitude < deadZone then + currentMoveVector = Vector3.new() + else + currentMoveVector = currentMoveVector.unit * ((inputAxisMagnitude - deadZone) / (1 - deadZone)) + -- NOTE: Making currentMoveVector a unit vector will cause the player to instantly go max speed + -- must check for zero length vector is using unit + currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y) + end + + MasterControl:AddToPlayerMovement(currentMoveVector) + end + + local function moveStick(pos) + local relativePosition = Vector2.new(pos.x - centerPosition.x, pos.y - centerPosition.y) + local length = relativePosition.magnitude + local maxLength = ThumbstickFrame.AbsoluteSize.x/2 + if IsFollowStick and length > maxLength then + local offset = relativePosition.unit * maxLength + ThumbstickFrame.Position = UDim2.new( + 0, pos.x - ThumbstickFrame.AbsoluteSize.x/2 - offset.x, + 0, pos.y - ThumbstickFrame.AbsoluteSize.y/2 - offset.y) + else + length = math.min(length, maxLength) + relativePosition = relativePosition.unit * length + end + StickImage.Position = UDim2.new(0, relativePosition.x + StickImage.AbsoluteSize.x/2, 0, relativePosition.y + StickImage.AbsoluteSize.y/2) + end + + -- input connections + ThumbstickFrame.InputBegan:connect(function(inputObject) + --A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event + --if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin) + if MoveTouchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch + or inputObject.UserInputState ~= Enum.UserInputState.Begin then + return + end + + MoveTouchObject = inputObject + ThumbstickFrame.Position = UDim2.new(0, inputObject.Position.x - ThumbstickFrame.Size.X.Offset/2, 0, inputObject.Position.y - ThumbstickFrame.Size.Y.Offset/2) + centerPosition = Vector2.new(ThumbstickFrame.AbsolutePosition.x + ThumbstickFrame.AbsoluteSize.x/2, + ThumbstickFrame.AbsolutePosition.y + ThumbstickFrame.AbsoluteSize.y/2) + local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y) + end) + + OnTouchMovedCn = UserInputService.TouchMoved:connect(function(inputObject, isProcessed) + if inputObject == MoveTouchObject then + centerPosition = Vector2.new(ThumbstickFrame.AbsolutePosition.x + ThumbstickFrame.AbsoluteSize.x/2, + ThumbstickFrame.AbsolutePosition.y + ThumbstickFrame.AbsoluteSize.y/2) + local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y) + doMove(direction) + moveStick(inputObject.Position) + end + end) + + OnTouchEnded = function() + ThumbstickFrame.Position = position + StickImage.Position = UDim2.new(0, ThumbstickFrame.Size.X.Offset/2 - thumbstickSize/4, 0, ThumbstickFrame.Size.Y.Offset/2 - thumbstickSize/4) + MoveTouchObject = nil + + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + MasterControl:SetIsJumping(false) + end + + OnTouchEndedCn = UserInputService.TouchEnded:connect(function(inputObject, isProcessed) + if inputObject == MoveTouchObject then + OnTouchEnded() + end + end) + + GuiService.MenuOpened:connect(function() + if MoveTouchObject then + OnTouchEnded() + end + end) + + ThumbstickFrame.Parent = parentFrame +end + +return Thumbstick diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/TouchJump.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/TouchJump.lua new file mode 100644 index 0000000..58668a3 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/TouchJump.lua @@ -0,0 +1,172 @@ +--[[ + // FileName: TouchJump + // Version 1.0 + // Written by: jmargh + // Description: Implements jump controls for touch devices. Use with Thumbstick and Thumbpad +--]] + +local Players = game:GetService('Players') +local GuiService = game:GetService('GuiService') + +local TouchJump = {} + +local MasterControl = require(script.Parent) + +--[[ Script Variables ]]-- +while not Players.LocalPlayer do + wait() +end +local LocalPlayer = Players.LocalPlayer +local Humanoid = MasterControl:GetHumanoid() +local JumpButton = nil +local OnInputEnded = nil -- defined in Create() +local CharacterAddedConnection = nil +local HumStateConnection = nil +local HumChangeConnection = nil +local ExternallyEnabled = false +local JumpPower = 0 +local JumpStateEnabled = true + +--[[ Constants ]]-- +local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png" + +--[[ Private Functions ]]-- + +local function disableButton() + JumpButton.Visible = false + OnInputEnded() +end + +local function enableButton() + if Humanoid and ExternallyEnabled then + if ExternallyEnabled then + if Humanoid.JumpPower > 0 then + JumpButton.Visible = true + end + end + end +end + +local function updateEnabled() + if JumpPower > 0 and JumpStateEnabled then + enableButton() + else + disableButton() + end +end + +local function humanoidChanged(prop) + if prop == "JumpPower" then + JumpPower = Humanoid.JumpPower + updateEnabled() + elseif prop == "Parent" then + if not Humanoid.Parent then + HumChangeConnection:disconnect() + end + end +end + +local function humandoidStateEnabledChanged(state, isEnabled) + if state == Enum.HumanoidStateType.Jumping then + JumpStateEnabled = isEnabled + updateEnabled() + end +end + +local function characterAdded(newCharacter) + if HumChangeConnection then + HumChangeConnection:disconnect() + end + -- rebind event to new Humanoid + Humanoid = nil + repeat + Humanoid = MasterControl:GetHumanoid() + wait() + until Humanoid and Humanoid.Parent == newCharacter + HumChangeConnection = Humanoid.Changed:connect(humanoidChanged) + HumStateConnection = Humanoid.StateEnabledChanged:connect(humandoidStateEnabledChanged) + JumpPower = Humanoid.JumpPower + JumpStateEnabled = Humanoid:GetStateEnabled(Enum.HumanoidStateType.Jumping) + updateEnabled() +end + +local function setupCharacterAddedFunction() + CharacterAddedConnection = LocalPlayer.CharacterAdded:connect(characterAdded) + if LocalPlayer.Character then + characterAdded(LocalPlayer.Character) + end +end + +--[[ Public API ]]-- +function TouchJump:Enable() + ExternallyEnabled = true + enableButton() +end + +function TouchJump:Disable() + ExternallyEnabled = false + disableButton() +end + +function TouchJump:Create(parentFrame) + if JumpButton then + JumpButton:Destroy() + JumpButton = nil + end + + local minAxis = math.min(parentFrame.AbsoluteSize.x, parentFrame.AbsoluteSize.y) + local isSmallScreen = minAxis <= 500 + local jumpButtonSize = isSmallScreen and 70 or 120 + + JumpButton = Instance.new('ImageButton') + JumpButton.Name = "JumpButton" + JumpButton.Visible = false + JumpButton.BackgroundTransparency = 1 + JumpButton.Image = TOUCH_CONTROL_SHEET + JumpButton.ImageRectOffset = Vector2.new(1, 146) + JumpButton.ImageRectSize = Vector2.new(144, 144) + JumpButton.Size = UDim2.new(0, jumpButtonSize, 0, jumpButtonSize) + + JumpButton.Position = isSmallScreen and UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize - 20) or + UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize * 1.75) + + local touchObject = nil + JumpButton.InputBegan:connect(function(inputObject) + --A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event + --if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin) + if touchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch + or inputObject.UserInputState ~= Enum.UserInputState.Begin then + return + end + + touchObject = inputObject + JumpButton.ImageRectOffset = Vector2.new(146, 146) + MasterControl:SetIsJumping(true) + end) + + OnInputEnded = function() + touchObject = nil + MasterControl:SetIsJumping(false) + JumpButton.ImageRectOffset = Vector2.new(1, 146) + end + + JumpButton.InputEnded:connect(function(inputObject) + if inputObject == touchObject then + OnInputEnded() + end + end) + + GuiService.MenuOpened:connect(function() + if touchObject then + OnInputEnded() + end + end) + + if not CharacterAddedConnection then + setupCharacterAddedFunction() + end + + JumpButton.Parent = parentFrame +end + +return TouchJump diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/VRNavigation.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/VRNavigation.lua new file mode 100644 index 0000000..8d9c49a --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/VRNavigation.lua @@ -0,0 +1,433 @@ +local VRService = game:GetService("VRService") +local UserInputService = game:GetService("UserInputService") +local RunService = game:GetService("RunService") +local Players = game:GetService("Players") +local PathfindingService = game:GetService("PathfindingService") +local ContextActionService = game:GetService("ContextActionService") +local StarterGui = game:GetService("StarterGui") + +local MasterControl = require(script.Parent) +local PathDisplay = nil +local LocalPlayer = Players.LocalPlayer + +local VRNavigation = {} + +local RECALCULATE_PATH_THRESHOLD = 4 +local NO_PATH_THRESHOLD = 12 +local MAX_PATHING_DISTANCE = 200 +local POINT_REACHED_THRESHOLD = 1 +local STOPPING_DISTANCE = 4 +local OFFTRACK_TIME_THRESHOLD = 2 + +local ZERO_VECTOR3 = Vector3.new(0, 0, 0) +local XZ_VECTOR3 = Vector3.new(1, 0, 1) + +local THUMBSTICK_DEADZONE = 0.22 + +local navigationRequestedConn = nil +local heartbeatConn = nil + +local currentDestination = nil +local currentPath = nil +local currentPoints = nil +local currentPointIdx = 0 +local currentMoveVector = Vector3.new(0, 0, 0) + +local expectedTimeToNextPoint = 0 +local timeReachedLastPoint = tick() + +local movementUpdateEvent = Instance.new("BindableEvent") +movementUpdateEvent.Name = "MovementUpdate" +movementUpdateEvent.Parent = script + +coroutine.wrap(function() + local PathDisplayModule = script.Parent.Parent:WaitForChild("PathDisplay") + if PathDisplayModule then + PathDisplay = require(PathDisplayModule) + end +end)() + +local function setLaserPointerMode(mode) + pcall(function() + StarterGui:SetCore("VRLaserPointerMode", mode) + end) +end + +local function getLocalHumanoid() + local character = LocalPlayer.Character + if not character then + return + end + + for _, child in pairs(character:GetChildren()) do + if child:IsA("Humanoid") then + return child + end + end + return nil +end + +local function hasBothHandControllers() + return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) and VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand) +end + +local function hasAnyHandControllers() + return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) or VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand) +end + +local function isMobileVR() + return UserInputService.TouchEnabled +end + +local function hasGamepad() + return UserInputService.GamepadEnabled +end + +local function shouldUseNavigationLaser() + --Places where we use the navigation laser: + -- mobile VR with any number of hands tracked + -- desktop VR with only one hand tracked + -- desktop VR with no hands and no gamepad (i.e. with Oculus remote?) + --using an Xbox controller with a desktop VR headset means no laser since the user has a thumbstick. + --in the future, we should query thumbstick presence with a features API + if isMobileVR() then + return true + else + if hasBothHandControllers() then + return false + end + if not hasAnyHandControllers() then + return not hasGamepad() + end + return true + end +end + +local function IsFinite(num) + return num == num and num ~= 1/0 and num ~= -1/0 +end + +local function IsFiniteVector3(vec3) + return IsFinite(vec3.x) and IsFinite(vec3.y) and IsFinite(vec3.z) +end + +local moving = false + +local function startFollowingPath(newPath) + currentPath = newPath + currentPoints = currentPath:GetPointCoordinates() + currentPointIdx = 1 + moving = true + + timeReachedLastPoint = tick() + + local humanoid = getLocalHumanoid() + if humanoid and humanoid.Torso and #currentPoints >= 1 then + local dist = (currentPoints[1] - humanoid.Torso.Position).magnitude + expectedTimeToNextPoint = dist / humanoid.WalkSpeed + end + + movementUpdateEvent:Fire("targetPoint", currentDestination) +end + +local function goToPoint(point) + currentPath = true + currentPoints = { point } + currentPointIdx = 1 + moving = true + + local humanoid = getLocalHumanoid() + local distance = (humanoid.Torso.Position - point).magnitude + local estimatedTimeRemaining = distance / humanoid.WalkSpeed + + timeReachedLastPoint = tick() + expectedTimeToNextPoint = estimatedTimeRemaining + + movementUpdateEvent:Fire("targetPoint", point) +end + +local function stopFollowingPath() + currentPath = nil + currentPoints = nil + currentPointIdx = 0 + moving = false + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = ZERO_VECTOR3 +end + +local function tryComputePath(startPos, destination) + local numAttempts = 0 + local newPath = nil + + while not newPath and numAttempts < 5 do + newPath = PathfindingService:ComputeSmoothPathAsync(startPos, destination, MAX_PATHING_DISTANCE) + numAttempts = numAttempts + 1 + + if newPath.Status == Enum.PathStatus.ClosestNoPath or newPath.Status == Enum.PathStatus.ClosestOutOfRange then + newPath = nil + break + end + + if newPath and newPath.Status == Enum.PathStatus.FailStartNotEmpty then + startPos = startPos + (destination - startPos).unit + newPath = nil + end + + if newPath and newPath.Status == Enum.PathStatus.FailFinishNotEmpty then + destination = destination + Vector3.new(0, 1, 0) + newPath = nil + end + end + + return newPath +end + +local function onNavigationRequest(destinationCFrame, requestedWith) + local destinationPosition = destinationCFrame.p + local lastDestination = currentDestination + + if not IsFiniteVector3(destinationPosition) then + return + end + + currentDestination = destinationPosition + + local humanoid = getLocalHumanoid() + if not humanoid or not humanoid.Torso then + return + end + + local currentPosition = humanoid.Torso.Position + local distanceToDestination = (currentDestination - currentPosition).magnitude + + if distanceToDestination < NO_PATH_THRESHOLD then + goToPoint(currentDestination) + return + end + + if not lastDestination or (currentDestination - lastDestination).magnitude > RECALCULATE_PATH_THRESHOLD then + local newPath = tryComputePath(currentPosition, currentDestination) + if newPath then + startFollowingPath(newPath) + if PathDisplay then + PathDisplay.setCurrentPoints(currentPoints) + PathDisplay.renderPath() + end + else + stopFollowingPath() + if PathDisplay then + PathDisplay.clearRenderedPath() + end + end + else + if moving then + currentPoints[#currentPoints] = currentDestination + else + goToPoint(currentDestination) + end + end +end + +local isJumpBound = false +local function onJumpAction(actionName, inputState, inputObj) + if inputState == Enum.UserInputState.Begin then + MasterControl:DoJump() + end +end + +local function bindJumpAction(active) + if active then + if not isJumpBound then + isJumpBound = true + ContextActionService:BindAction("VRJumpAction", onJumpAction, false, Enum.KeyCode.ButtonA) + end + else + if isJumpBound then + isJumpBound = false + ContextActionService:UnbindAction("VRJumpAction") + end + end +end + +local moveLatch = false +local controlCharacterGamepad = function(actionName, inputState, inputObject) + if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end + + if inputState == Enum.UserInputState.Cancel then + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + return + end + + if inputState ~= Enum.UserInputState.End then + stopFollowingPath() + if PathDisplay then + PathDisplay.clearRenderedPath() + end + + if shouldUseNavigationLaser() then + bindJumpAction(true) + setLaserPointerMode("Hidden") + end + + if inputObject.Position.magnitude > THUMBSTICK_DEADZONE then + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y) + if currentMoveVector.magnitude > 0 then + currentMoveVector = currentMoveVector.unit * math.min(1, inputObject.Position.magnitude) + end + MasterControl:AddToPlayerMovement(currentMoveVector) + + moveLatch = true + end + else + MasterControl:AddToPlayerMovement(-currentMoveVector) + currentMoveVector = Vector3.new(0,0,0) + + if shouldUseNavigationLaser() then + bindJumpAction(false) + setLaserPointerMode("Navigation") + end + + if moveLatch then + moveLatch = false + movementUpdateEvent:Fire("offtrack") + end + end +end + +local function onHeartbeat(dt) + local newMoveVector = currentMoveVector + local humanoid = getLocalHumanoid() + if not humanoid or not humanoid.Torso then + return + end + + if moving and currentPoints then + local currentPosition = humanoid.Torso.Position + local goalPosition = currentPoints[1] + local vectorToGoal = (goalPosition - currentPosition) * XZ_VECTOR3 + local moveDist = vectorToGoal.magnitude + local moveDir = vectorToGoal / moveDist + + if moveDist < POINT_REACHED_THRESHOLD then + local estimatedTimeRemaining = 0 + local prevPoint = currentPoints[1] + for i, point in pairs(currentPoints) do + if i ~= 1 then + local dist = (point - prevPoint).magnitude + prevPoint = point + estimatedTimeRemaining = estimatedTimeRemaining + (dist / humanoid.WalkSpeed) + end + end + + table.remove(currentPoints, 1) + currentPointIdx = currentPointIdx + 1 + + if #currentPoints == 0 then + stopFollowingPath() + if PathDisplay then + PathDisplay.clearRenderedPath() + end + return + else + if PathDisplay then + PathDisplay.setCurrentPoints(currentPoints) + PathDisplay.renderPath() + end + + local newGoal = currentPoints[1] + local distanceToGoal = (newGoal - currentPosition).magnitude + expectedTimeToNextPoint = distanceToGoal / humanoid.WalkSpeed + timeReachedLastPoint = tick() + end + else + local ignoreTable = { + game.Players.LocalPlayer.Character, + workspace.CurrentCamera + } + local obstructRay = Ray.new(currentPosition - Vector3.new(0, 1, 0), moveDir * 3) + local obstructPart, obstructPoint, obstructNormal = workspace:FindPartOnRayWithIgnoreList(obstructRay, ignoreTable) + + if obstructPart then + local heightOffset = Vector3.new(0, 100, 0) + local jumpCheckRay = Ray.new(obstructPoint + moveDir * 0.5 + heightOffset, -heightOffset) + local jumpCheckPart, jumpCheckPoint, jumpCheckNormal = workspace:FindPartOnRayWithIgnoreList(jumpCheckRay, ignoreTable) + + local heightDifference = jumpCheckPoint.Y - currentPosition.Y + if heightDifference < 6 and heightDifference > -2 then + humanoid.Jump = true + end + end + + local timeSinceLastPoint = tick() - timeReachedLastPoint + if timeSinceLastPoint > expectedTimeToNextPoint + OFFTRACK_TIME_THRESHOLD then + stopFollowingPath() + if PathDisplay then + PathDisplay.clearRenderedPath() + end + + movementUpdateEvent:Fire("offtrack") + end + + newMoveVector = currentMoveVector:Lerp(moveDir, dt * 10) + end + end + + if IsFiniteVector3(newMoveVector) then + MasterControl:AddToPlayerMovement(newMoveVector - currentMoveVector) + currentMoveVector = newMoveVector + end +end + +local userCFrameEnabledConn = nil +local function onUserCFrameEnabled() + if shouldUseNavigationLaser() then + bindJumpAction(false) + setLaserPointerMode("Navigation") + else + bindJumpAction(true) + setLaserPointerMode("Hidden") + end +end + +function VRNavigation:Enable() + navigationRequestedConn = VRService.NavigationRequested:connect(onNavigationRequest) + heartbeatConn = RunService.Heartbeat:connect(onHeartbeat) + + ContextActionService:BindAction("MoveThumbstick", controlCharacterGamepad, false, Enum.KeyCode.Thumbstick1) + ContextActionService:BindActivate(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2) + + userCFrameEnabledConn = VRService.UserCFrameEnabled:connect(onUserCFrameEnabled) + onUserCFrameEnabled() + + pcall(function() + VRService:SetTouchpadMode(Enum.VRTouchpad.Left, Enum.VRTouchpadMode.VirtualThumbstick) + VRService:SetTouchpadMode(Enum.VRTouchpad.Right, Enum.VRTouchpadMode.ABXY) + end) +end + +function VRNavigation:Disable() + stopFollowingPath() + + ContextActionService:UnbindAction("MoveThumbstick") + ContextActionService:UnbindActivate(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2) + + bindJumpAction(false) + setLaserPointerMode("Disabled") + + if navigationRequestedConn then + navigationRequestedConn:disconnect() + navigationRequestedConn = nil + end + if heartbeatConn then + heartbeatConn:disconnect() + heartbeatConn = nil + end + if userCFrameEnabledConn then + userCFrameEnabledConn:disconnect() + userCFrameEnabledConn = nil + end +end + +return VRNavigation diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/VehicleController.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/VehicleController.lua new file mode 100644 index 0000000..e2a3db1 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/VehicleController.lua @@ -0,0 +1,180 @@ +--[[ + // FileName: VehicleControl + // Version 1.0 + // Written by: jmargh + // Description: Implements in-game vehicle controls for all input devices + + // NOTE: This works for basic vehicles (single vehicle seat). If you use custom VehicleSeat code, + // multiple VehicleSeats or your own implementation of a VehicleSeat this will not work. +--]] + +local VehicleController = {} + +local ContextActionService = game:GetService('ContextActionService') +local Players = game:GetService('Players') +local RunService = game:GetService('RunService') + +local MasterControl = require(script.Parent) + +while not Players.LocalPlayer do + wait() +end +local LocalPlayer = Players.LocalPlayer +local CurrentVehicleSeat = nil +local CurrentThrottle = 0 +local CurrentSteer = 0 +local HumanoidSeatedCn = nil +local RenderSteppedCn = nil +local Accelerating = false +local Deccelerating = false +local TurningRight = false +local TurningLeft = false +-- Set this to true if you want to instead use the triggers for the throttle +local useTriggersForThrottle = true +-- Also set this to true if you want the thumbstick to not affect throttle, only triggers when a gamepad is conected +local onlyTriggersForThrottle = false + +local function onThrottleAccel(actionName, inputState, inputObject) + MasterControl:AddToPlayerMovement(Vector3.new(0, 0, -CurrentThrottle)) + CurrentThrottle = (inputState == Enum.UserInputState.End or Deccelerating) and 0 or -1 + MasterControl:AddToPlayerMovement(Vector3.new(0, 0, CurrentThrottle)) + Accelerating = not (inputState == Enum.UserInputState.End) + if (inputState == Enum.UserInputState.End) and Deccelerating then + CurrentThrottle = 1 + MasterControl:AddToPlayerMovement(Vector3.new(0, 0, CurrentThrottle)) + end +end + +local function onThrottleDeccel(actionName, inputState, inputObject) + MasterControl:AddToPlayerMovement(Vector3.new(0, 0, -CurrentThrottle)) + CurrentThrottle = (inputState == Enum.UserInputState.End or Accelerating) and 0 or 1 + MasterControl:AddToPlayerMovement(Vector3.new(0, 0, CurrentThrottle)) + Deccelerating = not (inputState == Enum.UserInputState.End) + if (inputState == Enum.UserInputState.End) and Accelerating then + CurrentThrottle = -1 + MasterControl:AddToPlayerMovement(Vector3.new(0, 0, CurrentThrottle)) + end +end + +local function onSteerRight(actionName, inputState, inputObject) + MasterControl:AddToPlayerMovement(Vector3.new(-CurrentSteer, 0, 0)) + CurrentSteer = (inputState == Enum.UserInputState.End or TurningLeft) and 0 or 1 + MasterControl:AddToPlayerMovement(Vector3.new(CurrentSteer, 0, 0)) + TurningRight = not (inputState == Enum.UserInputState.End) + if (inputState == Enum.UserInputState.End) and TurningLeft then + CurrentSteer = -1 + MasterControl:AddToPlayerMovement(Vector3.new(CurrentSteer, 0, 0)) + end +end + +local function onSteerLeft(actionName, inputState, inputObject) + MasterControl:AddToPlayerMovement(Vector3.new(-CurrentSteer, 0, 0)) + CurrentSteer = (inputState == Enum.UserInputState.End or TurningRight) and 0 or -1 + MasterControl:AddToPlayerMovement(Vector3.new(CurrentSteer, 0, 0)) + TurningLeft = not (inputState == Enum.UserInputState.End) + if (inputState == Enum.UserInputState.End) and TurningRight then + CurrentSteer = 1 + MasterControl:AddToPlayerMovement(Vector3.new(CurrentSteer, 0, 0)) + end +end + +local function getHumanoid() + local character = LocalPlayer and LocalPlayer.Character + if character then + for _,child in pairs(character:GetChildren()) do + if child:IsA('Humanoid') then + return child + end + end + end +end + +local function getClosestFittingValue(value) + if value > 0.5 then + return 1 + elseif value < -0.5 then + return -1 + end + return 0 +end + +local function onRenderStepped() + if CurrentVehicleSeat then + local moveValue = MasterControl:GetMoveVector() + local didSetThrottleSteerFloat = false + didSetThrottleSteerFloat = pcall(function() + if game:GetService("UserInputService"):GetGamepadConnected(Enum.UserInputType.Gamepad1) and onlyTriggersForThrottle and useTriggersForThrottle then + CurrentVehicleSeat.ThrottleFloat = -CurrentThrottle + else + CurrentVehicleSeat.ThrottleFloat = -moveValue.z + end + CurrentVehicleSeat.SteerFloat = moveValue.x + end) + + if didSetThrottleSteerFloat == false then + if game:GetService("UserInputService"):GetGamepadConnected(Enum.UserInputType.Gamepad1) and onlyTriggersForThrottle and useTriggersForThrottle then + CurrentVehicleSeat.Throttle = -CurrentThrottle + else + CurrentVehicleSeat.Throttle = getClosestFittingValue(-moveValue.z) + end + CurrentVehicleSeat.Steer = getClosestFittingValue(moveValue.x) + end + end +end + +local function onSeated(active, currentSeatPart) + if active then + if currentSeatPart and currentSeatPart:IsA('VehicleSeat') then + CurrentVehicleSeat = currentSeatPart + if useTriggersForThrottle then + ContextActionService:BindAction("throttleAccel", onThrottleAccel, false, Enum.KeyCode.ButtonR2) + ContextActionService:BindAction("throttleDeccel", onThrottleDeccel, false, Enum.KeyCode.ButtonL2) + end + ContextActionService:BindAction("arrowSteerRight", onSteerRight, false, Enum.KeyCode.Right) + ContextActionService:BindAction("arrowSteerLeft", onSteerLeft, false, Enum.KeyCode.Left) + local success = pcall(function() RunService:BindToRenderStep("VehicleControlStep", Enum.RenderPriority.Input.Value, onRenderStepped) end) + + if not success then + if RenderSteppedCn then return end + RenderSteppedCn = RunService.RenderStepped:connect(onRenderStepped) + end + end + else + CurrentVehicleSeat = nil + if useTriggersForThrottle then + ContextActionService:UnbindAction("throttleAccel") + ContextActionService:UnbindAction("throttleDeccel") + end + ContextActionService:UnbindAction("arrowSteerRight") + ContextActionService:UnbindAction("arrowSteerLeft") + MasterControl:AddToPlayerMovement(Vector3.new(-CurrentSteer, 0, -CurrentThrottle)) + CurrentThrottle = 0 + CurrentSteer = 0 + local success = pcall(function() RunService:UnbindFromRenderStep("VehicleControlStep") end) + if not success and RenderSteppedCn then + RenderSteppedCn:disconnect() + RenderSteppedCn = nil + end + end +end + +local function CharacterAdded(character) + local humanoid = getHumanoid() + while not humanoid do + wait() + humanoid = getHumanoid() + end + -- + if HumanoidSeatedCn then + HumanoidSeatedCn:disconnect() + HumanoidSeatedCn = nil + end + HumanoidSeatedCn = humanoid.Seated:connect(onSeated) +end + +if LocalPlayer.Character then + CharacterAdded(LocalPlayer.Character) +end +LocalPlayer.CharacterAdded:connect(CharacterAdded) + +return VehicleController diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/PathDisplay.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/PathDisplay.lua new file mode 100644 index 0000000..002f8c7 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/PathDisplay.lua @@ -0,0 +1,131 @@ + + +local PathDisplay = {} +PathDisplay.spacing = 8 +PathDisplay.image = "rbxasset://textures/Cursors/Gamepad/Pointer.png" +PathDisplay.imageSize = Vector2.new(2, 2) + +local currentPoints = {} +local renderedPoints = {} + +local pointModel = Instance.new("Model") +pointModel.Name = "PathDisplayPoints" + +local adorneePart = Instance.new("Part") +adorneePart.Anchored = true +adorneePart.CanCollide = false +adorneePart.Transparency = 1 +adorneePart.Name = "PathDisplayAdornee" +adorneePart.CFrame = CFrame.new(0, 0, 0) +adorneePart.Parent = pointModel + +local pointPool = {} +local poolTop = 30 +for i = 1, poolTop do + local point = Instance.new("ImageHandleAdornment") + point.Archivable = false + point.Adornee = adorneePart + point.Image = PathDisplay.image + point.Size = PathDisplay.imageSize + pointPool[i] = point +end + +local function retrieveFromPool() + local point = pointPool[1] + if not point then + return + end + + pointPool[1], pointPool[poolTop] = pointPool[poolTop], nil + poolTop = poolTop - 1 + return point +end + +local function returnToPool(point) + poolTop = poolTop + 1 + pointPool[poolTop] = point +end + +local function renderPoint(point, isLast) + if poolTop == 0 then + return + end + + local rayDown = Ray.new(point + Vector3.new(0, 2, 0), Vector3.new(0, -8, 0)) + local hitPart, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(rayDown, { game.Players.LocalPlayer.Character, workspace.CurrentCamera }) + if not hitPart then + return + end + + local pointCFrame = CFrame.new(hitPoint, hitPoint + hitNormal) + + local point = retrieveFromPool() + point.CFrame = pointCFrame + point.Parent = pointModel + return point +end + +function PathDisplay.setCurrentPoints(points) + if typeof(points) == 'table' then + currentPoints = points + else + currentPoints = {} + end +end + +function PathDisplay.clearRenderedPath() + for _, oldPoint in ipairs(renderedPoints) do + oldPoint.Parent = nil + returnToPool(oldPoint) + end + renderedPoints = {} + pointModel.Parent = nil +end + +function PathDisplay.renderPath() + PathDisplay.clearRenderedPath() + if not currentPoints or #currentPoints == 0 then + return + end + + local currentIdx = #currentPoints + local lastPos = currentPoints[currentIdx] + local distanceBudget = 0 + + renderedPoints[1] = renderPoint(lastPos, true) + if not renderedPoints[1] then + return + end + + while true do + local currentPoint = currentPoints[currentIdx] + local nextPoint = currentPoints[currentIdx - 1] + + if currentIdx < 2 then + break + else + + local toNextPoint = nextPoint - currentPoint + local distToNextPoint = toNextPoint.magnitude + + if distanceBudget > distToNextPoint then + distanceBudget = distanceBudget - distToNextPoint + currentIdx = currentIdx - 1 + else + local dirToNextPoint = toNextPoint.unit + local pointPos = currentPoint + (dirToNextPoint * distanceBudget) + local point = renderPoint(pointPos, false) + + if point then + renderedPoints[#renderedPoints + 1] = point + end + + distanceBudget = distanceBudget + PathDisplay.spacing + end + end + end + + pointModel.Parent = workspace.CurrentCamera +end + +return PathDisplay diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript.lua new file mode 100644 index 0000000..4739eda --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript.lua @@ -0,0 +1,7 @@ +--[[ + RobloxPlayerScript - This script loads the CameraScript and ControlScript modules + + 2018 PlayerScripts Update - AllYourBlox +--]] +local CameraScript = require(script:WaitForChild("CameraScript")) +local ControlScript = require(script:WaitForChild("ControlScript")) \ No newline at end of file diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript.lua new file mode 100644 index 0000000..c7067c4 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript.lua @@ -0,0 +1,548 @@ +--[[ + CameraScript - This module manages the selection of the current camera control module, + character occlusion module, and transparency controller. This script binds to + RenderStepped at Camera priority and calls the Update() methods on the active camera + control module, character occlusion module, and character transparency controller. + Camera control modules are instantiated and activated as-needed, they are no longer all + instantiated up front. + + 2018 PlayerScripts Update - AllYourBlox +--]] + +local CameraScript = {} + +-- NOTICE: Player property names do not all match their StarterPlayer equivalents, +-- with the differences noted in the comments on the right +local PLAYER_CAMERA_PROPERTIES = +{ + "CameraMinZoomDistance", + "CameraMaxZoomDistance", + "CameraMode", + "DevCameraOcclusionMode", + "DevComputerCameraMode", -- Corresponds to StarterPlayer.DevComputerCameraMovementMode + "DevTouchCameraMode", -- Corresponds to StarterPlayer.DevTouchCameraMovementMode + + -- Character movement mode + "DevComputerMovementMode", + "DevTouchMovementMode", + "DevEnableMouseLock", -- Corresponds to StarterPlayer.EnableMouseLockOption +} + +local USER_GAME_SETTINGS_PROPERTIES = +{ + "ComputerCameraMovementMode", + "ComputerMovementMode", + "ControlMode", + "GamepadCameraSensitivity", + "MouseSensitivity", + "RotationType", + "TouchCameraMovementMode", + "TouchMovementMode", +} + +--[[ Roblox Services ]]-- +local Players = game:GetService("Players") +local RunService = game:GetService("RunService") +local UserInputService = game:GetService("UserInputService") +local StarterPlayer = game:GetService("StarterPlayer") +local UserGameSettings = UserSettings():GetService("UserGameSettings") + +-- Camera math utility library +local CameraUtils = require(script:WaitForChild("CameraUtils")) + +-- Load camera modules (Note: each returns a new() constructor function but does not instantiate a controller) +local ClassicCamera = require(script:WaitForChild("ClassicCamera")) +local OrbitalCamera = require(script:WaitForChild("OrbitalCamera")) +local LegacyCamera = require(script:WaitForChild("LegacyCamera")) + +-- Load character-occlusion modules +local Invisicam = require(script:WaitForChild("Invisicam")) +local Poppercam do + local success, useNewPoppercam = pcall(UserSettings().IsUserFeatureEnabled, UserSettings(), "UserNewPoppercam") + if success and useNewPoppercam then + Poppercam = require(script:WaitForChild("Poppercam")) + else + Poppercam = require(script:WaitForChild("Poppercam_Classic")) + end +end + +-- Load the near-field character transparency controller +local TransparencyController = require(script:WaitForChild("TransparencyController")) +local MouseLockController = require(script:WaitForChild("MouseLockController")) + +-- Table of camera controllers that have been instantiated as needed +local instantiatedCameraControllers = {} +local instantiatedOcclusionModules = {} + +-- Management of which options appear on the Roblox User Settings screen +do + local PlayerScripts = Players.LocalPlayer:WaitForChild("PlayerScripts") + local canRegisterCameras = pcall(function() PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Default) end) + if canRegisterCameras then + PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Follow) + PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Classic) + + PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Default) + PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Follow) + PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Classic) + end +end + +-- Current active camera and occlusion module +local activeCameraController = nil +local activeOcclusionModule = nil +local activeTransparencyController = nil +local activeMouseLockController = nil + +local currentComputerCameraMovementMode = nil + +-- Connections to events +local cameraSubjectChangedConn = nil +local cameraTypeChangedConn = nil + +local function GetCameraMovementModeFromSettings() + local cameraMode = Players.LocalPlayer.CameraMode + + -- Lock First Person trumps all other settings and forces ClassicCamera + if cameraMode == Enum.CameraMode.LockFirstPerson then + return CameraUtils.ConvertCameraModeEnumToStandard(Enum.ComputerCameraMovementMode.Classic) + end + + local devMode, userMode + if UserInputService.TouchEnabled then + devMode = CameraUtils.ConvertCameraModeEnumToStandard(Players.LocalPlayer.DevTouchCameraMode) + userMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.TouchCameraMovementMode) + else + devMode = CameraUtils.ConvertCameraModeEnumToStandard(Players.LocalPlayer.DevComputerCameraMode) + userMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.ComputerCameraMovementMode) + end + + if devMode == Enum.DevComputerCameraMovementMode.UserChoice then + -- Developer is allowing user choice, so user setting is respected + return userMode + end + + return devMode +end + +local function ActivateOcclusionModule( occlusionMode ) + local newModuleCreator = nil + if occlusionMode == Enum.DevCameraOcclusionMode.Zoom then + newModuleCreator = Poppercam + elseif occlusionMode == Enum.DevCameraOcclusionMode.Invisicam then + newModuleCreator = Invisicam + else + warn("CameraScript ActivateOcclusionModule called with unsupported mode") + return + end + + -- First check to see if there is actually a change. If the module being requested is already + -- the currently-active solution then just make sure it's enabled and exit early + if activeOcclusionModule and activeOcclusionModule:GetOcclusionMode() == occlusionMode then + if not activeOcclusionModule:GetEnabled() then + activeOcclusionModule:Enable(true) + end + return + end + + -- Save a reference to the current active module (may be nil) so that we can disable it if + -- we are successful in activating its replacement + local prevOcclusionModule = activeOcclusionModule + + -- If there is no active module, see if the one we need has already been instantiated + activeOcclusionModule = instantiatedOcclusionModules[newModuleCreator] + + -- If the module was not already instantiated and selected above, instantiate it + if not activeOcclusionModule then + activeOcclusionModule = newModuleCreator.new() + if activeOcclusionModule then + instantiatedOcclusionModules[newModuleCreator] = activeOcclusionModule + end + end + + -- If we were successful in either selecting or instantiating the module, + -- enable it if it's not already the currently-active enabled module + if activeOcclusionModule then + local newModuleOcclusionMode = activeOcclusionModule:GetOcclusionMode() + -- Sanity check that the module we selected or instantiated actually supports the desired occlusionMode + if newModuleOcclusionMode ~= occlusionMode then + warn("CameraScript ActivateOcclusionModule mismatch: ",activeOcclusionModule:GetOcclusionMode(),"~=",occlusionMode) + end + + -- Deactivate current module if there is one + if prevOcclusionModule then + -- Sanity check that current module is not being replaced by itself (that should have been handled above) + if prevOcclusionModule ~= activeOcclusionModule then + prevOcclusionModule:Enable(false) + else + warn("CameraScript ActivateOcclusionModule failure to detect already running correct module") + end + end + + -- Occlusion modules need to be initialized with information about characters and cameraSubject + -- Invisicam needs the LocalPlayer's character + -- Poppercam needs all player characters and the camera subject + if occlusionMode == Enum.DevCameraOcclusionMode.Invisicam then + -- Optimization to only send Invisicam what we know it needs + if Players.LocalPlayer.Character then + activeOcclusionModule:CharacterAdded(Players.LocalPlayer, Players.LocalPlayer.Character) + end + else + -- Poppercam and any others that get added in the future and need the full player list for raycast ignore list + for _, player in pairs(Players:GetPlayers()) do + if player and player.Character then + activeOcclusionModule:CharacterAdded(player, player.Character) + end + end + activeOcclusionModule:OnCameraSubjectChanged(game.Workspace.CurrentCamera.CameraSubject) + end + + -- Activate new choice + activeOcclusionModule:Enable(true) + end +end + +-- When supplied, legacyCameraType is used and cameraMovementMode is ignored (should be nil anyways) +-- Next, if userCameraCreator is passed in, that is used as the cameraCreator +local function ActivateCameraController( cameraMovementMode, legacyCameraType ) + + local newCameraCreator = nil + + if legacyCameraType~=nil then + --[[ + This function has been passed a CameraType enum value. Some of these map to the use of + the LegacyCamera module, the value "Custom" will be translated to a movementMode enum + value based on Dev and User settings, and "Scriptable" will disable the camera controller. + --]] + + if legacyCameraType == Enum.CameraType.Scriptable then + if activeCameraController then + activeCameraController:Enable(false) + activeCameraController = nil + return + end + elseif legacyCameraType == Enum.CameraType.Custom then + cameraMovementMode = GetCameraMovementModeFromSettings() + + elseif legacyCameraType == Enum.CameraType.Track then + -- Note: The TrackCamera module was basically an older, less fully-featured + -- version of ClassicCamera, no longer actively maintained, but it is re-implemented in + -- case a game was dependent on its lack of ClassicCamera's extra functionality. + cameraMovementMode = Enum.ComputerCameraMovementMode.Classic + + elseif legacyCameraType == Enum.CameraType.Follow then + cameraMovementMode = Enum.ComputerCameraMovementMode.Follow + + elseif legacyCameraType == Enum.CameraType.Orbital then + cameraMovementMode = Enum.ComputerCameraMovementMode.Orbital + + elseif legacyCameraType == Enum.CameraType.Attach or + legacyCameraType == Enum.CameraType.Watch or + legacyCameraType == Enum.CameraType.Fixed then + newCameraCreator = LegacyCamera + else + warn("CameraScript encountered an unhandled Camera.CameraType value: ",legacyCameraType) + end + end + + if not newCameraCreator then + if cameraMovementMode == Enum.ComputerCameraMovementMode.Classic or + cameraMovementMode == Enum.ComputerCameraMovementMode.Follow or + cameraMovementMode == Enum.ComputerCameraMovementMode.Default then + newCameraCreator = ClassicCamera + elseif cameraMovementMode == Enum.ComputerCameraMovementMode.Orbital then + newCameraCreator = OrbitalCamera + else + warn("ActivateCameraController did not select a module.") + return + end + end + + -- Create the camera control module we need if it does not already exist in instantiatedCameraControllers + local newCameraController = nil + if not instantiatedCameraControllers[newCameraCreator] then + newCameraController = newCameraCreator.new() + instantiatedCameraControllers[newCameraCreator] = newCameraController + else + newCameraController = instantiatedCameraControllers[newCameraCreator] + end + + -- If there is a controller active and it's not the one we need, disable it, + -- if it is the one we need, make sure it's enabled + if activeCameraController then + if activeCameraController ~= newCameraController then + activeCameraController:Enable(false) + activeCameraController = newCameraController + activeCameraController:Enable(true) + elseif not activeCameraController:GetEnabled() then + activeCameraController:Enable(true) + end + elseif newCameraController ~= nil then + activeCameraController = newCameraController + activeCameraController:Enable(true) + end + + if activeCameraController then + if cameraMovementMode~=nil then + activeCameraController:SetCameraMovementMode(cameraMovementMode) + elseif legacyCameraType~=nil then + -- Note that this is only called when legacyCameraType is not a type that + -- was convertible to a ComputerCameraMovementMode value, i.e. really only applies to LegacyCamera + activeCameraController:SetCameraType(legacyCameraType) + end + end +end + +-- Note: The active transparency controller could be made to listen for this event itself. +local function OnCameraSubjectChanged() + if activeTransparencyController then + activeTransparencyController:SetSubject(game.Workspace.CurrentCamera.CameraSubject) + end + + if activeOcclusionModule then + activeOcclusionModule:OnCameraSubjectChanged(game.Workspace.CurrentCamera.CameraSubject) + end +end + +local function OnCameraTypeChanged(newCameraType) + if newCameraType == Enum.CameraType.Scriptable then + if UserInputService.MouseBehavior == Enum.MouseBehavior.LockCenter then + UserInputService.MouseBehavior = Enum.MouseBehavior.Default + end + end + + -- Forward the change to ActivateCameraController to handle + ActivateCameraController(nil, newCameraType) +end + +-- Note: Called whenever workspace.CurrentCamera changes, but also on initialization of this script +local function OnCurrentCameraChanged() + local currentCamera = game.Workspace.CurrentCamera + if not currentCamera then return end + + if cameraSubjectChangedConn then + cameraSubjectChangedConn:Disconnect() + end + + if cameraTypeChangedConn then + cameraTypeChangedConn:Disconnect() + end + + cameraSubjectChangedConn = currentCamera:GetPropertyChangedSignal("CameraSubject"):Connect(function() + OnCameraSubjectChanged(currentCamera.CameraSubject) + end) + + cameraTypeChangedConn = currentCamera:GetPropertyChangedSignal("CameraType"):Connect(function() + OnCameraTypeChanged(currentCamera.CameraType) + end) + + OnCameraSubjectChanged(currentCamera.CameraSubject) + OnCameraTypeChanged(currentCamera.CameraType) +end + +local function OnLocalPlayerCameraPropertyChanged(propertyName) + if propertyName == "CameraMode" then + -- CameraMode is only used to turn on/off forcing the player into first person view. The + -- Note: The case "Classic" is used for all other views and does not correspond only to the ClassicCamera module + if Players.LocalPlayer.CameraMode == Enum.CameraMode.LockFirstPerson then + -- Locked in first person, use ClassicCamera which supports this + if not activeCameraController or activeCameraController:GetModuleName() ~= "ClassicCamera" then + ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(Enum.DevComputerCameraMovementMode.Classic)) + end + + if activeCameraController then + activeCameraController:UpdateForDistancePropertyChange() + end + elseif Players.LocalPlayer.CameraMode == Enum.CameraMode.Classic then + -- Not locked in first person view + local cameraMovementMode = GetCameraMovementModeFromSettings() + ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode)) + else + warn("Unhandled value for property player.CameraMode: ",Players.LocalPlayer.CameraMode) + end + + elseif propertyName == "DevComputerCameraMode" or + propertyName == "DevTouchCameraMode" then + local cameraMovementMode = GetCameraMovementModeFromSettings() + ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode)) + + elseif propertyName == "DevCameraOcclusionMode" then + ActivateOcclusionModule(Players.LocalPlayer.DevCameraOcclusionMode) + + elseif propertyName == "CameraMinZoomDistance" or propertyName == "CameraMaxZoomDistance" then + if activeCameraController then + activeCameraController:UpdateForDistancePropertyChange() + end + elseif propertyName == "DevTouchMovementMode" then + + elseif propertyName == "DevComputerMovementMode" then + + elseif propertyName == "DevEnableMouseLock" then + -- This is the enabling/disabling of "Shift Lock" mode, not LockFirstPerson (which is a CameraMode) + + -- Note: Enabling and disabling of MouseLock mode is normally only a publish-time choice made via + -- the corresponding EnableMouseLockOption checkbox of StarterPlayer, and this script does not have + -- support for changing the availability of MouseLock at runtime (this would require listening to + -- Player.DevEnableMouseLock changes) + end +end + +local function OnUserGameSettingsPropertyChanged(propertyName) + + if propertyName == "ComputerCameraMovementMode" then + local cameraMovementMode = GetCameraMovementModeFromSettings() + ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode)) + + -- These remaining UserGameSettings properties are not currently used by camera scripts in Lua + -- and these cases could be removed. + elseif propertyName == "ComputerMovementMode" then + + elseif propertyName == "ControlMode" then + + elseif propertyName == "GamepadCameraSensitivity" then + + elseif propertyName == "MouseSensitivity" then + + elseif propertyName == "RotationType" then + + elseif propertyName == "TouchCameraMovementMode" then + + elseif propertyName == "TouchMovementMode" then + + end +end + +game.Workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(OnCurrentCameraChanged) + +-- Connect listeners to camera-related properties +for _, propertyName in pairs(PLAYER_CAMERA_PROPERTIES) do + Players.LocalPlayer:GetPropertyChangedSignal(propertyName):Connect(function() + OnLocalPlayerCameraPropertyChanged(propertyName) + end) +end + +for _, propertyName in pairs(USER_GAME_SETTINGS_PROPERTIES) do + UserGameSettings:GetPropertyChangedSignal(propertyName):Connect(function() + OnUserGameSettingsPropertyChanged(propertyName) + end) +end + +--[[ + Main RenderStep Update. The camera controller and occlusion module both have opportunities + to set and modify (respectively) the CFrame and Focus before it is set once on CurrentCamera. + The camera and occlusion modules should only return CFrames, not set the CFrame property of + CurrentCamera directly. +--]] +local function Update(dt) + local newCameraCFrame = nil + local newCameraFocus = nil + + if activeCameraController then + newCameraCFrame, newCameraFocus = activeCameraController:Update(dt) + activeCameraController:ApplyVRTransform() + else + newCameraCFrame = game.Workspace.CurrentCamera.CFrame + newCameraFocus = game.Workspace.CurrentCamera.Focus + end + + if activeOcclusionModule then + newCameraCFrame, newCameraFocus = activeOcclusionModule:Update(dt, newCameraCFrame, newCameraFocus) + end + + -- Here is where the new CFrame and Focus are set for this render frame + game.Workspace.CurrentCamera.CFrame = newCameraCFrame + game.Workspace.CurrentCamera.Focus = newCameraFocus + + -- Update to character local transparency as needed based on camera-to-subject distance + if activeTransparencyController then + activeTransparencyController:Update() + end +end + +-- [[ old module migrating code below this point ]] -- +local hasLastInput = false +local lastInputType = nil + +-- Formerly getCurrentCameraMode, this function resolves developer and user camera control settings to +-- decide which camera control module should be instantiated. The old method of converting redundant enum types +local function GetCameraControlChoice() + local player = Players.LocalPlayer + + if player then + if (hasLastInput and lastInputType == Enum.UserInputType.Touch) or UserInputService.TouchEnabled then + -- Touch + if player.DevTouchCameraMode == Enum.DevTouchCameraMovementMode.UserChoice then + return CameraUtils.ConvertCameraModeEnumToStandard( UserGameSettings.TouchCameraMovementMode ) + else + return CameraUtils.ConvertCameraModeEnumToStandard( player.DevTouchCameraMode ) + end + else + -- Computer + if player.DevComputerCameraMode == Enum.DevComputerCameraMovementMode.UserChoice then + local computerMovementMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.ComputerCameraMovementMode) + return CameraUtils.ConvertCameraModeEnumToStandard(computerMovementMode) + else + return CameraUtils.ConvertCameraModeEnumToStandard(player.DevComputerCameraMode) + end + end + end +end + +hasLastInput = pcall(function() + lastInputType = UserInputService:GetLastInputType() + UserInputService.LastInputTypeChanged:Connect(function(newLastInputType) + lastInputType = newLastInputType + end) +end) + +local function OnCharacterAdded(player, char) + if activeOcclusionModule then + activeOcclusionModule:CharacterAdded(player, char) + end +end + +local function OnCharacterRemoving(player, char) + if activeOcclusionModule then + activeOcclusionModule:CharacterRemoving(player, char) + end +end + +local function OnPlayerAdded(player) + player.CharacterAdded:Connect(function(character) OnCharacterAdded(player, character) end) + player.CharacterRemoving:Connect(function(character) OnCharacterRemoving(player, character) end) +end + +local function OnMouseLockToggled() + if activeMouseLockController then + local mouseLocked = activeMouseLockController:GetIsMouseLocked() + local mouseLockOffset = activeMouseLockController:GetMouseLockOffset() + if activeCameraController then + activeCameraController:SetIsMouseLocked(mouseLocked) + activeCameraController:SetMouseLockOffset(mouseLockOffset) + end + end +end + +OnPlayerAdded(Players.LocalPlayer) + +activeTransparencyController = TransparencyController.new() +activeTransparencyController:Enable(true) + +if not UserInputService.TouchEnabled then + activeMouseLockController = MouseLockController.new() + local toggleEvent = activeMouseLockController:GetBindableToggleEvent() + if toggleEvent then + toggleEvent:Connect(OnMouseLockToggled) + end +end + +ActivateCameraController(GetCameraControlChoice()) +ActivateOcclusionModule(Players.LocalPlayer.DevCameraOcclusionMode) +OnCurrentCameraChanged() -- Does initializations and makes first camera controller +RunService:BindToRenderStep("cameraRenderUpdate", Enum.RenderPriority.Camera.Value, Update) + +function CameraScript:GetActiveCameraController() + return activeCameraController +end + +return CameraScript diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/BaseCamera.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/BaseCamera.lua new file mode 100644 index 0000000..ab2d591 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/BaseCamera.lua @@ -0,0 +1,1504 @@ +--[[ + BaseCamera - Abstract base class for camera control modules + 2018 Camera Update - AllYourBlox +--]] + +--[[ Local Constants ]]-- +local UNIT_Z = Vector3.new(0,0,1) +local X1_Y0_Z1 = Vector3.new(1,0,1) --Note: not a unit vector, used for projecting onto XZ plane + +local THUMBSTICK_DEADZONE = 0.2 +local DEFAULT_DISTANCE = 12.5 -- Studs +local PORTRAIT_DEFAULT_DISTANCE = 25 -- Studs +local FIRST_PERSON_DISTANCE_THRESHOLD = 1.0 -- Below this value, snap into first person + +-- Note: DotProduct check in CoordinateFrame::lookAt() prevents using values within about +-- 8.11 degrees of the +/- Y axis, that's why these limits are currently 80 degrees +local MIN_Y = math.rad(-80) +local MAX_Y = math.rad(80) + +local VR_ANGLE = math.rad(15) +local VR_LOW_INTENSITY_ROTATION = Vector2.new(math.rad(15), 0) +local VR_HIGH_INTENSITY_ROTATION = Vector2.new(math.rad(45), 0) +local VR_LOW_INTENSITY_REPEAT = 0.1 +local VR_HIGH_INTENSITY_REPEAT = 0.4 + +local ZERO_VECTOR2 = Vector2.new(0,0) +local ZERO_VECTOR3 = Vector3.new(0,0,0) + +local TOUCH_SENSITIVTY = Vector2.new( 0.002 * math.pi, 0.0015 * math.pi) +local MOUSE_SENSITIVITY = Vector2.new( 0.002 * math.pi, 0.0015 * math.pi ) + +local MAX_TIME_FOR_DOUBLE_TAP = 1.5 +local MAX_TAP_POS_DELTA = 15 +local MAX_TAP_TIME_DELTA = 0.75 + +local SEAT_OFFSET = Vector3.new(0,5,0) +local VR_SEAT_OFFSET = Vector3.new(0,4,0) +local HEAD_OFFSET = Vector3.new(0,1.5,0) +local R15_HEAD_OFFSET = Vector3.new(0,2,0) + +local Util = require(script.Parent:WaitForChild("CameraUtils")) + +--[[ Roblox Services ]]-- +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local StarterGui = game:GetService("StarterGui") +local GuiService = game:GetService("GuiService") +local ContextActionService = game:GetService("ContextActionService") +local VRService = game:GetService("VRService") +local UserGameSettings = UserSettings():GetService("UserGameSettings") + +--[[ The Module ]]-- +local BaseCamera = {} +BaseCamera.__index = BaseCamera + +function BaseCamera.new() + local self = setmetatable({}, BaseCamera) + + -- So that derived classes have access to this + self.FIRST_PERSON_DISTANCE_THRESHOLD = FIRST_PERSON_DISTANCE_THRESHOLD + + self.cameraType = nil + self.cameraMovementMode = nil + + local player = Players.LocalPlayer + self.lastCameraTransform = nil + self.rotateInput = ZERO_VECTOR2 + self.userPanningCamera = false + self.lastUserPanCamera = tick() + + self.humanoidRootPart = nil + self.humanoidCache = {} + + -- Subject and position on last update call + self.lastSubject = nil + self.lastSubjectPosition = Vector3.new(0,5,0) + + -- These subject distance members refer to the nominal camera-to-subject follow distance that the camera + -- is trying to maintain, not the actual measured value. + -- The default is updated when screen orientation or the min/max distances change, + -- to be sure the default is always in range and appropriate for the orientation. + self.defaultSubjectDistance = Util.Clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, DEFAULT_DISTANCE) + self.currentSubjectDistance = Util.Clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, DEFAULT_DISTANCE) + + self.inFirstPerson = false + self.inMouseLockedMode = false + self.portraitMode = false + + self.enabled = false + + -- Input Event Connections + self.inputBeganConn = nil + self.inputChangedConn = nil + self.inputEndedConn = nil + + self.startPos = nil + self.lastPos = nil + self.panBeginLook = nil + + self.panEnabled = true + self.keyPanEnabled = true + self.distanceChangeEnabled = true + + self.PlayerGui = nil + + self.cameraChangedConn = nil + self.viewportSizeChangedConn = nil + + -- VR Support + self.shouldUseVRRotation = false + self.VRRotationIntensityAvailable = false + self.lastVRRotationIntensityCheckTime = 0 + self.lastVRRotationTime = 0 + self.vrRotateKeyCooldown = {} + self.cameraTranslationConstraints = Vector3.new(1, 1, 1) + self.humanoidJumpOrigin = nil + self.trackingHumanoid = nil + self.cameraFrozen = false + self.headHeightR15 = R15_HEAD_OFFSET + self.heightScaleChangedConn = nil + self.subjectStateChangedConn = nil + self.humanoidChildAddedConn = nil + self.humanoidChildRemovedConn = nil + + -- Gamepad support + self.activeGamepad = nil + self.gamepadPanningCamera = false + self.lastThumbstickRotate = nil + self.numOfSeconds = 0.7 + self.currentSpeed = 0 + self.maxSpeed = 6 + self.vrMaxSpeed = 4 + self.lastThumbstickPos = Vector2.new(0,0) + self.ySensitivity = 0.65 + self.lastVelocity = nil + self.gamepadConnectedConn = nil + self.gamepadDisconnectedConn = nil + self.currentZoomSpeed = 1.0 + self.L3ButtonDown = false + self.dpadLeftDown = false + self.dpadRightDown = false + + -- Touch input support + self.isDynamicThumbstickEnabled = false + self.fingerTouches = {} + self.numUnsunkTouches = 0 + self.inputStartPositions = {} + self.inputStartTimes = {} + self.startingDiff = nil + self.pinchBeginZoom = nil + self.userPanningTheCamera = false + self.touchActivateConn = nil + + -- Mouse locked formerly known as shift lock mode + self.mouseLockOffset = ZERO_VECTOR3 + + -- [[ NOTICE ]] -- + -- Initialization things used to always execute at game load time, but now these camera modules are instantiated + -- when needed, so the code here may run well after the start of the game + + if player.Character then + self:OnCharacterAdded(player.Character) + end + + player.CharacterAdded:Connect(function(char) + self:OnCharacterAdded(char) + end) + + if self.cameraChangedConn then self.cameraChangedConn:Disconnect() end + self.cameraChangedConn = workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function() + self:OnCurrentCameraChanged() + end) + + if self.playerCameraModeChangeConn then self.playerCameraModeChangeConn:Disconnect() end + self.playerCameraModeChangeConn = player:GetPropertyChangedSignal("CameraMode"):Connect(function() + self:OnPlayerCameraPropertyChange() + end) + + if self.minDistanceChangeConn then self.minDistanceChangeConn:Disconnect() end + self.minDistanceChangeConn = player:GetPropertyChangedSignal("CameraMinZoomDistance"):Connect(function() + self:OnPlayerCameraPropertyChange() + end) + + if self.maxDistanceChangeConn then self.maxDistanceChangeConn:Disconnect() end + self.maxDistanceChangeConn = player:GetPropertyChangedSignal("CameraMaxZoomDistance"):Connect(function() + self:OnPlayerCameraPropertyChange() + end) + + if self.playerDevTouchMoveModeChangeConn then self.playerDevTouchMoveModeChangeConn:Disconnect() end + self.playerDevTouchMoveModeChangeConn = player:GetPropertyChangedSignal("DevTouchMovementMode"):Connect(function() + self:OnDevTouchMovementModeChanged() + end) + self:OnDevTouchMovementModeChanged() -- Init + + if self.gameSettingsTouchMoveMoveChangeConn then self.gameSettingsTouchMoveMoveChangeConn:Disconnect() end + self.gameSettingsTouchMoveMoveChangeConn = UserGameSettings:GetPropertyChangedSignal("TouchMovementMode"):Connect(function() + self:OnGameSettingsTouchMovementModeChanged() + end) + self:OnGameSettingsTouchMovementModeChanged() -- Init + + UserGameSettings:SetCameraYInvertVisible() + UserGameSettings:SetGamepadCameraSensitivityVisible() + + self.hasGameLoaded = game:IsLoaded() + if not self.hasGameLoaded then + self.gameLoadedConn = game.Loaded:Connect(function() + self.hasGameLoaded = true + self.gameLoadedConn:Disconnect() + self.gameLoadedConn = nil + end) + end + + return self +end + +function BaseCamera:GetModuleName() + return "BaseCamera" +end + +function BaseCamera:OnCharacterAdded(char) + if UserInputService.TouchEnabled then + self.PlayerGui = Players.LocalPlayer:WaitForChild("PlayerGui") + if self.PlayerGui then + local screenGui = Instance.new("ScreenGui") + screenGui.Parent = self.PlayerGui + end + for _, child in ipairs(char:GetChildren()) do + if child:IsA("Tool") then + self.isAToolEquipped = true + end + end + char.ChildAdded:Connect(function(child) + if child:IsA("Tool") then + self.isAToolEquipped = true + end + end) + char.ChildRemoved:Connect(function(child) + if child:IsA("Tool") then + self.isAToolEquipped = false + end + end) + end +end + +function BaseCamera:GetHumanoidRootPart() + if not self.humanoidRootPart then + local player = Players.LocalPlayer + if player.Character then + local humanoid = player.Character:FindFirstChildOfClass("Humanoid") + if humanoid then + self.humanoidRootPart = humanoid.RootPart + end + end + end + return self.humanoidRootPart +end + +function BaseCamera:GetBodyPartToFollow(humanoid, isDead) + -- If the humanoid is dead, prefer the head part if one still exists as a sibling of the humanoid + if humanoid:GetState() == Enum.HumanoidStateType.Dead then + local character = humanoid.Parent + if character and character:IsA("Model") then + return character:FindFirstChild("Head") or humanoid.RootPart + end + end + + return humanoid.RootPart +end + +function BaseCamera:GetSubjectPosition() + local result = self.lastSubjectPosition + local camera = game.Workspace.CurrentCamera + local cameraSubject = camera and camera.CameraSubject + + if cameraSubject then + if cameraSubject:IsA("Humanoid") then + local humanoid = cameraSubject + local humanoidIsDead = humanoid:GetState() == Enum.HumanoidStateType.Dead + + if VRService.VREnabled and humanoidIsDead and humanoid == self.lastSubject then + result = self.lastSubjectPosition + else + local bodyPartToFollow = humanoid.RootPart + + -- If the humanoid is dead, prefer their head part as a follow target, if it exists + if humanoidIsDead then + if humanoid.Parent and humanoid.Parent:IsA("Model") then + bodyPartToFollow = humanoid.Parent:FindFirstChild("Head") or bodyPartToFollow + end + end + + if bodyPartToFollow and bodyPartToFollow:IsA("BasePart") then + local heightOffset = humanoid.RigType == Enum.HumanoidRigType.R15 and R15_HEAD_OFFSET or HEAD_OFFSET + if humanoidIsDead then + heightOffset = ZERO_VECTOR3 + end + + result = bodyPartToFollow.CFrame.p + bodyPartToFollow.CFrame:vectorToWorldSpace(heightOffset + humanoid.CameraOffset) + end + end + + elseif cameraSubject:IsA("VehicleSeat") then + local offset = SEAT_OFFSET + if VRService.VREnabled then + offset = VR_SEAT_OFFSET + end + result = cameraSubject.CFrame.p + cameraSubject.CFrame:vectorToWorldSpace(offset) + elseif cameraSubject:IsA("SkateboardPlatform") then + result = cameraSubject.CFrame.p + SEAT_OFFSET + elseif cameraSubject:IsA("BasePart") then + result = cameraSubject.CFrame.p + elseif cameraSubject:IsA("Model") then + if cameraSubject.PrimaryPart then + result = cameraSubject:GetPrimaryPartCFrame().p + else + result = cameraSubject:GetModelCFrame().p + end + end + else + -- cameraSubject is nil + -- Note: Previous RootCamera did not have this else case and let self.lastSubject and self.lastSubjectPosition + -- both get set to nil in the case of cameraSubject being nil. This function now exits here to preserve the + -- last set valid values for these, as nil values are not handled cases + return + end + + self.lastSubject = cameraSubject + self.lastSubjectPosition = result + + return result +end + +function BaseCamera:UpdateDefaultSubjectDistance() + local player = Players.LocalPlayer + if self.portraitMode then + self.defaultSubjectDistance = Util.Clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, PORTRAIT_DEFAULT_DISTANCE) + else + self.defaultSubjectDistance = Util.Clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, DEFAULT_DISTANCE) + end +end + +function BaseCamera:OnViewportSizeChanged() + local camera = game.Workspace.CurrentCamera + local size = camera.ViewportSize + self.portraitMode = size.X < size.Y + self:UpdateDefaultSubjectDistance() +end + +-- Listener for changes to workspace.CurrentCamera +function BaseCamera:OnCurrentCameraChanged() + if UserInputService.TouchEnabled then + if self.viewportSizeChangedConn then + self.viewportSizeChangedConn:Disconnect() + self.viewportSizeChangedConn = nil + end + + local newCamera = game.Workspace.CurrentCamera + + if newCamera then + self:OnViewportSizeChanged() + self.viewportSizeChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(function() + self:OnViewportSizeChanged() + end) + end + end + + -- VR support additions + if self.cameraSubjectChangedConn then + self.cameraSubjectChangedConn:Disconnect() + self.cameraSubjectChangedConn = nil + end + + local camera = game.Workspace.CurrentCamera + if camera then + self.cameraSubjectChangedConn = camera:GetPropertyChangedSignal("CameraSubject"):Connect(function() + self:OnNewCameraSubject() + end) + self:OnNewCameraSubject() + end +end + +function BaseCamera:OnDynamicThumbstickEnabled() + if UserInputService.TouchEnabled then + self.isDynamicThumbstickEnabled = true + end +end + +function BaseCamera:OnDynamicThumbstickDisabled() + self.isDynamicThumbstickEnabled = false +end + +function BaseCamera:OnGameSettingsTouchMovementModeChanged() + if Players.LocalPlayer.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice then + if UserGameSettings.TouchMovementMode.Name == "DynamicThumbstick" then + self:OnDynamicThumbstickEnabled() + else + self:OnDynamicThumbstickDisabled() + end + end +end + +function BaseCamera:OnDevTouchMovementModeChanged() + if Players.LocalPlayer.DevTouchMovementMode.Name == "DynamicThumbstick" then + self:OnDynamicThumbstickEnabled() + else + self:OnGameSettingsTouchMovementModeChanged() + end +end + +function BaseCamera:OnPlayerCameraPropertyChange() + -- This call forces re-evaluation of player.CameraMode and clamping to min/max distance which may have changed + self:SetCameraToSubjectDistance(self.currentSubjectDistance) +end + +function BaseCamera:GetCameraHeight() + if VRService.VREnabled and not self.inFirstPerson then + return math.sin(VR_ANGLE) * self.currentSubjectDistance + end + return 0 +end + +function BaseCamera:InputTranslationToCameraAngleChange(translationVector, sensitivity) + local camera = game.Workspace.CurrentCamera + if camera and camera.ViewportSize.X > 0 and camera.ViewportSize.Y > 0 and (camera.ViewportSize.Y > camera.ViewportSize.X) then + -- Screen has portrait orientation, swap X and Y sensitivity + return translationVector * Vector2.new( sensitivity.Y, sensitivity.X) + end + return translationVector * sensitivity +end + +function BaseCamera:Enable(enable) + if self.enabled ~= enable then + self.enabled = enable + if self.enabled then + self:ConnectInputEvents() + + if Players.LocalPlayer.CameraMode == Enum.CameraMode.LockFirstPerson then + self.currentSubjectDistance = 0.5 + if not self.inFirstPerson then + self:EnterFirstPerson() + end + end + else + self:DisconnectInputEvents() + -- Clean up additional event listeners and reset a bunch of properties + self:Cleanup() + end + end +end + +function BaseCamera:GetEnabled() + return self.enabled +end + +function BaseCamera:OnInputBegan(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + self:OnTouchBegan(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseButton2 then + self:OnMouse2Down(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseButton3 then + self:OnMouse3Down(input, processed) + end + -- Keyboard + if input.UserInputType == Enum.UserInputType.Keyboard then + self:OnKeyDown(input, processed) + end +end + +function BaseCamera:OnInputChanged(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + self:OnTouchChanged(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseMovement then + self:OnMouseMoved(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseWheel then + self:OnMouseWheel(input, processed) + end +end + +function BaseCamera:OnInputEnded(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + self:OnTouchEnded(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseButton2 then + self:OnMouse2Up(input, processed) + elseif input.UserInputType == Enum.UserInputType.MouseButton3 then + self:OnMouse3Up(input, processed) + end + -- Keyboard + if input.UserInputType == Enum.UserInputType.Keyboard then + self:OnKeyUp(input, processed) + end +end + +function BaseCamera:ConnectInputEvents() + self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed) + self:OnInputBegan(input, processed) + end) + + self.inputChangedConn = UserInputService.InputChanged:Connect(function(input, processed) + self:OnInputChanged(input, processed) + end) + + self.inputEndedConn = UserInputService.InputEnded:Connect(function(input, processed) + self:OnInputEnded(input, processed) + end) + + self.touchActivateConn = UserInputService.TouchTapInWorld:Connect(function(touchPos, processed) + self:OnTouchTap(touchPos) + end) + + self.menuOpenedConn = GuiService.MenuOpened:connect(function() + self:ResetInputStates() + end) + + self.gamepadConnectedConn = UserInputService.GamepadDisconnected:connect(function(gamepadEnum) + if self.activeGamepad ~= gamepadEnum then return end + self.activeGamepad = nil + self:AssignActivateGamepad() + end) + + self.gamepadDisconnectedConn = UserInputService.GamepadConnected:connect(function(gamepadEnum) + if self.activeGamepad == nil then + self:AssignActivateGamepad() + end + end) + + self:BindGamepadInputActions() + self:AssignActivateGamepad() + self:UpdateMouseBehavior() +end + +function BaseCamera:AssignActivateGamepad() + local connectedGamepads = UserInputService:GetConnectedGamepads() + if #connectedGamepads > 0 then + for i = 1, #connectedGamepads do + if self.activeGamepad == nil then + self.activeGamepad = connectedGamepads[i] + elseif connectedGamepads[i].Value < self.activeGamepad.Value then + self.activeGamepad = connectedGamepads[i] + end + end + end + + if self.activeGamepad == nil then -- nothing is connected, at least set up for gamepad1 + self.activeGamepad = Enum.UserInputType.Gamepad1 + end +end + +function BaseCamera:DisconnectInputEvents() + if self.inputBeganConn then + self.inputBeganConn:Disconnect() + self.inputBeganConn = nil + end + if self.inputChangedConn then + self.inputChangedConn:Disconnect() + self.inputChangedConn = nil + end + if self.inputEndedConn then + self.inputEndedConn:Disconnect() + self.inputEndedConn = nil + end +end + +function BaseCamera:Cleanup() + if self.menuOpenedConn then + self.menuOpenedConn:Disconnect() + self.menuOpenedConn = nil + end + if self.mouseLockToggleConn then + self.mouseLockToggleConn:Disconnect() + self.mouseLockToggleConn = nil + end + if self.gamepadConnectedConn then + self.gamepadConnectedConn:Disconnect() + self.gamepadConnectedConn = nil + end + if self.gamepadDisconnectedConn then + self.gamepadDisconnectedConn:Disconnect() + self.gamepadDisconnectedConn = nil + end + if self.subjectStateChangedConn then + self.subjectStateChangedConn:Disconnect() + self.subjectStateChangedConn = nil + end + if self.viewportSizeChangedConn then + self.viewportSizeChangedConn:Disconnect() + self.viewportSizeChangedConn = nil + end + if self.touchActivateConn then + self.touchActivateConn:Disconnect() + self.touchActivateConn = nil + end + + self.turningLeft = false + self.turningRight = false + self.lastCameraTransform = nil + self.lastSubjectCFrame = nil + self.userPanningTheCamera = false + self.rotateInput = Vector2.new() + self.gamepadPanningCamera = Vector2.new(0,0) + + -- Reset input states + self.startPos = nil + self.lastPos = nil + self.panBeginLook = nil + self.isRightMouseDown = false + self.isMiddleMouseDown = false + + self.fingerTouches = {} + self.numUnsunkTouches = 0 + + self.startingDiff = nil + self.pinchBeginZoom = nil + + -- Unlock mouse for example if right mouse button was being held down + if UserInputService.MouseBehavior ~= Enum.MouseBehavior.LockCenter then + UserInputService.MouseBehavior = Enum.MouseBehavior.Default + end +end + +-- This is called when settings menu is opened +function BaseCamera:ResetInputStates() + self.isRightMouseDown = false + self.isMiddleMouseDown = false + self:OnMousePanButtonReleased() -- this function doesn't seem to actually need parameters + + if UserInputService.TouchEnabled then + --[[menu opening was causing serious touch issues + this should disable all active touch events if + they're active when menu opens.]] + for inputObject in pairs(self.fingerTouches) do + self.fingerTouches[inputObject] = nil + end + self.panBeginLook = nil + self.startPos = nil + self.lastPos = nil + self.userPanningTheCamera = false + self.startingDiff = nil + self.pinchBeginZoom = nil + self.numUnsunkTouches = 0 + end +end + +function BaseCamera:GetGamepadPan(name, state, input) + if input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then +-- if self.L3ButtonDown then +-- -- L3 Thumbstick is depressed, right stick controls dolly in/out +-- if (input.Position.Y > THUMBSTICK_DEADZONE) then +-- self.currentZoomSpeed = 0.96 +-- elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then +-- self.currentZoomSpeed = 1.04 +-- else +-- self.currentZoomSpeed = 1.00 +-- end +-- else + if state == Enum.UserInputState.Cancel then + self.gamepadPanningCamera = ZERO_VECTOR2 + return + end + + local inputVector = Vector2.new(input.Position.X, -input.Position.Y) + if inputVector.magnitude > THUMBSTICK_DEADZONE then + self.gamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y) + else + self.gamepadPanningCamera = ZERO_VECTOR2 + end + --end + end +end + +function BaseCamera:DoGamepadZoom(name, state, input) + if input.UserInputType == self.activeGamepad then + if input.KeyCode == Enum.KeyCode.ButtonR3 then + if state == Enum.UserInputState.Begin then + if self.distanceChangeEnabled then + if self:GetCameraToSubjectDistance() > 0.5 then + self:SetCameraToSubjectDistance(0) + else + self:SetCameraToSubjectDistance(10) + end + end + end + elseif input.KeyCode == Enum.KeyCode.DPadLeft then + self.dpadLeftDown = (state == Enum.UserInputState.Begin) + elseif input.KeyCode == Enum.KeyCode.DPadRight then + self.dpadRightDown = (state == Enum.UserInputState.Begin) + end + + if self.dpadLeftDown then + self.currentZoomSpeed = 1.04 + elseif self.dpadRightDown then + self.currentZoomSpeed = 0.96 + else + self.currentZoomSpeed = 1.00 + end + end +-- elseif input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.ButtonL3 then +-- if (state == Enum.UserInputState.Begin) then +-- self.L3ButtonDown = true +-- elseif (state == Enum.UserInputState.End) then +-- self.L3ButtonDown = false +-- self.currentZoomSpeed = 1.00 +-- end +-- end +end + +function BaseCamera:BindGamepadInputActions() + ContextActionService:BindAction("RootCamGamepadPan", function(name, state, input) self:GetGamepadPan(name, state, input) end, false, Enum.KeyCode.Thumbstick2) + ContextActionService:BindAction("RootCamGamepadZoom", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.ButtonR3) + --ContextActionService:BindAction("RootGamepadZoomAlt", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.ButtonL3) + ContextActionService:BindAction("RootGamepadZoomOut", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.DPadLeft) + ContextActionService:BindAction("RootGamepadZoomIn", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.DPadRight) +end + +function BaseCamera:OnTouchBegan(input, processed) + local canUseDynamicTouch = self.isDynamicThumbstickEnabled and not processed + if canUseDynamicTouch then + self.fingerTouches[input] = processed + if not processed then + self.inputStartPositions[input] = input.Position + self.inputStartTimes[input] = tick() + self.numUnsunkTouches = self.numUnsunkTouches + 1 + end + end +end + +function BaseCamera:OnTouchChanged(input, processed) + if self.fingerTouches[input] == nil then + if self.isDynamicThumbstickEnabled then + return + end + self.fingerTouches[input] = processed + if not processed then + self.numUnsunkTouches = self.numUnsunkTouches + 1 + end + end + + if self.numUnsunkTouches == 1 then + if self.fingerTouches[input] == false then + self.panBeginLook = self.panBeginLook or self:GetCameraLookVector() + self.startPos = self.startPos or input.Position + self.lastPos = self.lastPos or self.startPos + self.userPanningTheCamera = true + + local delta = input.Position - self.lastPos + delta = Vector2.new(delta.X, delta.Y * UserGameSettings:GetCameraYInvertValue()) + if self.panEnabled then + local desiredXYVector = self:InputTranslationToCameraAngleChange(delta, TOUCH_SENSITIVTY) + self.rotateInput = self.rotateInput + desiredXYVector + end + self.lastPos = input.Position + end + else + self.panBeginLook = nil + self.startPos = nil + self.lastPos = nil + self.userPanningTheCamera = false + end + if self.numUnsunkTouches == 2 then + local unsunkTouches = {} + for touch, wasSunk in pairs(self.fingerTouches) do + if not wasSunk then + table.insert(unsunkTouches, touch) + end + end + if #unsunkTouches == 2 then + local difference = (unsunkTouches[1].Position - unsunkTouches[2].Position).magnitude + if self.startingDiff and self.pinchBeginZoom then + local scale = difference / math.max(0.01, self.startingDiff) + local clampedScale = Util.Clamp(0.1, 10, scale) + if self.distanceChangeEnabled then + self:SetCameraToSubjectDistance(self.pinchBeginZoom / clampedScale) + end + else + self.startingDiff = difference + self.pinchBeginZoom = self:GetCameraToSubjectDistance() + end + end + else + self.startingDiff = nil + self.pinchBeginZoom = nil + end +end + +function BaseCamera:CalcLookBehindRotateInput() + if not self.humanoidRootPart or not game.Workspace.CurrentCamera then + return nil + end + + local cameraLookVector = game.Workspace.CurrentCamera.CFrame.lookVector + local newDesiredLook = (self.humanoidRootPart.CFrame.lookVector - Vector3.new(0,0.23,0)).unit + local horizontalShift = Util.GetAngleBetweenXZVectors(newDesiredLook, cameraLookVector) + local vertShift = math.asin(cameraLookVector.Y) - math.asin(newDesiredLook.Y) + if not Util.IsFinite(horizontalShift) then + horizontalShift = 0 + end + if not Util.IsFinite(vertShift) then + vertShift = 0 + end + + return Vector2.new(horizontalShift, vertShift) +end + +function BaseCamera:OnTouchTap(position) + if self.isDynamicThumbstickEnabled and not self.isAToolEquipped then + if self.lastTapTime and tick() - self.lastTapTime < MAX_TIME_FOR_DOUBLE_TAP then +-- local tween = { +-- from = self:GetCameraToSubjectDistance(), +-- to = self.defaultSubjectDistance, +-- start = tick(), +-- duration = 0.2, +-- func = function(from, to, alpha) +-- self:SetCameraToSubjectDistance(from + (to - from)*alpha) +-- return to +-- end +-- } +-- tweens["Zoom"] = tween + self:SetCameraToSubjectDistance(self.defaultSubjectDistance) + else + if self.humanoidRootPart then + -- TODO: Replace this with proper tween that does not fight with user input + -- this overrides any actual user input with a rotate input amount to get the + -- camera looking from behind the character + self.rotateInput = self:CalcLookBehindRotateInput() + end + +-- local humanoid = self:GetHumanoid() +-- if humanoid then +-- local player = Players.LocalPlayer +-- if player and player.Character then +-- if humanoid and humanoid.RootPart then +-- local tween = { +-- from = this.RotateInput, +-- to = calcLookBehindRotateInput(humanoid.Torso), +-- start = tick(), +-- duration = 0.2, +-- func = function(from, to, alpha) +-- to = calcLookBehindRotateInput(humanoid.Torso) +-- if to then +-- this.RotateInput = from + (to - from)*alpha +-- end +-- return to +-- end +-- } +-- tweens["Rotate"] = tween +-- +-- -- reset old camera info so follow cam doesn't rotate us +-- this.LastCameraTransform = nil +-- end +-- end +-- end + end + self.lastTapTime = tick() + end +end + +function BaseCamera:IsTouchTap(input) + -- We can't make the assumption that the input exists in the inputStartPositions because we may have switched from a different camera type. + if self.inputStartPositions[input] then + local posDelta = (self.inputStartPositions[input] - input.Position).magnitude + if posDelta < MAX_TAP_POS_DELTA then + local timeDelta = self.inputStartTimes[input] - tick() + if timeDelta < MAX_TAP_TIME_DELTA then + return true + end + end + end + return false +end + +function BaseCamera:OnTouchEnded(input, processed) + if self.fingerTouches[input] == false then + if self.numUnsunkTouches == 1 then + self.panBeginLook = nil + self.startPos = nil + self.lastPos = nil + self.userPanningTheCamera = false + if self:IsTouchTap(input) then + self:OnTouchTap(input.Position) + end + elseif self.numUnsunkTouches == 2 then + self.startingDiff = nil + self.pinchBeginZoom = nil + end + end + + if self.fingerTouches[input] ~= nil and self.fingerTouches[input] == false then + self.numUnsunkTouches = self.numUnsunkTouches - 1 + end + self.fingerTouches[input] = nil + self.inputStartPositions[input] = nil + self.inputStartTimes[input] = nil +end + +function BaseCamera:OnMouse2Down(input, processed) + if processed then return end + + self.isRightMouseDown = true + self:OnMousePanButtonPressed(input, processed) +end + +function BaseCamera:OnMouse2Up(input, processed) + self.isRightMouseDown = false + self:OnMousePanButtonReleased(input, processed) +end + +function BaseCamera:OnMouse3Down(input, processed) + if processed then return end + + self.isMiddleMouseDown = true + self:OnMousePanButtonPressed(input, processed) +end + +function BaseCamera:OnMouse3Up(input, processed) + self.isMiddleMouseDown = false + self:OnMousePanButtonReleased(input, processed) +end + +function BaseCamera:OnMouseMoved(input, processed) + if not self.hasGameLoaded and VRService.VREnabled then + return + end + + local inputDelta = input.Delta + inputDelta = Vector2.new(inputDelta.X, inputDelta.Y * UserGameSettings:GetCameraYInvertValue()) + + if self.panEnabled and ((self.startPos and self.lastPos and self.panBeginLook) or self.inFirstPerson or self.inMouseLockedMode) then + local desiredXYVector = self:InputTranslationToCameraAngleChange(inputDelta,MOUSE_SENSITIVITY) + self.rotateInput = self.rotateInput + desiredXYVector + end + + if self.startPos and self.lastPos and self.panBeginLook then + self.lastPos = self.lastPos + input.Delta + end +end + +function BaseCamera:OnMousePanButtonPressed(input, processed) + if processed then return end + self:UpdateMouseBehavior() + self.panBeginLook = self.panBeginLook or self:GetCameraLookVector() + self.startPos = self.startPos or input.Position + self.lastPos = self.lastPos or self.startPos + self.userPanningTheCamera = true +end + +function BaseCamera:OnMousePanButtonReleased(input, processed) + self:UpdateMouseBehavior() + if not (self.isRightMouseDown or self.isMiddleMouseDown) then + self.panBeginLook = nil + self.startPos = nil + self.lastPos = nil + self.userPanningTheCamera = false + end +end + +function BaseCamera:OnMouseWheel(input, processed) + if not self.hasGameLoaded and VRService.VREnabled then + return + end + if not processed then + if self.distanceChangeEnabled then + local wheelInput = Util.Clamp(-1, 1, -input.Position.Z) + + local newDistance + if self.inFirstPerson and wheelInput > 0 then + newDistance = FIRST_PERSON_DISTANCE_THRESHOLD + else + -- The 0.156 and 1.7 values are the slope and intercept of a line that is replacing the old + -- rk4Integrator function which was not being used as an integrator, only to get a delta as a function of distance, + -- which was linear as it was being used. These constants preserve the status quo behavior. + newDistance = self.currentSubjectDistance + 0.156 * self.currentSubjectDistance * wheelInput + 1.7 * math.sign(wheelInput) + end + + self:SetCameraToSubjectDistance(newDistance) + end + end +end + +function BaseCamera:OnKeyDown(input, processed) + if not self.hasGameLoaded and VRService.VREnabled then + return + end + + if processed then + return + end + + if self.distanceChangeEnabled then + if input.KeyCode == Enum.KeyCode.I then + self:SetCameraToSubjectDistance( self.currentSubjectDistance - 5 ) + elseif input.KeyCode == Enum.KeyCode.O then + self:SetCameraToSubjectDistance( self.currentSubjectDistance + 5 ) + end + end + + if self.panBeginLook == nil and self.keyPanEnabled then + if input.KeyCode == Enum.KeyCode.Left then + self.turningLeft = true + elseif input.KeyCode == Enum.KeyCode.Right then + self.turningRight = true + elseif input.KeyCode == Enum.KeyCode.Comma then + local angle = Util.RotateVectorByAngleAndRound(self:GetCameraLookVector() * Vector3.new(1,0,1), -math.pi*0.1875, math.pi*0.25) + if angle ~= 0 then + self.rotateInput = self.rotateInput + Vector2.new(angle, 0) + self.lastUserPanCamera = tick() + self.lastCameraTransform = nil + end + elseif input.KeyCode == Enum.KeyCode.Period then + local angle = Util.RotateVectorByAngleAndRound(self:GetCameraLookVector() * Vector3.new(1,0,1), math.pi*0.1875, math.pi*0.25) + if angle ~= 0 then + self.rotateInput = self.rotateInput + Vector2.new(angle, 0) + self.lastUserPanCamera = tick() + self.lastCameraTransform = nil + end + elseif input.KeyCode == Enum.KeyCode.PageUp then + self.rotateInput = self.rotateInput + Vector2.new(0,math.rad(15)) + self.lastCameraTransform = nil + elseif input.KeyCode == Enum.KeyCode.PageDown then + self.rotateInput = self.rotateInput + Vector2.new(0,math.rad(-15)) + self.lastCameraTransform = nil + end + end +end + +function BaseCamera:OnKeyUp(input, processed) + if input.KeyCode == Enum.KeyCode.Left then + self.turningLeft = false + elseif input.KeyCode == Enum.KeyCode.Right then + self.turningRight = false + end +end + +function BaseCamera:UpdateMouseBehavior() + -- first time transition to first person mode or mouse-locked third person + if self.inFirstPerson or self.inMouseLockedMode then + pcall(function() UserGameSettings.RotationType = Enum.RotationType.CameraRelative end) + if UserInputService.MouseBehavior ~= Enum.MouseBehavior.LockCenter then + UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter + end + else + pcall(function() UserGameSettings.RotationType = Enum.RotationType.MovementRelative end) + if self.isRightMouseDown or self.isMiddleMouseDown then + UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition + else + UserInputService.MouseBehavior = Enum.MouseBehavior.Default + end + end +end + +function BaseCamera:UpdateForDistancePropertyChange() + -- Calling this setter with the current value will force checking that it is still + -- in range after a change to the min/max distance limits + self:SetCameraToSubjectDistance(self.currentSubjectDistance) +end + +function BaseCamera:SetCameraToSubjectDistance(desiredSubjectDistance) + local player = Players.LocalPlayer + + -- By default, camera modules will respect LockFirstPerson and override the currentSubjectDistance with 0 + -- regardless of what Player.CameraMinZoomDistance is set to, so that first person can be made + -- available by the developer without needing to allow players to mousewheel dolly into first person. + -- Some modules will override this function to remove or change first-person capability. + if player.CameraMode == Enum.CameraMode.LockFirstPerson then + self.currentSubjectDistance = 0.5 + if not self.inFirstPerson then + self:EnterFirstPerson() + end + else + local newSubjectDistance = Util.Clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, desiredSubjectDistance) + if newSubjectDistance < FIRST_PERSON_DISTANCE_THRESHOLD then + self.currentSubjectDistance = 0.5 + if not self.inFirstPerson then + self:EnterFirstPerson() + end + else + self.currentSubjectDistance = newSubjectDistance + if self.inFirstPerson then + self:LeaveFirstPerson() + end + end + end + + -- Returned only for convenience to the caller to know the outcome + return self.currentSubjectDistance +end + +function BaseCamera:SetCameraType( cameraType ) + --Used by derived classes + self.cameraType = cameraType +end + +function BaseCamera:GetCameraType() + return self.cameraType +end + +-- Movement mode standardized to Enum.ComputerCameraMovementMode values +function BaseCamera:SetCameraMovementMode( cameraMovementMode ) + self.cameraMovementMode = cameraMovementMode +end + +function BaseCamera:GetCameraMovementMode() + return self.cameraMovementMode +end + +function BaseCamera:SetIsMouseLocked(mouseLocked) + self.inMouseLockedMode = mouseLocked + self:UpdateMouseBehavior() +end + +function BaseCamera:GetIsMouseLocked() + return self.inMouseLockedMode +end + +function BaseCamera:SetMouseLockOffset(offsetVector) + self.mouseLockOffset = offsetVector +end + +function BaseCamera:GetMouseLockOffset() + return self.mouseLockOffset +end + +function BaseCamera:InFirstPerson() + return self.inFirstPerson +end + +function BaseCamera:EnterFirstPerson() + -- Overriden in ClassicCamera, the only module which supports FirstPerson +end + +function BaseCamera:LeaveFirstPerson() + -- Overriden in ClassicCamera, the only module which supports FirstPerson +end + +-- Nominal distance, set by dollying in and out with the mouse wheel or equivalent, not measured distance +function BaseCamera:GetCameraToSubjectDistance() + return self.currentSubjectDistance +end + +-- Actual measured distance to the camera Focus point, which may be needed in special circumstances, but should +-- never be used as the starting point for updating the nominal camera-to-subject distance (self.currentSubjectDistance) +-- since that is a desired target value set only by mouse wheel (or equivalent) input, PopperCam, and clamped to min max camera distance +function BaseCamera:GetMeasuredDistanceToFocus() + local camera = game.Workspace.CurrentCamera + if camera then + return (camera.CoordinateFrame.p - camera.Focus.p).magnitude + end + return nil +end + +function BaseCamera:GetCameraLookVector() + return game.Workspace.CurrentCamera and game.Workspace.CurrentCamera.CFrame.lookVector or UNIT_Z +end + +-- Replacements for RootCamera:RotateCamera() which did not actually rotate the camera +-- suppliedLookVector is not normally passed in, it's used only by Watch camera +function BaseCamera:CalculateNewLookCFrame(suppliedLookVector) + local currLookVector = suppliedLookVector or self:GetCameraLookVector() + local currPitchAngle = math.asin(currLookVector.y) + local yTheta = Util.Clamp(-MAX_Y + currPitchAngle, -MIN_Y + currPitchAngle, self.rotateInput.y) + local constrainedRotateInput = Vector2.new(self.rotateInput.x, yTheta) + local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector) + local newLookCFrame = CFrame.Angles(0, -constrainedRotateInput.x, 0) * startCFrame * CFrame.Angles(-constrainedRotateInput.y,0,0) + return newLookCFrame +end +function BaseCamera:CalculateNewLookVector(suppliedLookVector) + local newLookCFrame = self:CalculateNewLookCFrame(suppliedLookVector) + return newLookCFrame.lookVector +end + +function BaseCamera:CalculateNewLookVectorVR() + local subjectPosition = self:GetSubjectPosition() + local vecToSubject = (subjectPosition - game.Workspace.CurrentCamera.CFrame.p) + local currLookVector = (vecToSubject * X1_Y0_Z1).unit + local vrRotateInput = Vector2.new(self.rotateInput.x, 0) + local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector) + local yawRotatedVector = (CFrame.Angles(0, -vrRotateInput.x, 0) * startCFrame * CFrame.Angles(-vrRotateInput.y,0,0)).lookVector + return (yawRotatedVector * X1_Y0_Z1).unit +end + +function BaseCamera:GetHumanoid() + local player = Players.LocalPlayer + local character = player and player.Character + if character then + local resultHumanoid = self.humanoidCache[player] + if resultHumanoid and resultHumanoid.Parent == character then + return resultHumanoid + else + self.humanoidCache[player] = nil -- Bust Old Cache + local humanoid = character:FindFirstChildOfClass("Humanoid") + if humanoid then + self.humanoidCache[player] = humanoid + end + return humanoid + end + end + return nil +end + +function BaseCamera:GetHumanoidPartToFollow(humanoid, humanoidStateType) + if humanoidStateType == Enum.HumanoidStateType.Dead then + local character = humanoid.Parent + if character then + return character:FindFirstChild("Head") or humanoid.Torso + else + return humanoid.Torso + end + else + return humanoid.Torso + end +end + +function BaseCamera:UpdateGamepad() + local gamepadPan = self.gamepadPanningCamera + if gamepadPan and (self.hasGameLoaded or not VRService.VREnabled) then + gamepadPan = Util.GamepadLinearToCurve(gamepadPan) + local currentTime = tick() + if gamepadPan.X ~= 0 or gamepadPan.Y ~= 0 then + self.userPanningTheCamera = true + elseif gamepadPan == ZERO_VECTOR2 then + self.lastThumbstickRotate = nil + if self.lastThumbstickPos == ZERO_VECTOR2 then + self.currentSpeed = 0 + end + end + + local finalConstant = 0 + + if self.lastThumbstickRotate then + if VRService.VREnabled then + self.currentSpeed = self.vrMaxSpeed + else + local elapsedTime = (currentTime - self.lastThumbstickRotate) * 10 + self.currentSpeed = self.currentSpeed + (self.maxSpeed * ((elapsedTime*elapsedTime)/self.numOfSeconds)) + + if self.currentSpeed > self.maxSpeed then self.currentSpeed = self.maxSpeed end + + if self.lastVelocity then + local velocity = (gamepadPan - self.lastThumbstickPos)/(currentTime - self.lastThumbstickRotate) + local velocityDeltaMag = (velocity - self.lastVelocity).magnitude + + if velocityDeltaMag > 12 then + self.currentSpeed = self.currentSpeed * (20/velocityDeltaMag) + if self.currentSpeed > self.maxSpeed then self.currentSpeed = self.maxSpeed end + end + end + end + + local success, gamepadCameraSensitivity = pcall(function() return UserGameSettings.GamepadCameraSensitivity end) + finalConstant = success and (gamepadCameraSensitivity * self.currentSpeed) or self.currentSpeed + self.lastVelocity = (gamepadPan - self.lastThumbstickPos)/(currentTime - self.lastThumbstickRotate) + end + + self.lastThumbstickPos = gamepadPan + self.lastThumbstickRotate = currentTime + + return Vector2.new( gamepadPan.X * finalConstant, gamepadPan.Y * finalConstant * self.ySensitivity * UserGameSettings:GetCameraYInvertValue()) + end + + return ZERO_VECTOR2 +end + +-- [[ VR Support Section ]] -- + +function BaseCamera:ApplyVRTransform() + if not VRService.VREnabled then + return + end + + --we only want this to happen in first person VR + local rootJoint = self.humanoidRootPart and self.humanoidRootPart:FindFirstChild("RootJoint") + if not rootJoint then + return + end + + local cameraSubject = game.Workspace.CurrentCamera.CameraSubject + local isInVehicle = cameraSubject and cameraSubject:IsA("VehicleSeat") + + if self.inFirstPerson and not isInVehicle then + local vrFrame = VRService:GetUserCFrame(Enum.UserCFrame.Head) + local vrRotation = vrFrame - vrFrame.p + rootJoint.C0 = CFrame.new(vrRotation:vectorToObjectSpace(vrFrame.p)) * CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0) + else + rootJoint.C0 = CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0) + end +end + +function BaseCamera:IsInFirstPerson() + return self.inFirstPerson +end + +function BaseCamera:ShouldUseVRRotation() + if not VRService.VREnabled then + return false + end + + if not self.VRRotationIntensityAvailable and tick() - self.lastVRRotationIntensityCheckTime < 1 then + return false + end + + local success, vrRotationIntensity = pcall(function() return StarterGui:GetCore("VRRotationIntensity") end) + self.VRRotationIntensityAvailable = success and vrRotationIntensity ~= nil + self.lastVRRotationIntensityCheckTime = tick() + + self.shouldUseVRRotation = success and vrRotationIntensity ~= nil and vrRotationIntensity ~= "Smooth" + + return self.shouldUseVRRotation +end + +function BaseCamera:GetVRRotationInput() + local vrRotateSum = ZERO_VECTOR2 + local success, vrRotationIntensity = pcall(function() return StarterGui:GetCore("VRRotationIntensity") end) + + if not success then + return + end + + local vrGamepadRotation = self.GamepadPanningCamera or ZERO_VECTOR2 + local delayExpired = (tick() - self.lastVRRotationTime) >= self:GetRepeatDelayValue(vrRotationIntensity) + + if math.abs(vrGamepadRotation.x) >= self:GetActivateValue() then + if (delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2]) then + local sign = 1 + if vrGamepadRotation.x < 0 then + sign = -1 + end + vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity) * sign + self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = true + end + elseif math.abs(vrGamepadRotation.x) < self:GetActivateValue() - 0.1 then + self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = nil + end + if self.turningLeft then + if delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Left] then + vrRotateSum = vrRotateSum - self:GetRotateAmountValue(vrRotationIntensity) + self.vrRotateKeyCooldown[Enum.KeyCode.Left] = true + end + else + self.vrRotateKeyCooldown[Enum.KeyCode.Left] = nil + end + if self.turningRight then + if (delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Right]) then + vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity) + self.vrRotateKeyCooldown[Enum.KeyCode.Right] = true + end + else + self.vrRotateKeyCooldown[Enum.KeyCode.Right] = nil + end + + if vrRotateSum ~= ZERO_VECTOR2 then + self.lastVRRotationTime = tick() + end + + return vrRotateSum +end + +function BaseCamera:CancelCameraFreeze(keepConstraints) + if not keepConstraints then + self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, 1, self.cameraTranslationConstraints.z) + end + if self.cameraFrozen then + self.trackingHumanoid = nil + self.cameraFrozen = false + end +end + +function BaseCamera:StartCameraFreeze(subjectPosition, humanoidToTrack) + if not self.cameraFrozen then + self.humanoidJumpOrigin = subjectPosition + self.trackingHumanoid = humanoidToTrack + self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, 0, self.cameraTranslationConstraints.z) + self.cameraFrozen = true + end +end + +function BaseCamera:RescaleCameraOffset(newScaleFactor) + self.headHeightR15 = R15_HEAD_OFFSET * newScaleFactor +end + +function BaseCamera:OnHumanoidSubjectChildAdded(child) + if child.Name == "BodyHeightScale" and child:IsA("NumberValue") then + if self.heightScaleChangedConn then + self.heightScaleChangedConn:Disconnect() + end + self.heightScaleChangedConn = child.Changed:Connect(function(newScaleFactor) + self:RescaleCameraOffset(newScaleFactor) + end) + self:RescaleCameraOffset(child.Value) + end +end + +function BaseCamera:OnHumanoidSubjectChildRemoved(child) + if child.Name == "BodyHeightScale" then + self:RescaleCameraOffset(1) + if self.heightScaleChangedConn then + self.heightScaleChangedConn:Disconnect() + self.heightScaleChangedConn = nil + end + end +end + +function BaseCamera:OnNewCameraSubject() + if self.subjectStateChangedConn then + self.subjectStateChangedConn:Disconnect() + self.subjectStateChangedConn = nil + end + if self.humanoidChildAddedConn then + self.humanoidChildAddedConn:Disconnect() + self.humanoidChildAddedConn = nil + end + if self.humanoidChildRemovedConn then + self.humanoidChildRemovedConn:Disconnect() + self.humanoidChildRemovedConn = nil + end + if self.heightScaleChangedConn then + self.heightScaleChangedConn:Disconnect() + self.heightScaleChangedConn = nil + end + + local humanoid = workspace.CurrentCamera and workspace.CurrentCamera.CameraSubject + if self.trackingHumanoid ~= humanoid then + self:CancelCameraFreeze() + end + if humanoid and humanoid:IsA("Humanoid") then + self.humanoidChildAddedConn = humanoid.ChildAdded:Connect(function(child) + self:OnHumanoidSubjectChildAdded(child) + end) + self.humanoidChildRemovedConn = humanoid.ChildRemoved:Connect(function(child) + self:OnHumanoidSubjectChildRemoved(child) + end) + for _, child in pairs(humanoid:GetChildren()) do + self:OnHumanoidSubjectChildAdded(child) + end + + self.subjectStateChangedConn = humanoid.StateChanged:Connect(function(oldState, newState) + if VRService.VREnabled and newState == Enum.HumanoidStateType.Jumping and not self.inFirstPerson then + self:StartCameraFreeze(self:GetSubjectPosition(), humanoid) + elseif newState ~= Enum.HumanoidStateType.Jumping and newState ~= Enum.HumanoidStateType.Freefall then + self:CancelCameraFreeze(true) + end + end) + end +end + +function BaseCamera:GetVRFocus(subjectPosition, timeDelta) + local lastFocus = self.LastCameraFocus or subjectPosition + if not self.cameraFrozen then + self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, math.min(1, self.cameraTranslationConstraints.y + 0.42 * timeDelta), self.cameraTranslationConstraints.z) + end + + local newFocus + if self.cameraFrozen and self.humanoidJumpOrigin and self.humanoidJumpOrigin.y > lastFocus.y then + newFocus = CFrame.new(Vector3.new(subjectPosition.x, math.min(self.humanoidJumpOrigin.y, lastFocus.y + 5 * timeDelta), subjectPosition.z)) + else + newFocus = CFrame.new(Vector3.new(subjectPosition.x, lastFocus.y, subjectPosition.z):lerp(subjectPosition, self.cameraTranslationConstraints.y)) + end + + if self.cameraFrozen then + -- No longer in 3rd person + if self.inFirstPerson then -- not VRService.VREnabled + self:CancelCameraFreeze() + end + -- This case you jumped off a cliff and want to keep your character in view + -- 0.5 is to fix floating point error when not jumping off cliffs + if self.humanoidJumpOrigin and subjectPosition.y < (self.humanoidJumpOrigin.y - 0.5) then + self:CancelCameraFreeze() + end + end + + return newFocus +end + +function BaseCamera:GetRotateAmountValue(vrRotationIntensity) + vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity") + if vrRotationIntensity then + if vrRotationIntensity == "Low" then + return VR_LOW_INTENSITY_ROTATION + elseif vrRotationIntensity == "High" then + return VR_HIGH_INTENSITY_ROTATION + end + end + return ZERO_VECTOR2 +end + +function BaseCamera:GetRepeatDelayValue(vrRotationIntensity) + vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity") + if vrRotationIntensity then + if vrRotationIntensity == "Low" then + return VR_LOW_INTENSITY_REPEAT + elseif vrRotationIntensity == "High" then + return VR_HIGH_INTENSITY_REPEAT + end + end + return 0 +end + +function BaseCamera:Test() + print("BaseCamera:Test()") +end + +function BaseCamera:Update(dt) + warn("BaseCamera:Update() This is a virtual function that should never be getting called.") + return game.Workspace.CurrentCamera.CFrame, game.Workspace.CurrentCamera.Focus +end + +return BaseCamera diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/BaseOcclusion.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/BaseOcclusion.lua new file mode 100644 index 0000000..954a197 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/BaseOcclusion.lua @@ -0,0 +1,46 @@ +--[[ + BaseOcclusion - Abstract base class for character occlusion control modules + 2018 Camera Update - AllYourBlox +--]] + +--[[ The Module ]]-- +local BaseOcclusion = {} +BaseOcclusion.__index = BaseOcclusion +setmetatable(BaseOcclusion, { __call = function(_, ...) return BaseOcclusion.new(...) end}) + +function BaseOcclusion.new() + local self = setmetatable({}, BaseOcclusion) + return self +end + +-- Called when character is added +function BaseOcclusion:CharacterAdded(player, character) + +end + +-- Called when character is about to be removed +function BaseOcclusion:CharacterRemoving(player, character) + +end + +function BaseOcclusion:OnCameraSubjectChanged(newSubject) + +end + +--[[ Derived classes are required to override and implement all of the following functions ]]-- +function GetOcclusionMode() + -- Must be overriden in derived classes to return an Enum.DevCameraOcclusionMode value + warn("BaseOcclusion GetOcclusionMode must be overriden by derived classes") + return nil +end + +function BaseOcclusion:Enable(enabled) + warn("BaseOcclusion Enable must be overriden by derived classes") +end + +function BaseOcclusion:Update(dt, desiredCameraCFrame, desiredCameraFocus) + warn("BaseOcclusion Update must be overriden by derived classes") + return desiredCameraCFrame, desiredCameraFocus +end + +return BaseOcclusion diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/CameraUtils.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/CameraUtils.lua new file mode 100644 index 0000000..5af3e32 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/CameraUtils.lua @@ -0,0 +1,125 @@ +--[[ + CameraUtils - Math utility functions shared by multiple camera scripts + 2018 Camera Update - AllYourBlox +--]] + +local CameraUtils = {} + +local function round(num) + return math.floor(num + 0.5) +end + +-- Note, arguments do not match the new math.clamp +-- Eventually we will replace these calls with math.clamp, but right now +-- this is safer as math.clamp is not tolerant of min>max +function CameraUtils.Clamp(low, high, val) + return math.min(math.max(val, low), high) +end + +-- From TransparencyController +function CameraUtils.Round(num, places) + local decimalPivot = 10^places + return math.floor(num * decimalPivot + 0.5) / decimalPivot +end + +function CameraUtils.IsFinite(val) + return val == val and val ~= math.huge and val ~= -math.huge +end + +function CameraUtils.IsFiniteVector3(vec3) + return CameraUtils.IsFinite(vec3.X) and CameraUtils.IsFinite(vec3.Y) and CameraUtils.IsFinite(vec3.Z) +end + +-- Legacy implementation renamed +function CameraUtils.GetAngleBetweenXZVectors(v1, v2) + return math.atan2(v2.X*v1.Z-v2.Z*v1.X, v2.X*v1.X+v2.Z*v1.Z) +end + +function CameraUtils.RotateVectorByAngleAndRound(camLook, rotateAngle, roundAmount) + if camLook.Magnitude > 0 then + camLook = camLook.unit + local currAngle = math.atan2(camLook.z, camLook.x) + local newAngle = round((math.atan2(camLook.z, camLook.x) + rotateAngle) / roundAmount) * roundAmount + return newAngle - currAngle + end + return 0 +end + +-- K is a tunable parameter that changes the shape of the S-curve +-- the larger K is the more straight/linear the curve gets +local k = 0.35 +local lowerK = 0.8 +local function SCurveTranform(t) + t = CameraUtils.Clamp(-1,1,t) + if t >= 0 then + return (k*t) / (k - t + 1) + end + return -((lowerK*-t) / (lowerK + t + 1)) +end + +local DEADZONE = 0.1 +local function toSCurveSpace(t) + return (1 + DEADZONE) * (2*math.abs(t) - 1) - DEADZONE +end + +local function fromSCurveSpace(t) + return t/2 + 0.5 +end + +function CameraUtils.GamepadLinearToCurve(thumbstickPosition) + local function onAxis(axisValue) + local sign = 1 + if axisValue < 0 then + sign = -1 + end + local point = fromSCurveSpace(SCurveTranform(toSCurveSpace(math.abs(axisValue)))) + point = point * sign + return CameraUtils.Clamp(-1, 1, point) + end + return Vector2.new(onAxis(thumbstickPosition.x), onAxis(thumbstickPosition.y)) +end + +-- This function converts 4 different, redundant enumeration types to one standard so the values can be compared +function CameraUtils.ConvertCameraModeEnumToStandard( enumValue ) + + if enumValue == Enum.TouchCameraMovementMode.Default then + return Enum.ComputerCameraMovementMode.Follow + end + + if enumValue == Enum.ComputerCameraMovementMode.Default then + return Enum.ComputerCameraMovementMode.Classic + end + + if enumValue == Enum.TouchCameraMovementMode.Classic or + enumValue == Enum.DevTouchCameraMovementMode.Classic or + enumValue == Enum.DevComputerCameraMovementMode.Classic or + enumValue == Enum.ComputerCameraMovementMode.Classic then + return Enum.ComputerCameraMovementMode.Classic + end + + if enumValue == Enum.TouchCameraMovementMode.Follow or + enumValue == Enum.DevTouchCameraMovementMode.Follow or + enumValue == Enum.DevComputerCameraMovementMode.Follow or + enumValue == Enum.ComputerCameraMovementMode.Follow then + return Enum.ComputerCameraMovementMode.Follow + end + + if enumValue == Enum.TouchCameraMovementMode.Orbital or + enumValue == Enum.DevTouchCameraMovementMode.Orbital or + enumValue == Enum.DevComputerCameraMovementMode.Orbital or + enumValue == Enum.ComputerCameraMovementMode.Orbital then + return Enum.ComputerCameraMovementMode.Orbital + end + + -- Note: Only the Dev versions of the Enums have UserChoice as an option + if enumValue == Enum.DevTouchCameraMovementMode.UserChoice or + enumValue == Enum.DevComputerCameraMovementMode.UserChoice then + return Enum.DevComputerCameraMovementMode.UserChoice + end + + -- For any unmapped options return Classic camera + return Enum.ComputerCameraMovementMode.Classic +end + +return CameraUtils + diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/ClassicCamera.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/ClassicCamera.lua new file mode 100644 index 0000000..c36e9b9 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/ClassicCamera.lua @@ -0,0 +1,240 @@ +--[[ + ClassicCamera - Classic Roblox camera control module + 2018 Camera Update - AllYourBlox + + Note: This module also handles camera control types Follow and Track, the + latter of which is currently not distinguished from Classic +--]] + +-- Local private variables and constants +local ZERO_VECTOR2 = Vector2.new(0,0) + +local tweenAcceleration = math.rad(220) --Radians/Second^2 +local tweenSpeed = math.rad(0) --Radians/Second +local tweenMaxSpeed = math.rad(250) --Radians/Second +local TIME_BEFORE_AUTO_ROTATE = 2.0 --Seconds, used when auto-aligning camera with vehicles +local PORTRAIT_OFFSET = Vector3.new(0,-3,0) + +--[[ Services ]]-- +local PlayersService = game:GetService('Players') +local VRService = game:GetService("VRService") + +local Util = require(script.Parent:WaitForChild("CameraUtils")) + +--[[ The Module ]]-- +local BaseCamera = require(script.Parent:WaitForChild("BaseCamera")) +local ClassicCamera = setmetatable({}, BaseCamera) +ClassicCamera.__index = ClassicCamera + +function ClassicCamera.new() + local self = setmetatable(BaseCamera.new(), ClassicCamera) + + self.isFollowCamera = false + self.lastUpdate = tick() + + return self +end + +function ClassicCamera:GetModuleName() + return "ClassicCamera" +end + +-- Movement mode standardized to Enum.ComputerCameraMovementMode values +function ClassicCamera:SetCameraMovementMode( cameraMovementMode ) + BaseCamera.SetCameraMovementMode(self,cameraMovementMode) + self.isFollowCamera = cameraMovementMode == Enum.ComputerCameraMovementMode.Follow +end + +function ClassicCamera:Test() + print("ClassicCamera:Test()") +end + +function ClassicCamera:Update() + local now = tick() + local timeDelta = (now - self.lastUpdate) + + local camera = workspace.CurrentCamera + local newCameraCFrame = camera.CFrame + local newCameraFocus = camera.Focus + local player = PlayersService.LocalPlayer + local humanoid = self:GetHumanoid() + local cameraSubject = camera.CameraSubject + local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat') + local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform') + local isClimbing = humanoid and humanoid:GetState() == Enum.HumanoidStateType.Climbing + + if self.lastUpdate == nil or timeDelta > 1 then + self.lastCameraTransform = nil + end + + if self.lastUpdate then + local gamepadRotation = self:UpdateGamepad() + + if self:ShouldUseVRRotation() then + self.rotateInput = self.rotateInput + self:GetVRRotationInput() + else + -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from + local delta = math.min(0.1, timeDelta) + + if gamepadRotation ~= ZERO_VECTOR2 then + self.rotateInput = self.rotateInput + (gamepadRotation * delta) + end + + local angle = 0 + if not (isInVehicle or isOnASkateboard) then + angle = angle + (self.turningLeft and -120 or 0) + angle = angle + (self.turningRight and 120 or 0) + end + + if angle ~= 0 then + self.rotateInput = self.rotateInput + Vector2.new(math.rad(angle * delta), 0) + end + end + end + + -- Reset tween speed if user is panning + if self.userPanningTheCamera then + tweenSpeed = 0 + self.lastUserPanCamera = tick() + end + + local userRecentlyPannedCamera = now - self.lastUserPanCamera < TIME_BEFORE_AUTO_ROTATE + local subjectPosition = self:GetSubjectPosition() + + if subjectPosition and player and camera then + local zoom = self:GetCameraToSubjectDistance() + if zoom < 0.5 then + zoom = 0.5 + end + + if self:GetIsMouseLocked() and not self:IsInFirstPerson() then + -- We need to use the right vector of the camera after rotation, not before + local newLookCFrame = self:CalculateNewLookCFrame() + + local offset = self:GetMouseLockOffset() + local cameraRelativeOffset = offset.X * newLookCFrame.rightVector + offset.Y * newLookCFrame.upVector + offset.Z * newLookCFrame.lookVector + + --offset can be NAN, NAN, NAN if newLookVector has only y component + if Util.IsFiniteVector3(cameraRelativeOffset) then + subjectPosition = subjectPosition + cameraRelativeOffset + end + else + if not self.userPanningTheCamera and self.lastCameraTransform then + + local isInFirstPerson = self:IsInFirstPerson() + + if (isInVehicle or isOnASkateboard or (self.isFollowCamera and isClimbing)) and self.lastUpdate and humanoid and humanoid.Torso then + if isInFirstPerson then + if self.lastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then + local y = -Util.GetAngleBetweenXZVectors(self.lastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector) + if Util.IsFinite(y) then + self.rotateInput = self.rotateInput + Vector2.new(y, 0) + end + tweenSpeed = 0 + end + elseif not userRecentlyPannedCamera then + local forwardVector = humanoid.Torso.CFrame.lookVector + if isOnASkateboard then + forwardVector = cameraSubject.CFrame.lookVector + end + + tweenSpeed = Util.Clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta) + + local percent = Util.Clamp(0, 1, tweenSpeed * timeDelta) + if self:IsInFirstPerson() and not (self.isFollowCamera and self.isClimbing) then + percent = 1 + end + + local y = Util.GetAngleBetweenXZVectors(forwardVector, self:GetCameraLookVector()) + if Util.IsFinite(y) and math.abs(y) > 0.0001 then + self.rotateInput = self.rotateInput + Vector2.new(y * percent, 0) + end + end + + elseif self.isFollowCamera and (not (isInFirstPerson or userRecentlyPannedCamera) and not VRService.VREnabled) then + -- Logic that was unique to the old FollowCamera module + local lastVec = -(self.lastCameraTransform.p - subjectPosition) + + local y = Util.GetAngleBetweenXZVectors(lastVec, self:GetCameraLookVector()) + + -- This cutoff is to decide if the humanoid's angle of movement, + -- relative to the camera's look vector, is enough that + -- we want the camera to be following them. The point is to provide + -- a sizable deadzone to allow more precise forward movements. + local thetaCutoff = 0.4 + + -- Check for NaNs + if Util.IsFinite(y) and math.abs(y) > 0.0001 and math.abs(y) > thetaCutoff * timeDelta then + self.rotateInput = self.rotateInput + Vector2.new(y, 0) + end + end + end + end + + if not self.isFollowCamera then + local VREnabled = VRService.VREnabled + newCameraFocus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame.new(subjectPosition) + + local cameraFocusP = newCameraFocus.p + if VREnabled and not self:IsInFirstPerson() then + local cameraHeight = self:GetCameraHeight() + local vecToSubject = (subjectPosition - camera.CFrame.p) + local distToSubject = vecToSubject.magnitude + + -- Only move the camera if it exceeded a maximum distance to the subject in VR + if distToSubject > zoom or self.rotateInput.x ~= 0 then + local desiredDist = math.min(distToSubject, zoom) + vecToSubject = self:CalculateNewLookVectorVR() * desiredDist + local newPos = cameraFocusP - vecToSubject + local desiredLookDir = camera.CFrame.lookVector + if self.rotateInput.x ~= 0 then + desiredLookDir = vecToSubject + end + local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z) + self.rotateInput = ZERO_VECTOR2 + + newCameraCFrame = CFrame.new(newPos, lookAt) + Vector3.new(0, cameraHeight, 0) + end + else + local newLookVector = self:CalculateNewLookVector() + self.rotateInput = ZERO_VECTOR2 + newCameraCFrame = CFrame.new(cameraFocusP - (zoom * newLookVector), cameraFocusP) + end + else -- is FollowCamera + local newLookVector = self:CalculateNewLookVector() + self.rotateInput = ZERO_VECTOR2 + + if VRService.VREnabled then + newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta) + elseif self.portraitMode then + newCameraFocus = CFrame.new(subjectPosition + PORTRAIT_OFFSET) + else + newCameraFocus = CFrame.new(subjectPosition) + end + newCameraCFrame = CFrame.new(newCameraFocus.p - (zoom * newLookVector), newCameraFocus.p) + Vector3.new(0, self:GetCameraHeight(), 0) + end + + self.lastCameraTransform = newCameraCFrame + self.lastCameraFocus = newCameraFocus + if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then + self.lastSubjectCFrame = cameraSubject.CFrame + else + self.lastSubjectCFrame = nil + end + end + + self.lastUpdate = now + return newCameraCFrame, newCameraFocus +end + +function ClassicCamera:EnterFirstPerson() + self.inFirstPerson = true + self:UpdateMouseBehavior() +end + +function ClassicCamera:LeaveFirstPerson() + self.inFirstPerson = false + self:UpdateMouseBehavior() +end + +return ClassicCamera diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Invisicam.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Invisicam.lua new file mode 100644 index 0000000..4f7c775 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Invisicam.lua @@ -0,0 +1,560 @@ +--[[ + Invisicam - Occlusion module that makes objects occluding character view semi-transparent + 2018 Camera Update - AllYourBlox +--]] + +--[[ Camera Maths Utilities Library ]]-- +local Util = require(script.Parent:WaitForChild("CameraUtils")) + +--[[ Top Level Roblox Services ]]-- +local PlayersService = game:GetService("Players") +local RunService = game:GetService("RunService") + +--[[ Constants ]]-- +local ZERO_VECTOR3 = Vector3.new(0,0,0) +local USE_STACKING_TRANSPARENCY = true -- Multiple items between the subject and camera get transparency values that add up to TARGET_TRANSPARENCY +local TARGET_TRANSPARENCY = 0.75 -- Classic Invisicam's Value, also used by new invisicam for parts hit by head and torso rays +local TARGET_TRANSPARENCY_PERIPHERAL = 0.5 -- Used by new SMART_CIRCLE mode for items not hit by head and torso rays + +local MODE = { + --CUSTOM = 1, -- Retired, unutilized + LIMBS = 2, -- Track limbs + MOVEMENT = 3, -- Track movement + CORNERS = 4, -- Char model corners + CIRCLE1 = 5, -- Circle of casts around character + CIRCLE2 = 6, -- Circle of casts around character, camera relative + LIMBMOVE = 7, -- LIMBS mode + MOVEMENT mode + SMART_CIRCLE = 8, -- More sample points on and around character + CHAR_OUTLINE = 9, -- Dynamic outline around the character +} + +local LIMB_TRACKING_SET = { + -- Body parts common to R15 and R6 + ['Head'] = true, + + -- Body parts unique to R6 + ['Left Arm'] = true, + ['Right Arm'] = true, + ['Left Leg'] = true, + ['Right Leg'] = true, + + -- Body parts unique to R15 + ['LeftLowerArm'] = true, + ['RightLowerArm'] = true, + ['LeftUpperLeg'] = true, + ['RightUpperLeg'] = true +} + +local CORNER_FACTORS = { + Vector3.new(1,1,-1), + Vector3.new(1,-1,-1), + Vector3.new(-1,-1,-1), + Vector3.new(-1,1,-1) +} + +local CIRCLE_CASTS = 10 +local MOVE_CASTS = 3 +local SMART_CIRCLE_CASTS = 24 +local SMART_CIRCLE_INCREMENT = 2.0 * math.pi / SMART_CIRCLE_CASTS +local CHAR_OUTLINE_CASTS = 24 + +-- Used to sanitize user-supplied functions +local function AssertTypes(param, ...) + local allowedTypes = {} + local typeString = '' + for _, typeName in pairs({...}) do + allowedTypes[typeName] = true + typeString = typeString .. (typeString == '' and '' or ' or ') .. typeName + end + local theType = type(param) + assert(allowedTypes[theType], typeString .. " type expected, got: " .. theType) +end + +-- Helper function for Determinant of 3x3, not in CameraUtils for performance reasons +local function Det3x3(a,b,c,d,e,f,g,h,i) + return (a*(e*i-f*h)-b*(d*i-f*g)+c*(d*h-e*g)) +end + +-- Smart Circle mode needs the intersection of 2 rays that are known to be in the same plane +-- because they are generated from cross products with a common vector. This function is computing +-- that intersection, but it's actually the general solution for the point halfway between where +-- two skew lines come nearest to each other, which is more forgiving. +local function RayIntersection(p0, v0, p1, v1) + local v2 = v0:Cross(v1) + local d1 = p1.x - p0.x + local d2 = p1.y - p0.y + local d3 = p1.z - p0.z + local denom = Det3x3(v0.x,-v1.x,v2.x,v0.y,-v1.y,v2.y,v0.z,-v1.z,v2.z) + + if (denom == 0) then + return ZERO_VECTOR3 -- No solution (rays are parallel) + end + + local t0 = Det3x3(d1,-v1.x,v2.x,d2,-v1.y,v2.y,d3,-v1.z,v2.z) / denom + local t1 = Det3x3(v0.x,d1,v2.x,v0.y,d2,v2.y,v0.z,d3,v2.z) / denom + local s0 = p0 + t0 * v0 + local s1 = p1 + t1 * v1 + local s = s0 + 0.5 * ( s1 - s0 ) + + -- 0.25 studs is a threshold for deciding if the rays are + -- close enough to be considered intersecting, found through testing + if (s1-s0).Magnitude < 0.25 then + return s + else + return ZERO_VECTOR3 + end +end + + + +--[[ The Module ]]-- +local BaseOcclusion = require(script.Parent:WaitForChild("BaseOcclusion")) +local Invisicam = setmetatable({}, BaseOcclusion) +Invisicam.__index = Invisicam + +function Invisicam.new() + local self = setmetatable(BaseOcclusion.new(), Invisicam) + + self.character = nil + self.humanoidRootPart = nil + self.torsoPart = nil + self.headPart = nil + + self.childAddedConn = nil + self.childRemovedConn = nil + + self.behaviors = {} -- Map of modes to behavior fns + self.behaviors[MODE.LIMBS] = self.LimbBehavior + self.behaviors[MODE.MOVEMENT] = self.MoveBehavior + self.behaviors[MODE.CORNERS] = self.CornerBehavior + self.behaviors[MODE.CIRCLE1] = self.CircleBehavior + self.behaviors[MODE.CIRCLE2] = self.CircleBehavior + self.behaviors[MODE.LIMBMOVE] = self.LimbMoveBehavior + self.behaviors[MODE.SMART_CIRCLE] = self.SmartCircleBehavior + self.behaviors[MODE.CHAR_OUTLINE] = self.CharacterOutlineBehavior + + self.mode = MODE.SMART_CIRCLE + self.behaviorFunction = self.SmartCircleBehavior + + + self.savedHits = {} -- Objects currently being faded in/out + self.trackedLimbs = {} -- Used in limb-tracking casting modes + + self.camera = game.Workspace.CurrentCamera + + self.enabled = false + return self +end + +function Invisicam:Enable(enable) + self.enabled = enable + + if not enable then + self:Cleanup() + end +end + +function Invisicam:GetOcclusionMode() + return Enum.DevCameraOcclusionMode.Invisicam +end + +--[[ Module functions ]]-- +function Invisicam:LimbBehavior(castPoints) + for limb, _ in pairs(self.trackedLimbs) do + castPoints[#castPoints + 1] = limb.Position + end +end + +function Invisicam:MoveBehavior(castPoints) + for i = 1, MOVE_CASTS do + local position, velocity = self.humanoidRootPart.Position, self.humanoidRootPart.Velocity + local horizontalSpeed = Vector3.new(velocity.X, 0, velocity.Z).Magnitude / 2 + local offsetVector = (i - 1) * self.humanoidRootPart.CFrame.lookVector * horizontalSpeed + castPoints[#castPoints + 1] = position + offsetVector + end +end + +function Invisicam:CornerBehavior(castPoints) + local cframe = self.humanoidRootPart.CFrame + local centerPoint = cframe.p + local rotation = cframe - centerPoint + local halfSize = self.character:GetExtentsSize() / 2 --NOTE: Doesn't update w/ limb animations + castPoints[#castPoints + 1] = centerPoint + for i = 1, #CORNER_FACTORS do + castPoints[#castPoints + 1] = centerPoint + (rotation * (halfSize * CORNER_FACTORS[i])) + end +end + +function Invisicam:CircleBehavior(castPoints) + local cframe = nil + if self.mode == MODE.CIRCLE1 then + cframe = self.humanoidRootPart.CFrame + else + local camCFrame = self.camera.CoordinateFrame + cframe = camCFrame - camCFrame.p + self.humanoidRootPart.Position + end + castPoints[#castPoints + 1] = cframe.p + for i = 0, CIRCLE_CASTS - 1 do + local angle = (2 * math.pi / CIRCLE_CASTS) * i + local offset = 3 * Vector3.new(math.cos(angle), math.sin(angle), 0) + castPoints[#castPoints + 1] = cframe * offset + end +end + +function Invisicam:LimbMoveBehavior(castPoints) + self:LimbBehavior(castPoints) + self:MoveBehavior(castPoints) +end + +function Invisicam:CharacterOutlineBehavior(castPoints) + local torsoUp = self.torsoPart.CFrame.upVector.unit + local torsoRight = self.torsoPart.CFrame.rightVector.unit + + -- Torso cross of points for interior coverage + castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp + castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp + castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight + castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight + if self.headPart then + castPoints[#castPoints + 1] = self.headPart.CFrame.p + end + + local cframe = CFrame.new(ZERO_VECTOR3,Vector3.new(self.camera.CoordinateFrame.lookVector.X,0,self.camera.CoordinateFrame.lookVector.Z)) + local centerPoint = (self.torsoPart and self.torsoPart.Position or self.humanoidRootPart.Position) + + local partsWhitelist = {self.torsoPart} + if self.headPart then + partsWhitelist[#partsWhitelist + 1] = self.headPart + end + + for i = 1, CHAR_OUTLINE_CASTS do + local angle = (2 * math.pi * i / CHAR_OUTLINE_CASTS) + local offset = cframe * (3 * Vector3.new(math.cos(angle), math.sin(angle), 0)) + + offset = Vector3.new(offset.X, math.max(offset.Y, -2.25), offset.Z) + + local ray = Ray.new(centerPoint + offset, -3 * offset) + local hit, hitPoint = game.Workspace:FindPartOnRayWithWhitelist(ray, partsWhitelist, false, false) + + if hit then + -- Use hit point as the cast point, but nudge it slightly inside the character so that bumping up against + -- walls is less likely to cause a transparency glitch + castPoints[#castPoints + 1] = hitPoint + 0.2 * (centerPoint - hitPoint).unit + end + end +end + +function Invisicam:SmartCircleBehavior(castPoints) + local torsoUp = self.torsoPart.CFrame.upVector.unit + local torsoRight = self.torsoPart.CFrame.rightVector.unit + + -- SMART_CIRCLE mode includes rays to head and 5 to the torso. + -- Hands, arms, legs and feet are not included since they + -- are not canCollide and can therefore go inside of parts + castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp + castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp + castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight + castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight + if self.headPart then + castPoints[#castPoints + 1] = self.headPart.CFrame.p + end + + local cameraOrientation = self.camera.CFrame - self.camera.CFrame.p + local torsoPoint = Vector3.new(0,0.5,0) + (self.torsoPart and self.torsoPart.Position or self.humanoidRootPart.Position) + local radius = 2.5 + + -- This loop first calculates points in a circle of radius 2.5 around the torso of the character, in the + -- plane orthogonal to the camera's lookVector. Each point is then raycast to, to determine if it is within + -- the free space surrounding the player (not inside anything). Two iterations are done to adjust points that + -- are inside parts, to try to move them to valid locations that are still on their camera ray, so that the + -- circle remains circular from the camera's perspective, but does not cast rays into walls or parts that are + -- behind, below or beside the character and not really obstructing view of the character. This minimizes + -- the undesirable situation where the character walks up to an exterior wall and it is made invisible even + -- though it is behind the character. + for i = 1, SMART_CIRCLE_CASTS do + local angle = SMART_CIRCLE_INCREMENT * i - 0.5 * math.pi + local offset = radius * Vector3.new(math.cos(angle), math.sin(angle), 0) + local circlePoint = torsoPoint + cameraOrientation * offset + + -- Vector from camera to point on the circle being tested + local vp = circlePoint - self.camera.CFrame.p + + local ray = Ray.new(torsoPoint, circlePoint - torsoPoint) + local hit, hp, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.character}, false, false ) + local castPoint = circlePoint + + if hit then + local hprime = hp + 0.1 * hitNormal.unit -- Slightly offset hit point from the hit surface + local v0 = hprime - torsoPoint -- Vector from torso to offset hit point + local d0 = v0.magnitude + + local perp = (v0:Cross(vp)).unit + + -- Vector from the offset hit point, along the hit surface + local v1 = (perp:Cross(hitNormal)).unit + + -- Vector from camera to offset hit + local vprime = (hprime - self.camera.CFrame.p).unit + + -- This dot product checks to see if the vector along the hit surface would hit the correct + -- side of the invisicam cone, or if it would cross the camera look vector and hit the wrong side + if ( v0.unit:Dot(-v1) < v0.unit:Dot(vprime)) then + castPoint = RayIntersection(hprime, v1, circlePoint, vp) + + if castPoint.Magnitude > 0 then + local ray = Ray.new(hprime, castPoint - hprime) + local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.character}, false, false ) + + if hit then + local hprime2 = hitPoint + 0.1 * hitNormal.unit + castPoint = hprime2 + end + else + castPoint = hprime + end + else + castPoint = hprime + end + + local ray = Ray.new(torsoPoint, (castPoint - torsoPoint)) + local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.character}, false, false ) + + if hit then + local castPoint2 = hitPoint - 0.1 * (castPoint - torsoPoint).unit + castPoint = castPoint2 + end + end + + castPoints[#castPoints + 1] = castPoint + end +end + +function Invisicam:CheckTorsoReference() + if self.character then + self.torsoPart = self.character:FindFirstChild("Torso") + if not self.torsoPart then + self.torsoPart = self.character:FindFirstChild("UpperTorso") + if not self.torsoPart then + self.torsoPart = self.character:FindFirstChild("HumanoidRootPart") + end + end + + self.headPart = self.character:FindFirstChild("Head") + end +end + +function Invisicam:CharacterAdded(player, character) + -- We only want the LocalPlayer's character + if player~=PlayersService.LocalPlayer then return end + + if self.childAddedConn then + self.childAddedConn:Disconnect() + self.childAddedConn = nil + end + if self.childRemovedConn then + self.childRemovedConn:Disconnect() + self.childRemovedConn = nil + end + + self.character = character + + self.trackedLimbs = {} + local function childAdded(child) + if child:IsA("BasePart") then + if LIMB_TRACKING_SET[child.Name] then + self.trackedLimbs[child] = true + end + + if (child.Name == "Torso" or child.Name == "UpperTorso") then + self.torsoPart = child + end + + if (child.Name == "Head") then + self.headPart = child + end + end + end + + local function childRemoved(child) + self.trackedLimbs[child] = nil + + -- If removed/replaced part is 'Torso' or 'UpperTorso' double check that we still have a TorsoPart to use + self:CheckTorsoReference() + end + + self.childAddedConn = character.ChildAdded:Connect(childAdded) + self.childRemovedConn = character.ChildRemoved:Connect(childRemoved) + for _, child in pairs(self.character:GetChildren()) do + childAdded(child) + end +end + +function Invisicam:SetMode(newMode) + AssertTypes(newMode, 'number') + for modeName, modeNum in pairs(MODE) do + if modeNum == newMode then + self.mode = newMode + self.behaviorFunction = self.behaviors[self.mode] + return + end + end + error("Invalid mode number") +end + +function Invisicam:GetObscuredParts() + return self.savedHits +end + +-- Want to turn off Invisicam? Be sure to call this after. +function Invisicam:Cleanup() + for hit, originalFade in pairs(self.savedHits) do + hit.LocalTransparencyModifier = originalFade + end +end + +function Invisicam:Update(dt, desiredCameraCFrame, desiredCameraFocus) + + -- Bail if there is no Character + if not self.enabled or not self.character then + return desiredCameraCFrame, desiredCameraFocus + end + + self.camera = game.Workspace.CurrentCamera + + -- TODO: Move this to a GetHumanoidRootPart helper, probably combine with CheckTorsoReference + -- Make sure we still have a HumanoidRootPart + if not self.humanoidRootPart then + local humanoid = self.character:FindFirstChildOfClass("Humanoid") + if humanoid and humanoid.RootPart then + self.humanoidRootPart = humanoid.RootPart + else + -- Not set up with Humanoid? Try and see if there's one in the Character at all: + self.humanoidRootPart = self.character:FindFirstChild("HumanoidRootPart") + if not self.humanoidRootPart then + -- Bail out, since we're relying on HumanoidRootPart existing + return desiredCameraCFrame, desiredCameraFocus + end + end + + -- TODO: Replace this with something more sensible + local ancestryChangedConn + ancestryChangedConn = self.humanoidRootPart.AncestryChanged:Connect(function(child, parent) + if child == self.humanoidRootPart and not parent then + self.humanoidRootPart = nil + if ancestryChangedConn and ancestryChangedConn.Connected then + ancestryChangedConn:Disconnect() + ancestryChangedConn = nil + end + end + end) + end + + if not self.torsoPart then + self:CheckTorsoReference() + if not self.torsoPart then + -- Bail out, since we're relying on Torso existing, should never happen since we fall back to using HumanoidRootPart as torso + return desiredCameraCFrame, desiredCameraFocus + end + end + + -- Make a list of world points to raycast to + local castPoints = {} + self.behaviorFunction(self, castPoints) + + -- Cast to get a list of objects between the camera and the cast points + local currentHits = {} + local ignoreList = {self.character} + local function add(hit) + currentHits[hit] = true + if not self.savedHits[hit] then + self.savedHits[hit] = hit.LocalTransparencyModifier + end + end + + local hitParts + local hitPartCount = 0 + + -- Hash table to treat head-ray-hit parts differently than the rest of the hit parts hit by other rays + -- head/torso ray hit parts will be more transparent than peripheral parts when USE_STACKING_TRANSPARENCY is enabled + local headTorsoRayHitParts = {} + local partIsTouchingCamera = {} + + local perPartTransparencyHeadTorsoHits = TARGET_TRANSPARENCY + local perPartTransparencyOtherHits = TARGET_TRANSPARENCY + + if USE_STACKING_TRANSPARENCY then + + -- This first call uses head and torso rays to find out how many parts are stacked up + -- for the purpose of calculating required per-part transparency + local headPoint = self.headPart and self.headPart.CFrame.p or castPoints[1] + local torsoPoint = self.torsoPart and self.torsoPart.CFrame.p or castPoints[2] + hitParts = self.camera:GetPartsObscuringTarget({headPoint, torsoPoint}, ignoreList) + + -- Count how many things the sample rays passed through, including decals. This should only + -- count decals facing the camera, but GetPartsObscuringTarget does not return surface normals, + -- so my compromise for now is to just let any decal increase the part count by 1. Only one + -- decal per part will be considered. + for i = 1, #hitParts do + local hitPart = hitParts[i] + hitPartCount = hitPartCount + 1 -- count the part itself + headTorsoRayHitParts[hitPart] = true + for _, child in pairs(hitPart:GetChildren()) do + if child:IsA('Decal') or child:IsA('Texture') then + hitPartCount = hitPartCount + 1 -- count first decal hit, then break + break + end + end + end + + if (hitPartCount > 0) then + perPartTransparencyHeadTorsoHits = math.pow( ((0.5 * TARGET_TRANSPARENCY) + (0.5 * TARGET_TRANSPARENCY / hitPartCount)), 1 / hitPartCount ) + perPartTransparencyOtherHits = math.pow( ((0.5 * TARGET_TRANSPARENCY_PERIPHERAL) + (0.5 * TARGET_TRANSPARENCY_PERIPHERAL / hitPartCount)), 1 / hitPartCount ) + end + end + + -- Now get all the parts hit by all the rays + hitParts = self.camera:GetPartsObscuringTarget(castPoints, ignoreList) + + local partTargetTransparency = {} + + -- Include decals and textures + for i = 1, #hitParts do + local hitPart = hitParts[i] + + partTargetTransparency[hitPart] =headTorsoRayHitParts[hitPart] and perPartTransparencyHeadTorsoHits or perPartTransparencyOtherHits + + -- If the part is not already as transparent or more transparent than what invisicam requires, add it to the list of + -- parts to be modified by invisicam + if hitPart.Transparency < partTargetTransparency[hitPart] then + add(hitPart) + end + + -- Check all decals and textures on the part + for _, child in pairs(hitPart:GetChildren()) do + if child:IsA('Decal') or child:IsA('Texture') then + if (child.Transparency < partTargetTransparency[hitPart]) then + partTargetTransparency[child] = partTargetTransparency[hitPart] + add(child) + end + end + end + end + + -- Invisibilize objects that are in the way, restore those that aren't anymore + for hitPart, originalLTM in pairs(self.savedHits) do + if currentHits[hitPart] then + -- LocalTransparencyModifier gets whatever value is required to print the part's total transparency to equal perPartTransparency + hitPart.LocalTransparencyModifier = (hitPart.Transparency < 1) and ((partTargetTransparency[hitPart] - hitPart.Transparency) / (1.0 - hitPart.Transparency)) or 0 + else -- Restore original pre-invisicam value of LTM + hitPart.LocalTransparencyModifier = originalLTM + self.savedHits[hitPart] = nil + end + end + + -- Invisicam does not change the camera values + return desiredCameraCFrame, desiredCameraFocus +end + +return Invisicam \ No newline at end of file diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/LegacyCamera.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/LegacyCamera.lua new file mode 100644 index 0000000..7a6f351 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/LegacyCamera.lua @@ -0,0 +1,156 @@ +--[[ + LegacyCamera - Implements legacy controller types: Attach, Fixed, Watch + 2018 Camera Update - AllYourBlox +--]] + +-- Local private variables and constants +local UNIT_X = Vector3.new(1,0,0) +local UNIT_Y = Vector3.new(0,1,0) +local UNIT_Z = Vector3.new(0,0,1) +local X1_Y0_Z1 = Vector3.new(1,0,1) --Note: not a unit vector, used for projecting onto XZ plane +local ZERO_VECTOR3 = Vector3.new(0,0,0) +local ZERO_VECTOR2 = Vector2.new(0,0) + +local VR_PITCH_FRACTION = 0.25 +local tweenAcceleration = math.rad(220) --Radians/Second^2 +local tweenSpeed = math.rad(0) --Radians/Second +local tweenMaxSpeed = math.rad(250) --Radians/Second +local TIME_BEFORE_AUTO_ROTATE = 2.0 --Seconds, used when auto-aligning camera with vehicles +local PORTRAIT_OFFSET = Vector3.new(0,-3,0) + +local Util = require(script.Parent:WaitForChild("CameraUtils")) + +--[[ Services ]]-- +local PlayersService = game:GetService('Players') +local VRService = game:GetService("VRService") + + +--[[ The Module ]]-- +local BaseCamera = require(script.Parent:WaitForChild("BaseCamera")) +local LegacyCamera = setmetatable({}, BaseCamera) +LegacyCamera.__index = LegacyCamera + +function LegacyCamera.new() + local self = setmetatable(BaseCamera.new(), LegacyCamera) + + self.cameraType = Enum.CameraType.Fixed + self.lastUpdate = tick() + self.lastDistanceToSubject = nil + + return self +end + +function LegacyCamera:GetModuleName() + return "LegacyCamera" +end + +function LegacyCamera:Test() + print("LegacyCamera:Test()") +end + +--[[ Functions overriden from BaseCamera ]]-- +function LegacyCamera:SetCameraToSubjectDistance(desiredSubjectDistance) + return BaseCamera.SetCameraToSubjectDistance(self,desiredSubjectDistance) +end + +function LegacyCamera:Update(dt) + + -- Cannot update until cameraType has been set + if not self.cameraType then return end + + local now = tick() + local timeDelta = (now - self.lastUpdate) + local camera = workspace.CurrentCamera + local newCameraCFrame = camera.CFrame + local newCameraFocus = camera.Focus + local player = PlayersService.LocalPlayer + local humanoid = self:GetHumanoid() + local cameraSubject = camera and camera.CameraSubject + local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat') + local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform') + local isClimbing = humanoid and humanoid:GetState() == Enum.HumanoidStateType.Climbing + + if self.lastUpdate == nil or timeDelta > 1 then + self.lastDistanceToSubject = nil + end + local subjectPosition = self:GetSubjectPosition() + + if self.cameraType == Enum.CameraType.Fixed then + if self.lastUpdate then + -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from + local delta = math.min(0.1, now - self.lastUpdate) + local gamepadRotation = self:UpdateGamepad() + self.rotateInput = self.rotateInput + (gamepadRotation * delta) + end + + if subjectPosition and player and camera then + local distanceToSubject = self:GetCameraToSubjectDistance() + local newLookVector = self:CalculateNewLookVector() + self.rotateInput = ZERO_VECTOR2 + + newCameraFocus = camera.Focus -- Fixed camera does not change focus + newCameraCFrame = CFrame.new(camera.CFrame.p, camera.CFrame.p + (distanceToSubject * newLookVector)) + end + elseif self.cameraType == Enum.CameraType.Attach then + if subjectPosition and camera then + local distanceToSubject = self:GetCameraToSubjectDistance() + local humanoid = self:GetHumanoid() + if self.lastUpdate and humanoid and humanoid.RootPart then + + -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from + local delta = math.min(0.1, now - self.lastUpdate) + local gamepadRotation = self:UpdateGamepad() + self.rotateInput = self.rotateInput + (gamepadRotation * delta) + + local forwardVector = humanoid.RootPart.CFrame.lookVector + + local y = Util.GetAngleBetweenXZVectors(forwardVector, self:GetCameraLookVector()) + if Util.IsFinite(y) then + -- Preserve vertical rotation from user input + self.rotateInput = Vector2.new(y, self.rotateInput.Y) + end + end + + local newLookVector = self:CalculateNewLookVector() + self.rotateInput = ZERO_VECTOR2 + + newCameraFocus = CFrame.new(subjectPosition) + newCameraCFrame = CFrame.new(subjectPosition - (distanceToSubject * newLookVector), subjectPosition) + end + elseif self.cameraType == Enum.CameraType.Watch then + if subjectPosition and player and camera then + local cameraLook = nil + + local humanoid = self:GetHumanoid() + if humanoid and humanoid.RootPart then + -- TODO: let the paging buttons move the camera but not the mouse/touch + -- currently neither do + local diffVector = subjectPosition - camera.CFrame.p + cameraLook = diffVector.unit + + if self.lastDistanceToSubject and self.lastDistanceToSubject == self:GetCameraToSubjectDistance() then + -- Don't clobber the zoom if they zoomed the camera + local newDistanceToSubject = diffVector.magnitude + self:SetCameraToSubjectDistance(newDistanceToSubject) + end + end + + local distanceToSubject = self:GetCameraToSubjectDistance() + local newLookVector = self:CalculateNewLookVector(cameraLook) + self.rotateInput = ZERO_VECTOR2 + + newCameraFocus = CFrame.new(subjectPosition) + newCameraCFrame = CFrame.new(subjectPosition - (distanceToSubject * newLookVector), subjectPosition) + + self.lastDistanceToSubject = distanceToSubject + end + else + -- Unsupported type, return current values unchanged + return camera.CFrame, camera.Focus + end + + self.lastUpdate = now + return newCameraCFrame, newCameraFocus +end + +return LegacyCamera diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/MouseLockController.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/MouseLockController.lua new file mode 100644 index 0000000..432444a --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/MouseLockController.lua @@ -0,0 +1,211 @@ +--[[ + MouseLockController - Replacement for ShiftLockController, manages use of mouse-locked mode + 2018 Camera Update - AllYourBlox +--]] + +--[[ Constants ]]-- +local DEFAULT_MOUSE_LOCK_CURSOR = "rbxasset://textures/MouseLockedCursor.png" + +local Util = require(script.Parent:WaitForChild("CameraUtils")) + +--[[ Services ]]-- +local PlayersService = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local Settings = UserSettings() -- ignore warning +local GameSettings = Settings.GameSettings +local Mouse = PlayersService.LocalPlayer:GetMouse() + +--[[ The Module ]]-- +local MouseLockController = {} +MouseLockController.__index = MouseLockController + +function MouseLockController.new() + local self = setmetatable({}, MouseLockController) + + self.inputBeganConn = nil + self.isMouseLocked = false + self.savedMouseCursor = nil + self.boundKeys = {Enum.KeyCode.LeftShift, Enum.KeyCode.RightShift} -- defaults + + self.mouseLockToggledEvent = Instance.new("BindableEvent") + + local boundKeysObj = script:FindFirstChild("BoundKeys") + if (not boundKeysObj) or (not boundKeysObj:IsA("StringValue")) then + -- If object with correct name was found, but it's not a StringValue, destroy and replace + if boundKeysObj then + boundKeysObj:Destroy() + end + + boundKeysObj = Instance.new("StringValue") + boundKeysObj.Name = "BoundKeys" + boundKeysObj.Value = "LeftShift,RightShift" + boundKeysObj.Parent = script + end + + if boundKeysObj then + boundKeysObj.Changed:Connect(function(value) + self:OnBoundKeysObjectChanged(value) + end) + self:OnBoundKeysObjectChanged(boundKeysObj.Value) -- Initial setup call + end + + -- Watch for changes to user's ControlMode and ComputerMovementMode settings and update the feature availabilty accordingly + GameSettings.Changed:Connect(function(property) + if property == "ControlMode" or property == "ComputerMovementMode" then + self:UpdateMouseLockAvailability() + end + end) + + -- Watch for changes to DevEnableMouseLock and update the feature availabilty accordingly + PlayersService.LocalPlayer:GetPropertyChangedSignal("DevEnableMouseLock"):Connect(function() + self:UpdateMouseLockAvailability() + end) + + -- Watch for changes to DevEnableMouseLock and update the feature availabilty accordingly + PlayersService.LocalPlayer:GetPropertyChangedSignal("DevComputerMovementMode"):Connect(function() + self:UpdateMouseLockAvailability() + end) + + self:UpdateMouseLockAvailability() + + return self +end + +function MouseLockController:GetIsMouseLocked() + return self.isMouseLocked +end + +function MouseLockController:GetBindableToggleEvent() + return self.mouseLockToggledEvent.Event +end + +function MouseLockController:GetMouseLockOffset() + local offsetValueObj = script:FindFirstChild("CameraOffset") + if offsetValueObj and offsetValueObj:IsA("Vector3Value") then + return offsetValueObj.Value + else + -- If CameraOffset object was found but not correct type, destroy + if offsetValueObj then + offsetValueObj:Destroy() + end + offsetValueObj = Instance.new("Vector3Value") + offsetValueObj.Name = "CameraOffset" + offsetValueObj.Value = Vector3.new(1.75,0,0) -- Legacy Default Value + offsetValueObj.Parent = script + end + + if offsetValueObj and offsetValueObj.Value then + return offsetValueObj.Value + end + + return Vector3.new(1.75,0,0) +end + +function MouseLockController:UpdateMouseLockAvailability() + local devAllowsMouseLock = PlayersService.LocalPlayer.DevEnableMouseLock + local devMovementModeIsScriptable = PlayersService.LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.Scriptable + local userHasMouseLockModeEnabled = GameSettings.ControlMode == Enum.ControlMode.MouseLockSwitch + local userHasClickToMoveEnabled = GameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove + local MouseLockAvailable = devAllowsMouseLock and userHasMouseLockModeEnabled and not userHasClickToMoveEnabled and not devMovementModeIsScriptable + + if MouseLockAvailable~=self.enabled then + self:EnableMouseLock(MouseLockAvailable) + end +end + +function MouseLockController:OnBoundKeysObjectChanged(newValue) + self.boundKeys = {} -- Overriding defaults, note: possibly with nothing at all if boundKeysObj.Value is "" or contains invalid values + for token in string.gmatch(newValue,"[^%s,]+") do + for keyCode, keyEnum in pairs(Enum.KeyCode:GetEnumItems()) do + if token == keyEnum.Name then + self.boundKeys[#self.boundKeys+1] = keyEnum + break + end + end + end +end + +--[[ Local Functions ]]-- +function MouseLockController:OnMouseLockToggled() + self.isMouseLocked = not self.isMouseLocked + + if self.isMouseLocked then + local cursorImageValueObj = script:FindFirstChild("CursorImage") + if cursorImageValueObj and cursorImageValueObj:IsA("StringValue") and cursorImageValueObj.Value then + self.savedMouseCursor = Mouse.Icon + Mouse.Icon = cursorImageValueObj.Value + else + if cursorImageValueObj then + cursorImageValueObj:Destroy() + end + cursorImageValueObj = Instance.new("StringValue") + cursorImageValueObj.Name = "CursorImage" + cursorImageValueObj.Value = DEFAULT_MOUSE_LOCK_CURSOR + cursorImageValueObj.Parent = script + self.savedMouseCursor = Mouse.Icon + Mouse.Icon = DEFAULT_MOUSE_LOCK_CURSOR + end + else + if self.savedMouseCursor then + Mouse.Icon = self.savedMouseCursor + self.savedMouseCursor = nil + end + end + + self.mouseLockToggledEvent:Fire() +end + +function MouseLockController:OnInputBegan(input, processed) + if processed then return end + + if input.UserInputType == Enum.UserInputType.Keyboard then + for _, keyCode in pairs(self.boundKeys) do + if keyCode == input.KeyCode then + self:OnMouseLockToggled() + return + end + end + end +end + +--[[ Public API ]]-- +function MouseLockController:IsMouseLocked() + return self.enabled and self.isMouseLocked +end + +function MouseLockController:EnableMouseLock(enable) + if enable~=self.enabled then + + self.enabled = enable + + if self.enabled then + -- Enabling the mode + if self.inputBeganConn then + self.inputBeganConn:Disconnect() + end + self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed) + self:OnInputBegan(input, processed) + end) + else + -- Disabling + -- Restore mouse cursor + if Mouse.Icon~="" then + Mouse.Icon = "" + end + if self.inputBeganConn then + self.inputBeganConn:Disconnect() + end + self.inputBeganConn = nil + + -- If the mode is disabled while being used, fire the event to toggle it off + if self.isMouseLocked then + self.mouseLockToggledEvent:Fire() + end + + self.isMouseLocked = false + end + + end +end + +return MouseLockController diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/OrbitalCamera.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/OrbitalCamera.lua new file mode 100644 index 0000000..9bd0bfa --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/OrbitalCamera.lua @@ -0,0 +1,417 @@ +--[[ + OrbitalCamera - Spherical coordinates control camera for top-down games + 2018 Camera Update - AllYourBlox +--]] + +-- Local private variables and constants +local UNIT_X = Vector3.new(1,0,0) +local UNIT_Y = Vector3.new(0,1,0) +local UNIT_Z = Vector3.new(0,0,1) +local X1_Y0_Z1 = Vector3.new(1,0,1) --Note: not a unit vector, used for projecting onto XZ plane +local ZERO_VECTOR3 = Vector3.new(0,0,0) +local ZERO_VECTOR2 = Vector2.new(0,0) +local TAU = 2 * math.pi + +local VR_PITCH_FRACTION = 0.25 +local tweenAcceleration = math.rad(220) --Radians/Second^2 +local tweenSpeed = math.rad(0) --Radians/Second +local tweenMaxSpeed = math.rad(250) --Radians/Second +local TIME_BEFORE_AUTO_ROTATE = 2.0 --Seconds, used when auto-aligning camera with vehicles +local PORTRAIT_OFFSET = Vector3.new(0,-3,0) + +--[[ Gamepad Support ]]-- +local THUMBSTICK_DEADZONE = 0.2 + +-- Do not edit these values, they are not the developer-set limits, they are limits +-- to the values the camera system equations can correctly handle +local MIN_ALLOWED_ELEVATION_DEG = -80 +local MAX_ALLOWED_ELEVATION_DEG = 80 + +local externalProperties = {} +externalProperties["InitialDistance"] = 25 +externalProperties["MinDistance"] = 10 +externalProperties["MaxDistance"] = 100 +externalProperties["InitialElevation"] = 35 +externalProperties["MinElevation"] = 35 +externalProperties["MaxElevation"] = 35 +externalProperties["ReferenceAzimuth"] = -45 -- Angle around the Y axis where the camera starts. -45 offsets the camera in the -X and +Z directions equally +externalProperties["CWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CW as seen from above +externalProperties["CCWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CCW as seen from above +externalProperties["UseAzimuthLimits"] = false -- Full rotation around Y axis available by default + +local Util = require(script.Parent:WaitForChild("CameraUtils")) + +--[[ Services ]]-- +local PlayersService = game:GetService('Players') +local VRService = game:GetService("VRService") + +--[[ Utility functions specific to OrbitalCamera ]]-- +local function GetValueObject(name, defaultValue) + local valueObj = script:FindFirstChild(name) + if valueObj then + return valueObj.Value + end + return defaultValue +end + +--[[ The Module ]]-- +local BaseCamera = require(script.Parent:WaitForChild("BaseCamera")) +local OrbitalCamera = setmetatable({}, BaseCamera) +OrbitalCamera.__index = OrbitalCamera + + +function OrbitalCamera.new() + local self = setmetatable(BaseCamera.new(), OrbitalCamera) + + self.lastUpdate = tick() + + -- OrbitalCamera-specific members + self.changedSignalConnections = {} + self.refAzimuthRad = nil + self.curAzimuthRad = nil + self.minAzimuthAbsoluteRad = nil + self.maxAzimuthAbsoluteRad = nil + self.useAzimuthLimits = nil + self.curElevationRad = nil + self.minElevationRad = nil + self.maxElevationRad = nil + self.curDistance = nil + self.minDistance = nil + self.maxDistance = nil + + -- Gamepad + self.r3ButtonDown = false + self.l3ButtonDown = false + self.gamepadDollySpeedMultiplier = 1 + + self.lastUserPanCamera = tick() + + self.externalProperties = {} + self.externalProperties["InitialDistance"] = 25 + self.externalProperties["MinDistance"] = 10 + self.externalProperties["MaxDistance"] = 100 + self.externalProperties["InitialElevation"] = 35 + self.externalProperties["MinElevation"] = 35 + self.externalProperties["MaxElevation"] = 35 + self.externalProperties["ReferenceAzimuth"] = -45 -- Angle around the Y axis where the camera starts. -45 offsets the camera in the -X and +Z directions equally + self.externalProperties["CWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CW as seen from above + self.externalProperties["CCWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CCW as seen from above + self.externalProperties["UseAzimuthLimits"] = false -- Full rotation around Y axis available by default + self:LoadNumberValueParameters() + + return self +end + +function OrbitalCamera:LoadOrCreateNumberValueParameter(name, valueType, updateFunction) + local valueObj = script:FindFirstChild(name) + + if valueObj and valueObj:isA(valueType) then + -- Value object exists and is the correct type, use its value + self.externalProperties[name] = valueObj.Value + elseif self.externalProperties[name] ~= nil then + -- Create missing (or replace incorrectly-typed) valueObject with default value + valueObj = Instance.new(valueType) + valueObj.Name = name + valueObj.Parent = script + valueObj.Value = self.externalProperties[name] + else + print("externalProperties table has no entry for ",name) + return + end + + if updateFunction then + if self.changedSignalConnections[name] then + self.changedSignalConnections[name]:Disconnect() + end + self.changedSignalConnections[name] = valueObj.Changed:Connect(function(newValue) + self.externalProperties[name] = newValue + updateFunction(self) + end) + end +end + +function OrbitalCamera:SetAndBoundsCheckAzimuthValues() + self.minAzimuthAbsoluteRad = math.rad(self.externalProperties["ReferenceAzimuth"]) - math.abs(math.rad(self.externalProperties["CWAzimuthTravel"])) + self.maxAzimuthAbsoluteRad = math.rad(self.externalProperties["ReferenceAzimuth"]) + math.abs(math.rad(self.externalProperties["CCWAzimuthTravel"])) + self.useAzimuthLimits = self.externalProperties["UseAzimuthLimits"] + if self.useAzimuthLimits then + self.curAzimuthRad = math.max(self.curAzimuthRad, self.minAzimuthAbsoluteRad) + self.curAzimuthRad = math.min(self.curAzimuthRad, self.maxAzimuthAbsoluteRad) + end +end + +function OrbitalCamera:SetAndBoundsCheckElevationValues() + -- These degree values are the direct user input values. It is deliberate that they are + -- ranged checked only against the extremes, and not against each other. Any time one + -- is changed, both of the internal values in radians are recalculated. This allows for + -- A developer to change the values in any order and for the end results to be that the + -- internal values adjust to match intent as best as possible. + local minElevationDeg = math.max(self.externalProperties["MinElevation"], MIN_ALLOWED_ELEVATION_DEG) + local maxElevationDeg = math.min(self.externalProperties["MaxElevation"], MAX_ALLOWED_ELEVATION_DEG) + + -- Set internal values in radians + self.minElevationRad = math.rad(math.min(minElevationDeg, maxElevationDeg)) + self.maxElevationRad = math.rad(math.max(minElevationDeg, maxElevationDeg)) + self.curElevationRad = math.max(self.curElevationRad, self.minElevationRad) + self.curElevationRad = math.min(self.curElevationRad, self.maxElevationRad) +end + +function OrbitalCamera:SetAndBoundsCheckDistanceValues() + self.minDistance = self.externalProperties["MinDistance"] + self.maxDistance = self.externalProperties["MaxDistance"] + self.curDistance = math.max(self.curDistance, self.minDistance) + self.curDistance = math.min(self.curDistance, self.maxDistance) +end + +-- This loads from, or lazily creates, NumberValue objects for exposed parameters +function OrbitalCamera:LoadNumberValueParameters() + -- These initial values do not require change listeners since they are read only once + self:LoadOrCreateNumberValueParameter("InitialElevation", "NumberValue", nil) + self:LoadOrCreateNumberValueParameter("InitialDistance", "NumberValue", nil) + + -- Note: ReferenceAzimuth is also used as an initial value, but needs a change listener because it is used in the calculation of the limits + self:LoadOrCreateNumberValueParameter("ReferenceAzimuth", "NumberValue", self.SetAndBoundsCheckAzimuthValue) + self:LoadOrCreateNumberValueParameter("CWAzimuthTravel", "NumberValue", self.SetAndBoundsCheckAzimuthValues) + self:LoadOrCreateNumberValueParameter("CCWAzimuthTravel", "NumberValue", self.SetAndBoundsCheckAzimuthValues) + self:LoadOrCreateNumberValueParameter("MinElevation", "NumberValue", self.SetAndBoundsCheckElevationValues) + self:LoadOrCreateNumberValueParameter("MaxElevation", "NumberValue", self.SetAndBoundsCheckElevationValues) + self:LoadOrCreateNumberValueParameter("MinDistance", "NumberValue", self.SetAndBoundsCheckDistanceValues) + self:LoadOrCreateNumberValueParameter("MaxDistance", "NumberValue", self.SetAndBoundsCheckDistanceValues) + self:LoadOrCreateNumberValueParameter("UseAzimuthLimits", "BoolValue", self.SetAndBoundsCheckAzimuthValues) + + -- Internal values set (in radians, from degrees), plus sanitization + self.curAzimuthRad = math.rad(self.externalProperties["ReferenceAzimuth"]) + self.curElevationRad = math.rad(self.externalProperties["InitialElevation"]) + self.curDistance = self.externalProperties["InitialDistance"] + + self:SetAndBoundsCheckAzimuthValues() + self:SetAndBoundsCheckElevationValues() + self:SetAndBoundsCheckDistanceValues() +end + +function OrbitalCamera:GetModuleName() + return "OrbitalCamera" +end + +function OrbitalCamera:SetInitialOrientation(humanoid) + if not humanoid or not humanoid.RootPart then + warn("OrbitalCamera could not set initial orientation due to missing humanoid") + return + end + local newDesiredLook = (humanoid.RootPart.CFrame.lookVector - Vector3.new(0,0.23,0)).unit + local horizontalShift = Util.GetAngleBetweenXZVectors(newDesiredLook, self:GetCameraLookVector()) + local vertShift = math.asin(self:GetCameraLookVector().y) - math.asin(newDesiredLook.y) + if not Util.IsFinite(horizontalShift) then + horizontalShift = 0 + end + if not Util.IsFinite(vertShift) then + vertShift = 0 + end + self.rotateInput = Vector2.new(horizontalShift, vertShift) +end + +--[[ Functions of BaseCamera that are overridden by OrbitalCamera ]]-- +function OrbitalCamera:GetCameraToSubjectDistance() + return self.curDistance +end + +function OrbitalCamera:SetCameraToSubjectDistance(desiredSubjectDistance) + print("OrbitalCamera SetCameraToSubjectDistance ",desiredSubjectDistance) + local player = PlayersService.LocalPlayer + if player then + self.currentSubjectDistance = Util.Clamp(self.minDistance, self.maxDistance, desiredSubjectDistance) + + -- OrbitalCamera is not allowed to go into the first-person range + self.currentSubjectDistance = math.max(self.currentSubjectDistance, self.FIRST_PERSON_DISTANCE_THRESHOLD) + end + self.inFirstPerson = false + self:UpdateMouseBehavior() + return self.currentSubjectDistance +end + +function OrbitalCamera:CalculateNewLookVector(suppliedLookVector, xyRotateVector) + local currLookVector = suppliedLookVector or self:GetCameraLookVector() + local currPitchAngle = math.asin(currLookVector.y) + local yTheta = Util.Clamp(currPitchAngle - math.rad(MAX_ALLOWED_ELEVATION_DEG), currPitchAngle - math.rad(MIN_ALLOWED_ELEVATION_DEG), xyRotateVector.y) + local constrainedRotateInput = Vector2.new(xyRotateVector.x, yTheta) + local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector) + local newLookVector = (CFrame.Angles(0, -constrainedRotateInput.x, 0) * startCFrame * CFrame.Angles(-constrainedRotateInput.y,0,0)).lookVector + return newLookVector +end + +function OrbitalCamera:GetGamepadPan(name, state, input) + if input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then + if self.r3ButtonDown or self.l3ButtonDown then + -- R3 or L3 Thumbstick is depressed, right stick controls dolly in/out + if (input.Position.Y > THUMBSTICK_DEADZONE) then + self.gamepadDollySpeedMultiplier = 0.96 + elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then + self.gamepadDollySpeedMultiplier = 1.04 + else + self.gamepadDollySpeedMultiplier = 1.00 + end + else + if state == Enum.UserInputState.Cancel then + self.gamepadPanningCamera = ZERO_VECTOR2 + return + end + + local inputVector = Vector2.new(input.Position.X, -input.Position.Y) + if inputVector.magnitude > THUMBSTICK_DEADZONE then + self.gamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y) + else + self.gamepadPanningCamera = ZERO_VECTOR2 + end + end + end +end + +function OrbitalCamera:DoGamepadZoom(name, state, input) + if input.UserInputType == self.activeGamepad and (input.KeyCode == Enum.KeyCode.ButtonR3 or input.KeyCode == Enum.KeyCode.ButtonL3) then + if (state == Enum.UserInputState.Begin) then + self.r3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonR3 + self.l3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonL3 + elseif (state == Enum.UserInputState.End) then + if (input.KeyCode == Enum.KeyCode.ButtonR3) then + self.r3ButtonDown = false + elseif (input.KeyCode == Enum.KeyCode.ButtonL3) then + self.l3ButtonDown = false + end + if (not self.r3ButtonDown) and (not self.l3ButtonDown) then + self.gamepadDollySpeedMultiplier = 1.00 + end + end + end +end + +function OrbitalCamera:BindGamepadInputActions() + local ContextActionService = game:GetService('ContextActionService') + + ContextActionService:BindAction("OrbitalCamGamepadPan", function(name, state, input) self:GetGamepadPan(name, state, input) end, false, Enum.KeyCode.Thumbstick2) + ContextActionService:BindAction("OrbitalCamGamepadZoom", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.ButtonR3) + ContextActionService:BindAction("OrbitalCamGamepadZoomAlt", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.ButtonL3) +end + + +-- [[ Update ]]-- +function OrbitalCamera:Update(dt) + local now = tick() + local timeDelta = (now - self.lastUpdate) + local userPanningTheCamera = (self.UserPanningTheCamera == true) + local camera = workspace.CurrentCamera + local newCameraCFrame = camera.CFrame + local newCameraFocus = camera.Focus + local player = PlayersService.LocalPlayer + local humanoid = self:GetHumanoid() + local cameraSubject = camera and camera.CameraSubject + local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat') + local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform') + + if self.lastUpdate == nil or timeDelta > 1 then + self.lastCameraTransform = nil + end + + if self.lastUpdate then + local gamepadRotation = self:UpdateGamepad() + + if self:ShouldUseVRRotation() then + self.RotateInput = self.RotateInput + self:GetVRRotationInput() + else + -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from + local delta = math.min(0.1, timeDelta) + + if gamepadRotation ~= ZERO_VECTOR2 then + userPanningTheCamera = true + self.rotateInput = self.rotateInput + (gamepadRotation * delta) + end + + local angle = 0 + if not (isInVehicle or isOnASkateboard) then + angle = angle + (self.TurningLeft and -120 or 0) + angle = angle + (self.TurningRight and 120 or 0) + end + + if angle ~= 0 then + self.rotateInput = self.rotateInput + Vector2.new(math.rad(angle * delta), 0) + userPanningTheCamera = true + end + end + end + + -- Reset tween speed if user is panning + if userPanningTheCamera then + tweenSpeed = 0 + self.lastUserPanCamera = tick() + end + + local userRecentlyPannedCamera = now - self.lastUserPanCamera < TIME_BEFORE_AUTO_ROTATE + local subjectPosition = self:GetSubjectPosition() + + if subjectPosition and player and camera then + + -- Process any dollying being done by gamepad + -- TODO: Move this + if self.gamepadDollySpeedMultiplier ~= 1 then + self:SetCameraToSubjectDistance(self.currentSubjectDistance * self.gamepadDollySpeedMultiplier) + end + + local VREnabled = VRService.VREnabled + newCameraFocus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame.new(subjectPosition) + + local cameraFocusP = newCameraFocus.p + if VREnabled and not self:IsInFirstPerson() then + local cameraHeight = self:GetCameraHeight() + local vecToSubject = (subjectPosition - camera.CFrame.p) + local distToSubject = vecToSubject.magnitude + + -- Only move the camera if it exceeded a maximum distance to the subject in VR + if distToSubject > self.currentSubjectDistance or self.rotateInput.x ~= 0 then + local desiredDist = math.min(distToSubject, self.currentSubjectDistance) + + -- Note that CalculateNewLookVector is overriden from BaseCamera + vecToSubject = self:CalculateNewLookVector(vecToSubject.unit * X1_Y0_Z1, Vector2.new(self.rotateInput.x, 0)) * desiredDist + + local newPos = cameraFocusP - vecToSubject + local desiredLookDir = camera.CFrame.lookVector + if self.rotateInput.x ~= 0 then + desiredLookDir = vecToSubject + end + local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z) + self.RotateInput = ZERO_VECTOR2 + + newCameraCFrame = CFrame.new(newPos, lookAt) + Vector3.new(0, cameraHeight, 0) + end + else + -- self.RotateInput is a Vector2 of mouse movement deltas since last update + self.curAzimuthRad = self.curAzimuthRad - self.rotateInput.x + + if self.useAzimuthLimits then + self.curAzimuthRad = Util.Clamp(self.minAzimuthAbsoluteRad, self.maxAzimuthAbsoluteRad, self.curAzimuthRad) + else + self.curAzimuthRad = (self.curAzimuthRad ~= 0) and (math.sign(self.curAzimuthRad) * (math.abs(self.curAzimuthRad) % TAU)) or 0 + end + + self.curElevationRad = Util.Clamp(self.minElevationRad, self.maxElevationRad, self.curElevationRad + self.rotateInput.y) + + local cameraPosVector = self.currentSubjectDistance * ( CFrame.fromEulerAnglesYXZ( -self.curElevationRad, self.curAzimuthRad, 0 ) * UNIT_Z ) + local camPos = subjectPosition + cameraPosVector + + newCameraCFrame = CFrame.new(camPos, subjectPosition) + + self.rotateInput = ZERO_VECTOR2 + end + + self.lastCameraTransform = newCameraCFrame + self.lastCameraFocus = newCameraFocus + if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then + self.lastSubjectCFrame = cameraSubject.CFrame + else + self.lastSubjectCFrame = nil + end + end + + self.lastUpdate = now + return newCameraCFrame, newCameraFocus +end + +return OrbitalCamera diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam.lua new file mode 100644 index 0000000..1d24107 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam.lua @@ -0,0 +1,248 @@ +--[[ + Poppercam - Occlusion module that brings the camera closer to the subject when objects are blocking the view. +--]] + +--[[ Camera Maths Utilities Library ]]-- +local Util = require(script.Parent:WaitForChild("CameraUtils")) + +local RunService = game:GetService("RunService") +local PlayersService = game:GetService("Players") +local ContextActionService = game:GetService("ContextActionService") +local UserInputService = game:GetService("UserInputService") +local GameSettings = UserSettings().GameSettings + +-- Note: Zoom and Subject modules return single functions, Zoom() and Subject() respectively, +-- whereas Rotate returns a table with two functions, Step() and GetDelta() +local Zoom = require(script:WaitForChild("Zoom")) +local Subject = require(script:WaitForChild("Subject")) + +local Rotate = require(script:WaitForChild("Rotate")) +local SetTransparency = require(script:WaitForChild("SetTransparency")) + + +-- From Input +local INPUT_PRIORITY = Enum.ContextActionPriority.Low.Value +local ROTATION_SPEED_KEYS = math.rad(120) +local ROTATION_SPEED_MOUSE = Vector2.new(1, 0.77)*math.rad(15) +local ROTATION_SPEED_GAMEPAD = Vector2.new(1, 0.77)*math.rad(165) +local ZOOM_SPEED_MOUSE = 4 +local ZOOM_SPEED_KEYS = 0.75 + +-- Gamepad Thumbstick Helper Function +local AnalogCurve do + local DEADZONE = 0.1 + function AnalogCurve(x) + local y = (math.abs(x) - DEADZONE)/(1 - DEADZONE) + y = 0.255000975*(2^(2.299113817*y) - 1) + return math.clamp(y, 0, 1)*math.sign(x) + end +end + + +local portraitPopperFixFlagExists, portraitPopperFixFlagEnabled = pcall(function() + return UserSettings():IsUserFeatureEnabled("UserPortraitPopperFix") +end) +local FFlagUserPortraitPopperFix = portraitPopperFixFlagExists and portraitPopperFixFlagEnabled + + +--[[ The Module ]]-- +local BaseOcclusion = require(script.Parent:WaitForChild("BaseOcclusion")) +local Poppercam = setmetatable({}, BaseOcclusion) +Poppercam.__index = Poppercam + +function Poppercam.new() + local self = setmetatable(BaseOcclusion.new(), Poppercam) + + self.gamepad = { + Thumbstick2 = Vector2.new(), + } + self.keyboard = { + Left = 0, + Right = 0, + I = 0, + O = 0 + } + self.mouse = { + Movement = Vector2.new(), + Wheel = 0, + } + + self.UISConnections = {} + self.steppedConn = nil + self.worldDt = 1/60 + return self +end + +function Poppercam:GetPanning() + for _, input in ipairs(UserInputService:GetMouseButtonsPressed()) do + if input.UserInputType == Enum.UserInputType.MouseButton2 then + return true + end + end + return false +end + +function Poppercam:GetRotation() + local kKeyboard = Vector2.new(self.keyboard.Left - self.keyboard.Right, 0) + local kGamepad = self.gamepad.Thumbstick2 + local kMouse = self.mouse.Movement + self.mouse.Movement = Vector2.new() + local result = kKeyboard*ROTATION_SPEED_KEYS + kGamepad*ROTATION_SPEED_GAMEPAD + kMouse*ROTATION_SPEED_MOUSE + return result.Y, result.X +end + +function Poppercam:GetZoomDelta() + local kKeyboard = self.keyboard.O - self.keyboard.I + local kMouse = -self.mouse.Wheel + self.mouse.Wheel = 0 + return kKeyboard*ZOOM_SPEED_KEYS + kMouse*ZOOM_SPEED_MOUSE +end + +function Poppercam:GetOcclusionMode() + return Enum.DevCameraOcclusionMode.Zoom +end + +function Poppercam:Enable(enable) + if enable then + -- Enabling + + self:BindInputs() + + + else + -- Disabling + self:UnbindInputs() + self:ResetDeviceInputs() + end +end + +--function Poppercam:MouseLock(isFirstPerson) +-- if isFirstPerson then +-- GameSettings.RotationType = Enum.RotationType.CameraRelative +-- UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter +-- else +-- GameSettings.RotationType = Enum.RotationType.MovementRelative +-- if self:GetPanning() then +-- UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition +-- else +-- UserInputService.MouseBehavior = Enum.MouseBehavior.Default +-- end +-- end +--end + +function Poppercam:Update(renderDt, desiredCameraCFrame, desiredCameraFocus) + --print("desiredCameraCFrame: ",desiredCameraCFrame," desiredCameraFocus: ",desiredCameraFocus) + self.camera = game.Workspace.CurrentCamera + if self.camera.CameraType == Enum.CameraType.Custom then + local subject, subjectTransform, subjectRoot = Subject(self.worldDt) + if subject then + local pitch, yaw = self:GetRotation() + local zoomDelta = self:GetZoomDelta() + local newFocus = Rotate:Step(self.worldDt, subjectTransform, pitch, yaw) -- vehicle yaw spring must use the world step + local zoom, transparency, firstPerson = Zoom(renderDt, zoomDelta, newFocus, subjectRoot) + + --debug.profilebegin('write') + + -- CameraScript should already be doing all of this. Perhaps return this module's + -- recommendation for transparency? + --SetTransparency(subject, transparency) + --self:MouseLock(firstPerson) + --self.camera.Focus = focus + --self.camera.CFrame = focus*CFrame.new(0, 0, zoom) + + local newCameraCFrame = newFocus*CFrame.new(0, 0, zoom) + --debug.profileend() + return newCameraCFrame, newFocus + end + end + + return desiredCameraCFrame, desiredCameraFocus +end + +function Poppercam:ResetDeviceInputs() + for _, device in pairs{self.gamepad, self.keyboard, self.mouse} do + for k, v in pairs(device) do + device[k] = v*0 + end + end +end + +function Poppercam:BindInputs() + local function Thumbstick(action, state, input) + local position = input.Position + self.gamepad[input.KeyCode.Name] = Vector2.new(AnalogCurve(position.X), AnalogCurve(position.Y)) + end + + local function MouseMove(action, state, input) + local delta = input.Delta + self.mouse.Movement = Vector2.new(-delta.X, -delta.Y) + return Enum.ContextActionResult.Pass + end + + local function MouseWheel(action, state, input) + self.mouse.Wheel = input.Position.Z + return Enum.ContextActionResult.Pass + end + + local function Keypress(action, state, input) + self.keyboard[input.KeyCode.Name] = state == Enum.UserInputState.Begin and 1 or 0 + end + + ContextActionService:BindActionAtPriority("Camera/Thumbstick", Thumbstick, false, INPUT_PRIORITY, + Enum.KeyCode.Thumbstick2 + ) + ContextActionService:BindActionAtPriority("Camera/MouseMove", MouseMove, false, INPUT_PRIORITY, + Enum.UserInputType.MouseMovement + ) + ContextActionService:BindActionAtPriority("Camera/MouseWheel", MouseWheel, false, INPUT_PRIORITY, + Enum.UserInputType.MouseWheel + ) + ContextActionService:BindActionAtPriority("Camera/Keypress", Keypress, false, INPUT_PRIORITY, + Enum.KeyCode.Left, + Enum.KeyCode.Right, + Enum.KeyCode.I, + Enum.KeyCode.O + ) + + local resetFunction = function() self:ResetDeviceInputs() end + self.UISConnections["TextBoxFocused"] = UserInputService.TextBoxFocused:Connect(resetFunction) + self.UISConnections["TextBoxFocusReleased"] = UserInputService.TextBoxFocusReleased:Connect(resetFunction) + self.UISConnections["WindowFocused"] = UserInputService.WindowFocused:Connect(resetFunction) + self.UISConnections["WindowFocusReleased"] = UserInputService.WindowFocusReleased:Connect(resetFunction) + + self.steppedConn = RunService.Stepped:Connect(function(t,dt) + self.worldDt = dt + end) +end + +function Poppercam:UnbindInputs() + ContextActionService:UnbindAction("Camera/Thumbstick") + ContextActionService:UnbindAction("Camera/MouseMove") + ContextActionService:UnbindAction("Camera/MouseWheel") + ContextActionService:UnbindAction("Camera/Keypress") + + for _, connection in pairs(self.UISConnections) do + connection:Disconnect() + end + self.UISConnections = {} + + if self.steppedConn then + self.steppedConn:Disconnect() + end +end + +-- Called when character is added +function Poppercam:CharacterAdded(player, character) + +end + +-- Called when character is about to be removed +function Poppercam:CharacterRemoving(player, character) + +end + +function Poppercam:OnCameraSubjectChanged(newSubject) + +end + +return Poppercam \ No newline at end of file diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Rotate.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Rotate.lua new file mode 100644 index 0000000..f7f1624 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Rotate.lua @@ -0,0 +1,45 @@ + +local tau = math.pi*2 +local clamp = math.clamp +local fromEulerYXZ = CFrame.fromEulerAnglesYXZ + +local function lowpass(responsiveness) + local x = 0 + return function(nx) + x = responsiveness*nx + (1 - responsiveness)*x + return x + end +end + +------------------------------------------------------------------------------ + +local PITCH_LIMIT = math.rad(88) + +local pitch = math.rad(-10) +local yaw = math.rad(90) + +local dPitch = 0 +local dYaw = 0 +local oldSubjectTransform + +local dpf = lowpass(0.01) +local dyf = lowpass(0.01) + +------------------------------------------------------------------------------ + +local Rotate = {} do + function Rotate:Step(worldDt, subjectTransform, dPitch, dYaw) + yaw = (yaw + dYaw*worldDt)%tau + pitch = clamp(pitch + dPitch*worldDt, -PITCH_LIMIT, PITCH_LIMIT) + oldSubjectTransform = subjectTransform*fromEulerYXZ(pitch, yaw, 0) + return oldSubjectTransform + end + + function Rotate:GetDelta(dt) + local dp = dpf(dPitch) + local dy = dyf(dYaw) + return fromEulerYXZ(dp*dt, dy*dt, 0) + end +end + +return Rotate diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/SetTransparency.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/SetTransparency.lua new file mode 100644 index 0000000..6dfe652 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/SetTransparency.lua @@ -0,0 +1,19 @@ +local lastSubject = nil +local lastTransparency = 0 + +return function(subject, transparency) + if transparency == lastTransparency and subject == lastSubject then + return + end + + lastSubject = subject + lastTransparency = transparency + + local descendants = subject:GetDescendants() + for i = 1, #descendants do + local obj = descendants[i] + if obj:IsA'BasePart' then + obj.LocalTransparencyModifier = transparency + end + end +end \ No newline at end of file diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Subject.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Subject.lua new file mode 100644 index 0000000..70a261f --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Subject.lua @@ -0,0 +1,75 @@ +local ROOT_FOCUS_OFFSET = CFrame.new(0, 2, 0) + +local Maid = require(script:WaitForChild("Maid")) +local AngleSpring = require(script:WaitForChild("AngleSpring")) + +local camera = game.Workspace.CurrentCamera + +--------------------------------------------------------------------------------------- + +local subjectModel = nil +local subjectRootPart = nil +local subjectIsVehicle = false + +--------------------------------------------------------------------------------------- + +do + local maid = Maid.new() + + local function SubjectChanged() + maid:sweep() + local subj = camera.CameraSubject + + local function handleHumanoid(humanoid, isVehicle) + subjectModel = humanoid.Parent + subjectRootPart = humanoid.RootPart + subjectIsVehicle = isVehicle + maid:mark(humanoid:GetPropertyChangedSignal('RootPart'):Connect(SubjectChanged)) + maid:mark(humanoid:GetPropertyChangedSignal('Parent'):Connect(SubjectChanged)) + end + + if subj and subj:IsA('Humanoid') then + handleHumanoid(subj, false) + elseif subj and subj:IsA('VehicleSeat') then + handleHumanoid(subj.Occupant, true) + else + subjectModel = nil + subjectRootPart = nil + subjectIsVehicle = false + end + end + + SubjectChanged() + camera:GetPropertyChangedSignal'CameraSubject':Connect(SubjectChanged) +end + +--------------------------------------------------------------------------------------- + +local yawSpring = AngleSpring.new(2, 0) + +return function(dt) + if subjectModel and subjectRootPart then + + local subjectTransform = subjectRootPart.CFrame*ROOT_FOCUS_OFFSET + if not subjectIsVehicle then + subjectTransform = CFrame.new(subjectTransform.p) + else + local _, yaw = subjectTransform:toEulerAnglesYXZ() + yawSpring:setGoal(yaw) + yawSpring:step(dt) + subjectTransform = CFrame.new(subjectTransform.p)*CFrame.fromEulerAnglesYXZ(0, yawSpring:getState(), 0) + end + + return subjectModel, subjectTransform, subjectRootPart + else + return nil, CFrame.new(), Vector3.new() + end +end + + + + + + + + diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Subject/AngleSpring.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Subject/AngleSpring.lua new file mode 100644 index 0000000..a438aaa --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Subject/AngleSpring.lua @@ -0,0 +1,49 @@ +-- Critically damped angular spring + +local AngleSpring = {} do + AngleSpring.__index = AngleSpring + + local pi = math.pi + local tau = math.pi*2 + local exp = math.exp + + function AngleSpring.new(frequency, position) + local self = setmetatable({}, AngleSpring) + + self.f = frequency -- nominal frequency + self.g = position -- goal position + self.p = position -- position + self.v = position*0 -- velocity (*0 so that the types match) + + return self + end + + function AngleSpring:setGoal(g) + self.g = g + end + + function AngleSpring:getState() + return self.p, self.v + end + + function AngleSpring:step(dt) + local f = self.f*tau + local g = self.g + local p = self.p + local v = self.v + + local offset = (p - g + pi)%tau - pi + local decay = exp(-dt*f) + + -- Given: + -- f^2*(x[dt] - g) + 2*d*f*x'[dt] + x''[dt] = 0, + -- x[0] = p, + -- x'[0] = v + -- Solve for x[dt], x'[dt] + + self.p = (v*dt + offset*(f*dt + 1))*decay + g + self.v = (v - f*dt*(offset*f + v))*decay + end +end + +return AngleSpring \ No newline at end of file diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Subject/Maid.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Subject/Maid.lua new file mode 100644 index 0000000..91b44f2 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Subject/Maid.lua @@ -0,0 +1,41 @@ +local Maid = {} do + Maid.__index = Maid + + local destructors = { + ['function'] = function(item) + item() + end, + ['Instance'] = function(item) + item:Destroy() + end, + ['RBXScriptConnection'] = function(item) + item:Disconnect() + end, + } + + function Maid.new() + local self = setmetatable({}, Maid) + self.trash = {} + return self + end + + function Maid:mark(item) + if destructors[typeof(item)] then + local trash = self.trash + trash[#trash + 1] = item + else + error(('No destructor defined for type "%s"'):format(typeof(item)), 2) + end + end + + function Maid:sweep() + local trash = self.trash + for i = 1, #trash do + local item = trash[i] + destructors[typeof(item)](item) + end + self.trash = {} + end +end + +return Maid diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Zoom.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Zoom.lua new file mode 100644 index 0000000..626c566 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Zoom.lua @@ -0,0 +1,82 @@ +-------------------------------------------------------------------------------- +-- Zoom.lua +-- Controls the distance between the focus and the camera. +-------------------------------------------------------------------------------- + +local ZOOM_STIFFNESS = 4.5 +local ZOOM_DEFAULT = 16 +local ZOOM_ACCELERATION = 0.0375 + +local ZOOM_OPAQUE = 2 +local ZOOM_TRANSPARENT = 0.5 + +local ConstrainedSpring = require(script:WaitForChild("ConstrainedSpring")) +local Popper = require(script:WaitForChild("Popper")) + +local cframe = CFrame.new +local clamp = math.clamp +local min = math.min +local max = math.max + +local DIST_MIN, DIST_MAX do + local Player = game:GetService("Players").LocalPlayer + + local function updateBounds() + DIST_MIN = Player.CameraMinZoomDistance + DIST_MAX = Player.CameraMaxZoomDistance + end + + updateBounds() + + Player:GetPropertyChangedSignal('CameraMinZoomDistance'):Connect(updateBounds) + Player:GetPropertyChangedSignal('CameraMaxZoomDistance'):Connect(updateBounds) +end + +-------------------------------------------------------------------------------- + +local zoomSpring = ConstrainedSpring.new(ZOOM_STIFFNESS, ZOOM_DEFAULT, DIST_MIN, DIST_MAX) + +local function zoomToTransparency(zoom) + zoom = clamp(zoom, ZOOM_TRANSPARENT, ZOOM_OPAQUE) + local t = (zoom - ZOOM_TRANSPARENT)/(ZOOM_OPAQUE - ZOOM_TRANSPARENT) + return 1 - (3 - 2*t)*t*t +end + +local function stepTargetZoom(z, dz, zoomMin, zoomMax) + z = clamp(z + dz*(1 + z*ZOOM_ACCELERATION), zoomMin, zoomMax) + if z < ZOOM_OPAQUE then + z = dz <= 0 and zoomMin or ZOOM_OPAQUE + end + return z +end + +-------------------------------------------------------------------------------- + +function Zoom(renderDt, zoomDelta, focus, subjectRootPart) + + local prmax = max(zoomSpring.x, stepTargetZoom(zoomSpring.goal, zoomDelta, DIST_MIN, DIST_MAX)) + + local poppedZoom = Popper(focus*cframe(0, 0, DIST_MIN), prmax - DIST_MIN, subjectRootPart) + DIST_MIN + + local zoomMin = DIST_MIN + local zoomMax = min(DIST_MAX, poppedZoom) + zoomSpring:setBounds(zoomMin, zoomMax) + + local isPopped = zoomSpring.goal > zoomMax + + if zoomDelta ~= 0 then + if not isPopped then + zoomSpring.goal = stepTargetZoom(zoomSpring.goal, zoomDelta, zoomMin, zoomMax) + elseif zoomDelta < 0 then + zoomSpring.goal = stepTargetZoom(zoomMax, zoomDelta, zoomMin, zoomMax) + end + end + + local zoom = zoomSpring:step(renderDt) + local fp = zoomSpring.goal == DIST_MIN + local transparency = fp and zoomToTransparency(zoom) or 0 + + return zoom, transparency, fp +end + +return Zoom diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Zoom/ConstrainedSpring.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Zoom/ConstrainedSpring.lua new file mode 100644 index 0000000..a8cd548 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Zoom/ConstrainedSpring.lua @@ -0,0 +1,61 @@ +local ConstrainedSpring = {} do + ConstrainedSpring.__index = ConstrainedSpring + + local exp = math.exp + local tau = 2*math.pi + + function ConstrainedSpring.new(f, x, min, max) + x = math.clamp(x, min, max) + local self = setmetatable({}, ConstrainedSpring) + self.f = f + self.x = x + self.dx = 0 + self.min = min + self.max = max + self.goal = x + return self + end + + function ConstrainedSpring:step(dt) + local f = self.f*tau + local x = self.x + local dx = self.dx + local min = self.min + local max = self.max + local goal = self.goal + + -- Given: + -- f^2*(x[dt] - g) + 2*d*f*x'[dt] + x''[dt] = 0, + -- x[0] = p, + -- x'[0] = v + -- Solve for x[dt], x'[dt] + + local offset = goal - x + local step = f*dt + local decay = exp(-step) + + x = goal + (dx*dt - offset*(step + 1))*decay + dx = ((offset*f - dx)*step + dx)*decay + + -- Constrain + if x < min then + x = min + dx = 0 + elseif x > max then + x = max + dx = 0 + end + + self.x = x + self.dx = dx + + return x + end + + function ConstrainedSpring:setBounds(min, max) + self.min = min + self.max = max + end +end + +return ConstrainedSpring \ No newline at end of file diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Zoom/Popper.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Zoom/Popper.lua new file mode 100644 index 0000000..84ee231 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam/Zoom/Popper.lua @@ -0,0 +1,341 @@ +-------------------------------------------------------------------------------- +-- Popper.lua +-- Prevents your camera from clipping through walls. +-------------------------------------------------------------------------------- + +local Rotate = require(script.Parent.Parent:WaitForChild("Rotate")) + +local camera = game.Workspace.CurrentCamera + +local min = math.min +local tan = math.tan +local rad = math.rad +local inf = math.huge +local ray = Ray.new + +local function eraseFromEnd(t, toSize) + for i = #t, toSize + 1, -1 do + t[i] = nil + end +end + +local nearZ, projX, projY do + local function updateProjection() + local fov = rad(camera.FieldOfView) + local view = camera.ViewportSize + local ar = view.X/view.Y + + projY = 2*tan(fov/2) + projX = ar*projY + end + + camera:GetPropertyChangedSignal('FieldOfView'):Connect(updateProjection) + camera:GetPropertyChangedSignal('ViewportSize'):Connect(updateProjection) + + updateProjection() + + nearZ = camera.NearZ + camera:GetPropertyChangedSignal('NearPlaneZ'):Connect(function() + nearZ = camera.NearPlaneZ + end) +end + +local blacklist = {} do + local charMap = {} + + local function refreshIgnoreList() + local n = 1 + blacklist = {} + for player, character in pairs(charMap) do + blacklist[n] = character + n = n + 1 + end + end + + local function playerAdded(player) + local function characterAdded(character) + charMap[player] = character + refreshIgnoreList() + end + local function characterRemoving() + charMap[player] = nil + refreshIgnoreList() + end + + player.CharacterAdded:Connect(characterAdded) + player.CharacterRemoving:Connect(characterRemoving) + if player.Character then + characterAdded(player.Character) + end + end + + local function playerRemoving(player) + charMap[player] = nil + refreshIgnoreList() + end + + do + local Players = game:GetService('Players') + + Players.PlayerAdded:Connect(playerAdded) + Players.PlayerRemoving:Connect(playerRemoving) + + for _, player in ipairs(Players:GetPlayers()) do + playerAdded(player) + end + refreshIgnoreList() + end +end + +-------------------------------------------------------------------------------------------- +-- Popper uses the level geometry find an upper bound on subject-to-camera distance. +-- +-- Hard limits are applied immediately and unconditionally. They're generally caused +-- when level geometry intersects with the near plane (with exceptions, see below). +-- +-- Soft limits are only applied under certain conditions. +-- They're caused when level geometry occludes the subject without actually intersecting +-- with the near plane at the target distance. +-- +-- Soft limits can be promoted to hard limits and hard limits can be demoted to soft limits. +-- We usually don't want the latter to happen. +-- +-- A soft limit will be promoted to a hard limit if an obstruction +-- lies between the current and target camera positions. +-------------------------------------------------------------------------------------------- + +local subjectRootPart + +-- @todo cache +local function canCollide(part) + return subjectRootPart:CanCollideWith(part) +end + +local function canOcclude(part) + -- Filter for opaque, interactable objects + return + part.Transparency < 0.2 and + canCollide(part) and + not part:IsA('TrussPart') +end + +-- Offsets for the volume visibility test +local SCAN_SAMPLE_OFFSETS = { + Vector2.new( 0.4, 0.0), + Vector2.new(-0.4, 0.0), + Vector2.new( 0.0,-0.4), + Vector2.new( 0.0, 0.4), + Vector2.new( 0.0, 0.2), +} + +-------------------------------------------------------------------------------- +-- Piercing raycasts + +local function getCollisionPoint(origin, dir, blacklist) + local originalSize = #blacklist + repeat + local hitPart, hitPoint = workspace:FindPartOnRayWithIgnoreList( + ray(origin, dir), blacklist, false, true + ) + + if hitPart then + if canCollide(hitPart) then + eraseFromEnd(blacklist, originalSize) + return hitPoint, true + end + blacklist[#blacklist + 1] = hitPart + end + until not hitPart + + eraseFromEnd(blacklist, originalSize) + return origin + dir, false +end + +-------------------------------------------------------------------------------- + +local function queryPoint(origin, unitDir, dist, lastPos) + debug.profilebegin('queryPoint') + + local originalSize = #blacklist + + dist = dist + nearZ + local target = origin + unitDir*dist + + local softLimit = inf + local hardLimit = inf + local movingOrigin = origin + + repeat + local entryPart, entryPos = workspace:FindPartOnRayWithIgnoreList(ray(movingOrigin, target - movingOrigin), blacklist, false, true) + + if entryPart then + if canOcclude(entryPart) then + local wl = {entryPart} + local exitPart = workspace:FindPartOnRayWithWhitelist(ray(target, entryPos - target), wl, false, true) + + local lim = (entryPos - origin).Magnitude + + if exitPart then + local promote = false + if lastPos then + promote = + workspace:FindPartOnRayWithWhitelist(ray(lastPos, target - lastPos), wl, false, true) or + workspace:FindPartOnRayWithWhitelist(ray(target, lastPos - target), wl, false, true) + end + + if promote then + -- Ostensibly a soft limit, but the camera has passed through it in the last frame, so promote to a hard limit. + hardLimit = lim + elseif dist < softLimit then + -- Trivial soft limit + softLimit = lim + end + else + -- Trivial hard limit + hardLimit = lim + end + end + + blacklist[#blacklist + 1] = entryPart + movingOrigin = entryPos + end + until hardLimit < inf or not entryPart + + eraseFromEnd(blacklist, originalSize) + + debug.profileend() + return softLimit - nearZ, hardLimit - nearZ +end + +local function queryViewport(focus, dist) + debug.profilebegin('queryViewport') + + local fP = focus.p + local fX = focus.rightVector + local fY = focus.upVector + local fZ = -focus.lookVector + + local viewport = camera.ViewportSize + + local hardBoxLimit = inf + local softBoxLimit = inf + + -- Center the viewport on the PoI, sweep points on the edge towards the target, and take the minimum limits + for viewX = 0, 1 do + local worldX = fX*((viewX - 0.5)*projX) + + for viewY = 0, 1 do + local worldY = fY*((viewY - 0.5)*projY) + + local origin = fP + nearZ*(worldX + worldY) + local lastPos = camera:ViewportPointToRay( + viewport.x*viewX, + viewport.y*viewY + ).Origin + + local softPointLimit, hardPointLimit = queryPoint(origin, fZ, dist, lastPos) + + if hardPointLimit < hardBoxLimit then + hardBoxLimit = hardPointLimit + end + if softPointLimit < softBoxLimit then + softBoxLimit = softPointLimit + end + end + end + debug.profileend() + + return softBoxLimit, hardBoxLimit +end + +local function getBodyScale(humanoid, scaleName) + local scaleValue = humanoid:FindFirstChild(scaleName) + if scaleValue and scaleValue:IsA('NumberValue') then + return scaleValue.Value + end + return 1 +end + +local function testPromotion(focus, dist) + debug.profilebegin('testPromotion') + + local fP = focus.p + local fX = focus.rightVector + local fY = focus.upVector + local fZ = -focus.lookVector + + do + -- Dead reckoning the camera rotation and focus + debug.profilebegin('extrapolate') + + local SAMPLE_DT = 0.0625 + local SAMPLE_MAX_T = 1.25 + + local vel = subjectRootPart.Velocity + local speed = vel.Magnitude + local maxDist = (getCollisionPoint(fP, vel*SAMPLE_MAX_T) - fP).Magnitude + + for dt = 0, min(SAMPLE_MAX_T, maxDist/speed), SAMPLE_DT do + local origin = fP + vel*dt + local dir = -(focus*Rotate:GetDelta(dt)).lookVector + + if queryPoint(origin, dir, dist) >= dist then + return false + end + end + + debug.profileend() + end + + do + -- Test screen-space offsets from the focus for the presence of soft limits + debug.profilebegin('testOffsets') + + local humanoid = subjectRootPart.Parent:FindFirstChildOfClass('Humanoid') + + if humanoid then + local scaleX = getBodyScale(humanoid, 'BodyWidthScale') + local scaleY = getBodyScale(humanoid, 'BodyHeightScale') + local scaleZ = getBodyScale(humanoid, 'BodyDepthScale') + + local sampleScale = Vector2.new(math.sqrt(scaleX*scaleX + scaleZ*scaleZ), scaleY) + + for _, offset in ipairs(SCAN_SAMPLE_OFFSETS) do + local scaledOffset = offset*sampleScale + local pos, isHit = getCollisionPoint(fP, fX*scaledOffset.x + fY*scaledOffset.y) + if queryPoint(pos, (fP + fZ*dist - pos).Unit, dist) == inf then + return false + end + end + end + + debug.profileend() + end + + debug.profileend() + return true +end + +-------------------------------------------------------------------------------- + +function Popper(focus, targetDist, _subjectRootPart) + debug.profilebegin('popper') + + subjectRootPart = _subjectRootPart + + local dist = targetDist + local soft, hard = queryViewport(focus, targetDist) + if hard < dist then + dist = hard + end + if soft < dist and testPromotion(focus, targetDist) then + dist = soft + end + + subjectRootPart = nil + + debug.profileend() + return dist +end + +return Popper diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam_Classic.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam_Classic.lua new file mode 100644 index 0000000..d57c603 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/Poppercam_Classic.lua @@ -0,0 +1,177 @@ +--[[ + Poppercam - Occlusion module that brings the camera closer to the subject when objects are blocking the view + Refactored for 2018 Camera Update but functionality is unchanged - AllYourBlox +--]] + +--[[ Camera Maths Utilities Library ]]-- +local Util = require(script.Parent:WaitForChild("CameraUtils")) + +local PlayersService = game:GetService("Players") +local POP_RESTORE_RATE = 0.3 +local MIN_CAMERA_ZOOM = 0.5 +local VALID_SUBJECTS = { + 'Humanoid', + 'VehicleSeat', + 'SkateboardPlatform', +} + +local portraitPopperFixFlagExists, portraitPopperFixFlagEnabled = pcall(function() + return UserSettings():IsUserFeatureEnabled("UserPortraitPopperFix") +end) +local FFlagUserPortraitPopperFix = portraitPopperFixFlagExists and portraitPopperFixFlagEnabled + + +--[[ The Module ]]-- +local BaseOcclusion = require(script.Parent:WaitForChild("BaseOcclusion")) +local Poppercam = setmetatable({}, BaseOcclusion) +Poppercam.__index = Poppercam + +function Poppercam.new() + local self = setmetatable(BaseOcclusion.new(), Poppercam) + + self.camera = nil + self.cameraSubjectChangeConn = nil + + self.subjectPart = nil + + self.playerCharacters = {} -- For ignoring in raycasts + self.vehicleParts = {} -- Also just for ignoring + + self.lastPopAmount = 0 + self.lastZoomLevel = 0 + self.popperEnabled = false + + return self +end + +function Poppercam:GetOcclusionMode() + return Enum.DevCameraOcclusionMode.Zoom +end + +function Poppercam:Enable(enable) + +end + +-- Called when character is added +function Poppercam:CharacterAdded(player, character) + self.playerCharacters[player] = character +end + +-- Called when character is about to be removed +function Poppercam:CharacterRemoving(player, character) + self.playerCharacters[player] = nil +end + +function Poppercam:Update(dt, desiredCameraCFrame, desiredCameraFocus) + if self.popperEnabled then + self.camera = game.Workspace.CurrentCamera + local newCameraCFrame = desiredCameraCFrame + local focusPoint = desiredCameraFocus.p + + if FFlagUserPortraitPopperFix and self.subjectPart then + focusPoint = self.subjectPart.CFrame.p + end + + local ignoreList = {} + for _, character in pairs(self.playerCharacters) do + ignoreList[#ignoreList + 1] = character + end + for i = 1, #self.vehicleParts do + ignoreList[#ignoreList + 1] = self.vehicleParts[i] + end + + -- Get largest cutoff distance + -- This swapping and setting of the camera CFrame is a hack because we don't actually want to set it yet, + -- but GetLargestCutoffDistance can't be passed the desiredCameraCFrame, it only works for where the camera + -- currently is + local prevCameraCFrame = self.camera.CFrame + self.camera.CFrame = desiredCameraCFrame + self.camera.Focus = desiredCameraFocus + local largest = self.camera:GetLargestCutoffDistance(ignoreList) + + -- Then check if the player zoomed since the last frame, + -- and if so, reset our pop history so we stop tweening + local zoomLevel = (desiredCameraCFrame.p - focusPoint).Magnitude + if math.abs(zoomLevel - self.lastZoomLevel) > 0.001 then + self.lastPopAmount = 0 + end + + -- Finally, zoom the camera in (pop) by that most-cut-off amount, or the last pop amount if that's more + local popAmount = largest + if self.lastPopAmount > popAmount then + popAmount = self.lastPopAmount + end + + + -- TODO: Don't let Poppercam directly manipulate camera like this + if popAmount > 0 then + newCameraCFrame = desiredCameraCFrame + (desiredCameraCFrame.lookVector * popAmount) + self.lastPopAmount = popAmount - POP_RESTORE_RATE -- Shrink it for the next frame + if self.lastPopAmount < 0 then + self.lastPopAmount = 0 + end + end + + self.lastZoomLevel = zoomLevel + + -- Stop shift lock being able to see through walls by manipulating Camera focus inside the wall +-- if EnabledCamera and EnabledCamera:GetShiftLock() and not EnabledCamera:IsInFirstPerson() then +-- if EnabledCamera:GetCameraActualZoom() < 1 then +-- local subjectPosition = EnabledCamera.lastSubjectPosition +-- if subjectPosition then +-- Camera.Focus = CFrame_new(subjectPosition) +-- Camera.CFrame = CFrame_new(subjectPosition - MIN_CAMERA_ZOOM*EnabledCamera:GetCameraLook(), subjectPosition) +-- end +-- end +-- end + return newCameraCFrame, desiredCameraFocus + end + + -- Return unchanged values + return desiredCameraCFrame, desiredCameraFocus +end + +function Poppercam:OnCameraSubjectChanged(newSubject) + self.vehicleParts = {} + + self.lastPopAmount = 0 + + if newSubject then + -- Determine if we should be popping at all + self.popperEnabled = false + for _, subjectType in pairs(VALID_SUBJECTS) do + if newSubject:IsA(subjectType) then + self.popperEnabled = true + break + end + end + + -- Get all parts of the vehicle the player is controlling + if newSubject:IsA('VehicleSeat') then + self.vehicleParts = newSubject:GetConnectedParts(true) + end + + if FFlagUserPortraitPopperFix then + if newSubject:IsA("BasePart") then + self.subjectPart = newSubject + elseif newSubject:IsA("Model") then + if newSubject.PrimaryPart then + self.subjectPart = newSubject.PrimaryPart + else + -- Model has no PrimaryPart set, just use first BasePart + -- we can find as better-than-nothing solution (can still fail) + for _, child in pairs(newSubject:GetChildren()) do + if child:IsA("BasePart") then + self.subjectPart = child + break + end + end + end + elseif newSubject:IsA("Humanoid") then + self.subjectPart = newSubject.RootPart + end + end + end +end + +return Poppercam \ No newline at end of file diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/TransparencyController.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/TransparencyController.lua new file mode 100644 index 0000000..bcc61e1 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/CameraScript/TransparencyController.lua @@ -0,0 +1,189 @@ +--[[ + TransparencyController - Manages transparency of player character at close camera-to-subject distances + 2018 Camera Update - AllYourBlox +--]] + +local MAX_TWEEN_RATE = 2.8 -- per second + +local Util = require(script.Parent:WaitForChild("CameraUtils")) + +--[[ The Module ]]-- +local TransparencyController = {} +TransparencyController.__index = TransparencyController + +function TransparencyController.new() + local self = setmetatable({}, TransparencyController) + + self.lastUpdate = tick() + self.transparencyDirty = false + self.enabled = false + self.lastTransparency = nil + + self.descendantAddedConn, self.descendantRemovingConn = nil, nil + self.toolDescendantAddedConns = {} + self.toolDescendantRemovingConns = {} + self.cachedParts = {} + + return self +end + + +function TransparencyController:HasToolAncestor(object) + if object.Parent == nil then return false end + return object.Parent:IsA('Tool') or self:HasToolAncestor(object.Parent) +end + +function TransparencyController:IsValidPartToModify(part) + if part:IsA('BasePart') or part:IsA('Decal') then + return not self:HasToolAncestor(part) + end + return false +end + +function TransparencyController:CachePartsRecursive(object) + if object then + if self:IsValidPartToModify(object) then + self.cachedParts[object] = true + self.transparencyDirty = true + end + for _, child in pairs(object:GetChildren()) do + self:CachePartsRecursive(child) + end + end +end + +function TransparencyController:TeardownTransparency() + for child, _ in pairs(self.cachedParts) do + child.LocalTransparencyModifier = 0 + end + self.cachedParts = {} + self.transparencyDirty = true + self.lastTransparency = nil + + if self.descendantAddedConn then + self.descendantAddedConn:disconnect() + self.descendantAddedConn = nil + end + if self.descendantRemovingConn then + self.descendantRemovingConn:disconnect() + self.descendantRemovingConn = nil + end + for object, conn in pairs(self.toolDescendantAddedConns) do + conn:Disconnect() + self.toolDescendantAddedConns[object] = nil + end + for object, conn in pairs(self.toolDescendantRemovingConns) do + conn:Disconnect() + self.toolDescendantRemovingConns[object] = nil + end +end + +function TransparencyController:SetupTransparency(character) + self:TeardownTransparency() + + if self.descendantAddedConn then self.descendantAddedConn:disconnect() end + self.descendantAddedConn = character.DescendantAdded:Connect(function(object) + -- This is a part we want to invisify + if self:IsValidPartToModify(object) then + self.cachedParts[object] = true + self.transparencyDirty = true + -- There is now a tool under the character + elseif object:IsA('Tool') then + if self.toolDescendantAddedConns[object] then self.toolDescendantAddedConns[object]:Disconnect() end + self.toolDescendantAddedConns[object] = object.DescendantAdded:Connect(function(toolChild) + self.cachedParts[toolChild] = nil + if toolChild:IsA('BasePart') or toolChild:IsA('Decal') then + -- Reset the transparency + toolChild.LocalTransparencyModifier = 0 + end + end) + if self.toolDescendantRemovingConns[object] then self.toolDescendantRemovingConns[object]:disconnect() end + self.toolDescendantRemovingConns[object] = object.DescendantRemoving:Connect(function(formerToolChild) + wait() -- wait for new parent + if character and formerToolChild and formerToolChild:IsDescendantOf(character) then + if self:IsValidPartToModify(formerToolChild) then + self.cachedParts[formerToolChild] = true + self.transparencyDirty = true + end + end + end) + end + end) + if self.descendantRemovingConn then self.descendantRemovingConn:disconnect() end + self.descendantRemovingConn = character.DescendantRemoving:connect(function(object) + if self.cachedParts[object] then + self.cachedParts[object] = nil + -- Reset the transparency + object.LocalTransparencyModifier = 0 + end + end) + self:CachePartsRecursive(character) +end + + +function TransparencyController:Enable(enable) + if self.enabled ~= enable then + self.enabled = enable + self:Update() + end +end + +function TransparencyController:SetSubject(subject) + local character = nil + if subject and subject:IsA("Humanoid") then + character = subject.Parent + end + if subject and subject:IsA("VehicleSeat") and subject.Occupant then + character = subject.Occupant.Parent + end + if character then + self:SetupTransparency(character) + else + self:TeardownTransparency() + end +end + +function TransparencyController:Update() + local instant = false + local now = tick() + local currentCamera = workspace.CurrentCamera + + if currentCamera then + local transparency = 0 + if not self.enabled then + instant = true + else + local distance = (currentCamera.Focus.p - currentCamera.CoordinateFrame.p).magnitude + transparency = (distance<2) and (1.0-(distance-0.5)/1.5) or 0 --(7 - distance) / 5 + if transparency < 0.5 then + transparency = 0 + end + + if self.lastTransparency then + local deltaTransparency = transparency - self.lastTransparency + + -- Don't tween transparency if it is instant or your character was fully invisible last frame + if not instant and transparency < 1 and self.lastTransparency < 0.95 then + local maxDelta = MAX_TWEEN_RATE * (now - self.lastUpdate) + deltaTransparency = Util.Clamp(-maxDelta, maxDelta, deltaTransparency) + end + transparency = self.lastTransparency + deltaTransparency + else + self.transparencyDirty = true + end + + transparency = Util.Clamp(0, 1, Util.Round(transparency, 2)) + end + + if self.transparencyDirty or self.lastTransparency ~= transparency then + for child, _ in pairs(self.cachedParts) do + child.LocalTransparencyModifier = transparency + end + self.transparencyDirty = false + self.lastTransparency = transparency + end + end + self.lastUpdate = now +end + +return TransparencyController diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript.lua new file mode 100644 index 0000000..666d47a --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript.lua @@ -0,0 +1,328 @@ +--[[ + ControlScript - This module manages the selection of the current character control module + and calls update() on the active module on RenderStepped + + 2018 PlayerScripts Update - AllYourBlox +--]] +local ControlScript = {} + +--[[ Roblox Services ]]-- +local a = shared +local Players = game:GetService("Players") +local RunService = game:GetService("RunService") +local UserInputService = game:GetService("UserInputService") +local Settings = UserSettings() +local GameSettings = Settings.GameSettings + +local activeControlModule = nil -- Used to prevent unnecessarily expensive checks on each input event +local activeController = nil +local touchJumpController = nil +local moveFunction = Players.LocalPlayer.Move +local humanoid = nil +local lastInputType = Enum.UserInputType.None +local cameraRelative = true + +-- For Roblox VehicleController +local humanoidSeatedConn = nil +local vehicleController = nil + +local touchControlFrame = nil + +-- Modules - each returns a new() constructor function used to create controllers as needed +local Keyboard = require(script:WaitForChild("Keyboard")) +local Gamepad = require(script:WaitForChild("Gamepad")) +local TouchDPad = require(script:WaitForChild("TouchDPad")) +local DynamicThumbstick = require(script:WaitForChild("DynamicThumbstick")) + +-- These controllers handle only walk/run movement, jumping is handled by the +-- TouchJump controller if any of these are active +local ClickToMove = require(script:WaitForChild("ClickToMoveController")) +local TouchThumbstick = require(script:WaitForChild("TouchThumbstick")) +local TouchThumbpad = require(script:WaitForChild("TouchThumbpad")) +local TouchJump = require(script:WaitForChild("TouchJump")) + +-- Old control script notes that this must be required but is not used like a control module? +local VehicleController = require(script:WaitForChild("VehicleController")) + +-- The Modules above are used to construct controller instances as-needed, and this +-- table is a map from Module to the instance created from it +local controllers = {} + +-- Mapping from movement mode and lastInputType enum values to control modules to avoid huge if elseif switching +local movementEnumToModuleMap = { + [Enum.TouchMovementMode.DPad] = TouchDPad, + [Enum.DevTouchMovementMode.DPad] = TouchDPad, + [Enum.TouchMovementMode.Thumbpad] = TouchThumbpad, + [Enum.DevTouchMovementMode.Thumbpad] = TouchThumbpad, + [Enum.TouchMovementMode.Thumbstick] = TouchThumbstick, + [Enum.DevTouchMovementMode.Thumbstick] = TouchThumbstick, + [Enum.TouchMovementMode.DynamicThumbstick] = DynamicThumbstick, + [Enum.DevTouchMovementMode.DynamicThumbstick] = DynamicThumbstick, + [Enum.TouchMovementMode.ClickToMove] = ClickToMove, + [Enum.DevTouchMovementMode.ClickToMove] = ClickToMove, + + -- Current default + [Enum.TouchMovementMode.Default] = TouchThumbstick, + + [Enum.ComputerMovementMode.Default] = Keyboard, + [Enum.ComputerMovementMode.KeyboardMouse] = Keyboard, + [Enum.DevComputerMovementMode.KeyboardMouse] = Keyboard, + [Enum.DevComputerMovementMode.Scriptable] = nil, + [Enum.ComputerMovementMode.ClickToMove] = ClickToMove, + [Enum.DevComputerMovementMode.ClickToMove] = ClickToMove, +} + +-- Keyboard controller is really keyboard and mouse controller +local computerInputTypeToModuleMap = { + [Enum.UserInputType.Keyboard] = Keyboard, + [Enum.UserInputType.MouseButton1] = Keyboard, + [Enum.UserInputType.MouseButton2] = Keyboard, + [Enum.UserInputType.MouseButton3] = Keyboard, + [Enum.UserInputType.MouseWheel] = Keyboard, + [Enum.UserInputType.MouseMovement] = Keyboard, + [Enum.UserInputType.Gamepad1] = Gamepad, + [Enum.UserInputType.Gamepad2] = Gamepad, + [Enum.UserInputType.Gamepad3] = Gamepad, + [Enum.UserInputType.Gamepad4] = Gamepad, +} + +-- Returns module (possibly nil) and success code to differentiate returning nil due to error vs Scriptable +local function SelectComputerMovementModule() + if not (UserInputService.KeyboardEnabled or UserInputService.GamepadEnabled) then + return nil, false + end + local computerModule = nil + local DevMovementMode = Players.LocalPlayer.DevComputerMovementMode + if DevMovementMode == Enum.DevComputerMovementMode.UserChoice then + computerModule = computerInputTypeToModuleMap[lastInputType] + if not computerModule then + computerModule = movementEnumToModuleMap[GameSettings.ComputerMovementMode] + end + elseif DevMovementMode == Enum.DevComputerMovementMode.Scriptable then + return nil, true + elseif DevMovementMode == Enum.DevComputerMovementMode.ClickToMove then + computerModule = ClickToMove + else + computerModule = computerInputTypeToModuleMap[lastInputType] + if not computerModule then + computerModule = movementEnumToModuleMap[DevMovementMode] + end + end + return computerModule, true +end + +-- Choose current Touch control module based on settings (user, dev) +-- Returns module (possibly nil) and success code to differentiate returning nil due to error vs Scriptable +local function SelectTouchModule() + if not UserInputService.TouchEnabled then + return nil, false + end + local touchModule = nil + local DevMovementMode = Players.LocalPlayer.DevTouchMovementMode + if DevMovementMode == Enum.DevTouchMovementMode.UserChoice then + touchModule = movementEnumToModuleMap[GameSettings.TouchMovementMode] + elseif DevMovementMode == Enum.DevTouchMovementMode.Scriptable then + return nil, true + else + touchModule = movementEnumToModuleMap[DevMovementMode] + end + return touchModule, true +end + +local function OnRenderStepped() + if activeController and activeController.enabled and humanoid then + local moveVector = activeController:GetMoveVector() + + local vehicleConsumedInput = false + if vehicleController then + moveVector, vehicleConsumedInput = vehicleController:Update(moveVector, activeControlModule==Gamepad) + end + + -- User of vehicleConsumedInput is commented out to preserve legacy behavior, in case some game relies on Humanoid.MoveDirection still being set while in a VehicleSeat + --if not vehicleConsumedInput then + moveFunction(Players.LocalPlayer, moveVector, cameraRelative) + --end + + humanoid.Jump = activeController:GetIsJumping() or (touchJumpController and touchJumpController:GetIsJumping()) + end +end + +local function OnHumanoidSeated(active, currentSeatPart) + if active then + if currentSeatPart and currentSeatPart:IsA("VehicleSeat") then + if not vehicleController then + vehicleController = VehicleController.new() + end + vehicleController:Enable(true, currentSeatPart) + end + else + if vehicleController then + vehicleController:Enable(false, currentSeatPart) + end + end +end + +local function OnCharacterAdded(char) + humanoid = char:FindFirstChildOfClass("Humanoid") + while not humanoid do + char.ChildAdded:wait() + humanoid = char:FindFirstChildOfClass("Humanoid") + end + + if humanoidSeatedConn then + humanoidSeatedConn:Disconnect() + humanoidSeatedConn = nil + end + humanoidSeatedConn = humanoid.Seated:connect(OnHumanoidSeated) +end + +local function OnCharacterRemoving(char) + humanoid = nil +end + +-- Helper function to lazily instantiate a controller if it does not yet exist, +-- disable the active controller if it is different from the on being switched to, +-- and then enable the requested controller. The argument to this function must be +-- a reference to one of the control modules, i.e. Keyboard, Gamepad, etc. +local function SwitchToController(controlModule) + if not controlModule then + if activeController then + activeController:Enable(false) + end + activeController = nil + activeControlModule = nil + else + if not controllers[controlModule] then + controllers[controlModule] = controlModule.new() + end + + if activeController ~= controllers[controlModule] then + if activeController then + activeController:Enable(false) + end + activeController = controllers[controlModule] + activeControlModule = controlModule -- Only used to check if controller switch is necessary + if touchControlFrame then + activeController:Enable(true, touchControlFrame) + else + activeController:Enable(true) + end + if touchControlFrame and (activeControlModule == TouchThumbpad + or activeControlModule == TouchThumbstick + or activeControlModule == ClickToMove + or activeControlModule == DynamicThumbstick) then + touchJumpController = controllers[TouchJump] + if not touchJumpController then + touchJumpController = TouchJump.new() + end + touchJumpController:Enable(true, touchControlFrame) + else + if touchJumpController then + touchJumpController:Enable(false) + end + end + end + end +end + +local function OnLastInputTypeChanged(newLastInputType) + if lastInputType == newLastInputType then + warn("LastInputType Change listener called with current type.") + end + lastInputType = newLastInputType + + if lastInputType == Enum.UserInputType.Touch then + -- TODO: Check if touch module already active + local touchModule, success = SelectTouchModule() + if success then + while not touchControlFrame do + wait() + end + SwitchToController(touchModule) + end + else + local computerModule = computerInputTypeToModuleMap[lastInputType] + if computerModule then + SwitchToController(computerModule) + end + end +end + +-- Called when any relevant values of GameSettings or LocalPlayer change, forcing re-evalulation of +-- current control scheme +local function OnComputerMovementModeChange() + local controlModule, success = SelectComputerMovementModule() + if success then + SwitchToController(controlModule) + end +end + +local function OnTouchMovementModeChange() + local touchModule, success = SelectTouchModule() + if success then + while not touchControlFrame do + wait() + end + SwitchToController(touchModule) + end +end + +Players.LocalPlayer.CharacterAdded:Connect(OnCharacterAdded) +Players.LocalPlayer.CharacterRemoving:Connect(OnCharacterRemoving) +if Players.LocalPlayer.Character then + OnCharacterAdded(Players.LocalPlayer.Character) +end + +RunService:BindToRenderStep("ControlScriptRenderstep", Enum.RenderPriority.Input.Value, OnRenderStepped) + +UserInputService.LastInputTypeChanged:Connect(OnLastInputTypeChanged) + +local propertyChangeListeners = { + GameSettings:GetPropertyChangedSignal("TouchMovementMode"):Connect(OnTouchMovementModeChange), + Players.LocalPlayer:GetPropertyChangedSignal("DevTouchMovementMode"):Connect(OnTouchMovementModeChange), + + GameSettings:GetPropertyChangedSignal("ComputerMovementMode"):Connect(OnComputerMovementModeChange), + Players.LocalPlayer:GetPropertyChangedSignal("DevComputerMovementMode"):Connect(OnComputerMovementModeChange), +} + +--[[ Touch Device UI ]]-- +local PlayerGui = nil +local touchGui = nil +local playerGuiAddedConn = nil + +local function createTouchGuiContainer() + if touchGui then touchGui:Destroy() end + + -- Container for all touch device guis + touchGui = Instance.new('ScreenGui') + touchGui.Name = "TouchGui" + touchGui.ResetOnSpawn = false + + touchControlFrame = Instance.new("Frame") + touchControlFrame.Name = "TouchControlFrame" + touchControlFrame.Size = UDim2.new(1, 0, 1, 0) + touchControlFrame.BackgroundTransparency = 1 + touchControlFrame.Parent = touchGui + + touchGui.Parent = PlayerGui +end + +if UserInputService.TouchEnabled then + PlayerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui") + if PlayerGui then + createTouchGuiContainer() + OnLastInputTypeChanged(UserInputService:GetLastInputType()) + else + playerGuiAddedConn = Players.LocalPlayer.ChildAdded:Connect(function(child) + if child:IsA("PlayerGui") then + PlayerGui = child + createTouchGuiContainer() + playerGuiAddedConn:Disconnect() + playerGuiAddedConn = nil + OnLastInputTypeChanged(UserInputService:GetLastInputType()) + end + end) + end +end + +return ControlScript diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/BaseCharacterController.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/BaseCharacterController.lua new file mode 100644 index 0000000..0731d49 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/BaseCharacterController.lua @@ -0,0 +1,40 @@ +--[[ + BaseCharacterController - Abstract base class for character controllers, not intended to be + directly instantiated. + + 2018 PlayerScripts Update - AllYourBlox +--]] + +local ZERO_VECTOR3 = Vector3.new(0,0,0) + +--[[ Roblox Services ]]-- +local Players = game:GetService("Players") + +--[[ The Module ]]-- +local BaseCharacterController = {} +BaseCharacterController.__index = BaseCharacterController + +function BaseCharacterController.new() + local self = setmetatable({}, BaseCharacterController) + self.enabled = false + self.moveVector = ZERO_VECTOR3 + self.isJumping = false + return self +end + +function BaseCharacterController:GetMoveVector() + return self.moveVector +end + +function BaseCharacterController:GetIsJumping() + return self.isJumping +end + +-- Override in derived classes to set self.enabled and return boolean indicating +-- whether Enable/Disable was successful. Return true if controller is already in the requested state. +function BaseCharacterController:Enable(enable) + error("BaseCharacterController:Enable must be overriden in derived classes and should not be called.") + return false +end + +return BaseCharacterController \ No newline at end of file diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/ClickToMoveController.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/ClickToMoveController.lua new file mode 100644 index 0000000..5ede572 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/ClickToMoveController.lua @@ -0,0 +1,1074 @@ +--[[ + -- Original By Kip Turner, Copyright Roblox 2014 + -- Updated by Garnold to utilize the new PathfindingService API, 2017 + -- 2018 PlayerScripts Update - AllYourBlox +--]] + +--[[ Roblox Services ]]-- +local UserInputService = game:GetService("UserInputService") +local ContextActionService = game:GetService("ContextActionService") +local PathfindingService = game:GetService("PathfindingService") +local Players = game:GetService("Players") +local RunService = game:GetService("RunService") +local DebrisService = game:GetService('Debris') +local ReplicatedStorage = game:GetService('ReplicatedStorage') +local TweenService = game:GetService("TweenService") + +--[[ Constants ]]-- +local ZERO_VECTOR3 = Vector3.new(0,0,0) + local movementKeys = { + [Enum.KeyCode.W] = true; + [Enum.KeyCode.A] = true; + [Enum.KeyCode.S] = true; + [Enum.KeyCode.D] = true; + [Enum.KeyCode.Up] = true; + [Enum.KeyCode.Down] = true; + } +local FFlagUserNavigationFixClickToMoveInterruptionSuccess, FFlagUserNavigationFixClickToMoveInterruptionResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNavigationFixClickToMoveInterruption") end) +local FFlagUserNavigationFixClickToMoveInterruption = FFlagUserNavigationFixClickToMoveInterruptionSuccess and FFlagUserNavigationFixClickToMoveInterruptionResult + +local Player = Players.LocalPlayer +local PlayerScripts = Player.PlayerScripts + +local TouchJump = nil + +local SHOW_PATH = true + +local RayCastIgnoreList = workspace.FindPartOnRayWithIgnoreList + +local CurrentSeatPart = nil +local DrivingTo = nil + +local XZ_VECTOR3 = Vector3.new(1,0,1) +local ZERO_VECTOR3 = Vector3.new(0,0,0) +local ZERO_VECTOR2 = Vector2.new(0,0) + +local BindableEvent_OnFailStateChanged = nil +if UserInputService.TouchEnabled then +-- BindableEvent_OnFailStateChanged = MasterControl:GetClickToMoveFailStateChanged() +end + +--------------------------UTIL LIBRARY------------------------------- +local Utility = {} +do + local function ViewSizeX() + local camera = workspace.CurrentCamera + local x = camera and camera.ViewportSize.X or 0 + local y = camera and camera.ViewportSize.Y or 0 + if x == 0 then + return 1024 + else + if x > y then + return x + else + return y + end + end + end + Utility.ViewSizeX = ViewSizeX + + local function ViewSizeY() + local camera = workspace.CurrentCamera + local x = camera and camera.ViewportSize.X or 0 + local y = camera and camera.ViewportSize.Y or 0 + if y == 0 then + return 768 + else + if x > y then + return y + else + return x + end + end + end + Utility.ViewSizeY = ViewSizeY + + local function FindCharacterAncestor(part) + if part then + local humanoid = part:FindFirstChild("Humanoid") + if humanoid then + return part, humanoid + else + return FindCharacterAncestor(part.Parent) + end + end + end + Utility.FindCharacterAncestor = FindCharacterAncestor + + local function Raycast(ray, ignoreNonCollidable, ignoreList) + local ignoreList = ignoreList or {} + local hitPart, hitPos, hitNorm, hitMat = RayCastIgnoreList(workspace, ray, ignoreList) + if hitPart then + if ignoreNonCollidable and hitPart.CanCollide == false then + table.insert(ignoreList, hitPart) + return Raycast(ray, ignoreNonCollidable, ignoreList) + end + return hitPart, hitPos, hitNorm, hitMat + end + return nil, nil + end + Utility.Raycast = Raycast + + local function AveragePoints(positions) + local avgPos = ZERO_VECTOR2 + if #positions > 0 then + for i = 1, #positions do + avgPos = avgPos + positions[i] + end + avgPos = avgPos / #positions + end + return avgPos + end + Utility.AveragePoints = AveragePoints +end + +local humanoidCache = {} +local function findPlayerHumanoid(player) + local character = player and player.Character + if character then + local resultHumanoid = humanoidCache[player] + if resultHumanoid and resultHumanoid.Parent == character then + return resultHumanoid + else + humanoidCache[player] = nil -- Bust Old Cache + local humanoid = character:FindFirstChildOfClass("Humanoid") + if humanoid then + humanoidCache[player] = humanoid + end + return humanoid + end + end +end + +--------------------------CHARACTER CONTROL------------------------------- +local CurrentIgnoreList + +local function GetCharacter() + return Player and Player.Character +end + +local function GetTorso() + local humanoid = findPlayerHumanoid(Player) + return humanoid and humanoid.RootPart +end + +local function getIgnoreList() + if CurrentIgnoreList then + return CurrentIgnoreList + end + CurrentIgnoreList = {} + table.insert(CurrentIgnoreList, GetCharacter()) + return CurrentIgnoreList +end + +-----------------------------------PATHER-------------------------------------- + +local popupAdornee +local function getPopupAdorneePart() + --Handle the case of the adornee part getting deleted (camera changed, maybe) + if popupAdornee and not popupAdornee.Parent then + popupAdornee = nil + end + + --If the adornee doesn't exist yet, create it + if not popupAdornee then + popupAdornee = Instance.new("Part") + popupAdornee.Name = "ClickToMovePopupAdornee" + popupAdornee.Transparency = 1 + popupAdornee.CanCollide = false + popupAdornee.Anchored = true + popupAdornee.Size = Vector3.new(2, 2, 2) + popupAdornee.CFrame = CFrame.new() + + popupAdornee.Parent = workspace.CurrentCamera + end + + return popupAdornee +end + +local activePopups = {} +local function createNewPopup(popupType) + local newModel = Instance.new("ImageHandleAdornment") + + newModel.AlwaysOnTop = false + newModel.Transparency = 1 + newModel.Size = ZERO_VECTOR2 + newModel.SizeRelativeOffset = ZERO_VECTOR3 + newModel.Image = "rbxasset://textures/ui/move.png" + newModel.ZIndex = 20 + + local radius = 0 + if popupType == "DestinationPopup" then + newModel.Color3 = Color3.fromRGB(0, 175, 255) + radius = 1.25 + elseif popupType == "DirectWalkPopup" then + newModel.Color3 = Color3.fromRGB(0, 175, 255) + radius = 1.25 + elseif popupType == "FailurePopup" then + newModel.Color3 = Color3.fromRGB(255, 100, 100) + radius = 1.25 + elseif popupType == "PatherPopup" then + newModel.Color3 = Color3.fromRGB(255, 255, 255) + radius = 1 + newModel.ZIndex = 10 + end + newModel.Size = Vector2.new(5, 0.1) * radius + + local dataStructure = {} + dataStructure.Model = newModel + + activePopups[#activePopups + 1] = newModel + + function dataStructure:TweenIn() + local tweenInfo = TweenInfo.new(1.5, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out) + local tween1 = TweenService:Create(newModel, tweenInfo, { Size = Vector2.new(2,2) * radius }) + tween1:Play() + TweenService:Create(newModel, TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, 0, false, 0.1), { Transparency = 0, SizeRelativeOffset = Vector3.new(0, radius * 1.5, 0) }):Play() + return tween1 + end + + function dataStructure:TweenOut() + local tweenInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.In) + local tween1 = TweenService:Create(newModel, tweenInfo, { Size = ZERO_VECTOR2 }) + tween1:Play() + + coroutine.wrap(function() + tween1.Completed:Wait() + + for i = 1, #activePopups do + if activePopups[i] == newModel then + table.remove(activePopups, i) + break + end + end + end)() + return tween1 + end + + function dataStructure:Place(position, dest) + -- place the model at position + if not self.Model.Parent then + local popupAdorneePart = getPopupAdorneePart() + self.Model.Parent = popupAdorneePart + self.Model.Adornee = popupAdorneePart + + --Start the 10-stud long ray 2.5 studs above where the tap happened and point straight down to try to find + --the actual ground position. + local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0)) + local hitPart, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(ray, { workspace.CurrentCamera, Player.Character }) + + self.Model.CFrame = CFrame.new(hitPoint) + Vector3.new(0, -radius,0) + end + end + + return dataStructure +end + +local function createPopupPath(points, numCircles) + -- creates a path with the provided points, using the path and number of circles provided + local popups = {} + local stopTraversing = false + + local function killPopup(i) + -- kill all popups before and at i + for iter, v in pairs(popups) do + if iter <= i then + local tween = v:TweenOut() + spawn(function() + tween.Completed:Wait() + v.Model:Destroy() + end) + popups[iter] = nil + end + end + end + + local function stopFunction() + stopTraversing = true + killPopup(#points) + end + + spawn(function() + for i = 1, #points do + if stopTraversing then + break + end + + local includeWaypoint = i % numCircles == 0 + and i < #points + and (points[#points].Position - points[i].Position).magnitude > 4 + if includeWaypoint then + local popup = createNewPopup("PatherPopup") + popups[i] = popup + local nextPopup = points[i+1] + popup:Place(points[i].Position, nextPopup and nextPopup.Position or points[#points].Position) + local tween = popup:TweenIn() + wait(0.2) + end + end + end) + + return stopFunction, killPopup +end + +local function Pather(character, endPoint, surfaceNormal) + local this = {} + + this.Cancelled = false + this.Started = false + + this.Finished = Instance.new("BindableEvent") + this.PathFailed = Instance.new("BindableEvent") + + this.PathComputing = false + this.PathComputed = false + + this.TargetPoint = endPoint + this.TargetSurfaceNormal = surfaceNormal + + this.DiedConn = nil + this.SeatedConn = nil + this.MoveToConn = nil + this.CurrentPoint = 0 + + function this:Cleanup() + if this.stopTraverseFunc then + this.stopTraverseFunc() + end + + if this.MoveToConn then + this.MoveToConn:Disconnect() + this.MoveToConn = nil + end + + if this.DiedConn then + this.DiedConn:Disconnect() + this.DiedConn = nil + end + + if this.SeatedConn then + this.SeatedConn:Disconnect() + this.SeatedConn = nil + end + + this.humanoid = nil + end + + function this:Cancel() + this.Cancelled = true + this:Cleanup() + end + + function this:OnPathInterrupted() + -- Stop moving + this.Cancelled = true + this:OnPointReached(false) + end + + function this:ComputePath() + local humanoid = findPlayerHumanoid(Player) + local torso = humanoid and humanoid.Torso + local success = false + if torso then + if this.PathComputed or this.PathComputing then return end + this.PathComputing = true + success = pcall(function() + this.pathResult = PathfindingService:FindPathAsync(torso.CFrame.p, this.TargetPoint) + end) + this.pointList = this.pathResult and this.pathResult:GetWaypoints() + this.PathComputing = false + this.PathComputed = this.pathResult and this.pathResult.Status == Enum.PathStatus.Success or false + end + return true + end + + function this:IsValidPath() + if not this.pathResult then + this:ComputePath() + end + return this.pathResult.Status == Enum.PathStatus.Success + end + + function this:OnPointReached(reached) + + if reached and not this.Cancelled then + + this.CurrentPoint = this.CurrentPoint + 1 + + if this.CurrentPoint > #this.pointList then + -- End of path reached + if this.stopTraverseFunc then + this.stopTraverseFunc() + end + this.Finished:Fire() + this:Cleanup() + else + -- If next action == Jump, but the humanoid + -- is still jumping from a previous action + -- wait until it gets to the ground + if this.CurrentPoint + 1 <= #this.pointList then + local nextAction = this.pointList[this.CurrentPoint + 1].Action + if nextAction == Enum.PathWaypointAction.Jump then + local currentState = this.humanoid:GetState() + if currentState == Enum.HumanoidStateType.FallingDown or + currentState == Enum.HumanoidStateType.Freefall or + currentState == Enum.HumanoidStateType.Jumping then + + this.humanoid.FreeFalling:Wait() + + -- Give time to the humanoid's state to change + -- Otherwise, the jump flag in Humanoid + -- will be reset by the state change + wait(0.1) + end + end + end + + -- Move to the next point + if this.setPointFunc then + this.setPointFunc(this.CurrentPoint) + end + + local nextWaypoint = this.pointList[this.CurrentPoint] + + if nextWaypoint.Action == Enum.PathWaypointAction.Jump then + this.humanoid.Jump = true + end + this.humanoid:MoveTo(nextWaypoint.Position) + end + else + this.PathFailed:Fire() + this:Cleanup() + end + end + + function this:Start() + if CurrentSeatPart then + return + end + + this.humanoid = findPlayerHumanoid(Player) + if FFlagUserNavigationFixClickToMoveInterruption and not this.humanoid then + this.PathFailed:Fire() + return + end + + if this.Started then return end + this.Started = true + + if SHOW_PATH then + -- choose whichever one Mike likes best + this.stopTraverseFunc, this.setPointFunc = createPopupPath(this.pointList, 4) + end + + if #this.pointList > 0 then + if FFlagUserNavigationFixClickToMoveInterruption then + this.SeatedConn = this.humanoid.Seated:Connect(function(reached) this:OnPathInterrupted() end) + this.DiedConn = this.humanoid.Died:Connect(function(reached) this:OnPathInterrupted() end) + end + this.MoveToConn = this.humanoid.MoveToFinished:Connect(function(reached) this:OnPointReached(reached) end) + this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it. + this:OnPointReached(true) -- Move to first point + else + this.PathFailed:Fire() + if this.stopTraverseFunc then + this.stopTraverseFunc() + end + end + end + + this:ComputePath() + if not this.PathComputed then + -- set the end point towards the camera and raycasted towards the ground in case we hit a wall + local offsetPoint = this.TargetPoint + this.TargetSurfaceNormal*1.5 + local ray = Ray.new(offsetPoint, Vector3.new(0,-1,0)*50) + local newHitPart, newHitPos = RayCastIgnoreList(workspace, ray, getIgnoreList()) + if newHitPart then + this.TargetPoint = newHitPos + end + -- try again + this:ComputePath() + end + + return this +end + +------------------------------------------------------------------------- + +local function IsInBottomLeft(pt) + local joystickHeight = math.min(Utility.ViewSizeY() * 0.33, 250) + local joystickWidth = joystickHeight + return pt.X <= joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight +end + +local function IsInBottomRight(pt) + local joystickHeight = math.min(Utility.ViewSizeY() * 0.33, 250) + local joystickWidth = joystickHeight + return pt.X >= Utility.ViewSizeX() - joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight +end + +local function CheckAlive(character) + local humanoid = findPlayerHumanoid(Player) + return humanoid ~= nil and humanoid.Health > 0 +end + +local function GetEquippedTool(character) + if character ~= nil then + for _, child in pairs(character:GetChildren()) do + if child:IsA('Tool') then + return child + end + end + end +end + +local ExistingPather = nil +local ExistingIndicator = nil +local PathCompleteListener = nil +local PathFailedListener = nil + +local function CleanupPath() + DrivingTo = nil + if ExistingPather then + ExistingPather:Cancel() + end + if PathCompleteListener then + PathCompleteListener:Disconnect() + PathCompleteListener = nil + end + if PathFailedListener then + PathFailedListener:Disconnect() + PathFailedListener = nil + end + if ExistingIndicator then + local obj = ExistingIndicator + local tween = obj:TweenOut() + local tweenCompleteEvent = nil + tweenCompleteEvent = tween.Completed:connect(function() + tweenCompleteEvent:Disconnect() + obj.Model:Destroy() + end) + ExistingIndicator = nil + end +end + +local function getExtentsSize(Parts) + local maxX,maxY,maxZ = -math.huge,-math.huge,-math.huge + local minX,minY,minZ = math.huge,math.huge,math.huge + for i = 1, #Parts do + maxX,maxY,maxZ = math.max(maxX, Parts[i].Position.X), math.max(maxY, Parts[i].Position.Y), math.max(maxZ, Parts[i].Position.Z) + minX,minY,minZ = math.min(minX, Parts[i].Position.X), math.min(minY, Parts[i].Position.Y), math.min(minZ, Parts[i].Position.Z) + end + return Region3.new(Vector3.new(minX, minY, minZ), Vector3.new(maxX, maxY, maxZ)) +end + +local function inExtents(Extents, Position) + if Position.X < (Extents.CFrame.p.X - Extents.Size.X/2) or Position.X > (Extents.CFrame.p.X + Extents.Size.X/2) then + return false + end + if Position.Z < (Extents.CFrame.p.Z - Extents.Size.Z/2) or Position.Z > (Extents.CFrame.p.Z + Extents.Size.Z/2) then + return false + end + --ignoring Y for now + return true +end + +local function showQuickPopupAsync(position, popupType) + local popup = createNewPopup(popupType) + popup:Place(position, Vector3.new(0,position.y,0)) + local tweenIn = popup:TweenIn() + tweenIn.Completed:Wait() + local tweenOut = popup:TweenOut() + tweenOut.Completed:Wait() + popup.Model:Destroy() + popup = nil +end + +local FailCount = 0 +local function OnTap(tapPositions, goToPoint) + -- Good to remember if this is the latest tap event + local camera = workspace.CurrentCamera + local character = Player.Character + + if not CheckAlive(character) then return end + + -- This is a path tap position + if #tapPositions == 1 or goToPoint then + if camera then + local unitRay = camera:ScreenPointToRay(tapPositions[1].x, tapPositions[1].y) + local ray = Ray.new(unitRay.Origin, unitRay.Direction*1000) + + -- inivisicam stuff + local initIgnore = getIgnoreList() + local invisicamParts = {} --InvisicamModule and InvisicamModule:GetObscuredParts() or {} + local ignoreTab = {} + + -- add to the ignore list + for i, v in pairs(invisicamParts) do + ignoreTab[#ignoreTab+1] = i + end + for i = 1, #initIgnore do + ignoreTab[#ignoreTab+1] = initIgnore[i] + end + -- + local myHumanoid = findPlayerHumanoid(Player) + local hitPart, hitPt, hitNormal, hitMat = Utility.Raycast(ray, true, ignoreTab) + + local hitChar, hitHumanoid = Utility.FindCharacterAncestor(hitPart) + local torso = GetTorso() + local startPos = torso.CFrame.p + if goToPoint then + hitPt = goToPoint + hitChar = nil + end + if hitChar and hitHumanoid and hitHumanoid.RootPart and (hitHumanoid.Torso.CFrame.p - torso.CFrame.p).magnitude < 7 then + CleanupPath() + + if myHumanoid then + myHumanoid:MoveTo(hitPt) + end + -- Do shoot + local currentWeapon = GetEquippedTool(character) + if currentWeapon then + currentWeapon:Activate() + LastFired = tick() + end + elseif hitPt and character and not CurrentSeatPart then + local thisPather = Pather(character, hitPt, hitNormal) + if thisPather:IsValidPath() then + FailCount = 0 + + thisPather:Start() + if BindableEvent_OnFailStateChanged then + BindableEvent_OnFailStateChanged:Fire(false) + end + CleanupPath() + + local destinationPopup = createNewPopup("DestinationPopup") + destinationPopup:Place(hitPt, Vector3.new(0,hitPt.y,0)) + local failurePopup = createNewPopup("FailurePopup") + local currentTween = destinationPopup:TweenIn() + + + ExistingPather = thisPather + ExistingIndicator = destinationPopup + + PathCompleteListener = thisPather.Finished.Event:Connect(function() + if destinationPopup then + if ExistingIndicator == destinationPopup then + ExistingIndicator = nil + end + local tween = destinationPopup:TweenOut() + local tweenCompleteEvent = nil + tweenCompleteEvent = tween.Completed:Connect(function() + tweenCompleteEvent:Disconnect() + destinationPopup.Model:Destroy() + destinationPopup = nil + end) + end + if hitChar then + local humanoid = findPlayerHumanoid(Player) + local currentWeapon = GetEquippedTool(character) + if currentWeapon then + currentWeapon:Activate() + LastFired = tick() + end + if humanoid then + humanoid:MoveTo(hitPt) + end + end + end) + PathFailedListener = thisPather.PathFailed.Event:Connect(function() + if FFlagUserNavigationFixClickToMoveInterruption then + CleanupPath() + end + if failurePopup then + failurePopup:Place(hitPt, Vector3.new(0,hitPt.y,0)) + local failTweenIn = failurePopup:TweenIn() + failTweenIn.Completed:Wait() + local failTweenOut = failurePopup:TweenOut() + failTweenOut.Completed:Wait() + failurePopup.Model:Destroy() + failurePopup = nil + end + end) + else + if hitPt then + -- Feedback here for when we don't have a good path + local foundDirectPath = false + if (hitPt-startPos).Magnitude < 25 and (startPos.y-hitPt.y > -3) then + -- move directly here + if myHumanoid then + if myHumanoid.Sit then + myHumanoid.Jump = true + end + myHumanoid:MoveTo(hitPt) + foundDirectPath = true + end + end + + coroutine.wrap(showQuickPopupAsync)(hitPt, foundDirectPath and "DirectWalkPopup" or "FailurePopup") + end + end + elseif hitPt and character and CurrentSeatPart then + local destinationPopup = createNewPopup("DestinationPopup") + ExistingIndicator = destinationPopup + destinationPopup:Place(hitPt, Vector3.new(0,hitPt.y,0)) + destinationPopup:TweenIn() + + DrivingTo = hitPt + local ConnectedParts = CurrentSeatPart:GetConnectedParts(true) + + while wait() do + if CurrentSeatPart and ExistingIndicator == destinationPopup then + local ExtentsSize = getExtentsSize(ConnectedParts) + if inExtents(ExtentsSize, hitPt) then + local popup = destinationPopup + spawn(function() + local tweenOut = popup:TweenOut() + tweenOut.Completed:Wait() + popup.Model:Destroy() + end) + destinationPopup = nil + DrivingTo = nil + break + end + else + if CurrentSeatPart == nil and destinationPopup == ExistingIndicator then + DrivingTo = nil + OnTap(tapPositions, hitPt) + end + local popup = destinationPopup + spawn(function() + local tweenOut = popup:TweenOut() + tweenOut.Completed:Wait() + popup.Model:Destroy() + end) + destinationPopup = nil + break + end + end + end + end + elseif #tapPositions >= 2 then + if camera then + -- Do shoot + local avgPoint = Utility.AveragePoints(tapPositions) + local unitRay = camera:ScreenPointToRay(avgPoint.x, avgPoint.y) + local currentWeapon = GetEquippedTool(character) + if currentWeapon then + currentWeapon:Activate() + LastFired = tick() + end + end + end +end + +local function IsFinite(num) + return num == num and num ~= 1/0 and num ~= -1/0 +end + +local function findAngleBetweenXZVectors(vec2, vec1) + return math.atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z) +end + +local function DisconnectEvent(event) + if event then + event:Disconnect() + end +end + +--[[ The ClickToMove Controller Class ]]-- +local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController")) +local ClickToMove = setmetatable({}, BaseCharacterController) +ClickToMove.__index = ClickToMove + +function ClickToMove.new() + print("Instantiating Keyboard Controller") + local self = setmetatable(BaseCharacterController.new(), ClickToMove) + + self.fingerTouches = {} + self.numUnsunkTouches = 0 + -- PC simulation + self.mouse1Down = tick() + self.mouse1DownPos = Vector2.new() + self.mouse2DownTime = tick() + self.mouse2DownPos = Vector2.new() + self.mouse2UpTime = tick() + + + + self.tapConn = nil + self.inputBeganConn = nil + self.inputChangedConn = nil + self.inputEndedConn = nil + self.humanoidDiedConn = nil + self.characterChildAddedConn = nil + self.onCharacterAddedConn = nil + self.characterChildRemovedConn = nil + self.renderSteppedConn = nil + self.humanoidSeatedConn = nil + + self.running = false + + return self +end + +function ClickToMove:DisconnectEvents() + DisconnectEvent(self.tapConn) + DisconnectEvent(self.inputBeganConn) + DisconnectEvent(self.inputChangedConn) + DisconnectEvent(self.inputEndedConn) + DisconnectEvent(self.humanoidDiedConn) + DisconnectEvent(self.characterChildAddedConn) + DisconnectEvent(self.onCharacterAddedConn) + DisconnectEvent(self.renderSteppedConn) + DisconnectEvent(self.characterChildRemovedConn) + + -- TODO: Resolve with ControlScript handling of seating for vehicles + DisconnectEvent(self.humanoidSeatedConn) + + pcall(function() RunService:UnbindFromRenderStep("ClickToMoveRenderUpdate") end) +end + +function ClickToMove:OnTouchBegan(input, processed) + if self.fingerTouches[input] == nil and not processed then + self.numUnsunkTouches = self.numUnsunkTouches + 1 + end + self.fingerTouches[input] = processed +end + +function ClickToMove:OnTouchChanged(input, processed) + if self.fingerTouches[input] == nil then + self.fingerTouches[input] = processed + if not processed then + self.numUnsunkTouches = self.numUnsunkTouches + 1 + end + end +end + +function ClickToMove:OnTouchEnded(input, processed) + if self.fingerTouches[input] ~= nil and self.fingerTouches[input] == false then + self.numUnsunkTouches = self.numUnsunkTouches - 1 + end + self.fingerTouches[input] = nil +end + + +function ClickToMove:OnCharacterAdded(character) + self:DisconnectEvents() + + self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + self:OnTouchBegan(input, processed) + + -- Give back controls when they tap both sticks + local wasInBottomLeft = IsInBottomLeft(input.Position) + local wasInBottomRight = IsInBottomRight(input.Position) + if wasInBottomRight or wasInBottomLeft then + for otherInput, _ in pairs(self.fingerTouches) do + if otherInput ~= input then + local otherInputInLeft = IsInBottomLeft(otherInput.Position) + local otherInputInRight = IsInBottomRight(otherInput.Position) + if otherInput.UserInputState ~= Enum.UserInputState.End and ((wasInBottomLeft and otherInputInRight) or (wasInBottomRight and otherInputInLeft)) then + if BindableEvent_OnFailStateChanged then + BindableEvent_OnFailStateChanged:Fire(true) + end + return + end + end + end + end + end + + -- Cancel path when you use the keyboard controls. + if processed == false and input.UserInputType == Enum.UserInputType.Keyboard and movementKeys[input.KeyCode] then + CleanupPath() + end + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self.mouse1DownTime = tick() + self.mouse1DownPos = input.Position + end + if input.UserInputType == Enum.UserInputType.MouseButton2 then + self.mouse2DownTime = tick() + self.mouse2DownPos = input.Position + end + end) + + self.inputChangedConn = UserInputService.InputChanged:Connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + self:OnTouchChanged(input, processed) + end + end) + + self.inputEndedConn = UserInputService.InputEnded:Connect(function(input, processed) + if input.UserInputType == Enum.UserInputType.Touch then + self:OnTouchEnded(input, processed) + end + + if input.UserInputType == Enum.UserInputType.MouseButton2 then + self.mouse2UpTime = tick() + local currPos = input.Position + if self.mouse2UpTime - self.mouse2DownTime < 0.25 and (currPos - self.mouse2DownPos).magnitude < 5 then + local positions = {currPos} + OnTap(positions) + end + end + end) + + self.tapConn = UserInputService.TouchTap:Connect(function(touchPositions, processed) + if not processed then + OnTap(touchPositions) + end + end) + + local function computeThrottle(dist) + if dist > .2 then + return 0.5+(dist^2)/2 + else + return 0 + end + end + + local lastSteer = 0 + + --kP = how much the steering corrects for the current error in driving angle + --kD = how much the steering corrects for how quickly the error in driving angle is changing + local kP = 1 + local kD = 0.5 + local function getThrottleAndSteer(object, point) + local throttle, steer = 0, 0 + local oCF = object.CFrame + + local relativePosition = oCF:pointToObjectSpace(point) + local relativeZDirection = -relativePosition.z + local relativeDistance = relativePosition.magnitude + + -- throttle quadratically increases from 0-1 as distance from the selected point goes from 0-50, after 50, throttle is 1. + -- this allows shorter distance travel to have more fine-tuned control. + throttle = computeThrottle(math.min(1,relativeDistance/50))*math.sign(relativeZDirection) + + local steerAngle = -math.atan2(-relativePosition.x, -relativePosition.z) + steer = steerAngle/(math.pi/4) + + local steerDelta = steer - lastSteer + lastSteer = steer + local pdSteer = kP * steer + kD * steer + return throttle, pdSteer + end + + local function Update() + if CurrentSeatPart then + if DrivingTo then + local throttle, steer = getThrottleAndSteer(CurrentSeatPart, DrivingTo) + CurrentSeatPart.ThrottleFloat = throttle + CurrentSeatPart.SteerFloat = steer + else + CurrentSeatPart.ThrottleFloat = 0 + CurrentSeatPart.SteerFloat = 0 + end + end + + local cameraPos = workspace.CurrentCamera.CFrame.p + for i = 1, #activePopups do + local popup = activePopups[i] + popup.CFrame = CFrame.new(popup.CFrame.p, cameraPos) + end + end + + RunService:BindToRenderStep("ClickToMoveRenderUpdate",Enum.RenderPriority.Camera.Value - 1,Update) + + -- TODO: Resolve with control script seating functionality +-- local function onSeated(child, active, currentSeatPart) +-- if active then +-- if TouchJump and UserInputService.TouchEnabled then +-- TouchJump:Enable() +-- end +-- if currentSeatPart and currentSeatPart.ClassName == "VehicleSeat" then +-- CurrentSeatPart = currentSeatPart +-- end +-- else +-- CurrentSeatPart = nil +-- if TouchJump and UserInputService.TouchEnabled then +-- TouchJump:Disable() +-- end +-- end +-- end + + local function OnCharacterChildAdded(child) + if UserInputService.TouchEnabled then + if child:IsA('Tool') then + child.ManualActivationOnly = true + end + end + if child:IsA('Humanoid') then + DisconnectEvent(self.humanoidDiedConn) + self.humanoidDiedConn = child.Died:Connect(function() + if ExistingIndicator then + DebrisService:AddItem(ExistingIndicator.Model, 1) + end + end) +-- self.humanoidSeatedConn = child.Seated:Connect(function(active, seat) onSeated(child, active, seat) end) +-- if child.SeatPart then +-- onSeated(child, true, child.SeatPart) +-- end + end + end + + self.characterChildAddedConn = character.ChildAdded:Connect(function(child) + OnCharacterChildAdded(child) + end) + self.characterChildRemovedConn = character.ChildRemoved:Connect(function(child) + if UserInputService.TouchEnabled then + if child:IsA('Tool') then + child.ManualActivationOnly = false + end + end + end) + for _, child in pairs(character:GetChildren()) do + OnCharacterChildAdded(child) + end +end + +function ClickToMove:Start() + self:Enable(true) +end + +function ClickToMove:Stop() + self:Enable(false) +end + +function ClickToMove:Enable(enable) + if enable then + if not self.running then + if Player.Character then -- retro-listen + self:OnCharacterAdded(Player.Character) + end + self.onCharacterAddedConn = Player.CharacterAdded:Connect(function(char) + self:OnCharacterAdded(char) + end) + self.running = true + end + else + if self.running then + self:DisconnectEvents() + CleanupPath() + -- Restore tool activation on shutdown + if UserInputService.TouchEnabled then + local character = Player.Character + if character then + for _, child in pairs(character:GetChildren()) do + if child:IsA('Tool') then + child.ManualActivationOnly = false + end + end + end + end + DrivingTo = nil + self.running = false + end + end + self.enabled = enable +end + +return ClickToMove \ No newline at end of file diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/DynamicThumbstick.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/DynamicThumbstick.lua new file mode 100644 index 0000000..729e38e --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/DynamicThumbstick.lua @@ -0,0 +1,531 @@ +--[[ Constants ]]-- +local ZERO_VECTOR3 = Vector3.new(0,0,0) +local TOUCH_CONTROLS_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png" + +local MIDDLE_TRANSPARENCIES = { + 1 - 0.89, + 1 - 0.70, + 1 - 0.60, + 1 - 0.50, + 1 - 0.40, + 1 - 0.30, + 1 - 0.25 +} +local NUM_MIDDLE_IMAGES = #MIDDLE_TRANSPARENCIES + +local FADE_IN_OUT_BACKGROUND = true +local FADE_IN_OUT_MAX_ALPHA = 0.35 + +local FADE_IN_OUT_HALF_DURATION_DEFAULT = 0.3 +local FADE_IN_OUT_BALANCE_DEFAULT = 0.5 +local ThumbstickFadeTweenInfo = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) + +local Players = game:GetService("Players") +local GuiService = game:GetService("GuiService") +local UserInputService = game:GetService("UserInputService") +local RunService = game:GetService("RunService") +local TweenService = game:GetService("TweenService") + +--[[ The Module ]]-- +local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController")) +local DynamicThumbstick = setmetatable({}, BaseCharacterController) +DynamicThumbstick.__index = DynamicThumbstick + +function DynamicThumbstick.new() + local self = setmetatable(BaseCharacterController.new(), DynamicThumbstick) + + self.humanoid = nil + + self.tools = {} + self.toolEquipped = nil + + self.revertAutoJumpEnabledToFalse = false + + self.moveTouchObject = nil + self.moveTouchStartPosition = nil + + self.startImage = nil + self.endImage = nil + self.middleImages = {} + + self.startImageFadeTween = nil + self.endImageFadeTween = nil + self.middleImageFadeTweens = {} + + self.isFirstTouch = true + + self.isFollowStick = false + self.thumbstickFrame = nil + self.onTouchMovedConn = nil + self.onTouchEndedConn = nil + self.onTouchActivateConn = nil + self.onRenderSteppedConn = nil + + self.fadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT + self.fadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT + self.hasFadedBackgroundInPortrait = false + self.hasFadedBackgroundInLandscape = false + + self.tweenInAlphaStart = nil + self.tweenOutAlphaStart = nil + + -- If this module changes a player's humanoid's AutoJumpEnabled, it saves + -- the previous state in this variable to revert to + self.shouldRevertAutoJumpOnDisable = false + + return self +end + +-- Note: Overrides base class GetIsJumping with get-and-clear behavior to do a single jump +-- rather than sustained jumping. This is only to preserve the current behavior through the refactor. +function DynamicThumbstick:GetIsJumping() + local wasJumping = self.isJumping + self.isJumping = false + return wasJumping +end + +function DynamicThumbstick:EnableAutoJump(enable) + local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid") + if humanoid then + if enable then + self.shouldRevertAutoJumpOnDisable = (humanoid.AutoJumpEnabled == false) and (Players.LocalPlayer.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice) + humanoid.AutoJumpEnabled = true + elseif self.shouldRevertAutoJumpOnDisable then + humanoid.AutoJumpEnabled = false + end + end +end + +--[[ Public API ]]-- +function DynamicThumbstick:Enable(enable, uiParentFrame) + if enable == nil then return false end -- If nil, return false (invalid argument) + enable = enable and true or false -- Force anything non-nil to boolean before comparison + if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state + + if enable then + -- Enable + if not self.thumbstickFrame then + self:Create(uiParentFrame) + end + + if Players.LocalPlayer.Character then + self:OnCharacterAdded(Players.LocalPlayer.Character) + else + Players.LocalPlayer.CharacterAdded:Connect(function(char) + self:OnCharacterAdded(char) + end) + end + else + -- Disable + self:OnInputEnded() -- Cleanup + end + + self.enabled = enable + self.thumbstickFrame.Visible = enable +end + +function DynamicThumbstick:OnCharacterAdded(char) + + for _, child in ipairs(char:GetChildren()) do + if child:IsA("Tool") then + self.toolEquipped = child + end + end + + char.ChildAdded:Connect(function(child) + if child:IsA("Tool") then + self.toolEquipped = child + elseif child:IsA("Humanoid") then + self:EnableAutoJump(true) + end + + end) + char.ChildRemoved:Connect(function(child) + if child == self.toolEquipped then + self.toolEquipped = nil + end + end) + + self.humanoid = char:FindFirstChildOfClass("Humanoid") + if self.humanoid then + self:EnableAutoJump(true) + end +end + +-- Was called OnMoveTouchEnded in previous version +function DynamicThumbstick:OnInputEnded() + self.moveTouchObject = nil + self.moveVector = ZERO_VECTOR3 + self:FadeThumbstick(false) + self.thumbstickFrame.Active = true +end + +function DynamicThumbstick:FadeThumbstick(visible) + if not visible and self.moveTouchObject then + return + end + if self.isFirstTouch then return end + + if self.startImageFadeTween then + self.startImageFadeTween:Cancel() + end + if self.endImageFadeTween then + self.endImageFadeTween:Cancel() + end + for i = 1, #self.middleImages do + if self. middleImageFadeTweens[i] then + self.middleImageFadeTweens[i]:Cancel() + end + end + + if visible then + self.startImageFadeTween = TweenService:Create(self.startImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0 }) + self.startImageFadeTween:Play() + + self.endImageFadeTween = TweenService:Create(self.endImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0.2 }) + self.endImageFadeTween:Play() + + for i = 1, #self.middleImages do + self.middleImageFadeTweens[i] = TweenService:Create(self.middleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = MIDDLE_TRANSPARENCIES[i] }) + self.middleImageFadeTweens[i]:Play() + end + else + self.startImageFadeTween = TweenService:Create(self.startImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 }) + self.startImageFadeTween:Play() + + self.endImageFadeTween = TweenService:Create(self.endImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 }) + self.endImageFadeTween:Play() + + for i = 1, #self.middleImages do + self.middleImageFadeTweens[i] = TweenService:Create(self.middleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = 1 }) + self.middleImageFadeTweens[i]:Play() + end + end +end + +function DynamicThumbstick:FadeThumbstickFrame(fadeDuration, fadeRatio) + self.fadeInAndOutHalfDuration = fadeDuration * 0.5 + self.fadeInAndOutBalance = fadeRatio + self.tweenInAlphaStart = tick() +end + +function DynamicThumbstick:Create(parentFrame) + if self.thumbstickFrame then + self.thumbstickFrame:Destroy() + self.thumbstickFrame = nil + if self.onTouchMovedConn then + self.onTouchMovedConn:Disconnect() + self.onTouchMovedConn = nil + end + if self.onTouchEndedConn then + self.onTouchEndedCon:Disconnect() + self.onTouchEndedCon = nil + end + if self.onRenderSteppedConn then + self.onRenderSteppedConn:Disconnect() + self.onRenderSteppedConn = nil + end + if self.onTouchActivateConn then + self.onTouchActivateConn:Disconnect() + self.onTouchActivateConn = nil + end + end + + local ThumbstickSize = 45 + local ThumbstickRingSize = 20 + local MiddleSize = 10 + local MiddleSpacing = MiddleSize + 4 + local RadiusOfDeadZone = 2 + local RadiusOfMaxSpeed = 20 + + local screenSize = parentFrame.AbsoluteSize + local isBigScreen = math.min(screenSize.x, screenSize.y) > 500 + if isBigScreen then + ThumbstickSize = ThumbstickSize * 2 + ThumbstickRingSize = ThumbstickRingSize * 2 + MiddleSize = MiddleSize * 2 + MiddleSpacing = MiddleSpacing * 2 + RadiusOfDeadZone = RadiusOfDeadZone * 2 + RadiusOfMaxSpeed = RadiusOfMaxSpeed * 2 + end + + local function layoutThumbstickFrame(portraitMode) + if portraitMode then + self.thumbstickFrame.Size = UDim2.new(1, 0, 0.4, 0) + self.thumbstickFrame.Position = UDim2.new(0, 0, 0.6, 0) + else + self.thumbstickFrame.Size = UDim2.new(0.4, 0, 2/3, 0) + self.thumbstickFrame.Position = UDim2.new(0, 0, 1/3, 0) + end + end + + self.thumbstickFrame = Instance.new("TextButton") + self.thumbstickFrame.Text = "" + self.thumbstickFrame.Name = "Dynamicself.thumbstickFrame" + self.thumbstickFrame.Visible = false + self.thumbstickFrame.BackgroundTransparency = 1.0 + self.thumbstickFrame.BackgroundColor3 = Color3.fromRGB(0, 0, 0) + layoutThumbstickFrame(false) + + self.startImage = Instance.new("ImageLabel") + self.startImage.Name = "ThumbstickStart" + self.startImage.Visible = true + self.startImage.BackgroundTransparency = 1 + self.startImage.Image = TOUCH_CONTROLS_SHEET + self.startImage.ImageRectOffset = Vector2.new(1,1) + self.startImage.ImageRectSize = Vector2.new(144, 144) + self.startImage.ImageColor3 = Color3.new(0, 0, 0) + self.startImage.AnchorPoint = Vector2.new(0.5, 0.5) + self.startImage.Position = UDim2.new(0, ThumbstickRingSize * 3.3, 1, -ThumbstickRingSize * 2.8) + self.startImage.Size = UDim2.new(0, ThumbstickRingSize * 3.7, 0, ThumbstickRingSize * 3.7) + self.startImage.ZIndex = 10 + self.startImage.Parent = self.thumbstickFrame + + self.endImage = Instance.new("ImageLabel") + self.endImage.Name = "ThumbstickEnd" + self.endImage.Visible = true + self.endImage.BackgroundTransparency = 1 + self.endImage.Image = TOUCH_CONTROLS_SHEET + self.endImage.ImageRectOffset = Vector2.new(1,1) + self.endImage.ImageRectSize = Vector2.new(144, 144) + self.endImage.AnchorPoint = Vector2.new(0.5, 0.5) + self.endImage.Position = self.startImage.Position + self.endImage.Size = UDim2.new(0, ThumbstickSize * 0.8, 0, ThumbstickSize * 0.8) + self.endImage.ZIndex = 10 + self.endImage.Parent = self.thumbstickFrame + + for i = 1, NUM_MIDDLE_IMAGES do + self.middleImages[i] = Instance.new("ImageLabel") + self.middleImages[i].Name = "ThumbstickMiddle" + self.middleImages[i].Visible = false + self.middleImages[i].BackgroundTransparency = 1 + self.middleImages[i].Image = TOUCH_CONTROLS_SHEET + self.middleImages[i].ImageRectOffset = Vector2.new(1,1) + self.middleImages[i].ImageRectSize = Vector2.new(144, 144) + self.middleImages[i].ImageTransparency = MIDDLE_TRANSPARENCIES[i] + self.middleImages[i].AnchorPoint = Vector2.new(0.5, 0.5) + self.middleImages[i].ZIndex = 9 + self.middleImages[i].Parent = self.thumbstickFrame + end + + local CameraChangedConn = nil + local function onCurrentCameraChanged() + if CameraChangedConn then + CameraChangedConn:Disconnect() + CameraChangedConn = nil + end + local newCamera = workspace.CurrentCamera + if newCamera then + local function onViewportSizeChanged() + local size = newCamera.ViewportSize + local portraitMode = size.X < size.Y + layoutThumbstickFrame(portraitMode) + end + CameraChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(onViewportSizeChanged) + onViewportSizeChanged() + end + end + workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(onCurrentCameraChanged) + if workspace.CurrentCamera then + onCurrentCameraChanged() + end + + self.moveTouchStartPosition = nil + + self.startImageFadeTween = nil + self.endImageFadeTween = nil + self.middleImageFadeTweens = {} + + local function doMove(direction) + local currentMoveVector = direction + + -- Scaled Radial Dead Zone + local inputAxisMagnitude = currentMoveVector.magnitude + if inputAxisMagnitude < RadiusOfDeadZone then + currentMoveVector = Vector3.new() + else + currentMoveVector = currentMoveVector.unit*(1 - math.max(0, (RadiusOfMaxSpeed - currentMoveVector.magnitude)/RadiusOfMaxSpeed)) + currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y) + end + + self.moveVector = currentMoveVector + end + + local function layoutMiddleImages(startPos, endPos) + local startDist = (ThumbstickSize / 2) + MiddleSize + local vector = endPos - startPos + local distAvailable = vector.magnitude - (ThumbstickRingSize / 2) - MiddleSize + local direction = vector.unit + + local distNeeded = MiddleSpacing * NUM_MIDDLE_IMAGES + local spacing = MiddleSpacing + + if distNeeded < distAvailable then + spacing = distAvailable / NUM_MIDDLE_IMAGES + end + + for i = 1, NUM_MIDDLE_IMAGES do + local image = self.middleImages[i] + local distWithout = startDist + (spacing * (i - 2)) + local currentDist = startDist + (spacing * (i - 1)) + + if distWithout < distAvailable then + local pos = endPos - direction * currentDist + local exposedFraction = math.clamp(1 - ((currentDist - distAvailable) / spacing), 0, 1) + + image.Visible = true + image.Position = UDim2.new(0, pos.X, 0, pos.Y) + image.Size = UDim2.new(0, MiddleSize * exposedFraction, 0, MiddleSize * exposedFraction) + else + image.Visible = false + end + end + end + + local function moveStick(pos) + local startPos = Vector2.new(self.moveTouchStartPosition.X, self.moveTouchStartPosition.Y) - self.thumbstickFrame.AbsolutePosition + local endPos = Vector2.new(pos.X, pos.Y) - self.thumbstickFrame.AbsolutePosition + self.endImage.Position = UDim2.new(0, endPos.X, 0, endPos.Y) + layoutMiddleImages(startPos, endPos) + end + + -- input connections + self.thumbstickFrame.InputBegan:Connect(function(inputObject) + if inputObject.UserInputType ~= Enum.UserInputType.Touch or inputObject.UserInputState ~= Enum.UserInputState.Begin then + return + end + if self.moveTouchObject then + return + end + + if self.isFirstTouch then + self.isFirstTouch = false + local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out,0,false,0) + TweenService:Create(self.startImage, tweenInfo, {Size = UDim2.new(0, 0, 0, 0)}):Play() + TweenService:Create(self.endImage, tweenInfo, {Size = UDim2.new(0, ThumbstickSize, 0, ThumbstickSize), ImageColor3 = Color3.new(0,0,0)}):Play() + end + + self.moveTouchObject = inputObject + self.moveTouchStartPosition = inputObject.Position + local startPosVec2 = Vector2.new(inputObject.Position.X - self.thumbstickFrame.AbsolutePosition.X, inputObject.Position.Y - self.thumbstickFrame.AbsolutePosition.Y) + + self.startImage.Visible = true + self.startImage.Position = UDim2.new(0, startPosVec2.X, 0, startPosVec2.Y) + self.endImage.Visible = true + self.endImage.Position = self.startImage.Position + + self:FadeThumbstick(true) + moveStick(inputObject.Position) + + if FADE_IN_OUT_BACKGROUND then + local playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui") + local hasFadedBackgroundInOrientation = false + + -- only fade in/out the background once per orientation + if playerGui then + if playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or + playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight then + hasFadedBackgroundInOrientation = self.hasFadedBackgroundInLandscape + self.hasFadedBackgroundInLandscape = true + elseif playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait then + hasFadedBackgroundInOrientation = self.hasFadedBackgroundInPortrait + self.hasFadedBackgroundInPortrait = true + end + end + + if not hasFadedBackgroundInOrientation then + self.fadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT + self.fadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT + self.tweenInAlphaStart = tick() + end + end + end) + + self.onTouchMovedConn = UserInputService.TouchMoved:connect(function(inputObject) + if inputObject == self.moveTouchObject then + self.thumbstickFrame.Active = false + local direction = Vector2.new(inputObject.Position.x - self.moveTouchStartPosition.x, inputObject.Position.y - self.moveTouchStartPosition.y) + if math.abs(direction.x) > 0 or math.abs(direction.y) > 0 then + doMove(direction) + moveStick(inputObject.Position) + end + end + end) + + self.onRenderSteppedConn = RunService.RenderStepped:Connect(function() + if self.tweenInAlphaStart ~= nil then + local delta = tick() - self.tweenInAlphaStart + local fadeInTime = (self.fadeInAndOutHalfDuration * 2 * self.fadeInAndOutBalance) + self.thumbstickFrame.BackgroundTransparency = 1 - FADE_IN_OUT_MAX_ALPHA*math.min(delta/fadeInTime, 1) + if delta > fadeInTime then + self.tweenOutAlphaStart = tick() + self.tweenInAlphaStart = nil + end + elseif self.tweenOutAlphaStart ~= nil then + local delta = tick() - self.tweenOutAlphaStart + local fadeOutTime = (self.fadeInAndOutHalfDuration * 2) - (self.fadeInAndOutHalfDuration * 2 * self.fadeInAndOutBalance) + self.thumbstickFrame.BackgroundTransparency = 1 - FADE_IN_OUT_MAX_ALPHA + FADE_IN_OUT_MAX_ALPHA*math.min(delta/fadeOutTime, 1) + if delta > fadeOutTime then + self.tweenOutAlphaStart = nil + end + end + end) + + self.onTouchEndedConn = UserInputService.TouchEnded:connect(function(inputObject) + if inputObject == self.moveTouchObject then + self:OnInputEnded() + end + end) + + GuiService.MenuOpened:connect(function() + if self.moveTouchObject then + self:OnInputEnded() + end + end) + + local playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui") + while not playerGui do + Players.LocalPlayer.ChildAdded:wait() + playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui") + end + + local playerGuiChangedConn = nil + local originalScreenOrientationWasLandscape = playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or + playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight + + local function longShowBackground() + self.fadeInAndOutHalfDuration = 2.5 + self.fadeInAndOutBalance = 0.05 + self.tweenInAlphaStart = tick() + end + + playerGuiChangedConn = playerGui.Changed:connect(function(prop) + if prop == "CurrentScreenOrientation" then + if (originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait) or + (not originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation ~= Enum.ScreenOrientation.Portrait) then + + playerGuiChangedConn:disconnect() + longShowBackground() + + if originalScreenOrientationWasLandscape then + self.hasFadedBackgroundInPortrait = true + else + self.hasFadedBackgroundInLandscape = true + end + end + end + end) + + self.thumbstickFrame.Parent = parentFrame + + spawn(function() + if game:IsLoaded() then + longShowBackground() + else + game.Loaded:wait() + longShowBackground() + end + end) +end + +return DynamicThumbstick diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/Gamepad.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/Gamepad.lua new file mode 100644 index 0000000..737d9d2 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/Gamepad.lua @@ -0,0 +1,235 @@ +--[[ + Gamepad Character Control - This module handles controlling your avatar using a game console-style controller + + 2018 PlayerScripts Update - AllYourBlox +--]] + +local UserInputService = game:GetService("UserInputService") +local ContextActionService = game:GetService("ContextActionService") + +--[[ Constants ]]-- +local ZERO_VECTOR3 = Vector3.new(0,0,0) +local NONE = Enum.UserInputType.None +local thumbstickDeadzone = 0.2 + +--[[ The Module ]]-- +local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController")) +local Gamepad = setmetatable({}, BaseCharacterController) +Gamepad.__index = Gamepad + +function Gamepad.new() + print("Instantiating Gamepad Controller") + local self = setmetatable(BaseCharacterController.new(), Gamepad) + + self.forwardValue = 0 + self.backwardValue = 0 + self.leftValue = 0 + self.rightValue = 0 + + self.activeGamepad = NONE -- Enum.UserInputType.Gamepad1, 2, 3... + self.gamepadConnectedConn = nil + self.gamepadDisconnectedConn = nil + return self +end + +function Gamepad:Enable(enable) + if not UserInputService.GamepadEnabled then + return false + end + + if enable == self.enabled then + -- Module is already in the state being requested. True is returned here since the module will be in the state + -- expected by the code that follows the Enable() call. This makes more sense than returning false to indicate + -- no action was necessary. False indicates failure to be in requested/expected state. + return true + end + + self.forwardValue = 0 + self.backwardValue = 0 + self.leftValue = 0 + self.rightValue = 0 + self.moveVector = ZERO_VECTOR3 + + if enable then + self.activeGamepad = self:GetHighestPriorityGamepad() + if self.activeGamepad ~= NONE then + self:BindContextActions() + self:ConnectGamepadConnectionListeners() + else + -- No connected gamepads, failure to enable + return false + end + else + self:UnbindContextActions() + self:DisconnectGamepadConnectionListeners() + self.activeGamepad = NONE + end + + self.enabled = enable + return true +end + +-- This function selects the lowest number gamepad from the currently-connected gamepad +-- and sets it as the active gamepad +function Gamepad:GetHighestPriorityGamepad() + local connectedGamepads = UserInputService:GetConnectedGamepads() + local bestGamepad = NONE -- Note that this value is higher than all valid gamepad values + for _, gamepad in pairs(connectedGamepads) do + if gamepad.Value < bestGamepad.Value then + bestGamepad = gamepad + end + end + return bestGamepad +end + +function Gamepad:BindContextActions() + + if self.activeGamepad == NONE then + -- There must be an active gamepad to set up bindings + return false + end + + + local updateMovement = function(inputState) + if inputState == Enum.UserInputState.Cancel then + self.moveVector = ZERO_VECTOR3 + else + self.moveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue) + end + end + + -- Note: In the previous version of this code, the movement values were not zeroed-out on UserInputState. Cancel, now they are, + -- which fixes them from getting stuck on. + local handleMoveForward = function(actionName, inputState, inputObject) + self.forwardValue = (inputState == Enum.UserInputState.Begin) and -1 or 0 + updateMovement(inputState) + end + + local handleMoveBackward = function(actionName, inputState, inputObject) + self.backwardValue = (inputState == Enum.UserInputState.Begin) and 1 or 0 + updateMovement(inputState) + end + + local handleMoveLeft = function(actionName, inputState, inputObject) + self.leftValue = (inputState == Enum.UserInputState.Begin) and -1 or 0 + updateMovement(inputState) + end + + local handleMoveRight = function(actionName, inputState, inputObject) + self.rightValue = (inputState == Enum.UserInputState.Begin) and 1 or 0 + updateMovement(inputState) + end + + local handleJumpAction = function(actionName, inputState, inputObject) + self.isJumping = (inputState == Enum.UserInputState.Begin) + end + + local handleThumbstickInput = function(actionName, inputState, inputObject) + if self.activeGamepad ~= inputObject.UserInputType then return end + if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end + + if inputState == Enum.UserInputState.Cancel then + self.moveVector = ZERO_VECTOR3 + return + end + + if inputObject.Position.magnitude > thumbstickDeadzone then + self.moveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y) + else + self.moveVector = ZERO_VECTOR3 + end + end + + ContextActionService:BindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2) + ContextActionService:BindAction("jumpAction",handleJumpAction, false, Enum.KeyCode.ButtonA) + ContextActionService:BindAction("moveThumbstick",handleThumbstickInput, false, Enum.KeyCode.Thumbstick1) + + return true +end + +function Gamepad:UnbindContextActions() + if self.activeGamepad ~= NONE then + ContextActionService:UnbindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2) + end + ContextActionService:UnbindAction("moveThumbstick") + ContextActionService:UnbindAction("jumpAction") +end + +function Gamepad:OnNewGamepadConnected() + -- A new gamepad has been connected. + local bestGamepad = self:GetHighestPriorityGamepad() + + if bestGamepad == self.activeGamepad then + -- A new gamepad was connected, but our active gamepad is not changing + return + end + + if bestGamepad == NONE then + -- There should be an active gamepad when GamepadConnected fires, so this should not + -- normally be hit. If there is no active gamepad, unbind actions but leave + -- the module enabled and continue to listen for a new gamepad connection. + warn("Gamepad:OnNewGamepadConnected found no connected gamepads") + self:UnbindContextActions() + return + end + + if self.activeGamepad ~= NONE then + -- Switching from one active gamepad to another + ContextActionService:UnbindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2) + end + + self.activeGamepad = bestGamepad + ContextActionService:BindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2) +end + +function Gamepad:OnCurrentGamepadDisconnected() + if self.activeGamepad ~= NONE then + ContextActionService:UnbindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2) + end + + local bestGamepad = self:GetHighestPriorityGamepad() + + if self.activeGamepad ~= NONE and bestGamepad == self.activeGamepad then + warn("Gamepad:OnCurrentGamepadDisconnected found the supposedly disconnected gamepad in connectedGamepads.") + self:UnbindContextActions() + self.activeGamepad = NONE + return + end + + if bestGamepad == NONE then + -- No active gamepad, unbinding actions but leaving gamepad connection listener active + self:UnbindContextActions() + self.activeGamepad = NONE + else + -- Set new gamepad as active and bind to tool activation + self.activeGamepad = bestGamepad + ContextActionService:BindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2) + end +end + +function Gamepad:ConnectGamepadConnectionListeners() + self.gamepadConnectedConn = UserInputService.GamepadConnected:Connect(function(gamepadEnum) + self:OnNewGamepadConnected() + end) + + self.gamepadDisconnectedConn = UserInputService.GamepadDisconnected:Connect(function(gamepadEnum) + if self.activeGamepad == gamepadEnum then + self:OnCurrentGamepadDisconnected() + end + end) + +end + +function Gamepad:DisconnectGamepadConnectionListeners() + if self.gamepadConnectedConn then + self.gamepadConnectedConn:Disconnect() + self.gamepadConnectedConn = nil + end + + if self.gamepadDisconnectedConn then + self.gamepadDisconnectedConn:Disconnect() + self.gamepadDisconnectedConn = nil + end +end + +return Gamepad diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/Keyboard.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/Keyboard.lua new file mode 100644 index 0000000..b2e38f4 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/Keyboard.lua @@ -0,0 +1,150 @@ +--[[ + Keyboard Character Control - This module handles controlling your avatar from a keyboard + + 2018 PlayerScripts Update - AllYourBlox +--]] + +--[[ Roblox Services ]]-- +local UserInputService = game:GetService("UserInputService") +local ContextActionService = game:GetService("ContextActionService") + +--[[ Constants ]]-- +local ZERO_VECTOR3 = Vector3.new(0,0,0) + +--[[ The Module ]]-- +local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController")) +local Keyboard = setmetatable({}, BaseCharacterController) +Keyboard.__index = Keyboard + +function Keyboard.new() + print("Instantiating Keyboard Controller") + local self = setmetatable(BaseCharacterController.new(), Keyboard) + + self.textFocusReleasedConn = nil + self.textFocusGainedConn = nil + self.windowFocusReleasedConn = nil + + self.forwardValue = 0 + self.backwardValue = 0 + self.leftValue = 0 + self.rightValue = 0 + return self +end + +function Keyboard:Enable(enable) + if not UserInputService.KeyboardEnabled then + return false + end + + if enable == self.enabled then + -- Module is already in the state being requested. True is returned here since the module will be in the state + -- expected by the code that follows the Enable() call. This makes more sense than returning false to indicate + -- no action was necessary. False indicates failure to be in requested/expected state. + return true + end + + self.forwardValue = 0 + self.backwardValue = 0 + self.leftValue = 0 + self.rightValue = 0 + + if enable then + self:BindContextActions() + self:ConnectFocusEventListeners() + else + self:UnbindContextActions() + self:DisconnectFocusEventListeners() + end + + self.enabled = enable + return true +end + +function Keyboard:BindContextActions() + + local updateMovement = function(inputState) + if inputState == Enum.UserInputState.Cancel then + self.moveVector = ZERO_VECTOR3 + else + self.moveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue) + end + end + + -- Note: In the previous version of this code, the movement values were not zeroed-out on UserInputState. Cancel, now they are, + -- which fixes them from getting stuck on. + local handleMoveForward = function(actionName, inputState, inputObject) + self.forwardValue = (inputState == Enum.UserInputState.Begin) and -1 or 0 + updateMovement(inputState) + end + + local handleMoveBackward = function(actionName, inputState, inputObject) + self.backwardValue = (inputState == Enum.UserInputState.Begin) and 1 or 0 + updateMovement(inputState) + end + + local handleMoveLeft = function(actionName, inputState, inputObject) + self.leftValue = (inputState == Enum.UserInputState.Begin) and -1 or 0 + updateMovement(inputState) + end + + local handleMoveRight = function(actionName, inputState, inputObject) + self.rightValue = (inputState == Enum.UserInputState.Begin) and 1 or 0 + updateMovement(inputState) + end + + local handleJumpAction = function(actionName, inputState, inputObject) + self.isJumping = (inputState == Enum.UserInputState.Begin) + end + + -- TODO: Revert to KeyCode bindings so that in the future the abstraction layer from actual keys to + -- movement direction is done in Lua + ContextActionService:BindAction("moveForwardAction", handleMoveForward, false, Enum.PlayerActions.CharacterForward) + ContextActionService:BindAction("moveBackwardAction", handleMoveBackward, false, Enum.PlayerActions.CharacterBackward) + ContextActionService:BindAction("moveLeftAction", handleMoveLeft, false, Enum.PlayerActions.CharacterLeft) + ContextActionService:BindAction("moveRightAction", handleMoveRight, false, Enum.PlayerActions.CharacterRight) + ContextActionService:BindAction("jumpAction",handleJumpAction,false,Enum.PlayerActions.CharacterJump) +end + +function Keyboard:UnbindContextActions() + ContextActionService:UnbindAction("moveForwardAction") + ContextActionService:UnbindAction("moveBackwardAction") + ContextActionService:UnbindAction("moveLeftAction") + ContextActionService:UnbindAction("moveRightAction") + ContextActionService:UnbindAction("jumpAction") +end + +function Keyboard:ConnectFocusEventListeners() + local function onFocusReleased() + self.moveVector = ZERO_VECTOR3 + self.forwardValue = 0 + self.backwardValue = 0 + self.leftValue = 0 + self.rightValue = 0 + self.isJumping = false + end + + local function onTextFocusGained(textboxFocused) + self.isJumping = false + end + + self.textFocusReleasedConn = UserInputService.TextBoxFocusReleased:Connect(onFocusReleased) + self.textFocusGainedConn = UserInputService.TextBoxFocused:Connect(onTextFocusGained) + self.windowFocusReleasedConn = UserInputService.WindowFocused:Connect(onFocusReleased) +end + +function Keyboard:DisconnectFocusEventListeners() + if self.textFocusReleasedCon then + self.textFocusReleasedCon:Disconnect() + self.textFocusReleasedCon = nil + end + if self.textFocusGainedConn then + self.textFocusGainedConn:Disconnect() + self.textFocusGainedConn = nil + end + if self.windowFocusReleasedConn then + self.windowFocusReleasedConn:Disconnect() + self.windowFocusReleasedConn = nil + end +end + +return Keyboard \ No newline at end of file diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/PathDisplay.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/PathDisplay.lua new file mode 100644 index 0000000..3ea152d --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/PathDisplay.lua @@ -0,0 +1,131 @@ + + +local PathDisplay = {} +PathDisplay.spacing = 8 +PathDisplay.image = "rbxasset://textures/Cursors/Gamepad/Pointer.png" +PathDisplay.imageSize = Vector2.new(2, 2) + +local currentPoints = {} +local renderedPoints = {} + +local pointModel = Instance.new("Model") +pointModel.Name = "PathDisplayPoints" + +local adorneePart = Instance.new("Part") +adorneePart.Anchored = true +adorneePart.CanCollide = false +adorneePart.Transparency = 1 +adorneePart.Name = "PathDisplayAdornee" +adorneePart.CFrame = CFrame.new(0, 0, 0) +adorneePart.Parent = pointModel + +local pointPool = {} +local poolTop = 30 +for i = 1, poolTop do + local point = Instance.new("ImageHandleAdornment") + point.Archivable = false + point.Adornee = adorneePart + point.Image = PathDisplay.image + point.Size = PathDisplay.imageSize + pointPool[i] = point +end + +local function retrieveFromPool() + local point = pointPool[1] + if not point then + return + end + + pointPool[1], pointPool[poolTop] = pointPool[poolTop], nil + poolTop = poolTop - 1 + return point +end + +local function returnToPool(point) + poolTop = poolTop + 1 + pointPool[poolTop] = point +end + +local function renderPoint(point, isLast) + if poolTop == 0 then + return + end + + local rayDown = Ray.new(point + Vector3.new(0, 2, 0), Vector3.new(0, -8, 0)) + local hitPart, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(rayDown, { game.Players.LocalPlayer.Character, workspace.CurrentCamera }) + if not hitPart then + return + end + + local pointCFrame = CFrame.new(hitPoint, hitPoint + hitNormal) + + local point = retrieveFromPool() + point.CFrame = pointCFrame + point.Parent = pointModel + return point +end + +function PathDisplay.setCurrentPoints(points) + if typeof(points) == 'table' then + currentPoints = points + else + currentPoints = {} + end +end + +function PathDisplay.clearRenderedPath() + for _, oldPoint in ipairs(renderedPoints) do + oldPoint.Parent = nil + returnToPool(oldPoint) + end + renderedPoints = {} + pointModel.Parent = nil +end + +function PathDisplay.renderPath() + PathDisplay.clearRenderedPath() + if not currentPoints or #currentPoints == 0 then + return + end + + local currentIdx = #currentPoints + local lastPos = currentPoints[currentIdx] + local distanceBudget = 0 + + renderedPoints[1] = renderPoint(lastPos, true) + if not renderedPoints[1] then + return + end + + while true do + local currentPoint = currentPoints[currentIdx] + local nextPoint = currentPoints[currentIdx - 1] + + if currentIdx < 2 then + break + else + + local toNextPoint = nextPoint - currentPoint + local distToNextPoint = toNextPoint.magnitude + + if distanceBudget > distToNextPoint then + distanceBudget = distanceBudget - distToNextPoint + currentIdx = currentIdx - 1 + else + local dirToNextPoint = toNextPoint.unit + local pointPos = currentPoint + (dirToNextPoint * distanceBudget) + local point = renderPoint(pointPos, false) + + if point then + renderedPoints[#renderedPoints + 1] = point + end + + distanceBudget = distanceBudget + PathDisplay.spacing + end + end + end + + pointModel.Parent = workspace.CurrentCamera +end + +return PathDisplay diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/TouchDPad.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/TouchDPad.lua new file mode 100644 index 0000000..7673c70 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/TouchDPad.lua @@ -0,0 +1,177 @@ +--[[ + + + +--]] + +local Players = game:GetService("Players") +local GuiService = game:GetService("GuiService") + +--[[ Constants ]]-- +local DPAD_SHEET = "rbxasset://textures/ui/DPadSheet.png" +local ZERO_VECTOR3 = Vector3.new(0,0,0) +local COMPASS_DIR = { + Vector3.new(1, 0, 0), -- E + Vector3.new(1, 0, 1).unit, -- SE + Vector3.new(0, 0, 1), -- S + Vector3.new(-1, 0, 1).unit, -- SW + Vector3.new(-1, 0, 0), -- W + Vector3.new(-1, 0, -1).unit, -- NW + Vector3.new(0, 0, -1), -- N + Vector3.new(1, 0, -1).unit, -- NE +} + + +--[[ The Module ]]-- +local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController")) +local TouchDPad = setmetatable({}, BaseCharacterController) +TouchDPad.__index = TouchDPad + +function TouchDPad.new() + local self = setmetatable(BaseCharacterController.new(), TouchDPad) + + self.DPadFrame = nil + self.touchObject = nil + self.flBtn = nil + self.frBtn = nil + + return self +end + +--[[ Local Functions ]]-- +local function CreateArrowLabel(name, position, size, rectOffset, rectSize, parent) + local image = Instance.new("ImageLabel") + image.Name = name + image.Image = DPAD_SHEET + image.ImageRectOffset = rectOffset + image.ImageRectSize = rectSize + image.BackgroundTransparency = 1 + image.Size = size + image.Position = position + image.Parent = parent + return image +end + +function TouchDPad:GetCenterPosition() + return Vector2.new(self.DPadFrame.AbsolutePosition.x + self.DPadFrame.AbsoluteSize.x * 0.5, self.DPadFrame.AbsolutePosition.y + self.DPadFrame.AbsoluteSize.y * 0.5) +end + +--[[ Public API ]]-- +function TouchDPad:Enable(enable, uiParentFrame) + if enable == nil then return false end -- If nil, return false (invalid argument) + enable = enable and true or false -- Force anything non-nil to boolean before comparison + if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state + + if enable then + -- Enable + if not self.DPadFrame then + self:Create(uiParentFrame) + end + self.DPadFrame.Visible = true + else + -- Disable + self.DPadFrame.Visible = false + self:OnInputEnded() + end + self.enabled = enable +end + +-- Note: Overrides base class GetIsJumping with get-and-clear behavior to do a single jump +-- rather than sustained jumping. This is only to preserve the current behavior through the refactor. +function TouchDPad:GetIsJumping() + local wasJumping = self.isJumping + self.isJumping = false + return wasJumping +end + +function TouchDPad:OnInputEnded() + self.touchObject =nil + if self.flBtn then self.flBtn.Visible = false end + if self.frBtn then self.frBtn.Visible = false end + self.moveVector = ZERO_VECTOR3 +end + +function TouchDPad:Create(parentFrame) + if self.DPadFrame then + self.DPadFrame:Destroy() + self.DPadFrame = nil + end + + local position = UDim2.new(0, 10, 1, -230) + self.DPadFrame = Instance.new("Frame") + self.DPadFrame.Name = "DPadFrame" + self.DPadFrame.Active = true + self.DPadFrame.Visible = false + self.DPadFrame.Size = UDim2.new(0, 192, 0, 192) + self.DPadFrame.Position = position + self.DPadFrame.BackgroundTransparency = 1 + + local smArrowSize = UDim2.new(0, 23, 0, 23) + local lgArrowSize = UDim2.new(0, 64, 0, 64) + local smImgOffset = Vector2.new(46, 46) + local lgImgOffset = Vector2.new(128, 128) + + local bBtn = CreateArrowLabel("BackButton", UDim2.new(0.5, -32, 1, -64), lgArrowSize, Vector2.new(0, 0), lgImgOffset, self.DPadFrame) + local fBtn = CreateArrowLabel("ForwardButton", UDim2.new(0.5, -32, 0, 0), lgArrowSize, Vector2.new(0, 258), lgImgOffset, self.DPadFrame) + local lBtn = CreateArrowLabel("LeftButton", UDim2.new(0, 0, 0.5, -32), lgArrowSize, Vector2.new(129, 129), lgImgOffset, self.DPadFrame) + local rBtn = CreateArrowLabel("RightButton", UDim2.new(1, -64, 0.5, -32), lgArrowSize, Vector2.new(0, 129), lgImgOffset, self.DPadFrame) + local jumpBtn = CreateArrowLabel("JumpButton", UDim2.new(0.5, -32, 0.5, -32), lgArrowSize, Vector2.new(129, 0), lgImgOffset, self.DPadFrame) + self.flBtn = CreateArrowLabel("ForwardLeftButton", UDim2.new(0, 35, 0, 35), smArrowSize, Vector2.new(129, 258), smImgOffset, self.DPadFrame) + self.frBtn = CreateArrowLabel("ForwardRightButton", UDim2.new(1, -55, 0, 35), smArrowSize, Vector2.new(176, 258), smImgOffset, self.DPadFrame) + self.flBtn.Visible = false + self.frBtn.Visible = false + + -- input connections + jumpBtn.InputBegan:Connect(function(inputObject) + self.isJumping = true + end) + + local function normalizeDirection(inputPosition) + local jumpRadius = jumpBtn.AbsoluteSize.x*0.5 + local centerPosition = self:GetCenterPosition() + local direction = Vector2.new(inputPosition.x - centerPosition.x, inputPosition.y - centerPosition.y) + + if direction.magnitude > jumpRadius then + local angle = math.atan2(direction.y, direction.x) + local octant = (math.floor(8 * angle / (2 * math.pi) + 8.5)%8) + 1 + self.moveVector = COMPASS_DIR[octant] + end + + if not self.flBtn.Visible and self.moveVector == COMPASS_DIR[7] then + self.flBtn.Visible = true + self.frBtn.Visible = true + end + end + + self.DPadFrame.InputBegan:Connect(function(inputObject) + if self.touchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch then + return + end + + self.touchObject = inputObject + normalizeDirection(self.touchObject.Position) + end) + + self.DPadFrame.InputChanged:Connect(function(inputObject) + if inputObject == self.touchObject then + normalizeDirection(self.touchObject.Position) + self.isJumping = false + end + end) + + self.DPadFrame.InputEnded:connect(function(inputObject) + if inputObject == self.touchObject then + self:OnInputEnded() + end + end) + + GuiService.MenuOpened:Connect(function() + if self.touchObject then + self:OnInputEnded() + end + end) + + self.DPadFrame.Parent = parentFrame +end + +return TouchDPad diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/TouchJump.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/TouchJump.lua new file mode 100644 index 0000000..09cbd67 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/TouchJump.lua @@ -0,0 +1,202 @@ +--[[ + // FileName: TouchJump + // Version 1.0 + // Written by: jmargh + // Description: Implements jump controls for touch devices. Use with Thumbstick and Thumbpad +--]] + +local Players = game:GetService("Players") +local GuiService = game:GetService("GuiService") + +--[[ Constants ]]-- +local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png" + +--[[ The Module ]]-- +local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController")) +local TouchJump = setmetatable({}, BaseCharacterController) +TouchJump.__index = TouchJump + +function TouchJump.new() + local self = setmetatable(BaseCharacterController.new(), TouchJump) + + self.parentUIFrame = nil + self.jumpButton = nil + self.characterAddedConn = nil + self.humanoidStateEnabledChangedConn = nil + self.humanoidJumpPowerConn = nil + self.humanoidParentConn = nil + self.externallyEnabled = false + self.jumpPower = 0 + self.jumpStateEnabled = true + self.isJumping = false + self.humanoid = nil -- saved reference because property change connections are made using it + + return self +end + +function TouchJump:EnableButton(enable) + if enable then + if not self.jumpButton then + self:Create() + end + local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid") + if humanoid and self.externallyEnabled then + if self.externallyEnabled then + if humanoid.JumpPower > 0 then + self.jumpButton.Visible = true + end + end + end + else + self.jumpButton.Visible = false + self.isJumping = false + self.jumpButton.ImageRectOffset = Vector2.new(176, 222) + end +end + +function TouchJump:UpdateEnabled() + if self.jumpPower > 0 and self.jumpStateEnabled then + self:EnableButton(true) + else + self:EnableButton(false) + end +end + +function TouchJump:HumanoidChanged(prop) + local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid") + if humanoid then + if prop == "JumpPower" then + self.jumpPower = humanoid.JumpPower + self:UpdateEnabled() + elseif prop == "Parent" then + if not humanoid.Parent then + self.humanoidChangeConn:Disconnect() + end + end + end +end + +function TouchJump:HumanoidStateEnabledChanged(state, isEnabled) + if state == Enum.HumanoidStateType.Jumping then + self.jumpStateEnabled = isEnabled + self:UpdateEnabled() + end +end + +function TouchJump:CharacterAdded(char) + if self.humanoidChangeConn then + self.humanoidChangeConn:Disconnect() + self.humanoidChangeConn = nil + end + + self.humanoid = char:FindFirstChildOfClass("Humanoid") + while not self.humanoid do + char.ChildAdded:wait() + self.humanoid = char:FindFirstChildOfClass("Humanoid") + end + + self.humanoidJumpPowerConn = self.humanoid:GetPropertyChangedSignal("JumpPower"):Connect(function() + self.jumpPower = self.humanoid.JumpPower + self:UpdateEnabled() + end) + + self.humanoidParentConn = self.humanoid:GetPropertyChangedSignal("Parent"):Connect(function() + if not self.humanoid.Parent then + self.humanoidJumpPowerConn:Disconnect() + self.humanoidJumpPowerConn = nil + self.humanoidParentConn:Disconnect() + self.humanoidParentConn = nil + end + end) + + self.humanoidStateEnabledChangedConn = self.humanoid.StateEnabledChanged:Connect(function(state, enabled) + self:HumanoidStateEnabledChanged(state, enabled) + end) + + self.jumpPower = self.humanoid.JumpPower + self.jumpStateEnabled = self.humanoid:GetStateEnabled(Enum.HumanoidStateType.Jumping) + self:UpdateEnabled() +end + +function TouchJump:SetupCharacterAddedFunction() + self.characterAddedConn = Players.LocalPlayer.CharacterAdded:Connect(function(char) + self:CharacterAdded(char) + end) + if Players.LocalPlayer.Character then + self:CharacterAdded(Players.LocalPlayer.Character) + end +end + +--[[ Public API ]]-- +function TouchJump:Enable(enable, parentFrame) + self.parentUIFrame = parentFrame + self.externallyEnabled = enable + self:EnableButton(enable) +end + +function TouchJump:Create() + if not self.parentUIFrame then + return + end + + if self.jumpButton then + self.jumpButton:Destroy() + self.jumpButton = nil + end + + local minAxis = math.min(self.parentUIFrame.AbsoluteSize.x, self.parentUIFrame.AbsoluteSize.y) + local isSmallScreen = minAxis <= 500 + local jumpButtonSize = isSmallScreen and 70 or 120 + + self.jumpButton = Instance.new("ImageButton") + self.jumpButton.Name = "JumpButton" + self.jumpButton.Visible = false + self.jumpButton.BackgroundTransparency = 1 + self.jumpButton.Image = TOUCH_CONTROL_SHEET + self.jumpButton.ImageRectOffset = Vector2.new(1, 146) + self.jumpButton.ImageRectSize = Vector2.new(144, 144) + self.jumpButton.Size = UDim2.new(0, jumpButtonSize, 0, jumpButtonSize) + + self.jumpButton.Position = isSmallScreen and UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize - 20) or + UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize * 1.75) + + local touchObject = nil + self.jumpButton.InputBegan:connect(function(inputObject) + --A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event + --if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin) + if touchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch + or inputObject.UserInputState ~= Enum.UserInputState.Begin then + return + end + + touchObject = inputObject + self.jumpButton.ImageRectOffset = Vector2.new(146, 146) + self.isJumping = true + end) + + local OnInputEnded = function() + touchObject = nil + self.isJumping = false + self.jumpButton.ImageRectOffset = Vector2.new(1, 146) + end + + self.jumpButton.InputEnded:connect(function(inputObject) + if inputObject == touchObject then + OnInputEnded() + end + end) + + GuiService.MenuOpened:connect(function() + if touchObject then + OnInputEnded() + end + end) + + if not self.characterAddedConn then + self:SetupCharacterAddedFunction() + end + + self.jumpButton.Parent = self.parentUIFrame +end + +return TouchJump diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/TouchThumbpad.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/TouchThumbpad.lua new file mode 100644 index 0000000..cd3b569 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/TouchThumbpad.lua @@ -0,0 +1,251 @@ +--[[ + + Stub + +--]] + +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local GuiService = game:GetService("GuiService") + +--[[ Constants ]]-- +local DPAD_SHEET = "rbxasset://textures/ui/DPadSheet.png" +local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/TouchControlsSheet.png" +local ZERO_VECTOR3 = Vector3.new(0,0,0) +local UNIT_Z = Vector3.new(0,0,1) +local UNIT_X = Vector3.new(1,0,0) + +--[[ The Module ]]-- +local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController")) +local TouchThumbpad = setmetatable({}, BaseCharacterController) +TouchThumbpad.__index = TouchThumbpad + +function TouchThumbpad.new() + local self = setmetatable(BaseCharacterController.new(), TouchThumbpad) + + self.thumbpadFrame = nil + self.touchChangedConn = nil + self.touchEndedConn = nil + self.menuOpenedConn = nil + self.screenPos = nil + self.isRight, self.isLeft, self.isUp, self.isDown = false, false, false, false + self.smArrowSize = nil + self.lgArrowSize = nil + self.smImgOffset = nil + self.lgImgOffset = nil + return self +end + +--[[ Local Helper Functions ]]-- +local function doTween(guiObject, endSize, endPosition) + guiObject:TweenSizeAndPosition(endSize, endPosition, Enum.EasingDirection.InOut, Enum.EasingStyle.Linear, 0.15, true) +end + +local function CreateArrowLabel(name, position, size, rectOffset, rectSize, parent) + local image = Instance.new("ImageLabel") + image.Name = name + image.Image = DPAD_SHEET + image.ImageRectOffset = rectOffset + image.ImageRectSize = rectSize + image.BackgroundTransparency = 1 + image.ImageColor3 = Color3.fromRGB(190, 190, 190) + image.Size = size + image.Position = position + image.Parent = parent + return image +end + +--[[ Public API ]]-- +function TouchThumbpad:Enable(enable, uiParentFrame) + if enable == nil then return false end -- If nil, return false (invalid argument) + enable = enable and true or false -- Force anything non-nil to boolean before comparison + if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state + + if enable then + -- Enable + if not self.thumbpadFrame then + self:Create(uiParentFrame) + end + self.thumbpadFrame.Visible = true + else + -- Disable + self.thumbpadFrame.Visible = false + self:OnInputEnded() + end + self.enabled = enable +end + +function TouchThumbpad:OnInputEnded() + self.moveVector = Vector3.new(0,0,0) + self.isJumping = false + + self.thumbpadFrame.Position = self.screenPos + self.touchObject = nil + self.isUp, self.isDown, self.isLeft, self.isRight = false, false, false, false + doTween(self.dArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 1, self.lgImgOffset)) + doTween(self.uArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 0, self.smImgOffset)) + doTween(self.lArrow, self.smArrowSize, UDim2.new(0, self.smImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset)) + doTween(self.rArrow, self.smArrowSize, UDim2.new(1, self.lgImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset)) +end + +function TouchThumbpad:Create(parentFrame) + if self.thumbpadFrame then + self.thumbpadFrame:Destroy() + self.thumbpadFrame = nil + end + if self.touchChangedConn then + self.touchChangedConn:Disconnect() + self.touchChangedConn = nil + end + if self.touchEndedConn then + self.touchEndedConn:Disconnect() + self.touchEndedConn = nil + end + if self.menuOpenedConn then + self.menuOpenedConn:Disconnect() + self.menuOpenedConn = nil + end + + local minAxis = math.min(parentFrame.AbsoluteSize.x, parentFrame.AbsoluteSize.y) + local isSmallScreen = minAxis <= 500 + local thumbpadSize = isSmallScreen and 70 or 120 + self.screenPos = isSmallScreen and UDim2.new(0, thumbpadSize * 1.25, 1, -thumbpadSize - 20) or + UDim2.new(0, thumbpadSize * 0.5 - 10, 1, -thumbpadSize * 1.75 - 10) + + self.thumbpadFrame = Instance.new("Frame") + self.thumbpadFrame.Name = "ThumbpadFrame" + self.thumbpadFrame.Visible = false + self.thumbpadFrame.Active = true + self.thumbpadFrame.Size = UDim2.new(0, thumbpadSize + 20, 0, thumbpadSize + 20) + self.thumbpadFrame.Position = self.screenPos + self.thumbpadFrame.BackgroundTransparency = 1 + + local outerImage = Instance.new("ImageLabel") + outerImage.Name = "OuterImage" + outerImage.Image = TOUCH_CONTROL_SHEET + outerImage.ImageRectOffset = Vector2.new(0, 0) + outerImage.ImageRectSize = Vector2.new(220, 220) + outerImage.BackgroundTransparency = 1 + outerImage.Size = UDim2.new(0, thumbpadSize, 0, thumbpadSize) + outerImage.Position = UDim2.new(0, 10, 0, 10) + outerImage.Parent = self.thumbpadFrame + + self.smArrowSize = isSmallScreen and UDim2.new(0, 32, 0, 32) or UDim2.new(0, 64, 0, 64) + self.lgArrowSize = UDim2.new(0, self.smArrowSize.X.Offset * 2, 0, self.smArrowSize.Y.Offset * 2) + local imgRectSize = Vector2.new(110, 110) + self.smImgOffset = isSmallScreen and -4 or -9 + self.lgImgOffset = isSmallScreen and -28 or -55 + + self.dArrow = CreateArrowLabel("DownArrow", UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 1, self.lgImgOffset), self.smArrowSize, Vector2.new(8, 8), imgRectSize, outerImage) + self.uArrow = CreateArrowLabel("UpArrow", UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 0, self.smImgOffset), self.smArrowSize, Vector2.new(8, 266), imgRectSize, outerImage) + self.lArrow = CreateArrowLabel("LeftArrow", UDim2.new(0, self.smImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset), self.smArrowSize, Vector2.new(137, 137), imgRectSize, outerImage) + self.rArrow = CreateArrowLabel("RightArrow", UDim2.new(1, self.lgImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset), self.smArrowSize, Vector2.new(8, 137), imgRectSize, outerImage) + + local function doTween(guiObject, endSize, endPosition) + guiObject:TweenSizeAndPosition(endSize, endPosition, Enum.EasingDirection.InOut, Enum.EasingStyle.Linear, 0.15, true) + end + + local padOrigin = nil + local deadZone = 0.1 + self.isRight, self.isLeft, self.isUp, self.isDown = false, false, false, false + + local function doMove(pos) + --MasterControl:AddToPlayerMovement(-currentMoveVector) + + local moveDelta = pos - padOrigin + local moveVector2 = 2 * moveDelta / thumbpadSize + + -- Scaled Radial Dead Zone + if moveVector2.Magnitude < deadZone then + self.moveVector = ZERO_VECTOR3 + else + moveVector2 = moveVector2.unit * ((moveVector2.Magnitude - deadZone) / (1 - deadZone)) + + -- prevent NAN Vector from trying to do zerovector.Unit + if moveVector2.Magnitude == 0 then + self.moveVector = ZERO_VECTOR3 + else + self.moveVector = Vector3.new(moveVector2.x, 0, moveVector2.y).Unit + end + end + + --MasterControl:AddToPlayerMovement(currentMoveVector) + + local forwardDot = self.moveVector:Dot(-UNIT_Z) + local rightDot = self.moveVector:Dot(UNIT_X) + + if forwardDot > 0.5 then -- UP + if not self.isUp then + self.isUp, self.isDown = true, false + doTween(self.uArrow, self.lgArrowSize, UDim2.new(0.5, -self.smArrowSize.X.Offset, 0, self.smImgOffset - 1.5*self.smArrowSize.Y.Offset)) + doTween(self.dArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 1, self.lgImgOffset)) + end + elseif forwardDot < -0.5 then -- DOWN + if not self.isDown then + self.isDown, self.isUp = true, false + doTween(self.dArrow, self.lgArrowSize, UDim2.new(0.5, -self.smArrowSize.X.Offset, 1, self.lgImgOffset + 0.5*self.smArrowSize.Y.Offset)) + doTween(self.uArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 0, self.smImgOffset)) + end + else + self.isUp, self.isDown = false, false + doTween(self.dArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 1, self.lgImgOffset)) + doTween(self.uArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 0, self.smImgOffset)) + end + + if rightDot > 0.5 then + if not self.isRight then + self.isRight, self.isLeft = true, false + doTween(self.rArrow, self.lgArrowSize, UDim2.new(1, self.lgImgOffset + 0.5*self.smArrowSize.X.Offset, 0.5, -self.smArrowSize.Y.Offset)) + doTween(self.lArrow, self.smArrowSize, UDim2.new(0, self.smImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset)) + end + elseif rightDot < -0.5 then + if not self.isLeft then + self.isLeft, self.isRight = true, false + doTween(self.lArrow, self.lgArrowSize, UDim2.new(0, self.smImgOffset - 1.5*self.smArrowSize.X.Offset, 0.5, -self.smArrowSize.Y.Offset)) + doTween(self.rArrow, self.smArrowSize, UDim2.new(1, self.lgImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset)) + end + else + self.isRight, self.isLeft = false, false + doTween(self.lArrow, self.smArrowSize, UDim2.new(0, self.smImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset)) + doTween(self.rArrow, self.smArrowSize, UDim2.new(1, self.lgImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset)) + end + end + + --input connections + self.thumbpadFrame.InputBegan:connect(function(inputObject) + --A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event + --if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin) + if self.touchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch + or inputObject.UserInputState ~= Enum.UserInputState.Begin then + return + end + + self.thumbpadFrame.Position = UDim2.new(0, inputObject.Position.x - 0.5*self.thumbpadFrame.AbsoluteSize.x, 0, inputObject.Position.y - 0.5*self.thumbpadFrame.Size.Y.Offset) + padOrigin = Vector3.new(self.thumbpadFrame.AbsolutePosition.x +0.5* self.thumbpadFrame.AbsoluteSize.x, + self.thumbpadFrame.AbsolutePosition.y + 0.5*self.thumbpadFrame.AbsoluteSize.y, 0) + doMove(inputObject.Position) + self.touchObject = inputObject + end) + + self.touchChangedConn = UserInputService.TouchMoved:connect(function(inputObject, isProcessed) + if inputObject == self.touchObject then + doMove(self.touchObject.Position) + end + end) + + self.touchEndedConn = UserInputService.TouchEnded:Connect(function(inputObject) + if inputObject == self.touchObject then + self:OnInputEnded() + end + end) + + self.menuOpenedConn = GuiService.MenuOpened:Connect(function() + if self.touchObject then + self:OnInputEnded() + end + end) + + self.thumbpadFrame.Parent = parentFrame +end + +return TouchThumbpad diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/TouchThumbstick.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/TouchThumbstick.lua new file mode 100644 index 0000000..f782363 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/TouchThumbstick.lua @@ -0,0 +1,195 @@ +--[[ + + Stub + +--]] + +local Players = game:GetService("Players") +local GuiService = game:GetService("GuiService") +local UserInputService = game:GetService("UserInputService") + +--[[ Constants ]]-- +local ZERO_VECTOR3 = Vector3.new(0,0,0) +local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/TouchControlsSheet.png" + +--[[ The Module ]]-- +local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController")) +local TouchThumbstick = setmetatable({}, BaseCharacterController) +TouchThumbstick.__index = TouchThumbstick + +function TouchThumbstick.new() + local self = setmetatable(BaseCharacterController.new(), TouchThumbstick) + + self.isFollowStick = false + + self.thumbstickFrame = nil + self.moveTouchObject = nil + self.onTouchMovedConn = nil + self.onTouchEndedConn = nil + self.screenPos = nil + self.stickImage = nil + self.thumbstickSize = nil -- Float + + return self +end + +--[[ Public API ]]-- +function TouchThumbstick:Enable(enable, uiParentFrame) + if enable == nil then return false end -- If nil, return false (invalid argument) + enable = enable and true or false -- Force anything non-nil to boolean before comparison + if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state + + if enable then + -- Enable + if not self.thumbstickFrame then + self:Create(uiParentFrame) + end + self.thumbstickFrame.Visible = true + else + -- Disable + self.thumbstickFrame.Visible = false + self:OnInputEnded() + end + self.enabled = enable +end + +function TouchThumbstick:OnInputEnded() + self.thumbstickFrame.Position = self.screenPos + self.stickImage.Position = UDim2.new(0, self.thumbstickFrame.Size.X.Offset/2 - self.thumbstickSize/4, 0, self.thumbstickFrame.Size.Y.Offset/2 - self.thumbstickSize/4) + + self.moveVector = Vector3.new(0,0,0) + self.isJumping = false + + self.thumbstickFrame.Position = self.screenPos + self.moveTouchObject = nil +end + +function TouchThumbstick:Create(parentFrame) + + if self.thumbstickFrame then + self.thumbstickFrame:Destroy() + self.thumbstickFrame = nil + if self.onTouchMovedConn then + self.onTouchMovedConn:Disconnect() + self.onTouchMovedConn = nil + end + if self.onTouchEndedConn then + self.onTouchEndedConn:Disconnect() + self.onTouchEndedConn = nil + end + end + + local minAxis = math.min(parentFrame.AbsoluteSize.x, parentFrame.AbsoluteSize.y) + local isSmallScreen = minAxis <= 500 + self.thumbstickSize = isSmallScreen and 70 or 120 + self.screenPos = isSmallScreen and UDim2.new(0, (self.thumbstickSize/2) - 10, 1, -self.thumbstickSize - 20) or + UDim2.new(0, self.thumbstickSize/2, 1, -self.thumbstickSize * 1.75) + + self.thumbstickFrame = Instance.new("Frame") + self.thumbstickFrame.Name = "ThumbstickFrame" + self.thumbstickFrame.Active = true + self.thumbstickFrame.Visible = false + self.thumbstickFrame.Size = UDim2.new(0, self.thumbstickSize, 0, self.thumbstickSize) + self.thumbstickFrame.Position = self.screenPos + self.thumbstickFrame.BackgroundTransparency = 1 + + local outerImage = Instance.new("ImageLabel") + outerImage.Name = "OuterImage" + outerImage.Image = TOUCH_CONTROL_SHEET + outerImage.ImageRectOffset = Vector2.new() + outerImage.ImageRectSize = Vector2.new(220, 220) + outerImage.BackgroundTransparency = 1 + outerImage.Size = UDim2.new(0, self.thumbstickSize, 0, self.thumbstickSize) + outerImage.Position = UDim2.new(0, 0, 0, 0) + outerImage.Parent = self.thumbstickFrame + + self.stickImage = Instance.new("ImageLabel") + self.stickImage.Name = "StickImage" + self.stickImage.Image = TOUCH_CONTROL_SHEET + self.stickImage.ImageRectOffset = Vector2.new(220, 0) + self.stickImage.ImageRectSize = Vector2.new(111, 111) + self.stickImage.BackgroundTransparency = 1 + self.stickImage.Size = UDim2.new(0, self.thumbstickSize/2, 0, self.thumbstickSize/2) + self.stickImage.Position = UDim2.new(0, self.thumbstickSize/2 - self.thumbstickSize/4, 0, self.thumbstickSize/2 - self.thumbstickSize/4) + self.stickImage.ZIndex = 2 + self.stickImage.Parent = self.thumbstickFrame + + local centerPosition = nil + local deadZone = 0.05 + + local function DoMove(direction) + + local currentMoveVector = direction / (self.thumbstickSize/2) + + -- Scaled Radial Dead Zone + local inputAxisMagnitude = currentMoveVector.magnitude + if inputAxisMagnitude < deadZone then + currentMoveVector = Vector3.new() + else + currentMoveVector = currentMoveVector.unit * ((inputAxisMagnitude - deadZone) / (1 - deadZone)) + -- NOTE: Making currentMoveVector a unit vector will cause the player to instantly go max speed + -- must check for zero length vector is using unit + currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y) + end + + self.moveVector = currentMoveVector + end + + local function MoveStick(pos) + local relativePosition = Vector2.new(pos.x - centerPosition.x, pos.y - centerPosition.y) + local length = relativePosition.magnitude + local maxLength = self.thumbstickFrame.AbsoluteSize.x/2 + if self.isFollowStick and length > maxLength then + local offset = relativePosition.unit * maxLength + self.thumbstickFrame.Position = UDim2.new( + 0, pos.x - self.thumbstickFrame.AbsoluteSize.x/2 - offset.x, + 0, pos.y - self.thumbstickFrame.AbsoluteSize.y/2 - offset.y) + else + length = math.min(length, maxLength) + relativePosition = relativePosition.unit * length + end + self.stickImage.Position = UDim2.new(0, relativePosition.x + self.stickImage.AbsoluteSize.x/2, 0, relativePosition.y + self.stickImage.AbsoluteSize.y/2) + end + + -- input connections + self.thumbstickFrame.InputBegan:Connect(function(inputObject) + --A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event + --if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin) + if self.moveTouchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch + or inputObject.UserInputState ~= Enum.UserInputState.Begin then + return + end + + self.moveTouchObject = inputObject + self.thumbstickFrame.Position = UDim2.new(0, inputObject.Position.x - self.thumbstickFrame.Size.X.Offset/2, 0, inputObject.Position.y - self.thumbstickFrame.Size.Y.Offset/2) + centerPosition = Vector2.new(self.thumbstickFrame.AbsolutePosition.x + self.thumbstickFrame.AbsoluteSize.x/2, + self.thumbstickFrame.AbsolutePosition.y + self.thumbstickFrame.AbsoluteSize.y/2) + local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y) + end) + + self.onTouchMovedConn = UserInputService.TouchMoved:Connect(function(inputObject, isProcessed) + if inputObject == self.moveTouchObject then + centerPosition = Vector2.new(self.thumbstickFrame.AbsolutePosition.x + self.thumbstickFrame.AbsoluteSize.x/2, + self.thumbstickFrame.AbsolutePosition.y + self.thumbstickFrame.AbsoluteSize.y/2) + local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y) + DoMove(direction) + MoveStick(inputObject.Position) + end + end) + + self.onTouchEndedConn = UserInputService.TouchEnded:Connect(function(inputObject, isProcessed) + if inputObject == self.moveTouchObject then + self:OnInputEnded() + end + end) + + GuiService.MenuOpened:Connect(function() + if self.moveTouchObject then + self:OnInputEnded() + end + end) + + self.thumbstickFrame.Parent = parentFrame +end + +return TouchThumbstick diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/VRNavigation.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/VRNavigation.lua new file mode 100644 index 0000000..7224147 --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/VRNavigation.lua @@ -0,0 +1,454 @@ + +--[[ + +--]] + +local VRService = game:GetService("VRService") +local UserInputService = game:GetService("UserInputService") +local RunService = game:GetService("RunService") +local Players = game:GetService("Players") +local PathfindingService = game:GetService("PathfindingService") +local ContextActionService = game:GetService("ContextActionService") +local StarterGui = game:GetService("StarterGui") + +--local MasterControl = require(script.Parent) +local PathDisplay = nil +local LocalPlayer = Players.LocalPlayer + +--[[ Constants ]]-- +local RECALCULATE_PATH_THRESHOLD = 4 +local NO_PATH_THRESHOLD = 12 +local MAX_PATHING_DISTANCE = 200 +local POINT_REACHED_THRESHOLD = 1 +local STOPPING_DISTANCE = 4 +local OFFTRACK_TIME_THRESHOLD = 2 +local THUMBSTICK_DEADZONE = 0.22 + +local ZERO_VECTOR3 = Vector3.new(0,0,0) +local XZ_VECTOR3 = Vector3.new(1,0,1) + +--[[ Utility Functions ]]-- +local function IsFinite(num) + return num == num and num ~= 1/0 and num ~= -1/0 +end + +local function IsFiniteVector3(vec3) + return IsFinite(vec3.x) and IsFinite(vec3.y) and IsFinite(vec3.z) +end + +local movementUpdateEvent = Instance.new("BindableEvent") +movementUpdateEvent.Name = "MovementUpdate" +movementUpdateEvent.Parent = script + +coroutine.wrap(function() + local PathDisplayModule = script.Parent:WaitForChild("PathDisplay") + if PathDisplayModule then + PathDisplay = require(PathDisplayModule) + end +end)() + + +--[[ The Class ]]-- +local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController")) +local VRNavigation = setmetatable({}, BaseCharacterController) +VRNavigation.__index = VRNavigation + +function VRNavigation.new() + local self = setmetatable(BaseCharacterController.new(), VRNavigation) + + self.navigationRequestedConn = nil + self.heartbeatConn = nil + + self.currentDestination = nil + self.currentPath = nil + self.currentPoints = nil + self.currentPointIdx = 0 + + self.expectedTimeToNextPoint = 0 + self.timeReachedLastPoint = tick() + self.moving = false + + self.isJumpBound = false + self.moveLatch = false + + self.userCFrameEnabledConn = nil + + return self +end + +function VRNavigation:SetLaserPointerMode(mode) + pcall(function() + StarterGui:SetCore("VRLaserPointerMode", mode) + end) +end + +function VRNavigation:GetLocalHumanoid() + local character = LocalPlayer.Character + if not character then + return + end + + for _, child in pairs(character:GetChildren()) do + if child:IsA("Humanoid") then + return child + end + end + return nil +end + +function VRNavigation:HasBothHandControllers() + return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) and VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand) +end + +function VRNavigation:HasAnyHandControllers() + return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) or VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand) +end + +function VRNavigation:IsMobileVR() + return UserInputService.TouchEnabled +end + +function VRNavigation:HasGamepad() + return UserInputService.GamepadEnabled +end + +function VRNavigation:ShouldUseNavigationLaser() + --Places where we use the navigation laser: + -- mobile VR with any number of hands tracked + -- desktop VR with only one hand tracked + -- desktop VR with no hands and no gamepad (i.e. with Oculus remote?) + --using an Xbox controller with a desktop VR headset means no laser since the user has a thumbstick. + --in the future, we should query thumbstick presence with a features API + if self:IsMobileVR() then + return true + else + if self:HasBothHandControllers() then + return false + end + if not self:HasAnyHandControllers() then + return not self:HasGamepad() + end + return true + end +end + + + +function VRNavigation:StartFollowingPath(newPath) + currentPath = newPath + currentPoints = currentPath:GetPointCoordinates() + currentPointIdx = 1 + moving = true + + timeReachedLastPoint = tick() + + local humanoid = self:GetLocalHumanoid() + if humanoid and humanoid.Torso and #currentPoints >= 1 then + local dist = (currentPoints[1] - humanoid.Torso.Position).magnitude + expectedTimeToNextPoint = dist / humanoid.WalkSpeed + end + + movementUpdateEvent:Fire("targetPoint", self.currentDestination) +end + +function VRNavigation:GoToPoint(point) + currentPath = true + currentPoints = { point } + currentPointIdx = 1 + moving = true + + local humanoid = self:GetLocalHumanoid() + local distance = (humanoid.Torso.Position - point).magnitude + local estimatedTimeRemaining = distance / humanoid.WalkSpeed + + timeReachedLastPoint = tick() + expectedTimeToNextPoint = estimatedTimeRemaining + + movementUpdateEvent:Fire("targetPoint", point) +end + +function VRNavigation:StopFollowingPath() + currentPath = nil + currentPoints = nil + currentPointIdx = 0 + moving = false + --MasterControl:AddToPlayerMovement(-self.moveVector) + self.moveVector = ZERO_VECTOR3 +end + +function VRNavigation:TryComputePath(startPos, destination) + local numAttempts = 0 + local newPath = nil + + while not newPath and numAttempts < 5 do + newPath = PathfindingService:ComputeSmoothPathAsync(startPos, destination, MAX_PATHING_DISTANCE) + numAttempts = numAttempts + 1 + + if newPath.Status == Enum.PathStatus.ClosestNoPath or newPath.Status == Enum.PathStatus.ClosestOutOfRange then + newPath = nil + break + end + + if newPath and newPath.Status == Enum.PathStatus.FailStartNotEmpty then + startPos = startPos + (destination - startPos).unit + newPath = nil + end + + if newPath and newPath.Status == Enum.PathStatus.FailFinishNotEmpty then + destination = destination + Vector3.new(0, 1, 0) + newPath = nil + end + end + + return newPath +end + +function VRNavigation:OnNavigationRequest(destinationCFrame, inputUserCFrame ) + local destinationPosition = destinationCFrame.p + local lastDestination = self.currentDestination + + if not IsFiniteVector3(destinationPosition) then + return + end + + self.currentDestination = destinationPosition + + local humanoid = self:GetLocalHumanoid() + if not humanoid or not humanoid.Torso then + return + end + + local currentPosition = humanoid.Torso.Position + local distanceToDestination = (self.currentDestination - currentPosition).magnitude + + if distanceToDestination < NO_PATH_THRESHOLD then + self:GoToPoint(self.currentDestination) + return + end + + if not lastDestination or (self.currentDestination - lastDestination).magnitude > RECALCULATE_PATH_THRESHOLD then + local newPath = self:TryComputePath(currentPosition, self.currentDestination) + if newPath then + self:StartFollowingPath(newPath) + if PathDisplay then + PathDisplay.setCurrentPoints(self.currentPoints) + PathDisplay.renderPath() + end + else + self:StopFollowingPath() + if PathDisplay then + PathDisplay.clearRenderedPath() + end + end + else + if moving then + self.currentPoints[#currentPoints] = self.currentDestination + else + self:GoToPoint(self.currentDestination) + end + end +end + +function VRNavigation:OnJumpAction(actionName, inputState, inputObj) + if inputState == Enum.UserInputState.Begin then + --MasterControl:DoJump() + self.isJumping = true + end +end +function VRNavigation:BindJumpAction(active) + if active then + if not self.isJumpBound then + self.isJumpBound = true + ContextActionService:BindAction("VRJumpAction", (function() self:OnJumpAction() end), false, Enum.KeyCode.ButtonA) + end + else + if self.isJumpBound then + self.isJumpBound = false + ContextActionService:UnbindAction("VRJumpAction") + end + end +end + +function VRNavigation:ControlCharacterGamepad(actionName, inputState, inputObject) + if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end + + if inputState == Enum.UserInputState.Cancel then + --MasterControl:AddToPlayerMovement(-self.moveVector) + self.moveVector = ZERO_VECTOR3 + return + end + + if inputState ~= Enum.UserInputState.End then + self:StopFollowingPath() + if PathDisplay then + PathDisplay.clearRenderedPath() + end + + if self:ShouldUseNavigationLaser() then + self:BindJumpAction(true) + self:SetLaserPointerMode("Hidden") + end + + if inputObject.Position.magnitude > THUMBSTICK_DEADZONE then + --MasterControl:AddToPlayerMovement(-self.moveVector) + self.moveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y) + if self.moveVector.magnitude > 0 then + self.moveVector = self.moveVector.unit * math.min(1, inputObject.Position.magnitude) + end + + self.moveLatch = true + end + else + --MasterControl:AddToPlayerMovement(-self.moveVector) + self.moveVector = ZERO_VECTOR3 + + if self:ShouldUseNavigationLaser() then + self:BindJumpAction(false) + self:SetLaserPointerMode("Navigation") + end + + if self.moveLatch then + self.moveLatch = false + movementUpdateEvent:Fire("offtrack") + end + end +end + +function VRNavigation:OnHeartbeat(dt) + local newMoveVector = self.moveVector + local humanoid = self:GetLocalHumanoid() + if not humanoid or not humanoid.Torso then + return + end + + if self.moving and self.currentPoints then + local currentPosition = humanoid.Torso.Position + local goalPosition = currentPoints[1] + local vectorToGoal = (goalPosition - currentPosition) * XZ_VECTOR3 + local moveDist = vectorToGoal.magnitude + local moveDir = vectorToGoal / moveDist + + if moveDist < POINT_REACHED_THRESHOLD then + local estimatedTimeRemaining = 0 + local prevPoint = currentPoints[1] + for i, point in pairs(currentPoints) do + if i ~= 1 then + local dist = (point - prevPoint).magnitude + prevPoint = point + estimatedTimeRemaining = estimatedTimeRemaining + (dist / humanoid.WalkSpeed) + end + end + + table.remove(currentPoints, 1) + currentPointIdx = currentPointIdx + 1 + + if #currentPoints == 0 then + self:StopFollowingPath() + if PathDisplay then + PathDisplay.clearRenderedPath() + end + return + else + if PathDisplay then + PathDisplay.setCurrentPoints(currentPoints) + PathDisplay.renderPath() + end + + local newGoal = currentPoints[1] + local distanceToGoal = (newGoal - currentPosition).magnitude + expectedTimeToNextPoint = distanceToGoal / humanoid.WalkSpeed + timeReachedLastPoint = tick() + end + else + local ignoreTable = { + game.Players.LocalPlayer.Character, + workspace.CurrentCamera + } + local obstructRay = Ray.new(currentPosition - Vector3.new(0, 1, 0), moveDir * 3) + local obstructPart, obstructPoint, obstructNormal = workspace:FindPartOnRayWithIgnoreList(obstructRay, ignoreTable) + + if obstructPart then + local heightOffset = Vector3.new(0, 100, 0) + local jumpCheckRay = Ray.new(obstructPoint + moveDir * 0.5 + heightOffset, -heightOffset) + local jumpCheckPart, jumpCheckPoint, jumpCheckNormal = workspace:FindPartOnRayWithIgnoreList(jumpCheckRay, ignoreTable) + + local heightDifference = jumpCheckPoint.Y - currentPosition.Y + if heightDifference < 6 and heightDifference > -2 then + humanoid.Jump = true + end + end + + local timeSinceLastPoint = tick() - timeReachedLastPoint + if timeSinceLastPoint > expectedTimeToNextPoint + OFFTRACK_TIME_THRESHOLD then + self:StopFollowingPath() + if PathDisplay then + PathDisplay.clearRenderedPath() + end + + movementUpdateEvent:Fire("offtrack") + end + + newMoveVector = self.moveVector:Lerp(moveDir, dt * 10) + end + end + + if IsFiniteVector3(newMoveVector) then + --MasterControl:AddToPlayerMovement(newMoveVector - self.moveVector) + self.moveVector = newMoveVector + end +end + + +function VRNavigation:OnUserCFrameEnabled() + if self:ShouldUseNavigationLaser() then + self:BindJumpAction(false) + self:SetLaserPointerMode("Navigation") + else + self:BindJumpAction(true) + self:SetLaserPointerMode("Hidden") + end +end + +function VRNavigation:Enable(enable) + if enable then + self.navigationRequestedConn = VRService.NavigationRequested:Connect(function(destinationCFrame, inputUserCFrame) self:OnNavigationRequest(destinationCFrame, inputUserCFrame) end) + self.heartbeatConn = RunService.Heartbeat:Connect(function(dt) self:OnHeartbeat(dt) end) + + ContextActionService:BindAction("MoveThumbstick", (function(actionName, inputState, inputObject) self:ControlCharacterGamepad(actionName, inputState, inputObject) end), false, Enum.KeyCode.Thumbstick1) + ContextActionService:BindActivate(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2) + + self.userCFrameEnabledConn = VRService.UserCFrameEnabled:Connect(function() self:OnUserCFrameEnabled() end) + self:OnUserCFrameEnabled() + + pcall(function() + VRService:SetTouchpadMode(Enum.VRTouchpad.Left, Enum.VRTouchpadMode.VirtualThumbstick) + VRService:SetTouchpadMode(Enum.VRTouchpad.Right, Enum.VRTouchpadMode.ABXY) + end) + self.enabled = true + else + -- Disable + self:StopFollowingPath() + + ContextActionService:UnbindAction("MoveThumbstick") + ContextActionService:UnbindActivate(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2) + + self:BindJumpAction(false) + self:SetLaserPointerMode("Disabled") + + if self.navigationRequestedConn then + self.navigationRequestedConn:Disconnect() + self.navigationRequestedConn = nil + end + if self.heartbeatConn then + self.heartbeatConn:Disconnect() + self.heartbeatConn = nil + end + if self.userCFrameEnabledConn then + self.userCFrameEnabledConn:Disconnect() + self.userCFrameEnabledConn = nil + end + self.enabled = false + end +end + +return VRNavigation diff --git a/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/VehicleController.lua b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/VehicleController.lua new file mode 100644 index 0000000..bd6188d --- /dev/null +++ b/Client2018/content/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript/VehicleController.lua @@ -0,0 +1,104 @@ +--[[ + // FileName: VehicleControl + // Version 1.0 + // Written by: jmargh + // Description: Implements in-game vehicle controls for all input devices + + // NOTE: This works for basic vehicles (single vehicle seat). If you use custom VehicleSeat code, + // multiple VehicleSeats or your own implementation of a VehicleSeat this will not work. +--]] +local ContextActionService = game:GetService("ContextActionService") +local Players = game:GetService("Players") +local RunService = game:GetService("RunService") + +--[[ Constants ]]-- +-- Set this to true if you want to instead use the triggers for the throttle +local useTriggersForThrottle = true +-- Also set this to true if you want the thumbstick to not affect throttle, only triggers when a gamepad is conected +local onlyTriggersForThrottle = false +local ZERO_VECTOR3 = Vector3.new(0,0,0) + +-- Note that VehicleController does not derive from BaseCharacterController, it is a special case +local VehicleController = {} +VehicleController.__index = VehicleController + +function VehicleController.new() + local self = setmetatable({}, VehicleController) + self.enabled = false + self.vehicleSeat = nil + self.throttle = 0 + self.steer = 0 + + self.acceleration = 0 + self.decceleration = 0 + self.turningRight = 0 + self.turningLeft = 0 + + self.vehicleMoveVector = ZERO_VECTOR3 + + return self +end + +function VehicleController:Enable(enable, vehicleSeat) + if enable == self.enabled and vehicleSeat == self.vehicleSeat then + return + end + + if enable then + if vehicleSeat then + self.vehicleSeat = vehicleSeat + if useTriggersForThrottle then + ContextActionService:BindAction("throttleAccel", (function() self:OnThrottleAccel() end), false, Enum.KeyCode.ButtonR2) + ContextActionService:BindAction("throttleDeccel", (function() self:OnThrottleDeccel() end), false, Enum.KeyCode.ButtonL2) + end + ContextActionService:BindAction("arrowSteerRight", (function() self:OnSteerRight() end), false, Enum.KeyCode.Right) + ContextActionService:BindAction("arrowSteerLeft", (function() self:OnSteerLeft() end), false, Enum.KeyCode.Left) + end + else + if useTriggersForThrottle then + ContextActionService:UnbindAction("throttleAccel") + ContextActionService:UnbindAction("throttleDeccel") + end + ContextActionService:UnbindAction("arrowSteerRight") + ContextActionService:UnbindAction("arrowSteerLeft") + self.vehicleSeat = nil + end +end + +function VehicleController:OnThrottleAccel(actionName, inputState, inputObject) + self.acceleration = (inputState ~= Enum.UserInputState.End) and -1 or 0 + self.throttle = self.acceleration + self.decceleration +end + +function VehicleController:OnThrottleDeccel(actionName, inputState, inputObject) + self.decceleration = (inputState ~= Enum.UserInputState.End) and 1 or 0 + self.throttle = self.acceleration + self.decceleration +end + +function VehicleController:OnSteerRight(actionName, inputState, inputObject) + self.turningRight = (inputState ~= Enum.UserInputState.End) and 1 or 0 + self.steer = self.turningRight + self.turningLeft +end + +function VehicleController:OnSteerLeft(actionName, inputState, inputObject) + self.turningLeft = (inputState ~= Enum.UserInputState.End) and -1 or 0 + self.steer = self.turningRight + self.turningLeft +end + +-- Call this from a function bound to Renderstep with Input Priority +function VehicleController:Update(moveVector, usingGamepad) + if self.vehicleSeat then + moveVector = moveVector + Vector3.new(self.steer, 0, self.throttle) + if usingGamepad and onlyTriggersForThrottle and useTriggersForThrottle then + self.vehicleSeat.ThrottleFloat = -self.throttle + else + self.vehicleSeat.ThrottleFloat = -moveVector.Z + end + self.vehicleSeat.SteerFloat = moveVector.X + + return moveVector, true + end + return moveVector, false +end + +return VehicleController \ No newline at end of file diff --git a/Client2018/content/sky/moon.jpg b/Client2018/content/sky/moon.jpg new file mode 100644 index 0000000..247b6cd Binary files /dev/null and b/Client2018/content/sky/moon.jpg differ diff --git a/Client2018/content/sky/sun.jpg b/Client2018/content/sky/sun.jpg new file mode 100644 index 0000000..12b3829 Binary files /dev/null and b/Client2018/content/sky/sun.jpg differ diff --git a/Client2018/content/sounds/action_falling.mp3 b/Client2018/content/sounds/action_falling.mp3 new file mode 100644 index 0000000..6408717 Binary files /dev/null and b/Client2018/content/sounds/action_falling.mp3 differ diff --git a/Client2018/content/sounds/action_footsteps_plastic.mp3 b/Client2018/content/sounds/action_footsteps_plastic.mp3 new file mode 100644 index 0000000..6804420 Binary files /dev/null and b/Client2018/content/sounds/action_footsteps_plastic.mp3 differ diff --git a/Client2018/content/sounds/action_get_up.mp3 b/Client2018/content/sounds/action_get_up.mp3 new file mode 100644 index 0000000..ababd77 Binary files /dev/null and b/Client2018/content/sounds/action_get_up.mp3 differ diff --git a/Client2018/content/sounds/action_jump.mp3 b/Client2018/content/sounds/action_jump.mp3 new file mode 100644 index 0000000..7159074 Binary files /dev/null and b/Client2018/content/sounds/action_jump.mp3 differ diff --git a/Client2018/content/sounds/action_jump_land.mp3 b/Client2018/content/sounds/action_jump_land.mp3 new file mode 100644 index 0000000..3d68ce1 Binary files /dev/null and b/Client2018/content/sounds/action_jump_land.mp3 differ diff --git a/Client2018/content/sounds/action_swim.mp3 b/Client2018/content/sounds/action_swim.mp3 new file mode 100644 index 0000000..95dd2d5 Binary files /dev/null and b/Client2018/content/sounds/action_swim.mp3 differ diff --git a/Client2018/content/sounds/bass.wav b/Client2018/content/sounds/bass.wav new file mode 100644 index 0000000..5f4b791 Binary files /dev/null and b/Client2018/content/sounds/bass.wav differ diff --git a/Client2018/content/sounds/impact_explosion_03.mp3 b/Client2018/content/sounds/impact_explosion_03.mp3 new file mode 100644 index 0000000..4501c83 Binary files /dev/null and b/Client2018/content/sounds/impact_explosion_03.mp3 differ diff --git a/Client2018/content/sounds/impact_water.mp3 b/Client2018/content/sounds/impact_water.mp3 new file mode 100644 index 0000000..741862b Binary files /dev/null and b/Client2018/content/sounds/impact_water.mp3 differ diff --git a/Client2018/content/sounds/snap.mp3 b/Client2018/content/sounds/snap.mp3 new file mode 100644 index 0000000..8dd8d5a Binary files /dev/null and b/Client2018/content/sounds/snap.mp3 differ diff --git a/Client2018/content/sounds/uuhhh.mp3 b/Client2018/content/sounds/uuhhh.mp3 new file mode 100644 index 0000000..81b3565 Binary files /dev/null and b/Client2018/content/sounds/uuhhh.mp3 differ diff --git a/Client2018/content/textures/AnchorCursor.png b/Client2018/content/textures/AnchorCursor.png new file mode 100644 index 0000000..cb00a6a Binary files /dev/null and b/Client2018/content/textures/AnchorCursor.png differ diff --git a/Client2018/content/textures/AnimationEditor/Checkmark.png b/Client2018/content/textures/AnimationEditor/Checkmark.png new file mode 100644 index 0000000..2d7a684 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/Checkmark.png differ diff --git a/Client2018/content/textures/AnimationEditor/Close.png b/Client2018/content/textures/AnimationEditor/Close.png new file mode 100644 index 0000000..2281cae Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/Close.png differ diff --git a/Client2018/content/textures/AnimationEditor/RoundedBackground.png b/Client2018/content/textures/AnimationEditor/RoundedBackground.png new file mode 100644 index 0000000..602e90f Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/RoundedBackground.png differ diff --git a/Client2018/content/textures/AnimationEditor/RoundedBorder.png b/Client2018/content/textures/AnimationEditor/RoundedBorder.png new file mode 100644 index 0000000..8335459 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/RoundedBorder.png differ diff --git a/Client2018/content/textures/AnimationEditor/ScrollbarBottom.png b/Client2018/content/textures/AnimationEditor/ScrollbarBottom.png new file mode 100644 index 0000000..f84d082 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/ScrollbarBottom.png differ diff --git a/Client2018/content/textures/AnimationEditor/ScrollbarMiddle.png b/Client2018/content/textures/AnimationEditor/ScrollbarMiddle.png new file mode 100644 index 0000000..0e90165 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/ScrollbarMiddle.png differ diff --git a/Client2018/content/textures/AnimationEditor/ScrollbarTop.png b/Client2018/content/textures/AnimationEditor/ScrollbarTop.png new file mode 100644 index 0000000..beb8d14 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/ScrollbarTop.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_control_end.png b/Client2018/content/textures/AnimationEditor/button_control_end.png new file mode 100644 index 0000000..9493b62 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_control_end.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_control_next.png b/Client2018/content/textures/AnimationEditor/button_control_next.png new file mode 100644 index 0000000..e498efc Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_control_next.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_control_play.png b/Client2018/content/textures/AnimationEditor/button_control_play.png new file mode 100644 index 0000000..7e13020 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_control_play.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_control_previous.png b/Client2018/content/textures/AnimationEditor/button_control_previous.png new file mode 100644 index 0000000..95ad3cf Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_control_previous.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_control_start.png b/Client2018/content/textures/AnimationEditor/button_control_start.png new file mode 100644 index 0000000..2a13172 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_control_start.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_hierarchy_closed.png b/Client2018/content/textures/AnimationEditor/button_hierarchy_closed.png new file mode 100644 index 0000000..2a70e31 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_hierarchy_closed.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_hierarchy_opened.png b/Client2018/content/textures/AnimationEditor/button_hierarchy_opened.png new file mode 100644 index 0000000..c2d1e6d Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_hierarchy_opened.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_lock.png b/Client2018/content/textures/AnimationEditor/button_lock.png new file mode 100644 index 0000000..cf9ca95 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_lock.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_loop.png b/Client2018/content/textures/AnimationEditor/button_loop.png new file mode 100644 index 0000000..e585d2f Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_loop.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_pause_white@2x.png b/Client2018/content/textures/AnimationEditor/button_pause_white@2x.png new file mode 100644 index 0000000..7215f9b Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_pause_white@2x.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_popup_close.png b/Client2018/content/textures/AnimationEditor/button_popup_close.png new file mode 100644 index 0000000..3511f7c Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_popup_close.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_radio_background.png b/Client2018/content/textures/AnimationEditor/button_radio_background.png new file mode 100644 index 0000000..1f15a29 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_radio_background.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_radio_default.png b/Client2018/content/textures/AnimationEditor/button_radio_default.png new file mode 100644 index 0000000..5c54fd1 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_radio_default.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_radio_innercircle.png b/Client2018/content/textures/AnimationEditor/button_radio_innercircle.png new file mode 100644 index 0000000..b6a9efe Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_radio_innercircle.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_zoom.png b/Client2018/content/textures/AnimationEditor/button_zoom.png new file mode 100644 index 0000000..966d410 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_zoom.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_zoom_default_left.png b/Client2018/content/textures/AnimationEditor/button_zoom_default_left.png new file mode 100644 index 0000000..e371819 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_zoom_default_left.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_zoom_default_left@2x.png b/Client2018/content/textures/AnimationEditor/button_zoom_default_left@2x.png new file mode 100644 index 0000000..45c7cc7 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_zoom_default_left@2x.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_zoom_default_right.png b/Client2018/content/textures/AnimationEditor/button_zoom_default_right.png new file mode 100644 index 0000000..e371819 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_zoom_default_right.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_zoom_default_right@2x.png b/Client2018/content/textures/AnimationEditor/button_zoom_default_right@2x.png new file mode 100644 index 0000000..45c7cc7 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_zoom_default_right@2x.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_zoom_hoverpressed_left.png b/Client2018/content/textures/AnimationEditor/button_zoom_hoverpressed_left.png new file mode 100644 index 0000000..4d19d86 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_zoom_hoverpressed_left.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_zoom_hoverpressed_left@2x.png b/Client2018/content/textures/AnimationEditor/button_zoom_hoverpressed_left@2x.png new file mode 100644 index 0000000..3b2f615 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_zoom_hoverpressed_left@2x.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_zoom_hoverpressed_right.png b/Client2018/content/textures/AnimationEditor/button_zoom_hoverpressed_right.png new file mode 100644 index 0000000..4d19d86 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_zoom_hoverpressed_right.png differ diff --git a/Client2018/content/textures/AnimationEditor/button_zoom_hoverpressed_right@2x.png b/Client2018/content/textures/AnimationEditor/button_zoom_hoverpressed_right@2x.png new file mode 100644 index 0000000..3b2f615 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/button_zoom_hoverpressed_right@2x.png differ diff --git a/Client2018/content/textures/AnimationEditor/ic-checkbox-active.png b/Client2018/content/textures/AnimationEditor/ic-checkbox-active.png new file mode 100644 index 0000000..b897b38 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/ic-checkbox-active.png differ diff --git a/Client2018/content/textures/AnimationEditor/ic-checkbox-off.png b/Client2018/content/textures/AnimationEditor/ic-checkbox-off.png new file mode 100644 index 0000000..09fd802 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/ic-checkbox-off.png differ diff --git a/Client2018/content/textures/AnimationEditor/icon_checkmark.png b/Client2018/content/textures/AnimationEditor/icon_checkmark.png new file mode 100644 index 0000000..a61f1f2 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/icon_checkmark.png differ diff --git a/Client2018/content/textures/AnimationEditor/icon_close.png b/Client2018/content/textures/AnimationEditor/icon_close.png new file mode 100644 index 0000000..04e03cf Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/icon_close.png differ diff --git a/Client2018/content/textures/AnimationEditor/icon_dark_warning.png b/Client2018/content/textures/AnimationEditor/icon_dark_warning.png new file mode 100644 index 0000000..c8385d3 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/icon_dark_warning.png differ diff --git a/Client2018/content/textures/AnimationEditor/icon_delete.png b/Client2018/content/textures/AnimationEditor/icon_delete.png new file mode 100644 index 0000000..f643c9b Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/icon_delete.png differ diff --git a/Client2018/content/textures/AnimationEditor/icon_delete_disabled.png b/Client2018/content/textures/AnimationEditor/icon_delete_disabled.png new file mode 100644 index 0000000..8eac738 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/icon_delete_disabled.png differ diff --git a/Client2018/content/textures/AnimationEditor/icon_error.png b/Client2018/content/textures/AnimationEditor/icon_error.png new file mode 100644 index 0000000..fe8e0de Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/icon_error.png differ diff --git a/Client2018/content/textures/AnimationEditor/icon_hierarchy_end_white.png b/Client2018/content/textures/AnimationEditor/icon_hierarchy_end_white.png new file mode 100644 index 0000000..4b9fc99 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/icon_hierarchy_end_white.png differ diff --git a/Client2018/content/textures/AnimationEditor/icon_keyIndicator.png b/Client2018/content/textures/AnimationEditor/icon_keyIndicator.png new file mode 100644 index 0000000..9d92ac4 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/icon_keyIndicator.png differ diff --git a/Client2018/content/textures/AnimationEditor/icon_keyIndicator_selected.png b/Client2018/content/textures/AnimationEditor/icon_keyIndicator_selected.png new file mode 100644 index 0000000..ed16d3c Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/icon_keyIndicator_selected.png differ diff --git a/Client2018/content/textures/AnimationEditor/icon_pin.png b/Client2018/content/textures/AnimationEditor/icon_pin.png new file mode 100644 index 0000000..523ed1e Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/icon_pin.png differ diff --git a/Client2018/content/textures/AnimationEditor/icon_warning.png b/Client2018/content/textures/AnimationEditor/icon_warning.png new file mode 100644 index 0000000..a563e29 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/icon_warning.png differ diff --git a/Client2018/content/textures/AnimationEditor/icon_warning_ik.png b/Client2018/content/textures/AnimationEditor/icon_warning_ik.png new file mode 100644 index 0000000..67c98b2 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/icon_warning_ik.png differ diff --git a/Client2018/content/textures/AnimationEditor/icon_whitetriangle_down.png b/Client2018/content/textures/AnimationEditor/icon_whitetriangle_down.png new file mode 100644 index 0000000..09c5db8 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/icon_whitetriangle_down.png differ diff --git a/Client2018/content/textures/AnimationEditor/icon_whitetriangle_up.png b/Client2018/content/textures/AnimationEditor/icon_whitetriangle_up.png new file mode 100644 index 0000000..5bdc664 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/icon_whitetriangle_up.png differ diff --git a/Client2018/content/textures/AnimationEditor/image_keyframe_bounce_selected.png b/Client2018/content/textures/AnimationEditor/image_keyframe_bounce_selected.png new file mode 100644 index 0000000..6e8863f Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/image_keyframe_bounce_selected.png differ diff --git a/Client2018/content/textures/AnimationEditor/image_keyframe_bounce_unselected.png b/Client2018/content/textures/AnimationEditor/image_keyframe_bounce_unselected.png new file mode 100644 index 0000000..391c175 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/image_keyframe_bounce_unselected.png differ diff --git a/Client2018/content/textures/AnimationEditor/image_keyframe_constant_selected.png b/Client2018/content/textures/AnimationEditor/image_keyframe_constant_selected.png new file mode 100644 index 0000000..5d68420 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/image_keyframe_constant_selected.png differ diff --git a/Client2018/content/textures/AnimationEditor/image_keyframe_constant_unselected.png b/Client2018/content/textures/AnimationEditor/image_keyframe_constant_unselected.png new file mode 100644 index 0000000..1c976e6 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/image_keyframe_constant_unselected.png differ diff --git a/Client2018/content/textures/AnimationEditor/image_keyframe_cubic_selected.png b/Client2018/content/textures/AnimationEditor/image_keyframe_cubic_selected.png new file mode 100644 index 0000000..67a63a0 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/image_keyframe_cubic_selected.png differ diff --git a/Client2018/content/textures/AnimationEditor/image_keyframe_cubic_unselected.png b/Client2018/content/textures/AnimationEditor/image_keyframe_cubic_unselected.png new file mode 100644 index 0000000..2a9d7db Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/image_keyframe_cubic_unselected.png differ diff --git a/Client2018/content/textures/AnimationEditor/image_keyframe_elastic_selected.png b/Client2018/content/textures/AnimationEditor/image_keyframe_elastic_selected.png new file mode 100644 index 0000000..7216182 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/image_keyframe_elastic_selected.png differ diff --git a/Client2018/content/textures/AnimationEditor/image_keyframe_elastic_unselected.png b/Client2018/content/textures/AnimationEditor/image_keyframe_elastic_unselected.png new file mode 100644 index 0000000..e56ad88 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/image_keyframe_elastic_unselected.png differ diff --git a/Client2018/content/textures/AnimationEditor/image_keyframe_linear_selected.png b/Client2018/content/textures/AnimationEditor/image_keyframe_linear_selected.png new file mode 100644 index 0000000..3d4d969 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/image_keyframe_linear_selected.png differ diff --git a/Client2018/content/textures/AnimationEditor/image_keyframe_linear_unselected.png b/Client2018/content/textures/AnimationEditor/image_keyframe_linear_unselected.png new file mode 100644 index 0000000..3f3709c Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/image_keyframe_linear_unselected.png differ diff --git a/Client2018/content/textures/AnimationEditor/image_scrollbar_vertical_bot.png b/Client2018/content/textures/AnimationEditor/image_scrollbar_vertical_bot.png new file mode 100644 index 0000000..b419435 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/image_scrollbar_vertical_bot.png differ diff --git a/Client2018/content/textures/AnimationEditor/image_scrollbar_vertical_mid.png b/Client2018/content/textures/AnimationEditor/image_scrollbar_vertical_mid.png new file mode 100644 index 0000000..56558a3 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/image_scrollbar_vertical_mid.png differ diff --git a/Client2018/content/textures/AnimationEditor/image_scrollbar_vertical_top.png b/Client2018/content/textures/AnimationEditor/image_scrollbar_vertical_top.png new file mode 100644 index 0000000..e0437c5 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/image_scrollbar_vertical_top.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_dark_scalebar_arrows.png b/Client2018/content/textures/AnimationEditor/img_dark_scalebar_arrows.png new file mode 100644 index 0000000..a5b5527 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_dark_scalebar_arrows.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_dark_scalebar_bar.png b/Client2018/content/textures/AnimationEditor/img_dark_scalebar_bar.png new file mode 100644 index 0000000..b5627c7 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_dark_scalebar_bar.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_dark_scrubberhead.png b/Client2018/content/textures/AnimationEditor/img_dark_scrubberhead.png new file mode 100644 index 0000000..1d4abcb Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_dark_scrubberhead.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_dark_timetag_bg.png b/Client2018/content/textures/AnimationEditor/img_dark_timetag_bg.png new file mode 100644 index 0000000..f4b0ecb Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_dark_timetag_bg.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_forwardslash.png b/Client2018/content/textures/AnimationEditor/img_forwardslash.png new file mode 100644 index 0000000..ebe9092 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_forwardslash.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_key_border.png b/Client2018/content/textures/AnimationEditor/img_key_border.png new file mode 100644 index 0000000..bac1fe3 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_key_border.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_key_indicator_border.png b/Client2018/content/textures/AnimationEditor/img_key_indicator_border.png new file mode 100644 index 0000000..bb35719 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_key_indicator_border.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_key_indicator_inner.png b/Client2018/content/textures/AnimationEditor/img_key_indicator_inner.png new file mode 100644 index 0000000..10b29f5 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_key_indicator_inner.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_key_indicator_selected_border.png b/Client2018/content/textures/AnimationEditor/img_key_indicator_selected_border.png new file mode 100644 index 0000000..309841e Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_key_indicator_selected_border.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_key_indicator_selected_inner.png b/Client2018/content/textures/AnimationEditor/img_key_indicator_selected_inner.png new file mode 100644 index 0000000..10b29f5 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_key_indicator_selected_inner.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_key_inner.png b/Client2018/content/textures/AnimationEditor/img_key_inner.png new file mode 100644 index 0000000..c281f85 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_key_inner.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_key_selected_border.png b/Client2018/content/textures/AnimationEditor/img_key_selected_border.png new file mode 100644 index 0000000..84868ec Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_key_selected_border.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_key_selected_inner.png b/Client2018/content/textures/AnimationEditor/img_key_selected_inner.png new file mode 100644 index 0000000..03fd465 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_key_selected_inner.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_scalebar_arrows.png b/Client2018/content/textures/AnimationEditor/img_scalebar_arrows.png new file mode 100644 index 0000000..92c86ae Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_scalebar_arrows.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_scalebar_arrows_border.png b/Client2018/content/textures/AnimationEditor/img_scalebar_arrows_border.png new file mode 100644 index 0000000..33af34a Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_scalebar_arrows_border.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_scrubberhead.png b/Client2018/content/textures/AnimationEditor/img_scrubberhead.png new file mode 100644 index 0000000..8bed057 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_scrubberhead.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_timetag.png b/Client2018/content/textures/AnimationEditor/img_timetag.png new file mode 100644 index 0000000..99a0999 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_timetag.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_timetag_border.png b/Client2018/content/textures/AnimationEditor/img_timetag_border.png new file mode 100644 index 0000000..49a59e7 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_timetag_border.png differ diff --git a/Client2018/content/textures/AnimationEditor/img_triangle.png b/Client2018/content/textures/AnimationEditor/img_triangle.png new file mode 100644 index 0000000..1516c70 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/img_triangle.png differ diff --git a/Client2018/content/textures/AnimationEditor/menu_shadow_bottom.png b/Client2018/content/textures/AnimationEditor/menu_shadow_bottom.png new file mode 100644 index 0000000..64c5ca8 Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/menu_shadow_bottom.png differ diff --git a/Client2018/content/textures/AnimationEditor/menu_shadow_side_left.png b/Client2018/content/textures/AnimationEditor/menu_shadow_side_left.png new file mode 100644 index 0000000..3e1e14b Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/menu_shadow_side_left.png differ diff --git a/Client2018/content/textures/AnimationEditor/menu_shadow_side_right.png b/Client2018/content/textures/AnimationEditor/menu_shadow_side_right.png new file mode 100644 index 0000000..dc3a41f Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/menu_shadow_side_right.png differ diff --git a/Client2018/content/textures/AnimationEditor/menu_shadow_top.png b/Client2018/content/textures/AnimationEditor/menu_shadow_top.png new file mode 100644 index 0000000..47003ca Binary files /dev/null and b/Client2018/content/textures/AnimationEditor/menu_shadow_top.png differ diff --git a/Client2018/content/textures/ArrowCursor.png b/Client2018/content/textures/ArrowCursor.png new file mode 100644 index 0000000..694c26a Binary files /dev/null and b/Client2018/content/textures/ArrowCursor.png differ diff --git a/Client2018/content/textures/ArrowCursorDecalDrag.png b/Client2018/content/textures/ArrowCursorDecalDrag.png new file mode 100644 index 0000000..694c26a Binary files /dev/null and b/Client2018/content/textures/ArrowCursorDecalDrag.png differ diff --git a/Client2018/content/textures/ArrowFarCursor.png b/Client2018/content/textures/ArrowFarCursor.png new file mode 100644 index 0000000..daf5471 Binary files /dev/null and b/Client2018/content/textures/ArrowFarCursor.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Landscape/gr-color-block-mask-tablet.png b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-color-block-mask-tablet.png new file mode 100644 index 0000000..976bc95 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-color-block-mask-tablet.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Landscape/gr-color-block-mask-tablet@2x.png b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-color-block-mask-tablet@2x.png new file mode 100644 index 0000000..94e99b2 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-color-block-mask-tablet@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Landscape/gr-color-block-mask-tablet@3x.png b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-color-block-mask-tablet@3x.png new file mode 100644 index 0000000..7200e5d Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-color-block-mask-tablet@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Landscape/gr-primary-nav-tablet.png b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-primary-nav-tablet.png new file mode 100644 index 0000000..aa973ad Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-primary-nav-tablet.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Landscape/gr-primary-nav-tablet@2x.png b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-primary-nav-tablet@2x.png new file mode 100644 index 0000000..f443f8f Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-primary-nav-tablet@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Landscape/gr-primary-nav-tablet@3x.png b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-primary-nav-tablet@3x.png new file mode 100644 index 0000000..953f760 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-primary-nav-tablet@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Landscape/gr-selection-corner-tablet.png b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-selection-corner-tablet.png new file mode 100644 index 0000000..9791c4e Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-selection-corner-tablet.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Landscape/gr-selection-corner-tablet@2x.png b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-selection-corner-tablet@2x.png new file mode 100644 index 0000000..38911ce Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-selection-corner-tablet@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Landscape/gr-selection-corner-tablet@3x.png b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-selection-corner-tablet@3x.png new file mode 100644 index 0000000..16485f5 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Landscape/gr-selection-corner-tablet@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Portrait/gr-color-block-mask-phone.png b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-color-block-mask-phone.png new file mode 100644 index 0000000..2d581ca Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-color-block-mask-phone.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Portrait/gr-color-block-mask-phone@2x.png b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-color-block-mask-phone@2x.png new file mode 100644 index 0000000..4103540 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-color-block-mask-phone@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Portrait/gr-color-block-mask-phone@3x.png b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-color-block-mask-phone@3x.png new file mode 100644 index 0000000..03c36b0 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-color-block-mask-phone@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-half circle@2x.png b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-half circle@2x.png new file mode 100644 index 0000000..67bfc03 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-half circle@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-half-circle.png b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-half-circle.png new file mode 100644 index 0000000..fc3b6f0 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-half-circle.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-half-circle@3x.png b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-half-circle@3x.png new file mode 100644 index 0000000..12faccc Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-half-circle@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-rectangle.png b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-rectangle.png new file mode 100644 index 0000000..715da91 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-rectangle.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-rectangle@2x.png b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-rectangle@2x.png new file mode 100644 index 0000000..17400eb Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-rectangle@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-rectangle@3x.png b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-rectangle@3x.png new file mode 100644 index 0000000..ef52d0b Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-primary-nav-rectangle@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Portrait/gr-selection-corner-phone.png b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-selection-corner-phone.png new file mode 100644 index 0000000..954df6e Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-selection-corner-phone.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Portrait/gr-selection-corner-phone@2x.png b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-selection-corner-phone@2x.png new file mode 100644 index 0000000..3d7216e Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-selection-corner-phone@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Portrait/gr-selection-corner-phone@3x.png b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-selection-corner-phone@3x.png new file mode 100644 index 0000000..083af8b Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Portrait/gr-selection-corner-phone@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sheet.png b/Client2018/content/textures/AvatarEditorImages/Sheet.png new file mode 100644 index 0000000..efb94f4 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sheet.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-empty.png b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-empty.png new file mode 100644 index 0000000..5043d1d Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-empty.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-empty@2x.png b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-empty@2x.png new file mode 100644 index 0000000..fba16ba Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-empty@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-empty@3x.png b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-empty@3x.png new file mode 100644 index 0000000..29e91f3 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-empty@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-fill.png b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-fill.png new file mode 100644 index 0000000..63ced88 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-fill.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-fill@2x.png b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-fill@2x.png new file mode 100644 index 0000000..3fb53a6 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-fill@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-fill@3x.png b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-fill@3x.png new file mode 100644 index 0000000..5cc69fa Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-default-point-fill@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty.png b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty.png new file mode 100644 index 0000000..cc1a2a5 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@2x.png b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@2x.png new file mode 100644 index 0000000..b9fd04b Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@3x.png b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@3x.png new file mode 100644 index 0000000..7bfe852 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill.png b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill.png new file mode 100644 index 0000000..bc50d10 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@2x.png b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@2x.png new file mode 100644 index 0000000..30c1c22 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@3x.png b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@3x.png new file mode 100644 index 0000000..5f4ab0f Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slider.png b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slider.png new file mode 100644 index 0000000..12022f4 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slider.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slider@2x.png b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slider@2x.png new file mode 100644 index 0000000..1ae093c Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slider@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slider@3x.png b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slider@3x.png new file mode 100644 index 0000000..6d6ef28 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Sliders/gr-slider@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Stretch/bar-empty-mid.png b/Client2018/content/textures/AvatarEditorImages/Stretch/bar-empty-mid.png new file mode 100644 index 0000000..c2c751e Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Stretch/bar-empty-mid.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@2x.png b/Client2018/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@2x.png new file mode 100644 index 0000000..32d55f8 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@3x.png b/Client2018/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@3x.png new file mode 100644 index 0000000..de31bc7 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Stretch/bar-full-mid.png b/Client2018/content/textures/AvatarEditorImages/Stretch/bar-full-mid.png new file mode 100644 index 0000000..5990100 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Stretch/bar-full-mid.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Stretch/bar-full-mid@2x.png b/Client2018/content/textures/AvatarEditorImages/Stretch/bar-full-mid@2x.png new file mode 100644 index 0000000..a4f9403 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Stretch/bar-full-mid@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Stretch/bar-full-mid@3x.png b/Client2018/content/textures/AvatarEditorImages/Stretch/bar-full-mid@3x.png new file mode 100644 index 0000000..eeb9e7e Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Stretch/bar-full-mid@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Stretch/gr-tail.png b/Client2018/content/textures/AvatarEditorImages/Stretch/gr-tail.png new file mode 100644 index 0000000..deb2988 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Stretch/gr-tail.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/Stretch/gr-tail@2x.png b/Client2018/content/textures/AvatarEditorImages/Stretch/gr-tail@2x.png new file mode 100644 index 0000000..1a88af0 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/Stretch/gr-tail@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/btn.png b/Client2018/content/textures/AvatarEditorImages/btn.png new file mode 100644 index 0000000..112e4c3 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/btn.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/btn@2x.png b/Client2018/content/textures/AvatarEditorImages/btn@2x.png new file mode 100644 index 0000000..4e4394d Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/btn@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/btn@3x.png b/Client2018/content/textures/AvatarEditorImages/btn@3x.png new file mode 100644 index 0000000..113a28c Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/btn@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/circle_blue.png b/Client2018/content/textures/AvatarEditorImages/circle_blue.png new file mode 100644 index 0000000..63ced88 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/circle_blue.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/circle_blue@2x.png b/Client2018/content/textures/AvatarEditorImages/circle_blue@2x.png new file mode 100644 index 0000000..3fb53a6 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/circle_blue@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/circle_blue@3x.png b/Client2018/content/textures/AvatarEditorImages/circle_blue@3x.png new file mode 100644 index 0000000..5cc69fa Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/circle_blue@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/circle_gray4.png b/Client2018/content/textures/AvatarEditorImages/circle_gray4.png new file mode 100644 index 0000000..5043d1d Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/circle_gray4.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/circle_gray4@2x.png b/Client2018/content/textures/AvatarEditorImages/circle_gray4@2x.png new file mode 100644 index 0000000..fba16ba Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/circle_gray4@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/circle_gray4@3x.png b/Client2018/content/textures/AvatarEditorImages/circle_gray4@3x.png new file mode 100644 index 0000000..29e91f3 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/circle_gray4@3x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/gr-selection-border.png b/Client2018/content/textures/AvatarEditorImages/gr-selection-border.png new file mode 100644 index 0000000..95d8647 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/gr-selection-border.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/gr-selection-border@2x.png b/Client2018/content/textures/AvatarEditorImages/gr-selection-border@2x.png new file mode 100644 index 0000000..1541ea9 Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/gr-selection-border@2x.png differ diff --git a/Client2018/content/textures/AvatarEditorImages/gr-selection-border@3x.png b/Client2018/content/textures/AvatarEditorImages/gr-selection-border@3x.png new file mode 100644 index 0000000..a7d560e Binary files /dev/null and b/Client2018/content/textures/AvatarEditorImages/gr-selection-border@3x.png differ diff --git a/Client2018/content/textures/Blank.png b/Client2018/content/textures/Blank.png new file mode 100644 index 0000000..3add73f Binary files /dev/null and b/Client2018/content/textures/Blank.png differ diff --git a/Client2018/content/textures/CollisionGroupsEditor/ToolbarIcon.png b/Client2018/content/textures/CollisionGroupsEditor/ToolbarIcon.png new file mode 100644 index 0000000..08542c9 Binary files /dev/null and b/Client2018/content/textures/CollisionGroupsEditor/ToolbarIcon.png differ diff --git a/Client2018/content/textures/CollisionGroupsEditor/assign-hover.png b/Client2018/content/textures/CollisionGroupsEditor/assign-hover.png new file mode 100644 index 0000000..91f7db7 Binary files /dev/null and b/Client2018/content/textures/CollisionGroupsEditor/assign-hover.png differ diff --git a/Client2018/content/textures/CollisionGroupsEditor/assign.png b/Client2018/content/textures/CollisionGroupsEditor/assign.png new file mode 100644 index 0000000..fc915f6 Binary files /dev/null and b/Client2018/content/textures/CollisionGroupsEditor/assign.png differ diff --git a/Client2018/content/textures/CollisionGroupsEditor/checked-bluebg.png b/Client2018/content/textures/CollisionGroupsEditor/checked-bluebg.png new file mode 100644 index 0000000..3dddcfb Binary files /dev/null and b/Client2018/content/textures/CollisionGroupsEditor/checked-bluebg.png differ diff --git a/Client2018/content/textures/CollisionGroupsEditor/checked-whitebg.png b/Client2018/content/textures/CollisionGroupsEditor/checked-whitebg.png new file mode 100644 index 0000000..78bdb73 Binary files /dev/null and b/Client2018/content/textures/CollisionGroupsEditor/checked-whitebg.png differ diff --git a/Client2018/content/textures/CollisionGroupsEditor/delete-hover.png b/Client2018/content/textures/CollisionGroupsEditor/delete-hover.png new file mode 100644 index 0000000..a0b6401 Binary files /dev/null and b/Client2018/content/textures/CollisionGroupsEditor/delete-hover.png differ diff --git a/Client2018/content/textures/CollisionGroupsEditor/delete.png b/Client2018/content/textures/CollisionGroupsEditor/delete.png new file mode 100644 index 0000000..9437121 Binary files /dev/null and b/Client2018/content/textures/CollisionGroupsEditor/delete.png differ diff --git a/Client2018/content/textures/CollisionGroupsEditor/manage-hover.png b/Client2018/content/textures/CollisionGroupsEditor/manage-hover.png new file mode 100644 index 0000000..273f99e Binary files /dev/null and b/Client2018/content/textures/CollisionGroupsEditor/manage-hover.png differ diff --git a/Client2018/content/textures/CollisionGroupsEditor/manage.png b/Client2018/content/textures/CollisionGroupsEditor/manage.png new file mode 100644 index 0000000..81c6517 Binary files /dev/null and b/Client2018/content/textures/CollisionGroupsEditor/manage.png differ diff --git a/Client2018/content/textures/CollisionGroupsEditor/rename-hover.png b/Client2018/content/textures/CollisionGroupsEditor/rename-hover.png new file mode 100644 index 0000000..df47a4b Binary files /dev/null and b/Client2018/content/textures/CollisionGroupsEditor/rename-hover.png differ diff --git a/Client2018/content/textures/CollisionGroupsEditor/rename.png b/Client2018/content/textures/CollisionGroupsEditor/rename.png new file mode 100644 index 0000000..bd26275 Binary files /dev/null and b/Client2018/content/textures/CollisionGroupsEditor/rename.png differ diff --git a/Client2018/content/textures/CollisionGroupsEditor/unchecked.png b/Client2018/content/textures/CollisionGroupsEditor/unchecked.png new file mode 100644 index 0000000..dd8aa02 Binary files /dev/null and b/Client2018/content/textures/CollisionGroupsEditor/unchecked.png differ diff --git a/Client2018/content/textures/ConstraintCursor.png b/Client2018/content/textures/ConstraintCursor.png new file mode 100644 index 0000000..a89d0ad Binary files /dev/null and b/Client2018/content/textures/ConstraintCursor.png differ diff --git a/Client2018/content/textures/Cursors/Gamepad/Pointer.png b/Client2018/content/textures/Cursors/Gamepad/Pointer.png new file mode 100644 index 0000000..5c406dc Binary files /dev/null and b/Client2018/content/textures/Cursors/Gamepad/Pointer.png differ diff --git a/Client2018/content/textures/Cursors/Gamepad/Pointer@2x.png b/Client2018/content/textures/Cursors/Gamepad/Pointer@2x.png new file mode 100644 index 0000000..e896fbb Binary files /dev/null and b/Client2018/content/textures/Cursors/Gamepad/Pointer@2x.png differ diff --git a/Client2018/content/textures/Cursors/Gamepad/PointerOver.png b/Client2018/content/textures/Cursors/Gamepad/PointerOver.png new file mode 100644 index 0000000..7663ea1 Binary files /dev/null and b/Client2018/content/textures/Cursors/Gamepad/PointerOver.png differ diff --git a/Client2018/content/textures/Cursors/Gamepad/PointerOver@2x.png b/Client2018/content/textures/Cursors/Gamepad/PointerOver@2x.png new file mode 100644 index 0000000..e0dbd85 Binary files /dev/null and b/Client2018/content/textures/Cursors/Gamepad/PointerOver@2x.png differ diff --git a/Client2018/content/textures/DevConsole/Arrow.png b/Client2018/content/textures/DevConsole/Arrow.png new file mode 100644 index 0000000..f55bbff Binary files /dev/null and b/Client2018/content/textures/DevConsole/Arrow.png differ diff --git a/Client2018/content/textures/DevConsole/Clear.png b/Client2018/content/textures/DevConsole/Clear.png new file mode 100644 index 0000000..9f1bf3a Binary files /dev/null and b/Client2018/content/textures/DevConsole/Clear.png differ diff --git a/Client2018/content/textures/DevConsole/Close.png b/Client2018/content/textures/DevConsole/Close.png new file mode 100644 index 0000000..31d348a Binary files /dev/null and b/Client2018/content/textures/DevConsole/Close.png differ diff --git a/Client2018/content/textures/DevConsole/Error.png b/Client2018/content/textures/DevConsole/Error.png new file mode 100644 index 0000000..470b6e5 Binary files /dev/null and b/Client2018/content/textures/DevConsole/Error.png differ diff --git a/Client2018/content/textures/DevConsole/Filter-filled.png b/Client2018/content/textures/DevConsole/Filter-filled.png new file mode 100644 index 0000000..1f43780 Binary files /dev/null and b/Client2018/content/textures/DevConsole/Filter-filled.png differ diff --git a/Client2018/content/textures/DevConsole/Filter-stroke.png b/Client2018/content/textures/DevConsole/Filter-stroke.png new file mode 100644 index 0000000..6c00c0e Binary files /dev/null and b/Client2018/content/textures/DevConsole/Filter-stroke.png differ diff --git a/Client2018/content/textures/DevConsole/Info.png b/Client2018/content/textures/DevConsole/Info.png new file mode 100644 index 0000000..d6a13bb Binary files /dev/null and b/Client2018/content/textures/DevConsole/Info.png differ diff --git a/Client2018/content/textures/DevConsole/Maximize.png b/Client2018/content/textures/DevConsole/Maximize.png new file mode 100644 index 0000000..8016be6 Binary files /dev/null and b/Client2018/content/textures/DevConsole/Maximize.png differ diff --git a/Client2018/content/textures/DevConsole/Minimize.png b/Client2018/content/textures/DevConsole/Minimize.png new file mode 100644 index 0000000..0065af8 Binary files /dev/null and b/Client2018/content/textures/DevConsole/Minimize.png differ diff --git a/Client2018/content/textures/DevConsole/Search.png b/Client2018/content/textures/DevConsole/Search.png new file mode 100644 index 0000000..b9c95cb Binary files /dev/null and b/Client2018/content/textures/DevConsole/Search.png differ diff --git a/Client2018/content/textures/DevConsole/Sort.png b/Client2018/content/textures/DevConsole/Sort.png new file mode 100644 index 0000000..152394d Binary files /dev/null and b/Client2018/content/textures/DevConsole/Sort.png differ diff --git a/Client2018/content/textures/DevConsole/Warning.png b/Client2018/content/textures/DevConsole/Warning.png new file mode 100644 index 0000000..d614a50 Binary files /dev/null and b/Client2018/content/textures/DevConsole/Warning.png differ diff --git a/Client2018/content/textures/FillCursor.png b/Client2018/content/textures/FillCursor.png new file mode 100644 index 0000000..752cebc Binary files /dev/null and b/Client2018/content/textures/FillCursor.png differ diff --git a/Client2018/content/textures/FlatCursor.png b/Client2018/content/textures/FlatCursor.png new file mode 100644 index 0000000..7d6e845 Binary files /dev/null and b/Client2018/content/textures/FlatCursor.png differ diff --git a/Client2018/content/textures/GameSettings/ErrorIcon.png b/Client2018/content/textures/GameSettings/ErrorIcon.png new file mode 100644 index 0000000..e15c23e Binary files /dev/null and b/Client2018/content/textures/GameSettings/ErrorIcon.png differ diff --git a/Client2018/content/textures/GameSettings/RadioButton.png b/Client2018/content/textures/GameSettings/RadioButton.png new file mode 100644 index 0000000..533340c Binary files /dev/null and b/Client2018/content/textures/GameSettings/RadioButton.png differ diff --git a/Client2018/content/textures/GameSettings/ToolbarIcon.png b/Client2018/content/textures/GameSettings/ToolbarIcon.png new file mode 100644 index 0000000..be1bedf Binary files /dev/null and b/Client2018/content/textures/GameSettings/ToolbarIcon.png differ diff --git a/Client2018/content/textures/GameSettings/Warning.png b/Client2018/content/textures/GameSettings/Warning.png new file mode 100644 index 0000000..6e5f9e6 Binary files /dev/null and b/Client2018/content/textures/GameSettings/Warning.png differ diff --git a/Client2018/content/textures/GlueCursor.png b/Client2018/content/textures/GlueCursor.png new file mode 100644 index 0000000..fdba03c Binary files /dev/null and b/Client2018/content/textures/GlueCursor.png differ diff --git a/Client2018/content/textures/HingeCursor.png b/Client2018/content/textures/HingeCursor.png new file mode 100644 index 0000000..99700b1 Binary files /dev/null and b/Client2018/content/textures/HingeCursor.png differ diff --git a/Client2018/content/textures/Icon_Stream_Off.png b/Client2018/content/textures/Icon_Stream_Off.png new file mode 100644 index 0000000..bbda9be Binary files /dev/null and b/Client2018/content/textures/Icon_Stream_Off.png differ diff --git a/Client2018/content/textures/Icon_Stream_Off@2x.png b/Client2018/content/textures/Icon_Stream_Off@2x.png new file mode 100644 index 0000000..979ef9c Binary files /dev/null and b/Client2018/content/textures/Icon_Stream_Off@2x.png differ diff --git a/Client2018/content/textures/Icon_Stream_Off@3x.png b/Client2018/content/textures/Icon_Stream_Off@3x.png new file mode 100644 index 0000000..e365e43 Binary files /dev/null and b/Client2018/content/textures/Icon_Stream_Off@3x.png differ diff --git a/Client2018/content/textures/LockCursor.png b/Client2018/content/textures/LockCursor.png new file mode 100644 index 0000000..4520327 Binary files /dev/null and b/Client2018/content/textures/LockCursor.png differ diff --git a/Client2018/content/textures/MaterialCursor.png b/Client2018/content/textures/MaterialCursor.png new file mode 100644 index 0000000..f43fa99 Binary files /dev/null and b/Client2018/content/textures/MaterialCursor.png differ diff --git a/Client2018/content/textures/MorpherEditor/mainButtonIcon.png b/Client2018/content/textures/MorpherEditor/mainButtonIcon.png new file mode 100644 index 0000000..3b2f615 Binary files /dev/null and b/Client2018/content/textures/MorpherEditor/mainButtonIcon.png differ diff --git a/Client2018/content/textures/MotorCursor.png b/Client2018/content/textures/MotorCursor.png new file mode 100644 index 0000000..64efe3e Binary files /dev/null and b/Client2018/content/textures/MotorCursor.png differ diff --git a/Client2018/content/textures/MouseLockedCursor.png b/Client2018/content/textures/MouseLockedCursor.png new file mode 100644 index 0000000..2ab5527 Binary files /dev/null and b/Client2018/content/textures/MouseLockedCursor.png differ diff --git a/Client2018/content/textures/RoactStudioWidgets/button_default.png b/Client2018/content/textures/RoactStudioWidgets/button_default.png new file mode 100644 index 0000000..28eb7a7 Binary files /dev/null and b/Client2018/content/textures/RoactStudioWidgets/button_default.png differ diff --git a/Client2018/content/textures/RoactStudioWidgets/button_hover.png b/Client2018/content/textures/RoactStudioWidgets/button_hover.png new file mode 100644 index 0000000..5184f6d Binary files /dev/null and b/Client2018/content/textures/RoactStudioWidgets/button_hover.png differ diff --git a/Client2018/content/textures/RoactStudioWidgets/button_pressed.png b/Client2018/content/textures/RoactStudioWidgets/button_pressed.png new file mode 100644 index 0000000..5484114 Binary files /dev/null and b/Client2018/content/textures/RoactStudioWidgets/button_pressed.png differ diff --git a/Client2018/content/textures/RoactStudioWidgets/button_radiobutton_chosen.png b/Client2018/content/textures/RoactStudioWidgets/button_radiobutton_chosen.png new file mode 100644 index 0000000..2b1bbec Binary files /dev/null and b/Client2018/content/textures/RoactStudioWidgets/button_radiobutton_chosen.png differ diff --git a/Client2018/content/textures/RoactStudioWidgets/button_radiobutton_default.png b/Client2018/content/textures/RoactStudioWidgets/button_radiobutton_default.png new file mode 100644 index 0000000..fbb0cff Binary files /dev/null and b/Client2018/content/textures/RoactStudioWidgets/button_radiobutton_default.png differ diff --git a/Client2018/content/textures/RoactStudioWidgets/checkbox_square.png b/Client2018/content/textures/RoactStudioWidgets/checkbox_square.png new file mode 100644 index 0000000..af883a9 Binary files /dev/null and b/Client2018/content/textures/RoactStudioWidgets/checkbox_square.png differ diff --git a/Client2018/content/textures/RoactStudioWidgets/icon_tick.png b/Client2018/content/textures/RoactStudioWidgets/icon_tick.png new file mode 100644 index 0000000..997f788 Binary files /dev/null and b/Client2018/content/textures/RoactStudioWidgets/icon_tick.png differ diff --git a/Client2018/content/textures/RoactStudioWidgets/slider_caret.png b/Client2018/content/textures/RoactStudioWidgets/slider_caret.png new file mode 100644 index 0000000..5f5e4b1 Binary files /dev/null and b/Client2018/content/textures/RoactStudioWidgets/slider_caret.png differ diff --git a/Client2018/content/textures/RoactStudioWidgets/slider_caret_disabled.png b/Client2018/content/textures/RoactStudioWidgets/slider_caret_disabled.png new file mode 100644 index 0000000..5f5e4b1 Binary files /dev/null and b/Client2018/content/textures/RoactStudioWidgets/slider_caret_disabled.png differ diff --git a/Client2018/content/textures/StudioToolbox/ArrowDownIconWhite.png b/Client2018/content/textures/StudioToolbox/ArrowDownIconWhite.png new file mode 100644 index 0000000..c5f9c2a Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/ArrowDownIconWhite.png differ diff --git a/Client2018/content/textures/StudioToolbox/AudioPreview/light_pause.png b/Client2018/content/textures/StudioToolbox/AudioPreview/light_pause.png new file mode 100644 index 0000000..907703d Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/AudioPreview/light_pause.png differ diff --git a/Client2018/content/textures/StudioToolbox/AudioPreview/light_pause_hover.png b/Client2018/content/textures/StudioToolbox/AudioPreview/light_pause_hover.png new file mode 100644 index 0000000..1e7caf6 Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/AudioPreview/light_pause_hover.png differ diff --git a/Client2018/content/textures/StudioToolbox/AudioPreview/light_play.png b/Client2018/content/textures/StudioToolbox/AudioPreview/light_play.png new file mode 100644 index 0000000..c1ca46a Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/AudioPreview/light_play.png differ diff --git a/Client2018/content/textures/StudioToolbox/AudioPreview/light_play_hover.png b/Client2018/content/textures/StudioToolbox/AudioPreview/light_play_hover.png new file mode 100644 index 0000000..6b30a21 Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/AudioPreview/light_play_hover.png differ diff --git a/Client2018/content/textures/StudioToolbox/AudioPreview/pause.png b/Client2018/content/textures/StudioToolbox/AudioPreview/pause.png new file mode 100644 index 0000000..3fb55b7 Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/AudioPreview/pause.png differ diff --git a/Client2018/content/textures/StudioToolbox/AudioPreview/pause_hover.png b/Client2018/content/textures/StudioToolbox/AudioPreview/pause_hover.png new file mode 100644 index 0000000..56d26d0 Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/AudioPreview/pause_hover.png differ diff --git a/Client2018/content/textures/StudioToolbox/AudioPreview/play-audio.PNG b/Client2018/content/textures/StudioToolbox/AudioPreview/play-audio.PNG new file mode 100644 index 0000000..a39b548 Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/AudioPreview/play-audio.PNG differ diff --git a/Client2018/content/textures/StudioToolbox/AudioPreview/play.png b/Client2018/content/textures/StudioToolbox/AudioPreview/play.png new file mode 100644 index 0000000..e65db01 Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/AudioPreview/play.png differ diff --git a/Client2018/content/textures/StudioToolbox/AudioPreview/play_hover.png b/Client2018/content/textures/StudioToolbox/AudioPreview/play_hover.png new file mode 100644 index 0000000..561e7dc Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/AudioPreview/play_hover.png differ diff --git a/Client2018/content/textures/StudioToolbox/Clear.png b/Client2018/content/textures/StudioToolbox/Clear.png new file mode 100644 index 0000000..0d00fe3 Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/Clear.png differ diff --git a/Client2018/content/textures/StudioToolbox/ClearHover.png b/Client2018/content/textures/StudioToolbox/ClearHover.png new file mode 100644 index 0000000..fee06ad Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/ClearHover.png differ diff --git a/Client2018/content/textures/StudioToolbox/EndorsedBadge.png b/Client2018/content/textures/StudioToolbox/EndorsedBadge.png new file mode 100644 index 0000000..b3f5fb4 Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/EndorsedBadge.png differ diff --git a/Client2018/content/textures/StudioToolbox/NoBackgroundIcon.png b/Client2018/content/textures/StudioToolbox/NoBackgroundIcon.png new file mode 100644 index 0000000..1c1e179 Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/NoBackgroundIcon.png differ diff --git a/Client2018/content/textures/StudioToolbox/RoundedBackground.png b/Client2018/content/textures/StudioToolbox/RoundedBackground.png new file mode 100644 index 0000000..602e90f Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/RoundedBackground.png differ diff --git a/Client2018/content/textures/StudioToolbox/RoundedBorder.png b/Client2018/content/textures/StudioToolbox/RoundedBorder.png new file mode 100644 index 0000000..8335459 Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/RoundedBorder.png differ diff --git a/Client2018/content/textures/StudioToolbox/ScrollBarBottom.png b/Client2018/content/textures/StudioToolbox/ScrollBarBottom.png new file mode 100644 index 0000000..235055a Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/ScrollBarBottom.png differ diff --git a/Client2018/content/textures/StudioToolbox/ScrollBarMiddle.png b/Client2018/content/textures/StudioToolbox/ScrollBarMiddle.png new file mode 100644 index 0000000..ed7ca4a Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/ScrollBarMiddle.png differ diff --git a/Client2018/content/textures/StudioToolbox/ScrollBarTop.png b/Client2018/content/textures/StudioToolbox/ScrollBarTop.png new file mode 100644 index 0000000..d01c82f Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/ScrollBarTop.png differ diff --git a/Client2018/content/textures/StudioToolbox/Search.png b/Client2018/content/textures/StudioToolbox/Search.png new file mode 100644 index 0000000..f4934f7 Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/Search.png differ diff --git a/Client2018/content/textures/StudioToolbox/ToolboxIcon.png b/Client2018/content/textures/StudioToolbox/ToolboxIcon.png new file mode 100644 index 0000000..2e99ec3 Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/ToolboxIcon.png differ diff --git a/Client2018/content/textures/StudioToolbox/Voting/Thumb.png b/Client2018/content/textures/StudioToolbox/Voting/Thumb.png new file mode 100644 index 0000000..21a239b Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/Voting/Thumb.png differ diff --git a/Client2018/content/textures/StudioToolbox/Voting/thumb-down.png b/Client2018/content/textures/StudioToolbox/Voting/thumb-down.png new file mode 100644 index 0000000..d36de77 Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/Voting/thumb-down.png differ diff --git a/Client2018/content/textures/StudioToolbox/Voting/thumbs-down-filled.png b/Client2018/content/textures/StudioToolbox/Voting/thumbs-down-filled.png new file mode 100644 index 0000000..1976341 Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/Voting/thumbs-down-filled.png differ diff --git a/Client2018/content/textures/StudioToolbox/Voting/thumbs-up-filled.png b/Client2018/content/textures/StudioToolbox/Voting/thumbs-up-filled.png new file mode 100644 index 0000000..f97cbdd Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/Voting/thumbs-up-filled.png differ diff --git a/Client2018/content/textures/StudioToolbox/Voting/thumbup.png b/Client2018/content/textures/StudioToolbox/Voting/thumbup.png new file mode 100644 index 0000000..babd290 Binary files /dev/null and b/Client2018/content/textures/StudioToolbox/Voting/thumbup.png differ diff --git a/Client2018/content/textures/StudioUIEditor/icon_resize1.png b/Client2018/content/textures/StudioUIEditor/icon_resize1.png new file mode 100644 index 0000000..23aef57 Binary files /dev/null and b/Client2018/content/textures/StudioUIEditor/icon_resize1.png differ diff --git a/Client2018/content/textures/StudioUIEditor/icon_resize2.png b/Client2018/content/textures/StudioUIEditor/icon_resize2.png new file mode 100644 index 0000000..b50268f Binary files /dev/null and b/Client2018/content/textures/StudioUIEditor/icon_resize2.png differ diff --git a/Client2018/content/textures/StudioUIEditor/icon_resize3.png b/Client2018/content/textures/StudioUIEditor/icon_resize3.png new file mode 100644 index 0000000..3258512 Binary files /dev/null and b/Client2018/content/textures/StudioUIEditor/icon_resize3.png differ diff --git a/Client2018/content/textures/StudioUIEditor/icon_resize4.png b/Client2018/content/textures/StudioUIEditor/icon_resize4.png new file mode 100644 index 0000000..2ac469f Binary files /dev/null and b/Client2018/content/textures/StudioUIEditor/icon_resize4.png differ diff --git a/Client2018/content/textures/StudioUIEditor/icon_rotate1.png b/Client2018/content/textures/StudioUIEditor/icon_rotate1.png new file mode 100644 index 0000000..849be92 Binary files /dev/null and b/Client2018/content/textures/StudioUIEditor/icon_rotate1.png differ diff --git a/Client2018/content/textures/StudioUIEditor/icon_rotate2.png b/Client2018/content/textures/StudioUIEditor/icon_rotate2.png new file mode 100644 index 0000000..ee35889 Binary files /dev/null and b/Client2018/content/textures/StudioUIEditor/icon_rotate2.png differ diff --git a/Client2018/content/textures/StudioUIEditor/icon_rotate3.png b/Client2018/content/textures/StudioUIEditor/icon_rotate3.png new file mode 100644 index 0000000..a87e5bc Binary files /dev/null and b/Client2018/content/textures/StudioUIEditor/icon_rotate3.png differ diff --git a/Client2018/content/textures/StudioUIEditor/icon_rotate4.png b/Client2018/content/textures/StudioUIEditor/icon_rotate4.png new file mode 100644 index 0000000..e6934f7 Binary files /dev/null and b/Client2018/content/textures/StudioUIEditor/icon_rotate4.png differ diff --git a/Client2018/content/textures/StudioUIEditor/icon_rotate5.png b/Client2018/content/textures/StudioUIEditor/icon_rotate5.png new file mode 100644 index 0000000..2b69bc6 Binary files /dev/null and b/Client2018/content/textures/StudioUIEditor/icon_rotate5.png differ diff --git a/Client2018/content/textures/StudioUIEditor/icon_rotate6.png b/Client2018/content/textures/StudioUIEditor/icon_rotate6.png new file mode 100644 index 0000000..70dedbd Binary files /dev/null and b/Client2018/content/textures/StudioUIEditor/icon_rotate6.png differ diff --git a/Client2018/content/textures/StudioUIEditor/icon_rotate7.png b/Client2018/content/textures/StudioUIEditor/icon_rotate7.png new file mode 100644 index 0000000..048264c Binary files /dev/null and b/Client2018/content/textures/StudioUIEditor/icon_rotate7.png differ diff --git a/Client2018/content/textures/StudioUIEditor/icon_rotate8.png b/Client2018/content/textures/StudioUIEditor/icon_rotate8.png new file mode 100644 index 0000000..33afb5d Binary files /dev/null and b/Client2018/content/textures/StudioUIEditor/icon_rotate8.png differ diff --git a/Client2018/content/textures/StudioUIEditor/resizeHandleDropShadow.png b/Client2018/content/textures/StudioUIEditor/resizeHandleDropShadow.png new file mode 100644 index 0000000..b0aaf29 Binary files /dev/null and b/Client2018/content/textures/StudioUIEditor/resizeHandleDropShadow.png differ diff --git a/Client2018/content/textures/StudioUIEditor/valueBoxRoundedRectangle.png b/Client2018/content/textures/StudioUIEditor/valueBoxRoundedRectangle.png new file mode 100644 index 0000000..602e90f Binary files /dev/null and b/Client2018/content/textures/StudioUIEditor/valueBoxRoundedRectangle.png differ diff --git a/Client2018/content/textures/SurfacesDefault.png b/Client2018/content/textures/SurfacesDefault.png new file mode 100644 index 0000000..be149ad Binary files /dev/null and b/Client2018/content/textures/SurfacesDefault.png differ diff --git a/Client2018/content/textures/TerrainTools/button_arrow.png b/Client2018/content/textures/TerrainTools/button_arrow.png new file mode 100644 index 0000000..31b851c Binary files /dev/null and b/Client2018/content/textures/TerrainTools/button_arrow.png differ diff --git a/Client2018/content/textures/TerrainTools/button_arrow_down.png b/Client2018/content/textures/TerrainTools/button_arrow_down.png new file mode 100644 index 0000000..f3c6616 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/button_arrow_down.png differ diff --git a/Client2018/content/textures/TerrainTools/button_default.png b/Client2018/content/textures/TerrainTools/button_default.png new file mode 100644 index 0000000..28eb7a7 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/button_default.png differ diff --git a/Client2018/content/textures/TerrainTools/button_hover.png b/Client2018/content/textures/TerrainTools/button_hover.png new file mode 100644 index 0000000..5184f6d Binary files /dev/null and b/Client2018/content/textures/TerrainTools/button_hover.png differ diff --git a/Client2018/content/textures/TerrainTools/button_pressed.png b/Client2018/content/textures/TerrainTools/button_pressed.png new file mode 100644 index 0000000..5484114 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/button_pressed.png differ diff --git a/Client2018/content/textures/TerrainTools/checkbox_square.png b/Client2018/content/textures/TerrainTools/checkbox_square.png new file mode 100644 index 0000000..af883a9 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/checkbox_square.png differ diff --git a/Client2018/content/textures/TerrainTools/icon_regions_copy.png b/Client2018/content/textures/TerrainTools/icon_regions_copy.png new file mode 100644 index 0000000..c303407 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/icon_regions_copy.png differ diff --git a/Client2018/content/textures/TerrainTools/icon_regions_delete.png b/Client2018/content/textures/TerrainTools/icon_regions_delete.png new file mode 100644 index 0000000..f198e81 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/icon_regions_delete.png differ diff --git a/Client2018/content/textures/TerrainTools/icon_regions_fill.png b/Client2018/content/textures/TerrainTools/icon_regions_fill.png new file mode 100644 index 0000000..5599b1b Binary files /dev/null and b/Client2018/content/textures/TerrainTools/icon_regions_fill.png differ diff --git a/Client2018/content/textures/TerrainTools/icon_regions_move.png b/Client2018/content/textures/TerrainTools/icon_regions_move.png new file mode 100644 index 0000000..69dac91 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/icon_regions_move.png differ diff --git a/Client2018/content/textures/TerrainTools/icon_regions_paste.png b/Client2018/content/textures/TerrainTools/icon_regions_paste.png new file mode 100644 index 0000000..c10af88 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/icon_regions_paste.png differ diff --git a/Client2018/content/textures/TerrainTools/icon_regions_resize.png b/Client2018/content/textures/TerrainTools/icon_regions_resize.png new file mode 100644 index 0000000..eec2340 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/icon_regions_resize.png differ diff --git a/Client2018/content/textures/TerrainTools/icon_regions_rotate.png b/Client2018/content/textures/TerrainTools/icon_regions_rotate.png new file mode 100644 index 0000000..f9ffa77 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/icon_regions_rotate.png differ diff --git a/Client2018/content/textures/TerrainTools/icon_regions_select.png b/Client2018/content/textures/TerrainTools/icon_regions_select.png new file mode 100644 index 0000000..7b87052 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/icon_regions_select.png differ diff --git a/Client2018/content/textures/TerrainTools/icon_shape_cube.png b/Client2018/content/textures/TerrainTools/icon_shape_cube.png new file mode 100644 index 0000000..30b4f5a Binary files /dev/null and b/Client2018/content/textures/TerrainTools/icon_shape_cube.png differ diff --git a/Client2018/content/textures/TerrainTools/icon_shape_sphere.png b/Client2018/content/textures/TerrainTools/icon_shape_sphere.png new file mode 100644 index 0000000..41458ef Binary files /dev/null and b/Client2018/content/textures/TerrainTools/icon_shape_sphere.png differ diff --git a/Client2018/content/textures/TerrainTools/icon_terrain_big.png b/Client2018/content/textures/TerrainTools/icon_terrain_big.png new file mode 100644 index 0000000..6c27ece Binary files /dev/null and b/Client2018/content/textures/TerrainTools/icon_terrain_big.png differ diff --git a/Client2018/content/textures/TerrainTools/icon_tick.png b/Client2018/content/textures/TerrainTools/icon_tick.png new file mode 100644 index 0000000..997f788 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/icon_tick.png differ diff --git a/Client2018/content/textures/TerrainTools/icon_tick_grey.png b/Client2018/content/textures/TerrainTools/icon_tick_grey.png new file mode 100644 index 0000000..d451338 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/icon_tick_grey.png differ diff --git a/Client2018/content/textures/TerrainTools/mt_add.png b/Client2018/content/textures/TerrainTools/mt_add.png new file mode 100644 index 0000000..f9e0c40 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mt_add.png differ diff --git a/Client2018/content/textures/TerrainTools/mt_erode.png b/Client2018/content/textures/TerrainTools/mt_erode.png new file mode 100644 index 0000000..5ace872 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mt_erode.png differ diff --git a/Client2018/content/textures/TerrainTools/mt_generate.png b/Client2018/content/textures/TerrainTools/mt_generate.png new file mode 100644 index 0000000..dffcf7d Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mt_generate.png differ diff --git a/Client2018/content/textures/TerrainTools/mt_grow.png b/Client2018/content/textures/TerrainTools/mt_grow.png new file mode 100644 index 0000000..76e9333 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mt_grow.png differ diff --git a/Client2018/content/textures/TerrainTools/mt_paint.png b/Client2018/content/textures/TerrainTools/mt_paint.png new file mode 100644 index 0000000..03096f5 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mt_paint.png differ diff --git a/Client2018/content/textures/TerrainTools/mt_regions.png b/Client2018/content/textures/TerrainTools/mt_regions.png new file mode 100644 index 0000000..f9aaae8 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mt_regions.png differ diff --git a/Client2018/content/textures/TerrainTools/mt_smooth.png b/Client2018/content/textures/TerrainTools/mt_smooth.png new file mode 100644 index 0000000..76aadbc Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mt_smooth.png differ diff --git a/Client2018/content/textures/TerrainTools/mt_subtract.png b/Client2018/content/textures/TerrainTools/mt_subtract.png new file mode 100644 index 0000000..a9b2a6a Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mt_subtract.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_asphalt.png b/Client2018/content/textures/TerrainTools/mtrl_asphalt.png new file mode 100644 index 0000000..7b35820 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_asphalt.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_basalt.png b/Client2018/content/textures/TerrainTools/mtrl_basalt.png new file mode 100644 index 0000000..aaccf88 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_basalt.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_brick.png b/Client2018/content/textures/TerrainTools/mtrl_brick.png new file mode 100644 index 0000000..e0feaac Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_brick.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_cobblestone.png b/Client2018/content/textures/TerrainTools/mtrl_cobblestone.png new file mode 100644 index 0000000..f36e40b Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_cobblestone.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_concrete.png b/Client2018/content/textures/TerrainTools/mtrl_concrete.png new file mode 100644 index 0000000..8786719 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_concrete.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_crackedlava.png b/Client2018/content/textures/TerrainTools/mtrl_crackedlava.png new file mode 100644 index 0000000..03dcd6c Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_crackedlava.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_glacier.png b/Client2018/content/textures/TerrainTools/mtrl_glacier.png new file mode 100644 index 0000000..bd795db Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_glacier.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_grass.png b/Client2018/content/textures/TerrainTools/mtrl_grass.png new file mode 100644 index 0000000..bb58cb5 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_grass.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_ground.png b/Client2018/content/textures/TerrainTools/mtrl_ground.png new file mode 100644 index 0000000..b605720 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_ground.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_ice.png b/Client2018/content/textures/TerrainTools/mtrl_ice.png new file mode 100644 index 0000000..535b8b2 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_ice.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_leafygrass.png b/Client2018/content/textures/TerrainTools/mtrl_leafygrass.png new file mode 100644 index 0000000..fd4a10f Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_leafygrass.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_limestone.png b/Client2018/content/textures/TerrainTools/mtrl_limestone.png new file mode 100644 index 0000000..a0c1419 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_limestone.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_mud.png b/Client2018/content/textures/TerrainTools/mtrl_mud.png new file mode 100644 index 0000000..44ccbbc Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_mud.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_pavement.png b/Client2018/content/textures/TerrainTools/mtrl_pavement.png new file mode 100644 index 0000000..4306661 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_pavement.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_rock.png b/Client2018/content/textures/TerrainTools/mtrl_rock.png new file mode 100644 index 0000000..a66785e Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_rock.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_salt.png b/Client2018/content/textures/TerrainTools/mtrl_salt.png new file mode 100644 index 0000000..00d8c17 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_salt.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_sand.png b/Client2018/content/textures/TerrainTools/mtrl_sand.png new file mode 100644 index 0000000..df3431b Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_sand.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_sandstone.png b/Client2018/content/textures/TerrainTools/mtrl_sandstone.png new file mode 100644 index 0000000..9a31639 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_sandstone.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_slate.png b/Client2018/content/textures/TerrainTools/mtrl_slate.png new file mode 100644 index 0000000..e2b4a65 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_slate.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_snow.png b/Client2018/content/textures/TerrainTools/mtrl_snow.png new file mode 100644 index 0000000..9698dda Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_snow.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_water.png b/Client2018/content/textures/TerrainTools/mtrl_water.png new file mode 100644 index 0000000..0b9595a Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_water.png differ diff --git a/Client2018/content/textures/TerrainTools/mtrl_woodplanks.png b/Client2018/content/textures/TerrainTools/mtrl_woodplanks.png new file mode 100644 index 0000000..d7a3cee Binary files /dev/null and b/Client2018/content/textures/TerrainTools/mtrl_woodplanks.png differ diff --git a/Client2018/content/textures/TerrainTools/progress_bar.png b/Client2018/content/textures/TerrainTools/progress_bar.png new file mode 100644 index 0000000..4c8be1c Binary files /dev/null and b/Client2018/content/textures/TerrainTools/progress_bar.png differ diff --git a/Client2018/content/textures/TerrainTools/radio_button_bullet.png b/Client2018/content/textures/TerrainTools/radio_button_bullet.png new file mode 100644 index 0000000..0f96b39 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/radio_button_bullet.png differ diff --git a/Client2018/content/textures/TerrainTools/radio_button_bullet_dark.png b/Client2018/content/textures/TerrainTools/radio_button_bullet_dark.png new file mode 100644 index 0000000..c846d72 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/radio_button_bullet_dark.png differ diff --git a/Client2018/content/textures/TerrainTools/radio_button_frame.png b/Client2018/content/textures/TerrainTools/radio_button_frame.png new file mode 100644 index 0000000..fbb0cff Binary files /dev/null and b/Client2018/content/textures/TerrainTools/radio_button_frame.png differ diff --git a/Client2018/content/textures/TerrainTools/radio_button_frame_dark.png b/Client2018/content/textures/TerrainTools/radio_button_frame_dark.png new file mode 100644 index 0000000..b3d9af0 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/radio_button_frame_dark.png differ diff --git a/Client2018/content/textures/TerrainTools/sliderbar_blue.png b/Client2018/content/textures/TerrainTools/sliderbar_blue.png new file mode 100644 index 0000000..0fbbf6b Binary files /dev/null and b/Client2018/content/textures/TerrainTools/sliderbar_blue.png differ diff --git a/Client2018/content/textures/TerrainTools/sliderbar_button.png b/Client2018/content/textures/TerrainTools/sliderbar_button.png new file mode 100644 index 0000000..0f8e1c7 Binary files /dev/null and b/Client2018/content/textures/TerrainTools/sliderbar_button.png differ diff --git a/Client2018/content/textures/TerrainTools/sliderbar_grey.png b/Client2018/content/textures/TerrainTools/sliderbar_grey.png new file mode 100644 index 0000000..dd1cdaa Binary files /dev/null and b/Client2018/content/textures/TerrainTools/sliderbar_grey.png differ diff --git a/Client2018/content/textures/UnAnchorCursor.png b/Client2018/content/textures/UnAnchorCursor.png new file mode 100644 index 0000000..4ca5381 Binary files /dev/null and b/Client2018/content/textures/UnAnchorCursor.png differ diff --git a/Client2018/content/textures/UnlockCursor.png b/Client2018/content/textures/UnlockCursor.png new file mode 100644 index 0000000..b765c16 Binary files /dev/null and b/Client2018/content/textures/UnlockCursor.png differ diff --git a/Client2018/content/textures/WeldCursor.png b/Client2018/content/textures/WeldCursor.png new file mode 100644 index 0000000..a686be3 Binary files /dev/null and b/Client2018/content/textures/WeldCursor.png differ diff --git a/Client2018/content/textures/advClosed-hand-anchored.png b/Client2018/content/textures/advClosed-hand-anchored.png new file mode 100644 index 0000000..1d71af1 Binary files /dev/null and b/Client2018/content/textures/advClosed-hand-anchored.png differ diff --git a/Client2018/content/textures/advClosed-hand-no-weld.png b/Client2018/content/textures/advClosed-hand-no-weld.png new file mode 100644 index 0000000..d914399 Binary files /dev/null and b/Client2018/content/textures/advClosed-hand-no-weld.png differ diff --git a/Client2018/content/textures/advClosed-hand-weld.png b/Client2018/content/textures/advClosed-hand-weld.png new file mode 100644 index 0000000..2a8e734 Binary files /dev/null and b/Client2018/content/textures/advClosed-hand-weld.png differ diff --git a/Client2018/content/textures/advClosed-hand.png b/Client2018/content/textures/advClosed-hand.png new file mode 100644 index 0000000..1adb82f Binary files /dev/null and b/Client2018/content/textures/advClosed-hand.png differ diff --git a/Client2018/content/textures/advCursor-default.png b/Client2018/content/textures/advCursor-default.png new file mode 100644 index 0000000..289c415 Binary files /dev/null and b/Client2018/content/textures/advCursor-default.png differ diff --git a/Client2018/content/textures/advCursor-openedHand.png b/Client2018/content/textures/advCursor-openedHand.png new file mode 100644 index 0000000..1aea61b Binary files /dev/null and b/Client2018/content/textures/advCursor-openedHand.png differ diff --git a/Client2018/content/textures/advCursor-white.png b/Client2018/content/textures/advCursor-white.png new file mode 100644 index 0000000..0ec7adc Binary files /dev/null and b/Client2018/content/textures/advCursor-white.png differ diff --git a/Client2018/content/textures/advancedMove.png b/Client2018/content/textures/advancedMove.png new file mode 100644 index 0000000..8621f0f Binary files /dev/null and b/Client2018/content/textures/advancedMove.png differ diff --git a/Client2018/content/textures/advancedMoveResize.png b/Client2018/content/textures/advancedMoveResize.png new file mode 100644 index 0000000..a7421be Binary files /dev/null and b/Client2018/content/textures/advancedMoveResize.png differ diff --git a/Client2018/content/textures/advancedMove_joint.png b/Client2018/content/textures/advancedMove_joint.png new file mode 100644 index 0000000..76f7f0a Binary files /dev/null and b/Client2018/content/textures/advancedMove_joint.png differ diff --git a/Client2018/content/textures/advancedMove_keysOnly.png b/Client2018/content/textures/advancedMove_keysOnly.png new file mode 100644 index 0000000..1b55927 Binary files /dev/null and b/Client2018/content/textures/advancedMove_keysOnly.png differ diff --git a/Client2018/content/textures/advancedMove_noJoint.png b/Client2018/content/textures/advancedMove_noJoint.png new file mode 100644 index 0000000..6066608 Binary files /dev/null and b/Client2018/content/textures/advancedMove_noJoint.png differ diff --git a/Client2018/content/textures/blackBkg_round.png b/Client2018/content/textures/blackBkg_round.png new file mode 100644 index 0000000..2796afa Binary files /dev/null and b/Client2018/content/textures/blackBkg_round.png differ diff --git a/Client2018/content/textures/blackBkg_square.png b/Client2018/content/textures/blackBkg_square.png new file mode 100644 index 0000000..0382ce3 Binary files /dev/null and b/Client2018/content/textures/blackBkg_square.png differ diff --git a/Client2018/content/textures/blockUpperLeft.png b/Client2018/content/textures/blockUpperLeft.png new file mode 100644 index 0000000..93a8190 Binary files /dev/null and b/Client2018/content/textures/blockUpperLeft.png differ diff --git a/Client2018/content/textures/chatBubble_bot_notifyGray_dotDotDot.png b/Client2018/content/textures/chatBubble_bot_notifyGray_dotDotDot.png new file mode 100644 index 0000000..cd76f6f Binary files /dev/null and b/Client2018/content/textures/chatBubble_bot_notifyGray_dotDotDot.png differ diff --git a/Client2018/content/textures/emoji/page0.png b/Client2018/content/textures/emoji/page0.png new file mode 100644 index 0000000..0c22723 Binary files /dev/null and b/Client2018/content/textures/emoji/page0.png differ diff --git a/Client2018/content/textures/emoji/page1.png b/Client2018/content/textures/emoji/page1.png new file mode 100644 index 0000000..1fc38ea Binary files /dev/null and b/Client2018/content/textures/emoji/page1.png differ diff --git a/Client2018/content/textures/emoji/page10.png b/Client2018/content/textures/emoji/page10.png new file mode 100644 index 0000000..abc37a3 Binary files /dev/null and b/Client2018/content/textures/emoji/page10.png differ diff --git a/Client2018/content/textures/emoji/page11.png b/Client2018/content/textures/emoji/page11.png new file mode 100644 index 0000000..ba4f0b7 Binary files /dev/null and b/Client2018/content/textures/emoji/page11.png differ diff --git a/Client2018/content/textures/emoji/page12.png b/Client2018/content/textures/emoji/page12.png new file mode 100644 index 0000000..02a9817 Binary files /dev/null and b/Client2018/content/textures/emoji/page12.png differ diff --git a/Client2018/content/textures/emoji/page13.png b/Client2018/content/textures/emoji/page13.png new file mode 100644 index 0000000..aa8ce1a Binary files /dev/null and b/Client2018/content/textures/emoji/page13.png differ diff --git a/Client2018/content/textures/emoji/page14.png b/Client2018/content/textures/emoji/page14.png new file mode 100644 index 0000000..d7a1d56 Binary files /dev/null and b/Client2018/content/textures/emoji/page14.png differ diff --git a/Client2018/content/textures/emoji/page2.png b/Client2018/content/textures/emoji/page2.png new file mode 100644 index 0000000..a292446 Binary files /dev/null and b/Client2018/content/textures/emoji/page2.png differ diff --git a/Client2018/content/textures/emoji/page3.png b/Client2018/content/textures/emoji/page3.png new file mode 100644 index 0000000..578c214 Binary files /dev/null and b/Client2018/content/textures/emoji/page3.png differ diff --git a/Client2018/content/textures/emoji/page4.png b/Client2018/content/textures/emoji/page4.png new file mode 100644 index 0000000..47dd7c4 Binary files /dev/null and b/Client2018/content/textures/emoji/page4.png differ diff --git a/Client2018/content/textures/emoji/page5.png b/Client2018/content/textures/emoji/page5.png new file mode 100644 index 0000000..b6c21df Binary files /dev/null and b/Client2018/content/textures/emoji/page5.png differ diff --git a/Client2018/content/textures/emoji/page6.png b/Client2018/content/textures/emoji/page6.png new file mode 100644 index 0000000..0544b1d Binary files /dev/null and b/Client2018/content/textures/emoji/page6.png differ diff --git a/Client2018/content/textures/emoji/page7.png b/Client2018/content/textures/emoji/page7.png new file mode 100644 index 0000000..01f34a3 Binary files /dev/null and b/Client2018/content/textures/emoji/page7.png differ diff --git a/Client2018/content/textures/emoji/page8.png b/Client2018/content/textures/emoji/page8.png new file mode 100644 index 0000000..f9e2eb0 Binary files /dev/null and b/Client2018/content/textures/emoji/page8.png differ diff --git a/Client2018/content/textures/emoji/page9.png b/Client2018/content/textures/emoji/page9.png new file mode 100644 index 0000000..810037b Binary files /dev/null and b/Client2018/content/textures/emoji/page9.png differ diff --git a/Client2018/content/textures/explosion.png b/Client2018/content/textures/explosion.png new file mode 100644 index 0000000..5c5787d Binary files /dev/null and b/Client2018/content/textures/explosion.png differ diff --git a/Client2018/content/textures/face.png b/Client2018/content/textures/face.png new file mode 100644 index 0000000..08254c0 Binary files /dev/null and b/Client2018/content/textures/face.png differ diff --git a/Client2018/content/textures/glow.png b/Client2018/content/textures/glow.png new file mode 100644 index 0000000..886de92 Binary files /dev/null and b/Client2018/content/textures/glow.png differ diff --git a/Client2018/content/textures/gradient.png b/Client2018/content/textures/gradient.png new file mode 100644 index 0000000..540d93a Binary files /dev/null and b/Client2018/content/textures/gradient.png differ diff --git a/Client2018/content/textures/grid16.png b/Client2018/content/textures/grid16.png new file mode 100644 index 0000000..4c17cb0 Binary files /dev/null and b/Client2018/content/textures/grid16.png differ diff --git a/Client2018/content/textures/grid2.png b/Client2018/content/textures/grid2.png new file mode 100644 index 0000000..c9379c8 Binary files /dev/null and b/Client2018/content/textures/grid2.png differ diff --git a/Client2018/content/textures/grid4.png b/Client2018/content/textures/grid4.png new file mode 100644 index 0000000..55d88e3 Binary files /dev/null and b/Client2018/content/textures/grid4.png differ diff --git a/Client2018/content/textures/icon_ROBUX.png b/Client2018/content/textures/icon_ROBUX.png new file mode 100644 index 0000000..9e5c0f8 Binary files /dev/null and b/Client2018/content/textures/icon_ROBUX.png differ diff --git a/Client2018/content/textures/icon_ROBUX@2x.png b/Client2018/content/textures/icon_ROBUX@2x.png new file mode 100644 index 0000000..0b2a791 Binary files /dev/null and b/Client2018/content/textures/icon_ROBUX@2x.png differ diff --git a/Client2018/content/textures/loading/cancelButton.png b/Client2018/content/textures/loading/cancelButton.png new file mode 100644 index 0000000..77e6a39 Binary files /dev/null and b/Client2018/content/textures/loading/cancelButton.png differ diff --git a/Client2018/content/textures/loading/darkLoadingTexture.png b/Client2018/content/textures/loading/darkLoadingTexture.png new file mode 100644 index 0000000..90a0910 Binary files /dev/null and b/Client2018/content/textures/loading/darkLoadingTexture.png differ diff --git a/Client2018/content/textures/loading/loadingCircle.png b/Client2018/content/textures/loading/loadingCircle.png new file mode 100644 index 0000000..6788c78 Binary files /dev/null and b/Client2018/content/textures/loading/loadingCircle.png differ diff --git a/Client2018/content/textures/loading/loadingTexture.png b/Client2018/content/textures/loading/loadingTexture.png new file mode 100644 index 0000000..b58f420 Binary files /dev/null and b/Client2018/content/textures/loading/loadingTexture.png differ diff --git a/Client2018/content/textures/loading/loadingvignette.png b/Client2018/content/textures/loading/loadingvignette.png new file mode 100644 index 0000000..5952539 Binary files /dev/null and b/Client2018/content/textures/loading/loadingvignette.png differ diff --git a/Client2018/content/textures/loading/robloxTilt.png b/Client2018/content/textures/loading/robloxTilt.png new file mode 100644 index 0000000..068fe31 Binary files /dev/null and b/Client2018/content/textures/loading/robloxTilt.png differ diff --git a/Client2018/content/textures/loading/robloxTiltRed.png b/Client2018/content/textures/loading/robloxTiltRed.png new file mode 100644 index 0000000..8cfc292 Binary files /dev/null and b/Client2018/content/textures/loading/robloxTiltRed.png differ diff --git a/Client2018/content/textures/loading/robloxlogo.png b/Client2018/content/textures/loading/robloxlogo.png new file mode 100644 index 0000000..2d05445 Binary files /dev/null and b/Client2018/content/textures/loading/robloxlogo.png differ diff --git a/Client2018/content/textures/localizationExport.png b/Client2018/content/textures/localizationExport.png new file mode 100644 index 0000000..591c2c9 Binary files /dev/null and b/Client2018/content/textures/localizationExport.png differ diff --git a/Client2018/content/textures/localizationImport.png b/Client2018/content/textures/localizationImport.png new file mode 100644 index 0000000..10dda0e Binary files /dev/null and b/Client2018/content/textures/localizationImport.png differ diff --git a/Client2018/content/textures/localizationTargetEnglish.png b/Client2018/content/textures/localizationTargetEnglish.png new file mode 100644 index 0000000..5d528f1 Binary files /dev/null and b/Client2018/content/textures/localizationTargetEnglish.png differ diff --git a/Client2018/content/textures/localizationTargetSpanish.png b/Client2018/content/textures/localizationTargetSpanish.png new file mode 100644 index 0000000..34e1db9 Binary files /dev/null and b/Client2018/content/textures/localizationTargetSpanish.png differ diff --git a/Client2018/content/textures/localizationTestingIcon.png b/Client2018/content/textures/localizationTestingIcon.png new file mode 100644 index 0000000..dad16bd Binary files /dev/null and b/Client2018/content/textures/localizationTestingIcon.png differ diff --git a/Client2018/content/textures/localizationUIScrapingOff.png b/Client2018/content/textures/localizationUIScrapingOff.png new file mode 100644 index 0000000..ca3fc50 Binary files /dev/null and b/Client2018/content/textures/localizationUIScrapingOff.png differ diff --git a/Client2018/content/textures/localizationUIScrapingOn.png b/Client2018/content/textures/localizationUIScrapingOn.png new file mode 100644 index 0000000..e034998 Binary files /dev/null and b/Client2018/content/textures/localizationUIScrapingOn.png differ diff --git a/Client2018/content/textures/menuDownArrow.png b/Client2018/content/textures/menuDownArrow.png new file mode 100644 index 0000000..286ecd8 Binary files /dev/null and b/Client2018/content/textures/menuDownArrow.png differ diff --git a/Client2018/content/textures/meshPartFallback.png b/Client2018/content/textures/meshPartFallback.png new file mode 100644 index 0000000..1c1e179 Binary files /dev/null and b/Client2018/content/textures/meshPartFallback.png differ diff --git a/Client2018/content/textures/particles/common_alpha.dds b/Client2018/content/textures/particles/common_alpha.dds new file mode 100644 index 0000000..2cc9c9c Binary files /dev/null and b/Client2018/content/textures/particles/common_alpha.dds differ diff --git a/Client2018/content/textures/particles/explosion01_core_alpha.png b/Client2018/content/textures/particles/explosion01_core_alpha.png new file mode 100644 index 0000000..47ae484 Binary files /dev/null and b/Client2018/content/textures/particles/explosion01_core_alpha.png differ diff --git a/Client2018/content/textures/particles/explosion01_core_main.dds b/Client2018/content/textures/particles/explosion01_core_main.dds new file mode 100644 index 0000000..44fd908 Binary files /dev/null and b/Client2018/content/textures/particles/explosion01_core_main.dds differ diff --git a/Client2018/content/textures/particles/explosion01_implosion_color.png b/Client2018/content/textures/particles/explosion01_implosion_color.png new file mode 100644 index 0000000..2f1e9ed Binary files /dev/null and b/Client2018/content/textures/particles/explosion01_implosion_color.png differ diff --git a/Client2018/content/textures/particles/explosion01_implosion_main.dds b/Client2018/content/textures/particles/explosion01_implosion_main.dds new file mode 100644 index 0000000..a5bf1b5 Binary files /dev/null and b/Client2018/content/textures/particles/explosion01_implosion_main.dds differ diff --git a/Client2018/content/textures/particles/explosion01_shockwave_main.dds b/Client2018/content/textures/particles/explosion01_shockwave_main.dds new file mode 100644 index 0000000..7a30ca6 Binary files /dev/null and b/Client2018/content/textures/particles/explosion01_shockwave_main.dds differ diff --git a/Client2018/content/textures/particles/explosion01_smoke_alpha.dds b/Client2018/content/textures/particles/explosion01_smoke_alpha.dds new file mode 100644 index 0000000..99807c7 Binary files /dev/null and b/Client2018/content/textures/particles/explosion01_smoke_alpha.dds differ diff --git a/Client2018/content/textures/particles/explosion01_smoke_color_new.dds b/Client2018/content/textures/particles/explosion01_smoke_color_new.dds new file mode 100644 index 0000000..fd4df8f Binary files /dev/null and b/Client2018/content/textures/particles/explosion01_smoke_color_new.dds differ diff --git a/Client2018/content/textures/particles/explosion01_smoke_main.dds b/Client2018/content/textures/particles/explosion01_smoke_main.dds new file mode 100644 index 0000000..c37f99a Binary files /dev/null and b/Client2018/content/textures/particles/explosion01_smoke_main.dds differ diff --git a/Client2018/content/textures/particles/explosion_alpha.dds b/Client2018/content/textures/particles/explosion_alpha.dds new file mode 100644 index 0000000..add095f Binary files /dev/null and b/Client2018/content/textures/particles/explosion_alpha.dds differ diff --git a/Client2018/content/textures/particles/explosion_color.dds b/Client2018/content/textures/particles/explosion_color.dds new file mode 100644 index 0000000..f69e2cc Binary files /dev/null and b/Client2018/content/textures/particles/explosion_color.dds differ diff --git a/Client2018/content/textures/particles/fire_alpha.dds b/Client2018/content/textures/particles/fire_alpha.dds new file mode 100644 index 0000000..812a506 Binary files /dev/null and b/Client2018/content/textures/particles/fire_alpha.dds differ diff --git a/Client2018/content/textures/particles/fire_color.dds b/Client2018/content/textures/particles/fire_color.dds new file mode 100644 index 0000000..5d569ad Binary files /dev/null and b/Client2018/content/textures/particles/fire_color.dds differ diff --git a/Client2018/content/textures/particles/fire_main.dds b/Client2018/content/textures/particles/fire_main.dds new file mode 100644 index 0000000..22a5049 Binary files /dev/null and b/Client2018/content/textures/particles/fire_main.dds differ diff --git a/Client2018/content/textures/particles/fire_sparks_color.dds b/Client2018/content/textures/particles/fire_sparks_color.dds new file mode 100644 index 0000000..8876db1 Binary files /dev/null and b/Client2018/content/textures/particles/fire_sparks_color.dds differ diff --git a/Client2018/content/textures/particles/fire_sparks_main.dds b/Client2018/content/textures/particles/fire_sparks_main.dds new file mode 100644 index 0000000..4468e5a Binary files /dev/null and b/Client2018/content/textures/particles/fire_sparks_main.dds differ diff --git a/Client2018/content/textures/particles/forcefield_alpha.dds b/Client2018/content/textures/particles/forcefield_alpha.dds new file mode 100644 index 0000000..03fdac0 Binary files /dev/null and b/Client2018/content/textures/particles/forcefield_alpha.dds differ diff --git a/Client2018/content/textures/particles/forcefield_glow_alpha.dds b/Client2018/content/textures/particles/forcefield_glow_alpha.dds new file mode 100644 index 0000000..326310f Binary files /dev/null and b/Client2018/content/textures/particles/forcefield_glow_alpha.dds differ diff --git a/Client2018/content/textures/particles/forcefield_glow_color.dds b/Client2018/content/textures/particles/forcefield_glow_color.dds new file mode 100644 index 0000000..39dfb8e Binary files /dev/null and b/Client2018/content/textures/particles/forcefield_glow_color.dds differ diff --git a/Client2018/content/textures/particles/forcefield_glow_main.dds b/Client2018/content/textures/particles/forcefield_glow_main.dds new file mode 100644 index 0000000..d1a6472 Binary files /dev/null and b/Client2018/content/textures/particles/forcefield_glow_main.dds differ diff --git a/Client2018/content/textures/particles/forcefield_vortex_color.dds b/Client2018/content/textures/particles/forcefield_vortex_color.dds new file mode 100644 index 0000000..fe33e22 Binary files /dev/null and b/Client2018/content/textures/particles/forcefield_vortex_color.dds differ diff --git a/Client2018/content/textures/particles/forcefield_vortex_main.dds b/Client2018/content/textures/particles/forcefield_vortex_main.dds new file mode 100644 index 0000000..d67cf49 Binary files /dev/null and b/Client2018/content/textures/particles/forcefield_vortex_main.dds differ diff --git a/Client2018/content/textures/particles/legacy_fire_alpha_color.dds b/Client2018/content/textures/particles/legacy_fire_alpha_color.dds new file mode 100644 index 0000000..da0fb05 Binary files /dev/null and b/Client2018/content/textures/particles/legacy_fire_alpha_color.dds differ diff --git a/Client2018/content/textures/particles/smoke_color.dds b/Client2018/content/textures/particles/smoke_color.dds new file mode 100644 index 0000000..a4d0a4f Binary files /dev/null and b/Client2018/content/textures/particles/smoke_color.dds differ diff --git a/Client2018/content/textures/particles/smoke_main.dds b/Client2018/content/textures/particles/smoke_main.dds new file mode 100644 index 0000000..1aaef8d Binary files /dev/null and b/Client2018/content/textures/particles/smoke_main.dds differ diff --git a/Client2018/content/textures/particles/sparkles_color.dds b/Client2018/content/textures/particles/sparkles_color.dds new file mode 100644 index 0000000..4fd1f85 Binary files /dev/null and b/Client2018/content/textures/particles/sparkles_color.dds differ diff --git a/Client2018/content/textures/particles/sparkles_main.dds b/Client2018/content/textures/particles/sparkles_main.dds new file mode 100644 index 0000000..d1875fd Binary files /dev/null and b/Client2018/content/textures/particles/sparkles_main.dds differ diff --git a/Client2018/content/textures/rotationArrow.png b/Client2018/content/textures/rotationArrow.png new file mode 100644 index 0000000..5bd9eb5 Binary files /dev/null and b/Client2018/content/textures/rotationArrow.png differ diff --git a/Client2018/content/textures/shadowblurmask.png b/Client2018/content/textures/shadowblurmask.png new file mode 100644 index 0000000..ae33768 Binary files /dev/null and b/Client2018/content/textures/shadowblurmask.png differ diff --git a/Client2018/content/textures/sparkle.png b/Client2018/content/textures/sparkle.png new file mode 100644 index 0000000..510b061 Binary files /dev/null and b/Client2018/content/textures/sparkle.png differ diff --git a/Client2018/content/textures/transformFiveDegrees.png b/Client2018/content/textures/transformFiveDegrees.png new file mode 100644 index 0000000..1201cea Binary files /dev/null and b/Client2018/content/textures/transformFiveDegrees.png differ diff --git a/Client2018/content/textures/transformNinetyDegrees.png b/Client2018/content/textures/transformNinetyDegrees.png new file mode 100644 index 0000000..253ad36 Binary files /dev/null and b/Client2018/content/textures/transformNinetyDegrees.png differ diff --git a/Client2018/content/textures/transformOneDegree.png b/Client2018/content/textures/transformOneDegree.png new file mode 100644 index 0000000..79cb61f Binary files /dev/null and b/Client2018/content/textures/transformOneDegree.png differ diff --git a/Client2018/content/textures/transformTwentyTwoDegrees.png b/Client2018/content/textures/transformTwentyTwoDegrees.png new file mode 100644 index 0000000..556b024 Binary files /dev/null and b/Client2018/content/textures/transformTwentyTwoDegrees.png differ diff --git a/Client2018/content/textures/ui/Backpack/Backpack.png b/Client2018/content/textures/ui/Backpack/Backpack.png new file mode 100644 index 0000000..8d9048a Binary files /dev/null and b/Client2018/content/textures/ui/Backpack/Backpack.png differ diff --git a/Client2018/content/textures/ui/Backpack/Backpack@2x.png b/Client2018/content/textures/ui/Backpack/Backpack@2x.png new file mode 100644 index 0000000..9814297 Binary files /dev/null and b/Client2018/content/textures/ui/Backpack/Backpack@2x.png differ diff --git a/Client2018/content/textures/ui/Backpack/Backpack_Down.png b/Client2018/content/textures/ui/Backpack/Backpack_Down.png new file mode 100644 index 0000000..b687a19 Binary files /dev/null and b/Client2018/content/textures/ui/Backpack/Backpack_Down.png differ diff --git a/Client2018/content/textures/ui/Backpack/Backpack_Down@2x.png b/Client2018/content/textures/ui/Backpack/Backpack_Down@2x.png new file mode 100644 index 0000000..5c46d4a Binary files /dev/null and b/Client2018/content/textures/ui/Backpack/Backpack_Down@2x.png differ diff --git a/Client2018/content/textures/ui/Backpack/ScrollDownArrow.png b/Client2018/content/textures/ui/Backpack/ScrollDownArrow.png new file mode 100644 index 0000000..68e14aa Binary files /dev/null and b/Client2018/content/textures/ui/Backpack/ScrollDownArrow.png differ diff --git a/Client2018/content/textures/ui/Backpack/ScrollUpArrow.png b/Client2018/content/textures/ui/Backpack/ScrollUpArrow.png new file mode 100644 index 0000000..3824e05 Binary files /dev/null and b/Client2018/content/textures/ui/Backpack/ScrollUpArrow.png differ diff --git a/Client2018/content/textures/ui/Backpack_Close.png b/Client2018/content/textures/ui/Backpack_Close.png new file mode 100644 index 0000000..2b3679b Binary files /dev/null and b/Client2018/content/textures/ui/Backpack_Close.png differ diff --git a/Client2018/content/textures/ui/Backpack_Close@2x.png b/Client2018/content/textures/ui/Backpack_Close@2x.png new file mode 100644 index 0000000..1716f55 Binary files /dev/null and b/Client2018/content/textures/ui/Backpack_Close@2x.png differ diff --git a/Client2018/content/textures/ui/Backpack_Open.png b/Client2018/content/textures/ui/Backpack_Open.png new file mode 100644 index 0000000..24fba15 Binary files /dev/null and b/Client2018/content/textures/ui/Backpack_Open.png differ diff --git a/Client2018/content/textures/ui/Backpack_Open@2x.png b/Client2018/content/textures/ui/Backpack_Open@2x.png new file mode 100644 index 0000000..a56634c Binary files /dev/null and b/Client2018/content/textures/ui/Backpack_Open@2x.png differ diff --git a/Client2018/content/textures/ui/ButtonLeft.png b/Client2018/content/textures/ui/ButtonLeft.png new file mode 100644 index 0000000..f2654bd Binary files /dev/null and b/Client2018/content/textures/ui/ButtonLeft.png differ diff --git a/Client2018/content/textures/ui/ButtonLeftDown.png b/Client2018/content/textures/ui/ButtonLeftDown.png new file mode 100644 index 0000000..67cdc7f Binary files /dev/null and b/Client2018/content/textures/ui/ButtonLeftDown.png differ diff --git a/Client2018/content/textures/ui/ButtonRight.png b/Client2018/content/textures/ui/ButtonRight.png new file mode 100644 index 0000000..8defe2e Binary files /dev/null and b/Client2018/content/textures/ui/ButtonRight.png differ diff --git a/Client2018/content/textures/ui/ButtonRightDown.png b/Client2018/content/textures/ui/ButtonRightDown.png new file mode 100644 index 0000000..218f2d2 Binary files /dev/null and b/Client2018/content/textures/ui/ButtonRightDown.png differ diff --git a/Client2018/content/textures/ui/Chat/Chat.png b/Client2018/content/textures/ui/Chat/Chat.png new file mode 100644 index 0000000..d42e4ea Binary files /dev/null and b/Client2018/content/textures/ui/Chat/Chat.png differ diff --git a/Client2018/content/textures/ui/Chat/Chat@2x.png b/Client2018/content/textures/ui/Chat/Chat@2x.png new file mode 100644 index 0000000..57ea151 Binary files /dev/null and b/Client2018/content/textures/ui/Chat/Chat@2x.png differ diff --git a/Client2018/content/textures/ui/Chat/ChatDown.png b/Client2018/content/textures/ui/Chat/ChatDown.png new file mode 100644 index 0000000..002119b Binary files /dev/null and b/Client2018/content/textures/ui/Chat/ChatDown.png differ diff --git a/Client2018/content/textures/ui/Chat/ChatDown@2x.png b/Client2018/content/textures/ui/Chat/ChatDown@2x.png new file mode 100644 index 0000000..f25f80d Binary files /dev/null and b/Client2018/content/textures/ui/Chat/ChatDown@2x.png differ diff --git a/Client2018/content/textures/ui/Chat/ChatDownFlip.png b/Client2018/content/textures/ui/Chat/ChatDownFlip.png new file mode 100644 index 0000000..9523d39 Binary files /dev/null and b/Client2018/content/textures/ui/Chat/ChatDownFlip.png differ diff --git a/Client2018/content/textures/ui/Chat/ChatDownFlip@2x.png b/Client2018/content/textures/ui/Chat/ChatDownFlip@2x.png new file mode 100644 index 0000000..dd6b15e Binary files /dev/null and b/Client2018/content/textures/ui/Chat/ChatDownFlip@2x.png differ diff --git a/Client2018/content/textures/ui/Chat/ChatFlip.png b/Client2018/content/textures/ui/Chat/ChatFlip.png new file mode 100644 index 0000000..f0b508a Binary files /dev/null and b/Client2018/content/textures/ui/Chat/ChatFlip.png differ diff --git a/Client2018/content/textures/ui/Chat/ChatFlip@2x.png b/Client2018/content/textures/ui/Chat/ChatFlip@2x.png new file mode 100644 index 0000000..1e8d6d1 Binary files /dev/null and b/Client2018/content/textures/ui/Chat/ChatFlip@2x.png differ diff --git a/Client2018/content/textures/ui/Chat/MessageCounter.png b/Client2018/content/textures/ui/Chat/MessageCounter.png new file mode 100644 index 0000000..4ba3cbd Binary files /dev/null and b/Client2018/content/textures/ui/Chat/MessageCounter.png differ diff --git a/Client2018/content/textures/ui/Chat/MessageCounter@2x.png b/Client2018/content/textures/ui/Chat/MessageCounter@2x.png new file mode 100644 index 0000000..de2cc47 Binary files /dev/null and b/Client2018/content/textures/ui/Chat/MessageCounter@2x.png differ diff --git a/Client2018/content/textures/ui/Chat/ToggleChat.png b/Client2018/content/textures/ui/Chat/ToggleChat.png new file mode 100644 index 0000000..1bc7450 Binary files /dev/null and b/Client2018/content/textures/ui/Chat/ToggleChat.png differ diff --git a/Client2018/content/textures/ui/Chat/ToggleChat@2x.png b/Client2018/content/textures/ui/Chat/ToggleChat@2x.png new file mode 100644 index 0000000..5ab133e Binary files /dev/null and b/Client2018/content/textures/ui/Chat/ToggleChat@2x.png differ diff --git a/Client2018/content/textures/ui/Chat/ToggleChatDown.png b/Client2018/content/textures/ui/Chat/ToggleChatDown.png new file mode 100644 index 0000000..2956a50 Binary files /dev/null and b/Client2018/content/textures/ui/Chat/ToggleChatDown.png differ diff --git a/Client2018/content/textures/ui/Chat/ToggleChatDown@2x.png b/Client2018/content/textures/ui/Chat/ToggleChatDown@2x.png new file mode 100644 index 0000000..065be38 Binary files /dev/null and b/Client2018/content/textures/ui/Chat/ToggleChatDown@2x.png differ diff --git a/Client2018/content/textures/ui/Chat/ToggleChatDownFlip.png b/Client2018/content/textures/ui/Chat/ToggleChatDownFlip.png new file mode 100644 index 0000000..34d7376 Binary files /dev/null and b/Client2018/content/textures/ui/Chat/ToggleChatDownFlip.png differ diff --git a/Client2018/content/textures/ui/Chat/ToggleChatDownFlip@2x.png b/Client2018/content/textures/ui/Chat/ToggleChatDownFlip@2x.png new file mode 100644 index 0000000..c9a6dff Binary files /dev/null and b/Client2018/content/textures/ui/Chat/ToggleChatDownFlip@2x.png differ diff --git a/Client2018/content/textures/ui/Chat/ToggleChatFlip.png b/Client2018/content/textures/ui/Chat/ToggleChatFlip.png new file mode 100644 index 0000000..e929cfa Binary files /dev/null and b/Client2018/content/textures/ui/Chat/ToggleChatFlip.png differ diff --git a/Client2018/content/textures/ui/Chat/ToggleChatFlip@2x.png b/Client2018/content/textures/ui/Chat/ToggleChatFlip@2x.png new file mode 100644 index 0000000..1d02265 Binary files /dev/null and b/Client2018/content/textures/ui/Chat/ToggleChatFlip@2x.png differ diff --git a/Client2018/content/textures/ui/Chat/VRChatBackground.png b/Client2018/content/textures/ui/Chat/VRChatBackground.png new file mode 100644 index 0000000..694a50f Binary files /dev/null and b/Client2018/content/textures/ui/Chat/VRChatBackground.png differ diff --git a/Client2018/content/textures/ui/CloseButton.png b/Client2018/content/textures/ui/CloseButton.png new file mode 100644 index 0000000..6ba8d7b Binary files /dev/null and b/Client2018/content/textures/ui/CloseButton.png differ diff --git a/Client2018/content/textures/ui/CloseButton_dn.png b/Client2018/content/textures/ui/CloseButton_dn.png new file mode 100644 index 0000000..ee4b9ff Binary files /dev/null and b/Client2018/content/textures/ui/CloseButton_dn.png differ diff --git a/Client2018/content/textures/ui/DPadSheet.png b/Client2018/content/textures/ui/DPadSheet.png new file mode 100644 index 0000000..046e2e7 Binary files /dev/null and b/Client2018/content/textures/ui/DPadSheet.png differ diff --git a/Client2018/content/textures/ui/ErrorIcon.png b/Client2018/content/textures/ui/ErrorIcon.png new file mode 100644 index 0000000..420b21b Binary files /dev/null and b/Client2018/content/textures/ui/ErrorIcon.png differ diff --git a/Client2018/content/textures/ui/ErrorIconSmall.png b/Client2018/content/textures/ui/ErrorIconSmall.png new file mode 100644 index 0000000..213a1ff Binary files /dev/null and b/Client2018/content/textures/ui/ErrorIconSmall.png differ diff --git a/Client2018/content/textures/ui/ErrorPrompt/PrimaryButton.png b/Client2018/content/textures/ui/ErrorPrompt/PrimaryButton.png new file mode 100644 index 0000000..afae8bb Binary files /dev/null and b/Client2018/content/textures/ui/ErrorPrompt/PrimaryButton.png differ diff --git a/Client2018/content/textures/ui/ErrorPrompt/PrimaryButton@2x.png b/Client2018/content/textures/ui/ErrorPrompt/PrimaryButton@2x.png new file mode 100644 index 0000000..0f8157c Binary files /dev/null and b/Client2018/content/textures/ui/ErrorPrompt/PrimaryButton@2x.png differ diff --git a/Client2018/content/textures/ui/ErrorPrompt/PrimaryButton@3x.png b/Client2018/content/textures/ui/ErrorPrompt/PrimaryButton@3x.png new file mode 100644 index 0000000..72a5371 Binary files /dev/null and b/Client2018/content/textures/ui/ErrorPrompt/PrimaryButton@3x.png differ diff --git a/Client2018/content/textures/ui/ErrorPrompt/SecondaryButton.png b/Client2018/content/textures/ui/ErrorPrompt/SecondaryButton.png new file mode 100644 index 0000000..70b102b Binary files /dev/null and b/Client2018/content/textures/ui/ErrorPrompt/SecondaryButton.png differ diff --git a/Client2018/content/textures/ui/ErrorPrompt/SecondaryButton@2x.png b/Client2018/content/textures/ui/ErrorPrompt/SecondaryButton@2x.png new file mode 100644 index 0000000..cee0a86 Binary files /dev/null and b/Client2018/content/textures/ui/ErrorPrompt/SecondaryButton@2x.png differ diff --git a/Client2018/content/textures/ui/ErrorPrompt/SecondaryButton@3x.png b/Client2018/content/textures/ui/ErrorPrompt/SecondaryButton@3x.png new file mode 100644 index 0000000..0ac6f5c Binary files /dev/null and b/Client2018/content/textures/ui/ErrorPrompt/SecondaryButton@3x.png differ diff --git a/Client2018/content/textures/ui/ErrorPrompt/ShimmerOverlay.png b/Client2018/content/textures/ui/ErrorPrompt/ShimmerOverlay.png new file mode 100644 index 0000000..a38dcc8 Binary files /dev/null and b/Client2018/content/textures/ui/ErrorPrompt/ShimmerOverlay.png differ diff --git a/Client2018/content/textures/ui/ErrorPrompt/ShimmerOverlay@2x.png b/Client2018/content/textures/ui/ErrorPrompt/ShimmerOverlay@2x.png new file mode 100644 index 0000000..4747f05 Binary files /dev/null and b/Client2018/content/textures/ui/ErrorPrompt/ShimmerOverlay@2x.png differ diff --git a/Client2018/content/textures/ui/ErrorPrompt/ShimmerOverlay@3x.png b/Client2018/content/textures/ui/ErrorPrompt/ShimmerOverlay@3x.png new file mode 100644 index 0000000..923705d Binary files /dev/null and b/Client2018/content/textures/ui/ErrorPrompt/ShimmerOverlay@3x.png differ diff --git a/Client2018/content/textures/ui/ExpandArrowSheet.png b/Client2018/content/textures/ui/ExpandArrowSheet.png new file mode 100644 index 0000000..d52edc1 Binary files /dev/null and b/Client2018/content/textures/ui/ExpandArrowSheet.png differ diff --git a/Client2018/content/textures/ui/Gear.png b/Client2018/content/textures/ui/Gear.png new file mode 100644 index 0000000..56a46db Binary files /dev/null and b/Client2018/content/textures/ui/Gear.png differ diff --git a/Client2018/content/textures/ui/Gear_dn.png b/Client2018/content/textures/ui/Gear_dn.png new file mode 100644 index 0000000..38df9a3 Binary files /dev/null and b/Client2018/content/textures/ui/Gear_dn.png differ diff --git a/Client2018/content/textures/ui/Health-BKG-Center.png b/Client2018/content/textures/ui/Health-BKG-Center.png new file mode 100644 index 0000000..dea1b3b Binary files /dev/null and b/Client2018/content/textures/ui/Health-BKG-Center.png differ diff --git a/Client2018/content/textures/ui/Health-BKG-Center@2x.png b/Client2018/content/textures/ui/Health-BKG-Center@2x.png new file mode 100644 index 0000000..3fe7101 Binary files /dev/null and b/Client2018/content/textures/ui/Health-BKG-Center@2x.png differ diff --git a/Client2018/content/textures/ui/Health-BKG-Left-Cap.png b/Client2018/content/textures/ui/Health-BKG-Left-Cap.png new file mode 100644 index 0000000..9baf764 Binary files /dev/null and b/Client2018/content/textures/ui/Health-BKG-Left-Cap.png differ diff --git a/Client2018/content/textures/ui/Health-BKG-Left-Cap@2x.png b/Client2018/content/textures/ui/Health-BKG-Left-Cap@2x.png new file mode 100644 index 0000000..6a7825c Binary files /dev/null and b/Client2018/content/textures/ui/Health-BKG-Left-Cap@2x.png differ diff --git a/Client2018/content/textures/ui/Health-BKG-Right-Cap.png b/Client2018/content/textures/ui/Health-BKG-Right-Cap.png new file mode 100644 index 0000000..66af73a Binary files /dev/null and b/Client2018/content/textures/ui/Health-BKG-Right-Cap.png differ diff --git a/Client2018/content/textures/ui/Health-BKG-Right-Cap@2x.png b/Client2018/content/textures/ui/Health-BKG-Right-Cap@2x.png new file mode 100644 index 0000000..7c3f951 Binary files /dev/null and b/Client2018/content/textures/ui/Health-BKG-Right-Cap@2x.png differ diff --git a/Client2018/content/textures/ui/ImageSet/AE/img_set_1x_1.png b/Client2018/content/textures/ui/ImageSet/AE/img_set_1x_1.png new file mode 100644 index 0000000..42adc76 Binary files /dev/null and b/Client2018/content/textures/ui/ImageSet/AE/img_set_1x_1.png differ diff --git a/Client2018/content/textures/ui/ImageSet/AE/img_set_2x_1.png b/Client2018/content/textures/ui/ImageSet/AE/img_set_2x_1.png new file mode 100644 index 0000000..921927e Binary files /dev/null and b/Client2018/content/textures/ui/ImageSet/AE/img_set_2x_1.png differ diff --git a/Client2018/content/textures/ui/ImageSet/AE/img_set_3x_1.png b/Client2018/content/textures/ui/ImageSet/AE/img_set_3x_1.png new file mode 100644 index 0000000..42c90e4 Binary files /dev/null and b/Client2018/content/textures/ui/ImageSet/AE/img_set_3x_1.png differ diff --git a/Client2018/content/textures/ui/ImageSet/LuaApp/img_set_1x_1.png b/Client2018/content/textures/ui/ImageSet/LuaApp/img_set_1x_1.png new file mode 100644 index 0000000..fbcc8f6 Binary files /dev/null and b/Client2018/content/textures/ui/ImageSet/LuaApp/img_set_1x_1.png differ diff --git a/Client2018/content/textures/ui/ImageSet/LuaApp/img_set_2x_1.png b/Client2018/content/textures/ui/ImageSet/LuaApp/img_set_2x_1.png new file mode 100644 index 0000000..901362d Binary files /dev/null and b/Client2018/content/textures/ui/ImageSet/LuaApp/img_set_2x_1.png differ diff --git a/Client2018/content/textures/ui/ImageSet/LuaApp/img_set_3x_1.png b/Client2018/content/textures/ui/ImageSet/LuaApp/img_set_3x_1.png new file mode 100644 index 0000000..f028f03 Binary files /dev/null and b/Client2018/content/textures/ui/ImageSet/LuaApp/img_set_3x_1.png differ diff --git a/Client2018/content/textures/ui/Input/DashedLine.png b/Client2018/content/textures/ui/Input/DashedLine.png new file mode 100644 index 0000000..f9e45e3 Binary files /dev/null and b/Client2018/content/textures/ui/Input/DashedLine.png differ diff --git a/Client2018/content/textures/ui/Input/DashedLine90.png b/Client2018/content/textures/ui/Input/DashedLine90.png new file mode 100644 index 0000000..59ae6c7 Binary files /dev/null and b/Client2018/content/textures/ui/Input/DashedLine90.png differ diff --git a/Client2018/content/textures/ui/Input/Disk_padded.png b/Client2018/content/textures/ui/Input/Disk_padded.png new file mode 100644 index 0000000..ccbda80 Binary files /dev/null and b/Client2018/content/textures/ui/Input/Disk_padded.png differ diff --git a/Client2018/content/textures/ui/Input/IntroCamera.png b/Client2018/content/textures/ui/Input/IntroCamera.png new file mode 100644 index 0000000..bcf3f9c Binary files /dev/null and b/Client2018/content/textures/ui/Input/IntroCamera.png differ diff --git a/Client2018/content/textures/ui/Input/IntroCameraPinch.png b/Client2018/content/textures/ui/Input/IntroCameraPinch.png new file mode 100644 index 0000000..9521250 Binary files /dev/null and b/Client2018/content/textures/ui/Input/IntroCameraPinch.png differ diff --git a/Client2018/content/textures/ui/Input/IntroMove.png b/Client2018/content/textures/ui/Input/IntroMove.png new file mode 100644 index 0000000..a435304 Binary files /dev/null and b/Client2018/content/textures/ui/Input/IntroMove.png differ diff --git a/Client2018/content/textures/ui/Input/IntroMove@2x.png b/Client2018/content/textures/ui/Input/IntroMove@2x.png new file mode 100644 index 0000000..1ce8682 Binary files /dev/null and b/Client2018/content/textures/ui/Input/IntroMove@2x.png differ diff --git a/Client2018/content/textures/ui/Input/Ring_padded.png b/Client2018/content/textures/ui/Input/Ring_padded.png new file mode 100644 index 0000000..54cd6eb Binary files /dev/null and b/Client2018/content/textures/ui/Input/Ring_padded.png differ diff --git a/Client2018/content/textures/ui/Input/TouchControlsSheetV2.png b/Client2018/content/textures/ui/Input/TouchControlsSheetV2.png new file mode 100644 index 0000000..1999108 Binary files /dev/null and b/Client2018/content/textures/ui/Input/TouchControlsSheetV2.png differ diff --git a/Client2018/content/textures/ui/Keyboard/close_button_background.png b/Client2018/content/textures/ui/Keyboard/close_button_background.png new file mode 100644 index 0000000..ceec973 Binary files /dev/null and b/Client2018/content/textures/ui/Keyboard/close_button_background.png differ diff --git a/Client2018/content/textures/ui/Keyboard/close_button_icon.png b/Client2018/content/textures/ui/Keyboard/close_button_icon.png new file mode 100644 index 0000000..bb306ad Binary files /dev/null and b/Client2018/content/textures/ui/Keyboard/close_button_icon.png differ diff --git a/Client2018/content/textures/ui/Keyboard/close_button_selection.png b/Client2018/content/textures/ui/Keyboard/close_button_selection.png new file mode 100644 index 0000000..87bbfed Binary files /dev/null and b/Client2018/content/textures/ui/Keyboard/close_button_selection.png differ diff --git a/Client2018/content/textures/ui/Keyboard/key_selection_9slice.png b/Client2018/content/textures/ui/Keyboard/key_selection_9slice.png new file mode 100644 index 0000000..9c6fc50 Binary files /dev/null and b/Client2018/content/textures/ui/Keyboard/key_selection_9slice.png differ diff --git a/Client2018/content/textures/ui/Keyboard/mic_icon.png b/Client2018/content/textures/ui/Keyboard/mic_icon.png new file mode 100644 index 0000000..7e09484 Binary files /dev/null and b/Client2018/content/textures/ui/Keyboard/mic_icon.png differ diff --git a/Client2018/content/textures/ui/LoadingBKG.png b/Client2018/content/textures/ui/LoadingBKG.png new file mode 100644 index 0000000..2fc377d Binary files /dev/null and b/Client2018/content/textures/ui/LoadingBKG.png differ diff --git a/Client2018/content/textures/ui/LoadingScreen/BackgroundDark.png b/Client2018/content/textures/ui/LoadingScreen/BackgroundDark.png new file mode 100644 index 0000000..6df6d97 Binary files /dev/null and b/Client2018/content/textures/ui/LoadingScreen/BackgroundDark.png differ diff --git a/Client2018/content/textures/ui/LoadingScreen/BackgroundLight.png b/Client2018/content/textures/ui/LoadingScreen/BackgroundLight.png new file mode 100644 index 0000000..d1b2701 Binary files /dev/null and b/Client2018/content/textures/ui/LoadingScreen/BackgroundLight.png differ diff --git a/Client2018/content/textures/ui/LoadingScreen/LoadingSpinner.png b/Client2018/content/textures/ui/LoadingScreen/LoadingSpinner.png new file mode 100644 index 0000000..b577d8e Binary files /dev/null and b/Client2018/content/textures/ui/LoadingScreen/LoadingSpinner.png differ diff --git a/Client2018/content/textures/ui/Lobby/Buttons/glow_nine_slice.png b/Client2018/content/textures/ui/Lobby/Buttons/glow_nine_slice.png new file mode 100644 index 0000000..f67905e Binary files /dev/null and b/Client2018/content/textures/ui/Lobby/Buttons/glow_nine_slice.png differ diff --git a/Client2018/content/textures/ui/Lobby/Buttons/more_nine_slice_button.png b/Client2018/content/textures/ui/Lobby/Buttons/more_nine_slice_button.png new file mode 100644 index 0000000..7e287ed Binary files /dev/null and b/Client2018/content/textures/ui/Lobby/Buttons/more_nine_slice_button.png differ diff --git a/Client2018/content/textures/ui/Lobby/Buttons/nine_slice_button.png b/Client2018/content/textures/ui/Lobby/Buttons/nine_slice_button.png new file mode 100644 index 0000000..adf36ae Binary files /dev/null and b/Client2018/content/textures/ui/Lobby/Buttons/nine_slice_button.png differ diff --git a/Client2018/content/textures/ui/Lobby/Buttons/scroll_button.png b/Client2018/content/textures/ui/Lobby/Buttons/scroll_button.png new file mode 100644 index 0000000..e40ed8c Binary files /dev/null and b/Client2018/content/textures/ui/Lobby/Buttons/scroll_button.png differ diff --git a/Client2018/content/textures/ui/Lobby/Buttons/scroll_down.png b/Client2018/content/textures/ui/Lobby/Buttons/scroll_down.png new file mode 100644 index 0000000..b7ca49b Binary files /dev/null and b/Client2018/content/textures/ui/Lobby/Buttons/scroll_down.png differ diff --git a/Client2018/content/textures/ui/Lobby/Buttons/scroll_left.png b/Client2018/content/textures/ui/Lobby/Buttons/scroll_left.png new file mode 100644 index 0000000..1ad66bb Binary files /dev/null and b/Client2018/content/textures/ui/Lobby/Buttons/scroll_left.png differ diff --git a/Client2018/content/textures/ui/Lobby/Buttons/scroll_right.png b/Client2018/content/textures/ui/Lobby/Buttons/scroll_right.png new file mode 100644 index 0000000..7c2fa78 Binary files /dev/null and b/Client2018/content/textures/ui/Lobby/Buttons/scroll_right.png differ diff --git a/Client2018/content/textures/ui/Lobby/Buttons/scroll_up.png b/Client2018/content/textures/ui/Lobby/Buttons/scroll_up.png new file mode 100644 index 0000000..c411535 Binary files /dev/null and b/Client2018/content/textures/ui/Lobby/Buttons/scroll_up.png differ diff --git a/Client2018/content/textures/ui/Lobby/Icons/back_icon.png b/Client2018/content/textures/ui/Lobby/Icons/back_icon.png new file mode 100644 index 0000000..501a503 Binary files /dev/null and b/Client2018/content/textures/ui/Lobby/Icons/back_icon.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px-pressed.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px-pressed.png new file mode 100644 index 0000000..935ab7b Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px-pressed.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px-pressed@2x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px-pressed@2x.png new file mode 100644 index 0000000..cdfe139 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px-pressed@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px-pressed@3x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px-pressed@3x.png new file mode 100644 index 0000000..6464926 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px-pressed@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px.png new file mode 100644 index 0000000..9219b54 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@2x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@2x.png new file mode 100644 index 0000000..74ee0db Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@3x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@3x.png new file mode 100644 index 0000000..63a2428 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px-pressed.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px-pressed.png new file mode 100644 index 0000000..6b57943 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px-pressed.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px-pressed@2x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px-pressed@2x.png new file mode 100644 index 0000000..35ce93e Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px-pressed@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px-pressed@3x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px-pressed@3x.png new file mode 100644 index 0000000..628ce50 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px-pressed@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px.png new file mode 100644 index 0000000..afaf307 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px@2x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px@2x.png new file mode 100644 index 0000000..c3738c6 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px@3x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px@3x.png new file mode 100644 index 0000000..88745ae Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-btn-white-3px@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-capsule-circle.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-capsule-circle.png new file mode 100644 index 0000000..ec4cc6b Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-capsule-circle.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-capsule-circle@2x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-capsule-circle@2x.png new file mode 100644 index 0000000..3f8602e Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-capsule-circle@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-capsule-circle@3x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-capsule-circle@3x.png new file mode 100644 index 0000000..d6bb42b Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-capsule-circle@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-loading-indicator.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-loading-indicator.png new file mode 100644 index 0000000..3f2bed0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-loading-indicator.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-loading-indicator@2x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-loading-indicator@2x.png new file mode 100644 index 0000000..92be6e5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-loading-indicator@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-loading-indicator@3x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-loading-indicator@3x.png new file mode 100644 index 0000000..cc157ad Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-loading-indicator@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-search.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-search.png new file mode 100644 index 0000000..323c743 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-search.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-search@2x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-search@2x.png new file mode 100644 index 0000000..408c7c0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-search@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-search@3x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-search@3x.png new file mode 100644 index 0000000..3c30b7c Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-search@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-shadow.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-shadow.png new file mode 100644 index 0000000..15b9412 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-shadow.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-shadow@2x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-shadow@2x.png new file mode 100644 index 0000000..c2ec7eb Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-shadow@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/gr-shadow@3x.png b/Client2018/content/textures/ui/LuaApp/9-slice/gr-shadow@3x.png new file mode 100644 index 0000000..2cda209 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/gr-shadow@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/input-default.png b/Client2018/content/textures/ui/LuaApp/9-slice/input-default.png new file mode 100644 index 0000000..a1e3247 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/input-default.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/input-default@2x.png b/Client2018/content/textures/ui/LuaApp/9-slice/input-default@2x.png new file mode 100644 index 0000000..bad7c52 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/input-default@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/input-default@3x.png b/Client2018/content/textures/ui/LuaApp/9-slice/input-default@3x.png new file mode 100644 index 0000000..e92e175 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/input-default@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/white_button_9slices.png b/Client2018/content/textures/ui/LuaApp/9-slice/white_button_9slices.png new file mode 100644 index 0000000..70b102b Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/white_button_9slices.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/white_button_9slices@2x.png b/Client2018/content/textures/ui/LuaApp/9-slice/white_button_9slices@2x.png new file mode 100644 index 0000000..cee0a86 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/white_button_9slices@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/9-slice/white_button_9slices@3x.png b/Client2018/content/textures/ui/LuaApp/9-slice/white_button_9slices@3x.png new file mode 100644 index 0000000..0ac6f5c Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/9-slice/white_button_9slices@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-bc.png b/Client2018/content/textures/ui/LuaApp/category/ic-bc.png new file mode 100644 index 0000000..3877fb6 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-bc.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-bc@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-bc@2x.png new file mode 100644 index 0000000..b4ca553 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-bc@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-bc@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-bc@3x.png new file mode 100644 index 0000000..37afee5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-bc@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-cancel.png b/Client2018/content/textures/ui/LuaApp/category/ic-cancel.png new file mode 100644 index 0000000..c9d92d0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-cancel.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-cancel@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-cancel@2x.png new file mode 100644 index 0000000..bdcdf27 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-cancel@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-cancel@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-cancel@3x.png new file mode 100644 index 0000000..86f9a7b Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-cancel@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-default.png b/Client2018/content/textures/ui/LuaApp/category/ic-default.png new file mode 100644 index 0000000..1394221 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-default.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-default@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-default@2x.png new file mode 100644 index 0000000..44ed264 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-default@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-default@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-default@3x.png new file mode 100644 index 0000000..468a8ea Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-default@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-featured.png b/Client2018/content/textures/ui/LuaApp/category/ic-featured.png new file mode 100644 index 0000000..716f358 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-featured.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-featured@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-featured@2x.png new file mode 100644 index 0000000..a03cced Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-featured@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-featured@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-featured@3x.png new file mode 100644 index 0000000..712b018 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-featured@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-friend activity.png b/Client2018/content/textures/ui/LuaApp/category/ic-friend activity.png new file mode 100644 index 0000000..3dd503f Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-friend activity.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-friend activity@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-friend activity@2x.png new file mode 100644 index 0000000..5de2926 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-friend activity@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-friend activity@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-friend activity@3x.png new file mode 100644 index 0000000..315325a Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-friend activity@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-my favorite.png b/Client2018/content/textures/ui/LuaApp/category/ic-my favorite.png new file mode 100644 index 0000000..154fe4b Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-my favorite.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-my favorite@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-my favorite@2x.png new file mode 100644 index 0000000..58a148d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-my favorite@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-my favorite@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-my favorite@3x.png new file mode 100644 index 0000000..9eab9f1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-my favorite@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-my recent.png b/Client2018/content/textures/ui/LuaApp/category/ic-my recent.png new file mode 100644 index 0000000..8de756b Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-my recent.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-my recent@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-my recent@2x.png new file mode 100644 index 0000000..30c4466 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-my recent@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-my recent@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-my recent@3x.png new file mode 100644 index 0000000..b8e3dbb Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-my recent@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-popular in VR.png b/Client2018/content/textures/ui/LuaApp/category/ic-popular in VR.png new file mode 100644 index 0000000..a1fc643 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-popular in VR.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-popular in VR@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-popular in VR@2x.png new file mode 100644 index 0000000..9ca2410 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-popular in VR@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-popular in VR@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-popular in VR@3x.png new file mode 100644 index 0000000..3d5581b Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-popular in VR@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-popular in country.png b/Client2018/content/textures/ui/LuaApp/category/ic-popular in country.png new file mode 100644 index 0000000..10dd77a Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-popular in country.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-popular in country@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-popular in country@2x.png new file mode 100644 index 0000000..154bfcd Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-popular in country@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-popular in country@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-popular in country@3x.png new file mode 100644 index 0000000..cad0d22 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-popular in country@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-popular.png b/Client2018/content/textures/ui/LuaApp/category/ic-popular.png new file mode 100644 index 0000000..74becef Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-popular.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-popular@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-popular@2x.png new file mode 100644 index 0000000..bcf1fa1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-popular@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-popular@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-popular@3x.png new file mode 100644 index 0000000..cd02edf Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-popular@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-purchased.png b/Client2018/content/textures/ui/LuaApp/category/ic-purchased.png new file mode 100644 index 0000000..5a0653d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-purchased.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-purchased@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-purchased@2x.png new file mode 100644 index 0000000..b28f8af Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-purchased@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-purchased@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-purchased@3x.png new file mode 100644 index 0000000..6dd9b17 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-purchased@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-recommended.png b/Client2018/content/textures/ui/LuaApp/category/ic-recommended.png new file mode 100644 index 0000000..8749c0a Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-recommended.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-recommended@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-recommended@2x.png new file mode 100644 index 0000000..a1f953f Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-recommended@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-recommended@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-recommended@3x.png new file mode 100644 index 0000000..de4be52 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-recommended@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-top earning.png b/Client2018/content/textures/ui/LuaApp/category/ic-top earning.png new file mode 100644 index 0000000..6f47270 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-top earning.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-top earning@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-top earning@2x.png new file mode 100644 index 0000000..e11a731 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-top earning@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-top earning@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-top earning@3x.png new file mode 100644 index 0000000..cfb3188 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-top earning@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-top favorite.png b/Client2018/content/textures/ui/LuaApp/category/ic-top favorite.png new file mode 100644 index 0000000..6484b5f Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-top favorite.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-top favorite@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-top favorite@2x.png new file mode 100644 index 0000000..2fe5789 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-top favorite@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-top favorite@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-top favorite@3x.png new file mode 100644 index 0000000..52622f4 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-top favorite@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-top paid.png b/Client2018/content/textures/ui/LuaApp/category/ic-top paid.png new file mode 100644 index 0000000..e2b2a19 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-top paid.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-top paid@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-top paid@2x.png new file mode 100644 index 0000000..9d4a5bd Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-top paid@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-top paid@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-top paid@3x.png new file mode 100644 index 0000000..aecc6f4 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-top paid@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-top rated.png b/Client2018/content/textures/ui/LuaApp/category/ic-top rated.png new file mode 100644 index 0000000..1b6f08a Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-top rated.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-top rated@2x.png b/Client2018/content/textures/ui/LuaApp/category/ic-top rated@2x.png new file mode 100644 index 0000000..373be35 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-top rated@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/category/ic-top rated@3x.png b/Client2018/content/textures/ui/LuaApp/category/ic-top rated@3x.png new file mode 100644 index 0000000..6a7dde2 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/category/ic-top rated@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/dropdown/gr-contextual menu.png b/Client2018/content/textures/ui/LuaApp/dropdown/gr-contextual menu.png new file mode 100644 index 0000000..e8f097d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/dropdown/gr-contextual menu.png differ diff --git a/Client2018/content/textures/ui/LuaApp/dropdown/gr-contextual menu@2x.png b/Client2018/content/textures/ui/LuaApp/dropdown/gr-contextual menu@2x.png new file mode 100644 index 0000000..52172ff Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/dropdown/gr-contextual menu@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/dropdown/gr-contextual menu@3x.png b/Client2018/content/textures/ui/LuaApp/dropdown/gr-contextual menu@3x.png new file mode 100644 index 0000000..a26a828 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/dropdown/gr-contextual menu@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-down.png b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-down.png new file mode 100644 index 0000000..1270180 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-down.png differ diff --git a/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-down@2x.png b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-down@2x.png new file mode 100644 index 0000000..8f5cacc Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-down@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-down@3x.png b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-down@3x.png new file mode 100644 index 0000000..72f0811 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-down@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-left.png b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-left.png new file mode 100644 index 0000000..c8091a7 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-left.png differ diff --git a/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-left@2x.png b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-left@2x.png new file mode 100644 index 0000000..ee07594 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-left@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-left@3x.png b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-left@3x.png new file mode 100644 index 0000000..d0a28d4 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-left@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-right.png b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-right.png new file mode 100644 index 0000000..93d796e Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-right.png differ diff --git a/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-right@2x.png b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-right@2x.png new file mode 100644 index 0000000..ded858d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-right@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-right@3x.png b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-right@3x.png new file mode 100644 index 0000000..3d20dcf Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-right@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-up.png b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-up.png new file mode 100644 index 0000000..73f833a Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-up.png differ diff --git a/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-up@2x.png b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-up@2x.png new file mode 100644 index 0000000..62314dd Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-up@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-up@3x.png b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-up@3x.png new file mode 100644 index 0000000..a9c91b2 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/dropdown/gr-tip-up@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/bkg.png b/Client2018/content/textures/ui/LuaApp/graphic/bkg.png new file mode 100644 index 0000000..628f48d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/bkg.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/bkg@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/bkg@2x.png new file mode 100644 index 0000000..a2b91ac Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/bkg@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-add.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-add.png new file mode 100644 index 0000000..c863bce Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-add.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-add@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-add@2x.png new file mode 100644 index 0000000..7f8368f Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-add@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-add@3x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-add@3x.png new file mode 100644 index 0000000..b8a5b30 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-add@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-84x84.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-84x84.png new file mode 100644 index 0000000..1ac156d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-84x84.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@2x.png new file mode 100644 index 0000000..97f6bda Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@3x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@3x.png new file mode 100644 index 0000000..a167ba0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-90x90.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-90x90.png new file mode 100644 index 0000000..ac423bb Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-90x90.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@2x.png new file mode 100644 index 0000000..2e44709 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@3x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@3x.png new file mode 100644 index 0000000..1cf1cfa Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36.png new file mode 100644 index 0000000..3879628 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@2x.png new file mode 100644 index 0000000..952a375 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@3x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@3x.png new file mode 100644 index 0000000..baa0910 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-bloom-circle.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-bloom-circle.png new file mode 100644 index 0000000..5a41676 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-bloom-circle.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-bloom-circle@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-bloom-circle@2x.png new file mode 100644 index 0000000..e4347f1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-bloom-circle@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-bloom-circle@3x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-bloom-circle@3x.png new file mode 100644 index 0000000..437fd5c Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-bloom-circle@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-card.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-card.png new file mode 100644 index 0000000..4d02f21 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-card.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-card@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-card@2x.png new file mode 100644 index 0000000..0a0b1fe Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-card@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-card@3x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-card@3x.png new file mode 100644 index 0000000..cd78323 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-card@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-counter-slot-32x32.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-counter-slot-32x32.png new file mode 100644 index 0000000..eae8762 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-counter-slot-32x32.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-counter-slot-32x32@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-counter-slot-32x32@2x.png new file mode 100644 index 0000000..466a94c Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-counter-slot-32x32@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-counter-slot-32x32@3x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-counter-slot-32x32@3x.png new file mode 100644 index 0000000..37eb5ca Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-counter-slot-32x32@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-friend-on.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-friend-on.png new file mode 100644 index 0000000..a52299e Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-friend-on.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-friend-on@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-friend-on@2x.png new file mode 100644 index 0000000..c11184d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-friend-on@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-friend-on@3x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-friend-on@3x.png new file mode 100644 index 0000000..1ff2a7b Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-friend-on@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-friend.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-friend.png new file mode 100644 index 0000000..fa94313 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-friend.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-friend@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-friend@2x.png new file mode 100644 index 0000000..f8aac14 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-friend@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-friend@3x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-friend@3x.png new file mode 100644 index 0000000..09927c6 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-friend@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-notification badge.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-notification badge.png new file mode 100644 index 0000000..ca7d97d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-notification badge.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-notification badge@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-notification badge@2x.png new file mode 100644 index 0000000..6548bc0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-notification badge@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-notification badge@3x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-notification badge@3x.png new file mode 100644 index 0000000..a09950e Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-notification badge@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-play-circle.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-play-circle.png new file mode 100644 index 0000000..9350c52 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-play-circle.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-play-circle@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-play-circle@2x.png new file mode 100644 index 0000000..cb5fdcd Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-play-circle@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-play-circle@3x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-play-circle@3x.png new file mode 100644 index 0000000..3190433 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-play-circle@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-150x150px.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-150x150px.png new file mode 100644 index 0000000..3608a54 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-150x150px.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-150x150px@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-150x150px@2x.png new file mode 100644 index 0000000..3ca1bc2 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-150x150px@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-150x150px@3x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-150x150px@3x.png new file mode 100644 index 0000000..cadf11a Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-150x150px@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-border-36x36.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-border-36x36.png new file mode 100644 index 0000000..ecab811 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-border-36x36.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-border-36x36@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-border-36x36@2x.png new file mode 100644 index 0000000..03b94cc Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-border-36x36@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-border-36x36@3x.png b/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-border-36x36@3x.png new file mode 100644 index 0000000..b0d22fe Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/gr-profile-border-36x36@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/noNetworkConnection.png b/Client2018/content/textures/ui/LuaApp/graphic/noNetworkConnection.png new file mode 100644 index 0000000..9c270b2 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/noNetworkConnection.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/noNetworkConnection@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/noNetworkConnection@2x.png new file mode 100644 index 0000000..03c37c8 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/noNetworkConnection@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/noNetworkConnection@3x.png b/Client2018/content/textures/ui/LuaApp/graphic/noNetworkConnection@3x.png new file mode 100644 index 0000000..9379cf3 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/noNetworkConnection@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/ph-avatar-portrait.png b/Client2018/content/textures/ui/LuaApp/graphic/ph-avatar-portrait.png new file mode 100644 index 0000000..d3091db Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/ph-avatar-portrait.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/ph-avatar-portrait@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/ph-avatar-portrait@2x.png new file mode 100644 index 0000000..a537692 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/ph-avatar-portrait@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/ph-avatar-portrait@3x.png b/Client2018/content/textures/ui/LuaApp/graphic/ph-avatar-portrait@3x.png new file mode 100644 index 0000000..6c3298f Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/ph-avatar-portrait@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/shimmer.png b/Client2018/content/textures/ui/LuaApp/graphic/shimmer.png new file mode 100644 index 0000000..2fd648e Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/shimmer.png differ diff --git a/Client2018/content/textures/ui/LuaApp/graphic/shimmer@2x.png b/Client2018/content/textures/ui/LuaApp/graphic/shimmer@2x.png new file mode 100644 index 0000000..3e8a1f0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/graphic/shimmer@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-ROBUX.png b/Client2018/content/textures/ui/LuaApp/icons/ic-ROBUX.png new file mode 100644 index 0000000..7d690e8 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-ROBUX.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-ROBUX@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-ROBUX@2x.png new file mode 100644 index 0000000..6d88c27 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-ROBUX@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-ROBUX@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-ROBUX@3x.png new file mode 100644 index 0000000..16efb3c Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-ROBUX@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-add-down.png b/Client2018/content/textures/ui/LuaApp/icons/ic-add-down.png new file mode 100644 index 0000000..0a3bf2b Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-add-down.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-add-down@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-add-down@2x.png new file mode 100644 index 0000000..5dcd81b Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-add-down@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-add-down@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-add-down@3x.png new file mode 100644 index 0000000..80ca4f4 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-add-down@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-add.png b/Client2018/content/textures/ui/LuaApp/icons/ic-add.png new file mode 100644 index 0000000..649ae8f Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-add.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-add@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-add@2x.png new file mode 100644 index 0000000..77e1f58 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-add@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-add@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-add@3x.png new file mode 100644 index 0000000..2b12c24 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-add@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-down.png b/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-down.png new file mode 100644 index 0000000..a711550 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-down.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-down@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-down@2x.png new file mode 100644 index 0000000..36ac1d3 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-down@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-down@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-down@3x.png new file mode 100644 index 0000000..112080d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-down@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-right.png b/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-right.png new file mode 100644 index 0000000..c490b34 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-right.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-right@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-right@2x.png new file mode 100644 index 0000000..5ceb151 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-right@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-right@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-right@3x.png new file mode 100644 index 0000000..256287d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-arrow-right@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-avatar-on.png b/Client2018/content/textures/ui/LuaApp/icons/ic-avatar-on.png new file mode 100644 index 0000000..e43d2d1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-avatar-on.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-avatar-on@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-avatar-on@2x.png new file mode 100644 index 0000000..141c91e Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-avatar-on@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-avatar-on@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-avatar-on@3x.png new file mode 100644 index 0000000..f43fb54 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-avatar-on@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-avatar.png b/Client2018/content/textures/ui/LuaApp/icons/ic-avatar.png new file mode 100644 index 0000000..b1d9812 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-avatar.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-avatar@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-avatar@2x.png new file mode 100644 index 0000000..618e1bb Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-avatar@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-avatar@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-avatar@3x.png new file mode 100644 index 0000000..1ed77b2 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-avatar@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-back.png b/Client2018/content/textures/ui/LuaApp/icons/ic-back.png new file mode 100644 index 0000000..15ac578 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-back.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-back@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-back@2x.png new file mode 100644 index 0000000..0cc111a Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-back@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-back@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-back@3x.png new file mode 100644 index 0000000..7b3e002 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-back@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-bc-small.png b/Client2018/content/textures/ui/LuaApp/icons/ic-bc-small.png new file mode 100644 index 0000000..001177e Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-bc-small.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-bc-small@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-bc-small@2x.png new file mode 100644 index 0000000..e1aa587 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-bc-small@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-bc-small@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-bc-small@3x.png new file mode 100644 index 0000000..69641dc Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-bc-small@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-bc.png b/Client2018/content/textures/ui/LuaApp/icons/ic-bc.png new file mode 100644 index 0000000..88136ed Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-bc.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-bc@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-bc@2x.png new file mode 100644 index 0000000..b3efa46 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-bc@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-bc@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-bc@3x.png new file mode 100644 index 0000000..1cd43b0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-bc@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-blue-arrow.png b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-arrow.png new file mode 100644 index 0000000..0990a7d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-arrow.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-blue-arrow@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-arrow@2x.png new file mode 100644 index 0000000..6168c51 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-arrow@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-blue-arrow@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-arrow@3x.png new file mode 100644 index 0000000..1e551f2 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-arrow@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-blue-dot.png b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-dot.png new file mode 100644 index 0000000..2b1bbec Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-dot.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-blue-dot@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-dot@2x.png new file mode 100644 index 0000000..f7987d6 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-dot@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-blue-dot@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-dot@3x.png new file mode 100644 index 0000000..ffe172d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-dot@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-blue-online.png b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-online.png new file mode 100644 index 0000000..48d0e73 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-online.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-blue-online@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-online@2x.png new file mode 100644 index 0000000..16a3035 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-online@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-blue-online@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-online@3x.png new file mode 100644 index 0000000..9fdb8f8 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-blue-online@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-catalog-on.png b/Client2018/content/textures/ui/LuaApp/icons/ic-catalog-on.png new file mode 100644 index 0000000..8098e30 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-catalog-on.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-catalog-on@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-catalog-on@2x.png new file mode 100644 index 0000000..05844f0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-catalog-on@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-catalog-on@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-catalog-on@3x.png new file mode 100644 index 0000000..cbd43f7 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-catalog-on@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-catalog.png b/Client2018/content/textures/ui/LuaApp/icons/ic-catalog.png new file mode 100644 index 0000000..685d424 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-catalog.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-catalog@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-catalog@2x.png new file mode 100644 index 0000000..520649e Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-catalog@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-catalog@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-catalog@3x.png new file mode 100644 index 0000000..af341f7 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-catalog@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-chat-on.png b/Client2018/content/textures/ui/LuaApp/icons/ic-chat-on.png new file mode 100644 index 0000000..917e770 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-chat-on.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-chat-on@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-chat-on@2x.png new file mode 100644 index 0000000..11be8d6 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-chat-on@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-chat-on@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-chat-on@3x.png new file mode 100644 index 0000000..d0cdd33 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-chat-on@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-chat.png b/Client2018/content/textures/ui/LuaApp/icons/ic-chat.png new file mode 100644 index 0000000..33bc7da Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-chat.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-chat20x20.png b/Client2018/content/textures/ui/LuaApp/icons/ic-chat20x20.png new file mode 100644 index 0000000..577160d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-chat20x20.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-chat20x20@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-chat20x20@2x.png new file mode 100644 index 0000000..47429b9 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-chat20x20@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-chat20x20@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-chat20x20@3x.png new file mode 100644 index 0000000..c5643d2 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-chat20x20@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-chat@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-chat@2x.png new file mode 100644 index 0000000..cac93c4 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-chat@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-chat@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-chat@3x.png new file mode 100644 index 0000000..14476b7 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-chat@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-clear.png b/Client2018/content/textures/ui/LuaApp/icons/ic-clear.png new file mode 100644 index 0000000..5b76be5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-clear.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-clear@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-clear@2x.png new file mode 100644 index 0000000..15cc996 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-clear@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-clear@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-clear@3x.png new file mode 100644 index 0000000..ae72f99 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-clear@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-facebook.png b/Client2018/content/textures/ui/LuaApp/icons/ic-facebook.png new file mode 100644 index 0000000..078d622 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-facebook.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-favorite-filled.png b/Client2018/content/textures/ui/LuaApp/icons/ic-favorite-filled.png new file mode 100644 index 0000000..8fa0b77 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-favorite-filled.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-favorite-filled@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-favorite-filled@2x.png new file mode 100644 index 0000000..9419d89 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-favorite-filled@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-favorite.png b/Client2018/content/textures/ui/LuaApp/icons/ic-favorite.png new file mode 100644 index 0000000..fd51ec1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-favorite.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-favorite@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-favorite@2x.png new file mode 100644 index 0000000..ef6f41b Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-favorite@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-friend-on.png b/Client2018/content/textures/ui/LuaApp/icons/ic-friend-on.png new file mode 100644 index 0000000..e52eab2 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-friend-on.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-friend-on@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-friend-on@2x.png new file mode 100644 index 0000000..8942e4a Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-friend-on@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-friend-on@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-friend-on@3x.png new file mode 100644 index 0000000..3e69033 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-friend-on@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-friend.png b/Client2018/content/textures/ui/LuaApp/icons/ic-friend.png new file mode 100644 index 0000000..93b6dc6 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-friend.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-friend@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-friend@2x.png new file mode 100644 index 0000000..cfb5e10 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-friend@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-friend@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-friend@3x.png new file mode 100644 index 0000000..d8cbbc9 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-friend@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-game.png b/Client2018/content/textures/ui/LuaApp/icons/ic-game.png new file mode 100644 index 0000000..b1b1b9a Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-game.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-game@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-game@2x.png new file mode 100644 index 0000000..253963b Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-game@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-game@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-game@3x.png new file mode 100644 index 0000000..e488025 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-game@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-games-on.png b/Client2018/content/textures/ui/LuaApp/icons/ic-games-on.png new file mode 100644 index 0000000..bb6f4cc Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-games-on.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-games-on@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-games-on@2x.png new file mode 100644 index 0000000..4b97f66 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-games-on@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-games-on@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-games-on@3x.png new file mode 100644 index 0000000..12293e0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-games-on@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-games.png b/Client2018/content/textures/ui/LuaApp/icons/ic-games.png new file mode 100644 index 0000000..8d60528 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-games.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-games@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-games@2x.png new file mode 100644 index 0000000..5efd3a2 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-games@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-games@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-games@3x.png new file mode 100644 index 0000000..e27091b Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-games@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-google.png b/Client2018/content/textures/ui/LuaApp/icons/ic-google.png new file mode 100644 index 0000000..c346524 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-google.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-gray-arrow.png b/Client2018/content/textures/ui/LuaApp/icons/ic-gray-arrow.png new file mode 100644 index 0000000..68c24e7 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-gray-arrow.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-gray-arrow@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-gray-arrow@2x.png new file mode 100644 index 0000000..d3dbaad Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-gray-arrow@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-gray-arrow@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-gray-arrow@3x.png new file mode 100644 index 0000000..6d7af72 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-gray-arrow@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-green-dot.png b/Client2018/content/textures/ui/LuaApp/icons/ic-green-dot.png new file mode 100644 index 0000000..de85ab0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-green-dot.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-green-dot@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-green-dot@2x.png new file mode 100644 index 0000000..90f071d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-green-dot@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-green-dot@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-green-dot@3x.png new file mode 100644 index 0000000..208af03 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-green-dot@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-green-ingame.png b/Client2018/content/textures/ui/LuaApp/icons/ic-green-ingame.png new file mode 100644 index 0000000..c81a6c1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-green-ingame.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-green-ingame@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-green-ingame@2x.png new file mode 100644 index 0000000..41a9db2 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-green-ingame@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-green-ingame@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-green-ingame@3x.png new file mode 100644 index 0000000..788502c Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-green-ingame@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-home-on.png b/Client2018/content/textures/ui/LuaApp/icons/ic-home-on.png new file mode 100644 index 0000000..98b3821 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-home-on.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-home-on@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-home-on@2x.png new file mode 100644 index 0000000..765d45f Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-home-on@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-home-on@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-home-on@3x.png new file mode 100644 index 0000000..b7110ff Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-home-on@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-home.png b/Client2018/content/textures/ui/LuaApp/icons/ic-home.png new file mode 100644 index 0000000..5f29669 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-home.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-home@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-home@2x.png new file mode 100644 index 0000000..67459ba Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-home@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-home@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-home@3x.png new file mode 100644 index 0000000..219941c Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-home@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-about.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-about.png new file mode 100644 index 0000000..9f15260 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-about.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-about@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-about@2x.png new file mode 100644 index 0000000..27a89a7 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-about@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-about@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-about@3x.png new file mode 100644 index 0000000..ba4742d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-about@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-blog.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-blog.png new file mode 100644 index 0000000..cabd391 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-blog.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-blog@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-blog@2x.png new file mode 100644 index 0000000..84d3181 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-blog@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-blog@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-blog@3x.png new file mode 100644 index 0000000..9a71c4f Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-blog@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-builders-club.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-builders-club.png new file mode 100644 index 0000000..a729363 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-builders-club.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-builders-club@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-builders-club@2x.png new file mode 100644 index 0000000..15ab42e Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-builders-club@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-builders-club@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-builders-club@3x.png new file mode 100644 index 0000000..ecbfe67 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-builders-club@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-catalog.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-catalog.png new file mode 100644 index 0000000..d6d740c Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-catalog.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-catalog@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-catalog@2x.png new file mode 100644 index 0000000..10f0c09 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-catalog@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-catalog@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-catalog@3x.png new file mode 100644 index 0000000..a2b13c6 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-catalog@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-create.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-create.png new file mode 100644 index 0000000..7db7b0a Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-create.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-create@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-create@2x.png new file mode 100644 index 0000000..ad53a89 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-create@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-create@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-create@3x.png new file mode 100644 index 0000000..05498c8 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-create@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-events.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-events.png new file mode 100644 index 0000000..a586238 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-events.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-events@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-events@2x.png new file mode 100644 index 0000000..0940fc0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-events@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-events@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-events@3x.png new file mode 100644 index 0000000..a76e24f Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-events@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-friends.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-friends.png new file mode 100644 index 0000000..75366b7 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-friends.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-friends@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-friends@2x.png new file mode 100644 index 0000000..3629480 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-friends@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-friends@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-friends@3x.png new file mode 100644 index 0000000..91e86f5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-friends@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-groups.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-groups.png new file mode 100644 index 0000000..b07c7b9 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-groups.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-groups@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-groups@2x.png new file mode 100644 index 0000000..1e111ac Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-groups@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-groups@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-groups@3x.png new file mode 100644 index 0000000..412bf34 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-groups@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-help.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-help.png new file mode 100644 index 0000000..bbe33d6 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-help.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-help@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-help@2x.png new file mode 100644 index 0000000..8529801 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-help@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-help@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-help@3x.png new file mode 100644 index 0000000..6f569db Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-help@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-inventory.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-inventory.png new file mode 100644 index 0000000..6a7a91d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-inventory.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-inventory@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-inventory@2x.png new file mode 100644 index 0000000..df440ed Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-inventory@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-inventory@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-inventory@3x.png new file mode 100644 index 0000000..b622838 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-inventory@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-message.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-message.png new file mode 100644 index 0000000..deaef34 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-message.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-message@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-message@2x.png new file mode 100644 index 0000000..676fa23 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-message@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-message@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-message@3x.png new file mode 100644 index 0000000..0b3e1cb Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-message@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-on.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-on.png new file mode 100644 index 0000000..3cd2bb0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-on.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-on@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-on@2x.png new file mode 100644 index 0000000..8733a9c Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-on@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-on@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-on@3x.png new file mode 100644 index 0000000..8bdf1e5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-on@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-profile.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-profile.png new file mode 100644 index 0000000..86450b1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-profile.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-profile@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-profile@2x.png new file mode 100644 index 0000000..d204bff Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-profile@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-profile@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-profile@3x.png new file mode 100644 index 0000000..c570d88 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-profile@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-settings.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-settings.png new file mode 100644 index 0000000..7a0f9d1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-settings.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-settings@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-settings@2x.png new file mode 100644 index 0000000..03a254b Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-settings@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more-settings@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more-settings@3x.png new file mode 100644 index 0000000..375b1e5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more-settings@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more.png new file mode 100644 index 0000000..bad7231 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more@2x.png new file mode 100644 index 0000000..2f90207 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-more@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-more@3x.png new file mode 100644 index 0000000..2bc8325 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-more@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-notification.png b/Client2018/content/textures/ui/LuaApp/icons/ic-notification.png new file mode 100644 index 0000000..d520e19 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-notification.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-notification@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-notification@2x.png new file mode 100644 index 0000000..80e5165 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-notification@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-notification@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-notification@3x.png new file mode 100644 index 0000000..5f42460 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-notification@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-obc-small.png b/Client2018/content/textures/ui/LuaApp/icons/ic-obc-small.png new file mode 100644 index 0000000..7d460f7 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-obc-small.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-obc-small@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-obc-small@2x.png new file mode 100644 index 0000000..c650f77 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-obc-small@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-obc-small@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-obc-small@3x.png new file mode 100644 index 0000000..4b07abc Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-obc-small@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-obc.png b/Client2018/content/textures/ui/LuaApp/icons/ic-obc.png new file mode 100644 index 0000000..9cd21cd Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-obc.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-obc@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-obc@2x.png new file mode 100644 index 0000000..3901b32 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-obc@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-obc@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-obc@3x.png new file mode 100644 index 0000000..a6840ac Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-obc@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-orange-dot.png b/Client2018/content/textures/ui/LuaApp/icons/ic-orange-dot.png new file mode 100644 index 0000000..53520fc Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-orange-dot.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-orange-dot@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-orange-dot@2x.png new file mode 100644 index 0000000..e74f872 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-orange-dot@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-orange-dot@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-orange-dot@3x.png new file mode 100644 index 0000000..d09f935 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-orange-dot@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-orange-instudio.png b/Client2018/content/textures/ui/LuaApp/icons/ic-orange-instudio.png new file mode 100644 index 0000000..e055475 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-orange-instudio.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-orange-instudio@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-orange-instudio@2x.png new file mode 100644 index 0000000..c5db08f Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-orange-instudio@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-orange-instudio@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-orange-instudio@3x.png new file mode 100644 index 0000000..aadd900 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-orange-instudio@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-profile.png b/Client2018/content/textures/ui/LuaApp/icons/ic-profile.png new file mode 100644 index 0000000..fdf6f87 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-profile.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-profile@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-profile@2x.png new file mode 100644 index 0000000..8649c34 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-profile@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-profile@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-profile@3x.png new file mode 100644 index 0000000..bd3fe06 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-profile@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-search-gray.png b/Client2018/content/textures/ui/LuaApp/icons/ic-search-gray.png new file mode 100644 index 0000000..5d21cfe Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-search-gray.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-search-gray@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-search-gray@2x.png new file mode 100644 index 0000000..80441e1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-search-gray@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-search-gray@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-search-gray@3x.png new file mode 100644 index 0000000..0956ce1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-search-gray@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-search.png b/Client2018/content/textures/ui/LuaApp/icons/ic-search.png new file mode 100644 index 0000000..fefae0c Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-search.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-search@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-search@2x.png new file mode 100644 index 0000000..8585a75 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-search@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-search@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-search@3x.png new file mode 100644 index 0000000..ed9ff2f Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-search@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-tbc-small.png b/Client2018/content/textures/ui/LuaApp/icons/ic-tbc-small.png new file mode 100644 index 0000000..00e38c1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-tbc-small.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-tbc-small@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-tbc-small@2x.png new file mode 100644 index 0000000..83a95d1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-tbc-small@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-tbc-small@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-tbc-small@3x.png new file mode 100644 index 0000000..671613d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-tbc-small@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-tbc.png b/Client2018/content/textures/ui/LuaApp/icons/ic-tbc.png new file mode 100644 index 0000000..2e589d8 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-tbc.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-tbc@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-tbc@2x.png new file mode 100644 index 0000000..719eea6 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-tbc@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-tbc@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-tbc@3x.png new file mode 100644 index 0000000..064496c Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-tbc@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-thumb-down.png b/Client2018/content/textures/ui/LuaApp/icons/ic-thumb-down.png new file mode 100644 index 0000000..d36de77 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-thumb-down.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-thumb-down@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-thumb-down@2x.png new file mode 100644 index 0000000..5c5a883 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-thumb-down@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-down-filled.png b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-down-filled.png new file mode 100644 index 0000000..1976341 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-down-filled.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-down-filled@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-down-filled@2x.png new file mode 100644 index 0000000..d22246f Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-down-filled@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-down.png b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-down.png new file mode 100644 index 0000000..e433915 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-down.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-down@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-down@2x.png new file mode 100644 index 0000000..3a8d79d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-down@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-up-filled.png b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-up-filled.png new file mode 100644 index 0000000..f97cbdd Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-up-filled.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-up-filled@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-up-filled@2x.png new file mode 100644 index 0000000..cd79bf2 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-up-filled@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-up.png b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-up.png new file mode 100644 index 0000000..903de78 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-up.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-up@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-up@2x.png new file mode 100644 index 0000000..759ec68 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbs-up@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-thumbup.png b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbup.png new file mode 100644 index 0000000..1019621 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbup.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-thumbup@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbup@2x.png new file mode 100644 index 0000000..babd290 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbup@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-thumbup@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbup@3x.png new file mode 100644 index 0000000..df94820 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-thumbup@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-twitter.png b/Client2018/content/textures/ui/LuaApp/icons/ic-twitter.png new file mode 100644 index 0000000..5885087 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-twitter.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-view-details20x20.png b/Client2018/content/textures/ui/LuaApp/icons/ic-view-details20x20.png new file mode 100644 index 0000000..b8ec87e Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-view-details20x20.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-view-details20x20@2x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-view-details20x20@2x.png new file mode 100644 index 0000000..b5fa7e8 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-view-details20x20@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/ic-view-details20x20@3x.png b/Client2018/content/textures/ui/LuaApp/icons/ic-view-details20x20@3x.png new file mode 100644 index 0000000..f5f12f5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/ic-view-details20x20@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/icon_retry_white.png b/Client2018/content/textures/ui/LuaApp/icons/icon_retry_white.png new file mode 100644 index 0000000..6f7e8af Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/icon_retry_white.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/icon_retry_white@2x.png b/Client2018/content/textures/ui/LuaApp/icons/icon_retry_white@2x.png new file mode 100644 index 0000000..eae0f3d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/icon_retry_white@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/icons/icon_retry_white@3x.png b/Client2018/content/textures/ui/LuaApp/icons/icon_retry_white@3x.png new file mode 100644 index 0000000..d30d470 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/icons/icon_retry_white@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/voteBar/bar.png b/Client2018/content/textures/ui/LuaApp/voteBar/bar.png new file mode 100644 index 0000000..9328ab4 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/voteBar/bar.png differ diff --git a/Client2018/content/textures/ui/LuaApp/voteBar/bar@2x.png b/Client2018/content/textures/ui/LuaApp/voteBar/bar@2x.png new file mode 100644 index 0000000..5f6967f Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/voteBar/bar@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/voteBar/bar@3x.png b/Client2018/content/textures/ui/LuaApp/voteBar/bar@3x.png new file mode 100644 index 0000000..101c893 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/voteBar/bar@3x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/voteBar/thumbup.png b/Client2018/content/textures/ui/LuaApp/voteBar/thumbup.png new file mode 100644 index 0000000..47d1b9f Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/voteBar/thumbup.png differ diff --git a/Client2018/content/textures/ui/LuaApp/voteBar/thumbup@2x.png b/Client2018/content/textures/ui/LuaApp/voteBar/thumbup@2x.png new file mode 100644 index 0000000..98b3a1d Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/voteBar/thumbup@2x.png differ diff --git a/Client2018/content/textures/ui/LuaApp/voteBar/thumbup@3x.png b/Client2018/content/textures/ui/LuaApp/voteBar/thumbup@3x.png new file mode 100644 index 0000000..e1c7b32 Binary files /dev/null and b/Client2018/content/textures/ui/LuaApp/voteBar/thumbup@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/btn-control-sm.png b/Client2018/content/textures/ui/LuaChat/9-slice/btn-control-sm.png new file mode 100644 index 0000000..0623973 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/btn-control-sm.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-right.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-right.png new file mode 100644 index 0000000..b0a79f4 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-right.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-right@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-right@2x.png new file mode 100644 index 0000000..409dbfb Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-right@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-right@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-right@3x.png new file mode 100644 index 0000000..22ed017 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-right@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self-tip.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self-tip.png new file mode 100644 index 0000000..e00403f Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self-tip.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@2x.png new file mode 100644 index 0000000..cb4b777 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@3x.png new file mode 100644 index 0000000..4f2ad91 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self.png new file mode 100644 index 0000000..e6c833d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self2.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self2.png new file mode 100644 index 0000000..b6115fc Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self2.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self2@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self2@2x.png new file mode 100644 index 0000000..5bacef2 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self2@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self2@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self2@3x.png new file mode 100644 index 0000000..96fb44d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self2@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self@2x.png new file mode 100644 index 0000000..d1128bb Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self@3x.png new file mode 100644 index 0000000..9f824af Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-self@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip-right.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip-right.png new file mode 100644 index 0000000..517b79a Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip-right.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@2x.png new file mode 100644 index 0000000..49fed32 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@3x.png new file mode 100644 index 0000000..96e8a1e Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip.png new file mode 100644 index 0000000..c3e7bf6 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip@2x.png new file mode 100644 index 0000000..e0ef139 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip@3x.png new file mode 100644 index 0000000..1701d5a Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble-tip@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble.png new file mode 100644 index 0000000..7c6ec32 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble2.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble2.png new file mode 100644 index 0000000..fc86247 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble2.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble2@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble2@2x.png new file mode 100644 index 0000000..f7b7d40 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble2@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble2@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble2@3x.png new file mode 100644 index 0000000..bd0302d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble2@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble@2x.png new file mode 100644 index 0000000..8800522 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble@3x.png new file mode 100644 index 0000000..b627cb4 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/chat-bubble@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/error-toast.png b/Client2018/content/textures/ui/LuaChat/9-slice/error-toast.png new file mode 100644 index 0000000..3fcdffd Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/error-toast.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/error-toast@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/error-toast@2x.png new file mode 100644 index 0000000..b514105 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/error-toast@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/error-toast@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/error-toast@3x.png new file mode 100644 index 0000000..0cf667d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/error-toast@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/gr-mask-game-icon.png b/Client2018/content/textures/ui/LuaChat/9-slice/gr-mask-game-icon.png new file mode 100644 index 0000000..8e2ae96 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/gr-mask-game-icon.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/gr-mask-game-icon@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/gr-mask-game-icon@2x.png new file mode 100644 index 0000000..c7020c7 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/gr-mask-game-icon@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/gr-mask-game-icon@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/gr-mask-game-icon@3x.png new file mode 100644 index 0000000..0025b77 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/gr-mask-game-icon@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/hello-button.png b/Client2018/content/textures/ui/LuaChat/9-slice/hello-button.png new file mode 100644 index 0000000..22acfff Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/hello-button.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/hello-button@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/hello-button@2x.png new file mode 100644 index 0000000..415d1f5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/hello-button@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/hello-button@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/hello-button@3x.png new file mode 100644 index 0000000..5935ab4 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/hello-button@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/input-default.png b/Client2018/content/textures/ui/LuaChat/9-slice/input-default.png new file mode 100644 index 0000000..a1e3247 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/input-default.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/input-default@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/input-default@2x.png new file mode 100644 index 0000000..bad7c52 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/input-default@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/input-default@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/input-default@3x.png new file mode 100644 index 0000000..e92e175 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/input-default@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/input-send-message.png b/Client2018/content/textures/ui/LuaChat/9-slice/input-send-message.png new file mode 100644 index 0000000..0384815 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/input-send-message.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/input-send-message@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/input-send-message@2x.png new file mode 100644 index 0000000..ea0a875 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/input-send-message@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/input-send-message@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/input-send-message@3x.png new file mode 100644 index 0000000..19d5308 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/input-send-message@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/modal.png b/Client2018/content/textures/ui/LuaChat/9-slice/modal.png new file mode 100644 index 0000000..b127799 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/modal.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/modal@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/modal@2x.png new file mode 100644 index 0000000..1ffc6a9 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/modal@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/modal@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/modal@3x.png new file mode 100644 index 0000000..12d0f90 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/modal@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/new-message-indicator.png b/Client2018/content/textures/ui/LuaChat/9-slice/new-message-indicator.png new file mode 100644 index 0000000..38dd53a Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/new-message-indicator.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/new-message-indicator@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/new-message-indicator@2x.png new file mode 100644 index 0000000..3aa02fd Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/new-message-indicator@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/new-message-indicator@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/new-message-indicator@3x.png new file mode 100644 index 0000000..1287b87 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/new-message-indicator@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/scroll-bar.png b/Client2018/content/textures/ui/LuaChat/9-slice/scroll-bar.png new file mode 100644 index 0000000..c47871b Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/scroll-bar.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/scroll-bar@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/scroll-bar@2x.png new file mode 100644 index 0000000..bb7c513 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/scroll-bar@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/scroll-bar@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/scroll-bar@3x.png new file mode 100644 index 0000000..3e15a0d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/scroll-bar@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/search.png b/Client2018/content/textures/ui/LuaChat/9-slice/search.png new file mode 100644 index 0000000..6fdddea Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/search.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/search@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/search@2x.png new file mode 100644 index 0000000..857edd9 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/search@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/search@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/search@3x.png new file mode 100644 index 0000000..eb8a142 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/search@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/system-message.png b/Client2018/content/textures/ui/LuaChat/9-slice/system-message.png new file mode 100644 index 0000000..d26555b Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/system-message.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/system-message@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/system-message@2x.png new file mode 100644 index 0000000..3d85f6f Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/system-message@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/system-message@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/system-message@3x.png new file mode 100644 index 0000000..3e8c804 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/system-message@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/tag-bubble.png b/Client2018/content/textures/ui/LuaChat/9-slice/tag-bubble.png new file mode 100644 index 0000000..75f482a Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/tag-bubble.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/tag-bubble@2x.png b/Client2018/content/textures/ui/LuaChat/9-slice/tag-bubble@2x.png new file mode 100644 index 0000000..c1a412f Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/tag-bubble@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/9-slice/tag-bubble@3x.png b/Client2018/content/textures/ui/LuaChat/9-slice/tag-bubble@3x.png new file mode 100644 index 0000000..7ba23b8 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/9-slice/tag-bubble@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/friendmask.png b/Client2018/content/textures/ui/LuaChat/graphic/friendmask.png new file mode 100644 index 0000000..a42205f Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/friendmask.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-24x24.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-24x24.png new file mode 100644 index 0000000..35db759 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-24x24.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-24x24@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-24x24@2x.png new file mode 100644 index 0000000..4886ac6 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-24x24@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-24x24@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-24x24@3x.png new file mode 100644 index 0000000..b444c76 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-24x24@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-60x60.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-60x60.png new file mode 100644 index 0000000..0deee67 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-60x60.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-60x60@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-60x60@2x.png new file mode 100644 index 0000000..34a58a7 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-60x60@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-60x60@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-60x60@3x.png new file mode 100644 index 0000000..8daff78 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-game-border-60x60@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52.png new file mode 100644 index 0000000..d3725fe Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52@2x.png new file mode 100644 index 0000000..c775cab Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10.png new file mode 100644 index 0000000..8d7fab3 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@2x.png new file mode 100644 index 0000000..07f3a24 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@3x.png new file mode 100644 index 0000000..a5de74f Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12.png new file mode 100644 index 0000000..8b59d6a Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@2x.png new file mode 100644 index 0000000..80c2bd4 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@3x.png new file mode 100644 index 0000000..e9eb973 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14.png new file mode 100644 index 0000000..1200f62 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@2x.png new file mode 100644 index 0000000..6f214d9 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@3x.png new file mode 100644 index 0000000..8afb3a6 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6.png new file mode 100644 index 0000000..b9d4a49 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@2x.png new file mode 100644 index 0000000..8b59d6a Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@3x.png new file mode 100644 index 0000000..1f90cfd Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8.png new file mode 100644 index 0000000..be8d8c5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@2x.png new file mode 100644 index 0000000..c9632ff Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@3x.png new file mode 100644 index 0000000..0d194b0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame.png new file mode 100644 index 0000000..f622033 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame@2x.png new file mode 100644 index 0000000..24ff925 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame@3x.png new file mode 100644 index 0000000..6866f33 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-ingame@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10.png new file mode 100644 index 0000000..70bef5b Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@2x.png new file mode 100644 index 0000000..b7d0d3f Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@3x.png new file mode 100644 index 0000000..1daa6c7 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12.png new file mode 100644 index 0000000..32b5368 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12@3x.png new file mode 100644 index 0000000..7c38197 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x2@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x2@2x.png new file mode 100644 index 0000000..6a0afba Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x2@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14.png new file mode 100644 index 0000000..bb74b97 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@2x.png new file mode 100644 index 0000000..1015a19 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@3x.png new file mode 100644 index 0000000..eeb8cee Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8.png new file mode 100644 index 0000000..3d1a10a Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@2x.png new file mode 100644 index 0000000..f2d5ec5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@3x.png new file mode 100644 index 0000000..fb9489f Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio.png new file mode 100644 index 0000000..94c64f4 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio@2x.png new file mode 100644 index 0000000..0c6d285 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio@3x.png new file mode 100644 index 0000000..db89806 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6.png new file mode 100644 index 0000000..afd86c0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@2x.png new file mode 100644 index 0000000..32b5368 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@3x.png new file mode 100644 index 0000000..970a691 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-10x10.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-10x10.png new file mode 100644 index 0000000..6dd796d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-10x10.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@2x.png new file mode 100644 index 0000000..6632f9b Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@3x.png new file mode 100644 index 0000000..2c39a06 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-12x12.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-12x12.png new file mode 100644 index 0000000..e8b271d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-12x12.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@2x.png new file mode 100644 index 0000000..6c3645b Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@3x.png new file mode 100644 index 0000000..3b76e80 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-14x14.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-14x14.png new file mode 100644 index 0000000..4ee79a3 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-14x14.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@2x.png new file mode 100644 index 0000000..2673de7 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@3x.png new file mode 100644 index 0000000..26b4071 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-6x6.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-6x6.png new file mode 100644 index 0000000..d9f9181 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-6x6.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@2x.png new file mode 100644 index 0000000..e8b271d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@3x.png new file mode 100644 index 0000000..e3e9c60 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-8x8.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-8x8.png new file mode 100644 index 0000000..d26c309 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-8x8.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@2x.png new file mode 100644 index 0000000..b3428ea Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@3x.png new file mode 100644 index 0000000..6a14d5c Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online.png new file mode 100644 index 0000000..a8efbec Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online@2x.png new file mode 100644 index 0000000..1133089 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online@3x.png new file mode 100644 index 0000000..5c61715 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-indicator-online@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48.png new file mode 100644 index 0000000..47bd340 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48@2x.png new file mode 100644 index 0000000..578a10a Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-numbers.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-numbers.png new file mode 100644 index 0000000..f59ead5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-numbers.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-numbers@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-numbers@2x.png new file mode 100644 index 0000000..b4b9fbe Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-numbers@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-numbers@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-numbers@3x.png new file mode 100644 index 0000000..4f3279f Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-numbers@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-overlay-shadow.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-overlay-shadow.png new file mode 100644 index 0000000..4c1fa77 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-overlay-shadow.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-overlay-shadow@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-overlay-shadow@2x.png new file mode 100644 index 0000000..42e5a34 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-overlay-shadow@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-overlay-shadow@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-overlay-shadow@3x.png new file mode 100644 index 0000000..64615ab Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-overlay-shadow@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-36x36.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-36x36.png new file mode 100644 index 0000000..ecab811 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-36x36.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-36x36@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-36x36@2x.png new file mode 100644 index 0000000..03b94cc Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-36x36@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-36x36@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-36x36@3x.png new file mode 100644 index 0000000..b0d22fe Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-36x36@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted.png new file mode 100644 index 0000000..5917d8d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@2x.png new file mode 100644 index 0000000..911d0f7 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@3x.png new file mode 100644 index 0000000..c27bf49 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48.png new file mode 100644 index 0000000..e2f28f8 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48@2x.png new file mode 100644 index 0000000..b1cd13b Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48@3x.png new file mode 100644 index 0000000..9a0a00a Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-profile-border-48x48@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-send-on.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-send-on.png new file mode 100644 index 0000000..59255c1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-send-on.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-send-on@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-send-on@2x.png new file mode 100644 index 0000000..407fa57 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-send-on@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-send-on@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-send-on@3x.png new file mode 100644 index 0000000..ed4ad48 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-send-on@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-send.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-send.png new file mode 100644 index 0000000..b5f28b2 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-send.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-send@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-send@2x.png new file mode 100644 index 0000000..d647c1b Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-send@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/gr-send@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/gr-send@3x.png new file mode 100644 index 0000000..aced5ec Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/gr-send@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox-on.png b/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox-on.png new file mode 100644 index 0000000..0390cb4 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox-on.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox-on@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox-on@2x.png new file mode 100644 index 0000000..9ee440a Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox-on@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox-on@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox-on@3x.png new file mode 100644 index 0000000..b897b38 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox-on@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox.png b/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox.png new file mode 100644 index 0000000..cfd7260 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox@2x.png new file mode 100644 index 0000000..7a04603 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox@3x.png new file mode 100644 index 0000000..09fd802 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/ic-checkbox@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/indicator-background.png b/Client2018/content/textures/ui/LuaChat/graphic/indicator-background.png new file mode 100644 index 0000000..c36fb92 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/indicator-background.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/send-white.png b/Client2018/content/textures/ui/LuaChat/graphic/send-white.png new file mode 100644 index 0000000..e9df684 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/send-white.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/send-white@2x.png b/Client2018/content/textures/ui/LuaChat/graphic/send-white@2x.png new file mode 100644 index 0000000..030eeee Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/send-white@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/graphic/send-white@3x.png b/Client2018/content/textures/ui/LuaChat/graphic/send-white@3x.png new file mode 100644 index 0000000..af160d0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/graphic/send-white@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-add-friends.png b/Client2018/content/textures/ui/LuaChat/icons/ic-add-friends.png new file mode 100644 index 0000000..a68367b Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-add-friends.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-add-friends@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-add-friends@2x.png new file mode 100644 index 0000000..6e1b632 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-add-friends@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-add-friends@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-add-friends@3x.png new file mode 100644 index 0000000..b2a1be0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-add-friends@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-alert.png b/Client2018/content/textures/ui/LuaChat/icons/ic-alert.png new file mode 100644 index 0000000..85d6d39 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-alert.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-alert@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-alert@2x.png new file mode 100644 index 0000000..7fb786a Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-alert@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-alert@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-alert@3x.png new file mode 100644 index 0000000..ab44ae0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-alert@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-back-android.png b/Client2018/content/textures/ui/LuaChat/icons/ic-back-android.png new file mode 100644 index 0000000..2fb40d1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-back-android.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-back-android@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-back-android@2x.png new file mode 100644 index 0000000..af6490d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-back-android@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-back.png b/Client2018/content/textures/ui/LuaChat/icons/ic-back.png new file mode 100644 index 0000000..42a2433 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-back.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-back@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-back@2x.png new file mode 100644 index 0000000..e2b4462 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-back@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-bc.png b/Client2018/content/textures/ui/LuaChat/icons/ic-bc.png new file mode 100644 index 0000000..acfbdb5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-bc.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-bc@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-bc@2x.png new file mode 100644 index 0000000..be1c042 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-bc@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-bc@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-bc@3x.png new file mode 100644 index 0000000..85d2fbd Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-bc@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-chat-large.png b/Client2018/content/textures/ui/LuaChat/icons/ic-chat-large.png new file mode 100644 index 0000000..5a7141d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-chat-large.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-chat-large@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-chat-large@2x.png new file mode 100644 index 0000000..5673f80 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-chat-large@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-chat-large@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-chat-large@3x.png new file mode 100644 index 0000000..f07c89e Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-chat-large@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-check.png b/Client2018/content/textures/ui/LuaChat/icons/ic-check.png new file mode 100644 index 0000000..26d3557 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-check.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-check@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-check@2x.png new file mode 100644 index 0000000..7791da1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-check@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-check@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-check@3x.png new file mode 100644 index 0000000..38cc530 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-check@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-checkbox-on copy.png b/Client2018/content/textures/ui/LuaChat/icons/ic-checkbox-on copy.png new file mode 100644 index 0000000..511e290 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-checkbox-on copy.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-checkbox-on copy@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-checkbox-on copy@2x.png new file mode 100644 index 0000000..d2aa085 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-checkbox-on copy@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-checkbox-on copy@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-checkbox-on copy@3x.png new file mode 100644 index 0000000..fcd44c7 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-checkbox-on copy@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-clear-gray.png b/Client2018/content/textures/ui/LuaChat/icons/ic-clear-gray.png new file mode 100644 index 0000000..f39bdfc Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-clear-gray.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-clear-gray@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-clear-gray@2x.png new file mode 100644 index 0000000..18e07bd Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-clear-gray@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-clear-gray@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-clear-gray@3x.png new file mode 100644 index 0000000..c746437 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-clear-gray@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-clear-solid.png b/Client2018/content/textures/ui/LuaChat/icons/ic-clear-solid.png new file mode 100644 index 0000000..5839bd5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-clear-solid.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-clear-solid@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-clear-solid@2x.png new file mode 100644 index 0000000..48b7bbf Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-clear-solid@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-clear-solid@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-clear-solid@3x.png new file mode 100644 index 0000000..5ec68b8 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-clear-solid@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-close-gray2.png b/Client2018/content/textures/ui/LuaChat/icons/ic-close-gray2.png new file mode 100644 index 0000000..1dd1e05 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-close-gray2.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-close-gray2@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-close-gray2@2x.png new file mode 100644 index 0000000..3a845ff Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-close-gray2@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-close-gray2@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-close-gray2@3x.png new file mode 100644 index 0000000..3326324 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-close-gray2@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-close-white.png b/Client2018/content/textures/ui/LuaChat/icons/ic-close-white.png new file mode 100644 index 0000000..af8f7b8 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-close-white.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-create-group.png b/Client2018/content/textures/ui/LuaChat/icons/ic-create-group.png new file mode 100644 index 0000000..72c902c Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-create-group.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-create-group@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-create-group@2x.png new file mode 100644 index 0000000..6041c11 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-create-group@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-create-group@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-create-group@3x.png new file mode 100644 index 0000000..569e60d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-create-group@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-createchat1-24x24.png b/Client2018/content/textures/ui/LuaChat/icons/ic-createchat1-24x24.png new file mode 100644 index 0000000..ad3c8f9 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-createchat1-24x24.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-createchat1-24x24@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-createchat1-24x24@2x.png new file mode 100644 index 0000000..2dfef75 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-createchat1-24x24@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-createchat1-24x24@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-createchat1-24x24@3x.png new file mode 100644 index 0000000..73ee9b8 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-createchat1-24x24@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-friends.png b/Client2018/content/textures/ui/LuaChat/icons/ic-friends.png new file mode 100644 index 0000000..c2dddec Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-friends.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-friends@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-friends@2x.png new file mode 100644 index 0000000..aafb10e Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-friends@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-friends@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-friends@3x.png new file mode 100644 index 0000000..c2610a8 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-friends@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-game-pressed-24x24.png b/Client2018/content/textures/ui/LuaChat/icons/ic-game-pressed-24x24.png new file mode 100644 index 0000000..a04a67c Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-game-pressed-24x24.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-game-pressed-24x24@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-game-pressed-24x24@2x.png new file mode 100644 index 0000000..a8ee17d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-game-pressed-24x24@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-game-pressed-24x24@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-game-pressed-24x24@3x.png new file mode 100644 index 0000000..9276f55 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-game-pressed-24x24@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-game.png b/Client2018/content/textures/ui/LuaChat/icons/ic-game.png new file mode 100644 index 0000000..aeb09dc Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-game.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-game@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-game@2x.png new file mode 100644 index 0000000..233dbe2 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-game@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-game@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-game@3x.png new file mode 100644 index 0000000..4334107 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-game@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-group-16x16.png b/Client2018/content/textures/ui/LuaChat/icons/ic-group-16x16.png new file mode 100644 index 0000000..4be38ac Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-group-16x16.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-group-16x16@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-group-16x16@2x.png new file mode 100644 index 0000000..2d02bf0 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-group-16x16@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-group-16x16@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-group-16x16@3x.png new file mode 100644 index 0000000..a3204eb Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-group-16x16@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-group.png b/Client2018/content/textures/ui/LuaChat/icons/ic-group.png new file mode 100644 index 0000000..db3b1b7 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-group.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-group@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-group@2x.png new file mode 100644 index 0000000..48ec989 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-group@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-group@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-group@3x.png new file mode 100644 index 0000000..38a5549 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-group@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-info.png b/Client2018/content/textures/ui/LuaChat/icons/ic-info.png new file mode 100644 index 0000000..0a9e434 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-info.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-info@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-info@2x.png new file mode 100644 index 0000000..2801509 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-info@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-info@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-info@3x.png new file mode 100644 index 0000000..2fb4765 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-info@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-leave.png b/Client2018/content/textures/ui/LuaChat/icons/ic-leave.png new file mode 100644 index 0000000..136f273 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-leave.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-leave@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-leave@2x.png new file mode 100644 index 0000000..53f544f Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-leave@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-leave@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-leave@3x.png new file mode 100644 index 0000000..57966c6 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-leave@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-more.png b/Client2018/content/textures/ui/LuaChat/icons/ic-more.png new file mode 100644 index 0000000..718010b Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-more.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-more@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-more@2x.png new file mode 100644 index 0000000..d107f2f Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-more@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-more@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-more@3x.png new file mode 100644 index 0000000..7d871a6 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-more@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-nametag.png b/Client2018/content/textures/ui/LuaChat/icons/ic-nametag.png new file mode 100644 index 0000000..eee203e Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-nametag.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-nametag@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-nametag@2x.png new file mode 100644 index 0000000..1dc795d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-nametag@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-nametag@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-nametag@3x.png new file mode 100644 index 0000000..ad7710f Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-nametag@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-notification.png b/Client2018/content/textures/ui/LuaChat/icons/ic-notification.png new file mode 100644 index 0000000..7cc3f5d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-notification.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-notification@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-notification@2x.png new file mode 100644 index 0000000..7db1065 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-notification@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-notification@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-notification@3x.png new file mode 100644 index 0000000..c49aa2b Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-notification@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-pin.png b/Client2018/content/textures/ui/LuaChat/icons/ic-pin.png new file mode 100644 index 0000000..f7a8d92 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-pin.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-pin@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-pin@2x.png new file mode 100644 index 0000000..9befe4b Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-pin@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-pin@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-pin@3x.png new file mode 100644 index 0000000..7387c33 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-pin@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-pinpressed.png b/Client2018/content/textures/ui/LuaChat/icons/ic-pinpressed.png new file mode 100644 index 0000000..b893f0b Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-pinpressed.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-pinpressed@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-pinpressed@2x.png new file mode 100644 index 0000000..1103fa5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-pinpressed@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-pinpressed@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-pinpressed@3x.png new file mode 100644 index 0000000..37bfbc5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-pinpressed@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-profile.png b/Client2018/content/textures/ui/LuaChat/icons/ic-profile.png new file mode 100644 index 0000000..fdf6f87 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-profile.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-profile@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-profile@2x.png new file mode 100644 index 0000000..8649c34 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-profile@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-profile@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-profile@3x.png new file mode 100644 index 0000000..bd3fe06 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-profile@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-remove.png b/Client2018/content/textures/ui/LuaChat/icons/ic-remove.png new file mode 100644 index 0000000..089dba9 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-remove.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-remove@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-remove@2x.png new file mode 100644 index 0000000..a43d237 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-remove@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-remove@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-remove@3x.png new file mode 100644 index 0000000..0185b15 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-remove@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-resend.png b/Client2018/content/textures/ui/LuaChat/icons/ic-resend.png new file mode 100644 index 0000000..464bc87 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-resend.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-resend@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-resend@2x.png new file mode 100644 index 0000000..065d260 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-resend@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-resend@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-resend@3x.png new file mode 100644 index 0000000..c93f1d8 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-resend@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-robux.png b/Client2018/content/textures/ui/LuaChat/icons/ic-robux.png new file mode 100644 index 0000000..30e32cf Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-robux.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-robux@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-robux@2x.png new file mode 100644 index 0000000..b181a12 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-robux@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-robux@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-robux@3x.png new file mode 100644 index 0000000..67f39f5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-robux@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-search-gray.png b/Client2018/content/textures/ui/LuaChat/icons/ic-search-gray.png new file mode 100644 index 0000000..f2e0f74 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-search-gray.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-search-gray@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-search-gray@2x.png new file mode 100644 index 0000000..52661b5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-search-gray@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-search-gray@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-search-gray@3x.png new file mode 100644 index 0000000..365865f Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-search-gray@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-search.png b/Client2018/content/textures/ui/LuaChat/icons/ic-search.png new file mode 100644 index 0000000..fefae0c Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-search.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-search@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-search@2x.png new file mode 100644 index 0000000..8585a75 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-search@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-search@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-search@3x.png new file mode 100644 index 0000000..ed9ff2f Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-search@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-send.png b/Client2018/content/textures/ui/LuaChat/icons/ic-send.png new file mode 100644 index 0000000..c6951c1 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-send.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-send@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-send@2x.png new file mode 100644 index 0000000..d0afa8b Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-send@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-send@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-send@3x.png new file mode 100644 index 0000000..41a0658 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-send@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-unpin-20x20.png b/Client2018/content/textures/ui/LuaChat/icons/ic-unpin-20x20.png new file mode 100644 index 0000000..61f6d2e Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-unpin-20x20.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-unpin-20x20@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-unpin-20x20@2x.png new file mode 100644 index 0000000..6661058 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-unpin-20x20@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-unpin-20x20@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-unpin-20x20@3x.png new file mode 100644 index 0000000..be0e5a8 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-unpin-20x20@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-viewdetails-20x20.png b/Client2018/content/textures/ui/LuaChat/icons/ic-viewdetails-20x20.png new file mode 100644 index 0000000..b8ec87e Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-viewdetails-20x20.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-viewdetails-20x20@2x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-viewdetails-20x20@2x.png new file mode 100644 index 0000000..b5fa7e8 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-viewdetails-20x20@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/ic-viewdetails-20x20@3x.png b/Client2018/content/textures/ui/LuaChat/icons/ic-viewdetails-20x20@3x.png new file mode 100644 index 0000000..f5f12f5 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/ic-viewdetails-20x20@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-24x24.png b/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-24x24.png new file mode 100644 index 0000000..119547d Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-24x24.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-24x24@2x.png b/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-24x24@2x.png new file mode 100644 index 0000000..47ecc6b Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-24x24@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-24x24@3x.png b/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-24x24@3x.png new file mode 100644 index 0000000..0bc59ee Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-24x24@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24.png b/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24.png new file mode 100644 index 0000000..e72edaa Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@2x.png b/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@2x.png new file mode 100644 index 0000000..3691a33 Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@2x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@3x.png b/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@3x.png new file mode 100644 index 0000000..bf9e31b Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@3x.png differ diff --git a/Client2018/content/textures/ui/LuaChat/icons/share-game-thumbnail.png b/Client2018/content/textures/ui/LuaChat/icons/share-game-thumbnail.png new file mode 100644 index 0000000..1c4b30e Binary files /dev/null and b/Client2018/content/textures/ui/LuaChat/icons/share-game-thumbnail.png differ diff --git a/Client2018/content/textures/ui/Menu/Hamburger.png b/Client2018/content/textures/ui/Menu/Hamburger.png new file mode 100644 index 0000000..6e0c2e4 Binary files /dev/null and b/Client2018/content/textures/ui/Menu/Hamburger.png differ diff --git a/Client2018/content/textures/ui/Menu/Hamburger@2x.png b/Client2018/content/textures/ui/Menu/Hamburger@2x.png new file mode 100644 index 0000000..a5ce478 Binary files /dev/null and b/Client2018/content/textures/ui/Menu/Hamburger@2x.png differ diff --git a/Client2018/content/textures/ui/Menu/HamburgerDown.png b/Client2018/content/textures/ui/Menu/HamburgerDown.png new file mode 100644 index 0000000..aaf1032 Binary files /dev/null and b/Client2018/content/textures/ui/Menu/HamburgerDown.png differ diff --git a/Client2018/content/textures/ui/Menu/HamburgerDown@2x.png b/Client2018/content/textures/ui/Menu/HamburgerDown@2x.png new file mode 100644 index 0000000..43ab55c Binary files /dev/null and b/Client2018/content/textures/ui/Menu/HamburgerDown@2x.png differ diff --git a/Client2018/content/textures/ui/Menu/buttonActive.png b/Client2018/content/textures/ui/Menu/buttonActive.png new file mode 100644 index 0000000..f685760 Binary files /dev/null and b/Client2018/content/textures/ui/Menu/buttonActive.png differ diff --git a/Client2018/content/textures/ui/Menu/buttonBackground.png b/Client2018/content/textures/ui/Menu/buttonBackground.png new file mode 100644 index 0000000..379241a Binary files /dev/null and b/Client2018/content/textures/ui/Menu/buttonBackground.png differ diff --git a/Client2018/content/textures/ui/Menu/buttonHover.png b/Client2018/content/textures/ui/Menu/buttonHover.png new file mode 100644 index 0000000..916ddff Binary files /dev/null and b/Client2018/content/textures/ui/Menu/buttonHover.png differ diff --git a/Client2018/content/textures/ui/Menu/hamburger3D.png b/Client2018/content/textures/ui/Menu/hamburger3D.png new file mode 100644 index 0000000..a9d70c5 Binary files /dev/null and b/Client2018/content/textures/ui/Menu/hamburger3D.png differ diff --git a/Client2018/content/textures/ui/Menu/hoverPopupLeft.png b/Client2018/content/textures/ui/Menu/hoverPopupLeft.png new file mode 100644 index 0000000..484b0d2 Binary files /dev/null and b/Client2018/content/textures/ui/Menu/hoverPopupLeft.png differ diff --git a/Client2018/content/textures/ui/Menu/hoverPopupMid.png b/Client2018/content/textures/ui/Menu/hoverPopupMid.png new file mode 100644 index 0000000..0f55008 Binary files /dev/null and b/Client2018/content/textures/ui/Menu/hoverPopupMid.png differ diff --git a/Client2018/content/textures/ui/Menu/hoverPopupRight.png b/Client2018/content/textures/ui/Menu/hoverPopupRight.png new file mode 100644 index 0000000..ea0b32a Binary files /dev/null and b/Client2018/content/textures/ui/Menu/hoverPopupRight.png differ diff --git a/Client2018/content/textures/ui/Menu/rectBackground.png b/Client2018/content/textures/ui/Menu/rectBackground.png new file mode 100644 index 0000000..758edfe Binary files /dev/null and b/Client2018/content/textures/ui/Menu/rectBackground.png differ diff --git a/Client2018/content/textures/ui/Menu/rectBackgroundWhite.png b/Client2018/content/textures/ui/Menu/rectBackgroundWhite.png new file mode 100644 index 0000000..6bbb4b6 Binary files /dev/null and b/Client2018/content/textures/ui/Menu/rectBackgroundWhite.png differ diff --git a/Client2018/content/textures/ui/Modal.png b/Client2018/content/textures/ui/Modal.png new file mode 100644 index 0000000..ec08e5b Binary files /dev/null and b/Client2018/content/textures/ui/Modal.png differ diff --git a/Client2018/content/textures/ui/Motor.png b/Client2018/content/textures/ui/Motor.png new file mode 100644 index 0000000..140c5cc Binary files /dev/null and b/Client2018/content/textures/ui/Motor.png differ diff --git a/Client2018/content/textures/ui/PerformanceStats/BackgroundRounded.png b/Client2018/content/textures/ui/PerformanceStats/BackgroundRounded.png new file mode 100644 index 0000000..763d78e Binary files /dev/null and b/Client2018/content/textures/ui/PerformanceStats/BackgroundRounded.png differ diff --git a/Client2018/content/textures/ui/PerformanceStats/OvalKey.png b/Client2018/content/textures/ui/PerformanceStats/OvalKey.png new file mode 100644 index 0000000..90678c2 Binary files /dev/null and b/Client2018/content/textures/ui/PerformanceStats/OvalKey.png differ diff --git a/Client2018/content/textures/ui/PerformanceStats/TargetFiller.png b/Client2018/content/textures/ui/PerformanceStats/TargetFiller.png new file mode 100644 index 0000000..839cc28 Binary files /dev/null and b/Client2018/content/textures/ui/PerformanceStats/TargetFiller.png differ diff --git a/Client2018/content/textures/ui/PerformanceStats/TargetKey.png b/Client2018/content/textures/ui/PerformanceStats/TargetKey.png new file mode 100644 index 0000000..9008605 Binary files /dev/null and b/Client2018/content/textures/ui/PerformanceStats/TargetKey.png differ diff --git a/Client2018/content/textures/ui/PerformanceStats/TargetLine.png b/Client2018/content/textures/ui/PerformanceStats/TargetLine.png new file mode 100644 index 0000000..c1b434a Binary files /dev/null and b/Client2018/content/textures/ui/PerformanceStats/TargetLine.png differ diff --git a/Client2018/content/textures/ui/Plastic.png b/Client2018/content/textures/ui/Plastic.png new file mode 100644 index 0000000..0dd2054 Binary files /dev/null and b/Client2018/content/textures/ui/Plastic.png differ diff --git a/Client2018/content/textures/ui/PlayerList/BlockedIcon.png b/Client2018/content/textures/ui/PlayerList/BlockedIcon.png new file mode 100644 index 0000000..1ddb436 Binary files /dev/null and b/Client2018/content/textures/ui/PlayerList/BlockedIcon.png differ diff --git a/Client2018/content/textures/ui/PlayerList/CharacterImageBackground.png b/Client2018/content/textures/ui/PlayerList/CharacterImageBackground.png new file mode 100644 index 0000000..2620ac5 Binary files /dev/null and b/Client2018/content/textures/ui/PlayerList/CharacterImageBackground.png differ diff --git a/Client2018/content/textures/ui/PlayerList/TileShadowMissingTop.png b/Client2018/content/textures/ui/PlayerList/TileShadowMissingTop.png new file mode 100644 index 0000000..d09f5fb Binary files /dev/null and b/Client2018/content/textures/ui/PlayerList/TileShadowMissingTop.png differ diff --git a/Client2018/content/textures/ui/RecordDown.png b/Client2018/content/textures/ui/RecordDown.png new file mode 100644 index 0000000..9708d84 Binary files /dev/null and b/Client2018/content/textures/ui/RecordDown.png differ diff --git a/Client2018/content/textures/ui/ResetIcon.png b/Client2018/content/textures/ui/ResetIcon.png new file mode 100644 index 0000000..b16996e Binary files /dev/null and b/Client2018/content/textures/ui/ResetIcon.png differ diff --git a/Client2018/content/textures/ui/RobloxNameIcon.png b/Client2018/content/textures/ui/RobloxNameIcon.png new file mode 100644 index 0000000..360d9c2 Binary files /dev/null and b/Client2018/content/textures/ui/RobloxNameIcon.png differ diff --git a/Client2018/content/textures/ui/RobuxIcon.png b/Client2018/content/textures/ui/RobuxIcon.png new file mode 100644 index 0000000..d14fe16 Binary files /dev/null and b/Client2018/content/textures/ui/RobuxIcon.png differ diff --git a/Client2018/content/textures/ui/Scroll/scroll-bottom.png b/Client2018/content/textures/ui/Scroll/scroll-bottom.png new file mode 100644 index 0000000..9c5695c Binary files /dev/null and b/Client2018/content/textures/ui/Scroll/scroll-bottom.png differ diff --git a/Client2018/content/textures/ui/Scroll/scroll-bottom@2x.png b/Client2018/content/textures/ui/Scroll/scroll-bottom@2x.png new file mode 100644 index 0000000..85b70e6 Binary files /dev/null and b/Client2018/content/textures/ui/Scroll/scroll-bottom@2x.png differ diff --git a/Client2018/content/textures/ui/Scroll/scroll-middle.png b/Client2018/content/textures/ui/Scroll/scroll-middle.png new file mode 100644 index 0000000..f4489d7 Binary files /dev/null and b/Client2018/content/textures/ui/Scroll/scroll-middle.png differ diff --git a/Client2018/content/textures/ui/Scroll/scroll-middle@2x.png b/Client2018/content/textures/ui/Scroll/scroll-middle@2x.png new file mode 100644 index 0000000..22d0f11 Binary files /dev/null and b/Client2018/content/textures/ui/Scroll/scroll-middle@2x.png differ diff --git a/Client2018/content/textures/ui/Scroll/scroll-top.png b/Client2018/content/textures/ui/Scroll/scroll-top.png new file mode 100644 index 0000000..bc6dcce Binary files /dev/null and b/Client2018/content/textures/ui/Scroll/scroll-top.png differ diff --git a/Client2018/content/textures/ui/Scroll/scroll-top@2x.png b/Client2018/content/textures/ui/Scroll/scroll-top@2x.png new file mode 100644 index 0000000..0ead0e8 Binary files /dev/null and b/Client2018/content/textures/ui/Scroll/scroll-top@2x.png differ diff --git a/Client2018/content/textures/ui/SearchIcon.png b/Client2018/content/textures/ui/SearchIcon.png new file mode 100644 index 0000000..58772d0 Binary files /dev/null and b/Client2018/content/textures/ui/SearchIcon.png differ diff --git a/Client2018/content/textures/ui/SelectionBox.png b/Client2018/content/textures/ui/SelectionBox.png new file mode 100644 index 0000000..290b647 Binary files /dev/null and b/Client2018/content/textures/ui/SelectionBox.png differ diff --git a/Client2018/content/textures/ui/SelectionBox@2x.png b/Client2018/content/textures/ui/SelectionBox@2x.png new file mode 100644 index 0000000..65d1711 Binary files /dev/null and b/Client2018/content/textures/ui/SelectionBox@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/DropDown/DropDown.png b/Client2018/content/textures/ui/Settings/DropDown/DropDown.png new file mode 100644 index 0000000..39e6dc8 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/DropDown/DropDown.png differ diff --git a/Client2018/content/textures/ui/Settings/DropDown/DropDown@2x.png b/Client2018/content/textures/ui/Settings/DropDown/DropDown@2x.png new file mode 100644 index 0000000..ccdd651 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/DropDown/DropDown@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/AButtonDark.png b/Client2018/content/textures/ui/Settings/Help/AButtonDark.png new file mode 100644 index 0000000..da4ec7a Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/AButtonDark.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/AButtonDark@2x.png b/Client2018/content/textures/ui/Settings/Help/AButtonDark@2x.png new file mode 100644 index 0000000..b2e9cba Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/AButtonDark@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/AButtonLight.png b/Client2018/content/textures/ui/Settings/Help/AButtonLight.png new file mode 100644 index 0000000..431e4a7 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/AButtonLight.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/AButtonLight@2x.png b/Client2018/content/textures/ui/Settings/Help/AButtonLight@2x.png new file mode 100644 index 0000000..d140dba Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/AButtonLight@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/AButtonLightSmall.png b/Client2018/content/textures/ui/Settings/Help/AButtonLightSmall.png new file mode 100644 index 0000000..50f8035 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/AButtonLightSmall.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/BButtonDark.png b/Client2018/content/textures/ui/Settings/Help/BButtonDark.png new file mode 100644 index 0000000..9b607a9 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/BButtonDark.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/BButtonDark@2x.png b/Client2018/content/textures/ui/Settings/Help/BButtonDark@2x.png new file mode 100644 index 0000000..df1a1a5 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/BButtonDark@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/BButtonLight.png b/Client2018/content/textures/ui/Settings/Help/BButtonLight.png new file mode 100644 index 0000000..598b7b9 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/BButtonLight.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/BButtonLight@2x.png b/Client2018/content/textures/ui/Settings/Help/BButtonLight@2x.png new file mode 100644 index 0000000..e2e67db Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/BButtonLight@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/EscapeIcon.png b/Client2018/content/textures/ui/Settings/Help/EscapeIcon.png new file mode 100644 index 0000000..7c53d55 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/EscapeIcon.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/GenericController.png b/Client2018/content/textures/ui/Settings/Help/GenericController.png new file mode 100644 index 0000000..db52b1f Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/GenericController.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/GenericController@2x.png b/Client2018/content/textures/ui/Settings/Help/GenericController@2x.png new file mode 100644 index 0000000..3735735 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/GenericController@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/LeaveIcon.png b/Client2018/content/textures/ui/Settings/Help/LeaveIcon.png new file mode 100644 index 0000000..6d780ae Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/LeaveIcon.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/ResetIcon.png b/Client2018/content/textures/ui/Settings/Help/ResetIcon.png new file mode 100644 index 0000000..391ac31 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/ResetIcon.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/RotateCameraGesture.png b/Client2018/content/textures/ui/Settings/Help/RotateCameraGesture.png new file mode 100644 index 0000000..c0147d6 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/RotateCameraGesture.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/UseToolGesture.png b/Client2018/content/textures/ui/Settings/Help/UseToolGesture.png new file mode 100644 index 0000000..fe3c364 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/UseToolGesture.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/XButtonDark.png b/Client2018/content/textures/ui/Settings/Help/XButtonDark.png new file mode 100644 index 0000000..cc71edc Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/XButtonDark.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/XButtonDark@2x.png b/Client2018/content/textures/ui/Settings/Help/XButtonDark@2x.png new file mode 100644 index 0000000..0d53c7a Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/XButtonDark@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/XButtonLight.png b/Client2018/content/textures/ui/Settings/Help/XButtonLight.png new file mode 100644 index 0000000..62c13ec Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/XButtonLight.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/XButtonLight@2x.png b/Client2018/content/textures/ui/Settings/Help/XButtonLight@2x.png new file mode 100644 index 0000000..1d5bc77 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/XButtonLight@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/XboxController.png b/Client2018/content/textures/ui/Settings/Help/XboxController.png new file mode 100644 index 0000000..882124a Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/XboxController.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/XboxController@2x.png b/Client2018/content/textures/ui/Settings/Help/XboxController@2x.png new file mode 100644 index 0000000..9ea335a Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/XboxController@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/YButtonDark.png b/Client2018/content/textures/ui/Settings/Help/YButtonDark.png new file mode 100644 index 0000000..131c8ca Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/YButtonDark.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/YButtonDark@2x.png b/Client2018/content/textures/ui/Settings/Help/YButtonDark@2x.png new file mode 100644 index 0000000..3b5e223 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/YButtonDark@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/YButtonLight.png b/Client2018/content/textures/ui/Settings/Help/YButtonLight.png new file mode 100644 index 0000000..56ce742 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/YButtonLight.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/YButtonLight@2x.png b/Client2018/content/textures/ui/Settings/Help/YButtonLight@2x.png new file mode 100644 index 0000000..1a3ee30 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/YButtonLight@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Help/ZoomGesture.png b/Client2018/content/textures/ui/Settings/Help/ZoomGesture.png new file mode 100644 index 0000000..68ee910 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Help/ZoomGesture.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuBackground.png b/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuBackground.png new file mode 100644 index 0000000..4614da6 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuBackground.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuButton.png b/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuButton.png new file mode 100644 index 0000000..632c7d2 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuButton.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuButton@2x.png b/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuButton@2x.png new file mode 100644 index 0000000..a7f7a27 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuButton@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png b/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png new file mode 100644 index 0000000..4a4267e Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected@2x.png b/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected@2x.png new file mode 100644 index 0000000..3c33658 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuSelection.png b/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuSelection.png new file mode 100644 index 0000000..c0f8936 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuSelection.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuSelection@2x.png b/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuSelection@2x.png new file mode 100644 index 0000000..b3eb828 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarAssets/MenuSelection@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab.png b/Client2018/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab.png new file mode 100644 index 0000000..5b0c02c Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab@2x.png b/Client2018/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab@2x.png new file mode 100644 index 0000000..61a2380 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarIcons/HelpTab.png b/Client2018/content/textures/ui/Settings/MenuBarIcons/HelpTab.png new file mode 100644 index 0000000..a3a18c1 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarIcons/HelpTab.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarIcons/HelpTab@2x.png b/Client2018/content/textures/ui/Settings/MenuBarIcons/HelpTab@2x.png new file mode 100644 index 0000000..c55cfad Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarIcons/HelpTab@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarIcons/HomeTab.png b/Client2018/content/textures/ui/Settings/MenuBarIcons/HomeTab.png new file mode 100644 index 0000000..e1a6468 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarIcons/HomeTab.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarIcons/HomeTab@2x.png b/Client2018/content/textures/ui/Settings/MenuBarIcons/HomeTab@2x.png new file mode 100644 index 0000000..edec627 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarIcons/HomeTab@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon.png b/Client2018/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon.png new file mode 100644 index 0000000..fc253e4 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon@2x.png b/Client2018/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon@2x.png new file mode 100644 index 0000000..8ab99f7 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarIcons/RecordTab.png b/Client2018/content/textures/ui/Settings/MenuBarIcons/RecordTab.png new file mode 100644 index 0000000..37616b6 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarIcons/RecordTab.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarIcons/RecordTab@2x.png b/Client2018/content/textures/ui/Settings/MenuBarIcons/RecordTab@2x.png new file mode 100644 index 0000000..ee7db49 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarIcons/RecordTab@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab.png b/Client2018/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab.png new file mode 100644 index 0000000..69eb788 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab.png differ diff --git a/Client2018/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab@2x.png b/Client2018/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab@2x.png new file mode 100644 index 0000000..8efb061 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Players/AddFriendIcon.png b/Client2018/content/textures/ui/Settings/Players/AddFriendIcon.png new file mode 100644 index 0000000..d63e933 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Players/AddFriendIcon.png differ diff --git a/Client2018/content/textures/ui/Settings/Players/AddFriendIcon@2x.png b/Client2018/content/textures/ui/Settings/Players/AddFriendIcon@2x.png new file mode 100644 index 0000000..ddb5658 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Players/AddFriendIcon@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Players/FriendIcon.png b/Client2018/content/textures/ui/Settings/Players/FriendIcon.png new file mode 100644 index 0000000..ce78c14 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Players/FriendIcon.png differ diff --git a/Client2018/content/textures/ui/Settings/Players/FriendIcon@2x.png b/Client2018/content/textures/ui/Settings/Players/FriendIcon@2x.png new file mode 100644 index 0000000..420f532 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Players/FriendIcon@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Players/ReportFlagIcon.png b/Client2018/content/textures/ui/Settings/Players/ReportFlagIcon.png new file mode 100644 index 0000000..667ce8b Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Players/ReportFlagIcon.png differ diff --git a/Client2018/content/textures/ui/Settings/Players/ReportFlagIcon@2x.png b/Client2018/content/textures/ui/Settings/Players/ReportFlagIcon@2x.png new file mode 100644 index 0000000..e5346b0 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Players/ReportFlagIcon@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/Alert.png b/Client2018/content/textures/ui/Settings/Radial/Alert.png new file mode 100644 index 0000000..314d122 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/Alert.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/Alert@2x.png b/Client2018/content/textures/ui/Settings/Radial/Alert@2x.png new file mode 100644 index 0000000..6c3c966 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/Alert@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/Backpack.png b/Client2018/content/textures/ui/Settings/Radial/Backpack.png new file mode 100644 index 0000000..4658ae8 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/Backpack.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/Backpack@2x.png b/Client2018/content/textures/ui/Settings/Radial/Backpack@2x.png new file mode 100644 index 0000000..d222525 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/Backpack@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/Bottom.png b/Client2018/content/textures/ui/Settings/Radial/Bottom.png new file mode 100644 index 0000000..edb40d9 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/Bottom.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/BottomLeft.png b/Client2018/content/textures/ui/Settings/Radial/BottomLeft.png new file mode 100644 index 0000000..4560e63 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/BottomLeft.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/BottomLeftSelected.png b/Client2018/content/textures/ui/Settings/Radial/BottomLeftSelected.png new file mode 100644 index 0000000..f678b7d Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/BottomLeftSelected.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/BottomRight.png b/Client2018/content/textures/ui/Settings/Radial/BottomRight.png new file mode 100644 index 0000000..34ea9d2 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/BottomRight.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/BottomRightSelected.png b/Client2018/content/textures/ui/Settings/Radial/BottomRightSelected.png new file mode 100644 index 0000000..c044854 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/BottomRightSelected.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/BottomSelected.png b/Client2018/content/textures/ui/Settings/Radial/BottomSelected.png new file mode 100644 index 0000000..eed548c Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/BottomSelected.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/Chat.png b/Client2018/content/textures/ui/Settings/Radial/Chat.png new file mode 100644 index 0000000..b0bc68c Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/Chat.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/Chat@2x.png b/Client2018/content/textures/ui/Settings/Radial/Chat@2x.png new file mode 100644 index 0000000..1b9e5bc Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/Chat@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/EmptyBottom.png b/Client2018/content/textures/ui/Settings/Radial/EmptyBottom.png new file mode 100644 index 0000000..964257a Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/EmptyBottom.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/EmptyBottomLeft.png b/Client2018/content/textures/ui/Settings/Radial/EmptyBottomLeft.png new file mode 100644 index 0000000..a7b3a80 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/EmptyBottomLeft.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/EmptyBottomRight.png b/Client2018/content/textures/ui/Settings/Radial/EmptyBottomRight.png new file mode 100644 index 0000000..87e8935 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/EmptyBottomRight.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/EmptyTop.png b/Client2018/content/textures/ui/Settings/Radial/EmptyTop.png new file mode 100644 index 0000000..90da292 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/EmptyTop.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/EmptyTopLeft.png b/Client2018/content/textures/ui/Settings/Radial/EmptyTopLeft.png new file mode 100644 index 0000000..84fd9a4 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/EmptyTopLeft.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/EmptyTopRight.png b/Client2018/content/textures/ui/Settings/Radial/EmptyTopRight.png new file mode 100644 index 0000000..53be061 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/EmptyTopRight.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/Leave.png b/Client2018/content/textures/ui/Settings/Radial/Leave.png new file mode 100644 index 0000000..5f69290 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/Leave.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/Leave@2x.png b/Client2018/content/textures/ui/Settings/Radial/Leave@2x.png new file mode 100644 index 0000000..d4f66b5 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/Leave@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/Menu.png b/Client2018/content/textures/ui/Settings/Radial/Menu.png new file mode 100644 index 0000000..bff7118 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/Menu.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/Menu@2x.png b/Client2018/content/textures/ui/Settings/Radial/Menu@2x.png new file mode 100644 index 0000000..b56e6b6 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/Menu@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/PlayerList.png b/Client2018/content/textures/ui/Settings/Radial/PlayerList.png new file mode 100644 index 0000000..fae417d Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/PlayerList.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/PlayerList@2x.png b/Client2018/content/textures/ui/Settings/Radial/PlayerList@2x.png new file mode 100644 index 0000000..9a91e3e Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/PlayerList@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/RadialLabel.png b/Client2018/content/textures/ui/Settings/Radial/RadialLabel.png new file mode 100644 index 0000000..c7065c9 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/RadialLabel.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/RadialLabel@2x.png b/Client2018/content/textures/ui/Settings/Radial/RadialLabel@2x.png new file mode 100644 index 0000000..349aed3 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/RadialLabel@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/Top.png b/Client2018/content/textures/ui/Settings/Radial/Top.png new file mode 100644 index 0000000..a87d6eb Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/Top.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/TopLeft.png b/Client2018/content/textures/ui/Settings/Radial/TopLeft.png new file mode 100644 index 0000000..64b0ec1 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/TopLeft.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/TopLeftSelected.png b/Client2018/content/textures/ui/Settings/Radial/TopLeftSelected.png new file mode 100644 index 0000000..0069778 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/TopLeftSelected.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/TopRight.png b/Client2018/content/textures/ui/Settings/Radial/TopRight.png new file mode 100644 index 0000000..73831c9 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/TopRight.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/TopRightSelected.png b/Client2018/content/textures/ui/Settings/Radial/TopRightSelected.png new file mode 100644 index 0000000..9ead602 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/TopRightSelected.png differ diff --git a/Client2018/content/textures/ui/Settings/Radial/TopSelected.png b/Client2018/content/textures/ui/Settings/Radial/TopSelected.png new file mode 100644 index 0000000..6c73255 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Radial/TopSelected.png differ diff --git a/Client2018/content/textures/ui/Settings/ShareGame/icons.png b/Client2018/content/textures/ui/Settings/ShareGame/icons.png new file mode 100644 index 0000000..73ccfdb Binary files /dev/null and b/Client2018/content/textures/ui/Settings/ShareGame/icons.png differ diff --git a/Client2018/content/textures/ui/Settings/ShareGame/icons@2x.png b/Client2018/content/textures/ui/Settings/ShareGame/icons@2x.png new file mode 100644 index 0000000..beb7acc Binary files /dev/null and b/Client2018/content/textures/ui/Settings/ShareGame/icons@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/ShareGame/icons@3x.png b/Client2018/content/textures/ui/Settings/ShareGame/icons@3x.png new file mode 100644 index 0000000..ccc04e6 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/ShareGame/icons@3x.png differ diff --git a/Client2018/content/textures/ui/Settings/Slider/BarLeft.png b/Client2018/content/textures/ui/Settings/Slider/BarLeft.png new file mode 100644 index 0000000..1787ff0 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Slider/BarLeft.png differ diff --git a/Client2018/content/textures/ui/Settings/Slider/BarLeft@2x.png b/Client2018/content/textures/ui/Settings/Slider/BarLeft@2x.png new file mode 100644 index 0000000..b6bb751 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Slider/BarLeft@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Slider/BarRight.png b/Client2018/content/textures/ui/Settings/Slider/BarRight.png new file mode 100644 index 0000000..04bca90 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Slider/BarRight.png differ diff --git a/Client2018/content/textures/ui/Settings/Slider/BarRight@2x.png b/Client2018/content/textures/ui/Settings/Slider/BarRight@2x.png new file mode 100644 index 0000000..26e8a6f Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Slider/BarRight@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Slider/Left.png b/Client2018/content/textures/ui/Settings/Slider/Left.png new file mode 100644 index 0000000..c078d2c Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Slider/Left.png differ diff --git a/Client2018/content/textures/ui/Settings/Slider/Left@2x.png b/Client2018/content/textures/ui/Settings/Slider/Left@2x.png new file mode 100644 index 0000000..3ec089d Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Slider/Left@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Slider/Less.png b/Client2018/content/textures/ui/Settings/Slider/Less.png new file mode 100644 index 0000000..afbb0a6 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Slider/Less.png differ diff --git a/Client2018/content/textures/ui/Settings/Slider/More.png b/Client2018/content/textures/ui/Settings/Slider/More.png new file mode 100644 index 0000000..05edb54 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Slider/More.png differ diff --git a/Client2018/content/textures/ui/Settings/Slider/Right.png b/Client2018/content/textures/ui/Settings/Slider/Right.png new file mode 100644 index 0000000..a607724 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Slider/Right.png differ diff --git a/Client2018/content/textures/ui/Settings/Slider/Right@2x.png b/Client2018/content/textures/ui/Settings/Slider/Right@2x.png new file mode 100644 index 0000000..53ef3f8 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Slider/Right@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Slider/SelectedBarLeft.png b/Client2018/content/textures/ui/Settings/Slider/SelectedBarLeft.png new file mode 100644 index 0000000..6c98b40 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Slider/SelectedBarLeft.png differ diff --git a/Client2018/content/textures/ui/Settings/Slider/SelectedBarLeft@2x.png b/Client2018/content/textures/ui/Settings/Slider/SelectedBarLeft@2x.png new file mode 100644 index 0000000..6476f9e Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Slider/SelectedBarLeft@2x.png differ diff --git a/Client2018/content/textures/ui/Settings/Slider/SelectedBarRight.png b/Client2018/content/textures/ui/Settings/Slider/SelectedBarRight.png new file mode 100644 index 0000000..a0e0162 Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Slider/SelectedBarRight.png differ diff --git a/Client2018/content/textures/ui/Settings/Slider/SelectedBarRight@2x.png b/Client2018/content/textures/ui/Settings/Slider/SelectedBarRight@2x.png new file mode 100644 index 0000000..d5229bc Binary files /dev/null and b/Client2018/content/textures/ui/Settings/Slider/SelectedBarRight@2x.png differ diff --git a/Client2018/content/textures/ui/SingleButton.png b/Client2018/content/textures/ui/SingleButton.png new file mode 100644 index 0000000..27d2c0d Binary files /dev/null and b/Client2018/content/textures/ui/SingleButton.png differ diff --git a/Client2018/content/textures/ui/SingleButtonDown.png b/Client2018/content/textures/ui/SingleButtonDown.png new file mode 100644 index 0000000..4e50523 Binary files /dev/null and b/Client2018/content/textures/ui/SingleButtonDown.png differ diff --git a/Client2018/content/textures/ui/Slider-BKG-Center.png b/Client2018/content/textures/ui/Slider-BKG-Center.png new file mode 100644 index 0000000..7563293 Binary files /dev/null and b/Client2018/content/textures/ui/Slider-BKG-Center.png differ diff --git a/Client2018/content/textures/ui/Slider-BKG-Center@2x.png b/Client2018/content/textures/ui/Slider-BKG-Center@2x.png new file mode 100644 index 0000000..284dae0 Binary files /dev/null and b/Client2018/content/textures/ui/Slider-BKG-Center@2x.png differ diff --git a/Client2018/content/textures/ui/Slider-BKG-Left-Cap.png b/Client2018/content/textures/ui/Slider-BKG-Left-Cap.png new file mode 100644 index 0000000..ab76642 Binary files /dev/null and b/Client2018/content/textures/ui/Slider-BKG-Left-Cap.png differ diff --git a/Client2018/content/textures/ui/Slider-BKG-Left-Cap@2x.png b/Client2018/content/textures/ui/Slider-BKG-Left-Cap@2x.png new file mode 100644 index 0000000..defda45 Binary files /dev/null and b/Client2018/content/textures/ui/Slider-BKG-Left-Cap@2x.png differ diff --git a/Client2018/content/textures/ui/Slider-BKG-Right-Cap.png b/Client2018/content/textures/ui/Slider-BKG-Right-Cap.png new file mode 100644 index 0000000..82d19f3 Binary files /dev/null and b/Client2018/content/textures/ui/Slider-BKG-Right-Cap.png differ diff --git a/Client2018/content/textures/ui/Slider-BKG-Right-Cap@2x.png b/Client2018/content/textures/ui/Slider-BKG-Right-Cap@2x.png new file mode 100644 index 0000000..0dcd8be Binary files /dev/null and b/Client2018/content/textures/ui/Slider-BKG-Right-Cap@2x.png differ diff --git a/Client2018/content/textures/ui/Slider-Fill-Center.png b/Client2018/content/textures/ui/Slider-Fill-Center.png new file mode 100644 index 0000000..a9db65e Binary files /dev/null and b/Client2018/content/textures/ui/Slider-Fill-Center.png differ diff --git a/Client2018/content/textures/ui/Slider-Fill-Center@2x.png b/Client2018/content/textures/ui/Slider-Fill-Center@2x.png new file mode 100644 index 0000000..c3355cc Binary files /dev/null and b/Client2018/content/textures/ui/Slider-Fill-Center@2x.png differ diff --git a/Client2018/content/textures/ui/Slider-Fill-Left-Cap.png b/Client2018/content/textures/ui/Slider-Fill-Left-Cap.png new file mode 100644 index 0000000..5d796da Binary files /dev/null and b/Client2018/content/textures/ui/Slider-Fill-Left-Cap.png differ diff --git a/Client2018/content/textures/ui/Slider-Fill-Left-Cap@2x.png b/Client2018/content/textures/ui/Slider-Fill-Left-Cap@2x.png new file mode 100644 index 0000000..2ff6d0b Binary files /dev/null and b/Client2018/content/textures/ui/Slider-Fill-Left-Cap@2x.png differ diff --git a/Client2018/content/textures/ui/Slider-Fill-Right-Cap.png b/Client2018/content/textures/ui/Slider-Fill-Right-Cap.png new file mode 100644 index 0000000..188106b Binary files /dev/null and b/Client2018/content/textures/ui/Slider-Fill-Right-Cap.png differ diff --git a/Client2018/content/textures/ui/Slider-Fill-Right-Cap@2x.png b/Client2018/content/textures/ui/Slider-Fill-Right-Cap@2x.png new file mode 100644 index 0000000..84ae4c8 Binary files /dev/null and b/Client2018/content/textures/ui/Slider-Fill-Right-Cap@2x.png differ diff --git a/Client2018/content/textures/ui/Slider.png b/Client2018/content/textures/ui/Slider.png new file mode 100644 index 0000000..b11c5a3 Binary files /dev/null and b/Client2018/content/textures/ui/Slider.png differ diff --git a/Client2018/content/textures/ui/Slider_dn.png b/Client2018/content/textures/ui/Slider_dn.png new file mode 100644 index 0000000..2aa542e Binary files /dev/null and b/Client2018/content/textures/ui/Slider_dn.png differ diff --git a/Client2018/content/textures/ui/Slider_sel.png b/Client2018/content/textures/ui/Slider_sel.png new file mode 100644 index 0000000..2aa542e Binary files /dev/null and b/Client2018/content/textures/ui/Slider_sel.png differ diff --git a/Client2018/content/textures/ui/TixIcon.png b/Client2018/content/textures/ui/TixIcon.png new file mode 100644 index 0000000..8b08e62 Binary files /dev/null and b/Client2018/content/textures/ui/TixIcon.png differ diff --git a/Client2018/content/textures/ui/TopBar/Round.png b/Client2018/content/textures/ui/TopBar/Round.png new file mode 100644 index 0000000..a596802 Binary files /dev/null and b/Client2018/content/textures/ui/TopBar/Round.png differ diff --git a/Client2018/content/textures/ui/TopBar/dropshadow.png b/Client2018/content/textures/ui/TopBar/dropshadow.png new file mode 100644 index 0000000..0e9aa0b Binary files /dev/null and b/Client2018/content/textures/ui/TopBar/dropshadow.png differ diff --git a/Client2018/content/textures/ui/TopBar/dropshadow@2x.png b/Client2018/content/textures/ui/TopBar/dropshadow@2x.png new file mode 100644 index 0000000..9b14c0f Binary files /dev/null and b/Client2018/content/textures/ui/TopBar/dropshadow@2x.png differ diff --git a/Client2018/content/textures/ui/TouchControlsSheet.png b/Client2018/content/textures/ui/TouchControlsSheet.png new file mode 100644 index 0000000..da0293f Binary files /dev/null and b/Client2018/content/textures/ui/TouchControlsSheet.png differ diff --git a/Client2018/content/textures/ui/VR/Radial/Icons/2DUI.png b/Client2018/content/textures/ui/VR/Radial/Icons/2DUI.png new file mode 100644 index 0000000..23ce445 Binary files /dev/null and b/Client2018/content/textures/ui/VR/Radial/Icons/2DUI.png differ diff --git a/Client2018/content/textures/ui/VR/Radial/Icons/Backpack.png b/Client2018/content/textures/ui/VR/Radial/Icons/Backpack.png new file mode 100644 index 0000000..d8a6237 Binary files /dev/null and b/Client2018/content/textures/ui/VR/Radial/Icons/Backpack.png differ diff --git a/Client2018/content/textures/ui/VR/Radial/Icons/Recenter.png b/Client2018/content/textures/ui/VR/Radial/Icons/Recenter.png new file mode 100644 index 0000000..b21a586 Binary files /dev/null and b/Client2018/content/textures/ui/VR/Radial/Icons/Recenter.png differ diff --git a/Client2018/content/textures/ui/VR/Radial/SliceActive.png b/Client2018/content/textures/ui/VR/Radial/SliceActive.png new file mode 100644 index 0000000..8776b51 Binary files /dev/null and b/Client2018/content/textures/ui/VR/Radial/SliceActive.png differ diff --git a/Client2018/content/textures/ui/VR/Radial/SliceBackground.png b/Client2018/content/textures/ui/VR/Radial/SliceBackground.png new file mode 100644 index 0000000..fd9c037 Binary files /dev/null and b/Client2018/content/textures/ui/VR/Radial/SliceBackground.png differ diff --git a/Client2018/content/textures/ui/VR/Radial/SliceDisabled.png b/Client2018/content/textures/ui/VR/Radial/SliceDisabled.png new file mode 100644 index 0000000..ad926ad Binary files /dev/null and b/Client2018/content/textures/ui/VR/Radial/SliceDisabled.png differ diff --git a/Client2018/content/textures/ui/VR/VRPointerDiscBlue.png b/Client2018/content/textures/ui/VR/VRPointerDiscBlue.png new file mode 100644 index 0000000..ceab1b0 Binary files /dev/null and b/Client2018/content/textures/ui/VR/VRPointerDiscBlue.png differ diff --git a/Client2018/content/textures/ui/VR/VRPointerDiscRed.png b/Client2018/content/textures/ui/VR/VRPointerDiscRed.png new file mode 100644 index 0000000..ba88732 Binary files /dev/null and b/Client2018/content/textures/ui/VR/VRPointerDiscRed.png differ diff --git a/Client2018/content/textures/ui/VR/button.png b/Client2018/content/textures/ui/VR/button.png new file mode 100644 index 0000000..403dfde Binary files /dev/null and b/Client2018/content/textures/ui/VR/button.png differ diff --git a/Client2018/content/textures/ui/VR/buttonActive.png b/Client2018/content/textures/ui/VR/buttonActive.png new file mode 100644 index 0000000..f685760 Binary files /dev/null and b/Client2018/content/textures/ui/VR/buttonActive.png differ diff --git a/Client2018/content/textures/ui/VR/buttonBackground.png b/Client2018/content/textures/ui/VR/buttonBackground.png new file mode 100644 index 0000000..379241a Binary files /dev/null and b/Client2018/content/textures/ui/VR/buttonBackground.png differ diff --git a/Client2018/content/textures/ui/VR/buttonHover.png b/Client2018/content/textures/ui/VR/buttonHover.png new file mode 100644 index 0000000..916ddff Binary files /dev/null and b/Client2018/content/textures/ui/VR/buttonHover.png differ diff --git a/Client2018/content/textures/ui/VR/buttonSelected.png b/Client2018/content/textures/ui/VR/buttonSelected.png new file mode 100644 index 0000000..6759aea Binary files /dev/null and b/Client2018/content/textures/ui/VR/buttonSelected.png differ diff --git a/Client2018/content/textures/ui/VR/chat.png b/Client2018/content/textures/ui/VR/chat.png new file mode 100644 index 0000000..5017f20 Binary files /dev/null and b/Client2018/content/textures/ui/VR/chat.png differ diff --git a/Client2018/content/textures/ui/VR/circleWhite.png b/Client2018/content/textures/ui/VR/circleWhite.png new file mode 100644 index 0000000..728fe7b Binary files /dev/null and b/Client2018/content/textures/ui/VR/circleWhite.png differ diff --git a/Client2018/content/textures/ui/VR/closeButtonPadded.png b/Client2018/content/textures/ui/VR/closeButtonPadded.png new file mode 100644 index 0000000..0db5f8d Binary files /dev/null and b/Client2018/content/textures/ui/VR/closeButtonPadded.png differ diff --git a/Client2018/content/textures/ui/VR/hamburger.png b/Client2018/content/textures/ui/VR/hamburger.png new file mode 100644 index 0000000..a9d70c5 Binary files /dev/null and b/Client2018/content/textures/ui/VR/hamburger.png differ diff --git a/Client2018/content/textures/ui/VR/hoverPopupLeft.png b/Client2018/content/textures/ui/VR/hoverPopupLeft.png new file mode 100644 index 0000000..484b0d2 Binary files /dev/null and b/Client2018/content/textures/ui/VR/hoverPopupLeft.png differ diff --git a/Client2018/content/textures/ui/VR/hoverPopupMid.png b/Client2018/content/textures/ui/VR/hoverPopupMid.png new file mode 100644 index 0000000..0f55008 Binary files /dev/null and b/Client2018/content/textures/ui/VR/hoverPopupMid.png differ diff --git a/Client2018/content/textures/ui/VR/hoverPopupRight.png b/Client2018/content/textures/ui/VR/hoverPopupRight.png new file mode 100644 index 0000000..ea0b32a Binary files /dev/null and b/Client2018/content/textures/ui/VR/hoverPopupRight.png differ diff --git a/Client2018/content/textures/ui/VR/notifications.png b/Client2018/content/textures/ui/VR/notifications.png new file mode 100644 index 0000000..ce18573 Binary files /dev/null and b/Client2018/content/textures/ui/VR/notifications.png differ diff --git a/Client2018/content/textures/ui/VR/notifier_glow.png b/Client2018/content/textures/ui/VR/notifier_glow.png new file mode 100644 index 0000000..9483328 Binary files /dev/null and b/Client2018/content/textures/ui/VR/notifier_glow.png differ diff --git a/Client2018/content/textures/ui/VR/recenter.png b/Client2018/content/textures/ui/VR/recenter.png new file mode 100644 index 0000000..a287342 Binary files /dev/null and b/Client2018/content/textures/ui/VR/recenter.png differ diff --git a/Client2018/content/textures/ui/VR/recenterFrame.png b/Client2018/content/textures/ui/VR/recenterFrame.png new file mode 100644 index 0000000..1f02bdf Binary files /dev/null and b/Client2018/content/textures/ui/VR/recenterFrame.png differ diff --git a/Client2018/content/textures/ui/VR/rectBackground.png b/Client2018/content/textures/ui/VR/rectBackground.png new file mode 100644 index 0000000..758edfe Binary files /dev/null and b/Client2018/content/textures/ui/VR/rectBackground.png differ diff --git a/Client2018/content/textures/ui/VR/rectBackgroundWhite.png b/Client2018/content/textures/ui/VR/rectBackgroundWhite.png new file mode 100644 index 0000000..0446670 Binary files /dev/null and b/Client2018/content/textures/ui/VR/rectBackgroundWhite.png differ diff --git a/Client2018/content/textures/ui/VR/toggle2D.png b/Client2018/content/textures/ui/VR/toggle2D.png new file mode 100644 index 0000000..78def18 Binary files /dev/null and b/Client2018/content/textures/ui/VR/toggle2D.png differ diff --git a/Client2018/content/textures/ui/Vehicle/SpeedBar.png b/Client2018/content/textures/ui/Vehicle/SpeedBar.png new file mode 100644 index 0000000..1913f2b Binary files /dev/null and b/Client2018/content/textures/ui/Vehicle/SpeedBar.png differ diff --git a/Client2018/content/textures/ui/Vehicle/SpeedBar@2x.png b/Client2018/content/textures/ui/Vehicle/SpeedBar@2x.png new file mode 100644 index 0000000..3935f79 Binary files /dev/null and b/Client2018/content/textures/ui/Vehicle/SpeedBar@2x.png differ diff --git a/Client2018/content/textures/ui/Vehicle/SpeedBarBKG.png b/Client2018/content/textures/ui/Vehicle/SpeedBarBKG.png new file mode 100644 index 0000000..af55200 Binary files /dev/null and b/Client2018/content/textures/ui/Vehicle/SpeedBarBKG.png differ diff --git a/Client2018/content/textures/ui/Vehicle/SpeedBarBKG@2x.png b/Client2018/content/textures/ui/Vehicle/SpeedBarBKG@2x.png new file mode 100644 index 0000000..b85a6e5 Binary files /dev/null and b/Client2018/content/textures/ui/Vehicle/SpeedBarBKG@2x.png differ diff --git a/Client2018/content/textures/ui/Vehicle/SpeedBarEmpty.png b/Client2018/content/textures/ui/Vehicle/SpeedBarEmpty.png new file mode 100644 index 0000000..7889a14 Binary files /dev/null and b/Client2018/content/textures/ui/Vehicle/SpeedBarEmpty.png differ diff --git a/Client2018/content/textures/ui/Vehicle/SpeedBarEmpty@2x.png b/Client2018/content/textures/ui/Vehicle/SpeedBarEmpty@2x.png new file mode 100644 index 0000000..880b096 Binary files /dev/null and b/Client2018/content/textures/ui/Vehicle/SpeedBarEmpty@2x.png differ diff --git a/Client2018/content/textures/ui/account_over13.png b/Client2018/content/textures/ui/account_over13.png new file mode 100644 index 0000000..0a91b6c Binary files /dev/null and b/Client2018/content/textures/ui/account_over13.png differ diff --git a/Client2018/content/textures/ui/account_under13.png b/Client2018/content/textures/ui/account_under13.png new file mode 100644 index 0000000..8ecf24c Binary files /dev/null and b/Client2018/content/textures/ui/account_under13.png differ diff --git a/Client2018/content/textures/ui/btn_grey.png b/Client2018/content/textures/ui/btn_grey.png new file mode 100644 index 0000000..3ae3fff Binary files /dev/null and b/Client2018/content/textures/ui/btn_grey.png differ diff --git a/Client2018/content/textures/ui/btn_greyTransp.png b/Client2018/content/textures/ui/btn_greyTransp.png new file mode 100644 index 0000000..0a39a1e Binary files /dev/null and b/Client2018/content/textures/ui/btn_greyTransp.png differ diff --git a/Client2018/content/textures/ui/btn_newBlue.png b/Client2018/content/textures/ui/btn_newBlue.png new file mode 100644 index 0000000..d7b482e Binary files /dev/null and b/Client2018/content/textures/ui/btn_newBlue.png differ diff --git a/Client2018/content/textures/ui/btn_newBlue@2x.png b/Client2018/content/textures/ui/btn_newBlue@2x.png new file mode 100644 index 0000000..ca2f2e9 Binary files /dev/null and b/Client2018/content/textures/ui/btn_newBlue@2x.png differ diff --git a/Client2018/content/textures/ui/btn_newBlueGlow.png b/Client2018/content/textures/ui/btn_newBlueGlow.png new file mode 100644 index 0000000..1b49a6c Binary files /dev/null and b/Client2018/content/textures/ui/btn_newBlueGlow.png differ diff --git a/Client2018/content/textures/ui/btn_newBlueGlow@2x.png b/Client2018/content/textures/ui/btn_newBlueGlow@2x.png new file mode 100644 index 0000000..379ec28 Binary files /dev/null and b/Client2018/content/textures/ui/btn_newBlueGlow@2x.png differ diff --git a/Client2018/content/textures/ui/btn_newGrey.png b/Client2018/content/textures/ui/btn_newGrey.png new file mode 100644 index 0000000..fb6cb06 Binary files /dev/null and b/Client2018/content/textures/ui/btn_newGrey.png differ diff --git a/Client2018/content/textures/ui/btn_newGrey@2x.png b/Client2018/content/textures/ui/btn_newGrey@2x.png new file mode 100644 index 0000000..b877522 Binary files /dev/null and b/Client2018/content/textures/ui/btn_newGrey@2x.png differ diff --git a/Client2018/content/textures/ui/btn_newGreyGlow.png b/Client2018/content/textures/ui/btn_newGreyGlow.png new file mode 100644 index 0000000..fc3e25a Binary files /dev/null and b/Client2018/content/textures/ui/btn_newGreyGlow.png differ diff --git a/Client2018/content/textures/ui/btn_newGreyGlow@2x.png b/Client2018/content/textures/ui/btn_newGreyGlow@2x.png new file mode 100644 index 0000000..017b746 Binary files /dev/null and b/Client2018/content/textures/ui/btn_newGreyGlow@2x.png differ diff --git a/Client2018/content/textures/ui/btn_newWhite.png b/Client2018/content/textures/ui/btn_newWhite.png new file mode 100644 index 0000000..4e415ee Binary files /dev/null and b/Client2018/content/textures/ui/btn_newWhite.png differ diff --git a/Client2018/content/textures/ui/btn_newWhite@2x.png b/Client2018/content/textures/ui/btn_newWhite@2x.png new file mode 100644 index 0000000..82c0825 Binary files /dev/null and b/Client2018/content/textures/ui/btn_newWhite@2x.png differ diff --git a/Client2018/content/textures/ui/btn_newWhiteGlow.png b/Client2018/content/textures/ui/btn_newWhiteGlow.png new file mode 100644 index 0000000..0373243 Binary files /dev/null and b/Client2018/content/textures/ui/btn_newWhiteGlow.png differ diff --git a/Client2018/content/textures/ui/btn_newWhiteGlow@2x.png b/Client2018/content/textures/ui/btn_newWhiteGlow@2x.png new file mode 100644 index 0000000..e03f4dc Binary files /dev/null and b/Client2018/content/textures/ui/btn_newWhiteGlow@2x.png differ diff --git a/Client2018/content/textures/ui/btn_red.png b/Client2018/content/textures/ui/btn_red.png new file mode 100644 index 0000000..4ab7dec Binary files /dev/null and b/Client2018/content/textures/ui/btn_red.png differ diff --git a/Client2018/content/textures/ui/btn_redGlow.png b/Client2018/content/textures/ui/btn_redGlow.png new file mode 100644 index 0000000..5d6249e Binary files /dev/null and b/Client2018/content/textures/ui/btn_redGlow.png differ diff --git a/Client2018/content/textures/ui/btn_white.png b/Client2018/content/textures/ui/btn_white.png new file mode 100644 index 0000000..4c6d4c8 Binary files /dev/null and b/Client2018/content/textures/ui/btn_white.png differ diff --git a/Client2018/content/textures/ui/chatBubble_blue_notify_bkg.png b/Client2018/content/textures/ui/chatBubble_blue_notify_bkg.png new file mode 100644 index 0000000..a7925dc Binary files /dev/null and b/Client2018/content/textures/ui/chatBubble_blue_notify_bkg.png differ diff --git a/Client2018/content/textures/ui/chatBubble_green_notify_bkg.png b/Client2018/content/textures/ui/chatBubble_green_notify_bkg.png new file mode 100644 index 0000000..1f38d6a Binary files /dev/null and b/Client2018/content/textures/ui/chatBubble_green_notify_bkg.png differ diff --git a/Client2018/content/textures/ui/chatBubble_red_notify_bkg.png b/Client2018/content/textures/ui/chatBubble_red_notify_bkg.png new file mode 100644 index 0000000..7503309 Binary files /dev/null and b/Client2018/content/textures/ui/chatBubble_red_notify_bkg.png differ diff --git a/Client2018/content/textures/ui/chatBubble_white_notify_bkg.png b/Client2018/content/textures/ui/chatBubble_white_notify_bkg.png new file mode 100644 index 0000000..59be44c Binary files /dev/null and b/Client2018/content/textures/ui/chatBubble_white_notify_bkg.png differ diff --git a/Client2018/content/textures/ui/chat_teamButton.png b/Client2018/content/textures/ui/chat_teamButton.png new file mode 100644 index 0000000..ecceb50 Binary files /dev/null and b/Client2018/content/textures/ui/chat_teamButton.png differ diff --git a/Client2018/content/textures/ui/chat_teamButton@2x.png b/Client2018/content/textures/ui/chat_teamButton@2x.png new file mode 100644 index 0000000..d36c05e Binary files /dev/null and b/Client2018/content/textures/ui/chat_teamButton@2x.png differ diff --git a/Client2018/content/textures/ui/dialog_blue.png b/Client2018/content/textures/ui/dialog_blue.png new file mode 100644 index 0000000..96b9adb Binary files /dev/null and b/Client2018/content/textures/ui/dialog_blue.png differ diff --git a/Client2018/content/textures/ui/dialog_blue@2x.png b/Client2018/content/textures/ui/dialog_blue@2x.png new file mode 100644 index 0000000..c40fc31 Binary files /dev/null and b/Client2018/content/textures/ui/dialog_blue@2x.png differ diff --git a/Client2018/content/textures/ui/dialog_green.png b/Client2018/content/textures/ui/dialog_green.png new file mode 100644 index 0000000..64d012c Binary files /dev/null and b/Client2018/content/textures/ui/dialog_green.png differ diff --git a/Client2018/content/textures/ui/dialog_green@2x.png b/Client2018/content/textures/ui/dialog_green@2x.png new file mode 100644 index 0000000..775b4f9 Binary files /dev/null and b/Client2018/content/textures/ui/dialog_green@2x.png differ diff --git a/Client2018/content/textures/ui/dialog_purpose_help.png b/Client2018/content/textures/ui/dialog_purpose_help.png new file mode 100644 index 0000000..311d441 Binary files /dev/null and b/Client2018/content/textures/ui/dialog_purpose_help.png differ diff --git a/Client2018/content/textures/ui/dialog_purpose_quest.png b/Client2018/content/textures/ui/dialog_purpose_quest.png new file mode 100644 index 0000000..14efd26 Binary files /dev/null and b/Client2018/content/textures/ui/dialog_purpose_quest.png differ diff --git a/Client2018/content/textures/ui/dialog_purpose_shop.png b/Client2018/content/textures/ui/dialog_purpose_shop.png new file mode 100644 index 0000000..df2bb7f Binary files /dev/null and b/Client2018/content/textures/ui/dialog_purpose_shop.png differ diff --git a/Client2018/content/textures/ui/dialog_red.png b/Client2018/content/textures/ui/dialog_red.png new file mode 100644 index 0000000..6b3c644 Binary files /dev/null and b/Client2018/content/textures/ui/dialog_red.png differ diff --git a/Client2018/content/textures/ui/dialog_red@2x.png b/Client2018/content/textures/ui/dialog_red@2x.png new file mode 100644 index 0000000..c62b873 Binary files /dev/null and b/Client2018/content/textures/ui/dialog_red@2x.png differ diff --git a/Client2018/content/textures/ui/dialog_tail.png b/Client2018/content/textures/ui/dialog_tail.png new file mode 100644 index 0000000..b9686bc Binary files /dev/null and b/Client2018/content/textures/ui/dialog_tail.png differ diff --git a/Client2018/content/textures/ui/dialog_tail@2x.png b/Client2018/content/textures/ui/dialog_tail@2x.png new file mode 100644 index 0000000..ff18256 Binary files /dev/null and b/Client2018/content/textures/ui/dialog_tail@2x.png differ diff --git a/Client2018/content/textures/ui/dialog_white.png b/Client2018/content/textures/ui/dialog_white.png new file mode 100644 index 0000000..9911129 Binary files /dev/null and b/Client2018/content/textures/ui/dialog_white.png differ diff --git a/Client2018/content/textures/ui/dialog_white@2x.png b/Client2018/content/textures/ui/dialog_white@2x.png new file mode 100644 index 0000000..f80f843 Binary files /dev/null and b/Client2018/content/textures/ui/dialog_white@2x.png differ diff --git a/Client2018/content/textures/ui/dropdown_arrow.png b/Client2018/content/textures/ui/dropdown_arrow.png new file mode 100644 index 0000000..eddc0be Binary files /dev/null and b/Client2018/content/textures/ui/dropdown_arrow.png differ diff --git a/Client2018/content/textures/ui/dropdown_arrow@2x.png b/Client2018/content/textures/ui/dropdown_arrow@2x.png new file mode 100644 index 0000000..0db921e Binary files /dev/null and b/Client2018/content/textures/ui/dropdown_arrow@2x.png differ diff --git a/Client2018/content/textures/ui/homeButton.png b/Client2018/content/textures/ui/homeButton.png new file mode 100644 index 0000000..b9ad006 Binary files /dev/null and b/Client2018/content/textures/ui/homeButton.png differ diff --git a/Client2018/content/textures/ui/homeButton@2x.png b/Client2018/content/textures/ui/homeButton@2x.png new file mode 100644 index 0000000..2c2cdc0 Binary files /dev/null and b/Client2018/content/textures/ui/homeButton@2x.png differ diff --git a/Client2018/content/textures/ui/icon_BC-16.png b/Client2018/content/textures/ui/icon_BC-16.png new file mode 100644 index 0000000..dedd8d4 Binary files /dev/null and b/Client2018/content/textures/ui/icon_BC-16.png differ diff --git a/Client2018/content/textures/ui/icon_OBC-16.png b/Client2018/content/textures/ui/icon_OBC-16.png new file mode 100644 index 0000000..e539d5d Binary files /dev/null and b/Client2018/content/textures/ui/icon_OBC-16.png differ diff --git a/Client2018/content/textures/ui/icon_TBC-16.png b/Client2018/content/textures/ui/icon_TBC-16.png new file mode 100644 index 0000000..208dde3 Binary files /dev/null and b/Client2018/content/textures/ui/icon_TBC-16.png differ diff --git a/Client2018/content/textures/ui/icon_admin-16.png b/Client2018/content/textures/ui/icon_admin-16.png new file mode 100644 index 0000000..ccfb09a Binary files /dev/null and b/Client2018/content/textures/ui/icon_admin-16.png differ diff --git a/Client2018/content/textures/ui/icon_follower-16.png b/Client2018/content/textures/ui/icon_follower-16.png new file mode 100644 index 0000000..20dd9f1 Binary files /dev/null and b/Client2018/content/textures/ui/icon_follower-16.png differ diff --git a/Client2018/content/textures/ui/icon_following-16.png b/Client2018/content/textures/ui/icon_following-16.png new file mode 100644 index 0000000..a75dd9f Binary files /dev/null and b/Client2018/content/textures/ui/icon_following-16.png differ diff --git a/Client2018/content/textures/ui/icon_friendrequestrecieved-16.png b/Client2018/content/textures/ui/icon_friendrequestrecieved-16.png new file mode 100644 index 0000000..f7972f1 Binary files /dev/null and b/Client2018/content/textures/ui/icon_friendrequestrecieved-16.png differ diff --git a/Client2018/content/textures/ui/icon_friendrequestsent_16.png b/Client2018/content/textures/ui/icon_friendrequestsent_16.png new file mode 100644 index 0000000..947a5cf Binary files /dev/null and b/Client2018/content/textures/ui/icon_friendrequestsent_16.png differ diff --git a/Client2018/content/textures/ui/icon_friends_16.png b/Client2018/content/textures/ui/icon_friends_16.png new file mode 100644 index 0000000..17c5369 Binary files /dev/null and b/Client2018/content/textures/ui/icon_friends_16.png differ diff --git a/Client2018/content/textures/ui/icon_intern-16.png b/Client2018/content/textures/ui/icon_intern-16.png new file mode 100644 index 0000000..b4afb27 Binary files /dev/null and b/Client2018/content/textures/ui/icon_intern-16.png differ diff --git a/Client2018/content/textures/ui/icon_mutualfollowing-16.png b/Client2018/content/textures/ui/icon_mutualfollowing-16.png new file mode 100644 index 0000000..40adece Binary files /dev/null and b/Client2018/content/textures/ui/icon_mutualfollowing-16.png differ diff --git a/Client2018/content/textures/ui/icon_placeowner.png b/Client2018/content/textures/ui/icon_placeowner.png new file mode 100644 index 0000000..30f66ff Binary files /dev/null and b/Client2018/content/textures/ui/icon_placeowner.png differ diff --git a/Client2018/content/textures/ui/icon_star-16.png b/Client2018/content/textures/ui/icon_star-16.png new file mode 100644 index 0000000..189b9ca Binary files /dev/null and b/Client2018/content/textures/ui/icon_star-16.png differ diff --git a/Client2018/content/textures/ui/mouseLock_off.png b/Client2018/content/textures/ui/mouseLock_off.png new file mode 100644 index 0000000..c0b562a Binary files /dev/null and b/Client2018/content/textures/ui/mouseLock_off.png differ diff --git a/Client2018/content/textures/ui/mouseLock_off@2x.png b/Client2018/content/textures/ui/mouseLock_off@2x.png new file mode 100644 index 0000000..3773877 Binary files /dev/null and b/Client2018/content/textures/ui/mouseLock_off@2x.png differ diff --git a/Client2018/content/textures/ui/mouseLock_on.png b/Client2018/content/textures/ui/mouseLock_on.png new file mode 100644 index 0000000..9728774 Binary files /dev/null and b/Client2018/content/textures/ui/mouseLock_on.png differ diff --git a/Client2018/content/textures/ui/mouseLock_on@2x.png b/Client2018/content/textures/ui/mouseLock_on@2x.png new file mode 100644 index 0000000..c789d46 Binary files /dev/null and b/Client2018/content/textures/ui/mouseLock_on@2x.png differ diff --git a/Client2018/content/textures/ui/move.png b/Client2018/content/textures/ui/move.png new file mode 100644 index 0000000..d081458 Binary files /dev/null and b/Client2018/content/textures/ui/move.png differ diff --git a/Client2018/content/textures/ui/newBkg_square.png b/Client2018/content/textures/ui/newBkg_square.png new file mode 100644 index 0000000..901ffc1 Binary files /dev/null and b/Client2018/content/textures/ui/newBkg_square.png differ diff --git a/Client2018/content/textures/ui/newBkg_square@2x.png b/Client2018/content/textures/ui/newBkg_square@2x.png new file mode 100644 index 0000000..6956f48 Binary files /dev/null and b/Client2018/content/textures/ui/newBkg_square@2x.png differ diff --git a/Client2018/content/textures/ui/scroll-bottom.png b/Client2018/content/textures/ui/scroll-bottom.png new file mode 100644 index 0000000..f186156 Binary files /dev/null and b/Client2018/content/textures/ui/scroll-bottom.png differ diff --git a/Client2018/content/textures/ui/scroll-bottom@2x.png b/Client2018/content/textures/ui/scroll-bottom@2x.png new file mode 100644 index 0000000..3fb978f Binary files /dev/null and b/Client2018/content/textures/ui/scroll-bottom@2x.png differ diff --git a/Client2018/content/textures/ui/scroll-middle.png b/Client2018/content/textures/ui/scroll-middle.png new file mode 100644 index 0000000..1a7cfa7 Binary files /dev/null and b/Client2018/content/textures/ui/scroll-middle.png differ diff --git a/Client2018/content/textures/ui/scroll-middle@2x.png b/Client2018/content/textures/ui/scroll-middle@2x.png new file mode 100644 index 0000000..a101d48 Binary files /dev/null and b/Client2018/content/textures/ui/scroll-middle@2x.png differ diff --git a/Client2018/content/textures/ui/scroll-top.png b/Client2018/content/textures/ui/scroll-top.png new file mode 100644 index 0000000..1c85194 Binary files /dev/null and b/Client2018/content/textures/ui/scroll-top.png differ diff --git a/Client2018/content/textures/ui/scroll-top@2x.png b/Client2018/content/textures/ui/scroll-top@2x.png new file mode 100644 index 0000000..0720737 Binary files /dev/null and b/Client2018/content/textures/ui/scroll-top@2x.png differ diff --git a/Client2018/content/textures/ui/scrollbar.png b/Client2018/content/textures/ui/scrollbar.png new file mode 100644 index 0000000..1a3f361 Binary files /dev/null and b/Client2018/content/textures/ui/scrollbar.png differ diff --git a/Client2018/content/textures/ui/scrollbuttonDown.png b/Client2018/content/textures/ui/scrollbuttonDown.png new file mode 100644 index 0000000..fc6be92 Binary files /dev/null and b/Client2018/content/textures/ui/scrollbuttonDown.png differ diff --git a/Client2018/content/textures/ui/scrollbuttonDown_dn.png b/Client2018/content/textures/ui/scrollbuttonDown_dn.png new file mode 100644 index 0000000..38f1d5f Binary files /dev/null and b/Client2018/content/textures/ui/scrollbuttonDown_dn.png differ diff --git a/Client2018/content/textures/ui/scrollbuttonDown_ds.png b/Client2018/content/textures/ui/scrollbuttonDown_ds.png new file mode 100644 index 0000000..fb7f2f3 Binary files /dev/null and b/Client2018/content/textures/ui/scrollbuttonDown_ds.png differ diff --git a/Client2018/content/textures/ui/scrollbuttonDown_ovr.png b/Client2018/content/textures/ui/scrollbuttonDown_ovr.png new file mode 100644 index 0000000..38f1d5f Binary files /dev/null and b/Client2018/content/textures/ui/scrollbuttonDown_ovr.png differ diff --git a/Client2018/content/textures/ui/scrollbuttonUp.png b/Client2018/content/textures/ui/scrollbuttonUp.png new file mode 100644 index 0000000..311b520 Binary files /dev/null and b/Client2018/content/textures/ui/scrollbuttonUp.png differ diff --git a/Client2018/content/textures/ui/scrollbuttonUp_dn.png b/Client2018/content/textures/ui/scrollbuttonUp_dn.png new file mode 100644 index 0000000..aef3474 Binary files /dev/null and b/Client2018/content/textures/ui/scrollbuttonUp_dn.png differ diff --git a/Client2018/content/textures/ui/scrollbuttonUp_ds.png b/Client2018/content/textures/ui/scrollbuttonUp_ds.png new file mode 100644 index 0000000..551dbe7 Binary files /dev/null and b/Client2018/content/textures/ui/scrollbuttonUp_ds.png differ diff --git a/Client2018/content/textures/ui/scrollbuttonUp_ovr.png b/Client2018/content/textures/ui/scrollbuttonUp_ovr.png new file mode 100644 index 0000000..aef3474 Binary files /dev/null and b/Client2018/content/textures/ui/scrollbuttonUp_ovr.png differ diff --git a/Client2018/content/textures/ui/slider_new_tab.png b/Client2018/content/textures/ui/slider_new_tab.png new file mode 100644 index 0000000..ccc9ee1 Binary files /dev/null and b/Client2018/content/textures/ui/slider_new_tab.png differ diff --git a/Client2018/content/textures/ui/slider_new_tab@2x.png b/Client2018/content/textures/ui/slider_new_tab@2x.png new file mode 100644 index 0000000..07231fc Binary files /dev/null and b/Client2018/content/textures/ui/slider_new_tab@2x.png differ diff --git a/Client2018/content/textures/ui/vr_active.png b/Client2018/content/textures/ui/vr_active.png new file mode 100644 index 0000000..bda5347 Binary files /dev/null and b/Client2018/content/textures/ui/vr_active.png differ diff --git a/Client2018/content/textures/ui/vr_idle.png b/Client2018/content/textures/ui/vr_idle.png new file mode 100644 index 0000000..62b95c8 Binary files /dev/null and b/Client2018/content/textures/ui/vr_idle.png differ diff --git a/Client2018/content/textures/whiteCircle.png b/Client2018/content/textures/whiteCircle.png new file mode 100644 index 0000000..e1e5e4c Binary files /dev/null and b/Client2018/content/textures/whiteCircle.png differ diff --git a/Client2018/content/translations/CoreScriptLocalization.csv b/Client2018/content/translations/CoreScriptLocalization.csv new file mode 100644 index 0000000..adcf055 --- /dev/null +++ b/Client2018/content/translations/CoreScriptLocalization.csv @@ -0,0 +1,434 @@ +Key,Context,Example,Source,de,en-us,es,es-es,fr,it,ja,ko,pt,pt-br,ru,zh-cn,zh-tw +Average,,,,Durchschnitt,Average,Promedio,Promedio,Moyen,Media,平均,평균,Média,Média,Среднее,平均,平均 +BACKPACK_SEARCH,,,,Suchen,Search,Buscar,Buscar,Rechercher,Cerca,サーチ,검색,Pesquisar,Pesquisar,Поиск,搜索,搜尋 +Current,,,,Aktuell,Current,Actual,Actual,Actuel,Attuale,現在,현재,Atual,Atual,Текущее,当前,目前 +FriendPlayerPrompt.DoPromptRequestFriendPlayer,,,,Möchtest du {RBX_NAME} eine Freundesanfrage senden?,Would you like to send {RBX_NAME} a Friend Request?,¿Quieres enviar a {RBX_NAME} una solicitud de amistad?,¿Quieres enviar a {RBX_NAME} una solicitud de amistad?,Souhaitez-vous envoyer une demande d'ami à {RBX_NAME} ?,Vuoi inviare una richiesta di amicizia a {RBX_NAME}?,{RBX_NAME} に友達リクエストを送りますか?,{RBX_NAME}님에게 친구 요청을 보낼까요?,Gostaria de enviar um pedido de amizade para {RBX_NAME}?,Gostaria de enviar um pedido de amizade para {RBX_NAME}?,Отправить пользователю {RBX_NAME} запрос дружбы?,你想要向“{RBX_NAME}”发送好友请求吗?,您是否要寄給{RBX_NAME}交友請求? +FriendPlayerPrompt.DoPromptUnfriendPlayer,,,,Möchtest du {RBX_NAME} von deiner Freundesliste entfernen?,Would you like to remove {RBX_NAME} from your friends list?,¿Quieres eliminar a {RBX_NAME} de tu lista de amigos?,¿Quieres eliminar a {RBX_NAME} de tu lista de amigos?,Souhaitez-vous retirer {RBX_NAME} de votre liste d'amis ?,Vuoi rimuovere {RBX_NAME} dalla tua lista amici?,{RBX_NAME} を友達リストから削除しますか?,{RBX_NAME}님을 친구 목록에서 삭제할까요?,Gostaria de remover {RBX_NAME} da sua lista de amigos?,Gostaria de remover {RBX_NAME} da sua lista de amigos?,Удалить пользователя {RBX_NAME} из списка друзей?,你想要将“{RBX_NAME}”从你的好友名单中移除吗?,您是否要將{RBX_NAME}自朋友清單移除? +FriendPlayerPrompt.promptCompletedCallback.AtFriendLimit,,,,"Du kannst {RBX_NAME} keine Freundesanfrage senden, da dieser Spieler die max. Anzahl an Freunden erreicht hat.",You can not send a friend request to {RBX_NAME} because they are at the max friend limit.,No puedes enviar una solicitud de amistad a {RBX_NAME} porque ha alcanzado el límite máximo de amigos.,No puedes enviar una solicitud de amistad a {RBX_NAME} porque ha alcanzado el límite máximo de amigos.,"Vous ne pouvez pas envoyer une demande d'ami à {RBX_NAME}, car il a atteint sa limite maximale d'amis.",Non puoi inviare una richiesta di amicizia a {RBX_NAME} perché ha raggiunto il limite massimo di amici.,{RBX_NAME}の友達が最大数に達しているため友達リクエストが送れませんでした。,대상의 최대 친구 목록이 초과되어 {RBX_NAME}에게 친구 요청을 보낼 수 없어요.,Você não pode enviar um pedido de amizade para {RBX_NAME} pois ele(a) alcançou o limite máximo de amigos.,Você não pode enviar um pedido de amizade para {RBX_NAME} pois ele(a) alcançou o limite máximo de amigos.,Нельзя отправить запрос дружбы пользователю {RBX_NAME}: количество его друзей достигло максимума.,由于对方已达到好友数量上限,你无法向“{RBX_NAME}”发送好友请求。,您無法寄送交友請求給{RBX_NAME},原因是對方的朋友已達上限。 +FriendPlayerPrompt.promptCompletedCallback.UnknownError,,,,Beim Senden der Freundesanfrage an {RBX_NAME} ist ein Fehler aufgetreten. Bitte versuche es später erneut.,An error occurred while sending {RBX_NAME} a friend request. Please try again later.,Se ha producido un error al enviar una solicitud de amistad a {RBX_NAME}. Inténtalo de nuevo más tarde.,Se ha producido un error al enviar una solicitud de amistad a {RBX_NAME}. Inténtalo de nuevo más tarde.,Une erreur est survenue lors de l'envoi de la demande d'ami à {RBX_NAME}. Veuillez réessayer plus tard.,Si è verificato un errore nell'invio della richiesta di amicizia a {RBX_NAME}. Riprova più tardi.,{RBX_NAME} への友達リクエスト中にエラーが発生しました。しばらくしてからやり直してください。,{RBX_NAME}님에게 친구 요청 전송 중 오류 발생. 나중에 다시 시도하세요.,Um erro ocorreu ao enviar um pedido de amizade para {RBX_NAME}. Tente de novo mais tarde.,Um erro ocorreu ao enviar um pedido de amizade para {RBX_NAME}. Tente de novo mais tarde.,При отправлении запроса дружбы пользователю {RBX_NAME} произошла ошибка. Повторите попытку позже.,向“{RBX_NAME}”发送好友请求时发生错误。请稍候重试。,寄給{RBX_NAME}交友請求時發生錯誤。請稍後再試。 +KEY_DESCRIPTION_OPTIONAL,,,,Optional,Optional,Opcional,Opcional,Optionnel,Opzionale,オプション,선택 항목,Opcional,Opcional,По усмотрению,可选,可選填 +KEY_DESCRIPTION_SHORT_DECRIPTION_OPTIONAL,,,,Kurzbeschreibung (optional),Short Description (Optional),Descripción breve (opcional),Descripción breve (opcional),Description brève (facultatif),Descrizione breve (opzionale),簡単な説明(オプション),간략한 설명(선택 항목),Breve descrição (opcional),Breve descrição (opcional),Короткое описание (необязательно),短描述(可选),簡短描述(可選填) +KEY_PLAYER_IDLE_DISCONNECT,,,,"Deine Verbindung wurde getrennt, da du {RBX_NUMBER} Minuten lang untätig warst.",You were disconnected for being idle {RBX_NUMBER} minutes,Se te ha desconectado por no mostrar actividad durante {RBX_NUMBER} minutos.,Se te ha desconectado por no mostrar actividad durante {RBX_NUMBER} minutos.,Vous avez été déconnecté car vous êtes resté inactif durant {RBX_NUMBER} minutes.,Sei stato disconnesso perché inattivo per {RBX_NUMBER} minuti,{RBX_NUMBER} 分間アイドル状態だったので切断されました。,{RBX_NUMBER}분 동안 기본 상태로 있어 연결이 끊겼어요,Você perdeu a conexão por ficar inativo(a) por {RBX_NUMBER} minutos,Você perdeu a conexão por ficar inativo(a) por {RBX_NUMBER} minutos,"Вы отключены, так как не были активны в течение {RBX_NUMBER} мин.",由于闲置超过 {RBX_NUMBER} 分钟,你已被断开,您的連線中斷導因於閒置 {RBX_NUMBER} 分鐘 +Network,,,,(Netzwerk),(Network),(Red),(Red),(Réseau),(Rete),(ネットワーク),(네트워크),(Rede),(Rede),(Сеть),(网络),(網路) +NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Decreased,,,,Verringert auf {RBX_NUMBER},Decreased to {RBX_NUMBER},Reducción a {RBX_NUMBER},Reducción a {RBX_NUMBER},Réduit jusqu'à {RBX_NUMBER},Sceso a {RBX_NUMBER},{RBX_NUMBER}に減少,{RBX_NUMBER}(으)로 감소,Reduzido para {RBX_NUMBER},Reduzido para {RBX_NUMBER},Уменьшено до {RBX_NUMBER},减至 {RBX_NUMBER},減少至{RBX_NUMBER} +NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Increased,,,,Erhöht auf {RBX_NUMBER},Increased to {RBX_NUMBER},Aumento hasta {RBX_NUMBER},Aumento hasta {RBX_NUMBER},Augmenté jusqu'à {RBX_NUMBER},Salito a {RBX_NUMBER},{RBX_NUMBER}に増加,{RBX_NUMBER}(으)로 증가,Aumentado para {RBX_NUMBER},Aumentado para {RBX_NUMBER},Увеличено до {RBX_NUMBER},增至 {RBX_NUMBER},增加至{RBX_NUMBER} +NotificationScript2.FriendRequestEvent.Accept,,,,Du bist jetzt Freunde mit {RBX_NAME}!,You are now friends with {RBX_NAME}!,Ahora eres amigo de {RBX_NAME}.,Ahora eres amigo de {RBX_NAME}.,Vous êtes maintenant ami avec {RBX_NAME}!,Sei ora amici di {RBX_NAME}!,あなたは{RBX_NAME}の友達です!,이제 {RBX_NAME}님과 친구예요!,Agora você é amigo do {RBX_NAME}!,Agora você é amigo do {RBX_NAME}!,Вы теперь друзья с {RBX_NAME}!,你现在与 {RBX_NAME} 成为好友了!,您現在與{RBX_NAME}已是朋友! +NotificationScript2.NewFollower,,,,Neuer Follower {RBX_NAME} folgt dir jetzt!,New Follower {RBX_NAME} is now following you!,Seguidor nuevo: ¡{RBX_NAME} te está siguiendo!,Seguidor nuevo: ¡{RBX_NAME} te está siguiendo!,"Désormais, le nouvel abonné, {RBX_NAME}, vous suit !",Il nuovo follower {RBX_NAME} ha cominciato a seguirti!,新規フォロワー {RBX_NAME} があなたをフォローしました!,새 팔로워 {RBX_NAME}님이 회원님을 팔로우합니다!,"O novo seguidor, {RBX_NAME}, está seguindo você agora!","O novo seguidor, {RBX_NAME}, está seguindo você agora!",На вас подписался пользователь {RBX_NAME}!,新粉丝“{RBX_NAME}”现在关注了你!,新關注者{RBX_NAME}現正關注您! +NotificationScript2.onPointsAwarded.multiple,,,,Du hast {RBX_NUMBER} Punkte erhalten!,You received {RBX_NUMBER} points!,¡Has recibido {RBX_NUMBER} puntos!,¡Has recibido {RBX_NUMBER} puntos!,Vous avez reçu {RBX_NUMBER} points !,Hai ricevuto {RBX_NUMBER} punti!,{RBX_NUMBER} ポイントをゲットしました!,{RBX_NUMBER} 포인트를 받았어요!,Você recebeu {RBX_NUMBER} pontos!,Você recebeu {RBX_NUMBER} pontos!,Вы получили {RBX_NUMBER} очк.!,你得到了 {RBX_NUMBER} 点!,您得到{RBX_NUMBER}點! +NotificationScript2.onPointsAwarded.single,,,,Du hast {RBX_NUMBER} Punkt erhalten!,You received {RBX_NUMBER} point!,¡Has recibido {RBX_NUMBER} punto!,¡Has recibido {RBX_NUMBER} punto!,Vous avez reçu {RBX_NUMBER} point !,Hai ricevuto {RBX_NUMBER} punto!,{RBX_NUMBER} ポイントをゲットしました!,{RBX_NUMBER} 포인트를 받았어요!,Você recebeu {RBX_NUMBER} ponto!,Você recebeu {RBX_NUMBER} ponto!,Вы получили {RBX_NUMBER} очк.!,你得到了 {RBX_NUMBER} 点!,您得到{RBX_NUMBER}點! +NotificationScript2.onPointsRewarded.negative,,,,Du hast {RBX_NUMBER} Punkte verloren!,You lost {RBX_NUMBER} points!,¡Has perdido {RBX_NUMBER} puntos!,¡Has perdido {RBX_NUMBER} puntos!,Vous avez perdu {RBX_NUMBER} points !,Hai perso {RBX_NUMBER} punti!,{RBX_NUMBER} ポイント失いました!,{RBX_NUMBER} 포인트를 잃었어요!,Você perdeu {RBX_NUMBER} pontos!,Você perdeu {RBX_NUMBER} pontos!,Вы потеряли {RBX_NUMBER} очк.!,你丢失了 {RBX_NUMBER} 点!,您失去{RBX_NUMBER}點! +PlayerDropDown.onFollowButtonPress.success,,,,folgst jetzt {RBX_NAME},now following {RBX_NAME},Estás siguiendo a {RBX_NAME},Estás siguiendo a {RBX_NAME},Vous suivez désormais {RBX_NAME}.,stai seguendo {RBX_NAME},{RBX_NAME}人をフォロー中,{RBX_NAME} 팔로우 중,agora seguindo {RBX_NAME},agora seguindo {RBX_NAME},подписались на {RBX_NAME},现正关注“{RBX_NAME}”,現正關注{RBX_NAME} +PlayerDropDown.onUnfollowButtonPress.success,,,,Folgst nicht mehr {RBX_NAME},No longer following {RBX_NAME}.,Ya no estás siguiendo a {RBX_NAME}.,Ya no estás siguiendo a {RBX_NAME}.,Vous ne suivez plus {RBX_NAME}.,Non stai seguendo ancora {RBX_NAME}.,あなたはもはや{RBX_NAME}に従っていません。,{RBX_NAME}님 팔로우 취소.,Você não está mais seguindo {RBX_NAME}.,Você não está mais seguindo {RBX_NAME}.,Вы перестали следовать {RBX_NAME}.,停止关注“{RBX_NAME}”。,不再關注{RBX_NAME}。 +PurchasePromptScript.ERROR_MSG.INVALID_FUNDS,,,,dein Konto nicht über genügend ROBUX verfügt,your account does not have enough ROBUX,tu cuenta no tiene suficientes ROBUX,tu cuenta no tiene suficientes ROBUX,votre compte ne possède pas assez de ROBUX,il tuo account non ha abbastanza ROBUX,アカウントのROBUXが足りません。,계정의 ROBUX가 부족해요,sua conta não tem ROBUX suficientes,sua conta não tem ROBUX suficientes,у вас недостаточно ROBUX,你的帐户没有足够的 Robux,您帳戶下的 ROBUX 不足 +PurchasePromptScript.ERROR_MSG.MAINTENANCE,,,,ROBLOX Wartungsarbeiten durchführt,ROBLOX is performing maintenance,ROBLOX está efectuando tareas de mantenimiento,ROBLOX está efectuando tareas de mantenimiento,ROBLOX est en cours de maintenance,ROBLOX sta effettuando la manutenzione,ROBLOXはメンテナンス中です。,ROBLOX에서 점검 중이에요,ROBLOX está em manutenção,ROBLOX está em manutenção,Проводится техническое обслуживание ROBLOX,Roblox 正在维护中,ROBLOX 正在進行維護 +PurchasePromptScript.ERROR_MSG.PURCHASE_DISABLED,,,,Käufe im Spiel derzeit deaktiviert sind,In-game purchases are temporarily disabled,las compras dentro de la aplicación están desactivadas temporalmente,las compras dentro de la aplicación están desactivadas temporalmente,Les achats intégrés sont temporairement désactivés,Gli acquisti all'interno del gioco sono temporaneamente disattivati,ゲーム内購入は一時的に無効になっています。,일시적으로 게임 내 구매를 이용할 수 없어요,As compras no jogo estão temporariamente desabilitadas,As compras no jogo estão temporariamente desabilitadas,Внутриигровые покупки временно недоступны,游戏内购买功能暂时停用,遊戲內購買暫時停用 +PurchasePromptScript.ERROR_MSG.UNKNOWN,,,,ein Problem aufgetreten ist,something went wrong,algo ha ido mal,algo ha ido mal,quelque chose s'est mal passé,qualcosa è andato storto,何かが違う,문제가 생겼어요,algo deu errado,algo deu errado,возникли проблемы,有地方出错,有地方出錯 +PurchasePromptScript.PURCHASE_FAILED.CANNOT_GET_BALANCE,,,,Dein Guthaben kann derzeit nicht abgerufen werden. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.,Cannot retrieve your balance at this time. Your account has not been charged. Please try again later.,En este momento no es posible acceder a tu saldo. No se ha llevado a cabo ningún cobro en tu cuenta. Inténtalo de nuevo más tarde.,En este momento no es posible acceder a tu saldo. No se ha llevado a cabo ningún cobro en tu cuenta. Inténtalo de nuevo más tarde.,Impossible d'obtenir votre solde pour l'instant. Votre compte n'a pas été débité. Veuillez réessayer plus tard.,Impossibile recuperare i tuoi fondi in questo momento. Il tuo account non ha subito addebiti. Riprova più tardi.,現在は残額を参照できません。アカウントがチャージされませんでした。しばらくしてからやり直してください。,현재 잔액을 불러올 수 없어요. 계정에 비용이 청구되지 않아요. 나중에 다시 시도하세요.,Impossível obter seu saldo no momento. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Impossível obter seu saldo no momento. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Не удалось получить доступ к вашему балансу. Средства с вашей учетной записи не списаны. Повторите попытку позже.,当前无法读取你的余额。你的帐户未被扣款。请稍候重试。,此刻無法擷取您的餘額。未自您的帳戶收費。請稍後再試一次。 +PurchasePromptScript.PURCHASE_FAILED.CANNOT_GET_ITEM_PRICE,,,,Der Preis des Gegenstands kann derzeit nicht abgerufen werden. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.,We couldn't retrieve the price of the item at this time. Your account has not been charged. Please try again later.,En este momento no podemos acceder al precio de este objeto. No se ha llevado a cabo ningún cobro en tu cuenta. Inténtalo de nuevo más tarde.,En este momento no podemos acceder al precio de este objeto. No se ha llevado a cabo ningún cobro en tu cuenta. Inténtalo de nuevo más tarde.,Nous ne pouvons pas récupérer le prix de cet objet pour l'instant. Votre compte n'a pas été débité. Veuillez réessayer plus tard.,Impossibile recuperare il prezzo dell'oggetto in questo momento. Il tuo account non ha subito addebiti. Riprova più tardi.,現在はアイテムの価格が参照できません。アカウントがチャージされませんでした。しばらくしてからやり直してください。,현재 아이템 가격을 불러올 수 없어요. 계정에 비용이 청구되지 않아요. 나중에 다시 시도하세요.,Não conseguimos obter o preço do item no momento. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Não conseguimos obter o preço do item no momento. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Не удалось получить данные о цене товара. Средства с вашей учетной записи не списаны. Повторите попытку позже.,当前无法读取物品的价格。你的帐户未被扣款。请稍候重试。,此刻無法擷取項目的價格。未自您的帳戶收費。請稍後再試一次。 +PurchasePromptScript.PURCHASE_FAILED.LIMITED,,,,Von diesem limitierten Gegenstand gibt es keine weiteren Exemplare. Such auf www.roblox.com nach einem anderen Verkäufer. Dein Konto wurde nicht belastet.,This limited item has no more copies. Try buying from another user on www.roblox.com. Your account has not been charged.,Este objeto limitado no tiene más copias. Intenta comprárselo a otro usuario en www.roblox.com. No se ha llevado a cabo ningún cobro en tu cuenta.,Este objeto limitado no tiene más copias. Intenta comprárselo a otro usuario en www.roblox.com. No se ha llevado a cabo ningún cobro en tu cuenta.,Il n'y a plus d'exemplaires de cet objet en série limitée. Essayez de l'acheter à un autre utilisateur sur www.roblox.com. Votre compte n'a pas été débité.,Questo oggetto limitato non è più disponibile. Prova ad acquistarlo da un altro utente sul sito www.roblox.com. Il tuo account non ha subito addebiti.,この限定アイテムはもう残っていません。www.roblox.com で他のユーザから購入してみてください。アカウントがチャージされませんでした。,이 한정 아이템은 더 이상 재고가 없어요. www.roblox.com에서 다른 사용자에게 구매해보세요. 계정에 비용이 청구되지 않아요.,Esgotaram-se as cópias deste item limitado. Tente comprar de outro usuário em www.roblox.com. Nada foi cobrado da sua conta.,Esgotaram-se as cópias deste item limitado. Tente comprar de outro usuário em www.roblox.com. Nada foi cobrado da sua conta.,Этого ограниченного товара больше нет в продаже. Попробуйте купить его у другого пользователя на сайте www.roblox.com. Средства с вашей учетной записи не списаны.,此限量物品已售完。请尝试在 www.roblox.com 上从别的用户手中购买。你的帐户未被扣款。,此限量項目已無複本。請嘗試在 www.roblox.com 向其他使用者購買。未自您的帳戶收費。 +PurchasePromptScript.PURCHASE_FAILED.NOT_ENOUGH_TIX,,,,"Dieser Gegenstand kostet mehr Tickets, als du derzeit besitzt. Auf www.roblox.com kannst du Währungen tauschen, um mehr Tickets zu erhalten.",This item cost more tickets than you currently have. Try trading currency on www.roblox.com to get more tickets.,Este objeto cuesta más tiques de los que tienes en este momento. Intenta convertir tipos de moneda en www.roblox.com para obtener más tiques.,Este objeto cuesta más tiques de los que tienes en este momento. Intenta convertir tipos de moneda en www.roblox.com para obtener más tiques.,Cet objet coûte plus de tickets que vous n'en avez. Essayez d'échanger des devises sur www.roblox.com pour obtenir plus de tickets.,Questo oggetto costa più ticket di quanti tu ne abbia. Prova a scambiare valuta sul sito www.roblox.com per ottenere più ticket.,このアイテムの入手には現在お持ちのチケットでは足りません。www.roblox.com でお金のトレーディングをしてチケットを手に入れよう。,이 아이템은 현재 소지한 티켓보다 더 비싸요. 더 많은 티켓을 받으려면 www.roblox.com에서 통화를 거래하세요.,Este item custa mais bilhetes do que você tem no momento. Experimente trocar moeda do jogo em www.roblox.com para obter mais.,Este item custa mais bilhetes do que você tem no momento. Experimente trocar moeda do jogo em www.roblox.com para obter mais.,"Цена товара превышает имеющееся у вас количество купонов. Обменяйте валюту на странице www.roblox.com, чтобы получить больше купонов.",此物品的价格超过你当前拥有的票单。在 www.roblox.com 上尝试交易货币以获取更多票单。,此項目所需票券超過您目前擁有。請嘗試在 www.roblox.com 交易貨幣以取得更多票券。 +PurchasePromptScript.PURCHASE_FAILED.NOT_FOR_SALE,,,,Dieser Gegenstand steht derzeit nicht zum Verkauf. Dein Konto wurde nicht belastet.,This item is not currently for sale. Your account has not been charged.,Este objeto no está a la venta en este momento. No se ha llevado a cabo ningún cobro en tu cuenta.,Este objeto no está a la venta en este momento. No se ha llevado a cabo ningún cobro en tu cuenta.,Cet objet n'est pas en vente pour l'instant. Votre compte n'a pas été débité.,Questo oggetto non è attualmente in vendita. Il tuo account non ha subito addebiti.,このアイテムは現在売られていません。アカウントがチャージされませんでした。,이 아이템은 현재 판매 중이 아니에요. 계정에 비용이 청구되지 않아요.,Este item não está disponível para compra no momento. Nada foi cobrado da sua conta.,Este item não está disponível para compra no momento. Nada foi cobrado da sua conta.,Этот товар в настоящее время не продается. Средства с вашей учетной записи не списаны.,此物品当前为非卖品。你的帐户未被扣款。,此項目目前為非賣品,未自您的帳戶收費。 +PurchasePromptScript.PURCHASE_FAILED.PROMPT_PURCHASE_ON_GUEST,,,,"Du musst ein ROBLOX-Konto erstellen, um Gegenstände zu kaufen. Auf www.roblox.com findest du weitere Infos.","You need to create a ROBLOX account to buy items, visit www.roblox.com for more info.",Tienes que crear una cuenta de ROBLOX para comprar objetos. Visita www.roblox.com para obtener más información.,Tienes que crear una cuenta de ROBLOX para comprar objetos. Visita www.roblox.com para obtener más información.,"Vous devez créer un compte ROBLOX pour acheter des objets, rendez-vous sur www.roblox.com pour plus d'informations.","Per poter comprare oggetti, devi creare un account ROBLOX. Visita il sito www.roblox.com per maggiori informazioni.",アイテムを購入するにはROBLOX アカウントを作る必要があります。詳しくは www.roblox.com で。,아이템을 구매하려면 ROBLOX 계정을 만들어야 해요. 자세한 정보는 www.roblox.com을 방문하세요.,Você precisa criar uma conta ROBLOX para comprar itens. Visite www.roblox.com para mais informações.,Você precisa criar uma conta ROBLOX para comprar itens. Visite www.roblox.com para mais informações.,"Создайте учетную запись ROBLOX, чтобы покупать предметы. Посетите сайт www.roblox.com.",你需要创建 ROBLOX 帐户以购买物品,访问 www.roblox.com 以获取更多信息。,您需要建立 ROBLOX 帳戶以購買項目,更多資訊請瀏覽 www.roblox.com。 +PurchasePromptScript.PURCHASE_FAILED.THIRD_PARTY_DISABLED,,,,Gegenstände von Drittanbietern können an diesem Ort nicht verkauft werden. Dein Konto wurde nicht belastet.,Third-party item sales have been disabled for this place. Your account has not been charged.,Las ventas de objetos de terceros están desactivadas para este lugar. No se ha llevado a cabo ningún cobro en tu cuenta.,Las ventas de objetos de terceros están desactivadas para este lugar. No se ha llevado a cabo ningún cobro en tu cuenta.,Les ventes d'objets par des tiers ont été désactivées pour cet emplacement. Votre compte n'a pas été débité.,Le vendite di oggetti di terzi sono state disattivate per questa località. Il tuo account non ha subito addebiti.,サードパーティ製のアイテムの販売はここでは禁止されています。アカウントがチャージされませんでした。,여기서는 제삼자 아이템 판매를 이용할 수 없어요. 계정에 비용이 청구되지 않아요.,Vendas de itens de terceiros foram desabilitadas para este local. Nada foi cobrado da sua conta.,Vendas de itens de terceiros foram desabilitadas para este local. Nada foi cobrado da sua conta.,Продажа сторонних товаров в этом месте запрещена. Средства с вашей учетной записи не списаны.,此地点的第三方物品拍卖已停用。你的帐户未被扣款。,此地點的第三方項目大拍賣已停用。並未自您的帳戶收費。 +PurchasePromptScript.PURCHASE_FAILED.UNDER_13,,,,Dein Konto ist für Spieler unter 13 Jahren. Der Kauf dieses Gegenstands ist nicht gestattet. Dein Konto wurde nicht belastet.,Your account is under 13. Purchase of this item is not allowed. Your account has not been charged.,Tu cuenta es para menores de 13 años. La compra de este objeto no está permitida. No se ha llevado a cabo ningún cobro en tu cuenta.,Tu cuenta es para menores de 13 años. La compra de este objeto no está permitida. No se ha llevado a cabo ningún cobro en tu cuenta.,Votre compte est Moins de 13 ans. L'achat de cet objet n'est pas permis. Votre compte n'a pas été débité.,Il tuo account è per giocatori con meno di 13 anni. L'acquisto di questo oggetto non è permesso. Il tuo account non ha subito addebiti.,あなたのアカウントは13才以下です。このアイテムの購入は認められていません。アカウントがチャージされませんでした。,만 13세 미만의 계정으로 이 아이템을 구매할 수 없어요. 계정에 비용이 청구되지 않아요.,Sua conta é para menor de 13 anos. A compra deste item não é permitida. Nada foi cobrado da sua conta.,Sua conta é para menor de 13 anos. A compra deste item não é permitida. Nada foi cobrado da sua conta.,Возраст владельца учетной записи не превышает 13 лет. Вы не можете купить этот товар. Средства с вашей учетной записи не списаны.,此帐户的用户未满 13 岁,不允许购买此物品。你的帐户未被扣款。,您的帳戶未滿 13 歲,不允許購買此項目。未自您的帳戶收費。 +PurchasePromptScript.PURCHASE_MSG.BALANCE_FUTURE,,,,Nach dieser Transaktion wird dein Guthaben {RBX_NUMBER} betragen.,Your balance after this transaction will be {RBX_NUMBER}.,Tu saldo después de esta transacción será de {RBX_NUMBER}.,Tu saldo después de esta transacción será de {RBX_NUMBER}.,Votre solde après cette transaction sera de {RBX_NUMBER}.,"Dopo la transazione, i tuoi fondi saranno {RBX_NUMBER}.",取引後の残高は{RBX_NUMBER}になります。,이 거래 후의 예상 잔액은 {RBX_NUMBER}이에요.,Seu saldo depois desta transação será de {RBX_NUMBER}.,Seu saldo depois desta transação será de {RBX_NUMBER}.,После этой операции ваш баланс составит {RBX_NUMBER},此次交易后,你的余额将为 {RBX_NUMBER}。,您此交易後的餘額將為 {RBX_NUMBER}。 +PurchasePromptScript.PURCHASE_MSG.BALANCE_NOW,,,,Dein Guthaben beträgt nun {RBX_NUMBER}.,Your balance is now {RBX_NUMBER}.,Tu saldo es ahora de {RBX_NUMBER}.,Tu saldo es ahora de {RBX_NUMBER}.,Votre solde est désormais de {RBX_NUMBER}.,Ora i tuoi fondi sono {RBX_NUMBER}.,残高は現在{RBX_NUMBER}です。,현재 잔액은 {RBX_NUMBER}이에요.,Seu saldo agora é {RBX_NUMBER}.,Seu saldo agora é {RBX_NUMBER}.,Сейчас ваш баланс составляет {RBX_NUMBER}.,你的当前余额为 {RBX_NUMBER}。,您的餘額現在為{RBX_NUMBER}。 +PurchasePromptScript.PURCHASE_MSG.FAILED,,,,"Du konntest „{RBX_NAME1}“ nicht kaufen, da {RBX_NAME2}. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.",Your purchase of {RBX_NAME1} failed because {RBX_NAME2}. Your account has not been charged. Please try again later.,Tu compra de {RBX_NAME1} no ha funcionado porque {RBX_NAME2}. No se ha realizado ningún cobro. Inténtalo de nuevo más tarde.,Tu compra de {RBX_NAME1} no ha funcionado porque {RBX_NAME2}. No se ha realizado ningún cobro. Inténtalo de nuevo más tarde.,Votre achat de {RBX_NAME1} a échoué à cause de {RBX_NAME2}. Votre compte n'a pas été débité. Veuillez réessayer plus tard.,Il tuo acquisto di {RBX_NAME1} non è riuscito perché: {RBX_NAME2}. Il tuo account non ha subito addebiti. Riprova più tardi.,{RBX_NAME1}のため、{RBX_NAME2} の購入に失敗しました。アカウントがチャージされませんでした。しばらくしてからやり直してください。,{RBX_NAME1} 구매 실패. 실패 이유: {RBX_NAME2}. 계정에 비용이 청구되지 않았어요. 나중에 다시 시도하세요.,Sua compra de {RBX_NAME1} fracassou. Motivo: {RBX_NAME2}. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Sua compra de {RBX_NAME1} fracassou. Motivo: {RBX_NAME2}. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Не удалось купить товар {RBX_NAME1}: {RBX_NAME2}. Средства с вашей учетной записи не списаны. Повторите попытку позже.,由于“{RBX_NAME2}”,未能成功购买“{RBX_NAME1}”。你的帐户未被扣款。请稍候重试。,您購買{RBX_NAME1}未成,原因是{RBX_NAME2}。未自您的帳戶收費。請稍後再試一次。 +PurchasePromptScript.PURCHASE_MSG.FREE,,,,Möchtest du „{RBX_NAME2}“ gerne GRATIS nehmen?,Would you like to take {RBX_NAME2} for FREE?,¿Quieres obtener {RBX_NAME2} GRATIS?,¿Quieres obtener {RBX_NAME2} GRATIS?,Souhaitez-vous prendre l'objet {RBX_NAME2} GRATUITEMENT ?,Vuoi prendere l'oggetto {RBX_NAME2} GRATIS?,アイテム名{RBX_NAME2} を無料で入手したいですか?,무료로 {RBX_NAME2}을(를) 받을까요?,Gostaria de obter {RBX_NAME2} GRÁTIS?,Gostaria de obter {RBX_NAME2} GRÁTIS?,Хотите получить товар {RBX_NAME2} БЕСПЛАТНО?,你想要免费拿到“{RBX_NAME2}”吗?,您是否要免費收取{RBX_NAME2}? +PurchasePromptScript.PURCHASE_MSG.PURCHASE,,,,Möchtest du {RBX_NAME1} {RBX_NAME2} kaufen für:,Want to buy the {RBX_NAME1} {RBX_NAME2} for,¿Quieres comprar {RBX_NAME1} {RBX_NAME2} por,¿Quieres comprar {RBX_NAME1} {RBX_NAME2} por,Vous voulez acheter le {RBX_NAME1} {RBX_NAME2} pour,Vuoi comprare {RBX_NAME1} {RBX_NAME2} per,のために{RBX_NAME1} {RBX_NAME2} を購入希望,다음 금액으로 {RBX_NAME1} {RBX_NAME2}을(를) 구매할까요,Deseja comprar {RBX_NAME1} {RBX_NAME2} por,Deseja comprar {RBX_NAME1} {RBX_NAME2} por,Купить {RBX_NAME1} {RBX_NAME2} за,你想要以如下价格购买“{RBX_NAME1} - {RBX_NAME2}”:,想買{RBX_NAME1} {RBX_NAME2},花 +PurchasePromptScript.PURCHASE_MSG.SUCCEEDED,,,,Du hast „{RBX_NAME1}“ gekauft!,Your purchase of {RBX_NAME1} succeeded!,¡Tu compra de {RBX_NAME1} ha funcionado!,¡Tu compra de {RBX_NAME1} ha funcionado!,Votre achat de {RBX_NAME1} a réussi !,Il tuo acquisto di {RBX_NAME1} è andato a buon fine!,{RBX_NAME1} の購入に成功しました!,성공적으로 {RBX_NAME1}을(를) 구매했어요!,Sua compra de {RBX_NAME1} foi bem-sucedida!,Sua compra de {RBX_NAME1} foi bem-sucedida!,Вы купили товар {RBX_NAME1}!,购买“{RBX_NAME1}”成功!,您購買{RBX_NAME1}成功! +PurchasePromptScript.setBuyMoreRobuxDialog.PostBalanceText,,,,Die verbleibenden {RBX_NUMBER} ROBUX werden deinem Guthaben gutgeschrieben.,The remaining {RBX_NUMBER} ROBUX will be credited to your balance.,Los {RBX_NUMBER} ROBUX restantes se cargarán a tu saldo.,Los {RBX_NUMBER} ROBUX restantes se cargarán a tu saldo.,Les {RBX_NUMBER} ROBUX restants seront portés à votre solde.,I rimanenti {RBX_NUMBER} ROBUX verranno accreditati ai tuoi fondi.,残りの {RBX_NUMBER} ROBUX があなたの口座に返金されます。,남은 {RBX_NUMBER} ROBUX는 잔액에 추가돼요.,Os {RBX_NUMBER} ROBUX restantes serão somados ao seu saldo.,Os {RBX_NUMBER} ROBUX restantes serão somados ao seu saldo.,Оставшиеся {RBX_NUMBER} ROBUX будут перечислены на ваш баланс.,剩余的 {RBX_NUMBER} ROBUX 将计入你的余额。,剩餘的 {RBX_NUMBER} ROBUX 會計入您的餘額。 +PurchasePromptScript.setBuyMoreRobuxDialog.descriptionText,,,,"Du brauchst noch {RBX_NUMBER} ROBUX, um {RBX_NAME1} {RBX_NAME2} zu kaufen. Möchtest du mehr ROBUX kaufen?",You need {RBX_NUMBER} more ROBUX to buy the {RBX_NAME1} {RBX_NAME2}. Would you like to buy more ROBUX?,Necesitas {RBX_NUMBER} ROBUX más para comprar {RBX_NAME1} {RBX_NAME2}. ¿Quieres comprar más ROBUX?,Necesitas {RBX_NUMBER} ROBUX más para comprar {RBX_NAME1} {RBX_NAME2}. ¿Quieres comprar más ROBUX?,Vous avez besoin de {RBX_NUMBER} ROBUX de plus pour acheter le {RBX_NAME1} {RBX_NAME2}. Souhaitez-vous acheter plus de ROBUX ?,Hai bisogno di {RBX_NUMBER} ROBUX in più per comprare: {RBX_NAME1} {RBX_NAME2}. Vuoi acquistare altri ROBUX?,{RBX_NAME1} {RBX_NAME2}.を買うにはあと{RBX_NUMBER}のROBUXが必要です。もっとROBUXを買いますか?,{RBX_NAME1} {RBX_NAME2}을(를) 구매하려면 {RBX_NUMBER}개의 ROBUX가 더 필요해요. ROBUX를 더 구매할까요?,Você precisa de mais {RBX_NUMBER} ROBUX para comprar o(a) {RBX_NAME2} {RBX_NAME1}. Gostaria de comprar mais ROBUX?,Você precisa de mais {RBX_NUMBER} ROBUX para comprar o(a) {RBX_NAME2} {RBX_NAME1}. Gostaria de comprar mais ROBUX?,Для покупки товара {RBX_NAME1} {RBX_NAME2} требуется еще {RBX_NUMBER} ROBUX. Приобрести больше ROBUX?,你还需要 {RBX_NUMBER} ROBUX 才能购买 {RBX_NAME1} {RBX_NAME2}。你想要购买更多 ROBUX 吗?,您需要再多 {RBX_NUMBER} ROBUX 才能購買{RBX_NAME1} {RBX_NAME2}。您是否要加購 ROBUX? +PurchasePromptScript.setPurchaseDataInGui.invalidBC,,,,"Dieser Gegenstand erfordert {RBX_NAME1}. Klicke auf „Aufwerten“, um deinen Builders-Club-Status zu verbessern!",This item requires {RBX_NAME1}. Click 'Upgrade' to upgrade your Builders Club!,"Este objeto requiere {RBX_NAME1}. Dale a ""Mejorar"" para mejorar el Builders Club.","Este objeto requiere {RBX_NAME1}. Dale a ""Mejorar"" para mejorar el Builders Club.",Cet objet nécessite {RBX_NAME1}. Cliquez sur « Améliorer » pour améliorer votre Builders Club !,"Questo oggetto richiede: {RBX_NAME1}. Clicca su ""Migliora"" per potenziare il tuo Builders Club!",このアイテムには{RBX_NAME1}が必要です。「アップグレード」をクリックしてビルダーズクラブをアップグレード!,이 아이템에는 {RBX_NAME1}이(가) 필요해요. '업그레이드'를 클릭해 빌더스 클럽을 업그레이드하세요!,Este item requer {RBX_NAME1}. Clique em 'Melhorar' para melhorar seu Builders Club!,Este item requer {RBX_NAME1}. Clique em 'Melhorar' para melhorar seu Builders Club!,"Для этого предмета требуется: {RBX_NAME1}. Нажмите кнопку «Улучшение», чтобы улучшить клуб создателей!",此物品需要 {RBX_NAME1}。点按“升级”以升级你的 Builders Club !,此項目需要{RBX_NAME1}。按一下「升級」以升級您的建置者社團! +Received,,,,Empfangen,Received,Recibido,Recibido,Reçus,Ricevuti,受信済み,받음,Recebido,Recebido,Принято,已收到,收到 +Sent,,,,Gesendet,Sent,Enviado,Enviado,Envoyés,Inviati,送付済み,보냄,Enviado,Enviado,Отправлено,已发送,送出 +StatsUtil.KBps,,,,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} Ko/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} kB/s,{RBX_NUMBER} kB/s,{RBX_NUMBER} КБ/с,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s +StatsUtil.MB,,,,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} Mo,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} МБ,{RBX_NUMBER} MB,{RBX_NUMBER} MB +StatsUtil.ms,,,,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} мс,{RBX_NUMBER} ms,{RBX_NUMBER} ms +Target,,,,Ziel,Target,Objetivo,Objetivo,Cible,Scopo,目標,대상,Alvo,Alvo,Цель,目标,目標 +EnableVoiceKey,,,,Sprachchat aktivieren,Enable Voice Chat,Activar chat de voz,Activar chat de voz,Activer le chat vocal,Attiva chat vocale,ボイスチャットを有効にする,음성 채팅 활성화,Ativar conversa por chat,Ativar conversa por chat,Включить голосовой чат,启用语音聊天,啟用語音聊天 +DisableVoiceKey,,,,Sprachchat deaktivieren,Disable Voice Chat,Desactivar chat de voz,Desactivar chat de voz,Désactiver le chat vocal,Disattiva chat vocale,ボイスチャットを無効にする,음성 채팅 비활성화,Desativar conversa por chat,Desativar conversa por chat,Отключить голосовой чат,停用语音聊天,停用語音聊天 +,,,Account: Under 13 yrs,Konto: Unter 13 J.,,Cuenta: Menor de 13 años,Cuenta: Menor de 13 años,Compte : Moins de 13 ans,Account: meno di 13 anni,アカウント:13才以下,계정: 만 13세 미만,Conta: Menor de 13 anos,Conta: Menor de 13 anos,Учетная запись: Не старше 13 лет,帐户:13 岁以下,帳戶:未滿 13 歲 +,,,Right Mouse Button,Rechte Maustaste,,Botón derecho del ratón,Botón derecho del ratón,Bouton droit de la souris,Destro/Mouse,右マウスボタン,마우스 오른쪽 버튼,Mouse (direito),Mouse (direito),ПКМ,鼠标右键,右滑鼠鍵 +,,,Roblox Menu,Roblox-Menü,,Menú de Roblox,Menú de Roblox,Menu Roblox,Menu Roblox,Roblox メニュー,Roblox 메뉴,Menu Roblox,Menu Roblox,Меню Roblox,Roblox 菜单,Roblox 選單 +,,,Rotate,Drehen,,Rotar,Rotar,Tourner,Ruota,回転,회전,Girar,Girar,Вращение,旋转,旋轉 +,,,S/Down Arrow,S/Abwärtspfeil,,S/Cursor abajo,S/Cursor abajo,S/Flèche bas,S/Freccia GIÙ,S/下カーソル,S/아래쪽 화살표,S/seta para baixo,S/seta para baixo,S/стрелка вниз,S/下箭头,S 鍵 / 向下箭號 +,,,Save To Disk,Auf Festplatte speichern,,Guardar en disco,Guardar en disco,Sauvegarder sur le disque,Salva su disco,ディスクに保存,디스크에 저장,Salvar em disco,Salvar em disco,Сохранить на диск,保存至磁盘,儲存至磁碟 +,,,Scamming,Scamming,,Estafa,Estafa,Arnaque,Truffa,詐欺,신용 사기,Fraude,Fraude,Мошенничество,诈骗,詐騙 +,,,Screenshot,Screenshot,,Captura de pantalla,Captura de pantalla,Capture d'écran,Screenshot,スクリーンショット,스크린샷,Captura de tela,Captura de tela,Снимок экрана,屏幕截图,擷圖 +,,,Sent,Gesendet,,Enviado,Enviado,Envoyés,Inviato,送付済み,보냄,Enviado,Enviado,Отправленные,已发送,送出 +,,,Set by Developer,Vom Entwickler festgelegt,,Configurado por el desarrollador,Configurado por el desarrollador,Défini par développeur,Impostato da sviluppatore,開発者による設定,개발자 설정,Definido por desenvolvedor,Definido pelo desenvolvedor,Установлено разработчиком,由开发人员设置,開發人員所設定 +,,,Settings,Einstellungen,,Configuración,Configuración,Paramètres,Impostazioni,設定,설정,Configurações,Config.,Настройки,设置,設定 +,,,Are you sure you want to leave the game?,Möchtest du das Spiel wirklich verlassen?,,¿Seguro que deseas salir del juego?,¿Seguro que deseas salir del juego?,Voulez-vous vraiment quitter le jeu ?,Vuoi davvero uscire dal gioco?,ゲームをやめますか?,정말로 게임을 나갈까요?,Quer mesmo sair do jogo?,Quer mesmo sair do jogo?,Выйти из игры?,是否确定要离开游戏?,您確定要離開遊戲? +,,,Shift,Umschalttaste,,Mayúsculas,Mayúsculas,Maj,MAIUSC,シフト,Shift,Shift,Shift,Shift,Shift,Shift 鍵 +,,,Shift Lock Switch,Shift-Lock-Schalter,,Bloqueo de mayúsculas,Bloqueo de mayúsculas,Touche Verr. Vue,BLOC MAIUSC,シフトロックスイッチ,Shift Lock 전환,Trava do shift,Trava do shift,Переключение клавишей Shift,Shift Lock 开关,Shift Lock 開關 +,,,Space,Leertaste,,Espacio,Espacio,Espace,BARRA SPAZIATRICE,スペース,스페이스,Espaço,Espaço,Пробел,空格,空格鍵 +,,,Speed,Tempo,,Velocidad,Velocidad,Vitesse,Velocità,速さ,속도,Velocidade,Velocidade,Скорость,速度,速度 +,,,Submit,Senden,,Enviar,Enviar,Soumettre,Invia,送信,제출,Enviar,Enviar,Отправить,提交,提交 +,,,Swearing,Fluchen,,Palabras malsonantes,Palabras malsonantes,Insultes,Turpiloquio,ののしり,욕설,Palavrões,Palavrões,Ненормативная лексика,脏话谩骂,髒話謾罵 +,,,TAB,Tabulator,,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB 鍵 +,,,Take Free,Gratis nehmen,,Obtener gratis,Obtener gratis,Prendre gratuitement,Prendi gratis,無料配布,무료 획득,Obter grátis,Obter grátis,Получить бесплатно,免费获取,免費收取 +,,,Take Screenshot,Screenshot machen,,Hacer captura de pantalla,Hacer captura de pantalla,Capture d'écran,Cattura screenshot,スクリーンショットを撮る,스크린샷 캡쳐,Captura de tela,Captura de tela,Сделать снимок экрана,截取屏幕快照,擷圖 +,,,This is a popup,Dies ist ein Pop-up.,,Esta es una ventana emergente.,Esta es una ventana emergente.,Ceci est une fenêtre popup,Questo è un popup,これはポップアップです,팝업 메뉴입니다,Este é um popup,Este é um popup,Всплывающее сообщение,这是弹出式窗口,這是一個快顯視窗 +,,,Are you sure you want to reset your character?,Möchtest du deinen Charakter wirklich zurücksetzen?,,¿Seguro que deseas reiniciar tu personaje?,¿Seguro que deseas reiniciar tu personaje?,Voulez-vous vraiment réinitialiser le personnage ?,Vuoi davvero azzerare il tuo personaggio?,キャラクターをリセットしてよろしいですか?,정말로 캐릭터를 재설정할까요?,Quer mesmo reiniciar o personagem?,Quer mesmo reiniciar o personagem?,Сбросить персонажа?,是否确定要重置人物?,您確定要重設您的人物? +,,,Type Of Abuse,Art des Verstoßes,,Tipo de abuso,Tipo de abuso,Type d'abus,Tipo di abuso,不正の種類,오용 유형,Tipo de abuso,Tipo de abuso,Тип нарушения,滥用类型,濫行類型 +,,,Unequip Tools,Werkzeuge deaktivieren,,Desequipar herramientas,Desequipar herramientas,Outils non équipés,Rimuovi strum.,ツールを外す,도구 장착 해제,Desequipar,Desequipar,Убрать инструм.,取消配备工具,取消配備工具 +,,,Upgrade,Aufwerten,,Mejorar,Mejorar,Améliorer,Migliora,アップグレード,업그레이드,Melhorar,Melhorar,Улучшение,升级,升級 +,,,Upload to YouTube,Auf YouTube hochladen,,Cargar a YouTube,Cargar a YouTube,Télécharger sur YouTube,Carica su YouTube,YouTubeにアップロード,YouTube에 업로드,Upload para o YouTube,Envio para o YouTube,Загрузить на YouTube,上传至 Youtube,上傳至 YouTube +,,,Use Tool,Werkzeug verwenden,,Utilizar herramienta,Utilizar herramienta,Utiliser outil,Usa strumento,ツールを使用,도구 사용,Usar,Usar,Использовать инструмент,使用工具,使用工具 +,,,Video,Video,,Vídeo,Vídeo,Vidéo,Video,ビデオ,비디오,Vídeo,Vídeo,Видео,视频,影片 +,,,Video Settings,Video-Einstellungen,,Configuración de vídeo,Configuración de vídeo,Paramètres vidéo,Impostazioni video,ビデオ設定,비디오 설정,Configurações de vídeo,Configurações de vídeo,Настройки видео,视频设置,影片設定 +,,,Volume,Lautstärke,,Volumen,Volumen,Volume,Volume,ボリューム,볼륨,Volume,Volume,Громкость,音量,音量 +,,,W/Up Arrow,W/Aufwärtspfeil,,W/Cursor arriba,W/Cursor arriba,W/Flèche haut,W/Freccia SU,W/上カーソル,W/위쪽 화살표,W/seta para cima,W/seta para cima,W/стрелка вверх,W/上箭头,W 鍵 / 向上箭號 +,,,Which Player?,Welcher Spieler?,,¿A qué jugador?,¿A qué jugador?,Quel joueur ?,Quale giocatore?,どのプレーヤーですか?,플레이어 선택,Qual jogador?,Qual jogador?,Какой игрок?,哪位玩家?,哪位玩家? +,,,Automatic,Automatisch,,Automático,Automático,Automatique,Automatico,自動,자동,Automático,Automático,Автоматически,自动,自動 +,,,Wipeouts,Auslöschungen,,Destrucciones,Destrucciones,Éliminations,Annientamenti,死亡回数,사망,Aniquilações,Aniquilações,Уничтожения,死亡数,殲滅數 +,,,Zoom In,Einzoomen,,Acercar,Acercar,Zoom avant,Ingrandisci,ズームイン,확대,Aproximar zoom,Aproximar zoom,Приблизить,放大,放大 +,,,Zoom In/Out,Ein-/Auszoomen,,Acercar/alejar,Acercar/alejar,Zoom avant/arrière,Aumenta/Riduci,ズームイン/ズームアウト,확대/축소,Aprox./afastar,Aprox./afastar,Приблизить/отдалить,放大/缩小,放大/縮小 +,,,Zoom Out,Auszoomen,,Alejar,Alejar,Zoom arrière,Riduci,ズームアウト,축소,Afastar zoom,Afastar zoom,Отдалить,缩小,縮小 +,,,update me,aktualisiere mich,,actualízame,actualízame,tenez-moi au courant,aggiornami,自分をアップデート,업데이트 요청,atualize-me,atualize-me,обновите меня,向我发送更新信息,給我新消息 +,,,Add Friend,Freund hinzufügen,,Añadir amigo,Añadir amigo,Ajouter ami,Aggiungi amico,友達を追加,친구 추가,Adicionar amigo,Adicionar amigo,Добавить друга,添加好友,新增朋友 +,,,"Server found, loading...","Server gefunden, wird geladen ...",,Servidor encontrado. Cargando...,Servidor encontrado. Cargando...,"Serveur trouvé, chargement en cours...","Server trovato, caricamento...",サーバが見つかりました、ロード中です...,"서버 발견, 로드 중...","Servidor encontrado, carregando...","Servidor encontrado, carregando...",Сервер найден. Загрузка...,找到服务器,正在加载...,找到伺服器,載入中… +,,,Badge Awarded,Abzeichen verliehen,,Emblema concedido,Emblema concedido,Badge accordé,Contrassegno conferito,授与されたバッジ,배지 획득,Emblema concedido,Emblema concedido,Получен значок,已获得徽章,已獲徽章 +,,,Block Player,Spieler sperren,,Bloquear jugador,Bloquear jugador,Bloquer joueur,Blocca giocatore,プレーヤーをブロック,플레이어 차단,Bloquear jogador,Bloquear jogador,Заблокировать игрока,取消屏蔽玩家,封鎖玩家 +,,,Follow Player,Spieler folgen,,Seguir a jugador,Seguir a jugador,Suivre joueur,Segui giocatore,プレーヤーをフォロー,플레이어 팔로우,Seguir jogador,Seguir jogador,Подписаться на игрока,关注玩家,關注玩家 +,,,Backpack,Rucksack,,Mochila,Mochila,Sac à dos,Zaino,バックパック,배낭,Mochila,Mochila,Рюкзак,背包,背包 +,,,Friend,Freund,,Amigo,Amigo,Ami,Amico,友達,친구,Amigo,Amigo,Друг,好友,朋友 +,,,New Friend,Neuer Freund,,Amigo nuevo,Amigo nuevo,Nouvel ami,Nuovo amico,新しい友達,새 친구,Novo amigo,Novo amigo,Новый друг,新好友,新朋友 +,,,Point Awarded,Punkt verliehen,,Punto concedido,Punto concedido,Point accordé,Punto attribuito,ポイント授与,획득한 포인트,Ponto concedido,Ponto concedido,Получено очко,已获得点数,已獲點數 +,,,Points Awarded,Punkte verliehen,,Puntos concedidos,Puntos concedidos,Points accordés,Punti attribuiti,ポイント授与,획득한 포인트,Pontos concedidos,Pontos concedidos,Получены очки,已获得点数,已獲點數 +,,,"{RBX_NAME} won {RBX_NAME}'s ""{RBX_NAME}"" award!",{RBX_NAME} wurde von {RBX_NAME} die Auszeichnung „{RBX_NAME}“ verliehen!,,"¡{RBX_NAME} ha ganado el galardón ""{RBX_NAME}"" de {RBX_NAME}!","¡{RBX_NAME} ha ganado el galardón ""{RBX_NAME}"" de {RBX_NAME}!",{RBX_NAME} a gagné la récompense « {RBX_NAME} » de {RBX_NAME} !,"{RBX_NAME} ha vinto il premio ""{RBX_NAME}"" di {RBX_NAME}!","{RBX_NAME} {RBX_NAME}'の ""{RBX_NAME}"" を受賞!","{RBX_NAME}이(가) {RBX_NAME}의 ""{RBX_NAME}"" 상을 획득했어요!","{RBX_NAME} ganhou o prêmio ""{RBX_NAME}“ de {RBX_NAME}!","{RBX_NAME} ganhou o prêmio ""{RBX_NAME}“ de {RBX_NAME}!",{RBX_NAME} получает награду {RBX_NAME} «{RBX_NAME}»!,{RBX_NAME}赢得了{RBX_NAME}的“{RBX_NAME}”奖!,{RBX_NAME}贏得{RBX_NAME}的「{RBX_NAME}」獎! +,,,Report Abuse,Verstoß melden,,Denunciar abuso,Denunciar abuso,Signaler infraction,Segnala abuso,不正を報告,신고하기,Denunciar abuso,Denunciar abuso,Сообщить о нарушении,举报滥用,提報濫行 +,,,Request Sent,Anfrage gesendet,,Solicitud enviada,Solicitud enviada,Demande envoyée,Richiesta inviata,送信リクエスト,요청 보냄,Pedido enviado,Pedido enviado,Запрос отправлен,请求已发送,請求已送出 +,,,Send Friend Request,Freundesanfrage senden,,Enviar solicitud de amistad,Enviar solicitud de amistad,Envoyer demande d'ami,Invia richiesta di amicizia,友達リクエストを送信,친구 요청 전송,Enviar pedido de amizade,Enviar pedido de amizade,Отправить запрос дружбы,发送好友请求,傳送交友請求 +,,,Backspace,Rücktaste,,Retroceso,Retroceso,Retour,BACKSPACE,バックスペース,백스페이스,Backspace,Backspace,Backspace,回退,退格鍵 +,,,Sent you a friend request!,hat dir eine Freundesanfrage gesendet!,,te ha enviado una solicitud de amistad!,te ha enviado una solicitud de amistad!,vous a envoyé une demande d'ami !,ti ha inviato una richiesta di amicizia!,君に友達リクエストを送ったよ!,친구 요청을 보냈어요!,enviou um pedido de amizade para você!,enviou um pedido de amizade para você!,отправил вам запрос дружбы!,已向你发送好友请求!,已寄給您交友請求! +,,,Unblock Player,Spieler nicht mehr sperren,,Desbloquear jugador,Desbloquear jugador,Débloquer joueur,Sblocca giocatore,プレーヤーのブロックを解除,플레이어 차단 해제,Desbloq. jogador,Desbloq. jogador,Разблокировать игрока,取消屏蔽玩家,解除封鎖玩家 +,,,Unfollow Player,Spieler nicht mehr folgen,,Dejar de seguir a jugador,Dejar de seguir a jugador,Ne plus suivre joueur,Non seguire giocatore,プレーヤーをフォローしない,플레이어 팔로우 취소,Parar de seguir jogador,Parar de seguir jogador,Отписаться от игрока,取消关注玩家,取消關注玩家 +,,,Unfriend Player,Spieler als Freund entfernen,,Cancelar amistad con jugador,Cancelar amistad con jugador,Ne plus être ami du joueur,Togli amicizia a giocatore,プレーヤーを友達解除,플레이어 친구 끊기,Cancelar amizade,Cancelar amizade,Удалить из друзей,与玩家解除好友关系,與玩家解除朋友關係 +,,,You are,Du,,Estás,Estás,Vous,Sei,あなたは,현재,Você está,Você está,Вы,你是,您 +,,,Your balance after this transaction will be R${RBX_NUMBER},Nach dieser Transaktion wird dein Guthaben {RBX_NUMBER} R$ betragen.,,Tu saldo después de esta transacción será de {RBX_NUMBER} R$.,Tu saldo después de esta transacción será de {RBX_NUMBER} R$.,Votre solde après cette transaction sera de {RBX_NUMBER} R$,"Dopo la transazione, avrai R${RBX_NUMBER}",この取引の後の残高は R${RBX_NUMBER}です,이 거래 후의 예상 잔액은 R${RBX_NUMBER}이에요.,Seu saldo depois desta transação será de R$ {RBX_NUMBER},Seu saldo depois desta transação será de R$ {RBX_NUMBER},После этой операции ваш баланс составит R${RBX_NUMBER},此次交易后,你的余额将为 R${RBX_NUMBER}。,您此交易後的餘額將為 R${RBX_NUMBER}。 +,,,Bullying,Mobbing,,Abusos,Abusos,Harcèlement,Bullismo,いじめ,괴롭힘,Bullying,Bullying,Агрессивное поведение,欺凌,霸凌 +,,,Abuse Description,Beschreibung des Verstoßes,,Descripción del abuso,Descripción del abuso,Description de l'infraction,Descrizione abuso,不正の詳細,오용 설명,Descrição do abuso,Descrição do abuso,Описание нарушения,滥用描述,濫行描述 +,,,Buy,Kaufen,,Comprar,Comprar,Acheter,Compra,購入,구매,Comprar,Comprar,Купить,买,買 +,,,Button,Schaltfläche,,Botón,Botón,Bouton,Pulsante,ボタン,버튼,Botão,Botão,Кнопка,按钮,按鈕 +,,,DPad,Steuerkreuz,,Cruceta,Cruceta,Croix directionnelle,Croce direzionale,DPad,DPad,Direcional,Direcional,Крестовина,DPad,十字鍵 +,,,Default (Follow),Standard (Folgen),,Predeterminado (seguir),Predeterminado (seguir),Par défaut (Suivre),Predefinita (Segui),デフォルト (フォロー),기본값 (팔로우),Padrão (seguir),Padrão (seguir),По умолчанию (слежение),默认(追随),預設 (追蹤鏡頭) +,,,Default (Thumbstick),Standard (Daumenstick),,Predeterminado (stick),Predeterminado (stick),Par défaut (Joystick),Predefinita (Levetta),デフォルト (サムスティック),기본값 (엄지스틱),Padrão (thumbstick),Padrão (thumbstick),По умолчанию (аналоговый стик),默认(摇杆),預設 (模擬搖桿) +,,,Equip/Unequip Tools,Werkzeuge aktivieren/deaktivieren,,Equipar/desequipar herramientas,Equipar/desequipar herramientas,Prendre/Lâcher outils,Equipaggia/Rimuovi,ツールを装備/外す,도구 장착/해제,Trocar ferramentas,Trocar ferramentas,Назначить/убрать инструменты,配备/取消配备工具,配備/取消配備 工具 +,,,Move,Bewegen,,Movimiento,Movimiento,Se déplacer,Muovi,移動,이동,Mover,Mover,Передвижение,移动,移動 +,,,Reset Character,Charakter zurücksetzen,,Reiniciar personaje,Reiniciar personaje,Réinitialiser le personnage,Azzera personaggio,キャラクターをリセット,캐릭터 재설정,Reiniciar,Reiniciar,Сброс персонажа,重置人物,旋轉攝影機 +,,,Rotate Camera,Kamera drehen,,Rotar cámara,Rotar cámara,Rotation de caméra,Ruota visuale,カメラを回転,카메라 회전,Girar câmera,Girar câmera,Вращение камеры,旋转镜头,旋轉相機 +,,,Tap to Move,Zum Bewegen tippen,,Tocar para mover,Tocar para mover,Toucher pour se déplacer,Tocca per muovere,タップして移動,눌러서 이동,Toque para mover,Toque para mover,Коснитесь для передвижения,轻点以移动,觸屏移動 +,,,Thumbpad,Daumenpad,,Cruceta,Cruceta,Pavé directionnel,Croce direzionale,サムパッド,엄지패드,Thumbpad,Thumbpad,Кнопочная панель,拇指垫,拇指鍵盤 +,,,Thumbstick,Daumenstick,,Stick,Stick,Joystick,Levetta,サムスティック,엄지스틱,Thumbstick,Thumbstick,Аналоговый стик,摇杆,模擬搖桿 +,,,Buy Now,Jetzt kaufen,,Comprar ahora,Comprar ahora,Acheter maintenant,Compra ora,すぐ買う,지금 구매,Comprar agora,Comprar agora,Купить,立即购买,立即購買 +,,,These are the basic chat commands.,Das sind die grundlegenden Chatbefehle.,,Estos son los comandos básicos del chat.,Estos son los comandos básicos del chat.,Voici les commandes de chat basiques.,Questi sono i comandi base della chat.,これらは基本的なチャットコマンドです。,기본 채팅 명령어에요.,Esses são os comandos de chat básicos.,Esses são os comandos de chat básicos.,Это основные команды чата.,这些是基本聊天指令。,這些是基本聊天指令。 +,,,/join or /j : join channel.,/join oder /j : Kanal beitreten,,/join o /j : unirse al canal.,/join o /j : unirse al canal.,/join ou /j  : rejoindre le canal.,/join o /j : accedi al canale.,/join または /j : で、チャンネルに参加。,/join <채널> 또는 /j <채널> : 채널 가입.,/join ou /j : juntar-se a um canal.,/join ou /j : juntar-se a um canal.,/join <канал> или /j <канал> : подключиться к каналу.,/join or /j : 加入频道。,/join or /j : 加入頻道。 +,,,/leave or /l : leave channel. (leaves current if none specified),/leave oder /l : Kanal verlassen. (Ohne Angabe des Kanals wird der aktuelle Kanal verlassen.),,/leave o /l : salir del canal (sale del canal actual si no se especifica ninguno).,/leave o /l : salir del canal (sale del canal actual si no se especifica ninguno).,/leave ou /l  : quitter canal. (reste sur le même si aucun n'est spécifié),/leave o /l : lascia il canale (quello attuale se non specificato).,/leave または /l : でチャンネルから退出。 (指定しなければ現在のチャネルから退出します),/leave <채널> 또는 /l <채널> : 채널 나가기. (미지정 시 현재 채널에서 나가기),/leave ou /l : sair do canal. (sai do atual se não especificado),/leave ou /l : sair do canal. (sai do atual se não especificado),"/leave <канал> или /l <канал> : покинуть канал. (Вы покинете текущий канал, если не укажете другой.)",/leave 或 /l : 离开频道。(若未指定,离开当前频道),/leave 或 /l : 離開頻道。(若未指定,指離開目前頻道) +,,,Buy R$,R$ kaufen,,Comprar R$,Comprar R$,Acheter des R$,Compra R$,R$ を買う,R$ 구매,Comprar R$,Comprar R$,Купить R$,购买 R$,買 R$ +,,,You must wait {RBX_NUMBER} second before sending another message!,"Du musst {RBX_NUMBER} Sekunde lang warten, bevor du eine weitere Nachricht senden kannst!",,¡Debes esperar {RBX_NUMBER} segundo antes de enviar otro mensaje!,¡Debes esperar {RBX_NUMBER} segundo antes de enviar otro mensaje!,Vous devez attendre {RBX_NUMBER} seconde avant d'envoyer un autre message !,Devi aspettare {RBX_NUMBER} secondo prima di inviare un altro messaggio!,{RBX_NUMBER} 秒待ってから次のメッセージを送ってください!,추가 메시지를 보내기 전에 {RBX_NUMBER}초 동안 기다려야 해요!,Você precisa esperar {RBX_NUMBER} segundo antes de enviar outra mensagem!,Você precisa esperar {RBX_NUMBER} segundo antes de enviar outra mensagem!,Вы сможете отправить новое сообщение только через {RBX_NUMBER} сек.!,发送另一条消息前你必须等待 {RBX_NUMBER} 秒!,傳送另一則訊息之前您必須等候 {RBX_NUMBER} 秒! +,,,Reset Character,Charakter zurücksetzen,,Reiniciar personaje,Reiniciar personaje,Réinitialiser le personnage,Azzera personaggio,キャラクターをリセット,캐릭터 재설정,Reiniciar,Reiniciar,Сброс персонажа,重置人物,旋轉攝影機 +,,,"By clicking the 'Record Video' button, the menu will close and start recording your screen.","Wenn du auf die Schaltfläche „Video aufnehmen“ klickst, schließt sich das Menü und dein Bildschirm wird aufgezeichnet.",,"Al hacer clic en el botón ""Grabar vídeo"", el menú se cerrará y se empezará a grabar tu pantalla.","Al hacer clic en el botón ""Grabar vídeo"", el menú se cerrará y se empezará a grabar tu pantalla.","En cliquant sur le bouton Enregistrer vidéo, le menu se fermera et l'enregistrement de votre écran débutera.","Clicca sul pulsante ""Registra video"" per chiudere il menu e iniziare a registrare quanto avviene sullo schermo.",「ビデオの録画」ボタンをクリックするとメニューが閉じて画面の録画が始まります。,‘비디오 녹화’ 버튼을 클릭하면 메뉴가 종료되고 화면 녹화가 시작돼요.,"Ao clicar no botão ‘Gravar vídeo’, o menu será fechado e a tela começará a ser gravada.","Ao clicar no botão ‘Gravar vídeo’, o menu será fechado e a tela começará a ser gravada.",После нажатия на кнопку «Запись видео» это меню закроется и начнется запись видео с экрана.,点按“录制视频”按钮,目录会关闭,并开始录制你的屏幕。,按一下「錄製影片」按鈕,選單會關閉並開始錄下您的畫面。 +,,,To {RBX_NAME},An {RBX_NAME},,Para {RBX_NAME},Para {RBX_NAME},À {RBX_NAME},A {RBX_NAME},To {RBX_NAME},수신: {RBX_NAME},Para {RBX_NAME},Para {RBX_NAME},Получатель: {RBX_NAME},至“{RBX_NAME}”,至{RBX_NAME} +,,,From {RBX_NAME},Von {RBX_NAME},,De {RBX_NAME},De {RBX_NAME},De {RBX_NAME},Da {RBX_NAME},From {RBX_NAME},발신: {RBX_NAME},De {RBX_NAME},De {RBX_NAME},Отправитель: {RBX_NAME},自“{RBX_NAME}”,自{RBX_NAME} +,,,You were kicked from '{RBX_NAME}' for the following reason(s): {RBX_NAME},Du wurdest aus folgenden Gründen aus „{RBX_NAME}“ entfernt: {RBX_NAME},,"Se te ha expulsado de ""{RBX_NAME}"" por el/los motivo/s siguiente/s: {RBX_NAME}","Se te ha expulsado de ""{RBX_NAME}"" por el/los motivo/s siguiente/s: {RBX_NAME}",Vous avez été expulsé de {RBX_NAME} pour la/les raison(s) suivante(s) : {RBX_NAME},"Sei stato rimosso da ""{RBX_NAME}"" per le seguenti motivazioni: {RBX_NAME}",あなたは '{RBX_NAME}' から次の理由により外されました: {RBX_NAME},{RBX_NAME}'에서 퇴장당했어요. 퇴장 이유: {RBX_NAME},Você foi expulso(a) de '{RBX_NAME}‘. Motivo(s): {RBX_NAME},Você foi expulso(a) de '{RBX_NAME}‘. Motivo(s): {RBX_NAME},Вы исключены из «{RBX_NAME}» по следующим причинам: {RBX_NAME},“{RBX_NAME}”已将你踢出,原因为:{RBX_NAME},您自「{RBX_NAME}」遭踢出,原因如下:{RBX_NAME} +,,,{RBX_NAME} was kicked for the following reason(s): {RBX_NAME},{RBX_NAME} wurde aus folgenden Gründen entfernt: {RBX_NAME},,Se ha expulsado a {RBX_NAME} por el/los motivo/s siguiente/s: {RBX_NAME},Se ha expulsado a {RBX_NAME} por el/los motivo/s siguiente/s: {RBX_NAME},{RBX_NAME} a été expulsé pour la/les raison(s) suivante(s) : {RBX_NAME},{RBX_NAME} è stato rimosso per le seguenti motivazioni: {RBX_NAME},{RBX_NAME} は次の理由により外されました: {RBX_NAME},{RBX_NAME}님이 퇴장당했어요. 퇴장 이유: {RBX_NAME},{RBX_NAME} foi expulso(a). Motivo(s): {RBX_NAME},{RBX_NAME} foi expulso(a). Motivo(s): {RBX_NAME},Пользователь {RBX_NAME} исключен по следующим причинам: {RBX_NAME},已将“{RBX_NAME}”踢出,原因为:{RBX_NAME},{RBX_NAME}遭踢出的理由如下:{RBX_NAME} +,,,You were kicked from '{RBX_NAME}',Du wurdest aus „{RBX_NAME}“ entfernt.,,"Se te ha expulsado de ""{RBX_NAME}"".","Se te ha expulsado de ""{RBX_NAME}"".",Vous avez été expulsé de {RBX_NAME},"Sei stato rimosso da ""{RBX_NAME}""",あなたは '{RBX_NAME}' から外されました。,{RBX_NAME}'에서 퇴장당했어요.,Você foi expulso(a) de '{RBX_NAME}',Você foi expulso(a) de '{RBX_NAME}',Вы исключены из «{RBX_NAME}».,“{RBX_NAME}”已将你踢出,您自「{RBX_NAME}」遭踢出 +,,,"By clicking the 'Take Screenshot' button, the menu will close and take a screenshot and save it to your computer.","Wenn du auf die Schaltfläche „Screenshot machen“ klickst, schließt sich das Menü und ein Screenshot wird auf deinem Computer gespeichert.",,"Al hacer clic en el botón ""Hacer captura de pantalla"", el menú se cerrará y se hará una captura de pantalla que se guardará en tu ordenador.","Al hacer clic en el botón ""Hacer captura de pantalla"", el menú se cerrará y se hará una captura de pantalla que se guardará en tu ordenador.","En cliquant sur le bouton Capture d'écran, le menu se fermera et une capture d'écran sera sauvegardée dans votre ordinateur.","Clicca sul pulsante ""Cattura screenshot"" per chiudere il menu, catturare uno screenshot e salvarlo sul computer.",「スクリーンショットを撮る」ボタンをクリックするとメニューが閉じてスクリーンショットがコンピュータに保存されます。,스크린샷 캡쳐' 버튼을 클릭하면 메뉴가 종료되고 스크린샷을 캡쳐해 컴퓨터에 저장해요.,"Ao clicar no botão ‘Captura de tela’, o menu será fechado e a tela será capturada e salva no seu computador.","Ao clicar no botão ‘Captura de tela’, o menu será fechado e a tela será capturada e salva no seu computador.",После нажатия на кнопку «Сделать снимок экрана» это меню закроется и снимок сохранится на вашем компьютере.,点按“截取屏幕快照”按钮,目录会关闭,截取屏幕快照并保存至你的电脑。,按一下「擷圖」按鈕,選單就會關閉並擷一張圖以儲存至您的電腦。 +,,,{RBX_NAME} was kicked,{RBX_NAME} wurde entfernt,,Se ha expulsado a {RBX_NAME}.,Se ha expulsado a {RBX_NAME}.,{RBX_NAME} a été expulsé,{RBX_NAME} è stato rimosso,{RBX_NAME} は外されました。,{RBX_NAME}님이 퇴장당했어요.,{RBX_NAME} foi expulso(a),{RBX_NAME} foi expulso(a),Пользователь {RBX_NAME} исключен,“{RBX_NAME}”被踢出,{RBX_NAME}遭踢出 +,,,{RBX_NAME} was muted for the following reason(s): {RBX_NAME},{RBX_NAME} wurde aus folgenden Gründen stummgeschaltet: {RBX_NAME},,Se ha silenciado a {RBX_NAME} por el/los motivo/s siguiente/s: {RBX_NAME},Se ha silenciado a {RBX_NAME} por el/los motivo/s siguiente/s: {RBX_NAME},{RBX_NAME} a été bâillonné pour la/les raison(s) suivante(s) : {RBX_NAME},{RBX_NAME} non ha più la parola per le seguenti motivazioni: {RBX_NAME},{RBX_NAME} は次の理由によりミュートされました: {RBX_NAME},{RBX_NAME}님이 음소거 되었어요. 음소거 이유: {RBX_NAME},{RBX_NAME} foi silenciado(a). Motivo(s): {RBX_NAME},{RBX_NAME} foi silenciado(a). Motivo(s): {RBX_NAME},Пользователь {RBX_NAME} добавлен в список игнорируемых по следующим причинам: {RBX_NAME},“{RBX_NAME}”被禁言的原因为:{RBX_NAME},{RBX_NAME}遭靜音的理由如下:{RBX_NAME} +,,,You are now chatting in channel: '{RBX_NAME}',Du chattest jetzt in Kanal „{RBX_NAME}“.,,"Estás chateando en el canal ""{RBX_NAME}"".","Estás chateando en el canal ""{RBX_NAME}"".","Maintenant, vous discutez sur le canal : {RBX_NAME}","Ora stai parlando nel canale: ""{RBX_NAME}""",あなたの現在のチャットチャンネルは: '{RBX_NAME}' です。,{RBX_NAME}' 채널에서 채팅 중이에요,Você agora está no canal de chat: '{RBX_NAME}',Você agora está no canal de chat: '{RBX_NAME}',Вы общаетесь на канале «{RBX_NAME}»,你当前聊天的频道为:“{RBX_NAME}”,您此刻聊天的頻道在:「{RBX_NAME}」 +,,,You have left channel '{RBX_NAME}',Du hast Kanal „{RBX_NAME}“ verlassen.,,"Has salido del canal ""{RBX_NAME}"".","Has salido del canal ""{RBX_NAME}"".",Vous avez quitté le canal {RBX_NAME},"Hai lasciato il canale ""{RBX_NAME}""",チャンネル '{RBX_NAME}' を退出しました。,{RBX_NAME}' 채널에서 나왔어요,Você saiu do canal '{RBX_NAME}',Você saiu do canal '{RBX_NAME}',Вы покинули канал «{RBX_NAME}»,你已离开频道“{RBX_NAME}”,您已離開「{RBX_NAME}」頻道 +,,,CPU,CPU,,CPU,CPU,UCT,CPU,CPU,CPU,CPU,CPU,Процессор,CPU,中央處理器 +,,,ROBLOX version is out of date. Please uninstall and try again.,Die ROBLOX-Version ist nicht aktuell. Bitte deinstallieren und erneut versuchen.,,"Esta versión de ROBLOX es obsoleta. Por favor, desinstálala y vuelve a intentarlo.","Esta versión de ROBLOX es obsoleta. Por favor, desinstálala y vuelve a intentarlo.",Cette version de ROBLOX est obsolète. Veuillez désinstaller et réessayer.,Versione di ROBLOX obsoleta. Disinstalla e riprova.,古いバージョンのROBLOXです。アンインストールしてリトライしてください。,ROBLOX가 최신 버전이 아니에요. 삭제 후 다시 시도하세요.,Sua versão do ROBLOX não está atualizada. Desinstale e tente novamente.,Sua versão do ROBLOX não está atualizada. Desinstale e tente novamente.,Эта версия ROBLOX устарела. Удалите приложение и повторите попытку.,Roblox 版本已过期。请解除安装并重试。,ROBLOX 版本過時,請解除安裝後再試一次。 +,,,Connection attempt failed.,Verbindungsaufbau fehlgeschlagen.,,Ha fallado el intento de conexión.,Ha fallado el intento de conexión.,Échec de la tentative de connexion.,Tentativo di connessione non riuscito.,接続に失敗しました。,연결 시도 실패.,Tentativa de conexão fracassada.,Tentativa de conexão fracassada.,Не удалось подключиться.,尝试连接失败。,連線嘗試失敗。 +,,,Version not compatible with server. Please uninstall and try again.,Version nicht mit Server kompatibel. Bitte deinstallieren und erneut versuchen.,,"Esta versión no es compatible con el servidor. Por favor, desinstálala y vuelve a intentarlo.","Esta versión no es compatible con el servidor. Por favor, desinstálala y vuelve a intentarlo.",Version incompatible avec le serveur. Veuillez désinstaller et réessayer.,Versione non compatibile con il server. Disinstalla e riprova.,サーバと互換性のないバージョンです。アンインストールしてリトライしてください。,버전이 서버와 호환되지 않아요. 삭제 후 다시 시도하세요.,Versão incompatível com servidor. Desinstale e tente novamente.,Versão incompatível com servidor. Desinstale e tente novamente.,Версия приложения несовместима с сервером. Удалите приложение и повторите попытку.,版本与服务器不匹配。请解除安装并重试。,版本與伺服器不相容。請解除安裝後重試。 +,,,Network error {RBX_NUMBER},Netzwerkfehler {RBX_NUMBER},,Error de red {RBX_NUMBER},Error de red {RBX_NUMBER},Erreur réseau {RBX_NUMBER},Errore di rete {RBX_NUMBER},ネットワークエラー {RBX_NUMBER},네트워크 오류 {RBX_NUMBER},Erro de rede {RBX_NUMBER},Erro de rede {RBX_NUMBER},Ошибка сети {RBX_NUMBER},网络错误 {RBX_NUMBER},網路錯誤 {RBX_NUMBER} +,,,Camera Mode,Kameramodus,,Modo de cámara,Modo de cámara,Mode caméra,Modalità visuale,カメラのモード,카메라 모드,Modo da câmera,Modo da câmera,Режим камеры,镜头模式,攝影機模式 +,,,This game is not available. Please try another,Dieses Spiel ist nicht verfügbar. Bitte probiere ein anderes.,,Este juego no está disponible. Inténtalo con otro.,Este juego no está disponible. Inténtalo con otro.,Ce jeu n'est pas disponible. Veuillez en essayer un autre,Questo gioco non è disponibile. Provane un altro,このゲームは利用できません。他を試してください。,이용할 수 없는 게임이에요. 다른 게임을 시도하세요,Este jogo não está disponível. Tente outro,Este jogo não está disponível. Tente outro,Эта игра недоступна. Попробуйте другую,此游戏不可用。请试试另一个,此遊戲無法使用,請試另一個 +,,,Failed to connect to the Game. (ID = {RBX_NUMBER}: {RBX_NAME}),Verbindung zum Spiel fehlgeschlagen. (ID = {RBX_NUMBER}: {RBX_NAME}),,No se ha podido conectar con el juego. (ID = {RBX_NUMBER}: {RBX_NAME}),No se ha podido conectar con el juego. (ID = {RBX_NUMBER}: {RBX_NAME}),Échec de la connexion au jeu. (ID = {RBX_NUMBER} : {RBX_NAME}),Connessione al gioco non riuscita. (ID = {RBX_NUMBER}: {RBX_NAME}),ゲームに接続できませんでした。(ID = {RBX_NUMBER}: {RBX_NAME}),게임 연결에 실패했어요. (ID = {RBX_NUMBER}: {RBX_NAME}),Falha ao conectar no jogo. (ID = {RBX_NUMBER}: {RBX_NAME}),Falha ao conectar no jogo. (ID = {RBX_NUMBER}: {RBX_NAME}),Не удалось подключиться к игре. (ID = {RBX_NUMBER}: {RBX_NAME}),无法连接至游戏。(ID = {RBX_NUMBER}: {RBX_NAME}),無法連線到遊戲。(ID = {RBX_NUMBER}: {RBX_NAME}) +,,,This game has shut down,Dieses Spiel wurde abgebrochen.,,Este juego ha sido cerrado.,Este juego ha sido cerrado.,Ce jeu a dû fermer.,Questo gioco è stato chiuso,このゲームはシャットダウン中です。,게임이 종료되었습니다.,Este jogo foi encerrado,Este jogo foi encerrado,Эта игра больше не доступна,此游戏已关闭,此遊戲已關閉 +,,,You have lost the connection to the game,Die Verbindung zum Spiel wurde unterbrochen.,,Has perdido la conexión con el juego.,Has perdido la conexión con el juego.,Votre connexion au jeu a été perdue.,Sei stato disconnesso dal gioco,ゲームへの接続が失われました。,게임 연결이 끊겼어요,Você perdeu a conexão com o jogo,Você perdeu a conexão com o jogo,Прервано подключение к игре,你已中断与游戏的连接,您的遊戲連線被中斷 +,,,Notifications,Benachrichtigungen,,Notificaciones,Notificaciones,Notifications,Notifiche,お知らせ,알림,Notificações,Notificações,Уведомления,通知,通知 +,,,Remove From Hotbar,Schnellzugriff entfernen,,Eliminar de la barra de acceso rápido,Eliminar de la barra de acceso rápido,Retirer de la barre de raccourcis,Togli da barra scelta rap.,ホットバーから削除,핫바 창에서 제거,Remover da barra,Remover da barra,Удалить из панели быстрого доступа,从快捷栏删除,自快捷列移除 +,,,Select/Swap,Auswählen/Austauschen,,Seleccionar/alternar,Seleccionar/alternar,Sélection/Échange,Selez./Scambia,選択/交換,선택/교체,Selecionar/trocar,Selecionar/trocar,Выбрать/сменить,选取/切换,選取/切換 +,,,Close Backpack,Rucksack schließen,,Cerrar mochila,Cerrar mochila,Fermer le sac à dos,Chiudi zaino,バックパックを閉じる,배낭 닫기,Fechar mochila,Fechar mochila,Закрыть рюкзак,关闭背包,關閉背包 +,,,You have no notifications,Du hast keine Benachrichtigungen.,,No tienes notificaciones,No tienes notificaciones,Vous n'avez aucune notification,Non hai notifiche,お知らせはありません,알림이 없어요,Você não possui notificações,Você não possui notificações,Уведомлений нет,你没有通知,您無通知 +,,,Switch Tool,Werkzeug wechseln,,Alternar herramienta,Alternar herramienta,Outil d'échange,Cambia strumento,ツールを交換,도구 전환,Trocar ferramenta,Trocar ferramenta,Сменить инструмент,切换工具,切換工具 +,,,Camera Movement,Kamerabewegung,,Movimiento de la cámara,Movimiento de la cámara,Déplacement caméra,Movimento visuale,カメラの動き,카메라 이동,Movimento da câmera,Movimento da câmera,Движение камеры,镜头移动模式,攝影機移動 +,,,Game Menu Toggle,Spielmenü umschalten,,Alternar menú del juego,Alternar menú del juego,Menu du jeu (act./désact.),Attiva/Disattiva menu di gioco,ゲームメニュートグル,게임 메뉴 전환,Ativar menu do jogo,Ativar menu do jogo,Переключение игрового меню,游戏菜单切换,遊戲選單切換 +,,,Menu Navigation,Menünavigation,,Navegación del menú,Navegación del menú,Navigation du menu,Menu di navigazione,メニューナビゲーション,메뉴 내비게이션,Navegação do menu,Navegação do menu,Навигация меню,菜单导航,選單導覽 +,,,Back,Zurück,,Atrás,Atrás,Retour,Indietro,戻る,뒤로,Voltar,Voltar,Назад,返回,背面 +,,,Camera Zoom,Kamerazoom,,Distancia de la cámara,Distancia de la cámara,Zoom caméra,Zoom visuale,カメラズーム,카메라 줌,Zoom da câmera,Zoom da câmera,Масштаб камеры,镜头缩放,攝影機縮放 +,,,Account: Over 13 yrs,Konto: Über 13 J.,,Cuenta: Mayor de 13 años,Cuenta: Mayor de 13 años,Compte : Plus de 13 ans,Account: più di 13 anni,アカウント:13才以上,계정: 만 13세 이상,Conta: Mais de 13 anos,Conta: Mais de 13 anos,Учетная запись: Старше 13 лет,帐户:超过 13 岁,帳戶:超過 13 歲 +,,,Dismiss,Verwerfen,,Descartar,Descartar,Rejeter,Ignora,却下,취소,Dispensar,Dispensar,Отклонить,关闭,關閉 +,,,New Follower,Neuer Follower,,Seguidor nuevo,Seguidor nuevo,Nouvel abonné,Nuovo follower,新規フォロワー,새 팔로워,Novo seguidor,Novo seguidor,Новый подписчик,新粉丝,新關注者 +,,,Cancel,Abbrechen,,Cancelar,Cancelar,Annuler,Annulla,キャンセル,취소,Cancelar,Cancelar,Отмена,取消,取消 +,,,An error occurred while unblocking {RBX_NAME}. Please try again later.,Bei der Aufhebung der Sperre von {RBX_NAME} ist ein Fehler aufgetreten. Bitte versuche es später erneut.,,Se ha producido un error al desbloquear a {RBX_NAME}. Inténtalo de nuevo más tarde.,Se ha producido un error al desbloquear a {RBX_NAME}. Inténtalo de nuevo más tarde.,Une erreur est survenue lors de la levée du blocage de {RBX_NAME}. Veuillez réessayer plus tard.,Si è verificato un errore nello sblocco di {RBX_NAME}. Riprova più tardi.,{RBX_NAME}のブロック解除中にエラーが発生しました。しばらくしてからやり直してください。,{RBX_NAME}님 차단을 해제하는 중 오류 발생. 나중에 다시 시도하세요.,Um erro ocorreu ao desbloquear {RBX_NAME}. Tente de novo mais tarde.,Um erro ocorreu ao desbloquear {RBX_NAME}. Tente de novo mais tarde.,При разблокировке пользователя {RBX_NAME} произошла ошибка. Повторите попытку позже.,取消屏蔽“{RBX_NAME}”时出错。请稍候重试。,解除封鎖{RBX_NAME}時發生錯誤。請稍後再試。 +,,,Would you like to unblock {RBX_NAME}?,Möchtest du die Sperre von {RBX_NAME} aufheben?,,¿Quieres desbloquear a {RBX_NAME}?,¿Quieres desbloquear a {RBX_NAME}?,Souhaitez-vous lever le blocage de {RBX_NAME} ?,Vuoi sbloccare {RBX_NAME}?,{RBX_NAME}をブロック解除しますか?,{RBX_NAME}님 차단을 해제할까요?,Gostaria de desbloquear {RBX_NAME}?,Gostaria de desbloquear {RBX_NAME}?,Разблокировать пользователя {RBX_NAME}?,你想要解除对“{RBX_NAME}”的屏蔽吗?,您是否要解除封鎖{RBX_NAME}? +,,,You are too far away to chat!,"Du bist zu weit entfernt, um zu chatten!",,¡Estás demasiado lejos para chatear!,¡Estás demasiado lejos para chatear!,Vous êtes trop loin pour discuter !,Sei troppo lontano per chattare!,遠すぎてチャット出来ません!,채팅하기에 너무 멀어요!,Você está longe demais para participar do chat!,Você está longe demais para participar do chat!,Вы слишком далеко и не можете общаться в чате!,距离太远,无法聊天!,您距離太遠,無法聊天! +,,,Chat ended because you walked away,"Chat wurde beendet, da du dich entfernt hast.",,El chat ha finalizado porque te has alejado.,El chat ha finalizado porque te has alejado.,Le chat a pris fin car vous êtes parti,"La chat terminata, ti sei allontanato",立ち去ったのでチャットが終了しました。,이탈하여 채팅이 종료되었어요,O chat terminou porque você se afastou,O chat terminou porque você se afastou,Чат завершен: вы ушли.,你已走开,聊天结束,因您走遠,聊天已結束 +,,,Chat ended because you didn't reply,"Chat wurde beendet, da du nicht geantwortet hast.",,El chat ha finalizado porque no has contestado.,El chat ha finalizado porque no has contestado.,Le chat a pris fin car vous n'avez pas répondu,"La chat terminata, non hai risposto",返事をしなかったのでチャットが終了しました。,대답하지 않아 채팅이 종료되었어요,O chat terminou porque você não respondeu,O chat terminou porque você não respondeu,Чат завершен: вы не ответили.,你没有回答,聊天结束,因您未回答,聊天已結束 +,,,Goodbye!,Tschüss!,,¡Hasta luego!,¡Hasta luego!,Au revoir !,Addio!,さようなら!,안녕히 가세요!,Tchau!,Tchau!,До свидания!,再见!,再會! +,,,Character Movement,Charakterbewegung,,Movimiento del personaje,Movimiento del personaje,Déplacement personnage,Movimento personaggio,キャラクターの動き,캐릭터 이동,Movimento de personagem,Movimento de personagem,Движение персонажа,人物移动模式,人物移動 +,,,Your account balance will not be affected by this transaction.,Das Guthaben deines Kontos wird durch diese Transaktion nicht beeinflusst.,,Esta transacción no modificará el saldo de tu cuenta.,Esta transacción no modificará el saldo de tu cuenta.,Le solde de votre compte ne sera pas affecté par cette transaction.,I fondi del tuo account non verranno intaccati da questa transazione.,この取引であなたのアカウントは変化しません。,계정 잔액은 이 거래의 영향을 받지 않아요.,O saldo da sua conta não será afetado por esta transação.,O saldo da sua conta não será afetado por esta transação.,После операции средства не будут списаны с вашей учетной записи.,你的帐户余额将不会收到此次交易的影响。,您的帳戶餘額不受此交易影響。 +,,,You already own this item. Your account has not been charged.,Diesen Gegenstand besitzt du bereits. Dein Konto wurde nicht belastet.,,Ya tienes este objeto. No se ha llevado a cabo ningún cobro en tu cuenta.,Ya tienes este objeto. No se ha llevado a cabo ningún cobro en tu cuenta.,Vous possédez déjà cet objet. Votre compte n'a pas été débité.,Possiedi già questo oggetto. Il tuo account non ha subito addebiti.,既にこのアイテムを持っています。アカウントがチャージされませんでした。,이 아이템을 이미 가지고 있어요. 계정에 비용이 청구되지 않아요.,Você já possui este item. Nada foi cobrado da sua conta.,Você já possui este item. Nada foi cobrado da sua conta.,У вас уже есть этот предмет. Средства с вашей учетной записи не списаны.,你已拥有这件物品。你的帐户未被扣款。,您已有此物,未自您的帳戶收費。 +,,,This is a test purchase; your account will not be charged.,Dies ist ein Testkauf. Dein Konto wird dadurch nicht belastet.,,Esta compra es una prueba; no se llevará a cabo ningún cobro en tu cuenta.,Esta compra es una prueba; no se llevará a cabo ningún cobro en tu cuenta.,Ceci est un achat test ; votre compte ne sera pas débité.,Questo è un acquisto di prova; il tuo account non subirà addebiti.,これはテスト購入です。アカウントはチャージされません。,테스트 구매입니다. 계정에 비용이 청구되지 않아요.,Esta é uma compra de teste. Nada será cobrado da sua conta.,Esta é uma compra de teste. Nada será cobrado da sua conta.,Это тестовая покупка; средства не будут списаны с вашей учетной записи.,此次购买为测试性质;你的帐户将不会被扣款。,此為測試性購買;不會自您的帳戶收費。 +,,,This was a test purchase.,Dies war ein Testkauf.,,Esta compra ha sido una prueba.,Esta compra ha sido una prueba.,C'était un achat test.,Questo era un acquisto di prova.,テスト購入でした。,이 구매는 테스트용입니다.,Esta foi uma compra de teste.,Esta foi uma compra de teste.,Это была тестовая покупка.,此次购买为测试性质。,此為測試性購買。 +,,,Chat,Chat,,Chat,Chat,Chat,Chat,チャット,채팅,Chat,Chat,Чат,聊天,聊天 +,,,Builders Club,Builders Club,,Builders Club,Builders Club,Builders Club,Builders Club,ビルダーズクラブ,,Builders Club,Builders Club,Клуб создателей,Builders Club, +,,,Turbo Builders Club,Turbo Builders Club,,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,ターボビルダーズクラブ,,Turbo Builders Club,Turbo Builders Club,Потрясающий клуб создателей,Turbo Builders Club, +,,,Outrageous Builders Club,Outrageous Builders Club,,Outrageous Builders Club,Outrageous Builders Club,Outrageous Builders Club,Turbo Builders Club,もの凄いビルダーズクラブ,,Outrageous Builders Club,Outrageous Builders Club,Невероятный клуб создателей,Outrageous Builders Club, +,,,Image,Bild,,Imagen,Imagen,Image,Immagine,イメージ,이미지,Imagem,Imagem,Изображение,图像,圖像 +,,,T-Shirt,T-Shirt,,Camiseta,Camiseta,Tee-shirt,Maglietta,Tシャツ,티셔츠,Camiseta,Camiseta,Футболка,T 恤,T恤 +,,,Audio,Audio,,Audio,Audio,Audio,Audio,オーディオ,오디오,Áudio,Áudio,Звук,音频,音訊 +,,,Mesh,Mesh,,Mesh,Mesh,Maillage,Mesh,メッシュ,Mesh,Malha,Malha,Сетка,网格,網格 +,,,Lua,Lua,,Lua,Lua,Lua,Lua,Lua,Lua,Lua,Lua,Lua,Lua ,Lua +,,,HTML,HTML,,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML +,,,Text,Text,,Texto,Texto,Texte,Testo,テキスト,텍스트,Texto,Texto,Текст,文字,文字 +,,,Hat,Hut,,Sombrero,Sombrero,Chapeau,Cappello,帽子,모자,Chapéu,Chapéu,Головной убор,帽子,帽子 +,,,Place,Ort,,Lugar,Lugar,Emplacement,Località,プレース,장소,Local,Local,Разместить,地点,地點 +,,,Model,Modell,,Modelo,Modelo,Modèle,Modello,モデル,모델,Modelo,Modelo,Модель,模型,模型 +,,,Shirt,Hemd,,Camisa,Camisa,Chemise,Camicia,シャツ,셔츠,Camisa,Camisa,Рубашка,衬衫,上衣 +,,,Pants,Hose,,Pantalones,Pantalones,Pantalon,Pantaloni,パンツ,바지,Calças,Calças,Штаны,裤子,褲子 +,,,Decal,Decal,,Adhesivo,Adhesivo,Décalcomanie,Decalcomania,デカール,데칼,Decalque,Decalque,Наклейка,贴纸,貼花 +,,,Avatar,Avatar,,Avatar,Avatar,Avatar,Avatar,アバター,아바타,Avatar,Avatar,Аватар,虚拟形象,虛擬人偶 +,,,Head,Kopf,,Cabeza,Cabeza,Tête,Testa,頭,머리,Cabeça,Cabeça,Голова,头部,頭 +,,,Face,Gesicht,,Cara,Cara,Visage,Faccia,顔,얼굴,Rosto,Rosto,Лицо,脸部,臉 +,,,Gear,Ausrüstung,,Equipamiento,Equipamiento,Équipement,Attrezzatura,ギア,기어,Equipamento,Equipamento,Снаряжение,装备,裝備 +,,,Cheating/Exploiting,Schummeln/Ausnutzen von Spielfehlern,,Trampas/abuso de errores,Trampas/abuso de errores,Triche/Exploitation,Inganno/Sfruttamento,チート/悪用,속임수/악용,Trapaça/abuso,Trapaça/abuso,Жульничество,作弊/开挂,作弊/剝削 +,,,Badge,Abzeichen,,Emblema,Emblema,Badge,Contrassegno,バッジ,배지,Emblema,Emblema,Значок,徽章,徽章 +,,,Group Emblem,Gruppenemblem,,Emblema de grupo,Emblema de grupo,Emblème de groupe,Emblema gruppo,グループエンブレム,그룹 엠블렘,Emblema de grupo,Emblema de grupo,Эмблема группы,群组徽章,群組徽章 +,,,Animation,Animation,,Animación,Animación,Animation,Animazione,アニメーション,애니메이션,Animação,Animação,Анимация,动画,動畫 +,,,Arms,Arme,,Brazos,Brazos,Bras,Braccia,腕,팔,Braços,Braços,Руки,手臂,臂 +,,,Legs,Beine,,Piernas,Piernas,Jambes,Gambe,脚,다리,Pernas,Pernas,Ноги,腿部,腿 +,,,Torso,Körper,,Torso,Torso,Torse,Busto,胴体,상체,Tronco,Tronco,Корпус,身体主干,軀幹 +,,,Right Arm,Rechter Arm,,Brazo derecho,Brazo derecho,Bras droit,Braccio destro,右腕,오른팔,Braço direito,Braço direito,Правая рука,右臂,右臂 +,,,Left Arm,Linker Arm,,Brazo izquierdo,Brazo izquierdo,Bras gauche,Braccio sinistro,左腕,왼팔,Braço esquerdo,Braço esquerdo,Левая рука,左臂,左臂 +,,,Left Leg,Linkes Bein,,Pierna izquierda,Pierna izquierda,Jambe gauche,Gamba sinistra,左脚,왼쪽 다리,Perna esquerda,Perna esquerda,Левая нога,左腿,左腿 +,,,Right Leg,Rechtes Bein,,Pierna derecha,Pierna derecha,Jambe droite,Gamba destra,右脚,오른쪽 다리,Perna direita,Perna direita,Правая нога,右腿,右腿 +,,,Choose One,Wähle eine der folgenden Optionen,,Elige uno,Elige uno,En choisir un(e),Scegli,ひとつ選ぶ,한 가지 선택,Escolha um,Escolha um,Выберите один вариант,选择一项,請擇一 +,,,Package,Paket,,Paquete,Paquete,Pack,Pacchetto,パッケージ,패키지,Pacote,Pacote,Набор,套装,套件 +,,,YouTube Video,YouTube-Video,,Vídeo de YouTube,Vídeo de YouTube,Vidéo YouTube,Video su YouTube,YouTube ビデオ,YouTube 비디오,Vídeo do YouTube,Vídeo do YouTube,Видео YouTube,Youtube 视频,YouTube 影片 +,,,Game Pass,Spielpass,,Pase de juego,Pase de juego,Passe de jeu,Pass di gioco,ゲームパス,게임패스,Passe de jogo,Passe de jogo,Игровой пропуск,游戏通行证,遊戲通行證 +,,,Plugin,Plug-in,,Plugin,Plugin,Plugin,Plug-in,プラグイン,플러그인,Plugin,Plugin,Расширение,插件,外掛程式 +,,,SolidModel,SolidModel,,SolidModel,SolidModel,SolidModel,Modello solido,ソリッドモデル,,SolidModel,SolidModel,Твердая модель,SolidModel, +,,,MeshPart,MeshPart,,MeshPart,MeshPart,MeshPart,Parte mesh,メッシュパーツ,,SolidModel,SolidModel,Полигональная часть,MeshPart, +,,,Hair Accessory,Haar-Accessoire,,Accesorio para el pelo,Accesorio para el pelo,Accessoire de cheveux,Accessorio capelli,ヘアアクセサリ,헤어 액세서리,Acessório de cabelo,Acessório de cabelo,Аксессуар для волос,头发配饰,頭髮配件 +,,,Face Accessory,Gesichts-Accessoire,,Accesorio para la cara,Accesorio para la cara,Accessoire de visage,Accessorio faccia,顔アクセサリ,얼굴 액세서리,Acessório de rosto,Acessório de rosto,Аксессуар для лица,脸部配饰,臉部配件 +,,,Neck Accessory,Hals-Accessoire,,Accesorio para el cuello,Accesorio para el cuello,Accessoire de cou,Accessorio collo,首アクセサリ,목 액세서리,Acessório de pescoço,Acessório de pescoço,Аксессуар для шеи,颈部配饰,頸部配件 +,,,Shoulder Accessory,Schulter-Accessoire,,Accesorio para el hombro,Accesorio para el hombro,Accessoire d'épaule,Accessorio spalla,肩アクセサリ,어깨 액세서리,Acessório de ombro,Acessório de ombro,Аксессуар для плеч,肩部配饰,肩膀配件 +,,,Classic,Klassisch,,Clásico,Clásico,Classique,Classica,クラッシック,클래식,Clássico,Clássico,Классика,经典,古典 +,,,Front Accessory,Vorderseiten-Accessoire,,Accesorio frontal,Accesorio frontal,Accessoire avant,Accessorio anteriore,前面アクセサリ,가슴 액세서리,Acessório da frente,Acessório da frente,Передний аксессуар,正面配饰,正面配件 +,,,Back Accessory,Rückseiten-Accessoire,,Accesorio trasero,Accesorio trasero,Accessoire arrière,Accessorio posteriore,背面アクセサリ,등 액세서리,Acessório de trás,Acessório de trás,Задний аксессуар,背面配饰,背面配件 +,,,Waist Accessory,Taillen-Accessoire,,Accesorio para la cintura,Accesorio para la cintura,Accessoire de taille,Accessorio vita,ウエストアクセサリ,허리 액세서리,Acessório de cintura,Acessório de cintura,Аксессуар для талии,腰部配饰,腰部配件 +,,,Climb Animation,Kletteranimation,,Animación de escalada,Animación de escalada,Animation d'escalade,Animazione scalata,登りアニメーション,오르기 애니메이션,Animação de escalada,Animação de escalada,Анимация подъема,攀爬动画,攀爬動畫 +,,,Fall Animation,Fallanimation,,Animación de caída,Animación de caída,Animation de chute,Animazione caduta,落下アニメーション,낙하 애니메이션,Animação de queda,Animação de queda,Анимация падения,下降动画,下降動畫 +,,,Idle Animation,Untätigkeitsanimation,,Animación de inactividad,Animación de inactividad,Animation oisif,Animazione inattività,アイドル状態アニメーション,기본 애니메이션,Animação de inatividade,Animação de inatividade,Анимация ожидания,闲置动画,閒置動畫 +,,,Jump Animation,Springanimation,,Animación de salto,Animación de salto,Animation de saut,Animazione salto,ジャンプアニメーション,점프 애니메이션,Animação de salto,Animação de salto,Анимация прыжка,跳跃动画,跳躍動畫 +,,,Run Animation,Laufanimation,,Animación de carrera,Animación de carrera,Animation de course,Animazione corsa,ランアニメーション,달리기 애니메이션,Animação de corrida,Animação de corrida,Анимация бега,跑步动画,奔跑動畫 +,,,Swim Animation,Schwimmanimation,,Animación de natación,Animación de natación,Animation de nage,Animazione nuotata,泳ぎアニメーション,수영 애니메이션,Animação de nado,Animação de nado,Анимация плавания,游泳动画,游泳動畫 +,,,Walk Animation,Gehanimation,,Animación de marcha,Animación de marcha,Animation de marche,Animazione camminata,歩きアニメーション,걷기 애니메이션,Animação de caminhada,Animação de caminhada,Анимация ходьбы,行走动画,步行動畫 +,,,Click to Move,Zum Bewegen klicken,,Clic para moverse,Clic para moverse,Cliquer pour se déplacer,Clicca per muovere,クリックして移動,클릭해 이동,Clique para mover,Clique para mover,"Нажмите, чтобы передвигаться",点按以移动,滑鼠移動 +,,,This item cost more ROBUX than you have available. Please leave this game and go to the ROBUX screen to purchase more.,"Dieser Gegenstand kostet mehr ROBUX, als dir zur Verfügung stehen. Bitte verlasse dieses Spiel, gehe zum ROBUX-Menü und kaufe dort mehr.",,Este objeto cuesta más ROBUX de los que tienes disponibles. Sal de este juego y ve a la pantalla de ROBUX para comprar más.,Este objeto cuesta más ROBUX de los que tienes disponibles. Sal de este juego y ve a la pantalla de ROBUX para comprar más.,Cet objet coûte plus de ROBUX que vous n'en avez. Veuillez quitter le jeu et aller à l'écran ROBUX pour en acheter plus.,Questo oggetto costa più ROBUX di quanti tu ne abbia. Esci dal gioco e vai nella schermata dei ROBUX per acquistarne altri.,このアイテムを買うにはお持ちのROBUXでは足りません。ゲームをやめて購入のためROBUX画面に行ってください。,이 아이템의 가격은 소지한 ROBUX 보다 비싸요. 더 많은 ROBUX를 구매하려면 이 게임을 나간 후 ROBUX 화면으로 이동하세요.,Este item custa mais ROBUX do que você possui disponível. Saia do jogo e vá para a tela de ROBUX para comprar mais.,Este item custa mais ROBUX do que você possui disponível. Saia do jogo e vá para a tela de ROBUX para comprar mais.,"Цена товара превышает имеющееся у вас количество ROBUX. Выйдите из этой игры и перейдите в раздел ROBUX, чтобы купить больше.",此物品的价格超过了你拥有的 ROBUX。请离开此游戏,前往 ROBUX 屏幕以购买更多。,此項目花費的 ROBUX 超過您所能花用。請離開此遊戲,前往 ROBUX 畫面加購。 +,,,This item cost more ROBUX than you can purchase. Please visit www.roblox.com to purchase more ROBUX.,"Dieser Gegenstand kostet mehr ROBUX, als du kaufen kannst. Bitte besuche www.roblox.com, um mehr ROBUX zu kaufen.",,Este objeto cuesta más ROBUX de los que puedes comprar. Visita www.roblox.com para comprar más ROBUX.,Este objeto cuesta más ROBUX de los que puedes comprar. Visita www.roblox.com para comprar más ROBUX.,Cet objet coûte trop de ROBUX pour que vous l'achetiez. Veuillez vous rendre sur www.roblox.com pour acheter plus de ROBUX.,Questo oggetto costa più ROBUX di quanti tu possa acquistarne. Visita il sito www.roblox.com per acquistare altri ROBUX.,このアイテムを買うにはあなたが購入可能なROBUXでは足りません。www.roblox.com にアクセスしてROBUXを追加購入してください。,이 아이템 구매에는 더 많은 ROBUX가 필요해요. ROBUX를 더 구매하려면 www.roblox.com을 방문하세요.,Este item custa mais ROBUX do que você pode comprar. Visite www.roblox.com para comprar mais ROBUX.,Este item custa mais ROBUX do que você pode comprar. Visite www.roblox.com para comprar mais ROBUX.,"Цена товара превышает количество ROBUX, которое вы можете купить. Перейдите на страницу www.roblox.com, чтобы приобрести больше ROBUX.",此物品的价格超过了你拥有的 ROBUX。请前往 www.roblox.com 购买更多 ROBUX。,此項目花費的 ROBUX 超過您所能購買。請瀏覽 www.roblox.com 加購 ROBUX。 +,,,You need {RBX_NUMBER} more {RBX_NAME} to buy this item.,"Du benötigst noch {RBX_NUMBER} weitere {RBX_NAME}, um diesen Gegenstand zu kaufen.",,Necesitas {RBX_NUMBER} {RBX_NAME} más para comprar este objeto.,Necesitas {RBX_NUMBER} {RBX_NAME} más para comprar este objeto.,Vous avez besoin de {RBX_NUMBER} {RBX_NAME} de plus pour acheter cet objet.,Hai bisogno di {RBX_NUMBER} {RBX_NAME} in più per comprare questo oggetto.,このアイテムを買うにはあと {RBX_NUMBER} の {RBX_NAME} が必要です。,이 아이템을 구매하려면 {RBX_NAME}이(가) {RBX_NUMBER}개 더 필요해요.,Você precisa de mais {RBX_NUMBER} {RBX_NAME} para comprar este item.,Você precisa de mais {RBX_NUMBER} {RBX_NAME} para comprar este item.,Для покупки этого товара требуется на {RBX_NUMBER} больше {RBX_NAME}.,你还需要 {RBX_NUMBER} 个 “{RBX_NAME}” 才能购买此物品。,您需要再多 {RBX_NUMBER} 個{RBX_NAME}方能購買此項目。 +,,,Do you allow game to create new place in your inventory?,"Erlaubst du dem Spiel, mehr Platz in deinem Inventar zu schaffen?",,¿Quieres permitir que el juego cree un espacio nuevo en tu inventario?,¿Quieres permitir que el juego cree un espacio nuevo en tu inventario?,Autorisez-vous le jeu à créer un nouvel emplacement dans votre inventaire ?,Vuoi permettere al gioco di creare nuovo spazio nel tuo inventario?,あなたのインベントリに新しい場所を作成しても良いですか?,게임에서 인벤토리에 새로운 장소를 만들도록 할까요?,Você permite que o jogo crie um novo espaço em seu inventário?,Você permite que o jogo crie um novo espaço em seu inventário?,Разрешить игре создать новое место в вашем инвентаре?,你是否允许游戏在你的道具中创建新的地点?,您是否允許遊戲在您的庫存建立新位置? +,,,Hiding Core GUI,Grafische Standard-Benutzeroberfläche verbergen,,Ocultando GUI básica,Ocultando GUI básica,Cacher l'IGU principale,Nascondere interfaccia base,コア GUI を隠す。,코어 GUI 숨기기,Escondendo interface básica,Escondendo interface básica,Скрытие элементов основного интерфейса,隐藏核心 GUI,隱藏核心 GUI +,,,Ctrl-Shift-G again to restore.,Zum Wiederherstellen erneut Strg-Umschalt-G drücken.,,Vuelve a pulsar Ctrl-Mayús-G para restablecerla.,Vuelve a pulsar Ctrl-Mayús-G para restablecerla.,Ctrl-Maj-G pour la restaurer.,Ancora CTRL-MAIUSC-G per ripristinare.,Ctrl-Shift-G をもう一度押してリストア。,복원하려면 Ctrl-Shift-G를 클릭하세요.,Ctrl-Shift-G de novo para restaurar.,Ctrl-Shift-G de novo para restaurar.,"Снова нажмите Ctrl-Shift-G, чтобы восстановить.",重按 Ctrl-Shift-G 以还原。,再次按 Ctrl-Shift-G 鍵以復原。 +,,,Hiding Custom GUI,Individuelle grafische Benutzeroberfläche verbergen,,Ocultando GUI personalizada,Ocultando GUI personalizada,Cacher l'IGU personnalisée,Nascondere interfaccia personalizzata,カスタムGUIを隠す。,사용자 정의 GUI 숨기기,Escondendo interface personalizada,Escondendo interface personalizada,Скрытие элементов пользовательского интерфейса,隐藏自定义 GUI,隱藏自訂 GUI +,,,Ctrl-Shift-C again to restore.,Zum Wiederherstellen erneut Strg-Umschalt-C drücken.,,Vuelve a pulsar Ctrl-Mayús-C para restablecerla.,Vuelve a pulsar Ctrl-Mayús-C para restablecerla.,Ctrl-Maj-C pour la restaurer.,Ancora CTRL-MAIUSC-C per ripristinare.,Ctrl-Shift-Cをもう一度教えてリストア。,복원하려면 Ctrl-Shift-C를 클릭하세요.,Ctrl-Shift-C de novo para restaurar.,Ctrl-Shift-C de novo para restaurar.,"Снова нажмите Ctrl-Shift-C, чтобы восстановить.",重按 Ctrl-Shift-C 以还原。,再次按 Ctrl-Shift-C 鍵以復原。 +,,,"Requested game is full, retrying...",Gewünschtes Spiel ist voll. Neuer Versuch ...,,El juego solicitado está lleno. Intentándolo de nuevo...,El juego solicitado está lleno. Intentándolo de nuevo...,"Le jeu demandé est complet, nouvelle tentative...",Il gioco richiesto è al completo. Nuovo tentativo...,リクエストしたゲームは満員です。リトライ中...,요청한 게임이 가득 찼어요. 재시도 중...,O jogo solicitado está cheio. Tentando de novo...,O jogo solicitado está cheio. Tentando de novo...,Запрашиваемая игра переполнена. Новая попытка...,已请求的游戏已满员。正在重试...,請求的遊戲已滿額,重試中... +,,,Could not connect to game because game is disabled,"Verbindung zum Spiel konnte nicht hergestellt werden, da das Spiel deaktiviert ist.",,No se ha podido conectar al juego porque está desactivado.,No se ha podido conectar al juego porque está desactivado.,Impossible de se connecter au jeu car il a été désactivé,Impossibile connettersi al gioco perché è disattivato,ゲームが無効なため接続できませんでした。,게임이 비활성화되어 접속할 수 없어요,Impossível conectar pois o jogo está desabilitado,Impossível conectar pois o jogo está desabilitado,"Не удалось подключиться к игре, так как она недоступна.",此游戏已停用,无法连接。,無法連線到遊戲,因為遊戲已停用 +,,,Could not connect to game because game failed to start,"Verbindung zum Spiel konnte nicht hergestellt werden, da das Spiel nicht gestartet werden konnte.",,No se ha podido conectar al juego porque no se ha podido iniciar.,No se ha podido conectar al juego porque no se ha podido iniciar.,Impossible de se connecter au jeu car il n'a pas réussi à être lancé,Impossibile connettersi al gioco perché l'avvio non è riuscito,ゲームの起動に失敗したため接続できませんでした。,게임 시작에 실패하여 접속할 수 없어요,Impossível conectar pois o jogo não conseguiu iniciar,Impossível conectar pois o jogo não conseguiu iniciar,"Не удалось подключиться к игре, так как она не запустилась.",开始此游戏失败,无法连接。,無法連線到遊戲,因為遊戲無法啟動 +,,,D/Right Arrow,D/Rechtspfeil,,D/Cursor derecho,D/Cursor derecho,D/Flèche droite,D/Freccia DESTRA,D/右カーソル,D/오른쪽 화살표,D/seta direita,D/seta direita,D/стрелка вправо,D/右箭头,D 鍵 / 向右箭號 +,,,Could not connect to game because game has ended,"Verbindung zum Spiel konnte nicht hergestellt werden, da das Spiel beendet wurde.",,No se ha podido conectar al juego porque ha terminado.,No se ha podido conectar al juego porque ha terminado.,Impossible de se connecter au jeu car il est terminé,Impossibile connettersi al gioco perché è terminato,ゲームが終了したため接続できませんでした。,게임이 종료되어 접속할 수 없어요,Impossível conectar pois o jogo já acabou,Impossível conectar pois o jogo já acabou,"Не удалось подключиться к игре, так как она завершилась.",此游戏已结束,无法连接。,無法連線到遊戲,因為遊戲已結束 +,,,Could not connect to game because game is full,"Verbindung zum Spiel konnte nicht hergestellt werden, da das Spiel voll ist.",,No se ha podido conectar al juego porque está lleno.,No se ha podido conectar al juego porque está lleno.,Impossible de se connecter au jeu car il est complet,Impossibile connettersi al gioco perché è al completo,ゲームが満員なため接続できませんでした。,게임이 가득 차서 접속할 수 없어요,Impossível conectar pois o jogo está cheio,Impossível conectar pois o jogo está cheio,"Не удалось подключиться к игре, так как она переполнена.",此游戏已满员,无法连接。,無法連線到遊戲,因為此遊戲已滿額 +,,,Could not connect to game because user you were following has left the game,"Verbindung zum Spiel konnte nicht hergestellt werden, da der Benutzer, dem du gefolgt bist, das Spiel verlassen hat.",,No se ha podido conectar al juego porque el jugador al que seguías ha salido del mismo.,No se ha podido conectar al juego porque el jugador al que seguías ha salido del mismo.,Impossible de se connecter au jeu car l'utilisateur que vous suiviez l'a quitté,Impossibile connettersi al gioco perché l'utente che stavi seguendo è uscito,フォロー中のユーザーがゲームから退出したため接続できませんでした。,팔로우 중인 사용자가 게임에서 나가서 게임에 접속할 수 없어요,Impossível conectar pois o usuário que você estava seguindo saiu do jogo,Impossível conectar pois o usuário que você estava seguindo saiu do jogo,"Не удалось подключиться к игре, так как игрок, за которым вы последовали, ее покинул.",你关注的用户已离开游戏,因此无法连接。,無法連線至遊戲,因為您關注的使用者已離開遊戲 +,,,Could not connect to game because it is not available for your platform,"Verbindung zum Spiel konnte nicht hergestellt werden, da es für deine Plattform nicht verfügbar ist.",,No se ha podido conectar al juego porque no está disponible para tu plataforma.,No se ha podido conectar al juego porque no está disponible para tu plataforma.,Impossible de se connecter au jeu car il n'est pas disponible sur votre plateforme,Impossibile connettersi al gioco perché non è disponibile per la tua piattaforma,プラットフォームが非対応なため接続できませんでした。,플랫폼에서 이용할 수 없는 게임이므로 접속할 수 없어요,Impossível conectar pois o jogo não está disponível para a sua plataforma,Impossível conectar pois o jogo não está disponível para a sua plataforma,"Не удалось подключиться к игре, так как она недоступна на вашей платформе.",此游戏在你的平台上不可用,因此无法连接。,無法連線到遊戲,因為不適用於您的平台 +,,,Could not connect to game due to join script failure,"Verbindung zum Spiel konnte nicht hergestellt werden, da ein Problem mit dem Beitrittsskript aufgetreten ist.",,No se ha podido conectar al juego a causa de un error en el script de unión.,No se ha podido conectar al juego a causa de un error en el script de unión.,Impossible de se connecter au jeu du fait d'un échec du script pour le rejoindre,Impossibile connettersi al gioco per un errore dello script di accesso,参加スクリプトの失敗により接続できませんでした。,참가 스크립트를 가져오지 못해 게임에 접속할 수 없어요,Impossível conectar devido a uma falha no script de entrada,Impossível conectar devido a uma falha no script de entrada,Не удалось подключиться к игре из-за ошибки в сценарии подключения.,由于加入脚本失败,无法连接至游戏,無法連線到遊戲,原因是加入指令碼失敗 +,,,"Game join request expired or invalid, please try again.",Anfrage zum Spielbeitritt abgelaufen oder ungültig. Bitte versuche es erneut.,,La solicitud para unirse al juego ha caducado o no es válida. Inténtalo de nuevo.,La solicitud para unirse al juego ha caducado o no es válida. Inténtalo de nuevo.,"La demande pour rejoindre le jeu a expirée ou est invalide, veuillez réessayer.",Richiesta di accesso al gioco scaduta o non valida. Riprova.,ゲーム参加リクエストが期限切れか無効です。やり直してください。,게임 가입 요청이 만료되었거나 잘못되었어요. 다시 시도하세요.,Pedido de entrada no jogo expirado ou inválido. Tente novamente.,Pedido de entrada no jogo expirado ou inválido. Tente novamente.,"Срок приглашения в игру истек, или оно является недействительным. Повторите попытку.",加入游戏的请求已过期或无效,请重试。,遊戲加入請求過期或無效,請再試一次。 +,,,"Could not connect to game, please try again later.",Verbindung zum Spiel konnte nicht hergestellt werden. Bitte versuche es später erneut.,,No se ha podido conectar al juego. Inténtalo de nuevo más tarde.,No se ha podido conectar al juego. Inténtalo de nuevo más tarde.,"Impossible de se connecter au jeu, veuillez réessayer plus tard.",Impossibile connettersi al gioco. Riprova più tardi.,ゲームに接続できませんでした。しばらくしてからやり直してください。,게임에 접속할 수 없어요. 나중에 다시 시도하세요.,Impossível conectar ao jogo. Tente de novo mais tarde.,Impossível conectar ao jogo. Tente de novo mais tarde.,Не удалось подключиться к игре. Повторите попытку позже.,无法连接至游戏,请稍候重试。,無法連線到遊戲,請稍後再試 +,,,Confirm Unblock,Aufhebung der Sperre bestätigen,,Confirmar desbloqueo,Confirmar desbloqueo,Confirmer le déblocage,Conferma sblocco,ブロック解除を確認,차단 해제 확인,Confirmar desbloqueio,Confirmar desbloqueio,Подтверждение разблокировки,确认取消屏蔽,確認解除封鎖 +,,,Error Blocking Player,Fehler beim Sperren des Spielers,,Error al bloquear al jugador,Error al bloquear al jugador,Erreur en bloquant le joueur,Errore di blocco giocatore,プレーヤーブロックエラー,플레이어 차단 오류,Erro ao bloquear jogador,Erro ao bloquear jogador,Ошибка блокировки игрока,屏蔽玩家时出错,封鎖玩家時發生錯誤 +,,,Confirm Block,Sperre bestätigen,,Confirmar bloqueo,Confirmar bloqueo,Confirmer le blocage,Conferma blocco,ブロックを確認,차단 확인,Confirmar bloqueio,Confirmar bloqueio,Подтверждение блокировки,确认屏蔽,確認封鎖 +,,,Dating,Dating,,Solicitud de cita,Solicitud de cita,Drague,Appuntamento,デートの誘い,교제,Namoro,Namoro,Флирт,约会,約會 +,,,Error Unblocking Player,Fehler beim Aufheben der Sperre des Spielers,,Error al desbloquear al jugador,Error al desbloquear al jugador,Erreur en levant le blocage du joueur,Errore di sblocco giocatore,プレーヤーブロック解除エラー,플레이어 차단 해제 오류,Erro ao desbloquear jogador,Erro ao desbloquear jogador,Ошибка разблокировки игрока,取消屏蔽玩家时出错,解除封鎖玩家時發生錯誤 +,,,An error occurred while unfriending {RBX_NAME}. Please try again later.,Bei der Entfernung von {RBX_NAME} als Freund ist ein Fehler aufgetreten. Bitte versuche es später erneut.,,Se ha producido un error al cancelar la amistad con {RBX_NAME}. Inténtalo de nuevo más tarde.,Se ha producido un error al cancelar la amistad con {RBX_NAME}. Inténtalo de nuevo más tarde.,Une erreur est survenue lors de la suppression de l'ami {RBX_NAME}. Veuillez réessayer plus tard.,Si è verificato un errore nel togliere l'amicizia a {RBX_NAME}. Riprova più tardi.,{RBX_NAME}を友達解除する際にエラーが発生しました。しばらくしてからやり直してください。,{RBX_NAME}님을 친구 취소하는 도중 오류 발생. 나중에 다시 시도하세요.,Um erro ocorreu ao cancelar amizade com {RBX_NAME}. Tente de novo mais tarde.,Um erro ocorreu ao cancelar amizade com {RBX_NAME}. Tente de novo mais tarde.,При удалении пользователя {RBX_NAME} из друзей произошла ошибка. Повторите попытку позже.,与“{RBX_NAME}”解除好友关系时出错。请稍候重试。,與{RBX_NAME}解除朋友關係時發生錯誤。請稍後再試。 +,,,Okay,Okay,,Aceptar,Aceptar,D'accord,OK,OK,확인,Ok,Ok,ОК,好,好 +,,,Send Request,Anfrage senden,,Enviar solicitud,Enviar solicitud,Envoyer demande,Invia richiesta,リクエスト送信,요청 전송,Enviar pedido,Enviar pedido,Отправить запрос,发送请求,傳送要求 +,,,You can not send a friend request because you are at the max friend limit.,"Du kannst keine Freundesanfrage senden, da du die max. Anzahl an Freunden erreicht hast.",,No puedes enviar una solicitud de amistad porque has alcanzado el límite máximo de amigos.,No puedes enviar una solicitud de amistad porque has alcanzado el límite máximo de amigos.,Vous ne pouvez pas envoyer une demande d'ami car vous avez atteint votre limite maximale d'amis.,Non puoi inviare una richiesta di amicizia perché hai raggiunto il limite massimo di amici.,最大友達数に達したため友達リクエストが送れません。,최대 친구 목록이 초과되어 친구 요청을 보낼 수 없어요.,Você não pode enviar um pedido de amizade pois você alcançou o limite máximo de amigos.,Você não pode enviar um pedido de amizade pois você alcançou o limite máximo de amigos.,Вы не можете отправить запрос дружбы: количество ваших друзей достигло максимума.,由于你已达到好友数量上限,你无法发送好友请求。,您無法寄送交友請求,原因是您的朋友已達上限。 +,,,Friend Limit Reached,Max. Anzahl an Freunden erreicht,,Has alcanzado el límite de amigos,Has alcanzado el límite de amigos,Limite d'amis atteinte,Limiti di amici raggiunto,友達,친구 한도 도달,Limite de amigos alcançado,Limite de amigos alcançado,Достигнуто максимальное количество друзей,已达好友数量上限,已達朋友上限 +,,,Decline,Ablehnen,,Rechazar,Rechazar,Décliner,Declina,拒否する,거부,Recusar,Recusar,Отклонить,拒绝,拒絕 +,,,Account: 13+,Konto: 13+,,Cuenta: 13+,Cuenta: 13+,Compte : 13+,Account: più di 13 anni,アカウント:13+,계정: 만 13세 이상,Conta: 13+,Conta: 13+,Учетная запись: 13+,帐户:13+,帳戶:13+ +,,,Send Game Invites,Spieleinladungen senden,,Enviar invitaciones al juego,Enviar invitaciones al juego,Envoyer des invitations,Manda inviti di gioco,ゲームへの招待を送る,게임 초대 전송,Enviar convites,Enviar convites,Отправить приглашения,发送游戏邀请,傳送遊戲邀請 +,,,Safe Zone,Sichere Zone,,Zona segura,Zona segura,Zone sécurisée,Zona sicura,セーフゾーン,안전 구역,Zona de segurança,Zona de segurança,Безопасная зона,安全区,安全區 +,,,Adjust,Anpassen,,Ajustar,Ajustar,Ajuster,Modifica,調節,조정,Ajustar,Ajustar,Настроить,调整,調整 +,,,Joining server,Verbindung zum Server wird hergestellt,,Uniéndose al servidor...,Uniéndose al servidor...,Connexion au serveur...,Connessione al server,サーバーに接続中,서버 가입,Entrando no servidor,Entrando no servidor,Подключение к серверу,正在加入服务器,加入伺服器 +,,,Account: <13,Konto: <13,,Cuenta: <13,Cuenta: <13,Compte : <13,Account: <13,アカウント: <13,계정: 만 13세 미만,Conta: <13,Conta: <13,Учетная запись: <13,帐户:<13,帳戶:<13 +,,,Default (Classic),Standard (Klassisch),,Predeterminado (clásico),Predeterminado (clásico),Par défaut (Classique),Predefinita (Classica),デフォルト (クラッシック),기본값 (클래식),Padrão (clássico),Padrão (clássico),По умолчанию (классика),默认(经典),預設 (古典) +,,,Accept Friend Request,Freundschaftsanfrage akzeptieren,,Aceptar solicitud de amistad,Aceptar solicitud de amistad,Accepter l'invitation d'un ami,Accetta la richiesta di amicizia,友人のリクエストを受け入れる,친구 요청 수락,Aceitar solicitação de amizade,Aceitar solicitação de amizade,Принять запрос друга,接受好友请求,接受交友請求 +,,,Decline Friend Request,Freundschaftsanfrage ablehnen,,Rechazar solicitud de amistad,Rechazar solicitud de amistad,Refuser la demande d'ami,Declinare la richiesta di un amico,フレンドリクエストを拒否する,친구 요청 거부,Rejeitar pedido de amizade,Rejeitar pedido de amizade,Отклонить запрос друга,拒绝好友请求,拒絕交友請求 +,,,Developer Console,Entwicklerkonsole,,Consola de desarrollo,Consola de desarrollo,Console de développement,Console sviluppatore,デベロッパーコンソール,개발자 콘솔,Console de desenvolvimento,Console de desenvolvimento,Консоль разработчика,开发人员控制台,開發人員主機 +,,,Open,Öffnen,,Abrir,Abrir,Ouvert(e),Apri,開く,열기,Abrir,Abrir,Открыть,打开,開啟 +,,,Revoke Friend Request,Widerruf der Freundschaftsanfrage,,Revocar solicitud de amigo,Revocar solicitud de amigo,Révoquer une demande d'ami,Revoca richiesta amico,フレンドリクエストを取り消す,친구 요청 취소,Revocar pedido de amigo,Revocar pedido de amigo,Отменить запрос друга,撤销好友请求,撤銷交友請求 +,,,Screenshot Taken,Screenshot gespeichert,,Captura tomada,Captura tomada,Capture d'écran prise,Screenshot catturato,撮影したスクリーンショット,스크린샷 캡쳐 완료,Captura de tela salva,Captura de tela salva,Снимок экрана сделан,已截取屏幕快照,已拍螢幕畫面 +,,,Check out your screenshots folder to see it.,Sieh ihn dir im Ordner mit den Screenshots an.,,Puedes verla en la carpeta de capturas de pantalla.,Puedes verla en la carpeta de capturas de pantalla.,Ouvrez le dossier des captures d'écran pour la visualiser.,Vai nella cartella screenshot per vederlo.,確認するにはスクリーンショットフォルダをチェック。,확인하려면 스크린샷 폴더로 이동하세요.,Confira sua pasta de capturas de tela para vê-la.,Confira sua pasta de capturas de tela para vê-la.,"Откройте папку со снимками экрана, чтобы его увидеть.",请检查你的屏幕快照文件夹以查看。,請查閱您的擷圖資料夾以觀看。 +,,,Open Folder,Ordner öffnen,,Abrir carpeta,Abrir carpeta,Ouvrir le dossier,Apri cartella,フォルダを開く,폴더 열기,Abrir pasta,Abrir pasta,Открыть папку,打开文件夹,開啟資料夾 +,,,Video Recorded,Video aufgezeichnet,,Vídeo grabado,Vídeo grabado,Vidéo enregistrée,Video registrato,録画したビデオ,녹화한 비디오,Vídeo gravado,Vídeo gravado,Видео записано,视频已录制,已錄製影片 +,,,Default (Keyboard),Standard (Tastatur),,Predeterminado (teclado),Predeterminado (teclado),Par défaut (Clavier),Predefinita (Tastiera),デフォルト (キーボード),기본값 (키보드),Padrão (teclado),Padrão (teclado),По умолчанию (клавиатура),默认(键盘),預設 (鍵盤) +,,,Check out your videos folder to see it.,Sieh es dir im Ordner mit den Videos an.,,Puedes verlo en la carpeta de vídeos.,Puedes verlo en la carpeta de vídeos.,Ouvrez le dossier des vidéos pour la visualiser.,Vai nella cartella video per vederlo.,確認するにはビデオフォルダをチェック。,확인하려면 비디오 폴더로 이동하세요.,Confira sua pasta de vídeos para vê-lo.,Confira sua pasta de vídeos para vê-lo.,"Откройте папку с видеозаписями, чтобы его увидеть.",检查你的视频文件夹以查看。,請查看影片資料夾以觀看。 +,,,Thanks for your report! Our moderators will review the chat logs and evaluate what happened.,"Danke für deine Meldung! Unsere Moderatoren werden sich die Chatprotokolle ansehen und überprüfen, was vorgefallen ist.",,Gracias por denunciarlo. Nuestros moderadores comprobarán el registro del chat para analizar lo sucedido.,Gracias por denunciarlo. Nuestros moderadores comprobarán el registro del chat para analizar lo sucedido.,Merci pour ce signalement ! Nos modérateurs vont examiner le journal de chat et évaluer la situation.,Grazie della segnalazione! I moderatori controlleranno i registri della chat ed esamineranno l'accaduto.,ご報告ありがとうございます! モデレーターがチャットログから状況を確認致します。,신고해주셔서 감사합니다! 중재자가 채팅 기록을 검토하고 발생한 사항을 조사할 거예요.,Obrigado por sua denúncia! Nossos moderadores revisarão o histórico de chat e avaliarão o ocorrido.,Obrigado por sua denúncia! Nossos moderadores revisarão o histórico de chat e avaliarão o ocorrido.,"Спасибо за сообщение! Наши модераторы просмотрят журнал чата, чтобы оценить ситуацию.",感谢你的举报!我们的审查员将审阅聊天记录并进行评估。,感謝您的舉報!我們的仲裁者會檢視聊天記錄,評估發生的情形。 +,,,Thanks for your report! We've recorded your report for evaluation.,Danke für deine Meldung! Sie wurde zur Auswertung gespeichert.,,Gracias por denunciarlo. Hemos guardado la denuncia para analizar lo sucedido.,Gracias por denunciarlo. Hemos guardado la denuncia para analizar lo sucedido.,Merci pour ce signalement ! Nous l'avons bien pris en compte et allons l'examiner.,Grazie della segnalazione! La tua segnalazione è stata registrata per essere esaminata.,ご報告ありがとうございます! レポートは評価のため記録されました。,신고해주셔서 감사합니다! 평가를 위해 신고를 기록해두었어요.,Obrigado por sua denúncia! Registramos a sua denúncia para que seja avaliada.,Obrigado por sua denúncia! Registramos a sua denúncia para que seja avaliada.,Спасибо за сообщение! Оно будет рассмотрено.,感谢你的举报!我们已记录你的举报信息以进行评估。,感謝您的舉報!我們已記錄您的舉報以便評估。 +,,,Thanks for your report! Our moderators will evaluate the username.,Danke für deine Meldung! Unsere Moderatoren werden den Benutzernamen überprüfen.,,Gracias por denunciarlo. Nuestros moderadores analizarán el nombre de usuario.,Gracias por denunciarlo. Nuestros moderadores analizarán el nombre de usuario.,Merci pour ce signalement ! Nos modérateurs vont examiner le nom de cet utilisateur.,Grazie della segnalazione! I moderatori esamineranno il nome utente.,ご報告ありがとうございます! モデレーターが該当ユーザー名を確認致します。,신고해주셔서 감사합니다! 중재자가 사용자 이름을 조사할 거예요.,Obrigado por sua denúncia! Nossos moderadores avaliarão o nome de usuário.,Obrigado por sua denúncia! Nossos moderadores avaliarão o nome de usuário.,Спасибо за сообщение! Наши модераторы изучат поведение этого пользователя.,感谢你的举报!我们的审查员将对用户名进行评估。,感謝您的舉報!我們的仲裁者會評估此使用者名稱。 +,,,Thanks for your report! Our moderators will review the place and make a determination.,Danke für deine Meldung! Unsere Moderatoren werden sich den Ort ansehen und eine Entscheidung treffen.,,Gracias por denunciarlo. Nuestros moderadores analizarán el juego y tomarán una decisión.,Gracias por denunciarlo. Nuestros moderadores analizarán el juego y tomarán una decisión.,Merci pour ce signalement ! Nos modérateurs vont examiner le jeu avant de prendre une décision.,Grazie della segnalazione! I moderatori esamineranno il gioco e prenderanno una decisione.,ご報告ありがとうございます! モデレーターが場所を確認のうえ対応致します。,신고해주셔서 감사합니다! 중재자가 장소를 검토하고 결정을 내릴 거예요.,Obrigado por sua denúncia! Nossos moderadores verificarão o jogo e irão tomar uma decisão.,Obrigado por sua denúncia! Nossos moderadores verificarão o jogo e irão tomar uma decisão.,Спасибо за сообщение! Наши модераторы изучат это место и примут решение.,感谢你的举报!我们的审查员将查看该地点并进行评估。,感謝您的舉報!我們的仲裁者會檢視此地點並作出裁定。 +,,,Canceling...,Abbrechen ...,,Cancelando...,Cancelando...,Annulation...,Annullamento...,キャンセル中...,취소 중...,Cancelando...,Cancelando...,Отмена...,正在取消...,取消中... +,,,Memory,Speicher,,Memoria,Memoria,Mémoire,Memoria,メモリー,메모리,Memória,Memória,Память,内存,記憶體 +,,,Physics,Physik,,Física,Física,Physique,Fisica,物理エンジン,물리,Física,Física,Физика,物理,物理 +,,,Dev Console,Entwicklerkonsole,,Consola de desarrollo,Consola de desarrollo,Console de dév.,Console svilup.,Dev コンソール,개발자 콘솔,Console de dev.,Console de dev.,Консоль разработчика,开发控制台,開發主機 +,,,Camera Inverted,Kamera invertiert,,Cámara invertida,Cámara invertida,Caméra inversée,Macchina fotografica invertita,カメラの反転,카메라 반전,Câmera Invertida,Câmera Invertida,Перевернутая камера,镜头反转,攝影機反轉 +,,,Don't Leave,Nicht verlassen,,No salir,No salir,Ne pas quitter,Non uscire,やめない,나가지 않기,Não sair,Não sair,Не выходить,不要离开,勿離開 +,,,Don't Reset,Nicht zurücksetzen,,No reiniciar,No reiniciar,Ne pas réinitialiser,Non azzerare,リセットしない,재설정 안 하기,Não reiniciar,Não reiniciar,Не сбрасывать,不要重置,勿重設 +,,,Drop Tool,Werkzeug ablegen,,Soltar herramienta,Soltar herramienta,Lâcher outil,Lascia strumento,ドロップツール,도구 드롭,Largar,Largar,Выбросить инструм.,丢弃工具,投放工具 +,,,ESC,ESC,,ESC,ESC,ÉCHAP,ESC,ESC,ESC,ESC,ESC,ESC,ESC,ESC 鍵 +,,,Equip Tools,Werkzeuge aktivieren,,Equipar herramientas,Equipar herramientas,Prendre outils,Equipaggia strum.,ツールを装備する,도구 장착,Equipar,Equipar,Назначить инструм.,配备工具,配備工具 +,,,Follow,Folgen,,Seguir,Seguir,Suivre,Segui,フォロー,팔로우,Seguir,Seguir,Слежение,跟随,追蹤鏡頭 +,,,Fullscreen,Vollbild,,Pantalla completa,Pantalla completa,Plein écran,Schermo intero,フルスクリーン,전체 화면,Tela cheia,Tela cheia,Полный экран,全屏,全螢幕 +,,,GPU,GPU,,GPU,GPU,Processeur Graphique,GPU,GPU,GPU,GPU,GPU,Графический процессор,GPU,圖形處理器 (GPU) +,,,Game,Spiel,,Juego,Juego,Jeu,Gioco,ゲーム,게임,Jogo,Jogo,Игра,游戏,遊戲 +,,,Game or Player?,Spiel oder Spieler?,,¿Juego o jugador?,¿Juego o jugador?,Jeu ou joueur ?,Gioco o giocatore?,ゲームですかプレーヤーですか?,게임 혹은 플레이어?,Jogo ou jogador?,Jogo ou jogador?,Игра или игрок?,游戏或玩家?,遊戲或玩家? +,,,Goodbye!,Tschüss!,,¡Hasta luego!,¡Hasta luego!,Au revoir !,Addio!,さようなら!,안녕히 가세요!,Tchau!,Tchau!,До свидания!,再见!,再會! +,,,Graphics Level,Grafikstufe,,Nivel de gráficos,Nivel de gráficos,Niveau graphismes,Livello grafica,グラフィックレベル,그래픽 수준,Nível dos gráficos,Nível dos gráficos,Уровень графики,图形级别,圖形層級 +,,,Graphics Mode,Grafikmodus,,Modo de gráficos,Modo de gráficos,Mode graphismes,Modalità grafica,グラフィックモード,그래픽 모드,Modo de gráficos,Modo de gráficos,Режим графики,图形模式,圖形模式 +,,,Graphics Quality,Grafikqualität,,Calidad gráfica,Calidad gráfica,Qualité graphismes,Qualità grafica,グラフィック品質,그래픽 품질,Qualidade dos gráficos,Qualidade dos gráficos,Качество графики,图形画质,圖形畫質 +,,,Help,Hilfe,,Ayuda,Ayuda,Aide,Guida,ヘルプ,도움말,Ajuda,Ajuda,Справка,帮助,說明 +,,,Inappropriate Content,Unangemessener Inhalt,,Contenido inadecuado,Contenido inadecuado,Contenu inapproprié,Contenuto non appropriato,不適切なコンテンツ,부적절한 콘텐츠,Conteúdo inapropriado,Conteúdo inapropriado,Недопустимое содержимое,内容不当,內容不當 +,,,Inappropriate Username,Unangemessener Benutzername,,Nombre de usuario inadecuado,Nombre de usuario inadecuado,Nom d'utilisateur inapproprié,Nome utente non appropriato,不適切なユーザ名,부적절한 사용자 이름,Nome de usuário inapropriado,Nome de usuário inapropriado,Недопустимое имя пользователя,用户名不当,使用者名稱不當 +,,,Jump,Springen,,Saltar,Saltar,Sauter,Salta,ジャンプ,점프,Pular,Pular,Прыжок,跳跃,跳起 +,,,KOs,K.o.,,N.º de KO,N.º de KO,Ko,KO,KO回数,KO,Nocautes,Nocautes,Нокауты,击倒数,擊倒數 +,,,Keyboard + Mouse,Tastatur + Maus,,Teclado + ratón,Teclado + ratón,Clavier + souris,Tastiera e mouse,キーボード+ マウス,키보드 + 마우스,Teclado + mouse,Teclado + mouse,Клавиатура + мышь,键盘+鼠标,鍵盤 + 滑鼠 +,,,"1,2,3...","1,2,3...",,"1,2,3...","1,2,3...","1, 2, 3...","1, 2, 3...","1,2,3...","1,2,3...","1,2,3...","1,2,3...","1,2,3...","1,2,3...",1、2、3...... +,,,Label,Label,,Etiqueta,Etiqueta,Étiquette,Etichetta,ラベル,레이블,Rótulo,Rótulo,Метка,标签,標籤 +,,,Leave,Verlassen,,Salir,Salir,Quitter,Esci,やめる,나가기,Sair,Sair,Выйти,离开,離開 +,,,Leave Game,Spiel verlassen,,Salir del juego,Salir del juego,Quitter le jeu,Esci dal gioco,ゲームをやめる,게임 종료,Sair do jogo,Sair do jogo,Выйти из игры,离开游戏,離開遊戲 +,,,Left Mouse Button,Linke Maustaste,,Botón izquierdo del ratón,Botón izquierdo del ratón,Bouton gauche de la souris,Sinistro/Mouse,左マウスボタン,마우스 왼쪽 버튼,Mouse (esquerdo),Mouse (esquerdo),ЛКМ,鼠标左键,左滑鼠鍵 +,,,Loading,Laden,,Cargando,Cargando,En cours,Caricamento,ロード中,로드 중,Carregando,Carregando,Загрузка,正在载入,正在載入 +,,,Loading.,Laden.,,Cargando.,Cargando.,En cours.,Caricamento.,ロード中.,로드 중.,Carregando.,Carregando.,Загрузка,正在载入。,正在載入. +,,,Loading..,Laden ...,,Cargando..,Cargando..,En cours..,Caricamento..,ロード中..,로드 중..,Carregando..,Carregando..,Загрузка,正在载入..,正在載入.. +,,,Loading...,Laden ...,,Cargando...,Cargando...,En cours...,Caricam...,ロード中...,로드 중...,Carregando...,Carregando...,Загрузка,正在载入...,正在載入... +,,,Manual,Manuell,,Manual,Manual,Manuel,Manuale,マニュアル,수동,Manual,Manual,Вручную,手动,手動 +,,,Mem,Speicher,,Mem.,Mem.,Mém,Mem,メモリー,메모리,Mem.,Mem.,Память,内存,記憶體 +,,,A/Left Arrow,A/Linkspfeil,,A/Cursor izquierdo,A/Cursor izquierdo,A/Flèche gauche,A/Freccia SINISTRA,A/左カーソル,A/왼쪽 화살표,A/seta esquerda,A/seta esquerda,A/стрелка влево,A/左箭头,A 鍵 / 向左箭號 +,,,Menu Items,Menüobjekte,,Objetos del menú,Objetos del menú,Objets du menu,Oggetti menu,メニューアイテム,메뉴 아이템,Itens de menu,Itens de menu,Пункты меню,菜单项目,選單項目 +,,,Misc,Verschiedenes,,Misc.,Misc.,Divers,Vari,その他,기타,Misc.,Diversos,Разное,杂项,雜項 +,,,Mouse Sensitivity,Mausempfindlichkeit,,Sensibilidad del ratón,Sensibilidad del ratón,Sensibilité de la souris,Precisione mouse,マウス感度,마우스 감도,Sensibilidade do mouse,Sensibilidade do mouse,Чувствительность мыши,鼠标灵敏度,滑鼠靈敏度 +,,,Mouse Wheel,Mausrad,,Rueda del ratón,Rueda del ratón,Molette de la souris,Rotellina mouse,マウスホイール,마우스 휠,Roda do mouse,Roda do mouse,Колесо мыши,鼠标滚轮,滑鼠滾輪 +,,,Mouselock,Maussperre,,Bloqueo del ratón,Bloqueo del ratón,Verrouillage souris,Blocco mouse,マウスロック,마우스 잠금,Travar mouse,Travar mouse,Фиксация мыши,鼠标锁定,滑鼠鎖定 +,,,Move Backward,Rückwärts bewegen,,Moverse hacia atrás,Moverse hacia atrás,Reculer,Muovi indietro,後ろに移動,뒤로 이동,Mover (trás),Mover (trás),Назад,后退,倒退 +,,,Move Forward,Vorwärts bewegen,,Moverse hacia delante,Moverse hacia delante,Avancer,Muovi in avanti,前に移動,앞으로 이동,Mover (frente),Mover (frente),Вперед,向前,往前 +,,,Move Left,Nach links bewegen,,Moverse a la izquierda,Moverse a la izquierda,Aller à gauche,Muovi a sinistra,左に移動,왼쪽으로 이동,Mover (esquerda),Mover (esquerda),Влево,左移,左移 +,,,Move Right,Nach rechts bewegen,,Moverse a la derecha,Moverse a la derecha,Aller à droite,Muovi a destra,右に移動,오른쪽으로 이동,Mover (direita),Mover (direita),Вправо,右移,右移 +,,,Movement Mode,Bewegungsmodus,,Modo de movimiento,Modo de movimiento,Mode déplacement,Modalità movimento,動作モード,이동 모드,Modo de movimento,Modo de movimento,Режим передвижения,移动模式,移動模式 +,,,Accept,Annehmen,,Aceptar,Aceptar,Accepter,Accetta,同意する,수락,Aceitar,Aceitar,Принять,接受,接受 +,,,OK,Okay,,Aceptar,Aceptar,Ok,OK,OK,확인,OK,OK,OK,好,確定 +,,,Off,Aus,,Desactivado,Desactivado,Arrêt,Disattivato,Off,끔,Desligado,Desligado,Выкл.,关闭,關閉 +,,,Offsite Links,Externe Links,,Enlaces externos,Enlaces externos,Liens hors site,Collegamenti ad altri siti,外部リンク,외부 링크,Links externos,Links externos,Внешние ссылки,离线链接,異地連結 +,,,On,An,,Activado,Activado,Marche,Attivato,On,켬,Ligado,Ligado,Вкл.,开启,開啟 +,,,Perf. Stats,Leistungsw.,,Est. de rend.,Est. de rend.,Stats perf.,Stat. prest.,パフォーマンス状態,성능 상태,Estat. Des.,Estat. Des.,Показатели произ.,表现统计,表現統計 +,,,Performance Stats,Leistungswerte,,Estadísticas de rendimiento,Estadísticas de rendimiento,Stats de performance,Statistiche prestazioni,パフォーマンス状態,성능 상태,Estat. de desempenho,Estat. de desempenho,Показатели производительности,表现统计,表現統計 +,,,Personal Question,Persönliche Frage,,Pregunta personal,Pregunta personal,Question personnelle,Domanda personale,個人的な質問,개인 질문,Pergunta pessoal,Pergunta pessoal,Личный вопрос,私人问题,個人問題 +,,,Phys,Phys.,,Fís.,Fís.,Phys.,Fisico,物理,물리,Fís.,Fís.,Физ.,物理,物理 +,,,Player,Spieler,,Jugador,Jugador,Joueur,Giocatore,プレーヤー,플레이어,Jogador,Jogador,Игрок,玩家,玩家 +,,,Playerlist,Spielerliste,,Jugadores,Jugadores,Liste de joueurs,Lista giocatori,プレーヤーリスト,플레이어 목록,Jogadores,Jogadores,Список игроков,玩家名单,玩家名單 +,,,Accessories,Accessoires,,Accesorios,Accesorios,Accessoires,Accessori,アクセサリ,액세서리,Acessórios,Acessórios,Аксессуары,配饰,配件 +,,,Players,Spieler,,Jugadores,Jugadores,Joueurs,Giocatori,プレーヤー,플레이어,Jogadores,Jogadores,Игроки,玩家,玩家 +,,,Print Screen,Drucktaste,,Imprimir pantalla,Imprimir pantalla,Impression écran,STAMP,画面プリント,화면 캡쳐,Print screen,Captura de Tela,Print Screen,屏幕截图,PrtSc 鍵 +,,,Purchasing,Wird gekauft ...,,Compras,Compras,Achat,Acquisto,購入,구매 중,Comprando,Comprando,Покупка,正在购买,正在購買 +,,,Record,Aufnehmen,,Grabar,Grabar,Enregistrer,Registra,録画,녹화,Gravar,Gravar,Запись,录制,錄影 +,,,Record Video,Video aufnehmen,,Grabar vídeo,Grabar vídeo,Enregistrer vidéo,Registra video,ビデオ録画,비디오 녹화,Gravar vídeo,Gravar vídeo,Запись видео,录制视频,錄製影片 +,,,Recv,Empfangen,,Recib.,Recib.,Reçu,Ricev.,受信,받음,Receb.,Receb.,Получ.,收到,收到 +,,,Report,Melden,,Denunciar,Denunciar,Signaler,Segnala,レポート,신고,Denunciar,Denunciar,Сообщить,举报,舉報 +,,,Requesting Server...,Serveranfrage ...,,Solicitando servidor...,Solicitando servidor...,Demande serveur...,Richiesta server...,サーバーを要求...,서버 요청 중...,Solicitando servidor...,Solicitando servidor...,Запрос сервера...,正在向服务器发送请求...,正在要求伺服器... +,,,Reset,Zurücksetzen,,Reiniciar,Reiniciar,Réinitialiser,Azzera,リセット,재설정,Reiniciar,Reiniciar,Сброс,重置,重設 +,,,Resume Game,Spiel fortsetzen,,Seguir jugando,Seguir jugando,Reprendre le jeu,Riprendi gioco,ゲームを再開,게임 계속 진행,Continuar jogo,Continuar jogo,Возобновить,继续游戏,繼續遊戲 +,,,Stop Recording,Aufnahme beenden,,Detener grabación,Detener grabación,Arrêter l'enregistrement,Interrompi registrazione,録画を停止する,녹화 중지,Parar gravação,Parar gravação,Остановить запись,停止录音,停止錄製 +,,,Camera Sensitivity,Kameraempfindlichkeit,,Sensibilidad de la cámara,Sensibilidad de la cámara,Sensibilité de la caméra,Precisione telecamera,カメラ感度,카메라 민감도,Sensitividade da câmera,Sensitividade da câmera,Чувствительность камеры,相机敏感度,鏡頭靈敏度 +,,,Dynamic Thumbstick,Dynamischer Thumbstick,,Stick dinámico,Stick dinámico,Joystick dynamique,Levetta dinamica,ダイナミックサムスティック,다이내믹 엄지스틱,Thumbstick Dinâmico,Thumbstick Dinâmico,Динамический аналоговый стик,动态摇杆,動態拇指搖桿 +,,,"Developer has shut down all game servers or game server has shut down for other reasons, please reconnect",Der Entwickler hat alle Spielserver abgeschaltet oder die Verbindung zum Spielserver wurde aus anderen Gründen unterbrochen. Bitte erneut verbinden.,,El desarrollador ha cerrado todos los servidores del juego o el servidor del juego se ha cerrado por otras razones. Conéctate de nuevo.,El desarrollador ha cerrado todos los servidores del juego o el servidor del juego se ha cerrado por otras razones. Conéctate de nuevo.,"Le développeur a arrêté tous les serveurs du jeu, ou le serveur s'est arrêté pour d'autres raisons. Veuillez vous reconnecter.",Gli sviluppatori hanno chiuso tutti i server di gioco o il server si è chiuso per altri motivi. È necessario riconnettersi,ディベロッパーがすべてのゲームサーバーをシャットダウンしたか、他の理由でをシャットダウンしたようです。再接続してください,"개발자가 모든 게임 서버를 닫았거나, 다른 이유로 인해 게임 서버가 닫혔어요. 다시 연결하세요.",O desenvolvedor encerrou todos os servidores do jogo ou o servidor do jogo foi encerrado por outros motivos. Conecte-se novamente.,O desenvolvedor encerrou todos os servidores do jogo ou o servidor do jogo foi encerrado por outros motivos. Conecte-se novamente.,"Разработчик отключил все сервера игры или они не работают по какой-либо другой причине. Пожалуйста, установите соединение повторно.",开发人员已经关闭所有游戏服务器,或由于其他原因服务器已关闭,请重新连接,開發商已關閉所有遊戲伺服器,或因其他理由而關閉,請重新連接 +,,,Disconnected due to a bad hash,Verbindung aufgrund eines fehlerhaften Hashs unterbrochen,,Desconexión por un error de hash,Desconexión por un error de hash,Vous avez été déconnecté(e) en raison d'un mauvais hachage.,Disconnessione a causa di un cattivo funzionamento dell'hash,ハッシュが正しくないためコネクションが切断されました,잘못된 해시로 인해 연결 끊어짐,Desconexão devido a um erro de hash,Desconexão devido a um erro de hash,Соединение прервано из-за ошибок хеширования.,由于错误的哈希码,连接中断,因雜湊值不佳而斷線 +,,,Disconnected due to Security Key Mismatch,Verbindung aufgrund einer Sicherheitsschlüssel-Diskrepanz unterbrochen,,Desconexión por incompatibilidad de la clave de seguridad,Desconexión por incompatibilidad de la clave de seguridad,"Vous avez été déconnecté(e), car la clé de sécurité ne correspond pas.",Disconnessione a causa di una mancata corrispondenza della chiave di sicurezza,セキュリティキーの不一致によりコネクションが切断されました,보안 키 불일치로 인해 연결 끊어짐,Desconexão devido à incompatibilidade da Chave de Segurança,Desconexão devido à incompatibilidade da Chave de Segurança,Соединение прервано из-за несовпадения ключей безопасности.,由于安全密钥不匹配,连接中断,因安全性金鑰不符而斷線 +,,,Disconnected due to Security Key Mismatch,Verbindung aufgrund einer Sicherheitsschlüssel-Diskrepanz unterbrochen,,Desconexión por incompatibilidad de la clave de seguridad,Desconexión por incompatibilidad de la clave de seguridad,"Vous avez été déconnecté(e), car la clé de sécurité ne correspond pas.",Disconnessione a causa di una mancata corrispondenza della chiave di sicurezza,セキュリティキーの不一致によりコネクションが切断されました,보안 키 불일치로 인해 연결 끊어짐,Desconexão devido à incompatibilidade da Chave de Segurança,Desconexão devido à incompatibilidade da Chave de Segurança,Соединение прервано из-за несовпадения ключей безопасности.,由于安全密钥不匹配,连接中断,因安全性金鑰不符而斷線 +,,,"Protocol mismatch, please reconnect",Protokoll-Diskrepanz. Bitte erneut verbinden.,,Incompatibilidad del protocolo. Conéctate de nuevo.,Incompatibilidad del protocolo. Conéctate de nuevo.,"Le protocole ne correspond pas, veuillez vous reconnecter.",Mancata corrispondenza del protocollo; è necessario riconnettersi,プロトコルの不一致です。再接続してください,프로토콜이 일치하지 않아요. 다시 연결하세요.,Incompatibilidade do Protocolo. Conecte-se novamente.,Incompatibilidade do Protocolo. Conecte-se novamente.,"Несовпадение протокола, пожалуйста, установите соединение повторно.",协议不匹配,请重新连接,通訊協定不符,請重新連接 +,,,"Error while receiving data, please reconnect",Fehler beim Datenempfang. Bitte erneut verbinden,,Error al recibir los datos. Conéctate de nuevo.,Error al recibir los datos. Conéctate de nuevo.,"Erreur lors de la réception des données, veuillez vous reconnecter.",Errore di ricezione dei dati; è necessario riconnettersi,データを受信中にエラーが発生しました。再接続してください,데이터 수신 중 오류가 발생했어요. 다시 연결하세요.,Erro ao receber os dados. Conecte-se novamente.,Erro ao receber os dados. Conecte-se novamente.,"Ошибка при получении данных, пожалуйста, установите соединение повторно.",接收数据时出错,请重新连接,接收資料時發生錯誤,請重新連接 +,,,"Error while streaming data, please reconnect",Fehler bei der Datenübertragung. Bitte erneut verbinden.,,Error al transmitir los datos. Conéctate de nuevo.,Error al transmitir los datos. Conéctate de nuevo.,"Erreur lors de la diffusion des données, veuillez vous reconnecter.",Errore durante lo streaming dei dati; è necessario riconnettersi,データをストリーミング中にエラーが発生しました。再接続してください,데이터 스트리밍 중 오류가 발생했어요. 다시 연결하세요.,Erro ao transmitir os dados. Conecte-se novamente.,Erro ao transmitir os dados. Conecte-se novamente.,"Ошибка при передаче данных, пожалуйста, установите соединение повторно.",流式处理数据时出错,请重新连接,串流資料時發生錯誤,請重新連接 +,,,"Error while sending data, please reconnect",Fehler beim Senden der Daten. Bitte erneut verbinden.,,Error al enviar los datos. Conéctate de nuevo.,Error al enviar los datos. Conéctate de nuevo.,"Erreur lors de l'envoi des données, veuillez vous reconnecter.",Errore durante l'invio dei dati; è necessario riconnettersi,データを送信中にエラーが発生しました。再接続してください,데이터 전송 중 오류가 발생했어요. 다시 연결하세요.,Erro ao enviar os dados. Conecte-se novamente.,Erro ao enviar os dados. Conecte-se novamente.,"Ошибка при отправке данных, пожалуйста, установите соединение повторно.",发送数据时出错,请重新连接,傳送資料時發生錯誤,請重新連接 +,,,Invalid teleport destination,Ungültiger Teleport-Bestimmungsort,,Destino del teleport no válido,Destino del teleport no válido,Destination de téléportation non valide.,Destinazione del teletrasporto non valida,テレポートの移動先が無効です,잘못된 텔레포트 목적지,Destino de teleport inválido,Destino de teleport inválido,Некорректное назначение телепортирования.,传送目的地无效,無效的傳輸目的地 +,,,You are already playing a game. Please shut down the other game and try again,Du spielst bereits ein Spiel. Bitte brich das andere Spiel ab und versuche es erneut.,,Ya estás jugando a un juego. Cierra el otro juego e inténtalo de nuevo.,Ya estás jugando a un juego. Cierra el otro juego e inténtalo de nuevo.,"Vous êtes déjà dans un jeu. Veuillez le quitter, puis réessayez.",Stai già giocando a un gioco. Chiudi l'altro gioco e riprova,既にゲームをプレイ中です。ゲームを終了してもう一度試してください,게임을 플레이 중에요. 다른 게임을 닫고 다시 시도하세요.,Você já está jogando um jogo. Saia do jogo atual e tente novamente.,Você já está jogando um jogo. Saia do jogo atual e tente novamente.,"У вас уже запущена игра. Пожалуйста, закройте ее и попробуйте еще раз.",你已经在游戏中。请关闭其他游戏并重试,您已經在一場遊戲中。請關閉那場遊戲,然後再試一次 +,,,"Error processing ticket, please reconnect",Fehler bei der Ticketbearbeitung. Bitte erneut verbinden.,,Error al procesar un tique. Conéctate de nuevo.,Error al procesar un tique. Conéctate de nuevo.,"Erreur lors du traitement du ticket, veuillez vous reconnecter.",Errore di elaborazione della richiesta; è necessario riconnettersi,チケットの処理中にエラーが発生しました。再接続してください,티켓 처리 중 오류가 발생했어요. 다시 연결하세요.,Erro ao processar o tíquete. Conecte-se novamente.,Erro ao processar o tíquete. Conecte-se novamente.,"Сообщение об ошибке процесса, пожалуйста, установите соединение повторно.",处理票单时出错,请重新连接,處理票券時發生錯誤,請重新連接 +,,,Lost connection to server due to timeout,Die Verbindung zum Server wurde aufgrund einer Zeitüberschreitung unterbrochen.,,Se ha perdido la conexión con el servidor por agotamiento del tiempo de espera,Se ha perdido la conexión con el servidor por agotamiento del tiempo de espera,Connexion au serveur perdue ; la session a expiré.,Connessione persa con il server per un errore di time-out,タイムアウトでサーバーへの接続が失われました,시간 초과로 인해 연결 해제됨,A conexão com o servidor foi perdida pois o limite de tempo de espera foi atingido,A conexão com o servidor foi perdida pois o limite de tempo de espera foi atingido,Разрыв соединения с сервером из-за превышения времени ожидания.,由于超时,与服务器的连接已断开,因連線逾時,與伺服器失去連線 +,,,You have been kicked from the game,Du wurdest aus dem Spiel geworfen,,Se te ha expulsado del juego,Se te ha expulsado del juego,Vous avez été expulsé(e) du jeu.,Sei stato espulso dal gioco,ゲームからキックされました,게임에서 추방되었어요.,Você foi expulso do jogo,Você foi expulso do jogo,Вас исключили из игры.,你已被游戏踢出,您已被遊戲踢出 +,,,Kicked by server. Please close and rejoin another game,Vom Sever rausgeworfen. Bitte schließen und einem anderen Spiel beitreten.,,Expulsado del servidor. Cierra el juego y únete a otro.,Expulsado del servidor. Cierra el juego y únete a otro.,Le serveur vous a expulsé(e). Veuillez quitter le jeu et rejoindre une autre partie.,Espulso dal server. Chiudi e ricomincia una nuova partita,ゲームからキックされました。終了して別のゲームに参加してください,서버에서 추방되었어요. 게임을 닫은 후 다른 게임에 다시 참여하세요.,Você foi expulso pelo servidor. Feche a tela e entre em outro jogo.,Você foi expulso pelo servidor. Feche a tela e entre em outro jogo.,"Вы были исключены сервером. Пожалуйста, закройте игру и зайдите снова.",已被服务器踢出。请关闭并重新加入其他游戏,被伺服器踢出。請關閉並重新加入另一場遊戲 +,,,"Disconnected due to timeout, please reconnect",Aufgrund einer Zeitüberschreitung unterbrochen. Bitte erneut verbinden.,,Desconexión por agotamiento del tiempo de espera. Conéctate de nuevo.,Desconexión por agotamiento del tiempo de espera. Conéctate de nuevo.,"Vous avez été déconnecté(e), car votre session a expiré. Veuillez vous reconnecter.",Disconnessione a causa del time-out; è necessario riconnettersi,タイムアウトにより接続が失われました。再接続してください,시간 초과로 인해 연결이 끊어졌어요. 다시 연결하세요.,Desconexão devido ao limite de tempo de espera ter sido atingido. Conecte-se novamente.,Desconexão devido ao limite de tempo de espera ter sido atingido. Conecte-se novamente.,"Разрыв соединения из-за превышения времени ожидания, пожалуйста, установите соединение повторно.",由于超时,连接已断开。请重新连接,因連線逾時而斷線,請重新連接 +,,,"You have been disconnection from Team Create, please reconnect",Verbindung zu Team Create wurde unterbrochen. Bitte erneut verbinden.,,Te has desconectado del Team Create. Conéctate de nuevo.,Te has desconectado del Team Create. Conéctate de nuevo.,Vous avez été déconnecté(e) de la création d'équipe. Veuillez vous reconnecter.,Sei stato disconnesso dalla creazione della squadra; è necessario riconnettersi,Team Createとの接続が切断されました。再接続してください,Team Create 연결이 끊어졌어요. 다시 연결하세요.,Você foi desconectado do Team Create. Conecte-se novamente.,Você foi desconectado do Team Create. Conecte-se novamente.,"Соединение с командой создателей было прервано, пожалуйста, установите соединение повторно.",你已断开与团队开发的连接,请重新连接,您已從「建立隊伍」斷線,請重新連接 +,,,Server was shutdown due to no active players,"Der Server wurde abgeschaltet, weil keine aktiven Spieler vorhanden waren",,Se ha cerrado el servidor por inactividad de los jugadores,Se ha cerrado el servidor por inactividad de los jugadores,"Le serveur a été fermé, car aucun joueur n'était actif.",Il server è stato chiuso per mancanza di giocatori attivi,アクティブなプレイヤーがいないため、サーバーはシャットダウンされました,활성화된 플레이어가 없어 서버가 닫힘,O servidor foi encerrado pois não há jogadores ativos,O servidor foi encerrado pois não há jogadores ativos,Сервер был отключен из-за отсутствия игроков.,由于没有活跃玩家,服务器关闭,因無玩家上線,伺服器已關閉 +,,,"Disconnected from game, possibly due to game joined from another device",Verbindung zum Spiel wurde unterbochen. Möglicher Grund: Spielbeitritt von einem anderen Gerät.,,"Desconectado del juego, posiblemente por conectarse al juego desde otro dispositivo.","Desconectado del juego, posiblemente por conectarse al juego desde otro dispositivo.",Vous avez été déconnecté(e) du jeu ; il se peut que vous ayez rejoint le jeu depuis un autre appareil.,Sei stato disconnesso dal gioco forse perché hai giocato una partita con un altro dispositivo,他のデバイスから参加したゲームのために、ゲームへの接続から切断された可能性があります,게임 연결이 끊어졌어요. 다른 장치에서 게임에 참여했을 가능성이 있어요.,"Você foi desconectado do jogo, provavelmente porque entrou em um jogo através de outro dispositivo","Você foi desconectado do jogo, provavelmente porque entrou em um jogo através de outro dispositivo","Соединение с игрой прервано, возможно, из-за подключения к игре с другого устройства.",已断开与游戏的连接,可能是由于该游戏同时从另一个设备加入,已從遊戲斷線,可能是因從另一裝置加入遊戲而導致的 +,,,Developer has shut down this game server for maintenance,Der Entwickler hat diesen Spielserver aufgrund von Wartungsarbeiten abgeschaltet,,El desarrollador ha cerrado el servidor de este juego por mantenimiento,El desarrollador ha cerrado el servidor de este juego por mantenimiento,Le développeur a arrêté ce serveur de jeu pour effectuer la maintenance.,Lo sviluppatore ha chiuso questo server di gioco per manutenzione,メンテナンスのため、ディベロッパーによってゲームサーバーがシャットダウンされています。,개발자가 점검을 위해 이 게임 서버를 닫았어요.,O servidor deste jogo foi encerrado pelo desenvolvedor para manutenção,O servidor deste jogo foi encerrado pelo desenvolvedor para manutenção,Разработчик отключил игровой сервер для техобслуживания.,维护期间,开发人员已关闭此游戏服务器,為進行維護,開發商已關閉此遊戲伺服器 +,,,Roblox has shut down this game server for maintenance,Roblox hat diesen Spielserver aufgrund von Wartungsarbeiten abgeschaltet,,Roblox ha cerrado el servidor de este juego por mantenimiento,Roblox ha cerrado el servidor de este juego por mantenimiento,Roblox a arrêté ce serveur de jeu pour effectuer la maintenance.,Roblox ha chiuso questo server di gioco per manutenzione,メンテナンスのため、Roblox によってゲームサーバーがシャットダウンされています。,Roblox가 점검을 위해 이 게임 서버를 닫았어요.,O servidor deste jogo foi encerrado pelo Roblox para manutenção,O servidor deste jogo foi encerrado pelo Roblox para manutenção,Roblox отключил игровой сервер для техобслуживания.,维护期间,Roblox 已关闭此游戏服务器,為進行維護,Roblox已關閉此遊戲伺服器 +,,,This game has been disconnected because you have joined a game from another device,"Dieses Spiel wurde unterbrochen, weil du einem Spiel von einem anderen Gerät beigetreten bist",,Se ha desconectado este juego porque te has unido a un juego desde otro dispositivo,Se ha desconectado este juego porque te has unido a un juego desde otro dispositivo,"Vous avez été déconnecté(e) de ce jeu, car vous en avez rejoint un autre depuis un appareil différent.",Questo gioco è stato disconnesso perché hai giocato una partita con un altro dispositivo,別のデバイスからゲームに参加したため、このゲームへの接続が切断されました,다른 장치에서 게임에 참여하여 이 게임의 연결이 끊어졌어요.,Este jogo foi desconectado pois você entrou em um jogo através de outro dispositivo,Este jogo foi desconectado pois você entrou em um jogo através de outro dispositivo,"Соединение с этой игрой было прервано, так как вы стали играть с другого устройства.",由于你从另一设备加入游戏,此游戏已断开连接,因您從另一裝置加入了一場遊戲,因此此遊戲已斷線 +,,,"Disconnected from game, please reconnect",Verbindung zum Spiel unterbrochen. Bitte erneut verbinden.,,Desconectado del juego. Conéctate de nuevo.,Desconectado del juego. Conéctate de nuevo.,Vous avez été déconnecté(e) du jeu. Veuillez vous reconnecter.,Disconnesso dal gioco; è necessario riconnettersi,ゲームへの接続が切断されました。再接続してください,게임 연결이 끊어졌어요. 다시 연결하세요.,Você foi desconectado do jogo. Conecte-se novamente.,Você foi desconectado do jogo. Conecte-se novamente.,"Соединение и игрой прервано, пожалуйста, установите соединение повторно.",与游戏连接已断开,请重新连接,已從遊戲斷線,請重新連接 +,,,Waiting for an available server,Warten auf verfügbaren Server,,Esperando un servidor disponible,Esperando un servidor disponible,En attente d'un serveur disponible,In attesa di un server disponibile,使用可能なサーバを待機中,이용 가능한 서버를 기다리는 중,Aguardando um servidor disponível,Aguardando um servidor disponível,Ожидание доступного сервера,正在等待可用服务器,正在等候可用伺服器 +,,,"Server found, loading...","Server gefunden, wird geladen ...",,Servidor encontrado. Cargando...,Servidor encontrado. Cargando...,"Serveur trouvé, chargement en cours...","Server trovato, caricamento...",サーバが見つかりました、ロード中です...,"서버 발견, 로드 중...","Servidor encontrado, carregando...","Servidor encontrado, carregando...",Сервер найден. Загрузка...,找到服务器,正在加载...,找到伺服器,載入中… +,,,Joining server,Verbindung zum Server wird hergestellt,,Uniéndose a un servidor,Uniéndose a un servidor,Connexion au serveur...,Connessione al server,サーバーに接続中,서버 가입,Entrando no servidor,Entrando no servidor,Подключение к серверу,正在加入服务器,加入伺服器 +,,,This game is disabled,Dieses Spiel wurde deaktiviert,,Este juego está desactivado,Este juego está desactivado,Ce jeu est désactivé.,Questo gioco è stato disattivato,このゲームは無効です,이 게임은 비활성화되었어요.,Este jogo está desabilitado,Este jogo está desabilitado,Эта игра отключена,游戏已禁用,此遊戲已停用 +,,,Cannot find game server,Spielserver kann nicht gefunden werden,,No se ha encontrado el servidor del juego,No se ha encontrado el servidor del juego,Serveur de jeu introuvable.,Impossibile trovare il server di gioco,ゲームサーバーが見つかりません,게임 서버를 찾을 수 없음,Não foi possível encontrar o servidor do jogo,Não foi possível encontrar o servidor do jogo,Не могу найти игровой сервер,无法找到游戏服务器,無法找到遊戲伺服器 +,,,This game has ended,Dieses Spiel ist beendet,,Este juego ha finalizado,Este juego ha finalizado,Cette partie est terminée.,Questo gioco è stato chiuso,このゲームは終了しました,이 게임은 종료되었어요.,Este jogo acabou,Este jogo acabou,Эта игра закончена,此游戏已结束,此遊戲已結束 +,,,Requested game is full,Das angefragte Spiel ist voll,,El juego solicitado está lleno,El juego solicitado está lleno,La partie demandée est complète.,Il gioco richiesto è al completo,リクエストしたゲームは満員です。,요청한 게임이 가득 찼어요.,O jogo solicitado está cheio,O jogo solicitado está cheio,Запрашиваемая игра переполнена,请求的游戏已满员,要求的遊戲已滿 +,,,Followed user has left the game,"Der Benutzer, dem du folgst, hat das Spiel verlassen",,El usuario al que sigues ha abandonado el juego,El usuario al que sigues ha abandonado el juego,L'utilisateur que vous suiviez a quitté la partie.,L'utente seguito ha abbandonato il gioco,フォローしたユーザーがゲームを辞めました,팔로우 중인 사용자가 게임에서 나갔어요.,O usuário que você segue saiu do jogo,O usuário que você segue saiu do jogo,Отслеживаемый игрок вышел из игры,关注用户已离开游戏,您關注的使用者已離開遊戲 +,,,This game is restricted,Dieses Spiel ist begrenzt,,Este juego está restringido,Este juego está restringido,Ce jeu est en accès limité.,Questa partita è riservata,このゲームは制限されています,이 게임은 제한되어 있어요.,Este jogo é restrito,Este jogo é restrito,Эта игра запрещена,此游戏有限制,此遊戲已受限 +,,,Not authorized to join this game,"Du bist nicht befugt, diesem Spiel beizutreten",,No tienes autorización para unirte a este juego,No tienes autorización para unirte a este juego,Vous n'avez pas l'autorisation de rejoindre ce jeu.,Non hai l'autorizzazione per partecipare a questa partita,このゲームに参加する権限がありません,게임 참여 권한 없음,Você não tem autorização para entrar neste jogo,Você não tem autorização para entrar neste jogo,Не разрешено присоединяться к этой игре,没有加入此游戏的权限,無加入此遊戲的權限 +,,,Server is busy,Server ist ausgelastet,,El servidor está ocupado,El servidor está ocupado,Le serveur est occupé.,Il server è occupato,サーバーがビジー状態です,서버 사용량이 많아요.,Servidor ocupado,Servidor ocupado,Сервер занят,服务器忙,伺服器忙碌 +,,,Hash Expired,Hash abgelaufen,,El hash ha caducado,El hash ha caducado,Expiration du hachage.,Hash scaduto,ハッシュが期限切れです,해시 만료됨,Hash expirado,Hash expirado,Хеширование истекло,哈希码失效,雜湊值已過期 +,,,Hash Exception,Hash-Ausnahme,,Excepción del hash,Excepción del hash,Exception de hachage.,Eccezione hash,ハッシュが例外です,해시 예외,Exceção de hash,Exceção de hash,Исключение хеширования,哈希码例外,雜湊值例外狀況 +,,,Your party is too large to fit,Deine Party ist zu groß,,Tu equipo es demasiado grande,Tu equipo es demasiado grande,Votre groupe comporte trop de membres.,Il tuo gruppo è troppo grande,パーティーが大きすぎてフィットしません,파티가 너무 커서 맞지 않아요.,Seu time é muito grande,Seu time é muito grande,Ваша группа слишком велика,你的团队人数过多,无法加入,您的隊伍已超過可容納人數 +,,,A Http error has occured. Please close the client and try again.,Ein HTTP-Fehler ist aufgetreten. Schließe den Client und versuche es erneut.,,Error en el http. Cierra el cliente e inténtalo de nuevo.,Error en el http. Cierra el cliente e inténtalo de nuevo.,"Une erreur HTTP s'est produite. Veuillez fermer le client, puis réessayez.",Si è verificato un errore http. Chiudi il client e riprova.,HTTPエラーが発生しました。クライアントを終了して、もう一度試してください。,Http 오류가 발생했어요. 클라이언트를 닫고 다시 시도하세요.,Erro no http. Feche o cliente e tente novamente.,Erro no http. Feche o cliente e tente novamente.,"Произошла ошибка Http. Пожалуйста, закройте клиент и попробуйте снова.",发生 Http 错误。请关闭客户端并重试。,發生HTTP錯誤。請關閉客戶端並再試一次。 +,,,Can't join place {PLACEID:int}: {ERROR_REASON:translate},Beitritt zu {PLACEID:int} nicht möglich: {ERROR_REASON:translate},,No es posible unirse al lugar {PLACEID:int}: {ERROR_REASON:translate},No es posible unirse al lugar {PLACEID:int}: {ERROR_REASON:translate},Impossible de rejoindre l'emplacement {PLACEID:int} : {ERROR_REASON:translate},Impossibile raggiungere la località {PLACEID:int}: {ERROR_REASON:translate},{PLACEID:int}の場所に参加できません:{ERROR_REASON:translate},장소 {PLACEID:int}에 참여할 수 없음: {ERROR_REASON:translate},Não é possível entrar em {PLACEID:int}: {ERROR_REASON:translate},Não é possível entrar em {PLACEID:int}: {ERROR_REASON:translate},Нельзя присоединиться ({PLACEID:int}): {ERROR_REASON:translate},由于{ERROR_REASON:translate},你无法加入{PLACEID:int}:,因下列原因無法加入地點{PLACEID:int}:{ERROR_REASON:translate} +,,,Can't follow user: {ERROR_REASON:translate},Folgen von Benutzer nicht möglich: {ERROR_REASON:translate},,No es posible seguir al usuario: {ERROR_REASON:translate},No es posible seguir al usuario: {ERROR_REASON:translate},Impossible de suivre l'utilisateur : {ERROR_REASON:translate},Impossibile seguire l'utente: {ERROR_REASON:translate},ユーザーをフォローできません:{ERROR_REASON:translate},사용자를 팔로우할 수 없음: {ERROR_REASON:translate},Não é possível seguir o usuário: {ERROR_REASON:translate},Não é possível seguir o usuário: {ERROR_REASON:translate},Нельзя следить за пользователем: {ERROR_REASON:translate},由于{ERROR_REASON:translate},无法关注用户:,因下列原因無法關注使用者:{ERROR_REASON:translate} +,,,Cannot join private server: {ERROR_REASON:translate},Beitritt zu privatem Server nicht möglich: {ERROR_REASON:translate},,No es posible unirse al servidor privado: {ERROR_REASON:translate},No es posible unirse al servidor privado: {ERROR_REASON:translate},Impossible de rejoindre le serveur privé : {ERROR_REASON:translate},Impossibile accedere al server privato: {ERROR_REASON:translate},プライベートサーバーに参加できません:{ERROR_REASON:translate},비공개 서버에 참여할 수 없음: {ERROR_REASON:translate},Não é possível juntar-se ao servidor privado: {ERROR_REASON:translate},Não é possível juntar-se ao servidor privado: {ERROR_REASON:translate},Нельзя присоединиться к частному серверу: {ERROR_REASON:translate},由于{ERROR_REASON:translate},无法加入私人服务器:,因下列原因無法加入私人伺服器:{ERROR_REASON:translate} +,,,Cannot join game instance: {ERROR_REASON:translate},Beitritt zu Spielinstanz nicht möglich: {ERROR_REASON:translate},,No es posible unirse a una instancia del juego: {ERROR_REASON:translate},No es posible unirse a una instancia del juego: {ERROR_REASON:translate},Impossible de rejoindre l'instance de jeu : {ERROR_REASON:translate},Impossibile accedere all'area di gioco: {ERROR_REASON:translate},ゲームインスタンスに参加できません:{ERROR_REASON:translate},게임 인스턴스에 참여할 수 없음: {ERROR_REASON:translate},Não é possível juntar-se ao jogo: {ERROR_REASON:translate},Não é possível juntar-se ao jogo: {ERROR_REASON:translate},Нельзя войти в локацию игры: {ERROR_REASON:translate},由于{ERROR_REASON:translate},无法加入游戏:,因下列原因無法加入遊戲: {ERROR_REASON:translate} +,,,Invalid JSON response received,Ungültige JSON-Antwort erhalten,,Se ha recibido una respuesta JSON no válida,Se ha recibido una respuesta JSON no válida,Réponse JSON invalide reçue.,Ricevuta risposta JSON non valida,無効なJSON応答を受信しました,잘못된 JSON 응답 수신됨,Resposta do JSON inválida,Resposta do JSON inválida,Получен некорректный ответ JSON,接收到无效的 JSON,接收的JSON回應無效 +,,,Retrying,Neuer Versuch,,Intentándolo de nuevo,Intentándolo de nuevo,Nouvelle tentative,Nuovo tentativo,リトライ中,재시도 중,Tentando de novo,Tentando de novo,Новая попытка,正在重试,重試中 +,,,Waiting for an available server. ({COUNT:int}),Warten auf verfügbaren Server. ({COUNT:int}),,Esperando un servidor disponible. ({COUNT:int}),Esperando un servidor disponible. ({COUNT:int}),En attente d'un serveur disponible. ({COUNT:int}),In attesa di un server disponibile. ({COUNT:int}),使用可能なサーバを待機中 ({COUNT:int}),이용 가능한 서버를 기다리는 중 ({COUNT:int}),Aguardando um servidor disponível ({COUNT:int}),Aguardando um servidor disponível ({COUNT:int}),Ожидание доступного сервера ({COUNT:int}),正在等待可用服务器 ({COUNT:int}),正在等候可用伺服器 ({COUNT:int}) +,,,"Server found, loading.... ({COUNT:int})","Server gefunden, wird geladen .... ({COUNT:int})",,Servidor encontrado. Cargando.... ({COUNT:int}),Servidor encontrado. Cargando.... ({COUNT:int}),"Serveur trouvé, chargement en cours.... ({COUNT:int})","Server trovato, caricamento.... ({COUNT:int})",サーバが見つかりました、ロード中です... ({COUNT:int}),"서버 발견, 로드 중... ({COUNT:int})","Servidor encontrado, carregando... ({COUNT:int})","Servidor encontrado, carregando... ({COUNT:int})",Сервер найден. Загрузка... ({COUNT:int}),找到服务器,正在加载... ({COUNT:int}),找到伺服器,載入中… ({COUNT:int}) +,,,Joining server. ({COUNT:int}),Verbindung zum Server wird hergestellt. ({COUNT:int}),,Uniéndose a un servidor. ({COUNT:int}),Uniéndose a un servidor. ({COUNT:int}),Connexion au serveur.... ({COUNT:int}),Connessione al server. ({COUNT:int}),サーバーに接続中 ({COUNT:int}),서버 가입 ({COUNT:int}),Entrando no servidor ({COUNT:int}),Entrando no servidor ({COUNT:int}),Подключение к серверу ({COUNT:int}),正在加入服务器 ({COUNT:int}),加入伺服器 ({COUNT:int}) +,,,This game is disabled. ({COUNT:int}),Dieses Spiel wurde deaktiviert. ({COUNT:int}),,Este juego está desactivado. ({COUNT:int}),Este juego está desactivado. ({COUNT:int}),Ce jeu est désactivé.. ({COUNT:int}),Questo gioco è stato disattivato. ({COUNT:int}),このゲームは無効です ({COUNT:int}),이 게임은 비활성화되었어요. ({COUNT:int}),Este jogo está desabilitado ({COUNT:int}),Este jogo está desabilitado ({COUNT:int}),Эта игра отключена ({COUNT:int}),游戏已禁用 ({COUNT:int}),此遊戲已停用 ({COUNT:int}) +,,,Cannot find game server. ({COUNT:int}),Spielserver kann nicht gefunden werden. ({COUNT:int}),,No se ha encontrado el servidor del juego. ({COUNT:int}),No se ha encontrado el servidor del juego. ({COUNT:int}),Serveur de jeu introuvable.. ({COUNT:int}),Impossibile trovare il server di gioco. ({COUNT:int}),ゲームサーバーが見つかりません ({COUNT:int}),게임 서버를 찾을 수 없음 ({COUNT:int}),Não foi possível encontrar o servidor do jogo ({COUNT:int}),Não foi possível encontrar o servidor do jogo ({COUNT:int}),Не могу найти игровой сервер ({COUNT:int}),无法找到游戏服务器 ({COUNT:int}),無法找到遊戲伺服器 ({COUNT:int}) +,,,This game has ended. ({COUNT:int}),Dieses Spiel ist beendet. ({COUNT:int}),,Este juego ha finalizado. ({COUNT:int}),Este juego ha finalizado. ({COUNT:int}),Cette partie est terminée.. ({COUNT:int}),Questo gioco è stato chiuso. ({COUNT:int}),このゲームは終了しました ({COUNT:int}),이 게임은 종료되었어요. ({COUNT:int}),Este jogo acabou ({COUNT:int}),Este jogo acabou ({COUNT:int}),Эта игра закончена ({COUNT:int}),此游戏已结束 ({COUNT:int}),此遊戲已結束 ({COUNT:int}) +,,,Requested game is full. ({COUNT:int}),Das angefragte Spiel ist voll. ({COUNT:int}),,El juego solicitado está lleno. ({COUNT:int}),El juego solicitado está lleno. ({COUNT:int}),La partie demandée est complète.. ({COUNT:int}),Il gioco richiesto è al completo. ({COUNT:int}),リクエストしたゲームは満員です。 ({COUNT:int}),요청한 게임이 가득 찼어요. ({COUNT:int}),O jogo solicitado está cheio ({COUNT:int}),O jogo solicitado está cheio ({COUNT:int}),Запрашиваемая игра переполнена ({COUNT:int}),请求的游戏已满员 ({COUNT:int}),要求的遊戲已滿 ({COUNT:int}) +,,,Followed user has left the game. ({COUNT:int}),"Der Benutzer, dem du folgst, hat das Spiel verlassen. ({COUNT:int})",,El usuario al que sigues ha abandonado el juego. ({COUNT:int}),El usuario al que sigues ha abandonado el juego. ({COUNT:int}),L'utilisateur que vous suiviez a quitté la partie.. ({COUNT:int}),L'utente seguito ha abbandonato il gioco. ({COUNT:int}),フォローしたユーザーがゲームを辞めました ({COUNT:int}),팔로우 중인 사용자가 게임에서 나갔어요. ({COUNT:int}),O usuário que você segue saiu do jogo ({COUNT:int}),O usuário que você segue saiu do jogo ({COUNT:int}),Отслеживаемый игрок вышел из игры ({COUNT:int}),关注用户已离开游戏 ({COUNT:int}),您關注的使用者已離開遊戲 ({COUNT:int}) +,,,This game is restricted. ({COUNT:int}),Dieses Spiel ist begrenzt. ({COUNT:int}),,Este juego está restringido. ({COUNT:int}),Este juego está restringido. ({COUNT:int}),Ce jeu est en accès limité.. ({COUNT:int}),Questa partita è riservata. ({COUNT:int}),このゲームは制限されています ({COUNT:int}),이 게임은 제한되어 있어요. ({COUNT:int}),Este jogo é restrito ({COUNT:int}),Este jogo é restrito ({COUNT:int}),Эта игра запрещена ({COUNT:int}),此游戏有限制 ({COUNT:int}),此遊戲已受限 ({COUNT:int}) +,,,Not authorized to join this game. ({COUNT:int}),"Du bist nicht befugt, diesem Spiel beizutreten. ({COUNT:int})",,No tienes autorización para unirte a este juego. ({COUNT:int}),No tienes autorización para unirte a este juego. ({COUNT:int}),Vous n'avez pas l'autorisation de rejoindre ce jeu.. ({COUNT:int}),Non hai l'autorizzazione per partecipare a questa partita. ({COUNT:int}),このゲームに参加する権限がありません ({COUNT:int}),게임 참여 권한 없음 ({COUNT:int}),Você não tem autorização para entrar neste jogo ({COUNT:int}),Você não tem autorização para entrar neste jogo ({COUNT:int}),Не разрешено присоединяться к этой игре ({COUNT:int}),没有加入此游戏的权限 ({COUNT:int}),無加入此遊戲的權限 ({COUNT:int}) +,,,Server is busy. ({COUNT:int}),Server ist ausgelastet. ({COUNT:int}),,El servidor está ocupado. ({COUNT:int}),El servidor está ocupado. ({COUNT:int}),Le serveur est occupé.. ({COUNT:int}),Il server è occupato. ({COUNT:int}),サーバーがビジー状態です ({COUNT:int}),서버 사용량이 많아요. ({COUNT:int}),Servidor ocupado ({COUNT:int}),Servidor ocupado ({COUNT:int}),Сервер занят ({COUNT:int}),服务器忙 ({COUNT:int}),伺服器忙碌 ({COUNT:int}) +,,,Hash Expired. ({COUNT:int}),Hash abgelaufen. ({COUNT:int}),,El hash ha caducado. ({COUNT:int}),El hash ha caducado. ({COUNT:int}),Expiration du hachage.. ({COUNT:int}),Hash scaduto. ({COUNT:int}),ハッシュが期限切れです ({COUNT:int}),해시 만료됨 ({COUNT:int}),Hash expirado ({COUNT:int}),Hash expirado ({COUNT:int}),Хеширование истекло ({COUNT:int}),哈希码失效 ({COUNT:int}),雜湊值已過期 ({COUNT:int}) +,,,Hash Exception. ({COUNT:int}),Hash-Ausnahme. ({COUNT:int}),,Excepción del hash. ({COUNT:int}),Excepción del hash. ({COUNT:int}),Exception de hachage.. ({COUNT:int}),Eccezione hash. ({COUNT:int}),ハッシュが例外です ({COUNT:int}),해시 예외 ({COUNT:int}),Exceção de hash ({COUNT:int}),Exceção de hash ({COUNT:int}),Исключение хеширования ({COUNT:int}),哈希码例外 ({COUNT:int}),雜湊值例外狀況 ({COUNT:int}) +,,,Your party is too large to fit. ({COUNT:int}),Deine Party ist zu groß. ({COUNT:int}),,Tu equipo es demasiado grande. ({COUNT:int}),Tu equipo es demasiado grande. ({COUNT:int}),Votre groupe comporte trop de membres.. ({COUNT:int}),Il tuo gruppo è troppo grande. ({COUNT:int}),パーティーが大きすぎてフィットしません ({COUNT:int}),파티가 너무 커서 맞지 않아요. ({COUNT:int}),Seu time é muito grande ({COUNT:int}),Seu time é muito grande ({COUNT:int}),Ваша группа слишком велика ({COUNT:int}),你的团队人数过多,无法加入 ({COUNT:int}),您的隊伍已超過可容納人數 ({COUNT:int}) +,,,A Http error has occured. Please close the client and try again.. ({COUNT:int}),Ein HTTP-Fehler ist aufgetreten. Schließe den Client und versuche es erneut.. ({COUNT:int}),,Error en el http. Cierra el cliente e inténtalo de nuevo.. ({COUNT:int}),Error en el http. Cierra el cliente e inténtalo de nuevo.. ({COUNT:int}),"Une erreur HTTP s'est produite. Veuillez fermer le client, puis réessayez.. ({COUNT:int})",Si è verificato un errore http. Chiudi il client e riprova.. ({COUNT:int}),HTTPエラーが発生しました。クライアントを終了して、もう一度試してください。 ({COUNT:int}),Http 오류가 발생했어요. 클라이언트를 닫고 다시 시도하세요. ({COUNT:int}),Erro no http. Feche o cliente e tente novamente. ({COUNT:int}),Erro no http. Feche o cliente e tente novamente. ({COUNT:int}),"Произошла ошибка Http. Пожалуйста, закройте клиент и попробуйте снова. ({COUNT:int})",发生 Http 错误。请关闭客户端并重试。 ({COUNT:int}),發生HTTP錯誤。請關閉客戶端並再試一次。 ({COUNT:int}) +,,,Can't join place {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),Beitritt zu {PLACEID:int} nicht möglich: {ERROR_REASON:translate}. ({COUNT:int}),,No es posible unirse al lugar {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),No es posible unirse al lugar {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),Impossible de rejoindre l'emplacement {PLACEID:int} : {ERROR_REASON:translate}. ({COUNT:int}),Impossibile raggiungere la località {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),{PLACEID:int}の場所に参加できません:{ERROR_REASON:translate}. ({COUNT:int}),장소 {PLACEID:int}에 참여할 수 없음: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível entrar em {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível entrar em {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),Нельзя присоединиться ({PLACEID:int}): {ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},你无法加入{PLACEID:int}:. ({COUNT:int}),正在等候可用伺服器. {RET:translate}...({COUNT:int}) +,,,Can't follow user: {ERROR_REASON:translate}. ({COUNT:int}),Folgen von Benutzer nicht möglich: {ERROR_REASON:translate}. ({COUNT:int}),,No es posible seguir al usuario: {ERROR_REASON:translate}. ({COUNT:int}),No es posible seguir al usuario: {ERROR_REASON:translate}. ({COUNT:int}),Impossible de suivre l'utilisateur : {ERROR_REASON:translate}. ({COUNT:int}),Impossibile seguire l'utente: {ERROR_REASON:translate}. ({COUNT:int}),ユーザーをフォローできません:{ERROR_REASON:translate}. ({COUNT:int}),사용자를 팔로우할 수 없음: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível seguir o usuário: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível seguir o usuário: {ERROR_REASON:translate}. ({COUNT:int}),Нельзя следить за пользователем: {ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},无法关注用户:. ({COUNT:int}),找到伺服器,載入中…. {RET:translate}...({COUNT:int}) +,,,Cannot join private server: {ERROR_REASON:translate}. ({COUNT:int}),Beitritt zu privatem Server nicht möglich: {ERROR_REASON:translate}. ({COUNT:int}),,No es posible unirse al servidor privado: {ERROR_REASON:translate}. ({COUNT:int}),No es posible unirse al servidor privado: {ERROR_REASON:translate}. ({COUNT:int}),Impossible de rejoindre le serveur privé : {ERROR_REASON:translate}. ({COUNT:int}),Impossibile accedere al server privato: {ERROR_REASON:translate}. ({COUNT:int}),プライベートサーバーに参加できません:{ERROR_REASON:translate}. ({COUNT:int}),비공개 서버에 참여할 수 없음: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível juntar-se ao servidor privado: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível juntar-se ao servidor privado: {ERROR_REASON:translate}. ({COUNT:int}),Нельзя присоединиться к частному серверу: {ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},无法加入私人服务器:. ({COUNT:int}),加入伺服器. {RET:translate}...({COUNT:int}) +,,,Cannot join game instance: {ERROR_REASON:translate}. ({COUNT:int}),Beitritt zu Spielinstanz nicht möglich: {ERROR_REASON:translate}. ({COUNT:int}),,No es posible unirse a una instancia del juego: {ERROR_REASON:translate}. ({COUNT:int}),No es posible unirse a una instancia del juego: {ERROR_REASON:translate}. ({COUNT:int}),Impossible de rejoindre l'instance de jeu : {ERROR_REASON:translate}. ({COUNT:int}),Impossibile accedere all'area di gioco: {ERROR_REASON:translate}. ({COUNT:int}),ゲームインスタンスに参加できません:{ERROR_REASON:translate}. ({COUNT:int}),게임 인스턴스에 참여할 수 없음: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível juntar-se ao jogo: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível juntar-se ao jogo: {ERROR_REASON:translate}. ({COUNT:int}),Нельзя войти в локацию игры: {ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},无法加入游戏:. ({COUNT:int}),此遊戲已停用. {RET:translate}...({COUNT:int}) +,,,Roblox has shut down this game server for maintenance. {RET:translate}...({COUNT:int}),Roblox hat diesen Spielserver aufgrund von Wartungsarbeiten abgeschaltet. {RET:translate}...({COUNT:int}),,Roblox ha cerrado el servidor de este juego por mantenimiento. {RET:translate}...({COUNT:int}),Roblox ha cerrado el servidor de este juego por mantenimiento. {RET:translate}...({COUNT:int}),Roblox a arrêté ce serveur de jeu pour effectuer la maintenance.. {RET:translate}...({COUNT:int}),Roblox ha chiuso questo server di gioco per manutenzione. {RET:translate}...({COUNT:int}),メンテナンスのため、Roblox によってゲームサーバーがシャットダウンされています。. {RET:translate}...({COUNT:int}),Roblox가 점검을 위해 이 게임 서버를 닫았어요.. {RET:translate}...({COUNT:int}),O servidor deste jogo foi encerrado pelo Roblox para manutenção. {RET:translate}...({COUNT:int}),O servidor deste jogo foi encerrado pelo Roblox para manutenção. {RET:translate}...({COUNT:int}),Roblox отключил игровой сервер для техобслуживания.. {RET:translate}...({COUNT:int}),维护期间,Roblox 已关闭此游戏服务器. {RET:translate}...({COUNT:int}),無法找到遊戲伺服器. {RET:translate}...({COUNT:int}) +,,,This game has been disconnected because you have joined a game from another device. {RET:translate}...({COUNT:int}),"Dieses Spiel wurde unterbrochen, weil du einem Spiel von einem anderen Gerät beigetreten bist. {RET:translate}...({COUNT:int})",,Se ha desconectado este juego porque te has unido a un juego desde otro dispositivo. {RET:translate}...({COUNT:int}),Se ha desconectado este juego porque te has unido a un juego desde otro dispositivo. {RET:translate}...({COUNT:int}),"Vous avez été déconnecté(e) de ce jeu, car vous en avez rejoint un autre depuis un appareil différent.. {RET:translate}...({COUNT:int})",Questo gioco è stato disconnesso perché hai giocato una partita con un altro dispositivo. {RET:translate}...({COUNT:int}),別のデバイスからゲームに参加したため、このゲームへの接続が切断されました. {RET:translate}...({COUNT:int}),다른 장치에서 게임에 참여하여 이 게임의 연결이 끊어졌어요.. {RET:translate}...({COUNT:int}),Este jogo foi desconectado pois você entrou em um jogo através de outro dispositivo. {RET:translate}...({COUNT:int}),Este jogo foi desconectado pois você entrou em um jogo através de outro dispositivo. {RET:translate}...({COUNT:int}),"Соединение с этой игрой было прервано, так как вы стали играть с другого устройства.. {RET:translate}...({COUNT:int})",由于你从另一设备加入游戏,此游戏已断开连接. {RET:translate}...({COUNT:int}),此遊戲已結束. {RET:translate}...({COUNT:int}) +,,,"Disconnected from game, please reconnect. {RET:translate}...({COUNT:int})",Verbindung zum Spiel unterbrochen. Bitte erneut verbinden.. {RET:translate}...({COUNT:int}),,Desconectado del juego. Conéctate de nuevo.. {RET:translate}...({COUNT:int}),Desconectado del juego. Conéctate de nuevo.. {RET:translate}...({COUNT:int}),Vous avez été déconnecté(e) du jeu. Veuillez vous reconnecter.. {RET:translate}...({COUNT:int}),Disconnesso dal gioco; è necessario riconnettersi. {RET:translate}...({COUNT:int}),ゲームへの接続が切断されました。再接続してください. {RET:translate}...({COUNT:int}),게임 연결이 끊어졌어요. 다시 연결하세요.. {RET:translate}...({COUNT:int}),Você foi desconectado do jogo. Conecte-se novamente.. {RET:translate}...({COUNT:int}),Você foi desconectado do jogo. Conecte-se novamente.. {RET:translate}...({COUNT:int}),"Соединение и игрой прервано, пожалуйста, установите соединение повторно.. {RET:translate}...({COUNT:int})",与游戏连接已断开,请重新连接. {RET:translate}...({COUNT:int}),要求的遊戲已滿. {RET:translate}...({COUNT:int}) +,,,Waiting for an available server. {RET:translate}...({COUNT:int}),Warten auf verfügbaren Server. {RET:translate}...({COUNT:int}),,Esperando un servidor disponible. {RET:translate}...({COUNT:int}),Esperando un servidor disponible. {RET:translate}...({COUNT:int}),En attente d'un serveur disponible. {RET:translate}...({COUNT:int}),In attesa di un server disponibile. {RET:translate}...({COUNT:int}),使用可能なサーバを待機中. {RET:translate}...({COUNT:int}),이용 가능한 서버를 기다리는 중. {RET:translate}...({COUNT:int}),Aguardando um servidor disponível. {RET:translate}...({COUNT:int}),Aguardando um servidor disponível. {RET:translate}...({COUNT:int}),Ожидание доступного сервера. {RET:translate}...({COUNT:int}),正在等待可用服务器. {RET:translate}...({COUNT:int}),您關注的使用者已離開遊戲. {RET:translate}...({COUNT:int}) +,,,"Server found, loading.... {RET:translate}...({COUNT:int})","Server gefunden, wird geladen .... {RET:translate}...({COUNT:int})",,Servidor encontrado. Cargando.... {RET:translate}...({COUNT:int}),Servidor encontrado. Cargando.... {RET:translate}...({COUNT:int}),"Serveur trouvé, chargement en cours.... {RET:translate}...({COUNT:int})","Server trovato, caricamento.... {RET:translate}...({COUNT:int})",サーバが見つかりました、ロード中です.... {RET:translate}...({COUNT:int}),"서버 발견, 로드 중.... {RET:translate}...({COUNT:int})","Servidor encontrado, carregando.... {RET:translate}...({COUNT:int})","Servidor encontrado, carregando.... {RET:translate}...({COUNT:int})",Сервер найден. Загрузка.... {RET:translate}...({COUNT:int}),找到服务器,正在加载.... {RET:translate}...({COUNT:int}),此遊戲已受限. {RET:translate}...({COUNT:int}) +,,,Joining server. {RET:translate}...({COUNT:int}),Verbindung zum Server wird hergestellt. {RET:translate}...({COUNT:int}),,Uniéndose a un servidor. {RET:translate}...({COUNT:int}),Uniéndose a un servidor. {RET:translate}...({COUNT:int}),Connexion au serveur.... {RET:translate}...({COUNT:int}),Connessione al server. {RET:translate}...({COUNT:int}),サーバーに接続中. {RET:translate}...({COUNT:int}),서버 가입. {RET:translate}...({COUNT:int}),Entrando no servidor. {RET:translate}...({COUNT:int}),Entrando no servidor. {RET:translate}...({COUNT:int}),Подключение к серверу. {RET:translate}...({COUNT:int}),正在加入服务器. {RET:translate}...({COUNT:int}),無加入此遊戲的權限. {RET:translate}...({COUNT:int}) +,,,This game is disabled. {RET:translate}...({COUNT:int}),Dieses Spiel wurde deaktiviert. {RET:translate}...({COUNT:int}),,Este juego está desactivado. {RET:translate}...({COUNT:int}),Este juego está desactivado. {RET:translate}...({COUNT:int}),Ce jeu est désactivé.. {RET:translate}...({COUNT:int}),Questo gioco è stato disattivato. {RET:translate}...({COUNT:int}),このゲームは無効です. {RET:translate}...({COUNT:int}),이 게임은 비활성화되었어요.. {RET:translate}...({COUNT:int}),Este jogo está desabilitado. {RET:translate}...({COUNT:int}),Este jogo está desabilitado. {RET:translate}...({COUNT:int}),Эта игра отключена. {RET:translate}...({COUNT:int}),游戏已禁用. {RET:translate}...({COUNT:int}),伺服器忙碌. {RET:translate}...({COUNT:int}) +,,,Cannot find game server. {RET:translate}...({COUNT:int}),Spielserver kann nicht gefunden werden. {RET:translate}...({COUNT:int}),,No se ha encontrado el servidor del juego. {RET:translate}...({COUNT:int}),No se ha encontrado el servidor del juego. {RET:translate}...({COUNT:int}),Serveur de jeu introuvable.. {RET:translate}...({COUNT:int}),Impossibile trovare il server di gioco. {RET:translate}...({COUNT:int}),ゲームサーバーが見つかりません. {RET:translate}...({COUNT:int}),게임 서버를 찾을 수 없음. {RET:translate}...({COUNT:int}),Não foi possível encontrar o servidor do jogo. {RET:translate}...({COUNT:int}),Não foi possível encontrar o servidor do jogo. {RET:translate}...({COUNT:int}),Не могу найти игровой сервер. {RET:translate}...({COUNT:int}),无法找到游戏服务器. {RET:translate}...({COUNT:int}),雜湊值已過期. {RET:translate}...({COUNT:int}) +,,,This game has ended. {RET:translate}...({COUNT:int}),Dieses Spiel ist beendet. {RET:translate}...({COUNT:int}),,Este juego ha finalizado. {RET:translate}...({COUNT:int}),Este juego ha finalizado. {RET:translate}...({COUNT:int}),Cette partie est terminée.. {RET:translate}...({COUNT:int}),Questo gioco è stato chiuso. {RET:translate}...({COUNT:int}),このゲームは終了しました. {RET:translate}...({COUNT:int}),이 게임은 종료되었어요.. {RET:translate}...({COUNT:int}),Este jogo acabou. {RET:translate}...({COUNT:int}),Este jogo acabou. {RET:translate}...({COUNT:int}),Эта игра закончена. {RET:translate}...({COUNT:int}),此游戏已结束. {RET:translate}...({COUNT:int}),雜湊值例外狀況. {RET:translate}...({COUNT:int}) +,,,Requested game is full. {RET:translate}...({COUNT:int}),Das angefragte Spiel ist voll. {RET:translate}...({COUNT:int}),,El juego solicitado está lleno. {RET:translate}...({COUNT:int}),El juego solicitado está lleno. {RET:translate}...({COUNT:int}),La partie demandée est complète.. {RET:translate}...({COUNT:int}),Il gioco richiesto è al completo. {RET:translate}...({COUNT:int}),リクエストしたゲームは満員です。. {RET:translate}...({COUNT:int}),요청한 게임이 가득 찼어요.. {RET:translate}...({COUNT:int}),O jogo solicitado está cheio. {RET:translate}...({COUNT:int}),O jogo solicitado está cheio. {RET:translate}...({COUNT:int}),Запрашиваемая игра переполнена. {RET:translate}...({COUNT:int}),请求的游戏已满员. {RET:translate}...({COUNT:int}),您的隊伍已超過可容納人數. {RET:translate}...({COUNT:int}) +,,,Followed user has left the game. {RET:translate}...({COUNT:int}),"Der Benutzer, dem du folgst, hat das Spiel verlassen. {RET:translate}...({COUNT:int})",,El usuario al que sigues ha abandonado el juego. {RET:translate}...({COUNT:int}),El usuario al que sigues ha abandonado el juego. {RET:translate}...({COUNT:int}),L'utilisateur que vous suiviez a quitté la partie.. {RET:translate}...({COUNT:int}),L'utente seguito ha abbandonato il gioco. {RET:translate}...({COUNT:int}),フォローしたユーザーがゲームを辞めました. {RET:translate}...({COUNT:int}),팔로우 중인 사용자가 게임에서 나갔어요.. {RET:translate}...({COUNT:int}),O usuário que você segue saiu do jogo. {RET:translate}...({COUNT:int}),O usuário que você segue saiu do jogo. {RET:translate}...({COUNT:int}),Отслеживаемый игрок вышел из игры. {RET:translate}...({COUNT:int}),关注用户已离开游戏. {RET:translate}...({COUNT:int}),發生HTTP錯誤。請關閉客戶端並再試一次。. {RET:translate}...({COUNT:int}) +,,,This game is restricted. {RET:translate}...({COUNT:int}),Dieses Spiel ist begrenzt. {RET:translate}...({COUNT:int}),,Este juego está restringido. {RET:translate}...({COUNT:int}),Este juego está restringido. {RET:translate}...({COUNT:int}),Ce jeu est en accès limité.. {RET:translate}...({COUNT:int}),Questa partita è riservata. {RET:translate}...({COUNT:int}),このゲームは制限されています. {RET:translate}...({COUNT:int}),이 게임은 제한되어 있어요.. {RET:translate}...({COUNT:int}),Este jogo é restrito. {RET:translate}...({COUNT:int}),Este jogo é restrito. {RET:translate}...({COUNT:int}),Эта игра запрещена. {RET:translate}...({COUNT:int}),此游戏有限制. {RET:translate}...({COUNT:int}), +,,,Not authorized to join this game. {RET:translate}...({COUNT:int}),"Du bist nicht befugt, diesem Spiel beizutreten. {RET:translate}...({COUNT:int})",,No tienes autorización para unirte a este juego. {RET:translate}...({COUNT:int}),No tienes autorización para unirte a este juego. {RET:translate}...({COUNT:int}),Vous n'avez pas l'autorisation de rejoindre ce jeu.. {RET:translate}...({COUNT:int}),Non hai l'autorizzazione per partecipare a questa partita. {RET:translate}...({COUNT:int}),このゲームに参加する権限がありません. {RET:translate}...({COUNT:int}),게임 참여 권한 없음. {RET:translate}...({COUNT:int}),Você não tem autorização para entrar neste jogo. {RET:translate}...({COUNT:int}),Você não tem autorização para entrar neste jogo. {RET:translate}...({COUNT:int}),Не разрешено присоединяться к этой игре. {RET:translate}...({COUNT:int}),没有加入此游戏的权限. {RET:translate}...({COUNT:int}), +,,,Server is busy. {RET:translate}...({COUNT:int}),Server ist ausgelastet. {RET:translate}...({COUNT:int}),,El servidor está ocupado. {RET:translate}...({COUNT:int}),El servidor está ocupado. {RET:translate}...({COUNT:int}),Le serveur est occupé.. {RET:translate}...({COUNT:int}),Il server è occupato. {RET:translate}...({COUNT:int}),サーバーがビジー状態です. {RET:translate}...({COUNT:int}),서버 사용량이 많아요.. {RET:translate}...({COUNT:int}),Servidor ocupado. {RET:translate}...({COUNT:int}),Servidor ocupado. {RET:translate}...({COUNT:int}),Сервер занят. {RET:translate}...({COUNT:int}),服务器忙. {RET:translate}...({COUNT:int}), +,,,Hash Expired. {RET:translate}...({COUNT:int}),Hash abgelaufen. {RET:translate}...({COUNT:int}),,El hash ha caducado. {RET:translate}...({COUNT:int}),El hash ha caducado. {RET:translate}...({COUNT:int}),Expiration du hachage.. {RET:translate}...({COUNT:int}),Hash scaduto. {RET:translate}...({COUNT:int}),ハッシュが期限切れです. {RET:translate}...({COUNT:int}),해시 만료됨. {RET:translate}...({COUNT:int}),Hash expirado. {RET:translate}...({COUNT:int}),Hash expirado. {RET:translate}...({COUNT:int}),Хеширование истекло. {RET:translate}...({COUNT:int}),哈希码失效. {RET:translate}...({COUNT:int}), \ No newline at end of file diff --git a/Client2018/fmod.dll b/Client2018/fmod.dll new file mode 100644 index 0000000..65cde34 Binary files /dev/null and b/Client2018/fmod.dll differ diff --git a/Client2018/platformcontent/pc/fonts/NotoSansCJKjp-Regular.otf b/Client2018/platformcontent/pc/fonts/NotoSansCJKjp-Regular.otf new file mode 100644 index 0000000..296fbeb Binary files /dev/null and b/Client2018/platformcontent/pc/fonts/NotoSansCJKjp-Regular.otf differ diff --git a/Client2018/platformcontent/pc/terrain/diffuse.dds b/Client2018/platformcontent/pc/terrain/diffuse.dds new file mode 100644 index 0000000..8024eba Binary files /dev/null and b/Client2018/platformcontent/pc/terrain/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/terrain/diffusearray.dds b/Client2018/platformcontent/pc/terrain/diffusearray.dds new file mode 100644 index 0000000..af28a9e Binary files /dev/null and b/Client2018/platformcontent/pc/terrain/diffusearray.dds differ diff --git a/Client2018/platformcontent/pc/terrain/materials.json b/Client2018/platformcontent/pc/terrain/materials.json new file mode 100644 index 0000000..3de6276 --- /dev/null +++ b/Client2018/platformcontent/pc/terrain/materials.json @@ -0,0 +1,191 @@ +{ + "platform": "pc", + "atlas": + { + "pc": { + "width": 2048, + "height": 2048, + "tileSize": 256, + "tileCount": 6, + + "sliceSize": 512, + + "borderSize": 42 + }, + "ios": { + "width": 2048, + "height": 2048, + "tileSize": 256, + "tileCount": 6, + + "borderSize": 42 + }, + "android": { + "width": 2048, + "height": 2048, + "tileSize": 256, + "tileCount": 6, + + "borderSize": 42 + }, + "durango": { + "width": 32, + "height": 32, + "tileSize": 4, + "tileCount": 6, + + "sliceSize": 512, + + "borderSize": 42 + } + }, + "materials": + [ + { + "name": "Air" + }, + { + "name": "Water", + "water": 0.5 + }, + { + "name": "Grass", + "base_color": [ 106, 127, 63 ], + "texture_top": { "tiling": 0.25, "detiling": 0.25, "rotation": 1.0 }, + "texture_side": { "tiling": 0.2, "detiling": 0.2 }, + "texture_bottom": { "tiling": 0.45, "detiling": 0.45 } + }, + { + "name": "Slate", + "base_color": [ 63, 127, 107 ], + "shift": 0.1, + "texture": { "tiling": 0.55, "detiling": 0.55 } + }, + { + "name": "Concrete", + "base_color": [ 127, 102, 63 ], + "quantize": 0.5, + "type": "hard", + "texture_top": { "tiling": 0.5, "detiling": 0.8 }, + "texture_side": { "tiling": 0.4, "detiling": 0.4 } + }, + { + "name": "Brick", + "base_color": [ 138, 86, 62 ], + "cubify": 1, + "type": "hard", + "mapping": "cube", + "texture": { "tiling": 0.80, "detiling": 0.20 } + }, + { + "name": "Sand", + "base_color": [ 143, 126, 95 ], + "texture_top": { "tiling": 0.25, "detiling": 0.25 }, + "texture_side": { "tiling": 0.25, "detiling": 0.55 } + }, + { + "name": "WoodPlanks", + "base_color": [ 139, 109, 79 ], + "cubify": 1.0, + "type": "hard", + "mapping": "cube", + "texture": { "tiling": 0.8, "detiling": 0.5 } + }, + { + "name": "Rock", + "base_color": [ 102, 108, 111 ], + "shift": 0.3, + "type": "hardsoft", + "texture": { "tiling": 0.55, "detiling": 0.55, "rotation": 0.5 } + }, + { + "name": "Glacier", + "base_color": [ 101, 176, 234 ], + "texture_top": { "tiling": 0.23, "detiling": 0.55 }, + "texture_side": { "tiling": 0.2, "detiling": 0.9 }, + "texture_bottom": { "tiling": 0.23, "detiling": 0.6 } + }, + { + "name": "Snow", + "base_color": [ 195, 199, 218 ], + "texture": { "tiling": 0.3, "detiling": 0.3 } + }, + { + "name": "Sandstone", + "base_color": [ 137, 90, 71 ], + "texture_top": { "tiling": 0.5, "detiling": 0.7, "rotation": 0.5 }, + "texture_side": { "tiling": 0.3, "detiling": 0.5003 }, + "texture_bottom": { "tiling": 0.45, "detiling": 0.45 } + }, + { + "name": "Mud", + "base_color": [ 58, 46, 36 ], + "texture": { "tiling": 0.3, "detiling": 0.1 } + }, + { + "name": "Basalt", + "base_color": [ 30, 30, 37 ], + "shift": 0.2, + "type": "hardsoft", + "texture": { "tiling": 0.55, "detiling": 0.55 } + }, + { + "name": "Ground", + "base_color": [ 102, 92, 59 ], + "texture": { "tiling": 0.3, "detiling": 0.3, "rotation": 0.5 } + }, + { + "name": "CrackedLava", + "base_color": [ 232, 156, 74 ], + "shift": 0.1, + "texture": { "tiling": 0.28, "detiling": 0.3, "rotation": 0.5 } + }, + { + "name": "Asphalt", + "base_color": [ 115, 123, 107 ], + "texture_top": { "tiling": 0.25, "detiling": 0.23 }, + "texture_side": { "tiling": 0.2, "detiling": 0.9 } + }, + { + "name": "Cobblestone", + "base_color": [ 132, 123, 90 ], + "quantize": 0.15, + "type": "hardsoft", + "texture_top": { "tiling": 0.23, "detiling": 0.25 }, + "texture_side": { "tiling": 0.2, "detiling": 0.9 } + }, + { + "name": "Ice", + "base_color": [ 129, 194, 224 ], + "shift": 0.15, + "texture_top": { "tiling": 0.25, "detiling": 0.25, "rotation": 0.1 }, + "texture_side": { "tiling": 0.25, "detiling": 0.25 } + }, + { + "name": "LeafyGrass", + "base_color": [ 115, 132, 74 ], + "texture_top": { "tiling": 0.25, "detiling": 0.33, "rotation": 1 }, + "texture_side": { "tiling": 0.25, "detiling": 0.25 } + }, + { + "name": "Salt", + "base_color": [ 198, 189, 181 ], + "texture_top": { "tiling": 0.23, "detiling": 0.55 }, + "texture_side": { "tiling": 0.2, "detiling": 0.9} + }, + { + "name": "Limestone", + "base_color": [ 206, 173, 148 ], + "texture_top": { "tiling": 0.25, "detiling": 0.55, "rotation": 0.1 }, + "texture_side": { "tiling": 0.25, "detiling": 0.5 } + }, + { + "name": "Pavement", + "base_color": [ 148, 148, 140 ], + "quantize": 0.25, + "type": "hardsoft", + "texture_top": { "tiling": 0.46, "detiling": 0.25 }, + "texture_side": { "tiling": 0.46, "detiling": 0.25 } + } + ] +} diff --git a/Client2018/platformcontent/pc/terrain/normal.dds b/Client2018/platformcontent/pc/terrain/normal.dds new file mode 100644 index 0000000..7da4097 Binary files /dev/null and b/Client2018/platformcontent/pc/terrain/normal.dds differ diff --git a/Client2018/platformcontent/pc/terrain/normalarray.dds b/Client2018/platformcontent/pc/terrain/normalarray.dds new file mode 100644 index 0000000..5d3417b Binary files /dev/null and b/Client2018/platformcontent/pc/terrain/normalarray.dds differ diff --git a/Client2018/platformcontent/pc/terrain/specular.dds b/Client2018/platformcontent/pc/terrain/specular.dds new file mode 100644 index 0000000..8c2d0bf Binary files /dev/null and b/Client2018/platformcontent/pc/terrain/specular.dds differ diff --git a/Client2018/platformcontent/pc/terrain/speculararray.dds b/Client2018/platformcontent/pc/terrain/speculararray.dds new file mode 100644 index 0000000..0440b68 Binary files /dev/null and b/Client2018/platformcontent/pc/terrain/speculararray.dds differ diff --git a/Client2018/platformcontent/pc/textures/aluminum/diffuse.dds b/Client2018/platformcontent/pc/textures/aluminum/diffuse.dds new file mode 100644 index 0000000..fee64f0 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/aluminum/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/aluminum/normal.dds b/Client2018/platformcontent/pc/textures/aluminum/normal.dds new file mode 100644 index 0000000..466fcb4 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/aluminum/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/aluminum/normaldetail.dds b/Client2018/platformcontent/pc/textures/aluminum/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/aluminum/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/aluminum/specular.dds b/Client2018/platformcontent/pc/textures/aluminum/specular.dds new file mode 100644 index 0000000..1cd4a27 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/aluminum/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/brick/diffuse.dds b/Client2018/platformcontent/pc/textures/brick/diffuse.dds new file mode 100644 index 0000000..7f3af23 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/brick/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/brick/normal.dds b/Client2018/platformcontent/pc/textures/brick/normal.dds new file mode 100644 index 0000000..1548f2a Binary files /dev/null and b/Client2018/platformcontent/pc/textures/brick/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/brick/normaldetail.dds b/Client2018/platformcontent/pc/textures/brick/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/brick/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/brick/specular.dds b/Client2018/platformcontent/pc/textures/brick/specular.dds new file mode 100644 index 0000000..0ff01dd Binary files /dev/null and b/Client2018/platformcontent/pc/textures/brick/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/cobblestone/diffuse.dds b/Client2018/platformcontent/pc/textures/cobblestone/diffuse.dds new file mode 100644 index 0000000..1e1eab9 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/cobblestone/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/cobblestone/normal.dds b/Client2018/platformcontent/pc/textures/cobblestone/normal.dds new file mode 100644 index 0000000..2cfc1ad Binary files /dev/null and b/Client2018/platformcontent/pc/textures/cobblestone/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/cobblestone/normaldetail.dds b/Client2018/platformcontent/pc/textures/cobblestone/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/cobblestone/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/cobblestone/specular.dds b/Client2018/platformcontent/pc/textures/cobblestone/specular.dds new file mode 100644 index 0000000..6d14457 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/cobblestone/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/concrete/diffuse.dds b/Client2018/platformcontent/pc/textures/concrete/diffuse.dds new file mode 100644 index 0000000..2174765 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/concrete/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/concrete/normal.dds b/Client2018/platformcontent/pc/textures/concrete/normal.dds new file mode 100644 index 0000000..dfd9bb8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/concrete/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/concrete/normaldetail.dds b/Client2018/platformcontent/pc/textures/concrete/normaldetail.dds new file mode 100644 index 0000000..5c11a43 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/concrete/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/concrete/specular.dds b/Client2018/platformcontent/pc/textures/concrete/specular.dds new file mode 100644 index 0000000..bfeb384 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/concrete/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/diamondplate/diffuse.dds b/Client2018/platformcontent/pc/textures/diamondplate/diffuse.dds new file mode 100644 index 0000000..e4f5f3a Binary files /dev/null and b/Client2018/platformcontent/pc/textures/diamondplate/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/diamondplate/normal.dds b/Client2018/platformcontent/pc/textures/diamondplate/normal.dds new file mode 100644 index 0000000..88ece5f Binary files /dev/null and b/Client2018/platformcontent/pc/textures/diamondplate/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/diamondplate/normaldetail.dds b/Client2018/platformcontent/pc/textures/diamondplate/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/diamondplate/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/diamondplate/specular.dds b/Client2018/platformcontent/pc/textures/diamondplate/specular.dds new file mode 100644 index 0000000..9154362 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/diamondplate/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/fabric/diffuse.dds b/Client2018/platformcontent/pc/textures/fabric/diffuse.dds new file mode 100644 index 0000000..a0efd50 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/fabric/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/fabric/normal.dds b/Client2018/platformcontent/pc/textures/fabric/normal.dds new file mode 100644 index 0000000..4025a92 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/fabric/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/fabric/normaldetail.dds b/Client2018/platformcontent/pc/textures/fabric/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/fabric/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/fabric/specular.dds b/Client2018/platformcontent/pc/textures/fabric/specular.dds new file mode 100644 index 0000000..e3c973d Binary files /dev/null and b/Client2018/platformcontent/pc/textures/fabric/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/glass/diffuse.dds b/Client2018/platformcontent/pc/textures/glass/diffuse.dds new file mode 100644 index 0000000..76365f7 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/glass/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/glass/normal.dds b/Client2018/platformcontent/pc/textures/glass/normal.dds new file mode 100644 index 0000000..2159af6 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/glass/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/glass/normaldetail.dds b/Client2018/platformcontent/pc/textures/glass/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/glass/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/glass/specular.dds b/Client2018/platformcontent/pc/textures/glass/specular.dds new file mode 100644 index 0000000..9bba956 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/glass/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/granite/diffuse.dds b/Client2018/platformcontent/pc/textures/granite/diffuse.dds new file mode 100644 index 0000000..bd67946 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/granite/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/granite/normal.dds b/Client2018/platformcontent/pc/textures/granite/normal.dds new file mode 100644 index 0000000..1a168d9 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/granite/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/granite/normaldetail.dds b/Client2018/platformcontent/pc/textures/granite/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/granite/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/granite/specular.dds b/Client2018/platformcontent/pc/textures/granite/specular.dds new file mode 100644 index 0000000..aba04d5 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/granite/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/grass/diffuse.dds b/Client2018/platformcontent/pc/textures/grass/diffuse.dds new file mode 100644 index 0000000..c526c08 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/grass/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/grass/normal.dds b/Client2018/platformcontent/pc/textures/grass/normal.dds new file mode 100644 index 0000000..ba00e17 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/grass/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/grass/normaldetail.dds b/Client2018/platformcontent/pc/textures/grass/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/grass/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/grass/specular.dds b/Client2018/platformcontent/pc/textures/grass/specular.dds new file mode 100644 index 0000000..eb3fcbb Binary files /dev/null and b/Client2018/platformcontent/pc/textures/grass/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/ice/diffuse.dds b/Client2018/platformcontent/pc/textures/ice/diffuse.dds new file mode 100644 index 0000000..fee64f0 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/ice/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/ice/normal.dds b/Client2018/platformcontent/pc/textures/ice/normal.dds new file mode 100644 index 0000000..8752407 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/ice/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/ice/normaldetail.dds b/Client2018/platformcontent/pc/textures/ice/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/ice/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/ice/specular.dds b/Client2018/platformcontent/pc/textures/ice/specular.dds new file mode 100644 index 0000000..4689e14 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/ice/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/marble/diffuse.dds b/Client2018/platformcontent/pc/textures/marble/diffuse.dds new file mode 100644 index 0000000..ca63066 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/marble/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/marble/normal.dds b/Client2018/platformcontent/pc/textures/marble/normal.dds new file mode 100644 index 0000000..211ce43 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/marble/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/marble/normaldetail.dds b/Client2018/platformcontent/pc/textures/marble/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/marble/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/marble/specular.dds b/Client2018/platformcontent/pc/textures/marble/specular.dds new file mode 100644 index 0000000..5902a8d Binary files /dev/null and b/Client2018/platformcontent/pc/textures/marble/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/metal/diffuse.dds b/Client2018/platformcontent/pc/textures/metal/diffuse.dds new file mode 100644 index 0000000..72ac322 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/metal/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/metal/normal.dds b/Client2018/platformcontent/pc/textures/metal/normal.dds new file mode 100644 index 0000000..c4e5757 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/metal/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/metal/normaldetail.dds b/Client2018/platformcontent/pc/textures/metal/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/metal/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/metal/specular.dds b/Client2018/platformcontent/pc/textures/metal/specular.dds new file mode 100644 index 0000000..98a9f60 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/metal/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/noise.dds b/Client2018/platformcontent/pc/textures/noise.dds new file mode 100644 index 0000000..9e6d65f Binary files /dev/null and b/Client2018/platformcontent/pc/textures/noise.dds differ diff --git a/Client2018/platformcontent/pc/textures/pebble/diffuse.dds b/Client2018/platformcontent/pc/textures/pebble/diffuse.dds new file mode 100644 index 0000000..48b56cc Binary files /dev/null and b/Client2018/platformcontent/pc/textures/pebble/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/pebble/normal.dds b/Client2018/platformcontent/pc/textures/pebble/normal.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/pebble/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/pebble/normaldetail.dds b/Client2018/platformcontent/pc/textures/pebble/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/pebble/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/pebble/specular.dds b/Client2018/platformcontent/pc/textures/pebble/specular.dds new file mode 100644 index 0000000..bfeb384 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/pebble/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/plastic/diffuse.dds b/Client2018/platformcontent/pc/textures/plastic/diffuse.dds new file mode 100644 index 0000000..d4850dd Binary files /dev/null and b/Client2018/platformcontent/pc/textures/plastic/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/plastic/normal.dds b/Client2018/platformcontent/pc/textures/plastic/normal.dds new file mode 100644 index 0000000..3c2bf19 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/plastic/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/plastic/normaldetail.dds b/Client2018/platformcontent/pc/textures/plastic/normaldetail.dds new file mode 100644 index 0000000..29140fd Binary files /dev/null and b/Client2018/platformcontent/pc/textures/plastic/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/rust/diffuse.dds b/Client2018/platformcontent/pc/textures/rust/diffuse.dds new file mode 100644 index 0000000..59a4221 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/rust/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/rust/normal.dds b/Client2018/platformcontent/pc/textures/rust/normal.dds new file mode 100644 index 0000000..7d15959 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/rust/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/rust/normaldetail.dds b/Client2018/platformcontent/pc/textures/rust/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/rust/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/rust/specular.dds b/Client2018/platformcontent/pc/textures/rust/specular.dds new file mode 100644 index 0000000..9c3133e Binary files /dev/null and b/Client2018/platformcontent/pc/textures/rust/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/sand/diffuse.dds b/Client2018/platformcontent/pc/textures/sand/diffuse.dds new file mode 100644 index 0000000..5b91413 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/sand/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/sand/normal.dds b/Client2018/platformcontent/pc/textures/sand/normal.dds new file mode 100644 index 0000000..c8a2c84 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/sand/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/sand/normaldetail.dds b/Client2018/platformcontent/pc/textures/sand/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/sand/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/sand/specular.dds b/Client2018/platformcontent/pc/textures/sand/specular.dds new file mode 100644 index 0000000..6efec1b Binary files /dev/null and b/Client2018/platformcontent/pc/textures/sand/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/sky/sky512_bk.tex b/Client2018/platformcontent/pc/textures/sky/sky512_bk.tex new file mode 100644 index 0000000..fcdd232 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/sky/sky512_bk.tex differ diff --git a/Client2018/platformcontent/pc/textures/sky/sky512_dn.tex b/Client2018/platformcontent/pc/textures/sky/sky512_dn.tex new file mode 100644 index 0000000..d63e52d Binary files /dev/null and b/Client2018/platformcontent/pc/textures/sky/sky512_dn.tex differ diff --git a/Client2018/platformcontent/pc/textures/sky/sky512_ft.tex b/Client2018/platformcontent/pc/textures/sky/sky512_ft.tex new file mode 100644 index 0000000..215b51b Binary files /dev/null and b/Client2018/platformcontent/pc/textures/sky/sky512_ft.tex differ diff --git a/Client2018/platformcontent/pc/textures/sky/sky512_lf.tex b/Client2018/platformcontent/pc/textures/sky/sky512_lf.tex new file mode 100644 index 0000000..a096acf Binary files /dev/null and b/Client2018/platformcontent/pc/textures/sky/sky512_lf.tex differ diff --git a/Client2018/platformcontent/pc/textures/sky/sky512_rt.tex b/Client2018/platformcontent/pc/textures/sky/sky512_rt.tex new file mode 100644 index 0000000..6c0aaf1 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/sky/sky512_rt.tex differ diff --git a/Client2018/platformcontent/pc/textures/sky/sky512_up.tex b/Client2018/platformcontent/pc/textures/sky/sky512_up.tex new file mode 100644 index 0000000..a973654 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/sky/sky512_up.tex differ diff --git a/Client2018/platformcontent/pc/textures/slate/diffuse.dds b/Client2018/platformcontent/pc/textures/slate/diffuse.dds new file mode 100644 index 0000000..ff7ca6d Binary files /dev/null and b/Client2018/platformcontent/pc/textures/slate/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/slate/normal.dds b/Client2018/platformcontent/pc/textures/slate/normal.dds new file mode 100644 index 0000000..6f38346 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/slate/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/slate/normaldetail.dds b/Client2018/platformcontent/pc/textures/slate/normaldetail.dds new file mode 100644 index 0000000..7e6b0ca Binary files /dev/null and b/Client2018/platformcontent/pc/textures/slate/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/slate/specular.dds b/Client2018/platformcontent/pc/textures/slate/specular.dds new file mode 100644 index 0000000..271b6c1 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/slate/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/studs.dds b/Client2018/platformcontent/pc/textures/studs.dds new file mode 100644 index 0000000..6ff77fb Binary files /dev/null and b/Client2018/platformcontent/pc/textures/studs.dds differ diff --git a/Client2018/platformcontent/pc/textures/wangIndex.dds b/Client2018/platformcontent/pc/textures/wangIndex.dds new file mode 100644 index 0000000..e613ff1 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/wangIndex.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_01.dds b/Client2018/platformcontent/pc/textures/water/normal_01.dds new file mode 100644 index 0000000..e5b879d Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_01.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_02.dds b/Client2018/platformcontent/pc/textures/water/normal_02.dds new file mode 100644 index 0000000..1398f8e Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_02.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_03.dds b/Client2018/platformcontent/pc/textures/water/normal_03.dds new file mode 100644 index 0000000..16c5123 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_03.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_04.dds b/Client2018/platformcontent/pc/textures/water/normal_04.dds new file mode 100644 index 0000000..c0c079f Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_04.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_05.dds b/Client2018/platformcontent/pc/textures/water/normal_05.dds new file mode 100644 index 0000000..5b1442c Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_05.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_06.dds b/Client2018/platformcontent/pc/textures/water/normal_06.dds new file mode 100644 index 0000000..dcc0e0a Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_06.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_07.dds b/Client2018/platformcontent/pc/textures/water/normal_07.dds new file mode 100644 index 0000000..2f32ea0 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_07.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_08.dds b/Client2018/platformcontent/pc/textures/water/normal_08.dds new file mode 100644 index 0000000..1eeb6af Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_08.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_09.dds b/Client2018/platformcontent/pc/textures/water/normal_09.dds new file mode 100644 index 0000000..06ee34c Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_09.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_10.dds b/Client2018/platformcontent/pc/textures/water/normal_10.dds new file mode 100644 index 0000000..16ffff0 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_10.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_11.dds b/Client2018/platformcontent/pc/textures/water/normal_11.dds new file mode 100644 index 0000000..177238c Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_11.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_12.dds b/Client2018/platformcontent/pc/textures/water/normal_12.dds new file mode 100644 index 0000000..250c48d Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_12.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_13.dds b/Client2018/platformcontent/pc/textures/water/normal_13.dds new file mode 100644 index 0000000..a3e06ae Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_13.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_14.dds b/Client2018/platformcontent/pc/textures/water/normal_14.dds new file mode 100644 index 0000000..dabe7ee Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_14.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_15.dds b/Client2018/platformcontent/pc/textures/water/normal_15.dds new file mode 100644 index 0000000..a0c2646 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_15.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_16.dds b/Client2018/platformcontent/pc/textures/water/normal_16.dds new file mode 100644 index 0000000..f3dd37c Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_16.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_17.dds b/Client2018/platformcontent/pc/textures/water/normal_17.dds new file mode 100644 index 0000000..695042b Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_17.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_18.dds b/Client2018/platformcontent/pc/textures/water/normal_18.dds new file mode 100644 index 0000000..15c38f3 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_18.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_19.dds b/Client2018/platformcontent/pc/textures/water/normal_19.dds new file mode 100644 index 0000000..40cdd00 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_19.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_20.dds b/Client2018/platformcontent/pc/textures/water/normal_20.dds new file mode 100644 index 0000000..e427daf Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_20.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_21.dds b/Client2018/platformcontent/pc/textures/water/normal_21.dds new file mode 100644 index 0000000..f26a7eb Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_21.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_22.dds b/Client2018/platformcontent/pc/textures/water/normal_22.dds new file mode 100644 index 0000000..6d6af46 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_22.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_23.dds b/Client2018/platformcontent/pc/textures/water/normal_23.dds new file mode 100644 index 0000000..f6f354c Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_23.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_24.dds b/Client2018/platformcontent/pc/textures/water/normal_24.dds new file mode 100644 index 0000000..bc2804c Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_24.dds differ diff --git a/Client2018/platformcontent/pc/textures/water/normal_25.dds b/Client2018/platformcontent/pc/textures/water/normal_25.dds new file mode 100644 index 0000000..6ea49fb Binary files /dev/null and b/Client2018/platformcontent/pc/textures/water/normal_25.dds differ diff --git a/Client2018/platformcontent/pc/textures/wood/diffuse.dds b/Client2018/platformcontent/pc/textures/wood/diffuse.dds new file mode 100644 index 0000000..941b70f Binary files /dev/null and b/Client2018/platformcontent/pc/textures/wood/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/wood/normal.dds b/Client2018/platformcontent/pc/textures/wood/normal.dds new file mode 100644 index 0000000..aa3190c Binary files /dev/null and b/Client2018/platformcontent/pc/textures/wood/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/wood/normaldetail.dds b/Client2018/platformcontent/pc/textures/wood/normaldetail.dds new file mode 100644 index 0000000..5630e27 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/wood/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/wood/specular.dds b/Client2018/platformcontent/pc/textures/wood/specular.dds new file mode 100644 index 0000000..f5a8470 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/wood/specular.dds differ diff --git a/Client2018/platformcontent/pc/textures/woodplanks/diffuse.dds b/Client2018/platformcontent/pc/textures/woodplanks/diffuse.dds new file mode 100644 index 0000000..502b627 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/woodplanks/diffuse.dds differ diff --git a/Client2018/platformcontent/pc/textures/woodplanks/normal.dds b/Client2018/platformcontent/pc/textures/woodplanks/normal.dds new file mode 100644 index 0000000..760869f Binary files /dev/null and b/Client2018/platformcontent/pc/textures/woodplanks/normal.dds differ diff --git a/Client2018/platformcontent/pc/textures/woodplanks/normaldetail.dds b/Client2018/platformcontent/pc/textures/woodplanks/normaldetail.dds new file mode 100644 index 0000000..818b2a8 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/woodplanks/normaldetail.dds differ diff --git a/Client2018/platformcontent/pc/textures/woodplanks/specular.dds b/Client2018/platformcontent/pc/textures/woodplanks/specular.dds new file mode 100644 index 0000000..862b193 Binary files /dev/null and b/Client2018/platformcontent/pc/textures/woodplanks/specular.dds differ diff --git a/Client2018/shaders/keepme b/Client2018/shaders/keepme new file mode 100644 index 0000000..e69de29 diff --git a/Client2018/shaders/shaders_d3d10.pack b/Client2018/shaders/shaders_d3d10.pack new file mode 100644 index 0000000..0503e8d Binary files /dev/null and b/Client2018/shaders/shaders_d3d10.pack differ diff --git a/Client2018/shaders/shaders_d3d10_1.pack b/Client2018/shaders/shaders_d3d10_1.pack new file mode 100644 index 0000000..523548a Binary files /dev/null and b/Client2018/shaders/shaders_d3d10_1.pack differ diff --git a/Client2018/shaders/shaders_d3d11.pack b/Client2018/shaders/shaders_d3d11.pack new file mode 100644 index 0000000..1fd5b21 Binary files /dev/null and b/Client2018/shaders/shaders_d3d11.pack differ diff --git a/Client2018/shaders/shaders_d3d9.pack b/Client2018/shaders/shaders_d3d9.pack new file mode 100644 index 0000000..f72e47a Binary files /dev/null and b/Client2018/shaders/shaders_d3d9.pack differ diff --git a/Client2018/shaders/shaders_glsl.pack b/Client2018/shaders/shaders_glsl.pack new file mode 100644 index 0000000..3aaa9b6 Binary files /dev/null and b/Client2018/shaders/shaders_glsl.pack differ diff --git a/Client2018/shaders/shaders_glsl3.pack b/Client2018/shaders/shaders_glsl3.pack new file mode 100644 index 0000000..88013c8 Binary files /dev/null and b/Client2018/shaders/shaders_glsl3.pack differ diff --git a/Client2018/shaders/shaders_glsl3_old.pack b/Client2018/shaders/shaders_glsl3_old.pack new file mode 100644 index 0000000..5cf2c72 Binary files /dev/null and b/Client2018/shaders/shaders_glsl3_old.pack differ diff --git a/Client2018/shaders/shaders_glsl_old.pack b/Client2018/shaders/shaders_glsl_old.pack new file mode 100644 index 0000000..8eda1aa Binary files /dev/null and b/Client2018/shaders/shaders_glsl_old.pack differ diff --git a/Client2018/shaders/shaders_vulkan_desktop.pack b/Client2018/shaders/shaders_vulkan_desktop.pack new file mode 100644 index 0000000..47ae281 Binary files /dev/null and b/Client2018/shaders/shaders_vulkan_desktop.pack differ diff --git a/Client2018/shaders/shaders_vulkan_desktop_old.pack b/Client2018/shaders/shaders_vulkan_desktop_old.pack new file mode 100644 index 0000000..0672d18 Binary files /dev/null and b/Client2018/shaders/shaders_vulkan_desktop_old.pack differ diff --git a/Client2020/AppSettings.xml b/Client2020/AppSettings.xml new file mode 100644 index 0000000..cd30ce0 --- /dev/null +++ b/Client2020/AppSettings.xml @@ -0,0 +1,5 @@ + + + content + http://www.syntax.eco + diff --git a/Client2020/ExtraContent/LuaPackages/.luacheckrc b/Client2020/ExtraContent/LuaPackages/.luacheckrc new file mode 100644 index 0000000..5f5cf25 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/.luacheckrc @@ -0,0 +1,41 @@ +stds.roblox = { + globals = { + "game" + }, + read_globals = { + -- Roblox globals + "script", + + -- Extra functions + "tick", "warn", "spawn", + "wait", "settings", "typeof", "delay", + + -- Types + "Vector2", "Vector3", + "Color3", + "UDim", "UDim2", + "Rect", + "CFrame", + "Enum", + "Instance", + } +} + +stds.testez = { + read_globals = { + "describe", + "it", "itFOCUS", "itSKIP", + "FOCUS", "SKIP", "HACK_NO_XPCALL", + "expect", + } +} + +ignore = { + "212", -- unused arguments +} + +std = "lua51+roblox" + +files["**/*.spec.lua"] = { + std = "+testez", +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/.robloxrc b/Client2020/ExtraContent/LuaPackages/.robloxrc new file mode 100644 index 0000000..37c60d8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/.robloxrc @@ -0,0 +1,17 @@ +{ + "language": { + "mode": "noinfer" + }, + "lint": { + "MultiLineStatement": "disabled", + "DeprecatedGlobal": "disabled", + "LocalShadow": "disabled", + "LocalUnused": "disabled", + "FunctionUnused": "fatal", + "ImportUnused": "disabled", + "SameLineStatement": "fatal", + "ImplicitReturn": "disabled", + "UnknownGlobal": "fatal", + "GlobalUsedAsLocal": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Analytics/Analytics.lua b/Client2020/ExtraContent/LuaPackages/Analytics/Analytics.lua new file mode 100644 index 0000000..8bc5a5b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Analytics/Analytics.lua @@ -0,0 +1,56 @@ +--[[ + A centralized hub for basic metrics reporting. + This class is designed to provide a baseline exposure to the reporters. + + Context specific Analytics.lua objects should be created in sub-projects, + to report specific actions like chat interactions, or game page interactions. + + Analytics.lua and the reporters here in Common should serve to cover the + most common interactions. +]] + +local AnalyticsService = game:GetService("RbxAnalyticsService") +local Reporters = script.Parent.AnalyticsReporters + +local DiagReporter = require(Reporters.Diag) +local EventStreamReporter = require(Reporters.EventStream) +local GoogleAnalyticsReporter = require(Reporters.GoogleAnalytics) +local InfluxDbReporter = require(Reporters.Influx) + + +local Analytics = {} +Analytics.__index = Analytics + +-- reportingService : (Service, optional) an object that exposes the same functions as AnalyticsService +function Analytics.new(reportingService) + if not reportingService then + reportingService = AnalyticsService + end + + -- All public reporting functions are exposed by the objects defined in the properties + local self = {} + self.Diag = DiagReporter.new(reportingService) + self.EventStream = EventStreamReporter.new(reportingService) + self.GoogleAnalytics = GoogleAnalyticsReporter.new(reportingService) + self.InfluxDb = InfluxDbReporter.new(reportingService) + + setmetatable(self, Analytics) + + return self +end + +function Analytics.mock() + -- create a reporting service that does not fire any requests out to the world + local fakeReportingService = {} + function fakeReportingService.ReportCounter() end + function fakeReportingService.ReportInfluxSeries() end + function fakeReportingService.ReportStats() end + function fakeReportingService.SetRBXEvent() end + function fakeReportingService.SetRBXEventStream() end + function fakeReportingService.TrackEvent() end + function fakeReportingService.UpdateHeartbeatObject() end + + return Analytics.new(fakeReportingService) +end + +return Analytics diff --git a/Client2020/ExtraContent/LuaPackages/Analytics/Analytics.spec.lua b/Client2020/ExtraContent/LuaPackages/Analytics/Analytics.spec.lua new file mode 100644 index 0000000..794b160 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Analytics/Analytics.spec.lua @@ -0,0 +1,69 @@ +return function() + local Analytics = require(script.Parent.Analytics) + + describe("new()", function() + it("should properly construct a new object", function() + local na = Analytics.new() + expect(na).to.be.ok() + end) + + it("should accept a custom reporting service", function() + local fakeService = {} + local na = Analytics.new(fakeService) + expect(na).to.be.ok() + end) + + it("should have a reporter specifically for Diag", function() + local na = Analytics.new() + expect(na.Diag).to.be.ok() + end) + + it("should have a reporter specifically for RBXEventStream", function() + local na = Analytics.new() + expect(na.EventStream).to.be.ok() + end) + + it("should have a reporter specifically for Google Analytics", function() + local na = Analytics.new() + expect(na.GoogleAnalytics).to.be.ok() + end) + + it("should have a reporter specifically for Influx", function() + local na = Analytics.new() + expect(na.InfluxDb).to.be.ok() + end) + end) + + describe("mock()", function() + it("should properly construct a new object", function() + local ma = Analytics.mock() + expect(ma).to.be.ok() + expect(ma.Diag).to.be.ok() + expect(ma.EventStream).to.be.ok() + expect(ma.GoogleAnalytics).to.be.ok() + expect(ma.InfluxDb).to.be.ok() + end) + + it("should succeed for all function calls in Diag", function() + local ma = Analytics.mock() + ma.Diag:reportCounter("fakeCounter", 1) + ma.Diag:reportStats("fakeCategory", 1) + end) + + it("should succeed for all function call in EventStream", function() + local ma = Analytics.mock() + ma.EventStream:setRBXEvent("fakeContext", "fakeEventName") + ma.EventStream:setRBXEventStream("fakeContext", "fakeEventName") + end) + + it("should succeed for all function call in GoogleAnalytics", function() + local ma = Analytics.mock() + ma.GoogleAnalytics:trackEvent("fakeCategory", "fakeAction", "fakeLabel") + end) + + it("should succeed for all function call in Influx", function() + local ma = Analytics.mock() + ma.InfluxDb:reportSeries("fakeSeries", {}, 1) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Diag.lua b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Diag.lua new file mode 100644 index 0000000..b33b490 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Diag.lua @@ -0,0 +1,64 @@ +--[[ + Specialized analytics reporter for ephemeral counters. + Useful for tracking at-a-glance health of a feature. +]] + +local UserInputService = game:GetService("UserInputService") + +local Diag = {} +Diag.__index = Diag + +-- reportingService - (object) any object that defines the same functions for Diag as AnalyticsService +function Diag.new(reportingService) + local rsType = type(reportingService) + assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService") + + local self = { + _reporter = reportingService, + _isEnabled = true, + } + setmetatable(self, Diag) + + return self +end + +-- isEnabled : (boolean) +function Diag:setEnabled(isEnabled) + assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean") + self._isEnabled = isEnabled +end + +-- counterName : (string) the name of the ephemeral counter to increment +-- amount : (int) the value to increment the counter by +function Diag:reportCounter(counterName, amount) + assert(type(counterName) == "string", "Expected counterName to be a string") + assert(type(amount) == "number", "Expected amount to be a number") + assert(self._isEnabled, "This reporting service is disabled") + + -- use special naming convention for Xbox counters + -- the call to GetPlatform is wrapped in a pcall() because the Testing Service + -- executes the scripts in the wrong authorization level + local platformID = Enum.Platform.None + pcall(function() + platformID = UserInputService:GetPlatform() + end) + if platformID == Enum.Platform.XBoxOne then + counterName = "Xbox-" .. tostring(counterName) + end + + -- the AnalyticsService should automatically handle batch reporting + self._reporter:ReportCounter(counterName, amount) +end + + +-- category : (string) the name of the statistics buffer which to append the data +-- value : (number) any type of numeric value +function Diag:reportStats(category, value) + assert(type(category) == "string", "Expected category to be a string") + assert(type(value) == "number", "Expected value to be a number") + assert(self._isEnabled, "This reporting service is disabled") + + self._reporter:ReportStats(category, value) +end + +return Diag diff --git a/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Diag.spec.lua b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Diag.spec.lua new file mode 100644 index 0000000..6dadff2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Diag.spec.lua @@ -0,0 +1,176 @@ +return function() + local Diag = require(script.Parent.Diag) + + local testCounterName = "testCounter" + local testCounterAmount = 1 + local testCategoryName = "testCategory" + local testCategoryValue = 98 + + local badTestCounterName = 5 + local badTestCounterAmount = "hello" + local badTestCategoryName = {} + local badTestCategoryValue = {} + + local DebugReportingService = {} + function DebugReportingService:ReportCounter(counterName, amount) + assert(counterName == testCounterName, "Unexpected value for counterName: " .. counterName) + assert(amount == testCounterAmount, "Unexpected value for amount: " .. amount) + end + function DebugReportingService:ReportStats(categoryName, value) + assert(categoryName == testCategoryName, "Unexpected value for category: " .. categoryName) + if value then + assert(value == testCategoryValue, "Unexpected value for value: " .. value) + end + end + + + describe("new()", function() + it("should construct with a Reporting Service", function() + local diag = Diag.new(DebugReportingService) + expect(diag).to.be.ok() + end) + + it("should throw an error to be constructed without a Reporting Service", function() + expect(function() + Diag.new(nil) + end).to.throw() + end) + end) + + describe("setEnabled()", function() + it("should succeed with valid input", function() + local diag = Diag.new(DebugReportingService) + diag:setEnabled(false) + diag:setEnabled(true) + end) + it("should disable the reporter", function() + local diag = Diag.new(DebugReportingService) + diag:setEnabled(false) + expect(function() + diag:reportCounter(testCounterName, testCounterAmount) + end).to.throw() + end) + end) + + describe("reportCounter()", function() + it("should work when appropriately enabled / disabled", function() + local diag = Diag.new(DebugReportingService) + + expect(function() + diag:setEnabled(false) + diag:reportCounter(testCounterName, testCounterAmount) + end).to.throw() + + diag:setEnabled(true) + diag:reportCounter(testCounterName, testCounterAmount) + end) + + it("should succeed with valid input", function() + local diag = Diag.new(DebugReportingService) + diag:reportCounter(testCounterName, testCounterAmount) + end) + + it("should throw an error with invalid input for the counter name", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(badTestCounterName, testCounterAmount) + end).to.throw() + end) + + it("should throw an error with invalid input for the amount", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(testCounterName, badTestCounterAmount) + end).to.throw() + end) + + it("should throw an error with completely invalid input", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(badTestCounterName, badTestCounterAmount) + end).to.throw() + end) + + it("should throw an error if it is missing a counter name", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(nil, testCounterAmount) + end).to.throw() + end) + + it("should throw an error if it is missing an amount", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(testCounterName, nil) + end).to.throw() + end) + + it("should throw an error if it is missing any input", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(nil, nil) + end).to.throw() + end) + end) + + describe("reportStats()", function() + it("should work when appropriately enabled / disabled", function() + local diag = Diag.new(DebugReportingService) + + expect(function() + diag:setEnabled(false) + diag:reportStats(testCategoryName, testCategoryValue) + end).to.throw() + + diag:setEnabled(true) + diag:reportStats(testCategoryName, testCategoryValue) + end) + + it("should succeed with valid input", function() + local diag = Diag.new(DebugReportingService) + diag:reportStats(testCategoryName, testCategoryValue) + end) + + it("should throw an error with invalid input for the category name", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(badTestCategoryName, testCategoryValue) + end).to.throw() + end) + + it("should throw an error with invalid input for the value", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(testCategoryName, badTestCategoryValue) + end).to.throw() + end) + + it("should throw an error with completely invalid input", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(badTestCategoryName, badTestCategoryValue) + end).to.throw() + end) + + it("should throw an error if it is missing a category name", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(nil, testCategoryValue) + end).to.throw() + end) + + it("should throw an error if it is missing a value", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(testCategoryName, nil) + end).to.throw() + end) + + it("should throw an error if it is missing any input", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(nil, nil) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/EventStream.lua b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/EventStream.lua new file mode 100644 index 0000000..5a32390 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/EventStream.lua @@ -0,0 +1,154 @@ +--[[ + Specialized reporter for RBX Event Ingest data. + Useful for tracking explicit user interactions with screens and guis. +]] + +local UserInputService = game:GetService("UserInputService") + +local function getPlatformTarget() + local platformTarget = "unknownLua" + local platformEnum = Enum.Platform.None + + -- the call to GetPlatform is wrapped in a pcall() because the Testing Service + -- executes the scripts in the wrong authorization level + pcall(function() + platformEnum = UserInputService:GetPlatform() + end) + + -- bucket the platform based on consumer platform + local isDesktopClient = (platformEnum == Enum.Platform.Windows) or (platformEnum == Enum.Platform.OSX) + + local isMobileClient = (platformEnum == Enum.Platform.IOS) or (platformEnum == Enum.Platform.Android) + isMobileClient = isMobileClient or (platformEnum == Enum.Platform.UWP) + + local isConsole = (platformEnum == Enum.Platform.XBox360) or (platformEnum == Enum.Platform.XBoxOne) + isConsole = isConsole or (platformEnum == Enum.Platform.PS3) or (platformEnum == Enum.Platform.PS4) + isConsole = isConsole or (platformEnum == Enum.Platform.WiiU) + + -- assign a target based on the form factor + if isDesktopClient then + platformTarget = "client" + elseif isMobileClient then + platformTarget = "mobile" + elseif isConsole then + platformTarget = "console" + else + -- if we don't have a name for the form factor, report it here so that we can eventually track it down + platformTarget = platformTarget .. tostring(platformEnum) + end + + + return platformTarget +end + + +local EventStream = {} +EventStream.__index = EventStream + +-- reportingService - (object) any object that defines the same functions for Event Stream as AnalyticsService +function EventStream.new(reportingService) + local rsType = type(reportingService) + assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService") + + local self = { + _reporter = reportingService, + _isEnabled = true, + } + setmetatable(self, EventStream) + + return self +end + +-- isEnabled : (boolean) +function EventStream:setEnabled(isEnabled) + assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean") + self._isEnabled = isEnabled +end + +-- eventContext : (string) the location or context in which the event is occurring. +-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example. +-- additionalArgs : (optional, map) table for additional information to appear in the event stream. +function EventStream:setRBXEvent(eventContext, eventName, additionalArgs) + local target = getPlatformTarget() + additionalArgs = additionalArgs or {} + + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(eventName) == "string", "Expected eventName to be a string") + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(self._isEnabled, "This reporting service is disabled") + + -- This function fires reports to the server right away + self._reporter:SetRBXEvent(target, eventContext, eventName, additionalArgs) +end + + +-- eventContext : (string) the location or context in which the event is occurring. +-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example. +-- additionalArgs : (optional, map) map for extra keys to appear in the event stream. +function EventStream:setRBXEventStream(eventContext, eventName, additionalArgs) + local target = getPlatformTarget() + additionalArgs = additionalArgs or {} + + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(eventName) == "string", "Expected eventName to be a string") + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(self._isEnabled, "This reporting service is disabled") + + -- this function sends reports to the server in batches, not real-time + self._reporter:SetRBXEventStream(target, eventContext, eventName, additionalArgs) +end + + +function EventStream:releaseRBXEventStream() + assert(self._isEnabled, "This reporting service is disabled") + + self._reporter:ReleaseRBXEventStream(getPlatformTarget()) +end + + +-- eventContext : (string) the location or context in which the event is occurring. +-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example. +-- additionalArgs : (optional, map) map for extra keys to appear in the event stream. +function EventStream:sendEventDeferred(eventContext, eventName, additionalArgs) + local target = getPlatformTarget() + additionalArgs = additionalArgs or {} + + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(eventName) == "string", "Expected eventName to be a string") + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(self._isEnabled, "This reporting service is disabled") + + -- this function sends reports to the server in batches, not real-time + self._reporter:SendEventDeferred(target, eventContext, eventName, additionalArgs) +end + + +-- eventContext : (string) the location or context in which the event is occurring. +-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example. +-- additionalArgs : (optional, map) map for extra keys to appear in the event stream. +function EventStream:sendEventImmediately(eventContext, eventName, additionalArgs) + local target = getPlatformTarget() + additionalArgs = additionalArgs or {} + + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(eventName) == "string", "Expected eventName to be a string") + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(self._isEnabled, "This reporting service is disabled") + + -- this function sends reports to the server in batches, not real-time + self._reporter:SendEventImmediately(target, eventContext, eventName, additionalArgs) +end + + +-- additionalArgs : (optional, map) table for extra keys to appear in the event stream. +function EventStream:updateHeartbeatObject(additionalArgs) + additionalArgs = additionalArgs or {} + + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(self._isEnabled, "This reporting service is disabled") + + self._reporter:UpdateHeartbeatObject(additionalArgs) +end + + +return EventStream diff --git a/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/EventStream.spec.lua b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/EventStream.spec.lua new file mode 100644 index 0000000..597b6fc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/EventStream.spec.lua @@ -0,0 +1,393 @@ +return function() + + local EventStream = require(script.Parent.EventStream) + + local testArgs = { + testKey = "testValue" + } + local testContext = "testContext" + local testEvent = "testEventName" + local badTestArgs = "hello" + local badTestContext = {} + local badTestEvent = {} + + + local function isTableEqual(table1, table2) + if table1 == table2 then + return true + end + + if type(table1) ~= "table" then + return false + end + + if type(table2) ~= "table" then + return false + end + + for key, _ in pairs(table1) do + if table1[key] ~= table2[key] then + return false + end + end + for key, _ in pairs(table2) do + if table2[key] ~= table1[key] then + return false + end + end + + return true + end + + local function createDebugReportingService() + local function validateInputs(eventTarget, eventContext, eventName, additionalArgs) + assert(eventTarget, "no value found for eventTarget") + assert(eventContext == testContext, "unexpected value for eventContext : " .. eventContext) + assert(eventName == testEvent, "unexpected value for eventName : " .. eventName) + if additionalArgs and not isTableEqual(additionalArgs, {}) then + assert(isTableEqual(additionalArgs, testArgs), "unexpected value for additionalArgs") + end + end + + local DebugReportingService = {} + function DebugReportingService:SetRBXEvent(eventTarget, eventContext, eventName, additionalArgs) + validateInputs(eventTarget, eventContext, eventName, additionalArgs) + end + function DebugReportingService:SetRBXEventStream(eventTarget, eventContext, eventName, additionalArgs) + validateInputs(eventTarget, eventContext, eventName, additionalArgs) + end + function DebugReportingService:SendEventImmediately(eventTarget, eventContext, eventName, additionalArgs) + validateInputs(eventTarget, eventContext, eventName, additionalArgs) + end + function DebugReportingService:SendEventDeferred(eventTarget, eventContext, eventName, additionalArgs) + validateInputs(eventTarget, eventContext, eventName, additionalArgs) + end + function DebugReportingService:ReleaseRBXEventStream(eventTarget) + assert(eventTarget, "no value found for eventTarget") + end + function DebugReportingService:UpdateHeartbeatObject(additionalArgs) + if additionalArgs and not isTableEqual(additionalArgs, {}) then + assert(isTableEqual(additionalArgs, testArgs), "unexpected value for additionalArgs") + end + end + + return DebugReportingService + end + + + describe("new()", function() + it("should construct with a Reporting Service", function() + local es = EventStream.new(createDebugReportingService()) + expect(es).to.be.ok() + end) + + it("should not allow construction without a Reporting Service", function() + expect(function() + EventStream.new(nil) + end).to.throw() + end) + end) + + describe("setEnabled()", function() + it("should succeed with valid input", function() + local reporter = EventStream.new(createDebugReportingService()) + reporter:setEnabled(false) + reporter:setEnabled(true) + end) + it("should disable the reporter", function() + local reporter = EventStream.new(createDebugReportingService()) + reporter:setEnabled(false) + expect(function() + reporter:updateHeartbeatObject() + end).to.throw() + end) + end) + + describe("setRBXEvent()", function() + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:setRBXEvent(testContext, testEvent, testArgs) + end) + + it("should work when appropriately enabled / disabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:setRBXEvent(testContext, testEvent, testArgs) + end).to.throw() + + es:setEnabled(true) + es:setRBXEvent(testContext, testEvent, testArgs) + end) + + it("should throw an error if it is missing a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEvent(nil, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is missing an event name", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEvent(testContext, nil, testArgs) + end).to.throw() + end) + + it("should succeed even if there aren't any additional args", function() + local es = EventStream.new(createDebugReportingService()) + es:setRBXEvent(testContext, testEvent, nil) + end) + + it("should throw an error if it is given bad input for a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEvent(badTestContext, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for a event", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEvent(testContext, badTestEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for additionalArgs", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEvent(testContext, testEvent, badTestArgs) + end).to.throw() + end) + end) + + describe("setRBXEventStream()", function() + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:setRBXEventStream(testContext, testEvent, testArgs) + end) + + it("should work when appropriately enabled / disabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:setRBXEventStream(testContext, testEvent, testArgs) + end).to.throw() + + es:setEnabled(true) + es:setRBXEventStream(testContext, testEvent, testArgs) + end) + + it("should throw an error if it is missing a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEventStream(nil, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is missing an event name", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEventStream(testContext, nil, testArgs) + end).to.throw() + end) + + it("should succeed even if there aren't any additional args", function() + local es = EventStream.new(createDebugReportingService()) + es:setRBXEventStream(testContext, testEvent, nil) + end) + + it("should throw an error if it is given bad input for a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEventStream(badTestContext, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for a event", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEventStream(testContext, badTestEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for additionalArgs", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEventStream(testContext, testEvent, badTestArgs) + end).to.throw() + end) + end) + + describe("sendEventImmediately()", function() + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:sendEventImmediately(testContext, testEvent, testArgs) + end) + + it("should call without a fuss when enabled and throw when disabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:sendEventImmediately(testContext, testEvent, testArgs) + end).to.throw() + + es:setEnabled(true) + es:sendEventImmediately(testContext, testEvent, testArgs) + end) + + it("should throw an error if it is missing a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventImmediately(nil, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is missing an event name", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventImmediately(testContext, nil, testArgs) + end).to.throw() + end) + + it("should succeed even if there aren't any additional args", function() + local es = EventStream.new(createDebugReportingService()) + es:sendEventImmediately(testContext, testEvent, nil) + end) + + it("should throw an error if it is given bad input for a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventImmediately(badTestContext, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for a event", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventImmediately(testContext, badTestEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for additionalArgs", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventImmediately(testContext, testEvent, badTestArgs) + end).to.throw() + end) + end) + + describe("sendEventDeferred()", function() + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:sendEventDeferred(testContext, testEvent, testArgs) + end) + + it("should call without a fuss when enabled and throw when disabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:sendEventDeferred(testContext, testEvent, testArgs) + end).to.throw() + + es:setEnabled(true) + es:sendEventDeferred(testContext, testEvent, testArgs) + end) + + it("should throw an error if it is missing a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventDeferred(nil, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is missing an event name", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventDeferred(testContext, nil, testArgs) + end).to.throw() + end) + + it("should succeed even if there aren't any additional args", function() + local es = EventStream.new(createDebugReportingService()) + es:sendEventDeferred(testContext, testEvent, nil) + end) + + it("should throw an error if it is given bad input for a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventDeferred(badTestContext, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for a event", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventDeferred(testContext, badTestEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for additionalArgs", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventDeferred(testContext, testEvent, badTestArgs) + end).to.throw() + end) + end) + + describe("releaseRBXEventStream()", function() + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:releaseRBXEventStream() + end) + + it("should throw when disabled and succeed when enabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:releaseRBXEventStream() + end).to.throw() + + es:setEnabled(true) + es:releaseRBXEventStream() + end) + end) + + describe("updateHeartbeatObject()", function() + it("should work when appropriately enabled / disabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:updateHeartbeatObject(testArgs) + end).to.throw() + + expect(function() + es:setEnabled(true) + es:updateHeartbeatObject(testArgs) + end).never.to.throw() + end) + + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:updateHeartbeatObject(testArgs) + end) + + it("should succeed even if there aren't any additional args", function() + local es = EventStream.new(createDebugReportingService()) + es:updateHeartbeatObject(nil) + end) + + it("should throw an error with invalid input", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:updateHeartbeatObject(badTestArgs) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/GoogleAnalytics.lua b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/GoogleAnalytics.lua new file mode 100644 index 0000000..91e979c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/GoogleAnalytics.lua @@ -0,0 +1,51 @@ +--[[ + Specialized reporter for sending data to GA. + Useful for creating a breadcrumb trail of user interactions. + + Events in GA are aggregated and organized in order by category, action, label. +]] + +local GoogleAnalytics = {} +GoogleAnalytics.__index = GoogleAnalytics + +-- reportingService : (table or userdata) any object that defines the same functions for GA as AnalyticsService +function GoogleAnalytics.new(reportingService) + local rsType = type(reportingService) + assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService") + + local self = { + _reporter = reportingService, + _isEnabled = true, + } + setmetatable(self, GoogleAnalytics) + + return self +end + +-- isEnabled : (boolean) +function GoogleAnalytics:setEnabled(isEnabled) + assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean") + self._isEnabled = isEnabled +end + +-- category : (string) the most generic category by which to organize data, ex) LuaApp, Errors, GameSettings, etc. +-- action : (string) a specific event to record, ex) ButtonPressed, GameExit +-- label : (string, optional) a detail to differentiate one action over another, ex) LoginButton, Exit Code 0 +-- value : (integer, optional) the number of times this event has occurred +function GoogleAnalytics:trackEvent(category, action, label, value) + assert(type(category) == "string", "Expected category to be a string") + assert(type(action) == "string", "Expected action to be a string") + if label then + assert(type(label) == "string", "Expected label to be a string") + end + if value then + assert(type(value) == "number", "Expected value to be a number") + assert(value >= 0, "Expected value must not be a negative value") + end + assert(self._isEnabled, "This reporting service is disabled") + + self._reporter:TrackEvent(category, action, label, value) +end + + +return GoogleAnalytics diff --git a/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/GoogleAnalytics.spec.lua b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/GoogleAnalytics.spec.lua new file mode 100644 index 0000000..ede7486 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/GoogleAnalytics.spec.lua @@ -0,0 +1,140 @@ +return function() + local GoogleAnalytics = require(script.Parent.GoogleAnalytics) + + local testCategory = "testCategory" + local testAction = "testAction" + local testLabel = "testLabel" + local testValue = 6 + local badTestCategory = 13141 + local badTestAction = {} + local badTestLabel = {} + local badTestValue = "heyo" + + + local DebugReportingService = {} + function DebugReportingService:TrackEvent(category, action, label, value) + if category ~= testCategory then + error("unexpected value for category: " .. category) + end + if action ~= testAction then + error("unexpected value for action: " .. action) + end + if label then + if label ~= testLabel then + error("unexpected value for label: " .. label) + end + end + if value then + if value ~= testValue then + error("unexpected value for value: " .. value) + end + end + end + + + describe("new()", function() + it("should construct with a Reporting Service and Logging Service", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(ga).to.be.ok() + end) + + it("should fail to be constructed without a Reporting Service", function() + expect(function() + GoogleAnalytics.new(nil) + end).to.throw() + end) + end) + + describe("setEnabled()", function() + it("should succeed with valid input", function() + local reporter = GoogleAnalytics.new(DebugReportingService) + reporter:setEnabled(false) + reporter:setEnabled(true) + end) + it("should disable the reporter", function() + local reporter = GoogleAnalytics.new(DebugReportingService) + reporter:setEnabled(false) + expect(function() + reporter:trackEvent(testCategory, testAction, testLabel, testValue) + end).to.throw() + end) + end) + + describe("trackEvent()", function() + it("should work when appropriately enabled / disabled", function() + local ga = GoogleAnalytics.new(DebugReportingService) + + expect(function() + ga:setEnabled(false) + ga:trackEvent(testCategory, testAction, testLabel) + end).to.throw() + + ga:setEnabled(true) + ga:trackEvent(testCategory, testAction, testLabel) + end) + + it("should succeed with valid input", function() + local ga = GoogleAnalytics.new(DebugReportingService) + ga:trackEvent(testCategory, testAction, testLabel, testValue) + end) + + it("should throw an error if it is missing a category", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(nil, testAction, testLabel, testValue) + end).to.throw() + end) + + it("should throw an error if it is missing a testAction", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(testCategory, nil, testLabel, testValue) + end).to.throw() + end) + + it("should not throw an error if it is missing a label", function() + local ga = GoogleAnalytics.new(DebugReportingService) + ga:trackEvent(testCategory, testAction, nil, testValue) + end) + + it("should not throw an error if it is missing a value", function() + local ga = GoogleAnalytics.new(DebugReportingService) + ga:trackEvent(testCategory, testAction, testLabel) + end) + + it("should throw an error if it is given invalid input for category", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(badTestCategory, testAction, testLabel, testValue) + end).to.throw() + end) + + it("should throw an error if it is given invalid input for action", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(testCategory, badTestAction, testLabel, testValue) + end).to.throw() + end) + + it("should throw an error if it is given invalid input for label", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(testCategory, testAction, badTestLabel, testValue) + end).to.throw() + end) + + it("should throw an error if it is given invalid input for value", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(testCategory, testAction, testLabel, badTestValue) + end).to.throw() + + expect(function() + ga:trackEvent(testCategory, testAction, testLabel, -1) + end).to.throw() + end) + end) + + + +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Influx.lua b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Influx.lua new file mode 100644 index 0000000..89f287e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Influx.lua @@ -0,0 +1,47 @@ +--[[ + Specialized reporter for sending data to InfluxDb. + Useful for very detailed information about specific errors. + + Due to how Influx sends data, it is disallowed on XBox. + ~Kyler Mulherin (9/12/2017) +]] + +local Influx = {} +Influx.__index = Influx + +-- reportingService - (object) any object that defines the same functions for Influx as AnalyticsService +function Influx.new(reportingService) + local rsType = type(reportingService) + assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService") + + local self = { + _reporter = reportingService, + _isEnabled = true, + } + setmetatable(self, Influx) + + return self +end + +-- isEnabled : (boolean) +function Influx:setEnabled(isEnabled) + assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean") + self._isEnabled = isEnabled +end + +-- seriesName : (string) the name of the series as it will appear in InfluxDb +-- additionalArgs : (map) extra key/values to appear in each series +-- throttlingPercent : (int) the chance to actually report this series +function Influx:reportSeries(seriesName, additionalArgs, throttlingPercent) + additionalArgs = additionalArgs or {} + + assert(type(seriesName) == "string", "Expected seriesName to be a string") + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(type(throttlingPercent) == "number", "Expected throttlingPercent to be a number") + assert(throttlingPercent >= 0 and throttlingPercent <= 10000, "throttlingPercent must be between 0 - 10,000") + assert(self._isEnabled, "This reporting service is disabled") + + self._reporter:ReportInfluxSeries(seriesName, additionalArgs, throttlingPercent) +end + +return Influx diff --git a/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Influx.spec.lua b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Influx.spec.lua new file mode 100644 index 0000000..8d417ba --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Influx.spec.lua @@ -0,0 +1,165 @@ +return function() + local Influx = require(script.Parent.Influx) + + local testSeriesName = "testSeries" + local testArgs = { + testKey = "testValue" + } + local testThrottlingPercentage = 1000 + + local badTestSeriesName = 114 + local badTestArgs = "someString" + local badThrottlingPercentage1 = -15 + local badThrottlingPercentage2 = 150000 + local badThrottlingPercentage3 = "a lot" + + + local function isTableEqual(table1, table2) + if table1 == table2 then + return true + end + + if type(table2) ~= "table" then + return false + end + + for key, _ in pairs(table1) do + if table1[key] ~= table2[key] then + return false + end + end + for key, _ in pairs(table2) do + if table2[key] ~= table1[key] then + return false + end + end + + return true + end + + + local DebugReportingService = {} + function DebugReportingService:ReportInfluxSeries(seriesName, additionalArgs, throttlingPercentage) + if seriesName ~= seriesName then + error("Unexpected value for seriesName: " .. seriesName) + end + if throttlingPercentage ~= testThrottlingPercentage then + error("Unexpected value for throttlingPercentage: " .. throttlingPercentage) + end + if isTableEqual(additionalArgs, {}) == false then + if isTableEqual(additionalArgs, testArgs) == false then + error("Unexpected value for additionalArgs") + end + end + end + + + describe("new()", function() + it("should construct with a Reporting Service", function() + local influx = Influx.new(DebugReportingService) + expect(influx).to.be.ok() + end) + + it("should fail to be constructed without a Reporting Service", function() + expect(function() + Influx.new(nil) + end).to.throw() + end) + end) + + describe("setEnabled()", function() + it("should succeed with valid input", function() + local influx = Influx.new(DebugReportingService) + influx:setEnabled(false) + influx:setEnabled(true) + end) + it("should disable the reporter", function() + local influx = Influx.new(DebugReportingService) + influx:setEnabled(false) + expect(function() + influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage) + end).to.throw() + end) + end) + + describe("reportSeries()", function() + it("should work when appropriately enabled / disabled", function() + local influx = Influx.new(DebugReportingService) + + expect(function() + influx:setEnabled(false) + influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage) + end).to.throw() + + + influx:setEnabled(true) + influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage) + end) + + it("should succeed with valid input", function() + local influx = Influx.new(DebugReportingService) + influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage) + end) + + it("should succeed even if it is missing any additionalArgs", function() + local influx = Influx.new(DebugReportingService) + influx:reportSeries(testSeriesName, nil, testThrottlingPercentage) + end) + + it("should throw an error with invalid input for the seriesName", function() + local influx = Influx.new(DebugReportingService) + expect(function() + influx:reportSeries(badTestSeriesName, testArgs, testThrottlingPercentage) + end).to.throw() + end) + + it("should throw an error with invalid input for the throttlingPercentage - out of range - below zero", function() + expect(function() + local influx = Influx.new(DebugReportingService) + influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage1) + end).to.throw() + end) + + it("should throw an error with invalid input for the throttlingPercentage - out of range - above cap", function() + expect(function() + local influx = Influx.new(DebugReportingService) + influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage2) + end).to.throw() + end) + + it("should throw an error with invalid input for the throttlingPercentage - bad type", function() + expect(function() + local influx = Influx.new(DebugReportingService) + influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage3) + end).to.throw() + end) + + it("should throw an error with completely invalid input", function() + local influx = Influx.new(DebugReportingService) + expect(function() + influx:reportSeries(badTestSeriesName, badTestArgs, badThrottlingPercentage1) + end).to.throw() + end) + + it("should throw an error if it is missing a seriesName", function() + local influx = Influx.new(DebugReportingService) + expect(function() + influx:reportSeries(nil, testArgs, testThrottlingPercentage) + end).to.throw() + end) + + it("should throw an error if it is missing a throttlingPercentage", function() + local influx = Influx.new(DebugReportingService) + expect(function() + influx:reportSeries(testSeriesName, testArgs, nil) + end).to.throw() + end) + + it("should throw an error if it is missing any input", function() + local influx = Influx.new(DebugReportingService) + expect(function() + influx:reportSeries(nil, nil, nil) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/.robloxrc new file mode 100644 index 0000000..e12a7d8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/.robloxrc @@ -0,0 +1,7 @@ +{ + "lint": { + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/AnalyticsReporters/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/AnalyticsReporters/.robloxrc new file mode 100644 index 0000000..e721482 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/AnalyticsReporters/.robloxrc @@ -0,0 +1,9 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalShadow": "fatal", + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/AnalyticsReporters/Diag.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/AnalyticsReporters/Diag.lua new file mode 100644 index 0000000..ea43e3b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/AnalyticsReporters/Diag.lua @@ -0,0 +1,8 @@ +----------------------------------------------------------------------------- +--- --- +--- Under Migration to CorePackages --- +--- --- +--- Please put your changes in Analytics. --- +----------------------------------------------------------------------------- +local CorePackages = game:GetService("CorePackages") +return require(CorePackages.Analytics.AnalyticsReporters.Diag) diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Action.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Action.lua new file mode 100644 index 0000000..d3d1ff4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Action.lua @@ -0,0 +1,68 @@ +--[[ + A helper function to define a Rodux action creator with an associated name. + + Normally when creating a Rodux action, you can just create a function: + + return function(value) + return { + type = "MyAction", + value = value, + } + end + + And then when you check for it in your reducer, you either use a constant, + or type out the string name: + + if action.type == "MyAction" then + -- change some state + end + + Typos here are a remarkably common bug. We also have the issue that there's + no link between reducers and the actions that they respond to! + + `Action` (this helper) provides a utility that makes this a bit cleaner. + + Instead, define your Rodux action like this: + + return Action("MyAction", function(value) + return { + value = value, + } + end) + + We no longer need to add the `type` field manually. + + Additionally, the returned action creator now has a 'name' property that can + be checked by your reducer: + + local MyAction = require(Reducers.MyAction) + + ... + + if action.type == MyAction.name then + -- change some state! + end + + Now we have a clear link between our reducers and the actions they use, and + if we ever typo a name, we'll get a warning in LuaCheck as well as an error + at runtime! +]] + +return function(name, fn) + assert(type(name) == "string", "A name must be provided to create an Action") + assert(type(fn) == "function", "A function must be provided to create an Action") + + return setmetatable({ + name = name, + }, { + __call = function(self, ...) + local result = fn(...) + + assert(type(result) == "table", "An action must return a table") + + result.type = name + + return result + end + }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Action.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Action.spec.lua new file mode 100644 index 0000000..799c2b3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Action.spec.lua @@ -0,0 +1,85 @@ +return function() + local Action = require(script.Parent.Action) + + it("should return a table", function() + local action = Action("foo", function() + return {} + end) + + expect(action).to.be.a("table") + end) + + it("should set the name of the action", function() + local action = Action("foo", function() + return {} + end) + + expect(action.name).to.equal("foo") + end) + + it("should be able to be called as a function", function() + local action = Action("foo", function() + return {} + end) + + expect(action).never.to.throw() + end) + + it("should return a table when called as a function", function() + local action = Action("foo", function() + return {} + end) + + expect(action()).to.be.a("table") + end) + + it("should set the type of the action", function() + local action = Action("foo", function() + return {} + end) + + expect(action().type).to.equal("foo") + end) + + it("should set values", function() + local action = Action("foo", function(value) + return { + value = value + } + end) + + expect(action(100).value).to.equal(100) + end) + + it("should throw when passed a function", function() + local action = Action("foo", function() + return function() end + end) + + expect(action).to.throw() + end) + + it("should throw with a invalid name", function() + expect(function() + Action(nil, function() + return {} + end) + end).to.throw() + + expect(function() + Action(100, function() + return {} + end) + end).to.throw() + end) + + it("should throw when passed a invalid function", function() + expect(function() + Action("foo", nil) + end).to.throw() + + expect(function() + Action("foo", {}) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Color.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Color.lua new file mode 100644 index 0000000..5845ab9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Color.lua @@ -0,0 +1,19 @@ +local Color = {} + +function Color.RgbFromHex(hexColor) + assert(hexColor >= 0 and hexColor <= 0xffffff, "RgbFromHex: Out of range") + + local b = hexColor % 256 + hexColor = (hexColor - b) / 256 + local g = hexColor % 256 + hexColor = (hexColor - g) / 256 + local r = hexColor + + return r, g, b +end + +function Color.Color3FromHex(hexColor) + return Color3.fromRGB(Color.RgbFromHex(hexColor)) +end + +return Color \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Color.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Color.spec.lua new file mode 100644 index 0000000..af7c914 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Color.spec.lua @@ -0,0 +1,33 @@ +return function() + local Color = require(script.Parent.Color) + + describe("RgbFromHex", function() + it("should convert a hex color to rgb correctly", function() + local r, g, b = Color.RgbFromHex(0x232527) + expect(r).to.equal(35) + expect(g).to.equal(37) + expect(b).to.equal(39) + + r, g, b = Color.RgbFromHex(0x0) + expect(r).to.equal(0) + expect(g).to.equal(0) + expect(b).to.equal(0) + + r, g, b = Color.RgbFromHex(0xffffff) + expect(r).to.equal(255) + expect(g).to.equal(255) + expect(b).to.equal(255) + end) + + + it("should assert if given a hex color out of range", function() + expect(function() + Color.RgbFromHex(-1) + end).to.throw() + + expect(function() + Color.RgbFromHex(0x1000000) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Functional.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Functional.lua new file mode 100644 index 0000000..a40c8ad --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Functional.lua @@ -0,0 +1,131 @@ +--[[ + Provides an implementation of functional programming primitives. +]] + +local Functional = {} + +--[[ + Create a copy of a list with only values for which `callback` returns true +]] +function Functional.Filter(list, callback) + local new = {} + + for key = 1, #list do + local value = list[key] + if callback(value, key) then + table.insert(new, value) + end + end + + return new +end + +--[[ + Create a copy of a list where each value is transformed by `callback` +]] +function Functional.Map(list, callback) + local new = {} + + for key = 1, #list do + new[key] = callback(list[key], key) + end + + return new +end + +--[[ + Identical to Map, except that the result will be reversed. +]] +function Functional.MapReverse(list, callback) + local new = {} + + for key = #list, 1, -1 do + new[key] = callback(list[key], key) + end + + return new +end + +--[[ + Create a copy of a list doing a combination filter and map. + + If callback returns nil for any item, it is considered filtered from the + list. Any other value is considered the result of the 'map' operation. +]] +function Functional.FilterMap(list, callback) + local new = {} + + for key = 1, #list do + local value = list[key] + local result = callback(value, key) + + if result ~= nil then + table.insert(new, result) + end + end + + return new +end + +--[[ + Performs a left-fold of the list with the given initial value and callback. +]] +function Functional.Fold(list, initial, callback) + local accum = initial + + for key = 1, #list do + accum = callback(accum, list[key], key) + end + + return accum +end + +--[[ + Performs a fold over the entries in the given dictionary. +]] +function Functional.FoldDictionary(dictionary, initial, callback) + local accum = initial + + for key, value in pairs(dictionary) do + accum = callback(accum, key, value) + end + + return accum +end + +--[[ + Returns a list that contains at most `count` values from the given list. +]] +function Functional.Take(list, count, startingIndex) + startingIndex = startingIndex or 1 + + local maxIndex = count + (startingIndex - 1) + if maxIndex > #list then + maxIndex = #list + end + + local new = {} + + for i = startingIndex, maxIndex do + local value = list[i] + local newIndex = i - (startingIndex - 1) + new[newIndex] = value + end + + return new +end + +--[[ + If the list contains the sought-after element, return its index, or nil otherwise. +]] +function Functional.Find(list, value) + for index, element in ipairs(list) do + if element == value then + return index + end + end + + return nil +end + +return Functional \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Functional.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Functional.spec.lua new file mode 100644 index 0000000..4ea87a4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Functional.spec.lua @@ -0,0 +1,218 @@ +return function() + local Functional = require(script.Parent.Functional) + + local function identity(...) + return ... + end + + local function add(a, b) + return a + b + end + + describe("Filter", function() + it("should copy lists correctly", function() + local listA = {1, 2, 3} + local listB = Functional.Filter(listA, function() + return true + end) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the filter predicate", function() + local listA = {1, 2, 3, 4, 5} + local listB = Functional.Filter(listA, function(value, key) + expect(value).to.equal(key) + + return value % 2 == 0 + end) + + expect(listB[1]).to.equal(2) + expect(listB[2]).to.equal(4) + end) + end) + + describe("Map", function() + it("should copy lists correctly using the identity function", function() + local listA = {1, 2, 3} + local listB = Functional.Map(listA, identity) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the map predicate", function() + local listA = {1, 2, 3} + local listB = Functional.Map(listA, function(value, key) + expect(value).to.equal(key) + + return value * 2 + end) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i] * 2) + end + end) + end) + + describe("MapReverse", function() + it("should copy lists correctly using the identity function", function() + local listA = {1, 2, 3} + local listB = Functional.MapReverse(listA, identity) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the map predicate", function() + local listA = {1, 2, 3} + local listB = Functional.MapReverse(listA, function(value, key) + expect(value).to.equal(key) + + return value * 2 + end) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i] * 2) + end + end) + + it("should iterate backwards", function() + local list = {1, 2, 3} + local nextKey = 3 + + Functional.MapReverse(list, function(value, key) + expect(value).to.equal(nextKey) + expect(key).to.equal(nextKey) + + nextKey = nextKey - 1 + end) + + expect(nextKey).to.equal(0) + end) + end) + + describe("FilterMap", function() + it("should copy truthy lists using the identity function", function() + local listA = {1, 2, 3} + local listB = Functional.FilterMap(listA, identity) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the filter-map predicate", function() + local listA = {1, 2, 3, 4, 5} + + -- Create a list containing only the odd numbers, and double those numbers + local listB = Functional.FilterMap(listA, function(value, key) + expect(value).to.equal(key) + + if value % 2 == 0 then + return nil + end + + return value * 2 + end) + + expect(listB[1]).to.equal(2) + expect(listB[2]).to.equal(6) + expect(listB[3]).to.equal(10) + end) + end) + + describe("Fold", function() + it("should left-fold lists", function() + local list = {1, 2, 3, 4, 5} + + local sum = Functional.Fold(list, 0, add) + + expect(sum).to.equal(15) + end) + end) + + describe("Take", function() + it("should take values from a list", function() + local a = {1, 2, 3} + local b = Functional.Take(a, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + end) + + it("should not take past the end of a list", function() + local a = {1, 2, 3} + local b = Functional.Take(a, 4) + + expect(#b).to.equal(3) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + end) + + it("should copy all values when taking past the end of a list", function() + local a = {1, 2, 3} + local b = Functional.Take(a, 4) + + expect(#b).to.equal(#a) + expect(a[1]).to.equal(b[1]) + expect(a[2]).to.equal(b[2]) + expect(a[3]).to.equal(b[3]) + end) + + it("should take values from a starting index when provided", function() + local a = {1, 2, 3, 4} + local b = Functional.Take(a, 2, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(2) + expect(b[2]).to.equal(3) + end) + + it("should not take past the end of a list when the starting index is provided", function() + local a = {1, 2, 3, 4} + local b = Functional.Take(a, 3, 3) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(3) + expect(b[2]).to.equal(4) + end) + end) + + describe("Find", function() + it("should return index of matched item", function() + local a = {"foo", "bar", "garply"} + local b = Functional.Find(a, "bar") + + expect(b).to.equal(2) + end) + + it("should find the first example in the case of duplicates", function() + local a = {"foo", "bar", "garply", "bar"} + local b = Functional.Find(a, "bar") + + expect(b).to.equal(2) + end) + + it("should return nil if item is not found", function() + local a = {"foo", "bar", "garply"} + local b = Functional.Find(a, "fleebledegoop") + + expect(b).to.equal(nil) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Immutable.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Immutable.lua new file mode 100644 index 0000000..a73d203 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Immutable.lua @@ -0,0 +1,141 @@ +--[[ + Provides functions for manipulating immutable data structures. +]] + +local Immutable = {} + +--[[ + Merges dictionary-like tables together. +]] +function Immutable.JoinDictionaries(...) + local result = {} + + for i = 1, select("#", ...) do + local dictionary = select(i, ...) + for key, value in pairs(dictionary) do + result[key] = value + end + end + + return result +end + +--[[ + Joins any number of lists together into a new list +]] +function Immutable.JoinLists(...) + local new = {} + + for listKey = 1, select("#", ...) do + local list = select(listKey, ...) + local len = #new + + for itemKey = 1, #list do + new[len + itemKey] = list[itemKey] + end + end + + return new +end + +--[[ + Creates a new copy of the dictionary and sets a value inside it. +]] +function Immutable.Set(dictionary, key, value) + local new = {} + + for key, value in pairs(dictionary) do + new[key] = value + end + + new[key] = value + + return new +end + +--[[ + Creates a new copy of the list with the given elements appended to it. +]] +function Immutable.Append(list, ...) + local new = {} + local len = #list + + for key = 1, len do + new[key] = list[key] + end + + for i = 1, select("#", ...) do + new[len + i] = select(i, ...) + end + + return new +end + +--[[ + Remove elements from a dictionary +]] +function Immutable.RemoveFromDictionary(dictionary, ...) + local result = {} + + for key, value in pairs(dictionary) do + local found = false + for listKey = 1, select("#", ...) do + if key == select(listKey, ...) then + found = true + break + end + end + if not found then + result[key] = value + end + end + + return result +end + +--[[ + Remove the given key from the list. +]] +function Immutable.RemoveFromList(list, removeIndex) + local new = {} + + for i = 1, #list do + if i ~= removeIndex then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Remove the range from the list starting from the index. +]] +function Immutable.RemoveRangeFromList(list, index, count) + local new = {} + + for i = 1, #list do + if i < index or i >= index + count then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Creates a new list that has no occurrences of the given value. +]] +function Immutable.RemoveValueFromList(list, removeValue) + local new = {} + + for i = 1, #list do + if list[i] ~= removeValue then + table.insert(new, list[i]) + end + end + + return new +end + +return Immutable diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Immutable.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Immutable.spec.lua new file mode 100644 index 0000000..961e1cc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Immutable.spec.lua @@ -0,0 +1,284 @@ +return function() + local Immutable = require(script.Parent.Immutable) + + describe("JoinDictionaries", function() + it("should preserve immutability", function() + local a = {} + local b = {} + + local c = Immutable.JoinDictionaries(a, b) + + expect(c).never.to.equal(a) + expect(c).never.to.equal(b) + end) + + it("should treat list-like values like dictionary values", function() + local a = { + [1] = 1, + [2] = 2, + [3] = 3 + } + + local b = { + [1] = 11, + [2] = 22 + } + + local c = Immutable.JoinDictionaries(a, b) + + expect(c[1]).to.equal(b[1]) + expect(c[2]).to.equal(b[2]) + expect(c[3]).to.equal(a[3]) + end) + + it("should merge dictionary values correctly", function() + local a = { + hello = "world", + foo = "bar" + } + + local b = { + foo = "baz", + tux = "penguin" + } + + local c = Immutable.JoinDictionaries(a, b) + + expect(c.hello).to.equal(a.hello) + expect(c.foo).to.equal(b.foo) + expect(c.tux).to.equal(b.tux) + end) + + it("should merge multiple dictionaries", function() + local a = { + foo = "yes" + } + + local b = { + bar = "yup" + } + + local c = { + baz = "sure" + } + + local d = Immutable.JoinDictionaries(a, b, c) + + expect(d.foo).to.equal(a.foo) + expect(d.bar).to.equal(b.bar) + expect(d.baz).to.equal(c.baz) + end) + end) + + describe("JoinLists", function() + it("should preserve immutability", function() + local a = {} + local b = {} + + local c = Immutable.JoinLists(a, b) + + expect(c).never.to.equal(a) + expect(c).never.to.equal(b) + end) + + it("should treat list-like values correctly", function() + local a = {1, 2, 3} + local b = {4, 5, 6} + + local c = Immutable.JoinLists(a, b) + + expect(#c).to.equal(6) + + for i = 1, #c do + expect(c[i]).to.equal(i) + end + end) + + it("should merge multiple lists", function() + local a = {1, 2} + local b = {3, 4} + local c = {5, 6} + + local d = Immutable.JoinLists(a, b, c) + + expect(#d).to.equal(6) + + for i = 1, #d do + expect(d[i]).to.equal(i) + end + end) + end) + + describe("Set", function() + it("should preserve immutability", function() + local a = {} + + local b = Immutable.Set(a, "foo", "bar") + + expect(b).never.to.equal(a) + end) + + it("should treat numeric keys normally", function() + local a = {1, 2, 3} + + local b = Immutable.Set(a, 2, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(4) + expect(b[3]).to.equal(3) + end) + + it("should overwrite dictionary-like keys", function() + local a = { + foo = "bar", + baz = "qux" + } + + local b = Immutable.Set(a, "foo", "hello there") + + expect(b.foo).to.equal("hello there") + expect(b.baz).to.equal(a.baz) + end) + end) + + describe("Append", function() + it("should preserve immutability", function() + local a = {} + + local b = Immutable.Append(a, "another happy landing") + + expect(b).never.to.equal(a) + end) + + it("should append values", function() + local a = {1, 2, 3} + local b = Immutable.Append(a, 4, 5) + + expect(#b).to.equal(5) + + for i = 1, #b do + expect(b[i]).to.equal(i) + end + end) + end) + + describe("RemoveFromDictionary", function() + it("should preserve immutability", function() + local a = { foo = "bar" } + + local b = Immutable.RemoveFromDictionary(a, "foo") + + expect(b).to.never.equal(a) + end) + + it("should remove fields from the dictionary", function() + local a = { + foo = "bar", + baz = "qux", + boof = "garply", + } + + local b = Immutable.RemoveFromDictionary(a, "foo", "boof") + + expect(b.foo).to.never.be.ok() + expect(b.baz).to.equal("qux") + expect(b.boof).to.never.be.ok() + end) + end) + + describe("RemoveFromList", function() + it("should preserve immutability", function() + local a = {1, 2, 3} + local b = Immutable.RemoveFromList(a, 2) + + expect(b).never.to.equal(a) + end) + + it("should remove elements from the list", function() + local a = {1, 2, 3} + local b = Immutable.RemoveFromList(a, 2) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + end) + + describe("RemoveRangeFromList", function() + it("should preserve immutability", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 1) + + expect(b).never.to.equal(a) + end) + + it("should remove elements properly from the list 1", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 1) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + + it("should remove elements properly from the list 2", function() + local a = {1, 2, 3, 4, 5, 6} + local b = Immutable.RemoveRangeFromList(a, 1, 4) + + expect(b[1]).to.equal(5) + expect(b[2]).to.equal(6) + expect(b[3]).never.to.be.ok() + end) + + it("should remove elements properly from the list 3", function() + local a = {1, 2, 3, 4, 5, 6} + local b = Immutable.RemoveRangeFromList(a, 2, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(6) + expect(b[3]).never.to.be.ok() + end) + + it("should remove elements properly from the list 4", function() + local a = {1, 2, 3, 4, 5, 6, 7} + local b = Immutable.RemoveRangeFromList(a, 4, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + expect(b[4]).never.to.be.ok() + end) + + it("should not remove any elements when count is 0 or less", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 0) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + + local c = Immutable.RemoveRangeFromList(a, 2, -1) + expect(c[1]).to.equal(1) + expect(c[2]).to.equal(2) + expect(c[3]).to.equal(3) + end) + end) + + describe("RemoveValueFromList", function() + it("should preserve immutability", function() + local a = {1, 1, 1} + local b = Immutable.RemoveValueFromList(a, 1) + + expect(b).never.to.equal(a) + end) + + it("should remove all elements from the list", function() + local a = {1, 2, 2, 3} + local b = Immutable.RemoveValueFromList(a, 2) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Signal.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Signal.lua new file mode 100644 index 0000000..438738a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Signal.lua @@ -0,0 +1,75 @@ +--[[ + A limited, simple implementation of a Signal. + + Handlers are fired in order, and (dis)connections are properly handled when + executing an event. + + Signal uses Immutable to avoid invalidating the 'Fire' loop iteration. +]] + +local Immutable = require(script.Parent.Immutable) + +local Signal = {} + +Signal.__index = Signal + +function Signal.new() + local self = { + _listeners = {} + } + + setmetatable(self, Signal) + + return self +end + +function Signal:connect(callback) + local listener = { + callback = callback, + isConnected = true, + } + self._listeners = Immutable.Append(self._listeners, listener) + + local function disconnect() + listener.isConnected = false + self._listeners = Immutable.RemoveValueFromList(self._listeners, listener) + end + + return { + Disconnect = function() + warn(string.format( + "Connection:Disconnect() has been deprecated, use Connection:disconnect()\n%s]", + debug.traceback() + )) + disconnect() + end, + disconnect = disconnect, + } +end + +function Signal:fire(...) + for _, listener in ipairs(self._listeners) do + if listener.isConnected then + listener.callback(...) + end + end +end + +function Signal:Connect(...) + warn(string.format( + "Signal:Connect() has been deprecated, use Signal:connect()\n%s]", + debug.traceback() + )) + return self:connect(...) +end + +function Signal:Fire(...) + warn(string.format( + "Signal:Fire() has been deprecated, use Signal:fire()\n%s]", + debug.traceback() + )) + self:fire(...) +end + + +return Signal \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Signal.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Signal.spec.lua new file mode 100644 index 0000000..f00f947 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Signal.spec.lua @@ -0,0 +1,114 @@ +return function() + local Signal = require(script.Parent.Signal) + + it("should construct from nothing", function() + local signal = Signal.new() + + expect(signal).to.be.ok() + end) + + it("should fire connected callbacks", function() + local callCount = 0 + local value1 = "Hello World" + local value2 = 7 + + local callback = function(arg1, arg2) + expect(arg1).to.equal(value1) + expect(arg2).to.equal(value2) + callCount = callCount + 1 + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + + connection:disconnect() + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + end) + + it("should disconnect handlers", function() + local callback = function() + error("Callback was called after disconnect!") + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + connection:disconnect() + + signal:fire() + end) + + it("should fire handlers in order", function() + local signal = Signal.new() + local x = 0 + local y = 0 + + local callback1 = function() + expect(x).to.equal(0) + expect(y).to.equal(0) + x = x + 1 + end + + local callback2 = function() + expect(x).to.equal(1) + expect(y).to.equal(0) + y = y + 1 + end + + signal:connect(callback1) + signal:connect(callback2) + signal:fire() + + expect(x).to.equal(1) + expect(y).to.equal(1) + end) + + it("should continue firing despite mid-event disconnection", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionA + connectionA = signal:connect(function() + connectionA:disconnect() + countA = countA + 1 + end) + + signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(1) + end) + + it("should skip listeners that were disconnected during event evaluation", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionB + + signal:connect(function() + countA = countA + 1 + connectionB:disconnect() + end) + + connectionB = signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(0) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Text.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Text.lua new file mode 100644 index 0000000..3002173 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Text.lua @@ -0,0 +1,113 @@ +local EngineFeatureTextBoundsRoundUp = game:GetEngineFeature("TextBoundsRoundUp") + +local TextService = game:GetService("TextService") + +local Text = {} + +-- FYI: Any number greater than 2^30 will make TextService:GetTextSize give invalid results +local MAX_BOUND = 10000 + +-- Remove with EngineFeatureTextBoundsRoundUp +Text._TEMP_PATCHED_PADDING = Vector2.new(0, 0) + +if not EngineFeatureTextBoundsRoundUp then + Text._TEMP_PATCHED_PADDING = Vector2.new(2, 2) +end + +-- Wrapper function for GetTextSize +function Text.GetTextBounds(text, font, fontSize, bounds) + return TextService:GetTextSize(text, fontSize, font, bounds) + Text._TEMP_PATCHED_PADDING +end + +function Text.GetTextWidth(text, font, fontSize) + return Text.GetTextBounds(text, font, fontSize, Vector2.new(MAX_BOUND, MAX_BOUND)).X +end + +function Text.GetTextHeight(text, font, fontSize, widthCap) + return Text.GetTextBounds(text, font, fontSize, Vector2.new(widthCap, MAX_BOUND)).Y +end + +-- TODO(CLIPLAYEREX-391): Kill these truncate functions once we have official support for text truncation +function Text.Truncate(text, font, fontSize, widthInPixels, overflowMarker) + overflowMarker = overflowMarker or "" + + if Text.GetTextWidth(text, font, fontSize) > widthInPixels then + -- A binary search may be more efficient + local lastText = "" + for _, stopIndex in utf8.graphemes(text) do + local newText = string.sub(text, 1, stopIndex) .. overflowMarker + if Text.GetTextWidth(newText, font, fontSize) > widthInPixels then + return lastText + end + lastText = newText + end + else -- No truncation needed + return text + end + + return "" +end + +function Text.TruncateTextLabel(textLabel, overflowMarker) + textLabel.Text = Text.Truncate(textLabel.Text, textLabel.Font, + textLabel.TextSize, textLabel.AbsoluteSize.X, overflowMarker) +end + +-- Remove whitespace from the beginning and end of the string +function Text.Trim(str) + if type(str) ~= "string" then + error(string.format("Text.Trim called on non-string type %s.", type(str)), 2) + end + return (str:gsub("^%s*(.-)%s*$", "%1")) +end + +-- Remove whitespace from the end of the string +function Text.RightTrim(str) + if type(str) ~= "string" then + error(string.format("Text.RightTrim called on non-string type %s.", type(str)), 2) + end + return (str:gsub("%s+$", "")) +end + +-- Remove whitespace from the beginning of the string +function Text.LeftTrim(str) + if type(str) ~= "string" then + error(string.format("Text.LeftTrim called on non-string type %s.", type(str)), 2) + end + return (str:gsub("^%s+", "")) +end + +-- Replace multiple whitespace with one; remove leading and trailing whitespace +function Text.SpaceNormalize(str) + if type(str) ~= "string" then + error(string.format("Text.SpaceNormalize called on non-string type %s.", type(str)), 2) + end + return (str:gsub("%s+", " "):gsub("^%s+" , ""):gsub("%s+$" , "")) +end + +-- Splits a string by the provided pattern into a table. The pattern is interpreted as plain text. +function Text.Split(str, pattern) + if type(str) ~= "string" then + error(string.format("Text.Split called on non-string type %s.", type(str)), 2) + elseif type(pattern) ~= "string" then + error(string.format("Text.Split called with a pattern that is non-string type %s.", type(pattern)), 2) + elseif pattern == "" then + error("Text.Split called with an empty pattern.", 2) + end + + local result = {} + local currentPosition = 1 + + while true do + local patternStart, patternEnd = string.find(str, pattern, currentPosition, true) + if not patternStart or not patternEnd then break end + table.insert(result, string.sub(str, currentPosition, patternStart - 1)) + currentPosition = patternEnd + 1 + end + + table.insert(result, string.sub(str, currentPosition, string.len(str))) + + return result +end + +return Text \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Text.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Text.spec.lua new file mode 100644 index 0000000..830e59d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/Text.spec.lua @@ -0,0 +1,410 @@ +return function() + local Text = require(script.Parent.Text) + + describe("GetTextBounds", function() + it("should return a bounds of padding width and font-size height when the string is empty", function() + local bounds = Text.GetTextBounds("", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000)) + expect(bounds.X).to.equal(Text._TEMP_PATCHED_PADDING.x) + expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y) + end) + it("should return the height and width of a string as one line with large bounds", function() + local bounds = Text.GetTextBounds("One Two Three", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000)) + expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y) + end) + + it("should return the height of the string as multiple lines with short bounds", function() + local bounds = Text.GetTextBounds("One Two Three Four", Enum.Font.SourceSans, 18, Vector2.new(32, 1000)) + expect(bounds.Y > 18).to.equal(true) + end) + end) + + describe("GetTextHeight", function() + it("should return height equal to font size when string is empty", function() + local height = Text.GetTextHeight("", Enum.Font.SourceSans, 18, 0) + expect(height).to.equal(18 + Text._TEMP_PATCHED_PADDING.y) + end) + end) + + describe("GetTextWidth", function() + it("should return width equal to 1 when string is empty", function() + local width = Text.GetTextWidth("", Enum.Font.SourceSans, 18, 18) + expect(width).to.equal(Text._TEMP_PATCHED_PADDING.x) + end) + end) + + describe("Truncate", function() + it("should return empty string", function() + local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 0, "...") + expect(emptyQuery).to.be.a("string") + expect(emptyQuery).to.equal("") + end) + + it("should return empty string for not empty box", function() + local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 50, "...") + expect(emptyQuery).to.be.a("string") + expect(emptyQuery).to.equal("") + end) + + it("should truncate with ...", function() + local reallyLongQuery = Text.Truncate( + "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100, "...") + expect(reallyLongQuery).to.equal("One Two Thre...") + end) + + it("should truncate without a ...", function() + local reallyLongQueryNoOverflowMarker = Text.Truncate( + "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100) + expect(reallyLongQueryNoOverflowMarker).to.equal("One Two Three ") + end) + + it("should not truncate", function() + local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, 100) + expect(shouldFitQuery).to.equal("One Two") + end) + + it("should not truncate, off by one check", function() + local oneCharQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 100) + expect(oneCharQuery).to.equal("O") + end) + + it("should truncate, off by one check", function() + local oneCharNoRoomQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 0) + expect(oneCharNoRoomQuery).to.equal("") + end) + + it("should perform a negative width check", function() + local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, -100, "...") + expect(shouldFitQuery).to.equal("") + end) + + itFIXME("should truncate long graphemes properly", function() + -- 11-byte rainbow flag grapheme + -- Flag, zero-space-joiner, rainbow + local rainbowFlag = utf8.char(127987) .. utf8.char(8205) .. utf8.char(127752) + local oneFlagWithinLimit = Text.Truncate( + rainbowFlag, Enum.Font.SourceSans, 18, 100, "...") + expect(oneFlagWithinLimit).to.equal(rainbowFlag) + + local twoRainbowFlags = rainbowFlag .. rainbowFlag + local twoFlagsAreFine = Text.Truncate( + twoRainbowFlags, Enum.Font.SourceSans, 18, 100, "...") + expect(twoFlagsAreFine).to.equal(twoRainbowFlags) + + local fourRainbowFlags = twoRainbowFlags .. twoRainbowFlags + local fourFlagsIsTooLong = Text.Truncate( + fourRainbowFlags, Enum.Font.SourceSans, 18, 100, "...") + expect(fourFlagsIsTooLong).to.equal(twoRainbowFlags .. "...") -- With --fflags==true fails because of truncation + end) + end) + + describe("TruncateTextLabel", function() + it("should use text label attributes to truncate text", function() + local screenGui = Instance.new("ScreenGui") + local textLabel = Instance.new("TextLabel") + textLabel.Size = UDim2.new(0, 100, 0, 32) + textLabel.Text = "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve" + textLabel.Font = Enum.Font.SourceSans + textLabel.TextSize = 18 + textLabel.Parent = screenGui + Text.TruncateTextLabel(textLabel) + + expect(textLabel.Text).to.equal("One Two Three ") + end) + end) + + + describe("TrimString", function() + it("Should trim the string properly 1", function() + local trimmedInput = Text.Trim("") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly 2", function() + local trimmedInput = Text.Trim(" ") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly 3", function() + local trimmedInput = Text.Trim("ab") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly 4", function() + local trimmedInput = Text.Trim(" ab ") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly 5", function() + local trimmedInput = Text.Trim(" a b ") + local expected = "a b" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly 6", function() + local trimmedInput = Text.Trim("\r\n\t\f a\r\n\t\f ") + local expected = "a" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string with unicode characters properly", function() + local trimmedInput = Text.Trim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly 7", function() + local trimmedInput = Text.Trim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly 8", function() + local trimmedInput = Text.Trim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n") + local expected = "😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + end) + + + describe("RightTrimString", function() + it("Should right trim the string properly 1", function() + local trimmedInput = Text.RightTrim("") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly 2", function() + local trimmedInput = Text.RightTrim(" ") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly 3", function() + local trimmedInput = Text.RightTrim("ab") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly 4", function() + local trimmedInput = Text.RightTrim(" ab ") + local expected = " ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly 5", function() + local trimmedInput = Text.RightTrim(" a b ") + local expected = " a b" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly 6", function() + local trimmedInput = Text.RightTrim("\r\n\t\f a\r\n\t\f ") + local expected = "\r\n\t\f a" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string with unicode characters properly", function() + local trimmedInput = Text.RightTrim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly 7", function() + local trimmedInput = Text.RightTrim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ") + local expected = " 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly 8", function() + local trimmedInput = Text.RightTrim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n") + local expected = "\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + end) + + + describe("LeftTrimString", function() + it("Should left trim the string properly 1", function() + local trimmedInput = Text.LeftTrim("") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly 2", function() + local trimmedInput = Text.LeftTrim(" ") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly 3", function() + local trimmedInput = Text.LeftTrim("ab") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly 4", function() + local trimmedInput = Text.LeftTrim(" ab ") + local expected = "ab " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly 5", function() + local trimmedInput = Text.LeftTrim(" a b ") + local expected = "a b " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly 6", function() + local trimmedInput = Text.LeftTrim("\r\n\t\f a\r\n\t\f ") + local expected = "a\r\n\t\f " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string with unicode characters properly", function() + local trimmedInput = Text.LeftTrim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly 7", function() + local trimmedInput = Text.LeftTrim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n") + local expected = "😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n" + expect(trimmedInput).to.equal(expected) + end) + end) + + + describe("SpaceNormalize", function() + it("should remove multiple spaces between words", function() + local a = "This is not a normal sentence." + + expect(Text.SpaceNormalize(a)).to.equal("This is not a normal sentence.") + end) + + it("should remove leading and trailing whitespace", function() + local a = " SpaceTabSpaceTab " + + expect(Text.SpaceNormalize(a)).to.equal("SpaceTabSpaceTab") + end) + + it("should not change a string with no whitespace", function() + local a = "There'sNo%Whit.e\\space--InThis." + + expect(Text.SpaceNormalize(a)).to.equal(a) + end) + + it("should remove all whitespace in a string that is nothing but whitespace", function() + local a = " " + + expect(Text.SpaceNormalize(a)).to.equal("") + end) + + it("should handle the case where the string is empty", function() + local a = "" + + expect(Text.SpaceNormalize(a)).to.equal(a) + end) + + it("should throw an error if called an a non-string type", function() + local a = { first = 1, second = 2 } + + expect(function() + Text.SpaceNormalize(a) + end).to.throw() + end) + end) + + + describe("Split", function() + local function tableEquals(tb1, tb2) + local tables = { tb1, tb2 } + + for _,tb in ipairs(tables) do + for key in pairs(tb) do + if tb1[key] ~= tb2[key] then + return false + end + end + end + + return true + end + + it("should return the correct table for your standard use case", function() + local a = "this,is,comma,separated" + local pattern = "," + local expectedResult = { + [1] = "this", + [2] = "is", + [3] = "comma", + [4] = "separated", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should not remove whitespace", function() + local a = " SpaceTab , , Space" + local pattern = "," + local expectedResult = { + [1] = " SpaceTab ", + [2] = " ", + [3] = " Space", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should treat regular expressions as plain text", function() + local a = "Notyour^%s+normalstring.Thisisasecondsentence." + local b = "." + local c = "^%s+" + local d = "%A" + + local expectedB = { + [1] = "Notyour^%s+normalstring", + [2] = "Thisisasecondsentence", + [3] = "", + } + local expectedC = { + [1] = "Notyour", + [2] = "normalstring.Thisisasecondsentence." + } + local expectedD = { + [1] = "Notyour^%s+normalstring.Thisisasecondsentence." + } + + expect(tableEquals(Text.Split(a, b), expectedB)).to.equal(true) + expect(tableEquals(Text.Split(a, c), expectedC)).to.equal(true) + expect(tableEquals(Text.Split(a, d), expectedD)).to.equal(true) + end) + + it("should work when pattern is not in string", function() + local a = "The pattern you are looking for does not exist." + local pattern = "," + local expectedResult = { + [1] = "The pattern you are looking for does not exist.", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should work when called on an empty string", function() + local a = "" + local pattern = "," + local expectedResult = { + [1] = "", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should throw an error if called on an empty pattern", function() + local a = "The pattern definitely doesn't exist here." + local pattern = "" + + expect(function() + Text.Split(a, pattern) + end).to.throw() + end) + + it("should throw an error if called an a non-string type", function() + local a = { first = 1, second = 2 } + local b = "an actual string" + + expect(function() + Text.Split(a, b) + end).to.throw() + + expect(function() + Text.Split(b, a) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/memoize.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/memoize.lua new file mode 100644 index 0000000..261dcd0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/memoize.lua @@ -0,0 +1,68 @@ +--[[ + memoize creates a function as a wrapper that caches the last outputs of a function. + This is useful if you know that the function should return the same output every + time it is run with the same inputs. The function should only return an output, and + not have any side effects. These side effects are not cached. + + Without memoize's caching, even though the function ouputs the same values, the + memory locations of the values are different; tables made in the function, even if + they have the same values, won't be the same tables. + + memoize only caches the last set of inputs and ouputs. This means that it is only + helpful when the function is likely to be called with the same inputs multiple + times in a row. This is the case with most Roact use cases. + + Note that memoize only does a ** shallow check on table inputs ** . This means + that if the same table is input but the elements of the table are different then + it will be assumed that the table has not changed. + + In addition to all the previous warnings, memoize strips trailing nils. This means + that if foo is a memoized function and we call foo(), then foo(nil) will return a + cached value. This is opposed to how print handles input. print() only outputs a + new line, but print(nil) outputs "nil". This is because varargs can detect the + number of arguments passed in. So, be careful when using memoize with varargs. + Trailing nils will be stripped. + + The wrapper can take any number of inputs and give any number of outputs. + Leading and interspersed nils are handled gracefully. Trailing nils on the input + are stripped. +]] +local function captureSize(...) + return {...}, select("#", ...) +end + +local function memoize(func) + assert(type(func) == "function", "memoize requires a function to memoize") + + local lastArgs + local lastNumArgs + local lastOutput + local lastNumOutput + + return function(...) + local numArgs = select("#", ...) + + while numArgs > 0 and select(numArgs, ...) == nil do + numArgs = numArgs - 1 + end + + if numArgs ~= lastNumArgs then + lastArgs = {...} + lastNumArgs = numArgs + lastOutput, lastNumOutput = captureSize(func(...)) + return unpack(lastOutput, 1, lastNumOutput) + end + + for i = 1, lastNumArgs do + if select(i, ...) ~= lastArgs[i] then + lastArgs = {...} + lastOutput, lastNumOutput = captureSize(func(...)) + break + end + end + + return unpack(lastOutput, 1, lastNumOutput) + end +end + +return memoize \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/memoize.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/memoize.spec.lua new file mode 100644 index 0000000..5ae2b6e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Common/memoize.spec.lua @@ -0,0 +1,213 @@ +return function() + local memoize = require(script.Parent.memoize) + + describe("memoize", function() + it("should handle arity 0", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + expect(identity()).to.equal(nil) + expect(identity(nil)).to.equal(nil) + expect(identity(nil, nil)).to.equal(nil) + expect(callCount).to.equal(1) + end) + + it("should handle arity 1", function() + local callCount = 0 + local identity = memoize(function(a) + callCount = callCount + 1 + return a + end) + + expect(identity(5)).to.equal(5) + expect(identity(5)).to.equal(5) + expect(callCount).to.equal(1) + + expect(identity(6)).to.equal(6) + expect(callCount).to.equal(2) + + expect(identity(5)).to.equal(5) + expect(callCount).to.equal(3) + end) + + it("should handle arity 2", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + expect(callCount).to.equal(1) + + a, b = identity(6, 5) + expect(a).to.equal(6) + expect(b).to.equal(5) + + expect(callCount).to.equal(2) + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + expect(callCount).to.equal(3) + end) + + it("should handle mixed arity", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + expect(callCount).to.equal(1) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b = identity() + expect(a).to.equal(nil) + expect(b).to.equal(nil) + + a, b = identity() + expect(a).to.equal(nil) + expect(b).to.equal(nil) + + expect(callCount).to.equal(3) + end) + + it("should handle trailing nils", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(5, nil) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + expect(callCount).to.equal(1) + + a, b = identity(7) + expect(a).to.equal(7) + expect(b).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + expect(callCount).to.equal(3) + end) + + it("should handle leading nils", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(nil, 7) + expect(a).to.equal(nil) + expect(b).to.equal(7) + + a, b = identity(nil, 7) + expect(a).to.equal(nil) + expect(b).to.equal(7) + + expect(callCount).to.equal(1) + + a, b = identity(7) + expect(a).to.equal(7) + expect(b).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b = identity(nil, 7) + expect(a).to.equal(nil) + expect(b).to.equal(7) + + expect(callCount).to.equal(3) + end) + + it("should handle interspersed nils", function() + local callCount = 0 + local identity = memoize(function(a, b, c, d) + callCount = callCount + 1 + return a, b, c, d + end) + + local a, b, c, d + + a, b, c, d = identity(7, nil, 7, nil) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(7) + expect(d).to.equal(nil) + + -- Trailing nils can affect how interspersed nils are handled + a, b, c, d = identity(7, nil, 7) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(7) + expect(d).to.equal(nil) + + expect(callCount).to.equal(1) + + a, b, c, d = identity(7, nil, nil, nil) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(nil) + expect(d).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b, c, d = identity(7, nil, 7, nil) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(7) + expect(d).to.equal(nil) + + expect(callCount).to.equal(3) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/.robloxrc new file mode 100644 index 0000000..635c2ec --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/.robloxrc @@ -0,0 +1,5 @@ +{ + "lint": { + "LocalShadow": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/AddUser.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/AddUser.lua new file mode 100644 index 0000000..7ac8be2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/AddUser.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(user) + return { + user = user + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/AddUsers.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/AddUsers.lua new file mode 100644 index 0000000..fad0bcb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/AddUsers.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(users) + return { + users = users + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsCompleted.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsCompleted.lua new file mode 100644 index 0000000..b90860d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsCompleted.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId) + return { + userId = userId + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsFailed.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsFailed.lua new file mode 100644 index 0000000..a4beb24 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsFailed.lua @@ -0,0 +1,9 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId, response) + return { + userId = userId, + response = response + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsStarted.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsStarted.lua new file mode 100644 index 0000000..b90860d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsStarted.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId) + return { + userId = userId + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/ReceivedPlacesInfos.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/ReceivedPlacesInfos.lua new file mode 100644 index 0000000..266028f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/ReceivedPlacesInfos.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(placesInfos) + return { + placesInfos = placesInfos, + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/RemoveUser.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/RemoveUser.lua new file mode 100644 index 0000000..b407b15 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/RemoveUser.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId) + return { + userId = userId, + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetDeviceOrientation.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetDeviceOrientation.lua new file mode 100644 index 0000000..6cfa228 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetDeviceOrientation.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(deviceOrientation) + return { + deviceOrientation = deviceOrientation, + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetFriendCount.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetFriendCount.lua new file mode 100644 index 0000000..39aa112 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetFriendCount.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") +local Common = CorePackages.AppTempCommon.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(count) + return { + count = count, + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameIcons.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameIcons.lua new file mode 100644 index 0000000..458f12c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameIcons.lua @@ -0,0 +1,14 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) +local ArgCheck = require(CorePackages.ArgCheck) + +--[[ + Each entry in the table is a type of GameIcon with the universe id as key +]] +return Action(script.Name, function(iconsTable) + ArgCheck.isType(iconsTable, "table", "iconsTable") + + return { + gameIcons = iconsTable + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameIcons.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameIcons.spec.lua new file mode 100644 index 0000000..bc1086f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameIcons.spec.lua @@ -0,0 +1,27 @@ +return function() + local SetGameIcons = require(script.Parent.SetGameIcons) + + it("should assert if given a non-table for thumbnailsTable", function() + SetGameIcons({}) + + expect(function() + SetGameIcons("string") + end).to.throw() + + expect(function() + SetGameIcons(0) + end).to.throw() + + expect(function() + SetGameIcons(nil) + end).to.throw() + + expect(function() + SetGameIcons(false) + end).to.throw() + + expect(function() + SetGameIcons(function() end) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameThumbnails.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameThumbnails.lua new file mode 100644 index 0000000..8677990 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameThumbnails.lua @@ -0,0 +1,26 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +--[[ + Passes a table that looks like this : { "universeId" : {json}, ... } + + { + "26034470" : { + universeId : "26034470", + placeId : "70542190", + url : https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814, + final : true + }, {...}, ... + } + +]] + +--TODO MOBLUAPP-778 Refactor improper Setter Actions. +return Action(script.Name, function(thumbnailsTable) + assert(type(thumbnailsTable) == "table", + string.format("SetGameThumbnails action expects thumbnailsTable to be a table, was %s", type(thumbnailsTable))) + + return { + thumbnails = thumbnailsTable + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameUserIsPlaying.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameUserIsPlaying.lua new file mode 100644 index 0000000..7743f8f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameUserIsPlaying.lua @@ -0,0 +1,11 @@ +local CorePackages = game:GetService("CorePackages") +local Common = CorePackages.AppTempCommon.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(userId, universeId) + return { + userId = userId, + universeId = universeId, + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserIsFriend.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserIsFriend.lua new file mode 100644 index 0000000..690a05e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserIsFriend.lua @@ -0,0 +1,9 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId, isFriend) + return { + userId = userId, + isFriend = isFriend, + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserMembershipType.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserMembershipType.lua new file mode 100644 index 0000000..1c08dbe --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserMembershipType.lua @@ -0,0 +1,11 @@ +local CorePackages = game:GetService("CorePackages") +local Common = CorePackages.AppTempCommon.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(userId, membershipType) + return { + userId = userId, + membershipType = membershipType, + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserPresence.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserPresence.lua new file mode 100644 index 0000000..b25a118 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserPresence.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId, presence, lastLocation) + return { + userId = tostring(userId), + presence = presence, + lastLocation = lastLocation, + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserThumbnail.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserThumbnail.lua new file mode 100644 index 0000000..508cf70 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserThumbnail.lua @@ -0,0 +1,13 @@ +local CorePackages = game:GetService("CorePackages") +local Common = CorePackages.AppTempCommon.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(userId, image, thumbnailType, thumbnailSize) + return { + userId = userId, + image = image, + thumbnailType = thumbnailType, + thumbnailSize = thumbnailSize, + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/UpdateFetchingStatus.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/UpdateFetchingStatus.lua new file mode 100644 index 0000000..1fc68b2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/UpdateFetchingStatus.lua @@ -0,0 +1,9 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(key, status) + return { + key = key, + status = status + } +end) diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/UpdateFetchingStatus.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/UpdateFetchingStatus.spec.lua new file mode 100644 index 0000000..1cb23fe --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/UpdateFetchingStatus.spec.lua @@ -0,0 +1,21 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus) + + describe("Action UpdateFetchingStatus", function() + it("should return correct action name", function() + expect(UpdateFetchingStatus.name).to.equal("UpdateFetchingStatus") + end) + + it("should return correct action type name", function() + local action = UpdateFetchingStatus() + expect(action.type).to.equal(UpdateFetchingStatus.name) + end) + + it("should return a table with the correct key and status", function() + local action = UpdateFetchingStatus("key", "status") + expect(action.key).to.equal("key") + expect(action.status).to.equal("status") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/LoadingBar.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/LoadingBar.lua new file mode 100644 index 0000000..dfe129a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/LoadingBar.lua @@ -0,0 +1,51 @@ +local Workspace = game:GetService("Workspace") +local RunService = game:GetService('RunService') +local CorePackages = game:GetService("CorePackages") + +local Roact = require(CorePackages.Roact) + +local BAR_SLICE_CENTER = Rect.new(1, 0, 2, 3) +local BAR_MAX_SIZE = 15 +local BAR_MAX_AMPLITUDE = 40 +local BAR_DIAMETER = 4 +local BAR_PERIOD = 1.25 + +local LoadingBar = Roact.Component:extend("LoadingBar") + +function LoadingBar:init() + self.barRef = Roact.createRef() +end + +function LoadingBar:render() + local zIndex = self.props.ZIndex + + return Roact.createElement("ImageLabel", { + Image = "rbxasset://textures/ui/LuaApp/9-slice/gr-loading-indicator.png", + ScaleType = "Slice", + SliceCenter = BAR_SLICE_CENTER, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ZIndex = zIndex, + [Roact.Ref] = self.barRef, + }) +end + +function LoadingBar:didMount() + self.connection = RunService.RenderStepped:Connect(function() + local t = Workspace.DistributedGameTime + local instance = self.barRef.current + local period = 2.0 * math.pi / BAR_PERIOD + + local width = (BAR_MAX_SIZE/2) * (1 - math.cos(2*t*period)) + instance.Size = UDim2.new(0, BAR_DIAMETER + width, 0, BAR_DIAMETER) + + local x = BAR_MAX_AMPLITUDE * math.cos(t*period) + instance.Position = UDim2.new(0.5, x - width/2 - BAR_DIAMETER/2, 0.5, 0) + end) +end + +function LoadingBar:willUnmount() + self.connection:Disconnect() +end + +return LoadingBar \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/LoadingBar.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/LoadingBar.spec.lua new file mode 100644 index 0000000..163e7d8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/LoadingBar.spec.lua @@ -0,0 +1,17 @@ +return function() + local LoadingBar = require(script.Parent.LoadingBar) + + local CorePackages = game:GetService("CorePackages") + + local Roact = require(CorePackages.Roact) + + it("should create and destroy without errors", function() + local element = Roact.createElement(LoadingBar, { + Position = UDim2.new(0.5, 0, 0.5, 5), + AnchorPoint = Vector2.new(0.5, 0.5), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/AvatarThumbnailTypes.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/AvatarThumbnailTypes.lua new file mode 100644 index 0000000..67a7373 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/AvatarThumbnailTypes.lua @@ -0,0 +1,4 @@ +return { + AvatarThumbnail = "AvatarThumbnail", + HeadShot = "HeadShot", +} diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/RetrievalStatus.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/RetrievalStatus.lua new file mode 100644 index 0000000..0ea027a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/RetrievalStatus.lua @@ -0,0 +1,21 @@ +local RetrievalStatus = {} + +local EnumValues = +{ + NotStarted = "NotStarted", + Fetching = "Fetching", + Done = "Done", + Failed = "Failed", +} + +setmetatable(RetrievalStatus, + { + __newindex = function(t, key, index) + end, + __index = function(t, index) + assert(EnumValues[index] ~= nil, ("RetrievalStatus Enum has no value: " .. tostring(index))) + return EnumValues[index] + end + }) + +return RetrievalStatus \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/WebPresenceMap.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/WebPresenceMap.lua new file mode 100644 index 0000000..5d312c8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/WebPresenceMap.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") + +local User = require(CorePackages.AppTempCommon.LuaApp.Models.User) + +return { + [0] = User.PresenceType.OFFLINE, + [1] = User.PresenceType.ONLINE, + [2] = User.PresenceType.IN_GAME, + [3] = User.PresenceType.IN_STUDIO, +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/AvatarEditorNewCatalogEnabled.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/AvatarEditorNewCatalogEnabled.lua new file mode 100644 index 0000000..4fa53ff --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/AvatarEditorNewCatalogEnabled.lua @@ -0,0 +1,15 @@ +local CorePackages = game:GetService("CorePackages") + +local ThrottleUserId = require(CorePackages.AppTempCommon.LuaApp.Utils.ThrottleUserId) + +local FIntAvatarEditorNewCatalogButton = settings():GetFVariable("AvatarEditorNewCatalogButton2") + +return function(userId) + if tonumber(userId) then + local throttleNumber = tonumber(FIntAvatarEditorNewCatalogButton) + local id = tonumber(userId) + return ThrottleUserId(throttleNumber, id) + else + return false + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/ConvertUniverseIdToString.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/ConvertUniverseIdToString.lua new file mode 100644 index 0000000..8e3a7f8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/ConvertUniverseIdToString.lua @@ -0,0 +1,11 @@ +-- TODO: Delete this file when deleting the flag: LuaAppConvertUniverseIdToStringV364 +local FFlagLuaAppConvertUniverseIdToString = settings():GetFFlag("LuaAppConvertUniverseIdToStringV364") + +return function(universeId) + -- When the flag is on, we've converted the universe id to string at the place we received it + if FFlagLuaAppConvertUniverseIdToString then + return universeId + else + return tostring(universeId) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/GetEnableFriendFooterOnHomePage.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/GetEnableFriendFooterOnHomePage.lua new file mode 100644 index 0000000..ae6c0aa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/GetEnableFriendFooterOnHomePage.lua @@ -0,0 +1,15 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local ThrottleUserId = require(CorePackages.AppTempCommon.LuaApp.Utils.ThrottleUserId) + +local FIntEnableFriendFooterOnHomePage = settings():GetFVariable("EnableFriendFooterOnHomePageV369") + +-- Don't call this function globally because we cannot get the userId +-- Reason: The LocalPlayer wouldn't be ready if we called it globally. +return function() + local throttleNumber = tonumber(FIntEnableFriendFooterOnHomePage) + local userId = Players.LocalPlayer.UserId + + return ThrottleUserId(throttleNumber, userId) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/GetFFlagLuaAppFixLightTheme.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/GetFFlagLuaAppFixLightTheme.lua new file mode 100644 index 0000000..36c5870 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/GetFFlagLuaAppFixLightTheme.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("LuaAppFixLightTheme", false) + +return function() + return game:GetFastFlag("LuaAppFixLightTheme") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/GetFFlagUseDateTimeType.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/GetFFlagUseDateTimeType.lua new file mode 100644 index 0000000..7a2bbbd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/GetFFlagUseDateTimeType.lua @@ -0,0 +1,3 @@ +return function() + return settings():GetFFlag("UseDateTimeType3") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatSendGameLinkMessage.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatSendGameLinkMessage.lua new file mode 100644 index 0000000..0306124 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatSendGameLinkMessage.lua @@ -0,0 +1,19 @@ +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +return function(requestImpl, conversationId, universeId, decorators) + assert(requestImpl, "requestImpl is required") + assert(conversationId, "conversationId is required") + assert(universeId, "universeId is required") + + local payload = HttpService:JSONEncode({ + conversationId = conversationId, + universeId = universeId, + decorators = decorators + }) + local url = string.format("%s/send-game-link-message", Url.CHAT_URL) + + return requestImpl(url, "POST", { postBody = payload }) +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatSendMessage.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatSendMessage.lua new file mode 100644 index 0000000..bdf394a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatSendMessage.lua @@ -0,0 +1,16 @@ +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +return function(requestImpl, conversationId, messageText, decorators) + local payload = HttpService:JSONEncode({ + conversationId = conversationId, + message = messageText, + decorators = decorators + }) + + local url = string.format("%s/send-message", Url.CHAT_URL) + + return requestImpl(url, "POST", { postBody = payload }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatStartOneToOneConversation.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatStartOneToOneConversation.lua new file mode 100644 index 0000000..4f7a75f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatStartOneToOneConversation.lua @@ -0,0 +1,14 @@ +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +return function(requestImpl, userId, clientId) + local payload = HttpService:JSONEncode({ + participantuserId = userId + }) + + local url = string.format("%s/start-one-to-one-conversation", Url.CHAT_URL) + + return requestImpl(url, "POST", { postBody = payload }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesGetIcons.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesGetIcons.lua new file mode 100644 index 0000000..bc5548d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesGetIcons.lua @@ -0,0 +1,25 @@ +local CorePackages = game:GetService("CorePackages") +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +--[[ + Docs: https://thumbnails.roblox.com/docs#!/Games/get_v1_games_icons + This resolves to + { + "data": [ + { + "targetId": 0, + "state": "Error", + "imageUrl": "string" + } + ] +} +]] +return function (requestImpl, universeIds, size) + local qs = Url:makeQueryString({ + universeIds = table.concat(universeIds, ","), + format = "png", + size = size, + }) + local url = string.format("%sv1/games/icons?%s", Url.THUMBNAILS_URL, qs) + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesGetThumbnails.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesGetThumbnails.lua new file mode 100644 index 0000000..fb930f8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesGetThumbnails.lua @@ -0,0 +1,54 @@ +--[[ + *** DEPRECATED *** + TODO: removed this file after new thumbnail API is being in use without any flags + RELATED: GAMEDISC-27 GAMEDISC-126 FIntLuaAppPercentRollOutNewThumbnailsApiV3 +]] + +local CorePackages = game:GetService("CorePackages") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +--[[ + This endpoint returns a promise that resolves to: + [ + { + "final": true, + "url": "string", + "retryToken": "string", + "universeId": 0, + "placeId": 0 + }, {...}, ... + ] +]] + +-- requestImpl - (function>(url, requestMethod, options)) +-- imageTokens - (array) the placeIds of the places you want to get thumbnails for +-- height - (int) the height of the asset to render +-- width - (int) the width of the asset to render +return function(requestImpl, imageTokens, height, width) + local args = {} + + if height then + table.insert(args, string.format("height=%d", height)) + end + + if width then + table.insert(args, string.format("width=%d", width)) + end + + -- append all of the thumbnail tokens + local totalTokens = 0 + for _, value in pairs(imageTokens) do + totalTokens = totalTokens + 1 + table.insert(args, string.format("imageTokens=%s", value)) + end + if totalTokens == 0 then + error("cannot fetch thumbnails without tokens") + end + + -- construct the url + local url = string.format("%sv1/games/game-thumbnails?%s", Url.GAME_URL, table.concat(args, "&")) + + -- return a promise of the result listed above + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesMultigetPlaceDetails.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesMultigetPlaceDetails.lua new file mode 100644 index 0000000..65728aa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesMultigetPlaceDetails.lua @@ -0,0 +1,33 @@ +local CorePackages = game:GetService("CorePackages") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +--[[ + This endpoint returns games' information with a batches of place ids + Doc: https://games.roblox.com/docs#!/Games/get_v1_games_multiget_place_details + { + "placeId": 0, + "name": "string", + "description": "string", + "url": "string", + "builder": "string", + "builderId": 0, + "isPlayable": true, + "reasonProhibited": "string", + "universeId": 0, + "universeRootPlaceId": 0, + "price": 0, + "imageToken": "string" + } +]]-- + +return function(requestImpl, placeIds) + local argTable = { + placeIds = placeIds, + } + + local args = Url:makeQueryString(argTable) + local url = string.format("%s/v1/games/multiget-place-details?%s", Url.GAME_URL, args) + + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GetPlaceInfos.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GetPlaceInfos.lua new file mode 100644 index 0000000..883bc34 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GetPlaceInfos.lua @@ -0,0 +1,17 @@ +local CorePackages = game:GetService("CorePackages") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +return function(requestImpl, placeIds) + local argTable = { + placeIds = placeIds, + } + + -- construct the url + local args = Url:makeQueryString(argTable) + local url = string.format("%s/v1/games/multiget-place-details?%s", + Url.GAME_URL, args + ) + + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ThumbnailsGetAvatar.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ThumbnailsGetAvatar.lua new file mode 100644 index 0000000..8ff1525 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ThumbnailsGetAvatar.lua @@ -0,0 +1,39 @@ +local CorePackages = game:GetService("CorePackages") +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +--[[ + Documentation of endpoint: + https://thumbnails.roblox.com/docs#!/Avatar/get_v1_users_avatar + + input: + userIds + thumbnailSize + output: + [ + { + "targetId": number, + "state": string, + "imageUrl": string, + }, + ] +]] + +local MAX_USER_IDS = 100 + +return function (networkImpl, userIds, thumbnailSize) + assert(type(userIds) == "table", "ThumbnailsGetAvatar expects userIds to be a table") + + if #userIds == 0 or #userIds > MAX_USER_IDS then + error(string.format("ThumbnailsGetAvatar request expects userIds count between 1-%d", MAX_USER_IDS)) + end + + local queryString = Url:makeQueryString({ + userIds = table.concat(userIds, ","), + size = thumbnailSize, + format = "png", + }) + + local url = string.format("%sv1/users/avatar?%s", Url.THUMBNAILS_URL, queryString) + + return networkImpl(url, "GET") +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ThumbnailsGetAvatarHeadshot.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ThumbnailsGetAvatarHeadshot.lua new file mode 100644 index 0000000..58fc9cd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ThumbnailsGetAvatarHeadshot.lua @@ -0,0 +1,39 @@ +local CorePackages = game:GetService("CorePackages") +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +--[[ + Documentation of endpoint: + https://thumbnails.roblox.com/docs#!/Avatar/get_v1_users_avatar_headshot + + input: + userIds + thumbnailSize + output: + [ + { + "targetId": number, + "state": string, + "imageUrl": string, + }, + ] +]] + +local MAX_USER_IDS = 100 + +return function (networkImpl, userIds, thumbnailSize) + assert(type(userIds) == "table", "ThumbnailsGetAvatarHeadshot expects userIds to be a table") + + if #userIds == 0 or #userIds > MAX_USER_IDS then + error(string.format("ThumbnailsGetAvatarHeadshot request expects userIds count between 1-%d", MAX_USER_IDS)) + end + + local queryString = Url:makeQueryString({ + userIds = table.concat(userIds, ","), + size = thumbnailSize, + format = "png", + }) + + local url = string.format("%sv1/users/avatar-headshot?%s", Url.THUMBNAILS_URL, queryString) + + return networkImpl(url, "GET") +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriendCount.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriendCount.lua new file mode 100644 index 0000000..91ede51 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriendCount.lua @@ -0,0 +1,31 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +local isNewFriendsEndpointsEnabled = require(CorePackages.AppTempCommon.LuaChat.Flags.isNewFriendsEndpointsEnabled) + +--[[ + This endpoint returns a promise that resolves to: + + [ + { + "success:" true, + "count": "0" + }, + ] +]]-- + +-- requestImpl - (function>(url, requestMethod, options)) +return function(requestImpl) + + local url = string.format("%s/user/get-friendship-count?%s", + Url.API_URL, tostring(Players.LocalPlayer.UserId) + ) + + if isNewFriendsEndpointsEnabled() then + url = string.format("%s/my/friends/count", Url.FRIEND_URL) + end + + return requestImpl(url, "GET") +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriends.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriends.lua new file mode 100644 index 0000000..79b8653 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriends.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +return function(requestImpl, userId) + local url = string.format("%s/users/%s/friends", + Url.FRIEND_URL, userId + ) + + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetPresence.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetPresence.lua new file mode 100644 index 0000000..b054e6e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetPresence.lua @@ -0,0 +1,25 @@ +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +-- Endpoint documented here: +-- https://presence.roblox.com/docs + +return function(requestImpl, userIds) + local userIdsToNumber = {} + for _, id in pairs(userIds) do + local idToNumber = tonumber(id) + if idToNumber then + table.insert(userIdsToNumber, idToNumber) + end + end + + local payload = HttpService:JSONEncode({ + userIds = userIdsToNumber, + }) + + local url = string.format("%s/presence/users", Url.PRESENCE_URL) + + return requestImpl(url, "POST", { postBody = payload }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetThumbnail.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetThumbnail.lua new file mode 100644 index 0000000..78d181e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetThumbnail.lua @@ -0,0 +1,48 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) + +local THUMBNAIL_TYPE_BY_NAME = { + AvatarThumbnail = Enum.ThumbnailType.AvatarThumbnail, + HeadShot = Enum.ThumbnailType.HeadShot, +} + +local THUMBNAIL_SIZE_BY_NAME = { + Size48x48 = Enum.ThumbnailSize.Size48x48, + Size60x60 = Enum.ThumbnailSize.Size60x60, + Size100x100 = Enum.ThumbnailSize.Size100x100, + Size150x150 = Enum.ThumbnailSize.Size150x150, + Size352x352 = Enum.ThumbnailSize.Size352x352 +} + +return function(userId, thumbnailType, thumbnailSize) + return Promise.new(function(resolve, reject) + --Async methods will yield the thread + spawn(function() + local result = {success = false} + local success, message = pcall(function() + local image, isFinal = Players:GetUserThumbnailAsync( + tonumber(userId), THUMBNAIL_TYPE_BY_NAME[thumbnailType], THUMBNAIL_SIZE_BY_NAME[thumbnailSize] + ) + + result = { + success = true, + id = userId, + thumbnailType = thumbnailType, + thumbnailSize = thumbnailSize, + + image = isFinal and image or nil, + isFinal = isFinal, + } + end) + + if success then + resolve(result) + else + result.message = message + reject(result) + end + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Url.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Url.lua new file mode 100644 index 0000000..cdf2ef2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Url.lua @@ -0,0 +1,181 @@ +--[[ + Url Constructor + + Provides a single location for base urls. + +]]-- +local ContentProvider = game:GetService("ContentProvider") + +local FFlagLuaFixEconomyCreatorStatsUrl = game:DefineFastFlag("LuaFixEconomyCreatorStatsUrl", false) + +-- helper functions +local function parseBaseUrlInformation() + -- get the current base url from the current configuration + local baseUrl = ContentProvider.BaseUrl + + -- keep a copy of the base url (https://www.roblox.com/) + -- append a trailing slash if there isn't one + if baseUrl:sub(#baseUrl) ~= "/" then + baseUrl = baseUrl .. "/" + end + + -- parse out scheme (http, https) + local _, schemeEnd = baseUrl:find("://") + + -- parse out the prefix (www, kyle, ying, etc.) + local prefixIndex, prefixEnd = baseUrl:find("%.", schemeEnd + 1) + local basePrefix = baseUrl:sub(schemeEnd + 1, prefixIndex - 1) + + -- parse out the domain (roblox.com/, sitetest1.robloxlabs.com/, etc.) + local baseDomain = baseUrl:sub(prefixEnd + 1) + + return baseUrl, basePrefix, baseDomain +end +local function preventTableModification(aTable, key, value) + error("Attempt to modify read-only table") +end +local function createReadOnlyTable(aTable) + return setmetatable({}, { + __index = aTable, + __newindex = preventTableModification, + __metatable = false + }); +end + + +-- url construction building blocks +local _baseUrl, _basePrefix, _baseDomain = parseBaseUrlInformation() + +-- construct urls once +local _baseApiUrl = string.format("https://api.%s", _baseDomain) +local _baseApisUrl = string.format("https://apis.%s", _baseDomain) +local _baseAuthUrl = string.format("https://auth.%s", _baseDomain) +local _baseAccountSettingsUrl = string.format("https://accountsettings.%s", _baseDomain) +local _baseAvatarUrl = string.format("https://avatar.%s", _baseDomain) +local _baseCatalogUrl = string.format("https://catalog.%s", _baseDomain) +local _baseInventoryUrl = string.format("https://inventory.%s", _baseDomain) +local _baseChatUrl = string.format("https://chat.%sv2", _baseDomain) +local _baseFriendUrl = string.format("https://friends.%sv1", _baseDomain) +local _baseGameAssetUrl = string.format("https://assetgame.%s", _baseDomain) +local _baseGamesUrl = string.format("https://games.%s", _baseDomain) +local _baseGroupsUrl = string.format("https://groups.%s", _baseDomain) +local _baseNotificationUrl = string.format("https://notifications.%s", _baseDomain) +local _basePresenceUrl = string.format("https://presence.%sv1", _baseDomain) +local _baseRealtimeUrl = string.format("https://realtime.%s", _baseDomain) +local _baseWebUrl = string.format("https://web.%s", _baseDomain) +local _baseWwwUrl = string.format("https://www.%s", _baseDomain) +local _baseAdsUrl = string.format("https://ads.%s", _baseDomain) +local _baseFollowingsUrl = string.format("https://followings.%s", _baseDomain) +local _baseEconomyUrl = string.format("https://economy.%s", _baseDomain) +local _baseThumbnailsUrl = string.format("https://thumbnails.%s", _baseDomain) +local _baseAccountSettings = string.format("https://accountsettings.%s", _baseDomain) +local _basePremiumFeatures = string.format("https://premiumfeatures.%s", _baseDomain) +local _baseLocale = string.format("https://locale.%s", _baseDomain) +local _baseBadgesUrl = string.format("https://badges.%s", _baseDomain) +local _baseMetricsUrl = string.format("https://metrics.%sv1", _baseDomain) +local _baseApisRcsUrl = string.format("https://apis.rcs.%s", _baseDomain) +local _baseDiscussionsUrl = string.format("https://discussions.%s", _baseDomain) +local _baseContactsUrl = string.format("https://contacts.%s", _baseDomain) +local _baseSearchUrl = string.format("https://search.%s", _baseDomain) +local _baseStaticUrl = string.format("https://static.%s", _baseDomain) +local _baseGameSearchUITreatments = string.format("https://gamesearchuitreatments.api.%s", _baseDomain) +local _baseEconomyCreatorStats = FFlagLuaFixEconomyCreatorStatsUrl + and string.format("https://economycreatorstats.%s", _baseDomain) + or string.format("https://economycreatorstats.api.%s", _baseDomain) +local _baseUrlSecure = string.gsub(_baseUrl, "http://", "https://") + +-- public api +local Url = { + DOMAIN = _baseDomain, + PREFIX = _basePrefix, + BASE_URL = _baseUrl, + BASE_URL_SECURE = _baseUrlSecure, + API_URL = _baseApiUrl, + APIS_URL = _baseApisUrl, + AUTH_URL = _baseAuthUrl, + ACCOUNT_SETTINGS_URL = _baseAccountSettingsUrl, + AVATAR_URL = _baseAvatarUrl, + CATALOG_URL = _baseCatalogUrl, + INVENTORY_URL = _baseInventoryUrl, + GAME_URL = _baseGamesUrl, + GAME_ASSET_URL = _baseGameAssetUrl, + GROUPS_URL = _baseGroupsUrl, + CHAT_URL = _baseChatUrl, + FRIEND_URL = _baseFriendUrl, + PRESENCE_URL = _basePresenceUrl, + NOTIFICATION_URL = _baseNotificationUrl, + REALTIME_URL = _baseRealtimeUrl, + WEB_URL = _baseWebUrl, + WWW_URL = _baseWwwUrl, + ADS_URL = _baseAdsUrl, + SEARCH_URL = _baseSearchUrl, + GAME_SEARCH_UI_TREATMENTS = _baseGameSearchUITreatments, + FOLLOWINGS_URL = _baseFollowingsUrl, + ECONOMY_URL = _baseEconomyUrl, + THUMBNAILS_URL = _baseThumbnailsUrl, + BADGES_URL = _baseBadgesUrl, + ACCOUNT_SETTINGS = _baseAccountSettings, + PREMIUM_FEATURES = _basePremiumFeatures, + LOCALE = _baseLocale, + METRICS_URL = _baseMetricsUrl, + APIS_RCS_URL = _baseApisRcsUrl, + DISCUSSIONS_URL = _baseDiscussionsUrl, + CONTACTS_URL = _baseContactsUrl, + STATIC_URL = _baseStaticUrl, + BLOG_URL = "https://blog.roblox.com/", + CORP_URL = "https://corp.roblox.com/", + ECNOMY_CREATOR_STATS = _baseEconomyCreatorStats, +} + +function Url:getUserProfileUrl(userId) + return string.format("%susers/%s/profile", self.BASE_URL, userId) +end + +function Url:getUserFriendsUrl(userId) + return string.format("%susers/%s/friends", self.BASE_URL, userId) +end + +function Url:getUserInventoryUrl(userId) + return string.format("%susers/%s/inventory", self.BASE_URL, userId) +end + +function Url:getPlaceDefaultThumbnailUrl(placeId, width, height) + return string.format( + "%sThumbs/Asset.ashx?width=%d&height=%d&assetId=%s&ignorePlaceMediaItems=true", + self.BASE_URL, + width, + height, + tostring(placeId)) +end + +function Url:isVanitySite() + return self.PREFIX ~= "www" +end + +-- data - (table) a table of key/value pairs to format +function Url:makeQueryString(data) + --NOTE - This function can be used to create a query string of parameters + -- at the end of url query, or create a application/form-url-encoded post body string + local params = {} + + -- NOTE - Arrays are handled, but generally data is expected to be flat. + for key, value in pairs(data) do + if value ~= nil then --for optional params + if type(value) == "table" then + for i = 1, #value do + table.insert(params, key .. "=" .. value[i]) + end + else + table.insert(params, key .. "=" .. tostring(value)) + end + end + end + + return table.concat(params, "&") +end + + +-- prevent anyone from modifying this table: +Url = createReadOnlyTable(Url) + +return Url diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Url.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Url.spec.lua new file mode 100644 index 0000000..62b8fab --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Url.spec.lua @@ -0,0 +1,12 @@ +return function() + + local ContentProvider = game:GetService("ContentProvider") + local Url = require(script.Parent.Url) + + it("The base url has not been changed for debugging", function() + local baseUrl = ContentProvider.BaseUrl + + expect(baseUrl).to.equal(Url.BASE_URL) + end) + +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/MockId.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/MockId.lua new file mode 100644 index 0000000..92671ed --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/MockId.lua @@ -0,0 +1,17 @@ +--[[ + A function to return a fake ID, used for testing. + + We turn all IDs into strings as we typically use them as keys in the state. + It's better to use a string than a number, because a number would indicate + an array index. + + Roblox APIs expect to be given integers for IDs however, so just tonumber() + the ID in this case. +]] + +local lastId = 0 + +return function() + lastId = lastId + 1 + return tostring(lastId) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/.robloxrc new file mode 100644 index 0000000..fc3d643 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/.robloxrc @@ -0,0 +1,5 @@ +{ + "language": { + "mode": "nonstrict" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/Thumbnail.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/Thumbnail.lua new file mode 100644 index 0000000..27d4970 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/Thumbnail.lua @@ -0,0 +1,39 @@ +--[[ + { + universeId : string, + state : string, + url : string, + } +]] + +local Thumbnail = {} + +function Thumbnail.new() + local self = {} + + return self +end + +function Thumbnail.fromThumbnailData(thumbnailData, size) + local self = Thumbnail.new() + + self.universeId = tostring(thumbnailData.targetId) + self.state = thumbnailData.state + self.url = thumbnailData.imageUrl + self.size = size + + return self +end + +function Thumbnail.isCompleteThumbnailData(thumbnailData) + return type(thumbnailData) == "table" + and type(thumbnailData.targetId) == "number" + and type(thumbnailData.state) == "string" + and (type(thumbnailData.imageUrl) == "string" or thumbnailData.imageUrl == nil) +end + +function Thumbnail.checkStateIsFinal(thumbnailState) + return thumbnailState ~= "Pending" +end + +return Thumbnail \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/Thumbnail.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/Thumbnail.spec.lua new file mode 100644 index 0000000..d4c144d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/Thumbnail.spec.lua @@ -0,0 +1,20 @@ +return function() + local Thumbnail = require(script.Parent.Thumbnail) + + it("should set fields without errors", function() + local testData = + { + targetId = 123456, + state = "Completed", + imageUrl = "a url", + } + + local thumbnail = Thumbnail.fromThumbnailData(testData) + + expect(thumbnail).to.be.a("table") + expect(thumbnail.universeId).to.equal("123456") + expect(thumbnail.state).to.equal("Completed") + expect(thumbnail.url).to.equal("a url") + end) + +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/ThumbnailRequest.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/ThumbnailRequest.lua new file mode 100644 index 0000000..6df561f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/ThumbnailRequest.lua @@ -0,0 +1,18 @@ +local ThumbnailRequest = {} + +function ThumbnailRequest.new() + local self = {} + + return self +end + +function ThumbnailRequest.fromData(thumbnailType, thumbnailSize) + local self = ThumbnailRequest.new() + + self.thumbnailType = thumbnailType + self.thumbnailSize = thumbnailSize + + return self +end + +return ThumbnailRequest \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/User.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/User.lua new file mode 100644 index 0000000..9eb9ce2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/User.lua @@ -0,0 +1,134 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId) + +local User = {} + +User.PresenceType = { + OFFLINE = "OFFLINE", + ONLINE = "ONLINE", + IN_GAME = "IN_GAME", + IN_STUDIO = "IN_STUDIO", +} + +function User.new() + local self = {} + + return self +end + +function User.mock() + local self = User.new() + + self.id = MockId() + + self.isFetching = false + self.isFriend = false + self.lastLocation = nil + self.name = "USER NAME" + self.universeId = nil + self.placeId = nil + self.rootPlaceId = nil + self.gameInstanceId = nil + self.presence = User.PresenceType.OFFLINE + self.membership = nil + self.thumbnails = nil + self.lastOnline = nil + self.displayName = "DN+" .. self.name + + return self +end + +-- Note: Going forward, leverage User.fromDataTable() instead. +-- It accepts a more flexible parameter than User.fromData() and constructs the same User model +function User.fromData(id, name, isFriend) + local self = User.new() + + self.id = tostring(id) + + self.isFetching = false + self.isFriend = isFriend + self.lastLocation = nil + self.name = name + self.universeId = nil + self.placeId = nil + self.rootPlaceId = nil + self.gameInstanceId = nil + + self.presence = (Players.LocalPlayer and self.id == tostring(Players.LocalPlayer.UserId)) + and User.PresenceType.ONLINE or nil + self.thumbnails = nil + self.lastOnline = nil + + return self +end + +function User.fromDataTable(data) + local self = User.new() + + self.id = tostring(data.id) + self.isFriend = data.isFriend + self.presence = (Players.LocalPlayer + and self.id == tostring(Players.LocalPlayer.UserId)) and User.PresenceType.ONLINE or nil + self.isFetching = false + self.lastLocation = nil + self.name = data.name + self.displayName = data.displayName or data.name + self.universeId = nil + self.placeId = nil + self.rootPlaceId = nil + self.gameInstanceId = nil + self.thumbnails = nil + self.lastOnline = nil + + return self +end + +function User.compare(user1, user2) + assert(not(user1 == nil and user2 == nil)) + assert(user1 == nil or typeof(user1) == "table") + assert(user2 == nil or typeof(user2) == "table") + + -- Return false if any of the provided input is nil(empty). + if not user1 or not user2 then + return false + end + + for field, valueInUser2 in pairs(user2) do + if user1[field] ~= valueInUser2 then + return false + end + end + + for field, valueInUser1 in pairs(user1) do + if user2[field] ~= valueInUser1 then + return false + end + end + + return true +end + +function User.userPresenceToText(localization, user) + local presence = user.presence + local lastLocation = user.lastLocation + + if not presence then + return '' + end + + if presence == User.PresenceType.OFFLINE then + return localization:Format("Common.Presence.Label.Offline") + elseif presence == User.PresenceType.ONLINE then + return localization:Format("Common.Presence.Label.Online") + elseif (presence == User.PresenceType.IN_GAME) or (presence == User.PresenceType.IN_STUDIO) then + if lastLocation ~= nil then + return lastLocation + else + return localization:Format("Common.Presence.Label.Online") + end + end +end + +return User diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/User.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/User.spec.lua new file mode 100644 index 0000000..7f095fc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/User.spec.lua @@ -0,0 +1,98 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Immutable = require(CorePackages.AppTempCommon.Common.Immutable) + local User = require(CorePackages.AppTempCommon.LuaApp.Models.User) + + it("should detect if provided users are identical", function() + local clone1 = User.fromData(1, "Andy", true) + local clone2 = Immutable.Set(clone1, "isFriend", true) + + local result = User.compare(clone1, clone2) + expect(result).to.equal(true) + + result = User.compare(clone2, clone1) + expect(result).to.equal(true) + end) + + it("should detect when there is one or more fields with different values", function() + local andy = User.fromData(1, "Andy", true) + local ollie = Immutable.Set(andy, "name", "Ollie") + + local result = User.compare(andy, ollie) + expect(result).to.equal(false) + + result = User.compare(ollie, andy) + expect(result).to.equal(false) + end) + + it("should detect descrepancy when one user model contains more fields than the other", function() + local andy = User.fromData(1, "Andy", true) + local secretlyNotAndy = Immutable.Set(andy, "someDifferentField", "I'm Ollie!") + + local result = User.compare(andy, secretlyNotAndy) + expect(result).to.equal(false) + + result = User.compare(secretlyNotAndy, andy) + expect(result).to.equal(false) + end) + + it("should throw if invalid input is provided", function() + local aString = "I'm not a table." + local teddy = User.fromData(1, "Teddy", true) + + expect(function() User.compare(nil, nil) end).to.throw() + expect(function() User.compare(aString, nil) end).to.throw() + expect(function() User.compare(nil, aString) end).to.throw() + expect(function() User.compare(aString, aString) end).to.throw() + expect(function() User.compare(teddy, aString) end).to.throw() + expect(function() User.compare(aString, teddy) end).to.throw() + end) + + it("should return false if any one of the input is empty or nil)", function() + local emptyTable = {} + local teddy = User.fromData(1, "Teddy", true) + + local result = User.compare(teddy, nil) + expect(result).to.equal(false) + + result = User.compare(nil, teddy) + expect(result).to.equal(false) + + result = User.compare(teddy, emptyTable) + expect(result).to.equal(false) + + result = User.compare(emptyTable, teddy) + expect(result).to.equal(false) + end) + + describe("fromDataTable", function() + it("should properly set user data", function() + local data = { + id = 1, + name = "FooBar", + displayName = "FooBar+DN", + isFriend = false, + } + local user = User.fromDataTable(data) + + expect(user.id).to.equal("1") + expect(user.name).to.equal("FooBar") + expect(user.displayName).to.equal("FooBar+DN") + expect(user.isFriend).to.equal(false) + end) + + it("should still set user data without a displayName property", function() + local data = { + id = 1, + name = "FooBar", + isFriend = false, + } + local user = User.fromDataTable(data) + + expect(user.id).to.equal("1") + expect(user.name).to.equal("FooBar") + expect(user.displayName).to.equal("FooBar") + expect(user.isFriend).to.equal(false) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/NetworkProfiler.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/NetworkProfiler.lua new file mode 100644 index 0000000..a0bb17b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/NetworkProfiler.lua @@ -0,0 +1,51 @@ +local percentReporting = tonumber(settings():GetFVariable("PercentReportingNetworkProfileAfterStartup")) + +local FEATURE_NAME = "NetworkProfileDuringStartup" +local QUEUED_MEASURE_NAME = "Queued" +local NAME_LOOKUP_MEASURE_NAME = "NameLookup" +local CONNECT_MEASURE_NAME = "Connect" +local SSL_HANDSHAKE_MEASURE_NAME = "SSLHandshake" +local MAKE_REQUEST_MEASURE_NAME = "MakeRequest" +local RECEIVE_RESPONSE_MEASURE_NAME = "ReceiveResponse" + +local NetworkProfiler = {} +NetworkProfiler.__index = NetworkProfiler + +NetworkProfiler.aggregate = { + queued = 0.0, + nameLookup = 0.0, + connect = 0.0, + sslHandshake = 0.0, + makeRequest = 0.0, + receiveResponse = 0.0, +} + +function NetworkProfiler:track(timeProfile) + self.aggregate.queued = self.aggregate.queued + timeProfile.queued + if timeProfile.nameLookup >= 0 then + self.aggregate.nameLookup = self.aggregate.nameLookup + timeProfile.nameLookup + end + if timeProfile.connect >= 0 then + self.aggregate.connect = self.aggregate.connect + timeProfile.connect + end + if timeProfile.sslHandshake >= 0 then + self.aggregate.sslHandshake = self.aggregate.sslHandshake + timeProfile.sslHandshake + end + if timeProfile.makeRequest >= 0 then + self.aggregate.makeRequest = self.aggregate.makeRequest + timeProfile.makeRequest + end + if timeProfile.receiveResponse >= 0 then + self.aggregate.receiveResponse = self.aggregate.receiveResponse + timeProfile.receiveResponse + end +end + +function NetworkProfiler:report(reportToDiag) + reportToDiag(FEATURE_NAME, QUEUED_MEASURE_NAME, self.aggregate.queued, percentReporting) + reportToDiag(FEATURE_NAME, NAME_LOOKUP_MEASURE_NAME, self.aggregate.nameLookup, percentReporting) + reportToDiag(FEATURE_NAME, CONNECT_MEASURE_NAME, self.aggregate.connect, percentReporting) + reportToDiag(FEATURE_NAME, SSL_HANDSHAKE_MEASURE_NAME, self.aggregate.sslHandshake, percentReporting) + reportToDiag(FEATURE_NAME, MAKE_REQUEST_MEASURE_NAME, self.aggregate.makeRequest, percentReporting) + reportToDiag(FEATURE_NAME, RECEIVE_RESPONSE_MEASURE_NAME, self.aggregate.receiveResponse, percentReporting) +end + +return NetworkProfiler \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Promise.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Promise.lua new file mode 100644 index 0000000..ac10217 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Promise.lua @@ -0,0 +1,8 @@ +----------------------------------------------------------------------------- +--- --- +--- Under Migration to CorePackages --- +--- --- +----------------------------------------------------------------------------- +local CorePackages = game:GetService("CorePackages") + +return require(CorePackages.Promise) diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/PromiseUtilities.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/PromiseUtilities.lua new file mode 100644 index 0000000..6aaa493 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/PromiseUtilities.lua @@ -0,0 +1,91 @@ +--[[ + Provides utility functions for Promises +]] + +local CorePackages = game:GetService("CorePackages") + +local Result = require(CorePackages.AppTempCommon.LuaApp.Result) +local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) + +local PromiseUtilities = {} + +--[[ + Accept a table of promises; + promises = { + [1] = Promise.resolve(), + ["Home"] = Promise.reject(), + ... + } + Returns a new promise that: + * is resolved when all input promises are finished. + returns the results of each individual promises in a list of Results + results = { + [1] = Result1, + ["Home"] = Result2, + ... + } + * is never rejected. +]] +function PromiseUtilities.Batch(promises) + assert(type(promises) == "table", "PromiseUtilities expects a list of Promises!") + + local numberOfPromises = 0 + + for _, promise in pairs(promises) do + assert(Promise.is(promise), "PromiseUtilities expects a list of Promises!") + numberOfPromises = numberOfPromises + 1 + end + + return Promise.new(function(resolve, reject) + local totalCompleted = 0 + local results = {} + + local function promiseCompleted(key, success, value) + results[key] = Result.new(success, value) + totalCompleted = totalCompleted + 1 + + if totalCompleted == numberOfPromises then + resolve(results) + end + end + + if next(promises) == nil then + resolve(results) + end + + for key, promise in pairs(promises) do + promise:andThen( + function(result, ...) + if select("#", ...) > 0 then + warn("Promises in PromiseUtilities.Batch should not return tuple") + end + promiseCompleted(key, true, result) + end, + function(reason) + promiseCompleted(key, false, reason) + end + ) + end + end) +end + +function PromiseUtilities.CountResults(batchPromiseResults) + local totalCount = 0 + local failureCount = 0 + + for _, result in pairs(batchPromiseResults) do + local success, _ = result:unwrap() + if not success then + failureCount = failureCount + 1 + end + totalCount = totalCount + 1 + end + + return { + successCount = totalCount - failureCount, + failureCount = failureCount, + totalCount = totalCount, + } +end + +return PromiseUtilities \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/PromiseUtilities.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/PromiseUtilities.spec.lua new file mode 100644 index 0000000..a3b4f7a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/PromiseUtilities.spec.lua @@ -0,0 +1,188 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local PromiseUtilities = require(CorePackages.AppTempCommon.LuaApp.PromiseUtilities) + local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) + local Result = require(CorePackages.AppTempCommon.LuaApp.Result) + local TableUtilities = require(CorePackages.AppTempCommon.LuaApp.TableUtilities) + + describe("PromiseUtilities.Batch", function() + it("should assert if input is not a list of Promises", function() + expect(function() + PromiseUtilities.Batch() + end).to.throw() + + expect(function() + PromiseUtilities.Batch(Promise.resolve(), Promise.resolve()) + end).to.throw() + + expect(function() + PromiseUtilities.Batch({ + Promise.resolve(), + "something else" + }) + end).to.throw() + end) + + it("should invoke the given resolve callback when all promises are finished", function() + local promises = { + [1] = Promise.resolve(), + ["Home"] = Promise.resolve() + } + local callCount = 0 + + local batchedPromise = PromiseUtilities.Batch(promises):andThen( + function() + callCount = callCount + 1 + end + ) + + expect(batchedPromise).to.be.ok() + expect(callCount).to.equal(1) + expect(batchedPromise._status).to.equal(Promise.Status.Resolved) + end) + + it("should not invoke any callbacks when one of the promises are not finished", function() + local promises = { + [1] = Promise.resolve(), + ["Home"] = Promise.new(function() end) + } + local callCount = 0 + + local batchedPromise = PromiseUtilities.Batch(promises):andThen( + function() + callCount = callCount + 1 + end, + + function() + callCount = callCount + 1 + end + ) + + expect(batchedPromise).to.be.ok() + expect(callCount).to.equal(0) + expect(batchedPromise._status).to.equal(Promise.Status.Started) + end) + + it("should return the correct results of each individual promise", function() + local promises = { + [1] = Promise.resolve(5), + ["Home"] = Promise.reject("failed") + } + local promiseResults = nil + + local batchedPromise = PromiseUtilities.Batch(promises):andThen( + function(results) + promiseResults = results + end + ) + + expect(batchedPromise).to.be.ok() + expect(batchedPromise._status).to.equal(Promise.Status.Resolved) + expect(TableUtilities.FieldCount(promiseResults)).to.equal(2) + + expect(Result.is(promiseResults[1])).to.equal(true) + local success1, value1 = promiseResults[1]:unwrap() + expect(success1).to.equal(true) + expect(value1).to.equal(5) + local isMatchCalled1 = false + promiseResults[1]:match(function(result) + expect(result).to.equal(5) + isMatchCalled1 = true + end, + function() + error("should not be called") + end) + expect(isMatchCalled1).to.equal(true) + + + expect(Result.is(promiseResults["Home"])).to.equal(true) + local success2, value2 = promiseResults["Home"]:unwrap() + expect(success2).to.equal(false) + expect(value2).to.equal("failed") + local isMatchCalled2 = false + promiseResults["Home"]:match(function() + error("should not be called") + end, + function(err) + expect(err).to.equal("failed") + isMatchCalled2 = true + end) + expect(isMatchCalled2).to.equal(true) + end) + + it("should return the correct results of each individual promise that resolved later", function() + local resolveLater + local rejectLater + + local promises = { + [1] = Promise.new(function(resolve) + resolveLater = resolve + end), + ["Home"] = Promise.new(function(_, reject) + rejectLater = reject + end) + } + local promiseResults = nil + + local batchedPromise = PromiseUtilities.Batch(promises):andThen( + function(results) + promiseResults = results + end + ) + + resolveLater(5) + rejectLater("failed") + + expect(batchedPromise).to.be.ok() + expect(batchedPromise._status).to.equal(Promise.Status.Resolved) + expect(TableUtilities.FieldCount(promiseResults)).to.equal(2) + + expect(Result.is(promiseResults[1])).to.equal(true) + local success1, value1 = promiseResults[1]:unwrap() + expect(success1).to.equal(true) + expect(value1).to.equal(5) + + expect(Result.is(promiseResults["Home"])).to.equal(true) + local success2, value2 = promiseResults["Home"]:unwrap() + expect(success2).to.equal(false) + expect(value2).to.equal("failed") + end) + + it("should resolve if given an empty list of promises", function() + local emptyPromises = {} + local callCount = 0 + + local batchedPromise = PromiseUtilities.Batch(emptyPromises):andThen( + function(results) + callCount = callCount + 1 + end + ) + + expect(batchedPromise).to.be.ok() + expect(callCount).to.equal(1) + expect(batchedPromise._status).to.equal(Promise.Status.Resolved) + end) + end) + + describe("PromiseUtilities.CountResults", function() + it("should count the results correctly", function() + local emptyResults = {} + + local countResult = PromiseUtilities.CountResults(emptyResults) + + expect(countResult).to.be.ok() + expect(countResult.successCount).to.equal(0) + expect(countResult.failureCount).to.equal(0) + expect(countResult.totalCount).to.equal(0) + + local promiseResults = { Result.success(0), Result.success(0), Result.error(1) } + + countResult = PromiseUtilities.CountResults(promiseResults) + + expect(countResult).to.be.ok() + expect(countResult.successCount).to.equal(2) + expect(countResult.failureCount).to.equal(1) + expect(countResult.totalCount).to.equal(3) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/FetchingStatus.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/FetchingStatus.lua new file mode 100644 index 0000000..6481cff --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/FetchingStatus.lua @@ -0,0 +1,29 @@ +local CorePackages = game:GetService("CorePackages") + +local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus) +local Cryo = require(CorePackages.Cryo) + +return function(state, action) + state = state or {} + + if action.type == UpdateFetchingStatus.name then + local key = action.key + local status = action.status + local value + if status ~= nil then + value = status + else + value = Cryo.None + end + + state = Cryo.Dictionary.join( + state, + { + [key] = value, + } + ) + + end + + return state +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/FetchingStatus.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/FetchingStatus.spec.lua new file mode 100644 index 0000000..c3f9f36 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/FetchingStatus.spec.lua @@ -0,0 +1,52 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus) + local FetchingStatusReducer = require(CorePackages.AppTempCommon.LuaApp.Reducers.FetchingStatus) + local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus) + local TableUtilities = require(CorePackages.AppTempCommon.LuaApp.TableUtilities) + + local KEY_1 = "key_1" + local KEY_2 = "key_2" + + describe("FetchingStatus", function() + it("should be empty by default", function() + local state = FetchingStatusReducer(nil, {}) + + expect(TableUtilities.FieldCount(state)).to.equal(0) + end) + + it("should not be modified by other actions", function() + local oldState = FetchingStatusReducer(nil, {}) + local newState = FetchingStatusReducer(oldState, { type = "not a real action" }) + + expect(newState).to.equal(oldState) + end) + + it("should be changed using UpdateFetchingStatus", function() + local state = FetchingStatusReducer(nil, {}) + + state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, RetrievalStatus.Fetching)) + expect(state[KEY_1]).to.equal(RetrievalStatus.Fetching) + + state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, RetrievalStatus.Failed)) + expect(state[KEY_1]).to.equal(RetrievalStatus.Failed) + end) + + it("should store different values for different keys", function() + local state = FetchingStatusReducer(nil, {}) + + state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, RetrievalStatus.Failed)) + state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_2, RetrievalStatus.Done)) + + expect(state[KEY_1]).to.equal(RetrievalStatus.Failed) + expect(state[KEY_2]).to.equal(RetrievalStatus.Done) + end) + + it("should clear values for nil keys", function() + local state = { [KEY_1] = RetrievalStatus.Fetching } + + state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, nil)) + expect(state[KEY_1]).to.equal(nil) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Friends.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Friends.lua new file mode 100644 index 0000000..06305fe --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Friends.lua @@ -0,0 +1,43 @@ +local CorePackages = game:GetService("CorePackages") + +local Immutable = require(CorePackages.AppTempCommon.Common.Immutable) +local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus) + +local FetchUserFriendsStarted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsStarted) +local FetchUserFriendsFailed = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsFailed) +local FetchUserFriendsCompleted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsCompleted) + +local function setFieldPerUser(state, fieldName, userId, value) + local field = state[fieldName] or {} + return Immutable.JoinDictionaries(state, { + [fieldName] = Immutable.JoinDictionaries(field, { + [userId] = value + }) + }) +end + +local function setRetrievalStatus(state, userId, status) + return setFieldPerUser(state, "retrievalStatus", userId, status) +end + +local function setRetrievalFailureResponse(state, userId, response) + return setFieldPerUser(state, "retrievalFailureResponse", userId, response) +end + +return function(state, action) + state = state or { + retrievalStatus = {}, + retrievalFailureResponse = {}, + } + + if action.type == FetchUserFriendsStarted.name then + state = setRetrievalStatus(state, action.userId, RetrievalStatus.Fetching) + elseif action.type == FetchUserFriendsFailed.name then + state = setRetrievalStatus(state, action.userId, RetrievalStatus.Failed) + state = setRetrievalFailureResponse(state, action.userId, action.response) + elseif action.type == FetchUserFriendsCompleted.name then + state = setRetrievalStatus(state, action.userId, RetrievalStatus.Done) + end + + return state +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/UniversePlaceInfos.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/UniversePlaceInfos.lua new file mode 100644 index 0000000..2cfdb55 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/UniversePlaceInfos.lua @@ -0,0 +1,21 @@ +local CorePackages = game:GetService("CorePackages") + +local Immutable = require(CorePackages.AppTempCommon.Common.Immutable) +local ReceivedPlacesInfos = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedPlacesInfos) + +local LuaAppFlags = CorePackages.AppTempCommon.LuaApp.Flags +local convertUniverseIdToString = require(LuaAppFlags.ConvertUniverseIdToString) + +return function(state, action) + state = state or {} + + if action.type == ReceivedPlacesInfos.name then + for _, placeInfo in pairs(action.placesInfos) do + local universeId = convertUniverseIdToString(placeInfo.universeId) + + state = Immutable.Set(state, universeId, placeInfo) + end + end + + return state +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.lua new file mode 100644 index 0000000..b757634 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.lua @@ -0,0 +1,98 @@ +local CorePackages = game:GetService("CorePackages") + +local Immutable = require(CorePackages.AppTempCommon.Common.Immutable) + +local AddUser = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUser) +local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers) +local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence) +local RemoveUser = require(CorePackages.AppTempCommon.LuaApp.Actions.RemoveUser) +local SetUserIsFriend = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserIsFriend) +local SetUserMembershipType = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserMembershipType) +local SetUserPresence = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserPresence) +local SetUserThumbnail = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserThumbnail) + +return function(state, action) + state = state or {} + + if action.type == AddUser.name then + local user = action.user + state = Immutable.Set(state, user.id, user) + elseif action.type == AddUsers.name then + local addedUsers = action.users + local usersUpdate = {} + for userId, addedUser in pairs(addedUsers) do + local existingUser = state[userId] + if existingUser then + usersUpdate[userId] = Immutable.JoinDictionaries(existingUser, addedUser) + else + usersUpdate[userId] = addedUser + end + end + + state = Immutable.JoinDictionaries(state, usersUpdate) + + elseif action.type == SetUserIsFriend.name then + local user = state[action.userId] + if user then + local newUser = Immutable.Set(user, "isFriend", action.isFriend) + state = Immutable.Set(state, user.id, newUser) + else + warn("Setting isFriend on user", action.userId, "who doesn't exist yet") + end + elseif action.type == SetUserPresence.name then + local user = state[action.userId] + if user then + local newUser = Immutable.JoinDictionaries(user, { + presence = action.presence, + lastLocation = action.lastLocation, + }) + state = Immutable.Set(state, user.id, newUser) + else + warn("Setting presence on user", action.userId, "who doesn't exist yet") + end + elseif action.type == ReceivedUserPresence.name then + local user = state[action.userId] + if user then + state = Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(user, { + presence = action.presence, + lastLocation = action.lastLocation, + placeId = action.placeId, + rootPlaceId = action.rootPlaceId, + gameInstanceId = action.gameInstanceId, + lastOnline = action.lastOnline, + universeId = action.universeId, + }), + }) + end + elseif action.type == SetUserThumbnail.name then + local user = state[action.userId] + if user then + local thumbnails = user.thumbnails or {} + state = Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(user, { + thumbnails = Immutable.JoinDictionaries(thumbnails, { + [action.thumbnailType] = Immutable.JoinDictionaries(thumbnails[action.thumbnailType] or {}, { + [action.thumbnailSize] = action.image, + }), + }), + }), + }) + end + elseif action.type == SetUserMembershipType.name then + local user = state[action.userId] + if user then + state = Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(user, { + membership = action.membershipType, + }), + }) + end + elseif action.type == RemoveUser.name then + if state[action.userId] then + state = Immutable.RemoveFromDictionary(state, action.userId) + end + end + + return state +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.spec.lua new file mode 100644 index 0000000..4cda7ab --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.spec.lua @@ -0,0 +1,106 @@ +return function() + local CorePackages = game:GetService("CorePackages") + + local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId) + local User = require(CorePackages.AppTempCommon.LuaApp.Models.User) + local Users = require(CorePackages.AppTempCommon.LuaApp.Reducers.Users) + + local AddUser = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUser) + local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence) + local SetUserIsFriend = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserIsFriend) + local SetUserMembershipType = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserMembershipType) + local SetUserPresence = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserPresence) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = Users(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("AddUser", function() + it("should add a user to the store", function() + local user = User.mock() + local state = {} + + state = Users(state, AddUser(user)) + + expect(state[user.id]).to.equal(user) + end) + end) + + describe("SetUserIsFriend", function() + it("should set isFriend on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + expect(state[user.id].isFriend).to.equal(false) + + state = Users(state, SetUserIsFriend(user.id, true)) + expect(state[user.id].isFriend).to.equal(true) + + state = Users(state, SetUserIsFriend(user.id, false)) + expect(state[user.id].isFriend).to.equal(false) + end) + end) + + describe("SetUserPresence", function() + it("should set presence on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + expect(state[user.id].presence).to.equal(User.PresenceType.OFFLINE) + + state = Users(state, SetUserPresence(user.id, User.PresenceType.ONLINE)) + expect(state[user.id].presence).to.equal(User.PresenceType.ONLINE) + + state = Users(state, SetUserPresence(user.id, User.PresenceType.IN_GAME)) + expect(state[user.id].presence).to.equal(User.PresenceType.IN_GAME) + + state = Users(state, SetUserPresence(user.id, User.PresenceType.IN_STUDIO)) + expect(state[user.id].presence).to.equal(User.PresenceType.IN_STUDIO) + end) + end) + + describe("ReceivedUserPresence", function() + it("should set presence on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + local existingPresence = user.presence + local newPresence = 'ONLINE' + local lastLocation = MockId() + local newPlaceId = MockId() + + state = Users(state, ReceivedUserPresence(user.id, newPresence, lastLocation, newPlaceId)) + + expect(user.presence).to.equal(existingPresence) + expect(state[user.id].presence).to.equal(newPresence) + expect(state[user.id].lastLocation).to.equal(lastLocation) + expect(state[user.id].placeId).to.equal(newPlaceId) + end) + end) + + describe("SetUserMembershipType", function() + it("should set membership on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + local existingMembership = user.membership + local newMembership = Enum.MembershipType.BuildersClub + + state = Users(state, SetUserMembershipType(user.id, newMembership)) + + expect(user.membership).to.equal(existingMembership) + expect(state[user.id].membership).to.equal(newMembership) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Result.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Result.lua new file mode 100644 index 0000000..c27382f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Result.lua @@ -0,0 +1,8 @@ +----------------------------------------------------------------------------- +--- --- +--- Under Migration to CorePackages --- +--- --- +----------------------------------------------------------------------------- +local CorePackages = game:GetService("CorePackages") + +return require(CorePackages.Result) diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/AppStyleProvider.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/AppStyleProvider.lua new file mode 100644 index 0000000..0062238 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/AppStyleProvider.lua @@ -0,0 +1,34 @@ +--[[ + The is a wrapper for the style provider for apps. + props: + style : table - Includes the name of the theme and font being used. + { + themeName : string - The name of the theme being used. + fontName : string - The name of the font being used. + } +]] +local CorePackages = game:GetService("CorePackages") +local ArgCheck = require(CorePackages.ArgCheck) +local Roact = require(CorePackages.Roact) +local UIBlox = require(CorePackages.UIBlox) +local StyleProvider = UIBlox.Style.Provider +local StylePalette = require(script.Parent.StylePalette) + +local AppStyleProvider = Roact.Component:extend("AppStyleProvider") + +function AppStyleProvider:render() + local style = self.props.style + ArgCheck.isNotNil(style, "style prop for AppStyleProvider") + local themeName = style.themeName + local fontName = style.fontName + local stylePalette = StylePalette.new() + stylePalette:updateTheme(themeName) + stylePalette:updateFont(fontName) + local appStyle = stylePalette:currentStyle() + + return Roact.createElement(StyleProvider,{ + style = appStyle, + }, self.props[Roact.Children]) +end + +return AppStyleProvider \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/AppStyleProvider.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/AppStyleProvider.spec.lua new file mode 100644 index 0000000..4bb0c50 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/AppStyleProvider.spec.lua @@ -0,0 +1,32 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local AppStyleProvider = require(script.Parent.AppStyleProvider) + local Constants = require(script.Parent.Constants) + local appStyle = { + themeName = Constants.ThemeName.Dark, + fontName = Constants.FontName.Gotham, + } + it("should create and destroy without errors", function() + local element = Roact.createElement("Frame") + local appStyleProvider = Roact.createElement(AppStyleProvider, { + style = appStyle, + },{ + Element = element, + }) + + local instance = Roact.mount(appStyleProvider) + Roact.unmount(instance) + end) + + it("should throw when style prop is nil", function() + local element = Roact.createElement("Frame") + local appStyleProvider = Roact.createElement(AppStyleProvider, {},{ + Element = element, + }) + expect(function() + local instance = Roact.mount(appStyleProvider) + Roact.unmount(instance) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Colors.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Colors.lua new file mode 100644 index 0000000..0289700 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Colors.lua @@ -0,0 +1,23 @@ +local Colors = { + --Common colors + Black = Color3.fromRGB(0, 0, 0), + White = Color3.fromRGB(255, 255, 255), + Green = Color3.fromRGB(0, 176, 111), + Red = Color3.fromRGB(247, 75, 82), + + --Dark theme colors + Carbon = Color3.fromRGB(31, 33, 35), + Flint = Color3.fromRGB(57, 59, 61), + Graphite = Color3.fromRGB(101, 102, 104), + Obsidian = Color3.fromRGB(24, 25, 27), + Pumice = Color3.fromRGB(189, 190, 190), + Slate = Color3.fromRGB(35, 37, 39), + + --Light theme colors + Alabaster = Color3.fromRGB(242, 244, 245), + Ash = Color3.fromRGB(234, 237, 239), + Chalk = Color3.fromRGB(216, 219, 222), + Smoke = Color3.fromRGB(96, 97, 98), +} + +return Colors \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Constants.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Constants.lua new file mode 100644 index 0000000..9a373a4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Constants.lua @@ -0,0 +1,12 @@ +local Constants = {} + +Constants.ThemeName = { + Dark = "dark", + Light = "light", +} + +Constants.FontName = { + Gotham = "gotham", +} + +return Constants \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/Gotham.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/Gotham.lua new file mode 100644 index 0000000..b239e96 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/Gotham.lua @@ -0,0 +1,54 @@ +local baseSize = 16 +-- Nominal size conversion +-- https://confluence.rbx.com/display/PX/Font+Metrics +local nominalSizeFactor = 1.2 +local font = { + BaseSize = baseSize * nominalSizeFactor, + Title = { + Font = Enum.Font.GothamBlack, + RelativeSize = 32 / baseSize, + RelativeMinSize = 24 / baseSize, + }, + Header1 = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 20 / baseSize, + RelativeMinSize = 16 / baseSize, + }, + Header2 = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 16 / baseSize, + RelativeMinSize = 12 / baseSize, + }, + SubHeader1 = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 16 / baseSize, + RelativeMinSize = 12 / baseSize, + }, + Body = { + Font = Enum.Font.Gotham, + RelativeSize = 16 / baseSize, + RelativeMinSize = 12 / baseSize, + }, + CaptionHeader = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 12 / baseSize, + RelativeMinSize = 9 / baseSize, + }, + CaptionSubHeader = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 12 / baseSize, + RelativeMinSize = 9 / baseSize, + }, + CaptionBody = { + Font = Enum.Font.Gotham, + RelativeSize = 12 / baseSize, + RelativeMinSize = 9 / baseSize, + }, + Footer = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 10 / baseSize, + RelativeMinSize = 8 / baseSize, + }, +} + +return font \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/Gotham.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/Gotham.spec.lua new file mode 100644 index 0000000..0c31bf2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/Gotham.spec.lua @@ -0,0 +1,9 @@ +return function() + it("should be valid font palette without errors", function() + local CorePackages = game:GetService("CorePackages") + local UIBlox = require(CorePackages.UIBlox) + local validateFont = UIBlox.Style.Validator.validateFont + local Gotham = require(script.Parent.Gotham) + assert(validateFont(Gotham)) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/getFontFromName.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/getFontFromName.lua new file mode 100644 index 0000000..9dff393 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/getFontFromName.lua @@ -0,0 +1,20 @@ +local CorePackages = game:GetService("CorePackages") +local ArgCheck = require(CorePackages.ArgCheck) +local Logging = require(CorePackages.Logging) +local UIBlox = require(CorePackages.UIBlox) +local validateFont = UIBlox.Style.Validator.validateFont + +return function (fontName, defaultFont, fontMap) + local mappedFont + if fontName ~= nil and #fontName > 0 then + mappedFont = fontMap[string.lower(fontName)] + end + + if mappedFont == nil then + mappedFont = fontMap[defaultFont] + Logging.warn(string.format("Unrecognized font name: `%s`", tostring(fontName))) + end + + ArgCheck.assert(validateFont(mappedFont)) + return mappedFont +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/getFontFromName.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/getFontFromName.spec.lua new file mode 100644 index 0000000..a8c2b7a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/getFontFromName.spec.lua @@ -0,0 +1,33 @@ +return function() + local getFontFromName = require(script.Parent.getFontFromName) + local Constants = require(script.Parent.Parent.Constants) + it("should be able to get a font palette without errors", function() + local fontMap = { + [Constants.FontName.Gotham] = require(script.Parent.Gotham), + } + local fontTable = getFontFromName(Constants.FontName.Gotham, Constants.FontName.Gotham, fontMap) + expect(fontTable).to.be.a("table") + end) + + it("should be able to get a font palette using default without errors", function() + local fontMap = { + [Constants.FontName.Gotham] = require(script.Parent.Gotham), + } + local fontTable = getFontFromName("sourceSans", Constants.FontName.Gotham, fontMap) + expect(fontTable).to.be.a("table") + end) + + it("should throw the font palette is invalid", function() + expect(function() + local fontMap = { + [Constants.FontName.Gotham] = { + Font = { + Font = Enum.Font.Gotham, + RelativeSize = 1, + }, + }, + } + getFontFromName(Constants.FontName.Gotham, Constants.FontName.Gotham, fontMap) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/StylePalette.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/StylePalette.lua new file mode 100644 index 0000000..39d70de --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/StylePalette.lua @@ -0,0 +1,49 @@ +local getThemeFromName = require(script.Parent.Themes.getThemeFromName) +local getFontFromName = require(script.Parent.Fonts.getFontFromName) +local Constants = require(script.Parent.Constants) + +local StylePalette = {} +StylePalette.__index = StylePalette + +local DEFAULT_FONT = Constants.FontName.Gotham +local FONT_MAP = { + [Constants.FontName.Gotham] = require(script.Parent.Fonts.Gotham), +} + +local DEFAULT_THEME = Constants.ThemeName.Light +local THEME_MAP = { + [Constants.ThemeName.Dark] = require(script.Parent.Themes.DarkTheme), + [Constants.ThemeName.Light] = require(script.Parent.Themes.LightTheme), +} + +function StylePalette.new(style) + --By default a new style will be empty. + -- This will allow the font and theme to be merged independently even when one is empty. + local self = {} + + if style ~= nil then + self.Font = style.Font + self.Theme = style.Theme + end + + setmetatable(self, StylePalette) + return self +end + +function StylePalette:updateFont(fontName) + self.Font = getFontFromName(fontName, DEFAULT_FONT, FONT_MAP) +end + +function StylePalette:updateTheme(themeName) + self.Theme = getThemeFromName(themeName, DEFAULT_THEME, THEME_MAP) +end + +function StylePalette:currentStyle() + local style = { + Font = self.Font, + Theme = self.Theme, + } + return style +end + +return StylePalette \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/StylePalette.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/StylePalette.spec.lua new file mode 100644 index 0000000..94e6891 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/StylePalette.spec.lua @@ -0,0 +1,58 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local UIBlox = require(CorePackages.UIBlox) + local StylePalette = require(script.Parent.StylePalette) + local validateStye = UIBlox.Style.Validator.validateStyle + + it("should be able to create a style palette", function() + local stylePalette = StylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + end) + + it("should be able to create a style palette and be able to update theme 1", function() + local stylePalette = StylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + + stylePalette:updateTheme("light") + local newAppStyle = stylePalette:currentStyle() + expect(validateStye(newAppStyle)).equal(true) + end) + + it("should be able to create a style palette and be able to update theme 2", function() + local stylePalette = StylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + + stylePalette:updateFont("gotham") + local newAppStyle = stylePalette:currentStyle() + expect(validateStye(newAppStyle)).equal(true) + end) + + it("should be able to create a style palette and be able to merge an old one in", function() + local stylePalette = StylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + + local newstylePalette = StylePalette.new(stylePalette) + local newAppStyle = newstylePalette:currentStyle() + expect(validateStye(newAppStyle)).equal(true) + end) + + it("should be able to create a empty style palette", function() + local stylePalette = StylePalette.new() + local appStyle = stylePalette:currentStyle() + expect(appStyle.Font).equal(nil) + expect(appStyle.Theme).equal(nil) + expect(validateStye(appStyle)).equal(false) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.lua new file mode 100644 index 0000000..bde1d20 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.lua @@ -0,0 +1,162 @@ +local ThemesRoot = script.Parent +local StylesRoot = ThemesRoot.Parent +local Colors = require(StylesRoot.Colors) + +local theme = { + BackgroundDefault = { + Color = Colors.Slate, + Transparency = 0, + }, + BackgroundContrast = { + Color = Colors.Carbon, + Transparency = 0, + }, + BackgroundMuted = { + Color = Colors.Obsidian, + Transparency = 0, + }, + BackgroundUIDefault = { + Color = Colors.Flint, + Transparency = 0, + }, + BackgroundUIContrast = { + Color = Colors.Black, + Transparency = 0.3, -- Alpha 0.7 + }, + BackgroundOnHover = { + Color = Colors.White, + Transparency = 0.9, -- Alpha 0.1 + }, + BackgroundOnPress = { + Color = Colors.Black, + Transparency = 0.7, -- Alpha 0.3 + }, + + UIDefault = { + Color = Colors.Graphite, + Transparency = 0, + }, + UIMuted = { + Color = Colors.Obsidian, + Transparency = 0.2, -- Alpha 0.8 + }, + UIEmphasis = { + Color = Colors.White, + Transparency = 0.7, -- Alpha 0.3 + }, + + ContextualPrimaryDefault = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryOnHover = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryContent = { + Color = Colors.White, + Transparency = 0, + }, + + SystemPrimaryDefault = { + Color = Colors.White, + Transparency = 0, + }, + SystemPrimaryOnHover = { + Color = Colors.White, + Transparency = 0, + }, + SystemPrimaryContent = { + Color = Colors.Flint, + Transparency = 0, + }, + + SecondaryDefault = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + SecondaryOnHover = { + Color = Colors.White, + Transparency = 0, + }, + SecondaryContent = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + + IconDefault = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 alpha + }, + IconEmphasis = { + Color = Colors.White, + Transparency = 0, + }, + IconOnHover = { + Color = Colors.White, + Transparency = 0, + }, + + TextEmphasis = { + Color = Colors.White, + Transparency = 0, + }, + TextDefault = { + Color = Colors.Pumice, + Transparency = 0, + }, + TextMuted = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + + Divider = { + Color = Colors.White, + Transparency = 0.8, -- 0.2 Alpha + }, + Overlay = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + DropShadow = { + Color = Colors.Black, + Transparency = 0, + }, + NavigationBar = { + Color = Colors.Carbon, + Transparency = 0, + }, + PlaceHolder = { + Color = Colors.Flint, + Transparency = 0.5, -- 0.5 Alpha + }, + + OnlineStatus = { + Color = Colors.Green, + Transparency = 0, + }, + OfflineStatus = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + + Success = { + Color = Colors.Green, + Transparency = 0, + }, + Alert = { + Color = Colors.Red, + Transparency = 0, + }, + + Badge = { + Color = Colors.White, + Transparency = 0, + }, + BadgeContent = { + Color = Colors.Flint, + Transparency = 0, + }, +} + +return theme \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.spec.lua new file mode 100644 index 0000000..8f23083 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.spec.lua @@ -0,0 +1,9 @@ +return function() + it("should be a valid theme palette.", function() + local CorePackages = game:GetService("CorePackages") + local UIBlox = require(CorePackages.UIBlox) + local validateTheme = UIBlox.Style.Validator.validateTheme + local DarkTheme = require(script.Parent.DarkTheme) + assert(validateTheme(DarkTheme)) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.lua new file mode 100644 index 0000000..6679352 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.lua @@ -0,0 +1,168 @@ +local ThemesRoot = script.Parent +local StylesRoot = ThemesRoot.Parent +local LuaAppRoot = StylesRoot.Parent + +local Colors = require(StylesRoot.Colors) +local GetFFlagLuaAppFixLightTheme = require(LuaAppRoot.Flags.GetFFlagLuaAppFixLightTheme) + +local theme = { + BackgroundDefault = { + Color = Colors.Alabaster, + Transparency = 0, + }, + BackgroundContrast = { + Color = Colors.Ash, + Transparency = 0, + }, + BackgroundMuted = { + Color = Colors.Chalk, + Transparency = 0, + }, + BackgroundUIDefault = { + Color = Colors.White, + Transparency = 0, + }, + BackgroundUIContrast = { + Color = Colors.White, + Transparency = 0.1, -- Alpha 0.9 + }, + BackgroundOnHover = GetFFlagLuaAppFixLightTheme() and { + Color = Colors.Black, + Transparency = 0.9, -- Alpha 0.1 + } or { + Color = Colors.White, + Transparency = 0.7, -- Alpha 0.3 + }, + BackgroundOnPress = { + Color = Colors.Black, + Transparency = 0.9, -- Alpha 0.1 + }, + + UIDefault = { + Color = Colors.Pumice, + Transparency = 0, + }, + UIMuted = { + Color = Colors.Black, + Transparency = 0.9, -- Alpha 0.1 + }, + UIEmphasis = { + Color = Colors.Black, + Transparency = 0.7, -- Alpha 0.3 + }, + + ContextualPrimaryDefault = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryOnHover = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryContent = { + Color = Colors.White, + Transparency = 0, + }, + + SystemPrimaryDefault = { + Color = Colors.Flint, + Transparency = 0, + }, + SystemPrimaryOnHover = { + Color = Colors.Flint, + Transparency = 0, + }, + SystemPrimaryContent = { + Color = Colors.White, + Transparency = 0, + }, + + SecondaryDefault = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + SecondaryOnHover = { + Color = Colors.Flint, + Transparency = 0, + }, + SecondaryContent = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + + IconDefault = { + Color = Colors.Black, + Transparency = 0.4, -- 0.6 alpha + }, + IconEmphasis = { + Color = Colors.Flint, + Transparency = 0, + }, + IconOnHover = { + Color = Colors.Flint, + Transparency = 0, + }, + + TextEmphasis = { + Color = Colors.Flint, + Transparency = 0, + }, + TextDefault = { + Color = Colors.Smoke, + Transparency = 0, + }, + TextMuted = { + Color = Colors.Black, + Transparency = 0.4, -- 0.6 Alpha + }, + + Divider = { + Color = Colors.Pumice, + Transparency = 0, + }, + Overlay = { + Color = Colors.Black, + Transparency = 0.7, -- 0.3 Alpha + }, + DropShadow = { + Color = Colors.Black, + Transparency = 0, + }, + NavigationBar = { + Color = Colors.White, + Transparency = 0, + }, + PlaceHolder = { + Color = Colors.Chalk, + Transparency = 0.3, -- 0.7 Alpha + }, + + OnlineStatus = { + Color = Colors.Green, + Transparency = 0, + }, + OfflineStatus = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + + Success = { + Color = Colors.Green, + Transparency = 0, + }, + Alert = { + Color = Colors.Red, + Transparency = 0, + }, + + Badge = { + Color = Colors.Flint, + Transparency = 0, + }, + BadgeContent = { + Color = Colors.White, + Transparency = 0, + }, +} + +return theme \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.spec.lua new file mode 100644 index 0000000..5426b52 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.spec.lua @@ -0,0 +1,9 @@ +return function() + it("should be a valid theme palette.", function() + local CorePackages = game:GetService("CorePackages") + local UIBlox = require(CorePackages.UIBlox) + local validateTheme = UIBlox.Style.Validator.validateTheme + local LightTheme = require(script.Parent.DarkTheme) + assert(validateTheme(LightTheme)) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/getThemeFromName.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/getThemeFromName.lua new file mode 100644 index 0000000..c785b13 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/getThemeFromName.lua @@ -0,0 +1,19 @@ +local CorePackages = game:GetService("CorePackages") +local ArgCheck = require(CorePackages.ArgCheck) +local Logging = require(CorePackages.Logging) +local UIBlox = require(CorePackages.UIBlox) +local validateTheme = UIBlox.Style.Validator.validateTheme + +return function (themeName, defaultTheme, themeMap) + local mappedTheme + if themeName ~= nil and #themeName > 0 then + mappedTheme = themeMap[string.lower(themeName)] + end + + if mappedTheme == nil then + mappedTheme = themeMap[defaultTheme] + Logging.warn(string.format("Unrecognized theme name: `%s`", tostring(themeName))) + end + ArgCheck.assert(validateTheme(mappedTheme)) + return mappedTheme +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/getThemeFromName.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/getThemeFromName.spec.lua new file mode 100644 index 0000000..94a7552 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/getThemeFromName.spec.lua @@ -0,0 +1,33 @@ +return function() + local getThemeFromName = require(script.Parent.getThemeFromName) + local Constants = require(script.Parent.Parent.Constants) + it("should be able to get a theme palette without errors", function() + local themeMap = { + [Constants.ThemeName.Dark] = require(script.Parent.DarkTheme), + } + local themeTable = getThemeFromName(Constants.ThemeName.Dark, Constants.ThemeName.Dark,themeMap) + expect(themeTable).to.be.a("table") + end) + + it("should be able to get a theme palette using default without errors", function() + local themeMap = { + [Constants.ThemeName.Dark] = require(script.Parent.DarkTheme), + } + local themeTable = getThemeFromName("classic", Constants.ThemeName.Dark, themeMap) + expect(themeTable).to.be.a("table") + end) + + it("should throw with invalid theme palette", function() + expect(function() + local themeMap = { + [Constants.ThemeName.Dark] = { + Background = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + } + } + getThemeFromName(Constants.ThemeName.Dark, Constants.ThemeName.Dark, themeMap) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/TableUtilities.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/TableUtilities.lua new file mode 100644 index 0000000..f533197 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/TableUtilities.lua @@ -0,0 +1,21 @@ +----------------------------------------------------------------------------- +--- --- +--- Under Migration to CorePackages --- +--- --- +--- This file is a compatibility bridge between old and new api --- +----------------------------------------------------------------------------- + +local CorePackages = game:GetService("CorePackages") +local tutils = require(CorePackages.tutils) + +return { + CheckListConsistency = tutils.checkListConsistency, + DeepEqual = tutils.deepEqual, + EqualKey = tutils.equalKey, + FieldCount = tutils.fieldCount, + ListDifference = tutils.listDifferences, + Print = tutils.print, + RecursiveToString = tutils.toString, + ShallowEqual = tutils.shallowEqual, + TableDifference = tutils.tableDifference, +} diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/TableUtilities.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/TableUtilities.spec.lua new file mode 100644 index 0000000..ff71610 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/TableUtilities.spec.lua @@ -0,0 +1,228 @@ +return function() + local TableUtilities = require(script.Parent.TableUtilities) + local NotForProductionUse = game:GetService("CoreGui").RobloxGui.Modules.NotForProductionUse + local expectedFields = require(NotForProductionUse.UnitTestHelpers.expectedFields) + + describe("alias wrapper", function() + it("SHOULD have all required fields", function() + expectedFields(TableUtilities, { + "CheckListConsistency", + "DeepEqual", + "EqualKey", + "FieldCount", + "ListDifference", + "Print", + "RecursiveToString", + "ShallowEqual", + "TableDifference", + }) + end) + end) + + describe("legacy tests", function() + it("should return whether tables are equal to each other", function() + local tableA = nil + local tableB = nil + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = nil + tableB = {} + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = {} + tableB = nil + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = {} + tableB = {} + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(true) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(true) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value2", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value1", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value2", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + key2 = "value2", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + end) + + it("should return whether tables are equal to each other at key", function() + local tableA = nil + local tableB = nil + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = nil + tableB = {} + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = {} + tableB = nil + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = {} + tableB = {} + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(true) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value2", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value1", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value2", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + key2 = "value2", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(true) + expect(TableUtilities.EqualKey(tableA, tableB, "key2")).to.equal(false) + end) + + it("should return table's field count", function() + local t = {} + expect(TableUtilities.FieldCount(t)).to.equal(0) + + t = { + key1 = "value1", + } + expect(TableUtilities.FieldCount(t)).to.equal(1) + + t = { + key1 = "value1", + key2 = "value2", + } + expect(TableUtilities.FieldCount(t)).to.equal(2) + end) + + describe("TableUtilities.DeepEqual", function() + it("works for primitve data types", function() + expect(TableUtilities.DeepEqual(1, 1)).to.equal(true) + expect(TableUtilities.DeepEqual("str1", "str1")).to.equal(true) + expect(TableUtilities.DeepEqual(1, 2)).to.equal(false) + expect(TableUtilities.DeepEqual("str1", "str2")).to.equal(false) + end) + it("correctly identifies deeply-equal tables", function() + local table1 = { + num = 1, + innerTable = { + innerString = "str" + } + } + local table2 = { + num = 1, + innerTable = { + innerString = "str" + } + } + expect(TableUtilities.DeepEqual(table1, table2)).to.equal(true) + end) + it("correctly rejects non-deeply-equal tables", function() + local table1 = { + num = 1, + innerTable = { + innerString = "str" + } + } + local table2 = { + num = 1, + innerTable = { + innerString = "differentStr" + } + } + expect(TableUtilities.DeepEqual(table1, table2)).to.equal(false) + local table3 = { + num = 1, + innerTable = { + innerString = "str" + } + } + local table4 = { + num = 1, + innerTableWithDifferentKey = { + innerString = "str" + } + } + expect(TableUtilities.DeepEqual(table3, table4)).to.equal(false) + end) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/.robloxrc new file mode 100644 index 0000000..d5a9604 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/.robloxrc @@ -0,0 +1,5 @@ +{ + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGameIcons.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGameIcons.lua new file mode 100644 index 0000000..9a11f9e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGameIcons.lua @@ -0,0 +1,30 @@ +local CorePackages = game:GetService("CorePackages") +local AppTempCommon = CorePackages.AppTempCommon +local Promise = require(AppTempCommon.LuaApp.Promise) +local ApiFetchThumbnails = require(AppTempCommon.LuaApp.Utils.ApiFetchThumbnails) +local GamesGetIcons = require(AppTempCommon.LuaApp.Http.Requests.GamesGetIcons) +local SetGameIcons = require(AppTempCommon.LuaApp.Actions.SetGameIcons) + +local DEFAULT_ICON_SIZE = "150x150" + +return function (networkImpl, universeIds, imageSize) + return function(store) + local state = store:getState() + local stateToCheckForDuplicates = state.GameIcons + + -- Filter out the icons that are already in the store. + local idsToGet = {} + for _, targetId in pairs(universeIds) do + if stateToCheckForDuplicates[targetId] == nil then + table.insert(idsToGet, targetId) + end + end + + if #idsToGet == 0 then + return Promise.resolve() + else + return ApiFetchThumbnails.Fetch(networkImpl, + idsToGet, imageSize or DEFAULT_ICON_SIZE, "Game", GamesGetIcons, SetGameIcons, store) + end + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGameThumbnails.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGameThumbnails.lua new file mode 100644 index 0000000..333b127 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGameThumbnails.lua @@ -0,0 +1,91 @@ +local CorePackages = game:GetService("CorePackages") +local LuaApp = CorePackages.AppTempCommon.LuaApp + +local GamesGetThumbnails = require(LuaApp.Http.Requests.GamesGetThumbnails) +local SetGameThumbnails = require(LuaApp.Actions.SetGameThumbnails) + +local Functional = require(CorePackages.AppTempCommon.Common.Functional) +local Promise = require(LuaApp.Promise) +local Result = require(LuaApp.Result) + +local TableUtilities = require(LuaApp.TableUtilities) + +local THUMBNAIL_PAGE_COUNT = 20 +local THUMBNAIL_SIZE = 150 +local RETRY_MAX_COUNT = math.max(0, settings():GetFVariable("LuaAppNonFinalThumbnailMaxRetries")) +local RETRY_TIME_MULTIPLIER = 2 -- seconds + +local function convertToId(value) + if type(value) ~= "number" and type(value) ~= "string" then + return Result.error("convertToId expects value passed in to be a number or a string") + end + + return Result.success(tostring(value)) +end + +local function subdivideThumbnailTokenArray(thumbnailTokens, tokenLimit) + local someTokens = {} + for i = 1, #thumbnailTokens, tokenLimit do + local subArray = Functional.Take(thumbnailTokens, tokenLimit, i) + table.insert(someTokens, subArray) + end + + return someTokens +end + +local function fetchThumbnailBatch(networkImpl, store, thumbnailTokens) + return GamesGetThumbnails(networkImpl, thumbnailTokens, THUMBNAIL_SIZE, THUMBNAIL_SIZE):andThen(function(result) + local thumbnails = {} + local unfinalizedThumbnails = {} + + for _,image in pairs(result.responseBody) do + local convertToIdResult = convertToId(image.universeId) + + convertToIdResult:match(function(universeId) + if image.final == false then + unfinalizedThumbnails[universeId] = image.retryToken + else + -- index all of the thumbnails by universeId + thumbnails[universeId] = image + end + end, function(convertToIdError) + warn(convertToIdError) + end) + end + + store:dispatch(SetGameThumbnails(thumbnails)) + return Promise.resolve(unfinalizedThumbnails) + end) +end + +local function fetchSubdividedThumbnailsArray(networkImpl, store, thumbnailTokens) + return fetchThumbnailBatch(networkImpl, store, thumbnailTokens):andThen(function(unfinalizedThumbnails) + local remainingUnfinalizedThumbnails = unfinalizedThumbnails + + for retryCount = 1, RETRY_MAX_COUNT do + if TableUtilities.FieldCount(remainingUnfinalizedThumbnails) == 0 then + return -- Bail out, we're done! + end + + wait(RETRY_TIME_MULTIPLIER * math.pow(2, retryCount - 1)) + remainingUnfinalizedThumbnails = fetchThumbnailBatch(networkImpl, store, remainingUnfinalizedThumbnails):await() + end + end) +end + +local function fetchThumbnails(networkImpl, thumbnailTokens) + return function(store) + -- NOTE : because the size of each thumbnail token, me must limit the number we can fetch at a time. + -- So break apart the array of tokens we get into smaller, more manageable pieces. + local fetchPromises = {} + local someTokens = subdivideThumbnailTokenArray(thumbnailTokens, THUMBNAIL_PAGE_COUNT) + for _, thumbsArr in ipairs(someTokens) do + local promise = fetchSubdividedThumbnailsArray(networkImpl, store, thumbsArr) + table.insert(fetchPromises, promise) + end + + return Promise.all(fetchPromises) + end +end + +return fetchThumbnails diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGamesDataByPlaceIds.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGamesDataByPlaceIds.lua new file mode 100644 index 0000000..74d7e56 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGamesDataByPlaceIds.lua @@ -0,0 +1,37 @@ +local CorePackages = game:GetService("CorePackages") +local ApiFetchGameIcons = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchGameIcons) +local Functional = require(CorePackages.AppTempCommon.Common.Functional) +local GamesMultigetPlaceDetails = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.GamesMultigetPlaceDetails) +local PlaceInfoModel = require(CorePackages.AppTempCommon.LuaChat.Models.PlaceInfoModel) +local ReceivedPlacesInfos = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedPlacesInfos) + +local LuaAppFlags = CorePackages.AppTempCommon.LuaApp.Flags +local convertUniverseIdToString = require(LuaAppFlags.ConvertUniverseIdToString) + +return function(networkImpl, placeIds) + return function(store) + if not placeIds or #placeIds == 0 then + return + end + + return GamesMultigetPlaceDetails(networkImpl, placeIds):andThen(function(result) + local data = result.responseBody + + local thumbnailUniverseIds = {} + local placeInfos = Functional.Map(data, function(placeInfoData) + local placeInfo = PlaceInfoModel.fromWeb(placeInfoData) + local universeId = convertUniverseIdToString(placeInfo.universeId) + table.insert(thumbnailUniverseIds, universeId) + return placeInfo + end) + + store:dispatch(ReceivedPlacesInfos(placeInfos)) + + if #thumbnailUniverseIds > 0 then + store:dispatch(ApiFetchGameIcons(networkImpl, thumbnailUniverseIds)) + end + + return placeInfos + end) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchPlaceInfos.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchPlaceInfos.lua new file mode 100644 index 0000000..d80ae99 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchPlaceInfos.lua @@ -0,0 +1,24 @@ +local CorePackages = game:GetService("CorePackages") + +local Functional = require(CorePackages.AppTempCommon.Common.Functional) +local GetPlaceInfos = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.GetPlaceInfos) + +-- LuaChat +local PlaceInfoModel = require(CorePackages.AppTempCommon.LuaChat.Models.PlaceInfoModel) +local ReceivedMultiplePlaceInfos = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedMultiplePlaceInfos) + +return function(networkImpl, placeIds) + return function(store) + return GetPlaceInfos(networkImpl, placeIds):andThen(function(result) + local data = result.responseBody + + local placeInfos = Functional.Map(data, function(placeInfoData) + return PlaceInfoModel.fromWeb(placeInfoData) + end) + + store:dispatch(ReceivedMultiplePlaceInfos(placeInfos)) + + return placeInfos + end) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriendCount.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriendCount.lua new file mode 100644 index 0000000..04e2a0e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriendCount.lua @@ -0,0 +1,29 @@ +local CorePackages = game:GetService("CorePackages") + +local Actions = CorePackages.AppTempCommon.LuaApp.Actions +local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests + +local UsersGetFriendCount = require(Requests.UsersGetFriendCount) +local SetFriendCount = require(Actions.SetFriendCount) + +local isNewFriendsEndpointsEnabled = require(CorePackages.AppTempCommon.LuaChat.Flags.isNewFriendsEndpointsEnabled) + +return function(networkImpl) + return function(store) + return UsersGetFriendCount(networkImpl):andThen(function(result) + local data = result.responseBody + + if isNewFriendsEndpointsEnabled() then + if data.count then + store:dispatch(SetFriendCount(data.count)) + end + else + if data.success and data.count then + store:dispatch(SetFriendCount(data.count)) + end + end + + return data.count + end) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriends.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriends.lua new file mode 100644 index 0000000..8578a38 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriends.lua @@ -0,0 +1,68 @@ +local CorePackages = game:GetService("CorePackages") +local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests + +local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) +local ApiFetchUsersPresences = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchUsersPresences) +local ApiFetchUsersThumbnail = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchUsersThumbnail) +local UsersGetFriends = require(Requests.UsersGetFriends) + +local FetchUserFriendsStarted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsStarted) +local FetchUserFriendsFailed = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsFailed) +local FetchUserFriendsCompleted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsCompleted) +local UserModel = require(CorePackages.AppTempCommon.LuaApp.Models.User) +local UpdateUsers = require(CorePackages.AppTempCommon.LuaApp.Thunks.UpdateUsers) + +return function(requestImpl, userId, thumbnailRequest, checkPoints) + return function(store) + store:dispatch(FetchUserFriendsStarted(userId)) + + if checkPoints ~= nil and checkPoints.startFetchUserFriends ~= nil then + checkPoints:startFetchUserFriends() + end + + local fetchedUserIds = {} + return UsersGetFriends(requestImpl, userId):andThen(function(response) + local responseBody = response.responseBody + + local newUsers = {} + for _, userData in pairs(responseBody.data) do + local id = tostring(userData.id) + + userData.isFriend = true + local newUser = UserModel.fromDataTable(userData) + + table.insert(fetchedUserIds, id) + newUsers[newUser.id] = newUser + end + store:dispatch(UpdateUsers(newUsers)) + + if checkPoints ~= nil and checkPoints.finishFetchUserFriends ~= nil then + checkPoints:finishFetchUserFriends() + end + + return fetchedUserIds + end):andThen(function(userIds) + if checkPoints ~= nil and checkPoints.startFetchUsersPresences ~= nil then + checkPoints:startFetchUsersPresences() + end + -- Asynchronously fetch friend thumbnails so we don't block display of UI + store:dispatch(ApiFetchUsersThumbnail.Fetch(requestImpl, userIds, thumbnailRequest)) + + return store:dispatch(ApiFetchUsersPresences(requestImpl, userIds)) + end):andThen( + function(result) + store:dispatch(FetchUserFriendsCompleted(userId)) + + if checkPoints ~= nil and checkPoints.finishFetchUsersPresences ~= nil then + checkPoints:finishFetchUsersPresences() + end + + return Promise.resolve(fetchedUserIds) + end, + function(response) + store:dispatch(FetchUserFriendsFailed(userId, response)) + return Promise.reject(response) + end + ) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersPresences.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersPresences.lua new file mode 100644 index 0000000..cbaed26 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersPresences.lua @@ -0,0 +1,22 @@ +local CorePackages = game:GetService("CorePackages") + +local LuaApp = CorePackages.AppTempCommon.LuaApp +local ChatUtils = CorePackages.AppTempCommon.LuaChat.Utils + +local getPlaceIds = require(ChatUtils.getFriendsActiveGamesPlaceIdsFromUsersPresence) +local receiveUsersPresence = require(ChatUtils.receiveUsersPresence) + +local ApiFetchGamesDataByPlaceIds = require(LuaApp.Thunks.ApiFetchGamesDataByPlaceIds) +local UsersGetPresence = require(LuaApp.Http.Requests.UsersGetPresence) + +return function(networkImpl, userIds) + return function(store) + return UsersGetPresence(networkImpl, userIds):andThen(function(result) + local userPresences = result.responseBody.userPresences + receiveUsersPresence(userPresences, store) + + local placeIds = getPlaceIds(userPresences, store) + store:dispatch(ApiFetchGamesDataByPlaceIds(networkImpl, placeIds)) + end) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersThumbnail.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersThumbnail.lua new file mode 100644 index 0000000..7daded7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersThumbnail.lua @@ -0,0 +1,192 @@ +local CorePackages = game:GetService("CorePackages") + +local Cryo = require(CorePackages.Cryo) + +local Actions = CorePackages.AppTempCommon.LuaApp.Actions +local TableUtilities = require(CorePackages.AppTempCommon.LuaApp.TableUtilities) +local PromiseUtilities = require(CorePackages.AppTempCommon.LuaApp.PromiseUtilities) + +local ThumbnailsGetAvatar = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.ThumbnailsGetAvatar) +local ThumbnailsGetAvatarHeadshot = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.ThumbnailsGetAvatarHeadshot) + +local AvatarThumbnailTypes = require(CorePackages.AppTempCommon.LuaApp.Enum.AvatarThumbnailTypes) + +local SetUserThumbnail = require(Actions.SetUserThumbnail) +local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) +local PerformFetch = require(CorePackages.AppTempCommon.LuaApp.Thunks.Networking.Util.PerformFetch) +local Result = require(CorePackages.AppTempCommon.LuaApp.Result) + +local RETRY_MAX_COUNT = math.max(0, settings():GetFVariable("LuaAppNonFinalThumbnailMaxRetries")) +local RETRY_TIME_MULTIPLIER = math.max(0, settings():GetFVariable("LuaAppThumbnailsApiRetryTimeMultiplier")) + +local MAX_REQUEST_COUNT = 100 + +local ThumbnailsTypeToApiMap = { + [AvatarThumbnailTypes.AvatarThumbnail] = ThumbnailsGetAvatar, + [AvatarThumbnailTypes.HeadShot] = ThumbnailsGetAvatarHeadshot, +} + +local function subdivideEntries(entries, limit) + local subArrays = {} + for i = 1, #entries, limit do + local subArray = Cryo.List.getRange(entries, i, i + limit - 1) + table.insert(subArrays, subArray) + end + return subArrays +end + +local function keyMapper(userId, thumbnailType, thumbnailSize) + return "luaapp.usersthumbnailsapi." .. userId .. "." .. thumbnailType .. "." .. thumbnailSize +end + +local function isCompleteThumbnailData(entry) + return type(entry) == "table" + and type(entry.targetId) == "number" + and type(entry.state) == "string" + and type(entry.imageUrl) == "string" +end + +local ApiFetchUsersThumbnail = {} + +function ApiFetchUsersThumbnail.getThumbnailsSizeArgForSize(thumbnailSize) + assert(typeof(thumbnailSize) == "string", + string.format("ApiFetchUsersThumbnail expects a string for thumbnailSize. Type: %s", typeof(thumbnailSize)) + ) + + assert(string.match(thumbnailSize, 'Size.+x'), + string.format( + "ApiFetchUsersThumbnail expects thumbnailSize to follow format \"Size..x..\" Current thumbnailSize: ", + thumbnailSize + ) + ) + return string.gsub(thumbnailSize, "Size", "") +end + +function ApiFetchUsersThumbnail._fetch(networkImpl, listOfUserIds, thumbnailRequest) + local thumbnailSize = thumbnailRequest.thumbnailSize + local thumbnailType = thumbnailRequest.thumbnailType + + local thumbnailSizeRequestArg = ApiFetchUsersThumbnail.getThumbnailsSizeArgForSize(thumbnailSize) + local thumbnailsApiForThumbnailType = ThumbnailsTypeToApiMap[thumbnailType] + + assert(typeof(thumbnailType) == "string", + "ApiFetchUsersThumbnail expects thumbnailType to be a string") + assert(typeof(thumbnailsApiForThumbnailType) == "function", + "ApiFetchUsersThumbnail failed to find api for given type: ", thumbnailType) + + local function keyMapperForCurrentTypeAndSize(userId) + return keyMapper(userId, thumbnailType, thumbnailSize) + end + + local function getTableOfFailedResults(userIds) + local results = {} + for _, userId in pairs(userIds) do + local key = keyMapperForCurrentTypeAndSize(userId) + results[key] = Result.new(false, { + targetId = userId, + }) + end + return results + end + + return PerformFetch.Batch(listOfUserIds, keyMapperForCurrentTypeAndSize, function(store, userIdsToFetch) + local function fetchThumbnails(userIdsProvided) + return thumbnailsApiForThumbnailType(networkImpl, userIdsProvided, thumbnailSizeRequestArg):andThen( + function(result) + local results = getTableOfFailedResults(userIdsProvided) + local data = result and result.responseBody and result.responseBody.data + if typeof(data) == "table" then + for _, entry in pairs(data) do + if isCompleteThumbnailData(entry) then + local userId = tostring(entry.targetId) + local key = keyMapperForCurrentTypeAndSize(userId) + local success = false + if entry.state == "Completed" then + store:dispatch(SetUserThumbnail(tostring(entry.targetId), entry.imageUrl, thumbnailType, thumbnailSize)) + success = true + end + results[key] = Result.new(success, entry) + end + end + end + + return Promise.resolve(results) + end, + function(err) + local results = getTableOfFailedResults(userIdsProvided) + return Promise.resolve(results) + end + ) + end + + return fetchThumbnails(userIdsToFetch):andThen(function(results) + local completedThumbnails = {} + local thumbnailResults = results + + if _G.__TESTEZ_RUNNING_TEST__ then + RETRY_MAX_COUNT = 1 + RETRY_TIME_MULTIPLIER = 0.001 + end + + local function retry(retryCount) + local remainingUserIdsToFetch = {} + + for key, result in pairs(thumbnailResults) do + local isSuccessful, thumbnailInfo = result:unwrap() + + if isSuccessful and thumbnailInfo.state == "Completed" then + completedThumbnails[key] = result + elseif isSuccessful and thumbnailInfo.state == "Pending" then + table.insert(remainingUserIdsToFetch, thumbnailInfo.targetId) + end + end + + if TableUtilities.FieldCount(remainingUserIdsToFetch) == 0 then + return Promise.resolve(completedThumbnails) + end + + local delayPromise = Promise.new(function(resolve, reject) + coroutine.wrap(function() + wait(RETRY_TIME_MULTIPLIER * math.pow(2, retryCount - 1)) + resolve() + end)() + end) + + return delayPromise:andThen(function() + return fetchThumbnails(remainingUserIdsToFetch) + end):andThen(function(newResults) + thumbnailResults = newResults + if retryCount > 1 then + return retry(retryCount - 1) + else + return Promise.resolve(completedThumbnails) + end + end) + end + + return retry(RETRY_MAX_COUNT) + end) + end) +end + +function ApiFetchUsersThumbnail.Fetch(networkImpl, userIds, thumbnailRequests) + return function(store) + local allPromises = {} + local subArraysOfUserIds = subdivideEntries(userIds, MAX_REQUEST_COUNT) + + for _, thumbnailRequest in pairs(thumbnailRequests) do + for _, limitedListOfUserIds in pairs(subArraysOfUserIds) do + local promise = store:dispatch(ApiFetchUsersThumbnail._fetch(networkImpl, limitedListOfUserIds, thumbnailRequest)) + table.insert(allPromises, promise) + end + end + + return PromiseUtilities.Batch(allPromises) + end +end + +function ApiFetchUsersThumbnail.GetFetchingStatus(state, userId, thumbnailType, thumbnailSize) + return PerformFetch.GetStatus(state, keyMapper(userId, thumbnailType, thumbnailSize)) +end + +return ApiFetchUsersThumbnail \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiSendGameInvite.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiSendGameInvite.lua new file mode 100644 index 0000000..9ae7c77 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiSendGameInvite.lua @@ -0,0 +1,44 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests + +local ChatSendMessage = require(Requests.ChatSendMessage) +local ChatStartOneToOneConversation = require(Requests.ChatStartOneToOneConversation) + +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator) + + +local ChatSendGameLinkMessage = require(Requests.ChatSendGameLinkMessage) + +return function(networkImpl, userId, placeInfo) + local clientId = Players.LocalPlayer.UserId + + -- Construct the invite messages based on place info + local inviteTextMessage + inviteTextMessage = RobloxTranslator:FormatByKey( + "Feature.SettingsHub.Message.InviteToGameTitle", { PLACENAME = placeInfo.name } + ) + + return function(store) + return ChatStartOneToOneConversation(networkImpl, userId, clientId):andThen(function(conversationResult) + local conversation = conversationResult.responseBody.conversation + + return ChatSendMessage(networkImpl, conversation.id, inviteTextMessage):andThen(function() + local function handleResult(inviteResult) + local data = inviteResult.responseBody + + return { + resultType = data.resultType, + conversationId = conversation.id, + placeId = placeInfo.universeRootPlaceId, + } + end + + return ChatSendGameLinkMessage(networkImpl, conversation.id, placeInfo.universeId):andThen(handleResult) + end) + end) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/.robloxrc new file mode 100644 index 0000000..fc3d643 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/.robloxrc @@ -0,0 +1,5 @@ +{ + "language": { + "mode": "nonstrict" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/Util/PerformFetch.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/Util/PerformFetch.lua new file mode 100644 index 0000000..8143137 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/Util/PerformFetch.lua @@ -0,0 +1,245 @@ +local CorePackages = game:GetService("CorePackages") + +local Result = require(CorePackages.AppTempCommon.LuaApp.Result) +local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) + +local PromiseUtilities = require(CorePackages.AppTempCommon.LuaApp.PromiseUtilities) +local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus) +local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus) + +--[[ + PerformFetch wraps the notion of a network request together with its fetching status + so that it is easier to de-duplicate concurrent requests for the same resource. The + fetching status for individual fetching operations are available in the store as: + + storeState.FetchingStatus[key] + + When you use one of the methods in this helper, you provide a key (or keymap), and + supply a functor that will only be called when a fetch actually needs to be performed. + + Any follow-up andThen/catch clauses will be correctly daisy-chained onto the original + ongoing fetch request if one is already underway. +]] +local PerformFetch = {} + +local batchPromises = {} -- fetch key = outstanding promise from PerformFetch.Batch + +--[[ + Helper function for unit tests to be able to clean up batchPromises created from + previous test case. This is because unit tests don't wait until the mock requests + are resolved and moves onto the next test. If tests happen to generate duplicate + fetchStatusKey, unresolved batchPromise will throw thinking that the promise does + not have the correct status. +]] +function PerformFetch.ClearOutstandingPromiseStatus() + batchPromises = {} +end + +local function singleFetchKeymapper(item) + -- Single fetch keys are used directly + return item +end + +--[[ + Get the fetching status for a given status key. Defaults to + RetrievalStatus.NotStarted for missing keys. +]] +function PerformFetch.GetStatus(state, fetchStatusKey) + assert(typeof(state) == "table") + assert(typeof(fetchStatusKey) == "string") + assert(#fetchStatusKey > 0) + return state.FetchingStatus[fetchStatusKey] or RetrievalStatus.NotStarted +end + +--[[ + Perform a fetch operation for a single resource. + + Args: + fetchStatusKey - String key for the fetching status to index the Rodux store. + fetchFunctor - Functor to call when a fetch needs to be performed for fetchStatusKey. + + Returns: + A Promise that resolves or rejects in accordance with the result of fetchFunctor, or the + promise for the original fetch if one is already ongoing. + + Usage: + In your main thunk, wrap your inner store function with this thunk, like this: + + return function(arg1, arg2) + return PerformFetch.Single("mykey", function(store) + return doYourLogicHere() -- Must return a Promise!!! + end) + end + + Please note that in order for single fetches to integrate well with batch fetches, + your promise must NEVER resolve or reject with multiple arguments! Wrap your results + in a table instead. +]] +function PerformFetch.Single(fetchStatusKey, fetchFunctor) + assert(typeof(fetchStatusKey) == "string") + assert(typeof(fetchFunctor) == "function") + assert(#fetchStatusKey > 0) + + return function(store) + -- Call batch API to handle the individual fetch + return PerformFetch.Batch({ fetchStatusKey }, singleFetchKeymapper, function(batchStore, itemsToFetch) + assert(#itemsToFetch == 1) + + local functorPromise = fetchFunctor(batchStore) + assert(Promise.is(functorPromise)) + + return functorPromise:andThen(function(...) + assert(#{...} <= 1) + return Promise.resolve({ [fetchStatusKey] = Result.new(true, (...)) }) + end, function(...) + assert(#{...} <= 1) + return Promise.resolve({ [fetchStatusKey] = Result.new(false, (...)) }) + end) + end)(store):andThen(function(batchResults) + local success, value = batchResults[fetchStatusKey]:unwrap() + if success then + return Promise.resolve(value) + else + return Promise.reject(value) + end + end) + end +end + +--[[ + Perform a fetch operation for multiple resources at once (batching). + + Args: + items - The list of item ids that need to be fetched. + keyMapper - A function that maps items to string keys for the Rodux store. + fetchFunctor - A function that will be called when at least one item needs to be fetched. + + Returns: + A Promise that always resolves. Result data is returned in a single table to the + andThen() clause, where item fetch keys are the keys and the results for each key + are encoded using a Result object. + + Usage: + In your main thunk, wrap your inner store function with this thunk, like this: + + local MAPPER = function(item) + return doSomething(item) -- Each key must be unique + end + + return function(arg1, arg2) + local allItems = makeYourItemsList() + return PerformFetch.Batch(allItems, MAPPER, function(store, itemsToFetch) + return doYourLogicHere(itemsToFetch) -- Must return a Promise!!! + end) + end + + Your implementation of fetchFunctor should return a promise that resolves + according to the structure of PromiseUtilities.Batch, ex: + + return Promise.resolve({ + itemFetchKey1 = Result.new(true, payload1), + itemFetchKey2 = Result.new(false, payload2), -- failed + }) + + Any other resolving arguments will be dropped for consistency and safety of the API. + Since this is a batching API, your implementation should NOT reject(). + + Please keep in mind that batching calls have to fit into an environment where they may + be daisy chained onto other batching calls, and those results have to be amalgamated + at the end of the chain into unique tables for each of the callers! +]] +function PerformFetch.Batch(items, keyMapper, fetchFunctor) + assert(typeof(items) == "table") + assert(typeof(keyMapper) == "function") + assert(typeof(fetchFunctor) == "function") + + return function(store) + local itemsToFetch = {} + local itemsToFetchKeyMap = {} + local batchPromisesForItemsAlreadyBeingFetched = {} + + -- Filter out items that do not need to be fetched + for _, item in ipairs(items) do + local fetchStatusKey = keyMapper(item) + local fetchingStatus = PerformFetch.GetStatus(store:getState(), fetchStatusKey) + local batchPromise = batchPromises[fetchStatusKey] + + if batchPromise then + assert(fetchingStatus == RetrievalStatus.Fetching) + + batchPromisesForItemsAlreadyBeingFetched[fetchStatusKey] = batchPromise + else + assert(fetchingStatus ~= RetrievalStatus.Fetching) + + table.insert(itemsToFetch, item) + itemsToFetchKeyMap[item] = fetchStatusKey + end + end + + local doResolve + local batchFetchingPromise = Promise.new(function(resolve) + doResolve = resolve + end) + + -- Call functor if there are items to fetch, otherwise short-circuit it + -- We want to call it FIRST because we need to kick off async fetch before blocking + -- on other responses. + local functorPromise + if #itemsToFetch > 0 then + -- Place remaining items into fetching state and make entry in table before + -- we kick off functor just in case it returns already-completed promise + for _, fetchStatusKey in pairs(itemsToFetchKeyMap) do + store:dispatch(UpdateFetchingStatus(fetchStatusKey, RetrievalStatus.Fetching)) + batchPromises[fetchStatusKey] = batchFetchingPromise + end + + functorPromise = fetchFunctor(store, itemsToFetch) + assert(Promise.is(functorPromise)) + else + functorPromise = Promise.resolve({}) + end + + functorPromise:andThen(function(myResults) + myResults = myResults or {} -- No resolve args = empty table for ease of use + + return PromiseUtilities.Batch(batchPromisesForItemsAlreadyBeingFetched):andThen(function(batchResults) + local filteredResults = {} + for batchKey, batchResult in pairs(batchResults) do + -- Extract only the result for the key we care about from the batch results + local _, value = batchResult:unwrap() + filteredResults[batchKey] = value[batchKey] + end + + return myResults, filteredResults + end) + end, + function() + assert(false, "PerformFetch fetchFunctor should never reject") + end):andThen(function(myResults, batchResults) + -- Iterate on requested items rather than on actual result set + -- so that we are sure to check all our keys and ignore extra ones + for _, fetchKey in pairs(itemsToFetchKeyMap) do + local resultObj = myResults[fetchKey] + if Result.is(resultObj) then + batchResults[fetchKey] = resultObj + else + batchResults[fetchKey] = Result.error() + end + + -- Update fetching status in store from Result object status + -- (The extra parens unwrap a multi-return value!) + local itemStatus = (batchResults[fetchKey]:unwrap()) and RetrievalStatus.Done or RetrievalStatus.Failed + store:dispatch(UpdateFetchingStatus(fetchKey, itemStatus)) + batchPromises[fetchKey] = nil + end + + return batchResults + end):andThen(function(joinedResults) + doResolve(joinedResults) + end) + + return batchFetchingPromise + end +end + +return PerformFetch diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/Util/PerformFetch.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/Util/PerformFetch.spec.lua new file mode 100644 index 0000000..63bf9b8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/Util/PerformFetch.spec.lua @@ -0,0 +1,510 @@ +return function() + local PerformFetch = require(script.Parent.PerformFetch) + local CorePackages = game:GetService("CorePackages") + + local Rodux = require(CorePackages.Rodux) + local FetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Reducers.FetchingStatus) + local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus) + local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) + local Result = require(CorePackages.AppTempCommon.LuaApp.Result) + + local function batchKeyMapper(item) + return tostring(item) .. "_key" + end + + local TEST_ITEM_1 = "item1" + local TEST_ITEM_2 = "item2" + + local TEST_KEY_1 = batchKeyMapper(TEST_ITEM_1) + local TEST_KEY_2 = batchKeyMapper(TEST_ITEM_2) + + local function MockReducer(state, action) + state = state or {} + return { + FetchingStatus = FetchingStatus(state.FetchingStatus, action), + } + end + + local function makeResolver() + local startResolve + local startReject + local testPromise = Promise.new(function(resolve, reject) + startResolve = resolve + startReject = reject + end) + + return { + promise = testPromise, + resolve = startResolve, + reject = startReject + } + end + + local function doDispatchSingle(store, key, functor) + -- Wrap functor like a thunk would normally be + local thunkFunc = function() + return PerformFetch.Single(key, functor) + end + + return store:dispatch(thunkFunc()) + end + + local function doDispatchBatch(store, items, functor) + -- Wrap functor like a thunk would normally be + local thunkFunc = function() + return PerformFetch.Batch(items, batchKeyMapper, functor) + end + + return store:dispatch(thunkFunc()) + end + + local function doBasicSingleTest(key) + local resolver = makeResolver() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local thunkPromise = doDispatchSingle(store, key, function() + return resolver.promise + end) + + return { + store = store, + resolver = resolver, + promise = thunkPromise + } + end + + local function doBasicBatchTest(keys) + local resolver = makeResolver() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local thunkPromise = doDispatchBatch(store, keys, function() + return resolver.promise + end) + + return { + store = store, + resolver = resolver, + promise = thunkPromise + } + end + + describe("PerformFetch.GetStatus", function() + it("should return NotStarted for missing key", function() + local state = { FetchingStatus = {} } + local status = PerformFetch.GetStatus(state, TEST_KEY_1) + + expect(status).to.equal(RetrievalStatus.NotStarted) + end) + + it("should return matching status for state in store", function() + local statusesToTest = { + RetrievalStatus.NotStarted, + RetrievalStatus.Fetching, + RetrievalStatus.Done, + RetrievalStatus.Failed + } + + for _, testStatus in ipairs(statusesToTest) do + local state = { + FetchingStatus = { + [TEST_KEY_1] = testStatus + } + } + + expect(PerformFetch.GetStatus(state, TEST_KEY_1)).to.equal(testStatus) + end + + expect(#statusesToTest).to.equal(4) + end) + end) + + describe("PerformFetch.Single", function() + it("should set fetching state in store when fetch begins", function() + local bundle = doBasicSingleTest(TEST_KEY_1) + + expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Fetching) + bundle.resolver.resolve() -- clear key from global fetchingPromiseMap or later tests get blocked + end) + + it("should pass store parameter to fetch functor", function() + local originalStore = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local newStore + doDispatchSingle(originalStore, TEST_KEY_1, function(store) + newStore = store + return Promise.resolve() + end) + + expect(newStore ~= nil).to.equal(true) + end) + + it("should set fetching state to done for sync resolve", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.resolve() + end) + + expect(store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Done) + end) + + it("should set fetching state to failed for sync reject", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.reject() + end) + + expect(store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Failed) + end) + + it("should set fetching state to Done after async fetch resolves", function() + local bundle = doBasicSingleTest(TEST_KEY_1) + + bundle.resolver.resolve() + expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Done) + end) + + + it("should set fetching state to Failed after async fetch rejects", function() + local bundle = doBasicSingleTest(TEST_KEY_1) + + bundle.resolver.reject() + expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Failed) + end) + + + it("should not mix fetching status of two separate keys", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.resolve() + end) + + doDispatchSingle(store, TEST_KEY_2, function() + return Promise.reject() + end) + + expect(store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Done) + expect(store:getState().FetchingStatus[TEST_KEY_2]).to.equal(RetrievalStatus.Failed) + end) + + it("should pass original promise args to daisy-chained promise upon resolve", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local testTable = { a = 1 } + + local passedArg + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.resolve(testTable) + end):andThen(function(results) + passedArg = results + end) + + expect(passedArg).to.equal(testTable) + end) + + it("should pass original promise args to daisy-chained promise upon reject", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local testTable = { a = 1 } + + local passedArg + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.reject(testTable) + end):catch(function(results) + passedArg = results + end) + + expect(passedArg).to.equal(testTable) + end) + + it("should not call second thunk instance for same key while request is ongoing", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local firstThunkExecuted = false + local secondThunkExecuted = false + + local firstThunkResolver = makeResolver() + doDispatchSingle(store, TEST_KEY_1, function() + firstThunkExecuted = true + return firstThunkResolver.promise + end) + + doDispatchSingle(store, TEST_KEY_1, function() + secondThunkExecuted = true + return Promise.resolve() + end) + + expect(store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Fetching) + expect(firstThunkExecuted).to.equal(true) + expect(secondThunkExecuted).to.equal(false) + + firstThunkResolver.resolve() + end) + + it("should call both thunks when the first one is completed soon enough", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local firstThunkExecuted = false + local secondThunkExecuted = false + + doDispatchSingle(store, TEST_KEY_1, function() + firstThunkExecuted = true + return Promise.resolve() + end) + + doDispatchSingle(store, TEST_KEY_1, function() + secondThunkExecuted = true + return Promise.resolve() + end) + + expect(firstThunkExecuted).to.equal(true) + expect(secondThunkExecuted).to.equal(true) + end) + + it("should resolve daisy-chained promises after thunk resolves", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local chainedPromiseExecuted = false + + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.resolve() + end):andThen(function() + chainedPromiseExecuted = true + end):catch(function() + assert(false) + end) + + expect(chainedPromiseExecuted).to.equal(true) + end) + + it("should reject daisy-chained promises after thunk rejects", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local chainedCatchExecuted = false + + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.reject() + end):andThen(function() + assert(false) + end):catch(function() + chainedCatchExecuted = true + end) + + expect(chainedCatchExecuted).to.equal(true) + end) + + it("should resolve daisy-chained promises on second thunk after first resolves", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local secondPromiseResolved = false + + local startResolve + local firstThunkPromise = Promise.new(function(resolve) + startResolve = resolve + end) + + doDispatchSingle(store, TEST_KEY_1, function() + return firstThunkPromise + end) + + doDispatchSingle(store, TEST_KEY_1, function() + assert(false) + return Promise.reject() + end):andThen(function() + secondPromiseResolved = true + end):catch(function() + assert(false) + end) + + expect(secondPromiseResolved).to.equal(false) + + startResolve() + + expect(secondPromiseResolved).to.equal(true) + end) + + it("should reject daisy-chained promises on second thunk after first thunk rejects", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local secondPromiseRejected = false + + local startReject + local firstThunkPromise = Promise.new(function(_, reject) + startReject = reject + end) + + doDispatchSingle(store, TEST_KEY_1, function() + return firstThunkPromise + end) + + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.new(function() end) + end):andThen(function() + assert(false) + end):catch(function() + secondPromiseRejected = true + end) + + expect(secondPromiseRejected).to.equal(false) + + startReject() + + expect(secondPromiseRejected).to.equal(true) + end) + end) + + describe("PerformFetch.Batch", function() + it("should set fetching state in store for all batch items when fetching begins", function() + local originalItemList = { TEST_ITEM_1, TEST_ITEM_2 } + local bundle = doBasicBatchTest(originalItemList) + + expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Fetching) + expect(bundle.store:getState().FetchingStatus[TEST_KEY_2]).to.equal(RetrievalStatus.Fetching) + + local results = { + [TEST_KEY_1] = Result.new(true), + [TEST_KEY_2] = Result.new(true), + } + + bundle.resolver.resolve(results) -- Cleanup to avoid test blockage + end) + + it("should set fetching state to matching status for all batch items when fetching completes successfully", function() + local originalItemList = { TEST_ITEM_1, TEST_ITEM_2 } + local bundle = doBasicBatchTest(originalItemList) + + local results = { + [TEST_KEY_1] = Result.new(true), + [TEST_KEY_2] = Result.new(false), + } + + bundle.resolver.resolve(results) + + expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Done) + expect(bundle.store:getState().FetchingStatus[TEST_KEY_2]).to.equal(RetrievalStatus.Failed) + end) + + it("should fail items when they are not in the result list", function() + local originalItemList = { TEST_ITEM_1, TEST_ITEM_2 } + local bundle = doBasicBatchTest(originalItemList) + + bundle.resolver.resolve({}) + + expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Failed) + expect(bundle.store:getState().FetchingStatus[TEST_KEY_2]).to.equal(RetrievalStatus.Failed) + end) + + it("should return a daisy chainable batch style promise that resolves with results", function() + local originalItemList = { TEST_ITEM_1, TEST_ITEM_2 } + local bundle = doBasicBatchTest(originalItemList) + + local chainedResults = nil + bundle.promise:andThen(function(promisedResults) + chainedResults = promisedResults + end) + + local results = { + [TEST_KEY_1] = Result.new(true, 42), + [TEST_KEY_2] = Result.new(false, 29), + } + + bundle.resolver.resolve(results) + + local result1Status, result1Value = chainedResults[TEST_KEY_1]:unwrap() + local result2Status, result2Value = chainedResults[TEST_KEY_2]:unwrap() + + expect(result1Status).to.equal(true) + expect(result1Value).to.equal(42) + + expect(result2Status).to.equal(false) + expect(result2Value).to.equal(29) + end) + + it("should not call batch functor when there are no items to fetch", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local promise = doDispatchBatch(store, {}, function() + assert(false, "Functor should not be called when there are no items to fetch") + end) + + local promiseResolved = false + promise:andThen(function() + promiseResolved = true + end) + + expect(promiseResolved).to.equal(true) + end) + + it("should amalgamate results from multiple batch calls", function() + local testItemList = { TEST_ITEM_1, TEST_ITEM_2 } + + local bundle = doBasicBatchTest(testItemList) + + local promise2 = doDispatchBatch(bundle.store, testItemList, function(_, itemsToFetch) + assert(false, "second batch should not be called") + return Promise.resolve({ }) + end) + + local chainedResults = nil + promise2:andThen(function(promisedResults) + chainedResults = promisedResults + end) + + local results = { + [TEST_KEY_1] = Result.new(true, 41), + [TEST_KEY_2] = Result.new(false, 39), + } + + bundle.resolver.resolve(results) + + local result1Status, result1Value = chainedResults[TEST_KEY_1]:unwrap() + local result2Status, result2Value = chainedResults[TEST_KEY_2]:unwrap() + + expect(result1Status).to.equal(true) + expect(result1Value).to.equal(41) + + expect(result2Status).to.equal(false) + expect(result2Value).to.equal(39) + end) + end) + + describe("PerformFetch with mixed Single/Batch", function() + it("should include outstanding single results for matching batch keys", function() + local singleBundle = doBasicSingleTest(TEST_KEY_1) + + local batchItemCount = -1 + local batchPromise = doDispatchBatch(singleBundle.store, { TEST_ITEM_1, TEST_ITEM_2 }, + function(_, items) + batchItemCount = #items + return Promise.resolve({ [TEST_KEY_2] = Result.new(false, 35) }) + end) + + singleBundle.resolver.resolve(49) + + local chainedResults = nil + batchPromise:andThen(function(results) + chainedResults = results + end) + + local result1Status, result1Value = chainedResults[TEST_KEY_1]:unwrap() + local result2Status, result2Value = chainedResults[TEST_KEY_2]:unwrap() + + expect(result1Status).to.equal(true) + expect(result1Value).to.equal(49) + + expect(result2Status).to.equal(false) + expect(result2Value).to.equal(35) + + expect(batchItemCount).to.equal(1) + end) + + it("should use batch result for duplicate single request", function() + local batchBundle = doBasicBatchTest({ TEST_ITEM_1, TEST_ITEM_2 }) + + local singlePromise = doDispatchSingle(batchBundle.store, TEST_KEY_1, function() + assert(false, "Single functor should not be called") + return Promise.reject() + end) + + batchBundle.resolver.resolve({ + [TEST_KEY_1] = Result.new(true, 42), + [TEST_KEY_2] = Result.new(true, 39) + }) + + local chainedResult = nil + singlePromise:andThen(function(result) + chainedResult = result + end) + + expect(chainedResult).to.equal(42) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/UpdateUsers.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/UpdateUsers.lua new file mode 100644 index 0000000..c606bd6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/UpdateUsers.lua @@ -0,0 +1,50 @@ +local CorePackages = game:GetService("CorePackages") + +local User = require(CorePackages.AppTempCommon.LuaApp.Models.User) +local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers) +local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount) + +return function(users) + return function(store) + local friendCountOffset = 0 + local updatedUsers = {} + + for _, user in pairs(users) do + local needsUpdate = false + local userId = user.id + local isFriend = user.isFriend + local offset = 0 + + assert(typeof(isFriend) == "boolean") + + local userInStore = store:getState().Users[userId] + if userInStore then + -- Mark user with needsUpdate if any of the field is different + -- from the existing user information in Store. + if not User.compare(userInStore, user) then + needsUpdate = true + if userInStore.isFriend ~= isFriend then + offset = isFriend and 1 or -1 + end + end + else + needsUpdate = true + offset = isFriend and 1 or 0 + end + + if needsUpdate then + friendCountOffset = friendCountOffset + offset + updatedUsers[userId] = user + end + end + + if next(updatedUsers) then + store:dispatch(AddUsers(updatedUsers)) + end + + if friendCountOffset ~= 0 then + local currentFriendCount = store:getState().FriendCount + store:dispatch(SetFriendCount(currentFriendCount + friendCountOffset)) + end + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/UpdateUsers.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/UpdateUsers.spec.lua new file mode 100644 index 0000000..6b31edf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/UpdateUsers.spec.lua @@ -0,0 +1,123 @@ +return function() + local CorePackages = game:GetService("CorePackages") + + local Rodux = require(CorePackages.Rodux) + local Immutable = require(CorePackages.AppTempCommon.Common.Immutable) + + local UpdateUsers = require(CorePackages.AppTempCommon.LuaApp.Thunks.UpdateUsers) + + local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers) + local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount) + + local FriendCount = require(CorePackages.AppTempCommon.LuaChat.Reducers.FriendCount) + local Users = require(CorePackages.AppTempCommon.LuaApp.Reducers.Users) + + local User = require(CorePackages.AppTempCommon.LuaApp.Models.User) + + local function UsersReducerMonitor (state, action) + state = state or { + numberOfAddUsersCalled = 0, + numberOfUsersPassedIn = 0, + } + + if action.type == AddUsers.name then + state.numberOfAddUsersCalled = state.numberOfAddUsersCalled + 1 + state.numberOfUsersPassedIn = 0 + for _, _ in pairs(action.users) do + state.numberOfUsersPassedIn = state.numberOfUsersPassedIn + 1 + end + end + + return state + end + + local function FriendCountReducerMonitor (state, action) + state = state or { + numberOfSetFriendCountCalled = 0, + } + + if action.type == SetFriendCount.name then + state.numberOfSetFriendCountCalled = state.numberOfSetFriendCountCalled + 1 + end + + return state + end + + local function CustomReducer(state, action) + state = state or {} + + return { + Users = Users(state.Users, action), + UsersReducerMonitor = UsersReducerMonitor(state.UsersReducerMonitor, action), + + FriendCount = FriendCount(state.FriendCount, action), + FriendCountReducerMonitor = FriendCountReducerMonitor(state.FriendCountReducerMonitor, action), + } + end + + local listOfUsers = { + ["1"] = User.fromData(1, "Hedonism Bot", true), + ["2"] = User.fromData(2, "Hypno Toad", true), + ["3"] = User.fromData(3, "John Zoidberg", false), + ["4"] = User.fromData(4, "Pazuzu", true), + ["5"] = User.fromData(5, "Ogden Wernstrom", true), + ["6"] = User.fromData(6, "Lrrr", true), + } + + it("should do nothing if empty list of users is provided", function() + local store = Rodux.Store.new(CustomReducer, {}, { + Rodux.thunkMiddleware, + }) + store:dispatch(UpdateUsers({ })) + + local state = store:getState() + + expect(state.UsersReducerMonitor.numberOfAddUsersCalled).to.equal(0) + expect(state.FriendCountReducerMonitor.numberOfSetFriendCountCalled).to.equal(0) + end) + + it("should update only the number of users with modified data", function() + local store = Rodux.Store.new(CustomReducer, { + Users = listOfUsers, + }, { + Rodux.thunkMiddleware, + }) + + local currentUsers = store:getState().Users + local listOfUsersWithPotentialUpdates = { + Immutable.Set(currentUsers["2"], "presence", User.PresenceType.IN_GAME), -- changed + Immutable.Set(currentUsers["5"], "isFriend", false), -- changed + Immutable.Set(currentUsers["6"], "isFriend", true), -- did not change + } + + store:dispatch(UpdateUsers(listOfUsersWithPotentialUpdates)) + + local state = store:getState() + expect(state.UsersReducerMonitor.numberOfAddUsersCalled).to.equal(1) + expect(state.UsersReducerMonitor.numberOfUsersPassedIn).to.equal(2) + end) + + it("should correctly update the number of friends", function() + local store = Rodux.Store.new(CustomReducer, {}, { + Rodux.thunkMiddleware, + }) + store:dispatch(UpdateUsers(listOfUsers)) + + local state = store:getState() + expect(state.FriendCountReducerMonitor.numberOfSetFriendCountCalled).to.equal(1) + expect(state.FriendCount).to.equal(5) + + local currentUsers = store:getState().Users + local listOfUsersWithPotentialUpdates = { + Immutable.Set(currentUsers["2"], "presence", User.PresenceType.IN_GAME), -- friendship didn't change + Immutable.Set(currentUsers["5"], "isFriend", false), -- friendship changed + Immutable.Set(currentUsers["6"], "isFriend", false), -- friendship changed + User.fromData(7, "Nibbler", true), -- new friend + } + + store:dispatch(UpdateUsers(listOfUsersWithPotentialUpdates)) + + state = store:getState() + expect(state.FriendCount).to.equal(4) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/.robloxrc new file mode 100644 index 0000000..d5a9604 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/.robloxrc @@ -0,0 +1,5 @@ +{ + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ApiFetchThumbnails.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ApiFetchThumbnails.lua new file mode 100644 index 0000000..7167dad --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ApiFetchThumbnails.lua @@ -0,0 +1,73 @@ +local CorePackages = game:GetService("CorePackages") +local Cryo = require(CorePackages.Cryo) +local ArgCheck = require(CorePackages.ArgCheck) +local PromiseUtilities = require(CorePackages.AppTempCommon.LuaApp.PromiseUtilities) +local FetchSubdividedThumbnails = require(script.Parent.FetchSubdividedThumbnails) + +local PerformFetch = require(CorePackages.AppTempCommon.LuaApp.Thunks.Networking.Util.PerformFetch) + +local ICON_PAGE_COUNT = 100 +local ICON_SIZE = "150x150" + +local function convertToId(value) + return tostring(value) +end + +local ApiFetchThumbnails = {} + +local keyMapper = function (request) + local targetId = request.targetId + local size = request.iconSize and "."..request.iconSize or "" + local requestName = request.requestName and "."..request.requestName or "" + return "luaapp.thumbnails." .. convertToId(targetId)..size..requestName +end + +ApiFetchThumbnails.KeyMapper = keyMapper + +local function subdivideIdsArray(requests, limit) + local someTokens = {} + for i = 1, #requests, limit do + local subArray = Cryo.List.getRange(requests, i, i + limit - 1) + table.insert(someTokens, subArray) + end + return someTokens +end + +function ApiFetchThumbnails.Fetch(networkImpl, targetIds, imageSize, requestName, fetchFunction, storeDispatch, store) + local size = imageSize or ICON_SIZE + ArgCheck.isType(targetIds, "table", "targetIds") + ArgCheck.isType(requestName, "string", "requestName") + ArgCheck.isNonNegativeNumber(#targetIds, "targetIds count") + + local requests = {} + local promises = {} + -- Filter out the icons that are already in the store. + for _, targetId in pairs(targetIds) do + table.insert(requests, { + targetId = targetId, + iconSize = size, + }) + end + local subdividedRequestsArray = subdivideIdsArray(requests, ICON_PAGE_COUNT) + for _, subdividedRequests in ipairs(subdividedRequestsArray) do + table.insert( + promises, + store:dispatch(FetchSubdividedThumbnails.Fetch( + networkImpl, + subdividedRequests, + keyMapper, + requestName, + fetchFunction, + storeDispatch + )) + ) + end + + return PromiseUtilities.Batch(promises) +end + +function ApiFetchThumbnails.GetFetchingStatus(state, targetId, iconSize, requestName) + return PerformFetch.GetStatus(state, keyMapper({targetId = targetId, requestName = requestName, iconSize = iconSize})) +end + +return ApiFetchThumbnails \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/FetchSubdividedThumbnails.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/FetchSubdividedThumbnails.lua new file mode 100644 index 0000000..47d7a20 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/FetchSubdividedThumbnails.lua @@ -0,0 +1,139 @@ +local CorePackages = game:GetService("CorePackages") +local LuaApp = CorePackages.AppTempCommon.LuaApp +local ArgCheck = require(CorePackages.ArgCheck) + +local Thumbnail = require(LuaApp.Models.Thumbnail) + +local PerformFetch = require(LuaApp.Thunks.Networking.Util.PerformFetch) +local Promise = require(LuaApp.Promise) +local Result = require(LuaApp.Result) + +local TableUtilities = require(LuaApp.TableUtilities) + +local RETRY_MAX_COUNT = math.max(0, settings():GetFVariable("LuaAppNonFinalThumbnailMaxRetries")) +local RETRY_TIME_MULTIPLIER = math.max(0, settings():GetFVariable("LuaAppThumbnailsApiRetryTimeMultiplier")) -- seconds + +local FetchSubdividedThumbnails = {} + +function FetchSubdividedThumbnails._fetchIcons(store, networkImpl, targetIds, iconSize, keyMapper, requestName, fetchFunction, storeDispatch) + local function keyMapperForCurrentRequestNameAndSize(targetId) + return keyMapper({ + targetId = targetId, + requestName = requestName, + iconSize = iconSize + }) + end + + local function getTableOfFailedResults(failedTargetIds) + local results = {} + for _, targetId in pairs(failedTargetIds) do + local key = keyMapperForCurrentRequestNameAndSize(targetId) + results[key] = Result.new(false, { + targetId = targetId, + }) + end + return results + end + + return fetchFunction(networkImpl, targetIds, iconSize):andThen( + function(result) + local results = getTableOfFailedResults(targetIds) + local validIcons = {} + + local data = result and result.responseBody and result.responseBody.data + if typeof(data) == "table" then + for _, iconInfo in pairs(data) do + if Thumbnail.isCompleteThumbnailData(iconInfo) then + local targetId = tostring(iconInfo.targetId) + local success = false + if Thumbnail.checkStateIsFinal(iconInfo.state) then + validIcons[targetId] = Thumbnail.fromThumbnailData(iconInfo, iconSize) + success = true + end + results[keyMapperForCurrentRequestNameAndSize(targetId)] = Result.new(success, iconInfo) + end + end + end + store:dispatch(storeDispatch(validIcons)) + return Promise.resolve(results) + end, + function(err) + local results = getTableOfFailedResults(targetIds) + return Promise.resolve(results) + end + ) +end + +function FetchSubdividedThumbnails._fetch(store, networkImpl, targetIds, size, keyMapper, requestName, fetchFunction, storeDispatch) + return FetchSubdividedThumbnails._fetchIcons(store, networkImpl, targetIds, size, keyMapper, requestName, fetchFunction, storeDispatch) + :andThen(function(results) + local completedIcons = {} + local iconResults = results + + if _G.__TESTEZ_RUNNING_TEST__ then + RETRY_MAX_COUNT = 1 + RETRY_TIME_MULTIPLIER = 0.001 + end + + local function retry(retryCount) + local remainingUnfinalizedIcons = {} + + for k, result in pairs(iconResults) do + local isSuccessful, iconInfo = result:unwrap() + -- Retry icon request for targetId that failed. + if isSuccessful and Thumbnail.checkStateIsFinal(iconInfo.state) then + completedIcons[k] = result + else + table.insert(remainingUnfinalizedIcons, iconInfo) + end + end + + if TableUtilities.FieldCount(remainingUnfinalizedIcons) == 0 then + --All requests are successful + return Promise.resolve(completedIcons) + end + + local delayPromise = Promise.new(function(resolve, reject) + coroutine.wrap(function() + wait(RETRY_TIME_MULTIPLIER * math.pow(2, retryCount - 1)) + resolve() + end)() + end) + + return delayPromise:andThen(function() + return FetchSubdividedThumbnails._fetchIcons(store, networkImpl, + targetIds, size, keyMapper, requestName, fetchFunction, storeDispatch) + end):andThen(function(newResults) + iconResults = newResults + if retryCount > 1 then + return retry(retryCount - 1) + else + return Promise.resolve(completedIcons) + end + end) + end + + return retry(RETRY_MAX_COUNT) + end) +end + +function FetchSubdividedThumbnails.Fetch(networkImpl, requests, keyMapper, requestName, fetchFunction, storeDispatch) + ArgCheck.isType(requests, "table", "requests") + ArgCheck.isType(requestName, "string", "requestName") + ArgCheck.isNonNegativeNumber(#requests, "requests count") + + FetchSubdividedThumbnails.KeyMapper = keyMapper + return PerformFetch.Batch(requests, keyMapper, function(store, filteredrequests) + local targetIdsNeeded = {} + local size + -- Filter out the icons that are already in the store. + for _, request in ipairs(filteredrequests) do + local targetId = request.targetId + size = request.iconSize + table.insert(targetIdsNeeded, targetId) + end + return FetchSubdividedThumbnails._fetch(store, networkImpl, targetIdsNeeded, size, keyMapper, requestName, fetchFunction, storeDispatch) + end) +end + +return FetchSubdividedThumbnails \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ThrottleUserId.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ThrottleUserId.lua new file mode 100644 index 0000000..d1f96c2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ThrottleUserId.lua @@ -0,0 +1,10 @@ +-- Helper function to throttle based on player Id: +return function(throttle, userId) + assert(type(throttle) == "number") + assert(type(userId) == "number") + + -- Determine userRollout using last two digits of user ID: + -- (+1 to change range from 0-99 to 1-100 as 0 is off, 100 is full on): + local userRollout = (userId % 100) + 1 + return userRollout <= throttle +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ThrottleUserId.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ThrottleUserId.spec.lua new file mode 100644 index 0000000..ea0424d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ThrottleUserId.spec.lua @@ -0,0 +1,67 @@ +return function() + local ThrottleUserId = require(script.Parent.ThrottleUserId) + + describe("ThrottleUserId", function() + it("should always reject zero%", function() + local gating = ThrottleUserId(0, 10000) + expect(gating).to.equal(false) + + gating = ThrottleUserId(0, 10001) + expect(gating).to.equal(false) + + gating = ThrottleUserId(0, 10025) + expect(gating).to.equal(false) + + gating = ThrottleUserId(0, 10075) + expect(gating).to.equal(false) + + gating = ThrottleUserId(0, 10099) + expect(gating).to.equal(false) + + gating = ThrottleUserId(0, 10100) + expect(gating).to.equal(false) + end) + + it("should always accept 100%", function() + local gating = ThrottleUserId(100, 10000) + expect(gating).to.equal(true) + + gating = ThrottleUserId(100, 10001) + expect(gating).to.equal(true) + + gating = ThrottleUserId(100, 10025) + expect(gating).to.equal(true) + + gating = ThrottleUserId(100, 10075) + expect(gating).to.equal(true) + + gating = ThrottleUserId(100, 10099) + expect(gating).to.equal(true) + + gating = ThrottleUserId(100, 10100) + expect(gating).to.equal(true) + end) + + it("should reject IDs over throttle percent", function() + local gating = ThrottleUserId(25, 10050) + expect(gating).to.equal(false) + + gating = ThrottleUserId(50, 10075) + expect(gating).to.equal(false) + + gating = ThrottleUserId(75, 10099) + expect(gating).to.equal(false) + end) + + it("should accept IDs under throttle percent", function() + local gating = ThrottleUserId(1, 10100) + expect(gating).to.equal(true) + + gating = ThrottleUserId(10, 10109) + expect(gating).to.equal(true) + + gating = ThrottleUserId(25, 10023) + expect(gating).to.equal(true) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/.robloxrc new file mode 100644 index 0000000..635c2ec --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/.robloxrc @@ -0,0 +1,5 @@ +{ + "lint": { + "LocalShadow": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedConversation.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedConversation.lua new file mode 100644 index 0000000..4d63e47 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedConversation.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CorePackages").AppTempCommon +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(convo) + return { + conversation = convo, + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedMultiplePlaceInfos.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedMultiplePlaceInfos.lua new file mode 100644 index 0000000..2bb1bb1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedMultiplePlaceInfos.lua @@ -0,0 +1,10 @@ +local Modules = game:GetService("CorePackages").AppTempCommon + +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(placeInfos) + return { + placeInfos = placeInfos, + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedUserPresence.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedUserPresence.lua new file mode 100644 index 0000000..6d90f86 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedUserPresence.lua @@ -0,0 +1,30 @@ +local Modules = game:GetService("CorePackages").AppTempCommon + +local Action = require(Modules.Common.Action) +local LuaDateTime = require(Modules.LuaChat.DateTime) + +return Action(script.Name, function(userId, + presence, lastLocation, + placeId, rootPlaceId, + gameInstanceId, lastOnlineISO, universeId, previousUniverseId) + + local lastOnline = 0 + if lastOnlineISO ~= nil then + local lastDateTime = LuaDateTime.fromIsoDate(lastOnlineISO) + if lastDateTime ~= nil then + lastOnline = lastDateTime:GetUnixTimestamp() + end + end + + return { + userId = userId, + presence = presence, + lastLocation = lastLocation, + placeId = placeId, + rootPlaceId = rootPlaceId, + gameInstanceId = gameInstanceId, + lastOnline = lastOnline, + universeId = universeId, + previousUniverseId = previousUniverseId, + } +end) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/DateTime.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/DateTime.lua new file mode 100644 index 0000000..11c15a1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/DateTime.lua @@ -0,0 +1,621 @@ +local LocalizationService = game:GetService("LocalizationService") +local CorePackages = game:GetService("CorePackages") +local GetFFlagUseDateTimeType = require(CorePackages.AppTempCommon.LuaApp.Flags.GetFFlagUseDateTimeType) + +local FFlagChinaLicensingApp = settings():GetFFlag("ChinaLicensingApp") + +--[[ + This is a Lua implementation of the DateTime API proposal. It'll eventually + be implemented in C++ and merged into the rest of the codebase if this model + of working with dates ends up being useful. +]] + +local TimeZone = require(script.Parent.TimeZone) +local TimeUnit = require(script.Parent.TimeUnit) + +local LuaDateTime = {} + +local monthShortNames = { + "Jan", "Feb", "Mar", "Apr", + "May", "Jun", "Jul", "Aug", + "Sep", "Oct", "Nov", "Dec" +} + +local monthLongNames = { + "January", "February", "March", "April", + "May", "June", "July", "August", + "September", "October", "November", "December" +} + +local dayShortNames = { + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" +} + +local dayLongNames = { + "Sunday", "Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday" +} + +--[[ + We structure tokens like this to preserve order, since Lua associative + arrays have no inherent order. +]] +local tokens = { + {"YYYY", function(values) + return tostring(values.Year) + end}, + {"MMMM", function(values) + return monthLongNames[values.Month] + end}, + {"MMM", function(values) + return monthShortNames[values.Month] + end}, + {"MM", function(values) + return ("%02d"):format(values.Month) + end}, + {"M", function(values) + return tostring(values.Month) + end}, + {"DDDD", function(values) + return dayLongNames[values.WeekDay] + end}, + {"DDD", function(values) + return dayShortNames[values.WeekDay] + end}, + {"DD", function(values) + return ("%02d"):format(values.Day) + end}, + {"D", function(values) + return tostring(values.Day) + end}, + {"HH", function(values) + local hour = values.Hour + + return ("%02d"):format(hour) + end}, + {"H", function(values) + local hour = values.Hour + + return tostring(hour) + end}, + {"hh", function(values) + local hour = values.Hour % 12 + if hour == 0 then + hour = 12 + end + + return ("%02d"):format(hour) + end}, + {"h", function(values) + local hour = values.Hour % 12 + if hour == 0 then + hour = 12 + end + + return tostring(hour) + end}, + {"mm", function(values) + return ("%02d"):format(values.Minute) + end}, + {"m", function(values) + return tostring(values.Minute) + end}, + {"ss", function(values) + return ("%02d"):format(values.Seconds) + end}, + {"s", function(values) + return tostring(values.Seconds) + end}, + {"A", function(values) + if FFlagChinaLicensingApp then + return values.Hour >= 12 and "下午" or "上午" + else + return values.Hour >= 12 and "PM" or "AM" + end + end}, + {"a", function(values) + if FFlagChinaLicensingApp then + return values.Hour >= 12 and "下午" or "上午" + else + return values.Hour >= 12 and "pm" or "am" + end + end} +} + +local tokenKeys = {} +for _, pair in ipairs(tokens) do + table.insert(tokenKeys, pair[1]) +end + +local tokenMap = {} +for _, pair in ipairs(tokens) do + tokenMap[pair[1]] = pair[2] +end + +--[[ + What's the next token in this source? +]] +local function getToken(source, i) + local char = source:sub(i, i) + + for _, token in ipairs(tokenKeys) do + -- Only keep checking if the first character matches the token + if token:sub(1, 1) == char then + local match = source:sub(i, i + token:len() - 1) + + if match == token then + return token + end + end + end +end + +--[[ + An estimate of the current time zone's offset from UTC in seconds. + + This might fail for weird timezones (UTC +/- 14), but we can fix that by + picking a reference time that's further away from the Unix epoch. +]] +local function getTimeZoneOffset() + local actualEpoch = 86400 + 43200 + local epoch = os.time({year = 1970, month = 1, day = 2, isdst = -1}) + if epoch then + return actualEpoch - epoch + else + return 0 + end +end + +-- Remove all above functions and tables when clean up GetFFlagUseDateTimeType() + +--[[ + Create a DateTime with the given values in UTC. + + All values are optional! +]] +function LuaDateTime.new(year, month, day, hour, minute, seconds, milliseconds) + if GetFFlagUseDateTimeType() then + local self = {} + self.dateTime = DateTime.fromUniversalTime( + year or 1970, + month or 1, + day or 1, + hour or 0, + minute or 0, + seconds or 0, + milliseconds or 0) + setmetatable(self, LuaDateTime) + return self + end + + local tzOffset = getTimeZoneOffset() + local timestamp = os.time({ + year = year or 1970, + month = month or 1, + day = day or 1, + hour = hour or 0, + min = minute or 0, + sec = seconds or 0, + isdst = -1 + }) + if timestamp == nil then + timestamp = 0 + end + + if seconds then + local subseconds = seconds - math.floor(seconds) + timestamp = timestamp + subseconds + end + + return LuaDateTime.fromUnixTimestamp(timestamp + tzOffset) +end + +--[[ + Create a DateTime representing now. +]] +function LuaDateTime.now() + if GetFFlagUseDateTimeType() then + local self = {} + self.dateTime = DateTime.now() + setmetatable(self, LuaDateTime) + return self + end + + return LuaDateTime.fromUnixTimestamp(os.time()) +end + +--[[ + Create a Datetime from the given Unix timestamp. + + Limited to the range [0, 2^32) when GetFFlagUseDateTimeType() is off, which lets us represent + dates out to about 2038. + + When GetFFlagUseDateTimeType() is on, year range is 1400-9999, and timestamp range is between + first second of year 1400 to the last second of year 9999. +]] +function LuaDateTime.fromUnixTimestamp(timestamp) + assert(type(timestamp) == "number", "Invalid argument #1 to fromUnixTimestamp, expected number.") + + if GetFFlagUseDateTimeType() then + local self = {} + self.dateTime = DateTime.fromUnixTimestampMillis(timestamp*1000) + setmetatable(self, LuaDateTime) + return self + end + + local self = {} + + self.value = timestamp + + setmetatable(self, LuaDateTime) + + return self +end + +--[[ + Attempt to create a DateTime from an ISO 8601 date-time string. + + Will return nil on failure and output a warning to a console denoting what + went wrong. This can probably turned into a second return value if we need + to handle that data programmatically. +]] +function LuaDateTime.fromIsoDate(isoDate) + assert(type(isoDate) == "string", "Invalid argument #1 to DateTime.fromIsoDate, expected string.") + + if GetFFlagUseDateTimeType() then + local self = {} + self.dateTime = DateTime.fromIsoDate(isoDate) + setmetatable(self, LuaDateTime) + return self + end + + local datePattern = "^(%d+)%-(%d+)%-(%d+)" -- 0000-00-00 + local timePattern = "T(%d+):(%d+):(%d+%.?%d*)" -- T00:00:00 + local utcPattern = "Z$" + local timeZonePattern = "([+-]%d+):(%d+)$" -- either Z or +/- followed by "00:00" + + local timezone = 0 + local values = {1970, 1, 1, 0, 0, 0} + local year, month, day = isoDate:match(datePattern) + + if not year then + warn(("Invalid ISO 8601 date: %q"):format(isoDate)) + return nil + end + + values[1] = tonumber(year) + values[2] = tonumber(month) + values[3] = tonumber(day) + + local hour, minute, seconds = isoDate:match(timePattern) + + if hour then + values[4] = tonumber(hour) + values[5] = tonumber(minute) + values[6] = tonumber(seconds) + + local isUtc = isoDate:match(utcPattern) + + if not isUtc then + local offsetHours, offsetMinutes = isoDate:match(timeZonePattern) + + if not offsetHours then + local offsetTotal = getTimeZoneOffset() + offsetHours = offsetTotal / 3600 + offsetMinutes = 0 + + warn(("Invalid time zone in ISO 8601 date: %q -- falling back to local time"):format(isoDate)) + end + + timezone = 3600 * tonumber(offsetHours) + 60 * tonumber(offsetMinutes) + end + end + + local date = LuaDateTime.new(unpack(values)) + date.value = date.value - timezone + + return date +end + +--[[ + Format our current date using a formatting string. Look at the DateTime + proposal to see information about the different formatting tokens. + Generally, they try to resemble LDML and/or Moment.js-style formatting. + + The time zone parameter is optional and defaults to the current time zone, + TimeZone.Current. +]] +function LuaDateTime:Format(formatString, tz, localeId) + assert(type(formatString) == "string", "Invalid argument #1 to Format, expected string.") + + if GetFFlagUseDateTimeType() then + tz = tz or TimeZone.Current + localeId = localeId or LocalizationService.RobloxLocaleId + + if tz == TimeZone.UTC then + return self.dateTime:FormatUniversalTime(formatString, localeId) + elseif tz == TimeZone.Current then + return self.dateTime:FormatLocalTime(formatString, localeId) + else + error(("Invalid TimeZone \"%s\""):format(tostring(tz)), 2) + end + end + + tz = tz or TimeZone.Current + + local values = self:GetValues(tz) + + local buffer = {} + + local i = 1 + while i <= formatString:len() do + local char = formatString:sub(i, i) + local token = getToken(formatString, i) + + if token then + table.insert(buffer, tokenMap[token](values)) + i = i + token:len() + elseif char == "[" then + -- Crawl forward until the next ] and interpret that text literally + local j = i + while j <= formatString:len() do + j = j + 1 + + if formatString:sub(j, j) == "]" then + break + end + end + + table.insert(buffer, formatString:sub(i + 1, j - 1)) + i = j + 1 + else + table.insert(buffer, char) + i = i + 1 + end + end + + local result = table.concat(buffer) + + return result +end + +--[[ + Get a table of values representing the date-time in the given timezone. + + The time zone parameter is optional and defaults to the current zime zone, + TimeZone.Current. + + When GetFFlagUseDateTimeType() is true, table would include + {Year, Month, Day, Hour, Minute, Second, Millisecond} +]] +function LuaDateTime:GetValues(tz) + if GetFFlagUseDateTimeType() then + tz = tz or TimeZone.Current + + if tz == TimeZone.UTC then + return self.dateTime:ToUniversalTime() + elseif tz == TimeZone.Current then + return self.dateTime:ToLocalTime() + else + error(("Invalid TimeZone \"%s\""):format(tostring(tz)), 2) + end + end + + tz = tz or TimeZone.Current + + local reference + + if tz == TimeZone.Current then + reference = os.date("*t", self.value) + elseif tz == TimeZone.UTC then + reference = os.date("!*t", self.value) + end + + if not reference then + error(("Invalid TimeZone \"%s\""):format(tostring(tz)), 2) + end + + return { + Year = reference.year, + Month = reference.month, + Day = reference.day, + Hour = reference.hour, + Minute = reference.min, + Seconds = reference.sec, + WeekDay = reference.wday + } +end + +--[[ + Recover a Unix timestamp representing the DateTime's value. +]] +function LuaDateTime:GetUnixTimestamp() + if GetFFlagUseDateTimeType() then + if self.dateTime:ToUniversalTime().Millisecond > 0 then + return self.dateTime.UnixTimestamp + (self.dateTime.UnixTimestampMillis % 1000)/1000 + else + return self.dateTime.UnixTimestamp + end + end + + return self.value +end + +--[[ + Format the DateTime as an ISO 8601 date string with time attached. + + Always formats the time as UTC. There generally aren't many reasons to + generate an ISO 8601 date in another time zone. +]] +function LuaDateTime:GetIsoDate() + if GetFFlagUseDateTimeType() then + return self.dateTime:ToIsoDate() + end + + return self:Format("YYYY-MM-DD[T]HH:mm:ss[Z]", TimeZone.UTC) +end + +-- Used by IsSame +-- Remove when clean up GetFFlagUseDateTimeType() +local descendingGranularityUnits = { + { + unit = TimeUnit.Years, + key = "Year" + }, + { + unit = TimeUnit.Months, + key = "Month" + }, + { + unit = TimeUnit.Days, + key = "Day" + }, + { + unit = TimeUnit.Hours, + key = "Hour" + }, + { + unit = TimeUnit.Minutes, + key = "Minute" + }, + { + unit = TimeUnit.Seconds, + key = "Seconds" + } +} + +--[[ + Checks whether two DateTime values are the same, given a granularity and + timezone value. + + Granularity defaults to seconds and time zone defaults to the current local + time zone. + + Remove when clean up GetFFlagUseDateTimeType() +]] +function LuaDateTime:IsSame(other, granularity, timezone) + granularity = granularity or TimeUnit.Seconds + timezone = timezone or TimeZone.Current + + local selfUnix = self:GetUnixTimestamp() + local otherUnix = other:GetUnixTimestamp() + + if selfUnix == otherUnix then + return true + end + + local selfValues = self:GetValues(timezone) + local otherValues = other:GetValues(timezone) + + -- Week logic is special + if granularity == TimeUnit.Weeks then + local diff = math.abs(selfUnix - otherUnix) + local diffDays = diff / (60 * 60 * 24) + + -- Two dates separated by 7 or more whole days are never in the same week + if diffDays >= 7 then + return false + end + + -- Two dates separated by less than 7 days will be sorted monotonically + -- if they're in the same week + -- TODO: Use start-of-week value to shift WeekDay for locale + if selfUnix > otherUnix then + return selfValues.WeekDay >= otherValues.WeekDay + else + return selfValues.WeekDay <= otherValues.WeekDay + end + end + + for _, unit in ipairs(descendingGranularityUnits) do + local selfValue = selfValues[unit.key] + local otherValue = otherValues[unit.key] + + if selfValue ~= otherValue then + return false + end + + if unit.unit == granularity then + break + end + end + + return true +end + +--[[ + Get a human-readable timestamp relative to the given epoch, which defaults + to now. The format of the time is contextual to how far away the times are. +]] +function LuaDateTime:GetLongRelativeTime(epoch, timezone, localeId) + if GetFFlagUseDateTimeType() then + -- Not relative time format for now, will do that later in DateTime v2 + return self:Format("lll", timezone, localeId) + end + + timezone = timezone or TimeZone.Current + epoch = epoch or LuaDateTime.now() + + if FFlagChinaLicensingApp then + if self:IsSame(epoch, TimeUnit.Days, timezone) then + return self:Format("HH:mm A", timezone) + elseif self:IsSame(epoch, TimeUnit.Weeks, timezone) then + return self:Format("M月D日 | HH:mm A", timezone) + elseif self:IsSame(epoch, TimeUnit.Years, timezone) then + return self:Format("M月D日 | HH:mm A", timezone) + else + return self:Format("YYYY年M月D日 | HH:mm A", timezone) + end + else + if self:IsSame(epoch, TimeUnit.Days, timezone) then + return self:Format("h:mm A", timezone) + elseif self:IsSame(epoch, TimeUnit.Weeks, timezone) then + return self:Format("DDD | h:mm A", timezone) + elseif self:IsSame(epoch, TimeUnit.Years, timezone) then + return self:Format("MMM D | h:mm A", timezone) + else + return self:Format("MMM D, YYYY | h:mm A", timezone) + end + end +end + +--[[ + Get a human-readable timestamp relative to the given epoch, which defaults + to now. The format of the time is contextual to how far away the times are. +]] +function LuaDateTime:GetShortRelativeTime(epoch, timezone, localeId) + timezone = timezone or TimeZone.Current + + if GetFFlagUseDateTimeType() then + -- Not relative time format for now, will do that later in DateTime v2 + return self:Format("ll", timezone, localeId) + end + + epoch = epoch or LuaDateTime.now() + + if FFlagChinaLicensingApp then + if self:IsSame(epoch, TimeUnit.Days, timezone) then + return self:Format("HH:mm A", timezone) + elseif self:IsSame(epoch, TimeUnit.Weeks, timezone) then + return self:Format("M月D日", timezone) + elseif self:IsSame(epoch, TimeUnit.Years, timezone) then + return self:Format("M月D日", timezone) + else + return self:Format("YYYY年M月D日", timezone) + end + else + if self:IsSame(epoch, TimeUnit.Days, timezone) then + return self:Format("h:mm A", timezone) + elseif self:IsSame(epoch, TimeUnit.Weeks, timezone) then + return self:Format("DDD", timezone) + elseif self:IsSame(epoch, TimeUnit.Years, timezone) then + return self:Format("MMM D", timezone) + else + return self:Format("MMM D, YYYY", timezone) + end + end +end + +LuaDateTime.__index = LuaDateTime + +return LuaDateTime \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/DateTime.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/DateTime.spec.lua new file mode 100644 index 0000000..199fe0f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/DateTime.spec.lua @@ -0,0 +1,738 @@ +local CorePackages = game:GetService("CorePackages") +local GetFFlagUseDateTimeType = require(CorePackages.AppTempCommon.LuaApp.Flags.GetFFlagUseDateTimeType) +local FFlagChinaLicensingApp = settings():GetFFlag("ChinaLicensingApp") + +return function() + local LuaDateTime = require(script.Parent.DateTime) + local TimeZone = require(script.Parent.TimeZone) + local TimeUnit = require(script.Parent.TimeUnit) + local localeIds = { + "en-us", + "en-gb", + "en-au", + "en-ca", + "en-nz", + "de-de", + "es-es", + "es-mx", + "fr-fr", + "fr-ca", + "it-it", + "pt-pt", + "pt-br", + "ru-ru", + "ja-jp", + "ko-kr", + "zh-cn", + "zh-hk", + "zh-tw", + "zh-hans", + "zh-hant", + "zh-cjv", + } + + describe("Constructors", function() + it("should construct with 'new'", function() + expect(LuaDateTime.new()).to.be.ok() + expect(LuaDateTime.new(2017)).to.be.ok() + expect(LuaDateTime.new(2017, 5)).to.be.ok() + expect(LuaDateTime.new(2017, 5, 3)).to.be.ok() + expect(LuaDateTime.new(2017, 5, 3, 12)).to.be.ok() + expect(LuaDateTime.new(2017, 5, 3, 12, 34)).to.be.ok() + expect(LuaDateTime.new(2017, 5, 3, 12, 34, 51)).to.be.ok() + + if GetFFlagUseDateTimeType() then + expect(LuaDateTime.new(2017, 5, 3, 12, 34, 51, 999)).to.be.ok() + end + end) + + it("should construct with 'now'", function() + expect(LuaDateTime.now()).to.be.ok() + end) + + it("should construct from a Unix timestamp", function() + expect(LuaDateTime.fromUnixTimestamp(0)).to.be.ok() + expect(LuaDateTime.fromUnixTimestamp(os.time())).to.be.ok() + end) + + it("should construct from an ISO 8601 date", function() + if GetFFlagUseDateTimeType() then + -- Basic date + do + local date = LuaDateTime.fromIsoDate("1988-03-17") + expect(date).to.be.ok() + expect(date.dateTime:ToUniversalTime().Year).to.equal(1988) + expect(date.dateTime:ToUniversalTime().Month).to.equal(3) + expect(date.dateTime:ToUniversalTime().Day).to.equal(17) + expect(date.dateTime:ToUniversalTime().Hour).to.equal(0) + expect(date.dateTime:ToUniversalTime().Minute).to.equal(0) + expect(date.dateTime:ToUniversalTime().Second).to.equal(0) + expect(date.dateTime:ToUniversalTime().Millisecond).to.equal(0) + end + + -- Date and time + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16.999Z") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816.999) + expect(date.dateTime:ToUniversalTime().Year).to.equal(2017) + expect(date.dateTime:ToUniversalTime().Month).to.equal(4) + expect(date.dateTime:ToUniversalTime().Day).to.equal(10) + expect(date.dateTime:ToUniversalTime().Hour).to.equal(20) + expect(date.dateTime:ToUniversalTime().Minute).to.equal(40) + expect(date.dateTime:ToUniversalTime().Second).to.equal(16) + expect(date.dateTime:ToUniversalTime().Millisecond).to.equal(999) + end + + -- Date and time with no time zone + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16.1") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816.1) + expect(date.dateTime:ToUniversalTime().Year).to.equal(2017) + expect(date.dateTime:ToUniversalTime().Month).to.equal(4) + expect(date.dateTime:ToUniversalTime().Day).to.equal(10) + expect(date.dateTime:ToUniversalTime().Hour).to.equal(20) + expect(date.dateTime:ToUniversalTime().Minute).to.equal(40) + expect(date.dateTime:ToUniversalTime().Second).to.equal(16) + expect(date.dateTime:ToUniversalTime().Millisecond).to.equal(100) + end + + -- Date, time, and time zone offset + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16+01:00") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816 - 3600) + expect(date.dateTime:ToUniversalTime().Year).to.equal(2017) + expect(date.dateTime:ToUniversalTime().Month).to.equal(4) + expect(date.dateTime:ToUniversalTime().Day).to.equal(10) + expect(date.dateTime:ToUniversalTime().Hour).to.equal(19) + expect(date.dateTime:ToUniversalTime().Minute).to.equal(40) + expect(date.dateTime:ToUniversalTime().Second).to.equal(16) + expect(date.dateTime:ToUniversalTime().Millisecond).to.equal(0) + end + + -- Date, time, and negative time zone offset + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16-01:00") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816 + 3600) + expect(date.dateTime:ToUniversalTime().Year).to.equal(2017) + expect(date.dateTime:ToUniversalTime().Month).to.equal(4) + expect(date.dateTime:ToUniversalTime().Day).to.equal(10) + expect(date.dateTime:ToUniversalTime().Hour).to.equal(21) + expect(date.dateTime:ToUniversalTime().Minute).to.equal(40) + expect(date.dateTime:ToUniversalTime().Second).to.equal(16) + expect(date.dateTime:ToUniversalTime().Millisecond).to.equal(0) + end + else + -- Basic date + do + local date = LuaDateTime.fromIsoDate("1988-03-17") + expect(date).to.be.ok() + end + + -- Date and time + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16Z") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816) + end + + -- Date and time with no time zone + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16") + expect(date).to.be.ok() + end + + -- Date, time, and time zone offset + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16+01:00") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816 - 3600) + end + + -- Date, time, and negative time zone offset + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16-01:00") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816 + 3600) + end + end + end) + end) + + describe("Measurements", function() + it("should get values in UTC", function() + local date = LuaDateTime.new() + local values = date:GetValues(TimeZone.UTC) + + expect(values).to.be.ok() + expect(values.Year).to.be.a("number") + expect(values.Month).to.be.a("number") + expect(values.Day).to.be.a("number") + expect(values.Hour).to.be.a("number") + expect(values.Minute).to.be.a("number") + + if GetFFlagUseDateTimeType() then + expect(values.Second).to.be.a("number") + expect(values.Millisecond).to.be.a("number") + else + expect(values.Seconds).to.be.a("number") + + -- Locale specific! + expect(values.WeekDay).to.be.a("number") + end + end) + + it("should get values in local time", function() + local date = LuaDateTime.new() + local values = date:GetValues(TimeZone.Current) + + expect(values).to.be.ok() + expect(values.Year).to.be.a("number") + expect(values.Month).to.be.a("number") + expect(values.Day).to.be.a("number") + expect(values.Hour).to.be.a("number") + expect(values.Minute).to.be.a("number") + + if GetFFlagUseDateTimeType() then + expect(values.Second).to.be.a("number") + expect(values.Millisecond).to.be.a("number") + else + expect(values.Seconds).to.be.a("number") + + -- Locale specific! + expect(values.WeekDay).to.be.a("number") + end + end) + + it("should preserve values from 'new' constructor", function() + local date = LuaDateTime.new(2017, 11, 3, 12, 34, 51) + local values = date:GetValues(TimeZone.UTC) + + expect(values.Year).to.equal(2017) + expect(values.Month).to.equal(11) + expect(values.Day).to.equal(3) + expect(values.Hour).to.equal(12) + expect(values.Minute).to.equal(34) + + if GetFFlagUseDateTimeType() then + expect(values.Second).to.equal(51) + expect(values.Millisecond).to.equal(0) + else + expect(values.Seconds).to.equal(51) + end + end) + + it("should preserve Unix timestamp values", function() + do + local date = LuaDateTime.fromUnixTimestamp(0) + expect(date:GetUnixTimestamp()).to.equal(0) + end + + do + local date = LuaDateTime.fromUnixTimestamp(123456789) + expect(date:GetUnixTimestamp()).to.equal(123456789) + end + end) + end) + + describe("Formatting", function() + it("should preserve text within brackets", function() + local date = LuaDateTime.new(2017, 1, 2, 15, 8, 9) + + local function format(str) + return date:Format(str, TimeZone.UTC) + end + + expect(format("[Hello, world!]")).to.equal("Hello, world!") + expect(format("[YYYY-MM-DD]")).to.equal("YYYY-MM-DD") + end) + + it("should create identical ISO 8601 dates for UTC inputs", function() + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16Z") + expect(date:GetIsoDate()).to.equal("2017-04-10T20:40:16Z") + end) + + it("should have correct formatting tokens", function() + local date = LuaDateTime.new(2016, 1, 2, 15, 8, 9) + + -- Shortcut time zone specification + local function format(str, localeId) + return date:Format(str, TimeZone.UTC, localeId) + end + + expect(format("YYYY")).to.equal("2016") + expect(format("M")).to.equal("1") + expect(format("MM")).to.equal("01") + expect(format("D")).to.equal("2") + expect(format("DD")).to.equal("02") + expect(format("H")).to.equal("15") + expect(format("HH")).to.equal("15") + expect(format("h")).to.equal("3") + expect(format("hh")).to.equal("03") + expect(format("m")).to.equal("8") + expect(format("mm")).to.equal("08") + expect(format("s")).to.equal("9") + expect(format("ss")).to.equal("09") + + -- Locale-specific tests! + if GetFFlagUseDateTimeType() then + expect(format("SSS")).to.equal("000") + expect(format("SS")).to.equal("00") + expect(format("S")).to.equal("0") + expect(format("MMM", "en-us")).to.equal("Jan") + expect(format("MMMM", "en-us")).to.equal("January") + expect(format("MMM", "zh-cn")).to.equal("1月") + expect(format("MMMM", "zh-cn")).to.equal("一月") + expect(format("A", "en-us")).to.equal("PM") + expect(format("a", "en-us")).to.equal("pm") + expect(format("A", "zh-cn")).to.equal("下午") + expect(format("a", "zh-cn")).to.equal("下午") + else + expect(format("MMM")).to.equal("Jan") + expect(format("MMMM")).to.equal("January") + if FFlagChinaLicensingApp then + expect(format("A")).to.equal("下午") + expect(format("a")).to.equal("下午") + else + expect(format("A")).to.equal("PM") + expect(format("a")).to.equal("pm") + end + end + end) + + it("should handle dates around midnight", function() + local date = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + + expect(date:Format("H", TimeZone.UTC)).to.equal("0") + expect(date:Format("HH", TimeZone.UTC)).to.equal("00") + expect(date:Format("h", TimeZone.UTC)).to.equal("12") + expect(date:Format("hh", TimeZone.UTC)).to.equal("12") + + if GetFFlagUseDateTimeType() then + expect(date:Format("A", TimeZone.UTC, "en-us")).to.equal("AM") + expect(date:Format("a", TimeZone.UTC, "en-us")).to.equal("am") + expect(date:Format("A", TimeZone.UTC, "zh-cn")).to.equal("凌晨") + expect(date:Format("a", TimeZone.UTC, "zh-cn")).to.equal("凌晨") + else + if FFlagChinaLicensingApp then + expect(date:Format("a", TimeZone.UTC)).to.equal("上午") + else + expect(date:Format("a", TimeZone.UTC)).to.equal("am") + end + end + + end) + + it("should handle dates around noon", function() + local date = LuaDateTime.new(2017, 5, 23, 12, 0, 0) + + expect(date:Format("H", TimeZone.UTC)).to.equal("12") + expect(date:Format("HH", TimeZone.UTC)).to.equal("12") + expect(date:Format("h", TimeZone.UTC)).to.equal("12") + expect(date:Format("hh", TimeZone.UTC)).to.equal("12") + + if GetFFlagUseDateTimeType() then + expect(date:Format("A", TimeZone.UTC, "en-us")).to.equal("PM") + expect(date:Format("a", TimeZone.UTC, "en-us")).to.equal("pm") + expect(date:Format("A", TimeZone.UTC, "zh-cn")).to.equal("中午") + expect(date:Format("a", TimeZone.UTC, "zh-cn")).to.equal("中午") + else + if FFlagChinaLicensingApp then + expect(date:Format("a", TimeZone.UTC)).to.equal("下午") + else + expect(date:Format("a", TimeZone.UTC)).to.equal("pm") + end + end + end) + + it("should return correct 24-hour clock sequences", function() + local expected = { + "2017-09-13 00:00:00", + "2017-09-13 01:00:00", + "2017-09-13 02:00:00", + "2017-09-13 03:00:00", + "2017-09-13 04:00:00", + "2017-09-13 05:00:00", + "2017-09-13 06:00:00", + "2017-09-13 07:00:00", + "2017-09-13 08:00:00", + "2017-09-13 09:00:00", + "2017-09-13 10:00:00", + "2017-09-13 11:00:00", + "2017-09-13 12:00:00", + "2017-09-13 13:00:00", + "2017-09-13 14:00:00", + "2017-09-13 15:00:00", + "2017-09-13 16:00:00", + "2017-09-13 17:00:00", + "2017-09-13 18:00:00", + "2017-09-13 19:00:00", + "2017-09-13 20:00:00", + "2017-09-13 21:00:00", + "2017-09-13 22:00:00", + "2017-09-13 23:00:00", + "2017-09-14 00:00:00", + "2017-09-14 01:00:00", + } + + local formatString = "YYYY-MM-DD HH:mm:ss" + local date + + for _, localeId in ipairs(localeIds) do + date = LuaDateTime.new(2017, 9, 13, 0, 0, 0) + for i = 1, #expected do + local result = date:Format(formatString, TimeZone.UTC, localeId) + expect(result).to.equal(expected[i]) + + -- Advance once hour + date = date.fromUnixTimestamp(date:GetUnixTimestamp() + 3600) + end + end + end) + + it("should return correct 12-hour clock sequences", function() + local date = LuaDateTime.new(2017, 9, 13, 0, 0, 0) + + local formatString = "YYYY-MM-DD hh:mm:ss a" + + local expected + if GetFFlagUseDateTimeType() then + expected = { + ["en-us"] = { + "2017-09-13 12:00:00 am", + "2017-09-13 01:00:00 am", + "2017-09-13 02:00:00 am", + "2017-09-13 03:00:00 am", + "2017-09-13 04:00:00 am", + "2017-09-13 05:00:00 am", + "2017-09-13 06:00:00 am", + "2017-09-13 07:00:00 am", + "2017-09-13 08:00:00 am", + "2017-09-13 09:00:00 am", + "2017-09-13 10:00:00 am", + "2017-09-13 11:00:00 am", + "2017-09-13 12:00:00 pm", + "2017-09-13 01:00:00 pm", + "2017-09-13 02:00:00 pm", + "2017-09-13 03:00:00 pm", + "2017-09-13 04:00:00 pm", + "2017-09-13 05:00:00 pm", + "2017-09-13 06:00:00 pm", + "2017-09-13 07:00:00 pm", + "2017-09-13 08:00:00 pm", + "2017-09-13 09:00:00 pm", + "2017-09-13 10:00:00 pm", + "2017-09-13 11:00:00 pm", + "2017-09-14 12:00:00 am", + "2017-09-14 01:00:00 am", + }, + ["zh-cn"] = { + "2017-09-13 12:00:00 凌晨", + "2017-09-13 01:00:00 凌晨", + "2017-09-13 02:00:00 凌晨", + "2017-09-13 03:00:00 凌晨", + "2017-09-13 04:00:00 凌晨", + "2017-09-13 05:00:00 凌晨", + "2017-09-13 06:00:00 早上", + "2017-09-13 07:00:00 早上", + "2017-09-13 08:00:00 早上", + "2017-09-13 09:00:00 上午", + "2017-09-13 10:00:00 上午", + "2017-09-13 11:00:00 上午", + "2017-09-13 12:00:00 中午", + "2017-09-13 01:00:00 下午", + "2017-09-13 02:00:00 下午", + "2017-09-13 03:00:00 下午", + "2017-09-13 04:00:00 下午", + "2017-09-13 05:00:00 下午", + "2017-09-13 06:00:00 晚上", + "2017-09-13 07:00:00 晚上", + "2017-09-13 08:00:00 晚上", + "2017-09-13 09:00:00 晚上", + "2017-09-13 10:00:00 晚上", + "2017-09-13 11:00:00 晚上", + "2017-09-14 12:00:00 凌晨", + "2017-09-14 01:00:00 凌晨", + } + } + + for i = 1, #expected do + for j = 1, #expected[i] do + local result = date:Format(formatString, TimeZone.UTC, expected[i]) + expect(result).to.equal(expected[i][j]) + + -- Advance once hour + date = date.fromUnixTimestamp(date:GetUnixTimestamp() + 3600) + end + end + else + if FFlagChinaLicensingApp then + expected = { + "2017-09-13 12:00:00 上午", + "2017-09-13 01:00:00 上午", + "2017-09-13 02:00:00 上午", + "2017-09-13 03:00:00 上午", + "2017-09-13 04:00:00 上午", + "2017-09-13 05:00:00 上午", + "2017-09-13 06:00:00 上午", + "2017-09-13 07:00:00 上午", + "2017-09-13 08:00:00 上午", + "2017-09-13 09:00:00 上午", + "2017-09-13 10:00:00 上午", + "2017-09-13 11:00:00 上午", + "2017-09-13 12:00:00 下午", + "2017-09-13 01:00:00 下午", + "2017-09-13 02:00:00 下午", + "2017-09-13 03:00:00 下午", + "2017-09-13 04:00:00 下午", + "2017-09-13 05:00:00 下午", + "2017-09-13 06:00:00 下午", + "2017-09-13 07:00:00 下午", + "2017-09-13 08:00:00 下午", + "2017-09-13 09:00:00 下午", + "2017-09-13 10:00:00 下午", + "2017-09-13 11:00:00 下午", + "2017-09-14 12:00:00 上午", + "2017-09-14 01:00:00 上午", + } + else + expected = { + "2017-09-13 12:00:00 am", + "2017-09-13 01:00:00 am", + "2017-09-13 02:00:00 am", + "2017-09-13 03:00:00 am", + "2017-09-13 04:00:00 am", + "2017-09-13 05:00:00 am", + "2017-09-13 06:00:00 am", + "2017-09-13 07:00:00 am", + "2017-09-13 08:00:00 am", + "2017-09-13 09:00:00 am", + "2017-09-13 10:00:00 am", + "2017-09-13 11:00:00 am", + "2017-09-13 12:00:00 pm", + "2017-09-13 01:00:00 pm", + "2017-09-13 02:00:00 pm", + "2017-09-13 03:00:00 pm", + "2017-09-13 04:00:00 pm", + "2017-09-13 05:00:00 pm", + "2017-09-13 06:00:00 pm", + "2017-09-13 07:00:00 pm", + "2017-09-13 08:00:00 pm", + "2017-09-13 09:00:00 pm", + "2017-09-13 10:00:00 pm", + "2017-09-13 11:00:00 pm", + "2017-09-14 12:00:00 am", + "2017-09-14 01:00:00 am", + } + end + + for i = 1, #expected do + local result = date:Format(formatString, TimeZone.UTC) + expect(result).to.equal(expected[i]) + + -- Advance once hour + date = date.fromUnixTimestamp(date:GetUnixTimestamp() + 3600) + end + end + end) + + describe("LongRelativeTime", function() + if GetFFlagUseDateTimeType() then + it("SHOULD handle UTC time correctly with different locales", function() + local date = LuaDateTime.new(2015, 4, 20, 13, 0, 0) + for _, localeId in ipairs(localeIds) do + local longRelativeTime = date:GetLongRelativeTime(date, TimeZone.UTC, localeId) + expect(longRelativeTime).to.equal(date.dateTime:FormatUniversalTime("lll", localeId)) + end + end) + + it("SHOULD handle Local time correctly with different locales", function() + local date = LuaDateTime.fromUnixTimestamp(DateTime.fromLocalTime(2015, 4, 20, 13, 0, 0).UnixTimestamp) + for _, localeId in ipairs(localeIds) do + local longRelativeTime = date:GetLongRelativeTime(date, TimeZone.Current, localeId) + expect(longRelativeTime).to.equal(date.dateTime:FormatLocalTime("lll", localeId)) + end + end) + else + it("SHOULD handle same day case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2015, 4, 20, 13, 0, 0) + + if FFlagChinaLicensingApp then + expect(date:GetLongRelativeTime(now, TimeZone.UTC)).to.equal("13:00 下午") + else + expect(date:GetLongRelativeTime(now, TimeZone.UTC)).to.equal("1:00 PM") + end + end) + + it("SHOULD handle same week case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2015, 4, 19, 13, 0, 0) + if FFlagChinaLicensingApp then + expect(date:GetLongRelativeTime(now, TimeZone.UTC)).to.equal("4月19日 | 13:00 下午") + else + expect(date:GetLongRelativeTime(now, TimeZone.UTC)).to.equal("Sun | 1:00 PM") + end + end) + + it("SHOULD handle same year case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2015, 1, 20, 13, 0, 0) + if FFlagChinaLicensingApp then + expect(date:GetLongRelativeTime(now, TimeZone.UTC)).to.equal("1月20日 | 13:00 下午") + else + expect(date:GetLongRelativeTime(now, TimeZone.UTC)).to.equal("Jan 20 | 1:00 PM") + end + end) + + it("SHOULD handle different year case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2010, 1, 20, 13, 0, 0) + if FFlagChinaLicensingApp then + expect(date:GetLongRelativeTime(now, TimeZone.UTC)).to.equal("2010年1月20日 | 13:00 下午") + else + expect(date:GetLongRelativeTime(now, TimeZone.UTC)).to.equal("Jan 20, 2010 | 1:00 PM") + end + end) + end + end) + + describe("ShortRelativeTime", function() + if GetFFlagUseDateTimeType() then + it("SHOULD handle UTC time correctly with different locales", function() + local date = LuaDateTime.new(2015, 4, 20, 13, 0, 0) + for _, localeId in ipairs(localeIds) do + local shortRelativeTime = date:GetShortRelativeTime(date, TimeZone.UTC, localeId) + expect(shortRelativeTime).to.equal(date.dateTime:FormatUniversalTime("ll", localeId)) + end + end) + + it("SHOULD handle Local time correctly with different locales", function() + local date = LuaDateTime.fromUnixTimestamp(DateTime.fromLocalTime(2015, 4, 20, 13, 0, 0).UnixTimestamp) + for _, localeId in ipairs(localeIds) do + local shortRelativeTime = date:GetShortRelativeTime(date, TimeZone.Current, localeId) + expect(shortRelativeTime).to.equal(date.dateTime:FormatLocalTime("ll", localeId)) + end + end) + else + it("SHOULD handle same day case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2015, 4, 20, 13, 0, 0) + if FFlagChinaLicensingApp then + expect(date:GetShortRelativeTime(now, TimeZone.UTC)).to.equal("13:00 下午") + else + expect(date:GetShortRelativeTime(now, TimeZone.UTC)).to.equal("1:00 PM") + end + end) + + it("SHOULD handle same week case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2015, 4, 19, 13, 0, 0) + if FFlagChinaLicensingApp then + expect(date:GetShortRelativeTime(now, TimeZone.UTC)).to.equal("4月19日") + else + expect(date:GetShortRelativeTime(now, TimeZone.UTC)).to.equal("Sun") + end + end) + + it("SHOULD handle same year case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2015, 1, 20, 13, 0, 0) + if FFlagChinaLicensingApp then + expect(date:GetShortRelativeTime(now, TimeZone.UTC)).to.equal("1月20日") + else + expect(date:GetShortRelativeTime(now, TimeZone.UTC)).to.equal("Jan 20") + end + end) + + it("SHOULD handle different year case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2010, 1, 20, 13, 0, 0) + if FFlagChinaLicensingApp then + expect(date:GetShortRelativeTime(now, TimeZone.UTC)).to.equal("2010年1月20日") + else + expect(date:GetShortRelativeTime(now, TimeZone.UTC)).to.equal("Jan 20, 2010") + end + end) + end + end) + end) + + if not GetFFlagUseDateTimeType() then + describe("Comparisons", function() + describe("IsSame", function() + it("should equate dates with different granularity", function() + local value = LuaDateTime.new(2003, 6, 11, 15, 8, 9) + local same = LuaDateTime.new(2003, 6, 11, 15, 8, 9) + + expect(value:IsSame(value)).to.equal(true) + expect(value:IsSame(same)).to.equal(true) + + local units = {TimeUnit.Years, TimeUnit.Months, TimeUnit.Days, TimeUnit.Hours, TimeUnit.Minutes} + for _, unit in ipairs(units) do + expect(value:IsSame(same, unit)).to.equal(true) + end + + local sameMinute = LuaDateTime.new(2003, 6, 11, 15, 8, 10) + + expect(value:IsSame(sameMinute)).to.equal(false) + expect(value:IsSame(sameMinute, TimeUnit.Minutes)).to.equal(true) + expect(value:IsSame(sameMinute, TimeUnit.Years)).to.equal(true) + + local sameHour = LuaDateTime.new(2003, 6, 11, 15, 9, 0) + + expect(value:IsSame(sameHour)).to.equal(false) + expect(value:IsSame(sameHour, TimeUnit.Hours)).to.equal(true) + expect(value:IsSame(sameHour, TimeUnit.Years)).to.equal(true) + + local sameDay = LuaDateTime.new(2003, 6, 11, 14, 8, 9) + + expect(value:IsSame(sameDay)).to.equal(false) + expect(value:IsSame(sameDay, TimeUnit.Days)).to.equal(true) + expect(value:IsSame(sameDay, TimeUnit.Years)).to.equal(true) + + local sameMonth = LuaDateTime.new(2003, 6, 12, 15, 8, 9) + + expect(value:IsSame(sameMonth)).to.equal(false) + expect(value:IsSame(sameMonth, TimeUnit.Months)).to.equal(true) + expect(value:IsSame(sameMonth, TimeUnit.Years)).to.equal(true) + + local sameYear = LuaDateTime.new(2003, 7, 12, 15, 8, 9) + + expect(value:IsSame(sameYear)).to.equal(false) + expect(value:IsSame(sameYear, TimeUnit.Years)).to.equal(true) + + local diffYear = LuaDateTime.new(2004, 6, 11, 15, 8, 9) + + expect(value:IsSame(diffYear)).to.equal(false) + expect(value:IsSame(diffYear, TimeUnit.Years)).to.equal(false) + end) + + it("should equate values using week boundaries", function() + local sunday = LuaDateTime.new(2017, 5, 7) + local saturday = LuaDateTime.new(2017, 5, 13) + local monday = LuaDateTime.new(2017, 5, 8) + local tuesday = LuaDateTime.new(2017, 5, 9) + + -- TODO: Specify locale when that lands; default may break tests + local function sameWeek(a, b) + return a:IsSame(b, TimeUnit.Weeks, TimeZone.UTC) + end + + expect(sameWeek(monday, monday)).to.equal(true) + + expect(sameWeek(sunday, monday)).to.equal(true) + expect(sameWeek(tuesday, monday)).to.equal(true) + expect(sameWeek(saturday, monday)).to.equal(true) + + local nextSunday = LuaDateTime.new(2017, 5, 14) + + expect(sameWeek(nextSunday, monday)).to.equal(false) + end) + end) + end) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Flags/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Flags/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Flags/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Flags/isNewFriendsEndpointsEnabled.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Flags/isNewFriendsEndpointsEnabled.lua new file mode 100644 index 0000000..70fef7e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Flags/isNewFriendsEndpointsEnabled.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") +local ThrottleUserId = require(CorePackages.AppTempCommon.LuaApp.Utils.ThrottleUserId) + +return function() + return ThrottleUserId( + game:DefineFastInt("LuaChatUseNewFriendsEndpointsV2", 0), + Players.LocalPlayer.UserId + ) +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Models/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Models/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Models/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Models/PlaceInfoModel.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Models/PlaceInfoModel.lua new file mode 100644 index 0000000..216a257 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Models/PlaceInfoModel.lua @@ -0,0 +1,45 @@ +local CorePackages = game:GetService("CorePackages") + +local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId) + +local FFlagLuaAppConvertUniverseIdToString = settings():GetFFlag("LuaAppConvertUniverseIdToStringV364") + +local PlaceInfoModel = {} + +function PlaceInfoModel.new() + local self = {} + + return self +end + +function PlaceInfoModel.mock() + local self = PlaceInfoModel.new() + + self.builder = "builder" + self.builderId = MockId() + self.description = "description" + self.imageToken = MockId() + self.isPlayable = true + self.name = "name" + self.placeId = MockId() + self.price = 0 + self.reasonProhibited = nil + self.universeId = MockId() + self.universeRootPlaceId = MockId() + self.url = "url" + + return self +end + +function PlaceInfoModel.fromWeb(data) + local self = data or {} + self.placeId = tostring(self.placeId) + + if FFlagLuaAppConvertUniverseIdToString then + self.universeId = tostring(self.universeId) + end + + return self +end + +return PlaceInfoModel \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/FriendCount.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/FriendCount.lua new file mode 100644 index 0000000..bab1179 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/FriendCount.lua @@ -0,0 +1,12 @@ +local CorePackages = game:GetService("CorePackages") +local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount) + +return function(state, action) + state = state or 0 + + if action.type == SetFriendCount.name then + state = action.count + end + + return state +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/FriendCount.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/FriendCount.spec.lua new file mode 100644 index 0000000..d7cc7ec --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/FriendCount.spec.lua @@ -0,0 +1,18 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local FriendCount = require(CorePackages.AppTempCommon.LuaChat.Reducers.FriendCount) + local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount) + + it("should be zero by default", function() + local state = FriendCount(nil, {}) + + expect(state).to.equal(0) + end) + + it("should respond to SetFriendCount", function() + local state = FriendCount(nil, {}) + state = FriendCount(state, SetFriendCount(520)) + + expect(state).to.equal(520) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.lua new file mode 100644 index 0000000..4a9ab03 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.lua @@ -0,0 +1,22 @@ +local CorePackages = game:GetService("CorePackages") + +local Common = CorePackages.AppTempCommon.Common +local LuaChat = CorePackages.AppTempCommon.LuaChat + +local ReceivedMultiplePlaceInfos = require(LuaChat.Actions.ReceivedMultiplePlaceInfos) + +local Immutable = require(Common.Immutable) + +return function(state, action) + state = state or {} + if action.type == ReceivedMultiplePlaceInfos.name then + + local newInfos = {} + for _, placeInfo in ipairs(action.placeInfos) do + newInfos[placeInfo.placeId] = placeInfo + end + + state = Immutable.JoinDictionaries(state, newInfos) + end + return state +end diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.spec.lua new file mode 100644 index 0000000..f23ff9e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.spec.lua @@ -0,0 +1,36 @@ +return function() + local CorePackages = game:GetService("CorePackages") + + local LuaApp = CorePackages.AppTempCommon.LuaApp + local LuaChat = CorePackages.AppTempCommon.LuaChat + + local MockId = require(LuaApp.MockId) + local ReceivedMultiplePlaceInfos = require(LuaChat.Actions.ReceivedMultiplePlaceInfos) + + local PlaceInfosReducer = require(script.Parent.PlaceInfos) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = PlaceInfosReducer(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("ReceivedMultiplePlaceInfos", function() + it("should add place info to the store", function() + local state = PlaceInfosReducer(nil, {}) + + local placeId = MockId() + local returnedPlaceInfo = ReceivedMultiplePlaceInfos({ + { + placeId = placeId, + imageToken = "image-token", + }, + }) + + state = PlaceInfosReducer(state, returnedPlaceInfo) + + expect(state[placeId]).to.equal(returnedPlaceInfo.placeInfos[1]) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/TimeUnit.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/TimeUnit.lua new file mode 100644 index 0000000..1ce9701 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/TimeUnit.lua @@ -0,0 +1,17 @@ +local TimeUnit = setmetatable({}, { + __index = function(self, key) + error(("Invalid TimeUnit \"%s\""):format(tostring(key)), 2) + end +}) + +TimeUnit.Seconds = "Seconds" +TimeUnit.Minutes = "Minutes" +TimeUnit.Hours = "Hours" +TimeUnit.Days = "Days" +TimeUnit.Months = "Months" +TimeUnit.Years = "Years" + +-- Locale-specific +TimeUnit.Weeks = "Weeks" + +return TimeUnit \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/TimeZone.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/TimeZone.lua new file mode 100644 index 0000000..8c1e0c5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/TimeZone.lua @@ -0,0 +1,10 @@ +local TimeZone = setmetatable({}, { + __index = function(self, key) + error(("Invalid TimeZone \"%s\""):format(tostring(key)), 2) + end +}) + +TimeZone.UTC = -2 +TimeZone.Current = -1 + +return TimeZone \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/getFriendsActiveGamesPlaceIdsFromUsersPresence.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/getFriendsActiveGamesPlaceIdsFromUsersPresence.lua new file mode 100644 index 0000000..a42715f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/getFriendsActiveGamesPlaceIdsFromUsersPresence.lua @@ -0,0 +1,19 @@ +local CorePackages = game:GetService("CorePackages") + +local User = require(CorePackages.AppTempCommon.LuaApp.Models.User) +local WebPresenceMap = require(CorePackages.AppTempCommon.LuaApp.Enum.WebPresenceMap) +local convertUniverseIdToString = require(CorePackages.AppTempCommon.LuaApp.Flags.ConvertUniverseIdToString) + +return function(friendsPresence, store) + local placeIds = {} + + for _, presenceModel in pairs(friendsPresence) do + local universeId = convertUniverseIdToString(presenceModel.universeId) + if WebPresenceMap[presenceModel.userPresenceType] == User.PresenceType.IN_GAME + and (not store:getState().UniversePlaceInfos[universeId]) then + table.insert(placeIds, presenceModel.placeId) + end + end + + return placeIds +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/receiveUsersPresence.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/receiveUsersPresence.lua new file mode 100644 index 0000000..0f75e18 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/receiveUsersPresence.lua @@ -0,0 +1,32 @@ +local CorePackages = game:GetService("CorePackages") + +local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence) +local WebPresenceMap = require(CorePackages.AppTempCommon.LuaApp.Enum.WebPresenceMap) + +local FFlagLuaAppConvertUniverseIdToString = settings():GetFFlag("LuaAppConvertUniverseIdToStringV364") + +return function(friendsPresence, store) + for _, presenceModel in pairs(friendsPresence) do + local userInStore = store:getState().Users[tostring(presenceModel.userId)] + local previousUniverseId = userInStore and userInStore.universeId or nil + + local universeId + if FFlagLuaAppConvertUniverseIdToString then + universeId = presenceModel.universeId and tostring(presenceModel.universeId) or nil + else + universeId = presenceModel.universeId + end + + store:dispatch(ReceivedUserPresence( + tostring(presenceModel.userId), + WebPresenceMap[presenceModel.userPresenceType], + presenceModel.lastLocation, + presenceModel.placeId and tostring(presenceModel.placeId) or nil, + presenceModel.rootPlaceId and tostring(presenceModel.rootPlaceId) or nil, + presenceModel.gameId and tostring(presenceModel.gameId) or nil, + presenceModel.lastOnline and tostring(presenceModel.lastOnline) or nil, + universeId, + previousUniverseId + )) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/.robloxrc b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/.robloxrc new file mode 100644 index 0000000..e721482 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/.robloxrc @@ -0,0 +1,9 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalShadow": "fatal", + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/EventStream.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/EventStream.lua new file mode 100644 index 0000000..c630b9d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/EventStream.lua @@ -0,0 +1,78 @@ +local AnalyticsService = game:GetService("RbxAnalyticsService") +local RunService = game:GetService("RunService") +local UserInputService = game:GetService("UserInputService") + +local SETTINGS_HUB_INVITE_RELEASE_STREAM_TIME = 10 + +local function getPlatformTarget() + local platformTarget = "unknownLua" + local platformEnum = Enum.Platform.None + + -- the call to GetPlatform is wrapped in a pcall() because the Testing Service + -- executes the scripts in the wrong authorization level + pcall(function() + platformEnum = UserInputService:GetPlatform() + end) + + -- bucket the platform based on consumer platform + local isDesktopClient = (platformEnum == Enum.Platform.Windows) or (platformEnum == Enum.Platform.OSX) + + local isMobileClient = (platformEnum == Enum.Platform.IOS) or (platformEnum == Enum.Platform.Android) + isMobileClient = isMobileClient or (platformEnum == Enum.Platform.UWP) + + local isConsole = (platformEnum == Enum.Platform.XBox360) or (platformEnum == Enum.Platform.XBoxOne) + isConsole = isConsole or (platformEnum == Enum.Platform.PS3) or (platformEnum == Enum.Platform.PS4) + isConsole = isConsole or (platformEnum == Enum.Platform.WiiU) + + -- assign a target based on the form factor + if isDesktopClient then + platformTarget = "client" + elseif isMobileClient then + platformTarget = "mobile" + elseif isConsole then + platformTarget = "console" + else + -- if we don't have a name for the form factor, report it here so that we can eventually track it down + platformTarget = platformTarget .. tostring(platformEnum) + end + + return platformTarget +end + +local EventStream = {} +EventStream.__index = EventStream + +function EventStream.new(overridePlatformTarget, overrideAnalyticsImpl) + local self = {} + setmetatable(self, EventStream) + + self._analyticsImpl = overrideAnalyticsImpl or AnalyticsService + self._platformTarget = overridePlatformTarget or getPlatformTarget() + + return self +end + +function EventStream:setRBXEventStream(eventContext, eventName, additionalArgs) + additionalArgs = additionalArgs or {} + -- this function sends reports to the server in batches, not real-time + self._analyticsImpl:SetRBXEventStream(self._platformTarget, eventContext, eventName, additionalArgs) + + if not self.timerSteppedConnection then + local lastGameTime = time() + self.timerSteppedConnection = RunService.Stepped:Connect(function(gameTime) + if gameTime - lastGameTime > SETTINGS_HUB_INVITE_RELEASE_STREAM_TIME then + self:releaseRBXEventStream() + end + end) + end +end + +function EventStream:releaseRBXEventStream() + self._analyticsImpl:ReleaseRBXEventStream(self._platformTarget) + if self.timerSteppedConnection then + self.timerSteppedConnection:Disconnect() + self.timerSteppedConnection = nil + end +end + +return EventStream diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/httpRequest.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/httpRequest.lua new file mode 100644 index 0000000..7a0751a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/httpRequest.lua @@ -0,0 +1,97 @@ +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local LuaApp = CorePackages.AppTempCommon.LuaApp + +local Promise = require(LuaApp.Promise) + +local DEFAULT_THROTTLING_PRIORITY = Enum.ThrottlingPriority.Extreme +local DEFAULT_POST_ASYNC_CONTENT_TYPE = Enum.HttpContentType.ApplicationJson + +-- httpRequest : (table, optional) an object that implements the same http functions as the data model +return function(httpImpl) + + local function doHttpPost(url, options) + local jsonPayload + assert(options.postBody, "Expected a postBody to be specified with this request") + if type(options.postBody) == "table" then + jsonPayload = HttpService:JSONEncode(options.postBody) + elseif type(options.postBody) == "string" then + jsonPayload = options.postBody + else + error("Expected postBody to be a string or table") + end + + if not options.contentType then + options.contentType = DEFAULT_POST_ASYNC_CONTENT_TYPE + end + + if not options.throttlingPriority then + options.throttlingPriority = DEFAULT_THROTTLING_PRIORITY + end + + return function() + return httpImpl:PostAsyncFullUrl( + url, + jsonPayload, + options.throttlingPriority, + options.contentType + ) + end + end + + local function doHttpGet(url) + return function() + return httpImpl:GetAsyncFullUrl(url, DEFAULT_THROTTLING_PRIORITY) + end + end + + -- return the request function + -- url : (string) + -- requestMethod : (string) "GET", "POST" + -- args : (table, optional) + -- options.throttlingPriority : (Enum.ThrottlingPriority, optional) + -- options.contentType : (Enum.HttpContentType, optional) + -- options.postBody : (string, optional ("POST" only)) + -- RETURNS : (promise) + return function(url, requestMethod, options) + assert(type(url) == "string", "Expected url to be a string") + assert(type(requestMethod) == "string", "Expected requestMethod to be a string") + assert(not options or type(options) == "table", "Expected options to be a table") + requestMethod = string.upper(requestMethod) + + local httpFunction + if requestMethod == "POST" then + httpFunction = doHttpPost(url, options) + elseif requestMethod == "GET" then + httpFunction = doHttpGet(url) + else + error(string.format("Unsupported requestMethod : %s", requestMethod or "nil")) + end + + return Promise.new(function(resolve, reject) + if httpFunction then + spawn(function() + local success, response = pcall(httpFunction) + + if success then + local jsonSuccess, decodedJson = pcall(function() + return HttpService:JSONDecode(response) + end) + if jsonSuccess then + resolve({ + responseBody = decodedJson, + }) + else + reject(decodedJson) + end + else + reject(response) + end + end) + else + reject() + end + end) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/httpRequest.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/httpRequest.spec.lua new file mode 100644 index 0000000..fed3d32 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/httpRequest.spec.lua @@ -0,0 +1,61 @@ +return function() + local httpRequest = require(script.Parent.httpRequest) + + local function createTestRequestFunc(testResponse) + local requestService = {} + function requestService:GetAsyncFullUrl() + return testResponse + end + function requestService:PostAsyncFullUrl() + return testResponse + end + + return httpRequest(requestService) + end + + it("should return a function", function() + expect(httpRequest()).to.be.ok() + expect(type(httpRequest())).to.equal("function") + end) + + it("should validate its inputs", function() + local testRequest = createTestRequestFunc() + local function testParams(url, requestMethod, args) + return function() + testRequest(url, requestMethod, args) + end + end + + local validUrl = "friends.roblox.com" + local validMethod = "GET" + local validArgs = {} + + -- url checks + expect(testParams(nil, validMethod, validArgs)).to.throw() + expect(testParams(123, validMethod, validArgs)).to.throw() + expect(testParams({}, validMethod, validArgs)).to.throw() + expect(testParams(true, validMethod, validArgs)).to.throw() + expect(testParams(function() end, validMethod, validArgs)).to.throw() + + -- request method checks + expect(testParams(validUrl, nil, validArgs)).to.throw() + expect(testParams(validUrl, 123, validArgs)).to.throw() + expect(testParams(validUrl, {}, validArgs)).to.throw() + expect(testParams(validUrl, true, validArgs)).to.throw() + expect(testParams(validUrl, function() end, validArgs)).to.throw() + + -- args checks + expect(testParams(validUrl, validMethod, 123)).to.throw() + expect(testParams(validUrl, validMethod, "Test")).to.throw() + expect(testParams(validUrl, validMethod, true)).to.throw() + expect(testParams(validUrl, validMethod, function() end)).to.throw() + end) + + it("should throw an error if the requestMethod isn't supported", function() + local testRequest = createTestRequestFunc("foo") + + expect(function() + testRequest("testUrl", "GIVEANDTAKE") + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.lua new file mode 100644 index 0000000..3a0d69f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.lua @@ -0,0 +1,16 @@ +return function(targetString, blacklistedCharacter) + local charactersArray = {} + local indexArray = {} + for index, byte in utf8.codes(targetString) do + local graphemeCharacter = utf8.char(byte) + table.insert(charactersArray, 1, graphemeCharacter) + table.insert(indexArray, 1, index) + end + for index, graphemeCharacter in ipairs(charactersArray) do + if graphemeCharacter ~= blacklistedCharacter then + return targetString:sub(1, indexArray[index]) + end + end + + return "" +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.spec.lua b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.spec.lua new file mode 100644 index 0000000..a08d217 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.spec.lua @@ -0,0 +1,71 @@ +return function() + local trimCharacterFromEndString = require(script.Parent.trimCharacterFromEndString) + + describe("single byte characters", function() + it("should not trim a string if it does not end with passed character", function() + local passedString = "testing" + local passedCharacter = "/" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(passedString) + end) + + it("should trim a string if it ends with a single instance of the passed character", function() + local passedString = "testing/" + local passedCharacter = "/" + local expectedString = "testing" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + + it("should trim a string if it ends with multiple instances of the passed character", function() + local passedString = "testing///" + local passedCharacter = "/" + local expectedString = "testing" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + + it("should do nothing if the passed character is empty", function() + local passedString = "hunter2" + local passedCharacter = "" + local expectedString = "hunter2" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + end) + + describe("multiple byte characters", function() + it("should not trim a string if it does not end with passed character", function() + local passedString = "testing" + local passedCharacter = "🐶" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(passedString) + end) + + it("should trim a string if it ends with a single instance of the passed character", function() + local passedString = "testing🐶" + local passedCharacter = "🐶" + local expectedString = "testing" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + + it("should trim a string if it ends with multiple instances of the passed character", function() + local passedString = "testing🐶🐶🐶" + local passedCharacter = "🐶" + local expectedString = "testing" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + end) + + describe("a string with all blacklisted characters", function() + it("should return a empty string", function() + local passedString = "pppppppppppp" + local passedCharacter = "p" + local expectedString = "" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/ArgCheck.lua b/Client2020/ExtraContent/LuaPackages/ArgCheck.lua new file mode 100644 index 0000000..99367c4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/ArgCheck.lua @@ -0,0 +1,7 @@ +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.ArgCheckImpl) + +return require(CorePackages.ArgCheckImpl) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/ArgCheckImpl/.robloxrc b/Client2020/ExtraContent/LuaPackages/ArgCheckImpl/.robloxrc new file mode 100644 index 0000000..33c7a1c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/ArgCheckImpl/.robloxrc @@ -0,0 +1,9 @@ +{ + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "ImplicitReturn": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/ArgCheckImpl/ArgCheck.lua b/Client2020/ExtraContent/LuaPackages/ArgCheckImpl/ArgCheck.lua new file mode 100644 index 0000000..cd6d931 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/ArgCheckImpl/ArgCheck.lua @@ -0,0 +1,142 @@ +local function IsRunningInStudio() + return game:GetService("RunService"):IsStudio() +end + +local function assert_(condition, message) + if IsRunningInStudio() or _G.__TESTEZ_RUNNING_TEST__ then + assert(condition, message) + end +end + +local ArgCheck = {} + +function ArgCheck.isNonNegativeNumber(value, name) + -- Temporarily disabled outside of studio/tests. See MOBLUAPP-1161. + assert_(typeof(value) == "number" and value >= 0, string.format("expects %s to be a non-negative number!", name)) + + return value +end + +function ArgCheck.isType(value, expectedType, name) + assert_(typeof(value) == expectedType, + string.format("expects %s to be a %s! it was: %s", name, expectedType, typeof(value))) + + return value +end + +function ArgCheck.isInTypes(value, expectedTypes, name) + for _, expectedType in ipairs(expectedTypes) do + if typeof(value) == expectedType then + return value + end + end + + assert_(false, string.format("expects %s to be one of expectedTypes! it was: %s", name, typeof(value))) + + return value +end + +function ArgCheck.isTypeOrNil(value, expectedType, name) + assert_(value == nil or typeof(value) == expectedType, + string.format("expects %s to be a %s! it was: %s", name, expectedType, typeof(value))) + + return value +end + +function ArgCheck.isNotNil(value, name) + assert_(value ~= nil, string.format("expects %s to be not nil!", name)) + + return value +end + +function ArgCheck.isNonEmptyString(value, name) + assert_(typeof(value) == "string" and value ~= "" , + string.format("expects %s to be a non-empty string!", name)) + + return value +end + +function ArgCheck.isEqual(value, expectedValue, name) + assert_(value == expectedValue, string.format("expects %s to equal %s! it was: %s", name, tostring(expectedValue), tostring(value))) + + return value +end + +-- checks for a number or string representing an integer +function ArgCheck.representsInteger(value, name) + local numberValue = tonumber(value) + assert_(numberValue ~= nil , string.format("expects %s to represent a number!", name)) + assert_(numberValue % 1 == 0 , string.format("expects %s to represent an integer!", name)) + + return value +end + +--[[ + Checks if the value matches the given interface + iface is the interface description; it can be (in order of priority): + * a custom type name: checks against a type from dependencies (see below) + * an ArgCheck handler: + * "integer" => ArgCheck.representsInteger + * "nonEmptyString" => ArgCheck.isNonEmptyString + * a lua type (string): equivalent to ArgCheck.isType + * a list style table (only first item is considered): + checks for a list table with items matching the given interface + * a dict style table: checks for a table with keys matching the given interfaces + dependencies is a table of named interfaces that can be referenced in iface + Example: + local myTypes = { + Tree = { + value = "string", + leaves = {"string"}, + branches = {"Tree"}, + }, + } + ArgCheck.matchesInterface(someValue, "Tree", "myVal", myTypes) +]]-- +function ArgCheck.matchesInterface(value, iface, name, dependencies) + if IsRunningInStudio() or _G.__TESTEZ_RUNNING_TEST__ then + local checkFnList = { + integer = ArgCheck.representsInteger, + nonEmptyString = ArgCheck.isNonEmptyString, + } + if type(iface) == "string" then + if dependencies and dependencies[iface] then + ArgCheck.matchesInterface(value, dependencies[iface], name, dependencies) + else + local checkFn = checkFnList[iface] + if type(checkFn) == "function" then + checkFn(value, name) + else + ArgCheck.isType(value, iface, name) + end + end + else + -- assume iface describes a table (list or dict) + ArgCheck.isType(value, "table", name) + if iface[1] ~= nil then + for index, item in ipairs(value) do + ArgCheck.matchesInterface(item, iface[1], name .. "[" .. index .. "]", dependencies) + end + else + for key, desc in pairs(iface) do + if string.sub(key, 1, 1) ~= "_" then + local itemName = name .. "." .. key + local itemValue = value[key] + local isRequired = iface._required and iface._required[key] + if isRequired or itemValue ~= nil then + ArgCheck.matchesInterface(itemValue, desc, itemName, dependencies) + end + end + end + end + end + end + + return value +end + +function ArgCheck.assert(...) + assert_(...) +end + +return ArgCheck diff --git a/Client2020/ExtraContent/LuaPackages/ArgCheckImpl/ArgCheck.spec.lua b/Client2020/ExtraContent/LuaPackages/ArgCheckImpl/ArgCheck.spec.lua new file mode 100644 index 0000000..ead56a6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/ArgCheckImpl/ArgCheck.spec.lua @@ -0,0 +1,287 @@ +return function() + local ArgCheck = require(script.Parent.ArgCheck) + + describe("isNonNegativeNumber", function() + it("should assert if given non-number, or negative number", function() + expect(function() + ArgCheck.isNonNegativeNumber(nil, "") + end).to.throw() + expect(function() + ArgCheck.isNonNegativeNumber({}, "") + end).to.throw() + expect(function() + ArgCheck.isNonNegativeNumber("string", "") + end).to.throw() + expect(function() + ArgCheck.isNonNegativeNumber(-1, "") + end).to.throw() + end) + + it("should return the value if it is a non-negative number", function() + expect(ArgCheck.isNonNegativeNumber(0, "")).to.equal(0) + expect(ArgCheck.isNonNegativeNumber(1, "")).to.equal(1) + end) + end) + + describe("isType", function() + it("should assert if type is wrong", function() + expect(function() + ArgCheck.isType(nil, "number", "") + end).to.throw() + expect(function() + ArgCheck.isType("test", "number", "") + end).to.throw() + expect(function() + ArgCheck.isType(5, "string", "") + end).to.throw() + expect(function() + ArgCheck.isType(5, "table", "") + end).to.throw() + end) + + it("should return the value if the type is correct", function() + expect(ArgCheck.isType(0, "number", "")).to.equal(0) + expect(ArgCheck.isType("test", "string", "")).to.equal("test") + end) + end) + + describe("isInTypes", function() + it("should assert if type is not expected", function() + expect(function() + ArgCheck.isInTypes(nil, {"number", "string", "table"}, "") + end).to.throw() + expect(function() + ArgCheck.isInTypes("test", {"number", "table"}, "") + end).to.throw() + expect(function() + ArgCheck.isInTypes(5, {"string", "table"}, "") + end).to.throw() + expect(function() + ArgCheck.isInTypes({}, {"number", "string"}, "") + end).to.throw() + end) + + it("should return the value if the type is expected", function() + expect(ArgCheck.isInTypes(0, {"number", "string"}, "")).to.equal(0) + expect(ArgCheck.isInTypes("test", {"table", "string"}, "")).to.equal("test") + local testTable = {} + expect(ArgCheck.isInTypes(testTable, {"table", "string"}, "")).to.equal(testTable) + local testFunction = function() end + expect(ArgCheck.isInTypes(testFunction, {"function", "string"}, "")).to.equal(testFunction) + end) + end) + + describe("isTypeOrNil", function() + it("should assert if type is wrong", function() + expect(function() + ArgCheck.isTypeOrNil("test", "number", "") + end).to.throw() + expect(function() + ArgCheck.isTypeOrNil(5, "string", "") + end).to.throw() + expect(function() + ArgCheck.isTypeOrNil(5, "table", "") + end).to.throw() + end) + + it("should return the value if the type is correct", function() + expect(ArgCheck.isTypeOrNil(nil, "number", "")).to.equal(nil) + expect(ArgCheck.isTypeOrNil(0, "number", "")).to.equal(0) + expect(ArgCheck.isTypeOrNil("test", "string", "")).to.equal("test") + end) + end) + + describe("isNotNil", function() + it("should assert if type is nil", function() + expect(function() + ArgCheck.isNotNil(nil, "") + end).to.throw() + end) + + it("should return the value if it's not nil", function() + expect(ArgCheck.isNotNil(0, "")).to.equal(0) + expect(ArgCheck.isNotNil("test", "")).to.equal("test") + local testTable = {} + expect(ArgCheck.isNotNil(testTable, "")).to.equal(testTable) + local testFunction = function() end + expect(ArgCheck.isNotNil(testFunction, "")).to.equal(testFunction) + end) + end) + + describe("isEqual", function() + it("should assert if not equal", function() + expect(function() + ArgCheck.isEqual(0, nil, "") + end).to.throw() + expect(function() + ArgCheck.isEqual(2, 1, "") + end).to.throw() + expect(function() + ArgCheck.isEqual("", "test", "") + end).to.throw() + expect(function() + ArgCheck.isEqual({}, {}, "") + end).to.throw() + expect(function() + ArgCheck.isEqual(function() end, function() end, "") + end).to.throw() + end) + + it("should return the value if value is equal to expected value", function() + expect(ArgCheck.isEqual(nil, nil, "")).to.equal(nil) + expect(ArgCheck.isEqual(0, 0, "")).to.equal(0) + expect(ArgCheck.isEqual(true, true, "")).to.equal(true) + expect(ArgCheck.isEqual("test", "test", "")).to.equal("test") + local testTable = {} + expect(ArgCheck.isEqual(testTable, testTable, "")).to.equal(testTable) + local testFunction = function() end + expect(ArgCheck.isEqual(testFunction, testFunction, "")).to.equal(testFunction) + end) + end) + + describe("representsInteger", function() + it("should fail if not a number", function() + expect(function() + ArgCheck.representsInteger(nil, "") + end).to.throw() + expect(function() + ArgCheck.representsInteger({}, "") + end).to.throw() + expect(function() + ArgCheck.representsInteger(function()end, "") + end).to.throw() + expect(function() + ArgCheck.representsInteger(true, "") + end).to.throw() + expect(function() + ArgCheck.representsInteger("NaN", "") + end).to.throw() + expect(function() + ArgCheck.representsInteger("1test", "") + end).to.throw() + end) + + it("should fail if not an integer", function() + expect(function() + ArgCheck.representsInteger(1.5, "") + end).to.throw() + expect(function() + ArgCheck.representsInteger("1.5", "") + end).to.throw() + expect(function() + ArgCheck.representsInteger("1e-1", "") + end).to.throw() + end) + + it("should return the same value on success", function() + expect(ArgCheck.representsInteger(5, "")).to.equal(5) + expect(ArgCheck.representsInteger("-5", "")).to.equal("-5") + expect(ArgCheck.representsInteger("1e1", "")).to.equal("1e1") + expect(ArgCheck.representsInteger("0xa", "")).to.equal("0xa") + end) + end) + + describe("matchesInterface", function() + it("should match a simple interface", function() + local interface = { + num = "number", + str = "string", + bool = "boolean", + func = "function", + tab = "table", + list = {"number"}, + -- only num is required, rest is optional + _required = { + num = true, + } + } + local obj1 = { + num = 5, + str = "5", + bool = true, + func = function()end, + tab = {}, + list = {1, 2, 3} + } + local obj2 = { + num = "5", + } + local obj3 = { + num = 5, + list = {"NaN"}, + } + local obj4 = { + str = "5", + } + expect(function() + ArgCheck.matchesInterface(obj1, interface, "") + end).to.never.throw() + expect(function() + ArgCheck.matchesInterface(obj2, interface, "") + end).to.throw() + expect(function() + ArgCheck.matchesInterface(obj3, interface, "") + end).to.throw() + expect(function() + ArgCheck.matchesInterface(obj4, interface, "") + end).to.throw() + end) + + it("should match ArgCheck functions", function() + expect(function() + ArgCheck.matchesInterface("5", "nonEmptyString", "") + end).to.never.throw() + expect(function() + ArgCheck.matchesInterface(5, "nonEmptyString", "") + end).to.throw() + expect(function() + ArgCheck.matchesInterface("", "nonEmptyString", "") + end).to.throw() + expect(function() + ArgCheck.matchesInterface({str = "5"}, {str = "nonEmptyString"}, "") + end).to.never.throw() + end) + + it("should match dependent types", function() + local types = { + child = { + name = "string", + }, + parent = { + name = "string", + children = {"child"}, + } + } + local child1 = { + name = "child1", + } + local child2 = { + name = "child2", + } + local parent1 = { + name = "parent1", + children = {child1, child2}, + } + local parent2 = { + name = "parent2", + children = {"child1", "child2"}, + } + expect(function() + ArgCheck.matchesInterface(child1, types.child, "", types) + end).to.never.throw() + expect(function() + ArgCheck.matchesInterface(parent1, types.parent, "", types) + end).to.never.throw() + expect(function() + ArgCheck.matchesInterface(parent2, types.parent, "", types) + end).to.throw() + end) + + it("should return the same value on success", function() + expect(ArgCheck.matchesInterface(5, "number", "")).to.equal(5) + expect(ArgCheck.matchesInterface("5", "nonEmptyString", "")).to.equal("5") + local list = {1, 2, 3} + expect(ArgCheck.matchesInterface(list, {"number"}, "")).to.equal(list) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/ArgCheckImpl/init.lua b/Client2020/ExtraContent/LuaPackages/ArgCheckImpl/init.lua new file mode 100644 index 0000000..f949535 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/ArgCheckImpl/init.lua @@ -0,0 +1 @@ +return require(script.ArgCheck) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/AvatarExperienceDeps.lua b/Client2020/ExtraContent/LuaPackages/AvatarExperienceDeps.lua new file mode 100644 index 0000000..4a04356 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/AvatarExperienceDeps.lua @@ -0,0 +1,12 @@ +--[[ + Proxy package for dependencies for AvatarExperience. +]] + + +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.AvatarExperienceDeps) diff --git a/Client2020/ExtraContent/LuaPackages/CodeCoverage/.robloxrc b/Client2020/ExtraContent/LuaPackages/CodeCoverage/.robloxrc new file mode 100644 index 0000000..321fd28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/CodeCoverage/.robloxrc @@ -0,0 +1,12 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "ImplicitReturn": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/CodeCoverage/FileScanner.lua b/Client2020/ExtraContent/LuaPackages/CodeCoverage/FileScanner.lua new file mode 100644 index 0000000..0f473fd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/CodeCoverage/FileScanner.lua @@ -0,0 +1,17 @@ + +local CodeCoverage = script.Parent +local LineScanner = require(CodeCoverage.LineScanner) + +return function(fileLinesArray) + local scanner = LineScanner:new() + + local excludedLines = {} + local excludedIfNotHitLines = {} + for _, line in ipairs(fileLinesArray) do + local excluded, excludedIfNotHit = scanner:consume(line) + table.insert(excludedLines, excluded) + table.insert(excludedIfNotHitLines, excludedIfNotHit) + end + + return excludedLines, excludedIfNotHitLines +end diff --git a/Client2020/ExtraContent/LuaPackages/CodeCoverage/LcovReporter.lua b/Client2020/ExtraContent/LuaPackages/CodeCoverage/LcovReporter.lua new file mode 100644 index 0000000..ebfd902 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/CodeCoverage/LcovReporter.lua @@ -0,0 +1,32 @@ +local LcovReporter = {} +LcovReporter.__index = LcovReporter + + +function LcovReporter.generate(files, includeFilter) + local report = {} + + for _, file in ipairs(files) do + if includeFilter(file) then + table.insert(report, "TN:") + table.insert(report, "SF:" .. file.path) + local foundFirstHit = false + for lineNumber, line in ipairs(file.lines) do + if not line.ignored and not foundFirstHit then + foundFirstHit = true + end + + if foundFirstHit and not line.ignored then + table.insert(report, ("DA:%d,%d"):format(lineNumber, line.hits)) + end + end + table.insert(report, ("LH:%d"):format(file.hits)) + table.insert(report, ("LF:%d"):format(file.hits, file.misses)) + table.insert(report, "end_of_record") + end + end + + return table.concat(report, "\n") +end + + +return LcovReporter \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/CodeCoverage/LineScanner.lua b/Client2020/ExtraContent/LuaPackages/CodeCoverage/LineScanner.lua new file mode 100644 index 0000000..5255e61 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/CodeCoverage/LineScanner.lua @@ -0,0 +1,387 @@ +--[[ +The MIT License (MIT) + +Copyright (c) 2007 - 2018 Hisham Muhammad. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Original: https://github.com/keplerproject/luacov/6e6232d766f051d9668dab785582451bfd69ad17/master/src/luacov/linescanner.lua +]] + +local LineScanner = {} +LineScanner.__index = LineScanner + +function LineScanner:new() + return setmetatable( + { + first = true, + comment = false, + after_function = false, + enabled = true + }, + self + ) +end + +-- Raw version of string.gsub +local function replace(s, old, new) + old = old:gsub("%p", "%%%0") + new = new:gsub("%%", "%%%%") + return (s:gsub(old, new)) +end + +local fixups = { + {"=", " ?= ?"}, -- '=' may be surrounded by spaces + {"(", " ?%( ?"}, -- '(' may be surrounded by spaces + {")", " ?%) ?"}, -- ')' may be surrounded by spaces + {"", "x ?[%[%.]? ?[ntfx0']* ?%]?"}, -- identifier, possibly indexed once + {"", "x ?, ?x[x, ]*"}, -- at least two comma-separated identifiers + {"", "%[? ?[ntfx0']+ ?%]?"}, -- field, possibly like ["this"] + {"", "[ %(]*"} -- optional opening parentheses +} + +-- Utility function to make patterns more readable +local function fixup(pat) + for _, fixup_pair in ipairs(fixups) do + pat = replace(pat, fixup_pair[1], fixup_pair[2]) + end + + return pat +end + +--- Lines that are always excluded from accounting +local any_hits_exclusions = { + "", -- Empty line + "end[,; %)]*", -- Single "end" + "else", -- Single "else" + "repeat", -- Single "repeat" + "do", -- Single "do" + "if", -- Single "if" + "then", -- Single "then" + "while t do", -- "while true do" generates no code + "if t then", -- "if true then" generates no code + "local x", -- "local var" + fixup "local x=", -- "local var =" + fixup "local ", -- "local var1, ..., varN" + fixup "local =", -- "local var1, ..., varN =" + "local function x" -- "local function f (arg1, ..., argN)" +} + +--- Lines that are only excluded from accounting when they have 0 hits +local zero_hits_exclusions = { + "[ntfx0',= ]+,", -- "var1 var2," multi columns table stuff + "{ ?} ?,", -- Empty table before comma leaves no trace in tables and calls + fixup "=.+[,;]", -- "[123] = 23," "['foo'] = "asd"," + fixup "=function", -- "[123] = function(...)" + fixup "='", -- "[123] = [[", possibly with opening parens + "return function", -- "return function(arg1, ..., argN)" + "function", -- "function(arg1, ..., argN)" + "[ntfx0]", -- Single token expressions leave no trace in tables, function calls and sometimes assignments + "''", -- Same for strings + "{ ?}", -- Same for empty tables + fixup "", -- Same for local variables indexed once + fixup "local x=function", -- "local a = function(arg1, ..., argN)" + fixup "local x='", -- "local a = [[", possibly with opening parens + fixup "local x=(", -- "local a = (", possibly with several parens + fixup "local =(", -- "local a, b = (", possibly with several parens + fixup "local x=n", -- "local a = nil; local b = nil" produces no trace for the second statement + fixup "='", -- "a.b = [[", possibly with opening parens + fixup "=function", -- "a = function(arg1, ..., argN)" + "} ?,", -- "}," generates no trace if the table ends with a key-value pair + "} ?, ?function", -- same with "}, function(...)" + "break", -- "break" generates no trace in Lua 5.2+ + "{", -- "{" opening table + "}?[ %)]*", -- optional closing paren, possibly with several closing parens + "[ntf0']+ ?}[ %)]*" -- a constant at the end of a table, possibly with closing parens (for LuaJIT) +} + +local function excluded(exclusions, line) + for _, e in ipairs(exclusions) do + if line:match("^ *" .. e .. " *$") then + return true + end + end + + return false +end + +function LineScanner:find(pattern) + return self.line:find(pattern, self.i) +end + +-- Skips string literal with quote stored as self.quote. +-- @return boolean indicating success. +function LineScanner:skip_string() + -- Look for closing quote, possibly after even number of backslashes. + local _, quote_i = self:find("^(\\*)%1" .. self.quote) + if not quote_i then + _, quote_i = self:find("[^\\](\\*)%1" .. self.quote) + end + + if quote_i then + self.i = quote_i + 1 + self.quote = nil + table.insert(self.simple_line_buffer, "'") + return true + else + return false + end +end + +-- Skips long string literal with equal signs stored as self.equals. +-- @return boolean indicating success. +function LineScanner:skip_long_string() + local _, bracket_i = self:find("%]" .. self.equals .. "%]") + + if bracket_i then + self.i = bracket_i + 1 + self.equals = nil + + if self.comment then + self.comment = false + else + table.insert(self.simple_line_buffer, "'") + end + + return true + else + return false + end +end + +-- Skips function arguments. +-- @return boolean indicating success. +function LineScanner:skip_args() + local _, paren_i = self:find("%)") + + if paren_i then + self.i = paren_i + 1 + self.args = nil + return true + else + return false + end +end + +function LineScanner:skip_whitespace() + local next_i = self:find("%S") or #self.line + 1 + + if next_i ~= self.i then + self.i = next_i + table.insert(self.simple_line_buffer, " ") + end +end + +function LineScanner:skip_number() + if self:find("^0[xX]") then + self.i = self.i + 2 + end + + local _ + _, _, self.i = self:find("^[%x%.]*()") + + if self:find("^[eEpP][%+%-]") then + -- Skip exponent, too. + self.i = self.i + 2 + _, _, self.i = self:find("^[%x%.]*()") + end + + -- Skip LuaJIT number suffixes (i, ll, ull). + _, _, self.i = self:find("^[iull]*()") + table.insert(self.simple_line_buffer, "0") +end + +local keywords = {["nil"] = "n", ["true"] = "t", ["false"] = "f"} + +for _, keyword in ipairs( + { + "and", + "break", + "do", + "else", + "elseif", + "end", + "for", + "function", + "goto", + "if", + "in", + "local", + "not", + "or", + "repeat", + "return", + "then", + "until", + "while" + } +) do + keywords[keyword] = keyword +end + +function LineScanner:skip_name() + -- It is guaranteed that the first character matches "%a_". + local _, _, name = self:find("^([%w_]*)") + self.i = self.i + #name + + if keywords[name] then + name = keywords[name] + else + name = "x" + end + + table.insert(self.simple_line_buffer, name) + + if name == "function" then + -- This flag indicates that the next pair of parentheses (function args) must be skipped. + self.after_function = true + end +end + +-- Source lines can be explicitly ignored using `enable` and `disable` inline options. +-- An inline option is a simple comment: `-- luacov: enable` or `-- luacov: disable`. +-- Inline option parsing is not whitespace sensitive. +-- All lines starting from a line containing `disable` option and up to a line containing `enable` +-- option (or end of file) are excluded. + +function LineScanner:check_inline_options(comment_body) + if comment_body:find("^%s*luacov:%s*enable%s*$") then + self.enabled = true + elseif comment_body:find("^%s*luacov:%s*disable%s*$") then + self.enabled = false + end +end + +-- Consumes and analyzes a line. +-- @return boolean indicating whether line must be excluded. +-- @return boolean indicating whether line must be excluded if not hit. +function LineScanner:consume(line) + if self.first then + self.first = false + + if line:match("^#!") then + -- Ignore Unix hash-bang magic line. + return true, true + end + end + + self.line = line + -- As scanner goes through the line, it puts its simplified parts into buffer. + -- Punctuation is preserved. Whitespace is replaced with single space. + -- Literal strings are replaced with "''", so that a string literal + -- containing special characters does not confuse exclusion rules. + -- Numbers are replaced with "0". + -- Identifiers are replaced with "x". + -- Literal keywords (nil, true and false) are replaced with "n", "t" and "f", + -- other keywords are preserved. + -- Function declaration arguments are removed. + self.simple_line_buffer = {} + self.i = 1 + + while self.i <= #line do + -- One iteration of this loop handles one token, where + -- string literal start and end are considered distinct tokens. + if self.quote then + if not self:skip_string() then + -- String literal ends on another line. + break + end + elseif self.equals then + if not self:skip_long_string() then + -- Long string literal or comment ends on another line. + break + end + elseif self.args then + if not self:skip_args() then + -- Function arguments end on another line. + break + end + else + self:skip_whitespace() + + if self:find("^%.%d") then + self.i = self.i + 1 + end + + if self:find("^%d") then + self:skip_number() + elseif self:find("^[%a_]") then + self:skip_name() + else + if self:find("^%-%-") then + self.comment = true + self.i = self.i + 2 + end + + local _, bracket_i, equals = self:find("^%[(=*)%[") + if equals then + self.i = bracket_i + 1 + self.equals = equals + + if not self.comment then + table.insert(self.simple_line_buffer, "'") + end + elseif self.comment then + -- Simple comment, check if it contains inline options and skip line. + self.comment = false + local comment_body = self.line:sub(self.i) + self:check_inline_options(comment_body) + break + else + local char = line:sub(self.i, self.i) + + if char == "." then + -- Dot can't be saved as one character because of + -- ".." and "..." tokens and the fact that number literals + -- can start with one. + local _, _, dots = self:find("^(%.*)") + self.i = self.i + #dots + table.insert(self.simple_line_buffer, dots) + else + self.i = self.i + 1 + + if char == "'" or char == '"' then + table.insert(self.simple_line_buffer, "'") + self.quote = char + elseif self.after_function and char == "(" then + -- This is the opening parenthesis of function declaration args. + self.after_function = false + self.args = true + else + -- Save other punctuation literally. + -- This inserts an empty string when at the end of line, + -- which is fine. + table.insert(self.simple_line_buffer, char) + end + end + end + end + end + end + + if not self.enabled then + -- Disabled by inline options, always exclude the line. + return true, true + end + + local simple_line = table.concat(self.simple_line_buffer) + return excluded(any_hits_exclusions, simple_line), excluded(zero_hits_exclusions, simple_line) +end + +return LineScanner diff --git a/Client2020/ExtraContent/LuaPackages/CodeCoverage/Reporter.lua b/Client2020/ExtraContent/LuaPackages/CodeCoverage/Reporter.lua new file mode 100644 index 0000000..ecd1645 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/CodeCoverage/Reporter.lua @@ -0,0 +1,124 @@ +local CodeCoverage = script.Parent +local LineScanner = require(CodeCoverage.LineScanner) +local LcovReporter = require(CodeCoverage.LcovReporter) + +local ScriptContext = game:GetService("ScriptContext") +local CoreScriptSyncService = game:GetService("CoreScriptSyncService") + +local Reporter = {} +Reporter.__index = Reporter + +function Reporter.processCoverageStats() + local stats = ScriptContext:GetCoverageStats() + + local files = {} + for _, scriptStats in ipairs(stats) do + local aScript = scriptStats.Script; + + local source = aScript.Source + + local hits = scriptStats.Hits + local lineHit = 0 + local lineMissed = 0 + local lines = {} + + if scriptStats.HitsPrecise then + local sources = source:split('\n') + + for n,h in ipairs(hits) do + local ignored = h < 0 + + if h > 0 then + lineHit = lineHit + 1 + elseif h == 0 then + lineMissed = lineMissed + 1 + end + + lines[n] = { + source = sources[n], + ignored = ignored, + hits = math.max(h, 0) + } + end + else + local scanner = LineScanner:new() + local lineNumber = 1 + + for line in source:gmatch('([^\r\n]*)[\r\n]?') do + local excluded, excludedIfNotHit = scanner:consume(line) + + local ignored = excluded + + if not excluded then + if hits[lineNumber] and hits[lineNumber] > 0 then + lineHit = lineHit + 1 + else + if excludedIfNotHit then + ignored = true + else + lineMissed = lineMissed + 1 + end + end + end + + lines[lineNumber] = { + source = line, + ignored = ignored, + hits = hits[lineNumber] or 0 + } + + lineNumber = lineNumber + 1 + end + end + + table.insert(files, { + script = aScript, + path = CoreScriptSyncService:GetScriptFilePath(aScript), + lines = lines, + hits = lineHit, + misses = lineMissed, + }) + end + + return files +end + +local function matchesAny(str, excludes) + if not str or str:len() == 0 or not excludes then + return false + end + + for _,exclude in ipairs(excludes) do + if string.find(str, exclude) ~= nil then + return true + end + end + return false +end + +function Reporter.generateReport(path, excludes) + local report = LcovReporter.generate(Reporter.processCoverageStats(), function(file) + local isExcluded = file.script.Name:match(".spec$") + or file.script:FindFirstAncestor("TestEZ") + or file.script:IsDescendantOf(CodeCoverage) + or matchesAny(file.path, excludes) + local isIncluded = file.path and file.path:len() > 0 + + return isIncluded and not isExcluded + end) + if report:len() == 0 then + warn("Generating code coverage report failed. Produced report has zero size.") + return + end + + local success, message = pcall(function() -- New API + local fs = game:GetService("FileSystemService") + fs:WriteFile(path, report) + end) + + if not success then + warn("Failed to save code coverage report at path: " .. path .. "\nError: " .. message) + end +end + +return Reporter diff --git a/Client2020/ExtraContent/LuaPackages/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Cryo.lua new file mode 100644 index 0000000..43e131b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Cryo.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Cryo) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/ErrorReporters/.robloxrc b/Client2020/ExtraContent/LuaPackages/ErrorReporters/.robloxrc new file mode 100644 index 0000000..d828202 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/ErrorReporters/.robloxrc @@ -0,0 +1,11 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "ImplicitReturn": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReport.lua b/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReport.lua new file mode 100644 index 0000000..3f25fa9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReport.lua @@ -0,0 +1,232 @@ +--[[ + This module creates a crash object that can be sent to Backtrace. + For information about what are the acceptable fields, see document: + https://api.backtrace.io/#tag/submit-crash +]] + +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local Cryo = require(CorePackages.Packages.Cryo) +local t = require(CorePackages.Packages.t) + +local ProcessErrorStack = require(script.Parent.ProcessErrorStack) + +local DEFAULT_THREAD_NAME = "default" + +local IBacktraceStack = t.strictInterface({ + guessed_frame = t.optional(t.boolean), + funcName = t.optional(t.string), + address = t.optional(t.string), + line = t.optional(t.string), + column = t.optional(t.string), + sourceCode = t.optional(t.string), + library = t.optional(t.string), + debug_identifier = t.optional(t.string), + faulted = t.optional(t.boolean), + registers = t.optional(t.map(t.string, t.some(t.string, t.number))), +}) + +local IBacktraceThread = t.strictInterface({ + name = t.optional(t.string), + fault = t.optional(t.boolean), + stack = t.optional(t.array(IBacktraceStack)), +}) + +local IArch = t.strictInterface({ + name = t.string, + registers = t.map(t.string, t.string), +}) + +local ISourceCode = t.strictInterface({ + text = t.optional(t.string), + startLine = t.optional(t.number), + startColumn = t.optional(t.number), + startPos = t.optional(t.number), + path = t.optional(t.string), + tabWidth = t.optional(t.number), +}) + +local IPerm = t.strictInterface({ + read = t.boolean, + write = t.boolean, + exec = t.boolean, +}) + +local IMemory = t.strictInterface({ + start = t.string, + size = t.optional(t.number), + data = t.optional(t.string), + perms = t.optional(IPerm), +}) + +local IModule = t.strictInterface({ + start = t.string, + size = t.number, + code_file = t.optional(t.string), + version = t.optional(t.string), + debug_file = t.optional(t.string), + debug_identifier = t.optional(t.string), + debug_file_exists = t.optional(t.boolean), +}) + +local IAttributes = t.optional(t.map(t.string, t.some(t.string, t.number, t.boolean))) + +local IAnnotation = function(annotation) + local function checkTypeRecursive(value) + if type(value) == "table" then + for key, subValue in pairs(value) do + local valid, error = checkTypeRecursive(subValue) + if not valid then + return false, string.format("error when checking key: %s - %s", key, error) + end + end + return true + else + local type = t.some(t.string, t.number, t.boolean) + return type(value) + end + end + + return checkTypeRecursive(annotation) +end + +local IAnnotations = t.optional(t.map(t.string, IAnnotation)) + +local IBacktraceReport = t.intersection( + t.strictInterface({ + -- Must haves + uuid = t.string, + timestamp = t.number, + lang = t.string, + langVersion = t.string, + agent = t.string, + agentVersion = t.string, + threads = t.map(t.string, IBacktraceThread), + mainThread = t.string, + + -- Optionals + attributes = IAttributes, + annotations = IAnnotations, + symbolication = t.optional(t.literal("minidump")), + entryThread = t.optional(t.string), + arch = t.optional(IArch), + fingerprint = t.optional(t.string), + classifiers = t.optional(t.array(t.string)), + sourceCode = t.optional(t.map(t.string, ISourceCode)), + memory = t.optional(t.array(IMemory)), + modules = t.optional(t.array(IModule)), + }), + function(report) + local hasRegisters = false + + local threads = report.threads + for _, thread in pairs(threads) do + local stacks = thread.stack + if stacks ~= nil then + for _, stack in ipairs(stacks) do + if stack.registers ~= nil then + hasRegisters = true + break + end + end + end + if hasRegisters then + break + end + end + + if hasRegisters and report.arch == nil then + return false, "arch must exist if you want to have registers in the stack" + else + return true + end + end +) + +local BacktraceReport = { + IAttributes = IAttributes, + IAnnotations = IAnnotations, +} +BacktraceReport.__index = BacktraceReport + +function BacktraceReport:validate() + return IBacktraceReport(self) +end + +-- Return a basic report that has all the required fields +function BacktraceReport.new() + local self = { + uuid = HttpService:GenerateGUID(false):lower(), + timestamp = os.time(), + lang = "lua", + langVersion = "Roblox", + agent = "backtrace-Lua", + agentVersion = "0.1.0", + threads = {}, + mainThread = DEFAULT_THREAD_NAME, + } + + setmetatable(self, BacktraceReport) + + return self +end + +function BacktraceReport:addAttributes(newAttributes) + if type(newAttributes) ~= "table" then + warn("Cannot add attributes of type: ", type(newAttributes)) + return + end + + local attributes = self.attributes or {} + + attributes = Cryo.Dictionary.join(attributes, newAttributes) + + self.attributes = attributes +end + +function BacktraceReport:addAnnotations(newAnnotations) + if type(newAnnotations) ~= "table" then + warn("Cannot add annotations of type: ", type(newAnnotations)) + return + end + + local annotations = self.annotations or {} + + annotations = Cryo.Dictionary.join(annotations, newAnnotations) + + self.annotations = annotations +end + +function BacktraceReport:addStackToThread(stack, threadName) + local threads = self.threads + + threads = Cryo.Dictionary.join(threads, { + [threadName] = { + name = threadName, + stack = stack, + } + }) + + self.threads = threads +end + +function BacktraceReport:addStackToMainThread(stack) + self:addStackToThread(stack, self.mainThread) +end + +function BacktraceReport.fromMessageAndStack(errorMessage, errorStack) + local report = BacktraceReport.new() + + report:addAttributes({ + ["error.message"] = errorMessage, + }) + + local stack, sourceCode = ProcessErrorStack(errorStack) + report:addStackToMainThread(stack) + report.sourceCode = sourceCode + + return report +end + +return BacktraceReport diff --git a/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReport.spec.lua b/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReport.spec.lua new file mode 100644 index 0000000..3b055c2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReport.spec.lua @@ -0,0 +1,197 @@ +return function() + local BacktraceReport = require(script.Parent.BacktraceReport) + local CorePackages = game:GetService("CorePackages") + + local tutils = require(CorePackages.Packages.tutils) + + describe(".new", function() + it("should return a valid report", function() + local report = BacktraceReport.new() + local isValid = report:validate() + + expect(isValid).to.equal(true) + end) + end) + + describe(":validate", function() + it("should return false if the report has registers but no arch information, true otherwise", function() + local report = BacktraceReport.new() + + report.threads = { + default = { + stack = { + [1] = { + registers = { + rax = "16045690984833335023", + }, + } + }, + }, + } + + expect(report:validate()).to.equal(false) + + -- invalid arch + report.arch = { + name = "x64", + regsiters = nil, + } + + expect(report:validate()).to.equal(false) + + -- correct arch + report.arch = { + name = "x64", + registers = { + rax = "u64", + }, + } + + expect(report:validate()).to.equal(true) + end) + end) + + describe("IAnnotations", function() + local IAnnotations = BacktraceReport.IAnnotations + + it("should return false if not a table", function() + local result = IAnnotations("string") + expect(result).to.equal(false) + end) + + it("should return false if a non-table value is not string, number, or boolean", function() + local result = IAnnotations({ + Value = function() end, + }) + expect(result).to.equal(false) + + result = IAnnotations({ + Value = "ha", + Value2 = 1, + Value3 = false, + Recursive = { + Value11 = "haha", + MoreRecursive = { + Value12 = "hahaha", + Value22 = function() end, + }, + }, + }) + expect(result).to.equal(false) + end) + + it("should return true for an empty table", function() + expect(IAnnotations({})).to.equal(true) + end) + + it("should return true if a non-table value is either string, number, or boolean", function() + local result = IAnnotations({ + Value = "ha", + Value2 = 1, + Value3 = false, + Recursive = { + Value11 = "haha", + Value21 = 2, + Array = { + [1] = "hahaha", + [2] = 3, + [3] = true, + }, + }, + }) + expect(result).to.equal(true) + end) + end) + + describe(":addAttributes", function() + it("should correctly add attributes", function() + local report = BacktraceReport.new() + report:addAttributes({ + att1 = 1, + }) + expect(tutils.fieldCount(report.attributes)).to.equal(1) + expect(report.attributes.att1).to.equal(1) + report:addAttributes({ + att1 = 2, + att2 = false, + att3 = "test", + }) + expect(tutils.fieldCount(report.attributes)).to.equal(3) + expect(report.attributes.att1).to.equal(2) + expect(report.attributes.att2).to.equal(false) + expect(report.attributes.att3).to.equal("test") + end) + end) + + describe(":addAnnotations", function() + it("should correctly add annotations", function() + local environmentVariables = { + ENV_VAR_EXAMPLE = "example", + } + local dependencies = { + Roact = { + version = "0.2.0", + }, + Otter = { + version = "0.1.0", + }, + } + + local report = BacktraceReport.new() + + report:addAnnotations({ + EnvironmentVariables = environmentVariables, + }) + expect(tutils.fieldCount(report.annotations)).to.equal(1) + expect(tutils.deepEqual(report.annotations.EnvironmentVariables, environmentVariables)).to.equal(true) + + report:addAnnotations({ + SomeProperty = true, + Dependencies = dependencies, + }) + expect(tutils.fieldCount(report.annotations)).to.equal(3) + expect(tutils.deepEqual(report.annotations.EnvironmentVariables, environmentVariables)).to.equal(true) + expect(report.annotations.SomeProperty).to.equal(true) + expect(tutils.deepEqual(report.annotations.Dependencies, dependencies)).to.equal(true) + end) + end) + + describe(":addStackToThread", function() + it("should correctly add stack to the thread with the name provided", function() + local stack1 = { + [1] = { + line = "100", + funcName = "field testError", + sourceCode = "1", + } + } + local stack2 = { + [1] = { + line = "110", + funcName = "field ?", + sourceCode = "2", + } + } + + local report = BacktraceReport.new() + + report:addStackToThread(stack1, "main") + expect(tutils.fieldCount(report.threads)).to.equal(1) + expect(tutils.deepEqual(report.threads.main.stack, stack1)).to.equal(true) + + report:addStackToThread(stack2, "1") + expect(tutils.fieldCount(report.threads)).to.equal(2) + expect(tutils.deepEqual(report.threads.main.stack, stack1)).to.equal(true) + expect(tutils.deepEqual(report.threads["1"].stack, stack2)).to.equal(true) + end) + end) + + describe(".fromMessageAndStack", function() + it("should return a valid report", function() + local report = BacktraceReport.fromMessageAndStack("index nil", "Script 'Workspace.Script', Line 3") + local isValid = report:validate() + + expect(isValid).to.equal(true) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReporter.lua b/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReporter.lua new file mode 100644 index 0000000..7920f75 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReporter.lua @@ -0,0 +1,287 @@ +--[[ + Specialized reporter for sending data to Backtrace. + Useful for reporting Lua errors. +]] + +local CorePackages = game:GetService("CorePackages") + +local Cryo = require(CorePackages.Cryo) +local t = require(CorePackages.Packages.t) + +local BacktraceReport = require(script.Parent.BacktraceReport) +local ErrorQueue = require(script.Parent.Parent.ErrorQueue) + +local DEVELOPMENT_IN_STUDIO = game:GetService("RunService"):IsStudio() + +local DEFAULT_LOG_INTERVAL = 60 -- seconds + +local BacktraceReporter = {} +BacktraceReporter.__index = BacktraceReporter + +local IBacktraceReporter = t.strictInterface({ + httpService = t.some(t.instanceOf("HttpService"), t.interface({ + JSONEncode = t.callback, + JSONDecode = t.callback, + RequestInternal = t.callback, + })), + token = t.string, + processErrorReportMethod = t.optional(t.callback), + queueOptions = t.optional(t.table), + generateLogMethod = t.optional(t.callback), + logIntervalInSeconds = t.optional(t.numberPositive), +}) + +function BacktraceReporter.new(arguments) + local valid, message = IBacktraceReporter(arguments) + local self + + if valid then + self = { + _isEnabled = true, + _httpService = arguments.httpService, + _errorQueue = nil, + _reportUrl = game:GetFastString("ErrorUploadToBacktraceBaseUrl") .. "token=" .. arguments.token, + _processErrorReportMethod = arguments.processErrorReportMethod, + + _sharedAttributes = {}, + _sharedAnnotations = {}, + _generateLogMethod = arguments.generateLogMethod, + _logIntervalInSeconds = arguments.logIntervalInSeconds or DEFAULT_LOG_INTERVAL, + _lastLogTime = 0, + } + elseif (DEVELOPMENT_IN_STUDIO or _G.__TESTEZ_RUNNING_TEST__) then + error("invalid arguments for BacktraceReporter: " .. message) + else + self = { + _isEnabled = false, + } + end + + setmetatable(self, BacktraceReporter) + + -- Create and start the ErrorQueue for deferred reports. + if self._isEnabled then + self._errorQueue = ErrorQueue.new(function(...) + self:_reportErrorFromErrorQueue(...) + end, arguments.queueOptions) + + self._errorQueue:startTimer() + end + + return self +end + +function BacktraceReporter:sendErrorReport(report, log) + if not self._isEnabled then + return + end + + -- Validating the report can be slow; + -- And an invalid report might still be able to be processed, sent, and accepted by Backtrace. + -- So we don't validate reports in production. + if DEVELOPMENT_IN_STUDIO or _G.__TESTEZ_RUNNING_TEST__ then + assert(report:validate()) + end + + local encodeSuccess, jsonData = pcall(function() + return self._httpService:JSONEncode(report) + end) + + if not encodeSuccess then + warn("Cannot convert report to Json") + return + end + + pcall(function() + local httpRequest = self._httpService:RequestInternal({ + Url = self._reportUrl .. "&format=json", + Method = "POST", + Headers = { + ["Content-Type"] = "application/json", + }, + Body = jsonData, + }) + + httpRequest:Start(function(success, response) + -- Be aware that even when a response is 200, the report + -- might still be rejected/deleted by Backtrace after it is received. + if response.StatusCode == 200 and + log ~= nil then + + local decodeSuccesss, decodedBody = pcall(function() + return self._httpService:JSONDecode(response.Body) + end) + + if decodeSuccesss and decodedBody._rxid ~= nil then + self:_sendLogToReport(decodedBody._rxid, log) + end + end + end) + end) +end + +function BacktraceReporter:_sendLogToReport(reportRxid, log) + if type(log) ~= "string" or #log == 0 then + return + end + + pcall(function() + local httpRequest = self._httpService:RequestInternal({ + Url = self._reportUrl .. "&object=" .. reportRxid .. "&attachment_name=log.txt", + Method = "POST", + Headers = { + ["Content-Type"] = "text/plain", + }, + Body = log, + }) + + httpRequest:Start(function(reqSuccess, response) + -- We have no use for the result of this request right now. + end) + end) +end + +function BacktraceReporter:_generateLog() + if self._generateLogMethod ~= nil and + tick() - self._lastLogTime > self._logIntervalInSeconds then + self._lastLogTime = tick() + + local success, log = pcall(function() + return self._generateLogMethod() + end) + + if success and type(log) == "string" and #log > 0 then + return log + end + end + + return nil +end + +function BacktraceReporter:_generateErrorReport(errorMessage, errorStack, details) + local report = BacktraceReport.fromMessageAndStack(errorMessage, errorStack) + + report:addAttributes(self._sharedAttributes) + report:addAnnotations(self._sharedAnnotations) + + if type(details) == "string" and details ~= "" then + report:addAnnotations({ + ["stackDetails"] = details, + }) + end + + return report +end + +-- Immediate reports +-- You most likely should not use this. Use reportErrorDeferred instead. +function BacktraceReporter:reportErrorImmediately(errorMessage, errorStack, details) + if not self._isEnabled then + return + end + + local newReport = self:_generateErrorReport(errorMessage, errorStack, details) + + if self._processErrorReportMethod ~= nil then + newReport = self._processErrorReportMethod(newReport) + end + + local log = self:_generateLog() + + self:sendErrorReport(newReport, log) +end + +-- Deferred reports using an error queue +function BacktraceReporter:reportErrorDeferred(errorMessage, errorStack, details) + if not self._isEnabled then + return + end + + local errorKey = string.format("%s | %s", errorMessage, errorStack) + local errorData = {} + + -- If this error is a new one, we want a full report on it. + -- Similar errors following this one will be squashed in the queue and share report with this one + -- before they're flushed out and reported. + if not self._errorQueue:hasError(errorKey) then + local newReport = self:_generateErrorReport(errorMessage, errorStack, details) + + if self._processErrorReportMethod ~= nil then + newReport = self._processErrorReportMethod(newReport) + end + + errorData = { + backtraceReport = newReport, + log = self:_generateLog(), + } + end + + self._errorQueue:addError(errorKey, errorData) +end + +function BacktraceReporter:_reportErrorFromErrorQueue(errorKey, errorData, errorCount) + local errorReport = errorData.backtraceReport + local log = errorData.log + + errorReport:addAttributes({ + ErrorCount = errorCount, + }) + + self:sendErrorReport(errorReport, log) +end + +-- API for updating shared attributes/annotations +local IAttributes = BacktraceReport.IAttributes + +function BacktraceReporter:updateSharedAttributes(newAttributes) + -- Merge with current one first. This allows usage of Cryo.None. + local mergedAttributes = Cryo.Dictionary.join(self._sharedAttributes, newAttributes) + + -- Validate the merged result, and only update if it's valid. + local valid, message = IAttributes(mergedAttributes) + if not valid then + if DEVELOPMENT_IN_STUDIO or _G.__TESTEZ_RUNNING_TEST__ then + assert(valid, message) + else + return + end + end + + self._sharedAttributes = mergedAttributes +end + +local IAnnotations = BacktraceReport.IAnnotations + +function BacktraceReporter:updateSharedAnnotations(newAnnotations) + -- Although annotations can be nested tables, this is not a recursive merge. + local mergedAnnotations = Cryo.Dictionary.join(self._sharedAnnotations, newAnnotations) + + local valid, message = IAnnotations(mergedAnnotations) + if not valid then + if DEVELOPMENT_IN_STUDIO or _G.__TESTEZ_RUNNING_TEST__ then + assert(valid, message) + else + return + end + end + + self._sharedAnnotations = mergedAnnotations +end + +-- Flush all reports in the queue. +function BacktraceReporter:reportAllErrors() + if self._errorQueue ~= nil then + self._errorQueue:reportAllErrors() + end +end + +function BacktraceReporter:stop() + self._isEnabled = false + + if self._errorQueue ~= nil then + self:reportAllErrors() + self._errorQueue:stopTimer() + end +end + +return BacktraceReporter diff --git a/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReporter.spec.lua b/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReporter.spec.lua new file mode 100644 index 0000000..c371eab --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReporter.spec.lua @@ -0,0 +1,513 @@ +return function() + local BacktraceReporter = require(script.Parent.BacktraceReporter) + local BacktraceReport = require(script.Parent.BacktraceReport) + + local CorePackages = game:GetService("CorePackages") + local Cryo = require(CorePackages.Cryo) + local tutils = require(CorePackages.tutils) + + local requestsSent = 0 + local requestBody = nil + + local mockHttpRequestObj = {} + function mockHttpRequestObj:Start(onComplete) + requestsSent = requestsSent + 1 + onComplete(true, { + StatusCode = 200, + Body = { + _rxid = 12345, + } + }) + end + + local mockHttpService = {} + function mockHttpService:RequestInternal(options) + requestBody = options.Body + return mockHttpRequestObj + end + function mockHttpService:JSONEncode(data) + return data + end + function mockHttpService:JSONDecode(data) + return data + end + + local mockErrorMessage = "index nil" + local mockErrorStack = "Script 'Workspace.Script', Line 3" + + describe(".new", function() + it("should error if no httpService or token is passed in", function() + expect(function() + BacktraceReporter.new({}) + end).to.throw() + + expect(function() + BacktraceReporter.new({ + httpService = mockHttpService, + token = nil, + }) + end).to.throw() + + expect(function() + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "", + }) + reporter:stop() + end).to.never.throw() + end) + end) + + describe(":sendErrorReport", function() + it("should send error report through provided httpService", function() + requestsSent = 0 + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + local report = BacktraceReport.new() + + reporter:sendErrorReport(report) + + expect(requestsSent).to.equal(1) + + reporter:sendErrorReport(report) + + expect(requestsSent).to.equal(2) + + reporter:stop() + end) + + it("should assert if the error report is not valid", function() + requestsSent = 0 + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + local errorReport = { + error = "random", + } + + expect(function() + reporter:sendErrorReport(errorReport) + end).to.throw() + + expect(requestsSent).to.equal(0) + + reporter:stop() + end) + end) + + describe(":reportErrorImmediately", function() + it("should send error report through provided httpService", function() + requestsSent = 0 + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(2) + + reporter:stop() + end) + + it("should set details in the report if it's not nil", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack, "SomeDetails") + + expect(requestsSent).to.equal(1) + + expect(requestBody.annotations.stackDetails).to.equal("SomeDetails") + + reporter:stop() + end) + end) + + describe(":reportErrorDeferred", function() + it("should put error to a queue and send later", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + queueOptions = { + -- The queue should flush when there are 2 or more than 2 errors. + queueErrorLimit = 2, + }, + }) + + reporter:reportErrorDeferred(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(0) + + reporter:reportErrorDeferred(mockErrorMessage, mockErrorStack) + + -- These 2 errors would be squashed together + expect(requestsSent).to.equal(1) + expect(requestBody.attributes.ErrorCount).to.equal(2) + + reporter:stop() + end) + + it("should set details in the report if it's not nil", function() + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + queueOptions = { + -- The queue should flush when there are 2 or more than 2 errors. + queueErrorLimit = 2, + }, + }) + + requestsSent = 0 + requestBody = nil + reporter:reportErrorDeferred(mockErrorMessage, mockErrorStack, "SomeDetails") + reporter:reportErrorDeferred(mockErrorMessage, mockErrorStack, "SomeDetails") + + expect(requestsSent).to.equal(1) + + expect(requestBody.annotations.stackDetails).to.equal("SomeDetails") + + reporter:stop() + end) + end) + + describe("arguments.processErrorReportMethod", function() + it("should modify the error reports if passed in - reportErrorImmediately", function() + requestsSent = 0 + requestBody = nil + + local processErrorReport = function(report) + report.uuid = "id" + report.timestamp = 1 + report:addAttributes({ + ["Message"] = "test", + }) + return report + end + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + processErrorReportMethod = processErrorReport, + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + expect(requestBody.uuid).to.equal("id") + expect(requestBody.timestamp).to.equal(1) + expect(requestBody.attributes["Message"]).to.equal("test") + + reporter:stop() + end) + + it("should modify the error reports if passed in - reportErrorDeferred", function() + requestsSent = 0 + requestBody = nil + + local processErrorReport = function(report) + report.uuid = "id" + report.timestamp = 1 + report:addAttributes({ + ["Message"] = "test", + }) + return report + end + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + processErrorReportMethod = processErrorReport, + queueOptions = { + -- The queue should flush when there are 1 or more than 1 errors. + queueErrorLimit = 1, + }, + }) + + reporter:reportErrorDeferred(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + expect(requestBody.uuid).to.equal("id") + expect(requestBody.timestamp).to.equal(1) + expect(requestBody.attributes["Message"]).to.equal("test") + + reporter:stop() + end) + end) + + describe(":updateSharedAttributes", function() + it("should put the same attributes to all error reports", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + reporter:updateSharedAttributes({ + ["Message"] = "test", + ["Locale"] = "en-us", + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + expect(requestBody.attributes["Message"]).to.equal("test") + expect(requestBody.attributes["Locale"]).to.equal("en-us") + + requestBody = nil + reporter:reportErrorImmediately("some other message", mockErrorStack) + + expect(requestsSent).to.equal(2) + expect(requestBody.attributes["Message"]).to.equal("test") + expect(requestBody.attributes["Locale"]).to.equal("en-us") + + reporter:stop() + end) + + it("should merge attributes if called more than once", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + reporter:updateSharedAttributes({ + ["Message"] = "test", + ["Locale"] = "en-us", + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + expect(requestBody.attributes["Message"]).to.equal("test") + expect(requestBody.attributes["Locale"]).to.equal("en-us") + + reporter:updateSharedAttributes({ + ["Message"] = Cryo.None, + ["Locale"] = "zh-cn", + ["Theme"] = "light", + }) + + requestBody = nil + reporter:reportErrorImmediately("some other message", mockErrorStack) + + expect(requestsSent).to.equal(2) + expect(requestBody.attributes["Message"]).to.equal(nil) + expect(requestBody.attributes["Locale"]).to.equal("zh-cn") + expect(requestBody.attributes["Theme"]).to.equal("light") + + reporter:stop() + end) + + it("should throw if new attributes are ill-formatted", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + expect(function() + reporter:updateSharedAttributes({ + ["Message"] = Cryo.None, + ["Locale"] = "zh-cn", + ["Theme"] = function() end, -- callbacks are not allowed + }) + end).to.throw() + + reporter:stop() + end) + end) + + describe(":updateSharedAnnotations", function() + it("should put the same annotations to all error reports", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + local annotations = { + ["Message"] = "test", + ["AppInfo"] = { + ["Locale"] = "en-us", + ["Theme"] = "light", + }, + } + reporter:updateSharedAnnotations(annotations) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + expect(tutils.deepEqual(annotations, requestBody.annotations, true)).to.equal(true) + + requestBody = nil + reporter:reportErrorImmediately("some other message", mockErrorStack) + + expect(requestsSent).to.equal(2) + expect(tutils.deepEqual(annotations, requestBody.annotations, true)).to.equal(true) + + reporter:stop() + end) + + it("should merge annotations if called more than once", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + local annotations = { + ["Message"] = "test", + ["AppInfo"] = { + ["Locale"] = "en-us", + ["Theme"] = "light", + }, + ["AppVersion"] = "1.0", + } + reporter:updateSharedAnnotations(annotations) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + expect(tutils.deepEqual(annotations, requestBody.annotations, true)).to.equal(true) + + reporter:updateSharedAnnotations({ + ["Message"] = Cryo.None, + ["AppInfo"] = { + ["Theme"] = "dark", + }, + }) + + requestBody = nil + reporter:reportErrorImmediately("some other message", mockErrorStack) + + local expectedAnnotations = { + ["AppInfo"] = { + ["Theme"] = "dark", + }, + ["AppVersion"] = "1.0", + } + expect(requestsSent).to.equal(2) + expect(tutils.deepEqual(expectedAnnotations, requestBody.annotations, true)).to.equal(true) + + reporter:stop() + end) + + it("should throw if new annotations are ill-formatted", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + expect(function() + reporter:updateSharedAnnotations({ + ["Message"] = Cryo.None, + ["AppInfo"] = { + ["Locale"] = "en-us", + ["Theme"] = function() end, -- callbacks are not allowed + }, + }) + end).to.throw() + + reporter:stop() + end) + end) + + describe("Logging", function() + it("should send logs if provided generateLogMethod and error report is successful", function() + requestsSent = 0 + requestBody = nil + + local logText = "test log text" + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + generateLogMethod = function() + return logText + end, + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(2) -- one for error, one for log + expect(requestBody).to.equal(logText) + + reporter:stop() + end) + + it("should not send log if generateLogMethod did not return a string", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + generateLogMethod = function() + return 123 + end, + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + + reporter:stop() + end) + + it("should not send more than 1 log in logIntervalInSeconds provided", function() + requestsSent = 0 + requestBody = nil + + local logText = "test log text" + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + generateLogMethod = function() + return logText + end, + logIntervalInSeconds = 2, + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(2) -- one for error, one for log + expect(requestBody).to.equal(logText) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + expect(requestsSent).to.equal(3) -- only one more, the error report + + reporter:stop() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/ProcessErrorStack.lua b/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/ProcessErrorStack.lua new file mode 100644 index 0000000..a3e7e9a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/ProcessErrorStack.lua @@ -0,0 +1,101 @@ +--[[ + Module for splitting the errorStack string to a list. + + Currently works with: + Pattern of error from ScriptContext.Error: + see ScriptContext.extractCallStack + + Pattern of error from ScriptContext.Error: (luau) + see ScriptContext.extractCallStack + + Pattern of error from debug.traceback: + see ScriptContext.printCallStack + + Pattern of error from debug.traceback: (luau) + see luau_backtrace +]] + +local function splitStringWithMarks(string, matches) + if type(string) ~= "string" or + type(matches) ~= "table" then + return string, "" + end + + for _, match in ipairs(matches) do + local start, stop = string.find(string, match, nil, true) + + if start ~= nil then + local first = string.sub(string, 1, start - 1) + local rest = string.sub(string, stop + 1) + return first, rest + end + end + + return string, "" +end + +local function findFileNameFromPath(pathStr) + if type(pathStr) ~= "string" then + return "" + end + + return string.match(pathStr, "([^.]*)$") +end + +local function ProcessErrorStack(errorStack) + local stack = {} + local sourceCodeDict = {} + local numOfSourceCode = 0 + + if type(errorStack) ~= "string" then + return stack, sourceCodeDict + end + + for line in errorStack:gmatch("[^\r\n]+") do + local newLine + local source + local funcName + local lineNumber + + source, newLine = splitStringWithMarks(line, {", line ", ", Line ", ":"}) + lineNumber, funcName = splitStringWithMarks(newLine, {" - "}) + + if lineNumber ~= "" and source ~= "" then + -- Convert "Script 'filePath'" to filePath + local _, _, matchedSource = string.find(source, "Script '(.*)'") + if matchedSource ~= nil then + source = matchedSource + end + + local index = sourceCodeDict[source] + if index == nil then + numOfSourceCode = numOfSourceCode + 1 + index = numOfSourceCode + sourceCodeDict[source] = index + end + + -- If no funcName is provided, Backtrace has difficulty differentiating the error + -- stacks apart. So we want to have something. + if funcName == "" then + funcName = findFileNameFromPath(source) + end + + table.insert(stack, { + line = lineNumber, + funcName = funcName, + sourceCode = tostring(index), + }) + end + end + + local sourceCodeOutput = {} + for path, index in pairs(sourceCodeDict) do + sourceCodeOutput[tostring(index)] = { + path = path, + } + end + + return stack, sourceCodeOutput +end + +return ProcessErrorStack diff --git a/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/ProcessErrorStack.spec.lua b/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/ProcessErrorStack.spec.lua new file mode 100644 index 0000000..6543d87 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/ErrorReporters/Backtrace/ProcessErrorStack.spec.lua @@ -0,0 +1,202 @@ +return function() + local ProcessErrorStack = require(script.Parent.ProcessErrorStack) + local CorePackages = game:GetService("CorePackages") + + local tutils = require(CorePackages.Packages.tutils) + + local testCasesNormal = { + ScriptContextError = { + error = "CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame, line 98 - field testError\nCoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame, line 111 - field ?\nCorePackages.Packages._Index.roact.roact.SingleEventManager, line 83", + expectedOutput = { + stack = { + [1] = { + line = "98", + funcName = "field testError", + sourceCode = "1", + }, + [2] = { + line = "111", + funcName = "field ?", + sourceCode = "1", + }, + [3] = { + line = "83", + funcName = "SingleEventManager", + sourceCode = "2", + }, + }, + sourceCodeOutput = { + ["1"] = { + path = "CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame", + }, + ["2"] = { + path = "CorePackages.Packages._Index.roact.roact.SingleEventManager", + }, + }, + }, + }, + ScriptContextErrorLuau = { + error = "CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame, line 98\nCoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame, line 111\nCorePackages.Packages._Index.roact.roact.SingleEventManager, line 83", + expectedOutput = { + stack = { + [1] = { + line = "98", + funcName = "HomePageWithAvatarViewportFrame", + sourceCode = "1", + }, + [2] = { + line = "111", + funcName = "HomePageWithAvatarViewportFrame", + sourceCode = "1", + }, + [3] = { + line = "83", + funcName = "SingleEventManager", + sourceCode = "2", + }, + }, + sourceCodeOutput = { + ["1"] = { + path = "CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame", + }, + ["2"] = { + path = "CorePackages.Packages._Index.roact.roact.SingleEventManager", + }, + }, + }, + }, + DebugTraceback = { + error = "Stack Begin\nScript 'CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame', Line 100 - field testError\nScript 'CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame', Line 111 - field ?\nScript 'CorePackages.Packages._Index.roact.roact.SingleEventManager', Line 83\nStack End", + expectedOutput = { + stack = { + [1] = { + line = "100", + funcName = "field testError", + sourceCode = "1", + }, + [2] = { + line = "111", + funcName = "field ?", + sourceCode = "1", + }, + [3] = { + line = "83", + funcName = "SingleEventManager", + sourceCode = "2", + }, + }, + sourceCodeOutput = { + ["1"] = { + path = "CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame", + }, + ["2"] = { + path = "CorePackages.Packages._Index.roact.roact.SingleEventManager", + }, + }, + }, + }, + DebugTracebackLuau = { + error = "CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame:100\nCoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame:111\nCorePackages.Packages._Index.roact.roact.SingleEventManager:83", + expectedOutput = { + stack = { + [1] = { + line = "100", + funcName = "HomePageWithAvatarViewportFrame", + sourceCode = "1", + }, + [2] = { + line = "111", + funcName = "HomePageWithAvatarViewportFrame", + sourceCode = "1", + }, + [3] = { + line = "83", + funcName = "SingleEventManager", + sourceCode = "2", + }, + }, + sourceCodeOutput = { + ["1"] = { + path = "CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame", + }, + ["2"] = { + path = "CorePackages.Packages._Index.roact.roact.SingleEventManager", + }, + }, + }, + }, + OneLineError = { + error = "Script 'Workspace.Script', Line 3", + expectedOutput = { + stack = { + [1] = { + line = "3", + funcName = "Script", + sourceCode = "1", + }, + }, + sourceCodeOutput = { + ["1"] = { + path = "Workspace.Script", + }, + }, + }, + }, + PathWithNumbers = { + error = "Script 'Workspace1.Script2', Line 3", + expectedOutput = { + stack = { + [1] = { + line = "3", + funcName = "Script2", + sourceCode = "1", + }, + }, + sourceCodeOutput = { + ["1"] = { + path = "Workspace1.Script2", + }, + }, + }, + }, + } + + it("should convert the error strings to the correct format", function() + for key, testCase in pairs(testCasesNormal) do + local stack, sourceCodeOutput = ProcessErrorStack(testCase.error) + + expect(tutils.deepEqual(testCase.expectedOutput, { + stack = stack, + sourceCodeOutput = sourceCodeOutput, + }, true)).to.equal(true) + end + end) + + local testCasesOther = { + {}, + 123, + function() error("test") end, + "", + " - ", + ", line ", + ", line - ", + ":", + " - , line ", + } + + it("should return empty results with inputs that are not valid stack trace", function() + for _, testCase in ipairs(testCasesOther) do + local stack, sourceCodeOutput = ProcessErrorStack(testCase) + + expect(tutils.shallowEqual(stack, {})).to.equal(true) + expect(tutils.shallowEqual(sourceCodeOutput, {})).to.equal(true) + end + end) + + it("should return empty results with nil input", function() + local stack, sourceCodeOutput = ProcessErrorStack(nil) + + expect(tutils.shallowEqual(stack, {})).to.equal(true) + expect(tutils.shallowEqual(sourceCodeOutput, {})).to.equal(true) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/ErrorReporters/ErrorQueue.lua b/Client2020/ExtraContent/LuaPackages/ErrorReporters/ErrorQueue.lua new file mode 100644 index 0000000..0762b3c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/ErrorReporters/ErrorQueue.lua @@ -0,0 +1,129 @@ +local CorePackages = game:GetService("CorePackages") +local RunService = game:GetService("RunService") + +local Cryo = require(CorePackages.Packages.Cryo) +local t = require(CorePackages.Packages.t) + +-- Time limit is in seconds. +local DEFAULT_QUEUE_TIME_LIMIT = 30 +local DEFAULT_QUEUE_ERROR_LIMIT = 30 +local DEFAULT_QUEUE_KEY_LIMIT = 10 + +local ErrorQueue = {} +ErrorQueue.__index = ErrorQueue + +local IErrorQueue = t.tuple( + t.callback, + t.optional(t.strictInterface({ + queueTimeLimit = t.optional(t.numberPositive), + queueErrorLimit = t.optional(t.numberPositive), + queueKeyLimit = t.optional(t.numberPositive), + })) +) + +function ErrorQueue.new(reportMethod, options) + assert(IErrorQueue(reportMethod, options)) + + options = options or {} + + local self = { + _reportMethod = reportMethod, + + -- config + _queueTimeLimit = options.queueTimeLimit or DEFAULT_QUEUE_TIME_LIMIT, + _queueErrorLimit = options.queueErrorLimit or DEFAULT_QUEUE_ERROR_LIMIT, + _queueKeyLimit = options.queueKeyLimit or DEFAULT_QUEUE_KEY_LIMIT, + + _errors = {}, + _totalErrorCount = 0, + _totalKeyCount = 0, + + _runningTime = 0, + _renderSteppedConnection = nil, + } + + setmetatable(self, ErrorQueue) + + return self +end + +function ErrorQueue:hasError(errorKey) + if type(errorKey) ~= "string" or errorKey == "" then + return false + else + return self._errors[errorKey] ~= nil + end +end + +function ErrorQueue:addError(errorKey, errorData) + if type(errorKey) ~= "string" or errorKey == "" then + return + end + + if not self._errors[errorKey] then + -- Errors with the same key will be sent together as 1 error, with a count parameter. + -- We only keep the data from the oldest error with this key in the queue. + self._errors[errorKey] = { + data = errorData, + count = 1, + } + self._totalKeyCount = self._totalKeyCount + 1 + else + self._errors[errorKey].count = self._errors[errorKey].count + 1 + end + + self._totalErrorCount = self._totalErrorCount + 1 + + if self:isReadyToReport() then + self:reportAllErrors() + end +end + +function ErrorQueue:isReadyToReport() + return self._totalKeyCount >= self._queueKeyLimit or + self._totalErrorCount >= self._queueErrorLimit or + (self._totalErrorCount > 0 and self._runningTime >= self._queueTimeLimit) +end + +function ErrorQueue:reportAllErrors() + -- copy the error queue and instantly clear it out + local errors = Cryo.Dictionary.join(self._errors, {}) + + self._errors = {} + self._totalErrorCount = 0 + self._totalKeyCount = 0 + self._runningTime = 0 + + -- report the errors + for errorKey, errData in pairs(errors) do + self._reportMethod(errorKey, errData.data, errData.count) + end +end + +function ErrorQueue:_onRenderStep(dt) + self._runningTime = self._runningTime + dt + + if self:isReadyToReport() then + self:reportAllErrors() + end +end + +function ErrorQueue:startTimer() + if self._renderSteppedConnection == nil then + self._runningTime = 0 + + self._renderSteppedConnection = RunService.renderStepped:Connect(function(dt) + self:_onRenderStep(dt) + end) + end +end + +function ErrorQueue:stopTimer() + if self._renderSteppedConnection ~= nil then + self._renderSteppedConnection:Disconnect() + self._runningTime = 0 + self._renderSteppedConnection = nil + end +end + +return ErrorQueue diff --git a/Client2020/ExtraContent/LuaPackages/ErrorReporters/ErrorQueue.spec.lua b/Client2020/ExtraContent/LuaPackages/ErrorReporters/ErrorQueue.spec.lua new file mode 100644 index 0000000..44e7ddb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/ErrorReporters/ErrorQueue.spec.lua @@ -0,0 +1,301 @@ +return function() + local ErrorQueue = require(script.Parent.ErrorQueue) + local CorePackages = game:GetService("CorePackages") + + local tutils = require(CorePackages.Packages.tutils) + + local errorsToAdd = { + [1] = { + key = "test", + value = 2.33, + }, + [2] = { + key = "test", + value = 2, + }, + [3] = { + key = "test", + value = 3.14, + }, + [4] = { + key = "test2", + value = 123, + }, + [5] = { + key = "test2", + value = 456, + }, + [6] = { + key = "test3", + value = "something", + }, + } + + describe("queueErrorLimit", function() + it("should report errors correctly when the total error limit is reached", function() + local expectedErrorsReported = { + ["test"] = { + data = 2.33, + count = 3, + }, + ["test2"] = { + data = 123, + count = 2, + }, + ["test3"] = { + data = "something", + count = 1, + }, + } + + local errorsReported = {} + local reportCount = 0 + local reportMethod = function(errorKey, errorData, errorCount) + reportCount = reportCount + 1 + errorsReported[errorKey] = { + data = errorData, + count = errorCount, + } + end + + local errorQueue = ErrorQueue.new(reportMethod, { + queueErrorLimit = 6, + }) + + for _, error in ipairs(errorsToAdd) do + errorQueue:addError(error.key, error.value) + end + + expect(reportCount).to.equal(3) + for errorKey, expectedError in pairs(expectedErrorsReported) do + expect(tutils.deepEqual(expectedError, errorsReported[errorKey])).to.equal(true) + end + end) + + it("should not report errors before the total error limit is reached", function() + local errorsReported = {} + local reportCount = 0 + local reportMethod = function(errorKey, errorData, errorCount) + reportCount = reportCount + 1 + errorsReported[errorKey] = { + data = errorData, + count = errorCount, + } + end + + local errorQueue = ErrorQueue.new(reportMethod, { + queueErrorLimit = 6, + }) + + for index = 1, 5 do + local error = errorsToAdd[index] + errorQueue:addError(error.key, error.value) + end + + expect(reportCount).to.equal(0) + expect(tutils.deepEqual(errorsReported, {})).to.equal(true) + end) + end) + + describe("queueKeyLimit", function() + it("should report errors correctly when the total key limit is reached", function() + local expectedErrorsReported = { + ["test"] = { + data = 2.33, + count = 3, + }, + ["test2"] = { + data = 123, + count = 2, + }, + ["test3"] = { + data = "something", + count = 1, + }, + } + + local errorsReported = {} + local reportCount = 0 + local reportMethod = function(errorKey, errorData, errorCount) + reportCount = reportCount + 1 + errorsReported[errorKey] = { + data = errorData, + count = errorCount, + } + end + + local errorQueue = ErrorQueue.new(reportMethod, { + queueKeyLimit = 3, + queueErrorLimit = 100, + }) + + for _, error in ipairs(errorsToAdd) do + errorQueue:addError(error.key, error.value) + end + + expect(reportCount).to.equal(3) + for errorKey, expectedError in pairs(expectedErrorsReported) do + expect(tutils.deepEqual(expectedError, errorsReported[errorKey])).to.equal(true) + end + end) + + it("should not report errors before the total key limit is reached", function() + local errorsReported = {} + local reportCount = 0 + local reportMethod = function(errorKey, errorData, errorCount) + reportCount = reportCount + 1 + errorsReported[errorKey] = { + data = errorData, + count = errorCount, + } + end + + local errorQueue = ErrorQueue.new(reportMethod, { + queueKeyLimit = 4, + queueErrorLimit = 100, + }) + + for _, error in ipairs(errorsToAdd) do + errorQueue:addError(error.key, error.value) + end + + expect(reportCount).to.equal(0) + expect(tutils.deepEqual(errorsReported, {})).to.equal(true) + end) + end) + + describe("queueTimeLimit", function() + HACK_NO_XPCALL() + + it("should report errors correctly when the total time limit is reached", function() + local expectedErrorsReported = { + ["test"] = { + data = 2.33, + count = 3, + }, + ["test2"] = { + data = 123, + count = 2, + }, + ["test3"] = { + data = "something", + count = 1, + }, + } + + local errorsReported = {} + local reportCount = 0 + local reportMethod = function(errorKey, errorData, errorCount) + reportCount = reportCount + 1 + errorsReported[errorKey] = { + data = errorData, + count = errorCount, + } + end + + local errorQueue = ErrorQueue.new(reportMethod, { + queueTimeLimit = 0.5, + queueErrorLimit = 100, + queueKeyLimit = 100, + }) + + errorQueue:startTimer() + + for _, error in ipairs(errorsToAdd) do + errorQueue:addError(error.key, error.value) + end + + errorQueue:_onRenderStep(0.5) + + expect(reportCount).to.equal(3) + for errorKey, expectedError in pairs(expectedErrorsReported) do + expect(tutils.deepEqual(expectedError, errorsReported[errorKey])).to.equal(true) + end + + errorQueue:stopTimer() + end) + + it("should not report errors before the total time limit is reached", function() + local errorsReported = {} + local reportCount = 0 + local reportMethod = function(errorKey, errorData, errorCount) + reportCount = reportCount + 1 + errorsReported[errorKey] = { + data = errorData, + count = errorCount, + } + end + + local errorQueue = ErrorQueue.new(reportMethod, { + queueTimeLimit = 2, + queueErrorLimit = 100, + queueKeyLimit = 100, + }) + + errorQueue:startTimer() + + for _, error in ipairs(errorsToAdd) do + errorQueue:addError(error.key, error.value) + end + + errorQueue:_onRenderStep(1) + + expect(reportCount).to.equal(0) + expect(tutils.deepEqual(errorsReported, {})).to.equal(true) + + errorQueue:stopTimer() + end) + end) + + describe("reportAllErrors", function() + it("should report all errors correctly when called", function() + local expectedErrorsReported = { + ["test"] = { + data = 2.33, + count = 3, + }, + ["test2"] = { + data = 123, + count = 2, + }, + ["test3"] = { + data = "something", + count = 1, + }, + } + + local errorsReported = {} + local reportCount = 0 + local reportMethod = function(errorKey, errorData, errorCount) + reportCount = reportCount + 1 + errorsReported[errorKey] = { + data = errorData, + count = errorCount, + } + end + + local errorQueue = ErrorQueue.new(reportMethod, { + queueTimeLimit = 100, + queueErrorLimit = 100, + queueKeyLimit = 100, + }) + + errorQueue:startTimer() + + for _, error in ipairs(errorsToAdd) do + errorQueue:addError(error.key, error.value) + end + + expect(reportCount).to.equal(0) + + errorQueue:reportAllErrors() + + expect(reportCount).to.equal(3) + for errorKey, expectedError in pairs(expectedErrorsReported) do + expect(tutils.deepEqual(expectedError, errorsReported[errorKey])).to.equal(true) + end + + errorQueue:stopTimer() + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/InGameMenuDependencies.lua b/Client2020/ExtraContent/LuaPackages/InGameMenuDependencies.lua new file mode 100644 index 0000000..a359dff --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/InGameMenuDependencies.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.InGameMenuDependencies) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/InGameServices/.robloxrc b/Client2020/ExtraContent/LuaPackages/InGameServices/.robloxrc new file mode 100644 index 0000000..33c7a1c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/InGameServices/.robloxrc @@ -0,0 +1,9 @@ +{ + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "ImplicitReturn": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/InGameServices/MouseIconOverrideService.lua b/Client2020/ExtraContent/LuaPackages/InGameServices/MouseIconOverrideService.lua new file mode 100644 index 0000000..55bbf0c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/InGameServices/MouseIconOverrideService.lua @@ -0,0 +1,69 @@ +--[[ + Prevents conflicts when multiple CoreScript subsystems are trying to override + the mouse cursor at the same time. This uses a stack approach similar to how + ContextActionService works. The most recent override takes precedence, and any + previously set overrides will remain on the stack beneath it until removed. + + Example usage: + MouseIconOverrideService.push("PurchasePrompt", Enum.OverrideMouseIconBehavior.ForceShow) + MouseIconOverrideService.pop("PurchasePrompt") +]] + +local UserInputService = game:GetService("UserInputService") + +local FFlagUseCursorOverrideManager = settings():GetFFlag("UseCursorOverrideManager") + +local cursorOverrideStack = {} + +local function update() + local activeOverride = cursorOverrideStack[#cursorOverrideStack] + if activeOverride then + UserInputService.OverrideMouseIconBehavior = activeOverride[2] + else + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None + end +end + +return { + push = function(name, behavior) + if not FFlagUseCursorOverrideManager then + UserInputService.OverrideMouseIconBehavior = behavior + return + end + + assert(type(name) == "string") + assert(typeof(behavior) == "EnumItem") + assert(behavior.EnumType == Enum.OverrideMouseIconBehavior) + + for _, entry in ipairs(cursorOverrideStack) do + if entry[1] == name then + error("Already a cursor override named " .. name, 2) + end + end + + table.insert(cursorOverrideStack, {name, behavior}) + update() + end, + pop = function(name) + if not FFlagUseCursorOverrideManager then + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None + return + end + + assert(type(name) == "string") + + local idx + + for testIdx, entry in ipairs(cursorOverrideStack) do + if entry[1] == name then + idx = testIdx + break + end + end + + assert(idx, "No cursor override named " .. name) + + table.remove(cursorOverrideStack, idx) + update() + end, +} diff --git a/Client2020/ExtraContent/LuaPackages/Localization/.robloxrc b/Client2020/ExtraContent/LuaPackages/Localization/.robloxrc new file mode 100644 index 0000000..321fd28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Localization/.robloxrc @@ -0,0 +1,12 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "ImplicitReturn": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Localization/LocalizationConsumer.lua b/Client2020/ExtraContent/LuaPackages/Localization/LocalizationConsumer.lua new file mode 100644 index 0000000..8e6c135 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Localization/LocalizationConsumer.lua @@ -0,0 +1,100 @@ + local CorePackages = game:GetService("CorePackages") +local LocalizationService = game:GetService("LocalizationService") + +local Roact = require(CorePackages.Roact) +local ExternalEventConnection = require(CorePackages.RoactUtilities.ExternalEventConnection) + +local ArgCheck = require(CorePackages.ArgCheck) +local LocalizationKey = require(CorePackages.Localization.LocalizationKey) + +local LocalizationConsumer = Roact.Component:extend("LocalizationConsumer") + +function LocalizationConsumer:init() + local localization = self._context[LocalizationKey].localization + + if localization == nil then + error("LocalizationConsumer must be below a LocalizationProvider.") + end + + self.state = { + locale = LocalizationService.RobloxLocaleId, + } + + self.updateLocalization = function(newLocale) + if settings():GetFFlag("AppBridgeStartupController") then + newLocale = localization:GetLocale() + end + if newLocale ~= self.state.locale then + self:setState({ + locale = newLocale + }) + end + end + + if settings():GetFFlag("AppBridgeStartupController") then + self.connections = { + localization.changed:connect(self.updateLocalization) + } + end +end + +function LocalizationConsumer:willUnmount() + if settings():GetFFlag("AppBridgeStartupController") then + for _, connection in pairs(self.connections) do + connection:disconnect() + end + end +end + +function LocalizationConsumer:render() + local localization = self._context[LocalizationKey].localization + local render = self.props.render + local stringsToBeLocalized = self.props.stringsToBeLocalized + + ArgCheck.isType(render, "function", "LocalizationConsumer.props.render") + ArgCheck.isType(stringsToBeLocalized, "table", "LocalizationConsumer.props.stringsToBeLocalized") + + local localizedStrings = {} + for stringName, stringInfo in pairs(stringsToBeLocalized) do + if typeof(stringInfo) == "table" then + if typeof(stringInfo[1]) == "string" then + local success, result = pcall(function() + return localization:Format(stringInfo[1], stringInfo) + end) + + ArgCheck.isEqual(success, true, string.format( + "LocalizationConsumer finding value for translation key[%s]: %s", stringName, stringInfo[1])) + + localizedStrings[stringName] = success and result or "" + else + error(string.format("%s[1] in stringsToBeLocalized must be a string, got %s instead", + stringName, typeof(stringInfo[1]))) + end + elseif typeof(stringInfo) == "string" then + local success, result = pcall(function() + return localization:Format(stringInfo) + end) + + ArgCheck.isEqual(success, true, string.format( + "LocalizationConsumer finding value for translation key[%s]: %s", stringName, stringInfo)) + + localizedStrings[stringName] = success and result or "" + else + error(string.format("%s in stringsToBeLocalized must be a string or table, got %s instead", + stringName, typeof(stringInfo))) + end + end + + if settings():GetFFlag("AppBridgeStartupController") then + return render(localizedStrings) + else + return Roact.createElement(ExternalEventConnection, { + event = LocalizationService:GetPropertyChangedSignal("RobloxLocaleId"), + callback = self.updateLocalization, + }, { + Component = render(localizedStrings), + }) + end +end + +return LocalizationConsumer diff --git a/Client2020/ExtraContent/LuaPackages/Localization/LocalizationConsumer.spec.lua b/Client2020/ExtraContent/LuaPackages/Localization/LocalizationConsumer.spec.lua new file mode 100644 index 0000000..9a3dca2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Localization/LocalizationConsumer.spec.lua @@ -0,0 +1,4 @@ +return function() + itSKIP("SOC-6353 - These unit tests need to be moved from LuaApp", function() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Localization/LocalizationContext.lua b/Client2020/ExtraContent/LuaPackages/Localization/LocalizationContext.lua new file mode 100644 index 0000000..e78ef17 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Localization/LocalizationContext.lua @@ -0,0 +1,142 @@ +--[[ + Contains all of the loaded translations and provides methods to translate + keys and parameters to strings. + + LocalizationContext doesn't handle loading of specific languages, but does + recommend what languages should be loaded (if available). + + To create a new LocalizationContext: + + local currentLanguage = LocalizationService.RobloxLocaleId + local languages = LocalizationContext.getRelevantLanguages(currentLanguage) + + local translations = {} + + -- Use the list of languages to load a set of translation tables here. + -- A translation table is just a map from key to the translated string. + -- `translations` is a map from language to translation tables. + + local context = LocalizationContext.new(translations) + + -- Get a string that doesn't require parameters + context:getString(currentLanguage, "SOME_KEY") + + -- Passing parameters: + context:getString(currentLanguage, "FANCY_KEY", { + apples = 5, + }) + + Additional languages can be added after the LocalizationContext is created + by calling `addTranslations`. Whenever the user's language changes, call + `getRelevantLanguages` to get a new list of languages to load, load them, + then call `addTranslations` to merge them in with the existing tables. +]] + +--[[ + Finds the base language code for the given language, if there is one. + + We assume: + * Language codes are of the form LANGUAGE or LANGUAGE_COUNTRY + * LANGUAGE_COUNTRY is more specific than LANGUAGE +]] + +local function getBaseLanguage(languageName) + return languageName:match("^(%w+)[-_]") +end + +local LocalizationContext = {} +LocalizationContext.__index = LocalizationContext + +function LocalizationContext.new(translations) + local self = { + _translations = translations, + } + + setmetatable(self, LocalizationContext) + + return self +end + +--[[ + Add translations to an existing LocalizationContext, such as when a user + switches languages while the app is running. +]] +function LocalizationContext:addTranslations(translations) + self._translations = translations +end + +--[[ + Yields a list of languages relevant to the current user. + + When the user's language changes, query this value, load those translations, + and add them to the LocalizationContext using addTranslations. +]] +function LocalizationContext.getRelevantLanguages(primaryLanguage) + local languages = {} + + -- Load the language itself if available. + table.insert(languages, primaryLanguage) + + -- If there's a fallback for our current language, load that as well. + local fallbackLanguage = getBaseLanguage(primaryLanguage) + if fallbackLanguage then + table.insert(languages, fallbackLanguage) + end + + -- We should always load English, as it should contain every valid key. + table.insert(languages, "en-us") + return languages +end + +function LocalizationContext:_getSourceString(language, key) + local translationTable = self._translations[language] + + if not translationTable then + return nil + end + + return translationTable[key] +end + +--[[ + Translate a key with a set of arguments into the given language. + + `language` must be explicitly provided +]] +function LocalizationContext:getString(language, key, parameters) + local exactValue = self:_getSourceString(language, key) + + local baseLanguage = getBaseLanguage(language) + + local baseLanguageValue + if baseLanguage then + baseLanguageValue = self:_getSourceString(baseLanguage, key) + end + + local englishValue = self:_getSourceString("en-us", key) + + -- We try to find source strings in descending priority here: + local sourceString = exactValue or baseLanguageValue or englishValue + + -- Missing translations are considered a developer error, so we throw here. + if not sourceString then + local message = ( + "Couldn't find value for translation key %q!\n" .. + "Tried these languages: %s, %s, %s" + ):format( + key, + language, baseLanguage, "en-us" + ) + error(message, 2) + end + + -- If we have parameters to insert into the string, put them in! + -- We don't check for missing parameters, should we in the future? + if parameters then + return (sourceString:gsub("{(.-)}", parameters)) + else + return sourceString + end +end + +return LocalizationContext \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Localization/LocalizationContext.spec.lua b/Client2020/ExtraContent/LuaPackages/Localization/LocalizationContext.spec.lua new file mode 100644 index 0000000..8c7b8c0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Localization/LocalizationContext.spec.lua @@ -0,0 +1,66 @@ +return function() + local LocalizationContext = require(script.Parent.LocalizationContext) + + it("should pull from the correct language if available", function() + local context = LocalizationContext.new({ + ["es-mx"] = { + ["SomeKey"] = "Foo", + }, + ["es"] = { + ["SomeKey"] = "Bar", + }, + ["en-us"] = { + ["SomeKey"] = "Baz", + }, + }) + + expect(context:getString("es-mx", "SomeKey")).to.equal("Foo") + expect(context:getString("es", "SomeKey")).to.equal("Bar") + expect(context:getString("en", "SomeKey")).to.equal("Baz") + end) + + it("should fall through to a language's base language", function() + local context = LocalizationContext.new({ + ["es-mx"] = {}, + ["es"] = { + ["SomeKey"] = "Bar", + }, + ["en"] = { + ["SomeKey"] = "Baz", + }, + }) + + expect(context:getString("es-mx", "SomeKey")).to.equal("Bar") + expect(context:getString("es", "SomeKey")).to.equal("Bar") + expect(context:getString("en", "SomeKey")).to.equal("Baz") + end) + + it("should fall through to English if keys are missing in each table", function() + local context = LocalizationContext.new({ + ["es-mx"] = {}, + ["es"] = {}, + ["en-us"] = { + ["SomeKey"] = "Baz", + }, + }) + + expect(context:getString("es-mx", "SomeKey")).to.equal("Baz") + expect(context:getString("es", "SomeKey")).to.equal("Baz") + expect(context:getString("en_us", "SomeKey")).to.equal("Baz") + end) + + it("should replace formatting identifiers of the form {name}", function() + local context = LocalizationContext.new({ + ["en-us"] = { + ["SomeKey"] = "{greeting}, {target}!", + }, + }) + + local value = context:getString("en-us", "SomeKey", { + greeting = "Hello", + target = "world", + }) + + expect(value).to.equal("Hello, world!") + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Localization/LocalizationKey.lua b/Client2020/ExtraContent/LuaPackages/Localization/LocalizationKey.lua new file mode 100644 index 0000000..9207eb4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Localization/LocalizationKey.lua @@ -0,0 +1,4 @@ +local CorePackages = game:GetService("CorePackages") +local Symbol = require(CorePackages.Symbol) + +return Symbol.named("Localization") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Localization/LocalizationProvider.lua b/Client2020/ExtraContent/LuaPackages/Localization/LocalizationProvider.lua new file mode 100644 index 0000000..dd92ee6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Localization/LocalizationProvider.lua @@ -0,0 +1,19 @@ +local CorePackages = game:GetService("CorePackages") + +local Roact = require(CorePackages.Roact) +local LocalizationKey = require(CorePackages.Localization.LocalizationKey) + +local LocalizationProvider = Roact.Component:extend("LocalizationProvider") + +function LocalizationProvider:init(props) + local localization = props.localization + self._context[LocalizationKey] = { + localization = localization + } +end + +function LocalizationProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +return LocalizationProvider diff --git a/Client2020/ExtraContent/LuaPackages/Localization/NumberLocalization.lua b/Client2020/ExtraContent/LuaPackages/Localization/NumberLocalization.lua new file mode 100644 index 0000000..110f3d1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Localization/NumberLocalization.lua @@ -0,0 +1,299 @@ +-- Example locale-sensitive number formatting: +-- https://docs.oracle.com/cd/E19455-01/806-0169/overview-9/index.html + +--[[ + Locale specification: + [DECIMAL_SEPARATOR] = string for decimal point, if needed + [GROUP_DELIMITER] = string for groupings of numbers left of the decimal + List section = abbreviations for language, in increasing order + + Missing features in this code: + - No support for differences in number of digits per GROUP_DELIMITER. + Some Chinese dialects group by 10000 instead of 1000. + - No support for variable differences in number of digits per GROUP_DELIMITER. + Indian natural language groups the first 3 to left of decimal, then every 2 after that. + + See https://en.wikipedia.org/wiki/Decimal_separator#Digit_grouping +]] +local CorePackages = game:GetService("CorePackages") +local Logging = require(CorePackages.Logging) + +local RoundingBehaviour = require(script.Parent.RoundingBehaviour) + +local localeInfos = {} + +local DEFAULT_LOCALE = "en-us" + +-- Separator aliases to help avoid spelling errors +local DECIMAL_SEPARATOR = "decimalSeparator" +local GROUP_DELIMITER = "groupDelimiter" + +localeInfos["en-us"] = { + [DECIMAL_SEPARATOR] = ".", + [GROUP_DELIMITER] = ",", + { 1, "", }, + { 1e3, "K", }, + { 1e6, "M", }, + { 1e9, "B", }, +} + +localeInfos["es-es"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = ".", + { 1, "", }, + { 1e3, " mil", }, + { 1e6, " M", }, +} + +localeInfos["fr-fr"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = " ", + { 1, "", }, + { 1e3, " k", }, + { 1e6, " M", }, + { 1e9, " Md", }, +} + +localeInfos["de-de"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = " ", + { 1, "", }, + { 1e3, " Tsd.", }, + { 1e6, " Mio.", }, + { 1e9, " Mrd.", }, +} + +localeInfos["pt-br"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = ".", + { 1, "", }, + { 1e3, " mil", }, + { 1e6, " mi", }, + { 1e9, " bi", }, +} + +localeInfos["zh-cn"] = { + [DECIMAL_SEPARATOR] = ".", + [GROUP_DELIMITER] = ",", -- Chinese commonly uses 3 digit groupings, despite 10000s rule + { 1, "", }, + { 1e3, "千", }, + { 1e4, "万", }, + { 1e8, "亿", }, +} + +localeInfos["zh-tw"] = { + [DECIMAL_SEPARATOR] = ".", + [GROUP_DELIMITER] = ",", -- Chinese commonly uses 3 digit groupings, despite 10000s rule + { 1, "", }, + { 1e3, "千", }, + { 1e4, "萬", }, + { 1e8, "億", }, +} + +localeInfos["ko-kr"] = { + [DECIMAL_SEPARATOR] = ".", + [GROUP_DELIMITER] = ",", + { 1, "", }, + { 1e3, "천", }, + { 1e4, "만", }, + { 1e8, "억", }, +} + +localeInfos["ja-jp"] = { + [DECIMAL_SEPARATOR] = ".", + [GROUP_DELIMITER] = ",", + { 1, "", }, + { 1e3, "千", }, + { 1e4, "万", }, + { 1e8, "億", }, +} + +localeInfos["it-it"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = " ", + { 1, "", }, + { 1e3, " mila", }, + { 1e6, " Mln", }, + { 1e9, " Mld", }, +} + +localeInfos["ru-ru"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = ".", + { 1, "", }, + { 1e3, " тыс", }, + { 1e6, " млн", }, + { 1e9, " млрд", }, +} + +localeInfos["id-id"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = ".", + { 1, "", }, + { 1e3, " rb", }, + { 1e6, " jt", }, + { 1e9, " M", }, +} + +localeInfos["vi-vn"] = { + [DECIMAL_SEPARATOR] = ".", + [GROUP_DELIMITER] = " ", + { 1, "", }, + { 1e3, " N", }, + { 1e6, " Tr", }, + { 1e9, " T", }, +} + +localeInfos["th-th"] = { + [DECIMAL_SEPARATOR] = ".", + [GROUP_DELIMITER] = ",", + { 1, "", }, + { 1e3, " พ", }, + { 1e4, " ม", }, + { 1e5, " ส", }, + { 1e6, " ล", }, +} + +localeInfos["tr-tr"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = ".", + { 1, "", }, + { 1e3, " B", }, + { 1e6, " Mn", }, + { 1e9, " Mr", }, +} + +-- Aliases for languages that use the same mappings. +localeInfos["en-gb"] = localeInfos["en-us"] +localeInfos["es-mx"] = localeInfos["es-es"] + +local function findDecimalPointIndex(numberStr) + return string.find(numberStr, "%.") or #numberStr + 1 +end + +-- Find the base 10 offset needed to make 0.1 <= abs(number) < 1 +local function findDecimalOffset(number) + if number == 0 then + return 0 + end + + local offsetToOnesRange = math.floor(math.log10(math.abs(number))) + return -(offsetToOnesRange + 1) -- Offset one more (or less) digit +end + +local function roundToSignificantDigits(number, significantDigits, roundingBehaviour) + local offset = findDecimalOffset(number) + local multiplier = 10^(significantDigits + offset) + local significand + if roundingBehaviour == RoundingBehaviour.Truncate then + significand = math.modf(number * multiplier) + else + significand = math.floor(number * multiplier + 0.5) + end + return significand / multiplier; +end + +local function addGroupDelimiters(numberStr, delimiter) + local formatted = numberStr + local delimiterSubStr = string.format("%%1%s%%2", delimiter) + while true do + local lFormatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", delimiterSubStr) + formatted = lFormatted + if k == 0 then + break + end + end + return formatted +end + +local function findDenominationEntry(localeInfo, number, roundingBehaviour) + local denominationEntry = localeInfo[1] -- Default to base denominations + local absOfNumber = math.abs(number) + for i = #localeInfo, 2, -1 do + local entry = localeInfo[i] + local baseValue + if roundingBehaviour == RoundingBehaviour.Truncate then + baseValue = entry[1] + else + baseValue = entry[1] - (localeInfo[i - 1][1]) / 2 + end + if baseValue <= absOfNumber then + denominationEntry = entry + break + end + end + return denominationEntry +end + +local NumberLocalization = { } + +function NumberLocalization.localize(number, locale) + if number == 0 then + return "0" + end + + local localeInfo = localeInfos[locale] + if not localeInfo then + localeInfo = localeInfos[DEFAULT_LOCALE] + Logging.warn(string.format("Warning: Locale not found: '%s', reverting to '%s' instead.", + tostring(locale), DEFAULT_LOCALE)) + end + + if localeInfo.groupDelimiter then + return addGroupDelimiters(number, localeInfo.groupDelimiter) + end + + return number +end + +function NumberLocalization.abbreviate(number, locale, roundingBehaviour) + if number == 0 then + return "0" + end + + if roundingBehaviour == nil then + roundingBehaviour = RoundingBehaviour.RoundToClosest + end + + local localeInfo = localeInfos[locale] + if not localeInfo then + localeInfo = localeInfos[DEFAULT_LOCALE] + Logging.warn(string.format("Warning: Locale not found: '%s', reverting to '%s' instead.", + tostring(locale), DEFAULT_LOCALE)) + end + + -- select which denomination we are going to use + local denominationEntry = findDenominationEntry(localeInfo, number, roundingBehaviour) + local baseValue = denominationEntry[1] + local symbol = denominationEntry[2] + + -- Round to required significant digits + local significantQuotient = roundToSignificantDigits(number / baseValue, 3, roundingBehaviour) + + -- trim to 1 decimal point + local trimmedQuotient + if roundingBehaviour == RoundingBehaviour.Truncate then + trimmedQuotient = math.modf(significantQuotient * 10) / 10 + else + trimmedQuotient = math.floor(significantQuotient * 10 + 0.5) / 10 + end + local trimmedQuotientString = tostring(trimmedQuotient) + + -- Split the string into integer and fraction parts + local decimalPointIndex = findDecimalPointIndex(trimmedQuotientString) + local integerPart = string.sub(trimmedQuotientString, 1, decimalPointIndex - 1) + local fractionPart = string.sub(trimmedQuotientString, decimalPointIndex + 1, #trimmedQuotientString) + + -- Add group delimiters to integer part + if localeInfo.groupDelimiter then + integerPart = addGroupDelimiters(integerPart, localeInfo.groupDelimiter) + end + + if #fractionPart > 0 then + return integerPart .. localeInfo.decimalSeparator .. fractionPart .. symbol + else + return integerPart .. symbol + end +end + +return NumberLocalization diff --git a/Client2020/ExtraContent/LuaPackages/Localization/NumberLocalization.spec.lua b/Client2020/ExtraContent/LuaPackages/Localization/NumberLocalization.spec.lua new file mode 100644 index 0000000..fbc40ac --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Localization/NumberLocalization.spec.lua @@ -0,0 +1,110 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Logging = require(CorePackages.Logging) + local NumberLocalization = require(CorePackages.Localization.NumberLocalization) + + local RoundingBehaviour = require(script.Parent.RoundingBehaviour) + + local function checkLocale(locale, responseMapping) + for input, output in pairs(responseMapping) do + expect(NumberLocalization.localize(input, locale)).to.equal(output) + end + end + + local function checkValid_en_zh(locale) + checkLocale(locale, { + [0] = "0", + [1] = "1", + [25] = "25", + [364] = "364", + [4120] = "4,120", + [57860] = "57,860", + [624390] = "624,390", + [7857000] = "7,857,000", + [-12345678] = "-12,345,678", + [23987.45678] = "23,987.45678", + [-12.3456] = "-12.3456", + [-23987.45678] = "-23,987.45678", + }) + end + + describe("NumberLocalization.localize", function() + it("should default to en-us when locale is not recognized", function() + local logs = Logging.capture(function() + checkValid_en_zh("bad_locale") + end) + expect(string.match(logs.warnings[1], "^Warning: Locale not found:") ~= nil).to.equal(true) + end) + + it("should default to en-us when locale is nil", function() + local logs = Logging.capture(function() + checkValid_en_zh(nil) + end) + expect(string.match(logs.warnings[1], "^Warning: Locale not found:") ~= nil).to.equal(true) + end) + + it("should default to en-us when locale is empty", function() + local logs = Logging.capture(function() + checkValid_en_zh("") + end) + expect(string.match(logs.warnings[1], "^Warning: Locale not found:") ~= nil).to.equal(true) + end) + + it("should localize correctly. (en-us)", function() + checkValid_en_zh("en-us") + end) + + it("should localize correctly. (en-gb)", function() + checkValid_en_zh("en-gb") + end) + + it("should localize correctly. (zh-cn)", function() + checkValid_en_zh("zh-cn") + end) + + it("should localize correctly. (zh-tw)", function() + checkValid_en_zh("zh-tw") + end) + end) + + describe("NumberLocalization.abbreviate", function() + it("should round towards zero when using RoundingBehaviour.Truncate", function() + local roundToZeroMap = { + [0] = "0", + [1] = "1", + [25] = "25", + [364] = "364", + [4120] = "4.1K", + [57860] = "57.8K", + [624390] = "624K", + [999999] = "999K", + [7857000] = "7.8M", + [8e7] = "80M", + [9e8] = "900M", + [1e9] = "1B", + [1e12] = "1,000B", + [-0] = "0", + [-1] = "-1", + [-25] = "-25", + [-364] = "-364", + [-4120] = "-4.1K", + [-57860] = "-57.8K", + [-624390] = "-624K", + [-999999] = "-999K", + [-7857000] = "-7.8M", + [-8e7] = "-80M", + [-9e8] = "-900M", + [-1e9] = "-1B", + [-1e12] = "-1,000B", + [1.1] = "1.1", + [1499.99] = "1.4K", + [-1.1] = "-1.1", + [-1499.99] = "-1.4K", + } + + for input, output in pairs(roundToZeroMap) do + expect(NumberLocalization.abbreviate(input, "en-us", RoundingBehaviour.Truncate)).to.equal(output) + end + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Localization/RoundingBehaviour.lua b/Client2020/ExtraContent/LuaPackages/Localization/RoundingBehaviour.lua new file mode 100644 index 0000000..edcab47 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Localization/RoundingBehaviour.lua @@ -0,0 +1,7 @@ +local CorePackages = game:GetService("CorePackages") +local enumerate = require(CorePackages.enumerate) + +return enumerate("RoundingBehaviour", { + "RoundToClosest", + "Truncate", +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Localization/withLocalization.lua b/Client2020/ExtraContent/LuaPackages/Localization/withLocalization.lua new file mode 100644 index 0000000..4d5129c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Localization/withLocalization.lua @@ -0,0 +1,19 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) +local ArgCheck = require(CorePackages.ArgCheck) +local LocalizationConsumer = require(CorePackages.Localization.LocalizationConsumer) + +local function withLocalization(stringsToBeLocalized) + ArgCheck.isType(stringsToBeLocalized, "table", "stringsToBeLocalized passed to withLocalization()") + + return function(render) + ArgCheck.isType(render, "function", "render passed to withLocalization()") + + return Roact.createElement(LocalizationConsumer, { + render = render, + stringsToBeLocalized = stringsToBeLocalized, + }) + end +end + +return withLocalization \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Localization/withLocalization.spec.lua b/Client2020/ExtraContent/LuaPackages/Localization/withLocalization.spec.lua new file mode 100644 index 0000000..9a3dca2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Localization/withLocalization.spec.lua @@ -0,0 +1,4 @@ +return function() + itSKIP("SOC-6353 - These unit tests need to be moved from LuaApp", function() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Logging.lua b/Client2020/ExtraContent/LuaPackages/Logging.lua new file mode 100644 index 0000000..8826196 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Logging.lua @@ -0,0 +1,54 @@ +-- This is taken from Roact internal implementation details as a temporary helper +-- so that we can control warnings spew and also validate warning logs in unit tests. +-- We should clean it up and turn it into a nice central logging facility later on. + +local outputEnabled = true +local collectors = {} + +local function createLogInfo() + local logInfo = { + warnings = {}, + } + + setmetatable(logInfo, { + __tostring = function(self) + return ("LogInfo\n\tWarnings (%d):\n\t\t%s"):format( + #self.warnings, + table.concat(self.warnings, "\n\t\t") + ) + end, + }) + + return logInfo +end + +local Logging = {} + +function Logging.capture(callback) + local collector = createLogInfo() + + local wasOutputEnabled = outputEnabled + outputEnabled = false + collectors[collector] = true + + local success, result = pcall(callback) + + collectors[collector] = nil + outputEnabled = wasOutputEnabled + + assert(success, result) + + return collector +end + +function Logging.warn(message) + for collector in pairs(collectors) do + table.insert(collector.warnings, message) + end + + if outputEnabled then + warn(message) + end +end + +return Logging diff --git a/Client2020/ExtraContent/LuaPackages/LuaChatDeps.lua b/Client2020/ExtraContent/LuaPackages/LuaChatDeps.lua new file mode 100644 index 0000000..1647813 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/LuaChatDeps.lua @@ -0,0 +1,12 @@ + +--[[ + Proxy package for dependencies for LuaChat. +]] + +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.LuaChatDeps) diff --git a/Client2020/ExtraContent/LuaPackages/LuaDiscussionsDeps.lua b/Client2020/ExtraContent/LuaPackages/LuaDiscussionsDeps.lua new file mode 100644 index 0000000..0f0e860 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/LuaDiscussionsDeps.lua @@ -0,0 +1,12 @@ + +--[[ + Proxy package for dependencies for LuaDiscussions. +]] + +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.LuaDiscussionsDeps) diff --git a/Client2020/ExtraContent/LuaPackages/LuaSocialLibrariesDeps.lua b/Client2020/ExtraContent/LuaPackages/LuaSocialLibrariesDeps.lua new file mode 100644 index 0000000..d55def6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/LuaSocialLibrariesDeps.lua @@ -0,0 +1,12 @@ + +--[[ + Proxy package for dependencies for SocialLibraries. +]] + +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.LuaSocialLibrariesDeps) diff --git a/Client2020/ExtraContent/LuaPackages/Lumberyak.lua b/Client2020/ExtraContent/LuaPackages/Lumberyak.lua new file mode 100644 index 0000000..4dbd1d0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Lumberyak.lua @@ -0,0 +1,6 @@ +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Lumberyak) diff --git a/Client2020/ExtraContent/LuaPackages/Otter.lua b/Client2020/ExtraContent/LuaPackages/Otter.lua new file mode 100644 index 0000000..a20200b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Otter.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Otter) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/OtterApp.lua b/Client2020/ExtraContent/LuaPackages/OtterApp.lua new file mode 100644 index 0000000..4c60583 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/OtterApp.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.OtterApp) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/.robloxrc b/Client2020/ExtraContent/LuaPackages/Packages/.robloxrc new file mode 100644 index 0000000..6b52e84 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nocheck" + }, + "lint": { + "*": "disabled" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/AvatarExperienceDeps.lua b/Client2020/ExtraContent/LuaPackages/Packages/AvatarExperienceDeps.lua new file mode 100644 index 0000000..6d11e11 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/AvatarExperienceDeps.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["AvatarExperienceDeps"]["AvatarExperienceDeps"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/Cryo.lua new file mode 100644 index 0000000..1f2cc8e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/Dev/Rhodium.lua b/Client2020/ExtraContent/LuaPackages/Packages/Dev/Rhodium.lua new file mode 100644 index 0000000..6a51e47 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/Dev/Rhodium.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent._Index + +local package = PackageIndex["roblox_rhodium"]["rhodium"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/Dev/TestEZ.lua b/Client2020/ExtraContent/LuaPackages/Packages/Dev/TestEZ.lua new file mode 100644 index 0000000..edcea88 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/Dev/TestEZ.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent._Index + +local package = PackageIndex["roblox_testez"]["testez"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/InGameMenuDependencies.lua b/Client2020/ExtraContent/LuaPackages/Packages/InGameMenuDependencies.lua new file mode 100644 index 0000000..f218b9f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/InGameMenuDependencies.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["InGameMenuDependencies"]["InGameMenuDependencies"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/LuaChatDeps.lua b/Client2020/ExtraContent/LuaPackages/Packages/LuaChatDeps.lua new file mode 100644 index 0000000..6267110 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/LuaChatDeps.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["LuaChatDeps"]["LuaChatDeps"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/LuaDiscussionsDeps.lua b/Client2020/ExtraContent/LuaPackages/Packages/LuaDiscussionsDeps.lua new file mode 100644 index 0000000..7a47509 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/LuaDiscussionsDeps.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["LuaDiscussionsDeps"]["LuaDiscussionsDeps"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/LuaSocialLibrariesDeps.lua b/Client2020/ExtraContent/LuaPackages/Packages/LuaSocialLibrariesDeps.lua new file mode 100644 index 0000000..e2deffb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/LuaSocialLibrariesDeps.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["LuaSocialLibrariesDeps"]["LuaSocialLibrariesDeps"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/Lumberyak.lua b/Client2020/ExtraContent/LuaPackages/Packages/Lumberyak.lua new file mode 100644 index 0000000..46171d0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/Lumberyak.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_lumberyak"]["lumberyak"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/Otter.lua b/Client2020/ExtraContent/LuaPackages/Packages/Otter.lua new file mode 100644 index 0000000..e06526e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/PolicyProvider.lua b/Client2020/ExtraContent/LuaPackages/Packages/PolicyProvider.lua new file mode 100644 index 0000000..bd36e09 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/PolicyProvider.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_lua-roact-policy-provider"]["lua-roact-policy-provider"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/PremiumUpsellDeps.lua b/Client2020/ExtraContent/LuaPackages/Packages/PremiumUpsellDeps.lua new file mode 100644 index 0000000..a146a68 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/PremiumUpsellDeps.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["PremiumUpsellDeps"]["PremiumUpsellDeps"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/Promise.lua b/Client2020/ExtraContent/LuaPackages/Packages/Promise.lua new file mode 100644 index 0000000..b4ec544 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/Promise.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["lua-promise"]["lua-promise"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/PurchasePrompt.lua b/Client2020/ExtraContent/LuaPackages/Packages/PurchasePrompt.lua new file mode 100644 index 0000000..de7dba5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/PurchasePrompt.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_purchase-prompt"]["purchase-prompt"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/Result.lua b/Client2020/ExtraContent/LuaPackages/Packages/Result.lua new file mode 100644 index 0000000..b97b708 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/Result.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_lua-result"]["lua-result"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/Roact.lua b/Client2020/ExtraContent/LuaPackages/Packages/Roact.lua new file mode 100644 index 0000000..3edd80d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/RoactGamepad.lua b/Client2020/ExtraContent/LuaPackages/Packages/RoactGamepad.lua new file mode 100644 index 0000000..91d3905 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/RoactGamepad.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_roact-gamepad"]["roact-gamepad"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/RoactNavigation.lua b/Client2020/ExtraContent/LuaPackages/Packages/RoactNavigation.lua new file mode 100644 index 0000000..98c9312 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/RoactNavigation.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_roact-navigation"]["roact-navigation"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/RoactRodux.lua b/Client2020/ExtraContent/LuaPackages/Packages/RoactRodux.lua new file mode 100644 index 0000000..be5aab0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/RoactRodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_roact-rodux"]["roact-rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/Rodux.lua b/Client2020/ExtraContent/LuaPackages/Packages/Rodux.lua new file mode 100644 index 0000000..8917670 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/Rodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_rodux"]["rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/StringUtilities.lua b/Client2020/ExtraContent/LuaPackages/Packages/StringUtilities.lua new file mode 100644 index 0000000..4616eca --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/StringUtilities.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_string-utilities"]["string-utilities"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/UIBlox.lua b/Client2020/ExtraContent/LuaPackages/Packages/UIBlox.lua new file mode 100644 index 0000000..3243f95 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/UIBlox.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["UIBlox"]["UIBlox"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/UrlBuilder.lua b/Client2020/ExtraContent/LuaPackages/Packages/UrlBuilder.lua new file mode 100644 index 0000000..15958c3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/UrlBuilder.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_url-builder"]["url-builder"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/AvatarExperienceDeps/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/AvatarExperienceDeps/init.lua new file mode 100644 index 0000000..9055f30 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/AvatarExperienceDeps/init.lua @@ -0,0 +1,5 @@ +local AvatarExperineceDeps = script.Parent + +return { + RoactFitComponents = require(AvatarExperineceDeps.RoactFitComponents), +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/RoactFitComponents.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/RoactFitComponents.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/RoactFitComponents.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/lock.toml new file mode 100644 index 0000000..a71c2c3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/lock.toml @@ -0,0 +1,6 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "AvatarExperienceDeps" +version = "0.0.1" +commit = "5a31b41867d2c5ebf0b5670d9be27f3edd403545" +source = "git+https://github.com/roblox/avatar-experience-deps#master" +dependencies = ["RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components"] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/InGameMenuDependencies/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/InGameMenuDependencies/init.lua new file mode 100644 index 0000000..19038db --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/InGameMenuDependencies/init.lua @@ -0,0 +1,12 @@ +local InGameMenuDependencies = script.Parent + +return { + Roact = require(InGameMenuDependencies.Roact), + Rodux = require(InGameMenuDependencies.Rodux), + RoactRodux = require(InGameMenuDependencies.RoactRodux), + UIBlox = require(InGameMenuDependencies.UIBlox), + Otter = require(InGameMenuDependencies.Otter), + Cryo = require(InGameMenuDependencies.Cryo), + t = require(InGameMenuDependencies.t), + PolicyProvider = require(InGameMenuDependencies.PolicyProvider), +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Otter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/PolicyProvider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/PolicyProvider.lua new file mode 100644 index 0000000..5924c43 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/PolicyProvider.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_lua-roact-policy-provider"]["lua-roact-policy-provider"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Roact.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/RoactRodux.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/RoactRodux.lua new file mode 100644 index 0000000..1b8d1c2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/RoactRodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-rodux"]["roact-rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Rodux.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Rodux.lua new file mode 100644 index 0000000..96b67df --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Rodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_rodux"]["rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/UIBlox.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/UIBlox.lua new file mode 100644 index 0000000..2e0dcc2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/UIBlox.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["UIBlox"]["UIBlox"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/lock.toml new file mode 100644 index 0000000..fa14534 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/lock.toml @@ -0,0 +1,15 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "InGameMenuDependencies" +version = "0.1.0" +commit = "aecc56c1fa6348886a1073785dbb196b655a0a90" +source = "git+https://github.rbx.com/roblox/in-game-menu-dependencies#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter", + "PolicyProvider roblox/lua-roact-policy-provider 1d595dae git+https://github.com/roblox/lua-roact-policy-provider#master", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "RoactRodux roblox/roact-rodux 0.2.2 url+https://github.com/roblox/roact-rodux", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "UIBlox UIBlox a253d523 git+https://github.com/roblox/uiblox#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/t.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/AssetCard.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/AssetCard.lua new file mode 100644 index 0000000..0ee5c1d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/AssetCard.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["asset-card"]["asset-card"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/InfiniteScroller.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/InfiniteScroller.lua new file mode 100644 index 0000000..6ea364a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/InfiniteScroller.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_infinite-scroller-98304e77-0.5.6"]["infinite-scroller"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/LuaChatDeps/LuaChatDeps.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/LuaChatDeps/LuaChatDeps.spec.lua new file mode 100644 index 0000000..bb6a340 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/LuaChatDeps/LuaChatDeps.spec.lua @@ -0,0 +1,6 @@ +return function() + it("SHOULD initialize UIBlox when required", function() + local LuaChatDeps = require(script.Parent) + expect(LuaChatDeps.UIBlox.Config).to.be.ok() + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/LuaChatDeps/config/UIBlox.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/LuaChatDeps/config/UIBlox.lua new file mode 100644 index 0000000..4f31f2e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/LuaChatDeps/config/UIBlox.lua @@ -0,0 +1,4 @@ +return { + fixToastResizeConfig = true, + expandableTextAutomaticResizeConfig = true, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/LuaChatDeps/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/LuaChatDeps/init.lua new file mode 100644 index 0000000..bb93558 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/LuaChatDeps/init.lua @@ -0,0 +1,14 @@ +local LuaChatDeps = script.Parent + +-- initialize UIBlox once here, there should be no other +-- consumers of this instance of UIBlox +local UIBloxConfig = require(script.config.UIBlox) +local UIBlox = require(LuaChatDeps.UIBlox) +UIBlox.init(UIBloxConfig) + +return { + InfiniteScroll = require(LuaChatDeps.InfiniteScroller), + RoduxNetworking = require(LuaChatDeps.RoduxNetworking), + UIBlox = UIBlox, + AssetCard = require(LuaChatDeps.AssetCard), +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/RoduxNetworking.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/RoduxNetworking.lua new file mode 100644 index 0000000..4523682 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/RoduxNetworking.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["rodux-networking"]["rodux-networking"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/UIBlox.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/UIBlox.lua new file mode 100644 index 0000000..2e0dcc2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/UIBlox.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["UIBlox"]["UIBlox"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/lock.toml new file mode 100644 index 0000000..d9f8d4e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/lock.toml @@ -0,0 +1,11 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "LuaChatDeps" +version = "0.1.3" +commit = "6144362fd181fe94fd213ec8569ba95f0a61faed" +source = "git+https://github.rbx.com/roblox/lua-chat-deps#master" +dependencies = [ + "AssetCard asset-card c7b683fb git+https://github.com/roblox/asset-card#v1.0.2", + "InfiniteScroller roblox/infinite-scroller 0.5.6 url+https://github.com/roblox/infinite-scroller", + "RoduxNetworking rodux-networking ea19cfe3 git+https://github.rbx.com/roblox/rodux-networking#v1.0.1", + "UIBlox UIBlox a253d523 git+https://github.com/roblox/uiblox#master", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/InfiniteScroll.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/InfiniteScroll.lua new file mode 100644 index 0000000..efa0054 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/InfiniteScroll.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_infinite-scroller-98304e77-0.3.4"]["infinite-scroller"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/LuaDiscussionsDeps/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/LuaDiscussionsDeps/init.lua new file mode 100644 index 0000000..6313dbe --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/LuaDiscussionsDeps/init.lua @@ -0,0 +1,8 @@ +local LuaDiscussionsDeps = script.Parent + +return { + InfiniteScroll = require(LuaDiscussionsDeps.InfiniteScroll), + RoactFitComponents = require(LuaDiscussionsDeps.RoactFitComponents), + RoactNavigation = require(LuaDiscussionsDeps.RoactNavigation), + RoduxNetworking = require(LuaDiscussionsDeps.RoduxNetworking), +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoactFitComponents.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoactFitComponents.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoactFitComponents.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoactNavigation.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoactNavigation.lua new file mode 100644 index 0000000..8698791 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoactNavigation.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roact-navigation"]["roact-navigation"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoduxNetworking.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoduxNetworking.lua new file mode 100644 index 0000000..4523682 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoduxNetworking.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["rodux-networking"]["rodux-networking"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/lock.toml new file mode 100644 index 0000000..52175aa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/lock.toml @@ -0,0 +1,11 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "LuaDiscussionsDeps" +version = "0.1.2" +commit = "dfba285e3dd4e0fe5059b93d52880666e2924612" +source = "git+https://github.rbx.com/roblox/lua-discussions-deps#master" +dependencies = [ + "InfiniteScroll roblox/infinite-scroller 0.3.4 url+https://github.com/roblox/infinite-scroller", + "RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "RoactNavigation roact-navigation 0.1.1-linter-fix url+https://github.com/roblox/roact-navigation", + "RoduxNetworking rodux-networking ea19cfe3 git+https://github.rbx.com/roblox/rodux-networking#v1.0.1", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/GenericPagination.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/GenericPagination.lua new file mode 100644 index 0000000..f367c51 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/GenericPagination.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_genericpagination"]["genericpagination"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/LuaSocialLibrariesDeps/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/LuaSocialLibrariesDeps/init.lua new file mode 100644 index 0000000..ddfdbd7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/LuaSocialLibrariesDeps/init.lua @@ -0,0 +1,7 @@ +local LuaSocialLibrariesDeps = script.Parent + +return { + GenericPagination = require(LuaSocialLibrariesDeps.GenericPagination), + RoactFitComponents = require(LuaSocialLibrariesDeps.RoactFitComponents), + Mock = require(LuaSocialLibrariesDeps.Mock), +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/Mock.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/Mock.lua new file mode 100644 index 0000000..428f95d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/Mock.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["jtaylor_mock"]["mock"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/RoactFitComponents.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/RoactFitComponents.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/RoactFitComponents.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/lock.toml new file mode 100644 index 0000000..84be2f6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/lock.toml @@ -0,0 +1,10 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "LuaSocialLibrariesDeps" +version = "0.1.1" +commit = "6d4213307f9d059e422026c5f8ad1a37a3489a9b" +source = "git+https://github.rbx.com/roblox/lua-social-libraries-deps#master" +dependencies = [ + "GenericPagination roblox/genericpagination 6cc56178 git+https://github.rbx.com/roblox/genericpagination#master", + "Mock jtaylor/mock d2c4005c git+https://github.rbx.com/roblox/mock#master", + "RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/PremiumUpsellDeps/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/PremiumUpsellDeps/init.lua new file mode 100644 index 0000000..9311ba2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/PremiumUpsellDeps/init.lua @@ -0,0 +1,5 @@ +local PremiumUpsellDeps = script.Parent + +return { + RoactFitComponents = require(PremiumUpsellDeps.RoactFitComponents), +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/RoactFitComponents.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/RoactFitComponents.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/RoactFitComponents.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/lock.toml new file mode 100644 index 0000000..9d244da --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/lock.toml @@ -0,0 +1,6 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "PremiumUpsellDeps" +version = "0.0.0" +commit = "c51030b70236f2e9d69bb34348b7e045c2c437f6" +source = "git+https://github.com/roblox/premium-upsell-deps#master" +dependencies = ["RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components"] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/FitFrame.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/FitFrame.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/FitFrame.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/InfiniteScroller.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/InfiniteScroller.lua new file mode 100644 index 0000000..6ea364a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/InfiniteScroller.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_infinite-scroller-98304e77-0.5.6"]["infinite-scroller"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Otter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Roact.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/RoactGamepad.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/RoactGamepad.lua new file mode 100644 index 0000000..2696d48 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/RoactGamepad.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-gamepad"]["roact-gamepad"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Accordion/AccordionView.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Accordion/AccordionView.lua new file mode 100644 index 0000000..bf068fa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Accordion/AccordionView.lua @@ -0,0 +1,314 @@ +local AccordionRoot = script.Parent +local AppRoot = AccordionRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local SpringAnimatedItem = require(UIBloxRoot.Utility.SpringAnimatedItem) + +local ITEM_PADDING = 10 +local ITEM_WIDTH_SHRINK_STEP = 20 -- How much each item shrinks below card above it +local COMPACT_VIEW_PLACEHOLDER_HEIGHT = 10 +local PRESSED_SCALE = 0.9 + +local ANIMATION_SPRING_SETTINGS = { + dampingRatio = 1, + frequency = 3.5, +} + +local AccordionView = Roact.PureComponent:extend("AccordionView") + +AccordionView.defaultProps = { + maxItemsInCompactView = 3, +} + +local validateProps = t.strictInterface({ + items = t.table, + itemWidth = t.number, + itemHeight = t.number, + renderItem = t.callback, + + placeholderColor = t.Color3, + placeholderBaseTransparency = t.number, + + collapseButtonSize = t.number, + renderCollapseButton = t.callback, + + LayoutOrder = t.optional(t.integer), + maxItemsInCompactView = t.numberPositive, +}) + +function AccordionView:init() + self.state = { + expanded = false, + isExpandButtonPressed = false, + } + + self.onExpandButtonActivated = function() + self:setState({ + expanded = true, + isExpandButtonPressed = false, + }) + end + + self.onCollapseButtonActivated = function() + self:setState({ + expanded = false, + }) + end + + self.onExpandButtonInputBegan = function(_, inputObject) + if inputObject.UserInputState == Enum.UserInputState.Begin and + (inputObject.UserInputType == Enum.UserInputType.Touch or + inputObject.UserInputType == Enum.UserInputType.MouseButton1) then + self:setState({ + isExpandButtonPressed = true, + }) + end + end + + self.onExpandButtonInputEnded = function() + if self.state.isExpandButtonPressed then + self:setState({ + isExpandButtonPressed = false, + }) + end + end + + self.rootFrameRef = Roact.createRef() + + self.onListLayoutAbsoluteContentSizeChanged = function(rbx) + if self.rootFrameRef.current then + local itemWidth = self.props.itemWidth + local minimumHeight = self:getCompactTotalHeight() + + self.rootFrameRef.current.Size = UDim2.new(0, itemWidth, + 0, math.max(rbx.AbsoluteContentSize.Y, minimumHeight)) + end + end +end + +function AccordionView:getCompactTotalHeight() + local items = self.props.items + local itemHeight = self.props.itemHeight + local maxItemsInCompactView = self.props.maxItemsInCompactView + local totalNumberOfItems = #items + + if totalNumberOfItems == 0 then + return 0 + else + return itemHeight + (math.min(maxItemsInCompactView, totalNumberOfItems) - 1) * COMPACT_VIEW_PLACEHOLDER_HEIGHT + end +end + +function AccordionView:getLayoutInfo() + local items = self.props.items + local itemWidth = self.props.itemWidth + local itemHeight = self.props.itemHeight + local placeholderBaseTransparency = self.props.placeholderBaseTransparency + local maxItemsInCompactView = self.props.maxItemsInCompactView + local expanded = self.state.expanded + + local layoutData = {} + local totalNumberOfItems = #items + local itemsShownInCompactView = math.min(maxItemsInCompactView, totalNumberOfItems) + + local placeholderTransparencyStep = 0 + if itemsShownInCompactView > 1 then + placeholderTransparencyStep = (1 - placeholderBaseTransparency) / (itemsShownInCompactView - 1) + end + + for index = 1, totalNumberOfItems do + if expanded then + layoutData[index] = { + width = itemWidth, + height = itemHeight, + placeholderTransparency = 1, + itemTransparency = 0, + } + else + if index == 1 then + layoutData[index] = { + width = itemWidth, + height = itemHeight, + placeholderTransparency = 1, + itemTransparency = 0, + } + elseif index <= maxItemsInCompactView then + layoutData[index] = { + width = itemWidth - ITEM_WIDTH_SHRINK_STEP * (index - 1), + height = COMPACT_VIEW_PLACEHOLDER_HEIGHT, + placeholderTransparency = placeholderBaseTransparency + placeholderTransparencyStep * (index - 2), + itemTransparency = 1, + } + else + layoutData[index] = { + width = itemWidth - ITEM_WIDTH_SHRINK_STEP * (index - 1), + height = 0, + placeholderTransparency = 1, + itemTransparency = 1, + } + end + end + end + + return layoutData +end + +function AccordionView:render() + assert(validateProps(self.props)) + + local items = self.props.items + local totalNumberOfItems = #items + + if totalNumberOfItems == 0 then + return nil + end + + local layoutOrder = self.props.LayoutOrder + local itemWidth = self.props.itemWidth + local renderItem = self.props.renderItem + local placeholderColor = self.props.placeholderColor + local collapseButtonSize = self.props.collapseButtonSize + local renderCollapseButton = self.props.renderCollapseButton + + local expanded = self.state.expanded + local isExpandButtonPressed = self.state.isExpandButtonPressed + + local layoutData = self:getLayoutInfo() + + local accordionContent = { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, expanded and ITEM_PADDING or 0), + [Roact.Change.AbsoluteContentSize] = self.onListLayoutAbsoluteContentSizeChanged, + }), + Scaler = Roact.createElement(SpringAnimatedItem.AnimatedUIScale, { + springOptions = ANIMATION_SPRING_SETTINGS, + animatedValues = { + scale = isExpandButtonPressed and PRESSED_SCALE or 1, + }, + mapValuesToProps = function(values) + return { + Scale = values.scale, + } + end, + }), + CollapseButton = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + springOptions = ANIMATION_SPRING_SETTINGS, + animatedValues = { + -- Increase the size by 1 pixel so the animation looks better + -- when the spring is damping in the end. + sizeOffsetY = expanded and collapseButtonSize + 1 or 0, + }, + mapValuesToProps = function(values) + return { + Size = UDim2.new(0, collapseButtonSize, 0, values.sizeOffsetY), + } + end, + regularProps = { + BackgroundTransparency = 1, + ClipsDescendants = true, + [Roact.Children] = { + ButtonMoveContainer = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + springOptions = ANIMATION_SPRING_SETTINGS, + animatedValues = { + positionOffsetY = expanded and 0 or collapseButtonSize / 2, + }, + mapValuesToProps = function(values) + return { + Position = UDim2.new(0, 0, 0, values.positionOffsetY), + } + end, + regularProps = { + Size = UDim2.new(0, collapseButtonSize, 0, collapseButtonSize), + BackgroundTransparency = 1, + [Roact.Children] = { + Button = renderCollapseButton(self.onCollapseButtonActivated), + }, + }, + }), + }, + }, + }), + } + + for index, _ in ipairs(items) do + local layout = layoutData[index] + + accordionContent["Item" .. tostring(index)] = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + springOptions = ANIMATION_SPRING_SETTINGS, + animatedValues = { + width = layout.width, + height = layout.height, + }, + mapValuesToProps = function(values) + return { + Size = UDim2.new(0, values.width, 0, values.height), + } + end, + regularProps = { + Size = UDim2.new(1, 0, 0, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = index + 1, + ZIndex = totalNumberOfItems + 1 - index; + ClipsDescendants = true, + [Roact.Children] = { + Item = renderItem(items[index], layout.itemTransparency, ANIMATION_SPRING_SETTINGS), + Placeholder = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + springOptions = ANIMATION_SPRING_SETTINGS, + animatedValues = { + transparency = layout.placeholderTransparency, + }, + mapValuesToProps = function(values) + return { + BackgroundTransparency = values.transparency, + } + end, + regularProps = { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = placeholderColor, + BorderSizePixel = 0, + }, + }), + }, + }, + }) + end + + local canExpand = (totalNumberOfItems > 1) + local clickToExpand = canExpand and not expanded + + return Roact.createElement("Frame", { + Size = UDim2.new(0, itemWidth, 0, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + [Roact.Ref] = self.rootFrameRef, + }, { + ContentFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + }, + accordionContent + ), + ClickToExpandButton = clickToExpand and Roact.createElement("TextButton", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ZIndex = totalNumberOfItems + 1, + Text = "", + [Roact.Event.Activated] = self.onExpandButtonActivated, + [Roact.Event.InputBegan] = self.onExpandButtonInputBegan, + [Roact.Event.InputEnded] = self.onExpandButtonInputEnded, + }), + }) +end + +return AccordionView diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Accordion/AccordionView.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Accordion/AccordionView.spec.lua new file mode 100644 index 0000000..2a13ab2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Accordion/AccordionView.spec.lua @@ -0,0 +1,72 @@ +return function() + local AccordionRoot = script.Parent + local AppRoot = AccordionRoot.Parent + local UIBloxRoot = AppRoot.Parent + local Packages = UIBloxRoot.Parent + local AccordionView = require(AccordionRoot.AccordionView) + local Roact = require(Packages.Roact) + + describe("AccordionView", function() + it("should mount correctly", function() + local element = Roact.createElement(AccordionView, { + items = {"test", "test2"}, + itemWidth = 355, + itemHeight = 188, + renderItem = function(item, transparency) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Text = item, + BackgroundTransparency = transparency, + }) + end, + placeholderColor = Color3.fromRGB(255, 255, 255), + placeholderBaseTransparency = 0.5, + collapseButtonSize = 40, + renderCollapseButton = function(activatedCallback) + return Roact.createElement("TextButton", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Text = "close", + AutoButtonColor = false, + [Roact.Event.Activated] = activatedCallback, + }) + end, + }) + + local instance = Roact.mount(element) + + Roact.unmount(instance) + end) + + it("should mount correctly with empty items", function() + local element = Roact.createElement(AccordionView, { + items = {}, + itemWidth = 355, + itemHeight = 188, + renderItem = function(item, transparency) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Text = item, + BackgroundTransparency = transparency, + }) + end, + placeholderColor = Color3.fromRGB(255, 255, 255), + placeholderBaseTransparency = 0.5, + collapseButtonSize = 40, + renderCollapseButton = function(activatedCallback) + return Roact.createElement("TextButton", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Text = "close", + AutoButtonColor = false, + [Roact.Event.Activated] = activatedCallback, + }) + end, + }) + + local instance = Roact.mount(element) + + Roact.unmount(instance) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/FullscreenTitleBar.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/FullscreenTitleBar.lua new file mode 100644 index 0000000..58e4f04 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/FullscreenTitleBar.lua @@ -0,0 +1,221 @@ +local Bar = script.Parent +local App = Bar.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Otter = require(Packages.Otter) +local t = require(Packages.t) + +local IconButton = require(App.Button.IconButton) +local Images = require(App.ImageSet.Images) +local IconSize = require(App.ImageSet.Enum.IconSize) +local getIconSize = require(App.ImageSet.getIconSize) +local ControlState = require(Packages.UIBlox.Core.Control.Enum.ControlState) +local withStyle = require(UIBlox.Core.Style.withStyle) +local ThreeSectionBar = require(UIBlox.Core.Bar.ThreeSectionBar) + +local lerp = require(Packages.UIBlox.Utility.lerp) +local divideTransparency = require(Packages.UIBlox.Utility.divideTransparency) + +local TITLE_BAR_HEIGHT = 64 +local SHADOW_HEIGHT = 24 +local MARGIN = 20 +local PADDING_BETWEEN = 12 + +local TITLE_BAR_OFF_POS = UDim2.new(0, 0, 0, -(TITLE_BAR_HEIGHT + SHADOW_HEIGHT)) +local TITLE_BAR_ON_POS = UDim2.fromOffset(0, 0) + +local EXIT_BUTTON_IMAGE_ID = "icons/actions/previewShrink" +local CLOSE_BUTTON_IMAGE_ID = "icons/navigation/close" + +local GRADIENT_OPACITY = 0.25 + +local MOTOR_OPTIONS = { + frequency = 5, +} + +local FullscreenTitleBar = Roact.PureComponent:extend("FullscreenTitleBar") + +FullscreenTitleBar.validateProps = t.strictInterface({ + title = t.string, + isTriggered = t.optional(t.boolean), + onDisappear = t.optional(t.callback), + + exitFullscreen = t.optional(t.callback), + closeRoblox = t.optional(t.callback), +}) + +function FullscreenTitleBar:init() + local initProgress = self.props.isTriggered and 1 or 0 + local setProgress + self.progress, setProgress = Roact.createBinding(initProgress) + + self.exitControlState, self.setExitControlState = Roact.createBinding(self.state.exitControlState) + self.closeControlState, self.setCloseControlState = Roact.createBinding(self.state.closeControlState) + + self.titleBarPosition = self.progress:map(function(value) + return TITLE_BAR_OFF_POS:lerp(TITLE_BAR_ON_POS, value) + end) + + self.progressMotor = Otter.createSingleMotor(initProgress) + self.progressMotor:onStep(setProgress) +end + +function FullscreenTitleBar:render() + return withStyle(function(style) + local theme = style.Theme + local font = style.Font + + local backgroundStyle = theme.BackgroundUIDefault + local textColorStyle = theme.TextEmphasis + + local centerTextFont = font.Header2 + local centerTextSize = centerTextFont.RelativeSize * font.BaseSize + + local titleBarTransparency = self.progress:map(function(value) + local baseTransparency = backgroundStyle.Transparency + return lerp(1, baseTransparency, value) + end) + + local exitButtonTransparency = Roact.joinBindings({ + progress = self.progress, + controlState = self.exitControlState, + }):map(function(values) + local baseTransparency = theme.ContextualPrimaryDefault.Transparency + local transparencyDivisor = values.controlState == ControlState.Pressed and 2 or 1 + return lerp(1, divideTransparency(baseTransparency, transparencyDivisor), values.progress) + end) + + local closeButtonTransparency = Roact.joinBindings({ + progress = self.progress, + controlState = self.closeControlState, + }):map(function(values) + local baseTransparency = theme.ContextualPrimaryDefault.Transparency + local transparencyDivisor = values.controlState == ControlState.Pressed and 2 or 1 + return lerp(1, divideTransparency(baseTransparency, transparencyDivisor), values.progress) + end) + + local textTransparency = self.progress:map(function(value) + local baseTransparency = textColorStyle.Transparency + return lerp(1, baseTransparency, value) + end) + + local function renderCenterText() + return Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Font = centerTextFont.Font, + Size = UDim2.new(1, 0, 0, centerTextSize), + Text = self.props.title, + TextColor3 = textColorStyle.Color, + TextSize = centerTextSize, + TextTransparency = textTransparency, + TextTruncate = Enum.TextTruncate.AtEnd, + TextWrapped = false, + }) + end + + local function renderRightButtons() + return Roact.createFragment({ + ExitButton = Roact.createElement(IconButton, { + icon = Images[EXIT_BUTTON_IMAGE_ID], + iconSize = IconSize.Medium, + iconTransparency = exitButtonTransparency, + onActivated = self.props.exitFullscreen, + layoutOrder = 1, + onStateChanged = function(oldState, newState) + self.setExitControlState(newState) + end + }), + CloseButton = Roact.createElement(IconButton, { + icon = Images[CLOSE_BUTTON_IMAGE_ID], + iconSize = IconSize.Medium, + iconTransparency = closeButtonTransparency, + onActivated = self.props.closeRoblox, + layoutOrder = 2, + onStateChanged = function(oldState, newState) + self.setCloseControlState(newState) + end + }), + }) + end + + local function renderMirrorButtons() + local iconSize = getIconSize(IconSize.Medium) + return Roact.createFragment({ + Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(iconSize, iconSize), + }), + Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(iconSize, iconSize), + }), + }) + end + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Position = self.titleBarPosition, + Size = UDim2.new(1, 0, 0, TITLE_BAR_HEIGHT + SHADOW_HEIGHT), + }, { + BarFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Position = UDim2.fromOffset(0, 0), + Size = UDim2.new(1, 0, 0, TITLE_BAR_HEIGHT), + [Roact.Event.MouseLeave] = self.props.onDisappear, + }, { + ThreeSectionBar = Roact.createElement(ThreeSectionBar, { + BackgroundColor3 = backgroundStyle.Color, + BackgroundTransparency = titleBarTransparency, + barHeight = TITLE_BAR_HEIGHT, + marginLeft = MARGIN, + contentPaddingLeft = UDim.new(0, PADDING_BETWEEN), + renderLeft = renderMirrorButtons, + renderCenter = renderCenterText, + marginRight = MARGIN, + contentPaddingRight = UDim.new(0, PADDING_BETWEEN), + renderRight = renderRightButtons, + }), + }), + ShadowFrame = Roact.createElement("Frame", { + BackgroundTransparency = backgroundStyle.Transparency, + BackgroundColor3 = backgroundStyle.Color, + BorderSizePixel = 0, + Position = UDim2.fromOffset(0, TITLE_BAR_HEIGHT), + Size = UDim2.new(1, 0, 0, SHADOW_HEIGHT), + }, { + UIGradient = Roact.createElement("UIGradient", { + Rotation = 90, + Color = ColorSequence.new({ + ColorSequenceKeypoint.new(0, Color3.new(0, 0, 0)), + ColorSequenceKeypoint.new(1, Color3.new(0, 0, 0)), + }), + Transparency = NumberSequence.new({ + NumberSequenceKeypoint.new(0, 1 - GRADIENT_OPACITY), + NumberSequenceKeypoint.new(1, 1.0), + }), + }) + }) + }) + end) +end + +function FullscreenTitleBar:didMount() + self.progressMotor:start() +end + +function FullscreenTitleBar:didUpdate(prevProps, prevState) + if prevProps.isTriggered ~= self.props.isTriggered then + local newProgress = self.props.isTriggered and 1 or 0 + self.progressMotor:setGoal(Otter.spring(newProgress, MOTOR_OPTIONS)) + end +end + +function FullscreenTitleBar:willUnmount() + self.progressMotor:destroy() +end + +return FullscreenTitleBar diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/FullscreenTitleBar.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/FullscreenTitleBar.spec.lua new file mode 100644 index 0000000..c78526d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/FullscreenTitleBar.spec.lua @@ -0,0 +1,33 @@ +return function() + local Packages = script.Parent.Parent.Parent.Parent + local Roact = require(Packages.Roact) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + + local FullscreenTitleBar = require(script.Parent.FullscreenTitleBar) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + TestTitleBar = Roact.createElement(FullscreenTitleBar, { + title = "", + }) + }) + local instance = Roact.mount(element, nil, "FullscreenTitleBar") + Roact.unmount(instance) + end) + + it("should mount and unmount without issue with valid properties", function() + local element = mockStyleComponent({ + TestTitleBar = Roact.createElement(FullscreenTitleBar, { + title = "", + onDisappear = function() end, + isTriggered = false, + exitFullscreen = function() end, + closeRoblox = function() end, + }) + }) + local instance = Roact.mount(element, nil, "FullscreenTitleBar") + Roact.unmount(instance) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/HeaderBar.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/HeaderBar.lua new file mode 100644 index 0000000..f36b55d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/HeaderBar.lua @@ -0,0 +1,123 @@ +local Bar = script.Parent +local App = Bar.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(App.ImageSet.Images) +local IconSize = require(App.ImageSet.Enum.IconSize) +local getPageMargin = require(App.Container.getPageMargin) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local IconButton = require(UIBlox.App.Button.IconButton) +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) + +local ThreeSectionBar = require(UIBlox.Core.Bar.ThreeSectionBar) +local HeaderBar = Roact.PureComponent:extend("HeaderBar") +HeaderBar.renderLeft = { + backButton = function(onActivated) + return function(_) + return Roact.createElement(IconButton, { + size = UDim2.fromOffset(0, 0), + iconSize = IconSize.Medium, + icon = Images["icons/navigation/pushBack"], + onActivated = onActivated, + }) + end + end, +} + +HeaderBar.validateProps = t.strictInterface({ + -- The title of the screen + title = t.string, + + -- The function that is called when the back button is clicked + onBack = t.optional(t.callback), + + -- How tall the bar is + barHeight = t.optional(t.number), + + -- How much spacing between elements to allow on the right side of the bar + contentPaddingRight = t.optional(t.UDim), + + -- A function that returns a Roact Component, used for customizing buttons on the right side of the bar + renderRight = t.optional(t.callback), + renderLeft = t.optional(t.callback), + +}) + +-- default values are taken from Abstract +HeaderBar.defaultProps = { + barHeight = 48, + contentPaddingRight = UDim.new(0, 12), + renderRight = function() + return nil + end, + renderLeft = function() + return nil + end +} + +function HeaderBar:init() + self.state = { + margin = 0 + } + + self.onResize = function(rbx) + local margin = getPageMargin(rbx.AbsoluteSize.X) + self:setState({ + margin = margin + }) + end +end + +function HeaderBar:render() + return withStyle(function(style) + local theme = style.Theme + local font = style.Font + + local centerTextFontStyle = font.Header1 + local centerTextSize = centerTextFontStyle.RelativeSize * font.BaseSize + + local estimatedCenterWidth = GetTextSize( + self.props.title, + centerTextSize, + centerTextFontStyle.Font, + Vector2.new(1000, 1000) + ).X + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, self.props.barHeight), + [Roact.Change.AbsoluteSize] = self.onResize, + }, { + ThreeSectionBar = Roact.createElement(ThreeSectionBar, { + BackgroundTransparency = theme.BackgroundDefault.Transparency, + BackgroundColor3 = theme.BackgroundDefault.Color, + + barHeight = self.props.barHeight, + marginLeft = self.state.margin, + renderLeft = self.props.renderLeft, + renderCenter = function() + return Roact.createElement(GenericTextLabel, { + ClipsDescendants = true, + Size = UDim2.new(1, 0, 0, centerTextSize), + Text = self.props.title, + TextTruncate = Enum.TextTruncate.AtEnd, + TextWrapped = false, + fontStyle = centerTextFontStyle, + colorStyle = theme.TextEmphasis, + }) + end, + estimatedCenterWidth = estimatedCenterWidth, + marginRight = self.state.margin, + contentPaddingRight = self.props.contentPaddingRight, + renderRight = self.props.renderRight, + }) + }) + end) +end + +return HeaderBar diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/HeaderBar.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/HeaderBar.spec.lua new file mode 100644 index 0000000..4fcf0f9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/HeaderBar.spec.lua @@ -0,0 +1,114 @@ +return function() + local Bar = script.Parent + local App = Bar.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local HeaderBar = require(UIBlox.App.Bar.HeaderBar) + + local BARSIZE_SMALL = UDim2.new(0, 320, 0, 40) + local BARSIZE_MEDIUM = UDim2.new(0, 480, 0, 40) + local BARSIZE_LARGE = UDim2.new(0, 600, 0, 40) + local MARGIN_SMALL = 12 + local MARGIN_MEDIUM = 24 + local MARGIN_LARGE = 48 + + describe("lifecycle", function() + it("should mount and unmount without issues", function() + local element = mockStyleComponent({ + bar = Roact.createElement(HeaderBar, { + title = "Header Bar", + onBack = function() end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) + + describe("margin logic", function() + it("should have margin of 12 on small screens", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + barFrame = Roact.createElement("Frame", { + Size = UDim2.new(0, 0, 0, 0), + }, { + bar = Roact.createElement(HeaderBar, { + title = "Header Bar", + onBack = function() end, + }), + }) + }) + + local instance = Roact.mount(element, frame, "Frame") + local barFrame = frame:FindFirstChild("barFrame", true) + local bar = barFrame:FindFirstChild("bar") + local leftFrame = bar:FindFirstChild("leftFrame", true) + local margin = leftFrame:FindFirstChild("$margin", true) + expect(margin).to.be.ok() + + barFrame.Size = BARSIZE_SMALL + local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize] + expect(margin.PaddingLeft.Offset).to.equal(MARGIN_SMALL) + + Roact.unmount(instance) + end) + + it("should have margin of 24 on medium screens", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + barFrame = Roact.createElement("Frame", { + Size = UDim2.new(0, 0, 0, 0), + }, { + bar = Roact.createElement(HeaderBar, { + title = "Header Bar", + onBack = function() end, + }), + }) + }) + + local instance = Roact.mount(element, frame, "Frame") + local barFrame = frame:FindFirstChild("barFrame", true) + local bar = barFrame:FindFirstChild("bar") + local leftFrame = bar:FindFirstChild("leftFrame", true) + local margin = leftFrame:FindFirstChild("$margin", true) + expect(margin).to.be.ok() + + barFrame.Size = BARSIZE_MEDIUM + local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize] + expect(margin.PaddingLeft.Offset).to.equal(MARGIN_MEDIUM) + + Roact.unmount(instance) + end) + + it("should have margin of 48 on large screens", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + barFrame = Roact.createElement("Frame", { + Size = UDim2.new(0, 0, 0, 0), + }, { + bar = Roact.createElement(HeaderBar, { + title = "Header Bar", + onBack = function() end, + }), + }) + }) + + local instance = Roact.mount(element, frame, "Frame") + local barFrame = frame:FindFirstChild("barFrame", true) + local bar = barFrame:FindFirstChild("bar") + local leftFrame = bar:FindFirstChild("leftFrame", true) + local margin = leftFrame:FindFirstChild("$margin", true) + expect(margin).to.be.ok() + + barFrame.Size = BARSIZE_LARGE + local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize] + expect(margin.PaddingLeft.Offset).to.equal(MARGIN_LARGE) + + Roact.unmount(instance) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/RootHeaderBar.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/RootHeaderBar.lua new file mode 100644 index 0000000..db16e77 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/RootHeaderBar.lua @@ -0,0 +1,86 @@ +local Bar = script.Parent +local App = Bar.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local getPageMargin = require(App.Container.getPageMargin) + +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) + +local ThreeSectionBar = require(UIBlox.Core.Bar.ThreeSectionBar) +local RootHeaderBar = Roact.PureComponent:extend("HeaderBar") + +RootHeaderBar.validateProps = t.strictInterface({ + -- The title of the screen + title = t.string, + + -- How tall the bar is + barHeight = t.optional(t.number), + + -- A function that returns a Roact Component, used for customizing buttons on the right side of the bar + renderRight = t.optional(t.callback), + backgroundTransparency = t.optional(t.number), +}) + +RootHeaderBar.defaultProps = { + barHeight = 64, + renderRight = function() + return nil + end, +} + +function RootHeaderBar:init() + self:setState({ + margin = 0, + }) + + self.setPageMargin = function(rbx) + local margin = getPageMargin(rbx.AbsoluteSize.X) + self:setState({ + margin = margin + }) + end +end + +function RootHeaderBar:render() + return withStyle(function(style) + local theme = style.Theme + local font = style.Font + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, self.props.barHeight), + [Roact.Change.AbsoluteSize] = self.setPageMargin, + }, { + ThreeSectionBar = Roact.createElement(ThreeSectionBar, { + BackgroundTransparency = self.props.backgroundTransparency or theme.BackgroundDefault.Transparency, + BackgroundColor3 = theme.BackgroundDefault.Color, + + barHeight = self.props.barHeight, + contentPaddingRight = UDim.new(0, 0), + + marginLeft = self.state.margin, + marginRight = self.state.margin, + + renderRight = self.props.renderRight, + renderLeft = function(props) + return Roact.createFragment({ + Text = Roact.createElement(GenericTextLabel, { + fluidSizing = true, + Text = self.props.title, + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Left, + fontStyle = font.Title, + colorStyle = theme.TextEmphasis, + }, props[Roact.Children]) + }) + end, + }) + }) + end) +end + +return RootHeaderBar diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/RootHeaderBar.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/RootHeaderBar.spec.lua new file mode 100644 index 0000000..5073b85 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/RootHeaderBar.spec.lua @@ -0,0 +1,110 @@ +return function() + local Bar = script.Parent + local App = Bar.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local RootHeaderBar = require(UIBlox.App.Bar.RootHeaderBar) + + local BARSIZE_SMALL = UDim2.new(0, 320, 0, 40) + local BARSIZE_MEDIUM = UDim2.new(0, 480, 0, 40) + local BARSIZE_LARGE = UDim2.new(0, 600, 0, 40) + local MARGIN_SMALL = 12 + local MARGIN_MEDIUM = 24 + local MARGIN_LARGE = 48 + + describe("lifecycle", function() + it("should mount and unmount without issues", function() + local element = mockStyleComponent({ + bar = Roact.createElement(RootHeaderBar, { + title = "Root Header Bar", + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) + + describe("margin logic", function() + it("should have margin of 12 on small screens", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + barFrame = Roact.createElement("Frame", { + Size = UDim2.new(0, 0, 0, 0), + }, { + bar = Roact.createElement(RootHeaderBar, { + title = "Root Header Bar", + }), + }) + }) + + local instance = Roact.mount(element, frame, "Frame") + local barFrame = frame:FindFirstChild("barFrame", true) + local bar = barFrame:FindFirstChild("bar") + local leftFrame = bar:FindFirstChild("leftFrame", true) + local margin = leftFrame:FindFirstChild("$margin", true) + expect(margin).to.be.ok() + + barFrame.Size = BARSIZE_SMALL + local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize] + expect(margin.PaddingLeft.Offset).to.equal(MARGIN_SMALL) + + Roact.unmount(instance) + end) + + it("should have margin of 24 on medium screens", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + barFrame = Roact.createElement("Frame", { + Size = UDim2.new(0, 0, 0, 0), + }, { + bar = Roact.createElement(RootHeaderBar, { + title = "Root Header Bar", + }), + }) + }) + + local instance = Roact.mount(element, frame, "Frame") + local barFrame = frame:FindFirstChild("barFrame", true) + local bar = barFrame:FindFirstChild("bar") + local leftFrame = bar:FindFirstChild("leftFrame", true) + local margin = leftFrame:FindFirstChild("$margin", true) + expect(margin).to.be.ok() + + barFrame.Size = BARSIZE_MEDIUM + local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize] + expect(margin.PaddingLeft.Offset).to.equal(MARGIN_MEDIUM) + + Roact.unmount(instance) + end) + + it("should have margin of 48 on large screens", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + barFrame = Roact.createElement("Frame", { + Size = UDim2.new(0, 0, 0, 0), + }, { + bar = Roact.createElement(RootHeaderBar, { + title = "Root Header Bar", + }), + }) + }) + + local instance = Roact.mount(element, frame, "Frame") + local barFrame = frame:FindFirstChild("barFrame", true) + local bar = barFrame:FindFirstChild("bar") + local leftFrame = bar:FindFirstChild("leftFrame", true) + local margin = leftFrame:FindFirstChild("$margin", true) + expect(margin).to.be.ok() + + barFrame.Size = BARSIZE_LARGE + local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize] + expect(margin.PaddingLeft.Offset).to.equal(MARGIN_LARGE) + + Roact.unmount(instance) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/FullscreenTitleBar.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/FullscreenTitleBar.story.lua new file mode 100644 index 0000000..fa96b75 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/FullscreenTitleBar.story.lua @@ -0,0 +1,110 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local Bar = script.Parent.Parent +local App = Bar.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local FullscreenTitleBar = require(script.Parent.Parent.FullscreenTitleBar) + +local DISAPPEAR_DELAY = 0.5 + +local TitleBarStory = Roact.PureComponent:extend("TitleBarStory") + +function TitleBarStory:init() + self:setState({ + isTriggered = false, + }) + + self.triggerTitleBar = function() + print("Mouse entering trigger area") + if not self.state.isTriggered then + self:setState({ + isTriggered = true, + }) + end + end + + self.hideTitleBar = function() + print("Mouse leaving Title Bar area") + if self.state.isTriggered then + delay(DISAPPEAR_DELAY, function() + self:setState({ + isTriggered = false, + }) + end) + end + end + + self.buttonControl = function() + self:setState(function(prevState) + return { + isTriggered = not prevState.isTriggered, + } + end) + end +end + +function TitleBarStory:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + }), + ControlsFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 50), + LayoutOrder = 1, + }, { + TestButton = Roact.createElement("TextButton", { + Text = self.state.isTriggered and "Dismiss" or "Activate", + Size = UDim2.fromOffset(200, 50), + [Roact.Event.Activated] = self.buttonControl, + }), + }), + StoryItem = Roact.createElement(StoryItem, { + size = UDim2.fromScale(1, 1), + title = "FullscreenTitleBar", + subTitle = "App.Bar.FullscreenTitleBar", + layoutOrder = 2, + showDivider = true, + }, { + Demo = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + TriggerArea = Roact.createElement("Frame", { + BackgroundColor3 = Color3.fromRGB(0, 255, 255), + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 1), + [Roact.Event.MouseEnter] = self.triggerTitleBar, + }), + TitleBar = Roact.createElement(FullscreenTitleBar, { + title = "Roblox", + isTriggered = self.state.isTriggered, + onDisappear = self.hideTitleBar, + exitFullscreen = function() + print "Exit Fullscreen" + end, + closeRoblox = function() + print "Close Roblox" + end, + }), + }) + }), + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(TitleBarStory), + }), target, "FullscreenTitleBar") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/HeaderBar.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/HeaderBar.story.lua new file mode 100644 index 0000000..1e0cb6e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/HeaderBar.story.lua @@ -0,0 +1,136 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local Bar = script.Parent.Parent +local App = Bar.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local Images = require(App.ImageSet.Images) +local IconSize = require(App.ImageSet.Enum.IconSize) +local HeaderBar = require(Bar.HeaderBar) +local IconButton = require(UIBlox.App.Button.IconButton) +local TextButton = require(UIBlox.App.Button.TextButton) + +local function BarDemo() + return Roact.createElement(HeaderBar, { + title = string.rep("Header Bar Story", 1), + renderLeft = HeaderBar.renderLeft.backButton(function() + print("navProps.navigation.pop()") + end), + renderRight = function() + return Roact.createFragment({ + search = Roact.createElement(IconButton, { + size = UDim2.fromOffset(0, 0), + iconSize = IconSize.Medium, + icon = Images["icons/common/search"], + onActivated = function() + print "Opening Search!" + end, + layoutOrder = 1, + }), + premium = Roact.createElement(IconButton, { + size = UDim2.fromOffset(0, 0), + iconSize = IconSize.Medium, + icon = Images["icons/common/goldrobux"], + onActivated = function() + print "Oooh Shiny!" + end, + layoutOrder = 2, + }), + alert = Roact.createElement(IconButton, { + size = UDim2.fromOffset(0, 0), + iconSize = IconSize.Medium, + icon = Images['icons/common/notificationOn'], + onActivated = function() + print "Alert!" + end, + layoutOrder = 3, + }), + }) + end, + }) +end + +local function BarWithTextButtonsDemo() + return Roact.createElement(HeaderBar, { + title = string.rep("Header Bar Story", 1), + renderLeft = function() + return Roact.createFragment({ + search = Roact.createElement(TextButton, { + text = "Action 1", + onActivated = function() + print "Opening Search!" + end, + layoutOrder = 1, + }), + }) + end, + renderRight = function() + return Roact.createFragment({ + search = Roact.createElement(TextButton, { + text = "Action 2", + onActivated = function() + print "Opening Search!" + end, + layoutOrder = 1, + }), + }) + end, + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(StoryItem, { + size = UDim2.fromScale(1, 1), + title = "HeaderBar", + subTitle = "App.Bar.HeaderBar", + }, { + layout = Roact.createElement("UIListLayout"), + frame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(600, 45), + }, { + headerBar = Roact.createElement(BarDemo) + }), + frame2 = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(361, 45), + }, { + headerBar = Roact.createElement(BarDemo) + }), + frame3 = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(300, 45), + }, { + headerBar = Roact.createElement(BarDemo) + }), + + frame4 = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(600, 45), + }, { + headerBar = Roact.createElement(BarWithTextButtonsDemo) + }), + frame5 = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(361, 45), + }, { + headerBar = Roact.createElement(BarWithTextButtonsDemo) + }), + frame6 = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(300, 45), + }, { + headerBar = Roact.createElement(BarWithTextButtonsDemo) + }), + }), + }), target, "HeaderBar") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/RootHeaderBar.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/RootHeaderBar.story.lua new file mode 100644 index 0000000..81d1936 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/RootHeaderBar.story.lua @@ -0,0 +1,61 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local Bar = script.Parent.Parent +local App = Bar.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local getIconSizeUDim2 = require(App.ImageSet.getIconSizeUDim2) +local IconSize = require(App.ImageSet.Enum.IconSize) +local Images = require(App.ImageSet.Images) +local RootHeaderBar = require(Bar.RootHeaderBar) +local IconButton = require(UIBlox.App.Button.IconButton) + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 1, 0), + title = "RootHeaderBar", + subTitle = "App.Bar.RootHeaderBar", + }, { + Roact.createElement(RootHeaderBar, { + title = "Root Header Bar Story Long Text Example To Test Truncation", + renderRight = function() + return Roact.createFragment({ + search = Roact.createElement(IconButton, { + size = getIconSizeUDim2(IconSize.Small), + icon = Images['icons/common/search_small'], + onActivated = function() + print "Opening Search!" + end, + layoutOrder = 1, + }), + premium = Roact.createElement(IconButton, { + size = getIconSizeUDim2(IconSize.Small), + icon = Images['icons/common/goldrobux_small'], + onActivated = function() + print "Oooh Shiny!" + end, + layoutOrder = 2, + }), + alert = Roact.createElement(IconButton, { + size = getIconSizeUDim2(IconSize.Small), + icon = Images['icons/common/notificationOn'], + onActivated = function() + print "Alert!" + end, + layoutOrder = 3, + }), + }) + end, + }), + }) + }), target, "HeaderBar") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/AlertButton.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/AlertButton.lua new file mode 100644 index 0000000..950207e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/AlertButton.lua @@ -0,0 +1,55 @@ +local Button = script.Parent +local App = Button.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local Images = require(App.ImageSet.Images) +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) + +local validateButtonProps = require(Button.validateButtonProps) +local GenericButton = require(UIBlox.Core.Button.GenericButton) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) + +local AlertButton = Roact.PureComponent:extend("AlertButton") + +local BUTTON_STATE_COLOR = { + [ControlState.Default] = "Alert", +} + +local CONTENT_STATE_COLOR = { + [ControlState.Default] = "Alert", +} + +AlertButton.defaultProps = { + isDisabled = false, + isLoading = false, +} + +function AlertButton:render() + assert(validateButtonProps(self.props)) + local image = Images["component_assets/circle_17_stroke_1"] + return withSelectionCursorProvider(function(getSelectionCursor) + return Roact.createElement(GenericButton, { + Size = self.props.size, + AnchorPoint = self.props.anchorPoint, + Position = self.props.position, + LayoutOrder = self.props.layoutOrder, + SelectionImageObject = getSelectionCursor(CursorKind.RoundedRectNoInset), + icon = self.props.icon, + text = self.props.text, + isDisabled = self.props.isDisabled, + isLoading = self.props.isLoading, + onActivated = self.props.onActivated, + onStateChanged = self.props.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + buttonImage = image, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + }) + end) +end + +return AlertButton \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/AlertButton.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/AlertButton.spec.lua new file mode 100644 index 0000000..c9ba9d6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/AlertButton.spec.lua @@ -0,0 +1,62 @@ +return function() + local Button = script.Parent + local App = Button.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local Images = require(App.ImageSet.Images) + + local icon = Images["icons/common/robux_small"] + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local AlertButton = require(Button.AlertButton) + + it("should create and destroy Alert Button with text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(AlertButton, { + text = "Button", + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Alert Button with text icon only without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(AlertButton, { + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Alert Button with text and text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(AlertButton, { + text = "Button", + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a blank Alert Button without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(AlertButton, { + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ButtonStack.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ButtonStack.lua new file mode 100644 index 0000000..7238888 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ButtonStack.lua @@ -0,0 +1,149 @@ +local ButtonRoot = script.Parent +local AppRoot = ButtonRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local RoactGamepad = require(Packages.RoactGamepad) + +local AlertButton = require(ButtonRoot.AlertButton) +local PrimaryContextualButton = require(ButtonRoot.PrimaryContextualButton) +local PrimarySystemButton = require(ButtonRoot.PrimarySystemButton) +local SecondaryButton = require(ButtonRoot.SecondaryButton) +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local FitFrame = require(Packages.FitFrame) +local FitFrameOnAxis = FitFrame.FitFrameOnAxis + +local ButtonType = require(ButtonRoot.Enum.ButtonType) + +local validateButtonStack = require(AppRoot.Button.Validator.validateButtonStack) +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local BUTTON_HEIGHT = 36 + +local ButtonStack = Roact.PureComponent:extend("ButtonStack") + +ButtonStack.defaultProps = { + buttonHeight = BUTTON_HEIGHT, + marginBetween = 12, + minHorizontalButtonPadding = 8, +} + +function ButtonStack:init() + self.buttonRefs = RoactGamepad.createRefCache() + + self.state = { + frameWidth = 0 + } + + self.updateFrameSize = function(rbx) + local frameWidth = rbx.AbsoluteSize.X + if frameWidth ~= self.state.frameWidth then + self:setState({ + frameWidth = frameWidth, + }) + end + end +end + +function ButtonStack:render() + assert(validateButtonStack(self.props)) + + return withStyle(function(stylePalette) + local font = stylePalette.Font + local textSize = font.Body.RelativeSize * font.BaseSize + + local buttons = self.props.buttons + local paddingBetween = #buttons > 1 and self.props.marginBetween or 0 + local nonStackedButtonWidth = (self.state.frameWidth / #buttons) - (paddingBetween * (#buttons - 1) / #buttons) + + local isButtonStacked = false + local fillDirection + if self.props.forcedFillDirection then + isButtonStacked = self.props.forcedFillDirection == Enum.FillDirection.Vertical + fillDirection = self.props.forcedFillDirection + else + for _, button in ipairs(buttons) do + local buttonTextWidth = GetTextSize( + button.props.text or "", + textSize, + font.Body.Font, + Vector2.new(self.state.frameWidth, self.props.buttonHeight) + ) + if buttonTextWidth.X > (nonStackedButtonWidth - (2 * self.props.minHorizontalButtonPadding)) then + isButtonStacked = true + break + end + end + fillDirection = isButtonStacked and Enum.FillDirection.Vertical or Enum.FillDirection.Horizontal + end + + local buttonSize = isButtonStacked and UDim2.new(1, 0, 0, self.props.buttonHeight) + or UDim2.new(0, nonStackedButtonWidth, 0, self.props.buttonHeight) + + local buttonTable = {} + for colIndex, button in ipairs(buttons) do + local newProps = { + layoutOrder = isButtonStacked and (#buttons - colIndex) or colIndex, + size = buttonSize, + } + local buttonProps = Cryo.Dictionary.join(newProps, button.props) + + local buttonComponent + if button.buttonType == ButtonType.PrimaryContextual then + buttonComponent = PrimaryContextualButton + elseif button.buttonType == ButtonType.PrimarySystem then + buttonComponent = PrimarySystemButton + elseif button.buttonType == ButtonType.Alert then + buttonComponent = AlertButton + else + buttonComponent = SecondaryButton + end + + if UIBloxConfig.enableExperimentalGamepadSupport then + local gamepadFrameProps = { + Size = buttonSize, + BackgroundTransparency = 1, + [Roact.Ref] = self.buttonRefs[colIndex], + NextSelectionUp = (isButtonStacked and colIndex > 1) and self.buttonRefs[colIndex - 1] or nil, + NextSelectionDown = (isButtonStacked and colIndex < #buttons) and self.buttonRefs[colIndex + 1] or nil, + NextSelectionLeft = (not isButtonStacked and colIndex > 1) and self.buttonRefs[colIndex - 1] or nil, + NextSelectionRight = (not isButtonStacked and colIndex < #buttons) and self.buttonRefs[colIndex + 1] or nil, + inputBindings = { + Activated = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonA, button.props.onActivated), + }, + } + + table.insert(buttonTable, Roact.createElement(RoactGamepad.Focusable.Frame, gamepadFrameProps, { + Roact.createElement(buttonComponent, buttonProps) + })) + else + table.insert(buttonTable, Roact.createElement(buttonComponent, buttonProps)) + end + end + + return Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable[FitFrameOnAxis] or FitFrameOnAxis, { + BackgroundTransparency = 1, + contentPadding = UDim.new(0, paddingBetween), + FillDirection = fillDirection, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + LayoutOrder = 3, + minimumSize = UDim2.new(1, 0, 0, self.props.buttonHeight), + [Roact.Ref] = self.props[Roact.Ref], + [Roact.Change.AbsoluteSize] = self.updateFrameSize, + + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + }, + buttonTable + ) + end) +end + +return ButtonStack \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ButtonStack.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ButtonStack.spec.lua new file mode 100644 index 0000000..649f33a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ButtonStack.spec.lua @@ -0,0 +1,33 @@ +local ButtonRoot = script.Parent +local AppRoot = ButtonRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + +local ButtonStack = require(script.Parent.ButtonStack) + +local DEFAULT_REQUIRED_PROPS = { + buttons = { + { + props = { + text = "test", + onActivated = function() end, + }, + } + }, +} + +return function() + describe("lifecycle", function() + it("should mount and unmount button stacks without issue", function() + local tree = mockStyleComponent( + Roact.createElement(ButtonStack, DEFAULT_REQUIRED_PROPS) + ) + local handle = Roact.mount(tree) + Roact.unmount(handle) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/Enum/ButtonType.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/Enum/ButtonType.lua new file mode 100644 index 0000000..b17818b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/Enum/ButtonType.lua @@ -0,0 +1,13 @@ +local ButtonRoot = script.Parent.Parent +local AppRoot = ButtonRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local enumerate = require(Packages.enumerate) + +return enumerate("ButtonType", { + "Alert", + "PrimaryContextual", + "PrimarySystem", + "Secondary", +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/IconButton.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/IconButton.lua new file mode 100644 index 0000000..9112b53 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/IconButton.lua @@ -0,0 +1,153 @@ +local App = script:FindFirstAncestor("App") +local UIBlox = App.Parent +local Core = UIBlox.Core +local Packages = UIBlox.Parent + +local t = require(Packages.t) +local Roact = require(Packages.Roact) +local enumerate = require(Packages.enumerate) + +local Interactable = require(Core.Control.Interactable) + +local ControlState = require(Core.Control.Enum.ControlState) +local getContentStyle = require(Core.Button.getContentStyle) +local getIconSize = require(App.ImageSet.getIconSize) +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local bindingValidator = require(Core.Utility.bindingValidator) +local validateImage = require(Core.ImageSet.Validator.validateImage) + +local withStyle = require(Core.Style.withStyle) +local HoverButtonBackground = require(Core.Button.HoverButtonBackground) +local ImageSetComponent = require(Core.ImageSet.ImageSetComponent) +local IconSize = require(App.ImageSet.Enum.IconSize) + +local IconButton = Roact.PureComponent:extend("IconButton") +IconButton.debugProps = enumerate("debugProps", { + "controlState", +}) + +IconButton.validateProps = t.strictInterface({ + -- The state change callback for the button + onStateChanged = t.optional(t.callback), + + -- Is the button visually disabled + isDisabled = t.optional(t.boolean), + + colorStyleDefault = t.optional(t.string), + colorStyleHover = t.optional(t.string), + + --A Boolean value that determines whether user events are ignored and sink input + userInteractionEnabled = t.optional(t.boolean), + + -- The activated callback for the button + onActivated = t.optional(t.callback), + + anchorPoint = t.optional(t.Vector2), + layoutOrder = t.optional(t.number), + position = t.optional(t.UDim2), + size = t.optional(t.UDim2), + icon = t.optional(validateImage), + iconSize = t.optional(enumerateValidator(IconSize)), + iconColor3 = t.optional(t.Color3), + iconTransparency = t.optional(t.union(t.number, bindingValidator(t.number))), + + -- Override the default controlState + [IconButton.debugProps.controlState] = t.optional(enumerateValidator(ControlState)), +}) + +IconButton.defaultProps = { + anchorPoint = Vector2.new(0, 0), + layoutOrder = 0, + position = UDim2.new(0, 0, 0, 0), + size = nil, + icon = "", + iconSize = IconSize.Medium, + + colorStyleDefault = "SystemPrimaryDefault", + colorStyleHover = "SystemPrimaryDefault", + iconColor3 = nil, + iconTransparency = nil, + + isDisabled = false, + userInteractionEnabled = true, + + [IconButton.debugProps.controlState] = nil, +} + +function IconButton:init() + self:setState({ + controlState = ControlState.Initialize + }) + + self.onStateChanged = function(oldState, newState) + self:setState({ + controlState = newState, + }) + if self.props.onStateChanged then + self.props.onStateChanged(oldState, newState) + end + end + + local iconSizeToSizeScale = { + [IconSize.Small] = 1, + [IconSize.Medium] = 2, + [IconSize.Large] = 3, + [IconSize.XLarge] = 4, + [IconSize.XXLarge] = 5, + } + self.getSize = function(iconSizeMeasurement) + if self.props.size then + return self.props.size + end + + local iconSize = self.props.iconSize + local extents = iconSizeMeasurement + 4 * iconSizeToSizeScale[iconSize] + return UDim2.fromOffset(extents, extents) + end +end + +function IconButton:render() + return withStyle(function(style) + local iconSizeMeasurement = getIconSize(self.props.iconSize) + local size = self.getSize(iconSizeMeasurement) + local currentState = self.props[IconButton.debugProps.controlState] or self.state.controlState + + local iconStateColorMap = { + [ControlState.Default] = self.props.colorStyleDefault, + [ControlState.Hover] = self.props.colorStyleHover, + } + + local iconStyle = getContentStyle(iconStateColorMap, currentState, style) + + return Roact.createElement(Interactable, { + AnchorPoint = self.props.anchorPoint, + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + Size = size, + + isDisabled = self.props.isDisabled, + onStateChanged = self.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + BackgroundTransparency = 1, + AutoButtonColor = false, + + [Roact.Event.Activated] = self.props.onActivated, + }, { + sizeConstraint = Roact.createElement("UISizeConstraint", { + MinSize = Vector2.new(iconSizeMeasurement, iconSizeMeasurement), + }), + imageLabel = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.fromScale(0.5, 0.5), + Size = UDim2.fromOffset(iconSizeMeasurement, iconSizeMeasurement), + BackgroundTransparency = 1, + Image = self.props.icon, + ImageColor3 = self.props.iconColor3 or iconStyle.Color, + ImageTransparency = self.props.iconTransparency or iconStyle.Transparency, + }), + background = currentState == ControlState.Hover and Roact.createElement(HoverButtonBackground), + }) + end) +end + +return IconButton diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/IconButton.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/IconButton.spec.lua new file mode 100644 index 0000000..f67eaee --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/IconButton.spec.lua @@ -0,0 +1,268 @@ +return function() + local IconButton = require(script.Parent.IconButton) + + local App = script:FindFirstAncestor("App") + local UIBlox = App.Parent + local Core = UIBlox.Core + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local ControlState = require(Core.Control.Enum.ControlState) + local IconSize = require(App.ImageSet.Enum.IconSize) + + describe("props", function() + local BUTTON_NAME = "test:" .. tostring(math.random(0, 999)) + local runTest = function(props) + local folder = Instance.new("Folder") + local element = mockStyleComponent({ + [BUTTON_NAME] = Roact.createElement(IconButton, props), + }) + + local instance = Roact.mount(element, folder) + + return folder, function() + Roact.unmount(instance) + folder:Destroy() + end + end + + local function getImageLabel(folder) + return folder:FindFirstChild("imageLabel", true) + end + + local function getGuiObjectRoot(folder) + return folder:FindFirstChild(BUTTON_NAME, true) + end + + local function getIconColor3(folder) + return getImageLabel(folder).ImageColor3 + end + + local function getIconTransparency(folder) + return getImageLabel(folder).ImageTransparency + end + + local function getGuiObjectRootAbsoluteSize(folder) + return getGuiObjectRoot(folder).AbsoluteSize + end + + local function getGuiObjectRootSize(folder) + return getGuiObjectRoot(folder).Size + end + + describe("iconSize", function() + it("SHOULD resize gui object root AbsoluteSize", function() + local smallFolder, smallCleanup = runTest({ + iconSize = IconSize.Small, + }) + + local mediumFolder, mediumCleanup = runTest({ + iconSize = IconSize.Medium, + }) + + expect(getGuiObjectRootAbsoluteSize(smallFolder)).to.never.equal(getGuiObjectRootAbsoluteSize(mediumFolder)) + + smallCleanup() + mediumCleanup() + end) + end) + + describe("appearance override props", function() + it("SHOULD override ImageLabel.ImageColor3 with iconColor3", function() + for _ = 1, 50 do + local randomColor = BrickColor.random().Color + local folder, cleanup = runTest({ + iconColor3 = randomColor, + }) + + expect(getIconColor3(folder)).to.equal(randomColor) + + cleanup() + end + end) + + it("SHOULD override ImageLabel.ImageTransparency with iconTransparency", function() + for transparency = 0, 1, 0.1 do + local folder, cleanup = runTest({ + iconTransparency = transparency, + }) + + expect(getIconTransparency(folder)).to.be.near(transparency, 0.001) + + cleanup() + end + end) + + it("SHOULD override ImageLabel.ImageTransparency with iconTransparency from RoactBinding value", function() + for transparency = 0, 1, 0.1 do + local folder, cleanup = runTest({ + iconTransparency = Roact.createBinding(transparency), + }) + + expect(getIconTransparency(folder)).to.be.near(transparency, 0.001) + + cleanup() + end + end) + + it("SHOULD override root guiObject.AbsoluteSize with size", function() + local testSizes = { + UDim2.fromScale(0.5, 0.5), + UDim2.fromScale(1, 1), + UDim2.fromOffset(1000, 10), + UDim2.fromOffset(0, 100), + } + for _, size in ipairs(testSizes) do + local controlGroupFolder, cleanupControlGroup = runTest({ + size = nil, + }) + + local variableGroupFolder, cleanupVariableGroup = runTest({ + size = size, + }) + + local controlSize = getGuiObjectRootAbsoluteSize(controlGroupFolder) + local variableSize = getGuiObjectRootAbsoluteSize(variableGroupFolder) + expect(controlSize).to.never.equal(variableSize) + + expect(getGuiObjectRootSize(variableGroupFolder)).to.equal(size) + + cleanupControlGroup() + cleanupVariableGroup() + end + end) + end) + + describe("positional props", function() + it("SHOULD respect AnchorPoint", function() + local function testAnchorPoint(anchorPoint) + local folder, cleanup = runTest({ + anchorPoint = anchorPoint, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.AnchorPoint).to.equal(anchorPoint) + + cleanup() + end + + testAnchorPoint(Vector2.new(0, 0)) + testAnchorPoint(Vector2.new(0, 1)) + testAnchorPoint(Vector2.new(1, 0)) + testAnchorPoint(Vector2.new(1, 1)) + testAnchorPoint(Vector2.new(0.5, 0.5)) + end) + + it("SHOULD respect Position", function() + local function testPosition(position) + local folder, cleanup = runTest({ + position = position, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.Position).to.equal(position) + + cleanup() + end + + testPosition(UDim2.new(0, 0, 0, 0)) + testPosition(UDim2.new(0.5, 10, 1, 20)) + testPosition(UDim2.fromScale(1, 1)) + testPosition(UDim2.fromOffset(100, 000)) + end) + + it("SHOULD respect LayoutOrder", function() + local function testLayoutOrder(layoutOrder) + local folder, cleanup = runTest({ + layoutOrder = layoutOrder, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.LayoutOrder).to.equal(layoutOrder) + + cleanup() + end + + testLayoutOrder(0) + testLayoutOrder(1) + testLayoutOrder(2) + end) + + it("SHOULD respect Size", function() + local function testSize(size) + local folder, cleanup = runTest({ + size = size, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.Size).to.equal(size) + + cleanup() + end + + testSize(UDim2.new(0, 0, 0, 0)) + testSize(UDim2.new(0.5, 10, 1, 20)) + testSize(UDim2.fromScale(1, 1)) + testSize(UDim2.fromOffset(100, 000)) + end) + end) + + describe("debugProps.controlState", function() + local function isShowingBackground(folder) + return folder:FindFirstChild("background", true) ~= nil + end + + local function isImageTransparent(folder) + local imageLabel = folder:FindFirstChild("imageLabel", true) + assert(imageLabel, "imageLabel never mounted") + return imageLabel.ImageTransparency > 0 + end + + it("SHOULD render ControlState.Default with no issues", function() + local folder, cleanup = runTest({ + [IconButton.debugProps.controlState] = ControlState.Default, + }) + + expect(isShowingBackground(folder)).to.equal(false) + expect(isImageTransparent(folder)).to.equal(false) + + cleanup() + end) + + it("SHOULD render ControlState.Hover with no issues", function() + local folder, cleanup = runTest({ + [IconButton.debugProps.controlState] = ControlState.Hover, + }) + + expect(isShowingBackground(folder)).to.equal(true) + expect(isImageTransparent(folder)).to.equal(false) + + cleanup() + end) + + it("SHOULD render ControlState.Pressed with no issues", function() + local folder, cleanup = runTest({ + [IconButton.debugProps.controlState] = ControlState.Pressed, + }) + + expect(isShowingBackground(folder)).to.equal(false) + expect(isImageTransparent(folder)).to.equal(true) + + cleanup() + end) + + it("SHOULD render ControlState.Disabled with no issues", function() + local folder, cleanup = runTest({ + [IconButton.debugProps.controlState] = ControlState.Disabled, + }) + + expect(isShowingBackground(folder)).to.equal(false) + expect(isImageTransparent(folder)).to.equal(true) + + cleanup() + end) + end) + end) + +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimaryContextualButton.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimaryContextualButton.lua new file mode 100644 index 0000000..40a93b5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimaryContextualButton.lua @@ -0,0 +1,55 @@ +local Button = script.Parent +local App = Button.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local Images = require(App.ImageSet.Images) +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) +local validateButtonProps = require(Button.validateButtonProps) +local GenericButton = require(UIBlox.Core.Button.GenericButton) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) + +local PrimaryContextualButton = Roact.PureComponent:extend("PrimaryContextualButton") + +local BUTTON_STATE_COLOR = { + [ControlState.Default] = "ContextualPrimaryDefault", + [ControlState.Hover] = "ContextualPrimaryOnHover", +} + +local CONTENT_STATE_COLOR = { + [ControlState.Default] = "ContextualPrimaryContent", +} + +PrimaryContextualButton.defaultProps = { + isDisabled = false, + isLoading = false, +} + +function PrimaryContextualButton:render() + assert(validateButtonProps(self.props)) + local image = Images["component_assets/circle_17"] + return withSelectionCursorProvider(function(getSelectionCursor) + return Roact.createElement(GenericButton, { + Size = self.props.size, + AnchorPoint = self.props.anchorPoint, + Position = self.props.position, + LayoutOrder = self.props.layoutOrder, + SelectionImageObject = getSelectionCursor(CursorKind.RoundedRectNoInset), + icon = self.props.icon, + text = self.props.text, + isDisabled = self.props.isDisabled, + isLoading = self.props.isLoading, + onActivated = self.props.onActivated, + onStateChanged = self.props.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + buttonImage = image, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + }) + end) +end + +return PrimaryContextualButton \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimaryContextualButton.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimaryContextualButton.spec.lua new file mode 100644 index 0000000..655a90e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimaryContextualButton.spec.lua @@ -0,0 +1,62 @@ +return function() + local Button = script.Parent + local App = Button.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local Images = require(App.ImageSet.Images) + + local icon = Images["icons/common/robux_small"] + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local PrimaryContextualButton = require(Button.PrimaryContextualButton) + + it("should create and destroy Primary Contextual Button with text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimaryContextualButton, { + text = "Button", + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Primary Contextual Button with text icon only without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimaryContextualButton, { + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Primary Contextual Button with text and text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimaryContextualButton, { + text = "Button", + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a blank Primary Contextual Button without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimaryContextualButton, { + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimarySystemButton.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimarySystemButton.lua new file mode 100644 index 0000000..30fc95a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimarySystemButton.lua @@ -0,0 +1,56 @@ +local Button = script.Parent +local App = Button.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local Images = require(App.ImageSet.Images) +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) +local validateButtonProps = require(Button.validateButtonProps) +local GenericButton = require(UIBlox.Core.Button.GenericButton) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) + +local PrimarySystemButton = Roact.PureComponent:extend("PrimarySystemButton") + +local BUTTON_STATE_COLOR = { + [ControlState.Default] = "SystemPrimaryDefault", + [ControlState.Hover] = "SystemPrimaryOnHover", +} + +local CONTENT_STATE_COLOR = { + [ControlState.Default] = "SystemPrimaryContent", +} + + +PrimarySystemButton.defaultProps = { + isDisabled = false, + isLoading = false, +} + +function PrimarySystemButton:render() + assert(validateButtonProps(self.props)) + local image = Images["component_assets/circle_17"] + return withSelectionCursorProvider(function(getSelectionCursor) + return Roact.createElement(GenericButton, { + Size = self.props.size, + AnchorPoint = self.props.anchorPoint, + Position = self.props.position, + LayoutOrder = self.props.layoutOrder, + SelectionImageObject = getSelectionCursor(CursorKind.RoundedRectNoInset), + icon = self.props.icon, + text = self.props.text, + isDisabled = self.props.isDisabled, + isLoading = self.props.isLoading, + onActivated = self.props.onActivated, + onStateChanged = self.props.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + buttonImage = image, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + }) + end) +end + +return PrimarySystemButton \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimarySystemButton.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimarySystemButton.spec.lua new file mode 100644 index 0000000..cbcd415 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimarySystemButton.spec.lua @@ -0,0 +1,62 @@ +return function() + local Button = script.Parent + local App = Button.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local Images = require(App.ImageSet.Images) + + local icon = Images["icons/common/robux_small"] + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local PrimarySystemButton = require(Button.PrimarySystemButton) + + it("should create and destroy Primary System Button with text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimarySystemButton, { + text = "Button", + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Primary System Button with text icon only without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimarySystemButton, { + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Primary System Button with text and text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimarySystemButton, { + text = "Button", + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a blank Primary System Button without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimarySystemButton, { + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/SecondaryButton.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/SecondaryButton.lua new file mode 100644 index 0000000..b258155 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/SecondaryButton.lua @@ -0,0 +1,56 @@ +local Button = script.Parent +local App = Button.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local Images = require(App.ImageSet.Images) +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) +local validateButtonProps = require(Button.validateButtonProps) +local GenericButton = require(UIBlox.Core.Button.GenericButton) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) + +local SecondaryButton = Roact.PureComponent:extend("SecondaryButton") + +local BUTTON_STATE_COLOR = { + [ControlState.Default] = "SecondaryDefault", + [ControlState.Hover] = "SecondaryOnHover", +} + +local CONTENT_STATE_COLOR = { + [ControlState.Default] = "SecondaryContent", + [ControlState.Hover] = "SecondaryOnHover", +} + +SecondaryButton.defaultProps = { + isDisabled = false, + isLoading = false, +} + +function SecondaryButton:render() + assert(validateButtonProps(self.props)) + local image = Images["component_assets/circle_17_stroke_1"] + return withSelectionCursorProvider(function(getSelectionCursor) + return Roact.createElement(GenericButton, { + Size = self.props.size, + AnchorPoint = self.props.anchorPoint, + Position = self.props.position, + LayoutOrder = self.props.layoutOrder, + SelectionImageObject = getSelectionCursor(CursorKind.RoundedRectNoInset), + icon = self.props.icon, + text = self.props.text, + isDisabled = self.props.isDisabled, + isLoading = self.props.isLoading, + onActivated = self.props.onActivated, + onStateChanged = self.props.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + buttonImage = image, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + }) + end) +end + +return SecondaryButton \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/SecondaryButton.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/SecondaryButton.spec.lua new file mode 100644 index 0000000..16ddfec --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/SecondaryButton.spec.lua @@ -0,0 +1,62 @@ +return function() + local Button = script.Parent + local App = Button.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local Images = require(App.ImageSet.Images) + + local icon = Images["icons/common/robux_small"] + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local SecondaryButton = require(Button.SecondaryButton) + + it("should create and destroy Secondary Button with text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(SecondaryButton, { + text = "Button", + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Secondary Button with text icon only without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(SecondaryButton, { + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Secondary Button with text and text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(SecondaryButton, { + text = "Button", + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a blank Secondary Button without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(SecondaryButton, { + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/TextButton.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/TextButton.lua new file mode 100644 index 0000000..2ee15d3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/TextButton.lua @@ -0,0 +1,139 @@ +local App = script:FindFirstAncestor("App") +local UIBlox = App.Parent +local Core = UIBlox.Core +local Packages = UIBlox.Parent + +local t = require(Packages.t) +local Roact = require(Packages.Roact) +local enumerate = require(Packages.enumerate) + +local Interactable = require(Core.Control.Interactable) + +local ControlState = require(Core.Control.Enum.ControlState) +local getContentStyle = require(Core.Button.getContentStyle) +local GetTextSize = require(Core.Text.GetTextSize) +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) + +local withStyle = require(Core.Style.withStyle) +local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel) +local HoverButtonBackground = require(Core.Button.HoverButtonBackground) + +local VERTICAL_PADDING = 8 +local HORIZONTAL_PADDING = 11 + +local TextButton = Roact.PureComponent:extend("TextButton") +TextButton.debugProps = enumerate("debugProps", { + "getTextSize", + "controlState", +}) + +TextButton.validateProps = t.strictInterface({ + -- The state change callback for the button + onStateChanged = t.optional(t.callback), + + -- Is the button visually disabled + isDisabled = t.optional(t.boolean), + + fontStyle = t.optional(t.string), + colorStyleDefault = t.optional(t.string), + colorStyleHover = t.optional(t.string), + + --A Boolean value that determines whether user events are ignored and sink input + userInteractionEnabled = t.optional(t.boolean), + + -- The activated callback for the button + onActivated = t.optional(t.callback), + + anchorPoint = t.optional(t.Vector2), + layoutOrder = t.optional(t.number), + position= t.optional(t.UDim2), + size = t.optional(t.UDim2), + text = t.optional(t.string), + + -- A callback that replaces getTextSize implementation + [TextButton.debugProps.getTextSize] = t.optional(t.callback), + + -- Override the default controlState + [TextButton.debugProps.controlState] = t.optional(enumerateValidator(ControlState)), +}) + +TextButton.defaultProps = { + anchorPoint = Vector2.new(0, 0), + layoutOrder = 0, + position = UDim2.new(0, 0, 0, 0), + size = UDim2.fromScale(0, 0), + text = "", + + fontStyle = "Header2", + colorStyleDefault = "SystemPrimaryDefault", + colorStyleHover = "SystemPrimaryDefault", + + isDisabled = false, + userInteractionEnabled = true, + + [TextButton.debugProps.getTextSize] = GetTextSize, + [TextButton.debugProps.controlState] = nil, +} + +function TextButton:init() + self:setState({ + controlState = ControlState.Initialize + }) + + self.onStateChanged = function(oldState, newState) + self:setState({ + controlState = newState, + }) + if self.props.onStateChanged then + self.props.onStateChanged(oldState, newState) + end + end +end + +function TextButton:render() + return withStyle(function(style) + local currentState = self.props[TextButton.debugProps.controlState] or self.state.controlState + + local textStateColorMap = { + [ControlState.Default] = self.props.colorStyleDefault, + [ControlState.Hover] = self.props.colorStyleHover, + } + + local textStyle = getContentStyle(textStateColorMap, currentState, style) + local fontStyle = style.Font[self.props.fontStyle] + + local fontSize = fontStyle.RelativeSize * style.Font.BaseSize + local getTextSize = self.props[TextButton.debugProps.getTextSize] + local textWidth = getTextSize(self.props.text, fontSize, fontStyle.Font, Vector2.new(10000, 0)).X + + return Roact.createElement(Interactable, { + AnchorPoint = self.props.anchorPoint, + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + Size = self.props.size, + + isDisabled = self.props.isDisabled, + onStateChanged = self.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + BackgroundTransparency = 1, + AutoButtonColor = false, + + [Roact.Event.Activated] = self.props.onActivated, + }, { + sizeConstraint = Roact.createElement("UISizeConstraint", { + MinSize = Vector2.new(textWidth + VERTICAL_PADDING*2, fontSize + HORIZONTAL_PADDING*2), + }), + textLabel = Roact.createElement(GenericTextLabel, { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.fromScale(0.5, 0.5), + BackgroundTransparency = 1, + Text = self.props.text, + fontStyle = fontStyle, + colorStyle = textStyle, + }), + background = currentState == ControlState.Hover and Roact.createElement(HoverButtonBackground) + }) + end) +end + +return TextButton diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/TextButton.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/TextButton.spec.lua new file mode 100644 index 0000000..6535804 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/TextButton.spec.lua @@ -0,0 +1,276 @@ +return function() + local TextButton = require(script.Parent.TextButton) + + local App = script:FindFirstAncestor("App") + local UIBlox = App.Parent + local Core = UIBlox.Core + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local ControlState = require(Core.Control.Enum.ControlState) + + local noOpt = function() + end + local text = "Button" + + it("should create and destroy a button without errors", function() + local folder = Instance.new("Folder") + local element = mockStyleComponent({ + button = Roact.createElement(TextButton, { + text = text, + onActivated = noOpt, + fontStyle = "Body", + colorStyleDefault = "UIDefault", + colorStyleHover = "UIDefault", + }), + }) + + local instance = Roact.mount(element, folder) + local label = folder:FindFirstChildWhichIsA("TextLabel", true) + expect(label.Text).to.equal(text) + + Roact.unmount(instance) + end) + + it("should create and destroy a button that is disabled without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(TextButton, { + text = text, + onActivated = noOpt, + isDisabled = true, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a button without text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(TextButton, { + onActivated = noOpt, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should be created as a disabled button", function() + local buttonState = nil + local element = mockStyleComponent({ + button = Roact.createElement(TextButton, { + onActivated = noOpt, + onStateChanged = function(_, newState) + buttonState = newState + end, + isDisabled = true, + }), + }) + + local instance = Roact.mount(element) + expect(buttonState).to.equal(ControlState.Disabled) + Roact.unmount(instance) + end) + + describe("text prop", function() + local BUTTON_NAME = "test:" .. tostring(math.random(0, 999)) + local runTest = function(props) + local folder = Instance.new("Folder") + local element = mockStyleComponent({ + [BUTTON_NAME] = Roact.createElement(TextButton, props), + }) + + local instance = Roact.mount(element, folder) + + return folder, function() + Roact.unmount(instance) + folder:Destroy() + end + end + + it("SHOULD resize to text when not given size property", function() + local folder1, cleanup1 = runTest({ + text = string.rep("!", 1), + [TextButton.debugProps.getTextSize] = function() + return Vector2.new(1, 1) + end, + }) + local folder2, cleanup2 = runTest({ + text = string.rep("!", 10), + [TextButton.debugProps.getTextSize] = function() + return Vector2.new(10, 1) + end, + }) + + local firstSize = folder1:FindFirstChild(BUTTON_NAME, true).AbsoluteSize + local secondSize = folder2:FindFirstChild(BUTTON_NAME, true).AbsoluteSize + + expect(firstSize).to.never.equal(secondSize) + + cleanup1() + cleanup2() + end) + end) + + describe("positional props", function() + local BUTTON_NAME = "test:" .. tostring(math.random(0, 999)) + local runTest = function(props) + local folder = Instance.new("Folder") + local element = mockStyleComponent({ + [BUTTON_NAME] = Roact.createElement(TextButton, props), + }) + + local instance = Roact.mount(element, folder) + + return folder, function() + Roact.unmount(instance) + folder:Destroy() + end + end + + it("SHOULD respect AnchorPoint", function() + local function testAnchorPoint(anchorPoint) + local folder, cleanup = runTest({ + anchorPoint = anchorPoint, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.AnchorPoint).to.equal(anchorPoint) + + cleanup() + end + + testAnchorPoint(Vector2.new(0, 0)) + testAnchorPoint(Vector2.new(0, 1)) + testAnchorPoint(Vector2.new(1, 0)) + testAnchorPoint(Vector2.new(1, 1)) + testAnchorPoint(Vector2.new(0.5, 0.5)) + end) + + it("SHOULD respect Position", function() + local function testPosition(position) + local folder, cleanup = runTest({ + position = position, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.Position).to.equal(position) + + cleanup() + end + + testPosition(UDim2.new(0, 0, 0, 0)) + testPosition(UDim2.new(0.5, 10, 1, 20)) + testPosition(UDim2.fromScale(1, 1)) + testPosition(UDim2.fromOffset(100, 000)) + end) + + it("SHOULD respect LayoutOrder", function() + local function testLayoutOrder(layoutOrder) + local folder, cleanup = runTest({ + layoutOrder = layoutOrder, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.LayoutOrder).to.equal(layoutOrder) + + cleanup() + end + + testLayoutOrder(0) + testLayoutOrder(1) + testLayoutOrder(2) + end) + + it("SHOULD respect Size", function() + local function testSize(size) + local folder, cleanup = runTest({ + size = size, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.Size).to.equal(size) + + cleanup() + end + + testSize(UDim2.new(0, 0, 0, 0)) + testSize(UDim2.new(0.5, 10, 1, 20)) + testSize(UDim2.fromScale(1, 1)) + testSize(UDim2.fromOffset(100, 000)) + end) + end) + + describe("debugProps.controlState", function() + local BUTTON_NAME = "test:" .. tostring(math.random(0, 999)) + local runTest = function(props) + local folder = Instance.new("Folder") + local element = mockStyleComponent({ + [BUTTON_NAME] = Roact.createElement(TextButton, props), + }) + + local instance = Roact.mount(element, folder) + + return folder, function() + Roact.unmount(instance) + folder:Destroy() + end + end + + local function isShowingBackground(folder) + return folder:FindFirstChild("background", true) ~= nil + end + + local function isTextTransparent(folder) + local textLabel = folder:FindFirstChild("textLabel", true) + assert(textLabel, "textLabel never mounted") + return textLabel.TextTransparency > 0 + end + + it("SHOULD render ControlState.Default with no issues", function() + local folder, cleanup = runTest({ + [TextButton.debugProps.controlState] = ControlState.Default, + }) + + expect(isShowingBackground(folder)).to.equal(false) + expect(isTextTransparent(folder)).to.equal(false) + + cleanup() + end) + + it("SHOULD render ControlState.Hover with no issues", function() + local folder, cleanup = runTest({ + [TextButton.debugProps.controlState] = ControlState.Hover, + }) + + expect(isShowingBackground(folder)).to.equal(true) + expect(isTextTransparent(folder)).to.equal(false) + + cleanup() + end) + + it("SHOULD render ControlState.Pressed with no issues", function() + local folder, cleanup = runTest({ + [TextButton.debugProps.controlState] = ControlState.Pressed, + }) + + expect(isShowingBackground(folder)).to.equal(false) + expect(isTextTransparent(folder)).to.equal(true) + + cleanup() + end) + + it("SHOULD render ControlState.Disabled with no issues", function() + local folder, cleanup = runTest({ + [TextButton.debugProps.controlState] = ControlState.Disabled, + }) + + expect(isShowingBackground(folder)).to.equal(false) + expect(isTextTransparent(folder)).to.equal(true) + + cleanup() + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/Validator/validateButtonStack.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/Validator/validateButtonStack.lua new file mode 100644 index 0000000..282847a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/Validator/validateButtonStack.lua @@ -0,0 +1,46 @@ +local validatorRoot = script.Parent +local ButtonRoot = validatorRoot.Parent +local AppRoot = ButtonRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) +local Roact = require(Packages.Roact) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local validateButtonProps = require(ButtonRoot.validateButtonProps) + +local ButtonType = require(ButtonRoot.Enum.ButtonType) + +return t.strictInterface({ + -- buttons: A table of button tables that contain props that PrimaryContextualButton, + -- AlertButton, PrimarySystemButton, or SecondaryButton allow. Also contains a prop "buttonType" + -- to determine which of these button types to use. + buttons = t.array(t.strictInterface({ + buttonType = t.optional(enumerateValidator(ButtonType)), + props = validateButtonProps, + })), + + buttonHeight = t.optional(t.numberMin(0)), + + -- forceFillDirection: What fill direction to force into. If nil, then the fillDirection + -- will be Vertical and automatically change to Horizontal if any button's text is + -- too long. + forcedFillDirection = t.optional(t.enum(Enum.FillDirection)), + + -- marginBetween: the margin between each button. + marginBetween = t.optional(t.numberMin(0)), + + -- minHorizontalButtonPadding: The minimum left and right padding used to calculate + -- the when the button text overflows and automatically changes fillDirection. + -- The overflow calculation will be if the length of the button text is over + -- the button size - (2 * minHorizontalButtonPadding). + minHorizontalButtonPadding = t.optional(t.numberMin(0)), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/validateButtonProps.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/validateButtonProps.lua new file mode 100644 index 0000000..4768ad5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/validateButtonProps.lua @@ -0,0 +1,41 @@ +local Button = script.Parent +local App = Button.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Core = UIBlox.Core + +local t = require(Packages.t) + +local validateImage = require(Core.ImageSet.Validator.validateImage) + +return t.strictInterface({ + --The size of the button + size = t.optional(t.UDim2), + + --The anchor point of the button + anchorPoint = t.optional(t.Vector2), + + --The position of the button + position = t.optional(t.UDim2), + + --The layout order of the button + layoutOrder = t.optional(t.number), + + --The icon of the button + icon = t.optional(validateImage), + + --The text of the button + text = t.optional(t.string), + + --Is the button disabled + isDisabled = t.optional(t.boolean), + + --Is the button loading + isLoading = t.optional(t.boolean), + + --The activated callback for the button + onActivated = t.callback, + + --A Boolean value that determines whether user events are ignored and sink input + userInteractionEnabled = t.optional(t.boolean), +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonCell.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonCell.lua new file mode 100644 index 0000000..5d33b1c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonCell.lua @@ -0,0 +1,88 @@ +local SelectionGroup = script.Parent +local Small = SelectionGroup.Parent +local Cell = Small.Parent +local App = Cell.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local GenericSelectionCell = require(Packages.UIBlox.Core.Cell.GenericSelectionCell) + +local DEFAULT_IMAGE = Images["component_assets/circle_24_stroke_1"] +local SELECTED_IMAGE = Images["component_assets/circle_16"] +local DEFAULT_IMAGE_SIZE = 24 +local SELECTED_IMAGE_SIZE = 16 +local CELL_SIZE = 56 + +local SmallRadioButtonCell = Roact.PureComponent:extend("SmallRadioButtonCell") + +SmallRadioButtonCell.validateProps = t.strictInterface({ + -- Unique key to identify this selection. + key = t.string, + + -- Text to display + text = t.optional(t.string), + + -- Callback for when this selection is activated. + onActivated = t.optional(t.callback), + + -- Whether this selection is selected or not. + isSelected = t.optional(t.boolean), + + -- If this cell is disabled + isDisabled = t.optional(t.boolean), + + -- The LayoutOrder. + layoutOrder = t.optional(t.number), + + -- optional parameters for RoactGamepad + [Roact.Ref] = t.optional(t.table), + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), +}) + +SmallRadioButtonCell.defaultProps = { + text = "", + isSelected = false, +} + +function SmallRadioButtonCell:init() + self.onSetValue = function() + self.props.onActivated(self.props.key) + end +end + +function SmallRadioButtonCell:render() + assert(self.validateProps(self.props)) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, CELL_SIZE), + BorderSizePixel = 0, + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + }, { + GenericSelectionCell = Roact.createElement(GenericSelectionCell, { + isSelected = self.props.isSelected, + isDisabled = self.props.isDisabled, + defaultImage = DEFAULT_IMAGE, + selectedImage = SELECTED_IMAGE, + defaultImageSize = DEFAULT_IMAGE_SIZE, + selectedImageSize = SELECTED_IMAGE_SIZE, + text = self.props.text, + onActivated = self.onSetValue, + + [Roact.Ref] = self.props[Roact.Ref], + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + }), + }) +end + +return SmallRadioButtonCell \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonCell.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonCell.spec.lua new file mode 100644 index 0000000..4f04824 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonCell.spec.lua @@ -0,0 +1,25 @@ +return function() + local SelectionGroup = script.Parent + local Small = SelectionGroup.Parent + local Cell = Small.Parent + local App = Cell.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local SmallRadioButtonCell = require(script.Parent.SmallRadioButtonCell) + + it("should create and destroy SmallRadioButtonCell without errors", function() + local element = mockStyleComponent({ + smallRadioButtonCell = Roact.createElement(SmallRadioButtonCell, { + key = "1", + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonGroup.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonGroup.lua new file mode 100644 index 0000000..6733140 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonGroup.lua @@ -0,0 +1,103 @@ +local SelectionGroup = script.Parent +local Small = SelectionGroup.Parent +local Cell = Small.Parent +local App = Cell.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local RoactGamepad = require(Packages.RoactGamepad) +local t = require(Packages.t) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local SmallRadioButtonCell = require(UIBlox.App.Cell.Small.SelectionGroup.SmallRadioButtonCell) + +local SmallRadioButtonGroup = Roact.PureComponent:extend("SmallRadioButtonGroup") + +local buttonInterface = t.strictInterface({ + text = t.string, + key = t.string, + isDisabled = t.optional(t.boolean), +}) + +SmallRadioButtonGroup.validateProps = t.strictInterface({ + -- List of text, key pairs that will be used for each radio button. + items = t.optional(t.array(t.tuple(buttonInterface))), + + -- Which key is currently selected. + selectedValue = t.optional(t.string), + + -- Callback for when a cell is activated. + onActivated = t.callback, + + -- Layout order for this component. + layoutOrder = t.optional(t.number), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) + +SmallRadioButtonGroup.defaultProps = { + selectedValue = nil, +} + +function SmallRadioButtonGroup:init() + self.gamepadRefs = RoactGamepad.createRefCache() +end + +function SmallRadioButtonGroup:render() + assert(self.validateProps(self.props)) + + local smallRadioButtonCellGroup = {} + smallRadioButtonCellGroup.layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 1), + }) + for index, button in ipairs(self.props.items) do + if UIBloxConfig.enableExperimentalGamepadSupport then + smallRadioButtonCellGroup["smallRadioButtonCell"..button.key] = + Roact.createElement(RoactGamepad.Focusable[SmallRadioButtonCell], { + key = button.key, + text = button.text, + onActivated = self.props.onActivated, + isSelected = self.props.selectedValue == button.key, + isDisabled = button.isDisabled, + layoutOrder = index, + + [Roact.Ref] = self.gamepadRefs[index], + NextSelectionUp = index > 1 and self.gamepadRefs[index - 1] or nil, + NextSelectionDown = index < #self.props.items and self.gamepadRefs[index + 1] or nil, + }) + else + smallRadioButtonCellGroup["smallRadioButtonCell"..button.key] = Roact.createElement(SmallRadioButtonCell, { + key = button.key, + text = button.text, + onActivated = self.props.onActivated, + isSelected = self.props.selectedValue == button.key, + isDisabled = button.isDisabled, + layoutOrder = index, + }) + end + end + + local gamepadEnabled = (UIBloxConfig.enableExperimentalGamepadSupport and self.props.items and #self.props.items > 0) + + return Roact.createElement(gamepadEnabled and RoactGamepad.Focusable.Frame or "Frame", { + defaultChild = gamepadEnabled and self.gamepadRefs[1] or nil, + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionDown = self.props.NextSelectionDown, + NextSelectionUp = self.props.NextSelectionUp, + [Roact.Ref] = self.props[Roact.Ref], + }, smallRadioButtonCellGroup) +end + +return SmallRadioButtonGroup \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonGroup.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonGroup.spec.lua new file mode 100644 index 0000000..a9cb1ce --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonGroup.spec.lua @@ -0,0 +1,47 @@ +return function() + local SelectionGroup = script.Parent + local Small = SelectionGroup.Parent + local Cell = Small.Parent + local App = Cell.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local SmallRadioButtonGroup = require(script.Parent.SmallRadioButtonGroup) + + local ITEMS = { + { text = "Selection 1", key = "1" }, + { text = "Selection 3", key = "3" }, + { text = "Selection 2", key = "2" }, + { text = "Disabled Cell", key = "4", isDisabled = true } + } + + it("should create and destroy SmallRadioButtonGroup without errors", function() + local element = mockStyleComponent({ + smallRadioButtonGroup = Roact.createElement(SmallRadioButtonGroup, { + onActivated = function() end, + items = ITEMS, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy SmallRadioButtonGroup without errors with all optional props used", function() + local element = mockStyleComponent({ + smallRadioButtonGroup = Roact.createElement(SmallRadioButtonGroup, { + onActivated = function() end, + items = ITEMS, + selectedValue = "1", + layoutOrder = 1, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Constant/IconSize.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Constant/IconSize.lua new file mode 100644 index 0000000..5084343 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Constant/IconSize.lua @@ -0,0 +1,7 @@ +return { + Small = 16, + Regular = 36, + Large = 48, + XLarge = 96, + XXLarge = 192, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/CarouselHeader.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/CarouselHeader.lua new file mode 100644 index 0000000..c65a866 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/CarouselHeader.lua @@ -0,0 +1,106 @@ +local Carousel = script.Parent +local Container = Carousel.Parent +local App = Container.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Style.withStyle) + +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) + +local IconSize = require(App.ImageSet.Enum.IconSize) +local getIconSize = require(App.ImageSet.getIconSize) +local Images = require(App.ImageSet.Images) + +local Core = UIBlox.Core +local Interactable = require(Core.Control.Interactable) +local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel) +local ImageSetComponent = require(Core.ImageSet.ImageSetComponent) + +local MAX_BOUND = 10000 +local SEE_ALL_ARROW = Images["icons/navigation/pushRight_small"] +local TEXT_ICON_PADDING = 4 + +local CarouselHeader = Roact.PureComponent:extend("CarouselHeader") + +CarouselHeader.validateProps = t.strictInterface({ + -- The header text for the carousel + headerText = t.optional(t.string), + + -- The callback for the see all arrow. if nil, the arrow won't be shown + onSeeAll = t.optional(t.callback), + + -- The carousel left margin + carouselMargin = t.optional(t.number), + + -- The layout order + layoutOrder = t.optional(t.number), +}) + +CarouselHeader.defaultProps = { + headerText = "", + carouselMargin = 0, +} + +function CarouselHeader:render() + local headerText = self.props.headerText + local onSeeAll = self.props.onSeeAll + + local layoutOrder = self.props.layoutOrder + local carouselMargin = self.props.carouselMargin + + return withStyle(function(style) + local fontStyle = style.Font.Header1 + local baseSize = style.Font.BaseSize + local fontSize = fontStyle.RelativeSize * baseSize + local textFont = fontStyle.Font + + local textboxBounds = GetTextSize(headerText, fontSize, textFont, Vector2.new(MAX_BOUND, MAX_BOUND)) + local textboxSize = UDim2.fromOffset(textboxBounds.X + TEXT_ICON_PADDING + getIconSize(IconSize.Small), + textboxBounds.Y) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, textboxBounds.Y), + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + CarouselHeaderButton = Roact.createElement(Interactable, { + Position = UDim2.fromOffset(carouselMargin, 0), + Size = textboxSize, + AutoButtonColor = false, + BackgroundTransparency = 1, + [Roact.Event.Activated] = onSeeAll, + --Note State change is not being used right now. + onStateChanged = function()end, + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, TEXT_ICON_PADDING), + }), + HeaderText = Roact.createElement(GenericTextLabel, { + Text = headerText, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + LayoutOrder = 1, + fontStyle = fontStyle, + colorStyle = style.Theme.TextEmphasis, + }), + SeeAllArrow = onSeeAll and Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(getIconSize(IconSize.Small), getIconSize(IconSize.Small)), + BackgroundTransparency = 1, + Image = SEE_ALL_ARROW, + ImageColor3 = style.Theme.TextEmphasis.Color, + ImageTransparency = style.Theme.TextEmphasis.Transparency, + LayoutOrder = 2, + }) or nil, + }) + }) + end) +end + +return CarouselHeader diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/CarouselHeader.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/CarouselHeader.spec.lua new file mode 100644 index 0000000..e681d5e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/CarouselHeader.spec.lua @@ -0,0 +1,40 @@ +return function() + local Carousel = script.Parent + local Container = Carousel.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local CarouselHeader = require(script.Parent.CarouselHeader) + + describe("should create and destroy CarouselHeader with default props without errors", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + Item = Roact.createElement(CarouselHeader) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) + + describe("should create and destroy CarouselHeader without errors", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + Item = Roact.createElement(CarouselHeader, { + headerText = "test header", + onSeeAll = function() end, + carouselMargin = 12, + layoutOrder = 1, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/FreeFlowCarousel.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/FreeFlowCarousel.lua new file mode 100644 index 0000000..f907657 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/FreeFlowCarousel.lua @@ -0,0 +1,96 @@ +local Carousel = script.Parent +local Container = Carousel.Parent +local App = Container.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local FitFrame = require(Packages.FitFrame) +local FitFrameOnAxis = FitFrame.FitFrameOnAxis + +local CarouselHeader = require(Carousel.CarouselHeader) +local HorizontalCarousel = require(Carousel.HorizontalCarousel) + +local DEFAULT_INNER_PADDING = 12 +local DEFAULT_ITEM_PADDING = 12 +local DEFAULT_MARGIN = 24 + +local FreeFlowCarousel = Roact.PureComponent:extend("FreeFlowCarousel") + +FreeFlowCarousel.validateProps = t.strictInterface({ + -- A function to uniquely identify list items. Calling this on the same item twice + -- should give the same result according to ==. + -- See infinite scroller for more details. + identifier = t.optional(t.callback), + + -- The header text for the carousel + headerText = t.optional(t.string), + + -- The callback for the see all arrow. if nil, the arrow won't be shown + onSeeAll = t.optional(t.callback), + + -- The list for the items in the carousel + itemList = t.array(t.any), + + -- A callback function, called with each visible item in the itemList when the list is rendered. + renderItem = t.callback, + + -- The size of the item + itemSize = t.optional(t.Vector2), + + -- The padding between items + itemPadding = t.optional(t.number), + + -- The carousel margin + carouselMargin = t.optional(t.number), + + -- The inner padding between the header and the carousel + innerPadding = t.optional(t.number), + + -- The layoutOrder + layoutOrder = t.optional(t.integer), + + -- A callback function, called when the infinite scroll reaches the leading end of the itemList (index + -- #itemList). + loadNext = t.optional(t.callback), +}) + +FreeFlowCarousel.defaultProps = { + headerText = "", + innerPadding = DEFAULT_INNER_PADDING, + itemPadding = DEFAULT_ITEM_PADDING, + carouselMargin = DEFAULT_MARGIN, +} + +function FreeFlowCarousel:render() + local innerPadding = self.props.innerPadding + local carouselMargin = self.props.carouselMargin + + return Roact.createElement(FitFrameOnAxis, { + axis = FitFrameOnAxis.Axis.Vertical, + minimumSize = UDim2.fromScale(1, 0), + LayoutOrder = self.props.layoutOrder, + contentPadding = UDim.new(0, innerPadding), + BackgroundTransparency = 1, + }, { + CarouselHeader = Roact.createElement(CarouselHeader, { + headerText = self.props.headerText, + onSeeAll = self.props.onSeeAll, + carouselMargin = carouselMargin, + layoutOrder = 1, + }), + Carousel = Roact.createElement(HorizontalCarousel, { + identifier = self.props.identifier, + itemList = self.props.itemList, + renderItem = self.props.renderItem, + itemSize = self.props.itemSize, + itemPadding = innerPadding, + carouselMargin = carouselMargin, + layoutOrder = 2, + loadNext = self.props.loadNext, + }), + }) +end + +return FreeFlowCarousel \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/FreeFlowCarousel.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/FreeFlowCarousel.spec.lua new file mode 100644 index 0000000..3e33012 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/FreeFlowCarousel.spec.lua @@ -0,0 +1,84 @@ +return function() + local Carousel = script.Parent + local Container = Carousel.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local FreeFlowCarousel = require(script.Parent.FreeFlowCarousel) + + describe("should create and destroy with required props without errors", function() + it("should mount and unmount without issue", function() + local items = {} + for i=1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + + local renderItem = function(props) + return Roact.createElement("TextLabel", props) + end + + local element = mockStyleComponent({ + Item = Roact.createElement(FreeFlowCarousel, { + itemList = items, + renderItem = renderItem, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) + + describe("should create and destroy FreeFlowCarousel without errors", function() + it("should mount and unmount without issue", function() + local items = {} + for i=1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + + local renderItem = function(props) + return Roact.createElement("TextLabel", props) + end + + local loadNext = function() + for i=1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + end + + local element = mockStyleComponent({ + Item = Roact.createElement(FreeFlowCarousel, { + identifier = function(item) + return tostring(item) + end, + headerText = "test header", + onSeeAll = function()end, + itemList = items, + renderItem = renderItem, + itemSize = Vector2.new(100, 100), + itemPadding = 12, + carouselMargin = 36, + layoutOrder = 1, + loadNext = loadNext, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/HorizontalCarousel.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/HorizontalCarousel.lua new file mode 100644 index 0000000..c811b92 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/HorizontalCarousel.lua @@ -0,0 +1,269 @@ +local Carousel = script.Parent +local Container = Carousel.Parent +local App = Container.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(App.ImageSet.Images) + +local ScrollButton = require(Carousel.ScrollButton) + +local Core = UIBlox.Core +local Scroller = require(Core.InfiniteScroller).Scroller + +local DEFAULT_ITEM_PADDING = 12 + +local LEFT_ICON = Images["icons/actions/cycleLeft"] +local RIGHT_ICON = Images["icons/actions/cycleRight"] + +local MOTOR_OPTIONS = { + frequency = 2, + dampingRatio = 0.9, + restingPositionLimit = 0.5, + restingVelocityLimit = 0.1, +} + +local HorizontalCarousel = Roact.PureComponent:extend("HorizontalCarousel") + +HorizontalCarousel.validateProps = t.strictInterface({ + -- Required. The list of items to scroll through. + itemList = t.array(t.any), + + -- A callback function, called with each visible item in the itemList when the list is rendered. + renderItem = t.callback, + + -- A function to uniquely identify list items. Calling this on the same item twice + -- should give the same result according to ==. + -- See infinite scroller for more details. + identifier = t.optional(t.callback), + + -- The size of the item + itemSize = t.optional(t.Vector2), + + -- The padding between items + itemPadding = t.optional(t.number), + + -- The carousel margin + carouselMargin = t.optional(t.number), + + -- The layoutOrder + layoutOrder = t.optional(t.integer), + + -- A callback function, called when the carousel reaches the leading end of the itemList (index + -- #itemList). + loadNext = t.optional(t.callback), + + -- A callback function, called when the carousel reaches the trailing end of the itemList (index 1). + loadPrevious = t.optional(t.callback), + + -- Animate the scrolling + animateScrolling = t.optional(t.boolean), +}) + +HorizontalCarousel.defaultProps = { + itemSize = Vector2.new(1, 1), + itemPadding = DEFAULT_ITEM_PADDING, +} + +local function updateScrollState(newIndex, numberOfItemsShown, numOfItems, scrollerFocusLock) + if newIndex == nil then + return {} + end + + --Disable the buttons because there is nothing to show + if numOfItems == nil or numOfItems == 0 then + return { + showLeftButton = false, + showRightButton = false, + } + end + + local targetIndex = newIndex + local showLeftButton = true + local showRightButton = true + + if newIndex <= 1 then + -- If scrolling pass the 1st element then reset the target index to the first item + targetIndex = 1 + scrollerFocusLock = scrollerFocusLock + 1 + showLeftButton = false + elseif newIndex > numOfItems then + -- If scrolling pass the last element then reset the target index to the last item + targetIndex = numOfItems + scrollerFocusLock = scrollerFocusLock + 1 + showRightButton = false + elseif newIndex + numberOfItemsShown > numOfItems then + -- There is no more items outside of the carousel then hide the scroll button + -- There is also no need to update the scrollerFocusLock or target in this case + showRightButton = false + end + + return { + scrollerFocusLock = scrollerFocusLock, + index = targetIndex, + showLeftButton = showLeftButton, + showRightButton = showRightButton, + numOfItems = numOfItems, + } +end + +function HorizontalCarousel.getDerivedStateFromProps(nextProps, lastState) + local numOfItems = #nextProps.itemList + if lastState.numOfItems ~= numOfItems then + return updateScrollState(lastState.index, lastState.numberOfItemsShown, numOfItems, lastState.scrollerFocusLock) + end + return nil +end + +function HorizontalCarousel:init() + self.frameRef = Roact.createRef() + local carouselMetaData = {} + + self:setState({ + scrollerFocusLock = 0, + index = 1, + hovering = false, + showLeftButton = false, + showRightButton = false, + numberOfItemsShown = 0, + numOfItems = 0, + }) + + self.onMouseEnter = function(gui, input) + if input.UserInputType == Enum.UserInputType.MouseMovement then + local anchorIndex = carouselMetaData.anchorIndex + local newState = updateScrollState(anchorIndex, + self.state.numberOfItemsShown, + self.state.numOfItems, + self.state.scrollerFocusLock) + newState.hovering = true + self:setState(newState) + end + end + + self.onMouseLeave = function(gui, input) + if input.UserInputType == Enum.UserInputType.MouseMovement then + self:setState({ + hovering = false, + }) + end + end + + self.onResize = function(rbx) + local totalWidth = rbx.AbsoluteSize.X + local numberOfItemsShown = math.floor(totalWidth / (self.props.itemSize.X + self.props.itemPadding)) + self:setState({ + numberOfItemsShown = numberOfItemsShown, + }) + end + + self.onScrollUpdate = function(data) + carouselMetaData = data + end + + self.scrollLeft = function() + if carouselMetaData.animationActive then + return + end + local newIndex = carouselMetaData.anchorIndex - self.state.numberOfItemsShown + self:setState( + updateScrollState(newIndex, self.state.numberOfItemsShown, self.state.numOfItems, self.state.scrollerFocusLock + 1) + ) + end + + self.scrollRight = function() + if carouselMetaData.animationActive then + return + end + local newIndex = carouselMetaData.anchorIndex + self.state.numberOfItemsShown + self:setState( + updateScrollState(newIndex, self.state.numberOfItemsShown, self.state.numOfItems, self.state.scrollerFocusLock + 1) + ) + end +end + +function HorizontalCarousel:render() + local itemList = self.props.itemList + local itemSize = self.props.itemSize + local renderItem = self.props.renderItem + local itemPadding = self.props.itemPadding + + local carouselMargin = self.props.carouselMargin + local layoutOrder = self.props.layoutOrder + + local loadNext = self.props.loadNext + local loadPrevious = self.props.loadPrevious + + local scrollLeftButton + if self.state.hovering and self.state.showLeftButton then + scrollLeftButton = Roact.createElement(ScrollButton, { + icon = LEFT_ICON, + callback = self.scrollLeft, + }) + end + + local scrollRightButton + if self.state.hovering and self.state.showRightButton then + scrollRightButton = Roact.createElement(ScrollButton, { + icon = RIGHT_ICON, + callback = self.scrollRight, + }) + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, itemSize.Y), + LayoutOrder = layoutOrder, + BackgroundTransparency = 1, + [Roact.Ref] = self.frameRef, + [Roact.Event.InputBegan] = self.onMouseEnter, + [Roact.Event.InputEnded] = self.onMouseLeave, + [Roact.Change.AbsoluteSize] = self.onResize, + },{ + LeftMargin = Roact.createElement("Frame", { + Position = UDim2.fromScale(0, 0), + AnchorPoint = Vector2.new(0, 0), + Size = UDim2.new(0, carouselMargin, 1, 0), + BackgroundTransparency = 1, + ZIndex = 2, + },{ + ScrollLeftButton = scrollLeftButton, + }), + InfiniteScrollerCarousel = self.state.numberOfItemsShown > 0 and Roact.createElement(Scroller, { + identifier = self.props.identifier, + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromOffset(carouselMargin, 0), + ScrollBarThickness = 0, + ClipsDescendants = false, + padding = UDim.new(0, itemPadding), + orientation = Scroller.Orientation.Right, + itemList = itemList, + loadingBuffer = 1, + mountingBuffer = self.state.numberOfItemsShown * 3 * itemSize.X, + loadNext = loadNext, + loadPrevious = loadPrevious, + focusLock = self.state.scrollerFocusLock, + focusIndex = self.state.index, + anchorLocation = UDim.new(1, 0), + estimatedItemSize = itemSize.X, + renderItem = renderItem, + onScrollUpdate = self.onScrollUpdate, + animateScrolling = true, + animateOptions = MOTOR_OPTIONS, + }) or nil, + RightMargin = Roact.createElement("Frame", { + Position = UDim2.fromScale(1, 0), + AnchorPoint = Vector2.new(1, 0), + Size = UDim2.new(0, carouselMargin, 1, 0), + BackgroundTransparency = 1, + ZIndex = 2, + },{ + ScrollRightButton = scrollRightButton, + }), + }) +end + +return HorizontalCarousel diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/HorizontalCarousel.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/HorizontalCarousel.spec.lua new file mode 100644 index 0000000..1e5db2e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/HorizontalCarousel.spec.lua @@ -0,0 +1,93 @@ +return function() + local Carousel = script.Parent + local Container = Carousel.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local HorizontalCarousel = require(script.Parent.HorizontalCarousel) + + describe("should create and destroy with required props without errors", function() + it("should mount and unmount without issue", function() + local items = {} + for i=1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + + local renderItem = function(props) + return Roact.createElement("TextLabel", props) + end + + local element = mockStyleComponent({ + Item = Roact.createElement(HorizontalCarousel, { + itemList = items, + renderItem = renderItem, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) + + describe("should create and destroy HorizontalCarousel without errors", function() + it("should mount and unmount without issue", function() + local items = {} + for i=1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + + local renderItem = function(props) + return Roact.createElement("TextLabel", props) + end + + local loadNext = function() + for i=1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + end + + local loadPrevious = function() + items = {} + for i=1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + end + + local element = mockStyleComponent({ + Item = Roact.createElement(HorizontalCarousel, { + identifier = function(item) + return tostring(item) + end, + itemList = items, + renderItem = renderItem, + itemSize = Vector2.new(100, 100), + itemPadding = 12, + carouselMargin = 36, + layoutOrder = 1, + loadNext = loadNext, + loadPrevious = loadPrevious, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/ScrollButton.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/ScrollButton.lua new file mode 100644 index 0000000..d535bd5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/ScrollButton.lua @@ -0,0 +1,53 @@ +local Carousel = script.Parent +local Container = Carousel.Parent +local App = Container.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Style.withStyle) + +local Core = UIBlox.Core +local getIconSize = require(App.ImageSet.getIconSize) +local IconSize = require(App.ImageSet.Enum.IconSize) +local Interactable = require(Core.Control.Interactable) +local ImageSetComponent = require(Core.ImageSet.ImageSetComponent) + +local ScrollButton = Roact.PureComponent:extend("ScrollButton") + +ScrollButton.validateProps = t.strictInterface({ + -- The icon of the button + icon = t.table, + + -- Callback action + callback = t.callback, +}) + +function ScrollButton:render() + return withStyle(function(style) + return Roact.createElement(Interactable, { + AutoButtonColor = false, + Size = UDim2.fromScale(1, 1), + BackgroundColor3 = style.Theme.BackgroundUIContrast.Color, + BackgroundTransparency = style.Theme.BackgroundUIContrast.Transparency, + BorderSizePixel = 0, + [Roact.Event.Activated] = self.props.callback, + --Note State change is not being used right now. + onStateChanged = function()end, + }, { + Icon = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(getIconSize(IconSize.Medium), getIconSize(IconSize.Medium)), + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = self.props.icon, + ImageColor3 = style.Theme.IconEmphasis.Color, + ImageTransparency = style.Theme.IconEmphasis.Transparency, + }), + }) + + end) +end + +return ScrollButton \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/ScrollButton.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/ScrollButton.spec.lua new file mode 100644 index 0000000..67a75cd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/ScrollButton.spec.lua @@ -0,0 +1,29 @@ +return function() + local Carousel = script.Parent + local Container = Carousel.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local Images = require(App.ImageSet.Images) + local ScrollButton = require(script.Parent.ScrollButton) + local icon = Images["icons/actions/cycleLeft"] + + it("should create and destroy ScrollButton without errors", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + Item = Roact.createElement(ScrollButton, { + icon = icon, + callback = function()end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/FailedStatePage.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/FailedStatePage.lua new file mode 100644 index 0000000..1d1231f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/FailedStatePage.lua @@ -0,0 +1,61 @@ +local Indicator = script.Parent +local App = Indicator.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(UIBlox.App.ImageSet.Images) +local EmptyState = require(UIBlox.App.Indicator.EmptyState) +local SecondaryButton = require(UIBlox.App.Button.SecondaryButton) + +local RenderOnFailedStyle = require(UIBlox.App.Loading.Enum.RenderOnFailedStyle) + +local RETRY_BUTTON_HEIGHT = 44 +local RETRY_BUTTON_WIDTH = 44 +local RETRY_BACKGROUND_IMAGE = "icons/common/refresh" +local ICON = 'icons/status/noconnection_large' + +local FailedStatePage = Roact.PureComponent:extend("FailedStatePage") + +FailedStatePage.validateProps = t.strictInterface({ + -- The onRetry function is called when a button is pressed + onRetry = t.optional(t.callback), + -- The renderOnFailed renders a page from a RenderOnFailedStyle enum + renderOnFailed = t.optional(RenderOnFailedStyle.isEnumValue), + -- text for emptystate + text = t.string, +}) + +FailedStatePage.defaultProps = { + renderOnFailed = RenderOnFailedStyle.RetryButton, +} + +function FailedStatePage:render() + if self.props.renderOnFailed == RenderOnFailedStyle.EmptyStatePage then + return Roact.createElement(EmptyState, { + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + onActivated = self.props.onRetry, + icon = Images[ICON], + text = self.props.text, + }) + elseif self.props.renderOnFailed == RenderOnFailedStyle.RetryButton then + if self.props.onRetry then + return Roact.createElement(SecondaryButton, { + size = UDim2.fromOffset(RETRY_BUTTON_HEIGHT, RETRY_BUTTON_WIDTH), + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + onActivated = self.props.onRetry, + icon = Images[RETRY_BACKGROUND_IMAGE], + }) + else + error("OnRetry callback empty. OnRetry needs to be a function to render the RetryButton") + end + else + error("Failed to provide proper RenderOnFailedStyle") + end +end + +return FailedStatePage diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/FailedStatePage.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/FailedStatePage.spec.lua new file mode 100644 index 0000000..546e5e4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/FailedStatePage.spec.lua @@ -0,0 +1,36 @@ +return function() + local Container = script.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local FailedStatePage = require(script.Parent.FailedStatePage) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + local RenderOnFailedStyle = require(UIBlox.App.Loading.Enum.RenderOnFailedStyle) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + it("should mount and unmount with the optional props", function() + local element = mockStyleComponent({ + failedStatePage = Roact.createElement(FailedStatePage, { + onRetry = function() end, + renderOnFailed = RenderOnFailedStyle.RetryButton, + text = "Failed" + }) + }) + local instance = Roact.mount(element, frame, "FailedStatePage") + Roact.unmount(instance) + end) + it("should error when RetryButton passed without onRetry callback", function() + local element = mockStyleComponent({ + failedStatePage = Roact.createElement(FailedStatePage, { + renderOnFailed = RenderOnFailedStyle.RetryButton, + text = "Failed" + }) + }) + expect(function() + return Roact.mount(element, frame, "FailedStatePage") + end).to.throw() + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateContainer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateContainer.lua new file mode 100644 index 0000000..d5793e5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateContainer.lua @@ -0,0 +1,110 @@ +local Container = script.Parent +local App = Container.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local RetrievalStatus = require(UIBlox.App.Loading.Enum.RetrievalStatus) +local LoadingStateEnum = require(UIBlox.App.Loading.Enum.LoadingState) +local ReloadingStyle = require(UIBlox.App.Loading.Enum.ReloadingStyle) +local RenderOnFailedStyle = require(UIBlox.App.Loading.Enum.RenderOnFailedStyle) +local LoadingStatePage = require(UIBlox.App.Container.LoadingStatePage) +local FailedStatePage = require(UIBlox.App.Container.FailedStatePage) +local LoadingStateTables = require(UIBlox.App.Container.LoadingStateTables) + +local INITIAL_RELOADING_STYLE = ReloadingStyle.AllowReload +local INITIAL_LOADING_STATE = LoadingStateEnum.Loading + +local LoadingStateContainer = Roact.PureComponent:extend("LoadingStateContainer") + +LoadingStateContainer.validateProps = t.strictInterface({ + -- The dataStatus is a retrieval status enum + dataStatus = RetrievalStatus.isEnumValue, + -- Text for FailedStatePage EmptyState + failedText = t.string, + -- The onRetry function is called when a button is pressed + onRetry = t.optional(t.callback), + -- The renderOnLoaded is rendered if loading state is LoadingStateEnum.Loaded + renderOnLoaded = t.callback, + -- The renderOnFailed is rendered if loading state is LoadingStateEnum.Failed or is a RenderOnFailedStyle Enum. + renderOnFailed = t.optional(t.union(RenderOnFailedStyle.isEnumValue, t.callback)), + -- The renderOnLoading is rendered if loading state is LoadingStateEnum.Loading + renderOnLoading = t.optional(t.callback), + -- The reloadingStyle is the style of state table + reloadingStyle = t.optional(ReloadingStyle.isEnumValue), +}) + +LoadingStateContainer.defaultProps = { + renderOnFailed = RenderOnFailedStyle.RetryButton, + reloadingStyle = INITIAL_RELOADING_STYLE, +} + +function LoadingStateContainer:init() + self.onStateChange = function(oldState, newState) + self:setState({ + loadingState = newState, + }) + end + + self.updateState = function() + self.state.currentReloadingStyle:onStateChange(self.onStateChange) + self.state.currentReloadingStyle.events[self.props.dataStatus]() + end + + self:setState({ + loadingState = INITIAL_LOADING_STATE, + currentReloadingStyle = LoadingStateTables[INITIAL_RELOADING_STYLE](), + }) + + self.statePages = { + [LoadingStateEnum.Loading] = function() + if self.props.renderOnLoading then + return self.props.renderOnLoading() + else + return Roact.createElement(LoadingStatePage) + end + end, + [LoadingStateEnum.Failed] = function() + if t.callback(self.props.renderOnFailed) then + return self.props.renderOnFailed() + else + return Roact.createElement(FailedStatePage, { + onRetry = self.props.onRetry, + renderOnFailed = self.props.renderOnFailed, + text = self.props.failedText, + }) + end + end, + [LoadingStateEnum.Loaded] = function() + return self.props.renderOnLoaded() + end, + } +end + +function LoadingStateContainer.getDerivedStateFromProps(nextProps, lastState) + if lastState.currentReloadingStyle ~= nil and lastState.currentReloadingStyle ~= nextProps.reloadingStyle then + return { + --reloadingStyle = nextProps.reloadingStyle, + currentReloadingStyle = LoadingStateTables[nextProps.reloadingStyle]() + } + end +end + +function LoadingStateContainer:render() + return self.statePages[self.state.loadingState]() +end + +function LoadingStateContainer:didMount() + self.updateState() +end + +function LoadingStateContainer:didUpdate(prevProps) + if prevProps.dataStatus ~= self.props.dataStatus or prevProps.reloadingStyle ~= self.props.reloadingStyle then + self.updateState() + end +end + + +return LoadingStateContainer diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateContainer.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateContainer.spec.lua new file mode 100644 index 0000000..8cdd08e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateContainer.spec.lua @@ -0,0 +1,99 @@ +return function() + local Container = script.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local RetrievalStatus = require(UIBlox.App.Loading.Enum.RetrievalStatus) + local ReloadingStyle = require(UIBlox.App.Loading.Enum.ReloadingStyle) + local RenderOnFailedStyle = require(UIBlox.App.Loading.Enum.RenderOnFailedStyle) + local LoadingStateContainer = require(script.Parent.LoadingStateContainer) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + + it("should mount and unmount with only required props", function() + local element = mockStyleComponent({ + loadingStateContainer = Roact.createElement(LoadingStateContainer, { + renderOnLoaded = function() end, + dataStatus = RetrievalStatus.NotStarted, + failedText = "failed", + }) + }) + local instance = Roact.mount(element, frame, "LoadingStateContainer") + Roact.unmount(instance) + end) + + it("should mount and unmount with all props", function() + local element = mockStyleComponent({ + loadingStateContainer = Roact.createElement(LoadingStateContainer, { + dataStatus = RetrievalStatus.NotStarted, + onRetry = function() end, + renderOnLoaded = function() end, + renderOnFailed = function() end, + renderOnLoading = function() end, + reloadingStyle = ReloadingStyle.AllowReload, + failedText = "failed", + }) + }) + local instance = Roact.mount(element, frame, "LoadingStateContainer") + Roact.unmount(instance) + end) + + it("should render loading state page", function() + local element = mockStyleComponent({ + loadingStateContainer = Roact.createElement(LoadingStateContainer, { + dataStatus = RetrievalStatus.Fetching, + onRetry = function() end, + renderOnLoaded = function() end, + renderOnFailed = RenderOnFailedStyle.EmptyStatePage, + reloadingStyle = ReloadingStyle.AllowReload, + failedText = "failed", + }) + }) + local instance = Roact.mount(element, frame, "LoadingStateContainer") + local loadingIcon = frame:FindFirstChild("inner", true) + expect(loadingIcon).to.be.ok() + Roact.unmount(instance) + end) + + it("should render failed state page emptyState", function() + local element = mockStyleComponent({ + loadingStateContainer = Roact.createElement(LoadingStateContainer, { + dataStatus = RetrievalStatus.Failed, + onRetry = function() end, + renderOnLoaded = function() end, + renderOnFailed = RenderOnFailedStyle.EmptyStatePage, + reloadingStyle = ReloadingStyle.AllowReload, + failedText = "failed", + }) + }) + local instance = Roact.mount(element, frame, "LoadingStateContainer") + local content = frame:FindFirstChild("Content", true) + local buttonFrame = content:FindFirstChild("buttonFrame", true) + + expect(buttonFrame).to.be.ok() + Roact.unmount(instance) + end) + + it("should render failed state page RetryButton", function() + local element = mockStyleComponent({ + loadingStateContainer = Roact.createElement(LoadingStateContainer, { + dataStatus = RetrievalStatus.Failed, + onRetry = function() end, + renderOnLoaded = function() end, + renderOnFailed = RenderOnFailedStyle.RetryButton, + reloadingStyle = ReloadingStyle.AllowReload, + failedText = "failed", + }) + }) + local instance = Roact.mount(element, frame, "LoadingStateContainer") + local ButtonContent = frame:FindFirstChild("ButtonContent", true) + + expect(ButtonContent).to.be.ok() + Roact.unmount(instance) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStatePage.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStatePage.lua new file mode 100644 index 0000000..c9bfc55 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStatePage.lua @@ -0,0 +1,20 @@ +local Indicator = script.Parent +local App = Indicator.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local LoadingSpinner = require(UIBlox.App.Loading.LoadingSpinner) + +local LoadingStatePage = Roact.PureComponent:extend("LoadingStatePage") + +function LoadingStatePage:render() + return Roact.createElement(LoadingSpinner, { + size = UDim2.fromOffset(42, 42), + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + }) +end + +return LoadingStatePage diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStatePage.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStatePage.spec.lua new file mode 100644 index 0000000..37c497b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStatePage.spec.lua @@ -0,0 +1,21 @@ +return function() + local Container = script.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local LoadingStatePage = require(script.Parent.LoadingStatePage) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + it("should mount and unmount", function() + local element = mockStyleComponent({ + loadingStatePage = Roact.createElement(LoadingStatePage) + }) + local instance = Roact.mount(element, frame, "LoadingStatePage") + Roact.unmount(instance) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateTables.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateTables.lua new file mode 100644 index 0000000..da1f7e1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateTables.lua @@ -0,0 +1,62 @@ +local Container = script.Parent +local App = Container.Parent +local UIBlox = App.Parent + +local StateTable = require(UIBlox.StateTable.StateTable) +local ReloadingStyle = require(UIBlox.App.Loading.Enum.ReloadingStyle) +local RetrievalStatus = require(UIBlox.App.Loading.Enum.RetrievalStatus) +local LoadingStateEnum = require(UIBlox.App.Loading.Enum.LoadingState) + +local INITIAL_LOADING_STATE = LoadingStateEnum.Loading + +local LoadingStateTables = {} + +-- This state table allows reloading after it has loaded +LoadingStateTables[ReloadingStyle.AllowReload] = function() + return StateTable.new('AllowReload', INITIAL_LOADING_STATE, {}, { + [LoadingStateEnum.Loading] = { + [RetrievalStatus.NotStarted] = {}, + [RetrievalStatus.Fetching] = {}, + [RetrievalStatus.Done] = { nextState = LoadingStateEnum.Loaded }, + [RetrievalStatus.Failed] = { nextState = LoadingStateEnum.Failed }, + }, + [LoadingStateEnum.Loaded] = { + [RetrievalStatus.NotStarted] = {}, + [RetrievalStatus.Fetching] = { nextState = LoadingStateEnum.Loading }, + [RetrievalStatus.Done] = {}, + [RetrievalStatus.Failed] = { nextState = LoadingStateEnum.Failed }, + }, + [LoadingStateEnum.Failed] = { + [RetrievalStatus.NotStarted] = {}, + [RetrievalStatus.Fetching] = { nextState = LoadingStateEnum.Loading }, + [RetrievalStatus.Done] = { nextState = LoadingStateEnum.Loaded }, + [RetrievalStatus.Failed] = {}, + }, + }) +end + +-- This state table locks reloading after it has loaded. +LoadingStateTables[ReloadingStyle.LockReload] = function() + return StateTable.new('LockReload', INITIAL_LOADING_STATE, {}, { + [LoadingStateEnum.Loading] = { + [RetrievalStatus.NotStarted] = {}, + [RetrievalStatus.Fetching] = {}, + [RetrievalStatus.Done] = { nextState = LoadingStateEnum.Loaded }, + [RetrievalStatus.Failed] = { nextState = LoadingStateEnum.Failed }, + }, + [LoadingStateEnum.Loaded] = { + [RetrievalStatus.NotStarted] = {}, + [RetrievalStatus.Fetching] = { nextState = LoadingStateEnum.Failed }, + [RetrievalStatus.Done] = {}, + [RetrievalStatus.Failed] = { nextState = LoadingStateEnum.Failed }, + }, + [LoadingStateEnum.Failed] = { + [RetrievalStatus.NotStarted] = {}, + [RetrievalStatus.Fetching] = { nextState = LoadingStateEnum.Loading }, + [RetrievalStatus.Done] = { nextState = LoadingStateEnum.Loaded }, + [RetrievalStatus.Failed] = {}, + }, + }) +end + +return LoadingStateTables diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/VerticalScrollView.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/VerticalScrollView.lua new file mode 100644 index 0000000..2bbe610 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/VerticalScrollView.lua @@ -0,0 +1,160 @@ +local RunService = game:GetService("RunService") +local UserInputService = game:GetService("UserInputService") + +local App = script.Parent.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Cryo = require(Packages.Cryo) +local Otter = require(Packages.Otter) +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local withStyle = require(Packages.UIBlox.Core.Style.withStyle) + +local PADDING_HORIZONTAL = 24 +local SCROLL_BAR_RIGHT_PADDING = 4 +local MOUSE_SCROLL_BAR_THICKNESS = 8 +local TOUCH_OR_CONTROLLER_SCROLL_BAR_THICKNESS = 2 +local CANVAS_SIZE_X = UDim.new(0, 0) +local HIDE_SIDEBAR_AFTER_IN_SECONDS = 0.70 +local SPRING_PARAMETERS = { + frequency = 3, + dampingRatio = 1.5, +} + +local VerticalScrollView = Roact.Component:extend() + +VerticalScrollView.defaultProps = { + -- Frame Props + size = UDim2.new(1, 0, 1, 0), + -- ScrollingFrame Props + canvasSizeY = UDim.new(2, 0), + paddingHorizontal = PADDING_HORIZONTAL, +} + +VerticalScrollView.validateProps = t.strictInterface({ + -- Frame Props + size = t.optional(t.UDim2), + position = t.optional(t.UDim2), + + -- ScrollingFrame Props + canvasSizeY = t.optional(t.UDim), + paddingHorizontal = t.optional(t.numberMin(PADDING_HORIZONTAL/2)), + + -- Children + [Roact.Children] = t.optional(t.table) +}) + +function VerticalScrollView:init() + self:setState({ + scrollBarThickness = 0, + scrollingWithTouch = false, + }) + + self.scrollBarImageTransparency, self.updateScrollBarImageTransparency = Roact.createBinding(0) + self.scrollBarImageTransparencyMotor = Otter.createSingleMotor(0) + self.scrollBarImageTransparencyMotor:onStep(self.updateScrollBarImageTransparency) + + self.lastTimeCanvasPositionChanged = tick() + + self.waitToHideSidebarConnection = nil + self.waitToHideSidebar = function() + local currentTime = tick() + local delta = currentTime - self.lastTimeCanvasPositionChanged + if delta > HIDE_SIDEBAR_AFTER_IN_SECONDS then + self.scrollBarImageTransparencyMotor:setGoal(Otter.spring(1, SPRING_PARAMETERS)) + self.disconnectWaitToHideSidebar() + end + end + self.disconnectWaitToHideSidebar = function() + if self.waitToHideSidebarConnection then + self.waitToHideSidebarConnection:Disconnect() + self.waitToHideSidebarConnection = nil + end + end +end + +function VerticalScrollView:render() + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + local canvasSizeY = self.props.canvasSizeY + local children = self.props[Roact.Children] or {} + local position = self.props.position + local size = self.props.size + local paddingHorizontal = self.props.paddingHorizontal + + local scrollBarThickness = self.state.scrollBarThickness + + local scrollingFrameChildren = Cryo.Dictionary.join({ + scrollingFrameInnerMargin = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0,paddingHorizontal), + PaddingRight = UDim.new(0, paddingHorizontal - SCROLL_BAR_RIGHT_PADDING), }), + }, + children + ) + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Position = position, + Size = size, + }, { + scrollingFrameOuterMargins = Roact.createElement("UIPadding", { + PaddingRight = UDim.new(0, SCROLL_BAR_RIGHT_PADDING), + }), + scrollingFrame = Roact.createElement("ScrollingFrame", { + Active = true, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.fromScale(1, 1), + -- ScrollingFrame Specific + CanvasSize = UDim2.new(CANVAS_SIZE_X, canvasSizeY), + ScrollBarImageColor3 = theme.UIEmphasis.Color, + ScrollBarImageTransparency = self.scrollBarImageTransparency, + ScrollBarThickness = scrollBarThickness, + ScrollingDirection = Enum.ScrollingDirection.Y, + + -- https://jira.rbx.com/browse/MOBLUAPP-2451 + -- TODO: 1.) Currently code assumes that Mouse is on desktop and touch is on mobile + -- On a mac touch pad is reported as mouse not as touch + -- No sure how many users use mouse on a phone + -- TODO: 2.) how to handle controller actions - when we do this, + -- we should make this part of the code platform specific + [Roact.Event.InputBegan] = function(instance, input) + if input.UserInputType == Enum.UserInputType.MouseMovement then + self.disconnectWaitToHideSidebar() + self:setState({ + scrollBarThickness = MOUSE_SCROLL_BAR_THICKNESS, + }) + self.scrollBarImageTransparencyMotor:setGoal(Otter.instant(0)) + end + end, + [Roact.Event.InputEnded] = function(instance, input) + if input.UserInputType == Enum.UserInputType.MouseMovement then + self.disconnectWaitToHideSidebar() + self.scrollBarImageTransparencyMotor:setGoal(Otter.instant(1)) + end + end, + [Roact.Change.CanvasPosition] = function() + self.lastTimeCanvasPositionChanged = tick() + if not self.waitToHideSidebarConnection and + UserInputService:GetLastInputType() == Enum.UserInputType.Touch + then + self.scrollBarImageTransparencyMotor:setGoal(Otter.instant(0)) + self:setState({ + scrollBarThickness = TOUCH_OR_CONTROLLER_SCROLL_BAR_THICKNESS, + }) + self.waitToHideSidebarConnection = RunService.Heartbeat:Connect(self.waitToHideSidebar) + end + end, + }, scrollingFrameChildren) + }) + end) +end + +function VerticalScrollView:willUnmount() + self.disconnectWaitToHideSidebar() +end + +return VerticalScrollView diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/VerticalScrollView.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/VerticalScrollView.spec.lua new file mode 100644 index 0000000..66a4164 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/VerticalScrollView.spec.lua @@ -0,0 +1,52 @@ +return function() + local Container = script.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local VerticalScrollView = require(Container.VerticalScrollView) + + describe("mount/unmount", function() + it("should mount and unmount with default properties", function() + local verticalScrollViewWithStyle = mockStyleComponent({ + verticalScrollView = Roact.createElement(VerticalScrollView) + }) + local handle = Roact.mount(verticalScrollViewWithStyle) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + it("should mount and unmount with valid properties", function() + local verticalScrollViewWithStyle = mockStyleComponent({ + verticalScrollView = Roact.createElement(VerticalScrollView, { + position = UDim2.new(0, 50, 0,100), + size = UDim2.new(1, 30, 1, 50), + canvasSizeY = UDim.new(2, 0), + paddingHorizontal = 12, + }) + }) + local handle = Roact.mount(verticalScrollViewWithStyle) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + -- skipping this until https://jira.rbx.com/browse/MOBLUAPP-2424 is merged to CI + itSKIP("mount should throw when created with invalid properties", function() + local function expectToThrowForInvalidProps(props) + local verticalScrollViewWithStyle = mockStyleComponent({ + verticalScrollView = Roact.createElement(VerticalScrollView, props) + }) + expect(function() + Roact.mount(verticalScrollViewWithStyle) + end).to.throw() + end + + expectToThrowForInvalidProps({ position = 3 }) + expectToThrowForInvalidProps({ size = 3 }) + expectToThrowForInvalidProps({ canvasSizeY = 3 }) + expectToThrowForInvalidProps({ paddingHorizontal = 3 }) + expectToThrowForInvalidProps({ NotInTheInterface = "Really it is not there" }) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/getPageMargin.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/getPageMargin.lua new file mode 100644 index 0000000..af7d70c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/getPageMargin.lua @@ -0,0 +1,17 @@ +local Container = script.Parent +local App = Container.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +return function(containerWidth) + assert(t.number(containerWidth)) + if containerWidth <= 360 then + return 12 + elseif containerWidth > 360 and containerWidth <= 599 then + return 24 + else + return 48 + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/getPageMargin.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/getPageMargin.spec.lua new file mode 100644 index 0000000..023ad16 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/getPageMargin.spec.lua @@ -0,0 +1,22 @@ +return function() + local getPageMargin = require(script.Parent.getPageMargin) + describe("getPageMargin()", function() + it("should return the number 12", function() + local pageMargin = getPageMargin(312) + expect(pageMargin).to.equal(12) + end) + it("should return the number 24", function() + local pageMargin = getPageMargin(400) + expect(pageMargin).to.equal(24) + end) + it("should return the number 48", function() + local pageMargin = getPageMargin(700) + expect(pageMargin).to.equal(48) + end) + it("it should error", function() + expect(function() + return getPageMargin("String") + end).to.throw() + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Context/ContentProvider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Context/ContentProvider.lua new file mode 100644 index 0000000..3a30d14 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Context/ContentProvider.lua @@ -0,0 +1,6 @@ +local ContentProvider = game:GetService("ContentProvider") + +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) + +return Roact.createContext(ContentProvider) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Alert.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Alert.lua new file mode 100644 index 0000000..dbffa8a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Alert.lua @@ -0,0 +1,213 @@ +local AlertRoot = script.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local RoactGamepad = require(Packages.RoactGamepad) +local Cryo = require(Packages.Cryo) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local GetTextHeight = require(UIBlox.Core.Text.GetTextHeight) +local Images = require(AppRoot.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local ButtonStack = require(AppRoot.Button.ButtonStack) + +local FitFrame = require(Packages.FitFrame) +local FitFrameOnAxis = FitFrame.FitFrameOnAxis + +local AlertType = require(AlertRoot.Enum.AlertType) +local AlertTitle = require(AlertRoot.AlertTitle) + +local BACKGROUND_IMAGE = "component_assets/circle_17" +local MARGIN = 24 + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local enableAlertTitleIconConfig = UIBloxConfig.enableAlertTitleIconConfig + +local validateButtonStack = require(AppRoot.Button.Validator.validateButtonStack) + +local Alert = Roact.PureComponent:extend("Alert") + +local validateProps = t.strictInterface({ + alertType = enumerateValidator(AlertType), + anchorPoint = t.optional(t.Vector2), + bodyText = t.optional(t.string), + buttonStackInfo = t.optional(validateButtonStack), + margin = t.optional(t.table), + maxWidth = t.optional(t.number), + minWidth = t.optional(t.number), + middleContent = t.optional(t.callback), + middleContentPaddingBetweenBodyText = t.optional(t.number), + onMounted = t.optional(t.callback), + onAbsoluteSizeChanged = t.optional(t.callback), + paddingBetween = t.optional(t.number), + position = t.optional(t.UDim2), + screenSize = t.Vector2, + title = t.string, + titleIcon = enableAlertTitleIconConfig and t.optional(t.union(t.table, t.string)) or nil, + titleIconSize = enableAlertTitleIconConfig and t.optional(t.number) or nil, + titlePadding = t.optional(t.number), + titlePaddingWithIcon = t.optional(t.number), + + --Gamepad props + defaultChildRef = t.optional(t.table), + isMiddleContentFocusable = t.optional(t.boolean), +}) + +Alert.defaultProps = { + anchorPoint = Vector2.new(0.5, 0.5), + margin = { + top = 0, + bottom = MARGIN, + left = MARGIN, + right = MARGIN, + }, + maxWidth = 400, + middleContentPaddingBetweenBodyText = 12, + minWidth = 272, + paddingBetween = 24, + position = UDim2.new(0.5, 0, 0.5, 0), +} + +function Alert:init() + self.contentSize, self.changeContentSize = Roact.createBinding(Vector2.new(0, 0)) + + if UIBloxConfig.enableExperimentalGamepadSupport and self.props.isMiddleContentFocusable then + self.middleContentRef = Roact.createRef() + end + self.buttonStackRef = Roact.createRef() +end + +function Alert:didMount() + if self.props.onMounted then + self.props.onMounted() + end +end + +function Alert:render() + assert(validateProps(self.props)) + local isMiddleContentFocusable = UIBloxConfig.enableExperimentalGamepadSupport and self.props.isMiddleContentFocusable + + local totalWidth = math.clamp(self.props.screenSize.X - self.props.margin.left - self.props.margin.right, + self.props.minWidth, self.props.maxWidth) + local innerWidth = totalWidth - self.props.margin.left - self.props.margin.right + + return withStyle(function(stylePalette) + local font = stylePalette.Font + local theme = stylePalette.Theme + local textFont = font.Body.Font + + local fontSize = font.BaseSize * font.Body.RelativeSize + + local fullTextHeight = self.props.bodyText + and GetTextHeight(self.props.bodyText, textFont, fontSize, innerWidth) + or 0 + + local backgroundTransparency + local imageColor + local imageTransparency + if self.props.alertType == AlertType.Interactive then + imageColor = theme.BackgroundUIDefault.Color + imageTransparency = theme.BackgroundUIDefault.Transparency + backgroundTransparency = 1 + else + backgroundTransparency = theme.BackgroundUIContrast.Transparency + imageTransparency = 1 + end + + local buttonStackInfo = self.props.buttonStackInfo + if UIBloxConfig.enableExperimentalGamepadSupport and buttonStackInfo then + buttonStackInfo = Cryo.Dictionary.join(buttonStackInfo, { + [Roact.Ref] = self.buttonStackRef, + NextSelectionUp = self.middleContentRef, + }) + end + + return Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable[ImageSetComponent.Button] or ImageSetComponent.Button, { + Position = self.props.position, + AnchorPoint = self.props.anchorPoint, + Size = self.contentSize:map(function(absoluteSize) + return UDim2.new(0, absoluteSize.X, 0, absoluteSize.Y) + end), + BackgroundColor3 = theme.BackgroundUIDefault.Color, + BackgroundTransparency = backgroundTransparency, + BorderSizePixel = 0, + Image = Images[BACKGROUND_IMAGE], + ImageColor3 = imageColor, + ImageTransparency = imageTransparency, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + AutoButtonColor = false, + ClipsDescendants = true, + Selectable = false, + + [Roact.Ref] = self.props.defaultChildRef, + [Roact.Change.AbsoluteSize] = self.props.onAbsoluteSizeChanged, + defaultChild = UIBloxConfig.enableExperimentalGamepadSupport and self.buttonStackRef or nil, + }, { + AlertContents = Roact.createElement(FitFrameOnAxis, { + contentPadding = UDim.new(0, self.props.paddingBetween), + margin = self.props.margin, + minimumSize = UDim2.new(0, totalWidth, 0, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + [Roact.Change.AbsoluteSize] = function(rbx) + self.changeContentSize(rbx.AbsoluteSize) + end, + }, { + TitleContainer = Roact.createElement(AlertTitle, { + layoutOrder = 1, + margin = self.props.margin, + maxWidth = self.props.maxWidth, + minWidth = self.props.minWidth, + screenSize = self.props.screenSize, + title = self.props.title, + titleIcon = self.props.titleIcon, + titleIconSize = self.props.titleIconSize, + titlePadding = self.props.titlePadding, + titlePaddingWithIcon = self.props.titlePaddingWithIcon, + }), + Content = Roact.createElement(FitFrameOnAxis, { + BackgroundTransparency = 1, + contentPadding = UDim.new(0, self.props.middleContentPaddingBetweenBodyText), + LayoutOrder = 2, + minimumSize = UDim2.new(1, 0, 0, 0), + }, { + BodyText = self.props.bodyText and Roact.createElement(GenericTextLabel, { + BackgroundTransparency = 1, + colorStyle = theme.TextDefault, + fontStyle = font.Body, + LayoutOrder = 1, + Text = self.props.bodyText, + TextSize = fontSize, + TextXAlignment = Enum.TextXAlignment.Center, + Size = UDim2.new(1, 0, 0, fullTextHeight), + }), + MiddleContent = self.props.middleContent and Roact.createElement(isMiddleContentFocusable and + RoactGamepad.Focusable[FitFrameOnAxis] or FitFrameOnAxis, { + BackgroundTransparency = 1, + LayoutOrder = 2, + minimumSize = UDim2.new(1, 0, 0, 0), + + [Roact.Ref] = isMiddleContentFocusable and self.middleContentRef or nil, + NextSelectionDown = isMiddleContentFocusable and self.buttonStackRef or nil, + }, + { + Content = self.props.middleContent() + } + ), + }), + Buttons = buttonStackInfo and Roact.createElement(ButtonStack, buttonStackInfo), + }) + }) + end) +end + +return Alert \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Alert.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Alert.spec.lua new file mode 100644 index 0000000..fd54934 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Alert.spec.lua @@ -0,0 +1,61 @@ +local AlertRoot = script.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) + +local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + +local Alert = require(script.Parent.Alert) +local AlertType = require(AlertRoot.Enum.AlertType) + +local DEFAULT_REQUIRED_PROPS = { + alertType = AlertType.Informative, + title = "Hello World", + screenSize = Vector2.new(100, 100), +} + +local function mountAlert(props) + local combinedProps = DEFAULT_REQUIRED_PROPS + if props then + combinedProps = Cryo.Dictionary.join(DEFAULT_REQUIRED_PROPS, props) + end + local tree = mockStyleComponent( + Roact.createElement(Alert, combinedProps) + ) + local handle = Roact.mount(tree) + return tree, function() + Roact.unmount(handle) + end +end + +return function() + describe("lifecycle", function() + it("should mount and unmount informative alerts without issue", function() + local _, cleanup = mountAlert({ + alertType = AlertType.Informative, + }) + cleanup() + end) + + it("should mount and unmount interactive alerts without issue", function() + local _, cleanup = mountAlert({ + alertType = AlertType.Interactive, + buttonStackInfo = { + buttons = { + { + props = { + text = "test", + onActivated = function() end, + } + }, + }, + }, + }) + cleanup() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/AlertTitle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/AlertTitle.lua new file mode 100644 index 0000000..c83d3b6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/AlertTitle.lua @@ -0,0 +1,117 @@ +local AlertRoot = script.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local FitFrame = require(Packages.FitFrame) +local FitFrameOnAxis = FitFrame.FitFrameOnAxis + +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local MARGIN = 24 + +local AlertTitle = Roact.PureComponent:extend("AlertTitle") + +local validateProps = t.strictInterface({ + layoutOrder = t.optional(t.number), + margin = t.optional(t.table), + maxWidth = t.optional(t.number), + minWidth = t.optional(t.number), + screenSize = t.Vector2, + title = t.string, + titleIcon = t.optional(t.union(t.table, t.string)), + titleIconSize = t.optional(t.number), + titlePadding = t.optional(t.number), + titlePaddingWithIcon = t.optional(t.number), +}) + +AlertTitle.defaultProps = { + margin = { + top = 0, + bottom = MARGIN, + left = MARGIN, + right = MARGIN, + }, + maxWidth = 400, + minWidth = 272, + titleIconSize = 48, + titlePadding = 12, + titlePaddingWithIcon = 24, +} + +function AlertTitle:render() + assert(validateProps(self.props)) + + local totalWidth = math.clamp(self.props.screenSize.X - self.props.margin.left - self.props.margin.right, + self.props.minWidth, self.props.maxWidth) + local innerWidth = totalWidth - self.props.margin.left - self.props.margin.right + + local titleTopMargin + if self.props.titleIcon then + titleTopMargin = self.props.titlePaddingWithIcon + else + titleTopMargin = self.props.titlePadding + end + + return withStyle(function(stylePalette) + local font = stylePalette.Font + local theme = stylePalette.Theme + + local headerSize = font.BaseSize * font.Header1.RelativeSize + + return Roact.createElement(FitFrameOnAxis, { + BackgroundTransparency = 1, + contentPadding = UDim.new(0, 8), + HorizontalAlignment = Enum.HorizontalAlignment.Center, + LayoutOrder = self.props.layoutOrder, + margin = { + top = titleTopMargin, + bottom = 0, + left = 0, + right = 0, + }, + minimumSize = UDim2.new(1, 0, 0, 0), + }, { + TitleIcon = self.props.titleIcon and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = self.props.titleIcon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + LayoutOrder = 0, + Size = UDim2.new(0, self.props.titleIconSize, 0, self.props.titleIconSize), + }), + TitleArea = Roact.createElement(FitFrameOnAxis, { + BackgroundTransparency = 1, + contentPadding = UDim.new(0, self.props.titlePadding), + HorizontalAlignment = Enum.HorizontalAlignment.Center, + LayoutOrder = 1, + minimumSize = UDim2.new(1, 0, 0, 0), + }, { + Title = Roact.createElement(GenericTextLabel, { + colorStyle = theme.TextEmphasis, + fontStyle = font.Header1, + maxSize = Vector2.new(innerWidth, headerSize * 2), + LayoutOrder = 1, + Text = self.props.title, + TextSize = headerSize, + TextTruncate = Enum.TextTruncate.AtEnd, + }), + Underline = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = theme.Divider.Color, + BackgroundTransparency = theme.Divider.Transparency, + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, 1), + }), + }), + }) + end) +end + +return AlertTitle \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/AlertTitle.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/AlertTitle.spec.lua new file mode 100644 index 0000000..418fda1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/AlertTitle.spec.lua @@ -0,0 +1,28 @@ +local AlertRoot = script.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + +local AlertTitle = require(script.Parent.AlertTitle) + + +return function() + describe("lifecycle", function() + it("should mount and unmount informative alerts without issue", function() + local element = mockStyleComponent({ + Item = Roact.createElement(AlertTitle, { + title = "Hello World", + screenSize = Vector2.new(100, 100), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Enum/AlertType.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Enum/AlertType.lua new file mode 100644 index 0000000..d484cff --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Enum/AlertType.lua @@ -0,0 +1,13 @@ +local AlertRoot = script.Parent.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local enumerate = require(Packages.enumerate) + +return enumerate("AlertType", { + "Informative", + "Interactive", + "Loading", +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InformativeAlert.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InformativeAlert.lua new file mode 100644 index 0000000..bca321b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InformativeAlert.lua @@ -0,0 +1,62 @@ +local AlertRoot = script.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Alert = require(AlertRoot.Alert) +local AlertType = require(AlertRoot.Enum.AlertType) + +local MIN_WIDTH = 272 +local MAX_WIDTH = 400 +local MARGIN = 24 +local PADDING_BETWEEN = 24 +local TITLE_PADDING = 12 +local TITLE_MARGIN_WITH_ICON = 24 +local TITLE_ICON_SIZE = 48 + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local enableAlertTitleIconConfig = UIBloxConfig.enableAlertTitleIconConfig + +local InformativeAlert = Roact.PureComponent:extend("InformativeAlert") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + bodyText = t.optional(t.string), + onMounted = t.optional(t.callback), + position = t.optional(t.UDim2), + screenSize = t.Vector2, + title = t.string, + titleIcon = t.optional(t.union(t.table, t.string)), +}) + +function InformativeAlert:render() + assert(validateProps(self.props)) + return Roact.createElement(Alert, { + anchorPoint = self.props.anchorPoint, + alertType = AlertType.Informative, + bodyText = self.props.bodyText, + margin = { + top = 0, + bottom = MARGIN, + left = MARGIN, + right = MARGIN, + }, + maxWidth = MAX_WIDTH, + minWidth = MIN_WIDTH, + onMounted = self.props.onMounted, + paddingBetween = PADDING_BETWEEN, + position = self.props.position, + screenSize = self.props.screenSize, + title = self.props.title, + titleIcon = enableAlertTitleIconConfig and self.props.titleIcon or nil, + titleIconSize = enableAlertTitleIconConfig and TITLE_ICON_SIZE or nil, + titlePadding = TITLE_PADDING, + titlePaddingWithIcon = TITLE_MARGIN_WITH_ICON, + }) +end + +return InformativeAlert \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InformativeAlert.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InformativeAlert.spec.lua new file mode 100644 index 0000000..1a81edd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InformativeAlert.spec.lua @@ -0,0 +1,27 @@ +return function() + local AlertRoot = script.Parent + local DialogRoot = AlertRoot.Parent + local AppRoot = DialogRoot.Parent + local UIBlox = AppRoot.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local InformativeAlert = require(AlertRoot.InformativeAlert) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + Item = Roact.createElement(InformativeAlert, { + screenSize = Vector2.new(100, 100), + title = "Hello World", + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InteractiveAlert.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InteractiveAlert.lua new file mode 100644 index 0000000..9eea0cf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InteractiveAlert.lua @@ -0,0 +1,76 @@ +local AlertRoot = script.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Alert = require(AlertRoot.Alert) +local AlertType = require(AlertRoot.Enum.AlertType) + +local MIN_WIDTH = 272 +local MAX_WIDTH = 400 +local MARGIN = 24 +local PADDING_BETWEEN = 24 +local TITLE_PADDING = 12 +local TITLE_MARGIN_WITH_ICON = 24 +local TITLE_ICON_SIZE = 48 + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local enableAlertTitleIconConfig = UIBloxConfig.enableAlertTitleIconConfig +local validateButtonStack = require(AppRoot.Button.Validator.validateButtonStack) + +local InteractiveAlert = Roact.PureComponent:extend("InteractiveAlert") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + bodyText = t.optional(t.string), + buttonStackInfo = validateButtonStack, + middleContent = t.optional(t.callback), + onMounted = t.optional(t.callback), + onAbsoluteSizeChanged = t.optional(t.callback), + position = t.optional(t.UDim2), + screenSize = t.Vector2, + title = t.string, + titleIcon = t.optional(t.union(t.table, t.string)), + + --Gamepad props + defaultChildRef = t.optional(t.table), + isMiddleContentFocusable = t.optional(t.boolean), +}) + +function InteractiveAlert:render() + assert(validateProps(self.props)) + return Roact.createElement(Alert, { + anchorPoint = self.props.anchorPoint, + alertType = AlertType.Interactive, + bodyText = self.props.bodyText, + margin = { + top = 0, + bottom = MARGIN, + left = MARGIN, + right = MARGIN, + }, + maxWidth = MAX_WIDTH, + minWidth = MIN_WIDTH, + buttonStackInfo = self.props.buttonStackInfo, + middleContent = self.props.middleContent, + isMiddleContentFocusable = self.props.isMiddleContentFocusable, + onMounted = self.props.onMounted, + onAbsoluteSizeChanged = self.props.onAbsoluteSizeChanged, + paddingBetween = PADDING_BETWEEN, + position = self.props.position, + screenSize = self.props.screenSize, + title = self.props.title, + titleIcon = enableAlertTitleIconConfig and self.props.titleIcon or nil, + titleIconSize = enableAlertTitleIconConfig and TITLE_ICON_SIZE or nil, + titlePadding = TITLE_PADDING, + titlePaddingWithIcon = TITLE_MARGIN_WITH_ICON, + + defaultChildRef = self.props.defaultChildRef, + }) +end + +return InteractiveAlert \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InteractiveAlert.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InteractiveAlert.spec.lua new file mode 100644 index 0000000..2e1fa63 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InteractiveAlert.spec.lua @@ -0,0 +1,36 @@ +return function() + local AlertRoot = script.Parent + local DialogRoot = AlertRoot.Parent + local AppRoot = DialogRoot.Parent + local UIBlox = AppRoot.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local InteractiveAlert = require(AlertRoot.InteractiveAlert) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + Item = Roact.createElement(InteractiveAlert, { + buttonStackInfo = { + buttons = { + { + props = { + onActivated = function() end, + }, + }, + }, + }, + screenSize = Vector2.new(100, 100), + title = "Hello World", + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/LoadingAlert.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/LoadingAlert.lua new file mode 100644 index 0000000..2130ee5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/LoadingAlert.lua @@ -0,0 +1,79 @@ +local AlertRoot = script.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Alert = require(AlertRoot.Alert) +local AlertType = require(AlertRoot.Enum.AlertType) + +local MIN_WIDTH = 272 +local MAX_WIDTH = 400 +local MARGIN = 24 +local PADDING_BETWEEN = 24 +local TITLE_PADDING = 12 +local TITLE_MARGIN_WITH_ICON = 24 +local TITLE_ICON_SIZE = 48 + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local enableAlertTitleIconConfig = UIBloxConfig.enableAlertTitleIconConfig + +local LoadingSpinner = require(UIBlox.App.Loading.LoadingSpinner) + +local LoadingAlert = Roact.PureComponent:extend("LoadingAlert") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + bodyText = t.optional(t.string), + onMounted = t.optional(t.callback), + position = t.optional(t.UDim2), + screenSize = t.Vector2, + title = t.string, + titleIcon = t.optional(t.union(t.table, t.string)), +}) + +function LoadingAlert:init() + self.renderMiddleContent = function() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 48), + BackgroundTransparency = 1, + }, { + Spinner = Roact.createElement(LoadingSpinner, { + size = UDim2.fromOffset(48, 48), + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + }) + }) + end +end + +function LoadingAlert:render() + assert(validateProps(self.props)) + return Roact.createElement(Alert, { + anchorPoint = self.props.anchorPoint, + alertType = AlertType.Loading, + margin = { + top = 0, + bottom = MARGIN, + left = MARGIN, + right = MARGIN, + }, + maxWidth = MAX_WIDTH, + minWidth = MIN_WIDTH, + middleContent = self.renderMiddleContent, + onMounted = self.props.onMounted, + paddingBetween = PADDING_BETWEEN, + position = self.props.position, + screenSize = self.props.screenSize, + title = self.props.title, + titleIcon = enableAlertTitleIconConfig and self.props.titleIcon or nil, + titleIconSize = enableAlertTitleIconConfig and TITLE_ICON_SIZE or nil, + titlePadding = TITLE_PADDING, + titlePaddingWithIcon = TITLE_MARGIN_WITH_ICON, + }) +end + +return LoadingAlert diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/LoadingAlert.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/LoadingAlert.spec.lua new file mode 100644 index 0000000..de04a15 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/LoadingAlert.spec.lua @@ -0,0 +1,27 @@ +return function() + local AlertRoot = script.Parent + local DialogRoot = AlertRoot.Parent + local AppRoot = DialogRoot.Parent + local UIBlox = AppRoot.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local LoadingAlert = require(AlertRoot.LoadingAlert) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + Item = Roact.createElement(LoadingAlert, { + screenSize = Vector2.new(100, 100), + title = "Hello World", + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Validator/validateAlert.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Validator/validateAlert.lua new file mode 100644 index 0000000..c69aafb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Validator/validateAlert.lua @@ -0,0 +1,38 @@ +local validatorRoot = script.Parent +local ButtonRoot = validatorRoot.Parent +local AppRoot = ButtonRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local validateButtonProps = require(ButtonRoot.validateButtonProps) + +local ButtonType = require(ButtonRoot.Enum.ButtonType) + +return t.strictInterface({ + -- buttons: A table of button tables that contain props that PrimaryContextualButton, + -- AlertButton, PrimarySystemButton, or SecondaryButton allow. Also contains a prop "buttonType" + -- to determine which of these button types to use. + buttons = t.array(t.strictInterface({ + buttonType = enumerateValidator(ButtonType), + props = validateButtonProps, + })), + + buttonHeight = t.optional(t.numberMin(0)), + + -- forceFillDirection: What fill direction to force into. If nil, then the fillDirection + -- will be Vertical and automatically change to Horizontal if any button's text is + -- too long. + forcedFillDirection = t.optional(t.enum(Enum.FillDirection)), + + -- marginBetween: the margin between each button. + marginBetween = t.optional(t.numberMin(0)), + + -- minHorizontalButtonPadding: The minimum left and right padding used to calculate + -- the when the button text overflows and automatically changes fillDirection. + -- The overflow calculation will be if the length of the button text is over + -- the button size - (2 * minHorizontalButtonPadding). + minHorizontalButtonPadding = t.optional(t.numberMin(0)), +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/__stories__/Alert.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/__stories__/Alert.story.lua new file mode 100644 index 0000000..208f372 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/__stories__/Alert.story.lua @@ -0,0 +1,127 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local AlertRoot = script.Parent.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local Images = require(UIBlox.App.ImageSet.Images) + +local BACKGROUND_IMAGE = "icons/status/premium_small" + +local Alert = require(AlertRoot.Alert) +local AlertType = require(AlertRoot.Enum.AlertType) +local ButtonType = require(AppRoot.Button.Enum.ButtonType) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local enableAlertTitleIconConfig = UIBloxConfig.enableAlertTitleIconConfig + +local function close() + print("close") +end + +local function confirm() + print("confirm") +end + +local AlertContainer = Roact.PureComponent:extend("AlertContainer") + +function AlertContainer:init() + self.screenSize = nil + self.screenRef = Roact.createRef() + self.state = { + screenSize = Vector2.new(0, 0), + } + + self.changeScreenSize = function(rbx) + if self.state.screenSize ~= rbx.AbsoluteSize then + self:setState({ + screenSize = rbx.AbsoluteSize + }) + end + end + + self.renderMiddle = function() + return Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + LayoutOrder = 3, + Size = UDim2.new(1, 0, 0, 60), + },{ + CustomInner = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + LayoutOrder = 3, + Text = "Put any component you want here.", + TextSize = 13, + TextWrapped = true, + Size = UDim2.new(1, 0, 1, 0), + }), + }) + end +end + +function AlertContainer:didMount() + self.screenSize = self.screenRef and self.screenRef.current.AbsoluteSize +end + +function AlertContainer:render() + return Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 1, 0), + title = "AlertContainer", + subTitle = "<>", + }, { + Screen = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + [Roact.Ref] = self.screenRef, + [Roact.Change.AbsoluteSize] = self.changeScreenSize, + }, { + Alert = Roact.createElement(Alert, { + anchorPoint = Vector2.new(0.5, 0), + alertType = AlertType.Interactive, + bodyText = "Body text goes here. Both InformativeAlert and ".. + "InteractiveAlert use this component.", + buttonStackInfo = { + buttons = { + { + props = { + isDisabled = false, + onActivated = close, + text = "Cancel", + }, + }, + { + buttonType = ButtonType.PrimarySystem, + props = { + isDisabled = true, + onActivated = confirm, + text = "Confirm", + }, + }, + }, + }, + middleContent = self.renderMiddle, + onMounted = function() print("alert was mounted") end, + position = UDim2.new(0.5, 0, 0, 10), + screenSize = self.state.screenSize, + title = "Alert Component. Title goes up to 2 lines max.", + titleIcon = enableAlertTitleIconConfig and Images[BACKGROUND_IMAGE] or nil, + }) + }) + }) +end + +return function(target) + local story = Roact.createElement(StoryView, {}, { + Roact.createElement(AlertContainer) + }) + local handle = Roact.mount(story, target, "Alert") + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/EducationalModal.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/EducationalModal.lua new file mode 100644 index 0000000..d50da52 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/EducationalModal.lua @@ -0,0 +1,170 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local FitFrame = require(Packages.FitFrame) +local FitFrameVertical = FitFrame.FitFrameVertical + +local ImageSetLabel = require(UIBlox.Core.ImageSet.ImageSetComponent).Label +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local withStyle = require(UIBlox.Core.Style.withStyle) +local ButtonType = require(UIBlox.App.Button.Enum.ButtonType) +local getIconSize = require(AppRoot.ImageSet.getIconSize) +local IconSize = require(AppRoot.ImageSet.Enum.IconSize) + +local PartialPageModal = require(ModalRoot.PartialPageModal) + +local BODY_CONTENTS_WIDTH = 253 +local BODY_CONTENTS_MARGIN = 20 + +local EducationalModal = Roact.PureComponent:extend("EducationalModal") + +EducationalModal.validateProps = t.strictInterface({ + bodyContents = t.array(t.strictInterface({ + icon = t.union(t.string, t.table), + text = t.string, + layoutOrder = t.integer, + isSystemMenuIcon = t.optional(t.boolean), + })), + cancelText = t.string, + confirmText = t.string, + titleText = t.string, + titleBackgroundImageProps = t.strictInterface({ + image = t.string, + imageHeight = t.number, + }), + screenSize = t.Vector2, + + onDismiss = t.callback, + onCancel = t.callback, + onConfirm = t.callback, +}) + +local function ContentItem(props) + local totalTextSize = Vector2.new(16, 16) + local paddingBetween = 8 + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local font = stylePalette.Font + local textSize = font.Body.RelativeSize * font.BaseSize + + return Roact.createElement("Frame", { + Size = UDim2.new(0, BODY_CONTENTS_WIDTH, 0, totalTextSize.Y), + BackgroundTransparency = 1, + LayoutOrder = props.layoutOrder, + }, { + HorizontalLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + Padding = props.isSystemMenuIcon and UDim.new(0, paddingBetween + 2) + or UDim.new(0, paddingBetween), + VerticalAlignment = Enum.VerticalAlignment.Center + }), + Padding = Roact.createElement("UIPadding", { + PaddingLeft = props.isSystemMenuIcon and UDim.new(0, 2) + or UDim.new(0, 0), + }), + Icon = props.isSystemMenuIcon and Roact.createElement(ImageSetLabel, { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(32, 32), + Image = "rbxasset://textures/ui/TopBar/iconBase.png", + LayoutOrder = 1, + }, { + Icon = Roact.createElement(ImageSetLabel, { + ZIndex = 1, + BackgroundTransparency = 1, + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.fromOffset(24, 24), + Image = "rbxasset://textures/ui/TopBar/coloredlogo.png", + }), + }) or Roact.createElement(ImageSetLabel, { + Image = props.icon, + Size = UDim2.fromOffset(getIconSize(IconSize.Medium), getIconSize(IconSize.Medium)), + ImageColor3 = theme.IconDefault.Color, + ImageTransparency = theme.IconDefault.Transparency, + BackgroundTransparency = 1, + LayoutOrder = 1, + }), + Roact.createElement(GenericTextLabel, { + Size = UDim2.new(1, 0, 0, totalTextSize.Y), + BackgroundTransparency = 1, + Text = props.text, + TextSize = textSize, + colorStyle = theme.TextDefault, + TextTransparency = theme.TextDefault.Transparency, + fontStyle = font.Body, + TextXAlignment = Enum.TextXAlignment.Left, + TextWrapped = true, + LayoutOrder = 2, + }), + }) + end) +end + +function EducationalModal:init() + self.contentSize, self.changeContentSize = Roact.createBinding(Vector2.new(0, 0)) +end + +function EducationalModal:render() + local props = self.props + + local elements = {} + for _, content in ipairs(props.bodyContents) do + local current = Roact.createElement(ContentItem, { + icon = content.icon, + text = content.text, + layoutOrder = content.layoutOrder, + isSystemMenuIcon = content.isSystemMenuIcon, + }) + table.insert(elements, current) + end + + return Roact.createElement(PartialPageModal, { + title = props.titleText, + titleBackgroundImageProps = props.titleBackgroundImageProps, + screenSize = props.screenSize, + bottomPadding = 50, + buttonStackProps = { + buttons = { + { + props = { + isDisabled = false, + onActivated = props.onCancel, + text = props.cancelText, + }, + }, + { + buttonType = ButtonType.PrimarySystem, + props = { + isDisabled = false, + onActivated = props.onConfirm, + text = props.confirmText, + }, + }, + }, + }, + onCloseClicked = props.onDismiss, + }, { + BodyContents = Roact.createElement(FitFrameVertical, { + BackgroundTransparency = 1, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + width = UDim.new(1, 0), + contentPadding = UDim.new(0, 28), + margin = { + top = BODY_CONTENTS_MARGIN, + bottom = BODY_CONTENTS_MARGIN, + }, + [Roact.Change.AbsoluteSize] = function(rbx) + self.changeContentSize(rbx.AbsoluteSize) + end, + }, elements), + }) +end + +return EducationalModal diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/EducationalModal.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/EducationalModal.spec.lua new file mode 100644 index 0000000..892462f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/EducationalModal.spec.lua @@ -0,0 +1,61 @@ +return function() + local ModalRoot = script.Parent + local DialogRoot = ModalRoot.Parent + local AppRoot = DialogRoot.Parent + local UIBlox = AppRoot.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local Images = require(Packages.UIBlox.App.ImageSet.Images) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local EducationalModal = require(script.Parent.EducationalModal) + + local requiredProps = { + bodyContents = { + { + icon = Images["icons/logo/block"], + text = "Body 1", + layoutOrder = 1 + }, + { + icon = Images["icons/menu/home_on"], + text = "Body 2", + layoutOrder = 2 + }, + { + icon = Images["icons/menu/games_on"], + text = "Body 3", + layoutOrder = 3 + }, + }, + cancelText = "Cancel", + confirmText = "Confirm", + titleText = "Title", + titleBackgroundImageProps = { + image = "rbxassetid://2610133241", + imageHeight = 200, + }, + screenSize = Vector2.new(1920, 1080), + + onDismiss = function() + print("Dismiss") + end, + onCancel = function() + print("Cancel") + end, + onConfirm = function() + print("Confirm") + end, + } + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + EducationalModalDialog = Roact.createElement(EducationalModal, requiredProps), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/FullPageModal.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/FullPageModal.lua new file mode 100644 index 0000000..8703732 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/FullPageModal.lua @@ -0,0 +1,92 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local FitFrame = require(Packages.FitFrame) +local FitFrameVertical = FitFrame.FitFrameVertical + +local ButtonStack = require(AppRoot.Button.ButtonStack) + +local ModalTitle = require(ModalRoot.ModalTitle) +local ModalWindow = require(ModalRoot.ModalWindow) + +local FullPageModal = Roact.PureComponent:extend("FullPageModal") + +local MARGIN = 24 + +local validateProps = t.strictInterface({ + screenSize = t.Vector2, + [Roact.Children] = t.table, + + position = t.optional(t.UDim2), + title = t.optional(t.string), + + buttonStackProps = t.optional(t.table), -- Button stack validates the contents + + onCloseClicked = t.optional(t.callback), +}) + +function FullPageModal:init() + self.state = { + buttonFrameSize = Vector2.new(0, 0), + } + + self.changeButtonFrameSize = function(rbx) + if self.state.buttonFrameSize ~= rbx.AbsoluteSize then + self:setState({ + buttonFrameSize = rbx.AbsoluteSize + }) + end + end +end + + +function FullPageModal:render() + assert(validateProps(self.props)) + + return Roact.createElement(ModalWindow, { + isFullHeight = true, + screenSize = self.props.screenSize, + position = self.props.position, + }, { + TitleContainer = Roact.createElement(ModalTitle, { + title = self.props.title, + onCloseClicked = self.props.onCloseClicked, + }), + Content = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, -ModalTitle:GetHeight()), + Position = UDim2.new(0, 0, 0, ModalTitle:GetHeight()), + BackgroundTransparency = 1, + }, { + Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, MARGIN), + PaddingRight = UDim.new(0, MARGIN), + PaddingBottom = UDim.new(0, MARGIN), + }), + MiddleContent = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, -self.state.buttonFrameSize.Y), + BackgroundTransparency = 1, + LayoutOrder = 1 + }, self.props[Roact.Children]), + Buttons = self.props.buttonStackProps and Roact.createElement(FitFrameVertical, { + BackgroundTransparency = 1, + width = UDim.new(1, 0), + LayoutOrder = 2, + [Roact.Change.AbsoluteSize] = self.changeButtonFrameSize, + }, { + Roact.createElement(ButtonStack, self.props.buttonStackProps), + }), + }) + }) +end + +return FullPageModal \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/FullPageModal.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/FullPageModal.spec.lua new file mode 100644 index 0000000..fde98b9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/FullPageModal.spec.lua @@ -0,0 +1,56 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + +local ButtonType = require(AppRoot.Button.Enum.ButtonType) + +local FullPageModal = require(script.Parent.FullPageModal) + +return function() + describe("lifecycle", function() + it("should mount and unmount FullPageModal without issue", function() + local element = mockStyleComponent({ + FullPageModalContainer = Roact.createElement(FullPageModal, { + position = UDim2.new(0, 0, 0, 0), + title = "Title", + screenSize = Vector2.new(1920, 1080), + buttonStackProps = { + buttons = { + { + props = { + isDisabled = false, + onActivated = function() print("Cancel button was clicked") end, + text = "Cancel", + }, + }, + { + buttonType = ButtonType.PrimarySystem, + props = { + isDisabled = false, + onActivated = function() print("Confirm button was clicked") end, + text = "Confirm", + }, + }, + }, + }, + onCloseClicked = function() print("Close button was clicked") end, + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + Size = UDim2.new(1, 0, 0, 60), + }), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalTitle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalTitle.lua new file mode 100644 index 0000000..7ac2a2d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalTitle.lua @@ -0,0 +1,126 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local CoreRoot = UIBlox.Core +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(AppRoot.ImageSet.Images) +local ImageSetComponent = require(CoreRoot.ImageSet.ImageSetComponent) +local Controllable = require(CoreRoot.Control.Controllable) +local GenericTextLabel = require(CoreRoot.Text.GenericTextLabel.GenericTextLabel) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local X_BUTTON_SIZE = 36 +local X_LEFT_PADDING = 6 +local X_IMAGE = "icons/navigation/close" +local TITLE_HEIGHT = 48 +local TITLE_MAX_HEIGHT_WITH_IMAGE = 261 + +local ModalTitle = Roact.PureComponent:extend("ModalTitle") + +local validateProps = t.strictInterface({ + title = t.string, + position = t.optional(t.UDim2), + anchor = t.optional(t.Vector2), + onCloseClicked = t.optional(t.callback), + titleBackgroundImageProps = t.optional(t.strictInterface({ + image = t.string, + imageHeight = t.number, + })), +}) + +ModalTitle.defaultProps = { + title = "", + position = UDim2.new(0.5, 0, 0, 0), + anchor = Vector2.new(0.5, 0), +} + +function ModalTitle:GetHeight() + return TITLE_HEIGHT +end + +function ModalTitle:render() + assert(validateProps(self.props)) + local titleBackground = self.props.titleBackgroundImageProps + + return withStyle(function(stylePalette) + local font = stylePalette.Font + local theme = stylePalette.Theme + + local headerSize = font.BaseSize * font.Header1.RelativeSize + + local titleText = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, TITLE_HEIGHT), + BackgroundTransparency = 1, + }, { + CloseButton = Roact.createElement(Controllable, { + controlComponent = { + component = ImageSetComponent.Button, + props = { + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0, TITLE_HEIGHT * 0.5 + X_LEFT_PADDING, 0.5, 0), + Size = UDim2.new(0, TITLE_HEIGHT, 0, TITLE_HEIGHT), + [Roact.Event.Activated] = self.props.onCloseClicked, + }, + children = { + InputFillImage = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Size = UDim2.new(0, X_BUTTON_SIZE, 0, X_BUTTON_SIZE), + Image = Images[X_IMAGE], + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + }) + } + }, + onStateChanged = function(...) end, + }), + Title = Roact.createElement(GenericTextLabel, { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + colorStyle = theme.TextEmphasis, + fontStyle = font.Header1, + LayoutOrder = 1, + Text = self.props.title, + TextSize = headerSize, + TextTruncate = Enum.TextTruncate.AtEnd, + }), + Underline = not titleBackground and Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 1, 0), + BorderSizePixel = 0, + BackgroundColor3 = theme.Divider.Color, + BackgroundTransparency = theme.Divider.Transparency, + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, 1), + }), + }) + + if titleBackground then + local bgHeight = titleBackground.imageHeight + local height = math.min(math.max(TITLE_HEIGHT, bgHeight), TITLE_MAX_HEIGHT_WITH_IMAGE) + return Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.new(1, 0, 0, height), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundTransparency = 1, + ScaleType = Enum.ScaleType.Crop, + BorderSizePixel = 0, + Image = titleBackground.image, + ImageColor3 = Color3.new(255, 255, 255), + }, { + TitleText = titleText, + }) + else + return titleText + end + end) +end + +return ModalTitle \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalTitle.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalTitle.spec.lua new file mode 100644 index 0000000..c1d4c53 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalTitle.spec.lua @@ -0,0 +1,51 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + +local ModalTitle = require(script.Parent.ModalTitle) + +return function() + describe("lifecycle", function() + it("should mount and unmount ModalTitle without issue", function() + local element = mockStyleComponent({ + ModalTitleContainer = Roact.createElement(ModalTitle, { + title = "Title", + position = UDim2.new(0, 0, 0, 0), + anchor = Vector2.new(0, 0), + onCloseClicked = function() end, + titleBackgroundImageProps = { + image = "rbxassetid://2610133241", + imageHeight = 200, + }, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should throw on invalid props", function() + local element = mockStyleComponent({ + ModalTitleContainer = Roact.createElement(ModalTitle, { + title = "Title", + position = UDim2.new(0, 0, 0, 0), + anchor = Vector2.new(0, 0), + onCloseClicked = function() end, + titleBackgroundImageProps = { + image = "rbxassetid://2610133241", + }, + }) + }) + + expect(function() + Roact.mount(element) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalWindow.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalWindow.lua new file mode 100644 index 0000000..f81086a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalWindow.lua @@ -0,0 +1,130 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local FitFrame = require(Packages.FitFrame) +local FitFrameVertical = FitFrame.FitFrameVertical + +local Images = require(AppRoot.ImageSet.Images) +local withStyle = require(UIBlox.Core.Style.withStyle) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local SLICE_CENTER = Rect.new(8, 8, 9, 9) + +local ANCHORED_BACKGROUND_IMAGE = "component_assets/bullet_17" +local FLOATING_BACKGROUND_IMAGE = "component_assets/circle_17" + +local modalWindowAnchorPoint = UIBloxConfig.modalWindowAnchorPoint + +local ModalWindow = Roact.PureComponent:extend("ModalWindow") + +local MAX_WIDTH = 540 + +local validateProps = t.strictInterface({ + isFullHeight = t.boolean, + screenSize = t.Vector2, + [Roact.Children] = t.table, + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), +}) + +function ModalWindow:init() + self.contentSize, self.changeContentSize = Roact.createBinding(Vector2.new(0, 0)) +end + +-- Used to determine width of middle content for dynamically sizing children, see PartialPageModal +function ModalWindow:getWidth(screenWidth) + return self:isFullWidth(screenWidth) and screenWidth or MAX_WIDTH +end + +-- Used to determine if the modal is anchored in the middle or bottom of the screen +function ModalWindow:isFullWidth(screenWidth) + return screenWidth < MAX_WIDTH +end + +function ModalWindow:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local screenSize = self.props.screenSize + + local anchorPoint, backgroundImage, position, width + width = UDim.new(0, self:getWidth(screenSize.X)) + if self:isFullWidth(screenSize.X) then + anchorPoint = Vector2.new(0.5, 1) + backgroundImage = ANCHORED_BACKGROUND_IMAGE + position = self.props.position or UDim2.new(0.5, 0, 1, 0) + else + anchorPoint = Vector2.new(0.5, 0.5) + backgroundImage = FLOATING_BACKGROUND_IMAGE + position = self.props.position or UDim2.new(0.5, 0, 0.5, 0) + end + + if modalWindowAnchorPoint then + anchorPoint = self.props.anchorPoint or anchorPoint + end + + if self.props.isFullHeight then + return Roact.createElement(ImageSetComponent.Button, { + Position = position, + Size = UDim2.new(width, UDim.new(1, 0)), + AnchorPoint = anchorPoint, + BackgroundTransparency = 0, + BorderSizePixel = 0, + Image = Images[backgroundImage], + ImageColor3 = theme.BackgroundUIDefault.Color, + ImageTransparency = theme.BackgroundUIDefault.Transparency, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = SLICE_CENTER, + AutoButtonColor = false, + ClipsDescendants = true, + Selectable = false, + }, { + BackgroundImage = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.new(1, 0, 1, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, self.props[Roact.Children]) + }) + else + return Roact.createElement(ImageSetComponent.Button, { + Position = position, + Size = self.contentSize:map(function(absoluteSize) + return UDim2.new(0, absoluteSize.X, 0, absoluteSize.Y) + end), + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = Images[backgroundImage], + ImageColor3 = theme.BackgroundUIDefault.Color, + ImageTransparency = theme.BackgroundUIDefault.Transparency, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = SLICE_CENTER, + AutoButtonColor = false, + ClipsDescendants = true, + Selectable = false, + }, { + BackgroundImage = Roact.createElement(FitFrameVertical, { + Position = modalWindowAnchorPoint and UDim2.new(0.5, 0, 0.5, 0) or position, + AnchorPoint = modalWindowAnchorPoint and Vector2.new(0.5, 0.5) or anchorPoint, + BackgroundTransparency = 1, + BorderSizePixel = 0, + width = width, + [Roact.Change.AbsoluteSize] = function(rbx) + self.changeContentSize(rbx.AbsoluteSize) + end, + }, self.props[Roact.Children]) + }) + end + end) +end + +return ModalWindow \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalWindow.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalWindow.spec.lua new file mode 100644 index 0000000..6feefba --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalWindow.spec.lua @@ -0,0 +1,122 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + +local ModalWindow = require(script.Parent.ModalWindow) + +return function() + describe("lifecycle", function() + it("should mount and unmount full height and large width ModalWindow without issue", function() + local element = mockStyleComponent({ + PartialPageModalContainer = Roact.createElement(ModalWindow, { + isFullHeight = true, + screenSize = Vector2.new(1920, 1080), + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + Size = UDim2.new(1, 0, 1, 0), + }), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should mount and unmount full height and small width ModalWindow without issue", function() + local element = mockStyleComponent({ + PartialPageModalContainer = Roact.createElement(ModalWindow, { + isFullHeight = true, + screenSize = Vector2.new(400, 1080), + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + Size = UDim2.new(1, 0, 1, 0), + }), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should mount and unmount partial height and large width ModalWindow without issue", function() + local element = mockStyleComponent({ + PartialPageModalContainer = Roact.createElement(ModalWindow, { + isFullHeight = false, + screenSize = Vector2.new(1920, 1080), + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + Size = UDim2.new(1, 0, 1, 0), + }), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should mount and unmount partial height and small width ModalWindow without issue", function() + local element = mockStyleComponent({ + PartialPageModalContainer = Roact.createElement(ModalWindow, { + isFullHeight = false, + screenSize = Vector2.new(400, 1080), + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + Size = UDim2.new(1, 0, 1, 0), + }), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should mount and unmount arbitrary anchorPoints without issue", function() + local element = mockStyleComponent({ + AnchoredPageModalContainer = Roact.createElement(ModalWindow, { + anchorPoint = Vector2.new(0.25, 0.75), + isFullHeight = true, + screenSize = Vector2.new(1920, 1080), + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + Size = UDim2.new(1, 0, 1, 0), + }), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should return correct isFullWidth when screen size is larger than modal max width", function() + assert(ModalWindow:isFullWidth(800) == false) + end) + + it("should return correct isFullWidth when screen size is smaller than modal max width", function() + assert(ModalWindow:isFullWidth(400) == true) + end) + + it("should return correct getWidth when screen size is larger than modal max width", function() + assert(ModalWindow:getWidth(800) == 540) + end) + + it("should return correct getWidth when screen size is smaller than modal max width", function() + assert(ModalWindow:getWidth(400) == 400) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/PartialPageModal.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/PartialPageModal.lua new file mode 100644 index 0000000..9ce4f3f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/PartialPageModal.lua @@ -0,0 +1,88 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local ButtonStack = require(AppRoot.Button.ButtonStack) + +local FitFrame = require(Packages.FitFrame) +local FitFrameVertical = FitFrame.FitFrameVertical + +local ModalTitle = require(ModalRoot.ModalTitle) +local ModalWindow = require(ModalRoot.ModalWindow) + +local PartialPageModal = Roact.PureComponent:extend("PartialPageModal") + +local MARGIN = 24 + +local validateProps = t.strictInterface({ + screenSize = t.Vector2, + [Roact.Children] = t.table, + + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + title = t.optional(t.string), + titleBackgroundImageProps = t.optional(t.strictInterface({ + image = t.string, + imageHeight = t.number, + })), + bottomPadding = t.optional(t.number), + + buttonStackProps = t.optional(t.table), -- Button stack validates the contents + + onCloseClicked = t.optional(t.callback), +}) + +-- Used to determine width of middle content for dynamically sizing children in the content +-- Example: Multi-lined text that requires to know the width of its space that can also dynamically change its height. +function PartialPageModal:getMiddleContentWidth(screenWidth) + return ModalWindow:getWidth(screenWidth) - 2 * MARGIN +end + +function PartialPageModal:render() + assert(validateProps(self.props)) + local screenSize = self.props.screenSize + local bottomPadding = self.props.bottomPadding or MARGIN + + -- Only add bottom padding when window is anchored to the bottom + -- Used to align buttons with previous UI + if not ModalWindow:isFullWidth(screenSize.X) then + bottomPadding = MARGIN + end + + return Roact.createElement(ModalWindow, { + isFullHeight = false, + screenSize = screenSize, + position = self.props.position, + anchorPoint = self.props.anchorPoint, + }, { + TitleContainer = Roact.createElement(ModalTitle, { + title = self.props.title, + titleBackgroundImageProps = self.props.titleBackgroundImageProps, + onCloseClicked = self.props.onCloseClicked, + }), + Content = Roact.createElement(FitFrameVertical, { + Position = UDim2.new(0, 0, 0, ModalTitle.TITLE_HEIGHT), + width = UDim.new(1, 0), + margin = { + top = 0, + bottom = bottomPadding, + left = MARGIN, + right = MARGIN, + }, + BackgroundTransparency = 1, + }, { + MiddlContent = Roact.createElement(FitFrameVertical, { + width = UDim.new(1, 0), + BackgroundTransparency = 1, + }, self.props[Roact.Children]), + Buttons = self.props.buttonStackProps and Roact.createElement(ButtonStack, self.props.buttonStackProps), + }) + }) +end + +return PartialPageModal \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/PartialPageModal.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/PartialPageModal.spec.lua new file mode 100644 index 0000000..62fb3b4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/PartialPageModal.spec.lua @@ -0,0 +1,88 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + +local ButtonType = require(AppRoot.Button.Enum.ButtonType) + +local PartialPageModal = require(script.Parent.PartialPageModal) + +return function() + describe("lifecycle", function() + it("should mount and unmount PartialPageModal without issue", function() + local element = mockStyleComponent({ + PartialPageModalContainer = Roact.createElement(PartialPageModal, { + position = UDim2.new(0, 0, 0, 0), + anchorPoint = Vector2.new(0, 0.5), + title = "Title", + titleBackgroundImageProps = { + image = "rbxassetid://2610133241", + imageHeight = 200, + }, + screenSize = Vector2.new(1920, 1080), + bottomPadding = 100, + buttonStackProps = { + buttons = { + { + props = { + isDisabled = false, + onActivated = function() print("Cancel button was clicked") end, + text = "Cancel", + }, + }, + { + buttonType = ButtonType.PrimarySystem, + props = { + isDisabled = false, + onActivated = function() print("Confirm button was clicked") end, + text = "Confirm", + }, + }, + }, + }, + onCloseClicked = function() print("Close button was clicked") end, + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + Size = UDim2.new(1, 0, 1, 0), + }), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should throw on invalid props", function() + local element = mockStyleComponent({ + ModalTitleContainer = Roact.createElement(PartialPageModal, { + title = "Title", + titleBackgroundImageProps = { + image = "rbxassetid://2610133241", + }, + screenSize = Vector2.new(1920, 1080), + }) + }) + + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should return correct getMiddleContentWidth when screen size is larger than modal max width", function() + -- modal width - left margin - right margin + assert(PartialPageModal:getMiddleContentWidth(800) == 540 - 2 * 24) + end) + + it("should return correct getMiddleContentWidth when screen size is smaller than modal max width", function() + -- modal width - left margin - right margin + assert(PartialPageModal:getMiddleContentWidth(400) == 400 - 2 * 24) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/__stories__/ModalWindow.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/__stories__/ModalWindow.story.lua new file mode 100644 index 0000000..9131b58 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/__stories__/ModalWindow.story.lua @@ -0,0 +1,111 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local ModalsRoot = script.Parent.Parent +local DialogRoot = ModalsRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local ModalWindow = require(ModalsRoot.ModalWindow) + +local PortraitModal = Roact.PureComponent:extend("PortraitModal") + +function PortraitModal:init() + self.screenSize = nil + self.screenRef = Roact.createRef() + self.state = { + screenSize = Vector2.new(0, 0), + isFullHeight = false + } + + self.changeScreenSize = function(rbx) + if self.state.screenSize ~= rbx.AbsoluteSize then + self:setState({ + screenSize = rbx.AbsoluteSize + }) + end + end + + self.toggleisFullHeight = function() + self:setState({ + isFullHeight = not self.state.isFullHeight + }) + end +end + +function PortraitModal:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + }), + ButtonControlsFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 50), + LayoutOrder = 1, + }, { + Grid = Roact.createElement("UIGridLayout", { + CellSize = UDim2.new(0, 200, 0, 45), + SortOrder = Enum.SortOrder.LayoutOrder, + }), + DisableControl = Roact.createElement("TextButton", { + Text = self.state.isFullHeight and "Fit" or "Full Height", + [Roact.Event.Activated] = self.toggleisFullHeight, + LayoutOrder = 1, + }), + }), + Overview = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, -50), + BackgroundTransparency = 1, + LayoutOrder = 2, + }, { + Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 1, 0), + title = "ModalWindowContainer", + subTitle = "Expand and shrink the width of the window to see how the modal behaves on different widths", + }, { + ViewFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + [Roact.Ref] = self.screenRef, + [Roact.Change.AbsoluteSize] = self.changeScreenSize, + } , { + ModalWindowContainer = Roact.createElement(ModalWindow, { + isFullHeight = self.state.isFullHeight, + screenSize = self.state.screenSize, + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 60), + },{ + CustomInner = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + LayoutOrder = 3, + Text = "Put any component you want here.", + TextSize = 13, + TextWrapped = true, + Size = UDim2.new(1, 0, 1, 0), + }), + }), + }) + }) + }) + }) + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(PortraitModal), + }), target, "PortraitModal") + + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Enum/AnimationState.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Enum/AnimationState.lua new file mode 100644 index 0000000..1ecaa83 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Enum/AnimationState.lua @@ -0,0 +1,6 @@ +return { + Appearing = "Appearing", + Appeared = "Appeared", + Disappearing = "Disappearing", + Disappeared = "Disappeared", +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InformativeToast.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InformativeToast.lua new file mode 100644 index 0000000..8eb626d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InformativeToast.lua @@ -0,0 +1,64 @@ +local ToastRoot = script.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ToastFrame = require(ToastRoot.ToastFrame) +local validateToastIcon = require(ToastRoot.Validator.validateToastIcon) +local validateToastText = require(ToastRoot.Validator.validateToastText) + +local InformativeToast = Roact.PureComponent:extend("InformativeToast") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + iconProps = t.optional(validateToastIcon), + layoutOrder = t.optional(t.integer), + padding = t.optional(t.numberMin(0)), + position = t.optional(t.UDim2), + size = t.UDim2, + subtitleTextProps = t.optional(validateToastText), + textFrameSize = t.optional(t.UDim2), + titleTextProps = validateToastText, +}) + +InformativeToast.defaultProps = { + anchorPoint = Vector2.new(0.5, 0.5), + layoutOrder = 1, + position = UDim2.new(0.5, 0, 0.5, 0), + size = UDim2.new(1, 0, 1, 0), +} + +function InformativeToast:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + return Roact.createElement("Frame", { + AnchorPoint = self.props.anchorPoint, + BackgroundColor3 = theme.BackgroundUIContrast.Color, + BackgroundTransparency = theme.BackgroundUIContrast.Transparency, + BorderSizePixel = 0, + ClipsDescendants = true, + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + Size = self.props.size, + }, { + ToastFrame = Roact.createElement(ToastFrame, { + iconProps = self.props.iconProps, + padding = self.props.padding, + subtitleTextProps = self.props.subtitleTextProps, + textFrameSize = self.props.textFrameSize, + titleTextProps = self.props.titleTextProps, + }), + }) + end) +end + +return InformativeToast diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InformativeToast.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InformativeToast.spec.lua new file mode 100644 index 0000000..b20767b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InformativeToast.spec.lua @@ -0,0 +1,104 @@ +return function() + local Toast = script.Parent + local Dialog = Toast.Parent + local App = Dialog.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local Images = require(UIBlox.App.ImageSet.Images) + local TestStyle = require(UIBlox.App.Style.Validator.TestStyle) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local InformativeToast = require(Toast.InformativeToast) + + local ICON_SIZE = 36 + + local testText = "Item On Sale" + local testSubText = "test test test" + + local createInformativeToast = function(props) + return mockStyleComponent({ + InformativeToast = Roact.createElement(InformativeToast, props) + }) + end + + it("should throw on invalid titleTextProps", function() + local element = createInformativeToast({ + toastText = {}, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors with valid titleTextProps", function() + local element = createInformativeToast({ + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, 0, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with valid titleTextProps and subtitleTextProps", function() + local element = createInformativeToast({ + subtitleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.CaptionBody, + Size = UDim2.new(1, 0, 0.5, 0), + Text = testSubText, + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, 0, 0.5, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with icon", function() + local element = createInformativeToast({ + iconProps = { + Image = "rbxassetid://4126499279", + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, -ICON_SIZE, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with ImageSet compatible icon", function() + local element = createInformativeToast({ + iconProps = { + Image = Images["icons/status/warning"], + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, -ICON_SIZE, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InteractiveToast.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InteractiveToast.lua new file mode 100644 index 0000000..b7ef442 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InteractiveToast.lua @@ -0,0 +1,104 @@ +local ToastRoot = script.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) +local SpringAnimatedItem = require(UIBloxRoot.Utility.SpringAnimatedItem) + +local ToastFrame = require(ToastRoot.ToastFrame) +local validateToastIcon = require(ToastRoot.Validator.validateToastIcon) +local validateToastText = require(ToastRoot.Validator.validateToastText) + +local ANIMATION_SPRING_SETTINGS = { + dampingRatio = 1, + frequency = 4, +} +local PRESSED_SCALE = 0.95 +local TOAST_BACKGROUND_IMAGE = Images["component_assets/circle_21"] +local TOAST_BORDER_IMAGE = Images["component_assets/circle_21_stroke_1"] +local TOAST_SLICE_CENTER = Rect.new(10, 10, 11, 11) + +local InteractiveToast = Roact.PureComponent:extend("InteractiveToast") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + iconProps = t.optional(validateToastIcon), + layoutOrder = t.optional(t.integer), + padding = t.optional(t.numberMin(0)), + position = t.optional(t.UDim2), + pressed = t.optional(t.boolean), + pressedScale = t.number, + size = t.UDim2, + subtitleTextProps = t.optional(validateToastText), + textFrameSize = t.optional(t.UDim2), + titleTextProps = validateToastText, +}) + +InteractiveToast.defaultProps = { + anchorPoint = Vector2.new(0.5, 0.5), + layoutOrder = 1, + position = UDim2.new(0.5, 0, 0.5, 0), + pressedScale = PRESSED_SCALE, + size = UDim2.new(1, 0, 1, 0), +} + +function InteractiveToast:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + return Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = self.props.anchorPoint, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = TOAST_BACKGROUND_IMAGE, + ImageColor3 = theme.SystemPrimaryContent.Color, + ImageTransparency = theme.SystemPrimaryContent.Transparency, + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + ScaleType = Enum.ScaleType.Slice, + Size = self.props.size, + SliceCenter = TOAST_SLICE_CENTER, + }, { + Scaler = Roact.createElement(SpringAnimatedItem.AnimatedUIScale, { + springOptions = ANIMATION_SPRING_SETTINGS, + animatedValues = { + scale = self.props.pressed and self.props.pressedScale or 1, + }, + mapValuesToProps = function(values) + return { + Scale = values.scale, + } + end, + }), + ToastBorder = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = TOAST_BORDER_IMAGE, + ImageColor3 = theme.TextDefault.Color, + ImageTransparency = theme.TextDefault.Transparency, + Position = UDim2.new(0.5, 0, 0.5, 0), + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(1, 0, 1, 0), + SliceCenter = TOAST_SLICE_CENTER, + }), + ToastFrame = Roact.createElement(ToastFrame, { + iconProps = self.props.iconProps, + padding = self.props.padding, + subtitleTextProps = self.props.subtitleTextProps, + textFrameSize = self.props.textFrameSize, + titleTextProps = self.props.titleTextProps, + }), + }) + end) +end + +return InteractiveToast diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InteractiveToast.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InteractiveToast.spec.lua new file mode 100644 index 0000000..a19d6d8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InteractiveToast.spec.lua @@ -0,0 +1,125 @@ +return function() + local Toast = script.Parent + local Dialog = Toast.Parent + local App = Dialog.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local Images = require(UIBlox.App.ImageSet.Images) + local TestStyle = require(UIBlox.App.Style.Validator.TestStyle) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local InteractiveToast = require(Toast.InteractiveToast) + + local ICON_SIZE = 36 + + local testText = "System Outage" + local testSubText = "Tap to see more information" + + local createInteractiveToast = function(props) + return mockStyleComponent({ + InteractiveToast = Roact.createElement(InteractiveToast, props) + }) + end + + it("should throw on invalid titleTextProps", function() + local element = createInteractiveToast({ + textFrameSize = UDim2.new(1, 0, 1, 0), + titleTextProps = {}, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors with valid titleTextProps", function() + local element = createInteractiveToast({ + textFrameSize = UDim2.new(1, 0, 1, 0), + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, 0, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with valid titleTextProps and subtitleTextProps", function() + local element = createInteractiveToast({ + textFrameSize = UDim2.new(1, 0, 1, 0), + subtitleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.CaptionBody, + Size = UDim2.new(1, 0, 0.5, 0), + Text = testSubText, + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, 0, 0.5, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with icon", function() + local element = createInteractiveToast({ + textFrameSize = UDim2.new(1, 0, 1, 0), + iconProps = { + Image = "rbxassetid://4126499279", + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, -ICON_SIZE, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with ImageSet compatible icon", function() + local element = createInteractiveToast({ + textFrameSize = UDim2.new(1, 0, 1, 0), + iconProps = { + Image = Images["icons/status/warning"], + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, -ICON_SIZE, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when pressed", function() + local element = createInteractiveToast({ + pressed = true, + textFrameSize = UDim2.new(1, 0, 1, 0), + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, -ICON_SIZE, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/SlideFromTopToast.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/SlideFromTopToast.lua new file mode 100644 index 0000000..a10e5d6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/SlideFromTopToast.lua @@ -0,0 +1,236 @@ +local ToastRoot = script.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local SlidingDirection = require(UIBloxRoot.Core.Animation.Enum.SlidingDirection) +local SlidingContainer = require(UIBloxRoot.Core.Animation.SlidingContainer) +local StateTable = require(UIBloxRoot.StateTable.StateTable) + +local AnimationState = require(ToastRoot.Enum.AnimationState) +local InformativeToast = require(ToastRoot.InformativeToast) +local InteractiveToast = require(ToastRoot.InteractiveToast) +local ToastContainer = require(ToastRoot.ToastContainer) +local validateToastContent = require(ToastRoot.Validator.validateToastContent) + +local SlideFromTopToast = Roact.PureComponent:extend("SlideFromTopToast") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + duration = t.optional(t.number), + layoutOrder = t.optional(t.integer), + position = t.optional(t.UDim2), + show = t.optional(t.boolean), + size = t.optional(t.UDim2), + springOptions = t.optional(t.table), + toastContent = validateToastContent, +}) + +SlideFromTopToast.defaultProps = { + anchorPoint = Vector2.new(0.5, 0), + position = UDim2.new(0.5, 0, 0, 20), + show = true, +} + +local function toastContentEqual(toastContent1, toastContent2) + if toastContent1.iconColorStyle ~= toastContent2.iconColorStyle + or toastContent1.iconImage ~= toastContent2.iconImage + or toastContent1.iconSize ~= toastContent2.iconSize + or toastContent1.onActivated ~= toastContent2.onActivated + or toastContent1.onDismissed ~= toastContent2.onDismissed + or toastContent1.swipeUpDismiss ~= toastContent2.swipeUpDismiss + or toastContent1.toastSubtitle ~= toastContent2.toastSubtitle + or toastContent1.toastTitle ~= toastContent2.toastTitle then + return false + end + return true +end + +function SlideFromTopToast:init() + self.isMounted = false + + self.currentToastContent = self.props.toastContent + + self.onActivated = function() + self.stateTable.events.Activated({ + activated = true, + }) + end + + self.onAppeared = function() + if self.currentToastContent.onAppeared then + self.currentToastContent.onAppeared() + end + local duration = self.props.duration + if duration and duration > 0 then + local currentToastContent = self.currentToastContent + delay(duration, function() + if currentToastContent == self.currentToastContent then + self.stateTable.events.AutoDismiss() + end + end) + end + end + + self.onComplete = function() + self.stateTable.events.AnimationComplete() + end + + self.onDisappeared = function() + if self.state.context.activated then + if self.currentToastContent.onActivated then + self.currentToastContent.onActivated() + end + else + if self.currentToastContent.onDismissed then + self.currentToastContent.onDismissed() + end + end + end + + self.onTouchSwipe = function(_, swipeDir) + if swipeDir == Enum.SwipeDirection.Up then + self.stateTable.events.ForceDismiss() + end + end + + self.renderInteractiveToast = function(props) + return Roact.createElement(InteractiveToast, props) + end + + self.renderInformativeToast = function(props) + return Roact.createElement(InformativeToast, props) + end + + self.setContext = function(_, _, data) + return data + end + + self.updateToastContent = function() + if self.currentToastContent ~= self.props.toastContent then + self.currentToastContent = self.props.toastContent + if self.props.show then + -- Show next toast content + self.stateTable.events.ForceAppear({ + activated = false, + }) + end + end + end + + local initialState = AnimationState.Disappeared + self.state = { + currentState = initialState, + context = { + activated = false, + }, + } + + local stateTableName = string.format("Animated(%s)", tostring(self)) + self.stateTable = StateTable.new(stateTableName, initialState, {}, { + [AnimationState.Appearing] = { + AnimationComplete = { nextState = AnimationState.Appeared, action = self.onAppeared }, + ContentChanged = { nextState = AnimationState.Disappearing }, + ForceDismiss = { nextState = AnimationState.Disappearing }, + }, + [AnimationState.Appeared] = { + Activated = { nextState = AnimationState.Disappearing, action = self.setContext }, + AutoDismiss = { nextState = AnimationState.Disappearing }, + ContentChanged = { nextState = AnimationState.Disappearing }, + ForceDismiss = { nextState = AnimationState.Disappearing }, + }, + [AnimationState.Disappearing] = { + AnimationComplete = { nextState = AnimationState.Disappeared, action = self.onDisappeared }, + }, + [AnimationState.Disappeared] = { + ContentChanged = { nextState = AnimationState.Appearing, action = self.updateToastContent }, + ForceAppear = { nextState = AnimationState.Appearing, action = self.setContext }, + }, + }) + + self.stateTable:onStateChange(function(oldState, newState, updatedContext) + if self.isMounted and oldState ~= newState then + self:setState({ + currentState = newState, + context = updatedContext, + }) + end + end) +end + +function SlideFromTopToast:isShowing() + return self.state.currentState == AnimationState.Appearing or self.state.currentState == AnimationState.Appeared +end + +function SlideFromTopToast:render() + assert(validateProps(self.props)) + + local onActivated = self.currentToastContent.onActivated + local swipeUpDismiss = self.currentToastContent.swipeUpDismiss + if swipeUpDismiss == nil then + swipeUpDismiss = true + end + return Roact.createElement(SlidingContainer, { + show = self:isShowing(), + layoutOrder = self.props.layoutOrder, + onComplete = self.onComplete, + slidingDirection = SlidingDirection.Down, + springOptions = self.props.springOptions, + }, { + ToastContainer = Roact.createElement(ToastContainer, { + anchorPoint = self.props.anchorPoint, + position = self.props.position, + size = self.props.size, + -- Toast content props + iconColorStyle = self.currentToastContent.iconColorStyle, + iconImage = self.currentToastContent.iconImage, + iconSize = self.currentToastContent.iconSize, + onActivated = onActivated and self.onActivated, + onTouchSwipe = swipeUpDismiss and self.onTouchSwipe, + renderToast = onActivated and self.renderInteractiveToast or self.renderInformativeToast, + toastSubtitle = self.currentToastContent.toastSubtitle, + toastTitle = self.currentToastContent.toastTitle, + }), + }) +end + +function SlideFromTopToast:didMount() + self.isMounted = true + if self.props.show then + self.stateTable.events.ForceAppear({ + activated = false, + }) + end +end + +function SlideFromTopToast:willUnmount() + self.isMounted = false +end + +function SlideFromTopToast:didUpdate(oldProps, oldState) + if oldProps.show ~= self.props.show then + if self.props.show then + self.stateTable.events.ForceAppear({ + activated = false, + }) + else + self.stateTable.events.ForceDismiss() + end + end + if not toastContentEqual(oldProps.toastContent, self.props.toastContent) then + -- Toast content updated, need to force dismiss current toast and show the new one + self.stateTable.events.ContentChanged({ + activated = false, + }) + end + if oldState.currentState ~= self.state.currentState and + self.state.currentState == AnimationState.Disappeared then + self.updateToastContent() + end +end + +return SlideFromTopToast \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/SlideFromTopToast.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/SlideFromTopToast.spec.lua new file mode 100644 index 0000000..de8695c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/SlideFromTopToast.spec.lua @@ -0,0 +1,66 @@ +return function() + local ToastRoot = script.Parent + local DialogRoot = ToastRoot.Parent + local AppRoot = DialogRoot.Parent + local UIBloxRoot = AppRoot.Parent + local Packages = UIBloxRoot.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local SlideFromTopToast = require(ToastRoot.SlideFromTopToast) + + local createSlideFromTopToast = function(props) + return mockStyleComponent({ + SlideFromTopToast = Roact.createElement(SlideFromTopToast, props) + }) + end + + it("should throw on empty toastTitle", function() + local element = createSlideFromTopToast({ + toastContent = { + toastTitle = nil, + }, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors", function() + local element = createSlideFromTopToast({ + toastContent = { + toastTitle = "Test Title", + }, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when render InformativeToast", function() + local element = createSlideFromTopToast({ + toastContent = { + iconImage = "rbxassetid://4126499279", + toastSubtitle = "test test test", + toastTitle = "Item on sale", + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when render InteractiveToast", function() + local element = createSlideFromTopToast({ + toastContent = { + iconImage = "rbxassetid://4126499279", + onActivated = function() end, + toastSubtitle = "Tap to see more information", + toastTitle = "System Outage", + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastContainer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastContainer.lua new file mode 100644 index 0000000..e5fe8c5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastContainer.lua @@ -0,0 +1,268 @@ +local ToastRoot = script.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent +local UIBloxConfig = require(UIBloxRoot.UIBloxConfig) + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local GetTextSize = require(UIBloxRoot.Core.Text.GetTextSize) +local Images = require(UIBloxRoot.App.ImageSet.Images) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) +local validateColorInfo = require(UIBloxRoot.Core.Style.Validator.validateColorInfo) + +local DEFAULT_PADDING = 12 +local DEFAULT_ICON_SIZE = Vector2.new(36, 36) + +local MAX_WIDTH = 400 +local MIN_WIDTH = 24 +local MIN_HEIGHT = 60 + +local MAX_BOUND = 10000 + +local fixToastResizeConfig = UIBloxConfig.fixToastResizeConfig + +local function getTextHeight(text, font, fontSize, widthCap) + local bounds = Vector2.new(widthCap, MAX_BOUND) + local textSize = GetTextSize(text, fontSize, font, bounds) + return textSize.Y +end + +local ToastContainer = Roact.PureComponent:extend("ToastContainer") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + fitHeight = t.optional(t.boolean), + iconColorStyle = t.optional(validateColorInfo), + -- Optional image to be displayed in the toast. + iconImage = t.optional(t.union(t.table, t.string)), + iconSize = t.optional(t.Vector2), + layoutOrder = t.optional(t.integer), + onActivated = t.optional(t.callback), + onTouchSwipe = t.optional(t.callback), + padding = t.numberMin(0), + position = t.UDim2, + pressedScale = t.optional(t.number), + renderToast = t.callback, + size = t.UDim2, + sizeConstraint = t.optional(t.table), + toastSubtitle = t.optional(t.string), + toastTitle = t.string, +}) + +ToastContainer.defaultProps = { + anchorPoint = Vector2.new(0, 0), + fitHeight = true, + padding = DEFAULT_PADDING, + position = UDim2.new(0, 0, 0, 0), + size = UDim2.new(1, -DEFAULT_PADDING * 2, 0, 0), + sizeConstraint = { + MaxSize = Vector2.new(MAX_WIDTH, math.huge), + MinSize = Vector2.new(MIN_WIDTH, MIN_HEIGHT), + }, +} + +function ToastContainer:init() + self.containerRef = Roact.createRef() + self.isMounted = false + + self.state = { + containerWidth = 0, + pressed = false, + subtitleHeight = 0, + titleHeight = 0, + } + + self.getIconSize = function() + local iconSize = self.props.iconSize + local iconImage = self.props.iconImage + local imagesResolutionScale = Images.ImagesResolutionScale + if iconImage then + if iconSize then + return iconSize + elseif iconImage.ImageRectSize and imagesResolutionScale and imagesResolutionScale > 0 then + return iconImage.ImageRectSize / imagesResolutionScale + else + return DEFAULT_ICON_SIZE + end + end + return Vector2.new(0, 0) + end + + self.onButtonInputBegan = function(_, inputObject) + if inputObject.UserInputState == Enum.UserInputState.Begin and + (inputObject.UserInputType == Enum.UserInputType.Touch or + inputObject.UserInputType == Enum.UserInputType.MouseButton1) then + if not self.state.pressed and (fixToastResizeConfig or self.isMounted) then + self:setState({ + pressed = true, + }) + end + end + end + + self.onButtonInputEnded = function() + if self.state.pressed and (fixToastResizeConfig or self.isMounted) then + self:setState({ + pressed = false, + }) + end + end + + self.getTextHeights = function(stylePalette) + local iconImage = self.props.iconImage + local iconSize = self.getIconSize() + local padding = self.props.padding + local toastSubtitle = self.props.toastSubtitle + local toastTitle = self.props.toastTitle + + local font = stylePalette.Font + local titleStyle = font.Header2 + local subtitleStyle = font.CaptionBody + + local textFrameWidth = self.state.containerWidth - padding*2 + if iconImage then + textFrameWidth = textFrameWidth - iconSize.X - padding + end + + local titleFont = titleStyle.Font + local titleSize = titleStyle.RelativeSize * font.BaseSize + local titleHeight = math.max(0, getTextHeight(toastTitle, titleFont, titleSize, textFrameWidth)) + + local subtitleHeight = 0 + if toastSubtitle then + local subtitleFont = subtitleStyle.Font + local subtitleSize = subtitleStyle.RelativeSize * font.BaseSize + subtitleHeight = math.max(0, getTextHeight(toastSubtitle, subtitleFont, subtitleSize, textFrameWidth)) + end + + return subtitleHeight, titleHeight + end +end + +function ToastContainer:render() + assert(validateProps(self.props)) + local iconImage = self.props.iconImage + local iconSize = self.getIconSize() + local padding = self.props.padding + local toastSubtitle = self.props.toastSubtitle + local toastTitle = self.props.toastTitle + + return withStyle(function(stylePalette) + local subtitleHeight, titleHeight = self.getTextHeights(stylePalette) + local textFrameHeight + if fixToastResizeConfig then + textFrameHeight = titleHeight + subtitleHeight + else + textFrameHeight = self.state.titleHeight + self.state.subtitleHeight + end + + local size = self.props.size + if self.props.fitHeight then + local containerHeight = math.max(iconSize.Y, textFrameHeight) + padding*2 + size = UDim2.new(size.X.Scale, size.X.Offset, 0, containerHeight) + end + + local theme = stylePalette.Theme + local font = stylePalette.Font + local titleStyle = font.Header2 + local subtitleStyle = font.CaptionBody + + return Roact.createElement("TextButton", { + AnchorPoint = self.props.anchorPoint, + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + Size = size, + Text = "", + [Roact.Change.AbsoluteSize] = function(rbx) + if fixToastResizeConfig and self.state.containerWidth ~= rbx.AbsoluteSize.X then + self:setState({ + containerWidth = rbx.AbsoluteSize.X + }) + elseif not fixToastResizeConfig and self.state.containerWidth ~= rbx.AbsoluteSize.X then + local containerWidth = rbx.AbsoluteSize.X + local textFrameWidth = containerWidth - padding*2 + if iconImage then + textFrameWidth = textFrameWidth - iconSize.X - padding + end + + local titleFont = titleStyle.Font + local titleSize = titleStyle.RelativeSize * font.BaseSize + local newTitleHeight = math.max(0, getTextHeight(toastTitle, titleFont, titleSize, textFrameWidth)) + local newSubtitleHeight = 0 + + if toastSubtitle then + local subtitleFont = subtitleStyle.Font + local subtitleSize = subtitleStyle.RelativeSize * font.BaseSize + newSubtitleHeight = math.max(0, getTextHeight(toastSubtitle, subtitleFont, subtitleSize, textFrameWidth)) + end + + -- Wrapped in spawn in order to avoid issues if Roact connects changed signal before the Size + -- prop is set in older versions of Roact (older than 1.0) In 1.0, this is fixed by deferring event + -- handlers and setState calls until after the current update]] + spawn(function() + if self.isMounted then + self:setState({ + containerWidth = containerWidth, + subtitleHeight = newSubtitleHeight, + titleHeight = newTitleHeight, + }) + end + end) + end + end, + [Roact.Event.Activated] = self.props.onActivated, + [Roact.Event.InputBegan] = self.onButtonInputBegan, + [Roact.Event.InputEnded] = self.onButtonInputEnded, + [Roact.Event.TouchSwipe] = self.props.onTouchSwipe, + [Roact.Ref] = self.containerRef, + }, { + UISizeConstraint = Roact.createElement("UISizeConstraint", self.props.sizeConstraint), + Toast = self.props.renderToast({ + iconProps = iconImage and { + colorStyle = self.props.iconColorStyle, + Image = iconImage, + Size = UDim2.new(0, iconSize.X, 0, iconSize.Y), + } or nil, + padding = padding, + pressed = self.props.onActivated and self.state.pressed or nil, + pressedScale = self.props.pressedScale, + subtitleTextProps = toastSubtitle and { + colorStyle = theme.TextEmphasis, + fontStyle = subtitleStyle, + Size = fixToastResizeConfig and UDim2.new(1, 0, 0, subtitleHeight) + or UDim2.new(1, 0, 0, self.state.subtitleHeight), + Text = toastSubtitle, + } or nil, + textFrameSize = UDim2.new(1, iconImage and -iconSize.X - padding or 0, 0, textFrameHeight), + titleTextProps = { + colorStyle = theme.TextEmphasis, + fontStyle = titleStyle, + Size = fixToastResizeConfig and UDim2.new(1, 0, 0, titleHeight) or UDim2.new(1, 0, 0, self.state.titleHeight), + Text = toastTitle, + }, + }), + }) + end) +end + +function ToastContainer:didMount() + if fixToastResizeConfig then + self:setState({ + containerWidth = self.containerRef.current and self.containerRef.current.AbsoluteSize.X or 0 + }) + else + self.isMounted = true + end +end + +function ToastContainer:willUnmount() + if not fixToastResizeConfig then + self.isMounted = false + end +end + +return ToastContainer diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastContainer.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastContainer.spec.lua new file mode 100644 index 0000000..3942ce8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastContainer.spec.lua @@ -0,0 +1,69 @@ +return function() + local ToastRoot = script.Parent + local DialogRoot = ToastRoot.Parent + local AppRoot = DialogRoot.Parent + local UIBloxRoot = AppRoot.Parent + local Packages = UIBloxRoot.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local InformativeToast = require(ToastRoot.InformativeToast) + local InteractiveToast = require(ToastRoot.InteractiveToast) + local ToastContainer = require(ToastRoot.ToastContainer) + + local createToastContainer = function(props) + return mockStyleComponent({ + ToastContainer = Roact.createElement(ToastContainer, props) + }) + end + + it("should throw on empty renderToast and toastTitle", function() + local element = createToastContainer({ + renderToast = nil, + toastTitle = nil, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors", function() + local element = createToastContainer({ + renderToast = function() end, + toastTitle = "Test Title", + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when render InformativeToast", function() + local element = createToastContainer({ + iconImage = "rbxassetid://4126499279", + renderToast = function(props) + return Roact.createElement(InformativeToast, props) + end, + toastSubtitle = "test test test", + toastTitle = "Item on sale", + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when render InteractiveToast", function() + local element = createToastContainer({ + iconImage = "rbxassetid://4126499279", + onActivated = function() end, + renderToast = function(props) + return Roact.createElement(InteractiveToast, props) + end, + toastSubtitle = "Tap to see more information", + toastTitle = "System Outage", + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastFrame.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastFrame.lua new file mode 100644 index 0000000..55c4de2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastFrame.lua @@ -0,0 +1,86 @@ +local ToastRoot = script.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local ToastIcon = require(ToastRoot.ToastIcon) +local ToastText = require(ToastRoot.ToastText) +local validateToastIcon = require(ToastRoot.Validator.validateToastIcon) +local validateToastText = require(ToastRoot.Validator.validateToastText) + +local ToastFrame = Roact.PureComponent:extend("ToastFrame") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + iconProps = t.optional(validateToastIcon), + layoutOrder = t.optional(t.integer), + padding = t.numberMin(0), + position = t.optional(t.UDim2), + size = t.UDim2, + subtitleTextProps = t.optional(validateToastText), + textFrameSize = t.UDim2, + titleTextProps = validateToastText, +}) + +ToastFrame.defaultProps = { + padding = 0, + size = UDim2.new(1, 0, 1, 0), + textFrameSize = UDim2.new(1, 0, 1, 0), +} + +function ToastFrame:render() + assert(validateProps(self.props)) + + local iconProps = self.props.iconProps + local padding = self.props.padding + local subtitleTextProps = self.props.subtitleTextProps + + return Roact.createElement("Frame", { + AnchorPoint = self.props.anchorPoint, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = true, + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + Size = self.props.size, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + Padding = UDim.new(0, padding), + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + UIPadding = (padding > 0) and Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, padding), + PaddingLeft = UDim.new(0, padding), + PaddingRight = UDim.new(0, padding), + PaddingTop = UDim.new(0, padding), + }), + ToastIcon = iconProps and Roact.createElement(ToastIcon, Cryo.Dictionary.join(iconProps, { + LayoutOrder = 1, + })), + ToastTextFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = 2, + Size = self.props.textFrameSize, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + ToastTitle = Roact.createElement(ToastText, Cryo.Dictionary.join(self.props.titleTextProps, { + LayoutOrder = 1, + })), + ToastSubtitle = subtitleTextProps and Roact.createElement(ToastText, Cryo.Dictionary.join(subtitleTextProps, { + LayoutOrder = 2, + })), + }), + }) +end + +return ToastFrame diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastFrame.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastFrame.spec.lua new file mode 100644 index 0000000..5858ce7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastFrame.spec.lua @@ -0,0 +1,86 @@ +return function() + local Toast = script.Parent + local Dialog = Toast.Parent + local App = Dialog.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local TestStyle = require(UIBlox.App.Style.Validator.TestStyle) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local ToastFrame = require(Toast.ToastFrame) + + local ICON_SIZE = 36 + + local testText = "Test Title" + local testSubText = "test test test" + + local createToastFrame = function(props) + return mockStyleComponent({ + ToastFrame = Roact.createElement(ToastFrame, props) + }) + end + + it("should throw on invalid titleTextProps", function() + local element = createToastFrame({ + titleTextProps = {}, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors with valid titleTextProps", function() + local element = createToastFrame({ + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, 0, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with valid titleTextProps and subtitleTextProps", function() + local element = createToastFrame({ + subtitleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.CaptionBody, + Size = UDim2.new(1, 0, 0.5, 0), + Text = testSubText, + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, 0, 0.5, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with valid titleTextProps and iconProps", function() + local element = createToastFrame({ + iconProps = { + colorStyle = TestStyle.Theme.IconEmphasis, + Image = "rbxassetid://4126499279", + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, -ICON_SIZE, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastIcon.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastIcon.lua new file mode 100644 index 0000000..d95878a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastIcon.lua @@ -0,0 +1,51 @@ +local ToastRoot = script.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local validateColorInfo = require(UIBloxRoot.Core.Style.Validator.validateColorInfo) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ToastIcon = Roact.PureComponent:extend("ToastIcon") + +local validateProps = t.interface({ + colorStyle = t.optional(validateColorInfo), + Image = t.union(t.table, t.string), + Size = t.UDim2, +}) + +ToastIcon.defaultProps = { + BackgroundTransparency = 1, +} + +function ToastIcon:render() + assert(validateProps(self.props)) + + return withStyle(function(style) + local theme = style.Theme + + local colorStyle = self.props.colorStyle + if colorStyle == nil then + colorStyle = theme.IconEmphasis + end + + local imageColor = colorStyle.Color + local imageTransparency = colorStyle.Transparency + + local newProps = Cryo.Dictionary.join(self.props, { + colorStyle = Cryo.None, + ImageColor3 = imageColor, + ImageTransparency = imageTransparency, + }) + + return Roact.createElement(ImageSetComponent.Label, newProps) + end) +end + +return ToastIcon \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastIcon.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastIcon.spec.lua new file mode 100644 index 0000000..edf8ed2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastIcon.spec.lua @@ -0,0 +1,46 @@ +return function() + local ToastRoot = script.Parent + local DialogRoot = ToastRoot.Parent + local AppRoot = DialogRoot.Parent + local UIBloxRoot = AppRoot.Parent + local Packages = UIBloxRoot.Parent + + local Roact = require(Packages.Roact) + + local Images = require(UIBloxRoot.App.ImageSet.Images) + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local ToastIcon = require(ToastRoot.ToastIcon) + + local createToastIcon = function(props) + return mockStyleComponent({ + ToastIcon = Roact.createElement(ToastIcon, props) + }) + end + + it("should throw on invalid props", function() + local element = createToastIcon({}) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors with valid props", function() + local element = createToastIcon({ + Image = "rbxassetid://4126499279", + Size = UDim2.new(0, 36, 0, 36), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with ImageSet compatible icon", function() + local element = createToastIcon({ + Image = Images["icons/status/warning"], + Size = UDim2.new(0, 36, 0, 36), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastText.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastText.lua new file mode 100644 index 0000000..b3dd13d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastText.lua @@ -0,0 +1,34 @@ +local ToastRoot = script.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local validateFontInfo = require(UIBloxRoot.Core.Style.Validator.validateFontInfo) +local validateColorInfo = require(UIBloxRoot.Core.Style.Validator.validateColorInfo) +local GenericTextLabel = require(UIBloxRoot.Core.Text.GenericTextLabel.GenericTextLabel) + +local ToastText = Roact.PureComponent:extend("ToastText") + +local validateProps = t.interface({ + colorStyle = validateColorInfo, + fontStyle = validateFontInfo, + Size = t.UDim2, + Text = t.string, +}) + +ToastText.defaultProps = { + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, +} + +function ToastText:render() + assert(validateProps(self.props)) + + return Roact.createElement(GenericTextLabel, self.props) +end + +return ToastText diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastText.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastText.spec.lua new file mode 100644 index 0000000..89d73ef --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastText.spec.lua @@ -0,0 +1,38 @@ +return function() + local Toast = script.Parent + local Dialog = Toast.Parent + local App = Dialog.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local TestStyle = require(UIBlox.App.Style.Validator.TestStyle) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local ToastText = require(Toast.ToastText) + + local createToastText = function(props) + return mockStyleComponent({ + ToastText = Roact.createElement(ToastText, props) + }) + end + + it("should throw on invalid props", function() + local element = createToastText({}) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors with valid props", function() + local element = createToastText({ + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, 0, 1, 0), + Text = "System error", + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastContent.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastContent.lua new file mode 100644 index 0000000..daebcfb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastContent.lua @@ -0,0 +1,21 @@ +local Validator = script.Parent +local Toast = Validator.Parent +local Dialog = Toast.Parent +local App = Dialog.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local t = require(Packages.t) +local validateColorInfo = require(UIBlox.Core.Style.Validator.validateColorInfo) + +return t.strictInterface({ + iconColorStyle = t.optional(validateColorInfo), + -- Optional image to be displayed in the toast. + iconImage = t.optional(t.union(t.table, t.string)), + iconSize = t.optional(t.Vector2), + onActivated = t.optional(t.callback), + onAppeared = t.optional(t.callback), + onDismissed = t.optional(t.callback), + swipeUpDismiss = t.optional(t.boolean), + toastSubtitle = t.optional(t.string), + toastTitle = t.string, +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastIcon.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastIcon.lua new file mode 100644 index 0000000..c031e66 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastIcon.lua @@ -0,0 +1,22 @@ +local validatorRoot = script.Parent +local ToastRoot = validatorRoot.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local t = require(Packages.t) + +local validateColorInfo = require(UIBloxRoot.Core.Style.Validator.validateColorInfo) + +return t.strictInterface({ + -- ImageSet compatible image info or image directory + Image = t.union(t.table, t.string), + Size = t.UDim2, + + AnchorPoint = t.optional(t.Vector2), + -- The color table from the style palette + colorStyle = t.optional(validateColorInfo), + LayoutOrder = t.optional(t.integer), + Position = t.optional(t.UDim2), +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastText.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastText.lua new file mode 100644 index 0000000..8dde972 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastText.lua @@ -0,0 +1,31 @@ +local validatorRoot = script.Parent +local ToastRoot = validatorRoot.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local t = require(Packages.t) + +local validateFontInfo = require(UIBloxRoot.Core.Style.Validator.validateFontInfo) +local validateColorInfo = require(UIBloxRoot.Core.Style.Validator.validateColorInfo) + +return t.strictInterface({ + -- The color table from the style palette + colorStyle = validateColorInfo, + -- The Font table from the style palette + fontStyle = validateFontInfo, + Size = t.UDim2, + Text = t.string, + + AnchorPoint = t.optional(t.Vector2), + -- Whether the TextLabel is Fluid Sizing between the font's min and default sizes (optional) + fluidSizing = t.optional(t.boolean), + LayoutOrder = t.optional(t.integer), + -- The max size avaliable for the textbox + maxSize = t.optional(t.Vector2), + Position = t.optional(t.UDim2), + TextTruncate = t.optional(t.EnumItem), + TextXAlignment = t.optional(t.EnumItem), + TextYAlignment = t.optional(t.EnumItem), +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/__stories__/InformativeToastContainer.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/__stories__/InformativeToastContainer.story.lua new file mode 100644 index 0000000..f8a71cf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/__stories__/InformativeToastContainer.story.lua @@ -0,0 +1,46 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local storyRoot = script.Parent +local ToastRoot = storyRoot.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) + +local Images = require(UIBloxRoot.App.ImageSet.Images) +local InformativeToast = require(ToastRoot.InformativeToast) +local ToastContainer = require(ToastRoot.ToastContainer) + +local function InformativeToastContainer() + return Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 1, 0), + title = "InteractiveToastContainer", + subTitle = "<>", + }, { + ToastContainer = Roact.createElement(ToastContainer, { + anchorPoint = Vector2.new(0.5, 0), + iconImage = Images["icons/status/warning"], + position = UDim2.new(0.5, 0, 0, 20), + renderToast = function(props) + return Roact.createElement(InformativeToast, props) + end, + toastSubtitle = "Some details here", + toastTitle = "Title Case", + }), + }) +end + +return function(target) + local story = Roact.createElement(StoryView, {}, { + Roact.createElement(InformativeToastContainer) + }) + local handle = Roact.mount(story, target, "InformativeToast") + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/__stories__/InteractiveToastContainer.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/__stories__/InteractiveToastContainer.story.lua new file mode 100644 index 0000000..b9a7c85 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/__stories__/InteractiveToastContainer.story.lua @@ -0,0 +1,47 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local storyRoot = script.Parent +local ToastRoot = storyRoot.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) + +local Images = require(UIBloxRoot.App.ImageSet.Images) +local InteractiveToast = require(ToastRoot.InteractiveToast) +local ToastContainer = require(ToastRoot.ToastContainer) + +local function InteractiveToastContainer() + return Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 1, 0), + title = "InteractiveToastContainer", + subTitle = "<>", + }, { + ToastContainer = Roact.createElement(ToastContainer, { + anchorPoint = Vector2.new(0.5, 0), + iconImage = Images["icons/status/warning"], + onActivated = function() end, + position = UDim2.new(0.5, 0, 0, 20), + renderToast = function(props) + return Roact.createElement(InteractiveToast, props) + end, + toastSubtitle = "Some details here", + toastTitle = "Title Case", + }), + }) +end + +return function(target) + local story = Roact.createElement(StoryView, {}, { + Roact.createElement(InteractiveToastContainer) + }) + local handle = Roact.mount(story, target, "InteractiveToast") + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Enum/TooltipOrientation.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Enum/TooltipOrientation.lua new file mode 100644 index 0000000..8105751 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Enum/TooltipOrientation.lua @@ -0,0 +1,14 @@ +local AlertRoot = script.Parent.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local enumerate = require(Packages.enumerate) + +return enumerate(script.Name, { + "Bottom", + "Top", + "Right", + "Left", +}) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Tooltip.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Tooltip.lua new file mode 100644 index 0000000..3a79eed --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Tooltip.lua @@ -0,0 +1,104 @@ +local TooltipRoot = script.Parent +local DialogRoot = TooltipRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Style.withStyle) +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) + +local TooltipContainer = require(TooltipRoot.TooltipContainer) + +local TooltipOrientation = require(TooltipRoot.Enum.TooltipOrientation) + +local Tooltip = Roact.PureComponent:extend("Tooltip") + +Tooltip.validateProps = t.strictInterface({ + triggerPosition = t.Vector2, + triggerSize = t.Vector2, + bodyText = t.string, + headerText = t.optional(t.string), + onDismiss = t.optional(t.callback), + screenSize = t.optional(t.Vector2), --the app screen size + position = t.optional(t.UDim2), + orientation = t.optional(enumerateValidator(TooltipOrientation)), + triggerOnHover = t.optional(t.boolean), + forceClickTriggerPoint = t.optional(t.boolean), + isDirectChild = t.optional(t.boolean), +}) + +Tooltip.defaultProps = { + screenSize = Vector2.new(10000, 10000), + orientation = TooltipOrientation.Bottom, + triggerOnHover = false, + forceClickTriggerPoint = false, + isDirectChild = false, +} + +function Tooltip:init() + self.onDismissDefault = function() + if self.props.forceClickTriggerPoint or self.props.triggerOnHover then + return + end + if self.props.onDismiss then + self.props.onDismiss() + end + end +end + +function Tooltip:render() + local enableTriggerMask = self.props.forceClickTriggerPoint or self.props.triggerOnHover + local isDirectChild = self.props.isDirectChild + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + local tooltipComponents = { + -- Force Click Trigger Point should not prevent the rest of the UI from being interactable + Background = not self.props.forceClickTriggerPoint and Roact.createElement("TextButton", { + ZIndex = 0, + AutoButtonColor = false, + BackgroundColor3 = theme.Overlay.Color, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Position = isDirectChild and UDim2.fromOffset(-self.props.triggerPosition.X, -self.props.triggerPosition.Y) + or UDim2.fromOffset(0, 0), + Size = UDim2.fromOffset(self.props.screenSize.X, self.props.screenSize.Y), + Text = "", + [Roact.Event.Activated] = self.onDismissDefault, + [Roact.Event.TouchSwipe] = self.onDismissDefault, + [Roact.Event.MouseWheelForward] = self.onDismissDefault, + [Roact.Event.MouseWheelBackward] = self.onDismissDefault, + }), + TriggerPointMask = enableTriggerMask and Roact.createElement("TextButton", { + Text = "", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.fromOffset(self.props.triggerSize.X, self.props.triggerSize.Y), + Position = isDirectChild and UDim2.fromOffset(0, 0) + or UDim2.fromOffset(self.props.triggerPosition.X, self.props.triggerPosition.Y), + [Roact.Event.Activated] = self.props.onDismiss, + }), + TooltipContainer = Roact.createElement(TooltipContainer, { + triggerPosition = self.props.triggerPosition, + triggerSize = self.props.triggerSize, + bodyText = self.props.bodyText, + headerText = self.props.headerText, + screenSize = self.props.screenSize, + position = self.props.position, + orientation = self.props.orientation, + isDirectChild = isDirectChild, + }), + } + + return isDirectChild and Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, tooltipComponents) + or Roact.createFragment(tooltipComponents) + end) +end + +return Tooltip diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Tooltip.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Tooltip.spec.lua new file mode 100644 index 0000000..ddc7753 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Tooltip.spec.lua @@ -0,0 +1,66 @@ +return function() + local TooltipRoot = script.Parent + local DialogRoot = TooltipRoot.Parent + local App = DialogRoot.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local Tooltip = require(TooltipRoot.Tooltip) + local TooltipOrientation = require(TooltipRoot.Enum.TooltipOrientation) + + describe("mount/unmount", function() + it("should mount and unmount with required properties", function() + local element = mockStyleComponent({ + TooltipTest = Roact.createElement(Tooltip, { + -- required + triggerPosition = Vector2.new(0 ,0), + triggerSize = Vector2.new(0 ,0), + bodyText = "Tooltip body text", + }) + }) + local handle = Roact.mount(element) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + it("should mount and unmount with valid properties", function() + local element = mockStyleComponent({ + TooltipTest = Roact.createElement(Tooltip, { + -- required + triggerPosition = Vector2.new(0 ,0), + triggerSize = Vector2.new(0 ,0), + bodyText = "Tooltip body text", + -- optional + headerText = "Header", + onDismiss = function() end, + screenSize = Vector2.new(300 ,600), + position = UDim2.new(0, 0, 0, 0), + orientation = TooltipOrientation.Top, + triggerOnHover = true, + forceClickTriggerPoint = false, + isDirectChild = true, + }) + }) + local handle = Roact.mount(element) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + it("should mount and unmount with empty bodyText", function() + local element = mockStyleComponent({ + TooltipTest = Roact.createElement(Tooltip, { + -- required + triggerPosition = Vector2.new(0 ,0), + triggerSize = Vector2.new(0 ,0), + bodyText = "", + }) + }) + local handle = Roact.mount(element) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/TooltipContainer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/TooltipContainer.lua new file mode 100644 index 0000000..f1b22c3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/TooltipContainer.lua @@ -0,0 +1,223 @@ +local TooltipRoot = script.Parent +local DialogRoot = TooltipRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Otter = require(Packages.Otter) + +local withStyle = require(UIBlox.Style.withStyle) +local Images = require(AppRoot.ImageSet.Images) +local ImageSetLabel = require(UIBlox.Core.ImageSet.ImageSetComponent).Label +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) +local GetTextHeight = require(UIBlox.Core.Text.GetTextHeight) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local divideTransparency = require(UIBlox.Utility.divideTransparency) +local lerp = require(UIBlox.Utility.lerp) + +local TooltipOrientation = require(TooltipRoot.Enum.TooltipOrientation) +local getPositionInfo = require(TooltipRoot.getPositionInfo) + +local FRAME_MAX_WIDTH = 240 +local MARGIN = 12 +local PADDING_BETWEEN = 4 +local TRIANGLE_HEIGHT = 8 + +local TriangleImages = { + [TooltipOrientation.Bottom] = Images["component_assets/triangleUp_16"], + [TooltipOrientation.Top] = Images["component_assets/triangleDown_16"], + [TooltipOrientation.Right] = Images["component_assets/triangleLeft_16"], + [TooltipOrientation.Left] = Images["component_assets/triangleRight_16"], +} + +local MOTOR_OPTIONS = { + frequency = 50, + dampingRatio = 1, +} + +local TooltipContainer = Roact.PureComponent:extend("TooltipContainer") + +TooltipContainer.validateProps = t.strictInterface({ + triggerPosition = t.Vector2, + triggerSize = t.Vector2, + bodyText = t.string, + headerText = t.optional(t.string), + screenSize = t.optional(t.Vector2), --the app screen size + position = t.optional(t.UDim2), + orientation = t.optional(enumerateValidator(TooltipOrientation)), + isDirectChild = t.optional(t.boolean), +}) + +TooltipContainer.defaultProps = { + screenSize = Vector2.new(10000, 10000), + orientation = TooltipOrientation.Bottom, + isDirectChild = false, +} + +function TooltipContainer:init() + self.visible = true + + local setProgress + self.progress, setProgress = Roact.createBinding(0) + + self.progressMotor = Otter.createSingleMotor(0) + self.progressMotor:onStep(setProgress) + self.progressMotor:setGoal(Otter.spring(1, MOTOR_OPTIONS)) +end + +function TooltipContainer:render() + return withStyle(function(stylePalette) + local font = stylePalette.Font + local theme = stylePalette.Theme + + local headerFont = font.CaptionHeader + local bodyFont = font.CaptionBody + + local fontSize = font.BaseSize * font.CaptionBody.RelativeSize + + local bodyTextWidth = GetTextSize(self.props.bodyText, fontSize, bodyFont.Font, Vector2.new()).X + local innerWidth = math.min(bodyTextWidth, FRAME_MAX_WIDTH - 2 * MARGIN) + local frameWidth = innerWidth + 2 * MARGIN + + local bodyTextHeight = GetTextHeight(self.props.bodyText, bodyFont.Font, fontSize, innerWidth) + local headerTextHeight = self.props.headerText + and GetTextHeight(self.props.headerText, headerFont.Font, fontSize, innerWidth) + or 0 + + local frameHeight = self.props.headerText + and headerTextHeight + bodyTextHeight + MARGIN * 2 + PADDING_BETWEEN + or bodyTextHeight + MARGIN * 2 + + local positionInfo = getPositionInfo( + frameWidth, + frameHeight, + self.props.orientation, + self.props.triggerPosition, + self.props.triggerSize, + self.props.screenSize, + self.props.position) + + local containerPosition = self.progress:map(function(value) + local startPosition + if self.props.position then + startPosition = self.props.position + else + startPosition = self.props.isDirectChild and positionInfo.position or positionInfo.absolutePosition + end + local endPosition = startPosition + positionInfo.animatedDistance + return startPosition:lerp(endPosition, value) + end) + + local backgroundTransparency = self.progress:map(function(value) + local baseTransparency = theme.UIMuted.Transparency + local transparencyDivisor = 1 + return lerp(1, divideTransparency(baseTransparency, transparencyDivisor), value) + end) + + local textTransparency = self.progress:map(function(value) + local baseTransparency = theme.TextEmphasis.Transparency + local transparencyDivisor = 2 + return lerp(1, divideTransparency(baseTransparency, transparencyDivisor), value) + end) + + return Roact.createElement("Frame", { + Visible = self.visible, + Position = containerPosition, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = positionInfo.fillDirection == Enum.FillDirection.Vertical + and UDim2.fromOffset(frameWidth, frameHeight + TRIANGLE_HEIGHT) + or UDim2.fromOffset(frameWidth + TRIANGLE_HEIGHT, frameHeight), + }, { + UIListLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = positionInfo.fillDirection, + }), + CaretFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = positionInfo.caretFrameSize, + LayoutOrder = positionInfo.caretLayoutOrder, + }, { + Caret = Roact.createElement(ImageSetLabel, { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Position = positionInfo.caretPosition, + AnchorPoint = positionInfo.caretAnchorPoint, + Size = positionInfo.caretImageSize, + Image = TriangleImages[positionInfo.updatedOrientation], + ImageColor3 = theme.UIMuted.Color, + ImageTransparency = backgroundTransparency, + [Roact.Ref] = self.caretRef, + }), + }), + Content = Roact.createElement("TextButton", { + AutoButtonColor = false, + Text = "", + Size = UDim2.fromOffset(frameWidth, frameHeight), + BackgroundColor3 = theme.UIMuted.Color, + BackgroundTransparency = backgroundTransparency, + BorderSizePixel = 0, + LayoutOrder = positionInfo.contentLayoutOrder, + }, { + VerticalLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = UDim.new(0, PADDING_BETWEEN), + VerticalAlignment = Enum.VerticalAlignment.Center + }), + Padding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, MARGIN), + PaddingBottom = UDim.new(0, MARGIN), + PaddingLeft = UDim.new(0, MARGIN), + PaddingRight = UDim.new(0, MARGIN), + }), + Header = self.props.headerText and Roact.createElement(GenericTextLabel, { + colorStyle = theme.TextEmphasis, + fontStyle = headerFont, + LayoutOrder = 1, + Text = self.props.headerText, + TextTransparency = textTransparency, + TextXAlignment = Enum.TextXAlignment.Left, + Size = UDim2.new(1, 0, 0, headerTextHeight), + }), + Body = Roact.createElement(GenericTextLabel, { + colorStyle = theme.TextDefault, + fontStyle = bodyFont, + LayoutOrder = 2, + Text = self.props.bodyText, + TextTransparency = textTransparency, + TextXAlignment = Enum.TextXAlignment.Left, + Size = UDim2.new(1, 0, 0, bodyTextHeight), + }), + }), + }) + end) +end + +function TooltipContainer:didMount() + self.progressMotor:start() +end + +function TooltipContainer:didUpdate(lastProps, lastState) + if lastProps.triggerPosition ~= self.props.triggerPosition then + if self.props.triggerPosition.Y < 0 + or self.props.triggerPosition.Y + self.props.triggerSize.Y > self.props.screenSize.Y + then + self.visible = false + else + self.visible = true + end + end +end + +function TooltipContainer:willUnmount() + self.progressMotor:destroy() + self.progressMotor = nil +end + +return TooltipContainer diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/TooltipContainer.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/TooltipContainer.spec.lua new file mode 100644 index 0000000..0f49335 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/TooltipContainer.spec.lua @@ -0,0 +1,63 @@ +return function() + local TooltipRoot = script.Parent + local DialogRoot = TooltipRoot.Parent + local App = DialogRoot.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local TooltipContainer = require(TooltipRoot.TooltipContainer) + local TooltipOrientation = require(TooltipRoot.Enum.TooltipOrientation) + + describe("mount/unmount", function() + it("should mount and unmount with required properties", function() + local element = mockStyleComponent({ + TooltipTest = Roact.createElement(TooltipContainer, { + -- required + triggerPosition = Vector2.new(0 ,0), + triggerSize = Vector2.new(0 ,0), + bodyText = "Tooltip body text", + }) + }) + local handle = Roact.mount(element) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + it("should mount and unmount with valid properties", function() + local element = mockStyleComponent({ + TooltipTest = Roact.createElement(TooltipContainer, { + -- required + triggerPosition = Vector2.new(0 ,0), + triggerSize = Vector2.new(0 ,0), + bodyText = "Tooltip body text", + -- optional + headerText = "Header", + screenSize = Vector2.new(300 ,600), + position = UDim2.new(0, 0, 0, 0), + orientation = TooltipOrientation.Top, + isDirectChild = true, + }) + }) + local handle = Roact.mount(element) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + it("should mount and unmount with empty bodyText", function() + local element = mockStyleComponent({ + TooltipTest = Roact.createElement(TooltipContainer, { + -- required + triggerPosition = Vector2.new(0 ,0), + triggerSize = Vector2.new(0 ,0), + bodyText = "", + }) + }) + local handle = Roact.mount(element) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/getPositionInfo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/getPositionInfo.lua new file mode 100644 index 0000000..3d10af5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/getPositionInfo.lua @@ -0,0 +1,140 @@ +--[[ + Calculate absolute position of tooltip, as well as caret position / point direction, etc + return: + - absolutePosition: UDim2, Tooltip start absolute position + - position: UDim2, Tooltip start position offset from trigger point + - animatedDistance: UDim2, diff of animated position and start position + - updatedOrientation: Enum, tooltip may flip from bottom to top + - fillDirection: Enum, fill direction of caret and content frame + - caretLayoutOrder: number, 1: on top or left + - contentLayoutOrder: number + - caretPosition: UDim2, caret position relates to tooltip frame and trigger point + - caretImageSize: UDim2 + - caretAnchorPoint: Vector2 +]] + +local TooltipOrientation = require(script.Parent.Enum.TooltipOrientation) +local MARGIN = 12 +local TRIANGLE_HEIGHT = 8 -- height / width will be swapped if pointing left / right +local TRIANGLE_WIDTH = 16 + +return function(frameWidth, frameHeight, orientation, triggerPosition, triggerSize, screenSize, userInputPosition) + local positionInfo = {} + + local triggerCenter = triggerPosition + triggerSize * 0.5 + local triggerEnd = triggerPosition + triggerSize + + local absolutePosX + local absolutePosY + local offsetX + local offsetY + local animatedOffsetX = 0 + local animatedOffsetY = 0 + + local minOffset = MARGIN + + if orientation == TooltipOrientation.Bottom or orientation == TooltipOrientation.Top then + -- Vertical + + -- if space not enough under target, flip to top + -- disabled if user set position manually + if not userInputPosition and frameHeight + TRIANGLE_HEIGHT + MARGIN > screenSize.Y - triggerEnd.Y then + orientation = TooltipOrientation.Top + end + + local maxOffsetX = screenSize.X - MARGIN - frameWidth + absolutePosX = math.clamp(triggerCenter.X - frameWidth * 0.5, minOffset, maxOffsetX) + -- Tooltip offset from trigger point + offsetX = absolutePosX - triggerPosition.X + + local caretOffsetX + local maxCaretOffsetX = frameWidth - MARGIN - TRIANGLE_WIDTH + + if frameWidth < TRIANGLE_WIDTH + 2 * MARGIN then + positionInfo.caretPosition = UDim2.fromScale(0.5, 0) + elseif userInputPosition then + local dist = triggerSize.X * 0.5 + math.abs(triggerSize.X * userInputPosition.X.Scale + userInputPosition.X.Offset) + caretOffsetX = math.clamp(dist, MARGIN, maxCaretOffsetX) + positionInfo.caretPosition = UDim2.fromOffset(caretOffsetX, 0) + -- enough space for both left and right side + -- or frameWidth is less than TRIANGLE_WIDTH + 2 * MARGIN + elseif triggerCenter.X - frameWidth * 0.5 >= MARGIN and triggerCenter.X + frameWidth * 0.5 <= screenSize.X - MARGIN + then + positionInfo.caretPosition = UDim2.fromScale(0.5, 0) + else + caretOffsetX = math.clamp(triggerCenter.X - absolutePosX, MARGIN, maxCaretOffsetX) + positionInfo.caretPosition = UDim2.fromOffset(caretOffsetX, 0) + end + + positionInfo.caretFrameSize = UDim2.fromOffset(frameWidth, TRIANGLE_HEIGHT) + positionInfo.caretImageSize = UDim2.fromOffset(TRIANGLE_WIDTH, TRIANGLE_HEIGHT) + positionInfo.caretAnchorPoint = Vector2.new(0.5, 0) + positionInfo.fillDirection = Enum.FillDirection.Vertical + + if orientation == TooltipOrientation.Bottom then + -- caret pointing top + absolutePosY = triggerPosition.Y + triggerSize.Y + offsetY = triggerSize.Y + positionInfo.caretLayoutOrder = 1 + positionInfo.contentLayoutOrder = 2 + animatedOffsetY = 4 + else + -- caret pointing bottom + absolutePosY = triggerPosition.Y - frameHeight - TRIANGLE_HEIGHT + offsetY = -(frameHeight + TRIANGLE_HEIGHT) + positionInfo.caretLayoutOrder = 2 + positionInfo.contentLayoutOrder = 1 + animatedOffsetY = -4 + end + else + -- Horizontal + local maxOffsetY = screenSize.Y - MARGIN - frameHeight + absolutePosY = math.clamp(triggerCenter.Y - frameHeight * 0.5, minOffset, maxOffsetY) + offsetY = absolutePosY - triggerPosition.Y + + local caretOffsetY + local maxCaretOffsetY = frameHeight - MARGIN - TRIANGLE_WIDTH + if userInputPosition then + local dist = triggerSize.Y * 0.5 + math.abs(triggerSize.Y * userInputPosition.Y.Scale + userInputPosition.Y.Offset) + caretOffsetY = math.clamp(dist, MARGIN, maxCaretOffsetY) + positionInfo.caretPosition = UDim2.fromOffset(0, caretOffsetY) + -- frameHeight always greater than or equal to TRIANGLE_WIDTH + 2 * MARGIN + -- caret should in center for one-line tooltip + elseif triggerCenter.Y - frameHeight * 0.5 >= MARGIN and triggerCenter.Y + frameHeight * 0.5 <= screenSize.Y - MARGIN + or frameHeight <= 40 + then + positionInfo.caretPosition = UDim2.fromScale(0, 0.5) + else + local maxCaretOffset = frameHeight - MARGIN - TRIANGLE_WIDTH + caretOffsetY = math.clamp(triggerCenter.Y - absolutePosY, MARGIN, maxCaretOffset) + positionInfo.caretPosition = UDim2.fromOffset(0, caretOffsetY) + end + + positionInfo.caretFrameSize = UDim2.fromOffset(TRIANGLE_HEIGHT, frameHeight) + positionInfo.caretImageSize = UDim2.fromOffset(TRIANGLE_HEIGHT, TRIANGLE_WIDTH) + positionInfo.caretAnchorPoint = Vector2.new(0, 0.5) + positionInfo.fillDirection = Enum.FillDirection.Horizontal + + if orientation == TooltipOrientation.Right then + -- caret pointing left + absolutePosX = triggerEnd.X + offsetX = triggerSize.X + positionInfo.caretLayoutOrder = 1 + positionInfo.contentLayoutOrder = 2 + animatedOffsetX = 4 + else + -- caret pointing right + absolutePosX = triggerPosition.X - frameWidth - TRIANGLE_HEIGHT + offsetX = -(frameWidth - TRIANGLE_HEIGHT) + positionInfo.caretLayoutOrder = 2 + positionInfo.contentLayoutOrder = 1 + animatedOffsetX = -4 + end + end + + positionInfo.absolutePosition = UDim2.fromOffset(absolutePosX, absolutePosY) + positionInfo.position = UDim2.fromOffset(offsetX, offsetY) + positionInfo.animatedDistance = UDim2.fromOffset(animatedOffsetX, animatedOffsetY) + positionInfo.updatedOrientation = orientation + return positionInfo +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/getPositionInfo.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/getPositionInfo.spec.lua new file mode 100644 index 0000000..b39b204 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/getPositionInfo.spec.lua @@ -0,0 +1,35 @@ +return function() + local MARGIN = 12 + local TRIANGLE_HEIGHT = 8 -- height / width will be swapped if pointing left / right + local TRIANGLE_WIDTH = 16 + local getPositionInfo = require(script.Parent.getPositionInfo) + local TooltipOrientation = require(script.Parent.Enum.TooltipOrientation) + + describe("getPositionInfo()", function() + local triggerSize = Vector2.new(10, 10) + local screenSize = Vector2.new(800, 600) + it("should return proper position info", function() + local positionInfo = getPositionInfo(50, 30, TooltipOrientation.Bottom, Vector2.new(0, 0), triggerSize, screenSize) + expect(positionInfo).to.be.ok() + expect(positionInfo.absolutePosition).to.equal(UDim2.fromOffset(MARGIN, 10)) + expect(positionInfo.position).to.equal(UDim2.fromOffset(MARGIN, 10)) + expect(positionInfo.animatedDistance).to.equal(UDim2.fromOffset(0, 4)) + expect(positionInfo.updatedOrientation).to.equal(TooltipOrientation.Bottom) + expect(positionInfo.fillDirection).to.equal(Enum.FillDirection.Vertical) + expect(positionInfo.caretLayoutOrder).to.equal(1) + expect(positionInfo.contentLayoutOrder).to.equal(2) + expect(positionInfo.caretPosition).to.equal(UDim2.fromOffset(MARGIN, 0)) + expect(positionInfo.caretImageSize).to.equal(UDim2.fromOffset(TRIANGLE_WIDTH, TRIANGLE_HEIGHT)) + expect(positionInfo.caretAnchorPoint).to.equal(Vector2.new(0.5, 0)) + end) + it("should flip if space not enough under trigger point", function() + local positionInfo = getPositionInfo(50, 30, TooltipOrientation.Bottom, Vector2.new(0, 580), triggerSize, screenSize) + expect(positionInfo.absolutePosition).to.equal(UDim2.fromOffset(MARGIN, 542)) + expect(positionInfo.position).to.equal(UDim2.fromOffset(MARGIN, -38)) + expect(positionInfo.animatedDistance).to.equal(UDim2.fromOffset(0, -4)) + expect(positionInfo.updatedOrientation).to.equal(TooltipOrientation.Top) + expect(positionInfo.caretLayoutOrder).to.equal(2) + expect(positionInfo.contentLayoutOrder).to.equal(1) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/DefaultMetricsGridView.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/DefaultMetricsGridView.lua new file mode 100644 index 0000000..fe07c20 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/DefaultMetricsGridView.lua @@ -0,0 +1,144 @@ +local GridRoot = script.Parent +local AppRoot = GridRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local GridView = require(GridRoot.GridView) + +-- It is an error to have a window height > the maximum height the grid view can +-- grow to, so we check it here. +local function validateWindowHeight(props) + if props.windowHeight ~= nil and props.windowHeight > props.maxHeight then + return false, ("windowHeight must be less than or equal to maxHeight\nmaxHeight: %f\nwindowHeight: %f"):format( + props.maxHeight, + props.windowHeight + ) + end + + return true +end + +local isGridViewProps = t.intersection( + t.strictInterface({ + -- A function that, given the width of grid cells, returns the height of + -- grid cells. + getItemHeight = t.callback, + -- A grid metrics getter function (see GridMetrics). + getItemMetrics = t.callback, + -- How much of the grid view is visible. This determines the size of cells + -- in the grid. + windowHeight = t.optional(t.numberMin(0)), + -- A function that, given an item, returns a Roact element representing that + -- item. The item should expect to fill its parent. Setting LayoutOrder is + -- not necessary. + renderItem = t.callback, + -- The spacing between grid cells, on each axis. + itemPadding = t.Vector2, + -- All the items that can be displayed in the grid. renderItem should be + -- able to use all values in this table. + items = t.table, + -- The maximum height the grid view is allowed to grow to. + maxHeight = t.numberMin(0), + -- The layout order of the grid. + LayoutOrder = t.optional(t.integer), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), + restorePreviousChildFocus = t.optional(t.boolean), + onFocusGained = t.optional(t.callback), + + -- which selection will initally be selected (if using roact-gamepad) + defaultChildIndex = t.optional(t.numberMin(1)), + }), + validateWindowHeight +) + +local DefaultMetricsGridView = Roact.PureComponent:extend("DefaultMetricsGridView") + +DefaultMetricsGridView.defaultProps = { + maxHeight = math.huge, +} + +function DefaultMetricsGridView:init() + self.isMounted = false + + self.state = { + containerWidth = 0, + } + + self.checkSetInitialContainerWidth = function(rbx) + if self.isMounted and rbx:IsDescendantOf(game) then + self:setState({ + containerWidth = rbx.AbsoluteSize.X, + }) + end + end +end + +function DefaultMetricsGridView:render() + assert(isGridViewProps(self.props)) + + local itemMetrics = self.props.getItemMetrics(self.state.containerWidth, self.props.itemPadding.X) + local itemHeight = self.props.getItemHeight(itemMetrics.itemWidth) + + local size = Vector2.new( + math.max(0, itemMetrics.itemWidth), + math.max(0, itemHeight) + ) + + if self.state.containerWidth == 0 then + return Roact.createElement("Frame", { + Transparency = 1, + Size = UDim2.new(1, 0, 0, 0), + + [Roact.Change.AbsoluteSize] = self.checkSetInitialContainerWidth, + [Roact.Event.AncestryChanged] = self.checkSetInitialContainerWidth, + }) + end + + return Roact.createElement(GridView, { + renderItem = self.props.renderItem, + windowHeight = self.props.windowHeight, + maxHeight = self.props.maxHeight, + itemSize = size, + itemPadding = self.props.itemPadding, + items = self.props.items, + LayoutOrder = self.props.LayoutOrder, + + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + [Roact.Ref] = self.props[Roact.Ref], + + -- Optional gamepad props + defaultChildIndex = self.props.defaultChildIndex, + restorePreviousChildFocus = self.props.restorePreviousChildFocus, + onFocusGained = self.props.onFocusGained, + + onWidthChanged = function(newWidth) + if self.isMounted then + self:setState({ + containerWidth = newWidth, + }) + end + end, + }) +end + +function DefaultMetricsGridView:didMount() + self.isMounted = true +end + +function DefaultMetricsGridView:willUnmount() + self.isMounted = false +end + +return DefaultMetricsGridView \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/DefaultMetricsGridView.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/DefaultMetricsGridView.spec.lua new file mode 100644 index 0000000..dcdca12 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/DefaultMetricsGridView.spec.lua @@ -0,0 +1,182 @@ +local GridRoot = script.Parent +local AppRoot = GridRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) + +local DefaultMetricsGridView = require(script.Parent.DefaultMetricsGridView) +local GridMetrics = require(script.Parent.GridMetrics) + +-- Default properties used in testMount. Can be overridden. +local defaultTestProperties = { + renderItem = function() end, + getItemHeight = function() return 0 end, + getItemMetrics = GridMetrics.getSmallMetrics, + windowHeight = 0, + itemPadding = Vector2.new(), + items = {}, +} + +local function testMount(props) + local mergedProps = Cryo.Dictionary.join(defaultTestProperties, props) + local element = Roact.createElement(DefaultMetricsGridView, mergedProps) + local handle = Roact.mount(element, nil) + Roact.unmount(handle) +end + +return function() + describe("renderItem", function() + it("must be a function", function() + testMount({ + renderItem = function() end, + }) + + expect(function() + testMount({ + renderItem = "Frame", + }) + end).to.throw() + end) + end) + + describe("getItemHeight", function() + it("must be a function", function() + testMount({ + getItemHeight = function() return 25 end, + }) + + expect(function() + testMount({ + getItemHeight = 50, + }) + end).to.throw() + end) + end) + + describe("getItemMetrics", function() + it("must be a function", function() + testMount({ + getItemMetrics = GridMetrics.getMediumMetrics, + }) + + expect(function() + testMount({ + getItemMetrics = "large", + }) + end).to.throw() + end) + end) + + describe("windowHeight", function() + it("must be a number or nil", function() + testMount({ + windowHeight = 25, + }) + + testMount({ + windowHeight = Cryo.None, + }) + + expect(function() + testMount({ + windowHeight = Vector2.new(1, 0), + }) + end).to.throw() + end) + + it("must be non-negative", function() + expect(function() + testMount({ + windowHeight = -10, + }) + end).to.throw() + end) + + it("must be less than or equal to maxHeight", function() + testMount({ + maxHeight = 200, + windowHeight = 100, + }) + + testMount({ + maxHeight = 200, + windowHeight = 200, + }) + + expect(function() + testMount({ + maxHeight = 200, + windowHeight = 300, + }) + end).to.throw() + end) + end) + + describe("itemPadding", function() + it("must be a Vector2", function() + testMount({ + itemPadding = Vector2.new(10, 40), + }) + + expect(function() + testMount({ + itemPadding = UDim2.new(0, 10, 0.05, 0), + }) + end).to.throw() + end) + end) + + describe("items", function() + it("must be an array", function() + testMount({ + items = { 1, 2, 3 }, + }) + + expect(function() + testMount({ + items = "a,b,c", + }) + end).to.throw() + end) + end) + + describe("maxHeight", function() + it("must be a number", function() + testMount({ + maxHeight = 25, + }) + + expect(function() + testMount({ + maxHeight = Vector2.new(1, 0), + }) + end).to.throw() + end) + + it("must be non-negative", function() + expect(function() + testMount({ + maxHeight = -10, + }) + end).to.throw() + end) + end) + + describe("LayoutOrder", function() + it("must be an integer", function() + expect(function() + testMount({ + LayoutOrder = "1", + }) + end).to.throw() + + expect(function() + testMount({ + LayoutOrder = 0.5, + }) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridMetrics.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridMetrics.lua new file mode 100644 index 0000000..fd38151 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridMetrics.lua @@ -0,0 +1,72 @@ +--[[ + Documentation: + https://confluence.roblox.com/display/DESIGN/Grid+Systems + https://docs.google.com/spreadsheets/d/1zLNqGop2ha2Y4Twcvh3w_LwuAr2CwkM-AvTlhPrz7WU/edit?usp=sharing +]] + +local GridRoot = script.Parent +local AppRoot = GridRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent +local t = require(Packages.t) + +local mediumSettings = { + minimumItemsPerRow = 2, + minimumItemWidth = 160, +} + +local largeSettings = { + minimumItemsPerRow = 1, + minimumItemWidth = 332, +} + +local function getItemMetrics(containerWidth, horizontalPadding, settingsTable) + local itemsPerRow = math.floor( + (containerWidth + horizontalPadding) / (settingsTable.minimumItemWidth + horizontalPadding)) + itemsPerRow = math.max(settingsTable.minimumItemsPerRow, itemsPerRow) + local itemWidth = math.floor((containerWidth - (itemsPerRow - 1) * horizontalPadding) / itemsPerRow) + + return { + itemsPerRow = itemsPerRow, + itemWidth = itemWidth, + } +end + +local GridMetrics = {} + +local isMetricsSettings = t.strictInterface({ + minimumItemsPerRow = t.intersection(t.integer, t.numberMin(1)), + minimumItemWidth = t.numberMin(0), +}) + +local isGetterFunctionArgs = t.tuple( + t.numberMin(0), -- containerWidth + t.number -- horizontalPadding +) + +function GridMetrics.makeCustomMetricsGetter(settings) + assert(isMetricsSettings(settings)) + + return function(containerWidth, horizontalPadding) + assert(isGetterFunctionArgs(containerWidth, horizontalPadding)) + return getItemMetrics(containerWidth, horizontalPadding, settings) + end +end + +GridMetrics.getLargeMetrics = GridMetrics.makeCustomMetricsGetter(largeSettings) +GridMetrics.getMediumMetrics = GridMetrics.makeCustomMetricsGetter(mediumSettings) + +function GridMetrics.getSmallMetrics(containerWidth, horizontalPadding) + -- The small metrics are specifically defined to be one card more than the + -- medium metrics, so we grab that and then compute item width manually. + local mediumItemsPerRow = GridMetrics.getMediumMetrics(containerWidth, horizontalPadding).itemsPerRow + local itemsPerRow = mediumItemsPerRow + 1 + local itemWidth = math.floor((containerWidth - (itemsPerRow - 1) * horizontalPadding) / itemsPerRow) + + return { + itemsPerRow = itemsPerRow, + itemWidth = itemWidth, + } +end + +return GridMetrics \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridMetrics.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridMetrics.spec.lua new file mode 100644 index 0000000..b033ea5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridMetrics.spec.lua @@ -0,0 +1,135 @@ +local GridRoot = script.Parent +local AppRoot = GridRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local t = require(Packages.t) + +local GridMetrics = require(script.Parent.GridMetrics) + +local isMetricsData = t.strictInterface({ + itemsPerRow = t.intersection(t.integer, t.numberMin(1)), + itemWidth = t.numberMin(0), +}) + +return function() + describe("makeCustomMetricsGetter", function() + it("should be a function", function() + expect(typeof(GridMetrics.makeCustomMetricsGetter)).to.equal("function") + end) + + it("should make metrics getter functions", function() + local getter = GridMetrics.makeCustomMetricsGetter({ + minimumItemsPerRow = 1, + minimumItemWidth = 160, + }) + + expect(typeof(getter)).to.equal("function") + end) + + it("should make functions that validate their arguments", function() + local getter = GridMetrics.makeCustomMetricsGetter({ + minimumItemsPerRow = 1, + minimumItemWidth = 160, + }) + + expect(function() + getter(-20, 10) + end).to.throw() + + expect(function() + getter(20, Vector2.new(10, 10)) + end).to.throw() + + expect(function() + getter(Vector2.new(10, 10), 0) + end).to.throw() + end) + + it("should throw if given invalid settings", function() + expect(function() + GridMetrics.makeCustomMetricsGetter({ + minimumItemsPerRow = 0, + }) + end).to.throw() + end) + end) + + describe("getLargeMetrics", function() + it("should be a function", function() + expect(typeof(GridMetrics.getLargeMetrics)).to.equal("function") + end) + + it("should return a metrics structure", function() + assert(isMetricsData(GridMetrics.getLargeMetrics(100, 10))) + end) + + it("should throw if given invalid arguments", function() + expect(function() + GridMetrics.getLargeMetrics(-20, 10) + end).to.throw() + + expect(function() + GridMetrics.getLargeMetrics(20, Vector2.new(10, 10)) + end).to.throw() + + expect(function() + GridMetrics.getLargeMetrics(Vector2.new(10, 10), 0) + end).to.throw() + end) + end) + + describe("getMediumMetrics", function() + it("should be a function", function() + expect(typeof(GridMetrics.getMediumMetrics)).to.equal("function") + end) + + it("should return a metrics structure", function() + assert(isMetricsData(GridMetrics.getMediumMetrics(100, 10))) + end) + + it("should throw if given invalid arguments", function() + expect(function() + GridMetrics.getMediumMetrics(-20, 10) + end).to.throw() + + expect(function() + GridMetrics.getMediumMetrics(20, Vector2.new(10, 10)) + end).to.throw() + + expect(function() + GridMetrics.getMediumMetrics(Vector2.new(10, 10), 0) + end).to.throw() + end) + end) + + describe("getSmallMetrics", function() + it("should be a function", function() + expect(typeof(GridMetrics.getSmallMetrics)).to.equal("function") + end) + + it("should return a metrics structure", function() + assert(isMetricsData(GridMetrics.getSmallMetrics(100, 10))) + end) + + it("should throw if given invalid arguments", function() + expect(function() + GridMetrics.getSmallMetrics(-20, 10) + end).to.throw() + + expect(function() + GridMetrics.getSmallMetrics(20, Vector2.new(10, 10)) + end).to.throw() + + expect(function() + GridMetrics.getSmallMetrics(Vector2.new(10, 10), 0) + end).to.throw() + end) + + it("should always have a card count 1 more than the medium getter", function() + local mediumSettings = GridMetrics.getMediumMetrics(400, 10) + local smallSettings = GridMetrics.getSmallMetrics(400, 10) + expect(smallSettings.itemsPerRow).to.equal(mediumSettings.itemsPerRow + 1) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridView.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridView.lua new file mode 100644 index 0000000..fb27afc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridView.lua @@ -0,0 +1,240 @@ +local GridRoot = script.Parent +local AppRoot = GridRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent +local UIBloxConfig = require(UIBloxRoot.UIBloxConfig) +local RoactGamepad = require(Packages.RoactGamepad) + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local positiveVector2 = require(UIBloxRoot.Utility.isPositiveVector2) + +local validateProps = t.strictInterface({ + -- A function that, given an item, returns a Roact element representing that + -- item. The item should expect to fill its parent. Setting LayoutOrder is + -- not necessary. + renderItem = t.callback, + -- The size of a grid item, in pixels. + itemSize = positiveVector2, + -- The spacing between grid cells, on each axis. + itemPadding = t.Vector2, + -- All the items that can be displayed in the grid. renderItem should be + -- able to use all values in this table. This must be an array (we don't + -- check if it is for performance reasons). + items = t.table, + -- The maximum height the grid view is allowed to grow to. + maxHeight = t.numberMin(0), + -- The height of the visible window in the grid view. If nil, the grid view + -- will render all of its items. + windowHeight = t.optional(t.numberMin(0)), + -- The layout order of the grid. + LayoutOrder = t.optional(t.integer), + -- Called when the grid view measures a change in its width. Used in + -- DefaultMetricsGridView to resize the grid cells. + onWidthChanged = t.optional(t.callback), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), + restorePreviousChildFocus = t.optional(t.boolean), + onFocusGained = t.optional(t.callback), + + -- which selection will initally be selected (if using roact-gamepad + defaultChildIndex = t.optional(t.numberMin(1)), +}) + +local GridView = Roact.PureComponent:extend("GridView") + +GridView.defaultProps = { + maxHeight = math.huge, + restorePreviousChildFocus = true, +} + +function GridView:init() + self.frameRef = Roact.createRef() + self.isMounted = false + + self.state = { + containerWidth = 0, + containerYPosition = 0, + } + + self.focusableRefs = RoactGamepad.createRefCache() +end + +function GridView:render() + assert(validateProps(self.props)) + local items = self.props.items + local itemCount = #items + + local itemSize = self.props.itemSize + local itemPadding = self.props.itemPadding + local maxHeight = self.props.maxHeight + local containerWidth = self.state.containerWidth + local containerYOffset = self.state.containerYPosition + local defaultChildIndex = self.props.defaultChildIndex + local startIndex = 1 + local endIndex = itemCount + local gridChildren = {} + local x, y = 0, 0 + local maxPossibleVisibleItems = itemCount + + local itemsPerRow = math.floor((containerWidth + itemPadding.X) / (itemSize.X + itemPadding.X)) + local totalRows = math.ceil(itemCount / itemsPerRow) + local maximumRenderableRows = math.floor((maxHeight + itemPadding.Y) / (itemSize.Y + itemPadding.Y)) + local displayedRows = math.min(maximumRenderableRows, totalRows) + local containerHeight = displayedRows * itemSize.Y + math.max(displayedRows - 1, 0) * itemPadding.Y + + if self.props.windowHeight ~= nil then + if UIBloxConfig.enableExperimentalGamepadSupport then + --ensure that when you scroll you don't see items "pop" into existence at the bottom + local padRows = 2 + local visibleRows = math.floor((self.props.windowHeight + itemPadding.Y) / (itemSize.Y + itemPadding.Y)) + padRows + local startingRow = math.floor((containerYOffset + itemPadding.Y) / (itemSize.Y + itemPadding.Y)) + local finalPadRows = 1 + local endingRow = math.min(displayedRows, startingRow + visibleRows) + finalPadRows + + startIndex = math.max(1, startingRow * itemsPerRow + 1) + endIndex = math.min(itemCount, endingRow * itemsPerRow) + y = startingRow * itemSize.Y + startingRow * itemPadding.Y + + local maxPossibleRowsDisplayed = math.min(maximumRenderableRows, visibleRows) + finalPadRows + maxPossibleVisibleItems = math.abs(maxPossibleRowsDisplayed * itemsPerRow) + else + -- Add one to ensure that when you scroll you don't see items "pop" into existence with windowing. + local visibleRows = math.floor((self.props.windowHeight + itemPadding.Y) / (itemSize.Y + itemPadding.Y)) + 1 + local startingRow = math.floor((containerYOffset + itemPadding.Y) / (itemSize.Y + itemPadding.Y)) + local endingRow = math.min(displayedRows, startingRow + visibleRows) + + startIndex = math.max(1, startingRow * itemsPerRow + 1) + endIndex = math.min(itemCount, endingRow * itemsPerRow + itemsPerRow) + y = startingRow * itemSize.Y + startingRow * itemPadding.Y + end + end + + -- using maxPossibleVisibleItems means the amount of render keys will not change between renders (assuming + -- positioning/size props don't change) this is important to ensure gamepad selection stability + local maxRenderKey = UIBloxConfig.enableExperimentalGamepadSupport and + maxPossibleVisibleItems or math.abs(startIndex - endIndex) + 1 + + local function calculateRenderKey(index) + return index % maxRenderKey + end + + local function getItemIndexRef(inputRow, inputCol) + local isRowAndColInRange = inputRow > 0 and inputCol > 0 and inputCol <= itemsPerRow + local index = 1 + (((inputRow-1)*itemsPerRow) + (inputCol-1)) + local isIndexInRange = index >= startIndex and index <= endIndex + return isIndexInRange and isRowAndColInRange and self.focusableRefs[calculateRenderKey(index)] or nil + end + + -- If the item height is already greater than the maximum size we shouldn't + -- render _anything_ + if containerHeight < maxHeight then + for itemIndex = startIndex, endIndex do + local renderKey = calculateRenderKey(itemIndex) + + local currentRow = 1 + (math.floor((itemIndex - 1) / itemsPerRow)) + local currentCol = 1 + ((itemIndex - 1) % itemsPerRow) + gridChildren[renderKey] = Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable.Frame or "Frame", { + BackgroundTransparency = 1, + Position = UDim2.new(0, x, 0, y), + Size = UDim2.new(0, itemSize.X, 0, itemSize.Y), + + NextSelectionLeft = getItemIndexRef(currentRow, currentCol-1), + NextSelectionRight = getItemIndexRef(currentRow, currentCol+1), + NextSelectionUp = getItemIndexRef(currentRow-1, currentCol), + NextSelectionDown = getItemIndexRef(currentRow+1, currentCol), + [Roact.Ref] = getItemIndexRef(currentRow, currentCol), + -- Optional Gamepad prop callback which is called when a grid member is focused on + onFocusGained = UIBloxConfig.enableExperimentalGamepadSupport and self.props.onFocusGained or nil, + }, { + Content = self.props.renderItem(items[itemIndex], itemIndex) + }) + + x = math.floor(x + itemSize.X + itemPadding.X) + + -- If the x position overflows the maximum size, wrap further content + -- onto another row. We check for just itemSize because the final + -- grid item doesn't have padding tacked onto the end of it. + if x + itemSize.X > containerWidth and itemIndex < endIndex then + x = 0 + y = y + itemPadding.Y + itemSize.Y + end + end + end + + return Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable.Frame or "Frame", { + BackgroundTransparency = 1, + LayoutOrder = self.props.LayoutOrder, + Size = UDim2.new(1, 0, 0, containerHeight), + [Roact.Change.AbsolutePosition] = self.props.windowHeight ~= nil and function(rbx) + spawn(function() + if self.isMounted then + self:setState({ + containerYPosition = -math.min(0, rbx.AbsolutePosition.Y), + }) + end + end) + end or nil, + [Roact.Change.AbsoluteSize] = function(rbx) + spawn(function() + if self.isMounted then + self:setState({ + containerWidth = rbx.AbsoluteSize.X, + }) + end + + if self.props.onWidthChanged ~= nil then + self.props.onWidthChanged(rbx.AbsoluteSize.X) + end + end) + end, + + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + + [Roact.Ref] = UIBloxConfig.enableExperimentalGamepadSupport and self.props[Roact.Ref] or self.frameRef, + -- Optional Gamepad prop for which grid member to focus on by default + defaultChild = (UIBloxConfig.enableExperimentalGamepadSupport and defaultChildIndex) and + self.focusableRefs[defaultChildIndex] or nil, + -- Optional Gamepad prop for whether the previous focused on grid member should be refocused + -- when returning focus to the grid + restorePreviousChildFocus = UIBloxConfig.enableExperimentalGamepadSupport and + self.props.restorePreviousChildFocus or nil, + }, gridChildren) +end + +function GridView:didMount() + self.isMounted = true + + local ref = UIBloxConfig.enableExperimentalGamepadSupport and self.props[Roact.Ref] or self.frameRef + + if ref.current and ref.current.AbsoluteSize.X ~= 0 then + self:setState({ + containerWidth = ref.current.AbsoluteSize.X, + }) + + if self.props.onWidthChanged ~= nil then + delay(0, function() + if ref.current then + self.props.onWidthChanged(ref.current.AbsoluteSize.X) + end + end) + end + end +end + +function GridView:willUnmount() + self.isMounted = false +end + +return GridView \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridView.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridView.spec.lua new file mode 100644 index 0000000..01a5333 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridView.spec.lua @@ -0,0 +1,224 @@ +local GridRoot = script.Parent +local AppRoot = GridRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent +local UIBloxConfig = require(UIBloxRoot.UIBloxConfig) + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) + +local GridView = require(GridRoot.GridView) + +-- Used to snapshot grid items for updating unit tests. +-- luacheck: ignore unused function snapshotGridItems +local function snapshotGridItems(grid) + local records = {} + for _, child in ipairs(grid:GetChildren()) do + table.insert(records, { + item = child.Content.Text, + relativePosition = child.AbsolutePosition - grid.AbsolutePosition, + }) + end + + table.sort(records, function(a, b) + return tonumber(a.item) < tonumber(b.item) + end) + + local buffer = { "{\n" } + for _, record in ipairs(records) do + table.insert(buffer, "\t{ relativePosition = Vector2.new(") + table.insert(buffer, record.relativePosition.X) + table.insert(buffer, ", ") + table.insert(buffer, record.relativePosition.Y) + table.insert(buffer, "), content = \"") + table.insert(buffer, record.item) + table.insert(buffer, "\" },\n") + end + table.insert(buffer, "}") + print(table.concat(buffer, "")) +end + +local function validateSnapshot(grid, snapshot) + local textToItemMap = {} + -- Grid views don't guarantee that child names are stable in any way, + -- particularly with windowing - the name of a grid item does not + -- reflect the index of the list item it was created for. + for _, child in ipairs(grid:GetChildren()) do + textToItemMap[child.Content.Text] = child + end + + assert(#snapshot == #grid:GetChildren(), + ("wrong number of children: %d present in grid, but %d in snapshot"):format( + #grid:GetChildren(), + #snapshot + ) + ) + + for _, record in ipairs(snapshot) do + local item = textToItemMap[record.content] + local relativePosition = item.AbsolutePosition - grid.AbsolutePosition + assert(relativePosition.X == record.relativePosition.X and relativePosition.Y == record.relativePosition.Y, + ("item %s: relative positions did not match: {%g, %g} ~= {%g, %g}"):format( + record.content, + relativePosition.X, + relativePosition.Y, + record.relativePosition.X, + record.relativePosition.Y + ) + ) + end +end + +return function() + HACK_NO_XPCALL() + + it("should lay items out sequentially", function() + -- This is a fairly unidiomatic test to verify that the grid view lays + -- out items correctly. It tears apart the rendered Roact tree, using + -- knowledge of GridView's internals, and determines if the grid items' + -- positions match what they should be. The values used in this test are + -- designed to test several edge cases of the grid view layout system. + local expectedRelativePositions = { + Vector2.new(0, 0), + Vector2.new(112, 0), + Vector2.new(224, 0), + -- The fourth item is expected to wrap because it can't completely + -- fit in the empty space on the first row after the third item. + Vector2.new(0, 112), + } + + local tree = Roact.createElement("Frame", { + Size = UDim2.new(0, 350, 0, 200), + }, { + Grid = Roact.createElement(GridView, { + renderItem = function(i) + return Roact.createElement("TextLabel", { + Text = i, + }) + end, + items = { 1, 2, 3, 4 }, + itemPadding = Vector2.new(12, 12), + itemSize = Vector2.new(100, 100), + }), + }) + + local container = Instance.new("ScreenGui") + local handle = Roact.mount(tree, container, "GridTest") + local grid = container.GridTest.Grid + + -- Grids expand to fill their parent on the X axis, and expand to fit + -- the total number of items in the grid on the Y axis. + expect(grid.AbsoluteSize.X).to.equal(350) + expect(grid.AbsoluteSize.Y).to.equal(212) + + local textToItemMap = {} + -- Grid views don't guarantee that child names are stable in any way, + -- particularly with windowing - the name of a grid item does not + -- reflect the index of the list item it was created for. + for _, child in ipairs(grid:GetChildren()) do + textToItemMap[child.Content.Text] = child + end + + for itemValue, expectedRelativePosition in ipairs(expectedRelativePositions) do + local item = textToItemMap[tostring(itemValue)] + assert(item ~= nil, "couldn't find item for index " .. itemValue) + + local relativePosition = item.AbsolutePosition - grid.AbsolutePosition + expect(relativePosition.X).to.equal(expectedRelativePosition.X) + expect(relativePosition.Y).to.equal(expectedRelativePosition.Y) + end + + -- Grids don't use a layout object, so this check will work for ensuring + -- that all the items were rendered. + expect(#grid:GetChildren()).to.equal(#expectedRelativePositions) + Roact.unmount(handle) + end) + + it("should window items if windowHeight is specified", function() + local itemCount = 100 + local items = {} + for i = 1, itemCount do + table.insert(items, i) + end + + local tree = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, (itemCount / 4) * 25), + }, { + Grid = Roact.createElement(GridView, { + renderItem = function(i) + return Roact.createElement("TextLabel", { + Text = i, + }) + end, + items = items, + itemPadding = Vector2.new(7, 7), + itemSize = Vector2.new(46, 38), + windowHeight = 127, + }), + }) + + local container = Instance.new("ScreenGui") + local handle = Roact.mount(tree, container, "GridTest") + local grid = container.GridTest.Grid + + -- Grids expand to fill their parent on the X axis, and expand to fit + -- the total number of items in the grid on the Y axis. When windowing, + -- they will still size themselves to fit _all_ the grids, even if + -- they're not all rendered! + expect(grid.AbsoluteSize.X).to.equal(100) + + local initialSnapshot = { + { relativePosition = Vector2.new(0, 0), content = "1" }, + { relativePosition = Vector2.new(53, 0), content = "2" }, + { relativePosition = Vector2.new(0, 45), content = "3" }, + { relativePosition = Vector2.new(53, 45), content = "4" }, + { relativePosition = Vector2.new(0, 90), content = "5" }, + { relativePosition = Vector2.new(53, 90), content = "6" }, + { relativePosition = Vector2.new(0, 135), content = "7" }, + { relativePosition = Vector2.new(53, 135), content = "8" }, + } + + if UIBloxConfig.enableExperimentalGamepadSupport then + initialSnapshot = Cryo.List.join(initialSnapshot, { + { relativePosition = Vector2.new(0, 180), content = "9" }, + { relativePosition = Vector2.new(53, 180), content = "10" }, + }) + end + + -- snapshotGridItems(grid) + validateSnapshot(grid, initialSnapshot) + + -- If we move the grid, it will relayout itself and render a different + -- set of items. + grid.Position = UDim2.new(0, 0, 0, -250) + -- Dummy read necessary to force the windowing to update. In the test + -- scenario that we have, absolute position will not update until the + -- property is read somewhere in the tree. + local _ = grid.AbsolutePosition + + local afterMoveSnapshot = { + { relativePosition = Vector2.new(0, 225), content = "11" }, + { relativePosition = Vector2.new(53, 225), content = "12" }, + { relativePosition = Vector2.new(0, 270), content = "13" }, + { relativePosition = Vector2.new(53, 270), content = "14" }, + { relativePosition = Vector2.new(0, 315), content = "15" }, + { relativePosition = Vector2.new(53, 315), content = "16" }, + { relativePosition = Vector2.new(0, 360), content = "17" }, + { relativePosition = Vector2.new(53, 360), content = "18" }, + } + + if UIBloxConfig.enableExperimentalGamepadSupport then + afterMoveSnapshot = Cryo.List.join(afterMoveSnapshot, { + { relativePosition = Vector2.new(0, 405), content = "19" }, + { relativePosition = Vector2.new(53, 405), content = "20" }, + }) + end + + wait() + + -- snapshotGridItems(grid) + validateSnapshot(grid, afterMoveSnapshot) + + Roact.unmount(handle) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/ScrollingGridView.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/ScrollingGridView.lua new file mode 100644 index 0000000..f0e1dac --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/ScrollingGridView.lua @@ -0,0 +1,71 @@ +local Grid = script.Parent +local App = Grid.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local VerticalScrollView = require(App.Container.VerticalScrollView) + +local ScrollingGridView = Roact.PureComponent:extend("ScrollingGridView") + +ScrollingGridView.validateProps = t.strictInterface({ + items = t.table, + renderItem = t.callback, + itemSize = t.Vector2, + size = t.optional(t.UDim2), + itemPadding = t.optional(t.Vector2), + horizontalAlignment = t.optional(t.EnumItem), +}) + +ScrollingGridView.defaultProps = { + itemPadding = Vector2.new(12, 24), + horizontalAlignment = Enum.HorizontalAlignment.Center, +} + +function ScrollingGridView:init() + self.gridRef = Roact.createRef() + self.state ={ + contentSize = Vector2.new(0, 0) + } + + self.onGridResize = function() + local contentSize = self.gridRef.current.AbsoluteContentSize + self:setState({ + contentSize = contentSize, + }) + end +end + +function ScrollingGridView:render() + local contentSize = self.state.contentSize + + local gridItems = {} + + for key, value in pairs(self.props.items) do + gridItems[key] = self.props.renderItem(value) + end + + gridItems.GridLayout = Roact.createElement("UIGridLayout", { + CellSize = UDim2.fromOffset(self.props.itemSize.X, self.props.itemSize.Y), + CellPadding = UDim2.fromOffset(self.props.itemPadding.X, self.props.itemPadding.Y), + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = self.props.horizontalAlignment, + [Roact.Change.AbsoluteContentSize] = self.onGridResize, + [Roact.Ref] = self.gridRef, + }) + + return Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + [Roact.Change.AbsoluteSize] = self.onResize, + }, { + VerticalScrollView = Roact.createElement(VerticalScrollView, { + size = UDim2.fromScale(1, 1), + canvasSizeY = UDim.new(0, contentSize.Y), + }, gridItems) + }) +end + +return ScrollingGridView \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/ScrollingGridView.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/ScrollingGridView.spec.lua new file mode 100644 index 0000000..5b04c14 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/ScrollingGridView.spec.lua @@ -0,0 +1,48 @@ +return function() + local Grid = script.Parent + local App = Grid.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local ScrollingGridView = require(script.Parent.ScrollingGridView) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + ScrollingGridView = Roact.createElement(ScrollingGridView, { + items = {1, 2, 3}, + renderItem = function(i) + return Roact.createElement("TextLabel", { + Text = i, + Size = UDim2.new(1, 0, 1, 0), + }) + end, + itemSize = Vector2.new(100, 100), + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with all props", function() + local element = mockStyleComponent({ + ScrollingGridView = Roact.createElement(ScrollingGridView, { + items = {1, 2, 3}, + renderItem = function(i) + return Roact.createElement("TextLabel", { + Text = i, + Size = UDim2.new(1, 0, 1, 0), + }) + end, + itemSize = Vector2.new(100, 100), + size = UDim2.new(1, 0, 1, 0), + itemPadding = Vector2.new(12, 24), + horizontalAlignment = Enum.HorizontalAlignment.Center, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/__stories__/Grid.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/__stories__/Grid.story.lua new file mode 100644 index 0000000..4894df8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/__stories__/Grid.story.lua @@ -0,0 +1,135 @@ +local GridRoot = script.Parent.Parent +local AppRoot = GridRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent +local Roact = require(Packages.Roact) +local UIBloxConfig = require(UIBloxRoot.UIBloxConfig) +local RoactGamepad = require(Packages.RoactGamepad) + +local DefaultMetricsGridView = require(GridRoot.DefaultMetricsGridView) +local GridMetrics = require(GridRoot.GridMetrics) + +local InputManager = require(Packages.StoryComponents.InputManager) + +local DemoComponent = Roact.PureComponent:extend("DemoComponent") + +function DemoComponent:init() + self.scrollingRef = Roact.createRef() + + self.state = { + windowSize = Vector2.new(0, 0), + metrics = GridMetrics.getSmallMetrics, + } + + if UIBloxConfig.enableExperimentalGamepadSupport then + self.focusController = RoactGamepad.createFocusController() + end +end + +function DemoComponent:updateWindowSize() + self:setState({ + windowSize = self.scrollingRef.current.AbsoluteWindowSize, + }) +end + +function DemoComponent:render() + local items = {} + + for i = 1, 100 do + table.insert(items, i) + end + + local metrics = { + { + name = "Small", + getter = GridMetrics.getSmallMetrics, + }, + { + name = "Medium", + getter = GridMetrics.getMediumMetrics, + }, + { + name = "Large", + getter = GridMetrics.getLargeMetrics, + }, + } + + local selectorChildren = { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + }) + } + + for index, item in ipairs(metrics) do + selectorChildren[item.name] = Roact.createElement("TextButton", { + Size = UDim2.new(1 / #metrics, 0, 1, 0), + Text = item.name, + LayoutOrder = index, + [Roact.Event.Activated] = function() + self:setState({ + metrics = GridMetrics["get" .. item.name .. "Metrics"], + }) + end, + }) + end + + return Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable.Frame or "Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + + focusController = UIBloxConfig.enableExperimentalGamepadSupport and self.focusController or nil, + }, { + InputManager = UIBloxConfig.enableExperimentalGamepadSupport and Roact.createElement(InputManager, { + focusController = self.focusController, + }), + MetricsSelector = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 30), + BackgroundTransparency = 1, + }, selectorChildren), + GridScroller = Roact.createElement("ScrollingFrame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, -30), + Position = UDim2.new(0, 0, 0, 30), + ScrollBarImageColor3 = Color3.new(0, 0, 0), + VerticalScrollBarInset = Enum.ScrollBarInset.Always, + [Roact.Ref] = self.scrollingRef, + [Roact.Change.AbsoluteSize] = function() + wait(0) + self:updateWindowSize() + end, + }, { + GridView = Roact.createElement(DefaultMetricsGridView, { + renderItem = function(i) + return Roact.createElement("TextLabel", { + Text = i, + LayoutOrder = i, + Size = UDim2.new(1, 0, 1, 0), + }) + end, + getItemHeight = function(width) + return width + end, + getItemMetrics = self.state.metrics, + windowHeight = self.state.windowSize.Y, + itemPadding = Vector2.new(12, 12), + items = items, + + defaultChildIndex = UIBloxConfig.enableExperimentalGamepadSupport and 1 or nil, + }) + }) + }) +end + +function DemoComponent:didMount() + self:updateWindowSize() +end + +return function(target) + local handle = Roact.mount(Roact.createElement(DemoComponent), target, "2DGrid") + + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Enum/IconSize.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Enum/IconSize.lua new file mode 100644 index 0000000..68d7035 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Enum/IconSize.lua @@ -0,0 +1,13 @@ +local ImageSet = script.Parent.Parent +local AppRoot = ImageSet.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate("IconSize", { + "Small", + "Medium", + "Large", + "XLarge", + "XXLarge", +}) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/GetImageSetData.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/GetImageSetData.lua new file mode 100644 index 0000000..51ae555 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/GetImageSetData.lua @@ -0,0 +1,776 @@ + +-- luacheck: ignore +------------------------------------------------------------------ +-- AUTOGENERATED +-- by bin/ImagePacker.py in UIBlox +-- +-- DO NOT EDIT +------------------------------------------------------------------ + +local assets_1x = nil +function make_assets_1x() assets_1x = { + ['component_assets/bulletDown_17_stroke_3'] = { ImageRectOffset = Vector2.new(492, 156), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/bulletLeft_17_stroke_3'] = { ImageRectOffset = Vector2.new(457, 87), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/bulletRight_17_stroke_3'] = { ImageRectOffset = Vector2.new(492, 174), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/bulletUp_17_stroke_3'] = { ImageRectOffset = Vector2.new(439, 87), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/bullet_17'] = { ImageRectOffset = Vector2.new(492, 107), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_16'] = { ImageRectOffset = Vector2.new(481, 0), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['component_assets/circle_17'] = { ImageRectOffset = Vector2.new(492, 254), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_17_mask'] = { ImageRectOffset = Vector2.new(492, 223), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_17_stroke_1'] = { ImageRectOffset = Vector2.new(492, 205), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_17_stroke_3'] = { ImageRectOffset = Vector2.new(492, 125), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_21'] = { ImageRectOffset = Vector2.new(489, 60), ImageRectSize = Vector2.new(21, 21), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_21_stroke_1'] = { ImageRectOffset = Vector2.new(489, 82), ImageRectSize = Vector2.new(21, 21), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_22_stroke_3'] = { ImageRectOffset = Vector2.new(489, 37), ImageRectSize = Vector2.new(22, 22), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_24_stroke_1'] = { ImageRectOffset = Vector2.new(0, 481), ImageRectSize = Vector2.new(24, 24), ImageSet = 'img_set_1x_2' }, + ['component_assets/circle_25'] = { ImageRectOffset = Vector2.new(221, 481), ImageRectSize = Vector2.new(25, 25), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_26_stroke_3'] = { ImageRectOffset = Vector2.new(157, 478), ImageRectSize = Vector2.new(26, 26), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_28_padding_10'] = { ImageRectOffset = Vector2.new(296, 352), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_29'] = { ImageRectOffset = Vector2.new(127, 478), ImageRectSize = Vector2.new(29, 29), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_29_mask'] = { ImageRectOffset = Vector2.new(97, 478), ImageRectSize = Vector2.new(29, 29), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_29_stroke_1'] = { ImageRectOffset = Vector2.new(62, 478), ImageRectSize = Vector2.new(29, 29), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_30_stroke_3'] = { ImageRectOffset = Vector2.new(0, 478), ImageRectSize = Vector2.new(30, 30), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_36'] = { ImageRectOffset = Vector2.new(0, 148), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['component_assets/circle_36_stroke_1'] = { ImageRectOffset = Vector2.new(0, 111), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['component_assets/circle_42_stroke_3'] = { ImageRectOffset = Vector2.new(194, 337), ImageRectSize = Vector2.new(42, 42), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_49'] = { ImageRectOffset = Vector2.new(194, 381), ImageRectSize = Vector2.new(49, 49), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_49_mask'] = { ImageRectOffset = Vector2.new(194, 431), ImageRectSize = Vector2.new(49, 49), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_49_stroke_1'] = { ImageRectOffset = Vector2.new(439, 37), ImageRectSize = Vector2.new(49, 49), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_52_stroke_3'] = { ImageRectOffset = Vector2.new(194, 284), ImageRectSize = Vector2.new(52, 52), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_69_stroke_3'] = { ImageRectOffset = Vector2.new(247, 37), ImageRectSize = Vector2.new(69, 69), ImageSet = 'img_set_1x_1' }, + ['component_assets/dropshadow_25'] = { ImageRectOffset = Vector2.new(296, 446), ImageRectSize = Vector2.new(37, 37), ImageSet = 'img_set_1x_1' }, + ['component_assets/dropshadow_28'] = { ImageRectOffset = Vector2.new(296, 303), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['component_assets/dropshadow_chatOff'] = { ImageRectOffset = Vector2.new(0, 74), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['component_assets/dropshadow_chatOn'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['component_assets/dropshadow_more'] = { ImageRectOffset = Vector2.new(0, 37), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['component_assets/halfcircleLeft_17'] = { ImageRectOffset = Vector2.new(184, 478), ImageRectSize = Vector2.new(8, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/halfcircleRight_17'] = { ImageRectOffset = Vector2.new(475, 87), ImageRectSize = Vector2.new(8, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/square_7_stroke_3'] = { ImageRectOffset = Vector2.new(239, 337), ImageRectSize = Vector2.new(7, 7), ImageSet = 'img_set_1x_1' }, + ['component_assets/triangleDown_16'] = { ImageRectOffset = Vector2.new(317, 98), ImageRectSize = Vector2.new(16, 8), ImageSet = 'img_set_1x_1' }, + ['component_assets/triangleLeft_16'] = { ImageRectOffset = Vector2.new(334, 463), ImageRectSize = Vector2.new(8, 16), ImageSet = 'img_set_1x_1' }, + ['component_assets/triangleRight_16'] = { ImageRectOffset = Vector2.new(334, 446), ImageRectSize = Vector2.new(8, 16), ImageSet = 'img_set_1x_1' }, + ['component_assets/triangleUp_16'] = { ImageRectOffset = Vector2.new(479, 27), ImageRectSize = Vector2.new(16, 8), ImageSet = 'img_set_1x_1' }, + ['component_assets/user_60_mask'] = { ImageRectOffset = Vector2.new(378, 37), ImageRectSize = Vector2.new(60, 60), ImageSet = 'img_set_1x_1' }, + ['component_assets/vignette_246'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(246, 246), ImageSet = 'img_set_1x_1' }, + ['gradient/gradient_0_100'] = { ImageRectOffset = Vector2.new(237, 337), ImageRectSize = Vector2.new(1, 40), ImageSet = 'img_set_1x_1' }, + ['icons/actions/accept'] = { ImageRectOffset = Vector2.new(345, 340), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/block'] = { ImageRectOffset = Vector2.new(148, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/calendar'] = { ImageRectOffset = Vector2.new(493, 303), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/actions/compose'] = { ImageRectOffset = Vector2.new(296, 148), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/cycleLeft'] = { ImageRectOffset = Vector2.new(222, 148), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/cycleRight'] = { ImageRectOffset = Vector2.new(345, 414), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/edit/add'] = { ImageRectOffset = Vector2.new(456, 451), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/edit/clear'] = { ImageRectOffset = Vector2.new(419, 451), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/edit/clear_small'] = { ImageRectOffset = Vector2.new(493, 320), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/actions/edit/copy'] = { ImageRectOffset = Vector2.new(382, 414), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/edit/delete'] = { ImageRectOffset = Vector2.new(419, 414), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/edit/edit'] = { ImageRectOffset = Vector2.new(382, 451), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/edit/remove'] = { ImageRectOffset = Vector2.new(456, 414), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/favoriteOff'] = { ImageRectOffset = Vector2.new(370, 148), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/favoriteOn'] = { ImageRectOffset = Vector2.new(345, 451), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/feedback'] = { ImageRectOffset = Vector2.new(456, 303), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/filter'] = { ImageRectOffset = Vector2.new(419, 377), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/friends/friendAdd'] = { ImageRectOffset = Vector2.new(419, 340), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/friends/friendInvite'] = { ImageRectOffset = Vector2.new(345, 377), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/friends/friendRemove'] = { ImageRectOffset = Vector2.new(382, 340), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/friends/friendsplaying'] = { ImageRectOffset = Vector2.new(456, 340), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/info'] = { ImageRectOffset = Vector2.new(148, 296), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/info_small'] = { ImageRectOffset = Vector2.new(493, 357), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/actions/previewExpand'] = { ImageRectOffset = Vector2.new(185, 148), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/previewShrink'] = { ImageRectOffset = Vector2.new(407, 148), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/randomize'] = { ImageRectOffset = Vector2.new(259, 148), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/reject'] = { ImageRectOffset = Vector2.new(419, 451), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/respawn'] = { ImageRectOffset = Vector2.new(333, 148), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/selectOn'] = { ImageRectOffset = Vector2.new(382, 303), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/selectOn_small'] = { ImageRectOffset = Vector2.new(345, 488), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/actions/send'] = { ImageRectOffset = Vector2.new(419, 303), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/share'] = { ImageRectOffset = Vector2.new(382, 377), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/truncationCollapse'] = { ImageRectOffset = Vector2.new(444, 148), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/truncationExpand'] = { ImageRectOffset = Vector2.new(442, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/viewOff'] = { ImageRectOffset = Vector2.new(321, 484), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/actions/viewOn'] = { ImageRectOffset = Vector2.new(493, 340), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/actions/vote/voteDownOff'] = { ImageRectOffset = Vector2.new(148, 370), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/vote/voteDownOn'] = { ImageRectOffset = Vector2.new(148, 444), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/vote/voteUpOff'] = { ImageRectOffset = Vector2.new(148, 333), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/vote/voteUpOn'] = { ImageRectOffset = Vector2.new(148, 407), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/zoomIn'] = { ImageRectOffset = Vector2.new(345, 303), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/zoomOut'] = { ImageRectOffset = Vector2.new(456, 377), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/common/goldrobux'] = { ImageRectOffset = Vector2.new(296, 74), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/goldrobux_small'] = { ImageRectOffset = Vector2.new(362, 488), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/common/more'] = { ImageRectOffset = Vector2.new(407, 74), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/notificationOff'] = { ImageRectOffset = Vector2.new(111, 148), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/notificationOn'] = { ImageRectOffset = Vector2.new(370, 74), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/play'] = { ImageRectOffset = Vector2.new(444, 74), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/refresh'] = { ImageRectOffset = Vector2.new(111, 111), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/refresh_small'] = { ImageRectOffset = Vector2.new(165, 481), ImageRectSize = Vector2.new(14, 15), ImageSet = 'img_set_1x_2' }, + ['icons/common/robux'] = { ImageRectOffset = Vector2.new(333, 74), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/robux_small'] = { ImageRectOffset = Vector2.new(493, 377), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/common/search'] = { ImageRectOffset = Vector2.new(259, 74), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/search_small'] = { ImageRectOffset = Vector2.new(493, 394), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/common/settings'] = { ImageRectOffset = Vector2.new(185, 74), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/user'] = { ImageRectOffset = Vector2.new(222, 74), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/user_60'] = { ImageRectOffset = Vector2.new(317, 37), ImageRectSize = Vector2.new(60, 60), ImageSet = 'img_set_1x_1' }, + ['icons/controls/close-ingame'] = { ImageRectOffset = Vector2.new(259, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/controls'] = { ImageRectOffset = Vector2.new(111, 74), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/emoteOff'] = { ImageRectOffset = Vector2.new(37, 296), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/emoteOn'] = { ImageRectOffset = Vector2.new(222, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/arrowDown'] = { ImageRectOffset = Vector2.new(493, 414), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/controls/keys/arrowLeft'] = { ImageRectOffset = Vector2.new(399, 488), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/controls/keys/arrowRight'] = { ImageRectOffset = Vector2.new(493, 431), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/controls/keys/arrowUp'] = { ImageRectOffset = Vector2.new(382, 488), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/controls/keys/dpadDown'] = { ImageRectOffset = Vector2.new(74, 333), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/dpadLeft'] = { ImageRectOffset = Vector2.new(259, 37), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/dpadRight'] = { ImageRectOffset = Vector2.new(333, 37), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/dpadUp'] = { ImageRectOffset = Vector2.new(74, 74), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/key_single'] = { ImageRectOffset = Vector2.new(407, 37), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/key_wide'] = { ImageRectOffset = Vector2.new(340, 0), ImageRectSize = Vector2.new(64, 36), ImageSet = 'img_set_1x_1' }, + ['icons/controls/keys/key_xwide'] = { ImageRectOffset = Vector2.new(247, 0), ImageRectSize = Vector2.new(92, 36), ImageSet = 'img_set_1x_1' }, + ['icons/controls/keys/xboxA'] = { ImageRectOffset = Vector2.new(111, 37), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxB'] = { ImageRectOffset = Vector2.new(222, 37), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxLS'] = { ImageRectOffset = Vector2.new(185, 37), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxLSDirectional'] = { ImageRectOffset = Vector2.new(74, 222), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxLSHorizontal'] = { ImageRectOffset = Vector2.new(74, 259), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxLSVertical'] = { ImageRectOffset = Vector2.new(444, 37), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxLT'] = { ImageRectOffset = Vector2.new(370, 37), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxRB'] = { ImageRectOffset = Vector2.new(74, 148), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxRS'] = { ImageRectOffset = Vector2.new(74, 185), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxRSDirectional'] = { ImageRectOffset = Vector2.new(74, 37), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxRSHorizontal'] = { ImageRectOffset = Vector2.new(296, 37), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxRSVertical'] = { ImageRectOffset = Vector2.new(148, 37), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxRT'] = { ImageRectOffset = Vector2.new(74, 111), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxView'] = { ImageRectOffset = Vector2.new(74, 296), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxX'] = { ImageRectOffset = Vector2.new(74, 407), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxY'] = { ImageRectOffset = Vector2.new(74, 370), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxmenu'] = { ImageRectOffset = Vector2.new(74, 444), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/leaderboardOff'] = { ImageRectOffset = Vector2.new(37, 222), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/leaderboardOn'] = { ImageRectOffset = Vector2.new(37, 333), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/mouse/clickLeft'] = { ImageRectOffset = Vector2.new(37, 444), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/mouse/clickRight'] = { ImageRectOffset = Vector2.new(37, 370), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/mouse/scroll'] = { ImageRectOffset = Vector2.new(37, 407), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/players'] = { ImageRectOffset = Vector2.new(37, 185), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/screenrecord'] = { ImageRectOffset = Vector2.new(148, 74), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/screenshot'] = { ImageRectOffset = Vector2.new(37, 259), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/topmenu-shadow'] = { ImageRectOffset = Vector2.new(443, 205), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/controls/vehicle/backward'] = { ImageRectOffset = Vector2.new(37, 148), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/vehicle/driver'] = { ImageRectOffset = Vector2.new(407, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/vehicle/exit'] = { ImageRectOffset = Vector2.new(37, 74), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/vehicle/flip'] = { ImageRectOffset = Vector2.new(37, 37), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/vehicle/forward'] = { ImageRectOffset = Vector2.new(444, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/vehicle/passenger'] = { ImageRectOffset = Vector2.new(37, 111), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/weapon/fire'] = { ImageRectOffset = Vector2.new(296, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/weapon/scopeOff'] = { ImageRectOffset = Vector2.new(333, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/weapon/scopeOn'] = { ImageRectOffset = Vector2.new(370, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/graphic/error_xlarge'] = { ImageRectOffset = Vector2.new(0, 381), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_1x_1' }, + ['icons/graphic/loadingspinner'] = { ImageRectOffset = Vector2.new(247, 303), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/graphic/premium_large'] = { ImageRectOffset = Vector2.new(247, 254), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/graphic/premium_xlarge'] = { ImageRectOffset = Vector2.new(97, 284), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_1x_1' }, + ['icons/graphic/success_xlarge'] = { ImageRectOffset = Vector2.new(0, 284), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_1x_1' }, + ['icons/imageUnavailable'] = { ImageRectOffset = Vector2.new(296, 401), ImageRectSize = Vector2.new(44, 44), ImageSet = 'img_set_1x_1' }, + ['icons/logo/block'] = { ImageRectOffset = Vector2.new(111, 185), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/logo/letterform'] = { ImageRectOffset = Vector2.new(0, 247), ImageRectSize = Vector2.new(207, 36), ImageSet = 'img_set_1x_1' }, + ['icons/menu/about_large'] = { ImageRectOffset = Vector2.new(443, 156), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/avatar_off'] = { ImageRectOffset = Vector2.new(185, 111), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/avatar_on'] = { ImageRectOffset = Vector2.new(370, 111), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/blog'] = { ImageRectOffset = Vector2.new(222, 111), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/blog_large'] = { ImageRectOffset = Vector2.new(247, 156), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/chat_off'] = { ImageRectOffset = Vector2.new(444, 111), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/chat_on'] = { ImageRectOffset = Vector2.new(111, 333), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/create_large'] = { ImageRectOffset = Vector2.new(394, 205), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/customize'] = { ImageRectOffset = Vector2.new(259, 111), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/customize_large'] = { ImageRectOffset = Vector2.new(247, 107), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/events_large'] = { ImageRectOffset = Vector2.new(345, 107), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/feed'] = { ImageRectOffset = Vector2.new(111, 222), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/feed_large'] = { ImageRectOffset = Vector2.new(296, 107), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/friends'] = { ImageRectOffset = Vector2.new(148, 111), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/friends_large'] = { ImageRectOffset = Vector2.new(394, 107), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/games_off'] = { ImageRectOffset = Vector2.new(111, 370), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/games_on'] = { ImageRectOffset = Vector2.new(148, 222), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/giftcard'] = { ImageRectOffset = Vector2.new(333, 111), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/groups'] = { ImageRectOffset = Vector2.new(148, 185), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/groups_large'] = { ImageRectOffset = Vector2.new(443, 107), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/help_large'] = { ImageRectOffset = Vector2.new(296, 156), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/home_off'] = { ImageRectOffset = Vector2.new(407, 111), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/home_on'] = { ImageRectOffset = Vector2.new(111, 444), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/inventory'] = { ImageRectOffset = Vector2.new(111, 296), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/inventoryOff'] = { ImageRectOffset = Vector2.new(111, 259), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/inventoryOn'] = { ImageRectOffset = Vector2.new(111, 296), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/inventory_large'] = { ImageRectOffset = Vector2.new(345, 205), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/messages_large'] = { ImageRectOffset = Vector2.new(345, 156), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/more_off'] = { ImageRectOffset = Vector2.new(407, 74), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/more_on'] = { ImageRectOffset = Vector2.new(148, 148), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/profile'] = { ImageRectOffset = Vector2.new(296, 111), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/scanqr_large'] = { ImageRectOffset = Vector2.new(394, 156), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/settings_large'] = { ImageRectOffset = Vector2.new(296, 205), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/shop'] = { ImageRectOffset = Vector2.new(111, 407), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/shop_large'] = { ImageRectOffset = Vector2.new(247, 205), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/trade'] = { ImageRectOffset = Vector2.new(148, 259), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/navigation/close'] = { ImageRectOffset = Vector2.new(0, 370), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/navigation/pushBack'] = { ImageRectOffset = Vector2.new(0, 185), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/navigation/pushRight'] = { ImageRectOffset = Vector2.new(0, 333), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/navigation/pushRight_small'] = { ImageRectOffset = Vector2.new(481, 17), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/navigation/swipe'] = { ImageRectOffset = Vector2.new(0, 222), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/navigation/swipeDown'] = { ImageRectOffset = Vector2.new(0, 259), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/navigation/swipeUp'] = { ImageRectOffset = Vector2.new(0, 296), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/placeholder/placeholderOff'] = { ImageRectOffset = Vector2.new(405, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/placeholder/placeholderOn'] = { ImageRectOffset = Vector2.new(208, 247), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/placeholder/placeholderOn_small'] = { ImageRectOffset = Vector2.new(492, 272), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/status/alert'] = { ImageRectOffset = Vector2.new(0, 407), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/alert_small'] = { ImageRectOffset = Vector2.new(37, 481), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/error_large'] = { ImageRectOffset = Vector2.new(345, 254), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/status/games/people-playing_large'] = { ImageRectOffset = Vector2.new(247, 401), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/status/games/people-playing_small'] = { ImageRectOffset = Vector2.new(148, 481), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/games/rating_large'] = { ImageRectOffset = Vector2.new(247, 450), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/status/games/rating_small'] = { ImageRectOffset = Vector2.new(481, 128), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/games/sessions_large'] = { ImageRectOffset = Vector2.new(296, 254), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/status/games/sessions_small'] = { ImageRectOffset = Vector2.new(481, 111), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/imageunavailable'] = { ImageRectOffset = Vector2.new(443, 254), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/status/imageunavailable_small'] = { ImageRectOffset = Vector2.new(296, 484), ImageRectSize = Vector2.new(24, 24), ImageSet = 'img_set_1x_1' }, + ['icons/status/item/bundle'] = { ImageRectOffset = Vector2.new(481, 91), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/item/limited'] = { ImageRectOffset = Vector2.new(111, 481), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/item/owned'] = { ImageRectOffset = Vector2.new(481, 74), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/noconnection'] = { ImageRectOffset = Vector2.new(0, 444), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/noconnection_large'] = { ImageRectOffset = Vector2.new(247, 352), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/status/oof_xlarge'] = { ImageRectOffset = Vector2.new(97, 381), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_1x_1' }, + ['icons/status/pending_small'] = { ImageRectOffset = Vector2.new(481, 37), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/player/admin'] = { ImageRectOffset = Vector2.new(436, 488), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/status/player/developer'] = { ImageRectOffset = Vector2.new(493, 451), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/status/player/following'] = { ImageRectOffset = Vector2.new(490, 488), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/status/player/friend'] = { ImageRectOffset = Vector2.new(493, 468), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/status/player/intern'] = { ImageRectOffset = Vector2.new(473, 488), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/status/player/pending'] = { ImageRectOffset = Vector2.new(419, 488), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/status/player/videostar'] = { ImageRectOffset = Vector2.new(456, 488), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/status/premium'] = { ImageRectOffset = Vector2.new(185, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/premium_large'] = { ImageRectOffset = Vector2.new(394, 254), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/status/premium_small'] = { ImageRectOffset = Vector2.new(91, 481), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/private'] = { ImageRectOffset = Vector2.new(74, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/private_small'] = { ImageRectOffset = Vector2.new(54, 481), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/public'] = { ImageRectOffset = Vector2.new(37, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/public_small'] = { ImageRectOffset = Vector2.new(128, 481), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/success'] = { ImageRectOffset = Vector2.new(111, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/success_small'] = { ImageRectOffset = Vector2.new(74, 481), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/unavailable'] = { ImageRectOffset = Vector2.new(148, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/unavailable_small'] = { ImageRectOffset = Vector2.new(481, 54), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/warning'] = { ImageRectOffset = Vector2.new(0, 407), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/warning_small'] = { ImageRectOffset = Vector2.new(37, 481), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['squircles/fill'] = { ImageRectOffset = Vector2.new(479, 0), ImageRectSize = Vector2.new(26, 26), ImageSet = 'img_set_1x_1' }, + ['squircles/hollow'] = { ImageRectOffset = Vector2.new(194, 481), ImageRectSize = Vector2.new(26, 26), ImageSet = 'img_set_1x_1' }, + ['squircles/hollowBold'] = { ImageRectOffset = Vector2.new(31, 478), ImageRectSize = Vector2.new(30, 30), ImageSet = 'img_set_1x_1' }, + ['truncate_arrows/actions_truncationCollapse'] = { ImageRectOffset = Vector2.new(444, 148), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['truncate_arrows/actions_truncationExpand'] = { ImageRectOffset = Vector2.new(442, 0), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, +} end + +local assets_3x = nil +function make_assets_3x() assets_3x = { + ['component_assets/bulletDown_17_stroke_3'] = { ImageRectOffset = Vector2.new(406, 218), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_4' }, + ['component_assets/bulletLeft_17_stroke_3'] = { ImageRectOffset = Vector2.new(458, 218), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_4' }, + ['component_assets/bulletRight_17_stroke_3'] = { ImageRectOffset = Vector2.new(329, 957), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_1' }, + ['component_assets/bulletUp_17_stroke_3'] = { ImageRectOffset = Vector2.new(562, 218), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_4' }, + ['component_assets/bullet_17'] = { ImageRectOffset = Vector2.new(947, 149), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_16'] = { ImageRectOffset = Vector2.new(614, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['component_assets/circle_17'] = { ImageRectOffset = Vector2.new(195, 957), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_17_mask'] = { ImageRectOffset = Vector2.new(510, 218), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_4' }, + ['component_assets/circle_17_stroke_1'] = { ImageRectOffset = Vector2.new(277, 957), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_17_stroke_3'] = { ImageRectOffset = Vector2.new(381, 957), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_21'] = { ImageRectOffset = Vector2.new(67, 957), ImageRectSize = Vector2.new(63, 63), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_21_stroke_1'] = { ImageRectOffset = Vector2.new(131, 957), ImageRectSize = Vector2.new(63, 63), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_22_stroke_3'] = { ImageRectOffset = Vector2.new(0, 957), ImageRectSize = Vector2.new(66, 66), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_24_stroke_1'] = { ImageRectOffset = Vector2.new(948, 942), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_3x_2' }, + ['component_assets/circle_25'] = { ImageRectOffset = Vector2.new(947, 0), ImageRectSize = Vector2.new(75, 75), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_26_stroke_3'] = { ImageRectOffset = Vector2.new(631, 939), ImageRectSize = Vector2.new(78, 78), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_28_padding_10'] = { ImageRectOffset = Vector2.new(0, 867), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['component_assets/circle_29'] = { ImageRectOffset = Vector2.new(920, 299), ImageRectSize = Vector2.new(87, 87), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_29_mask'] = { ImageRectOffset = Vector2.new(920, 389), ImageRectSize = Vector2.new(87, 87), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_29_stroke_1'] = { ImageRectOffset = Vector2.new(920, 477), ImageRectSize = Vector2.new(87, 87), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_30_stroke_3'] = { ImageRectOffset = Vector2.new(631, 848), ImageRectSize = Vector2.new(90, 90), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_36'] = { ImageRectOffset = Vector2.new(145, 867), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_2' }, + ['component_assets/circle_36_stroke_1'] = { ImageRectOffset = Vector2.new(578, 145), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_2' }, + ['component_assets/circle_42_stroke_3'] = { ImageRectOffset = Vector2.new(887, 718), ImageRectSize = Vector2.new(126, 126), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_49'] = { ImageRectOffset = Vector2.new(739, 866), ImageRectSize = Vector2.new(147, 147), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_49_mask'] = { ImageRectOffset = Vector2.new(739, 718), ImageRectSize = Vector2.new(147, 147), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_49_stroke_1'] = { ImageRectOffset = Vector2.new(739, 570), ImageRectSize = Vector2.new(147, 147), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_52_stroke_3'] = { ImageRectOffset = Vector2.new(470, 848), ImageRectSize = Vector2.new(156, 156), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_69_stroke_3'] = { ImageRectOffset = Vector2.new(739, 0), ImageRectSize = Vector2.new(207, 207), ImageSet = 'img_set_3x_1' }, + ['component_assets/dropshadow_25'] = { ImageRectOffset = Vector2.new(887, 866), ImageRectSize = Vector2.new(111, 111), ImageSet = 'img_set_3x_1' }, + ['component_assets/dropshadow_28'] = { ImageRectOffset = Vector2.new(578, 0), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['component_assets/dropshadow_chatOff'] = { ImageRectOffset = Vector2.new(724, 869), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_2' }, + ['component_assets/dropshadow_chatOn'] = { ImageRectOffset = Vector2.new(723, 145), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_2' }, + ['component_assets/dropshadow_more'] = { ImageRectOffset = Vector2.new(868, 145), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_2' }, + ['component_assets/halfcircleLeft_17'] = { ImageRectOffset = Vector2.new(247, 957), ImageRectSize = Vector2.new(24, 51), ImageSet = 'img_set_3x_1' }, + ['component_assets/halfcircleRight_17'] = { ImageRectOffset = Vector2.new(433, 957), ImageRectSize = Vector2.new(24, 51), ImageSet = 'img_set_3x_1' }, + ['component_assets/square_7_stroke_3'] = { ImageRectOffset = Vector2.new(710, 988), ImageRectSize = Vector2.new(21, 21), ImageSet = 'img_set_3x_1' }, + ['component_assets/triangleDown_16'] = { ImageRectOffset = Vector2.new(887, 978), ImageRectSize = Vector2.new(48, 24), ImageSet = 'img_set_3x_1' }, + ['component_assets/triangleLeft_16'] = { ImageRectOffset = Vector2.new(710, 939), ImageRectSize = Vector2.new(24, 48), ImageSet = 'img_set_3x_1' }, + ['component_assets/triangleRight_16'] = { ImageRectOffset = Vector2.new(999, 149), ImageRectSize = Vector2.new(24, 48), ImageSet = 'img_set_3x_1' }, + ['component_assets/triangleUp_16'] = { ImageRectOffset = Vector2.new(936, 978), ImageRectSize = Vector2.new(48, 24), ImageSet = 'img_set_3x_1' }, + ['component_assets/user_60_mask'] = { ImageRectOffset = Vector2.new(739, 389), ImageRectSize = Vector2.new(180, 180), ImageSet = 'img_set_3x_1' }, + ['component_assets/vignette_246'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(738, 738), ImageSet = 'img_set_3x_1' }, + ['gradient/gradient_0_100'] = { ImageRectOffset = Vector2.new(627, 848), ImageRectSize = Vector2.new(3, 120), ImageSet = 'img_set_3x_1' }, + ['icons/actions/accept'] = { ImageRectOffset = Vector2.new(0, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/block'] = { ImageRectOffset = Vector2.new(0, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/actions/calendar'] = { ImageRectOffset = Vector2.new(327, 738), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/actions/compose'] = { ImageRectOffset = Vector2.new(327, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/cycleLeft'] = { ImageRectOffset = Vector2.new(218, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/cycleRight'] = { ImageRectOffset = Vector2.new(109, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/edit/add'] = { ImageRectOffset = Vector2.new(109, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/edit/clear'] = { ImageRectOffset = Vector2.new(109, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/edit/clear_small'] = { ImageRectOffset = Vector2.new(327, 836), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/actions/edit/copy'] = { ImageRectOffset = Vector2.new(109, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/edit/delete'] = { ImageRectOffset = Vector2.new(763, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/edit/edit'] = { ImageRectOffset = Vector2.new(109, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/edit/remove'] = { ImageRectOffset = Vector2.new(872, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/favoriteOff'] = { ImageRectOffset = Vector2.new(654, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/favoriteOn'] = { ImageRectOffset = Vector2.new(327, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/feedback'] = { ImageRectOffset = Vector2.new(0, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/filter'] = { ImageRectOffset = Vector2.new(436, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/friends/friendAdd'] = { ImageRectOffset = Vector2.new(0, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/friends/friendInvite'] = { ImageRectOffset = Vector2.new(0, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/friends/friendRemove'] = { ImageRectOffset = Vector2.new(0, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/friends/friendsplaying'] = { ImageRectOffset = Vector2.new(0, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/info'] = { ImageRectOffset = Vector2.new(109, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/info_small'] = { ImageRectOffset = Vector2.new(327, 689), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/actions/previewExpand'] = { ImageRectOffset = Vector2.new(0, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/previewShrink'] = { ImageRectOffset = Vector2.new(218, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/randomize'] = { ImageRectOffset = Vector2.new(436, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/reject'] = { ImageRectOffset = Vector2.new(109, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/respawn'] = { ImageRectOffset = Vector2.new(109, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/selectOn'] = { ImageRectOffset = Vector2.new(545, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/selectOn_small'] = { ImageRectOffset = Vector2.new(327, 885), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/actions/send'] = { ImageRectOffset = Vector2.new(218, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/share'] = { ImageRectOffset = Vector2.new(109, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/truncationCollapse'] = { ImageRectOffset = Vector2.new(654, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/truncationExpand'] = { ImageRectOffset = Vector2.new(109, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/viewOff'] = { ImageRectOffset = Vector2.new(327, 934), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/actions/viewOn'] = { ImageRectOffset = Vector2.new(327, 787), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/actions/vote/voteDownOff'] = { ImageRectOffset = Vector2.new(763, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/vote/voteDownOn'] = { ImageRectOffset = Vector2.new(872, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/vote/voteUpOff'] = { ImageRectOffset = Vector2.new(218, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/vote/voteUpOn'] = { ImageRectOffset = Vector2.new(218, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/zoomIn'] = { ImageRectOffset = Vector2.new(545, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/zoomOut'] = { ImageRectOffset = Vector2.new(0, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/common/goldrobux'] = { ImageRectOffset = Vector2.new(436, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/common/goldrobux_small'] = { ImageRectOffset = Vector2.new(327, 640), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/common/more'] = { ImageRectOffset = Vector2.new(436, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/common/notificationOff'] = { ImageRectOffset = Vector2.new(872, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/common/notificationOn'] = { ImageRectOffset = Vector2.new(436, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/common/play'] = { ImageRectOffset = Vector2.new(545, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/common/refresh'] = { ImageRectOffset = Vector2.new(763, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/common/refresh_small'] = { ImageRectOffset = Vector2.new(145, 976), ImageRectSize = Vector2.new(42, 45), ImageSet = 'img_set_3x_2' }, + ['icons/common/robux'] = { ImageRectOffset = Vector2.new(545, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/common/robux_small'] = { ImageRectOffset = Vector2.new(327, 542), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/common/search'] = { ImageRectOffset = Vector2.new(654, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/common/search_small'] = { ImageRectOffset = Vector2.new(327, 591), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/common/settings'] = { ImageRectOffset = Vector2.new(436, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/common/user'] = { ImageRectOffset = Vector2.new(436, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/common/user_60'] = { ImageRectOffset = Vector2.new(739, 208), ImageRectSize = Vector2.new(180, 180), ImageSet = 'img_set_3x_1' }, + ['icons/controls/close-ingame'] = { ImageRectOffset = Vector2.new(545, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/controls/controls'] = { ImageRectOffset = Vector2.new(545, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/controls/emoteOff'] = { ImageRectOffset = Vector2.new(872, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/emoteOn'] = { ImageRectOffset = Vector2.new(654, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/controls/keys/arrowDown'] = { ImageRectOffset = Vector2.new(327, 444), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/arrowLeft'] = { ImageRectOffset = Vector2.new(327, 346), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/arrowRight'] = { ImageRectOffset = Vector2.new(327, 395), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/arrowUp'] = { ImageRectOffset = Vector2.new(327, 493), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/dpadDown'] = { ImageRectOffset = Vector2.new(654, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/controls/keys/dpadLeft'] = { ImageRectOffset = Vector2.new(654, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/controls/keys/dpadRight'] = { ImageRectOffset = Vector2.new(218, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/dpadUp'] = { ImageRectOffset = Vector2.new(218, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/key_single'] = { ImageRectOffset = Vector2.new(218, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/key_wide'] = { ImageRectOffset = Vector2.new(277, 848), ImageRectSize = Vector2.new(192, 108), ImageSet = 'img_set_3x_1' }, + ['icons/controls/keys/key_xwide'] = { ImageRectOffset = Vector2.new(0, 848), ImageRectSize = Vector2.new(276, 108), ImageSet = 'img_set_3x_1' }, + ['icons/controls/keys/xboxA'] = { ImageRectOffset = Vector2.new(872, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxB'] = { ImageRectOffset = Vector2.new(218, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxLS'] = { ImageRectOffset = Vector2.new(218, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxLSDirectional'] = { ImageRectOffset = Vector2.new(654, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/controls/keys/xboxLSHorizontal'] = { ImageRectOffset = Vector2.new(763, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/controls/keys/xboxLSVertical'] = { ImageRectOffset = Vector2.new(872, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/controls/keys/xboxLT'] = { ImageRectOffset = Vector2.new(763, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/controls/keys/xboxRB'] = { ImageRectOffset = Vector2.new(872, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/controls/keys/xboxRS'] = { ImageRectOffset = Vector2.new(763, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxRSDirectional'] = { ImageRectOffset = Vector2.new(654, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxRSHorizontal'] = { ImageRectOffset = Vector2.new(872, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/controls/keys/xboxRSVertical'] = { ImageRectOffset = Vector2.new(545, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxRT'] = { ImageRectOffset = Vector2.new(763, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/controls/keys/xboxView'] = { ImageRectOffset = Vector2.new(218, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxX'] = { ImageRectOffset = Vector2.new(872, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/controls/keys/xboxY'] = { ImageRectOffset = Vector2.new(218, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxmenu'] = { ImageRectOffset = Vector2.new(763, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/controls/leaderboardOff'] = { ImageRectOffset = Vector2.new(545, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/controls/leaderboardOn'] = { ImageRectOffset = Vector2.new(109, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/mouse/clickLeft'] = { ImageRectOffset = Vector2.new(436, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/mouse/clickRight'] = { ImageRectOffset = Vector2.new(327, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/mouse/scroll'] = { ImageRectOffset = Vector2.new(218, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/players'] = { ImageRectOffset = Vector2.new(327, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/screenrecord'] = { ImageRectOffset = Vector2.new(436, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/screenshot'] = { ImageRectOffset = Vector2.new(109, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/topmenu-shadow'] = { ImageRectOffset = Vector2.new(724, 289), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/controls/vehicle/backward'] = { ImageRectOffset = Vector2.new(109, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/vehicle/driver'] = { ImageRectOffset = Vector2.new(109, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/vehicle/exit'] = { ImageRectOffset = Vector2.new(109, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/vehicle/flip'] = { ImageRectOffset = Vector2.new(109, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/vehicle/forward'] = { ImageRectOffset = Vector2.new(109, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/vehicle/passenger'] = { ImageRectOffset = Vector2.new(109, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/weapon/fire'] = { ImageRectOffset = Vector2.new(763, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/weapon/scopeOff'] = { ImageRectOffset = Vector2.new(654, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/weapon/scopeOn'] = { ImageRectOffset = Vector2.new(545, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/graphic/error_xlarge'] = { ImageRectOffset = Vector2.new(0, 578), ImageRectSize = Vector2.new(288, 288), ImageSet = 'img_set_3x_2' }, + ['icons/graphic/loadingspinner'] = { ImageRectOffset = Vector2.new(579, 289), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/graphic/premium_large'] = { ImageRectOffset = Vector2.new(434, 289), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/graphic/premium_xlarge'] = { ImageRectOffset = Vector2.new(289, 0), ImageRectSize = Vector2.new(288, 288), ImageSet = 'img_set_3x_2' }, + ['icons/graphic/success_xlarge'] = { ImageRectOffset = Vector2.new(0, 289), ImageRectSize = Vector2.new(288, 288), ImageSet = 'img_set_3x_2' }, + ['icons/imageUnavailable'] = { ImageRectOffset = Vector2.new(887, 570), ImageRectSize = Vector2.new(132, 132), ImageSet = 'img_set_3x_1' }, + ['icons/logo/block'] = { ImageRectOffset = Vector2.new(872, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/logo/letterform'] = { ImageRectOffset = Vector2.new(0, 739), ImageRectSize = Vector2.new(621, 108), ImageSet = 'img_set_3x_1' }, + ['icons/menu/about_large'] = { ImageRectOffset = Vector2.new(579, 434), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/menu/avatar_off'] = { ImageRectOffset = Vector2.new(545, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/avatar_on'] = { ImageRectOffset = Vector2.new(218, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/blog'] = { ImageRectOffset = Vector2.new(436, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/blog_large'] = { ImageRectOffset = Vector2.new(434, 434), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/menu/chat_off'] = { ImageRectOffset = Vector2.new(654, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/chat_on'] = { ImageRectOffset = Vector2.new(872, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/create_large'] = { ImageRectOffset = Vector2.new(434, 724), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/menu/customize'] = { ImageRectOffset = Vector2.new(327, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/customize_large'] = { ImageRectOffset = Vector2.new(579, 579), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/menu/events_large'] = { ImageRectOffset = Vector2.new(724, 724), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/menu/feed'] = { ImageRectOffset = Vector2.new(327, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/feed_large'] = { ImageRectOffset = Vector2.new(869, 289), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/menu/friends'] = { ImageRectOffset = Vector2.new(218, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/friends_large'] = { ImageRectOffset = Vector2.new(434, 869), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/menu/games_off'] = { ImageRectOffset = Vector2.new(545, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/games_on'] = { ImageRectOffset = Vector2.new(436, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/giftcard'] = { ImageRectOffset = Vector2.new(218, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/groups'] = { ImageRectOffset = Vector2.new(218, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/groups_large'] = { ImageRectOffset = Vector2.new(869, 579), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/menu/help_large'] = { ImageRectOffset = Vector2.new(579, 724), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/menu/home_off'] = { ImageRectOffset = Vector2.new(327, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/home_on'] = { ImageRectOffset = Vector2.new(654, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/inventory'] = { ImageRectOffset = Vector2.new(327, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/inventoryOff'] = { ImageRectOffset = Vector2.new(763, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/inventoryOn'] = { ImageRectOffset = Vector2.new(327, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/inventory_large'] = { ImageRectOffset = Vector2.new(869, 434), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/menu/messages_large'] = { ImageRectOffset = Vector2.new(579, 869), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/menu/more_off'] = { ImageRectOffset = Vector2.new(436, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/more_on'] = { ImageRectOffset = Vector2.new(327, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/profile'] = { ImageRectOffset = Vector2.new(763, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/scanqr_large'] = { ImageRectOffset = Vector2.new(434, 579), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/menu/settings_large'] = { ImageRectOffset = Vector2.new(724, 579), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/menu/shop'] = { ImageRectOffset = Vector2.new(327, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/menu/shop_large'] = { ImageRectOffset = Vector2.new(724, 434), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/menu/trade'] = { ImageRectOffset = Vector2.new(327, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/navigation/close'] = { ImageRectOffset = Vector2.new(0, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/navigation/pushBack'] = { ImageRectOffset = Vector2.new(0, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/navigation/pushRight'] = { ImageRectOffset = Vector2.new(869, 724), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_2' }, + ['icons/navigation/pushRight_small'] = { ImageRectOffset = Vector2.new(663, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/navigation/swipe'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/navigation/swipeDown'] = { ImageRectOffset = Vector2.new(869, 833), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_2' }, + ['icons/navigation/swipeUp'] = { ImageRectOffset = Vector2.new(0, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/placeholder/placeholderOff'] = { ImageRectOffset = Vector2.new(622, 739), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_1' }, + ['icons/placeholder/placeholderOn'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/placeholder/placeholderOn_small'] = { ImageRectOffset = Vector2.new(376, 346), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/alert'] = { ImageRectOffset = Vector2.new(0, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/alert_small'] = { ImageRectOffset = Vector2.new(810, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/error_large'] = { ImageRectOffset = Vector2.new(289, 289), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/status/games/people-playing_large'] = { ImageRectOffset = Vector2.new(289, 579), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/status/games/people-playing_small'] = { ImageRectOffset = Vector2.new(425, 297), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/games/rating_large'] = { ImageRectOffset = Vector2.new(289, 724), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/status/games/rating_small'] = { ImageRectOffset = Vector2.new(523, 297), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/games/sessions_large'] = { ImageRectOffset = Vector2.new(289, 434), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/status/games/sessions_small'] = { ImageRectOffset = Vector2.new(474, 297), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/imageunavailable'] = { ImageRectOffset = Vector2.new(289, 869), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/status/imageunavailable_small'] = { ImageRectOffset = Vector2.new(947, 76), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_3x_1' }, + ['icons/status/item/bundle'] = { ImageRectOffset = Vector2.new(859, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/item/limited'] = { ImageRectOffset = Vector2.new(957, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/item/owned'] = { ImageRectOffset = Vector2.new(908, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/noconnection'] = { ImageRectOffset = Vector2.new(0, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/noconnection_large'] = { ImageRectOffset = Vector2.new(868, 0), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/status/oof_xlarge'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(288, 288), ImageSet = 'img_set_3x_2' }, + ['icons/status/pending_small'] = { ImageRectOffset = Vector2.new(376, 297), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/player/admin'] = { ImageRectOffset = Vector2.new(768, 297), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/player/developer'] = { ImageRectOffset = Vector2.new(670, 297), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/player/following'] = { ImageRectOffset = Vector2.new(915, 297), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/player/friend'] = { ImageRectOffset = Vector2.new(719, 297), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/player/intern'] = { ImageRectOffset = Vector2.new(817, 297), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/player/pending'] = { ImageRectOffset = Vector2.new(866, 297), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/player/videostar'] = { ImageRectOffset = Vector2.new(621, 297), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/premium'] = { ImageRectOffset = Vector2.new(109, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/premium_large'] = { ImageRectOffset = Vector2.new(723, 0), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/status/premium_small'] = { ImageRectOffset = Vector2.new(761, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/private'] = { ImageRectOffset = Vector2.new(0, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/private_small'] = { ImageRectOffset = Vector2.new(964, 297), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/public'] = { ImageRectOffset = Vector2.new(218, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/public_small'] = { ImageRectOffset = Vector2.new(572, 297), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/success'] = { ImageRectOffset = Vector2.new(0, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/success_small'] = { ImageRectOffset = Vector2.new(712, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/unavailable'] = { ImageRectOffset = Vector2.new(0, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/unavailable_small'] = { ImageRectOffset = Vector2.new(327, 297), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['icons/status/warning'] = { ImageRectOffset = Vector2.new(0, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/warning_small'] = { ImageRectOffset = Vector2.new(810, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_4' }, + ['squircles/fill'] = { ImageRectOffset = Vector2.new(869, 942), ImageRectSize = Vector2.new(78, 78), ImageSet = 'img_set_3x_2' }, + ['squircles/hollow'] = { ImageRectOffset = Vector2.new(327, 218), ImageRectSize = Vector2.new(78, 78), ImageSet = 'img_set_3x_4' }, + ['squircles/hollowBold'] = { ImageRectOffset = Vector2.new(920, 208), ImageRectSize = Vector2.new(90, 90), ImageSet = 'img_set_3x_1' }, + ['truncate_arrows/actions_truncationCollapse'] = { ImageRectOffset = Vector2.new(654, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['truncate_arrows/actions_truncationExpand'] = { ImageRectOffset = Vector2.new(109, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, +} end + +local assets_2x = nil +function make_assets_2x() assets_2x = { + ['component_assets/bulletDown_17_stroke_3'] = { ImageRectOffset = Vector2.new(236, 461), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_3' }, + ['component_assets/bulletLeft_17_stroke_3'] = { ImageRectOffset = Vector2.new(271, 461), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_3' }, + ['component_assets/bulletRight_17_stroke_3'] = { ImageRectOffset = Vector2.new(99, 311), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_3' }, + ['component_assets/bulletUp_17_stroke_3'] = { ImageRectOffset = Vector2.new(99, 445), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_3' }, + ['component_assets/bullet_17'] = { ImageRectOffset = Vector2.new(99, 212), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_16'] = { ImageRectOffset = Vector2.new(159, 459), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_2' }, + ['component_assets/circle_17'] = { ImageRectOffset = Vector2.new(99, 346), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_17_mask'] = { ImageRectOffset = Vector2.new(182, 461), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_17_stroke_1'] = { ImageRectOffset = Vector2.new(99, 410), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_17_stroke_3'] = { ImageRectOffset = Vector2.new(99, 247), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_21'] = { ImageRectOffset = Vector2.new(139, 461), ImageRectSize = Vector2.new(42, 42), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_21_stroke_1'] = { ImageRectOffset = Vector2.new(342, 459), ImageRectSize = Vector2.new(42, 42), ImageSet = 'img_set_2x_2' }, + ['component_assets/circle_22_stroke_3'] = { ImageRectOffset = Vector2.new(460, 0), ImageRectSize = Vector2.new(44, 44), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_24_stroke_1'] = { ImageRectOffset = Vector2.new(244, 459), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_2x_2' }, + ['component_assets/circle_25'] = { ImageRectOffset = Vector2.new(193, 459), ImageRectSize = Vector2.new(50, 50), ImageSet = 'img_set_2x_2' }, + ['component_assets/circle_26_stroke_3'] = { ImageRectOffset = Vector2.new(106, 459), ImageRectSize = Vector2.new(52, 52), ImageSet = 'img_set_2x_2' }, + ['component_assets/circle_28_padding_10'] = { ImageRectOffset = Vector2.new(0, 97), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['component_assets/circle_29'] = { ImageRectOffset = Vector2.new(73, 0), ImageRectSize = Vector2.new(58, 58), ImageSet = 'img_set_2x_7' }, + ['component_assets/circle_29_mask'] = { ImageRectOffset = Vector2.new(191, 0), ImageRectSize = Vector2.new(58, 58), ImageSet = 'img_set_2x_7' }, + ['component_assets/circle_29_stroke_1'] = { ImageRectOffset = Vector2.new(132, 0), ImageRectSize = Vector2.new(58, 58), ImageSet = 'img_set_2x_7' }, + ['component_assets/circle_30_stroke_3'] = { ImageRectOffset = Vector2.new(447, 194), ImageRectSize = Vector2.new(60, 60), ImageSet = 'img_set_2x_2' }, + ['component_assets/circle_36'] = { ImageRectOffset = Vector2.new(194, 194), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['component_assets/circle_36_stroke_1'] = { ImageRectOffset = Vector2.new(415, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_2' }, + ['component_assets/circle_42_stroke_3'] = { ImageRectOffset = Vector2.new(380, 97), ImageRectSize = Vector2.new(84, 84), ImageSet = 'img_set_2x_4' }, + ['component_assets/circle_49'] = { ImageRectOffset = Vector2.new(0, 410), ImageRectSize = Vector2.new(98, 98), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_49_mask'] = { ImageRectOffset = Vector2.new(0, 311), ImageRectSize = Vector2.new(98, 98), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_49_stroke_1'] = { ImageRectOffset = Vector2.new(0, 212), ImageRectSize = Vector2.new(98, 98), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_52_stroke_3'] = { ImageRectOffset = Vector2.new(386, 387), ImageRectSize = Vector2.new(104, 104), ImageSet = 'img_set_2x_2' }, + ['component_assets/circle_69_stroke_3'] = { ImageRectOffset = Vector2.new(0, 73), ImageRectSize = Vector2.new(138, 138), ImageSet = 'img_set_2x_3' }, + ['component_assets/dropshadow_25'] = { ImageRectOffset = Vector2.new(430, 73), ImageRectSize = Vector2.new(74, 74), ImageSet = 'img_set_2x_3' }, + ['component_assets/dropshadow_28'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['component_assets/dropshadow_chatOff'] = { ImageRectOffset = Vector2.new(194, 340), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['component_assets/dropshadow_chatOn'] = { ImageRectOffset = Vector2.new(194, 267), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['component_assets/dropshadow_more'] = { ImageRectOffset = Vector2.new(194, 413), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['component_assets/halfcircleLeft_17'] = { ImageRectOffset = Vector2.new(488, 35), ImageRectSize = Vector2.new(16, 34), ImageSet = 'img_set_2x_2' }, + ['component_assets/halfcircleRight_17'] = { ImageRectOffset = Vector2.new(488, 0), ImageRectSize = Vector2.new(16, 34), ImageSet = 'img_set_2x_2' }, + ['component_assets/square_7_stroke_3'] = { ImageRectOffset = Vector2.new(66, 493), ImageRectSize = Vector2.new(14, 14), ImageSet = 'img_set_2x_1' }, + ['component_assets/triangleDown_16'] = { ImageRectOffset = Vector2.new(0, 493), ImageRectSize = Vector2.new(32, 16), ImageSet = 'img_set_2x_1' }, + ['component_assets/triangleLeft_16'] = { ImageRectOffset = Vector2.new(491, 420), ImageRectSize = Vector2.new(16, 32), ImageSet = 'img_set_2x_2' }, + ['component_assets/triangleRight_16'] = { ImageRectOffset = Vector2.new(491, 387), ImageRectSize = Vector2.new(16, 32), ImageSet = 'img_set_2x_2' }, + ['component_assets/triangleUp_16'] = { ImageRectOffset = Vector2.new(33, 493), ImageRectSize = Vector2.new(32, 16), ImageSet = 'img_set_2x_1' }, + ['component_assets/user_60_mask'] = { ImageRectOffset = Vector2.new(386, 73), ImageRectSize = Vector2.new(120, 120), ImageSet = 'img_set_2x_2' }, + ['component_assets/vignette_246'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(492, 492), ImageSet = 'img_set_2x_1' }, + ['gradient/gradient_0_100'] = { ImageRectOffset = Vector2.new(493, 0), ImageRectSize = Vector2.new(2, 80), ImageSet = 'img_set_2x_1' }, + ['icons/actions/accept'] = { ImageRectOffset = Vector2.new(73, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/block'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/actions/calendar'] = { ImageRectOffset = Vector2.new(382, 0), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/actions/compose'] = { ImageRectOffset = Vector2.new(438, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/cycleLeft'] = { ImageRectOffset = Vector2.new(146, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/cycleRight'] = { ImageRectOffset = Vector2.new(73, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/edit/add'] = { ImageRectOffset = Vector2.new(365, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/edit/clear'] = { ImageRectOffset = Vector2.new(73, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/edit/clear_small'] = { ImageRectOffset = Vector2.new(283, 0), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/actions/edit/copy'] = { ImageRectOffset = Vector2.new(219, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/edit/delete'] = { ImageRectOffset = Vector2.new(146, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/edit/edit'] = { ImageRectOffset = Vector2.new(292, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/edit/remove'] = { ImageRectOffset = Vector2.new(73, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/favoriteOff'] = { ImageRectOffset = Vector2.new(0, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/favoriteOn'] = { ImageRectOffset = Vector2.new(0, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/feedback'] = { ImageRectOffset = Vector2.new(430, 170), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_3' }, + ['icons/actions/filter'] = { ImageRectOffset = Vector2.new(365, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/friends/friendAdd'] = { ImageRectOffset = Vector2.new(0, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/friends/friendInvite'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/friends/friendRemove'] = { ImageRectOffset = Vector2.new(430, 364), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_3' }, + ['icons/actions/friends/friendsplaying'] = { ImageRectOffset = Vector2.new(430, 437), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_3' }, + ['icons/actions/info'] = { ImageRectOffset = Vector2.new(73, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/info_small'] = { ImageRectOffset = Vector2.new(349, 0), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/actions/previewExpand'] = { ImageRectOffset = Vector2.new(430, 267), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_3' }, + ['icons/actions/previewShrink'] = { ImageRectOffset = Vector2.new(438, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/randomize'] = { ImageRectOffset = Vector2.new(0, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/reject'] = { ImageRectOffset = Vector2.new(73, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/respawn'] = { ImageRectOffset = Vector2.new(146, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/selectOn'] = { ImageRectOffset = Vector2.new(292, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/selectOn_small'] = { ImageRectOffset = Vector2.new(366, 461), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_3' }, + ['icons/actions/send'] = { ImageRectOffset = Vector2.new(146, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/share'] = { ImageRectOffset = Vector2.new(73, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/truncationCollapse'] = { ImageRectOffset = Vector2.new(219, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/truncationExpand'] = { ImageRectOffset = Vector2.new(73, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/viewOff'] = { ImageRectOffset = Vector2.new(250, 0), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/actions/viewOn'] = { ImageRectOffset = Vector2.new(316, 0), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/actions/vote/voteDownOff'] = { ImageRectOffset = Vector2.new(146, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/vote/voteDownOn'] = { ImageRectOffset = Vector2.new(146, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/vote/voteUpOff'] = { ImageRectOffset = Vector2.new(146, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/vote/voteUpOn'] = { ImageRectOffset = Vector2.new(219, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/zoomIn'] = { ImageRectOffset = Vector2.new(0, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/zoomOut'] = { ImageRectOffset = Vector2.new(0, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/common/goldrobux'] = { ImageRectOffset = Vector2.new(0, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/common/goldrobux_small'] = { ImageRectOffset = Vector2.new(415, 0), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/common/more'] = { ImageRectOffset = Vector2.new(0, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/common/notificationOff'] = { ImageRectOffset = Vector2.new(365, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/common/notificationOn'] = { ImageRectOffset = Vector2.new(0, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/common/play'] = { ImageRectOffset = Vector2.new(438, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/common/refresh'] = { ImageRectOffset = Vector2.new(365, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/common/refresh_small'] = { ImageRectOffset = Vector2.new(99, 480), ImageRectSize = Vector2.new(28, 30), ImageSet = 'img_set_2x_3' }, + ['icons/common/robux'] = { ImageRectOffset = Vector2.new(438, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/common/robux_small'] = { ImageRectOffset = Vector2.new(73, 59), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/common/search'] = { ImageRectOffset = Vector2.new(438, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/common/search_small'] = { ImageRectOffset = Vector2.new(448, 0), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/common/settings'] = { ImageRectOffset = Vector2.new(0, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/common/user'] = { ImageRectOffset = Vector2.new(0, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/common/user_60'] = { ImageRectOffset = Vector2.new(386, 266), ImageRectSize = Vector2.new(120, 120), ImageSet = 'img_set_2x_2' }, + ['icons/controls/close-ingame'] = { ImageRectOffset = Vector2.new(365, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/controls'] = { ImageRectOffset = Vector2.new(292, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/emoteOff'] = { ImageRectOffset = Vector2.new(0, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/emoteOn'] = { ImageRectOffset = Vector2.new(292, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/arrowDown'] = { ImageRectOffset = Vector2.new(172, 59), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/controls/keys/arrowLeft'] = { ImageRectOffset = Vector2.new(106, 59), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/controls/keys/arrowRight'] = { ImageRectOffset = Vector2.new(139, 59), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/controls/keys/arrowUp'] = { ImageRectOffset = Vector2.new(205, 59), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/controls/keys/dpadDown'] = { ImageRectOffset = Vector2.new(438, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/dpadLeft'] = { ImageRectOffset = Vector2.new(365, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/dpadRight'] = { ImageRectOffset = Vector2.new(146, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/dpadUp'] = { ImageRectOffset = Vector2.new(219, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/key_single'] = { ImageRectOffset = Vector2.new(219, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/key_wide'] = { ImageRectOffset = Vector2.new(185, 0), ImageRectSize = Vector2.new(128, 72), ImageSet = 'img_set_2x_3' }, + ['icons/controls/keys/key_xwide'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(184, 72), ImageSet = 'img_set_2x_3' }, + ['icons/controls/keys/xboxA'] = { ImageRectOffset = Vector2.new(146, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxB'] = { ImageRectOffset = Vector2.new(219, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxLS'] = { ImageRectOffset = Vector2.new(438, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxLSDirectional'] = { ImageRectOffset = Vector2.new(146, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxLSHorizontal'] = { ImageRectOffset = Vector2.new(219, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxLSVertical'] = { ImageRectOffset = Vector2.new(365, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxLT'] = { ImageRectOffset = Vector2.new(292, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxRB'] = { ImageRectOffset = Vector2.new(292, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxRS'] = { ImageRectOffset = Vector2.new(219, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxRSDirectional'] = { ImageRectOffset = Vector2.new(292, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxRSHorizontal'] = { ImageRectOffset = Vector2.new(438, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxRSVertical'] = { ImageRectOffset = Vector2.new(365, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxRT'] = { ImageRectOffset = Vector2.new(219, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxView'] = { ImageRectOffset = Vector2.new(146, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxX'] = { ImageRectOffset = Vector2.new(146, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxY'] = { ImageRectOffset = Vector2.new(73, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxmenu'] = { ImageRectOffset = Vector2.new(146, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/leaderboardOff'] = { ImageRectOffset = Vector2.new(292, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/leaderboardOn'] = { ImageRectOffset = Vector2.new(73, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/mouse/clickLeft'] = { ImageRectOffset = Vector2.new(73, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/mouse/clickRight'] = { ImageRectOffset = Vector2.new(73, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/mouse/scroll'] = { ImageRectOffset = Vector2.new(73, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/players'] = { ImageRectOffset = Vector2.new(438, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/screenrecord'] = { ImageRectOffset = Vector2.new(365, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/screenshot'] = { ImageRectOffset = Vector2.new(73, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/topmenu-shadow'] = { ImageRectOffset = Vector2.new(97, 291), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['icons/controls/vehicle/backward'] = { ImageRectOffset = Vector2.new(146, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/vehicle/driver'] = { ImageRectOffset = Vector2.new(219, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/vehicle/exit'] = { ImageRectOffset = Vector2.new(292, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/vehicle/flip'] = { ImageRectOffset = Vector2.new(0, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/vehicle/forward'] = { ImageRectOffset = Vector2.new(73, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/vehicle/passenger'] = { ImageRectOffset = Vector2.new(0, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/weapon/fire'] = { ImageRectOffset = Vector2.new(0, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/weapon/scopeOff'] = { ImageRectOffset = Vector2.new(0, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/weapon/scopeOn'] = { ImageRectOffset = Vector2.new(0, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/graphic/error_xlarge'] = { ImageRectOffset = Vector2.new(193, 73), ImageRectSize = Vector2.new(192, 192), ImageSet = 'img_set_2x_2' }, + ['icons/graphic/loadingspinner'] = { ImageRectOffset = Vector2.new(97, 194), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['icons/graphic/premium_large'] = { ImageRectOffset = Vector2.new(97, 97), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['icons/graphic/premium_xlarge'] = { ImageRectOffset = Vector2.new(193, 266), ImageRectSize = Vector2.new(192, 192), ImageSet = 'img_set_2x_2' }, + ['icons/graphic/success_xlarge'] = { ImageRectOffset = Vector2.new(0, 266), ImageRectSize = Vector2.new(192, 192), ImageSet = 'img_set_2x_2' }, + ['icons/imageUnavailable'] = { ImageRectOffset = Vector2.new(291, 97), ImageRectSize = Vector2.new(88, 88), ImageSet = 'img_set_2x_4' }, + ['icons/logo/block'] = { ImageRectOffset = Vector2.new(0, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/logo/letterform'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(414, 72), ImageSet = 'img_set_2x_2' }, + ['icons/menu/about_large'] = { ImageRectOffset = Vector2.new(97, 388), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['icons/menu/avatar_off'] = { ImageRectOffset = Vector2.new(438, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/avatar_on'] = { ImageRectOffset = Vector2.new(219, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/blog'] = { ImageRectOffset = Vector2.new(292, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/blog_large'] = { ImageRectOffset = Vector2.new(236, 364), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_3' }, + ['icons/menu/chat_off'] = { ImageRectOffset = Vector2.new(365, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/chat_on'] = { ImageRectOffset = Vector2.new(219, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/create_large'] = { ImageRectOffset = Vector2.new(333, 364), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_3' }, + ['icons/menu/customize'] = { ImageRectOffset = Vector2.new(438, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/customize_large'] = { ImageRectOffset = Vector2.new(236, 170), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_3' }, + ['icons/menu/events_large'] = { ImageRectOffset = Vector2.new(139, 73), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_3' }, + ['icons/menu/feed'] = { ImageRectOffset = Vector2.new(365, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/feed_large'] = { ImageRectOffset = Vector2.new(236, 267), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_3' }, + ['icons/menu/friends'] = { ImageRectOffset = Vector2.new(438, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/friends_large'] = { ImageRectOffset = Vector2.new(194, 97), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['icons/menu/games_off'] = { ImageRectOffset = Vector2.new(438, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/games_on'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/giftcard'] = { ImageRectOffset = Vector2.new(365, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/groups'] = { ImageRectOffset = Vector2.new(219, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/groups_large'] = { ImageRectOffset = Vector2.new(139, 170), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_3' }, + ['icons/menu/help_large'] = { ImageRectOffset = Vector2.new(333, 170), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_3' }, + ['icons/menu/home_off'] = { ImageRectOffset = Vector2.new(292, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/home_on'] = { ImageRectOffset = Vector2.new(438, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/inventory'] = { ImageRectOffset = Vector2.new(219, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/inventoryOff'] = { ImageRectOffset = Vector2.new(292, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/inventoryOn'] = { ImageRectOffset = Vector2.new(219, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/inventory_large'] = { ImageRectOffset = Vector2.new(139, 364), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_3' }, + ['icons/menu/messages_large'] = { ImageRectOffset = Vector2.new(236, 73), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_3' }, + ['icons/menu/more_off'] = { ImageRectOffset = Vector2.new(0, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/more_on'] = { ImageRectOffset = Vector2.new(292, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/profile'] = { ImageRectOffset = Vector2.new(365, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/scanqr_large'] = { ImageRectOffset = Vector2.new(333, 267), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_3' }, + ['icons/menu/settings_large'] = { ImageRectOffset = Vector2.new(333, 73), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_3' }, + ['icons/menu/shop'] = { ImageRectOffset = Vector2.new(365, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/menu/shop_large'] = { ImageRectOffset = Vector2.new(139, 267), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_3' }, + ['icons/menu/trade'] = { ImageRectOffset = Vector2.new(292, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/navigation/close'] = { ImageRectOffset = Vector2.new(267, 267), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['icons/navigation/pushBack'] = { ImageRectOffset = Vector2.new(413, 194), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['icons/navigation/pushRight'] = { ImageRectOffset = Vector2.new(267, 194), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['icons/navigation/pushRight_small'] = { ImageRectOffset = Vector2.new(465, 97), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_4' }, + ['icons/navigation/swipe'] = { ImageRectOffset = Vector2.new(267, 340), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['icons/navigation/swipeDown'] = { ImageRectOffset = Vector2.new(267, 413), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['icons/navigation/swipeUp'] = { ImageRectOffset = Vector2.new(340, 194), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['icons/placeholder/placeholderOff'] = { ImageRectOffset = Vector2.new(314, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_3' }, + ['icons/placeholder/placeholderOn'] = { ImageRectOffset = Vector2.new(387, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_3' }, + ['icons/placeholder/placeholderOn_small'] = { ImageRectOffset = Vector2.new(333, 461), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_3' }, + ['icons/status/alert'] = { ImageRectOffset = Vector2.new(413, 267), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['icons/status/alert_small'] = { ImageRectOffset = Vector2.new(465, 130), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_4' }, + ['icons/status/error_large'] = { ImageRectOffset = Vector2.new(0, 291), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['icons/status/games/people-playing_large'] = { ImageRectOffset = Vector2.new(194, 0), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['icons/status/games/people-playing_small'] = { ImageRectOffset = Vector2.new(73, 92), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/games/rating_large'] = { ImageRectOffset = Vector2.new(388, 0), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['icons/status/games/rating_small'] = { ImageRectOffset = Vector2.new(469, 59), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/games/sessions_large'] = { ImageRectOffset = Vector2.new(291, 0), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['icons/status/games/sessions_small'] = { ImageRectOffset = Vector2.new(73, 125), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/imageunavailable'] = { ImageRectOffset = Vector2.new(97, 0), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['icons/status/imageunavailable_small'] = { ImageRectOffset = Vector2.new(293, 459), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_2x_2' }, + ['icons/status/item/bundle'] = { ImageRectOffset = Vector2.new(73, 290), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/item/limited'] = { ImageRectOffset = Vector2.new(73, 224), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/item/owned'] = { ImageRectOffset = Vector2.new(73, 257), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/noconnection'] = { ImageRectOffset = Vector2.new(340, 267), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['icons/status/noconnection_large'] = { ImageRectOffset = Vector2.new(0, 388), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['icons/status/oof_xlarge'] = { ImageRectOffset = Vector2.new(0, 73), ImageRectSize = Vector2.new(192, 192), ImageSet = 'img_set_2x_2' }, + ['icons/status/pending_small'] = { ImageRectOffset = Vector2.new(73, 356), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/player/admin'] = { ImageRectOffset = Vector2.new(337, 59), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/player/developer'] = { ImageRectOffset = Vector2.new(370, 59), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/player/following'] = { ImageRectOffset = Vector2.new(304, 59), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/player/friend'] = { ImageRectOffset = Vector2.new(403, 59), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/player/intern'] = { ImageRectOffset = Vector2.new(238, 59), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/player/pending'] = { ImageRectOffset = Vector2.new(271, 59), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/player/videostar'] = { ImageRectOffset = Vector2.new(436, 59), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/premium'] = { ImageRectOffset = Vector2.new(413, 413), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['icons/status/premium_large'] = { ImageRectOffset = Vector2.new(0, 194), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['icons/status/premium_small'] = { ImageRectOffset = Vector2.new(73, 422), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/private'] = { ImageRectOffset = Vector2.new(340, 413), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['icons/status/private_small'] = { ImageRectOffset = Vector2.new(73, 191), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/public'] = { ImageRectOffset = Vector2.new(413, 340), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['icons/status/public_small'] = { ImageRectOffset = Vector2.new(73, 158), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/success'] = { ImageRectOffset = Vector2.new(340, 340), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['icons/status/success_small'] = { ImageRectOffset = Vector2.new(73, 389), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/unavailable'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/status/unavailable_small'] = { ImageRectOffset = Vector2.new(73, 323), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_7' }, + ['icons/status/warning'] = { ImageRectOffset = Vector2.new(413, 267), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['icons/status/warning_small'] = { ImageRectOffset = Vector2.new(465, 130), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_4' }, + ['squircles/fill'] = { ImageRectOffset = Vector2.new(0, 459), ImageRectSize = Vector2.new(52, 52), ImageSet = 'img_set_2x_2' }, + ['squircles/hollow'] = { ImageRectOffset = Vector2.new(53, 459), ImageRectSize = Vector2.new(52, 52), ImageSet = 'img_set_2x_2' }, + ['squircles/hollowBold'] = { ImageRectOffset = Vector2.new(386, 194), ImageRectSize = Vector2.new(60, 60), ImageSet = 'img_set_2x_2' }, + ['truncate_arrows/actions_truncationCollapse'] = { ImageRectOffset = Vector2.new(219, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['truncate_arrows/actions_truncationExpand'] = { ImageRectOffset = Vector2.new(73, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, +} end + +return function(scaleType) + if scaleType > 2.5 then + if not assets_3x then make_assets_3x() end + return assets_3x, 3 + elseif scaleType > 1.5 then + if not assets_2x then make_assets_2x() end + return assets_2x, 2 + end + if not assets_1x then make_assets_1x() end + return assets_1x, 1 +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_1x_1.png b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_1x_1.png new file mode 100644 index 0000000..0b1d066 Binary files /dev/null and b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_1x_1.png differ diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_1x_2.png b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_1x_2.png new file mode 100644 index 0000000..f38d048 Binary files /dev/null and b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_1x_2.png differ diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_1.png b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_1.png new file mode 100644 index 0000000..4777dcc Binary files /dev/null and b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_1.png differ diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_2.png b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_2.png new file mode 100644 index 0000000..816f81b Binary files /dev/null and b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_2.png differ diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_3.png b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_3.png new file mode 100644 index 0000000..aef62c0 Binary files /dev/null and b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_3.png differ diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_4.png b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_4.png new file mode 100644 index 0000000..db20882 Binary files /dev/null and b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_4.png differ diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_5.png b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_5.png new file mode 100644 index 0000000..3856be1 Binary files /dev/null and b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_5.png differ diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_6.png b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_6.png new file mode 100644 index 0000000..3a66e22 Binary files /dev/null and b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_6.png differ diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_7.png b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_7.png new file mode 100644 index 0000000..bf7f3b5 Binary files /dev/null and b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_7.png differ diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_1.png b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_1.png new file mode 100644 index 0000000..e5e07ce Binary files /dev/null and b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_1.png differ diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_2.png b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_2.png new file mode 100644 index 0000000..42170a2 Binary files /dev/null and b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_2.png differ diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_3.png b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_3.png new file mode 100644 index 0000000..361fca1 Binary files /dev/null and b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_3.png differ diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_4.png b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_4.png new file mode 100644 index 0000000..d4a9013 Binary files /dev/null and b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_4.png differ diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Images.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Images.lua new file mode 100644 index 0000000..dde8713 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Images.lua @@ -0,0 +1,74 @@ +-- This file just provides a convenient interface to query for images +local GetImageSetData = require(script.Parent.GetImageSetData) + +local GuiService = game:GetService("GuiService") + +-- fallback spritesheet image to use if CorePackages is unavailable +local FALLBACK_IMAGES = { + ["img_set_1x_1"] = "http://www.roblox.com/asset/?id=5585783844", + ["img_set_1x_2"] = "http://www.roblox.com/asset/?id=5585784274", + --["./img_set_1x_3"] = "", +} + +local CorePackages = script:FindFirstAncestor("CorePackages") +local success, scale = pcall(GuiService.GetResolutionScale, GuiService) + +if not success or not CorePackages then + scale = 1 +end + +local sourceData = GetImageSetData(scale) + +local function getPackagePath() + local packageRoot = script.Parent + + if CorePackages == nil then + -- We're not running in CI as a core script, no internal path + return nil + end + + local path = {} + local current = packageRoot + while current ~= nil and current ~= CorePackages do + table.insert(path, 1, current.Name) + current = current.Parent + end + + return "LuaPackages/" .. table.concat(path, "/") +end + +local function getImagePath(packagePath, imageName) + if packagePath == nil then + -- fallback to an uploaded image + return FALLBACK_IMAGES[imageName] + else + return string.format("rbxasset://%s/ImageAtlas/%s.png", packagePath, imageName) + end +end + +local packagePath = getPackagePath() +local Images = { + ImagesResolutionScale = scale, +} + +for key, value in pairs(sourceData) do + assert(typeof(value) == "table") + local imageProps = {} + for imageKey, imageValue in pairs(value) do + if imageKey == "ImageSet" then + imageProps.Image = getImagePath(packagePath, imageValue) + else + imageProps[imageKey] = imageValue + end + end + Images[key] = imageProps +end + +-- Attach a metamethod to guard against typos +setmetatable(Images, { + __index = function(_, key) + error(("%q is not a valid member of Images"):format(tostring(key)), 2) + end, +}) + +return Images diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Images.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Images.spec.lua new file mode 100644 index 0000000..148fb1d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Images.spec.lua @@ -0,0 +1,9 @@ +return function() + local Images = require(script.Parent.Images) + + it("should throw on invalid key", function() + expect(function() + local _ = Images["never a real key"] + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSize.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSize.lua new file mode 100644 index 0000000..ada3fa3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSize.lua @@ -0,0 +1,16 @@ +local ImageSet = script.Parent + +local IconSize = require(ImageSet.Enum.IconSize) + +local IconSizeMap = { + [IconSize.Small] = 16, + [IconSize.Medium] = 36, + [IconSize.Large] = 48, + [IconSize.XLarge] = 96, + [IconSize.XXLarge] = 192, +} + +return function(iconSizeEnum) + assert(IconSize.isEnumValue(iconSizeEnum)) + return IconSizeMap[iconSizeEnum] +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSize.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSize.spec.lua new file mode 100644 index 0000000..70656cd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSize.spec.lua @@ -0,0 +1,33 @@ +local ImageSet = script.Parent +local IconSizeEnum = require(ImageSet.Enum.IconSize) + +return function() + local getIconSize = require(script.Parent.getIconSize) + describe("getIconSize()", function() + it("should return the number 16", function() + local iconSize = getIconSize(IconSizeEnum.Small) + expect(iconSize).to.equal(16) + end) + it("should return the number 36", function() + local iconSize = getIconSize(IconSizeEnum.Medium) + expect(iconSize).to.equal(36) + end) + it("should return the number 48", function() + local iconSize = getIconSize(IconSizeEnum.Large) + expect(iconSize).to.equal(48) + end) + it("should return the number 96", function() + local iconSize = getIconSize(IconSizeEnum.XLarge) + expect(iconSize).to.equal(96) + end) + it("should return the number 192", function() + local iconSize = getIconSize(IconSizeEnum.XXLarge) + expect(iconSize).to.equal(192) + end) + it("should error", function() + expect(function() + return getIconSize("ASD") + end).to.throw() + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSizeUDim2.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSizeUDim2.lua new file mode 100644 index 0000000..3489a0b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSizeUDim2.lua @@ -0,0 +1,10 @@ +local ImageSet = script.Parent + +local IconSize = require(ImageSet.Enum.IconSize) +local getIconSize = require(ImageSet.getIconSize) + +return function(iconSizeEnum) + assert(IconSize.isEnumValue(iconSizeEnum)) + local size = getIconSize(iconSizeEnum) + return UDim2.fromOffset(size, size) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSizeUDim2.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSizeUDim2.spec.lua new file mode 100644 index 0000000..5abf687 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSizeUDim2.spec.lua @@ -0,0 +1,34 @@ +local ImageSet = script.Parent +local IconSizeEnum = require(ImageSet.Enum.IconSize) + +return function() + local getIconSizeUDim2 = require(ImageSet.getIconSizeUDim2) + describe("getIconSizeUDim2()", function() + it("should return a UDim2 of value (0, 16, 0, 16)", function() + local iconSize = getIconSizeUDim2(IconSizeEnum.Small) + expect(iconSize).to.equal(UDim2.fromOffset(16, 16)) + end) + it("should return a UDim2 of value (0, 36, 0, 36)", function() + local iconSize = getIconSizeUDim2(IconSizeEnum.Medium) + expect(iconSize).to.equal(UDim2.fromOffset(36, 36)) + end) + it("should return a UDim2 of value (0, 48, 0, 48)", function() + local iconSize = getIconSizeUDim2(IconSizeEnum.Large) + expect(iconSize).to.equal(UDim2.fromOffset(48, 48)) + end) + it("should return a UDim2 of value (0, 96, 0, 96)", function() + local iconSize = getIconSizeUDim2(IconSizeEnum.XLarge) + expect(iconSize).to.equal(UDim2.fromOffset(96, 96)) + end) + it("should return a UDim2 of value (0, 192, 0, 192)", function() + local iconSize = getIconSizeUDim2(IconSizeEnum.XXLarge) + expect(iconSize).to.equal(UDim2.fromOffset(192, 192)) + end) + + it("should error", function() + expect(function() + return getIconSizeUDim2("ASD") + end).to.throw() + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/Badge.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/Badge.lua new file mode 100644 index 0000000..f47346d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/Badge.lua @@ -0,0 +1,135 @@ +local TextService = game:GetService("TextService") + +local Indicator = script.Parent +local App = Indicator.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local withStyle = require(UIBlox.Core.Style.withStyle) + +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) + +local Images = require(UIBlox.App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local divideTransparency = require(UIBlox.Utility.divideTransparency) + +local BADGE_MIN_WIDTH = 24 +local INNER_PADDING = 2 +local TEXT_PADDING = 5 +local SHADOW_SIZE_OFFSET = 6 + +local MAX_BADGE_VALUE = 99 +local MAX_BADGE_TEXT = "99+" +local MAX_TEXT_LENGTH = 4 + +local ELLIPSES = "..." + +local BACKGROUND_CIRCLE_IMAGE = Images["component_assets/circle_25"] +local INNER_CIRCLE_IMAGE = Images["component_assets/circle_21"] + +local Badge = Roact.PureComponent:extend("Badge") + +Badge.validateProps = t.strictInterface({ + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + + disabled = t.optional(t.boolean), + hasShadow = t.optional(t.boolean), + value = t.union(t.string, t.integer), +}) + +Badge.defaultProps = { + position = UDim2.new(0, 0, 0, 0), + anchorPoint = Vector2.new(0, 0), + + disabled = false, + hasShadow = false, +} + +function Badge:render() + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local font = stylePalette.Font + + local badgeText = tostring(self.props.value) + if t.number(self.props.value) and self.props.value > MAX_BADGE_VALUE then + badgeText = MAX_BADGE_TEXT + elseif t.string(self.props.value) and utf8.len(utf8.nfcnormalize(self.props.value)) > MAX_TEXT_LENGTH then + local byteOffset = utf8.offset(self.props.value, MAX_TEXT_LENGTH) - 1 + badgeText = string.sub(self.props.value, 1, byteOffset) .. ELLIPSES + end + + local baseSize = stylePalette.Font.BaseSize + local fontSize = font.CaptionBody.RelativeSize * baseSize + + local textBounds = TextService:GetTextSize(badgeText, fontSize, font.CaptionBody.Font, Vector2.new(10000, 10000)).X + local badgeWidth = textBounds + (TEXT_PADDING * 2) + (INNER_PADDING * 2) + if badgeWidth < BADGE_MIN_WIDTH then + badgeWidth = BADGE_MIN_WIDTH + end + + return Roact.createElement("Frame", { + Position = self.props.position, + AnchorPoint = self.props.anchorPoint, + BackgroundTransparency = 1, + Size = UDim2.fromOffset(badgeWidth, BADGE_MIN_WIDTH), + }, { + Shadow = self.props.hasShadow and Roact.createElement(ImageSetComponent.Label, { + ZIndex = 1, + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Size = UDim2.new(1, SHADOW_SIZE_OFFSET * 2, 1, SHADOW_SIZE_OFFSET * 2), + + Image = Images["component_assets/dropshadow_25"], + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(18, 18, 19, 19), + }), + + Background = Roact.createElement(ImageSetComponent.Label, { + ZIndex = 2, + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + + ImageColor3 = theme.BackgroundDefault.Color, + ImageTransparency = divideTransparency(theme.BackgroundDefault.Transparency, self.props.disabled and 2 or 1), + Image = BACKGROUND_CIRCLE_IMAGE, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(14, 14, 15, 15), + }), + + Inner = Roact.createElement(ImageSetComponent.Label, { + ZIndex = 3, + BackgroundTransparency = 1, + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.new(1, -(INNER_PADDING * 2), 1, -(INNER_PADDING * 2)), + + ImageColor3 = theme.Badge.Color, + ImageTransparency = divideTransparency(theme.Badge.Transparency, self.props.disabled and 2 or 1), + Image = INNER_CIRCLE_IMAGE, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(12, 12, 13, 13), + }, { + TextLabel = Roact.createElement(GenericTextLabel, { + fontStyle = font.CaptionBody, + colorStyle = theme.BadgeContent, + + BackgroundTransparency = 1, + Text = badgeText, + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.fromScale(1, 1), + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + }), + }), + }) + end) +end + +return Badge \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/Badge.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/Badge.spec.lua new file mode 100644 index 0000000..85450c9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/Badge.spec.lua @@ -0,0 +1,68 @@ +return function() + local Indicator = script.Parent + local App = Indicator.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local Badge = require(script.Parent.Badge) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + it("should mount and unmount with only the required props", function() + local element = mockStyleComponent({ + radioButton = Roact.createElement(Badge, { + value = 60, + }) + }) + local instance = Roact.mount(element, frame, "Badge") + Roact.unmount(instance) + end) + + it("should mount and unmount with all the props", function() + local element = mockStyleComponent({ + radioButton = Roact.createElement(Badge, { + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0, 1), + + disabled = true, + hasShadow = true, + + value = 60, + }) + }) + local instance = Roact.mount(element, frame, "Badge") + Roact.unmount(instance) + end) + + it("should accept string values", function() + local element = mockStyleComponent({ + radioButton = Roact.createElement(Badge, { + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0, 1), + + value = "New", + }) + }) + local instance = Roact.mount(element, frame, "Badge") + Roact.unmount(instance) + end) + + it("should display as 99+ for values above 99", function() + local element = mockStyleComponent({ + radioButton = Roact.createElement(Badge, { + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0, 1), + + value = 100, + }) + }) + local instance = Roact.mount(element, frame, "Badge") + + local textLabel = frame:FindFirstChild("TextLabel", true) + expect(textLabel.Text).to.equal("99+") + + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/EmptyState.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/EmptyState.lua new file mode 100644 index 0000000..4be73be --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/EmptyState.lua @@ -0,0 +1,131 @@ +local Indicator = script.Parent +local App = Indicator.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local validateImage = require(UIBlox.Core.ImageSet.Validator.validateImage) +local withStyle = require(UIBlox.Style.withStyle) +local getPageMargin = require(App.Container.getPageMargin) +local IconSize = require(App.ImageSet.Enum.IconSize) +local getIconSize = require(App.ImageSet.getIconSize) +local Images = require(App.ImageSet.Images) +local SecondaryButton = require(App.Button.SecondaryButton) + +local DEFAULT_ICON = 'icons/status/oof_xlarge' +local DEFAULT_BUTTON_ICON = 'icons/common/refresh' +local ICON_TEXT_PADDING = 12 +local TEXT_BUTTON_PADDING = 24 +local BUTTON_HEIGHT = 48 +local BUTTON_MAX_SIZE = 640 + +local EmptyState = Roact.PureComponent:extend("EmptyState") + +EmptyState.validateProps = t.strictInterface({ + text = t.string, + icon = t.optional(validateImage), + size = t.optional(t.UDim2), + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + buttonIcon = t.optional(validateImage), + onActivated = t.optional(t.callback), +}) + +EmptyState.defaultProps = { + icon = Images[DEFAULT_ICON], + size = UDim2.fromScale(1, 1), + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + buttonIcon = Images[DEFAULT_BUTTON_ICON], +} + +function EmptyState:init() + self:setState({ + absoluteSize = Vector2.new(0, 0), + }) + self.onAbsoluteSizeChange = function(frame) + self:setState({ + absoluteSize = frame.AbsoluteSize, + }) + end +end + +function EmptyState:render() + return withStyle(function(style) + return Roact.createElement("Frame", { + [Roact.Change.AbsoluteSize] = self.onAbsoluteSizeChange, + Size = self.props.size, + Position = self.props.position, + AnchorPoint = self.props.anchorPoint, + BackgroundTransparency = 1, + }, { + Content = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 187), + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Icon = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0), + Size = UDim2.fromOffset(getIconSize(IconSize.XLarge), getIconSize(IconSize.XLarge)), + LayoutOrder = 1, + Image = self.props.icon, + BackgroundTransparency = 1, + ImageColor3 = style.Theme.IconEmphasis.Color, + ImageTransparency = style.Theme.IconEmphasis.Transparency, + }), + iconTextPadding = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.fromOffset(0, ICON_TEXT_PADDING), + BackgroundTransparency = 1, + LayoutOrder = 2, + }), + Text = Roact.createElement(GenericTextLabel, { + Text = self.props.text, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + LayoutOrder = 3, + fontStyle = style.Font.Body, + colorStyle = style.Theme.TextDefault, + }), + textButtonPadding = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.fromOffset(0, TEXT_BUTTON_PADDING), + BackgroundTransparency = 1, + LayoutOrder = 4, + }), + buttonFrame = self.props.onActivated and Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, BUTTON_HEIGHT), + AnchorPoint = Vector2.new(0.5, 0), + BackgroundTransparency = 1, + LayoutOrder = 5, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, getPageMargin(self.state.absoluteSize.X)), + PaddingRight = UDim.new(0, getPageMargin(self.state.absoluteSize.X)), + }), + UISizeConstraint = Roact.createElement("UISizeConstraint", { + MaxSize = Vector2.new(BUTTON_MAX_SIZE, BUTTON_HEIGHT), + }), + Button = Roact.createElement(SecondaryButton, { + size = UDim2.fromScale(1, 1), + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + onActivated = self.props.onActivated, + icon = self.props.buttonIcon + }) + }) + }) + }) + end) +end + +return EmptyState diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/EmptyState.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/EmptyState.spec.lua new file mode 100644 index 0000000..2a73466 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/EmptyState.spec.lua @@ -0,0 +1,39 @@ +return function() + local Indicator = script.Parent + local App = Indicator.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local EmptyState = require(script.Parent.EmptyState) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + local Images = require(UIBlox.App.ImageSet.Images) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + it("should mount and unmount with only the required props", function() + local element = mockStyleComponent({ + emptyState = Roact.createElement(EmptyState, { + text = "No [Items]", + }) + }) + local instance = Roact.mount(element, frame, "EmptyState") + Roact.unmount(instance) + end) + + it("should mount and unmount with all the props", function() + local element = mockStyleComponent({ + emptyState = Roact.createElement(EmptyState, { + text = "No [Items]", + icon = Images['icons/status/oof_xlarge'], + buttonIcon = Images['icons/common/refresh'], + size = UDim2.fromScale(1, 1), + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + onActivated = (function() print("callback") end), + }) + }) + local instance = Roact.mount(element, frame, "EmptyState") + Roact.unmount(instance) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/__stories__/Badge.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/__stories__/Badge.story.lua new file mode 100644 index 0000000..988342c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/__stories__/Badge.story.lua @@ -0,0 +1,183 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local Indicator = script.Parent.Parent +local App = Indicator.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local Badge = require(Indicator.Badge) +local Images = require(App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local DarkTheme = require(Packages.UIBlox.App.Style.Themes.DarkTheme) + +local BadgeStory = Roact.PureComponent:extend("BadgeStory") + +function BadgeStory:init() + self.state = { + badgeValue = "", + } +end + +function BadgeStory:render() + local badgeValue = self.state.badgeValue + if tonumber(badgeValue) then + badgeValue = tonumber(badgeValue) + end + if badgeValue == "" then + badgeValue = 0 + end + + return Roact.createElement("TextButton", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Text = "", + }, { + ScrollingFrame = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, 0, 1, 0), + CanvasSize = UDim2.new(1, 0, 0, 1500), + BackgroundTransparency = 1, + ScrollingDirection = Enum.ScrollingDirection.Y, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }), + + ValueUpdated = Roact.createElement("Frame", { + Size = UDim2.fromOffset(200, 100), + BackgroundTransparency = 1, + }, { + TextBox = Roact.createElement("TextBox", { + Size = UDim2.fromOffset(200, 50), + Text = tostring(self.state.badgeValue), + TextColor3 = Color3.new(0, 0, 0), + PlaceholderColor3 = Color3.new(0, 0, 0), + PlaceholderText = "Enter Badge Value", + + [Roact.Change.Text] = function(rbx) + self:setState({ + badgeValue = rbx.Text, + }) + end, + }) + }), + + BadgeStory = Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 0, 200), + layoutOrder = 2, + title = "Badge", + subTitle = "Indicator.Badge", + }, { + Icon = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Size = UDim2.new(0, 36, 0, 36), + Image = Images["icons/common/notificationOn"], + }, { + Badge = badgeValue ~= 0 and Roact.createElement(Badge, { + position = UDim2.new(0.5, 0, 0.5, 0), + anchorPoint = Vector2.new(0, 1), + + value = badgeValue, + }), + }), + }), + + DisabledBadgeStory = Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 0, 200), + layoutOrder = 3, + title = "Disabled Badge", + subTitle = "Indicator.Badge", + }, { + Icon = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Size = UDim2.new(0, 36, 0, 36), + ImageTransparency = 0.5, + Image = Images["icons/common/notificationOn"], + }, { + Badge = badgeValue ~= 0 and Roact.createElement(Badge, { + position = UDim2.new(0.5, 0, 0.5, 0), + anchorPoint = Vector2.new(0, 1), + + disabled = true, + value = badgeValue, + }), + }), + }), + + BadgeTileStory = Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 0, 300), + layoutOrder = 4, + title = "Badge Tile", + subTitle = "Indicator.Badge", + }, { + Tile = Roact.createElement("Frame", { + Size = UDim2.fromOffset(160, 160), + BackgroundTransparency = DarkTheme.BackgroundUIDefault.Transparency, + BackgroundColor3 = DarkTheme.BackgroundUIDefault.Color, + }, { + Badge = badgeValue ~= 0 and Roact.createElement(Badge, { + position = UDim2.new(1, -10, 0, 10), + anchorPoint = Vector2.new(1, 0), + + value = badgeValue, + }), + }), + }), + + BadgeShadowStory = Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 0, 200), + layoutOrder = 5, + title = "Badge With Shadow", + subTitle = "Indicator.Badge", + }, { + Background = Roact.createElement("Frame", { + BackgroundColor3 = Color3.fromRGB(128, 187, 219), + Size = UDim2.new(0, 200, 1, 0), + }, { + DropShadow = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.new(0, 36, 0, 36), + Image = Images["component_assets/dropshadow_chatOff"], + ZIndex = 1, + }), + + Icon = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.new(0, 36, 0, 36), + Image = Images["icons/menu/chat_off"], + ZIndex = 2, + }, { + Badge = badgeValue ~= 0 and Roact.createElement(Badge, { + position = UDim2.new(0, 24, 0.5, 0), + anchorPoint = Vector2.new(0, 0.5), + + hasShadow = true, + value = badgeValue, + }) + }), + }), + }), + }) + }) +end + +return function(target) + local story = Roact.createElement(StoryView, {}, { + Roact.createElement(BadgeStory) + }) + local handle = Roact.mount(story, target, "BadgeStory") + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Checkbox.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Checkbox.lua new file mode 100644 index 0000000..57dff97 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Checkbox.lua @@ -0,0 +1,89 @@ +local Packages = script.Parent.Parent.Parent.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(Packages.UIBlox.Core.Style.withStyle) +local Images = require(Packages.UIBlox.App.ImageSet.Images) +local InputButton = require(Packages.UIBlox.Core.InputButton.InputButton) + +--TODO: This code is considered Control.Checkbox by design, consider moving this out of InputButton for consistency. + +local Checkbox = Roact.PureComponent:extend("Checkbox") + +local validateProps = t.strictInterface({ + text = t.string, + isSelected = t.optional(t.boolean), + isDisabled = t.optional(t.boolean), + onActivated = t.callback, + size = t.optional(t.UDim2), + layoutOrder = t.optional(t.number), + [Roact.Ref] = t.optional(t.table), +}) + +Checkbox.defaultProps = { + text = "Checkbox Text", + isSelected = false, + isDisabled = false, +} + +local CHECKMARK_SIZE = 14 + +function Checkbox:init() + self.state = { + value = self.props.isSelected + } + + self.onFlip = function() + if self.props.isDisabled then return end + self.props.onActivated(not self.props.isSelected) + end +end + +function Checkbox:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + local image + local imageColor + local fillImage + local fillImageSize + + local transparency = theme.TextDefault.Transparency + local textColor = theme.TextDefault.Color + local fillImageColor = theme.SystemPrimaryContent.Color + + if self.props.isDisabled then + transparency = 0.5 + end + + if self.props.isSelected then + image = Images["squircles/fill"] + imageColor = theme.SystemPrimaryDefault.Color + fillImage = Images["icons/status/success_small"] + fillImageSize = UDim2.new(0, CHECKMARK_SIZE, 0, CHECKMARK_SIZE) + else + image = Images["squircles/hollow"] + imageColor = theme.TextDefault.Color + end + + return Roact.createElement(InputButton, { + text = self.props.text, + onActivated = self.onFlip, + size = self.props.size, + image = image, + imageColor = imageColor, + fillImage = fillImage, + fillImageSize = fillImageSize, + fillImageColor = fillImageColor, + selectedColor = theme.SystemPrimaryDefault.Color, + textColor = textColor, + transparency = transparency, + layoutOrder = self.props.layoutOrder, + isDisabled = self.props.isDisabled, + }) + end) +end + +return Checkbox diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Checkbox.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Checkbox.spec.lua new file mode 100644 index 0000000..4c29fb9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Checkbox.spec.lua @@ -0,0 +1,60 @@ +return function() + local Packages = script.Parent.Parent.Parent.Parent + local Roact = require(Packages.Roact) + local Checkbox = require(script.Parent.Checkbox) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + local Images = require(Packages.UIBlox.App.ImageSet.Images) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + checkbox = Roact.createElement(Checkbox, { + text = "something", + onActivated = function () end, + size = UDim2.new(1, 0, 1, 0), + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element, frame, "Checkbox") + Roact.unmount(instance) + end) + + it("should have a hollow squircle as its false image", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + checkbox = Roact.createElement(Checkbox, { + text = "something", + onActivated = function () end, + size = UDim2.new(1, 0, 1, 0), + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element, frame, "Checkbox") + local image = frame:FindFirstChildWhichIsA("ImageButton", true) + Roact.update(instance, element) + expect(image.ImageRectOffset).to.equal(Images["squircles/hollow"].ImageRectOffset) + + Roact.unmount(instance) + end) + + it("should have a filled squircle as its true image", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + checkbox = Roact.createElement(Checkbox, { + text = "something", + isSelected = true, + onActivated = function () end, + size = UDim2.new(1, 0, 1, 0), + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element, frame, "Checkbox") + local image = frame:FindFirstChildWhichIsA("ImageButton", true) + expect(image.ImageRectOffset).to.equal(Images["squircles/fill"].ImageRectOffset) + + Roact.unmount(instance) + end) + end) + +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/CheckboxList.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/CheckboxList.lua new file mode 100644 index 0000000..ddbcf45 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/CheckboxList.lua @@ -0,0 +1,90 @@ +local Packages = script.Parent.Parent.Parent.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) +local Checkbox = require(script.Parent.Checkbox) + +local CheckboxList = Roact.PureComponent:extend("CheckboxList") + +local validateButton = t.strictInterface({ + label = t.string, + isSelected = t.optional(t.boolean), + isDisabled = t.optional(t.boolean), +}) + +local validateProps = t.strictInterface({ + checkboxes = t.array(t.union(t.string, validateButton)), + onActivated = t.callback, + elementSize = t.UDim2, + atMost = t.optional(t.number), + layoutOrder = t.optional(t.number) +}) + +local function numTrue(truthTable) + local num = 0 + for _, value in pairs(truthTable) do + if value then + num = num + 1 + end + end + + return num +end + +function CheckboxList:init() + local atMost = self.props.atMost or #self.props.checkboxes + + local selectedIndices = {} + local disabledIndices = {} + + for i, v in ipairs(self.props.checkboxes) do + if type(v) == "table" then + selectedIndices[i] = v.isSelected or false + disabledIndices[i] = v.isDisabled or false + end + end + + assert(numTrue(selectedIndices) < atMost, "number of 'isSelected' must be less than atMost!") + + self.state = { + selectedIndices = selectedIndices, + disabledIndices = disabledIndices, + } + + self.doLogic = function(key) + self:setState({ + selectedIndices = Cryo.Dictionary.join(self.state.selectedIndices, + {[key] = not self.state.selectedIndices[key] and numTrue(self.state.selectedIndices) < atMost}) + }) + self.props.onActivated(self.state.selectedIndices) + end +end + +function CheckboxList:render() + assert(validateProps(self.props)) + + local checkboxes = {} + checkboxes.layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder + }) + + for i, value in ipairs(self.props.checkboxes) do + checkboxes["Checkbox"..i] = Roact.createElement(Checkbox, { + text = type(value) == "table" and value.label or value, + isSelected = self.state.selectedIndices[i], + isDisabled = self.state.disabledIndices[i], + onActivated = function() self.doLogic(i) end, + size = self.props.elementSize, + layoutOrder = i, + }) + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + }, checkboxes) +end + +return CheckboxList diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/CheckboxList.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/CheckboxList.spec.lua new file mode 100644 index 0000000..8e6a6e7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/CheckboxList.spec.lua @@ -0,0 +1,56 @@ +return function() + local Packages = script.Parent.Parent.Parent.Parent + local Roact = require(Packages.Roact) + local CheckboxList = require(script.Parent.CheckboxList) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + local Images = require(Packages.UIBlox.App.ImageSet.Images) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + CheckboxList = Roact.createElement(CheckboxList, { + checkboxes = {"a", "b", "c"}, + onActivated = function() end, + elementSize = UDim2.new(0, 50, 0, 50), + }) + }) + local instance = Roact.mount(element, frame, "CheckboxList") + Roact.unmount(instance) + end) + + it("should have the proper default values", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + CheckboxList = Roact.createElement(CheckboxList, { + checkboxes = { + { + label = "a", + isSelected = true + }, + "b", + { + label = "c", + isSelected = true + } + }, + atMost = 6, + onActivated = function() end, + elementSize = UDim2.new(0, 50, 0, 50), + layoutOrder = 4, + }) + }) + local instance = Roact.mount(element, frame, "CheckboxList") + local image1 = frame:FindFirstChild("Checkbox1", true) + local image3 = frame:FindFirstChild("Checkbox3", true) + expect(image1.InputButtonImage.InputFillImage.ImageRectOffset).to.equal( + Images["icons/status/success_small"].ImageRectOffset) + expect(image3.InputButtonImage.InputFillImage.ImageRectOffset).to.equal( + Images["icons/status/success_small"].ImageRectOffset) + + Roact.unmount(instance) + end) + + end) + +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButton.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButton.lua new file mode 100644 index 0000000..6805057 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButton.lua @@ -0,0 +1,86 @@ +local Packages = script.Parent.Parent.Parent.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(Packages.UIBlox.Core.Style.withStyle) +local Images = require(Packages.UIBlox.App.ImageSet.Images) +local InputButton = require(Packages.UIBlox.Core.InputButton.InputButton) + +local RadioButton = Roact.PureComponent:extend("RadioButton") + +local validateProps = t.strictInterface({ + text = t.string, + isSelected = t.optional(t.boolean), + isDisabled = t.optional(t.boolean), + onActivated = t.callback, + size = t.UDim2, + layoutOrder = t.optional(t.number), + key = t.number, +}) + +RadioButton.defaultProps = { + text = "RadioButton Text", + isSelected = false, + isDisabled = false, + layoutOrder = 0, +} + +local INNER_BUTTON_SIZE = 18 + +function RadioButton:init() + self.onSetValue = function() + if not self.props.isDisabled then + self.props.onActivated(self.props.key) + end + end +end + +function RadioButton:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + local image = Images["component_assets/circle_24_stroke_1"] + local imageColor = theme.TextDefault.Color + + local fillImage = Images["component_assets/circle_16"] + local fillImageColor = theme.TextDefault.Color + + local fillImageSize + local isSelected = self.props.isSelected + + local textColor = theme.TextDefault.Color + local transparency = theme.TextDefault.Transparency + + if self.props.isDisabled then + transparency = 0.5 + end + + if isSelected then + fillImageSize = UDim2.new(0, INNER_BUTTON_SIZE, 0, INNER_BUTTON_SIZE) + fillImageColor = theme.SystemPrimaryDefault.Color + imageColor = theme.SystemPrimaryDefault.Color + else + fillImageSize = UDim2.new(0, 0, 0, 0) + end + + return Roact.createElement(InputButton, { + text = self.props.text, + onActivated = self.onSetValue, + size = self.props.size, + image = image, + imageColor = imageColor, + fillImage = fillImage, + fillImageSize = fillImageSize, + fillImageColor = fillImageColor, + selectedColor = theme.SystemPrimaryDefault.Color, + textColor = textColor, + transparency = transparency, + layoutOrder = self.props.layoutOrder, + isDisabled = self.props.isDisabled, + }) + end) +end + +return RadioButton diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButton.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButton.spec.lua new file mode 100644 index 0000000..54fce73 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButton.spec.lua @@ -0,0 +1,59 @@ +return function() + local Packages = script.Parent.Parent.Parent.Parent + local Roact = require(Packages.Roact) + local RadioButton = require(script.Parent.RadioButton) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + radioButton = Roact.createElement(RadioButton, { + text = "something", + onActivated = function () end, + size = UDim2.new(1, 0, 1, 0), + layoutOrder = 1, + key = 1, + }) + }) + local instance = Roact.mount(element, frame, "Checkbox") + Roact.unmount(instance) + end) + + it("should have an empty circle as its false image", function() + local element = mockStyleComponent({ + radioButton = Roact.createElement(RadioButton, { + text = "something", + onActivated = function () end, + size = UDim2.new(1, 0, 1, 0), + layoutOrder = 1, + key = 1, + }) + }) + local instance = Roact.mount(element, frame, "Checkbox") + local image = frame:FindFirstChild("InputFillImage", true) + expect(image.Size).to.equal(UDim2.new(0, 0)) + + Roact.unmount(instance) + end) + + it("should have a filled circle as its true image", function() + local element = mockStyleComponent({ + radioButton = Roact.createElement(RadioButton, { + text = "something", + isSelected = true, + onActivated = function () end, + size = UDim2.new(1, 0, 1, 0), + layoutOrder = 1, + key = 1, + }) + }) + local instance = Roact.mount(element, frame, "Checkbox") + local image = frame:FindFirstChild("InputFillImage", true) + expect(image.Size).never.to.equal(UDim2.new(0, 0)) + + Roact.unmount(instance) + end) + end) + +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButtonList.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButtonList.lua new file mode 100644 index 0000000..181d919 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButtonList.lua @@ -0,0 +1,69 @@ +local Packages = script.Parent.Parent.Parent.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local RadioButton = require(script.Parent.RadioButton) + +local RadioButtonList = Roact.PureComponent:extend("RadioButtonList") + +local validateButton = t.strictInterface({ + label = t.string, + isDisabled = t.optional(t.boolean), +}) + +local validateProps = t.strictInterface({ + radioButtons = t.array(t.union(t.string, validateButton)), + onActivated = t.callback, + elementSize = t.UDim2, + selectedValue = t.optional(t.number), + layoutOrder = t.optional(t.number) +}) + +function RadioButtonList:init() + self.state = { + currentValue = self.props.selectedValue or 0, + } + + local disabledIndices = {} + for i, v in ipairs(self.props.radioButtons) do + disabledIndices[i] = type(v) == "table" and v.isDisabled or false + end + self.state.disabledIndices = disabledIndices + + self.doLogic = function(key) + if self.state.disabledIndices[key] then return end + self:setState({ + currentValue = key, + }) + self.props.onActivated(key) + end +end + +function RadioButtonList:render() + assert(validateProps(self.props)) + + local radioButtons = {} + radioButtons.layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder + }) + + for i, value in ipairs(self.props.radioButtons) do + radioButtons["RadioButton"..i] = Roact.createElement(RadioButton, { + text = type(value) == "table" and value.label or value, + isSelected = i == self.state.currentValue, + isDisabled = self.state.disabledIndices[i], + onActivated = self.doLogic, + size = self.props.elementSize, + layoutOrder = i, + key = i, + }) + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + }, radioButtons) +end + +return RadioButtonList \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButtonList.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButtonList.spec.lua new file mode 100644 index 0000000..ca70688 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButtonList.spec.lua @@ -0,0 +1,43 @@ +return function() + local Packages = script.Parent.Parent.Parent.Parent + local Roact = require(Packages.Roact) + local RadioButtonList = require(script.Parent.RadioButtonList) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + local Images = require(Packages.UIBlox.App.ImageSet.Images) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + RadioButtonList = Roact.createElement(RadioButtonList, { + radioButtons = {"a", "b", "c"}, + onActivated = function() end, + elementSize = UDim2.new(0, 50, 0, 50), + }) + }) + local instance = Roact.mount(element, frame, "RadioButtonList") + Roact.unmount(instance) + end) + + it("should have the proper default value", function() + local element = mockStyleComponent({ + RadioButtonList = Roact.createElement(RadioButtonList, { + radioButtons = {"a", "b", "c"}, + selectedValue = 2, + onActivated = function() end, + elementSize = UDim2.new(0, 50, 0, 50), + layoutOrder = 4, + }) + }) + + local instance = Roact.mount(element, frame, "RadioButtonList") + local image2 = frame:FindFirstChild("RadioButton2", true) + expect(image2.InputButtonImage.InputFillImage.ImageRectOffset).to.equal( + Images["component_assets/circle_16"].ImageRectOffset) + + Roact.unmount(instance) + end) + + end) + +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Toggle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Toggle.lua new file mode 100644 index 0000000..0c5885d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Toggle.lua @@ -0,0 +1,210 @@ +local InputButton = script.Parent +local App = InputButton.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Otter = require(Packages.Otter) +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local withStyle = require(Packages.UIBlox.Core.Style.withStyle) +local Images = require(Packages.UIBlox.App.ImageSet.Images) +local Controllable = require(Packages.UIBlox.Core.Control.Controllable) +local ControlState = require(Packages.UIBlox.Core.Control.Enum.ControlState) +local ImageSetComponent = require(Packages.UIBlox.Core.ImageSet.ImageSetComponent) + +local divideTransparency = require(Packages.UIBlox.Utility.divideTransparency) +local lerp = require(Packages.UIBlox.Utility.lerp) + +local SPRING_PARAMETERS = { + frequency = 4, +} + +local KNOB_OFF_POSITION = UDim2.new(0, -4, 0.5, 0) +local KNOB_ON_POSITION = UDim2.new(0, 20, 0.5, 0) + +local TRACK_IMAGE_ID = "component_assets/circle_36_stroke_1" +local TRACK_SLICE_CENTER = Rect.new(18, 18, 18, 18) + +local TRACK_FILL_IMAGE_ID = "component_assets/circle_36" +local TRACK_FILL_SLICE_CENTER = Rect.new(18, 18, 18, 18) + +local KNOB_IMAGE_ID = "component_assets/circle_28_padding_10" +local KNOB_SHADOW_IMAGE_ID = "component_assets/dropshadow_28" + +local validateProps = t.strictInterface({ + isSelected = t.optional(t.boolean), + isDisabled = t.optional(t.boolean), + onActivated = t.callback, + + layoutOrder = t.optional(t.integer), + anchorPoint = t.optional(t.Vector2), + position = t.optional(t.UDim2), +}) + +local InnerToggle = Roact.PureComponent:extend("Toggle") + +function InnerToggle:init() + local initialProgress = self.props.isSelected and 1 or 0 + local setProgress + self.progress, setProgress = Roact.createBinding(initialProgress) + + self.style, self.setStyle = Roact.createBinding(self.props.style) + self.controlState, self.setControlState = Roact.createBinding(self.state.controlState) + + local joinedBinding = Roact.joinBindings({ + progress = self.progress, + style = self.style, + controlState = self.controlState, + }) + + self.fillTransparency = joinedBinding:map(function(values) + local baseTransparency = values.style.Theme.ContextualPrimaryDefault.Transparency + local transparencyDivisor = values.controlState == ControlState.Disabled and 2 or 1 + return lerp(1, divideTransparency(baseTransparency, transparencyDivisor), values.progress) + end) + + self.knobPosition = self.progress:map(function(value) + return KNOB_OFF_POSITION:lerp(KNOB_ON_POSITION, value) + end) + + self.knobTransparency = Roact.joinBindings({ + style = self.style, + controlState = self.controlState, + }):map(function(values) + local baseTransparency = values.style.Theme.ContextualPrimaryDefault.Transparency + local transparencyDivisor = values.controlState == ControlState.Disabled and 2 or 1 + return divideTransparency(baseTransparency, transparencyDivisor) + end) + + -- We need to fade the track outline out when the toggle is selected, because + -- otherwise it creates a visually jarring border around the filled track. + self.trackTransparency = joinedBinding:map(function(values) + local targetTransparency = values.controlState == ControlState.Hover + and values.style.Theme.SecondaryOnHover.Transparency + or values.style.Theme.SecondaryDefault.Transparency + + if values.controlState == ControlState.Disabled then + targetTransparency = 1 - (1 - targetTransparency) / 2 + end + + return lerp(targetTransparency, 1, values.progress) + end) + + self.trackColor = Roact.joinBindings({ + style = self.style, + controlState = self.controlState, + }):map(function(values) + if values.controlState == ControlState.Hover then + return values.style.Theme.SecondaryOnHover.Color + else + return values.style.Theme.SecondaryDefault.Color + end + end) + + self.fillColor = self.style:map(function(style) + return style.Theme.ContextualPrimaryDefault.Color + end) + + self.progressMotor = Otter.createSingleMotor(initialProgress) + self.progressMotor:onStep(setProgress) +end + +function InnerToggle:render() + return withStyle(function(style) + return Roact.createElement(Controllable, { + controlComponent = { + component = "ImageButton", + props = { + BackgroundTransparency = 1, + Image = "", + Size = UDim2.fromOffset(60, 44), + Position = self.props.position, + LayoutOrder = self.props.layoutOrder, + AnchorPoint = self.props.anchorPoint, + [Roact.Event.Activated] = not self.props.isDisabled and self.props.onActivated, + }, + children = { + Track = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(60, 36), + BackgroundTransparency = 1, + Image = Images[TRACK_IMAGE_ID], + ScaleType = Enum.ScaleType.Slice, + SliceCenter = TRACK_SLICE_CENTER, + ImageTransparency = self.trackTransparency, + ImageColor3 = self.trackColor, + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + }), + Fill = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(60, 36), + BackgroundTransparency = 1, + Image = Images[TRACK_FILL_IMAGE_ID], + ScaleType = Enum.ScaleType.Slice, + SliceCenter = TRACK_FILL_SLICE_CENTER, + ImageColor3 = self.fillColor, + ImageTransparency = self.fillTransparency, + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + ZIndex = 2, + }), + Knob = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(44, 44), + BackgroundTransparency = 1, + Image = Images[KNOB_IMAGE_ID], + ImageTransparency = self.knobTransparency, + Position = self.knobPosition, + AnchorPoint = Vector2.new(0, 0.5), + ZIndex = 4, + }), + KnobShadow = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(44, 44), + BackgroundTransparency = 1, + Image = Images[KNOB_SHADOW_IMAGE_ID], + ImageTransparency = self.knobTransparency, + Position = self.knobPosition, + AnchorPoint = Vector2.new(0, 0.5), + ZIndex = 3, + }), + } + }, + isDisabled = self.props.isDisabled, + onStateChanged = function(_, newState) + self.setControlState(newState) + end, + }) + end) +end + +function InnerToggle:didMount() + self.progressMotor:start() +end + +function InnerToggle:didUpdate(lastProps, lastState) + if lastProps.isSelected ~= self.props.isSelected then + local newProgress = self.props.isSelected and 1 or 0 + self.progressMotor:setGoal(Otter.spring(newProgress, SPRING_PARAMETERS)) + end + + if lastProps.style ~= self.props.style then + self.setStyle(self.props.style) + end +end + +function InnerToggle:willUnmount() + self.progressMotor:destroy() +end + +local function injectUIBloxStyle(props) + -- Validate props here, since the inner toggle receives these props plus + -- the style prop! + assert(validateProps(props)) + return withStyle(function(style) + return Roact.createElement(InnerToggle, Cryo.Dictionary.join(props, { + style = style, + })) + end) +end + +return injectUIBloxStyle diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Toggle.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Toggle.spec.lua new file mode 100644 index 0000000..e3d4b12 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Toggle.spec.lua @@ -0,0 +1,19 @@ +return function() + local Packages = script.Parent.Parent.Parent.Parent + local Roact = require(Packages.Roact) + local Toggle = require(script.Parent.Toggle) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + TestToggle = Roact.createElement(Toggle, { + onActivated = function() + end, + }) + }) + local instance = Roact.mount(element, nil, "Toggle") + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultiLineCheckbox.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultiLineCheckbox.story.lua new file mode 100644 index 0000000..6d05138 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultiLineCheckbox.story.lua @@ -0,0 +1,82 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Packages = ReplicatedStorage.Packages +local Roact = require(Packages.Roact) +local UIBlox = require(Packages.UIBlox) +local GenericTextLabel = require(Packages.UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local withStyle = UIBlox.Core.Style.withStyle + +local StoryComponents = Packages.StoryComponents +local StoryView = require(StoryComponents.StoryView) +local StoryItem = require(StoryComponents.StoryItem) + +local CheckboxList = UIBlox.App.InputButton.CheckboxList + +return function(target) + local MulitlineCheckboxDemo = Roact.PureComponent:extend("MulitlineCheckboxDemo") + + function MulitlineCheckboxDemo:render() + return withStyle(function(style) + return Roact.createElement(StoryView, {}, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }), + Label = Roact.createElement(GenericTextLabel, { + BackgroundTransparency = 1, + Text = "This CheckboxList can have up to 3 boxes selected at a time", + Size = UDim2.new(1, 0, 0, 50), + LayoutOrder = 1, + fontStyle = style.Font.Header2, + colorStyle = style.Theme.TextDefault, + }), + CheckboxListFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 480, 0, 300), + LayoutOrder = 2, + }, { + CheckboxList = Roact.createElement(CheckboxList, { + atMost = 3, + checkboxes = { + { + label = "Selected and Disabled", + isSelected = true, + isDisabled = true, + }, + { + label = "Unselected and Disabled", + isSelected = false, + isDisabled = true, + }, + { + label = "Selected and Enabled", + isSelected = true, + isDisabled = false, + }, + "Unselected and Enabled", + "This is a checkbox that has an absurd amount of text in its label to demonstrate wrapping", + }, + onActivated = function() end, + elementSize = UDim2.new(0, 480, 0, 54), + layoutOrder = 1, + }), + }), + }) + end) + end + + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + StoryItem = Roact.createElement(StoryItem, { + title = "A demonstration of checkboxes with long labels", + subTitle = "Shows how text can wrap when it is long", + showDivider = true, + }, { + Roact.createElement(MulitlineCheckboxDemo) + }) + }), target, "CheckboxList") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultipleCheckboxes.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultipleCheckboxes.story.lua new file mode 100644 index 0000000..616ce33 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultipleCheckboxes.story.lua @@ -0,0 +1,75 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Packages = script.Parent.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local CheckboxList = require(script.Parent.Parent.CheckboxList) + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local yourComponent = Roact.Component:extend("yourComponent") +function yourComponent:init() + self.ref = Roact.createRef() +end + +function yourComponent:render() + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, -50), + BackgroundColor3 = Color3.fromRGB(55, 55, 55), + }, { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + + }), + checkboxes = Roact.createElement(CheckboxList, { + layoutOrder = 1, + atMost = 3, + checkboxes = { + { + label = "Vaporeon", + isSelected = true, + isDisabled = true, + }, + "Jolteon", + "Flareon", + "Espeon", + "Umbreon", + "Leafeon", + "Glaceon", + "Sylveon", + }, + onActivated = function(value) + local values = "" + for k, v in pairs(value) do + if v then + values = values .. k .. ", " + end + end + if self.ref.current then + self.ref.current.Text = "Pick at most 3. The current value is: " .. values + print(self.ref.current.Text) + end + end, + elementSize = UDim2.new(0, 480, 0, 54), + }), + text = Roact.createElement("TextLabel", { + LayoutOrder = 2, + TextSize = 14, + Text = "Pick at most 3", + TextXAlignment = Enum.TextXAlignment.Left, + Size = UDim2.new(1, 0, 0, 50), + [Roact.Ref] = self.ref, + }), + }) + +end + + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(yourComponent), + }), target, "MultipleCheckboxes") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultipleRadioButtons.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultipleRadioButtons.story.lua new file mode 100644 index 0000000..e78310e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultipleRadioButtons.story.lua @@ -0,0 +1,39 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Packages = script.Parent.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local RadioButtonList = require(script.Parent.Parent.RadioButtonList) + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +return function(target) + local styleProvider = Roact.createElement(StoryView, {}, { + Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Color3.fromRGB(55, 55, 55), + }, { + Roact.createElement(RadioButtonList, { + radioButtons = { + { + label = "Bulbasaur" + }, + "Squirtle", + "Charmander", + { + label = "Mewtwo", + isDisabled = true, + }, + }, + onActivated = function(value) + print("The current value is: ", value) + end, + selectedValue = 2, + elementSize = UDim2.new(0, 480, 0, 54), + }) + }) + }) + local handle = Roact.mount(styleProvider, target, "MultipleRadioButtons") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/LoadingState.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/LoadingState.lua new file mode 100644 index 0000000..6d6b0dd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/LoadingState.lua @@ -0,0 +1,11 @@ +local Loading = script.Parent.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate("LoadingState", { + "Loading", + "Failed", + "Loaded" +}) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/ReloadingStyle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/ReloadingStyle.lua new file mode 100644 index 0000000..0545378 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/ReloadingStyle.lua @@ -0,0 +1,10 @@ +local Loading = script.Parent.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate("ReloadingStyle", { + "AllowReload", + "LockReload", +}) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/RenderOnFailedStyle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/RenderOnFailedStyle.lua new file mode 100644 index 0000000..31b9571 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/RenderOnFailedStyle.lua @@ -0,0 +1,10 @@ +local Loading = script.Parent.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate("RenderOnFailedStyle", { + "EmptyStatePage", + "RetryButton", +}) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/RetrievalStatus.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/RetrievalStatus.lua new file mode 100644 index 0000000..04db7bd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/RetrievalStatus.lua @@ -0,0 +1,12 @@ +local Loading = script.Parent.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate("RetrievalStatus", { + "NotStarted", + "Fetching", + "Done", + "Failed", +}) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadableImage.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadableImage.lua new file mode 100644 index 0000000..9cf3964 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadableImage.lua @@ -0,0 +1,319 @@ +local Loading = script.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) + +local ShimmerPanel = require(Loading.ShimmerPanel) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local Images = require(UIBlox.App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local ContentProviderContext = require(UIBlox.App.Context.ContentProvider) + +local LOAD_FAILED_RETRY_COUNT = 3 +local RETRY_TIME_MULTIPLIER = 1.5 + +local decal = Instance.new("Decal") +local inf = math.huge +local loadedImagesByUri = {} + +local LoadingState = { + InProgress = "InProgress", + Failed = "Failed", + Loaded = "Loaded", +} + +local function shouldLoadImage(image) + return image ~= nil and loadedImagesByUri[image] == nil +end + +local validateProps = t.strictInterface({ + -- The anchor point of the final and loading image + AnchorPoint = t.optional(t.Vector2), + -- The background color of the final image. Defaults to placeholder background color. + BackgroundColor3 = t.optional(t.Color3), + -- The background transparency of the final image. Defaults to placeholder transparency. + BackgroundTransparency = t.optional(t.number), + -- The corner radius of the image, shimmer, and failed image's rounded corners. + cornerRadius = t.optional(t.UDim), + -- The final image + Image = t.optional(t.string), + -- The transparency of the final and loading image + ImageTransparency = t.optional(t.number), + -- The image rect offset of the final and loading image + ImageRectOffset = t.optional(t.union(t.Vector2, t.table)), + -- The image rect size of the final and loading image + ImageRectSize = t.optional(t.union(t.Vector2, t.table)), + -- The layout order of the final and loading image + LayoutOrder = t.optional(t.integer), + -- The loading image which shows if useShimmerAnimationWhileLoading is false + loadingImage = t.optional(t.string), + -- The max size of all images shown + MaxSize = t.optional(t.Vector2), + -- The min size of all images shown + MinSize = t.optional(t.Vector2), + -- The function to call when loading is complete + onLoaded = t.optional(t.callback), + -- The position point of the loading image + Position = t.optional(t.UDim2), + -- The scale type of the final and loading image + ScaleType = t.optional(t.enum(Enum.ScaleType)), + -- The size point of the loading image + Size = t.UDim2, + -- Whether or not to show a static image or the shimmer animation while loading + useShimmerAnimationWhileLoading = t.optional(t.boolean), + -- Whether to show failed state when failed + showFailedStateWhenLoadingFailed = t.optional(t.boolean), + -- The ZIndex of the loading and final images + ZIndex = t.optional(t.integer), + + contentProvider = t.union(t.instanceOf("ContentProvider"), t.table), +}) + +local LoadableImage = Roact.PureComponent:extend("LoadableImage") + +LoadableImage.defaultProps = { + BackgroundTransparency = 0, + cornerRadius = UDim.new(0, 0), + MaxSize = Vector2.new(inf, inf), + MinSize = Vector2.new(0, 0), + useShimmerAnimationWhileLoading = false, + showFailedStateWhenLoadingFailed = false, +} + +function LoadableImage:init() + self.state = { + loadingState = loadedImagesByUri[self.props.Image] and LoadingState.Loaded or LoadingState.InProgress, + } + self._isMounted = false + + + self.isLoadingComplete = function(image) + if image == Roact.None or image == nil then + return false + else + return self.state.loadingState ~= LoadingState.InProgress + end + end +end + +function LoadableImage:render() + assert(validateProps(self.props)) + + local anchorPoint = self.props.AnchorPoint + local layoutOrder = self.props.LayoutOrder + local size = self.props.Size + local position = self.props.Position + local backgroundColor3 = self.props.BackgroundColor3 + local backgroundTransparency = self.props.BackgroundTransparency + local cornerRadius = self.props.cornerRadius + local scaleType = self.props.ScaleType + local zIndex = self.props.ZIndex + local image = self.props.Image + local imageTransparency = self.props.ImageTransparency + local imageRectOffset = self.props.ImageRectOffset + local imageRectSize = self.props.ImageRectSize + local maxSize = self.props.MaxSize + local minSize = self.props.MinSize + local loadingImage = self.props.loadingImage + local useShimmerAnimationWhileLoading = self.props.useShimmerAnimationWhileLoading + local showFailedStateWhenLoadingFailed = self.props.showFailedStateWhenLoadingFailed + local loadingComplete = self.isLoadingComplete(image) + local loadingFailed = self.state.loadingState == LoadingState.Failed + local hasUISizeConstraint = false + + if maxSize.X ~= inf or maxSize.Y ~= inf or minSize.X ~= 0 or minSize.Y ~= 0 then + hasUISizeConstraint = true + end + + local sizeConstraint = hasUISizeConstraint and Roact.createElement("UISizeConstraint", { + MaxSize = maxSize, + MinSize = minSize, + }) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + if loadingFailed and showFailedStateWhenLoadingFailed then + local failedImage = Images["icons/status/imageunavailable"] + local failedImageSize = failedImage.ImageRectSize / Images.ImagesResolutionScale + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BorderSizePixel = 0, + BackgroundColor3 = theme.PlaceHolder.Color, + BackgroundTransparency = theme.PlaceHolder.Transparency, + LayoutOrder = layoutOrder, + Position = position, + Size = size, + ZIndex = zIndex, + }, { + EmptyIcon = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(0.5, 0.5), + Image = failedImage, + ImageColor3 = theme.UIDefault.Color, + ImageTransparency = theme.UIDefault.Transparency, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, failedImageSize.X, 0, failedImageSize.Y), + }, { + UICorner = cornerRadius ~= UDim.new(0, 0) and Roact.createElement("UICorner", { + CornerRadius = cornerRadius, + }) or nil, + }), + UISizeConstraint = sizeConstraint, + UICorner = cornerRadius ~= UDim.new(0, 0) and Roact.createElement("UICorner", { + CornerRadius = cornerRadius, + }) or nil, + }) + elseif not loadingComplete and useShimmerAnimationWhileLoading then + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BorderSizePixel = 0, + BackgroundColor3 = theme.PlaceHolder.Color, + BackgroundTransparency = theme.PlaceHolder.Transparency, + LayoutOrder = layoutOrder, + Position = position, + Size = size, + ZIndex = zIndex, + }, { + Shimmer = Roact.createElement(ShimmerPanel, { + Size = UDim2.new(1, 0, 1, 0), + cornerRadius = cornerRadius, + }), + UISizeConstraint = sizeConstraint, + UICorner = Roact.createElement("UICorner", { + CornerRadius = cornerRadius, + }) or nil, + }) + else + return Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = anchorPoint, + BackgroundColor3 = backgroundColor3 or theme.PlaceHolder.Color, + BackgroundTransparency = backgroundTransparency or theme.PlaceHolder.Transparency, + BorderSizePixel = 0, + Image = loadingComplete and image or loadingImage, + ImageTransparency = imageTransparency, + ImageRectOffset = imageRectOffset, + ImageRectSize = imageRectSize, + LayoutOrder = layoutOrder, + Position = position, + ScaleType = scaleType, + Size = size, + ZIndex = zIndex, + }, { + UISizeConstraint = sizeConstraint, + UICorner = cornerRadius ~= UDim.new(0, 0) and Roact.createElement("UICorner", { + CornerRadius = cornerRadius, + }) or nil, + }) + end + end) +end + +function LoadableImage:didUpdate(oldProps) + if oldProps.Image ~= self.props.Image then + self:_loadImage() + end +end + +function LoadableImage:didMount() + self._isMounted = true + + self:_loadImage() +end + +function LoadableImage:willUnmount() + self._isMounted = false +end + +function LoadableImage:_loadImage() + local image = self.props.Image + + if shouldLoadImage(image) then + self:setState({ + loadingState = LoadingState.InProgress + }) + else + if loadedImagesByUri[image] then + self:setState({ + loadingState = LoadingState.Loaded + }) + elseif loadedImagesByUri[image] == false then + self:setState({ + loadingState = LoadingState.Failed + }) + end + return + end + + -- Synchronization/Batching work should be done in engine for performance improvements + -- related ticket: CLIPLAYEREX-1764 + coroutine.wrap(function() + local retryCount = 0 + local loadingFailed + + while loadedImagesByUri[image] == nil and retryCount <= LOAD_FAILED_RETRY_COUNT do + if retryCount > 0 then + wait(RETRY_TIME_MULTIPLIER * math.pow(2, retryCount - 1)) + end + + loadingFailed = false + decal.Texture = image + + self.props.contentProvider:PreloadAsync({decal}, function(contentId, assetFetchStatus) + if contentId == image and assetFetchStatus == Enum.AssetFetchStatus.Failure then + loadingFailed = true + end + end) + + -- Image load succeeded, no retry required + if not loadingFailed then + break + end + + retryCount = retryCount + 1 + end + + if loadingFailed == nil then + loadingFailed = not loadedImagesByUri[image] + else + loadedImagesByUri[image] = not loadingFailed + end + + if self._isMounted and self.props.Image == image then + self:setState({ + loadingState = loadingFailed and LoadingState.Failed or LoadingState.Loaded, + }) + + if self.props.onLoaded then + self.props.onLoaded() + end + end + end)() +end + +function LoadableImage.isLoaded(image) + if image == Roact.None or image == nil then + return false + else + return loadedImagesByUri[image] == true + end +end + +return function(props) + return Roact.createElement(ContentProviderContext.Consumer, { + render = function(contentProvider) + local propsWithContentProvider = Cryo.Dictionary.join(props, { + contentProvider = contentProvider, + }) + + return Roact.createElement(LoadableImage, propsWithContentProvider) + end, + }) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadableImage.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadableImage.spec.lua new file mode 100644 index 0000000..33da6d7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadableImage.spec.lua @@ -0,0 +1,114 @@ +return function() + local Loading = script.Parent + local App = Loading.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local LoadableImage = require(Loading.LoadableImage) + + local ContentProviderContext = require(UIBlox.App.Context.ContentProvider) + + local function getMockedContentProvider() + local MockContentProvider = {} + MockContentProvider.__index = MockContentProvider + function MockContentProvider.new() + local self = {} + + setmetatable(self, { + __index = MockContentProvider, + }) + return self + end + + function MockContentProvider:PreloadAsync(assets, callback) + for _, value in ipairs(assets) do + callback(value, Enum.AssetFetchStatus.Success) + end + end + + return MockContentProvider.new() + end + + local testImage = "https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814" + local defaultLoadImage = "rbxasset://textures/ui/LuaApp/icons/ic-game.png" + + describe("LoadableImage", function() + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + Image = Roact.createElement(LoadableImage, { + Image = testImage, + Size = UDim2.fromOffset(80, 80), + Position = UDim2.fromOffset(50, 50), + BackgroundColor3 = Color3.new(0, 0, 0), + BackgroundTransparency = 0, + loadingImage = defaultLoadImage, + }) + }) + local instance = Roact.mount(element, nil, "LoadableImageSample") + Roact.unmount(instance) + end) + it("should not set loading image if image is already in cache", function() + local element = mockStyleComponent({ + ContentProviderProvider = Roact.createElement(ContentProviderContext.Provider, { + value = getMockedContentProvider(), + }, { + Image = Roact.createElement(LoadableImage, { + Image = testImage, + Size = UDim2.fromOffset(80, 80), + Position = UDim2.fromOffset(50, 50), + BackgroundColor3 = Color3.new(0, 0, 0), + BackgroundTransparency = 0, + loadingImage = defaultLoadImage, + }) + }) + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "LoadableImageSample") + expect(container.LoadableImageSample.Image).never.to.equal(defaultLoadImage) + Roact.unmount(instance) + end) + it("should create and destroy on all non-default optional parameters without errors", function() + local element = mockStyleComponent({ + ContentProviderProvider = Roact.createElement(ContentProviderContext.Provider, { + value = getMockedContentProvider(), + }, { + Image = Roact.createElement(LoadableImage, { + AnchorPoint = Vector2.new(0.5, 0), + BackgroundColor3 = Color3.new(255, 0, 0), + BackgroundTransparency = 0.5, + Image = testImage, + ImageRectOffset = Vector2.new(0, 0), + ImageRectSize = Vector2.new(50, 50), + LayoutOrder = 1, + loadingImage = defaultLoadImage, + MaxSize = Vector2.new(10, 10), + MinSize = Vector2.new(1, 1), + Position = UDim2.fromOffset(50, 50), + Size = UDim2.fromOffset(80, 80), + useShimmerAnimationWhileLoading = false, + ZIndex = 1, + }) + }) + }) + local instance = Roact.mount(element, nil, "LoadableImageSample") + Roact.unmount(instance) + end) + it("should set failed image without errors", function() + local element = mockStyleComponent({ + Image = Roact.createElement(LoadableImage, { + Image = "invalid-image-url", + Size = UDim2.fromOffset(80, 80), + Position = UDim2.fromOffset(50, 50), + BackgroundColor3 = Color3.new(0, 0, 0), + BackgroundTransparency = 0, + loadingImage = defaultLoadImage, + showFailedStateWhenLoadingFailed = true, + }) + }) + local instance = Roact.mount(element, nil, "LoadableImageSample") + Roact.unmount(instance) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadingSpinner.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadingSpinner.lua new file mode 100644 index 0000000..3af3e16 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadingSpinner.lua @@ -0,0 +1,32 @@ +local Loading = script.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(UIBlox.App.ImageSet.Images) + +local SpinningImage = require(UIBlox.Core.Animation.SpinningImage) + +local LoadingSpinner = Roact.PureComponent:extend("LoadingSpinner") + +LoadingSpinner.validateProps = t.strictInterface({ + size = t.optional(t.UDim2), + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + rotationRate = t.optional(t.number), +}) + +function LoadingSpinner:render() + return Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + size = self.props.size, + position = self.props.position, + anchorPoint = self.props.anchorPoint, + rotationRate = self.props.rotationRate, + }) +end + +return LoadingSpinner diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadingSpinner.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadingSpinner.spec.lua new file mode 100644 index 0000000..e0b603b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadingSpinner.spec.lua @@ -0,0 +1,25 @@ +return function() + local Loading = script.Parent + local App = Loading.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local LoadingSpinner = require(Loading.LoadingSpinner) + + it("should create and destroy without errors", function() + local spinner = Roact.createElement(LoadingSpinner) + local instance = Roact.mount(spinner, nil, "LoadingSpinnerTest") + Roact.unmount(instance) + end) + + it("should accept valid props", function() + local spinner = Roact.createElement(LoadingSpinner, { + rotationRate = 1, + anchorPoint = Vector2.new(1, 2), + position = UDim2.new(1, 2, 3, 4), + }) + local instance = Roact.mount(spinner, nil, "LoadingSpinnerTest") + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/ShimmerPanel.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/ShimmerPanel.lua new file mode 100644 index 0000000..24f61f7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/ShimmerPanel.lua @@ -0,0 +1,86 @@ +local Loading = script.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local TextureScroller = require(script.Parent.TextureScroller) + +local validateProps = t.strictInterface({ + AnchorPoint = t.optional(t.Vector2), + LayoutOrder = t.optional(t.integer), + Position = t.optional(t.UDim2), + Size = t.UDim2, + + -- The corner radius of the image's rounded corners. Defaults to UDim(0, 0) for corners with no rounding. + cornerRadius = t.optional(t.UDim), + + -- The loading image that will move across the panel + Image = t.optional(t.string), + + -- The pixel dimensions of the moving image + imageDimensions = t.optional(t.Vector2), + + -- The scale of the moving image + imageScale = t.optional(t.number), + + -- The speed of the moving image + shimmerSpeed = t.optional(t.number), +}) + +local ShimmerPanel = Roact.PureComponent:extend("ShimmerPanel") + +ShimmerPanel.defaultProps = { + cornerRadius = UDim.new(0, 0), + Image = "rbxasset://textures/ui/LuaApp/graphic/shimmer.png", + imageDimensions = Vector2.new(219, 250), + imageScale = 2.5, + shimmerSpeed = 4, +} + +function ShimmerPanel:render() + assert(validateProps(self.props)) + + local anchorPoint = self.props.AnchorPoint + local layoutOrder = self.props.LayoutOrder + local position = self.props.Position + local cornerRadius = self.props.cornerRadius + local shimmerImage = self.props.Image + local shimmerImageDimensions = self.props.imageDimensions + local imageScale = self.props.imageScale + local shimmerSpeed = self.props.shimmerSpeed + local size = self.props.Size + + local imageRectSize = shimmerImageDimensions / imageScale + + local repeatTime = 0 + if shimmerSpeed ~= 0 then + repeatTime = (imageScale + 1) / shimmerSpeed + end + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + return Roact.createElement(TextureScroller, { + anchorPoint = anchorPoint, + layoutOrder = layoutOrder, + backgroundColor3 = theme.PlaceHolder.Color, + backgroundTransparency = theme.PlaceHolder.Transparency, + cornerRadius = cornerRadius, + image = shimmerImage, + imageRectSize = imageRectSize, + imageAnchorPoint = Vector2.new(0, 0.5), + imageTransparency = 0, + imageRectOffsetStart = Vector2.new(shimmerImageDimensions.X, shimmerImageDimensions.Y / 2), + imageRectOffsetEnd = Vector2.new(-imageRectSize.X, shimmerImageDimensions.Y / 2), + imageScrollCycleTime = repeatTime, + position = position, + size = size, + }) + end) +end + +return ShimmerPanel \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/ShimmerPanel.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/ShimmerPanel.spec.lua new file mode 100644 index 0000000..921bcb7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/ShimmerPanel.spec.lua @@ -0,0 +1,22 @@ +return function() + local Loading = script.Parent + local App = Loading.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local ShimmerPanel = require(Loading.ShimmerPanel) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + ShimmerPanel = Roact.createElement(ShimmerPanel, { + Size = UDim2.new(0, 100, 0, 100), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/TextureScroller.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/TextureScroller.lua new file mode 100644 index 0000000..ab1d345 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/TextureScroller.lua @@ -0,0 +1,134 @@ +local RunService = game:GetService("RunService") +local Loading = script.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local ExternalEventConnection = require(UIBlox.Utility.ExternalEventConnection) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local function floorVector2(vector2) + return Vector2.new(math.floor(vector2.X), math.floor(vector2.Y)) +end + +local validateProps = t.strictInterface({ + -- The anchor point of the panel + anchorPoint = t.optional(t.Vector2), + -- The layout order of the panel + layoutOrder = t.optional(t.integer), + -- The background color of the panel + backgroundColor3 = t.optional(t.Color3), + -- The background transparency of the panel + backgroundTransparency = t.optional(t.number), + -- The position of the panel + position = t.optional(t.UDim2), + -- The corner radius of the image's rounded corners. Defaults to UDim(0, 0) for corners with no rounding. + cornerRadius = t.optional(t.UDim), + -- The image that will move across the panel + image = t.string, + -- The tranparency of the moving image + imageTransparency = t.optional(t.number), + -- The anchor point of the moving image rect + imageAnchorPoint = t.optional(t.Vector2), + -- The start position of the moving image rect + imageRectOffsetStart = t.Vector2, + -- The end position of the moving image rect + imageRectOffsetEnd = t.Vector2, + -- The take it takes for the image to move from the start positon + -- to the end positions + imageScrollCycleTime = t.optional(t.number), + -- The size of the image rect that is projected onto the panel + imageRectSize = t.Vector2, + -- The size point of the panel + size = t.optional(t.UDim2), +}) + +local TextureScroller = Roact.PureComponent:extend("TextureScroller") + +TextureScroller.defaultProps = { + backgroundTransparency = 1, + cornerRadius = UDim.new(0, 0), + imageAnchorPoint = Vector2.new(0, 0), + imageScrollCycleTime = 1, +} + +function TextureScroller:init() + self.lerpValue = 0 + self.imageRef = Roact.createRef() + + self.renderSteppedCallback = function(dt) + local imageScrollCycleTime = self.props.imageScrollCycleTime + local imageRectOffsetStart = self.props.imageRectOffsetStart + local imageRectOffsetEnd = self.props.imageRectOffsetEnd + local imageRectSize = self.props.imageRectSize + local imageAnchorPoint = self.props.imageAnchorPoint + + local anchoredImageOffsetStart = Vector2.new( + imageRectOffsetStart.X - imageAnchorPoint.X * imageRectSize.X, + imageRectOffsetStart.Y - imageAnchorPoint.Y * imageRectSize.Y) + local anchoredImageOffsetEnd = Vector2.new( + imageRectOffsetEnd.X - imageAnchorPoint.X * imageRectSize.X, + imageRectOffsetEnd.Y - imageAnchorPoint.Y * imageRectSize.Y) + + local lerpPerFrame = 0 + if imageScrollCycleTime ~= 0 then + lerpPerFrame = (dt / imageScrollCycleTime) + end + self.lerpValue = (self.lerpValue + lerpPerFrame) % 1 + if self.imageRef.current then + self.imageRef.current.ImageRectOffset = floorVector2( + anchoredImageOffsetStart:lerp(anchoredImageOffsetEnd, self.lerpValue)) + end + end +end + +function TextureScroller:render() + assert(validateProps(self.props)) + local anchorPoint = self.props.anchorPoint + local backgroundColor = self.props.backgroundColor3 + local backgroundTransparency = self.props.backgroundTransparency + local cornerRadius = self.props.cornerRadius + local image = self.props.image + local imageRectSize = self.props.imageRectSize + local imageTransparency = self.props.imageTransparency + local layoutOrder = self.props.layoutOrder + local position = self.props.position + local size = self.props.size + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BackgroundColor3 = backgroundColor, + BackgroundTransparency = backgroundTransparency, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + Position = position, + Size = size, + }, { + TextureScrollerImage = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = image, + ImageTransparency = imageTransparency, + Size = UDim2.new(1, 0, 1, 0), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(0, 0, imageRectSize.X, imageRectSize.Y), + [Roact.Ref] = self.imageRef, + ImageRectSize = imageRectSize, + }, { + UICorner = cornerRadius ~= UDim.new(0, 0) and Roact.createElement("UICorner", { + CornerRadius = cornerRadius, + }) or nil, + }), + renderStepped = Roact.createElement(ExternalEventConnection, { + callback = self.renderSteppedCallback, + event = RunService.renderStepped, + }), + UICorner = cornerRadius ~= UDim.new(0, 0) and Roact.createElement("UICorner", { + CornerRadius = cornerRadius, + }) or nil, + }) +end + +return TextureScroller \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/TextureScroller.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/TextureScroller.spec.lua new file mode 100644 index 0000000..c6edad0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/TextureScroller.spec.lua @@ -0,0 +1,22 @@ +return function() + local Loading = script.Parent + local App = Loading.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local TextureScroller = require(Loading.TextureScroller) + + it("should create and destroy without errors", function() + local element = Roact.createElement(TextureScroller, { + image = "rbxasset://textures/ui/LuaApp/graphic/shimmer_darkTheme.png", + imageScrollCycleTime = 2, + imageRectOffsetStart = Vector2.new(219, 0), + imageRectOffsetEnd = Vector2.new(-219, 0), + imageRectSize = Vector2.new(219, 250), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/BaseMenu.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/BaseMenu.lua new file mode 100644 index 0000000..6e0d064 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/BaseMenu.lua @@ -0,0 +1,5 @@ +local makeBaseMenu = require(script.Parent.makeBaseMenu) + +local Cell = require(script.Parent.Cell) + +return makeBaseMenu(Cell, "BackgroundUIDefault") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/BaseMenu.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/BaseMenu.spec.lua new file mode 100644 index 0000000..6448a2b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/BaseMenu.spec.lua @@ -0,0 +1,79 @@ +return function() + local Menu = script.Parent + local App = Menu.Parent + local UIBloxRoot = App.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local Images = require(UIBloxRoot.App.ImageSet.Images) + + local BaseMenu = require(script.Parent.BaseMenu) + + describe("lifecycle", function() + it("should mount and unmount without issue with only required props", function() + local element = mockStyleComponent({ + BaseMenu = Roact.createElement(BaseMenu, { + buttonProps = { + { + text = "Test Text", + onActivated = function() + print("test") + end, + } + }, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should mount and unmount without issue with all props", function() + local element = mockStyleComponent({ + BaseMenu = Roact.createElement(BaseMenu, { + buttonProps = { + { + icon = Images["component_assets/circle_17"], + text = "Option 1", + onActivated = function() + print("Option 1 pressed") + end, + }, + { + icon = Images["component_assets/circle_17"], + text = "Option 2", + keyCodeLabel = Enum.KeyCode.Tab, + selected = false, + onActivated = function() + print("Option 2 pressed") + end, + + iconColorOverride = Color3.new(1, 0, 0), + textColorOverride = Color3.new(1, 0, 0), + }, + { + icon = Images["component_assets/circle_17"], + text = "Option 3", + selected = true, + onActivated = function() + print("Option 3 pressed") + end, + }, + }, + + width = UDim.new(0, 300), + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/Cell.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/Cell.lua new file mode 100644 index 0000000..3a37096 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/Cell.lua @@ -0,0 +1,3 @@ +local makeCell = require(script.Parent.makeCell) + +return makeCell("BackgroundUIDefault") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/Cell.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/Cell.spec.lua new file mode 100644 index 0000000..a87bd36 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/Cell.spec.lua @@ -0,0 +1,103 @@ +return function() + local Menu = script.Parent + local App = Menu.Parent + local UIBloxRoot = App.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local Images = require(UIBloxRoot.App.ImageSet.Images) + + local Cell = require(script.Parent.Cell) + + describe("lifecycle", function() + it("should mount and unmount without issue with only required props", function() + local element = mockStyleComponent({ + Cell = Roact.createElement(Cell, { + text = "Test String", + onActivated = function() + print("Test") + end, + + elementHeight = 50, + hasRoundTop = true, + hasRoundBottom = true, + hasDivider = false, + + layoutOrder = 1, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should mount and unmount without issue with all props", function() + local element = mockStyleComponent({ + Cell = Roact.createElement(Cell, { + icon = Images["component_assets/circle_17"], + text = "Test String", + keyCodeLabel = Enum.KeyCode.Tab, + selected = false, + onActivated = function() + print("Test") + end, + iconColorOverride = Color3.new(1, 0, 0), + textColorOverride = Color3.new(1, 0, 0), + + elementHeight = 60, + hasRoundTop = false, + hasRoundBottom = false, + hasDivider = true, + + layoutOrder = 4, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should correctly hold what it's given", function() + local instanceRef = Roact.createRef() + + local folder = Instance.new("Folder") + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + [Roact.Ref] = instanceRef, + }, { + Cell = Roact.createElement(Cell, { + text = "someSampleText", + icon = Images["component_assets/circle_17"], + onActivated = function() + print("Test") + end, + + elementHeight = 50, + hasRoundTop = true, + hasRoundBottom = true, + hasDivider = false, + + layoutOrder = 13, + }), + }), + }) + + local instance = Roact.mount(element, folder) + + local cell = instanceRef.current.Cell + local icon = cell.LeftAlignedContent.Icon + local text = cell.LeftAlignedContent.Text + + expect(cell.LayoutOrder).to.equal(13) + expect(icon.ImageRectOffset).to.equal(Images["component_assets/circle_17"].ImageRectOffset) + expect(text.Text).to.equal("someSampleText") + + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/ContextualMenu.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/ContextualMenu.lua new file mode 100644 index 0000000..bcedf50 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/ContextualMenu.lua @@ -0,0 +1,5 @@ +local makeContextualMenu = require(script.Parent.makeContextualMenu) + +local BaseMenu = require(script.Parent.BaseMenu) + +return makeContextualMenu(BaseMenu, "BackgroundUIDefault") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/ContextualMenu.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/ContextualMenu.spec.lua new file mode 100644 index 0000000..7c0629d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/ContextualMenu.spec.lua @@ -0,0 +1,92 @@ +return function() + local Menu = script.Parent + local App = Menu.Parent + local UIBloxRoot = App.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local Images = require(UIBloxRoot.App.ImageSet.Images) + + local ContextualMenu = require(script.Parent.ContextualMenu) + local MenuDirection = require(script.Parent.MenuDirection) + + describe("lifecycle", function() + it("should mount and unmount without issue with only required props", function() + local element = mockStyleComponent({ + ContextualMenu = Roact.createElement(ContextualMenu, { + buttonProps = { + { + text = "Test Text", + onActivated = function() + print("test") + end, + } + }, + + open = true, + menuDirection = MenuDirection.Down, + openPositionY = UDim.new(0, 20), + + screenSize = Vector2.new(1024, 1024), + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should mount and unmount without issue with all props", function() + local element = mockStyleComponent({ + ContextualMenu = Roact.createElement(ContextualMenu, { + buttonProps = { + { + icon = Images["component_assets/circle_17"], + text = "Option 1", + onActivated = function() + print("Option 1 pressed") + end, + }, + { + icon = Images["component_assets/circle_17"], + text = "Option 2", + keyCodeLabel = Enum.KeyCode.Tab, + selected = false, + onActivated = function() + print("Option 2 pressed") + end, + + iconColorOverride = Color3.new(1, 0, 0), + textColorOverride = Color3.new(1, 0, 0), + }, + { + icon = Images["component_assets/circle_17"], + text = "Option 3", + selected = true, + onActivated = function() + print("Option 3 pressed") + end, + }, + }, + + open = true, + menuDirection = MenuDirection.Up, + openPositionY = UDim.new(0, 20), + + screenSize = Vector2.new(1024, 1024), + + onDismiss = function() + print("Dismiss") + end, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenu.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenu.lua new file mode 100644 index 0000000..12535ba --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenu.lua @@ -0,0 +1,160 @@ +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) + +local Images = require(UIBlox.App.ImageSet.Images) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) + +local DropdownMenuList = require(UIBlox.App.Menu.DropdownMenuList) +local DropdownMenuCell = require(UIBlox.App.Menu.DropdownMenuCell) + +local BUTTON_IMAGE = "component_assets/circle_17_stroke_1" +local COLLAPSE_IMAGE = "truncate_arrows/actions_truncationCollapse" +local EXPAND_IMAGE = "truncate_arrows/actions_truncationExpand" + +local DropdownMenuComponent = Roact.Component:extend("DropdownMenuComponent") + +DropdownMenuComponent.validateProps = t.strictInterface({ + -- Texts shown by the DropdownCell when no value is selected, i.e. the initial state. + placeholder = t.string, + + -- The callback function when a value is selected, passing the selected value as the parameter. + onChange = t.callback, + + -- Size of the DropdownCell. + size = t.UDim2, + + -- Size of the display area, used to determine the position for dropdown menu and the size of dismiss layer. + screenSize = t.Vector2, + + -- If the component is in error state (shows the error style). + errorState = t.optional(t.boolean), + + -- If the component is disabled. + isDisabled = t.optional(t.boolean), + + -- Array of datas for menu cells + cellDatas = t.array(t.strictInterface({ + -- Icon can either be an Image in a ImageSet or a regular image asset + icon = t.optional(t.union(t.table, t.string)), + + -- value of the cell, also the text displayed in this cell + text = t.string, + + -- is the cell is disabled + disabled = t.optional(t.boolean), + + -- A KeyCode to display a keycode hint for, the display string based on the users keyboard is displayed. + keyCodeLabel = t.optional(t.enum(Enum.KeyCode)), + + -- the color to override the default icon color + iconColorOverride = t.optional(t.Color3), + + -- the color to override the default text color + textColorOverride = t.optional(t.Color3), + })) +}) + +function DropdownMenuComponent:init() + self.rootRef = Roact.createRef() + + self:setState({ + menuOpen = false, + selectedValue = self.props.placeholder, + }) + + self.openMenu = function() + self:setState({ + menuOpen = true + }) + end + + self.closeMenu = function() + self:setState({ + menuOpen = false + }) + end + + self.onSelect = function(cell) + local value = cell.LeftAlignedContent.Text.text + self:setState({ + menuOpen = false, + selectedValue = value, + }) + self.props.onChange(value) + end + + self.mapCellData = function(cellData, cellIndex) + local functionalCell = {} + for i,v in pairs(cellData) do + functionalCell[i] = v + end + functionalCell.onActivated = self.onSelect + functionalCell.selected = self.state.selectedValue == functionalCell.text + return functionalCell + end +end + +function DropdownMenuComponent:render() + local cellDatas = self.props.cellDatas + local functionalCells = Cryo.List.map(cellDatas, self.mapCellData) + + local defaultState = "SecondaryDefault" + local hoverState = "SecondaryOnHover" + local textState = "TextEmphasis" + + if self.state.menuOpen then + hoverState = defaultState + end + + if self.props.errorState then + defaultState = "Alert" + hoverState = "Alert" + end + + return Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + }, { + SpawnButton = Roact.createElement(DropdownMenuCell, { + Size = self.props.size, + buttonImage = Images[BUTTON_IMAGE], + buttonStateColorMap = { + [ControlState.Default] = defaultState, + [ControlState.Hover] = hoverState, + }, + contentStateColorMap = { + [ControlState.Default] = textState, + }, + icon = self.state.menuOpen and Images[COLLAPSE_IMAGE] or Images[EXPAND_IMAGE], + text = self.state.selectedValue, + isDisabled = self.props.isDisabled, + isLoading = false, + isActivated = self.state.menuOpen, + hasContent = self.state.selectedValue ~= self.props.placeholder, + userInteractionEnabled = true, + onActivated = self.openMenu, + }), + + DropdownMenuList = Roact.createElement(DropdownMenuList, { + buttonProps = functionalCells, + + zIndex = 2, + open = self.state.menuOpen, + openPositionY = UDim.new(0, 12), + buttonSize = self.props.size, + + closeBackgroundVisible = false, + screenSize = self.props.screenSize, + + onDismiss = self.closeMenu, + }), + }) +end + +return DropdownMenuComponent diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenu.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenu.spec.lua new file mode 100644 index 0000000..af8a59a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenu.spec.lua @@ -0,0 +1,79 @@ +return function() + local Menu = script.Parent + local App = Menu.Parent + local UIBlox = App.Parent + local Roact = require(UIBlox.Parent.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local Images = require(UIBlox.App.ImageSet.Images) + + local DropdownMenu = require(script.Parent.DropdownMenu) + + describe("lifecycle", function() + it("should mount and unmount without issue with only required props", function() + local element = mockStyleComponent({ + DropdownMenu = Roact.createElement(DropdownMenu, { + placeholder = "Placeholder Text", + onChange = print, + size = UDim2.new(0,250,0,48), + screenSize = Vector2.new(500,500), + cellDatas = { + { + text = "Item 1", + }, + { + text = "Item 2", + } + } + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should mount and unmount without issue with all props", function() + local element = mockStyleComponent({ + DropdownMenu = Roact.createElement(DropdownMenu, { + placeholder = "Placeholder Text", + errorState = false, + isDisabled = true, + onChange = print, + size = UDim2.new(0,250,0,48), + screenSize = Vector2.new(500,500), + cellDatas = { + { + text = "Item 1", + icon = Images["component_assets/circle_17_stroke_1"], + disabled = false, + -- A KeyCode to display a keycode hint for, the display string based on the users keyboard is displayed. + keyCodeLabel = Enum.KeyCode.Up, + + iconColorOverride = Color3.new(0,0,0), + textColorOverride = Color3.new(0,0,0), + }, + { + text = "Item 2", + icon = Images["component_assets/circle_17_stroke_1"], + disabled = false, + -- A KeyCode to display a keycode hint for, the display string based on the users keyboard is displayed. + keyCodeLabel = Enum.KeyCode.Up, + + iconColorOverride = Color3.new(0,0,0), + textColorOverride = Color3.new(0,0,0), + } + } + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenuCell.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenuCell.lua new file mode 100644 index 0000000..a5c4878 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenuCell.lua @@ -0,0 +1,252 @@ +--[[ + Create a generic button that can be themed for different state the background and content. +]] +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Core = UIBlox.Core + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) + +local Interactable = require(Core.Control.Interactable) + +local ControlState = require(Core.Control.Enum.ControlState) + +local withStyle = require(Core.Style.withStyle) +local ImageSetComponent = require(Core.ImageSet.ImageSetComponent) +local ShimmerPanel = require(App.Loading.ShimmerPanel) +local IconSize = require(App.Constant.IconSize) +local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel) + +local validateImage = require(Core.ImageSet.Validator.validateImage) +local ButtonGetContentStyle = require(Core.Button.getContentStyle) + +local CONTENT_PADDING = 5 +local DropdownMenuCell = Roact.PureComponent:extend("DropdownMenuCell") + + + +local function getButtonStyle(contentMap, controlState, style, isActive) + local buttonStyle = ButtonGetContentStyle(contentMap, controlState, style) + if (controlState ~= ControlState.Disabled and + controlState ~= ControlState.Pressed) and + isActive then + buttonStyle.Transparency = 0.5 * buttonStyle.Transparency + 0.5 + end + return buttonStyle +end + +local function getContentStyle(contentMap, controlState, style, isActive, hasContent) + local contentStyle = ButtonGetContentStyle(contentMap, controlState, style) + + if (controlState ~= ControlState.Disabled and + controlState ~= ControlState.Pressed) and + (isActive or not hasContent) then + contentStyle.Transparency = 0.5 * contentStyle.Transparency + 0.5 + end + return contentStyle +end + +function DropdownMenuCell:init() + self:setState({ + controlState = ControlState.Initialize, + }) + + self.onStateChanged = function(oldState, newState) + self:setState({ + controlState = newState, + }) + if self.props.onStateChanged then + self.props.onStateChanged(oldState, newState) + end + end +end + +local colorStateMap = t.interface({ + -- The default state theme color class + [ControlState.Default] = t.string, +}) + +DropdownMenuCell.validateProps = t.interface({ + --The icon of the button + icon = t.optional(validateImage), + + --The text of the button + text = t.optional(t.string), + + --The image being used as the background of the button + buttonImage = validateImage, + + --The theme color class mapping for different button states + buttonStateColorMap = colorStateMap, + + --The theme color class mapping for different content tates + contentStateColorMap = colorStateMap, + + --The theme color class mapping for different text tates + textStateColorMap = t.optional(colorStateMap), + + --The theme color class mapping for different icon tates + iconStateColorMap = t.optional(colorStateMap), + + --Is the button disabled + isDisabled = t.optional(t.boolean), + + --Is the button activated + isActivated = t.optional(t.boolean), + + --Does the button hold a selected value + hasContent = t.optional(t.boolean), + + --Is the button loading + isLoading = t.optional(t.boolean), + + --The activated callback for the button + onActivated = t.callback, + + --THe state change callback for the button + onStateChanged = t.optional(t.callback), + + --A Boolean value that determines whether user events are ignored and sink input + userInteractionEnabled = t.optional(t.boolean), + + -- Note that this component can accept all valid properties of the Roblox ImageButton instance +}) + +DropdownMenuCell.defaultProps = { + isDisabled = false, + isLoading = false, + SliceCenter = Rect.new(8, 8, 9, 9), +} + +function DropdownMenuCell:render() + return withStyle(function(style) + + assert(t.table(style), "Style provider is missing.") + + local currentState = self.state.controlState + + local text = self.props.text + local icon = self.props.icon + local isLoading = self.props.isLoading + local isDisabled = self.props.isDisabled + + local buttonStateColorMap = self.props.buttonStateColorMap + local contentStateColorMap = self.props.contentStateColorMap + local textStateColorMap = self.props.textStateColorMap or contentStateColorMap + local iconStateColorMap = self.props.iconStateColorMap or contentStateColorMap + + if isLoading then + isDisabled = true + end + + local buttonStyle = getButtonStyle(buttonStateColorMap, currentState, style, self.props.isActivated) + local textStyle = text and getContentStyle( + textStateColorMap, + currentState, + style, + self.props.isActivated, + self.props.hasContent) + local iconStyle = icon and getContentStyle( + iconStateColorMap, + currentState, + style, + self.props.isActivated, + true) + local buttonImage = self.props.buttonImage + local fontStyle = style.Font.Header2 + + local buttonContentLayer + if isLoading then + buttonContentLayer = { + isLoadingShimmer = Roact.createElement(ShimmerPanel, { + Size = UDim2.fromScale(1,1), + }) + } + else + buttonContentLayer = self.props[Roact.Children] or { + TextContainer = Roact.createElement("Frame", { + Size = UDim2.fromScale(1,1), + BackgroundTransparency = 1, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, CONTENT_PADDING), + }), + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, 12), + }), + Text = text and Roact.createElement(GenericTextLabel, { + BackgroundTransparency = 1, + Text = text, + fontStyle = fontStyle, + colorStyle = textStyle, + LayoutOrder = 1, + }) or nil, + }), + IconContainer = Roact.createElement("Frame",{ + Size = UDim2.fromScale(1,1), + BackgroundTransparency = 1, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, CONTENT_PADDING), + }), + Padding = Roact.createElement("UIPadding", { + PaddingRight = UDim.new(0, 20), + }), + Icon = icon and Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(IconSize.Regular, IconSize.Regular), + BackgroundTransparency = 1, + Image = icon, + ImageColor3 = iconStyle.Color, + ImageTransparency = iconStyle.Transparency, + LayoutOrder = 2, + }) or nil, + }), + } + end + + local PROPS_FILTER = { + isActivated = Cryo.None, + hasContent = Cryo.None, + icon = Cryo.None, + text = Cryo.None, + buttonImage = Cryo.None, + buttonStateColorMap = Cryo.None, + contentStateColorMap = Cryo.None, + textStateColorMap = Cryo.None, + iconStateColorMap = Cryo.None, + onActivated = Cryo.None, + isLoading = Cryo.None, + [Roact.Children] = Cryo.None, + isDisabled = isDisabled, + onStateChanged = self.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + Image = buttonImage, + ScaleType = Enum.ScaleType.Slice, + ImageColor3 = buttonStyle.Color, + ImageTransparency = buttonStyle.Transparency, + BackgroundTransparency = 1, + AutoButtonColor = false, + [Roact.Event.Activated] = self.props.onActivated, + } + + return Roact.createElement(Interactable, Cryo.Dictionary.join(self.props, PROPS_FILTER), { + ButtonContent = Roact.createElement("Frame", { + Size = UDim2.fromScale(1,1), + BackgroundTransparency = 1, + }, buttonContentLayer) + }) + end) +end +return DropdownMenuCell diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenuList.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenuList.lua new file mode 100644 index 0000000..838c294 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenuList.lua @@ -0,0 +1,172 @@ +-- https://share.goabstract.com/b4b09f34-438a-4d5e-ba7f-8f1f0657f4dd + +local GuiService = game:GetService("GuiService") + +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local BaseMenu = require(script.Parent.BaseMenu) +local validateButtonProps = require(script.Parent.validateButtonProps) + +local dropdownMenuListComponent = Roact.PureComponent:extend("DropdownMenuList") + +dropdownMenuListComponent.validateProps = t.strictInterface({ + buttonProps = validateButtonProps, + + zIndex = t.optional(t.integer), + open = t.boolean, + openPositionY = t.UDim, + + closeBackgroundVisible = t.optional(t.boolean), + screenSize = t.Vector2, + + onDismiss = t.optional(t.callback), + buttonSize = t.UDim2 +}) + +dropdownMenuListComponent.defaultProps = { + zIndex = 2, + closeBackgroundVisible = false, +} + +function dropdownMenuListComponent:init() + self:setState({ + absoluteSize = Vector2.new(0, 0), + absolutePosition = Vector2.new(0, 0), + visible = false + }) + + self.setAbsoluteSize = function(rbx) + self:setState({ + absoluteSize = rbx.AbsoluteSize, + }) + end + + self.setAbsolutePosition = function(rbx) + self:setState({ + absolutePosition = rbx.AbsolutePosition, + }) + end + + self.dismissMenu = function() + if self.state.visible then + self:setState({ + visible = false + }) + self.props.onDismiss() + end + end +end + +function dropdownMenuListComponent:render() + return withStyle(function(stylePalette) + local topCornerInset, _ = GuiService:GetGuiInset() + local absolutePosition = self.state.absolutePosition + topCornerInset + + local anchorPointY = 0 + local menuYScale = 1 + local menuYOffset = self.props.buttonSize.Y + local menuWidth = self.props.buttonSize.X + + if self.state.absolutePosition.Y > self.props.screenSize.Y / 2 then + anchorPointY = 1 + menuYScale = -1 + menuYOffset = UDim.new(0,0) + end + + local menuPositionY + if menuYScale == 1 then + menuPositionY = menuYOffset + self.props.openPositionY + else + menuPositionY = menuYOffset - self.props.openPositionY + end + local menuPosition = UDim2.new(0,0,menuPositionY.Scale,menuPositionY.Offset) + + if self.props.screenSize.X < 640 then + anchorPointY = 1 + menuPosition = UDim2.new( + -self.props.buttonSize.X.Scale / 2, + -absolutePosition.X + self.props.screenSize.X / 2 - self.props.buttonSize.X.Offset / 2, + 0, + self.props.screenSize.Y - absolutePosition.Y -24 + ) + end + + local backgroundTransparency = stylePalette.Theme.Overlay.Transparency + if not self.props.closeBackgroundVisible then + backgroundTransparency = 1 + end + + + + return Roact.createElement("Frame", { + Size = UDim2.fromOffset(self.props.screenSize.X, self.props.screenSize.Y), + BackgroundTransparency = 1, + Visible = self.state.visible, + ZIndex = self.props.zIndex, + + [Roact.Change.AbsoluteSize] = self.setAbsoluteSize, + + [Roact.Change.AbsolutePosition] = self.setAbsolutePosition, + }, { + Background = Roact.createElement("TextButton", { + ZIndex = 1, + Text = "", + BorderSizePixel = 0, + BackgroundColor3 = stylePalette.Theme.Overlay.Color, + BackgroundTransparency = backgroundTransparency, + AutoButtonColor = false, + + Position = UDim2.fromOffset(-absolutePosition.X, -absolutePosition.Y), + Size = UDim2.fromOffset(self.props.screenSize.X, self.props.screenSize.Y), + + [Roact.Event.Activated] = self.dismissMenu, + }), + + PositionFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + Position = menuPosition, + ZIndex = 2, + }, { + BaseMenu = Roact.createElement(BaseMenu, { + buttonProps = self.props.buttonProps, + + width = menuWidth, + position = UDim2.fromScale(0, 0), + anchorPoint = Vector2.new(0, anchorPointY), + }), + }), + }) + end) +end + +function dropdownMenuListComponent:didMount() + if self.props.open then + self:setState({ + visible = true + }) + end +end + +function dropdownMenuListComponent:didUpdate(previousProps, previousState) + if self.props.open ~= previousProps.open then + if self.props.open then + self:setState({ + visible = true + }) + else + self:setState({ + visible = false + }) + end + end +end + +return dropdownMenuListComponent diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/KeyLabel.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/KeyLabel.lua new file mode 100644 index 0000000..6787cff --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/KeyLabel.lua @@ -0,0 +1,195 @@ +local TextService = game:GetService("TextService") +local UserInputService = game:GetService("UserInputService") + +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local Images = require(UIBlox.App.ImageSet.Images) + +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) + +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +-- Additional width for keys with centered text like [Backspace], [Enter], etc +local CENTERED_EXTRA_WIDTH = 14 * 2 + +-- Side padding for off-centered keys [Shift ] +local OFF_CENTER_PADDING = 9 + +local TEXT_CENTER_OFFSET = -1 + +local KEY_LABEL_HEIGHT = 36 + +-- Big and small font sizes for key text. +local BIG = "CaptionHeader" +local SMALL = "Footer" + +local CONTENT_OVERRIDE_MAP = { + [Enum.KeyCode.Escape] = {text = "ESC", fontKey = SMALL, width = 36}, + [Enum.KeyCode.Space] = {text = "Space", width = 92}, + [Enum.KeyCode.LeftShift] = {text = "Shift", width = 66, alignment = Enum.TextXAlignment.Left}, + [Enum.KeyCode.LeftControl] = {text = "Ctrl", width = 66, alignment = Enum.TextXAlignment.Left}, + [Enum.KeyCode.Tab] = {text = "Tab", width = 56}, + + [Enum.KeyCode.LeftSuper] = {text = "Command"}, + [Enum.KeyCode.LeftMeta] = {text = "fn"}, + [Enum.KeyCode.LeftAlt] = {text = "Opt"}, + + [Enum.KeyCode.Tilde] = {text = "~", fontKey = BIG}, + + [Enum.KeyCode.F10] = {fontKey = BIG, width = 36}, + [Enum.KeyCode.F11] = {fontKey = BIG, width = 36}, + + [Enum.KeyCode.Up] = {image = Images["icons/controls/keys/arrowDown"]}, + [Enum.KeyCode.Down] = {image = Images["icons/controls/keys/arrowLeft"]}, + [Enum.KeyCode.Left] = {image = Images["icons/controls/keys/arrowRight"]}, + [Enum.KeyCode.Right] = {image = Images["icons/controls/keys/arrowUp"]}, +} + +local KeyLabel = Roact.PureComponent:extend("KeyLabel") + +KeyLabel.validateProps = t.strictInterface({ + keyCode = t.enum(Enum.KeyCode), + + anchorPoint = t.optional(t.Vector2), + position = t.optional(t.UDim2), + layoutOrder = t.optional(t.integer), + + [Roact.Change.AbsoluteSize] = t.optional(t.callback), +}) + +local cachedKeyCodeStrings = {} +local function getStringForKeyCode(keyCode) + if cachedKeyCodeStrings[keyCode] == nil then + cachedKeyCodeStrings[keyCode] = UserInputService:GetStringForKeyCode(keyCode) + end + return cachedKeyCodeStrings[keyCode] +end + +function KeyLabel:getLabelWidthAndContent(style) + local override = CONTENT_OVERRIDE_MAP[self.props.keyCode] + local font = style.Font + + if override and override.image then + local width = 36 + local content = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = override.image, + ImageColor3 = style.Theme.IconEmphasis.Color, + ImageTransparency = style.Theme.IconEmphasis.Transparency, + Position = UDim2.new(0.5, 0, 0.5, -1), + Size = UDim2.fromOffset(16, 16), + }) + + return width, content + else + local text, fontKey, width, alignment do + if override and override.text then + text = override.text + else + local keyString = getStringForKeyCode(self.props.keyCode) + if keyString and keyString ~= "" then + text = keyString + else + text = self.props.keyCode.Name + end + end + + local textIsShort = text:len() < 3 + + if override and override.fontKey then + fontKey = override.fontKey + else + fontKey = textIsShort and BIG or SMALL + end + + if override and override.width then + width = override.width + elseif textIsShort then + width = 36 + else + local fontStyle = style.Font[fontKey] + + local textSize = fontStyle.RelativeSize * style.Font.BaseSize + local fontType = fontStyle.Font + + local textWidth = TextService:GetTextSize( + text, + textSize, + fontType, + Vector2.new(math.huge, 36) + ).X + + width = textWidth + CENTERED_EXTRA_WIDTH + end + + if override and override.alignment then + alignment = override.alignment + end + end + + local contentFont = font[fontKey] + local contentTextSize = font.BaseSize * contentFont.RelativeSize + + local content = Roact.createElement(GenericTextLabel, { + colorStyle = style.Theme.TextDefault, + fontStyle = contentFont, + + Text = text, + TextSize = contentTextSize, + + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromOffset(0, TEXT_CENTER_OFFSET), + TextXAlignment = alignment, + }) + + return width, content, alignment + end +end + +function KeyLabel:render() + return withStyle(function(style) + local borderTheme = style.Theme.UIEmphasis + + local width, content, alignment = self:getLabelWidthAndContent(style) + + local padding + if alignment then + padding = OFF_CENTER_PADDING + end + + return Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + + ImageTransparency = borderTheme.Transparency, + ImageColor3 = borderTheme.Color, + + Image = Images["icons/controls/keys/key_single"], + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(Vector2.new(9, 7), Vector2.new(26, 26)), + + Size = UDim2.fromOffset(width, KEY_LABEL_HEIGHT), + Position = self.props.Position, + AnchorPoint = self.props.AnchorPoint, + + LayoutOrder = self.props.LayoutOrder, + + [Roact.Change.AbsoluteSize] = self.props[Roact.Change.AbsoluteSize] + }, { + Padding = padding and Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, padding), + PaddingRight = UDim.new(0, padding) + }), + LabelContent = content + }) + end) +end + +return KeyLabel \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/KeyLabel.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/KeyLabel.spec.lua new file mode 100644 index 0000000..b775759 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/KeyLabel.spec.lua @@ -0,0 +1,81 @@ +return function() + local Menu = script.Parent + local App = Menu.Parent + local UIBloxRoot = App.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + + local KeyLabel = require(script.Parent.KeyLabel) + + describe("lifecycle", function() + it("should mount and unmount without issue with only required props", function() + local element = mockStyleComponent({ + KeyLabel = Roact.createElement(KeyLabel, { + keyCode = Enum.KeyCode.X, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should mount and unmount without issue with all props", function() + local element = mockStyleComponent({ + KeyLabel = Roact.createElement(KeyLabel, { + keyCode = Enum.KeyCode.X, + + anchorPoint = Vector2.new(0.5, 0.5), + position = UDim2.fromScale(0.5, 0.5), + layoutOrder = 1, + + [Roact.Change.AbsoluteSize] = function(rbx) + print("Size changed!") + end, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should mount and unmount without issue when using special override keycodes", function() + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + }, { + EscapeKeyLabel = Roact.createElement(KeyLabel, { + keyCode = Enum.KeyCode.Escape, + }), + + SpaceKeyLabel = Roact.createElement(KeyLabel, { + keyCode = Enum.KeyCode.Space, + }), + + LeftControlKeyLabel = Roact.createElement(KeyLabel, { + keyCode = Enum.KeyCode.LeftControl, + }), + + F10KeyLabel = Roact.createElement(KeyLabel, { + keyCode = Enum.KeyCode.F10, + }), + + DownKeyLabel = Roact.createElement(KeyLabel, { + keyCode = Enum.KeyCode.Down, + }), + }) + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/MenuDirection.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/MenuDirection.lua new file mode 100644 index 0000000..791e121 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/MenuDirection.lua @@ -0,0 +1,10 @@ +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate(script.Name, { + "Up", + "Down", +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayBaseMenu.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayBaseMenu.lua new file mode 100644 index 0000000..5de3beb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayBaseMenu.lua @@ -0,0 +1,5 @@ +local makeBaseMenu = require(script.Parent.makeBaseMenu) + +local OverlayCell = require(script.Parent.OverlayCell) + +return makeBaseMenu(OverlayCell, "BackgroundUIContrast") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayCell.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayCell.lua new file mode 100644 index 0000000..4c7166d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayCell.lua @@ -0,0 +1,3 @@ +local makeCell = require(script.Parent.makeCell) + +return makeCell("BackgroundUIContrast") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayContextualMenu.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayContextualMenu.lua new file mode 100644 index 0000000..c05d046 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayContextualMenu.lua @@ -0,0 +1,5 @@ +local makeContextualMenu = require(script.Parent.makeContextualMenu) + +local OverlayBaseMenu = require(script.Parent.OverlayBaseMenu) + +return makeContextualMenu(OverlayBaseMenu, "BackgroundUIContrast") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/__stories__/CellTypes.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/__stories__/CellTypes.story.lua new file mode 100644 index 0000000..f782680 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/__stories__/CellTypes.story.lua @@ -0,0 +1,162 @@ +local Menu = script.Parent.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local StoryView = require(Packages.StoryComponents.StoryView) +local StoryItem = require(Packages.StoryComponents.StoryItem) + +local Cell = require(Menu.Cell) + +local CellTypesOverviewComponent = Roact.Component:extend("CellTypesOverviewComponent") + +function CellTypesOverviewComponent:render() + return Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + }, { + Overview = Roact.createElement("ScrollingFrame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + }, { + Grid = Roact.createElement("UIGridLayout", { + CellSize = UDim2.fromOffset(300, 200), + FillDirectionMaxCells = 2, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Padding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, 10), + PaddingLeft = UDim.new(0, 20), + }), + + TextOnly = Roact.createElement(StoryItem, { + size = UDim2.fromOffset(300, 128), + layoutOrder = 1, + title = "Text Only", + subTitle = "", + showDivider = true, + }, { + Button = Roact.createElement(Cell, { + text = "Title Case", + onActivated = function() + + end, + + elementHeight = 56, + hasRoundTop = false, + hasRoundBottom = false, + hasDivider = true, + + layoutOrder = 2, + }), + }), + + TextSelected = Roact.createElement(StoryItem, { + size = UDim2.fromOffset(300, 128), + layoutOrder = 2, + title = "Text Selected", + subTitle = "", + showDivider = true, + }, { + Button = Roact.createElement(Cell, { + text = "Title Case", + selected = true, + onActivated = function() + + end, + + elementHeight = 56, + hasRoundTop = false, + hasRoundBottom = false, + hasDivider = true, + + layoutOrder = 2, + }), + }), + + TextAndIcon = Roact.createElement(StoryItem, { + size = UDim2.fromOffset(300, 128), + layoutOrder = 3, + title = "Text and Icon", + subTitle = "", + showDivider = true, + }, { + Button = Roact.createElement(Cell, { + icon = Images["icons/menu/friends"], + text = "Title Case", + onActivated = function() + + end, + + elementHeight = 56, + hasRoundTop = false, + hasRoundBottom = false, + hasDivider = true, + + layoutOrder = 2, + }), + }), + + CellWithKeyLabel = Roact.createElement(StoryItem, { + size = UDim2.fromOffset(300, 128), + layoutOrder = 4, + title = "Cell with KeyLabel", + subTitle = "", + showDivider = true, + }, { + Button = Roact.createElement(Cell, { + icon = Images["icons/menu/friends"], + text = "Title Case", + keyCodeLabel = Enum.KeyCode.E, + onActivated = function() + + end, + + elementHeight = 56, + hasRoundTop = false, + hasRoundBottom = false, + hasDivider = true, + + layoutOrder = 2, + }), + }), + + Disabled = Roact.createElement(StoryItem, { + size = UDim2.fromOffset(300, 128), + layoutOrder = 5, + title = "Disabled", + subTitle = "", + showDivider = true, + }, { + Button = Roact.createElement(Cell, { + icon = Images["icons/menu/friends"], + text = "Title Case", + onActivated = function() + + end, + + elementHeight = 56, + hasRoundTop = false, + hasRoundBottom = false, + hasDivider = true, + + disabled = true, + layoutOrder = 2, + }), + }), + }) + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(CellTypesOverviewComponent), + }), target, "CellTypes") + + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeBaseMenu.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeBaseMenu.lua new file mode 100644 index 0000000..6a59d50 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeBaseMenu.lua @@ -0,0 +1,178 @@ +-- https://share.goabstract.com/b4b09f34-438a-4d5e-ba7f-8f1f0657f4dd + +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local RoactGamepad = require(Packages.RoactGamepad) +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) +local validateButtonProps = require(script.Parent.validateButtonProps) + +local Images = require(Packages.UIBlox.App.ImageSet.Images) +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local MENU_BACKGROUND_ASSET = Images["component_assets/circle_17"] + +local ELEMENT_HEIGHT = 56 +local MAXIMUM_ELEMENTS = 7 +local MAXIMUM_HEIGHT = ELEMENT_HEIGHT * (MAXIMUM_ELEMENTS + 0.5) + +local ROUNDED_CORNER_SIZE = 4 + +local SCROLLBAR_OFFSET = 4 + +local function makeBaseMenu(cellComponent, backgroundThemeKey) + local baseMenuComponent = Roact.PureComponent:extend("BaseMenuFor" ..backgroundThemeKey) + + baseMenuComponent.validateProps = t.strictInterface({ + buttonProps = validateButtonProps, + + width = t.optional(t.UDim), + -- The position can either be passed as a UDim2 or a Roact binding. + position = t.optional(t.union(t.UDim2, t.table)), + anchorPoint = t.optional(t.Vector2), + layoutOrder = t.optional(t.number), + }) + + baseMenuComponent.defaultProps = { + width = UDim.new(1, 0), + position = UDim2.new(0, 0, 0, 0), + } + + function baseMenuComponent:init() + if UIBloxConfig.enableExperimentalGamepadSupport then + self.gamepadRefs = RoactGamepad.createRefCache() + end + end + + function baseMenuComponent:render() + local menuHeight = #self.props.buttonProps * ELEMENT_HEIGHT + local needsScrollbar = false + if menuHeight >= MAXIMUM_HEIGHT then + menuHeight = MAXIMUM_HEIGHT + needsScrollbar = true + end + + local children = {} + for index, cellProps in ipairs(self.props.buttonProps) do + local mergedProps = Cryo.Dictionary.join(cellProps, { + elementHeight = ELEMENT_HEIGHT, + hasRoundTop = index == 1 and not needsScrollbar, + hasRoundBottom = index == #self.props.buttonProps and not needsScrollbar, + hasDivider = index < #self.props.buttonProps, + layoutOrder = index, + }) + + if UIBloxConfig.enableExperimentalGamepadSupport then + children["cell " .. index] = Roact.createElement(RoactGamepad.Focusable.Frame, { + Size = UDim2.new(self.props.width, UDim.new(0, ELEMENT_HEIGHT)), + BackgroundTransparency = 1, + LayoutOrder = index, + + [Roact.Ref] = self.gamepadRefs[index], + NextSelectionUp = index > 1 and self.gamepadRefs[index - 1] or nil, + NextSelectionDown = index < #self.props.buttonProps and self.gamepadRefs[index + 1] or nil, + inputBindings = { + Activated = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonA, cellProps.onActivated), + }, + }, { + Cell = Roact.createElement(cellComponent, mergedProps) + }) + else + children["cell " .. index] = Roact.createElement(cellComponent, mergedProps) + end + end + + children.layout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + local imageSize = MENU_BACKGROUND_ASSET.ImageRectSize + local imageOffset = MENU_BACKGROUND_ASSET.ImageRectOffset + + local imageRectOffset = imageOffset + local imageWidth = imageSize.X + local halfImageWidth = imageWidth / 2 + + return Roact.createElement("Frame", { + AnchorPoint = self.props.anchorPoint, + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + Size = UDim2.new(self.props.width.Scale, self.props.width.Offset, 0, menuHeight), + Position = self.props.position, + }, { + TopRoundedCorner = needsScrollbar and Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, ROUNDED_CORNER_SIZE), + Position = UDim2.fromScale(0, 0), + + Image = MENU_BACKGROUND_ASSET.Image, + ScaleType = Enum.ScaleType.Slice, + SliceScale = 0.5 / Images.ImagesResolutionScale, + SliceCenter = Rect.new(halfImageWidth - 1, halfImageWidth - 1, halfImageWidth +1, halfImageWidth), + ImageRectSize = Vector2.new(imageWidth, halfImageWidth), + ImageRectOffset = imageRectOffset, + + ImageTransparency = theme[backgroundThemeKey].Transparency, + ImageColor3 = theme[backgroundThemeKey].Color, + }), + + -- We turn off ClipsDescendants on the ScrollingFrame to allow the scroll bar to be offset over the contents. + ClippingFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, needsScrollbar and -(ROUNDED_CORNER_SIZE * 2) or 0), + Position = UDim2.fromScale(0, 0.5), + AnchorPoint = Vector2.new(0, 0.5), + ClipsDescendants = true, + }, { + ScrollingFrame = Roact.createElement("ScrollingFrame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, needsScrollbar and -SCROLLBAR_OFFSET or 0, 1, 0), + BorderSizePixel = 0, + ScrollBarThickness = 4, + ScrollBarImageColor3 = theme.UIEmphasis.Color, + ScrollBarImageTransparency = theme.UIEmphasis.Transparency, + ScrollingDirection = Enum.ScrollingDirection.Y, + CanvasSize = UDim2.new( + 1, + 0, + 0, + #self.props.buttonProps * ELEMENT_HEIGHT + ), + ClipsDescendants = false, + }, children), + }), + + BottomRoundedCorner = needsScrollbar and Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, ROUNDED_CORNER_SIZE), + Position = UDim2.fromScale(0, 1), + AnchorPoint = Vector2.new(0, 1), + + Image = MENU_BACKGROUND_ASSET.Image, + ScaleType = Enum.ScaleType.Slice, + SliceScale = 0.5 / Images.ImagesResolutionScale, + SliceCenter = Rect.new(halfImageWidth - 1, 0, halfImageWidth + 1, 1), + ImageRectSize = Vector2.new(imageWidth, halfImageWidth), + ImageRectOffset = imageOffset + Vector2.new(0, halfImageWidth), + + ImageTransparency = theme[backgroundThemeKey].Transparency, + ImageColor3 = theme[backgroundThemeKey].Color, + }), + }) + end) + end + + return baseMenuComponent +end + +return makeBaseMenu \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeCell.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeCell.lua new file mode 100644 index 0000000..b4d376e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeCell.lua @@ -0,0 +1,324 @@ +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local ImageSetComponent = require(Packages.UIBlox.Core.ImageSet.ImageSetComponent) +local Images = require(Packages.UIBlox.App.ImageSet.Images) +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local divideTransparency = require(UIBlox.Utility.divideTransparency) + +local Controllable = require(Packages.UIBlox.Core.Control.Controllable) +local ControlState = require(Packages.UIBlox.Core.Control.Enum.ControlState) + +local KeyLabel = require(script.Parent.KeyLabel) + +local TEXT_ONLY_PADDING = 24 --Text only padding at the start and end of cells +local ELEMENT_PADDING = 12 --Padding between elements +local SELECTED_ICON_PADDING = 24 --Padding for selected icons at the end of cells +local KEYLABEL_PADDING = 16 --Padding for key labels at the end of cells + +local ICON_SIZE = 36 +local SELECTED_ICON_SIZE = 16 + +local CELL_BACKGROUND_ASSET = Images["component_assets/circle_17"] + +local function makeCell(backgroundThemeKey) + local cellComponent = Roact.PureComponent:extend("CellFor" ..backgroundThemeKey) + + cellComponent.validateProps = t.strictInterface({ + -- Icon can either be an Image in a ImageSet or a regular image asset + icon = t.optional(t.union(t.table, t.string)), + text = t.string, + onActivated = t.callback, + + -- A KeyCode to display a keycode hint for, the display string based on the users keyboard is displayed. + keyCodeLabel = t.optional(t.enum(Enum.KeyCode)), + selected = t.optional(t.boolean), + + iconColorOverride = t.optional(t.Color3), + textColorOverride = t.optional(t.Color3), + + elementHeight = t.integer, + hasRoundTop = t.boolean, + hasRoundBottom = t.boolean, + + hasDivider = t.boolean, + disabled = t.optional(t.boolean), + + layoutOrder = t.integer, + }) + + cellComponent.defaultProps = { + selected = false, + disabled = false, + } + + function cellComponent:init() + self.state = { + controlState = ControlState.Default, + + keyLabelWidth = 0, + } + + self.keyLabelSizeChanged = function(rbx) + self:setState({ + keyLabelWidth = rbx.AbsoluteSize.X, + }) + end + + self.setControlState = function(controlState) + self:setState({ + controlState = controlState, + }) + end + end + + function cellComponent:getImageProperties() + local imageSize = CELL_BACKGROUND_ASSET.ImageRectSize + local imageOffset = CELL_BACKGROUND_ASSET.ImageRectOffset + + local xOffset = 8 * Images.ImagesResolutionScale + local yOffset = 8 * Images.ImagesResolutionScale + local imageCenter = Rect.new(xOffset, yOffset, imageSize.x - xOffset, imageSize.y - yOffset) + + local imageWidth = imageSize.X + local halfImageWidth = math.floor(imageWidth / 2) + + local imageRectSize, imageRectOffset, sliceCenter + + if self.props.hasRoundTop and self.props.hasRoundBottom then + imageRectSize = imageSize + imageRectOffset = imageOffset + sliceCenter = imageCenter + elseif self.props.hasRoundTop then + imageRectSize = Vector2.new(imageWidth, halfImageWidth) + imageRectOffset = imageOffset + sliceCenter = Rect.new(halfImageWidth - 1, halfImageWidth - 1, halfImageWidth +1, halfImageWidth) + elseif self.props.hasRoundBottom then + imageRectSize = Vector2.new(imageWidth, halfImageWidth) + imageRectOffset = imageOffset + Vector2.new(0, halfImageWidth) + sliceCenter = Rect.new(halfImageWidth - 1, 0, halfImageWidth + 1, 1) + else + imageRectSize = Vector2.new(1, 1) + imageRectOffset = imageOffset + Vector2.new(halfImageWidth, halfImageWidth) + sliceCenter = Rect.new(0, 0, 0, 0) + end + + return imageRectSize, imageRectOffset, sliceCenter + end + + function cellComponent:render() + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local font = stylePalette.Font + + local leftPadding = TEXT_ONLY_PADDING + if self.props.icon then + leftPadding = ELEMENT_PADDING + end + local rightPadding = 0 + if self.props.keyCodeLabel then + rightPadding = KEYLABEL_PADDING + elseif self.props.selected then + rightPadding = SELECTED_ICON_PADDING + end + + local overlayTheme = { + Color = Color3.new(1, 1, 1), + Transparency = 1, + } + + if self.state.controlState == ControlState.Pressed then + overlayTheme = theme.BackgroundOnPress + elseif self.state.controlState == ControlState.Hover then + overlayTheme = theme.BackgroundOnHover + end + + local imageRectSize, imageRectOffset, sliceCenter = self:getImageProperties() + + local textLengthOffset = 0 + local textOnly = true + if self.props.icon then + textOnly = false + leftPadding = ELEMENT_PADDING + textLengthOffset = ELEMENT_PADDING + ICON_SIZE + end + + if self.props.selected then + textOnly = false + textLengthOffset = textLengthOffset + SELECTED_ICON_SIZE + SELECTED_ICON_PADDING + end + + if self.props.keyCodeLabel then + textOnly = false + textLengthOffset = textLengthOffset + KEYLABEL_PADDING + self.state.keyLabelWidth + end + + -- Add start and end padding for text. + if textOnly then + textLengthOffset = textLengthOffset + TEXT_ONLY_PADDING * 2 + else + textLengthOffset = textLengthOffset + ELEMENT_PADDING * 2 + end + + local textTheme = theme.TextEmphasis + if self.props.textColorOverride then + textTheme = { + Color = self.props.textColorOverride, + Transparency = theme.TextEmphasis.Transparency + } + end + if self.state.controlState == ControlState.Pressed or self.props.disabled then + textTheme = { + Color = textTheme.Color, + Transparency = divideTransparency(theme.TextEmphasis.Transparency, 2) + } + end + + local cellStyle = theme[backgroundThemeKey] + + return Roact.createElement(Controllable, { + controlComponent = { + component = "ImageButton", + props = { + Size = UDim2.new(1, 0, 0, self.props.elementHeight), + BackgroundTransparency = 1, + + Image = CELL_BACKGROUND_ASSET.Image, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = sliceCenter, + ImageRectSize = imageRectSize, + ImageRectOffset = imageRectOffset, + SliceScale = 1 / Images.ImagesResolutionScale, + + ImageTransparency = cellStyle.Transparency, + ImageColor3 = cellStyle.Color, + AutoButtonColor = false, + LayoutOrder = self.props.layoutOrder, + + BorderSizePixel = 0, + + [Roact.Event.Activated] = self.props.onActivated, + }, + children = { + Divider = Roact.createElement("Frame", { + BackgroundColor3 = theme.Divider.Color, + BackgroundTransparency = theme.Divider.Transparency, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 1), + Position = UDim2.fromScale(0, 1), + AnchorPoint = Vector2.new(0, 1), + Visible = self.props.hasDivider, + }), + + StateOverlay = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + + Image = CELL_BACKGROUND_ASSET.Image, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = sliceCenter, + ImageRectSize = imageRectSize, + ImageRectOffset = imageRectOffset, + SliceScale = 1 / Images.ImagesResolutionScale, + + ImageColor3 = overlayTheme.Color, + ImageTransparency = overlayTheme.Transparency, + BorderSizePixel = 0, + Size = UDim2.fromScale(1, 1), + ZIndex = 2, + }), + + LeftAlignedContent = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + }, { + Layout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, ELEMENT_PADDING), + }), + + LeftPadding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, leftPadding) + }), + + Icon = self.props.icon and Roact.createElement(ImageSetComponent.Label, { + Image = self.props.icon, + Size = UDim2.fromOffset(ICON_SIZE, ICON_SIZE), + BackgroundTransparency = 1, + ImageColor3 = self.props.iconColorOverride or theme.IconEmphasis.Color, + ImageTransparency = divideTransparency( + theme.IconEmphasis.Transparency, + self.props.disabled and 2 or 1 + ), + LayoutOrder = 1, + }), + + Text = Roact.createElement(GenericTextLabel, { + fontStyle = font.Header2, + colorStyle = textTheme, + + BackgroundTransparency = 1, + Size = UDim2.new(1, -textLengthOffset, 1, 0), + Text = self.props.text, + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Left, + LayoutOrder = 2, + }), + }), + + RightAlignedContent = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + }, { + Layout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Right, + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, ELEMENT_PADDING), + }), + + RightPadding = Roact.createElement("UIPadding", { + PaddingRight = UDim.new(0, rightPadding) + }), + + KeyLabel = self.props.keyCodeLabel and Roact.createElement(KeyLabel, { + keyCode = self.props.keyCodeLabel, + + layoutOrder = 2, + + [Roact.Change.AbsoluteSize] = self.keyLabelSizeChanged + }), + + SelectedIcon = Roact.createElement(ImageSetComponent.Label, { + Image = Images["icons/status/success_small"], + Size = UDim2.fromOffset(SELECTED_ICON_SIZE, SELECTED_ICON_SIZE), + LayoutOrder = 1, + BackgroundTransparency = 1, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Visible = self.props.selected + }), + }), + }, + }, + onStateChanged = function(_, newState) + self.setControlState(newState) + end, + isDisabled = self.props.disabled, + }) + end) + end + + return cellComponent +end + +return makeCell \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeContextualMenu.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeContextualMenu.lua new file mode 100644 index 0000000..82a9871 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeContextualMenu.lua @@ -0,0 +1,190 @@ +-- https://share.goabstract.com/b4b09f34-438a-4d5e-ba7f-8f1f0657f4dd + +local GuiService = game:GetService("GuiService") + +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Otter = require(Packages.Otter) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) + +local MenuDirection = require(script.Parent.MenuDirection) +local validateButtonProps = require(script.Parent.validateButtonProps) + +local MOTOR_OPTIONS_OPEN = { + frequency = 4, + dampingRatio = 1, +} + +local MOTOR_OPTIONS_CLOSE = { + frequency = 5.4, + dampingRatio = 1, +} + +local CONTEXT_MENU_PADDING = 12 +local LARGE_WIDTH_SIZE = 300 +local LARGE_WIDTH_THRESHOLD = 640 + +local function makeContextualMenu(baseMenuComponent, backgroundThemeKey) + local contextualMenuComponent = Roact.PureComponent:extend("ContextualMenuFor" .. backgroundThemeKey) + + contextualMenuComponent.validateProps = t.strictInterface({ + buttonProps = validateButtonProps, + + zIndex = t.optional(t.integer), + open = t.boolean, + menuDirection = enumerateValidator(MenuDirection), + openPositionY = t.UDim, + + closeBackgroundVisible = t.optional(t.boolean), + screenSize = t.Vector2, + + onDismiss = t.optional(t.callback), + }) + + contextualMenuComponent.defaultProps = { + zIndex = 2, + closeBackgroundVisible = true, + } + + function contextualMenuComponent:init() + self.wasDismissed = false + + self.positionPercentBinding, self.positionPercentBindingUpdate = Roact.createBinding(0) + + self.motor = Otter.createSingleMotor(0) + self.motor:onStep(self.positionPercentBindingUpdate) + self.motor:onComplete(function() + if self.wasDismissed then + self.wasDismissed = false + if self.props.onDismiss then + self.props.onDismiss() + end + end + end) + + self.state = { + absoluteSize = Vector2.new(0, 0), + absolutePosition = Vector2.new(0, 0), + } + + self.positionBinding = self.positionPercentBinding:map(function(positionPercent) + if self.props.menuDirection == MenuDirection.Down then + return UDim2.fromScale(0.5, positionPercent - 1) + else + return UDim2.fromScale(0.5, 1 - positionPercent) + end + end) + + self.visibleBinding = self.positionPercentBinding:map(function(positionPercent) + return positionPercent ~= 0 + end) + end + + function contextualMenuComponent:render() + return withStyle(function(stylePalette) + local contextMenuWidth = UDim.new(1, -CONTEXT_MENU_PADDING * 2) + if self.state.absoluteSize.X > LARGE_WIDTH_THRESHOLD then + contextMenuWidth = UDim.new(0, LARGE_WIDTH_SIZE) + end + + local anchorPointY = 0 + if self.props.menuDirection == MenuDirection.Up then + anchorPointY = 1 + end + + local backgroundTransparency = stylePalette.Theme.Overlay.Transparency + if not self.props.closeBackgroundVisible then + backgroundTransparency = 1 + end + + local topCornerInset, _ = GuiService:GetGuiInset() + local absolutePosition = self.state.absolutePosition + topCornerInset + + return Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + Visible = self.visibleBinding, + ZIndex = self.props.zIndex, + + [Roact.Change.AbsoluteSize] = function(rbx) + self:setState({ + absoluteSize = rbx.AbsoluteSize, + }) + end, + + [Roact.Change.AbsolutePosition] = function(rbx) + self:setState({ + absolutePosition = rbx.AbsolutePosition, + }) + end, + }, { + Background = Roact.createElement("TextButton", { + ZIndex = 1, + Text = "", + BorderSizePixel = 0, + BackgroundColor3 = stylePalette.Theme.Overlay.Color, + BackgroundTransparency = backgroundTransparency, + AutoButtonColor = false, + + Position = UDim2.fromOffset(-absolutePosition.X, -absolutePosition.Y), + Size = UDim2.fromOffset(self.props.screenSize.X, self.props.screenSize.Y), + + [Roact.Event.Activated] = function() + if not self.wasDismissed then + self.wasDismissed = true + self.motor:setGoal(Otter.spring(0, MOTOR_OPTIONS_CLOSE)) + end + end, + }), + + PositionFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + Position = UDim2.new(0, 0, self.props.openPositionY.Scale, self.props.openPositionY.Offset), + ZIndex = 2, + }, { + BaseMenu = Roact.createElement(baseMenuComponent, { + buttonProps = self.props.buttonProps, + + width = contextMenuWidth, + position = self.positionBinding, + anchorPoint = Vector2.new(0.5, anchorPointY), + }), + }), + }) + end) + end + + function contextualMenuComponent:didMount() + if self.props.open then + self.wasDismissed = false + self.motor:setGoal(Otter.spring(1, MOTOR_OPTIONS_OPEN)) + end + end + + function contextualMenuComponent:didUpdate(previousProps, previousState) + if self.props.open ~= previousProps.open then + if self.props.open then + self.wasDismissed = false + self.motor:setGoal(Otter.spring(1, MOTOR_OPTIONS_OPEN)) + else + self.motor:setGoal(Otter.spring(0, MOTOR_OPTIONS_CLOSE)) + end + end + end + + function contextualMenuComponent:wilUnmount() + self.motor:destroy() + end + + return contextualMenuComponent +end + +return makeContextualMenu \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/validateButtonProps.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/validateButtonProps.lua new file mode 100644 index 0000000..0339123 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/validateButtonProps.lua @@ -0,0 +1,21 @@ +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +return t.array(t.strictInterface({ + -- Icon can either be an Image in a ImageSet or a regular image asset + icon = t.optional(t.union(t.table, t.string)), + text = t.string, + onActivated = t.callback, + disabled = t.optional(t.boolean), + + -- A KeyCode to display a keycode hint for, the display string based on the users keyboard is displayed. + keyCodeLabel = t.optional(t.enum(Enum.KeyCode)), + selected = t.optional(t.boolean), + + iconColorOverride = t.optional(t.Color3), + textColorOverride = t.optional(t.Color3), +})) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/LargePill.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/LargePill.lua new file mode 100644 index 0000000..7c605df --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/LargePill.lua @@ -0,0 +1,159 @@ +local Pill = script.Parent +local App = Pill.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(App.ImageSet.Images) +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) +local GenericButton = require(UIBlox.Core.Button.GenericButton) +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local getContentStyle = require(Pill.getContentStyle) + +local HEIGHT = 48 +local PADDING = 24 + +local BUTTON_STATE_COLOR = { + [ControlState.Default] = "SecondaryDefault", + [ControlState.Hover] = "SecondaryOnHover", +} + +local SELECTED_BUTTON_STATE_COLOR = { + [ControlState.Default] = "UIDefault", +} + +local CONTENT_STATE_COLOR = { + [ControlState.Default] = "SecondaryContent", + [ControlState.Hover] = "SecondaryOnHover", +} + +local SELECTED_CONTENT_STATE_COLOR = { + [ControlState.Default] = "SecondaryOnHover", +} + +local LargePill = Roact.PureComponent:extend("LargePill") + +LargePill.validateProps = t.strictInterface({ + -- Position in an ordered layout + layoutOrder = t.optional(t.number), + -- Width of the pill + width = t.optional(t.UDim), + -- Text shown in the Pill + text = t.optional(t.string), + -- Flag that indicates that the Pill is selected (background is filled) + isSelected = t.optional(t.boolean), + -- Flag that indicates that the Pill is still loading + isLoading = t.optional(t.boolean), + -- Flag that indicates that the Pill is disabled + isDisabled = t.optional(t.boolean), + -- BackgroundColor (used for the loading animation) + backgroundColor = t.optional(t.Color3), + -- Callback function when the Pill is clicked + onActivated = t.callback, + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) + +LargePill.defaultProps = { + layoutOrder = 1, + width = UDim.new(.5, 0), + text = "", + isSelected = false, + isLoading = false, + isDisabled = false, +} + +function LargePill:init() + self.state = { + controlState = ControlState.Initialize + } + + self.onStateChanged = function(oldState, newState) + self:setState({ + controlState = newState, + }) + end +end + +function LargePill:render() + local isSelected = self.props.isSelected + local image = isSelected and Images["component_assets/circle_49"] or Images["component_assets/circle_49_stroke_1"] + + local buttonColors = isSelected and SELECTED_BUTTON_STATE_COLOR or BUTTON_STATE_COLOR + local contentColors = isSelected and SELECTED_CONTENT_STATE_COLOR or CONTENT_STATE_COLOR + + return withStyle(function(style) + return withSelectionCursorProvider(function(getSelectionCursor) + local theme = style.Theme + local currentState = self.state.controlState + local textStyle = getContentStyle(contentColors, currentState, style) + local size = UDim2.new(self.props.width, UDim.new(0, HEIGHT)) + local sliceCenter = Rect.new(24, 24, 25, 25) + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + }, { + Button = Roact.createElement(GenericButton, { + Size = size, + SliceCenter = sliceCenter, + SelectionImageObject = getSelectionCursor(CursorKind.LargePill), + isLoading = self.props.isLoading, + isDisabled = self.props.isDisabled, + text = self.props.text, + onActivated = self.props.onActivated, + buttonImage = image, + buttonStateColorMap = buttonColors, + contentStateColorMap = contentColors, + onStateChanged = self.onStateChanged, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + [Roact.Ref] = self.props[Roact.Ref], + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, PADDING), + }), + Text = Roact.createElement(GenericTextLabel, { + Size = UDim2.new(1, -2 * PADDING, 1, 0), + BackgroundTransparency = 1, + Text = self.props.text, + fontStyle = style.Font.Header2, + colorStyle = textStyle, + LayoutOrder = 2, + TextTruncate = Enum.TextTruncate.AtEnd, + }), + }), + Mask = self.props.isLoading and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = Images["component_assets/circle_49_mask"], + ImageColor3 = self.props.backgroundColor or theme.BackgroundDefault.Color, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = sliceCenter, + Size = size, + ZIndex = 3, + }), + }) + end) + end) +end + +return LargePill \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/LargePill.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/LargePill.spec.lua new file mode 100644 index 0000000..99dc7f2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/LargePill.spec.lua @@ -0,0 +1,39 @@ +return function() + local Pill = script.Parent + local App = Pill.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local LargePill = require(Pill.LargePill) + + it("should create and destroy Large Pill with text without errors", function() + local element = mockStyleComponent({ + pill = Roact.createElement(LargePill, { + text = "Large Pill", + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + it("should create and destroy Large Pill with all properties without errors", function() + local element = mockStyleComponent({ + pill = Roact.createElement(LargePill, { + text = "Large Pill", + width = UDim.new(0.75, 0), + isSelected = true, + isLoading = true, + isDisabled = true, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/SmallPill.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/SmallPill.lua new file mode 100644 index 0000000..855f415 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/SmallPill.lua @@ -0,0 +1,160 @@ +local Pill = script.Parent +local App = Pill.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(App.ImageSet.Images) +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) +local GenericButton = require(UIBlox.Core.Button.GenericButton) +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local getContentStyle = require(Pill.getContentStyle) + +local HEIGHT = 28 +local PADDING = 12 + +local BUTTON_STATE_COLOR = { + [ControlState.Default] = "SecondaryDefault", + [ControlState.Hover] = "SecondaryOnHover", +} + +local SELECTED_BUTTON_STATE_COLOR = { + [ControlState.Default] = "BackgroundOnHover", +} + +local CONTENT_STATE_COLOR = { + [ControlState.Default] = "TextMuted", + [ControlState.Hover] = "TextEmphasis", +} + +local SELECTED_CONTENT_STATE_COLOR = { + [ControlState.Default] = "TextEmphasis", +} + +local SmallPill = Roact.PureComponent:extend("SmallPill") + +SmallPill.validateProps = t.strictInterface({ + -- Position in an ordered layout + layoutOrder = t.optional(t.number), + -- Text shown in the Pill + text = t.optional(t.string), + -- Flag that indicates that the Pill is selected (background is filled) + isSelected = t.optional(t.boolean), + -- Flag that indicates that the Pill is still loading + isLoading = t.optional(t.boolean), + -- Flag that indicates that the Pill is disabled + isDisabled = t.optional(t.boolean), + -- BackgroundColor (used for the loading animation) + backgroundColor = t.optional(t.Color3), + -- Callback function when the Pill is clicked + onActivated = t.callback, + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) + +SmallPill.defaultProps = { + layoutOrder = 1, + text = "", + isSelected = false, + isLoading = false, + isDisabled = false, +} + +function SmallPill:init() + self.state = { + controlState = ControlState.Initialize + } + + self.onStateChanged = function(oldState, newState) + self:setState({ + controlState = newState, + }) + end +end + +function SmallPill:render() + local isSelected = self.props.isSelected + local image = isSelected and Images["component_assets/circle_29"] or Images["component_assets/circle_29_stroke_1"] + local text = self.props.text + + local buttonColors = isSelected and SELECTED_BUTTON_STATE_COLOR or BUTTON_STATE_COLOR + local contentColors = isSelected and SELECTED_CONTENT_STATE_COLOR or CONTENT_STATE_COLOR + local sliceCenter = Rect.new(14, 14, 15, 15) + + return withStyle(function(style) + return withSelectionCursorProvider(function(getSelectionCursor) + local theme = style.Theme + local fontStyle = style.Font.CaptionHeader + local textSize = fontStyle.RelativeSize * style.Font.BaseSize + local textWidth = GetTextSize(text, textSize, fontStyle.Font, Vector2.new()).X + local size = UDim2.new(0, textWidth + PADDING * 2, 0, HEIGHT) + + local currentState = self.state.controlState + local textStyle = getContentStyle(contentColors, currentState, style) + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + }, { + Button = Roact.createElement(GenericButton, { + Size = size, + SliceCenter = sliceCenter, + SelectionImageObject = getSelectionCursor(CursorKind.SmallPill), + isLoading = self.props.isLoading, + isDisabled = self.props.isDisabled, + text = self.props.text, + onActivated = self.props.onActivated, + buttonImage = image, + buttonStateColorMap = buttonColors, + contentStateColorMap = contentColors, + onStateChanged = self.onStateChanged, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + [Roact.Ref] = self.props[Roact.Ref], + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, PADDING), + }), + Text = Roact.createElement(GenericTextLabel, { + BackgroundTransparency = 1, + Text = self.props.text, + fontStyle = fontStyle, + colorStyle = textStyle, + LayoutOrder = 2, + }) + }), + Mask = self.props.isLoading and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = Images["component_assets/circle_29_mask"], + ImageColor3 = self.props.backgroundColor or theme.BackgroundDefault.Color, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = sliceCenter, + Size = size, + ZIndex = 3, + }), + }) + end) + end) +end + +return SmallPill \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/SmallPill.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/SmallPill.spec.lua new file mode 100644 index 0000000..f57a385 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/SmallPill.spec.lua @@ -0,0 +1,38 @@ +return function() + local Pill = script.Parent + local App = Pill.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local SmallPill = require(Pill.SmallPill) + + it("should create and destroy Small Pill with text without errors", function() + local element = mockStyleComponent({ + pill = Roact.createElement(SmallPill, { + text = "Small Pill", + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + it("should create and destroy Small Pill with all properties without errors", function() + local element = mockStyleComponent({ + pill = Roact.createElement(SmallPill, { + text = "Large Pill", + isSelected = true, + isLoading = true, + isDisabled = true, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/getContentStyle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/getContentStyle.lua new file mode 100644 index 0000000..808c02d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/getContentStyle.lua @@ -0,0 +1,23 @@ +local Pill = script.Parent +local Apps = Pill.Parent +local UIBlox = Apps.Parent + +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) + +-- Copied from GenericButton. TODO: factor it out +return function(contentMap, controlState, style) + local contentThemeClass = contentMap[controlState] + or contentMap[ControlState.Default] + + local contentStyle = { + Color = style.Theme[contentThemeClass].Color, + Transparency = style.Theme[contentThemeClass].Transparency, + } + + --Based on the design specs, the disabled and pressed state is 0.5 * alpha value + if controlState == ControlState.Disabled or + controlState == ControlState.Pressed then + contentStyle.Transparency = 0.5 * contentStyle.Transparency + 0.5 + end + return contentStyle +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/LargePill.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/LargePill.lua new file mode 100644 index 0000000..dd938b2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/LargePill.lua @@ -0,0 +1,21 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local ASSET_NAME = "component_assets/circle_52_stroke_3" + +return function(props) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(26, 26, 27, 27), + + [Roact.Ref] = props[Roact.Ref], + }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/RoundedRect.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/RoundedRect.lua new file mode 100644 index 0000000..65d65b2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/RoundedRect.lua @@ -0,0 +1,23 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local INSET_ADJUSTMENT = 6 +local ASSET_NAME = "component_assets/circle_17_stroke_3" + +return function(props) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + BackgroundTransparency = 1, + Size = UDim2.new(1, INSET_ADJUSTMENT * 2, 1, INSET_ADJUSTMENT * 2), + Position = UDim2.new(0, -INSET_ADJUSTMENT, 0, -INSET_ADJUSTMENT), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + + [Roact.Ref] = props[Roact.Ref], + }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/RoundedRectNoInset.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/RoundedRectNoInset.lua new file mode 100644 index 0000000..84da8d7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/RoundedRectNoInset.lua @@ -0,0 +1,21 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local ASSET_NAME = "component_assets/circle_17_stroke_3" + +return function(props) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + + [Roact.Ref] = props[Roact.Ref], + }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/SmallPill.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/SmallPill.lua new file mode 100644 index 0000000..4905419 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/SmallPill.lua @@ -0,0 +1,21 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local ASSET_NAME = "component_assets/circle_30_stroke_3" + +return function(props) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(15, 15, 16, 16), + + [Roact.Ref] = props[Roact.Ref], + }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/CursorKind.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/CursorKind.lua new file mode 100644 index 0000000..96725d6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/CursorKind.lua @@ -0,0 +1,15 @@ +local Packages = script.Parent.Parent.Parent.Parent + +local enumerate = require(Packages.enumerate) + +local RoundedRectCursor = require(script.Parent.Components.RoundedRect) +local RoundedRectNoInsetCursor = require(script.Parent.Components.RoundedRectNoInset) +local SmallPillCursor = require(script.Parent.Components.SmallPill) +local LargePillCursor = require(script.Parent.Components.LargePill) + +return enumerate(script.Name, { + RoundedRect = RoundedRectCursor, + RoundedRectNoInset = RoundedRectNoInsetCursor, + SmallPill = SmallPillCursor, + LargePill = LargePillCursor, +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionCursorProvider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionCursorProvider.lua new file mode 100644 index 0000000..31abe06 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionCursorProvider.lua @@ -0,0 +1,61 @@ +local Packages = script.Parent.Parent.Parent.Parent + +local Roact = require(Packages.Roact) +local RoactGamepad = require(Packages.RoactGamepad) +local Cryo = require(Packages.Cryo) + +local CursorKind = require(script.Parent.CursorKind) +local SelectionImageContext = require(script.Parent.SelectionImageContext) + +local SelectionCursorProvider = Roact.PureComponent:extend("SelectionCursorProvider") + +function SelectionCursorProvider:init() + self.refs = RoactGamepad.createRefCache() + self:setState({ + mountedCursors = {} + }) + + self.getSelectionCursor = function(cursorKind) + assert(CursorKind.isEnumValue(cursorKind), + ("Invalid arg #1: expected a CursorKind enum variant, got %s"):format(tostring(cursorKind))) + + if self.state.mountedCursors[cursorKind] == nil then + self:setState(function(state) + return { + mountedCursors = Cryo.Dictionary.join(state.mountedCursors, { + [cursorKind] = true, + }) + } + end) + end + + -- Note that we return the ref here even if it shouldn't exist yet; + -- thanks to the refCache, we know that the ref created here is the same + -- one that will be ultimately assigned to the cursor component once the + -- setState completes and the component does re-render + return self.refs[cursorKind] + end +end + +function SelectionCursorProvider:render() + local cursors = {} + for cursorKind, _ in pairs(self.state.mountedCursors) do + local CursorComponent = cursorKind.rawValue() + local key = tostring(CursorComponent) + cursors[key] = Roact.createElement(CursorComponent, { + [Roact.Ref] = self.refs[cursorKind], + }) + end + + return Roact.createElement(SelectionImageContext.Provider, { + value = self.getSelectionCursor, + }, { + CursorContainer = Roact.createElement("Frame", { + Size = UDim2.new(0, 0, 0, 0), + Visible = false, + }, cursors), + Children = Roact.createFragment(self.props[Roact.Children]), + }) +end + +return SelectionCursorProvider diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionCursorProvider.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionCursorProvider.spec.lua new file mode 100644 index 0000000..4f705cc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionCursorProvider.spec.lua @@ -0,0 +1,81 @@ +return function() + local Packages = script.Parent.Parent.Parent.Parent + + local Roact = require(Packages.Roact) + + local SelectionCursorProvider = require(script.Parent.SelectionCursorProvider) + local SelectionImageContext = require(script.Parent.SelectionImageContext) + local CursorKind = require(script.Parent.CursorKind) + + describe("Managed singleton cache of UI elements for use as selection cursors", function() + it("should provide a ref that refers to an ImageLabel", function() + local ref + local function CaptureRef() + return Roact.createElement(SelectionImageContext.Consumer, { + render = function(getSelectionCursor) + ref = getSelectionCursor(CursorKind.RoundedRect) + return nil + end, + }) + end + + local tree = Roact.mount(Roact.createElement(SelectionCursorProvider, {}, { + RefCapturer = Roact.createElement(CaptureRef) + })) + + expect(typeof(ref.getValue)).to.equal("function") + expect(typeof(ref:getValue())).to.equal("Instance") + expect(ref:getValue():IsA("ImageLabel")).to.equal(true) + + Roact.unmount(tree) + end) + + it("should return the same object on multiple calls", function() + local capturedRefs = {} + local function CaptureRef(props) + return Roact.createElement(SelectionImageContext.Consumer, { + render = function(getSelectionCursor) + capturedRefs[props.key] = getSelectionCursor(CursorKind.RoundedRect) + return nil + end, + }) + end + + local tree = Roact.mount(Roact.createElement(SelectionCursorProvider, {}, { + RefCapturer1 = Roact.createElement(CaptureRef, { + key = "ref1", + }), + RefCapturer2 = Roact.createElement(CaptureRef, { + key = "ref2", + }), + })) + + expect(capturedRefs.ref1).to.be.ok() + expect(capturedRefs.ref2).to.be.ok() + expect(capturedRefs.ref1).to.equal(capturedRefs.ref2) + + Roact.unmount(tree) + end) + + it("Should throw an error when invoked with an invalid argument", function() + local badAssetName = "doesn't exist" + local function BadCursor(props) + return Roact.createElement(SelectionImageContext.Consumer, { + render = function(getSelectionCursor) + getSelectionCursor(badAssetName) + return nil + end, + }) + end + + local ok, err = pcall(function() + Roact.mount(Roact.createElement(SelectionCursorProvider, {}, { + BadCursor = Roact.createElement(BadCursor), + })) + end) + + expect(ok).to.equal(false) + expect(err:find(badAssetName)).to.be.ok() + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionImageContext.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionImageContext.lua new file mode 100644 index 0000000..53b1fdf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionImageContext.lua @@ -0,0 +1,9 @@ +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) + +local SelectionImageContext = Roact.createContext(function() + -- By default, provides no cursors + return nil +end) + +return SelectionImageContext \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/withSelectionCursorProvider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/withSelectionCursorProvider.lua new file mode 100644 index 0000000..d0b470b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/withSelectionCursorProvider.lua @@ -0,0 +1,13 @@ +local Packages = script.Parent.Parent.Parent.Parent + +local Roact = require(Packages.Roact) + +local SelectionImageContext = require(script.Parent.SelectionImageContext) + +local function SelectionCursorConsumer(renderWithCursor) + return Roact.createElement(SelectionImageContext.Consumer, { + render = renderWithCursor, + }) +end + +return SelectionCursorConsumer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/ContextualSlider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/ContextualSlider.lua new file mode 100644 index 0000000..462dfe9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/ContextualSlider.lua @@ -0,0 +1,3 @@ +local makeAppOneKnobSlider = require(script.Parent.makeAppOneKnobSlider) + +return makeAppOneKnobSlider("ContextualPrimaryDefault") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/ContextualSlider.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/ContextualSlider.spec.lua new file mode 100644 index 0000000..8ea413b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/ContextualSlider.spec.lua @@ -0,0 +1,45 @@ +return function() + local Slider = script.Parent + local App = Slider.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local ContextualSlider = require(script.Parent.ContextualSlider) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + contextualSlider = Roact.createElement(ContextualSlider, { + value = 10, + min = 0, + max = 100, + onValueChanged = function() end, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with all props", function() + local element = mockStyleComponent({ + contextualSlider = Roact.createElement(ContextualSlider, { + value = 10, + min = 0, + max = 100, + stepInterval = 1, + onValueChanged = function() end, + isDisabled = false, + textInputEnabled = true, + + width = UDim.new(1, 1), + position = UDim2.new(1, 1, 1, 1), + anchorPoint = Vector2.new(0.5, 0.5), + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SliderTextInput.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SliderTextInput.lua new file mode 100644 index 0000000..d964468 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SliderTextInput.lua @@ -0,0 +1,165 @@ +-- Specialized TextBox for handling the text boxes that sliders can have. + +local UserInputService = game:GetService("UserInputService") + +local Packages = script.Parent.Parent.Parent.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local ImageSetComponent = require(Packages.UIBlox.Core.ImageSet.ImageSetComponent) +local Images = require(Packages.UIBlox.App.ImageSet.Images) +local withStyle = require(Packages.UIBlox.Style.withStyle) + +local divideTransparency = require(Packages.UIBlox.Utility.divideTransparency) + +local ExternalEventConnection = require(Packages.UIBlox.Utility.ExternalEventConnection) + +local SliderTextInput = Roact.PureComponent:extend("SliderTextInput") +SliderTextInput.validateProps = t.strictInterface({ + position = t.optional(t.union(t.UDim2, t.table)), + anchorPoint = t.optional(t.Vector2), + value = t.number, + min = t.number, + max = t.number, + disabled = t.optional(t.boolean), + stepInterval = t.numberPositive, + onValueChanged = t.callback, + layoutOrder = t.optional(t.integer), +}) + +SliderTextInput.defaultProps = { + disabled = false, +} + +function SliderTextInput:init() + self.textBoxRef = Roact.createRef() +end + +function SliderTextInput:render() + return withStyle(function(style) + local transparencyDivisor = self.props.disabled and 2 or 1 + local textTransparency = divideTransparency( + style.Theme.TextDefault.Transparency, + transparencyDivisor) + + local borderTransparency = divideTransparency( + style.Theme.Divider.Transparency, + transparencyDivisor) + + local backgroundTransparency = divideTransparency( + style.Theme.BackgroundUIContrast.Transparency, + transparencyDivisor) + + return Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = Images["component_assets/circle_16"], + ImageColor3 = style.Theme.BackgroundUIContrast.Color, + ImageTransparency = backgroundTransparency, + Position = self.props.position, + AnchorPoint = self.props.anchorPoint, + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.fromOffset(56, 36), + SliceCenter = Rect.new(8, 8, 8, 8), + LayoutOrder = self.props.layoutOrder, + }, { + Border = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = Images["component_assets/circle_17_stroke_1"], + ImageColor3 = style.Theme.Divider.Color, + ImageTransparency = borderTransparency, + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.fromScale(1, 1), + SliceCenter = Rect.new(8, 8, 8, 8), + }), + TextBox = Roact.createElement("TextBox", { + [Roact.Ref] = self.textBoxRef, + BackgroundTransparency = 1, + ClearTextOnFocus = false, + Font = style.Font.Body.Font, + TextSize = style.Font.Body.RelativeSize * style.Font.BaseSize, + TextColor3 = style.Theme.TextDefault.Color, + TextTransparency = textTransparency, + Size = UDim2.fromScale(1, 1), + Text = tostring(self.props.value), + TextScaled = true, + TextEditable = not self.props.disabled, + ZIndex = 2, + + [Roact.Event.Focused] = self.props.disabled and function(rbx) + rbx:ReleaseFocus() + end or nil, + [Roact.Event.FocusLost] = function(rbx, enterPressed) + if not enterPressed then + return + end + + local newValue = tonumber(rbx.Text) + + if newValue == nil then + rbx.Text = tostring(self.props.value) + return + end + + newValue = math.clamp( + math.floor(newValue / self.props.stepInterval + 0.5) * self.props.stepInterval, + self.props.min, + self.props.max) + + self.props.onValueChanged(newValue) + end, + }, { + TextSizeConstraint = Roact.createElement("UITextSizeConstraint", { + MinTextSize = style.Font.Body.RelativeMinSize * style.Font.BaseSize, + MaxTextSize = style.Font.Body.RelativeSize * style.Font.BaseSize, + }) + }), + UserInputConnection = not self.props.disabled and Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputBegan, + callback = function(input, gameProcessed) + if input.UserInputType ~= Enum.UserInputType.Keyboard then + return + end + + if UserInputService:GetFocusedTextBox() ~= self.textBoxRef.current then + return + end + + local direction = 0 + if input.KeyCode == Enum.KeyCode.Up then + direction = 1 + elseif input.KeyCode == Enum.KeyCode.Down then + direction = -1 + end + + if UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) + or UserInputService:IsKeyDown(Enum.KeyCode.RightShift) then + direction = direction * 10 + end + + if direction ~= 0 then + local rawNewValue = self.props.value + self.props.stepInterval * direction + local newValue = math.clamp( + math.floor(rawNewValue / self.props.stepInterval + 0.5) * self.props.stepInterval, + self.props.min, + self.props.max) + + if newValue ~= self.props.value then + self.props.onValueChanged(newValue) + end + end + end, + }) + }) + end) +end + +function SliderTextInput:didMount() + -- Set the textbox's TextInputType here, because the property is RobloxScript + -- only and not accessible in Horsecat or some tests. + pcall(function() + self.textBoxRef.current.TextInputType = Enum.TextInputType.Number + end) +end + +return SliderTextInput diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SliderTextInput.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SliderTextInput.spec.lua new file mode 100644 index 0000000..bd36e5f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SliderTextInput.spec.lua @@ -0,0 +1,42 @@ +return function() + local Slider = script.Parent + local App = Slider.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local SliderTextInput = require(script.Parent.SliderTextInput) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + sliderTextInput = Roact.createElement(SliderTextInput, { + value = 10, + min = 0, + max = 100, + stepInterval = 1, + onValueChanged = function() end, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with all props", function() + local element = mockStyleComponent({ + sliderTextInput = Roact.createElement(SliderTextInput, { + position = UDim2.new(1, 1, 1, 1), + anchorPoint = Vector2.new(0.5, 0.5), + value = 10, + min = 0, + max = 100, + disabled = false, + stepInterval = 1, + onValueChanged = function() end, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SystemSlider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SystemSlider.lua new file mode 100644 index 0000000..c471edf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SystemSlider.lua @@ -0,0 +1,3 @@ +local makeAppOneKnobSlider = require(script.Parent.makeAppOneKnobSlider) + +return makeAppOneKnobSlider("SystemPrimaryDefault") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SystemSlider.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SystemSlider.spec.lua new file mode 100644 index 0000000..d70cab3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SystemSlider.spec.lua @@ -0,0 +1,45 @@ +return function() + local Slider = script.Parent + local App = Slider.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local SystemSlider = require(script.Parent.SystemSlider) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + systemSlider = Roact.createElement(SystemSlider, { + value = 10, + min = 0, + max = 100, + onValueChanged = function() end, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with all props", function() + local element = mockStyleComponent({ + systemSlider = Roact.createElement(SystemSlider, { + value = 10, + min = 0, + max = 100, + stepInterval = 1, + onValueChanged = function() end, + isDisabled = false, + textInputEnabled = true, + + width = UDim.new(1, 1), + position = UDim2.new(1, 1, 1, 1), + anchorPoint = Vector2.new(0.5, 0.5), + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobContextualSlider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobContextualSlider.lua new file mode 100644 index 0000000..31d15cc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobContextualSlider.lua @@ -0,0 +1,3 @@ +local makeAppTwoKnobSlider = require(script.Parent.makeAppTwoKnobSlider) + +return makeAppTwoKnobSlider("ContextualPrimaryDefault") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobContextualSlider.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobContextualSlider.spec.lua new file mode 100644 index 0000000..45fe448 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobContextualSlider.spec.lua @@ -0,0 +1,46 @@ +return function() + local Slider = script.Parent + local App = Slider.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local TwoKnobContextualSlider = require(script.Parent.TwoKnobContextualSlider) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + systemSlider = Roact.createElement(TwoKnobContextualSlider, { + lowerValue = 10, + upperValue = 90, + min = 0, + max = 100, + onValueChanged = function() end, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with all props", function() + local element = mockStyleComponent({ + systemSlider = Roact.createElement(TwoKnobContextualSlider, { + lowerValue = 10, + upperValue = 90, + min = 0, + max = 100, + stepInterval = 1, + onValueChanged = function() end, + isDisabled = false, + + width = UDim.new(1, 1), + position = UDim2.new(1, 1, 1, 1), + anchorPoint = Vector2.new(0.5, 0.5), + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobSystemSlider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobSystemSlider.lua new file mode 100644 index 0000000..0b937cd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobSystemSlider.lua @@ -0,0 +1,3 @@ +local makeAppTwoKnobSlider = require(script.Parent.makeAppTwoKnobSlider) + +return makeAppTwoKnobSlider("SystemPrimaryDefault") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobSystemSlider.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobSystemSlider.spec.lua new file mode 100644 index 0000000..dbab05e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobSystemSlider.spec.lua @@ -0,0 +1,46 @@ +return function() + local Slider = script.Parent + local App = Slider.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local TwoKnobSystemSlider = require(script.Parent.TwoKnobSystemSlider) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + systemSlider = Roact.createElement(TwoKnobSystemSlider, { + lowerValue = 10, + upperValue = 90, + min = 0, + max = 100, + onValueChanged = function() end, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with all props", function() + local element = mockStyleComponent({ + systemSlider = Roact.createElement(TwoKnobSystemSlider, { + lowerValue = 10, + upperValue = 90, + min = 0, + max = 100, + stepInterval = 1, + onValueChanged = function() end, + isDisabled = false, + + width = UDim.new(1, 1), + position = UDim2.new(1, 1, 1, 1), + anchorPoint = Vector2.new(0.5, 0.5), + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/__stories__/SliderTextInput.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/__stories__/SliderTextInput.story.lua new file mode 100644 index 0000000..57970cb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/__stories__/SliderTextInput.story.lua @@ -0,0 +1,63 @@ +local SliderRoot = script.Parent.Parent +local App = SliderRoot.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local StoryView = require(Packages.StoryComponents.StoryView) + +local SliderTextInput = require(SliderRoot.SliderTextInput) + +local Story = Roact.PureComponent:extend("Story") + +function Story:init() + self:setState({ + value = 10 + }) +end + +function Story:render() + return Roact.createFragment({ + List = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + Padding = UDim.new(0, 10), + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Normal = Roact.createElement(SliderTextInput, { + layoutOrder = 1, + value = self.state.value, + min = 0, + max = 100, + stepInterval = 10, + onValueChanged = function(newValue) + print(newValue) + self:setState({ + value = newValue, + }) + end, + }), + Disabled = Roact.createElement(SliderTextInput, { + layoutOrder = 2, + value = self.state.value, + min = 0, + max = 100, + stepInterval = 10, + disabled = true, + onValueChanged = function(newValue) + end, + }), + }) +end + +return function(target) + local styleProvider = Roact.createElement(StoryView, {}, { + Roact.createElement(Story) + }) + + local handle = Roact.mount(styleProvider, target, "SliderTextInput") + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppOneKnobSlider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppOneKnobSlider.lua new file mode 100644 index 0000000..d62bd17 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppOneKnobSlider.lua @@ -0,0 +1,104 @@ +local Slider = script.Parent +local App = Slider.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local makeAppSlider = require(Slider.makeAppSlider) +local SliderTextInput = require(Slider.SliderTextInput) +local withStyle = require(UIBlox.Core.Style.withStyle) +local validateStyle = require(App.Style.Validator.validateStyle) + +local function wrapStyle(component) + return function(props) + return withStyle(function(style) + local joinedProps = Cryo.Dictionary.join(props, { + style = style, + }) + + return Roact.createElement(component, joinedProps) + end) + end +end + +local function makeAppOneKnobSlider(trackFillThemeKey) + local oneKnobAppSliderComponent = Roact.PureComponent:extend("OneKnobAppSliderFor" .. trackFillThemeKey) + local appSliderComponent = makeAppSlider(trackFillThemeKey, false) + oneKnobAppSliderComponent.validateProps = t.strictInterface({ + value = t.number, + min = t.number, + max = t.number, + onValueChanged = t.callback, + stepInterval = t.optional(t.numberPositive), + textInputEnabled = t.optional(t.boolean), + isDisabled = t.optional(t.boolean), + + width = t.optional(t.UDim), + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + layoutOrder = t.optional(t.integer), + --Internal Only - Don't Pass In + style = validateStyle + }) + + oneKnobAppSliderComponent.defaultProps = { + stepInterval = 1, + width = UDim.new(1, 0), + textInputEnabled = false + } + + function oneKnobAppSliderComponent:render() + local props = self.props + + local sliderProps = { + value = props.value, + min = props.min, + max = props.max, + stepInterval = props.stepInterval, + isDisabled = props.isDisabled, + onValueChanged = props.onValueChanged, + style = props.style + } + + if not props.textInputEnabled then + sliderProps.width = props.width + sliderProps.position = props.position + sliderProps.anchorPoint = props.anchorPoint + sliderProps.layoutOrder = props.layoutOrder + return Roact.createElement(appSliderComponent, sliderProps) + else + sliderProps.width = UDim.new(1, -(56 + 12)) + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new( + props.width.Scale, + props.width.Offset, + 0, + 44 + ), + AnchorPoint = props.anchorPoint, + LayoutOrder = props.layoutOrder, + Position = props.position, + }, { + Slider = Roact.createElement(appSliderComponent, sliderProps), + TextInput = Roact.createElement(SliderTextInput, { + position = UDim2.new(1, 0, 0.5, 0), + anchorPoint = Vector2.new(1, 0.5), + value = props.value, + min = props.min, + max = props.max, + disabled = props.isDisabled, + stepInterval = props.stepInterval, + onValueChanged = props.onValueChanged, + }) + }) + end + end + + return wrapStyle(oneKnobAppSliderComponent) +end + +return makeAppOneKnobSlider \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppSlider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppSlider.lua new file mode 100644 index 0000000..433ed57 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppSlider.lua @@ -0,0 +1,213 @@ +local Slider = script.Parent +local App = Slider.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Otter = require(Packages.Otter) + +local Colors = require(App.Style.Colors) +local GenericSlider = require(UIBlox.Core.Slider.GenericSlider) +local Images = require(App.ImageSet.Images) + +local SPRING_PARAMETERS = { + frequency = 5, +} + +local divideTransparency = require(UIBlox.Utility.divideTransparency) +local lerp = require(UIBlox.Utility.lerp) + +local function makeAppSlider(trackFillThemeKey, isTwoKnobs) + -- Creates a slider using the specified theme key for the track fill color. + local appSliderComponent = Roact.PureComponent:extend("AppSliderFor" .. trackFillThemeKey) + + appSliderComponent.defaultProps = { + textInputEnabled = false, + stepInterval = 1, + width = UDim.new(1, 0), + } + + function appSliderComponent:init() + local setPressedProgressLower, setPressedProgressUpper + self.pressedProgressLower, setPressedProgressLower = Roact.createBinding(0) + self.pressedProgressUpper, setPressedProgressUpper = Roact.createBinding(0) + self.disabled, self.setDisabled = Roact.createBinding(self.props.isDisabled) + self.style, self.setStyle = Roact.createBinding(self.props.style) + + local joinedBindings = Roact.joinBindings({ + disabled = self.disabled, + pressedProgressLower = self.pressedProgressLower, + pressedProgressUpper = self.pressedProgressUpper, + style = self.style, + }) + + self.trackColor = joinedBindings:map(function(values) + return values.style.Theme.UIMuted.Color + end) + + self.trackTransparency = joinedBindings:map(function(values) + return divideTransparency( + values.style.Theme.UIMuted.Transparency, + values.disabled and 2 or 1) + end) + + self.trackFillColor = joinedBindings:map(function(values) + return values.style.Theme[trackFillThemeKey].Color + end) + + self.trackFillTransparency = joinedBindings:map(function(values) + return divideTransparency( + values.style.Theme[trackFillThemeKey].Transparency, + values.disabled and 2 or 1) + end) + + self.knobColorLower = joinedBindings:map(function(values) + if values.disabled then + return Colors.Pumice + end + -- The knob, when unpressed, is white on all themes. + local unpressedColor = Color3.new(1, 1, 1) + -- If the slider is a SystemSlider, it should be pumice on pressed + local pressedColor = Colors.Pumice + if trackFillThemeKey ~= "SystemPrimaryDefault" then + pressedColor = values.style.Theme[trackFillThemeKey].Color + end + + return unpressedColor:lerp(pressedColor, values.pressedProgressLower) + end) + + self.knobColorUpper = joinedBindings:map(function(values) + if values.disabled then + return Colors.Pumice + end + -- The knob, when unpressed, is white on all themes. + local unpressedColor = Color3.new(1, 1, 1) + -- If the slider is a SystemSlider, it should be pumice on pressed + local pressedColor = Colors.Pumice + if trackFillThemeKey ~= "SystemPrimaryDefault" then + pressedColor = values.style.Theme[trackFillThemeKey].Color + end + + return unpressedColor:lerp(pressedColor, values.pressedProgressUpper) + end) + + self.knobTransparency = joinedBindings:map(function(values) + return divideTransparency( + values.style.Theme[trackFillThemeKey].Transparency, + values.disabled and 2 or 1) + end) + + self.knobShadowTransparencyLower = joinedBindings:map(function(values) + if values.disabled then + return 1 + else + return lerp(values.style.Theme.DropShadow.Transparency, 1, values.pressedProgressLower) + end + end) + + self.knobShadowTransparencyUpper = joinedBindings:map(function(values) + if values.disabled then + return 1 + else + return lerp(values.style.Theme.DropShadow.Transparency, 1, values.pressedProgressUpper) + end + end) + + self.pressedMotorLower = Otter.createSingleMotor(0) + self.pressedMotorLower:onStep(setPressedProgressLower) + + self.pressedMotorUpper = Otter.createSingleMotor(0) + self.pressedMotorUpper:onStep(setPressedProgressUpper) + + self.onDragStartLower = function() + self.pressedMotorLower:setGoal(Otter.spring(1, SPRING_PARAMETERS)) + end + + self.onDragStartUpper = function() + self.pressedMotorUpper:setGoal(Otter.spring(1, SPRING_PARAMETERS)) + end + + self.onDragEnd = function() + self.pressedMotorLower:setGoal(Otter.spring(0, SPRING_PARAMETERS)) + self.pressedMotorUpper:setGoal(Otter.spring(0, SPRING_PARAMETERS)) + end + end + + function appSliderComponent:render() + local props = self.props + local sliderProps = { + min = props.min, + max = props.max, + stepInterval = props.stepInterval, + isDisabled = props.isDisabled, + width = props.width, + position = props.position, + anchorPoint = props.anchorPoint, + layoutOrder = props.layoutOrder, + + onValueChanged = props.onValueChanged, + onDragStartLower = self.onDragStartLower, + onDragStartUpper = self.onDragStartUpper, + onDragEnd = self.onDragEnd, + + trackImage = Images["component_assets/circle_16"], + trackColor = self.trackColor, + trackTransparency = self.trackTransparency, + trackSliceCenter = Rect.new(8, 8, 8, 8), + + trackFillImage = Images["component_assets/circle_16"], + trackFillColor = self.trackFillColor, + trackFillTransparency = self.trackFillTransparency, + trackFillSliceCenter = Rect.new(8, 8, 8, 8), + + knobImage = Images["component_assets/circle_28_padding_10"], + knobColorLower = self.knobColorLower, + knobColorUpper = self.knobColorUpper, + knobTransparency = self.knobTransparency, + + knobImagePadding = 10, + + knobShadowImage = Images["component_assets/dropshadow_28"], + knobShadowTransparencyLower = self.knobShadowTransparencyLower, + knobShadowTransparencyUpper = self.knobShadowTransparencyUpper, + } + + if isTwoKnobs then + sliderProps.upperValue = props.upperValue + sliderProps.lowerValue = props.lowerValue + else + sliderProps.lowerValue = props.value + end + + return Roact.createElement(GenericSlider, sliderProps) + end + + function appSliderComponent:didMount() + self.pressedMotorLower:start() + self.pressedMotorUpper:start() + end + + function appSliderComponent:didUpdate(prevProps) + if prevProps.style ~= self.props.style then + self.setStyle(self.props.style) + end + + if prevProps.isDisabled ~= self.props.isDisabled then + self.setDisabled(self.props.isDisabled) + + if self.props.isDisabled then + self.pressedMotorLower:setGoal(Otter.spring(0, SPRING_PARAMETERS)) + self.pressedMotorUpper:setGoal(Otter.spring(0, SPRING_PARAMETERS)) + end + end + end + + function appSliderComponent:willUnmount() + self.pressedMotorLower:destroy() + self.pressedMotorUpper:destroy() + end + + return appSliderComponent +end + +return makeAppSlider \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppTwoKnobSlider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppTwoKnobSlider.lua new file mode 100644 index 0000000..3b2d28a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppTwoKnobSlider.lua @@ -0,0 +1,70 @@ +local Slider = script.Parent +local App = Slider.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local makeAppSlider = require(Slider.makeAppSlider) +local withStyle = require(UIBlox.Core.Style.withStyle) +local validateStyle = require(App.Style.Validator.validateStyle) + +local function wrapStyle(component) + return function(props) + return withStyle(function(style) + local joinedProps = Cryo.Dictionary.join(props, { + style = style, + }) + + return Roact.createElement(component, joinedProps) + end) + end +end + +local function makeAppTwoKnobSlider(trackFillThemeKey) + local twoKnobAppSliderComponent = Roact.PureComponent:extend("TwoKnobAppSliderFor" .. trackFillThemeKey) + local appSliderComponent = makeAppSlider(trackFillThemeKey, true) + local twoKnobSliderInterface = t.strictInterface({ + --value of the first knob (must be less than or equal to upperValue) + lowerValue = t.number, + --value of the second knob (must be greater than or equal to lowerValue) + upperValue = t.number, + min = t.number, + max = t.number, + stepInterval = t.optional(t.numberPositive), + onValueChanged = t.callback, + isDisabled = t.optional(t.boolean), + + width = t.optional(t.UDim), + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + layoutOrder = t.optional(t.integer), + --Internal Only - Don't Pass In + style = validateStyle + }) + + local function valueValidator(props) + if props.lowerValue > props.upperValue then + return false, "The upper value must be greater than or equal to the lower" + end + + return true + end + + twoKnobAppSliderComponent.validateProps = t.intersection(twoKnobSliderInterface, valueValidator) + + twoKnobAppSliderComponent.defaultProps = { + stepInterval = 1, + width = UDim.new(1, 0), + } + + function twoKnobAppSliderComponent:render() + return Roact.createElement(appSliderComponent, self.props) + end + + return wrapStyle(twoKnobAppSliderComponent) +end + +return makeAppTwoKnobSlider \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStylePalette.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStylePalette.lua new file mode 100644 index 0000000..9d6e9a8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStylePalette.lua @@ -0,0 +1,57 @@ +local Style = script.Parent +local getThemeFromName = require(Style.Themes.getThemeFromName) +local getFontFromName = require(Style.Fonts.getFontFromName) +local Constants = require(Style.Constants) + +local validateStyle = require(Style.Validator.validateStyle) + +local AppStylePalette = {} +AppStylePalette.__index = AppStylePalette + +local DEFAULT_FONT = Constants.FontName.Gotham +local FONT_MAP = { + [Constants.FontName.Gotham] = require(script.Parent.Fonts.Gotham), +} + +local DEFAULT_THEME = Constants.ThemeName.Light +local THEME_MAP = { + [Constants.ThemeName.Dark] = require(script.Parent.Themes.DarkTheme), + [Constants.ThemeName.Light] = require(script.Parent.Themes.LightTheme), +} + +function AppStylePalette.new(style) + --By default a new style will be empty. + -- This will allow the font and theme to be merged independently even when one is empty. + local self = {} + + if style ~= nil then + self.Font = style.Font + self.Theme = style.Theme + else + self.Font = getFontFromName("", DEFAULT_FONT, FONT_MAP) + self.Theme = getThemeFromName("", DEFAULT_THEME, THEME_MAP) + end + + setmetatable(self, AppStylePalette) + return self +end + +function AppStylePalette:updateFont(fontName) + self.Font = getFontFromName(fontName, DEFAULT_FONT, FONT_MAP) +end + +function AppStylePalette:updateTheme(themeName) + self.Theme = getThemeFromName(themeName, DEFAULT_THEME, THEME_MAP) +end + +function AppStylePalette:currentStyle() + local style = { + Font = self.Font, + Theme = self.Theme, + } + + assert(validateStyle(style)) + return style +end + +return AppStylePalette \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStylePalette.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStylePalette.spec.lua new file mode 100644 index 0000000..38b73b7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStylePalette.spec.lua @@ -0,0 +1,49 @@ +return function() + local Style = script.Parent + local AppStylePalette = require(script.Parent.AppStylePalette) + local validateStye = require(Style.Validator.validateStyle) + + it("should be able to create a style palette", function() + local stylePalette = AppStylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + end) + + it("should be able to create a style palette and be able to update theme", function() + local stylePalette = AppStylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + + stylePalette:updateTheme("light") + local newAppStyle = stylePalette:currentStyle() + expect(validateStye(newAppStyle)).equal(true) + end) + + it("should be able to create a style palette and be able to update font", function() + local stylePalette = AppStylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + + stylePalette:updateFont("gotham") + local newAppStyle = stylePalette:currentStyle() + expect(validateStye(newAppStyle)).equal(true) + end) + + it("should be able to create a style palette and be able to merge an old one in", function() + local stylePalette = AppStylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + + local newstylePalette = AppStylePalette.new(stylePalette) + local newAppStyle = newstylePalette:currentStyle() + expect(validateStye(newAppStyle)).equal(true) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStyleProvider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStyleProvider.lua new file mode 100644 index 0000000..7f1d21a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStyleProvider.lua @@ -0,0 +1,41 @@ +--[[ + The is a wrapper for the style provider for apps. +]] +local Style = script.Parent +local Core = Style.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local StyleProvider = require(UIBlox.Core.Style.StyleProvider) + +local AppStylePalette = require(script.Parent.AppStylePalette) + +local AppStyleProvider = Roact.Component:extend("AppStyleProvider") + +local validateProps = t.strictInterface({ + -- The current style of the app. + style = t.strictInterface({ + themeName = t.string, + fontName = t.string + }), + [Roact.Children] = t.table +}) + +function AppStyleProvider:render() + assert(validateProps(self.props)) + local style = self.props.style + local themeName = style.themeName + local fontName = style.fontName + local stylePalette = AppStylePalette.new() + stylePalette:updateTheme(themeName) + stylePalette:updateFont(fontName) + local appStyle = stylePalette:currentStyle() + + return Roact.createElement(StyleProvider,{ + style = appStyle, + }, self.props[Roact.Children]) +end + +return AppStyleProvider \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStyleProvider.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStyleProvider.spec.lua new file mode 100644 index 0000000..4820f48 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStyleProvider.spec.lua @@ -0,0 +1,36 @@ +return function() + local Style = script.Parent + local Core = Style.Parent + local UIBlox = Core.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + + local AppStyleProvider = require(script.Parent.AppStyleProvider) + local Constants = require(script.Parent.Constants) + local appStyle = { + themeName = Constants.ThemeName.Dark, + fontName = Constants.FontName.Gotham, + } + it("should create and destroy without errors", function() + local element = Roact.createElement("Frame") + local appStyleProvider = Roact.createElement(AppStyleProvider, { + style = appStyle, + },{ + Element = element, + }) + + local instance = Roact.mount(appStyleProvider) + Roact.unmount(instance) + end) + + it("should throw when style prop is nil", function() + local element = Roact.createElement("Frame") + local appStyleProvider = Roact.createElement(AppStyleProvider, {},{ + Element = element, + }) + expect(function() + local instance = Roact.mount(appStyleProvider) + Roact.unmount(instance) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Colors.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Colors.lua new file mode 100644 index 0000000..ffe9b59 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Colors.lua @@ -0,0 +1,23 @@ +local Colors = { + --Common colors + Black = Color3.fromRGB(0, 0, 0), + White = Color3.fromRGB(255, 255, 255), + Green = Color3.fromRGB(0, 176, 111), + Red = Color3.fromRGB(247, 75, 82), + + --Dark theme colors + Carbon = Color3.fromRGB(25, 27, 29), + Flint = Color3.fromRGB(57, 59, 61), + Graphite = Color3.fromRGB(101, 102, 104), + Obsidian = Color3.fromRGB(17, 18, 20), + Pumice = Color3.fromRGB(189, 190, 190), + Slate = Color3.fromRGB(35, 37, 39), + + --Light theme colors + Alabaster = Color3.fromRGB(242, 244, 245), + Ash = Color3.fromRGB(222, 225, 227), + Chalk = Color3.fromRGB(199, 203, 206), + Smoke = Color3.fromRGB(96, 97, 98), +} + +return Colors \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Colors.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Colors.spec.lua new file mode 100644 index 0000000..c3082b4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Colors.spec.lua @@ -0,0 +1,6 @@ +return function() + it("should be able to require Colors without errors", function() + local Colors = require(script.Parent.Colors) + expect(Colors).to.be.a("table") + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Constants.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Constants.lua new file mode 100644 index 0000000..9a373a4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Constants.lua @@ -0,0 +1,12 @@ +local Constants = {} + +Constants.ThemeName = { + Dark = "dark", + Light = "light", +} + +Constants.FontName = { + Gotham = "gotham", +} + +return Constants \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Constants.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Constants.spec.lua new file mode 100644 index 0000000..05a49d6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Constants.spec.lua @@ -0,0 +1,6 @@ +return function() + it("should be able to require Constants without errors", function() + local Constants = require(script.Parent.Constants) + expect(Constants).to.be.a("table") + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/Gotham.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/Gotham.lua new file mode 100644 index 0000000..b239e96 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/Gotham.lua @@ -0,0 +1,54 @@ +local baseSize = 16 +-- Nominal size conversion +-- https://confluence.rbx.com/display/PX/Font+Metrics +local nominalSizeFactor = 1.2 +local font = { + BaseSize = baseSize * nominalSizeFactor, + Title = { + Font = Enum.Font.GothamBlack, + RelativeSize = 32 / baseSize, + RelativeMinSize = 24 / baseSize, + }, + Header1 = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 20 / baseSize, + RelativeMinSize = 16 / baseSize, + }, + Header2 = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 16 / baseSize, + RelativeMinSize = 12 / baseSize, + }, + SubHeader1 = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 16 / baseSize, + RelativeMinSize = 12 / baseSize, + }, + Body = { + Font = Enum.Font.Gotham, + RelativeSize = 16 / baseSize, + RelativeMinSize = 12 / baseSize, + }, + CaptionHeader = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 12 / baseSize, + RelativeMinSize = 9 / baseSize, + }, + CaptionSubHeader = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 12 / baseSize, + RelativeMinSize = 9 / baseSize, + }, + CaptionBody = { + Font = Enum.Font.Gotham, + RelativeSize = 12 / baseSize, + RelativeMinSize = 9 / baseSize, + }, + Footer = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 10 / baseSize, + RelativeMinSize = 8 / baseSize, + }, +} + +return font \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/Gotham.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/Gotham.spec.lua new file mode 100644 index 0000000..c0c7940 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/Gotham.spec.lua @@ -0,0 +1,9 @@ +return function() + it("should be valid font palette without errors", function() + local Fonts = script.Parent + local Style = Fonts.Parent + local validateFont = require(Style.Validator.validateFont) + local Gotham = require(Fonts.Gotham) + assert(validateFont(Gotham)) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/getFontFromName.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/getFontFromName.lua new file mode 100644 index 0000000..f28d81e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/getFontFromName.lua @@ -0,0 +1,18 @@ +local Themes = script.Parent +local Style = Themes.Parent + +local validateFont = require(Style.Validator.validateFont) + +return function (fontName, defaultFont, fontMap) + local mappedFont + if fontName ~= nil and #fontName > 0 then + mappedFont = fontMap[string.lower(fontName)] + end + + if mappedFont == nil then + mappedFont = fontMap[defaultFont] + end + + assert(validateFont(mappedFont)) + return mappedFont +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/getFontFromName.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/getFontFromName.spec.lua new file mode 100644 index 0000000..be29422 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/getFontFromName.spec.lua @@ -0,0 +1,36 @@ +return function() + local Themes = script.Parent + local Style = Themes.Parent + local Constants = require(Style.Constants) + local getFontFromName = require(script.Parent.getFontFromName) + + it("should be able to get a font palette without errors", function() + local fontMap = { + [Constants.FontName.Gotham] = require(script.Parent.Gotham), + } + local fontTable = getFontFromName(Constants.FontName.Gotham, Constants.FontName.Gotham, fontMap) + expect(fontTable).to.be.a("table") + end) + + it("should be able to get a font palette using default without errors", function() + local fontMap = { + [Constants.FontName.Gotham] = require(script.Parent.Gotham), + } + local fontTable = getFontFromName("sourceSans", Constants.FontName.Gotham, fontMap) + expect(fontTable).to.be.a("table") + end) + + it("should throw the font palette is invalid", function() + expect(function() + local fontMap = { + [Constants.FontName.Gotham] = { + Font = { + Font = Enum.Font.Gotham, + RelativeSize = 1, + }, + }, + } + getFontFromName(Constants.FontName.Gotham, Constants.FontName.Gotham, fontMap) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/DarkTheme.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/DarkTheme.lua new file mode 100644 index 0000000..bde1d20 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/DarkTheme.lua @@ -0,0 +1,162 @@ +local ThemesRoot = script.Parent +local StylesRoot = ThemesRoot.Parent +local Colors = require(StylesRoot.Colors) + +local theme = { + BackgroundDefault = { + Color = Colors.Slate, + Transparency = 0, + }, + BackgroundContrast = { + Color = Colors.Carbon, + Transparency = 0, + }, + BackgroundMuted = { + Color = Colors.Obsidian, + Transparency = 0, + }, + BackgroundUIDefault = { + Color = Colors.Flint, + Transparency = 0, + }, + BackgroundUIContrast = { + Color = Colors.Black, + Transparency = 0.3, -- Alpha 0.7 + }, + BackgroundOnHover = { + Color = Colors.White, + Transparency = 0.9, -- Alpha 0.1 + }, + BackgroundOnPress = { + Color = Colors.Black, + Transparency = 0.7, -- Alpha 0.3 + }, + + UIDefault = { + Color = Colors.Graphite, + Transparency = 0, + }, + UIMuted = { + Color = Colors.Obsidian, + Transparency = 0.2, -- Alpha 0.8 + }, + UIEmphasis = { + Color = Colors.White, + Transparency = 0.7, -- Alpha 0.3 + }, + + ContextualPrimaryDefault = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryOnHover = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryContent = { + Color = Colors.White, + Transparency = 0, + }, + + SystemPrimaryDefault = { + Color = Colors.White, + Transparency = 0, + }, + SystemPrimaryOnHover = { + Color = Colors.White, + Transparency = 0, + }, + SystemPrimaryContent = { + Color = Colors.Flint, + Transparency = 0, + }, + + SecondaryDefault = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + SecondaryOnHover = { + Color = Colors.White, + Transparency = 0, + }, + SecondaryContent = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + + IconDefault = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 alpha + }, + IconEmphasis = { + Color = Colors.White, + Transparency = 0, + }, + IconOnHover = { + Color = Colors.White, + Transparency = 0, + }, + + TextEmphasis = { + Color = Colors.White, + Transparency = 0, + }, + TextDefault = { + Color = Colors.Pumice, + Transparency = 0, + }, + TextMuted = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + + Divider = { + Color = Colors.White, + Transparency = 0.8, -- 0.2 Alpha + }, + Overlay = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + DropShadow = { + Color = Colors.Black, + Transparency = 0, + }, + NavigationBar = { + Color = Colors.Carbon, + Transparency = 0, + }, + PlaceHolder = { + Color = Colors.Flint, + Transparency = 0.5, -- 0.5 Alpha + }, + + OnlineStatus = { + Color = Colors.Green, + Transparency = 0, + }, + OfflineStatus = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + + Success = { + Color = Colors.Green, + Transparency = 0, + }, + Alert = { + Color = Colors.Red, + Transparency = 0, + }, + + Badge = { + Color = Colors.White, + Transparency = 0, + }, + BadgeContent = { + Color = Colors.Flint, + Transparency = 0, + }, +} + +return theme \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/DarkTheme.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/DarkTheme.spec.lua new file mode 100644 index 0000000..2fa298a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/DarkTheme.spec.lua @@ -0,0 +1,9 @@ +return function() + it("should be a valid theme palette.", function() + local Themes = script.Parent + local Style = Themes.Parent + local validateTheme = require(Style.Validator.validateTheme) + local DarkTheme = require(Themes.DarkTheme) + assert(validateTheme(DarkTheme)) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/LightTheme.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/LightTheme.lua new file mode 100644 index 0000000..b1b1826 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/LightTheme.lua @@ -0,0 +1,162 @@ +local ThemesRoot = script.Parent +local StylesRoot = ThemesRoot.Parent +local Colors = require(StylesRoot.Colors) + +local theme = { + BackgroundDefault = { + Color = Colors.Alabaster, + Transparency = 0, + }, + BackgroundContrast = { + Color = Colors.Ash, + Transparency = 0, + }, + BackgroundMuted = { + Color = Colors.Chalk, + Transparency = 0, + }, + BackgroundUIDefault = { + Color = Colors.White, + Transparency = 0, + }, + BackgroundUIContrast = { + Color = Colors.White, + Transparency = 0.1, -- Alpha 0.9 + }, + BackgroundOnHover = { + Color = Colors.Black, + Transparency = 0.9, -- Alpha 0.1 + }, + BackgroundOnPress = { + Color = Colors.Black, + Transparency = 0.9, -- Alpha 0.1 + }, + + UIDefault = { + Color = Colors.Pumice, + Transparency = 0, + }, + UIMuted = { + Color = Colors.Black, + Transparency = 0.9, -- Alpha 0.1 + }, + UIEmphasis = { + Color = Colors.Black, + Transparency = 0.7, -- Alpha 0.3 + }, + + ContextualPrimaryDefault = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryOnHover = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryContent = { + Color = Colors.White, + Transparency = 0, + }, + + SystemPrimaryDefault = { + Color = Colors.Flint, + Transparency = 0, + }, + SystemPrimaryOnHover = { + Color = Colors.Flint, + Transparency = 0, + }, + SystemPrimaryContent = { + Color = Colors.White, + Transparency = 0, + }, + + SecondaryDefault = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + SecondaryOnHover = { + Color = Colors.Flint, + Transparency = 0, + }, + SecondaryContent = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + + IconDefault = { + Color = Colors.Black, + Transparency = 0.4, -- 0.6 alpha + }, + IconEmphasis = { + Color = Colors.Flint, + Transparency = 0, + }, + IconOnHover = { + Color = Colors.Flint, + Transparency = 0, + }, + + TextEmphasis = { + Color = Colors.Flint, + Transparency = 0, + }, + TextDefault = { + Color = Colors.Smoke, + Transparency = 0, + }, + TextMuted = { + Color = Colors.Black, + Transparency = 0.4, -- 0.6 Alpha + }, + + Divider = { + Color = Colors.Pumice, + Transparency = 0, + }, + Overlay = { + Color = Colors.Black, + Transparency = 0.7, -- 0.3 Alpha + }, + DropShadow = { + Color = Colors.Black, + Transparency = 0, + }, + NavigationBar = { + Color = Colors.White, + Transparency = 0, + }, + PlaceHolder = { + Color = Colors.Black, + Transparency = 0.9, -- 0.1 Alpha + }, + + OnlineStatus = { + Color = Colors.Green, + Transparency = 0, + }, + OfflineStatus = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + + Success = { + Color = Colors.Green, + Transparency = 0, + }, + Alert = { + Color = Colors.Red, + Transparency = 0, + }, + + Badge = { + Color = Colors.Flint, + Transparency = 0, + }, + BadgeContent = { + Color = Colors.White, + Transparency = 0, + }, +} + +return theme \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/LightTheme.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/LightTheme.spec.lua new file mode 100644 index 0000000..ca136b6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/LightTheme.spec.lua @@ -0,0 +1,9 @@ +return function() + it("should be a valid theme palette.", function() + local Themes = script.Parent + local Style = Themes.Parent + local validateTheme = require(Style.Validator.validateTheme) + local LightTheme = require(Themes.LightTheme) + assert(validateTheme(LightTheme)) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/getThemeFromName.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/getThemeFromName.lua new file mode 100644 index 0000000..dca471c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/getThemeFromName.lua @@ -0,0 +1,17 @@ +local Themes = script.Parent +local Style = Themes.Parent + +local validateTheme = require(Style.Validator.validateTheme) + +return function (themeName, defaultTheme, themeMap) + local mappedTheme + if themeName ~= nil and #themeName > 0 then + mappedTheme = themeMap[string.lower(themeName)] + end + + if mappedTheme == nil then + mappedTheme = themeMap[defaultTheme] + end + assert(validateTheme(mappedTheme)) + return mappedTheme +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/getThemeFromName.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/getThemeFromName.spec.lua new file mode 100644 index 0000000..870af42 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/getThemeFromName.spec.lua @@ -0,0 +1,36 @@ +return function() + local Themes = script.Parent + local Style = Themes.Parent + local Constants = require(Style.Constants) + + local getThemeFromName = require(script.Parent.getThemeFromName) + it("should be able to get a theme palette without errors", function() + local themeMap = { + [Constants.ThemeName.Dark] = require(script.Parent.DarkTheme), + } + local themeTable = getThemeFromName(Constants.ThemeName.Dark, Constants.ThemeName.Dark,themeMap) + expect(themeTable).to.be.a("table") + end) + + it("should be able to get a theme palette using default without errors", function() + local themeMap = { + [Constants.ThemeName.Dark] = require(script.Parent.DarkTheme), + } + local themeTable = getThemeFromName("classic", Constants.ThemeName.Dark, themeMap) + expect(themeTable).to.be.a("table") + end) + + it("should throw with invalid theme palette", function() + expect(function() + local themeMap = { + [Constants.ThemeName.Dark] = { + Background = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + } + } + getThemeFromName(Constants.ThemeName.Dark, Constants.ThemeName.Dark, themeMap) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/TestStyle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/TestStyle.lua new file mode 100644 index 0000000..e3ac269 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/TestStyle.lua @@ -0,0 +1,67 @@ +local color = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, +} +local testTheme = { + BackgroundDefault = color, + BackgroundContrast = color, + BackgroundMuted = color, + BackgroundUIDefault = color, + BackgroundUIContrast = color, + BackgroundOnHover = color, + BackgroundOnPress = color, + UIDefault = color, + UIMuted = color, + UIEmphasis = color, + ContextualPrimaryDefault = color, + ContextualPrimaryOnHover = color, + ContextualPrimaryContent = color, + SystemPrimaryDefault = color, + SystemPrimaryOnHover = color, + SystemPrimaryContent = color, + SecondaryDefault = color, + SecondaryOnHover = color, + SecondaryContent = color, + IconDefault = color, + IconEmphasis = color, + IconOnHover = color, + TextEmphasis = color, + TextDefault = color, + TextMuted = color, + Divider = color, + Overlay = color, + DropShadow = color, + NavigationBar = color, + PlaceHolder = color, + OnlineStatus = color, + OfflineStatus = color, + Success = color, + Alert = color, + Badge = color, + BadgeContent = color, +} + +local font = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 1, + RelativeMinSize = 1, +} +local testFont = { + BaseSize = 10, + Title = font, + Header1 = font, + Header2 = font, + SubHeader1 = font, + Body = font, + CaptionHeader = font, + CaptionSubHeader = font, + CaptionBody = font, + Footer = font, +} + +local testStyle = { + Theme = testTheme, + Font = testFont, +} + +return testStyle \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/TestStyle.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/TestStyle.spec.lua new file mode 100644 index 0000000..92ab26c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/TestStyle.spec.lua @@ -0,0 +1,7 @@ +return function() + local validateStyle = require(script.Parent.validateStyle) + local testStyle = require(script.Parent.TestStyle) + it("Should be valid", function() + assert(validateStyle(testStyle)) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateFont.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateFont.lua new file mode 100644 index 0000000..0286a3b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateFont.lua @@ -0,0 +1,24 @@ +local Validator = script.Parent +local Style = Validator.Parent +local App = Style.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +local Font = require(UIBlox.Core.Style.Validator.validateFontInfo) + +local FontPalette = t.strictInterface({ + BaseSize = t.numberMinExclusive(0), + Title = Font, + Header1 = Font, + Header2 = Font, + SubHeader1 = Font, + Body = Font, + CaptionHeader = Font, + CaptionSubHeader = Font, + CaptionBody = Font, + Footer = Font, +}) + +return FontPalette diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateStyle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateStyle.lua new file mode 100644 index 0000000..62b7e96 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateStyle.lua @@ -0,0 +1,17 @@ +local Validator = script.Parent +local Style = Validator.Parent +local App = Style.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +local validateTheme = require(Validator.validateTheme) +local validateFont = require(Validator.validateFont) + +local StylePalette = t.strictInterface({ + Theme = validateTheme, + Font = validateFont, +}) + +return StylePalette diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateTheme.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateTheme.lua new file mode 100644 index 0000000..e5a74b5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateTheme.lua @@ -0,0 +1,59 @@ +local Validator = script.Parent +local Style = Validator.Parent +local App = Style.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +local Color = require(UIBlox.Core.Style.Validator.validateColorInfo) + +local ThemePalette = t.strictInterface({ + BackgroundDefault = Color, + BackgroundContrast = Color, + BackgroundMuted = Color, + BackgroundUIDefault = Color, + BackgroundUIContrast = Color, + BackgroundOnHover = Color, + BackgroundOnPress = Color, + + UIDefault = Color, + UIMuted = Color, + UIEmphasis = Color, + + ContextualPrimaryDefault = Color, + ContextualPrimaryOnHover = Color, + ContextualPrimaryContent = Color, + + SystemPrimaryDefault = Color, + SystemPrimaryOnHover = Color, + SystemPrimaryContent = Color, + + SecondaryDefault = Color, + SecondaryOnHover = Color, + SecondaryContent = Color, + + IconDefault = Color, + IconEmphasis = Color, + IconOnHover = Color, + + TextEmphasis = Color, + TextDefault = Color, + TextMuted = Color, + + Divider = Color, + Overlay = Color, + DropShadow = Color, + NavigationBar = Color, + PlaceHolder = Color, + + OnlineStatus = Color, + OfflineStatus = Color, + Success = Color, + Alert = Color, + + Badge = Color, + BadgeContent = Color, +}) + +return ThemePalette diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Text/ExpandableTextArea/ExpandableTextArea.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Text/ExpandableTextArea/ExpandableTextArea.lua new file mode 100644 index 0000000..be87016 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Text/ExpandableTextArea/ExpandableTextArea.lua @@ -0,0 +1,300 @@ +local ExpandableTextAreaRoot = script.Parent +local Text = ExpandableTextAreaRoot.Parent +local App = Text.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local RoactGamepad = require(Packages.RoactGamepad) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local SpringAnimatedItem = require(UIBlox.Utility.SpringAnimatedItem) +local GetTextHeight = require(UIBlox.Core.Text.GetTextHeight) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local Images = require(UIBlox.App.ImageSet.Images) +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local ExpandableTextUtils = require(UIBlox.Core.Text.ExpandableText.ExpandableTextUtils) + +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local expandableTextAutomaticResizeConfig = UIBloxConfig.expandableTextAutomaticResizeConfig + +local DEFAULT_PADDING_TOP = 30 +local PADDING_TOP = DEFAULT_PADDING_TOP +local SPACING_Y = 10 +local DEFAULT_PADDING_BOTTOM = 5 +local PADDING_BOTTOM = DEFAULT_PADDING_BOTTOM +local DOWN_ARROW_SIZE = UDim2.new(0, 36, 0, 36) +local PRESSABLE_AREA_SIZE = UDim2.new(1, 0, 0, 36) +local GRADIENT_HEIGHT = 30 +local GRADIENT_IMAGE = Images["gradient/gradient_0_100"] +local DOWN_ARROW_IMAGE_EXPAND = Images["truncate_arrows/actions_truncationExpand"] +local DOWN_ARROW_IMAGE_COLLAPSE = Images["truncate_arrows/actions_truncationCollapse"] + +-- TODO remove this when CLIPLAYEREX-1633 is fixed +local PATCHED_PADDING = 2 + +local ANIMATION_SPRING_SETTINGS = { + dampingRatio = 1, + frequency = 3.5, +} +local GRADIENT_ANIMATION_SPRING_SETTINGS = { + dampingRatio = 1, + frequency = 3.5, +} + +local SpringImageComponent = SpringAnimatedItem.wrap(ImageSetComponent.Label) +local ExpandableTextArea = Roact.PureComponent:extend("ExpandableTextArea") + +ExpandableTextArea.defaultProps = { + compactNumberOfLines = 2, + Text = "", +} + +local validateProps = t.strictInterface({ + Text = t.optional(t.string), + Position = t.optional(t.UDim2), + compactNumberOfLines = t.optional(t.number), + LayoutOrder = t.optional(t.number), + width = expandableTextAutomaticResizeConfig and t.optional(t.UDim) or t.UDim, + padding = t.optional(t.Vector2), + onClick = t.optional(t.callback), + + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) + +function ExpandableTextArea:init() + self.state = { + isExpanded = false, + frameWidth = 0, + } + + self.onClick = function() + self:setState(function(state) + return { + isExpanded = not state.isExpanded + } + end) + if self.props.onClick then + self.props.onClick(self.state.isExpanded) + end + end + + self.ref = Roact.createRef() + self.layoutRef = Roact.createRef() + + -- Remove isMounted once expandableTextAutomaticResizeConfig is removed + self.isMounted = false +end + +function ExpandableTextArea:getRef() + if UIBloxConfig.enableExperimentalGamepadSupport then + return self.props[Roact.Ref] or self.ref + end + return self.ref +end + +function ExpandableTextArea:applyFit(y) + local ref = self:getRef() + if not ref.current then + return + end + local frame = ref.current + local offset = (y + PADDING_TOP + PADDING_BOTTOM) + local width = self.props.width + if not expandableTextAutomaticResizeConfig or width then + frame.Size = UDim2.new(width.Scale, width.Offset, 0, offset) + else + frame.Size = UDim2.new(1, 0, 0, offset) + end +end + +function ExpandableTextArea:didMount() + self.isMounted = true + local layout = self.layoutRef.current + if layout then + local size = layout.AbsoluteContentSize + self:applyFit(size.y) + end +end + +function ExpandableTextArea:willUnmount() + self.isMounted = false +end + +function ExpandableTextArea:render() + assert(validateProps(self.props)) + local descriptionText = self.props.Text + local position = self.props.Position + local compactNumberOfLines = self.props.compactNumberOfLines + local layoutOrder = self.props.LayoutOrder + local width = self.props.width + local padding = self.props.padding + local ref = self:getRef() + + PADDING_TOP = padding and padding.Y or DEFAULT_PADDING_TOP + PADDING_BOTTOM = padding and padding.X or DEFAULT_PADDING_BOTTOM + + return withStyle(function(stylePalette) + return withSelectionCursorProvider(function(getSelectionCursor) + local theme = stylePalette.Theme + local font = stylePalette.Font + local textSize = font.BaseSize * font.Body.RelativeSize + local fullTextHeight, compactHeight + if UIBloxConfig.enableExperimentalGamepadSupport then + fullTextHeight, compactHeight = ExpandableTextUtils.getExpandableTextHeights( + font, self.state.frameWidth, descriptionText, compactNumberOfLines) + else + local textFont = font.Body.Font + fullTextHeight = GetTextHeight(descriptionText, textFont, textSize, self.state.frameWidth) + compactHeight = compactNumberOfLines * textSize + PATCHED_PADDING + end + + local compactSize = UDim2.new(1, 0, 0, compactHeight + PADDING_BOTTOM) + local fullSize = UDim2.new(1, 0, 0, fullTextHeight + PADDING_BOTTOM) + local canExpand = fullTextHeight > compactHeight + local isExpanded = not canExpand or self.state.isExpanded + + local size = isExpanded and fullSize or compactSize + local gradientHeight = isExpanded and 0 or GRADIENT_HEIGHT + + local isFocusable = UIBloxConfig.enableExperimentalGamepadSupport and canExpand + local frameComponent = isFocusable and RoactGamepad.Focusable.Frame or "Frame" + + return Roact.createElement(frameComponent, { + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + Position = position, + Size = (not expandableTextAutomaticResizeConfig or width) and UDim2.new(width.Scale, width.Offset, 0, 0) + or UDim2.new(1, 0, 0, 0), + SelectionImageObject = getSelectionCursor(CursorKind.RoundedRect), + [Roact.Ref] = ref, + [Roact.Change.AbsoluteSize] = function(rbx) + if self.state.frameWidth ~= rbx.AbsoluteSize.X then + -- Wrapped in spawn in order to avoid issues if Roact connects changed signal before the Size + -- prop is set in older versions of Roact (older than 1.0) In 1.0, this is fixed by deferring event + -- handlers and setState calls until after the current update]] + if expandableTextAutomaticResizeConfig then + self:setState({ + frameWidth = rbx.AbsoluteSize.X, + }) + else + spawn(function() + if self.isMounted then + self:setState({ + frameWidth = rbx.AbsoluteSize.X, + }) + end + end) + end + end + end, + + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + inputBindings = isFocusable and { + Activated = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonA, self.onClick), + } or nil, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = UDim.new(0, SPACING_Y), + [Roact.Change.AbsoluteContentSize] = function(rbx) + self:applyFit(rbx.AbsoluteContentSize.y) + end, + + [Roact.Ref] = self.layoutRef, + }), + UIPadding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, PADDING_TOP), + }), + ExpandableContainer = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + animatedValues = { + height = size.Y.Offset, + }, + mapValuesToProps = function(values) + return { + Size = UDim2.new(1, 0, size.Y.Scale, values.height), + } + end, + regularProps = { + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = true, + Size = size, + LayoutOrder = 0, + }, + springOptions = ANIMATION_SPRING_SETTINGS, + }, { + DescriptionText = Roact.createElement(GenericTextLabel, { + colorStyle = theme.TextDefault, + fontStyle = font.Body, + Size = fullSize, + Text = descriptionText, + TextSize = textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextWrapped = true, + BackgroundTransparency = 1, + }), + Gradient = canExpand and Roact.createElement(SpringImageComponent, { + animatedValues = { + height = gradientHeight, + }, + mapValuesToProps = function(values) + return { + Size = UDim2.new(1, 0, 0, values.height), + } + end, + regularProps = { + Size = UDim2.new(1, 0, 0, GRADIENT_HEIGHT), + Position = UDim2.new(0, 0, 1, 0), + AnchorPoint = Vector2.new(0, 1), + BackgroundTransparency = 1, + Image = GRADIENT_IMAGE, + ImageColor3 = theme.BackgroundDefault.Color, + }, + springOptions = GRADIENT_ANIMATION_SPRING_SETTINGS, + }) + }), + ButtonContainer = canExpand and Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 10), + LayoutOrder = 1, + }, { + PressableButton = Roact.createElement("TextButton", { + Position = UDim2.new(0, 0, 0, -24), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = PRESSABLE_AREA_SIZE, + Text = "", + [Roact.Event.Activated] = self.onClick, + }, { + DownArrow = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Position = UDim2.new(0.5, 0, 0, 0), + Image = (size == fullSize) and DOWN_ARROW_IMAGE_COLLAPSE or DOWN_ARROW_IMAGE_EXPAND, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Size = DOWN_ARROW_SIZE, + }), + }), + }) + }) + end) + end) +end + +return ExpandableTextArea \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Text/ExpandableTextArea/ExpandableTextArea.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Text/ExpandableTextArea/ExpandableTextArea.spec.lua new file mode 100644 index 0000000..7c5238f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Text/ExpandableTextArea/ExpandableTextArea.spec.lua @@ -0,0 +1,34 @@ +return function() + local ExpandableTextAreaFolder = script.Parent + local Text = ExpandableTextAreaFolder.Parent + local App = Text.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local ExpandableTextArea = require(ExpandableTextAreaFolder.ExpandableTextArea) + + local UIBloxConfig = require(UIBlox.UIBloxConfig) + local expandableTextAutomaticResizeConfig = UIBloxConfig.expandableTextAutomaticResizeConfig + + local descriptionText = [[ + This golden crown was awarded as a prize in the June 2007 Domino Rally Building Contest. + Perhaps its most unique characteristic is its ability to inspire viewers with awe + while at the same time making the wearer look goofy. + ]] + + describe("ExpandableTextArea", function() + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + Image = Roact.createElement(ExpandableTextArea, { + Text = descriptionText, + width = not expandableTextAutomaticResizeConfig and UDim.new(0, 200) or nil, + }) + }) + + local instance = Roact.mount(element, nil, "ExpandableTextArea") + Roact.unmount(instance) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/Tile.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/Tile.lua new file mode 100644 index 0000000..7b56072 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/Tile.lua @@ -0,0 +1,191 @@ +local BaseTile = script.Parent +local TileRoot = BaseTile.Parent +local App = TileRoot.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local RoactGamepad = require(Packages.RoactGamepad) + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) + +local TileName = require(BaseTile.TileName) +local TileThumbnail = require(BaseTile.TileThumbnail) +local TileBanner = require(BaseTile.TileBanner) + +local Tile = Roact.PureComponent:extend("Tile") + +local tileInterface = t.strictInterface({ + -- The footer Roact element. + footer = t.optional(t.table), + + -- The item's name that will show a loading state if nil + name = t.optional(t.string), + + -- The number of lines of text for the item name + titleTextLineCount = t.optional(t.integer), + + -- The vertical padding between elements in the ItemTile + innerPadding = t.optional(t.integer), + + -- The function that gets called on itemTile click + onActivated = t.optional(t.callback), + + -- The item's thumbnail that will show a loading state if nil + thumbnail = t.optional(t.union(t.string, t.table)), + + -- The item thumbnail's size if not UDm2.new(1, 0, 1, 0) + thumbnailSize = t.optional(t.UDim2), + + -- Optional text to display in the Item Tile banner in place of the footer + bannerText = t.optional(t.string), + + -- Whether the tile is selected or not + isSelected = t.optional(t.boolean), + + -- Optional boolean indicating whether to create an overlay to round the corners of the image + hasRoundedCorners = t.optional(t.boolean), + + -- Optional image to be displayed in the title component + -- Image information should be ImageSet compatible + titleIcon = t.optional(t.table), + + -- Optional Roact elements that are overlayed over the thumbnail component + thumbnailOverlayComponents = t.optional(t.table), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) + +local function tileBannerUseValidator(props) + if props.bannerText and props.footer then + return false, "A custom footer and bannerText can't be used together" + end + + return true +end + +local validateProps = t.intersection(tileInterface, tileBannerUseValidator) + +Tile.defaultProps = { + titleTextLineCount = 2, + innerPadding = 8, + isSelected = false, + hasRoundedCorners = true, +} + +function Tile:init() + self.state = { + tileWidth = 0, + tileHeight = 0, + } + + self.onAbsoluteSizeChange = function(rbx) + local tileWidth = rbx.AbsoluteSize.X + local tileHeight = rbx.AbsoluteSize.Y + self:setState({ + tileWidth = tileWidth, + tileHeight = tileHeight, + }) + end +end + +function Tile:render() + assert(validateProps(self.props)) + local footer = self.props.footer + local name = self.props.name + local titleTextLineCount = self.props.titleTextLineCount + local innerPadding = self.props.innerPadding + local onActivated = self.props.onActivated + local thumbnail = self.props.thumbnail + local thumbnailSize = self.props.thumbnailSize + local bannerText = self.props.bannerText + local hasRoundedCorners = self.props.hasRoundedCorners + local isSelected = self.props.isSelected + local titleIcon = self.props.titleIcon + local thumbnailOverlayComponents = self.props.thumbnailOverlayComponents + + return withStyle(function(stylePalette) + return withSelectionCursorProvider(function(getSelectionCursor) + local font = stylePalette.Font + + local tileHeight = self.state.tileHeight + local tileWidth = self.state.tileWidth + + local maxTitleTextHeight = math.ceil(font.BaseSize * font.Header2.RelativeSize * titleTextLineCount) + local footerHeight = tileHeight - tileWidth - innerPadding - maxTitleTextHeight - innerPadding + footerHeight = math.max(0, footerHeight) + + local hasFooter = footer ~= nil or bannerText ~= nil + + -- TODO: use generic/state button from UIBlox + return Roact.createElement("TextButton", { + Text = "", + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Selectable = false, + [Roact.Event.Activated] = onActivated, + [Roact.Change.AbsoluteSize] = self.onAbsoluteSizeChange, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, innerPadding), + }), + Thumbnail = Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport + and RoactGamepad.Focusable.Frame or "Frame", { + Size = UDim2.new(1, 0, 1, 0), + SizeConstraint = Enum.SizeConstraint.RelativeXX, + BackgroundTransparency = 1, + LayoutOrder = 1, + + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + [Roact.Ref] = self.props[Roact.Ref], + SelectionImageObject = getSelectionCursor(CursorKind.RoundedRectNoInset), + inputBindings = UIBloxConfig.enableExperimentalGamepadSupport and { + Activate = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonA, onActivated) + } or nil, + }, { + Image = Roact.createElement(TileThumbnail, { + Image = thumbnail, + hasRoundedCorners = hasRoundedCorners, + isSelected = isSelected, + overlayComponents = thumbnailOverlayComponents, + imageSize = thumbnailSize, + }), + }), + Name = (titleTextLineCount > 0 and tileWidth > 0) and Roact.createElement(TileName, { + titleIcon = titleIcon, + name = name, + maxHeight = maxTitleTextHeight, + maxWidth = tileWidth, + LayoutOrder = 2, + }), + FooterContainer = hasFooter and Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, footerHeight), + BackgroundTransparency = 1, + LayoutOrder = 3, + }, { + Banner = bannerText and Roact.createElement(TileBanner, { + bannerText = bannerText, + }), + Footer = not bannerText and footer, + }), + }) + end) + end) +end + +return Tile diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/Tile.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/Tile.spec.lua new file mode 100644 index 0000000..ac926bb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/Tile.spec.lua @@ -0,0 +1,61 @@ +return function() + local BaseTile = script.Parent + local TileRoot = BaseTile.Parent + local App = TileRoot.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local Tile = require(BaseTile.Tile) + + it("should create and destroy without errors", function() + local testImage = "https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814" + local testName = "some test name" + local createFooter = function() + return Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }) + end + local onActivated = function() end + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }, { + Tile = Roact.createElement(Tile, { + footer = createFooter(), + name = testName, + onActivated = onActivated, + thumbnail = testImage, + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should not render name when no lines are allocated to name", function() + local testName = "test text" + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }, { + Tile = Roact.createElement(Tile, { + name = testName, + onActivated = function() end, + titleTextLineCount = 0, + }) + }) + }) + + local container = Instance.new("ScreenGui") + local instance = Roact.mount(element, container, "TitleTest") + + expect(container.TitleTest).to.be.ok() + expect(container.TitleTest.Frame).to.be.ok() + expect(container.TitleTest.Frame:FindFirstChild("Name")).to.never.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileBanner.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileBanner.lua new file mode 100644 index 0000000..3aaf4dd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileBanner.lua @@ -0,0 +1,52 @@ +local BaseTile = script.Parent +local Tile = BaseTile.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local TileBanner = Roact.PureComponent:extend("TileBanner") + +local TEXT_PADDING = 6 + +local validateProps = t.strictInterface({ + -- The text to display in the banner + bannerText = t.string, +}) + +function TileBanner:render() + assert(validateProps(self.props)) + + local bannerText = self.props.bannerText + + return withStyle(function(stylePalette) + local font = stylePalette.Font + local theme = stylePalette.Theme + + local bannerHeight = TEXT_PADDING + font.CaptionBody.RelativeSize * font.BaseSize + + return Roact.createElement("Frame", { + BackgroundColor3 = theme.SystemPrimaryDefault.Color, + BackgroundTransparency = theme.SystemPrimaryDefault.Transparency, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, bannerHeight), + }, { + TextLabel = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Font = font.CaptionBody.Font, + TextSize = font.CaptionBody.RelativeSize * font.BaseSize, + Text = bannerText, + TextColor3 = theme.SystemPrimaryContent.Color, + TextTransparency = theme.SystemPrimaryContent.Transparency, + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Center, + Size = UDim2.new(1, 0, 1, 0), + }), + }) + end) +end + +return TileBanner \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileBanner.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileBanner.spec.lua new file mode 100644 index 0000000..17a6f45 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileBanner.spec.lua @@ -0,0 +1,26 @@ +return function() + local BaseTile = script.Parent + local Tile = BaseTile.Parent + local App = Tile.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local TileBanner = require(script.Parent.TileBanner) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 50), + }, { + TileBanner = Roact.createElement(TileBanner, { + bannerText = "ONLY 12.3K LEFT!", + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileName.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileName.lua new file mode 100644 index 0000000..70ac28a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileName.lua @@ -0,0 +1,137 @@ +local BaseTile = script.Parent +local Tile = BaseTile.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) +local Images = require(UIBlox.App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local ImageTextLabel = require(UIBlox.Core.Text.ImageTextLabel.ImageTextLabel) +local ShimmerPanel = require(UIBlox.App.Loading.ShimmerPanel) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local fixItemTilePremiumIcon = UIBloxConfig.fixItemTilePremiumIcon + +local ICON_PADDING = 4 +local LINE_PADDING = 4 + +local ItemTileName = Roact.PureComponent:extend("ItemTileName") + +local validateProps = t.strictInterface({ + LayoutOrder = t.optional(t.integer), + + maxHeight = t.intersection(t.integer, t.numberMin(0)), + maxWidth = t.intersection(t.integer, t.numberMin(0)), + + -- Loading skeleton will be rendered if name is not included + name = t.optional(t.string), + + -- Optional image to be displayed in the title component + -- Image information should be ImageSet compatible + titleIcon = t.optional(t.table), +}) + +function ItemTileName:render() + assert(validateProps(self.props)) + + local layoutOrder = self.props.LayoutOrder + local maxHeight = self.props.maxHeight + local maxWidth = self.props.maxWidth + local name = self.props.name + local titleIcon = self.props.titleIcon + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local font = stylePalette.Font + local textSize = font.BaseSize * font.Header2.RelativeSize + + if name ~= nil then + local titleIconSize = titleIcon and titleIcon.ImageRectSize / Images.ImagesResolutionScale or Vector2.new(0, 0) + + if fixItemTilePremiumIcon then + return Roact.createElement(ImageTextLabel, { + imageProps = titleIcon and { + BackgroundTransparency = 1, + Image = titleIcon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Size = UDim2.new(0, titleIconSize.X, 0, titleIconSize.Y), + AnchorPoint = Vector2.new(0, 0), + Position = UDim2.new(0, 0, 0, 0), + } or nil, + + genericTextLabelProps = { + fontStyle = font.Header2, + colorStyle = theme.TextEmphasis, + Text = name, + TextTruncate = Enum.TextTruncate.AtEnd, + }, + + frameProps = { + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, + + maxSize = Vector2.new(maxWidth, maxHeight), + padding = ICON_PADDING, + }) + else + local labelWidth = titleIcon and (maxWidth - titleIconSize.X - ICON_PADDING) or maxWidth + + local labelTextSize = GetTextSize(name, textSize, font.Header2.Font, Vector2.new(labelWidth, maxHeight)) + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + Size = UDim2.new(0, maxWidth, 0, labelTextSize.Y), + }, { + Icon = titleIcon and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = titleIcon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Size = UDim2.new(0, titleIconSize.X, 0, titleIconSize.Y), + }), + + Name = Roact.createElement("TextLabel", { + AnchorPoint = Vector2.new(1, 0), + Position = UDim2.new(1, 0, 0, 0), + Size = UDim2.new(0, labelWidth, 1, 0), + BackgroundTransparency = 1, + TextSize = textSize, + TextColor3 = theme.TextEmphasis.Color, + TextTransparency = theme.TextEmphasis.Transparency, + Font = font.Header2.Font, + Text = name, + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextWrapped = true, + }), + }) + end + else + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + Size = UDim2.new(0, maxWidth, 0, maxHeight), + }, { + FirstLine = Roact.createElement(ShimmerPanel, { + Size = UDim2.new(1, 0, 0, textSize), + }), + + SecondLine = Roact.createElement(ShimmerPanel, { + Position = UDim2.new(0, 0, 0, textSize + LINE_PADDING), + Size = UDim2.new(0.4, 0, 0, textSize), + }), + }) + end + end) +end + +return ItemTileName \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileName.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileName.spec.lua new file mode 100644 index 0000000..2fcb0da --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileName.spec.lua @@ -0,0 +1,46 @@ +return function() + local BaseTile = script.Parent + local Tile = BaseTile.Parent + local App = Tile.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local TileName = require(script.Parent.TileName) + + it("should create and destroy without errors", function() + local testName = "some test name" + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }, { + TileName = Roact.createElement(TileName, { + name = testName, + maxHeight = 100, + maxWidth = 100, + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy with loading state without errors", function() + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }, { + TileName = Roact.createElement(TileName, { + name = nil, + maxHeight = 100, + maxWidth = 100, + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileSelectionOverlay.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileSelectionOverlay.lua new file mode 100644 index 0000000..4ba9054 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileSelectionOverlay.lua @@ -0,0 +1,62 @@ +local BaseTile = script.Parent +local Tile = BaseTile.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local Images = require(UIBlox.App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local TileSelectionOverlay = Roact.PureComponent:extend("TileSelectionOverlay") + +local PADDING_RIGHT = 6 +local PADDING_TOP = 6 + +TileSelectionOverlay.validateProps = t.strictInterface({ + ZIndex = t.optional(t.integer), + cornerRadius = t.optional(t.UDim), +}) + +TileSelectionOverlay.defaultProps = { + cornerRadius = UDim.new(0, 0), +} + +function TileSelectionOverlay:render() + local zIndex = self.props.ZIndex + local cornerRadius = self.props.cornerRadius + + local selectionIcon = Images["icons/actions/selectOn"] + local imageSize = selectionIcon.ImageRectSize / Images.ImagesResolutionScale + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + return Roact.createElement("Frame", { + BackgroundColor3 = theme.Overlay.Color, + BackgroundTransparency = theme.Overlay.Transparency, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + ZIndex = zIndex, + }, { + SelectionImage = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(1, 0), + BackgroundTransparency = 1, + Image = selectionIcon, + Position = UDim2.new(1, -PADDING_RIGHT, 0, PADDING_TOP), + Size = UDim2.new(0, imageSize.X, 0, imageSize.Y), + }), + UICorner = UIBloxConfig.useNewUICornerRoundedCorners and cornerRadius ~= UDim.new(0, 0) + and Roact.createElement("UICorner", { + CornerRadius = cornerRadius, + }) or nil, + }) + end) +end + +return TileSelectionOverlay \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileSelectionOverlay.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileSelectionOverlay.spec.lua new file mode 100644 index 0000000..de66139 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileSelectionOverlay.spec.lua @@ -0,0 +1,26 @@ +return function() + local BaseTile = script.Parent + local Tile = BaseTile.Parent + local App = Tile.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local TileSelectionOverlay = require(script.Parent.TileSelectionOverlay) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }, { + TileSelectionOverlay = Roact.createElement(TileSelectionOverlay, { + cornerRadius = UDim.new(0, 10), + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileThumbnail.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileThumbnail.lua new file mode 100644 index 0000000..31bcaed --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileThumbnail.lua @@ -0,0 +1,106 @@ +local BaseTile = script.Parent +local Tile = BaseTile.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local Images = require(UIBlox.App.ImageSet.Images) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local LoadableImage = require(UIBlox.App.Loading.LoadableImage) +local TileSelectionOverlay = require(BaseTile.TileSelectionOverlay) + +local TileThumbnail = Roact.PureComponent:extend("TileThumbnail") + +TileThumbnail.defaultProps = { + imageSize = UDim2.new(1, 0, 1, 0), +} + +local CORNER_RADIUS = UDim.new(0, 10) + +function TileThumbnail:render() + local hasRoundedCorners = self.props.hasRoundedCorners + local image = self.props.Image + local imageSize = self.props.imageSize + local isSelected = self.props.isSelected + local overlayComponents = self.props.overlayComponents + + local isImageSetImage = typeof(image) == "table" + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + }, { + ImageContainer = Roact.createElement("Frame", { + BackgroundColor3 = theme.PlaceHolder.Color, + BackgroundTransparency = theme.PlaceHolder.Transparency, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 0, + }, { + Image = not isImageSetImage and Roact.createElement(LoadableImage, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundColor3 = theme.PlaceHolder.Color, + BackgroundTransparency = theme.PlaceHolder.Transparency, + Image = image, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = imageSize, + ZIndex = 0, + cornerRadius = UIBloxConfig.useNewUICornerRoundedCorners and hasRoundedCorners and CORNER_RADIUS or nil, + showFailedStateWhenLoadingFailed = true, + useShimmerAnimationWhileLoading = true, + }), + + ImageSetImage = isImageSetImage and Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = image, + ImageColor3 = theme.UIEmphasis.Color, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = imageSize, + }, { + UICorner = UIBloxConfig.useNewUICornerRoundedCorners and hasRoundedCorners + and Roact.createElement("UICorner", { + CornerRadius = CORNER_RADIUS, + }) or nil, + }), + + UICorner = UIBloxConfig.useNewUICornerRoundedCorners and hasRoundedCorners + and Roact.createElement("UICorner", { + CornerRadius = CORNER_RADIUS, + }) or nil, + }), + + ComponentsFrame = overlayComponents and Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 1, + }, overlayComponents), + + SelectionOverlay = isSelected and Roact.createElement(TileSelectionOverlay, { + ZIndex = 2, + cornerRadius = hasRoundedCorners and CORNER_RADIUS or nil, + }), + + RoundedCornersOverlay = not UIBloxConfig.useNewUICornerRoundedCorners and hasRoundedCorners + and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = Images["component_assets/circle_17_mask"], + ImageColor3 = theme.BackgroundDefault.Color, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 3, + }), + }) + end) +end + +return TileThumbnail diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileThumbnail.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileThumbnail.spec.lua new file mode 100644 index 0000000..e95dff3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileThumbnail.spec.lua @@ -0,0 +1,50 @@ +return function() + local BaseTile = script.Parent + local Tile = BaseTile.Parent + local App = Tile.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local TileThumbnail = require(script.Parent.TileThumbnail) + + it("should create and destroy without errors", function() + local testImage = "https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814" + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }, { + ItemTileIcon = Roact.createElement(TileThumbnail, { + Image = testImage, + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy with additional components without errors", function() + local testImage = "https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814" + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }, { + ItemTileIcon = Roact.createElement(TileThumbnail, { + Image = testImage, + isSelected = true, + overlayComponents = { + Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + }), + } + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/Enum/ItemTileEnums.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/Enum/ItemTileEnums.lua new file mode 100644 index 0000000..1e565fa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/Enum/ItemTileEnums.lua @@ -0,0 +1,23 @@ +local Tile = script.Parent.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local enumerate = require(Packages.enumerate) + +local strict = require(UIBlox.Utility.strict) + +return strict({ + ItemIconType = enumerate("ItemIconType", { + "AnimationBundle", + "Bundle" + }), + StatusStyle = enumerate("StatusStyle", { + "Alert", + "Info" + }), + Restriction = enumerate("Restriction", { + "Limited", + "LimitedUnique" + }) +}, script.Name) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemIcon.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemIcon.lua new file mode 100644 index 0000000..020ad1e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemIcon.lua @@ -0,0 +1,62 @@ +local ItemTile = script.Parent +local Tile = ItemTile.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local Images = require(UIBlox.App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local ItemTileEnums = require(Tile.Enum.ItemTileEnums) + +local ItemIcon = Roact.PureComponent:extend("ItemIcon") + +local ItemIconTypesMap = { + [ItemTileEnums.ItemIconType.AnimationBundle] = Images["icons/status/item/bundle"], + [ItemTileEnums.ItemIconType.Bundle] = Images["icons/status/item/bundle"], +} + +local PADDING_BOTTOM = 12 +local PADDING_RIGHT = 12 + +local function isValidItemIconType(value) + if ItemIconTypesMap[value] then + return true + end + + return false, "Unknown ItemType " .. value +end + +local validateProps = t.strictInterface({ + -- Enum specifying the item type + itemIconType = t.intersection(enumerateValidator(ItemTileEnums.ItemIconType), isValidItemIconType), +}) + +function ItemIcon:render() + assert(validateProps(self.props)) + + local itemIconType = self.props.itemIconType + + local icon = ItemIconTypesMap[itemIconType] + local imageSize = icon.ImageRectSize / Images.ImagesResolutionScale + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + return Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(1, 1), + BackgroundTransparency = 1, + Image = icon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Position = UDim2.new(1, -PADDING_RIGHT, 1, -PADDING_BOTTOM), + Size = UDim2.new(0, imageSize.X, 0, imageSize.Y), + }) + end) +end + +return ItemIcon diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemRestrictionStatus.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemRestrictionStatus.lua new file mode 100644 index 0000000..f1ad44f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemRestrictionStatus.lua @@ -0,0 +1,117 @@ +local ItemTile = script.Parent +local Tile = ItemTile.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) +local Images = require(UIBlox.App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local ItemTileEnums = require(Tile.Enum.ItemTileEnums) + +local ItemRestrictionStatus = Roact.PureComponent:extend("ItemRestrictionStatus") + +local MAX_TEXT_SIZE = Vector2.new(50, 20) +local CONTENT_PADDING = Vector2.new(8, 8) + +local PADDING_LEFT = 12 +local PADDING_BOTTOM = 12 +local TEXT_PADDING = 10 + +local validateProps = t.strictInterface({ + -- Enum specifying the restriction type + restrictionTypes = t.map(enumerateValidator(ItemTileEnums.Restriction), t.boolean), + + -- Optional information about the restriction + restrictionInfo = t.optional(t.table), +}) + +local function getAdditionalText(restrictionTypes, restrictionInfo) + local additionalText = "" + + if restrictionTypes[ItemTileEnums.Restriction.LimitedUnique] then + additionalText = "#" + end + + if restrictionInfo and restrictionInfo.limitedSerialNumber then + additionalText = additionalText .. " " .. restrictionInfo.limitedSerialNumber + end + + return additionalText +end + +local function getRestrictionIcon(restrictionTypes) + if restrictionTypes[ItemTileEnums.Restriction.Limited] or + restrictionTypes[ItemTileEnums.Restriction.LimitedUnique] then + return Images["icons/status/item/limited"] + end + + return nil +end + +function ItemRestrictionStatus:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local fontInfo = stylePalette.Font + + local restrictionInfo = self.props.restrictionInfo + local restrictionTypes = self.props.restrictionTypes + local additionalText = getAdditionalText(restrictionTypes, restrictionInfo) + + local font = fontInfo.CaptionHeader.Font + local fontSize = fontInfo.BaseSize * fontInfo.CaptionHeader.RelativeSize + local textSize = GetTextSize(additionalText, fontSize, font, MAX_TEXT_SIZE) + + local icon = getRestrictionIcon(restrictionTypes) + local imageSize = icon and icon.ImageRectSize / Images.ImagesResolutionScale or Vector2.new(0, 0) + + local xSize = imageSize.X + textSize.X + CONTENT_PADDING.X + local ySize = math.max(imageSize.Y, textSize.Y) + CONTENT_PADDING.Y + + return Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0, 1), + BackgroundTransparency = 1, + Image = Images["component_assets/circle_17"], + ImageColor3 = theme.UIDefault.Color, + ImageTransparency = theme.UIDefault.Transparency, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + Position = UDim2.new(0, PADDING_LEFT, 1, -PADDING_BOTTOM), + Size = UDim2.new(0, xSize, 0, ySize), + }, { + Icon = icon and Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + Image = icon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Position = UDim2.new(0, CONTENT_PADDING.X / 2, 0.5, 0), + Size = UDim2.new(0, imageSize.X, 0, imageSize.Y), + }), + + Text = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Font = font, + TextSize = fontSize, + Text = additionalText, + TextColor3 = theme.TextMuted.Color, + TextTransparency = theme.TextMuted.TextTransparency, + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + Position = UDim2.new(0, imageSize.X, 0, 0), + Size = UDim2.new(0, textSize.X + TEXT_PADDING, 1, 0), + }), + }) + end) +end + +return ItemRestrictionStatus diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTile.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTile.lua new file mode 100644 index 0000000..d267bd8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTile.lua @@ -0,0 +1,161 @@ +local ItemTileRoot = script.Parent +local TileRoot = ItemTileRoot.Parent +local App = TileRoot.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local Images = require(UIBlox.App.ImageSet.Images) +local ItemRestrictionStatus = require(ItemTileRoot.ItemRestrictionStatus) +local ItemTileStatus = require(ItemTileRoot.ItemTileStatus) +local ItemTileEnums = require(TileRoot.Enum.ItemTileEnums) +local ItemIcon = require(ItemTileRoot.ItemIcon) +local Tile = require(TileRoot.BaseTile.Tile) + +local ItemTile = Roact.PureComponent:extend("ItemTile") + +local itemTileInterface = t.strictInterface({ + -- The footer Roact element. + footer = t.optional(t.table), + + -- The item's name that will show a loading state if nil + name = t.optional(t.string), + + -- The number of lines of text for the item name + titleTextLineCount = t.optional(t.integer), + + -- The vertical padding between elements in the ItemTile + innerPadding = t.optional(t.integer), + + -- The function that gets called on itemTile click + onActivated = t.optional(t.callback), + + -- The item's thumbnail that will show a loading state if nil + thumbnail = t.optional(t.string), + + -- Optional text to display in the Item Tile banner in place of the footer + bannerText = t.optional(t.string), + + -- Optional enum specifying the item icon type, will create an icon showing the item type on the card + itemIconType = t.optional(enumerateValidator(ItemTileEnums.ItemIconType)), + + -- Whether the tile is selected or not + isSelected = t.optional(t.boolean), + + -- Whether the tile is for a premium item or not + isPremium = t.optional(t.boolean), + + -- Enums specifying the restriction types if there are restrictions for the item + restrictionTypes = t.optional(t.map(enumerateValidator(ItemTileEnums.Restriction), t.boolean)), + + -- Optional information about the restriction + restrictionInfo = t.optional(t.table), + + -- Optional boolean indicating whether to create an overlay to round the corners of the image + hasRoundedCorners = t.optional(t.boolean), + + -- Optional tile status text + statusText = t.optional(t.string), + + -- Enum specifying the style for the status component + statusStyle = t.optional(enumerateValidator(ItemTileEnums.StatusStyle)), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) + +local function tileBannerUseValidator(props) + if props.bannerText and props.footer then + return false, "A custom footer and bannerText can't be used together" + end + + return true +end + +local validateProps = t.intersection(itemTileInterface, tileBannerUseValidator) + +ItemTile.defaultProps = { + titleTextLineCount = 2, + innerPadding = 8, + isSelected = false, + isPremium = false, + hasRoundedCorners = true, +} + +function ItemTile:render() + assert(validateProps(self.props)) + + local footer = self.props.footer + local bannerText = self.props.bannerText + local hasRoundedCorners = self.props.hasRoundedCorners + local innerPadding = self.props.innerPadding + local isPremium = self.props.isPremium + local isSelected = self.props.isSelected + local itemIconType = self.props.itemIconType + local name = self.props.name + local onActivated = self.props.onActivated + local restrictionInfo = self.props.restrictionInfo + local restrictionTypes = self.props.restrictionTypes + local statusStyle = self.props.statusStyle + local statusText = self.props.statusText + local titleTextLineCount = self.props.titleTextLineCount + local thumbnail = self.props.thumbnail + + local hasOverlayComponents = false + local overlayComponents = {} + + if itemIconType then + hasOverlayComponents = true + + overlayComponents.ItemIconType = Roact.createElement(ItemIcon, { + itemIconType = itemIconType, + }) + end + + if restrictionTypes then + hasOverlayComponents = true + + overlayComponents.RestrictionStatus = Roact.createElement(ItemRestrictionStatus, { + restrictionInfo = restrictionInfo, + restrictionTypes = restrictionTypes, + }) + end + + if statusText then + hasOverlayComponents = true + + overlayComponents.Status = Roact.createElement(ItemTileStatus, { + statusStyle = statusStyle, + statusText = statusText, + }) + end + + return Roact.createElement(Tile, { + bannerText = bannerText, + footer = footer, + hasRoundedCorners = hasRoundedCorners, + innerPadding = innerPadding, + isSelected = isSelected, + name = name, + onActivated = onActivated, + thumbnail = thumbnail, + thumbnailOverlayComponents = hasOverlayComponents and overlayComponents or nil, + titleIcon = isPremium and Images["icons/status/premium_small"] or nil, + titleTextLineCount = titleTextLineCount, + + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + [Roact.Ref] = self.props[Roact.Ref], + }) +end + +return ItemTile diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTileFooter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTileFooter.lua new file mode 100644 index 0000000..4e93d3a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTileFooter.lua @@ -0,0 +1,87 @@ +local ItemTileRoot = script.Parent +local TileRoot = ItemTileRoot.Parent +local App = TileRoot.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local Images = require(UIBlox.App.ImageSet.Images) + +local ShimmerPanel = require(UIBlox.App.Loading.ShimmerPanel) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local ICON_PADDING = 4 + +local ItemTileFooter = Roact.PureComponent:extend("ItemTileFooter") + +local validateProps = t.strictInterface({ + -- The price text of footer + priceText = t.optional(t.string), + + -- Is the item owned + isOwned = t.optional(t.boolean) +}) + +function ItemTileFooter:render() + assert(validateProps(self.props)) + + local priceText = self.props.priceText + local isOwned = self.props.isOwned + + local icon = Images["icons/common/robux_small"] + if isOwned then + icon = Images["icons/status/item/owned"] + end + return withStyle(function(stylePalette) + local font = stylePalette.Font.SubHeader1.Font + local fontSize = stylePalette.Font.BaseSize * stylePalette.Font.SubHeader1.RelativeSize + local theme = stylePalette.Theme + + local iconSize = icon.ImageRectSize / Images.ImagesResolutionScale + + local priceIsNumber = priceText and tonumber(priceText:sub(1, 1)) + local showIcon = priceText and (priceIsNumber or isOwned) + + local iconPadding = 0 + if showIcon then + iconPadding = iconSize.X + ICON_PADDING + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + Shimmer = not priceText and Roact.createElement(ShimmerPanel, { + Size = UDim2.new(0.8, 0, 0, fontSize), + }), + + icon = showIcon and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = icon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Size = UDim2.new(0, iconSize.X, 0, iconSize.Y), + }), + + TextLabel = priceText and Roact.createElement("TextLabel", { + AnchorPoint = Vector2.new(1, 0), + BackgroundTransparency = 1, + Position = UDim2.new(1, 0, 0, 0), + Size = UDim2.new(1, -iconPadding, 1, 0), + Font = font, + TextColor3 = theme.SecondaryContent.Color, + TextTransparency = theme.SecondaryContent.Transparency, + TextSize = fontSize, + Text = priceText, + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + }) + }) + end) +end + +return ItemTileFooter \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTileStatus.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTileStatus.lua new file mode 100644 index 0000000..e6e7e3f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTileStatus.lua @@ -0,0 +1,94 @@ +local ItemTileRoot = script.Parent +local TileRoot = ItemTileRoot.Parent +local App = TileRoot.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) +local Images = require(UIBlox.App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local ItemTileEnums = require(TileRoot.Enum.ItemTileEnums) + +local ItemTileStatus = Roact.PureComponent:extend("ItemTileStatus") + +local MAX_TEXT_SIZE = Vector2.new(50, 20) +local TEXT_PADDING = Vector2.new(10, 6) + +local PADDING_LEFT = 12 +local PADDING_TOP = 12 + +local validateProps = t.strictInterface({ + -- The text to display in the status component + statusText = t.string, + + -- Enum specifying the style for the status component + statusStyle = enumerateValidator(ItemTileEnums.StatusStyle), +}) + +local function getStyle(theme, statusStyle) + if statusStyle == ItemTileEnums.StatusStyle.Info then + return { + Background = theme.SystemPrimaryDefault, + Text = theme.SystemPrimaryContent, + } + elseif statusStyle == ItemTileEnums.StatusStyle.Alert then + return { + Background = theme.Alert, + Text = theme.TextEmphasis, + } + else + return { + Background = theme.SystemPrimaryDefault, + Text = theme.SystemPrimaryContent, + } + end +end + +function ItemTileStatus:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local fontInfo = stylePalette.Font + + local statusText = self.props.statusText + local statusStyle = self.props.statusStyle + + local font = fontInfo.CaptionHeader.Font + local fontSize = fontInfo.BaseSize * fontInfo.CaptionHeader.RelativeSize + local textSize = GetTextSize(statusText, fontSize, font, MAX_TEXT_SIZE) + + local styleInfo = getStyle(theme, statusStyle) + + return Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = Images["component_assets/circle_17"], + ImageColor3 = styleInfo.Background.Color, + ImageTransparency = styleInfo.Background.Transparency, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + Position = UDim2.new(0, PADDING_LEFT, 0, PADDING_TOP), + Size = UDim2.new(0, textSize.X + TEXT_PADDING.X, 0, textSize.Y + TEXT_PADDING.Y), + }, { + Text = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Font = font, + TextSize = fontSize, + Text = statusText, + TextColor3 = styleInfo.Text.Color, + TextTransparency = styleInfo.Text.TextTransparency, + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + Size = UDim2.new(1, 0, 1, 0), + }), + }) + end) +end + +return ItemTileStatus \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/MenuTile/MenuTile.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/MenuTile/MenuTile.lua new file mode 100644 index 0000000..4301cce --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/MenuTile/MenuTile.lua @@ -0,0 +1,264 @@ +local TextService = game:GetService("TextService") + +local MenuTileRoot = script.Parent +local Tile = MenuTileRoot.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Core = UIBlox.Core +local Packages = UIBlox.Parent + +local Otter = require(Packages.Otter) +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local Badge = require(App.Indicator.Badge) +local IconSize = require(App.ImageSet.Enum.IconSize) +local getIconSize = require(App.ImageSet.getIconSize) +local Images = require(App.ImageSet.Images) + +local ControlState = require(Core.Control.Enum.ControlState) +local Interactable = require(Core.Control.Interactable) +local ImageSetComponent = require(Core.ImageSet.ImageSetComponent) +local validateImage = require(Core.ImageSet.Validator.validateImage) + +local withStyle = require(UIBlox.Style.withStyle) +local divideTransparency = require(UIBlox.Utility.divideTransparency) + +local FULLY_TRANSPARENT = 1 +local LIST_PADDING = UDim.new(0, 12) +local PADDING_PADDING = UDim.new(0, 8) +local TITLE_MAX_NUMBER_OF_LINES = 2 + +-- ~0.33 duration +local SPRING_PARAMETERS = { + frequency = 6, + dampingRatio = 1, +} + +local Z_INDEX = { + BACKGROUND = 1, + HOVER_MASK = 2, + ICON_AND_TITLE_CONTAINER = 3, + BADGE_CONTAINER = 4, + ROUNDED_CORNERS_MASK = 5, +} + +local LAYOUT_ORDER = { + ICON = 1, + TITLE = 2, +} + +local MenuTile = Roact.Component:extend("MenuTile") + +MenuTile.defaultProps = { + size = UDim2.fromScale(1, 1), +} + +MenuTile.validateProps = t.strictInterface({ + -- Frame Props + size = t.optional(t.UDim2), + position = t.optional(t.UDim2), + layoutOrder = t.optional(t.number), + + -- Menu Tile specific props + badgeValue = t.optional(t.union(t.string, t.number)), + icon = validateImage, + title = t.string, + onActivated = t.callback, +}) + +function MenuTile:init() + self.hoverTransparency, self.updateHoverTransparency = Roact.createBinding(FULLY_TRANSPARENT) + self.hoverTransparencyMotor = Otter.createSingleMotor(FULLY_TRANSPARENT) + self.hoverTransparencyMotor:onStep(self.updateHoverTransparency) + self.hoverTransparencyMotor:onComplete(function(value) + if value == FULLY_TRANSPARENT then + self:setState({ + showHoverMask = false, + }) + end + end) + + self:setState({ + backgroundTransparency = 0, + iconTransparency = 0, + titleTransparency = 0, + showHoverMask = false, + }) +end + +function MenuTile:render() + local backgroundTransparency = self.state.backgroundTransparency + local iconTransparency = self.state.iconTransparency + local titleTransparency = self.state.titleTransparency + + local badgeValue = self.props.badgeValue + local icon = self.props.icon + local layoutOrder = self.props.layoutOrder + local onActivated = self.props.onActivated + local position = self.props.position + local size = self.props.size + local title = self.props.title + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + local backgroundStyle = theme.BackgroundUIDefault + local iconStyle = theme.IconDefault + local roundedCornersStyle = theme.BackgroundDefault + local hoverStyle = theme.BackgroundOnHover + + local titleStyle = theme.TextDefault + local titleFont = stylePalette.Font.SubHeader1 + local titleFontSize = titleFont.RelativeSize * stylePalette.Font.BaseSize + local titleTextOneLineSizeY = TextService:GetTextSize(title, titleFontSize, titleFont.Font, + Vector2.new(100, titleFontSize)).Y + + local function onStateChanged(oldState, newState) + if newState == ControlState.Hover then + self:setState({ + backgroundTransparency = backgroundStyle.Transparency, + iconTransparency = iconStyle.Transparency, + titleTransparency = titleStyle.Transparency, + showHoverMask = true, + }) + self.hoverTransparencyMotor:setGoal(Otter.spring(hoverStyle.Transparency, SPRING_PARAMETERS)) + elseif newState == ControlState.Default then + self:setState({ + backgroundTransparency = backgroundStyle.Transparency, + iconTransparency = iconStyle.Transparency, + titleTransparency = titleStyle.Transparency, + }) + self.hoverTransparencyMotor:setGoal(Otter.spring(FULLY_TRANSPARENT, SPRING_PARAMETERS)) + elseif newState == ControlState.Pressed then + self:setState({ + backgroundTransparency = divideTransparency(backgroundStyle.Transparency, 2), + iconTransparency = divideTransparency(iconStyle.Transparency,2), + titleTransparency = divideTransparency(titleStyle.Transparency, 2), + showHoverMask = false, + }) + self.hoverTransparencyMotor:setGoal(Otter.instant(FULLY_TRANSPARENT)) + end + end + + return Roact.createElement(Interactable, { + Size = size, + Position = position, + BackgroundTransparency = 1, -- Default is 0 + LayoutOrder = layoutOrder, + onStateChanged = onStateChanged, + [Roact.Event.Activated] = onActivated, + }, { + MenuTileFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = size, + }, { + Background = Roact.createElement("Frame", { + BackgroundColor3 = backgroundStyle.Color, + BackgroundTransparency = backgroundTransparency, + BorderSizePixel = 0, + Size = UDim2.fromScale(1,1), + ZIndex = Z_INDEX.BACKGROUND, + }, { + RoundedCornerUI = UIBloxConfig.useNewUICornerRoundedCorners and Roact.createElement("UICorner", { + CornerRadius = UDim.new(0, 8), + }), + }), + HoverMask = self.state.showHoverMask and Roact.createElement("Frame", { + BackgroundColor3 = hoverStyle.Color, + BackgroundTransparency = self.hoverTransparency, + BorderSizePixel = 0, + Size = UDim2.fromScale(1,1), + ZIndex = Z_INDEX.HOVER_MASK, + }, { + RoundedCornerUI = UIBloxConfig.useNewUICornerRoundedCorners and Roact.createElement("UICorner", { + CornerRadius = UDim.new(0, 8), + }), + }), + IconAndTitleContainer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1,1), + ZIndex = Z_INDEX.ICON_AND_TITLE_CONTAINER + }, { + IconAndTitleUIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + Padding = LIST_PADDING, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + IconAndTitleUIPadding = Roact.createElement("UIPadding", { + PaddingBottom = PADDING_PADDING, + PaddingLeft = PADDING_PADDING, + PaddingRight = PADDING_PADDING, + -- pad by the height of the title line, to position Icon in the middle, + -- title height is always 2 lines high + PaddingTop = PADDING_PADDING + UDim.new(0, titleTextOneLineSizeY), + }), + Icon = icon and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = icon, + ImageColor3 = iconStyle.Color, + ImageTransparency = iconTransparency, + LayoutOrder = LAYOUT_ORDER.ICON, + Size = UDim2.fromOffset(getIconSize(IconSize.Large), getIconSize(IconSize.Large)), + }), + -- GenericText, does not limit to 2 lines + Title = title and Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Font = titleFont.Font, + LayoutOrder = LAYOUT_ORDER.TITLE, + Size = UDim2.new(1, 0, 0, titleTextOneLineSizeY * TITLE_MAX_NUMBER_OF_LINES), + Text = title, + TextColor3 = titleStyle.Color, + TextSize = titleFontSize, + TextTransparency = titleTransparency, + TextTruncate = Enum.TextTruncate.AtEnd, + TextWrapped = true, + TextYAlignment = Enum.TextYAlignment.Top, + }), + }), + BadgeContainer = badgeValue and Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1,1), + ZIndex = Z_INDEX.BADGE_CONTAINER, + }, { + BadgeUIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + Padding = LIST_PADDING, + VerticalAlignment = Enum.VerticalAlignment.Top, + }), + BadgeUIPadding = Roact.createElement("UIPadding", { + PaddingBottom = PADDING_PADDING, + PaddingLeft = PADDING_PADDING, + PaddingRight = PADDING_PADDING, + PaddingTop = PADDING_PADDING, + }), + Badge = Roact.createElement(Badge, { + value = badgeValue, + }), + }), + RoundedCornersMask = not UIBloxConfig.useNewUICornerRoundedCorners and + Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = Images["component_assets/circle_17_mask"], + ImageColor3 = roundedCornersStyle.Color, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + Size = UDim2.fromScale(1, 1), + ZIndex = Z_INDEX.ROUNDED_CORNERS_MASK, + }), + } + ) + }) + end) +end + +function MenuTile:willUnmount() + if self.hoverTransparencyMotor then + self.hoverTransparencyMotor:destroy() + end +end +return MenuTile diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/MenuTile/MenuTile.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/MenuTile/MenuTile.spec.lua new file mode 100644 index 0000000..f98284b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/MenuTile/MenuTile.spec.lua @@ -0,0 +1,83 @@ +return function() + local MenuTileRoot = script.Parent + local Tile = MenuTileRoot.Parent + local App = Tile.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local Images = require(App.ImageSet.Images) + local MenuTile = require(MenuTileRoot.MenuTile) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local function onActivatedDummy() end + + describe("mount/unmount", function() + it("should mount and unmount with default properties", function() + local menuTileWithStyle = mockStyleComponent({ + MenuTileTest = Roact.createElement(MenuTile, { + -- required + icon = Images["icons/menu/shop_large"], + onActivated = onActivatedDummy, + title = "Shop", + }) + }) + local handle = Roact.mount(menuTileWithStyle) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + it("should mount and unmount with valid properties", function() + local menuTileWithStyle = mockStyleComponent({ + MenuTileTest = Roact.createElement(MenuTile, { + -- required + icon = Images["icons/menu/shop_large"], + onActivated = onActivatedDummy, + title = "Shop", + -- optional + badgeValue = "0", + layoutOrder = 2, + position = UDim2.new(0, 50, 0,100), + size = UDim2.new(1, 30, 1, 50), + }) + }) + local handle = Roact.mount(menuTileWithStyle) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + -- skipping this until https://jira.rbx.com/browse/MOBLUAPP-2424 is merged to CI + itSKIP("mount should throw when created with invalid properties", function() + local function expectToThrowForInvalidProps(props) + -- make sure we start with valid props + local testProps = { + icon = Images["icons/menu/shop_large"], + onActivated = onActivatedDummy, + title = "Shop" + } + -- add/replace props passed in + for name, _ in pairs(props) do + testProps[name] = props[name] + end + + local menuTileWithStyle = mockStyleComponent({ + MenuTileTest = Roact.createElement(MenuTile, testProps) + }) + + expect(function() + Roact.mount(menuTileWithStyle) + end).to.throw() + end + + expectToThrowForInvalidProps({ icon = 1 }) + expectToThrowForInvalidProps({ onActivated= 2 }) + expectToThrowForInvalidProps({ title = 3 }) + expectToThrowForInvalidProps({ badgeValue = onActivatedDummy }) + expectToThrowForInvalidProps({ layoutOrder = "3" }) + expectToThrowForInvalidProps({ position = 3 }) + expectToThrowForInvalidProps({ size = 3 }) + expectToThrowForInvalidProps({ NotInTheInterface = "Really it is not there" }) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/SaveTile/SaveTile.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/SaveTile/SaveTile.lua new file mode 100644 index 0000000..a6b2676 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/SaveTile/SaveTile.lua @@ -0,0 +1,66 @@ +local SaveTileRoot = script.Parent +local TileRoot = SaveTileRoot.Parent +local App = TileRoot.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(UIBlox.App.ImageSet.Images) +local Tile = require(TileRoot.BaseTile.Tile) + +local SaveTile = Roact.PureComponent:extend("SaveTile") + + +local validateProps = t.strictInterface({ + -- Optional boolean indicating whether to create an overlay to round the corners of the image + hasRoundedCorners = t.optional(t.boolean), + + -- The function that gets called on SaveTile click + onActivated = t.optional(t.callback), + + -- The item's thumbnail that will show a loading state if nil + thumbnail = t.optional(t.union(t.string, t.table)), + + -- The item thumbnail's size + thumbnailSize = t.optional(t.UDim2), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) + +SaveTile.defaultProps = { + hasRoundedCorners = true, + thumbnail = Images["icons/actions/edit/add"], + thumbnailSize = UDim2.new(0, 36, 0, 36), +} + +function SaveTile:render() + assert(validateProps(self.props)) + + local hasRoundedCorners = self.props.hasRoundedCorners + local onActivated = self.props.onActivated + local thumbnail = self.props.thumbnail + local thumbnailSize = self.props.thumbnailSize + + return Roact.createElement(Tile, { + hasRoundedCorners = hasRoundedCorners, + name = "", + onActivated = onActivated, + thumbnail = thumbnail, + thumbnailSize = thumbnailSize, + + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + [Roact.Ref] = self.props[Roact.Ref], + }) +end + +return SaveTile diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/__stories__/Tile.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/__stories__/Tile.story.lua new file mode 100644 index 0000000..6e4974d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/__stories__/Tile.story.lua @@ -0,0 +1,150 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local TileRoot = script.Parent.Parent +local App = TileRoot.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Tile = require(TileRoot.BaseTile.Tile) +local Images = require(UIBlox.App.ImageSet.Images) + +local TileStoryContainer = Roact.PureComponent:extend("TileStoryContainer") + +local PADDING = 20 +local FOOTER_HEIGHT = 50 +local NAME_HEIGHT = 20 + +local function createFooter() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, FOOTER_HEIGHT), + }, { + TextLabel = Roact.createElement("TextLabel", { + Text = "Your custom footer goes here.", + Size = UDim2.new(1, 0, 1, 0), + }) + }) +end + +function TileStoryContainer:init() + self.state = { + image = nil, + longerLoadImage = nil, + name = nil, + } +end + +function TileStoryContainer:didMount() + -- Simulate component load + spawn(function() + wait(2.0) + self:setState({ + image = "rbxassetid://924320031", + name = "Item Name", + }) + wait(2.0) + self:setState({ + longerLoadImage = "rbxassetid://924320031", + }) + end) +end + +function TileStoryContainer:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, 20), + PaddingTop = UDim.new(0, 20), + }), + + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 20), + }), + + FullItemTileContainer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = 0, + Size = UDim2.new(0, 200, 0, 200 + NAME_HEIGHT + PADDING + FOOTER_HEIGHT), + }, { + FullItemTile = Roact.createElement(Tile, { + footer = createFooter(), + name = self.state.name, + onActivated = function() end, + hasRoundedCorners = false, + thumbnail = self.state.image, + }), + }), + + LongerLoadItemTileContainer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = 1, + Size = UDim2.new(0, 200, 0, 200 + NAME_HEIGHT + PADDING+ FOOTER_HEIGHT), + }, { + ThumbnailLongerLoadItemTile = Roact.createElement(Tile, { + footer = createFooter(), + name = self.state.name, + onActivated = function() end, + hasRoundedCorners = false, + thumbnail = self.state.longerLoadImage, + }), + }), + + FooterlessItemTileContainer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 200, 0, 200 + NAME_HEIGHT), + LayoutOrder = 2, + }, { + FooterlessItemTile = Roact.createElement(Tile, { + name = self.state.name, + onActivated = function() end, + hasRoundedCorners = false, + thumbnail = self.state.image, + }), + }), + + OverriddenSizedImageTileContainer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 200, 0, 200 + NAME_HEIGHT), + LayoutOrder = 3, + }, { + OverriddenSizedImagesetTile = Roact.createElement(Tile, { + name = self.state.name, + onActivated = function() end, + hasRoundedCorners = false, + thumbnail = self.state.image, + thumbnailSize = UDim2.new(0, 100, 0, 100), + }), + }), + + ImagesetTileContainer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 200, 0, 200 + NAME_HEIGHT), + LayoutOrder = 4, + }, { + ImagesetTile = Roact.createElement(Tile, { + name = "", + onActivated = function() end, + thumbnail = Images["icons/status/item/owned"], + thumbnailSize = UDim2.new(0, 25, 0, 25), + }), + }), + }) +end + +return function(target) + local styleProvider = Roact.createElement(StoryView, {}, { + Roact.createElement(TileStoryContainer) + }) + + local handle = Roact.mount(styleProvider, target, "TileStoryContainer") + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/Enum/SlidingDirection.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/Enum/SlidingDirection.lua new file mode 100644 index 0000000..e064287 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/Enum/SlidingDirection.lua @@ -0,0 +1,11 @@ +local Core = script.Parent.Parent.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate(script.Name, { + "Up", + "Down", + "Left", + "Right", +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.lua new file mode 100644 index 0000000..5d8bd9e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.lua @@ -0,0 +1,74 @@ +local AnimationRoot = script.Parent +local Core = AnimationRoot.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local t = require(UIBlox.Parent.t) +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local SlidingDirection = require(AnimationRoot.Enum.SlidingDirection) +local SpringAnimatedItem = require(UIBlox.Utility.SpringAnimatedItem) + +local ANIMATION_SPRING_SETTINGS = { + dampingRatio = 1, + frequency = 4, +} + +-- A transparent frame covers the whole page, represent current navigated page +local SlidingContainer = Roact.PureComponent:extend("SlidingContainer") + +local InitialPosition = { + -- Slide up from bottom of page + [SlidingDirection.Up] = UDim2.new(0, 0, 1, 0), + + -- Slide down from top of page + [SlidingDirection.Down] = UDim2.new(0, 0, -1, 0), + + -- Slide left from right of page + [SlidingDirection.Left] = UDim2.new(1, 0, 0, 0), + + -- Slide right from left of page + [SlidingDirection.Right] = UDim2.new(-1, 0, 0, 0), +} + +local validateProps = t.strictInterface({ + layoutOrder = t.optional(t.integer), + onComplete = t.optional(t.callback), + show = t.optional(t.boolean), + slidingDirection = enumerateValidator(SlidingDirection), + springOptions = t.optional(t.table), + [Roact.Children] = t.optional(t.table), +}) + +SlidingContainer.defaultProps = { + springOptions = ANIMATION_SPRING_SETTINGS, +} + +function SlidingContainer:render() + assert(validateProps(self.props)) + + local show = self.props.show + local slidingDirection = self.props.slidingDirection + + return Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + springOptions = self.props.springOptions, + animatedValues = { + step = show and 0 or 1, + }, + mapValuesToProps = function(values) + local position = InitialPosition[slidingDirection] + return { + Position = UDim2.new(position.X.Scale * values.step, 0, position.Y.Scale * values.step, 0), + } + end, + regularProps = { + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + Position = show and InitialPosition[slidingDirection] or UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + }, + onComplete = self.props.onComplete, + [Roact.Children] = self.props[Roact.Children], + }) +end + +return SlidingContainer diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.spec.lua new file mode 100644 index 0000000..38fc8d4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.spec.lua @@ -0,0 +1,44 @@ +return function() + local UIBloxRoot = script.Parent.Parent.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local SlidingContainer = require(script.Parent.SlidingContainer) + local SlidingDirection = require(script.Parent.Enum.SlidingDirection) + + local createSlidingContainer = function(props) + return mockStyleComponent({ + SlidingContainer = Roact.createElement(SlidingContainer, props) + }) + end + + it("should throw on empty slidingDirection", function() + local element = createSlidingContainer({}) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors with valid slidingDirection", function() + local element = createSlidingContainer({ + slidingDirection = SlidingDirection.Down, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when show is true", function() + local element = createSlidingContainer({ + show = true, + slidingDirection = SlidingDirection.Right, + [Roact.Children] = { + Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }), + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.story.lua new file mode 100644 index 0000000..893a676 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.story.lua @@ -0,0 +1,54 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Roact = require(ReplicatedStorage.Packages.Roact) + +local SlidingContainer = require(script.Parent.SlidingContainer) +local SlidingDirection = require(script.Parent.Enum.SlidingDirection) + +local SlidingContainerComponent = Roact.PureComponent:extend("SlidingContainerComponent") + +function SlidingContainerComponent:init() + self.state = { + show = false, + } + + self.buttonRef = Roact.createRef() +end + +function SlidingContainerComponent:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + SlidingContainer = Roact.createElement(SlidingContainer, { + show = self.state.show, + slidingDirection = SlidingDirection.Down, + }, { + Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }) + }), + ControlButton = Roact.createElement("TextButton", { + BackgroundColor3 = Color3.fromRGB(2, 183, 87), + Size = UDim2.new(0, 200, 0, 50), + Text = "Page slide down", + ZIndex = 2, + [Roact.Event.Activated] = function() + self:setState({ + show = not self.state.show, + }) + self.buttonRef.current.Text = self.state.show + and "Page slide up" or "Page slide down" + end, + [Roact.Ref] = self.buttonRef, + }) + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(SlidingContainerComponent), target, "SlidingContainer") + + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.lua new file mode 100644 index 0000000..bbc9005 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.lua @@ -0,0 +1,74 @@ +local RunService = game:GetService("RunService") +local AnimationRoot = script.Parent +local Core = AnimationRoot.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local t = require(UIBlox.Parent.t) + +local ImageSetLabel = require(UIBlox.Core.ImageSet.ImageSetComponent).Label + +local SpinningImage = Roact.PureComponent:extend("SpinningImage") + +SpinningImage.validateProps = t.strictInterface({ + image = t.table, + size = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + position = t.optional(t.UDim2), + rotationRate = t.optional(t.number), +}) + +SpinningImage.defaultProps = { + rotationRate = 360, +} + +function SpinningImage:init() + self.state = { + angle = 0, + } +end + +function SpinningImage:didMount() + self.heartbeatConnection = RunService.Heartbeat:Connect(function(dt) + local newAngle = self.state.angle + self.props.rotationRate*dt + if newAngle > 360 then + newAngle = newAngle - 360 + elseif newAngle < 0 then + newAngle = newAngle + 360 + end + self:setState({ + angle = newAngle + }) + end) +end + +function SpinningImage:willUnmount() + self.heartbeatConnection:Disconnect() +end + +function SpinningImage.getDerivedStateFromProps(nextProps, lastState) + local imageSize = nextProps.image.ImageRectSize + return { + size = nextProps.size or UDim2.fromOffset(imageSize.X, imageSize.Y) + } +end + +function SpinningImage:render() + return Roact.createElement("Frame", { + Size = self.state.size, + AnchorPoint = self.props.anchorPoint, + Position = self.props.position, + BackgroundTransparency = 1, + }, { + inner = Roact.createElement(ImageSetLabel, { + Size = self.state.size, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.fromScale(0.5, 0.5), + Image = self.props.image, + Rotation = self.state.angle, + BackgroundTransparency = 1, + }) + }) +end + +return SpinningImage diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.spec.lua new file mode 100644 index 0000000..4432e4f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.spec.lua @@ -0,0 +1,34 @@ +return function() + local UIBloxRoot = script.Parent.Parent.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local SpinningImage = require(script.Parent.SpinningImage) + local Images = require(UIBloxRoot.App.ImageSet.Images) + + it("should throw on empty image", function() + local element = Roact.createElement(SpinningImage, {}) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors with valid image", function() + local element = Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should accept all valid props", function() + local element = Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + position = UDim2.new(1, 2, 3, 4), + anchorPoint = Vector2.new(1, 2), + rotationRate = 1, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.story.lua new file mode 100644 index 0000000..b0b4d09 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.story.lua @@ -0,0 +1,47 @@ +local CoreRoot = script.Parent.Parent +local UIBlox = CoreRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local Images = require(UIBlox.App.ImageSet.Images) + +local SpinningImage = require(script.Parent.SpinningImage) + +local SpinningImageStory = Roact.PureComponent:extend("SpinningImageStory") + +function SpinningImageStory:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + Layout = Roact.createElement("UIListLayout"), + Spinner1 = Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + rotationRate = -720, + }), + Spinner2 = Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + rotationRate = -360, + }), + Spinner3 = Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + rotationRate = 0, + }), + Spinner4 = Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + }), + Spinner5 = Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + rotationRate = 720, + }), + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(SpinningImageStory), target, "SpinningImageContainer") + + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/ThreeSectionBar.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/ThreeSectionBar.lua new file mode 100644 index 0000000..a22b10b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/ThreeSectionBar.lua @@ -0,0 +1,206 @@ +local Bar = script.Parent +local Core = Bar.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) +local FitFrame = require(Packages.FitFrame) +local bindingValidator = require(Core.Utility.bindingValidator) + +local PADDING_BETWEEN_SIDE_AND_CENTER = 8 + +local ThreeSectionBar = Roact.PureComponent:extend("ThreeSectionBar") +ThreeSectionBar.validateProps = t.strictInterface({ + BackgroundColor3 = t.Color3, + + BackgroundTransparency = t.optional(t.union(t.number, bindingValidator(t.number))), + barHeight = t.optional(t.number), + contentPaddingLeft = t.optional(t.UDim), + contentPaddingRight = t.optional(t.UDim), + estimatedCenterWidth = t.optional(t.number), + marginLeft = t.optional(t.number), + marginRight = t.optional(t.number), + onWidthChange = t.optional(t.callback), + renderCenter = t.optional(t.callback), + renderLeft = t.optional(t.callback), + renderRight = t.optional(t.callback), +}) + +ThreeSectionBar.defaultProps = { + barHeight = 32, + BackgroundTransparency = 0, + marginLeft = 0, + marginRight = 0, + + contentPaddingLeft = UDim.new(0, 0), + contentPaddingRight = UDim.new(0, 0), + + renderLeft = nil, + renderRight = nil, + renderCenter = nil, + + onWidthChange = function() + return nil + end, + estimatedCenterWidth = math.huge, +} + +function ThreeSectionBar:init() + self.leftWidth, self.updateLeftWidth = Roact.createBinding(0) + self.rightWidth, self.updateRightWidth = Roact.createBinding(0) + self.fullWidth, self.updateFullWidth = Roact.createBinding(0) + + self.computeCenteredSize = function(widths) + local leftWidth, rightWidth, fullWidth = widths[1], widths[2], widths[3] + + local largestWidth = math.max(leftWidth, rightWidth) + + local centerPoint = Vector2.new(fullWidth/2, 0) + local largestEdge = Vector2.new(largestWidth, 0) + + local distance = (centerPoint - largestEdge).magnitude + + if not self.props.renderLeft or not self.props.renderRight then + return UDim2.new(1, -largestWidth, 1, 0) + end + + -- multiply by 2, since we are splitting distance by both sides + return UDim2.new(0, distance * 2, 1, 0) + end + + self.computeBumpedPosition = function(widths) + local leftWidth, rightWidth, fullWidth = widths[1], widths[2], widths[3] + + local x = (fullWidth - leftWidth - rightWidth) /2 + + return UDim2.new(0, leftWidth + x, 0.5, 0) + end + + self.computeBumpedSize = function(widths) + local leftWidth, rightWidth = widths[1], widths[2] + + return UDim2.new(1, -leftWidth - rightWidth, 1, 0) + end +end + +function ThreeSectionBar:render() + local centerAnchor + local centerPosition + + if not self.props.renderLeft and self.props.renderRight then + centerAnchor = Vector2.new(0, 0.5) + centerPosition = UDim2.fromScale(0, 0.5) + elseif self.props.renderLeft and not self.props.renderRight then + centerAnchor = Vector2.new(1, 0.5) + centerPosition = UDim2.fromScale(1, 0.5) + else + centerAnchor = Vector2.new(0.5, 0.5) + centerPosition = UDim2.fromScale(0.5, 0.5) + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, self.props.barHeight), + BackgroundColor3 = self.props.BackgroundColor3, + BackgroundTransparency = self.props.BackgroundTransparency, + BorderSizePixel = 0, + + [Roact.Change.AbsoluteSize] = function(rbx) + self.props.onWidthChange(rbx.AbsoluteSize.X) + self.updateFullWidth(rbx.AbsoluteSize.X) + end, + }, { + leftFrame = self.props.renderLeft and Roact.createElement(FitFrame.FitFrameHorizontal, { + AnchorPoint = Vector2.new(0, 0), + Position = UDim2.fromScale(0, 0), + BackgroundTransparency = 1, + + minimumSize = UDim.new(0, 200), + height = UDim.new(1, 0), + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + contentPadding = self.props.contentPaddingLeft, + margin = { + top = 0, + left = self.props.marginLeft, + right = PADDING_BETWEEN_SIDE_AND_CENTER, + bottom = 0, + }, + [Roact.Change.AbsoluteSize] = function(rbx) + self.updateLeftWidth(rbx.AbsoluteSize.X) + end, + }, { + leftContent = self.props.renderLeft(Cryo.Dictionary.join(self.props, { + [Roact.Children] = { + -- introduce a size constraint in order to give the renderRight priority + sizeConstraint = Roact.createElement("UISizeConstraint", { + MaxSize = Roact.joinBindings({self.leftWidth, self.rightWidth, self.fullWidth}):map(function(widths) + local _, rightWidth, fullWidth = widths[1], widths[2], widths[3] + + local maxLeftWidth = math.max(0, fullWidth - rightWidth - self.props.marginLeft) + + return Vector2.new(maxLeftWidth, math.huge) + end), + }), + } + })), + }), + + centerFrame = self.props.renderCenter and Roact.createElement("Frame", { + AnchorPoint = centerAnchor, + BackgroundTransparency = 1, + + Position = Roact.joinBindings({self.leftWidth, self.rightWidth, self.fullWidth}):map(function(widths) + local centeredSize = self.computeCenteredSize(widths) + + if centeredSize.X.Offset <= self.props.estimatedCenterWidth then + return self.computeBumpedPosition(widths) + else + return centerPosition + end + end), + Size = Roact.joinBindings({self.leftWidth, self.rightWidth, self.fullWidth}):map(function(widths) + local centeredSize = self.computeCenteredSize(widths) + + if centeredSize.X.Offset <= self.props.estimatedCenterWidth then + return self.computeBumpedSize(widths) + else + return centeredSize + end + end), + }, { + ["$layout"] = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + centerContent = self.props.renderCenter(self.props), + }), + + rightFrame = self.props.renderRight and Roact.createElement(FitFrame.FitFrameHorizontal, { + AnchorPoint = Vector2.new(1, 0), + Position = UDim2.fromScale(1, 0), + BackgroundTransparency = 1, + + minimumSize = UDim.new(0, 200), + height = UDim.new(1, 0), + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + VerticalAlignment = Enum.VerticalAlignment.Center, + contentPadding = self.props.contentPaddingRight, + margin = { + top = 0, + left = PADDING_BETWEEN_SIDE_AND_CENTER, + right = self.props.marginRight, + bottom = 0, + }, + [Roact.Change.AbsoluteSize] = function(rbx) + self.updateRightWidth(rbx.AbsoluteSize.X) + end, + }, { + rightContent = self.props.renderRight(self.props), + }) + }) +end + +return ThreeSectionBar diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoCenterRender.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoCenterRender.story.lua new file mode 100644 index 0000000..cbed773 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoCenterRender.story.lua @@ -0,0 +1,35 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Bar = script.Parent.Parent +local Core = Bar.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local ThreeSectionBar = require(Bar.ThreeSectionBar) + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(ThreeSectionBar, { + renderLeft = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 255, 255), + Size = UDim2.new(0, 50, 1, 0), + Text = "Left", + }) + end, + renderRight = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 255, 222), + Size = UDim2.new(0, 50, 1, 0), + Text = "Right", + }) + end + }), + }), target, "ThreeSectionBar") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoLeftRender.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoLeftRender.story.lua new file mode 100644 index 0000000..6632063 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoLeftRender.story.lua @@ -0,0 +1,35 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Bar = script.Parent.Parent +local Core = Bar.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local ThreeSectionBar = require(Bar.ThreeSectionBar) + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(ThreeSectionBar, { + renderCenter = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 222, 255), + Size = UDim2.new(1, 0, 1, 0), + Text = "This Element fills up the remaining space", + }) + end, + renderRight = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 255, 222), + Size = UDim2.new(0, 50, 1, 0), + Text = "Right", + }) + end + }), + }), target, "ThreeSectionBar") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoRightRender.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoRightRender.story.lua new file mode 100644 index 0000000..33ea4a2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoRightRender.story.lua @@ -0,0 +1,35 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Bar = script.Parent.Parent +local Core = Bar.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local ThreeSectionBar = require(Bar.ThreeSectionBar) + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(ThreeSectionBar, { + renderLeft = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 255, 255), + Size = UDim2.new(0, 50, 1, 0), + Text = "Left", + }) + end, + renderCenter = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 222, 255), + Size = UDim2.new(1, 0, 1, 0), + Text = "This Element fills up the remaining space", + }) + end, + }), + }), target, "ThreeSectionBar") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/ThreeSectionBar.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/ThreeSectionBar.story.lua new file mode 100644 index 0000000..7e6f1cb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/ThreeSectionBar.story.lua @@ -0,0 +1,42 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Bar = script.Parent.Parent +local Core = Bar.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local ThreeSectionBar = require(Bar.ThreeSectionBar) + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(ThreeSectionBar, { + renderLeft = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 255, 255), + Size = UDim2.new(0, 50, 1, 0), + Text = "Left", + }) + end, + renderCenter = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 222, 255), + Size = UDim2.new(0, 50, 1, 0), + Text = "Center", + }) + end, + renderRight = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 255, 222), + Size = UDim2.new(0, 50, 1, 0), + Text = "Right", + }) + end + }), + }), target, "ThreeSectionBar") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/GenericButton.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/GenericButton.lua new file mode 100644 index 0000000..94cd632 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/GenericButton.lua @@ -0,0 +1,196 @@ +--[[ + Create a generic button that can be themed for different state the background and content. +]] +local Button = script.Parent +local Core = Button.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) + +local Interactable = require(Core.Control.Interactable) + +local ControlState = require(Core.Control.Enum.ControlState) +local getContentStyle = require(script.Parent.getContentStyle) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local ImageSetComponent = require(Core.ImageSet.ImageSetComponent) +local ShimmerPanel = require(UIBlox.App.Loading.ShimmerPanel) +local IconSize = require(UIBlox.App.ImageSet.Enum.IconSize) +local getIconSize = require(UIBlox.App.ImageSet.getIconSize) +local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel) + +local validateImage = require(Core.ImageSet.Validator.validateImage) + +local CONTENT_PADDING = 5 + +local GenericButton = Roact.PureComponent:extend("GenericButton") + +function GenericButton:init() + self.state = { + controlState = ControlState.Initialize + } + + self.onStateChanged = function(oldState, newState) + self:setState({ + controlState = newState, + }) + if self.props.onStateChanged then + self.props.onStateChanged(oldState, newState) + end + end +end + +local colorStateMap = t.interface({ + -- The default state theme color class + [ControlState.Default] = t.string, +}) + +local validateProps = t.interface({ + --The icon of the button + icon = t.optional(validateImage), + + --The text of the button + text = t.optional(t.string), + + --The image being used as the background of the button + buttonImage = validateImage, + + --The theme color class mapping for different button states + buttonStateColorMap = colorStateMap, + + --The theme color class mapping for different content tates + contentStateColorMap = t.optional(colorStateMap), + + --The theme color class mapping for different text tates + textStateColorMap = t.optional(colorStateMap), + + --The theme color class mapping for different icon tates + iconStateColorMap = t.optional(colorStateMap), + + --Is the button disabled + isDisabled = t.optional(t.boolean), + + --Is the button loading + isLoading = t.optional(t.boolean), + + --The activated callback for the button + onActivated = t.callback, + + --The state change callback for the button + onStateChanged = t.optional(t.callback), + + --A Boolean value that determines whether user events are ignored and sink input + userInteractionEnabled = t.optional(t.boolean), + + -- Note that this component can accept all valid properties of the Roblox ImageButton instance +}) + +GenericButton.defaultProps = { + isDisabled = false, + isLoading = false, + SliceCenter = Rect.new(8, 8, 9, 9), +} + +function GenericButton:render() + return withStyle(function(style) + + assert(validateProps(self.props)) + assert(t.table(style), "Style provider is missing.") + + local currentState = self.state.controlState + + local text = self.props.text + local icon = self.props.icon + local isLoading = self.props.isLoading + local isDisabled = self.props.isDisabled + + local buttonStateColorMap = self.props.buttonStateColorMap + local contentStateColorMap = self.props.contentStateColorMap + local textStateColorMap = self.props.textStateColorMap or contentStateColorMap + local iconStateColorMap = self.props.iconStateColorMap or contentStateColorMap + + if text then + assert(colorStateMap(textStateColorMap), "textStateColorMap is missing or invalid.") + end + if icon then + assert(colorStateMap(iconStateColorMap), "iconStateColorMap is missing or invalid.") + end + + if isLoading then + isDisabled = true + end + + local buttonStyle = getContentStyle(buttonStateColorMap, currentState, style) + local textStyle = text and getContentStyle(textStateColorMap, currentState, style) + local iconStyle = icon and getContentStyle(iconStateColorMap, currentState, style) + local buttonImage = self.props.buttonImage + local fontStyle = style.Font.Header2 + + local buttonContentLayer + if isLoading then + buttonContentLayer = { + isLoadingShimmer = Roact.createElement(ShimmerPanel, { + Size = UDim2.new(1, 0, 1, 0), + }) + } + else + buttonContentLayer = self.props[Roact.Children] or { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, CONTENT_PADDING), + }), + Icon = icon and Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.new(0, getIconSize(IconSize.Medium), 0, getIconSize(IconSize.Medium)), + BackgroundTransparency = 1, + Image = icon, + ImageColor3 = iconStyle.Color, + ImageTransparency = iconStyle.Transparency, + LayoutOrder = 1, + }) or nil, + Text = text and Roact.createElement(GenericTextLabel, { + BackgroundTransparency = 1, + Text = text, + fontStyle = fontStyle, + colorStyle = textStyle, + LayoutOrder = 2, + }) or nil, + } + end + + return Roact.createElement(Interactable, Cryo.Dictionary.join(self.props, { + icon = Cryo.None, + text = Cryo.None, + buttonImage = Cryo.None, + buttonStateColorMap = Cryo.None, + contentStateColorMap = Cryo.None, + textStateColorMap = Cryo.None, + iconStateColorMap = Cryo.None, + onActivated = Cryo.None, + isLoading = Cryo.None, + [Roact.Children] = Cryo.None, + isDisabled = isDisabled, + onStateChanged = self.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + Image = buttonImage, + ScaleType = Enum.ScaleType.Slice, + ImageColor3 = buttonStyle.Color, + ImageTransparency = buttonStyle.Transparency, + BackgroundTransparency = 1, + AutoButtonColor = false, + [Roact.Event.Activated] = self.props.onActivated, + }), { + ButtonContent = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, buttonContentLayer) + }) + end) +end + +return GenericButton diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/GenericButton.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/GenericButton.spec.lua new file mode 100644 index 0000000..8b90a21 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/GenericButton.spec.lua @@ -0,0 +1,161 @@ +return function() + local Button = script.Parent + local Core = Button.Parent + local UIBlox = Core.Parent + local Packages = UIBlox.Parent + + local App = UIBlox.App + local Images = require(App.ImageSet.Images) + + local Roact = require(Packages.Roact) + local GenericButton = require(Button.GenericButton) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local ControlState = require(Core.Control.Enum.ControlState) + + local IMAGE = Images["component_assets/circle_17"] + + local text = "Button" + local icon = Images["icons/common/robux_small"] + + local BUTTON_STATE_COLOR = { + [ControlState.Default] = "SystemPrimaryDefault", + } + + local CONTENT_STATE_COLOR = { + [ControlState.Default] = "SystemPrimaryContent", + } + + it("should create and destroy a button without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + text = text, + icon = icon, + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a button that is disabled without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + text = text, + icon = icon, + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + onActivated = function()end, + isDisabled = true, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a button that is loading without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + text = text, + icon = icon, + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + onActivated = function()end, + isLoading = true, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a button overriding text color without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + text = text, + icon = icon, + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + textStateColorMap = CONTENT_STATE_COLOR, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a button overriding icon color without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + text = text, + icon = icon, + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + iconStateColorMap = CONTENT_STATE_COLOR, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a button overriding text and icon color without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + text = text, + icon = icon, + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + textStateColorMap = CONTENT_STATE_COLOR, + iconStateColorMap = CONTENT_STATE_COLOR, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a button without text and icon color without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should be created as a disabled button", function() + -- luacheck: ignore unused argument oldState + local buttonState = nil + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + onActivated = function()end, + onStateChanged = function(oldState, newState) + buttonState = newState + end, + isDisabled = true, + }), + }) + + local instance = Roact.mount(element) + expect(buttonState).to.equal(ControlState.Disabled) + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/HoverButtonBackground.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/HoverButtonBackground.lua new file mode 100644 index 0000000..69a482e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/HoverButtonBackground.lua @@ -0,0 +1,32 @@ +--[[ + Creates a background square that shows up behind buttons when hovered. +]] +local Button = script.Parent +local Core = Button.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(Core.Style.withStyle) + +local CORNER_RADIUS = 8 + +local HoverButtonBackground = Roact.PureComponent:extend("HoverButtonBackground") + +function HoverButtonBackground:render() + return withStyle(function(style) + local backgroundHover = style.Theme.BackgroundOnHover + + return Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundColor3 = backgroundHover.Color, + BackgroundTransparency = backgroundHover.Transparency, + }, { + corner = Roact.createElement("UICorner", { + CornerRadius = UDim.new(0, CORNER_RADIUS), + }), + }) + end) +end + +return HoverButtonBackground diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/__stories__/GenericButton.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/__stories__/GenericButton.story.lua new file mode 100644 index 0000000..830e06c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/__stories__/GenericButton.story.lua @@ -0,0 +1,119 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryWithControls = require(ReplicatedStorage.Packages.StoryComponents.StoryWithControls) + +local Button = script.Parent.Parent +local Core = Button.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local App = UIBlox.App +local Images = require(App.ImageSet.Images) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) + +local GenericButton = require(Button.GenericButton) + +local GenericButtonOverviewComponent = Roact.PureComponent:extend("GenericButtonOverviewComponent") + +function GenericButtonOverviewComponent:init() + self.isMounted = false + self.state = { + isDisabled = false, + isLoading = false, + userInteractionEnabled = true, + } + + self.toggleDisabled = function() + if self.isMounted then + self:setState({ + isDisabled = not self.state.isDisabled + }) + end + end + + self.toggleLoading = function() + if self.isMounted then + self:setState({ + isLoading = not self.state.isLoading + }) + end + end + + self.toggleUserInteraction = function() + if self.isMounted then + self:setState({ + userInteractionEnabled = not self.state.userInteractionEnabled + }) + end + end +end + +function GenericButtonOverviewComponent:render() + local isDisabled = self.state.isDisabled + local isLoading = self.state.isLoading + local userInteractionEnabled = self.state.userInteractionEnabled + local buttonImage = Images["component_assets/circle_17"] + return withStyle(function(style) + return Roact.createElement(StoryWithControls, { + title = "Generic Button", + subTitle = "<>", + controls = { + { + text = self.state.disabled and "Enable Buttons" or "Disable Buttons", + onActivated = self.toggleDisabled, + }, + { + text = isLoading and "Load Buttons" or "Unload Buttons", + onActivated = self.toggleLoading, + }, + { + text = "userInteractionEnabled = "..tostring(userInteractionEnabled), + onActivated = self.toggleUserInteraction, + }, + }, + }, { + Button = Roact.createElement(GenericButton, { + Size = UDim2.new(0, 144, 0, 48), + buttonImage = buttonImage, + buttonStateColorMap = { + [ControlState.Default] = "UIDefault", + [ControlState.Hover] = "UIEmphasis", + }, + contentStateColorMap = { + [ControlState.Default] = "UIDefault", + }, + isDisabled = isDisabled, + isLoading = isLoading, + userInteractionEnabled = userInteractionEnabled, + onActivated = function() + print("Generic Button Clicked!") + end, + onStateChanged = function(oldState, newState) + print("state changed \n oldState:", oldState, " newState:", newState) + end + }) + }) + end) +end + +function GenericButtonOverviewComponent:didMount() + self.isMounted = true +end + +function GenericButtonOverviewComponent:willUnmount() + self.isMounted = false +end + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(GenericButtonOverviewComponent), + }), target, "Button") + + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/getContentStyle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/getContentStyle.lua new file mode 100644 index 0000000..6e0b291 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/getContentStyle.lua @@ -0,0 +1,22 @@ +local Button = script.Parent +local Core = Button.Parent + +local ControlState = require(Core.Control.Enum.ControlState) + +return function(contentMap, controlState, style) + local contentThemeClass = contentMap[controlState] + or contentMap[ControlState.Default] + + local contentStyle = { + Color = style.Theme[contentThemeClass].Color, + Transparency = style.Theme[contentThemeClass].Transparency, + } + + --Based on the design specs, the disabled and pressed state is 0.5 * alpha value + if controlState == ControlState.Disabled or + controlState == ControlState.Pressed then + contentStyle.Transparency = 0.5 * contentStyle.Transparency + 0.5 + end + + return contentStyle +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericCell.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericCell.lua new file mode 100644 index 0000000..32f8b00 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericCell.lua @@ -0,0 +1,162 @@ +local Cell = script.Parent +local Core = Cell.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel) +local Interactable = require(Core.Control.Interactable) +local validateColorInfo = require(UIBlox.Core.Style.Validator.validateColorInfo) +local validateFontInfo = require(UIBlox.Core.Style.Validator.validateFontInfo) + +local GenericCell = Roact.PureComponent:extend("GenericCell") + +local TOP_BOTTOM_PADDING = 12 +local LEFT_RIGHT_PADDING = 24 + +GenericCell.validateProps = t.strictInterface({ + -- Callback for when this selection is activated. + onActivated = t.optional(t.callback), + + -- Whether this selection is selected or not. + isSelected = t.optional(t.boolean), + + -- If this cell is disabled. + isDisabled = t.optional(t.boolean), + + -- Callback for when the Control State has changed. + onStateChanged = t.callback, + + -- Center title text. + titleText = t.optional(t.string), + + -- Center subtitle text. + subtitleText = t.optional(t.string), + + -- Generic right content to render. + renderRightContent = t.optional(t.callback), + + -- Width of generic right content to render, + rightContentWidth = t.optional(t.number), + + -- Generic left content to render. + renderLeftContent = t.optional(t.callback), + + -- Width of the generic left content to render. + leftContentWidth = t.optional(t.number), + + -- Color style. + colorStyle = validateColorInfo, + + -- Text Style for the title text. + textStyle = t.optional(validateColorInfo), + + -- Font style for the title text. + fontStyle = t.optional(validateFontInfo), + + -- Text style for the subtitle text. + subtitleTextStyle = t.optional(validateColorInfo), + + -- Font style for the subtitle text. + subtitleFontStyle = t.optional(validateFontInfo), + + -- Divider style. + dividerStyle = t.table, + + -- optional parameters for RoactGamepad + [Roact.Ref] = t.optional(t.table), + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), +}) + +GenericCell.defaultProps = { + rightContentWidth = 0, + leftContentWidth = 0, + isDisabled = false, +} + +function GenericCell:render() + assert(self.validateProps(self.props)) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BorderSizePixel = 0, + BackgroundTransparency = 1, + }, { + Interactable = Roact.createElement(Interactable, { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = self.props.colorStyle.Color, + BackgroundTransparency = self.props.colorStyle.Transparency, + BorderSizePixel = 0, + AutoButtonColor = false, + [Roact.Event.Activated] = (not self.props.isDisabled) and self.props.onActivated, + [Roact.Ref] = self.props[Roact.Ref], + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + isDisabled = self.props.isDisabled, + onStateChanged = self.props.onStateChanged, + }, { + Contents = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + LayoutOrder = 1, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, LEFT_RIGHT_PADDING), + PaddingRight = UDim.new(0, LEFT_RIGHT_PADDING), + PaddingTop = UDim.new(0, TOP_BOTTOM_PADDING), + PaddingBottom = UDim.new(0, TOP_BOTTOM_PADDING), + }), + UIListLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + Frame = Roact.createElement("Frame", { + Size = UDim2.new(1, -self.props.rightContentWidth - self.props.leftContentWidth, 1, 0), + BackgroundTransparency = 1, + LayoutOrder = 2, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + TitleText = self.props.titleText and Roact.createElement(GenericTextLabel, { + Size = UDim2.new(1, 0, 1, 0), + colorStyle = self.props.textStyle, + fontStyle = self.props.fontStyle, + Text = self.props.titleText, + LayoutOrder = 1, + TextXAlignment = Enum.TextXAlignment.Left, + }), + SubTitleText = self.props.subtitleText and Roact.createElement(GenericTextLabel, { + Size = UDim2.new(1, 0, 1, 0), + colorStyle = self.props.subtitleTextStyle, + fontStyle = self.props.subtitleFontStyle, + Text = self.props.subtitleText, + LayoutOrder = 2, + TextXAlignment = Enum.TextXAlignment.Left, + }), + }), + RightContent = self.props.renderRightContent and self.props.renderRightContent(), + LeftContent = self.props.renderLeftContent and self.props.renderLeftContent(), + }), + Divider = Roact.createElement("Frame", { + Size = UDim2.new(1, -LEFT_RIGHT_PADDING, 0, 1), + Position = UDim2.new(0, LEFT_RIGHT_PADDING, 1, -1), + BorderSizePixel = 0, + BackgroundColor3 = self.props.dividerStyle.Color, + BackgroundTransparency = self.props.dividerStyle.Transparency, + }), + }) + }) +end + +return GenericCell \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericCell.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericCell.spec.lua new file mode 100644 index 0000000..8517a87 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericCell.spec.lua @@ -0,0 +1,99 @@ +return function() + local Cell = script.Parent + local Core = Cell.Parent + local UIBlox = Core.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local GenericCell = require(Cell.GenericCell) + + local MOCK_THEME = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + } + + local MOCK_FONT = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 12, + RelativeMinSize = 12, + } + + it("should create and destroy GenericCell without errors", function() + local element = mockStyleComponent({ + genericCell = Roact.createElement(GenericCell, { + dividerStyle = MOCK_THEME, + colorStyle = MOCK_THEME, + onStateChanged = function() end, + renderLeftContent = function() + return Roact.createElement("Frame") + end, + renderRightContent = function() + return Roact.createElement("Frame") + end, + titleText = "title", + subtitleText = "subtitle", + fontStyle = MOCK_FONT, + textStyle = MOCK_THEME, + subtitleFontStyle = MOCK_FONT, + subtitleTextStyle = MOCK_THEME, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy GenericSelectionCell with left component without errors", function() + local element = mockStyleComponent({ + genericCell = Roact.createElement(GenericCell, { + dividerStyle = MOCK_THEME, + colorStyle = MOCK_THEME, + onStateChanged = function() end, + renderLeftContent = function() + return Roact.createElement("Frame") + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy GenericSelectionCell with right component without errors", function() + local element = mockStyleComponent({ + genericCell = Roact.createElement(GenericCell, { + dividerStyle = MOCK_THEME, + colorStyle = MOCK_THEME, + onStateChanged = function() end, + renderRightContent = function() + return Roact.createElement("Frame") + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + + it("should create and destroy GenericSelectionCell with Center Text without errors", function() + local element = mockStyleComponent({ + genericCell = Roact.createElement(GenericCell, { + dividerStyle = MOCK_THEME, + colorStyle = MOCK_THEME, + onStateChanged = function() end, + titleText = "title", + subtitleText = "subtitle", + fontStyle = MOCK_FONT, + subtitleFontStyle = MOCK_FONT, + textStyle = MOCK_THEME, + subtitleTextStyle = MOCK_THEME, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericSelectionCell.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericSelectionCell.lua new file mode 100644 index 0000000..094736e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericSelectionCell.lua @@ -0,0 +1,193 @@ +local Cell = script.Parent +local Core = Cell.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local ControlState = require(Packages.UIBlox.Core.Control.Enum.ControlState) +local GenericCell = require(Packages.UIBlox.Core.Cell.GenericCell) +local ImageSetComponent = require(Packages.UIBlox.Core.ImageSet.ImageSetComponent) + +local GenericSelectionCell = Roact.PureComponent:extend("GenericSelectionCell") + +local CELL_STATE_COLOR = { + [ControlState.Default] = "BackgroundDefault", + [ControlState.Hover] = "BackgroundOnHover", + [ControlState.Pressed] = "BackgroundOnPress", +} + +local ICON_STATE_COLOR = { + [ControlState.Default] = "IconDefault", + [ControlState.Hover] = "IconEmphasis", + [ControlState.Pressed] = "IconDefault", +} + +local function getCellStyle(contentMap, controlState, style) + local buttonThemeClass = contentMap[controlState] + or contentMap[ControlState.Default] + + local buttonStyle = { + Color = style.Theme[buttonThemeClass].Color, + Transparency = style.Theme[buttonThemeClass].Transparency, + } + + -- Default/Disabled background color should be theme agnostic. Other control states deal with just + -- White/Black alpha transparency which work for any background color. + if controlState == ControlState.Default or controlState == ControlState.Disabled then + buttonStyle.Transparency = 1 + end + + return buttonStyle +end + +local function getIconStyle(contentMap, controlState, style) + local iconThemeClass = contentMap[controlState] + or contentMap[ControlState.Default] + + local iconStyle = { + Color = style.Theme[iconThemeClass].Color, + Transparency = style.Theme[iconThemeClass].Transparency, + } + + --Based on the design specs, the disabled and pressed state is 0.5 * alpha value + if controlState == ControlState.Disabled or + controlState == ControlState.Pressed then + iconStyle.Transparency = 0.5 * iconStyle.Transparency + 0.5 + end + + return iconStyle +end + +local function getTextStyle(theme, controlState) + local textStyle = { + Color = theme.Color, + Transparency = theme.Transparency, + } + + --Based on the design specs, the disabled and pressed state is 0.5 * alpha value + if controlState == ControlState.Disabled or + controlState == ControlState.Pressed then + textStyle.Transparency = 0.5 * textStyle.Transparency + 0.5 + end + + return textStyle +end + +GenericSelectionCell.validateProps = t.strictInterface({ + -- The title text to display + text = t.string, + + -- Subtitle text to display + subtitleText = t.optional(t.string), + + -- Default Image to render for the right component. + defaultImage = t.union(t.string, t.table), + + -- Image to render inside the defaultImage when this component is selected. + selectedImage = t.union(t.string, t.table), + + -- Size of the default image + defaultImageSize = t.number, + + -- Size of the selected image + selectedImageSize = t.number, + + -- Callback for when this selection is activated. + onActivated = t.optional(t.callback), + + -- Whether this cell is selected or not. + isSelected = t.optional(t.boolean), + + -- If this cell is disabled + isDisabled = t.optional(t.boolean), + + -- optional parameters for RoactGamepad + [Roact.Ref] = t.optional(t.table), + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), +}) + +GenericSelectionCell.defaultProps = { + isSelected = false, +} + +function GenericSelectionCell:init() + self.state = { + controlState = ControlState.Default + } + + self.onStateChanged = function(_, newState) + self:setState({ + controlState = newState, + }) + end +end + +function GenericSelectionCell:render() + assert(self.validateProps(self.props)) + + return withStyle(function(stylePalette) + local font = stylePalette.Font + local theme = stylePalette.Theme + + local iconStyle = getIconStyle(ICON_STATE_COLOR, self.state.controlState, stylePalette) + local colorStyle = getCellStyle(CELL_STATE_COLOR, self.state.controlState, stylePalette) + local textStyle = getTextStyle(theme.TextEmphasis, self.state.controlState) + local subtitleTextStyle = getTextStyle(theme.TextDefault, self.state.controlState) + local dividerStyle = theme.Divider + + return Roact.createElement(GenericCell, { + titleText = self.props.text, + colorStyle = colorStyle, + textStyle = textStyle, + fontStyle = font.Header2, + subtitleText = self.props.subtitleText, + subtitleTextStyle = subtitleTextStyle, + subtitleFontStyle = font.Body, + rightContentWidth = self.props.defaultImageSize, + onActivated = self.props.onActivated, + dividerStyle = dividerStyle, + isDisabled = self.props.isDisabled, + [Roact.Ref] = self.props[Roact.Ref], + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + renderRightContent = function() + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, self.props.defaultImageSize, 1, 0), + LayoutOrder = 3, + }, { + SelectionImage = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = self.props.defaultImage, + Size = UDim2.new(0, self.props.defaultImageSize, 0, self.props.defaultImageSize), + ImageColor3 = iconStyle.Color, + ImageTransparency = iconStyle.Transparency, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + }, { + SelectedImage = self.props.isSelected and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = self.props.selectedImage, + Size = UDim2.new(0, self.props.selectedImageSize, 0, self.props.selectedImageSize), + ImageColor3 = iconStyle.Color, + ImageTransparency = iconStyle.Transparency, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + }) + }) + }) + end, + onStateChanged = self.onStateChanged, + }) + end) +end + +return GenericSelectionCell \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericSelectionCell.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericSelectionCell.spec.lua new file mode 100644 index 0000000..773a64c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericSelectionCell.spec.lua @@ -0,0 +1,29 @@ +return function() + local Cell = script.Parent + local Core = Cell.Parent + local UIBlox = Core.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local Images = require(Packages.UIBlox.App.ImageSet.Images) + + local GenericSelectionCell = require(Cell.GenericSelectionCell) + + local DEFAULT_IMAGE = Images["component_assets/circle_24_stroke_1"] + + it("should create and destroy GenericSelectionCell without errors", function() + local element = mockStyleComponent({ + genericCell = Roact.createElement(GenericSelectionCell, { + text = "text", + defaultImage = DEFAULT_IMAGE, + selectedImage = DEFAULT_IMAGE, + defaultImageSize = 16, + selectedImageSize = 16, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/Config.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/Config.lua new file mode 100644 index 0000000..c84d64e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/Config.lua @@ -0,0 +1,97 @@ +--[[ + Exposes an interface to set global configuration values for a package. + + Configuration can only occur once, and should only be done by an application + using this package, not a library. + + Any keys that aren't recognized will cause errors. Configuration is only + intended for configuring the package itself, not extensions or libraries. + + Configuration is expected to be set immediately after loading the package. Setting + configuration values after an application starts may produce unpredictable + behavior. +]] + +local Config = {} + +-- Every valid configuration value should be non-nil in the config table. +function Config.new(defaultConfig) + local self = {} + self.defaultConfig = defaultConfig + self.defaultConfigKeys = {} + + for key in pairs(defaultConfig) do + table.insert(self.defaultConfigKeys, key) + end + + self._currentConfig = setmetatable({}, { + __index = function(_, key) + local message = ( + "Invalid global configuration key %q. Valid configuration keys are: %s" + ):format( + tostring(key), + table.concat(self.defaultConfigKeys, ", ") + ) + + error(message, 3) + end + }) + + -- We manually bind these methods here so that the Config's methods can be + -- used without passing in self, since they could get exposed on the + -- root object. + self.set = function(...) + return Config.set(self, ...) + end + + self.get = function(...) + return Config.get(self, ...) + end + + self.scoped = function(...) + return Config.scoped(self, ...) + end + + self.set(defaultConfig) + + return self +end + +function Config:set(configValues) + -- Validate values without changing any configuration. + -- We only want to apply this configuration if it's valid! + for key, value in pairs(configValues) do + if self.defaultConfig[key] == nil then + local message = ( + "Invalid global configuration key %q (type %s). Valid configuration keys are: %s" + ):format( + tostring(key), + typeof(key), + table.concat(self.defaultConfigKeys, ", ") + ) + + error(message, 3) + end + + -- Right now, all configuration values must be boolean. + if typeof(value) ~= "boolean" then + local message = ( + "Invalid value %q (type %s) for global configuration key %q. Valid values are: true, false" + ):format( + tostring(value), + typeof(value), + tostring(key) + ) + + error(message, 3) + end + + self._currentConfig[key] = value + end +end + +function Config:get() + return self._currentConfig +end + +return Config \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/Config.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/Config.spec.lua new file mode 100644 index 0000000..e37d596 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/Config.spec.lua @@ -0,0 +1,57 @@ +return function() + local Config = require(script.Parent.Config) + + local defaultConfig = { + -- This is an example flag + exampleFlag = false, + } + + it("should accept valid configuration", function() + local config = Config.new(defaultConfig) + local values = config.get() + + expect(values.exampleFlag).to.equal(false) + + config.set({ + exampleFlag = true, + }) + + expect(values.exampleFlag).to.equal(true) + end) + + it("should reject invalid configuration keys", function() + local config = Config.new(defaultConfig) + + local badKey = "garblegoop" + + local ok, err = pcall(function() + config.set({ + [badKey] = true, + }) + end) + + expect(ok).to.equal(false) + + -- The error should mention our bad key somewhere. + expect(err:find(badKey)).to.be.ok() + end) + + it("should reject invalid configuration values", function() + local config = Config.new(defaultConfig) + + local goodKey = "exampleFlag" + local badValue = "Hello there!" + + local ok, err = pcall(function() + config.set({ + [goodKey] = badValue, + }) + end) + + expect(ok).to.equal(false) + + -- The error should mention both our key and value + expect(err:find(goodKey)).to.be.ok() + expect(err:find(badValue)).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/makeConfigurable.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/makeConfigurable.lua new file mode 100644 index 0000000..40a0af9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/makeConfigurable.lua @@ -0,0 +1,75 @@ +local Config = require(script.Parent.Config) + +local function trimTrailingNewline(str) + if str:sub(-1, -1) == "\n" then + return str:sub(1, -2) + end + + return str +end + +return function(initializeLibrary, name, defaultConfig) + if typeof(name) ~= "string" then + error("Bad argument #2 - expected a string for the name of the library") + end + if typeof(defaultConfig) ~= "table" then + error("Bad argument #3 - expected a default config table for the library") + end + + local Library = {} + local LibraryConfig = Config.new(defaultConfig) + + function Library.init(config) + setmetatable(Library, nil) + + if config then + LibraryConfig.set(config) + end + + Library.Config = LibraryConfig.get() + + local contents = initializeLibrary() + for key, value in pairs(contents) do + Library[key] = value + end + + local firstInitTraceback = trimTrailingNewline(debug.traceback()) + Library.init = function() + local currentInitTraceback = trimTrailingNewline(debug.traceback()) + warn(string.format("%s has already been configured\nFirst init traceback:\n%s\nCurrent init traceback:\n%s", + name, firstInitTraceback, currentInitTraceback)) + end + + return setmetatable(Library, { + __index = function(self, key) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + + __newindex = function(self, key, value) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + }) + end + + return setmetatable(Library, { + __index = function(self, key) + error(("You must call %s.init(config) before using it!"):format(name), 2) + end, + + __newindex = function(self, key, value) + error(("You must call %s.init(config) before using it!"):format(name), 2) + end, + }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/makeConfigurable.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/makeConfigurable.spec.lua new file mode 100644 index 0000000..601e37a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/makeConfigurable.spec.lua @@ -0,0 +1,85 @@ +return function() + local makeConfigurable = require(script.Parent.makeConfigurable) + + local name = "Library" + + -- Function provide by a library that returns the API + local function initializeLibrary() + return { + someAPI = function() + return true + end + } + end + + local defaultConfig = { + -- This is an example flag + exampleFlag = false, + -- This is another example flag + anotherExampleFlag = false, + } + + local config = { + -- This is an example flag + ["exampleFlag"] = true, + } + + it("should require a name", function() + local ok = pcall(function() + makeConfigurable(initializeLibrary, nil, defaultConfig) + end) + + expect(ok).to.equal(false) + end) + + it("should require a default config", function() + local ok = pcall(function() + makeConfigurable(initializeLibrary, name, nil) + end) + + expect(ok).to.equal(false) + end) + + it("should have an init function", function() + local Library = makeConfigurable(initializeLibrary, name, defaultConfig) + + expect(typeof(Library.init)).to.equal("function") + end) + + it("should not have loaded its API before calling init", function() + local Library = makeConfigurable(initializeLibrary, name, defaultConfig) + + local ok = pcall(function() + Library.someAPI() + end) + + expect(ok).to.equal(false) + end) + + it("should not require a config when calling init", function() + local Library = makeConfigurable(initializeLibrary, name, defaultConfig) + + local ok = pcall(function() + Library.init() + end) + + expect(ok).to.equal(true) + end) + + it("should have a loaded config after calling init", function() + local Library = makeConfigurable(initializeLibrary, name, defaultConfig) + Library.init(config) + + expect(Library.Config).to.be.ok() + expect(Library.Config.exampleFlag).to.equal(true) + expect(Library.Config.anotherExampleFlag).to.equal(false) + end) + + it("should have a loaded API after calling init", function() + local Library = makeConfigurable(initializeLibrary, name, defaultConfig) + Library.init(config) + + expect(typeof(Library.someAPI)).to.equal("function") + expect(Library.someAPI()).to.equal(true) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Controllable.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Controllable.lua new file mode 100644 index 0000000..e153b24 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Controllable.lua @@ -0,0 +1,230 @@ +--[[ + Creates a Roact wrapper component that tracks state based on Roact input events to a UI control component. +]] +local ControlRoot = script.Parent +local CoreRoot = ControlRoot.Parent +local UIBloxRoot = CoreRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local ControlState = require(ControlRoot.Enum.ControlState) +local StateTable = require(UIBloxRoot.StateTable.StateTable) + +local Controllable = Roact.PureComponent:extend("Controllable") + +function Controllable:init() + self.isMounted = false + local initialState = ControlState.Initialize + self.state = { + currentState = initialState + } + local stateTableName = string.format("Controllable(%s)", tostring(self)) + + self.setDisabled = function(isDisabled) + if isDisabled then + self.stateTable.events.Disable() + else + self.stateTable.events.Enable() + end + end + + self.stateTable = StateTable.new(stateTableName, initialState, {}, + { + [ControlState.Initialize] = { + Enable = { nextState = ControlState.Default }, + Disable = { nextState = ControlState.Disabled }, + }, + [ControlState.Default] = { + OnPressed = { nextState = ControlState.Pressed }, + StartHover = { nextState = ControlState.Hover }, + OnSelectionGained = { nextState = ControlState.Selected }, + Disable = { nextState = ControlState.Disabled }, + }, + [ControlState.Hover] = { + OnSelectionGained = { nextState = ControlState.Selected }, + OnPressed = { nextState = ControlState.Pressed }, + EndHover = { nextState = ControlState.Default }, + Disable = { nextState = ControlState.Disabled }, + }, + [ControlState.Pressed] = { + OnSelectionGained = { nextState = ControlState.SelectedPressed }, + OnReleased = { nextState = ControlState.Default }, + OnReleasedHover = { nextState = ControlState.Hover }, + Disable = { nextState = ControlState.Disabled }, + }, + [ControlState.Selected] = { + OnSelectionLost = { nextState = ControlState.Default }, + OnPressed = { nextState = ControlState.SelectedPressed }, + Disable = { nextState = ControlState.Disabled }, + }, + [ControlState.SelectedPressed] = { + OnSelectionLost = { nextState = ControlState.Default }, + OnReleased = { nextState = ControlState.Selected }, + Disable = { nextState = ControlState.Disabled }, + }, + [ControlState.Disabled] = { + Enable = { nextState = ControlState.Default }, + }, + }) + + self.stateTable:onStateChange(function(oldState, newState) + self:setState({ + currentState = newState, + }) + if self.props.onStateChanged then + self.props.onStateChanged(oldState, newState) + end + end) +end + +local validateProps = t.strictInterface({ + -- The component that is controlled + controlComponent = t.strictInterface({ + -- the actual UI control component + component = t.union(t.callback, t.string, t.table), + + -- the props to pass to the UI control component + props = t.optional(t.table), + + -- child components + children = t.optional(t.table), + }), + + --callback function that's called when state changes + onStateChanged = t.callback, + + --disables state changes, and disables activating the UI control component + isDisabled = t.optional(t.boolean), + + --A Boolean value that determines whether user events are ignored and sink input + userInteractionEnabled = t.optional(t.boolean), +}) + +Controllable.defaultProps = { + userInteractionEnabled = true, + isDisabled = false, +} + +function Controllable:render() + assert(validateProps(self.props)) + + local controlComponent = self.props.controlComponent + local userInteractionEnabled = self.props.userInteractionEnabled + + if self.state.currentState == ControlState.Initialize then + return nil + end + + local newChildProps = Cryo.Dictionary.join( + self.props, + controlComponent.props or {}, + { + Selectable = true, + Active = not self.props.isDisabled, + [Roact.Event.MouseEnter] = function(...) + if not userInteractionEnabled then + return nil + end + self.stateTable.events.StartHover() + if controlComponent.props[Roact.Event.MouseEnter] ~= nil then + return controlComponent.props[Roact.Event.MouseEnter](...) + end + end, + [Roact.Event.MouseLeave] = function(...) + if not userInteractionEnabled then + return nil + end + self.stateTable.events.EndHover() + if controlComponent.props[Roact.Event.MouseLeave] ~= nil then + return controlComponent.props[Roact.Event.MouseLeave](...) + end + end, + [Roact.Event.InputBegan] = function(...) + if not userInteractionEnabled then + return nil + end + local inputObject = select(2, ...) + if inputObject.UserInputType == Enum.UserInputType.MouseButton1 or + inputObject.UserInputType == Enum.UserInputType.Touch or + inputObject.KeyCode == Enum.KeyCode.ButtonA then + self.stateTable.events.OnPressed() + end + if controlComponent.props[Roact.Event.InputBegan] ~= nil then + return controlComponent.props[Roact.Event.InputBegan](...) + end + end, + [Roact.Event.InputEnded] = function(...) + if not userInteractionEnabled then + return nil + end + local inputObject = select(2, ...) + if inputObject.UserInputType == Enum.UserInputType.MouseButton1 then + self.stateTable.events.OnReleasedHover() + elseif inputObject.UserInputType == Enum.UserInputType.Touch or + inputObject.KeyCode == Enum.KeyCode.ButtonA or + inputObject.UserInputType == Enum.UserInputType.MouseMovement then + self.stateTable.events.OnReleased() + end + if controlComponent.props[Roact.Event.InputEnded] ~= nil then + return controlComponent.props[Roact.Event.InputEnded](...) + end + end, + [Roact.Event.SelectionGained] = function(...) + if not userInteractionEnabled then + return nil + end + self.stateTable.events.OnSelectionGained() + if controlComponent.props[Roact.Event.SelectionGained] ~= nil then + return controlComponent.props[Roact.Event.SelectionGained](...) + end + end, + [Roact.Event.SelectionLost] = function(...) + if not userInteractionEnabled then + return nil + end + self.stateTable.events.OnSelectionLost() + if controlComponent.props[Roact.Event.SelectionLost] ~= nil then + return controlComponent.props[Roact.Event.SelectionLost](...) + end + end, + [Roact.Event.Activated] = function(...) + if not userInteractionEnabled then + return nil + end + if controlComponent.props[Roact.Event.Activated] then + if self.state.currentState ~= ControlState.Disabled then + return controlComponent.props[Roact.Event.Activated](...) + end + end + end, + + userInteractionEnabled = Cryo.None, + isDisabled = Cryo.None, + onStateChanged = Cryo.None, + [Roact.Children] = Cryo.None, + controlComponent = Cryo.None, + } + ) + + return Roact.createElement(controlComponent.component, newChildProps, controlComponent.children) +end + +function Controllable:didMount() + self.isMounted = true + self.setDisabled(self.props.isDisabled) +end + +function Controllable:didUpdate(previousProps) + if self.props.isDisabled ~= previousProps.isDisabled then + self.setDisabled(self.props.isDisabled) + end +end + +function Controllable:willUnmount() + self.isMounted = false +end + +return Controllable diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Controllable.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Controllable.spec.lua new file mode 100644 index 0000000..61d9350 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Controllable.spec.lua @@ -0,0 +1,113 @@ +--Skipping these tests until Roact 1.x. and spawn setstate is removed. +--https://jira.rbx.com/browse/UIBLOX-56 +return function() + local ControlRoot = script.Parent + local CoreRoot = ControlRoot.Parent + local UIBloxRoot = CoreRoot.Parent + local Packages = UIBloxRoot.Parent + + local Roact = require(Packages.Roact) + + local Controllable = require(ControlRoot.Controllable) + local ControlState = require(ControlRoot.Enum.ControlState) + + it("should create and destroy without errors", function() + -- luacheck: ignore unused argument oldState + local buttonState = nil + local element = Roact.createElement(Controllable, { + controlComponent = { + component = "TextButton", + props = {}, + }, + onStateChanged = function(oldState, newState) + buttonState = newState + end + }) + + local instance = Roact.mount(element) + expect(buttonState).to.equal(ControlState.Default) + Roact.unmount(instance) + end) + + it("should start isDisabled", function() + -- luacheck: ignore unused argument oldState + local buttonState = nil + local element = Roact.createElement(Controllable, { + controlComponent = { + component = "TextButton", + props = {}, + }, + onStateChanged = function(oldState, newState) + buttonState = newState + end, + isDisabled = true, + }) + + local instance = Roact.mount(element) + delay(0, function() expect(buttonState).to.equal(ControlState.Disabled) end) + Roact.unmount(instance) + end) + + it("should change from default to disabled", function() + -- luacheck: ignore unused argument oldState + local buttonState = nil + local element = Roact.createElement(Controllable, { + controlComponent = { + component = "TextButton", + props = {}, + }, + onStateChanged = function(oldState, newState) + buttonState = newState + end, + }) + + local instance = Roact.mount(element) + expect(buttonState).to.equal(ControlState.Default) + + Roact.update(instance, Roact.createElement(Controllable, { + controlComponent = { + component = "TextButton", + props = {}, + }, + onStateChanged = function(oldState, newState) + buttonState = newState + end, + isDisabled = true, + })) + expect(buttonState).to.equal(ControlState.Disabled) + + Roact.unmount(instance) + end) + + it("should change from isDisabled to default", function() + -- luacheck: ignore unused argument oldState + local buttonState = nil + local element = Roact.createElement(Controllable, { + controlComponent = { + component = "TextButton", + props = {}, + }, + onStateChanged = function(oldState, newState) + buttonState = newState + end, + + isDisabled = true, + }) + + local instance = Roact.mount(element) + expect(buttonState).to.equal(ControlState.Disabled) + + Roact.update(instance, Roact.createElement(Controllable, { + controlComponent = { + component = "TextButton", + props = {}, + }, + onStateChanged = function(oldState, newState) + buttonState = newState + end, + })) + expect(buttonState).to.equal(ControlState.Default) + + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Enum/ControlState.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Enum/ControlState.lua new file mode 100644 index 0000000..200bddc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Enum/ControlState.lua @@ -0,0 +1,14 @@ +local Core = script.Parent.Parent.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate(script.Name, { + "Initialize", + "Default", + "Pressed", + "Hover", + "Selected", + "SelectedPressed", + "Disabled", +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Interactable.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Interactable.lua new file mode 100644 index 0000000..5fce064 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Interactable.lua @@ -0,0 +1,48 @@ +--[[ + Interactable is a component that can be used as a container that responds to control state changes. + It accepts all props that can be passed into a ImageButton in additional to + isDisabled: bool = false + onStateChanged: function(oldState: ControlState, newState: ControlState) +]] +local Control = script.Parent +local Core = Control.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local Controllable = require(UIBlox.Core.Control.Controllable) + +local Interactable = Roact.PureComponent:extend("Interactable") + +Interactable.validateProps = t.interface({ + -- Is the interactable disabled + isDisabled = t.optional(t.boolean), + + -- function(oldState: ControlState, newState: ControlState) + -- A callback function for when the interactable state has changed + onStateChanged = t.callback, + + -- Note that this component can accept all valid properties of the Roblox ImageButton instance +}) + +function Interactable:render() + local controllerProps = { + onStateChanged = self.props.onStateChanged, + isDisabled = self.props.isDisabled, + userInteractionEnabled = self.props.userInteractionEnabled, + controlComponent = { + component = ImageSetComponent.Button, + props = self.props, + children = self.props[Roact.Children] or {}, + }, + } + + return Roact.createElement(Controllable, controllerProps) +end + +return Interactable \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Interactable.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Interactable.spec.lua new file mode 100644 index 0000000..dbb5fc3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Interactable.spec.lua @@ -0,0 +1,27 @@ +return function() + local Control = script.Parent + local Core = Control.Parent + local UIBlox = Core.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local Interactable = require(Control.Interactable) + + it("should create and destroy without errors", function() + local controllableButton = Roact.createElement(Interactable, { + onStateChanged = function() end, + Size = UDim2.new(1, 1, 1, -1), + Position = UDim2.new(1, -1, 1, 1), + AnchorPoint = Vector2.new(0.1, 0.1), + LayoutOrder = 1, + BackgroundTransparency = 0.5, + ImageColor3 = Color3.fromRGB(25, 25, 25), + ImageTransparency = 0.5, + BorderSizePixel = 2, + AutoButtonColor = true, + }) + local instance = Roact.mount(controllableButton) + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/ImageSetComponent.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/ImageSetComponent.lua new file mode 100644 index 0000000..80bf972 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/ImageSetComponent.lua @@ -0,0 +1,15 @@ +local createImageSetComponent = require(script.Parent.createImageSetComponent) + +local GuiService = game:GetService("GuiService") + +local CorePackages = script:FindFirstAncestor("CorePackages") + +local success, scale = pcall(GuiService.GetResolutionScale, GuiService) +if not success or not CorePackages then + scale = 1 +end + +return { + Button = createImageSetComponent("ImageButton", scale), + Label = createImageSetComponent("ImageLabel", scale), +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/ImageSetComponent.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/ImageSetComponent.spec.lua new file mode 100644 index 0000000..f2077d8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/ImageSetComponent.spec.lua @@ -0,0 +1,31 @@ +return function() + local ImageSetComponent = require(script.Parent.ImageSetComponent) + local ImageSet = script.Parent + local Core = ImageSet.Parent + local UIBlox = Core.Parent + local Images = require(UIBlox.App.ImageSet.Images) + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + + it("should create and destroy button without errors", function() + local element = Roact.createElement(ImageSetComponent.Button, { + Size = UDim2.new(1, 0, 1, 0), + Image = Images["component_assets/circle_17_stroke_1"], + BackgroundTransparency = 1, + BorderSizePixel = 0, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy label without errors", function() + local element = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.new(1, 0, 1, 0), + Image = Images["component_assets/circle_17_stroke_1"], + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/Validator/validateImage.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/Validator/validateImage.lua new file mode 100644 index 0000000..1a58c85 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/Validator/validateImage.lua @@ -0,0 +1,11 @@ +local Validator = script.Parent +local ImageSet = Validator.Parent +local Core = ImageSet.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +local validateImageSetData = require(Validator.validateImageSetData) + +return t.union(t.string, validateImageSetData) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/Validator/validateImageSetData.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/Validator/validateImageSetData.lua new file mode 100644 index 0000000..ffb110e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/Validator/validateImageSetData.lua @@ -0,0 +1,14 @@ +local Validator = script.Parent +local ImageSet = Validator.Parent +local Core = ImageSet.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local t = require(Packages.t) + +local ImageSetData = t.strictInterface({ + ImageRectOffset = t.Vector2, + ImageRectSize = t.Vector2, + Image = t.string, +}) + +return ImageSetData \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/createImageSetComponent.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/createImageSetComponent.lua new file mode 100644 index 0000000..c67c851 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/createImageSetComponent.lua @@ -0,0 +1,45 @@ +local ImageSet = script.Parent +local Core = ImageSet.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +return function(innerComponent, resolutionScale) + assert(resolutionScale > 0) + + return function(props) + local fullProps = {} + local imageSetProps + local usesImageSet = false + + for key, value in pairs(props) do + if key == "Image" and typeof(value) == "table" then + usesImageSet = true + imageSetProps = value + else + fullProps[key] = value + end + end + + if usesImageSet then + for imageKey, imageValue in pairs(imageSetProps) do + if not fullProps[imageKey] then + fullProps[imageKey] = imageValue + elseif imageKey == "ImageRectOffset" then + fullProps[imageKey] = imageValue + fullProps[imageKey] * resolutionScale + elseif imageKey == "ImageRectSize" then + fullProps[imageKey] = fullProps[imageKey] * resolutionScale + end + end + end + + if usesImageSet and fullProps.SliceCenter then + local min = fullProps.SliceCenter.Min * resolutionScale + local max = fullProps.SliceCenter.Max * resolutionScale + fullProps.SliceCenter = Rect.new(min, max) + fullProps.SliceScale = (fullProps.SliceScale or 1) / resolutionScale + end + + return Roact.createElement(innerComponent, fullProps) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/createImageSetComponent.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/createImageSetComponent.spec.lua new file mode 100644 index 0000000..72cdefe --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/createImageSetComponent.spec.lua @@ -0,0 +1,80 @@ +return function() + local createImageSetComponent = require(script.Parent.createImageSetComponent) + local ImageSet = script.Parent + local Core = ImageSet.Parent + local UIBlox = Core.Parent + local Images = require(UIBlox.App.ImageSet.Images) + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + + it("should create and destroy button without errors", function() + local element = Roact.createElement(createImageSetComponent("ImageButton", 1), { + Size = UDim2.new(0, 8, 0, 8), + Image = Images["component_assets/circle_17_stroke_1"], + BackgroundTransparency = 1, + BorderSizePixel = 0, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy button with slice center", function() + local element = Roact.createElement(createImageSetComponent("ImageButton", 1), { + Size = UDim2.new(0, 8, 0, 8), + Image = Images["component_assets/circle_17_stroke_1"], + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 7, 10, 9), + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "ImageSetComponentTest") + expect(container.ImageSetComponentTest.SliceCenter.Min.X).to.equal(8) + expect(container.ImageSetComponentTest.SliceCenter.Min.Y).to.equal(7) + expect(container.ImageSetComponentTest.SliceCenter.Max.X).to.equal(10) + expect(container.ImageSetComponentTest.SliceCenter.Max.Y).to.equal(9) + Roact.unmount(instance) + end) + + it("should create and destroy button with scaled slice center", function() + local element = Roact.createElement(createImageSetComponent("ImageButton", 2), { + Size = UDim2.new(0, 8, 0, 8), + Image = Images["component_assets/circle_17_stroke_1"], + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(11, 12, 14, 17), + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "ImageSetComponentTest") + expect(container.ImageSetComponentTest.SliceCenter.Min.X).to.equal(22) + expect(container.ImageSetComponentTest.SliceCenter.Min.Y).to.equal(24) + expect(container.ImageSetComponentTest.SliceCenter.Max.X).to.equal(28) + expect(container.ImageSetComponentTest.SliceCenter.Max.Y).to.equal(34) + Roact.unmount(instance) + end) + + it("should create and destroy button with scaled image rect offset and size", function() + local image = Images["component_assets/circle_17_stroke_1"] + local scale = 2 + local element = Roact.createElement(createImageSetComponent("ImageButton", scale), { + Size = UDim2.new(0, 8, 0, 8), + Image = image, + ImageRectOffset = Vector2.new(2, 2), + ImageRectSize = Vector2.new(15, 15), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "ImageSetComponentTest") + expect(container.ImageSetComponentTest.ImageRectOffset.X).to.equal(image.ImageRectOffset.X + 2 * scale) + expect(container.ImageSetComponentTest.ImageRectOffset.Y).to.equal(image.ImageRectOffset.Y + 2 * scale) + expect(container.ImageSetComponentTest.ImageRectSize.X).to.equal(15 * scale) + expect(container.ImageSetComponentTest.ImageRectSize.Y).to.equal(15 * scale) + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InfiniteScroller/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InfiniteScroller/init.lua new file mode 100644 index 0000000..ea86168 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InfiniteScroller/init.lua @@ -0,0 +1,5 @@ +local Core = script.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +return require(Packages.InfiniteScroller) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/InputButton.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/InputButton.lua new file mode 100644 index 0000000..766f2b2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/InputButton.lua @@ -0,0 +1,199 @@ +local Packages = script.Parent.Parent.Parent.Parent +local TextService = game:GetService("TextService") + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) + +local withStyle = require(Packages.UIBlox.Core.Style.withStyle) +local ImageSetComponent = require(Packages.UIBlox.Core.ImageSet.ImageSetComponent) +local Controllable = require(Packages.UIBlox.Core.Control.Controllable) +local ControlState = require(Packages.UIBlox.Core.Control.Enum.ControlState) + +local FitTextLabel = require(Packages.FitFrame).FitTextLabel + +-- TODO AVBURST-3748: Remove this soon after TextBoundsRoundUp is turned on to make the UIBlox places display +-- the same way as the App +local EngineFeatureTextBoundsRoundUp do + local success, value = pcall(function() + return game:GetEngineFeature("TextBoundsRoundUp") + end) + EngineFeatureTextBoundsRoundUp = success and value +end + +local InputButton = Roact.PureComponent:extend("InputButton") + +local validateProps = t.strictInterface({ + text = t.string, + size = t.optional(t.UDim2), + image = t.table, + imageColor = t.Color3, + fillImage = t.optional(t.table), + fillImageSize = t.optional(t.UDim2), + fillImageColor = t.optional(t.Color3), + selectedColor = t.Color3, + textColor = t.Color3, + transparency = t.number, + onActivated = t.callback, + isDisabled = t.optional(t.boolean), + layoutOrder = t.optional(t.number), +}) + +InputButton.defaultProps = { + layoutOrder = 0, + isDisabled = false, +} + +local SELECTION_BUTTON_SIZE = 26 +local HORIZONTAL_PADDING = 8 + +function InputButton:init() + self.state = { + outerImage = self.props.image, + outerTransparency = 1, + outerImageColor = self.props.imageColor, + innerImage = self.props.image, + innerImageColor = self.props.fillImageColor, + innerTransparency = 1, + } + + self.changeSprite = function(buttonState) + if buttonState == ControlState.Hover then + if not self.props.isDisabled then + self:setState({ + outerImageColor = self.props.selectedColor + }) + end + elseif buttonState == ControlState.Default then + self:setState({ + outerImageColor = self.props.imageColor + }) + end + end + + if not self.props.size then + --Initalize height to SELECTION_BUTTON_SIZE as the height can't be smaller than the button height + self.sizeBinding, self.updateSizeBinding = Roact.createBinding(UDim2.new(1, 0, 0, SELECTION_BUTTON_SIZE)) + + self.textAbsoluteSizeChanged = function(rbx) + local sizeY = SELECTION_BUTTON_SIZE + if rbx.AbsoluteSize.Y > sizeY then + sizeY = rbx.AbsoluteSize.Y + end + self.updateSizeBinding(UDim2.new(1, 0, 0, sizeY)) + end + end +end + +function InputButton:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local font = stylePalette.Font + local fontSize = font.Body.RelativeSize * font.BaseSize + + local textComponent + local textComponentProps = { + LayoutOrder = 2, + BackgroundTransparency = 1, + + Text = self.props.text, + TextXAlignment = Enum.TextXAlignment.Left, + + TextSize = fontSize, + Font = font.Body.Font, + TextWrapped = true, + TextColor3 = self.props.textColor, + TextTransparency = self.props.transparency, + } + + if self.props.size then + local size = self.props.size + local frameSize = Vector2.new(size.X.Offset - SELECTION_BUTTON_SIZE - HORIZONTAL_PADDING, size.Y.Offset) + local touchZoneWidth = TextService:GetTextSize(self.props.text, fontSize, font.Body.Font, frameSize).X + if (not EngineFeatureTextBoundsRoundUp) and touchZoneWidth > 0 then + -- GetTextSize documentation recommends to add a pixel of padding to the result to ensure no text is cut off + -- Only add that extra padding if there is text to display + touchZoneWidth = touchZoneWidth + 1 + end + + textComponent = "TextButton" + textComponentProps = Cryo.Dictionary.join(textComponentProps, { + Size = UDim2.new(0, touchZoneWidth, 1, 0), + [Roact.Event.Activated] = not self.props.isDisabled and self.props.onActivated or nil, + }) + else + textComponent = FitTextLabel + textComponentProps = Cryo.Dictionary.join(textComponentProps, { + width = UDim.new(1, -SELECTION_BUTTON_SIZE - HORIZONTAL_PADDING), + onActivated = self.props.onActivated, + [Roact.Change.AbsoluteSize] = self.textAbsoluteSizeChanged, + }) + end + + local fillImage = self.props.fillImage + + return Roact.createElement("Frame", { + Size = self.props.size or self.sizeBinding, + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + }, { + HorizontalLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + Padding = UDim.new(0, HORIZONTAL_PADDING), + VerticalAlignment = Enum.VerticalAlignment.Center + }), + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, HORIZONTAL_PADDING), + }), + InputButtonImage = Roact.createElement(Controllable, { + controlComponent = { + component = ImageSetComponent.Button, + props = { + BackgroundTransparency = 1, + Size = UDim2.new(0, SELECTION_BUTTON_SIZE, 0, SELECTION_BUTTON_SIZE), + Image = self.props.image, + ImageTransparency = self.props.transparency, + ScaleType = self.props.buttonSliceType, + SliceCenter = self.props.buttonSliceCenter, + ImageColor3 = self.state.outerImageColor, + [Roact.Event.Activated] = self.props.onActivated, + LayoutOrder = 1, + }, + children = { + InputFillImage = fillImage and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Size = self.props.fillImageSize, + Image = fillImage, + ImageTransparency = self.props.transparency, + ImageColor3 = self.props.fillImageColor, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + }) + } + }, + isDisabled = self.props.isDisabled, + + onStateChanged = function(_, newState) + self.changeSprite(newState) + end, + }), + -- Only create this element if there is text to display + InputButtonText = (self.props.text ~= "") and Roact.createElement(Controllable, { + controlComponent = + { + component = textComponent, + props = textComponentProps, + }, + isDisabled = self.props.isDisabled, + onStateChanged = function(_, newState) + self.changeSprite(newState) + end, + }) + } + ) + end) +end + +return InputButton diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/InputButton.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/InputButton.spec.lua new file mode 100644 index 0000000..32fb42e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/InputButton.spec.lua @@ -0,0 +1,37 @@ +return function() + local InputButtonRoot = script.Parent + local Core = InputButtonRoot.Parent + local UIBlox = Core.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local Images = require(Packages.UIBlox.App.ImageSet.Images) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + + local InputButton = require(script.Parent.InputButton) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + InputButton = Roact.createElement(InputButton, { + text = "some text", + size = UDim2.new(1, 0, 1, 0), + image = Images["component_assets/circle_24_stroke_1"], + imageColor = Color3.fromRGB(55, 111, 55), + fillImage = Images["component_assets/circle_16"], + fillImageSize = UDim2.new(10, 10), + fillImageColor = Color3.fromRGB(111, 222, 111), + selectedColor = Color3.fromRGB(8, 9, 8), + textColor = Color3.fromRGB(1, 2, 3), + transparency = 0.5, + onActivated = function(value) print(value) end, + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element, frame, "Box") + + Roact.unmount(instance) + end) + end) + +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/__stories__/InputButton.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/__stories__/InputButton.story.lua new file mode 100644 index 0000000..af96b4a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/__stories__/InputButton.story.lua @@ -0,0 +1,96 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local InputButtonRoot = script.Parent.Parent +local Core = InputButtonRoot.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local withStyle = require(UIBlox.Core.Style.withStyle) +local Images = require(Packages.UIBlox.App.ImageSet.Images) +local Roact = require(Packages.Roact) +local InputButton = require(InputButtonRoot.InputButton) + +local InputButtonStory = Roact.PureComponent:extend("InputButtonStory") + +local function createButton(props) + local theme = props.style.Theme.SystemPrimaryDefault + + return Roact.createElement(InputButton, { + text = props.text, + size = props.size, + image = Images["component_assets/circle_24_stroke_1"], + imageColor = Color3.fromRGB(0, 255, 0), + fillImage = Images["component_assets/circle_16"], + fillImageSize = UDim2.new(10, 10), + fillImageColor = Color3.fromRGB(111, 222, 111), + selectedColor = Color3.fromRGB(255, 0, 0), + textColor = theme.Color, + transparency = theme.Transparency, + onActivated = function(value) print(value) end, + layoutOrder = props.layoutOrder, + }) +end + +function InputButtonStory:render() + return withStyle(function(style) + return Roact.createElement(StoryItem, { + layoutOrder = 1, + title = "Input Button", + subTitle = "InputButton.InputButton", + showDivider = true, + }, { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + VerticalAlignment = Enum.VerticalAlignment.Top, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + Padding = UDim.new(0, 20), + }), + + noTextButton = createButton({ + text = "", + size = UDim2.new(0, 130, 0, 20), + layoutOrder = 1, + style = style, + }), + oneLineButton = createButton({ + text = "Text", + size = UDim2.new(0, 130, 0, 20), + layoutOrder = 2, + style = style, + }), + twoLineButton = createButton({ + text = "Two lines of text", + size = UDim2.new(0, 130, 0, 40), + layoutOrder = 3, + style = style, + }), + threeLineButton = createButton({ + text = "This has three lines of text", + size = UDim2.new(0, 130, 0, 60), + layoutOrder = 4, + style = style, + }), + tenLineButton = createButton({ + text = "An example of the unreasonable amount of text wrapping we could do. This is 10 lines", + size = UDim2.new(0, 130, 0, 200), + layoutOrder = 5, + style = style, + }), + }) + end) +end + +return function(target) + local styleProvider = Roact.createElement(StoryView, {}, { + Story = Roact.createElement(InputButtonStory), + }) + + local handle = Roact.mount(styleProvider, target, "InputButton") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.lua new file mode 100644 index 0000000..6bcd4af --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.lua @@ -0,0 +1,458 @@ +local PLUGINGUI_INPUT_CAPTURER_ZINDEX = 100000 +local SLIDER_HEIGHT = 36 +local KNOB_HEIGHT = 44 + +local UserInputService = game:GetService("UserInputService") + +local SliderRoot = script.Parent +local CoreRoot = SliderRoot.Parent +local UIBloxRoot = CoreRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local ImageSetComponent = require(CoreRoot.ImageSet.ImageSetComponent) + +local lerp = require(UIBloxRoot.Utility.lerp) + +local GenericSlider = Roact.PureComponent:extend("GenericSlider") + +GenericSlider.validateProps = t.strictInterface({ + --value of the first knob if the slider has two knobs, otherwise value of the only knob + lowerValue = t.number, + --value of the second knob if the slider has two knobs + upperValue = t.optional(t.number), + min = t.number, + max = t.number, + stepInterval = t.numberPositive, + isDisabled = t.optional(t.boolean), + + onValueChanged = t.callback, + --drag start function for first knob if the slider has two knobs, otherwise function for only knob + onDragStartLower = t.optional(t.callback), + --drag start function of the second knob if the slider has two knobs + onDragStartUpper = t.optional(t.callback), + onDragEnd = t.optional(t.callback), + + trackImage = t.union(t.string, t.table), + -- Allow bindings for style props + trackColor = t.union(t.Color3, t.table), + trackTransparency = t.union(t.number, t.table), + trackSliceCenter = t.optional(t.Rect), + + trackFillImage = t.union(t.string, t.table), + trackFillColor = t.union(t.Color3, t.table), + trackFillTransparency = t.union(t.number, t.table), + trackFillSliceCenter = t.optional(t.Rect), + + knobImage = t.union(t.string, t.table), + --knob color value of the first knob if the slider has two knobs, otherwise value of the only knob + knobColorLower = t.union(t.Color3, t.table), + --knob color value of the second knob if the slider has two knobs + knobColorUpper = t.optional(t.union(t.Color3, t.table)), + knobTransparency = t.union(t.number, t.table), + + knobImagePadding = t.optional(t.numberMin(0)), + + knobShadowImage = t.union(t.string, t.table), + --knob shadow transparency value of the first knob if the slider has two knobs, otherwise value of the only knob + knobShadowTransparencyLower = t.union(t.number, t.table), + --knob shadow transparency value of the second knob if the slider has two knobs + knobShadowTransparencyUpper = t.optional(t.union(t.number, t.table)), + + width = t.optional(t.UDim), + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + layoutOrder = t.optional(t.integer), +}) + +GenericSlider.defaultProps = { + width = UDim.new(1, 0), + knobImagePadding = 0, +} + +function GenericSlider:init() + self.rootRef = Roact.createRef() + self.lowerKnobDrag = false + self.upperKnobDrag = false +end + +function GenericSlider:render() + local isTwoKnobs = self:hasTwoKnobs() + local fillPercentLower = (self.props.lowerValue - self.props.min) / (self.props.max - self.props.min) + local fillPercentUpper = isTwoKnobs and (self.props.upperValue - self.props.min) / (self.props.max - self.props.min) + or nil + local visibleSize = KNOB_HEIGHT - (self.props.knobImagePadding * 2) + local positionOffsetLower = lerp(visibleSize / 2, -visibleSize / 2, fillPercentLower) + local positionOffsetUpper = isTwoKnobs and lerp(visibleSize / 2, -visibleSize / 2, fillPercentUpper) or nil + local knobPositionLower = UDim2.new(fillPercentLower, positionOffsetLower, 0.5, 0) + local knobPositionUpper = isTwoKnobs and UDim2.new(fillPercentUpper, positionOffsetUpper, 0.5, 0) or nil + local fillSize = isTwoKnobs and UDim2.fromScale(fillPercentUpper - fillPercentLower, 1) + or UDim2.fromScale(fillPercentLower, 1) + + return Roact.createElement(ImageSetComponent.Button, { + BackgroundTransparency = 1, + AnchorPoint = self.props.anchorPoint, + Size = UDim2.new(self.props.width.Scale, self.props.width.Offset, 0, SLIDER_HEIGHT), + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + [Roact.Event.InputBegan] = function(rbx, inputObject) + if self.props.isDisabled then + return + end + + self:onInputBegan(inputObject, false) + end, + [Roact.Ref] = self.rootRef, + }, { + Track = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + ImageColor3 = self.props.trackColor, + ImageTransparency = self.props.trackTransparency, + Image = self.props.trackImage, + Size = UDim2.new(1, 0, 0, 4), + Position = UDim2.fromScale(0.5, 0.5), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = self.props.trackSliceCenter, + }, { + TrackFill = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + ImageColor3 = self.props.trackFillColor, + ImageTransparency = self.props.trackFillTransparency, + Image = self.props.trackFillImage, + Size = fillSize, + Position = isTwoKnobs and UDim2.new(fillPercentLower, 0, 0, 0) or UDim2.new(0, 0, 0, 0), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = self.props.trackFillSliceCenter, + }) + }), + LowerKnob = Roact.createElement(ImageSetComponent.Button, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + ImageColor3 = self.props.knobColorLower, + ImageTransparency = self.props.knobTransparency, + Image = self.props.knobImage, + Size = UDim2.fromOffset(KNOB_HEIGHT, KNOB_HEIGHT), + Position = knobPositionLower, + ZIndex = 3, + + [Roact.Event.InputBegan] = function(rbx, inputObject) + if self.props.isDisabled then + return + end + + self:onInputBegan(inputObject, true) + end, + }), + LowerKnobShadow = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + ImageTransparency = self.props.knobShadowTransparencyLower, + Image = self.props.knobShadowImage, + Size = UDim2.fromOffset(44, 44), + Position = knobPositionLower, + ZIndex = 2, + }), + UpperKnob = isTwoKnobs and Roact.createElement(ImageSetComponent.Button, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + ImageColor3 = self.props.knobColorUpper, + ImageTransparency = self.props.knobTransparency, + Image = self.props.knobImage, + Size = UDim2.fromOffset(KNOB_HEIGHT, KNOB_HEIGHT), + Position = knobPositionUpper, + ZIndex = 3, + + [Roact.Event.InputBegan] = function(rbx, inputObject) + if self.props.isDisabled then + return + end + + self:onInputBegan(inputObject, true) + end, + }), + UpperKnobShadow = isTwoKnobs and Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + ImageTransparency = self.props.knobShadowTransparencyUpper, + Image = self.props.knobShadowImage, + Size = UDim2.fromOffset(44, 44), + Position = knobPositionUpper, + ZIndex = 2, + }), + }) +end + +function GenericSlider:didMount() + local root = self.rootRef.current + + -- When didMount is first called we're still orphaned; we need to wait until + -- we're in the DataModel before checking whether we can use UserInputService. + -- Using a connection on AncestryChanged means we won't yield a frame to + -- figure this out. + local ancestryChangedConnection + ancestryChangedConnection = root.AncestryChanged:Connect(function() + if not root:IsDescendantOf(game) then + return + end + + ancestryChangedConnection:Disconnect() + + -- If we're mounted in a PluginGui, we cannot use UserInputService and we + -- need to resort to less clean methods to capture mouse movements. + self.canUseUserInputService = root:FindFirstAncestorWhichIsA("PluginGui") == nil + end) +end + +function GenericSlider:didUpdate() + if self.props.disabled then + self:stopListeningForDrag() + end +end + +function GenericSlider:willUnmount() + self:stopListeningForDrag() +end + +function GenericSlider:onInputBegan(inputObject, isKnob) + if self.props.disabled then + return + end + + local inputType = inputObject.UserInputType + + if inputType ~= Enum.UserInputType.MouseButton1 and inputType ~= Enum.UserInputType.Touch then + return + end + + if inputType == Enum.UserInputType.Touch and not isKnob then + return + end + + local position = inputObject.Position.X + self:processDrag(position) + self:startListeningForDrag() +end + +function GenericSlider:startListeningForDrag() + local root = self.rootRef.current + + if root == nil then + return + end + + if self.dragging then + return + end + + if self.canUseUserInputService then + -- This is the nice clean path, where we can just use UserInputService to + -- capture the mouse movements. We will use this path in all production + -- cases (desktop, mobile, console, etc.) + self.moveConnection = UserInputService.InputChanged:Connect(function(inputObject) + -- We don't check whether the input was processed by something else + -- because we don't care about it: when we move the mouse, we want to + -- move the slider to match the movement, regardless of whether the + -- mouse movement was processed by something else. + + if not self.dragging then + return + end + + local inputType = inputObject.UserInputType + + if inputType ~= Enum.UserInputType.MouseMovement and inputType ~= Enum.UserInputType.Touch then + return + end + + if inputObject.UserInputState ~= Enum.UserInputState.Change then + return + end + + self:processDrag(inputObject.Position.X) + end) + + self.releaseConnection = UserInputService.InputEnded:Connect(function(inputObject) + local inputType = inputObject.UserInputType + if inputType ~= Enum.UserInputType.MouseButton1 and inputType ~= Enum.UserInputType.Touch then + return + end + + -- Stop listening for drag events before processing the input, since + -- that involves a callback to the user of the slider. + -- Only process one knob slider input because the two knob slider + -- should not move if it is not being dragged (since the track is not clickable) + -- and, therefore, should not process if the drag is ending + self:stopListeningForDrag() + self:processOneKnobDrag(inputObject.Position.X) + end) + + -- If the window loses focus the user can release the mouse and we won't + -- know about it, so the slider could get "stuck" to the mouse, even + -- though the user has let go of the mouse button. To resolve this, we + -- stop listening to events when we lose focus. + self.focusLostConnection = UserInputService.WindowFocusReleased:Connect(function() + self:stopListeningForDrag() + end) + else + -- This is the ugly, scary path, where UserInputService isn't available to + -- us and we have to cheat. In a PluginGui, UserInputService doesn't work; + -- its events only fire in the main viewport. The only way, currently, to + -- capture input like this is by creating a fake button at the top level + -- so that it overlays everything, then listening to input events on that. + -- This process is of less importance than the UserInputService connection + -- above, because it will only be run when the slider is used in a + -- Horsecat story or a similar environment. + local pluginGui = root:FindFirstAncestorWhichIsA("PluginGui") + + local inputCapturer = Instance.new("ImageButton") + inputCapturer.BackgroundTransparency = 1 + inputCapturer.Image = "" + inputCapturer.Name = "SliderPluginGuiInputCapturer" + inputCapturer.Size = UDim2.new(1, 0, 1, 0) + inputCapturer.ZIndex = PLUGINGUI_INPUT_CAPTURER_ZINDEX + self.moveConnection = inputCapturer.MouseMoved:Connect(function(x) + self:processDrag(x) + end) + + self.releaseConnection = inputCapturer.MouseButton1Up:Connect(function(x) + self:stopListeningForDrag() + self:processOneKnobDrag(x) + end) + + self.focusLostConnection = inputCapturer.MouseLeave:Connect(function(x) + self:stopListeningForDrag() + self:processOneKnobDrag(x) + end) + + inputCapturer.Parent = pluginGui + self.inputCapturerButton = inputCapturer + end + + self.dragging = true + + if self.lowerKnobDrag and self.props.onDragStartLower ~= nil then + self.props.onDragStartLower() + end + + if self.upperKnobDrag and self.props.onDragStartUpper ~= nil then + self.props.onDragStartUpper() + end +end + +function GenericSlider:getSteppedValue(x) + local root = self.rootRef.current + if root == nil then + return 0 + end + + local min = self.props.min + local max = self.props.max + local stepInterval = self.props.stepInterval + + local absoluteWidth = root.AbsoluteSize.X + local relativeX = x - root.AbsolutePosition.X + local clampedX = math.clamp(relativeX, 0, absoluteWidth) + local fractional = clampedX / absoluteWidth + local unsteppedValue = (fractional * (max - min)) + min + return math.floor(unsteppedValue / stepInterval + 0.5) * stepInterval +end + +function GenericSlider:processDrag(x) + if self:hasTwoKnobs() then + self:processTwoKnobDrag(x) + else + self:processOneKnobDrag(x) + end +end + +function GenericSlider:processOneKnobDrag(x) + if self:hasTwoKnobs() then + return + end + + local steppedValue = self:getSteppedValue(x) + self.lowerKnobDrag = true + + if steppedValue ~= self.props.lowerValue then + self.props.onValueChanged(steppedValue) + end +end + +function GenericSlider:processTwoKnobDrag(x) + local steppedValue = self:getSteppedValue(x) + local lowerValue = self.props.lowerValue + local upperValue = self.props.upperValue + + if not self.lowerKnobDrag and not self.upperKnobDrag then + --Set which knob is being dragged (both if they are at the same position) + if steppedValue == lowerValue then + self.lowerKnobDrag = true + end + if steppedValue == upperValue then + self.upperKnobDrag = true + end + elseif self.lowerKnobDrag and self.upperKnobDrag then + --decides which knob to actually drag and change the value of when both are atop one another + if steppedValue - self.props.stepInterval >= upperValue then + self.upperKnobDrag = true + self.lowerKnobDrag = false + upperValue = steppedValue + elseif steppedValue + self.props.stepInterval <= lowerValue then + self.upperKnobDrag = false + self.lowerKnobDrag = true + lowerValue = steppedValue + end + elseif self.lowerKnobDrag then + --drag the left knob (but not sofar as to surpass the right knob) + if steppedValue <= upperValue then + lowerValue = steppedValue + end + elseif self.upperKnobDrag then + --drag the right knob (but not sofar as to surpass the left knob) + if steppedValue >= lowerValue then + upperValue = steppedValue + end + end + + if upperValue ~= self.props.upperValue or lowerValue ~= self.props.lowerValue then + self.props.onValueChanged(lowerValue, upperValue) + end +end + +function GenericSlider:stopListeningForDrag() + if self.moveConnection ~= nil then + self.moveConnection:Disconnect() + self.moveConnection = nil + end + + if self.releaseConnection ~= nil then + self.releaseConnection:Disconnect() + self.releaseConnection = nil + end + + if self.focusLostConnection ~= nil then + self.focusLostConnection:Disconnect() + self.focusLostConnection = nil + end + + if self.inputCapturerButton ~= nil then + self.inputCapturerButton:Destroy() + self.inputCapturerButton = nil + end + + self.dragging = false + self.lowerKnobDrag = false + self.upperKnobDrag = false + + if self.props.onDragEnd ~= nil then + self.props.onDragEnd() + end +end + +function GenericSlider:hasTwoKnobs() + return self.props.upperValue ~= nil +end + +return GenericSlider \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.spec.lua new file mode 100644 index 0000000..837aa91 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.spec.lua @@ -0,0 +1,81 @@ +return function() + local Slider = script.Parent + local Core = Slider.Parent + local UIBlox = Core.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local GenericSlider = require(Slider.GenericSlider) + + it("should create and destroy without errors", function() + local genericSlider = Roact.createElement(GenericSlider, { + lowerValue = 0, + min = -10, + max = 10, + stepInterval = 1, + + onValueChanged = function() end, + + trackImage = "rbxassetid://3792530835", + -- Allow bindings for style props + trackColor = Color3.fromRGB(0, 0, 0), + trackTransparency = 0, + + trackFillImage = "rbxassetid://3792530835", + trackFillColor = Color3.fromRGB(0, 0, 0), + trackFillTransparency = 0, + + knobImage = "rbxassetid://3792530835", + knobColorLower = Color3.fromRGB(0, 0, 0), + knobTransparency = 0, + + knobShadowImage = "rbxassetid://3792530835", + knobShadowTransparencyLower = 0, + }) + local instance = Roact.mount(genericSlider) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with all props", function() + local genericSlider = Roact.createElement(GenericSlider, { + lowerValue = 0, + upperValue = 8, + min = -10, + max = 10, + stepInterval = 1, + isDisabled = false, + + onValueChanged = function() end, + onDragStartLower = function() end, + onDragEnd = function() end, + + trackImage = "rbxassetid://3792530835", + -- Allow bindings for style props + trackColor = Color3.fromRGB(0, 0, 0), + trackTransparency = 0, + trackSliceCenter = Rect.new(8, 8, 9, 9), + + trackFillImage = "rbxassetid://3792530835", + trackFillColor = Color3.fromRGB(0, 0, 0), + trackFillTransparency = 0, + trackFillSliceCenter = Rect.new(8, 8, 9, 9), + + knobImage = "rbxassetid://3792530835", + knobColorLower = Color3.fromRGB(0, 0, 0), + knobTransparency = 0, + + knobImagePadding = 0, + + knobShadowImage = "rbxassetid://3792530835", + knobShadowTransparencyLower = 0, + + width = UDim.new(1, 1, 1, 1), + position = UDim2.new(1, 1, 1, 1), + anchorPoint = Vector2.new(0.5, 0.5), + layoutOrder = 0, + }) + local instance = Roact.mount(genericSlider) + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.story.lua new file mode 100644 index 0000000..1c929b6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.story.lua @@ -0,0 +1,74 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Roact = require(ReplicatedStorage.Packages.Roact) + +local GenericSlider = require(script.Parent.GenericSlider) + +local Images = require(script.Parent.Parent.Parent.App.ImageSet.Images) + +local Story = Roact.PureComponent:extend("Story") + +function Story:init() + self:setState({ + value = 50, + drag = false, + }) +end + +function Story:render() + return Roact.createElement(GenericSlider, { + lowerValue = self.state.value, + min = 0, + max = 100, + stepInterval = 10, + isDisabled = false, + + onValueChanged = function(value) + print("New slider value:", value) + self:setState({ + value = value, + }) + end, + onDragStartLower = function() + self:setState({ + drag = true, + }) + end, + onDragEnd = function() + self:setState({ + drag = false, + }) + end, + + trackImage = Images["component_assets/circle_16"], + trackTransparency = 0, + trackSliceCenter = Rect.new(8, 8, 8, 8), + trackColor = Color3.new(0, 0, 0), + + trackFillImage = Images["component_assets/circle_16"], + trackFillColor = Color3.new(1, 0, 0), + trackFillSliceCenter = Rect.new(8, 8, 8, 8), + trackFillTransparency = 0, + + knobImage = Images["component_assets/circle_28_padding_10"], + knobColorLower = Color3.new(0.8, 0.8, 0.8), + knobTransparency = 0, + + knobImagePadding = 0, + + knobShadowImage = Images["component_assets/dropshadow_28"], + knobShadowTransparencyLower = self.state.drag and 1 or 0, + + position = UDim2.fromScale(0.5, 0.5), + width = UDim.new(0.8, 0), + anchorPoint = Vector2.new(0.5, 0.5), + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(Story), target, "GenericSlider") + + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleConsumer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleConsumer.lua new file mode 100644 index 0000000..24b31dd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleConsumer.lua @@ -0,0 +1,52 @@ +local Style = script.Parent +local Core = Style.Parent +local UIBlox = Core.Parent + +--Note: remove along with styleRefactorConfig +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local styleRefactorConfig = UIBloxConfig.styleRefactorConfig + +if not styleRefactorConfig then + return require(UIBlox.Style.withStyle) +end +--- + +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Palette = require(Style.Symbol.Palette) + +local StyleConsumer = Roact.Component:extend("StyleConsumer") + +local validateProps = t.strictInterface({ + render = t.callback, +}) + +function StyleConsumer:init() + self.palette = self._context[Palette] + local currentStyle = self.palette.style + + self.state = { + style = currentStyle, + } +end + +function StyleConsumer:render() + assert(validateProps(self.props)) + return self.props.render(self.state.style) +end + +function StyleConsumer:didMount() + self.disconnectStyleListener = self.palette.signal:subscribe(function(newStyle) + self:setState({ + style = newStyle, + }) + end) +end + +function StyleConsumer:willUnmount() + self.disconnectStyleListener() +end + +return StyleConsumer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleConsumer.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleConsumer.spec.lua new file mode 100644 index 0000000..1ff82a1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleConsumer.spec.lua @@ -0,0 +1,39 @@ +return function() + local Style = script.Parent + local Core = Style.Parent + local UIBlox = Core.Parent + --Note: remove along with styleRefactorConfig + local UIBloxConfig = require(UIBlox.UIBloxConfig) + local styleRefactorConfig = UIBloxConfig.styleRefactorConfig + + if not styleRefactorConfig then + return + end + --- + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local StyleProvider = require(Style.StyleProvider) + + local StyleConsumer = require(script.Parent.StyleConsumer) + + + it("should create and destroy without errors", function() + local renderFunction = function(style) + expect(style).to.be.a("table") + return Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }) + end + local element = Roact.createElement(StyleProvider, { + style = {}, + }, { + StyleConsumer = Roact.createElement(StyleConsumer, { + render = renderFunction + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StylePalette.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StylePalette.lua new file mode 100644 index 0000000..f5f7573 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StylePalette.lua @@ -0,0 +1,24 @@ +local Style = script.Parent +local Core = Style.Parent +local UIBlox = Core.Parent + +local createSignal = require(UIBlox.Utility.createSignal) + +local StylePalette = {} + +function StylePalette.new(style) + local self = {} + self.style = style + self.signal = createSignal() + setmetatable(self, { + __index = StylePalette, + }) + return self +end + +function StylePalette:update(newStyle) + self.style = newStyle + self.signal:fire(newStyle) +end + +return StylePalette \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StylePalette.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StylePalette.spec.lua new file mode 100644 index 0000000..9c64c1f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StylePalette.spec.lua @@ -0,0 +1,53 @@ +return function() + local StylePalette = require(script.Parent.StylePalette) + + local testTheme = { + Background1 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background2 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background3 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background4 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0.3, -- Alpha 0.7 + }, + } + + local testFont = { + Normal = Enum.Font.Gotham, + Title = Enum.Font.GothamBold, + } + + it("should connect and fire signal for style change without errors", function() + local testStyle = { + Theme = testTheme, + Font = testFont, + } + local appStyle = StylePalette.new(testStyle) + + expect(appStyle.style).to.be.a("table") + + local testValue = "some test theme" + local testTable = { + Theme = testValue, + } + local disconnect = appStyle.signal:subscribe(function(newValues) + expect(newValues).to.be.a("table") + expect(newValues.Theme).to.equal(testValue) + end) + + appStyle:update(testTable) + + expect(appStyle.style).to.be.a("table") + expect(appStyle.style.Theme).to.equal(testValue) + disconnect() + end) + +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleProvider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleProvider.lua new file mode 100644 index 0000000..8d661d8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleProvider.lua @@ -0,0 +1,46 @@ +local Style = script.Parent +local Core = Style.Parent +local UIBlox = Core.Parent + +--Note: remove along with styleRefactorConfig +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local styleRefactorConfig = UIBloxConfig.styleRefactorConfig + +if not styleRefactorConfig then + return require(UIBlox.Style.StyleProvider) +end +--- + +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local StylePalette = require(Style.StylePalette) +local Palette = require(Style.Symbol.Palette) + +local StyleProvider = Roact.Component:extend("StyleProvider") + +local validateProps = t.strictInterface({ + -- The current style of the app. + style = t.table, + [Roact.Children] = t.table, +}) + +function StyleProvider:init() + local style = self.props.style + self.stylePalette = StylePalette.new(style) + self._context[Palette] = self.stylePalette +end + +function StyleProvider:render() + assert(validateProps(self.props)) + return Roact.oneChild(self.props[Roact.Children]) +end + +function StyleProvider:didUpdate(previousProps) + if self.props.style ~= previousProps.style then + self.style:update(self.props.style) + end +end + +return StyleProvider \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleProvider.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleProvider.spec.lua new file mode 100644 index 0000000..31411d9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleProvider.spec.lua @@ -0,0 +1,68 @@ +return function() + local Style = script.Parent + local Core = Style.Parent + local UIBlox = Core.Parent + --Note: remove along with styleRefactorConfig + local UIBloxConfig = require(UIBlox.UIBloxConfig) + local styleRefactorConfig = UIBloxConfig.styleRefactorConfig + + if not styleRefactorConfig then + return + end + --- + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + + local StyleProvider = require(script.Parent.StyleProvider) + + local testTheme = { + Background1 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background2 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background3 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background4 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0.3, -- Alpha 0.7 + }, + } + + local testFont = { + Normal = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 1, + RelativeMinSize = 1, + }, + Title = { + Font = Enum.Font.GothamBold, + RelativeSize = 1, + RelativeMinSize = 1, + }, + } + + local testStyle = { + Theme = testTheme, + Font = testFont, + } + + it("should create and destroy without errors", function() + local someComponent = Roact.createElement("TextLabel", { + Text = "test", + }) + local styleProvider = Roact.createElement(StyleProvider, { + style = testStyle, + }, { + SomeComponent = someComponent, + }) + + local roactInstance = Roact.mount(styleProvider) + Roact.unmount(roactInstance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Symbol/Palette.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Symbol/Palette.lua new file mode 100644 index 0000000..6708c02 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Symbol/Palette.lua @@ -0,0 +1,7 @@ +local Style = script.Parent.Parent +local Core = Style.Parent +local Symbol = require(Core.Utility.Symbol) + +local Palette = Symbol.named("Palette") + +return Palette \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Validator/validateColorInfo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Validator/validateColorInfo.lua new file mode 100644 index 0000000..b13357c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Validator/validateColorInfo.lua @@ -0,0 +1,12 @@ +local Validator = script.Parent +local Style = Validator.Parent +local App = Style.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +return t.strictInterface({ + Color = t.Color3, + Transparency = t.numberConstrained(0, 1), +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Validator/validateFontInfo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Validator/validateFontInfo.lua new file mode 100644 index 0000000..a77d959 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Validator/validateFontInfo.lua @@ -0,0 +1,13 @@ +local Validator = script.Parent +local Style = Validator.Parent +local App = Style.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +return t.strictInterface({ + RelativeSize = t.numberMinExclusive(0), + RelativeMinSize = t.numberMinExclusive(0), + Font = t.EnumItem, +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/withStyle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/withStyle.lua new file mode 100644 index 0000000..96bd76b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/withStyle.lua @@ -0,0 +1,27 @@ +local Style = script.Parent +local Core = Style.Parent +local UIBlox = Core.Parent + +--Note: remove along with styleRefactorConfig +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local styleRefactorConfig = UIBloxConfig.styleRefactorConfig + +if not styleRefactorConfig then + return require(UIBlox.Style.withStyle) +end +--- + +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + + +local StyleConsumer = require(Style.StyleConsumer) + +--[[ + This is a utility function that will wrap StyleConsumer. + `renderCallback` will be invoked with the current style. It should return a Roact element. +]] +return function(renderCallback) + assert(type(renderCallback) == "function", "Expect renderCallback to be a function.") + return Roact.createElement(StyleConsumer, { render = renderCallback }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/withStyle.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/withStyle.spec.lua new file mode 100644 index 0000000..b5b7abc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/withStyle.spec.lua @@ -0,0 +1,80 @@ +return function() + local Style = script.Parent + local Core = Style.Parent + local UIBlox = Core.Parent + + --Note: remove along with styleRefactorConfig + local UIBloxConfig = require(UIBlox.UIBloxConfig) + local styleRefactorConfig = UIBloxConfig.styleRefactorConfig + + if not styleRefactorConfig then + return + end + --- + + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + + local StyleProvider = require(Style.StyleProvider) + + local withStyle = require(script.Parent.withStyle) + + local testTheme = { + Background1 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background2 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background3 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background4 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0.3, -- Alpha 0.7 + }, + } + + local testFont = { + Normal = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 1, + RelativeMinSize = 1, + }, + Title = { + Font = Enum.Font.GothamBold, + RelativeSize = 1, + RelativeMinSize = 1, + }, + } + + local testStyle = { + Theme = testTheme, + Font = testFont, + } + + it("should create and destroy without errors", function() + local someTestElement = Roact.Component:extend("someTestElement") + -- luacheck: ignore unused argument self + function someTestElement:render() + return withStyle(function(style) + expect(style).to.be.a("table") + return Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }) + end) + end + + local element = Roact.createElement(StyleProvider, { + style = testStyle, + }, { + someTestElement = Roact.createElement(someTestElement), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ExpandableText/ExpandableTextUtils.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ExpandableText/ExpandableTextUtils.lua new file mode 100644 index 0000000..17d84eb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ExpandableText/ExpandableTextUtils.lua @@ -0,0 +1,21 @@ +local GetTextHeight = require(script.Parent.Parent.GetTextHeight) + +local function getExpandableTextHeights(font, frameWidth, textContent, compactNumberOfLines) + local textSize = font.BaseSize * font.Body.RelativeSize + local fullTextHeight = GetTextHeight(textContent, font.Body.Font, textSize, frameWidth) + local compactHeight = compactNumberOfLines * textSize + + return fullTextHeight, compactHeight +end + +--Function for whether or not an ExpandableTextArea can expand given parameters +--regarding font, text, width of the frame container, and number of lines in the compact view +local function getCanExpand(font, frameWidth, textContent, compactNumberOfLines) + local fullTextHeight, compactHeight = getExpandableTextHeights(font, frameWidth, textContent, compactNumberOfLines) + return fullTextHeight > compactHeight +end + +return { + getExpandableTextHeights = getExpandableTextHeights, + getCanExpand = getCanExpand, +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/GenericTextLabel.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/GenericTextLabel.lua new file mode 100644 index 0000000..1844a9e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/GenericTextLabel.lua @@ -0,0 +1,95 @@ +local GenericTextLabelRoot = script.Parent +local Text = GenericTextLabelRoot.Parent +local App = Text.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) +local validateFontInfo = require(UIBlox.Core.Style.Validator.validateFontInfo) +local validateColorInfo = require(UIBlox.Core.Style.Validator.validateColorInfo) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local GenericTextLabel = Roact.PureComponent:extend("GenericTextLabel") + +local MAX_BOUND = 10000 + +local validateProps = t.interface({ + -- The max size avaliable for the textbox + maxSize = t.optional(t.Vector2), + + -- The Font table from the style palette + fontStyle = validateFontInfo, + + -- The color table from the style palette + colorStyle = validateColorInfo, + + -- Whether the TextLabel is Fluid Sizing between the font's min and default sizes (optional) + fluidSizing = t.optional(t.boolean), + + -- Note that this component can accept all valid properties of the Roblox TextLabel instance +}) + +GenericTextLabel.defaultProps = { + maxSize = Vector2.new(MAX_BOUND, MAX_BOUND), + fluidSizing = false, +} + +function GenericTextLabel:render() + assert(validateProps(self.props)) + + local text = self.props.Text + local isFluidSizing = self.props.fluidSizing + + return withStyle(function(stylePalette) + local font = self.props.fontStyle + local color = self.props.colorStyle + local textColor = color.Color + local textTransparency = color.Transparency + + local baseSize = stylePalette.Font.BaseSize + local fontSizeMin = font.RelativeMinSize * baseSize + local fontSizeMax = font.RelativeSize * baseSize + local textFont = font.Font + + local textboxSize = self.props.Size + if textboxSize == nil then + local sampleText = text + if self.props.TextTruncate == Enum.TextTruncate.AtEnd then + sampleText = sampleText.."..." + end + local textBounds = self.props.maxSize + local textboxBounds = GetTextSize(sampleText, fontSizeMax, textFont, textBounds) + textboxSize = UDim2.new(0, textboxBounds.X, 0, textboxBounds.Y) + end + + local newProps = Cryo.Dictionary.join(self.props, { + [Roact.Children] = Cryo.None, + fluidSizing = Cryo.None, + fontStyle = Cryo.None, + colorStyle = Cryo.None, + maxSize = Cryo.None, + Size = textboxSize, + Text = text, + Font = textFont, + TextSize = fontSizeMax, + TextColor3 = textColor, + TextTransparency = textTransparency, + TextWrapped = self.props.TextWrapped == nil and true or self.props.TextWrapped, + TextScaled = isFluidSizing, + BackgroundTransparency = 1, + }) + + return Roact.createElement("TextLabel", newProps, Cryo.Dictionary.join({ + UITextSizeConstraint = isFluidSizing and Roact.createElement("UITextSizeConstraint", { + MaxTextSize = fontSizeMax, + MinTextSize = fontSizeMin, + } or nil) + }, self.props[Roact.Children] or {})) + end) +end + +return GenericTextLabel diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/GenericTextLabel.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/GenericTextLabel.spec.lua new file mode 100644 index 0000000..0d7165c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/GenericTextLabel.spec.lua @@ -0,0 +1,73 @@ +return function() + local GenericTextLabelRoot = script.Parent + local Text = GenericTextLabelRoot.Parent + local App = Text.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local GenericTextLabel = require(GenericTextLabelRoot.GenericTextLabel) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local TestStyle = require(UIBlox.App.Style.Validator.TestStyle) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + provider = Roact.createElement(GenericTextLabel, { + Text = "Test text", + Size = UDim2.new(0, 100, 0, 50), + colorStyle = TestStyle.Theme.SystemPrimaryDefault, + fontStyle = TestStyle.Font.Title, + fluidSizing = true, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and be able to auto size itself if not size is given", function() + local element = mockStyleComponent({ + provider = Roact.createElement(GenericTextLabel, { + Text = "Test text", + colorStyle = TestStyle.Theme.SystemPrimaryDefault, + fontStyle = TestStyle.Font.Title, + fluidSizing = true, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and be able to respect the max size", function() + local element = mockStyleComponent({ + provider = Roact.createElement(GenericTextLabel, { + Text = "Test text", + maxSize = Vector2.new(200, 40), + colorStyle = TestStyle.Theme.SystemPrimaryDefault, + fontStyle = TestStyle.Font.Title, + fluidSizing = true, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should accept all properties of the TextLabel instance", function() + local element = mockStyleComponent({ + provider = Roact.createElement(GenericTextLabel, { + Text = "Test text", + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(.5, 0, .5, 0), + colorStyle = TestStyle.Theme.SystemPrimaryDefault, + fontStyle = TestStyle.Font.Header1, + TextYAlignment = Enum.TextYAlignment.Bottom, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/__stories__/GenericTextLabel.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/__stories__/GenericTextLabel.story.lua new file mode 100644 index 0000000..2d9341d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/__stories__/GenericTextLabel.story.lua @@ -0,0 +1,51 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local GenericTextLabelRoot = script.Parent.Parent +local Text = GenericTextLabelRoot.Parent +local Core = Text.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local withStyle = require(UIBlox.Core.Style.withStyle) + +local Roact = require(Packages.Roact) + +local GenericTextLabel = require(GenericTextLabelRoot.GenericTextLabel) + +local GenericTextLabelStory = Roact.PureComponent:extend("GenericTextLabelStory") + +function GenericTextLabelStory:render() + return withStyle(function(style) + local theme = style.Theme + local font = style.Font + return Roact.createElement(StoryItem, { + size = UDim2.new(0, 300, 0, 128), + layoutOrder = 1, + title = "Generic Text Label", + subTitle = "Text.GenericTextLabel", + showDivider = true, + }, { + GenericTextLabel = Roact.createElement(GenericTextLabel, { + Text = "Phantom Forces [Sniper Update!]", + Size = UDim2.new(0, 150, 0, 45), + colorStyle = theme.SystemPrimaryDefault, + fontStyle = font.Header1, + fluidSizing = true, + }), + }) + end) +end + +return function(target) + local styleProvider = Roact.createElement(StoryView, {}, { + Story = Roact.createElement(GenericTextLabelStory), + }) + + local handle = Roact.mount(styleProvider, target, "GenericTextLabel") + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GetTextHeight.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GetTextHeight.lua new file mode 100644 index 0000000..b499a14 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GetTextHeight.lua @@ -0,0 +1,12 @@ +local GetTextSize = require(script.Parent.GetTextSize) + +-- NOTE: Any number greater than 2^30 will make TextService:GetTextSize give invalid results +local MAX_BOUND = 10000 + +local function getTextHeight(text, font, fontSize, widthCap) + local bounds = Vector2.new(widthCap, MAX_BOUND) + local textSize = GetTextSize(text, fontSize, font, bounds) + return textSize.Y +end + +return getTextHeight \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GetTextSize.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GetTextSize.lua new file mode 100644 index 0000000..30bc720 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GetTextSize.lua @@ -0,0 +1,14 @@ +-- This file implements a wrapper function for TextService:GetTextSize() +-- Extra padding is added to the returned size because of a rounding issue with GetTextSize +-- TODO: Remove this temporary additional padding when CLIPLAYEREX-1633 is fixed + +local TextService = game:GetService("TextService") + +local TEMPORARY_TEXT_SIZE_PADDING = Vector2.new(2, 2) + +local function getTextSize(...) + local textSize = TextService:GetTextSize(...) + return textSize + TEMPORARY_TEXT_SIZE_PADDING +end + +return getTextSize \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/ImageTextLabel.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/ImageTextLabel.lua new file mode 100644 index 0000000..d29342d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/ImageTextLabel.lua @@ -0,0 +1,102 @@ +local ImageTextLabelRoot = script.Parent +local Text = ImageTextLabelRoot.Parent +local App = Text.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) +local validateFontInfo = require(UIBlox.Core.Style.Validator.validateFontInfo) +local withStyle = require(UIBlox.Core.Style.withStyle) + +-- This component is used to inline an icon into your text. +-- The current version of the component will only work with the icon being before the text +-- Text must be aligned at the top left and cannot be dynamic. +-- Icon positioning is left up to the user to allow as much functionality as possible +local ImageTextLabel = Roact.PureComponent:extend("ImageTextLabel") + +local MAX_BOUND = 10000 + +-- If this component ever becomes public, please restrict the props that can be used on imageProps, textProps +-- Removing a used prop in the future will be difficult. +ImageTextLabel.validateProps = t.interface({ + imageProps = t.optional(t.interface({ + Size = t.UDim2, + })), + + genericTextLabelProps = t.interface({ + Text = t.string, + fontStyle = validateFontInfo, + + AnchorPoint = t.None, + Position = t.None, + Size = t.None, + TextXAlignment = t.None, + TextYAlignment = t.None, + TextScaled = t.None, + maxSize = t.None, + }), + + frameProps = t.optional(t.interface({ + Size = t.None, + })), + + maxSize = t.optional(t.Vector2), + padding = t.optional(t.number), +}) + +ImageTextLabel.defaultProps = { + maxSize = Vector2.new(MAX_BOUND, MAX_BOUND), + frameProps = {}, + padding = 0, +} + +function ImageTextLabel:render() + local genericTextLabelProps = self.props.genericTextLabelProps + local imageProps = self.props.imageProps + local frameProps = self.props.frameProps + local padding = self.props.padding + local text = self.props.genericTextLabelProps.Text + local maxSize = self.props.maxSize + + return withStyle(function(stylePalette) + local fontStyle = genericTextLabelProps.fontStyle + + local baseSize = stylePalette.Font.BaseSize + local fontSize = fontStyle.RelativeSize * baseSize + local font = fontStyle.Font + + if imageProps then + -- This method has flaws given our non-monospaced font but is the easiest closest approach to getting inlined icons. + local spaceTextSize = GetTextSize(" ", fontSize, font, Vector2.new(0, 0)) + - GetTextSize(" ", fontSize, font, Vector2.new(0, 0)) + local numSpaces = math.ceil((imageProps.Size.X.Offset + padding) / spaceTextSize.X) + text = string.rep(" ", numSpaces)..text + end + + local labelTextSize = GetTextSize(text, fontSize, font, Vector2.new(maxSize.X, maxSize.Y)) + + return Roact.createElement("Frame", Cryo.Dictionary.join(frameProps, { + Size = UDim2.new(0, labelTextSize.X, 0, labelTextSize.Y) + }), { + Icon = self.props.imageProps and Roact.createElement(ImageSetComponent.Label, imageProps) or nil, + Name = Roact.createElement(GenericTextLabel, Cryo.Dictionary.join(genericTextLabelProps, { + Text = text, + AnchorPoint = Vector2.new(0, 0), + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + fluidSizing = false, + maxSize = maxSize, + })), + }) + end) +end + +return ImageTextLabel \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/ImageTextLabel.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/ImageTextLabel.spec.lua new file mode 100644 index 0000000..7e32e40 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/ImageTextLabel.spec.lua @@ -0,0 +1,74 @@ +return function() + local ImageTextLabelRoot = script.Parent + local Text = ImageTextLabelRoot.Parent + local App = Text.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local ImageTextLabel = require(ImageTextLabelRoot.ImageTextLabel) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local TestStyle = require(UIBlox.App.Style.Validator.TestStyle) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + Roact.createElement(ImageTextLabel, { + imageProps = { + Size = UDim2.new(0, 50, 0, 50), + }, + genericTextLabelProps = { + Text = "Text", + TextSize = 15, + colorStyle = TestStyle.Theme.SystemPrimaryDefault, + fontStyle = TestStyle.Font.Title, + }, + padding = 4, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should handle not having an image", function() + local element = mockStyleComponent({ + Roact.createElement(ImageTextLabel, { + genericTextLabelProps = { + Text = "Text", + TextSize = 15, + colorStyle = TestStyle.Theme.SystemPrimaryDefault, + fontStyle = TestStyle.Font.Title, + }, + padding = 4, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should accept all properties", function() + local element = mockStyleComponent({ + Roact.createElement(ImageTextLabel, { + imageProps = { + Size = UDim2.new(0, 50, 0, 50), + }, + genericTextLabelProps = { + Text = "Text", + TextSize = 15, + colorStyle = TestStyle.Theme.SystemPrimaryDefault, + fontStyle = TestStyle.Font.Title, + }, + frameProps = { + BackgroundTransparency = 1, + }, + padding = 4, + maxSize = Vector2.new(100, 100), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/__stories__/ImageTextLabel.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/__stories__/ImageTextLabel.story.lua new file mode 100644 index 0000000..ea0af53 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/__stories__/ImageTextLabel.story.lua @@ -0,0 +1,130 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local ImageTextLabelRoot = script.Parent.Parent +local Text = ImageTextLabelRoot.Parent +local Core = Text.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local Images = require(UIBlox.App.ImageSet.Images) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local Roact = require(Packages.Roact) + +local ImageTextLabel = require(ImageTextLabelRoot.ImageTextLabel) + +local ImageTextLabelStory = Roact.PureComponent:extend("ImageTextLabelStory") + +function ImageTextLabelStory:render() + return withStyle(function(style) + local theme = style.Theme + local font = style.Font + + local titleIcon = Images["icons/status/premium_small"] + local titleIconSize = titleIcon.ImageRectSize / Images.ImagesResolutionScale + + return Roact.createElement(StoryItem, { + size = UDim2.new(0, 300, 0, 128), + layoutOrder = 1, + title = "Image Text Label", + subTitle = "Text.ImageTextLabel", + showDivider = true, + }, { + verticalLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = UDim.new(0, 10), + }), + OneLineNoIcon = Roact.createElement(ImageTextLabel, { + genericTextLabelProps = { + TextSize = 15, + colorStyle = theme.TextEmphasis, + fontStyle = font.Header2, + Text = "One line with no Icon", + TextTruncate = Enum.TextTruncate.AtEnd, + }, + frameProps = { + BackgroundTransparency = 1, + LayoutOrder = 1, + }, + padding = 4, + }), + OneLine = Roact.createElement(ImageTextLabel, { + imageProps = { + BackgroundTransparency = 1, + Image = titleIcon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Size = UDim2.new(0, titleIconSize.X, 0, titleIconSize.Y), + AnchorPoint = Vector2.new(0, 0), + Position = UDim2.new(0, 0, 0, 0), + }, + genericTextLabelProps = { + TextSize = 15, + colorStyle = theme.TextEmphasis, + fontStyle = font.Header2, + Text = "One line with icon", + TextTruncate = Enum.TextTruncate.AtEnd, + }, + frameProps = { + BackgroundTransparency = 1, + LayoutOrder = 2, + }, + padding = 4, + }), + TwoLinesNoIcon = Roact.createElement(ImageTextLabel, { + genericTextLabelProps = { + TextSize = 15, + colorStyle = theme.TextEmphasis, + fontStyle = font.Header2, + Text = "Multiple lined text that should truncate at the end", + TextTruncate = Enum.TextTruncate.AtEnd, + }, + frameProps = { + BackgroundTransparency = 1, + LayoutOrder = 3, + }, + padding = 4, + maxSize = Vector2.new(170, 40), + }), + TwoLines = Roact.createElement(ImageTextLabel, { + imageProps = { + BackgroundTransparency = 1, + Image = titleIcon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Size = UDim2.new(0, titleIconSize.X, 0, titleIconSize.Y), + AnchorPoint = Vector2.new(0, 0), + Position = UDim2.new(0, 0, 0, 0), + }, + genericTextLabelProps = { + TextSize = 15, + colorStyle = theme.TextEmphasis, + fontStyle = font.Header2, + Text = "Multiple lined text that should truncate at the end", + TextTruncate = Enum.TextTruncate.AtEnd, + }, + frameProps = { + BackgroundTransparency = 1, + LayoutOrder = 4, + }, + padding = 4, + maxSize = Vector2.new(170, 40), + }) + }) + end) +end + +return function(target) + local styleProvider = Roact.createElement(StoryView, {}, { + Story = Roact.createElement(ImageTextLabelStory), + }) + + local handle = Roact.mount(styleProvider, target, "ImageTextLabel") + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/Symbol.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/Symbol.lua new file mode 100644 index 0000000..8b6adaf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/Symbol.lua @@ -0,0 +1,43 @@ +--[[ + A 'Symbol' is an opaque marker type that can be used to signify unique + statuses. Symbols have the type 'userdata', but when printed to the console, + the name of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +--[[ + Create an unnamed Symbol. Usually, you should create a named Symbol using + Symbol.named(name) +]] +function Symbol.unnamed() + local self = newproxy(true) + + getmetatable(self).__tostring = function() + return "Unnamed Symbol" + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/Symbol.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/Symbol.spec.lua new file mode 100644 index 0000000..e05061d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/Symbol.spec.lua @@ -0,0 +1,24 @@ +return function() + local Symbol = require(script.Parent.Symbol) + + describe("named", function() + it("should give an opaque object", function() + local symbol = Symbol.named("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = Symbol.named("foo") + + expect(tostring(symbol):find("foo")).to.be.ok() + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.named("abc") + local symbolB = Symbol.named("abc") + + expect(symbolA).never.to.equal(symbolB) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/bindingValidator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/bindingValidator.lua new file mode 100644 index 0000000..cb4e963 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/bindingValidator.lua @@ -0,0 +1,32 @@ +-- Validator for RoactBinding props +-- expectedType: meta type functions of `t` (e.g. `t.number`) + +local function bindingValidator(expectedType) + return function(binding) + local bindingType = string.match(tostring(binding), "RoactBinding") + + if typeof(binding) ~= "table" or not bindingType then + warn(string.format("RoactBinding expected, got %s", typeof(binding))) + return false + end + + local success, value = pcall(function() + return binding:getValue() + end) + + if not success then + warn("getValue() not defined") + return false + end + + local valueSuccess, valueErrMsg = expectedType(value) + if not valueSuccess then + warn(string.format("RoactBinding value: %s", valueErrMsg)) + return false + end + + return true + end +end + +return bindingValidator diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/bindingValidator.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/bindingValidator.spec.lua new file mode 100644 index 0000000..e8dfa02 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/bindingValidator.spec.lua @@ -0,0 +1,25 @@ +return function() + local UtilityRoot = script.Parent + local UIBloxRoot = UtilityRoot.Parent.Parent + local t = require(UIBloxRoot.Parent.t) + local Roact = require(UIBloxRoot.Parent.Roact) + + local bindingValidator = require(script.Parent.bindingValidator) + local numberBindingValidator = bindingValidator(t.number) + + it("should return false for non-bindings", function() + expect(numberBindingValidator("")).to.equal(false) + expect(numberBindingValidator({})).to.equal(false) + expect(numberBindingValidator(function() end)).to.equal(false) + end) + + it("should return false if binding value type not match", function() + local binding = Roact.createBinding("0") + expect(numberBindingValidator(binding)).to.equal(false) + end) + + it("should return true if binding value matches expected type", function() + local binding = Roact.createBinding(0) + expect(numberBindingValidator(binding)).to.equal(true) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheet.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheet.lua new file mode 100644 index 0000000..69e25ea --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheet.lua @@ -0,0 +1,147 @@ +local Packages = script.Parent.Parent.Parent + +local Otter = require(Packages.Otter) +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) +local withStyle = require(Packages.UIBlox.Core.Style.withStyle) +local ModalBottomSheetButton = require(script.Parent.ModalBottomSheetButton) + +-- https://share.goabstract.com/cfe90baa-ab79-4f34-ad1b-3ef389d39da4 +local ModalBottomSheet = Roact.PureComponent:extend("ModalBottomSheet") + +local WIDTH_THRESHOLD = 600 +local ELEMENT_HEIGHT = 56 +local MAXIMUM_SHEET_ELEMENTS = 7 +local MAXIMUM_SHEET_HEIGHT = ELEMENT_HEIGHT * (MAXIMUM_SHEET_ELEMENTS + 0.5) + +local MOTOR_OPTIONS = { + frequency = 4, + dampingRatio = 1, +} + +local validateProps = t.strictInterface({ + buttonModels = t.array(t.table), + -- this is screenWidth of the app, and is only used to calculate whether the MBS width is fixed or not + screenWidth = t.number, + -- a callback that when fired should result in this component no longer being rendered + -- this should probably relate to closeCentralOverlay in CI + onDismiss = t.callback, + + showImages = t.optional(t.boolean), + bottomGap = t.optional(t.number), + sheetContentXSize = t.optional(t.UDim), + sheetContentXPosition = t.optional(t.UDim), +}) + +ModalBottomSheet.defaultProps = { + bottomGap = 0, + showImages = true, + sheetContentXSize = UDim.new(1, 0), + sheetContentXPosition = UDim.new(0, 0), +} + +function ModalBottomSheet:init() + self.motor = Otter.createSingleMotor(0) + self.ref = Roact.createRef() + self.active = true +end + +function ModalBottomSheet:render() + assert(validateProps(self.props)) + local sheetContentXPosition = self.props.sheetContentXPosition + local sheetContentXSize = self.props.sheetContentXSize + + self.sheetHeight = #self.props.buttonModels * ELEMENT_HEIGHT + if #self.props.buttonModels >= MAXIMUM_SHEET_ELEMENTS then + self.sheetHeight = MAXIMUM_SHEET_HEIGHT + end + local children = {} + for index, buttonProps in ipairs(self.props.buttonModels) do + local mergedProps = Cryo.Dictionary.join(buttonProps, { + hasRoundTop = index == 1, + hasRoundBottom = index == #self.props.buttonModels, + isFixed = self.props.screenWidth > WIDTH_THRESHOLD, + elementHeight = ELEMENT_HEIGHT, + showImage = self.props.showImages, + LayoutOrder = index, + onActivatedAndDismissed = function(a) + if buttonProps.onActivated then + buttonProps.onActivated(a) + end + if not buttonProps.stayOnActivated then + self.props.onDismiss() + end + end, + }) + + children["button " .. index] = Roact.createElement(ModalBottomSheetButton, mergedProps) + end + + children.layout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + return withStyle(function(stylePalette) + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + Background = Roact.createElement("TextButton", { + ZIndex = 0, + AutoButtonColor = false, + BackgroundColor3 = stylePalette.Theme.Overlay.Color, + BackgroundTransparency = stylePalette.Theme.Overlay.Transparency, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + Text = "", + [Roact.Event.Activated] = function() + self.active = false + self.motor:setGoal(Otter.spring(0, MOTOR_OPTIONS)) + end + }), + SheetContent = Roact.createElement("ScrollingFrame", { + BackgroundTransparency = 1, + Size = UDim2.new(sheetContentXSize.Scale, sheetContentXSize.Offset, 0, self.sheetHeight), + Position = UDim2.new(sheetContentXPosition.Scale, sheetContentXPosition.Offset, 1, 0), + ScrollBarThickness = 0, + CanvasSize = UDim2.new( + sheetContentXSize.Scale, + sheetContentXSize.Offset, + 0, + #self.props.buttonModels * ELEMENT_HEIGHT), + ClipsDescendants = true, + [Roact.Ref] = self.ref, + }, children), + }) + end) +end + +function ModalBottomSheet:didMount() + local sheetContentXPosition = self.props.sheetContentXPosition + self.motor:onStep(function(value) + if self.ref.current then + self.ref.current.Position = UDim2.new( + sheetContentXPosition.Scale, + sheetContentXPosition.Offset, + 1, + -(self.sheetHeight + self.props.bottomGap) * value) + end + end) + self.motor:setGoal(Otter.spring(1, MOTOR_OPTIONS)) + self.motor:onComplete(function() + if not self.active then + self.props.onDismiss() + end + end) + self.motor:start() +end + +function ModalBottomSheet:wilUnmount() + self.motor:destroy() +end + +return ModalBottomSheet \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheet.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheet.spec.lua new file mode 100644 index 0000000..dc67bff --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheet.spec.lua @@ -0,0 +1,87 @@ +return function() + local ComponentRoot = script.Parent + local UIBloxRoot = ComponentRoot.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local Images = require(UIBloxRoot.App.ImageSet.Images) + + local ModalBottomSheet = require(script.Parent.ModalBottomSheet) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + ModalBottomSheet = Roact.createElement(ModalBottomSheet, { + buttonModels = {}, + screenWidth = 100, + onDismiss = function() end, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should correctly hold what it's given", function() + local instanceRef = Roact.createRef() + + local folder = Instance.new("Folder") + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + [Roact.Ref] = instanceRef, + }, { + ModalBottomSheet = Roact.createElement(ModalBottomSheet, { + onDismiss = function() end, + screenWidth = 1000, + buttonModels = { + { + icon = Images["component_assets/circle_17"], + text = "someSampleText", + }, + }, + }), + }), + }) + + local instance = Roact.mount(element, folder) + + local modalBottomSheet = instanceRef.current.ModalBottomSheet + local button = modalBottomSheet.SheetContent["button 1"] + + local icon = button.buttonContents:FindFirstChildWhichIsA("ImageLabel") + expect(icon.ImageRectOffset).to.equal(Images["component_assets/circle_17"].ImageRectOffset) + local text = button.buttonContents:FindFirstChildWhichIsA("TextLabel") + expect(text.Text).to.equal("someSampleText") + + Roact.unmount(instance) + end) + + it("should work correctly when renderRightElement is present", function() + local element = mockStyleComponent({ + ModalBottomSheet = Roact.createElement(ModalBottomSheet, { + onDismiss = function() end, + screenWidth = 1000, + buttonModels = { + { + icon = Images["component_assets/circle_17"], + text = "someSampleText", + renderRightElement = function() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }) + end, + }, + }, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheetButton.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheetButton.lua new file mode 100644 index 0000000..2a1bc53 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheetButton.lua @@ -0,0 +1,202 @@ +local Packages = script.Parent.Parent.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(Packages.UIBlox.Core.Style.withStyle) +local ImageSetComponent = require(Packages.UIBlox.Core.ImageSet.ImageSetComponent) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local ModalBottomSheetButton = Roact.PureComponent:extend("ModalBottomSheetButton") +local imageSize = Images["component_assets/circle_17"].ImageRectSize +local imageOffset = Images["component_assets/circle_17"].ImageRectOffset + +local xOffset = 8 * Images.ImagesResolutionScale +local yOffset = 8 * Images.ImagesResolutionScale +local imageCenter = Rect.new(xOffset, yOffset, imageSize.x - xOffset, imageSize.y - yOffset) + +-- https://share.goabstract.com/cfe90baa-ab79-4f34-ad1b-3ef389d39da4 +local WIDTH_FIXED = 300 +local WIDTH_MARGIN = 16 +local WIDTH_INNER_MARGIN = 24 +local WIDTH_INNER_MARGIN_ICON = 12 + +local validateProps = t.strictInterface({ + icon = t.optional(t.union(t.table, t.string)), + text = t.optional(t.string), + onActivated = t.optional(t.callback), + renderRightElement = t.optional(t.callback), + + showImage = t.boolean, + isFixed = t.boolean, + onActivatedAndDismissed = t.callback, + elementHeight = t.integer, + hasRoundBottom = t.boolean, + hasRoundTop = t.boolean, + LayoutOrder = t.integer, + stayOnActivated = t.optional(t.boolean), +}) + +ModalBottomSheetButton.defaultProps = { + icon = {}, + text = "", + onActivated = function() end, +} + +function ModalBottomSheetButton:init() + self.ref = Roact.createRef() + self.onColorChange = function(styledColor) + if not self.ref.current then return end + self.ref.current.ImageColor3 = styledColor + end + -- TODO(UIBLOX-30): Update with Controllable.lua + self.onInputBegan = function(inputObject) + return inputObject.UserInputType == Enum.UserInputType.MouseButton1 or + inputObject.UserInputType == Enum.UserInputType.Touch + end + self.onInputEnd = function(inputObject) + return inputObject.UserInputType == Enum.UserInputType.MouseButton1 or + inputObject.UserInputType == Enum.UserInputType.Touch + end +end + +function ModalBottomSheetButton:render() + assert(validateProps(self.props)) + local hasRoundTop = self.props.hasRoundTop + local hasRoundBottom = self.props.hasRoundBottom + + local ImageRectSize + local ImageRectOffset + local SliceCenter + + local s100 = imageSize.X + local s50 = s100 / 2 + + -- we are slicing around a 1x1 pixel in the center + if hasRoundTop and hasRoundBottom then + ImageRectSize = imageSize + ImageRectOffset = imageOffset + SliceCenter = imageCenter + elseif hasRoundTop then + ImageRectSize = Vector2.new(s100, s50) + ImageRectOffset = imageOffset + SliceCenter = Rect.new(s50-1, s50-1, s50+1, s50) + elseif hasRoundBottom then + ImageRectSize = Vector2.new(s100, s50) + ImageRectOffset = imageOffset + Vector2.new(0, s50) + SliceCenter = Rect.new(s50-1, 0, s50+1, 1) + else + ImageRectSize = Vector2.new(1, 1) + ImageRectOffset = imageOffset + Vector2.new(s50, s50) + end + + local elementHeight = self.props.elementHeight + -- Width is dependant on parent width + local buttonSize + if self.props.isFixed then + buttonSize = UDim2.new(0, WIDTH_FIXED, 0, elementHeight) + else + buttonSize = UDim2.new(1, -WIDTH_MARGIN*2, 0, elementHeight) + end + + local padding = WIDTH_INNER_MARGIN + if self.props.showImage or self.props.renderRightElement then + padding = WIDTH_INNER_MARGIN_ICON + end + + local textWidthOffset = padding + local iconSize = elementHeight * 0.8 + if self.props.showImage then + textWidthOffset = textWidthOffset + iconSize + padding + end + if self.props.renderRightElement then + textWidthOffset = textWidthOffset + iconSize + padding + end + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local font = stylePalette.Font + local transparency = theme.BackgroundUIDefault.Transparency + local textColor = theme.TextEmphasis.Color + + return Roact.createElement("ImageButton", { + AutoButtonColor = false, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = SliceCenter, + Image = Images["component_assets/circle_17"].Image, + ImageColor3 = theme.BackgroundUIDefault.Color, + ImageRectSize = ImageRectSize, + ImageRectOffset = ImageRectOffset, + ImageTransparency = transparency, + Size = buttonSize, + LayoutOrder = self.props.LayoutOrder, + [Roact.Ref] = self.ref, + [Roact.Event.Activated] = self.props.onActivatedAndDismissed, + [Roact.Event.InputBegan] = function(_, inputObject) + if self.onInputBegan(inputObject) then + self.onColorChange(theme.BackgroundOnPress.Color) + end + end, + [Roact.Event.InputEnded] = function (_, inputObject) + if self.onInputEnd(inputObject) then + self.onColorChange(theme.BackgroundUIDefault.Color) + end + end, + }, { + buttonContents = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + horizontalLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, padding), + }), + padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, padding), + PaddingTop = UDim.new(0.1, 0), + PaddingBottom = UDim.new(0.1, 0), + }), + icon = self.props.showImage and Roact.createElement(ImageSetComponent.Label, { + Image = self.props.icon, + ImageColor3 = textColor, + ImageTransparency = transparency, + BackgroundTransparency = 1, + Size = UDim2.new(0, iconSize, 0, iconSize), + LayoutOrder = 1, + }) or nil, + textLabel = Roact.createElement("TextLabel", { + TextXAlignment = Enum.TextXAlignment.Left, + BackgroundTransparency = 1, + Size = UDim2.new(1, -textWidthOffset, 1, 0), + Text = self.props.text, + TextTransparency = transparency, + Font = font.Header2.Font, + TextColor3 = textColor, + TextSize = font.Header2.RelativeSize * font.BaseSize, + TextTruncate = Enum.TextTruncate.AtEnd, + LayoutOrder = 2, + }), + rightContainer = self.props.renderRightElement and Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, iconSize, 0, iconSize), + LayoutOrder = 3, + }, { + hint = self.props.renderRightElement(), + }) + }), + bottomBorder = not hasRoundBottom and Roact.createElement("Frame", { + LayoutOrder = 0, + BackgroundTransparency = 1, + BackgroundColor3 = theme.Divider.Color, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 1), + AnchorPoint = Vector2.new(0, 1), + Position = UDim2.new(0, 0, 1, 0), + }) + }) + end) +end + +return ModalBottomSheetButton \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option1.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option1.story.lua new file mode 100644 index 0000000..7241c8f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option1.story.lua @@ -0,0 +1,100 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local ModalBottomSheet = require(script.Parent.Parent.ModalBottomSheet) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local doSomething = function(a) + print(a, "was pressed!") +end + +local dummyModalButtons1 = { + { + icon = Images["component_assets/circle_17"], + text = "option 1", + onActivated = doSomething, + }, +} + +local function withStyle(tree) + return Roact.createElement(StoryView, {}, { + oneChild = tree, + }) +end + +local function mountWithStyle(tree, target, name) + local styledTree = withStyle(tree) + + return Roact.mount(styledTree, target, name) +end + +local overlayComponent = Roact.Component:extend("overlayComponent") + +function overlayComponent:render() + local ModalContainer = self.props.ModalContainer + local showModal = self.state.showModal + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + Layout = Roact.createElement("UIListLayout", { + + }), + TestButton1 = Roact.createElement("TextButton", { + Text = "Spawn 1 Choice", + Size = UDim2.new(1, 0, 0.2, 0), + BackgroundColor3 = Color3.fromRGB(222, 0, 0), + AutoButtonColor = false, + [Roact.Event.Activated] = function() + self:setState({ + showModal = true, + }) + end + }), + modal = showModal and Roact.createElement(Roact.Portal, { + target = ModalContainer, + }, { + sheet = Roact.createElement(ModalBottomSheet, { + bottomGap = 10, + screenWidth = self.props.width, + onDismiss = function() + self:setState({ + showModal = false, + }) + end, + buttonModels = dummyModalButtons1, + }) + }) + }) +end + +return function(target) + local ModalContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ZIndex = 10, + }) + + Roact.mount(ModalContainer, target, "ModalContainer") + + local handle = mountWithStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = 0, + }), target, "preview") + + local connection = target:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + local tree = withStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = target.AbsoluteSize.X, + })) + + Roact.update(handle, tree) + end) + + return function() + connection:Disconnect() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option2.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option2.story.lua new file mode 100644 index 0000000..01dda98 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option2.story.lua @@ -0,0 +1,106 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local ModalBottomSheet = require(script.Parent.Parent.ModalBottomSheet) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local doSomething = function(a) + print(a, "was pressed!") +end + +local dummyModalButtons2 = { + { + icon = Images["component_assets/circle_17"], + text = "option 1, with no images", + onActivated = doSomething, + }, + { + text = "option 2, with no images", + onActivated = doSomething, + }, +} + +local function withStyle(tree) + return Roact.createElement(StoryView, {}, { + oneChild = tree, + }) +end + +local function mountWithStyle(tree, target, name) + local styledTree = withStyle(tree) + + return Roact.mount(styledTree, target, name) +end + +local overlayComponent = Roact.Component:extend("overlayComponent") + +function overlayComponent:render() + local ModalContainer = self.props.ModalContainer + local showModal = self.state.showModal + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + Layout = Roact.createElement("UIListLayout", { + + }), + TestButton1 = Roact.createElement("TextButton", { + Text = "Spawn 2 Choice", + Size = UDim2.new(1, 0, 0.2, 0), + BackgroundColor3 = Color3.fromRGB(22, 22, 222), + AutoButtonColor = false, + [Roact.Event.Activated] = function() + self:setState({ + showModal = true, + }) + end + }), + modal = showModal and Roact.createElement(Roact.Portal, { + target = ModalContainer, + }, { + sheet = Roact.createElement(ModalBottomSheet, { + bottomGap = 10, + screenWidth = self.props.width, + onDismiss = function() + self:setState({ + showModal = false, + }) + end, + showImages = false, + buttonModels = dummyModalButtons2, + }) + }) + }) +end + +return function(target) + local ModalContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ZIndex = 10, + }) + + Roact.mount(ModalContainer, target, "ModalContainer") + + local handle = mountWithStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = 0, + }), target, "preview") + + local connection = target:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + local tree = withStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = target.AbsoluteSize.X, + })) + + Roact.update(handle, tree) + end) + + return function() + connection:Disconnect() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option9.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option9.story.lua new file mode 100644 index 0000000..c09d6f5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option9.story.lua @@ -0,0 +1,141 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local ModalBottomSheet = require(script.Parent.Parent.ModalBottomSheet) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local doSomething = function(a) + print(a, "was pressed!") +end + +local dummyModalButtons9 = { + { + icon = Images["component_assets/circle_17"], + text = "option 1", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 2", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 3", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 4", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 5", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 6", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 7", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 8", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 9", + onActivated = doSomething, + }, +} + +local function withStyle(tree) + return Roact.createElement(StoryView, {}, { + oneChild = tree, + }) +end + +local function mountWithStyle(tree, target, name) + local styledTree = withStyle(tree) + + return Roact.mount(styledTree, target, name) +end + +local overlayComponent = Roact.Component:extend("overlayComponent") + +function overlayComponent:render() + local ModalContainer = self.props.ModalContainer + local showModal = self.state.showModal + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + Layout = Roact.createElement("UIListLayout", { + + }), + TestButton1 = Roact.createElement("TextButton", { + Text = "Spawn 9 Choice", + Size = UDim2.new(1, 0, 0.2, 0), + BackgroundColor3 = Color3.fromRGB(0, 111, 0), + AutoButtonColor = false, + [Roact.Event.Activated] = function() + self:setState({ + showModal = true, + }) + end + }), + modal = showModal and Roact.createElement(Roact.Portal, { + target = ModalContainer, + }, { + sheet = Roact.createElement(ModalBottomSheet, { + bottomGap = 10, + screenWidth = self.props.width, + onDismiss = function() + self:setState({ + showModal = false, + }) + end, + buttonModels = dummyModalButtons9, + }) + }) + }) +end + +return function(target) + local ModalContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ZIndex = 10, + }) + + Roact.mount(ModalContainer, target, "ModalContainer") + + local handle = mountWithStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = 0, + }), target, "preview") + + local connection = target:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + local tree = withStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = target.AbsoluteSize.X, + })) + + Roact.update(handle, tree) + end) + + return function() + connection:Disconnect() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/RenderHint.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/RenderHint.story.lua new file mode 100644 index 0000000..6f5cea1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/RenderHint.story.lua @@ -0,0 +1,118 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local ModalBottomSheet = require(script.Parent.Parent.ModalBottomSheet) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local doSomething = function(a) + print(a, "was pressed!") +end + +local renderDummyHint = function() + return Roact.createElement("TextLabel", { + BackgroundTransparency = 0, + BorderSizePixel = 0, + BackgroundColor3 = Color3.new(255, 255, 255), + Text = "R", + TextColor3 = Color3.fromRGB(0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + }) +end + +local dummyModalButtons1 = { + { + icon = Images["component_assets/circle_17"], + text = "option 1", + onActivated = doSomething, + renderRightElement = renderDummyHint, + }, + { + icon = Images["component_assets/circle_17"], + text = "longer option that will be truncated", + onActivated = doSomething, + renderRightElement = renderDummyHint, + }, +} + +local function withStyle(tree) + return Roact.createElement(StoryView, {}, { + oneChild = tree, + }) +end + +local function mountWithStyle(tree, target, name) + local styledTree = withStyle(tree) + + return Roact.mount(styledTree, target, name) +end + +local overlayComponent = Roact.Component:extend("overlayComponent") + +function overlayComponent:render() + local ModalContainer = self.props.ModalContainer + local showModal = self.state.showModal + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + Layout = Roact.createElement("UIListLayout", { + + }), + TestButton1 = Roact.createElement("TextButton", { + Text = "Spawn 2 Choice", + Size = UDim2.new(1,0,0.2,0), + BackgroundColor3 = Color3.fromRGB(222,0,0), + AutoButtonColor = false, + [Roact.Event.Activated] = function() + self:setState({ + showModal = true, + }) + end + }), + modal = showModal and Roact.createElement(Roact.Portal, { + target = ModalContainer, + }, { + sheet = Roact.createElement(ModalBottomSheet, { + bottomGap = 10, + screenWidth = self.props.width, + onDismiss = function() + self:setState({ + showModal = false, + }) + end, + buttonModels = dummyModalButtons1, + }) + }) + }) +end + +return function(target) + local ModalContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ZIndex = 10, + }) + + Roact.mount(ModalContainer, target, "ModalContainer") + + local handle = mountWithStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = 0, + }), target, "preview") + + local connection = target:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + local tree = withStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = target.AbsoluteSize.X, + })) + + Roact.update(handle, tree) + end) + + return function() + connection:Disconnect() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Repositioned.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Repositioned.story.lua new file mode 100644 index 0000000..02641e0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Repositioned.story.lua @@ -0,0 +1,123 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local ModalBottomSheet = require(script.Parent.Parent.ModalBottomSheet) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local doSomething = function(a) + print(a, "was pressed!") +end + +local dummyModalButtons5 = { + { + icon = Images["component_assets/circle_17"], + text = "option 1", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 2", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 3", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 4", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 5", + onActivated = doSomething, + }, +} + +local function withStyle(tree) + return Roact.createElement(StoryView, {}, { + oneChild = tree, + }) +end + +local function mountWithStyle(tree, target, name) + local styledTree = withStyle(tree) + + return Roact.mount(styledTree, target, name) +end + +local overlayComponent = Roact.Component:extend("overlayComponent") + +function overlayComponent:render() + local ModalContainer = self.props.ModalContainer + local showModal = self.state.showModal + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + Layout = Roact.createElement("UIListLayout", { + + }), + TestButton1 = Roact.createElement("TextButton", { + Text = "Spawn repositioned Choice", + Size = UDim2.new(1, 0, 0.2, 0), + BackgroundColor3 = Color3.fromRGB(0, 111, 0), + AutoButtonColor = false, + [Roact.Event.Activated] = function() + self:setState({ + showModal = true, + }) + end + }), + modal = showModal and Roact.createElement(Roact.Portal, { + target = ModalContainer, + }, { + sheet = Roact.createElement(ModalBottomSheet, { + bottomGap = 10, + screenWidth = self.props.width, + onDismiss = function() + self:setState({ + showModal = false, + }) + end, + buttonModels = dummyModalButtons5, + sheetContentXSize = UDim.new(0.5, 0), + sheetContentXPosition = UDim.new(0.5, 0), + }) + }) + }) +end + +return function(target) + local ModalContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ZIndex = 10, + }) + + Roact.mount(ModalContainer, target, "ModalContainer") + + local handle = mountWithStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = 0, + }), target, "preview") + + local connection = target:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + local tree = withStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = target.AbsoluteSize.X, + })) + + Roact.reconcile(handle, tree) + end) + + return function() + connection:Disconnect() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/StayOnActivated.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/StayOnActivated.story.lua new file mode 100644 index 0000000..85a6ab3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/StayOnActivated.story.lua @@ -0,0 +1,106 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local ModalBottomSheet = require(script.Parent.Parent.ModalBottomSheet) + +local doSomething = function(a) + print(a, "was pressed!") +end + +local dummyModalButtons2 = { + { + text = "Stay on click", + onActivated = doSomething, + stayOnActivated = true, + }, + { + text = "Dismiss on click", + onActivated = doSomething, + stayOnActivated = false, + }, +} + +local function withStyle(tree) + return Roact.createElement(StoryView, {}, { + oneChild = tree, + }) +end + +local function mountWithStyle(tree, target, name) + local styledTree = withStyle(tree) + + return Roact.mount(styledTree, target, name) +end + +local overlayComponent = Roact.Component:extend("overlayComponent") + +function overlayComponent:render() + local ModalContainer = self.props.ModalContainer + local showModal = self.state.showModal + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + Layout = Roact.createElement("UIListLayout", { + + }), + TestButton1 = Roact.createElement("TextButton", { + Text = "Spawn Choices", + Size = UDim2.new(1, 0, 0.2, 0), + BackgroundColor3 = Color3.fromRGB(22, 22, 222), + AutoButtonColor = false, + [Roact.Event.Activated] = function() + self:setState({ + showModal = true, + }) + end + }), + modal = showModal and Roact.createElement(Roact.Portal, { + target = ModalContainer, + }, { + sheet = Roact.createElement(ModalBottomSheet, { + bottomGap = 10, + screenWidth = self.props.width, + onDismiss = function() + self:setState({ + showModal = false, + }) + end, + showImages = false, + buttonModels = dummyModalButtons2, + }) + }) + }) +end + +return function(target) + local ModalContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ZIndex = 10, + }) + + Roact.mount(ModalContainer, target, "ModalContainer") + + local handle = mountWithStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = 0, + }), target, "preview") + + local connection = target:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + local tree = withStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = target.AbsoluteSize.X, + })) + + Roact.update(handle, tree) + end) + + return function() + connection:Disconnect() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/StateTable/StateTable.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/StateTable/StateTable.lua new file mode 100644 index 0000000..8efc88c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/StateTable/StateTable.lua @@ -0,0 +1,195 @@ +local UIBloxRoot = script.Parent.Parent +local Cryo = require(UIBloxRoot.Parent.Cryo) + +local StateTable = {} + +StateTable.__index = StateTable + +local function validateStateTableItem(item, qualifier) + local type = typeof(item) + local isValid = type == "string" or type == "userdata" + assert(isValid, string.format("%s '%s' must be a string or userdata but is a %s", qualifier, tostring(item), type)) +end + +--[[ + This class method creates a new StateTable instance that you can use to control complicated + logic that is based upon your state machine design. Ex: + + self.stateTable = StateTable.new(name, initialState, initialContext, { + InitialState = { + EventName1 = { nextState = "StateOne", action = self.actionDoSomething }, + EventName2 = { nextState = "FinalState" } -- actions are optional + }, + StateOne = { + EventName1 = { nextState = "FinalState", action = self.actionDoSomethingAtLast }, + EventName3 = { action = self.actionDoSomethingElse } -- will maintain current state + }, + FinalState = {} -- transitions are optional + }) + + Arguments: + name - A debug name for this StateTable. (String) + initialState - Name of the beginning state for this StateTable. (String) + initialContext - A reference to an existing table where you hold all sidecar contextual data + that needs to be manipulated by this StateTable's actions. (Table) + transitionTable - Description of the state machine structure. (Table) + + The outermost keys in "transitionTable" represent individual states in your design, each of which + contains a description of the events that can be called while in that state. Calling an event + triggers a transition to a new state while also (optionally) running an action functor. + + (All states and events must be simple strings or userdata.) + (If using userdata for states and events, implement a tostring metamethod for ease of debugging.) + + Named events in your state table will be converted into functions that you can call directly. + Calling these event functions will transition the StateTable to the appropriate nextState and + call the registered action handler, if any. You may pass arguments to your actions through the + event function by passing them as a table. Ex: + + self.stateTable.events.EventName1(args) + + The combination of named states and events in StateTable make up the control flow portion of your + state machine. To run business logic, you need to implement Actions. + + Each action functor accepts four arguments: the current state, the next state, event arguments, + and the contextual data table that you passed in when you called the event function. Your action + functor should return a table containing the keys that need to be updated from currentContext. + The returned table will be merged into currentContext and passed back to your onStateChange + callback. Ex: + + function actionDoSomething(currentState, nextState, args, currentContext) + local contextDiff = doSomething(args, currentContext) + return contextDiff + end + + Do NOT update your own copy of the StateTable's internal state variable or your context in actions. + If this is an action-less transition, you'll fail to update it! + + To update your context and own tracking of the current state at the same time, listen to changes + via StateTable:onStateChange. See the documentation of that method for more details. +]] +function StateTable.new(name, initialState, initialContext, transitionTable) + assert(typeof(name) == "string", "name must be a string") + assert(#name > 0, "name must not be an empty string") + + validateStateTableItem(initialState, "initialState") + assert(initialContext == nil or typeof(initialContext) == "table", "initialContext must be a table or nil") + + assert(typeof(transitionTable) == "table", "transitionTable must be a table") + assert(typeof(transitionTable[initialState]) == "table", "initialState must be present in transitionTable") + + local self = {} + setmetatable(self, StateTable) + + self.name = name + self.currentState = initialState + self.currentContext = initialContext or {} + self.transitionTable = {} + self.events = {} + + for state, eventTable in pairs(transitionTable) do + validateStateTableItem(state, "state") + assert(typeof(eventTable) == "table", string.format("state '%s' must map to a table", tostring(state))) + + local parsedEventTable = {} + for event, eventData in pairs(eventTable) do + validateStateTableItem(event, "event") + assert(typeof(eventData) == "table", string.format("event '%s' must map to a table", tostring(event))) + + local nextState = eventData.nextState + local action = eventData.action + + if nextState ~= nil then + validateStateTableItem(nextState, "nextState") + + -- Check that the transition lands on a known state + assert(transitionTable[nextState] ~= nil, + string.format("nextState '%s' does not exist in transitionTable", tostring(nextState))) + end + + assert(action == nil or typeof(action) == "function", "action must be a function") + + parsedEventTable[event] = eventData + + -- Create a function to make it easy to call this event + if self.events[event] == nil then + self.events[event] = function(args) + return self:handleEvent(event, args) + end + end + end + + self.transitionTable[state] = parsedEventTable + end + + -- catch calls to invalid events earlier + setmetatable(self.events, { + __index = function(_, event) + error(string.format("'%s' is not a valid event in StateTable '%s'", + tostring(event), self.name), 2) + end + }) + + return self +end + +--[[ + It is recommended that you use the auto-generated event functions instead + of calling this method; see StateTable.new. + + Process an event through this StateTable instance. Pass in the name + of the event, and optional arguments. The arguments will be passed to the + registered action handler for the state/event transition, if any. + + This function does not return anything. Listen to changes using + StateTable:onStateChange if you need to store the current state or use the + results of an action. +]] +function StateTable:handleEvent(event, args) + validateStateTableItem(event, "event") + assert(args == nil or typeof(args) == "table", "args must be nil or valid table") + + local currentState = self.currentState + local eventMap = self.transitionTable[currentState] + + assert(eventMap ~= nil, "no transition events for current state") + + if eventMap[event] ~= nil then + local eventData = eventMap[event] + local nextState = eventData.nextState or currentState + local action = eventData.action + + local updatedContext = self.currentContext + if action ~= nil then + local contextDiff = action(currentState, nextState, args, self.currentContext) or {} + updatedContext = Cryo.Dictionary.join(self.currentContext, contextDiff) + self.currentContext = updatedContext + end + + self.currentState = nextState + + if self.stateChangeHandler ~= nil then + self.stateChangeHandler(currentState, nextState, updatedContext) + end + end +end + +--[[ + Register a function to process changes in state. Your function should have + the following signature and return nothing: + + function handleStateChange(oldState, newState, updatedContext) + self.currentState = newState + self.currentContext = updatedContext + end + + The updatedContext parameter contains the table that was returned by the action + handler associated with the event transition. +]] +function StateTable:onStateChange(stateChangeHandler) + assert(stateChangeHandler == nil or typeof(stateChangeHandler) == "function", + "stateChangeHandler must be nil or a function") + self.stateChangeHandler = stateChangeHandler +end + +return StateTable diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/StateTable/StateTable.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/StateTable/StateTable.spec.lua new file mode 100644 index 0000000..df751d9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/StateTable/StateTable.spec.lua @@ -0,0 +1,525 @@ +return function() + local StateTable = require(script.Parent.StateTable) + + local TEST_NAME = "test_state_table_name" + local TEST_INITIAL_STATE = "Initial" + local TEST_DATA = { foo = 1 } + + local DUMMY_ACTION = function(_, _, data) + return data + end + + local function FieldCount(t) + local fieldCount = 0 + for _ in pairs(t) do + fieldCount = fieldCount + 1 + end + return fieldCount + end + + local function ShallowEqual(A, B) + if not A or not B then + return false + elseif A == B then + return true + end + + for key, value in pairs(A) do + if B[key] ~= value then + return false + end + end + for key, value in pairs(B) do + if A[key] ~= value then + return false + end + end + + return true + end + + describe("StateTable.new validation throw tests", function() + it("should throw if name is nil", function() + expect(function() + StateTable.new(nil, TEST_INITIAL_STATE, {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if name is not a string", function() + expect(function() + StateTable.new(5, TEST_INITIAL_STATE, {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if name is empty", function() + expect(function() + StateTable.new("", TEST_INITIAL_STATE, {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if initialState is nil", function() + expect(function() + StateTable.new(TEST_NAME, nil, {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if initialState is not a string", function() + expect(function() + StateTable.new(TEST_NAME, 5, {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if initialState is empty", function() + expect(function() + StateTable.new(TEST_NAME, "", {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if initialContext is wrong type", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, 5, { Initial = {} }) + end).to.throw() + end) + + it("should throw if transitionTable is nil", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, nil) + end).to.throw() + end) + + it("should throw if empty transitionTable is provided", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, {}) + end).to.throw() + end) + + it("should throw if non-table transitionTable is provided", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, 1) + end).to.throw() + end) + + it("should throw if no arguments are provided", function() + expect(function() + StateTable.new() + end).to.throw() + end) + + it("should throw if non-string/userdata used for state name", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { [1] = {} }) + end).to.throw() + end) + + it("should throw if non-table is used for state table", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = 1 }) + end).to.throw() + end) + + it("should throw if non-string/userdata is used for event name", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { [1] = {} } }) + end).to.throw() + end) + + it("should throw if non-table is used for event data table", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = 1 } }) + end).to.throw() + end) + + it("should throw if non-string/userdata used for nextState", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { nextState = 1 } } }) + end).to.throw() + end) + + it("should throw if non-function used for action", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { action = 1 } } }) + end).to.throw() + end) + + it("should throw if initial state not in transitionTable", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { NotInitial = {} }) + end).to.throw() + end) + end) + + describe("StateTable.new validation success tests", function() + it("should not throw if empty event table is provided", function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} }) + end) + + it("should not throw if empty event data table is provided", function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = {} } }) + end) + + it("should not throw if action is missing", function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Next" } + }, + Next = {} + }) + end) + + it("should not throw if nextState is missing", function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { action = function() end } } }) + end) + end) + + describe("StateTable.new event function creation tests", function() + it("should create an event function for a single event", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { } } }) + expect(FieldCount(st.events)).to.equal(1) + expect(typeof(st.events.Event1)).to.equal("function") + end) + + it("should create different event functions for multiple events", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { }, Event2 = { } } }) + expect(FieldCount(st.events)).to.equal(2) + expect(typeof(st.events.Event1)).to.equal("function") + expect(typeof(st.events.Event2)).to.equal("function") + end) + + it("should not create event functions without at least one event", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} }) + expect(FieldCount(st.events)).to.equal(0) + end) + end) + + describe("StateTable onStateChange registration tests", function() + it("should assert when non-function provided", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} }) + expect(function() + st:onStateChange(5) + end).to.throw() + end) + + it("should not assert when function provided", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} }) + st:onStateChange(function() end) + end) + + it("should not assert when nil provided", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} }) + st:onStateChange(nil) + end) + + it("should call onStateChange handler when transition occurs", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = {} + } + }) + + local called = false + st:onStateChange(function() + called = true + end) + + st.events.Event1(nil) + expect(called).to.equal(true) + end) + + it("should not call onStateChange handler after it has been de-registered", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = {} + } + }) + + local called = false + st:onStateChange(function() + called = true + end) + + st:onStateChange(nil) + + st.events.Event1(nil) + expect(called).to.equal(false) + end) + end) + + describe("StateTable event call tests", function() + it("should change state when handleEvent is called", function() + local action1Called = false + local testAction1 = function() action1Called = true end + + local action2Called = false + local testAction2 = function() action2Called = true end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two", action = testAction1 } + }, + Two = { + Event2 = { action = testAction2 } + } + }) + + st:handleEvent("Event1", nil) + expect(action1Called).to.equal(true) + expect(action2Called).to.equal(false) + + action1Called = false + action2Called = false + + st:handleEvent("Event2", nil) + expect(action1Called).to.equal(false) + expect(action2Called).to.equal(true) + end) + + it("should call mapped action when handleEvent is called", function() + local actionOldState, actionNewState, actionData + local testAction = function(oldState, newState, data) + actionOldState = oldState + actionNewState = newState + actionData = data + end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two", action = testAction } + }, + Two = {} + }) + + st:handleEvent("Event1", TEST_DATA) + expect(actionOldState).to.equal("Initial") + expect(actionNewState).to.equal("Two") + expect(ShallowEqual(actionData, TEST_DATA)).to.equal(true) + end) + + it("should return expected nextState and data in onStateChange callback when handleEvent is called", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two", action = DUMMY_ACTION } + }, + Two = {} + }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st:handleEvent("Event1", TEST_DATA) + + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Two") + expect(ShallowEqual(updatedContext, TEST_DATA)).to.equal(true) + end) + + it("should call mapped action when event functor is called", function() + local actionOldState, actionNewState, actionData + local testAction = function(oldState, newState, data) + actionOldState = oldState + actionNewState = newState + actionData = data + end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two", action = testAction } + }, + Two = {} + }) + + st.events.Event1(TEST_DATA) + expect(actionOldState).to.equal("Initial") + expect(actionNewState).to.equal("Two") + expect(ShallowEqual(actionData, TEST_DATA)).to.equal(true) + end) + + it("should return expected nextState and data in onStateChange callback when event functor is called", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two", action = DUMMY_ACTION } + }, + Two = {} + }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st.events.Event1(TEST_DATA) + + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Two") + expect(ShallowEqual(updatedContext, TEST_DATA)).to.equal(true) + end) + + it("should still return nextState in onStateChange callback when no action handler is provided", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two" } + }, + Two = {} + }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st.events.Event1(TEST_DATA) + + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Two") + expect(typeof(updatedContext)).to.equal("table") + expect(FieldCount(updatedContext)).to.equal(0) + end) + + it("should return current state and empty data if new state and action handler are not specified", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { } } }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st.events.Event1(TEST_DATA) + + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Initial") + expect(FieldCount(updatedContext)).to.equal(0) + end) + + it("should return current state and matching data if only action handler is provided", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { action = DUMMY_ACTION } } }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st.events.Event1(TEST_DATA) + + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Initial") + expect(ShallowEqual(updatedContext, TEST_DATA)).to.equal(true) + end) + + it("should return empty data in onStateChange callback when nil data is provided to no-action event", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = {} } }) + + local updatedContext + st:onStateChange(function(_, _, uc) + updatedContext = uc + end) + + st.events.Event1(nil) + + expect(FieldCount(updatedContext)).to.equal(0) + end) + + it("should merge context updates with old context", function() + local initialContext = { foo = 1 } + + local function action1() + return { bar = 2 } + end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, initialContext, { + Initial = { + Event1 = { action = action1 } + } + }) + + local updatedContext + st:onStateChange(function(_, _, uc) + updatedContext = uc + end) + + st.events.Event1() + + expect(ShallowEqual(updatedContext, { foo = 1, bar = 2 })).to.equal(true) + end) + + it("should pass args to actions", function() + local passedArgs + local function action1(_, _, args) + passedArgs = args + end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { Event1 = { action = action1 } } + }) + + local theArgs = { argsAreHere = true } + st.events.Event1(theArgs) + + expect(ShallowEqual(theArgs, passedArgs)).to.equal(true) + end) + + it("should call actions independently for different events", function() + local testData1 = TEST_DATA + local testData2 = { foo = 2 } + + local action1OldState, action1NewState, action1Data + local testAction1 = function(oldState, newState, data) + action1OldState = oldState + action1NewState = newState + action1Data = data + return data + end + + local action2OldState, action2NewState, action2Data + local testAction2 = function(oldState, newState, data) + action2OldState = oldState + action2NewState = newState + action2Data = data + return data + end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { action = testAction1 }, + Event2 = { nextState = "Two", action = testAction2 }, + }, + Two = {} + }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st.events.Event1(testData1) + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Initial") + expect(ShallowEqual(updatedContext, testData1)).to.equal(true) + + st.events.Event2(testData2) + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Two") + expect(ShallowEqual(updatedContext, testData2)).to.equal(true) + + expect(action1OldState).to.equal("Initial") + expect(action2OldState).to.equal("Initial") + expect(action1NewState).to.equal("Initial") + expect(action2NewState).to.equal("Two") + + expect(ShallowEqual(action1Data, testData1)).to.equal(true) + expect(ShallowEqual(action2Data, testData2)).to.equal(true) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/AppStyle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/AppStyle.lua new file mode 100644 index 0000000..0685b54 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/AppStyle.lua @@ -0,0 +1,22 @@ +local StyleRoot = script.Parent +local UIBloxRoot = StyleRoot.Parent +local createSignal = require(UIBloxRoot.Utility.createSignal) + +local AppStyle = {} + +function AppStyle.new(style) + local self = {} + self.style = style + self.signal = createSignal() + setmetatable(self, { + __index = AppStyle, + }) + return self +end + +function AppStyle:update(newStyle) + self.style = newStyle + self.signal:fire(newStyle) +end + +return AppStyle \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/AppStyle.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/AppStyle.spec.lua new file mode 100644 index 0000000..d6bae7e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/AppStyle.spec.lua @@ -0,0 +1,60 @@ +return function() + local AppStyle = require(script.Parent.AppStyle) + + local testTheme = { + Background1 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background2 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background3 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background4 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0.3, -- Alpha 0.7 + }, + } + + local testFont = { + Normal = Enum.Font.Gotham, + Title = Enum.Font.GothamBold, + } + + local testAssets = { + ButtonFill9Slice = "buttonFill", + ButtonBorder9Slice = "buttonStroke", + Button9Slice = Rect.new(8, 8, 9, 9), + } + + it("should connect and fire signal for style change without errors", function() + local testStyle = { + Theme = testTheme, + Font = testFont, + Assets = testAssets, + } + local appStyle = AppStyle.new(testStyle) + + expect(appStyle.style).to.be.a("table") + + local testValue = "some test theme" + local testTable = { + Theme = testValue, + } + local disconnect = appStyle.signal:subscribe(function(newValues) + expect(newValues).to.be.a("table") + expect(newValues.Theme).to.equal(testValue) + end) + + appStyle:update(testTable) + + expect(appStyle.style).to.be.a("table") + expect(appStyle.style.Theme).to.equal(testValue) + disconnect() + end) + +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleConsumer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleConsumer.lua new file mode 100644 index 0000000..4f6b681 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleConsumer.lua @@ -0,0 +1,47 @@ +local StyleRoot = script.Parent +local UIBloxRoot = StyleRoot.Parent +local Roact = require(UIBloxRoot.Parent.Roact) +local t = require(UIBloxRoot.Parent.t) + +local StyleConsumer = Roact.Component:extend("StyleConsumer") + +--Note: remove along with styleRefactorConfig +local UIBloxConfig = require(UIBloxRoot.UIBloxConfig) +local styleRefactorConfig = UIBloxConfig.styleRefactorConfig +--- + +local validateProps = t.strictInterface({ + render = t.callback, +}) + +function StyleConsumer:init() + if styleRefactorConfig then + warn("Using deprecated `UIBlox.Style.withStyle`. Please use `UIBlox.Core.Style.withStyle`") + end + + self.appStyle = self._context.AppStyle + local currentStyle = self.appStyle.style + + self.state = { + style = currentStyle, + } +end + +function StyleConsumer:render() + assert(validateProps(self.props)) + return self.props.render(self.state.style) +end + +function StyleConsumer:didMount() + self.disconnectStyleListener = self.appStyle.signal:subscribe(function(newStyle) + self:setState({ + style = newStyle, + }) + end) +end + +function StyleConsumer:willUnmount() + self.disconnectStyleListener() +end + +return StyleConsumer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleConsumer.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleConsumer.spec.lua new file mode 100644 index 0000000..4ea2b0e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleConsumer.spec.lua @@ -0,0 +1,29 @@ +return function() + local StyleRoot = script.Parent + local UIBloxRoot = StyleRoot.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local testStyle = require(StyleRoot.Validator.TestStyle) + local StyleProvider = require(StyleRoot.StyleProvider) + local StyleConsumer = require(StyleRoot.StyleConsumer) + + + it("should create and destroy without errors", function() + local renderFunction = function(style) + expect(style).to.be.a("table") + return Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }) + end + local element = Roact.createElement(StyleProvider, { + style = testStyle, + }, { + StyleConsumer = Roact.createElement(StyleConsumer, { + render = renderFunction + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleProvider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleProvider.lua new file mode 100644 index 0000000..c313ba5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleProvider.lua @@ -0,0 +1,43 @@ +local StyleRoot = script.Parent +local UIBloxRoot = StyleRoot.Parent +local Roact = require(UIBloxRoot.Parent.Roact) +local t = require(UIBloxRoot.Parent.t) +local AppStyle = require(StyleRoot.AppStyle) +local validateStyle = require(StyleRoot.Validator.validateStyle) + +local StyleProvider = Roact.Component:extend("StyleProvider") + +--Note: remove along with styleRefactorConfig +local UIBloxConfig = require(UIBloxRoot.UIBloxConfig) +local styleRefactorConfig = UIBloxConfig.styleRefactorConfig +--- + +local validateStyleProviderProps = t.strictInterface({ + -- The current style of the app. + style = validateStyle, + [Roact.Children] = t.table, +}) + +function StyleProvider:init() + if styleRefactorConfig then + warn("Using deprecated `UIBlox.Style.Provider`. Please use `UIBlox.Core.Style.Provider`") + end + + local style = self.props.style + self.appStyle = AppStyle.new(style) + self._context.AppStyle = self.appStyle +end + +function StyleProvider:render() + assert(validateStyleProviderProps(self.props)) + assert(self.props.style ~= nil, "StyleProvider style should not be nil.") + return Roact.oneChild(self.props[Roact.Children]) +end + +function StyleProvider:didUpdate(previousProps) + if self.props.style ~= previousProps.style then + self.appStyle:update(self.props.style) + end +end + +return StyleProvider \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleProvider.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleProvider.spec.lua new file mode 100644 index 0000000..8ea95b7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleProvider.spec.lua @@ -0,0 +1,37 @@ +return function() + local StyleRoot = script.Parent + local UIBloxRoot = StyleRoot.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local testStyle = require(StyleRoot.Validator.TestStyle) + local StyleProvider = require(StyleRoot.StyleProvider) + + it("should create and destroy without errors", function() + local someComponent = Roact.createElement("TextLabel", { + Text = "test", + }) + local styleProvider = Roact.createElement(StyleProvider, { + style = testStyle, + }, { + SomeComponent = someComponent, + }) + + local roactInstance = Roact.mount(styleProvider) + Roact.unmount(roactInstance) + end) + + it("should throw given an invalid style palette", function() + local invalidStyle = {} + local someComponent = Roact.createElement("TextLabel", { + Text = "test", + }) + local styleProvider = Roact.createElement(StyleProvider, { + style = invalidStyle, + }, { + SomeComponent = someComponent, + }) + expect(function() + local roactInstance = Roact.mount(styleProvider) + Roact.unmount(roactInstance) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/TestStyle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/TestStyle.lua new file mode 100644 index 0000000..e3ac269 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/TestStyle.lua @@ -0,0 +1,67 @@ +local color = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, +} +local testTheme = { + BackgroundDefault = color, + BackgroundContrast = color, + BackgroundMuted = color, + BackgroundUIDefault = color, + BackgroundUIContrast = color, + BackgroundOnHover = color, + BackgroundOnPress = color, + UIDefault = color, + UIMuted = color, + UIEmphasis = color, + ContextualPrimaryDefault = color, + ContextualPrimaryOnHover = color, + ContextualPrimaryContent = color, + SystemPrimaryDefault = color, + SystemPrimaryOnHover = color, + SystemPrimaryContent = color, + SecondaryDefault = color, + SecondaryOnHover = color, + SecondaryContent = color, + IconDefault = color, + IconEmphasis = color, + IconOnHover = color, + TextEmphasis = color, + TextDefault = color, + TextMuted = color, + Divider = color, + Overlay = color, + DropShadow = color, + NavigationBar = color, + PlaceHolder = color, + OnlineStatus = color, + OfflineStatus = color, + Success = color, + Alert = color, + Badge = color, + BadgeContent = color, +} + +local font = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 1, + RelativeMinSize = 1, +} +local testFont = { + BaseSize = 10, + Title = font, + Header1 = font, + Header2 = font, + SubHeader1 = font, + Body = font, + CaptionHeader = font, + CaptionSubHeader = font, + CaptionBody = font, + Footer = font, +} + +local testStyle = { + Theme = testTheme, + Font = testFont, +} + +return testStyle \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/TestStyle.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/TestStyle.spec.lua new file mode 100644 index 0000000..92ab26c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/TestStyle.spec.lua @@ -0,0 +1,7 @@ +return function() + local validateStyle = require(script.Parent.validateStyle) + local testStyle = require(script.Parent.TestStyle) + it("Should be valid", function() + assert(validateStyle(testStyle)) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateColorInfo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateColorInfo.lua new file mode 100644 index 0000000..ec3f168 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateColorInfo.lua @@ -0,0 +1,9 @@ +local ValidatorRoot = script.Parent +local StyleRoot = ValidatorRoot.Parent +local UIBloxRoot = StyleRoot.Parent +local t = require(UIBloxRoot.Parent.t) + +return t.strictInterface({ + Color = t.Color3, + Transparency = t.numberConstrained(0, 1), +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateFont.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateFont.lua new file mode 100644 index 0000000..cf91832 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateFont.lua @@ -0,0 +1,21 @@ +local ValidatorRoot = script.Parent +local StyleRoot = ValidatorRoot.Parent +local UIBloxRoot = StyleRoot.Parent +local t = require(UIBloxRoot.Parent.t) + +local Font = require(ValidatorRoot.validateFontInfo) + +local FontPalette = t.strictInterface({ + BaseSize = t.numberMinExclusive(0), + Title = Font, + Header1 = Font, + Header2 = Font, + SubHeader1 = Font, + Body = Font, + CaptionHeader = Font, + CaptionSubHeader = Font, + CaptionBody = Font, + Footer = Font, +}) + +return FontPalette diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateFontInfo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateFontInfo.lua new file mode 100644 index 0000000..7060de4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateFontInfo.lua @@ -0,0 +1,10 @@ +local ValidatorRoot = script.Parent +local StyleRoot = ValidatorRoot.Parent +local UIBloxRoot = StyleRoot.Parent +local t = require(UIBloxRoot.Parent.t) + +return t.strictInterface({ + RelativeSize = t.numberMinExclusive(0), + RelativeMinSize = t.numberMinExclusive(0), + Font = t.EnumItem, +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateStyle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateStyle.lua new file mode 100644 index 0000000..17c9157 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateStyle.lua @@ -0,0 +1,13 @@ +local ValidatorRoot = script.Parent +local StyleRoot = ValidatorRoot.Parent +local UIBloxRoot = StyleRoot.Parent +local t = require(UIBloxRoot.Parent.t) +local validateTheme = require(ValidatorRoot.validateTheme) +local validateFont = require(ValidatorRoot.validateFont) + +local StylePalette = t.strictInterface({ + Theme = validateTheme, + Font = validateFont, +}) + +return StylePalette diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateTheme.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateTheme.lua new file mode 100644 index 0000000..dc01d9e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateTheme.lua @@ -0,0 +1,56 @@ +local ValidatorRoot = script.Parent +local StyleRoot = ValidatorRoot.Parent +local UIBloxRoot = StyleRoot.Parent +local t = require(UIBloxRoot.Parent.t) + +local Color = require(ValidatorRoot.validateColorInfo) + +local ThemePalette = t.strictInterface({ + BackgroundDefault = Color, + BackgroundContrast = Color, + BackgroundMuted = Color, + BackgroundUIDefault = Color, + BackgroundUIContrast = Color, + BackgroundOnHover = Color, + BackgroundOnPress = Color, + + UIDefault = Color, + UIMuted = Color, + UIEmphasis = Color, + + ContextualPrimaryDefault = Color, + ContextualPrimaryOnHover = Color, + ContextualPrimaryContent = Color, + + SystemPrimaryDefault = Color, + SystemPrimaryOnHover = Color, + SystemPrimaryContent = Color, + + SecondaryDefault = Color, + SecondaryOnHover = Color, + SecondaryContent = Color, + + IconDefault = Color, + IconEmphasis = Color, + IconOnHover = Color, + + TextEmphasis = Color, + TextDefault = Color, + TextMuted = Color, + + Divider = Color, + Overlay = Color, + DropShadow = Color, + NavigationBar = Color, + PlaceHolder = Color, + + OnlineStatus = Color, + OfflineStatus = Color, + Success = Color, + Alert = Color, + + Badge = Color, + BadgeContent = Color, +}) + +return ThemePalette diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/withStyle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/withStyle.lua new file mode 100644 index 0000000..f5c3fc7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/withStyle.lua @@ -0,0 +1,13 @@ +local StyleRoot = script.Parent +local UIBloxRoot = StyleRoot.Parent +local Roact = require(UIBloxRoot.Parent.Roact) +local StyleConsumer = require(StyleRoot.StyleConsumer) + +--[[ + This is a utility function that will wrap StyleConsumer. + `renderCallback` will be invoked with the current style. It should return a Roact element. +]] +return function(renderCallback) + assert(type(renderCallback) == "function", "Expect renderCallback to be a function.") + return Roact.createElement(StyleConsumer, { render = renderCallback }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/withStyle.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/withStyle.spec.lua new file mode 100644 index 0000000..54dcb5b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/withStyle.spec.lua @@ -0,0 +1,30 @@ +return function() + local StyleRoot = script.Parent + local UIBloxRoot = StyleRoot.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local StyleProvider = require(StyleRoot.StyleProvider) + local withStyle = require(StyleRoot.withStyle) + local testStyle = require(StyleRoot.Validator.TestStyle) + + it("should create and destroy without errors", function() + local someTestElement = Roact.Component:extend("someTestElement") + -- luacheck: ignore unused argument self + function someTestElement:render() + return withStyle(function(style) + expect(style).to.be.a("table") + return Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }) + end) + end + + local element = Roact.createElement(StyleProvider, { + style = testStyle, + }, { + someTestElement = Roact.createElement(someTestElement), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/UIBloxConfig.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/UIBloxConfig.lua new file mode 100644 index 0000000..c50b312 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/UIBloxConfig.lua @@ -0,0 +1 @@ +return require(script.Parent).Config \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/UIBloxDefaultConfig.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/UIBloxDefaultConfig.lua new file mode 100644 index 0000000..53a4856 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/UIBloxDefaultConfig.lua @@ -0,0 +1,32 @@ +return { + -- fixToastResizeConfig: fixes bug where Toasts + -- will not resize when text changes. + fixToastResizeConfig = false, + + -- expandableTextAutomaticResizeConfig: refactor of ExpandableTextArea to + -- automatically resize to fit its container. Also removes width prop. + expandableTextAutomaticResizeConfig = false, + + -- enableAlertTitleIconConfig: turning this on allows the Alert component to take + -- in an optional titleIcon prop, which displays an icon above the Alert's title. + enableAlertTitleIconConfig = false, + + --styleRefactorConfig: switch to use the refactored style system from Core and App. + styleRefactorConfig = false, + + -- fixes the premium icon and text placement on item tiles + -- when the flag is false, the second line of title text will align with the start of the text. + -- when the flag is true, the second line will align with the start of the premium icon. + fixItemTilePremiumIcon = false, + + --modalWindowAnchorPoint: Allows passing an anchorPoint to a modalWindow (makes it easier + -- to use with a tween), and fix issues when the anchorPoint/position are not the usual (0.5, 0.5) + modalWindowAnchorPoint = false, + + --enableExperimentalGamepadSupport: Enables support of gamepad navigation via the roact-gamepad + -- library. This is currently experimental and not yet ready for release. + enableExperimentalGamepadSupport = false, + + --useNewUICornerRoundedCorners: Uses the new roblox CornerUI Instance instead of mask-based UI corners + useNewUICornerRoundedCorners = false, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/ExternalEventConnection.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/ExternalEventConnection.lua new file mode 100644 index 0000000..e0b3a30 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/ExternalEventConnection.lua @@ -0,0 +1,51 @@ +--[[ + A component that establishes a connection to a Roblox event when it is rendered. +]] +local GridRoot = script.Parent +local UIBloxRoot = GridRoot.Parent +local Roact = require(UIBloxRoot.Parent.Roact) +local ExternalEventConnection = Roact.Component:extend("ExternalEventConnection") + +function ExternalEventConnection:init() + self.connection = nil +end + +--[[ + Render the child component so that ExternalEventConnections can be nested like so: + + Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputBegan, + callback = inputBeganCallback, + }, { + Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputEnded, + callback = inputChangedCallback, + }) + }) +]] +function ExternalEventConnection:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +function ExternalEventConnection:didMount() + local event = self.props.event + local callback = self.props.callback + + self.connection = event:Connect(callback) +end + +function ExternalEventConnection:didUpdate(oldProps) + if self.props.event ~= oldProps.event or self.props.callback ~= oldProps.callback then + self.connection:Disconnect() + + self.connection = self.props.event:Connect(self.props.callback) + end +end + +function ExternalEventConnection:willUnmount() + self.connection:Disconnect() + + self.connection = nil +end + +return ExternalEventConnection \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.lua new file mode 100644 index 0000000..8c1b90a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.lua @@ -0,0 +1,158 @@ +--[[ + Creates a Roact component which will automatically animate. + The animations are created with Otter springs. + + Example: + To create a frame whose position animates up / down: + AnimatedFrame = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + regularProps = { + Size = ..., + BackgroundColor3 = ..., + }, + animatedValues = { + positionY = goal, + }, + mapValuesToProps = function(values) + return { + Position = UDim2.new(0, 0, 0, values.positionY), + } + end, + }) + Whenever goal changes, the frame will animate toward the new position. +]] + +local Packages = script.Parent.Parent.Parent + +local Otter = require(Packages.Otter) +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local PropTypes = t.intersection( + t.strictInterface({ + -- Values in this table will be animated with Otter. + animatedValues = t.table, + + -- This function describes how the animated values should be converted to + -- Roblox properties. + mapValuesToProps = t.callback, + + -- Options to pass to Otter's spring configuration + springOptions = t.optional(t.table), + + -- Called when the animation is complete + onComplete = t.optional(t.callback), + + -- Props to pass to the inner component. + regularProps = t.table, + + -- Children passed in by Roact + [Roact.Children] = t.optional(t.table), + }), + function(props) + if props[Roact.Children] ~= nil and props.regularProps[Roact.Children] ~= nil then + return false, "Children must be specified in one place, but the [Roact.Children] key was found" .. + " in both props and props.regularProps on SpringAnimatedItem." + end + + return true + end +) + +local SpringAnimatedItem = {} + +function SpringAnimatedItem.wrap(component) + local AnimatedComponent = Roact.PureComponent:extend(string.format("SpringAnimatedItem(%s)", + tostring(component))) + + AnimatedComponent.defaultProps = { + regularProps = {}, + } + + function AnimatedComponent:init() + self.ref = self.props.regularProps[Roact.Ref] or Roact.createRef() + + self.applyAnimatedValues = function(values) + local rbx = self.ref.current + if rbx == nil then + return + end + + local mapValuesToProps = self.props.mapValuesToProps + + local rbxProps = mapValuesToProps(values) + for key, value in pairs(rbxProps) do + rbx[key] = value + end + end + + self.onComplete = function() + if self.props.onComplete then + self.props.onComplete() + end + end + + self.motor = nil + end + + function AnimatedComponent:didMount() + local animatedValues = self.props.animatedValues + + -- Apply initial values + self.applyAnimatedValues(animatedValues) + + -- Set up motor + self.motor = Otter.createGroupMotor(animatedValues) + self.motor:onStep(function(newValues) + self.applyAnimatedValues(newValues) + end) + self.motor:onComplete(self.onComplete) + self.motor:start() + end + + function AnimatedComponent:willUpdate(newProps) + if self.props.regularProps[Roact.Ref] ~= newProps.regularProps[Roact.Ref] and + newProps.regularProps[Roact.Ref] ~= nil then + self.ref = newProps.regularProps[Roact.Ref] + end + end + + function AnimatedComponent:render() + assert(PropTypes(self.props)) + + local regularProps = self.props.regularProps + local props = Cryo.Dictionary.join(regularProps, { + [Roact.Ref] = self.ref, + [Roact.Children] = self.props[Roact.Children], + }) + + return Roact.createElement(component, props) + end + + function AnimatedComponent:didUpdate(oldProps) + -- If the animatedValues changed, set new goals for the motor so they animate. + if self.props.animatedValues ~= oldProps.animatedValues then + local goals = {} + for key, newValue in pairs(self.props.animatedValues) do + local springOptions = self.props.springOptions -- nil means default + goals[key] = Otter.spring(newValue, springOptions) + end + self.motor:setGoal(goals) + end + end + + function AnimatedComponent:willUnmount() + self.motor:destroy() + self.motor = nil + end + + return AnimatedComponent +end + +SpringAnimatedItem.AnimatedFrame = SpringAnimatedItem.wrap("Frame") +SpringAnimatedItem.AnimatedScrollingFrame = SpringAnimatedItem.wrap("ScrollingFrame") +SpringAnimatedItem.AnimatedImageLabel = SpringAnimatedItem.wrap("ImageLabel") +SpringAnimatedItem.AnimatedTextButton = SpringAnimatedItem.wrap("TextButton") +SpringAnimatedItem.AnimatedUIScale = SpringAnimatedItem.wrap("UIScale") + +return SpringAnimatedItem diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.spec.lua new file mode 100644 index 0000000..87bdd72 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.spec.lua @@ -0,0 +1,181 @@ +return function() + local SpringAnimatedItem = require(script.Parent.SpringAnimatedItem) + local Packages = script.Parent.Parent.Parent + local Roact = require(Packages.Roact) + + describe("SpringAnimatedItem", function() + local function testAnimatedComponent(component) + local element = Roact.createElement(component, { + animatedValues = { + positionY = 100, + }, + mapValuesToProps = function(values) + return { + Position = UDim2.new(0, 0, 0, values.positionY), + } + end, + }) + + local instance = Roact.mount(element) + + Roact.update(instance, Roact.createElement(component, { + animatedValues = { + positionY = 200, + }, + mapValuesToProps = function(values) + return { + Position = UDim2.new(0, 0, 0, values.positionY), + } + end, + })) + + Roact.unmount(instance) + end + + local function testAnimatedUIScale() + local element = Roact.createElement(SpringAnimatedItem.AnimatedUIScale, { + animatedValues = { + Scale = 0.8, + }, + mapValuesToProps = function(values) + return { + Scale = UDim2.new(0, 0, 0, values.Scale), + } + end, + }) + + local instance = Roact.mount(element) + + Roact.update(instance, Roact.createElement(SpringAnimatedItem.AnimatedUIScale, { + animatedValues = { + Scale = 1, + }, + mapValuesToProps = function(values) + return { + Scale = UDim2.new(0, 0, 0, values.Scale), + } + end, + })) + + Roact.unmount(instance) + end + + it("should throw if prop types are not correct", function() + local function testShouldThrow(props) + expect(function() + local element = Roact.createElement(SpringAnimatedItem.AnimatedFrame, props) + + Roact.mount(element) + end).to.throw() + end + + testShouldThrow({ + animatedValues = 1, + }) + + testShouldThrow({ + animatedValues = {}, + }) + + testShouldThrow({ + animatedValues = {}, + mapValuesToProps = "string", + }) + + testShouldThrow({ + animatedValues = {}, + mapValuesToProps = function() end, + springOptions = "string", + }) + + testShouldThrow({ + animatedValues = {}, + mapValuesToProps = function() end, + onComplete = "string", + }) + + testShouldThrow({ + animatedValues = {}, + mapValuesToProps = function() end, + regularProps = "string", + }) + end) + + it("should create/destroy/update pre-made components without errors", function() + testAnimatedComponent(SpringAnimatedItem.AnimatedFrame) + testAnimatedComponent(SpringAnimatedItem.AnimatedImageLabel) + testAnimatedComponent(SpringAnimatedItem.AnimatedTextButton) + testAnimatedUIScale() + end) + + it(".wrap() should generate a component successfully", function() + local AnimatedImageButton = SpringAnimatedItem.wrap("ImageButton") + + testAnimatedComponent(AnimatedImageButton) + end) + + it("should support children in the regularProps table", function() + local container = Instance.new("Folder") + local element = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + animatedValues = {}, + mapValuesToProps = function() + return {} + end, + regularProps = { + [Roact.Children] = { + Foo = Roact.createElement("StringValue"), + }, + }, + }) + + local tree = Roact.mount(element, container) + local instance = container:GetChildren()[1] + expect(instance.ClassName).to.equal("Frame") + + local child = instance:FindFirstChild("Foo") + expect(child).to.be.ok() + expect(child.ClassName).to.equal("StringValue") + Roact.unmount(tree) + end) + + it("should support children in the top-level table", function() + local container = Instance.new("Folder") + local element = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + animatedValues = {}, + mapValuesToProps = function() + return {} + end, + }, { + Foo = Roact.createElement("StringValue"), + }) + + local tree = Roact.mount(element, container) + local instance = container:GetChildren()[1] + expect(instance.ClassName).to.equal("Frame") + + local child = instance:FindFirstChild("Foo") + expect(child).to.be.ok() + expect(child.ClassName).to.equal("StringValue") + Roact.unmount(tree) + end) + + it("should throw if children are specified in multiple ways", function() + local element = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + animatedValues = {}, + mapValuesToProps = function() + return {} + end, + regularProps = { + [Roact.Children] = { + Bar = Roact.createElement("IntValue"), + } + }, + }, { + Foo = Roact.createElement("StringValue"), + }) + + local success = pcall(Roact.mount, element) + assert(not success, "Roact.mount should have thrown an error, but it did not") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.story.lua new file mode 100644 index 0000000..1901d73 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.story.lua @@ -0,0 +1,58 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Roact = require(ReplicatedStorage.Packages.Roact) + +local SpringAnimatedItem = require(script.Parent.SpringAnimatedItem) + +local TestButton = Roact.PureComponent:extend("TestButton") + +function TestButton:init() + self.state = { + activated = false, + } + + self.ref = Roact.createRef() +end + +function TestButton:render() + local activated = self.state.activated + + return Roact.createElement(SpringAnimatedItem.AnimatedTextButton, { + animatedValues = { + backgroundTransparency = activated and 0 or 0.5, + height = activated and 200 or 100, + positionOffsetY = activated and 200 or 0, + }, + mapValuesToProps = function(values) + return { + BackgroundTransparency = values.backgroundTransparency, + Size = UDim2.new(0, 200, 0, values.height), + Position = UDim2.new(0, 0, 0, values.positionOffsetY), + } + end, + regularProps = { + Text = "SpringAnimatedItem", + Size = UDim2.new(0, 200, 0, 0), + BackgroundColor3 = Color3.fromRGB(2, 183, 87), + AutoButtonColor = false, + [Roact.Event.Activated] = function() + self:setState({ + activated = not self.state.activated, + }) + + -- Change the text here, to verify that ref forwarding works properly. + self.ref.current.Text = self.state.activated + and "SpringAnimatedItem - Activated" or "SpringAnimatedItem" + end, + [Roact.Ref] = self.ref, + }, + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(TestButton), target, "SpringAnimatedItem") + + return function() + Roact.unmount(handle) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/createSignal.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/createSignal.lua new file mode 100644 index 0000000..029adc1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/createSignal.lua @@ -0,0 +1,75 @@ +--[[ + This is a simple signal implementation that has a dead-simple API. + + local signal = createSignal() + + local disconnect = signal:subscribe(function(foo) + print("Cool foo:", foo) + end) + + signal:fire("something") + + disconnect() +]] + +local function addToMap(map, addKey, addValue) + local new = {} + + for key, value in pairs(map) do + new[key] = value + end + + new[addKey] = addValue + + return new +end + +local function removeFromMap(map, removeKey) + local new = {} + + for key, value in pairs(map) do + if key ~= removeKey then + new[key] = value + end + end + + return new +end + +local function createSignal() + local connections = {} + + local function subscribe(_, callback) + assert(typeof(callback) == "function", "Can only subscribe to signals with a function.") + + local connection = { + callback = callback, + } + + connections = addToMap(connections, callback, connection) + + local function disconnect() + assert(not connection.disconnected, "Listeners can only be disconnected once.") + + connection.disconnected = true + connections = removeFromMap(connections, callback) + end + + return disconnect + end + + local function fire(_, ...) + for callback, connection in pairs(connections) do + if not connection.disconnected then + callback(...) + end + end + end + + return { + subscribe = subscribe, + fire = fire, + } +end + +return createSignal \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/createSignal.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/createSignal.spec.lua new file mode 100644 index 0000000..4fd50e0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/createSignal.spec.lua @@ -0,0 +1,16 @@ +return function() + local createSignal = require(script.Parent.createSignal) + + it("should connect and fire signals without errors", function() + local signal = createSignal() + + local testValue = "Some test value." + local disconnect = signal:subscribe(function(newValues) + expect(newValues).to.equal(testValue) + end) + + signal:fire(testValue) + disconnect() + end) + +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/divideTransparency.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/divideTransparency.lua new file mode 100644 index 0000000..066f6bf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/divideTransparency.lua @@ -0,0 +1,9 @@ +--[[ + Divides a transparency value by a value, as if it were opacity. + divideTransparency(0, 2) -> 0.5 + divideTransparency(0.3, 2) -> 0.65 +]] + +return function(transparency, divisor) + return 1 - (1 - transparency) / divisor +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/divideTransparency.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/divideTransparency.spec.lua new file mode 100644 index 0000000..c684f08 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/divideTransparency.spec.lua @@ -0,0 +1,9 @@ +return function() + local divideTransparency = require(script.Parent.divideTransparency) + + it("should divide transparency", function() + expect(divideTransparency(0, 2)).to.equal(0.5) + expect(divideTransparency(0.5, 2)).to.equal(0.75) + expect(divideTransparency(0, 4)).to.equal(0.75) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/enumValidator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/enumValidator.lua new file mode 100644 index 0000000..cca3d45 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/enumValidator.lua @@ -0,0 +1,18 @@ +-- Validator for Roblox enums + +local UtilityRoot = script.Parent +local UIBloxRoot = UtilityRoot.Parent + +local t = require(UIBloxRoot.Parent.t) + +local function enumValidator(enum) + local validators = {} + + for _, enumItem in pairs(enum) do + validators[#validators + 1] = t.literal(enumItem) + end + + return t.union(unpack(validators)) +end + +return enumValidator \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/enumerateValidator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/enumerateValidator.lua new file mode 100644 index 0000000..c7fcc91 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/enumerateValidator.lua @@ -0,0 +1,7 @@ +-- Validator for custom enums created by enumerate + +return function(enum) + return function(value) + return enum.isEnumValue(value) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/isPositiveVector2.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/isPositiveVector2.lua new file mode 100644 index 0000000..cb26583 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/isPositiveVector2.lua @@ -0,0 +1,18 @@ +local UtilityRoot = script.Parent +local UIBloxRoot = UtilityRoot.Parent + +local t = require(UIBloxRoot.Parent.t) + +local positiveVector2 = t.intersection( + t.Vector2, + function(value) + if value.X < 0 or value.Y < 0 then + return false, + ("each component of the Vector2 must be >= 0; component values are: %d, %d"):format(value.X, value.Y) + end + + return true + end +) + +return positiveVector2 \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/isPositiveVector2.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/isPositiveVector2.spec.lua new file mode 100644 index 0000000..d660ba7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/isPositiveVector2.spec.lua @@ -0,0 +1,17 @@ +local isPositiveVector2 = require(script.Parent.isPositiveVector2) + +return function() + it("should return false for non-Vector2s", function() + expect(isPositiveVector2(0)).to.equal(false) + end) + + it("should return false for Vector2s with a negative component", function() + expect(isPositiveVector2(Vector2.new(-100, 100))).to.equal(false) + expect(isPositiveVector2(Vector2.new(100, -100))).to.equal(false) + expect(isPositiveVector2(Vector2.new(-100, -100))).to.equal(false) + end) + + it("should return true for Vector2s with positive components", function() + assert(isPositiveVector2(Vector2.new(100, 100))) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/lerp.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/lerp.lua new file mode 100644 index 0000000..47b3ff0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/lerp.lua @@ -0,0 +1,3 @@ +return function(a, b, alpha) + return (1 - alpha) * a + b * alpha +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/lerp.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/lerp.spec.lua new file mode 100644 index 0000000..b2e9b0d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/lerp.spec.lua @@ -0,0 +1,10 @@ +return function() + local lerp = require(script.Parent.lerp) + + it("should linearly interpolate numbers", function() + expect(lerp(0, 4, 0)).to.equal(0) + expect(lerp(0, 4, 1)).to.equal(4) + expect(lerp(0, 4, 0.5)).to.equal(2) + expect(lerp(0, 4, 0.75)).to.equal(3) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/mockStyleComponent.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/mockStyleComponent.lua new file mode 100644 index 0000000..3c08c6c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/mockStyleComponent.lua @@ -0,0 +1,18 @@ +local Component = script.Parent +local UIBlox = Component.Parent +local Roact = require(UIBlox.Parent.Roact) + +local AppStyleProvider = require(UIBlox.App.Style.AppStyleProvider) + +return function(elements) + return Roact.createElement(AppStyleProvider, { + style = { + themeName = "dark", + fontName = "gotham", + }, + }, { + Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, elements) + }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/strict.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/strict.lua new file mode 100644 index 0000000..c1d21a5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/strict.lua @@ -0,0 +1,27 @@ +local function strict(t, name) + name = name or tostring(t) + + return setmetatable(t, { + __index = function(self, key) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + + __newindex = function(self, key, value) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + }) +end + +return strict \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/strict.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/strict.spec.lua new file mode 100644 index 0000000..fc44bff --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/strict.spec.lua @@ -0,0 +1,25 @@ +return function() + local strict = require(script.Parent.strict) + + it("should error when getting a nonexistent key", function() + local t = strict({ + a = 1, + b = 2, + }) + + expect(function() + return t.c + end).to.throw() + end) + + it("should error when setting a nonexistent key", function() + local t = strict({ + a = 1, + b = 2, + }) + + expect(function() + t.c = 3 + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/init.lua new file mode 100644 index 0000000..6e3a6e0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/init.lua @@ -0,0 +1,267 @@ +local makeConfigurable = require(script.Core.Config.makeConfigurable) +local UIBloxDefaultConfig = require(script.UIBloxDefaultConfig) +local Packages = script.Parent + +local function initializeLibrary() + local strict = require(script.Utility.strict) + + local UIBlox = {} + + UIBlox.Core = strict({ + Animation = strict({ + SpringAnimatedItem = require(script.Utility.SpringAnimatedItem), + }), + + Bar = strict({ + ThreeSection = require(script.Core.Bar.ThreeSectionBar), + }), + + ImageSet = strict({ + Button = require(script.Core.ImageSet.ImageSetComponent).Button, + Label = require(script.Core.ImageSet.ImageSetComponent).Label, + Validator = strict({ + validateImage = require(script.Core.ImageSet.Validator.validateImage), + }), + }), + + Control = strict({ + Enum = strict({ + ControlState = require(script.Core.Control.Enum.ControlState) + }), + Interactable = require(script.Core.Control.Interactable), + }), + + Style = strict({ + Validator = strict({ + validateFontInfo = require(script.Core.Style.Validator.validateFontInfo), + validateColorInfo = require(script.Core.Style.Validator.validateColorInfo), + }), + Palette = require(script.Core.Style.Symbol.Palette), + Provider = require(script.Core.Style.StyleProvider), + withStyle = require(script.Core.Style.withStyle), + }), + + Text = strict({ + ExpandableText = strict({ + GetCanExpand = require(script.Core.Text.ExpandableText.ExpandableTextUtils).getCanExpand + }), + }), + + InfiniteScroller = strict(require(Packages.InfiniteScroller)), + }) + + UIBlox.App = strict({ + Context = strict({ + ContentProvider = require(script.App.Context.ContentProvider) + }), + + ImageSet = strict({ + Images = require(script.App.ImageSet.Images), + getIconSize = require(script.App.ImageSet.getIconSize), + getIconSizeUDim2 = require(script.App.ImageSet.getIconSizeUDim2), + Enum = strict({ + IconSize = require(script.App.ImageSet.Enum.IconSize) + }), + }), + + Accordion = strict({ + AccordionView = require(script.App.Accordion.AccordionView), + }), + + Bar = strict({ + HeaderBar = require(script.App.Bar.HeaderBar), + RootHeaderBar = require(script.App.Bar.RootHeaderBar), + FullscreenTitleBar = require(script.App.Bar.FullscreenTitleBar), + }), + + Button = strict({ + Enum = strict({ + ButtonType = require(script.App.Button.Enum.ButtonType), + }), + PrimaryContextualButton = require(script.App.Button.PrimaryContextualButton), + PrimarySystemButton = require(script.App.Button.PrimarySystemButton), + SecondaryButton = require(script.App.Button.SecondaryButton), + AlertButton = require(script.App.Button.AlertButton), + ButtonStack = require(script.App.Button.ButtonStack), + TextButton = require(script.App.Button.TextButton), + IconButton = require(script.App.Button.IconButton), + }), + + Cell = strict({ + Small = strict({ + SelectionGroup = strict({ + SmallRadioButtonGroup = require(script.App.Cell.Small.SelectionGroup.SmallRadioButtonGroup), + }) + }) + }), + + Text = strict({ + ExpandableTextArea = require(script.App.Text.ExpandableTextArea.ExpandableTextArea), + }), + + Loading = strict({ + Enum = strict({ + RetrievalStatus = require(script.App.Loading.Enum.RetrievalStatus), + LoadingState = require(script.App.Loading.Enum.LoadingState), + RenderOnFailedStyle = require(script.App.Loading.Enum.RenderOnFailedStyle), + ReloadingStyle = require(script.App.Loading.Enum.ReloadingStyle), + }), + LoadableImage = require(script.App.Loading.LoadableImage), + ShimmerPanel = require(script.App.Loading.ShimmerPanel), + LoadingSpinner = require(script.App.Loading.LoadingSpinner), + }), + + InputButton = strict({ + RadioButtonList = require(script.App.InputButton.RadioButtonList), + CheckboxList = require(script.App.InputButton.CheckboxList), + Checkbox = require(script.App.InputButton.Checkbox), + Toggle = require(script.App.InputButton.Toggle), + }), + + Container = strict({ + Carousel = strict({ + FreeFlowCarousel = require(script.App.Container.Carousel.FreeFlowCarousel) + }), + VerticalScrollView = require(script.App.Container.VerticalScrollView), + getPageMargin = require(script.App.Container.getPageMargin), + LoadingStateContainer = require(script.App.Container.LoadingStateContainer), + }), + + Slider = strict({ + ContextualSlider = require(script.App.Slider.ContextualSlider), + SystemSlider = require(script.App.Slider.SystemSlider), + TwoKnobSystemSlider = require(script.App.Slider.TwoKnobSystemSlider), + TwoKnobContextualSlider = require(script.App.Slider.TwoKnobContextualSlider), + }), + + Grid = strict({ + GridView = require(script.App.Grid.GridView), + GridMetrics = require(script.App.Grid.GridMetrics), + DefaultMetricsGridView = require(script.App.Grid.DefaultMetricsGridView), + ScrollingGridView = require(script.App.Grid.ScrollingGridView), + }), + + Pill = strict({ + SmallPill = require(script.App.Pill.SmallPill), + LargePill = require(script.App.Pill.LargePill), + }), + + Tile = strict({ + Enum = strict({ + ItemTileEnums = require(script.App.Tile.Enum.ItemTileEnums), + }), + SaveTile = require(script.App.Tile.SaveTile.SaveTile), + ItemTile = require(script.App.Tile.ItemTile.ItemTile), + ItemTileFooter = require(script.App.Tile.ItemTile.ItemTileFooter), + MenuTile = require(script.App.Tile.MenuTile.MenuTile), + }), + + Dialog = strict({ + Modal = strict({ + -- TEMPORARY WORK! This should not be available yet. Please contact Eric Sauer for more info + --FullPageModal = require(script.App.Dialog.Modal.FullPageModal), + PartialPageModal = require(script.App.Dialog.Modal.PartialPageModal), + EducationalModal = require(script.App.Dialog.Modal.EducationalModal), + }), + Alert = strict({ + InformativeAlert = require(script.App.Dialog.Alert.InformativeAlert), + InteractiveAlert = require(script.App.Dialog.Alert.InteractiveAlert), + LoadingAlert = require(script.App.Dialog.Alert.LoadingAlert), + }), + Enum = strict({ + AlertType = require(script.App.Dialog.Alert.Enum.AlertType), + TooltipOrientation = require(script.App.Dialog.Tooltip.Enum.TooltipOrientation), + }), + Toast = require(script.App.Dialog.Toast.SlideFromTopToast), + Tooltip = require(script.App.Dialog.Tooltip.Tooltip), + }), + + Constant = strict({ + -- DEPRECATED: use App.ImageSet.getIconSize to get the size + IconSize = require(script.App.Constant.IconSize), + }), + + Style = strict({ + Validator = strict({ + validateFont = require(script.App.Style.Validator.validateFont), + validateTheme = require(script.App.Style.Validator.validateTheme), + validateStyle = require(script.App.Style.Validator.validateStyle), + }), + AppStyleProvider = require(script.App.Style.AppStyleProvider), + Colors = require(script.App.Style.Colors), + Constants = require(script.App.Style.Constants), + }), + + Indicator = strict({ + Badge = require(script.App.Indicator.Badge), + EmptyState = require(script.App.Indicator.EmptyState), + }), + + Menu = strict({ + BaseMenu = require(script.App.Menu.BaseMenu), + OverlayBaseMenu = require(script.App.Menu.OverlayBaseMenu), + + ContextualMenu = require(script.App.Menu.ContextualMenu), + OverlayContextualMenu = require(script.App.Menu.OverlayContextualMenu), + + MenuDirection = require(script.App.Menu.MenuDirection), + + DropdownMenu = require(script.App.Menu.DropdownMenu), + + }), + + SelectionImage = strict({ + SelectionCursorProvider = require(script.App.SelectionImage.SelectionCursorProvider), + CursorKind = require(script.App.SelectionImage.CursorKind), + withSelectionCursorProvider = require(script.App.SelectionImage.withSelectionCursorProvider), + }) + }) + + -- DEPRECATED SECTION + + -- DEPRECATED: Use Core.Style instead + UIBlox.Style = { + Provider = require(script.Style.StyleProvider), + withStyle = require(script.Style.withStyle), + Validator = { + validateStyle = require(script.Style.Validator.validateStyle), + validateFont = require(script.Style.Validator.validateFont), + validateFontInfo = require(script.Style.Validator.validateFontInfo), + validateTheme = require(script.Style.Validator.validateTheme), + validateColorInfo = require(script.Style.Validator.validateColorInfo), + }, + } + + -- DEPRECATED: This is kept for compatibility. Use App.Accordion.AccordionView instead. + UIBlox.AccordionView = require(script.App.Accordion.AccordionView) + + -- DEPRECATED: This is kept for compatibility. This should not be used because it is an old design. + -- Use ContextualMenu instead + UIBlox.ModalBottomSheet = require(script.ModalBottomSheet.ModalBottomSheet) + + -- DEPRECATED: This is kept for compatibility. + UIBlox.Utility = { + ExternalEventConnection = require(script.Utility.ExternalEventConnection), + --Use Core.Animation.SpringAnimatedItem instead + SpringAnimatedItem = require(script.Utility.SpringAnimatedItem), + } + + -- DEPRECATED: use Core.Loading instead. + UIBlox.Loading = { + LoadableImage = require(script.App.Loading.LoadableImage), + ShimmerPanel = require(script.App.Loading.ShimmerPanel), + } + + -- DEPRECATED: use App.Tile instead. + UIBlox.Tile = { + SaveTile = require(script.App.Tile.SaveTile.SaveTile), + ItemTile = require(script.App.Tile.ItemTile.ItemTile), + ItemTileEnums = require(script.App.Tile.Enum.ItemTileEnums), + } + + -- END DEPRECATED SECTION + + return UIBlox +end + +return makeConfigurable(initializeLibrary, "UIBlox", UIBloxDefaultConfig) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/enumerate.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/enumerate.lua new file mode 100644 index 0000000..780f356 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/enumerate.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_enumerate"]["enumerate"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/lock.toml new file mode 100644 index 0000000..44789dc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/lock.toml @@ -0,0 +1,15 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "UIBlox" +version = "0.1.1" +commit = "a253d52373c4ce1611090c04ae2a02994274f1f2" +source = "git+https://github.com/roblox/uiblox#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "InfiniteScroller roblox/infinite-scroller 0.5.6 url+https://github.com/roblox/infinite-scroller", + "Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "RoactGamepad roblox/roact-gamepad 0.4.4 url+https://github.com/roblox/roact-gamepad", + "enumerate roblox/enumerate 1.0.0 url+https://github.com/roblox/enumerate", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/t.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/UIBlox/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/Roact.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/Rodux.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/Rodux.lua new file mode 100644 index 0000000..96b67df --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/Rodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_rodux"]["rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/UIBlox.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/UIBlox.lua new file mode 100644 index 0000000..2e0dcc2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/UIBlox.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["UIBlox"]["UIBlox"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/asset-card/asset-card/Components/AssetCard.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/asset-card/asset-card/Components/AssetCard.lua new file mode 100644 index 0000000..9e0bc82 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/asset-card/asset-card/Components/AssetCard.lua @@ -0,0 +1,103 @@ +local TextService = game:GetService("TextService") + +local root = script.Parent.Parent +local Packages = root.Parent + +local UIBlox = require(Packages.UIBlox) +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local LoadableImage = UIBlox.App.Loading.LoadableImage + +local IMAGE_SIZE = 128 +local FOOTER_HEIGHT = 20 + +local MAX_TEXT_BOUND = 10000 +local TEXT_LINE_HEIGHT = 20 +local TEXT_PADDING = 12 +local TEXT_TOP_PADDING = 8 + +local AssetCard = Roact.PureComponent:extend("AssetCard") + +local validateProps = t.interface({ + image = t.string, + imageTransparency = t.number, + textColor3 = t.Color3, + textTransparency = t.number, + text = t.string, + font = t.enum(Enum.Font), + onActivated = t.callback, +}) + +AssetCard.defaultProps = { + image = "", + imageTransparency = 0, + textColor3 = Color3.fromRGB(242, 244, 245), + textTransparency = 0, + text = "tucker was here", + font = Enum.Font.Gotham, + onActivated = function() end, +} + +function AssetCard:render() + local props = self.props + assert(validateProps(props)) + + local text = props.text + local font = props.font + + local textLabelWidth = IMAGE_SIZE - (TEXT_PADDING*2) + local textWidth = TextService:GetTextSize(text, 16, font, Vector2.new(MAX_TEXT_BOUND, MAX_TEXT_BOUND)).X + + local lines = 1 + + if textWidth > textLabelWidth then + lines = 2 + end + + local textFooterSize = FOOTER_HEIGHT+(TEXT_LINE_HEIGHT*lines) + + return Roact.createElement("ImageButton", { + Size = UDim2.new(0, IMAGE_SIZE, 0, IMAGE_SIZE + textFooterSize), + BackgroundTransparency = 1, + [Roact.Event.Activated] = props.onActivated, + }, { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + }), + assetIcon = Roact.createElement(LoadableImage, { + Size = UDim2.new(0, IMAGE_SIZE, 0, IMAGE_SIZE), + BackgroundTransparency = 0.5, + LayoutOrder = 1, + Image = props.image, + ImageTransparency = props.imageTransparency, + useShimmerAnimationWhileLoading = true, + showFailedStateWhenLoadingFailed = true, + }), + nameContainer = Roact.createElement("Frame", { + Size = UDim2.new(0, IMAGE_SIZE, 0, textFooterSize), + BackgroundTransparency = 1, + LayoutOrder = 2, + }, { + padding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0,TEXT_TOP_PADDING), + PaddingBottom = UDim.new(0, TEXT_PADDING), + PaddingLeft = UDim.new(0, TEXT_PADDING), + PaddingRight = UDim.new(0,TEXT_PADDING), + }), + assetName = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Font = font, + TextTransparency = props.textTransparency, + TextColor3 = props.textColor3, + TextSize = 16, + TextTruncate = Enum.TextTruncate.AtEnd, + TextWrapped = true, + Text = text, + }) + }) + }) +end + +return AssetCard diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/asset-card/asset-card/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/asset-card/asset-card/init.lua new file mode 100644 index 0000000..ce59c8b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/asset-card/asset-card/init.lua @@ -0,0 +1,3 @@ +return { + AssetCard = require(script.Components.AssetCard), +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/lock.toml new file mode 100644 index 0000000..e944af0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/lock.toml @@ -0,0 +1,11 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "asset-card" +version = "1.0.1" +commit = "c7b683fb0c31888433127311ce45b691b9cf6086" +source = "git+https://github.com/roblox/asset-card#v1.0.2" +dependencies = [ + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "UIBlox UIBlox a253d523 git+https://github.com/roblox/uiblox#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/t.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/asset-card/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/List/List.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/List/List.lua new file mode 100644 index 0000000..177c311 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/List/List.lua @@ -0,0 +1,356 @@ +local Root = script.Parent.Parent +local Cryo = require(Root.Parent.Cryo) +local binarySearch = require(Root.binarySearch.binarySearch) + +local List = {} + +List.__index = List + +--[[ + Create a new List from a list of values +]] +function List.new(...) + local self = { + values = {}, + _immutableDataStructureType = List, + } + + setmetatable(self, List) + + List._insertInPlace(self, 1, ...) + + return self +end + +--[[ + Internal method that absorbs an existing list-style table as the underlying data. +]] +function List._newCannibalizeTable(tab) + local self = { + values = tab, + _immutableDataStructureType = List, + } + + setmetatable(self, List) + + return self +end + +--[[ + Check if a given value is a list. +]] +function List.is(object) + if type(object) ~= "table" then + return false + end + return object._immutableDataStructureType == List +end + +--[[ + Creates a new List from a list-style table. +]] +function List.newFromListTable(table) + return List.new(unpack(table)) +end + +--[[ + Returns the size of a List. +]] +function List:size() + return #self.values +end + +--[[ + Creates a (shallow) copy of a list. +]] +function List:copy() + return List.new(unpack(self.values)) +end + +--[[ + Returns the (read only) value at the index. +]] +function List:get(index) + assert(index >= 1 and index <= #self.values, "Index out of bounds!") + return self.values[index] +end + +--[[ + Creates a new List at which the value at index is changed to value. +]] +function List:set(index, value) + assert(value ~= nil, "Cannot set a value to nil. Use remove() to remove values") + assert(1 <= index and index <= #self.values, "Index out of bounds!") + local new = self:copy() + new.values[index] = value + return new +end + +--[[ + Expects (any number of) dictionaries of the form { [index] = value }. + Preferred for when multiple sets are needed repeatedly, + as this version does no intermediate copying. +]] +function List:batchSet(...) + local new = self:copy() + local length = select("#", ...) + local values = new.values + for i = 1, length do + local pair = select(i, ...) + for key, value in pairs(pair) do + assert(1 <= key and key <= #values) + values[key] = value + end + end + return new +end + +--[[ + Creates a new List where the element at index is deleted. +]] +function List:remove(index) + assert(index >= 1 and index <= #self.values, "Index out of bounds!") + local new = self:copy() + table.remove(new.values, index) + return new +end + +--[[ + Creates a new List where the elements are inserted at index. +]] +function List:insert(index, ...) + assert(index >= 1 and index <= #self.values + 1, "Index out of bounds!") + local new = self:copy() + new:_insertInPlace(index, ...) + return new +end + +--[[ + Push a value onto the end of the list. +]] +function List:pushBack(value) + return self:insert(#self.values + 1, value) +end + +--[[ + Push a value onto the front of the list. +]] +function List:pushFront(value) + return self:insert(1, value) +end + +--[[ + Pop a value from the back of the list. +]] +function List:popBack() + return self:remove(#self.values) +end + +--[[ + Pop a value from the front of the list. +]] +function List:popFront() + return self:remove(1) +end + +--[[ + Returns a (shallow) copy of the underlying table. +]] +function List:toTable() + local new = self:copy() + return new.values +end + +--[[ + Internal method that inserts a number of values in place. +]] +function List:_insertInPlace(index, ...) + local length = select("#", ...) + if length == 0 then + return + end + self:_shift(index, length) + local values = self.values + for i = 0, length - 1 do + values[i + index] = select(i + 1, ...) + end +end + +--[[ + Internal method that shifts over a number of values to make space for insertion. +]] +function List:_shift(index, numPlacesToShift) + local values = self.values + for i = #self.values, index, -1 do + values[i + numPlacesToShift] = values[i] + values[i] = nil + end +end + +--[[ + Create a copy of the List with only values for which `callback` returns true. + Calls the callback with (value, index). +]] +function List:filter(callback) + local new = Cryo.List.filter(self.values, callback) + return List._newCannibalizeTable(new) +end + +--[[ + Create a copy of the List doing a combination filter and map. + + If callback returns nil for any item, it is considered filtered from the + list. Any other value is considered the result of the 'map' operation. +]] +function List:filterMap(callback) + local new = Cryo.List.filterMap(self.values, callback) + return List._newCannibalizeTable(new) +end + +--[[ + Returns the index of the first value found or nil if not found. +]] +function List:find(value) + return Cryo.List.find(self.values, value) +end + +--[[ + Returns the index of the first value for which predicate(value, index) is truthy, or nil if not found. +]] +function List:findWhere(predicate) + return Cryo.List.findWhere(self.values, predicate) +end + +--[[ + Performs a left-fold of the List with the given initial value and callback. +]] +function List:foldLeft(callback, initialValue) + return Cryo.List.foldLeft(self.values, callback, initialValue) +end + +--[[ + Performs a right-fold of the List with the given initial value and callback. +]] +function List:foldRight(callback, initialValue) + return Cryo.List.foldRight(self.values, callback, initialValue) +end + +--[[ + Returns a new List containing only the elements within the given range. +]] +function List:getRange(startIndex, endIndex) + local new = Cryo.List.getRange(self.values, startIndex, endIndex) + return List._newCannibalizeTable(new) +end + +--[[ + Create a copy of the List where each value is transformed by `callback` +]] +function List:map(callback) + local new = Cryo.List.map(self.values, callback) + return List._newCannibalizeTable(new) +end + +--[[ + Joins any number of Lists together into a new List +]] +function List:join(...) + local otherLists = {} + local len = select("#", ...) + for i = 1, len do + otherLists[i] = select(i, ...).values + end + local new = Cryo.List.join(self.values, unpack(otherLists)) + return List._newCannibalizeTable(new) +end + +--[[ + Create a copy of the List with removing the range from the List starting from the index. +]] +function List:removeRange(startIndex, endIndex) + local new = Cryo.List.removeRange(self.values, startIndex, endIndex) + return List._newCannibalizeTable(new) +end + +--[[ + Creates a new List that has no occurrences of the given value. +]] +function List:removeValue(value) + local new = Cryo.List.removeValue(self.values, value) + return List._newCannibalizeTable(new) +end + +--[[ + Returns a new List with the reversed order of the given list +]] +function List:reverse() + local new = Cryo.List.reverse(self.values) + return List._newCannibalizeTable(new) +end + +--[[ + Returns a new List, ordered with the given sort callback. + If no callback is given, the default table.sort will be used. +]] +function List:sort(callback) + local new = Cryo.List.sort(self.values, callback) + return List._newCannibalizeTable(new) +end + +--[[ + Returns an iterator that traverses the List in a forward direction. + e.g. + for index, value in list:iterator() do + ... + end +]] +function List:iterator() + local i = 0 + local length = #self.values + + return function() + i = i + 1 + if i <= length then + return i, self.values[i] + end + end +end + +--[[ + Returns an iterator that traverses the List in a backward direction. + e.g. + for index, value in list:reverseIterator() do + ... + end +]] +function List:reverseIterator() + local i = #self.values + 1 + + return function() + i = i - 1 + if i > 0 then + return i, self.values[i] + end + end +end + +--[[ + Does a binarySearch in the List, assuming that it is sorted. + Returns the leftmost index of the occurence of value according to the given comaprator, if provided, + otherwise <. If not found, returns nil. +]] +function List:binarySearch(value, comparator) + return binarySearch(self.values, value, comparator) +end + +--[[ + Potential TODO: + Add O(n) methods for sorted Lists (e.g. setIntersection, setUnion, etc.) +]] + +--[[ + Specify the behavior of deepJoin for OrderedMap. +]] +List.deepJoin = List.join + +return List \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/List/List.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/List/List.spec.lua new file mode 100644 index 0000000..24153d2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/List/List.spec.lua @@ -0,0 +1,559 @@ +return function() + local List = require(script.Parent.List) + describe("Basic list operations", function() + + it("Creates a new empty list", function() + local list = List.new() + expect(list).to.be.ok() + expect(list:size()).to.equal(0) + local tab = list:toTable() + expect(next(tab)).to.never.be.ok() + end) + + it("Can tell apart Lists from non-Lists", function() + local nonlist = {} + expect(List.is(nonlist)).to.equal(false) + local number = 5 + expect(List.is(number)).to.equal(false) + local list = List.new() + expect(List.is(list)).to.equal(true) + end) + + it("Creates a table with some values", function() + local list = List.new(3, 4, 5) + expect(list).to.be.ok() + expect(list:size()).to.equal(3) + expect(list:get(1)).to.equal(3) + expect(list:get(2)).to.equal(4) + expect(list:get(3)).to.equal(5) + end) + + it("Creates a table with some values from table", function() + local list = List.newFromListTable({ 3, 4, 5 }) + expect(list).to.be.ok() + expect(list:size()).to.equal(3) + expect(list:get(1)).to.equal(3) + expect(list:get(2)).to.equal(4) + expect(list:get(3)).to.equal(5) + end) + + it("Can create a copy", function() + local list = List.new(3, 4, 5) + local listCopy = list:copy() + + expect(listCopy).never.to.equal(list) + expect(list:get(1)).to.equal(3) + expect(list:get(2)).to.equal(4) + expect(list:get(3)).to.equal(5) + expect(listCopy:size()).to.equal(3) + end) + + it("Can be converted immutably to a table", function() + local list = List.new(3, 4, 5) + local tab = list:toTable() + tab[2] = 50 + expect(list:get(1)).to.equal(3) + expect(list:get(2)).to.equal(4) + expect(list:get(3)).to.equal(5) + end) + + it("Supports immutable set", function() + local list = List.new(3, 4, 5) + local newList = list:set(2, 1000) + + expect(newList).never.to.equal(list) + expect(list:get(2)).to.equal(4) + expect(newList:get(2)).to.equal(1000) + end) + + it("Supports deletion", function() + local list = List.new(3, 4, 5, 6, 7) + local newList = list:remove(1) + + expect(newList).never.to.equal(list) + expect(list:get(1)).to.equal(3) + expect(newList:get(1)).to.equal(4) + expect(newList:size()).to.equal(4) + + local newNewList = newList:remove(2) + expect(newNewList).never.to.equal(newList) + expect(newList:get(2)).to.equal(5) + expect(newNewList:get(2)).to.equal(6) + expect(newNewList:size()).to.equal(3) + end) + + it("Supports insertion", function() + local list = List.new() + local newList = list:insert(1, 4) + + expect(newList).never.to.equal(list) + expect(newList:get(1)).to.equal(4) + expect(newList:size()).to.equal(1) + + local anotherList = List.new(3, 4, 5, 6, 7) + local newAnotherList = anotherList:insert(2, 100, 101) + + expect(newAnotherList:get(2)).to.equal(100) + expect(newAnotherList:get(3)).to.equal(101) + expect(newAnotherList:get(4)).to.equal(4) + expect(newAnotherList:size()).to.equal(7) + end) + + it("Supports push/pop at the back", function() + local list = List.new(3, 4, 5, 6, 7) + local newList = list:pushBack(100) + expect(newList:size()).to.equal(6) + expect(newList:get(newList:size())).to.equal(100) + + local newNewList = list:popBack() + expect(newNewList:size()).to.equal(4) + expect(newNewList:get(newNewList:size())).to.equal(6) + + local empty = List.new() + local nonempty = empty:pushBack(10) + expect(nonempty:size()).to.equal(1) + expect(nonempty:get(nonempty:size())).to.equal(10) + end) + + it("Supports push/pop at the front", function() + local list = List.new(3, 4, 5, 6, 7) + local newList = list:pushFront(100) + expect(newList:size()).to.equal(6) + expect(newList:get(1)).to.equal(100) + + local newNewList = list:popFront() + expect(newNewList:size()).to.equal(4) + expect(newNewList:get(1)).to.equal(4) + + local empty = List.new() + local nonempty = empty:pushFront(10) + expect(nonempty:size()).to.equal(1) + expect(nonempty:get(1)).to.equal(10) + end) + + it("Supports batch setting", function() + local list = List.new(2, 3, 4, 5, 6, 7) + local newList = list:batchSet({ + [2] = 100, + [4] = 1000, + }) + + expect(newList).never.to.equal(list) + expect(newList:get(1)).to.equal(2) + expect(newList:get(2)).to.equal(100) + expect(newList:get(4)).to.equal(1000) + expect(newList:size()).to.equal(6) + end) + end) + + describe("More advanced functionality", function() + describe("Filtering", function() + it("should use the callback", function() + local list = List.new(3, 4, 5, 6, 7) + local newList = list:filter(function(value, index) + return value % 2 == 0 + end) + + expect(newList).never.to.equal(list) + expect(newList:size()).to.equal(2) + expect(list:get(1)).to.equal(3) + expect(newList:get(1)).to.equal(4) + expect(list:get(2)).to.equal(4) + expect(newList:get(2)).to.equal(6) + end) + + it("should work with an empty List", function() + local called = false + local function callback() + called = true + return true + end + local list = List.new() + local newList = list:filter(callback) + expect(newList:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("FilterMapping", function() + it("should return a new table", function() + local list = List.new(1, 2, 3) + local function callback() + return 1 + end + local newList = list:filterMap(callback) + + expect(list).never.to.equal(newList) + end) + + it("should correctly use the filter callback", function() + local list = List.new(1, 2, 3, 4, 5) + local function doubleOddOnly(value) + if value % 2 == 0 then + return nil + else + return value * 2 + end + end + local newList = list:filterMap(doubleOddOnly) + + expect(newList:size()).to.equal(3) + expect(newList:get(1)).to.equal(2) + expect(newList:get(2)).to.equal(6) + expect(newList:get(3)).to.equal(10) + end) + + it("should work with an empty table", function() + local called = false + local function callback() + called = true + return true + end + local list = List.new() + + local newList = list:filterMap(callback) + expect(newList:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Joining", function() + it("Should work with two tables", function() + local first = List.new(1, 2, 3, 4, 5) + local second = List.new(100, 101, 102) + local newList = first:join(second) + expect(newList:get(1)).to.equal(1) + expect(newList:get(5)).to.equal(5) + expect(newList:get(6)).to.equal(100) + expect(newList:get(8)).to.equal(102) + expect(newList:size()).to.equal(8) + end) + + it("Should work with multiple tables", function() + local first = List.new(1, 2, 3, 4, 5) + local second = List.new(100, 101, 102) + local third = List.new(1000, 1001, 1002) + local newList = first:join(second, third) + expect(newList:get(1)).to.equal(1) + expect(newList:get(5)).to.equal(5) + expect(newList:get(6)).to.equal(100) + expect(newList:get(8)).to.equal(102) + expect(newList:get(9)).to.equal(1000) + expect(newList:get(11)).to.equal(1002) + expect(newList:size()).to.equal(11) + end) + end) + + describe("Iterators", function() + it("Should work in a for loop", function() + local list = List.new(3, 4, 5, 6, 7) + local count = 1 + for index, value in list:iterator() do + if count == 1 then + expect(value).to.equal(3) + elseif count == 5 then + expect(value).to.equal(7) + end + expect(value).to.equal(index + 2) + count = count + 1 + end + end) + + it("Should work for empty lists", function() + local list = List.new() + local wasAnythingDone = false + for _, _ in list:iterator() do + wasAnythingDone = true + end + expect(wasAnythingDone).to.equal(false) + end) + + it("Should work in reverse", function() + local list = List.new(3, 4, 5, 6, 7) + local count = 1 + for index, value in list:reverseIterator() do + if count == 5 then + expect(value).to.equal(3) + elseif count == 1 then + expect(value).to.equal(7) + end + expect(value).to.equal(index + 2) + count = count + 1 + end + end) + + describe("binarySearch", function() + it("Works for a sorted List", function() + local list = List.new(1, 3, 5, 6, 7) + expect(list:binarySearch(1)).to.equal(1) + expect(list:binarySearch(3)).to.equal(2) + expect(list:binarySearch(5)).to.equal(3) + expect(list:binarySearch(6)).to.equal(4) + expect(list:binarySearch(7)).to.equal(5) + expect(list:binarySearch(100)).to.never.be.ok() + end) + + it("Works for a sorted List with duplicates", function() + local list = List.new(1, 1, 3, 3, 3, 5, 5, 10, 11) + expect(list:binarySearch(1)).to.equal(1) + expect(list:binarySearch(3)).to.equal(3) + expect(list:binarySearch(5)).to.equal(6) + expect(list:binarySearch(10)).to.equal(8) + expect(list:binarySearch(11)).to.equal(9) + end) + + it("Works for an empty List", function() + local list = List.new() + expect(list:binarySearch(1)).to.never.be.ok() + end) + + it("Works for a special comparator", function() + local list = List.new(5, 6, 1, 5, 7, 3) + local reverse = function(lhs, rhs) + return rhs < lhs + end + local newList = list:sort(reverse) + expect(newList:binarySearch(7, reverse)).to.equal(1) + expect(newList:binarySearch(6, reverse)).to.equal(2) + expect(newList:binarySearch(5, reverse)).to.equal(3) + expect(newList:binarySearch(3, reverse)).to.equal(5) + expect(newList:binarySearch(1, reverse)).to.equal(6) + end) + end) + end) + + --[[ + TODO: port over more of these Cryo tests, if necessary. + ]] + describe("Cryo functions", function() + describe("find", function() + local list = List.new(5, 4, 3, 2, 1) + it("should return the correct index", function() + expect(list:find(1)).to.equal(5) + expect(list:find(2)).to.equal(4) + expect(list:find(3)).to.equal(3) + expect(list:find(4)).to.equal(2) + expect(list:find(5)).to.equal(1) + end) + + it("should work with an empty table", function() + local empty = List.new() + expect(empty:find(1)).to.never.be.ok() + end) + + it("should return nil when the given value is not found", function() + expect(list:find(1000)).to.never.be.ok() + end) + + it("should return the index of the first value found", function() + local repeated = List.new(1, 2, 2) + + expect(repeated:find(2)).to.equal(2) + end) + end) + + describe("findWhere", function() + it("should return the correct index", function() + local list = List.new(1, 5, 10, 7) + local isEven = function(value) + return value % 2 == 0 + end + + local isOdd = function(value) + return value % 2 == 1 + end + + expect(list:findWhere(isEven)).to.equal(3) + expect(list:findWhere(isOdd)).to.equal(1) + end) + + it("should work with an empty table", function() + local empty = List.new() + local anything = function() + return true + end + expect(empty:findWhere(anything)).to.never.be.ok() + end) + + it("should return nil when the when no value satisfies the predicate", function() + local numbers = List.new(1, 2, 3) + local isFour = function(value) + return value == 4 + end + + expect(numbers:findWhere(isFour)).to.never.be.ok() + end) + + it("should return the index of the first value for which the predicate is true", function() + local numbers = List.new(1, 1, 1, 2, 2) + + local isTwo = function(value) + return value == 2 + end + + expect(numbers:findWhere(isTwo)).to.equal(4) + end) + + it("should allow access to both value and index in the predicate function", function() + local numbers = List.new(1, 1, 2, 2, 1) + + local sumValueAndIndexToFive = function(value, index) + return value + index == 5 + end + + expect(numbers:findWhere(sumValueAndIndexToFive)).to.equal(3) + end) + end) + + describe("foldLeft", function() + it("should call the callback for each element", function() + local a = List.new(4, 5, 6) + local copy = {} + + a:foldLeft(function(accum, value, index) + copy[index] = value + return accum + end, 0) + + expect(#copy).to.equal(a:size()) + + for key, value in a:iterator() do + expect(value).to.equal(copy[key]) + end + end) + end) + + describe("foldRight", function() + it("should call the callback for each element", function() + local a = List.new(4, 5, 6) + local copy = {} + + a:foldRight(function(accum, value, index) + copy[index] = value + return accum + end, 0) + + expect(#copy).to.equal(a:size()) + + for key, value in a:iterator() do + expect(value).to.equal(copy[key]) + end + end) + end) + + describe("getRange", function() + it("should return the correct range", function() + local a = List.new(1, 2, 3, 4) + local b = a:getRange(2, 3) + + expect(b:get(1)).to.equal(2) + expect(b:get(2)).to.equal(3) + expect(b:size()).to.equal(2) + + local c = a:getRange(4, 4) + expect(c:size()).to.equal(1) + expect(c:get(1)).to.equal(4) + end) + end) + + describe("map", function() + it("should call the callback for each element", function() + local a = List.new(5, 6, 7) + local copy = {} + a:map(function(value, index) + copy[index] = value + return value + end) + + for key, value in a:iterator() do + expect(copy[key]).to.equal(value) + end + + for key, value in pairs(copy) do + expect(value).to.equal(a:get(key)) + end + end) + end) + + describe("removeRange", function() + it("should remove elements properly", function() + local a = List.new(1, 2, 3) + local b = a:removeRange(2, 2) + + expect(b:size()).to.equal(2) + expect(b:get(1)).to.equal(1) + expect(b:get(2)).to.equal(3) + + local c = List.new(1, 2, 3, 4, 5, 6) + local d = c:removeRange(1, 4) + + expect(d:size()).to.equal(2) + expect(d:get(1)).to.equal(5) + expect(d:get(2)).to.equal(6) + + local e = c:removeRange(2, 5) + + expect(e:size()).to.equal(2) + expect(e:get(1)).to.equal(1) + expect(e:get(2)).to.equal(6) + end) + end) + + describe("removeValue", function() + it("should remove all occurences of the same given value", function() + local a = List.new(1, 2, 2, 3) + local b = a:removeValue(2) + + expect(b:size()).to.equal(2) + expect(b:get(1)).to.equal(1) + expect(b:get(2)).to.equal(3) + end) + end) + + describe("reverse", function() + it("should reverse the list", function() + local a = List.new(1, 2, 3, 4) + local b = a:reverse() + + expect(b:get(1)).to.equal(4) + expect(b:get(2)).to.equal(3) + expect(b:get(3)).to.equal(2) + expect(b:get(4)).to.equal(1) + end) + end) + + describe("sort", function() + it("should sort with the default table.sort when no callback is given", function() + local a = List.new(4, 2, 5, 3, 1) + local b = a:sort() + + local aTable = a:toTable() + table.sort(aTable) + + expect(b:size()).to.equal(a:size()) + for i = 1, #aTable do + expect(b:get(i)).to.equal(aTable[i]) + end + end) + + it("should sort with the given callback", function() + local a = List.new(1, 2, 5, 3, 4) + local function order(first, second) + return first > second + end + local b = a:sort(order) + + local aTable = a:toTable() + + table.sort(aTable, order) + + expect(b:size()).to.equal(#aTable) + for i = 1, #a do + expect(b:get(i)).to.equal(aTable[i]) + end + end) + end) + end) + + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/None.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/None.lua new file mode 100644 index 0000000..2b461fa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/None.lua @@ -0,0 +1,3 @@ +-- Alias for Cryo.None +local Cryo = require(script.Parent.Parent.Cryo) +return Cryo.None \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedMap/OrderedMap.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedMap/OrderedMap.lua new file mode 100644 index 0000000..8bf478a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedMap/OrderedMap.lua @@ -0,0 +1,479 @@ +local Root = script.Parent.Parent +local None = require(Root.None) +local List = require(Root.List.List) +local UnorderedMap = require(Root.UnorderedMap.UnorderedMap) +local binarySearch = require(Root.binarySearch.binarySearch) +local deepJoin = require(Root.deepJoin.deepJoin) +local sort = require(Root.sort.sort) + +local OrderedMap = {} + +--[[ + Currently, exactly batchSet and join will remove None values. +]] + +OrderedMap.__index = OrderedMap + +--[[ + A BST implementation is not necessary for an immutable copy-on-write OrderedMap, since + we cannot take much advantage of O(log n) insert/deletes anyways. +]] + +--[[ + Create a new OrderedMap from a sortPredicate and any number of dictionaries of values of the form + { [key] = value } + sortPredicate should be a function with the following signature: + sortPredicate(key1, key2) -> bool + returning true if key1 < key2 in the sorting invariant. +]] +function OrderedMap.new(sortPredicate, ...) + local self = { + keys = {}, + values = {}, + sortPredicate = sortPredicate or sort.default, + _immutableDataStructureType = OrderedMap, + } + + setmetatable(self, OrderedMap) + + OrderedMap._insertInPlace(self, ...) + + return self +end + +--[[ + Returns a new UnorderedMap with the same key-value pairs. +]] +function OrderedMap:toUnorderedMap() + return UnorderedMap._newCannibalizeTable(self.values) +end + +--[[ + Returns if a value is an OrderedMap. +]] +function OrderedMap.is(value) + if type(value) ~= "table" then + return false + end + return value._immutableDataStructureType == OrderedMap +end + +--[[ + Returns the value at key. +]] +function OrderedMap:get(key) + return self.values[key] +end + +--[[ + Returns a new OrderedMap, setting the value at key to be value. +]] +function OrderedMap:set(key, value) + if self:get(key) == nil then + local new = self:copy() + new:_insertPairInPlace(key, value) + return new + end + local new = self:copy() + new.values[key] = value + return new +end + +--[[ + Gets the index-th key, value in the OrderedMap according to the sorting invariant. +]] +function OrderedMap:getByIndex(index) + local id = self.keys[index] + + if id == nil then + return nil + end + + return id, self.values[id] +end + +--[[ + Returns a List of all of the values in the map. +]] +function OrderedMap:getValues() + local new = {} + for index, key in ipairs(self.keys) do + new[index] = self.values[key] + end + return List._newCannibalizeTable(new) +end + +--[[ + Returns a List of all of the keys in the map. +]] +function OrderedMap:getKeys() + return List._newCannibalizeTable(self.keys) +end + +--[[ + Returns the size (number of key-value pairs) of the map. +]] +function OrderedMap:size() + return #self.keys +end + +--[[ + Returns a new OrderedMap with the pairs at all given keys removed. +]] +function OrderedMap:remove(...) + local new = OrderedMap.new(self.sortPredicate) + + local len = select("#", ...) + + local newKeys = new.keys + local newValues = new.values + + for key, value in pairs(self.values) do + newValues[key] = value + end + + for i = 1, len do + local key = select(i, ...) + + newValues[key] = nil + end + + for _, value in ipairs(self.keys) do + if new.values[value] ~= nil then + newKeys[#(newKeys)+1] = value + end + end + + return new +end + +--[[ + Internal method for removing a value from the map, in place. +]] +function OrderedMap:_removeInPlace(key) + self.values[key] = nil + local indexToRemove = binarySearch(self.keys, key, self.sortPredicate) + if not indexToRemove then + return + end + table.remove(self.keys, indexToRemove) +end + +--[[ + Joins any number of (basic table) dictionaries of values of the form + { [key] = value } + into the OrderedMap, creating a new OrderedMap. +]] +function OrderedMap:batchSet(...) + local new = self:copyRemoveNone() + new:_insertInPlaceRemoveNone(...) + return new +end + +--[[ + Creates a (shallow) copy of the OrderedMap. +]] +function OrderedMap:copy() + local new = OrderedMap.new(self.sortPredicate) + + local newKeys = new.keys + local newValues = new.values + + for key, value in ipairs(self.keys) do + newKeys[key] = value + end + + for key, value in pairs(self.values) do + newValues[key] = value + end + + return new +end + +--[[ + Creates a (shallow) copy of the OrderedMap, with all pairs with value None removed. +]] +function OrderedMap:copyRemoveNone() + local new = OrderedMap.new(self.sortPredicate) + + local newKeys = new.keys + local newValues = new.values + + for key, value in ipairs(self.keys) do + if self.values[value] ~= None then + newKeys[key] = value + end + end + + for key, value in pairs(self.values) do + if value ~= None then + newValues[key] = value + end + end + + return new +end + + +--[[ + Returns the first key, value in the OrderedMap. +]] +function OrderedMap:first() + if self.keys[1] then + return self.keys[1], self:get(self.keys[1]) + end +end + +--[[ + Returns the last key, value in the OrderedMap. +]] +function OrderedMap:last() + local i = #self.keys + if self.keys[i] then + return self.keys[i], self:get(self.keys[i]) + end +end + +--[[ + Create a new OrderedMap, applying the given predicate to each element in + this OrderedMap. + + Predicate should have the signature + predicate(value, key) -> newValue, newKey + + Analogous to 'map' on a list. +]] +function OrderedMap:map(predicate) + local new = OrderedMap.new(self.sortPredicate) + local keyChanged = false + for key, value in self:iterator() do + local newValue, newKey = predicate(value, key) + newKey = newKey or key + newValue = newValue or value + if key ~= newKey then + keyChanged = true + end + new:_insertPairInPlaceUnsorted(newKey, newValue) + end + + if keyChanged then + new:_sortInPlace() + end + + return new +end + +--[[ + Create a new OrderedMap, where each key-value pair is included iff callback(value, key) is truthy. + + callback should be a function of signature + callback(value, key) -> bool +]] +function OrderedMap:filter(callback) + local new = OrderedMap.new(self.sortPredicate) + for key, value in self:iterator() do + if callback(value, key) then + new:_insertPairInPlaceUnsorted(key, value) + end + end + + new:_sortInPlace() + return new +end + +--[[ + Join any number of OrderedMaps. The sorting comparator of the leftmost argument/self is + used for the returned OrderedMap. +]] +function OrderedMap:join(...) + local new = self:copyRemoveNone() + + for i = 1, select("#", ...) do + local other = select(i, ...) + + if other:size() > 0 then + for key, value in other:iterator() do + new:_insertPairInPlaceUnsortedRemoveNone(key, value) + end + end + end + + new:_sortInPlace() + + return new +end + +--[[ + Internal method for inserting values without sorting the map. + + This means that the invariants that the map exposes will be broken until + the _sortInPlace() method is called. +]] +function OrderedMap:_insertInPlaceUnsorted(...) + local len = select("#", ...) + for i = 1, len do + local pair = select(i, ...) + for key, value in pairs(pair) do + self:_insertPairInPlaceUnsorted(key, value) + end + end +end + +--[[ + Internal method for inserting a single pair without sorting the map. + + This means that the invariants that the map exposes will be broken until + the _sortInPlace() method is called. +]] +function OrderedMap:_insertPairInPlaceUnsorted(key, value) + if self.values[key] == nil then + table.insert(self.keys, key) + end + self.values[key] = value +end + +--[[ + Internal method for inserting values without sorting the map, removing None instances. + + This means that the invariants that the map exposes will be broken until + the _sortInPlace() method is called. +]] +function OrderedMap:_insertInPlaceUnsortedRemoveNone(...) + local len = select("#", ...) + for i = 1, len do + local pair = select(i, ...) + for key, value in pairs(pair) do + self:_insertPairInPlaceUnsortedRemoveNone(key, value) + end + end +end + +--[[ + Internal method for inserting a single pair without sorting the map, removing None instances. + + This means that the invariants that the map exposes will be broken until + the _sortInPlace() method is called. +]] +function OrderedMap:_insertPairInPlaceUnsortedRemoveNone(key, value) + if value == None then + self:_removeInPlace(key) + else + if self.values[key] == nil then + table.insert(self.keys, key) + end + self.values[key] = value + end +end + +--[[ + Sorts the map; used in cases where the map would become out-of-order when + using internal recommendations. +]] +function OrderedMap:_sortInPlace() + table.sort(self.keys, self.sortPredicate) +end + +--[[ + Returns an iterator for the OrderedMap. + Example: + for key, value in orderedMap:iterator() do +]] +function OrderedMap:iterator() + local i = 0 + local length = #self.keys + + -- Iterator function + return function() + i = i + 1 + if i <= length then + local key = self.keys[i] + return key, self.values[key], i + end + end +end + +--[[ + Returns an iterator for the OrderedMap that traverses in the reverse direction. + Example: + for key, value in orderedMap:reverseIterator() do +]] +function OrderedMap:reverseIterator() + local i = #self.keys + 1 + + -- Iterator function + return function() + i = i - 1 + if i > 0 then + local key = self.keys[i] + return key, self.values[key], i + end + end +end + +--[[ + Internal function that inserts the given values into the map in-place. + Expects { [key] = value } dictionaries. + + This operation mutates the map; generally you should use set or batchSet instead. +]] +function OrderedMap:_insertInPlace(...) + self:_insertInPlaceUnsorted(...) + self:_sortInPlace() +end + +--[[ + Internal function that inserts the given pair into the map in-place. + + This operation mutates the map; generally you should use set or batchSet instead. +]] +function OrderedMap:_insertPairInPlace(key, value) + self:_insertPairInPlaceUnsorted(key, value) + self:_sortInPlace() +end + +--[[ + Internal function that inserts the given values into the map in-place, removing None instances. + Expects { [key] = value } dictionaries. + + This operation mutates the map; generally you should use set or batchSet instead. +]] +function OrderedMap:_insertInPlaceRemoveNone(...) + self:_insertInPlaceUnsortedRemoveNone(...) + self:_sortInPlace() +end + +--[[ + Specify the behavior of deepJoin for OrderedMap. +]] +function OrderedMap:deepJoin(rhs) + local newMap = OrderedMap.new(self.sortPredicate) + for lhsKey, lhsValue in self:iterator() do + local rhsValue = rhs:get(lhsKey) + if rhsValue then + if type(rhsValue) == "table" and type(lhsValue) == "table" then + newMap:_insertPairInPlaceUnsorted(lhsKey, deepJoin(lhsValue, rhsValue)) + else + if rhsValue ~= None then + newMap:_insertPairInPlaceUnsorted(lhsKey, rhsValue) + end + end + else + if lhsValue ~= None then + newMap:_insertPairInPlaceUnsorted(lhsKey, lhsValue) + end + end + end + + -- Copy over rhs keys that aren't in lhs + for rhsKey, rhsValue in rhs:iterator() do + local lhsValue = self:get(rhsKey) + if not lhsValue and rhsValue ~= None then + newMap:_insertPairInPlaceUnsorted(rhsKey, rhsValue) + end + end + + newMap:_sortInPlace() + return newMap +end + +return OrderedMap \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedMap/OrderedMap.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedMap/OrderedMap.spec.lua new file mode 100644 index 0000000..7bdb02e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedMap/OrderedMap.spec.lua @@ -0,0 +1,621 @@ +return function() + local Root = script.Parent.Parent + local OrderedMap = require(script.Parent.OrderedMap) + local UnorderedMap = require(Root.UnorderedMap.UnorderedMap) + local None = require(Root.None) + local sort = require(Root.sort.sort) + + describe("Basic ordered map operations", function() + + it("Creates a new empty table", function() + local map = OrderedMap.new() + expect(map).to.be.ok() + expect(map:size()).to.equal(0) + end) + + it("Can tell apart OrderedMaps from non-OrderedMaps", function() + local nonmap = {} + expect(OrderedMap.is(nonmap)).to.equal(false) + local number = 5 + expect(OrderedMap.is(number)).to.equal(false) + local map = OrderedMap.new() + expect(OrderedMap.is(map)).to.equal(true) + end) + + it("Correctly returns nil when accessing an OOB index", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + expect(map:getByIndex(1000)).to.never.be.ok() + expect(map:size()).to.equal(3) + expect(map:get("apple")).to.equal(1) + expect(map:get("grapes")).to.equal(2) + expect(map:get("banana")).to.equal(10) + end) + + it("Creates an ordered with some values", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + expect(map).to.be.ok() + expect(map:size()).to.equal(3) + expect(map:get("apple")).to.equal(1) + expect(map:get("grapes")).to.equal(2) + expect(map:get("banana")).to.equal(10) + + -- Good sorting by key + local firstKey, firstValue = map:first() + local secondKey, secondValue = map:getByIndex(2) + local lastKey, lastValue = map:last() + expect(firstKey).to.equal("apple") + expect(firstValue).to.equal(1) + expect(secondKey).to.equal("banana") + expect(secondValue).to.equal(10) + expect(lastKey).to.equal("grapes") + expect(lastValue).to.equal(2) + end) + + it("Returns nil on a miss", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + expect(map:get("broccoli")).never.to.be.ok() + end) + + it("Creates a table with some values from multiple tables", function() + local map = OrderedMap.new(sort.reverse, { + apple = 1, + grapes = 10, + banana = 2, + }, { + orange = 5, + banana = 5, + }) + expect(map).to.be.ok() + expect(map:size()).to.equal(4) + expect(map:get("orange")).to.equal(5) + expect(map:get("grapes")).to.equal(10) + expect(map:get("banana")).to.equal(5) + end) + + it("Can create a copy", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + local mapCopy = map:copy() + + expect(mapCopy).never.to.equal(map) + expect(mapCopy).to.be.ok() + expect(mapCopy:size()).to.equal(3) + expect(mapCopy:get("apple")).to.equal(1) + expect(mapCopy:get("grapes")).to.equal(2) + expect(mapCopy:get("banana")).to.equal(10) + + -- Good sorting by key + local firstKey, firstValue = mapCopy:first() + local secondKey, secondValue = mapCopy:getByIndex(2) + local lastKey, lastValue = map:last() + expect(firstKey).to.equal("apple") + expect(firstValue).to.equal(1) + expect(secondKey).to.equal("banana") + expect(secondValue).to.equal(10) + expect(lastKey).to.equal("grapes") + expect(lastValue).to.equal(2) + end) + + it("Can be converted immutably to Lists of keys and values", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + local keys = map:getKeys() + local values = map:getValues() + + expect(keys:get(1)).to.equal("apple") + expect(keys:get(2)).to.equal("banana") + expect(keys:get(3)).to.equal("grapes") + expect(values:get(1)).to.equal(1) + expect(values:get(2)).to.equal(10) + expect(values:get(3)).to.equal(2) + end) + + it("Supports immutable set", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + + local newMap = map:set("apple", 1000) + + expect(newMap).never.to.equal(map) + expect(map:get("apple")).to.equal(1) + expect(newMap:get("apple")).to.equal(1000) + + local newNewMap = newMap:set("lettuce", -100) + expect(newNewMap:get("lettuce")).to.equal(-100) + end) + + it("Supports batch set", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + + local newMap = map:batchSet({ + apple = 100, + watermelon = 4, + }, { + watermelon = 100, + kiwi = 3, + }) + + expect(newMap).never.to.equal(map) + expect(map:get("apple")).to.equal(1) + expect(newMap:get("apple")).to.equal(100) + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect((newMap:first())).to.equal("apple") + expect((newMap:last())).to.equal("watermelon") + expect(map:get("kiwi")).to.never.be.ok() + expect(newMap:size()).to.equal(5) + end) + + it("Supports join", function() + local map1 = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + + local map2 = OrderedMap.new(sort.reverse, { + apple = 100, + watermelon = 4, + }) + + local map3 = OrderedMap.new(sort.reverse, { + watermelon = 100, + kiwi = 3, + }) + + local newMap = map1:join(map2, map3) + + expect(newMap).never.to.equal(map1) + expect(map1:get("apple")).to.equal(1) + expect(newMap:get("apple")).to.equal(100) + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect((newMap:first())).to.equal("apple") + expect((newMap:last())).to.equal("watermelon") + expect(map1:get("kiwi")).to.never.be.ok() + expect(newMap:size()).to.equal(5) + end) + + it("Supports None", function() + local map1 = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + useless = None, + }) + expect(map1:size()).to.equal(4) + + local map2 = OrderedMap.new(sort.reverse, { + apple = None, + watermelon = 4, + }) + + local map3 = OrderedMap.new(sort.reverse, { + watermelon = 100, + kiwi = 3, + }) + + local newMap = map1:join(map2, map3) + + expect(newMap).never.to.equal(map1) + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect(newMap:get("useless")).to.never.be.ok() + expect((newMap:first())).to.equal("banana") + expect((newMap:last())).to.equal("watermelon") + expect(newMap:size()).to.equal(4) + + newMap = map1:batchSet({ + apple = None, + watermelon = 4, + }, { + watermelon = 100, + kiwi = 3, + }) + + expect(newMap).never.to.equal(map1) + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect(newMap:get("useless")).to.never.be.ok() + expect((newMap:first())).to.equal("banana") + expect((newMap:last())).to.equal("watermelon") + expect(newMap:size()).to.equal(4) + end) + + it("Supports deletion", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + local newMap = map:remove("apple", "grapes") + + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("grapes")).to.never.be.ok() + expect(newMap:get("banana")).to.equal(10) + expect(map:get("apple")).to.be.ok() + expect(map:get("grapes")).to.be.ok() + expect(map:get("banana")).to.be.ok() + expect(newMap:size()).to.equal(1) + end) + end) + + describe("More advanced functionality", function() + describe("Mapping", function() + it("should use the callback", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + + local newMap = map:map(function(value, key) + return value * 2, key .. " fruit" + end) + + expect(newMap).never.to.equal(map) + expect(newMap:size()).to.equal(3) + expect(map:get("apple fruit")).to.never.be.ok() + expect(newMap:get("apple fruit")).to.equal(2) + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("banana fruit")).to.equal(20) + end) + + it("should maintain the sorting invariant", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + cheese = 5, + banana = 10, + }) + + local newMap = map:map(function(value, key) + return value, key:sub(2) + end) + + expect(newMap).never.to.equal(map) + expect(newMap:size()).to.equal(3) + expect(newMap:get("pple")).to.equal(1) + local firstKey, firstValue = newMap:first() + expect(firstKey).to.equal("anana") + expect(firstValue).to.equal(10) + local secondKey, secondValue = newMap:getByIndex(2) + expect(secondKey).to.equal("heese") + expect(secondValue).to.equal(5) + local lastKey, lastValue = newMap:last() + expect(lastKey).to.equal("pple") + expect(lastValue).to.equal(1) + end) + + it("should work with an empty OrderedMap", function() + local called = false + local function callback() + called = true + return "placeholderkey", "placeholdervalue" + end + local map = OrderedMap.new() + local newMap = map:map(callback) + expect(newMap:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Filtering", function() + it("should use the callback", function() + local map = OrderedMap.new(sort.default, { + orange = 100, + apple = 1, + grapes = 2, + banana = 10, + lemons = 4, + }) + + local newMap = map:filter(function(value, key) + return value < 9 or key == "orange" + end) + + expect(newMap).never.to.equal(map) + expect(newMap:size()).to.equal(4) + expect(newMap:get("apple")).to.equal(1) + expect(newMap:get("banana")).to.never.be.ok() + expect(map:get("banana")).to.equal(10) + expect(newMap:get("orange")).to.equal(100) + end) + + it("should maintain the sorting invariant", function() + local map = OrderedMap.new(sort.default, { + orange = 100, + apple = 1, + grapes = 2, + banana = 10, + lemons = 4, + }) + + local newMap = map:filter(function(value, key) + return value < 9 or key == "orange" + end) + + local firstKey, firstValue = newMap:first() + expect(firstKey).to.equal("apple") + expect(firstValue).to.equal(1) + local secondKey, secondValue = newMap:getByIndex(2) + expect(secondKey).to.equal("grapes") + expect(secondValue).to.equal(2) + local lastKey, lastValue = newMap:last() + expect(lastKey).to.equal("orange") + expect(lastValue).to.equal(100) + end) + + it("should work with an empty OrderedMap", function() + local called = false + local function callback() + called = true + return true + end + local map = OrderedMap.new() + local newMap = map:filter(callback) + expect(newMap:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Iterators", function() + it("Should work in a for loop", function() + local map = OrderedMap.new(sort.default, { + orange = 100, + apple = 1, + grapes = 2, + banana = 10, + lemons = 4, + }) + local count = 1 + for key, value in map:iterator() do + if count == 1 then + expect(key).to.equal("apple") + expect(value).to.equal(1) + elseif count == 2 then + expect(key).to.equal("banana") + expect(value).to.equal(10) + elseif count == 3 then + expect(key).to.equal("grapes") + expect(value).to.equal(2) + elseif count == 4 then + expect(key).to.equal("lemons") + expect(value).to.equal(4) + elseif count == 5 then + expect(key).to.equal("orange") + expect(value).to.equal(100) + end + count = count + 1 + end + expect(count - 1).to.equal(5) + end) + + it("Should work for empty maps", function() + local map = OrderedMap.new() + local doAnything = false + for _, _ in map:iterator() do + doAnything = true + end + expect(doAnything).to.equal(false) + end) + + it("Should work in reverse", function() + local map = OrderedMap.new(sort.default, { + orange = 100, + apple = 1, + grapes = 2, + banana = 10, + lemons = 4, + }) + local count = 1 + for key, value in map:reverseIterator() do + if count == 5 then + expect(key).to.equal("apple") + expect(value).to.equal(1) + elseif count == 4 then + expect(key).to.equal("banana") + expect(value).to.equal(10) + elseif count == 3 then + expect(key).to.equal("grapes") + expect(value).to.equal(2) + elseif count == 2 then + expect(key).to.equal("lemons") + expect(value).to.equal(4) + elseif count == 1 then + expect(key).to.equal("orange") + expect(value).to.equal(100) + end + count = count + 1 + end + expect(count - 1).to.equal(5) + end) + end) + describe("Downcasting", function() + it("Should successfully downcast to an UnorderedMap", function() + local ordered = OrderedMap.new(sort.default, { + orange = 100, + apple = 1, + grapes = 2, + banana = 10, + lemons = 4, + }) + local unordered = ordered:toUnorderedMap() + expect(UnorderedMap.is(unordered)).to.equal(true) + expect(unordered:get("orange")).to.equal(100) + expect(unordered:get("apple")).to.equal(1) + expect(unordered:get("grapes")).to.equal(2) + expect(unordered:get("banana")).to.equal(10) + expect(unordered:get("lemons")).to.equal(4) + expect(unordered:size()).to.equal(5) + end) + end) + end) + + describe("deepJoin", function() + it("Should work with basic maps", function() + local map1 = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + + local map2 = OrderedMap.new(sort.default, { + apple = 100, + watermelon = 4, + }) + + local newMap = map1:deepJoin(map2) + + expect(newMap:get("apple")).to.equal(100) + expect(newMap:get("watermelon")).to.equal(4) + expect(newMap:get("grapes")).to.equal(2) + expect(newMap:get("banana")).to.equal(10) + expect((newMap:first())).to.equal("apple") + expect((newMap:last())).to.equal("watermelon") + expect(newMap:size()).to.equal(4) + end) + + it("Should work with nested maps", function() + local innerMap1 = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + + local innerMap2 = OrderedMap.new(sort.default, { + peach = -10, + grapes = 15 + }) + + local innerMap3 = OrderedMap.new(sort.default, { + apple = 100, + banana = 1000, + beans = 400, + }) + + local innerMap4 = OrderedMap.new(sort.default, { + peach = 30, + }) + + local outerMap1 = OrderedMap.new(sort.default, { + inner1 = innerMap1, + inner2 = innerMap2, + irrelevantInteger = 5, + }) + + local outerMap2 = OrderedMap.new(sort.default, { + inner1 = innerMap3, + inner2 = innerMap4, + irrelevantString = "hello!", + }) + + local newMap = outerMap1:deepJoin(outerMap2) + expect(newMap:size()).to.equal(4) + expect(newMap:get("irrelevantInteger")).to.equal(5) + expect(newMap:get("irrelevantString")).to.equal("hello!") + + local mergedInner1 = newMap:get("inner1") + local mergedInner2 = newMap:get("inner2") + expect(mergedInner1:size()).to.equal(4) + expect(mergedInner1:get("apple")).to.equal(100) + expect(mergedInner1:get("grapes")).to.equal(2) + expect(mergedInner1:get("banana")).to.equal(1000) + expect(mergedInner1:get("beans")).to.equal(400) + + expect(mergedInner2:size()).to.equal(2) + expect(mergedInner2:get("peach")).to.equal(30) + expect(mergedInner2:get("grapes")).to.equal(15) + end) + + it("Should work with an empty map", function() + local map1 = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + + local map2 = OrderedMap.new(sort.default) + local newMap = map1:deepJoin(map2) + + expect(newMap:get("apple")).to.equal(1) + expect(newMap:get("grapes")).to.equal(2) + expect(newMap:get("banana")).to.equal(10) + end) + + it("Should work with None", function() + local innerMap1 = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + useless = None, + }) + + local innerMap2 = OrderedMap.new(sort.default, { + peach = -10, + grapes = 15, + }) + + local innerMap3 = OrderedMap.new(sort.default, { + apple = None, + banana = 1000, + beans = 400, + }) + + local innerMap4 = OrderedMap.new(sort.default, { + grapes = None, + peach = 30, + }) + + local outerMap1 = OrderedMap.new(sort.default, { + inner1 = innerMap1, + inner2 = innerMap2, + irrelevantInteger = 5, + }) + + local outerMap2 = OrderedMap.new(sort.default, { + inner1 = innerMap3, + inner2 = innerMap4, + irrelevantInteger = None, + }) + + local newMap = outerMap1:deepJoin(outerMap2) + expect(newMap:size()).to.equal(2) + expect(newMap:get("irrelevantInteger")).to.never.be.ok() + + local mergedInner1 = newMap:get("inner1") + local mergedInner2 = newMap:get("inner2") + expect(mergedInner1:size()).to.equal(3) + expect(mergedInner1:get("apple")).to.never.be.ok() + + expect(mergedInner2:size()).to.equal(1) + expect(mergedInner2:get("grapes")).to.never.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedSet/OrderedSet.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedSet/OrderedSet.lua new file mode 100644 index 0000000..5f0074a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedSet/OrderedSet.lua @@ -0,0 +1,278 @@ +local Root = script.Parent.Parent +local OrderedMap = require(Root.OrderedMap.OrderedMap) +local UnorderedSet = require(Root.UnorderedSet.UnorderedSet) +local sort = require(Root.sort.sort) + +local OrderedSet = {} + +OrderedSet.__index = OrderedSet + +--[[ + Create a new OrderedSet from a sortPredicate and any number of keys + sortPredicate should be a function with the following signature: + sortPredicate(key1, key2) -> bool + returning true if key1 < key2 in the sorting invariant. +]] +function OrderedSet.new(sortPredicate, ...) + sortPredicate = sortPredicate or sort.default + local keyMap = OrderedSet._unpackedToTrueMap(...) + + local self = { + internalMap = OrderedMap.new(sortPredicate, keyMap), + _immutableDataStructureType = OrderedSet, + } + + setmetatable(self, OrderedSet) + + return self +end + +--[[ + Returns a new UnorderedSet with the same entries. +]] +function OrderedSet:toUnorderedSet() + local newInternalMap = self.internalMap:toUnorderedMap() + return UnorderedSet._newCannibalizeMap(newInternalMap) +end + +--[[ + Internal method that absorbs an existing OrderedMap as the underlying data structure for a + new OrderedSet. +]] +function OrderedSet._newCannibalizeMap(orderedMap) + local self = { + internalMap = orderedMap, + _immutableDataStructureType = OrderedSet, + } + + setmetatable(self, OrderedSet) + + return self +end + +--[[ + Internal method that takes an unpacked list of values (...) and transforms it into a table + in which each value is a key with value true. +]] +function OrderedSet._unpackedToTrueMap(...) + local keyMap = {} + local len = select("#", ...) + for i = 1, len do + keyMap[select(i, ...)] = true + end + return keyMap +end + +--[[ + Returns if a value is an OrderedSet. +]] +function OrderedSet.is(value) + if type(value) ~= "table" then + return false + end + return value._immutableDataStructureType == OrderedSet +end + +--[[ + Returns true if a key is in the set, false otherwise. +]] +function OrderedSet:find(key) + return self.internalMap:get(key) ~= nil +end + +--[[ + Returns a new OrderedSet, inserting a number of values into the set. +]] +function OrderedSet:insert(...) + local keyMap = OrderedSet._unpackedToTrueMap(...) + local newMap = self.internalMap:batchSet(keyMap) + return OrderedSet._newCannibalizeMap(newMap) +end + +--[[ + Gets the index-th key in the OrderedSet according to the sorting invariant. +]] +function OrderedSet:getByIndex(index) + return (self.internalMap:getByIndex(index)) +end + +--[[ + Returns a copy of the table of all of the keys in the map. +]] +function OrderedSet:getKeys() + return self.internalMap:getKeys() +end + +--[[ + Return the number of keys in the set. +]] +function OrderedSet:size() + return self.internalMap:size() +end + +--[[ + Returns a new OrderedSet, removing a number of values into the set. +]] +function OrderedSet:remove(...) + local newMap = self.internalMap:remove(...) + return OrderedSet._newCannibalizeMap(newMap) +end + +--[[ + Returns a (shallow) copy of the OrderedSet. +]] +function OrderedSet:copy() + local newMap = self.internalMap:copy() + return OrderedSet._newCannibalizeMap(newMap) +end + +--[[ + Returns the first key in the OrderedSet. +]] +function OrderedSet:first() + return (self.internalMap:first()) +end + +--[[ + Returns the last key in the OrderedSet. +]] +function OrderedSet:last() + return (self.internalMap:last()) +end + +--[[ + Create a new OrderedSet, applying the given predicate to each element in + this OrderedSet. + + Predicate should have the signature + predicate(key) -> newKey + + Analogous to 'map' on a list. +]] +function OrderedSet:map(predicate) + local alteredPredicate = function(_, key) + return nil, predicate(key) + end + local newMap = self.internalMap:map(alteredPredicate) + return OrderedSet._newCannibalizeMap(newMap) +end + +--[[ + Create a new OrderedSet, where each key is included iff callback(key) is truthy. + + callback should be a function of signature + callback(key) -> bool +]] +function OrderedSet:filter(callback) + local alteredCallback = function(_, key) + return callback(key) + end + local newMap = self.internalMap:filter(alteredCallback) + return OrderedSet._newCannibalizeMap(newMap) +end + +--[[ + Union any number of OrderedSets. The sorting comparator of the leftmost argument/self is + used for the returned OrderedSet. +]] +function OrderedSet:union(...) + local internalMaps = {} + local len = select("#", ...) + for i = 1, len do + internalMaps[i] = select(i, ...).internalMap + end + local newMap = self.internalMap:join(unpack(internalMaps)) + return OrderedSet._newCannibalizeMap(newMap) +end + +--[[ + Intersect any number of OrderedSets. The sorting comparator of the leftmost argument/self is + used for the returned OrderedSet. +]] +function OrderedSet:intersection(...) + local len = select("#", ...) + local args = { ... } + + local filterer = function(key) + for i = 1, len do + local set = args[i] + if not set:find(key) then + return false + end + end + return true + end + + return self:filter(filterer) +end + +--[[ + Find the difference between any number OrderedSets. The sorting comparator of the leftmost argument/self is + used for the returned OrderedSet. + The behavior of A:difference(B, C, D) is equivalent to the behavior of + A:difference(B:union(C, D)). +]] +function OrderedSet:difference(...) + local newSet = OrderedSet.new(self.sortPredicate) + local len = select("#", ...) + for _, key in self:iterator() do + local shouldRemain = true + for i = 1, len do + if select(i, ...):find(key) then + shouldRemain = false + break + end + end + if shouldRemain then + newSet.internalMap:_insertPairInPlaceUnsorted(key, true) + end + end + newSet.internalMap:_sortInPlace() + return newSet +end + +--[[ + Returns an iterator for the OrderedSet. + Example: + for index, key in orderedSet:iterator() do +]] +function OrderedSet:iterator() + local i = 0 + local length = self.internalMap:size() + + -- Iterator function + return function() + i = i + 1 + if i <= length then + local key = self:getByIndex(i) + return i, key + end + end +end + +--[[ + Returns an iterator for the OrderedSet that traverses in the reverse direction. + Example: + for index, key in orderedSet:reverseIterator() do +]] +function OrderedSet:reverseIterator() + local i = self.internalMap:size() + 1 + + -- Iterator function + return function() + i = i - 1 + if i > 0 then + local key = self:getByIndex(i) + return i, key + end + end +end + +OrderedSet.join = OrderedSet.union + +--[[ + Specify the behavior of deepJoin for OrderedSet. +]] +OrderedSet.deepJoin = OrderedSet.union + +return OrderedSet \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedSet/OrderedSet.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedSet/OrderedSet.spec.lua new file mode 100644 index 0000000..505203c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedSet/OrderedSet.spec.lua @@ -0,0 +1,359 @@ +return function() + local Root = script.Parent.Parent + local OrderedSet = require(script.Parent.OrderedSet) + local UnorderedSet = require(Root.UnorderedSet.UnorderedSet) + local sort = require(Root.sort.sort) + describe("Basic ordered map operations", function() + + it("Creates a new empty set", function() + local set = OrderedSet.new() + expect(set).to.be.ok() + expect(set:size()).to.equal(0) + end) + + it("Can tell apart OrderedSets from non-OrderedSets", function() + local nonset = {} + expect(OrderedSet.is(nonset)).to.equal(false) + local number = 5 + expect(OrderedSet.is(number)).to.equal(false) + local set = OrderedSet.new() + expect(OrderedSet.is(set)).to.equal(true) + end) + + it("Creates an ordered set with some values", function() + local set = OrderedSet.new(sort.default, 3, 2, 1, 3, 4) + expect(set).to.be.ok() + expect(set:size()).to.equal(4) + expect(set:find(1)).to.equal(true) + expect(set:find(2)).to.equal(true) + expect(set:find(3)).to.equal(true) + expect(set:find(4)).to.equal(true) + + -- Good sorting by key + expect(set:first()).to.equal(1) + expect(set:getByIndex(1)).to.equal(1) + expect(set:getByIndex(2)).to.equal(2) + expect(set:getByIndex(3)).to.equal(3) + expect(set:getByIndex(4)).to.equal(4) + expect(set:last()).to.equal(4) + end) + + it("Returns nil on a miss", function() + local set = OrderedSet.new(sort.default, 3, 2, 1, 3, 4) + expect(set:find(10000)).to.equal(false) + end) + + it("Creates a table with a reverse sort invariant", function() + local set = OrderedSet.new(sort.reverse, 3, 2, 1, 3, 4) + + -- Good sorting by key + expect(set:first()).to.equal(4) + expect(set:getByIndex(4)).to.equal(1) + expect(set:getByIndex(3)).to.equal(2) + expect(set:getByIndex(2)).to.equal(3) + expect(set:getByIndex(1)).to.equal(4) + expect(set:last()).to.equal(1) + end) + + it("Can create a copy", function() + local set = OrderedSet.new(sort.default, 3, 2, 1, 3, 4) + local setCopy = set:copy() + + expect(setCopy).never.to.equal(set) + expect(setCopy).to.be.ok() + expect(setCopy:size()).to.equal(4) + expect(setCopy:find(1)).to.equal(true) + expect(setCopy:find(2)).to.equal(true) + expect(setCopy:find(3)).to.equal(true) + expect(setCopy:find(4)).to.equal(true) + + -- Good sorting by key + expect(setCopy:first()).to.equal(1) + expect(setCopy:getByIndex(1)).to.equal(1) + expect(setCopy:getByIndex(2)).to.equal(2) + expect(setCopy:getByIndex(3)).to.equal(3) + expect(setCopy:getByIndex(4)).to.equal(4) + expect(setCopy:last()).to.equal(4) + end) + + it("Can be converted immutably to a list of keys", function() + local set = OrderedSet.new(sort.default, 10, 11, 11, 10, 15) + local keys = set:getKeys() + + expect(keys:get(1)).to.equal(10) + expect(keys:get(2)).to.equal(11) + expect(keys:get(3)).to.equal(15) + end) + + it("Supports immutable insert", function() + local set = OrderedSet.new(sort.default, 10, 11, 11, 10, 15) + + local newSet = set:insert(12) + expect(newSet).never.to.equal(set) + expect(newSet:find(12)).to.equal(true) + expect(newSet:size()).to.equal(4) + + expect(set:size()).to.equal(3) + expect(set:find(12)).to.equal(false) + + local newNewSet = newSet:insert(100, -100, 25, 10, 100) + expect(newNewSet:size()).to.equal(7) + end) + + it("Supports union", function() + local set1 = OrderedSet.new(sort.default, 4, 3, 2, 1) + + local set2 = OrderedSet.new(sort.default, 3, 4, 5, 6) + + local set3 = OrderedSet.new(sort.default, 3, 6, 100) + + local newSet = set1:union(set2, set3) + + expect(newSet).never.to.equal(set1) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(5)).to.equal(true) + expect(newSet:find(6)).to.equal(true) + expect(newSet:find(100)).to.equal(true) + expect(set1:size()).to.equal(4) + expect(newSet:size()).to.equal(7) + + local emptySet = OrderedSet.new() + local newNewSet = newSet:union(emptySet) + expect(newNewSet:size()).to.equal(7) + end) + + it("Supports deletion", function() + local set = OrderedSet.new(sort.default, 3, 2, 1, 4, 5, 6) + local newSet = set:remove(2, 4) + + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(false) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(false) + expect(newSet:find(5)).to.equal(true) + expect(newSet:find(6)).to.equal(true) + expect(newSet:size()).to.equal(4) + end) + + it("Supports intersection", function() + local set1 = OrderedSet.new(sort.default, 4, 3, 2, 1) + + local set2 = OrderedSet.new(sort.default, 3, 4, 5, 6, 1) + + local set3 = OrderedSet.new(sort.default, 3, 6, 100, 1) + + local newSet = set1:intersection(set2, set3) + + expect(newSet).never.to.equal(set1) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(false) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(false) + expect(newSet:find(5)).to.equal(false) + expect(newSet:find(6)).to.equal(false) + expect(newSet:find(100)).to.equal(false) + expect(set1:size()).to.equal(4) + expect(newSet:size()).to.equal(2) + + local emptySet = OrderedSet.new() + local newNewSet = newSet:intersection(emptySet) + expect(newNewSet:size()).to.equal(0) + end) + + it("Supports set difference", function() + local set1 = OrderedSet.new(sort.default, 10, 4, 3, 2, 1) + + local set2 = OrderedSet.new(sort.default, 3, 4, 5) + + local set3 = OrderedSet.new(sort.default, 100, 101) + + local newSet = set1:difference(set2, set3) + + expect(newSet).never.to.equal(set1) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(10)).to.equal(true) + expect(newSet:find(3)).to.equal(false) + expect(newSet:find(4)).to.equal(false) + expect(newSet:find(5)).to.equal(false) + expect(newSet:find(100)).to.equal(false) + expect(newSet:find(100)).to.equal(false) + expect(newSet:find(101)).to.equal(false) + expect(set1:size()).to.equal(5) + expect(newSet:size()).to.equal(3) + + local emptySet = OrderedSet.new() + local newNewSet = set1:difference(emptySet) + expect(newNewSet:size()).to.equal(5) + end) + end) + + describe("More advanced functionality", function() + describe("Mapping", function() + it("should use the callback", function() + local set = OrderedSet.new(sort.default, 4, 3, 2, 1) + + local newSet = set:map(function(key) + return key * 2 + end) + + expect(newSet:find(1)).to.equal(false) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(6)).to.equal(true) + expect(newSet:find(8)).to.equal(true) + expect(newSet:size()).to.equal(4) + end) + + it("should maintain the sorting invariant", function() + local set = OrderedSet.new(sort.default, 3, 2, 1, 4) + + local newSet = set:map(function(key) + return key % 3 + end) + + expect(newSet:find(0)).to.equal(true) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + + expect(newSet:getByIndex(1)).to.equal(0) + expect(newSet:getByIndex(2)).to.equal(1) + expect(newSet:getByIndex(3)).to.equal(2) + + expect(newSet:size()).to.equal(3) + end) + + it("should work with an empty OrderedSet", function() + local called = false + local function callback() + called = true + return "placeholderkey" + end + local set = OrderedSet.new() + local newSet = set:map(callback) + expect(newSet:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Filtering", function() + it("should use the callback", function() + local set = OrderedSet.new(sort.default, 4, 3, 2, 1) + local newSet = set:filter(function(key) + return key >= 3 + end) + + expect(newSet).never.to.equal(set) + + expect(newSet:size()).to.equal(2) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(1)).to.equal(false) + expect(newSet:find(2)).to.equal(false) + end) + + it("should maintain the sorting invariant", function() + local set = OrderedSet.new(sort.default, 4, 3, 2, 1, 6, 5, 100) + local newSet = set:filter(function(key) + return key >= 3 + end) + + expect(newSet).never.to.equal(set) + + expect(newSet:size()).to.equal(5) + expect(newSet:getByIndex(1)).to.equal(3) + expect(newSet:getByIndex(2)).to.equal(4) + expect(newSet:last()).to.equal(100) + end) + + it("should work with an empty OrderedSet", function() + local called = false + local function callback() + called = true + return true + end + local set = OrderedSet.new() + local newSet = set:filter(callback) + expect(newSet:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Iterators", function() + it("Should work in a for loop", function() + local set = OrderedSet.new(sort.default, 8, 3, 2, 1, 100) + local count = 1 + for index, key in set:iterator() do + if count == 1 then + expect(index).to.equal(1) + expect(key).to.equal(1) + elseif count == 2 then + expect(index).to.equal(2) + expect(key).to.equal(2) + elseif count == 3 then + expect(index).to.equal(3) + expect(key).to.equal(3) + elseif count == 4 then + expect(index).to.equal(4) + expect(key).to.equal(8) + elseif count == 5 then + expect(index).to.equal(5) + expect(key).to.equal(100) + end + count = count + 1 + end + expect(count - 1).to.equal(5) + end) + + it("Should work for empty sets", function() + local set = OrderedSet.new() + local doAnything = false + for _, _ in set:iterator() do + doAnything = true + end + expect(doAnything).to.equal(false) + end) + + it("Should work in reverse", function() + local set = OrderedSet.new(sort.default, 8, 3, 2, 1, 100) + local count = 1 + for index, key in set:reverseIterator() do + if count == 5 then + expect(index).to.equal(1) + expect(key).to.equal(1) + elseif count == 4 then + expect(index).to.equal(2) + expect(key).to.equal(2) + elseif count == 3 then + expect(index).to.equal(3) + expect(key).to.equal(3) + elseif count == 2 then + expect(index).to.equal(4) + expect(key).to.equal(8) + elseif count == 1 then + expect(index).to.equal(5) + expect(key).to.equal(100) + end + count = count + 1 + end + expect(count - 1).to.equal(5) + end) + end) + + describe("Downcasting", function() + it("Should successfully downcast to an UnorderedSet", function() + local ordered = OrderedSet.new(sort.default, 5, 4, 3, 2, 1) + local unordered = ordered:toUnorderedSet() + expect(UnorderedSet.is(unordered)).to.equal(true) + for i = 5, 1, -1 do + expect(unordered:find(i)).to.equal(true) + end + expect(unordered:size()).to.equal(5) + end) + end) + + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedMap/UnorderedMap.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedMap/UnorderedMap.lua new file mode 100644 index 0000000..bc2b46b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedMap/UnorderedMap.lua @@ -0,0 +1,303 @@ +local Root = script.Parent.Parent +local Cryo = require(Root.Parent.Cryo) +local List = require(Root.List.List) +local None = require(Root.None) +local deepJoin = require(Root.deepJoin.deepJoin) + +local UnorderedMap = {} + +UnorderedMap.__index = UnorderedMap + +--[[ + Create a new UnorderedMap from any number of dictionaries of values of the form + { [key] = value } +]] +function UnorderedMap.new(...) + local self = { + pairs = {}, + length = 0, + _immutableDataStructureType = UnorderedMap + } + + setmetatable(self, UnorderedMap) + + UnorderedMap._insertInPlace(self, ...) + + return self +end + +--[[ + Internal method that absorbs an existing table and constructs a new UnorderdMap + from it as the underlying data. +]] +function UnorderedMap._newCannibalizeTable(tab) + local count = 0 + for _ in pairs(tab) do + count = count + 1 + end + local self = { + pairs = tab, + length = count, + _immutableDataStructureType = UnorderedMap + } + + setmetatable(self, UnorderedMap) + + return self +end + +--[[ + Returns if a value is an UnorderedMap. +]] +function UnorderedMap.is(value) + if type(value) ~= "table" then + return false + end + return value._immutableDataStructureType == UnorderedMap +end + +--[[ + Returns the value at key. +]] +function UnorderedMap:get(key) + return self.pairs[key] +end + +--[[ + Returns a new UnorderedMap, setting the value at key to be value. +]] +function UnorderedMap:set(key, value) + if self:get(key) == nil then + self.length = self.length + 1 + end + local new = self:copy() + new.pairs[key] = value + return new +end + +--[[ + Returns a List of all of the values in the map. +]] +function UnorderedMap:getValues() + return List._newCannibalizeTable(Cryo.Dictionary.values(self.pairs)) +end + +--[[ + Returns a List of all of the keys in the map. +]] +function UnorderedMap:getKeys() + return List._newCannibalizeTable(Cryo.Dictionary.keys(self.pairs)) +end + +--[[ + Returns the size (number of key-value pairs) of the map. +]] +function UnorderedMap:size() + return self.length +end + +--[[ + Returns a new UnorderedMap with the pairs at all given keys removed. +]] +function UnorderedMap:remove(...) + local new = self:copy() + + local newPairs = new.pairs + + local len = select("#", ...) + for i = 1, len do + local key = select(i, ...) + if new:get(key) ~= nil then + new.length = new.length - 1 + newPairs[key] = nil + end + end + return new +end + +--[[ + Joins any number of (basic table) dictionaries of values of the form + { [key] = value } + into the OrderedMap, creating a new OrderedMap. +]] +function UnorderedMap:batchSet(...) + local new = self:copyRemoveNone() + new:_insertInPlaceRemoveNone(...) + return new +end + +--[[ + Returns a (shallow) copy of the UnorderedMap. +]] +function UnorderedMap:copy() + return UnorderedMap.new(self.pairs) +end + +--[[ + Returns a (shallow) copy of the UnorderedMap, removing all None instances. +]] +function UnorderedMap:copyRemoveNone() + local new = {} + for key, value in self:iterator() do + if value ~= None then + new[key] = value + end + end + return UnorderedMap._newCannibalizeTable(new) +end + +--[[ + Create a new UnorderedMap, applying the given predicate to each element in + this UnorderedMap. + + Predicate should have the signature + predicate(value, key) -> newValue, newKey + + Analogous to 'map' on a list. + +]] +function UnorderedMap:map(predicate) + local new = UnorderedMap.new() + for key, value in self:iterator() do + local newValue, newKey = predicate(value, key) + newKey = newKey or key + newValue = newValue or value + new:_insertPairInPlace(newKey, newValue) + end + + return new +end + +--[[ + Create a new UnorderedMap, where each key-value pair is included iff callback(value, key) is truthy. + + callback should be a function of signature + callback(value, key) -> bool +]] +function UnorderedMap:filter(callback) + local new = UnorderedMap.new() + for key, value in self:iterator() do + if callback(value, key) then + new:_insertPairInPlace(key, value) + end + end + + return new +end + +--[[ + Join any number of UnorderedMaps. +]] +function UnorderedMap:join(...) + local new = self:copyRemoveNone() + + for i = 1, select("#", ...) do + local other = select(i, ...) + + if other:size() > 0 then + for key, value in other:iterator() do + new:_insertPairInPlaceRemoveNone(key, value) + end + end + end + + return new +end + +--[[ + Internal method for inserting values in place. +]] +function UnorderedMap:_insertInPlace(...) + local len = select("#", ...) + for i = 1, len do + local pair = select(i, ...) + for key, value in pairs(pair) do + self:_insertPairInPlace(key, value) + end + end +end + +--[[ + Internal method for inserting a single pair in place. +]] +function UnorderedMap:_insertPairInPlace(key, value) + if self:get(key) == nil then + self.length = self.length + 1 + end + self.pairs[key] = value +end + +--[[ + Internal method for inserting values in place, removing all None instances. +]] +function UnorderedMap:_insertInPlaceRemoveNone(...) + local len = select("#", ...) + for i = 1, len do + local pair = select(i, ...) + for key, value in pairs(pair) do + self:_insertPairInPlaceRemoveNone(key, value) + end + end +end + +--[[ + Internal method for inserting values in place, removing all None instances. +]] +function UnorderedMap:_insertPairInPlaceRemoveNone(key, value) + if value == None then + if self:get(key) ~= nil then + self.pairs[key] = nil + self.length = self.length - 1 + end + else + if self:get(key) == nil then + self.length = self.length + 1 + end + self.pairs[key] = value + end +end + +--[[ + Specify the behavior of deepJoin for UnorderedMap. +]] +function UnorderedMap:deepJoin(rhs) + local newMap = UnorderedMap.new() + for lhsKey, lhsValue in self:iterator() do + local rhsValue = rhs:get(lhsKey) + if rhsValue then + if type(rhsValue) == "table" and type(lhsValue) == "table" then + newMap:_insertPairInPlace(lhsKey, deepJoin(lhsValue, rhsValue)) + else + if rhsValue ~= None then + newMap:_insertPairInPlace(lhsKey, rhsValue) + end + end + else + if lhsValue ~= None then + newMap:_insertPairInPlace(lhsKey, lhsValue) + end + end + end + + -- Copy over rhs keys that aren't in lhs + for rhsKey, rhsValue in rhs:iterator() do + local lhsValue = self:get(rhsKey) + if not lhsValue and rhsValue ~= None then + newMap:_insertPairInPlace(rhsKey, rhsValue) + end + end + + return newMap +end + +--[[ + Returns an iterator for the UnorderedMap. + Key-value pairs are returned in an undefined order. + Example: + for key, value in unorderedMap:iterator() do +]] +function UnorderedMap:iterator() + return next, self.pairs, nil +end + +return UnorderedMap \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedMap/UnorderedMap.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedMap/UnorderedMap.spec.lua new file mode 100644 index 0000000..6b4d8b1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedMap/UnorderedMap.spec.lua @@ -0,0 +1,476 @@ +return function() + local Root = script.Parent.Parent + local UnorderedMap = require(script.Parent.UnorderedMap) + local None = require(Root.None) + + describe("Basic unordered map operations", function() + + it("Creates a new empty table", function() + local map = UnorderedMap.new() + expect(map).to.be.ok() + expect(map:size()).to.equal(0) + end) + + it("Can tell apart UnorderedMaps from non-UnorderedMaps", function() + local nonmap = {} + expect(UnorderedMap.is(nonmap)).to.equal(false) + local number = 5 + expect(UnorderedMap.is(number)).to.equal(false) + local map = UnorderedMap.new() + expect(UnorderedMap.is(map)).to.equal(true) + end) + + it("Creates an ordered with some values", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + expect(map).to.be.ok() + expect(map:size()).to.equal(3) + expect(map:get("apple")).to.equal(1) + expect(map:get("grapes")).to.equal(2) + expect(map:get("banana")).to.equal(10) + end) + + it("Returns nil on a miss", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + expect(map:get("broccoli")).never.to.be.ok() + end) + + it("Creates a table with some values from multiple tables", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 10, + banana = 2, + }, { + orange = 5, + banana = 5, + }) + expect(map).to.be.ok() + expect(map:size()).to.equal(4) + expect(map:get("orange")).to.equal(5) + expect(map:get("grapes")).to.equal(10) + expect(map:get("banana")).to.equal(5) + end) + + it("Can create a copy", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + local mapCopy = map:copy() + + expect(mapCopy).never.to.equal(map) + expect(mapCopy).to.be.ok() + expect(mapCopy:size()).to.equal(3) + expect(mapCopy:get("apple")).to.equal(1) + expect(mapCopy:get("grapes")).to.equal(2) + expect(mapCopy:get("banana")).to.equal(10) + end) + + it("Can be converted immutably to lists of keys and values", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + local keys = map:getKeys() + local values = map:getValues() + + expect(keys:find("apple")).to.be.ok() + expect(keys:find("banana")).to.be.ok() + expect(keys:find("grapes")).to.be.ok() + expect(keys:size()).to.equal(3) + expect(values:find(1)).to.be.ok() + expect(values:find(2)).to.be.ok() + expect(values:find(10)).to.be.ok() + expect(values:size()).to.equal(3) + end) + + it("Supports immutable set", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + + local newMap = map:set("apple", 1000) + + expect(newMap).never.to.equal(map) + expect(map:get("apple")).to.equal(1) + expect(newMap:get("apple")).to.equal(1000) + + local newNewMap = newMap:set("lettuce", -100) + expect(newNewMap:get("lettuce")).to.equal(-100) + end) + + it("Supports batch set", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + + local newMap = map:batchSet({ + apple = 100, + watermelon = 4, + }, { + watermelon = 100, + kiwi = 3, + }) + + expect(newMap).never.to.equal(map) + expect(map:get("apple")).to.equal(1) + expect(newMap:get("apple")).to.equal(100) + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect(map:get("kiwi")).to.never.be.ok() + expect(newMap:size()).to.equal(5) + end) + + it("Supports join", function() + local map1 = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + + local map2 = UnorderedMap.new({ + apple = 100, + watermelon = 4, + }) + + local map3 = UnorderedMap.new({ + watermelon = 100, + kiwi = 3, + }) + + local newMap = map1:join(map2, map3) + + expect(newMap).never.to.equal(map1) + expect(map1:get("apple")).to.equal(1) + expect(newMap:get("apple")).to.equal(100) + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect(map1:get("kiwi")).to.never.be.ok() + expect(newMap:size()).to.equal(5) + end) + + it("Supports None", function() + local map1 = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + useless = None, + }) + expect(map1:size()).to.equal(4) + + local map2 = UnorderedMap.new({ + apple = None, + watermelon = 4, + }) + + local map3 = UnorderedMap.new({ + watermelon = 100, + kiwi = 3, + }) + + local newMap = map1:join(map2, map3) + + expect(newMap).never.to.equal(map1) + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect(newMap:get("useless")).to.never.be.ok() + expect(newMap:size()).to.equal(4) + + newMap = map1:batchSet({ + apple = None, + watermelon = 4, + }, { + watermelon = 100, + kiwi = 3, + }) + + expect(newMap).never.to.equal(map1) + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect(newMap:get("useless")).to.never.be.ok() + expect(newMap:size()).to.equal(4) + end) + + + it("Supports deletion", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + local newMap = map:remove("apple", "grapes") + + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("grapes")).to.never.be.ok() + expect(newMap:get("banana")).to.equal(10) + expect(map:get("apple")).to.be.ok() + expect(map:get("grapes")).to.be.ok() + expect(map:get("banana")).to.be.ok() + expect(newMap:size()).to.equal(1) + end) + end) + + describe("More advanced functionality", function() + describe("Mapping", function() + it("should use the callback", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + + local newMap = map:map(function(value, key) + return value * 2, key .. " fruit" + end) + + expect(newMap).never.to.equal(map) + expect(newMap:size()).to.equal(3) + expect(map:get("apple fruit")).to.never.be.ok() + expect(newMap:get("apple fruit")).to.equal(2) + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("banana fruit")).to.equal(20) + end) + + it("should work with an empty UnorderedMap", function() + local called = false + local function callback() + called = true + return "placeholderkey", "placeholdervalue" + end + local map = UnorderedMap.new() + local newMap = map:map(callback) + expect(newMap:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Filtering", function() + it("should use the callback", function() + local map = UnorderedMap.new({ + orange = 100, + apple = 1, + grapes = 2, + banana = 10, + lemons = 4, + }) + + local newMap = map:filter(function(value, key) + return value < 9 or key == "orange" + end) + + expect(newMap).never.to.equal(map) + expect(newMap:size()).to.equal(4) + expect(newMap:get("apple")).to.equal(1) + expect(newMap:get("banana")).to.never.be.ok() + expect(map:get("banana")).to.equal(10) + expect(newMap:get("orange")).to.equal(100) + end) + + it("should work with an empty OrderedMap", function() + local called = false + local function callback() + called = true + return true + end + local map = UnorderedMap.new() + local newMap = map:filter(callback) + expect(newMap:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Iterators", function() + it("Should work in a for loop", function() + local map = UnorderedMap.new({ + orange = 100, + apple = 1, + grapes = 2, + banana = 10, + lemons = 4, + }) + local count = 0 + for key, value in map:iterator() do + if key == "orange" then + expect(value).to.equal(100) + elseif key == "apple" then + expect(value).to.equal(1) + elseif key == "grapes" then + expect(value).to.equal(2) + elseif key == "banana" then + expect(value).to.equal(10) + elseif key == "lemons" then + expect(value).to.equal(4) + else + error("There should not be such a key") + end + count = count + 1 + end + expect(count).to.equal(5) + end) + + it("Should work for empty maps", function() + local map = UnorderedMap.new() + local doAnything = false + for _, _ in map:iterator() do + doAnything = true + end + expect(doAnything).to.equal(false) + end) + end) + end) + + describe("deepJoin", function() + it("Should work with basic maps", function() + local map1 = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + + local map2 = UnorderedMap.new({ + apple = 100, + watermelon = 4, + }) + + local newMap = map1:deepJoin(map2) + + expect(newMap:get("apple")).to.equal(100) + expect(newMap:get("grapes")).to.equal(2) + expect(newMap:get("banana")).to.equal(10) + expect(newMap:get("watermelon")).to.equal(4) + + expect(newMap:size()).to.equal(4) + end) + + it("Should work with nested maps", function() + local innerMap1 = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + + local innerMap2 = UnorderedMap.new({ + peach = -10, + grapes = 15 + }) + + local innerMap3 = UnorderedMap.new({ + apple = 100, + banana = 1000, + beans = 400, + }) + + local innerMap4 = UnorderedMap.new({ + peach = 30, + }) + + local outerMap1 = UnorderedMap.new({ + inner1 = innerMap1, + inner2 = innerMap2, + irrelevantInteger = 5, + }) + + local outerMap2 = UnorderedMap.new({ + inner1 = innerMap3, + inner2 = innerMap4, + irrelevantString = "hello!", + }) + + local newMap = outerMap1:deepJoin(outerMap2) + expect(newMap:size()).to.equal(4) + expect(newMap:get("irrelevantInteger")).to.equal(5) + expect(newMap:get("irrelevantString")).to.equal("hello!") + + local mergedInner1 = newMap:get("inner1") + local mergedInner2 = newMap:get("inner2") + expect(mergedInner1:size()).to.equal(4) + expect(mergedInner1:get("apple")).to.equal(100) + expect(mergedInner1:get("grapes")).to.equal(2) + expect(mergedInner1:get("banana")).to.equal(1000) + expect(mergedInner1:get("beans")).to.equal(400) + + expect(mergedInner2:size()).to.equal(2) + expect(mergedInner2:get("peach")).to.equal(30) + expect(mergedInner2:get("grapes")).to.equal(15) + end) + + it("Should work with an empty map", function() + local map1 = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + + local map2 = UnorderedMap.new() + local newMap = map1:deepJoin(map2) + + expect(newMap:get("apple")).to.equal(1) + expect(newMap:get("grapes")).to.equal(2) + expect(newMap:get("banana")).to.equal(10) + end) + + it("Should work with None", function() + local innerMap1 = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + useless = None, + }) + + local innerMap2 = UnorderedMap.new({ + peach = -10, + grapes = 15, + }) + + local innerMap3 = UnorderedMap.new({ + apple = None, + banana = 1000, + beans = 400, + }) + + local innerMap4 = UnorderedMap.new({ + grapes = None, + peach = 30, + }) + + local outerMap1 = UnorderedMap.new({ + inner1 = innerMap1, + inner2 = innerMap2, + irrelevantInteger = 5, + }) + + local outerMap2 = UnorderedMap.new({ + inner1 = innerMap3, + inner2 = innerMap4, + irrelevantInteger = None, + }) + + local newMap = outerMap1:deepJoin(outerMap2) + expect(newMap:size()).to.equal(2) + expect(newMap:get("irrelevantInteger")).to.never.be.ok() + + local mergedInner1 = newMap:get("inner1") + local mergedInner2 = newMap:get("inner2") + expect(mergedInner1:size()).to.equal(3) + expect(mergedInner1:get("apple")).to.never.be.ok() + + expect(mergedInner2:size()).to.equal(1) + expect(mergedInner2:get("grapes")).to.never.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedSet/UnorderedSet.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedSet/UnorderedSet.lua new file mode 100644 index 0000000..9456fa2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedSet/UnorderedSet.lua @@ -0,0 +1,216 @@ +local Root = script.Parent.Parent +local UnorderedMap = require(Root.UnorderedMap.UnorderedMap) + +local UnorderedSet = {} + +UnorderedSet.__index = UnorderedSet + +--[[ + Create a new UnorderedSet from any number of keys +]] +function UnorderedSet.new(...) + local keyMap = UnorderedSet._unpackedToTrueMap(...) + + local self = { + internalMap = UnorderedMap.new(keyMap), + _immutableDataStructureType = UnorderedSet, + } + + setmetatable(self, UnorderedSet) + + return self +end + +--[[ + Internal method that absorbs an existing Unorderedmap as the underlying data structure for a + new UnorderedSet. +]] +function UnorderedSet._newCannibalizeMap(unorderedMap) + local self = { + internalMap = unorderedMap, + _immutableDataStructureType = UnorderedSet, + } + + setmetatable(self, UnorderedSet) + + return self +end + +--[[ + Internal method that takes an unpacked list of values (...) and transforms it into a table + in which each value is a key with value true. +]] +function UnorderedSet._unpackedToTrueMap(...) + local keyMap = {} + local len = select("#", ...) + for i = 1, len do + keyMap[select(i, ...)] = true + end + return keyMap +end + +--[[ + Returns if a value is an UnorderedSet. +]] +function UnorderedSet.is(value) + if type(value) ~= "table" then + return false + end + return value._immutableDataStructureType == UnorderedSet +end + +--[[ + Returns true if a key is in the set, false otherwise. +]] +function UnorderedSet:find(id) + return self.internalMap:get(id) ~= nil +end + +--[[ + Returns a new UnorderedSet, inserting a number of values into the set. +]] +function UnorderedSet:insert(...) + local keyMap = UnorderedSet._unpackedToTrueMap(...) + local newMap = self.internalMap:batchSet(keyMap) + return UnorderedSet._newCannibalizeMap(newMap) +end + +--[[ + Returns a copy of the table of all of the keys in the map. +]] +function UnorderedSet:getKeys() + return self.internalMap:getKeys() +end + +--[[ + Return the number of keys in the set. +]] +function UnorderedSet:size() + return self.internalMap:size() +end + +--[[ + Returns a new UnorderedSet, removing a number of values into the set. +]] +function UnorderedSet:remove(...) + local newMap = self.internalMap:remove(...) + return UnorderedSet._newCannibalizeMap(newMap) +end + +--[[ + Returns a (shallow) copy of the UnorderedSet. +]] +function UnorderedSet:copy() + local newMap = self.internalMap:copy() + return UnorderedSet._newCannibalizeMap(newMap) +end + +--[[ + Create a new UnorderedSet, applying the given predicate to each element in + this OrderedSet. + + Predicate should have the signature + predicate(key) -> newKey + + Analogous to 'map' on a list. +]] +function UnorderedSet:map(predicate) + local alteredPredicate = function(_, key) + return nil, predicate(key) + end + local newMap = self.internalMap:map(alteredPredicate) + return UnorderedSet._newCannibalizeMap(newMap) +end + +--[[ + Create a new OrderedSet, where each key is included iff callback(key) is truthy. + + callback should be a function of signature + callback(key) -> bool +]] +function UnorderedSet:filter(callback) + local alteredCallback = function(_, key) + return callback(key) + end + local newMap = self.internalMap:filter(alteredCallback) + return UnorderedSet._newCannibalizeMap(newMap) +end + +--[[ + Union any number of UnorderedSets. +]] +function UnorderedSet:union(...) + local internalMaps = {} + local len = select("#", ...) + for i = 1, len do + internalMaps[i] = select(i, ...).internalMap + end + local newMap = self.internalMap:join(unpack(internalMaps)) + return UnorderedSet._newCannibalizeMap(newMap) +end + +--[[ + Find the difference between any number UnorderedSets. + The behavior of A:difference(B, C, D) is equivalent to the behavior of + A:difference(B:union(C, D)). +]] +function UnorderedSet:difference(...) + local newSet = UnorderedSet.new() + local len = select("#", ...) + for key in self:iterator() do + local shouldRemain = true + for i = 1, len do + if select(i, ...):find(key) then + shouldRemain = false + break + end + end + if shouldRemain then + newSet.internalMap:_insertPairInPlace(key, true) + end + end + return newSet +end + + +--[[ + Intersect any number of UnorderedSets. +]] +function UnorderedSet:intersection(...) + local len = select("#", ...) + local args = { ... } + + local filterer = function(key) + for i = 1, len do + local set = args[i] + if not set:find(key) then + return false + end + end + return true + end + + return self:filter(filterer) +end + +--[[ + Returns an iterator for the UnorderedSet. + Keys are returned in an undefined order. + Example: + for key in orderedSet:iterator() do +]] +function UnorderedSet:iterator() + local alteredNext = function(table, key) + return (next(table, key)) + end + return alteredNext, self.internalMap.pairs +end + +--[[ + Specify the behavior of deepJoin for UnorderedSet. +]] +UnorderedSet.deepJoin = UnorderedSet.union + +UnorderedSet.join = UnorderedSet.union + +return UnorderedSet \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedSet/UnorderedSet.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedSet/UnorderedSet.spec.lua new file mode 100644 index 0000000..ece985d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedSet/UnorderedSet.spec.lua @@ -0,0 +1,255 @@ +return function() + local UnorderedSet = require(script.Parent.UnorderedSet) + describe("Basic ordered map operations", function() + + it("Creates a new empty set", function() + local set = UnorderedSet.new() + expect(set).to.be.ok() + expect(set:size()).to.equal(0) + end) + + it("Can tell apart UnorderedSets from non-UnorderedSets", function() + local nonset = {} + expect(UnorderedSet.is(nonset)).to.equal(false) + local number = 5 + expect(UnorderedSet.is(number)).to.equal(false) + local set = UnorderedSet.new() + expect(UnorderedSet.is(set)).to.equal(true) + end) + + it("Creates an unordered set with some values", function() + local set = UnorderedSet.new(3, 2, 1, 3, 4) + expect(set).to.be.ok() + expect(set:size()).to.equal(4) + expect(set:find(1)).to.equal(true) + expect(set:find(2)).to.equal(true) + expect(set:find(3)).to.equal(true) + expect(set:find(4)).to.equal(true) + end) + + it("Returns nil on a miss", function() + local set = UnorderedSet.new(3, 2, 1, 3, 4) + expect(set:find(10000)).to.equal(false) + end) + + it("Can create a copy", function() + local set = UnorderedSet.new(3, 2, 1, 3, 4) + local setCopy = set:copy() + + expect(setCopy).never.to.equal(set) + expect(setCopy).to.be.ok() + expect(setCopy:size()).to.equal(4) + expect(setCopy:find(1)).to.equal(true) + expect(setCopy:find(2)).to.equal(true) + expect(setCopy:find(3)).to.equal(true) + expect(setCopy:find(4)).to.equal(true) + end) + + it("Can be converted immutably to a List of keys", function() + local set = UnorderedSet.new(10, 11, 11, 10, 15) + local keys = set:getKeys() + expect(keys:size()).to.equal(3) + end) + + it("Supports immutable insert", function() + local set = UnorderedSet.new(10, 11, 11, 10, 15) + + local newSet = set:insert(12) + expect(newSet).never.to.equal(set) + expect(newSet:find(12)).to.equal(true) + expect(newSet:size()).to.equal(4) + + expect(set:size()).to.equal(3) + expect(set:find(12)).to.equal(false) + + local newNewSet = newSet:insert(100, -100, 25, 10, 100) + expect(newNewSet:size()).to.equal(7) + end) + + it("Supports union", function() + local set1 = UnorderedSet.new(4, 3, 2, 1) + + local set2 = UnorderedSet.new(3, 4, 5, 6) + + local set3 = UnorderedSet.new(3, 6, 100) + + local newSet = set1:union(set2, set3) + + expect(newSet).never.to.equal(set1) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(5)).to.equal(true) + expect(newSet:find(6)).to.equal(true) + expect(newSet:find(100)).to.equal(true) + expect(set1:size()).to.equal(4) + expect(newSet:size()).to.equal(7) + + local emptySet = UnorderedSet.new() + local newNewSet = newSet:union(emptySet) + expect(newNewSet:size()).to.equal(7) + end) + + it("Supports deletion", function() + local set = UnorderedSet.new(3, 2, 1, 4, 5, 6) + local newSet = set:remove(2, 4) + + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(false) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(false) + expect(newSet:find(5)).to.equal(true) + expect(newSet:find(6)).to.equal(true) + expect(newSet:size()).to.equal(4) + end) + + it("Supports intersection", function() + local set1 = UnorderedSet.new(4, 3, 2, 1) + + local set2 = UnorderedSet.new(3, 4, 5, 6, 1) + + local set3 = UnorderedSet.new(3, 6, 100, 1) + + local newSet = set1:intersection(set2, set3) + + expect(newSet).never.to.equal(set1) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(false) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(false) + expect(newSet:find(5)).to.equal(false) + expect(newSet:find(6)).to.equal(false) + expect(newSet:find(100)).to.equal(false) + expect(set1:size()).to.equal(4) + expect(newSet:size()).to.equal(2) + + local emptySet = UnorderedSet.new() + local newNewSet = newSet:intersection(emptySet) + expect(newNewSet:size()).to.equal(0) + end) + + it("Supports set difference", function() + local set1 = UnorderedSet.new(10, 4, 3, 2, 1) + + local set2 = UnorderedSet.new(3, 4, 5) + + local set3 = UnorderedSet.new(100, 101) + + local newSet = set1:difference(set2, set3) + + expect(newSet).never.to.equal(set1) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(10)).to.equal(true) + expect(newSet:find(3)).to.equal(false) + expect(newSet:find(4)).to.equal(false) + expect(newSet:find(5)).to.equal(false) + expect(newSet:find(100)).to.equal(false) + expect(newSet:find(100)).to.equal(false) + expect(newSet:find(101)).to.equal(false) + expect(set1:size()).to.equal(5) + expect(newSet:size()).to.equal(3) + + local emptySet = UnorderedSet.new() + local newNewSet = set1:difference(emptySet) + expect(newNewSet:size()).to.equal(5) + end) + end) + + describe("More advanced functionality", function() + describe("Mapping", function() + it("should use the callback", function() + local set = UnorderedSet.new(4, 3, 2, 1) + + local newSet = set:map(function(key) + return key * 2 + end) + + expect(newSet:find(1)).to.equal(false) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(6)).to.equal(true) + expect(newSet:find(8)).to.equal(true) + expect(newSet:size()).to.equal(4) + end) + + it("should work with an empty UnorderedSet", function() + local called = false + local function callback() + called = true + return "placeholderkey" + end + local set = UnorderedSet.new() + local newSet = set:map(callback) + expect(newSet:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Filtering", function() + it("should use the callback", function() + local set = UnorderedSet.new(4, 3, 2, 1) + local newSet = set:filter(function(key) + return key >= 3 + end) + + expect(newSet).never.to.equal(set) + + expect(newSet:size()).to.equal(2) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(1)).to.equal(false) + expect(newSet:find(2)).to.equal(false) + end) + + it("should work with an empty UnorderedSet", function() + local called = false + local function callback() + called = true + return true + end + local set = UnorderedSet.new() + local newSet = set:filter(callback) + expect(newSet:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Iterators", function() + it("Should work in a for loop", function() + local set = UnorderedSet.new(8, 3, 2, 1, 100) + local seen = {} + local count = 0 + for key in set:iterator() do + seen[key] = true + count = count + 1 + end + expect(count).to.equal(5) + expect(seen[8]).to.equal(true) + expect(seen[3]).to.equal(true) + expect(seen[2]).to.equal(true) + expect(seen[1]).to.equal(true) + expect(seen[100]).to.equal(true) + end) + + it("Should work for empty sets", function() + local set = UnorderedSet.new() + local doAnything = false + for _ in set:iterator() do + doAnything = true + end + expect(doAnything).to.equal(false) + end) + + it("Should only provide keys", function() + local set = UnorderedSet.new(1, 2) + for key, value in set:iterator() do + expect(value).to.never.be.ok() + expect(key).to.be.ok() + end + end) + end) + + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/binarySearch/binarySearch.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/binarySearch/binarySearch.lua new file mode 100644 index 0000000..faeafef --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/binarySearch/binarySearch.lua @@ -0,0 +1,29 @@ +--[[ + Utility that returns the leftmost index of the occurence of value in a sorted list according to sortPredicate, + if provided, otherwise <. If not found, returns nil. +]] + +local function binarySearch(list, value, sortPredicate) + sortPredicate = sortPredicate or function(lhs, rhs) + return lhs < rhs + end + local low = 1 + local high = #list + while low <= high do + local mid = low + math.floor((high - low) / 2) + if sortPredicate(value, list[mid]) then + high = mid - 1 + elseif sortPredicate(list[mid], value) then + low = mid + 1 + else + -- Go as left as we can + while mid >= 1 and not (sortPredicate(list[mid], value) or sortPredicate(value, list[mid])) do + mid = mid - 1 + end + return mid + 1 + end + end + return nil +end + +return binarySearch \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/binarySearch/binarySearch.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/binarySearch/binarySearch.spec.lua new file mode 100644 index 0000000..4f00380 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/binarySearch/binarySearch.spec.lua @@ -0,0 +1,38 @@ +return function() + local binarySearch = require(script.Parent.binarySearch) + it("Works for a sorted list", function() + local list = { 1, 3, 5, 6, 7 } + expect(binarySearch(list, 1)).to.equal(1) + expect(binarySearch(list, 3)).to.equal(2) + expect(binarySearch(list, 5)).to.equal(3) + expect(binarySearch(list, 6)).to.equal(4) + expect(binarySearch(list, 7)).to.equal(5) + expect(binarySearch(list, 100)).to.never.be.ok() + end) + + it("Works for a sorted list with duplicates", function() + local list = { 1, 1, 3, 3, 3, 5, 5, 10, 11 } + expect(binarySearch(list, 1)).to.equal(1) + expect(binarySearch(list, 3)).to.equal(3) + expect(binarySearch(list, 5)).to.equal(6) + expect(binarySearch(list, 10)).to.equal(8) + expect(binarySearch(list, 11)).to.equal(9) + end) + + it("Works for an empty list", function() + local list = {} + expect(binarySearch(list, 1)).to.never.be.ok() + end) + + it("Works for a special comparator", function() + local list = { 7, 6, 5, 5, 3, 1 } + local reverse = function(lhs, rhs) + return rhs < lhs + end + expect(binarySearch(list, 7, reverse)).to.equal(1) + expect(binarySearch(list, 6, reverse)).to.equal(2) + expect(binarySearch(list, 5, reverse)).to.equal(3) + expect(binarySearch(list, 3, reverse)).to.equal(5) + expect(binarySearch(list, 1, reverse)).to.equal(6) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/deepJoin/deepJoin.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/deepJoin/deepJoin.lua new file mode 100644 index 0000000..ea98905 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/deepJoin/deepJoin.lua @@ -0,0 +1,50 @@ +local Root = script.Parent.Parent +local Cryo = require(Root.Parent.Cryo) +local None = require(Root.None) +local _deepJoinHelper + +--[[ + deepJoin deeply merges any number of immutable data structures. + It conforms to the following behavior: + 1. UnorderedMaps and OrderedMaps are merged with pairs in later maps overwriting pairs in earlier maps. Values + are recursively deepJoined. None is respected. UnorderedMaps will not be joined with OrderedMaps, and vice versa. + 2. UnorderedSets and OrderedSets are unioned. UnorderedSets will not be unioned with OrderedSets, and vice versa. + 3. Lists are joined (i.e. appended) to one another according to List:join(). + 4. Joining two differently typed (i.e. OrderedSet and UnorderedSet) immutable data structures will return the + RHS immutable data structure. + 5. All other values are overwritten, taking the furthest right value. +]] +local function deepJoin(...) + local values = { ... } + if #values == 0 then + return nil + end + return Cryo.List.foldLeft(values, function(accumulator, value, index) + if index == 1 then + return accumulator + end + return _deepJoinHelper(accumulator, value) + end, values[1]) +end + +function _deepJoinHelper(table1, table2) + if table2 == None then + return nil + end + if type(table1) ~= type(table2) or type(table1) ~= "table" then + return table2 + end + + if getmetatable(table1) == getmetatable(table2) and type(table1.deepJoin) == "function" then + if type(table1.deepJoin) == "function" then + return table1:deepJoin(table2) + else + warn([[deepJoining two tables with the same metatable, but not implementing deepJoin. + Overriding with rightmost table.]]) + end + end + + return table2 +end + +return deepJoin \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/deepJoin/deepJoin.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/deepJoin/deepJoin.spec.lua new file mode 100644 index 0000000..29126a9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/deepJoin/deepJoin.spec.lua @@ -0,0 +1,267 @@ +return function() + local Root = script.Parent.Parent + local OrderedMap = require(Root.OrderedMap.OrderedMap) + local UnorderedMap = require(Root.UnorderedMap.UnorderedMap) + local OrderedSet = require(Root.OrderedSet.OrderedSet) + local UnorderedSet = require(Root.UnorderedSet.UnorderedSet) + local List = require(Root.List.List) + local None = require(Root.None) + local sort = require(Root.sort.sort) + + local deepJoin = require(script.Parent.deepJoin) + + describe("deepJoin", function() + it("Should return nil when called on no values", function() + expect(deepJoin()).to.never.be.ok() + end) + + it("Should work with basic types", function() + expect(deepJoin(1, 2)).to.equal(2) + local table1 = { + key1 = "value1", + key2 = "value2" + } + local table2 = { + key1 = "adifferentvalue1", + key3 = "value3" + } + local newtable = deepJoin(table1, table2) + expect(newtable).to.equal(table2) + end) + + it("Should work with OrderedSet", function() + local set1 = OrderedSet.new(sort.default, 4, 3, 2, 1) + local set2 = OrderedSet.new(sort.default, 3, 5) + local newSet = deepJoin(set1, set2) + expect(OrderedSet.is(newSet)).to.equal(true) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(5)).to.equal(true) + expect(newSet:size()).to.equal(5) + expect(newSet:first()).to.equal(1) + expect(newSet:last()).to.equal(5) + end) + + it("Should work with UnorderedSet", function() + local set1 = UnorderedSet.new(4, 3, 2, 1) + local set2 = UnorderedSet.new(3, 5) + local newSet = deepJoin(set1, set2) + expect(UnorderedSet.is(newSet)).to.equal(true) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(5)).to.equal(true) + expect(newSet:size()).to.equal(5) + end) + + it("Should work with Lists", function() + local list1 = List.new(5, 4, 3, 2, 1) + local list2 = List.new(100, 101) + local newList = deepJoin(list1, list2) + expect(List.is(newList)).to.equal(true) + expect(newList:size()).to.equal(7) + expect(newList:get(1)).to.equal(5) + expect(newList:get(newList:size())).to.equal(101) + end) + + it("Should work for some crazy nested maps", function() + local outermostMap1 = UnorderedMap.new({ + innerMap1 = UnorderedMap.new({ + innerList = List.new(3, 4, 4), + innerObject = { + key1 = "value1", + key2 = "value2" + } + }), + innerMap2 = OrderedMap.new(sort.reverse, { + apple = 1, + orange = 100, + }), + differentlyTypedStructure = UnorderedSet.new(3, 4, 5), + irrelevantValue = -1000, + }) + + local outermostMap2 = UnorderedMap.new({ + innerMap1 = UnorderedMap.new({ + innerList = List.new(1000), + innerObject = { + key1 = "differentvalue1", + key3 = "value3" + } + }), + innerMap2 = OrderedMap.new(sort.default, { + kiwi = 3, + orange = -5, + }), + differentlyTypedStructure = List.new(3, 4, 5), + irrelevantValue = 1000, + anotherIrrelvantValue = 50, + }) + + local mergedMap = deepJoin(outermostMap1, outermostMap2) + expect(UnorderedMap.is(mergedMap)).to.equal(true) + expect(mergedMap:size()).to.equal(5) + + expect(mergedMap:get("irrelevantValue")).to.equal(1000) + expect(mergedMap:get("anotherIrrelvantValue")).to.equal(50) + + local differentlyTypedStructure = mergedMap:get("differentlyTypedStructure") + expect(List.is(differentlyTypedStructure)).to.equal(true) + expect(differentlyTypedStructure:get(2)).to.equal(4) + + local innerMap1 = mergedMap:get("innerMap1") + local innerMap2 = mergedMap:get("innerMap2") + expect(UnorderedMap.is(innerMap1)).to.equal(true) + expect(OrderedMap.is(innerMap2)).to.equal(true) + local innerList = innerMap1:get("innerList") + local innerObject = innerMap1:get("innerObject") + + expect(List.is(innerList)).to.equal(true) + expect(innerList:size()).to.equal(4) + expect(innerList:get(1)).to.equal(3) + expect(innerList:get(2)).to.equal(4) + expect(innerList:get(3)).to.equal(4) + expect(innerList:get(4)).to.equal(1000) + + expect(innerObject["key1"]).to.equal("differentvalue1") + expect(innerObject["key2"]).to.never.be.ok() + expect(innerObject["key3"]).to.equal("value3") + + expect(OrderedMap.is(innerMap2)).to.equal(true) + expect(innerMap2:get("apple")).to.equal(1) + expect(innerMap2:get("orange")).to.equal(-5) + expect(innerMap2:get("kiwi")).to.equal(3) + expect((innerMap2:first())).to.equal("orange") + expect((innerMap2:last())).to.equal("apple") + end) + + it("Should work for a reasonable case", function() + local state = UnorderedMap.new({ + [100] = UnorderedMap.new({ + username = "Cool username 1", + information = { + settings = "example 1", + }, + }), + [105] = UnorderedMap.new({ + username = "Cool username 2", + information = { + settings = "example 2", + }, + }), + [110] = UnorderedMap.new({ + username = "Cool username 3", + information = { + settings = "example 3", + }, + }), + [111] = UnorderedMap.new({ + username = "Cool username 4", + information = { + settings = "example 4", + }, + }), + }) + + local actionData = UnorderedMap.new({ + [100] = UnorderedMap.new({ + username = "Updated cool username 1", + information = { + settings = "updated example 1", + }, + }), + [110] = UnorderedMap.new({ + username = "Updated cool username 3", + information = { + settings = "updated example 3", + }, + }), + }) + + local newState = deepJoin(state, actionData) + expect(newState:size()).to.equal(4) + expect(newState:get(105)).to.equal(state:get(105)) + expect(newState:get(111)).to.equal(state:get(111)) + expect(newState:get(100)).never.to.equal(state:get(100)) + expect(newState:get(110)).never.to.equal(state:get(110)) + local user100 = newState:get(100) + local user110 = newState:get(110) + expect(user100:get("username")).to.equal("Updated cool username 1") + expect(user110:get("username")).to.equal("Updated cool username 3") + expect(user100:get("information")["settings"]).to.equal("updated example 1") + expect(user110:get("information")["settings"]).to.equal("updated example 3") + end) + + describe("Multiple joins and None", function() + it("Works in basic cases", function() + expect(deepJoin(1, 2, 3)).to.equal(3) + local data1 = { + name = "Jim", + age = 22 + } + local data2 = { + name = "James" + } + local data3 = { + birthday = 11 + } + local newData = deepJoin(data1, data2, data3) + expect(newData).to.equal(data3) + end) + + it("Should work with OrderedSet", function() + local set1 = OrderedSet.new(sort.default, 4, 3, 2, 1) + local set2 = OrderedSet.new(sort.default, 3, 5) + local set3 = OrderedSet.new(sort.default, 100, 4) + local newSet = deepJoin(set1, set2, set3) + expect(OrderedSet.is(newSet)).to.equal(true) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(5)).to.equal(true) + expect(newSet:find(100)).to.equal(true) + expect(newSet:size()).to.equal(6) + expect(newSet:first()).to.equal(1) + expect(newSet:last()).to.equal(100) + end) + + it("Should work with UnorderedSet", function() + local set1 = UnorderedSet.new(4, 3, 2, 1) + local set2 = UnorderedSet.new(3, 5) + local set3 = UnorderedSet.new(100, 4) + local newSet = deepJoin(set1, set2, set3) + expect(UnorderedSet.is(newSet)).to.equal(true) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(5)).to.equal(true) + expect(newSet:find(100)).to.equal(true) + expect(newSet:size()).to.equal(6) + end) + + it("Should work with Lists", function() + local list1 = List.new(5, 4, 3, 2, 1) + local list2 = List.new(100, 101) + local list3 = List.new(1000, 1001) + local newList = deepJoin(list1, list2, list3) + expect(List.is(newList)).to.equal(true) + expect(newList:size()).to.equal(9) + expect(newList:get(1)).to.equal(5) + expect(newList:get(6)).to.equal(100) + expect(newList:get(newList:size())).to.equal(1001) + end) + + describe("Working with None", function() + it("Should work in basic cases", function() + local result = deepJoin(5, None) + expect(result).to.never.be.ok() + end) + end) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/init.lua new file mode 100644 index 0000000..955714f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/init.lua @@ -0,0 +1,10 @@ +return { + List = require(script.List.List), + sort = require(script.sort.sort), + OrderedMap = require(script.OrderedMap.OrderedMap), + OrderedSet = require(script.OrderedSet.OrderedSet), + UnorderedMap = require(script.UnorderedMap.UnorderedMap), + UnorderedSet = require(script.UnorderedSet.UnorderedSet), + deepJoin = require(script.deepJoin.deepJoin), + None = require(script.None), +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/init.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/init.spec.lua new file mode 100644 index 0000000..ff7e8e3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/init.spec.lua @@ -0,0 +1,5 @@ +return function() + it("should load", function() + require(script.Parent) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/sort/sort.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/sort/sort.lua new file mode 100644 index 0000000..b1b74b5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/sort/sort.lua @@ -0,0 +1,10 @@ +local sort = { + default = function(key1, key2) + return key1 < key2 + end, + reverse = function(key1, key2) + return key2 < key1 + end, +} + +return sort \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/sort/sort.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/sort/sort.spec.lua new file mode 100644 index 0000000..a17f3ef --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/sort/sort.spec.lua @@ -0,0 +1,25 @@ +return function() + local sort = require(script.Parent.sort) + it("Should be an table with two functions", function() + expect(sort.default).to.be.a("function") + expect(sort.reverse).to.be.a("function") + local less = 1 + local more = 2 + expect(sort.default(less, more)).to.equal(true) + expect(sort.default(more, less)).to.equal(false) + expect(sort.reverse(less, more)).to.equal(false) + expect(sort.reverse(more, less)).to.equal(true) + end) + + it("Should should work well with table.sort", function() + local values = {4, 3, 5, 2, 1} + table.sort(values, sort.default) + for i = 1, #values do + expect(values[i]).to.equal(i) + end + table.sort(values, sort.reverse) + for i = 1, #values do + expect(values[i]).to.equal(5 - i + 1) + end + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/lock.toml new file mode 100644 index 0000000..b358360 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/freeze/lock.toml @@ -0,0 +1,6 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "freeze" +version = "0.1.0" +commit = "ef89f9d0444a2a7f63d5fd913a38824f3edba69f" +source = "git+https://github.rbx.com/roblox/freeze#master" +dependencies = ["Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo"] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/lock.toml new file mode 100644 index 0000000..7343ee2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "jtaylor/mock" +version = "0.1.0" +commit = "d2c4005c863fd2f9fa74b7391ada64906d242847" +source = "git+https://github.rbx.com/roblox/mock#master" diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/AnyCallMatches.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/AnyCallMatches.lua new file mode 100644 index 0000000..996dead --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/AnyCallMatches.lua @@ -0,0 +1,50 @@ +-- Functions to check if a mock was ever called with given arguments. + +local getCalls = require(script.Parent.getCalls) +local cmpLiteralArgs = require(script.Parent.cmpLiteralArgs) +local cmpPredicateArgs = require(script.Parent.cmpPredicateArgs) +local fmtArgs = require(script.Parent.fmtArgs) + +local AnyCallMatches = {} + +function AnyCallMatches.args(mock, test) + local callList = getCalls(mock) + local size = #callList + if size == 0 then + return false, "mock was not called" + end + + local msg + for i = 1, size do + local result + result, msg = test(callList[i].args) + if result then + return true + end + end + + if not msg then + msg = fmtArgs(callList[size].args) .. " did not match" + end + if size > 1 then + msg = msg .. " (+" .. (size - 1) .. " other calls)" + end + + return false, msg +end + +function AnyCallMatches.literals(mock, ...) + local expected = table.pack(...) + return AnyCallMatches.args(mock, function(actual) + return cmpLiteralArgs(expected, actual) + end) +end + +function AnyCallMatches.predicates(mock, ...) + local predicates = table.pack(...) + return AnyCallMatches.args(mock, function(actual) + return cmpPredicateArgs(predicates, actual) + end) +end + +return AnyCallMatches diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/MagicMock.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/MagicMock.lua new file mode 100644 index 0000000..4111575 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/MagicMock.lua @@ -0,0 +1,76 @@ +local symbols = require(script.Parent.symbols) +local Spy = require(script.Parent.Spy) + +local MagicMock = {} +local MetaMock = {} + +function MetaMock:__index(key) + -- Any access to an undefined member will return a new MagicMock. + local meta = getmetatable(self) + local child = meta[symbols.Children][key] + if child == nil then + child = MagicMock.new() + meta[symbols.Children][key] = child + return child + elseif child == symbols.None then + return nil + else + return child + end +end + +function MetaMock:__newindex(key, value) + -- Store any assigned values for later recall. + local meta = getmetatable(self) + if type(value) == "function" then + local _, wrapper = Spy.new(value) + value = wrapper + elseif value == nil then + value = symbols.None + end + + if key == symbols.ReturnValue then + meta[symbols.ReturnValue] = { value, n=1 } + else + meta[symbols.Children][key] = value + end +end + +function MetaMock:__call(...) + -- Any call to a MagicMock will store the args and then return the + -- ReturnValue (or call that if it's a function). If no return + -- value was set, this will create a new MagicMock. + local meta = getmetatable(self) + local call = { + args = table.pack(...), + result = meta[symbols.ReturnValue], + } + if call.result == nil then + local child = MagicMock.new() + call.result = { child, n=1 } + meta[symbols.ReturnValue] = call.result + elseif call.result == symbols.None then + call.result = { n=1 } + end + + table.insert(meta[symbols.Calls], call) + return table.unpack(call.result) +end + +function MagicMock.new(lock) + local mock = { + [symbols.Calls] = {}, + [symbols.Children] = {}, + [symbols.Lock] = lock, + [symbols.ReturnValue] = nil, + } + + -- Copy the Meta functions from MetaMock + for k, v in pairs(MetaMock) do + mock[k] = v + end + + return setmetatable({}, mock) +end + +return MagicMock diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/Spy.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/Spy.lua new file mode 100644 index 0000000..df221b2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/Spy.lua @@ -0,0 +1,37 @@ +-- Create a wrapper around a function to capture what arguments it was called +-- with. + +local symbols = require(script.Parent.symbols) + +local Spy = {} +Spy.__index = Spy + +local spyLookup = {} +setmetatable(spyLookup, {__mode = "kv"}) + +function Spy.new(inner) + local spy = { + [symbols.Calls] = {} + } + setmetatable(spy, Spy) + + local wrapper = function(...) + local call = { + args = table.pack(...), + result = table.pack(inner(...)), + } + table.insert(spy[symbols.Calls], call) + return table.unpack(call.result) + end + + spy.inner = wrapper + spyLookup[wrapper] = spy + + return spy, wrapper +end + +function Spy.lookup(wrapper) + return spyLookup[wrapper] +end + +return Spy \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/cmpLiteralArgs.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/cmpLiteralArgs.lua new file mode 100644 index 0000000..e218148 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/cmpLiteralArgs.lua @@ -0,0 +1,17 @@ +-- Compare two packed tables for shallow equality. +local fmtArgs = require(script.Parent.fmtArgs) + +return function(e, a) + if e.n ~= a.n then + local msg = "number of literals in " .. fmtArgs(e) .. " does not match number of args in " .. fmtArgs(a) + return false, msg + end + + for i = 1, e.n do + if e[i] ~= a[i] then + local msg = "expected " .. fmtArgs(e) .. ", got " .. fmtArgs(a) + return false, msg + end + end + return true +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/cmpPredicateArgs.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/cmpPredicateArgs.lua new file mode 100644 index 0000000..f1fdc04 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/cmpPredicateArgs.lua @@ -0,0 +1,17 @@ +-- Compare a packed table with a packed table of predicates. +local fmtArgs = require(script.Parent.fmtArgs) + +return function(p, x) + if p.n ~= x.n then + local msg = "number of args in " .. fmtArgs(x) .. " does not match number of predicates" + return false, msg + end + + for i = 1, x.n do + if not p[i](x[i]) then + local msg = fmtArgs(x) .. " does not match predicates at position " .. i + return false, msg + end + end + return true +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/fmtArgs.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/fmtArgs.lua new file mode 100644 index 0000000..bbec14f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/fmtArgs.lua @@ -0,0 +1,11 @@ +return function(args) + local msg = {} + for i = 1, args.n do + if type(args[i]) == "string" then + table.insert(msg, '"' .. args[i] .. '"') + else + table.insert(msg, tostring(args[i])) + end + end + return "{" .. table.concat(msg, ", ") .. "}" +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/getCalls.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/getCalls.lua new file mode 100644 index 0000000..59cf63c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/getCalls.lua @@ -0,0 +1,29 @@ +local symbols = require(script.Parent.symbols) +local Spy = require(script.Parent.Spy) + +return function(mock) + -- To support the usecase of invoking getCalls on a function + -- wrapped with a spy + if type(mock) == "function" then + local spy = Spy.lookup(mock) + if spy then + return spy[symbols.Calls] + end + error("Calling getCalls on a non-spy function") + end + + -- To support the usecase of invoking getCalls on spy itself + local argsList = rawget(mock, symbols.Calls) + + -- To support the usecase of invoking getCalls on MagicMock + if argsList == nil then + local meta = getmetatable(mock) + argsList = meta[symbols.Calls] + end + + if argsList == nil then + error("Calling getCalls on a non-mock") + end + + return argsList +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/init.lua new file mode 100644 index 0000000..4015b79 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/init.lua @@ -0,0 +1,8 @@ +local symbols = require(script.symbols) + +return { + MagicMock = require(script.MagicMock), + AnyCallMatches = require(script.AnyCallMatches), + getCalls = require(script.getCalls), + Spy = require(script.Spy), +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/symbols.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/symbols.lua new file mode 100644 index 0000000..1e4b0fa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/symbols.lua @@ -0,0 +1,16 @@ +local function newSymbol(name) + local symbol = newproxy(true) + name = ("Mocks.%s"):format(name) + getmetatable(symbol).__tostring = function() + return name + end + return symbol +end + +return { + Calls = newSymbol("Calls"), + Children = newSymbol("Children"), + Lock = newSymbol("Lock"), + None = newSymbol("None"), + ReturnValue = newSymbol("ReturnValue"), +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/lua-promise/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/lua-promise/lock.toml new file mode 100644 index 0000000..b62968a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/lua-promise/lock.toml @@ -0,0 +1,6 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "lua-promise" +version = "0.1.0" +commit = "bbb9e1628901e29b8fad444ed1c9743ccbb3280b" +source = "git+https://github.rbx.com/roblox/lua-promise#master" +dependencies = ["tutils tutils 0be577db git+https://github.rbx.com/roblox/tutils#master"] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/lua-promise/lua-promise/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/lua-promise/lua-promise/init.lua new file mode 100644 index 0000000..4e90842 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/lua-promise/lua-promise/init.lua @@ -0,0 +1,366 @@ +--[[ + An implementation of Promises similar to Promise/A+. +]] +local tutils = require(script.Parent.tutils) + +local PROMISE_DEBUG = true + +-- If promise debugging is on, use a version of pcall that warns on failure. +-- This is useful for finding errors that happen within Promise itself. +local wpcall +if PROMISE_DEBUG then + wpcall = function(f, ...) + local result = { pcall(f, ...) } + + if not result[1] then + warn(result[2]) + end + + return unpack(result) + end +else + wpcall = pcall +end + +--[[ + Creates a function that invokes a callback with correct error handling and + resolution mechanisms. +]] +local function createAdvancer(callback, resolve, reject) + return function(...) + local result = { wpcall(callback, ...) } + local ok = table.remove(result, 1) + + if ok then + resolve(unpack(result)) + else + reject(unpack(result)) + end + end +end + +local function isEmpty(t) + return next(t) == nil +end + +local Promise = {} +Promise.__index = Promise + +Promise.Status = { + Started = "Started", + Resolved = "Resolved", + Rejected = "Rejected", +} + +--[[ + Constructs a new Promise with the given initializing callback. + + This is generally only called when directly wrapping a non-promise API into + a promise-based version. + + The callback will receive 'resolve' and 'reject' methods, used to start + invoking the promise chain. + + For example: + + local function get(url) + return Promise.new(function(resolve, reject) + spawn(function() + resolve(HttpService:GetAsync(url)) + end) + end) + end + + get("https://google.com") + :andThen(function(stuff) + print("Got some stuff!", stuff) + end) +]] +function Promise.new(callback) + local promise = { + -- Used to locate where a promise was created + _source = debug.traceback(), + + -- A tag to identify us as a promise + _type = "Promise", + + _status = Promise.Status.Started, + + -- A table containing a list of all results, whether success or failure. + -- Only valid if _status is set to something besides Started + _value = nil, + + -- If an error occurs with no observers, this will be set. + _unhandledRejection = false, + + -- Queues representing functions we should invoke when we update! + _queuedResolve = {}, + _queuedReject = {}, + } + + setmetatable(promise, Promise) + + local function resolve(...) + promise:_resolve(...) + end + + local function reject(...) + promise:_reject(...) + end + + local ok, err = wpcall(callback, resolve, reject) + + if not ok and promise._status == Promise.Status.Started then + reject(err) + end + + return promise +end + +--[[ + Create a promise that represents the immediately resolved value. +]] +function Promise.resolve(value) + return Promise.new(function(resolve) + resolve(value) + end) +end + +--[[ + Create a promise that represents the immediately rejected value. +]] +function Promise.reject(value) + return Promise.new(function(_, reject) + reject(value) + end) +end + +--[[ + Returns a new promise that: + * is resolved when all input promises resolve + * is rejected if ANY input promises reject +]] +function Promise.all(...) + local promises = {...} + + -- check if we've been given a list of promises, not just a variable number of promises + if type(promises[1]) == "table" and promises[1]._type ~= "Promise" then + -- we've been given a table of promises already + promises = promises[1] + end + + return Promise.new(function(resolve, reject) + local isResolved = false + local results = {} + local totalCompleted = 0 + local function promiseCompleted(index, result) + if isResolved then + return + end + + results[index] = result + totalCompleted = totalCompleted + 1 + + if totalCompleted == #promises then + resolve(results) + isResolved = true + end + end + + if #promises == 0 then + resolve(results) + isResolved = true + return + end + + for index, promise in ipairs(promises) do + -- if a promise isn't resolved yet, add listeners for when it does + if promise._status == Promise.Status.Started then + promise:andThen(function(result) + promiseCompleted(index, result) + end):catch(function(reason) + isResolved = true + reject(reason) + end) + + -- if a promise is already resolved, move on + elseif promise._status == Promise.Status.Resolved then + promiseCompleted(index, unpack(promise._value)) + + -- if a promise is rejected, reject the whole chain + else --if promise._status == Promise.Status.Rejected then + isResolved = true + reject(unpack(promise._value)) + end + end + end) +end + +--[[ + Is the given object a Promise instance? +]] +function Promise.is(object) + if type(object) ~= "table" then + return false + end + + return object._type == "Promise" +end + +--[[ + Creates a new promise that receives the result of this promise. + + The given callbacks are invoked depending on that result. +]] +function Promise:andThen(successHandler, failureHandler) + -- Even if we haven't specified a failure handler, the rejection will be automatically + -- passed on by the advancer; other promises in the chain may have unhandled rejections, + -- but this one is taken care of as soon as any andThen is connected + self._unhandledRejection = false + + -- Create a new promise to follow this part of the chain + return Promise.new(function(resolve, reject) + -- Our default callbacks just pass values onto the next promise. + -- This lets success and failure cascade correctly! + + local successCallback = resolve + if successHandler then + successCallback = createAdvancer(successHandler, resolve, reject) + end + + local failureCallback = reject + if failureHandler then + failureCallback = createAdvancer(failureHandler, resolve, reject) + end + + if self._status == Promise.Status.Started then + -- If we haven't resolved yet, put ourselves into the queue + table.insert(self._queuedResolve, successCallback) + table.insert(self._queuedReject, failureCallback) + elseif self._status == Promise.Status.Resolved then + -- This promise has already resolved! Trigger success immediately. + successCallback(unpack(self._value)) + elseif self._status == Promise.Status.Rejected then + -- This promise died a terrible death! Trigger failure immediately. + failureCallback(unpack(self._value)) + end + end) +end + +--[[ + Used to catch any errors that may have occurred in the promise. +]] +function Promise:catch(failureCallback) + return self:andThen(nil, failureCallback) +end + +--[[ + Yield until the promise is completed. + + This matches the execution model of normal Roblox functions. +]] +function Promise:await() + self._unhandledRejection = false + + if self._status == Promise.Status.Started then + local result + local bindable = Instance.new("BindableEvent") + + self:andThen(function(...) + result = {...} + bindable:Fire(true) + end, function(...) + result = {...} + bindable:Fire(false) + end) + + local ok = bindable.Event:Wait() + bindable:Destroy() + + if not ok then + error(tostring(result[1]), 2) + end + + return unpack(result) + elseif self._status == Promise.Status.Resolved then + return unpack(self._value) + elseif self._status == Promise.Status.Rejected then + error(tostring(self._value[1]), 2) + end +end + +function Promise:_resolve(...) + if self._status ~= Promise.Status.Started then + return + end + + -- If the resolved value was a Promise, we chain onto it! + if Promise.is((...)) then + -- Without this warning, arguments sometimes mysteriously disappear + if select("#", ...) > 1 then + local message = ("When returning a Promise from andThen, extra arguments are discarded! See:\n\n%s"):format( + self._source + ) + warn(message) + end + + (...):andThen(function(...) + self:_resolve(...) + end, function(...) + self:_reject(...) + end) + + return + end + + self._status = Promise.Status.Resolved + self._value = {...} + + -- We assume that these callbacks will not throw errors. + for _, callback in ipairs(self._queuedResolve) do + callback(...) + end +end + +function Promise:_reject(...) + if self._status ~= Promise.Status.Started then + return + end + + self._status = Promise.Status.Rejected + self._value = {...} + + -- If there are any rejection handlers, call those! + if not isEmpty(self._queuedReject) then + -- We assume that these callbacks will not throw errors. + for _, callback in ipairs(self._queuedReject) do + callback(...) + end + else + -- At this point, no one was able to observe the error. + -- An error handler might still be attached if the error occurred + -- synchronously. We'll wait one tick, and if there are still no + -- observers, then we should put a message in the console. + + self._unhandledRejection = true + -- Rather than trying to figure out how to pack/represent the rejection values, + -- we just stringify the first value and leave the rest alone + local err = tutils.toString((...)) + + spawn(function() + -- Someone observed the error, hooray! + if not self._unhandledRejection then + return + end + + -- Build a reasonable message + local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format( + err, + self._source + ) + warn(message) + end) + end +end + +return Promise diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/lua-promise/tutils.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/lua-promise/tutils.lua new file mode 100644 index 0000000..cb6c720 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/lua-promise/tutils.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["tutils"]["tutils"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Otter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Roact.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/lock.toml new file mode 100644 index 0000000..63ff929 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/lock.toml @@ -0,0 +1,10 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roact-navigation" +version = "0.1.1-linter-fix" +commit = "943c1c661c396a39ef3ca8f45927d4242732581d" +source = "url+https://github.com/roblox/roact-navigation" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/BackBehavior.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/BackBehavior.lua new file mode 100644 index 0000000..5fe5b20 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/BackBehavior.lua @@ -0,0 +1,21 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +local NONE_TOKEN = NavigationSymbol("NONE") +local INITIAL_ROUTE_TOKEN = NavigationSymbol("INITIAL_ROUTE") +local ORDER_TOKEN = NavigationSymbol("ORDER") +local HISTORY_TOKEN = NavigationSymbol("HISTORY") + +--[[ + BackBehavior provides shared constants that are used to configure back + action styles for different navigators. Note that not all routers support + all BackBehaviors and they will fall back to appropriate defaults for + those cases. +]] +local BackBehavior = { + None = NONE_TOKEN, + InitialRoute = INITIAL_ROUTE_TOKEN, + Order = ORDER_TOKEN, + History = HISTORY_TOKEN, +} + +return BackBehavior diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/EdgeInsets.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/EdgeInsets.lua new file mode 100644 index 0000000..4540250 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/EdgeInsets.lua @@ -0,0 +1,38 @@ +--[[ + EdgeInsets provides standard tooling to conveniently create a table + to represent insets from the edge of a component's viewable area. + This is most useful for identifying a "safe area" for visible content, + e.g. reacting to variable status bar heights and other view adornments. + + Positive values represent insets into the viewable area, e.g. smaller + than the container. + + Several constructors are provided: + + EdgeInsets.new(top: UDim, right: UDim, bottom: UDim, left: UDim) + Creates a new edge insets from four UDims that you provide. + + EdgeInsets.fromOffsets(top: number, right: number, bottom: number, left: number) + Creates an offsets-only EdgeInsets using four numbers that you provide. +]] +local EdgeInsets = {} + +function EdgeInsets.new(topUDim, rightUDim, bottomUDim, leftUDim) + return { + top = topUDim or UDim.new(0, 0), + right = rightUDim or UDim.new(0, 0), + bottom = bottomUDim or UDim.new(0, 0), + left = leftUDim or UDim.new(0, 0), + } +end + +function EdgeInsets.fromOffsets(top, right, bottom, left) + return { + top = UDim.new(0, top or 0), + right = UDim.new(0, right or 0), + bottom = UDim.new(0, bottom or 0), + left = UDim.new(0, left or 0), + } +end + +return EdgeInsets diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationActions.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationActions.lua new file mode 100644 index 0000000..5719d57 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationActions.lua @@ -0,0 +1,76 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +local BACK_TOKEN = NavigationSymbol("BACK") +local INIT_TOKEN = NavigationSymbol("INIT") +local NAVIGATE_TOKEN = NavigationSymbol("NAVIGATE") +local SET_PARAMS_TOKEN = NavigationSymbol("SET_PARAMS") +local COMPLETE_TRANSITION_TOKEN = NavigationSymbol("COMPLETE_TRANSITION") + +--[[ + NavigationActions provides shared constants and methods to construct + actions that are dispatched to routers to cause a change in the route. +]] +local NavigationActions = { + Back = BACK_TOKEN, + Init = INIT_TOKEN, + Navigate = NAVIGATE_TOKEN, + SetParams = SET_PARAMS_TOKEN, + CompleteTransition = COMPLETE_TRANSITION_TOKEN, +} + +NavigationActions.__index = NavigationActions + +-- Navigate back in the history (temporally). +function NavigationActions.back(payload) + local data = payload or {} + return { + type = BACK_TOKEN, + key = data.key, + immediate = data.immediate, + } +end + +-- Initialize the navigation history if not already defined. +function NavigationActions.init(payload) + local data = payload or {} + return { + type = INIT_TOKEN, + params = data.params, + } +end + +-- Navigate to an existing or new route. +function NavigationActions.navigate(payload) + local data = payload or {} + return { + type = NAVIGATE_TOKEN, + routeName = data.routeName, + params = data.params, + action = data.action, + key = data.key, + } +end + +-- Swap out the params for an existing route, matched by the given key. +function NavigationActions.setParams(payload) + local data = payload or {} + return { + type = SET_PARAMS_TOKEN, + key = data.key, + params = data.params, + } +end + +-- For internal use. Triggers completion of a transition animation, if needed by the router. +-- This would be sent on e.g. didMount of the new page, so the router knows that the new screen +-- is ready to be displayed before it animates it in place. +function NavigationActions.completeTransition(payload) + local data = payload or {} + return { + type = COMPLETE_TRANSITION_TOKEN, + key = data.key, + toChildKey = data.toChildKey, + } +end + +return NavigationActions diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationEvents.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationEvents.lua new file mode 100644 index 0000000..a60610b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationEvents.lua @@ -0,0 +1,21 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +local WILL_FOCUS_TOKEN = NavigationSymbol("WILL_FOCUS") +local DID_FOCUS_TOKEN = NavigationSymbol("DID_FOCUS") +local WILL_BLUR_TOKEN = NavigationSymbol("WILL_BLUR") +local DID_BLUR_TOKEN = NavigationSymbol("DID_BLUR") +local ACTION_TOKEN = NavigationSymbol("ACTION") +local REFOCUS_TOKEN = NavigationSymbol("REFOCUS") + +--[[ + NavigationEvents provides shared constants that are used to register + listeners for different RoactNavigation UI state changes. +]] +return { + WillFocus = WILL_FOCUS_TOKEN, + DidFocus = DID_FOCUS_TOKEN, + WillBlur = WILL_BLUR_TOKEN, + DidBlur = DID_BLUR_TOKEN, + Action = ACTION_TOKEN, + Refocus = REFOCUS_TOKEN, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationSymbol.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationSymbol.lua new file mode 100644 index 0000000..90762dc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationSymbol.lua @@ -0,0 +1,15 @@ +-- Taken from Roact.Symbol and modified to produce exact string names +-- to allow for serialization/pathing. + +return function (name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + -- Unlike Symbols in Roact, we need the exact names. + getmetatable(self).__tostring = function() + return name + end + + return self +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NoneSymbol.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NoneSymbol.lua new file mode 100644 index 0000000..10acee1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NoneSymbol.lua @@ -0,0 +1,11 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +--[[ + RoactNavigation.None allows us to declare that certain values should be explicitly + removed from navigation props, e.g. for stack navigation options where we want to + remove the default header from a screen when drawing in a stack navigator with + headerMode == StackHeaderMode.Screen. +]] +local NONE_SYMBOL = NavigationSymbol("NONE") + +return NONE_SYMBOL diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/StackActions.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/StackActions.lua new file mode 100644 index 0000000..387ed44 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/StackActions.lua @@ -0,0 +1,80 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +local POP_TOKEN = NavigationSymbol("POP") +local POP_TO_TOP_TOKEN = NavigationSymbol("POP_TO_TOP") +local PUSH_TOKEN = NavigationSymbol("PUSH") +local RESET_TOKEN = NavigationSymbol("RESET") +local REPLACE_TOKEN = NavigationSymbol("REPLACE") + +--[[ + StackActions provides shared constants and methods to construct + actions that are dispatched to routers to cause a change in the route. + These actions are specific to Stack navigation. See NavigationActions + if you need to use more general APIs. +]] +local StackActions = { + Pop = POP_TOKEN, + PopToTop = POP_TO_TOP_TOKEN, + Push = PUSH_TOKEN, + Reset = RESET_TOKEN, + Replace = REPLACE_TOKEN, +} + +StackActions.__index = StackActions + +-- Pop the top-most item off the route stack, if any. +function StackActions.pop(payload) + local data = payload or {} + return { + type = POP_TOKEN, + n = data.n, + } +end + +-- Pop all the items except the last one off the route stack. +function StackActions.popToTop(payload) + local data = payload or {} + return { + type = POP_TO_TOP_TOKEN, + key = data.key, + } +end + +-- Push a new item onto the route stack. +function StackActions.push(payload) + local data = payload or {} + return { + type = PUSH_TOKEN, + routeName = data.routeName, + params = data.params, + action = data.action, + } +end + +-- Reset the route stack and replace it with a new stack, +-- specified by a list of actions to be applied. +function StackActions.reset(payload) + local data = payload or {} + return { + type = RESET_TOKEN, + index = data.index, + actions = data.actions, + key = data.key, + } +end + +-- Replace the route for the given key with a new route. +function StackActions.replace(payload) + local data = payload or {} + return { + type = REPLACE_TOKEN, + key = data.key, + newKey = data.newKey, + routeName = data.routeName, + params = data.params, + action = data.action, + immediate = data.immediate, + } +end + +return StackActions diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/StateUtils.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/StateUtils.lua new file mode 100644 index 0000000..415a6a6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/StateUtils.lua @@ -0,0 +1,262 @@ +local Cryo = require(script.Parent.Parent.Cryo) + +--[[ + StateUtils provides utilities to read and write standard route data. + Routes have the following general structure: + { + index = , + routes = [ + { + routeName = , + key = , + params = , + action = , + }, + ... + ] + } + + This structure is independent of the notion of stack, tab, drawer, or any + other kind of navigation. It simply represents a list of pages and their + parameters. Different kinds of routers can treat the data in their own way. +]] +local StateUtils = {} +StateUtils.__index = StateUtils + +-- Get the route matching the given key. Returns nil if no match is found. +function StateUtils.get(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + for _, route in ipairs(state.routes) do + if route.key == key then + return route + end + end + + return nil +end + +-- Get the route at the given index. Returns nil if no match is found. +function StateUtils.getAtIndex(state, index) + assert(type(state) == "table", "state must be a table") + assert(type(index) == "number", "index must be a number") + assert(index >= 0, "index must be non-negative") + + return state.routes[index] +end + +-- Get the active route from state. Returns nil if no routes. +function StateUtils.getActiveRoute(state) + assert(type(state) == "table", "state must be a table") + + local index = state.index + if index <= 0 then + return nil + end + + return state.routes[index] +end + +-- Get the index of the route matching the given key. Returns nil if no match is found. +function StateUtils.indexOf(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + for index, route in ipairs(state.routes) do + if route.key == key then + return index + end + end + + return nil +end + +-- Returns true if a route exists matching the given key, false otherwise. +function StateUtils.has(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + for _, route in ipairs(state.routes) do + if route.key == key then + return true + end + end + + return false +end + +-- Push a new route into the navigation state. Makes the pushed route active. +function StateUtils.push(state, route) + assert(type(state) == "table", "state must be a table") + assert(type(route) == "table", "route must be a table") + + assert(StateUtils.indexOf(state, route.key) == nil, + string.format("route with key '%s' already exists", route.key)) + + local routes = Cryo.List.join(state.routes, { route }) + return Cryo.Dictionary.join(state, { + index = #routes, + routes = routes, + }) +end + +-- Pop the top-most route from the navigation state (NOT the active route). +-- Makes the new top-most route active. +function StateUtils.pop(state) + assert(type(state) == "table", "state must be a table") + + if #state.routes == 0 then + -- NOTE: Popping empty state is a no-op + return state + end + + local routes = Cryo.List.removeIndex(state.routes, #state.routes) + return Cryo.Dictionary.join(state, { + index = #routes, + routes = routes, + }) +end + +-- Sets the active route to match the given index. +function StateUtils.jumpToIndex(state, index) + assert(type(state) == "table", "state must be a table") + assert(type(index) == "number", "index must be a number") + + if index == state.index then + return state + end + + assert(state.routes[index] ~= nil, + string.format("cannot jump to out-of-range index '%d'", index)) + + return Cryo.Dictionary.join(state, { + index = index, + }) +end + +-- Sets the active route to match the given key. +function StateUtils.jumpTo(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + local index = StateUtils.indexOf(state, key) + return StateUtils.jumpToIndex(state, index) +end + +-- Sets the active route to the previous route in the list. +function StateUtils.back(state) + assert(type(state) == "table", "state must be a table") + + local index = state.index - 1 + if not state.routes[index] then + return state + end + + return StateUtils.jumpToIndex(state, index) +end + +-- Sets the active route to the next route in the list. +function StateUtils.forward(state) + assert(type(state) == "table", "state must be a table") + + local index = state.index + 1 + if not state.routes[index] then + return state + end + + return StateUtils.jumpToIndex(state, index) +end + +-- Replace the route matching the given key. Sets the active route to the +-- newly replaced entry. Prunes the old entries that follow the replaced one. +function StateUtils.replaceAndPrune(state, key, route) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + assert(type(route) == "table", "route must be a table") + + local index = StateUtils.indexOf(state, key) + local replaced = StateUtils.replaceAtIndex(state, index, route) + + return Cryo.Dictionary.join(replaced, { + routes = { unpack(replaced.routes, 1, index) } + }) +end + +-- Replace the route matching the given key without pruning the following routes. +-- The active route will be updated to match the newly replaced one unless +-- preserveIndex is true. +function StateUtils.replaceAt(state, key, route, preserveIndex) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + assert(type(route) == "table", "route must be a table") + assert(preserveIndex == nil or type(preserveIndex) == "boolean", + "preserveIndex must be nil or a boolean") + + local index = StateUtils.indexOf(state, key) + local nextIndex = preserveIndex and state.index or index + local nextState = StateUtils.replaceAtIndex(state, index, route) + nextState.index = nextIndex + return nextState +end + +-- Replace the route at the given index. Updates the active route to point to +-- the replaced entry. +function StateUtils.replaceAtIndex(state, index, route) + assert(type(state) == "table", "state must be a table") + assert(type(index) == "number", "index must be a number") + assert(type(route) == "table", "route must be a table") + + assert(state.routes[index] ~= nil, + string.format("index '%d' does not exist in route '%s'", index, route.key)) + + if state.routes[index] == route and index == state.index then + return state + end + + local routes = Cryo.List.join(state.routes) + routes[index] = route + + return Cryo.Dictionary.join(state, { + index = index, + routes = routes, + }) +end + +-- Wipe away the existing routes and replace them with new routes. +-- Sets the active route to the provided index (if provided), otherwise +-- sets the active route to the last one in the list. +function StateUtils.reset(state, routes, index) + assert(type(state) == "table", "state must be a table") + assert(type(routes) == "table" and #routes > 0, + "routes must be a list with at least one element") + assert(index == nil or type(index) == "number", + "index must be a number or nil") + + local nextIndex = not index and #routes or index + + -- Bail out without replacing IFF index and routes all match + if #state.routes == #routes and state.index == nextIndex then + local routesAreEqual = true + for i = 1, #routes, 1 do + if state.routes[i] ~= routes[i] then + routesAreEqual = false + break + end + end + + if routesAreEqual then + return state + end + end + + assert(routes[nextIndex] ~= nil, + string.format("cannot reset index '%d' that does not exist", nextIndex)) + + return Cryo.Dictionary.join(state, { + index = nextIndex, + routes = routes, + }) +end + +return StateUtils diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/BackBehavior.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/BackBehavior.spec.lua new file mode 100644 index 0000000..cec6ddd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/BackBehavior.spec.lua @@ -0,0 +1,19 @@ +return function() + local BackBehavior = require(script.Parent.Parent.BackBehavior) + + describe("BackBehavior token tests", function() + it("should return same object for each token for multiple calls", function() + expect(BackBehavior.None).to.equal(BackBehavior.None) + expect(BackBehavior.InitialRoute).to.equal(BackBehavior.InitialRoute) + expect(BackBehavior.Order).to.equal(BackBehavior.Order) + expect(BackBehavior.History).to.equal(BackBehavior.History) + end) + + it("should return matching string names for symbols", function() + expect(tostring(BackBehavior.None)).to.equal("NONE") + expect(tostring(BackBehavior.InitialRoute)).to.equal("INITIAL_ROUTE") + expect(tostring(BackBehavior.Order)).to.equal("ORDER") + expect(tostring(BackBehavior.History)).to.equal("HISTORY") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/EdgeInsets.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/EdgeInsets.spec.lua new file mode 100644 index 0000000..03b67ab --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/EdgeInsets.spec.lua @@ -0,0 +1,75 @@ +return function() + local EdgeInsets = require(script.Parent.Parent.EdgeInsets) + + it("should create EdgeInsets with provided UDims", function() + local top = UDim.new(0, 0) + local right = UDim.new(1, 0) + local bottom = UDim.new(0, 1) + local left = UDim.new(1, 1) + + local result = EdgeInsets.new(top, right, bottom, left) + expect(result.top).to.equal(top) + expect(result.right).to.equal(right) + expect(result.bottom).to.equal(bottom) + expect(result.left).to.equal(left) + end) + + it("should create EdgeInsets with default UDims", function() + local result = EdgeInsets.new() + expect(typeof(result.top)).to.equal("UDim") + expect(result.top.Scale).to.equal(0) + expect(result.top.Offset).to.equal(0) + + expect(typeof(result.right)).to.equal("UDim") + expect(result.right.Scale).to.equal(0) + expect(result.right.Offset).to.equal(0) + + expect(typeof(result.bottom)).to.equal("UDim") + expect(result.bottom.Scale).to.equal(0) + expect(result.bottom.Offset).to.equal(0) + + expect(typeof(result.left)).to.equal("UDim") + expect(result.left.Scale).to.equal(0) + expect(result.left.Offset).to.equal(0) + end) + + it("should create EdgeInsets with provided offset", function() + local result = EdgeInsets.fromOffsets(0, 1, 2, 3) + + expect(typeof(result.top)).to.equal("UDim") + expect(result.top.Scale).to.equal(0) + expect(result.top.Offset).to.equal(0) + + expect(typeof(result.right)).to.equal("UDim") + expect(result.right.Scale).to.equal(0) + expect(result.right.Offset).to.equal(1) + + expect(typeof(result.bottom)).to.equal("UDim") + expect(result.bottom.Scale).to.equal(0) + expect(result.bottom.Offset).to.equal(2) + + expect(typeof(result.left)).to.equal("UDim") + expect(result.left.Scale).to.equal(0) + expect(result.left.Offset).to.equal(3) + end) + + it("should create EdgeInsets with defaults for missing values", function() + local result = EdgeInsets.fromOffsets() + + expect(typeof(result.top)).to.equal("UDim") + expect(result.top.Scale).to.equal(0) + expect(result.top.Offset).to.equal(0) + + expect(typeof(result.right)).to.equal("UDim") + expect(result.right.Scale).to.equal(0) + expect(result.right.Offset).to.equal(0) + + expect(typeof(result.bottom)).to.equal("UDim") + expect(result.bottom.Scale).to.equal(0) + expect(result.bottom.Offset).to.equal(0) + + expect(typeof(result.left)).to.equal("UDim") + expect(result.left.Scale).to.equal(0) + expect(result.left.Offset).to.equal(0) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationActions.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationActions.spec.lua new file mode 100644 index 0000000..03d08db --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationActions.spec.lua @@ -0,0 +1,80 @@ +return function() + local NavigationActions = require(script.Parent.Parent.NavigationActions) + + describe("NavigationActions token tests", function() + it("should return same object for each token for multiple calls", function() + expect(NavigationActions.Back).to.equal(NavigationActions.Back) + expect(NavigationActions.Init).to.equal(NavigationActions.Init) + expect(NavigationActions.Navigate).to.equal(NavigationActions.Navigate) + expect(NavigationActions.SetParams).to.equal(NavigationActions.SetParams) + expect(NavigationActions.CompleteTransition).to.equal(NavigationActions.CompleteTransition) + end) + + it("should return matching string names for symbols", function() + expect(tostring(NavigationActions.Back)).to.equal("BACK") + expect(tostring(NavigationActions.Init)).to.equal("INIT") + expect(tostring(NavigationActions.Navigate)).to.equal("NAVIGATE") + expect(tostring(NavigationActions.SetParams)).to.equal("SET_PARAMS") + expect(tostring(NavigationActions.CompleteTransition)).to.equal("COMPLETE_TRANSITION") + end) + end) + + describe("NavigationActions function tests", function() + it("should return a back action with matching data for a call to back()", function() + local backTable = NavigationActions.back({ + key = "the_key", + immediate = true, + }) + + expect(backTable.type).to.equal(NavigationActions.Back) + expect(backTable.key).to.equal("the_key") + expect(backTable.immediate).to.equal(true) + end) + + it("should return an init action with matching data for call to init()", function() + local initTable = NavigationActions.init({ + params = "foo", + }) + + expect(initTable.type).to.equal(NavigationActions.Init) + expect(initTable.params).to.equal("foo") + end) + + it("should return a navigate action with matching data for call to navigate()", function() + local navigateTable = NavigationActions.navigate({ + routeName = "routeName", + params = "foo", + action = "action", + key = "key", + }) + + expect(navigateTable.type).to.equal(NavigationActions.Navigate) + expect(navigateTable.routeName).to.equal("routeName") + expect(navigateTable.params).to.equal("foo") + expect(navigateTable.action).to.equal("action") + expect(navigateTable.key).to.equal("key") + end) + + it("should return a set params action with matching data for call to setParams()", function() + local setParamsTable = NavigationActions.setParams({ + key = "key", + params = "foo", + }) + + expect(setParamsTable.type).to.equal(NavigationActions.SetParams) + expect(setParamsTable.key).to.equal("key") + expect(setParamsTable.params).to.equal("foo") + end) + + it("should return a complete transition action with matching data for call to completeTransition()", function() + local completeTransitionTable = NavigationActions.completeTransition({ + key = "key", + toChildKey = "toChildKey", + }) + + expect(completeTransitionTable.type).to.equal(NavigationActions.CompleteTransition) + expect(completeTransitionTable.key).to.equal("key") + expect(completeTransitionTable.toChildKey).to.equal("toChildKey") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationEvents.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationEvents.spec.lua new file mode 100644 index 0000000..ed776d7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationEvents.spec.lua @@ -0,0 +1,23 @@ +return function() + local NavigationEvents = require(script.Parent.Parent.NavigationEvents) + + describe("NavigationEvents token tests", function() + it("should return same object for each token for multiple calls", function() + expect(NavigationEvents.WillFocus).to.equal(NavigationEvents.WillFocus) + expect(NavigationEvents.DidFocus).to.equal(NavigationEvents.DidFocus) + expect(NavigationEvents.WillBlur).to.equal(NavigationEvents.WillBlur) + expect(NavigationEvents.DidBlur).to.equal(NavigationEvents.DidBlur) + expect(NavigationEvents.Action).to.equal(NavigationEvents.Action) + expect(NavigationEvents.Refocus).to.equal(NavigationEvents.Refocus) + end) + + it("should return matching string names for symbols", function() + expect(tostring(NavigationEvents.WillFocus)).to.equal("WILL_FOCUS") + expect(tostring(NavigationEvents.DidFocus)).to.equal("DID_FOCUS") + expect(tostring(NavigationEvents.WillBlur)).to.equal("WILL_BLUR") + expect(tostring(NavigationEvents.DidBlur)).to.equal("DID_BLUR") + expect(tostring(NavigationEvents.Action)).to.equal("ACTION") + expect(tostring(NavigationEvents.Refocus)).to.equal("REFOCUS") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationSymbol.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationSymbol.spec.lua new file mode 100644 index 0000000..a43690b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationSymbol.spec.lua @@ -0,0 +1,22 @@ +return function() + local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol) + + it("should give an opaque object", function() + local symbol = NavigationSymbol("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = NavigationSymbol("foo") + + expect(tostring(symbol)).to.equal("foo") + end) + + it("should be unique when constructed", function() + local symbolA = NavigationSymbol("abc") + local symbolB = NavigationSymbol("abc") + + expect(symbolA).never.to.equal(symbolB) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NoneSymbol.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NoneSymbol.spec.lua new file mode 100644 index 0000000..afcbced --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NoneSymbol.spec.lua @@ -0,0 +1,11 @@ +return function() + local NoneSymbol = require(script.Parent.Parent.NoneSymbol) + + it("should return same object for each token for multiple calls", function() + expect(NoneSymbol).to.equal(NoneSymbol) + end) + + it("should return matching string names for symbols", function() + expect(tostring(NoneSymbol)).to.equal("NONE") + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/StackActions.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/StackActions.spec.lua new file mode 100644 index 0000000..7baf9cc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/StackActions.spec.lua @@ -0,0 +1,82 @@ +return function() + local StackActions = require(script.Parent.Parent.StackActions) + + describe("StackActions token tests", function() + it("should return same object for each token for multiple calls", function() + expect(StackActions.Pop).to.equal(StackActions.Pop) + expect(StackActions.PopToTop).to.equal(StackActions.PopToTop) + expect(StackActions.Push).to.equal(StackActions.Push) + expect(StackActions.Reset).to.equal(StackActions.Reset) + expect(StackActions.Replace).to.equal(StackActions.Replace) + end) + + it("should return matching string names for symbols", function() + expect(tostring(StackActions.Pop)).to.equal("POP") + expect(tostring(StackActions.PopToTop)).to.equal("POP_TO_TOP") + expect(tostring(StackActions.Push)).to.equal("PUSH") + expect(tostring(StackActions.Reset)).to.equal("RESET") + expect(tostring(StackActions.Replace)).to.equal("REPLACE") + end) + end) + + describe("StackActions function tests", function() + it("should return a pop action for pop()", function() + local popTable = StackActions.pop({ + n = "n", + }) + + expect(popTable.type).to.equal(StackActions.Pop) + expect(popTable.n).to.equal("n") + end) + + it("should return a pop to top action for popToTop()", function() + local popToTopTable = StackActions.popToTop() + + expect(popToTopTable.type).to.equal(StackActions.PopToTop) + end) + + it("should return a push action for push()", function() + local pushTable = StackActions.push({ + routeName = "routeName", + params = "params", + action = "action", + }) + + expect(pushTable.type).to.equal(StackActions.Push) + expect(pushTable.routeName).to.equal("routeName") + expect(pushTable.params).to.equal("params") + expect(pushTable.action).to.equal("action") + end) + + it("should return a reset action for reset()", function() + local resetTable = StackActions.reset({ + index = "index", + actions = "actions", + key = "key", + }) + + expect(resetTable.type).to.equal(StackActions.Reset) + expect(resetTable.index).to.equal("index") + expect(resetTable.key).to.equal("key") + end) + + it("should return a replace action for replace()", function() + local replaceTable = StackActions.replace({ + key = "key", + newKey = "newKey", + routeName = "routeName", + params = "params", + action = "action", + immediate = "immediate", + }) + + expect(replaceTable.type).to.equal(StackActions.Replace) + expect(replaceTable.key).to.equal("key") + expect(replaceTable.newKey).to.equal("newKey") + expect(replaceTable.routeName).to.equal("routeName") + expect(replaceTable.params).to.equal("params") + expect(replaceTable.action).to.equal("action") + expect(replaceTable.immediate).to.equal("immediate") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/StateUtils.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/StateUtils.spec.lua new file mode 100644 index 0000000..279873f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/StateUtils.spec.lua @@ -0,0 +1,725 @@ +return function() + local StateUtils = require(script.Parent.Parent.StateUtils) + + describe("StateUtils.get tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.get(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.get({}, 5) + end).to.throw() + end) + + it("should return nil if key is not found in routes", function() + local result = StateUtils.get({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + }, + }, + }, "key") + + expect(result).to.equal(nil) + end) + + it("should return route if key is found in routes", function() + local result = StateUtils.get({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + }, + }, "foo-1") + + expect(result.routeName).to.equal("foo") + expect(result.key).to.equal("foo-1") + end) + end) + + describe("StateUtils.getAtIndex tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.getAtIndex(nil, 0) + end).to.throw() + end) + + it("should assert if index is negative", function() + expect(function() + StateUtils.getAtIndex({}, -1) + end).to.throw() + end) + + it("should return nil if index is not found", function() + local result = StateUtils.getAtIndex({ + index = 1, + routes = { + { + routeName = "foo1", + key = "foo-1", + }, + { + routeName = "foo2", + key = "foo-2", + } + } + }, 5) + + expect(result).to.equal(nil) + end) + + it("should return a matching route", function() + local result = StateUtils.getAtIndex({ + index = 1, + routes = { + { + routeName = "foo1", + key = "foo-1", + }, + { + routeName = "foo2", + key = "foo-2", + } + } + }, 2) + + expect(result.routeName).to.equal("foo2") + expect(result.key).to.equal("foo-2") + end) + end) + + describe("StateUtils.getActiveRoute tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.getActiveRoute(nil) + end).to.throw() + end) + + it("should return nil if no routes", function() + local result = StateUtils.getActiveRoute({ + index = 0, + routes = {}, + }) + + expect(result).to.equal(nil) + end) + + it("should return active route", function() + local result = StateUtils.getActiveRoute({ + index = 1, + routes = { + { + routeName = "active", + key = "active-1", + } + }, + }) + + expect(result.routeName).to.equal("active") + expect(result.key).to.equal("active-1") + end) + end) + + describe("StateUtils.indexOf tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.indexOf(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.indexOf({}, 5) + end).to.throw() + end) + + it("should return nil if key is not found in routes", function() + local result = StateUtils.indexOf({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + }, + }, "key") + + expect(result).to.equal(nil) + end) + + it("should return index if key is found in routes", function() + local result = StateUtils.indexOf({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + }, + { + routeName = "foo2", + key = "foo-2", + } + }, + }, "foo-2") + + expect(result).to.equal(2) + end) + end) + + describe("StateUtils.has tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.has(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.has({}, 5) + end).to.throw() + end) + + it("should return false if key is not in routes", function() + local result = StateUtils.has({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + } + }, "key") + + expect(result).to.equal(false) + end) + + it("should return true if key is found in routes", function() + local result = StateUtils.has({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + } + }, "foo-1") + + expect(result).to.equal(true) + end) + end) + + describe("StateUtils.push tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.push(nil, {}) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.push({}, 5) + end).to.throw() + end) + + it("should assert if route.key is already present", function() + expect(function() + StateUtils.push({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + } + }, { + routeName = "foo", + key = "foo-1", + }) + end).to.throw() + end) + + it("should insert new route if it doesn't exist", function() + local newState = StateUtils.push({ + index = 1, + routes = { + { + routeName = "first", + key = "foo-1", + }, + }, + }, { + routeName = "second", + key = "foo-2", + }) + + expect(newState.index).to.equal(2) + expect(#newState.routes).to.equal(2) + expect(newState.routes[newState.index].key).to.equal("foo-2") + expect(newState.routes[newState.index].routeName).to.equal("second") + end) + end) + + describe("StateUtils.pop tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.pop(nil) + end).to.throw() + end) + + it("should return existing state if routes is empty", function() + local initialState = { + index = 0, + routes = {}, + } + + local newState = StateUtils.pop(initialState) + expect(newState).to.equal(initialState) + end) + + it("should return empty state if popping one route", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "route-1", }, + }, + } + + local newState = StateUtils.pop(initialState) + expect(newState.index).to.equal(0) + expect(#newState.routes).to.equal(0) + end) + + it("should remove top route if popping with more than one route", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "route-1", }, + { routeName = "route", key = "route-2", }, + }, + } + + local newState = StateUtils.pop(initialState) + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(1) + expect(newState.routes[1].key).to.equal("route-1") + end) + end) + + describe("StateUtils.jumpToIndex tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.jumpToIndex(nil, 0) + end).to.throw() + end) + + it("should assert if index is not a number", function() + expect(function() + StateUtils.jumpToIndex({}, "foo") + end).to.throw() + end) + + it("should assert if index does not match a route", function() + expect(function() + StateUtils.jumpToIndex({ + index = 1, + routes = { { routeName = "first", key = "first-1" } } + }, 5) + end).to.throw() + end) + + it("should return original state if index matches current", function() + local initialState = { + index = 1, + routes = { { routeName = "one", key = "1" } } + } + + local newState = StateUtils.jumpToIndex(initialState, 1) + expect(newState).to.equal(initialState) + end) + + it("should return updated state if index differs", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "route-1" }, + { routeName = "route", key = "route-2" }, + }, + } + + local newState = StateUtils.jumpToIndex(initialState, 2) + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.jumpTo tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.jumpTo(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.jumpTo({}, 0) + end).to.throw() + end) + + it("should return original state if key is already active route", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.jumpTo(initialState, "key-1") + expect(newState).to.equal(initialState) + end) + + it("should return state with new active route if key is not active", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.jumpTo(initialState, "key-2") + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.back tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.back(nil) + end).to.throw() + end) + + it("should return original state if route for new index does not exist", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + } + } + + local newState = StateUtils.back(initialState) + expect(newState).to.equal(initialState) + end) + + it("should remove top state if there is somewhere to go", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.back(initialState) + expect(newState.index).to.equal(1) + end) + end) + + describe("StateUtils.forward tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.forward(nil) + end).to.throw() + end) + + it("should not walk off the end of the route list", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + } + } + + local newState = StateUtils.forward(initialState) + expect(newState).to.equal(initialState) + end) + + it("should move to next route if available", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.forward(initialState) + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.replaceAndPrune tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.replaceAndPrune(nil, "key", {}) + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.replaceAndPrune({}, 0, {}) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.replaceAndPrune({}, "key", 0) + end).to.throw() + end) + + it("should replace matching route and prune following routes", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.replaceAndPrune(initialState, "key-1", { + routeName = "newRoute", key = "key-3" + }) + + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(1) + expect(newState.routes[1].routeName).to.equal("newRoute") + expect(newState.routes[1].key).to.equal("key-3") + end) + end) + + describe("StateUtils.replaceAt tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.replaceAt(nil, "key", {}, false) + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.replaceAt({}, 0, {}, false) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.replaceAt({}, "key", 0, false) + end).to.throw() + end) + + it("should assert if preserveIndex is not a boolean", function() + expect(function() + StateUtils.replaceAt({}, "key", {}, 0) + end).to.throw() + end) + + it("should replace matching route, not prune, and update index", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.replaceAt(initialState, "key-1", { + routeName = "newRoute", key = "key-3" + }, false) + + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(2) + expect(newState.routes[1].routeName).to.equal("newRoute") + expect(newState.routes[1].key).to.equal("key-3") + end) + + it("should replace matching route, not prune, and preserve existing index", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.replaceAt(initialState, "key-1", { + routeName = "newRoute", key = "key-3" + }, true) + + expect(newState.index).to.equal(2) + expect(#newState.routes).to.equal(2) + expect(newState.routes[1].routeName).to.equal("newRoute") + expect(newState.routes[1].key).to.equal("key-3") + end) + end) + + describe("StateUtils.replaceAtIndex tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.replaceAtIndex(nil, 0, {}) + end).to.throw() + end) + + it("should assert if index is not a number", function() + expect(function() + StateUtils.replaceAtIndex({}, nil, {}) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.replaceAtIndex({}, 5, nil) + end).to.throw() + end) + + it("should assert if index does not exist", function() + expect(function() + StateUtils.replaceAtIndex({ + index = 0, + routes = {} + }, 5, { routeName = "name", key = "key" }) + end).to.throw() + end) + + it("should return original state if inputs are same", function() + local testRoute = { routeName = "name", key = "key" } + local initialState = { + index = 1, + routes = { testRoute }, + } + + local newState = StateUtils.replaceAtIndex(initialState, 1, testRoute) + expect(newState).to.equal(initialState) + end) + + it("should replace route at index if route is not equal", function() + local initialState = { + index = 1, + routes = { + { routeName = "name", key = "key" } + }, + } + + local newState = StateUtils.replaceAtIndex(initialState, 1, { + routeName = "newName", + key = "key", + }) + + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(1) + expect(newState.routes[1].routeName).to.equal("newName") + expect(newState.routes[1].key).to.equal("key") + end) + + it("should update index, if new index differs but route does not", function() + local testRoute = { routeName = "name", key = "key-2" } + local initialState = { + index = 1, + routes = { + { routeName = "name", key = "key-1" }, + testRoute, + } + } + + local newState = StateUtils.replaceAtIndex(initialState, 2, testRoute) + expect(newState).never.to.equal(initialState) + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.reset tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.reset(nil, {}, 0) + end).to.throw() + end) + + it("should assert if routes is not a table", function() + expect(function() + StateUtils.reset({}, nil, 0) + end).to.throw() + end) + + it("should assert if index is not a number", function() + expect(function() + StateUtils.reset({}, {}, "foo") + end).to.throw() + end) + + it("should NOT assert if index is nil", function() + expect(function() + StateUtils.reset({}, {}) + end).to.throw() + end) + + it("should return original state if index matches and all routes are same objects", function() + local route1 = { routeName = "route1", key = "route-1" } + local route2 = { routeName = "route2", key = "route-2" } + + local initialState = { + index = 2, + routes = { route1, route2 }, + } + + local newState = StateUtils.reset(initialState, { + route1, + route2, + }, 2) + + expect(newState).to.equal(initialState) + end) + + it("should update state if index is not specified and old index is not last route", function() + local route1 = { routeName = "route1", key = "route-1" } + local route2 = { routeName = "route2", key = "route-2" } + + local initialState = { + index = 1, + routes = { route1, route2 }, + } + + local newState = StateUtils.reset(initialState, { + route1, + route2, + }) + + expect(newState).never.to.equal(initialState) + expect(newState.index).to.equal(2) + end) + + it("should update state if index matches but routes differ", function() + local route1 = { routeName = "route1", key = "route-1" } + local route2 = { routeName = "route2", key = "route-2" } + + local initialState = { + index = 1, + routes = { route1, route2 }, + } + + local newState = StateUtils.reset(initialState, { + route1, + { routeName = "route3", key = "route-3" }, + }, 1) + + expect(newState).never.to.equal(initialState) + expect(#newState.routes).to.equal(2) + expect(newState.index).to.equal(1) + expect(newState.routes[2].routeName).to.equal("route3") + expect(newState.routes[2].key).to.equal("route-3") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/createAppContainer.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/createAppContainer.spec.lua new file mode 100644 index 0000000..c691891 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/createAppContainer.spec.lua @@ -0,0 +1,97 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Roact) + local createAppContainer = require(script.Parent.Parent.createAppContainer) + local createSwitchNavigator = require(script.Parent.Parent.navigators.createSwitchNavigator) + + it("should be a function", function() + expect(type(createAppContainer)).to.equal("function") + end) + + it("should return a valid component when mounting a switch navigator", function() + local TestNavigator = createSwitchNavigator({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local TestApp = createAppContainer(TestNavigator) + local element = Roact.createElement(TestApp) + local instance = Roact.mount(element) + + Roact.unmount(instance) + end) + + it("should throw when navigator has both navigation and container props", function() + local TestAppComponent = Roact.Component:extend("TestAppComponent") + TestAppComponent.router = {} + function TestAppComponent:render() end + + local element = Roact.createElement(createAppContainer(TestAppComponent), { + navigation = {}, + somePropThatShouldNotBeHere = true, + }) + + local status, err = pcall(function() + Roact.mount(element) + end) + + expect(status).to.equal(false) + expect(string.find(err, "This navigator has both 'navigation' and container props.")).to.never.equal(nil) + end) + + it("should throw when not passed a table for AppComponent", function() + local TestAppComponent = 5 + + local status, err = pcall(function() + createAppContainer(TestAppComponent) + end) + + expect(status).to.equal(false) + expect(string.find(err, "AppComponent must be a navigator or a stateful Roact " .. + "component with a 'router' field")).to.never.equal(nil) + end) + + it("should throw when passed a stateful component without router field", function() + local TestAppComponent = Roact.Component:extend("TestAppComponent") + + local status, err = pcall(function() + createAppContainer(TestAppComponent) + end) + + expect(status).to.equal(false) + expect(string.find(err, "AppComponent must be a navigator or a stateful Roact " .. + "component with a 'router' field")).to.never.equal(nil) + end) + + it("should connect and disconnect from backActionSignal", function() + local TestNavigator = createSwitchNavigator({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local backHandler = nil + local backSignal = { + connect = function(handler) + backHandler = handler + return { + disconnect = function() + backHandler = nil + end + } + end + } + + local element = Roact.createElement(createAppContainer(TestNavigator), { + backActionSignal = backSignal, + }) + + local instance = Roact.mount(element) + expect(backHandler).to.never.equal(nil) + Roact.unmount(instance) + expect(backHandler).to.equal(nil) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildEventSubscriber.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildEventSubscriber.spec.lua new file mode 100644 index 0000000..09c033b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildEventSubscriber.spec.lua @@ -0,0 +1,464 @@ +return function() + local NavigationEvents = require(script.Parent.Parent.NavigationEvents) + local getChildEventSubscriber = require(script.Parent.Parent.getChildEventSubscriber) + + local function dummyAddListener() end + + local function makeListenerBundle() + local testUpstreamListenerMap = {} + local function testAddUpstreamListener(eventType, callback) + testUpstreamListenerMap[eventType] = callback + + return { + disconnect = function() + testUpstreamListenerMap[eventType] = nil + end + } + end + + return { + listenerMap = testUpstreamListenerMap, + addListener = testAddUpstreamListener, + } + end + + local SIMPLE_TEST_KEY = "Foo" + + local SIMPLE_TEST_STATE = { + state = { + routes = { + { key = SIMPLE_TEST_KEY } + }, + index = 1, + }, + lastState = { + routes = { + { key = SIMPLE_TEST_KEY } + }, + index = 1, + }, + action = { + type = "SomeAction" + }, + } + + it("should return a table with correct members", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + expect(type(childSubscriber.addListener)).to.equal("function") + expect(type(childSubscriber.emit)).to.equal("function") + end) + + describe("addListener tests", function() + it("should throw on invalid eventType", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + expect(function() + childSubscriber.addListener("BadSymbol", function() end) + end).to.throw() + end) + + it("should throw on invalid eventHandler", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + expect(function() + childSubscriber.addListener(NavigationEvents.Action, 5) + end).to.throw() + end) + + it("should allow disconnect of listener", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + local connection = childSubscriber.addListener(NavigationEvents.Refocus, function() end) + connection.disconnect() + end) + end) + + describe("emit tests", function() + it("should throw when trying to emit any event besides Refocus", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + expect(function() + childSubscriber.emit(NavigationEvents.WillFocus) + end).to.throw() + + expect(function() + childSubscriber.emit(NavigationEvents.DidFocus) + end).to.throw() + + expect(function() + childSubscriber.emit(NavigationEvents.WillBlur) + end).to.throw() + + expect(function() + childSubscriber.emit(NavigationEvents.DidBlur) + end).to.throw() + + expect(function() + childSubscriber.emit(NavigationEvents.Action) + end).to.throw() + end) + + it("should throw when payload is not a table", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + expect(function() + childSubscriber.emit(NavigationEvents.Refocus, 5) + end).to.throw() + end) + + it("should allow external caller to emit a refocus event with valid payload", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + local testPayload = { a = 1 } + local outputPayload = nil + + childSubscriber.addListener(NavigationEvents.Refocus, function(payload) + outputPayload = payload + end) + + childSubscriber.emit(NavigationEvents.Refocus, testPayload) + expect(outputPayload.a).to.equal(1) + expect(outputPayload.type).to.equal(NavigationEvents.Refocus) + end) + + it("should allow external caller to emit a refocus event with nil payload", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + local outputPayload = nil + + childSubscriber.addListener(NavigationEvents.Refocus, function(payload) + outputPayload = payload + end) + + childSubscriber.emit(NavigationEvents.Refocus) + expect(outputPayload.type).to.equal(NavigationEvents.Refocus) + end) + end) + + describe("upstream event handling tests", function() + it("should register subscriptions for supported event types", function() + local testUpstreamListenerMap = {} + local function testAddUpstreamListener(eventType, callback) + expect(testUpstreamListenerMap[eventType]).to.equal(nil) + testUpstreamListenerMap[eventType] = true + + return { + disconnect = function() end + } + end + + getChildEventSubscriber(testAddUpstreamListener, SIMPLE_TEST_KEY) + + expect(testUpstreamListenerMap[NavigationEvents.Action]).to.equal(true) + expect(testUpstreamListenerMap[NavigationEvents.WillFocus]).to.equal(true) + expect(testUpstreamListenerMap[NavigationEvents.DidFocus]).to.equal(true) + expect(testUpstreamListenerMap[NavigationEvents.WillBlur]).to.equal(true) + expect(testUpstreamListenerMap[NavigationEvents.DidBlur]).to.equal(true) + expect(testUpstreamListenerMap[NavigationEvents.Refocus]).to.equal(true) + end) + + it("should disconnect subscriptions on DidBlur when there is no new route", function() + local testUpstreamListenerMap = {} + local function testAddUpstreamListener(eventType, callback) + testUpstreamListenerMap[eventType] = callback + + return { + disconnect = function() + testUpstreamListenerMap[eventType] = false + end + } + end + + local childSubscriber = getChildEventSubscriber( + testAddUpstreamListener, SIMPLE_TEST_KEY, NavigationEvents.DidBlur) + + childSubscriber.addListener(NavigationEvents.Action, function() end) + + testUpstreamListenerMap[NavigationEvents.DidBlur]({ + state = {}, + action = { + type = "SomeAction" + } + }) + + expect(testUpstreamListenerMap[NavigationEvents.Action]).to.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.WillFocus]).to.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.DidFocus]).to.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.WillBlur]).to.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.DidBlur]).to.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.Refocus]).to.equal(false) + end) + + it("should NOT disconnect subscriptions on DidBlur when there is a new route", function() + local testUpstreamListenerMap = {} + local function testAddUpstreamListener(eventType, callback) + testUpstreamListenerMap[eventType] = callback + + return { + disconnect = function() + testUpstreamListenerMap[eventType] = false + end + } + end + + local childSubscriber = getChildEventSubscriber( + testAddUpstreamListener, SIMPLE_TEST_KEY, NavigationEvents.DidBlur) + + childSubscriber.addListener(NavigationEvents.Action, function() end) + + testUpstreamListenerMap[NavigationEvents.DidBlur](SIMPLE_TEST_STATE) + + expect(testUpstreamListenerMap[NavigationEvents.Action]).to.never.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.WillFocus]).to.never.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.DidFocus]).to.never.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.WillBlur]).to.never.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.DidBlur]).to.never.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.Refocus]).to.never.equal(false) + end) + + it("should propagate refocus event from upstream", function() + local bundle = makeListenerBundle() + + local outputPayload = nil + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY) + childSubscriber.addListener(NavigationEvents.Refocus, function(payload) + outputPayload = payload + end) + + bundle.listenerMap[NavigationEvents.Refocus]({ a = 1 }) + + expect(outputPayload.a).to.equal(1) + expect(outputPayload.type).to.equal(NavigationEvents.Refocus) + end) + + it("should emit WillFocus on WillFocus event when previously blurred and child is current index", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY) + + local willFocusPayload = nil + childSubscriber.addListener(NavigationEvents.WillFocus, function(payload) + willFocusPayload = payload + end) + + bundle.listenerMap[NavigationEvents.WillFocus](SIMPLE_TEST_STATE) + + -- Detailed analysis of generated payload. Further tests will just check that functor was called. + expect(willFocusPayload).to.never.equal(nil) + expect(willFocusPayload.state).to.never.equal(nil) + expect(willFocusPayload.lastState).to.never.equal(nil) + expect(willFocusPayload.action.type).to.equal("SomeAction") + expect(willFocusPayload.type).to.equal(NavigationEvents.WillFocus) + end) + + it("should emit WillFocus AND DidFocus on Action event when previously blurred and child is current index", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY) + + local willFocusCalled = false + childSubscriber.addListener(NavigationEvents.WillFocus, function() + willFocusCalled = true + end) + + local didFocusCalled = false + childSubscriber.addListener(NavigationEvents.DidFocus, function() + didFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action](SIMPLE_TEST_STATE) + expect(willFocusCalled).to.equal(true) + expect(didFocusCalled).to.equal(true) + end) + + it("should NOT emit WillFocus or DidFocus on Action event when previously blurred and child is NOT current index", + function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY) + + local willFocusCalled = false + childSubscriber.addListener(NavigationEvents.WillFocus, function() + willFocusCalled = true + end) + + local didFocusCalled = false + childSubscriber.addListener(NavigationEvents.DidFocus, function() + didFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action]({ + state = { + routes = { + { key = SIMPLE_TEST_KEY }, + { key = "NOT_SIMPLE_TEST_KEY" }, + }, + index = 2, + }, + action = { + "SomeAction" + }, + }) + + expect(willFocusCalled).to.equal(false) + expect(didFocusCalled).to.equal(false) + end) + + it("should emit DidFocus on DidFocus event when previous event was WillFocus and child is current index", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, NavigationEvents.WillFocus) + + local didFocusCalled = false + childSubscriber.addListener(NavigationEvents.DidFocus, function() + didFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.DidFocus](SIMPLE_TEST_STATE) + + expect(didFocusCalled).to.equal(true) + end) + + it("should emit DidFocus on Action event when previous event was WillFocus and child is current index", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, NavigationEvents.WillFocus) + + local didFocusCalled = false + childSubscriber.addListener(NavigationEvents.DidFocus, function() + didFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action](SIMPLE_TEST_STATE) + + expect(didFocusCalled).to.equal(true) + end) + + it("should NOT emit DidFocus on DidFocus event when previous event was WillFocus while transitioning", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, NavigationEvents.WillFocus) + + local didFocusCalled = false + childSubscriber.addListener(NavigationEvents.DidFocus, function() + didFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.DidFocus]({ + state = { + routes = { + { key = SIMPLE_TEST_KEY } + }, + index = 1, + isTransitioning = true, + }, + action = { + "SomeAction" + }, + }) + + expect(didFocusCalled).to.equal(false) + end) + + it("should emit WillBlur on WillBlur event when previous event was DidFocus", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, NavigationEvents.DidFocus) + + local willBlurCalled = false + childSubscriber.addListener(NavigationEvents.WillBlur, function() + willBlurCalled = true + end) + + bundle.listenerMap[NavigationEvents.WillBlur](SIMPLE_TEST_STATE) + + expect(willBlurCalled).to.equal(true) + end) + + it("should emit Action on Action event when previous event was DidFocus", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, NavigationEvents.DidFocus) + + local actionCalled = false + childSubscriber.addListener(NavigationEvents.Action, function() + actionCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action](SIMPLE_TEST_STATE) + + expect(actionCalled).to.equal(true) + end) + + it("should emit DidBlur on DidBlur event when previous event was WillBlur", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, NavigationEvents.WillBlur) + + local didBlurCalled = false + childSubscriber.addListener(NavigationEvents.DidBlur, function() + didBlurCalled = true + end) + + bundle.listenerMap[NavigationEvents.DidBlur](SIMPLE_TEST_STATE) + + expect(didBlurCalled).to.equal(true) + end) + + it("should emit DidBlur on Action event when previous event was WillBlur and we've finished transitioning", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, "Foo", NavigationEvents.WillBlur) + + local didBlurCalled = false + childSubscriber.addListener(NavigationEvents.DidBlur, function() + didBlurCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action]({ + state = { + routes = { + { key = "Foo" }, -- Transitioned away from this route! + { key = "Bar" }, + }, + index = 2, + }, + lastState = { + routes = { + { key = "Foo" }, + { key = "Bar" }, + }, + index = 1, + }, + action = { + type = "SomeAction" + }, + }) + + expect(didBlurCalled).to.equal(true) + end) + + it("should emit WillFocus on Action event when previois event was WillBlur, while transitioning to child", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, "Bar", NavigationEvents.WillBlur) + + local willFocusCalled = false + childSubscriber.addListener(NavigationEvents.WillFocus, function() + willFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action]({ + state = { + routes = { + { key = "Foo" }, -- Transitioned away from this route! + { key = "Bar" }, + }, + index = 2, + isTransitioning = true, + }, + lastState = { + routes = { + { key = "Foo" }, + { key = "Bar" }, + }, + index = 1, + }, + action = { + type = "SomeAction" + }, + }) + + expect(willFocusCalled).to.equal(true) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildNavigation.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildNavigation.spec.lua new file mode 100644 index 0000000..1ebdf8a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildNavigation.spec.lua @@ -0,0 +1,119 @@ +return function() + local getChildNavigation = require(script.Parent.Parent.getChildNavigation) + + it("should return nil if there is no route matching requested key", function() + local testNavigation = { + state = { + routes = { + { key = "a" } + } + } + } + + local childNav = getChildNavigation(testNavigation, "invalid_child", function() + return testNavigation + end) + + expect(childNav).to.equal(nil) + end) + + it("should return cached child if its state is a top-level route", function() + local testNavigation = { + state = { + routes = { + { key = "a" } + }, + + }, + } + + testNavigation._childrenNavigation = { + a = { + state = testNavigation.state.routes[1] + } + } + + local childNav = getChildNavigation(testNavigation, "a", function() + return testNavigation + end) + + expect(childNav).to.equal(testNavigation._childrenNavigation.a) + end) + + it("should update cache and return new data when child's state has changed", function() + local testNavigation = { + state = { + routes = { + { key = "a", routeName = "a" }, + { key = "b", routeName = "b" }, + }, + index = 1, + }, + router = { + getComponentForRouteName = function(routeName) + return function() end + end, + getActionCreators = function() end + } + } + + local oldStateA = { + state = { + routes = { + { key = "a", routeName = "a" }, + { key = "b", routeName = "b" }, + }, + index = 2, + }, + } + + testNavigation._childrenNavigation = { + a = oldStateA + } + + local childNav = getChildNavigation(testNavigation, "a", function() + return testNavigation + end) + + expect(childNav).to.equal(testNavigation._childrenNavigation["a"]) + expect(childNav.state).to.equal(testNavigation.state.routes[1]) + expect(type(childNav.getParam)).to.equal("function") + end) + + it("should create a new entry if cached child does not exist yet", function() + local testNavigation = { + state = { + routes = { + { key = "a", routeName = "a", params = { a = 1 } }, + { key = "b", routeName = "b" }, + }, + index = 1, + }, + router = { + getComponentForRouteName = function(routeName) + return function() end + end, + getActionCreators = function() end, + }, + addListener = function() + return { + disconnect = function() end + } + end, + isFocused = function() + return true + end, + } + + local childNav = getChildNavigation(testNavigation, "a", function() + return testNavigation + end) + + expect(testNavigation._childrenNavigation["a"]).to.never.equal(nil) + expect(childNav).to.equal(testNavigation._childrenNavigation["a"]) + expect(childNav.isFocused()).to.equal(true) + + expect(childNav.getParam("a", 0)).to.equal(1) + expect(childNav.getParam("b", 0)).to.equal(0) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildrenNavigationCache.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildrenNavigationCache.spec.lua new file mode 100644 index 0000000..b96a218 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildrenNavigationCache.spec.lua @@ -0,0 +1,63 @@ +return function() + local getChildrenNavigationCache = require(script.Parent.Parent.getChildrenNavigationCache) + + it("should return empty table if navigation arg not provided", function() + expect(getChildrenNavigationCache()._childrenNavigation).to.equal(nil) + end) + + it("should populate navigation._childrenNavigation as a side-effect", function() + local navigation = { state = {} } + local result = getChildrenNavigationCache(navigation) + expect(result).to.never.equal(nil) + expect(navigation._childrenNavigation).to.equal(result) + end) + + it("should delete children cache keys that are no longer valid", function() + local navigation = { + state = { + routes = { + { key = "one" }, + { key = "two" }, + { key = "three" }, + } + }, + _childrenNavigation = { + one = {}, + two = {}, + three = {}, + four = {}, + } + } + + local result = getChildrenNavigationCache(navigation) + expect(result.one).to.never.equal(nil) + expect(result.two).to.never.equal(nil) + expect(result.three).to.never.equal(nil) + expect(result.four).to.equal(nil) + end) + + it("should not delete children cache keys if in transitioning state", function() + local navigation = { + state = { + routes = { + { key = "one" }, + { key = "two" }, + { key = "three" }, + }, + isTransitioning = true, + }, + _childrenNavigation = { + one = {}, + two = {}, + three = {}, + four = {}, + } + } + + local result = getChildrenNavigationCache(navigation) + expect(result.one).to.never.equal(nil) + expect(result.two).to.never.equal(nil) + expect(result.three).to.never.equal(nil) + expect(result.four).to.never.equal(nil) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getNavigation.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getNavigation.spec.lua new file mode 100644 index 0000000..bfd1a59 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getNavigation.spec.lua @@ -0,0 +1,94 @@ +return function() + local NavigationEvents = require(script.Parent.Parent.NavigationEvents) + local getNavigation = require(script.Parent.Parent.getNavigation) + + local function makeTestBundle(testState) + testState = testState or { + routes = { + { key = "a" } + }, + index = 1, + } + + local testActions = {} + local bundle = { + testActions = testActions, + testState = testState, + testRouter = { + getActionCreators = function() + return testActions + end + }, + testDispatch = function() end, + testActionSubscribers = {}, + testGetScreenProps = function() end, + } + + function bundle.testGetCurrentNavigation() + return bundle.navigation + end + + bundle.navigation = getNavigation( + bundle.testRouter, + bundle.testState, + bundle.testDispatch, + bundle.testActionSubscribers, + bundle.testGetScreenProps, + bundle.testGetCurrentNavigation + ) + + return bundle + end + + it("should build out correct public props", function() + local bundle = makeTestBundle() + + expect(bundle.navigation.actions).to.equal(bundle.testActions) + expect(bundle.navigation.router).to.equal(bundle.testRouter) + expect(bundle.navigation.state).to.equal(bundle.testState) + expect(bundle.navigation.dispatch).to.equal(bundle.testDispatch) + expect(bundle.navigation.getScreenProps).to.equal(bundle.testGetScreenProps) + expect(#bundle.navigation._childrenNavigation).to.equal(0) + end) + + describe("isFocused tests", function() + it("should return focused=true for child key matching index", function() + local bundle = makeTestBundle() + expect(bundle.navigation.isFocused("a")).to.equal(true) + end) + + it("should return focused=false for child key not matching index", function() + local bundle = makeTestBundle({ + routes = { + { key = "a" }, + { key = "b" }, + }, + index = 2, + }) + expect(bundle.navigation.isFocused("a")).to.equal(false) + end) + + it("should return focused=true if no child key provided (parent always focused)", function() + local bundle = makeTestBundle() + expect(bundle.navigation.isFocused()).to.equal(true) + end) + end) + + describe("addListener tests", function() + it("should short-circuit subscriptions for non-Action events", function() + local bundle = makeTestBundle() + + local testHandler = function() end + bundle.navigation.addListener(NavigationEvents.WillFocus, testHandler) + expect(bundle.testActionSubscribers[testHandler]).to.equal(nil) + end) + + it("should add Action event handlers to actionSubscribers set", function() + local bundle = makeTestBundle() + + local testHandler = function() end + bundle.navigation.addListener(NavigationEvents.Action, testHandler) + expect(bundle.testActionSubscribers[testHandler]).to.equal(true) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/init.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/init.spec.lua new file mode 100644 index 0000000..623fee6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/init.spec.lua @@ -0,0 +1,178 @@ +return function() + local RoactNavigation = require(script.Parent.Parent) + local Roact = require(script.Parent.Parent.Parent.Roact) + local EdgeInsets = require(script.Parent.Parent.EdgeInsets) + local StackHeaderMode = require(script.Parent.Parent.views.StackView.StackHeaderMode) + local StackPresentationStyle = require(script.Parent.Parent.views.StackView.StackPresentationStyle) + local NoneSymbol = require(script.Parent.Parent.NoneSymbol) + + it("should load", function() + require(script.Parent.Parent) + end) + + it("should return a function for createAppContainer", function() + expect(type(RoactNavigation.createAppContainer)).to.equal("function") + end) + + it("should return a function for getNavigation", function() + expect(type(RoactNavigation.getNavigation)).to.equal("function") + end) + + it("should return an appropriate table for Context", function() + expect(type(RoactNavigation.Context)).to.equal("table") + expect(type(RoactNavigation.Context.Provider)).to.equal("table") + expect(type(RoactNavigation.Context.Consumer)).to.equal("table") + expect(type(RoactNavigation.Context.connect)).to.equal("function") + end) + + it("should return a table for Provider", function() + expect(type(RoactNavigation.Provider)).to.equal("table") + end) + + it("should return a table for Consumer", function() + expect(type(RoactNavigation.Consumer)).to.equal("table") + end) + + it("should return a function for connect", function() + expect(type(RoactNavigation.connect)).to.equal("function") + end) + + it("should return a function for withNavigation", function() + expect(type(RoactNavigation.withNavigation)).to.equal("function") + end) + + it("should return a function for withNavigationFocus", function() + expect(type(RoactNavigation.withNavigationFocus)).to.equal("function") + end) + + it("should return a function for createSwitchNavigator", function() + expect(type(RoactNavigation.createSwitchNavigator)).to.equal("function") + end) + + it("should return a function for createTopBarStackNavigator", function() + expect(type(RoactNavigation.createTopBarStackNavigator)).to.equal("function") + end) + + it("should return a function for createNavigator", function() + expect(type(RoactNavigation.createNavigator)).to.equal("function") + end) + + it("should return a function for StackRouter", function() + expect(type(RoactNavigation.StackRouter)).to.equal("function") + end) + + it("should return a function for SwitchRouter", function() + expect(type(RoactNavigation.SwitchRouter)).to.equal("function") + end) + + it("should return a function for TabRouter", function() + expect(type(RoactNavigation.TabRouter)).to.equal("function") + end) + + it("should return a table for Actions", function() + expect(type(RoactNavigation.Actions)).to.equal("table") + end) + + it("should return a table for StackActions", function() + expect(type(RoactNavigation.StackActions)).to.equal("table") + end) + + it("should return a table for BackBehavior", function() + expect(type(RoactNavigation.BackBehavior)).to.equal("table") + end) + + it("should return a table for Events", function() + expect(type(RoactNavigation.Events)).to.equal("table") + end) + + it("should return a valid component for EventsAdapter", function() + expect(RoactNavigation.EventsAdapter.render).never.to.equal(nil) + local instance = Roact.mount(Roact.createElement(RoactNavigation.EventsAdapter, { + navigation = { + addListener = function() + return { disconnect = function() end } + end + } + })) + Roact.unmount(instance) + end) + + it("should return EdgeInsets", function() + expect(RoactNavigation.EdgeInsets).to.equal(EdgeInsets) + end) + + it("should return StackPresentationStyle", function() + expect(RoactNavigation.StackPresentationStyle).to.equal(StackPresentationStyle) + end) + + it("should return StackHeaderMode", function() + expect(RoactNavigation.StackHeaderMode).to.equal(StackHeaderMode) + end) + + it("should return NoneSymbol", function() + expect(RoactNavigation.None).to.equal(NoneSymbol) + end) + + it("should return a valid component for SceneView", function() + expect(RoactNavigation.SceneView.render).never.to.equal(nil) + local instance = Roact.mount(Roact.createElement(RoactNavigation.SceneView, { + navigation = {}, + component = function() end, + })) + Roact.unmount(instance) + end) + + it("should return a valid component for SwitchView", function() + expect(RoactNavigation.SwitchView.render).never.to.equal(nil) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo", } + }, + index = 1, + } + } + + local instance = Roact.mount(Roact.createElement(RoactNavigation.SwitchView, { + descriptors = { + Foo = { + getComponent = function() + return function() end + end, + navigation = testNavigation, + } + }, + navigation = testNavigation, + })) + Roact.unmount(instance) + end) + + it("should return a table for TopBar", function() + expect(type(RoactNavigation.TopBar)).to.equal("table") + end) + + it("should return a table for TopBarBackButton", function() + expect(type(RoactNavigation.TopBarBackButton)).to.equal("table") + end) + + it("should return a table for TopBarTitleContainer", function() + expect(type(RoactNavigation.TopBarTitleContainer)).to.equal("table") + end) + + it("should return a function for createConfigGetter", function() + expect(type(RoactNavigation.createConfigGetter)).to.equal("function") + end) + + it("should return a function for getScreenForRouteName", function() + expect(type(RoactNavigation.getScreenForRouteName)).to.equal("function") + end) + + it("should return a function for validateRouteConfigMap", function() + expect(type(RoactNavigation.validateRouteConfigMap)).to.equal("function") + end) + + it("should return a function for getActiveChildNavigationOptions", function() + expect(type(RoactNavigation.getActiveChildNavigationOptions)).to.equal("function") + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/createAppContainer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/createAppContainer.lua new file mode 100644 index 0000000..fec578a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/createAppContainer.lua @@ -0,0 +1,264 @@ +local Roact = require(script.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Cryo) +local NavigationActions = require(script.Parent.NavigationActions) +local NavigationEvents = require(script.Parent.NavigationEvents) +local AppNavigationContext = require(script.Parent.views.AppNavigationContext) +local getNavigation = require(script.Parent.getNavigation) +local validate = require(script.Parent.utils.validate) + +local function validateProps(props) + if not props.navigation then + return + end + + local errStr = + "This navigator has both 'navigation' and container props. " .. + "It is unclear if it should own its own state. Remove the " .. + "container props or don't pass a 'navigation' prop." + + for key in pairs(props) do + validate(key == "screenProps" or key == "navigation", errStr) + end +end + +--[[ + Construct a container Roact component that will host the navigation hierarchy + specified by your main AppComponent. AppComponent must be a navigator created by + a Roact-Navigation helper function, or a stateful Roact component + + If you are using a custom stateful Roact component, make sure to set the 'router' + field so that it can be hooked into the navigation system. You must also pass your + 'navigation' prop to any child navigators. + + Additional props: + renderLoading - Roact component to render while the app is loading. + backActionSignal - Signal that allows the container to listen to external + back action events (e.g. Android back button). +]] +return function(AppComponent) + validate(type(AppComponent) == "table" and AppComponent.router ~= nil, + "AppComponent must be a navigator or a stateful Roact component with a 'router' field") + + local containerName = string.format("NavigationContainer(%s)", tostring(AppComponent)) + local NavigationContainer = Roact.Component:extend(containerName) + + function NavigationContainer.getDerivedStateFromProps(nextProps) + validateProps(nextProps) + return nil + end + + function NavigationContainer:init() + validateProps(self.props) + + local backActionSignal = self.props.backActionSignal + + self._actionEventSubscribers = {} + self._initialAction = NavigationActions.init() + + local containerIsStateful = self:_isStateful() + + if containerIsStateful and backActionSignal ~= nil then + self.subs = backActionSignal.connect(function() + if not self._isMounted then + if self.subs then + self.subs.disconnect() + self.subs = nil + end + else + self:dispatch(NavigationActions.back()) + end + end) + end + + local initialNav = nil + if containerIsStateful and not self.props.persistenceKey then + initialNav = AppComponent.router.getStateForAction(self._initialAction) + end + + self.state = { + nav = initialNav, + } + end + + function NavigationContainer:_renderLoading() + local renderLoading = self.props.renderLoading + if renderLoading then + return renderLoading() + else + return nil + end + end + + function NavigationContainer:render() + local navigation = self.props.navigation + + if self:_isStateful() then + local navState = self.state.nav + if not navState then + return self:_renderLoading() + end + + if not self._navigation or self._navigation.state ~= navState then + self._navigation = getNavigation( + AppComponent.router, + navState, + function(...) + return self:dispatch(...) + end, + self._actionEventSubscribers, + function(...) + return self:_getScreenProps(...) + end, + function() + return self._navigation + end + ) + end + + navigation = self._navigation + end + + validate(navigation ~= nil, "failed to get navigation") + + return Roact.createElement(AppNavigationContext.Provider, { + navigation = navigation, + }, { + -- Provide navigation prop for top-level component so it doesn't have to connect. + AppComponent = Roact.createElement(AppComponent, Cryo.Dictionary.join(self.props, { + navigation = navigation, + })) + }) + end + + function NavigationContainer:didMount() + self._isMounted = true + + if not self:_isStateful() then + return + end + + local action = self._initialAction + local startupState = self.state.nav + + if not startupState then + startupState = AppComponent.router.getStateForAction(action) + end + + local function dispatchActionEvents() + -- _actionEventSubscribers is a table(handler, true), e.g. a Set container + for subscriber in pairs(self._actionEventSubscribers) do + subscriber({ + type = NavigationEvents.Action, + action = action, + state = self.state.nav, + -- there is no lastState for initial mounting + }) + end + end + + if startupState ~= self.state.nav then + self:setState({ + nav = startupState + }) + end + + dispatchActionEvents() + end + + function NavigationContainer:willUnmount() + self._isMounted = false + + -- TODO: Disconnect from from URL listener once implemented + + if self.subs then + self.subs.disconnect() + self.subs = nil + end + end + + function NavigationContainer:didUpdate() + -- Clear cached _navState every time we update. + if self._navState == self.state.nav then + self._navState = nil + end + end + + function NavigationContainer:_isStateful() + return not self.props.navigation + end + + -- NOTE: Not implementing _validateProps; it is duplicate + + -- NOTE: Not implementing _handleOpenURL; app should have a component + -- that transforms URLs into paths for AppContainer instead. + + function NavigationContainer:_onNavigationStateChange(prevNav, nextNav, action) + local onNavigationStateChange = self.props.onNavigationStateChange + + if type(onNavigationStateChange) == "function" then + onNavigationStateChange(prevNav, nextNav, action) + end + end + + function NavigationContainer:_getScreenProps() + return self.props.screenProps + end + + function NavigationContainer:dispatch(action) + if self.props.navigation then + return self.props.navigation.dispatch(action) + end + + self._navState = self._navState or self.state.nav + + local lastNavState = self._navState + validate(lastNavState ~= nil, "navState should be set in constructor if stateful") + + local reducedState = AppComponent.router.getStateForAction(action, lastNavState) + local navState = reducedState + if not navState then + navState = lastNavState + end + + local function dispatchActionEvents() + -- _actionEventSubscribers is a table(handler, true), e.g. a Set container + for subscriber in pairs(self._actionEventSubscribers) do + subscriber({ + type = NavigationEvents.Action, + action = action, + state = navState, + lastState = lastNavState, + }) + end + end + + if reducedState == nil then + -- Router returns nil when action has been handled and there is no state change. + -- dispatch() must return true whenever something has been handled. + dispatchActionEvents() + return true + end + + if navState ~= lastNavState then + -- Update cache to ensure that subsequent calls do not discard this change + self._navState = navState + + -- TODO: We have to dispatch events before or after setState (which mounts/unmounts components) + -- based upon the specific event type, to ensure that pages get them in the correct order... + + self:setState({ + nav = navState + }) + + self:_onNavigationStateChange(lastNavState, navState, action) + dispatchActionEvents() + -- TODO: Add call to persist navigation state here, if we ever implement it. + return true + end + + dispatchActionEvents() + return false + end + + return NavigationContainer +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildEventSubscriber.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildEventSubscriber.lua new file mode 100644 index 0000000..0773952 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildEventSubscriber.lua @@ -0,0 +1,181 @@ +local Cryo = require(script.Parent.Parent.Cryo) +local NavigationEvents = require(script.Parent.NavigationEvents) +local validate = require(script.Parent.utils.validate) + +--[[ + This utility will fire focus and blur events for the child based upon action events + and the current navigation state. +]] +return function(addListener, key, initialLastFocusEvent) + initialLastFocusEvent = initialLastFocusEvent or NavigationEvents.DidBlur + + local upstreamSubscribers = {} + + local subscriberMap = { + [NavigationEvents.Action] = {}, + [NavigationEvents.WillFocus] = {}, + [NavigationEvents.DidFocus] = {}, + [NavigationEvents.WillBlur] = {}, + [NavigationEvents.DidBlur] = {}, + [NavigationEvents.Refocus] = {}, + } + + local function disconnectAll() + for _, subscriberList in pairs(subscriberMap) do + for x in pairs(subscriberList) do + subscriberList[x] = nil + end + end + + for _, subs in pairs(upstreamSubscribers) do + if subs then + subs.disconnect() + end + end + end + + local function emit(subscriberType, payload) + local payloadWithType = Cryo.Dictionary.join(payload or {}, { type = subscriberType }) + local subscribers = subscriberMap[subscriberType] + if subscribers then + for _, subs in ipairs(subscribers) do + subs(payloadWithType) + end + end + end + + -- lastFocusEvent keeps track of focus state for one route. We assume that we are initially + -- in blurred state. If we are focused on initialization, then the first NavigationEvents.Action + -- will cause onFocus+willFocus to fire because we started off 'blurred'. + local lastFocusEvent = initialLastFocusEvent + + for eventType in pairs(subscriberMap) do + upstreamSubscribers[eventType] = addListener(eventType, function(payload) + if eventType == NavigationEvents.Refocus then + emit(eventType, payload) + return + end + + local state = payload.state + local lastState = payload.lastState + local action = payload.action + + local lastRoutes = lastState and lastState.routes + local routes = state and state.routes + + local focusKey = routes and routes[state.index].key or nil + local isChildFocused = focusKey == key + + local lastRoute = nil + if lastRoutes then + for _, route in ipairs(lastRoutes) do + if route.key == key then + lastRoute = route + break + end + end + end + + local newRoute = nil + if routes then + for _, route in ipairs(routes) do + if route.key == key then + newRoute = route + break + end + end + end + + local childPayload = { + context = string.format("%s:%s_%s", key, tostring(action.type), payload.context or 'Root'), + state = newRoute, + lastState = lastRoute, + action = action, + type = eventType, + } + + local isTransitioning = state and state.isTransitioning or false + + local previouslyLastFocusEvent = lastFocusEvent + + if lastFocusEvent == NavigationEvents.DidBlur then + -- Child is currently blurred; look for willFocus conditions + if isChildFocused and (eventType == NavigationEvents.WillFocus + or eventType == NavigationEvents.Action) then + lastFocusEvent = NavigationEvents.WillFocus + emit(lastFocusEvent, childPayload) + end + end + + if lastFocusEvent == NavigationEvents.WillFocus then + -- We are mid-focus. Look for didFocus conditions. If state.isTransitioning is false + -- then we know this child event happens immediately after willFocus + if (eventType == NavigationEvents.DidFocus or eventType == NavigationEvents.Action) + and isChildFocused and not isTransitioning then + lastFocusEvent = NavigationEvents.DidFocus + emit(lastFocusEvent, childPayload) + end + end + + if lastFocusEvent == NavigationEvents.DidFocus then + -- The child is currently focused. Look for blurring events. + if not isChildFocused or eventType == NavigationEvents.WillBlur then + lastFocusEvent = NavigationEvents.WillBlur + emit(lastFocusEvent, childPayload) + elseif eventType == NavigationEvents.Action + and previouslyLastFocusEvent == NavigationEvents.DidFocus then + -- While focused, pass action events to children to be handled by focused grandchildren + emit(NavigationEvents.Action, childPayload) + end + end + + if lastFocusEvent == NavigationEvents.WillBlur then + -- The child is mid-blur. Wait for transition to end. + if eventType == NavigationEvents.Action and not isChildFocused and not isTransitioning then + -- Child is done blurring because transitioning ended or there is no transition to do + lastFocusEvent = NavigationEvents.DidBlur + emit(lastFocusEvent, childPayload) + elseif eventType == NavigationEvents.DidBlur then + -- Pass through parent's DidBlur event + lastFocusEvent = NavigationEvents.DidBlur + emit(lastFocusEvent, childPayload) + elseif eventType == NavigationEvents.Action and isChildFocused and isTransitioning then + lastFocusEvent = NavigationEvents.WillFocus + emit(lastFocusEvent, childPayload) + end + end + + if lastFocusEvent == NavigationEvents.DidBlur and not newRoute then + -- Page is dead, disconnect subscribers + disconnectAll() + end + end) + end + + return { + addListener = function(eventType, eventHandler) + local subscribers = subscriberMap[eventType] + validate(subscribers ~= nil, "Invalid event type '%s'", tostring(eventType)) + validate(type(eventHandler) == "function", + "eventHandler for '%s' must be a function", tostring(eventType)) + table.insert(subscribers, eventHandler) + return { + disconnect = function() + for idx, subs in ipairs(subscribers) do + if subs == eventHandler then + table.remove(subscribers, idx) + break + end + end + end + } + end, + emit = function(eventType, payload) + validate(eventType == NavigationEvents.Refocus, + "navigation.emit only supports NavigationEvents.Refocus currently.") + validate(payload == nil or type(payload) == "table", + "navigation.emit payloads must be a table or nil") + emit(eventType, payload) + end, + } +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildNavigation.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildNavigation.lua new file mode 100644 index 0000000..b4afb1b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildNavigation.lua @@ -0,0 +1,115 @@ +local Cryo = require(script.Parent.Parent.Cryo) +local getChildEventSubscriber = require(script.Parent.getChildEventSubscriber) +local getChildRouter = require(script.Parent.routers.getChildRouter) +local getNavigationActionCreators = require(script.Parent.routers.getNavigationActionCreators) +local getChildrenNavigationCache = require(script.Parent.getChildrenNavigationCache) + +local function createParamGetter(route) + return function(paramName, defaultValue) + local params = route.params + return params and params[paramName] or defaultValue + end +end + +local function getChildNavigation(navigation, childKey, getCurrentParentNavigation) + local children = getChildrenNavigationCache(navigation) + + local childRoute = nil + for _, route in ipairs(navigation.state.routes) do + if route.key == childKey then + childRoute = route + break + end + end + + if not childRoute then + return nil + end + + local requestedChild = children[childKey] + + if requestedChild and requestedChild.state == childRoute then + return requestedChild + end + + local childRouter = getChildRouter(navigation.router, childRoute.routeName) + + -- If the route has children that match our routes schema then get a reference + -- to the focused grandchild so we can pass the correct action creators to the + -- child router so that any action that depends on the child route will behave + -- as expected. + local focusedGrandChildRoute = nil + if childRoute.routes and type(childRoute.index) == "number" then + focusedGrandChildRoute = childRoute.routes[childRoute.index] + end + + local childRouterActionCreators = childRouter and + childRouter.getActionCreators(focusedGrandChildRoute, childRoute.key) or {} + + local actionCreators = Cryo.Dictionary.join( + navigation.actions or {}, + navigation.router.getActionCreators(childRoute, navigation.state.key) or {}, + childRouterActionCreators or {}, + getNavigationActionCreators(childRoute) or {}) + + local actionHelpers = {} + for key, creator in pairs(actionCreators) do + actionHelpers[key] = function(...) + local action = creator(...) + return navigation.dispatch(action) + end + end + + if requestedChild then + -- Update cache value for requestedChild because child's state has changed + children[childKey] = Cryo.Dictionary.join(requestedChild, actionHelpers, { + state = childRoute, + router = childRouter, + actions = actionCreators, + getParam = createParamGetter(childRoute), + }) + + return children[childKey] + else + -- No cached value for requestedChild. Create a new entry. + local childSubscriber = getChildEventSubscriber(navigation.addListener, childKey) + + children[childKey] = Cryo.Dictionary.join(actionHelpers, { + state = childRoute, + router = childRouter, + actions = actionCreators, + getParam = createParamGetter(childRoute), + getChildNavigation = function(grandChildKey) + return getChildNavigation(children[childKey], grandChildKey, function() + local nav = getCurrentParentNavigation() + return nav and nav.getChildNavigation(childKey) or nil + end) + end, + isFocused = function() + local currentNavigation = getCurrentParentNavigation() + if not currentNavigation then + return false + end + + local state = currentNavigation.state + local routes = state.routes + local index = state.index + + if not currentNavigation.isFocused() then + return false + end + + -- If we're transitioning to this state then we are NOT focused until the transition is over. + return (routes[index].key == childKey and state.isTransitioning ~= true) or false + end, + dispatch = navigation.dispatch, + getScreenProps = navigation.getScreenProps, + addListener = childSubscriber.addListener, + emit = childSubscriber.emit, + }) + + return children[childKey] + end +end + +return getChildNavigation diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildrenNavigationCache.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildrenNavigationCache.lua new file mode 100644 index 0000000..b9271da --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildrenNavigationCache.lua @@ -0,0 +1,26 @@ +return function(navigation) + if not navigation then + return {} + end + + if not navigation._childrenNavigation then + navigation._childrenNavigation = {} + end + + local childrenNavigationCache = navigation._childrenNavigation + + local childKeys = {} + for _, route in ipairs(navigation.state.routes or {}) do + childKeys[route.key] = true + end + + if not navigation.state.isTransitioning then + for cacheKey, _ in pairs(childrenNavigationCache) do + if not childKeys[cacheKey] then + childrenNavigationCache[cacheKey] = nil + end + end + end + + return navigation._childrenNavigation +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getNavigation.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getNavigation.lua new file mode 100644 index 0000000..2b8568f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getNavigation.lua @@ -0,0 +1,52 @@ +local Cryo = require(script.Parent.Parent.Cryo) +local NavigationEvents = require(script.Parent.NavigationEvents) +local getNavigationActionCreators = require(script.Parent.routers.getNavigationActionCreators) +local getChildNavigation = require(script.Parent.getChildNavigation) +local getChildrenNavigationCache = require(script.Parent.getChildrenNavigationCache) + +return function(router, state, dispatch, actionSubscribers, getScreenProps, getCurrentNavigation) + local actions = router.getActionCreators(state, nil) + + local navigation = { + actions = actions, + router = router, + state = state, + dispatch = dispatch, + getScreenProps = getScreenProps, + _childrenNavigation = getChildrenNavigationCache(getCurrentNavigation()), + } + + function navigation.getChildNavigation(childKey) + return getChildNavigation(navigation, childKey, getCurrentNavigation) + end + + function navigation.isFocused(childKey) + local routes = getCurrentNavigation().state.routes + local index = getCurrentNavigation().state.index + + return not childKey or routes[index].key == childKey + end + + function navigation.addListener(event, handler) + if event ~= NavigationEvents.Action then + return { disconnect = function() end } + else + actionSubscribers[handler] = true + return { + disconnect = function() + actionSubscribers[handler] = nil + end + } + end + end + + local actionCreators = Cryo.Dictionary.join(getNavigationActionCreators(navigation.state), actions) + + for actionName, _ in pairs(actionCreators) do + navigation[actionName] = function(...) + navigation.dispatch(actionCreators[actionName](...)) + end + end + + return navigation +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/init.lua new file mode 100644 index 0000000..c159c82 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/init.lua @@ -0,0 +1,60 @@ +-- Generator information: +-- Human name: Roact Navigation +-- Variable name: RoactNavigation +-- Repo name: roact-navigation + +return { + -- Navigation container construction + createAppContainer = require(script.createAppContainer), + getNavigation = require(script.getNavigation), + + -- Context Access + Context = require(script.views.AppNavigationContext), + Provider = require(script.views.AppNavigationContext).Provider, + Consumer = require(script.views.AppNavigationContext).Consumer, + connect = require(script.views.AppNavigationContext).connect, + + withNavigation = require(script.views.withNavigation), + withNavigationFocus = require(script.views.withNavigationFocus), + + -- Navigators + createTopBarStackNavigator = require(script.navigators.createTopBarStackNavigator), + createSwitchNavigator = require(script.navigators.createSwitchNavigator), + createNavigator = require(script.navigators.createNavigator), + + -- Routers + StackRouter = require(script.routers.StackRouter), + SwitchRouter = require(script.routers.SwitchRouter), + TabRouter = require(script.routers.TabRouter), + + -- Navigation Actions + Actions = require(script.NavigationActions), + StackActions = require(script.StackActions), + BackBehavior = require(script.BackBehavior), + + -- Navigation Events + Events = require(script.NavigationEvents), + EventsAdapter = require(script.views.NavigationEventsAdapter), + + -- Additional Types + EdgeInsets = require(script.EdgeInsets), + StackPresentationStyle = require(script.views.StackView.StackPresentationStyle), + StackHeaderMode = require(script.views.StackView.StackHeaderMode), + None = require(script.NoneSymbol), + + -- Screen Views + SceneView = require(script.views.SceneView), + SwitchView = require(script.views.SwitchView), + StackView = require(script.views.StackView.StackView), + + -- Top Bar Components + TopBar = require(script.views.TopBar.TopBar), + TopBarBackButton = require(script.views.TopBar.TopBarBackButton), + TopBarTitleContainer = require(script.views.TopBar.TopBarTitleContainer), + + -- Utilities + createConfigGetter = require(script.routers.createConfigGetter), + getScreenForRouteName = require(script.routers.getScreenForRouteName), + validateRouteConfigMap = require(script.routers.validateRouteConfigMap), + getActiveChildNavigationOptions = require(script.utils.getActiveChildNavigationOptions), +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createNavigator.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createNavigator.spec.lua new file mode 100644 index 0000000..e890099 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createNavigator.spec.lua @@ -0,0 +1,78 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local createNavigator = require(script.Parent.Parent.createNavigator) + + local testRouter = { + getScreenOptions = function() return nil end, + } + + it("should return a Roact component that exposes navigator fields", function() + local testComponentMounted = nil + local TestViewComponent = Roact.Component:extend("TestViewComponent") + function TestViewComponent:render() end + function TestViewComponent:didMount() testComponentMounted = true end + function TestViewComponent:willUnmount() testComponentMounted = false end + + local testNavOptions = {} + + local navigator = createNavigator(TestViewComponent, testRouter, { + navigationOptions = testNavOptions, + }) + + expect(type(navigator.render)).to.equal("function") + expect(navigator.router).to.equal(testRouter) + expect(navigator.navigationOptions).to.equal(testNavOptions) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + }, + index = 1 + }, + getChildNavigation = function() return nil end, -- stub + } + + -- Try to mount it + local instance = Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + + expect(testComponentMounted).to.equal(true) + Roact.unmount(instance) + expect(testComponentMounted).to.equal(false) + end) + + it("should throw when trying to mount without navigation prop", function() + local TestViewComponent = function() end + + local navigator = createNavigator(TestViewComponent, testRouter, { + navigationOptions = {} + }) + + expect(function() + Roact.mount(Roact.createElement(navigator)) + end).to.throw() + end) + + it("should throw when trying to mount without routes", function() + local TestViewComponent = function() end + + local navigator = createNavigator(TestViewComponent, testRouter, { + navigationOptions = {} + }) + + local testNavigation = { + state = { + index = 1 + }, + getChildNavigation = function() return nil end, -- stub + } + + expect(function() + Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + end).to.throw() + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createSwitchNavigator.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createSwitchNavigator.spec.lua new file mode 100644 index 0000000..c331c26 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createSwitchNavigator.spec.lua @@ -0,0 +1,43 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local createSwitchNavigator = require(script.Parent.Parent.createSwitchNavigator) + local getChildNavigation = require(script.Parent.Parent.Parent.getChildNavigation) + + it("should return a mountable Roact component", function() + local navigator = createSwitchNavigator({ + routes = { + Foo = function() end + }, + initialRouteName = "Foo", + }) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + }, + index = 1 + }, + router = navigator.router + } + + function testNavigation.getChildNavigation(childKey) + return getChildNavigation(testNavigation, childKey, function() + return testNavigation + end) + end + + function testNavigation.addListener(symbol, callback) + return { + disconnect = function() end + } + end + + local instance = Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + + Roact.unmount(instance) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createTopBarStackNavigator.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createTopBarStackNavigator.spec.lua new file mode 100644 index 0000000..9decdf8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createTopBarStackNavigator.spec.lua @@ -0,0 +1,43 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local createTopBarStackNavigator = require(script.Parent.Parent.createTopBarStackNavigator) + local getChildNavigation = require(script.Parent.Parent.Parent.getChildNavigation) + + it("should return a mountable Roact component", function() + local navigator = createTopBarStackNavigator({ + routes = { + Foo = function() end + }, + initialRouteName = "Foo", + }) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + }, + index = 1 + }, + router = navigator.router + } + + function testNavigation.getChildNavigation(childKey) + return getChildNavigation(testNavigation, childKey, function() + return testNavigation + end) + end + + function testNavigation.addListener(symbol, callback) + return { + disconnect = function() end + } + end + + local instance = Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + + Roact.unmount(instance) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createNavigator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createNavigator.lua new file mode 100644 index 0000000..5d60055 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createNavigator.lua @@ -0,0 +1,78 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local validate = require(script.Parent.Parent.utils.validate) + +return function(navigatorViewComponent, router, navigationConfig) + local Navigator = Roact.Component:extend("Navigator") + + -- These statics need to be accessible to routers + Navigator.router = router + Navigator.navigationOptions = navigationConfig.navigationOptions + + function Navigator:init() + local screenProps = self.props.screenProps + + self.state = { + descriptors = {}, + screenProps = screenProps + } + end + + function Navigator.getDerivedStateFromProps(nextProps, prevState) + local prevDescriptors = prevState.descriptors + local navigation = nextProps.navigation + local screenProps = nextProps.screenProps + + validate(navigation ~= nil, "The navigation prop is missing for this navigator") + + local routes = navigation.state.routes + + validate(type(routes) == "table", "No 'routes' found in navigation state. " .. + "Don't try to pass the navigation prop from a Roact component to a Navigator child.") + + local descriptors = {} + + for _, route in ipairs(routes) do + if prevDescriptors and prevDescriptors[route.key] and + route == prevDescriptors[route.key].state and + screenProps == prevState.screenProps then + descriptors[route.key] = prevDescriptors[route.key] + else + local getComponent = function() + return router.getComponentForRouteName(route.routeName) + end + + local childNavigation = navigation.getChildNavigation(route.key) + local options = router.getScreenOptions(childNavigation, screenProps) + + descriptors[route.key] = { + key = route.key, + getComponent = getComponent, + options = options, + state = route, + navigation = childNavigation, + } + end + end + + return { + descriptors = descriptors, + screenProps = screenProps, + } + end + + function Navigator:render() + local navigation = self.props.navigation + local screenProps = self.state.screenProps + local descriptors = self.state.descriptors + + return Roact.createElement(navigatorViewComponent, Cryo.Dictionary.join(self.props, { + screenProps = screenProps, + navigation = navigation, + navigationConfig = navigationConfig, + descriptors = descriptors, + })) + end + + return Navigator +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createSwitchNavigator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createSwitchNavigator.lua new file mode 100644 index 0000000..a99c98d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createSwitchNavigator.lua @@ -0,0 +1,12 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local createNavigator = require(script.Parent.createNavigator) +local SwitchRouter = require(script.Parent.Parent.routers.SwitchRouter) +local SwitchView = require(script.Parent.Parent.views.SwitchView) + +return function(config) + local router = SwitchRouter(config) + return createNavigator(SwitchView, router, Cryo.Dictionary.join(config, { + routes = Cryo.None, -- navigator config doesn't need routes, remove from props + })) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createTopBarStackNavigator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createTopBarStackNavigator.lua new file mode 100644 index 0000000..35697c2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createTopBarStackNavigator.lua @@ -0,0 +1,12 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local createNavigator = require(script.Parent.createNavigator) +local StackRouter = require(script.Parent.Parent.routers.StackRouter) +local StackView = require(script.Parent.Parent.views.StackView.StackView) + +return function(config) + local router = StackRouter(config) + return createNavigator(StackView, router, Cryo.Dictionary.join(config, { + routes = Cryo.None, -- navigator config doesn't need routes + })) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/StackRouter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/StackRouter.lua new file mode 100644 index 0000000..809d18a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/StackRouter.lua @@ -0,0 +1,564 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local NavigationActions = require(script.Parent.Parent.NavigationActions) +local StackActions = require(script.Parent.Parent.StackActions) +local KeyGenerator = require(script.Parent.Parent.utils.KeyGenerator) +local StateUtils = require(script.Parent.Parent.StateUtils) +local getScreenForRouteName = require(script.Parent.getScreenForRouteName) +local createConfigGetter = require(script.Parent.createConfigGetter) +local validateRouteConfigMap = require(script.Parent.validateRouteConfigMap) +local validate = require(script.Parent.Parent.utils.validate) +local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol) +local NoneSymbol = require(script.Parent.Parent.NoneSymbol) + +local STACK_ROUTER_ROOT_KEY = "StackRouterRoot" +local CHILD_IS_SCREEN = NavigationSymbol("CHILD_IS_SCREEN") + +local defaultActionCreators = function() return {} end + +local function behavesLikePushAction(action) + return action.type == NavigationActions.Navigate or + action.type == StackActions.Push +end + +local function isResetToRootStack(action) + return action.type == StackActions.Reset and action.key == NoneSymbol +end + +return function(config) + validate(type(config) == "table", "config must be a table") + + local routeConfigs = validateRouteConfigMap(config.routes) + local routeNames = Cryo.Dictionary.keys(routeConfigs) + + -- find child child routers + local childRouters = {} + for _, routeName in ipairs(routeNames) do + local screen = getScreenForRouteName(routeConfigs, routeName) + if type(screen) == "table" and screen.router then + -- if it has a router then it's a navigator + childRouters[routeName] = screen.router + else + -- TODO: This is a hack to make this code behave like React-Navigation's usage of + -- null and undefined values for childRouters. We should come up with a better approach. + childRouters[routeName] = CHILD_IS_SCREEN + end + end + + local getCustomActionCreators = config.getCustomActionCreators or defaultActionCreators + + local initialRouteParams = config.initialRouteParams or {} + local initialRouteName = validate(config.initialRouteName, "initialRouteName must be provided") + + local initialRouteIndex = Cryo.List.find(routeNames, initialRouteName) + + -- dump an error if initialRouteName is not in routes. + if initialRouteIndex == nil then + local availableRouteStr = "" + for _, name in ipairs(routeNames) do + availableRouteStr = availableRouteStr .. name .. "," + end + + error(string.format("Invalid initialRouteName '%s'. Must be one of [%s]", initialRouteName, availableRouteStr), 2) + end + + local initialChildRouter = childRouters[initialRouteName] + + local function getInitialState() + local route = {} + + if initialChildRouter ~= nil and initialChildRouter ~= CHILD_IS_SCREEN then + route = initialChildRouter.getStateForAction(NavigationActions.init({ + params = initialRouteParams, + })) + end + + local initialRouteConfig = routeConfigs[initialRouteName] + local initialRouteConfigParams = type(initialRouteConfig) == "table" and initialRouteConfig.params or {} + + local params = Cryo.Dictionary.join( + initialRouteConfigParams, -- params set in routes table! + route.params or {}, + initialRouteParams or {} -- params provided at top level + ) + + local initialRouteKey = config.initialRouteKey + route = Cryo.Dictionary.join(route, params, { + routeName = initialRouteName, + key = initialRouteKey or KeyGenerator.generateKey() + }) + + return { + key = STACK_ROUTER_ROOT_KEY, + isTransitioning = false, + index = 1, + routes = { route } + } + end + + local function getParamsForRouteAndAction(routeName, action) + local routeConfig = routeConfigs[routeName] + if type(routeConfig) == "table" and routeConfig.params then + return Cryo.Dictionary.join(routeConfig.params, action.params) + else + return action.params + end + end + + -- Strip out the CHILD_IS_SCREEN hacked elements before exposing publicly. + local strippedChildRouters = {} + for routerName, router in pairs(childRouters) do + if router ~= CHILD_IS_SCREEN then + strippedChildRouters[routerName] = router + end + end + + local StackRouter = { + childRouters = strippedChildRouters, + getScreenOptions = createConfigGetter(routeConfigs, config.defaultNavigationOptions), + _CHILD_IS_SCREEN = CHILD_IS_SCREEN, -- expose symbol for testing purposes + } + + function StackRouter.getComponentForState(state) + local activeChildRoute = state.routes[state.index] or {} + local routeName = activeChildRoute.routeName + validate(routeName, "There is no route defined for index '%d'. " .. + "Make sure that you passed in a navigation state with a " .. + "valid stack index.", state.index) + + local childRouter = childRouters[routeName] + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + return childRouters[routeName].getComponentForState(activeChildRoute) + end + + return getScreenForRouteName(routeConfigs, routeName) + end + + function StackRouter.getComponentForRouteName(routeName) + return getScreenForRouteName(routeConfigs, routeName) + end + + function StackRouter.getActionCreators(route, navStateKey) + return Cryo.Dictionary.join(getCustomActionCreators(route, navStateKey), { + pop = function(n, params) + return StackActions.pop(Cryo.Dictionary.join({ + n = n, + }, params or {})) + end, + popToTop = function(params) + return StackActions.popToTop(params) + end, + push = function(routeName, params, action) + return StackActions.push({ + routeName = routeName, + params = params, + action = action, + }) + end, + replace = function(replaceWith, params, action, newKey) + if type(replaceWith) == "string" then + return StackActions.replace({ + routeName = replaceWith, + params = params, + action = action, + key = route.key, + newKey = newKey, + }) + end + + validate(type(replaceWith) == "table", "replaceWith must be a table or string") + validate(params == nil, "params cannot be provided to .replace() when specifying a table") + validate(action == nil, "Child action cannot be provided to .replace() when specifying a table") + validate(newKey == nil, "newKey cannot be provided to .replace() when specifying a table") + + return StackActions.replace(replaceWith) + end, + reset = function(actions, index) + local resetIndex = index + if index == nil then + resetIndex = #actions - 1 + end + + return StackActions.reset({ + actions = actions, + index = resetIndex, + key = navStateKey, + }) + end, + dismiss = function() + return NavigationActions.back({ + key = navStateKey, + }) + end, + }) + end + + function StackRouter.getStateForAction(action, state) + -- Set up initial state if needed + state = state or getInitialState() + + local activeChildRoute = state.routes[state.index] + + if not isResetToRootStack(action) and action.type ~= NavigationActions.Navigate then + local activeChildRouter = childRouters[activeChildRoute.routeName] + if activeChildRouter ~= nil and activeChildRouter ~= CHILD_IS_SCREEN then + local route = activeChildRouter.getStateForAction(action, activeChildRoute) + if route ~= nil and route ~= activeChildRoute then + return StateUtils.replaceAt( + state, + activeChildRoute.key, + route, + action.type == NavigationActions.SetParams -- don't change index for setParam action + ) + end + end + elseif action.type == NavigationActions.Navigate then + -- Traverse routes from top of the stack to the bottom; active route has first opportunity + for i = #state.routes, 1, -1 do + local childRoute = state.routes[i] + local childRouter = childRouters[childRoute.routeName] + local childAction = action + if action.routeName == childRoute.routeName and action.action then + childAction = action.action + end + + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + local nextRouteState = childRouter.getStateForAction(childAction, childRoute) + if nextRouteState == nil or nextRouteState ~= childRoute then + local newState = StateUtils.replaceAndPrune( + state, + nextRouteState and nextRouteState.key or childRoute.key, + nextRouteState and nextRouteState or childRoute + ) + + local newTransitioning = state.isTransitioning + if state.index ~= newState.index then + newTransitioning = action.immediate ~= true + end + + return Cryo.Dictionary.join(newState, { + isTransitioning = newTransitioning, + }) + end + end + end + end + + -- Handle push and navigation actions. This must happen after focused child router + -- has had its chance to handle the action. + if behavesLikePushAction(action) and childRouters[action.routeName] ~= nil then + local childRouter = childRouters[action.routeName] + validate(action.type ~= StackActions.Push or action.key == nil, + "StackRouter does not support key on the push action") + + -- Before pushing new route, try to find existing one in the stack. + local lastRouteIndex = nil + for idx, route in ipairs(state.routes) do + if (action.key and route.key == action.key) or (route.routeName == action.routeName) then + lastRouteIndex = idx + break + end + end + + -- An instance of this route exists already and we're dealing with a Navigate action. + if action.type ~= StackActions.Push and lastRouteIndex ~= nil then + -- If index or params have not changed, leave state alone + if state.index == lastRouteIndex and not action.params then + return nil + end + + -- Remove unused routes at tail + local routes = Cryo.List.removeRange(state.routes, lastRouteIndex + 1, #state.routes) + + -- Apply params if provided + if action.params then + local route = state.routes[lastRouteIndex] + routes[lastRouteIndex] = Cryo.Dictionary.join(route, { + params = Cryo.Dictionary.join(route.params or {}, action.params) + }) + end + + -- Return state with new index, changing isTransitioning only if index has changed + local newIsTransitioning = state.isTransitioning + if state.index ~= lastRouteIndex then + newIsTransitioning = action.immediate ~= true + end + + return Cryo.Dictionary.join(state, { + isTransitioning = newIsTransitioning, + index = lastRouteIndex, + routes = routes, + }) + end + + local route + if childRouter ~= CHILD_IS_SCREEN then + -- Delegate to child router + local childAction = action.action or NavigationActions.init({ + params = getParamsForRouteAndAction(action.routeName, action) + }) + + route = Cryo.Dictionary.join({ + -- TODO: Does it make sense to wipe out the params here, or to incorporate params at all? + params = getParamsForRouteAndAction(action.routeName, action), + }, childRouter.getStateForAction(childAction), { + routeName = action.routeName, + key = action.key or KeyGenerator.generateKey(), + }) + else + -- Create new route from scratch + route = { + params = getParamsForRouteAndAction(action.routeName, action), + routeName = action.routeName, + key = action.key or KeyGenerator.generateKey(), + } + + end + + return Cryo.Dictionary.join(StateUtils.push(state, route), { + isTransitioning = action.immediate ~= true, + }) + elseif action.type == StackActions.Push and childRouters[action.routeName] == nil then + -- Return original state to bubble the action up + return state + end + + -- Handle navigation to other child routers that are not pushed yet. + if behavesLikePushAction(action) then + local childRouterNames = Cryo.Dictionary.keys(childRouters) + for _, childRouterName in ipairs(childRouterNames) do + local childRouter = childRouters[childRouterName] + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + -- Start with blank state for each child router + local initChildRoute = childRouter.getStateForAction(NavigationActions.init()) + + -- Check to see if it handles our action + local navigatedChildRoute = childRouter.getStateForAction(action, initChildRoute) + + local routeToPush = nil + if navigatedChildRoute == nil then + -- Push initial route if the router returned nil when handling action + routeToPush = initChildRoute + elseif navigatedChildRoute ~= initChildRoute then + -- Push new route if state changed in response to this action + routeToPush = navigatedChildRoute + end + + if routeToPush then + local route = Cryo.Dictionary.join(routeToPush, { + routeName = childRouterName, + key = action.key or KeyGenerator.generateKey(), + }) + + return Cryo.Dictionary.join(StateUtils.push(state, route), { + isTransitioning = action.immediate ~= true, + }) + end + end + end + end + + -- Handle pop-to-top behavior. This must happen after children have had a chance to handle + -- the action, so that the inner stack always pops first. + if action.type == StackActions.PopToTop then + -- Refuse to handle pop to top if a key is given that does not correspond to this router + if action.key and state.key ~= action.key then + return state + end + + -- If we're already at the top then return current state to allow action to bubble up. + if state.index <= 1 then + return state + end + + return Cryo.Dictionary.join(state, { + isTransitioning = action.immediate ~= true, + index = 1, + routes = { state.routes[1] } + }) + end + + if action.type == StackActions.Replace then + local routeIndex = nil + + -- If there is no key, set index to last route in stack + if not action.key and #state.routes > 0 then + routeIndex = #state.routes + else + for idx, route in ipairs(state.routes) do + if route.key == action.key then + routeIndex = idx + break + end + end + end + + if routeIndex then + local childRouter = childRouters[action.routeName] + local childState = {} + + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + local childAction = action.action or NavigationActions.init({ + params = getParamsForRouteAndAction(action.routeName, action) + }) + + childState = childRouter.getStateForAction(childAction) + end + + -- shallow copy and update routes + local routes = Cryo.List.join(state.routes) + routes[routeIndex] = Cryo.Dictionary.join({ + params = getParamsForRouteAndAction(action.routeName, action), + }, childState, { + routeName = action.routeName, + key = action.newKey or KeyGenerator.generateKey(), + }) + + return Cryo.Dictionary.join(state, { + routes = routes, + }) + end + end + + if action.type == NavigationActions.CompleteTransition and + (action.key == nil or action.key == state.key) and + action.toChildKey == state.routes[state.index].key and + state.isTransitioning then + return Cryo.Dictionary.join(state, { + isTransitioning = false, + }) + end + + if action.type == NavigationActions.SetParams then + local key = action.key + + local lastRouteIndex = nil + local lastRoute = nil + for idx, route in ipairs(state.routes) do + if route.key == key then + lastRouteIndex = idx + lastRoute = route + break + end + end + + if lastRoute then + local params = Cryo.Dictionary.join(lastRoute.params or {}, action.params or {}) + -- shallow copy and update routes + local routes = Cryo.List.join(state.routes) + routes[lastRouteIndex] = Cryo.Dictionary.join(lastRoute, { + params = params, + }) + + return Cryo.Dictionary.join(state, { + routes = routes, + }) + end + end + + if action.type == StackActions.Reset then + -- Only handle reset actions with matching key (or none) + if action.key ~= nil and action.key ~= state.key then + return state + end + + local specifiedActions = action.actions or {} + + local newRoutes = {} + for _, newStackAction in ipairs(specifiedActions) do + local router = childRouters[newStackAction.routeName] + + local childState = {} + if router ~= nil and router ~= CHILD_IS_SCREEN then + local childAction = newStackAction.action or NavigationActions.init({ + params = getParamsForRouteAndAction(newStackAction.routeName, newStackAction), + }) + + childState = router.getStateForAction(childAction) + end + + table.insert(newRoutes, Cryo.Dictionary.join({ + params = getParamsForRouteAndAction(newStackAction.routeName, newStackAction) + }, childState, { + routeName = newStackAction.routeName, + key = newStackAction.key or KeyGenerator.generateKey(), + })) + end + + return Cryo.Dictionary.join(state, { + routes = newRoutes, + index = action.index or #specifiedActions, + }) + end + + if action.type == NavigationActions.Back or + action.type == StackActions.Pop then + local key = action.key + local n = action.n + local immediate = action.immediate + + local backRouteIndex = state.index -- index to go back *FROM* + if action.type == StackActions.Pop and n ~= nil then + backRouteIndex = math.max(1, state.index - n + 1) + elseif key and key ~= NoneSymbol then + -- If key is specified and is not ours, we should NOT try to navigate back + -- because it might be intended for our parent! (So clear the backRouteIndex.) + backRouteIndex = 0 + for idx, route in ipairs(state.routes) do + if route.key == key then + backRouteIndex = idx + break + end + end + end + + if backRouteIndex > 1 then + return Cryo.Dictionary.join(state, { + routes = Cryo.List.removeRange(state.routes, backRouteIndex, #state.routes), + index = backRouteIndex - 1, + isTransitioning = immediate ~= true, + }) + end + end + + -- At this point, we've handled the behavior of the active route and any + -- stack actions. Now we allow non-active child routers to try to process the action, + -- and switch to them if they can handle it. + local keyIndex = action.key and StateUtils.indexOf(state, action.key) or nil + + -- Traverse from top of stack to bottom. + for i = #state.routes, 1, -1 do + local childRoute = state.routes[i] + -- Skip over the active route since we already let it try. + -- Also, skip calling getStateForAction on other child routers + -- if the provided key is in the route's state + if (childRoute.key ~= activeChildRoute.key) and + (not keyIndex or childRoute.key == action.key) then + local childRouter = childRouters[childRoute.routeName] + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + local route = childRouter.getStateForAction(action, childRoute) + if not route then + return state + end + + if route ~= childRoute then + return StateUtils.replaceAt( + state, + childRoute.key, + route, + -- don't change index for these action types + action.type == NavigationActions.SetParams or + action.type == StackActions.CompleteTransition + ) + end + end + end + end + + return state + end + + -- TODO: Implement StackRouter.getPathAndParamsForState after we add path expression support + -- TODO: Implement StackRouter.getActionForPathAndParams after we add path expression support + + return StackRouter +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/SwitchRouter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/SwitchRouter.lua new file mode 100644 index 0000000..c7675fd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/SwitchRouter.lua @@ -0,0 +1,331 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local NavigationActions = require(script.Parent.Parent.NavigationActions) +local BackBehavior = require(script.Parent.Parent.BackBehavior) +local getScreenForRouteName = require(script.Parent.getScreenForRouteName) +local createConfigGetter = require(script.Parent.createConfigGetter) +local validateRouteConfigMap = require(script.Parent.validateRouteConfigMap) +local validate = require(script.Parent.Parent.utils.validate) + +local defaultActionCreators = function() return {} end + +-- Until Cryo has a List function to do this, provide shallow copy+replace index +local function immutableReplaceListIndex(list, index, value) + local result = {} + for i, ival in ipairs(list) do + result[i] = ival + end + + result[index] = value + return result +end + +local function childrenUpdateWithoutSwitchingIndex(actionType) + return actionType == NavigationActions.SetParams or + actionType == NavigationActions.CompleteTransition +end + +local function collectChildRouters(routeConfigs) + local childRouters = {} + for routeName, _ in pairs(routeConfigs) do + local screen = getScreenForRouteName(routeConfigs, routeName) + if type(screen) == "table" and screen.router then + childRouters[routeName] = screen.router + end + end + + return childRouters +end + +local function getParamsForRoute(routeConfigs, routeName, initialParams) + local routeConfig = routeConfigs[routeName] + if type(routeConfig) == "table" and routeConfig.params then + return Cryo.Dictionary.join(routeConfig.params, initialParams) + else + return initialParams + end +end + + +return function(config) + validate(type(config) == "table", "config must be a table") + + local routeConfigs = validateRouteConfigMap(config.routes) + + -- Order is how we map the active index into the list of possible routes. + -- Lua does not guarantee any sense of order of table keys in dictionaries, so + -- we have to require the initialRouteName parameter instead defaulting to the + -- first route in the map. + local order = config.order or Cryo.Dictionary.keys(routeConfigs) + + local getCustomActionCreators = config.getCustomActionCreators or defaultActionCreators + local initialRouteParams = config.initialRouteParams or {} + + local initialRouteName = validate(config.initialRouteName, + "initialRouteName must be provided") + + local backBehavior = config.backBehavior or BackBehavior.None + local backShouldNavigateToInitialRoute = backBehavior == BackBehavior.InitialRoute + + local resetOnBlur = true + if type(config.resetOnBlur) == "boolean" then + resetOnBlur = config.resetOnBlur + end + + local initialRouteIndex = Cryo.List.find(order, initialRouteName) + if initialRouteIndex == nil then + local availableRouteStr = "" + for _, name in ipairs(order) do + availableRouteStr = availableRouteStr .. name .. "," + end + + error(string.format("Invalid initialRouteName '%s'. Must be one of [%s]", initialRouteName, availableRouteStr), 2) + end + + local childRouters = collectChildRouters(routeConfigs) + + local function resetChildRoute(routeName) + -- TODO: Do we want to merge initialRouteParams on TOP of route-specific params? + -- There is a comment in RoactNavigation that this is incorrect behavior, but they + -- do it to be consistent with their StackRouter. Do we even need this feature? + local initialParams = routeName == initialRouteName and initialRouteParams or {} + local params = getParamsForRoute(routeConfigs, routeName, initialParams) + local childRouter = childRouters[routeName] + if childRouter then + local childAction = NavigationActions.init() + return Cryo.Dictionary.join(childRouter.getStateForAction(childAction), { + key = routeName, + routeName = routeName, + params = params, + }) + else + return { + key = routeName, + routeName = routeName, + params = params, + } + end + end + + local function getNextState(prevState, possibleNextState) + if not prevState then + return possibleNextState + end + + if prevState.index ~= possibleNextState.index and resetOnBlur then + local prevRouteName = prevState.routes[prevState.index].routeName + local nextRoutes = immutableReplaceListIndex( + possibleNextState.routes, + prevState.index, + resetChildRoute(prevRouteName)) + + return Cryo.Dictionary.join(possibleNextState, { + routes = nextRoutes, + }) + else + return possibleNextState + end + end + + local function getInitialState() + return { + routes = Cryo.List.map(order, resetChildRoute), + index = initialRouteIndex, + isTransitioning = false, + } + end + + local SwitchRouter = { + childRouters = childRouters, + getScreenOptions = createConfigGetter(routeConfigs, config.defaultNavigationOptions) + } + + function SwitchRouter.getActionCreators(route, stateKey) + return getCustomActionCreators(route, stateKey) + end + + function SwitchRouter.getStateForAction(action, inputState) + local prevState = inputState and Cryo.Dictionary.join(inputState) or nil + local state = inputState or getInitialState() + local activeChildIndex = state.index + + if action.type == NavigationActions.Init then + -- TODO: React-Navigation has a comment that wonders why we merge params into child routes. + -- Need to understand if we really want to do this. + local params = action.params + if params then + state.routes = Cryo.List.map(state.routes, function(route) + local initialParams = route.routeName == initialRouteName and initialRouteParams or {} + return Cryo.Dictionary.join(route, { + params = Cryo.Dictionary.join(route.params, params, initialParams) + }) + end) + end + end + + -- Let the active child try to handle the action first. + local activeChildLastState = state.routes[state.index] + local activeChildRouter = childRouters[order[state.index]] + if activeChildRouter then + local activeChildState = activeChildRouter.getStateForAction(action, activeChildLastState) + if not activeChildState and inputState then + -- Child ran into error with known inputState. Propagate to caller. + return nil + end + + if activeChildState and activeChildState ~= activeChildLastState then + local routes = immutableReplaceListIndex(state.routes, state.index, activeChildState) + return getNextState(prevState, Cryo.Dictionary.join(state, { + routes = routes + })) + end + end + + -- Child did not handle it, so try to process the action ourselves. + local isBackEligible = not action.key or action.key == activeChildLastState.key + if action.type == NavigationActions.Back then + if isBackEligible and backShouldNavigateToInitialRoute then + activeChildIndex = initialRouteIndex + else + return state + end + end + + local didNavigate = false + if action.type == NavigationActions.Navigate then + for index, childId in ipairs(order) do + if childId == action.routeName then + activeChildIndex = index + didNavigate = true + break + end + end + + if didNavigate then + local childState = state.routes[activeChildIndex] + local childRouter = childRouters[action.routeName] + local newChildState = childState + + if action.action and childRouter then + local childStateUpdate = childRouter.getStateForAction(action.action, childState) + if childStateUpdate then + newChildState = childStateUpdate + end + end + + if action.params then + newChildState = Cryo.Dictionary.join(newChildState, { + params = Cryo.Dictionary.join(newChildState.params or {}, action.params) + }) + end + + if newChildState ~= childState then + local routes = immutableReplaceListIndex(state.routes, activeChildIndex, newChildState) + local nextState = Cryo.Dictionary.join(state, { + routes = routes, + index = activeChildIndex, + }) + + return getNextState(prevState, nextState) + elseif newChildState == childState and state.index == activeChildIndex and prevState then + return nil + end + end + end + + if action.type == NavigationActions.SetParams then + local key = action.key + local lastIndex, lastRoute + for index, route in ipairs(state.routes) do + if route.key == key then + lastIndex = index + lastRoute = route + break + end + end + + if lastRoute then + local params = Cryo.Dictionary.join(lastRoute.params or {}, action.params) + local mergedRoute = Cryo.Dictionary.join(lastRoute, { + params = params + }) + + local routes = immutableReplaceListIndex(state.routes, lastIndex, mergedRoute) + return getNextState(prevState, Cryo.Dictionary.join(state, { + routes = routes + })) + end + end + + if activeChildIndex ~= state.index then + return getNextState(prevState, Cryo.Dictionary.join(state, { index = activeChildIndex })) + elseif didNavigate and not inputState then + return state + elseif didNavigate then + return Cryo.Dictionary.join(state) + end + + -- Let other children handle it and switch to first child that returns a new state + local index = state.index + local routes = state.routes + + for i, childId in ipairs(order) do + if i ~= index then + local childRouter = childRouters[childId] + local childState = routes[i] + if childRouter then + childState = childRouter.getStateForAction(action, childState) + end + + if not childState then + index = i + break + end + + if childState ~= routes[i] then + routes = immutableReplaceListIndex(routes, i, childState) + index = i + break + end + end + end + + -- Nested routers can be updated after switching children with actions such as SetParams + -- and CompleteTransition + if childrenUpdateWithoutSwitchingIndex(action.type) then + index = state.index + end + + if index ~= state.index or routes ~= state.routes then + return getNextState(prevState, Cryo.Dictionary.join(state, { + index = index, + routes = routes, + })) + end + + return state + end + + function SwitchRouter.getComponentForState(state) + local activeRoute = state.routes[state.index] or {} + local routeName = activeRoute.routeName + validate(routeName, "There is no route defined for index '%d'. " .. + "Make sure that you passed in a navigation state with a " .. + "valid tab/screen index.", state.index) + + local childRouter = childRouters[routeName] + if childRouter then + return childRouter.getComponentForState(state.routes[state.index]) + else + return getScreenForRouteName(routeConfigs, routeName) + end + end + + function SwitchRouter.getComponentForRouteName(routeName) + return getScreenForRouteName(routeConfigs, routeName) + end + + -- TODO: Implement SwitchRouter.getPathAndParamsForState after we add path expression support + -- TODO: Implement SwitchRouter.getActionForPathAndParams after we add path expression support + + return SwitchRouter +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/TabRouter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/TabRouter.lua new file mode 100644 index 0000000..052d391 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/TabRouter.lua @@ -0,0 +1,13 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local SwitchRouter = require(script.Parent.SwitchRouter) +local BackBehavior = require(script.Parent.Parent.BackBehavior) + +return function(config) + -- Provide defaults suitable for tab routing. + local modifiedConfig = Cryo.Dictionary.join({ + resetOnBlur = false, + backBehavior = BackBehavior.InitialRoute, + }, config) + + return SwitchRouter(modifiedConfig) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/StackRouter.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/StackRouter.spec.lua new file mode 100644 index 0000000..2f6640d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/StackRouter.spec.lua @@ -0,0 +1,1191 @@ +return function() + local StackRouter = require(script.Parent.Parent.StackRouter) + local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) + local StackActions = require(script.Parent.Parent.Parent.StackActions) + + -- local TableUtilities = require(script.Parent.Parent.Parent.utils.TableUtilities) + + local function expectError(functor, msg) + local status, err = pcall(functor) + + if status ~= false then + error("expectError: Test function should have thrown error, but it passed", 2) + end + if string.find(err, msg) == nil then + error(string.format("expectError: Expected error message '%s' not found in actual message: '%s'", msg, err), 2) + end + end + + it("should be a function", function() + expect(type(StackRouter)).to.equal("function") + end) + + it("should throw when passed a non-table", function() + expectError(function() + StackRouter(5) + end, "config must be a table") + end) + + it("should throw for invalid routes config", function() + expect(function() + StackRouter({ routes = 5 }) + end).to.throw() + end) + + it("should throw if initialRouteName is not provided", function() + expectError(function() + StackRouter({ routes = { + Foo = function() end, + }}) + end, "initialRouteName must be provided") + end) + + it("should throw if initialRouteName is not found in routes table", function() + expectError(function() + StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "MyRoute", + }) + end, "Invalid initialRouteName 'MyRoute'. Must be one of %[Bar,Foo,%]") + end) + + it("should expose childRouters as a member", function() + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + router = "A", + }, + }, + Bar = { + screen = { + render = function() end, + router = "B", + }, + }, + }, + initialRouteName = "Foo", + }) + + expect(router.childRouters.Foo).to.equal("A") + expect(router.childRouters.Bar).to.equal("B") + end) + + it("should not expose childRouters list members if they are CHILD_IS_SCREEN", function() + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + router = "A", + }, + }, + Bar = { + screen = { + render = function() end, + }, + }, + }, + initialRouteName = "Foo", + }) + + expect(router.childRouters.Foo).to.equal("A") + + expect(router._CHILD_IS_SCREEN).to.never.equal(nil) + for _, childRouter in pairs(router.childRouters) do + expect(childRouter).to.never.equal(router._CHILD_IS_SCREEN) + end + end) + + describe("getScreenOptions tests", function() + it("should correctly configure default screen options", function() + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + } + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + } + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("FooTitle") + end) + + it("should correctly configure route-specified screen options", function() + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + }, + navigationOptions = { title = "RouteFooTitle" }, + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("RouteFooTitle") + end) + + it("should correctly configure component-specified screen options", function() + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentFooTitle" }, + }, + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("ComponentFooTitle") + end) + end) + + describe("getActionCreators tests", function() + it("should return basic action creators table if none are provided", function() + local router = StackRouter({ + routes = { + Foo = { render = function() end }, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + + local fieldCount = 0 + for _ in pairs(actionCreators) do + fieldCount = fieldCount + 1 + end + + expect(fieldCount).to.equal(6) + expect(type(actionCreators.pop)).to.equal("function") + expect(type(actionCreators.popToTop)).to.equal("function") + expect(type(actionCreators.push)).to.equal("function") + expect(type(actionCreators.replace)).to.equal("function") + expect(type(actionCreators.reset)).to.equal("function") + expect(type(actionCreators.dismiss)).to.equal("function") + end) + + it("should call custom action creators function if provided", function() + local router = StackRouter({ + routes = { + Foo = { render = function() end }, + }, + initialRouteName = "Foo", + getCustomActionCreators = function() + return { a = 1, popToTop = 2 } + end, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.a).to.equal(1) + + -- make sure that we merged the default ones on top! + expect(type(actionCreators.pop)).to.equal("function") + expect(type(actionCreators.popToTop)).to.equal("function") + end) + + it("should build a pop action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.pop(1).type).to.equal(StackActions.Pop) + end) + + it("should build a pop to top action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.popToTop().type).to.equal(StackActions.PopToTop) + end) + + it("should build a push action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.push("Foo").type).to.equal(StackActions.Push) + end) + + it("should build a replace action with a string replaceWith arg", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo", key = "Foo" }, "key") + expect(actionCreators.replace("Foo").type).to.equal(StackActions.Replace) + end) + + it("should build a replace action with a table replaceWith arg", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.replace({ routeName = "Foo" }).type).to.equal(StackActions.Replace) + end) + + it("should build a reset action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.reset({ + actions = { NavigationActions.navigate({ routeName = "Foo" }) }, + }).type).to.equal(StackActions.Reset) + end) + + it("should build a dismiss action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.dismiss().type).to.equal(NavigationActions.Back) + end) + end) + + describe("getComponentForState tests", function() + it("should return component matching requested state", function() + local testComponent = function() end + local router = StackRouter({ + routes = { + Foo = { screen = testComponent }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForState({ + routes = { + { routeName = "Foo" }, + }, + index = 1, + }) + expect(component).to.equal(testComponent) + end) + + it("should throw if there is no route matching active index", function() + local router = StackRouter({ + routes = { + Foo = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + expectError(function() + router.getComponentForState({ + routes = { + Foo = { screen = function() end }, + }, + index = 2, + }) + end, "There is no route defined for index '2'. " .. + "Make sure that you passed in a navigation state with a " .. + "valid stack index.") + + end) + + it("should descend child router for requested route", function() + local testComponent = function() end + local childRouter = StackRouter({ + routes = { + Bar = { screen = testComponent } + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + router = childRouter, + } + }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForState({ + routes = { + { + routeName = "Foo", + routes = { -- Child router's routes + { routeName = "Bar" }, + }, + index = 1 + }, + }, + index = 1, + }) + expect(component).to.equal(testComponent) + end) + end) + + describe("getComponentForRouteName tests", function() + it("should return a component that matches the given route name", function() + local testComponent = function() end + local router = StackRouter({ + routes = { + Foo = testComponent, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForRouteName("Foo") + expect(component).to.equal(testComponent) + end) + + it("should return a component that matches the given route name from accessed childRouter", function() + local testComponent = function() end + local childRouter = StackRouter({ + routes = { + Bar = testComponent, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + }, + initialRouteName = "Foo", + }) + + local component = router.childRouters.Foo.getComponentForRouteName("Bar") + expect(component).to.equal(testComponent) + end) + end) + + describe("getStateForAction tests", function() + it("should return initial state for init action", function() + local router = StackRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(#state.routes).to.equal(1) + expect(state.routes[state.index].routeName).to.equal("Foo") + expect(state.isTransitioning).to.equal(false) + end) + + it("should adjust initial state index to match initialRouteName's index", function() + local router = StackRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(state.routes[state.index].routeName).to.equal("Foo") + + local router2 = StackRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local state2 = router2.getStateForAction(NavigationActions.init(), nil) + expect(state2.routes[state2.index].routeName).to.equal("Bar") + end) + + it("should incorporate child router state", function() + local childRouter = StackRouter({ + routes = { + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + City = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + local activeState = state.routes[state.index] + expect(activeState.routeName).to.equal("Foo") -- parent's tracking uses parent's route name + expect(activeState.routes[activeState.index].routeName).to.equal("Bar") + end) + + it("should let active child handle non-init action first", function() + local childRouter = StackRouter({ + routes = { + Bar = { screen = function() end }, + City = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "City" })) + + expect(state.routes[1].routes[2].routeName).to.equal("City") + expect(state.index).to.equal(1) + expect(state.routes[1].index).to.equal(2) + end) + + it("should make historical inactive child router active if it handles action", function() + local childRouter = StackRouter({ + routes = { + City = function() end, + State = function() end, + }, + initialRouteName = "City", + }) + + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = { + render = function() end, + router = childRouter, + } + }, + initialRouteName = "Foo", + }) + + local initialState = { + routes = { + [1] = { routeName = "Foo", key = "Foo1" }, + [2] = { + routeName = "Bar", + key = "Bar", + routes = { + [1] = { routeName = "City", key = "City", }, + }, + index = 1 + }, + [3] = { routeName = "Foo", key = "Foo2" }, + }, + index = 3, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "State" }), initialState) + expect(resultState.routes[2].index).to.equal(2) + expect(resultState.routes[2].routes[2].routeName).to.equal("State") + expect(#resultState.routes[2].routes).to.equal(2) + expect(resultState.index).to.equal(2) + end) + + it("should go back to previous stack entry on back action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar", + } + }, + index = 2, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Foo") + expect(#resultState.routes).to.equal(1) -- it should delete top entry! + end) + + it("should not go back if at root of stack", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(resultState).to.equal(initialState) + end) + + it("should go back out of child stack if on root of child", function() + local childRouter = StackRouter({ + routes = { + Bar = { screen = function() end }, + City = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + Cat = function() end, + }, + initialRouteName = "Cat", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Cat", + key = "Cat", + }, + [2] = { + routeName = "Foo", + key = "Foo", + routes = { + [1] = { + routeName = "Bar", + key = "Bar", + } + }, + index = 1, + } + }, + index = 2, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(resultState.index).to.equal(1) + expect(#resultState.routes).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Cat") + end) + + it("should go back within active child if not on root of child", function() + local childRouter = StackRouter({ + routes = { + Bar = { screen = function() end }, + City = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + Cat = function() end, + }, + initialRouteName = "Cat", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Cat", + key = "Cat", + }, + [2] = { + routeName = "Foo", + key = "Foo", + routes = { + [1] = { + routeName = "Bar", + key = "Bar", + }, + [2] = { + routeName = "City", + key = "City", + }, + }, + index = 2, + } + }, + index = 2, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[1].routeName).to.equal("Cat") + expect(resultState.routes[2].routeName).to.equal("Foo") + + expect(#resultState.routes[2].routes).to.equal(1) + expect(resultState.routes[2].index).to.equal(1) + expect(resultState.routes[2].routes[1].routeName).to.equal("Bar") + end) + + it("should pop to top", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar1", + }, + [3] = { + routeName = "Bar", + key = "Bar2", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(StackActions.popToTop(), initialState) + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Foo") + end) + + it("should pop to top through child router", function() + local childRouter = StackRouter({ + routes = { + Bar = function() end, + City = function() end, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + screen = function() end, + router = childRouter, + }, + Crazy = function() end, + }, + initialRouteName = "Crazy", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Crazy", + key = "Crazy", + }, + [2] = { + routeName = "Foo", + key = "Foo", + routes = { + [1] = { + routeName = "Bar", + key = "Bar", + }, + [2] = { + routeName = "City", + key = "City", + }, + }, + index = 2, + } + }, + index = 2, + } + + local resultState = router.getStateForAction(StackActions.popToTop(), initialState) + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Crazy") + end) + + it("should push a new entry on navigate without instance of that screen", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + } + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + end) + + it("should jump to existing entry in stack if one exists already, on navigate", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + City = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar", + }, + [3] = { + routeName = "City", + key = "City", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + params = { a = 1 }, + }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + expect(resultState.routes[2].params.a).to.equal(1) + end) + + it("should always push new entry on push action even with pre-existing instance of that screen", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + City = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar", + }, + [3] = { + routeName = "City", + key = "City", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(StackActions.push({ routeName = "Foo" }), initialState) + expect(#resultState.routes).to.equal(4) + expect(resultState.index).to.equal(4) + expect(resultState.routes[4].routeName).to.equal("Foo") + end) + + it("should navigate to inactive child if route not present elsewhere", function() + local childRouter = StackRouter({ + routes = { + Bar = { screen = function() end }, + City = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + Cat = function() end, + }, + initialRouteName = "Cat", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Cat", + key = "Cat", + }, + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "City" }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Foo") + + expect(#resultState.routes[2].routes).to.equal(2) + expect(resultState.routes[2].index).to.equal(2) + expect(resultState.routes[2].routes[2].routeName).to.equal("City") + end) + + it("should set params on route for setParams action", function() + local router = StackRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + initialRouteName = "Foo", + initialRouteKey = "FooKey", + }) + + local newState = router.getStateForAction(NavigationActions.setParams({ + key = "FooKey", + params = { a = 1 }, + })) + + expect(newState.routes[newState.index].params.a).to.equal(1) + end) + + it("should combine params from action and route config", function() + local router = StackRouter({ + routes = { + Foo = { render = function() end }, + Bar = { + screen = function() end, + params = { a = 1 }, + }, + }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + params = { b = 2 }, + })) + + expect(newState.routes[2].params.a).to.equal(1) + expect(newState.routes[2].params.b).to.equal(2) + end) + + it("should init and then replace initial route if prior state is not provided", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(StackActions.replace({ + routeName = "Bar", + })) + + expect(#newState.routes).to.equal(1) + expect(newState.index).to.equal(1) + expect(newState.routes[1].routeName).to.equal("Bar") + end) + + it("should replace top route if no key is provided", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + } + }, + index = 1, + } + + local newState = router.getStateForAction(StackActions.replace({ + routeName = "Bar", + }), initialState) + + expect(#newState.routes).to.equal(1) + expect(newState.index).to.equal(1) + expect(newState.routes[1].routeName).to.equal("Bar") + end) + + it("should replace keyed route if provided", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar", + } + }, + index = 2, + } + + local newState = router.getStateForAction(StackActions.replace({ + routeName = "Foo", + key = "Bar", + newKey = "NewFoo", + }), initialState) + + expect(#newState.routes).to.equal(2) + expect(newState.index).to.equal(2) + expect(newState.routes[2].routeName).to.equal("Foo") + expect(newState.routes[2].key).to.equal("NewFoo") + end) + + it("should reset top-level routes if not given a key", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo1", + }, + [2] = { + routeName = "Foo", + key = "Foo2", + }, + }, + index = 2, + } + + local resultState = router.getStateForAction(StackActions.reset({ + actions = { + NavigationActions.navigate({ routeName = "Bar" }) + } + }), initialState) + + -- "actions" array replaces entire state, bypassing initial route config! + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Bar") + end) + + it("should reset keyed route if provided", function() + local childRouter = StackRouter({ + routes = { + City = function() end, + State = function() end, + }, + initialRouteName = "City", + }) + + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = { + screen = function() end, + router = childRouter, + }, + }, + initialRouteName = "Bar", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo1", + }, + [2] = { + routeName = "Bar", + key = "Bar", + routes = { + [1] = { + routeName = "City", + key = "City", + } + }, + index = 1, + }, + }, + index = 2, + } + + local resultState = router.getStateForAction(StackActions.reset({ + actions = { + NavigationActions.navigate({ routeName = "State" }) + }, + key = "Bar", + }), initialState) + + -- "actions" array replaces entire state, bypassing initial route config! + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + expect(resultState.routes[2].routes[1].routeName).to.equal("City") + end) + + it("should mark state as transitioning, then clear it on CompleteTransition action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + } + }, + index = 1, + } + + local transitioningState = router.getStateForAction(StackActions.push({ routeName = "Bar" }), initialState) + expect(transitioningState.isTransitioning).to.equal(true) + + local completedState = router.getStateForAction(NavigationActions.completeTransition({ + toChildKey = transitioningState.routes[2].key, -- Need actual key to identify target + }), transitioningState) + + expect(completedState.isTransitioning).to.equal(false) + end) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/SwitchRouter.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/SwitchRouter.spec.lua new file mode 100644 index 0000000..2548212 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/SwitchRouter.spec.lua @@ -0,0 +1,691 @@ +return function() + local SwitchRouter = require(script.Parent.Parent.SwitchRouter) + local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) + local BackBehavior = require(script.Parent.Parent.Parent.BackBehavior) + + local function expectError(functor, msg) + local status, err = pcall(functor) + + if status ~= false then + error("expectError: Test function should have thrown error, but it passed", 2) + end + if string.find(err, msg) == nil then + error(string.format("expectError: Expected error message '%s' not found in actual message: '%s'", msg, err), 2) + end + end + + it("should be a function", function() + expect(type(SwitchRouter)).to.equal("function") + end) + + it("should throw when passed a non-table", function() + expectError(function() + SwitchRouter(5) + end, "config must be a table") + end) + + it("should throw for invalid routes config", function() + expect(function() + SwitchRouter({ routes = 5 }) + end).to.throw() -- throw is from validateRouteConfigs, so do not depend on message + end) + + it("should throw if initialRouteName is not provided", function() + expectError(function() + SwitchRouter({ routes = { + Foo = function() end, + } }) + end, "initialRouteName must be provided") + end) + + it("should throw if initialRouteName is not found in routes table", function() + expectError(function() + SwitchRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "MyRoute", + }) + end, "Invalid initialRouteName 'MyRoute'. Must be one of %[Bar,Foo,%]") + end) + + it("should expose childRouters as a member", function() + local router = SwitchRouter({ + routes = { + Foo = { + screen = { + render = function() end, + router = "A", + }, + }, + Bar = { + screen = { + render = function() end, + router = "B", + }, + }, + }, + initialRouteName = "Foo", + }) + + expect(router.childRouters.Foo).to.equal("A") + expect(router.childRouters.Bar).to.equal("B") + end) + + describe("getScreenOptions tests", function() + it("should correctly configure default screen options", function() + local router = SwitchRouter({ + routes = { + Foo = { + screen = { + render = function() end, + } + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("FooTitle") + end) + + it("should correctly configure route-specified screen options", function() + local router = SwitchRouter({ + routes = { + Foo = { + screen = { + render = function() end, + }, + navigationOptions = { title = "RouteFooTitle" }, + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("RouteFooTitle") + end) + + it("should correctly configure component-specified screen options", function() + local router = SwitchRouter({ + routes = { + Foo = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentFooTitle" }, + }, + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("ComponentFooTitle") + end) + end) + + describe("getActionCreators tests", function() + it("should return empty action creators table if none are provided", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + + local fieldCount = 0 + for _ in pairs(actionCreators) do + fieldCount = fieldCount + 1 + end + + expect(fieldCount).to.equal(0) + end) + + it("should call custom action creators function if provided", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + }, + initialRouteName = "Foo", + getCustomActionCreators = function() + return { a = 1 } + end, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.a).to.equal(1) + end) + end) + + describe("getComponentForState tests", function() + it("should return component matching requested state", function() + local testComponent = function() end + local router = SwitchRouter({ + routes = { + Foo = { screen = testComponent }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForState({ + routes = { + { routeName = "Foo" }, + }, + index = 1, + }) + expect(component).to.equal(testComponent) + end) + + it("should throw if there is no route matching active index", function() + local router = SwitchRouter({ + routes = { + Foo = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + expectError(function() + router.getComponentForState({ + routes = { + Foo = { screen = function() end }, + }, + index = 2, + }) + end, "There is no route defined for index '2'. " .. + "Make sure that you passed in a navigation state with a " .. + "valid tab/screen index.") + + end) + + it("should descend child router for requested route", function() + local testComponent = function() end + local childRouter = SwitchRouter({ + routes = { + Bar = { screen = testComponent } + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + screen = { + render = function() end, + router = childRouter, + } + }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForState({ + routes = { + { + routeName = "Foo", + routes = { -- Child router's routes + { routeName = "Bar" }, + }, + index = 1 + }, + }, + index = 1, + }) + expect(component).to.equal(testComponent) + end) + end) + + describe("getComponentForRouteName tests", function() + it("should return a component that matches the given route name", function() + local testComponent = function() end + local router = SwitchRouter({ + routes = { + Foo = { screen = testComponent }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForRouteName("Foo") + expect(component).to.equal(testComponent) + end) + end) + + describe("getStateForAction tests", function() + it("should return initial state for init action", function() + local router = SwitchRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(#state.routes).to.equal(2) + expect(state.routes[state.index].routeName).to.equal("Foo") + expect(state.isTransitioning).to.equal(false) + end) + + it("should adjust initial state index to match initialRouteName's index", function() + local router = SwitchRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(state.routes[state.index].routeName).to.equal("Foo") + + local router2 = SwitchRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local state2 = router2.getStateForAction(NavigationActions.init(), nil) + expect(state2.routes[state2.index].routeName).to.equal("Bar") + end) + + it("should respect optional order property", function() + local router = SwitchRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(state.routes[1].routeName).to.equal("Foo") + expect(state.routes[2].routeName).to.equal("Bar") + end) + + it("should incorporate child router state", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + City = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + local activeState = state.routes[state.index] + expect(activeState.routeName).to.equal("Foo") -- parent's tracking uses parent's route name + expect(activeState.routes[activeState.index].routeName).to.equal("Bar") + end) + + it("should let active child handle non-init action first", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { screen = function() end }, + City = { screen = function() end }, + }, + order = { "Bar", "City" }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + State = { render = function() end }, + }, + order = { "Foo", "State" }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "City" })) + expect(state.routes[1].index).to.equal(2) + expect(state.index).to.equal(1) + end) + + it("should go back to initial route index if BackBehavior.InitialRoute", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + backBehavior = BackBehavior.InitialRoute, + initialRouteName = "Foo", + }) + + local prevState = { + routes = { + { routeName = "Foo" }, + { routeName = "Bar" }, + }, + index = 2, + } + + local newState = router.getStateForAction(NavigationActions.back(), prevState) + expect(newState.index).to.equal(1) + end) + + it("should not change state on back action if BackBehavior.None", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local prevState = { + routes = { + { routeName = "Foo" }, + { routeName = "Bar" }, + }, + index = 2, + } + + local newState = router.getStateForAction(NavigationActions.back(), prevState) + expect(newState).to.equal(prevState) + end) + + it("should change active route on navigate", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" })) + expect(newState.index).to.equal(2) + expect(newState.routes[newState.index].routeName).to.equal("Bar") + end) + + it("should pass sub-action to child router on navigate", function() + local childRouter = SwitchRouter({ + routes = { + City = { screen = function() end }, + State = { screen = function() end }, + }, + initialRouteName = "City", + }) + + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { + render = function() end, + router = childRouter, + }, + }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + action = NavigationActions.navigate({ routeName = "State" }), + })) + + local activeRoute = newState.routes[newState.index] + expect(activeRoute.routeName).to.equal("Bar") + expect(activeRoute.routes[activeRoute.index].routeName).to.equal("State") + end) + + it("should return initial state if navigating to active child without previous state", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + City = { render = function() end }, + }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Foo", + })) + + expect(newState.routes[newState.index].routeName).to.equal("Foo") + expect(newState.isTransitioning).to.equal(false) + end) + + it("should reset state for deactivated route by default", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local initialState = { + routes = { + { routeName = "Foo", params = { a = 1 } }, + { routeName = "Bar" }, + }, + index = 1, + } + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(state.routes[1].params.a).to.equal(nil) -- should be empty + end) + + it("should not reset state for deactivated route if resetOnBlur is false", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + resetOnBlur = false, + }) + + local testParams = { a = 1 } + + local initialState = { + routes = { + { routeName = "Foo", params = testParams }, + { routeName = "Bar" }, + }, + index = 1, + } + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(state.routes[1].params).to.equal(testParams) + end) + + it("should set params on route for setParams action", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(NavigationActions.setParams({ + key = "Foo", -- By default, key == routeName + params = { a = 1 }, + })) + + expect(newState.routes[newState.index].params.a).to.equal(1) + end) + + it("should preserve route configured params for child router", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { + screen = function() end, + params = { a = 2 }, + }, + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + params = { a = 1 }, + router = childRouter, + }, + City = { render = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init()) + expect(state.routes[state.index].params.a).to.equal(1) + end) + + it("should merge initialRouteParams with initial route's own params", function() + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + params = { a = 1 }, + }, + Bar = { + render = function() end, + params = { a = 1 }, + }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + initialRouteParams = { a = 2, b = 3 }, + }) + + local state = router.getStateForAction(NavigationActions.init()) + expect(state.routes[1].params.a).to.equal(2) + expect(state.routes[1].params.b).to.equal(3) + expect(state.routes[2].params.a).to.equal(1) + expect(state.routes[2].params.b).to.equal(nil) + end) + + it("should merge init action params with initial route's own params and initialRouteParams", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end, params = { a = 1 } } + }, + initialRouteName = "Foo", + initialRouteParams = { c = 3 }, + }) + + local state = router.getStateForAction(NavigationActions.init({ params = { b = 2 } })) + expect(state.routes[1].params.a).to.equal(1) + expect(state.routes[1].params.b).to.equal(2) + expect(state.routes[1].params.c).to.equal(3) + end) + + it("should merge navigate action params for child router", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { + screen = function() end, + params = { a = 2 }, + }, + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + params = { b = 3 }, + })) + + expect(state.routes[1].routes[1].params.a).to.equal(2) + expect(state.routes[1].routes[1].params.b).to.equal(3) + end) + + it("should propagate a child router getStateForAction failure to caller", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + }, + initialRouteName = "Foo", + }) + + -- need to properly initialize state because we're being abusive of getStateForAction + local initialState = router.getStateForAction(NavigationActions.init()) + + childRouter.getStateForAction = function() return nil end + + local state = router.getStateForAction(NavigationActions.navigate("Bar"), initialState) + expect(state).to.equal(nil) + end) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/TabRouter.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/TabRouter.spec.lua new file mode 100644 index 0000000..f18a109 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/TabRouter.spec.lua @@ -0,0 +1,67 @@ +return function() + local TabRouter = require(script.Parent.Parent.TabRouter) + local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) + + -- NOTE: Most functional tests are covered by SwitchRouter.spec.lua + -- We just check that we can mount a basic case, and check that our custom + -- defaults for resetOnBlur and backBehavior work as expected. + + it("should return a component that matches the given route name", function() + local testComponent = function() end + local router = TabRouter({ + routes = { + Foo = { screen = testComponent }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForRouteName("Foo") + expect(component).to.equal(testComponent) + end) + + it("should not reset state for deactivated route", function() + local router = TabRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local testParams = { a = 1 } + + local initialState = { + routes = { + { routeName = "Foo", params = testParams }, + { routeName = "Bar" }, + }, + index = 1, + } + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(state.routes[1].params).to.equal(testParams) + end) + + it("should go back to initial route index", function() + local router = TabRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local prevState = { + routes = { + { routeName = "Foo" }, + { routeName = "Bar" }, + }, + index = 2, + } + + local newState = router.getStateForAction(NavigationActions.back(), prevState) + expect(newState.index).to.equal(1) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/createConfigGetter.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/createConfigGetter.spec.lua new file mode 100644 index 0000000..6cb2e53 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/createConfigGetter.spec.lua @@ -0,0 +1,115 @@ +return function() + local createConfigGetter = require(script.Parent.Parent.createConfigGetter) + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + + it("should return a function", function() + local result = createConfigGetter({}, {}) + expect(type(result)).to.equal("function") + end) + + it("should return a screen config when called", function() + local HomeScreen = Roact.Component:extend("HomeScreen") + HomeScreen.navigationOptions = function(props) + local username = props.navigation.state.params and + props.navigation.state.params.user or "anonymous" + + return { + title = string.format("Welcome %s", username), + gesturesEnabled = true, + } + end + function HomeScreen:render() return nil end + + local SettingsScreen = Roact.Component:extend("SettingsScreen") + SettingsScreen.navigationOptions = { + title = "Settings!!!", + gesturesEnabled = false, + } + function SettingsScreen:render() return nil end + + local NotificationScreen = Roact.Component:extend("NotificationScreen") + NotificationScreen.navigationOptions = function(props) + local gesturesEnabled = true + if props.navigation.state.params then + gesturesEnabled = not props.navigation.state.params.fullscreen + end + + return { + title = "42", + gesturesEnabled = gesturesEnabled + } + end + + local getScreenOptions = createConfigGetter({ + Home = { screen = HomeScreen }, + Settings = { screen = SettingsScreen }, + Notifications = { + screen = NotificationScreen, + navigationOptions = { + title = "10 new notifications", + } + } + }) + + local routes = { + { key = "A", routeName = "Home", }, + { key = "B", routeName = "Home", params = { user = "jane"} }, + { key = "C", routeName = "Settings", }, + { key = "D", routeName = "Notifications", }, + { key = "E", routeName = "Notifications", params = { fullscreen = true } }, + } + + expect(getScreenOptions({ state = routes[1] }, {}).title + ).to.equal("Welcome anonymous") + + expect(getScreenOptions({ state = routes[2] }, {}).title + ).to.equal("Welcome jane") + + expect(getScreenOptions({ state = routes[1] }, {}).gesturesEnabled + ).to.equal(true) + + expect(getScreenOptions({ state = routes[3] }, {}).title + ).to.equal("Settings!!!") + + expect(getScreenOptions({ state = routes[3] }, {}).gesturesEnabled + ).to.equal(false) + + expect(getScreenOptions({ state = routes[4] }, {}).title + ).to.equal("10 new notifications") + + expect(getScreenOptions({ state = routes[4] }, {}).gesturesEnabled + ).to.equal(true) + + expect(getScreenOptions({ state = routes[5] }, {}).gesturesEnabled + ).to.equal(false) + end) + + it("should override default config with component-specific config", function() + local getScreenOptions = createConfigGetter({ + Home = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentHome" }, + }, + }, + defaultNavigationOptions = { title = "DefaultTitle" }, + }) + + expect(getScreenOptions({ state = { routeName = "Home" } }).title).to.equal("ComponentHome") + end) + + it("should override component-specific config with route-specific config", function() + local getScreenOptions = createConfigGetter({ + Home = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentHome" }, + }, + navigationOptions = { title = "RouteHome" }, + }, + defaultNavigationOptions = { title = "DefaultTitle" }, + }) + + expect(getScreenOptions({ state = { routeName = "Home" } }).title).to.equal("RouteHome") + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getChildRouter.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getChildRouter.spec.lua new file mode 100644 index 0000000..dec6a68 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getChildRouter.spec.lua @@ -0,0 +1,60 @@ +return function() + local getChildRouter = require(script.Parent.Parent.getChildRouter) + + it("should throw if router is not a table", function() + local status, err = pcall(function() + getChildRouter(5, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "router must be a table")).to.never.equal(nil) + end) + + it("should throw if routeName is not a string", function() + local status, err = pcall(function() + getChildRouter({}, 5) + end) + + expect(status).to.equal(false) + expect(string.find(err, "routeName must be a string")).to.never.equal(nil) + end) + + it("should return child router if found", function() + local childRouter = {} + local result = getChildRouter({ + childRouters = { + myRoute = childRouter, + } + }, "myRoute") + + expect(result).to.equal(childRouter) + end) + + it("should look up component router if no child router is found", function() + local component = { router = {} } + + local result = getChildRouter({ + getComponentForRouteName = function(routeName) + if routeName == "myRoute" then + return component + else + return nil + end + end + }, "myRoute") + + expect(result).to.equal(component.router) + end) + + it("should throw if no child routers are specified and getComponentForRouteName is not a function", function() + local status, err = pcall(function() + getChildRouter({ + getComponentForRouteName = 5 + }, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "router.getComponentForRouteName must be a function if no child routers are specified") + ).to.never.equal(nil) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getNavigationActionCreators.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getNavigationActionCreators.spec.lua new file mode 100644 index 0000000..bafe6b7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getNavigationActionCreators.spec.lua @@ -0,0 +1,101 @@ +return function() + local getNavigationActionCreators = require(script.Parent.Parent.getNavigationActionCreators) + local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) + + local function expectError(functor, msg) + local status, err = pcall(functor) + expect(status).to.equal(false) + expect(string.find(err, msg)).to.never.equal(nil) + end + + it("should return a table with correct functions when called", function() + local result = getNavigationActionCreators() + expect(type(result.goBack)).to.equal("function") + expect(type(result.navigate)).to.equal("function") + expect(type(result.setParams)).to.equal("function") + end) + + describe("goBack tests", function() + it("should return a Back action when called", function() + local result = getNavigationActionCreators().goBack("theKey") + expect(result.type).to.equal(NavigationActions.Back) + expect(result.key).to.equal("theKey") + end) + + it("should throw when route.key is not a string", function() + expectError(function() + getNavigationActionCreators({ key = 5 }).goBack() + end, "%.goBack%(%): key should be a string") + end) + + it("should fall back to route.key if key is not provided", function() + local result = getNavigationActionCreators({ key = "routeKey" }).goBack() + expect(result.key).to.equal("routeKey") + end) + + it("should override route.key if key is provided", function() + local result = getNavigationActionCreators({ key = "routeKey" }).goBack("theKey") + expect(result.key).to.equal("theKey") + end) + end) + + describe("navigate tests", function() + it("should return a Navigate action when called", function() + local theParams = {} + local childAction = {} + local result = getNavigationActionCreators().navigate("theRoute", theParams, childAction) + expect(result.type).to.equal(NavigationActions.Navigate) + expect(result.routeName).to.equal("theRoute") + expect(result.params).to.equal(theParams) + expect(result.action).to.equal(childAction) + end) + + it("should return a navigate action with matching properties when called with a table", function() + local testNavigateTo = { + routeName = "theRoute", + params = {}, + action = {}, + } + + local result = getNavigationActionCreators().navigate(testNavigateTo) + expect(result.type).to.equal(NavigationActions.Navigate) + expect(result.routeName).to.equal("theRoute") + expect(result.params).to.equal(testNavigateTo.params) + expect(result.action).to.equal(testNavigateTo.action) + end) + + it("should throw when navigateTo is not a valid type", function() + expectError(function() + getNavigationActionCreators().navigate(5) + end, "%.navigate%(%): navigateTo must be a string or table") + end) + + it("should throw when params is provided with a table navigateTo", function() + expectError(function() + getNavigationActionCreators().navigate({}, {}) + end, "%.navigate%(%): params can only be provided with a string navigateTo value") + end) + + it("should throw when action is provided with a table navigateTo", function() + expectError(function() + getNavigationActionCreators().navigate({}, nil, {}) + end, "%.navigate%(%): child action can only be provided with a string navigateTo value") + end) + end) + + describe("setParams tests", function() + it("should return a SetParams action when called", function() + local theParams = {} + local result = getNavigationActionCreators({ key = "theKey" }).setParams(theParams) + expect(result.type).to.equal(NavigationActions.SetParams) + expect(result.key).to.equal("theKey") + expect(result.params).to.equal(theParams) + end) + + it("should throw when called by a root navigator", function() + expectError(function() + getNavigationActionCreators({}).setParams({}) + end, "%.setParams%(%): cannot be called by the root navigator") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getScreenForRouteName.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getScreenForRouteName.spec.lua new file mode 100644 index 0000000..3ad8af1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getScreenForRouteName.spec.lua @@ -0,0 +1,89 @@ +return function() + local getScreenForRouteName = require(script.Parent.Parent.getScreenForRouteName) + + it("should throw for invalid arg types", function() + local status, err = pcall(function() + getScreenForRouteName("", "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "routeConfigs must be a table")).to.never.equal(nil) + + status, err = pcall(function() + getScreenForRouteName({}, 5) + end) + + expect(status).to.equal(false) + expect(string.find(err, "routeName must be a string")).to.never.equal(nil) + end) + + it("should throw if requested route is not present within table", function() + local status, err = pcall(function() + getScreenForRouteName({ + notMyRoute = function() return "foo" end + }, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "There is no route defined for key 'myRoute'.")).to.never.equal(nil) + end) + + it("should return raw table if screen and getScreen are not props", function() + local screenComponent = { render = function() return nil end } + local result = getScreenForRouteName({ + myRoute = screenComponent + }, "myRoute") + + expect(result).to.equal(screenComponent) + end) + + it("should return screen prop if it is set in route data table", function() + local screenComponent = { render = function() return nil end } + local result = getScreenForRouteName({ + myRoute = { + screen = screenComponent + } + }, "myRoute") + + expect(result).to.equal(screenComponent) + end) + + it("should return object returned by getScreen function if object is valid Roact element", function() + local screenComponent = { render = function() return nil end } + local result = getScreenForRouteName({ + myRoute = { + getScreen = function() return screenComponent end + } + }, "myRoute") + + expect(result).to.equal(screenComponent) + end) + + it("should throw if getScreen does not return a valid Roact element", function() + local status, err = pcall(function() + getScreenForRouteName({ + myRoute = { + getScreen = function() return nil end + } + }, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "The getScreen function defined for route 'myRoute'" .. + " did not return a valid screen or navigator")).to.never.equal(nil) + end) + + it("should throw if screen is not a valid Roact element", function() + local status, err = pcall(function() + getScreenForRouteName({ + myRoute = { + screen = 5, + } + }, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "screen param for key 'myRoute' must be a valid Roact component.")).to.never.equal(nil) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/validateRouteConfigMap.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/validateRouteConfigMap.spec.lua new file mode 100644 index 0000000..59a3500 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/validateRouteConfigMap.spec.lua @@ -0,0 +1,101 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local validateRouteConfigMap = require(script.Parent.Parent.validateRouteConfigMap) + + local function expectError(functor, msg) + local status, err = pcall(functor) + expect(status).to.equal(false) + expect(string.find(err, msg)).to.never.equal(nil) + end + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + return nil + end + + + it("should throw if routeConfigs is not a table", function() + expectError(function() + validateRouteConfigMap(5) + end, "routeConfigs must be a table") + end) + + it("should throw if routeConfigs is empty", function() + expectError(function() + validateRouteConfigMap({}) + end, "Please specify at least one route when configuring a navigator%.") + end) + + it("should throw if routeConfigs contains an invalid Roact element", function() + expectError(function() + validateRouteConfigMap({ + myRoute = 5, + }) + end, "The component for route 'myRoute' must be a Roact component or table with 'getScreen'%.") + end) + + it("should throw when both screen and getScreen are provided for same component", function() + expectError(function() + validateRouteConfigMap({ + myRoute = { + screen = "TheScreen", + getScreen = function() end, + } + }) + end, "Route 'myRoute' should provide 'screen' or 'getScreen', but not both%.") + end) + + it("should throw for a simple table where screen is not a Roact component", function() + expectError(function() + validateRouteConfigMap({ + myRoute = { + screen = {}, + } + }) + end, "The component for route 'myRoute' must be a Roact component or table with 'getScreen'%.") + end) + + it("should throw for a non-function getScreen", function() + expectError(function() + validateRouteConfigMap({ + myRoute = { + getScreen = 5 + } + }) + end, "The component for route 'myRoute' must be a Roact component or table with 'getScreen'%.") + end) + + it("should pass for valid basic routeConfigs", function() + validateRouteConfigMap({ + basicComponentRoute = TestComponent, + functionalComponentRoute = function() end, + stringNameComponentRoute = "Frame", + portalComponentRoute = Roact.Portal, + }) + end) + + it("should pass for valid screen prop type routeConfigs", function() + validateRouteConfigMap({ + basicComponentRoute = { + screen = TestComponent, + }, + functionalComponentRoute = { + screen = function() end, + }, + stringNameComponentRoute = { + screen = "Frame", + }, + portalComponentRoute = { + screen = Roact.Portal, + }, + }) + end) + + it("should pass for valid getScreen route configs", function() + validateRouteConfigMap({ + getScreenRoute = { + getScreen = function() end, + } + }) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/validateScreenOptions.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/validateScreenOptions.spec.lua new file mode 100644 index 0000000..c1bb8ee --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/validateScreenOptions.spec.lua @@ -0,0 +1,25 @@ +return function() + local validateScreenOptions = require(script.Parent.Parent.validateScreenOptions) + + it("should not throw when there are no problems", function() + validateScreenOptions({ title = "foo" }, { routeName = "foo" }) + end) + + it("should throw error if no routeName is provided", function() + local status, err = pcall(function() + validateScreenOptions({ title = "bar" }, {}) + end) + + expect(status).to.equal(false) + expect(string.find(err, "route.routeName must be a string")).to.never.equal(nil) + end) + + it("should throw error for options with function for title", function() + expect(function() + validateScreenOptions({ + title = function() end, + }, { routeName = "foo" }) + end).to.throw() + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/createConfigGetter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/createConfigGetter.lua new file mode 100644 index 0000000..627f478 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/createConfigGetter.lua @@ -0,0 +1,53 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local getScreenForRouteName = require(script.Parent.getScreenForRouteName) +local validateScreenOptions = require(script.Parent.validateScreenOptions) +local validate = require(script.Parent.Parent.utils.validate) + +local function applyConfig(configurer, navigationOptions, configProps) + navigationOptions = navigationOptions or {} + + local configurerType = type(configurer) + if configurerType == "function" then + return Cryo.Dictionary.join(navigationOptions, + configurer(Cryo.Dictionary.join(configProps or {}, { + navigationOptions = navigationOptions + }))) + elseif configurerType == "table" then + return Cryo.Dictionary.join(navigationOptions, configurer) + else + return navigationOptions + end +end + +return function(routeConfigs, navigatorScreenConfig) + return function(navigation, screenProps) + screenProps = screenProps or {} + local route = navigation.state + + validate(type(route) == "table", "navigation.state must be a table") + validate(type(route.routeName == "string"), "routeName must be a string") + + local component = getScreenForRouteName(routeConfigs, route.routeName) + local routeConfig = routeConfigs[route.routeName] + + local routeScreenConfig = nil + if routeConfig ~= component then + routeScreenConfig = routeConfig.navigationOptions + end + + local componentScreenConfig = type(component) == "table" + and component.navigationOptions or {} + + local configOptions = { + navigation = navigation, + screenProps = screenProps, + } + + local outputConfig = applyConfig(navigatorScreenConfig, {}, configOptions) + outputConfig = applyConfig(componentScreenConfig, outputConfig, configOptions) + outputConfig = applyConfig(routeScreenConfig, outputConfig, configOptions) + + validateScreenOptions(outputConfig, route) + return outputConfig + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getChildRouter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getChildRouter.lua new file mode 100644 index 0000000..fc50799 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getChildRouter.lua @@ -0,0 +1,20 @@ +local validate = require(script.Parent.Parent.utils.validate) + +return function(router, routeName) + validate(type(router) == "table", "router must be a table") + validate(type(routeName) == "string", "routeName must be a string") + + if router.childRouters and router.childRouters[routeName] then + return router.childRouters[routeName] + end + + validate(type(router.getComponentForRouteName) == "function", + "router.getComponentForRouteName must be a function if no child routers are specified") + local component = router.getComponentForRouteName(routeName) + if type(component) == "table" then + return component.router + else + return nil + end +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getNavigationActionCreators.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getNavigationActionCreators.lua new file mode 100644 index 0000000..753ce08 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getNavigationActionCreators.lua @@ -0,0 +1,42 @@ +local NavigationActions = require(script.Parent.Parent.NavigationActions) +local validate = require(script.Parent.Parent.utils.validate) + +return function(route) + local result = {} + + -- Go back to screen identified by 'key', or the default for current route. + function result.goBack(key) + if key == nil and route.key then + validate(type(route.key) == "string", ".goBack(): key should be a string") + key = route.key + end + + return NavigationActions.back({ key = key }) + end + + -- Navigate to a different screen, either by route name+params+action, or + -- by passing a raw navigation table. + function result.navigate(navigateTo, params, action) + if type(navigateTo) == "string" then + return NavigationActions.navigate({ + routeName = navigateTo, + params = params, + action = action, + }) + else + validate(type(navigateTo) == "table", ".navigate(): navigateTo must be a string or table") + validate(params == nil, ".navigate(): params can only be provided with a string navigateTo value") + validate(action == nil, ".navigate(): child action can only be provided with a string navigateTo value") + + return NavigationActions.navigate(navigateTo) + end + end + + -- Change navigation params for current route + function result.setParams(params) + validate(type(route.key) == "string", ".setParams(): cannot be called by the root navigator") + return NavigationActions.setParams({ params = params, key = route.key }) + end + + return result +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getScreenForRouteName.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getScreenForRouteName.lua new file mode 100644 index 0000000..16519c7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getScreenForRouteName.lua @@ -0,0 +1,32 @@ +local validate = require(script.Parent.Parent.utils.validate) +local isValidRoactElementType = require(script.Parent.Parent.utils.isValidRoactElementType) + +-- Extract a single screen Roact component/navigator from +-- a navigator's config. +return function(routeConfigs, routeName) + validate(type(routeConfigs) == "table", "routeConfigs must be a table") + validate(type(routeName) == "string", "routeName must be a string") + + local routeConfig = routeConfigs[routeName] + validate(routeConfig ~= nil, "There is no route defined for key '%s'.", routeName) + + local routeConfigType = type(routeConfig) + + if routeConfigType == "table" then + if routeConfig.screen ~= nil then + validate(isValidRoactElementType(routeConfig.screen), + "screen param for key '%s' must be a valid Roact component.", routeName) + return routeConfig.screen + elseif type(routeConfig.getScreen) == "function" then + local screen = routeConfig.getScreen() + validate(isValidRoactElementType(screen), + "The getScreen function defined for route '%s' did not return a valid screen or navigator", routeName) + return screen + end + end + + validate(isValidRoactElementType(routeConfig), + "Value for key '%s' must be a route config table or a valid Roact component.", routeName) + + return routeConfig +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/validateRouteConfigMap.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/validateRouteConfigMap.lua new file mode 100644 index 0000000..720eaf8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/validateRouteConfigMap.lua @@ -0,0 +1,41 @@ +local validate = require(script.Parent.Parent.utils.validate) +local isValidRoactElementType = require(script.Parent.Parent.utils.isValidRoactElementType) + +--[[ + This utility checks to make sure that configs passed to a + router are in the correct format. + + Example: + routeConfigs = { + routeNameEx1 = Roact.Component, + routeNameEx2 = { + screen = Roact.Component, + }, + routeNameEx3 = { + getScreen = function() + return Roact.Component + end + } + } +]] +return function(routeConfigs) + validate(type(routeConfigs) == "table", "routeConfigs must be a table") + + local atLeastOne = false + for routeName, routeConfig in pairs(routeConfigs) do + local configIsTable = type(routeConfig) == "table" or false + local screenConfig = configIsTable and routeConfig or {} -- easy index .screen/.getScreen + local screenComponent = configIsTable and routeConfig.screen or routeConfig + + validate(isValidRoactElementType(screenComponent) or type(screenConfig.getScreen) == "function", + "The component for route '%s' must be a Roact component or table with 'getScreen'.", + routeName) + validate(screenConfig.screen == nil or screenConfig.getScreen == nil, + "Route '%s' should provide 'screen' or 'getScreen', but not both.", routeName) + atLeastOne = true + end + + validate(atLeastOne, "Please specify at least one route when configuring a navigator.") + + return routeConfigs +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/validateScreenOptions.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/validateScreenOptions.lua new file mode 100644 index 0000000..87b731d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/validateScreenOptions.lua @@ -0,0 +1,11 @@ +local validate = require(script.Parent.Parent.utils.validate) + +return function(screenOptions, route) + validate(type(screenOptions) == "table", "screenOptions must be a table") + validate(type(route) == "table", "route must be a table") + validate(type(route.routeName) == "string", "route.routeName must be a string") + validate(type(screenOptions.title) ~= "function", + "title cannot be defined as a function in navigation options for screen '%s'", + route.routeName) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/KeyGenerator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/KeyGenerator.lua new file mode 100644 index 0000000..b21abea --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/KeyGenerator.lua @@ -0,0 +1,19 @@ +local uniqueBaseId = "id-" .. tostring(math.random(100000, 1000000)) +local uuidCount = 0 + +local KeyGenerator = {} + +-- NOTE: FOR TESTING ONLY. +-- Normalize keys so that tests can be consistent. +function KeyGenerator.normalizeKeys() + uniqueBaseId = "id-test-" + uuidCount = 0 +end + +-- Get a string key that is unique for this session. +function KeyGenerator.generateKey() + uuidCount = uuidCount + 1 + return uniqueBaseId .. tostring(uuidCount) +end + +return KeyGenerator diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/TableUtilities.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/TableUtilities.lua new file mode 100644 index 0000000..5922e53 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/TableUtilities.lua @@ -0,0 +1,255 @@ +--[[ + Provides functions for comparing and printing lua tables. +]] + +local TableUtilities = {} +local defaultIgnore = {} + +local function makeKeyString(key) + if type(key) == "string" then + return string.format("%s", key) + else + return string.format("[%s]", tostring(key)) + end +end + +local function makeValueString(value) + local valueType = type(value) + if valueType == "string" then + return string.format("%q", value) + elseif valueType == "function" or valueType == "table" then + return string.format("<%s>", tostring(value)) + else + return string.format("%s", tostring(value)) + end +end + +local function printKeypair(key, value, indentStr, comment) + local keyString = makeKeyString(key) + local valueString = makeValueString(value) + + local commentStr = comment and string.format(" -- %s", comment) or "" + print(string.format("%s%s = %s,%s", indentStr, keyString, valueString, commentStr)) +end + +--[[ + Takes two tables A and B, returns if they have the same key-value pairs + Except ignored keys +]] +function TableUtilities.ShallowEqual(A, B, ignore) + if not A or not B then + return false + elseif A == B then + return true + end + + if not ignore then + ignore = defaultIgnore + end + + for key, value in pairs(A) do + if B[key] ~= value and not ignore[key] then + return false + end + end + for key, value in pairs(B) do + if A[key] ~= value and not ignore[key] then + return false + end + end + + return true +end + +--[[ + Takes two tables A, B and a key, returns if two tables have the same value at key +]] +function TableUtilities.EqualKey(A, B, key) + if A and B and key and key ~= "" and A[key] and B[key] and A[key] == B[key] then + return true + end + return false +end + +--[[ + Takes two tables A and B, returns a new table with elements of A + which are either not keys in B or have a different value in B +]] +function TableUtilities.TableDifference(A, B) + local new = {} + + for key, value in pairs(A) do + if B[key] ~= A[key] then + new[key] = value + end + end + + return new +end + + +--[[ + Takes a list and returns a table whose + keys are elements of the list and whose + values are all true +]] +local function membershipTable(list) + local result = {} + for i = 1, #list do + result[list[i]] = true + end + return result +end + + +--[[ + Takes a table and returns a list of keys in that table +]] +local function listOfKeys(t) + local result = {} + for key,_ in pairs(t) do + table.insert(result, key) + end + return result +end + + +--[[ + Takes two lists A and B, returns a new list of elements of A + which are not in B +]] +function TableUtilities.ListDifference(A, B) + return listOfKeys(TableUtilities.TableDifference(membershipTable(A), membershipTable(B))) +end + + +--[[ + For debugging. Returns false if the given table has any of the following: + - a key that is neither a number or a string + - a mix of number and string keys + - number keys which are not exactly 1..#t +]] +function TableUtilities.CheckListConsistency(t) + local containsNumberKey = false + local containsStringKey = false + local numberConsistency = true + + local index = 1 + for x, _ in pairs(t) do + if type(x) == 'string' then + containsStringKey = true + elseif type(x) == 'number' then + if index ~= x then + numberConsistency = false + end + containsNumberKey = true + else + return false + end + + if containsStringKey and containsNumberKey then + return false + end + + index = index + 1 + end + + if containsNumberKey then + return numberConsistency + end + + return true +end + + +--[[ + For debugging, serializes the given table to a reasonable string that might even interpret as lua. +]] +function TableUtilities.RecursiveToString(t, indent) + indent = indent or '' + + if type(t) == 'table' then + local result = "" + if not TableUtilities.CheckListConsistency(t) then + result = result .. "-- WARNING: this table fails the list consistency test\n" + end + result = result .. "{\n" + for k,v in pairs(t) do + if type(k) == 'string' then + result = result + .. " " + .. indent + .. tostring(k) + .. " = " + .. TableUtilities.RecursiveToString(v, " "..indent) + ..";\n" + end + if type(k) == 'number' then + result = result .. " " .. indent .. TableUtilities.RecursiveToString(v, " "..indent)..",\n" + end + end + result = result .. indent .. "}" + return result + else + return tostring(t) + end +end + +--[[ + For debugging. Prints the table on multiple lines to overcome log-line length + limitations which are otherwise necessary for performance. Use sparingly. +]] +function TableUtilities.Print(t, indent) + indent = indent or ' ' + + if type(t) ~= "table" then + error("TableUtilities.Print must be passed a table", 2) + end + + -- For cycle detection + local printedTables = {} + + local function recurse(subTable, tableKey, level) + -- Prevent cycles by keeping track of what tables we have printed + printedTables[subTable] = true + + local indentStr = string.rep(indent, level) + local valueIndentStr = string.rep(indent, level + 1) + + if tableKey then + print(string.format("%s%s = %s {", indentStr, makeKeyString(tableKey), makeValueString(subTable))) + else + print(string.format("%s%s {", indentStr, makeValueString(subTable))) + end + + for key, value in pairs(subTable) do + if type(value) == "table" then + if printedTables[value] then + printKeypair(key, value, valueIndentStr, "Possible cycle") + else + recurse(value, key, level + 1) + end + else + printKeypair(key, value, valueIndentStr) + end + end + + print(string.format("%s}%s", indentStr, (level > 0 and "," or ""))) + end + + recurse(t, nil, 0) +end + +--[[ + Takes a table and returns the field count +]] +function TableUtilities.FieldCount(t) + local fieldCount = 0 + for _ in pairs(t) do + fieldCount = fieldCount + 1 + end + return fieldCount +end + +return TableUtilities + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/KeyGenerator.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/KeyGenerator.spec.lua new file mode 100644 index 0000000..2e414ac --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/KeyGenerator.spec.lua @@ -0,0 +1,14 @@ +return function() + local KeyGenerator = require(script.Parent.Parent.KeyGenerator) + + it("should generate a new string key when called", function() + KeyGenerator.normalizeKeys() + + expect(KeyGenerator.generateKey()).to.equal("id-test-1") + expect(KeyGenerator.generateKey()).to.equal("id-test-2") + end) + + it("should generate unique string keys without being normalized", function() + expect(KeyGenerator.generateKey()).to.never.equal(KeyGenerator.generateKey()) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/getActiveChildNavigationOptions.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/getActiveChildNavigationOptions.spec.lua new file mode 100644 index 0000000..99c57a5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/getActiveChildNavigationOptions.spec.lua @@ -0,0 +1,47 @@ +return function() + local getActiveChildNavigationOptions = require(script.Parent.Parent.getActiveChildNavigationOptions) + + it("should return a function", function() + expect(type(getActiveChildNavigationOptions)).to.equal("function") + end) + + it("should ask router for current screen options and return them", function() + local testInputScreenOpts = {} + local testScreenOpts = {} + + local navigation = { + state = { + routes = { + { key = "123" } + }, + index = 1, + + }, + router = {}, -- stub + } + + function navigation.getChildNavigation(key) + if key == "123" then + return navigation + else + return nil + end + end + + local testOutputScreenOpts = nil + function navigation.router.getScreenOptions(activeNav, screenProps) + testOutputScreenOpts = screenProps + + if activeNav == navigation then + return testScreenOpts + else + return nil + end + end + + expect(getActiveChildNavigationOptions(navigation, testInputScreenOpts)) + .to.equal(testScreenOpts) + expect(testOutputScreenOpts).to.equal(testInputScreenOpts) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/isValidRoactElementType.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/isValidRoactElementType.spec.lua new file mode 100644 index 0000000..d4d66d9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/isValidRoactElementType.spec.lua @@ -0,0 +1,19 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local isValidRoactElementType = require(script.Parent.Parent.isValidRoactElementType) + + it("should return true for valid element types", function() + expect(isValidRoactElementType("foo")).to.equal(true) + expect(isValidRoactElementType(function() return "foo" end)).to.equal(true) + expect(isValidRoactElementType(Roact.Portal)).to.equal(true) + expect(isValidRoactElementType( + { render = function() return "foo" end })).to.equal(true) + end) + + it("should return false for invalid element types", function() + expect(isValidRoactElementType(5)).to.equal(false) + expect(isValidRoactElementType({ render = "bad" })).to.equal(false) + expect(isValidRoactElementType( + { notRender = function() return "foo" end })).to.equal(false) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/getActiveChildNavigationOptions.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/getActiveChildNavigationOptions.lua new file mode 100644 index 0000000..a036af6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/getActiveChildNavigationOptions.lua @@ -0,0 +1,11 @@ +return function(navigation, screenProps) + local state = navigation.state + local router = navigation.router + local getChildNavigation = navigation.getChildNavigation + + local activeRoute = state.routes[state.index] + local activeNavigation = getChildNavigation(activeRoute.key) + + return router.getScreenOptions(activeNavigation, screenProps) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/getSceneIndicesForInterpolationInputRange.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/getSceneIndicesForInterpolationInputRange.lua new file mode 100644 index 0000000..a2e5c7c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/getSceneIndicesForInterpolationInputRange.lua @@ -0,0 +1,44 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) + +return function(props) + local scene = props.scene + local scenes = props.scenes + + local index = scene.index + local lastSceneIndexInScenes = #scenes + local isBack = not scenes[lastSceneIndexInScenes].isActive + + if isBack then + local currentSceneIndexInScenes = Cryo.List.find(scenes, scene) + + local targetSceneIndexInScenes = nil + for i, iScene in ipairs(scenes) do + if iScene.isActive then + targetSceneIndexInScenes = i + break + end + end + + local targetSceneIndex = scenes[targetSceneIndexInScenes].index + local lastSceneIndex = scenes[lastSceneIndexInScenes].index + + if index ~= targetSceneIndex and currentSceneIndexInScenes == lastSceneIndexInScenes then + return { + first = math.min(targetSceneIndex, index - 1), + last = index + 1, + } + elseif index == targetSceneIndex and currentSceneIndexInScenes == targetSceneIndexInScenes then + return { + first = index - 1, + last = math.max(lastSceneIndex, index + 1) + } + elseif index == targetSceneIndex or currentSceneIndexInScenes > targetSceneIndexInScenes then + return nil + end + end + + return { + first = index - 1, + last = index + 1 + } +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/isValidRoactElementType.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/isValidRoactElementType.lua new file mode 100644 index 0000000..05d5214 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/isValidRoactElementType.lua @@ -0,0 +1,11 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) + +-- Returns true if the provided object can be used by Roact.createElement(). +-- We have this method because Roact does not expose a type-checking API yet. +return function(elementType) + local theType = type(elementType) + + return theType == "string" or theType == "function" or elementType == Roact.Portal or + (theType == "table" and type(elementType.render) == "function") +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/validate.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/validate.lua new file mode 100644 index 0000000..a31a4ce --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/validate.lua @@ -0,0 +1,19 @@ +--[[ + Validate() provides a mechanism to validate input arguments and + internal state for your components. You can call it like this: + + function myFunc(arg1, arg2) + validate(arg1 ~= arg2, "arg1 (%s) and arg2 (%s) must be different!", + tostring(arg1), tostring(arg2)) + return doSomething(arg1, arg2) + end + + The error will be surfaced at the *call site* of your function. +]] +return function(result, ...) + if not result then + error(string.format(...), 3) + end + + return result +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/AppNavigationContext.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/AppNavigationContext.lua new file mode 100644 index 0000000..ac91d5c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/AppNavigationContext.lua @@ -0,0 +1,60 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol) +local validate = require(script.Parent.Parent.utils.validate) + +local APP_NAVIGATION_CONTEXT = NavigationSymbol("APP_NAVIGATION_CONTEXT") + +-- Provider +local NavigationProvider = Roact.Component:extend("NavigationProvider") + +function NavigationProvider:init() + local navigation = self.props.navigation + validate(navigation ~= nil, "AppNavigationContext.Provider requires a 'navigation' prop.") + self._context[APP_NAVIGATION_CONTEXT] = { navigation = navigation } +end + +function NavigationProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +-- Consumer +local NavigationConsumer = Roact.Component:extend("NavigationConsumer") + +function NavigationConsumer:render() + local renderProp = self.props.render + local context = self._context[APP_NAVIGATION_CONTEXT] or {} + local navigation = self.props.navigation or context.navigation + + validate(renderProp ~= nil, "AppNavigationContext.Consumer requires 'render' prop.") + validate(navigation ~= nil, "AppNavigationContext.Consumer requires a navigation prop or context entry.") + + return renderProp(navigation) +end + +-- Static connector +local function connect(innerComponent) + local componentName = string.format("NavigationConnection(%s)", tostring(innerComponent)) + local Connection = Roact.Component:extend(componentName) + + function Connection:render() + local props = self.props + + return Roact.createElement(NavigationConsumer, { + navigation = props.navigation, -- can be passed directly to wrapper + render = function(navigation) + return Roact.createElement(innerComponent, Cryo.Dictionary.join({ + navigation = navigation + }, props)) -- join other props last so someone can manually pass in 'navigation' + end + }) + end + + return Connection +end + +return { + Provider = NavigationProvider, + Consumer = NavigationConsumer, + connect = connect, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ContentHeightFitFrame.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ContentHeightFitFrame.lua new file mode 100644 index 0000000..2d89978 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ContentHeightFitFrame.lua @@ -0,0 +1,90 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Roact) +local validate = require(script.Parent.Parent.utils.validate) + +--[[ + ContentHeightFitFrame creates a UIListLayout-based FitFrame around a single child. + The FitFrame will grow/shrink in height to exactly fit its child, but takes up the + entire useable horizontal space in its parent container. + + Props: + contentHorizontalAlignment - How a smaller-than-width child should be aligned + within the ContentHeightFitFrame. (Optional) + initialHeight - Starting height for the ContentHeightFirFrame. (Optional) + onHeightChanged - Callback to monitor height changes. (Optional) + + In addition to the above props, the normal Frame props can be passed through + (BackgroundColor3, BackgroundTransparency, etc). +]] +local ContentHeightFitFrame = Roact.Component:extend("ContentHeightFitFrame") + +ContentHeightFitFrame.defaultProps = { + initialHeight = 0, + contentHorizontalAlignment = Enum.HorizontalAlignment.Center, +} + +function ContentHeightFitFrame:init() + self._layoutRef = Roact.createRef() + + local containerRef = Roact.createRef() + self._getRef = function() + return self.props[Roact.Ref] or containerRef + end + + self._onResize = function() + local onHeightChanged = self.props.onHeightChanged + local layoutInstance = self._layoutRef.current + local containerInstance = self:_getRef().current + + if not layoutInstance or not containerInstance then + return + end + + local layoutSize = layoutInstance.AbsoluteContentSize + if layoutSize.Y ~= containerInstance.Size.Y then + containerInstance.Size = UDim2.new(1, 0, 0, layoutSize.Y) + + if onHeightChanged then + onHeightChanged(layoutSize.Y) + end + end + end +end + +function ContentHeightFitFrame:render() + validate(self.props.Size == nil, "Size cannot be specified with ContentHeightFitFrame!") + local contentHorizontalAlignment = self.props.contentHorizontalAlignment + local initialHeight = self.props.initialHeight + local children = self.props[Roact.Children] + + local containerProps = Cryo.Dictionary.join(self.props, { + contentHorizontalAlignment = Cryo.None, + initialHeight = Cryo.None, + onHeightChanged = Cryo.None, + [Roact.Children] = Cryo.None, + [Roact.Ref] = self:_getRef(), + Size = UDim2.new(1, 0, 0, initialHeight), -- Will adjust by size change callback. + }) + + return Roact.createElement("Frame", containerProps, { + ["$FitLayout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = contentHorizontalAlignment, + VerticalAlignment = Enum.VerticalAlignment.Top, + Padding = UDim.new(0, 0), + [Roact.Change.AbsoluteContentSize] = self._onResize, + [Roact.Ref] = self._layoutRef, + }), + ["$Content"] = Roact.oneChild(children), + }) +end + +function ContentHeightFitFrame:didMount() + self._onResize() +end + +function ContentHeightFitFrame:didUpdate() + self._onResize() +end + +return ContentHeightFitFrame diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/NavigationEventsAdapter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/NavigationEventsAdapter.lua new file mode 100644 index 0000000..76c8c1b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/NavigationEventsAdapter.lua @@ -0,0 +1,91 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local AppNavigationContext = require(script.Parent.AppNavigationContext) +local NavigationEvents = require(script.Parent.Parent.NavigationEvents) +local validate = require(script.Parent.Parent.utils.validate) + +--[[ + NavigationEventsAdapter providers a wrapper component that allows you to subscribe + to the navigation lifecycle events without having to explicitly manage your own + listener subscriptions. + + Usage: + + function MyComponent:init() + self.willFocus = function() + -- Do tasks that need to happen right before the component will appear on screen. + end + + self.didFocus = function() + -- Do tasks that need to happen right after the component appears on screen. + end + end + + function MyComponent:render() + -- Note that you must capture the self reference lexically, if you need it. + return Roact.createElement(RoactNavigation.EventsAdapter, { + [RoactNavigation.Events.WillFocus] = self.willFocus, + [RoactNavigation.Events.DidFocus] = self.didFocus, + [RoactNavigation.Events.WillBlur] = self.willBlur, + [RoactNavigation.Events.DidBlur] = self.didBlur, + }, ) + end + + Remember that focus and blur events may be called more than once in the lifetime of a + component. If you navigate away from a component and then come back later, it will receive + willBlur/didBlur and then willFocus/didFocus events. + + Also remember that your event handlers must capture any self reference lexically, if necessary. +]] +local NavigationEventsAdapter = Roact.Component:extend("NavigationEventsAdapter") + +function NavigationEventsAdapter:init() + self.subscriptions = {} +end + +function NavigationEventsAdapter:_subscribeAll() + local navigation = self.props.navigation + assert(navigation ~= nil, "NavigationEventsAdapter can only be used within the view hierarchy of a navigator.") + + for _, symbol in pairs(NavigationEvents) do + self.subscriptions[symbol] = navigation.addListener(symbol, function(...) + -- Retrieve callback from props each time, in case props change. + local callback = self.props[symbol] or nil + if callback then + validate(type(callback) == "function", "Value for event '%s' must be a function callback", tostring(symbol)) + callback(...) + end + end) + end +end + +function NavigationEventsAdapter:_disconnectAll() + for _, symbol in pairs(NavigationEvents) do + local sub = self.subscriptions[symbol] + if sub then + sub.disconnect() + self.subscriptions[symbol] = nil + end + end +end + +function NavigationEventsAdapter:didMount() + self:_subscribeAll() +end + +function NavigationEventsAdapter:willUnmount() + self:_disconnectAll() +end + +function NavigationEventsAdapter:didUpdate(prevProps) + if self.props.navigation ~= prevProps.navigation then + -- This component might get reused for different state, so we need to hook back up to events + self:_disconnectAll() + self:_subscribeAll() + end +end + +function NavigationEventsAdapter:render() + return Roact.createElement("Folder", nil, self.props[Roact.Children]) +end + +return AppNavigationContext.connect(NavigationEventsAdapter) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/SceneView.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/SceneView.lua new file mode 100644 index 0000000..4e8fc00 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/SceneView.lua @@ -0,0 +1,21 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local AppNavigationContext = require(script.Parent.AppNavigationContext) + +local SceneView = Roact.PureComponent:extend("SceneView") + +function SceneView:render() + local screenProps = self.props.screenProps + local component = self.props.component + local navigation = self.props.navigation + + return Roact.createElement(AppNavigationContext.Provider, { + navigation = navigation, + }, { + Scene = Roact.createElement(component, { + screenProps = screenProps, + navigation = navigation, + }) + }) +end + +return SceneView diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ScenesReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ScenesReducer.lua new file mode 100644 index 0000000..9ab7ca4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ScenesReducer.lua @@ -0,0 +1,201 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local TableUtilities = require(script.Parent.Parent.utils.TableUtilities) +local validate = require(script.Parent.Parent.utils.validate) + +local SCENE_KEY_PREFIX = "scene_" + +-- Compare two scenes based upon index and view key. +local function compareScenes(a, b) + if a.index == b.index then + -- compare the route keys + local delta = #a.key - #b.key + if delta == 0 then + return a.key < b.key + else + return delta < 0 + end + else + -- rank by index first + return a.index < b.index + end +end + +local function routesAreShallowEqual(a, b) + if not a or not b then + return a == b + end + + if a.key ~= b.key then + return false + end + + return TableUtilities.ShallowEqual(a, b) +end + +local function scenesAreShallowEqual(a, b) + return + a.key == b.key and + a.index == b.index and + a.isStale == b.isStale and + a.isActive == b.isActive and + routesAreShallowEqual(a, b) +end + +return function(scenes, nextState, prevState, descriptors) + -- Always update descriptors. See react-navigation's bug, here: + -- https://github.com/react-navigation/react-navigation/issues/4271 + -- TODO: Do we need this? Can we do a real fix? + for _, scene in ipairs(scenes) do + local route = scene.route + if descriptors and descriptors[route.key] then + scene.desciptor = descriptors[route.key] + end + end + + -- Bail out early if state is not updated + if prevState == nextState then + return scenes + end + + local prevScenes = {} + local freshScenes = {} + local staleScenes = {} + + -- previously stale scenes should be marked stale + for _, scene in ipairs(scenes) do + local key = scene.key + if scene.isStale then + staleScenes[key] = scene + end + + prevScenes[key] = scene + end + + local nextKeys = {} -- fake set! + local nextRoutes = nextState.routes + local nextRoutesLength = #nextRoutes + + -- Clip nextRoutes to stop at index because index is top of stack! + if nextRoutesLength > nextState.index then + print("Warning: StackRouter provided invalid state. Index should always be the top route") + nextRoutes = Cryo.List.removeRange(nextRoutes, nextState.index, nextRoutesLength) + end + + for index, route in ipairs(nextRoutes) do + local key = SCENE_KEY_PREFIX .. route.key + local descriptor = descriptors and descriptors[route.key] or nil + + local scene = { + index = index, + isActive = false, + isStale = false, + key = key, + route = route, + descriptor = descriptor, + } + + validate(not nextKeys[key], "navigation.state.routes[%d].key '%s' conflicts with another route!", index, key) + nextKeys[key] = true + + if staleScenes[key] then + -- Previously stale scene was added to nextState, so we remove it from + -- the map of stale scenes. + staleScenes[key] = nil + end + + freshScenes[key] = scene + end + + if prevState then + local prevRoutes = prevState.routes + local prevRoutesLength = #prevRoutes + if prevRoutesLength > prevState.index then + print("StackRouter provided invalid state. Index should always be the top route.") + prevRoutes = Cryo.List.removeRange(prevRoutes, prevState.index, prevRoutesLength) + end + + -- Search previous routes and mark any removed scenes as stale + for index, route in ipairs(prevRoutes) do + local key = SCENE_KEY_PREFIX .. route.key + -- Skip any refreshed scenes + if not freshScenes[key] then + local lastScene = nil + for _, scene in ipairs(scenes) do + if scene.route.key == route.key then + lastScene = scene + break + end + end + + local descriptor = descriptors[route.key] + if lastScene then + descriptor = lastScene.descriptor + end + + if descriptor then + staleScenes[key] = { + index = index, + isActive = false, + isStale = true, + key = key, + route = route, + descriptor = descriptor, + } + end + end + end + end + + local nextScenes = {} + + local function mergeScene(nextScene) + local key = nextScene.key + local prevScene = prevScenes[key] or nil + if prevScene and scenesAreShallowEqual(prevScene, nextScene) then + -- reuse prevScene to avoid re-render + table.insert(nextScenes, prevScene) + else + table.insert(nextScenes, nextScene) + end + end + + for _, scene in pairs(staleScenes) do + mergeScene(scene) + end + + for _, scene in pairs(freshScenes) do + mergeScene(scene) + end + + table.sort(nextScenes, compareScenes) + + local activeScenesCount = 0 + for index, scene in ipairs(nextScenes) do + local isActive = not scene.isStale and scene.index == nextState.index + if isActive ~= scene.isActive then + nextScenes[index] = Cryo.Dictionary.join(scene, { + isActive = isActive, + }) + end + + if isActive then + activeScenesCount = activeScenesCount + 1 + end + end + + validate(activeScenesCount == 1, "There should only be one active scene, not %d", activeScenesCount) + + -- Conditionally return nextScenes based upon shallow comparison, for performance + if #nextScenes ~= #scenes then + return nextScenes + end + + for index, scene in ipairs(nextScenes) do + if not scenesAreShallowEqual(scenes[index], scene) then + return nextScenes + end + end + + -- Scenes have not changed + return scenes +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ScreenGuiWrapper.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ScreenGuiWrapper.lua new file mode 100644 index 0000000..1bb98fa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ScreenGuiWrapper.lua @@ -0,0 +1,36 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Roact) + +local ScreenGuiWrapper = Roact.PureComponent:extend("ScreenGuiWrapper") + +ScreenGuiWrapper.defaultProps = { + DisplayOrder = 0, + OnTopOfCoreBlur = false, + visible = true, +} + +function ScreenGuiWrapper:render() + local props = self.props + local component = props.component + local visible = props.visible + local displayOrder = props.DisplayOrder + local onTopOfCoreBlur = props.OnTopOfCoreBlur + + local filteredProps = Cryo.Dictionary.join(props, { + component = Cryo.None, + DisplayOrder = Cryo.None, + OnTopOfCoreBlur = Cryo.None, + -- visible prop is passed down for convenience of inner component. + }) + + return Roact.createElement("ScreenGui", { + Enabled = visible, + ZIndexBehavior = Enum.ZIndexBehavior.Sibling, + DisplayOrder = displayOrder, + OnTopOfCoreBlur = onTopOfCoreBlur, + }, { + InnerComponent = Roact.createElement(component, filteredProps) + }) +end + +return ScreenGuiWrapper diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackHeaderMode.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackHeaderMode.lua new file mode 100644 index 0000000..a16b48c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackHeaderMode.lua @@ -0,0 +1,15 @@ +local NavigationSymbol = require(script.Parent.Parent.Parent.NavigationSymbol) + +local FLOAT_SYMBOL = NavigationSymbol("FLOAT") +local SCREEN_SYMBOL = NavigationSymbol("SCREEN") +local NONE_SYMBOL = NavigationSymbol("NONE") + +--[[ + StackHeaderMode determines the behavior of the header when screens + are pushed/popped from the stack. +]] +return { + None = NONE_SYMBOL, -- No header. + Float = FLOAT_SYMBOL, -- Header that stays in place during transitions. + Screen = SCREEN_SYMBOL, -- Header that sticks to each screen and transitions with it. +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackPresentationStyle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackPresentationStyle.lua new file mode 100644 index 0000000..d11af5e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackPresentationStyle.lua @@ -0,0 +1,43 @@ +local NavigationSymbol = require(script.Parent.Parent.Parent.NavigationSymbol) + +local DEFAULT_SYMBOL = NavigationSymbol("DEFAULT") +local MODAL_SYMBOL = NavigationSymbol("MODAL") +local OVERLAY_SYMBOL = NavigationSymbol("OVERLAY") + +--[[ + StackPresentationStyle is used with stack navigators/views to determine + the behavior of a given screen when it is pushed/popped from the stack, as + well as the visual effects/transitions applied while the view is on screen. +]] +return { + --[[ + The Default presentation style draws stack cards so that they will + slide in and out from the right side of the screen one at a time. No + special visual effects are applied, and cards always fill the entire space + available. Your screen content is rendered over an opaque background color + by default, but you have the option to draw the cards with a semi- or fully + transparent background via navigationOptions. Cards always prevent + tap-through of the content underneath them (in case your navigation + container is transparent). + ]] + Default = DEFAULT_SYMBOL, + + --[[ + The Modal presentation style causes screens to animate up/down from the + bottom of the navigation container and visually stack on top of each + other. Cards are opaque by default, but you may set navigationOptions + to make them semi- or fully transparent so that you can see the underlying + cards. Modal cards always prevent tap-through of any underlying UI, including + other cards in the same stack. + ]] + Modal = MODAL_SYMBOL, + + --[[ + The Overlay presentation style causes screens to pop in (later they will fade in) + on top of the underlying screens. Like modals, they visually stack on top of each + other. Cards are opaque by default, but you may set navigationOptions to make + them semi- or fully transparent. Overlay cards always prevent tap-through of any + underlying UI, including other cards in the same stack. + ]] + Overlay = OVERLAY_SYMBOL +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackView.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackView.lua new file mode 100644 index 0000000..9071f8e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackView.lua @@ -0,0 +1,100 @@ +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) +local StackViewLayout = require(script.Parent.StackViewLayout) +local Transitioner = require(script.Parent.Parent.Transitioner) +local StackViewTransitionConfigs = require(script.Parent.StackViewTransitionConfigs) +local StackPresentationStyle = require(script.Parent.StackPresentationStyle) + +local defaultNavigationConfig = { + mode = StackPresentationStyle.Default, +} + + +local StackView = Roact.Component:extend("StackView") + +function StackView:init() + self._doRender = function(...) + return self:_render(...) + end + + self._doConfigureTransition = function(...) + return self:_configureTransition(...) + end + + self._doOnTransitionEnd = function(...) + return self:_onTransitionEnd(...) + end +end + +function StackView:render() + local screenProps = self.props.screenProps + local navigation = self.props.navigation + local descriptors = self.props.descriptors + local onTransitionStart = self.props.onTransitionStart + or self.props.navigationConfig.onTransitionStart + + -- Transitioner handles setting up the animation motors and making that data + -- available to the lower layer. + return Roact.createElement(Transitioner, { + render = self._doRender, + configureTransition = self._doConfigureTransition, + screenProps = screenProps, + navigation = navigation, + descriptors = descriptors, + onTransitionStart = onTransitionStart, + onTransitionEnd = self._doOnTransitionEnd, + }) +end + +function StackView:didMount() + local navigation = self.props.navigation + if navigation.state.isTransitioning then + navigation.dispatch(NavigationActions.completeTransition({ + key = navigation.state.key, + })) + end +end + +function StackView:_render(transitionProps, lastTransitionProps) + local screenProps = self.props.screenProps + local navigationConfig = Cryo.Dictionary.join(defaultNavigationConfig, self.props.navigationConfig) + local descriptors = self.props.descriptors + + return Roact.createElement(StackViewLayout, Cryo.Dictionary.join(navigationConfig, { + screenProps = screenProps, + descriptors = descriptors, + transitionProps = transitionProps, + lastTransitionProps = lastTransitionProps, + })) +end + +function StackView:_configureTransition(transitionProps, prevTransitionProps) + return StackViewTransitionConfigs.getTransitionConfig( + self.props.navigationConfig.transitionConfig, + transitionProps, + prevTransitionProps, + self.props.navigationConfig.mode + ).transitionSpec +end + +function StackView:_onTransitionEnd(transition, lastTransition) + local navigationConfig = self.props.navigationConfig + local navigation = self.props.navigation + local onTransitionEnd = navigationConfig.onTransitionEnd + local transitionDestKey = transition.scene.route.key + local isCurrentKey = navigation.state.routes[navigation.state.index].key == transitionDestKey + + if transition.navigation.state.isTransitioning and isCurrentKey then + navigation.dispatch(NavigationActions.completeTransition({ + key = navigation.state.key, + toChildKey = transitionDestKey, + })) + end + + if onTransitionEnd then + onTransitionEnd(transition, lastTransition) + end +end + +return StackView diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewCard.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewCard.lua new file mode 100644 index 0000000..a848dd8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewCard.lua @@ -0,0 +1,114 @@ +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local validate = require(script.Parent.Parent.Parent.utils.validate) + +--[[ + Render a scene as a card for use in a StackView. This component is + responsible for correctly positioning the scene content in relation + to the other scenes. The content will be rendered inside a completely + transparent Frame whose position and size are controlled by the + transition logic. Any additional visual effects must be supplied by + the container or the child element created by renderScene(). + + Props: + renderScene(scene) -- Render prop to draw the scene inside the card. + initialPosition -- Starting position for the card. (Animated by Otter from there). + positionStep -- Stepper function from StackViewInterpolator. + position -- Otter motor for the position of the card. + scene -- Scene that the card is to render. + forceHidden -- Forcibly disable card rendering (e.g. animated off-screen). + transparent -- Card allows underlying content to show through (default: false). + cardColor3 -- Color of the card background if it's not transparent (default: engine setting). +]] +local StackViewCard = Roact.Component:extend("StackViewCard") + +StackViewCard.defaultProps = { + transparent = false, +} + +function StackViewCard:init() + local currentNavIndex = self.props.navigation.state.index + + self._isMounted = false + self._positionLastValue = currentNavIndex + + local selfRef = Roact.createRef() + self._getRef = function() + return self.props[Roact.Ref] or selfRef + end +end + +function StackViewCard:render() + local forceHidden = self.props.forceHidden + local cardColor3 = self.props.cardColor3 + local transparent = self.props.transparent + local initialPosition = self.props.initialPosition + local renderScene = self.props.renderScene + local scene = self.props.scene + + validate(type(renderScene) == "function", "renderScene must be a function") + + return Roact.createElement("Frame", { + Position = initialPosition, + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = cardColor3 or nil, + BackgroundTransparency = transparent and 1 or nil, + BorderSizePixel = 0, + ClipsDescendants = true, + Visible = not forceHidden, + [Roact.Ref] = self:_getRef(), + }, { + ["$content"] = renderScene(scene), + }) +end + +function StackViewCard:didMount() + self._isMounted = true + + local position = self.props.position + self._positionDisconnector = position:onStep(function(...) + self:_onPositionStep(...) + end) +end + +function StackViewCard:willUnmount() + self._isMounted = false + + if self._positionDisconnector then + self._positionDisconnector() + self._positionDisconnector = nil + end +end + +function StackViewCard:didUpdate(oldProps) + local position = self.props.position + local positionStep = self.props.positionStep + + if position ~= oldProps.position then + self._positionDisconnector() + self._positionDisconnector = position:onStep(function(...) + self:_onPositionStep(...) + end) + end + + if positionStep ~= oldProps.positionStep then + -- The motor won't fire just because stepper function has changed. We have to + -- update the position to match new requirements based upon last motor value. + self:_onPositionStep(self._positionLastValue) + end +end + +function StackViewCard:_onPositionStep(value) + if not self._isMounted then + return + end + + local positionStep = self.props.positionStep + + if positionStep then + positionStep(self:_getRef(), value) + end + + self._positionLastValue = value +end + +return StackViewCard diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewInterpolator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewInterpolator.lua new file mode 100644 index 0000000..84be517 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewInterpolator.lua @@ -0,0 +1,220 @@ +--[[ + Provides builders to create functions that interpolate the current Otter motor + position into the correct translation for stack cards based upon their associated + scene. + + Interpolator builders expect the following props as input: + { + initialPositionValue = , + scene = , + layout = { + initWidth = , + initHeight = , + isMeasured = , + } + } + + Each builder returns a props table to be merged onto your other StackViewCard props, ex: + { + positionStep = , + initialPosition = , + forceHidden = true, -- May disable card visibility if it's outside interpolating range. + } + + The props table may contain other changes, depending on the requirements of the animation. +]] +local getSceneIndicesForInterpolationInputRange = require( + script.Parent.Parent.Parent.utils.getSceneIndicesForInterpolationInputRange) + +-- Helper interpolates t with range [0,1] into the range [a,b]. +local function lerp(a, b, t) + return a * (1 - t) + b * t +end + +-- Render initial style when layout hasn't been measured yet. +local function forInitial(props) + local initialPositionValue = props.initialPositionValue + local scene = props.scene + + local forceHidden = initialPositionValue ~= scene.index + local translate = forceHidden and 1000000 or 0 + + return { + forceHidden = forceHidden, + initialPosition = UDim2.new(0, translate, 0, translate), + positionStep = nil, + } +end + +-- Slide-in from right style (e.g. navigation stack view). +local function forHorizontal(props) + local initialPositionValue = props.initialPositionValue + local layout = props.layout + local scene = props.scene + + if not layout.isMeasured then + return forInitial(props) + end + + local interpolate = getSceneIndicesForInterpolationInputRange(props) + + -- getSceneIndices* returns nil if card is not visible and need not be + -- considered for the animation until state changes. + if not interpolate then + return { + forceHidden = true, + initialPosition = UDim2.new(0, 100000, 0, 100000), + positionStep = nil, + } + end + + local first = interpolate.first + local last = interpolate.last + local index = scene.index + + local width = layout.initWidth + + local function calculate(positionValue) + -- 3 range LERP + if positionValue < first then + return width + elseif positionValue < index then + return lerp(width, 0, (positionValue - first) / (index - first)) + elseif positionValue == index then + return 0 + elseif positionValue < last then + return lerp(0, -width, (positionValue - index) / (last - index)) + else + return -width + end + end + + local function stepper(cardRef, positionValue) + local cardInstance = cardRef.current + if not cardInstance then + return + end + + local oldPosition = cardInstance.Position + cardInstance.Position = UDim2.new( + oldPosition.X.Scale, + calculate(positionValue), + oldPosition.Y.Scale, + oldPosition.Y.Offset + ) + end + + local initialPosition = UDim2.new(0, calculate(initialPositionValue), 0, 0) + + return { + initialPosition = initialPosition, + positionStep = stepper, + } +end + +-- Slide-in from bottom style (e.g. modals). +local function forVertical(props) + local initialPositionValue = props.initialPositionValue + local layout = props.layout + local scene = props.scene + + if not layout.isMeasured then + return forInitial(props) + end + + local interpolate = getSceneIndicesForInterpolationInputRange(props) + + if not interpolate then + return { + forceHidden = true, + initialPosition = UDim2.new(0, 100000, 0, 100000), + positionStep = nil, + } + end + + local first = interpolate.first + local index = scene.index + local height = layout.initHeight + + local function calculate(positionValue) + -- 2 range LERP + if positionValue < first then + return height + elseif positionValue < index then + return lerp(height, 0, (positionValue - first) / (index - first)) + else + return 0 + end + end + + local function stepper(cardRef, positionValue) + local cardInstance = cardRef.current + if not cardInstance then + return + end + + local oldPosition = cardInstance.Position + cardInstance.Position = UDim2.new( + oldPosition.X.Scale, + oldPosition.X.Offset, + oldPosition.Y.Scale, + calculate(positionValue) + ) + end + + local initialPosition = UDim2.new(0, 0, 0, calculate(initialPositionValue)) + + return { + initialPosition = initialPosition, + positionStep = stepper, + } +end + +-- Fade in place animation (e.g. popovers and toasts). Note that since we don't currently have +-- group transparency, this 'animation' just pops the views in for now. +local function forFade(props) + local initialPositionValue = props.initialPositionValue + local layout = props.layout + local scene = props.scene + + if not layout.isMeasured then + return forInitial(props) + end + + local interpolate = getSceneIndicesForInterpolationInputRange(props) + + if not interpolate then + return { + forceHidden = true, + initialPosition = UDim2.new(0, 100000, 0, 100000), + positionStep = nil, + } + end + + local index = scene.index + + local function calculate(positionValue) + return positionValue >= index - 0.5 + end + + local function stepper(cardRef, positionValue) + local cardInstance = cardRef.current + if not cardInstance then + return + end + + cardInstance.Visible = calculate(positionValue) + end + + return { + forceHidden = not calculate(initialPositionValue), + initialPosition = UDim2.new(0, 0, 0, 0), + positionStep = stepper, + } +end + +return { + forHorizontal = forHorizontal, + forVertical = forVertical, + forFade = forFade, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewLayout.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewLayout.lua new file mode 100644 index 0000000..5f7ccd0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewLayout.lua @@ -0,0 +1,420 @@ +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local Otter = require(script.Parent.Parent.Parent.Parent.Otter) +local AppNavigationContext = require(script.Parent.Parent.AppNavigationContext) +local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) +local StackHeaderMode = require(script.Parent.StackHeaderMode) +local StackPresentationStyle = require(script.Parent.StackPresentationStyle) +local NoneSymbol = require(script.Parent.Parent.Parent.NoneSymbol) +local StackViewTransitionConfigs = require(script.Parent.StackViewTransitionConfigs) +local StackViewCard = require(script.Parent.StackViewCard) +local SceneView = require(script.Parent.Parent.SceneView) +local TopBar = require(script.Parent.Parent.TopBar.TopBar) +local ContentHeightFitFrame = require(script.Parent.Parent.ContentHeightFitFrame) +local validate = require(script.Parent.Parent.Parent.utils.validate) + +local defaultScreenOptions = { + overlayEnabled = false, + overlayColor3 = Color3.new(0, 0, 0), + overlayTransparency = 0.7, + -- cardColor3 default not needed; we use the engine's default frame color +} + +-- Helper interpolates t with range [0,1] into the range [a,b]. +local function lerp(a, b, t) + return a * (1 - t) + b * t +end + +local function calculateFadeTransparency(scene, index, positionValue) + local navigationOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {}) + local overlayEnabled = navigationOptions.overlayEnabled + local overlayTransparency = navigationOptions.overlayTransparency + + if overlayEnabled then + local pRange = math.max(math.min(1 + positionValue - index, 1), 0) + return lerp(1, overlayTransparency, pRange) + else + return 1 + end +end + + +local StackViewLayout = Roact.Component:extend("StackViewLayout") + +function StackViewLayout:init() + local startingIndex = self.props.transitionProps.navigation.state.index + + self._isMounted = false + self._scenesContainerRef = Roact.createRef() + + self._overlayFrameRefs = {} -- map of scene indexes to refs + + self._positionLastValue = startingIndex + + self._renderScene = function(scene) + return self:_renderInnerScene(scene) + end +end + +function StackViewLayout:_getHeaderMode() + if self.props.headerMode then + return self.props.headerMode + elseif self.props.mode == StackPresentationStyle.Modal then + return StackHeaderMode.Screen + else + -- TODO: Change back to Float when TopBar implements it + -- return StackHeaderMode.Float + return StackHeaderMode.Screen + end +end + +function StackViewLayout:_renderHeader(scene, headerMode) + local options = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {}) + local header = options.header + + validate(type(header) ~= "string", + "header must be a valid Roact component, RoactNavigation.None, or nil, not a string") + + -- If HeaderMode is Screen and no header was explicitly removed from + -- navigationOptions, then we do NOT want to render a header for this screen! + if header == NoneSymbol and headerMode == StackHeaderMode.Screen then + return nil + end + + -- We will use header component if supplied, otherwise use default TopBar. + local headerComponent = header ~= NoneSymbol and header or function(headerProps) + return Roact.createElement(TopBar, headerProps) + end + + local transitionProps = self.props.transitionProps or {} + local passProps = Cryo.Dictionary.join(self.props, { + transitionProps = Cryo.None, + }) + + return Roact.createElement(AppNavigationContext.Provider, { + navigation = scene.descriptor.navigation, + }, { + ["$header"] = Roact.createElement(headerComponent, Cryo.Dictionary.join( + passProps, + transitionProps, -- transitionProps override directly passed props + { + scene = scene, + mode = headerMode, + }) + ) + }) +end + +function StackViewLayout:_reset(resetToIndex, frequency) + local position = self.props.transitionProps.position + + position:setGoal(Otter.spring(resetToIndex, { + frequency, + })) +end + +function StackViewLayout:_goBack(backFromIndex, frequency) + local navigation = self.props.transitionProps.navigation + local position = self.props.transitionProps.position + local scenes = self.props.transitionProps.scenes + + local toValue = math.max(backFromIndex - 1, 1) + + -- Set up temporary completion handler + local onCompleteDisconnector + onCompleteDisconnector = position:onComplete(function() + if onCompleteDisconnector then + onCompleteDisconnector() + onCompleteDisconnector = nil + end + + local backFromScene + for _, scene in ipairs(scenes) do + if scene.index == toValue + 1 then + backFromScene = scene + break + end + end + + if backFromScene then + navigation.dispatch(NavigationActions.back({ + key = backFromScene.route.key, + immediate = true, + })) + + navigation.dispatch(NavigationActions.completeTransition()) + end + end) + + position:setGoal(Otter.spring(toValue, { + frequency = frequency, + })) +end + +function StackViewLayout:_onFloatingHeaderHeightChanged(height) + local scenesContainer = self._scenesContainerRef.current + if self._isMounted and scenesContainer then + scenesContainer.Position = UDim2.new(0, 0, 0, height) + scenesContainer.Size = UDim2.new(1, 0, 1, -height) + end +end + +function StackViewLayout:_renderCard(scene, index) + local transitionProps = self.props.transitionProps -- Core animation info from Transitioner. + local lastTransitionProps = self.props.lastTransitionProps -- Previous transition info. + local transitionConfig = self.state.transitionConfig -- State based info from scene config. + + local navigationOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {}) + + local cardColor3 = navigationOptions.cardColor3 + local overlayEnabled = navigationOptions.overlayEnabled + + local initialPositionValue = transitionProps.scene.index + if lastTransitionProps then + initialPositionValue = lastTransitionProps.scene.index + end + + local cardInterpolationProps = {} + local screenInterpolator = transitionConfig.screenInterpolator + if screenInterpolator then + cardInterpolationProps = screenInterpolator( + Cryo.Dictionary.join(transitionProps, { + initialPositionValue = initialPositionValue, + scene = scene, + }) + ) + end + + -- Merge down the various prop packages to be applied to StackViewCard. + return Roact.createElement(StackViewCard, Cryo.Dictionary.join( + transitionProps, cardInterpolationProps, { + key = "card_" .. tostring(scene.key), + scene = scene, + renderScene = self._renderScene, + transparent = overlayEnabled, + cardColor3 = cardColor3, + }) + ) +end + +function StackViewLayout:_renderInnerScene(scene) + local navigation = scene.descriptor.navigation + + local sceneComponent = scene.descriptor.getComponent() + local screenProps = self.props.screenProps + + local sceneElement = Roact.createElement(SceneView, { + screenProps = screenProps, + navigation = navigation, + component = sceneComponent, + }) + + local headerMode = self:_getHeaderMode() + if headerMode == StackHeaderMode.Screen then + --[[ + This ref is used to change the scene container size whenever the header changes height. + It's not too expensive to create one every time this thing is rendered, and since it's + not being set on the actual scene element then it won't trigger a bunch of reconciling + beyond this immediate layer. Its lifetime is the same as the onSizeChanged callback. + ]] + local sceneWrapperRef = Roact.createRef() + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + }, { + screenHeader = Roact.createElement(ContentHeightFitFrame, { + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + onHeightChanged = function(height) + local sceneWrapper = sceneWrapperRef.current + if self._isMounted and sceneWrapper then + sceneWrapper.Position = UDim2.new(0, 0, 0, height) + sceneWrapper.Size = UDim2.new(1, 0, 1, -height) + end + end + }, { + headerContent = self:_renderHeader(scene, headerMode) + }), + sceneWrapper = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + [Roact.Ref] = sceneWrapperRef, + }, { + scene = sceneElement, + }) + }) + else + return sceneElement + end +end + +function StackViewLayout:render() + local headerMode = self:_getHeaderMode() + local transitionProps = self.props.transitionProps + local topMostOpaqueSceneIndex = self.state.topMostOpaqueSceneIndex + local scenes = transitionProps.scenes + local floatingHeader = nil + + if headerMode == StackHeaderMode.Float then + local scene = transitionProps.scene + floatingHeader = Roact.createElement(ContentHeightFitFrame, { + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + onHeightChanged = function(...) + self:_onFloatingHeaderHeightChanged(...) + end + }, { + headerContent = self:_renderHeader(scene, headerMode) + }) + end + + local renderedScenes = Cryo.List.map(scenes, function(scene, idx) + -- The card is obscured if: + -- It's not the active card (e.g. we're transitioning TO it). + -- It's hidden underneath an opaque card that is NOT currently transitioning. + -- It's completely off-screen. + local cardObscured = idx < topMostOpaqueSceneIndex and not scene.isActive + + local navigationOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {}) + local overlayColor3 = navigationOptions.overlayColor3 + + -- Each scene gets its own overlay frame whose transparency must be managed. + local overlayFrameRef = self._overlayFrameRefs[idx] + if not overlayFrameRef then + overlayFrameRef = Roact.createRef() + self._overlayFrameRefs[idx] = overlayFrameRef + end + + -- Wrap all cards in a TextButton so we can control hidden state and ZIndex without bleeding props. + -- This button also provides the card's overlay effect when required and prevents pass-through of + -- stray touches. + return Roact.createElement("TextButton", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = overlayColor3, + BackgroundTransparency = calculateFadeTransparency(scene, idx, self._positionLastValue), + AutoButtonColor = false, + BorderSizePixel = 0, + Text = " ", + ZIndex = idx, + Visible = not cardObscured, + [Roact.Ref] = overlayFrameRef, + }, { + -- Cards need to have unique keys so that instances of the same components are not + -- reused for different scenes. (Could lead to unanticipated lifecycle problems). + ["card_" .. scene.key] = self:_renderCard(scene, idx), + }) + end) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + }, { + floatingHeader = floatingHeader or nil, + scenesContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), -- Overridden by _onFloatingHeaderHeightChanged + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + [Roact.Ref] = self._scenesContainerRef, + }, renderedScenes), + }) +end + +function StackViewLayout.getDerivedStateFromProps(nextProps, lastState) + local transitionProps = nextProps.transitionProps + local scenes = transitionProps.scenes + local state = transitionProps.navigation.state + local isTransitioning = state.isTransitioning + local topMostIndex = #scenes + + local isOverlayMode = nextProps.mode == StackPresentationStyle.Modal or + nextProps.mode == StackPresentationStyle.Overlay + + -- Find the last opaque scene in a modal stack so that we can optimize rendering. + local topMostOpaqueSceneIndex = 0 + if isOverlayMode then + for idx = topMostIndex, 1, -1 do + local scene = scenes[idx] + local navigationOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {}) + + -- Card covers other pages if it's not an overlay and it's not the top-most index while transitioning. + if not navigationOptions.overlayEnabled and not (isTransitioning and idx == topMostIndex) then + topMostOpaqueSceneIndex = idx + break + end + end + else + for idx = topMostIndex, 1, -1 do + if not (isTransitioning and idx == topMostIndex) then + topMostOpaqueSceneIndex = idx + break + end + end + end + + return { + topMostOpaqueSceneIndex = topMostOpaqueSceneIndex, + transitionConfig = StackViewTransitionConfigs.getTransitionConfig( + nextProps.transitionConfig, + nextProps.transitionProps, + nextProps.lastTransitionProps, + nextProps.mode), + } +end + +function StackViewLayout:didMount() + self._isMounted = true + + self._positionDisconnector = self.props.transitionProps.position:onStep(function(...) + self:_onPositionStep(...) + end) +end + +function StackViewLayout:willUnmount() + self._isMounted = false + + if self._positionDisconnector then + self._positionDisconnector() + self._positionDisconnector = nil + end +end + +function StackViewLayout:didUpdate(oldProps) + local position = self.props.transitionProps.position + + if position ~= oldProps.transitionProps.position then + self._positionDisconnector() + self._positionDisconnector = position:onStep(function(...) + self:_onPositionStep(...) + end) + end +end + +function StackViewLayout:_onPositionStep(value) + if self._isMounted then + local transitionProps = self.props.transitionProps + local scenes = transitionProps.scenes + + for idx, scene in ipairs(scenes) do + local frameRef = self._overlayFrameRefs[idx] + local frameInstance = frameRef and frameRef.current + + if frameInstance then + frameInstance.BackgroundTransparency = calculateFadeTransparency(scene, idx, value) + end + end + + self._positionLastValue = value + end +end + +return StackViewLayout diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewTransitionConfigs.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewTransitionConfigs.lua new file mode 100644 index 0000000..83e7a33 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewTransitionConfigs.lua @@ -0,0 +1,53 @@ +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) +local StackViewInterpolator = require(script.Parent.StackViewInterpolator) +local StackPresentationStyle = require(script.Parent.StackPresentationStyle) + +local DefaultTransitionSpec = { + frequency = 3, -- Hz + dampingRatio = 1, +} + +local SlideFromRight = { + transitionSpec = DefaultTransitionSpec, + screenInterpolator = StackViewInterpolator.forHorizontal, +} + +local ModalSlideFromBottom = { + transitionSpec = DefaultTransitionSpec, + screenInterpolator = StackViewInterpolator.forVertical, +} + +local FadeInPlace = { + transitionSpec = DefaultTransitionSpec, + screenInterpolator = StackViewInterpolator.forFade, +} + +local function getDefaultTransitionConfig(transitionProps, prevTransitionProps, presentationStyle) + if presentationStyle == StackPresentationStyle.Modal then + return ModalSlideFromBottom + elseif presentationStyle == StackPresentationStyle.Overlay then + return FadeInPlace + else + return SlideFromRight + end +end + +local function getTransitionConfig(transitionConfigurer, transitionProps, prevTransitionProps, presentationStyle) + local defaultConfig = getDefaultTransitionConfig(transitionProps, prevTransitionProps, presentationStyle) + if transitionConfigurer then + return Cryo.Dictionary.join( + defaultConfig, + transitionConfigurer(transitionProps, prevTransitionProps, presentationStyle) + ) + end + + return defaultConfig +end + +return { + getDefaultTransitionConfig = getDefaultTransitionConfig, + getTransitionConfig = getTransitionConfig, + SlideFromRight = SlideFromRight, + ModalSlideFromBottom = ModalSlideFromBottom, + FadeInPlace = FadeInPlace, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackHeaderMode.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackHeaderMode.spec.lua new file mode 100644 index 0000000..49ec372 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackHeaderMode.spec.lua @@ -0,0 +1,17 @@ +return function() + local StackHeaderMode = require(script.Parent.Parent.StackHeaderMode) + + describe("StackMode token tests", function() + it("should return same object for each token for multiple calls", function() + expect(StackHeaderMode.None).to.equal(StackHeaderMode.None) + expect(StackHeaderMode.Float).to.equal(StackHeaderMode.Float) + expect(StackHeaderMode.Screen).to.equal(StackHeaderMode.Screen) + end) + + it("should return matching string names for symbols", function() + expect(tostring(StackHeaderMode.None)).to.equal("NONE") + expect(tostring(StackHeaderMode.Float)).to.equal("FLOAT") + expect(tostring(StackHeaderMode.Screen)).to.equal("SCREEN") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackPresentationStyle.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackPresentationStyle.spec.lua new file mode 100644 index 0000000..0a64c09 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackPresentationStyle.spec.lua @@ -0,0 +1,17 @@ +return function() + local StackPresentationStyle = require(script.Parent.Parent.StackPresentationStyle) + + describe("StackPresentationStyle token tests", function() + it("should return same object for each token for multiple calls", function() + expect(StackPresentationStyle.Default).to.equal(StackPresentationStyle.Default) + expect(StackPresentationStyle.Modal).to.equal(StackPresentationStyle.Modal) + expect(StackPresentationStyle.Overlay).to.equal(StackPresentationStyle.Overlay) + end) + + it("should return matching string names for symbols", function() + expect(tostring(StackPresentationStyle.Default)).to.equal("DEFAULT") + expect(tostring(StackPresentationStyle.Modal)).to.equal("MODAL") + expect(tostring(StackPresentationStyle.Overlay)).to.equal("OVERLAY") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackView.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackView.spec.lua new file mode 100644 index 0000000..cd1a24e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackView.spec.lua @@ -0,0 +1,8 @@ +return function() + -- local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + -- local StackView = require(script.Parent.Parent.StackView) + + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewCard.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewCard.spec.lua new file mode 100644 index 0000000..1b3e68f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewCard.spec.lua @@ -0,0 +1,36 @@ +return function() + local Otter = require(script.Parent.Parent.Parent.Parent.Parent.Otter) + local Roact = require(script.Parent.Parent.Parent.Parent.Parent.Roact) + local StackViewCard = require(script.Parent.Parent.StackViewCard) + + it("should mount its renderProp and pass it scene", function() + local didRender = false + local testScene = { + isActive = true, + index = 1, + } + + local renderedScene = nil + local element = Roact.createElement(StackViewCard, { + renderScene = function(theScene) + renderedScene = theScene + return Roact.createElement(function() + didRender = true -- verifies component is attached to tree + end) + end, + scene = testScene, + position = Otter.createSingleMotor(1), + navigation = { + state = { + index = 1, + } + } + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(renderedScene).to.equal(testScene) + expect(didRender).to.equal(true) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewInterpolator.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewInterpolator.spec.lua new file mode 100644 index 0000000..b3092a6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewInterpolator.spec.lua @@ -0,0 +1,7 @@ +return function() + local StackViewInterpolator = require(script.Parent.Parent.StackViewInterpolator) + + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewLayout.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewLayout.spec.lua new file mode 100644 index 0000000..9882e6d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewLayout.spec.lua @@ -0,0 +1,8 @@ +return function() + -- local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + -- local StackViewLayout = require(script.Parent.Parent.StackViewLayout) + + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewTransitionConfigs.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewTransitionConfigs.spec.lua new file mode 100644 index 0000000..46e4e1e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewTransitionConfigs.spec.lua @@ -0,0 +1,5 @@ +return function() + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/SwitchView.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/SwitchView.lua new file mode 100644 index 0000000..6602cc7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/SwitchView.lua @@ -0,0 +1,21 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local SceneView = require(script.Parent.SceneView) + +local SwitchView = Roact.Component:extend("SwitchView") + +function SwitchView:render() + local navState = self.props.navigation.state + local screenProps = self.props.screenProps + + local activeKey = navState.routes[navState.index].key + local descriptor = self.props.descriptors[activeKey] + local childComponent = descriptor.getComponent() + + return Roact.createElement(SceneView, { + component = childComponent, + navigation = descriptor.navigation, + screenProps = screenProps, + }) +end + +return SwitchView diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBar.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBar.lua new file mode 100644 index 0000000..195a236 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBar.lua @@ -0,0 +1,222 @@ +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) +local TopBarBackButton = require(script.Parent.TopBarBackButton) +local TopBarTitleContainer = require(script.Parent.TopBarTitleContainer) +local isValidRoactElementType = require(script.Parent.Parent.Parent.utils.isValidRoactElementType) +local StackHeaderMode = require(script.Parent.Parent.StackView.StackHeaderMode) +local validate = require(script.Parent.Parent.Parent.utils.validate) + +local TopBar = Roact.Component:extend("TopBar") + +local DEFAULT_HEIGHT = 56 +local DEFAULT_LEFT_WIDTH = UDim.new(0.3, 0) +local DEFAULT_CENTER_WIDTH = UDim.new(0.4, 0) +local DEFAULT_RIGHT_WIDTH = UDim.new(0.3, 0) + +function TopBar:_getTopBarTitleString(scene) + local options = scene.descriptor.options + if type(options.headerTitle) == "string" then + return options.headerTitle + end + if options.title and type(options.title) ~= "string" then + error("Invalid title for route" .. scene.route.routeName .. + " - title must be string or nil, instead it was of type " .. type(options.title)) + end + return options.title +end + +--[[ + Render either the provided headerTitleContainer component or our TopBarTitleContainer component +]] +function TopBar:_renderTitleComponent(props) + + local options = props.scene.descriptor.options + local headerSubtitle = options.headerSubtitle + local headerTitleContainerStyle = options.headerTitleContainerStyle or {} + + local renderHeaderTitle = options.renderHeaderTitle + local renderHeaderSubtitle = options.renderHeaderSubtitle + + local titleString = self:_getTopBarTitleString(props.scene) + local headerTitleStyle = options.headerTitleStyle + local headerSubtitleStyle = options.headerSubtitleStyle + + local renderHeaderTitleContainer = options.renderHeaderTitleContainer + + headerTitleContainerStyle = Cryo.Dictionary.join({ + Size = UDim2.new(DEFAULT_CENTER_WIDTH, UDim.new(1, 0)) + }, headerTitleContainerStyle) + + if renderHeaderTitleContainer then + return Roact.createElement(renderHeaderTitleContainer, { + headerTitleContainerStyle = headerTitleContainerStyle, + headerTitleStyle = headerTitleStyle, + headerSubtitleStyle = headerSubtitleStyle, + headerTitle = titleString, + headerSubtitle = headerSubtitle, + renderHeaderTitle = renderHeaderTitle, + renderHeaderSubtitle = renderHeaderSubtitle, + }) + end + + return Roact.createElement(TopBarTitleContainer, { + headerTitleContainerStyle = headerTitleContainerStyle, + headerTitleStyle = headerTitleStyle, + headerSubtitleStyle = headerSubtitleStyle, + headerTitle = titleString, + headerSubtitle = headerSubtitle, + renderHeaderTitle = renderHeaderTitle, + renderHeaderSubtitle = renderHeaderSubtitle, + }) +end + +function TopBar:_renderLeftComponent(props) + local options = props.scene.descriptor.options + + local renderHeaderLeftContainer = options.renderHeaderLeftContainer + local headerLeftContainerStyle = options.headerLeftContainerStyle or {} + local renderHeaderBackButton = options.renderHeaderBackButton + local headerBackButtonStyle = options.headerBackButtonStyle + + headerLeftContainerStyle = Cryo.Dictionary.join({ + Size = UDim2.new(DEFAULT_LEFT_WIDTH, UDim.new(1, 0)), + }, headerLeftContainerStyle) + + local function goBack() + props.scene.descriptor.navigation.goBack(props.scene.descriptor.key) + end + + if renderHeaderLeftContainer then + return Roact.createElement(renderHeaderLeftContainer, { + goBack = goBack, + headerLeftContainerStyle = headerLeftContainerStyle, + renderHeaderBackButton = renderHeaderBackButton, + headerBackButtonStyle = headerBackButtonStyle, + + }) + end + + -- Don't display anything if there's no page to go back to, by default + if props.scene.index == 1 then + return Roact.createElement("Frame", { + Size = UDim2.new(DEFAULT_LEFT_WIDTH, UDim.new(1, 0)), + BackgroundTransparency = 1, + }) + end + + return Roact.createElement(TopBarBackButton, { + goBack = goBack, + headerLeftContainerStyle = headerLeftContainerStyle, + renderHeaderBackButton = renderHeaderBackButton, + headerBackButtonStyle = headerBackButtonStyle, + }) +end + +function TopBar:_renderRightComponent(props) + local renderHeaderRight = props.scene.descriptor.options.renderHeaderRight + if renderHeaderRight then + return Roact.createElement(props.scene.descriptor.options.renderHeaderRight) + end + return Roact.createElement("Frame", { + Size = UDim2.new(DEFAULT_RIGHT_WIDTH, UDim.new(1, 0)), + BackgroundTransparency = 1, + LayoutOrder = 3, + }) +end + +function TopBar:_renderHeader(props) + local options = props.scene.descriptor.options + local left = self:_renderLeftComponent(props) + local center = self:_renderTitleComponent(props) + local right = self:_renderRightComponent(props) + local headerStyle = options.headerStyle or {} + local DEFAULT_HEADER_STYLE = { + Size = UDim2.new(1, 0, 0, DEFAULT_HEIGHT) + } + + headerStyle = Cryo.Dictionary.join(DEFAULT_HEADER_STYLE, headerStyle) + return Roact.createElement("Frame", headerStyle, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + left = left, + center = center, + right = right, + }) +end + +function TopBar:render() + local appBar + local mode = self.props.mode + local scene = self.props.scene + + if mode == StackHeaderMode.Float then + local scenesByIndex = {} + for _, s in ipairs(self.props.scenes) do + scenesByIndex[scene.index] = s + end + -- For each scene, create its props + local scenesProps = Cryo.List.map(Cryo.Dictionary.values(scenesByIndex), function(s, index) + return { + position = self.props.position, + scene = s + } + end) + appBar = Cryo.List.map(scenesProps, self._renderHeader) + error("TODO: implement support for Float") + else + local headerProps = { + scene = self.props.scene, + } + appBar = self:_renderHeader(headerProps) + end + return appBar +end + +local function validateProps(props) + local options = props.scene.descriptor.options + local headerTitle = options.headerTitle + local renderHeaderTitle = options.renderHeaderTitle + + local headerSubtitle = options.headerSubtitle + local renderHeaderSubtitle = options.renderHeaderSubtitle + + local renderHeaderTitleContainer = options.renderHeaderTitleContainer + local renderHeaderRight = options.renderHeaderRight + local renderLeftContainer = options.renderLeftContainer + local renderHeaderBackButton = options.renderHeaderBackButton + + validate(not (headerTitle and renderHeaderTitle), "You must not specify both headerTitle and renderHeaderTitle.") + validate(not (headerSubtitle and renderHeaderSubtitle), + "You must not specify both headerSubtitle and renderHeaderSubtitle.") + + if renderHeaderTitle then + validate(isValidRoactElementType(renderHeaderTitle), "renderHeaderTitle must be a valid Roact element type.") + end + if renderHeaderSubtitle then + validate(isValidRoactElementType(renderHeaderSubtitle), "renderHeaderSubtitle must be a valid Roact element type.") + end + if renderHeaderTitleContainer then + validate(isValidRoactElementType(renderHeaderTitleContainer), + "renderHeaderTitleContainer must be a valid Roact element type.") + end + if renderHeaderRight then + validate(isValidRoactElementType(renderHeaderRight), "renderHeaderRight must be a valid Roact element type.") + end + if renderLeftContainer then + validate(isValidRoactElementType(renderLeftContainer), "renderLeftContainer must be a valid Roact element type.") + end + if renderHeaderBackButton then + validate(isValidRoactElementType(renderHeaderBackButton), + "renderHeaderBackButton must be a valid Roact element type.") + end +end + +function TopBar.getDerivedStateFromProps(nextProps) + validateProps(nextProps) + return {} +end + +return TopBar \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBarBackButton.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBarBackButton.lua new file mode 100644 index 0000000..334e2e4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBarBackButton.lua @@ -0,0 +1,74 @@ +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) + +local TopBarBackButton = Roact.PureComponent:extend("TopBarBackButton") + +--[[ + Here rather than in defaultProps to allow one to overwrite specific parts of each style +]] +local DEFAULT_STYLES = { + headerLeftContainerStyle = { + Size = UDim2.new(0.3, 0, 1, 0), + BackgroundTransparency = 1, + }, + headerBackButtonStyle = { + Text = "<-", + LayoutOrder = 2, + Size = UDim2.new(0, 40, 0, 40), + }, +} + +TopBarBackButton.defaultProps = { + headerBackButtonStyle = {}, + headerLeftContainerStyle = {}, + goBack = function() end, +} + +function TopBarBackButton:_renderBackButton() + local renderHeaderBackButton = self.props.renderHeaderBackButton + local headerBackButtonStyle = self.props.headerBackButtonStyle + local goBack = self.props.goBack + + if renderHeaderBackButton then + headerBackButtonStyle = Cryo.Dictionary.join(headerBackButtonStyle, { + LayoutOrder = 2, + }) + return Roact.createElement(renderHeaderBackButton, { + goBack = goBack, + headerBackButtonStyle = headerBackButtonStyle, + }) + end + + headerBackButtonStyle = Cryo.Dictionary.join( + DEFAULT_STYLES.headerBackButtonStyle, + headerBackButtonStyle, + { + [Roact.Event.Activated] = goBack, + } + ) + + return Roact.createElement("TextButton", headerBackButtonStyle) +end + +function TopBarBackButton:render() + local headerLeftContainerStyle = self.props.headerLeftContainerStyle + headerLeftContainerStyle = Cryo.Dictionary.join(DEFAULT_STYLES.headerLeftContainerStyle, headerLeftContainerStyle, { + LayoutOrder = 1, + }) + return Roact.createElement("Frame", headerLeftContainerStyle, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + }), + Spacer = Roact.createElement("Frame", { + Size = UDim2.new(0, 12, 1, 0), + BackgroundTransparency = 1, + LayoutOrder = 1, + }), + Button = self:_renderBackButton(), + }) +end + +return TopBarBackButton \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBarTitleContainer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBarTitleContainer.lua new file mode 100644 index 0000000..d03a0a4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBarTitleContainer.lua @@ -0,0 +1,137 @@ +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) + +local TopBarTitleContainer = Roact.PureComponent:extend("TopBarTitleContainer") + +local DEFAULT_STYLES = { + headerTitleContainerStyle = { + Size = UDim2.new(1, 0, 1, 0), + LayoutOrder = 2, + BackgroundTransparency = 1, + }, + headerTitleStyle = { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0.6, 0), + LayoutOrder = 1, + TextSize = 28, + Text = "Title", + TextColor3 = Color3.fromRGB(0, 0, 0), + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + }, + headerSubtitleStyle = { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0.2, 0), + LayoutOrder = 2, + TextSize = 14, + Text = "Subtitle", + TextColor3 = Color3.fromRGB(0, 0, 0), + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + } +} + +TopBarTitleContainer.defaultProps = { + headerTitleContainerStyle = {}, + headerTitleStyle = {}, + headerSubtitleStyle = {}, +} + +--[[ + Returns a Roact element that holds the title and subtitle elements +]] +function TopBarTitleContainer:_renderContainer(children) + local renderHeaderTitleContainer = self.props.renderHeaderTitleContainer + local headerTitle = self.props.headerTitle + local headerTitleStyle = self.props.headerTitleStyle + local headerSubtitle = self.props.headerSubtitle + local headerSubtitleStyle = self.props.headerSubtitleStyle + local headerTitleContainerStyle = self.props.headerTitleContainerStyle + + headerTitleContainerStyle = Cryo.Dictionary.join( + DEFAULT_STYLES.headerTitleContainerStyle, + headerTitleContainerStyle + ) + if renderHeaderTitleContainer then + return Roact.createElement(renderHeaderTitleContainer, { + headerTitleContainerStyle = headerTitleContainerStyle, + headerTitle = headerTitle, + headerTitleStyle = headerTitleStyle, + headerSubtitle = headerSubtitle, + headerSubtitleStyle = headerSubtitleStyle, + renderHeaderTitle = children["Title"], + renderHeaderSubtitle = children["Subtitle"], + }) + end + children["Layout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + }) + return Roact.createElement("Frame", headerTitleContainerStyle, children) + +end + +--[[ + Returns a Roact element representing the title +]] +function TopBarTitleContainer:_renderTitle() + local headerTitle = self.props.headerTitle + local renderHeaderTitle = self.props.renderHeaderTitle + local headerTitleStyle = self.props.headerTitleStyle + + if renderHeaderTitle then + return Roact.createElement(renderHeaderTitle, { + headerTitle = headerTitle, + headerTitleStyle = headerTitleStyle, + }) + end + + headerTitleStyle = Cryo.Dictionary.join( + DEFAULT_STYLES.headerTitleStyle, + { + Text = headerTitle, + }, + headerTitleStyle) + return Roact.createElement("TextLabel", headerTitleStyle) +end + +--[[ + Returns a Roact element representing the subtitle +]] +function TopBarTitleContainer:_renderSubtitle() + local headerSubtitle = self.props.headerSubtitle + local renderHeaderSubtitle = self.props.renderHeaderSubtitle + local headerSubtitleStyle = self.props.headerSubtitleStyle + + if not (headerSubtitle or renderHeaderSubtitle) then + return nil + end + + if renderHeaderSubtitle then + return Roact.createElement(renderHeaderSubtitle, { + headerSubtitle = headerSubtitle, + headerSubtitleStyle = headerSubtitleStyle, + }) + end + + headerSubtitleStyle = Cryo.Dictionary.join( + DEFAULT_STYLES.headerSubtitleStyle, + headerSubtitleStyle, + { + Text = headerSubtitle, + } + ) + return Roact.createElement("TextLabel", headerSubtitleStyle) +end + +function TopBarTitleContainer:render() + local children = { + Title = self:_renderTitle(), + Subtitle = self:_renderSubtitle(), + } + return self:_renderContainer(children) +end + +return TopBarTitleContainer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBar.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBar.spec.lua new file mode 100644 index 0000000..d0d08ae --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBar.spec.lua @@ -0,0 +1,364 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Parent.Roact) + local TopBar = require(script.Parent.Parent.TopBar) + + it("should mount and unmount without issue", function() + local scene = { + descriptor = { + options = { + headerTitle = "HOME", + }, + navigation = { + goBack = function(key) + print(key) + end, + }, + key = "aScene" + }, + index = 1, + } + local props = { + scenes = { + scene, + }, + scene = scene, + } + + local instance = Roact.mount(Roact.createElement(TopBar, props)) + Roact.unmount(instance) + end) + + itSKIP("should detect when to display the back button", function() + local scene = { + descriptor = { + options = { + headerTitle = "HOME", + }, + navigation = { + goBack = function(key) + print(key) + end, + }, + key = "aScene" + }, + index = 2, + } + local props = { + scenes = { + scene, + }, + scene = scene, + } + + local topBar = Roact.createElement(TopBar, props) + local frame = Instance.new("Frame") + local handle = Roact.mount(topBar, frame) + + local Button = frame:FindFirstChild("Button", true) + + expect(Button).to.be.ok() + + Roact.unmount(handle) + frame:Destroy() + end) + + itSKIP("should detect when NOT to display the back button", function() + local scene = { + descriptor = { + options = { + headerTitle = "HOME", + }, + navigation = { + goBack = function(key) + print(key) + end, + }, + key = "aScene" + }, + index = 1, + } + local props = { + scenes = { + scene, + }, + scene = scene, + } + + local topBar = Roact.createElement(TopBar, props) + local frame = Instance.new("Frame") + local handle = Roact.mount(topBar, frame) + + local Button = frame:FindFirstChild("Button", true) + + expect(Button).to.never.be.ok() + + Roact.unmount(handle) + frame:Destroy() + end) + + it("should throw if we provide the wrong props", function() + local function renderAndMountAndCleanup(component, props) + local handle = Roact.mount(Roact.createElement(component, props)) + Roact.unomunt(handle) + end + + local scene1 = { + descriptor = { + options = { + headerTitle = "Throw Title!", + renderHeaderTitle = function() + return Roact.createElement("TextLabel") + end, + }, + navigation = { + goBack = function(key) + print(key) + end, + }, + key = "aScene" + }, + index = 2, + } + local props1 = { + scenes = { + scene1, + }, + scene = scene1, + } + expect(function() + renderAndMountAndCleanup(TopBar, props1) + end).to.throw() + local scene2 = { + descriptor = { + options = { + headerSubtitle = "Throw Subtitle!", + renderHeaderSubtitle = function() + return Roact.createElement("TextLabel") + end, + }, + navigation = { + goBack = function(key) + print(key) + end, + }, + key = "aScene" + }, + index = 2, + } + local props2 = { + scenes = { + scene2, + }, + scene = scene2, + } + expect(function() + renderAndMountAndCleanup(TopBar, props2) + end).to.throw() + local scene3 = { + descriptor = { + options = { + renderHeaderSubtitle = 38139, + }, + navigation = { + goBack = function(key) + print(key) + end, + }, + key = "aScene" + }, + index = 2, + } + local props3 = { + scenes = { + scene3, + }, + scene = scene3, + } + expect(function() + renderAndMountAndCleanup(TopBar, props3) + end).to.throw() + end) + + itSKIP("should properly pass on the options to the default components", function() + local testHeaderBackgroundColor = Color3.fromRGB(200, 200, 200) + local testTitle = "Title!" + local testSubtitle = "Subtitle!" + local testTitleSize = 55 + local testSubtitleSize = 11 + local testTitleContainerColor = Color3.fromRGB(144, 124, 123) + local testLeftContainerColor = Color3.fromRGB(23, 235, 244) + + local scene = { + descriptor = { + options = { + headerTitle = testTitle, + headerSubtitle = testSubtitle, + headerStyle = { + BackgroundColor3 = testHeaderBackgroundColor + }, + headerTitleStyle = { + TextSize = testTitleSize, + }, + headerSubtitleStyle = { + TextSize = testSubtitleSize, + }, + headerTitleContainerStyle = { + BackgroundColor3 = testTitleContainerColor, + }, + headerLeftContainerStyle = { + BackgroundColor3 = testLeftContainerColor, + } + }, + navigation = { + goBack = function(key) + print(key) + end, + }, + key = "aScene" + }, + index = 2, + } + local props = { + scenes = { + scene, + }, + scene = scene, + } + + local topBar = Roact.createElement(TopBar, props) + local frame = Instance.new("Frame") + local handle = Roact.mount(topBar, frame) + + local TopBarOutermost = frame:FindFirstChildWhichIsA("Frame", true) + local Title = frame:FindFirstChild("Title", true) + local Subtitle = frame:FindFirstChild("Subtitle", true) + local LeftContainer = frame:FindFirstChild("left", true) + local CenterContainer = frame:FindFirstChild("center", true) + + expect(TopBarOutermost.BackgroundColor3).to.equal(testHeaderBackgroundColor) + expect(Title.Text).to.equal(testTitle) + expect(Subtitle.Text).to.equal(testSubtitle) + expect(Title.TextSize).to.equal(testTitleSize) + expect(Subtitle.TextSize).to.equal(testSubtitleSize) + expect(LeftContainer.BackgroundColor3).to.equal(testLeftContainerColor) + expect(CenterContainer.BackgroundColor3).to.equal(testTitleContainerColor) + + Roact.unmount(handle) + frame:Destroy() + end) + + itSKIP("should accept render props and inject props", function() + local testHeaderBackgroundColor = Color3.fromRGB(200, 200, 200) + local testTitle = "Title!" + local testSubtitle = "Subtitle!" + local testTitleSize = 55 + local testSubtitleSize = 11 + local testTitleContainerColor = Color3.fromRGB(144, 124, 123) + local testLeftContainerColor = Color3.fromRGB(23, 235, 244) + local testRightContainerColor = Color3.fromRGB(233, 0, 2) + local testKey = "testKey" + + local scene = { + descriptor = { + options = { + renderHeaderTitle = function(props) + return Roact.createElement("TextLabel", { + Text = props.headerTitle, + TextSize = props.headerTitleStyle.TextSize, + }) + end, + renderHeaderSubtitle = function(props) + return Roact.createElement("TextLabel", { + Text = props.headerSubtitle, + TextSize = props.headerSubtitleStyle.TextSize, + }) + end, + renderHeaderTitleContainer = function(props) + return Roact.createElement("Frame", { + BackgroundColor3 = props.headerTitleContainerStyle.BackgroundColor3, + }, { + Title = Roact.createElement(props.renderHeaderTitle, { + headerTitleStyle = props.headerTitleStyle, + headerTitle = testTitle + }), + Subtitle = Roact.createElement(props.renderHeaderSubtitle, { + headerSubtitleStyle = props.headerSubtitleStyle, + headerSubtitle = testSubtitle + }), + }) + end, + renderHeaderRight = function() + return Roact.createElement("Frame", { + BackgroundColor3 = testRightContainerColor + }) + end, + renderHeaderLeftContainer = function(props) + return Roact.createElement("Frame", { + BackgroundColor3 = props.headerLeftContainerStyle.BackgroundColor3, + }, { + Button = Roact.createElement(props.renderHeaderBackButton, { + goBack = props.goBack, + }), + }) + end, + renderHeaderBackButton = function(props) + return Roact.createElement("ImageButton", { + [Roact.Event.Activated] = props.goBack, + }) + end, + headerStyle = { + BackgroundColor3 = testHeaderBackgroundColor + }, + headerTitleStyle = { + TextSize = testTitleSize, + }, + headerSubtitleStyle = { + TextSize = testSubtitleSize, + }, + headerTitleContainerStyle = { + BackgroundColor3 = testTitleContainerColor, + }, + headerLeftContainerStyle = { + BackgroundColor3 = testLeftContainerColor, + } + }, + navigation = { + goBack = function(key) + return key + end, + }, + key = testKey + }, + index = 2, + } + local props = { + scenes = { + scene, + }, + scene = scene, + } + + local topBar = Roact.createElement(TopBar, props) + local frame = Instance.new("Frame") + local handle = Roact.mount(topBar, frame) + + local TopBarOutermost = frame:FindFirstChildWhichIsA("Frame", true) + local Title = frame:FindFirstChild("Title", true) + local Subtitle = frame:FindFirstChild("Subtitle", true) + local LeftContainer = frame:FindFirstChild("left", true) + local CenterContainer = frame:FindFirstChild("center", true) + local RightContainer = frame:FindFirstChild("right", true) + + expect(TopBarOutermost.BackgroundColor3).to.equal(testHeaderBackgroundColor) + expect(Title.Text).to.equal(testTitle) + expect(Subtitle.Text).to.equal(testSubtitle) + expect(Title.TextSize).to.equal(testTitleSize) + expect(Subtitle.TextSize).to.equal(testSubtitleSize) + expect(LeftContainer.BackgroundColor3).to.equal(testLeftContainerColor) + expect(CenterContainer.BackgroundColor3).to.equal(testTitleContainerColor) + expect(RightContainer.BackgroundColor3).to.equal(testRightContainerColor) + + Roact.unmount(handle) + frame:Destroy() + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBarBackButton.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBarBackButton.spec.lua new file mode 100644 index 0000000..4436010 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBarBackButton.spec.lua @@ -0,0 +1,36 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Parent.Roact) + local TopBarBackButton = require(script.Parent.Parent.TopBarBackButton) + + it("should mount and unmount without issue", function() + local instance = Roact.mount(Roact.createElement(TopBarBackButton)) + Roact.unmount(instance) + end) + + itSKIP("should accept render props and inject them with props", function() + local testText = "This is some test text!" + local props = { + renderHeaderBackButton = function(props) + local headerBackButtonStyle = props.headerBackButtonStyle + return Roact.createElement("TextButton", { + Text = headerBackButtonStyle.Text + }) + end, + headerBackButtonStyle = { + Text = testText, + } + } + + local backButton = Roact.createElement(TopBarBackButton, props) + local frame = Instance.new("Frame") + local handle = Roact.mount(backButton, frame) + + local textButton = frame:FindFirstChildWhichIsA("TextButton", true) + local text = textButton.Text + expect(text).to.equal(testText) + + Roact.unmount(handle) + frame:Destroy() + end) + +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBarTitleContainer.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBarTitleContainer.spec.lua new file mode 100644 index 0000000..524d70b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBarTitleContainer.spec.lua @@ -0,0 +1,98 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Parent.Roact) + local TopBarTitleContainer = require(script.Parent.Parent.TopBarTitleContainer) + + it("should mount and unmount without issue", function() + local instance = Roact.mount(Roact.createElement(TopBarTitleContainer)) + Roact.unmount(instance) + end) + + itSKIP("should inject the default with the provided style", function() + local testTitle = "Test title!" + local testSubtitle = "Test subtitle!" + local testTitleSize = 55 + local testSubtitleSize = 11 + local props = { + headerTitleStyle = { + TextSize = testTitleSize + }, + headerSubtitleStyle = { + TextSize = testSubtitleSize, + }, + headerTitle = testTitle, + headerSubtitle = testSubtitle, + } + + local container = Roact.createElement(TopBarTitleContainer, props) + local frame = Instance.new("Frame") + local handle = Roact.mount(container, frame) + + local Title = frame:FindFirstChild("Title", true) + local Subtitle = frame:FindFirstChild("Subtitle", true) + local titleText = Title.Text + local subtitleText = Subtitle.Text + local titleSize = Title.TextSize + local subtitleSize = Subtitle.TextSize + + expect(titleText).to.equal(testTitle) + expect(subtitleText).to.equal(testSubtitle) + expect(titleSize).to.equal(testTitleSize) + expect(subtitleSize).to.equal(testSubtitleSize) + + Roact.unmount(handle) + frame:Destroy() + end) + + itSKIP("should accept render props and inject them with props", function() + local testTitle = "Test title!" + local testSubtitle = "Test subtitle!" + local testTitleSize = 55 + local testSubtitleSize = 11 + local props = { + renderHeaderTitle = function(props) + local headerTitle = props.headerTitle + local headerTitleStyle = props.headerTitleStyle + return Roact.createElement("TextLabel", { + Text = headerTitle, + TextSize = headerTitleStyle.TextSize, + }) + end, + renderHeaderSubtitle = function(props) + local headerSubtitle = props.headerSubtitle + local headerSubtitleStyle = props.headerSubtitleStyle + return Roact.createElement("TextLabel", { + Text = headerSubtitle, + TextSize = headerSubtitleStyle.TextSize, + }) + end, + headerTitleStyle = { + TextSize = testTitleSize + }, + headerSubtitleStyle = { + TextSize = testSubtitleSize, + }, + headerTitle = testTitle, + headerSubtitle = testSubtitle, + } + + local container = Roact.createElement(TopBarTitleContainer, props) + local frame = Instance.new("Frame") + local handle = Roact.mount(container, frame) + + local Title = frame:FindFirstChild("Title", true) + local Subtitle = frame:FindFirstChild("Subtitle", true) + local titleText = Title.Text + local subtitleText = Subtitle.Text + local titleSize = Title.TextSize + local subtitleSize = Subtitle.TextSize + + expect(titleText).to.equal(testTitle) + expect(subtitleText).to.equal(testSubtitle) + expect(titleSize).to.equal(testTitleSize) + expect(subtitleSize).to.equal(testSubtitleSize) + + Roact.unmount(handle) + frame:Destroy() + end) + +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/Transitioner.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/Transitioner.lua new file mode 100644 index 0000000..a8a5bd7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/Transitioner.lua @@ -0,0 +1,298 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Roact) +local Otter = require(script.Parent.Parent.Parent.Otter) +local ScenesReducer = require(script.Parent.ScenesReducer) +local validate = require(script.Parent.Parent.utils.validate) + +local DEFAULT_TRANSITION_SPEC = { + frequency = 4 -- Hz +} + +local function buildTransitionProps(props, state) + local navigation = props.navigation + local options = props.options + + local layout = state.layout + local position = state.position + local scenes = state.scenes + + local activeScene + for _, x in ipairs(scenes) do + if x.isActive then + activeScene = x + break + end + end + + validate(activeScene, "Could not find active scene") + + return { + layout = layout, + navigation = navigation, + position = position, + scenes = scenes, + scene = activeScene, + options = options, + index = activeScene.index, + } +end + +local function filterStale(scenes) + local filtered = Cryo.List.filter(scenes, function(scene) + return not scene.isStale + end) + + if #filtered == #scenes then + return scenes + else + return filtered + end +end + +local Transitioner = Roact.Component:extend("Transitioner") + +function Transitioner:init() + local navigationState = self.props.navigation.state + local descriptors = self.props.descriptors + + self.state = { + -- Layout is passed to StackViewLayout in order to allow it to + -- sync animations. + layout = { + height = Otter.createSingleMotor(0), + width = Otter.createSingleMotor(0), + initWidth = 0, + initHeight = 0, + isMeasured = false, + }, + position = Otter.createSingleMotor(navigationState.index), + scenes = ScenesReducer({}, navigationState, nil, descriptors), + } + + self._doOnAbsoluteSizeChanged = function(...) + return self:_onAbsoluteSizeChanged(...) + end + + self._positionLastValue = navigationState.index + + self._prevTransitionProps = nil + self._transitionProps = buildTransitionProps(self.props, self.state) + + self._isMounted = false + self._isTransitionRunning = false + self._transitionQueue = {} + + self._completeSignalDisconnector = self.state.position:onComplete(function() + spawn(function() + if self._isMounted then + self:_onTransitionEnd() + end + end) + end) + + self._stepSignalDisconnector = self.state.position:onStep(function(value) + self._positionLastValue = value + end) +end + +function Transitioner:didMount() + self._isMounted = true +end + +function Transitioner:willUnmount() + self._isMounted = false + + if self._completeSignalDisconnector then + self._completeSignalDisconnector() + self._completeSignalDisconnector = nil + end + + if self._stepSignalDisconnector then + self._stepSignalDisconnector() + self._stepSignalDisconnector = nil + end +end + +function Transitioner:didUpdate(prevProps) + -- React-navigation uses componentWillReceiveProps that is only called when Parent + -- re-renders or when this component is actually being given new props, so we need to + -- filter here. If not, this would trigger on setState and enter an infinite loop. + if self.props ~= prevProps then + if self._isTransitionRunning then + local mostRecentTransition = self._transitionQueue[#self._transitionQueue] or {} + -- don't enqueue spurious extra copies of same transition props + if mostRecentTransition.prevProps ~= prevProps then + table.insert(self._transitionQueue, { prevProps = prevProps }) + end + + return + end + + self:_startTransition(prevProps, self.props) + end +end + +function Transitioner:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = true, + [Roact.Change.AbsoluteSize] = self._doOnAbsoluteSizeChanged, + }, { + ["$InnerComponent"] = self.props.render( + self._transitionProps, self._prevTransitionProps), + }) +end + +-- equivalent to React-Nav's Transitioner._onLayout +function Transitioner:_onAbsoluteSizeChanged(rbx) + local width = rbx.AbsoluteSize.X + local height = rbx.AbsoluteSize.Y + + if width == self.state.layout.initWidth and + height == self.state.layout.initHeight then + return + end + + local layout = Cryo.Dictionary.join(self.state.layout, { + initWidth = width, + initHeight = height, + isMeasured = true, + }) + + layout.width:setGoal(Otter.instant(width)) + layout.height:setGoal(Otter.instant(height)) + + local nextState = Cryo.Dictionary.join(self.state, { + layout = layout, + }) + + self._transitionProps = buildTransitionProps(self.props, nextState) + + spawn(function() + if self._isMounted then + self:setState(nextState) + end + end) +end + +function Transitioner:_computeScenes(props, nextProps) + local nextScenes = ScenesReducer( + self.state.scenes, + nextProps.navigation.state, + props.navigation.state, + nextProps.descriptors) + + if not nextProps.navigation.state.isTransitioning then + nextScenes = filterStale(nextScenes) + end + + if nextScenes == self.state.scenes then + return nil + end + + return nextScenes +end + +function Transitioner:_startTransition(props, nextProps) + local indexHasChanged = props.navigation.state.index ~= nextProps.navigation.state.index + local nextScenes = self:_computeScenes(props, nextProps) + + if not nextScenes then + -- If nextScenes is nil, nothing has changed, so report transition end, then bail + self._prevTransitionProps = self._transitionProps + + -- Ensure that position is set to final position before firing transitionEnd + -- See https://github.com/react-navigation/react-navigation/issues/5247 + self.state.position:setGoal(Otter.instant(props.navigation.state.index)) + -- Transition end will be called by position motor. + return + end + + local nextState = Cryo.Dictionary.join(self.state, { + scenes = nextScenes, + }) + + local position = nextState.position + local toValue = nextProps.navigation.state.index + + -- compute transitionProps + self._prevTransitionProps = self._transitionProps + self._transitionProps = buildTransitionProps(nextProps, nextState) + local isTransitioning = self._transitionProps.navigation.state.isTransitioning + + if not isTransitioning or not indexHasChanged then + -- If state is not transitioning, then we go immediately to new index. + -- Likewise, if the index has not changed then we still need to set up initial + -- positions via setState. + self:setState(nextState) + + if nextProps.onTransitionStart then + nextProps.onTransitionStart(self._transitionProps, self._prevTransitionProps) + end + + if indexHasChanged then + position:setGoal(Otter.instant(toValue)) + -- motor will call end for us + else + -- motor not running, need to end manually + self:_onTransitionEnd() + end + elseif isTransitioning then + self._isTransitionRunning = true + self:setState(nextState) + + if nextProps.onTransitionStart then + nextProps.onTransitionStart(self._transitionProps, self._prevTransitionProps) + end + + -- get transition spec + local transitionUserSpec = {} + if nextProps.configureTransition then + transitionUserSpec = nextProps.configureTransition( + self._transitionProps, self._prevTransitionProps) or {} + end + + local transitionSpec = Cryo.Dictionary.join(DEFAULT_TRANSITION_SPEC, transitionUserSpec) + + local positionHasChanged = self._positionLastValue ~= toValue + if indexHasChanged and positionHasChanged then + position:setGoal(Otter.spring(nextProps.navigation.state.index, transitionSpec)) + -- motor will call end transition + else + -- motor not running, end transition manually + self:_onTransitionEnd() + end + end +end + +function Transitioner:_onTransitionEnd() + local prevTransitionProps = self._prevTransitionProps + self._prevTransitionProps = nil + + local scenes = filterStale(self.state.scenes) + + local nextState = Cryo.Dictionary.join(self.state, { + scenes = scenes, + }) + + self._transitionProps = buildTransitionProps(self.props, nextState) + + self:setState(nextState) + + if self.props.onTransitionEnd then + self.props.onTransitionEnd(self._transitionProps, prevTransitionProps) + end + + local firstQueuedTransition = self._transitionQueue[1] + if firstQueuedTransition then + local prevProps = firstQueuedTransition.prevProps + self._transitionQueue = Cryo.List.removeIndex(self._transitionQueue, 1) + self:_startTransition(prevProps, self.props) + else + self._isTransitionRunning = false + end +end + +return Transitioner diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/AppNavigationContext.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/AppNavigationContext.spec.lua new file mode 100644 index 0000000..814b205 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/AppNavigationContext.spec.lua @@ -0,0 +1,91 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local AppNavigationContext = require(script.Parent.Parent.AppNavigationContext) + + it("should propagate navigation prop from provider to consumer", function() + local testNavigationContextProp = {} + local testComponentNavContext = nil + + local provider = Roact.createElement(AppNavigationContext.Provider, { + navigation = testNavigationContextProp + }, { + Child = Roact.createElement(AppNavigationContext.Consumer, { + render = function(navigation) + testComponentNavContext = navigation + end + }) + }) + + local instance = Roact.mount(provider) + expect(testComponentNavContext).to.equal(testNavigationContextProp) + Roact.unmount(instance) + end) + + it("should override context navigation prop if custom prop is set on consumer", function() + local testCustomNavigationProp = {} + local testComponentNavContext = nil + + local provider = Roact.createElement(AppNavigationContext.Provider, { + navigation = {} + }, { + Child = Roact.createElement(AppNavigationContext.Consumer, { + navigation = testCustomNavigationProp, + render = function(navigation) + testComponentNavContext = navigation + end + }) + }) + + local instance = Roact.mount(provider) + expect(testComponentNavContext).to.equal(testCustomNavigationProp) + Roact.unmount(instance) + end) + + it("should pass navigation prop to statically wrapped components", function() + local testNavigationContextProp = {} + local passedNavigationProp = nil + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + passedNavigationProp = self.props.navigation + end + + local WrappedComponent = AppNavigationContext.connect(TestComponent) + + local element = Roact.createElement(AppNavigationContext.Provider, { + navigation = testNavigationContextProp + }, { + Child = Roact.createElement(WrappedComponent) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(passedNavigationProp).to.equal(testNavigationContextProp) + end) + + it("should override static wrapper navigation prop when navigation is directly set", function() + local testNavigationProp = {} + local passedNavigationProp = nil + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + passedNavigationProp = self.props.navigation + end + + local WrappedComponent = AppNavigationContext.connect(TestComponent) + + local element = Roact.createElement(AppNavigationContext.Provider, { + navigation = {} + }, { + Child = Roact.createElement(WrappedComponent, { + navigation = testNavigationProp + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(passedNavigationProp).to.equal(testNavigationProp) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ContentHeightFitFrame.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ContentHeightFitFrame.spec.lua new file mode 100644 index 0000000..3e41511 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ContentHeightFitFrame.spec.lua @@ -0,0 +1,21 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local ContentHeightFitFrame = require(script.Parent.Parent.ContentHeightFitFrame) + + -- This must be skipped because Lemur does not behave like engine. + itSKIP("should size to contents", function() + local element = Roact.createElement(ContentHeightFitFrame, nil, { + ChildOne = Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 100), + }), + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "FitTest") + + expect(container.FitTest.Size.X.Offset).to.equal(50) + expect(container.FitTest.Size.Y.Offset).to.equal(100) + + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/NavigationEventsAdapter.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/NavigationEventsAdapter.spec.lua new file mode 100644 index 0000000..75a3dbd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/NavigationEventsAdapter.spec.lua @@ -0,0 +1,61 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local NavigationEvents = require(script.Parent.Parent.Parent.NavigationEvents) + local NavigationEventsAdapter = require(script.Parent.Parent.NavigationEventsAdapter) + + it("should subscribe to events for each registered handler", function() + local mockNavContext = { + subscribedHandlers = {}, + calledHandlers = {}, + } + + function mockNavContext:makeHandler(symbol) + return function() + self.calledHandlers[symbol] = true + end + end + + function mockNavContext.addListener(symbol, callback) + mockNavContext.subscribedHandlers[symbol] = callback + return { + disconnect = function() + mockNavContext.subscribedHandlers[symbol] = nil + end + } + end + + local adapter = Roact.createElement(NavigationEventsAdapter, { + navigation = mockNavContext, + [NavigationEvents.WillFocus] = mockNavContext:makeHandler(NavigationEvents.WillFocus), + [NavigationEvents.DidFocus] = mockNavContext:makeHandler(NavigationEvents.DidFocus), + [NavigationEvents.WillBlur] = mockNavContext:makeHandler(NavigationEvents.WillBlur), + [NavigationEvents.DidBlur] = mockNavContext:makeHandler(NavigationEvents.DidBlur), + }) + + local instance = Roact.mount(adapter) + + expect(type(mockNavContext.subscribedHandlers[NavigationEvents.WillFocus])).to.equal("function") + expect(type(mockNavContext.subscribedHandlers[NavigationEvents.DidFocus])).to.equal("function") + expect(type(mockNavContext.subscribedHandlers[NavigationEvents.WillBlur])).to.equal("function") + expect(type(mockNavContext.subscribedHandlers[NavigationEvents.DidBlur])).to.equal("function") + + mockNavContext.subscribedHandlers[NavigationEvents.WillFocus]() + expect(mockNavContext.calledHandlers[NavigationEvents.WillFocus]).to.equal(true) + + mockNavContext.subscribedHandlers[NavigationEvents.DidFocus]() + expect(mockNavContext.calledHandlers[NavigationEvents.DidFocus]).to.equal(true) + + mockNavContext.subscribedHandlers[NavigationEvents.WillBlur]() + expect(mockNavContext.calledHandlers[NavigationEvents.WillBlur]).to.equal(true) + + mockNavContext.subscribedHandlers[NavigationEvents.DidBlur]() + expect(mockNavContext.calledHandlers[NavigationEvents.DidBlur]).to.equal(true) + + Roact.unmount(instance) + + expect(mockNavContext.subscribedHandlers[NavigationEvents.WillFocus]).to.equal(nil) + expect(mockNavContext.subscribedHandlers[NavigationEvents.DidFocus]).to.equal(nil) + expect(mockNavContext.subscribedHandlers[NavigationEvents.WillBlur]).to.equal(nil) + expect(mockNavContext.subscribedHandlers[NavigationEvents.DidBlur]).to.equal(nil) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/SceneView.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/SceneView.spec.lua new file mode 100644 index 0000000..8cd3941 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/SceneView.spec.lua @@ -0,0 +1,36 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local SceneView = require(script.Parent.Parent.SceneView) + local withNavigation = require(script.Parent.Parent.withNavigation) + + it("should mount inner component and pass down required props+context.navigation", function() + local testComponentNavigationFromProp = nil + local testComponentScreenProps = nil + local testComponentNavigationFromContext = nil + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + testComponentNavigationFromProp = self.props.navigation + testComponentScreenProps = self.props.screenProps + + return withNavigation(function(navigation) + testComponentNavigationFromContext = navigation + end) + end + + local testScreenProps = {} + local testNav = {} + local element = Roact.createElement(SceneView, { + screenProps = testScreenProps, + navigation = testNav, + component = TestComponent, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(testComponentScreenProps).to.equal(testScreenProps) + expect(testComponentNavigationFromProp).to.equal(testNav) + expect(testComponentNavigationFromContext).to.equal(testNav) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ScenesReducer.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ScenesReducer.spec.lua new file mode 100644 index 0000000..46e4e1e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ScenesReducer.spec.lua @@ -0,0 +1,5 @@ +return function() + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ScreenGuiWrapper.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ScreenGuiWrapper.spec.lua new file mode 100644 index 0000000..1d81665 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ScreenGuiWrapper.spec.lua @@ -0,0 +1,50 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local ScreenGuiWrapper = require(script.Parent.Parent.ScreenGuiWrapper) + + it("should mount the inner component if visible", function() + local innerComponentProps = nil + + local innerComponent = function(props) + innerComponentProps = props + return Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50) + }) + end + + local instance = Roact.mount(Roact.createElement(ScreenGuiWrapper, { + component = innerComponent, + visible = true, + myPassedInValue = 5, + })) + + expect(innerComponentProps).to.never.equal(nil) + expect(innerComponentProps.visible).to.equal(true) + expect(innerComponentProps.DisplayOrder).to.equal(nil) + expect(innerComponentProps.component).to.equal(nil) + expect(innerComponentProps.myPassedInValue).to.equal(5) + + Roact.unmount(instance) + end) + + it("should still mount the inner component if ScreenGui is not visible", function() + local innerComponentProps = nil + + local innerComponent = function(props) + innerComponentProps = props + return Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50) + }) + end + + local instance = Roact.mount(Roact.createElement(ScreenGuiWrapper, { + component = innerComponent, + visible = false, + })) + + expect(innerComponentProps).to.never.equal(nil) + expect(innerComponentProps.visible).to.equal(false) + + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/SwitchView.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/SwitchView.spec.lua new file mode 100644 index 0000000..05a9592 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/SwitchView.spec.lua @@ -0,0 +1,54 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local SwitchView = require(script.Parent.Parent.SwitchView) + local withNavigation = require(script.Parent.Parent.withNavigation) + + it("should mount and pass required props and context", function() + local testScreenProps = {} + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" } + }, + index = 1, + }, + } + + local testComponentNavigationFromProp = nil + local testComponentScreenProps = nil + local testComponentNavigationFromContext = nil + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + testComponentNavigationFromProp = self.props.navigation + testComponentScreenProps = self.props.screenProps + + return withNavigation(function(navigation) + testComponentNavigationFromContext = navigation + end) + end + + local testDescriptors = { + Foo = { + getComponent = function() + return TestComponent + end, + navigation = testNavigation, + } + } + + local element = Roact.createElement(SwitchView, { + screenProps = testScreenProps, + navigation = testNavigation, + descriptors = testDescriptors, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(testComponentNavigationFromProp).to.equal(testNavigation) + expect(testComponentScreenProps).to.equal(testScreenProps) + expect(testComponentNavigationFromContext).to.equal(testNavigation) + end) + +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/Transitioner.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/Transitioner.spec.lua new file mode 100644 index 0000000..ddbe63a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/Transitioner.spec.lua @@ -0,0 +1,8 @@ +return function() + -- local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + -- local Transitioner = require(script.Parent.Parent.Transitioner) + + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/withNavigation.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/withNavigation.spec.lua new file mode 100644 index 0000000..7cf9988 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/withNavigation.spec.lua @@ -0,0 +1,34 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local withNavigation = require(script.Parent.Parent.withNavigation) + local AppNavigationContext = require(script.Parent.Parent.AppNavigationContext) + + it("should throw if no renderProp is provided", function() + local status, err = pcall(function() + withNavigation(nil) + end) + + expect(status).to.equal(false) + expect(string.find(err, "withNavigation must be passed a render prop")).to.never.equal(nil) + end) + + it("should extract navigation object from provider and pass it through", function() + local testNavigation = {} + local extractedNavigation = nil + + local rootElement = Roact.createElement(AppNavigationContext.Provider, { + navigation = testNavigation, + }, { + Child = Roact.createElement(function() + return withNavigation(function(nav) + extractedNavigation = nav + end) + end) + }) + + local rootInstance = Roact.mount(rootElement) + Roact.unmount(rootInstance) + + expect(extractedNavigation).to.equal(testNavigation) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/withNavigationFocus.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/withNavigationFocus.spec.lua new file mode 100644 index 0000000..88b5696 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/withNavigationFocus.spec.lua @@ -0,0 +1,147 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local AppNavigationContext = require(script.Parent.Parent.AppNavigationContext) + local NavigationEvents = require(script.Parent.Parent.Parent.NavigationEvents) + local withNavigationFocus = require(script.Parent.Parent.withNavigationFocus) + + it("should pass focused=true when initially focused", function() + local testNavigation, testFocused + local component = function() + return withNavigationFocus(function(navigation, focused) + testNavigation = navigation + testFocused = focused + return nil + end) + end + + local navigationProp = { + isFocused = function() + return true + end, + addListener = function() + return { + disconnect = function() end + } + end + } + + local rootElement = Roact.createElement(AppNavigationContext.Provider, { + navigation = navigationProp, + }, { + child = Roact.createElement(component) + }) + + local instance = Roact.mount(rootElement) + expect(testNavigation).to.equal(navigationProp) + expect(testFocused).to.equal(true) + + Roact.unmount(instance) + end) + + it("should pass focused=false when initially unfocused", function() + local testNavigation, testFocused + local component = function() + return withNavigationFocus(function(navigation, focused) + testNavigation = navigation + testFocused = focused + return nil + end) + end + + local navigationProp = { + isFocused = function() + return false + end, + addListener = function() + return { + disconnect = function() end + } + end + } + + local rootElement = Roact.createElement(AppNavigationContext.Provider, { + navigation = navigationProp, + }, { + child = Roact.createElement(component) + }) + + local instance = Roact.mount(rootElement) + expect(testNavigation).to.equal(navigationProp) + expect(testFocused).to.equal(false) + + Roact.unmount(instance) + end) + + it("should re-render and set focused status for events", function() + local testListeners = {} + local testFocused = false + local component = function() + return withNavigationFocus(function(navigation, focused) + testFocused = focused + return nil + end) + end + + local navigationProp = { + isFocused = function() + return false + end, + addListener = function(event, listener) + testListeners[event] = listener + return { + disconnect = function() + testListeners[event] = nil + end + } + end + } + + local rootElement = Roact.createElement(AppNavigationContext.Provider, { + navigation = navigationProp, + }, { + child = Roact.createElement(component) + }) + + local instance = Roact.mount(rootElement) + expect(testFocused).to.equal(false) + expect(type(testListeners[NavigationEvents.DidFocus])).to.equal("function") + expect(type(testListeners[NavigationEvents.WillBlur])).to.equal("function") + + testListeners[NavigationEvents.DidFocus]() + expect(testFocused).to.equal(true) + + testListeners[NavigationEvents.WillBlur]() + expect(testFocused).to.equal(false) + + Roact.unmount(instance) + expect(testListeners[NavigationEvents.DidFocus]).to.equal(nil) + expect(testListeners[NavigationEvents.WillBlur]).to.equal(nil) + end) + + it("should throw when renderProp is not provided", function() + local success, err = pcall(function() + withNavigationFocus(nil) + end) + + expect(success).to.equal(false) + expect(string.find(err, + "withNavigationFocus must be passed a render prop")).to.never.equal(nil) + end) + + it("should throw when used outside of a navigation provider", function() + local component = function() + return withNavigationFocus(function(navigation, focused) + + end) + end + + local element = Roact.createElement(component) + + local success, _ = pcall(function() + Roact.unmount(Roact.mount(element)) + end) + + expect(success).to.equal(false) + -- We do not test the message because NavigationConsumer gets in the way here. + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/withNavigation.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/withNavigation.lua new file mode 100644 index 0000000..4590801 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/withNavigation.lua @@ -0,0 +1,24 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local AppNavigationContext = require(script.Parent.AppNavigationContext) +local validate = require(script.Parent.Parent.utils.validate) + +--[[ + withNavigation() is a convenience function that you can use in your component's + render function to access the navigation context object. For example: + + function MyComponent:render() + return withNavigation(function(navigation) + return Roact.createElement("TextButton", { + [Roact.Activated] = function() + navigation.navigate("DetailPage") + end + }) + end) + end +]] +return function(renderProp) + validate(renderProp ~= nil, "withNavigation must be passed a render prop") + return Roact.createElement(AppNavigationContext.Consumer, { + render = renderProp + }) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/withNavigationFocus.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/withNavigationFocus.lua new file mode 100644 index 0000000..cf9d96b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/withNavigationFocus.lua @@ -0,0 +1,89 @@ +--[[ + withNavigationFocus() is a convenience function that extends withNavigation(), + allowing your render function (and therefor your subgraph) to access the + navigation context object AND an additional boolean that indicates whether or + not the containing screen component is in focus. For example: + + function MyButtonComponent:render() + return withNavigationFocus(function(navigation, focused) + return Roact.createElement("TextButton", { + Enabled = focused, + [Roact.Event.Activated] = function() + navigation.navigate("DetailPage") + end, + }) + end) + end + + This is very useful when writing generic components that need to work with + the navigation system (e.g. preventing buttons from navigating when a screen + is not in focus so you don't cause double-navigation). + + Note that if you ONLY need the 'navigation' context object, it is recommended + that you use withNavigation() for performance reasons. +]] +local Roact = require(script.Parent.Parent.Parent.Roact) +local NavigationEvents = require(script.Parent.Parent.NavigationEvents) +local AppNavigationContext = require(script.Parent.AppNavigationContext) +local validate = require(script.Parent.Parent.utils.validate) + + +local NavigationFocusComponent = Roact.Component:extend("NavigationFocusComponent") + +function NavigationFocusComponent:init() + local navigation = self.props.navigation + self.state = { + isFocused = navigation and navigation.isFocused() or false + } +end + +function NavigationFocusComponent:didMount() + local navigation = self.props.navigation + validate(navigation ~= nil, + "withNavigationFocus can only be used within the view hierarchy of a navigator. " .. + "The wrapped component cannot access 'navigation' from props or context.") + + self._didFocusListener = navigation.addListener(NavigationEvents.DidFocus, function() + -- no spawn because we expect this to be called directly from safe paths + self:setState({ + isFocused = true, + }) + end) + + self._willBlurListener = navigation.addListener(NavigationEvents.WillBlur, function() + -- no spawn because we expect this to be called directly from safe paths + self:setState({ + isFocused = false, + }) + end) +end + +function NavigationFocusComponent:willUnmount() + if self._didFocusListener then + self._didFocusListener:disconnect() + self._didFocusListener = nil + end + + if self._willBlurListener then + self._willBlurListener:disconnect() + self._willBlurListener = nil + end +end + +function NavigationFocusComponent:render() + local isFocused = self.state.isFocused + local navigation = self.props.navigation + local render = self.props.render + + return render(navigation, isFocused) +end + +NavigationFocusComponent = AppNavigationContext.connect(NavigationFocusComponent) + +return function(renderProp) + validate(renderProp ~= nil, "withNavigationFocus must be passed a render prop") + + return Roact.createElement(NavigationFocusComponent, { + render = renderProp + }) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/init.lua new file mode 100644 index 0000000..46cf353 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/init.lua @@ -0,0 +1,12 @@ +--[[ + Defines utilities for working with 'dictionary-like' tables. + + Dictionaries can be indexed by any value, but don't have the ordering + expectations that lists have. +]] + +return { + join = require(script.join), + keys = require(script.keys), + values = require(script.values), +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/init.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/init.spec.lua new file mode 100644 index 0000000..ff7e8e3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/init.spec.lua @@ -0,0 +1,5 @@ +return function() + it("should load", function() + require(script.Parent) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/join.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/join.lua new file mode 100644 index 0000000..2af8270 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/join.lua @@ -0,0 +1,30 @@ +local None = require(script.Parent.Parent.None) + +--[[ + Combine a number of dictionary-like tables into a new table. + + Keys specified in later tables will overwrite keys in previous tables. + + Use `Cryo.None` as a value to remove a key. This is necessary because + Lua does not distinguish between a value not being present in a table and a + value being `nil`. +]] +local function join(...) + local new = {} + + for i = 1, select("#", ...) do + local source = select(i, ...) + + for key, value in pairs(source) do + if value == None then + new[key] = nil + else + new[key] = value + end + end + end + + return new +end + +return join \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/join.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/join.spec.lua new file mode 100644 index 0000000..73eec64 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/join.spec.lua @@ -0,0 +1,94 @@ +return function() + local join = require(script.Parent.join) + local None = require(script.Parent.Parent.None) + + it("should return a new table", function() + local a = {} + + expect(join(a)).never.to.equal(a) + end) + + it("should merge tables, overwriting previous values", function() + local a = { + foo = "foo-a", + bar = "bar-a", + } + + local b = { + foo = "foo-b", + baz = "baz-b", + } + + local c = join(a, b) + + expect(c.foo).to.equal(b.foo) + expect(c.bar).to.equal(a.bar) + expect(c.baz).to.equal(b.baz) + end) + + it("should remove values set to None", function() + local a = { + foo = "foo-a", + } + + local b = { + foo = None, + } + + local c = join(a, b) + + expect(c.foo).to.equal(nil) + end) + + it("should not mutate passed in tables", function() + local mutationsA = 0 + local mutationsB = 0 + + local a = {} + local b = { + foo = "foo-b", + } + + setmetatable(a, { + __newindex = function() + mutationsA = mutationsA + 1 + end, + }) + + setmetatable(b, { + __newindex = function() + mutationsB = mutationsB + 1 + end, + }) + + join(a, b) + + expect(mutationsA).to.equal(0) + expect(mutationsB).to.equal(0) + expect(b.foo).to.equal("foo-b") + end) + + it("should accept arbitrary numbers of tables", function() + local a = { + foo = "foo-a", + } + + local b = { + bar = "bar-b", + } + + local c = { + baz = "baz-c", + } + + local d = join(a, b, c) + + expect(d.foo).to.equal(a.foo) + expect(d.bar).to.equal(b.bar) + expect(d.baz).to.equal(c.baz) + end) + + it("should accept zero tables", function() + expect(join()).to.be.a("table") + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/keys.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/keys.lua new file mode 100644 index 0000000..e2e4d1a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/keys.lua @@ -0,0 +1,16 @@ +--[[ + Returns a list of the keys from the given dictionary. +]] +local function keys(dictionary) + local new = {} + local index = 1 + + for key in pairs(dictionary) do + new[index] = key + index = index + 1 + end + + return new +end + +return keys \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/keys.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/keys.spec.lua new file mode 100644 index 0000000..a342a82 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/keys.spec.lua @@ -0,0 +1,74 @@ +return function() + local keys = require(script.Parent.keys) + local None = require(script.Parent.Parent.None) + + it("should not mutate the given table", function() + local a = { + Foo = "FooValue", + Bar = "BarValue" + } + local aCopy = { + Foo = "FooValue", + Bar = "BarValue" + } + + keys(a) + + for key, value in pairs(a) do + expect(aCopy[key]).to.equal(value) + end + for key, value in pairs(aCopy) do + expect(a[key]).to.equal(value) + end + end) + + it("should return the correct keys", function() + local a = { + Foo = "FooValue", + Bar = "BarValue", + Test = "TestValue" + } + local keyCount = { + Foo = 1, + Bar = 1, + Test = 1 + } + local b = keys(a) + + expect(#b).to.equal(3) + for _, key in ipairs(b) do + expect(keyCount[key]).never.to.equal(nil) + keyCount[key] = keyCount[key] - 1 + end + for _, count in pairs(keyCount) do + expect(count).to.equal(0) + end + end) + + it("should work with an empty table", function() + local a = keys({}) + + expect(next(a)).to.equal(nil) + end) + + it("should contain a None element if there is a None key in the dictionary", function() + local a = { + [None] = "Foo", + Bar = "BarValue" + } + local keyCount = { + [None] = 1, + Bar = 1 + } + local b = keys(a) + + expect(#b).to.equal(2) + for _, key in ipairs(b) do + expect(keyCount[key]).never.to.equal(nil) + keyCount[key] = keyCount[key] - 1 + end + for _, count in pairs(keyCount) do + expect(count).to.equal(0) + end + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/values.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/values.lua new file mode 100644 index 0000000..d97e49f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/values.lua @@ -0,0 +1,17 @@ +--[[ + Returns a list of the values of the given dictionary. +]] + +local function values(dictionary) + local new = {} + local index = 1 + + for _, value in pairs(dictionary) do + new[index] = value + index = index + 1 + end + + return new +end + +return values \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/values.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/values.spec.lua new file mode 100644 index 0000000..e87271d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/values.spec.lua @@ -0,0 +1,96 @@ +return function() + local values = require(script.Parent.values) + local None = require(script.Parent.Parent.None) + + it("should not mutate the given table", function() + local a = { + Foo = "FooValue", + Bar = "BarValue" + } + local aCopy = { + Foo = "FooValue", + Bar = "BarValue" + } + + values(a) + + for key, value in pairs(a) do + expect(aCopy[key]).to.equal(value) + end + for key, value in pairs(aCopy) do + expect(a[key]).to.equal(value) + end + end) + + it("should return the correct values", function() + local a = { + Foo = "FooValue", + Bar = "BarValue", + Test = "TestValue" + } + local valueCount = { + FooValue = 1, + BarValue = 1, + TestValue = 1 + } + local b = values(a) + + expect(#b).to.equal(3) + for _, value in ipairs(b) do + expect(valueCount[value]).never.to.equal(nil) + valueCount[value] = valueCount[value] - 1 + end + for _, count in pairs(valueCount) do + expect(count).to.equal(0) + end + end) + + it("should return duplicates if two values are the same", function() + local a = { + Foo = "FooValue", + Bar = "BarValue", + Test = "FooValue" + } + local valueCount = { + FooValue = 2, + BarValue = 1, + } + local b = values(a) + + expect(#b).to.equal(3) + for _, value in ipairs(b) do + expect(valueCount[value]).never.to.equal(nil) + valueCount[value] = valueCount[value] - 1 + end + for _, count in pairs(valueCount) do + expect(count).to.equal(0) + end + end) + + it("should work with an empty table", function() + local a = values({}) + + expect(next(a)).to.equal(nil) + end) + + it("should contain a None element if there is a None value in the dictionary", function() + local a = { + Foo = None, + Bar = "BarValue" + } + local valueCount = { + [None] = 1, + BarValue = 1 + } + local b = values(a) + + expect(#b).to.equal(2) + for _, value in ipairs(b) do + expect(valueCount[value]).never.to.equal(nil) + valueCount[value] = valueCount[value] - 1 + end + for _, count in pairs(valueCount) do + expect(count).to.equal(0) + end + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filter.lua new file mode 100644 index 0000000..637978b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filter.lua @@ -0,0 +1,20 @@ +--[[ + Create a copy of a list with only values for which `callback` returns true. + Calls the callback with (value, index). +]] +local function filter(list, callback) + local new = {} + local index = 1 + + for i = 1, #list do + local value = list[i] + if callback(value, i) then + new[index] = value + index = index + 1 + end + end + + return new +end + +return filter \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filter.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filter.spec.lua new file mode 100644 index 0000000..1f6346d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filter.spec.lua @@ -0,0 +1,77 @@ +return function() + local filter = require(script.Parent.filter) + + it("should call the callback for each element", function() + local a = { + "foo1", + "foo2", + "foo3" + } + local copy = {} + local function copyCallback(value, index) + copy[index] = value + return true + end + filter(a, copyCallback) + + for key, value in pairs(a) do + expect(copy[key]).to.equal(value) + end + + for key, value in pairs(copy) do + expect(value).to.equal(a[key]) + end + end) + + it("should correctly use the filter callback", function() + local a = {1, 2, 3, 4, 5} + local function evenOnly(value) + return value % 2 == 0 + end + local b = filter(a, evenOnly) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(2) + expect(b[2]).to.equal(4) + end) + + it("should copy the list correctly", function() + local a = {1, 2, 3} + local function keepAll() + return true + end + local b = filter(a, keepAll) + + expect(b).never.to.equal(a) + + for key, value in pairs(a) do + expect(b[key]).to.equal(value) + end + + for key, value in pairs(b) do + expect(value).to.equal(a[key]) + end + end) + + it("should work with an empty table", function() + local called = false + local function callback() + called = true + return true + end + local a = filter({}, callback) + + expect(#a).to.equal(0) + expect(called).to.equal(false) + end) + + it("should remove all element from a list when callback return always false", function() + local a = {6, 2, 8, 6, 7} + local function removeAll() + return false + end + local b = filter(a, removeAll) + + expect(#b).to.equal(0) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filterMap.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filterMap.lua new file mode 100644 index 0000000..494fd66 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filterMap.lua @@ -0,0 +1,23 @@ +--[[ + Create a copy of a list doing a combination filter and map. + + If callback returns nil for any item, it is considered filtered from the + list. Any other value is considered the result of the 'map' operation. +]] +local function filterMap(list, callback) + local new = {} + local index = 1 + + for i = 1, #list do + local result = callback(list[i], i) + + if result ~= nil then + new[index] = result + index = index + 1 + end + end + + return new +end + +return filterMap \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filterMap.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filterMap.spec.lua new file mode 100644 index 0000000..a77f2c5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filterMap.spec.lua @@ -0,0 +1,92 @@ +return function() + local filterMap = require(script.Parent.filterMap) + + it("should return a new table", function() + local a = {1, 2, 3} + local function callback() + return 1 + end + local b = filterMap(a, callback) + + expect(b).never.to.equal(a) + end) + + it("should call the callback for each element", function() + local a = { + "foo1", + "foo2", + "foo3" + } + local copy = {} + local function callback(value, index) + copy[index] = value + return value + end + filterMap(a, callback) + + for key, value in pairs(a) do + expect(copy[key]).to.equal(value) + end + + for key, value in pairs(copy) do + expect(value).to.equal(a[key]) + end + end) + + it("should correctly use the filter callback", function() + local a = {1, 2, 3, 4, 5} + local function doubleOddOnly(value) + if value % 2 == 0 then + return nil + else + return value * 2 + end + end + local b = filterMap(a, doubleOddOnly) + + expect(#b).to.equal(3) + expect(b[1]).to.equal(2) + expect(b[2]).to.equal(6) + expect(b[3]).to.equal(10) + end) + + it("should copy the list correctly", function() + local a = {1, 2, 3} + local function copyCallback(value) + return value + end + local b = filterMap(a, copyCallback) + + expect(b).never.to.equal(a) + + for key, value in pairs(a) do + expect(b[key]).to.equal(value) + end + + for key, value in pairs(b) do + expect(value).to.equal(a[key]) + end + end) + + it("should work with an empty table", function() + local called = false + local function callback() + called = true + return true + end + local a = filterMap({}, callback) + + expect(#a).to.equal(0) + expect(called).to.equal(false) + end) + + it("should remove all elements from a list when callback return always nil", function() + local a = {6, 2, 8, 6, 7} + local function removeAll() + return nil + end + local b = filterMap(a, removeAll) + + expect(#b).to.equal(0) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/find.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/find.lua new file mode 100644 index 0000000..d2fa161 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/find.lua @@ -0,0 +1,14 @@ +--[[ + Returns the index of the first value found or nil if not found. +]] + +local function find(list, value) + for i = 1, #list do + if list[i] == value then + return i + end + end + return nil +end + +return find \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/find.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/find.spec.lua new file mode 100644 index 0000000..c7ca169 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/find.spec.lua @@ -0,0 +1,30 @@ +return function() + local find = require(script.Parent.find) + + it("should return the correct index", function() + local a = {5, 4, 3, 2, 1} + + expect(find(a, 1)).to.equal(5) + expect(find(a, 2)).to.equal(4) + expect(find(a, 3)).to.equal(3) + expect(find(a, 4)).to.equal(2) + expect(find(a, 5)).to.equal(1) + end) + + it("should work with an empty table", function() + expect(find({}, 1)).to.equal(nil) + end) + + it("should return nil when the given value is not found", function() + local a = {1, 2, 3} + + expect(find(a, 4)).to.equal(nil) + expect(type(find(a, 4))).to.equal("nil") + end) + + it("should return the index of the first value found", function() + local list = {1, 2, 2} + + expect(find(list, 2)).to.equal(2) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/findWhere.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/findWhere.lua new file mode 100644 index 0000000..dfac37a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/findWhere.lua @@ -0,0 +1,14 @@ +--[[ + Returns the index of the first value for which predicate(value, index) is truthy, or nil if not found. +]] + +local function findWhere(list, predicate) + for i = 1, #list do + if predicate(list[i], i) then + return i + end + end + return nil +end + +return findWhere \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/findWhere.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/findWhere.spec.lua new file mode 100644 index 0000000..9a2c937 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/findWhere.spec.lua @@ -0,0 +1,63 @@ +return function() + local findWhere = require(script.Parent.findWhere) + + it("should return the correct index", function() + local numbers = { 1, 5, 10, 7 } + local isEven = function(value) + return value % 2 == 0 + end + + local isOdd = function(value) + return value % 2 == 1 + end + + expect(findWhere(numbers, isEven)).to.equal(3) + expect(findWhere(numbers, isOdd)).to.equal(1) + end) + + it("should work with an empty table", function() + local anything = function() + return true + end + expect(findWhere({}, anything)).to.equal(nil) + end) + + it("should return nil when the when no value satisfies the predicate", function() + local numbers = { 1, 2, 3 } + local isFour = function(value) + return value == 4 + end + + expect(findWhere(numbers, isFour)).to.equal(nil) + end) + + it("should return the index of the first value for which the predicate is true", function() + local list = { 1, 1, 1, 2, 2 } + + local isTwo = function(value) + return value == 2 + end + + expect(findWhere(list, isTwo)).to.equal(4) + end) + + it("should allow access to table index in the predicate function", function() + local list = { 5, 4, 3, 2, 1 } + + local isIndexFour = function(_, index) + return index == 4 + end + + expect(findWhere(list, isIndexFour)).to.equal(4) + end) + + it("should allow access to both value and index in the predicate function", function() + local list = { 1, 1, 2, 2, 1 } + + local sumValueAndIndexToFive = function(value, index) + return value + index == 5 + end + + expect(findWhere(list, sumValueAndIndexToFive)).to.equal(3) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldLeft.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldLeft.lua new file mode 100644 index 0000000..9b254e4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldLeft.lua @@ -0,0 +1,14 @@ +--[[ + Performs a left-fold of the list with the given initial value and callback. +]] +local function foldLeft(list, callback, initialValue) + local accum = initialValue + + for i = 1, #list do + accum = callback(accum, list[i], i) + end + + return accum +end + +return foldLeft \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldLeft.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldLeft.spec.lua new file mode 100644 index 0000000..43ee185 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldLeft.spec.lua @@ -0,0 +1,61 @@ +return function() + local foldLeft = require(script.Parent.foldLeft) + + it("should call the callback", function() + local a = {1, 2, 3} + local called = 0 + + foldLeft(a, function() + called = called + 1 + end, 0) + + expect(called).to.equal(3) + end) + + it("should not call the callback when the list is empty", function() + local called = false + + foldLeft({}, function() + called = true + end, 0) + + expect(called).to.equal(false) + end) + + it("should call the callback for each element", function() + local a = {4, 5, 6} + local copy = {} + + foldLeft(a, function(accum, value, index) + copy[index] = value + return accum + end, 0) + + expect(#copy).to.equal(#a) + + for key, value in pairs(a) do + expect(value).to.equal(copy[key]) + end + end) + + it("should pass the same modified initial value to the callback", function() + local a = {5, 4, 3} + local initialValue = {} + + foldLeft(a, function(accum) + expect(accum).to.equal(initialValue) + return accum + end, initialValue) + end) + + it("should call the callback in the correct order", function() + local a = {5, 4, 3} + local index = 1 + + foldLeft(a, function(accum, value) + expect(value).to.equal(a[index]) + index = index + 1 + return accum + end, 0) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldRight.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldRight.lua new file mode 100644 index 0000000..981504c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldRight.lua @@ -0,0 +1,14 @@ +--[[ + Performs a right-fold of the list with the given initial value and callback. +]] +local function foldRight(list, callback, initialValue) + local accum = initialValue + + for i = #list, 1, -1 do + accum = callback(accum, list[i], i) + end + + return accum +end + +return foldRight \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldRight.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldRight.spec.lua new file mode 100644 index 0000000..25ebc2b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldRight.spec.lua @@ -0,0 +1,61 @@ +return function() + local foldRight = require(script.Parent.foldRight) + + it("should call the callback", function() + local a = {1, 2, 3} + local called = 0 + + foldRight(a, function() + called = called + 1 + end, 0) + + expect(called).to.equal(3) + end) + + it("should not call the callback when the list is empty", function() + local called = false + + foldRight({}, function() + called = true + end, 0) + + expect(called).to.equal(false) + end) + + it("should call the callback for each element", function() + local a = {4, 5, 6} + local copy = {} + + foldRight(a, function(accum, value, index) + copy[index] = value + return accum + end, 0) + + expect(#copy).to.equal(#a) + + for key, value in pairs(a) do + expect(value).to.equal(copy[key]) + end + end) + + it("should pass the same modified initial value to the callback", function() + local a = {5, 4, 3} + local initialValue = {} + + foldRight(a, function(accum) + expect(accum).to.equal(initialValue) + return accum + end, initialValue) + end) + + it("should call the callback in the correct order", function() + local a = {5, 4, 3} + local index = 3 + + foldRight(a, function(accum, value) + expect(value).to.equal(a[index]) + index = index - 1 + return accum + end, 0) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/getRange.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/getRange.lua new file mode 100644 index 0000000..8df4949 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/getRange.lua @@ -0,0 +1,19 @@ +--[[ + Returns a new list containing only the elements within the given range. +]] + +local function getRange(list, startIndex, endIndex) + assert(startIndex <= endIndex, "startIndex must be less than or equal to endIndex") + + local new = {} + local index = 1 + + for i = math.max(1, startIndex), math.min(#list, endIndex) do + new[index] = list[i] + index = index + 1 + end + + return new +end + +return getRange \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/getRange.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/getRange.spec.lua new file mode 100644 index 0000000..50f5641 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/getRange.spec.lua @@ -0,0 +1,62 @@ +return function() + local getRange = require(script.Parent.getRange) + + it("should return the correct range", function() + local a = {1, 2, 3, 4} + local b = getRange(a, 2, 3) + + expect(b[1]).to.equal(2) + expect(b[2]).to.equal(3) + expect(#b).to.equal(2) + + local c = getRange(a, 4, 4) + expect(#c).to.equal(1) + expect(c[1]).to.equal(4) + end) + + it("should throw when the start index is higher than the end index", function() + local a = {5, 8, 7, 2, 3, 7} + + expect(function() + getRange(a, 4, 1) + end).to.throw() + end) + + it("should copy the table", function() + local a = {6, 8, 1, 3, 7, 2} + local b = getRange(a, 1, #a) + + for key, value in pairs(a) do + expect(b[key]).to.equal(value) + end + + for key, value in pairs(b) do + expect(value).to.equal(a[key]) + end + end) + + it("should work with an empty table", function() + local a = getRange({}, 1, 5) + + expect(a).to.be.a("table") + expect(#a).to.equal(0) + end) + + it("should work when the start index is smaller that 1", function() + local a = {1, 2, 3, 4} + local b = getRange(a, -2, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + end) + + it("should work when the end index is larger that the list length", function() + local a = {1, 2, 3, 4} + local b = getRange(a, 3, 18) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(3) + expect(b[2]).to.equal(4) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/init.lua new file mode 100644 index 0000000..c0022c4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/init.lua @@ -0,0 +1,22 @@ +--[[ + Defines utilities for working with 'list-like' tables. +]] + +return { + filter = require(script.filter), + filterMap = require(script.filterMap), + find = require(script.find), + findWhere = require(script.findWhere), + foldLeft = require(script.foldLeft), + foldRight = require(script.foldRight), + getRange = require(script.getRange), + join = require(script.join), + map = require(script.map), + removeIndex = require(script.removeIndex), + removeRange = require(script.removeRange), + removeValue = require(script.removeValue), + replaceIndex = require(script.replaceIndex), + reverse = require(script.reverse), + sort = require(script.sort), + toSet = require(script.toSet), +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/init.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/init.spec.lua new file mode 100644 index 0000000..ff7e8e3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/init.spec.lua @@ -0,0 +1,5 @@ +return function() + it("should load", function() + require(script.Parent) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/join.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/join.lua new file mode 100644 index 0000000..496a26f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/join.lua @@ -0,0 +1,25 @@ +local None = require(script.Parent.Parent.None) + +--[[ + Joins any number of lists together into a new list +]] +local function join(...) + local new = {} + + for listKey = 1, select("#", ...) do + local list = select(listKey, ...) + local len = #new + + for itemKey = 1, #list do + if list[itemKey] == None then + len = len - 1 + else + new[len + itemKey] = list[itemKey] + end + end + end + + return new +end + +return join \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/join.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/join.spec.lua new file mode 100644 index 0000000..49dfaaa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/join.spec.lua @@ -0,0 +1,44 @@ +return function() + local join = require(script.Parent.join) + local None = require(script.Parent.Parent.None) + + it("should return a new table", function() + local a = {} + + expect(join(a)).never.to.equal(a) + end) + + it("should remove elements equal to None", function() + local a = { + "foo-a" + } + + local b = { + None, + "foo-b" + } + + local c = join(a, b) + + expect(c[1]).to.equal("foo-a") + expect(c[2]).to.equal("foo-b") + expect(c[3]).to.equal(nil) + end) + + it("should accept arbitrary numbers of tables", function() + local a = {1} + local b = {2} + local c = {3} + + local d = join(a, b, c) + + expect(#d).to.equal(3) + expect(d[1]).to.equal(1) + expect(d[2]).to.equal(2) + expect(d[3]).to.equal(3) + end) + + it("should accept zero tables", function() + expect(join()).to.be.a("table") + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/map.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/map.lua new file mode 100644 index 0000000..ef74e96 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/map.lua @@ -0,0 +1,14 @@ +--[[ + Create a copy of a list where each value is transformed by `callback` +]] +local function map(list, callback) + local new = {} + + for i = 1, #list do + new[i] = callback(list[i], i) + end + + return new +end + +return map \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/map.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/map.spec.lua new file mode 100644 index 0000000..eec73c3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/map.spec.lua @@ -0,0 +1,61 @@ +return function() + local map = require(script.Parent.map) + + it("should return a new table", function() + local a = {1, 2, 3} + + expect(map(a, function() end)).never.to.equal(a) + end) + + it("should call the callback for each element", function() + local a = {5, 6, 7} + local copy = {} + map(a, function(value, index) + copy[index] = value + return value + end) + + for key, value in pairs(a) do + expect(copy[key]).to.equal(value) + end + + for key, value in pairs(copy) do + expect(value).to.equal(a[key]) + end + end) + + it("should copy list", function() + local a = {1, 2, 3} + local b = map(a, function(value) + return value + end) + + for key, value in pairs(a) do + expect(b[key]).to.equal(value) + end + + for key, value in pairs(b) do + expect(value).to.equal(a[key]) + end + end) + + it("should sets the new values to the result of the given callback", function() + local a = {5, 6, 7} + local b = map(a, function(value) + return value * 2 + end) + + expect(#b).to.equal(#a) + for i = 1, #a do + expect(b[i]).to.equal(a[i] * 2) + end + end) + + it("should work with an empty list", function() + local a = {} + local b = map(a, function() end) + + expect(b).to.be.a("table") + expect(b).never.to.equal(a) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeIndex.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeIndex.lua new file mode 100644 index 0000000..b7bce5b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeIndex.lua @@ -0,0 +1,19 @@ +--[[ + Remove the element at the given index. +]] +local function removeIndex(list, index) + local new = {} + local removed = 0 + + for i = 1, #list do + if i == index then + removed = 1 + else + new[i - removed] = list[i] + end + end + + return new +end + +return removeIndex \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeIndex.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeIndex.spec.lua new file mode 100644 index 0000000..2f4c975 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeIndex.spec.lua @@ -0,0 +1,53 @@ +return function() + local removeIndex = require(script.Parent.removeIndex) + local None = require(script.Parent.Parent.None) + + it("should remove the element at the given index", function() + local a = { + "first", + "second", + "third" + } + + local b = removeIndex(a, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal("first") + expect(b[2]).to.equal("third") + end) + + it("should not remove any element if index is out of bound", function() + local a = { + "first", + "second", + "third" + } + local b = removeIndex(a, 4) + + expect(#b).to.equal(#a) + for i = 1, #a do + expect(b[i]).to.equal(a[i]) + end + + local c = removeIndex(a, -2) + + expect(#c).to.equal(#a) + for i = 1, #a do + expect(c[i]).to.equal(a[i]) + end + end) + + it("should work with a None element", function() + local a = { + "first", + None, + "third" + } + + local b = removeIndex(a, 1) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(None) + expect(b[2]).to.equal("third") + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeRange.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeRange.lua new file mode 100644 index 0000000..a4e4d2f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeRange.lua @@ -0,0 +1,23 @@ +--[[ + Remove the range from the list starting from the index. +]] +local function removeRange(list, startIndex, endIndex) + assert(startIndex <= endIndex, "startIndex must be less than or equal to endIndex") + + local new = {} + local index = 1 + + for i = 1, math.min(#list, startIndex - 1) do + new[index] = list[i] + index = index + 1 + end + + for i = endIndex + 1, #list do + new[index] = list[i] + index = index + 1 + end + + return new +end + +return removeRange \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeRange.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeRange.spec.lua new file mode 100644 index 0000000..73ffbf7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeRange.spec.lua @@ -0,0 +1,75 @@ +return function() + local removeRange = require(script.Parent.removeRange) + local None = require(script.Parent.Parent.None) + + it("should remove elements properly", function() + local a = {1, 2, 3} + local b = removeRange(a, 2, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + + local c = {1, 2, 3, 4, 5, 6} + local d = removeRange(c, 1, 4) + + expect(#d).to.equal(2) + expect(d[1]).to.equal(5) + expect(d[2]).to.equal(6) + + local e = removeRange(c, 2, 5) + + expect(#e).to.equal(2) + expect(e[1]).to.equal(1) + expect(e[2]).to.equal(6) + end) + + it("should throw when the start index is higher than the end index", function() + local a = {1, 2, 3} + + expect(function() + removeRange(a, 2, 0) + end).to.throw() + + expect(function() + removeRange(a, 1, -1) + end).to.throw() + end) + + it("should copy the table when then indexes are higher than the list length", function() + local a = {1, 2, 3} + local b = removeRange(a, 4, 7) + + expect(#b).to.equal(3) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + end) + + it("should work when the start index is smaller than 1", function() + local a = {1, 2, 3, 4} + local b = removeRange(a, -5, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(3) + expect(b[2]).to.equal(4) + end) + + it("should work when the end index is greater than the list length", function() + local a = {1, 2, 3, 4} + local b = removeRange(a, 3, 8) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + end) + + it("should work with a None element", function() + local a = {1, None, 3} + local b = removeRange(a, 1, 1) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(None) + expect(b[2]).to.equal(3) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeValue.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeValue.lua new file mode 100644 index 0000000..4930db0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeValue.lua @@ -0,0 +1,18 @@ +--[[ + Creates a new list that has no occurrences of the given value. +]] +local function removeValue(list, value) + local new = {} + local index = 1 + + for i = 1, #list do + if list[i] ~= value then + new[index] = list[i] + index = index + 1 + end + end + + return new +end + +return removeValue \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeValue.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeValue.spec.lua new file mode 100644 index 0000000..705c2ba --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeValue.spec.lua @@ -0,0 +1,43 @@ +return function() + local removeValue = require(script.Parent.removeValue) + local None = require(script.Parent.Parent.None) + + it("should remove the given value", function() + local a = {1, 4, 3} + local b = removeValue(a, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + end) + + it("should remove all occurences of the same given value", function() + local a = {1, 2, 2, 3} + local b = removeValue(a, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + end) + + it("should work with an empty list", function() + local a = removeValue({}, 1) + + expect(a).to.be.a("table") + expect(#a).to.equal(0) + end) + + it("should work with a None element", function() + local a = {1, 2, None, 3} + local b = removeValue(a, 2) + + expect(#b).to.equal(3) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(None) + expect(b[3]).to.equal(3) + + local c = removeValue(a, None) + + expect(c[3]).to.equal(3) + expect(#c).to.equal(3) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/replaceIndex.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/replaceIndex.lua new file mode 100644 index 0000000..6a010f0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/replaceIndex.lua @@ -0,0 +1,22 @@ +--[[ + Returns a new list with the new value replaced at the given index. +]] + +local function replaceIndex(list, index, value) + local new = {} + local len = #list + + assert(index <= len, "index must be less or equal than the list length") + + for i = 1, len do + if i == index then + new[i] = value + else + new[i] = list[i] + end + end + + return new +end + +return replaceIndex \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/replaceIndex.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/replaceIndex.spec.lua new file mode 100644 index 0000000..8e30e53 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/replaceIndex.spec.lua @@ -0,0 +1,52 @@ +return function() + local replaceIndex = require(script.Parent.replaceIndex) + + it("should return a new table", function() + local list = {1, 2, 3} + + expect(replaceIndex(list, 2, 0)).never.to.equal(list) + end) + + it("should not mutate the original list", function() + local list = {false, "foo", 3} + local value = {} + replaceIndex(list, 2, value) + + expect(#list).to.equal(3) + expect(list[1]).to.equal(false) + expect(list[2]).to.equal("foo") + expect(list[3]).to.equal(3) + end) + + it("should replace the value at the given index", function() + local list = {1, 2, 3} + local value = {} + local result = replaceIndex(list, 2, value) + + expect(result[1]).to.equal(1) + expect(result[2]).to.equal(value) + expect(result[3]).to.equal(3) + expect(next(result[2])).to.equal(nil) + end) + + it("should throw if the given index is higher than the list length", function() + local list = {1} + + expect(function() + replaceIndex(list, #list + 1, {}) + end).to.throw() + end) + + it("should be able to replace to a falsy value", function() + local tableElement = {} + local list = {tableElement, false, "value", true} + local newValue = false + + local result = replaceIndex(list, 3, newValue) + + expect(result[1]).to.equal(tableElement) + expect(result[2]).to.equal(false) + expect(result[3]).to.equal(newValue) + expect(result[4]).to.equal(true) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/reverse.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/reverse.lua new file mode 100644 index 0000000..8c24fad --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/reverse.lua @@ -0,0 +1,17 @@ +--[[ + Returns a new list with the reversed order of the given list +]] + +local function reverse(list) + local new = {} + local len = #list + local top = len + 1 + + for i = 1, len do + new[i] = list[top - i] + end + + return new +end + +return reverse \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/reverse.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/reverse.spec.lua new file mode 100644 index 0000000..147ce1d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/reverse.spec.lua @@ -0,0 +1,46 @@ +return function() + local reverse = require(script.Parent.reverse) + + it("should return a new table", function() + local a = {1, 2, 3} + + expect(reverse(a)).never.to.equal(a) + end) + + it("should not mutate the given table", function() + local a = {1, 2, 3} + reverse(a) + + expect(#a).to.equal(3) + expect(a[1]).to.equal(1) + expect(a[2]).to.equal(2) + expect(a[3]).to.equal(3) + end) + + it("should contain the same elements", function() + local a = { + "Foo", + "Bar" + } + local aSet = { + Foo = true, + Bar = true + } + local b = reverse(a) + + expect(#b).to.equal(2) + for _, value in ipairs(b) do + expect(aSet[value]).to.equal(true) + end + end) + + it("should reverse the list", function() + local a = {1, 2, 3, 4} + local b = reverse(a) + + expect(b[1]).to.equal(4) + expect(b[2]).to.equal(3) + expect(b[3]).to.equal(2) + expect(b[4]).to.equal(1) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/sort.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/sort.lua new file mode 100644 index 0000000..a8fc8fd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/sort.lua @@ -0,0 +1,18 @@ +--[[ + Returns a new list, ordered with the given sort callback. + If no callback is given, the default table.sort will be used. +]] + +local function sort(list, callback) + local new = {} + + for i = 1, #list do + new[i] = list[i] + end + + table.sort(new, callback) + + return new +end + +return sort \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/sort.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/sort.spec.lua new file mode 100644 index 0000000..b5c9d1e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/sort.spec.lua @@ -0,0 +1,74 @@ +return function() + local sort = require(script.Parent.sort) + + it("should return a new table", function() + local a = {} + + expect(sort(a)).never.to.equal(a) + end) + + it("should not mutate the given table", function() + local a = {77, "foo", 2} + local function order(first, second) + return tostring(first) < tostring(second) + end + sort(a, order) + + expect(#a).to.equal(3) + expect(a[1]).to.equal(77) + expect(a[2]).to.equal("foo") + expect(a[3]).to.equal(2) + end) + + it("should contain the same elements from the given table", function() + local a = { + "Foo", + "Bar", + "Test" + } + local elementSet = { + Foo = true, + Bar = true, + Test = true + } + local b = sort(a) + + expect(#b).to.equal(3) + for _, value in ipairs(b) do + expect(elementSet[value]).to.equal(true) + end + end) + + it("should sort with the default table.sort when no callback is given", function() + local a = {4, 2, 5, 3, 1} + local b = sort(a) + + table.sort(a) + + expect(#b).to.equal(#a) + for i = 1, #a do + expect(b[i]).to.equal(a[i]) + end + end) + + it("should sort with the given callback", function() + local a = {1, 2, 5, 3, 4} + local function order(first, second) + return first > second + end + local b = sort(a, order) + + table.sort(a, order) + + expect(#b).to.equal(#a) + for i = 1, #a do + expect(b[i]).to.equal(a[i]) + end + end) + + it("should work with an empty table", function() + local a = sort({}) + + expect(#a).to.equal(0) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/toSet.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/toSet.lua new file mode 100644 index 0000000..692dbc9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/toSet.lua @@ -0,0 +1,16 @@ +--[[ + Create a dictionary where each value in the given list corresponds to a key + in the dictionary with a value of true +]] + +local function toSet(list) + local new = {} + + for i = 1, #list do + new[list[i]] = true + end + + return new +end + +return toSet \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/toSet.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/toSet.spec.lua new file mode 100644 index 0000000..f9f06f8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/toSet.spec.lua @@ -0,0 +1,42 @@ +return function() + local toSet = require(script.Parent.toSet) + + it("should return a new table", function() + local a = {1, 2, 3} + + expect(toSet(a)).never.to.equal(a) + end) + + it("should not mutate the given table", function() + local a = {"a", "b", "c"} + toSet(a) + + for k, v in pairs(a) do + if k == 1 then + expect(v).to.equal("a") + elseif k == 2 then + expect(v).to.equal("b") + elseif k == 3 then + expect(v).to.equal("c") + else + error("Extra key was added to table a") + end + end + end) + + it("should have every value in a as a key mapped to true in b", function() + local a = {1, 2, 3, "a", "b", "c"} + local b = toSet(a) + + expect(#b).to.equal(3) + expect(b[1]).to.equal(true) + expect(b[2]).to.equal(true) + expect(b[3]).to.equal(true) + + expect(b[4]).to.equal(nil) + + expect(b["a"]).to.equal(true) + expect(b["b"]).to.equal(true) + expect(b["c"]).to.equal(true) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/None.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/None.lua new file mode 100644 index 0000000..3fc61bd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/None.lua @@ -0,0 +1,15 @@ +--[[ + Represents a value that is intentionally present, but should be interpreted + as `nil`. + + Cryo.None is used by included utilities to make removing values more + ergonomic. +]] + +local None = newproxy(true) + +getmetatable(None).__tostring = function() + return "Cryo.None" +end + +return None \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/None.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/None.spec.lua new file mode 100644 index 0000000..33907f5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/None.spec.lua @@ -0,0 +1,14 @@ +return function() + local None = require(script.Parent.None) + + it("should be a userdata", function() + expect(None).to.be.a("userdata") + end) + + it("should have a nice string name", function() + local coerced = tostring(None) + + expect(coerced:find("^userdata: ")).never.to.be.ok() + expect(coerced:find("None")).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/init.lua new file mode 100644 index 0000000..e952302 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/init.lua @@ -0,0 +1,6 @@ +return { + Dictionary = require(script.Dictionary), + List = require(script.List), + isEmpty = require(script.isEmpty), + None = require(script.None), +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/init.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/init.spec.lua new file mode 100644 index 0000000..ff7e8e3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/init.spec.lua @@ -0,0 +1,5 @@ +return function() + it("should load", function() + require(script.Parent) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/isEmpty.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/isEmpty.lua new file mode 100644 index 0000000..d41de09 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/isEmpty.lua @@ -0,0 +1,5 @@ +local function isEmpty(object) + return next(object) == nil +end + +return isEmpty \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/lock.toml new file mode 100644 index 0000000..97c383c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/cryo" +version = "1.0.0" +commit = "272caa8f3f3b3b29296b462f80b65cc9b1c92f1e" +source = "url+https://github.com/roblox/cryo" diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/enumerate.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/enumerate.lua new file mode 100644 index 0000000..ab1305e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/enumerate.lua @@ -0,0 +1,100 @@ +local function strict(t, name) + name = name or tostring(t) + + return setmetatable(t, { + __index = function(self, key) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + + __newindex = function(self, key, value) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + }) +end + +local function addEnumValue(enumInternal, enumInternalRawValues, enumName, valueName, rawValue) + local isValueNameString = typeof(valueName) == "string" + assert(isValueNameString, "Only string names are supported for enums!") + if isValueNameString then + assert(valueName ~= "fromRawValue", "fromRawValue is reserved") + assert(valueName ~= "isEnumValue", "isEnumValue is reserved") + end + assert(enumInternal[valueName] == nil, "Enum value names can only be used once!") + assert(enumInternalRawValues[valueName] == nil, "Enum values can only be used once!") + + local value = newproxy(true) + local valueMetatable = getmetatable(value) + + valueMetatable.__tostring = function() + return ("%s.%s"):format(enumName, valueName) + end + + valueMetatable.__index = strict({ + rawValue = function() + return rawValue + end + }) + + enumInternal[valueName] = value + enumInternalRawValues[rawValue] = value +end + +local function enumerate(enumName, values) + assert(typeof(enumName) == "string", "Bad argument #1 - enums must be created using a string name!") + assert(typeof(values) == "table", "Bad argument #2 - enums must be created using a table!") + + local enumInternal = {} + local enumInternalRawValues = {} + + -- Allow a list-like syntax for string enums for convenience + if values[1] ~= nil then + for _, valueName in ipairs(values) do + addEnumValue(enumInternal, enumInternalRawValues, enumName, valueName, valueName) + end + else + for valueName, rawValue in pairs(values) do + addEnumValue(enumInternal, enumInternalRawValues, enumName, valueName, rawValue) + end + end + + function enumInternal.fromRawValue(rawValue) + return enumInternalRawValues[rawValue] + end + + function enumInternal.isEnumValue(value) + if typeof(value) ~= "userdata" then + return false + end + + for _, enumValue in pairs(enumInternal) do + if enumValue == value then + return true + end + end + + return nil + end + + local enum = newproxy(true) + local meta = getmetatable(enum) + meta.__index = strict(enumInternal, enumName) + meta.__tostring = function() + return enumName + end + + return enum +end + +return enumerate \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/enumerate.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/enumerate.spec.lua new file mode 100644 index 0000000..9272b65 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/enumerate.spec.lua @@ -0,0 +1,69 @@ +return function() + local enumerate = require(script.Parent.enumerate) + + describe("Any Enum", function() + local AnyEnum = enumerate("AnyEnum", { "A", "B", "C" }) + + it("should be valid", function() + expect(AnyEnum).to.be.ok() + end) + + it("should have the correct name", function() + expect(tostring(AnyEnum)).to.be.equal("AnyEnum") + end) + + it("cannot be altered", function() + expect(function() + AnyEnum.A = "B" + end).to.throw() + end) + + it("should error when accessing an invalid value", function() + expect(function() + local _ = AnyEnum.D + end).to.throw() + end) + + it("should have values that can be compared for equality", function() + expect(AnyEnum.A).to.equal(AnyEnum.A) + expect(AnyEnum.A == AnyEnum.B).to.equal(false) + end) + + it("should have userdata values", function() + expect(typeof(AnyEnum.A)).to.equal("userdata") + end) + + it("should have values with correct rawValue types", function() + expect(typeof(AnyEnum.A.rawValue())).to.equal("string") + end) + + it("should have values with a useful name", function() + expect(tostring(AnyEnum.A)).to.equal("AnyEnum.A") + end) + + it("should have values with correct rawValues", function() + expect(AnyEnum.A.rawValue()).to.equal("A") + end) + + it("should return the correct value from a rawValue", function() + expect(AnyEnum.fromRawValue("A")).to.equal(AnyEnum.A) + end) + + it("should detect whether a value is an enum value", function() + expect(AnyEnum.isEnumValue(AnyEnum.A)).to.equal(true) + expect(AnyEnum.isEnumValue("A")).to.equal(false) + end) + end) + + it("should error when creating an enum with a non-string name", function() + expect(function() + enumerate(1, { "A", "B", "C" }) + end).to.throw() + end) + + it("should error when creating an enum with duplicate values", function() + expect(function() + enumerate(1, { "A", "B", "C", "C" }) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/init.lua new file mode 100644 index 0000000..7edae60 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/init.lua @@ -0,0 +1 @@ +return require(script.enumerate) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/init.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/init.spec.lua new file mode 100644 index 0000000..2a950f0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/init.spec.lua @@ -0,0 +1,7 @@ +return function() + local enumerate = require(script.Parent) + + it("should load a function", function() + expect(typeof(enumerate)).to.equal("function") + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/lock.toml new file mode 100644 index 0000000..5bad4c9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/enumerate" +version = "1.0.0" +commit = "48daaf0df47eaf36c154d691a8320239e4a1312e" +source = "url+https://github.com/roblox/enumerate" diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/Promise.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/Promise.lua new file mode 100644 index 0000000..5dea2b4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/Promise.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["lua-promise"]["lua-promise"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/LinkedList.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/LinkedList.lua new file mode 100644 index 0000000..a34ea50 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/LinkedList.lua @@ -0,0 +1,51 @@ +local LinkedList = {} +LinkedList.__index = LinkedList + +LinkedList.createNode = function(value) + local node = { + previous = nil, + next = nil, + value = value, + } + + setmetatable(node, LinkedList) + + return node +end + +-- Inserts a new node between the 'self' node and its 'next' node. Can also be used to append a node to the end +function LinkedList:CreateNext(value) + local nextNode = { + previous = self, + next = self.next, + value = value or {}, + } + + if self.next then + self.next.previous = nextNode + end + + setmetatable(nextNode, LinkedList) + self.next = nextNode + return nextNode +end + + +-- Inserts a new node between the 'self' node and its 'previous' node. Can also prepend a node to the beginning +function LinkedList:CreatePrevious(value) + local previousNode = { + previous = self.previous, + next = self, + value = value or {}, + } + + if self.previous then + self.previous.next = previousNode + end + + setmetatable(previousNode, LinkedList) + self.previous = previousNode + return previousNode +end + +return LinkedList diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/LinkedList.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/LinkedList.spec.lua new file mode 100644 index 0000000..3294bdb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/LinkedList.spec.lua @@ -0,0 +1,66 @@ +return function() + local LinkedList = require(script.Parent.LinkedList) + + describe("it should initialize properly", function() + local node = LinkedList.createNode(456) + local emptyNode = LinkedList.createNode() + + it("given a value", function() + expect(node.value).to.equal(456) + end) + + it("given nothing", function() + expect(emptyNode.value).to.equal(nil) + end) + end) + + describe("it should properly link nodes", function() + it("when calling createNext on the end of a list", function() + local nodeA = LinkedList.createNode("a") + local nodeB = nodeA:CreateNext("b") + + expect(nodeA.next).to.equal(nodeB) + expect(nodeA.next.value).to.equal(nodeB.value) + + expect(nodeB.previous).to.equal(nodeA) + expect(nodeB.previous.value).to.equal(nodeA.value) + end) + + it("when calling createNext inbetween Nodes", function() + local nodeA = LinkedList.createNode("a") + local nodeC = nodeA:CreateNext("c") + local nodeB = nodeA:CreateNext("b") + + expect(nodeA.next).to.equal(nodeB) + expect(nodeB.previous).to.equal(nodeA) + expect(nodeB.next).to.equal(nodeC) + expect(nodeC.previous).to.equal(nodeB) + end) + + it("when calling createPrevious on the beginning of a list", function() + local nodeA = LinkedList.createNode("a") + local nodeZ = nodeA:CreatePrevious("z") + + expect(nodeA.previous).to.equal(nodeZ) + expect(nodeA.previous.value).to.equal(nodeZ.value) + + expect(nodeZ.next).to.equal(nodeA) + expect(nodeZ.next.value).to.equal(nodeA.value) + end) + + it("when calling createPrevious inbetween Nodes", function() + local nodeA = LinkedList.createNode("a") + local nodeY = nodeA:CreatePrevious("y") + local nodeZ = nodeA:CreatePrevious("z") + + expect(nodeA.previous).to.equal(nodeZ) + expect(nodeZ.previous).to.equal(nodeY) + expect(nodeZ.next).to.equal(nodeA) + expect(nodeY.next).to.equal(nodeZ) + end) + + + + end) + +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/Paginator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/Paginator.lua new file mode 100644 index 0000000..c544be8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/Paginator.lua @@ -0,0 +1,106 @@ +--[[ + Make sure to read api-reference.md for how to use. +--]] + +local dependencies = require(script.Parent.dependencies) +local t = dependencies.t +local LinkedList = dependencies.LinkedList +local Promise = dependencies.Promise + +local Paginator = { + llRoot = nil, + llIndex = nil, + _isFetching = false, +} +Paginator.__index = Paginator + +local requiredProps = t.strictInterface({ + pageSize = t.number, + fetchWithCursor = t.callback, + fetchInit = t.callback, +}) + +Paginator.new = function(props) + assert(requiredProps(props)) + local self = {} + for k, v in pairs(props) do + self[k] = v + end + setmetatable(self, Paginator) + + self:_init() + return self +end + +function Paginator:_init() + -- we don't know our currentCursor... just our previous and next + self._isFetching = true + return self.fetchInit():andThen(function(previousCursor, nextCursor) + self.llRoot = LinkedList.createNode() + self.llIndex = self.llRoot + self.llIndex:CreatePrevious(previousCursor) + self.llIndex:CreateNext(nextCursor) + self._isFetching = false + end) + +end + +function Paginator:getCurrent() + return self.llIndex.value or {} +end + +function Paginator:getNext() + if self._isFetching then + return Promise.reject("Paginator is currently busy. Please wait.") + end + + local cursor = self.llIndex.next.value or "" + + if cursor == "" then + return Promise.reject("Next cursor is invalid") + end + + self.llIndex = self.llIndex.next + self._isFetching = true + + return self.fetchWithCursor(cursor):andThen(function(previousCursor, nextCursor) + self.llIndex.previous.value = previousCursor + if self.llIndex.next then + self.llIndex.next.value = nextCursor + else + self.llIndex:CreateNext(nextCursor) + end + self._isFetching = false + end) +end + +function Paginator:getPrevious() + if self._isFetching then + return Promise.reject("Paginator is currently busy. Please wait.") + end + + local cursor = self.llIndex.previous.value or "" + + if cursor == "" then + return Promise.reject("Previous cursor is invalid") + end + + self.llIndex = self.llIndex.previous + self._isFetching = true + + return self.fetchWithCursor(cursor, true):andThen(function(previousCursor, nextCursor) + self.llIndex.next.value = nextCursor + if self.llIndex.previous then + self.llIndex.previous.value = previousCursor + else + self.llIndex:CreatePrevious(previousCursor) + end + self._isFetching = false + end) +end + +function Paginator:isFetching() + return self._isFetching +end + +return Paginator diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/Paginator.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/Paginator.spec.lua new file mode 100644 index 0000000..46e400a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/Paginator.spec.lua @@ -0,0 +1,200 @@ +local dependencies = require(script.Parent.dependencies) +local Promise = dependencies.Promise + +return function() + local Paginator = require(script.Parent.Paginator) + + describe("it should initialize properly", function() + local mockFetchInit = function(params) + return function() + return Promise.resolve():andThen(function() + return params-1, params+1 + end) + end + end + + local mockFetchWithCursor = function(cursor) + return Promise.resolve():andThen(function() + local newPrevCursor = cursor - 1 + local newNextCursor = cursor + 1 + return newPrevCursor, newNextCursor + end) + end + + local startCursor = 4 + + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + it("given proper values", function() + expect(paginator).to.be.ok() + end) + end) + + describe("it should return the correct cursors", function() + local mockFetchInit = function(params) + return function() + return Promise.resolve():andThen(function() + return params-1, params+1 + end) + end + end + + local mockFetchWithCursor = function(cursor) + return Promise.resolve():andThen(function() + local newPrevCursor = cursor - 1 + local newNextCursor = cursor + 1 + return newPrevCursor, newNextCursor + end) + end + + local startCursor = 4 + + it("when fetching next", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getNext() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor + 1) + end) + + it("when fetching previous", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getPrevious() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor - 1) + end) + + it("when fetching next then previous", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getNext() + paginator:getPrevious() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor) + end) + + it("when fetching previous then next", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getPrevious() + paginator:getNext() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor) + end) + + it("when fetching next next previous", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getPrevious() + paginator:getNext() + paginator:getNext() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor + 1) + end) + + it("when fetching previous previous next", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getPrevious() + paginator:getPrevious() + paginator:getNext() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor - 1) + end) + + it("when fetching previous next next", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getPrevious() + paginator:getNext() + paginator:getNext() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor + 1) + end) + + it("when fetching next previous previous", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getNext() + paginator:getPrevious() + paginator:getPrevious() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor - 1) + end) + + it("when fetching next next next", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getNext() + paginator:getNext() + paginator:getNext() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor + 3) + end) + + it("when fetching previous previous previous", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getPrevious() + paginator:getPrevious() + paginator:getPrevious() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor - 3) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/dependencies.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/dependencies.lua new file mode 100644 index 0000000..03130a7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/dependencies.lua @@ -0,0 +1,8 @@ +local ROOT = script.Parent +local Packages = script:FindFirstAncestor("Packages") + +return { + t = require(Packages.t), + LinkedList = require(ROOT.LinkedList), + Promise = require(Packages.Promise), +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/init.lua new file mode 100644 index 0000000..4b6e3f8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/init.lua @@ -0,0 +1,8 @@ +-- Generator information: +-- Human name: GenericPagination +-- Variable name: GenericPagination +-- Repo name: genericpagination + +local Paginator = require(script.Paginator) + +return Paginator \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/inspect.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/inspect.lua new file mode 100644 index 0000000..656d247 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/inspect.lua @@ -0,0 +1,333 @@ +local inspect ={ + _VERSION = 'inspect.lua 3.1.0', + _URL = 'http://github.com/kikito/inspect.lua', + _DESCRIPTION = 'human-readable representations of tables', + _LICENSE = [[ + MIT LICENSE + + Copyright (c) 2013 Enrique García Cota + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ]] + } + + local tostring = tostring + + inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) + inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) + + local function rawpairs(t) + return next, t, nil + end + + -- Apostrophizes the string if it has quotes, but not aphostrophes + -- Otherwise, it returns a regular quoted string + local function smartQuote(str) + if str:match('"') and not str:match("'") then + return "'" .. str .. "'" + end + return '"' .. str:gsub('"', '\\"') .. '"' + end + + -- \a => '\\a', \0 => '\\0', 31 => '\31' + local shortControlCharEscapes = { + ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", + ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" + } + local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031 + for i=0, 31 do + local ch = string.char(i) + if not shortControlCharEscapes[ch] then + shortControlCharEscapes[ch] = "\\"..i + longControlCharEscapes[ch] = string.format("\\%03d", i) + end + end + + local function escape(str) + return (str:gsub("\\", "\\\\") + :gsub("(%c)%f[0-9]", longControlCharEscapes) + :gsub("%c", shortControlCharEscapes)) + end + + local function isIdentifier(str) + return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) + end + + local function isSequenceKey(k, sequenceLength) + return type(k) == 'number' + and 1 <= k + and k <= sequenceLength + and math.floor(k) == k + end + + local defaultTypeOrders = { + ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, + ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 + } + + local function sortKeys(a, b) + local ta, tb = type(a), type(b) + + -- strings and numbers are sorted numerically/alphabetically + if ta == tb and (ta == 'string' or ta == 'number') then return a < b end + + local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] + -- Two default types are compared according to the defaultTypeOrders table + if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] + elseif dta then return true -- default types before custom ones + elseif dtb then return false -- custom types after default ones + end + + -- custom types are sorted out alphabetically + return ta < tb + end + + -- For implementation reasons, the behavior of rawlen & # is "undefined" when + -- tables aren't pure sequences. So we implement our own # operator. + local function getSequenceLength(t) + local len = 1 + local v = rawget(t,len) + while v ~= nil do + len = len + 1 + v = rawget(t,len) + end + return len - 1 + end + + local function getNonSequentialKeys(t) + local keys, keysLength = {}, 0 + local sequenceLength = getSequenceLength(t) + for k,_ in rawpairs(t) do + if not isSequenceKey(k, sequenceLength) then + keysLength = keysLength + 1 + keys[keysLength] = k + end + end + table.sort(keys, sortKeys) + return keys, keysLength, sequenceLength + end + + local function countTableAppearances(t, tableAppearances) + tableAppearances = tableAppearances or {} + + if type(t) == 'table' then + if not tableAppearances[t] then + tableAppearances[t] = 1 + for k,v in rawpairs(t) do + countTableAppearances(k, tableAppearances) + countTableAppearances(v, tableAppearances) + end + countTableAppearances(getmetatable(t), tableAppearances) + else + tableAppearances[t] = tableAppearances[t] + 1 + end + end + + return tableAppearances + end + + local copySequence = function(s) + local copy, len = {}, #s + for i=1, len do copy[i] = s[i] end + return copy, len + end + + local function makePath(path, ...) + local keys = {...} + local newPath, len = copySequence(path) + for i=1, #keys do + newPath[len + i] = keys[i] + end + return newPath + end + + local function processRecursive(process, item, path, visited) + if item == nil then return nil end + if visited[item] then return visited[item] end + + local processed = process(item, path) + if type(processed) == 'table' then + local processedCopy = {} + visited[item] = processedCopy + local processedKey + + for k,v in rawpairs(processed) do + processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) + if processedKey ~= nil then + processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) + end + end + + local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) + if type(mt) ~= 'table' then mt = nil end -- ignore not nil/table __metatable field + setmetatable(processedCopy, mt) + processed = processedCopy + end + return processed + end + + + + ------------------------------------------------------------------- + + local Inspector = {} + local Inspector_mt = {__index = Inspector} + + function Inspector:puts(...) + local args = {...} + local buffer = self.buffer + local len = #buffer + for i=1, #args do + len = len + 1 + buffer[len] = args[i] + end + end + + function Inspector:down(f) + self.level = self.level + 1 + f() + self.level = self.level - 1 + end + + function Inspector:tabify() + self:puts(self.newline, string.rep(self.indent, self.level)) + end + + function Inspector:alreadyVisited(v) + return self.ids[v] ~= nil + end + + function Inspector:getId(v) + local id = self.ids[v] + if not id then + local tv = type(v) + id = (self.maxIds[tv] or 0) + 1 + self.maxIds[tv] = id + self.ids[v] = id + end + return tostring(id) + end + + function Inspector:putKey(k) + if isIdentifier(k) then return self:puts(k) end + self:puts("[") + self:putValue(k) + self:puts("]") + end + + function Inspector:putTable(t) + if t == inspect.KEY or t == inspect.METATABLE then + self:puts(tostring(t)) + elseif self:alreadyVisited(t) then + self:puts('
') + elseif self.level >= self.depth then + self:puts('{...}') + else + if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end + + local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t) + local mt = getmetatable(t) + + self:puts('{') + self:down(function() + local count = 0 + for i=1, sequenceLength do + if count > 0 then self:puts(',') end + self:puts(' ') + self:putValue(t[i]) + count = count + 1 + end + + for i=1, nonSequentialKeysLength do + local k = nonSequentialKeys[i] + if count > 0 then self:puts(',') end + self:tabify() + self:putKey(k) + self:puts(' = ') + self:putValue(t[k]) + count = count + 1 + end + + if type(mt) == 'table' then + if count > 0 then self:puts(',') end + self:tabify() + self:puts(' = ') + self:putValue(mt) + end + end) + + if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing } + self:tabify() + elseif sequenceLength > 0 then -- array tables have one extra space before closing } + self:puts(' ') + end + + self:puts('}') + end + end + + function Inspector:putValue(v) + local tv = type(v) + + if tv == 'string' then + self:puts(smartQuote(escape(v))) + elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or + tv == 'cdata' or tv == 'ctype' then + self:puts(tostring(v)) + elseif tv == 'table' then + self:putTable(v) + else + self:puts('<', tv, ' ', self:getId(v), '>') + end + end + + ------------------------------------------------------------------- + + function inspect.inspect(root, options) + options = options or {} + + local depth = options.depth or math.huge + local newline = options.newline or '\n' + local indent = options.indent or ' ' + local process = options.process + + if process then + root = processRecursive(process, root, {}, {}) + end + + local inspector = setmetatable({ + depth = depth, + level = 0, + buffer = {}, + ids = {}, + maxIds = {}, + newline = newline, + indent = indent, + tableAppearances = countTableAppearances(root) + }, Inspector_mt) + + inspector:putValue(root) + + return table.concat(inspector.buffer) + end + + setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) + + return inspect diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/publicApi.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/publicApi.spec.lua new file mode 100644 index 0000000..5dd2c1d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/publicApi.spec.lua @@ -0,0 +1,7 @@ +return function() + it("SHOULD return a valid, constructable object", function() + local api = require(script.Parent) + expect(api).to.be.ok() + expect(api.new).to.be.ok() + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/lock.toml new file mode 100644 index 0000000..15db0fa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/lock.toml @@ -0,0 +1,9 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/genericpagination" +version = "0.1.0" +commit = "6cc56178d99b731a71f6920ace8b3f3d4aaf5235" +source = "git+https://github.rbx.com/roblox/genericpagination#master" +dependencies = [ + "Promise lua-promise bbb9e162 git+https://github.rbx.com/roblox/lua-promise#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/t.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/FitFrame.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/FitFrame.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/FitFrame.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Otter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Roact.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/KeyPool.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/KeyPool.lua new file mode 100644 index 0000000..55d6006 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/KeyPool.lua @@ -0,0 +1,80 @@ +--[[ + KeyPool provides a pool of objects suitable for use as map keys. + + Create a new KeyPool, then call pool:get() to get a new key. Once you're done with it, call key:release(). + + Example: + local pool = KeyPool.new("foo") + ... + local key1 = pool:get() + local key2 = pool:get() + map[key1] = thing1 + map[key2] = thing2 + ... + map[key1] = nil + key1:release() + key1 = nil +]] +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local t = require(Root.t) + +-- Forward declarations +local KeyPool = {} +KeyPool.__index = KeyPool + +local Key = {} +Key.__index = Key + +-- This is Key.new, but we don't want to expose that publicly. +local function newkey(pool, index) + local key = { + pool = pool, + index = index, + } + + setmetatable(key, Key) + return key +end + +-- KeyPool functions + +function KeyPool.new(class) + assert(t.string(class)) + + local pool = { + class = class, + available = {}, + limit = 0, + count = 0, + } + + setmetatable(pool, KeyPool) + return pool +end + +-- Get a currently unused key, or create a new one if everything is in use. +function KeyPool:get() + if self.count == 0 then + self.limit = self.limit + 1 + return newkey(self, self.limit) + end + + local key = self.available[self.count] + self.count = self.count - 1 + return key +end + +-- Key functions + +function Key:__tostring() + return self.pool.class .. "_" .. tostring(self.index) +end + +-- Return this key to the pool it came from. Whatever previously held this key should not keep the reference after +-- calling this. +function Key:release() + self.pool.count = self.pool.count + 1 + self.pool.available[self.pool.count] = self +end + +return KeyPool diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/NotifyReady.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/NotifyReady.lua new file mode 100644 index 0000000..a564707 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/NotifyReady.lua @@ -0,0 +1 @@ +return {} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Orientation.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Orientation.lua new file mode 100644 index 0000000..a31399b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Orientation.lua @@ -0,0 +1,31 @@ +-- Enum for specifying the leading edge of the scroller. +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local t = require(Root.t) + +local Orientation = { + Up = "Orientation.Up", + Down = "Orientation.Down", + Left = "Orientation.Left", + Right = "Orientation.Right", +} + +local metaindex = { + isOrientation = t.union( + t.literal(Orientation.Up), + t.literal(Orientation.Down), + t.literal(Orientation.Left), + t.literal(Orientation.Right) + ) +} + +setmetatable(Orientation, { + __index = function(self, key) + return metaindex[key] or + error(tostring(key) .. " is not a valid member of Scroller.Orientation", 2) + end, + __newindex = function() + error("Scroller.Orientation is read-only", 2) + end, +}) + +return Orientation diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Round.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Round.lua new file mode 100644 index 0000000..2755954 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Round.lua @@ -0,0 +1,28 @@ +local epsilon = 1e-15 + +return { + nearest = function(num) + local q, r = math.modf(num) + if r <= -0.5 then + return q - 1 + elseif r >= 0.5 then + return q + 1 + else + return q + end + end, + towardsZero = function(num) + local result, _ = math.modf(num) + return result + end, + awayFromZero = function(num) + local q, r = math.modf(num) + if r < -epsilon then + return q - 1 + elseif r > epsilon then + return q + 1 + else + return q + end + end, +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Scroller.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Scroller.lua new file mode 100644 index 0000000..2536967 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Scroller.lua @@ -0,0 +1,866 @@ +local RunService = game:GetService("RunService") + +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local t = require(Root.t) + +local FitFrame = require(Root.FitFrame).FitFrameOnAxis +local findNewIndices = require(script.Parent.findNewIndices) +local Round = require(script.Parent.Round) +local KeyPool = require(script.Parent.KeyPool) + +local NotifyReady = require(script.Parent.NotifyReady) + +local debugPrint = function() end + +local Scroller = Roact.PureComponent:extend("Scroller") + +Scroller.Orientation = require(script.Parent.Orientation) + +local isVertical = { + [Scroller.Orientation.Up] = true, + [Scroller.Orientation.Down] = true, + [Scroller.Orientation.Left] = false, + [Scroller.Orientation.Right] = false, +} + +local isReverse = { + [Scroller.Orientation.Up] = true, + [Scroller.Orientation.Down] = false, + [Scroller.Orientation.Left] = true, + [Scroller.Orientation.Right] = false, +} + +local direction = { + [Scroller.Orientation.Up] = -1, + [Scroller.Orientation.Down] = 1, + [Scroller.Orientation.Left] = -1, + [Scroller.Orientation.Right] = 1, +} + +Scroller.validateProps = t.strictInterface({ + -- Required. The list of items to scroll through. + itemList = t.array(t.any), + + -- Required. A callback function, called with each visible item in the itemList when the list is rendered. + renderItem = t.callback, + + -- A function to uniquely identify list items. Calling this on the same item twice should give the same result + -- accoring to ==. + identifier = t.optional(t.callback), + + -- One of the Scroller.Orientation enums. Determines the leading edge of the infinite scroll. + orientation = t.optional(Scroller.Orientation.isOrientation), + + -- A callback function, called when the infinite scroll reaches the leading end of the itemList (index + -- #itemList). + loadNext = t.optional(t.callback), + + -- A callback function, called when the infinite scroll reaches the trailing end of the itemList (index 1). + loadPrevious = t.optional(t.callback), + + -- Padding between elements in the scrolling frame. The Scale is relative to the size of the scrolling frame. + padding = t.optional(t.UDim), + + -- The minimum number of unmounted elements to keep at the top and bottom of the list. If there are fewer than + -- this call loadNext or loadPrevious. + loadingBuffer = t.optional(t.numberPositive), + + -- The amount of space above and below the view to render items in. + mountingBuffer = t.optional(t.numberPositive), + + -- The amount of empty space to keep at the top and bottom on the scroll. + dragBuffer = t.optional(t.numberPositive), + + -- An initial guess at the average size of an item. + estimatedItemSize = t.optional(t.numberPositive), + + -- The maximum distance to search for moved elements. + maximumSearchDistance = t.optional(t.numberPositive), + + -- The element to put in focus initially. + focusIndex = t.optional(t.integer), + + -- An arbitrary value to prevent the list from refocusing every render. Change this to cause the list to reset + -- and refocus on the new focusIndex. + focusLock = t.optional(t.any), + + -- The position within the view to keep still as other things move. The Scale is relative to the size of the + -- scrolling frame. + anchorLocation = t.optional(t.UDim), + + ---- INTERNAL ONLY ---- + [NotifyReady] = t.any, +}) + +-- Default values for all the infinite-scroller-specific props. Any prop not in this list will be passed on to the +-- underlying ScrollingFrame. +Scroller.defaultProps = { + itemList = {}, + renderItem = {}, + identifier = function(item) + return item + end, + orientation = Scroller.Orientation.Down, + loadNext = false, + loadPrevious = false, + padding = UDim.new(0, 0), + loadingBuffer = 10, + mountingBuffer = 200, + dragBuffer = 100, + estimatedItemSize = 50, + maximumSearchDistance = 100, + focusIndex = 1, + focusLock = {}, + anchorLocation = UDim.new(0, 0), + [NotifyReady] = false, +} + +function Scroller:render() + debugPrint("render") + + -- Gather vertical/horizontal specific variables. + local axis = isVertical[self.props.orientation] and { + fillDirection = Enum.FillDirection.Vertical, + fitDirection = FitFrame.Axis.Vertical, + minimumSize = UDim2.new(1, 0, 0, 0), + canvasSize = UDim2.new(0, 0, 0, self.state.size), + paddingSide = "PaddingTop", + } or { + fillDirection = Enum.FillDirection.Horizontal, + fitDirection = FitFrame.Axis.Horizontal, + minimumSize = UDim2.new(0, 0, 1, 0), + canvasSize = UDim2.new(0, self.state.size, 0, 0), + paddingSide = "PaddingLeft", + } + + -- Remove non-standard props from list to pass on to ScrollingFrame. These are the same props given in + -- defaultProps. + local props = Cryo.Dictionary.join( + self.props, + self.propsToClear, + { + CanvasSize = axis.canvasSize, + [Roact.Change.CanvasPosition] = self.onScroll, + [Roact.Change.AbsoluteSize] = self.onResize, + [Roact.Ref] = self:getRef(), + } + ) + + local children = { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = axis.fillDirection, + Padding = UDim.new(0, self.itemPadding), + [Roact.Change.AbsoluteContentSize] = self.onContentResize, + }), + padding = Roact.createElement("UIPadding", { + [axis.paddingSide] = UDim.new(0, self.state.padding), + }), + } + + -- Trailing and leading indicies won't be set if this isn't true. + if self.state.ready and not Cryo.isEmpty(self.props.itemList) then + debugPrint(" Rendering elements between", self.state.trail.index, "and", self.state.lead.index) + for n = self.state.trail.index, self.state.lead.index do + local metadata = self:getMetadata(n) + children[metadata.name] = Roact.createElement(FitFrame, { + minimumSize = axis.minimumSize, + axis = axis.fitDirection, + FillDirection = axis.fillDirection, + BackgroundTransparency = 1, + LayoutOrder = isReverse[self.props.orientation] and -n or n, + [Roact.Ref] = metadata.ref, + }, { + item = self.props.renderItem(self.props.itemList[n], false), + }) + end + end + + return Roact.createElement("ScrollingFrame", props, children) +end + +function Scroller:willUpdate(nextProps, nextState) + debugPrint("willUpdate") + + if not nextState.ready then + return + end + + self.sizeDebounce = true + + local deletions = {} + local additions = {} + + if not Cryo.isEmpty(self.props.itemList) and self.state.lead then + for n = self.state.trail.index, self.state.lead.index do + local id = self.props.identifier(self.props.itemList[n]) + deletions[id] = true + end + end + + if not Cryo.isEmpty(nextProps.itemList) and nextState.lead then + for n = nextState.trail.index, nextState.lead.index do + local item = nextProps.itemList[n] + local id = nextProps.identifier(item) + if deletions[id] then + -- Element is in both ranges. + deletions[id] = nil + else + additions[id] = item + end + end + end + + -- Clear names first, so new items can use them. + for id, _ in pairs(deletions) do + self:clearMetadata(id) + end + for id, item in pairs(additions) do + self:updateMetadata(id, item, nextProps) + end + + -- The focus lock changed, clear the non-state anchor variables. + if self.state.lastFocusLock ~= nextState.lastFocusLock then + self.anchorFramePosition = 0 + self.anchorCanvasPosition = self.relativeAnchorLocation + end +end + +function Scroller:didUpdate(previousProps, previousState) + debugPrint("didUpdate") + + if Cryo.isEmpty(self.props.itemList) then + return + end + + if not self.state.ready then + self.onResize(self:getRef().current) + return + end + + if not self:adjustCanvas(self.scrollingForward, self.scrollingBackward) then + self:moveToAnchor() + self:loadMore() + self.sizeDebounce = false + end +end + +function Scroller.getDerivedStateFromProps(nextProps, lastState) + debugPrint("getDerivedStateFromProps") + if not lastState.ready or Cryo.isEmpty(nextProps.itemList) then + return + end + + local listSize = #nextProps.itemList + + -- Reset the state if the focus lock changes. This is guaranteed to be true the first time. + if lastState.lastFocusLock ~= nextProps.focusLock then + debugPrint(" Resetting focus") + local focusID = nextProps.identifier(nextProps.itemList[nextProps.focusIndex]) + return { + listSize = listSize, + trail = {index=nextProps.focusIndex, id=focusID}, + anchor = {index=nextProps.focusIndex, id=focusID}, + lead = {index=nextProps.focusIndex, id=focusID}, + padding = 0, + size = 0, + lastFocusLock = nextProps.focusLock, + } + end + + local trailIndex, anchorIndex, leadIndex = findNewIndices(nextProps, lastState) + debugPrint(" Trailing index moved from", lastState.trail.index, "to", trailIndex) + debugPrint(" Anchor index moved from", lastState.anchor.index, "to", anchorIndex) + debugPrint(" Leading index moved from", lastState.lead.index, "to", leadIndex) + + -- There are 8 possibilities here as any combination of these could be deleted. Also, we can't use findIndexAt + -- here since that requires access to the children's measurements. + if not anchorIndex then + if leadIndex and trailIndex then + -- Estimate that the new anchor is proportionally the same distance from the lead and trail indices. + if leadIndex == trailIndex then + -- Guard against divide by zero. + anchorIndex = leadIndex + else + local oldRatio = (lastState.anchor.index - lastState.lead.index) + / (lastState.trail.index - lastState.lead.index) + anchorIndex = Round.nearest((trailIndex - leadIndex) * oldRatio + leadIndex) + anchorIndex = math.min(math.max(anchorIndex, 1), listSize) + end + elseif leadIndex then + -- Given only the new leading index, estimate that the new anchor is the same distance away as it was. + anchorIndex = leadIndex + lastState.anchor.index - lastState.lead.index + anchorIndex = math.min(math.max(anchorIndex, 1), listSize) + elseif trailIndex then + -- Given only the new trailing index, estimate that the new anchor is the same distance away as it was. + anchorIndex = trailIndex + lastState.anchor.index - lastState.trail.index + anchorIndex = math.min(math.max(anchorIndex, 1), listSize) + else + -- Everything is gone. Just reuse the same index if that's still within the bounds of the list. + anchorIndex = math.min(math.max(lastState.anchor.index, 1), listSize) + end + debugPrint(" Anchor index moved to", anchorIndex) + end + + -- If the leading and trailing indices haven't been worked out yet, estimate that the new ones should be the + -- same distance from the anchor as the old ones were. + if not trailIndex then + trailIndex = anchorIndex + lastState.trail.index - lastState.anchor.index + trailIndex = math.min(math.max(trailIndex, 1), listSize) + debugPrint(" Trailing index moved to", trailIndex) + end + if not leadIndex then + leadIndex = anchorIndex + lastState.lead.index - lastState.anchor.index + leadIndex = math.min(math.max(leadIndex, 1), listSize) + debugPrint(" Leading index moved to", leadIndex) + end + + local trailID = nextProps.identifier(nextProps.itemList[trailIndex]) + local anchorID = nextProps.identifier(nextProps.itemList[anchorIndex]) + local leadID = nextProps.identifier(nextProps.itemList[leadIndex]) + + return { + listSize = listSize, + trail = {index=trailIndex, id=trailID}, + anchor = {index=anchorIndex, id=anchorID}, + lead = {index=leadIndex, id=leadID}, + } +end + +function Scroller:init() + debugPrint("init") + + -- Only self:getRef() should access this. + self._ref = Roact.createRef() + + self.scrollDebounce = false + self.sizeDebounce = true + + self.onScroll = function(rbx) + debugPrint("onScroll") + debugPrint(" CanvasPosition is", rbx.CanvasPosition) + if self.scrollDebounce then + debugPrint(" Debouncing scroll") + return + end + + local delta, newState = self:recalculateAnchor() + self.scrollingBackward = delta < 0 + self.scrollingForward = delta > 0 + debugPrint(" Delta is", delta) + + if not Cryo.isEmpty(newState) then + self:setState(newState) + end + + -- Handle any passed in scroll callback. + if self.props[Roact.Change.CanvasPosition] then + self.props[Roact.Change.CanvasPosition](rbx) + end + end + + self.onResize = function(rbx) + debugPrint("onResize") + local size = self:measure(rbx.AbsoluteSize) + local pos = self:measure(rbx.AbsolutePosition) + + self.itemPadding = self.props.padding.Scale * size + self.props.padding.Offset + if isReverse[self.props.orientation] then + self.relativeAnchorLocation = self.props.anchorLocation.Scale * size + self.props.anchorLocation.Offset + else + self.relativeAnchorLocation = (1 - self.props.anchorLocation.Scale) * size + - self.props.anchorLocation.Offset + end + self.absoluteAnchorLocation = self.relativeAnchorLocation + pos + self.mountAboveAnchor = self.relativeAnchorLocation + self.props.mountingBuffer + self.mountBelowAnchor = size - self.relativeAnchorLocation + self.props.mountingBuffer + + -- Handle any passed in resize callback. + if self.props[Roact.Change.AbsoluteSize] then + self.props[Roact.Change.AbsoluteSize](rbx) + end + + if not self.state.ready then + debugPrint(" Setting initial anchor position to", self.relativeAnchorLocation) + self.anchorFramePosition = 0 + self.anchorCanvasPosition = self.relativeAnchorLocation + + coroutine.wrap(function() + RunService.Heartbeat:Wait() + self:setState({ + ready = true + }) + + -- This should only be set by tests. + if self.props[NotifyReady] then + self.props[NotifyReady]:Fire() + end + end)() + else + self:moveToAnchor() + self:setState({}) -- Force a rerender. + end + end + + self.onContentResize = function() + debugPrint("onContentResize") + + if self.sizeDebounce or not self.state.ready then + debugPrint(" Skipping onContentResize") + return + end + + self:moveToAnchor() + self:setState({}) -- Force a rerender. + end + + self.anchorCanvasPosition = 0 + self.anchorFramePosition = 0 + + self.metadata = {} + self.pools = {} + self.refpool = {} + + self.scrollingBackward = false + self.scrollingForward = false + + -- Store the list of props to not pass on to the underlying scrolling frame. + self.propsToClear = {} + for k, _ in pairs(Scroller.defaultProps) do + self.propsToClear[k] = Cryo.None + end + + -- This will get updated shortly, but one render will happen before state.ready is set + self.state = { + ready = false, + lastFocusLock = nil, + padding = 0, + size = 0, + } +end + +-- Find which element is currently closest to the anchor position. +function Scroller:recalculateAnchor() + debugPrint("recalculateAnchor") + + -- Find the index of the element at the appropriate position + local index = self:findIndexAt( + self:absoluteToCanvasPosition(self.absoluteAnchorLocation), self.state.anchor.index, false) + + local delta + if index == self.state.anchor.index then + debugPrint(" Current anchor still works") + return 0, {} + elseif index < self.state.anchor.index then + delta = -1 + else + delta = 1 + end + + debugPrint(" New anchor at index", index) + + -- Store the new anchor's details + self.anchorCanvasPosition = self:getAnchorCanvasFromIndex(index) + self.anchorFramePosition = self:getAnchorFrameFromIndex(index) + debugPrint(" New anchor at canvas position", self.anchorCanvasPosition) + debugPrint(" New anchor at frame position", self.anchorFramePosition) + return delta, { + anchor = {index=index, id=self:getID(index)}, + } +end + +-- Move all the rendered elements up or down to put the anchor back where it was. +function Scroller:resetAnchorPosition() + debugPrint("resetAnchorPosition") + debugPrint(" Anchor index is", self.state.anchor.index) + local offset = self:getAnchorCanvasPosition() + debugPrint(" Anchor is at", offset) + debugPrint(" Anchor should be at", self.anchorCanvasPosition) + local diff = math.floor(self.anchorCanvasPosition - offset + 0.5) + if diff ~= 0 then + debugPrint(" Changing padding from", self.state.padding, "to", (self.state.padding + diff)) + return {padding = self.state.padding + diff} + else + return {} + end +end + +-- Get the current padding from the UIPadding child. +function Scroller:getCurrentPadding() + local pad = self:getCurrent().padding + -- Only one of these will be non-zero + return pad.PaddingTop.Offset + pad.PaddingLeft.Offset +end + +-- Move the top and bottom of the range to be rendered up and down to make sure enough things are being rendered. +function Scroller:recalculateBounds(trimTrailing, trimLeading) + debugPrint("recalculateBounds") + debugPrint(" Leading index was", self.state.lead.index) + debugPrint(" Trailing index was", self.state.trail.index) + + local anchorPos = self:getAnchorCanvasPosition() + local mountTop = anchorPos - self.mountAboveAnchor + local mountBottom = anchorPos + self.mountBelowAnchor + debugPrint(" Target for top at", mountTop) + debugPrint(" Target for bottom at", mountBottom) + + local topIndex = self:findIndexAt(mountTop, nil, true) + debugPrint(" Found new top index at", topIndex) + local bottomIndex = self:findIndexAt(mountBottom, nil, true) + debugPrint(" Found new bottom index at", bottomIndex) + + local leadIndex = math.max(topIndex, bottomIndex) + if leadIndex < self.state.lead.index and not trimLeading then + leadIndex = self.state.lead.index + end + + local trailIndex = math.min(topIndex, bottomIndex) + if trailIndex > self.state.trail.index and not trimTrailing then + trailIndex = self.state.trail.index + end + + if trailIndex < self.state.trail.index or leadIndex > self.state.lead.index then + debugPrint(" Changing leading index to", leadIndex) + debugPrint(" Changing trailing index to", trailIndex) + return { + trail = {index=trailIndex, id=self:getID(trailIndex)}, + lead = {index=leadIndex, id=self:getID(leadIndex)}, + } + else + return {} + end +end + +-- Find the index of the element that overlaps the given canvas-relative position. +function Scroller:findIndexAt(targetPos, hintIndex, extrapolate) + debugPrint(" findIndexAt") + -- Get the distance from the hinted index or the anchor. + local currentIndex = hintIndex or self.state.anchor.index + local currentDist = self:distanceToPosition(currentIndex, targetPos) + debugPrint(" Searching from index", currentIndex) + debugPrint(" Position is", currentDist, "from", targetPos) + if currentDist == 0 then + return currentIndex + end + + -- Get the distance from one end of the list. + local nextIndex = (currentDist < 0) and self.state.trail.index or self.state.lead.index + debugPrint(" Nearest end at", nextIndex) + if currentIndex == nextIndex then + debugPrint(" Hint index already at end") + -- If the target position lies outside of the loaded elements. + if currentIndex + currentDist < self.state.trail.index + or currentIndex + currentDist > self.state.lead.index then + debugPrint(" Target out of bounds") + if not extrapolate then + -- Do not extrapolate. Return the closest loaded element. + return currentIndex + end + + -- Extrapolate using the estimated item size. + local delta = Round.awayFromZero(currentDist / self.props.estimatedItemSize) + debugPrint(" Estimating target at", delta, "from end") + return math.min(math.max(currentIndex + delta, 1), self.state.listSize) + end + else + local nextDist = self:distanceToPosition(nextIndex, targetPos) + debugPrint(" End is", nextDist, "from target") + if nextDist == 0 then + return nextIndex + end + + -- If the target position lies outside of the loaded elements. + if currentDist * nextDist > 0 then + debugPrint(" Target out of bounds") + if not extrapolate then + -- Do not extrapolate. Return the closest loaded element. + return nextIndex + end + + -- Extrapolate using the estimated item size. + local delta = Round.awayFromZero(nextDist / self.props.estimatedItemSize) + debugPrint(" Estimating target at", delta, "from end") + return math.min(math.max(nextIndex + delta, 1), self.state.listSize) + end + + -- Jump to the approximate location of the target based on the distance from current and next. + local totalDist = math.abs(currentDist) + math.abs(nextDist) + local indexCount = math.abs(currentIndex - nextIndex) + currentIndex = currentIndex + Round.nearest(indexCount * currentDist / totalDist) + currentDist = self:distanceToPosition(currentIndex, targetPos) + debugPrint(" Interpolated index is", currentIndex) + debugPrint(" Distance from interpolated index is", currentDist) + end + + -- Linear search from best guess index. + while currentDist ~= 0 do + if currentDist < 0 then + currentIndex = currentIndex - 1 + else + currentIndex = currentIndex + 1 + end + currentDist = self:distanceToPosition(currentIndex, targetPos) + debugPrint(" Distance after step is", currentDist) + end + + return currentIndex +end + +-- Expand the size of the scrolling frame's canvas to make sure everything still fits. +function Scroller:expandCanvas(newState) + debugPrint("expandCanvas") + local reverse = isReverse[self.props.orientation] + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + + local size = self.state.size + local newPadding = newState.padding or self.state.padding + local oldPadding = self:getCurrentPadding() + + local bottomPos = self:getChildCanvasPosition(bottomIndex) + + self:getChildSize(bottomIndex) - (oldPadding - newPadding) + debugPrint(" Bottom of bottom child is", bottomPos) + debugPrint(" Canvas size is", self.state.size) + debugPrint(" Canvas bottom should be", bottomPos + self.props.dragBuffer) + if bottomPos > self.state.size - self.props.dragBuffer then + -- Plus footer + size = bottomPos + self.props.dragBuffer + debugPrint(" Expanding canvas bottom to size", size) + end + + debugPrint(" Padding is", newPadding) + debugPrint(" Padding should be", self.props.dragBuffer) + if newPadding < self.props.dragBuffer then + -- Minus header + local diff = newPadding - self.props.dragBuffer + size = size - diff + self.anchorCanvasPosition = self.anchorCanvasPosition - diff + newPadding = self.props.dragBuffer + debugPrint(" Expanding canvas top to size", size) + debugPrint(" Shifting anchor to", self.anchorCanvasPosition) + debugPrint(" Padding is now", newPadding) + end + + if size ~= self.state.size or newPadding ~= self.state.padding then + debugPrint(" Changing size from", self.state.size, "to", size) + debugPrint(" Changing padding from", self.state.padding, "to", newPadding) + return { + size = size, + padding = newPadding, + } + else + return {} + end +end + +-- Try and get the canvas as close to correct as possible this rendering pass. +function Scroller:adjustCanvas(trimTrailing, trimLeading) + debugPrint("adjustCanvas") + + local newState = Cryo.Dictionary.join( + self:resetAnchorPosition(), + self:recalculateBounds(trimTrailing, trimLeading) + ) + + if not newState.trail and not newState.lead then + newState = Cryo.Dictionary.join(newState, self:expandCanvas(newState)) + end + + if Cryo.isEmpty(newState) then + return false + end + + self:setState(newState) + return true +end + +-- Move the cavnas position so that the anchor element is in the same place on the screen. +function Scroller:moveToAnchor() + debugPrint("moveToAnchor") + local currentPos = self:getAnchorFramePosition() + debugPrint(" Anchor was at frame position", self.anchorFramePosition) + debugPrint(" Anchor is currently at frame position", currentPos) + self:setScroll(self:measure(self:getCurrent().CanvasPosition) + currentPos - self.anchorFramePosition) +end + +-- Call loadNext and loadPrevious if needed. +function Scroller:loadMore() + debugPrint("loadMore") + if self.props.loadPrevious and self.state.trail.index <= self.props.loadingBuffer then + debugPrint(" Calling loadPrevious") + self.props.loadPrevious() + end + if self.props.loadNext and self.state.lead.index > self.state.listSize - self.props.loadingBuffer then + debugPrint(" Calling loadNext") + self.props.loadNext() + end +end + +-- Set the current canvas position according to Orientation without calling onScroll. +function Scroller:setScroll(pos) + self.scrollDebounce = true + debugPrint(" Scrolling to", pos) + self:getCurrent().CanvasPosition = isVertical[self.props.orientation] + and Vector2.new(self:getCurrent().CanvasPosition.X, pos) + or Vector2.new(pos, self:getCurrent().CanvasPosition.Y) + self.scrollDebounce = false +end + +-- Returns the signed distance from the element to the given canvas-relative position, or 0 if the element overlaps +-- it. The sign of the distance is relative to the list indices. For this distance calculation, the padding between +-- elements is considered part of the current element. Returns nil if the element is not currently rendered. +function Scroller:distanceToPosition(index, pos) + local child = self:getRbx(index) + if not child then + return nil + end + + local childTop = self:absoluteToCanvasPosition(self:measure(child.AbsolutePosition)) - self.itemPadding + local childBottom = childTop + self:measure(child.AbsoluteSize) + 2 * self.itemPadding + + if pos < childTop then + return (pos - childTop) * direction[self.props.orientation] + elseif pos > childBottom then + return (pos - childBottom) * direction[self.props.orientation] + else + return 0 + end +end + +-- Get the canvas-relative position of the current anchor element. +function Scroller:getAnchorCanvasPosition() + return self:getAnchorCanvasFromIndex(self.state.anchor.index) +end + +function Scroller:getAnchorCanvasFromIndex(index) + local scale = self.props.anchorLocation.Scale + if not isReverse[self.props.orientation] then + scale = 1 - scale + end + + return Round.nearest(self:getChildCanvasPosition(index) + scale * self:getChildSize(index)) +end + +-- Get the frame-relative position of the current anchor element. +function Scroller:getAnchorFramePosition() + return self:getAnchorFrameFromIndex(self.state.anchor.index) +end + +function Scroller:getAnchorFrameFromIndex(index) + local scale = self.props.anchorLocation.Scale + if not isReverse[self.props.orientation] then + scale = 1 - scale + end + + return Round.nearest(self:getChildFramePosition(index) + scale * self:getChildSize(index)) + - self.relativeAnchorLocation +end + +-- Convert an AbsolutePosition to a position relative to the top-left corner of the canvas. +function Scroller:absoluteToCanvasPosition(position) + local current = self:getCurrent() + local canvas = current.CanvasPosition + local absolute = current.AbsolutePosition + return position + self:measure(canvas) - self:measure(absolute) +end + +-- Convert an AbsolutePosition to a position relative to the top-left corner of the scrolling frame. +function Scroller:absoluteToFramePosition(position) + local current = self:getCurrent() + local absolute = current.AbsolutePosition + return position - self:measure(absolute) +end + +-- Get the canvas-relative position of the element at the specified index. +function Scroller:getChildCanvasPosition(index) + local current = self:getRbx(index) + return current and self:absoluteToCanvasPosition(self:measure(current.AbsolutePosition)) or 0 +end + +-- Get the frame-relative position of the element at the specified index. +function Scroller:getChildFramePosition(index) + local current = self:getRbx(index) + return current and self:absoluteToFramePosition(self:measure(current.AbsolutePosition)) or 0 +end + +-- Get the absolute size of the element at the specified index. +function Scroller:getChildSize(index) + local current = self:getRbx(index) + return current and self:measure(current.AbsoluteSize) or 0 +end + +-- Get the ID of an element at a specific index. +function Scroller:getID(index) + return self.props.identifier(self.props.itemList[index]) +end + +-- Create or update a metadata entry for the given element. This can't use +-- self.props in willUpdate as any props it uses could be out of date. +function Scroller:updateMetadata(id, item, props) + local meta = self.metadata[id] + if not meta then + meta = {} + self.metadata[id] = meta + end + + if not meta.name then + local elem = props.renderItem(item, false) + local class = tostring(elem.component) + local pool = self:getKeyPool(class) + meta.name = pool:get() + end + + if not self.refpool[meta.name] then + self.refpool[meta.name] = Roact.createRef() + end + meta.ref = self.refpool[meta.name] +end + +-- Clear the metadata for an element that is being unloaded. +function Scroller:clearMetadata(id) + local meta = self.metadata[id] + if not meta then + return + end + + meta.name:release() + meta.name = nil + meta.ref = nil +end + +-- Get the key pool for the given class of elements, or create a new one if that doesn't exist yet. +function Scroller:getKeyPool(class) + if not self.pools[class] then + self.pools[class] = KeyPool.new(class) + end + return self.pools[class] +end + +-- Get the metadata info for the element at the specified index. +function Scroller:getMetadata(index) + return self.metadata[self:getID(index)] +end + +-- Get the current Roblox instance from the ref stored in the metadata. +function Scroller:getRbx(index) + local meta = self:getMetadata(index) + return meta and meta.ref and meta.ref.current +end + +-- Return X or Y depending on the orientation. +function Scroller:measure(vecOrUDim2) + return isVertical[self.props.orientation] and vecOrUDim2.Y or vecOrUDim2.X +end + +-- Get the current ScrollingFrame instance. +function Scroller:getCurrent() + return self:getRef().current +end + +function Scroller:getRef() + -- Make sure to get the ref from props if that exists. + return self.props[Roact.Ref] or self._ref +end + +return Scroller diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/findNewIndices.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/findNewIndices.lua new file mode 100644 index 0000000..d983a90 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/findNewIndices.lua @@ -0,0 +1,61 @@ +-- Find the new indicies of the trailing, anchor and leading elements. +-- props is expected to contain itemList, the identifier function and the maximum search distance. +-- state is expected to contain the old top, anchor and bottom indices and ids. +return function(props, state) + local topIndex = state.trail.index + local topID = state.trail.id + local anchorIndex = state.anchor.index + local anchorID = state.anchor.id + local bottomIndex = state.lead.index + local bottomID = state.lead.id + + local listSize = #props.itemList + + -- If too much got deleted and the previous anchor index is off the bottom of the list, start the search from + -- the bottom. + if anchorIndex > listSize then + anchorIndex = listSize + end + + -- No access to self:getID + local getID = function(index) + return props.identifier(props.itemList[index]) + end + local topStill = getID(topIndex) == topID + local anchorStill = getID(anchorIndex) == anchorID + local bottomStill = getID(bottomIndex) == bottomID + if topStill and anchorStill and bottomStill then + -- Nothing important moved + return topIndex, anchorIndex, bottomIndex + end + + local step = 0 + local foundTop = topStill and topIndex or nil + local foundAnchor = nil + local foundBottom = bottomStill and bottomIndex or nil + + -- Scan outward from the old anchor index until we find the top and bottom or hit the max distance + local deltas = {top=-1, bottom=1} + repeat + for _, delta in pairs(deltas) do + local pos = anchorIndex + delta * step + + if pos >= 1 and pos <= listSize then + local id = getID(pos) + if id == topID then + foundTop = pos + end + if id == anchorID then + foundAnchor = pos + end + if id == bottomID then + foundBottom = pos + end + end + end + + step = step + 1 + until (foundTop and foundAnchor and foundBottom) or step > props.maximumSearchDistance + + return foundTop, foundAnchor, foundBottom +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/FragmentThing.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/FragmentThing.lua new file mode 100644 index 0000000..98d53b1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/FragmentThing.lua @@ -0,0 +1,17 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +return function(props) + return Roact.createFragment({ + foo = Roact.createElement("Frame", { + BackgroundColor3 = props.color, + Size = UDim2.new(0, props.width, 0, props.width), + LayoutOrder = props.LayoutOrder, + }), + bar = Roact.createElement("Frame", { + BackgroundColor3 = props.color, + Size = UDim2.new(0, props.width, 0, props.width), + LayoutOrder = props.LayoutOrder + 1, + }) + }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/FunctionThing.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/FunctionThing.lua new file mode 100644 index 0000000..a70d3fa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/FunctionThing.lua @@ -0,0 +1,10 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +return function(props) + return Roact.createElement("Frame", { + BackgroundColor3 = props.color, + Size = UDim2.new(0, props.width, 0, props.width), + LayoutOrder = props.LayoutOrder, + }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/NestedThing.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/NestedThing.lua new file mode 100644 index 0000000..64cdd37 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/NestedThing.lua @@ -0,0 +1,24 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +local Thing = Roact.PureComponent:extend("Thing") + +function Thing:render() + return Roact.createElement("Frame", { + BackgroundColor3 = self.props.color, + Size = UDim2.new(0, self.props.width, 0, self.props.width), + LayoutOrder = self.props.LayoutOrder, + }) +end + +local ThingRoot = Roact.PureComponent:extend("ThingRoot") + +function ThingRoot:render() + return Roact.createElement(Thing, { + color = self.props.color, + width = self.props.width, + LayoutOrder = self.props.LayoutOrder, + }) +end + +return ThingRoot diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/ResizingThing.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/ResizingThing.lua new file mode 100644 index 0000000..6552ff7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/ResizingThing.lua @@ -0,0 +1,37 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +local Thing = Roact.PureComponent:extend("Thing") + +function Thing:init() + self.state = { + clicked = false, + token = nil, + } +end + +function Thing.getDerivedStateFromProps(nextProps, lastState) + -- Use the token we're already being passed as a surrogate lock, just for this story. + -- Normally, this would be a separate prop. + if nextProps.token ~= lastState.token then + return { + clicked = false, + token = nextProps.token, + } + end +end + +function Thing:render() + return Roact.createElement("Frame", { + BackgroundColor3 = self.props.color, + Size = UDim2.new(1, -self.props.width, 0, self.state.clicked and 10 or self.props.width), + LayoutOrder = self.props.LayoutOrder, + [Roact.Event.InputBegan] = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self:setState({clicked = not self.state.clicked}) + end + end, + }) +end + +return Thing diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/init.lua new file mode 100644 index 0000000..fd1d114 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/init.lua @@ -0,0 +1,23 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +local SCREEN_SIZE = Vector2.new(800, 480) + +return { + name = "Scroller", + storyRoot = script, + middleware = function(story, target) + local tree = Roact.createElement("Frame", { + BackgroundColor3 = Color3.fromRGB(30, 31, 28), + Size = UDim2.new(0, SCREEN_SIZE.X, 0, SCREEN_SIZE.Y), + ClipsDescendants = true, + }, { + Story = Roact.createElement(story) + }) + + local handle = Roact.mount(tree, target, "Root") + return function() + Roact.unmount(handle) + end + end, +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.deletion.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.deletion.story.lua new file mode 100644 index 0000000..2f59f28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.deletion.story.lua @@ -0,0 +1,75 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - item deletion test") + +function Story:init() + self.state.items = {} + for i = -20,20 do + table.insert(self.state.items, i) + end +end + +function Story:render() + return Roact.createElement("Frame", { + Size=UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 100, 0, 100), + padding = UDim.new(), + itemList = self.state.items, + focusIndex = 21, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = item == 0 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, 128 - 8*item, 128 + 8*item), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, + self.props, + {toDelete = Cryo.None} + )), + deletion = Roact.createElement("TextButton", { + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "Delete", + [Roact.Event.Activated] = function() + local nextItems + if self.props.toDelete then + nextItems = Cryo.List.filter(self.state.items, function(item) + for _, v in pairs(self.props.toDelete) do + if item == v then + return false + end + end + return true + end) + else + local n = math.random(1, #self.state.items) + print("Deleting index " .. tostring(n)) + nextItems = Cryo.List.removeIndex(self.state.items, n) + end + self:setState({ + items = nextItems + }) + end, + }), + }) +end + +return Story \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.few.large.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.few.large.story.lua new file mode 100644 index 0000000..a976be7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.few.large.story.lua @@ -0,0 +1,30 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - a few large items") + +function Story:render() + return Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 50, 0, 50), + padding = UDim.new(), + itemList = {1, 2, 3, 4, 5, 6, 7}, + focusIndex = 4, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 49, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + BackgroundColor3 = Color3.fromRGB(item*30, item*30, item*30), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, self.props)) +end + +return Story \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.infinite.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.infinite.story.lua new file mode 100644 index 0000000..ea63c4e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.infinite.story.lua @@ -0,0 +1,125 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - infinite scroll in both directions") + +function Story:init() + self.state.items = { + { + token = 0, + color = Color3.fromRGB(255, 255, 255), + } + } + self.state.size = Vector2.new(50, 50) + + self.loadPrevious = function() + local newItems = {} + local n = self.state.items[1].token + for i = n-10, n-1 do + table.insert(newItems, { + token = i, + color = Color3.fromRGB(0, 128 - i, 128 + i), + }) + end + self:setState({ + items = Cryo.List.join(newItems, self.state.items) + }) + end + + self.loadNext = function() + local newItems = {} + local n = self.state.items[#self.state.items].token + for i = n+1, n+10 do + table.insert(newItems, { + token = i, + color = Color3.fromRGB(0, 128 - i, 128 + i), + }) + end + self:setState({ + items = Cryo.List.join(self.state.items, newItems) + }) + end +end + +function Story:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, self.state.size.X, 0, self.state.size.Y), + padding = UDim.new(0, 3), + itemList = self.state.items, + loadNext = self.loadNext, + loadPrevious = self.loadPrevious, + focusIndex = 1, + anchorLocation = UDim.new(0.5, 0), + orientation = Scroller.Orientation.Up, + estimatedItemSize = 10, + mountingBuffer = 50, + identifier = function(item) + return item.token + end, + renderItem = function(item, _) + assert(item.token, "Item's token is unset") + assert(item.color, "Item's color is unset") + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = item.color, + }, { + ["INDEX" .. tostring(item.token)] = Roact.createElement("Frame"), + }) + end, + }, self.props)), + moveUp = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "^", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, -20) + }) + end, + }), + moveDown = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 100), + AnchorPoint = Vector2.new(1, 0), + Text = "v", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, 20) + }) + end, + }), + moveLeft = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -100, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = "<", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(-20, 0) + }) + end, + }), + moveRight = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, 0, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = ">", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(20, 0) + }) + end, + }), + }) +end + +return Story \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.resize.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.resize.story.lua new file mode 100644 index 0000000..66194ff --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.resize.story.lua @@ -0,0 +1,113 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Bar = Roact.PureComponent:extend("Bar") + +function Bar:init() + self.state = { + clicked = false, + } +end + +function Bar:render() + return Roact.createElement("TextButton", Cryo.Dictionary.join( + { + Size = UDim2.new(1, 0, 0, self.state.clicked and 30 or 10), + [Roact.Event.Activated] = function() + self:setState({ + clicked = not self.state.clicked, + }) + end, + }, + self.props + )) +end + +local Story = Roact.PureComponent:extend("Rhodium Story - frame resize test") + +function Story:init() + self.state = { + size = Vector2.new(50, 50), + } +end + +function Story:render() + return Roact.createElement("Frame", { + Size=UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, self.state.size.X, 0, self.state.size.Y), + padding = UDim.new(), + itemList = {1, 2, 3}, + focusIndex = 2, + anchorLocation = UDim.new(0, 0), + orientation = Scroller.Orientation.Down, + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + renderItem = function(item, _) + return Roact.createElement(Bar, { + BackgroundColor3 = item == 2 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, -128 + 128*item, 376 - 128*item), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, + self.props + )), + moveUp = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "^", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, -20) + }) + end, + }), + moveDown = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 100), + AnchorPoint = Vector2.new(1, 0), + Text = "v", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, 20) + }) + end, + }), + moveLeft = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -100, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = "<", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(-20, 0) + }) + end, + }), + moveRight = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, 0, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = ">", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(20, 0) + }) + end, + }), + }) +end + +return Story \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.several.small.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.several.small.story.lua new file mode 100644 index 0000000..d67e076 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.several.small.story.lua @@ -0,0 +1,33 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - enough small items to fill the view") + +function Story:render() + return Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 50, 0, 50), + padding = UDim.new(), + itemList = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, + focusIndex = 6, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = item == 6 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, item*23, item*23), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, self.props)) +end + +return Story \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.single.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.single.story.lua new file mode 100644 index 0000000..d8ba0ae --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.single.story.lua @@ -0,0 +1,22 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - a single small item") + +function Story:render() + return Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 50, 0, 50), + itemList = {1}, + anchorLocation = UDim.new(0.5, 0), + dragBuffer = 0, + renderItem = function() + return Roact.createElement("Frame", {Size=UDim2.new(0, 10, 0, 10)}) + end, + }, self.props)) +end + +return Story \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/scroller.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/scroller.story.lua new file mode 100644 index 0000000..3157319 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/scroller.story.lua @@ -0,0 +1,139 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Thing = require(script.Parent.ResizingThing) + +local Story = Roact.PureComponent:extend("Story") + +function Story:render() + return Roact.createFragment({ + scroller = Roact.createElement(Scroller, { + BackgroundColor3 = Color3.fromRGB(56, 19, 18), + Size = UDim2.new(0, self.state.size.X, 1, self.state.size.Y), + Position = UDim2.new(0, 50, 0, 50), + ScrollBarThickness = 8, + padding = UDim.new(0, 5), + orientation = Scroller.Orientation.Down, + itemList = self.state.items, + loadNext = self.loadNext, + loadPrevious = self.loadPrevious, + focusLock = self.state.lock, + focusIndex = 101, + anchorLocation = UDim.new(0, 0), + estimatedItemSize = 40, + identifier = function(item) return item.token end, + renderItem = function(item, _) + return Roact.createElement(Thing, item) + end, + }), + refresh = Roact.createElement("TextButton", { + Size = UDim2.new(0, 110, 0, 30), + Position = UDim2.new(1, -50, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = "Refresh", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickRefresh, + }), + up = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -90, 0, 90), + AnchorPoint = Vector2.new(1, 0), + Text = "^", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickUp, + }), + down = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -90, 0, 170), + AnchorPoint = Vector2.new(1, 0), + Text = "v", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickDown, + }), + left = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -130, 0, 130), + AnchorPoint = Vector2.new(1, 0), + Text = "<", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickLeft, + }), + right = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -50, 0, 130), + AnchorPoint = Vector2.new(1, 0), + Text = ">", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickRight, + }), + }) +end + +local function generate(token) + if token == 0 then + return {color=Color3.fromRGB(255, 255, 255), width=50, token=0} + end + return {color=Color3.fromRGB(128-token, 255, 128+token), width=50+40*math.sin(token/5), token=token} +end + +function Story:init() + local items = {} + for i = -100,100 do table.insert(items, generate(i)) end + self.state = { + lock = 1, + size = Vector2.new(200, -100), + items = items, + } + + self.loadNext = function() + self:setState({ + items = Cryo.List.join(self.state.items, {generate(self.state.items[#self.state.items].token + 1)}), + }) + end + + self.loadPrevious = function() + self:setState({ + items = Cryo.List.join({generate(self.state.items[1].token - 1)}, self.state.items), + }) + end + + self.clickRefresh = function() + print("Recentering") + self:setState({ + lock = self.state.lock + 1 + }) + end + + self.clickUp = function() + print("Moving up") + self:setState({ + size = self.state.size + Vector2.new(0, -20), + }) + end + + self.clickDown = function() + print("Moving down") + self:setState({ + size = self.state.size + Vector2.new(0, 20), + }) + end + + self.clickLeft = function() + print("Moving left") + self:setState({ + size = self.state.size + Vector2.new(-20, 0), + }) + end + + self.clickRight = function() + print("Moving right") + self:setState({ + size = self.state.size + Vector2.new(20, 0), + }) + end +end + +return Story \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/init.lua new file mode 100644 index 0000000..61e7222 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/init.lua @@ -0,0 +1,5 @@ +local Scroller = require(script.Components.Scroller) + +return { + Scroller = Scroller, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/lock.toml new file mode 100644 index 0000000..4be0335 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/lock.toml @@ -0,0 +1,12 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/infinite-scroller" +version = "0.3.4" +commit = "43d4eedf0d7a0a97d2dd55e7f066ac617e7fde9f" +source = "url+https://github.com/roblox/infinite-scroller" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/t.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/FitFrame.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/FitFrame.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/FitFrame.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Otter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Roact.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Distance.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Distance.lua new file mode 100644 index 0000000..0a959bd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Distance.lua @@ -0,0 +1,16 @@ +return { + -- Returns the signed distance from a point to a range. Returns 0 if the + -- point is within the range, positive if it's below and negative if it's + -- above. + fromPointToRangeSigned = function(point, rangeTop, rangeSize) + local rangeBottom = rangeTop + rangeSize + + if point < rangeTop then + return point - rangeTop + elseif point > rangeBottom then + return point - rangeBottom + else + return 0 + end + end +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/KeyPool.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/KeyPool.lua new file mode 100644 index 0000000..81f7669 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/KeyPool.lua @@ -0,0 +1,80 @@ +--[[ + KeyPool provides a pool of objects suitable for use as map keys. + + Create a new KeyPool, then call pool:get() to get a new key. Once you're done with it, call key:release(). + + Example: + local pool = KeyPool.new("foo") + ... + local key1 = pool:get() + local key2 = pool:get() + map[key1] = thing1 + map[key2] = thing2 + ... + map[key1] = nil + key1:release() + key1 = nil +]] +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local t = require(Root.t) + +-- Forward declarations +local KeyPool = {} +KeyPool.__index = KeyPool + +local Key = {} +Key.__index = Key + +-- This is Key.new, but we don't want to expose that publicly. +local function newkey(pool, index) + local key = { + pool = pool, + index = index, + } + + setmetatable(key, Key) + return key +end + +-- KeyPool functions + +function KeyPool.new(class) + assert(t.string(class)) + + local pool = { + class = class, + available = {}, + limit = 0, + count = 0, + } + + setmetatable(pool, KeyPool) + return pool +end + +-- Get a currently unused key, or create a new one if everything is in use. +function KeyPool:get() + if self.count == 0 then + self.limit = self.limit + 1 + return newkey(self, self.limit) + end + + local key = self.available[self.count] + self.count = self.count - 1 + return key +end + +-- Key functions + +function Key:__tostring() + return self.pool.class .. "_" .. string.format("%02d", tostring(self.index)) +end + +-- Return this key to the pool it came from. Whatever previously held this key should not keep the reference after +-- calling this. +function Key:release() + self.pool.count = self.pool.count + 1 + self.pool.available[self.pool.count] = self +end + +return KeyPool diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/NotifyReady.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/NotifyReady.lua new file mode 100644 index 0000000..a564707 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/NotifyReady.lua @@ -0,0 +1 @@ +return {} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Orientation.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Orientation.lua new file mode 100644 index 0000000..a31399b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Orientation.lua @@ -0,0 +1,31 @@ +-- Enum for specifying the leading edge of the scroller. +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local t = require(Root.t) + +local Orientation = { + Up = "Orientation.Up", + Down = "Orientation.Down", + Left = "Orientation.Left", + Right = "Orientation.Right", +} + +local metaindex = { + isOrientation = t.union( + t.literal(Orientation.Up), + t.literal(Orientation.Down), + t.literal(Orientation.Left), + t.literal(Orientation.Right) + ) +} + +setmetatable(Orientation, { + __index = function(self, key) + return metaindex[key] or + error(tostring(key) .. " is not a valid member of Scroller.Orientation", 2) + end, + __newindex = function() + error("Scroller.Orientation is read-only", 2) + end, +}) + +return Orientation diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Round.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Round.lua new file mode 100644 index 0000000..2755954 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Round.lua @@ -0,0 +1,28 @@ +local epsilon = 1e-15 + +return { + nearest = function(num) + local q, r = math.modf(num) + if r <= -0.5 then + return q - 1 + elseif r >= 0.5 then + return q + 1 + else + return q + end + end, + towardsZero = function(num) + local result, _ = math.modf(num) + return result + end, + awayFromZero = function(num) + local q, r = math.modf(num) + if r < -epsilon then + return q - 1 + elseif r > epsilon then + return q + 1 + else + return q + end + end, +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Scroller.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Scroller.lua new file mode 100644 index 0000000..649e349 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Scroller.lua @@ -0,0 +1,1365 @@ +local RunService = game:GetService("RunService") + +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local t = require(Root.t) +local Otter = require(Root.Otter) + +local FitFrame = require(Root.FitFrame).FitFrameOnAxis +local findNewIndices = require(script.Parent.findNewIndices) +local relocateIndices = require(script.Parent.relocateIndices) +local Round = require(script.Parent.Round) +local Distance = require(script.Parent.Distance) +local KeyPool = require(script.Parent.KeyPool) + +local NotifyReady = require(script.Parent.NotifyReady) + +local debugPrint = function() end + +local Scroller = Roact.PureComponent:extend("Scroller") + +Scroller.Orientation = require(script.Parent.Orientation) + +local MOTOR_OPTIONS = { + frequency = 4, + dampingRatio = 1, +} + +local isVertical = { + [Scroller.Orientation.Up] = true, + [Scroller.Orientation.Down] = true, + [Scroller.Orientation.Left] = false, + [Scroller.Orientation.Right] = false, +} + +local isReverse = { + [Scroller.Orientation.Up] = true, + [Scroller.Orientation.Down] = false, + [Scroller.Orientation.Left] = true, + [Scroller.Orientation.Right] = false, +} + +local direction = { + [Scroller.Orientation.Up] = -1, + [Scroller.Orientation.Down] = 1, + [Scroller.Orientation.Left] = -1, + [Scroller.Orientation.Right] = 1, +} + +Scroller.validateProps = t.interface({ + -- Required. The list of items to scroll through. + itemList = t.array(t.any), + + -- Required. A callback function, called with each visible item in the itemList when the list is rendered. + renderItem = t.callback, + + -- A function to uniquely identify list items. Calling this on the same item twice should give the same result + -- accoring to ==. + identifier = t.optional(t.callback), + + -- One of the Scroller.Orientation enums. Determines the leading edge of the infinite scroll. + orientation = t.optional(Scroller.Orientation.isOrientation), + + -- A callback function, called when the infinite scroll reaches the leading end of the itemList (index + -- #itemList). + loadNext = t.optional(t.callback), + + -- A callback function, called when the infinite scroll reaches the trailing end of the itemList (index 1). + loadPrevious = t.optional(t.callback), + + -- Padding between elements in the scrolling frame. The Scale is relative to the size of the scrolling frame. + padding = t.optional(t.UDim), + + -- The minimum number of unmounted elements to keep at the top and bottom of the list. If there are fewer than + -- this call loadNext or loadPrevious. + loadingBuffer = t.optional(t.numberPositive), + + -- The amount of space above and below the view to render items in. + mountingBuffer = t.optional(t.numberPositive), + + -- The amount of empty space to keep at the top and bottom on the scroll. + dragBuffer = t.optional(t.numberMin(0)), + + -- An initial guess at the average size of an item. + estimatedItemSize = t.optional(t.numberPositive), + + -- The maximum distance to search for moved elements. + maximumSearchDistance = t.optional(t.numberPositive), + + -- The element to put in focus initially. + focusIndex = t.optional(t.integer), + + -- An arbitrary value to prevent the list from refocusing every render. Change this to cause the list to reset + -- and refocus on the new focusIndex. + focusLock = t.optional(t.any), + + -- The position within the view to keep still as other things move. The Scale is relative to the size of the + -- scrolling frame. + anchorLocation = t.optional(t.UDim), + + -- Animate the scrolling + animateScrolling = t.optional(t.boolean), + + --Animation options + animateOptions = t.optional(t.table), + + -- Properties that should trigger rerenders of the children elements even though the scroller itself does not + -- use them. + extraProps = t.optional(t.table), + + -- A callback function that will update the index change + onScrollUpdate = t.optional(t.callback), + + -- Which components to disable instance recycling for. + recyclingDisabledFor = t.optional(t.array(t.string)), + + ---- INTERNAL ONLY ---- + [NotifyReady] = t.any, +}) + +-- Default values for all the infinite-scroller-specific props. Any prop not in this list will be passed on to the +-- underlying ScrollingFrame. +Scroller.defaultProps = { + itemList = {}, + renderItem = {}, + identifier = function(item) + return item + end, + orientation = Scroller.Orientation.Down, + loadNext = function() end, + loadPrevious = function() end, + padding = UDim.new(0, 0), + loadingBuffer = 10, + mountingBuffer = 200, + dragBuffer = 0, + estimatedItemSize = 50, + maximumSearchDistance = 100, + focusIndex = 1, + focusLock = {}, + anchorLocation = UDim.new(0, 0), + animateScrolling = false, + animateOptions = MOTOR_OPTIONS, + extraProps = {}, + onScrollUpdate = function() end, + recyclingDisabledFor = {}, + [NotifyReady] = false, +} + +function Scroller:render() + debugPrint("render") + + -- Gather vertical/horizontal specific variables. + local axis = isVertical[self.props.orientation] and { + fillDirection = Enum.FillDirection.Vertical, + scrollDirection = Enum.ScrollingDirection.Y, + fitDirection = FitFrame.Axis.Vertical, + minimumSize = UDim2.new(1, 0, 0, 0), + canvasSize = UDim2.new(0, 0, 0, self.state.size), + paddingSize = UDim2.new(0, 0, 0, self.state.padding), + } or { + fillDirection = Enum.FillDirection.Horizontal, + scrollDirection = Enum.ScrollingDirection.X, + fitDirection = FitFrame.Axis.Horizontal, + minimumSize = UDim2.new(0, 0, 1, 0), + canvasSize = UDim2.new(0, self.state.size, 0, 0), + paddingSize = UDim2.new(0, self.state.padding, 0, 0), + } + + -- Remove non-standard props from list to pass on to ScrollingFrame. These are the same props given in + -- defaultProps. + local props = Cryo.Dictionary.join( + self.props, + self.propsToClear, + { + CanvasSize = axis.canvasSize, + ScrollingDirection = axis.scrollDirection, + [Roact.Change.CanvasPosition] = self.onScroll, + [Roact.Change.AbsoluteSize] = self.onResize, + [Roact.Ref] = self:getRef(), + } + ) + + local children = { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = axis.fillDirection, + Padding = UDim.new(0, self.itemPadding), + [Roact.Change.AbsoluteContentSize] = self.onContentResize, + }), + padding = Roact.createElement("Frame", { + Size = axis.paddingSize, + LayoutOrder = -1 - (self.state.listSize or 0), + BackgroundTransparency = 1, + }), + } + + -- Trailing and leading indicies won't be set if this isn't true. + if self.state.ready and not Cryo.isEmpty(self.props.itemList) then + debugPrint(" Rendering elements between", self.state.trail.index, "and", self.state.lead.index) + for n = self.state.trail.index, self.state.lead.index do + local metadata = self:getMetadata(n) + children[metadata.name] = Roact.createElement(FitFrame, { + minimumSize = axis.minimumSize, + axis = axis.fitDirection, + FillDirection = axis.fillDirection, + BackgroundTransparency = 1, + LayoutOrder = isReverse[self.props.orientation] and -n or n, + [Roact.Ref] = metadata.ref, + }, { + item = self.props.renderItem(self.props.itemList[n], false), + }) + end + end + + return Roact.createElement("ScrollingFrame", props, children) +end + +function Scroller:shouldUpdate(nextProps, nextState) + if not self.alive then + return false + end + + debugPrint("shouldUpdate") + + -- Check for state and props changes in the same way PureComponent would, + -- but go one more level down for extraProps. + if nextState ~= self.state then + debugPrint(" State changed") + return true + end + + for key, value in pairs(nextProps) do + if self.props[key] ~= value then + if key ~= "extraProps" then + debugPrint(" Prop changed:", key) + return true + end + + for extraKey, extraValue in pairs(value) do + if self.props.extraProps[extraKey] ~= extraValue then + debugPrint(" Extra prop changed:", extraKey) + return true + end + end + end + end + + for key, value in pairs(self.props) do + if nextProps[key] ~= value then + if key ~= "extraProps" then + debugPrint(" Prop changed:", key) + return true + end + + for extraKey, extraValue in pairs(value) do + if nextProps.extraProps[extraKey] ~= extraValue then + debugPrint(" Extra prop changed:", extraKey) + return true + end + end + end + end + + return false +end + +function Scroller:willUpdate(nextProps, nextState) + if not self.alive then + return + end + + debugPrint("willUpdate") + + if not nextState.ready then + return + end + + self.sizeDebounce = true + + local deletions = {} + local additions = {} + + if not Cryo.isEmpty(self.props.itemList) and self.state.lead then + for n = self.state.trail.index, self.state.lead.index do + local id = self.props.identifier(self.props.itemList[n]) + deletions[id] = true + end + end + + if not Cryo.isEmpty(nextProps.itemList) and nextState.lead then + for n = nextState.trail.index, nextState.lead.index do + local item = nextProps.itemList[n] + local id = nextProps.identifier(item) + if deletions[id] then + -- Element is in both ranges. + deletions[id] = nil + else + additions[id] = item + end + end + end + + -- Clear names first, so new items can use them. + for id, _ in pairs(deletions) do + self:clearMetadata(id) + end + for id, item in pairs(additions) do + self:updateMetadata(id, item, nextProps) + end + + -- The focus lock changed, clear the non-state anchor variables. + if self.state.lastFocusLock ~= nextState.lastFocusLock then + self.scrollDebounce = true + self.motorActive = false + self.anchorFramePosition = 0 + if self.state.listSize and self.state.listSize > nextState.listSize then + -- Perform a full reset when the list decreases in size to ensure the canvas + -- is sized properly + self.anchorCanvasPosition = self.relativeAnchorLocation + else + -- The canvas hasn't necessarily been reset in size here, so we need to convert the frame + -- coordinates of relativeAnchorLocation to canvas coordinates. + self.anchorCanvasPosition = self:frameToCanvasPosition(self.relativeAnchorLocation) + end + end +end + +function Scroller:didUpdate(previousProps, previousState) + if not self.alive then + return + end + + debugPrint("didUpdate") + + if Cryo.isEmpty(self.props.itemList) then + return + end + + if not self.state.ready then + self.onResize(self:getRef().current) + return + end + + if self.props.focusIndex ~= previousProps.focusIndex and + self.props.focusLock ~= previousProps.focusLock then + + self.indexChanged = { + oldIndex = previousProps.focusIndex, + newIndex = self.props.focusIndex, + lastFocusLock = self.props.focusLock, + } + self.motorActive = false + + debugPrint("self.props.focusIndex", self.props.focusIndex) + debugPrint("self.state.anchor.index", self.state.anchor.index) + debugPrint("previousState.anchor.index", previousState.anchor.index) + end + + local adjustedCanvas = self:adjustCanvas(self.scrollingForward, self.scrollingBackward) + if not adjustedCanvas then + if self.indexChanged and self.props.animateScrolling then + self:scrollToAnchor() + else + self:moveToAnchor() + end + + if self.anchorOffset ~= 0 then + self:setState({}) + return + end + + -- The canvas has finished adjusting itself after a resize + self.resized = false + + self:loadMore() + self.sizeDebounce = false + + --Return the updated index + if self.props.onScrollUpdate then + self.props.onScrollUpdate({ + leadIndex = self.state.lead.index, + anchorIndex = self.state.anchor.index, + trailIndex = self.state.trail.index, + animationActive = self.motorActive, + }) + end + end +end + +function Scroller.getDerivedStateFromProps(nextProps, lastState) + debugPrint("getDerivedStateFromProps") + if not lastState.ready or Cryo.isEmpty(nextProps.itemList) then + return nil + end + + local listSize = #nextProps.itemList + local lastFocusLock = nil + + -- Reset the state if the focus lock changes. This is guaranteed to be true the first time. + if lastState.lastFocusLock ~= nextProps.focusLock then + debugPrint(" Resetting focus lock", lastState.lastFocusLock," to ", nextProps.focusLock) + if nextProps.animateScrolling and lastState.lastFocusLock ~= nil then + lastFocusLock = nextProps.focusLock + else + local focusID = nextProps.identifier(nextProps.itemList[nextProps.focusIndex]) + return { + listSize = listSize, + trail = {index=nextProps.focusIndex, id=focusID}, + anchor = {index=nextProps.focusIndex, id=focusID}, + lead = {index=nextProps.focusIndex, id=focusID}, + padding = 0, + size = 0, + lastFocusLock = nextProps.focusLock, + } + end + end + + local trailIndex, anchorIndex, leadIndex = findNewIndices(nextProps, lastState) + debugPrint(" Trailing index moved from", lastState.trail.index, "to", trailIndex) + debugPrint(" Anchor index moved from", lastState.anchor.index, "to", anchorIndex) + debugPrint(" Leading index moved from", lastState.lead.index, "to", leadIndex) + + -- Nothing changed. Return early to avoid triggering an update. + if anchorIndex and lastState.anchor.index == anchorIndex + and trailIndex and lastState.trail.index == trailIndex + and leadIndex and lastState.lead.index == leadIndex then + debugPrint(" No change, returning early") + if listSize == lastState.listSize then + if lastFocusLock then + return { + lastFocusLock = lastFocusLock, + } + else + return nil + end + else + return { + listSize = listSize, + lastFocusLock = lastFocusLock, + } + end + end + + -- TODO #51 findNewIndices, state and this should all agree on a format + local newIndices = relocateIndices( + {trailIndex=trailIndex, anchorIndex=anchorIndex, leadIndex=leadIndex}, + {trailIndex=lastState.trail.index, anchorIndex=lastState.anchor.index, leadIndex=lastState.lead.index}, + listSize + ) + + debugPrint(" Anchor index moved to", newIndices.anchorIndex) + debugPrint(" Trailing index moved to", newIndices.trailIndex) + debugPrint(" Leading index moved to", newIndices.leadIndex) + + local trailID = nextProps.identifier(nextProps.itemList[newIndices.trailIndex]) + local anchorID = nextProps.identifier(nextProps.itemList[newIndices.anchorIndex]) + local leadID = nextProps.identifier(nextProps.itemList[newIndices.leadIndex]) + + return { + listSize = listSize, + trail = {index=newIndices.trailIndex, id=trailID}, + anchor = {index=newIndices.anchorIndex, id=anchorID}, + lead = {index=newIndices.leadIndex, id=leadID}, + lastFocusLock = lastFocusLock, + } +end + +function Scroller:init() + debugPrint("init") + + -- Only self:getRef() should access this. + self._ref = Roact.createRef() + + self.motorPrevValue = 0 + + self.motorOnStep = function(value) + debugPrint("onStep", value) + if not self.motorActive or self.indexChanged == nil then + self.motor:stop() + return + end + local currentValue = self.indexChanged.currentPos + if currentValue == nil then + self.motor:stop() + return + end + local diff = value - self.motorPrevValue + if self:getCurrent() then + self:scrollRelative(diff) + self.motorPrevValue = value + end + end + + self.motorOnComplete = function() + debugPrint("otter onComplete") + self.motorActive = false + --Return the updated index + if self.props.onScrollUpdate then + self.props.onScrollUpdate({ + leadIndex = self.props.focusIndex, + anchorIndex = self.props.focusIndex, + trailIndex = self.props.focusIndex, + animationActive = self.motorActive, + }) + end + self.motorPrevValue = 0 + self.indexChanged = nil + if self.motor then + self.motor:destroy() + end + end + + self.motorActive = false + self.springLock = 0 + self.scrollDebounce = false + self.sizeDebounce = true + + --Used to track index changes + self.indexChanged = nil + + self.onScroll = function(rbx) + debugPrint("onScroll") + if not self.alive then + return + end + + debugPrint(" CanvasPosition is", rbx.CanvasPosition) + if self.scrollDebounce then + debugPrint(" Debouncing scroll") + return + end + + local delta, newState = self:recalculateAnchor() + self.scrollingBackward = delta < 0 + self.scrollingForward = delta > 0 + debugPrint(" Delta is", delta) + + if not Cryo.isEmpty(newState) then + self:setState(newState) + end + + -- Handle any passed in scroll callback. + if self.props[Roact.Change.CanvasPosition] then + self.props[Roact.Change.CanvasPosition](rbx) + end + end + + self.onResize = function(rbx) + debugPrint("onResize") + if not self.alive then + return + end + + local size = self:measure(rbx.AbsoluteSize) + local pos = self:measure(rbx.AbsolutePosition) + + self.itemPadding = self.props.padding.Scale * size + self.props.padding.Offset + if isReverse[self.props.orientation] then + self.relativeAnchorLocation = Round.nearest( + self.props.anchorLocation.Scale * size + self.props.anchorLocation.Offset) + else + self.relativeAnchorLocation = Round.nearest( + (1 - self.props.anchorLocation.Scale) * size - self.props.anchorLocation.Offset) + end + self.absoluteAnchorLocation = self.relativeAnchorLocation + pos + self.mountAboveAnchor = self.relativeAnchorLocation + self.props.mountingBuffer + self.mountBelowAnchor = size - self.relativeAnchorLocation + self.props.mountingBuffer + self.resized = true + + -- Handle any passed in resize callback. + if self.props[Roact.Change.AbsoluteSize] then + self.props[Roact.Change.AbsoluteSize](rbx) + end + + if not self.state.ready then + debugPrint(" Setting initial anchor position to", self.relativeAnchorLocation) + -- When setting this for the first time, set the frame position of the current anchor to 0, + -- and its canvas position to equal where it should be in the frame. When the scroller goes + -- to correct this, the anchor will end up in the right place with the right padding around it. + self.anchorFramePosition = 0 + self.anchorCanvasPosition = self.relativeAnchorLocation + + coroutine.wrap(function() + RunService.Heartbeat:Wait() + if not self.state.ready and self.alive then + self:setState({ + ready = true + }) + + -- This should only be set by tests. + if self.props[NotifyReady] then + self.props[NotifyReady]:Fire() + end + end + end)() + else + self:setState({}) -- Force a rerender. + end + end + + self.onContentResize = function() + debugPrint("onContentResize") + + if not self.alive or self.sizeDebounce or not self.state.ready then + debugPrint(" Skipping onContentResize") + return + end + self:setState({}) -- Force a rerender. + end + + self.anchorCanvasPosition = 0 + self.anchorFramePosition = 0 + self.anchorOffset = 0 + + self.metadata = {} + self.pools = {} + self.refpool = {} + + self.scrollingBackward = false + self.scrollingForward = false + + self.lastLoadPrevItems = nil + self.lastLoadNextItems = nil + + self.alive = true + + -- Store the list of props to not pass on to the underlying scrolling frame. + self.propsToClear = {} + for k, _ in pairs(Scroller.defaultProps) do + self.propsToClear[k] = Cryo.None + end + + -- This will get updated shortly, but one render will happen before state.ready is set + self:setState({ + ready = false, + lastFocusLock = nil, + padding = 0, + size = 0, + }) +end + +function Scroller:willUnmount() + if self.motor then + self.motor:destroy() + end + + self.alive = false +end + +-- Find which element is currently closest to the anchor position. +function Scroller:recalculateAnchor() + debugPrint("recalculateAnchor") + + -- Find the index of the element at the appropriate position + local index = self:findIndexAt( + self:absoluteToCanvasPosition(self.absoluteAnchorLocation), self.state.anchor.index, false) + + self.anchorCanvasPosition = self:getAnchorCanvasFromIndex(index) + self.anchorFramePosition = self:getAnchorFrameFromIndex(index) + + local delta + if index == self.state.anchor.index then + debugPrint(" Current anchor still works") + return 0, {} + elseif index < self.state.anchor.index then + delta = -1 + else + delta = 1 + end + + debugPrint(" New anchor at index", index) + + -- Store the new anchor's details + debugPrint(" New anchor at canvas position", self.anchorCanvasPosition) + debugPrint(" New anchor at frame position", self.anchorFramePosition) + return delta, { + anchor = {index=index, id=self:getID(index)}, + } +end + +-- Move all the rendered elements up or down to put the anchor back where it was. +function Scroller:resetAnchorPosition() + debugPrint("resetAnchorPosition") + debugPrint(" Anchor index is", self.state.anchor.index) + local offset = self:getAnchorCanvasPosition() + debugPrint(" Anchor is at", offset) + debugPrint(" Anchor offset is", self.anchorOffset) + local newPos = self.anchorCanvasPosition - self.anchorOffset + debugPrint(" Anchor should be at", newPos) + local diff = Round.nearest(newPos - offset) + if diff ~= 0 then + debugPrint(" Changing padding from", self.state.padding, "to", (self.state.padding + diff)) + self.anchorCanvasPosition = self.anchorCanvasPosition - self.anchorOffset + self.anchorOffset = 0 + return {padding = self.state.padding + diff} + else + return {} + end +end + +-- Get the current padding from the UIPadding child. +function Scroller:getCurrentPadding() + local pad = self:getCurrent().padding + -- Only one of these will be non-zero + return pad.Size.X.Offset + pad.Size.Y.Offset +end + +-- Move the top and bottom of the range to be rendered up and down to make sure +-- enough things are being rendered. +function Scroller:recalculateBounds(trimTrailing, trimLeading) + debugPrint("recalculateBounds") + debugPrint(" Leading index was", self.state.lead.index) + debugPrint(" Trailing index was", self.state.trail.index) + + local anchorPos = self:getAnchorCanvasPosition() + local mountTop = anchorPos - self.mountAboveAnchor + local mountBottom = anchorPos + self.mountBelowAnchor + debugPrint(" Target for top at", mountTop) + debugPrint(" Target for bottom at", mountBottom) + + local topIndex = self:findIndexAt(mountTop, nil, true) + debugPrint(" Found new top index at", topIndex) + local bottomIndex = self:findIndexAt(mountBottom, nil, true) + debugPrint(" Found new bottom index at", bottomIndex) + + local leadIndex = math.max(topIndex, bottomIndex) + if leadIndex < self.state.lead.index and not trimLeading then + leadIndex = self.state.lead.index + end + + local trailIndex = math.min(topIndex, bottomIndex) + if trailIndex > self.state.trail.index and not trimTrailing then + trailIndex = self.state.trail.index + end + + if trailIndex < self.state.trail.index or leadIndex > self.state.lead.index then + debugPrint(" Changing leading index to", leadIndex) + debugPrint(" Changing trailing index to", trailIndex) + return { + trail = {index=trailIndex, id=self:getID(trailIndex)}, + lead = {index=leadIndex, id=self:getID(leadIndex)}, + } + else + return {} + end +end + +-- Find the index of the element that overlaps the given canvas-relative position. +function Scroller:findIndexAt(targetPos, hintIndex, extrapolate) + debugPrint(" findIndexAt") + -- Get the distance from the hinted index or the anchor. + local currentIndex = hintIndex or self.state.anchor.index + local currentDist = self:distanceToPosition(currentIndex, targetPos) + debugPrint(" Searching from index", currentIndex) + debugPrint(" Position is", currentDist, "from", targetPos) + if currentDist == 0 then + return currentIndex + end + + -- Get the distance from one end of the list. + local nextIndex = (currentDist < 0) and self.state.trail.index or self.state.lead.index + debugPrint(" Nearest end at", nextIndex) + if currentIndex == nextIndex then + debugPrint(" Hint index already at end") + -- If the target position lies outside of the loaded elements. + if currentIndex + currentDist < self.state.trail.index + or currentIndex + currentDist > self.state.lead.index then + debugPrint(" Target out of bounds") + if not extrapolate then + -- Do not extrapolate. Return the closest loaded element. + return currentIndex + end + + -- Extrapolate using the estimated item size. + local delta = Round.awayFromZero(currentDist / self.props.estimatedItemSize) + debugPrint(" Estimating target at", delta, "from end") + return math.min(math.max(currentIndex + delta, 1), self.state.listSize) + end + else + local nextDist = self:distanceToPosition(nextIndex, targetPos) + debugPrint(" End is", nextDist, "from target") + if nextDist == 0 then + return nextIndex + end + + -- If the target position lies outside of the loaded elements. + if currentDist * nextDist > 0 then + debugPrint(" Target out of bounds") + if not extrapolate then + -- Do not extrapolate. Return the closest loaded element. + return nextIndex + end + + -- Extrapolate using the estimated item size. + local delta = Round.awayFromZero(nextDist / self.props.estimatedItemSize) + debugPrint(" Estimating target at", delta, "from end") + return math.min(math.max(nextIndex + delta, 1), self.state.listSize) + end + + -- Jump to the approximate location of the target based on the distance from current and next. + local totalDist = math.abs(currentDist) + math.abs(nextDist) + local indexCount = math.abs(currentIndex - nextIndex) + currentIndex = currentIndex + Round.nearest(indexCount * currentDist / totalDist) + currentDist = self:distanceToPosition(currentIndex, targetPos) + debugPrint(" Interpolated index is", currentIndex) + debugPrint(" Distance from interpolated index is", currentDist) + end + + -- Linear search from best guess index. + while currentDist ~= 0 do + if currentDist < 0 then + currentIndex = currentIndex - 1 + else + currentIndex = currentIndex + 1 + end + currentDist = self:distanceToPosition(currentIndex, targetPos) + debugPrint(" Distance after step is", currentDist) + end + + return currentIndex +end + +function Scroller:trimTop(newState) + local reverse = isReverse[self.props.orientation] + local padding = newState.padding or self.state.padding + local topIndex = reverse and self.state.lead.index or self.state.trail.index + + if padding == self.props.dragBuffer then + return {} + end + + local keepGoing = false + + -- trimTop only happens under very specific circumstances. + -- in a top down list + if not reverse then + -- if the top most element is at the top + if topIndex == 1 then + -- and if the padding is greater than the distance between the top of the frame and the top element's starting position + if padding > self.relativeAnchorLocation + self.props.dragBuffer then + keepGoing = true + end + end + else -- OR in a bottom up list + -- if the top most element is at the top + if topIndex == self.state.listSize then + -- and if the nonzero padding is equivalent to position of the top element + the size of the dragBuffer REEXAMINE THIS CONDITIONAL + if padding > self.props.dragBuffer then + keepGoing = true + end + end + end + + if not keepGoing then + return {} + end + + local size = newState.size or self.state.size + + local newPadding = self.props.dragBuffer + if not reverse then + newPadding = self.relativeAnchorLocation + newPadding + end + local paddingDiff = padding - newPadding + local newSize = Round.nearest(size - paddingDiff) + local newAnchorPos = self.anchorCanvasPosition - paddingDiff + debugPrint(" Anchor moved from", self.anchorCanvasPosition, "to", newAnchorPos) + self.anchorCanvasPosition = newAnchorPos + + if reverse then + local newAnchorFramePosition = self.anchorFramePosition - paddingDiff + debugPrint(" Anchor frame position moved from", self.anchorFramePosition, "to", newAnchorFramePosition) + self.anchorFramePosition = newAnchorFramePosition + end + + debugPrint(" Trimming padding from", padding, "to", newPadding) + debugPrint(" Reducing canvas size from", size, "to", newSize) + return { + size = newSize, + padding = newPadding, + } +end + +function Scroller:trimBottom(newState) + local reverse = isReverse[self.props.orientation] + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + local padding = newState.padding or self.state.padding + local size = newState.size or self.state.size + + local absSize = self:measure(self:getCurrent().AbsoluteSize) + local minSize = absSize + local childSize = self:getChildSize(bottomIndex) + local bottomPos = self:getChildCanvasPosition(bottomIndex) + childSize + local newSize = bottomPos + self.props.dragBuffer + + if not reverse then + minSize = minSize - math.max(0, padding) + newSize = Round.nearest(newSize + self.relativeAnchorLocation) + else -- reverse + local oldPadding = self:getCurrentPadding() + bottomPos = self:getChildCanvasPosition(bottomIndex) + childSize - (oldPadding - padding) + newSize = Round.nearest(bottomPos + self.props.dragBuffer + (absSize - self.relativeAnchorLocation)) + end + + local returnState = {} + if newSize > minSize and newSize < Round.nearest(size) then + returnState.size = newSize + debugPrint(" Changing canvas size from", size, "to", returnState.newSize) + + if reverse then + -- Changing the canvas size without changing the padding requires the anchorFramePosition to be + -- reset to prevent moveToAnchor from adjusting the position of the canvas + self.anchorFramePosition = 0 + if padding > 0 then + + -- When the scroller is reversed, the start of the list can have excess padding from when the canvas has been + -- expanded while loading more items. When that is the case, trim the padding along with the canvas size. + local sizeDiff = Round.nearest(size - newSize) + local newAnchorPos = self.anchorCanvasPosition - sizeDiff + local newPadding = math.max(0, padding - sizeDiff) + + debugPrint(" Moving anchor from", self.anchorCanvasPosition, "to", newAnchorPos) + self.anchorCanvasPosition = newAnchorPos + + returnState.padding = newPadding + debugPrint(" Changing padding from", padding, "to", returnState.padding) + end + end + elseif bottomPos + self.props.dragBuffer < size then + if not reverse then + -- Shift the list so that the bottom element is touching the bottom of the frame + -- while keeping the canvas size the same. This is to ensure that when scrolling to the top of the + -- list there is still the anchor space between the top element and the top of the frame + local diff = Round.nearest(size - bottomPos + self.props.dragBuffer) + if diff ~= 0 then + local newPadding = padding + diff + local newAnchorPos = self.anchorCanvasPosition + diff + + debugPrint(" Moving anchor from", self.anchorCanvasPosition, "to", newAnchorPos) + self.anchorCanvasPosition = newAnchorPos + + -- Since the anchor position does not update before moveToAnchor is called, update the + -- anchorFramePosition here + self.anchorFramePosition = self.anchorFramePosition + diff + + returnState.padding = newPadding + debugPrint(" Changing padding from", padding, "to", returnState.padding) + end + end + end + + return returnState +end + + +-- When the ends of the list are loaded, adjust the padding and canvas size so that the ends of +-- the canvas do not extend past the items in the list +-- This adjustment preserves the space between the start of the list and the anchor +-- Eg when the anchorLocation is (0.5, 0) on a 100px tall frame, there is 50px of empty space +-- between the leading edge of the frame and the first element. +function Scroller:adjustEdges(newState) + debugPrint("adjustEdges") + local reverse = isReverse[self.props.orientation] + local topIndex = reverse and self.state.lead.index or self.state.trail.index + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + + if not reverse then + if topIndex == 1 then + return self:trimTop(newState) + elseif bottomIndex == self.state.listSize then + return self:trimBottom(newState) + end + else -- reverse + if bottomIndex == 1 then + return self:trimBottom(newState) + elseif topIndex == self.state.listSize then + return self:trimTop(newState) + end + end + + return {} +end + +-- Expand the size of the scrolling frame's canvas to make sure everything still fits. +function Scroller:expandCanvas(newState) + debugPrint("expandCanvas") + local reverse = isReverse[self.props.orientation] + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + + local size = newState.size or self.state.size + local originalSize = size + local newPadding = newState.padding or self.state.padding + local originalPadding = newPadding + local oldPadding = self:getCurrentPadding() + + local bottomPos = self:getChildCanvasPosition(bottomIndex) + + self:getChildSize(bottomIndex) - (oldPadding - newPadding) + + local bottomTarget = bottomPos + self.props.dragBuffer + + debugPrint(" Bottom of bottom child is", bottomPos) + debugPrint(" Canvas size is", originalSize) + debugPrint(" Canvas bottom should be", bottomTarget) + + local minSize = self:measure(self:getCurrent().AbsoluteSize) - math.max(0, newPadding) + if originalSize < minSize then + size = minSize + debugPrint(" Expanding canvas to minimum size", size) + end + + if originalSize < bottomTarget then + -- Plus footer + size = math.max(bottomTarget, size) + debugPrint(" Expanding canvas bottom to size", size) + end + + debugPrint(" Padding is", newPadding) + debugPrint(" Padding should be at least", self.props.dragBuffer) + if newPadding < self.props.dragBuffer then + -- Minus header + local diff = newPadding - self.props.dragBuffer + size = size - diff + self.anchorCanvasPosition = self.anchorCanvasPosition - diff + newPadding = self.props.dragBuffer + debugPrint(" Expanding canvas top to size", size) + debugPrint(" Shifting anchor to", self.anchorCanvasPosition) + debugPrint(" Padding is now", newPadding) + end + + if size ~= originalSize or newPadding ~= originalPadding then + debugPrint(" Changing size from", originalSize, "to", size) + debugPrint(" Changing padding from", originalPadding, "to", newPadding) + return { + size = size, + padding = newPadding, + } + else + debugPrint(" No changes to size or padding") + return {} + end +end + +-- Try and get the canvas as close to correct as possible this rendering pass. +function Scroller:adjustCanvas(trimTrailing, trimLeading) + debugPrint("adjustCanvas") + + local newState = Cryo.Dictionary.join( + self:resetAnchorPosition(), + self:recalculateBounds(trimTrailing, trimLeading) + ) + + if not newState.trail and not newState.lead then + newState = Cryo.Dictionary.join(newState, self:expandCanvas(newState)) + newState = Cryo.Dictionary.join(newState, self:adjustEdges(newState)) + end + + if Cryo.isEmpty(newState) then + debugPrint(" No state changes after adjustment") + return false + end + + self:setState(newState) + return true +end + +-- Move the canvas position so that the anchor element is in the same place on the screen. +function Scroller:scrollToAnchor() + if self.motorActive then + return + end + debugPrint("scrollToAnchor") + if self.indexChanged == nil then + self:moveToAnchor() + end + + local newIndex = self.indexChanged.newIndex + local previousIndex = self.indexChanged.oldIndex + debugPrint(" newIndex", newIndex) + debugPrint(" previousIndex", previousIndex) + + local oldPos = self:measure(self:getCurrent().CanvasPosition) + self.relativeAnchorLocation + local newPos = self:getAnchorCanvasFromIndex(newIndex) + + debugPrint(" old anchor pos", oldPos) + debugPrint(" new anchor pos", newPos) + + self.indexChanged.currentPos = oldPos + self.indexChanged.newPos = newPos + self.motorActive = true + self.springLock = self.springLock + 1 + + local delta = newPos - oldPos + debugPrint(" delta", delta) + self.motor = Otter.createSingleMotor(0) + self.motor:onStep(self.motorOnStep) + self.motor:onComplete(self.motorOnComplete) + self.motor:setGoal(Otter.spring(delta, self.props.animateOptions)) +end + +-- Move the canvas position so that the anchor element is in the same place on the screen. +function Scroller:moveToAnchor() + debugPrint("moveToAnchor") + if self.motorActive then + return + end + if self:isScrollingWithElasticBehavior() then + return + end + + local currentPos = self:getAnchorFramePosition() + debugPrint(" Anchor was at frame position", self.anchorFramePosition) + debugPrint(" Anchor is currently at frame position", currentPos) + local newPos = self:measure(self:getCurrent().CanvasPosition) + currentPos - self.anchorFramePosition + debugPrint(" Canvas should scroll to", newPos) + local current = self:getCurrent() + local maxPos = math.max(0, self:measure(current.CanvasSize).Offset - self:measure(current.AbsoluteSize)) + + self:setScroll(newPos) + if newPos < 0 then + debugPrint(" Canvas scroll limited to 0, was", newPos) + self.anchorOffset = Round.towardsZero(newPos) + elseif newPos >= maxPos then + debugPrint(" Canvas scroll limited to", maxPos, ", was", newPos) + self.anchorOffset = Round.towardsZero(newPos - maxPos) + else + debugPrint(" Clearing anchorOffset") + self.anchorOffset = 0 + end +end + +-- Prevent scrolling when experiencing elastic behavior on touch devices +function Scroller:isScrollingWithElasticBehavior() + -- When the canvas is resized the bottom of the canvas is bigger than the canvas for + -- the first update. Since a resize is not an elastic scroll skip these checks after a resize + if self.resized then + return false + end + + -- Check if the top of the list has scrolled past the frame because of ElasticBehavior + local reverse = isReverse[self.props.orientation] + local topIndex = reverse and self.state.lead.index or self.state.trail.index + local startOfListIndex = reverse and self.state.listSize or 1 + if self:measure(self:getCurrent().CanvasPosition) < 0 and topIndex == startOfListIndex then + return true + end + + -- Check if the bottom of the list has scrolled past the frame because of ElasticBehavior + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + local absSize = self:measure(self:getCurrent().AbsoluteSize) + local endOfListIndex = reverse and 1 or self.state.listSize + local bottomOfCanvas = Round.nearest(self:measure(self:getCurrent().CanvasPosition) + absSize) + local canvasSize = Round.nearest(self:measure(self:getCurrent().CanvasSize).Offset) + + if bottomOfCanvas > canvasSize and bottomIndex == endOfListIndex then + return true + end + + return false +end + +-- Call loadNext and loadPrevious if needed. +function Scroller:loadMore() + debugPrint("loadMore") + if self.props.loadPrevious and + self.state.trail.index <= self.props.loadingBuffer and + self.props.itemList ~= self.lastLoadPrevItems then + debugPrint(" Calling loadPrevious") + self.lastLoadPrevItems = self.props.itemList + self.props.loadPrevious() + end + if self.props.loadNext and + self.state.lead.index > self.state.listSize - self.props.loadingBuffer and + self.props.itemList ~= self.lastLoadNextItems then + debugPrint(" Calling loadNext") + self.lastLoadNextItems = self.props.itemList + self.props.loadNext() + end +end + +-- Set the current canvas position according to Orientation without calling +-- onScroll. +function Scroller:setScroll(pos) + debugPrint(" Scrolling to", pos) + self.scrollDebounce = true + self:getCurrent().CanvasPosition = isVertical[self.props.orientation] + and Vector2.new(self:getCurrent().CanvasPosition.X, pos) + or Vector2.new(pos, self:getCurrent().CanvasPosition.Y) + self.scrollDebounce = false +end + +-- Scroll by a relative amount +function Scroller:scrollRelative(amount) + debugPrint(" Current CanvasPosition", self:getCurrent().CanvasPosition) + debugPrint("self.motorActive", self.motorActive) + self:setScroll(self:measure(self:getCurrent().CanvasPosition) + amount, true) + self.onScroll(self:getCurrent()) +end + + +-- Returns the signed distance from the element to the given canvas-relative +-- position, or 0 if the element overlaps it. The sign of the distance is +-- relative to the list indices. For this distance calculation, the padding +-- between elements is considered part of the current element. Returns nil if +-- the element is not currently rendered. +function Scroller:distanceToPosition(index, pos) + local child = self:getRbx(index) + if not child then + return nil + end + + local childTop = self:absoluteToCanvasPosition(self:measure(child.AbsolutePosition)) - self.itemPadding + local childSize = self:measure(child.AbsoluteSize) + 2 * self.itemPadding + + return Distance.fromPointToRangeSigned(pos, childTop, childSize) * direction[self.props.orientation] +end + +-- Get the canvas-relative position of the current anchor element. +function Scroller:getAnchorCanvasPosition() + return self:getAnchorCanvasFromIndex(self.state.anchor.index) +end + +function Scroller:getAnchorCanvasFromIndex(index) + local scale = self.props.anchorLocation.Scale + if not isReverse[self.props.orientation] then + scale = 1 - scale + end + + return Round.nearest(self:getChildCanvasPosition(index) + scale * self:getChildSize(index)) +end + +-- Get the frame-relative position of the current anchor element. +function Scroller:getAnchorFramePosition() + return self:getAnchorFrameFromIndex(self.state.anchor.index) +end + +function Scroller:getAnchorFrameFromIndex(index) + local scale = self.props.anchorLocation.Scale + if not isReverse[self.props.orientation] then + scale = 1 - scale + end + + return Round.nearest(self:getChildFramePosition(index) + scale * self:getChildSize(index)) + - self.relativeAnchorLocation +end + +-- Convert an AbsolutePosition to a position relative to the top-left corner of the canvas. +function Scroller:absoluteToCanvasPosition(position) + local current = self:getCurrent() + local canvas = current.CanvasPosition + local absolute = current.AbsolutePosition + return position + self:measure(canvas) - self:measure(absolute) +end + +-- Convert an AbsolutePosition to a position relative to the top-left corner of the scrolling frame. +function Scroller:absoluteToFramePosition(position) + local current = self:getCurrent() + local absolute = current.AbsolutePosition + return position - self:measure(absolute) +end + +-- Convert a position relative to the frame to a position relative to the canvas. +function Scroller:frameToCanvasPosition(position) + local current = self:getCurrent() + local canvas = current.CanvasPosition + return position + self:measure(canvas) +end + +-- Get the canvas-relative position of the element at the specified index. +function Scroller:getChildCanvasPosition(index) + local current = self:getRbx(index) + return current and self:absoluteToCanvasPosition(self:measure(current.AbsolutePosition)) or 0 +end + +-- Get the frame-relative position of the element at the specified index. +function Scroller:getChildFramePosition(index) + local current = self:getRbx(index) + return current and self:absoluteToFramePosition(self:measure(current.AbsolutePosition)) or 0 +end + +-- Get the absolute size of the element at the specified index. +function Scroller:getChildSize(index) + local current = self:getRbx(index) + return current and self:measure(current.AbsoluteSize) or 0 +end + +-- Get the ID of an element at a specific index. +function Scroller:getID(index) + return self.props.identifier(self.props.itemList[index]) +end + +-- Create or update a metadata entry for the given element. This can't use +-- self.props in willUpdate as any props it uses could be out of date. +function Scroller:updateMetadata(id, item, props) + local meta = self.metadata[id] + if not meta then + meta = {} + self.metadata[id] = meta + end + + if not meta.name then + local elem = props.renderItem(item, false) + meta.class = tostring(elem.component) + local pool = self:getKeyPool(meta.class) + meta.name = pool:get() + end + + if not self.refpool[meta.name] then + self.refpool[meta.name] = Roact.createRef() + end + meta.ref = self.refpool[meta.name] +end + +-- Clear the metadata for an element that is being unloaded. +function Scroller:clearMetadata(id) + local meta = self.metadata[id] + if not meta then + return + end + + -- Not releasing the names seems like it would be a memory leak, but this + -- relies on the fact that the key pool does not track in use keys. Rather, + -- the key tracks which pool it came from. So if nothing is using an + -- unreleased key, it will be garbage collected and never reused. + if not Cryo.List.find(self.props.recyclingDisabledFor, meta.class) then + meta.name:release() + end + meta.name = nil + meta.ref = nil +end + +-- Get the key pool for the given class of elements, or create a new one if that doesn't exist yet. +function Scroller:getKeyPool(class) + if not self.pools[class] then + self.pools[class] = KeyPool.new(class) + end + return self.pools[class] +end + +-- Get the metadata info for the element at the specified index. +function Scroller:getMetadata(index) + return self.metadata[self:getID(index)] +end + +-- Get the current Roblox instance from the ref stored in the metadata. +function Scroller:getRbx(index) + local meta = self:getMetadata(index) + return meta and meta.ref and meta.ref.current +end + +-- Return X or Y depending on the orientation. +function Scroller:measure(vecOrUDim2) + return isVertical[self.props.orientation] and vecOrUDim2.Y or vecOrUDim2.X +end + +-- Get the current ScrollingFrame instance. +function Scroller:getCurrent() + return self:getRef().current +end + +function Scroller:getRef() + -- Make sure to get the ref from props if that exists. + return self.props[Roact.Ref] or self._ref +end + +return Scroller diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/findNewIndices.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/findNewIndices.lua new file mode 100644 index 0000000..b00ee3b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/findNewIndices.lua @@ -0,0 +1,67 @@ +-- Find the new indicies of the trailing, anchor and leading elements. +-- props is expected to contain itemList, the identifier function and the maximum search distance. +-- state is expected to contain the old top, anchor and bottom indices and ids. +return function(props, state) + local topIndex = state.trail.index + local topID = state.trail.id + local anchorIndex = state.anchor.index + local anchorID = state.anchor.id + local bottomIndex = state.lead.index + local bottomID = state.lead.id + + local listSize = #props.itemList + + -- If too much got deleted and the previous anchor index is off the bottom of the list, start the search from + -- the bottom. + if topIndex > listSize then + topIndex = listSize + end + if anchorIndex > listSize then + anchorIndex = listSize + end + if bottomIndex > listSize then + bottomIndex = listSize + end + + -- No access to self:getID + local getID = function(index) + return props.identifier(props.itemList[index]) + end + local topStill = getID(topIndex) == topID + local anchorStill = getID(anchorIndex) == anchorID + local bottomStill = getID(bottomIndex) == bottomID + if topStill and anchorStill and bottomStill then + -- Nothing important moved + return topIndex, anchorIndex, bottomIndex + end + + local step = 0 + local foundTop = topStill and topIndex or nil + local foundAnchor = nil + local foundBottom = bottomStill and bottomIndex or nil + + -- Scan outward from the old anchor index until we find the top and bottom or hit the max distance + local deltas = {top=-1, bottom=1} + repeat + for _, delta in pairs(deltas) do + local pos = anchorIndex + delta * step + + if pos >= 1 and pos <= listSize then + local id = getID(pos) + if id == topID then + foundTop = pos + end + if id == anchorID then + foundAnchor = pos + end + if id == bottomID then + foundBottom = pos + end + end + end + + step = step + 1 + until (foundTop and foundAnchor and foundBottom) or step > props.maximumSearchDistance + + return foundTop, foundAnchor, foundBottom +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/relocateIndices.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/relocateIndices.lua new file mode 100644 index 0000000..79b926d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/relocateIndices.lua @@ -0,0 +1,57 @@ +local Round = require(script.Parent.Round) + +-- Returns the three new indices with any nils filled in and any misorderings corrected. +-- new and old should be tables containing an anchorIndex, a leadIndex and a trailIndex. +return function(new, old, listSize) + local newAnchor = new.anchorIndex + local newLead = new.leadIndex + local newTrail = new.trailIndex + + -- There are 8 possibilities here as any combination of these could be deleted. Also, we can't use findIndexAt + -- here since that requires access to the children's measurements. + if not newAnchor then + if newLead and newTrail then + -- Estimate that the new anchor is proportionally the same distance from the lead and trail indices. + if newLead == newTrail then + -- Guard against divide by zero. + newAnchor = newLead + else + local oldRatio = (old.anchorIndex - old.leadIndex) / (old.trailIndex - old.leadIndex) + newAnchor = Round.nearest((newTrail - newLead) * oldRatio + newLead) + newAnchor = math.min(math.max(newAnchor, 1), listSize) + end + elseif newLead then + -- Given only the new leading index, estimate that the new anchor is the same distance away as it was. + newAnchor = newLead + old.anchorIndex - old.leadIndex + newAnchor = math.min(math.max(newAnchor, 1), listSize) + elseif newTrail then + -- Given only the new trailing index, estimate that the new anchor is the same distance away as it was. + newAnchor = newTrail + old.anchorIndex - old.trailIndex + newAnchor = math.min(math.max(newAnchor, 1), listSize) + else + -- Everything is gone. Just reuse the same index if that's still within the bounds of the list. + newAnchor = math.min(math.max(old.anchorIndex, 1), listSize) + end + end + + -- If the leading and trailing indices haven't been worked out yet, estimate that the new ones should be the + -- same distance from the anchor as the old ones were. + if not newTrail then + newTrail = newAnchor + old.trailIndex - old.anchorIndex + newTrail = math.min(math.max(newTrail, 1), listSize) + end + if not newLead then + newLead = newAnchor + old.leadIndex - old.anchorIndex + newLead = math.min(math.max(newLead, 1), listSize) + end + + -- Make sure the resulting indices are in the right order. + local minIndex = math.min(newAnchor, newLead, newTrail) + local maxIndex = math.max(newAnchor, newLead, newTrail) + + return { + trailIndex = minIndex, + anchorIndex = newAnchor, + leadIndex = maxIndex, + } +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/ComplexThing.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/ComplexThing.lua new file mode 100644 index 0000000..bf7e225 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/ComplexThing.lua @@ -0,0 +1,56 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +ComplexThing = Roact.PureComponent:extend("ComplexThing") + +ComplexThing.defaultProps = { + -- things start to lag at 5? + nestedLayer = 3, + Size = UDim2.fromScale(0.5, 0.5), +} + +function ComplexThing:render() + --temporary hard limit + if self.props.nestedLayer > 6 then + return Roact.createElement("Frame", { + Size = UDim2.fromOffset(100, 100) + }) + end + + local children = {} + + if self.props.nestedLayer > 1 then + children = { + ["TL"..self.props.nestedLayer] = Roact.createElement(ComplexThing, { + Position = UDim2.fromScale(0, 0), + nestedLayer = self.props.nestedLayer - 1, + }), + ["TR"..self.props.nestedLayer] = Roact.createElement(ComplexThing, { + Position = UDim2.fromScale(0.5, 0), + nestedLayer = self.props.nestedLayer - 1, + }), + ["BL"..self.props.nestedLayer] = Roact.createElement(ComplexThing, { + Position = UDim2.fromScale(0, 0.5), + nestedLayer = self.props.nestedLayer - 1, + }), + ["BR"..self.props.nestedLayer] = Roact.createElement(ComplexThing, { + Position = UDim2.fromScale(0.5, 0.5), + nestedLayer = self.props.nestedLayer - 1, + }), + } + end + + local r = math.random(255) + local g = math.random(255) + local b = math.random(255) + + return Roact.createElement("Frame", { + Size = self.props.Size, + Position = self.props.Position, + BackgroundColor3 = Color3.fromRGB(r, g, b), + BorderSizePixel = self.props.Size.X.Scale == 0.5 and 0 or 4, + BorderColor3 = Color3.fromRGB(255, 255, 255), + }, children) +end + +return ComplexThing diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/FragmentThing.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/FragmentThing.lua new file mode 100644 index 0000000..98d53b1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/FragmentThing.lua @@ -0,0 +1,17 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +return function(props) + return Roact.createFragment({ + foo = Roact.createElement("Frame", { + BackgroundColor3 = props.color, + Size = UDim2.new(0, props.width, 0, props.width), + LayoutOrder = props.LayoutOrder, + }), + bar = Roact.createElement("Frame", { + BackgroundColor3 = props.color, + Size = UDim2.new(0, props.width, 0, props.width), + LayoutOrder = props.LayoutOrder + 1, + }) + }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/FunctionThing.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/FunctionThing.lua new file mode 100644 index 0000000..a70d3fa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/FunctionThing.lua @@ -0,0 +1,10 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +return function(props) + return Roact.createElement("Frame", { + BackgroundColor3 = props.color, + Size = UDim2.new(0, props.width, 0, props.width), + LayoutOrder = props.LayoutOrder, + }) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/NestedThing.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/NestedThing.lua new file mode 100644 index 0000000..64cdd37 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/NestedThing.lua @@ -0,0 +1,24 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +local Thing = Roact.PureComponent:extend("Thing") + +function Thing:render() + return Roact.createElement("Frame", { + BackgroundColor3 = self.props.color, + Size = UDim2.new(0, self.props.width, 0, self.props.width), + LayoutOrder = self.props.LayoutOrder, + }) +end + +local ThingRoot = Roact.PureComponent:extend("ThingRoot") + +function ThingRoot:render() + return Roact.createElement(Thing, { + color = self.props.color, + width = self.props.width, + LayoutOrder = self.props.LayoutOrder, + }) +end + +return ThingRoot diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/ResizingThing.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/ResizingThing.lua new file mode 100644 index 0000000..9385d87 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/ResizingThing.lua @@ -0,0 +1,38 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +local Thing = Roact.PureComponent:extend("Thing") + +function Thing:init() + self.state = { + clicked = false, + token = nil, + } +end + +function Thing.getDerivedStateFromProps(nextProps, lastState) + -- Use the token we're already being passed as a surrogate lock, just for this story. + -- Normally, this would be a separate prop. + if nextProps.token ~= lastState.token then + return { + clicked = false, + token = nextProps.token, + } + end + return nil +end + +function Thing:render() + return Roact.createElement("Frame", { + BackgroundColor3 = self.props.color, + Size = UDim2.new(1, -self.props.width, 0, self.state.clicked and 10 or self.props.width), + LayoutOrder = self.props.LayoutOrder, + [Roact.Event.InputBegan] = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self:setState({clicked = not self.state.clicked}) + end + end, + }) +end + +return Thing diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/animatedScroller.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/animatedScroller.story.lua new file mode 100644 index 0000000..d7658a8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/animatedScroller.story.lua @@ -0,0 +1,154 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Story") + +function Story:init() + local startingItems = {} + for i = 1, self.props.startingItems or 50 do + table.insert(startingItems, i) + end + self:setState({ + items = startingItems, + lock = 1, + width = 260, + itemSize = 30, + }) + + self.focus = self.props.startingFocus or 1 +end + +function Story:render() + return Roact.createFragment({ + output = Roact.createElement("Frame", { + Size = UDim2.new(0, self.state.width, 0, self.state.itemSize + 30), + BackgroundTransparency = 1, + },{ + scroller = Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + orientation = Scroller.Orientation.Right, + padding = UDim.new(0, 5), + itemList = self.state.items, + focusIndex = self.focus, + focusLock = self.state.lock, + anchorLocation = UDim.new(1, -15), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + dragBuffer = 120, + animateScrolling = true, + identifier = function(item) + assert(item) + return item + end, + renderItem = function(item, _) + return Roact.createElement("TextLabel", { + Size = UDim2.new(0, self.state.itemSize, 0, self.state.itemSize), + Text = tostring(item), + BackgroundColor3 = Color3.fromRGB(255, item*5, (51 - item)*5), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + onScrollUpdate = function(data) + self.focus = data.anchorIndex + self.animationActive = data.animationActive + end, + }, self.props, { + startingItems = Cryo.None, + startingFocus = Cryo.None, + })), + }), + scrollLeft = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(0, 0, 0, 100), + Text = "<", + [Roact.Event.Activated] = function() + if self.animationActive then + return + end + self.focus = math.max(self.focus - 4, 1) + self:setState({lock = self.state.lock + 1}) + end, + }), + scrollRight = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(0, 270, 0, 100), + Text = ">", + [Roact.Event.Activated] = function() + if self.animationActive then + return + end + self.focus = math.min(self.focus + 4, #self.state.items) + self:setState({lock = self.state.lock + 1}) + end, + }), + decreaseWidth = Roact.createElement("TextButton", { + Size = UDim2.new(0, 60, 0, 30), + Position = UDim2.new(0, 90, 0, 100), + Text = "Width -", + [Roact.Event.Activated] = function() + if self.animationActive then + return + end + self.focus = math.max(self.focus - 4, 1) + self:setState({ + width = self.state.width - 10, + }) + end, + }), + increaseWidth = Roact.createElement("TextButton", { + Size = UDim2.new(0, 60, 0, 30), + Position = UDim2.new(0, 150, 0, 100), + Text = "Width +", + [Roact.Event.Activated] = function() + if self.animationActive then + return + end + self:setState({ + width = self.state.width + 10, + }) + end, + }), + removeItem = Roact.createElement("TextButton", { + Size = UDim2.new(0, 60, 0, 30), + Position = UDim2.new(0, 30, 0, 100), + Text = "Item -", + [Roact.Event.Activated] = function() + if self.animationActive then + return + end + local items = self.state.items + local lock = nil + if self.focus >= #items then + self.focus = #items - 1 + lock = self.state.lock + 1 + end + self:setState({ + items = Cryo.List.removeIndex(items, #items), + lock = lock, + }) + end, + }), + addItem = Roact.createElement("TextButton", { + Size = UDim2.new(0, 60, 0, 30), + Position = UDim2.new(0, 210, 0, 100), + Text = "Item +", + [Roact.Event.Activated] = function() + if self.animationActive then + return + end + local items = self.state.items + self:setState({ + items = Cryo.List.join(items, {#items + 1}), + }) + end, + }), + }) +end + +return Story diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/chat.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/chat.story.lua new file mode 100644 index 0000000..23c0c88 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/chat.story.lua @@ -0,0 +1,87 @@ +local HttpService = game:GetService("HttpService") + +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Box = function(props) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, 40), + Text = props.text, + }) +end + +local Story = Roact.PureComponent:extend("Story") + +function Story:init() + self.state = { + items = {}, + lock = 0, + index = 1, + size = Vector2.new(400, -100), + } +end + +function Story:render() + return Roact.createFragment({ + scroller = Roact.createElement(Scroller, { + BackgroundColor3 = Color3.fromRGB(56, 19, 18), + Size = UDim2.new(0, self.state.size.X, 1, self.state.size.Y), + Position = UDim2.new(0, 50, 0, 50), + ScrollBarThickness = 8, + padding = UDim.new(0, 5), + orientation = Scroller.Orientation.Down, + itemList = self.state.items, + focusLock = self.state.lock, + focusIndex = self.state.index, + anchorLocation = UDim.new(0, 0), + estimatedItemSize = 40, + dragBuffer = 0, + extraProps = self.state.items, + identifier = function(item) + return item.guid + end, + renderItem = function(item, _) + return Roact.createElement(Box, item) + end, + onScrollUpdate = function(data) + self.indexData = data + end + }), + textbox = Roact.createElement("TextBox", { + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(1, -100, 0, 0), + [Roact.Event.FocusLost] = function(rbx, entered) + if entered and rbx.Text ~= "" then + local newItem = { + text = rbx.Text, + guid = HttpService:GenerateGUID(false), + } + + local newState + if self.state.items then + newState = { items = Cryo.List.join(self.state.items, { newItem })} + + if self.indexData and self.indexData.anchorIndex == #self.state.items then + newState.lock = self.state.lock + 1 + newState.index = #newState.items + end + else + newState = { + items = { newItem }, + index = 1, + lock = 1, + } + end + + rbx.Text = "" + self:setState(newState) + end + end + }), + }) +end + +return Story \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/complexThing.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/complexThing.story.lua new file mode 100644 index 0000000..dbd32b4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/complexThing.story.lua @@ -0,0 +1,41 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) + +local ComplexThing = require(script.Parent.ComplexThing) +local Story = Roact.PureComponent:extend("Story") + +Story.defaultProps = { + numThings = 5, +} + +function Story:init() + self.ref = Roact.createRef() + + self.manyComplexThings = { + layout = Roact.createElement("UIListLayout", { + + }) + } + local function makeComplexThing() + return Roact.createElement(ComplexThing, { + Size = UDim2.fromOffset(256, 256), + nestedLayer = 5, + }) + end + for _ = 1, self.props.numThings do + table.insert(self.manyComplexThings, makeComplexThing()) + end +end + +function Story:render() + return Roact.createElement("ScrollingFrame", { + Position = UDim2.new(0, 0, 0, 300), + ClipsDescendants = false, + Size = UDim2.fromOffset(256, 256), + CanvasSize = UDim2.new(1, 0, 0, self.props.numThings * 256), + ScrollingDirection = Enum.ScrollingDirection.Y, + }, self.manyComplexThings) +end + +return Story diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/elementShuffle.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/elementShuffle.story.lua new file mode 100644 index 0000000..67019b7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/elementShuffle.story.lua @@ -0,0 +1,80 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - item shuffle test") + +local NUM_ITEMS = 10 +function Story:init() + self.state.items = {} + for i = 1,NUM_ITEMS do + table.insert(self.state.items, {id = i}) + end + + self.state.focusLock = 1 +end + +function Story:render() + return Roact.createElement("Frame", { + Size=UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 100, 0, 100), + padding = UDim.new(), + itemList = self.state.items, + focusIndex = 1, + focusLock = self.state.focusLock, + anchorLocation = UDim.new(1, 0), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + identifier = function(item) + return item.id + end, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 20, 0, 20), + BackgroundColor3 = Color3.fromRGB(0, 128 - 8*item.id, 128 + 8*item.id), + }, { + ["INDEX" .. tostring(item.id)] = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Text = item.id, + TextColor3 = Color3.new(1, 1, 1), + BackgroundTransparency = 1, + }), + }) + end, + }, + self.props, + {toDelete = Cryo.None} + )), + shuffle = Roact.createElement("TextButton", { + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "Shuffle", + [Roact.Event.Activated] = function() + local nextItems = self.state.items + + for _, v in pairs(nextItems) do + v.id = v.id+1 + if v.id > NUM_ITEMS then + v.id = 1 + end + end + + self:setState({ + items = nextItems, + focusLock = self.state.focusLock + 1, + }) + end, + }), + }) +end + +return Story diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/init.lua new file mode 100644 index 0000000..c0a4fec --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/init.lua @@ -0,0 +1,27 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +Roact.setGlobalConfig({ + propValidation = true, +}) + +local SCREEN_SIZE = Vector2.new(800, 800) + +return { + name = "Scroller", + storyRoot = script, + middleware = function(story, target) + local tree = Roact.createElement("Frame", { + BackgroundColor3 = Color3.fromRGB(30, 31, 28), + Size = UDim2.new(0, SCREEN_SIZE.X, 0, SCREEN_SIZE.Y), + ClipsDescendants = true, + }, { + Story = Roact.createElement(story) + }) + + local handle = Roact.mount(tree, target, "Root") + return function() + Roact.unmount(handle) + end + end, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.addition.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.addition.story.lua new file mode 100644 index 0000000..c814f99 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.addition.story.lua @@ -0,0 +1,88 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - item addition test") + +function Story:init() + local items = {} + local numberOfItems = self.props.numberOfItems or 30 + for i = 1, numberOfItems do + table.insert(items, {id = i}) + end + + self.state = { + itemList = items, + } +end + +function Story:render() + local focusIndex = self.props.addToFront and 1 or #self.state.itemList + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + Size = UDim2.new(0, 100, 0, 200), + focusIndex = focusIndex, + focusLock = self.state.itemList[focusIndex].id, + anchorLocation = UDim.new(1, 0), + dragBuffer = 200, + itemList = self.state.itemList, + orientation = Scroller.Orientation.Up, + identifier = function(item) + return item.id + end, + renderItem = function(item, _) + local r = 88+88*math.sin(math.rad(8*item.id+90)) + local g = 88+44*math.sin(math.rad(8*item.id+0)) + local b = 88+88*math.sin(math.rad(8*item.id+180)) + + return Roact.createElement("Frame", { + Size = UDim2.new(0, 20, 0, 20), + BackgroundColor3 = Color3.fromRGB(r, g, b), + }, { + ["INDEX" .. tostring(item.id)] = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Text = item.id, + TextColor3 = Color3.new(1, 1, 1), + BackgroundTransparency = 1, + }), + }) + end, + }, + self.props, + { + addToFront = Cryo.None, + numberOfItems = Cryo.None, + } + )), + addition = Roact.createElement("TextButton", { + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "Add Item", + + [Roact.Event.Activated] = function() + local nextId = #self.state.itemList + 1 + + local nextItems + if self.props.addToFront then + nextItems = Cryo.List.join({{id = nextId}}, self.state.itemList) + else + nextItems = Cryo.List.join(self.state.itemList, {{id = nextId}}) + end + + self:setState({ + itemList = nextItems, + }) + end, + }), + }) +end + +return Story diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.deletion.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.deletion.story.lua new file mode 100644 index 0000000..63aff65 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.deletion.story.lua @@ -0,0 +1,78 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - item deletion test") + +function Story:init() + self.state.items = {} + for i = -20,20 do + table.insert(self.state.items, {id = i}) + end +end + +function Story:render() + return Roact.createElement("Frame", { + Size=UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 100, 0, 100), + padding = UDim.new(), + itemList = self.state.items, + focusIndex = 21, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + identifier = function(item) + return item.id + end, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = item == 0 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, 128 - 8*item.id, 128 + 8*item.id), + }, { + ["INDEX" .. tostring(item.id)] = Roact.createElement("Frame"), + }) + end, + }, + self.props, + {toDelete = Cryo.None} + )), + deletion = Roact.createElement("TextButton", { + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "Delete", + [Roact.Event.Activated] = function() + local nextItems + if self.props.toDelete then + nextItems = Cryo.List.filter(self.state.items, function(item) + for _, v in pairs(self.props.toDelete) do + if item.id == v then + return false + end + end + return true + end) + else + local n = math.random(1, #self.state.items) + print("Deleting index " .. tostring(n)) + nextItems = Cryo.List.removeIndex(self.state.items, n) + end + self:setState({ + items = nextItems + }) + end, + }), + }) +end + +return Story \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.extras.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.extras.story.lua new file mode 100644 index 0000000..e26838b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.extras.story.lua @@ -0,0 +1,78 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - extra props test") + +local smallScroll = Roact.PureComponent:extend("smallScroll") +function smallScroll:render() + return Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 50, 0, 50), + padding = UDim.new(), + focusIndex = 6, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + }, self.props)) +end + +function Story:init() + self.state = { + items = {}, + store = {}, + } + for i = 1, 11 do + self.state.items[i] = i + self.state.store[i] = (i == 6 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, i*23, i*23)) + end + + self.renderItem = function(item) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = self.state.store[item], + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end +end + +function Story:render() + return Roact.createElement("Frame", {}, { + scroller = Roact.createElement(smallScroll, Cryo.Dictionary.join({ + itemList = self.state.items, + renderItem = self.renderItem, + extraProps = {self.state.store}, + }, self.props, {newColor = Cryo.None})), + change = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(0, 100, 0, 10), + BackgroundColor3 = Color3.new(1, 1, 1), + Text = "X", + [Roact.Event.Activated] = function() + local newColor = self.props.newColor + if not newColor then + newColor = Color3.fromRGB( + math.random(0, 255), + math.random(0, 255), + math.random(0, 255) + ) + print("Changing color to ", newColor) + end + + local store = {} + for i = 1, 11 do + store[i] = newColor + end + self:setState({store = store}) + end, + }), + }) +end + +return Story diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.few.large.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.few.large.story.lua new file mode 100644 index 0000000..a976be7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.few.large.story.lua @@ -0,0 +1,30 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - a few large items") + +function Story:render() + return Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 50, 0, 50), + padding = UDim.new(), + itemList = {1, 2, 3, 4, 5, 6, 7}, + focusIndex = 4, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 49, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + BackgroundColor3 = Color3.fromRGB(item*30, item*30, item*30), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, self.props)) +end + +return Story \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.infinite.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.infinite.story.lua new file mode 100644 index 0000000..ea63c4e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.infinite.story.lua @@ -0,0 +1,125 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - infinite scroll in both directions") + +function Story:init() + self.state.items = { + { + token = 0, + color = Color3.fromRGB(255, 255, 255), + } + } + self.state.size = Vector2.new(50, 50) + + self.loadPrevious = function() + local newItems = {} + local n = self.state.items[1].token + for i = n-10, n-1 do + table.insert(newItems, { + token = i, + color = Color3.fromRGB(0, 128 - i, 128 + i), + }) + end + self:setState({ + items = Cryo.List.join(newItems, self.state.items) + }) + end + + self.loadNext = function() + local newItems = {} + local n = self.state.items[#self.state.items].token + for i = n+1, n+10 do + table.insert(newItems, { + token = i, + color = Color3.fromRGB(0, 128 - i, 128 + i), + }) + end + self:setState({ + items = Cryo.List.join(self.state.items, newItems) + }) + end +end + +function Story:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, self.state.size.X, 0, self.state.size.Y), + padding = UDim.new(0, 3), + itemList = self.state.items, + loadNext = self.loadNext, + loadPrevious = self.loadPrevious, + focusIndex = 1, + anchorLocation = UDim.new(0.5, 0), + orientation = Scroller.Orientation.Up, + estimatedItemSize = 10, + mountingBuffer = 50, + identifier = function(item) + return item.token + end, + renderItem = function(item, _) + assert(item.token, "Item's token is unset") + assert(item.color, "Item's color is unset") + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = item.color, + }, { + ["INDEX" .. tostring(item.token)] = Roact.createElement("Frame"), + }) + end, + }, self.props)), + moveUp = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "^", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, -20) + }) + end, + }), + moveDown = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 100), + AnchorPoint = Vector2.new(1, 0), + Text = "v", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, 20) + }) + end, + }), + moveLeft = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -100, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = "<", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(-20, 0) + }) + end, + }), + moveRight = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, 0, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = ">", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(20, 0) + }) + end, + }), + }) +end + +return Story \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.rearrange.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.rearrange.story.lua new file mode 100644 index 0000000..4604bee --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.rearrange.story.lua @@ -0,0 +1,69 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - enough small items to fill the view") + +Story.defaultProps = { + startItems = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, + newItems = {11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}, +} + +function Story:init() + self.state.items = self.props.startItems +end + +function Story:render() + return Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + }, { + Scroller = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.fromOffset(50, 50), + padding = UDim.new(), + itemList = self.state.items, + focusIndex = 6, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = item == 6 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, item*23, item*23), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, + self.props, + { + startItems = Cryo.None, + newItems = Cryo.None, + clicked = Cryo.None, + } + )), + Button = Roact.createElement("TextButton", { + Size = UDim2.fromOffset(100, 50), + Position = UDim2.fromScale(1, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "Rearrange", + [Roact.Event.Activated] = function() + self:setState({ + items = self.props.newItems, + }) + if self.props.clicked then + self.props.clicked() + end + end, + }) + }) +end + +return Story diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.removeTopEntry.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.removeTopEntry.story.lua new file mode 100644 index 0000000..ce8853c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.removeTopEntry.story.lua @@ -0,0 +1,96 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - delete top item test") + +local END_OF_LIST_INDEX = 100 + +function Story:init() + self.state.items = {} + + if self.props.loadAll then + for i = 1, END_OF_LIST_INDEX do + table.insert(self.state.items, {id = i}) + end + else + for i = 1, 20 do + table.insert(self.state.items, {id = i}) + end + end +end + +function Story:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(38, 161, 38), + Size = UDim2.new(0, 100, 0, 100), + padding = UDim.new(), + itemList = self.state.items, + focusIndex = 1, + anchorLocation = UDim.new(1, 0), + estimatedItemSize = 20, + orientation = Scroller.Orientation.Up, + BackgroundTransparency = 0, + dragBuffer = 0, + ElasticBehavior = Enum.ElasticBehavior.Always, + ScrollBarThickness = 10, + VerticalScrollBarInset = Enum.ScrollBarInset.Always, + identifier = function(item) + return item.id + end, + renderItem = function(item, _) + return Roact.createElement("TextButton", { + Size = UDim2.new(0, 20, 0, 20), + BackgroundColor3 = Color3.fromRGB(200,200, 200), + Text = tostring(item.id) + }, { + ["INDEX" .. tostring(item.id)] = Roact.createElement("Frame"), + }) + end, + loadNext = function() + if not self.props.loadAll then + local newItems = {} + local n = self.state.items[#self.state.items].id + local endIndex = math.min(n + 10, END_OF_LIST_INDEX) + for i = n+1, endIndex do + table.insert(newItems, { + id = i, + }) + end + + if not Cryo.isEmpty(newItems) then + self:setState({ + items = Cryo.List.join(self.state.items, newItems) + }) + end + end + end, + }, + self.props, + {deleteLastItem = Cryo.None}, + {loadAll = Cryo.None} + )), + deletion = Roact.createElement("TextButton", { + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "Delete", + [Roact.Event.Activated] = function() + local indexToDelete = self.props.deleteLastItem and #self.state.items or 1 + local nextItems = Cryo.List.removeIndex(self.state.items, indexToDelete) + self:setState({ + items = nextItems + }) + end, + }), + }) +end + +return Story diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.resize.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.resize.story.lua new file mode 100644 index 0000000..2f602b8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.resize.story.lua @@ -0,0 +1,121 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Bar = Roact.PureComponent:extend("Bar") + +function Bar:init() + self.state = { + clicked = false, + } +end + +function Bar:render() + return Roact.createElement("TextButton", Cryo.Dictionary.join( + { + Size = UDim2.new(1, 0, 0, self.state.clicked and 30 or 10), + [Roact.Event.Activated] = function() + self:setState({ + clicked = not self.state.clicked, + }) + end, + }, + self.props + )) +end + +local Story = Roact.PureComponent:extend("Rhodium Story - frame resize test") +Story.defaultProps = { + resizeAmount = 20, + initialHeight = 50, +} + +function Story:init() + self.state = { + size = Vector2.new(50, self.props.initialHeight), + } +end + +function Story:render() + return Roact.createElement("Frame", { + Size=UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, self.state.size.X, 0, self.state.size.Y), + padding = UDim.new(), + itemList = {1, 2, 3}, + focusIndex = 2, + anchorLocation = UDim.new(0, 0), + orientation = Scroller.Orientation.Down, + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + renderItem = function(item, _) + return Roact.createElement(Bar, { + BackgroundColor3 = item == 2 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, -128 + 128*item, 376 - 128*item), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, + self.props, + { + resizeAmount = Cryo.None, + initialHeight = Cryo.None, + } + )), + moveUp = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "^", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, -self.props.resizeAmount) + }) + end, + }), + moveDown = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 100), + AnchorPoint = Vector2.new(1, 0), + Text = "v", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, self.props.resizeAmount) + }) + end, + }), + moveLeft = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -100, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = "<", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(-self.props.resizeAmount, 0) + }) + end, + }), + moveRight = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, 0, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = ">", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(self.props.resizeAmount, 0) + }) + end, + }), + }) +end + +return Story diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.several.small.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.several.small.story.lua new file mode 100644 index 0000000..4313821 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.several.small.story.lua @@ -0,0 +1,46 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - enough small items to fill the view") + +function Story:render() + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = self.props.frameSize or UDim2.new(0, 50, 0, 50), + }, { + layout = Roact.createElement("UIListLayout", { + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }), + + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 50, 0, 50), + padding = UDim.new(), + itemList = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, + focusIndex = 6, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = item == 6 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, item*23, item*23), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, + self.props, + { frameSize = Cryo.None } + )) + }) +end + +return Story diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.single.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.single.story.lua new file mode 100644 index 0000000..d8ba0ae --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.single.story.lua @@ -0,0 +1,22 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - a single small item") + +function Story:render() + return Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 50, 0, 50), + itemList = {1}, + anchorLocation = UDim.new(0.5, 0), + dragBuffer = 0, + renderItem = function() + return Roact.createElement("Frame", {Size=UDim2.new(0, 10, 0, 10)}) + end, + }, self.props)) +end + +return Story \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.swapLists.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.swapLists.story.lua new file mode 100644 index 0000000..e65a943 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.swapLists.story.lua @@ -0,0 +1,87 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - Swap a large list for a smaller one") +Story.defaultProps = { + anchorLocation = UDim.new(1, -10), + focusIndex = 1, + orientation = Scroller.Orientation.Up, + size = UDim2.new(0, 400, 0, 300), + + shortToLong = false, -- Swap from a short list to a long list when true, swap from a long list to a short list when false + longItemList = {"a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"}, + shortItemList = {"Z", "X", "Y"}, +} + +function Story:init() + self.state = { + items = self.props.shortToLong and self.props.shortItemList or self.props.longItemList, + } +end + +function Story:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroller = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(56, 19, 18), + Size = self.props.size, + orientation = self.props.orientation, + itemList = self.state.items, + focusLock = self.state.items[1], + focusIndex = self.props.focusIndex, + anchorLocation = self.props.anchorLocation, + estimatedItemSize = 40, + dragBuffer = 0, + identifier = function(item) + return item + end, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 300, 0, 40), + BackgroundColor3 = Color3.fromRGB(0, 255, 21), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Text = item, + TextColor3 = Color3.new(0, 0, 0), + BackgroundTransparency = 1, + }), + }) + end, + }, + self.props, + { + shortToLong = Cryo.None, + longItemList = Cryo.None, + shortItemList = Cryo.None, + size = Cryo.None, + } + )), + swapButton = Roact.createElement("TextButton", { + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(1, -100, 0, 0), + Text = "Swap Lists", + + [Roact.Event.Activated] = function() + -- Swap lists + if #self.state.items == #self.props.shortItemList then + self:setState({ + items = self.props.longItemList, + }) + else + self:setState({ + items = self.props.shortItemList, + }) + end + end, + }), + }) +end + +return Story diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/scroller.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/scroller.story.lua new file mode 100644 index 0000000..521afa6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/scroller.story.lua @@ -0,0 +1,204 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +-- Note: if you set this to ComplexThing, you may crash +local Thing = require(script.Parent.ResizingThing) + +local Story = Roact.PureComponent:extend("Story") + +function Story:render() + return Roact.createFragment({ + scroller = Roact.createElement(Scroller, { + BackgroundColor3 = Color3.fromRGB(56, 19, 18), + Size = UDim2.new(0, self.state.size.X, 1, self.state.size.Y), + Position = UDim2.new(0, 50, 0, 50), + ScrollBarThickness = 8, + padding = UDim.new(0, 5), + orientation = Scroller.Orientation.Down, + itemList = self.state.items, + loadNext = self.loadNext, + loadPrevious = self.loadPrevious, + focusLock = self.state.lock, + focusIndex = self.state.index, + anchorLocation = UDim.new(0, 0), + estimatedItemSize = 40, + identifier = function(item) return item.token end, + renderItem = function(item, _) + return Roact.createElement(Thing, item) + end, + onScrollUpdate = function(data) + self.indexData = data + end + }), + refresh = Roact.createElement("TextButton", { + Size = UDim2.new(0, 110, 0, 30), + Position = UDim2.new(1, -50, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = "Refresh", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickRefresh, + }), + up = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -90, 0, 90), + AnchorPoint = Vector2.new(1, 0), + Text = "^", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickUp, + }), + down = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -90, 0, 170), + AnchorPoint = Vector2.new(1, 0), + Text = "v", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickDown, + }), + left = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -130, 0, 130), + AnchorPoint = Vector2.new(1, 0), + Text = "<", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickLeft, + }), + right = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -50, 0, 130), + AnchorPoint = Vector2.new(1, 0), + Text = ">", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickRight, + }), + skipUp = Roact.createElement("TextButton", { + Size = UDim2.new(0, 60, 0, 30), + Position = UDim2.new(1, -90, 0, 220), + AnchorPoint = Vector2.new(1, 0), + Text = "SkipUp", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.skipUp, + }), + skipDown = Roact.createElement("TextButton", { + Size = UDim2.new(0, 60, 0, 30), + Position = UDim2.new(1, -90, 0, 270), + AnchorPoint = Vector2.new(1, 0), + Text = "SkipDown", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.skipDown, + }), + SkipAmount = Roact.createElement("TextBox", { + Size = UDim2.new(0, 60, 0, 30), + Position = UDim2.new(1, 0, 0, 270), + AnchorPoint = Vector2.new(1, 0), + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + Text = "SkipAmt", + PlaceholderText = "Skip", + ClearTextOnFocus = true, + [Roact.Change.Text] = self.onChangeText, + }), + }) +end + +local function generate(token) + if token == 0 then + return {color=Color3.fromRGB(255, 255, 255), width=50, token=0} + end + return {color=Color3.fromRGB(128-token, 255, 128+token), width=50+40*math.sin(token/5), token=token} +end + +function Story:init() + local items = {} + for i = -100,100 do table.insert(items, generate(i)) end + self.state = { + lock = 1, + index = 101, + skipAmount = 1, + size = Vector2.new(200, -100), + items = items, + } + self.indexData = { + anchorIndex = 101, + } + + self.loadNext = function() + self:setState({ + items = Cryo.List.join(self.state.items, {generate(self.state.items[#self.state.items].token + 1)}), + }) + end + + self.loadPrevious = function() + self:setState({ + items = Cryo.List.join({generate(self.state.items[1].token - 1)}, self.state.items), + }) + end + + self.clickRefresh = function() + print("Recentering") + self:setState({ + lock = self.state.lock + 1, + }) + end + + self.clickUp = function() + print("Moving up") + self:setState({ + size = self.state.size + Vector2.new(0, -20), + }) + end + + self.clickDown = function() + print("Moving down") + self:setState({ + size = self.state.size + Vector2.new(0, 20), + }) + end + + self.clickLeft = function() + print("Moving left") + self:setState({ + size = self.state.size + Vector2.new(-20, 0), + }) + end + + self.clickRight = function() + print("Moving right") + self:setState({ + size = self.state.size + Vector2.new(20, 0), + }) + end + + self.skipUp = function() + local newIndex = self.indexData.anchorIndex + self.state.skipAmount + print("Skipping up to index:", newIndex) + self:setState({ + lock = self.state.lock + 1, + index = newIndex, + }) + end + + self.skipDown = function() + local newIndex = self.indexData.anchorIndex - self.state.skipAmount + print("Skipping down to index:", newIndex) + self:setState({ + lock = self.state.lock + 1, + index = newIndex, + }) + end + + self.onChangeText = function(rbx) + local skipAmount = tonumber(rbx.Text) + print("skipAmount:", skipAmount) + if skipAmount then + self:setState({ + skipAmount = skipAmount, + }) + else + rbx.Text = self.state.skipAmount + end + end +end + +return Story diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/scrollerDebugger.story.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/scrollerDebugger.story.lua new file mode 100644 index 0000000..feae994 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/scrollerDebugger.story.lua @@ -0,0 +1,513 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller +local ComplexThing = require(script.Parent.ComplexThing) + +local TextService = game:GetService("TextService") + +local debugScroller = Roact.PureComponent:extend("debugScroller") + +local COLORS = { + DEFAULT = Color3.fromRGB(188, 188, 188), + WHITE = Color3.fromRGB(244, 244, 244), + BLACK = Color3.fromRGB(0, 0, 0), + TRUE = Color3.fromRGB(188, 222, 188), + FALSE = Color3.fromRGB(222, 188, 188), + + FOCUS_INDEX = Color3.fromRGB(0, 255, 0), + ANCHOR_LOCATION = Color3.fromRGB(0, 127, 255), + DRAG_BUFFER = Color3.fromRGB(222, 0, 222), + CANVAS = Color3.fromRGB(244, 188, 66), + LEAD_INDEX = Color3.fromRGB(0, 255, 0), + TRAIL_INDEX = Color3.fromRGB(255, 0, 0), + ANCHOR_INDEX = Color3.fromRGB(255, 255, 0), + PADDING = Color3.fromRGB(145, 88, 222), +} + +local COMPLEX_THING_SIZE = 128 + +local LAYOUT_ORDER_INDEX = 0 +function getNextLayout() + LAYOUT_ORDER_INDEX = LAYOUT_ORDER_INDEX + 1 + return LAYOUT_ORDER_INDEX +end + +function makeLabel(text, value, color) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, 20), + Text = string.format("%s: %s", text, tostring(value)), + TextColor3 = color or COLORS.WHITE, + TextXAlignment = Enum.TextXAlignment.Left, + BorderSizePixel = 0, + BackgroundTransparency = 1, + LayoutOrder = getNextLayout(), + }) +end + +function makeButton(text, func, color) + color = color or COLORS.DEFAULT + return Roact.createElement("TextButton", { + Size = UDim2.fromOffset(110, 30), + BackgroundColor3 = color, + TextWrapped = true, + Text = text, + [Roact.Event.Activated] = func, + LayoutOrder = getNextLayout(), + }) +end + +function debugScroller:makeToggleButton(value) + local function doToggle() + self:setState({ + [value] = not self.state[value] + }) + end + return makeButton(value, doToggle, self.state[value] and COLORS.TRUE or COLORS.FALSE) +end + +function makeLabelLine(text, position, color, pointsRight) + color = color or COLORS.BLACK + local LINE_WIDTH = 20 + + local textSize = TextService:GetTextSize(text, 8, Enum.Font.Legacy, Vector2.new()) + return Roact.createElement("Frame", { + Size = UDim2.fromOffset(textSize.X + LINE_WIDTH, textSize.Y), + Position = position, + AnchorPoint = pointsRight and Vector2.new(1, 0.5) or Vector2.new(0, 0.5), + BackgroundTransparency = 1, + }, { + Roact.createFragment({ + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + labelLine = Roact.createElement("Frame", { + Position = position, + Size = UDim2.fromOffset(LINE_WIDTH, 1), + BorderSizePixel = 0, + BackgroundColor3 = color, + LayoutOrder = 2, + }), + label = Roact.createElement("TextLabel", { + AnchorPoint = Vector2.new(0, 0.5), + Size = UDim2.new(1, -LINE_WIDTH, 0, 8), + Text = text, + TextColor3 = color, + BackgroundTransparency = 1, + LayoutOrder = pointsRight and 1 or 3 + }) + }) + }) +end + +debugScroller.defaultProps = { + anchorLocation = UDim.new(1, 0), + mountingBuffer = 150, + dragBuffer = 0, + focusIndex = 1, + + Size = UDim2.new(1, -20, 0, 100), + numItems = 20, +} + +function debugScroller:init() + self.initialState = { + items = {}, + -- Scroller props. These can be changed by debugger buttons + focusLock = 1, + orientation = Scroller.Orientation.Up, + clipsDescendants = false, + nestedLayer = 1, + + -- Scroller internals. Don't change these manually, meant for DISPLAY ONLY + canvasPosition = 0, + canvasSize = 0, + paddingSize = -1, + paddingPosition = 0, + anchorLinePositionY = 0, + leadIndex = -1, + trailIndex = -1, + anchorIndex = -1, + } + + self.mutableState = { + loadPreviousEnabled = false, + loadNextEnabled = false, + } + + for i = 1,self.props.numItems do + table.insert(self.initialState.items, {id = i}) + end + + self.state = Cryo.Dictionary.join(self.initialState, self.mutableState) + + -- horsecat storybooks have a Y-offset that we need to address before using absolutePosition + self.initialY, self.updateInitialY = Roact.createBinding(0) + + self.ref = Roact.createRef() +end + +function debugScroller:render() + return Roact.createElement("Frame", { + Size=UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + [Roact.Change.AbsolutePosition] = function(rbx) + self.updateInitialY(rbx.AbsolutePosition.Y) + end + }, { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + }), + visibilityFrame = Roact.createElement("Frame", { + LayoutOrder = 1, + Size = UDim2.new(0, 200, 1, 0), + BackgroundTransparency = 1, + }, { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + }), + anchorLocationLabel = makeLabel("anchorLocation", self.props.anchorLocation, COLORS.ANCHOR_LOCATION), + anchorIndexLabel = makeLabel("anchorIndex", self.state.anchorIndex, COLORS.ANCHOR_INDEX), + leadIndexLabel = makeLabel("leadIndex", self.state.leadIndex, COLORS.LEAD_INDEX), + trailIndexLabel = makeLabel("trailIndex", self.state.trailIndex, COLORS.TRAIL_INDEX), + canvasPositionLabel = makeLabel("canvasPosition", self.state.canvasPosition, COLORS.CANVAS), + canvasSizeLabel = makeLabel("canvasSize", self.state.canvasSize, COLORS.CANVAS), + paddingSizeLabel = makeLabel("size of padding Frame", self.state.paddingSize, COLORS.PADDING), + + whiteSpace = Roact.createElement("Frame", { Size = UDim2.fromOffset(0, 20), BackgroundTransparency = 1, LayoutOrder = getNextLayout()}), + + disableLoadPrevious = self:makeToggleButton("loadPreviousEnabled"), + disableLoadNext = self:makeToggleButton("loadNextEnabled"), + clipsDescendants = self:makeToggleButton("clipsDescendants"), + }), + + scrollerFrame = Roact.createElement("Frame", { + LayoutOrder = 2, + Size = UDim2.new(0, 160, 0, 300), + BackgroundTransparency = 1, + }, { + scroller = Roact.createElement(Scroller, { + ElasticBehavior = Enum.ElasticBehavior.Always, + ClipsDescendants = self.state.clipsDescendants, + Position = UDim2.fromOffset(0, 200), + BackgroundColor3 = Color3.fromRGB(111, 111, 111), + Size = self.props.Size, + + orientation = self.state.orientation, + padding = UDim.new(), + itemList = self.state.items, + focusIndex = self.props.focusIndex, + focusLock = self.state.focusLock, + anchorLocation = self.props.anchorLocation, + dragBuffer = self.props.dragBuffer, + mountingBuffer = self.props.mountingBuffer, + estimatedItemSize = self.state.nestedLayer ~= 1 and COMPLEX_THING_SIZE or 20, + [Roact.Ref] = self.ref, + identifier = function(item) + return item.id + end, + --recyclingDisabledFor={"ComplexThing"}, + renderItem = function(item, _) + if self.state.nestedLayer ~= 1 then + return Roact.createElement(ComplexThing, { + Size = UDim2.fromOffset(COMPLEX_THING_SIZE, COMPLEX_THING_SIZE), + nestedLayer = self.state.nestedLayer, + }) + else + local r = 88+88*math.sin(math.rad(8*item.id+90)) + local g = 88+44*math.sin(math.rad(8*item.id+0)) + local b = 88+88*math.sin(math.rad(8*item.id+180)) + + local leadIndexId = self.state.items[self.state.leadIndex] and self.state.items[self.state.leadIndex].id + local trailIndexId = self.state.items[self.state.trailIndex] and self.state.items[self.state.trailIndex].id + local anchorIndexId = self.state.items[self.state.anchorIndex] and self.state.items[self.state.anchorIndex].id + + return Roact.createElement("Frame", { + Size = UDim2.new(0, 20, 0, 20), + BackgroundColor3 = Color3.fromRGB(r, g, b), + }, { + ["INDEX" .. tostring(item.id)] = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Text = item.id, + TextColor3 = Color3.new(1, 1, 1), + BackgroundTransparency = 1, + }, { + item.id == leadIndexId and makeLabelLine("leadIndex ", + UDim2.fromScale(1, 0), + COLORS.LEAD_INDEX, + false), + item.id == trailIndexId and makeLabelLine("trailIndex ", + UDim2.fromScale(1, 0), + COLORS.TRAIL_INDEX, + false), + item.id == anchorIndexId and makeLabelLine("anchorIndex ", + UDim2.new(), + COLORS.ANCHOR_INDEX, + true) + }), + }) + end + end, + + loadPrevious = function() + if not self.state.loadPreviousEnabled then + return + end + local newItems = {} + local n = self.state.items[1].id + for i = n-10, n-1 do + table.insert(newItems, { + id = i, + }) + end + self:setState({ + items = Cryo.List.join(newItems, self.state.items) + }) + end, + + loadNext = function() + if not self.state.loadNextEnabled then + return + end + local newItems = {} + local n = self.state.items[#self.state.items].id + for i = n+1, n+10 do + table.insert(newItems, { + id = i, + }) + end + self:setState({ + items = Cryo.List.join(self.state.items, newItems) + }) + end, + + [Roact.Change.CanvasPosition] = function() + if not self.ref.current then + return + end + + local rbx = self.ref.current + + -- this feels dirty, but allows us to visualize the padding frame + local padding_rbx = rbx:FindFirstChild("padding") + + local anchorLinePositionY = rbx.AbsolutePosition.Y - self.props.anchorLocation.Offset + if self.state.orientation == Scroller.Orientation.Down then + anchorLinePositionY = anchorLinePositionY + (1 - self.props.anchorLocation.Scale) * rbx.AbsoluteSize.Y + elseif self.state.orientation == Scroller.Orientation.Up then + anchorLinePositionY = anchorLinePositionY + self.props.anchorLocation.Scale * rbx.AbsoluteSize.Y + else + anchorLinePositionY = 0 + end + + self:setState({ + canvasPosition = rbx.CanvasPosition.Y, + canvasSize = rbx.CanvasSize.Y.Offset, + paddingSize = padding_rbx.Size.Y.Offset, + + anchorLinePositionY = anchorLinePositionY, + paddingPosition = padding_rbx.AbsolutePosition.Y, + }) + end, + + onScrollUpdate = function(indices) + self.state.leadIndex = indices.leadIndex + self.state.anchorIndex = indices.anchorIndex + self.state.trailIndex = indices.trailIndex + end + }), + + anchorLine = self.ref.current and makeLabelLine("anchorLocation ", + UDim2.fromOffset(20, self.state.anchorLinePositionY - self.initialY:getValue()), + COLORS.ANCHOR_LOCATION, + true), + + dragBuffer1 = self.ref.current and self.props.dragBuffer ~= 0 and makeLabelLine("dragBuffer ", + UDim2.fromOffset(20, self.props.dragBuffer + self.ref.current.AbsolutePosition.Y - self.initialY:getValue()), + COLORS.DRAG_BUFFER, + true), + + dragBuffer2 = self.ref.current and self.props.dragBuffer ~= 0 and makeLabelLine("dragBuffer ", + UDim2.fromOffset(20, self.ref.current.AbsoluteSize.Y - self.props.dragBuffer + self.ref.current.AbsolutePosition.Y - self.initialY:getValue()), + COLORS.DRAG_BUFFER, + true), + + mountingBuffer1 = self.ref.current and makeLabelLine("mountingBuffer ", + UDim2.fromOffset(20, -self.props.mountingBuffer + self.ref.current.AbsolutePosition.Y - self.initialY:getValue()), + COLORS.WHITE, + true), + + mountingBuffer2 = self.ref.current and makeLabelLine("mountingBuffer ", + UDim2.fromOffset(20, self.ref.current.AbsoluteSize.Y + self.props.mountingBuffer + self.ref.current.AbsolutePosition.Y - self.initialY:getValue()), + COLORS.WHITE, + true), + + canvasEstimate = self.ref.current and Roact.createElement("Frame", { + Size = UDim2.fromOffset(40, self.state.canvasSize), + Position = UDim2.fromOffset(20, self.state.paddingPosition - self.initialY:getValue()), + BackgroundColor3 = COLORS.CANVAS, + BorderSizePixel = 0, + ZIndex = -10, + }), + + paddingEstimate = self.ref.current and Roact.createElement("Frame", { + Size = UDim2.fromOffset(30, self.state.paddingSize), + Position = UDim2.fromOffset(0, self.state.paddingPosition - self.initialY:getValue()), + BackgroundColor3 = COLORS.PADDING, + BorderSizePixel = 0, + ZIndex = -9, + }), + }), + operationsFrame = Roact.createElement("Frame", { + LayoutOrder = 3, + Size = UDim2.new(0, 100, 1, 0), + BackgroundTransparency = 1, + }, { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + }), + reset = makeButton("Reset ItemList", function() + local newState = Cryo.Dictionary.join(self.initialState, { + focusLock = self.state.focusLock + 1, + loadNext = self.state.loadNext, + loadPrev = self.state.loadPrev, + clipsDescendants = self.state.clipsDescendants, + }) + self:setState(newState) + end), + + -- Reversing the Orientation currently does not call resize, does not update the anchorLocation + -- reverse = makeButton("Reverse Orientation", function() + -- local newOrientation + -- if self.state.orientation == Scroller.Orientation.Down then + -- newOrientation = Scroller.Orientation.Up + -- else + -- newOrientation = Scroller.Orientation.Down + -- end + -- self:setState({ + -- orientation = newOrientation, + -- focusLock = self.state.focusLock + 1, + -- }) + -- end), + + rotateForward = makeButton("Rotate Forward", function() + local nextItems = {} + local numItems = #self.state.items + local a = self.state.items[numItems] + table.insert(nextItems, a) + for i = 1, numItems-1 do + table.insert(nextItems, self.state.items[i]) + end + + self:setState({ + focusLock = self.state.focusLock + 1, + items = nextItems, + }) + end), + + rotateBack = makeButton("Rotate Backward", function() + local nextItems = {} + local a = self.state.items[1] + for i = 2, #self.state.items do + table.insert(nextItems, self.state.items[i]) + end + table.insert(nextItems, a) + + self:setState({ + focusLock = self.state.focusLock + 1, + items = nextItems, + }) + end), + + insertFront = makeButton("Insert Front", function() + local nextItems = {} + table.insert(nextItems, {id = self.state.items[1].id-1 }) + for k, v in pairs(self.state.items) do + table.insert(nextItems, v) + end + + self:setState({ + focusLock = self.state.focusLock + 1, + items = nextItems, + }) + end), + + insertBack = makeButton("Insert Back", function() + local nextItems = {} + for k, v in pairs(self.state.items) do + table.insert(nextItems, v) + end + table.insert(nextItems, {id = self.state.items[#self.state.items].id+1 }) + + self:setState({ + focusLock = self.state.focusLock + 1, + items = nextItems, + }) + end), + + removeFront = makeButton("Remove Front", function() + local nextItems = {} + local numItems = #self.state.items + for i = 2, numItems do + table.insert(nextItems, self.state.items[i]) + end + + self:setState({ + focusLock = self.state.focusLock + 1, + items = nextItems, + }) + end), + + removeBack = makeButton("Remove Back", function() + local nextItems = {} + local numItems = #self.state.items + for i = 1, numItems-1 do + table.insert(nextItems, self.state.items[i]) + end + + self:setState({ + focusLock = self.state.focusLock + 1, + items = nextItems, + }) + end), + + + reverseList = makeButton("Reverse List", function() + local nextItems = {} + local numItems = #self.state.items + for i = 1, numItems do + nextItems[i] = self.state.items[numItems - i + 1] + end + + self:setState({ + focusLock = self.state.focusLock + 1, + items = nextItems, + }) + end), + + scrollUpOnce = makeButton("Scroll Up 1px", function() + if self.ref.current then + self.ref.current.CanvasPosition = self.ref.current.CanvasPosition - Vector2.new(0, 1) + end + end), + + scrollDownOnce = makeButton("Scroll Down 1px", function() + if self.ref.current then + self.ref.current.CanvasPosition = self.ref.current.CanvasPosition + Vector2.new(0, 1) + end + end), + + toggleComplexity = makeButton("Toggle Complexity", function() + self:setState({ + nestedLayer = self.state.nestedLayer%5 + 1 + }) + end), + }) + }) +end + +return debugScroller diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/init.lua new file mode 100644 index 0000000..61e7222 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/init.lua @@ -0,0 +1,5 @@ +local Scroller = require(script.Components.Scroller) + +return { + Scroller = Scroller, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/lock.toml new file mode 100644 index 0000000..2b7d825 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/lock.toml @@ -0,0 +1,12 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/infinite-scroller" +version = "0.5.6" +commit = "d622d74bec4a599c5f8ef642194fa9eaf973b5c0" +source = "url+https://github.com/roblox/infinite-scroller" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/t.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-result/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-result/lock.toml new file mode 100644 index 0000000..c25e560 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-result/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/lua-result" +version = "0.1.0" +commit = "c6817c8455aa1f5922d83dd44c8bedbc7726e51d" +source = "git+https://github.rbx.com/roblox/lua-result#master" diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-result/lua-result/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-result/lua-result/init.lua new file mode 100644 index 0000000..7148129 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-result/lua-result/init.lua @@ -0,0 +1,100 @@ +local Result = {} +Result.__index = Result + +local ResultTypeSymbol = newproxy(true) + +function Result.new(status, value) + assert(typeof(status) == "boolean") + + local result = { + -- Used to locate where a result was created + _source = debug.traceback(), + + -- A tag to identify us as a result + [ResultTypeSymbol] = true, + + _status = status, + + -- The value success value or error message. + _value = value, + } + + setmetatable(result, Result) + + return result +end + +function Result.success(value) + return Result.new(true, value) +end + +function Result.error(value) + return Result.new(false, value) +end + +--[[ + Is the given object a Result instance? +]] +function Result.is(object) + if typeof(object) ~= "table" then + return false + end + + return object[ResultTypeSymbol] +end + +--[[ + The given callbacks are invoked depending on that result. + + Creates a new result that receives the output of the callback. +]] +function Result:match(successHandler, errorHandler) + assert(typeof(successHandler) == "function" or typeof(successHandler) == "nil", + string.format("Result:match expects successHandler to be a function or nil, got %s", typeof(successHandler))) + assert(typeof(errorHandler) == "function" or typeof(errorHandler) == "nil", + string.format("Result:match expects errorHandler to be a function or nil, got %s", typeof(errorHandler))) + + local newResult + if self._status then + if successHandler ~= nil then + newResult = successHandler(self._value) + else + return self + end + else + if errorHandler ~= nil then + newResult = errorHandler(self._value) + else + return self + end + end + if Result.is(newResult) then + return newResult + else + return Result.success(newResult) + end +end + +--[[ + The given callback is invoked if the result is success. + + Creates a new result that receives the output of the callback. +]] +function Result:matchSuccess(successHandler) + return self:match(successHandler, nil) +end + +--[[ + The given callback is invoked if the result is error. + + Creates a new result that receives the output of the callback. +]] +function Result:matchError(errorHandler) + return self:match(nil, errorHandler) +end + +function Result:unwrap() + return self._status, self._value +end + +return Result \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Lumberyak.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Lumberyak.lua new file mode 100644 index 0000000..3237201 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Lumberyak.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_lumberyak"]["lumberyak"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Mock.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Mock.lua new file mode 100644 index 0000000..428f95d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Mock.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["jtaylor_mock"]["mock"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Promise.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Promise.lua new file mode 100644 index 0000000..5dea2b4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Promise.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["lua-promise"]["lua-promise"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Roact.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Symbol.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Symbol.lua new file mode 100644 index 0000000..f9086fa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Symbol.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_lua-symbol"]["lua-symbol"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lock.toml new file mode 100644 index 0000000..28476ad --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lock.toml @@ -0,0 +1,14 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/lua-roact-policy-provider" +version = "0.1.0" +commit = "1d595dae48c654c54a76f07a2ddb95bfad53dc43" +source = "git+https://github.com/roblox/lua-roact-policy-provider#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Lumberyak roblox/lumberyak 47552268 git+https://github.rbx.com/roblox/lumberyak#master", + "Mock jtaylor/mock d2c4005c git+https://github.rbx.com/roblox/mock#master", + "Promise lua-promise bbb9e162 git+https://github.rbx.com/roblox/lua-promise#master", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "Symbol roblox/lua-symbol 139fdfe6 git+https://github.rbx.com/roblox/lua-symbol#master", + "tutils tutils 0be577db git+https://github.rbx.com/roblox/tutils#master", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/Logger.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/Logger.lua new file mode 100644 index 0000000..939f724 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/Logger.lua @@ -0,0 +1,6 @@ +local Packages = script.Parent.Parent +local Lumberyak = require(Packages.Lumberyak) + +local logger = Lumberyak.Logger.new() + +return logger diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/Provider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/Provider.lua new file mode 100644 index 0000000..232276d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/Provider.lua @@ -0,0 +1,23 @@ +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) + +local appPolicyKey = require(script.Parent.appPolicyKey) + +return function() + local PolicyProvider = Roact.Component:extend("PolicyProvider") + + function PolicyProvider:init(props) + assert(type(props.policy) == "table", "Provider expects props.policy to be a table") + + self._context[appPolicyKey] = { + presentationPolicy = props.policy, + staticExternalPolicy = props.policyData, + } + end + + function PolicyProvider:render() + return Roact.oneChild(self.props[Roact.Children]) + end + + return PolicyProvider +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/appPolicyKey.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/appPolicyKey.lua new file mode 100644 index 0000000..9ef702e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/appPolicyKey.lua @@ -0,0 +1,4 @@ +local Packages = script.Parent.Parent +local Symbol = require(Packages.Symbol) + +return Symbol.named("AppPolicy") diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/appPolicyKey.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/appPolicyKey.spec.lua new file mode 100644 index 0000000..2213e8f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/appPolicyKey.spec.lua @@ -0,0 +1,9 @@ +return function() + local appPolicyKey = require(script.Parent.appPolicyKey) + + describe("require return value", function() + it("SHOULD return a valid Symbol", function() + expect(appPolicyKey).to.be.ok() + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/connect.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/connect.lua new file mode 100644 index 0000000..3a8792a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/connect.lua @@ -0,0 +1,99 @@ +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local Promise = require(Packages.Promise) + +local Logger = require(script.Parent.Logger) +local appPolicyKey = require(script.Parent.appPolicyKey) + +local function mergePolicies(base, params) + local policyWrapper = {} + + for _, wrapper in ipairs(params) do + policyWrapper = Cryo.Dictionary.join(policyWrapper, wrapper(base)) + end + + return policyWrapper +end + +return function(getPolicyImpl) + assert(getPolicyImpl, "expected getPolicyImpl") + + return function(mapper) + assert(type(mapper) == "function", "connect expects mapper to be a function") + + return function(component) + local name = ("AppPolicy(%s)"):format(tostring(component)) + local componentLogger = Logger:new(name) + componentLogger:setContext({ + prefix = string.format("%s: ", name), + }) + + local providerNotFound = string.format("%s: Not a descendent of PolicyProvider", name) + local Connection = Roact.PureComponent:extend(name) + + componentLogger:trace("Connected to component: {}", tostring(component)) + + function Connection:init(props) + self.policyContext = self._context[appPolicyKey] + assert(self.policyContext, providerNotFound) + + self.setWithEmptyPolicy = function() + self.state = { + policy = mergePolicies({}, self.policyContext.presentationPolicy), + } + end + + if self.policyContext.staticExternalPolicy then + -- if we already have a staticExternalPolicy, there is + -- no need to read it from our implementation + self.state = { + policy = mergePolicies(self.policyContext.staticExternalPolicy, self.policyContext.presentationPolicy), + } + else + local retrievedExternalPolicy = getPolicyImpl.read() + if retrievedExternalPolicy then + self.state = { + policy = mergePolicies(retrievedExternalPolicy, self.policyContext.presentationPolicy), + } + else + self.setWithEmptyPolicy() + componentLogger:trace("No app policy data available") + end + + self.onPolicyChanged = function(newExternalPolicy) + self:setState({ + policy = mergePolicies(newExternalPolicy, self.policyContext.presentationPolicy), + }) + end + end + end + + function Connection:didMount() + if self._context[appPolicyKey].staticExternalPolicy then + return + end + self.connection = getPolicyImpl.onPolicyChanged(function(incomingExternalPolicy) + componentLogger:trace("Received policy update from MemStorageService") + self.onPolicyChanged(incomingExternalPolicy) + end) + end + + function Connection:render() + local policyProps = mapper(self.state.policy, self.props) + local newProps = Cryo.Dictionary.join(self.props, policyProps) + return Roact.createElement(component, newProps) + end + + function Connection:willUnmount() + if self.connection then + self.connection:Disconnect() + end + -- sometimes the callback will fire even after :Disconnect was called + self.onPolicyChange = nil + end + + return Connection + end + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/connect.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/connect.spec.lua new file mode 100644 index 0000000..a13dc32 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/connect.spec.lua @@ -0,0 +1,542 @@ +return function() + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + local tutils = require(Packages.tutils) + local Mock = require(Packages.Mock) + local MagicMock = Mock.MagicMock + + local fromMemStorageService = require(script.Parent.getPolicyImplementations.fromMemStorageService) + + local Provider = require(script.Parent.Provider) + local providerInstance = Provider() + + local function checkForPropsAfterMounting(params) + assert(params.expectedProps) + assert(params.mapper) + assert(params.policyProp) + assert(params.connect) + assert(params.provider) + + params.shouldCheckPropsIf = params.shouldCheckPropsIf or function() + return true + end + + local hasBaseComponentEverRendered = false + local hasPropsEverBeenChecked = false + + local baseComponent = function(props) + hasBaseComponentEverRendered = true + + if params.shouldCheckPropsIf() then + hasPropsEverBeenChecked = true + for propName, propValue in pairs(params.expectedProps) do + if props[propName] ~= propValue then + fail(string.format( + "Expected baseComponent to have prop `%s` = `%s`." .. " " .. + "Got: `%s` instead." .. " " .. + "(Check the `mapper` function is correctly formatted)", + propName, + tostring(propValue), + tostring(props[propName]) + )) + end + end + end + + return Roact.createElement("Folder") + end + + local wrappedComponent = params.connect(params.mapper)(baseComponent) + + local tree = Roact.createElement(params.provider, { + policy = params.policyProp, + policyData = params.policyDataProp, + }, { + wrappedComponent = Roact.createElement(wrappedComponent) + }) + + local instance = Roact.mount(tree) + if params.funcAfterMounting then + params.funcAfterMounting() + end + Roact.unmount(instance) + + expect(hasBaseComponentEverRendered).to.equal(true) + expect(hasPropsEverBeenChecked).to.equal(true) + end + + describe("WHEN required", function() + local connect = require(script.Parent.connect) + it("SHOULD return a function", function() + expect(connect).to.be.a("function") + end) + + describe("GIVEN a fromMemStorageServiceWithBehavior", function() + local behavior = "mockBehavior" + + describe("GIVEN empty dependencies", function() + local getPolicyImpl = fromMemStorageService({ + HttpService = MagicMock.new(), + MemStorageService = MagicMock.new(), + })(behavior) + local connectInstance = connect(getPolicyImpl) + + it("SHOULD return a function", function() + expect(connectInstance).to.be.a("function") + end) + + describe("GIVEN a static mapper function", function() + local mapper = function() + return { + foo = "bar", + } + end + local mappedConnection = connectInstance(mapper) + + it("SHOULD return a function", function() + expect(mappedConnection).to.be.a("function") + end) + + describe("GIVEN a component", function() + local baseComponent = function() + return Roact.createElement("Folder") + end + local wrappedComponent = mappedConnection(baseComponent) + + it("SHOULD return a new component", function() + expect(baseComponent).to.never.equal(wrappedComponent) + end) + + describe("GIVEN a Roact tree without a Provider", function() + local tree = Roact.createElement(wrappedComponent) + + it("SHOULD throw", function() + expect(function() + Roact.mount(tree) + end).to.throw() + end) + end) + + describe("GIVEN a Roact tree with a Provider", function() + describe("GIVEN a nil policy prop", function() + local tree = Roact.createElement(providerInstance, { + policy = nil, + }, { + wrappedComponent = Roact.createElement(wrappedComponent) + }) + + it("SHOULD throw", function() + expect(function() + Roact.mount(tree) + end).to.throw() + end) + end) + + describe("GIVEN an empty policy prop", function() + local tree = Roact.createElement(providerInstance, { + policy = {}, + }, { + wrappedComponent = Roact.createElement(wrappedComponent) + }) + + it("SHOULD mount and unmount successfully", function() + local instance = Roact.mount(tree) + Roact.unmount(instance) + end) + end) + + describe("GIVEN policy prop with a single (static) definition", function() + local mockPolicy1 = function(_) + return { + isFeatureEnabled = "mockPolicy1Enabled", + } + end + + it("SHOULD allow mapper to create props for base component", function() + checkForPropsAfterMounting({ + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + } + end, + expectedProps = { + isPolicy1FeatureEnabled = "mockPolicy1Enabled" + }, + }) + end) + end) + + describe("GIVEN policy prop with a multiple (static) definitions", function() + local mockPolicy1 = function(_) + return { + isFeature1Enabled = 100, + } + end + + local mockPolicy2 = function(_) + return { + isFeature2Enabled = 200, + } + end + + it("SHOULD allow mapper to create props for base component", function() + checkForPropsAfterMounting({ + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1, mockPolicy2 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeature1Enabled, + isPolicy2FeatureEnabled = policy.isFeature2Enabled, + } + end, + expectedProps = { + isPolicy1FeatureEnabled = 100, + isPolicy2FeatureEnabled = 200, + }, + }) + end) + end) + end) + end) + end) + end) + + describe("GIVEN MemStorageService can retrieve a string and HttpService can decode it", function() + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function(str) + return { + foo = "bar", + } + end + + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return "mockStorageData" + end + + local getPolicyImpl = fromMemStorageService({ + HttpService = mockHttpService, + MemStorageService = mockMemStorageService, + })(behavior) + + local connectInstance = connect(getPolicyImpl) + + describe("GIVEN policy prop with a single dynamic definition (read from MemStorageService)", function() + local mockPolicy1 = function(policy) + return { + isFeatureEnabled = policy.foo, + } + end + + it("SHOULD allow mapper to create props for base component", function() + checkForPropsAfterMounting({ + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + } + end, + expectedProps = { + -- bar is the result from HttpService's JSONDecode + -- (mocking the pull from MemStorageService) + isPolicy1FeatureEnabled = "bar" + }, + }) + end) + end) + end) + + describe("GIVEN MemStorageService cannot retrieve a string", function() + local mockHttpService = MagicMock.new() + + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return nil + end + + local getPolicyImpl = fromMemStorageService({ + HttpService = mockHttpService, + MemStorageService = mockMemStorageService, + })(behavior) + + local connectInstance = connect(getPolicyImpl) + + describe("GIVEN policy prop with a single dynamic definition (read from MemStorageService)", function() + it("SHOULD allow mapper to create props for base component", function() + local wasMockPolicy1EverCalled = false + local mockPolicy1 = function(policy) + wasMockPolicy1EverCalled = true + + -- policy should be an empty table because + -- MemStorageService is providing invalid values + expect(tutils.shallowEqual(policy, {})).to.equal(true) + return { + isFeatureEnabled = policy and policy.foo, + } + end + + checkForPropsAfterMounting({ + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + } + end, + expectedProps = {}, + }) + + expect(wasMockPolicy1EverCalled).to.equal(true) + end) + end) + end) + + describe("GIVEN MemStorageService updates with a new string", function() + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function(_, jsonString) + if jsonString == "mockJsonString" then + return { + isFeatureEnabled = "hello.world", + } + end + + return nil + end + + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return nil + end + + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + + local getPolicyImpl = fromMemStorageService({ + HttpService = mockHttpService, + MemStorageService = mockMemStorageService, + })(behavior) + + local connectInstance = connect(getPolicyImpl) + + describe("GIVEN policy prop with a single dynamic definition (read from MemStorageService)", function() + it("SHOULD allow mapper to create props for base component", function() + local numberOfTimesMockPolicy1EverCalled = 0 + local mockPolicy1 = function(policy) + numberOfTimesMockPolicy1EverCalled = numberOfTimesMockPolicy1EverCalled + 1 + + return { + isFeatureEnabled = policy and policy.isFeatureEnabled, + } + end + + checkForPropsAfterMounting({ + funcAfterMounting = function() + updateMemStorageService:Fire("mockJsonString") + end, + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + } + end, + shouldCheckPropsIf = function() + return numberOfTimesMockPolicy1EverCalled > 1 + end, + expectedProps = { + isPolicy1FeatureEnabled = "hello.world", + }, + }) + + expect(numberOfTimesMockPolicy1EverCalled).to.equal(2) + end) + end) + end) + + describe("GIVEN MemStorageService updates with a nil value", function() + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function(_, jsonString) + if jsonString == "mockJsonString" then + return { + isFeatureEnabled = "hello.world", + } + end + + return nil + end + + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return nil + end + + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + + local getPolicyImpl = fromMemStorageService({ + HttpService = mockHttpService, + MemStorageService = mockMemStorageService, + })(behavior) + + local connectInstance = connect(getPolicyImpl) + + describe("GIVEN policy prop with a single dynamic definition (read from MemStorageService)", function() + it("SHOULD allow mapper to create props for base component", function() + local numberOfTimesMockPolicy1EverCalled = 0 + local mockPolicy1 = function(policy) + numberOfTimesMockPolicy1EverCalled = numberOfTimesMockPolicy1EverCalled + 1 + + return { + isFeatureEnabled = policy and policy.isFeatureEnabled, + } + end + + checkForPropsAfterMounting({ + funcAfterMounting = function() + updateMemStorageService:Fire("mockJsonString") + end, + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + } + end, + shouldCheckPropsIf = function() + return numberOfTimesMockPolicy1EverCalled > 1 + end, + expectedProps = { + isPolicy1FeatureEnabled = "hello.world", + }, + }) + + expect(numberOfTimesMockPolicy1EverCalled).to.equal(2) + end) + end) + end) + + describe("GIVEN a policyData prop", function() + local mockHttpService = MagicMock.new() + local mockMemStorageService = MagicMock.new() + + local getPolicyImpl = fromMemStorageService({ + HttpService = mockHttpService, + MemStorageService = mockMemStorageService, + })(behavior) + + local connectInstance = connect(getPolicyImpl) + local mockPolicy1 = function(policy) + return { + isFeatureEnabled = policy.mockIsFeatureEnabled, + } + end + + it("SHOULD return policyData and not call MemStorageService", function() + checkForPropsAfterMounting({ + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + policyDataProp = { + mockIsFeatureEnabled = "hello.world", + }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + } + end, + expectedProps = { + isPolicy1FeatureEnabled = "hello.world", + }, + }) + + local callsGetItem = Mock.getCalls(mockMemStorageService.GetItem) + expect(#callsGetItem).to.equal(0) + + local callsBindAndFire = Mock.getCalls(mockMemStorageService.BindAndFire) + expect(#callsBindAndFire).to.equal(0) + end) + end) + end) + + describe("GIVEN a policy prop that reads `foo`", function() + local mockPolicy1 = function(policy) + return { + isFeatureEnabled = policy.foo, + } + end + + describe("GIVEN getPolicyImpl with a static response", function() + local getPolicyImpl = MagicMock.new() + getPolicyImpl.read = function() + return { + foo = "bar", + } + end + + local connectInstance = connect(getPolicyImpl) + + it("SHOULD pass props to the lower component from the Promise resolution", function() + checkForPropsAfterMounting({ + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + hasProblemsFetching = (policy.isFeatureEnabled == nil), + } + end, + expectedProps = { + isPolicy1FeatureEnabled = "bar", + hasProblemsFetching = false, + }, + }) + end) + end) + + describe("GIVEN getPolicyImpl with a nil response", function() + local getPolicyImpl = MagicMock.new() + getPolicyImpl.read = function() + return nil + end + + local connectInstance = connect(getPolicyImpl) + + it("SHOULD not be able to define `isPolicy1FeatureEnabled`", function() + checkForPropsAfterMounting({ + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + hasProblemsFetching = (policy.isFeatureEnabled == nil) + } + end, + expectedProps = { + isPolicy1FeatureEnabled = nil, + hasProblemsFetching = true, + }, + }) + end) + end) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/endToEnd.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/endToEnd.spec.lua new file mode 100644 index 0000000..d4a48d9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/endToEnd.spec.lua @@ -0,0 +1,64 @@ +return function() + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + + local PolicyProvider = require(script.Parent) + + it("SHOULD work", function() + local presentationPolicy = function(externalPolicy) + return { + isFeatureEnabled = externalPolicy.isFeatureEnabled or false, + } + end + + local externalPolicy = { + isFeatureEnabled = true, + } + local getPolicyImpl = PolicyProvider.GetPolicyImplementations.Static(externalPolicy) + local UniversalAppPolicyProvider = PolicyProvider.withGetPolicyImplementation(getPolicyImpl) + + local function MyComponent(props) + -- Values from PolicyProvider can be accessed just like regular props + local isFeatureEnabled = props.isFeatureEnabled + + return Roact.createElement("ScreenGui", nil, { + Label = Roact.createElement("TextLabel", { + -- ...and used in your components! + Text = "isFeatureEnabled: " .. tostring(isFeatureEnabled), + Size = UDim2.new(1, 0, 1, 0), + }) + }) + end + + -- `mapPolicyToProps` should return a table containing props that will be passed to + -- your component! + + -- `connect` returns a function, so we call that function, passing in our + -- component, getting back a new component! + MyComponent = UniversalAppPolicyProvider.connect( + function(incomingPresentationPolicy, props) + -- mapPolicyToProps is run every time the policy's state updates. + -- It's also run whenever the component receives new props. + return { + isFeatureEnabled = incomingPresentationPolicy.isFeatureEnabled, + } + end + )(MyComponent) + + local app = Roact.createElement(UniversalAppPolicyProvider.Provider, { + -- policy can be a list of PresentationalPolicies + policy = { presentationPolicy }, + }, { + Main = Roact.createElement(MyComponent), + }) + + local folder = Instance.new("Folder") + Roact.mount(app, folder) + + -- if the label has the following string, we know + -- everything is working! + local Label = folder:FindFirstChild("Label", true) + expect(Label).to.be.ok() + expect(Label.Text).to.equal("isFeatureEnabled: true") + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromMemStorageService.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromMemStorageService.lua new file mode 100644 index 0000000..ed8aa44 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromMemStorageService.lua @@ -0,0 +1,74 @@ +local DefaultHttpService = game:GetService("HttpService") +local DefaultMemStorageService = game:GetService("MemStorageService") +local DefaultPlayersService = game:GetService("Players") + +return function(dependencies) + dependencies = dependencies or {} + dependencies.HttpService = dependencies.HttpService or DefaultHttpService + dependencies.MemStorageService = dependencies.MemStorageService or DefaultMemStorageService + dependencies.PlayersService = dependencies.PlayersService or DefaultPlayersService + + assert(dependencies.HttpService, "expected dependencies.HttpService") + assert(dependencies.MemStorageService, "expected dependencies.MemStorageService") + assert(dependencies.PlayersService, "expected dependencies.PlayersService") + + local HttpService = dependencies.HttpService + local MemStorageService = dependencies.MemStorageService + local PlayersService = dependencies.PlayersService + + return function(behavior) + assert(behavior, "expected behavior") + + local function getStoreKey() + local userId = -1 + if PlayersService.LocalPlayer then + userId = PlayersService.LocalPlayer.UserId + end + return "GUAC:" .. userId .. ":" .. behavior + end + + local previouslyReadValue + + return { + read = function() + local storeKey = getStoreKey() + local policyData = MemStorageService:GetItem(storeKey) + if policyData and #policyData > 0 then + local success, policy = pcall(function() + return HttpService:JSONDecode(policyData) + end) + if success then + -- Be sure to store the json string + previouslyReadValue = policyData + return policy + end + end + + return nil + end, + + onPolicyChanged = function(func) + local storeKey = getStoreKey() + local memStorageConnection = MemStorageService:BindAndFire(storeKey, function(newPolicyData) + -- MemStorageService will not du-duplicate the same item from storage + if newPolicyData ~= previouslyReadValue then + if newPolicyData and #newPolicyData > 0 then + local success, decodedExternalPolicy = pcall(function() + return HttpService:JSONDecode(newPolicyData) + end) + if success then + -- never store garbage + previouslyReadValue = newPolicyData + if func then + func(decodedExternalPolicy) + end + end + end + end + end) + + return memStorageConnection + end, + } + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromMemStorageService.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromMemStorageService.spec.lua new file mode 100644 index 0000000..5d374d1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromMemStorageService.spec.lua @@ -0,0 +1,273 @@ +return function() + local Packages = script.Parent.Parent.Parent + local Mock = require(Packages.Mock) + local MagicMock = Mock.MagicMock + + local fromMemStorageService = require(script.Parent.fromMemStorageService) + + describe("GIVEN a behavior", function() + local behavior = "mockBehavior" + describe("GIVEN a MemStorageService and HttpService with functional GetItem, BindAndFire and JSONDecode", function() + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return "jsonExternalPolicy" + end + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function() + return "decodedExternalPolicy" + end + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + HttpService = mockHttpService, + })(behavior) + it("SHOULD return the policy when `read` is invoked", function() + local result = fromMemStorageServiceInstance.read() + expect(result).to.equal("decodedExternalPolicy") + end) + + it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function() + local result = fromMemStorageServiceInstance.onPolicyChanged() + expect(result).to.be.ok() + expect(result.Disconnect).to.be.ok() + result:Disconnect() + end) + + it("SHOULD invoke passed in function with JSONDecode results when updateMemStorageService is fired", function() + local wasEverCalled = false + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + wasEverCalled = true + expect(value).to.equal("decodedExternalPolicy") + end) + + updateMemStorageService:Fire("jsonExternalPolicyUpdated") + + result:Disconnect() + + expect(wasEverCalled).to.equal(true) + end) + + it("SHOULD NOT invoke passed in function with JSONDecode results ".. + "when updateMemStorageService is fired with the same value", function() + local timesEverCalled = 0 + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + timesEverCalled = timesEverCalled + 1 + expect(value).to.equal("decodedExternalPolicy") + end) + + updateMemStorageService:Fire("foo") + updateMemStorageService:Fire("foo") + + result:Disconnect() + + expect(timesEverCalled).to.equal(1) + end) + + it("SHOULD NOT invoke passed in function with JSONDecode results ".. + "when updateMemStorageService is fired with the same value, while ignore nils", function() + local timesEverCalled = 0 + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + timesEverCalled = timesEverCalled + 1 + expect(value).to.equal("decodedExternalPolicy") + end) + + updateMemStorageService:Fire("bar") + updateMemStorageService:Fire(nil) + updateMemStorageService:Fire("bar") + + result:Disconnect() + + expect(timesEverCalled).to.equal(1) + end) + end) + + describe("GIVEN a functional MemStorageService and broken HttpService JSONDecode", function() + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return "jsonExternalPolicy" + end + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function() + return nil + end + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + HttpService = mockHttpService, + })(behavior) + it("SHOULD return nil when `read` is invoked", function() + local result = fromMemStorageServiceInstance.read() + expect(result).to.equal(nil) + end) + + it("SHOULD invoke passed in function with JSONDecode results when updateMemStorageService is fired", function() + local wasEverCalled = false + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + wasEverCalled = true + expect(value).to.equal(nil) + end) + + updateMemStorageService:Fire("jsonExternalPolicyUpdated") + + result:Disconnect() + + expect(wasEverCalled).to.equal(true) + end) + end) + + describe("GIVEN a MemStorageService that always returns invalid results", function() + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return nil + end + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + local mockHttpService = MagicMock.new() + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + HttpService = mockHttpService, + })(behavior) + it("SHOULD return nil when `read` is invoked", function() + local result = fromMemStorageServiceInstance.read() + expect(result).to.equal(nil) + end) + + it("SHOULD never invoke passed function with JSONDecode results when updateMemStorageService is fired", function() + local wasEverCalled = false + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + wasEverCalled = true + end) + + updateMemStorageService:Fire(nil) + + result:Disconnect() + + expect(wasEverCalled).to.equal(false) + end) + end) + + describe("GIVEN a PlayersService with a missing LocalPlayer", function() + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return "mockStorageItemJSON" + end + + local mockHttpService = MagicMock.new() + local mockPlayersService = MagicMock.new() + mockPlayersService.LocalPlayer = nil + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + PlayersService = mockPlayersService, + HttpService = mockHttpService, + })(behavior) + + it("SHOULD still return a value when `read` is invoked", function() + local result = fromMemStorageServiceInstance.read() + expect(result).to.be.ok() + end) + end) + + describe("GIVEN a HttpService that can throw based on MemStorageService's GetItem", function() + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return "garbage" + end + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function(_, value) + if value == "validJson" then + return { foo = true } + end + + error("invalid json") + end + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + HttpService = mockHttpService, + })(behavior) + + it("SHOULD return nil since GetItem returns garbage", function() + local result = fromMemStorageServiceInstance.read() + expect(result).to.never.be.ok() + end) + + it("SHOULD never invoke passed function with JSONDecode results when updateMemStorageService is fired", function() + local numberOfTimesCalled = 0 + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + numberOfTimesCalled = numberOfTimesCalled + 1 + end) + + updateMemStorageService:Fire("validJson") + updateMemStorageService:Fire("garbage") + updateMemStorageService:Fire("validJson") + + result:Disconnect() + + expect(numberOfTimesCalled).to.equal(1) + end) + end) + + describe("GIVEN a new instance of fromMemStorageService", function() + local validJson = "validJson" + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return validJson + end + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function(_, value) + return "tableFromJson" + end + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + HttpService = mockHttpService, + })(behavior) + + describe("WHEN read returns a value and onPolicyChanged returns the same value", function() + it("SHOULD not fire onPolicyChanged function", function() + local numberOfTimesCalled = 0 + fromMemStorageServiceInstance.onPolicyChanged(function() + numberOfTimesCalled = numberOfTimesCalled + 1 + end) + local result = fromMemStorageServiceInstance.read() + -- intentionally fire the same value that we read + updateMemStorageService:Fire(validJson) + + expect(numberOfTimesCalled).to.equal(0) + end) + end) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromPolicyService.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromPolicyService.lua new file mode 100644 index 0000000..9b1a350 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromPolicyService.lua @@ -0,0 +1,61 @@ +local DefaultPolicyService = game:GetService("PolicyService") +local DefaultPlayersService = game:GetService("Players") + +local root = script.Parent.Parent +local Packages = root.Parent +local Promise = require(Packages.Promise) + +local Logger = require(root.Logger) + +return function(dependencies) + dependencies = dependencies or {} + dependencies.PolicyService = dependencies.PolicyService or DefaultPolicyService + dependencies.PlayersService = dependencies.PlayersService or DefaultPlayersService + + assert(dependencies.PolicyService, "expected dependencies.PolicyService") + assert(dependencies.PlayersService, "expected dependencies.PlayersService") + + local PolicyService = dependencies.PolicyService + local PlayersService = dependencies.PlayersService + + return function() + return { + read = function() + return nil + end, + + onPolicyChanged = function(func) + local onPolicyChangedEvent = Instance.new("BindableEvent") + + -- be sure to connect before our Promise can resolve + local connection = onPolicyChangedEvent.Event:Connect(func) + + Promise.new(function(resolve, reject) + local player = PlayersService.LocalPlayer + if player then + local success, result = pcall(function() + return PolicyService:GetPolicyInfoForPlayerAsync(player) + end) + if success then + if result then + resolve(result) + else + reject("GetPolicyInfoForPlayerAsync return nil value") + end + else + reject("GetPolicyInfoForPlayerAsync had an error when calling") + end + else + reject("LocalPlayer not found") + end + end):andThen(function(newPolicy) + onPolicyChangedEvent:Fire(newPolicy) + end):catch(function(errorString) + Logger:warning("Could not fetch from PolicyService due to error: {}", errorString) + end) + + return connection + end, + } + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromPolicyService.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromPolicyService.spec.lua new file mode 100644 index 0000000..3208230 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromPolicyService.spec.lua @@ -0,0 +1,160 @@ +return function() + local Packages = script.Parent.Parent.Parent + local Mock = require(Packages.Mock) + local MagicMock = Mock.MagicMock + + local fromPolicyService = require(script.Parent.fromPolicyService) + + describe("GIVEN a fully mocked dependencies", function() + local mockPolicyService = MagicMock.new() + mockPolicyService.GetPolicyInfoForPlayerAsync = function() + return "mockGetPolicyInfoForPlayerAsyncResponse" + end + + local dependencies = { + PolicyService = mockPolicyService, + PlayersService = MagicMock.new(), + } + + describe("WHEN invoked", function() + local fromPolicyServiceInstance = fromPolicyService(dependencies)() + + it("SHOULD return a table", function() + expect(fromPolicyServiceInstance).to.be.a("table") + end) + + it("SHOULD return a table with read and `onPolicyChanged` fields", function() + expect(fromPolicyServiceInstance.read).to.be.a("function") + expect(fromPolicyServiceInstance.onPolicyChanged).to.be.a("function") + end) + + describe("WHEN `onPolicyChanged` is invoked", function() + local timesEverCalled = 0 + local lastValue + local connection = fromPolicyServiceInstance.onPolicyChanged(function(value) + timesEverCalled = timesEverCalled + 1 + lastValue = value + end) + it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function() + expect(connection).to.be.ok() + expect(connection.Disconnect).to.be.ok() + end) + + it("SHOULD fire when it resolves with data", function() + expect(timesEverCalled).to.equal(1) + expect(lastValue).to.equal("mockGetPolicyInfoForPlayerAsyncResponse") + end) + end) + + describe("WHEN `read` is invoked", function() + local result = fromPolicyServiceInstance.read() + it("SHOULD return nil", function() + expect(result).to.never.be.ok() + end) + end) + end) + end) + + describe("GIVEN a PlayersService with no LocalPlayer", function() + local mockPlayersService = MagicMock.new() + mockPlayersService.LocalPlayer = nil + + local dependencies = { + PolicyService = MagicMock.new(), + PlayersService = mockPlayersService, + } + + describe("WHEN invoked", function() + local fromPolicyServiceInstance = fromPolicyService(dependencies)() + + it("SHOULD return a table", function() + expect(fromPolicyServiceInstance).to.be.a("table") + end) + + describe("WHEN `onPolicyChanged` is invoked", function() + local timesEverCalled = 0 + local connection = fromPolicyServiceInstance.onPolicyChanged(function(value) + timesEverCalled = timesEverCalled + 1 + end) + it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function() + expect(connection).to.be.ok() + expect(connection.Disconnect).to.be.ok() + end) + + it("SHOULD never fire when it rejects", function() + expect(timesEverCalled).to.equal(0) + end) + end) + end) + end) + + describe("GIVEN a PolicyService that throws", function() + local mockPolicyService = MagicMock.new() + mockPolicyService.GetPolicyInfoForPlayerAsync = function() + error("Throw") + end + + local dependencies = { + PolicyService = mockPolicyService, + PlayersService = MagicMock.new(), + } + + describe("WHEN invoked", function() + local fromPolicyServiceInstance = fromPolicyService(dependencies)() + + it("SHOULD return a table", function() + expect(fromPolicyServiceInstance).to.be.a("table") + end) + + describe("WHEN `onPolicyChanged` is invoked", function() + local timesEverCalled = 0 + local connection = fromPolicyServiceInstance.onPolicyChanged(function(value) + timesEverCalled = timesEverCalled + 1 + end) + it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function() + expect(connection).to.be.ok() + expect(connection.Disconnect).to.be.ok() + end) + + it("SHOULD never fire when it rejects", function() + expect(timesEverCalled).to.equal(0) + end) + end) + end) + end) + + describe("GIVEN a PolicyService that returns nil", function() + local mockPolicyService = MagicMock.new() + mockPolicyService.GetPolicyInfoForPlayerAsync = function() + return nil + end + + local dependencies = { + PolicyService = mockPolicyService, + PlayersService = MagicMock.new(), + } + + describe("WHEN invoked", function() + local fromPolicyServiceInstance = fromPolicyService(dependencies)() + + it("SHOULD return a table", function() + expect(fromPolicyServiceInstance).to.be.a("table") + end) + + describe("WHEN `onPolicyChanged` is invoked", function() + local timesEverCalled = 0 + local connection = fromPolicyServiceInstance.onPolicyChanged(function(value) + timesEverCalled = timesEverCalled + 1 + end) + it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function() + expect(connection).to.be.ok() + expect(connection.Disconnect).to.be.ok() + end) + + it("SHOULD never fire when it rejects", function() + expect(timesEverCalled).to.equal(0) + end) + end) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromStaticSource.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromStaticSource.lua new file mode 100644 index 0000000..73400ab --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromStaticSource.lua @@ -0,0 +1,19 @@ +local function noOpt() +end + +return function() + return function(externalPolicy) + assert(externalPolicy, "expected externalPolicy") + + return { + read = function() + return externalPolicy + end, + + onPolicyChanged = function(func) + func = func or noOpt + return Instance.new("BindableEvent").Event:Connect(func) + end, + } + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromStaticSource.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromStaticSource.spec.lua new file mode 100644 index 0000000..174e1e6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromStaticSource.spec.lua @@ -0,0 +1,45 @@ +return function() + describe("WHEN required", function() + local fromStaticSource = require(script.Parent.fromStaticSource) + + it("SHOULD have the following interface", function() + expect(fromStaticSource).to.be.a("function") + end) + + describe("WHEN invoked", function() + local fromStaticSourceInstance = fromStaticSource() + it("SHOULD have the following interface", function() + expect(fromStaticSourceInstance).to.be.a("function") + end) + + describe("GIVEN a static value", function() + local mockExternalPolicy = "mockExternalPolicy" + + describe("WHEN invoked", function() + local fromStaticSourceInstanceWithExternalPolicy = fromStaticSourceInstance(mockExternalPolicy) + + it("SHOULD have the following interface", function() + expect(fromStaticSourceInstanceWithExternalPolicy).to.be.a("table") + expect(fromStaticSourceInstanceWithExternalPolicy.read).to.be.a("function") + expect(fromStaticSourceInstanceWithExternalPolicy.onPolicyChanged).to.be.a("function") + end) + + describe("WHEN read is invoked", function() + local result = fromStaticSourceInstanceWithExternalPolicy.read() + it("SHOULD return the static value", function() + expect(result).to.equal(mockExternalPolicy) + end) + end) + + describe("WHEN onPolicyChanged is invoked", function() + local result = fromStaticSourceInstanceWithExternalPolicy.onPolicyChanged() + it("SHOULD return a Disconnect-able object", function() + expect(result).to.be.ok() + expect(result.Disconnect).to.be.ok() + end) + end) + end) + end) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/init.lua new file mode 100644 index 0000000..55f3785 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/init.lua @@ -0,0 +1,28 @@ +local connect = require(script.connect) +local Provider = require(script.Provider) +local Logger = require(script.Logger) + +local GetPolicyImplementations = script.getPolicyImplementations +local fromMemStorageService = require(GetPolicyImplementations.fromMemStorageService) +local fromPolicyService = require(GetPolicyImplementations.fromPolicyService) +local fromStaticSource = require(GetPolicyImplementations.fromStaticSource) + +return { + withGetPolicyImplementation = function(getPolicyImpl) + -- assign default + assert(getPolicyImpl.read, "expected getPolicyImpl to have `read` function") + assert(getPolicyImpl.onPolicyChanged, "expected getPolicyImpl to have `onPolicyChanged` function") + return { + connect = connect(getPolicyImpl), + Provider = Provider(), + } + end, + + GetPolicyImplementations = { + MemStorageService = fromMemStorageService(), + PolicyService = fromPolicyService(), + Static = fromStaticSource(), + }, + + Logger = Logger, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/publicInterface.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/publicInterface.spec.lua new file mode 100644 index 0000000..4ab35c6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/publicInterface.spec.lua @@ -0,0 +1,82 @@ +return function() + local Packages = script.Parent.Parent + local tutils = require(Packages.tutils) + local Cryo = require(Packages.Cryo) + + -- Given an interface and a list of expected interface members, + -- this expectation will fail the test + local function expectInterface(actualInterface, expectedInterface) + -- build a list of keys that are in the actualInterface + local foundKeys = Cryo.List.map(expectedInterface, function(value) + if actualInterface[value] then + return value + end + end) + + -- compare the list of keys found in actualInterface and expectedInterface + local result = tutils.shallowEqual(foundKeys, expectedInterface) + if not result then + local differences = tutils.listDifferences(expectedInterface, foundKeys) + fail(string.format("Expected interface missing the following: %s", tutils.toString(differences))) + end + end + + describe("WHEN require is invoked", function() + local PolicyProvider = require(script.Parent) + + it("SHOULD have the following public interface", function() + expectInterface(PolicyProvider, { "withGetPolicyImplementation", "GetPolicyImplementations", "Logger" }) + end) + + it("SHOULD have all GetPolicyImplementations", function() + expectInterface(PolicyProvider.GetPolicyImplementations, { "MemStorageService", "PolicyService", "Static" }) + end) + + describe("WHEN withGetPolicyImplementation is invoked", function() + describe("GIVEN a stubbed GetPolicyImpl", function() + local stubbedGetPolicyImpl = { + read = function() + end, + onPolicyChanged = function() + end, + } + it("SHOULD have the following public interface", function() + local initializedProvider = PolicyProvider.withGetPolicyImplementation(stubbedGetPolicyImpl) + expectInterface(initializedProvider, { "connect", "Provider" }) + end) + end) + + describe("GIVEN a nothing", function() + it("SHOULD throw", function() + expect(function() + PolicyProvider.withGetPolicyImplementation() + end).to.throw() + end) + end) + + describe("GIVEN a GetPolicyImpl without a read", function() + local withoutRead = { + onPolicyChanged = function() + end + } + it("SHOULD throw", function() + expect(function() + PolicyProvider.withGetPolicyImplementation(withoutRead) + end).to.throw() + end) + end) + + describe("GIVEN a GetPolicyImpl without a onPolicyChanged", function() + local withoutOnPolicyChanged = { + read = function() + end + } + it("SHOULD throw", function() + expect(function() + PolicyProvider.withGetPolicyImplementation(withoutOnPolicyChanged) + end).to.throw() + end) + end) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/tutils.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/tutils.lua new file mode 100644 index 0000000..cb6c720 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/tutils.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["tutils"]["tutils"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lock.toml new file mode 100644 index 0000000..645b43f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/lua-symbol" +version = "0.1.0" +commit = "139fdfe6e4d4eca690887d3beb80e1514af501bf" +source = "git+https://github.rbx.com/roblox/lua-symbol#master" diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/Symbol.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/Symbol.lua new file mode 100644 index 0000000..8b6adaf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/Symbol.lua @@ -0,0 +1,43 @@ +--[[ + A 'Symbol' is an opaque marker type that can be used to signify unique + statuses. Symbols have the type 'userdata', but when printed to the console, + the name of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +--[[ + Create an unnamed Symbol. Usually, you should create a named Symbol using + Symbol.named(name) +]] +function Symbol.unnamed() + local self = newproxy(true) + + getmetatable(self).__tostring = function() + return "Unnamed Symbol" + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/Symbol.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/Symbol.spec.lua new file mode 100644 index 0000000..cde9be0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/Symbol.spec.lua @@ -0,0 +1,45 @@ +return function() + local Symbol = require(script.Parent.Symbol) + + describe("named", function() + it("should give an opaque object", function() + local symbol = Symbol.named("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = Symbol.named("foo") + + expect(tostring(symbol):find("foo")).to.be.ok() + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.named("abc") + local symbolB = Symbol.named("abc") + + expect(symbolA).never.to.equal(symbolB) + end) + end) + + describe("unnamed", function() + it("should give an opaque object", function() + local symbol = Symbol.unnamed() + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to some string", function() + local symbol = Symbol.unnamed() + + expect(tostring(symbol)).to.be.a("string") + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.unnamed() + local symbolB = Symbol.unnamed() + + expect(symbolA).never.to.equal(symbolB) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/init.lua new file mode 100644 index 0000000..6aaa8ef --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/init.lua @@ -0,0 +1 @@ +return require(script.Symbol) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lock.toml new file mode 100644 index 0000000..158d747 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lock.toml @@ -0,0 +1,6 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/lumberyak" +version = "0.1.0" +commit = "47552268e68c899226295e82936d3b68dfd53565" +source = "git+https://github.rbx.com/roblox/lumberyak#master" +dependencies = ["Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo"] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/Logger.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/Logger.lua new file mode 100644 index 0000000..49bfc08 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/Logger.lua @@ -0,0 +1,247 @@ +local Root = script.Parent.Parent +local Cryo = require(Root.Cryo) + +local Logger = {} +Logger.__index = Logger + +Logger.Levels = { + Error = "Error", + Warning = "Warning", + Info = "Info", + Debug = "Debug", + Trace = "Trace", +} + +local levelOrder = { + Logger.Levels.Error, + Logger.Levels.Warning, + Logger.Levels.Info, + Logger.Levels.Debug, + Logger.Levels.Trace, +} + +local levelRank = {} +for k, v in pairs(levelOrder) do + levelRank[v] = k +end + +function Logger.Levels.fromString(str) + if type(str) ~= "string" then + return nil + end + for _, k in pairs(levelOrder) do + if string.lower(k) == string.lower(str) then + return k + end + end + return nil +end + +function Logger.new(parent, name) + local logger = { + name = name, + sinks = {}, + children = {}, + parent = parent, + context = {}, + dirty = true, + active = {}, + cache = { + sinks = {}, + context = {}, + } + } + + for k, _ in pairs(Logger.Levels) do + if parent then + logger.active[k] = parent.active[k] + else + logger.active[k] = false + end + end + + if parent then + parent.children[logger] = true + end + + setmetatable(logger, Logger) + return logger +end + +-- Activate `level` and above logging levels. +local function setActive(level, node) + local maxLevel = levelRank[level] + if maxLevel then + for n = 1,maxLevel do + node.active[levelOrder[n]] = true + end + for k, _ in pairs(node.children) do + setActive(level, k) + end + end +end + +-- Set the dirty flag for `node` and all its children. +local function setDirty(node) + node.dirty = true + for k, _ in pairs(node.children) do + setDirty(k) + end +end + +-- Update the context and sinks cache for `node` and its ancestors. +local function updateCache(node) + if not node.dirty then + return + end + + if not node.parent then + node.cache.context = node.context + node.cache.sinks = node.sinks + node.dirty = false + return + end + + updateCache(node.parent) + + -- Dictionary join the context. List join the sinks. Concatenate the prefixes. + node.cache.context = Cryo.Dictionary.join(node.parent.cache.context, node.context) + if node.parent.cache.context.prefix and node.context.prefix then + node.cache.context.prefix = node.parent.cache.context.prefix .. node.context.prefix + end + node.cache.sinks = Cryo.List.join(node.parent.cache.sinks, node.sinks) + node.dirty = false +end + +-- Set the parent of this Logger and update its cache, active bits and dirty bit. +function Logger:setParent(parent) + if self.parent then + self.parent.children[self] = nil + end + + updateCache(parent) + self.parent = parent + self.parent.children[self] = true + + local maxLevel = -1 + for _, sink in pairs(parent.cache.sinks) do + local sinkLevel = levelRank[sink.maxLevel] + if sinkLevel then + maxLevel = math.max(maxLevel, levelRank[sink.maxLevel]) + end + end + + if maxLevel > -1 then + setActive(levelOrder[maxLevel], self) + end + + setDirty(self) +end + +function Logger:addSink(sink) + setActive(sink.maxLevel, self) + table.insert(self.sinks, sink) + setDirty(self) +end + +function Logger:setContext(context) + self.context = context + setDirty(self) +end + +local function log(level, node, args) + if node.dirty then + updateCache(node) + end + + -- Collect per-log context. + local fullContext = { + level = level, + rawMessage = args, + loggerName = node.name, + } + + -- Call any functions in the context. + for k, v in pairs(node.cache.context) do + if type(v) == "function" then + fullContext[k] = v() + else + fullContext[k] = v + end + end + + -- Interpolate the log message. + local interpMsg + if args.n == 0 then + interpMsg = "LUMBERYAK INTERNAL: No log message given" + else + interpMsg = args[1] + end + if fullContext.prefix then + interpMsg = fullContext.prefix .. interpMsg + end + + if interpMsg:find("{") then + local i = 1 + interpMsg = (interpMsg:gsub("{(.-)}", function(w) + -- Treat {} as a positional arg. + if w == "" then + i = i + 1 + return args[i] + end + return fullContext[w] or w + end)) + if i < args.n then + interpMsg = interpMsg .. "\nLUMBERYAK INTERNAL: Too many arguments given for format string" + elseif i > args.n then + interpMsg = interpMsg .. "\nLUMBERYAK INTERNAL: Too few arguments given for format string" + end + elseif args.n > 1 then + interpMsg = interpMsg .. "\nLUMBERYAK INTERNAL: Too many arguments given for format string" + end + + -- Send the message to any sinks that are listening to the right level. + local rank = levelRank[level] + for _, k in pairs(node.cache.sinks) do + if levelRank[k.maxLevel] and levelRank[k.maxLevel] >= rank then + k:log(interpMsg, fullContext) + end + end +end + +function Logger:error(...) + if not self.active[Logger.Levels.Error] then + return + end + log(Logger.Levels.Error, self, table.pack(...)) +end + +function Logger:warning(...) + if not self.active[Logger.Levels.Warning] then + return + end + log(Logger.Levels.Warning, self, table.pack(...)) +end + +function Logger:info(...) + if not self.active[Logger.Levels.Info] then + return + end + log(Logger.Levels.Info, self, table.pack(...)) +end + +function Logger:debug(...) + if not self.active[Logger.Levels.Debug] then + return + end + log(Logger.Levels.Debug, self, table.pack(...)) +end + +function Logger:trace(...) + if not self.active[Logger.Levels.Trace] then + return + end + log(Logger.Levels.Trace, self, table.pack(...)) +end + +return Logger diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/Logger.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/Logger.spec.lua new file mode 100644 index 0000000..e2a5778 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/Logger.spec.lua @@ -0,0 +1,659 @@ +return function() + local Logger = require(script.Parent.Logger) + + local function newSink(level) + return { + maxLevel = level, + seen = {}, + log = function(self, message, context) + table.insert(self.seen, {message=message, context=context}) + end, + } + end + + describe("A new Logger", function() + it("should be creatable without a parent", function() + expect(function() + local _ = Logger.new() + end).to.never.throw() + end) + + it("should be creatable with a parent", function() + expect(function() + local log1 = Logger.new() + local _ = Logger.new(log1) + end).to.never.throw() + end) + + it("should be creatable with a parent, alternate syntax", function() + expect(function() + local log1 = Logger.new() + local _ = log1:new() + end).to.never.throw() + end) + + it("should add sinks", function() + expect(function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + end).to.never.throw() + end) + + it("should add context", function() + expect(function() + local log = Logger.new() + log:setContext({foo = "bar"}) + end).to.never.throw() + end) + end) + + describe("Basic logging", function() + it("to the root logger", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo") + end) + + it("to a child logger", function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + + log2:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo") + end) + + it("to a sibling logger", function() + local log1 = Logger.new() + local log2 = log1:new() + local log3 = log1:new() + local sink = newSink(Logger.Levels.Info) + log2:addSink(sink) + + log3:info("foo") + + expect(#sink.seen).to.equal(0) + end) + + it("to a parent logger", function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(Logger.Levels.Info) + log2:addSink(sink) + + log1:info("foo") + + expect(#sink.seen).to.equal(0) + end) + + it("to a child logger, sink added first", function() + local log1 = Logger.new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + local log2 = log1:new() + + log2:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo") + end) + + it("to a sibling logger, sink added first", function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(Logger.Levels.Info) + log2:addSink(sink) + local log3 = log1:new() + + log3:info("foo") + + expect(#sink.seen).to.equal(0) + end) + end) + + describe("When logging different levels", function() + local cases = { + [Logger.Levels.Error] = 1, + [Logger.Levels.Warning] = 2, + [Logger.Levels.Info] = 3, + [Logger.Levels.Debug] = 4, + [Logger.Levels.Trace] = 5, + } + + for level, count in pairs(cases) do + it("logging to the root should respect " .. level, function() + local log = Logger.new() + local sink = newSink(level) + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(count) + end) + + it("logging to the child should respect " .. level, function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(level) + log1:addSink(sink) + + log2:error("error") + log2:warning("warning") + log2:info("info") + log2:debug("debug") + log2:trace("trace") + + expect(#sink.seen).to.equal(count) + end) + end + + describe("should treat invalid log levels as disabled", function() + it("should log to no levels when given a bad maxLevel", function() + local log = Logger.new() + local sink = newSink("not-a-level") + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(0) + end) + + it("should log to no levels when given nil", function() + local log = Logger.new() + local sink = newSink(nil) + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(0) + end) + + it("should handle multiple sinks when some are disabled", function() + local log = Logger.new() + local sink1 = newSink(nil) + local sink2 = newSink(Logger.Levels.Trace) + log:addSink(sink1) + log:addSink(sink2) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink1.seen).to.equal(0) + expect(#sink2.seen).to.equal(5) + end) + end) + end) + + describe("When logging different levels using fromString", function() + local cases = { + ["error"] = 1, + ["Warning"] = 2, + ["INFO"] = 3, + ["dEBUG"] = 4, + ["TrAcE"] = 5, + ["invalid"] = 0, + } + + for level, count in pairs(cases) do + it("fromString should handle " .. level, function() + local log = Logger.new() + local sink = newSink(Logger.Levels.fromString(level)) + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(count) + end) + end + end) + + describe("sinks", function() + it("should be disabled without error when maxLevel isn't set", function() + local log = Logger.new() + local seen = 0 + log:addSink({ + log = function() + seen = seen + 1 + end, + }) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(seen).to.equal(0) + end) + + it("should be disabled without error when maxLevel is set incorrectly", function() + local log = Logger.new() + local seen = 0 + log:addSink({ + maxLevel = "foo", + log = function() + seen = seen + 1 + end, + }) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(seen).to.equal(0) + end) + + it("should not cause problems with parenting when not set", function() + local log1 = Logger.new() + local seen = 0 + log1:addSink({ + log = function() + seen = seen + 1 + end, + }) + + local log2 = log1:new() + + log2:error("error") + log2:warning("warning") + log2:info("info") + log2:debug("debug") + log2:trace("trace") + + expect(seen).to.equal(0) + end) + end) + + describe("should treat invalid log levels as disabled", function() + it("should log to no levels when given a bad maxLevel", function() + local log = Logger.new() + local sink = newSink("not-a-level") + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(0) + end) + + it("should log to no levels when given nil", function() + local log = Logger.new() + local sink = newSink(nil) + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(0) + end) + + it("should handle multiple sinks when some are disabled", function() + local log = Logger.new() + local sink1 = newSink(nil) + local sink2 = newSink(Logger.Levels.Trace) + log:addSink(sink1) + log:addSink(sink2) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink1.seen).to.equal(0) + expect(#sink2.seen).to.equal(5) + end) + end) + + describe("with positional arguments", function() + it("should work with one arg", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {}", "bar") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo bar") + end) + + it("should work with two args", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {} {}", "bar", "baz") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo bar baz") + end) + + it("should output a warning with too many arguments", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {}", "bar", "baz") + + assert(string.find(sink.seen[1].message, "LUMBERYAK INTERNAL"), + "Expected an internal warning, got [[\n" .. sink.seen[1].message .. "\n]]") + end) + + it("should output a warning with too few arguments", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {} {}", "bar") + + assert(string.find(sink.seen[1].message, "LUMBERYAK INTERNAL"), + "Expected an internal warning, got [[\n" .. sink.seen[1].message .. "\n]]") + end) + end) + + describe("When passing in context", function() + describe("with context from root", function() + it("should pass along static context", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({bar = 1}) + + log:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].context.bar).to.equal(1) + end) + + it("should call dynamic context", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({bar = function() + return 1 + end}) + + log:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].context.bar).to.equal(1) + end) + end) + + describe("when combining context", function() + it("should merge non-overlapping context", function() + local log1 = Logger.new() + local log2 = log1:new() + local log3 = log2:new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + log1:setContext({bar = 1}) + log2:setContext({baz = 2}) + log3:setContext({quz = 3}) + + log3:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].context.bar).to.equal(1) + expect(sink.seen[1].context.baz).to.equal(2) + expect(sink.seen[1].context.quz).to.equal(3) + end) + + it("should overwrite overlapping context", function() + local log1 = Logger.new() + local log2 = log1:new() + local log3 = log2:new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + log1:setContext({bar = 1, baz = 1, quz = 1}) + log2:setContext({bar = 2, baz = 2}) + log3:setContext({baz = 3}) + + log3:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].context.quz).to.equal(1) + expect(sink.seen[1].context.bar).to.equal(2) + expect(sink.seen[1].context.baz).to.equal(3) + end) + + it("should call dynamic context", function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + log1:setContext({bar = 1}) + log2:setContext({bar = function() + return 2 + end}) + + log2:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].context.bar).to.equal(2) + end) + end) + end) + + describe("Message interpolation", function() + it("should leave plain messages alone", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({bar = "baz"}) + + log:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo") + end) + + it("should substitute info from static context", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({bar = "baz"}) + + log:info("foo {bar}") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo baz") + end) + + it("should substitute info from dynamic context", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({bar = function() + return "baz" + end}) + + log:info("foo {bar}") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo baz") + end) + end) + + describe("Message prefix", function() + it("should prepend the prefix", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({prefix = "foo: "}) + + log:info("bar") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo: bar") + end) + + it("should interpolate the prefix", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({prefix = "{foo}: ", foo = "baz"}) + + log:info("bar") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("baz: bar") + end) + + it("should stack prefixes", function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + log1:setContext({prefix = "foo: "}) + log2:setContext({prefix = "bar: "}) + + log2:info("baz") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo: bar: baz") + end) + end) + + it("Should get the name of logger used", function() + local log1 = Logger.new(nil, "log1") + local log2 = log1:new("log2") + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + + log1:info("{loggerName}") + log2:info("{loggerName}") + + expect(#sink.seen).to.equal(2) + expect(sink.seen[1].message).to.equal("log1") + expect(sink.seen[2].message).to.equal("log2") + end) + + describe("setParent should work", function() + it("when calling A->B then B->C", function() + local a = Logger.new() + local b = Logger.new() + local c = Logger.new() + + local sink = newSink(Logger.Levels.Info) + a:addSink(sink) + + a:setContext({a = "A"}) + b:setContext({b = "B"}) + c:setContext({c = "C"}) + + b:setParent(a) + c:setParent(b) + + c:info("{a} {b} {c}") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("A B C") + end) + + it("when calling B->C then A->B", function() + local a = Logger.new() + local b = Logger.new() + local c = Logger.new() + + local sink = newSink(Logger.Levels.Info) + a:addSink(sink) + + a:setContext({a = "A"}) + b:setContext({b = "B"}) + c:setContext({c = "C"}) + + c:setParent(b) + b:setParent(a) + + c:info("{a} {b} {c}") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("A B C") + end) + + it("when moving D from B to C", function() + local a = Logger.new() + local b = Logger.new() + local c = Logger.new() + local d = Logger.new() + + local sink = newSink(Logger.Levels.Info) + a:addSink(sink) + + a:setContext({x = "A"}) + b:setContext({y = "B"}) + c:setContext({y = "C"}) + d:setContext({z = "D"}) + + c:setParent(a) + b:setParent(a) + + d:setParent(b) + d:info("{x} {y} {z}") + + d:setParent(c) + d:info("{x} {y} {z}") + + expect(#sink.seen).to.equal(2) + expect(sink.seen[1].message).to.equal("A B D") + expect(sink.seen[2].message).to.equal("A C D") + end) + + it("when mixing setParent and static parents", function() + local a = Logger.new() + local b = Logger.new() + local c = b:new() + + local sink = newSink(Logger.Levels.Info) + a:addSink(sink) + + a:setContext({a = "A"}) + b:setContext({b = "B"}) + c:setContext({c = "C"}) + + b:setParent(a) + + c:info("{a} {b} {c}") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("A B C") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/MockLogger.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/MockLogger.lua new file mode 100644 index 0000000..1e63570 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/MockLogger.lua @@ -0,0 +1,47 @@ +-- This mock has the same API as the Logger, but none of the calls do anything. +-- Note that MockLogger and Logger can't be parented to each other. + +local MockLogger = {} + +MockLogger.Levels = { + Error = "MockError", + Warning = "MockWarning", + Info = "MockInfo", + Debug = "MockDebug", + Trace = "MockTrace", + fromString = function() + return "MockInfo" + end, +} + +MockLogger.__index = MockLogger + +function MockLogger.new() + return setmetatable({}, MockLogger) +end + +function MockLogger.setParent() +end + +function MockLogger.setContext() +end + +function MockLogger.addSink() +end + +function MockLogger.error() +end + +function MockLogger.warning() +end + +function MockLogger.info() +end + +function MockLogger.debug() +end + +function MockLogger.trace() +end + +return MockLogger diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/MockLogger.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/MockLogger.spec.lua new file mode 100644 index 0000000..c7fcbe7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/MockLogger.spec.lua @@ -0,0 +1,22 @@ +return function() + local MockLogger = require(script.Parent.MockLogger) + local Logger = require(script.Parent.Logger) + + it("MockLogger should have the same API as Logger", function() + for k, v in pairs(Logger) do + local mock = MockLogger[k] + assert(mock, "Expected a mock of " .. k) + assert(type(mock) == type(v), + "Expected the type of " .. k .. " to be " .. type(v) .. ", got " .. type(mock)) + end + end) + + it("MockLogger.Levels should have the same API as Logger.Levels", function() + for k, v in pairs(Logger.Levels) do + local mock = MockLogger.Levels[k] + assert(mock, "Expected a mock of " .. k) + assert(type(mock) == type(v), + "Expected the type of " .. k .. " to be " .. type(v) .. ", got " .. type(mock)) + end + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/benchmark/benchmark.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/benchmark/benchmark.lua new file mode 100644 index 0000000..1b75663 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/benchmark/benchmark.lua @@ -0,0 +1,159 @@ +local Logger = require(Workspace.LoadedCode.Packages.Lumberyak.Logger) + +local runs = 2000000 + +local function timeit(makeLogger, callLogger) + local log = makeLogger() + + local t = tick() + for _ = 1, runs do + callLogger(log) + end + return (tick() - t) / runs +end + +local function callSimple(log) + log:info("foo") +end + +local function callInterp(log) + log:info("foo {}", 2) +end + +local function simple() + local count = 0 + local countSink = { + maxLevel = Logger.Levels.Info, + log = function(_, _, context) + count = count + context.number + end, + } + + local log = Logger.new() + log:setContext({number = 1}) + log:addSink(countSink) + return log +end + +local function empty() + return { + info = function() end + } +end + +local function off() + local count = 0 + local countSink = { + maxLevel = Logger.Levels.Error, + log = function(_, _, context) + count = count + context.number + end, + } + + local log = Logger.new() + log:setContext({number = 1}) + log:addSink(countSink) + return log +end + +local function short() + local count = 0 + local countSink = { + maxLevel = Logger.Levels.Info, + log = function(_, _, context) + count = count + context.number + end, + } + + local log = Logger.new() + local child = log:new() + log:setContext({number = 1}) + log:addSink(countSink) + return child +end + +local function long() + local count = 0 + local countSink = { + maxLevel = Logger.Levels.Info, + log = function(_, _, context) + count = count + context.number + end, + } + + local log = Logger.new() + log:setContext({number = 1}) + log:addSink(countSink) + local a = log:new() + local b = a:new() + local c = b:new() + local d = c:new() + return d +end + +local function many() + local count = 0 + local sink1 = { + maxLevel = Logger.Levels.Error, + log = function(_, _, context) + count = count + context.number + end, + } + local sink2 = { + maxLevel = Logger.Levels.Warning, + log = function(_, _, context) + count = count + context.number + end, + } + local sink3 = { + maxLevel = Logger.Levels.Info, + log = function(_, _, context) + count = count + context.number + end, + } + local sink4 = { + maxLevel = Logger.Levels.Debug, + log = function(_, _, context) + count = count + context.number + end, + } + local sink5 = { + maxLevel = Logger.Levels.Trace, + log = function(_, _, context) + count = count + context.number + end, + } + + local log = Logger.new() + log:setContext({number = 1}) + log:addSink(sink1) + log:addSink(sink2) + log:addSink(sink3) + log:addSink(sink4) + log:addSink(sink5) + return log +end + +local base = timeit(empty, callSimple) +local interp = timeit(empty, callInterp) + +local tests = { + ["Log level off"] = off, + ["Simple logger"] = simple, + ["Short chain"] = short, + ["Long chain"] = long, + ["Many sinks"] = many, +} + +local function fmt(name, case, t) + print(string.format("%-40s %0.3e [%0.4fx]", name .. " - " .. case, t, t / base)) +end + +fmt("Empty function", "Simple", base) +fmt("Empty function", "Interpolation", interp) +for k, v in pairs(tests) do + local t1 = timeit(v, callSimple) + fmt(k, "Simple", t1) + local t2 = timeit(v, callInterp) + fmt(k, "Interpolation", t2) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/app/PrintSink.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/app/PrintSink.lua new file mode 100644 index 0000000..90b6ff3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/app/PrintSink.lua @@ -0,0 +1,24 @@ +local Logger = require(script.Parent.Parent.Parent.Logger) + +local PrintSink = {} + +function PrintSink.new(level) + local printer = { + maxLevel = level + } + + setmetatable(printer, PrintSink) + return printer +end + +function PrintSink:log(message, context) + if context.level == Logger.Levels.Error then + error(message, 5) + elseif context.level == Logger.Levels.Warning then + warn(message) + else + print(message) + end +end + +return PrintSink diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/app/app.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/app/app.lua new file mode 100644 index 0000000..2d1f265 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/app/app.lua @@ -0,0 +1,13 @@ +local log = require(script.Parent.appLogger) +-- The app has some way to import the page, but not vice versa. +local page = require(script.Parent.Parent.page.page) +local printer = require(script.Parent.PrintSink) + +log:addSink(printer.new(log.Levels.Error)) + +return function() + log:info("calling {root}") + page.init(log) + + return page.doSomething() +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/app/appLogger.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/app/appLogger.lua new file mode 100644 index 0000000..a02f33d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/app/appLogger.lua @@ -0,0 +1,4 @@ +local Logger = require(script.Parent.Parent.Parent.Logger) +local log = Logger.new() +log:setContext({root = "root"}) +return log diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/example.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/example.spec.lua new file mode 100644 index 0000000..c74c9ea --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/example.spec.lua @@ -0,0 +1,26 @@ +return function() + local app = require(script.Parent.app.app) + local log = require(script.Parent.app.appLogger) + + local function newSink(level) + return { + maxLevel = level, + seen = {}, + log = function(self, message, context) + table.insert(self.seen, {message=message, context=context}) + end, + } + end + + it("should generate expected messages", function() + local sink = newSink(log.Levels.Info) + log:addSink(sink) + + local result = app() + expect(result).to.equal("done") + expect(#sink.seen).to.equal(3) + expect(sink.seen[1].message).to.equal("calling root") + expect(sink.seen[2].message).to.equal("calling root foo") + expect(sink.seen[3].message).to.equal("calling root foo bar") + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/page/component.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/page/component.lua new file mode 100644 index 0000000..21e165c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/page/component.lua @@ -0,0 +1,7 @@ +local log = require(script.Parent.pageLogger):new() +log:setContext({bar = "bar"}) + +return function() + log:info("calling {root} {foo} {bar}") + return "done" +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/page/page.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/page/page.lua new file mode 100644 index 0000000..324b9ec --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/page/page.lua @@ -0,0 +1,13 @@ +-- foo does not need to require root +local log = require(script.Parent.pageLogger) +local component = require(script.Parent.component) + +return { + init = function(logParent) + log:setParent(logParent) + end, + doSomething = function() + log:info("calling {root} {foo}") + return component() + end +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/page/pageLogger.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/page/pageLogger.lua new file mode 100644 index 0000000..d8d0b89 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/example/page/pageLogger.lua @@ -0,0 +1,4 @@ +local Logger = require(script.Parent.Parent.Parent.Logger) +local log = Logger.new() +log:setContext({foo = "foo"}) +return log diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/init.lua new file mode 100644 index 0000000..ddbb40f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak/lumberyak/init.lua @@ -0,0 +1,3 @@ +return { + Logger = require(script.Logger), +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/lock.toml new file mode 100644 index 0000000..bf889bd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/otter" +version = "0.1.2" +commit = "572480526578e1bbd583959e959f0d10118c05a5" +source = "url+https://github.com/roblox/otter" diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/assign.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/assign.lua new file mode 100644 index 0000000..7b763d5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/assign.lua @@ -0,0 +1,13 @@ +local function assign(target, ...) + for i = 1, select("#", ...) do + local source = select(i, ...) + + for key, value in pairs(source) do + target[key] = value + end + end + + return target +end + +return assign \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createGroupMotor.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createGroupMotor.lua new file mode 100644 index 0000000..fbddea3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createGroupMotor.lua @@ -0,0 +1,136 @@ +local RunService = game:GetService("RunService") + +local assign = require(script.Parent.assign) +local createSignal = require(script.Parent.createSignal) + +local GroupMotor = {} +GroupMotor.prototype = {} +GroupMotor.__index = GroupMotor.prototype + +local function createGroupMotor(initialValues) + assert(typeof(initialValues) == "table") + + local states = {} + + for key, value in pairs(initialValues) do + states[key] = { + value = value, + complete = true, + } + end + + local self = { + __goals = {}, + __states = states, + __allComplete = true, + __onComplete = createSignal(), + __onStep = createSignal(), + __running = false, + } + + setmetatable(self, GroupMotor) + + return self +end + +function GroupMotor.prototype:start() + if self.__running then + return + end + + self.__connection = RunService.Heartbeat:Connect(function(dt) + self:step(dt) + end) + + self.__running = true +end + +function GroupMotor.prototype:stop() + if self.__connection ~= nil then + self.__connection:Disconnect() + self.__running = false + end +end + +function GroupMotor.prototype:step(dt) + assert(typeof(dt) == "number") + + if self.__allComplete then + return + end + + local allComplete = true + local values = {} + + for key, state in pairs(self.__states) do + if not state.complete then + local goal = self.__goals[key] + + if goal ~= nil then + local maybeNewState = goal:step(state, dt) + + if maybeNewState ~= nil then + state = maybeNewState + self.__states[key] = maybeNewState + end + else + state.complete = true + end + + if not state.complete then + allComplete = false + end + end + + values[key] = state.value + end + + local wasAllComplete = self.__allComplete + self.__allComplete = allComplete + + self.__onStep:fire(values) + + -- Check self.__allComplete as the motor may have been restarted in the onStep callback + -- even if allComplete is true. + if self.__allComplete and not wasAllComplete then + self:stop() + self.__onComplete:fire(values) + end +end + +function GroupMotor.prototype:setGoal(goals) + assert(typeof(goals) == "table") + + self.__goals = assign({}, self.__goals, goals) + + for key in pairs(goals) do + local state = self.__states[key] + + if state == nil then + error(("Cannot set goal for the value %s because it doesn't exist"):format(tostring(key)), 2) + end + + state.complete = false + end + + self.__allComplete = false + self:start() +end + +function GroupMotor.prototype:onStep(callback) + assert(typeof(callback) == "function") + + return self.__onStep:subscribe(callback) +end + +function GroupMotor.prototype:onComplete(callback) + assert(typeof(callback) == "function") + + return self.__onComplete:subscribe(callback) +end + +function GroupMotor.prototype:destroy() + self:stop() +end + +return createGroupMotor diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createGroupMotor.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createGroupMotor.spec.lua new file mode 100644 index 0000000..0da323a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createGroupMotor.spec.lua @@ -0,0 +1,255 @@ +return function() + local validateMotor = require(script.Parent.validateMotor) + local createSpy = require(script.Parent.createSpy) + + local createGroupMotor = require(script.Parent.createGroupMotor) + + -- test motion object that completes after step has been called numSteps times + local function createStepper(numSteps) + local self = { + stepCount = 0, + } + + self.step = function(_, state, dt) + self.stepCount = self.stepCount + 1 + + if self.stepCount >= numSteps then + return { + value = state.value, + velocity = state.velocity, + complete = true, + } + end + + return state + end + + setmetatable(self, { + __index = function(_, key) + error(("%q is not a valid member of stepper"):format(key)) + end, + }) + + return self + end + + it("should be a valid motor", function() + local motor = createGroupMotor({}) + validateMotor(motor) + motor:destroy() + end) + + describe("onStep", function() + it("should not be called initially", function() + local motor = createGroupMotor({ + x = 0, + }) + + local spy = createSpy() + motor:onStep(spy.value) + + motor:setGoal({ + x = createStepper(5), + }) + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + end) + + describe("setGoal", function() + it("should work as intended in onComplete callbacks", function() + local motor = createGroupMotor({ + x = 0, + }) + + local spy = createSpy(function() + motor:setGoal({ x = createStepper(3), }) + end) + + motor:onComplete(spy.value) + + motor:setGoal({ x = createStepper(3), }) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(1) + --make sure the motor continues to run after calling setGoal in onComplete + expect(motor.__running).to.equal(true) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(2) + expect(motor.__running).to.equal(true) + + motor:destroy() + end) + end) + + describe("onComplete should be called when", function() + it("has completed its motion", function() + local motor = createGroupMotor({ + x = 0, + }) + + motor:setGoal({ + x = createStepper(5), + }) + + local spy = createSpy() + + motor:onComplete(spy.value) + + for _ = 1, 5 do + motor:step(1) + end + + expect(spy.callCount).to.equal(1) + + motor:destroy() + end) + + it("has multiple atributes in motion", function() + local motor = createGroupMotor({ + x = 0, + y = 10, + }) + + motor:setGoal({ + x = createStepper(2), + y = createStepper(5), + }) + + local spy = createSpy() + + motor:onComplete(spy.value) + + for _ = 1, 2 do + motor:step(1) + end + + expect(spy.callCount).to.equal(0) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(1) + + motor:destroy() + end) + + it("has restarted its motion", function() + local motor = createGroupMotor({ + x = 0, + }) + + motor:setGoal({ + x = createStepper(3), + }) + + local spy = createSpy() + + motor:onComplete(spy.value) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(1) + + motor:setGoal({ + x = createStepper(3), + }) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(2) + + motor:destroy() + end) + end) + + describe("onComplete should not be called when", function() + it("has no goals set", function() + local motor = createGroupMotor({ + x = 2, + }) + + local spy = createSpy() + motor:onComplete(spy.value) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + + it("has not completed motion", function() + local motor = createGroupMotor({ + x = 0, + }) + + motor:setGoal({ + x = createStepper(2), + }) + + local spy = createSpy() + motor:onComplete(spy.value) + + motor:step(1) + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + + it("has one non-completed motion", function() + local motor = createGroupMotor({ + x = 0, + y = 0, + }) + + motor:setGoal({ + x = createStepper(0), + y = createStepper(2), + }) + + local spy = createSpy() + motor:onComplete(spy.value) + + motor:step(1) + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + + it("does not call step", function() + local motor = createGroupMotor({ + x = 0, + }) + + motor:setGoal({ + x = createStepper(0), + }) + + local spy = createSpy() + motor:onComplete(spy.value) + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSignal.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSignal.lua new file mode 100644 index 0000000..5632b80 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSignal.lua @@ -0,0 +1,61 @@ +local function addToMap(map, addKey, addValue) + local new = {} + + for key, value in pairs(map) do + new[key] = value + end + + new[addKey] = addValue + + return new +end + +local function removeFromMap(map, removeKey) + local new = {} + + for key, value in pairs(map) do + if key ~= removeKey then + new[key] = value + end + end + + return new +end + +local function createSignal() + local connections = {} + + local function subscribe(self, callback) + assert(typeof(callback) == "function", "Can only subscribe to signals with a function.") + + local connection = { + callback = callback, + } + + connections = addToMap(connections, callback, connection) + + local function disconnect() + assert(not connection.disconnected, "Listeners can only be disconnected once.") + + connection.disconnected = true + connections = removeFromMap(connections, callback) + end + + return disconnect + end + + local function fire(self, ...) + for callback, connection in pairs(connections) do + if not connection.disconnected then + callback(...) + end + end + end + + return { + subscribe = subscribe, + fire = fire, + } +end + +return createSignal \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSignal.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSignal.spec.lua new file mode 100644 index 0000000..ed9cf97 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSignal.spec.lua @@ -0,0 +1,89 @@ +return function() + local createSignal = require(script.Parent.createSignal) + + local createSpy = require(script.Parent.createSpy) + + it("should fire subscribers and disconnect them", function() + local signal = createSignal() + + local spy = createSpy() + local disconnect = signal:subscribe(spy.value) + + expect(spy.callCount).to.equal(0) + + local a = 1 + local b = {} + local c = "hello" + signal:fire(a, b, c) + + expect(spy.callCount).to.equal(1) + spy:assertCalledWith(a, b, c) + + disconnect() + + signal:fire() + + expect(spy.callCount).to.equal(1) + end) + + it("should handle multiple subscribers", function() + local signal = createSignal() + + local spyA = createSpy() + local spyB = createSpy() + + local disconnectA = signal:subscribe(spyA.value) + local disconnectB = signal:subscribe(spyB.value) + + expect(spyA.callCount).to.equal(0) + expect(spyB.callCount).to.equal(0) + + local a = {} + local b = 67 + signal:fire(a, b) + + expect(spyA.callCount).to.equal(1) + spyA:assertCalledWith(a, b) + + expect(spyB.callCount).to.equal(1) + spyB:assertCalledWith(a, b) + + disconnectA() + + signal:fire(b, a) + + expect(spyA.callCount).to.equal(1) + + expect(spyB.callCount).to.equal(2) + spyB:assertCalledWith(b, a) + + disconnectB() + end) + + it("should stop firing a connection if disconnected mid-fire", function() + local signal = createSignal() + + -- In this test, we'll connect two listeners that each try to disconnect + -- the other. Because the order of listeners firing isn't defined, we + -- have to be careful to handle either case. + + local disconnectA + local disconnectB + + local spyA = createSpy(function() + disconnectB() + end) + + local spyB = createSpy(function() + disconnectA() + end) + + disconnectA = signal:subscribe(spyA.value) + disconnectB = signal:subscribe(spyB.value) + + signal:fire() + + -- Exactly once listener should have been called. + expect(spyA.callCount + spyB.callCount).to.equal(1) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSingleMotor.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSingleMotor.lua new file mode 100644 index 0000000..32cced3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSingleMotor.lua @@ -0,0 +1,95 @@ +local RunService = game:GetService("RunService") + +local createSignal = require(script.Parent.createSignal) + +local SingleMotor = {} +SingleMotor.prototype = {} +SingleMotor.__index = SingleMotor.prototype + +local function createSingleMotor(initialValue) + assert(typeof(initialValue) == "number") + + local self = { + __goal = nil, + __state = { + value = initialValue, + complete = true, + }, + __onComplete = createSignal(), + __onStep = createSignal(), + __running = false, + } + + setmetatable(self, SingleMotor) + + return self +end + +function SingleMotor.prototype:start() + if self.__running then + return + end + + self.__connection = RunService.Heartbeat:Connect(function(dt) + self:step(dt) + end) + + self.__running = true +end + +function SingleMotor.prototype:stop() + if self.__connection ~= nil then + self.__connection:Disconnect() + end + + self.__running = false +end + +function SingleMotor.prototype:step(dt) + assert(typeof(dt) == "number") + + if self.__state.complete then + return + end + + if self.__goal == nil then + return + end + + local newState = self.__goal:step(self.__state, dt) + + if newState ~= nil then + self.__state = newState + end + + self.__onStep:fire(self.__state.value) + + if self.__state.complete then + self:stop() + self.__onComplete:fire(self.__state.value) + end +end + +function SingleMotor.prototype:setGoal(goal) + self.__goal = goal + self.__state.complete = false + self:start() +end + +function SingleMotor.prototype:onStep(callback) + assert(typeof(callback) == "function") + + return self.__onStep:subscribe(callback) +end + +function SingleMotor.prototype:onComplete(callback) + assert(typeof(callback) == "function") + + return self.__onComplete:subscribe(callback) +end + +function SingleMotor.prototype:destroy() + self:stop() +end + +return createSingleMotor \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSingleMotor.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSingleMotor.spec.lua new file mode 100644 index 0000000..1696242 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSingleMotor.spec.lua @@ -0,0 +1,179 @@ +return function() + local validateMotor = require(script.Parent.validateMotor) + local createSpy = require(script.Parent.createSpy) + + local createSingleMotor = require(script.Parent.createSingleMotor) + + + local identityGoal = { + step = function(self, state, dt) + return state + end, + } + + -- test motion object that completes after step has been called numSteps times + local function createStepper(numSteps) + local self = { + stepCount = 0, + } + + self.step = function(_, state, dt) + self.stepCount = self.stepCount + 1 + + if self.stepCount >= numSteps then + return { + value = state.value, + velocity = state.velocity, + complete = true, + } + end + + return state + end + + setmetatable(self, { + __index = function(_, key) + error(("%q is not a valid member of stepper"):format(key)) + end, + }) + + return self + end + + it("should be a valid motor", function() + local motor = createSingleMotor(0) + validateMotor(motor) + motor:destroy() + end) + + it("should invoke subscribers with new values", function() + local motor = createSingleMotor(8) + motor:setGoal(identityGoal) + + local spy = createSpy() + + local disconnect = motor:onStep(spy.value) + + expect(spy.callCount).to.equal(0) + + motor:step(1) + + expect(spy.callCount).to.equal(1) + spy:assertCalledWith(8) + + disconnect() + + motor:step(1) + + expect(spy.callCount).to.equal(1) + + motor:destroy() + end) + + describe("setGoal", function() + it("should work as intended in onComplete callbacks", function() + local motor = createSingleMotor(0) + + local spy = createSpy(function() + motor:setGoal(createStepper(3)) + end) + + motor:onComplete(spy.value) + + motor:setGoal(createStepper(3)) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(1) + --make sure the motor continues to run after calling setGoal in onComplete + expect(motor.__running).to.equal(true) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(2) + expect(motor.__running).to.equal(true) + + motor:destroy() + end) + end) + + describe("onComplete should be called when", function() + it("has completed its motion", function() + local motor = createSingleMotor(0) + motor:setGoal(createStepper(5)) + + local spy = createSpy() + + motor:onComplete(spy.value) + + for _ = 1, 5 do + motor:step(1) + end + + expect(spy.callCount).to.equal(1) + + motor:destroy() + end) + + it("has restarted its motion", function() + local motor = createSingleMotor(0) + motor:setGoal(createStepper(5)) + + local spy = createSpy() + + motor:onComplete(spy.value) + + expect(spy.callCount).to.equal(0) + + for _ = 1, 5 do + motor:step(1) + end + + expect(spy.callCount).to.equal(1) + + motor:setGoal(createStepper(5)) + + for _ = 1, 5 do + motor:step(1) + end + + expect(spy.callCount).to.equal(2) + + motor:destroy() + end) + end) + + describe("onComplete should not be called when", function() + it("has not completed motion", function() + local motor = createSingleMotor(0) + motor:setGoal(createStepper(10)) + + local spy = createSpy() + + motor:onComplete(spy.value) + + motor:step(1) + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + + it("does not call step", function() + local motor = createSingleMotor(0) + motor:setGoal(createStepper(0)) + + local spy = createSpy() + + motor:onComplete(spy.value) + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSpy.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSpy.lua new file mode 100644 index 0000000..8f23651 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSpy.lua @@ -0,0 +1,38 @@ +local function createSpy(inner) + local self = { + callCount = 0, + values = {}, + valuesLength = 0, + } + + self.value = function(...) + self.callCount = self.callCount + 1 + self.values = {...} + self.valuesLength = select("#", ...) + + if inner ~= nil then + return inner(...) + end + end + + self.assertCalledWith = function(_, ...) + local len = select("#", ...) + + assert(self.valuesLength, len, "length of expected values differs from stored values") + + for i = 1, len do + local expected = select(i, ...) + assert(self.values[i], expected, "value differs") + end + end + + setmetatable(self, { + __index = function(_, key) + error(("%q is not a valid member of spy"):format(key)) + end, + }) + + return self +end + +return createSpy \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/init.lua new file mode 100644 index 0000000..88a73be --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/init.lua @@ -0,0 +1,6 @@ +return { + createGroupMotor = require(script.createGroupMotor), + createSingleMotor = require(script.createSingleMotor), + spring = require(script.spring), + instant = require(script.instant), +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/init.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/init.spec.lua new file mode 100644 index 0000000..688ee33 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/init.spec.lua @@ -0,0 +1,5 @@ +return function() + it("should load successfully", function() + require(script.Parent) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/instant.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/instant.lua new file mode 100644 index 0000000..0ab2a7d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/instant.lua @@ -0,0 +1,15 @@ +local function step(self, state, dt) + return { + value = self.__targetValue, + complete = true, + } +end + +local function instant(targetValue) + return { + __targetValue = targetValue, + step = step, + } +end + +return instant \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/instant.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/instant.spec.lua new file mode 100644 index 0000000..84ffb0c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/instant.spec.lua @@ -0,0 +1,39 @@ +return function() + local instant = require(script.Parent.instant) + + it("should have the expected APIs", function() + local goal = instant(5) + + expect(goal).to.be.a("table") + expect(goal.step).to.be.a("function") + end) + + it("should immediately complete", function() + local state = { + value = 5, + complete = false, + } + + local goal = instant(10) + state = goal:step(state, 1e-3) + + expect(state.value).to.equal(10) + expect(state.complete).to.equal(true) + end) + + it("should remove extra values from state", function() + local state = { + value = 5, + complete = false, + + velocity = 7, + somethingElse = {}, + } + + local goal = instant(10) + state = goal:step(state, 1e-3) + + expect(state.velocity).to.never.be.ok() + expect(state.somethingElse).to.never.be.ok() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/spring.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/spring.lua new file mode 100644 index 0000000..ff3bfe1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/spring.lua @@ -0,0 +1,159 @@ +--[[ + An analytical spring solution as a function of damping ratio and frequency. + + Adapted from + https://gist.github.com/Fraktality/1033625223e13c01aa7144abe4aaf54d +]] + +local assign = require(script.Parent.assign) + +local pi = math.pi +local abs = math.abs +local exp = math.exp +local sin = math.sin +local cos = math.cos +local sqrt = math.sqrt + +local DEFAULT_RESTING_VELOCITY_LIMIT = 1e-3 +local DEFAULT_RESTING_POSITION_LIMIT = 1e-2 + +local function step(self, state, dt) + -- Advance the spring simulation by dt seconds. + -- Take the damped harmonic oscillator ODE: + -- f^2*(X[t] - g) + 2*d*f*X'[t] + X''[t] = 0 + -- Where X[t] is position at time t, g is desired position, f is angular frequency, and d is damping ratio. + -- Apply constant initial conditions: + -- X[0] = p0 + -- X'[0] = v0 + -- Solve the IVP to get analytic expressions for X[t] and X'[t]. + -- The solution takes on one of three forms for d=1, d<1, and d>1 + + local d = self.__dampingRatio + local f = self.__frequency * 2 * pi -- Rad/s + local g = self.__goalPosition + local velLimit = self.__restingVelocityLimit + local posLimit = self.__restingPositionLimit + + local p0 = state.value + local v0 = state.velocity or 0 + + local offset = p0 - g + local decay = exp(-dt*d*f) + + local p1, v1 + + if d == 1 then -- Critically damped + p1 = (v0*dt + offset*(f*dt + 1))*decay + g + v1 = (v0 - f*dt*(offset*f + v0))*decay + + elseif d < 1 then -- Underdamped + local c = sqrt(1 - d*d) + + local i = cos(f*c*dt) + local j = sin(f*c*dt) + + -- Problem: Damping ratios close to 1 can cause numerical instability. + -- Solution: Rearrange to group terms involving j/c, then find an approximation z for j/c. + -- z = sin(dt*f*c)/c + -- Substitute a for dt*f + -- z = sin(a*c)/c + -- Take the 5th-order series expansion of z at c = 0 + -- z = a - (a^3*c^2)/6 + (a^5*c^4)/120 + O(c^6) + -- z ≈ a - (a^3*c^2)/6 + (a^5*c^4)/120 + -- Rewrite in Horner form to mitigate precision issues + -- z ≈ a + ((a*a)*(c*c)*(c*c)/20 - c*c)*(a*a*a)/6 + + local z + if c > 1e-4 then + z = j/c + else + local a = dt*f + z = a + ((a*a)*(c*c)*(c*c)/20 - c*c)*(a*a*a)/6 + end + + -- Repeat the process with a->dt and c->b=f*c for the f->0 case + local y + if f*c > 1e-4 then + y = j/(f*c) + else + local b = f*c + y = dt + ((dt*dt)*(b*b)*(b*b)/20 - b*b)*(dt*dt*dt)/6 + end + + p1 = (offset*(i + d*z) + v0*y)*decay + g + v1 = (v0*(i - z*d) - offset*(z*f))*decay + + else -- Overdamped + local c = sqrt(d*d - 1) + + local r1 = -f*(d - c) + local r2 = -f*(d + c) + + local co2 = (v0 - r1*offset)/(2*f*c) + local co1 = offset - co2 + + local e1 = co1*exp(r1*dt) + local e2 = co2*exp(r2*dt) + + p1 = e1 + e2 + g + v1 = r1*e1 + r2*e2 + end + + local positionOffset = abs(p1 - self.__goalPosition) + local velocityOffset = abs(v1) + + local complete = velocityOffset < velLimit and positionOffset < posLimit + + if complete then + p1 = self.__goalPosition + v1 = 0 + end + + return { + value = p1, + velocity = v1, + complete = complete, + } +end + +local function spring(goalPosition, inputOptions) + assert(typeof(goalPosition) == "number") + + local options = { + dampingRatio = 1, + frequency = 1, + restingVelocityLimit = DEFAULT_RESTING_VELOCITY_LIMIT, + restingPositionLimit = DEFAULT_RESTING_POSITION_LIMIT, + } + + if inputOptions ~= nil then + assert(typeof(inputOptions) == "table") + assign(options, inputOptions) + end + + local dampingRatio = options.dampingRatio + local frequency = options.frequency + local restingVelocityLimit = options.restingVelocityLimit + local restingPositionLimit = options.restingPositionLimit + + assert(typeof(dampingRatio) == "number") + assert(typeof(frequency) == "number") + assert(typeof(restingVelocityLimit) == "number") + assert(typeof(restingPositionLimit) == "number") + + assert(restingVelocityLimit >= 0, "Expected restingVelocityLimit >= 0") + assert(restingPositionLimit >= 0, "Expected restingPositionLimit >= 0") + + local self = { + __dampingRatio = dampingRatio, + __frequency = frequency, -- Hz + __restingVelocityLimit = restingVelocityLimit, + __restingPositionLimit = restingPositionLimit, + __goalPosition = goalPosition, + step = step, + } + + return self +end + +return spring diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/spring.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/spring.spec.lua new file mode 100644 index 0000000..343f20e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/spring.spec.lua @@ -0,0 +1,206 @@ +return function() + local spring = require(script.Parent.spring) + + it("should have all expected APIs", function() + expect(spring).to.be.a("function") + + local s = spring(1, { + dampingRatio = 0.1, + frequency = 10, + restingVelocityLimit = 0.1, + restingPositionLimit = 0.01, + }) + + expect(s).to.be.a("table") + expect(s.step).to.be.a("function") + + -- handle when spring lacks option table + s = spring(1) + + expect(s).to.be.a("table") + expect(s.step).to.be.a("function") + end) + + it("should handle being still correctly", function() + local s = spring(1, { + dampingRatio = 0.1, + frequency = 10, + restingVelocityLimit = 0.1, + restingPositionLimit = 0.01, + }) + + local state = s:step({ + value = 1, + velocity = 0, + complete = false, + }, 1) + + expect(state.value).to.equal(1) + expect(state.velocity).to.equal(0) + expect(state.complete).to.equal(true) + end) + + it("should return not complete when in motion", function() + local goal = spring(100, { + dampingRatio = 0.1, + frequency = 10, + }) + + local state = { + value = 1, + velocity = 0, + complete = false, + } + + state = goal:step(state, 1e-3) + + expect(state.value < 100).to.equal(true) + expect(state.velocity > 0).to.equal(true) + expect(state.complete).to.equal(false) + end) + + describe("should eventaully complete when", function() + it("is critically damped", function() + local s = spring(3, { + dampingRatio = 1, + frequency = 0.5, + }) + + local state = { + value = 1, + velocity = 0, + complete = false, + } + + while not state.complete do + state = s:step(state, 0.5) + end + + expect(state.complete).to.equal(true) + expect(state.value).to.equal(3) + expect(state.velocity).to.equal(0) + end) + + it("is over damped", function() + local s = spring(3, { + dampingRatio = 10, + frequency = 0.5, + }) + + local state = { + value = 1, + velocity = 0, + complete = false, + } + + while not state.complete do + state = s:step(state, 0.5) + end + + expect(state.complete).to.equal(true) + expect(state.value).to.equal(3) + expect(state.velocity).to.equal(0) + end) + + it("is under damped", function() + local s = spring(3, { + dampingRatio = 0.1, + frequency = 0.5, + }) + + local state = { + value = 1, + velocity = 0, + complete = false, + } + + while not state.complete do + state = s:step(state, 0.5) + end + + expect(state.complete).to.equal(true) + expect(state.value).to.equal(3) + expect(state.velocity).to.equal(0) + end) + end) + + describe("should handle infinite time deltas when", function() + -- TODO: This test is broken. + itSKIP("is critically damped", function() + local s = spring(20, { + dampingRatio = 1, + frequency = 1, + }) + + local state = { + value = -10, + velocity = 0, + complete = false, + } + state = s:step(state, math.huge) + + expect(state.complete).to.equal(true) + expect(state.value).to.equal(20) + expect(state.velocity).to.equal(0) + end) + + -- TODO: This test is broken. + itSKIP("is underdamped", function() + local s = spring(20, { + dampingRatio = 0.5, + frequency = 1, + }) + + local state = { + value = -10, + velocity = 0, + complete = false, + } + state = s:step(state, math.huge) + + expect(state.complete).to.equal(true) + expect(state.value).to.equal(20) + expect(state.velocity).to.equal(0) + end) + + it("is overdamped", function() + local s = spring(20, { + dampingRatio = 2, + frequency = 1, + }) + + local state = { + value = -10, + velocity = 0, + complete = false, + } + state = s:step(state, math.huge) + + expect(state.complete).to.equal(true) + expect(state.value).to.equal(20) + expect(state.velocity).to.equal(0) + end) + end) + + it("should remain complete when completed", function() + local s = spring(3, { + dampingRatio = 1, + frequency = 0.5, + }) + + local state = { + value = 1, + velocity = 0, + complete = false, + } + + while not state.complete do + state = s:step(state, 0.5) + end + state = s:step(state, 0.5) + + expect(state.complete).to.equal(true) + expect(state.value).to.equal(3) + expect(state.velocity).to.equal(0) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/validateMotor.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/validateMotor.lua new file mode 100644 index 0000000..3741759 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/validateMotor.lua @@ -0,0 +1,12 @@ +local function validateMotor(motor) + assert(typeof(motor) == "table") + assert(typeof(motor.start) == "function") + assert(typeof(motor.stop) == "function") + assert(typeof(motor.step) == "function") + assert(typeof(motor.setGoal) == "function") + assert(typeof(motor.onStep) == "function") + assert(typeof(motor.onComplete) == "function") + assert(typeof(motor.destroy) == "function") +end + +return validateMotor \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/FitFrame.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/FitFrame.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/FitFrame.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Otter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Roact.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/RoactRodux.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/RoactRodux.lua new file mode 100644 index 0000000..1b8d1c2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/RoactRodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-rodux"]["roact-rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Rodux.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Rodux.lua new file mode 100644 index 0000000..96b67df --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Rodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_rodux"]["rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/UIBlox.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/UIBlox.lua new file mode 100644 index 0000000..2e0dcc2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/UIBlox.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["UIBlox"]["UIBlox"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/lock.toml new file mode 100644 index 0000000..2a2969a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/lock.toml @@ -0,0 +1,15 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/purchase-prompt" +version = "0.1.6" +commit = "ad4ca756b1cee3e65122ca3459e2ae38668fcd54" +source = "git+https://github.com/roblox/purchasepromptscript-roact#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "RoactRodux roblox/roact-rodux 0.2.2 url+https://github.com/roblox/roact-rodux", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "UIBlox UIBlox a253d523 git+https://github.com/roblox/uiblox#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/AccountInfoReceived.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/AccountInfoReceived.lua new file mode 100644 index 0000000..31ba693 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/AccountInfoReceived.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "accountInfo") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/BundleProductInfoReceived.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/BundleProductInfoReceived.lua new file mode 100644 index 0000000..ea54e9e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/BundleProductInfoReceived.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "bundleProductInfo") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/CompleteRequest.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/CompleteRequest.lua new file mode 100644 index 0000000..8540ef9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/CompleteRequest.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/ErrorOccurred.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/ErrorOccurred.lua new file mode 100644 index 0000000..40d3358 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/ErrorOccurred.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "purchaseError") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PremiumInfoRecieved.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PremiumInfoRecieved.lua new file mode 100644 index 0000000..a0f81b3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PremiumInfoRecieved.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "premiumInfo") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/ProductInfoReceived.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/ProductInfoReceived.lua new file mode 100644 index 0000000..40b4778 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/ProductInfoReceived.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "productInfo") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PromptNativeUpsell.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PromptNativeUpsell.lua new file mode 100644 index 0000000..836f9a5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PromptNativeUpsell.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "robuxProductId", "robuxPurchaseAmount") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PurchaseCompleteRecieved.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PurchaseCompleteRecieved.lua new file mode 100644 index 0000000..8540ef9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PurchaseCompleteRecieved.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestAssetPurchase.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestAssetPurchase.lua new file mode 100644 index 0000000..f66e2dd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestAssetPurchase.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "id", "equipIfPurchased", "isRobloxPurchase") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestBundlePurchase.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestBundlePurchase.lua new file mode 100644 index 0000000..1c92d3b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestBundlePurchase.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "id") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestGamepassPurchase.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestGamepassPurchase.lua new file mode 100644 index 0000000..1c92d3b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestGamepassPurchase.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "id") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestPremiumPurchase.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestPremiumPurchase.lua new file mode 100644 index 0000000..8540ef9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestPremiumPurchase.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestProductPurchase.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestProductPurchase.lua new file mode 100644 index 0000000..8d8e28c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestProductPurchase.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "id", "equipIfPurchased") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestSubscriptionPurchase.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestSubscriptionPurchase.lua new file mode 100644 index 0000000..1c92d3b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestSubscriptionPurchase.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "id") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetABVariation.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetABVariation.lua new file mode 100644 index 0000000..47a39f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetABVariation.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "key", "variation") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetGamepadEnabled.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetGamepadEnabled.lua new file mode 100644 index 0000000..49f1164 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetGamepadEnabled.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "enabled") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetProduct.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetProduct.lua new file mode 100644 index 0000000..ba760fb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetProduct.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "id", "infoType", "equipIfPurchased") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetPromptState.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetPromptState.lua new file mode 100644 index 0000000..a106faf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetPromptState.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "promptState") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetWindowState.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetWindowState.lua new file mode 100644 index 0000000..cbd05ab --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetWindowState.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "state") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/StartHidingPrompt.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/StartHidingPrompt.lua new file mode 100644 index 0000000..8540ef9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/StartHidingPrompt.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/StartPurchase.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/StartPurchase.lua new file mode 100644 index 0000000..44cb373 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/StartPurchase.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "purchasingStartTime") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/makeActionCreator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/makeActionCreator.lua new file mode 100644 index 0000000..d67c577 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/makeActionCreator.lua @@ -0,0 +1,42 @@ +--[[ + A helper function to define a named Rodux action creator. + + Takes a name followed by a list of fields that should be + provided to the resulting action creator. + + Returns an object with a name field that can also be called + to create an action. When called, it will validate its given + arguments against the expected set of arguments. +]] +local function makeActionCreator(name, ...) + local fields = {...} + + assert(type(name) == "string", + "Bad argument #1 to makeActionCreator, expected string") + + for i = 1, select("#", ...) do + assert(typeof(select(i, ...)) == "string", + "Bad argument to makeActionCreator, all arguments must be of type string") + end + + return setmetatable({ + name = name + }, { + __call = function(self, ...) + local result = { + type = name, + } + + assert(select("#", ...) == #fields, + "Incorrect number of arguments provided to action creator " .. name) + + for index, argName in ipairs(fields) do + result[argName] = select(index, ...) + end + + return result + end + }) +end + +return makeActionCreator \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/makeActionCreator.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/makeActionCreator.spec.lua new file mode 100644 index 0000000..e739fd0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/makeActionCreator.spec.lua @@ -0,0 +1,47 @@ +return function() + local makeActionCreator = require(script.Parent.makeActionCreator) + + describe("the generated action creator", function() + it("should throw if given an invalid arguments", function() + expect(function() + makeActionCreator(100) + end).to.throw() + expect(function() + makeActionCreator("Test", 12) + end).to.throw() + end) + + it("should generate a callable table with a 'name' field", function() + local actionCreator = makeActionCreator("Action") + + expect(type(actionCreator)).to.equal("table") + expect(actionCreator.name).to.equal("Action") + expect(actionCreator).never.to.throw() + end) + end) + + describe("the action object generated by the action creator", function() + it("should expect a matching number of inputs", function() + local actionWithFields = makeActionCreator("AddItem", "id", "title") + + expect(function() + actionWithFields(10, "Apple", "extra arg") + end).to.throw() + expect(function() + actionWithFields(10) + end).to.throw() + + expect(function() + actionWithFields(10, "Orange") + end).never.to.throw() + end) + + it("should correctly count trailing nils", function() + local actionWithFields = makeActionCreator("SetProperty", "value", "default") + + expect(function() + actionWithFields("1", nil) + end).never.to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/BrowserPurchaseFinishedConnector.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/BrowserPurchaseFinishedConnector.lua new file mode 100644 index 0000000..89e6eed --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/BrowserPurchaseFinishedConnector.lua @@ -0,0 +1,42 @@ +--[[ + Connects to GuiService's browser close callback to retry purchase after upsell +]] +local Root = script.Parent.Parent.Parent +local GuiService = game:GetService("GuiService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local retryAfterUpsell = require(Root.Thunks.retryAfterUpsell) +local connectToStore = require(Root.connectToStore) + +local ExternalEventConnection = require(script.Parent.ExternalEventConnection) + +local function BrowserPurchaseFinishedConnector(props) + local onBrowserWindowClosed = props.onBrowserWindowClosed + + --[[ + CLILUACORE-309: The browser window closing is the ONLY + indication we have about when the user finished interacting + with the upsell flow on desktop. + ]] + return Roact.createElement(ExternalEventConnection, { + event = GuiService.BrowserWindowClosed, + callback = onBrowserWindowClosed, + }) +end + +local function mapDispatchToProps(dispatch) + return { + onBrowserWindowClosed = function() + dispatch(retryAfterUpsell()) + end, + } +end + +BrowserPurchaseFinishedConnector = connectToStore( + nil, + mapDispatchToProps +)(BrowserPurchaseFinishedConnector) + +return BrowserPurchaseFinishedConnector \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/EventConnections.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/EventConnections.lua new file mode 100644 index 0000000..6746ea8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/EventConnections.lua @@ -0,0 +1,38 @@ +--[[ + Connects relevant Roblox engine events to the rodux store +]] +local Root = script.Parent.Parent.Parent +local UserInputService = game:GetService("UserInputService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local UpsellFlow = require(Root.Enums.UpsellFlow) +local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow) + +local MarketplaceServiceEventConnector = require(script.Parent.MarketplaceServiceEventConnector) +local InputTypeManager = require(script.Parent.InputTypeManager) +local BrowserPurchaseFinishedConnector = require(script.Parent.BrowserPurchaseFinishedConnector) +local NativePurchaseFinishedConnector = require(script.Parent.NativePurchaseFinishedConnector) +local PlayerConnector = require(script.Parent.PlayerConnector) + +local function EventConnections() + local upsellConnector + local upsellFlow = getUpsellFlow(UserInputService:GetPlatform()) + if upsellFlow == UpsellFlow.Web then + upsellConnector = Roact.createElement(BrowserPurchaseFinishedConnector) + elseif upsellFlow == UpsellFlow.Mobile then + upsellConnector = Roact.createElement(NativePurchaseFinishedConnector) + end + + local enableInputManager = UserInputService:GetPlatform() ~= Enum.Platform.XBoxOne + + return Roact.createElement("Folder", {}, { + MarketPlaceServiceEventConnector = Roact.createElement(MarketplaceServiceEventConnector), + InputTypeManager = enableInputManager and Roact.createElement(InputTypeManager) or nil, + UpsellFinishedConnector = upsellConnector, + PlayerConnector = Roact.createElement(PlayerConnector), + }) +end + +return EventConnections \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/ExternalEventConnection.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/ExternalEventConnection.lua new file mode 100644 index 0000000..1888c57 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/ExternalEventConnection.lua @@ -0,0 +1,53 @@ +--[[ + A component that establishes a connection to a Roblox event when it is rendered. +]] +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local ExternalEventConnection = Roact.Component:extend("ExternalEventConnection") + +function ExternalEventConnection:init() + self.connection = nil +end + +--[[ + Render the child component so that ExternalEventConnections can be nested like so: + + Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputBegan, + callback = inputBeganCallback, + }, { + Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputEnded, + callback = inputChangedCallback, + }) + }) +]] +function ExternalEventConnection:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +function ExternalEventConnection:didMount() + local event = self.props.event + local callback = self.props.callback + + self.connection = event:Connect(callback) +end + +function ExternalEventConnection:didUpdate(oldProps) + if self.props.event ~= oldProps.event or self.props.callback ~= oldProps.callback then + self.connection:Disconnect() + + self.connection = self.props.event:Connect(self.props.callback) + end +end + +function ExternalEventConnection:willUnmount() + self.connection:Disconnect() + + self.connection = nil +end + +return ExternalEventConnection \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/InputTypeManager.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/InputTypeManager.lua new file mode 100644 index 0000000..ca410f1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/InputTypeManager.lua @@ -0,0 +1,105 @@ +--[[ + Sets whether or not gamepad buttons should be shown, based on recently + received inputs +]] +local Root = script.Parent.Parent.Parent +local CorePackages = game:GetService("CorePackages") +local UserInputService = game:GetService("UserInputService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local MouseIconOverrideService = require(CorePackages.InGameServices.MouseIconOverrideService) + +local SetGamepadEnabled = require(Root.Actions.SetGamepadEnabled) +local PromptState = require(Root.Enums.PromptState) +local connectToStore = require(Root.connectToStore) + +local ExternalEventConnection = require(script.Parent.ExternalEventConnection) + +local CURSOR_OVERRIDE_KEY = "PurchasePromptOverrideKey" + +local gamepadInputs = { + [Enum.UserInputType.Gamepad1] = true, + [Enum.UserInputType.Gamepad2] = true, + [Enum.UserInputType.Gamepad3] = true, + [Enum.UserInputType.Gamepad4] = true, +} + +local InputTypeManager = Roact.Component:extend("InputTypeManager") + +function InputTypeManager:init() + local setGamepadEnabled = self.props.setGamepadEnabled + + self.dispatchOnChange = function(lastInputType) + local newEnabledStatus + if gamepadInputs[lastInputType] then + newEnabledStatus = true + else + newEnabledStatus = false + end + + setGamepadEnabled(newEnabledStatus) + end + + self.cursorOverridden = false +end + +function InputTypeManager:render() + return Roact.createElement(ExternalEventConnection, { + event = UserInputService.LastInputTypeChanged, + callback = self.dispatchOnChange, + }) +end + +function InputTypeManager:didUpdate(prevProps, prevState) + local didShow = prevProps.promptState == PromptState.None + and self.props.promptState ~= PromptState.None + local didHide = prevProps.promptState ~= PromptState.None + and self.props.promptState == PromptState.None + + local isShown = self.props.promptState ~= PromptState.None + + local overrideStatus = self.props.gamepadEnabled + and Enum.OverrideMouseIconBehavior.ForceHide + or Enum.OverrideMouseIconBehavior.ForceShow + + -- If we're already showing the prompt and the gamepad status changed + if isShown and prevProps.gamepadEnabled ~= self.props.gamepadEnabled then + if self.cursorOverridden then + MouseIconOverrideService.pop(CURSOR_OVERRIDE_KEY) + end + MouseIconOverrideService.push(CURSOR_OVERRIDE_KEY, overrideStatus) + self.cursorOverridden = true + -- If the purchase prompt goes from None to shown + elseif didShow then + MouseIconOverrideService.push(CURSOR_OVERRIDE_KEY, overrideStatus) + self.cursorOverridden = true + -- If the purchase prompt goes from shown to None + elseif didHide then + MouseIconOverrideService.pop(CURSOR_OVERRIDE_KEY) + self.cursorOverridden = false + end +end + +local function mapStateToProps(state) + return { + promptState = state.promptState, + gamepadEnabled = state.gamepadEnabled, + } +end + +local function mapDispatchToProps(dispatch) + return { + setGamepadEnabled = function(enabled) + dispatch(SetGamepadEnabled(enabled)) + end, + } +end + +InputTypeManager = connectToStore( + mapStateToProps, + mapDispatchToProps +)(InputTypeManager) + +return InputTypeManager \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LayoutValuesConsumer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LayoutValuesConsumer.lua new file mode 100644 index 0000000..dc65091 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LayoutValuesConsumer.lua @@ -0,0 +1,43 @@ +--[[ + LayoutValuesConsumer will extract the LayoutValues object + from context and pass it into the given render callback +]] +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local LayoutValuesKey = require(Root.Symbols.LayoutValuesKey) + +local LayoutValuesConsumer = Roact.Component:extend("LayoutValuesConsumer") + +-- TODO(esauer): Add validation when adding t +-- local validateProps = t.strictInterface({ +-- render = t.callback, +-- }) + +function LayoutValuesConsumer:init() + self.layoutValues = self._context[LayoutValuesKey] + self.state = { + layout = self.layoutValues.layout, + } +end + +function LayoutValuesConsumer:render() + -- assert(validateProps(self.props)) + return self.props.render(self.state.layout) +end + +function LayoutValuesConsumer:didMount() + self.disconnectLayoutListener = self.layoutValues.signal:subscribe(function(newLayout) + self:setState({ + layout = newLayout, + }) + end) +end + +function LayoutValuesConsumer:willUnmount() + self.disconnectLayoutListener() +end + +return LayoutValuesConsumer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LayoutValuesProvider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LayoutValuesProvider.lua new file mode 100644 index 0000000..c13064d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LayoutValuesProvider.lua @@ -0,0 +1,65 @@ +--[[ + LayoutValuesProvider is a simple wrapper component that injects the + specified services into context +]] +local Root = script.Parent.Parent.Parent + +local ContentProvider = game:GetService("ContentProvider") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local LayoutValues = require(Root.Services.LayoutValues) +local LayoutValuesKey = require(Root.Symbols.LayoutValuesKey) +local connectToStore = require(Root.connectToStore) + +local LayoutValuesProvider = Roact.Component:extend("LayoutValuesProvider") + +function LayoutValuesProvider:init(props) + assert(type(props.isTenFootInterface) == "boolean", "Expected required prop 'isTenFootInterface' to be a boolean") + assert(type(props.render) == "function", "Expected prop 'render' to be a function") + + self.layoutValues = LayoutValues.new(self.props.isTenFootInterface, false) + self._context[LayoutValuesKey] = self.layoutValues +end + +function LayoutValuesProvider:didMount() + -- preload images + spawn(function() + local assets = {} + + for _, image in pairs(self.layoutValues.layout.Image) do + local decal = Instance.new("Decal") + decal.Texture = image.Path + table.insert(assets, decal) + end + + ContentProvider:PreloadAsync(assets) + + for _,asset in pairs(assets) do + asset:Destroy() + end + end) +end + +function LayoutValuesProvider:render() + return self.props.render() +end + +function LayoutValuesProvider:didUpdate(previousProps) + if self.props.isTenFootInterface ~= previousProps.isTenFootInterface then + self.layoutValues:update(self.props.isTenFootInterface) + end +end + +local function mapStateToProps(state) + return { + abVariations = state.abVariations, + } +end + +LayoutValuesProvider = connectToStore( + mapStateToProps +)(LayoutValuesProvider) + +return LayoutValuesProvider \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LocalizationContextConsumer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LocalizationContextConsumer.lua new file mode 100644 index 0000000..c0f2b6a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LocalizationContextConsumer.lua @@ -0,0 +1,23 @@ +--[[ + LocalizationContextConsumer will extract the localization context + object from Roact context and provide it to the given render callback + + Used for components that need to perform localization using this + project's LocalizationService +]] +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local LocalizationContextKey = require(Root.Symbols.LocalizationContextKey) + +local LocalizationContextConsumer = Roact.Component:extend("LocalizationContextConsumer") + +function LocalizationContextConsumer:render() + local localizationContext = self._context[LocalizationContextKey] + + return self.props.render(localizationContext) +end + +return LocalizationContextConsumer diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LocalizationContextProvider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LocalizationContextProvider.lua new file mode 100644 index 0000000..5019ef0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LocalizationContextProvider.lua @@ -0,0 +1,25 @@ +--[[ + LocalizationContextProvider is a simple wrapper component that injects the + specified services into context +]] +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local LocalizationContextKey = require(Root.Symbols.LocalizationContextKey) + +local LocalizationContextProvider = Roact.Component:extend("LocalizationContextProvider") + +function LocalizationContextProvider:init(props) + assert(props.localizationContext, "Missing required prop 'localizationContext'") + assert(props.render, "Missing required prop 'render'") + + self._context[LocalizationContextKey] = props.localizationContext +end + +function LocalizationContextProvider:render() + return self.props.render(LocalizationContextKey) +end + +return LocalizationContextProvider \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/MarketplaceServiceEventConnector.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/MarketplaceServiceEventConnector.lua new file mode 100644 index 0000000..94e3147 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/MarketplaceServiceEventConnector.lua @@ -0,0 +1,140 @@ +--[[ + Connects Rodux store to external MarketplaceService events +]] +local Root = script.Parent.Parent.Parent +local MarketplaceService = game:GetService("MarketplaceService") +local Players = game:GetService("Players") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local PurchaseError = require(Root.Enums.PurchaseError) +local completePurchase = require(Root.Thunks.completePurchase) +local initiatePurchase = require(Root.Thunks.initiatePurchase) +local initiateBundlePurchase = require(Root.Thunks.initiateBundlePurchase) +local initiatePremiumPurchase = require(Root.Thunks.initiatePremiumPurchase) +local initiateSubscriptionPurchase = require(Root.Thunks.initiateSubscriptionPurchase) +local connectToStore = require(Root.connectToStore) + +local GetFFlagPromptRobloxPurchaseEnabled = require(Root.Flags.GetFFlagPromptRobloxPurchaseEnabled) +local GetFFlagDeveloperSubscriptionsEnabled = require(Root.Flags.GetFFlagDeveloperSubscriptionsEnabled) + +local ExternalEventConnection = require(script.Parent.ExternalEventConnection) + +local function MarketplaceServiceEventConnector(props) + local onPurchaseRequest = props.onPurchaseRequest + local onProductPurchaseRequest = props.onProductPurchaseRequest + local onPurchaseGamePassRequest = props.onPurchaseGamePassRequest + local onServerPurchaseVerification = props.onServerPurchaseVerification + local onBundlePurchaseRequest = props.onBundlePurchaseRequest + local onPremiumPurchaseRequest = props.onPremiumPurchaseRequest + local onRobloxPurchaseRequest = props.onRobloxPurchaseRequest + local onSubscriptionPurchaseRequest = props.onSubscriptionPurchaseRequest + + return Roact.createFragment({ + Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.PromptPurchaseRequested, + callback = onPurchaseRequest, + }), + RobloxPurchase = GetFFlagPromptRobloxPurchaseEnabled() and Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.PromptRobloxPurchaseRequested, + callback = onRobloxPurchaseRequest, + }), + Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.PromptProductPurchaseRequested, + callback = onProductPurchaseRequest, + }), + Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.PromptGamePassPurchaseRequested, + callback = onPurchaseGamePassRequest, + }), + Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.ServerPurchaseVerification, + callback = onServerPurchaseVerification, + }), + Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.PromptBundlePurchaseRequested, + callback = onBundlePurchaseRequest, + }), + Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.PromptPremiumPurchaseRequested, + callback = onPremiumPurchaseRequest, + }), + SubscriptionsPurchase = GetFFlagDeveloperSubscriptionsEnabled() and Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.PromptSubscriptionPurchaseRequested, + callback = onSubscriptionPurchaseRequest, + }) + }) +end + +MarketplaceServiceEventConnector = connectToStore(nil, +function(dispatch) + local function onPurchaseRequest(player, assetId, equipIfPurchased, currencyType) + if player == Players.LocalPlayer then + dispatch(initiatePurchase(assetId, Enum.InfoType.Asset, equipIfPurchased, false)) + end + end + + local function onRobloxPurchaseRequest(assetId, equipIfPurchased) + dispatch(initiatePurchase(assetId, Enum.InfoType.Asset, equipIfPurchased, true)) + end + + local function onProductPurchaseRequest(player, productId, equipIfPurchased, currencyType) + if player == Players.LocalPlayer then + dispatch(initiatePurchase(productId, Enum.InfoType.Product, equipIfPurchased)) + end + end + + local function onPurchaseGamePassRequest(player, gamePassId) + if player == Players.LocalPlayer then + dispatch(initiatePurchase(gamePassId, Enum.InfoType.GamePass, false)) + end + end + + -- Specific to purchasing dev products + local function onServerPurchaseVerification(serverResponseTable) + if not serverResponseTable then + dispatch(ErrorOccurred(PurchaseError.UnknownFailure)) + else + local playerId = serverResponseTable["playerId"] + if playerId ~= nil then + playerId = tonumber(serverResponseTable["playerId"]) + end + if playerId == Players.LocalPlayer.UserId then + dispatch(completePurchase()) + end + end + end + + local function onBundlePurchaseRequest(player, bundleId) + if player == Players.LocalPlayer then + dispatch(initiateBundlePurchase(bundleId)) + end + end + + local function onPremiumPurchaseRequest(player) + if player == Players.LocalPlayer then + dispatch(initiatePremiumPurchase()) + end + end + + local function onSubscriptionPurchaseRequest(player, subscriptionId) + if player == Players.LocalPlayer then + dispatch(initiateSubscriptionPurchase(subscriptionId)) + end + end + + return { + onPurchaseRequest = onPurchaseRequest, + onRobloxPurchaseRequest = onRobloxPurchaseRequest, + onProductPurchaseRequest = onProductPurchaseRequest, + onPurchaseGamePassRequest = onPurchaseGamePassRequest, + onServerPurchaseVerification = onServerPurchaseVerification, + onBundlePurchaseRequest = onBundlePurchaseRequest, + onPremiumPurchaseRequest = onPremiumPurchaseRequest, + onSubscriptionPurchaseRequest = onSubscriptionPurchaseRequest, + } +end)(MarketplaceServiceEventConnector) + +return MarketplaceServiceEventConnector diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/MultiTextLocalizer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/MultiTextLocalizer.lua new file mode 100644 index 0000000..2c629ac --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/MultiTextLocalizer.lua @@ -0,0 +1,40 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) +local t = require(LuaPackages.t) + +local LocalizationService = require(Root.Localization.LocalizationService) + +local LocalizationContextConsumer = require(script.Parent.LocalizationContextConsumer) + +local validateProps = t.strictInterface({ + keys = t.table, + render = t.callback, +}) + +local validateItem = t.strictInterface({ + key = t.string, + params = t.optional(t.table), +}) + +local function MultiTextLocalizer(props) + assert(validateProps(props)) + for _, item in pairs(props.keys) do + assert(validateItem(item)) + end + + local render = props.render + + return Roact.createElement(LocalizationContextConsumer, { + render = function(localizationContext) + local textMap = {} + for key, item in pairs(props.keys) do + textMap[key] = LocalizationService.getString(localizationContext, item.key, item.params) + end + return render(textMap) + end, + }) +end + +return MultiTextLocalizer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/NativePurchaseFinishedConnector.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/NativePurchaseFinishedConnector.lua new file mode 100644 index 0000000..57eb31e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/NativePurchaseFinishedConnector.lua @@ -0,0 +1,45 @@ +--[[ + Connects to MarketplaceService's callback for completing a native purchase, so that we can + retry after an upsell purchase was processed +]] +local Root = script.Parent.Parent.Parent + +local MarketplaceService = game:GetService("MarketplaceService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local PurchaseError = require(Root.Enums.PurchaseError) +local retryAfterUpsell = require(Root.Thunks.retryAfterUpsell) +local connectToStore = require(Root.connectToStore) + +local ExternalEventConnection = require(script.Parent.ExternalEventConnection) + +local function NativePurchaseFinishedConnector(props) + local nativePurchaseFinished = props.nativePurchaseFinished + + return Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.NativePurchaseFinished, + callback = nativePurchaseFinished, + }) +end + +local function mapDispatchToProps(dispatch) + return { + nativePurchaseFinished = function(player, productId, wasPurchased) + if wasPurchased then + dispatch(retryAfterUpsell()) + else + dispatch(ErrorOccurred(PurchaseError.InvalidFunds)) + end + end, + } +end + +NativePurchaseFinishedConnector = connectToStore( + nil, + mapDispatchToProps +)(NativePurchaseFinishedConnector) + +return NativePurchaseFinishedConnector \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/NumberLocalizer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/NumberLocalizer.lua new file mode 100644 index 0000000..2c93ac9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/NumberLocalizer.lua @@ -0,0 +1,24 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local LocalizationService = require(Root.Localization.LocalizationService) + +local LocalizationContextConsumer = require(script.Parent.LocalizationContextConsumer) + +local function NumberLocalizer(props) + local number = props.number + local render = props.render + + assert(typeof(number) == "number", "prop 'number' must be provided") + assert(typeof(render) == "function", "Render prop must be a function") + + return Roact.createElement(LocalizationContextConsumer, { + render = function(localizationContext) + return render(LocalizationService.formatNumber(localizationContext, number)) + end, + }) +end + +return NumberLocalizer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/PlayerConnector.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/PlayerConnector.lua new file mode 100644 index 0000000..47fe83c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/PlayerConnector.lua @@ -0,0 +1,41 @@ +--[[ + Connects to MarketplaceService's callback for completing a native purchase, so that we can + retry after an upsell purchase was processed +]] +local Root = script.Parent.Parent.Parent + +local Players = game:GetService("Players") +local ABTestService = game:GetService("ABTestService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local connectToStore = require(Root.connectToStore) + +local ExternalEventConnection = require(script.Parent.ExternalEventConnection) + +local function PlayerConnector(props) + local playerConnects = props.playerConnects + + return Roact.createElement(ExternalEventConnection, { + event = Players.PlayerAdded, + callback = playerConnects, + }) +end + +local function mapDispatchToProps(dispatch) + return { + playerConnects = function(player) + if player == Players.LocalPlayer then + ABTestService:InitializeForUserId(Players.LocalPlayer.UserId) + end + end, + } +end + +PlayerConnector = connectToStore( + nil, + mapDispatchToProps +)(PlayerConnector) + +return PlayerConnector \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/TextLocalizer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/TextLocalizer.lua new file mode 100644 index 0000000..9d223f9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/TextLocalizer.lua @@ -0,0 +1,25 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local LocalizationService = require(Root.Localization.LocalizationService) + +local LocalizationContextConsumer = require(script.Parent.LocalizationContextConsumer) + +local function TextLocalizer(props) + local key = props.key + local params = props.params + local render = props.render + + assert(typeof(key) == "string", "String key must be provided") + assert(typeof(render) == "function", "Render prop must be a function") + + return Roact.createElement(LocalizationContextConsumer, { + render = function(localizationContext) + return render(LocalizationService.getString(localizationContext, key, params)) + end, + }) +end + +return TextLocalizer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/provideRobloxLocale.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/provideRobloxLocale.lua new file mode 100644 index 0000000..4ef7847 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/provideRobloxLocale.lua @@ -0,0 +1,23 @@ +--[[ + Helper for supplying the current Roblox Locale to the Roact + tree via context using the LocalizationContextProvider +]] +local Root = script.Parent.Parent.Parent + +local LocalizationService = game:GetService("LocalizationService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local getLocalizationContext = require(Root.Localization.getLocalizationContext) + +local LocalizationContextProvider = require(script.Parent.LocalizationContextProvider) + +local function provideRobloxLocale(renderFunc) + return Roact.createElement(LocalizationContextProvider, { + localizationContext = getLocalizationContext(LocalizationService.RobloxLocaleId), + render = renderFunc + }) +end + +return provideRobloxLocale \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/withLayoutValues.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/withLayoutValues.lua new file mode 100644 index 0000000..e1576fe --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/withLayoutValues.lua @@ -0,0 +1,18 @@ +--[[ + Helpful wrapper around LayoutValuesConsumer to make it a + little less verbose to use +]] +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local LayoutValuesConsumer = require(script.Parent.LayoutValuesConsumer) + +local function withLayoutValues(renderFunc) + return Roact.createElement(LayoutValuesConsumer, { + render = renderFunc, + }) +end + +return withLayoutValues \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/AutoSizedText.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/AutoSizedText.lua new file mode 100644 index 0000000..90a9537 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/AutoSizedText.lua @@ -0,0 +1,52 @@ +local Root = script.Parent.Parent.Parent +local TextService = game:GetService("TextService") + +local LuaPackages = Root.Parent +local UIBlox = require(LuaPackages.UIBlox) +local Roact = require(LuaPackages.Roact) +local t = require(LuaPackages.t) + +local withStyle = UIBlox.Style.withStyle + +local AutoSizedText = Roact.PureComponent:extend("AutoSizedText") + +local validateProps = t.strictInterface({ + text = t.string, + width = t.number, + layoutOrder = t.number, +}) + +function AutoSizedText:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local fonts = stylePalette.Font + + local text = self.props.text + local textSize = fonts.Body.RelativeSize * fonts.BaseSize + local font = fonts.Body.Font + + local totalTextSize + if text ~= nil then + totalTextSize = TextService:GetTextSize(text, textSize, font, Vector2.new(self.props.width, 10000)) + else + totalTextSize = Vector2.new(0, 0) + end + + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, totalTextSize.Y), + BackgroundTransparency = 1, + Text = text, + TextSize = textSize, + TextColor3 = theme.TextDefault.Color, + TextTransparency = theme.TextDefault.Transparency, + Font = font, + TextXAlignment = Enum.TextXAlignment.Left, + TextWrapped = true, + LayoutOrder = self.props.layoutOrder, + }) + end) +end + +return AutoSizedText \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/AutoSizedText.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/AutoSizedText.spec.lua new file mode 100644 index 0000000..cb85320 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/AutoSizedText.spec.lua @@ -0,0 +1,23 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local AutoSizedText = require(script.Parent.AutoSizedText) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(AutoSizedText, { + text = "Test", + width = 0, + layoutOrder = 0, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/BulletPoint.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/BulletPoint.lua new file mode 100644 index 0000000..0d945b0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/BulletPoint.lua @@ -0,0 +1,73 @@ +local Root = script.Parent.Parent.Parent +local TextService = game:GetService("TextService") + +local LuaPackages = Root.Parent +local UIBlox = require(LuaPackages.UIBlox) +local Roact = require(LuaPackages.Roact) +local t = require(LuaPackages.t) +local Cryo = require(LuaPackages.Cryo) + +local Images = UIBlox.App.ImageSet.Images +local withStyle = UIBlox.Style.withStyle +local IconSize = UIBlox.App.Constant.IconSize + +local CHECK_ICON = "icons/status/success_small" +local TEXT_LEFT_PADDING = IconSize.Small + 16 + +local BulletPoint = Roact.PureComponent:extend("BulletPoint") + +local validateProps = t.strictInterface({ + text = t.string, + width = t.number, + layoutOrder = t.number, +}) + +function BulletPoint:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local fonts = stylePalette.Font + + local text = self.props.text + local textSize = fonts.Body.RelativeSize * fonts.BaseSize + local font = fonts.Body.Font + + local totalTextSize + if text ~= nil then + totalTextSize = TextService:GetTextSize(text, textSize, font, + Vector2.new(self.props.width - TEXT_LEFT_PADDING, 10000)) + else + totalTextSize = Vector2.new(0, 0) + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, totalTextSize.Y), + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + } , { + Roact.createElement("ImageLabel", Cryo.Dictionary.join(Images[CHECK_ICON], { + Position = UDim2.new(0, 2, 0, 2), + Size = UDim2.new(0, IconSize.Small, 0, IconSize.Small), + ImageColor3 = theme.IconDefault.Color, + ImageTransparency = theme.IconDefault.Transparency, + BackgroundTransparency = 1, + })), + Roact.createElement("TextLabel", { + Position = UDim2.new(0, TEXT_LEFT_PADDING, 0, 0), + Size = UDim2.new(1, -TEXT_LEFT_PADDING, 0, totalTextSize.Y), + BackgroundTransparency = 1, + Text = text, + TextSize = textSize, + TextColor3 = theme.TextDefault.Color, + TextTransparency = theme.TextDefault.Transparency, + Font = font, + TextXAlignment = Enum.TextXAlignment.Left, + TextWrapped = true, + LayoutOrder = self.props.layoutOrder, + }) + }) + end) +end + +return BulletPoint \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/BulletPoint.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/BulletPoint.spec.lua new file mode 100644 index 0000000..16e012a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/BulletPoint.spec.lua @@ -0,0 +1,23 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local BulletPoint = require(script.Parent.BulletPoint) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(BulletPoint, { + text = "Test", + width = 0, + layoutOrder = 0, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumModal.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumModal.lua new file mode 100644 index 0000000..0e68a0b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumModal.lua @@ -0,0 +1,209 @@ +local Root = script.Parent.Parent.Parent +local LuaPackages = Root.Parent + +local Roact = require(LuaPackages.Roact) +local Cryo = require(LuaPackages.Cryo) +local FitFrame = require(LuaPackages.FitFrame) +local FitFrameVertical = FitFrame.FitFrameVertical +local UIBlox = require(LuaPackages.UIBlox) +local PartialPageModal = UIBlox.App.Dialog.Modal.PartialPageModal +local ButtonType = UIBlox.App.Button.Enum.ButtonType +local Images = UIBlox.App.ImageSet.Images + +local PromptState = require(Root.Enums.PromptState) +local launchPremiumUpsell = require(Root.Thunks.launchPremiumUpsell) +local hideWindow = require(Root.Thunks.hideWindow) +local connectToStore = require(Root.connectToStore) + +local MultiTextLocalizer = require(script.Parent.Parent.Connection.MultiTextLocalizer) + +local AutoSizedText = require(script.Parent.AutoSizedText) +local BulletPoint = require(script.Parent.BulletPoint) + +local PREMIUM_ICON = "icons/graphic/premium_large" + +local PremiumModal = Roact.Component:extend(script.Name) + +local PREMIUM_MODAL_LOC_KEY = "CoreScripts.PremiumModal.%s" + +local CONTENT_PADDING = 24 +local CONDENSED_CONTENT_PADDING = 12 +local ICON_SIZE = 80 +local CONDENSED_ICON_SIZE = 48 + +-- TODO(esauer): +-- Make the 120 calculation come directly from UIBlox Modal +-- self.updateContentSizes should automatically know how to make the calculation + +function PremiumModal:init() + self.isCondensed = false + -- Hack :( The modal should tell you what the width is, this just prevents having to render + -- 540 (Max modal width) - 24 * 2 (Side paddings) + self.contentSize = Vector2.new(self.props.screenSize.X > 492 and 492 or self.props.screenSize.X, 0) + self.contentSizes, self.changeContentSizes = Roact.createBinding({ + padding = UDim.new(0, CONTENT_PADDING), + iconSize = UDim2.new(1, 0, 0, ICON_SIZE) + }) + + self.purchasePremium = function() + self.props.purchasePremium() + end + + self.updateContentSizes = function() + -- 120 is the height of the components of the modal not including the customized content + if self.isCondensed then + self.isCondensed = self.props.screenSize.Y < self.contentSize.Y + 120 + + ICON_SIZE - CONDENSED_ICON_SIZE + + (CONTENT_PADDING - CONDENSED_CONTENT_PADDING) * 2 + else + self.isCondensed = self.props.screenSize.Y <= self.contentSize.Y + 120 + end + + self.changeContentSizes({ + padding = UDim.new(0, self.isCondensed and CONDENSED_CONTENT_PADDING or CONTENT_PADDING), + iconSize = UDim2.new(1, 0, 0, self.isCondensed and CONDENSED_ICON_SIZE or ICON_SIZE) + }) + end +end + +function PremiumModal:didUpdate(prevProps) + if self.props.screenSize ~= prevProps.screenSize then + self.updateContentSizes() + end +end + +function PremiumModal:render() + local promptState = self.props.promptState + local premiumProductInfo = self.props.premiumProductInfo + local screenSize = self.props.screenSize + + return Roact.createElement(MultiTextLocalizer, { + keys = { + titleLocalizedText = { + key = PREMIUM_MODAL_LOC_KEY:format("Title.PremiumRequired"), + }, + monthlyLocalizedText = { + key = PREMIUM_MODAL_LOC_KEY:format("Action.PricePerMonth"), + params = { + price = premiumProductInfo.currencySymbol..tostring(premiumProductInfo.price) + }, + }, + descLocalizedText = { + key = PREMIUM_MODAL_LOC_KEY:format("Body.Description"), + }, + bulletPoint1Text = { + key = PREMIUM_MODAL_LOC_KEY:format("Body.RobuxMonthlyV2"), + params = { + robux = premiumProductInfo.robuxAmount + }, + }, + bulletPoint2Text = { + key = PREMIUM_MODAL_LOC_KEY:format("Body.PremiumOnlyAreas"), + }, + bulletPoint3Text = { + key = PREMIUM_MODAL_LOC_KEY:format("Body.RobuxDiscount"), + }, + }, + render = function(locTextMap) + return Roact.createElement(PartialPageModal, { + title = locTextMap.titleLocalizedText, + screenSize = screenSize, + buttonStackProps = { + buttons = { + { + buttonType = ButtonType.PrimarySystem, + props = { + isDisabled = promptState ~= PromptState.PremiumUpsell, + onActivated = self.purchasePremium, + text = locTextMap.monthlyLocalizedText, + }, + }, + }, + buttonHeight = 48, + }, + onCloseClicked = self.props.setHideWindow + }, { + Roact.createElement(FitFrameVertical, { + BackgroundTransparency = 1, + width = UDim.new(1, 0), + contentPadding = UDim.new(0, 24), + margin = { + top = 24, + bottom = 24, + }, + [Roact.Change.AbsoluteSize] = function(rbx) + self.contentSize = rbx.AbsoluteSize + self.updateContentSizes() + end + }, { + Icon = Roact.createElement("ImageLabel", Cryo.Dictionary.join(Images[PREMIUM_ICON], { + AnchorPoint = Vector2.new(0.5, 0.5), + Size = self.contentSizes:map(function(values) + return values.iconSize + end), + ScaleType = Enum.ScaleType.Fit, + BackgroundTransparency = 1, + LayoutOrder = 1, + })), + Roact.createElement(AutoSizedText, { + text = locTextMap.descLocalizedText, + width = self.contentSize.X, + layoutOrder = 2, + }), + Roact.createElement(FitFrameVertical, { + BackgroundTransparency = 1, + LayoutOrder = 3, + width = UDim.new(1, 0), + contentPadding = self.contentSizes:map(function(values) + return values.padding + end), + }, { + Bullet1 = Roact.createElement(BulletPoint, { + text = locTextMap.bulletPoint1Text, + width = self.contentSize.X, + layoutOrder = 1, + }), + Bullet2 = Roact.createElement(BulletPoint, { + text = locTextMap.bulletPoint2Text, + width = self.contentSize.X, + layoutOrder = 2, + }), + Bullet3 = Roact.createElement(BulletPoint, { + text = locTextMap.bulletPoint3Text, + width = self.contentSize.X, + layoutOrder = 3, + }) + }), + }) + }) + end + }) +end + +local function mapStateToProps(state) + return { + premiumProductInfo = state.premiumProductInfo, + promptState = state.promptState, + requestType = state.promptRequest.requestType, + purchaseError = state.purchaseError, + windowState = state.windowState, + } +end + +local function mapDispatchToProps(dispatch) + return { + purchasePremium = function() + dispatch(launchPremiumUpsell()) + end, + setHideWindow = function() + dispatch(hideWindow()) + end, + } +end + +PremiumModal = connectToStore( + mapStateToProps, + mapDispatchToProps +)(PremiumModal) + +return PremiumModal \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumModal.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumModal.spec.lua new file mode 100644 index 0000000..424baf6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumModal.spec.lua @@ -0,0 +1,43 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local RequestType = require(Root.Enums.RequestType) + local WindowState = require(Root.Enums.WindowState) + local Reducer = require(Root.Reducers.Reducer) + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local PremiumModal = require(script.Parent.PremiumModal) + PremiumModal = PremiumModal.getUnconnected() + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, { + overrideStore = Rodux.Store.new(Reducer) + }, { + Roact.createElement(PremiumModal, { + premiumProductInfo = { + premiumFeatureTypeName = "Subscription", + mobileProductId = "com.roblox.robloxmobile.RobloxPremium450", + description = "Roblox Premium 450", + price = 4.99, + robuxAmount = 450, + isSubscriptionOnly = false, + currencySymbol = "$", + }, + promptState = PromptState.PremiumUpsell, + promptRequest = { + requestType = RequestType.Premium + }, + windowState = WindowState.Hidden, + screenSize = Vector2.new(100, 100) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumPrompt.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumPrompt.lua new file mode 100644 index 0000000..5c03bf3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumPrompt.lua @@ -0,0 +1,206 @@ +local Root = script.Parent.Parent.Parent +local GuiService = game:GetService("GuiService") +local ContextActionService = game:GetService("ContextActionService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) +local Otter = require(LuaPackages.Otter) +local UIBlox = require(LuaPackages.UIBlox) +local InteractiveAlert = UIBlox.App.Dialog.Alert.InteractiveAlert +local ButtonType = UIBlox.App.Button.Enum.ButtonType + +local PurchaseError = require(Root.Enums.PurchaseError) +local PromptState = require(Root.Enums.PromptState) +local RequestType = require(Root.Enums.RequestType) +local WindowState = require(Root.Enums.WindowState) +local completeRequest = require(Root.Thunks.completeRequest) +local launchPremiumUpsell = require(Root.Thunks.launchPremiumUpsell) +local hideWindow = require(Root.Thunks.hideWindow) +local connectToStore = require(Root.connectToStore) + +local ExternalEventConnection = require(script.Parent.Parent.Connection.ExternalEventConnection) +local MultiTextLocalizer = require(script.Parent.Parent.Connection.MultiTextLocalizer) + +local PremiumModal = require(script.Parent.PremiumModal) + +local PREMIUM_MODAL_LOC_KEY = "CoreScripts.PremiumModal.%s" +local PREMIUM_BUTTON_BIND = "PremiumBackButton" + +local PremiumPrompt = Roact.Component:extend(script.Name) + +local SPRING_CONFIG = { + dampingRatio = 1, + frequency = 1.6, +} + +local function isRelevantRequestType(requestType) + return requestType == RequestType.Premium +end + +function PremiumPrompt:init() + self.state = { + screenSize = Vector2.new(0, 0), + } + + self.changeScreenSize = function(rbx) + if self.state.screenSize ~= rbx.AbsoluteSize then + self:setState({ + screenSize = rbx.AbsoluteSize, + }) + end + end + + local animationProgress, setProgress = Roact.createBinding(0) + + self.motor = Otter.createSingleMotor(0) + self.motor:start() + + self.motor:onStep(setProgress) + self.animationProgress = animationProgress + + self.motor:onComplete(function() + -- Do not complete the request if we are waiting for the browser or native purchase to complete + if self.props.windowState == WindowState.Hidden + and isRelevantRequestType(self.props.requestType) + and self.props.promptState ~= PromptState.UpsellInProgress then + self.props.setCompleteRequest() + end + end) + + self.onClose = function() + self.props.hideWindow() + end +end + +function PremiumPrompt:didUpdate(prevProps, prevState) + if prevProps.windowState ~= self.props.windowState then + local goal = (self.props.windowState == WindowState.Hidden or not isRelevantRequestType(self.props.requestType)) and 0 or 1 + self.motor:setGoal(Otter.spring(goal, SPRING_CONFIG)) + + if self.props.windowState == WindowState.Shown then + ContextActionService:BindCoreAction( + PREMIUM_BUTTON_BIND, + function(actionName, inputState, inputObj) + if inputState == Enum.UserInputState.Begin then + self.onClose() + end + end, false, Enum.KeyCode.ButtonB) + else + ContextActionService:UnbindCoreAction(PREMIUM_BUTTON_BIND) + end + end +end + +function PremiumPrompt:RenderError() + local purchaseError = self.props.purchaseError + + local errorKey + if purchaseError == PurchaseError.AlreadyPremium then + errorKey = "Error.AlreadyPremium" + elseif purchaseError == PurchaseError.PremiumUnavailablePlatform then + errorKey = "Error.PlatformUnavailable" + else + errorKey = "Error.Unavailable" + end + + return Roact.createElement(MultiTextLocalizer, { + keys = { + titleLocalizedText = { + key = PREMIUM_MODAL_LOC_KEY:format("Title.Error"), + }, + errorLocalizedText = { + key = PREMIUM_MODAL_LOC_KEY:format(errorKey), + }, + okLocalizedText = { + key = "CoreScripts.PurchasePrompt.Button.OK", + }, + }, + render = function(textMap) + return Roact.createElement(InteractiveAlert, { + bodyText = textMap.errorLocalizedText, + buttonStackInfo = { + buttons = { + { + buttonType = ButtonType.PrimarySystem, + props = { + onActivated = self.onClose, + text = textMap.okLocalizedText, + }, + }, + }, + }, + screenSize = self.state.screenSize, + title = textMap.titleLocalizedText, + }) + end + }) +end + +function PremiumPrompt:render() + local promptState = self.props.promptState + local requestType = self.props.requestType + + local contents + if promptState == PromptState.None or not isRelevantRequestType(requestType) then + --[[ + When the prompt is hidden, we'd rather not keep unused Roblox + instances for it around, so we don't render them + ]] + contents = nil + else + if promptState == PromptState.Error then + contents = self:RenderError() + else + contents = Roact.createElement(PremiumModal,{ + screenSize = self.state.screenSize, + }) + end + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + AnchorPoint = Vector2.new(0.5, 0), + Position = self.animationProgress:map(function(value) + return UDim2.new(0.5, 0, 1 - value, 0) + end), + [Roact.Change.AbsoluteSize] = self.changeScreenSize, + BackgroundTransparency = 1, + }, { + PremiumPrompt = contents, + OnCoreGuiMenuOpened = Roact.createElement(ExternalEventConnection, { + event = GuiService.MenuOpened, + callback = self.onClose, + }) + }) +end + +local function mapStateToProps(state) + return { + premiumProductInfo = state.premiumProductInfo, + promptState = state.promptState, + requestType = state.promptRequest.requestType, + purchaseError = state.purchaseError, + windowState = state.windowState, + } +end + +local function mapDispatchToProps(dispatch) + return { + purchasePremium = function() + dispatch(launchPremiumUpsell()) + end, + hideWindow = function() + dispatch(hideWindow()) + end, + setCompleteRequest = function() + dispatch(completeRequest()) + end + } +end + +PremiumPrompt = connectToStore( + mapStateToProps, + mapDispatchToProps +)(PremiumPrompt) + +return PremiumPrompt \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumPrompt.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumPrompt.spec.lua new file mode 100644 index 0000000..f792e04 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumPrompt.spec.lua @@ -0,0 +1,42 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local RequestType = require(Root.Enums.RequestType) + local WindowState = require(Root.Enums.WindowState) + local Reducer = require(Root.Reducers.Reducer) + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local PremiumPrompt = require(script.Parent.PremiumPrompt) + PremiumPrompt = PremiumPrompt.getUnconnected() + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, { + overrideStore = Rodux.Store.new(Reducer) + }, { + Roact.createElement(PremiumPrompt, { + premiumProductInfo = { + premiumFeatureTypeName = "Subscription", + mobileProductId = "com.roblox.robloxmobile.RobloxPremium450", + description = "Roblox Premium 450", + price = 4.99, + robuxAmount = 450, + isSubscriptionOnly = false, + currencySymbol = "$", + }, + promptState = PromptState.PremiumUpsell, + promptRequest = { + requestType = RequestType.Premium + }, + windowState = WindowState.Hidden, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AdditionalDetailLabel.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AdditionalDetailLabel.lua new file mode 100644 index 0000000..97cfc2a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AdditionalDetailLabel.lua @@ -0,0 +1,119 @@ +local Root = script.Parent.Parent.Parent + +local UserInputService = game:GetService("UserInputService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + + +local PromptState = require(Root.Enums.PromptState) +local UpsellFlow = require(Root.Enums.UpsellFlow) +local LocalizationService = require(Root.Localization.LocalizationService) +local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow) +local isMockingPurchases = require(Root.Utils.isMockingPurchases) +local getPlayerPrice = require(Root.Utils.getPlayerPrice) +local connectToStore = require(Root.connectToStore) + +local TextLocalizer = require(script.Parent.Parent.Connection.TextLocalizer) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local PURCHASE_DETAILS_KEY = "CoreScripts.PurchasePrompt.PurchaseDetails.%s" + +local function AdditionalDetailLabel(props) + return withLayoutValues(function(values) + local layoutOrder = props.layoutOrder + local messageKey = props.messageKey + local messageParams = props.messageParams + + if messageKey == nil then + -- We return an empty frame to preserve UIListLayout spacing + return Roact.createElement("Frame", { + LayoutOrder = layoutOrder, + Size = values.Size.AdditionalDetailsLabel, + BackgroundTransparency = 1, + BorderSizePixel = 0, + }) + end + + return Roact.createElement(TextLocalizer, { + key = messageKey, + params = messageParams, + render = function(localizedText) + return Roact.createElement("TextLabel", { + Text = localizedText, + LayoutOrder = layoutOrder, + Size = values.Size.AdditionalDetailsLabel, + BackgroundTransparency = 1, + BorderSizePixel = 0, + TextColor3 = Color3.new(1, 1, 1), + Font = Enum.Font.SourceSans, + TextSize = values.TextSize.AdditionalDetails, + TextYAlignment = Enum.TextYAlignment.Top, + TextScaled = true, + TextWrapped = true, + }, { + TextSizeConstraint = Roact.createElement("UITextSizeConstraint", { + MaxTextSize = values.TextSize.AdditionalDetails, + }) + }) + end, + }) + end) +end + +local function mapStateToProps(state) + local promptState = state.promptState + + local messageKey = nil + local messageParams = nil + + local isPlayerPremium = state.accountInfo.membershipType == 4 + local price = getPlayerPrice(state.productInfo, isPlayerPremium) + local balance = state.accountInfo.balance + + if promptState == PromptState.PromptPurchase then + if price == 0 then + messageKey = PURCHASE_DETAILS_KEY:format("BalanceUnaffected") + elseif isMockingPurchases() then + messageKey = PURCHASE_DETAILS_KEY:format("MockPurchase") + else + messageKey = PURCHASE_DETAILS_KEY:format("BalanceFutureV2") + messageParams = { + BALANCE_FUTURE = LocalizationService.numberParam(balance - price), + } + end + elseif promptState == PromptState.RobuxUpsell then + local upsellFlow = getUpsellFlow(UserInputService:GetPlatform()) + + if upsellFlow ~= UpsellFlow.Web then + local upsellRobux = state.nativeUpsell.robuxPurchaseAmount + local amountNeeded = price - balance + + local amountRemaining = upsellRobux - amountNeeded + messageKey = PURCHASE_DETAILS_KEY:format("RemainingAfterUpsell") + messageParams = { + REMAINING_ROBUX = LocalizationService.numberParam(amountRemaining), + } + end + elseif promptState == PromptState.PurchaseComplete then + if isMockingPurchases() then + messageKey = PURCHASE_DETAILS_KEY:format("MockPurchaseComplete") + else + messageKey = PURCHASE_DETAILS_KEY:format("BalanceNow") + messageParams = { + BALANCE_NOW = LocalizationService.numberParam(balance - price), + } + end + end + + return { + messageKey = messageKey, + messageParams = messageParams, + } +end + +AdditionalDetailLabel = connectToStore( + mapStateToProps +)(AdditionalDetailLabel) + +return AdditionalDetailLabel diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AdditionalDetailLabel.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AdditionalDetailLabel.spec.lua new file mode 100644 index 0000000..134eccf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AdditionalDetailLabel.spec.lua @@ -0,0 +1,35 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local AdditionalDetailLabel = require(script.Parent.AdditionalDetailLabel) + + AdditionalDetailLabel = AdditionalDetailLabel.getUnconnected() + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(AdditionalDetailLabel, { + layoutOrder = 1, + messageKey = "test", + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when showing no text", function() + local emptyMessageElement = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(AdditionalDetailLabel, { + layoutOrder = 1, + }) + }) + + local instance = Roact.mount(emptyMessageElement) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AnimatedDot.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AnimatedDot.lua new file mode 100644 index 0000000..044da75 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AnimatedDot.lua @@ -0,0 +1,38 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local GRAY = Color3.fromRGB(184, 184, 184) +local BLUE = Color3.fromRGB(0, 162, 255) + +local function Dot(props) + local layoutOrder = props.layoutOrder + local time = props.time + local size = 0.8 + local color = GRAY + + if time >= layoutOrder -1 and time <= layoutOrder then + local animationProgress = math.sin(math.pi * (time % 1)) + size = size + (1 - size) * animationProgress + color = GRAY:lerp(BLUE, animationProgress) + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1/3, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + }, { + Dot = Roact.createElement("Frame", { + Size = UDim2.new(0.7, 0, size, 0), + SizeConstraint = Enum.SizeConstraint.RelativeYY, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundColor3 = color, + BorderSizePixel = 0, + }) + }) +end + +return Dot \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AnimatedDot.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AnimatedDot.spec.lua new file mode 100644 index 0000000..0ad803d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AnimatedDot.spec.lua @@ -0,0 +1,18 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local AnimatedDot = require(script.Parent.AnimatedDot) + + it("should create and destroy without errors", function() + local element = Roact.createElement(AnimatedDot, { + time = 1, + layoutOrder = 1, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoResizeList.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoResizeList.lua new file mode 100644 index 0000000..24f92ba --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoResizeList.lua @@ -0,0 +1,82 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) +local Cryo = require(LuaPackages.Cryo) + +local AutoResizeList = Roact.Component:extend("AutoResizeList") + +function AutoResizeList:init() + self.containerRef = Roact.createRef() + self.listRef = Roact.createRef() + + self.contentSizeCallback = function() + if self.listRef.current and self.containerRef.current then + self:resizeContainer() + end + end +end + +function AutoResizeList:didMount() + self:resizeContainer() +end + +function AutoResizeList:resizeContainer() + self.containerRef.current.Size = UDim2.new( + 0, self.listRef.current.AbsoluteContentSize.X, + 0, self.listRef.current.AbsoluteContentSize.Y) +end + +function AutoResizeList:render() + local layoutOrder = self.props.layoutOrder + local position = self.props.position + local anchorPoint = self.props.anchorPoint + local backgroundImage = self.props.backgroundImage + local sliceCenter = self.props.sliceCenter + + local horizontalAlignment = self.props.horizontalAlignment + local verticalAlignment = self.props.verticalAlignment + local fillDirection = self.props.fillDirection + local listPadding = self.props.listPadding + + local children = Cryo.Dictionary.join(self.props[Roact.Children] or {}, { + ["$ListLayout"] = Roact.createElement("UIListLayout", { + HorizontalAlignment = horizontalAlignment, + VerticalAlignment = verticalAlignment, + FillDirection = fillDirection, + Padding = listPadding, + SortOrder = Enum.SortOrder.LayoutOrder, + + [Roact.Ref] = self.listRef, + [Roact.Change.AbsoluteContentSize] = self.contentSizeCallback, + }) + }) + + if backgroundImage == nil then + return Roact.createElement("Frame", { + LayoutOrder = layoutOrder, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Position = position, + AnchorPoint = anchorPoint, + + [Roact.Ref] = self.containerRef, + }, children) + else + local scaleType = (sliceCenter ~= nil) and Enum.ScaleType.Slice or Enum.ScaleType.Stretch + return Roact.createElement("ImageLabel", { + Image = backgroundImage, + LayoutOrder = layoutOrder, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScaleType = scaleType, + SliceCenter = sliceCenter, + Position = position, + AnchorPoint = anchorPoint, + + [Roact.Ref] = self.containerRef, + }, children) + end +end + +return AutoResizeList \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoResizeList.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoResizeList.spec.lua new file mode 100644 index 0000000..7e5bb97 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoResizeList.spec.lua @@ -0,0 +1,17 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local AutoResizeList = require(script.Parent.AutoResizeList) + + it("should create and destroy without errors", function() + local element = Roact.createElement(AutoResizeList, { + layoutOrder = 1, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoSizedTextLabel.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoSizedTextLabel.lua new file mode 100644 index 0000000..c4a5687 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoSizedTextLabel.lua @@ -0,0 +1,38 @@ +local Root = script.Parent.Parent.Parent +local TextService = game:GetService("TextService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) +local Cryo = require(LuaPackages.Cryo) + +local AutoSizedTextLabel = Roact.PureComponent:extend("AutoSizedTextLabel") + +function AutoSizedTextLabel:render() + local text = self.props.Text + local textSize = self.props.TextSize + local font = self.props.Font + local width = self.props.width + local maxHeight = self.props.maxHeight + + local totalTextSize + if text ~= nil then + totalTextSize = TextService:GetTextSize(text, textSize, font, Vector2.new(width, 10000)) + else + totalTextSize = Vector2.new(0, 0) + end + + local height = totalTextSize.Y + if maxHeight and height > maxHeight then + height = maxHeight + end + + local textLabelProps = Cryo.Dictionary.join(self.props, { + width = Cryo.None, + maxHeight = Cryo.None, + Size = UDim2.new(0, width, 0, height), + }) + + return Roact.createElement("TextLabel", textLabelProps) +end + +return AutoSizedTextLabel diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoSizedTextLabel.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoSizedTextLabel.spec.lua new file mode 100644 index 0000000..75b467e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoSizedTextLabel.spec.lua @@ -0,0 +1,46 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local AutoSizedTextLabel = require(script.Parent.AutoSizedTextLabel) + + it("should create and destroy without errors", function() + local element = Roact.createElement(AutoSizedTextLabel, { + Text = "Hello", + TextSize = 10, + Font = Enum.Font.SourceSans, + width = 100, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should not throw even if no text is provided", function() + local element = Roact.createElement(AutoSizedTextLabel, { + TextSize = 10, + Font = Enum.Font.SourceSans, + width = 100, + }) + + expect(Roact.mount(element)).to.be.ok() + end) + + it("should clamp its height if maxHeight was provided", function() + local element = Roact.createElement(AutoSizedTextLabel, { + Text = "Really long text that should get to a height larger than 1 line!", + TextSize = 10, + Font = Enum.Font.SourceSans, + width = 100, + maxHeight = 2, + }) + + local folder = Instance.new("Frame") + Roact.mount(element, folder) + local textLabel = folder:FindFirstChildWhichIsA("GuiObject") + + expect(textLabel.Size.Y.Offset).to.equal(2) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Button.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Button.lua new file mode 100644 index 0000000..2fa4f4d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Button.lua @@ -0,0 +1,161 @@ +local Root = script.Parent.Parent.Parent +local ContextActionService = game:GetService("ContextActionService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local connectToStore = require(Root.connectToStore) + +local TextLocalizer = require(script.Parent.Parent.Connection.TextLocalizer) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local Button = Roact.PureComponent:extend("Button") + +function Button:init() + local onClick = self.props.onClick + local imageDown = self.props.imageDown + local imageUp = self.props.imageUp + + self.state = { + currentImage = imageUp + } + + self.inputBegan = function() + self:setState({ + currentImage = imageDown + }) + end + + self.inputEnded = function() + self:setState({ + currentImage = imageUp + }) + end + + self.activated = function() + onClick() + self:setState({ + currentImage = imageUp + }) + end +end + +function Button:didMount() + -- Some buttons need to support additional button bindings + local bindings = self.props.additionalBindings or {} + table.insert(bindings, self.props.gamepadButton) + + ContextActionService:BindCoreAction( + self.props.stringKey, + function(actionName, inputState, inputObj) + --[[ + CLILUACORE-521: + InputState MUST be 'Begin' in this case; otherwise, opening + the settings menu it will create new ContextActionService + bindings. When it does this, they trigger the 'Cancel' input + state, which invoke our binding (in order to tell it that + it's being canceled) + ]] + if inputState == Enum.UserInputState.Begin then + self.props.onClick() + end + end, + false, + unpack(bindings) + ) +end + +function Button:willUnmount() + ContextActionService:UnbindCoreAction(self.props.stringKey) +end + +function Button:render() + assert(typeof(self.props.gamepadButton) == "EnumItem" + and self.props.gamepadButton.EnumType == Enum.KeyCode, + "Prop 'gamepadButton' is required and must be of type Enum.KeyCode") + + return withLayoutValues(function(values) + local stringKey = self.props.stringKey + local font = self.props.font + local size = self.props.size + local position = self.props.position + + local gamepadEnabled = self.props.gamepadEnabled + local gamepadButton = self.props.gamepadButton + + local imageData = values.Image[self.state.currentImage] + + local buttonContents = { + ButtonLabel = Roact.createElement(TextLocalizer, { + key = stringKey, + render = function(localizedText) + return Roact.createElement("TextLabel", { + Text = localizedText, + Font = font, + Size = UDim2.new(0.6, 0, 0.8, 0), + Position = UDim2.new(0.2, 0, 0.1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + TextColor3 = Color3.new(1, 1, 1), + TextSize = values.TextSize.Button, + TextScaled = true, + TextWrapped = false, + LayoutOrder = 2, + }, { + TextSizeConstraint = Roact.createElement("UITextSizeConstraint", { + MaxTextSize = values.TextSize.Button, + }), + }) + end, + }) + } + + if gamepadEnabled then + -- Using a frame allows the icon to be left-aligned and still + -- be in a size-constrained section of the overall button + buttonContents.ButtonIcon = Roact.createElement("Frame", { + Size = UDim2.new(0.2, 0, 0.8, 0), + Position = UDim2.new(0, 0, 0.1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + Icon = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, values.Size.ButtonIconPadding, 0, values.Size.ButtonIconYOffset), + SizeConstraint = Enum.SizeConstraint.RelativeYY, + ScaleType = Enum.ScaleType.Fit, + Image = values.Image[gamepadButton.Name].Path, + BackgroundTransparency = 1, + BorderSizePixel = 0, + }) + }) + end + + return Roact.createElement("ImageButton", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + AutoButtonColor = false, + Modal = true, + + Size = size, + Position = position, + ScaleType = Enum.ScaleType.Slice, + Image = imageData.Path, + SliceCenter = imageData.SliceCenter, + + [Roact.Event.InputBegan] = self.inputBegan, + [Roact.Event.InputEnded] = self.inputEnded, + [Roact.Event.Activated] = self.activated, + }, buttonContents) + end) +end + +local function mapStateToProps(state) + return { + gamepadEnabled = state.gamepadEnabled + } +end + +Button = connectToStore(mapStateToProps)(Button) + +return Button \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Button.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Button.spec.lua new file mode 100644 index 0000000..1b2f929 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Button.spec.lua @@ -0,0 +1,50 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local Button = require(script.Parent.Button) + + Button = Button.getUnconnected() + + it("should create and destroy without errors with gamepad disabled", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(Button, { + gamepadEnabled = false, + + stringKey = "testing123", + gamepadButton = Enum.KeyCode.ButtonA, + font = Enum.Font.SourceSans, + imageUp = "ButtonUp", + imageDown = "ButtonDown", + onClick = function() + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with gamepad enabled", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(Button, { + gamepadEnabled = true, + + stringKey = "testing123", + gamepadButton = Enum.KeyCode.ButtonA, + font = Enum.Font.SourceSans, + imageUp = "ButtonUp", + imageDown = "ButtonDown", + onClick = function() + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/CancelButton.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/CancelButton.lua new file mode 100644 index 0000000..8d44052 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/CancelButton.lua @@ -0,0 +1,28 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local Button = require(script.Parent.Button) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local function CancelButton(props) + return withLayoutValues(function(values) + local onClick = props.onClick + + return Roact.createElement(Button, { + font = Enum.Font.SourceSans, + imageUp = "ButtonUpRight", + imageDown = "ButtonDownRight", + gamepadButton = Enum.KeyCode.ButtonB, + + stringKey = "CoreScripts.PurchasePrompt.CancelPurchase.Cancel", + size = UDim2.new(0.5, 0, 1, 0), + position = UDim2.new(0.5, 0, 0, 0), + + onClick = onClick, + }) + end) +end + +return CancelButton \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/CancelButton.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/CancelButton.spec.lua new file mode 100644 index 0000000..7a6233b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/CancelButton.spec.lua @@ -0,0 +1,22 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local CancelButton = require(script.Parent.CancelButton) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(CancelButton, { + onClick = function() + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ConfirmButton.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ConfirmButton.lua new file mode 100644 index 0000000..18c2e3d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ConfirmButton.lua @@ -0,0 +1,52 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local ClickScamDetector = require(Root.Utils.ClickScamDetector) + +local Button = require(script.Parent.Button) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local CONFIRM_BUTTON = Enum.KeyCode.ButtonA + +local ConfirmButton = Roact.Component:extend("ConfirmButton") + +function ConfirmButton:init() + local onClick = self.props.onClick + + self.activated = function() + if self.clickScamDetector:isClickValid() then + onClick() + end + end + + self.clickScamDetector = ClickScamDetector.new({ + buttonInput = CONFIRM_BUTTON, + initialDelay = 0.5, + }) +end + +function ConfirmButton:willUnmount() + self.clickScamDetector:destroy() +end + +function ConfirmButton:render() + return withLayoutValues(function(values) + local stringKey = self.props.stringKey + + return Roact.createElement(Button, { + font = Enum.Font.SourceSansBold, + imageUp = "ButtonUpLeft", + imageDown = "ButtonDownLeft", + gamepadButton = CONFIRM_BUTTON, + + stringKey = stringKey, + size = UDim2.new(0.5, 0, 1, 0), + + onClick = self.activated, + }) + end) +end + +return ConfirmButton \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ConfirmButton.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ConfirmButton.spec.lua new file mode 100644 index 0000000..164cbe9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ConfirmButton.spec.lua @@ -0,0 +1,23 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local ConfirmButton = require(script.Parent.ConfirmButton) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(ConfirmButton, { + stringKey = "testing123", + onClick = function() + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/InProgressContents.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/InProgressContents.lua new file mode 100644 index 0000000..48366f6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/InProgressContents.lua @@ -0,0 +1,62 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local TextLocalizer = require(script.Parent.Parent.Connection.TextLocalizer) + +local AutoSizedTextLabel = require(script.Parent.AutoSizedTextLabel) +local PurchasingAnimation = require(script.Parent.PurchasingAnimation) + +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local function InProgressContents(props) + return withLayoutValues(function(values) + return Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + + ScaleType = Enum.ScaleType.Slice, + Image = values.Image.InProgressBackground.Path, + SliceCenter = values.Image.InProgressBackground.SliceCenter, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + ListLayout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 20) + }), + PurchasingText = Roact.createElement(TextLocalizer, { + key = "CoreScripts.PurchasePrompt.Purchasing", + render = function(localizedText) + return Roact.createElement(AutoSizedTextLabel, { + width = values.Size.Dialog.X.Offset, + Text = localizedText, + BackgroundTransparency = 1, + BorderSizePixel = 0, + TextColor3 = Color3.new(1, 1, 1), + Font = Enum.Font.SourceSans, + TextSize = values.TextSize.Purchasing, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + TextScaled = true, + TextWrapped = true, + LayoutOrder = 1, + }, { + TextSizeConstraint = Roact.createElement("UITextSizeConstraint", { + MaxTextSize = values.TextSize.Purchasing, + }), + }) + end, + }), + PurchasingAnimation = Roact.createElement(PurchasingAnimation, { + layoutOrder = 2, + }), + }) + end) +end + +return InProgressContents \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/InProgressContents.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/InProgressContents.spec.lua new file mode 100644 index 0000000..c8550d8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/InProgressContents.spec.lua @@ -0,0 +1,22 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local InProgressContents = require(script.Parent.InProgressContents) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(InProgressContents, { + anchorPoint = Vector2.new(0, 0), + position = UDim2.new(0, 0, 0, 0), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ItemPreviewImage.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ItemPreviewImage.lua new file mode 100644 index 0000000..fe51de3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ItemPreviewImage.lua @@ -0,0 +1,94 @@ +local Root = script.Parent.Parent.Parent +local LuaPackages = Root.Parent +local Cryo = require(LuaPackages.Cryo) +local Roact = require(LuaPackages.Roact) +local UIBlox = require(LuaPackages.UIBlox) +local UIBloxImages = UIBlox.App.ImageSet.Images + +local PurchaseError = require(Root.Enums.PurchaseError) +local PromptState = require(Root.Enums.PromptState) +local connectToStore = require(Root.connectToStore) + +local PREMIUM_ICON = UIBloxImages["icons/graphic/premium_large"] +local ADULT_ERROR_ICON = UIBloxImages["icons/status/error_large"] + +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local function ItemPreviewImage(props) + return withLayoutValues(function(values) + local layoutOrder = props.layoutOrder + local promptState = props.promptState + local purchaseError = props.purchaseError + local productImageUrl = props.productImageUrl + + local showPremiumIcon = false + local showAdultErrorIcon = false + local backgroundTransparency = 0 + if promptState == PromptState.AdultConfirmation then + showAdultErrorIcon = true + backgroundTransparency = 1 + elseif promptState == PromptState.Error then + if purchaseError == PurchaseError.PremiumOnly then + showPremiumIcon = true + else + productImageUrl = values.Image.ErrorIcon.Path + end + end + + return Roact.createElement("Frame", { + Size = values.Size.ItemPreviewContainerFrame, + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + }, { + PremiumIcon = showPremiumIcon and Roact.createElement("ImageLabel", Cryo.Dictionary.join(PREMIUM_ICON, { + Size = values.Size.ItemPreview, + BackgroundTransparency = 1, + BorderSizePixel = 0, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + ImageTransparency = 0, + })) or nil, + ItemPreviewImageContainer = not showPremiumIcon and Roact.createElement("Frame", { + Size = values.Size.ItemPreviewWhiteFrame, + BackgroundTransparency = backgroundTransparency, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + BorderSizePixel = 0, + BackgroundColor3 = Color3.new(1, 1, 1), + }, { + ItemImage = showAdultErrorIcon and Roact.createElement("ImageLabel", Cryo.Dictionary.join(ADULT_ERROR_ICON, { + Size = values.Size.ItemPreview, + BackgroundTransparency = 1, + BorderSizePixel = 0, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + ImageTransparency = 0, + })) + or Roact.createElement("ImageLabel", { + Size = values.Size.ItemPreview, + BackgroundTransparency = 1, + BorderSizePixel = 0, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Image = productImageUrl, + ImageTransparency = 0, + }), + }) or nil, + }) + end) +end + +local function mapStateToProps(state) + return { + promptState = state.promptState, + purchaseError = state.purchaseError, + productImageUrl = state.productInfo.imageUrl, + } +end + +ItemPreviewImage = connectToStore( + mapStateToProps +)(ItemPreviewImage) + +return ItemPreviewImage diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ItemPreviewImage.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ItemPreviewImage.spec.lua new file mode 100644 index 0000000..4c53b17 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ItemPreviewImage.spec.lua @@ -0,0 +1,24 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local ItemPreviewImage = require(script.Parent.ItemPreviewImage) + + ItemPreviewImage = ItemPreviewImage.getUnconnected() + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(ItemPreviewImage, { + layoutOrder = 1, + imageUrl = "", + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/OkButton.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/OkButton.lua new file mode 100644 index 0000000..2d6612d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/OkButton.lua @@ -0,0 +1,30 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local Button = require(script.Parent.Button) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local function OkButton(props) + return withLayoutValues(function(values) + local onClick = props.onClick + + return Roact.createElement(Button, { + font = Enum.Font.SourceSans, + imageUp = "ButtonUp", + imageDown = "ButtonDown", + gamepadButton = Enum.KeyCode.ButtonA, + additionalBindings = { + Enum.KeyCode.ButtonB, + }, + + stringKey = "CoreScripts.PurchasePrompt.Button.OK", + size = UDim2.new(1, 0, 0, values.Size.ButtonHeight-4), + + onClick = onClick + }) + end) +end + +return OkButton \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/OkButton.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/OkButton.spec.lua new file mode 100644 index 0000000..0a97258 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/OkButton.spec.lua @@ -0,0 +1,22 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local OkButton = require(script.Parent.OkButton) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(OkButton, { + onClick = function() + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Price.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Price.lua new file mode 100644 index 0000000..417e54f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Price.lua @@ -0,0 +1,60 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local NumberLocalizer = require(script.Parent.Parent.Connection.NumberLocalizer) +local AutoResizeList = require(script.Parent.AutoResizeList) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +return function(props) + return withLayoutValues(function(values) + local layoutOrder = props.layoutOrder + local price = props.price + + return Roact.createElement(AutoResizeList, { + layoutOrder = layoutOrder, + horizontalAlignment = Enum.HorizontalAlignment.Left, + verticalAlignment = Enum.VerticalAlignment.Center, + fillDirection = Enum.FillDirection.Horizontal, + }, { + RobuxIconContainer = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundTransparency = 1, + Size = values.Size.RobuxIconContainerFrame, + LayoutOrder = 1, + }, { + RobuxIcon = Roact.createElement("ImageLabel", { + Size = values.Size.RobuxIcon, + BackgroundTransparency = 1, + BorderSizePixel = 0, + AnchorPoint = Vector2.new(0, 0.5), + Position = UDim2.new(0, 0, 0.5, 0), + Image = values.Image.RobuxIcon.Path, + }), + }), + PriceTextLabel = Roact.createElement(NumberLocalizer, { + number = price, + render = function(localizedNumber) + return Roact.createElement("TextLabel", { + Text = localizedNumber, + LayoutOrder = 2, + Size = values.Size.PriceTextLabel, + BackgroundTransparency = 1, + BorderSizePixel = 0, + TextColor3 = values.TextColor.PriceLabel, + Font = Enum.Font.SourceSansBold, + TextSize = values.TextSize.Default, + TextXAlignment = Enum.TextXAlignment.Left, + TextScaled = true, + TextWrapped = true, + }, { + TextSizeConstraint = Roact.createElement("UITextSizeConstraint", { + MaxTextSize = values.TextSize.Default, + }) + }) + end, + }) + }) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Price.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Price.spec.lua new file mode 100644 index 0000000..30a5ac6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Price.spec.lua @@ -0,0 +1,23 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local Price = require(script.Parent.Price) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(Price, { + layoutOrder = 1, + imageUrl = "", + price = 50, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ProductDescription.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ProductDescription.lua new file mode 100644 index 0000000..ac35d0a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ProductDescription.lua @@ -0,0 +1,148 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local PromptState = require(Root.Enums.PromptState) +local PurchaseError = require(Root.Enums.PurchaseError) + +local LocalizationService = require(Root.Localization.LocalizationService) + +local TextLocalizer = require(script.Parent.Parent.Connection.TextLocalizer) +local AutoSizedTextLabel = require(script.Parent.AutoSizedTextLabel) +local Price = require(script.Parent.Price) + +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local getPlayerPrice = require(Root.Utils.getPlayerPrice) +local connectToStore = require(Root.connectToStore) + +local PURCHASE_MESSAGE_KEY = "CoreScripts.PurchasePrompt.PurchaseMessage.%s" + +local function ProductDescription(props) + return withLayoutValues(function(values) + local layoutOrder = props.layoutOrder + local descriptionKey = props.descriptionKey + local descriptionParams = props.descriptionParams + local showPrice = props.showPrice + local price = props.price + + return Roact.createElement("Frame", { + BorderSizePixel = 0, + Size = values.Size.ProductDescription, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + ProductDescriptionPadding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, values.Size.ProductDescriptionPaddingTop), + }), + ProductDescriptionListLayout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + ProductDescriptionText = Roact.createElement(TextLocalizer, { + key = descriptionKey, + params = descriptionParams, + render = function(localizedText) + return Roact.createElement(AutoSizedTextLabel, { + width = values.Size.ProductDescription.X.Offset - values.Size.HorizontalPadding, + maxHeight = showPrice + and values.Size.ProductDescription.Y.Offset - values.Size.RobuxIconContainerFrame.Y.Offset + or values.Size.ProductDescription.Y.Offset, + Text = localizedText, + BackgroundTransparency = 1, + BorderSizePixel = 0, + TextColor3 = Color3.new(1, 1, 1), + Font = Enum.Font.SourceSans, + TextSize = values.TextSize.ProductDescription, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextScaled = true, + TextWrapped = true, + LayoutOrder = 1, + }, { + TextSizeConstraint = Roact.createElement("UITextSizeConstraint", { + MaxTextSize = values.TextSize.ProductDescription, + }), + }) + end, + }), + Price = showPrice and Roact.createElement(Price, { + layoutOrder = 2, + price = price, + }) or nil, + }) + end) +end + +local function mapStateToProps(state) + local promptState = state.promptState + local isPlayerPremium = state.accountInfo.membershipType == 4 + local price = getPlayerPrice(state.productInfo, isPlayerPremium) + local isFree = price == 0 + local canPurchase = promptState ~= PromptState.Error + + local descriptionKey + local descriptionParams + + + if promptState == PromptState.PurchaseComplete then + descriptionKey = PURCHASE_MESSAGE_KEY:format("Succeeded") + descriptionParams = { + ITEM_NAME = state.productInfo.name, + NEEDED_AMOUNT = LocalizationService.numberParam( + price - state.accountInfo.balance + ), + ASSET_TYPE = LocalizationService.nestedKeyParam( + LocalizationService.getKeyFromItemType(state.productInfo.itemType)) + } + elseif promptState == PromptState.RobuxUpsell then + descriptionKey = PURCHASE_MESSAGE_KEY:format("NeedMoreRobux") + descriptionParams = { + ITEM_NAME = state.productInfo.name, + NEEDED_AMOUNT = LocalizationService.numberParam( + price - state.accountInfo.balance + ), + ASSET_TYPE = LocalizationService.nestedKeyParam( + LocalizationService.getKeyFromItemType(state.productInfo.itemType)) + } + elseif promptState == PromptState.AdultConfirmation then + descriptionKey = PURCHASE_MESSAGE_KEY:format("AdultConfirmation") + elseif promptState == PromptState.Error then + descriptionKey = LocalizationService.getErrorKey(state.purchaseError) + if state.purchaseError == PurchaseError.UnknownFailure then + descriptionParams = { + ITEM_NAME = state.productInfo.name + } + end + elseif promptState ~= PromptState.None then + if isFree then + descriptionKey = PURCHASE_MESSAGE_KEY:format("Free") + descriptionParams = { + ITEM_NAME = state.productInfo.name, + } + else + descriptionKey = PURCHASE_MESSAGE_KEY:format("Purchase") + descriptionParams = { + ASSET_TYPE = LocalizationService.nestedKeyParam( + LocalizationService.getKeyFromItemType(state.productInfo.itemType)), + ITEM_NAME = state.productInfo.name, + } + end + end + + return { + descriptionKey = descriptionKey, + descriptionParams = descriptionParams, + showPrice = not isFree and canPurchase and promptState ~= PromptState.AdultConfirmation, + price = price, + } +end + +ProductDescription = connectToStore( + mapStateToProps +)(ProductDescription) + +return ProductDescription diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ProductDescription.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ProductDescription.spec.lua new file mode 100644 index 0000000..f6f191c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ProductDescription.spec.lua @@ -0,0 +1,31 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + local Rodux = require(LuaPackages.Rodux) + + local Reducer = require(Root.Reducers.Reducer) + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local ProductDescription = require(script.Parent.ProductDescription) + + ProductDescription = ProductDescription.getUnconnected() + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, { + overrideStore = Rodux.Store.new(Reducer, { + productInfo = { + price = 10, + }, + }) + }, { + Roact.createElement(ProductDescription, { + descriptionKey = "CoreScripts.PurchasePrompt.PurchaseMessage.Purchase", + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptButtons.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptButtons.lua new file mode 100644 index 0000000..0af92dd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptButtons.lua @@ -0,0 +1,100 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local PromptState = require(Root.Enums.PromptState) +local purchaseItem = require(Root.Thunks.purchaseItem) +local launchRobuxUpsell = require(Root.Thunks.launchRobuxUpsell) +local getPlayerPrice = require(Root.Utils.getPlayerPrice) +local connectToStore = require(Root.connectToStore) + +local ConfirmButton = require(script.Parent.ConfirmButton) +local CancelButton = require(script.Parent.CancelButton) +local OkButton = require(script.Parent.OkButton) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local CONFIRM_PURCHASE_KEY = "CoreScripts.PurchasePrompt.ConfirmPurchase.%s" + +local PromptButtons = Roact.PureComponent:extend("PromptButtons") + +function PromptButtons:render() + return withLayoutValues(function(values) + local layoutOrder = self.props.layoutOrder + local onClose = self.props.onClose + local promptState = self.props.promptState + local price = self.props.price + + local onBuy = self.props.onBuy + local onRobuxUpsell = self.props.onRobuxUpsell + + local children + if promptState == PromptState.PurchaseComplete + or promptState == PromptState.Error + then + children = { + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, 4), + }), + OkButton = Roact.createElement(OkButton, { + onClick = onClose, + }), + } + else + local confirmButtonStringKey = CONFIRM_PURCHASE_KEY:format("BuyNow") + local leftButtonCallback = onBuy + if price == 0 then + confirmButtonStringKey = CONFIRM_PURCHASE_KEY:format("TakeFree") + elseif promptState == PromptState.RobuxUpsell then + confirmButtonStringKey = CONFIRM_PURCHASE_KEY:format("BuyRobuxV2") + leftButtonCallback = onRobuxUpsell + elseif promptState == PromptState.AdultConfirmation then + confirmButtonStringKey = "CoreScripts.PurchasePrompt.Button.OK" + leftButtonCallback = onRobuxUpsell + end + children = { + ConfirmButton = Roact.createElement(ConfirmButton, { + stringKey = confirmButtonStringKey, + onClick = leftButtonCallback, + }), + CancelButton = Roact.createElement(CancelButton, { + onClick = onClose, + }), + } + end + + return Roact.createElement("Frame", { + LayoutOrder = layoutOrder, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, values.Size.ButtonHeight) + }, children) + end) +end + +local function mapStateToProps(state) + local isPlayerPremium = state.accountInfo.membershipType == 4 + local price = getPlayerPrice(state.productInfo, isPlayerPremium) + return { + promptState = state.promptState, + price = price, + } +end + +local function mapDispatchToProps(dispatch) + return { + onBuy = function() + dispatch(purchaseItem()) + end, + onRobuxUpsell = function() + dispatch(launchRobuxUpsell()) + end, + } +end + +PromptButtons = connectToStore( + mapStateToProps, + mapDispatchToProps +)(PromptButtons) + +return PromptButtons diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptButtons.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptButtons.spec.lua new file mode 100644 index 0000000..2cf3e66 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptButtons.spec.lua @@ -0,0 +1,44 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local PromptState = require(Root.Enums.PromptState) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local PromptButtons = require(script.Parent.PromptButtons) + PromptButtons = PromptButtons.getUnconnected() + + local function noop() + end + + it("should create and destroy without errors with one button", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(PromptButtons, { + layoutOrder = 1, + onClose = noop, + promptState = PromptState.PurchaseComplete, + price = 1, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with two buttons", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(PromptButtons, { + layoutOrder = 1, + onClose = noop, + promptState = PromptState.PromptPurchase, + price = 1, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptContents.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptContents.lua new file mode 100644 index 0000000..904e6cc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptContents.lua @@ -0,0 +1,56 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local AutoResizeList = require(script.Parent.AutoResizeList) +local ItemPreviewImage = require(script.Parent.ItemPreviewImage) +local ProductDescription = require(script.Parent.ProductDescription) +local PromptButtons = require(script.Parent.PromptButtons) +local AdditionalDetailLabel = require(script.Parent.AdditionalDetailLabel) + +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local function PromptContents(props) + return withLayoutValues(function(values) + local onClose = props.onClose + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BorderSizePixel = 0, + BackgroundTransparency = 1, + }, { + ListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + PromptBody = Roact.createElement(AutoResizeList, { + layoutOrder = 1, + backgroundImage = values.Image.PromptBackground.Path, + sliceCenter = values.Image.PromptBackground.SliceCenter, + fillDirection = Enum.FillDirection.Vertical, + }, { + ProductInfo = Roact.createElement(AutoResizeList, { + layoutOrder = 1, + fillDirection = Enum.FillDirection.Horizontal, + }, { + ItemPreviewImage = Roact.createElement(ItemPreviewImage, { + layoutOrder = 1, + }), + ProductDescription = Roact.createElement(ProductDescription, { + layoutOrder = 2, + }) + }), + AdditionalDetails = Roact.createElement(AdditionalDetailLabel, { + layoutOrder = 2, + }), + }), + PromptButtons = Roact.createElement(PromptButtons, { + layoutOrder = 2, + onClose = onClose, + }), + }) + end) +end + +return PromptContents diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptContents.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptContents.spec.lua new file mode 100644 index 0000000..1804892 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptContents.spec.lua @@ -0,0 +1,39 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local PromptContents = require(script.Parent.PromptContents) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, { + promptState = PromptState.PromptPurchase, + overrideStore = Rodux.Store.new(Reducer, { + promptState = PromptState.PromptPurchase, + accountInfo = { + balance = 100, + }, + productInfo = { + assetTypeId = 2, -- T-shirt + price = 10, + itemType = 2, + }, + }) + }, { + Roact.createElement(PromptContents, { + layoutOrder = 1, + onClose = function() + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasePrompt.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasePrompt.lua new file mode 100644 index 0000000..831691a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasePrompt.lua @@ -0,0 +1,126 @@ +local Root = script.Parent.Parent.Parent +local GuiService = game:GetService("GuiService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) +local Otter = require(LuaPackages.Otter) + +local RequestType = require(Root.Enums.RequestType) +local PromptState = require(Root.Enums.PromptState) +local WindowState = require(Root.Enums.WindowState) +local hideWindow = require(Root.Thunks.hideWindow) +local completeRequest = require(Root.Thunks.completeRequest) +local connectToStore = require(Root.connectToStore) + +local ExternalEventConnection = require(script.Parent.Parent.Connection.ExternalEventConnection) +local PromptContents = require(script.Parent.PromptContents) +local InProgressContents = require(script.Parent.InProgressContents) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local PurchasePrompt = Roact.Component:extend(script.Name) + +local SPRING_CONFIG = { + dampingRatio = 1, + frequency = 1.6, +} + +local function isRelevantRequestType(requestType) + return requestType == RequestType.Asset + or requestType == RequestType.Bundle + or requestType == RequestType.GamePass + or requestType == RequestType.Product + or requestType == RequestType.Subscription +end + +function PurchasePrompt:init() + local animationProgress, setProgress = Roact.createBinding(0) + + self.motor = Otter.createSingleMotor(0) + self.motor:start() + + self.motor:onStep(setProgress) + self.animationProgress = animationProgress + + self.motor:onComplete(function() + if self.props.windowState == WindowState.Hidden and isRelevantRequestType(self.props.requestType) then + self.props.completeRequest() + end + end) + + self.onClose = function() + self.props.hideWindow() + end +end + +function PurchasePrompt:didUpdate(prevProps, prevState) + if prevProps.windowState ~= self.props.windowState then + local goal = (self.props.windowState == WindowState.Hidden or not isRelevantRequestType(self.props.requestType)) and 0 or 1 + self.motor:setGoal(Otter.spring(goal, SPRING_CONFIG)) + end +end + +function PurchasePrompt:render() + return withLayoutValues(function(values) + local promptState = self.props.promptState + local requestType = self.props.requestType + + local contents + if promptState == PromptState.None or not isRelevantRequestType(requestType) then + --[[ + When the prompt is hidden, we'd rather not keep unused Roblox + instances for it around, so we don't render them + ]] + contents = nil + elseif promptState == PromptState.PurchaseInProgress or promptState == PromptState.UpsellInProgress then + contents = Roact.createElement(InProgressContents) + else + contents = Roact.createElement(PromptContents, { + onClose = self.onClose, + }) + end + + return Roact.createElement("Frame", { + Size = values.Size.Dialog, + BorderSizePixel = 0, + BackgroundTransparency = 1, + AnchorPoint = self.animationProgress:map(function(value) + return Vector2.new(0.5, 1 - 0.5 * value) + end), + Position = self.animationProgress:map(function(value) + return UDim2.new(0.5, 0, 0.5 * value, 0) + end), + }, { + PromptContents = contents, + OnCoreGuiMenuOpened = Roact.createElement(ExternalEventConnection, { + event = GuiService.MenuOpened, + callback = self.onClose, + }) + }) + end) +end + +local function mapStateToProps(state) + return { + promptState = state.promptState, + requestType = state.promptRequest.requestType, + windowState = state.windowState, + } +end + +local function mapDispatchToProps(dispatch) + return { + hideWindow = function() + dispatch(hideWindow()) + end, + completeRequest = function() + dispatch(completeRequest()) + end + } +end + +PurchasePrompt = connectToStore( + mapStateToProps, + mapDispatchToProps +)(PurchasePrompt) + +return PurchasePrompt diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasePrompt.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasePrompt.spec.lua new file mode 100644 index 0000000..19e94d3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasePrompt.spec.lua @@ -0,0 +1,35 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local PurchasePrompt = require(script.Parent.PurchasePrompt) + PurchasePrompt = PurchasePrompt.getUnconnected() + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, { + overrideStore = Rodux.Store.new(Reducer, { + promptState = PromptState.PromptPurchase, + accountInfo = { + balance = 100, + }, + productInfo = { + assetTypeId = 2, -- T-shirt + price = 10, + itemType = 2, + }, + }) + }, { + Roact.createElement(PurchasePrompt) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasingAnimation.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasingAnimation.lua new file mode 100644 index 0000000..7839c4a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasingAnimation.lua @@ -0,0 +1,68 @@ +local Root = script.Parent.Parent.Parent +local RunService = game:GetService("RunService") +local Workspace = game:GetService("Workspace") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local AnimatedDot = require(script.Parent.AnimatedDot) +local ExternalEventConnection = require(script.Parent.Parent.Connection.ExternalEventConnection) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local ANIMATION_SPEED_MULTIPLIER = 2.00 + +local PurchasingAnimation = Roact.Component:extend("PurchasingAnimation") + +function PurchasingAnimation:init() + self.state = { + gameTime = 0, + } + + self.onRenderStepped = function() + self:setState({ + gameTime = Workspace.DistributedGameTime + }) + end +end + +function PurchasingAnimation:render() + return withLayoutValues(function(values) + local layoutOrder = self.props.layoutOrder + + local gameTime = self.state.gameTime + + local animationTime = (gameTime * ANIMATION_SPEED_MULTIPLIER) % 3 + + return Roact.createElement("Frame", { + Size = values.Size.PurchasingAnimation, + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + }, { + RenderSteppedConnection = Roact.createElement(ExternalEventConnection, { + event = RunService.RenderStepped, + callback = self.onRenderStepped, + }), + ListLayout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + AnimatedDot1 = Roact.createElement(AnimatedDot, { + layoutOrder = 1, + time = animationTime, + }), + AnimatedDot2 = Roact.createElement(AnimatedDot, { + layoutOrder = 2, + time = animationTime, + }), + AnimatedDot3 = Roact.createElement(AnimatedDot, { + layoutOrder = 3, + time = animationTime, + }), + }) + end) +end + +return PurchasingAnimation \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasingAnimation.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasingAnimation.spec.lua new file mode 100644 index 0000000..e9d3403 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasingAnimation.spec.lua @@ -0,0 +1,21 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local PurchasingAnimation = require(script.Parent.PurchasingAnimation) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(PurchasingAnimation, { + layoutOrder = 1, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePromptApp.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePromptApp.lua new file mode 100644 index 0000000..ac00a5c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePromptApp.lua @@ -0,0 +1,79 @@ +local Root = script.Parent.Parent +local CorePackages = game:GetService("CorePackages") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) +local Rodux = require(LuaPackages.Rodux) +local RoactRodux = require(LuaPackages.RoactRodux) +local UIBlox = require(LuaPackages.UIBlox) +local StyleProvider = UIBlox.Style.Provider + +local Reducer = require(Root.Reducers.Reducer) +local Network = require(Root.Services.Network) +local Analytics = require(Root.Services.Analytics) +local PlatformInterface = require(Root.Services.PlatformInterface) +local ExternalSettings = require(Root.Services.ExternalSettings) +local Thunk = require(Root.Thunk) + +local PremiumPrompt = require(script.Parent.PremiumPrompt.PremiumPrompt) +local PurchasePrompt = require(script.Parent.PurchasePrompt.PurchasePrompt) +local EventConnections = require(script.Parent.Connection.EventConnections) +local LayoutValuesProvider = require(script.Parent.Connection.LayoutValuesProvider) +local provideRobloxLocale = require(script.Parent.Connection.provideRobloxLocale) + +local DarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme) +local Gotham = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham) + +local PurchasePromptApp = Roact.Component:extend("PurchasePromptApp") + +function PurchasePromptApp:init() + local initialState = {} + + local network = Network.new() + local analytics = Analytics.new() + local platformInterface = PlatformInterface.new() + local externalSettings = ExternalSettings.new() + + self.state = { + store = Rodux.Store.new(Reducer, initialState, { + Thunk.middleware({ + [Network] = network, + [Analytics] = analytics, + [PlatformInterface] = platformInterface, + [ExternalSettings] = externalSettings, + }), + }), + isTenFootInterface = externalSettings.isTenFootInterface(), + } +end + +function PurchasePromptApp:render() + return provideRobloxLocale(function() + return Roact.createElement(RoactRodux.StoreProvider, { + store = self.state.store, + }, { + StyleProvider = Roact.createElement(StyleProvider, { + style = { + Theme = DarkTheme, + Font = Gotham, + }, + }, { + PurchasePrompt = Roact.createElement(LayoutValuesProvider, { + isTenFootInterface = self.state.isTenFootInterface, + render = function() + return Roact.createElement("ScreenGui", { + AutoLocalize = false, + IgnoreGuiInset = true, + }, { + PremiumPromptUI = Roact.createElement(PremiumPrompt), + PurchasePromptUI = Roact.createElement(PurchasePrompt), + EventConnections = Roact.createElement(EventConnections), + }) + end + }) + }) + }) + end) +end + +return PurchasePromptApp \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePromptApp.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePromptApp.spec.lua new file mode 100644 index 0000000..4de2c47 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePromptApp.spec.lua @@ -0,0 +1,15 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local PurchasePromptApp = require(script.Parent.PurchasePromptApp) + + it("should create and destroy without errors", function() + local element = Roact.createElement(PurchasePromptApp) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/ItemType.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/ItemType.lua new file mode 100644 index 0000000..3dea2fa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/ItemType.lua @@ -0,0 +1,13 @@ +--[[ + Enumeration of types of items that can be purchased. +]] +local createEnum = require(script.Parent.createEnum) + +local ItemType = createEnum("ItemType", { + "Asset", + "GamePass", + "Product", + "Bundle", +}) + +return ItemType \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/PromptState.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/PromptState.lua new file mode 100644 index 0000000..944746a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/PromptState.lua @@ -0,0 +1,18 @@ +--[[ + Enumerated state of the purchase prompt +]] +local createEnum = require(script.Parent.createEnum) + +local PromptState = createEnum("PromptState", { + "None", + "PremiumUpsell", + "RobuxUpsell", + "PromptPurchase", + "PurchaseInProgress", + "UpsellInProgress", + "AdultConfirmation", + "PurchaseComplete", + "Error", +}) + +return PromptState diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/PurchaseError.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/PurchaseError.lua new file mode 100644 index 0000000..856b027 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/PurchaseError.lua @@ -0,0 +1,35 @@ +--[[ + Enumeration of all possible error states +]] +local createEnum = require(script.Parent.createEnum) + +local PurchaseError = createEnum("PurchaseError", { + -- Pre-purchase network failures + "CannotGetBalance", + "CannotGetItemPrice", + + -- Premium + "AlreadyPremium", + "PremiumUnavailable", + "PremiumUnavailablePlatform", + + -- Item unvailable + "NotForSale", + "AlreadyOwn", + "PremiumOnly", + "Under13", + "Limited", + "Guest", + "ThirdPartyDisabled", + "NotEnoughRobux", + "NotEnoughRobuxXbox", + "NotEnoughRobuxNoUpsell", + + -- Network-reported failures + "UnknownFailure", + "UnknownFailureNoItemName", + "PurchaseDisabled", + "InvalidFunds", +}) + +return PurchaseError \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/RequestType.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/RequestType.lua new file mode 100644 index 0000000..0cc81ff --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/RequestType.lua @@ -0,0 +1,13 @@ +local createEnum = require(script.Parent.createEnum) + +local RequestType = createEnum("RequestType", { + "None", + "Asset", + "Bundle", + "GamePass", + "Product", + "Premium", + "Subscription", +}) + +return RequestType \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/UpsellFlow.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/UpsellFlow.lua new file mode 100644 index 0000000..67ff7be --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/UpsellFlow.lua @@ -0,0 +1,11 @@ +local createEnum = require(script.Parent.createEnum) + +local UpsellFlow = createEnum("UpsellFlow", { + "Web", + "Mobile", + "Xbox", + "Unavailable", + "None", +}) + +return UpsellFlow \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/WindowState.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/WindowState.lua new file mode 100644 index 0000000..aafbb23 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/WindowState.lua @@ -0,0 +1,8 @@ +local createEnum = require(script.Parent.createEnum) + +local WindowState = createEnum("WindowState", { + "Hidden", + "Shown", +}) + +return WindowState \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/createEnum.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/createEnum.lua new file mode 100644 index 0000000..2b28cab --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/createEnum.lua @@ -0,0 +1,58 @@ +--[[ + An implementation of an enumerated type in Lua. Creates enumerated + types with uniquely identifiable values (using symbol) + + Note that resulting enum object does not associate ordinals with its + values, and cannot be iterated through. It can, however, test if a provided + value is a member of its set of values with the `isMember` function + + This is valuable for the purchase prompt because it relies heavily on + enumerated values to determine things like state and which errors occurred. +]] +local Root = script.Parent.Parent +local Symbol = require(Root.Symbols.Symbol) +local strict = require(Root.strict) + +--[[ + Returns a new enum type with the given name. +]] +local function createEnum(enumName, values) + assert(typeof(enumName) == "string", "Bad argument #1, expected string") + assert(typeof(values) == "table", "Bad argument #2, expected list of values") + + local enumInternal = {} + + for _, valueName in ipairs(values) do + assert(valueName ~= "isMember", "Shadowing 'isMember' function is not allowed") + assert(typeof(valueName) == "string", "Only string names are supported for enum types") + + local enumValue = Symbol.named(valueName) + local asString = ("%s.%s"):format(enumName, valueName) + getmetatable(enumValue).__tostring = function() + return asString + end + + enumInternal[valueName] = enumValue + end + + function enumInternal.isMember(value) + if typeof(value) ~= "userdata" then + return false + end + + for _, enumeratedValue in pairs(enumInternal) do + if value == enumeratedValue then + return true + end + end + + return false + end + + local enum = newproxy(true) + getmetatable(enum).__index = enumInternal + + return strict(enumInternal, enumName) +end + +return createEnum \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/createEnum.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/createEnum.spec.lua new file mode 100644 index 0000000..93f71ea --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/createEnum.spec.lua @@ -0,0 +1,66 @@ +return function() + local createEnum = require(script.Parent.createEnum) + + describe("validation rules", function() + it("should throw errors if given invalid values", function() + expect(function() + createEnum(1, {}) + end).to.throw() + + expect(function() + createEnum("MyEnum", "not a table") + end).to.throw() + end) + + it("should throw errors if provided table contains invalid values", function() + expect(function() + createEnum("IllegalShadowing", { + "isMember", + }) + end).to.throw() + + expect(function() + createEnum("MyEnum", { + "Test", + 12, + }) + end) + end) + end) + + describe("enum properties", function() + it("should have a reasonable string format for debugging", function() + local MyEnum = createEnum("MyEnum", { + "Value", + }) + + expect(tostring(MyEnum.Value)).to.equal("MyEnum.Value") + end) + + it("should provide an isMember function to check membership", function() + local MyEnum = createEnum("MyEnum", { + "Value", + }) + + expect(MyEnum.isMember(MyEnum.Value)).to.equal(true) + expect(MyEnum.isMember(newproxy(true))).to.equal(false) + expect(MyEnum.isMember("Value")).to.equal(false) + end) + + it("should generate objects that are unique even when named the same", function() + + local Enum1 = createEnum("MyEnum", { + "Value", + }) + local Enum2 = createEnum("MyEnum", { + "Value", + }) + + expect(Enum1.isMember(Enum1.Value)).to.equal(true) + expect(Enum2.isMember(Enum2.Value)).to.equal(true) + + expect(Enum1.isMember(Enum2.Value)).to.equal(false) + expect(Enum2.isMember(Enum1.Value)).to.equal(false) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagAdultConfirmationEnabled.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagAdultConfirmationEnabled.lua new file mode 100644 index 0000000..a1a1d27 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagAdultConfirmationEnabled.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("AdultConfirmationEnabledV4", false) + +return function() + return game:GetFastFlag("AdultConfirmationEnabledV4") +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagAdultConfirmationEnabledNew.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagAdultConfirmationEnabledNew.lua new file mode 100644 index 0000000..fc83fe4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagAdultConfirmationEnabledNew.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("AdultConfirmationEnabledNew", false) + +return function() + return game:GetFastFlag("AdultConfirmationEnabledNew") +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagDeveloperSubscriptionsEnabled.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagDeveloperSubscriptionsEnabled.lua new file mode 100644 index 0000000..4468b66 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagDeveloperSubscriptionsEnabled.lua @@ -0,0 +1,3 @@ +return function() + return settings():GetFFlag("DeveloperSubscriptionsEnabled") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagDisableRobuxUpsell.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagDisableRobuxUpsell.lua new file mode 100644 index 0000000..0cb4755 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagDisableRobuxUpsell.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("DisableRobuxUpsell", false) + +return function() + return game:GetFastFlag("DisableRobuxUpsell") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagHideThirdPartyPurchaseFailure.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagHideThirdPartyPurchaseFailure.lua new file mode 100644 index 0000000..74583b8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagHideThirdPartyPurchaseFailure.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("HideThirdPartyPurchaseFailure", false) + +return function() + return game:GetFastFlag("HideThirdPartyPurchaseFailure") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagIGPPPremiumPrice.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagIGPPPremiumPrice.lua new file mode 100644 index 0000000..2f6b0ea --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagIGPPPremiumPrice.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("IGPPPremiumPriceV2", false) + +return function() + return game:GetFastFlag("IGPPPremiumPriceV2") +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagLuaPremiumCatalogIGPP.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagLuaPremiumCatalogIGPP.lua new file mode 100644 index 0000000..5c631fb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagLuaPremiumCatalogIGPP.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("LuaPremiumCatalogIGPP", false) + +return function() + return game:GetFastFlag("LuaPremiumCatalogIGPP") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagLuaUseThirdPartyPermissions.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagLuaUseThirdPartyPermissions.lua new file mode 100644 index 0000000..31f85a6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagLuaUseThirdPartyPermissions.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("LuaUseThirdPartyPermissions", false) + +return function() + return game:GetFastFlag("LuaUseThirdPartyPermissions") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagProductPercentLocFix.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagProductPercentLocFix.lua new file mode 100644 index 0000000..54385a1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagProductPercentLocFix.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("ProductPercentLocFix", false) + +return function() + return game:GetFastFlag("ProductPercentLocFix") +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagPromptRobloxPurchaseEnabled.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagPromptRobloxPurchaseEnabled.lua new file mode 100644 index 0000000..2010872 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagPromptRobloxPurchaseEnabled.lua @@ -0,0 +1,3 @@ +return function() + return settings():GetFFlag("PromptRobloxPurchaseEnabled") +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagUpsellDirectToPackage.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagUpsellDirectToPackage.lua new file mode 100644 index 0000000..d3feac6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagUpsellDirectToPackage.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("UpsellDirectToPackage", false) + +return function() + return game:GetFastFlag("UpsellDirectToPackage") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/KeyMappings.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/KeyMappings.lua new file mode 100644 index 0000000..83c43a6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/KeyMappings.lua @@ -0,0 +1,84 @@ +local Root = script.Parent.Parent + +local PurchaseError = require(Root.Enums.PurchaseError) + +local KeyMappings = {} + +local PURCHASE_FAILED_KEY = "CoreScripts.PurchasePrompt.PurchaseFailed.%s" +local ASSET_TYPE_KEY = "Common.AssetTypes.Label.%s" + +KeyMappings.AssetTypeById = { + --[[ + This key is a special case; developer products only exist + within the context of a game, so they're localized with the + rest of the purchase prompt strings. + ]] + ["0"] = "CoreScripts.PurchasePrompt.ProductType.Product", + + --[[ + The rest of these are asset types associated with Roblox + assets that exist outside of games, mostly related to + avatar customization + ]] + ["2"] = ASSET_TYPE_KEY:format("TShirt"), + ["3"] = ASSET_TYPE_KEY:format("Audio"), + ["4"] = ASSET_TYPE_KEY:format("Mesh"), + ["8"] = ASSET_TYPE_KEY:format("Hat"), + ["9"] = ASSET_TYPE_KEY:format("Place"), + ["10"] = ASSET_TYPE_KEY:format("Model"), + ["11"] = ASSET_TYPE_KEY:format("Shirt"), + ["12"] = ASSET_TYPE_KEY:format("Pants"), + ["13"] = ASSET_TYPE_KEY:format("Decal"), + ["17"] = ASSET_TYPE_KEY:format("Head"), + ["18"] = ASSET_TYPE_KEY:format("Face"), + ["19"] = ASSET_TYPE_KEY:format("Gear"), + ["21"] = ASSET_TYPE_KEY:format("Badge"), + ["24"] = ASSET_TYPE_KEY:format("Animation"), + ["27"] = ASSET_TYPE_KEY:format("Torso"), + ["28"] = ASSET_TYPE_KEY:format("RightArm"), + ["29"] = ASSET_TYPE_KEY:format("LeftArm"), + ["30"] = ASSET_TYPE_KEY:format("LeftLeg"), + ["31"] = ASSET_TYPE_KEY:format("RightLeg"), + ["32"] = ASSET_TYPE_KEY:format("Package"), + ["34"] = ASSET_TYPE_KEY:format("GamePass"), + ["38"] = ASSET_TYPE_KEY:format("Plugin"), + ["40"] = ASSET_TYPE_KEY:format("MeshPart"), + ["41"] = ASSET_TYPE_KEY:format("Hair"), + ["42"] = ASSET_TYPE_KEY:format("Face"), + ["43"] = ASSET_TYPE_KEY:format("Neck"), + ["44"] = ASSET_TYPE_KEY:format("Shoulder"), + ["45"] = ASSET_TYPE_KEY:format("Front"), + ["46"] = ASSET_TYPE_KEY:format("Back"), + ["47"] = ASSET_TYPE_KEY:format("Waist"), + ["48"] = ASSET_TYPE_KEY:format("Climb"), + ["49"] = ASSET_TYPE_KEY:format("Death"), + ["50"] = ASSET_TYPE_KEY:format("Fall"), + ["51"] = ASSET_TYPE_KEY:format("Idle"), + ["52"] = ASSET_TYPE_KEY:format("Jump"), + ["53"] = ASSET_TYPE_KEY:format("Run"), + ["54"] = ASSET_TYPE_KEY:format("Swim"), + ["55"] = ASSET_TYPE_KEY:format("Walk"), + ["56"] = ASSET_TYPE_KEY:format("Pose"), + ["61"] = ASSET_TYPE_KEY:format("Emote"), +} + +KeyMappings.PurchaseErrorKey = { + [PurchaseError.CannotGetBalance] = PURCHASE_FAILED_KEY:format("CannotGetBalance"), + [PurchaseError.CannotGetItemPrice] = PURCHASE_FAILED_KEY:format("CannotGetItemPrice"), + [PurchaseError.NotForSale] = PURCHASE_FAILED_KEY:format("NotForSale"), + [PurchaseError.AlreadyOwn] = PURCHASE_FAILED_KEY:format("AlreadyOwn"), + [PurchaseError.Under13] = PURCHASE_FAILED_KEY:format("Under13"), + [PurchaseError.Limited] = PURCHASE_FAILED_KEY:format("Limited"), + [PurchaseError.Guest] = PURCHASE_FAILED_KEY:format("PromptPurchaseOnGuest"), + [PurchaseError.ThirdPartyDisabled] = PURCHASE_FAILED_KEY:format("ThirdPartyDisabled"), + [PurchaseError.NotEnoughRobux] = PURCHASE_FAILED_KEY:format("NotEnoughRobux"), + [PurchaseError.NotEnoughRobuxXbox] = PURCHASE_FAILED_KEY:format("NotEnoughRobuxXbox"), + [PurchaseError.NotEnoughRobuxNoUpsell] = PURCHASE_FAILED_KEY:format("NotEnoughRobuxNoUpsell"), + [PurchaseError.UnknownFailure] = PURCHASE_FAILED_KEY:format("UnknownFailure"), + [PurchaseError.UnknownFailureNoItemName] = PURCHASE_FAILED_KEY:format("UnknownFailureNoItemName"), + [PurchaseError.PurchaseDisabled] = PURCHASE_FAILED_KEY:format("PurchaseDisabled"), + [PurchaseError.InvalidFunds] = PURCHASE_FAILED_KEY:format("InvalidFunds"), + [PurchaseError.PremiumOnly] = PURCHASE_FAILED_KEY:format("PremiumOnly"), +} + +return KeyMappings \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bg-bg.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bg-bg.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bg-bg.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bn-bd.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bn-bd.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bn-bd.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bs-ba.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bs-ba.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bs-ba.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/cs-cz.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/cs-cz.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/cs-cz.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/da-dk.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/da-dk.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/da-dk.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/de-de.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/de-de.lua new file mode 100644 index 0000000..55ee19a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/de-de.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[Accessoires]], + ["Common.AssetTypes.Label.Hat"] = [[Hut]], + ["Common.AssetTypes.Label.Hair"] = [[Haare]], + ["Common.AssetTypes.Label.Face"] = [[Gesicht]], + ["Common.AssetTypes.Label.Neck"] = [[Hals]], + ["Common.AssetTypes.Label.Shoulder"] = [[Schulter]], + ["Common.AssetTypes.Label.Front"] = [[Vorderseite]], + ["Common.AssetTypes.Label.Back"] = [[Rückseite]], + ["Common.AssetTypes.Label.Waist"] = [[Taille]], + ["Common.AssetTypes.Label.Animations"] = [[Animationen]], + ["Common.AssetTypes.Label.Audio"] = [[Audiodateien]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[Avataranimationen]], + ["Common.AssetTypes.Label.Badges"] = [[Abzeichen]], + ["Common.AssetTypes.Label.Decals"] = [[Decals]], + ["Common.AssetTypes.Label.Faces"] = [[Gesichter]], + ["Common.AssetTypes.Label.GamePasses"] = [[Spielpässe]], + ["Common.AssetTypes.Label.Gear"] = [[Ausrüstung]], + ["Common.AssetTypes.Label.Heads"] = [[Köpfe]], + ["Common.AssetTypes.Label.Meshes"] = [[Meshes]], + ["Common.AssetTypes.Label.Models"] = [[Modelle]], + ["Common.AssetTypes.Label.Packages"] = [[Pakete]], + ["Common.AssetTypes.Label.Pants"] = [[Hosen]], + ["Common.AssetTypes.Label.Places"] = [[Orte]], + ["Common.AssetTypes.Label.Plugins"] = [[Plug-ins]], + ["Common.AssetTypes.Label.Shirts"] = [[Hemden]], + ["Common.AssetTypes.Label.TShirts"] = [[T-Shirts]], + ["Common.AssetTypes.Label.VipServers"] = [[VIP-Server]], + ["Common.AssetTypes.Label.Run"] = [[Laufen]], + ["Common.AssetTypes.Label.Walk"] = [[Gehen]], + ["Common.AssetTypes.Label.Fall"] = [[Fallen]], + ["Common.AssetTypes.Label.Jump"] = [[Springen]], + ["Common.AssetTypes.Label.Idle"] = [[Untätig]], + ["Common.AssetTypes.Label.Swim"] = [[Schwimmen]], + ["Common.AssetTypes.Label.Climb"] = [[Klettern]], + ["Common.AssetTypes.Label.Hats"] = [[Hüte]], + ["Common.AssetTypes.Label.Shoulders"] = [[Schultern]], + ["Common.AssetTypes.Label.Death"] = [[Tod]], + ["Common.AssetTypes.Label.Pose"] = [[Pose]], + ["Common.AssetTypes.Label.Head"] = [[Kopf]], + ["Common.AssetTypes.Label.TShirt"] = [[T-Shirt]], + ["Common.AssetTypes.Label.Shirt"] = [[Hemd]], + ["Common.AssetTypes.Label.Decal"] = [[Decal]], + ["Common.AssetTypes.Label.Model"] = [[Modell]], + ["Common.AssetTypes.Label.Plugin"] = [[Plug-in]], + ["Common.AssetTypes.Label.MeshPart"] = [[Mesh-Teil]], + ["Common.AssetTypes.Label.GamePass"] = [[Spielpass]], + ["Common.AssetTypes.Label.Badge"] = [[Abzeichen]], + ["Common.AssetTypes.Label.Package"] = [[Paket]], + ["Common.AssetTypes.Label.Place"] = [[Ort]], + ["Common.AssetTypes.Label.LeftArm"] = [[Linker Arm]], + ["Common.AssetTypes.Label.LeftLeg"] = [[Linkes Bein]], + ["Common.AssetTypes.Label.RightArm"] = [[Rechter Arm]], + ["Common.AssetTypes.Label.RightLeg"] = [[Rechtes Bein]], + ["Common.AssetTypes.Label.Torso"] = [[Torso]], + ["Common.AssetTypes.Label.Animation"] = [[Animation]], + ["Common.AssetTypes.Label.Emote"] = [[Emote]], + ["Common.BuildersClub.Label.PlanFree"] = [[Gratis]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Klassisch]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[„Builders Club“-Mitgliedschaft]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[„Turbo Builders Club“-Mitgliedschaft]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[„Outrageous Builders Club“-Mitgliedschaft]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[Ja]], + ["Common.BuildersClub.Label.No"] = [[Nein]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[NIEMALS]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[Auf Lebenszeit]], + ["Common.BuildersClub.Label.Membership"] = [[Mitgliedschaft]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Premium erforderlich]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Durch Roblox Premium erhältst du:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[450 Robux pro Monat]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Zugriff auf exklusive Premium-Vorteile]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[10% Bonus beim Kauf von Robux]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price}/Monat]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[Der Kauf von Roblox Premium wird auf deiner Plattform nicht unterstützt. Bitte verwende deinen Desktop-Computer, um Premium zu kaufen.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[Der Entwickler versucht, dich dazu aufzufordern, Roblox Premium zu kaufen, aber du bist bereits ein Premium-Mitglied!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[Premium ist derzeit nicht verfügbar. Bitte versuche es später wieder!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[{robux} Robux im Monat]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[Kauf wurde nicht abgeschlossen, bitte versuche es erneut.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[Fehler]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[Gratis nehmen]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[Aufwerten]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[Jetzt kaufen]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[R$ kaufen]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[Abbrechen]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[Okay]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[Wird gekauft ...]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[Das Guthaben deines Kontos wird durch diese Transaktion nicht beeinflusst.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[Dies ist ein Testkauf. Dein Konto wird dadurch nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[Dies war ein Testkauf.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[Dieser Artikel erfordert {BC_LEVEL}. Klicke auf „Aufwerten“, um deinen Builders-Club-Status zu verbessern!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[Nach dieser Transaktion wird dein Guthaben {BALANCE_FUTURE} R$ betragen.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[Dein Guthaben beträgt nun {BALANCE_NOW}.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[Die verbleibenden {REMAINING_ROBUX} Robux werden deinem Guthaben gutgeschrieben.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[Dein Kauf ist fehlgeschlagen, da dein Konto nicht über genügend Robux verfügt. Dein Konto wurde nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[Dein Kauf ist fehlgeschlagen, da du ein Abonnement benötigst, um diesen Artikel zu kaufen. Dein Konto wurde nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[Dieser Artikel kostet mehr Robux, als dir zur Verfügung stehen. Bitte verlasse dieses Spiel, gehe zum Robux-Menü und kaufe dort mehr.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[Dieser Artikel kostet mehr Robux, als du kaufen kannst. Bitte besuche www.roblox.com, um mehr Robux zu kaufen.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[Dein Kauf ist fehlgeschlagen, da Käufe im Spiel derzeit deaktiviert sind. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[Dein Kauf ist fehlgeschlagen, da ein Problem aufgetreten ist. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[Dein Kauf von „{ITEM_NAME}“ ist fehlgeschlagen, da ein Problem aufgetreten ist. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[Dein Guthaben kann derzeit nicht abgerufen werden. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[Der Preis des Artikels kann derzeit nicht abgerufen werden. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[Von diesem limitierten Artikel gibt es keine weiteren Exemplare. Such auf www.roblox.com nach einem anderen Verkäufer. Dein Konto wurde nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[Dieser Artikel steht derzeit nicht zum Verkauf. Dein Konto wurde nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[Du musst ein Roblox-Konto erstellen, um Artikel zu kaufen. Auf www.roblox.com findest du weitere Infos.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[Artikel von Drittanbietern können an diesem Ort nicht verkauft werden. Dein Konto wurde nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[Dein Konto ist für Spieler unter 13 Jahren. Der Kauf dieses Artikels ist nicht gestattet. Dein Konto wurde nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[Diesen Artikel besitzt du bereits. Dein Konto wurde nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[Möchtest du „{ITEM_NAME}“ gerne GRATIS nehmen?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[Möchtest du „{ITEM_NAME}“ ({ASSET_TYPE}) kaufen für]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[Du hast „{ITEM_NAME}“ gekauft!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[Du brauchst noch {NEEDED_AMOUNT} Robux, um „{ITEM_NAME}“ ({ASSET_TYPE}) zu kaufen. Möchtest du mehr Robux kaufen?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[Produkt]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[Paket]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[Ja]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[Abbrechen]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[Du verlängerst dieses Abonnement derzeit nicht. Deine Abonnements haben sich nicht geändert.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[Du hast dein {ITEM_NAME} -Abonnement gekündigt. Du behältst alle Leistungen bis zum Ende der Zahlungsperiode.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[Möchtest du wirklich dein {ITEM_NAME}-Abonnement kündigen? Du behältst alle Leistungen bis zum Ende der Zahlungsperiode.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[Möchtest du das {ITEM_NAME}-Abonnement {ITEM_NAME} abonnieren? Kosten:]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[Abonnement]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[{PRICE} pro Monat]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[Abonnieren]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[Du musst Premium-Mitglied sein, um dich anzumelden!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[Aufwerten]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[Dein Kauf ist fehlgeschlagen, da du Premium-Mitglied sein musst, um dich anzumelden. Dein Konto wurde nicht belastet]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Robux kaufen]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[Dein Kontostand nach dieser Transaktion beträgt {BALANCE_FUTURE} Robux]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[Dieser Gegenstand kostet mehr Robux, als dir zur Verfügung stehen. ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Nur Premium]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[Du musst Roblox Premium abonniert haben, um diesen Gegenstand zu kaufen.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[Dieser Kauf involviert den Austausch von echtem Geld. Ich bestätige, dass ich mindestens 18 Jahre alt bin und dass ich ein Elternteil oder Erziehungsberechtigter des Kontoinhabers bin. Ich genehmige diesen Kauf und stimme den Nutzungsbedingungen zu.]], +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/el-gr.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/el-gr.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/el-gr.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/en-us.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/en-us.lua new file mode 100644 index 0000000..7583958 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/en-us.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[Accessories]], + ["Common.AssetTypes.Label.Hat"] = [[Hat]], + ["Common.AssetTypes.Label.Hair"] = [[Hair]], + ["Common.AssetTypes.Label.Face"] = [[Face]], + ["Common.AssetTypes.Label.Neck"] = [[Neck]], + ["Common.AssetTypes.Label.Shoulder"] = [[Shoulder]], + ["Common.AssetTypes.Label.Front"] = [[Front]], + ["Common.AssetTypes.Label.Back"] = [[Back]], + ["Common.AssetTypes.Label.Waist"] = [[Waist]], + ["Common.AssetTypes.Label.Animations"] = [[Animations]], + ["Common.AssetTypes.Label.Audio"] = [[Audio]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[Avatar Animations]], + ["Common.AssetTypes.Label.Badges"] = [[Badges]], + ["Common.AssetTypes.Label.Decals"] = [[Decals]], + ["Common.AssetTypes.Label.Faces"] = [[Faces]], + ["Common.AssetTypes.Label.GamePasses"] = [[Game Passes]], + ["Common.AssetTypes.Label.Gear"] = [[Gear]], + ["Common.AssetTypes.Label.Heads"] = [[Heads]], + ["Common.AssetTypes.Label.Meshes"] = [[Meshes]], + ["Common.AssetTypes.Label.Models"] = [[Models]], + ["Common.AssetTypes.Label.Packages"] = [[Packages]], + ["Common.AssetTypes.Label.Pants"] = [[Pants]], + ["Common.AssetTypes.Label.Places"] = [[Places]], + ["Common.AssetTypes.Label.Plugins"] = [[Plugins]], + ["Common.AssetTypes.Label.Shirts"] = [[Shirts]], + ["Common.AssetTypes.Label.TShirts"] = [[T-Shirts]], + ["Common.AssetTypes.Label.VipServers"] = [[VIP Servers]], + ["Common.AssetTypes.Label.Run"] = [[Run]], + ["Common.AssetTypes.Label.Walk"] = [[Walk]], + ["Common.AssetTypes.Label.Fall"] = [[Fall]], + ["Common.AssetTypes.Label.Jump"] = [[Jump]], + ["Common.AssetTypes.Label.Idle"] = [[Idle]], + ["Common.AssetTypes.Label.Swim"] = [[Swim]], + ["Common.AssetTypes.Label.Climb"] = [[Climb]], + ["Common.AssetTypes.Label.Hats"] = [[Hats]], + ["Common.AssetTypes.Label.Shoulders"] = [[Shoulders]], + ["Common.AssetTypes.Label.Death"] = [[Death]], + ["Common.AssetTypes.Label.Pose"] = [[Pose]], + ["Common.AssetTypes.Label.Head"] = [[Head]], + ["Common.AssetTypes.Label.TShirt"] = [[T-Shirt]], + ["Common.AssetTypes.Label.Shirt"] = [[Shirt]], + ["Common.AssetTypes.Label.Decal"] = [[Decal]], + ["Common.AssetTypes.Label.Model"] = [[Model]], + ["Common.AssetTypes.Label.Plugin"] = [[Plugin]], + ["Common.AssetTypes.Label.MeshPart"] = [[Mesh Part]], + ["Common.AssetTypes.Label.GamePass"] = [[Game Pass]], + ["Common.AssetTypes.Label.Badge"] = [[Badge]], + ["Common.AssetTypes.Label.Package"] = [[Package]], + ["Common.AssetTypes.Label.Place"] = [[Place]], + ["Common.AssetTypes.Label.LeftArm"] = [[Left Arm]], + ["Common.AssetTypes.Label.LeftLeg"] = [[Left Leg]], + ["Common.AssetTypes.Label.RightArm"] = [[Right Arm]], + ["Common.AssetTypes.Label.RightLeg"] = [[Right Leg]], + ["Common.AssetTypes.Label.Torso"] = [[Torso]], + ["Common.AssetTypes.Label.Animation"] = [[Animation]], + ["Common.AssetTypes.Label.Emote"] = [[Emote]], + ["Common.BuildersClub.Label.PlanFree"] = [[Free]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Classic]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Builders Club Membership]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Turbo Builders Club Membership]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Outrageous Builders Club Membership]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[Yes]], + ["Common.BuildersClub.Label.No"] = [[No]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[NEVER]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[Lifetime]], + ["Common.BuildersClub.Label.Membership"] = [[Membership]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Premium Required]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Roblox Premium will get you:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[450 Robux per month]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Access to Premium only benefits]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[10% bonus when buying Robux]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price}/month]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[Purchasing Roblox Premium is not supported on your platform. Please use your desktop to purchase Premium.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[Looks like the developer is trying to prompt you to purchase Roblox Premium, but you are already a Premium member!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[Premium is unavailable at the moment. Please again try later!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[{robux} Robux per month]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[Purchase was not complete, please try again.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[Error]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[Take Free]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[Upgrade]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[Buy Now]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[Buy R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[Cancel]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[OK]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[Purchasing]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[Your account balance will not be affected by this transaction.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[This is a test purchase; your account will not be charged.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[This was a test purchase.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[This item requires {BC_LEVEL}. Click 'Upgrade' to upgrade your Builders Club!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[Your balance after this transaction will be R${BALANCE_FUTURE}]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[Your balance is now {BALANCE_NOW}.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[The remaining {REMAINING_ROBUX} Robux will be credited to your balance.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[Your purchase failed because your account does not have enough Robux. Your account has not been charged.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[Your purchase failed because you need a subscription to purchase this item. Your account has not been charged.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[This item cost more Robux than you have available. Please leave this game and go to the Robux screen to purchase more.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[This item cost more Robux than you can purchase. Please visit www.roblox.com to purchase more Robux.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[Your purchase failed because In-game purchases are temporarily disabled. Your account has not been charged. Please try again later.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[Your purchase failed because something went wrong. Your account has not been charged. Please try again later.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[Your purchase of {ITEM_NAME} failed because something went wrong. Your account has not been charged. Please try again later.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[Cannot retrieve your balance at this time. Your account has not been charged. Please try again later.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[We couldn't retrieve the price of the item at this time. Your account has not been charged. Please try again later.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[This limited item has no more copies. Try buying from another user on www.roblox.com. Your account has not been charged.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[This item is not currently for sale. Your account has not been charged.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[You need to create a ROBLOX account to buy items, visit www.roblox.com for more info.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[Third-party item sales have been disabled for this place. Your account has not been charged.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[Your account is under 13. Purchase of this item is not allowed. Your account has not been charged.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[You already own this item. Your account has not been charged.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[Would you like to take {ITEM_NAME} for FREE?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[Want to buy the {ASSET_TYPE} {ITEM_NAME} for]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[Your purchase of {ITEM_NAME} succeeded!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[You need {NEEDED_AMOUNT} more Robux to buy the {ASSET_TYPE} {ITEM_NAME}. Would you like to buy more Robux?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[Product]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[Bundle]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[Yes]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[Canceling]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[You aren't currently renewing this subscription. Your subscriptions have not changed.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[You've canceled your {ITEM_NAME} subscription. You will retain your benefits until the end of the pay period.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[Are you sure you would like to cancel your {ITEM_NAME} subscription? You will retain your benefits until the end of the pay period.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[Want to subscribe to the subscription {ITEM_NAME} for]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[Subscription]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[{PRICE} per month]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[Subscribe]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[You must be a Premium member in order to subscribe!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[Upgrade]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[Your purchase failed because you need to be a Premium member to subscribe. Your account has not been charged]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Buy Robux]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[Your balance after this transaction will be {BALANCE_FUTURE} Robux]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[This item cost more Robux than you have available. ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Premium Only]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[You need to have Roblox Premium in order to purchase this item.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[This purchase involves the exchange of real money. I agree that I am at least 18 years of age, and am the parent or legal guardian of the account owner. I authorize this purchase and agree to the Terms of Service.]], +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/es-es.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/es-es.lua new file mode 100644 index 0000000..7c77b7a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/es-es.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[Accesorios]], + ["Common.AssetTypes.Label.Hat"] = [[Sombrero]], + ["Common.AssetTypes.Label.Hair"] = [[Pelo]], + ["Common.AssetTypes.Label.Face"] = [[Cara]], + ["Common.AssetTypes.Label.Neck"] = [[Cuello]], + ["Common.AssetTypes.Label.Shoulder"] = [[Hombro]], + ["Common.AssetTypes.Label.Front"] = [[Frontal]], + ["Common.AssetTypes.Label.Back"] = [[Trasero]], + ["Common.AssetTypes.Label.Waist"] = [[Cintura]], + ["Common.AssetTypes.Label.Animations"] = [[Animaciones]], + ["Common.AssetTypes.Label.Audio"] = [[Sonidos]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[Animaciones de avatar]], + ["Common.AssetTypes.Label.Badges"] = [[Emblemas]], + ["Common.AssetTypes.Label.Decals"] = [[Adhesivos]], + ["Common.AssetTypes.Label.Faces"] = [[Caras]], + ["Common.AssetTypes.Label.GamePasses"] = [[Pases del juego]], + ["Common.AssetTypes.Label.Gear"] = [[Equipamiento]], + ["Common.AssetTypes.Label.Heads"] = [[Cabezas]], + ["Common.AssetTypes.Label.Meshes"] = [[Mallas]], + ["Common.AssetTypes.Label.Models"] = [[Modelos]], + ["Common.AssetTypes.Label.Packages"] = [[Paquetes]], + ["Common.AssetTypes.Label.Pants"] = [[Pantalones]], + ["Common.AssetTypes.Label.Places"] = [[Lugares]], + ["Common.AssetTypes.Label.Plugins"] = [[Complementos]], + ["Common.AssetTypes.Label.Shirts"] = [[Camisas]], + ["Common.AssetTypes.Label.TShirts"] = [[Camisetas]], + ["Common.AssetTypes.Label.VipServers"] = [[Servidores VIP]], + ["Common.AssetTypes.Label.Run"] = [[Carrera]], + ["Common.AssetTypes.Label.Walk"] = [[Marcha]], + ["Common.AssetTypes.Label.Fall"] = [[Caída]], + ["Common.AssetTypes.Label.Jump"] = [[Salto]], + ["Common.AssetTypes.Label.Idle"] = [[Inactividad]], + ["Common.AssetTypes.Label.Swim"] = [[Nado]], + ["Common.AssetTypes.Label.Climb"] = [[Escalada]], + ["Common.AssetTypes.Label.Hats"] = [[Sombreros]], + ["Common.AssetTypes.Label.Shoulders"] = [[Hombros]], + ["Common.AssetTypes.Label.Death"] = [[La muerte]], + ["Common.AssetTypes.Label.Pose"] = [[Pose]], + ["Common.AssetTypes.Label.Head"] = [[Cabeza]], + ["Common.AssetTypes.Label.TShirt"] = [[Camiseta]], + ["Common.AssetTypes.Label.Shirt"] = [[Camisa]], + ["Common.AssetTypes.Label.Decal"] = [[Adhesivo]], + ["Common.AssetTypes.Label.Model"] = [[Modelo]], + ["Common.AssetTypes.Label.Plugin"] = [[Complemento]], + ["Common.AssetTypes.Label.MeshPart"] = [[Parte de la malla]], + ["Common.AssetTypes.Label.GamePass"] = [[Pase del juego]], + ["Common.AssetTypes.Label.Badge"] = [[Emblema]], + ["Common.AssetTypes.Label.Package"] = [[Paquete]], + ["Common.AssetTypes.Label.Place"] = [[Lugar]], + ["Common.AssetTypes.Label.LeftArm"] = [[Brazo izquierdo]], + ["Common.AssetTypes.Label.LeftLeg"] = [[Pierna izquierda]], + ["Common.AssetTypes.Label.RightArm"] = [[Brazo derecho]], + ["Common.AssetTypes.Label.RightLeg"] = [[Pierna derecha]], + ["Common.AssetTypes.Label.Torso"] = [[Torso]], + ["Common.AssetTypes.Label.Animation"] = [[Animación]], + ["Common.AssetTypes.Label.Emote"] = [[Emote]], + ["Common.BuildersClub.Label.PlanFree"] = [[Gratis]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Clásico]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Suscripción al Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Suscripción al Turbo Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Suscripción al Outrageous Builders Club]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[Sí]], + ["Common.BuildersClub.Label.No"] = [[No]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[NUNCA]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Builders Club Clásico]], + ["Common.BuildersClub.Label.Lifetime"] = [[Vitalicia]], + ["Common.BuildersClub.Label.Membership"] = [[Suscripción]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Se requiere Premium]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Con Roblox Premium también obtienes:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[450 Robux al mes]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Acceso a beneficios exclusivos para suscriptores de Premium]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[10% de Robux extra en la compra de nuestra moneda virtual]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price} al mes]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[La compra de Roblox Premium no es compatible con tu plataforma. Usa un equipo de escritorio para suscribirte.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[Parece que el desarrollador te quiere motivar a que te suscribas a Premium, pero tú ya tienes una suscripción a Premium.]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[Premium no está disponible en este momento. Inténtalo más tarde.]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[{robux} Robux al mes]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[No se ha finalizado la compra. Inténtalo de nuevo.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[Error]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[Llévatelo gratis]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[Mejorar]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[Comprar ahora]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[Comprar R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[Cancelar]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[Aceptar]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[Comprando]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[Esta operación no afectará tu saldo.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[Esta es una compra de prueba; no se te cobrará por esta operación.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[Esta fue una compra de prueba.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[Para comprar este objeto se requiere {BC_LEVEL}. Haz clic en Mejorar para pasar a un nivel de suscripción superior del Builders Club.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[Tu saldo después de esta transacción será de R${BALANCE_FUTURE}]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[Tu saldo es de {BALANCE_NOW}.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[Los {REMAINING_ROBUX} Robux restantes se abonarán a tu saldo.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[La compra no se ha realizado porque la cuenta no tiene suficientes Robux. No se te ha cobrado.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[La compra no se ha realizado porque se necesita una suscripción para adquirir este objeto. No se ha cobrado.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[No tienes suficientes Robux para comprar este objeto. Sal del juego y dirígete a la pantalla de Robux para obtener más.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[No tienes suficientes Robux para comprar este objeto. Visita la página www.roblox.com para obtener más Robux.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[La compra no se ha realizado porque las compras dentro del juego están desactivadas temporalmente. No se te ha cobrado. Inténtalo de nuevo más tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[La compra no se ha realizado porque algo ha ido mal. No se te ha cobrado. Inténtalo de nuevo más tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[La compra de {ITEM_NAME} no se ha realizado porque algo ha ido mal. No se te ha cobrado. Inténtalo de nuevo más tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[No se ha podido recuperar tu saldo en este momento. No se te ha cobrado. Inténtalo de nuevo más tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[No se ha podido recuperar el precio de este objeto en este momento. No se te ha cobrado. Inténtalo de nuevo más tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[No hay más copias disponibles de este objeto de edición limitada. Intenta comprarlo de otro usuario en www.roblox.com. No se te ha cobrado.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[Este objeto no está en venta en este momento. No se te ha cobrado.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[Tienes que crear una cuenta de Roblox para comprar objetos. Visita www.roblox.com para más información.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[Se ha desactivado la venta de objetos de terceros para este lugar. No se te ha cobrado.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[Tu cuenta es para menores de 13 años. No se permite la compra de este objeto. No se te ha cobrado.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[Ya tienes este objeto. No se te ha cobrado.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[El objeto {ITEM_NAME} es gratuito. ¿Te lo quieres llevar?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[¿Quieres comprar {ASSET_TYPE} {ITEM_NAME} por]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[¡La compra de {ITEM_NAME} se ha realizado correctamente!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[Se necesitan {NEEDED_AMOUNT} Robux más para comprar {ASSET_TYPE} {ITEM_NAME}. ¿Quieres obtener más Robux?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[Artículos]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[Paquete]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[Sí]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[Cancelando]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[No estás renovando la suscripción. Esta no ha cambiado.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[Has cancelado tu suscripción de {ITEM_NAME}. Mantendrás todos los beneficios hasta el final del periodo pagado.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[¿Seguro que quieres cancelar la suscripción de {ITEM_NAME}? Mantendrás todos tus beneficios hasta el final del periodo pagado.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[¿Quieres suscribirte a {ITEM_NAME} para]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[Suscripción]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[{PRICE} por mes]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[Subscribir]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[Debes ser miembro de Premium para suscribirte.]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[Mejorar]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[La compra no se ha realizado porque necesitas ser miembro de Premium para suscribirte. No se te ha cobrado]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Comprar Robux]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[Tu saldo después de esta transacción será de {BALANCE_FUTURE} Robux]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[No tienes suficientes Robux para comprar este objeto. ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Solo Premium]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[Necesitas Roblox Premium para comprar este objeto.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[Esta compra implica el intercambio de moneda real. Certifico que soy mayor de 18 años y que soy el progenitor o tutor del propietario de la cuenta. Autorizo esta compra y acepto los Términos de servicio.]], +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/et-ee.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/et-ee.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/et-ee.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fi-fi.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fi-fi.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fi-fi.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fil-ph.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fil-ph.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fil-ph.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fr-fr.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fr-fr.lua new file mode 100644 index 0000000..90a45ae --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fr-fr.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[Accessoires]], + ["Common.AssetTypes.Label.Hat"] = [[Chapeau]], + ["Common.AssetTypes.Label.Hair"] = [[Cheveux]], + ["Common.AssetTypes.Label.Face"] = [[Visage]], + ["Common.AssetTypes.Label.Neck"] = [[Cou]], + ["Common.AssetTypes.Label.Shoulder"] = [[Épaules]], + ["Common.AssetTypes.Label.Front"] = [[Avant]], + ["Common.AssetTypes.Label.Back"] = [[Retour]], + ["Common.AssetTypes.Label.Waist"] = [[Taille]], + ["Common.AssetTypes.Label.Animations"] = [[Animations]], + ["Common.AssetTypes.Label.Audio"] = [[Audio]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[Animations d'avatar]], + ["Common.AssetTypes.Label.Badges"] = [[Badges]], + ["Common.AssetTypes.Label.Decals"] = [[Insignes]], + ["Common.AssetTypes.Label.Faces"] = [[Visages]], + ["Common.AssetTypes.Label.GamePasses"] = [[Passes de jeu]], + ["Common.AssetTypes.Label.Gear"] = [[Équipement]], + ["Common.AssetTypes.Label.Heads"] = [[Têtes]], + ["Common.AssetTypes.Label.Meshes"] = [[Maillages]], + ["Common.AssetTypes.Label.Models"] = [[Modèles]], + ["Common.AssetTypes.Label.Packages"] = [[Packs]], + ["Common.AssetTypes.Label.Pants"] = [[Pantalons]], + ["Common.AssetTypes.Label.Places"] = [[Emplacements]], + ["Common.AssetTypes.Label.Plugins"] = [[Plugins]], + ["Common.AssetTypes.Label.Shirts"] = [[Chemises]], + ["Common.AssetTypes.Label.TShirts"] = [[Tee-shirts]], + ["Common.AssetTypes.Label.VipServers"] = [[Serveurs VIP]], + ["Common.AssetTypes.Label.Run"] = [[Course]], + ["Common.AssetTypes.Label.Walk"] = [[Marche]], + ["Common.AssetTypes.Label.Fall"] = [[Chute]], + ["Common.AssetTypes.Label.Jump"] = [[Saut]], + ["Common.AssetTypes.Label.Idle"] = [[Inaction]], + ["Common.AssetTypes.Label.Swim"] = [[Nage]], + ["Common.AssetTypes.Label.Climb"] = [[Escalade]], + ["Common.AssetTypes.Label.Hats"] = [[Chapeaux]], + ["Common.AssetTypes.Label.Shoulders"] = [[Épaules]], + ["Common.AssetTypes.Label.Death"] = [[Mort]], + ["Common.AssetTypes.Label.Pose"] = [[Pose]], + ["Common.AssetTypes.Label.Head"] = [[Tête]], + ["Common.AssetTypes.Label.TShirt"] = [[Tee-shirt]], + ["Common.AssetTypes.Label.Shirt"] = [[Chemise]], + ["Common.AssetTypes.Label.Decal"] = [[Insigne]], + ["Common.AssetTypes.Label.Model"] = [[Modèle]], + ["Common.AssetTypes.Label.Plugin"] = [[Plugin]], + ["Common.AssetTypes.Label.MeshPart"] = [[Partie de maillage]], + ["Common.AssetTypes.Label.GamePass"] = [[Passe de jeu]], + ["Common.AssetTypes.Label.Badge"] = [[Badge]], + ["Common.AssetTypes.Label.Package"] = [[Pack]], + ["Common.AssetTypes.Label.Place"] = [[Emplacement]], + ["Common.AssetTypes.Label.LeftArm"] = [[Bras gauche]], + ["Common.AssetTypes.Label.LeftLeg"] = [[Jambe gauche]], + ["Common.AssetTypes.Label.RightArm"] = [[Bras droit]], + ["Common.AssetTypes.Label.RightLeg"] = [[Jambe droite]], + ["Common.AssetTypes.Label.Torso"] = [[Torse]], + ["Common.AssetTypes.Label.Animation"] = [[Animation]], + ["Common.AssetTypes.Label.Emote"] = [[Emote]], + ["Common.BuildersClub.Label.PlanFree"] = [[Gratuit]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Classique]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Abonnement au Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Abonnement au Turbo Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Abonnement à l'Outrageous Builders Club]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[Oui]], + ["Common.BuildersClub.Label.No"] = [[Non]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[JAMAIS]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[À vie]], + ["Common.BuildersClub.Label.Membership"] = [[Abonnement]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Niveau Premium requis]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Avec Roblox Premium, vous avez droit à :]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[450 Robux par mois]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Accédez à des avantages réservés aux membres Premium]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[10 % de bonus lors de l'achat de Robux]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price} par mois]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[L'achat de Roblox Premium n'est pas pris en charge sur votre plateforme. Veuillez utiliser un ordinateur pour l'acheter.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[Il semble que le dévelopeur essaie de vous inviter à acheter Roblox Premium mais vous êtes déjà une membre Premium !]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[Le niveau Premium n'est pas disponible pour le moment. Réessayez plus tard !]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[{robux} Robux par mois]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[L'achat n'est pas finalisé, réessaie s'il te plaît.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[Erreur]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[Prendre gratuitement]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[Améliorer]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[Acheter maintenant]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[Acheter des R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[Annuler]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[OK]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[Achat]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[Le solde de votre compte ne sera pas affecté par cette transaction.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[Ceci est un achat test ; votre compte ne sera pas débité.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[C'était un achat test.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[Cet objet nécessite {BC_LEVEL}. Cliquez sur « Améliorer » pour améliorer votre Builders Club !]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[Votre solde après cette transaction sera de {BALANCE_FUTURE} R$]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[Votre solde est désormais de {BALANCE_NOW}.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[Les {REMAINING_ROBUX} Robux restants seront portés à votre solde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[Échec de la transaction. Motif : votre compte ne possède pas assez de Robux. Votre compte n'a pas été débité.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[Échec de la transaction. Motif : il vous faut un abonnement pour acheter cet objet. Votre compte n'a pas été débité.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[Cette objet coûte plus de Robux que vous n'en avez. Veuillez quitter le jeu et aller à l'écran Robux pour en acheter plus.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[Cet objet coûte trop de Robux pour que vous l'achetiez. Veuillez vous rendre sur www.roblox.com pour acheter plus de Robux.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[Échec de la transaction. Motif : les achats en jeu sont temporairement désactivés. Votre compte n'a pas été débité. Veuillez réessayer plus tard.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[Échec de la transaction. Motif : un problème est survenu. Votre compte n'a pas été débité. Veuillez réessayer plus tard.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[Votre achat de {ITEM_NAME} a échoué. Motif : un problème est survenu. Votre compte n'a pas été débité. Veuillez réessayer plus tard.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[Impossible d'obtenir votre solde pour l'instant. Votre compte n'a pas été débité. Veuillez réessayer plus tard.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[Nous ne pouvons pas récupérer le prix de cet objet pour l'instant. Votre compte n'a pas été débité. Veuillez réessayer plus tard.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[Il n'y a plus d'exemplaires de cet objet en série limitée. Essayez de l'acheter à un autre utilisateur sur www.roblox.com. Votre compte n'a pas été débité.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[Cet objet n'est pas en vente pour l'instant. Votre compte n'a pas été débité.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[Vous devez créer un compte ROBLOX pour acheter des objets, rendez-vous sur www.roblox.com pour plus d'informations.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[Les ventes d'objets par des tiers ont été désactivées pour cet emplacement. Votre compte n'a pas été débité.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[Le propriétaire de ce compte a moins de 13 ans. L'achat de cet objet n'est pas permis. Votre compte n'a pas été débité.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[Vous possédez déjà cet objet. Votre compte n'a pas été débité.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[Souhaitez-vous prendre l'objet {ITEM_NAME} GRATUITEMENT ?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[Vous voulez acheter le {ASSET_TYPE} {ITEM_NAME} pour]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[Votre achat de {ITEM_NAME} a réussi !]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[Vous avez besoin de {NEEDED_AMOUNT} Robux de plus pour acheter le {ASSET_TYPE} {ITEM_NAME}. Souhaitez-vous acheter plus de Robux ?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[Produit]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[Paquet]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[Oui]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[Annulation en cours]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[Tu n'as pas prévu de renouveller cet abonnement. Tes abonnements n'ont pas changé.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[Tu as annulé ton abonnement à {ITEM_NAME}. Tu garderas tes avantages jusqu'à la fin de la période payée.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[Es-tu sûr-e de vouloir annuler ton abonnement à {ITEM_NAME} ? Tu garderas tes avantages jusqu'à la fin de la période payée.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[Tu veux t'abonner à {ITEM_NAME} pour]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[Abonnement]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[{PRICE} par mois]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[S'inscrire]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[Tu dois être un membre Premium pour t'inscrire !]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[Actualiser]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[Échec de la transaction. Motif : il te faut un abonnement premium pour acheter cet objet. Ton compte n'a pas été débité]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Acheter des Robux]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[Ton solde après cette transaction sera de {BALANCE_FUTURE} Robux.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[Cette objet coûte plus de Robux que vous n'en avez. ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Premium uniquement]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[Tu dois avoir Roblox Premium pour acheter cet article.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[Cet achat implique l'échange d'argent réel. Je certifie avoir au moins 18 ans et être le parent ou le tuteur légal du propriétaire du compte. J'autorise cet achat et j'accepte les conditions de service.]], +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hi-in.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hi-in.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hi-in.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hr-hr.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hr-hr.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hr-hr.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hu-hu.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hu-hu.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hu-hu.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/id-id.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/id-id.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/id-id.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/it-it.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/it-it.lua new file mode 100644 index 0000000..d066f0b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/it-it.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[Accessori]], + ["Common.AssetTypes.Label.Hat"] = [[Cappello]], + ["Common.AssetTypes.Label.Hair"] = [[Capelli]], + ["Common.AssetTypes.Label.Face"] = [[Faccia]], + ["Common.AssetTypes.Label.Neck"] = [[Collo]], + ["Common.AssetTypes.Label.Shoulder"] = [[Spalle]], + ["Common.AssetTypes.Label.Front"] = [[Fronte]], + ["Common.AssetTypes.Label.Back"] = [[Indietro]], + ["Common.AssetTypes.Label.Waist"] = [[Vita]], + ["Common.AssetTypes.Label.Animations"] = [[Animazioni]], + ["Common.AssetTypes.Label.Audio"] = [[Audio]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[Animazioni avatar]], + ["Common.AssetTypes.Label.Badges"] = [[Contrassegni]], + ["Common.AssetTypes.Label.Decals"] = [[Decalcomanie]], + ["Common.AssetTypes.Label.Faces"] = [[Facce]], + ["Common.AssetTypes.Label.GamePasses"] = [[Pass di gioco]], + ["Common.AssetTypes.Label.Gear"] = [[Attrezzatura]], + ["Common.AssetTypes.Label.Heads"] = [[Teste]], + ["Common.AssetTypes.Label.Meshes"] = [[Mesh]], + ["Common.AssetTypes.Label.Models"] = [[Modelli]], + ["Common.AssetTypes.Label.Packages"] = [[Pacchetti]], + ["Common.AssetTypes.Label.Pants"] = [[Pantaloni]], + ["Common.AssetTypes.Label.Places"] = [[Località]], + ["Common.AssetTypes.Label.Plugins"] = [[Plug-in]], + ["Common.AssetTypes.Label.Shirts"] = [[Camicie]], + ["Common.AssetTypes.Label.TShirts"] = [[Magliette]], + ["Common.AssetTypes.Label.VipServers"] = [[Server VIP]], + ["Common.AssetTypes.Label.Run"] = [[Corri]], + ["Common.AssetTypes.Label.Walk"] = [[Cammina]], + ["Common.AssetTypes.Label.Fall"] = [[Cadi]], + ["Common.AssetTypes.Label.Jump"] = [[Salta]], + ["Common.AssetTypes.Label.Idle"] = [[Inattivo]], + ["Common.AssetTypes.Label.Swim"] = [[Nuota]], + ["Common.AssetTypes.Label.Climb"] = [[Scala]], + ["Common.AssetTypes.Label.Hats"] = [[Cappelli]], + ["Common.AssetTypes.Label.Shoulders"] = [[Spalle]], + ["Common.AssetTypes.Label.Death"] = [[Morte]], + ["Common.AssetTypes.Label.Pose"] = [[Posa]], + ["Common.AssetTypes.Label.Head"] = [[Testa]], + ["Common.AssetTypes.Label.TShirt"] = [[Maglietta]], + ["Common.AssetTypes.Label.Shirt"] = [[Camicia]], + ["Common.AssetTypes.Label.Decal"] = [[Decalcomania]], + ["Common.AssetTypes.Label.Model"] = [[Modello]], + ["Common.AssetTypes.Label.Plugin"] = [[Plug-in]], + ["Common.AssetTypes.Label.MeshPart"] = [[Parte mesh]], + ["Common.AssetTypes.Label.GamePass"] = [[Pass di gioco]], + ["Common.AssetTypes.Label.Badge"] = [[Contrassegno]], + ["Common.AssetTypes.Label.Package"] = [[Pacchetto]], + ["Common.AssetTypes.Label.Place"] = [[Località]], + ["Common.AssetTypes.Label.LeftArm"] = [[Braccio sinistro]], + ["Common.AssetTypes.Label.LeftLeg"] = [[Gamba sinistra]], + ["Common.AssetTypes.Label.RightArm"] = [[Braccio destro]], + ["Common.AssetTypes.Label.RightLeg"] = [[Gamba destra]], + ["Common.AssetTypes.Label.Torso"] = [[Busto]], + ["Common.AssetTypes.Label.Animation"] = [[Animazione]], + ["Common.AssetTypes.Label.Emote"] = [[Emoticon]], + ["Common.BuildersClub.Label.PlanFree"] = [[Gratis]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Classica]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Turbo]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Abbonamento Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Abbonamento Turbo Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Abbonamento Outrageous Builders Club]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[Sì]], + ["Common.BuildersClub.Label.No"] = [[No]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[MAI]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Builders Club Classico]], + ["Common.BuildersClub.Label.Lifetime"] = [[A vita]], + ["Common.BuildersClub.Label.Membership"] = [[Abbonamento]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Premium necessario]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Con Roblox Premium otterrai:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[450 Robux al mese]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Accesso a vantaggi esclusivi Premium]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[Bonus del 10% quando acquisti Robux]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price}/mese]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[Non è possibile acquistare Roblox Premium sulla tua piattaforma. Acquista la versione Premium da un computer desktop.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[Sembra che lo sviluppatore stia cercando di farti acquistare Roblox Premium, ma sei già un membro Premium!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[Premium non è al momento disponibile. Riprova più tardi!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[{robux} Robux al mese]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[L'acquisto non è stato completato, riprova.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[Errore]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[Prendi gratis]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[Aggiorna]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[Compra ora]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[Compra R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[Annulla]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[OK]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[Acquisto]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[I fondi del tuo account non verranno intaccati da questa transazione.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[Questo è un acquisto di prova; il tuo account non subirà addebiti.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[Questo era un acquisto di prova.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[Questo oggetto richiede: {BC_LEVEL}. Clicca su "Migliora" per potenziare il tuo Builders Club!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[Dopo la transazione, avrai R${BALANCE_FUTURE}]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[Ora i tuoi fondi sono {BALANCE_NOW}.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[I rimanenti {REMAINING_ROBUX} Robux verranno accreditati ai tuoi fondi.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[Il tuo acquisto non è andato a buon fine poiché il tuo account non ha abbastanza Robux. Il tuo account non ha subito addebiti.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[Il tuo acquisto non è riuscito poiché hai bisogno di un abbonamento per acquistare questo articolo. Il tuo account non ha subito alcun addebito.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[Questo oggetto costa più Robux di quanti tu ne abbia. Esci dal gioco e vai nella schermata dei ROBUX per acquistarne altri.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[Questo oggetto costa più Robux di quanti tu possa acquistarne. Visita il sito www.roblox.com per acquistare altri ROBUX.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[Il tuo acquisto non è andato a buon fine poiché gli acquisti in gioco sono temporaneamente disabilitati. Il tuo account non ha subito alcun addebito. Riprova più tardi.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[L'acquisto non è andato a buon fine a causa di un errore. Il tuo account non ha subito addebiti. Riprova più tardi.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[L'acquisto di {ITEM_NAME} non è andato a buon fine a causa di un errore. Il tuo account non ha subito addebiti. Riprova più tardi.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[Impossibile recuperare i tuoi fondi in questo momento. Il tuo account non ha subito addebiti. Riprova più tardi.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[Impossibile recuperare il prezzo dell'oggetto in questo momento. Il tuo account non ha subito addebiti. Riprova più tardi.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[Questo oggetto limitato non è più disponibile. Prova ad acquistarlo da un altro utente sul sito www.roblox.com. Il tuo account non ha subito addebiti.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[Questo oggetto non è attualmente in vendita. Il tuo account non ha subito addebiti.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[Per poter comprare oggetti, devi creare un account ROBLOX. Visita il sito www.roblox.com per maggiori informazioni.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[Le vendite di oggetti di terzi sono state disattivate per questa località. Il tuo account non ha subito addebiti.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[Il tuo account è per giocatori con meno di 13 anni. L'acquisto di questo oggetto non è permesso. Il tuo account non ha subito addebiti.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[Possiedi già questo oggetto. Il tuo account non ha subito addebiti.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[Vuoi prendere l'oggetto {ITEM_NAME} GRATIS?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[Vuoi comprare {ASSET_TYPE} {ITEM_NAME} per]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[Il tuo acquisto di {ITEM_NAME} è andato a buon fine!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[Hai bisogno di {NEEDED_AMOUNT} Robux in più per comprare: {ASSET_TYPE} {ITEM_NAME}. Vuoi acquistare altri ROBUX?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[Prodotto]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[Bundle]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[Sì]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[Annullamento]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[Al momento non stai rinnovando l'abbonamento. I tuoi abbonamenti non sono cambiati.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[Hai disdetto l'abbonamento a {ITEM_NAME}. Manterrai i tuoi benefici fino alla fine del periodo di pagamento.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[Sei sicuro di voler disdire l'abbonamento a {ITEM_NAME}. Manterrai i tuoi benefici fino alla fine del periodo di pagamento.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[Vuoi abbonarti a {ITEM_NAME} per]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[Abbonamento]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[{PRICE} al mese]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[Abbonati]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[Devi essere un membro Premium per poterti abbonare!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[Aggiorna]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[Il tuo acquisto non è riuscito poiché devi essere un membro Premium per abbonarti. Il tuo account non ha subito alcun addebito.]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Acquista Robux]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[Dopo la transazione, i tuoi fondi ammonteranno a {BALANCE_FUTURE} Robux.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[Questo oggetto costa più Robux di quanti tu ne abbia. ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Solo Premium]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[Hai bisogno di Roblox Premium per poter comprare questo oggetto.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[Solo gli adulti possono fare acquisti su Roblox. Riconosco di avere almeno 18 anni. Sono il proprietario di questo account o il genitore o il tutore legale del proprietario. Autorizzo questo acquisto e accetto i Termini di servizio.]], +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ja-jp.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ja-jp.lua new file mode 100644 index 0000000..1bb5da1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ja-jp.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[アクセサリ]], + ["Common.AssetTypes.Label.Hat"] = [[帽子]], + ["Common.AssetTypes.Label.Hair"] = [[髪]], + ["Common.AssetTypes.Label.Face"] = [[顔]], + ["Common.AssetTypes.Label.Neck"] = [[首]], + ["Common.AssetTypes.Label.Shoulder"] = [[肩]], + ["Common.AssetTypes.Label.Front"] = [[正面]], + ["Common.AssetTypes.Label.Back"] = [[背面]], + ["Common.AssetTypes.Label.Waist"] = [[腰]], + ["Common.AssetTypes.Label.Animations"] = [[アニメーション]], + ["Common.AssetTypes.Label.Audio"] = [[オーディオ]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[アバターアニメ]], + ["Common.AssetTypes.Label.Badges"] = [[バッジ]], + ["Common.AssetTypes.Label.Decals"] = [[デカール]], + ["Common.AssetTypes.Label.Faces"] = [[顔]], + ["Common.AssetTypes.Label.GamePasses"] = [[ゲームパス]], + ["Common.AssetTypes.Label.Gear"] = [[ギア]], + ["Common.AssetTypes.Label.Heads"] = [[頭]], + ["Common.AssetTypes.Label.Meshes"] = [[メッシュ]], + ["Common.AssetTypes.Label.Models"] = [[モデル]], + ["Common.AssetTypes.Label.Packages"] = [[パッケージ]], + ["Common.AssetTypes.Label.Pants"] = [[ズボン]], + ["Common.AssetTypes.Label.Places"] = [[プレース]], + ["Common.AssetTypes.Label.Plugins"] = [[プラグイン]], + ["Common.AssetTypes.Label.Shirts"] = [[シャツ]], + ["Common.AssetTypes.Label.TShirts"] = [[Tシャツ]], + ["Common.AssetTypes.Label.VipServers"] = [[VIPサーバー]], + ["Common.AssetTypes.Label.Run"] = [[走る]], + ["Common.AssetTypes.Label.Walk"] = [[歩く]], + ["Common.AssetTypes.Label.Fall"] = [[落下]], + ["Common.AssetTypes.Label.Jump"] = [[ジャンプ]], + ["Common.AssetTypes.Label.Idle"] = [[待機]], + ["Common.AssetTypes.Label.Swim"] = [[泳ぐ]], + ["Common.AssetTypes.Label.Climb"] = [[登る]], + ["Common.AssetTypes.Label.Hats"] = [[帽子]], + ["Common.AssetTypes.Label.Shoulders"] = [[肩]], + ["Common.AssetTypes.Label.Death"] = [[死]], + ["Common.AssetTypes.Label.Pose"] = [[ポーズ]], + ["Common.AssetTypes.Label.Head"] = [[頭]], + ["Common.AssetTypes.Label.TShirt"] = [[Tシャツ]], + ["Common.AssetTypes.Label.Shirt"] = [[シャツ]], + ["Common.AssetTypes.Label.Decal"] = [[デカール]], + ["Common.AssetTypes.Label.Model"] = [[モデル]], + ["Common.AssetTypes.Label.Plugin"] = [[プラグイン]], + ["Common.AssetTypes.Label.MeshPart"] = [[メッシュパーツ]], + ["Common.AssetTypes.Label.GamePass"] = [[ゲームパス]], + ["Common.AssetTypes.Label.Badge"] = [[バッジ]], + ["Common.AssetTypes.Label.Package"] = [[パッケージ]], + ["Common.AssetTypes.Label.Place"] = [[プレース]], + ["Common.AssetTypes.Label.LeftArm"] = [[左腕]], + ["Common.AssetTypes.Label.LeftLeg"] = [[左脚]], + ["Common.AssetTypes.Label.RightArm"] = [[右腕]], + ["Common.AssetTypes.Label.RightLeg"] = [[右脚]], + ["Common.AssetTypes.Label.Torso"] = [[胴体]], + ["Common.AssetTypes.Label.Animation"] = [[アニメーション]], + ["Common.AssetTypes.Label.Emote"] = [[エモート]], + ["Common.BuildersClub.Label.PlanFree"] = [[無料]], + ["Common.BuildersClub.Label.PlanClassic"] = [[クラシック]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Builders Club メンバーシップ]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Turbo Builders Club メンバーシップ]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Outrageous Builders Club メンバーシップ]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[はい]], + ["Common.BuildersClub.Label.No"] = [[いいえ]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[しない]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[クラシック Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[永久]], + ["Common.BuildersClub.Label.Membership"] = [[メンバーシップ]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Premiumが必要です]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Roblox Premiumには以下が含まれています:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[1ヶ月に450Robux]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Premium限定特典にアクセス]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[Robux購入時に10%のボーナス]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price}/月額]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[お使いのプラットフォームはRoblox Premiumの購入に対応していません。Premiumを購入するにはデスクトップをお使いください。]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[開発者がRoblox Premiumを購入するように促しているようですが、すでにPremiumメンバーです!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[現在、Premiumが利用できません。あとでお試しください!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[1ヶ月につき{robux} Robux]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[購入は完了していません。もう一度お試しください。]], + ["CoreScripts.PremiumModal.Title.Error"] = [[エラー]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[無料配布]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[アップグレード]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[今すぐ買う]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[R$ を買う]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[キャンセル]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[OK]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[購入しています]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[この取引であなたのアカウント残高は変わりません。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[これはテスト購入です。アカウントには課金されません。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[これはテスト購入です。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[このアイテムを買うには{BC_LEVEL}である必要があります。「アップグレード」をクリックしてBuilders Clubをアップグレードしてください!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[この取引の後の残高は R${BALANCE_FUTURE}です]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[現在の残高は{BALANCE_NOW}です。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[残りの{REMAINING_ROBUX} Robuxがあなたのアカウントに払い戻しされます。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[アカウントのRobuxが不足しているため購入を完了できませんでした。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[このアイテムを買うにはサブスクリプションが必要なため、購入を完了できませんでした。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[このアイテムを買うにはお手持ちのRobuxでは足りません。ゲームを終了して、Robux画面で追加購入してください。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[このアイテムは、お手持ちのRobuxでは購入できません。www.roblox.comでRobuxを追加購入してください。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[ゲーム内購入が一時的に無効になっているため、購入できませんでした。アカウントは課金されていません。後でもう一度お試し下さい。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[問題が発生したため、購入を完了できませんでした。アカウントは課金されていません。後でもう一度お試し下さい。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[問題が発生したため、{ITEM_NAME} の購入を完了できませんでした。アカウントは課金されていません。後でもう一度お試し下さい。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[現在、残高を取得できません。アカウントは課金されていません。後でもう一度お試し下さい。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[現在、アイテム価格を取得できません。アカウントは課金されていません。後でもう一度お試し下さい。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[この限定アイテムはもう残っていません。www.roblox.com で他のユーザーから購入してみてください。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[このアイテムは現在売られていません。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[アイテムを買うにはRobloxアカウントを作る必要があります。詳しくは www.roblox.com でご確認ください。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[サードパーティ製のアイテムを売ることは、ここでは禁止されています。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[アカウントが13歳未満用です。このアイテムの購入は許可されていません。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[このアイテムはすでに持っています。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[{ITEM_NAME} を無料で入手しますか?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[{ASSET_TYPE} {ITEM_NAME} を以下の値段で買いますか:]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[{ITEM_NAME}を購入しました!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[{ASSET_TYPE} {ITEM_NAME}を買うにはあと{NEEDED_AMOUNT}のRobuxが必要です。もっとRobuxを買い足しますか?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[製品]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[バンドル]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[はい]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[キャンセル中]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[現在、サブスクリプションを更新していません。サブスクリプション料金は課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[{ITEM_NAME}のサブスクリプションをキャンセルしました。お支払い期間の終了日までサービスをご利用できます。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[{ITEM_NAME}のサブスクリプションをキャンセルしますか。お支払い期間の終了日までサービスをご利用できます。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[{ITEM_NAME}のサブスクリプションを申し込みます]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[サブスクリプション]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[月額 {PRICE} ]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[サブスクリプション契約する]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[サブスクリプション契約するには、Premiumメンバーである必要があります!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[アップグレード]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[このアイテムを買うにはサブスクリプションが必要なため、購入を完了できませんでした。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Robuxを買う]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[取引後の残高は{BALANCE_FUTURE} Robuxになります]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[このアイテムの費用は利用できるRobuxの額を超えています。 ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Premium限定]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[このアイテムを購入するには、Roblox Premiumが必要です。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[この購入には、実際のお金の交換が関わります。私の年齢が少なくとも18歳であり、アカウント所有者の親か法的保護者であることを認めます。この購入を許可し、利用規約に同意します。]], +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ka-ge.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ka-ge.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ka-ge.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/kk-kz.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/kk-kz.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/kk-kz.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/km-kh.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/km-kh.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/km-kh.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ko-kr.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ko-kr.lua new file mode 100644 index 0000000..25f8211 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ko-kr.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[장신구]], + ["Common.AssetTypes.Label.Hat"] = [[모자]], + ["Common.AssetTypes.Label.Hair"] = [[헤어]], + ["Common.AssetTypes.Label.Face"] = [[얼굴]], + ["Common.AssetTypes.Label.Neck"] = [[목]], + ["Common.AssetTypes.Label.Shoulder"] = [[어깨]], + ["Common.AssetTypes.Label.Front"] = [[가슴]], + ["Common.AssetTypes.Label.Back"] = [[등]], + ["Common.AssetTypes.Label.Waist"] = [[허리]], + ["Common.AssetTypes.Label.Animations"] = [[애니메이션]], + ["Common.AssetTypes.Label.Audio"] = [[오디오]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[아바타 애니메이션]], + ["Common.AssetTypes.Label.Badges"] = [[배지]], + ["Common.AssetTypes.Label.Decals"] = [[데칼]], + ["Common.AssetTypes.Label.Faces"] = [[얼굴]], + ["Common.AssetTypes.Label.GamePasses"] = [[게임패스]], + ["Common.AssetTypes.Label.Gear"] = [[장비]], + ["Common.AssetTypes.Label.Heads"] = [[머리]], + ["Common.AssetTypes.Label.Meshes"] = [[메시]], + ["Common.AssetTypes.Label.Models"] = [[모델]], + ["Common.AssetTypes.Label.Packages"] = [[패키지]], + ["Common.AssetTypes.Label.Pants"] = [[바지]], + ["Common.AssetTypes.Label.Places"] = [[플레이스]], + ["Common.AssetTypes.Label.Plugins"] = [[플러그인]], + ["Common.AssetTypes.Label.Shirts"] = [[셔츠]], + ["Common.AssetTypes.Label.TShirts"] = [[티셔츠]], + ["Common.AssetTypes.Label.VipServers"] = [[VIP 서버]], + ["Common.AssetTypes.Label.Run"] = [[달리기]], + ["Common.AssetTypes.Label.Walk"] = [[걷기]], + ["Common.AssetTypes.Label.Fall"] = [[낙하]], + ["Common.AssetTypes.Label.Jump"] = [[점프]], + ["Common.AssetTypes.Label.Idle"] = [[대기]], + ["Common.AssetTypes.Label.Swim"] = [[수영]], + ["Common.AssetTypes.Label.Climb"] = [[오르기]], + ["Common.AssetTypes.Label.Hats"] = [[모자]], + ["Common.AssetTypes.Label.Shoulders"] = [[어깨]], + ["Common.AssetTypes.Label.Death"] = [[사망]], + ["Common.AssetTypes.Label.Pose"] = [[포즈]], + ["Common.AssetTypes.Label.Head"] = [[머리]], + ["Common.AssetTypes.Label.TShirt"] = [[티셔츠]], + ["Common.AssetTypes.Label.Shirt"] = [[셔츠]], + ["Common.AssetTypes.Label.Decal"] = [[데칼]], + ["Common.AssetTypes.Label.Model"] = [[모델]], + ["Common.AssetTypes.Label.Plugin"] = [[플러그인]], + ["Common.AssetTypes.Label.MeshPart"] = [[메시 파트]], + ["Common.AssetTypes.Label.GamePass"] = [[게임패스]], + ["Common.AssetTypes.Label.Badge"] = [[배지]], + ["Common.AssetTypes.Label.Package"] = [[패키지]], + ["Common.AssetTypes.Label.Place"] = [[플레이스]], + ["Common.AssetTypes.Label.LeftArm"] = [[왼팔]], + ["Common.AssetTypes.Label.LeftLeg"] = [[왼 다리]], + ["Common.AssetTypes.Label.RightArm"] = [[오른팔]], + ["Common.AssetTypes.Label.RightLeg"] = [[오른 다리]], + ["Common.AssetTypes.Label.Torso"] = [[몸통]], + ["Common.AssetTypes.Label.Animation"] = [[애니메이션]], + ["Common.AssetTypes.Label.Emote"] = [[감정 표현]], + ["Common.BuildersClub.Label.PlanFree"] = [[무료]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Classic]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Builders Club 멤버십]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Turbo Builders Club 멤버십]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Outrageous Builders Club 멤버십]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[예]], + ["Common.BuildersClub.Label.No"] = [[아니요]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[절대 안 함]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[평생]], + ["Common.BuildersClub.Label.Membership"] = [[멤버십]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Premium 필요]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Roblox Premium 회원을 위한 다양한 혜택:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[매달 450 Robux 획득]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Premium 전용 혜택 이용]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[Robux 구입 시 10% 추가 보너스 증정]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price}/월]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[Roblox Premium 구매가 지원되지 않는 플랫폼입니다. Premium을 구매하려면 데스크톱을 사용하세요.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[개발자가 Roblox Premium 구매를 도와드리려 한 것 같네요. 그런데 이미 Premium 회원이세요!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[Premium이 일시적으로 이용 불가능합니다. 나중에 다시 시도하세요!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[매월 {robux} Robux 지급]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[구매를 완료하지 못했습니다. 다시 시도하세요.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[오류]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[무료 획득]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[업그레이드]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[지금 구매하기]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[R$ 구매]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[취소]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[확인]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[구매 중]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[본 거래는 계정 잔액에 영향을 주지 않습니다.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[테스트 구매입니다. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[테스트 구매입니다.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[본 아이템은 {BC_LEVEL}이(가) 필요합니다. '업그레이드' 버튼을 클릭해 Builders Club을 업그레이드하세요!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[본 거래 후 예상 잔액은 R${BALANCE_FUTURE}입니다.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[현재 잔액은 {BALANCE_NOW}입니다.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[남은 {REMAINING_ROBUX} Robux는 회원님의 계정에 적립됩니다.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[Robux가 부족해 구매하지 못했습니다. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[본 아이템을 구매에 필요한 플랜에 가입하지 않아 아이템을 구매하지 못했습니다. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[Robux가 부족해 본 아이템을 구매할 수 없습니다. 게임을 종료한 후 Robux 화면으로 이동하여 로벅스를 추가 구매하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[Robux가 부족해 본 아이템을 구매할 수 없습니다. Robux를 구매하려면 www.roblox.com을 방문하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[게임 내 구매의 일시적으로 비활성화로 인해 구매하지 못했습니다. 계정에 비용이 청구되지 않아요. 나중에 다시 시도하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[오류가 발생해 구매하지 못했습니다. 계정에 비용이 청구되지 않아요. 나중에 다시 시도하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[오류가 발생해 {ITEM_NAME} 구매하지 못했습니다. 계정에 비용이 청구되지 않아요. 나중에 다시 시도하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[지금은 잔액을 불러올 수 없습니다. 계정에 비용이 청구되지 않아요. 나중에 다시 시도하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[지금은 아이템 가격을 불러올 수 없어요. 계정에 비용이 청구되지 않아요. 나중에 다시 시도하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[본 아이템은 한정판 아이템으로 더 이상 재고가 없습니다. www.roblox.com에서 다른 사용자에게 구매해보세요. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[현재 판매 중인 아이템이 아닙니다. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[ROBLOX 계정을 만들어야 아이템을 구매할 수 있습니다. 자세한 정보는 www.roblox.com에서 확인하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[본 장소에 대한 제삼자 아이템 판매가 비활성화 상태입니다. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[본 아이템은 만 13세 미만 계정으로 구매할 수 없어요. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[이미 보유하고 있는 아이템입니다. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[무료로 {ITEM_NAME}을 획득하시겠습니까?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[다음 가격으로 {ASSET_TYPE} {ITEM_NAME}을(를) 구매하시겠습니까?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[{ITEM_NAME} 구매 성공!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[{NEEDED_AMOUNT} Robux가 더 있어야 {ASSET_TYPE} {ITEM_NAME}을(를) 구매할 수 있습니다. Robux를 구매하시겠습니까?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[상품]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[번들]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[예]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[취소 중]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[가입을 갱신하지 않습니다. 변경 사항이 없습니다.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[{ITEM_NAME} 가입을 취소했어요. 지불 기간이 끝날 때까지 혜택은 계속됩니다.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[{ITEM_NAME} 가입을 정말로 취소할까요? 취소하더라도 지불 기간이 끝날 때까지 혜택은 계속됩니다.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[{ITEM_NAME}에 가입할까요? 비용은]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[가입]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[매월 {PRICE}]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[가입]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[가입하려면 Premium 멤버여야 해요!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[업그레이드]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[Premium 멤버가 아니어서 아이템을 구매하지 못했어요. 비용도 청구되지 않았습니다.]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Robux 구매]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[본 거래 후 예상 잔액은 {BALANCE_FUTURE} Robux입니다.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[아이템 구매에 충분한 Robux를 보유하고 있지 않습니다.]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Premium 전용]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[이 아이템을 구매하려면 Roblox Premium 회원이어야 해요.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[이 구매는 실물 화폐 교환으로 이루어집니다. 본인은 18세 이상이며 계정 소유자의 부모 또는 법적 보호자임에 동의합니다. 또한 이 구매를 승인하고 서비스 약관에 동의합니다.]], +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/lt-lt.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/lt-lt.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/lt-lt.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/lv-lv.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/lv-lv.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/lv-lv.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ms-my.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ms-my.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ms-my.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/my-mm.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/my-mm.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/my-mm.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/nb-no.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/nb-no.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/nb-no.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/nl-nl.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/nl-nl.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/nl-nl.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/pl-pl.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/pl-pl.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/pl-pl.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/pt-br.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/pt-br.lua new file mode 100644 index 0000000..e550e27 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/pt-br.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[Acessórios]], + ["Common.AssetTypes.Label.Hat"] = [[Chapéu]], + ["Common.AssetTypes.Label.Hair"] = [[Cabelo]], + ["Common.AssetTypes.Label.Face"] = [[Rosto]], + ["Common.AssetTypes.Label.Neck"] = [[Pescoço]], + ["Common.AssetTypes.Label.Shoulder"] = [[Ombro]], + ["Common.AssetTypes.Label.Front"] = [[Frente]], + ["Common.AssetTypes.Label.Back"] = [[Costas]], + ["Common.AssetTypes.Label.Waist"] = [[Cintura]], + ["Common.AssetTypes.Label.Animations"] = [[Animações]], + ["Common.AssetTypes.Label.Audio"] = [[Áudio]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[Animações de avatar]], + ["Common.AssetTypes.Label.Badges"] = [[Emblemas]], + ["Common.AssetTypes.Label.Decals"] = [[Adesivos]], + ["Common.AssetTypes.Label.Faces"] = [[Rostos]], + ["Common.AssetTypes.Label.GamePasses"] = [[Passes de jogo]], + ["Common.AssetTypes.Label.Gear"] = [[Equipamentos]], + ["Common.AssetTypes.Label.Heads"] = [[Cabeças]], + ["Common.AssetTypes.Label.Meshes"] = [[Malhas]], + ["Common.AssetTypes.Label.Models"] = [[Modelos]], + ["Common.AssetTypes.Label.Packages"] = [[Pacotes]], + ["Common.AssetTypes.Label.Pants"] = [[Calças]], + ["Common.AssetTypes.Label.Places"] = [[Locais]], + ["Common.AssetTypes.Label.Plugins"] = [[Plugins]], + ["Common.AssetTypes.Label.Shirts"] = [[Camisas]], + ["Common.AssetTypes.Label.TShirts"] = [[Camisetas]], + ["Common.AssetTypes.Label.VipServers"] = [[Servidores VIP]], + ["Common.AssetTypes.Label.Run"] = [[Correr]], + ["Common.AssetTypes.Label.Walk"] = [[Andar]], + ["Common.AssetTypes.Label.Fall"] = [[Cair]], + ["Common.AssetTypes.Label.Jump"] = [[Pular]], + ["Common.AssetTypes.Label.Idle"] = [[Inatividade]], + ["Common.AssetTypes.Label.Swim"] = [[Nadar]], + ["Common.AssetTypes.Label.Climb"] = [[Escalar]], + ["Common.AssetTypes.Label.Hats"] = [[Chapéus]], + ["Common.AssetTypes.Label.Shoulders"] = [[Ombros]], + ["Common.AssetTypes.Label.Death"] = [[Morte]], + ["Common.AssetTypes.Label.Pose"] = [[Pose]], + ["Common.AssetTypes.Label.Head"] = [[Cabeça]], + ["Common.AssetTypes.Label.TShirt"] = [[Camisetas]], + ["Common.AssetTypes.Label.Shirt"] = [[Camisas]], + ["Common.AssetTypes.Label.Decal"] = [[Adesivos]], + ["Common.AssetTypes.Label.Model"] = [[Modelos]], + ["Common.AssetTypes.Label.Plugin"] = [[Plugins]], + ["Common.AssetTypes.Label.MeshPart"] = [[Parte da malha]], + ["Common.AssetTypes.Label.GamePass"] = [[Passe de jogo]], + ["Common.AssetTypes.Label.Badge"] = [[Emblemas]], + ["Common.AssetTypes.Label.Package"] = [[Pacotes]], + ["Common.AssetTypes.Label.Place"] = [[Locais]], + ["Common.AssetTypes.Label.LeftArm"] = [[Braço esquerdo]], + ["Common.AssetTypes.Label.LeftLeg"] = [[Perna esquerda]], + ["Common.AssetTypes.Label.RightArm"] = [[Braço direito]], + ["Common.AssetTypes.Label.RightLeg"] = [[Perna direita]], + ["Common.AssetTypes.Label.Torso"] = [[Tronco]], + ["Common.AssetTypes.Label.Animation"] = [[Animações]], + ["Common.AssetTypes.Label.Emote"] = [[Emote]], + ["Common.BuildersClub.Label.PlanFree"] = [[Grátis]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Clássico]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Assinatura do Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Assinatura do Turbo Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Assinatura do Outrageous Builders Club]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[Sim]], + ["Common.BuildersClub.Label.No"] = [[Não]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[NUNCA]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[Vitalícia]], + ["Common.BuildersClub.Label.Membership"] = [[Assinatura]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Requer Premium]], + ["CoreScripts.PremiumModal.Body.Description"] = [[O Roblox Premium dá direito a:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[450 Robux por mês]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Acesso a benefícios exclusivos]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[Bônus de 10% ao comprar Robux]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price}/mês]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[A compra do Roblox Premium não está disponível na sua plataforma. Use o computador para comprar a assinatura Premium.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[Parece que o desenvolvedor quer te convidar para comprar uma inscrição do Roblox Premium, mas você já se inscreveu!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[O Roblox Premium não está disponível no momento. Tente mais tarde!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[{robux} Robux por mês]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[A compra não foi concluída; tente novamente.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[Erro]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[Pegue de graça]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[Melhorar]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[Comprar agora]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[Comprar R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[Cancelar]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[OK]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[Comprando]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[O saldo da sua conta não será afetado por esta transação.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[Esta é uma compra de teste. Nada será cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[Esta foi uma compra de teste.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[Este item requer {BC_LEVEL}. Clique em 'Melhorar' para melhorar seu Builders Club!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[Seu saldo depois desta transação será de R$ {BALANCE_FUTURE}]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[Seu saldo agora é {BALANCE_NOW}.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[Os {REMAINING_ROBUX} Robux restantes serão somados ao seu saldo.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[Sua compra falhou porque sua conta não tem ROBUX suficientes. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[Sua compra falhou porque você precisa de uma assinatura para comprar este item. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[Este item custa mais Robux do que você possui disponível. Saia do jogo e vá para a tela de Robux para comprar mais.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[Este item custa mais Robux do que você pode comprar. Visite www.roblox.com para comprar mais Robux.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[Sua compra falhou porque as compras no jogo estão temporariamente desabilitadas. Nada foi cobrado da sua conta. Tente de novo mais tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[Sua compra falhou porque algo deu errado. Nada foi cobrado da sua conta. Tente de novo mais tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[Sua compra do item {ITEM_NAME} falhou porque algo deu errado. Nada foi cobrado da sua conta. Tente de novo mais tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[Impossível obter seu saldo no momento. Nada foi cobrado da sua conta. Tente de novo mais tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[Não conseguimos obter o preço do item no momento. Nada foi cobrado da sua conta. Tente de novo mais tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[Esgotaram-se as cópias deste item limitado. Tente comprar de outro usuário em www.roblox.com. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[Este item não está à venda no momento. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[Você precisa criar uma conta ROBLOX para comprar itens. Visite www.roblox.com para mais informações.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[Vendas de itens de terceiros foram desabilitadas para este local. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[Sua conta é para menor de 13 anos. A compra deste item não é permitida. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[Você já possui este item. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[Gostaria de obter {ITEM_NAME} GRÁTIS?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[Deseja comprar o(a) {ASSET_TYPE} {ITEM_NAME} por]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[Sua compra de {ITEM_NAME} foi bem-sucedida!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[Você precisa de mais {NEEDED_AMOUNT} Robux para comprar o(a) {ASSET_TYPE} {ITEM_NAME}. Gostaria de comprar mais Robux?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[Produto]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[Pacote]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[Sim]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[Cancelando]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[Você não está renovando esta assinatura no momento. Suas assinaturas não foram alteradas.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[Você cancelou sua assinatura {ITEM_NAME}. Você continuará com os benefícios até o fim do período de pagamento.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[Você quer mesmo cancelar sua assinatura {ITEM_NAME}? Você continuará com os benefícios até o fim do período de pagamento.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[Deseja adquirir a assinatura {ITEM_NAME} por]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[Assinatura]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[{PRICE} por mês]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[Assinar]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[Você deve ser um membro Premium para assinar!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[Melhorar]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[Sua compra falhou porque você precisa ser um membro Premium para assinar. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Comprar Robux]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[Seu saldo depois desta transação será de {BALANCE_FUTURE} Robux]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[Você não tem Robux suficientes para comprar este item. ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Apenas Premium]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[Você precisa ter Roblox Premium para comprar este item.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[Só adultos podem efetuar compras na Roblox. Eu afirmo ter mais de 18 anos e sou o dono desta conta ou pai ou responsável legal do dono. Autorizo esta compra e concordo com os Termos de Uso.]], +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ro-ro.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ro-ro.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ro-ro.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ru-ru.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ru-ru.lua new file mode 100644 index 0000000..13ff657 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ru-ru.lua @@ -0,0 +1,26 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Требуется премиум-подписка]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Преимущества премиум-подписки Roblox:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[450 Robux в месяц;]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Доступ к возможностям только для премиум-подписчиков]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[бонус в 10% при покупке Robux.]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price}/месяц]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[Покупка премиум-подписки Roblox не поддерживается на вашей платформе. Используйте ПК для покупки.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[Похоже, разработчик пытается убедить вас купить премиум-подписку Roblox, а она у вас уже есть!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[В настоящий момент премиум-подписка недоступна. Повторите попытку позже!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[{robux} Robux в месяц;]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[Покупка не удалась, попробуйте еще раз.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[Ошибка]], +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/si-lk.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/si-lk.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/si-lk.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sk-sk.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sk-sk.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sk-sk.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sl-sl.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sl-sl.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sl-sl.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sq-al.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sq-al.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sq-al.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sr-rs.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sr-rs.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sr-rs.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sv-se.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sv-se.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sv-se.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/th-th.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/th-th.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/th-th.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/tr-tr.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/tr-tr.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/tr-tr.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/uk-ua.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/uk-ua.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/uk-ua.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/vi-vn.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/vi-vn.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/vi-vn.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-cjv.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-cjv.lua new file mode 100644 index 0000000..9afc684 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-cjv.lua @@ -0,0 +1,154 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[配饰]], + ["Common.AssetTypes.Label.Hat"] = [[帽子]], + ["Common.AssetTypes.Label.Hair"] = [[发型]], + ["Common.AssetTypes.Label.Face"] = [[表情]], + ["Common.AssetTypes.Label.Neck"] = [[颈部]], + ["Common.AssetTypes.Label.Shoulder"] = [[肩部]], + ["Common.AssetTypes.Label.Front"] = [[正面]], + ["Common.AssetTypes.Label.Back"] = [[背面]], + ["Common.AssetTypes.Label.Waist"] = [[腰部]], + ["Common.AssetTypes.Label.Animations"] = [[动画]], + ["Common.AssetTypes.Label.Audio"] = [[音频]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[虚拟形象动画]], + ["Common.AssetTypes.Label.Badges"] = [[徽章]], + ["Common.AssetTypes.Label.Decals"] = [[贴花]], + ["Common.AssetTypes.Label.Faces"] = [[表情]], + ["Common.AssetTypes.Label.GamePasses"] = [[游戏通行证]], + ["Common.AssetTypes.Label.Gear"] = [[装备]], + ["Common.AssetTypes.Label.Heads"] = [[头部]], + ["Common.AssetTypes.Label.Meshes"] = [[网格]], + ["Common.AssetTypes.Label.Models"] = [[模型]], + ["Common.AssetTypes.Label.Packages"] = [[套装]], + ["Common.AssetTypes.Label.Pants"] = [[裤子]], + ["Common.AssetTypes.Label.Places"] = [[场景]], + ["Common.AssetTypes.Label.Plugins"] = [[插件]], + ["Common.AssetTypes.Label.Shirts"] = [[衬衫]], + ["Common.AssetTypes.Label.TShirts"] = [[T 恤]], + ["Common.AssetTypes.Label.VipServers"] = [[VIP 服务器]], + ["Common.AssetTypes.Label.Run"] = [[奔跑]], + ["Common.AssetTypes.Label.Walk"] = [[步行]], + ["Common.AssetTypes.Label.Fall"] = [[下落]], + ["Common.AssetTypes.Label.Jump"] = [[跳跃]], + ["Common.AssetTypes.Label.Idle"] = [[闲置]], + ["Common.AssetTypes.Label.Swim"] = [[游泳]], + ["Common.AssetTypes.Label.Climb"] = [[攀爬]], + ["Common.AssetTypes.Label.Hats"] = [[帽子]], + ["Common.AssetTypes.Label.Shoulders"] = [[肩部]], + ["Common.AssetTypes.Label.Death"] = [[死亡]], + ["Common.AssetTypes.Label.Pose"] = [[姿势]], + ["Common.AssetTypes.Label.Head"] = [[头部]], + ["Common.AssetTypes.Label.TShirt"] = [[T 恤]], + ["Common.AssetTypes.Label.Shirt"] = [[衬衫]], + ["Common.AssetTypes.Label.Decal"] = [[贴花]], + ["Common.AssetTypes.Label.Model"] = [[模型]], + ["Common.AssetTypes.Label.Plugin"] = [[插件]], + ["Common.AssetTypes.Label.MeshPart"] = [[网格组件]], + ["Common.AssetTypes.Label.GamePass"] = [[游戏通行证]], + ["Common.AssetTypes.Label.Badge"] = [[徽章]], + ["Common.AssetTypes.Label.Package"] = [[套装]], + ["Common.AssetTypes.Label.Place"] = [[场景]], + ["Common.AssetTypes.Label.LeftArm"] = [[左臂]], + ["Common.AssetTypes.Label.LeftLeg"] = [[左腿]], + ["Common.AssetTypes.Label.RightArm"] = [[右臂]], + ["Common.AssetTypes.Label.RightLeg"] = [[右腿]], + ["Common.AssetTypes.Label.Torso"] = [[躯干]], + ["Common.AssetTypes.Label.Animation"] = [[动画]], + ["Common.AssetTypes.Label.Emote"] = [[动作]], + ["Common.BuildersClub.Label.PlanFree"] = [[免费]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Classic]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Builders Club 会员资格]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Turbo Builders Club 会员资格]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Outrageous Builders Club 会员资格]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[是]], + ["Common.BuildersClub.Label.No"] = [[否]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[从不]], + ["Common.BuildersClub.Label.Robux"] = [[罗宝]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[终身]], + ["Common.BuildersClub.Label.Membership"] = [[会员资格]], + ["CoreScripts.InGameMenu.EducationalPopup.MenuIconTooltip"] = [[]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[需要 Premium 会员]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Roblox Premium 将给你:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[每月 450 Robux]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Premium 会员限定福利]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[购买 Robux 时额外增送 10%]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price} / 月]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[你的平台不支持购买 Roblox Premium。请使用电脑购买 Premium 会员。]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[开发者似乎想给你发送购买 Roblox Premium 的提示,但你已经是 Premium 会员了!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[当前无法购买 Premium,请稍后重试。]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[每月 {robux} Robux]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[购买未完成,请重试。]], + ["CoreScripts.PremiumModal.Title.Error"] = [[错误]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[免费领取]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[升级]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[立即购买]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[购买 R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[取消]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[好]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[正在购买]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[此次交易不会影响你的帐户余额。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[此为测试性购买,你的帐户不会被收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[此为测试性购买。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[此物品需要 {BC_LEVEL}。请点按“升级”来升级你的 Builders Club!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[你在此次交易后的余额将为 R${BALANCE_FUTURE}]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[你的当前余额为 {BALANCE_NOW}。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[剩余的 {REMAINING_ROBUX} 罗宝将会加进你的余额中。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[由于你帐户的罗宝余额不足,购买失败。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[你的订阅等级不足,无法购买此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[你的罗宝不足,无法购买此物品。请离开此游戏,然后前往罗宝屏幕购买更多罗宝。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[你的罗宝不足,无法购买此物品。请购买更多罗宝。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[挑战内购买暂时停用,购买失败。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[购买过程中发生错误,购买失败。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[购买过程中发生错误,购买“{ITEM_NAME}”失败。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[当前无法取回你的余额信息。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[当前无法读取此物品价格。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[此限量物品已售完。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[此物品当前为非卖品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[若要购买此物品,请先创建 Roblox 帐户,如需更多信息,请访问 www.roblox.com。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[此场景已停用第三方物品贩售。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[因为你的账号为 13 岁以下,因此不允许购买此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[你已拥有此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[是否要免费领取“{ITEM_NAME}”?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[确定购买{ASSET_TYPE}“{ITEM_NAME}”?价格为]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[“{ITEM_NAME}”购买成功!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[你还需要 {NEEDED_AMOUNT} 罗宝才能购买“{ASSET_TYPE}{ITEM_NAME}”。是否要购买更多罗宝?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[产品]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[套装]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[是]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[正在取消]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[你没有订阅此项目,你的订阅状态并未改变。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[您已取消“{ITEM_NAME}”的订阅。订阅期结束前,你还将继续享有订阅福利。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[确定要取消你的“{ITEM_NAME}”订阅?订阅期结束前,你仍将继续享有订阅福利。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[确定订阅“{ITEM_NAME}”?价格为]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[订阅]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[每月 {PRICE}]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[订阅]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[你必须成为高级会员才能订阅!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[升级]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[你需要成为Premium 会员才能订阅,因此你无法购买此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[购买罗宝]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[此次交易后,你的余额将为 {BALANCE_FUTURE} 罗宝]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[你的罗宝不足,无法购买此物品。 ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[仅限高级会员]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[若要购买此物品,请先成为罗布乐思高级会员。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[只有成人可以在罗布乐思上进行购买。我同意我已满 18 岁,且为当前账户的所有者、所有者父母或所有者的法定监护人。我将对此项购买进行授权,并同意使用条款。]], +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-cn.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-cn.lua new file mode 100644 index 0000000..d949cd2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-cn.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[配饰]], + ["Common.AssetTypes.Label.Hat"] = [[帽子]], + ["Common.AssetTypes.Label.Hair"] = [[发型]], + ["Common.AssetTypes.Label.Face"] = [[表情]], + ["Common.AssetTypes.Label.Neck"] = [[颈部]], + ["Common.AssetTypes.Label.Shoulder"] = [[肩部]], + ["Common.AssetTypes.Label.Front"] = [[正面]], + ["Common.AssetTypes.Label.Back"] = [[背面]], + ["Common.AssetTypes.Label.Waist"] = [[腰部]], + ["Common.AssetTypes.Label.Animations"] = [[动画]], + ["Common.AssetTypes.Label.Audio"] = [[音频]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[虚拟形象动画]], + ["Common.AssetTypes.Label.Badges"] = [[徽章]], + ["Common.AssetTypes.Label.Decals"] = [[贴花]], + ["Common.AssetTypes.Label.Faces"] = [[表情]], + ["Common.AssetTypes.Label.GamePasses"] = [[游戏通行证]], + ["Common.AssetTypes.Label.Gear"] = [[装备]], + ["Common.AssetTypes.Label.Heads"] = [[头部]], + ["Common.AssetTypes.Label.Meshes"] = [[网格]], + ["Common.AssetTypes.Label.Models"] = [[模型]], + ["Common.AssetTypes.Label.Packages"] = [[套装]], + ["Common.AssetTypes.Label.Pants"] = [[裤子]], + ["Common.AssetTypes.Label.Places"] = [[场景]], + ["Common.AssetTypes.Label.Plugins"] = [[插件]], + ["Common.AssetTypes.Label.Shirts"] = [[衬衫]], + ["Common.AssetTypes.Label.TShirts"] = [[T 恤]], + ["Common.AssetTypes.Label.VipServers"] = [[VIP 服务器]], + ["Common.AssetTypes.Label.Run"] = [[奔跑]], + ["Common.AssetTypes.Label.Walk"] = [[步行]], + ["Common.AssetTypes.Label.Fall"] = [[下落]], + ["Common.AssetTypes.Label.Jump"] = [[跳跃]], + ["Common.AssetTypes.Label.Idle"] = [[闲置]], + ["Common.AssetTypes.Label.Swim"] = [[游泳]], + ["Common.AssetTypes.Label.Climb"] = [[攀爬]], + ["Common.AssetTypes.Label.Hats"] = [[帽子]], + ["Common.AssetTypes.Label.Shoulders"] = [[肩部]], + ["Common.AssetTypes.Label.Death"] = [[死亡]], + ["Common.AssetTypes.Label.Pose"] = [[姿势]], + ["Common.AssetTypes.Label.Head"] = [[头部]], + ["Common.AssetTypes.Label.TShirt"] = [[T 恤]], + ["Common.AssetTypes.Label.Shirt"] = [[衬衫]], + ["Common.AssetTypes.Label.Decal"] = [[贴花]], + ["Common.AssetTypes.Label.Model"] = [[模型]], + ["Common.AssetTypes.Label.Plugin"] = [[插件]], + ["Common.AssetTypes.Label.MeshPart"] = [[网格组件]], + ["Common.AssetTypes.Label.GamePass"] = [[游戏通行证]], + ["Common.AssetTypes.Label.Badge"] = [[徽章]], + ["Common.AssetTypes.Label.Package"] = [[套装]], + ["Common.AssetTypes.Label.Place"] = [[场景]], + ["Common.AssetTypes.Label.LeftArm"] = [[左臂]], + ["Common.AssetTypes.Label.LeftLeg"] = [[左腿]], + ["Common.AssetTypes.Label.RightArm"] = [[右臂]], + ["Common.AssetTypes.Label.RightLeg"] = [[右腿]], + ["Common.AssetTypes.Label.Torso"] = [[躯干]], + ["Common.AssetTypes.Label.Animation"] = [[动画]], + ["Common.AssetTypes.Label.Emote"] = [[动作]], + ["Common.BuildersClub.Label.PlanFree"] = [[免费]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Classic]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Builders Club 会员资格]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Turbo Builders Club 会员资格]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Outrageous Builders Club 会员资格]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[是]], + ["Common.BuildersClub.Label.No"] = [[否]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[从不]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[终身]], + ["Common.BuildersClub.Label.Membership"] = [[会员资格]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[需要 Premium 会员]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Roblox Premium 将给你:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[每月 450 Robux]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Premium 会员限定福利]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[购买 Robux 时额外增送 10%]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price} / 月]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[你的平台不支持购买 Roblox Premium。请使用电脑购买 Premium 会员。]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[开发者似乎想给你发送购买 Roblox Premium 的提示,但你已经是 Premium 会员了!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[当前无法购买 Premium,请稍后重试。]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[每月 {robux} Robux]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[购买未完成,请重试。]], + ["CoreScripts.PremiumModal.Title.Error"] = [[错误]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[免费领取]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[升级]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[立即购买]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[购买 R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[取消]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[好]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[正在购买]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[此次交易不会影响你的帐户余额。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[此为测试性购买,你的帐户不会被收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[此为测试性购买。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[此物品需要 {BC_LEVEL}。请点按“升级”来升级你的 Builders Club!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[你在此次交易后的余额将为 R${BALANCE_FUTURE}]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[你的当前余额为 {BALANCE_NOW}。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[剩余的 {REMAINING_ROBUX} Robux 将会加进你的余额中。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[由于你帐户的 Robux 余额不足,购买失败。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[你的订阅等级不足,无法购买此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[你的 Robux 不足,无法购买此物品。请离开此游戏,然后前往 Robux 屏幕购买更多 Robux。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[你的 Robux 不足,无法购买此物品。请购买更多 Robux。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[游戏内购买暂时停用,购买失败。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[购买过程中发生错误,购买失败。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[购买过程中发生错误,购买“{ITEM_NAME}”失败。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[当前无法取回你的余额信息。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[当前无法读取此物品价格。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[此限量物品已售完。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[此物品当前为非卖品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[若要购买此物品,请先创建 Roblox 帐户,如需更多信息,请访问 www.roblox.com。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[此场景已停用第三方物品贩售。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[因为你的账号为 13 岁以下,因此不允许购买此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[你已拥有此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[是否要免费领取“{ITEM_NAME}”?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[确定购买{ASSET_TYPE}“{ITEM_NAME}”?价格为]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[“{ITEM_NAME}”购买成功!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[你还需要 {NEEDED_AMOUNT} Robux 才能购买“{ASSET_TYPE}{ITEM_NAME}”。是否要购买更多 Robux?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[产品]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[套装]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[是]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[正在取消]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[你没有订阅此项目,你的订阅状态并未改变。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[您已取消“{ITEM_NAME}”的订阅。订阅期结束前,你还将继续享有订阅福利。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[确定要取消你的“{ITEM_NAME}”订阅?订阅期结束前,你仍将继续享有订阅福利。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[确定订阅“{ITEM_NAME}”?价格为]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[订阅]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[每月 {PRICE}]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[订阅]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[你必须成为 Premium 会员才能订阅!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[升级]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[你需要成为Premium 会员才能订阅,因此你无法购买此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[购买 Robux]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[此次交易后,你的余额将为 {BALANCE_FUTURE} Robux]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[你的 Robux 不足,无法购买此物品。 ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Premium 会员限定]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[该物品仅限 Roblox Premium 会员才能购买。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[只有成年用户才能在 Roblox 上进行购买。购买者声明本人已满 18 岁,且为当前账户的所有者、所有者父母或所有者的法定监护人。购买者将对此购买进行授权,并同意 Roblox 使用条款。]], +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-tw.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-tw.lua new file mode 100644 index 0000000..544181e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-tw.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[飾品]], + ["Common.AssetTypes.Label.Hat"] = [[帽子]], + ["Common.AssetTypes.Label.Hair"] = [[髮型]], + ["Common.AssetTypes.Label.Face"] = [[臉部]], + ["Common.AssetTypes.Label.Neck"] = [[頸部]], + ["Common.AssetTypes.Label.Shoulder"] = [[肩膀]], + ["Common.AssetTypes.Label.Front"] = [[正面]], + ["Common.AssetTypes.Label.Back"] = [[背面]], + ["Common.AssetTypes.Label.Waist"] = [[腰部]], + ["Common.AssetTypes.Label.Animations"] = [[動畫]], + ["Common.AssetTypes.Label.Audio"] = [[音訊]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[虛擬人偶動畫]], + ["Common.AssetTypes.Label.Badges"] = [[徽章]], + ["Common.AssetTypes.Label.Decals"] = [[貼花]], + ["Common.AssetTypes.Label.Faces"] = [[臉部]], + ["Common.AssetTypes.Label.GamePasses"] = [[遊戲證]], + ["Common.AssetTypes.Label.Gear"] = [[裝備]], + ["Common.AssetTypes.Label.Heads"] = [[頭部]], + ["Common.AssetTypes.Label.Meshes"] = [[網格]], + ["Common.AssetTypes.Label.Models"] = [[模型]], + ["Common.AssetTypes.Label.Packages"] = [[套裝]], + ["Common.AssetTypes.Label.Pants"] = [[褲子]], + ["Common.AssetTypes.Label.Places"] = [[地點]], + ["Common.AssetTypes.Label.Plugins"] = [[外掛程式]], + ["Common.AssetTypes.Label.Shirts"] = [[襯衫]], + ["Common.AssetTypes.Label.TShirts"] = [[T 恤]], + ["Common.AssetTypes.Label.VipServers"] = [[VIP 伺服器]], + ["Common.AssetTypes.Label.Run"] = [[奔跑]], + ["Common.AssetTypes.Label.Walk"] = [[步行]], + ["Common.AssetTypes.Label.Fall"] = [[跌落]], + ["Common.AssetTypes.Label.Jump"] = [[跳躍]], + ["Common.AssetTypes.Label.Idle"] = [[閒置]], + ["Common.AssetTypes.Label.Swim"] = [[游泳]], + ["Common.AssetTypes.Label.Climb"] = [[攀爬]], + ["Common.AssetTypes.Label.Hats"] = [[帽子]], + ["Common.AssetTypes.Label.Shoulders"] = [[肩膀]], + ["Common.AssetTypes.Label.Death"] = [[死亡]], + ["Common.AssetTypes.Label.Pose"] = [[姿勢]], + ["Common.AssetTypes.Label.Head"] = [[頭部]], + ["Common.AssetTypes.Label.TShirt"] = [[T 恤]], + ["Common.AssetTypes.Label.Shirt"] = [[襯衫]], + ["Common.AssetTypes.Label.Decal"] = [[貼花]], + ["Common.AssetTypes.Label.Model"] = [[模型]], + ["Common.AssetTypes.Label.Plugin"] = [[外掛程式]], + ["Common.AssetTypes.Label.MeshPart"] = [[網格零件]], + ["Common.AssetTypes.Label.GamePass"] = [[遊戲證]], + ["Common.AssetTypes.Label.Badge"] = [[徽章]], + ["Common.AssetTypes.Label.Package"] = [[套裝]], + ["Common.AssetTypes.Label.Place"] = [[地點]], + ["Common.AssetTypes.Label.LeftArm"] = [[左臂]], + ["Common.AssetTypes.Label.LeftLeg"] = [[左腿]], + ["Common.AssetTypes.Label.RightArm"] = [[右臂]], + ["Common.AssetTypes.Label.RightLeg"] = [[右腿]], + ["Common.AssetTypes.Label.Torso"] = [[軀幹]], + ["Common.AssetTypes.Label.Animation"] = [[動畫]], + ["Common.AssetTypes.Label.Emote"] = [[動作]], + ["Common.BuildersClub.Label.PlanFree"] = [[免費]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Classic]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Builders Club 會員資格]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Turbo Builders Club 會員資格]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Outrageous Builders Club 會員資格]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[是]], + ["Common.BuildersClub.Label.No"] = [[否]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[永不]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[Lifetime]], + ["Common.BuildersClub.Label.Membership"] = [[會員資格]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[需要 Premium]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Roblox Premium 將會給您:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[每個月 450 Robux]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[獲得 Premium 限定福利]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[購買 Robux 時獲得 10% 額外 Robux]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price} / 月]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[您的平台不支援購買 Roblox Premium,請使用電腦購買 Premium。]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[開發人員似乎想給您購買 Roblox Premium 的提示,但您已經是 Premium 會員了!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[目前無法購買 Premium,請稍後再試!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[每月 {robux} Robux]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[購買未完成,請重新嘗試。]], + ["CoreScripts.PremiumModal.Title.Error"] = [[錯誤]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[免費領取]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[升級]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[現在購買]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[購買 R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[取消]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[確定]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[正在購買]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[您的帳號餘額不受此交易影響。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[此為測試性購買,您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[此為測試性購買。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[此道具需要 {BC_LEVEL}。按下「升級」來升級您的 Builders Club!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[您在此交易後的餘額將為 R${BALANCE_FUTURE}]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[您目前的餘額為 {BALANCE_NOW}。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[剩餘的 {REMAINING_ROBUX} Robux 將會加入您的餘額。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[您的 Robux 不足,無法購買。您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[您的訂閱等級不足,無法購買此道具。您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[此道具的 Robux 價格超過您可以購買的 Robux 金額,請離開遊戲並前往 www.roblox.com 購買更多 Robux。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[此道具的 Robux 價格超過您可以購買的 Robux 金額,請前往 www.roblox.com 購買更多 Robux。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[遊戲中購買暫時停用,無法購買。您的帳號餘額維持不變,請稍後再試。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[購買過程發生錯誤,無法購買。您的帳號餘額維持不變,請稍後再試。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[購買過程發生錯誤,無法購買 {ITEM_NAME}。您的帳號餘額維持不變,請稍後再試。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[目前無法取得您的餘額。您的帳號餘額維持不變,請稍後再試。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[目前無法取得道具價格。您的帳號餘額維持不變,請稍後再試。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[此限量道具已售完,請前往 www.roblox.com 向其他使用者購買。您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[此道具目前為非賣品。您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[若要購買道具,請前往 www.roblox.com 建立 Roblox 帳號。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[此地點的第三方買賣目前停用,您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[您的帳號為 13 歲以下,不允許購買此道具。您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[您已擁有此道具,您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[您要免費領取 {ITEM_NAME} 嗎?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[購買{ASSET_TYPE} {ITEM_NAME}?價格為]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[成功購買 {ITEM_NAME}!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[您還需要 {NEEDED_AMOUNT} Robux 才能購買 {ASSET_TYPE} {ITEM_NAME}。您要加購 Robux 嗎?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[商品]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[組合包]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[是]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[正在取消]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[您沒有訂閱此項目,您的訂閱狀態沒有改變。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[您已取消 {ITEM_NAME} 訂閱。訂閱期間結束前,您還會繼續享有訂閱福利。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[確定取消 {ITEM_NAME} 訂閱?訂閱期間結束前,您還會繼續享有訂閱福利。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[確定訂閱 {ITEM_NAME}?價格為]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[訂閱]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[每月 {PRICE}]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[訂閱]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[您必須是 Premium 會員才可以訂閱!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[升級]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[您不是 Premium 會員,無法購買此道具。您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[購買 Robux]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[您在此交易後的餘額將為 {BALANCE_FUTURE} Robux]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[您的 Robux 不足,無法購買此道具。]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Premium 限定]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[若要購買此道具,請訂閱 Roblox Premium。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[此購買將會涉及到真實金錢交易。我同意我滿 18 歲,並是帳號主人的家長或法定監護人。我批准此筆購買並同意服務條款。]], +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/LocalizationService.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/LocalizationService.lua new file mode 100644 index 0000000..d960c10 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/LocalizationService.lua @@ -0,0 +1,207 @@ +local Root = script.Parent.Parent + +local ItemType = require(Root.Enums.ItemType) +local PurchaseError = require(Root.Enums.PurchaseError) +local Symbol = require(Root.Symbols.Symbol) + +local KeyMappings = require(script.Parent.KeyMappings) + +local FFlagChinaLicensingApp = settings():GetFFlag("ChinaLicensingApp") +local HARDCODED_CLB_TRANSLATIONS = { + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[由于你帐户的乐币余额不足,购买失败。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[你的乐币余额不足,无法购买此物品。]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[购买乐币]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[你在此次交易后的余额将为 {BALANCE_FUTURE} 乐币]], +} + +local GetFFlagProductPercentLocFix = require(Root.Flags.GetFFlagProductPercentLocFix) + +local DEBUG_LOCALIZATION = false + +--[[ + Locale-specific group delimiters for displaying numbers. Used to + format values like 100000 to strings like "100,000". This table + does not provide any info regarding decimal separators +]] +local groupDelimiterByLocale = { + ["en-us"] = ",", + ["en-gb"] = ",", + ["es-mx"] = ",", + ["es-es"] = ".", + ["fr-fr"] = " ", + ["de-de"] = " ", + ["pt-br"] = ".", + ["zh-cn"] = ",", + ["zh-cjv"] = ",", + ["zh-tw"] = ",", + ["ko-kr"] = ",", + ["ja-jp"] = ",", + ["it-it"] = " ", + ["ru-ru"] = ".", + ["id-id"] = ".", + ["vi-vn"] = ".", + ["th-th"] = ",", + ["tr-tr"] = ".", +} + +--[[ + This is a marker used to indicate that a provided param needs a locale-aware + formatting pass. We need this for nested localization and number formatting +]] +local FormattedParamTag = Symbol.named("FormattedParam") + +local function isFormattedParam(paramValue) + return typeof(paramValue) == "table" and paramValue[FormattedParamTag] == true +end + +local function createFormattedParam(formatFunc) + return { + [FormattedParamTag] = true, + format = formatFunc, + } +end + +--[[ + Looks up the given key in the localization context's translation table +]] +local function getLocalizedString(localizationContext, key) + local translations = localizationContext.translations + local fallbackTranslations = localizationContext.fallbackTranslations + + if FFlagChinaLicensingApp then + --[[ + We've been instructed by Tencent to replace all references to 'Robux' with a new + word for it; we don't want to do this as translations, since we don't want to affect + other Chinese language users. + + So our approach here is to short-circuit translation with hard-coded strings for the + case where we + a. Are in the China Licensing Build + b. Encounter the specific string keys that were problematic + ]] + if HARDCODED_CLB_TRANSLATIONS[key] ~= nil then + return HARDCODED_CLB_TRANSLATIONS[key] + end + end + + if DEBUG_LOCALIZATION and translations[key] == nil then + warn(("Missing translation for %s in locale %s"):format(key, localizationContext.locale)) + end + + if fallbackTranslations ~= nil and translations[key] == nil then + return fallbackTranslations[key] + end + + return translations[key] +end + +--[[ + Separates digits in a number into groups of three using the given + delimiter and ignoring anything after a decimal point + + This function is not locale-aware, and will not be useful for + formatting numbers in languages that use inconsistent group sizes like + Indian numbering systems and myriad-based Chinese numbering systems +]] +local function addGroupDelimiters(numberStr, delimiter) + local delimiterReplace = string.format("%%1%s%%2", delimiter) + + -- Repeat substitution until there are no more unbroken four-digit sequences + local substitutions + repeat + numberStr, substitutions = string.gsub(numberStr, "^(-?%d+)(%d%d%d)", delimiterReplace) + until substitutions == 0 + + return numberStr +end + +local LocalizationService = {} + +function LocalizationService.formatNumber(localizationContext, number) + local delimiter = groupDelimiterByLocale[localizationContext.locale] or "," + return addGroupDelimiters(number, delimiter) +end + +--[[ + Generates a placeholder for a number param that needs locale-aware formatting +]] +function LocalizationService.numberParam(number) + return createFormattedParam(function(localizationContext) + return LocalizationService.formatNumber(localizationContext, number) + end) +end + +--[[ + Generates a placeholder for a param substitution that needs + its own localization pass +]] +function LocalizationService.nestedKeyParam(key) + return createFormattedParam(function(localizationContext) + return getLocalizedString(localizationContext, key) + end) +end + +--[[ + Utility function returns the localization key for a given item type +]] +function LocalizationService.getKeyFromItemType(itemType) + assert(ItemType.isMember(itemType) or typeof(itemType) == "number" or typeof(itemType) == "string", + "provided item type " ..tostring(itemType) .." must be a number, string, or ItemType enum") + + local localizationKey + if itemType == ItemType.Bundle then + localizationKey = "CoreScripts.PurchasePrompt.ItemType.Bundle" + else + localizationKey = KeyMappings.AssetTypeById[tostring(itemType)] + end + + if DEBUG_LOCALIZATION and localizationKey == nil then + warn("Invalid Asset Type id " .. tostring(itemType)) + end + + return localizationKey +end + +--[[ + Utility function to retrieve relevant localization key for various + types of errors that may be encountered +]] +function LocalizationService.getErrorKey(errorType) + assert(PurchaseError.isMember(errorType), + "provided value " .. tostring(errorType) .. " is not a member of PurchaseError enum") + + return KeyMappings.PurchaseErrorKey[errorType] +end + +--[[ + The primary function of this object + + Retrieves a localized string from the provided context with the + given key and performs parameter substitutions +]] +function LocalizationService.getString(localizationContext, key, params) + assert(localizationContext ~= nil, "Must provide valid localization context") + + local localizedString = getLocalizedString(localizationContext, key) + + if params ~= nil then + for param, value in pairs(params) do + local replacement = value + local paramPlaceholder = ("{%s}"):format(param) + + if isFormattedParam(value) then + replacement = value.format(localizationContext) + end + + if GetFFlagProductPercentLocFix() then + localizedString = string.gsub(localizedString, paramPlaceholder, function() return replacement end) + else + localizedString = string.gsub(localizedString, paramPlaceholder, replacement) + end + end + end + + return localizedString +end + +return LocalizationService diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/getLocalizationContext.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/getLocalizationContext.lua new file mode 100644 index 0000000..25945c6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/getLocalizationContext.lua @@ -0,0 +1,27 @@ +local Locales = script.Parent.Locales + +local FALLBACK_LOCALE = "en-us" + +local function getLocalizationContext(locale) + local primary = Locales:FindFirstChild(locale) + + if primary ~= nil then + return { + locale = locale, + translations = require(primary), + fallbackTranslations = require(Locales:FindFirstChild(FALLBACK_LOCALE)) + } + else + --[[ + If the requested language is not available, fallback to + the default; for now, this will be American English. + ]] + local fallback = Locales:FindFirstChild(FALLBACK_LOCALE) + return { + locale = FALLBACK_LOCALE, + translations = require(fallback), + } + end +end + +return getLocalizationContext \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Misc/Constants.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Misc/Constants.lua new file mode 100644 index 0000000..ac2b6b8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Misc/Constants.lua @@ -0,0 +1,9 @@ +local Root = script.Parent.Parent + +local strict = require(Root.strict) + +return strict({ + ABTests = strict({ + ADULT_CONFIRMATION = "AllUsers.Payments.U13PurchaseCombinedABTest", + }, "Constants.ABTests") +}, "Constants") diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Misc/createSignal.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Misc/createSignal.lua new file mode 100644 index 0000000..029adc1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Misc/createSignal.lua @@ -0,0 +1,75 @@ +--[[ + This is a simple signal implementation that has a dead-simple API. + + local signal = createSignal() + + local disconnect = signal:subscribe(function(foo) + print("Cool foo:", foo) + end) + + signal:fire("something") + + disconnect() +]] + +local function addToMap(map, addKey, addValue) + local new = {} + + for key, value in pairs(map) do + new[key] = value + end + + new[addKey] = addValue + + return new +end + +local function removeFromMap(map, removeKey) + local new = {} + + for key, value in pairs(map) do + if key ~= removeKey then + new[key] = value + end + end + + return new +end + +local function createSignal() + local connections = {} + + local function subscribe(_, callback) + assert(typeof(callback) == "function", "Can only subscribe to signals with a function.") + + local connection = { + callback = callback, + } + + connections = addToMap(connections, callback, connection) + + local function disconnect() + assert(not connection.disconnected, "Listeners can only be disconnected once.") + + connection.disconnected = true + connections = removeFromMap(connections, callback) + end + + return disconnect + end + + local function fire(_, ...) + for callback, connection in pairs(connections) do + if not connection.disconnected then + callback(...) + end + end + end + + return { + subscribe = subscribe, + fire = fire, + } +end + +return createSignal \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Models/PremiumProduct.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Models/PremiumProduct.lua new file mode 100644 index 0000000..202baf8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Models/PremiumProduct.lua @@ -0,0 +1,44 @@ +--[[ + Docs: https://premiumfeatures.roblox.com/docs#!/PremiumFeaturesProducts/get_v1_products + + Provides a model for response from product purchase request. +]] +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local t = require(LuaPackages.t) + +local strict = require(Root.strict) + +local checkJson = t.interface({ + productId = t.number, + mobileProductId = t.string, + robuxAmount = t.number, + isSubscriptionOnly = t.boolean, + premiumFeatureTypeName = t.string, + description = t.string, + price = t.interface({ + amount = t.number, + currency = t.interface({ + currencySymbol = t.string + }) + }) +}) + +return function(jsonData) + local success, error = checkJson(jsonData) + if not success then + return nil + end + + return { + productId = jsonData.productId, + mobileProductId = jsonData.mobileProductId, + robuxAmount = jsonData.robuxAmount, + isSubscriptionOnly = jsonData.isSubscriptionOnly, + premiumFeatureTypeName = jsonData.premiumFeatureTypeName, + description = jsonData.description, + price = jsonData.price.amount, + currencySymbol = jsonData.price.currency.currencySymbol, + } +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/NativeProducts.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/NativeProducts.lua new file mode 100644 index 0000000..7f35ee2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/NativeProducts.lua @@ -0,0 +1,181 @@ +--[[ + New products (Premium): + + Product naming conventions: + All prefixed with "com.roblox.robloxmobile" + iOS: PascalCase product name + Android, Amazon, UWP: all lowercase product name + + Product naming conventions: + iOS: prefixed with "com.roblox.robloxmobile", PascalCase product name + Android, Amazon, UWP: prefixed with "com.roblox.client", all lowercase product name + + CLILUACORE-310: Ideally we would retrieve these values via native platform code, + like we do with Xbox, or from some reasonable endpoint. As it's implemented now, + we need to make client changes in order to introduce new products +]] + +local NativeProducts = { + IOS = { + PremiumSubscribed = { + { + robuxValue = 88, + productId = "com.roblox.robloxmobile.Premium88Subscribed", + }, { + robuxValue = 175, + productId = "com.roblox.robloxmobile.Premium175Subscribed", + }, { + robuxValue = 265, + productId = "com.roblox.robloxmobile.Premium265Subscribed", + }, { + robuxValue = 350, + productId = "com.roblox.robloxmobile.Premium350Subscribed", + }, { + robuxValue = 440, + productId = "com.roblox.robloxmobile.Premium440Subscribed2", + }, { + robuxValue = 880, + productId = "com.roblox.robloxmobile.Premium880Subscribed", + }, { + robuxValue = 1870, + productId = "com.roblox.robloxmobile.Premium1870Subscribed", + }, + }, + PremiumNotSubscribed = { + { + robuxValue = 80, + productId = "com.roblox.robloxmobile.Premium80Robux", + }, { + robuxValue = 160, + productId = "com.roblox.robloxmobile.Premium160Robux", + }, { + robuxValue = 240, + productId = "com.roblox.robloxmobile.Premium240Robux", + }, { + robuxValue = 320, + productId = "com.roblox.robloxmobile.Premium320Robux", + }, { + robuxValue = 400, + productId = "com.roblox.robloxmobile.Premium400Robux", + }, { + robuxValue = 800, + productId = "com.roblox.robloxmobile.Premium800Robux", + }, { + robuxValue = 1700, + productId = "com.roblox.robloxmobile.Premium1700Robux", + }, + }, + }, + Standard = { + PremiumSubscribed = { + { + robuxValue = 88, + productId = "com.roblox.robloxmobile.premium88subscribed", + }, { + robuxValue = 175, + productId = "com.roblox.robloxmobile.premium175subscribed", + }, { + robuxValue = 265, + productId = "com.roblox.robloxmobile.premium265subscribed", + }, { + robuxValue = 350, + productId = "com.roblox.robloxmobile.premium350subscribed", + }, { + robuxValue = 440, + productId = "com.roblox.robloxmobile.premium440subscribed2", + }, { + robuxValue = 880, + productId = "com.roblox.robloxmobile.premium880subscribed", + }, { + robuxValue = 1870, + productId = "com.roblox.robloxmobile.premium1870subscribed", + } + }, + PremiumSubscribedLarger = { + { + robuxValue = 88, + productId = "com.roblox.robloxmobile.premium88subscribed", + }, { + robuxValue = 175, + productId = "com.roblox.robloxmobile.premium175subscribed", + }, { + robuxValue = 265, + productId = "com.roblox.robloxmobile.premium265subscribed", + }, { + robuxValue = 350, + productId = "com.roblox.robloxmobile.premium350subscribed", + }, { + robuxValue = 440, + productId = "com.roblox.robloxmobile.premium440subscribed2", + }, { + robuxValue = 880, + productId = "com.roblox.robloxmobile.premium880subscribed", + }, { + robuxValue = 1870, + productId = "com.roblox.robloxmobile.premium1870subscribed", + }, { + robuxValue = 4950, + productId = "com.roblox.robloxmobile.premium4950subscribed", + }, { + robuxValue = 11000, + productId = "com.roblox.robloxmobile.premium11000subscribed", + }, + }, + PremiumNotSubscribed = { + { + robuxValue = 80, + productId = "com.roblox.robloxmobile.premium80robux", + }, { + robuxValue = 160, + productId = "com.roblox.robloxmobile.premium160robux", + }, { + robuxValue = 240, + productId = "com.roblox.robloxmobile.premium240robux", + }, { + robuxValue = 320, + productId = "com.roblox.robloxmobile.premium320robux", + }, { + robuxValue = 400, + productId = "com.roblox.robloxmobile.premium400robux", + }, { + robuxValue = 800, + productId = "com.roblox.robloxmobile.premium800robux", + }, { + robuxValue = 1700, + productId = "com.roblox.robloxmobile.premium1700robux", + }, + }, + PremiumNotSubscribedLarger = { + { + robuxValue = 80, + productId = "com.roblox.robloxmobile.premium80robux", + }, { + robuxValue = 160, + productId = "com.roblox.robloxmobile.premium160robux", + }, { + robuxValue = 240, + productId = "com.roblox.robloxmobile.premium240robux", + }, { + robuxValue = 320, + productId = "com.roblox.robloxmobile.premium320robux", + }, { + robuxValue = 400, + productId = "com.roblox.robloxmobile.premium400robux", + }, { + robuxValue = 800, + productId = "com.roblox.robloxmobile.premium800robux", + }, { + robuxValue = 1700, + productId = "com.roblox.robloxmobile.premium1700robux", + }, { + robuxValue = 4500, + productId = "com.roblox.robloxmobile.premium4500robux", + }, { + robuxValue = 10000, + productId = "com.roblox.robloxmobile.premium10000robux", + }, + }, + } +} + +return NativeProducts \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/XboxCatalogData.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/XboxCatalogData.lua new file mode 100644 index 0000000..ccb9c1d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/XboxCatalogData.lua @@ -0,0 +1,46 @@ +--[[ + CLILUACORE-311: We need to find a proper way to encapsulate this; + conditionally depending on PlatformService is bad! +]] +local PlatformService = nil +pcall(function() + PlatformService = game:GetService("PlatformService") +end) + +local Promise = require(script.Parent.Parent.Promise) + +local function parseRobuxValue(productInfo) + local rawText = productInfo and productInfo.Name + local noJunk = string.gsub(rawText, ",", "") + noJunk = noJunk and string.match(noJunk, "[0-9]+") or nil + return noJunk and tonumber(noJunk) or 1000 +end + +local XboxCatalogData = {} + +function XboxCatalogData.GetCatalogInfoAsync() + if PlatformService == nil then + error("PlatformService unavailable; are you on XboxOne?") + end + + local promisified = Promise.promisify(function() + return PlatformService:BeginGetCatalogInfo() + end) + + return promisified() + :andThen(function(catalogInfo) + local availableProducts = {} + + for _, productInfo in pairs(catalogInfo) do + local product = { + robuxValue = parseRobuxValue(productInfo), + productId = productInfo.ProductId + } + table.insert(availableProducts, product) + end + + return availableProducts + end) +end + +return XboxCatalogData diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/getUpsellFlow.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/getUpsellFlow.lua new file mode 100644 index 0000000..73a2b97 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/getUpsellFlow.lua @@ -0,0 +1,24 @@ +local Root = script.Parent.Parent + +local UpsellFlow = require(Root.Enums.UpsellFlow) + +local FFlagChinaLicensingApp = settings():GetFFlag("ChinaLicensingApp") +local GetFFlagDisableRobuxUpsell = require(Root.Flags.GetFFlagDisableRobuxUpsell) + +local function getUpsellFlow(platform) + if FFlagChinaLicensingApp or GetFFlagDisableRobuxUpsell() then + return UpsellFlow.Unavailable + end + + if platform == Enum.Platform.Windows or platform == Enum.Platform.OSX or platform == Enum.Platform.Linux then + return UpsellFlow.Web + elseif platform == Enum.Platform.IOS or platform == Enum.Platform.Android or platform == Enum.Platform.UWP then + return UpsellFlow.Mobile + elseif platform == Enum.Platform.XBoxOne then + return UpsellFlow.Xbox + end + + return UpsellFlow.None +end + +return getUpsellFlow diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/selectRobuxProduct.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/selectRobuxProduct.lua new file mode 100644 index 0000000..7c60d03 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/selectRobuxProduct.lua @@ -0,0 +1,52 @@ +local XboxCatalogData = require(script.Parent.XboxCatalogData) +local NativeProducts = require(script.Parent.NativeProducts) + +local Promise = require(script.Parent.Parent.Promise) + +local function sortAscending(a, b) + return a.robuxValue < b.robuxValue +end + +local function selectProduct(neededRobux, availableProducts) + table.sort(availableProducts, sortAscending) + + for _, product in ipairs(availableProducts) do + if product.robuxValue >= neededRobux then + return Promise.resolve(product) + end + end + + return Promise.reject() +end + +local function selectRobuxProduct(platform, neededRobux, userIsSubscribed) + -- Premium is not yet enabled for XBox, so we always use the existing approach + if platform == Enum.Platform.XBoxOne then + return XboxCatalogData.GetCatalogInfoAsync() + :andThen(function(availableProducts) + return selectProduct(neededRobux, availableProducts) + end) + end + + local productOptions + if platform == Enum.Platform.IOS then + productOptions = userIsSubscribed + and NativeProducts.IOS.PremiumSubscribed + or NativeProducts.IOS.PremiumNotSubscribed + else -- This product format is standard for other supported platforms (Android, Amazon, and UWP) + if platform == Enum.Platform.Android then + -- Contains upsell for 4500 and 10000 packages only available on android + productOptions = userIsSubscribed + and NativeProducts.Standard.PremiumSubscribedLarger + or NativeProducts.Standard.PremiumNotSubscribedLarger + else + productOptions = userIsSubscribed + and NativeProducts.Standard.PremiumSubscribed + or NativeProducts.Standard.PremiumNotSubscribed + end + end + + return selectProduct(neededRobux, productOptions) +end + +return selectRobuxProduct \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/selectRobuxProduct.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/selectRobuxProduct.spec.lua new file mode 100644 index 0000000..7e0837a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/selectRobuxProduct.spec.lua @@ -0,0 +1,57 @@ +return function() + local selectRobuxProduct = require(script.Parent.selectRobuxProduct) + + describe("premium products", function() + it("should select the appropriate premium product when user IS NOT premium", function() + selectRobuxProduct(Enum.Platform.IOS, 80, false) + :andThen(function(iosProduct) + expect(iosProduct).to.be.ok() + expect(iosProduct.productId).to.equal("com.roblox.robloxmobile.Premium80Robux") + end) + + selectRobuxProduct(Enum.Platform.Android, 80, false) + :andThen(function(androidProduct) + expect(androidProduct).to.be.ok() + expect(androidProduct.productId).to.equal("com.roblox.robloxmobile.premium80robux") + end) + + selectRobuxProduct(Enum.Platform.Android, 9000, false) + :andThen(function(androidProduct) + expect(androidProduct).to.be.ok() + expect(androidProduct.productId).to.equal("com.roblox.robloxmobile.premium10000robux") + end) + + selectRobuxProduct(Enum.Platform.UWP, 80, false) + :andThen(function(uwpProduct) + expect(uwpProduct).to.be.ok() + expect(uwpProduct.productId).to.equal("com.roblox.robloxmobile.premium80robux") + end) + end) + + it("should select the appropriate premium product when user IS premium", function() + selectRobuxProduct(Enum.Platform.IOS, 88, true) + :andThen(function(iosProduct) + expect(iosProduct).to.be.ok() + expect(iosProduct.productId).to.equal("com.roblox.robloxmobile.Premium88Subscribed") + end) + + selectRobuxProduct(Enum.Platform.Android, 88, true) + :andThen(function(androidProduct) + expect(androidProduct).to.be.ok() + expect(androidProduct.productId).to.equal("com.roblox.robloxmobile.premium88subscribed") + end) + + selectRobuxProduct(Enum.Platform.Android, 9000, true) + :andThen(function(androidProduct) + expect(androidProduct).to.be.ok() + expect(androidProduct.productId).to.equal("com.roblox.robloxmobile.premium11000subscribed") + end) + + selectRobuxProduct(Enum.Platform.UWP, 88, true) + :andThen(function(uwpProduct) + expect(uwpProduct).to.be.ok() + expect(uwpProduct.productId).to.equal("com.roblox.robloxmobile.premium88subscribed") + end) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getAccountInfo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getAccountInfo.lua new file mode 100644 index 0000000..823bf8f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getAccountInfo.lua @@ -0,0 +1,40 @@ +local Root = script.Parent.Parent +local UserInputService = game:GetService("UserInputService") + +local PurchaseError = require(Root.Enums.PurchaseError) +local Promise = require(Root.Promise) + +local MAX_ROBUX = 2147483647 + +local function getAccountInfo(network, externalSettings) + if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then + return Promise.all({ + accountInfo = network.getAccountInfo(), + xboxBalance = network.getXboxRobuxBalance(), + }):andThen(function(results) + local accountInfo = results.accountInfo + -- Override balance with platform-specific balance + accountInfo.RobuxBalance = results.xboxBalance.Robux + + return Promise.resolve(accountInfo) + end) + end + + return network.getAccountInfo() + :andThen(function(result) + --[[ + In studio, we falsely report that users have the maximum amount + of robux, so that they can always test the normal purchase flow + ]] + if externalSettings.isStudio() then + result.RobuxBalance = MAX_ROBUX + end + + return Promise.resolve(result) + end) + :catch(function(failure) + return Promise.reject(PurchaseError.UnknownFailure) + end) +end + +return getAccountInfo \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getBundleDetails.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getBundleDetails.lua new file mode 100644 index 0000000..703f3e9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getBundleDetails.lua @@ -0,0 +1,16 @@ +local Root = script.Parent.Parent + +local PurchaseError = require(Root.Enums.PurchaseError) +local Promise = require(Root.Promise) + +local function getBundleDetails(network, bundleId) + return network.getBundleDetails(bundleId) + :andThen(function(result) + return Promise.resolve(result) + end) + :catch(function(failure) + return Promise.reject(PurchaseError.UnknownFailure) + end) +end + +return getBundleDetails \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getIsAlreadyOwned.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getIsAlreadyOwned.lua new file mode 100644 index 0000000..d490666 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getIsAlreadyOwned.lua @@ -0,0 +1,14 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") + +local PurchaseError = require(Root.Enums.PurchaseError) +local Promise = require(Root.Promise) + +local function getIsAlreadyOwned(network, id, infoType) + return network.getPlayerOwns(Players.LocalPlayer, id, infoType) + :catch(function(failure) + return Promise.reject(PurchaseError.UnknownFailure) + end) +end + +return getIsAlreadyOwned \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getPremiumProductInfo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getPremiumProductInfo.lua new file mode 100644 index 0000000..cef4b5a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getPremiumProductInfo.lua @@ -0,0 +1,12 @@ +local Root = script.Parent.Parent +local PurchaseError = require(Root.Enums.PurchaseError) +local Promise = require(Root.Promise) + +local function getPremiumProductInfo(network) + return network.getPremiumProductInfo() + :catch(function(failure) + return Promise.reject(PurchaseError.UnknownFailureNoItemName) + end) +end + +return getPremiumProductInfo \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getPremiumUpsellPrecheck.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getPremiumUpsellPrecheck.lua new file mode 100644 index 0000000..8219305 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getPremiumUpsellPrecheck.lua @@ -0,0 +1,14 @@ +local Root = script.Parent.Parent +local Promise = require(Root.Promise) + +local function getPremiumUpsellPrecheck(network) + return network.getPremiumUpsellPrecheck() + :andThen(function(results) + return Promise.resolve(true) + end) + :catch(function(failure) + return Promise.resolve(false) + end) +end + +return getPremiumUpsellPrecheck \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getProductInfo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getProductInfo.lua new file mode 100644 index 0000000..3eb76c4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getProductInfo.lua @@ -0,0 +1,13 @@ +local Root = script.Parent.Parent + +local PurchaseError = require(Root.Enums.PurchaseError) +local Promise = require(Root.Promise) + +local function getProductInfo(network, id, infoType) + return network.getProductInfo(id, infoType) + :catch(function(failure) + return Promise.reject(PurchaseError.UnknownFailureNoItemName) + end) +end + +return getProductInfo \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getProductPurchasableDetails.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getProductPurchasableDetails.lua new file mode 100644 index 0000000..cb9e713 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getProductPurchasableDetails.lua @@ -0,0 +1,16 @@ +local Root = script.Parent.Parent + +local PurchaseError = require(Root.Enums.PurchaseError) +local Promise = require(Root.Promise) + +local function getProductPurchasableDetails(network, productId) + return network.getProductPurchasableDetails(productId) + :andThen(function(result) + return Promise.resolve(result) + end) + :catch(function(failure) + return Promise.reject(PurchaseError.UnknownFailure) + end) +end + +return getProductPurchasableDetails \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getToolAsset.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getToolAsset.lua new file mode 100644 index 0000000..4c95897 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getToolAsset.lua @@ -0,0 +1,22 @@ +local function getToolAsset(network, assetId) + return network.loadAssetForEquip(assetId) + :andThen(function(tool) + if tool:IsA("Tool") then + return tool + else + local children = tool:GetChildren() + for _, child in ipairs(children) do + if child:IsA("Tool") then + return child + end + end + end + end) + :catch(function(failure) + -- There isn't really much we can do here with error reporting, + -- since the failure is unrelated to purchasing itself + return nil + end) +end + +return getToolAsset \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/performPurchase.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/performPurchase.lua new file mode 100644 index 0000000..a450e37 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/performPurchase.lua @@ -0,0 +1,54 @@ +local Root = script.Parent.Parent + +local PurchaseError = require(Root.Enums.PurchaseError) +local Promise = require(Root.Promise) + +local function performPurchase(network, infoType, productId, expectedPrice, requestId, isRobloxPurchase) + return network.performPurchase(infoType, productId, expectedPrice, requestId, isRobloxPurchase) + :andThen(function(result) + --[[ + User might purchase the product through the web after having + opened the purchase prompt, so an AlreadyOwned status is + acceptable. + ]] + + --[[ + Assets and Gamepasses use the new economy purchasing endpoint. Developer Products still use + the old marketplace/submitpurchase endpoint. + ]] + if infoType == Enum.InfoType.Asset or infoType == Enum.InfoType.GamePass or infoType == Enum.InfoType.Bundle then + if result.purchased or result.reason == "AlreadyOwned" then + return Promise.resolve(result) + elseif result.reason == "EconomyDisabled" then + return Promise.reject(PurchaseError.PurchaseDisabled) + else + return Promise.reject(PurchaseError.UnknownFailure) + end + elseif infoType == Enum.InfoType.Product then + if result.success or result.status == "AlreadyOwned" then + return Promise.resolve(result) + elseif not result.receipt then + return Promise.reject(PurchaseError.UnknownFailure) + else + if result.status == "EconomyDisabled" then + return Promise.reject(PurchaseError.PurchaseDisabled) + else + return Promise.reject(PurchaseError.UnknownFailure) + end + end + elseif infoType == Enum.InfoType.Subscription then + if result.success or result.reason == "AlreadyOwned" then + return Promise.resolve(result) + elseif result.reason == "EconomyDisabled" then + return Promise.reject(PurchaseError.PurchaseDisabled) + else + return Promise.reject(PurchaseError.UnknownFailure) + end + end + + end, function(failure) + return Promise.reject(PurchaseError.UnknownFailure) + end) +end + +return performPurchase \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/postPremiumImpression.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/postPremiumImpression.lua new file mode 100644 index 0000000..65ddc62 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/postPremiumImpression.lua @@ -0,0 +1,5 @@ +local function postPremiumImpression(network) + return network.postPremiumImpression() +end + +return postPremiumImpression \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Promise.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Promise.lua new file mode 100644 index 0000000..9ce6c7e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Promise.lua @@ -0,0 +1,422 @@ +--[[ + An implementation of Promises similar to Promise/A+. +]] + +local PROMISE_DEBUG = false + +--[[ + Packs a number of arguments into a table and returns its length. + Used to cajole varargs without dropping sparse values. +]] +local function pack(...) + local len = select("#", ...) + + return len, { ... } +end + +--[[ + wpcallPacked is a version of xpcall that: + * Returns the length of the result first + * Returns the result packed into a table + * Passes extra arguments through to the passed function, which xpcall does not + * Issues a warning if PROMISE_DEBUG is enabled +]] +local function wpcallPacked(f, ...) + local argsLength, args = pack(...) + + local body = function() + return f(unpack(args, 1, argsLength)) + end + + local resultLength, result = pack(xpcall(body, debug.traceback)) + + -- If promise debugging is on, warn whenever a pcall fails. + -- This is useful for debugging issues within the Promise implementation + -- itself. + if PROMISE_DEBUG and not result[1] then + warn(result[2]) + end + + return resultLength, result +end + +--[[ + Creates a function that invokes a callback with correct error handling and + resolution mechanisms. +]] +local function createAdvancer(callback, resolve, reject) + return function(...) + local resultLength, result = wpcallPacked(callback, ...) + local ok = result[1] + + if ok then + resolve(unpack(result, 2, resultLength)) + else + reject(unpack(result, 2, resultLength)) + end + end +end + +local function isEmpty(t) + return next(t) == nil +end + +local Promise = {} +Promise.__index = Promise + +Promise.Status = { + Started = "Started", + Resolved = "Resolved", + Rejected = "Rejected", +} + +--[[ + Constructs a new Promise with the given initializing callback. + This is generally only called when directly wrapping a non-promise API into + a promise-based version. + The callback will receive 'resolve' and 'reject' methods, used to start + invoking the promise chain. + For example: + local function get(url) + return Promise.new(function(resolve, reject) + spawn(function() + resolve(HttpService:GetAsync(url)) + end) + end) + end + get("https://google.com") + :andThen(function(stuff) + print("Got some stuff!", stuff) + end) +]] +function Promise.new(callback) + local promise = { + -- Used to locate where a promise was created + _source = debug.traceback(), + + -- A tag to identify us as a promise + _type = "Promise", + + _status = Promise.Status.Started, + + -- A table containing a list of all results, whether success or failure. + -- Only valid if _status is set to something besides Started + _values = nil, + + -- Lua doesn't like sparse arrays very much, so we explicitly store the + -- length of _values to handle middle nils. + _valuesLength = -1, + + -- If an error occurs with no observers, this will be set. + _unhandledRejection = false, + + -- Queues representing functions we should invoke when we update! + _queuedResolve = {}, + _queuedReject = {}, + } + + setmetatable(promise, Promise) + + local function resolve(...) + promise:_resolve(...) + end + + local function reject(...) + promise:_reject(...) + end + + local _, result = wpcallPacked(callback, resolve, reject) + local ok = result[1] + local err = result[2] + + if not ok and promise._status == Promise.Status.Started then + reject(err) + end + + return promise +end + +--[[ + Create a promise that represents the immediately resolved value. +]] +function Promise.resolve(value) + return Promise.new(function(resolve) + resolve(value) + end) +end + +--[[ + Create a promise that represents the immediately rejected value. +]] +function Promise.reject(value) + return Promise.new(function(_, reject) + reject(value) + end) +end + +--[[ + Returns a new promise that: + * is resolved when all input promises resolve + * is rejected if ANY input promises reject +]] +function Promise.all(...) + local promises = {...} + + -- check if we've been given a list of promises, not just a variable number of promises + if type(promises[1]) == "table" and promises[1]._type ~= "Promise" then + -- we've been given a table of promises already + promises = promises[1] + end + + return Promise.new(function(resolve, reject) + local isResolved = false + local results = {} + local totalCompleted = 0 + local promiseCount = 0 + + -- If we're agnostic about whether the promises are a table + -- or a list, users can provide tables with useful keys if they like + for _ in pairs(promises) do + promiseCount = promiseCount + 1 + end + + local function promiseCompleted(key, result) + if isResolved then + return + end + + results[key] = result + totalCompleted = totalCompleted + 1 + + if totalCompleted == promiseCount then + resolve(results) + isResolved = true + end + end + + if promiseCount == 0 then + resolve(results) + isResolved = true + return + end + + for key, promise in pairs(promises) do + -- if a promise isn't resolved yet, add listeners for when it does + if promise._status == Promise.Status.Started then + promise:andThen(function(result) + promiseCompleted(key, result) + end):catch(function(reason) + isResolved = true + reject(reason) + end) + + -- if a promise is already resolved, move on + elseif promise._status == Promise.Status.Resolved then + promiseCompleted(key, unpack(promise._values)) + + -- if a promise is rejected, reject the whole chain + else --if promise._status == Promise.Status.Rejected then + -- We catch here to indicate that the intermediate rejection + -- has been handled and seen + promise:catch(function(reason) + isResolved = true + reject(unpack(promise._values)) + end) + end + end + end) +end + +--[[ + Is the given object a Promise instance? +]] +function Promise.is(object) + if type(object) ~= "table" then + return false + end + + return object._type == "Promise" +end + +--[[ + Construct a promise from a yielding function +]] +function Promise.promisify(callback) + return function(...) + local args = {...} + local argLength = select("#", ...) + + return Promise.new(function(resolve, reject) + spawn(function() + local success, result = pcall(callback, unpack(args, 1, argLength)) + + if success then + resolve(result) + else + reject(result) + end + end) + end) + end +end + +function Promise:getStatus() + return self._status +end + +--[[ + Creates a new promise that receives the result of this promise. + The given callbacks are invoked depending on that result. +]] +function Promise:andThen(successHandler, failureHandler) + self._unhandledRejection = false + + -- Create a new promise to follow this part of the chain + return Promise.new(function(resolve, reject) + -- Our default callbacks just pass values onto the next promise. + -- This lets success and failure cascade correctly! + + local successCallback = resolve + if successHandler then + successCallback = createAdvancer(successHandler, resolve, reject) + end + + local failureCallback = reject + if failureHandler then + failureCallback = createAdvancer(failureHandler, resolve, reject) + end + + if self._status == Promise.Status.Started then + -- If we haven't resolved yet, put ourselves into the queue + table.insert(self._queuedResolve, successCallback) + table.insert(self._queuedReject, failureCallback) + elseif self._status == Promise.Status.Resolved then + -- This promise has already resolved! Trigger success immediately. + successCallback(unpack(self._values, 1, self._valuesLength)) + elseif self._status == Promise.Status.Rejected then + -- This promise died a terrible death! Trigger failure immediately. + failureCallback(unpack(self._values, 1, self._valuesLength)) + end + end) +end + +--[[ + Used to catch any errors that may have occurred in the promise. +]] +function Promise:catch(failureCallback) + return self:andThen(nil, failureCallback) +end + +--[[ + Yield until the promise is completed. + This matches the execution model of normal Roblox functions. +]] +function Promise:await() + self._unhandledRejection = false + + if self._status == Promise.Status.Started then + local result + local resultLength + local bindable = Instance.new("BindableEvent") + + self:andThen(function(...) + result = {...} + resultLength = select("#", ...) + bindable:Fire(true) + end, function(...) + result = {...} + resultLength = select("#", ...) + bindable:Fire(false) + end) + + local ok = bindable.Event:Wait() + bindable:Destroy() + + return ok, unpack(result, 1, resultLength) + elseif self._status == Promise.Status.Resolved then + return true, unpack(self._values, 1, self._valuesLength) + elseif self._status == Promise.Status.Rejected then + return false, unpack(self._values, 1, self._valuesLength) + end +end + +function Promise:_resolve(...) + if self._status ~= Promise.Status.Started then + return + end + + local argLength = select("#", ...) + + -- If the resolved value was a Promise, we chain onto it! + if Promise.is((...)) then + -- Without this warning, arguments sometimes mysteriously disappear + if argLength > 1 then + local message = ( + "When returning a Promise from andThen, extra arguments are " .. + "discarded! See:\n\n%s" + ):format( + self._source + ) + warn(message) + end + + (...):andThen(function(...) + self:_resolve(...) + end, function(...) + self:_reject(...) + end) + + return + end + + self._status = Promise.Status.Resolved + self._values = {...} + self._valuesLength = argLength + + -- We assume that these callbacks will not throw errors. + for _, callback in ipairs(self._queuedResolve) do + callback(...) + end +end + +function Promise:_reject(...) + if self._status ~= Promise.Status.Started then + return + end + + self._status = Promise.Status.Rejected + self._values = {...} + self._valuesLength = select("#", ...) + + -- If there are any rejection handlers, call those! + if not isEmpty(self._queuedReject) then + -- We assume that these callbacks will not throw errors. + for _, callback in ipairs(self._queuedReject) do + callback(...) + end + else + -- At this point, no one was able to observe the error. + -- An error handler might still be attached if the error occurred + -- synchronously. We'll wait one tick, and if there are still no + -- observers, then we should put a message in the console. + + self._unhandledRejection = true + local err = tostring((...)) + + spawn(function() + -- Someone observed the error, hooray! + if not self._unhandledRejection then + return + end + + -- Build a reasonable message + local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format( + err, + self._source + ) + warn(message) + end) + end +end + +return Promise \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ABVariationReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ABVariationReducer.lua new file mode 100644 index 0000000..ca09fe3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ABVariationReducer.lua @@ -0,0 +1,19 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) +local Cryo = require(LuaPackages.Cryo) + +local SetABVariation = require(Root.Actions.SetABVariation) + +local ABVariationReducer = Rodux.createReducer({}, { + [SetABVariation.name] = function(state, action) + assert(type(action.key) == "string", "Expected 'key' to be a string") + assert(type(action.variation) == "string", "Expected 'variation' to be a string") + return Cryo.Dictionary.join(state, { + [action.key] = action.variation + }) + end, +}) + +return ABVariationReducer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/AccountInfoReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/AccountInfoReducer.lua new file mode 100644 index 0000000..80a5f44 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/AccountInfoReducer.lua @@ -0,0 +1,19 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local AccountInfoReceived = require(Root.Actions.AccountInfoReceived) + +local ProductInfoReducer = Rodux.createReducer({}, { + [AccountInfoReceived.name] = function(state, action) + local accountInfo = action.accountInfo + + return { + balance = accountInfo.RobuxBalance, + membershipType = accountInfo.MembershipType, + } + end, +}) + +return ProductInfoReducer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/GamepadEnabledReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/GamepadEnabledReducer.lua new file mode 100644 index 0000000..115dc07 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/GamepadEnabledReducer.lua @@ -0,0 +1,17 @@ +local Root = script.Parent.Parent +local UserInputService = game:GetService("UserInputService") + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local SetGamepadEnabled = require(Root.Actions.SetGamepadEnabled) + +local gamepadDefault = UserInputService:GetPlatform() == Enum.Platform.XBoxOne + +local GamepadEnabledReducer = Rodux.createReducer(gamepadDefault, { + [SetGamepadEnabled.name] = function(state, action) + return action.enabled + end, +}) + +return GamepadEnabledReducer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/HasCompletedPurchaseReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/HasCompletedPurchaseReducer.lua new file mode 100644 index 0000000..6f82065 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/HasCompletedPurchaseReducer.lua @@ -0,0 +1,16 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local PurchaseCompleteRecieved = require(Root.Actions.PurchaseCompleteRecieved) +local CompleteRequest = require(Root.Actions.CompleteRequest) + +return Rodux.createReducer(false, { + [PurchaseCompleteRecieved.name] = function(state, action) + return true + end, + [CompleteRequest.name] = function(state, action) + return false + end, +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/NativeUpsellReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/NativeUpsellReducer.lua new file mode 100644 index 0000000..4fcb3b7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/NativeUpsellReducer.lua @@ -0,0 +1,18 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local PromptNativeUpsell = require(Root.Actions.PromptNativeUpsell) + +local NativeUpsellReducer = Rodux.createReducer({}, { + [PromptNativeUpsell.name] = function(state, action) + + return { + robuxProductId = action.robuxProductId, + robuxPurchaseAmount = action.robuxPurchaseAmount, + } + end, +}) + +return NativeUpsellReducer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PremiumProductsReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PremiumProductsReducer.lua new file mode 100644 index 0000000..063283a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PremiumProductsReducer.lua @@ -0,0 +1,18 @@ +local Root = script.Parent.Parent +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local CompleteRequest = require(Root.Actions.CompleteRequest) +local PremiumInfoRecieved = require(Root.Actions.PremiumInfoRecieved) + +local PremiumProductsReducer = Rodux.createReducer({}, { + [PremiumInfoRecieved.name] = function(state, action) + return action.premiumInfo + end, + [CompleteRequest.name] = function(state, action) + -- Clear product info when we hide the prompt + return {} + end, +}) + +return PremiumProductsReducer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ProductInfoReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ProductInfoReducer.lua new file mode 100644 index 0000000..63f1f43 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ProductInfoReducer.lua @@ -0,0 +1,60 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local CompleteRequest = require(Root.Actions.CompleteRequest) +local ProductInfoReceived = require(Root.Actions.ProductInfoReceived) +local BundleProductInfoReceived = require(Root.Actions.BundleProductInfoReceived) +local ItemType = require(Root.Enums.ItemType) +local getPreviewImageUrl = require(Root.getPreviewImageUrl) + +local USER_OUTFIT = "UserOutfit" + +local ProductInfoReducer = Rodux.createReducer({}, { + [ProductInfoReceived.name] = function(state, action) + local productInfo = action.productInfo + + return { + name = productInfo.Name, + price = productInfo.PriceInRobux or 0, + premiumPrice = productInfo.PremiumPriceInRobux, + imageUrl = getPreviewImageUrl(productInfo), + assetTypeId = productInfo.AssetTypeId, + productId = productInfo.ProductId, + membershipTypeRequired = productInfo.MinimumMembershipLevel, + itemType = productInfo.AssetTypeId + } + end, + + [BundleProductInfoReceived.name] = function(state, action) + local bundleProductInfo = action.bundleProductInfo + + -- For now we need the user outfit id to show the image of the bundle. + local costumeId + for _, item in ipairs(bundleProductInfo.items) do + if item.type == USER_OUTFIT then + costumeId = item.id + end + end + bundleProductInfo.costumeId = costumeId + bundleProductInfo.itemType = ItemType.Bundle + + return { + name = bundleProductInfo.name, + price = bundleProductInfo.product.priceInRobux or 0, + imageUrl = getPreviewImageUrl(bundleProductInfo), + assetTypeId = nil, + productId = bundleProductInfo.product.id, + membershipTypeRequired = nil, + itemType = bundleProductInfo.itemType + } + end, + + [CompleteRequest.name] = function(state, action) + -- Clear product info when we hide the prompt + return {} + end, +}) + +return ProductInfoReducer diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ProductReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ProductReducer.lua new file mode 100644 index 0000000..05b9eb9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ProductReducer.lua @@ -0,0 +1,22 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local SetProduct = require(Root.Actions.SetProduct) +local CompleteRequest = require(Root.Actions.CompleteRequest) + +local ProductReducer = Rodux.createReducer({}, { + [SetProduct.name] = function(state, action) + return { + id = action.id, + infoType = action.infoType, + equipIfPurchased = action.equipIfPurchased, + } + end, + [CompleteRequest.name] = function(state, action) + return {} + end, +}) + +return ProductReducer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PromptRequestReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PromptRequestReducer.lua new file mode 100644 index 0000000..33f2ea7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PromptRequestReducer.lua @@ -0,0 +1,69 @@ +local Root = script.Parent.Parent +local LuaPackages = Root.Parent + +local Rodux = require(LuaPackages.Rodux) + +local RequestAssetPurchase = require(Root.Actions.RequestAssetPurchase) +local RequestBundlePurchase = require(Root.Actions.RequestBundlePurchase) +local RequestGamepassPurchase = require(Root.Actions.RequestGamepassPurchase) +local RequestProductPurchase = require(Root.Actions.RequestProductPurchase) +local RequestPremiumPurchase = require(Root.Actions.RequestPremiumPurchase) +local RequestSubscriptionPurchase = require(Root.Actions.RequestSubscriptionPurchase) +local CompleteRequest = require(Root.Actions.CompleteRequest) +local RequestType = require(Root.Enums.RequestType) + +local EMPTY_STATE = { requestType = RequestType.None } + +local RequestReducer = Rodux.createReducer(EMPTY_STATE, { + [RequestAssetPurchase.name] = function(state, action) + return { + id = action.id, + infoType = Enum.InfoType.Asset, + requestType = RequestType.Asset, + equipIfPurchased = action.equipIfPurchased, + isRobloxPurchase = action.isRobloxPurchase, + } + end, + [RequestGamepassPurchase.name] = function(state, action) + return { + id = action.id, + infoType = Enum.InfoType.GamePass, + requestType = RequestType.GamePass, + isRobloxPurchase = false, + } + end, + [RequestProductPurchase.name] = function(state, action) + return { + id = action.id, + infoType = Enum.InfoType.Product, + requestType = RequestType.Product, + isRobloxPurchase = false, + } + end, + [RequestBundlePurchase.name] = function(state, action) + return { + id = action.id, + infoType = Enum.InfoType.Bundle, + requestType = RequestType.Bundle, + isRobloxPurchase = true, + } + end, + [RequestPremiumPurchase.name] = function(state, action) + return { + requestType = RequestType.Premium, + } + end, + [RequestSubscriptionPurchase.name] = function(state, action) + return { + id = action.id, + infoType = Enum.InfoType.Subscription, + requestType = RequestType.Subscription, + } + end, + [CompleteRequest.name] = function(state, action) + -- Clear product info when we hide the prompt + return EMPTY_STATE + end, +}) + +return RequestReducer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PromptStateReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PromptStateReducer.lua new file mode 100644 index 0000000..08d0ae5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PromptStateReducer.lua @@ -0,0 +1,31 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local SetPromptState = require(Root.Actions.SetPromptState) +local CompleteRequest = require(Root.Actions.CompleteRequest) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local StartPurchase = require(Root.Actions.StartPurchase) +local PromptNativeUpsell = require(Root.Actions.PromptNativeUpsell) +local PromptState = require(Root.Enums.PromptState) + +local PromptStateReducer = Rodux.createReducer(PromptState.None, { + [SetPromptState.name] = function(state, action) + return action.promptState + end, + [CompleteRequest.name] = function(state, action) + return PromptState.None + end, + [ErrorOccurred.name] = function(state, action) + return PromptState.Error + end, + [StartPurchase.name] = function(state, action) + return PromptState.PurchaseInProgress + end, + [PromptNativeUpsell.name] = function(state, action) + return PromptState.RobuxUpsell + end, +}) + +return PromptStateReducer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PurchaseErrorReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PurchaseErrorReducer.lua new file mode 100644 index 0000000..4bd1899 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PurchaseErrorReducer.lua @@ -0,0 +1,16 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local CompleteRequest = require(Root.Actions.CompleteRequest) + +return Rodux.createReducer({}, { + [ErrorOccurred.name] = function(state, action) + return action.purchaseError + end, + [CompleteRequest.name] = function(state, action) + return {} + end, +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PurchasingStartTimeReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PurchasingStartTimeReducer.lua new file mode 100644 index 0000000..6e9cfc3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PurchasingStartTimeReducer.lua @@ -0,0 +1,15 @@ +local Root = script.Parent.Parent + +local StartPurchase = require(Root.Actions.StartPurchase) + +local function PurchasingStartTimeReducer(state, action) + state = state or -1 + + if action.type == StartPurchase.name then + return action.purchasingStartTime + end + + return state +end + +return PurchasingStartTimeReducer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/Reducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/Reducer.lua new file mode 100644 index 0000000..dfe8487 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/Reducer.lua @@ -0,0 +1,37 @@ +--[[ + The main reducer for the app's store +]] +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local PromptRequestReducer = require(script.Parent.PromptRequestReducer) +local ProductInfoReducer = require(script.Parent.ProductInfoReducer) +local PremiumProductsReducer = require(script.Parent.PremiumProductsReducer) +local NativeUpsellReducer = require(script.Parent.NativeUpsellReducer) +local PromptStateReducer = require(script.Parent.PromptStateReducer) +local PurchaseErrorReducer = require(script.Parent.PurchaseErrorReducer) +local AccountInfoReducer = require(script.Parent.AccountInfoReducer) +local PurchasingStartTimeReducer = require(script.Parent.PurchasingStartTimeReducer) +local HasCompletedPurchaseReducer = require(script.Parent.HasCompletedPurchaseReducer) +local GamepadEnabledReducer = require(script.Parent.GamepadEnabledReducer) +local ABVariationReducer = require(script.Parent.ABVariationReducer) +local WindowStateReducer = require(script.Parent.WindowStateReducer) + +local Reducer = Rodux.combineReducers({ + promptRequest = PromptRequestReducer, + productInfo = ProductInfoReducer, + premiumProductInfo = PremiumProductsReducer, + nativeUpsell = NativeUpsellReducer, + promptState = PromptStateReducer, + purchaseError = PurchaseErrorReducer, + accountInfo = AccountInfoReducer, + purchasingStartTime = PurchasingStartTimeReducer, + hasCompletedPurchase = HasCompletedPurchaseReducer, + gamepadEnabled = GamepadEnabledReducer, + abVariations = ABVariationReducer, + windowState = WindowStateReducer, +}) + +return Reducer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/WindowStateReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/WindowStateReducer.lua new file mode 100644 index 0000000..197cf8b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/WindowStateReducer.lua @@ -0,0 +1,38 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local SetPromptState = require(Root.Actions.SetPromptState) +local CompleteRequest = require(Root.Actions.CompleteRequest) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local StartPurchase = require(Root.Actions.StartPurchase) +local PromptNativeUpsell = require(Root.Actions.PromptNativeUpsell) +local SetWindowState = require(Root.Actions.SetWindowState) +local WindowState = require(Root.Enums.WindowState) +local PromptState = require(Root.Enums.PromptState) + +return Rodux.createReducer(WindowState.Hidden, { + [SetPromptState.name] = function(state, action) + if action.promptState == PromptState.None then + return WindowState.Hidden + else + return WindowState.Shown + end + end, + [SetWindowState.name] = function(state, action) + return action.state + end, + [ErrorOccurred.name] = function(state, action) + return WindowState.Shown + end, + [StartPurchase.name] = function(state, action) + return WindowState.Shown + end, + [PromptNativeUpsell.name] = function(state, action) + return WindowState.Shown + end, + [CompleteRequest.name] = function(state, action) + return WindowState.Hidden + end, +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/Analytics.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/Analytics.lua new file mode 100644 index 0000000..100e6b3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/Analytics.lua @@ -0,0 +1,49 @@ +local Players = game:GetService("Players") +local AnalyticsService = game:GetService("RbxAnalyticsService") +local MarketplaceService = game:GetService("MarketplaceService") + +local Analytics = {} + +function Analytics.new() + local service = {} + + setmetatable(service, { + __tostring = function() + return "Service(Analytics)" + end + }) + + function service.reportRobuxUpsellStarted() + return MarketplaceService:ReportRobuxUpsellStarted() + end + + function service.reportNativeUpsellStarted(productId) + AnalyticsService:SendEventImmediately("mobile", "robuxSelected", "mobileUpsell", { + productId = productId, + }) + end + + function service.signalPurchaseSuccess(id, infoType, salePrice, result) + if infoType == Enum.InfoType.Product then + MarketplaceService:SignalClientPurchaseSuccess(result.receipt, Players.LocalPlayer.UserId, id) + else + MarketplaceService:ReportAssetSale(id, salePrice) + end + end + + function service.signalPremiumUpsellShownPremium() + AnalyticsService:SetRBXEvent("client", "InGamePrompt", "PremiumUpsellShownPremium", { gameID = game.GameId }) + end + + function service.signalPremiumUpsellShownNonPremium() + AnalyticsService:SetRBXEvent("client", "InGamePrompt", "PremiumUpsellShownNonPremium", { gameID = game.GameId }) + end + + function service.signalAdultLegalTextShown() + AnalyticsService:SetRBXEvent("client", "InGamePrompt", "AdultLegalTextShown", { gameID = game.GameId }) + end + + return service +end + +return Analytics diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/ExternalSettings.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/ExternalSettings.lua new file mode 100644 index 0000000..6e883c2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/ExternalSettings.lua @@ -0,0 +1,60 @@ +local Root = script.Parent.Parent +local RunService = game:GetService("RunService") +local GuiService = game:GetService("GuiService") +local UserInputService = game:GetService("UserInputService") +local GetFFlagLuaUseThirdPartyPermissions = require(Root.Flags.GetFFlagLuaUseThirdPartyPermissions) +local GetFFlagHideThirdPartyPurchaseFailure = require(Root.Flags.GetFFlagHideThirdPartyPurchaseFailure) + +local ExternalSettings = {} + +function ExternalSettings.new() + local service = {} + + setmetatable(service, { + __tostring = function() + return "Service(ExternalSettings)" + end, + }) + + function service.getPlatform() + return UserInputService:GetPlatform() + end + + function service.isStudio() + return RunService:IsStudio() + end + + function service.isThirdPartyPurchaseAllowed() + -- If PermissionsService is not created (flag is not enabled), don't fail. + local result = true + pcall(function() + result = game:GetService("PermissionsService"):GetIsThirdPartyPurchaseAllowed() + end) + return result + end + + function service.getLuaUseThirdPartyPermissions() + return GetFFlagLuaUseThirdPartyPermissions() + end + + function service.getFlagHideThirdPartyPurchaseFailure() + return GetFFlagHideThirdPartyPurchaseFailure() + end + + -- TODO(DEVTOOLS-4227): Remove this flag + function service.getFlagRestrictSales2() + return settings():GetFFlag("RestrictSales2") + end + + function service.getFlagOrder66() + return settings():GetFFlag("Order66") + end + + function service.isTenFootInterface() + return GuiService:IsTenFootInterface() + end + + return service +end + +return ExternalSettings diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/LayoutValues.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/LayoutValues.lua new file mode 100644 index 0000000..ba3da14 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/LayoutValues.lua @@ -0,0 +1,186 @@ +local Root = script.Parent.Parent + +local createSignal = require(Root.Misc.createSignal) +local strict = require(Root.strict) + +local FFlagChinaLicensingApp = settings():GetFFlag("ChinaLicensingApp") + +local function makeImageData(path, sliceCenter) + return { + Path = "rbxasset://textures/" .. path, + SliceCenter = sliceCenter, + } +end + +local LayoutValues = {} +LayoutValues.__tostring = function() + return "Service(LayoutValues)" +end + +function LayoutValues.new(isTenFoot) + local self = {} + setmetatable(self, { + __index = LayoutValues, + }) + + self.signal = createSignal() + self.layout = self:generate(isTenFoot) + + return self +end + +function LayoutValues:update(isTenFoot) + self.layout = self:generate(isTenFoot) + self.signal:fire(self.layout) +end + +function LayoutValues:generate(isTenFoot) + local scaleFactor = isTenFoot and 3 or 1 + + local ButtonHeight = 44 * scaleFactor + local PostTextHeight = 30 * scaleFactor + + local RobuxIconPadding = 6 * scaleFactor + + local RobuxIconWidth = 16 * scaleFactor + local RobuxIconHeight = RobuxIconWidth + + local ProductDescriptionPaddingTop = 18 * scaleFactor + local ProductDescriptionWidth = 210 * scaleFactor + local ProductDescriptionHeight = 106 * scaleFactor + + local PurchasingAnimationWidth = 96 * scaleFactor + local PurchasingAnimationHeight = 20 * scaleFactor + + local HorizontalPadding = 25 * scaleFactor + local ItemPreviewBorder = 2 * scaleFactor + local ItemPreviewWidth = 64 * scaleFactor + local ItemPreviewHeight = 64 * scaleFactor + + local ButtonIconPadding = 3 * scaleFactor + -- Button icons have drop shadow and need a slight offset in order to look centered + local ButtonIconYOffset = 2 * scaleFactor + + local ItemPreviewBackgroundWidth = ItemPreviewWidth + 2 * ItemPreviewBorder + local ItemPreviewBackgroundHeight = ItemPreviewHeight + 2 * ItemPreviewBorder + + local ItemPreviewContainerWidth = ItemPreviewBackgroundWidth + 2 * HorizontalPadding + local ItemPreviewContainerHeight = ItemPreviewBackgroundHeight + 2 * HorizontalPadding + + --[[ + Sizes for UI elements + ]] + local Size = { + AdditionalDetailsLabel = UDim2.new(1, 0, 0, PostTextHeight), + + ItemPreview = UDim2.new(0, ItemPreviewWidth, 0, ItemPreviewHeight), + ItemPreviewWhiteFrame = UDim2.new(0, ItemPreviewBackgroundWidth, 0, ItemPreviewBackgroundHeight), + ItemPreviewContainerFrame = UDim2.new(0, ItemPreviewContainerWidth, 0, ItemPreviewContainerHeight), + + HorizontalPadding = HorizontalPadding, + ProductDescription = UDim2.new(0, ProductDescriptionWidth, 0, ProductDescriptionHeight), + ProductDescriptionPaddingTop = ProductDescriptionPaddingTop, + + RobuxIconContainerFrame = UDim2.new(0, RobuxIconWidth + RobuxIconPadding, 0, RobuxIconHeight + 2 * RobuxIconPadding), + RobuxIcon = UDim2.new(0, RobuxIconWidth, 0, RobuxIconHeight), + PriceTextLabel = UDim2.new( + 0, ProductDescriptionWidth - (RobuxIconWidth + RobuxIconPadding), + 0, RobuxIconHeight + ), + + PurchasingAnimation = UDim2.new(0, PurchasingAnimationWidth, 0, PurchasingAnimationHeight), + + ButtonIconPadding = ButtonIconPadding, + ButtonIconYOffset = ButtonIconYOffset, + ButtonHeight = ButtonHeight, + Dialog = UDim2.new( + 0, ItemPreviewContainerWidth + ProductDescriptionWidth, + 0, math.max(ItemPreviewContainerHeight, ProductDescriptionHeight) + PostTextHeight + ButtonHeight + ), + } + + --[[ + Font sizes for UI elements + ]] + local TextSize = { + Default = 18 * scaleFactor, + ProductDescription = 18 * scaleFactor, + Button = 24 * scaleFactor, + AdditionalDetails = 14 * scaleFactor, + Purchasing = 36 * scaleFactor, + } + + --[[ + Special text colors, as needed + ]] + local TextColor = { + PriceLabel = Color3.new(1, 1, 1) + } + + --[[ + Background images, including slice center + ]] + local Image = {} + Image.PromptBackground = isTenFoot + and makeImageData("ui/PurchasePrompt/PurchasePromptBG@2x.png", Rect.new(17, 17, 19, 19)) + or makeImageData("ui/PurchasePrompt/PurchasePromptBG.png", Rect.new(8, 9, 10, 10)) + Image.InProgressBackground = isTenFoot + and makeImageData("ui/PurchasePrompt/LoadingBG@2x.png", Rect.new(17, 17, 19, 19)) + or makeImageData("ui/PurchasePrompt/LoadingBG.png", Rect.new(9, 9, 11, 11)) + + Image.ButtonUpLeft = isTenFoot + and makeImageData("ui/PurchasePrompt/LeftButton@2x.png", Rect.new(18, 5, 20, 7)) + or makeImageData("ui/PurchasePrompt/LeftButton.png", Rect.new(8, 3, 10, 4)) + Image.ButtonDownLeft = isTenFoot + and makeImageData("ui/PurchasePrompt/LeftButtonDown@2x.png", Rect.new(18, 5, 20, 7)) + or makeImageData("ui/PurchasePrompt/LeftButtonDown.png", Rect.new(8, 3, 10, 4)) + Image.ButtonUpRight = isTenFoot + and makeImageData("ui/PurchasePrompt/RightButton@2x.png", Rect.new(3, 5, 5, 7)) + or makeImageData("ui/PurchasePrompt/RightButton.png", Rect.new(2, 3, 3, 4)) + Image.ButtonDownRight = isTenFoot + and makeImageData("ui/PurchasePrompt/RightButtonDown@2x.png", Rect.new(3, 5, 5, 7)) + or makeImageData("ui/PurchasePrompt/RightButtonDown.png", Rect.new(2, 3, 3, 4)) + Image.ButtonUp = isTenFoot + and makeImageData("ui/PurchasePrompt/SingleButton@2x.png", Rect.new(18, 5, 20, 7)) + or makeImageData("ui/PurchasePrompt/SingleButton.png", Rect.new(8, 3, 10, 4)) + Image.ButtonDown = isTenFoot + and makeImageData("ui/PurchasePrompt/SingleButtonDown@2x.png", Rect.new(18, 5, 20, 7)) + or makeImageData("ui/PurchasePrompt/SingleButtonDown.png", Rect.new(8, 3, 10, 4)) + + Image.PremiumIcon = isTenFoot + and makeImageData("ui/PurchasePrompt/premium@2x.png") + or makeImageData("ui/PurchasePrompt/premium.png") + + if FFlagChinaLicensingApp then + Image.RobuxIcon = isTenFoot + and makeImageData("ui/clb_robux_20@3x.png") + or makeImageData("ui/clb_robux_20.png") + else + -- Set a reference to both so they can be preloaded and displayed depending on AB test + Image.RobuxIcon = isTenFoot + and makeImageData("ui/common/robux_small@2x.png") + or makeImageData("ui/common/robux_small.png") + end + + Image.ErrorIcon = isTenFoot + and makeImageData("ui/ErrorIcon.png") + or makeImageData("ui/ErrorIcon.png") + + Image.ButtonA = isTenFoot + and makeImageData("ui/Settings/Help/AButtonDark@2x.png") + or makeImageData("ui/Settings/Help/AButtonDark.png") + Image.ButtonB = isTenFoot + and makeImageData("ui/Settings/Help/BButtonDark@2x.png") + or makeImageData("ui/Settings/Help/BButtonDark.png") + + local LayoutValues = strict({ + Size = strict(Size, "LayoutValues.Size"), + TextSize = strict(TextSize, "LayoutValues.TextSize"), + TextColor = strict(TextColor, "LayoutValues.TextColor"), + Image = strict(Image, "LayoutValues.Image"), + }, "LayoutValues") + + return LayoutValues +end + +return LayoutValues \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/Network.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/Network.lua new file mode 100644 index 0000000..b70dff5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/Network.lua @@ -0,0 +1,240 @@ +local Root = script.Parent.Parent +local HttpService = game:GetService("HttpService") +local ContentProvider = game:GetService("ContentProvider") +local MarketplaceService = game:GetService("MarketplaceService") +local InsertService = game:GetService("InsertService") +local UserInputService = game:GetService("UserInputService") +local Players = game:GetService("Players") + +local Promise = require(Root.Promise) +local PremiumProduct = require(Root.Models.PremiumProduct) + +-- This is the approximate strategy for URL building that we use elsewhere +local BASE_URL = string.gsub(ContentProvider.BaseUrl:lower(), "/m.", "/www.") +BASE_URL = string.gsub(BASE_URL, "http:", "https:") + +local API_URL = string.gsub(BASE_URL, "https://www", "https://api") +local AB_TEST_URL = string.gsub(BASE_URL, "https://www", "https://abtesting") +local BASE_CATALOG_URL = string.gsub(BASE_URL, "https://www.", "https://catalog.") +local BASE_ECONOMY_URL = string.gsub(BASE_URL, "https://www.", "https://economy.") +local PREMIUM_FEATURES_URL = string.gsub(BASE_URL, "https://www.", "https://premiumfeatures.") +local ECONOMY_CREATOR_STATS_URL = string.gsub(BASE_URL, "https://www.", "https://economycreatorstats.") + +local function request(options, resolve, reject) + return HttpService:RequestInternal(options):Start(function(success, response) + if success then + local result + success, result = pcall(HttpService.JSONDecode, HttpService, response.Body) + + if success then + resolve(result) + else + reject("Could not parse JSON.") + end + else + reject(tostring(response.StatusMessage)) + end + end) +end + +local AB_SUBJECT_TYPE_USER_ID = 1 + +local function getABTestGroup(userId, testName) + local abTestRequest = { + { + ExperimentName = testName, + SubjectType = AB_SUBJECT_TYPE_USER_ID, + SubjectTargetId = userId, + } + } + + return Promise.new(function(resolve, reject) + return request({ + Url = AB_TEST_URL .. "v1/get-enrollments", + Method = "POST", + Body = HttpService:JSONEncode(abTestRequest), + Headers = { + ["Content-Type"] = "application/json", + ["Accept"] = "application/json", + } + }, resolve, reject) + end) +end + +local function getProductInfo(id, infoType) + return MarketplaceService:GetProductInfo(id, infoType) +end + +local function getPremiumProductInfo() + return Promise.new(function(resolve, reject) + request({ + Url = PREMIUM_FEATURES_URL .. "v1/products?typeName=Subscription", + Method = "GET", + }, function(response) + -- Gets cheapest premium package + local subscription + for _, product in ipairs(response.products) do + local iapProduct = PremiumProduct(product) + if iapProduct ~= nil then + if subscription == nil or subscription.robuxAmount > iapProduct.robuxAmount then + subscription = iapProduct + end + end + end + + -- Remove after backend fixes their end... + local platform = UserInputService:GetPlatform() + if platform == Enum.Platform.Android and subscription ~= nil and subscription.mobileProductId ~= nil then + subscription.mobileProductId = subscription.mobileProductId:lower() + end + + resolve(subscription) + end, reject) + end) +end + +local function getPlayerOwns(player, id, infoType) + if infoType == Enum.InfoType.Asset then + return MarketplaceService:PlayerOwnsAsset(player, id) + elseif infoType == Enum.InfoType.GamePass then + return MarketplaceService:UserOwnsGamePassAsync(player.UserId, id) + elseif infoType == Enum.InfoType.Subscription then + return MarketplaceService:IsPlayerSubscribed(player, id) + end + + return false +end + +local function performPurchase(infoType, productId, expectedPrice, requestId, isRobloxPurchase) + local success, result = pcall(function() + return MarketplaceService:PerformPurchase(infoType, productId, expectedPrice, requestId, isRobloxPurchase) + end) + + if success then + return result + end + + error(tostring(result)) +end + +local function loadAssetForEquip(assetId) + return InsertService:LoadAsset(assetId) +end + +local function getAccountInfo() + return Promise.new(function(resolve, reject) + request({ + Url = API_URL .. "users/account-info", + Method = "GET", + }, resolve, reject) + end) +end + +local function getXboxRobuxBalance() + return Promise.new(function(resolve, reject) + request({ + Url = API_URL .. "my/platform-currency-budget", + Method = "GET", + }, resolve, reject) + end) +end + +local function getBundleDetails(bundleId) + local url = BASE_CATALOG_URL .."v1/bundles/" ..tostring(bundleId) .."/details" + local options = { + Url = url, + Method = "GET", + } + + return Promise.new(function(resolve, reject) + spawn(function() + request(options, resolve, reject) + end) + end) +end + +local function getProductPurchasableDetails(productId) + local url = BASE_ECONOMY_URL .."v1/products/" ..tostring(productId) .."?showPurchasable=true" + local options = { + Url = url, + Method = "GET" + } + + return Promise.new(function(resolve, reject) + spawn(function() + request(options, resolve, reject) + end) + end) +end + +local function postPremiumImpression() + local url = ECONOMY_CREATOR_STATS_URL.."v1/universes/" ..tostring(game.GameId) .."/premium-impressions/increment" + local options = { + Url = url, + Method = "POST", + Body = HttpService:JSONEncode("{}"), + Headers = { + ["Content-Type"] = "application/json", + ["Accept"] = "application/json", + } + } + + return Promise.new(function(resolve, reject) + spawn(function() + return HttpService:RequestInternal(options):Start(function(success, response) + -- Ignore all responses, don't need to do anything + end) + end) + end) +end + +local function getPremiumUpsellPrecheck() + local options = { + Url = string.format("%sv1/users/%d/premium-upsell-precheck?universeId=%d&placeId=%d", + PREMIUM_FEATURES_URL, Players.LocalPlayer.UserId, game.GameId, game.PlaceId), + Method = "GET", + } + + return Promise.new(function(resolve, reject) + spawn(function() + return HttpService:RequestInternal(options):Start(function(success, response) + if success and response.StatusCode == 200 then + resolve() + else + reject() + end + end) + end) + end) +end + +local Network = {} + +-- TODO: "Promisify" is not strictly necessary with the new `request` structure, +-- refactor this to clean up the overzealous promise-wrapping +function Network.new() + local networkService = { + getABTestGroup = Promise.promisify(getABTestGroup), + getProductInfo = Promise.promisify(getProductInfo), + getPlayerOwns = Promise.promisify(getPlayerOwns), + performPurchase = Promise.promisify(performPurchase), + loadAssetForEquip = Promise.promisify(loadAssetForEquip), + getAccountInfo = Promise.promisify(getAccountInfo), + getXboxRobuxBalance = Promise.promisify(getXboxRobuxBalance), + getBundleDetails = getBundleDetails, + getProductPurchasableDetails = getProductPurchasableDetails, + getPremiumProductInfo = Promise.promisify(getPremiumProductInfo), + postPremiumImpression = Promise.promisify(postPremiumImpression), + getPremiumUpsellPrecheck = Promise.promisify(getPremiumUpsellPrecheck), + } + + setmetatable(networkService, { + __tostring = function() + return "Service(Network)" + end + }) + + return networkService +end + +return Network \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/PlatformInterface.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/PlatformInterface.lua new file mode 100644 index 0000000..04e9b10 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/PlatformInterface.lua @@ -0,0 +1,57 @@ +local Root = script.Parent.Parent +local ContentProvider = game:GetService("ContentProvider") +local GuiService = game:GetService("GuiService") +local MarketplaceService = game:GetService("MarketplaceService") +local PlatformService = nil +pcall(function() + PlatformService = game:GetService("PlatformService") +end) + +local GetFFlagUpsellDirectToPackage = require(Root.Flags.GetFFlagUpsellDirectToPackage) + +local BASE_URL = string.gsub(ContentProvider.BaseUrl:lower(), "/m.", "/www.") + +local PlatformInterface = {} + +function PlatformInterface.new() + local service = {} + + setmetatable(service, { + __tostring = function() + return "Service(PlatformInterface)" + end, + }) + + function service.signalMockPurchasePremium() + MarketplaceService:SignalMockPurchasePremium() + end + + function service.startPremiumUpsell(productId) + local url = nil + if GetFFlagUpsellDirectToPackage() then + url = ("%supgrades/paymentmethods?ap=%d"):format(BASE_URL, productId) + else + url = ("%spremium/membership"):format(BASE_URL) + end + + GuiService:OpenBrowserWindow(url) + end + + function service.startRobuxUpsellWeb() + local url = ("%sUpgrades/Robux.aspx"):format(BASE_URL) + + GuiService:OpenBrowserWindow(url) + end + + function service.promptNativePurchase(player, mobileProductId) + return MarketplaceService:PromptNativePurchase(player, mobileProductId) + end + + function service.beginPlatformStorePurchase(xboxProductId) + return PlatformService:BeginPlatformStorePurchase(xboxProductId) + end + + return service +end + +return PlatformInterface \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/LayoutValuesKey.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/LayoutValuesKey.lua new file mode 100644 index 0000000..c965e19 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/LayoutValuesKey.lua @@ -0,0 +1,5 @@ +local Symbol = require(script.Parent.Symbol) + +local LayoutValuesKey = Symbol.named("LayoutValuesKey") + +return LayoutValuesKey \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/LocalizationContextKey.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/LocalizationContextKey.lua new file mode 100644 index 0000000..700afc2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/LocalizationContextKey.lua @@ -0,0 +1,5 @@ +local Symbol = require(script.Parent.Symbol) + +local LocalizationContextKey = Symbol.named("LocalizationContextKey") + +return LocalizationContextKey \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/Symbol.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/Symbol.lua new file mode 100644 index 0000000..305d66a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/Symbol.lua @@ -0,0 +1,30 @@ +--[[ + A 'Symbol' is an opaque marker type. + + Symbols have the type 'userdata', but when printed to the console, the name + of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockAnalytics.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockAnalytics.lua new file mode 100644 index 0000000..e62a81d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockAnalytics.lua @@ -0,0 +1,44 @@ +--[[ + Mocks our analytics interface so we can make sure certain thunks + trigger analytics calls without actually calling the real ones. +]] +local createSpy = require(script.Parent.createSpy) + +local MockAnalytics = {} + +function MockAnalytics.new() + local reportRobuxUpsellStarted = createSpy() + local signalPurchaseSuccess = createSpy() + local signalPremiumUpsellShownPremium = createSpy() + local signalPremiumUpsellShownNonPremium = createSpy() + local signalAdultLegalTextShown = createSpy() + + local mockService = { + reportRobuxUpsellStarted = reportRobuxUpsellStarted.value, + signalPurchaseSuccess = signalPurchaseSuccess.value, + signalPremiumUpsellShownPremium = signalPremiumUpsellShownPremium.value, + signalPremiumUpsellShownNonPremium = signalPremiumUpsellShownNonPremium.value, + signalAdultLegalTextShown = signalAdultLegalTextShown.value, + } + + local spies = { + reportRobuxUpsellStarted = reportRobuxUpsellStarted, + signalPurchaseSuccess = signalPurchaseSuccess, + signalPremiumUpsellShownPremium = signalPremiumUpsellShownPremium, + signalPremiumUpsellShownNonPremium = signalPremiumUpsellShownNonPremium, + signalAdultLegalTextShown = signalAdultLegalTextShown, + } + + setmetatable(mockService, { + __tostring = function() + return "Service(MockAnalytics)" + end, + }) + + return { + spies = spies, + mockService = mockService, + } +end + +return MockAnalytics diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockExternalSettings.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockExternalSettings.lua new file mode 100644 index 0000000..9eac893 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockExternalSettings.lua @@ -0,0 +1,75 @@ +--[[ + Mocks some external settings so we can test the purchase prompt's + behavior under various external circumstances. +]] +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Cryo = require(LuaPackages.Cryo) +local GetFFlagLuaUseThirdPartyPermissions = require(Root.Flags.GetFFlagLuaUseThirdPartyPermissions) + +local DEFAULT_FLAG_STATES = { + -- Allow restriction of third-party sales. Was never properly turned on in + -- the old prompt. We should change this if it defaults to on. + RestrictSales2 = false, + -- Disables all in-game purchasing. A kill-switch for emergency purposes + Order66 = false, +} + +local MockExternalSettings = {} + +function MockExternalSettings.new(isStudio, isTenFoot, flags, platform) + local service = {} + + flags = Cryo.Dictionary.join(DEFAULT_FLAG_STATES, flags) + + --[[ + getMockFlag allows you to test both flag states for tests unrelated to your flag. Usage: + function service.getFFlagTestFlag() + return getMockFlag(flags.TestFlag, GetFFlagTestFlag()) + end + ]] + local function getMockFlag(mockFlag, systemFlag) + if mockFlag ~= nil then + return mockFlag + end + return systemFlag + end + + function service.getPlatform() + return platform + end + + function service.isStudio() + return isStudio + end + + function service.isThirdPartyPurchaseAllowed() + return flags.PermissionsServiceIsThirdPartyPurchaseAllowed + end + + function service.getLuaUseThirdPartyPermissions() + return getMockFlag(flags.LuaUseThirdPartyPermissions, GetFFlagLuaUseThirdPartyPermissions()) + end + + function service.getFlagHideThirdPartyPurchaseFailure() + return flags.HideThirdPartyPurchaseFailure + end + + -- TODO(DEVTOOLS-4227): Remove this flag + function service.getFlagRestrictSales2() + return flags.RestrictSales2 + end + + function service.getFlagOrder66() + return flags.Order66 + end + + function service.isTenFootInterface() + return isTenFoot + end + + return service +end + +return MockExternalSettings diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockNetwork.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockNetwork.lua new file mode 100644 index 0000000..4dca864 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockNetwork.lua @@ -0,0 +1,147 @@ +--[[ + Mock network implementation that returns values in the expected + formats, or returns promise rejections if specified +]] +local Promise = require(script.Parent.Parent.Promise) + +local function getABTestGroup() + return Promise.resolve(false) +end + +local function getProductInfo(id, infoType) + return Promise.resolve({ + AssetId = 1, + AssetTypeId = 2, + ContentRatingTypeId = 0, + Creator = { + CreatorType = "Group", + CreatorTargetId = 1, + Name = "ROBLOX", + Id = 1, + }, + Description = "This item isn't real!", + IconImageAssetId = 1, + IsForSale = true, + IsLimited = false, + IsLimitedUnique = false, + IsNew = false, + IsPublicDomain = false, + MinimumMembershipLevel = 0, + Name = "Test Item", + PriceInRobux = 100, + ProductId = 1, + }) +end + +local function getPlayerOwns(player, id, infoType) + return Promise.resolve(false) +end + +local function performPurchase(infoType, productId, expectedPrice, requestId) + return Promise.resolve({ + success = true, + purchased = true, + receipt = "fake-receipt-hash", + }) +end + +local function loadAssetForEquip(assetId) + return Promise.resolve(Instance.new("Tool")) +end + +local function getAccountInfo() + return Promise.resolve({ + RobuxBalance = 2147483647, + MembershipType = 0, + }) +end + +local function getBundleDetails(bundleId) + return Promise.resolve({ + id = 1, + name = "mock-name", + description = "mock-description", + items = { + [1] = { + id = 1, + name = "outfit-name", + type = "UserOutfit", + }, + }, + creator = { + id = 1, + name = "ROBLOX", + type = "User", + }, + product = { + id = 1, + isForSale = true, + priceInRobux = 100, + } + }) +end + +local function getProductPurchasableDetails(productId) + return Promise.resolve({ + purchasable = false, + reason = "mock-reason", + price = 100, + }) +end + +local function postPremiumImpression() + return Promise.resolve() +end + +local function getPremiumUpsellPrecheck() + return Promise.resolve(true) +end + +local function networkFailure(id, infoType) + return Promise.reject("Failed to access network service") +end + +local MockNetwork = {} +MockNetwork.__index = MockNetwork + +function MockNetwork.new(shouldFail) + local mockNetworkService + + if shouldFail then + mockNetworkService = { + getABTestGroup = networkFailure, + getProductInfo = networkFailure, + getPlayerOwns = networkFailure, + performPurchase = networkFailure, + loadAssetForEquip = networkFailure, + getAccountInfo = networkFailure, + getBundleDetails = networkFailure, + getProductPurchasableDetails = networkFailure, + postPremiumImpression = networkFailure, + getPremiumUpsellPrecheck = networkFailure, + } + else + mockNetworkService = { + getABTestGroup = getABTestGroup, + getProductInfo = getProductInfo, + getPlayerOwns = getPlayerOwns, + performPurchase = performPurchase, + loadAssetForEquip = loadAssetForEquip, + getAccountInfo = getAccountInfo, + getBundleDetails = getBundleDetails, + getProductPurchasableDetails = getProductPurchasableDetails, + postPremiumImpression = postPremiumImpression, + getPremiumUpsellPrecheck = getPremiumUpsellPrecheck, + } + end + + setmetatable(mockNetworkService, { + __tostring = function() + return "MockService(Network)" + end + }) + + return mockNetworkService +end + +return MockNetwork \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockPlatformInterface.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockPlatformInterface.lua new file mode 100644 index 0000000..191357a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockPlatformInterface.lua @@ -0,0 +1,41 @@ +--[[ + Mocks calls to certain platform-specific functions so that we can + ensure they're being called properly by our thunks. +]] +local createSpy = require(script.Parent.createSpy) + +local MockPlatformInterface = {} + +function MockPlatformInterface.new() + local startRobuxUpsellWeb = createSpy() + local promptNativePurchase = createSpy() + local startPremiumUpsell = createSpy() + local signalMockPurchasePremium = createSpy() + + local mockService = { + startPremiumUpsell = startPremiumUpsell.value, + signalMockPurchasePremium = signalMockPurchasePremium.value, + startRobuxUpsellWeb = startRobuxUpsellWeb.value, + promptNativePurchase = promptNativePurchase.value, + } + + local spies = { + startPremiumUpsell = startPremiumUpsell, + signalMockPurchasePremium = signalMockPurchasePremium, + startRobuxUpsellWeb = startRobuxUpsellWeb, + promptNativePurchase = promptNativePurchase, + } + + setmetatable(mockService, { + __tostring = function() + return "Service(MockPlatformInterface)" + end, + }) + + return { + spies = spies, + mockService = mockService, + } +end + +return MockPlatformInterface \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/UnitTestContainer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/UnitTestContainer.lua new file mode 100644 index 0000000..89c67a3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/UnitTestContainer.lua @@ -0,0 +1,66 @@ +--[[ + Component that wraps its provided children with a store provider, + a LayoutValues object, and a ScreenGui. Convenient for testing! +]] +local Root = script.Parent.Parent +local LocalizationService = game:GetService("LocalizationService") +local CorePackages = game:GetService("CorePackages") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) +local Rodux = require(LuaPackages.Rodux) +local RoactRodux = require(LuaPackages.RoactRodux) +local UIBlox = require(LuaPackages.UIBlox) +local StyleProvider = UIBlox.Style.Provider + +local LayoutValuesProvider = require(Root.Components.Connection.LayoutValuesProvider) +local LocalizationContextProvider = require(Root.Components.Connection.LocalizationContextProvider) +local getLocalizationContext = require(Root.Localization.getLocalizationContext) +local Reducer = require(Root.Reducers.Reducer) +local LayoutValues = require(Root.Services.LayoutValues) + +local DarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme) +local Gotham = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham) + +local UnitTestContainer = Roact.Component:extend("UnitTestContainer") + +function UnitTestContainer:init() + self.layoutValues = LayoutValues.new(false, false).layout + self.store = self.props.overrideStore or Rodux.Store.new(Reducer, {}) + + local locale = self.props.overrideLocale or LocalizationService.RobloxLocaleId + self.localizationContext = getLocalizationContext(locale) +end + +function UnitTestContainer:render() + assert(self.props[Roact.Children] ~= nil and #self.props[Roact.Children] > 0, + "UnitTestContainer: no children provided, nothing will be tested") + + return Roact.createElement(RoactRodux.StoreProvider, { + store = self.store, + }, { + PurchasePrompt = Roact.createElement(StyleProvider, { + style = { + Theme = DarkTheme, + Font = Gotham, + }, + }, { + Roact.createElement(LocalizationContextProvider, { + localizationContext = self.localizationContext, + render = function() + return Roact.createElement(LayoutValuesProvider, { + isTenFootInterface = false, + render = function() + return Roact.createElement("ScreenGui", { + AutoLocalize = false, + IgnoreGuiInset = true, + }, self.props[Roact.Children]) + end, + }) + end, + }) + }) + }) +end + +return UnitTestContainer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/createSpy.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/createSpy.lua new file mode 100644 index 0000000..e1ab9fc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/createSpy.lua @@ -0,0 +1,66 @@ +--[[ + A utility used to create a function spy that can be used to robustly test + that functions are invoked the correct number of times and with the correct + number of arguments. + + This should only be used in tests. +]] + +local function createSpy(inner) + local self = { + callCount = 0, + values = {}, + valuesLength = 0, + } + + self.value = function(...) + self.callCount = self.callCount + 1 + self.values = {...} + self.valuesLength = select("#", ...) + + if inner ~= nil then + return inner(...) + end + end + + self.assertCalledWith = function(_, ...) + local len = select("#", ...) + + if self.valuesLength ~= len then + error(("Expected %d arguments, but was called with %d arguments"):format( + self.valuesLength, + len + ), 2) + end + + for i = 1, len do + local expected = select(i, ...) + + assert(self.values[i] == expected, "value differs") + end + end + + self.captureValues = function(_, ...) + local len = select("#", ...) + local result = {} + + assert(self.valuesLength == len, "length of expected values differs from stored values") + + for i = 1, len do + local key = select(i, ...) + result[key] = self.values[i] + end + + return result + end + + setmetatable(self, { + __index = function(_, key) + error(("%q is not a valid member of spy"):format(key)) + end, + }) + + return self +end + +return createSpy \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunk.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunk.lua new file mode 100644 index 0000000..79ca8da --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunk.lua @@ -0,0 +1,86 @@ +--[[ + An upgraded version of Rodux's thunk middleware that describes a table + format for thunks. This allows them to be named (via the `type` field) + and gives us cleaner methods for dependency injection via providing + services to our middleware that can be threaded into any thunks that + might request them +]] +local Root = script.Parent +local Symbol = require(Root.Symbols.Symbol) + +local Thunk = {} + +local ThunkTag = Symbol.named("ThunkTag") + +function Thunk.middleware(services) + services = services or {} + + return function(nextDispatch, store) + --[[ + This middleware doesn't need to do anything during initialization + so we go straight to returning the wrapped dispatch function + ]] + return function(action) + if action[ThunkTag] == true then + local injectedServices = {} + + for _, service in pairs(action.requiredServices) do + local providedService = services[service] + + if providedService == nil then + error(( + "Service with key %s is a dependency but was not provided" + ):format(service)) + end + + injectedServices[service] = providedService + end + + --[[ + By convention, we return the result of our thunk operation. + This value is not guaranteed to have any particular form or + meaning, but it prevents our middleware from conditionally returning, + which is a dangerous pattern in Lua. + ]] + return action(store, injectedServices) + else + return nextDispatch(action) + end + end + end +end + +function Thunk.new(name, requiredServices, onInvoke) + assert(typeof(name) == "string", "Bad arg #1: name must be a string") + assert(requiredServices == nil or typeof(requiredServices) == "table", + "Bad arg #2: requiredServices must be a table or nil") + assert(typeof(onInvoke) == "function", "Bad arg #3: onInvoke must be a function") + + requiredServices = requiredServices or {} + + return setmetatable({ + [ThunkTag] = true, + type = name, + requiredServices = requiredServices, + }, { + __call = function(self, ...) + onInvoke(...) + end, + }) +end + +function Thunk.test(thunk, store, providedServices) + assert(typeof(thunk) == "table" and thunk[ThunkTag] == true, + "Test Error - Bad arg #1: Must provide a valid thunk") + + if #thunk.requiredServices > 0 then + for _, service in ipairs(thunk.requiredServices) do + assert(providedServices[service] ~= nil, + "Test Error - Bad arg #3: Missing required service "..tostring(service)) + end + end + + return thunk(store, providedServices) +end + +return Thunk \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunk.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunk.spec.lua new file mode 100644 index 0000000..14f2263 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunk.spec.lua @@ -0,0 +1,106 @@ +return function() + local Root = script.Parent + local LuaPackages = Root.Parent + + local Rodux = require(LuaPackages.Rodux) + + local Thunk = require(script.Parent.Thunk) + + describe("Thunk middleware", function() + local function lastActionReducer(state, action) + return { + count = (state.count or 0) + 1, + lastAction = action, + } + end + + it("should only intercept thunk objects", function() + local store = Rodux.Store.new(lastActionReducer, {}, { Thunk.middleware() }) + + expect(store:getState().count).to.equal(1) + expect(store:getState().lastAction.type).to.equal("@@INIT") + + local thunk = Thunk.new("Foo", {}, function() + -- do nothing in particular + end) + store:dispatch(thunk) + + expect(store:getState().count).to.equal(1) + expect(store:getState().lastAction.type).to.equal("@@INIT") + + store:dispatch({ type = "NewAction" }) + + expect(store:getState().count).to.equal(2) + expect(store:getState().lastAction.type).to.equal("NewAction") + end) + + it("should invoke the provided functions of intercepted thunks", function() + local store = Rodux.Store.new(lastActionReducer, {}, { Thunk.middleware() }) + local thunkInvocations = 0 + + local thunk = Thunk.new("Foo", {}, function() + thunkInvocations = thunkInvocations + 1 + end) + + expect(thunkInvocations).to.equal(0) + + store:dispatch(thunk) + expect(thunkInvocations).to.equal(1) + + store:dispatch(thunk) + expect(thunkInvocations).to.equal(2) + end) + + it("should provide only the requested services to the thunk on invocation", function() + local fooServiceKey = newproxy(false) + local barServiceKey = newproxy(false) + local FooService = {} + local BarService = {} + + local store = Rodux.Store.new(lastActionReducer, {}, { + Thunk.middleware({ + [fooServiceKey] = FooService, + [barServiceKey] = BarService, + }) + }) + + local servicesFound = nil + local thunk = Thunk.new("Foo", { fooServiceKey }, function(store, services) + servicesFound = services + end) + + store:dispatch(thunk) + expect(servicesFound[fooServiceKey]).to.equal(FooService) + expect(servicesFound[barServiceKey]).never.to.be.ok() + end) + + it("should throw if thunks requests services that are not provided", function() + local store = Rodux.Store.new(lastActionReducer, {}, { Thunk.middleware() }) + local thunk = Thunk.new("Foo", { "fakeService" }, function() + -- do nothing in particular + end) + + expect(function() store:dispatch(thunk) end).to.throw() + end) + end) + + describe("Thunk constructor", function() + it("should validate arguments", function() + local noop = function() end + + expect(Thunk.new).to.throw() + expect(function() Thunk.new(10, nil, noop) end).to.throw() + expect(function() Thunk.new("Foo", 10, noop) end).to.throw() + expect(function() Thunk.new("Foo", nil, 10) end).to.throw() + end) + + it("should produce a callable table", function() + local thunk = Thunk.new("Foo", {}, function() + -- do nothing in particular + end) + + expect(type(thunk)).to.equal("table") + expect(function() thunk() end).never.to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completePurchase.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completePurchase.lua new file mode 100644 index 0000000..2437f53 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completePurchase.lua @@ -0,0 +1,33 @@ +local Root = script.Parent.Parent +local Workspace = game:GetService("Workspace") + +local SetPromptState = require(Root.Actions.SetPromptState) +local PurchaseCompleteRecieved = require(Root.Actions.PurchaseCompleteRecieved) +local PromptState = require(Root.Enums.PromptState) +local Thunk = require(Root.Thunk) + +--[[ + This delay is used to make sure the animation plays long enough + for the player to see that the purchase is happening; it's only + for visual effect +]] +local DELAY = 1 + +local function completePurchase() + return Thunk.new(script.Name, {}, function(store, services) + local startTime = store:getState().purchasingStartTime + local timeElapsed = Workspace.DistributedGameTime - startTime + + store:dispatch(PurchaseCompleteRecieved()) + + if timeElapsed >= DELAY then + return store:dispatch(SetPromptState(PromptState.PurchaseComplete)) + else + delay(DELAY - timeElapsed, function() + return store:dispatch(SetPromptState(PromptState.PurchaseComplete)) + end) + end + end) +end + +return completePurchase \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completeRequest.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completeRequest.lua new file mode 100644 index 0000000..c1150ef --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completeRequest.lua @@ -0,0 +1,42 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") +local MarketplaceService = game:GetService("MarketplaceService") + +local CompleteRequest = require(Root.Actions.CompleteRequest) +local RequestType = require(Root.Enums.RequestType) +local PurchaseError = require(Root.Enums.PurchaseError) +local Thunk = require(Root.Thunk) + +local function completeRequest() + return Thunk.new(script.Name, {}, function(store, services) + local state = store:getState() + local requestType = state.promptRequest.requestType + local purchaseError = state.purchaseError + local id = state.promptRequest.id + local didPurchase = state.hasCompletedPurchase + + if requestType == RequestType.Product then + local playerId = Players.LocalPlayer.UserId + + MarketplaceService:SignalPromptProductPurchaseFinished(playerId, id, didPurchase) + elseif requestType == RequestType.GamePass then + MarketplaceService:SignalPromptGamePassPurchaseFinished(Players.LocalPlayer, id, didPurchase) + elseif requestType == RequestType.Bundle then + MarketplaceService:SignalPromptBundlePurchaseFinished(Players.LocalPlayer, id, didPurchase) + elseif requestType == RequestType.Asset then + MarketplaceService:SignalPromptPurchaseFinished(Players.LocalPlayer, id, didPurchase) + + local assetTypeId = state.productInfo.assetTypeId + if didPurchase and assetTypeId then + -- AssetTypeId returned by the platform endpoint might not exist in the AssetType Enum + pcall(function() MarketplaceService:SignalAssetTypePurchased(Players.LocalPlayer, assetTypeId) end) + end + elseif requestType == RequestType.Premium then + MarketplaceService:SignalPromptPremiumPurchaseFinished(didPurchase or purchaseError == PurchaseError.AlreadyPremium) + end + + return store:dispatch(CompleteRequest()) + end) +end + +return completeRequest diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completeRequest.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completeRequest.spec.lua new file mode 100644 index 0000000..ccad2be --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completeRequest.spec.lua @@ -0,0 +1,202 @@ +return function() + local Root = script.Parent.Parent + local MarketplaceService = game:GetService("MarketplaceService") + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local RequestType = require(Root.Enums.RequestType) + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local createSpy = require(Root.Test.createSpy) + local Thunk = require(Root.Thunk) + + local completeRequest = require(script.Parent.completeRequest) + + describe("should signal prompt finished when purchase was not made", function() + it("should signal product purchase finished", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.PromptPurchase, + promptRequest = { + id = 123, + requestType = RequestType.Product, + infoType = Enum.InfoType.Product + }, + }) + + local thunk = completeRequest() + + local finishedSignalSpy = createSpy() + local connection = MarketplaceService.PromptProductPurchaseFinished:Connect(finishedSignalSpy.value) + + Thunk.test(thunk, store) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.None) + + expect(finishedSignalSpy.callCount).to.equal(1) + + local values = finishedSignalSpy:captureValues("userId", "productId", "didPurchase") + + expect(values.productId).to.equal(123) + expect(values.didPurchase).to.equal(false) + + connection:Disconnect() + end) + + it("should signal game pass purchase finished", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.Error, + promptRequest = { + id = 456, + requestType = RequestType.GamePass, + infoType = Enum.InfoType.GamePass + }, + }) + + local thunk = completeRequest() + + local finishedSignalSpy = createSpy() + local connection = MarketplaceService.PromptGamePassPurchaseFinished:Connect(finishedSignalSpy.value) + + Thunk.test(thunk, store) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.None) + + expect(finishedSignalSpy.callCount).to.equal(1) + + local values = finishedSignalSpy:captureValues("player", "gamePassId", "didPurchase") + + expect(values.gamePassId).to.equal(456) + expect(values.didPurchase).to.equal(false) + + connection:Disconnect() + end) + + it("should signal asset purchase finished", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.Error, + promptRequest = { + id = 789, + requestType = RequestType.Asset, + infoType = Enum.InfoType.Asset + }, + }) + + local thunk = completeRequest() + + local finishedSignalSpy = createSpy() + local connection = MarketplaceService.PromptPurchaseFinished:Connect(finishedSignalSpy.value) + + Thunk.test(thunk, store) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.None) + + expect(finishedSignalSpy.callCount).to.equal(1) + + local values = finishedSignalSpy:captureValues("player", "assetId", "didPurchase") + + expect(values.assetId).to.equal(789) + expect(values.didPurchase).to.equal(false) + + connection:Disconnect() + end) + end) + + describe("should signal prompt finished when purchase was completed", function() + it("should signal product purchase finished", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.PurchaseComplete, + promptRequest = { + id = 123, + requestType = RequestType.Product, + infoType = Enum.InfoType.Product + }, + hasCompletedPurchase = true, + }) + + local thunk = completeRequest() + + local finishedSignalSpy = createSpy() + local connection = MarketplaceService.PromptProductPurchaseFinished:Connect(finishedSignalSpy.value) + + Thunk.test(thunk, store) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.None) + + expect(finishedSignalSpy.callCount).to.equal(1) + + local values = finishedSignalSpy:captureValues("userId", "productId", "didPurchase") + + expect(values.productId).to.equal(123) + expect(values.didPurchase).to.equal(true) + + connection:Disconnect() + end) + + it("should signal game pass purchase finished", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.PurchaseComplete, + promptRequest = { + id = 456, + requestType = RequestType.GamePass, + infoType = Enum.InfoType.GamePass + }, + hasCompletedPurchase = true, + }) + + local thunk = completeRequest() + + local finishedSignalSpy = createSpy() + local connection = MarketplaceService.PromptGamePassPurchaseFinished:Connect(finishedSignalSpy.value) + + Thunk.test(thunk, store) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.None) + + expect(finishedSignalSpy.callCount).to.equal(1) + + local values = finishedSignalSpy:captureValues("player", "gamePassId", "didPurchase") + + expect(values.gamePassId).to.equal(456) + expect(values.didPurchase).to.equal(true) + + connection:Disconnect() + end) + + it("should signal asset purchase finished", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.PurchaseComplete, + promptRequest = { + id = 789, + requestType = RequestType.Asset, + infoType = Enum.InfoType.Asset + }, + hasCompletedPurchase = true, + }) + + local thunk = completeRequest() + + local finishedSignalSpy = createSpy() + local connection = MarketplaceService.PromptPurchaseFinished:Connect(finishedSignalSpy.value) + + Thunk.test(thunk, store) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.None) + + expect(finishedSignalSpy.callCount).to.equal(1) + + local values = finishedSignalSpy:captureValues("player", "assetId", "didPurchase") + + expect(values.assetId).to.equal(789) + expect(values.didPurchase).to.equal(true) + + connection:Disconnect() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/hideWindow.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/hideWindow.lua new file mode 100644 index 0000000..62b9bf2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/hideWindow.lua @@ -0,0 +1,13 @@ +local Root = script.Parent.Parent + +local SetWindowState = require(Root.Actions.SetWindowState) +local WindowState = require(Root.Enums.WindowState) +local Thunk = require(Root.Thunk) + +local function hideWindow(productInfo, accountInfo, alreadyOwned) + return Thunk.new(script.Name, {}, function(store, services) + return store:dispatch(SetWindowState(WindowState.Hidden)) + end) +end + +return hideWindow \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateBundlePurchase.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateBundlePurchase.lua new file mode 100644 index 0000000..521a42d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateBundlePurchase.lua @@ -0,0 +1,67 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") + +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local RequestBundlePurchase = require(Root.Actions.RequestBundlePurchase) +local PurchaseError = require(Root.Enums.PurchaseError) +local getBundleDetails = require(Root.Network.getBundleDetails) +local getProductPurchasableDetails = require(Root.Network.getProductPurchasableDetails) +local getAccountInfo = require(Root.Network.getAccountInfo) +local Network = require(Root.Services.Network) +local ExternalSettings = require(Root.Services.ExternalSettings) +local hasPendingRequest = require(Root.Utils.hasPendingRequest) +local Promise = require(Root.Promise) +local Thunk = require(Root.Thunk) + +local resolveBundlePromptState = require(script.Parent.resolveBundlePromptState) + +local requiredServices = { + Network, + ExternalSettings, +} + +local function initiateBundlePurchase(bundleId) + return Thunk.new(script.Name, requiredServices, function(store, services) + local network = services[Network] + local externalSettings = services[ExternalSettings] + + if hasPendingRequest(store:getState()) then + return nil + end + + store:dispatch(RequestBundlePurchase(bundleId)) + + local isStudio = externalSettings.isStudio() + + if not isStudio and Players.LocalPlayer.UserId <= 0 then + store:dispatch(ErrorOccurred(PurchaseError.Guest)) + return nil + end + + if externalSettings.getFlagOrder66() then + store:dispatch(ErrorOccurred(PurchaseError.PurchaseDisabled)) + return nil + end + + return Promise.all({ + bundleDetails = getBundleDetails(network, bundleId), + accountInfo = getAccountInfo(network, externalSettings) + }) + :andThen(function(results) + local bundleProductId = results.bundleDetails.product.id + getProductPurchasableDetails(network, bundleProductId) + :andThen(function(productPurchasableDetails) + store:dispatch(resolveBundlePromptState( + productPurchasableDetails, + results.bundleDetails, + results.accountInfo + )) + end) + end) + :catch(function(errorReason) + store:dispatch(ErrorOccurred(errorReason)) + end) + end) +end + +return initiateBundlePurchase \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateBundlePurchase.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateBundlePurchase.spec.lua new file mode 100644 index 0000000..2d77b09 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateBundlePurchase.spec.lua @@ -0,0 +1,70 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local RequestType = require(Root.Enums.RequestType) + local Reducer = require(Root.Reducers.Reducer) + local Network = require(Root.Services.Network) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockNetwork = require(Root.Test.MockNetwork) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + + local initiateBundlePurchase = require(script.Parent.initiateBundlePurchase) + + it("should run without errors", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiateBundlePurchase(15) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(), + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.promptRequest.id).to.equal(15) + expect(state.promptRequest.requestType).to.equal(RequestType.Bundle) + end) + + it("should abort when a purchase is already in progress", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.PromptPurchase, + promptRequest = { + id = 12, + infoType = Enum.InfoType.Product, + requestType = RequestType.Product + } + }) + + -- Initiate a purchase for a different product id + local thunk = initiateBundlePurchase(999) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(), + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + expect(state.promptRequest.id).to.equal(12) + expect(state.promptState).to.equal(PromptState.PromptPurchase) + end) + + it("should resolve to an error state if a network failure occurs", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiateBundlePurchase(15) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(true), + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.Error) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePremiumPurchase.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePremiumPurchase.lua new file mode 100644 index 0000000..0a6a333 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePremiumPurchase.lua @@ -0,0 +1,52 @@ +local Root = script.Parent.Parent + +local Promise = require(Root.Promise) +local Thunk = require(Root.Thunk) +local PurchaseError = require(Root.Enums.PurchaseError) + +local RequestPremiumPurchase = require(Root.Actions.RequestPremiumPurchase) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local getPremiumUpsellPrecheck = require(Root.Network.getPremiumUpsellPrecheck) +local getPremiumProductInfo = require(Root.Network.getPremiumProductInfo) +local getAccountInfo = require(Root.Network.getAccountInfo) +local Network = require(Root.Services.Network) +local ExternalSettings = require(Root.Services.ExternalSettings) +local resolvePremiumPromptState = require(Root.Thunks.resolvePremiumPromptState) +local hasPendingRequest = require(Root.Utils.hasPendingRequest) + +local requiredServices = { + Network, + ExternalSettings, +} + +local function initiatePremiumPurchase(id, infoType, equipIfPurchased) + return Thunk.new(script.Name, requiredServices, function(store, services) + local network = services[Network] + local externalSettings = services[ExternalSettings] + + if hasPendingRequest(store:getState()) then + return nil + end + store:dispatch(RequestPremiumPurchase()) + + if externalSettings.getFlagOrder66() then + store:dispatch(ErrorOccurred(PurchaseError.PurchaseDisabled)) + return nil + end + + local shouldPrecheck = not externalSettings.isStudio() + return Promise.all({ + canShowUpsell = shouldPrecheck and getPremiumUpsellPrecheck(network) or Promise.resolve(true), + premiumProductInfo = getPremiumProductInfo(network), + accountInfo = getAccountInfo(network, externalSettings), + }) + :andThen(function(results) + store:dispatch(resolvePremiumPromptState(results.accountInfo, results.premiumProductInfo, results.canShowUpsell)) + end) + :catch(function(errorReason) + store:dispatch(ErrorOccurred(errorReason)) + end) + end) +end + +return initiatePremiumPurchase diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePurchase.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePurchase.lua new file mode 100644 index 0000000..5920580 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePurchase.lua @@ -0,0 +1,89 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") +local ABTestService = game:GetService("ABTestService") + +local SetABVariation = require(Root.Actions.SetABVariation) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local RequestAssetPurchase = require(Root.Actions.RequestAssetPurchase) +local RequestGamepassPurchase = require(Root.Actions.RequestGamepassPurchase) +local RequestProductPurchase = require(Root.Actions.RequestProductPurchase) +local PurchaseError = require(Root.Enums.PurchaseError) +local Constants = require(Root.Misc.Constants) +local Network = require(Root.Services.Network) +local getProductInfo = require(Root.Network.getProductInfo) +local getIsAlreadyOwned = require(Root.Network.getIsAlreadyOwned) +local getAccountInfo = require(Root.Network.getAccountInfo) +local ExternalSettings = require(Root.Services.ExternalSettings) +local hasPendingRequest = require(Root.Utils.hasPendingRequest) +local Promise = require(Root.Promise) +local Thunk = require(Root.Thunk) + +local GetFFlagAdultConfirmationEnabled = require(Root.Flags.GetFFlagAdultConfirmationEnabled) + +local resolvePromptState = require(script.Parent.resolvePromptState) + +local requiredServices = { + Network, + ExternalSettings, +} + +local function initiatePurchase(id, infoType, equipIfPurchased, isRobloxPurchase) + return Thunk.new(script.Name, requiredServices, function(store, services) + local network = services[Network] + local externalSettings = services[ExternalSettings] + + if hasPendingRequest(store:getState()) then + return nil + end + + if GetFFlagAdultConfirmationEnabled() then + pcall(function() + store:dispatch(SetABVariation(Constants.ABTests.ADULT_CONFIRMATION, + ABTestService:GetVariant(Constants.ABTests.ADULT_CONFIRMATION))) + end) + end + + if infoType == Enum.InfoType.Asset then + store:dispatch(RequestAssetPurchase(id, equipIfPurchased, isRobloxPurchase)) + elseif infoType == Enum.InfoType.GamePass then + store:dispatch(RequestGamepassPurchase(id)) + elseif infoType == Enum.InfoType.Product then + store:dispatch(RequestProductPurchase(id, equipIfPurchased)) + else + assert(false, "Invalid product type") + return nil + end + + local isStudio = externalSettings.isStudio() + + if not isStudio and Players.LocalPlayer.UserId <= 0 then + store:dispatch(ErrorOccurred(PurchaseError.Guest)) + return nil + end + + if externalSettings.getFlagOrder66() then + store:dispatch(ErrorOccurred(PurchaseError.PurchaseDisabled)) + return nil + end + + return Promise.all({ + productInfo = getProductInfo(network, id, infoType), + accountInfo = getAccountInfo(network, externalSettings), + alreadyOwned = getIsAlreadyOwned(network, id, infoType), + }) + :andThen(function(results) + -- Once we've finished all of our async data fetching, we'll + -- resolve the state of the prompt + store:dispatch(resolvePromptState( + results.productInfo, + results.accountInfo, + results.alreadyOwned + )) + end) + :catch(function(errorReason) + store:dispatch(ErrorOccurred(errorReason)) + end) + end) +end + +return initiatePurchase diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePurchase.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePurchase.spec.lua new file mode 100644 index 0000000..1b253b9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePurchase.spec.lua @@ -0,0 +1,87 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local RequestType = require(Root.Enums.RequestType) + local PromptState = require(Root.Enums.PromptState) + local PurchaseError = require(Root.Enums.PurchaseError) + local Reducer = require(Root.Reducers.Reducer) + local Network = require(Root.Services.Network) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockNetwork = require(Root.Test.MockNetwork) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + + local initiatePurchase = require(script.Parent.initiatePurchase) + + it("should run without errors", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiatePurchase(15, Enum.InfoType.Product, false) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(), + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + }) + + local state = store:getState() + + expect(state.promptRequest.id).to.equal(15) + end) + + it("should abort when a purchase is already in progress", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.PromptPurchase, + promptRequest = { + id = 12, + requestType = RequestType.Product, + infoType = Enum.InfoType.Product, + } + }) + + -- Initiate a purchase for a different product id + local thunk = initiatePurchase(999, Enum.InfoType.Product, false) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(), + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + }) + + local state = store:getState() + expect(state.promptRequest.id).to.equal(12) + expect(state.promptState).to.equal(PromptState.PromptPurchase) + end) + + it("should resolve to an error state if a network failure occurs", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiatePurchase(15, Enum.InfoType.Product, false) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(true), + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + }) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.Error) + end) + + it("should resolve to an error state if purchasing is disabled", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiatePurchase(15, Enum.InfoType.Product, false) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(true), + [ExternalSettings] = MockExternalSettings.new(false, false, { + Order66 = true, + }), + }) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.Error) + expect(state.purchaseError).to.equal(PurchaseError.PurchaseDisabled) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateSubscriptionPurchase.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateSubscriptionPurchase.lua new file mode 100644 index 0000000..9786386 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateSubscriptionPurchase.lua @@ -0,0 +1,64 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") + +local Promise = require(Root.Promise) +local Thunk = require(Root.Thunk) +local PurchaseError = require(Root.Enums.PurchaseError) + +local RequestSubscriptionPurchase = require(Root.Actions.RequestSubscriptionPurchase) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local getProductInfo = require(Root.Network.getProductInfo) +local getIsAlreadyOwned = require(Root.Network.getIsAlreadyOwned) +local getAccountInfo = require(Root.Network.getAccountInfo) +local Network = require(Root.Services.Network) +local ExternalSettings = require(Root.Services.ExternalSettings) +local hasPendingRequest = require(Root.Utils.hasPendingRequest) +local resolveSubscriptionPromptState = require(Root.Thunks.resolveSubscriptionPromptState) +local GetFFlagDeveloperSubscriptionsEnabled = require(Root.Flags.GetFFlagDeveloperSubscriptionsEnabled) + +local requiredServices = { + Network, + ExternalSettings, +} + +local function initiateSubscriptionPurchase(id) + return Thunk.new(script.Name, requiredServices, function(store, services) + local network = services[Network] + local externalSettings = services[ExternalSettings] + + if not GetFFlagDeveloperSubscriptionsEnabled() or hasPendingRequest(store:getState()) then + return nil + end + store:dispatch(RequestSubscriptionPurchase(id)) + + local isStudio = externalSettings.isStudio() + + if not isStudio and Players.LocalPlayer.UserId <= 0 then + store:dispatch(ErrorOccurred(PurchaseError.Guest)) + return nil + end + + if externalSettings.getFlagOrder66() then + store:dispatch(ErrorOccurred(PurchaseError.PurchaseDisabled)) + return nil + end + + return Promise.all({ + productInfo = getProductInfo(network, id, Enum.InfoType.Subscription), + accountInfo = getAccountInfo(network, externalSettings), + alreadyOwned = getIsAlreadyOwned(network, id, Enum.InfoType.Subscription), + }) + :andThen(function(results) + store:dispatch(resolveSubscriptionPromptState( + results.productInfo, + results.accountInfo, + results.alreadyOwned + )) + end) + :catch(function(errorReason) + store:dispatch(ErrorOccurred(errorReason)) + end) + end) +end + +return initiateSubscriptionPurchase \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateSubscriptionPurchase.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateSubscriptionPurchase.spec.lua new file mode 100644 index 0000000..8a3e8d2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateSubscriptionPurchase.spec.lua @@ -0,0 +1,92 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local RequestType = require(Root.Enums.RequestType) + local PromptState = require(Root.Enums.PromptState) + local PurchaseError = require(Root.Enums.PurchaseError) + local Reducer = require(Root.Reducers.Reducer) + local Network = require(Root.Services.Network) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockNetwork = require(Root.Test.MockNetwork) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + local GetFFlagDeveloperSubscriptionsEnabled = require(Root.Flags.GetFFlagDeveloperSubscriptionsEnabled) + + local initiateSubscriptionPurchase = require(script.Parent.initiateSubscriptionPurchase) + + if not GetFFlagDeveloperSubscriptionsEnabled() then + return + end + + it("should run without errors", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiateSubscriptionPurchase(15) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(), + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + }) + + local state = store:getState() + + expect(state.promptRequest.id).to.equal(15) + end) + + it("should abort when a purchase is already in progress", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.PromptPurchase, + promptRequest = { + id = 12, + requestType = RequestType.Product, + infoType = Enum.InfoType.Product, + } + }) + + -- Initiate a purchase for a different product id + local thunk = initiateSubscriptionPurchase(999) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(), + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + }) + + local state = store:getState() + expect(state.promptRequest.id).to.equal(12) + expect(state.promptState).to.equal(PromptState.PromptPurchase) + end) + + it("should resolve to an error state if a network failure occurs", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiateSubscriptionPurchase(15) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(true), + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + }) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.Error) + end) + + it("should resolve to an error state if purchasing is disabled", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiateSubscriptionPurchase(15) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(true), + [ExternalSettings] = MockExternalSettings.new(false, false, { + Order66 = true, + }), + }) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.Error) + expect(state.purchaseError).to.equal(PurchaseError.PurchaseDisabled) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchPremiumUpsell.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchPremiumUpsell.lua new file mode 100644 index 0000000..65656b7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchPremiumUpsell.lua @@ -0,0 +1,60 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") + +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local PurchaseCompleteRecieved = require(Root.Actions.PurchaseCompleteRecieved) +local SetPromptState = require(Root.Actions.SetPromptState) +local SetWindowState = require(Root.Actions.SetWindowState) +local UpsellFlow = require(Root.Enums.UpsellFlow) +local PromptState = require(Root.Enums.PromptState) +local PurchaseError = require(Root.Enums.PurchaseError) +local WindowState = require(Root.Enums.WindowState) +local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow) +local PlatformInterface = require(Root.Services.PlatformInterface) +local ExternalSettings = require(Root.Services.ExternalSettings) +local hideWindow = require(Root.Thunks.hideWindow) +local Thunk = require(Root.Thunk) + +local requiredServices = { + PlatformInterface, + ExternalSettings, +} + +local function launchPremiumUpsell() + return Thunk.new(script.Name, requiredServices, function(store, services) + local platformInterface = services[PlatformInterface] + local externalSettings = services[ExternalSettings] + local state = store:getState() + local premiumProductInfo = state.premiumProductInfo + + if externalSettings.isStudio() then + -- Signal back end that they clicked yes + -- waits for SignalPromptPremiumPurchaseFinished to report membership changed + platformInterface.signalMockPurchasePremium() + store:dispatch(PurchaseCompleteRecieved()) + return store:dispatch(SetWindowState(WindowState.Hidden)) + end + + local upsellFlow = getUpsellFlow(externalSettings.getPlatform()) + + if upsellFlow == UpsellFlow.Web then + local productId = premiumProductInfo.productId + + platformInterface.startPremiumUpsell(productId) + store:dispatch(SetPromptState(PromptState.UpsellInProgress)) + store:dispatch(hideWindow()) + + elseif upsellFlow == UpsellFlow.Mobile then + local nativeProductId = premiumProductInfo.mobileProductId + + platformInterface.promptNativePurchase(Players.LocalPlayer, nativeProductId) + store:dispatch(SetPromptState(PromptState.UpsellInProgress)) + store:dispatch(hideWindow()) + + else + store:dispatch(ErrorOccurred(PurchaseError.PremiumUnavailablePlatform)) + end + end) +end + +return launchPremiumUpsell \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchPremiumUpsell.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchPremiumUpsell.spec.lua new file mode 100644 index 0000000..7493ec9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchPremiumUpsell.spec.lua @@ -0,0 +1,110 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local WindowState = require(Root.Enums.WindowState) + local PurchaseError = require(Root.Enums.PurchaseError) + local Reducer = require(Root.Reducers.Reducer) + local PlatformInterface = require(Root.Services.PlatformInterface) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockPlatformInterface = require(Root.Test.MockPlatformInterface) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + + local launchPremiumUpsell = require(script.Parent.launchPremiumUpsell) + + it("should run without errors on Studio", function() + local store = Rodux.Store.new(Reducer, { + premiumProductInfo = { + id = 350, + } + }) + + local thunk = launchPremiumUpsell() + local platformInterface = MockPlatformInterface.new() + local externalSettings = MockExternalSettings.new(true, false, {}, Enum.Platform.Windows) + + Thunk.test(thunk, store, { + [PlatformInterface] = platformInterface.mockService, + [ExternalSettings] = externalSettings + }) + + local state = store:getState() + + expect(platformInterface.spies.signalMockPurchasePremium.callCount).to.equal(1) + expect(state.promptState).to.equal(PromptState.None) + expect(state.windowState).to.equal(WindowState.Hidden) + end) + + it("should run without errors on Desktop", function() + local store = Rodux.Store.new(Reducer, { + premiumProductInfo = { + id = 350, + } + }) + + local thunk = launchPremiumUpsell() + local platformInterface = MockPlatformInterface.new() + local externalSettings = MockExternalSettings.new(false, false, {}, Enum.Platform.Windows) + + Thunk.test(thunk, store, { + [PlatformInterface] = platformInterface.mockService, + [ExternalSettings] = externalSettings + }) + + local state = store:getState() + + -- https://jira.rbx.com/browse/EC-46 + -- expect(platformInterface.spies.startPremiumUpsell.callCount).to.equal(1) + -- expect(state.promptState).to.equal(PromptState.UpsellInProgress) + end) + + it("should run without errors on Mobile", function() + local store = Rodux.Store.new(Reducer, { + premiumProductInfo = { + id = 350, + } + }) + + local thunk = launchPremiumUpsell() + local platformInterface = MockPlatformInterface.new() + local externalSettings = MockExternalSettings.new(false, false, {}, Enum.Platform.IOS) + + Thunk.test(thunk, store, { + [PlatformInterface] = platformInterface.mockService, + [ExternalSettings] = externalSettings + }) + + local state = store:getState() + + -- https://jira.rbx.com/browse/EC-46 + -- expect(platformInterface.spies.promptNativePurchase.callCount).to.equal(1) + -- expect(state.promptState).to.equal(PromptState.UpsellInProgress) + end) + + it("should run into error on unsupported platforms", function() + local store = Rodux.Store.new(Reducer, { + premiumProductInfo = { + id = 350, + } + }) + + local thunk = launchPremiumUpsell() + local platformInterface = MockPlatformInterface.new() + local externalSettings = MockExternalSettings.new(false, false, {}, Enum.Platform.XBoxOne) + + Thunk.test(thunk, store, { + [PlatformInterface] = platformInterface.mockService, + [ExternalSettings] = externalSettings + }) + + local state = store:getState() + + -- https://jira.rbx.com/browse/EC-46 + -- expect(platformInterface.spies.startPremiumUpsell.callCount).to.equal(0) + -- expect(state.purchaseError).to.equal(PurchaseError.PremiumUnavailablePlatform) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchRobuxUpsell.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchRobuxUpsell.lua new file mode 100644 index 0000000..15df2a4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchRobuxUpsell.lua @@ -0,0 +1,78 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") + +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local SetPromptState = require(Root.Actions.SetPromptState) +local UpsellFlow = require(Root.Enums.UpsellFlow) +local PromptState = require(Root.Enums.PromptState) +local PurchaseError = require(Root.Enums.PurchaseError) +local Constants = require(Root.Misc.Constants) +local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow) +local Analytics = require(Root.Services.Analytics) +local PlatformInterface = require(Root.Services.PlatformInterface) +local Thunk = require(Root.Thunk) +local Promise = require(Root.Promise) + +local retryAfterUpsell = require(script.Parent.retryAfterUpsell) + +local GetFFlagAdultConfirmationEnabled = require(Root.Flags.GetFFlagAdultConfirmationEnabled) +local GetFFlagAdultConfirmationEnabledNew = require(Root.Flags.GetFFlagAdultConfirmationEnabledNew) + +local requiredServices = { + Analytics, + PlatformInterface, +} + +local function launchRobuxUpsell() + return Thunk.new(script.Name, requiredServices, function(store, services) + local analytics = services[Analytics] + local platformInterface = services[PlatformInterface] + local state = store:getState() + local abVars = state.abVariations + + if (GetFFlagAdultConfirmationEnabledNew() + or (GetFFlagAdultConfirmationEnabled() and (abVars[Constants.ABTests.ADULT_CONFIRMATION] == "Variation1"))) + and state.accountInfo.AgeBracket ~= 0 + and state.promptState ~= PromptState.AdultConfirmation then + analytics.signalAdultLegalTextShown() + store:dispatch(SetPromptState(PromptState.AdultConfirmation)) + return + end + + local upsellFlow = getUpsellFlow(UserInputService:GetPlatform()) + + if upsellFlow == UpsellFlow.Web then + platformInterface.startRobuxUpsellWeb() + analytics.reportRobuxUpsellStarted() + store:dispatch(SetPromptState(PromptState.UpsellInProgress)) + + elseif upsellFlow == UpsellFlow.Mobile then + local nativeProductId = store:getState().nativeUpsell.robuxProductId + + analytics.reportNativeUpsellStarted(nativeProductId) + platformInterface.promptNativePurchase(Players.LocalPlayer, nativeProductId) + store:dispatch(SetPromptState(PromptState.UpsellInProgress)) + + elseif upsellFlow == UpsellFlow.Xbox then + local nativeProductId = store:getState().nativeUpsell.robuxProductId + store:dispatch(SetPromptState(PromptState.UpsellInProgress)) + return Promise.new(function(resolve, reject) + local platformPurchaseResult = platformInterface.beginPlatformStorePurchase(nativeProductId) + + Promise.resolve(platformPurchaseResult) + end) + :andThen(function(result) + if result ~= 0 then + store:dispatch(retryAfterUpsell) + end + end) + elseif upsellFlow == UpsellFlow.Unavailable then + store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobuxNoUpsell)) + else + warn("Need more Robux: platform not supported for Robux purchase") + end + end) +end + +return launchRobuxUpsell diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchRobuxUpsell.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchRobuxUpsell.spec.lua new file mode 100644 index 0000000..c06aa7b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchRobuxUpsell.spec.lua @@ -0,0 +1,128 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local Analytics = require(Root.Services.Analytics) + local PlatformInterface = require(Root.Services.PlatformInterface) + local MockAnalytics = require(Root.Test.MockAnalytics) + local MockPlatformInterface = require(Root.Test.MockPlatformInterface) + local Constants = require(Root.Misc.Constants) + local Thunk = require(Root.Thunk) + + local GetFFlagAdultConfirmationEnabled = require(Root.Flags.GetFFlagAdultConfirmationEnabled) + local GetFFlagAdultConfirmationEnabledNew = require(Root.Flags.GetFFlagAdultConfirmationEnabledNew) + + local launchRobuxUpsell = require(script.Parent.launchRobuxUpsell) + + it("should run without errors", function() + local store = Rodux.Store.new(Reducer, { + accountInfo = { + AgeBracket = 0, + }, + promptState = PromptState.PromptPurchase, + }) + + local thunk = launchRobuxUpsell() + local analytics = MockAnalytics.new() + local platformInterface = MockPlatformInterface.new() + + Thunk.test(thunk, store, { + [Analytics] = analytics.mockService, + [PlatformInterface] = platformInterface.mockService, + }) + + local state = store:getState() + + if not settings():GetFFlag("ChinaLicensingApp") then + expect(analytics.spies.reportRobuxUpsellStarted.callCount).to.equal(1) + expect(platformInterface.spies.startRobuxUpsellWeb.callCount).to.equal(1) + expect(state.promptState).to.equal(PromptState.UpsellInProgress) + end + end) + + if GetFFlagAdultConfirmationEnabledNew() then + it("should show adult legal text if under 13", function() + local store = Rodux.Store.new(Reducer, { + accountInfo = { + AgeBracket = 1, + }, + promptState = PromptState.PromptPurchase, + }) + + local thunk = launchRobuxUpsell() + local analytics = MockAnalytics.new() + local platformInterface = MockPlatformInterface.new() + + Thunk.test(thunk, store, { + [Analytics] = analytics.mockService, + [PlatformInterface] = platformInterface.mockService, + }) + + local state = store:getState() + + expect(analytics.spies.signalAdultLegalTextShown.callCount).to.equal(1) + expect(state.promptState).to.equal(PromptState.AdultConfirmation) + end) + end + + if GetFFlagAdultConfirmationEnabled() then + it("should show adult legal text if under 13 and part of ab test", function() + local store = Rodux.Store.new(Reducer, { + accountInfo = { + AgeBracket = 1, + }, + promptState = PromptState.PromptPurchase, + abVariations = { + [Constants.ABTests.ADULT_CONFIRMATION] = "Variation1", + } + }) + + local thunk = launchRobuxUpsell() + local analytics = MockAnalytics.new() + local platformInterface = MockPlatformInterface.new() + + Thunk.test(thunk, store, { + [Analytics] = analytics.mockService, + [PlatformInterface] = platformInterface.mockService, + }) + + local state = store:getState() + + expect(analytics.spies.signalAdultLegalTextShown.callCount).to.equal(1) + expect(state.promptState).to.equal(PromptState.AdultConfirmation) + end) + + it("should continue as normal if under 13 and not apart of ab test", function() + local store = Rodux.Store.new(Reducer, { + accountInfo = { + AgeBracket = 1, + }, + promptState = PromptState.PromptPurchase, + abVariations = { + [Constants.ABTests.ADULT_CONFIRMATION] = "Control", + } + }) + + local thunk = launchRobuxUpsell() + local analytics = MockAnalytics.new() + local platformInterface = MockPlatformInterface.new() + + Thunk.test(thunk, store, { + [Analytics] = analytics.mockService, + [PlatformInterface] = platformInterface.mockService, + }) + + local state = store:getState() + + if not settings():GetFFlag("ChinaLicensingApp") then + expect(analytics.spies.reportRobuxUpsellStarted.callCount).to.equal(1) + expect(platformInterface.spies.startRobuxUpsellWeb.callCount).to.equal(1) + expect(state.promptState).to.equal(PromptState.UpsellInProgress) + end + end) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/purchaseItem.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/purchaseItem.lua new file mode 100644 index 0000000..622af77 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/purchaseItem.lua @@ -0,0 +1,82 @@ +local Root = script.Parent.Parent +local HttpService = game:GetService("HttpService") +local Workspace = game:GetService("Workspace") +local Players = game:GetService("Players") + +local StartPurchase = require(Root.Actions.StartPurchase) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local ItemType = require(Root.Enums.ItemType) +local getToolAsset = require(Root.Network.getToolAsset) +local performPurchase = require(Root.Network.performPurchase) +local Network = require(Root.Services.Network) +local Analytics = require(Root.Services.Analytics) +local getPlayerPrice = require(Root.Utils.getPlayerPrice) +local Thunk = require(Root.Thunk) +local Promise = require(Root.Promise) + +local GetFFlagPromptRobloxPurchaseEnabled = require(Root.Flags.GetFFlagPromptRobloxPurchaseEnabled) + +local completePurchase = require(script.Parent.completePurchase) + +-- Only tools can be equipped on purchase +local ASSET_TYPE_TOOL = 19 + +local requiredServices = { + Network, + Analytics, +} + +local function purchaseItem() + return Thunk.new(script.Name, requiredServices, function(store, services) + local network = services[Network] + local analytics = services[Analytics] + + store:dispatch(StartPurchase(Workspace.DistributedGameTime)) + + local state = store:getState() + + local requestId = HttpService:GenerateGUID(false) + + local id = state.promptRequest.id + local infoType = state.promptRequest.infoType + local equipIfPurchased = state.promptRequest.equipIfPurchased + local isRobloxPurchase = GetFFlagPromptRobloxPurchaseEnabled() and state.promptRequest.isRobloxPurchase or false + + local isPlayerPremium = state.accountInfo.membershipType == 4 + local salePrice = getPlayerPrice(state.productInfo, isPlayerPremium) + local assetTypeId = state.productInfo.assetTypeId + local productId = state.productInfo.productId + + local itemType = state.productInfo.itemType + + return performPurchase(network, infoType, productId, salePrice, requestId, isRobloxPurchase) + :andThen(function(result) + --[[ + If the purchase was successful, we signal success, + record analytics, and equip the item if needed + ]] + store:dispatch(completePurchase()) + + -- Marketplace Analytics for bundles is not available yet. + if itemType ~= ItemType.Bundle then + analytics.signalPurchaseSuccess(id, infoType, salePrice, result) + end + + if equipIfPurchased and assetTypeId == ASSET_TYPE_TOOL then + return getToolAsset(network, id) + :andThen(function(tool) + if tool then + tool.Parent = Players.LocalPlayer.Backpack + end + end) + end + + return Promise.resolve() + end) + :catch(function(errorReason) + store:dispatch(ErrorOccurred(errorReason)) + end) + end) +end + +return purchaseItem diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/purchaseItem.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/purchaseItem.spec.lua new file mode 100644 index 0000000..8d6e56f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/purchaseItem.spec.lua @@ -0,0 +1,52 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local Network = require(Root.Services.Network) + local Analytics = require(Root.Services.Analytics) + local MockNetwork = require(Root.Test.MockNetwork) + local MockAnalytics = require(Root.Test.MockAnalytics) + local Thunk = require(Root.Thunk) + + local purchaseItem = require(script.Parent.purchaseItem) + + it("should run without errors", function() + local store = Rodux.Store.new(Reducer) + + local thunk = purchaseItem() + local network = MockNetwork.new() + local analytics = MockAnalytics.new() + + Thunk.test(thunk, store, { + [Network] = network, + [Analytics] = analytics.mockService, + }) + + local state = store:getState() + + expect(analytics.spies.signalPurchaseSuccess.callCount).to.equal(1) + expect(state.promptState).to.equal(PromptState.PurchaseInProgress) + end) + + it("should resolve to an error state if a network error occurs", function() + local store = Rodux.Store.new(Reducer) + + local thunk = purchaseItem() + local network = MockNetwork.new(true) + local analytics = MockAnalytics.new() + + Thunk.test(thunk, store, { + [Network] = network, + [Analytics] = analytics.mockService, + }) + + local state = store:getState() + + expect(analytics.spies.signalPurchaseSuccess.callCount).to.equal(0) + expect(state.promptState).to.equal(PromptState.Error) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveBundlePromptState.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveBundlePromptState.lua new file mode 100644 index 0000000..2e4b4d9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveBundlePromptState.lua @@ -0,0 +1,73 @@ +local Root = script.Parent.Parent +local UserInputService = game:GetService("UserInputService") + +local SetPromptState = require(Root.Actions.SetPromptState) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local BundleProductInfoReceived = require(Root.Actions.BundleProductInfoReceived) +local AccountInfoReceived = require(Root.Actions.AccountInfoReceived) +local PromptNativeUpsell = require(Root.Actions.PromptNativeUpsell) +local PromptState = require(Root.Enums.PromptState) +local PurchaseError = require(Root.Enums.PurchaseError) +local UpsellFlow = require(Root.Enums.UpsellFlow) +local selectRobuxProduct = require(Root.NativeUpsell.selectRobuxProduct) +local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow) +local Thunk = require(Root.Thunk) + +local function getPurchasableStatus(productPurchasableDetails) + local reason = productPurchasableDetails.reason + + if reason == "InsufficientFunds" then + return PurchaseError.NotEnoughRobux + elseif reason == "AlreadyOwned" then + return PurchaseError.AlreadyOwn + elseif reason == "NotForSale" then + return PurchaseError.NotForSale + elseif reason == "ContentRatingRestricted" then + return PurchaseError.Under13 + else + return PurchaseError.UnknownFailure + end +end + +local function resolveBundlePromptState(productPurchasableDetails, bundleDetails, accountInfo) + return Thunk.new(script.Name, {}, function(store, services) + store:dispatch(BundleProductInfoReceived(bundleDetails)) + store:dispatch(AccountInfoReceived(accountInfo)) + + local canPurchase = productPurchasableDetails.purchasable + local failureReason = getPurchasableStatus(productPurchasableDetails) + local price = productPurchasableDetails.price + local platform = UserInputService:GetPlatform() + local upsellFlow = getUpsellFlow(platform) + + if not canPurchase then + if failureReason == PurchaseError.NotEnoughRobux then + if upsellFlow == UpsellFlow.Web then + return store:dispatch(SetPromptState(PromptState.RobuxUpsell)) + else + local neededRobux = price - accountInfo.RobuxBalance + local hasMembership = accountInfo.MembershipType > 0 + + return selectRobuxProduct(platform, neededRobux, hasMembership) + :andThen(function(product) + -- We found a valid upsell product for the current platform + store:dispatch(PromptNativeUpsell(product.productId, product.robuxValue)) + end, function() + -- No upsell item will provide sufficient funds to make this purchase + if platform == Enum.Platform.XBoxOne then + store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobuxXbox)) + else + store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobux)) + end + end) + end + else + return store:dispatch(ErrorOccurred(failureReason)) + end + end + + return store:dispatch(SetPromptState(PromptState.PromptPurchase)) + end) +end + +return resolveBundlePromptState \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveBundlePromptState.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveBundlePromptState.spec.lua new file mode 100644 index 0000000..00321b7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveBundlePromptState.spec.lua @@ -0,0 +1,133 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + + local resolveBundlePromptState = require(script.Parent.resolveBundlePromptState) + + local function getTestPurchasableDetails() + return { + purchasable = true, + reason = "mock-reason", + price = 100, + } + end + + local function getTestBundleDetails() + return { + id = 1, + name = "mock-name", + description = "mock-description", + items = { + [1] = { + id = 1, + name = "outfit-name", + type = "UserOutfit", + }, + }, + creator = { + id = 1, + name = "ROBLOX", + type = "User", + }, + product = { + id = 1, + isForSale = true, + priceInRobux = 100, + } + } + end + + it("should populate store with provided info", function() + local store = Rodux.Store.new(Reducer, {}) + + local purchasableDetails = getTestPurchasableDetails() + local bundleDetails = getTestBundleDetails() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolveBundlePromptState(purchasableDetails, bundleDetails, accountInfo) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.productInfo.name).to.be.ok() + expect(state.accountInfo.balance).to.be.ok() + end) + + it("should resolve state to Error if prerequisites are failed", function() + local store = Rodux.Store.new(Reducer, {}) + + local purchasableDetails = getTestPurchasableDetails() + local bundleDetails = getTestBundleDetails() -- Set product to not for sale + purchasableDetails.purchasable = false + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolveBundlePromptState(purchasableDetails, bundleDetails, accountInfo) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.Error) + end) + + it("should resolve state to PromptPurchase if account meets requirements", function() + local store = Rodux.Store.new(Reducer, {}) + + local purchasableDetails = getTestPurchasableDetails() + local bundleDetails = getTestBundleDetails() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolveBundlePromptState(purchasableDetails, bundleDetails, accountInfo) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.PromptPurchase) + end) + + it("should resolve state to RobuxUpsell if account is short on Robux", function() + local store = Rodux.Store.new(Reducer, {}) + + local purchasableDetails = getTestPurchasableDetails() + local bundleDetails = getTestBundleDetails() + + purchasableDetails.purchasable = false + purchasableDetails.reason = "InsufficientFunds" + -- Player will not have enough robux + local accountInfo = { + RobuxBalance = 0, + MembershipType = 0, + } + local thunk = resolveBundlePromptState(purchasableDetails, bundleDetails, accountInfo) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.RobuxUpsell) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePremiumPromptState.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePremiumPromptState.lua new file mode 100644 index 0000000..a1e99a6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePremiumPromptState.lua @@ -0,0 +1,65 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") + +local SetPromptState = require(Root.Actions.SetPromptState) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local PremiumInfoRecieved = require(Root.Actions.PremiumInfoRecieved) +local AccountInfoReceived = require(Root.Actions.AccountInfoReceived) +local PromptState = require(Root.Enums.PromptState) +local PurchaseError = require(Root.Enums.PurchaseError) +local Analytics = require(Root.Services.Analytics) +local ExternalSettings = require(Root.Services.ExternalSettings) +local Network = require(Root.Services.Network) +local postPremiumImpression = require(Root.Network.postPremiumImpression) +local completeRequest = require(Root.Thunks.completeRequest) +local Thunk = require(Root.Thunk) + + +local requiredServices = { + Network, + ExternalSettings, + Analytics, +} + +local function resolvePremiumPromptState(accountInfo, premiumProduct, canShowUpsell) + return Thunk.new(script.Name, requiredServices, function(store, services) + local network = services[Network] + local externalSettings = services[ExternalSettings] + local analytics = services[Analytics] + local platform = externalSettings.getPlatform() + + store:dispatch(PremiumInfoRecieved(premiumProduct)) + store:dispatch(AccountInfoReceived(accountInfo)) + + if canShowUpsell == false then + return store:dispatch(completeRequest()) + end + + if externalSettings.isStudio() then + if Players.LocalPlayer.MembershipType == Enum.MembershipType.Premium then + return store:dispatch(ErrorOccurred(PurchaseError.AlreadyPremium)) + end + else + if accountInfo.MembershipType == 4 then + analytics.signalPremiumUpsellShownPremium() + return store:dispatch(ErrorOccurred(PurchaseError.AlreadyPremium)) + end + end + + if platform == Enum.Platform.XBoxOne then + return store:dispatch(ErrorOccurred(PurchaseError.PremiumUnavailablePlatform)) + end + + if premiumProduct == nil then + return store:dispatch(ErrorOccurred(PurchaseError.PremiumUnavailable)) + end + + if not externalSettings.isStudio() then + analytics.signalPremiumUpsellShownNonPremium() + postPremiumImpression(network) + end + return store:dispatch(SetPromptState(PromptState.PremiumUpsell)) + end) +end + +return resolvePremiumPromptState diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePremiumPromptState.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePremiumPromptState.spec.lua new file mode 100644 index 0000000..a36655a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePremiumPromptState.spec.lua @@ -0,0 +1,116 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local Analytics = require(Root.Services.Analytics) + local ExternalSettings = require(Root.Services.ExternalSettings) + local Network = require(Root.Services.Network) + local MockAnalytics = require(Root.Test.MockAnalytics) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local MockNetwork = require(Root.Test.MockNetwork) + local Thunk = require(Root.Thunk) + + local resolvePremiumPromptState = require(script.Parent.resolvePremiumPromptState) + + local function getTestProductInfo() + return { + premiumFeatureTypeName = "Subscription", + mobileProductId = "com.roblox.robloxmobile.RobloxPremium450", + description = "Roblox Premium 450", + price = 4.99, + currencySymbol = "$", + isSubscriptionOnly = false, + robuxAmount = 450 + } + end + + it("should populate store with provided info", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePremiumPromptState(accountInfo, productInfo) + + Thunk.test(thunk, store, { + [Analytics] = MockAnalytics.new().mockService, + [ExternalSettings] = MockExternalSettings.new(false, false, { + }, true), + [Network] = MockNetwork.new(), + }) + + local state = store:getState() + + expect(state.premiumProductInfo.mobileProductId).to.be.ok() + expect(state.accountInfo.membershipType).to.be.ok() + end) + + it("should resolve state to Error if failed to get premium products", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = nil + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePremiumPromptState(accountInfo, productInfo) + + Thunk.test(thunk, store, { + [Analytics] = MockAnalytics.new().mockService, + [ExternalSettings] = MockExternalSettings.new(false, false, {}, true), + [Network] = MockNetwork.new(), + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.Error) + end) + + it("should show the upsell given correct data", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePremiumPromptState(accountInfo, productInfo, true) + + Thunk.test(thunk, store, { + [Analytics] = MockAnalytics.new().mockService, + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + [Network] = MockNetwork.new(), + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.PremiumUpsell) + end) + + it("should complete the request and show nothing when failing precheck", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePremiumPromptState(accountInfo, productInfo, false) + + Thunk.test(thunk, store, { + [Analytics] = MockAnalytics.new().mockService, + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + [Network] = MockNetwork.new(), + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.None) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePromptState.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePromptState.lua new file mode 100644 index 0000000..ccec9aa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePromptState.lua @@ -0,0 +1,82 @@ +local Root = script.Parent.Parent +local UserInputService = game:GetService("UserInputService") + +local SetPromptState = require(Root.Actions.SetPromptState) +local ProductInfoReceived = require(Root.Actions.ProductInfoReceived) +local AccountInfoReceived = require(Root.Actions.AccountInfoReceived) +local PromptNativeUpsell = require(Root.Actions.PromptNativeUpsell) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local CompleteRequest = require(Root.Actions.CompleteRequest) +local PromptState = require(Root.Enums.PromptState) +local PurchaseError = require(Root.Enums.PurchaseError) +local UpsellFlow = require(Root.Enums.UpsellFlow) +local selectRobuxProduct = require(Root.NativeUpsell.selectRobuxProduct) +local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow) +local ExternalSettings = require(Root.Services.ExternalSettings) +local meetsPrerequisites = require(Root.Utils.meetsPrerequisites) +local getPlayerProductInfoPrice = require(Root.Utils.getPlayerProductInfoPrice) +local Thunk = require(Root.Thunk) + +local requiredServices = { + ExternalSettings, +} + +local function resolvePromptState(productInfo, accountInfo, alreadyOwned) + return Thunk.new(script.Name, requiredServices, function(store, services) + local externalSettings = services[ExternalSettings] + + store:dispatch(ProductInfoReceived(productInfo)) + store:dispatch(AccountInfoReceived(accountInfo)) + + local restrictThirdParty = externalSettings.getLuaUseThirdPartyPermissions() or externalSettings.getFlagRestrictSales2() + + local canPurchase, failureReason = meetsPrerequisites(productInfo, alreadyOwned, restrictThirdParty, externalSettings) + if not canPurchase then + if externalSettings.getFlagHideThirdPartyPurchaseFailure() then + if not externalSettings.isStudio() and failureReason == PurchaseError.ThirdPartyDisabled then + -- Do not annoy player with 3rd party failure notifications. + return store:dispatch(CompleteRequest()) + end + return store:dispatch(ErrorOccurred(failureReason)) + else + return store:dispatch(ErrorOccurred(failureReason)) + end + end + + local isPlayerPremium = accountInfo.MembershipType == 4 + local price = getPlayerProductInfoPrice(productInfo, isPlayerPremium) + local platform = UserInputService:GetPlatform() + local upsellFlow = getUpsellFlow(platform) + + if price > accountInfo.RobuxBalance then + + if upsellFlow == UpsellFlow.Unavailable then + return store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobuxNoUpsell)) + end + + if upsellFlow == UpsellFlow.Web then + return store:dispatch(SetPromptState(PromptState.RobuxUpsell)) + else + local neededRobux = price - accountInfo.RobuxBalance + local hasMembership = accountInfo.MembershipType > 0 + + return selectRobuxProduct(platform, neededRobux, hasMembership) + :andThen(function(product) + -- We found a valid upsell product for the current platform + store:dispatch(PromptNativeUpsell(product.productId, product.robuxValue)) + end, function() + -- No upsell item will provide sufficient funds to make this purchase + if platform == Enum.Platform.XBoxOne then + store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobuxXbox)) + else + store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobux)) + end + end) + end + end + + return store:dispatch(SetPromptState(PromptState.PromptPurchase)) + end) +end + +return resolvePromptState diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePromptState.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePromptState.spec.lua new file mode 100644 index 0000000..0b927b2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePromptState.spec.lua @@ -0,0 +1,139 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + + local resolvePromptState = require(script.Parent.resolvePromptState) + local RequestType = require(Root.Enums.RequestType) + + local function getTestProductInfo() + return { + IsForSale = true, + Name = "Test Product", + PriceInRobux = 10, + MinimumMembershipLevel = 0, + Creator = { + CreatorType = "User", + CreatorTargetId = 1, + }, + } + end + + it("should populate store with provided info", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.productInfo.name).to.be.ok() + expect(state.accountInfo.balance).to.be.ok() + end) + + it("should resolve state to None if hiding 3rd party purchase failure", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + -- Make creator a 3rd party + productInfo.AssetId = 0 + productInfo.Creator.CreatorTargetId = game.CreatorId + 2 + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, { + LuaUseThirdPartyPermissions = true, + PermissionsServiceIsThirdPartyPurchaseAllowed = false, + HideThirdPartyPurchaseFailure = true, + }) + }) + + local state = store:getState() + + expect(state.promptRequest.requestType).to.equal(RequestType.None) + expect(state.promptState).to.equal(PromptState.None) + end) + + it("should resolve state to Error if prerequisites are failed", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + -- Set product to not for sale + productInfo.IsForSale = false + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.Error) + end) + + it("should resolve state to PromptPurchase if account meets requirements", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + if not settings():GetFFlag("ChinaLicensingApp") then + expect(state.promptState).to.equal(PromptState.PromptPurchase) + end + end) + + it("should resolve state to RobuxUpsell if account is short on Robux", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + -- Player will not have enough robux + local accountInfo = { + RobuxBalance = 0, + MembershipType = 0, + } + local thunk = resolvePromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + if not settings():GetFFlag("ChinaLicensingApp") then + expect(state.promptState).to.equal(PromptState.RobuxUpsell) + end + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveSubscriptionPromptState.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveSubscriptionPromptState.lua new file mode 100644 index 0000000..fd38826 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveSubscriptionPromptState.lua @@ -0,0 +1,64 @@ +local Root = script.Parent.Parent +local UserInputService = game:GetService("UserInputService") + +local SetPromptState = require(Root.Actions.SetPromptState) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local ProductInfoReceived = require(Root.Actions.ProductInfoReceived) +local AccountInfoReceived = require(Root.Actions.AccountInfoReceived) +local PromptState = require(Root.Enums.PromptState) +local PurchaseError = require(Root.Enums.PurchaseError) +local UpsellFlow = require(Root.Enums.UpsellFlow) +local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow) +local PromptNativeUpsell = require(Root.Actions.PromptNativeUpsell) +local selectRobuxProduct = require(Root.NativeUpsell.selectRobuxProduct) +local Thunk = require(Root.Thunk) + + +local function resolveSubscriptionPromptState(productInfo, accountInfo, alreadyOwned) + return Thunk.new(script.Name, {}, function(store, services) + store:dispatch(ProductInfoReceived(productInfo)) + store:dispatch(AccountInfoReceived(accountInfo)) + + if alreadyOwned then + return store:dispatch(ErrorOccurred(PurchaseError.AlreadyOwn)) + end + + if not productInfo.IsForSale then + return store:dispatch(ErrorOccurred(PurchaseError.NotForSale)) + end + + local price = productInfo.PriceInRobux or 0 + local platform = UserInputService:GetPlatform() + local upsellFlow = getUpsellFlow(platform) + + if price > accountInfo.RobuxBalance then + if upsellFlow == UpsellFlow.Unavailable then + return store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobuxNoUpsell)) + end + + if upsellFlow == UpsellFlow.Web then + return store:dispatch(SetPromptState(PromptState.RobuxUpsell)) + else + local neededRobux = price - accountInfo.RobuxBalance + local hasMembership = accountInfo.MembershipType > 0 + + return selectRobuxProduct(platform, neededRobux, hasMembership) + :andThen(function(product) + -- We found a valid upsell product for the current platform + store:dispatch(PromptNativeUpsell(product.productId, product.robuxValue)) + end, function() + -- No upsell item will provide sufficient funds to make this purchase + if platform == Enum.Platform.XBoxOne then + store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobuxXbox)) + else + store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobux)) + end + end) + end + end + + return store:dispatch(SetPromptState(PromptState.PromptPurchase)) + end) +end + +return resolveSubscriptionPromptState diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveSubscriptionPromptState.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveSubscriptionPromptState.spec.lua new file mode 100644 index 0000000..3276020 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveSubscriptionPromptState.spec.lua @@ -0,0 +1,112 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + local GetFFlagDeveloperSubscriptionsEnabled = require(Root.Flags.GetFFlagDeveloperSubscriptionsEnabled) + + local resolveSubscriptionPromptState = require(script.Parent.resolveSubscriptionPromptState) + + if not GetFFlagDeveloperSubscriptionsEnabled() then + return + end + + local function getTestProductInfo() + return { + IsForSale = true, + Name = "Test Product", + PriceInRobux = 10, + MinimumMembershipLevel = 0, + } + end + + it("should populate store with provided info", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolveSubscriptionPromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.productInfo.name).to.be.ok() + expect(state.accountInfo.balance).to.be.ok() + end) + + it("should resolve state to Error if prerequisites are failed", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + -- Set product to not for sale + productInfo.IsForSale = false + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolveSubscriptionPromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.Error) + end) + + it("should resolve state to PromptPurchase if account meets requirements", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolveSubscriptionPromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + if not settings():GetFFlag("ChinaLicensingApp") then + expect(state.promptState).to.equal(PromptState.PromptPurchase) + end + end) + + it("should resolve state to RobuxUpsell if account is short on Robux", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + -- Player will not have enough robux + local accountInfo = { + RobuxBalance = 0, + MembershipType = 0, + } + local thunk = resolveSubscriptionPromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + if not settings():GetFFlag("ChinaLicensingApp") then + expect(state.promptState).to.equal(PromptState.RobuxUpsell) + end + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/retryAfterUpsell.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/retryAfterUpsell.lua new file mode 100644 index 0000000..8758b18 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/retryAfterUpsell.lua @@ -0,0 +1,75 @@ +local Root = script.Parent.Parent + +local AccountInfoReceived = require(Root.Actions.AccountInfoReceived) +local PurchaseCompleteRecieved = require(Root.Actions.PurchaseCompleteRecieved) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local PurchaseError = require(Root.Enums.PurchaseError) +local PromptState = require(Root.Enums.PromptState) +local RequestType = require(Root.Enums.RequestType) +local getAccountInfo = require(Root.Network.getAccountInfo) +local Network = require(Root.Services.Network) +local ExternalSettings = require(Root.Services.ExternalSettings) +local completeRequest = require(Root.Thunks.completeRequest) +local getPlayerPrice = require(Root.Utils.getPlayerPrice) +local Thunk = require(Root.Thunk) + +local purchaseItem = require(script.Parent.purchaseItem) + +local MAX_RETRIES = 3 +local RETRY_RATE = 1 + +local requiredServices = { + Network, + ExternalSettings, +} + +local function retryAfterUpsell(retriesRemaining) + retriesRemaining = retriesRemaining or MAX_RETRIES + + return Thunk.new(script.Name, requiredServices, function(store, services) + local network = services[Network] + local externalSettings = services[ExternalSettings] + local state = store:getState() + local requestType = state.promptRequest.requestType + local promptState = state.promptState + + if requestType == RequestType.Premium then + if promptState == PromptState.UpsellInProgress then + store:dispatch(PurchaseCompleteRecieved()) + store:dispatch(completeRequest()) + end + else + return getAccountInfo(network, externalSettings) + :andThen(function(accountInfo) + local state = store:getState() + + local isPlayerPremium = state.accountInfo.membershipType == 4 + local price = getPlayerPrice(state.productInfo, isPlayerPremium) + + local balance = accountInfo.RobuxBalance + + store:dispatch(AccountInfoReceived(accountInfo)) + + if price ~= nil and price > balance then + if retriesRemaining > 0 then + -- Upsell result may not yet have propagated, so we need to + -- wait a while and try again + delay(RETRY_RATE, function() + store:dispatch(retryAfterUpsell(retriesRemaining - 1)) + end) + else + store:dispatch(ErrorOccurred(PurchaseError.InvalidFunds)) + end + else + -- Upsell was successful and purchase can now be completed + store:dispatch(purchaseItem()) + end + end) + :catch(function(error) + store:dispatch(ErrorOccurred(error)) + end) + end + end) +end + +return retryAfterUpsell diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/retryAfterUpsell.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/retryAfterUpsell.spec.lua new file mode 100644 index 0000000..3baaf3e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/retryAfterUpsell.spec.lua @@ -0,0 +1,43 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local Reducer = require(Root.Reducers.Reducer) + local Network = require(Root.Services.Network) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockNetwork = require(Root.Test.MockNetwork) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + + local retryAfterUpsell = require(script.Parent.retryAfterUpsell) + + it("should run without errors", function() + local store = Rodux.Store.new(Reducer, { + productInfo = { + price = 0, + membershipTypeRequired = 0, + } + }) + + local thunk = retryAfterUpsell() + local network = MockNetwork.new() + local externalSettings = MockExternalSettings.new(true, false, {}) + + Thunk.test(thunk, store, { + [Network] = network, + [ExternalSettings] = externalSettings, + }) + + local state = store:getState() + local accountInfo + network.getAccountInfo():andThen(function(result) + accountInfo = result + end) + + -- Account info should be re-populated + expect(state.accountInfo.balance).to.be.equal(accountInfo.RobuxBalance) + expect(state.accountInfo.membershipType).to.be.equal(accountInfo.MembershipType) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/ClickScamDetector.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/ClickScamDetector.lua new file mode 100644 index 0000000..be79f05 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/ClickScamDetector.lua @@ -0,0 +1,108 @@ +local Root = script.Parent.Parent +local UserInputService = game:GetService("UserInputService") + +local LuaPackages = Root.Parent +local Cryo = require(LuaPackages.Cryo) + +--[[ + CLILUACORE-318: Revisit this approach and evaluate if it adequately + addresses existing or future scams +]] +local ClickScamDetector = {} +ClickScamDetector.__index = ClickScamDetector + +--[[ + Create a new scam detector with the specified options + + This object will track any clicks or confirm button presses that occur + during its lifetime and attempt to determine whether the player is + being asked to spam clicks or button presses. When processing an action, + users of this object can abort their behavior if calling isClickValid + returns false. +]] +function ClickScamDetector.new(options) + --[[ + Overwrite default values with provided options + ]] + options = Cryo.Dictionary.join({ + -- The number of clicks within the window that is interpreted as a scam + clickSpeedThreshold = 3, + -- The window in which to measure clicks + clickTimeWindow = 1, + -- A delay to allow clicks to stack up and prevent immediate clicks + initialDelay = 1, + -- Allow a button input to be associated with this click detection + buttonInput = nil, + }, options or {}) + + local self = { + _inputConnection = nil, + + _clickCount = 0, + _startTime = tick(), + _options = options, + } + + setmetatable(self, ClickScamDetector) + + self._inputConnection = UserInputService.InputBegan:Connect(function(input) + self:_onInput(input) + end) + + return self +end + +--[[ + Track mouse inputs, counting the number that occurred in the last + CLICK_TIME_WINDOW duration + + Includes touch inputs and pressing the A button on a gamepad +]] +function ClickScamDetector:_onInput(input) + local inputType = input.UserInputType + + local isGamepad = self._options.buttonInput ~= nil and input.KeyCode == self._options.buttonInput + + local isMouseOrTouch = inputType == Enum.UserInputType.MouseButton1 + or inputType == Enum.UserInputType.Touch + + if isGamepad or isMouseOrTouch then + self._clickCount = self._clickCount + 1 + + delay(self._options.clickTimeWindow, function() + self._clickCount = self._clickCount - 1 + end) + end +end + +--[[ + Determine whether or not there's a possibility of a scam occurring + and return whether or not we believe the click to be valid +]] +function ClickScamDetector:isClickValid() + --[[ + If the mouse behavior is locked by dev-facing APIs, clicks are not valid + ]] + if UserInputService.MouseBehavior == Enum.MouseBehavior.LockCurrentPosition then + return false + end + + --[[ + Don't allow any clicks until the initial delay has passed; + ]] + if tick() - self._startTime < self._options.initialDelay then + return false + end + + return self._clickCount / self._options.clickTimeWindow < self._options.clickSpeedThreshold +end + +--[[ + Cleanup connection to InputService; should be called when + the UI element using this object is destroyed +]] +function ClickScamDetector:destroy() + self._inputConnection:Disconnect() +end + +return ClickScamDetector \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/ClickScamDetector.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/ClickScamDetector.spec.lua new file mode 100644 index 0000000..b09626b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/ClickScamDetector.spec.lua @@ -0,0 +1,33 @@ +return function() + local UserInputService = game:GetService("UserInputService") + + local ClickScamDetector = require(script.Parent.ClickScamDetector) + + -- We need better ways to fake time passing, so that we can test further functionality; + -- May want to indirect the `tick` function in the ClickScamDetector and allow overriding + + it("should always report a scam if the mouse is locked", function() + local clickScamDetector = ClickScamDetector.new() + + -- No clicks have been fired + UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition + expect(clickScamDetector:isClickValid()).to.equal(false) + + UserInputService.MouseBehavior = Enum.MouseBehavior.Default + clickScamDetector:destroy() + end) + + it("should report a scam if there are many clicks in quick succession", function() + local clickScamDetector = ClickScamDetector.new() + + local fakeInput = { + UserInputType = Enum.UserInputType.MouseButton1, + } + clickScamDetector:_onInput(fakeInput) + clickScamDetector:_onInput(fakeInput) + clickScamDetector:_onInput(fakeInput) + expect(clickScamDetector:isClickValid()).to.equal(false) + + clickScamDetector:destroy() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerPrice.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerPrice.lua new file mode 100644 index 0000000..8edee6d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerPrice.lua @@ -0,0 +1,20 @@ +local Root = script.Parent.Parent + +local GetFFlagIGPPPremiumPrice = require(Root.Flags.GetFFlagIGPPPremiumPrice) + +-- Used on the data in the state +return function(productInfo, isPlayerPremium) + if GetFFlagIGPPPremiumPrice() then + if isPlayerPremium then + if productInfo.premiumPrice ~= nil then + return productInfo.premiumPrice + else + return productInfo.price + end + else + return productInfo.price + end + else + return productInfo.price + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerPrice.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerPrice.spec.lua new file mode 100644 index 0000000..f512cdd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerPrice.spec.lua @@ -0,0 +1,33 @@ +return function() + local Root = script.Parent.Parent + + local GetFFlagIGPPPremiumPrice = require(Root.Flags.GetFFlagIGPPPremiumPrice) + + local getPlayerPrice = require(script.Parent.getPlayerPrice) + + it("should return correct sale price when not premium", function() + local productInfo = { + price = 5, + premiumPrice = 10, + } + + local price = getPlayerPrice(productInfo, false) + + expect(price).to.equal(5) + end) + + it("should return correct sale price when premium", function() + local productInfo = { + price = 5, + premiumPrice = 10, + } + + local price = getPlayerPrice(productInfo, true) + + if GetFFlagIGPPPremiumPrice() then + expect(price).to.equal(10) + else + expect(price).to.equal(5) + end + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerProductInfoPrice.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerProductInfoPrice.lua new file mode 100644 index 0000000..df1a4e7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerProductInfoPrice.lua @@ -0,0 +1,21 @@ +local Root = script.Parent.Parent + +local GetFFlagIGPPPremiumPrice = require(Root.Flags.GetFFlagIGPPPremiumPrice) + +-- Used on the data directly from the endpoint +-- TODO: Consolidate this with the other function +return function(productInfo, isPlayerPremium) + if GetFFlagIGPPPremiumPrice() then + if isPlayerPremium then + if productInfo.PremiumPriceInRobux ~= nil then + return productInfo.PremiumPriceInRobux + else + return productInfo.PriceInRobux or 0 + end + else + return productInfo.PriceInRobux or 0 + end + else + return productInfo.PriceInRobux or 0 + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerProductInfoPrice.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerProductInfoPrice.spec.lua new file mode 100644 index 0000000..b8098ef --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerProductInfoPrice.spec.lua @@ -0,0 +1,33 @@ +return function() + local Root = script.Parent.Parent + + local GetFFlagIGPPPremiumPrice = require(Root.Flags.GetFFlagIGPPPremiumPrice) + + local getPlayerProductInfoPrice = require(script.Parent.getPlayerProductInfoPrice) + + it("should return correct sale price when not premium", function() + local productInfo = { + PriceInRobux = 5, + PremiumPriceInRobux = 10, + } + + local price = getPlayerProductInfoPrice(productInfo, false) + + expect(price).to.equal(5) + end) + + it("should return correct sale price when premium", function() + local productInfo = { + PriceInRobux = 5, + PremiumPriceInRobux = 10, + } + + local price = getPlayerProductInfoPrice(productInfo, true) + + if GetFFlagIGPPPremiumPrice() then + expect(price).to.equal(10) + else + expect(price).to.equal(5) + end + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/hasPendingRequest.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/hasPendingRequest.lua new file mode 100644 index 0000000..b374441 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/hasPendingRequest.lua @@ -0,0 +1,7 @@ +local Root = script.Parent.Parent + +local RequestType = require(Root.Enums.RequestType) + +return function(state) + return state.promptRequest.requestType ~= RequestType.None +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/isMockingPurchases.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/isMockingPurchases.lua new file mode 100644 index 0000000..8e4cf69 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/isMockingPurchases.lua @@ -0,0 +1,12 @@ +local RunService = game:GetService("RunService") + +--[[ + CLILUACORE-314: This should be something we get from MarketplaceService, + so that we'll always be in sync w/ the engine about whether or + not we're mocking purchases +]] +local function isMockingPurchases() + return RunService:IsStudio() +end + +return isMockingPurchases \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/meetsPrerequisites.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/meetsPrerequisites.lua new file mode 100644 index 0000000..ab216b6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/meetsPrerequisites.lua @@ -0,0 +1,84 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") +local Workspace = game:GetService("Workspace") + +local PurchaseError = require(Root.Enums.PurchaseError) + +local GetFFlagLuaPremiumCatalogIGPP = require(Root.Flags.GetFFlagLuaPremiumCatalogIGPP) + +local CONTENT_RATING_13_PLUS = 1 +local ROBLOX_CREATOR = 1 +local DEVELOPER_PRODUCT_TYPE = "Developer Product" + +local THIRD_PARTY_WARNING = "AllowThirdPartySales has blocked the purchase" + .. " prompt for %d created by %d. To sell this asset made by a" + .. " different %s, you will need to enable AllowThirdPartySales." + +local function meetsPrerequisites(productInfo, alreadyOwned, restrictThirdParty, externalSettings) + if alreadyOwned then + return false, PurchaseError.AlreadyOwn + end + + if not (productInfo.IsForSale or productInfo.IsPublicDomain) then + return false, PurchaseError.NotForSale + end + + if productInfo.IsLimited or productInfo.IsLimitedUnique then + if productInfo.Remaining == nil or productInfo.Remaining == 0 then + return false, PurchaseError.Limited + end + end + + if GetFFlagLuaPremiumCatalogIGPP() and productInfo.MinimumMembershipLevel == Enum.MembershipType.Premium.Value + and Players.LocalPlayer.MembershipType ~= Enum.MembershipType.Premium then + return false, PurchaseError.PremiumOnly + end + + if productInfo.ContentRatingTypeId == CONTENT_RATING_13_PLUS and Players.LocalPlayer:GetUnder13() then + return false, PurchaseError.Under13 + end + + local allowThirdPartyPurchase = true + if externalSettings.getLuaUseThirdPartyPermissions() then + -- Use Q2 2020 universe-wide permission to restrict access. + allowThirdPartyPurchase = externalSettings.isThirdPartyPurchaseAllowed() + else + -- TODO(DEVTOOLS-4227): Need to remove here before removing AllowThirdPartySales from DataModel/Workspace. + allowThirdPartyPurchase = Workspace.AllowThirdPartySales + end + + -- Restricting third party sales is only valid for Assets and Game Passes + if productInfo.ProductType ~= DEVELOPER_PRODUCT_TYPE + and restrictThirdParty + and not allowThirdPartyPurchase + then + local isGroupGame = game.CreatorType == Enum.CreatorType.Group + local isGroupAsset = productInfo.Creator.CreatorType == "Group" + local productCreator = tonumber(productInfo.Creator.CreatorTargetId) + + --[[ + Third party sales will be restricted if the creator of the asset + is not the creator of this game, whether the creators be users or groups + ]] + if productCreator ~= ROBLOX_CREATOR + and (isGroupGame ~= isGroupAsset or productCreator ~= game.CreatorId) + then + --[[ + Typically we avoid messaging the console for core scripts, but + in this case we want to inform developers why their game isn't + allowing sales while they're testing. + ]] + warn((THIRD_PARTY_WARNING):format( + productInfo.AssetId, + productCreator, + isGroupGame and "group" or "user" + )) + return false, PurchaseError.ThirdPartyDisabled + end + end + + -- No failed prerequisites + return true, nil +end + +return meetsPrerequisites diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/meetsPrerequisites.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/meetsPrerequisites.spec.lua new file mode 100644 index 0000000..d9a7cbe --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/meetsPrerequisites.spec.lua @@ -0,0 +1,140 @@ +return function() + local Root = script.Parent.Parent + local Workspace = game:GetService("Workspace") + + local PurchaseError = require(Root.Enums.PurchaseError) + + local GetFFlagLuaPremiumCatalogIGPP = require(Root.Flags.GetFFlagLuaPremiumCatalogIGPP) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + + local meetsPrerequisites = require(script.Parent.meetsPrerequisites) + local defaultExternalSettings = MockExternalSettings.new(false, false, {}) + + local function getValidProductInfo() + return { + IsForSale = true, + IsPublicDomain = true, + IsLimited = false, + ContentRatingTypeId = 0, + AssetId = 0, + -- Assets have ProductType "User Product" + ProductType = "User Product", + Creator = { + CreatorType = "User", + CreatorTargetId = 1, + }, + } + end + + it("should return true if prerequisites are all met", function() + local productInfo = getValidProductInfo() + local met, _ = meetsPrerequisites(productInfo, false, false, defaultExternalSettings) + + expect(met).to.equal(true) + end) + + it("should return true if third party restrictions do not apply", function() + local productInfo = getValidProductInfo() + Workspace.AllowThirdPartySales = false + + -- Set creator id to game's creator id + productInfo.Creator.CreatorTargetId = game.CreatorId + + local met, _ = meetsPrerequisites(productInfo, false, true, defaultExternalSettings) + + expect(met).to.equal(true) + end) + + it("should return false if the player owns the item already", function() + local productInfo = getValidProductInfo() + local met, errorReason = meetsPrerequisites(productInfo, true, true, defaultExternalSettings) + + expect(met).to.equal(false) + expect(errorReason).to.equal(PurchaseError.AlreadyOwn) + end) + + it("should return false if the product is not for sale", function() + local productInfo = getValidProductInfo() + productInfo.IsForSale = false + productInfo.IsPublicDomain = false + + local met, errorReason = meetsPrerequisites(productInfo, false, false, defaultExternalSettings) + + expect(met).to.equal(false) + expect(errorReason).to.equal(PurchaseError.NotForSale) + end) + + it("should return false if no copies are available", function() + local productInfo = getValidProductInfo() + productInfo.IsLimited = true + productInfo.Remaining = 0 + + local met, errorReason = meetsPrerequisites(productInfo, false, false, defaultExternalSettings) + + expect(met).to.equal(false) + expect(errorReason).to.equal(PurchaseError.Limited) + end) + + it("should return false if third-party sales are restricted by permissions service", function() + local productInfo = getValidProductInfo() + + -- Set product creator to a number that is ~= to game.CreatorId and is > 1 + -- (which is considered ROBLOX, and is never restricted) + productInfo.Creator.CreatorTargetId = game.CreatorId + 2 + + local externalSettings = MockExternalSettings.new(false, false, { + LuaUseThirdPartyPermissions = true, + PermissionsServiceIsThirdPartyPurchaseAllowed = false, + }) + local met, errorReason = meetsPrerequisites(productInfo, false, true, externalSettings) + + expect(met).to.equal(false) + expect(errorReason).to.equal(PurchaseError.ThirdPartyDisabled) + end) + + it("should return true if third-party sales are allowed by permissions service", function() + local productInfo = getValidProductInfo() + + -- Set product creator to a number that is ~= to game.CreatorId and is > 1 + -- (which is considered ROBLOX, and is never restricted) + productInfo.Creator.CreatorTargetId = game.CreatorId + 2 + + local externalSettings = MockExternalSettings.new(false, false, { + LuaUseThirdPartyPermissions = true, + PermissionsServiceIsThirdPartyPurchaseAllowed = true, + }) + local met, errorReason = meetsPrerequisites(productInfo, false, true, externalSettings) + + expect(met).to.equal(true) + end) + + it("should return false if third-party sales are restricted", function() + local productInfo = getValidProductInfo() + Workspace.AllowThirdPartySales = false + + -- Set product creator to a number that is ~= to game.CreatorId and is > 1 + -- (which is considered ROBLOX, and is never restricted) + productInfo.Creator.CreatorTargetId = game.CreatorId + 2 + + local externalSettings = MockExternalSettings.new(false, false, { + LuaUseThirdPartyPermissions = false, + RestrictSales2 = true, + }) + local met, errorReason = meetsPrerequisites(productInfo, false, true, externalSettings) + + expect(met).to.equal(false) + expect(errorReason).to.equal(PurchaseError.ThirdPartyDisabled) + end) + + if GetFFlagLuaPremiumCatalogIGPP() then + it("should return false if premium purchase", function() + local productInfo = getValidProductInfo() + productInfo.MinimumMembershipLevel = 4 + + local met, errorReason = meetsPrerequisites(productInfo, false, true, defaultExternalSettings) + + expect(met).to.equal(false) + expect(errorReason).to.equal(PurchaseError.PremiumOnly) + end) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/connectToStore.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/connectToStore.lua new file mode 100644 index 0000000..60560b6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/connectToStore.lua @@ -0,0 +1,26 @@ +--[[ + Small wrapper for RoactRodux's connect function that + additionally exposes the original, unconnected component + for testing +]] +local Root = script.Parent + +local LuaPackages = Root.Parent +local RoactRodux = require(LuaPackages.RoactRodux) + +local function connectToStore(mapStateToProps, mapDispatchToProps) + return function(innerComponent) + local connectedComponent = RoactRodux.UNSTABLE_connect2( + mapStateToProps, + mapDispatchToProps + )(innerComponent) + + function connectedComponent.getUnconnected() + return innerComponent + end + + return connectedComponent + end +end + +return connectToStore \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/getPreviewImageUrl.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/getPreviewImageUrl.lua new file mode 100644 index 0000000..8d6b3e9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/getPreviewImageUrl.lua @@ -0,0 +1,32 @@ +local Root = script.Parent +local ContentProvider = game:GetService("ContentProvider") +local ItemType = require(Root.Enums.ItemType) + +local BASE_URL = string.gsub(ContentProvider.BaseUrl:lower(), "https?://m.", "https?://www.") +local THUMBNAIL_URL = BASE_URL.."thumbs/asset.ashx?assetid=" +local BUNDLE_THUMBNAIL_URL = BASE_URL.."outfit-thumbnail/image?userOutfitId=%s&width=100&height=100&format=png" + +local XBOX_DEFAULT_IMAGE = "rbxasset://textures/ui/Shell/Icons/ROBUXIcon@1080.png" + +--[[ + Depending on the type of item, get the proper preview image, sized correctly +]] +local function getPreviewImageUrl(productInfo, platform) + local imageId + + -- AssetId will only be populated if ProductInfo was from an asset + if productInfo.itemType == ItemType.Bundle then + return string.format(BUNDLE_THUMBNAIL_URL, productInfo.costumeId) + elseif productInfo.AssetId ~= nil and productInfo.AssetId ~= 0 then + imageId = productInfo.AssetId + elseif productInfo.IconImageAssetId ~= nil then + imageId = productInfo.IconImageAssetId + elseif platform == Enum.Platform.XBoxOne then + -- XBoxOne has its own default image if anything doesn't load + return XBOX_DEFAULT_IMAGE + end + + return THUMBNAIL_URL..tostring(imageId).."&x=100&y=100&format=png" +end + +return getPreviewImageUrl \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/init.lua new file mode 100644 index 0000000..86f18aa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/init.lua @@ -0,0 +1,22 @@ +local Root = script +local LuaPackages = Root.Parent +local CoreGui = game:GetService("CoreGui") +local RunService = game:GetService("RunService") + +local Roact = require(LuaPackages.Roact) + +local PurchasePromptApp = require(script.Components.PurchasePromptApp) + +local function mountPurchasePrompt() + if RunService:IsStudio() and RunService:IsEdit() then + return nil + end + + local handle = Roact.mount(Roact.createElement(PurchasePromptApp), CoreGui, "PurchasePromptApp") + + return handle +end + +return { + mountPurchasePrompt = mountPurchasePrompt, +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/strict.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/strict.lua new file mode 100644 index 0000000..f23357b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/strict.lua @@ -0,0 +1,27 @@ +--[[ + Locks a table from indexing or setting any keys that are not already defined + + Useful for constants or any unchanging data, where indexing non-existent values + is always a mistake +]] +local function invalidKey(self, key) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + tostring(self) + ) + + error(message, 2) +end + +local function strict(t, name) + return setmetatable(t, { + __index = invalidKey, + __newindex = invalidKey, + __tostring = function() + return name + end, + }) +end + +return strict \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/strict.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/strict.spec.lua new file mode 100644 index 0000000..5e0e7ad --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/strict.spec.lua @@ -0,0 +1,30 @@ +return function() + local strict = require(script.Parent.strict) + + it("should produce a table that throws errors when indexing invalid keys", function() + local object = strict({ + x = 1, + y = 3, + }, "object") + + expect(function() + print(object.z) + end).to.throw() + expect(function() + object.z = 1 + end).to.throw() + + expect(function() + object.x = 2 + end).never.to.throw() + end) + + it("should return the given name with the resulting table's tostring", function() + local object = strict({ + x = 1, + y = 3, + }, "object") + + expect(tostring(object)).to.equal("object") + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/t.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/lock.toml new file mode 100644 index 0000000..b317d20 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/rhodium" +version = "0.2.5" +commit = "7e623fc1d95af129f8fe9ca959160969e469e2ef" +source = "url+https://github.com/roblox/rhodium" diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/Element.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/Element.lua new file mode 100644 index 0000000..e9e083c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/Element.lua @@ -0,0 +1,272 @@ +local VirtualInput = require(script.Parent.VirtualInput) +local XPath = require(script.Parent.XPath) + +local Element = {} +Element.__index = Element + +function Element.new(argument) + local self = {} + if type(argument) == "string" then + self.path = XPath.new(argument) + elseif type(argument) == "table" and argument.__type == "XPath" then + self.path = argument + elseif type(argument) == "userdata" then + self.path = XPath.new(argument) + self.rbxInstance = argument + else + error("invalid parameter for element") + end + + setmetatable(self, Element) + local scrollNums = self:_scrollingFrames(self.rbxInstance) + + self.isInScrollingFrame = scrollNums ~= 0 + + return self +end + +function Element:getAttribute(name) + return self:getRbxInstance()[name] +end + +function Element:getLocation() + return self:getRbxInstance().AbsolutePosition +end + +function Element:getRect() + local topLeft = self:getLocation() + local bottomRight = self:getSize() + topLeft + return Rect.new(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y) +end + +function Element:getSize() + return self:getRbxInstance().AbsoluteSize +end + +function Element:getCenter() + return self:getLocation()+self:getSize()/2 +end + +function Element:getText() + return self:getRbxInstance().Text +end + +function Element:getAnchor() + return self:getLocation()+self.anchor +end + +-- Set anchor at offset from absolute position +function Element:setAnchor(offsetX, offsetY) + if(offsetX > self:getSize().x or offsetY > self:getSize().y or offsetX < 0 or offsetY < 0) then + error("Attempt to set anchor beyond element's bounds") + else + self.anchor = Vector2.new(offsetX, offsetY) + end +end + +function Element:isDisplayed() + return self:getRbxInstance().Visible +end + +function Element:isSelected() + return self:getRbxInstance().Selected +end + +function Element:getRbxInstance() + return self:waitForRbxInstance(self.path.waitDelay, self.path.waitTimeout) +end + +function Element:waitForRbxInstance(timeout, delay) + if self.rbxInstance == nil and self.path ~= nil then + self.path:setWait(timeout, delay) + self.rbxInstance = self.path:waitForFirstInstance() + end + + if self.rbxInstance and not self.anchor then + if pcall(function() local size = self.rbxInstance.AbsoluteSize end) then + self.anchor = self.rbxInstance.AbsoluteSize/2 + else + self.anchor = nil + end + end + + return self.rbxInstance +end + +function Element:_override(class) + for k, v in pairs(class) do + if not k:find("^_") then + self[k] = v + end + end +end + +function Element:centralizeInstance() + self:_centralizeInScrollingFrame(self:getRbxInstance()) +end + +function Element:centralize() + local instance = self:getRbxInstance() + if instance then + self:centralizeInstance() + else + self:centralizeWithInfiniteScrolling() + end +end + +function Element:_scrollingFrames(instance) + if instance == nil or instance == game then return 0 end + local num = self:_scrollingFrames(instance.Parent) + if instance.ClassName == "ScrollingFrame" then num = num + 1 end + return num +end + +function Element:_centralizeInScrollingFrame(child, parent) + if child == game then return end + parent = parent or child.Parent + if parent == game then return end + + if parent.ClassName == "ScrollingFrame" then + self:_centralizeInScrollingFrame(parent, parent.Parent) + -- this is computational error tolerate. + local threshold = 2 + + --first scroll down to make child appears neas screen, + --so that we can access child.AbsolutPosition property + local isChildInScreen = false + while not isChildInScreen do + + local prevChildPosition = child.AbsolutePosition + local prevCanvasPosition = parent.CanvasPosition + -- when scroll too much at one time, the element may move out side of screen immediately + -- its AbsoluteSize will not update. limit to 300 + local scrollDistance = Vector2.new(math.min(300, parent.AbsoluteSize.X), math.min(300, parent.AbsoluteSize.Y)) + parent.CanvasPosition = parent.CanvasPosition + scrollDistance + wait() + local deltaCanvas = (parent.CanvasPosition - prevCanvasPosition) + local isBottom = deltaCanvas.Magnitude <= threshold + local deltaChild = child.AbsolutePosition - prevChildPosition + isChildInScreen = isBottom or deltaChild.Magnitude > threshold + end + --second scroll to centerize the child, at most twice. + for _ = 1, 2 do + local frameCenter = parent.AbsolutePosition + parent.AbsoluteSize/2 + local childCenter = child.AbsolutePosition + child.AbsoluteSize/2 + local delta = childCenter - frameCenter + if delta.Magnitude <= threshold then break end + parent.CanvasPosition = parent.CanvasPosition + delta + wait() + end + else + self:_centralizeInScrollingFrame(child, parent.Parent) + end +end + +function Element:_scrollToFindInstance(scrollingFrame, absPath) +-- first reset scrollingFrame to zero position + scrollingFrame.CanvasPosition = Vector2.new(0, 0) + local width = scrollingFrame.AbsoluteSize.X + local height = scrollingFrame.AbsoluteSize.Y + + local isBottom = false + local instance + local threshold = 2 + while not isBottom do + wait(0.1) + --if find the element then return + instance = absPath:getFirstInstance() + if instance then return instance end + --scroll + local oldPosition = scrollingFrame.CanvasPosition + scrollingFrame.CanvasPosition = scrollingFrame.CanvasPosition + + Vector2.new(math.min(width, 300), math.min(height, 300)) + --wait for content to refresh + local delta = scrollingFrame.CanvasPosition - oldPosition + isBottom = delta.Magnitude < threshold + --if it is the bottom, then return not found + end + return nil +end + +function Element:centralizeWithInfiniteScrolling() + local instances, lastSeenIndex = self.path:getInstances() + if #instances > 0 then self:centralizeInstance() end + + local lastSeenPath = self.path:copy() + while #lastSeenPath.data > lastSeenIndex do + table.remove(lastSeenPath.data) + end + + local lastSeenInstance = lastSeenPath:getFirstInstance() + local lastScrollingFrame = nil + while true do + if lastSeenInstance.ClassName == "ScrollingFrame" then + lastScrollingFrame = lastSeenInstance + break + end + lastSeenInstance = lastSeenInstance.Parent + if lastSeenInstance == game then break end + end + if lastScrollingFrame == nil then return end + if self:_scrollToFindInstance(lastScrollingFrame, self.path) == nil then return end + self:_centralizeInScrollingFrame(self:getRbxInstance()) +end + +function Element:setPluginWindow() + local window = self.rbxInstance:FindFirstAncestorOfClass("DockWidgetPluginGui") + VirtualInput.setCurrentWindow(window) +end + +function Element:click(repeatCount) + self:centralize() + self:setPluginWindow() + + repeatCount = repeatCount or 1 + VirtualInput.Mouse.multiClick(self:getAnchor(), repeatCount) +end + +function Element:rightClick() + self:centralize() + self:setPluginWindow() + + VirtualInput.Mouse.rightClick(self:getAnchor()) +end + +function Element:mouseWheel(num) + self:centralize() + VirtualInput.Mouse.mouseWheel(self:getAnchor(), num) +end + +function Element:mouseDrag(xOffset, yOffset, duration) + self:centralize() + local posTo = self:getAnchor() + Vector2.new(xOffset, yOffset) + VirtualInput.Mouse.mouseDrag(self:getAnchor(), posTo, duration, true) +end + +function Element:mouseDragTo(posTo, duration) + self:centralize() + VirtualInput.Mouse.mouseDrag(self:getAnchor(), posTo, duration, true) +end + +function Element:sendKey(key) + self:setPluginWindow() + VirtualInput.Keyboard.hitKey(key) +end + +function Element:sendText(str) + self:click() + wait(0) + VirtualInput.Text.sendText(str) +end + +function Element:tap() + self:centralize() + VirtualInput.Touch.tap(self:getAnchor()) +end + +function Element:touchScroll(xOffset, yOffset, duration, multitouchId) + self:centralize() + VirtualInput.Touch.touchScroll(self:getAnchor(), xOffset, yOffset, duration, true, multitouchId) +end + +return Element diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/Element.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/Element.spec.lua new file mode 100644 index 0000000..9068103 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/Element.spec.lua @@ -0,0 +1,46 @@ +return function() + local XPath = require(script.Parent.XPath) + local Element = require(script.Parent.Element) + + local function makeInstance(className, props, children) + local instance = Instance.new(className) + if children then + for _, child in ipairs(children) do + child.Parent = instance + end + end + if props then + for k, v in pairs(props) do + instance[k] = v + end + end + return instance + end + + local root = makeInstance("Folder", + { + Name = "root", + Parent = workspace + }, { + makeInstance("Frame", {Name = "Frame"}, { + makeInstance("TextLabel", { + Text = "Label1" + }), + }), + }) + + describe("element creation", function() + it("valid element creation", function() + local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame]") + local validElement = Element.new(path:cat(XPath.new("TextLabel"))) + + expect(validElement:getRbxInstance()).to.be.ok() + end) + it("invalid element creation", function() + local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame]") + local invalidElement = Element.new(path:cat(XPath.new("TextLabel2"))) + + expect(invalidElement:getRbxInstance()).to.never.be.ok() + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/GamePad.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/GamePad.lua new file mode 100644 index 0000000..412f16c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/GamePad.lua @@ -0,0 +1,105 @@ +local VirtualInputUtils = require(script.Parent.Parent.VirtualInputUtils) + +local VirtualInputManager = game:GetService("VirtualInputManager") + +local GamePad = {} +GamePad.__index = GamePad +local gamePadDeviceId = 123 + +GamePad.KeyCode = { + ButtonX = Enum.KeyCode.ButtonX, + ButtonY = Enum.KeyCode.ButtonY, + ButtonA = Enum.KeyCode.ButtonA, + ButtonB = Enum.KeyCode.ButtonB, + ButtonR1 = Enum.KeyCode.ButtonR1, + ButtonL1 = Enum.KeyCode.ButtonL1, + ButtonR2 = Enum.KeyCode.ButtonR2, + ButtonL2 = Enum.KeyCode.ButtonL2, + ButtonR3 = Enum.KeyCode.ButtonR3, + ButtonL3 = Enum.KeyCode.ButtonL3, + ButtonStart = Enum.KeyCode.ButtonStart, + ButtonSelect = Enum.KeyCode.ButtonSelect, + DPadLeft = Enum.KeyCode.DPadLeft, + DPadRight = Enum.KeyCode.DPadRight, + DPadUp = Enum.KeyCode.DPadUp, + DPadDown = Enum.KeyCode.DPadDown, + Thumbstick1 = Enum.KeyCode.Thumbstick1, + Thumbstick2 = Enum.KeyCode.Thumbstick2, +} + +function GamePad.new() + local self = {deviceId = gamePadDeviceId} + gamePadDeviceId = gamePadDeviceId + 1 + setmetatable(self, GamePad) + VirtualInputManager:HandleGamepadConnect(self.deviceId) + return self +end + +function GamePad:disconnect() + VirtualInputManager:HandleGamepadDisconnect(self.deviceId) +end + +function GamePad:pressButton(button) + VirtualInputManager:HandleGamepadButtonInput(self.deviceId, button, 1); +end + +function GamePad:releaseButton(button) + VirtualInputManager:HandleGamepadButtonInput(self.deviceId, button, 0); +end + +function GamePad:hitButton(button) + self:pressButton(button) + self:releaseButton(button) +end + +function GamePad:moveStickTo(stick, vec2) + VirtualInputManager:HandleGamepadAxisInput(self.deviceId, stick, vec2.x, vec2.y, 0) +end + +function GamePad:smoothMoveStickTo(stick, from, to, duration) + duration = duration or 0 + if duration == 0 then + self:moveStickTo(stick, to) + return + end + local passed = 0 + local function run(dt) + local ratio = passed / duration + passed = passed + dt + if ratio < 1 then + local pos = from + (to - from) * ratio + self:moveStickTo(stick, pos) + return false + else + self:moveStickTo(stick, to) + return true + end + end + VirtualInputUtils.__syncRun(run) +end + +function GamePad:swingStick(stick, pos, duration) + duration = duration or 0 + local origin = Vector2.new(0, 0) + self:moveStickTo(stick, origin) + self:smoothMoveStickTo(stick, origin, pos, duration / 2) + self:smoothMoveStickTo(stick, pos, origin, duration / 2) +end + +function GamePad:swingLeft(stick, duration) + self:swingStick(stick, Vector2.new(-1, 0), duration) +end + +function GamePad:swingRight(stick, duration) + self:swingStick(stick, Vector2.new(1, 0), duration) +end + +function GamePad:swingTop(stick, duration) + self:swingStick(stick, Vector2.new(0, 1), duration) +end + +function GamePad:swingDown(stick, duration) + self:swingStick(stick, Vector2.new(0, -1), duration) +end + +return GamePad \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Keyboard.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Keyboard.lua new file mode 100644 index 0000000..5f0553b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Keyboard.lua @@ -0,0 +1,24 @@ +local VirtualInputUtils = require(script.Parent.Parent.VirtualInputUtils) + +local VirtualInputManager = game:GetService("VirtualInputManager") + +local Keyboard = {} + +function Keyboard.SendKeyEvent(isPressed, keyCode, isRepeated) + VirtualInputManager:SendKeyEvent(isPressed, keyCode, isRepeated, VirtualInputUtils.getCurrentWindow()) +end + +function Keyboard.pressKey(keyCode) + Keyboard.SendKeyEvent(true, keyCode, false) +end + +function Keyboard.releaseKey(keyCode) + Keyboard.SendKeyEvent(false, keyCode, false) +end + +function Keyboard.hitKey(keyCode) + Keyboard.pressKey(keyCode) + Keyboard.releaseKey(keyCode) +end + +return Keyboard \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Mouse.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Mouse.lua new file mode 100644 index 0000000..ec821e7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Mouse.lua @@ -0,0 +1,118 @@ +local VirtualInputUtils = require(script.Parent.Parent.VirtualInputUtils) + +local VirtualInputManager = game:GetService("VirtualInputManager") +local InputVisualizer = require(script.Parent.Parent.InputVisualizer):new() + +local Mouse = {} + +function Mouse.sendMouseButtonEvent(x, y, button, isDown, repeatCount) + x, y = VirtualInputUtils.__handleGuiInset(x, y) + VirtualInputManager:SendMouseButtonEvent(x, y, button, isDown, VirtualInputUtils.getCurrentWindow(), repeatCount or 0) +end + +function Mouse.SendMouseMoveEvent(x, y) + x, y = VirtualInputUtils.__handleGuiInset(x, y) + VirtualInputManager:SendMouseMoveEvent(x, y, VirtualInputUtils.getCurrentWindow()) +end + +function Mouse.SendMouseWheelEvent(x, y, isForwardScroll) + x, y = VirtualInputUtils.__handleGuiInset(x, y) + VirtualInputManager:SendMouseWheelEvent(x, y, isForwardScroll, VirtualInputUtils.getCurrentWindow()) +end + +function Mouse.mouseWheel(vec2, num) + local forward = false + if num < 0 then + forward = true + num = -num + end + for _ = 1, num do + Mouse.SendMouseWheelEvent(vec2.x, vec2.y, forward) + end +end + +local function click(vec2, count, clickType) + InputVisualizer:click(vec2, VirtualInputUtils.getCurrentWindow()) + Mouse.sendMouseButtonEvent(vec2.x, vec2.y, clickType, true, count) + Mouse.sendMouseButtonEvent(vec2.x, vec2.y, clickType, false, count) +end + +local function multiClick(vec2, count, clickType) + local waiting = true + local repeatCount = 0 + return function() + if waiting then + waiting = false + return false + elseif count >= 1 then + click(vec2, repeatCount, clickType) + count = count - 1 + repeatCount = repeatCount + 1 + waiting = true + return false + elseif count == 0 then + return true + end + end +end + +function Mouse.click(vec2) + VirtualInputUtils.__syncRun(multiClick(vec2, 1, 0)) +end + +function Mouse.multiClick(vec2, count) + VirtualInputUtils.__syncRun(multiClick(vec2, count, 0)) +end + +function Mouse.rightClick(vec2) + VirtualInputUtils.__syncRun(multiClick(vec2, 1, 1)) +end + +function Mouse.mouseLeftDown(vec2) + Mouse.sendMouseButtonEvent(vec2.x, vec2.y, 0, true) +end + +function Mouse.mouseLeftUp(vec2) + Mouse.sendMouseButtonEvent(vec2.x, vec2.y, 0, false) +end + +function Mouse.mouseRightDown(vec2) + Mouse.sendMouseButtonEvent(vec2.x, vec2.y, 1, true) +end + +function Mouse.mouseRightUp(vec2) + Mouse.sendMouseButtonEvent(vec2.x, vec2.y, 1, false) +end + +function Mouse.mouseMove(vec2) + Mouse.SendMouseMoveEvent(vec2.X, vec2.Y) +end + +local function drag(posFrom, posTo, duration) + local passed = 0 + local started = false + return function(dt) + if not started then + Mouse.mouseLeftDown(posFrom) + started = true + else + passed = passed + dt + if duration and passed < duration then + local percent = passed / duration + local pos = (posTo - posFrom) * percent + posFrom + Mouse.mouseMove(pos) + else + Mouse.mouseMove(posTo) + Mouse.mouseLeftUp(posTo) + return true + end + end + return false + end +end + +function Mouse.mouseDrag(posFrom, posTo, duration) + VirtualInputUtils.__syncRun(drag(posFrom, posTo, duration)) +end + +return Mouse \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Text.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Text.lua new file mode 100644 index 0000000..4a6b5b6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Text.lua @@ -0,0 +1,11 @@ +local VirtualInputUtils = require(script.Parent.Parent.VirtualInputUtils) + +local VirtualInputManager = game:GetService("VirtualInputManager") + +local Text = {} + +function Text.sendText(str) + VirtualInputManager:sendTextInputCharacterEvent(str, VirtualInputUtils.getCurrentWindow()) +end + +return Text diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Touch.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Touch.lua new file mode 100644 index 0000000..585de78 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Touch.lua @@ -0,0 +1,71 @@ +local VirtualInputUtils = require(script.Parent.Parent.VirtualInputUtils) + +local VirtualInputManager = game:GetService("VirtualInputManager") + +local Touch = {} +local defaultTouchId = 123456 + +function Touch.SendTouchEvent(touchId, state, x, y) + x, y = VirtualInputUtils.__handleGuiInset(x, y) + VirtualInputManager:SendTouchEvent(touchId, state, x, y) +end + +function Touch.touchStart(vec2, multitouchId) + local touchId = defaultTouchId + (multitouchId or 0) + Touch.SendTouchEvent(touchId, 0, vec2.x, vec2.y) +end + +function Touch.touchMove(vec2, multitouchId) + local touchId = defaultTouchId + (multitouchId or 0) + Touch.SendTouchEvent(touchId, 1, vec2.x, vec2.y) +end + +function Touch.touchStop(vec2, multitouchId) + local touchId = defaultTouchId + (multitouchId or 0) + Touch.SendTouchEvent(touchId, 2, vec2.x, vec2.y) +end + +local function smoothSwipe(posFrom, posTo, duration, multitouchId) + local passed = 0 + local started = false + local touchId = defaultTouchId + (multitouchId or 0) + return function(dt) + if not started then + Touch.touchStart(posFrom, touchId) + started = true + else + passed = passed + dt + if duration and passed < duration then + local percent = passed / duration + local pos = (posTo - posFrom) * percent + posFrom + Touch.touchMove(pos, touchId) + else + Touch.touchMove(posTo, touchId) + Touch.touchStop(posTo, touchId) + return true + end + end + return false + end +end + +function Touch.swipe(posFrom, posTo, duration, async, multitouchId) + local touchId = defaultTouchId + (multitouchId or 0) + if async == true then + VirtualInputUtils.__asyncRun(smoothSwipe(posFrom, posTo, duration, touchId)) + else + VirtualInputUtils.__syncRun(smoothSwipe(posFrom, posTo, duration, touchId)) + end +end + +function Touch.touchScroll(startPos, xOffset, yOffset, duration, async, multitouchId) + local posTo = startPos + Vector2.new(xOffset, yOffset) + Touch.swipe(startPos, posTo, duration, async, multitouchId) +end + +function Touch.tap(vec2) + Touch.touchStart(vec2) + Touch.touchStop(vec2) +end + +return Touch \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputVisualizer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputVisualizer.lua new file mode 100644 index 0000000..7bdfec0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputVisualizer.lua @@ -0,0 +1,79 @@ +local InputVisualizer = {} +InputVisualizer.__index = InputVisualizer + +local TweenService = game:GetService("TweenService") +local Debris = game:GetService("Debris") +local UserInputService = game:GetService("UserInputService") + +function InputVisualizer.new() + local self = {} + local state, guiRoot = pcall(function() return game.CoreGui.Parent.CoreGui end) + if state == false then + local LocalPlayer = game.Players.LocalPlayer + while LocalPlayer == nil do + LocalPlayer = game.Players.LocalPlayer + wait() + end + guiRoot = LocalPlayer.PlayerGui + end + + local GuiName = "InputVisualizer" + if guiRoot:FindFirstChild(GuiName) == nil then + local screenGui = Instance.new("ScreenGui") + screenGui.Name = GuiName + screenGui.DisplayOrder = 1000000 + screenGui.Parent = guiRoot + end + guiRoot = guiRoot[GuiName] + self.guiRoot = guiRoot + + setmetatable(self, InputVisualizer) + return self +end + +function InputVisualizer:onInputBegan(input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self:click(Vector2.new(input.Position.X, input.Position.Y)) + elseif input.UserInputType == Enum.UserInputType.Touch then + self:click(Vector2.new(input.Position.X, input.Position.Y)) + end +end + +function InputVisualizer:click(vec2, pluginGui) + local delay = 0.5 + local image = nil + if image == nil then + image = Instance.new("ImageLabel") + image.Image = "rbxassetid://1549893588" + image.BackgroundTransparency = 1 + image.Parent = pluginGui or self.guiRoot + image.Size = UDim2.new(0, 20, 0, 20) + image.Name = "MouseClick" + image.ZIndex = 10 + end + + image.Visible = true + image.Position = UDim2.new(0, vec2.X-image.Size.X.Offset/2, 0, vec2.Y-image.Size.Y.Offset/2) + image.ImageTransparency = 0 + local goal = {ImageTransparency = 1} + local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false) + local tween = TweenService:Create(image, tweenInfo, goal) + tween:Play() + Debris:AddItem(image, delay) +end + +function InputVisualizer:enable() + self.handler = UserInputService.InputBegan:connect( + function(input, gameProcessed) + self:onInputBegan(input, gameProcessed) + end) +end + +function InputVisualizer:disable() + if self.handler then + self.handler:Disconnect() + end + self.handler = nil +end + +return InputVisualizer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/RemoteRhodium.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/RemoteRhodium.lua new file mode 100644 index 0000000..3cc4488 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/RemoteRhodium.lua @@ -0,0 +1,76 @@ +local RemoteRhodium = {} +local HttpService = game:getService("HttpService") + +local rootPath = nil + +local function split(s, delimiter) + local result = {}; + while(s:len()>0) do + local pos, stop = string.find(s,delimiter,1,true) + if pos == nil then + table.insert(result,s) + s = "" + else + table.insert(result,string.sub(s,1,pos-1)) + s = string.sub(s,stop+1) + if s == "" then table.insert(result,"") end + end + end + return result +end + +function RemoteRhodium.setCommandPath(p) + rootPath = p +end + +local function onCommand(command) + assert(type(command) == "string", "command should be a string") + + local op = command:find("(", 1, true) + local cp = command:reverse():find(")", 1, true) + if cp ~= nil then + cp = #command - cp + 1 + end + + local args = {} + if op then + assert(cp, "invalid syntex, expecting \")\"") + local argStr = command:sub(op+1, cp-1) + if #argStr > 0 then + args = HttpService:JSONDecode("["..argStr.."]") + end + command = command:sub(1, op-1) + end + + local subPathTab = split(command, ".") + + local instance = rootPath + for i = 1, #subPathTab do + local p = subPathTab[i] + instance = instance[p] + if instance == nil then + error("can not find " .. p) + end + if type(instance) == "userdata" and instance.ClassName == "ModuleScript" then + instance = require(instance) + end + end + if type(instance) ~= "function" then + error("target is not a function") + end + return instance(unpack(args)) +end + +function RemoteRhodium.setCommandPath(p) + rootPath = p +end + +local success, RhodiumService = pcall(function() + return game:getService("RhodiumService") + end) + +if success then + RhodiumService.onCommand = onCommand +end + +return RemoteRhodium \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInput.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInput.lua new file mode 100644 index 0000000..37e6933 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInput.lua @@ -0,0 +1,20 @@ +local Keyboard = require(script.Parent.InputTypes.Keyboard) +local Mouse = require(script.Parent.InputTypes.Mouse) +local Touch = require(script.Parent.InputTypes.Touch) +local Text = require(script.Parent.InputTypes.Text) +local GamePad = require(script.Parent.InputTypes.GamePad) + +local VirtualInputUtils = require(script.Parent.VirtualInputUtils) + +local VirtualInput = { + Keyboard = Keyboard, + Mouse = Mouse, + Touch = Touch, + Text = Text, + GamePad = GamePad, + + setCurrentWindow = VirtualInputUtils.setCurrentWindow, + getCurrentWindow = VirtualInputUtils.getCurrentWindow, +} + +return VirtualInput \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInput.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInput.spec.lua new file mode 100644 index 0000000..06b5591 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInput.spec.lua @@ -0,0 +1,14 @@ +return function() + describe("VirtualInput", function() + it("should load", function() + local VirtualInput = require(script.Parent.VirtualInput) + + expect(VirtualInput).to.be.ok() + expect(VirtualInput.Keyboard).to.be.ok() + expect(VirtualInput.Mouse).to.be.ok() + expect(VirtualInput.Touch).to.be.ok() + expect(VirtualInput.GamePad).to.be.ok() + expect(VirtualInput.Text).to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInputUtils.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInputUtils.lua new file mode 100644 index 0000000..dddaa9d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInputUtils.lua @@ -0,0 +1,64 @@ +local RunService = game:GetService("RunService") +local GuiService = game:GetService("GuiService") + +local runSet = {} +local signals = {} + +RunService.Heartbeat:Connect(function(dt) + local finishedList = {} + for runable, _ in pairs(runSet) do + local finished = runable(dt) + if finished then + table.insert(finishedList, runable) + end + end + + for _, toDelete in ipairs(finishedList) do + runSet[toDelete] = nil + end + + for signal, _ in pairs(signals) do + if signal == false then + signals[signal] = nil + else + signal:Fire(dt) + end + end +end) + +local VirtualInputUtils = {} + +local currentWindow = nil + +function VirtualInputUtils.setCurrentWindow(window) + local old = currentWindow + currentWindow = window + return old +end + +function VirtualInputUtils.getCurrentWindow() + return currentWindow +end + +function VirtualInputUtils.__asyncRun(runable) + runSet[runable] = true +end + +function VirtualInputUtils.__syncRun(runable) + local signal = Instance.new("BindableEvent") + signals[signal] = true + local dt = 0 + while true do + local finished = runable(dt) + if finished then break end + dt = signal.Event:Wait() + end + signals[signal] = false +end + +function VirtualInputUtils.__handleGuiInset(x, y) + local guiOffset, _ = GuiService:GetGuiInset() + return x + guiOffset.X, y + guiOffset.Y +end + +return VirtualInputUtils \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/XPath.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/XPath.lua new file mode 100644 index 0000000..4bb4e45 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/XPath.lua @@ -0,0 +1,622 @@ +local XPath = {} +XPath.__index = XPath +XPath.__type = "XPath" + +local specialChars = [[\.=[],]] +local specialCharMap = {} +for i = 1, #specialChars do + local ch = specialChars:sub(i, i) + specialCharMap[ch] = true +end + +function XPath.addSlash(str) + local tab = {} + for i = 1, str:len() do + local ch = str:sub(i, i) + if specialCharMap[ch] then + table.insert(tab, "\\") + end + table.insert(tab, ch) + end + return table.concat(tab) +end + +function XPath.removeSlash(str) + local tab = {} + local isBackSlash = false + for i = 1, str:len() do + local ch = str:sub(i, i) + if ch == "\\" and isBackSlash == false then + isBackSlash = true + else + if isBackSlash == true then + isBackSlash = false + end + table.insert(tab, ch) + end + end + return table.concat(tab) +end + +local function splitByCharWithSlash(s, token) + local result = {} + if s == nil or s == "" then return result end + local isBackSlash = false + local lastIndex = 1 + for i = 1, s:len() do + local ch = s:sub(i, i) + if ch == "\\" and isBackSlash == false then + isBackSlash = true + else + if isBackSlash == true then + isBackSlash = false + else + if ch == token then + table.insert(result, (s:sub(lastIndex, i-1))) + lastIndex = i+1 + end + end + end + end + table.insert(result, (s:sub(lastIndex, s:len()))) + return result +end + +local function deepCopy(t) + local t2 = {} + for k, v in pairs(t) do + if type(v) == "table" then + t2[k] = deepCopy(v) + else + t2[k] = v + end + end + return t2 +end + +function XPath.new(obj, root) + local self = {data = {}, root = root, waitDelay = 0.2, waitTimeOut = 2} + setmetatable(self, XPath) + + if type(obj) == "string" then + self:fromString(obj) + elseif type(obj) == "userdata" then + local current = obj + while current do + local name = current.Name + if current.ClassName == "DataModel" then + name = "game" + end + table.insert(self.data, 1, {name = name}) + current = current.Parent + end + elseif getmetatable(obj).__type == XPath.__type then + return obj:copy() + else + error("unknown parameter ", obj) + end + return self +end + +function XPath:size() + return #self.data +end + +function XPath:mergeFilter(index, additionalFilter) + if index > self:size() then error("bad index") end + local filter = self.data[index].filter or {} + local filterDict = {} + for _, item in ipairs(filter) do + filterDict[item.key] = item.value + end + if additionalFilter then + for _, item in ipairs(additionalFilter) do + filterDict[item.key] = tostring(item.value) + end + end + local newFilter = {} + for k, v in pairs(filterDict) do + table.insert(newFilter, {key = k, value = v}) + end + self.data[index].filter = newFilter + return self +end + +function XPath:fromString(str) + local inBracket = false + local isBackSlash = false + local data = {} + local lastIndex = 1 + str = str .. "." + for i = 1, str:len() do + local ch = str:sub(i, i) + if ch == "\\" and isBackSlash == false then + isBackSlash = true + else + if isBackSlash == true then + isBackSlash = false + else + if ch == "." then + if not inBracket then + table.insert(data, {name = str:sub(lastIndex, i-1)}) + lastIndex = i+1 + end + elseif ch == "[" then + if inBracket == true then + error("no nested bracket allowed: " .. str) + end + inBracket = true + elseif ch == "]" then + if not inBracket then + error("unbalanced brackets: " .. str) + end + inBracket = false + end + end + end + end + if inBracket == true then error("unbalanced brackets: " .. str) end + + for i = 1, #data do + local name, filters = data[i].name:match("%s*(.*[^\\])%[(.*[^\\])%]%s*") + if name == nil then + filters = "" + name = data[i].name + end + if name ~= nil then + data[i].name = XPath.removeSlash(name) + local filterArray = splitByCharWithSlash(filters, ",") + local filterObjs = {} + for _, filter in ipairs(filterArray) do + local key, value = filter:match("^%s*(.-[^\\])%s*=%s*(.-)%s*$") + if key then + table.insert(filterObjs, {key = key, value = value}) + end + end + data[i].filter = filterObjs + end + end + self.data = data +end + +function XPath:copy() + local result = deepCopy(self) + setmetatable(result, XPath) + return result +end + + +function XPath:parent() + local newOne = self:copy() + if #newOne.data <= 1 then + --error("this is the root") + return newOne + else + table.remove(newOne.data, #newOne.data) + end + return newOne +end + +function XPath:_itemToString(item) + local result = XPath.addSlash(item.name) + if item.filter and #item.filter > 0 then + local filter = {} + for _, v in ipairs(item.filter) do + table.insert(filter, v.key .. " = " ..v.value) + end + result = result .. "[" .. table.concat(filter, ", ") .. "]" + end + return result +end + +function XPath:toString(arg) + if arg == nil then + local tab = {} + for _, item in ipairs(self.data) do + table.insert(tab, self:_itemToString(item)) + end + return table.concat(tab, ".") + elseif type(arg) == "number" then + if arg < 0 then arg = self:size() + arg + 1 end + if arg > self:size() or arg < 1 then error("invalid index") end + return self:_itemToString(self.data[arg]) + elseif type(arg) == "table" then + return self:_itemToString(arg) + end +end + +function XPath:hasChild(child) + return child:relative(self) ~= nil +end + +function XPath:relative(root) + if self:size()<#root.data then return nil end + local newRoot = root:copy() + local newSelf = self:copy() + + while #newRoot.data > 0 do + if newRoot.data[1].name ~= newSelf.data[1].name then return nil end + table.remove(newRoot.data, 1) + table.remove(newSelf.data, 1) + end + return newSelf +end + + +function XPath:cat(path) + local newOne = self:copy() + for _, k in ipairs(path.data) do + table.insert(newOne.data, k) + end + return newOne +end + +function XPath:clearFilter() + for i = 1, self:size() do + self.data[i].filter = nil + end + return self +end + +local function getProperty(instance, property) + local state, result = pcall(function() return instance[property] end ) + return state == true and result or nil +end + +local function propertyEqual(lhs, rhs) + return tostring(lhs) == tostring(rhs) +end + +local function propertyMatch(prop, expr) + prop = tostring(prop) + expr = tostring(expr) + return expr == "*" or prop == expr +end + +local function findChildrenByName(instance, name) + local children = instance:GetChildren() + local result = {} + for _, child in ipairs(children) do + if propertyMatch(getProperty(child, "Name"), name) then + table.insert(result, child) + end + end + -- for game.Players, their is no child "LocalPlayer", but you can access it. + if #result == 0 then + local instance = getProperty(instance, name) + if instance then + table.insert(result, instance) + end + end + return result +end + +local function findChildren(instances, name) + local result = {} + for _, instance in ipairs(instances) do + local children = findChildrenByName(instance, name) + for _, child in ipairs(children) do + table.insert(result, child) + end + end + return result +end + +local function findCandidates(instances, path) + for _, name in ipairs(path) do + instances = findChildren(instances, name) + end + return instances +end + +local function passFilter(instance, filter) + local key = filter.key + local path, propertyName = key:match("^%.?(.*[^\\])%.(%w-)$") + if propertyName == nil then + path = "" + propertyName = key:match("^%.?(%w-)$") + end + local pathData = splitByCharWithSlash(path, ".") + for i = 1, #pathData do pathData[i] = XPath.removeSlash(pathData[i]) end + local canditates = findCandidates({instance}, pathData) + for _, canditate in ipairs(canditates) do + local property = getProperty(canditate, propertyName) + if propertyMatch(property, XPath.removeSlash(filter.value)) then + return true + end + end + return false +end + +local function passFilters(instance, filters) + for _, filter in ipairs(filters) do + if not passFilter(instance, filter) then + return false + end + end + return true +end + +local function applyFilters(canditates, filters) + if filters == nil then return canditates end + local result = {} + for _, canditate in ipairs(canditates) do + if passFilters(canditate, filters) then + table.insert(result, canditate) + end + end + return result +end + +function XPath:getFirstInstance() + local instances = self:getInstances() + if #instances == 0 then return nil else return instances[1] end +end + +function XPath:getInstances() + if self:size() <1 then error("instance " .. self:toString() .. " does not exist") end + + local rootInstance = nil + local rootName = self.data[1].name + + if rootName == "game" then + rootInstance = game + elseif rootName == "PluginGuiService" then + rootInstance = game:GetService("PluginGuiService") + end + + if self.root == nil and rootInstance == nil then + error("instance " .. self:toString() .. " does not exist") + end + + local instances = {self.root or rootInstance} + local i = self.root and 1 or 2 + while i <= self:size() do + local name = self.data[i].name + local filters = self.data[i].filter + local candidates = findChildren(instances, name) + instances = applyFilters(candidates, filters) + if #instances == 0 then return instances, i-1 end + i = i + 1 + end + return instances, i-1 +end + +function XPath:setWait(timeOut, delay) + self.waitDelay = delay or self.waitDelay + self.waitTimeOut = timeOut or self.waitTimeOut + return self +end + +function XPath:waitFor(execute, condition, delay, timeOut) + delay = delay or self.waitDelay + timeOut = timeOut or self.waitTimeOut + timeOut = tick()+timeOut + while true do + local result = execute() + if condition(result) then + return result, true + end + wait(delay) + if tick() > timeOut then return result, false end + end +end + +function XPath:waitForFirstInstance() +-- return self:waitForInstances(function(instances) return #instances >0 end)[1] + local instances = self:waitForNInstances(1) + if instances ~= nil and #instances > 0 then return instances[1] end + return nil +end + +function XPath:waitForInstances(condition) + if type(condition) ~= "function" then error("arg #1 should be a function") end + return self:waitFor(function() + return self:getInstances() + end, condition) +end + +function XPath:waitForDisappear() + local _, state = self:waitForInstances(function(instances) return #instances == 0 end) + return state == true +end + +function XPath:waitForNInstances(n) + return self:waitForInstances(function(instances) + return #instances >= n + end) +end + +local function makeInstance(className, props, children) + local instance = Instance.new(className) + if children then + for _, child in ipairs(children) do + child.Parent = instance + end + end + if props then + for k, v in pairs(props) do + instance[k] = v + end + end + return instance +end + +local function test() + local function closeTo(lhs, rhs, err) + return math.abs(lhs-rhs) <= err + end + + local specialChars = [[special chars !"#$%&'()*+,-./:;<=>?@[]\^_`{|}~]] + local convertedSpecialChars = [[special chars !"#$%&'()*+\,-\./:;<\=>?@\[\]\\^_`{|}~]] + + local root + if game.Workspace:FindFirstChild("root") == nil then + root = makeInstance("Folder", + { + Name = "root", + Parent = game.Workspace + }, { + makeInstance("Frame", {Name = "Frame"}, { + makeInstance("TextButton", { + Text = "Button1" + }), + makeInstance("TextLabel", { + Text = "Label1" + }), + makeInstance("ImageButton", { + + }) + }), + makeInstance("Frame", {Name = "Frame"}, { + makeInstance("TextButton", { + Text = "Button2" + }), + makeInstance("TextLabel", { + Text = "Label2" + }) + }), + makeInstance("Frame", {Name = "Frame"}, { + makeInstance("TextLabel", {Text = "Label3"}), + makeInstance("Frame", {Name = specialChars}, { + makeInstance("TextButton", {Name = "TextButton3"}) + }), + makeInstance("TextLabel", {Name = "SpecialCharLabel", Text = specialChars}) + }) + }) + end + + local containerPath = XPath.new("game.Workspace.root.Frame") + + local getContainerDetail = function(root) + return { + textButton = XPath.new("TextButton", root), + textLabel = XPath.new("TextLabel", root), + } + end + + local createSearch = function(container, relativePath, property, value) + local rootPath = container:copy() + local filter = {{key = "." .. relativePath:toString().."."..property, value = value}} + rootPath:mergeFilter(rootPath:size(), filter) + return rootPath + end + + local rootPath = createSearch(containerPath, getContainerDetail().textButton, "Text", "Button2") + print("createSearch:", rootPath:toString()) + local rootInstance = rootPath:waitForFirstInstance() + assert(rootInstance) + + local containerDetail = getContainerDetail(rootInstance) + + local label2 = containerDetail.textLabel:waitForFirstInstance() + assert(label2.Text == "Label2") + + print("relative path test") + + local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2]") + local instance = path:getFirstInstance() + assert(instance) + local relativePath = XPath.new("TextButton", instance) + local instance = relativePath:getFirstInstance() + assert(instance.Text == "Button2") + + local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame].TextLabel") + local instance = path:getFirstInstance() + assert(instance.Text=="Label2") + local newPathString = XPath.new(instance):toString() + assert(newPathString=="game.Workspace.root.Frame.TextLabel") + + local pathStr = "game.Workspace.root.Frame[."..convertedSpecialChars..".TextButton3.Name = TextButton3].TextLabel" + local path = XPath.new(pathStr) + assert(path:toString()==pathStr) + local instance = path:getFirstInstance() + assert(instance.Text=="Label3") + + local pathStr = "game.Workspace.root.Frame[.SpecialCharLabel.Text = "..convertedSpecialChars.."].TextLabel" + local path = XPath.new(pathStr) + assert(path:toString()==pathStr) + local instance = path:getFirstInstance() + assert(instance.Text=="Label3") + + local pathStr = "game.Workspace.root.Frame."..convertedSpecialChars..".TextButton3" + local path = XPath.new(pathStr) + local newPathStr = path:toString() + assert(newPathStr==pathStr) + local instance = path:getFirstInstance() + assert(instance.Name=="TextButton3") + + local rootPath = XPath.new("game.Workspace.root") + local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame].TextLabel") + local relativePath = path:relative(rootPath) + relativePath:clearFilter() + + print("testing getFirstInstance()") + print(path:toString()) + local instance = path:getFirstInstance() + assert(instance.Text == "Label2") + + print("testing wildcard *") + path = XPath.new("game.Workspace.root.*[.TextButton.Text = Button2].TextLabel") + instance = path:getFirstInstance() + assert(instance.Text == "Label2") + + path = XPath.new("game.Workspace.root.*[.ImageButton.Name = *].TextLabel") + instance = path:getFirstInstance() + assert(instance.Text == "Label1") + + local timeBefore = nil + print("testing timeout waitForNInstances() ") + path = XPath.new("game.Workspace.root.Frame.TextLabel") + timeBefore = tick() + local instances, state = path:setWait(2):waitForNInstances(5) + assert(closeTo(tick() - timeBefore, 2, 0.5)) + assert(state == false) + + print("testing normal waitForNInstances() ") + timeBefore = tick() + spawn(function() + wait(2) + makeInstance("Frame", { + Name = "Frame", + Parent = game.Workspace.root + }, { + makeInstance("TextButton", { + Text = "Button3" + }), + makeInstance("TextLabel", { + Text = "Label3" + }) + }) + end) + local instances = path:setWait(5):waitForNInstances(5) + assert(closeTo(tick() - timeBefore, 2, 0.5)) + assert(#instances >= 5) + + print("testing getFirstInstance() ") + path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button3]") + instance = path:getFirstInstance() + assert(instance.TextButton.Text == "Button3") + + print("testing timeout waitForDisappear() ") + timeBefore = tick() + local notExist = path:setWait(2):waitForDisappear() + assert(notExist == false) + assert(closeTo(tick() - timeBefore, 2, 0.5)) + + print("testing normal waitForDisappear() ") + timeBefore = tick() + spawn(function() + wait(2) + instance:Destroy() + end) + local notExist = path:setWait(5):waitForDisappear() + assert(closeTo(tick() - timeBefore, 2, 0.5)) + assert(notExist == true) + + print("test finised") +end + +--test() + +return XPath \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/XPath.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/XPath.spec.lua new file mode 100644 index 0000000..250d5d0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/XPath.spec.lua @@ -0,0 +1,96 @@ +return function() + local XPath = require(script.Parent.XPath) + + local function makeInstance(className, props, children) + local instance = Instance.new(className) + if children then + for _, child in ipairs(children) do + child.Parent = instance + end + end + if props then + for k, v in pairs(props) do + instance[k] = v + end + end + return instance + end + + local specialChars = [[special chars !"#$%&'()*+,-./:;<=>?@[]\^_`{|}~]] + local convertedSpecialChars = [[special chars !"#$%&'()*+\,-\./:;<\=>?@\[\]\\^_`{|}~]] + + local root = makeInstance("Folder", + { + Name = "root", + Parent = workspace + }, { + makeInstance("Frame", {Name = "Frame"}, { + makeInstance("TextButton", { + Text = "Button1" + }), + makeInstance("TextLabel", { + Text = "Label1" + }), + makeInstance("ImageButton", { + + }) + }), + makeInstance("Frame", {Name = "Frame"}, { + makeInstance("TextButton", { + Text = "Button2" + }), + makeInstance("TextLabel", { + Text = "Label2" + }) + }), + makeInstance("Frame", {Name = "Frame"}, { + makeInstance("TextLabel", {Text = "Label3"}), + makeInstance("Frame", {Name = specialChars}, { + makeInstance("TextButton", {Name = "TextButton3"}) + }), + makeInstance("TextLabel", {Name = "SpecialCharLabel", Text = specialChars}) + }) + }) + + describe("basic tests", function() + it("getFirstInstance should work", function() + local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame].TextLabel") + local instance = path:getFirstInstance() + expect(instance.Text).to.equal("Label2") + expect(XPath.new(instance):toString()).to.equal("game.Workspace.root.Frame.TextLabel") + end) + it("wildcard should work", function() + local path = XPath.new("game.Workspace.root.*[.TextButton.Text = Button2].TextLabel") + local instance = path:getFirstInstance() + expect(instance.Text).to.equal("Label2") + end) + it("wildcard on property should work", function() + local path = XPath.new("game.Workspace.root.*[.ImageButton.Name = *].TextLabel") + local instance = path:getFirstInstance() + expect(instance.Text).to.equal("Label1") + end) + end) + describe("should work with special chars", function() + it("should work with special chars in path", function() + local pathStr = "game.Workspace.root.Frame."..convertedSpecialChars..".TextButton3" + local path = XPath.new(pathStr) + expect(path:toString()).to.equal(pathStr) + local instance = path:getFirstInstance() + expect(instance.Name).to.equal("TextButton3") + end) + it("should work with special chars in filter keys", function() + local pathStr = "game.Workspace.root.Frame[."..convertedSpecialChars..".TextButton3.Name = TextButton3].TextLabel" + local path = XPath.new(pathStr) + expect(path:toString()).to.equal(pathStr) + local instance = path:getFirstInstance() + expect(instance.Text).to.equal("Label3") + end) + it("should work with special chars in filter values", function() + local pathStr = "game.Workspace.root.Frame[.SpecialCharLabel.Text = "..convertedSpecialChars.."].TextLabel" + local path = XPath.new(pathStr) + expect(path:toString()).to.equal(pathStr) + local instance = path:getFirstInstance() + expect(instance.Text).to.equal("Label3") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/init.lua new file mode 100644 index 0000000..04acd1e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/init.lua @@ -0,0 +1,11 @@ +local Element = require(script.Element) +local VirtualInput = require(script.VirtualInput) +local XPath = require(script.XPath) + +local Rhodium = { + Element = Element, + VirtualInput = VirtualInput, + XPath = XPath, +} + +return Rhodium \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/init.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/init.spec.lua new file mode 100644 index 0000000..4c2c632 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/init.spec.lua @@ -0,0 +1,12 @@ +return function() + describe("Rhodium", function() + it("should load", function() + local Rhodium = require(script.Parent) + + expect(Rhodium).to.be.ok() + expect(Rhodium.Element).to.be.ok() + expect(Rhodium.XPath).to.be.ok() + expect(Rhodium.VirtualInput).to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/Roact.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/lock.toml new file mode 100644 index 0000000..bbc72f7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/lock.toml @@ -0,0 +1,9 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/roact-fit-components" +version = "1.2.5" +commit = "b784928fdef64215d38e2febae28412a3b2a520b" +source = "url+https://github.com/roblox/roact-fit-components" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameHorizontal.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameHorizontal.lua new file mode 100644 index 0000000..280f847 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameHorizontal.lua @@ -0,0 +1,21 @@ +local root = script.Parent +local Packages = root.Parent + +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) + +local FitFrameOnAxis = require(script.Parent.FitFrameOnAxis) + +return function(props) + props = props or {} + local height = props.height + + local filteredProps = Cryo.Dictionary.join(props, { + axis = FitFrameOnAxis.Axis.Horizontal, + minimumSize = UDim2.new(UDim.new(0, 0), height), + + height = Cryo.None, + }) + + return Roact.createElement(FitFrameOnAxis, filteredProps) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameOnAxis.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameOnAxis.lua new file mode 100644 index 0000000..bb20c26 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameOnAxis.lua @@ -0,0 +1,182 @@ +local root = script.Parent +local Packages = root.Parent + +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) + +local Rect = require(root.Rect) + +local FitFrameOnAxis = Roact.PureComponent:extend("FitFrameOnAxis") +FitFrameOnAxis.Axis = { + Horizontal = {}, + Vertical = {}, + Both = {}, +} + +FitFrameOnAxis.defaultProps = { + axis = FitFrameOnAxis.Axis.Vertical, + minimumSize = UDim2.new(UDim.new(0, 0), UDim.new(0, 0)), + margin = Rect.square(0), + + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + ImageSet = {}, + VerticalAlignment = Enum.VerticalAlignment.Top, + contentPadding = UDim.new(0, 0), + textProps = nil, +} + +function FitFrameOnAxis:init() + self.layoutRef = Roact.createRef() + self.frameRef = self.props[Roact.Ref] or Roact.createRef() + + self.onResize = function() + local currentLayout = self.layoutRef.current + local currentFrame = self.frameRef.current + if not currentFrame or not currentLayout then + return + end + + currentFrame.Size = self:__getSize(currentLayout) + end +end + +function FitFrameOnAxis:render() + assert(self.props.Size == nil, "Size is not a valid property of FitFrameOnAxis. Did you mean `minimumSize`?") + local children = self.props[Roact.Children] or {} + local filteredProps = self:__getFilteredProps() + + local instanceType = self.props.onActivated and "ImageButton" or "ImageLabel" + + children = Cryo.Dictionary.join(children, { + ["$layout"] = Roact.createElement("UIListLayout", { + FillDirection = self.props.FillDirection, + HorizontalAlignment = self.props.HorizontalAlignment, + Padding = self.props.contentPadding, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = self.props.VerticalAlignment, + + [Roact.Change.AbsoluteContentSize] = self.onResize, + [Roact.Ref] = self.layoutRef, + }), + ["$margin"] = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, self.props.margin.left), + PaddingRight = UDim.new(0, self.props.margin.right), + PaddingTop = UDim.new(0, self.props.margin.top), + PaddingBottom = UDim.new(0, self.props.margin.bottom), + }), + }) + + if self.props.textProps then + return Roact.createElement(instanceType, filteredProps, { + TextLabel = Roact.createElement("TextLabel", Cryo.Dictionary.join(self.props.textProps, { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + })), + + ChildFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + }, children), + }) + else + return Roact.createElement(instanceType, filteredProps, children) + end +end + +function FitFrameOnAxis:didMount() + self.onResize() +end + +function FitFrameOnAxis:didUpdate() + self.onResize() +end + +function FitFrameOnAxis:__getFilteredProps() + -- Will return a new prop map after removing + -- Roact.Children and any defaultProps in an effort + -- to only return safe Roblox Instance "ImageLabel" + -- properties that may be present. + local filteredProps = Cryo.Dictionary.join(self.props.ImageSet, { + [Roact.Ref] = self.frameRef, + [Roact.Event.Activated] = self.props.onActivated, + }) + + for property, _ in pairs(FitFrameOnAxis.defaultProps) do + filteredProps[property] = Cryo.None + end + + filteredProps.textProps = Cryo.None + + return Cryo.Dictionary.join(self.props, filteredProps, { + onActivated = Cryo.None, + [Roact.Children] = Cryo.None, + }) +end + +function FitFrameOnAxis:__getSize(currentLayout) + if self.props.axis == FitFrameOnAxis.Axis.Both then + return self:__getBothAxisSize(currentLayout) + else + -- Arrangement of UDims are flip-flopped based + -- on which axis is our primary axis + local axisUDim = self:__getAxisUDim(currentLayout.AbsoluteContentSize) + local otherUDim = self:__getOtherUDim() + + if self.props.axis == FitFrameOnAxis.Axis.Vertical then + return UDim2.new(otherUDim, axisUDim) + elseif self.props.axis == FitFrameOnAxis.Axis.Horizontal then + return UDim2.new(axisUDim, otherUDim) + end + end +end + +function FitFrameOnAxis:__getBothAxisSize(currentLayout) + local minimumSize = self.props.minimumSize + local absoluteContentSize = currentLayout.AbsoluteContentSize + + local xAxis = UDim.new(minimumSize.X.Scale, absoluteContentSize.X + self:__getHorizontalMargin()) + local yAxis = UDim.new(minimumSize.Y.Scale, absoluteContentSize.Y + self:__getVerticalMargin()) + + return UDim2.new(xAxis, yAxis) +end + +function FitFrameOnAxis:__getAxisUDim(vector2) + -- Merges minimumSize with given Vector2 + -- to create UDim for primary axis + local minimumSize = self.props.minimumSize + + local targetUDim + local lengthOfChildren + if self.props.axis == FitFrameOnAxis.Axis.Vertical then + targetUDim = minimumSize.Y + lengthOfChildren = vector2.Y + self:__getVerticalMargin() + elseif self.props.axis == FitFrameOnAxis.Axis.Horizontal then + targetUDim = minimumSize.X + lengthOfChildren = vector2.X + self:__getHorizontalMargin() + end + + return UDim.new(targetUDim.Scale, math.max(lengthOfChildren, targetUDim.Offset)) +end + +function FitFrameOnAxis:__getVerticalMargin() + return self.props.margin.top + self.props.margin.bottom +end + +function FitFrameOnAxis:__getHorizontalMargin() + return self.props.margin.left + self.props.margin.right +end + +function FitFrameOnAxis:__getOtherUDim() + -- Since there is no primary axis to merge with, + -- this UDim is entirely represented by minimumSize + local minimumSize = self.props.minimumSize + + if self.props.axis == FitFrameOnAxis.Axis.Vertical then + return minimumSize.X + elseif self.props.axis == FitFrameOnAxis.Axis.Horizontal then + return minimumSize.Y + end +end + +return FitFrameOnAxis diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameVertical.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameVertical.lua new file mode 100644 index 0000000..0aade4c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameVertical.lua @@ -0,0 +1,21 @@ +local root = script.Parent +local Packages = root.Parent + +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) + +local FitFrameOnAxis = require(script.Parent.FitFrameOnAxis) + +return function(props) + props = props or {} + local width = props.width + + local filteredProps = Cryo.Dictionary.join(props, { + axis = FitFrameOnAxis.Axis.Vertical, + minimumSize = UDim2.new(width, UDim.new(0, 0)), + + width = Cryo.None, + }) + + return Roact.createElement(FitFrameOnAxis, filteredProps) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitTextLabel.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitTextLabel.lua new file mode 100644 index 0000000..453dce8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitTextLabel.lua @@ -0,0 +1,120 @@ +local root = script.Parent +local Packages = root.Parent + +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) + +local EngineFeatureTextBoundsRoundUp do + local success, value = pcall(function() + return game:GetEngineFeature("TextBoundsRoundUp") + end) + EngineFeatureTextBoundsRoundUp = success and value +end + +-- We need to add 2 to these values as a workaround to a documented engine bug +local TextService = game:GetService("TextService") +local function getTextHeight(text, fontSize, font, widthCap) + if EngineFeatureTextBoundsRoundUp then + return TextService:GetTextSize(text, fontSize, font, Vector2.new(widthCap, 10000)).Y + else + return TextService:GetTextSize(text, fontSize, font, Vector2.new(widthCap, 10000)).Y + 2 + end +end + +local function getTextWidth(text, fontSize, font) + if EngineFeatureTextBoundsRoundUp then + return TextService:GetTextSize(text, fontSize, font, Vector2.new(10000, 10000)).X + else + return TextService:GetTextSize(text, fontSize, font, Vector2.new(10000, 10000)).X + 2 + end +end + +local FitTextLabel = Roact.PureComponent:extend("FitTextLabel") +FitTextLabel.Width = { + FitToText = {}, +} +FitTextLabel.defaultProps = { + Font = Enum.Font.SourceSans, + Text = "Label", + TextSize = 12, + TextWrapped = true, + + maximumWidth = math.huge, +} + +function FitTextLabel:init() + self.frameRef = Roact.createRef() + + self.onResize = function() + if not self.frameRef.current then + return + end + + self.frameRef.current.Size = self:__getSize(self.frameRef.current) + end +end + +function FitTextLabel:render() + local instanceType = self.props.onActivated and "TextButton" or "TextLabel" + return Roact.createElement(instanceType, self:__getFilteredProps()) +end + +function FitTextLabel:didMount() + self.onResize() +end + +function FitTextLabel:didUpdate() + self.onResize() +end + +function FitTextLabel:__getFilteredProps() + -- Will return a new prop map after removing + -- Roact.Children and any defaultProps in an effort + -- to only return safe Roblox Instance "TextLabel" + -- properties that may be present. + local filteredProps = { + width = Cryo.None, + maximumWidth = Cryo.None, + onActivated = Cryo.None, + Size = UDim2.new(self.props.width, UDim.new(0, 0)), + [Roact.Ref] = self.frameRef, + + [Roact.Children] = Cryo.Dictionary.join(self.props[Roact.Children] or {}, { + sizeConstraint = self.props.maximumWidth < math.huge and Roact.createElement("UISizeConstraint", { + MaxSize = Vector2.new(self.props.maximumWidth, math.huge), + }) + }), + + [Roact.Event.Activated] = self.props.onActivated, + + [Roact.Change.AbsoluteSize] = function(rbx) + if self.props[Roact.Change.AbsoluteSize] then + self.props[Roact.Change.AbsoluteSize](rbx) + end + self.onResize() + end, + } + + return Cryo.Dictionary.join(self.props, filteredProps) +end + +function FitTextLabel:__getSize(rbx) + local maximumWidth = self.props.maximumWidth + local width = self.props.width + if width == FitTextLabel.Width.FitToText then + local textWidth = getTextWidth(self.props.Text, self.props.TextSize, self.props.Font) + width = UDim.new(0, math.min(textWidth, maximumWidth)) + end + + local widthCap = math.max(maximumWidth < math.huge and maximumWidth or 0, rbx.AbsoluteSize.X) + local textHeight = getTextHeight( + self.props.Text, + self.props.TextSize, + self.props.Font, + widthCap + ) + + return UDim2.new(width, UDim.new(0, textHeight)) +end + +return FitTextLabel diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/Rect.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/Rect.lua new file mode 100644 index 0000000..4f2f8c7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/Rect.lua @@ -0,0 +1,26 @@ +return { + rectangle = function(horizontal, vertical) + return { + top = vertical, + bottom = vertical, + left = horizontal, + right = horizontal, + } + end, + square = function(x) + return { + top = x, + bottom = x, + left = x, + right = x, + } + end, + quad = function(top, right, bottom, left) + return { + top = top, + bottom = bottom, + left = left, + right = right, + } + end, +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/init.lua new file mode 100644 index 0000000..ac42b94 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/init.lua @@ -0,0 +1,7 @@ +return { + FitFrameHorizontal = require(script.FitFrameHorizontal), + FitFrameOnAxis = require(script.FitFrameOnAxis), + FitFrameVertical = require(script.FitFrameVertical), + FitTextLabel = require(script.FitTextLabel), + Rect = require(script.Rect), +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/Roact.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/enumerate.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/enumerate.lua new file mode 100644 index 0000000..780f356 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/enumerate.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_enumerate"]["enumerate"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/lock.toml new file mode 100644 index 0000000..7ae4b4b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/lock.toml @@ -0,0 +1,11 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/roact-gamepad" +version = "0.4.4" +commit = "e5eb28c3a4981f971378372570ce9cd4ee023137" +source = "url+https://github.com/roblox/roact-gamepad" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "enumerate roblox/enumerate 1.0.0 url+https://github.com/roblox/enumerate", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusContext.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusContext.lua new file mode 100644 index 0000000..acc9dac --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusContext.lua @@ -0,0 +1,4 @@ +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) + +return Roact.createContext(nil) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusController.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusController.lua new file mode 100644 index 0000000..6ae1e76 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusController.lua @@ -0,0 +1,359 @@ +--!nonstrict +-- Manages groups of selectable elements, reacting to selection changes for +-- individual items and triggering events for group selection changes +local Packages = script.Parent.Parent +local Cryo = require(Packages.Cryo) + +local Input = require(script.Parent.Input) +local createSignal = require(script.Parent.createSignal) +local debugPrint = require(script.Parent.debugPrint) + +local InternalApi = require(script.Parent.FocusControllerInternalApi) + +local FocusControllerInternal = {} +FocusControllerInternal.__index = FocusControllerInternal + +function FocusControllerInternal.new() + local self = setmetatable({ + selectionChangedSignal = createSignal(), + boundInputsChangedSignal = createSignal(), + + focusNodeTree = {}, + allNodes = {}, + + rootRef = nil, + engineInterface = nil, + captureFocusOnInitialize = false, + inputDisconnectors = {}, + boundInputs = {}, + focusedLeaf = nil, + }, FocusControllerInternal) + + return self +end + +function FocusControllerInternal:moveFocusTo(ref) + if self.engineInterface == nil then + error("FocusController is not connected to a component hierarchy!", 2) + end + + debugPrint("[FOCUS] Move focus to", ref) + local node = self.allNodes[ref] + + if node ~= nil and not self:isNodeFocused(node) then + node:focus() + end +end + +function FocusControllerInternal:moveFocusToNeighbor(neighborProp) + if self.engineInterface == nil then + error("FocusController is not connected to a component hierarchy!", 2) + end + + if self.focusedLeaf ~= nil then + debugPrint("[FOCUS] Move focus to", neighborProp, "from", self.focusedLeaf.ref) + local refValue = self.focusedLeaf.ref:getValue() + if refValue ~= nil and refValue[neighborProp] ~= nil then + self:setSelection(refValue[neighborProp]) + end + end +end + +function FocusControllerInternal:getSelection() + return self.engineInterface.getSelection() +end + +function FocusControllerInternal:setSelection(ref) + self.engineInterface.setSelection(ref) +end + +function FocusControllerInternal:registerNode(parentNode, refKey, node) + if parentNode ~= nil then + debugPrint("[TREE ] Registering child node", refKey) + + local parentEntry = self.focusNodeTree[parentNode] or {} + parentEntry[refKey] = node + self.focusNodeTree[parentNode] = parentEntry + else + debugPrint("[TREE ] Registering root node", refKey) + self.rootRef = refKey + end + + self.allNodes[refKey] = node +end + +function FocusControllerInternal:deregisterNode(parentNode, refKey) + debugPrint("[TREE ] Deregistering child node", refKey) + if parentNode ~= nil then + self.focusNodeTree[parentNode][refKey] = nil + end + + self.allNodes[refKey] = nil +end + +function FocusControllerInternal:descendantRemovedRefocus() + -- If focusedLeaf is nil, then we've lost focus altogether, which likely + -- means that focus belongs to a different focusable tree. Since that's out + -- of our control, we can stop here. + if self.focusedLeaf == nil then + return + end + + -- If the currently focused leaf has a nil ref, then its associated host + -- component has unmounted and we need to refocus. + if self.focusedLeaf.ref:getValue() == nil then + debugPrint("[FOCUS] Focused node was removed; refocusing from nearest existing ancestor") + + -- Climb up the focusedLeaf's ancestry until we find a node that still + -- exists; if we do find one, focus it + local ancestorNode = self.focusedLeaf.parent + while ancestorNode ~= nil and self.allNodes[ancestorNode.ref] == nil do + ancestorNode = ancestorNode.Parent + end + + if ancestorNode ~= nil then + ancestorNode:focus() + end + end +end + +function FocusControllerInternal:descendantAddedRefocus() + -- If focusedLeaf is nil, then we've lost focus altogether, which likely + -- means that focus belongs to a different focusable tree. Since that's out + -- of our control, we can stop here. + if self.focusedLeaf == nil then + return + end + + -- If the current focusedLeaf has children, then descendants must have been + -- added to it, and we should re-run its focus logic. + if not Cryo.isEmpty(self:getChildren(self.focusedLeaf)) then + -- A new descendant was introduced, which means that we need to refocus + -- the current leaf + debugPrint("[FOCUS] Currently-focused node is no longer a leaf; refocusing", self.focusedLeaf.ref) + self.focusedLeaf:focus() + end +end + +function FocusControllerInternal:getChildren(parentNode) + return self.focusNodeTree[parentNode] or {} +end + +function FocusControllerInternal:isNodeFocused(node) + if self.focusedLeaf == nil then + return false + end + + if self.focusedLeaf == node then + return true + end + + -- Find out if one of the focused leaf's parents is equal to the provided + -- node, in which case, it remains in focus + local parentNode = self.focusedLeaf.parent + while parentNode ~= nil do + if parentNode == node then + return true + end + + parentNode = parentNode.parent + end + + return false +end + +-- Prints a human-readable version of the node tree. +function FocusControllerInternal:debugPrintTree() + local function recursePrintTree(node, indent) + -- Print the current node + debugPrint(indent, tostring(node.ref)) + + -- Recurse through children + local children = self:getChildren(node) + for _, childNode in pairs(children) do + recursePrintTree(childNode, indent .. " ") + end + end + + debugPrint("Printing Focus Node Tree:") + local rootNode = self.allNodes[self.rootRef] + recursePrintTree(rootNode, "") +end + +function FocusControllerInternal:updateInputBindings() + local newBindings = {} + + local focusChainNode = self.focusedLeaf + while focusChainNode ~= nil do + for _, binding in pairs(focusChainNode.inputBindings) do + local key = Input.getUniqueKey(binding) + local existing = newBindings[key] + if existing == nil then + debugPrint("[INPUT] Bind input", key) + newBindings[key] = binding + end + end + + focusChainNode = focusChainNode.parent + end + + -- It's pretty straightforward to simply disconnect and reconnect all event + -- connections whenever this function is called; we wouldn't typically be + -- able to rely on binding identity equality anyways + for _, disconnector in pairs(self.inputDisconnectors) do + disconnector() + end + + self.inputDisconnectors = {} + self.boundInputs = {} + for key, binding in pairs(newBindings) do + self.inputDisconnectors[key] = Input.connectToEvent(binding, self.engineInterface) + if binding.keyCode then + self.boundInputs[binding.keyCode] = binding.meta or {} + end + end +end + +function FocusControllerInternal:initialize(engineInterface) + -- If the engineInterface is already set, then this FocusController was + -- probably also assigned to another tree + if self.engineInterface ~= nil then + error("FocusController cannot be initialized more than once; make sure you are not passing it to multiple components") + end + + self.engineInterface = engineInterface + + -- Create a connection to the GuiService property relevant to the navigation + -- tree we want to connect + self.guiServiceConnection = engineInterface.subscribeToSelectionChanged(function() + -- This FocusController is not attached to an Instance hierarchy yet, so + -- we shouldn't try to manage selection + if self.rootRef == nil then + return + end + + -- Track whether or not the previous focus was inside this hierarchy + local wasPreviouslyFocused = self.focusedLeaf ~= nil + + -- Nil out our focusedLeaf (we'll recalculate it if necessary) and get + -- the current selection + self.focusedLeaf = nil + local selectedInstance = engineInterface.getSelection() + local rootRefValue = self.rootRef:getValue() + + -- If selection is occurring within this FocusControllerInternal's + -- hierarchy, we need to recompute the currently focused leaf + if selectedInstance ~= nil then + if rootRefValue == selectedInstance or selectedInstance:IsDescendantOf(rootRefValue) then + debugPrint( + "[EVENT] Selection changed to", + selectedInstance, + "in focus hierarchy beginning at", + rootRefValue + ) + + -- Find the currently-focused node within our hierarchy and set + -- self.focusedLeaf accordingly. + for ref, node in pairs(self.allNodes) do + if selectedInstance == ref:getValue() then + self.focusedLeaf = node + break + end + end + end + end + + -- We should fire our selectionChanged signal in the event that any of + -- the following occur: + -- 1. Selection moved within the hierarchy + -- 2. Selection moved from outside the hierarchy to an element inside it + -- 3. Selection moved from inside the hierarchy to an element outside it + if self.focusedLeaf ~= nil or wasPreviouslyFocused then + self.selectionChangedSignal:fire() + + -- Update input connections here + self:updateInputBindings() + self.boundInputsChangedSignal:fire(self.boundInputs) + end + end) + + if self.captureFocusOnInitialize then + self:captureFocus() + end +end + +function FocusControllerInternal:captureFocus() + if self.engineInterface == nil then + self.captureFocusOnInitialize = true + else + self.allNodes[self.rootRef]:focus() + end +end + +function FocusControllerInternal:releaseFocus() + if self.engineInterface ~= nil then + self.engineInterface.setSelection(nil) + end +end + +function FocusControllerInternal:teardown() + if self.guiServiceConnection ~= nil then + self.guiServiceConnection:Disconnect() + end + + -- Disconnect all bound inputs. These can be left dangling when a whole tree + -- is unmounted at once + for _, disconnect in pairs(self.inputDisconnectors) do + disconnect() + end + + -- Make sure this controller is restored to its uninitialized state + self.rootRef = nil + self.engineInterface = nil + self.captureFocusOnInitialize = false + self.focusedLeaf = nil +end + +function FocusControllerInternal:subscribeToSelectionChange(callback) + debugPrint("[TREE ] New subscription to selection change event") + return self.selectionChangedSignal:subscribe(callback) +end + +-- Creates an object with a public API for managing focus. This object can be +-- used in components to direct focus as necessary +function FocusControllerInternal.createPublicApiWrapper() + local focusControllerInternal = FocusControllerInternal.new() + + return { + [InternalApi] = focusControllerInternal, + moveFocusTo = function(...) + focusControllerInternal:moveFocusTo(...) + end, + moveFocusLeft = function() + focusControllerInternal:moveFocusToNeighbor("NextSelectionLeft") + end, + moveFocusRight = function() + focusControllerInternal:moveFocusToNeighbor("NextSelectionRight") + end, + moveFocusUp = function() + focusControllerInternal:moveFocusToNeighbor("NextSelectionUp") + end, + moveFocusDown = function() + focusControllerInternal:moveFocusToNeighbor("NextSelectionDown") + end, + captureFocus = function() + focusControllerInternal:captureFocus() + end, + releaseFocus = function() + focusControllerInternal:releaseFocus() + end, + getBoundInputs = function() + return focusControllerInternal.boundInputs + end, + subscribeToBoundInputsChanged = function(callback) + return focusControllerInternal.boundInputsChangedSignal:subscribe(callback) + end, + } +end + +return FocusControllerInternal \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusController.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusController.spec.lua new file mode 100644 index 0000000..55b26cb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusController.spec.lua @@ -0,0 +1,452 @@ +return function() + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + + local FocusNode = require(script.Parent.FocusNode) + local FocusController = require(script.Parent.FocusController) + local InternalApi = require(script.Parent.FocusControllerInternalApi) + local Input = require(script.Parent.Input) + + local MockEngine = require(script.Parent.Test.MockEngine) + local createSpy = require(script.Parent.Test.createSpy) + + local function createRootNode(ref) + local node = FocusNode.new({ + focusController = FocusController.createPublicApiWrapper(), + [Roact.Ref] = ref, + }) + + node:attachToTree(nil, function() end) + + return node + end + + local function addChildNode(parentNode) + local instance = Instance.new("Frame") + instance.Parent = parentNode.ref:getValue() + + local childRef, _ = Roact.createBinding(instance) + local childNode = FocusNode.new({ + parentFocusNode = parentNode, + [Roact.Ref] = childRef, + }) + childNode:attachToTree(parentNode, function() end) + + return childNode, childRef + end + + describe("event management", function() + it("should not fire subscribed signals before it's initialized", function() + local ref, _ = Roact.createBinding(Instance.new("Frame")) + local focusNode = createRootNode(ref) + + local focusController = focusNode.focusController[InternalApi] + local selectionChangeSpy = createSpy() + focusController:subscribeToSelectionChange(selectionChangeSpy.value) + + local mockEngine, _ = MockEngine.new() + + mockEngine:simulateSelectionChanged(Instance.new("Frame")) + expect(selectionChangeSpy.callCount).to.equal(0) + end) + + it("should fire subscribed signals when selection changes occur once initialized", function() + local ref, _ = Roact.createBinding(Instance.new("Frame")) + local focusNode = createRootNode(ref) + + local focusController = focusNode.focusController[InternalApi] + local selectionChangeSpy = createSpy() + focusController:subscribeToSelectionChange(selectionChangeSpy.value) + local mockEngine, engineInterface = MockEngine.new() + + expect(selectionChangeSpy.callCount).to.equal(0) + + focusController:initialize(engineInterface) + mockEngine:simulateSelectionChanged(ref:getValue()) + expect(selectionChangeSpy.callCount).to.equal(1) + end) + end) + + describe("focus logic", function() + it("should consider selected objects to be in focus", function() + local ref, _ = Roact.createBinding(Instance.new("Frame")) + local focusNode = createRootNode(ref) + + local _, engineInterface = MockEngine.new() + local focusController = focusNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + expect(focusController:isNodeFocused(focusNode)).to.equal(false) + focusNode:focus() + expect(focusController:isNodeFocused(focusNode)).to.equal(true) + end) + + it("should consider parent nodes of selected objects to be in focus", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + local childNode, _ = addChildNode(parentNode) + + local _, engineInterface = MockEngine.new() + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + expect(focusController:isNodeFocused(parentNode)).to.equal(false) + expect(focusController:isNodeFocused(childNode)).to.equal(false) + childNode:focus() + expect(focusController:isNodeFocused(parentNode)).to.equal(true) + expect(focusController:isNodeFocused(childNode)).to.equal(true) + end) + + it("should change its notion of focus when selection is changed", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local childNodeA, _ = addChildNode(parentNode) + local childNodeB, childRefB = addChildNode(parentNode) + + local _, engineInterface = MockEngine.new() + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + childNodeA:focus() + + expect(focusController:isNodeFocused(childNodeA)).to.equal(true) + expect(focusController:isNodeFocused(childNodeB)).to.equal(false) + + engineInterface.setSelection(childRefB:getValue()) + expect(focusController:isNodeFocused(childNodeA)).to.equal(false) + expect(focusController:isNodeFocused(childNodeB)).to.equal(true) + end) + + it("should change its notion of focus when selection changes via the engine", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local childNodeA, _ = addChildNode(parentNode) + local childNodeB, childRefB = addChildNode(parentNode) + + local mockEngine, engineInterface = MockEngine.new() + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + childNodeA:focus() + + expect(focusController:isNodeFocused(childNodeA)).to.equal(true) + expect(focusController:isNodeFocused(childNodeB)).to.equal(false) + + mockEngine:simulateSelectionChanged(childRefB:getValue()) + expect(focusController:isNodeFocused(childNodeA)).to.equal(false) + expect(focusController:isNodeFocused(childNodeB)).to.equal(true) + end) + + it("should only track selection changes inside its hierarchy", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local childNodeA, _ = addChildNode(parentNode) + local childNodeB, _ = addChildNode(parentNode) + + local mockEngine, engineInterface = MockEngine.new() + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + local selectionChangedSpy = createSpy() + focusController:subscribeToSelectionChange(selectionChangedSpy.value) + + childNodeA:focus() + expect(focusController:isNodeFocused(childNodeA)).to.equal(true) + expect(selectionChangedSpy.callCount).to.equal(1) + + -- Moving selection within the hierarchy should trigger fire events + childNodeB:focus() + expect(selectionChangedSpy.callCount).to.equal(2) + + -- When selection changes from _inside_ this hierarchy to nil, + -- notify accordingly + mockEngine:simulateSelectionChanged(nil) + expect(selectionChangedSpy.callCount).to.equal(3) + + -- Moving back into the hierarchy should once again trigger events + childNodeA:focus() + expect(selectionChangedSpy.callCount).to.equal(4) + + -- When selection changes from inside the hierarchy to outside the + -- hierarchy, events should fire + local unconnectedFrame = Instance.new("Frame") + mockEngine:simulateSelectionChanged(unconnectedFrame) + expect(selectionChangedSpy.callCount).to.equal(5) + + -- When selection changes between elements outside the hierarchy, + -- no events should be triggered + local unconnectedFrame2 = Instance.new("Frame") + mockEngine:simulateSelectionChanged(unconnectedFrame2) + expect(selectionChangedSpy.callCount).to.equal(5) + + -- When selection changes from an element outside the hierarchy to + -- nil, no events should be triggered + mockEngine:simulateSelectionChanged(nil) + expect(selectionChangedSpy.callCount).to.equal(5) + end) + end) + + describe("Tree-level focus management", function() + it("should not automatically capture focus", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local focusControllerInternal = parentNode.focusController[InternalApi] + expect(focusControllerInternal:isNodeFocused(parentNode)).to.equal(false) + end) + + it("should focus the top-level node when captureFocus is called", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local _, engineInterface = MockEngine.new() + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + parentNode.focusController.captureFocus() + local focusControllerInternal = parentNode.focusController[InternalApi] + expect(focusControllerInternal:isNodeFocused(parentNode)).to.equal(true) + end) + + it("should focus the top-level node when captureFocus is called, even if initialized afterwards", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local _, engineInterface = MockEngine.new() + + -- Capture focus first, then initialize the node afterwards. This + -- simulates scenarios in which a component wants to captureFocus on + -- didMount, but is not yet parented to the DataModel + parentNode.focusController.captureFocus() + local focusControllerInternal = parentNode.focusController[InternalApi] + focusControllerInternal:initialize(engineInterface) + + expect(focusControllerInternal:isNodeFocused(parentNode)).to.equal(true) + end) + + it("should set selection to nil when focus is released", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local _, engineInterface = MockEngine.new() + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + parentNode.focusController.captureFocus() + expect(engineInterface.getSelection()).to.equal(rootRef:getValue()) + + parentNode.focusController.releaseFocus() + expect(engineInterface.getSelection()).to.equal(nil) + end) + end) + + describe("Input binding", function() + it("should only call bound inputs when the element is in focus", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local childNodeA, _ = addChildNode(parentNode) + local callbackSpyA = createSpy() + childNodeA.inputBindings = { + action = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, callbackSpyA.value), + } + local childNodeB, _ = addChildNode(parentNode) + local callbackSpyB = createSpy() + childNodeB.inputBindings = { + action = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, callbackSpyB.value), + } + + local mockEngine, engineInterface = MockEngine.new() + local focusControllerInternal = parentNode.focusController[InternalApi] + focusControllerInternal:initialize(engineInterface) + expect(callbackSpyA.callCount).to.equal(0) + expect(callbackSpyB.callCount).to.equal(0) + + childNodeA:focus() + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + expect(callbackSpyA.callCount).to.equal(1) + expect(callbackSpyB.callCount).to.equal(0) + + childNodeB:focus() + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + expect(callbackSpyA.callCount).to.equal(1) + expect(callbackSpyB.callCount).to.equal(1) + end) + + it("should allow input bindings to override parent input bindings", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + local callbackSpyParent = createSpy() + parentNode.inputBindings = { + action = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, callbackSpyParent.value), + } + + -- ChildA does not override the parent's binding + local childNodeA, _ = addChildNode(parentNode) + + -- ChildB has a binding to the same button, which overrides the parent + local childNodeB, _ = addChildNode(parentNode) + local callbackSpyChild = createSpy() + childNodeB.inputBindings = { + action = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, callbackSpyChild.value), + } + + local mockEngine, engineInterface = MockEngine.new() + local focusControllerInternal = parentNode.focusController[InternalApi] + focusControllerInternal:initialize(engineInterface) + expect(callbackSpyParent.callCount).to.equal(0) + expect(callbackSpyChild.callCount).to.equal(0) + + -- When A is focused, we should use the parent's input binding + childNodeA:focus() + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + expect(callbackSpyParent.callCount).to.equal(1) + expect(callbackSpyChild.callCount).to.equal(0) + + -- When B is focused, we should use child B's input binding + childNodeB:focus() + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + expect(callbackSpyParent.callCount).to.equal(1) + expect(callbackSpyChild.callCount).to.equal(1) + end) + + it("should override onStep bindings the same as begin and end", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + local callbackSpyParent = createSpy() + parentNode.inputBindings = { + action = Input.PublicInterface.onStep(Enum.KeyCode.ButtonX, callbackSpyParent.value), + } + + -- ChildA does not override the parent's binding + local childNodeA, _ = addChildNode(parentNode) + + -- ChildB has a binding to the same button, which overrides the parent + local childNodeB, _ = addChildNode(parentNode) + local callbackSpyChild = createSpy() + childNodeB.inputBindings = { + action = Input.PublicInterface.onStep(Enum.KeyCode.ButtonX, callbackSpyChild.value), + } + + local mockEngine, engineInterface = MockEngine.new() + local focusControllerInternal = parentNode.focusController[InternalApi] + focusControllerInternal:initialize(engineInterface) + expect(callbackSpyParent.callCount).to.equal(0) + expect(callbackSpyChild.callCount).to.equal(0) + + -- When A is focused, we should use the parent's input binding + childNodeA:focus() + mockEngine:renderStep(0.03) + expect(callbackSpyParent.callCount).to.equal(1) + expect(callbackSpyChild.callCount).to.equal(0) + + -- When B is focused, both it and the parent's binding run + childNodeB:focus() + mockEngine:renderStep(0.03) + expect(callbackSpyParent.callCount).to.equal(1) + expect(callbackSpyChild.callCount).to.equal(1) + end) + + it("should override onMoveStep bindings wholesale, since they don't differ by keycode", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + local callbackSpyParent = createSpy() + parentNode.inputBindings = { + action = Input.PublicInterface.onMoveStep(callbackSpyParent.value), + } + + -- ChildA does not override the parent's binding + local childNodeA, _ = addChildNode(parentNode) + + -- ChildB has a binding to the same button, which overrides the parent + local childNodeB, _ = addChildNode(parentNode) + local callbackSpyChild = createSpy() + childNodeB.inputBindings = { + action = Input.PublicInterface.onMoveStep(callbackSpyChild.value), + } + + local mockEngine, engineInterface = MockEngine.new() + local focusControllerInternal = parentNode.focusController[InternalApi] + focusControllerInternal:initialize(engineInterface) + expect(callbackSpyParent.callCount).to.equal(0) + expect(callbackSpyChild.callCount).to.equal(0) + + -- When A is focused, we should use the parent's input binding + childNodeA:focus() + mockEngine:renderStep(0.03) + expect(callbackSpyParent.callCount).to.equal(1) + expect(callbackSpyChild.callCount).to.equal(0) + + -- When B is focused, its binding is run instead + childNodeB:focus() + mockEngine:renderStep(0.03) + expect(callbackSpyParent.callCount).to.equal(1) + expect(callbackSpyChild.callCount).to.equal(1) + end) + + it("should update input bindings when focus changes", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local boundInputsChangedSpy = createSpy() + local disconnect = parentNode.focusController.subscribeToBoundInputsChanged(boundInputsChangedSpy.value) + + local childNodeA, _ = addChildNode(parentNode) + childNodeA.inputBindings = { + action = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, function() end, { + key = "actionX" + }), + } + local childNodeB, _ = addChildNode(parentNode) + childNodeB.inputBindings = { + action1 = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonY, function() end, { + key = "actionY" + }), + action2 = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonA, function() end), + } + + local _, engineInterface = MockEngine.new() + local focusControllerInternal = parentNode.focusController[InternalApi] + focusControllerInternal:initialize(engineInterface) + + expect(boundInputsChangedSpy.callCount).to.equal(0) + childNodeA:focus() + expect(boundInputsChangedSpy.callCount).to.equal(1) + + local boundInputs = parentNode.focusController.getBoundInputs() + expect(boundInputs[Enum.KeyCode.ButtonY]).to.equal(nil) + expect(boundInputs[Enum.KeyCode.ButtonA]).to.equal(nil) + expect(boundInputs[Enum.KeyCode.ButtonX].key).to.equal("actionX") + + childNodeB:focus() + expect(boundInputsChangedSpy.callCount).to.equal(2) + + boundInputs = parentNode.focusController.getBoundInputs() + expect(boundInputs[Enum.KeyCode.ButtonY].key).to.equal("actionY") + expect(boundInputs[Enum.KeyCode.ButtonX]).to.equal(nil) + -- expect a binding without a meta table field to return an empty table rather than nil + expect(#boundInputs[Enum.KeyCode.ButtonA]).to.equal(0) + + disconnect() + childNodeA:focus() + expect(boundInputsChangedSpy.callCount).to.equal(2) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusControllerInternalApi.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusControllerInternalApi.lua new file mode 100644 index 0000000..16b8ed0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusControllerInternalApi.lua @@ -0,0 +1,3 @@ +local Symbol = require(script.Parent.Symbol) + +return Symbol.named("FocusControllerInternalApi") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusNode.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusNode.lua new file mode 100644 index 0000000..d9056a0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusNode.lua @@ -0,0 +1,164 @@ +--!nonstrict +-- Manages groups of selectable elements, reacting to selection changes for +-- individual items and triggering events for group selection changes +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) + +local InternalApi = require(script.Parent.FocusControllerInternalApi) + +local FocusNode = {} +FocusNode.__index = FocusNode + +function FocusNode.new(navProps) + local focusController + if navProps.parentFocusNode ~= nil then + focusController = navProps.parentFocusNode.focusController + elseif navProps.focusController ~= nil then + focusController = navProps.focusController + else + -- FIXME: do we ever even hit this? + error("Cannot create node without focus manager") + end + + local self = setmetatable({ + focusController = focusController, + ref = navProps[Roact.Ref], + + lastFocused = nil, + }, FocusNode) + + self:updateNavProps(navProps) + + return self +end + +function FocusNode:__getFocusControllerInternal() + return self.focusController[InternalApi] +end + +function FocusNode:__findDefaultChildNode() + local lowestLayoutOrder = math.huge + local lowestLayoutOrderChild = nil + + local focusController = self:__getFocusControllerInternal() + local children = focusController:getChildren(self) + local groupHostObject = self.ref:getValue() + + -- Iterate through all children of this node, looking for a LayoutOrder + -- associated with each node. Picking the lowest LayoutOrder is a good + -- approximation of selecting the "first" child of a given container if the + -- case that we don't have a default or a previous value to restore + for ref, child in pairs(children) do + local hostObject = ref:getValue() + + -- For each child node, determine whether it or any of its ancestors (up + -- to the group) has the LayoutOrder property defined + while hostObject ~= groupHostObject and hostObject ~= nil do + if hostObject:isA("GuiObject") then + local layoutOrder = hostObject.LayoutOrder + + -- LayoutOrder == 0 is the default value; in most cases, this + -- implies that it wasn't explicitly set, so we should ignore it + if layoutOrder ~= 0 then + if layoutOrder < lowestLayoutOrder then + lowestLayoutOrder = layoutOrder + lowestLayoutOrderChild = child + end + + -- Once we've found a layout order to associate with the + -- Focusable, we break out and move on to the next child + break + end + + end + + hostObject = hostObject.Parent + end + end + + if lowestLayoutOrderChild ~= nil then + return lowestLayoutOrderChild + else + -- If no valid target was returned, we return any valid member + local _, arbitraryChild = next(children) + + return arbitraryChild + end +end + +function FocusNode:updateNavProps(navProps) + local restorePreviousChildFocus = false + if navProps.restorePreviousChildFocus ~= nil then + restorePreviousChildFocus = navProps.restorePreviousChildFocus + end + + self.defaultChildRef = navProps.defaultChild + self.restorePreviousChildFocus = restorePreviousChildFocus + self.inputBindings = navProps.inputBindings or {} + + local focusController = self:__getFocusControllerInternal() + if focusController:isNodeFocused(self) then + focusController:updateInputBindings() + end +end + +function FocusNode:focus() + local focusController = self:__getFocusControllerInternal() + local children = focusController:getChildren(self) + if Cryo.isEmpty(children) then + focusController:setSelection(self.ref:getValue()) + else + if self.restorePreviousChildFocus and self.lastFocused ~= nil then + focusController:moveFocusTo(self.lastFocused) + elseif self.defaultChildRef ~= nil then + focusController:moveFocusTo(self.defaultChildRef) + else + local defaultChild = self:__findDefaultChildNode() + if defaultChild ~= nil then + defaultChild:focus() + end + end + end +end + +function FocusNode:attachToTree(parent, onFocusChanged) + local focusController = self:__getFocusControllerInternal() + focusController:registerNode(parent, self.ref, self) + + self.parent = parent + self.disconnectSelectionListener = focusController:subscribeToSelectionChange(function() + -- Perform focus management operations set up by the FocusNode's owner + local focused = focusController:isNodeFocused(self) + onFocusChanged(focused) + + if self.parent ~= nil and focused then + self.parent.lastFocused = self.ref + end + + -- Keep track of the last focused ref so that we can provide it to + -- self.props.selectionRule whenever we regain focus + local children = focusController:getChildren(self) + if not Cryo.isEmpty(children) and focused then + -- For the special-case scenario in which the ref for our group + -- gained selection, we follow any established rules to find the + -- correct member of the group to bounce selection to, managed in + -- the `focus` callback on focusController + if focusController:getSelection() == self.ref:getValue() then + self:focus() + end + end + end) +end + +function FocusNode:detachFromTree() + local focusController = self:__getFocusControllerInternal() + focusController:deregisterNode(self.parent, self.ref) + + if self.disconnectSelectionListener ~= nil then + self.disconnectSelectionListener() + self.disconnectSelectionListener = nil + end +end + +return FocusNode \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusNode.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusNode.spec.lua new file mode 100644 index 0000000..a661d05 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusNode.spec.lua @@ -0,0 +1,262 @@ +return function() + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + + local FocusNode = require(script.Parent.FocusNode) + local FocusController = require(script.Parent.FocusController) + local InternalApi = require(script.Parent.FocusControllerInternalApi) + + local MockEngine = require(script.Parent.Test.MockEngine) + + local function createRootNode(ref) + local node = FocusNode.new({ + focusController = FocusController.createPublicApiWrapper(), + [Roact.Ref] = ref, + }) + + node:attachToTree(nil, function() end) + + return node + end + + local function addChildNode(parentNode) + local instance = Instance.new("Frame") + instance.Parent = parentNode.ref:getValue() + + local childRef, _ = Roact.createBinding(instance) + local childNode = FocusNode.new({ + parentFocusNode = parentNode, + [Roact.Ref] = childRef, + }) + childNode:attachToTree(parentNode, function() end) + + return childNode, childRef + end + + describe("basic selection behavior", function() + it("should set selection to a ref when the ref's node is focused", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local focusNode = createRootNode(rootRef) + local focusController = focusNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + focusNode:focus() + expect(engineInterface.getSelection()).to.equal(rootRef:getValue()) + end) + + it("should redirect selection to a child when a non-leaf node is focused", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local _, childRef = addChildNode(parentNode) + + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + parentNode:focus() + expect(engineInterface.getSelection()).to.equal(childRef:getValue()) + end) + + it("should redirect selection to a child when a non-leaf node gains selection", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local mockEngine, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local _, childRef = addChildNode(parentNode) + + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + mockEngine:simulateSelectionChanged(rootRef:getValue()) + expect(engineInterface.getSelection()).to.equal(childRef:getValue()) + end) + end) + + describe("child auto-selection behavior", function() + it("should select the provided default when present", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local childNodeA, _ = addChildNode(parentNode) + local childNodeB, childRefB = addChildNode(parentNode) + parentNode.defaultChildRef = childRefB + + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + parentNode:focus() + expect(focusController:isNodeFocused(childNodeB)).to.equal(true) + + childNodeA:focus() + parentNode:focus() + expect(focusController:isNodeFocused(childNodeB)).to.equal(true) + end) + + it("should restore previous selection when the option is enabled", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local childNodeA, _ = addChildNode(parentNode) + local childNodeB, _ = addChildNode(parentNode) + parentNode.restorePreviousChildFocus = true + + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + childNodeA:focus() + parentNode:focus() + expect(focusController:isNodeFocused(childNodeA)).to.equal(true) + + childNodeB:focus() + parentNode:focus() + expect(focusController:isNodeFocused(childNodeB)).to.equal(true) + end) + + it("should defer to the default child ref when no child was previously focused", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local childNodeA, _ = addChildNode(parentNode) + local childNodeB, _ = addChildNode(parentNode) + local childNodeC, childRefC = addChildNode(parentNode) + parentNode.defaultChildRef = childRefC + parentNode.restorePreviousChildFocus = true + + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + parentNode:focus() + expect(focusController:isNodeFocused(childNodeC)).to.equal(true) + + childNodeA:focus() + childNodeB:focus() + parentNode:focus() + expect(focusController:isNodeFocused(childNodeB)).to.equal(true) + end) + end) + + describe("initial selection logic when nothing is specified", function() + local function addChildNested(parentNode, parentInstance) + local instance = Instance.new("Frame") + instance.Parent = parentInstance + + local childRef, _ = Roact.createBinding(instance) + local childNode = FocusNode.new({ + parentFocusNode = parentNode, + [Roact.Ref] = childRef, + }) + childNode:attachToTree(parentNode, function() end) + + return childNode, childRef + end + + it("should choose the child with the lowest LayoutOrder", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + local childNode1, childRef1 = addChildNode(parentNode) + childRef1:getValue().LayoutOrder = 1 + local _, childRef2 = addChildNode(parentNode) + childRef2:getValue().LayoutOrder = 2 + local _, childRef3 = addChildNode(parentNode) + childRef3:getValue().LayoutOrder = 3 + + parentNode:focus() + expect(focusController:isNodeFocused(childNode1)).to.equal(true) + end) + + it("should choose the lowest layout order even of nested elements", function() + local function insertAncestor(instance) + local newParent = Instance.new("Frame") + local grandparent = instance.Parent + instance.Parent = newParent + newParent.Parent = grandparent + end + + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + local childNode1, childRef1 = addChildNode(parentNode) + insertAncestor(childRef1:getValue()) + childRef1:getValue().Parent.LayoutOrder = 1 + local _, childRef2 = addChildNode(parentNode) + insertAncestor(childRef2:getValue()) + childRef2:getValue().Parent.LayoutOrder = 2 + local _, childRef3 = addChildNode(parentNode) + insertAncestor(childRef3:getValue()) + childRef3:getValue().Parent.LayoutOrder = 3 + + parentNode:focus() + expect(focusController:isNodeFocused(childNode1)).to.equal(true) + end) + + it("should only use the LayoutOrder closest to each child", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + -- Insert an intermediate frame with LayoutOrder = 1; we want to + -- make sure not to traverse back up to this and choose the wrong + -- child + local intermediateFrame = Instance.new("Frame") + intermediateFrame.Parent = rootRef:getValue() + intermediateFrame.LayoutOrder = 1 + + local childNode1, childRef1 = addChildNested(parentNode, intermediateFrame) + childRef1:getValue().LayoutOrder = 2 + local _, childRef2 = addChildNested(parentNode, intermediateFrame) + childRef2:getValue().LayoutOrder = 3 + local _, childRef3 = addChildNested(parentNode, intermediateFrame) + childRef3:getValue().LayoutOrder = 4 + + parentNode:focus() + expect(focusController:isNodeFocused(childNode1)).to.equal(true) + end) + + it("should safely skip past non-GuiObjects", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + -- Insert an intermediate frame with LayoutOrder = 1; we want to + -- make sure not to traverse back up to this and choose the wrong + -- child + local intermediateFolder = Instance.new("Folder") + intermediateFolder.Parent = rootRef:getValue() + + local childNode1, childRef1 = addChildNested(parentNode, intermediateFolder) + childRef1:getValue().LayoutOrder = 1 + local _, childRef2 = addChildNested(parentNode, intermediateFolder) + childRef2:getValue().LayoutOrder = 2 + + -- None specified; this one will have to climb to the tree + local _, _ = addChildNested(parentNode, intermediateFolder) + + expect(function() + parentNode:focus() + end).never.to.throw() + + expect(focusController:isNodeFocused(childNode1)).to.equal(true) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Input.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Input.lua new file mode 100644 index 0000000..1855fee --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Input.lua @@ -0,0 +1,197 @@ +local debugPrint = require(script.Parent.debugPrint) +local InputBindingKind = require(script.Parent.InputBindingKind) + +local INPUT_TYPES = { + [Enum.UserInputType.Keyboard] = true, + [Enum.UserInputType.Gamepad1] = true, + [Enum.UserInputType.Gamepad2] = true, + [Enum.UserInputType.Gamepad3] = true, + [Enum.UserInputType.Gamepad4] = true, + [Enum.UserInputType.Gamepad5] = true, + [Enum.UserInputType.Gamepad6] = true, + [Enum.UserInputType.Gamepad7] = true, + [Enum.UserInputType.Gamepad8] = true, +} + +--[[ + In order to reduce the friction of tracking this globally, we track the + gamepad connection state of each "engine interface" individually. In + production contexts, this table will have one key-value pair. When running + tests, each test will provide it's own mock engine interface; this table + will keep them separate and prevent tests from interfering with one another +]] +local engineGamepadState = {} + +local function initializeEngineGamepadState() + return { + gamepadConnectedConnection = nil, + gamepadDisconnectedConnection = nil, + onStepConnections = 0, + primaryGamepadState = {}, + } +end + +local function getEngineState(engineInterface) + if engineGamepadState[engineInterface] == nil then + engineGamepadState[engineInterface] = initializeEngineGamepadState() + end + + return engineGamepadState[engineInterface] +end + +local function updatePrimaryGamepad(engineInterface) + local engineState = getEngineState(engineInterface) + + local primaryGamepad = Enum.UserInputType.Gamepad1 + for _, gamepadNum in ipairs(engineInterface.getNavigationGamepads()) do + if engineInterface.getGamepadConnected(gamepadNum) then + primaryGamepad = gamepadNum + break + end + end + + -- States returned by getGamepadState are mutable and updated by the engine, + -- so this table only needs to be setup once per gamepad update + local states = engineInterface.getGamepadState(primaryGamepad) + + engineState.primaryGamepadState = {} + for _, state in ipairs(states) do + engineState.primaryGamepadState[state.KeyCode] = state + end +end + +local function getInputEvent(action, matchInput) + return function(inputObject) + if matchInput(inputObject) then + debugPrint("[EVENT] Process input: ", + inputObject.KeyCode, + "-", + inputObject.UserInputState + ) + action(inputObject) + end + end +end + +local function wrapWithGamepadStateListener(engineInterface, connection) + local engineState = getEngineState(engineInterface) + + if engineState.onStepConnections == 0 then + updatePrimaryGamepad(engineInterface) + engineState.gamepadConnectedConnection = engineInterface.subscribeToGamepadConnected(function() + updatePrimaryGamepad(engineInterface) + end) + engineState.gamepadDisconnectedConnection = engineInterface.subscribeToGamepadDisconnected(function() + updatePrimaryGamepad(engineInterface) + end) + end + engineState.onStepConnections = engineState.onStepConnections + 1 + + return function() + connection:Disconnect() + + engineState.onStepConnections = engineState.onStepConnections - 1 + if engineState.onStepConnections == 0 then + engineState.gamepadConnectedConnection:Disconnect() + engineState.gamepadConnectedConnection = nil + engineState.gamepadDisconnectedConnection:Disconnect() + engineState.gamepadDisconnectedConnection = nil + end + end +end + +-- Returns a function that can be called to disconnect from the event +local function connectToEvent(binding, engineInterface) + if binding.kind == InputBindingKind.Begin then + local function matchInput(inputObject) + return INPUT_TYPES[inputObject.UserInputType] + and inputObject.UserInputState == Enum.UserInputState.Begin + and inputObject.KeyCode == binding.keyCode + end + + local connection = engineInterface.subscribeToInputBegan(getInputEvent(binding.action, matchInput)) + + return function() + connection:Disconnect() + end + elseif binding.kind == InputBindingKind.End then + local function matchInput(inputObject) + return INPUT_TYPES[inputObject.UserInputType] + and inputObject.UserInputState == Enum.UserInputState.End + and inputObject.KeyCode == binding.keyCode + end + + local connection = engineInterface.subscribeToInputEnded(getInputEvent(binding.action, matchInput)) + + return function() + connection:Disconnect() + end + elseif binding.kind == InputBindingKind.Step then + local engineState = getEngineState(engineInterface) + + local connection = engineInterface.subscribeToRenderStepped(function(step) + debugPrint("[EVENT] Render step triggered onStep callback") + binding.action(engineState.primaryGamepadState[binding.keyCode], step) + end) + + return wrapWithGamepadStateListener(engineInterface, connection) + elseif binding.kind == InputBindingKind.MoveStep then + local engineState = getEngineState(engineInterface) + + local connection = engineInterface.subscribeToRenderStepped(function(step) + debugPrint("[EVENT] Render step triggered onMoveStep callback") + local moveState = { + [Enum.KeyCode.Thumbstick1] = engineState.primaryGamepadState[Enum.KeyCode.Thumbstick1], + [Enum.KeyCode.DPadUp] = engineState.primaryGamepadState[Enum.KeyCode.DPadUp], + [Enum.KeyCode.DPadDown] = engineState.primaryGamepadState[Enum.KeyCode.DPadDown], + [Enum.KeyCode.DPadLeft] = engineState.primaryGamepadState[Enum.KeyCode.DPadLeft], + [Enum.KeyCode.DPadRight] = engineState.primaryGamepadState[Enum.KeyCode.DPadRight], + } + + binding.action(moveState, step) + end) + + return wrapWithGamepadStateListener(engineInterface, connection) + end +end + +local function makeInputBinding(kind) + return function(keyCode, action, meta) + assert(typeof(keyCode) == "EnumItem" and keyCode.EnumType == Enum.KeyCode, + "Invalid argument #1: expected a member of Enum.KeyCode") + assert(typeof(action) == "function", "Invalid argument #2: expected a function") + + return { + kind = kind, + keyCode = keyCode, + action = action, + meta = meta, + } + end +end + +local function onMoveStepInputBinding(action) + return { + kind = InputBindingKind.MoveStep, + action = action, + } +end + +local function getUniqueKey(binding) + if binding.keyCode then + return tostring(binding.kind) .. "-" .. tostring(binding.keyCode) + else + return tostring(binding.kind) + end +end + +return { + getUniqueKey = getUniqueKey, + connectToEvent = connectToEvent, + PublicInterface = { + onBegin = makeInputBinding(InputBindingKind.Begin), + onEnd = makeInputBinding(InputBindingKind.End), + onStep = makeInputBinding(InputBindingKind.Step), + onMoveStep = onMoveStepInputBinding, + } +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Input.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Input.spec.lua new file mode 100644 index 0000000..fdd1dff --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Input.spec.lua @@ -0,0 +1,312 @@ +return function() + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + + local Input = require(script.Parent.Input) + local createSpy = require(script.Parent.Test.createSpy) + local MockEngine = require(script.Parent.Test.MockEngine) + + describe("onBegin and onEnd", function() + it("should run respond to relevant events when onBegin is connected", function() + local mockEngine, engineInterface = MockEngine.new() + + local onBeginSpy = createSpy() + local onBegin = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonA, onBeginSpy.value) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.Begin, + }) + + expect(onBeginSpy.callCount).to.equal(0) + + local disconnector = Input.connectToEvent(onBegin, engineInterface) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.Begin, + }) + + expect(onBeginSpy.callCount).to.equal(1) + local captured = onBeginSpy:captureValues("inputObject") + + expect(captured.inputObject.KeyCode).to.equal(Enum.KeyCode.ButtonA) + expect(captured.inputObject.UserInputState).to.equal(Enum.UserInputState.Begin) + + -- When state is `End`, the callback shouldn't be called + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.End, + }) + expect(onBeginSpy.callCount).to.equal(1) + + -- After disconnecting, the callback shouldn't be called + disconnector() + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.Begin, + }) + expect(onBeginSpy.callCount).to.equal(1) + end) + + it("should run respond to relevant events when onEnd is connected", function() + local mockEngine, engineInterface = MockEngine.new() + + local onEndSpy = createSpy() + local onEnd = Input.PublicInterface.onEnd(Enum.KeyCode.ButtonA, onEndSpy.value) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.End, + }) + + expect(onEndSpy.callCount).to.equal(0) + + local disconnector = Input.connectToEvent(onEnd, engineInterface) + + -- When state is `Begin`, the callback shouldn't be called + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.Begin, + }) + expect(onEndSpy.callCount).to.equal(0) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.End, + }) + + expect(onEndSpy.callCount).to.equal(1) + local captured = onEndSpy:captureValues("inputObject") + + expect(captured.inputObject.KeyCode).to.equal(Enum.KeyCode.ButtonA) + expect(captured.inputObject.UserInputState).to.equal(Enum.UserInputState.End) + + -- After disconnecting, the callback shouldn't be called + disconnector() + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.End, + }) + expect(onEndSpy.callCount).to.equal(1) + end) + + it("should ignore events with unrelated KeyCodes", function() + local mockEngine, engineInterface = MockEngine.new() + + local onBeginSpy = createSpy() + local onBegin = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonA, onBeginSpy.value) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.End, + }) + + expect(onBeginSpy.callCount).to.equal(0) + + local disconnector = Input.connectToEvent(onBegin, engineInterface) + + -- When the KeyCode does not match, the callback won't be fired + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonB, + UserInputState = Enum.UserInputState.Begin, + }) + expect(onBeginSpy.callCount).to.equal(0) + + disconnector() + end) + + it("should keep a separate notion of gamepad state for each 'engine interface', for testing purposes", function() + local mockEngine1, engineInterface1 = MockEngine.new() + local mockEngine2, engineInterface2 = MockEngine.new() + + local onStepSpy1 = createSpy() + local onStep1 = Input.PublicInterface.onStep(Enum.KeyCode.Thumbstick1, onStepSpy1.value) + local disconnector1 = Input.connectToEvent(onStep1, engineInterface1) + + local onStepSpy2 = createSpy() + local onStep2 = Input.PublicInterface.onStep(Enum.KeyCode.Thumbstick1, onStepSpy2.value) + local disconnector2 = Input.connectToEvent(onStep2, engineInterface2) + + mockEngine1:simulateInput({ + KeyCode = Enum.KeyCode.Thumbstick1, + UserInputState = Enum.UserInputState.Begin, + Delta = Vector3.new(1, 1, 0), + }) + mockEngine1:renderStep(0.03) + + expect(onStepSpy1.callCount).to.equal(1) + expect(onStepSpy2.callCount).to.equal(0) + + local capturedValues1 = onStepSpy1:captureValues("inputObject", "_") + local thumbstickValue1 = capturedValues1.inputObject + expect(thumbstickValue1.Delta).to.equal(Vector3.new(1, 1, 0)) + + mockEngine2:simulateInput({ + KeyCode = Enum.KeyCode.Thumbstick1, + UserInputState = Enum.UserInputState.Begin, + Delta = Vector3.new(-1, -1, 0), + }) + mockEngine2:renderStep(0.03) + + expect(onStepSpy1.callCount).to.equal(1) + expect(onStepSpy2.callCount).to.equal(1) + + local capturedValues2 = onStepSpy2:captureValues("inputObject", "_") + local thumbstickValue2 = capturedValues2.inputObject + + -- Verify that the first one didn't change + expect(thumbstickValue1.Delta).to.equal(Vector3.new(1, 1, 0)) + expect(thumbstickValue2.Delta).to.equal(Vector3.new(-1, -1, 0)) + + disconnector1() + disconnector2() + end) + end) + + describe("onStep", function() + it("should fire once per render step when connected", function() + local mockEngine, engineInterface = MockEngine.new() + + local onStepSpy = createSpy() + local onStep = Input.PublicInterface.onStep(Enum.KeyCode.ButtonA, onStepSpy.value) + + expect(onStepSpy.callCount).to.equal(0) + local disconnector = Input.connectToEvent(onStep, engineInterface) + + mockEngine:renderStep(0.1) + + expect(onStepSpy.callCount).to.equal(1) + local captured = onStepSpy:captureValues("_inputObjects", "deltaTime") + expect(captured.deltaTime).to.equal(0.1) + + disconnector() + end) + + it("should accept a keyCode and provide a relevant inputObject to the callback", function() + local mockEngine, engineInterface = MockEngine.new() + + local onStepSpy = createSpy() + local onStep = Input.PublicInterface.onStep(Enum.KeyCode.Thumbstick1, onStepSpy.value) + + expect(onStepSpy.callCount).to.equal(0) + local disconnector = Input.connectToEvent(onStep, engineInterface) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.Thumbstick1, + UserInputState = Enum.UserInputState.Change, + Position = Vector3.new(0, 0, 0), + Delta = Vector3.new(0, 0, 0), + }) + mockEngine:renderStep(0.1) + + expect(onStepSpy.callCount).to.equal(1) + local captured = onStepSpy:captureValues("inputObject", "_deltaTime") + expect(captured.inputObject.KeyCode).to.equal(Enum.KeyCode.Thumbstick1) + expect(captured.inputObject.UserInputState).to.equal(Enum.UserInputState.Change) + expect(captured.inputObject.Position).to.equal(Vector3.new(0, 0, 0)) + expect(captured.inputObject.Delta).to.equal(Vector3.new(0, 0, 0)) + + disconnector() + end) + + it("should provide an up-to-date inputObject to the callback", function() + local mockEngine, engineInterface = MockEngine.new() + + local onStepSpy = createSpy() + local onStep = Input.PublicInterface.onStep(Enum.KeyCode.Thumbstick1, onStepSpy.value) + + expect(onStepSpy.callCount).to.equal(0) + local disconnector = Input.connectToEvent(onStep, engineInterface) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.Thumbstick1, + UserInputState = Enum.UserInputState.Change, + Position = Vector3.new(0, 0, 0), + Delta = Vector3.new(0, 0, 0), + }) + mockEngine:renderStep(0.1) + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.Thumbstick1, + UserInputState = Enum.UserInputState.End, + Position = Vector3.new(1, 0, 0), + Delta = Vector3.new(1, 0, 0), + }) + mockEngine:renderStep(0.1) + + expect(onStepSpy.callCount).to.equal(2) + local captured = onStepSpy:captureValues("inputObject", "_deltaTime") + expect(captured.inputObject.KeyCode).to.equal(Enum.KeyCode.Thumbstick1) + expect(captured.inputObject.UserInputState).to.equal(Enum.UserInputState.End) + expect(captured.inputObject.Position).to.equal(Vector3.new(1, 0, 0)) + expect(captured.inputObject.Delta).to.equal(Vector3.new(1, 0, 0)) + + disconnector() + end) + end) + + describe("onMoveStep", function() + local function simulateSeveralInputs(mockEngine, state, keyCodes) + for _, keyCode in ipairs(keyCodes) do + mockEngine:simulateInput({ + KeyCode = keyCode, + UserInputState = state, + }) + end + end + + it("should reflect the state of each input at each render step", function() + local mockEngine, engineInterface = MockEngine.new() + + local onMoveStepSpy = createSpy() + local onStep = Input.PublicInterface.onMoveStep(onMoveStepSpy.value) + + expect(onMoveStepSpy.callCount).to.equal(0) + local disconnector = Input.connectToEvent(onStep, engineInterface) + + simulateSeveralInputs(mockEngine, Enum.UserInputState.Begin, { + Enum.KeyCode.Thumbstick1, + Enum.KeyCode.DPadUp, + Enum.KeyCode.DPadDown, + Enum.KeyCode.DPadLeft, + Enum.KeyCode.DPadRight, + }) + mockEngine:renderStep(0.1) + + expect(onMoveStepSpy.callCount).to.equal(1) + + local captured = onMoveStepSpy:captureValues("inputObjects", "_deltaTime") + local captured = { + Thumbstick1 = captured.inputObjects[Enum.KeyCode.Thumbstick1], + DPadUp = captured.inputObjects[Enum.KeyCode.DPadUp], + DPadDown = captured.inputObjects[Enum.KeyCode.DPadDown], + DPadLeft = captured.inputObjects[Enum.KeyCode.DPadLeft], + DPadRight = captured.inputObjects[Enum.KeyCode.DPadRight], + } + expect(captured.Thumbstick1.UserInputState).to.equal(Enum.UserInputState.Begin) + expect(captured.DPadUp.UserInputState).to.equal(Enum.UserInputState.Begin) + expect(captured.DPadDown.UserInputState).to.equal(Enum.UserInputState.Begin) + expect(captured.DPadLeft.UserInputState).to.equal(Enum.UserInputState.Begin) + expect(captured.DPadRight.UserInputState).to.equal(Enum.UserInputState.Begin) + + simulateSeveralInputs(mockEngine, Enum.UserInputState.Change, { + Enum.KeyCode.Thumbstick1, + Enum.KeyCode.DPadUp, + Enum.KeyCode.DPadDown, + Enum.KeyCode.DPadLeft, + Enum.KeyCode.DPadRight, + }) + mockEngine:renderStep(0.1) + + -- Shouldn't need to recapture, since the inputObjects themselves + -- are being mutated directly by the engine + expect(captured.Thumbstick1.UserInputState).to.equal(Enum.UserInputState.Change) + expect(captured.DPadUp.UserInputState).to.equal(Enum.UserInputState.Change) + expect(captured.DPadDown.UserInputState).to.equal(Enum.UserInputState.Change) + expect(captured.DPadLeft.UserInputState).to.equal(Enum.UserInputState.Change) + expect(captured.DPadRight.UserInputState).to.equal(Enum.UserInputState.Change) + + disconnector() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/InputBindingKind.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/InputBindingKind.lua new file mode 100644 index 0000000..b95dc1f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/InputBindingKind.lua @@ -0,0 +1,9 @@ +local Packages = script.Parent.Parent +local enumerate = require(Packages.enumerate) + +return enumerate("InputBindingKind", { + "Begin", + "End", + "Step", + "MoveStep", +}) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Symbol.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Symbol.lua new file mode 100644 index 0000000..305d66a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Symbol.lua @@ -0,0 +1,30 @@ +--[[ + A 'Symbol' is an opaque marker type. + + Symbols have the type 'userdata', but when printed to the console, the name + of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/MockEngine.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/MockEngine.lua new file mode 100644 index 0000000..de61dbe --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/MockEngine.lua @@ -0,0 +1,246 @@ +local Packages = script.Parent.Parent.Parent +local Cryo = require(Packages.Cryo) + +local createSignal = require(script.Parent.Parent.createSignal) + +local MockEngine = {} +MockEngine.__index = MockEngine + +local VALID_INPUT_TYPES = { + [Enum.UserInputType.Gamepad1] = true, + [Enum.UserInputType.Gamepad2] = true, + [Enum.UserInputType.Gamepad3] = true, + [Enum.UserInputType.Gamepad4] = true, + [Enum.UserInputType.Gamepad5] = true, + [Enum.UserInputType.Gamepad6] = true, + [Enum.UserInputType.Gamepad7] = true, + [Enum.UserInputType.Gamepad8] = true, + [Enum.UserInputType.Keyboard] = true, +} + +local GAMEPAD_STATES = { + Enum.KeyCode.Thumbstick2, + Enum.KeyCode.DPadDown, + Enum.KeyCode.DPadUp, + Enum.KeyCode.ButtonL3, + Enum.KeyCode.ButtonL2, + Enum.KeyCode.DPadRight, + Enum.KeyCode.ButtonR1, + Enum.KeyCode.ButtonSelect, + Enum.KeyCode.ButtonStart, + Enum.KeyCode.ButtonY, + Enum.KeyCode.DPadLeft, + Enum.KeyCode.ButtonR2, + Enum.KeyCode.ButtonR3, + Enum.KeyCode.ButtonX, + Enum.KeyCode.Thumbstick1, + Enum.KeyCode.ButtonB, + Enum.KeyCode.ButtonA, + Enum.KeyCode.ButtonL1, +} + +local DIRECTIONAL_INPUTS = { + [Enum.KeyCode.DPadUp] = "NextSelectionUp", + [Enum.KeyCode.Up] = "NextSelectionUp", + [Enum.KeyCode.DPadDown] = "NextSelectionDown", + [Enum.KeyCode.Down] = "NextSelectionDown", + [Enum.KeyCode.DPadLeft] = "NextSelectionLeft", + [Enum.KeyCode.Left] = "NextSelectionLeft", + [Enum.KeyCode.DPadRight] = "NextSelectionRight", + [Enum.KeyCode.Right] = "NextSelectionRight", +} + +local function defaultInputObject(keyCode) + return { + KeyCode = keyCode, + + Delta = Vector3.new(), + Position = Vector3.new(), + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.End, + } +end + +local function wrapDisconnector(disconnect) + return { + Disconnect = disconnect + } +end + +function MockEngine.new() + local self = setmetatable({ + -- Mock engine state + __mockSelected = nil, + __connectedGamepads = {}, + __gamepadStates = {}, + + -- Signals that represent engine events firing + __selectionChangedSignal = createSignal(), + __inputSignal = createSignal(), + __renderStepped = createSignal(), + __gamepadConnected = createSignal(), + __gamepadDisconnected = createSignal(), + + __interface = nil, + }, MockEngine) + + local mockInterface = { + getSelection = function() + return self.__mockSelected + end, + setSelection = function(selectionTarget) + self.__mockSelected = selectionTarget + + -- Since this isn't driven by the engine, manually fire the signal + self.__selectionChangedSignal:fire() + end, + getGamepadConnected = function(id) + return self.__connectedGamepads[id] or false + end, + getGamepadState = function(id) + if self.__gamepadStates[id] == nil then + self:__initializeGamepadState(id) + end + + -- To mimic the real engine behavior, this returns a table that can + -- continue to be mutated by the mock engine + return self.__gamepadStates[id] + end, + getNavigationGamepads = function(id) + return Cryo.Dictionary.keys(self.__connectedGamepads) + end, + subscribeToSelectionChanged = function(callback) + local disconnector = self.__selectionChangedSignal:subscribe(callback) + + return wrapDisconnector(disconnector) + end, + subscribeToInputBegan = function(callback) + local disconnector = self.__inputSignal:subscribe(callback) + + return wrapDisconnector(disconnector) + end, + subscribeToInputChanged = function(callback) + local disconnector = self.__inputSignal:subscribe(callback) + + return wrapDisconnector(disconnector) + end, + subscribeToInputEnded = function(callback) + local disconnector = self.__inputSignal:subscribe(callback) + + return wrapDisconnector(disconnector) + end, + subscribeToRenderStepped = function(callback) + local disconnector = self.__renderStepped:subscribe(callback) + + return wrapDisconnector(disconnector) + end, + subscribeToGamepadConnected = function(callback) + local disconnector = self.__gamepadConnected:subscribe(callback) + + return wrapDisconnector(disconnector) + end, + subscribeToGamepadDisconnected = function(callback) + local disconnector = self.__gamepadDisconnected:subscribe(callback) + + return wrapDisconnector(disconnector) + end, + } + + self.__interface = mockInterface + + return self, mockInterface +end + +function MockEngine:__initializeGamepadState(id) + self.__gamepadStates[id] = Cryo.List.map(GAMEPAD_STATES, function(keyCode) + return defaultInputObject(keyCode) + end) +end + +function MockEngine:simulateSelectionChanged(selectionTarget) + self.__mockSelected = selectionTarget + self.__selectionChangedSignal:fire() +end + +function MockEngine:simulateInput(inputObject) + -- Simplify the input object so that we don't always need to provide all the + -- values + local inputObject = Cryo.Dictionary.join({ + Delta = Vector3.new(), + Position = Vector3.new(), + UserInputType = Enum.UserInputType.Gamepad1, + }, inputObject) + + assert(typeof(inputObject.KeyCode) == "EnumItem" and inputObject.KeyCode.EnumType == Enum.KeyCode, + "Invalid inputObject.KeyCode: expected a member of Enum.KeyCode") + assert(VALID_INPUT_TYPES[inputObject.UserInputType], "Invalid inputObject.UserInputType") + + -- Simulate navigational actions by jumping to relevant neighbors + local keyCode = inputObject.KeyCode + local neighborProperty = DIRECTIONAL_INPUTS[keyCode] + + -- To mimic engine behavior, this happens *before* the input signal is + -- processed + if neighborProperty ~= nil and self.__interface.getSelection() ~= nil then + self.__interface.setSelection(self.__interface.getSelection()[neighborProperty]) + end + + -- For gamepad inputs, update the gamepad state InputObject + if inputObject.UserInputType ~= Enum.UserInputType.Keyboard then + local gamepadId = inputObject.UserInputType + + -- As a shorthand, if the originating gamepad for this simulated input + -- isn't connected yet, we first simulate connecting that gamepad + if not self.__connectedGamepads[gamepadId] then + self:connectGamepad(gamepadId) + end + + local gamepadState = self.__gamepadStates[gamepadId] + local index = Cryo.List.findWhere(gamepadState, function(state) + return state.KeyCode == keyCode + end) + + if index == nil then + error(("Invalid InputObject: KeyCode %s is not possible on %s"):format( + tostring(keyCode), + tostring(gamepadId) + ), 2) + end + + for key, value in pairs(inputObject) do + gamepadState[index][key] = value + end + end + + -- Pass on input by firing the input signal with an approximation of a + -- InputObject + self.__inputSignal:fire(inputObject) +end + +function MockEngine:renderStep(deltaTime) + if deltaTime == nil then + deltaTime = 1 / 30 + end + + self.__renderStepped:fire(deltaTime) +end + +function MockEngine:connectGamepad(id) + assert(typeof(id) == "EnumItem" and id.EnumType == Enum.UserInputType, + "Invalid argument #1: expected a member of Enum.UserInputType") + + self.__connectedGamepads[id] = true + self:__initializeGamepadState(id) + + self.__gamepadConnected:fire(id) +end + +function MockEngine:disconnectGamepad(id) + assert(typeof(id) == "EnumItem" and id.EnumType == Enum.UserInputType, + "Invalid argument #1: expected a member of Enum.UserInputType") + + self.__connectedGamepads[id] = nil + self.__gamepadDisconnected:fire(id) +end + +return MockEngine \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/MockEngine.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/MockEngine.spec.lua new file mode 100644 index 0000000..fb4b849 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/MockEngine.spec.lua @@ -0,0 +1,210 @@ +return function() + local Packages = script.Parent.Parent.Parent + local Cryo = require(Packages.Cryo) + + local MockEngine = require(script.Parent.MockEngine) + local createSpy = require(script.Parent.createSpy) + + describe("selection logic simulation", function() + it("should fire a change signal when simulating selection change", function() + local mockEngine, engineInterface = MockEngine.new() + + local selectionChangedSpy = createSpy() + engineInterface.subscribeToSelectionChanged(selectionChangedSpy.value) + + local selectionTarget = Instance.new("Frame") + mockEngine:simulateSelectionChanged(selectionTarget) + + expect(selectionChangedSpy.callCount).to.equal(1) + expect(engineInterface.getSelection()).to.equal(selectionTarget) + end) + + it("should automatically simulate selection change when simulating gamepad directional inputs", function() + local mockEngine, engineInterface = MockEngine.new() + + -- Create two UI elements that are vertical neighbors + local upper = Instance.new("Frame") + local lower = Instance.new("Frame") + upper.NextSelectionDown = lower + lower.NextSelectionUp = upper + + -- Set starting selection to the upper element of the two + engineInterface.setSelection(upper) + expect(engineInterface.getSelection()).to.equal(upper) + + local selectionChangedSpy = createSpy() + local inputBeganSpy = createSpy() + engineInterface.subscribeToSelectionChanged(selectionChangedSpy.value) + engineInterface.subscribeToInputBegan(inputBeganSpy.value) + + -- Simulate a downward input, and confirm that the expected + -- selection change occurs and selection is now on the lower element + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.DPadDown + }) + expect(inputBeganSpy.callCount).to.equal(1) + expect(selectionChangedSpy.callCount).to.equal(1) + expect(engineInterface.getSelection()).to.equal(lower) + + -- Simulate a downward input, and confirm that the expected + -- selection change occurs and selection returns to the upper + -- element + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.DPadUp + }) + expect(inputBeganSpy.callCount).to.equal(2) + expect(selectionChangedSpy.callCount).to.equal(2) + expect(engineInterface.getSelection()).to.equal(upper) + end) + + it("should automatically simulate selection change when simulating keyboard directional inputs", function() + local mockEngine, engineInterface = MockEngine.new() + + -- Create two UI elements that are vertical neighbors + local upper = Instance.new("Frame") + local lower = Instance.new("Frame") + upper.NextSelectionDown = lower + lower.NextSelectionUp = upper + + -- Set starting selection to the upper element of the two + engineInterface.setSelection(upper) + expect(engineInterface.getSelection()).to.equal(upper) + + local selectionChangedSpy = createSpy() + local inputBeganSpy = createSpy() + engineInterface.subscribeToSelectionChanged(selectionChangedSpy.value) + engineInterface.subscribeToInputBegan(inputBeganSpy.value) + + -- Simulate a downward input, and confirm that the expected + -- selection change occurs and selection is now on the lower element + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.DPadDown, + UserInputState = Enum.UserInputState.Begin, + }) + expect(inputBeganSpy.callCount).to.equal(1) + expect(selectionChangedSpy.callCount).to.equal(1) + expect(engineInterface.getSelection()).to.equal(lower) + + -- Simulate a downward input, and confirm that the expected + -- selection change occurs and selection returns to the upper + -- element + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.DPadUp, + UserInputState = Enum.UserInputState.Begin, + }) + expect(inputBeganSpy.callCount).to.equal(2) + expect(selectionChangedSpy.callCount).to.equal(2) + expect(engineInterface.getSelection()).to.equal(upper) + end) + end) + + describe("gamepad state simulation", function() + it("should fire events when gamepads are connected and disconnected", function() + local mockEngine, engineInterface = MockEngine.new() + + local gamepadConnectedSpy = createSpy() + local gamepadDisconnectedSpy = createSpy() + + engineInterface.subscribeToGamepadConnected(gamepadConnectedSpy.value) + engineInterface.subscribeToGamepadDisconnected(gamepadDisconnectedSpy.value) + + expect(gamepadConnectedSpy.callCount).to.equal(0) + expect(gamepadDisconnectedSpy.callCount).to.equal(0) + + local gamepad1 = Enum.UserInputType.Gamepad1 + mockEngine:connectGamepad(gamepad1) + + expect(gamepadConnectedSpy.callCount).to.equal(1) + gamepadConnectedSpy:assertCalledWith(gamepad1) + expect(gamepadDisconnectedSpy.callCount).to.equal(0) + + mockEngine:disconnectGamepad(gamepad1) + + expect(gamepadConnectedSpy.callCount).to.equal(1) + expect(gamepadDisconnectedSpy.callCount).to.equal(1) + gamepadDisconnectedSpy:assertCalledWith(gamepad1) + end) + + it("should report gamepad connection status", function() + local mockEngine, engineInterface = MockEngine.new() + + expect(engineInterface.getGamepadConnected(gamepad1)).to.equal(false) + + local gamepad1 = Enum.UserInputType.Gamepad1 + mockEngine:connectGamepad(gamepad1) + + expect(engineInterface.getGamepadConnected(gamepad1)).to.equal(true) + + mockEngine:disconnectGamepad(gamepad1) + + expect(engineInterface.getGamepadConnected(gamepad1)).to.equal(false) + end) + + it("should automatically 'connect' a gamepad when a matching input is simulated", function() + local mockEngine, engineInterface = MockEngine.new() + + expect(engineInterface.getGamepadConnected(Enum.UserInputType.Gamepad1)).to.equal(false) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.Begin, + UserInputType = Enum.UserInputType.Gamepad1, + }) + + expect(engineInterface.getGamepadConnected(Enum.UserInputType.Gamepad1)).to.equal(true) + end) + + it("should give access to current gamepad input state", function() + local mockEngine, engineInterface = MockEngine.new() + + local gamepad1 = Enum.UserInputType.Gamepad1 + mockEngine:connectGamepad(gamepad1) + + -- Check that a valid state is returned even when no inputs for the + -- given keyCode have been issued. There should be an entry for each + -- possible KeyCode (Thumbstick 1 and 2, L1/L2/L3, R1/R2/R3, + -- A/B/X/Y, Select/Start, DPad up/down/left/right), 18 total + local gamepadState = engineInterface.getGamepadState(gamepad1) + expect(#gamepadState).to.equal(18) + + local buttonAIndex = Cryo.List.findWhere(gamepadState, function(inputObject) + return inputObject.KeyCode == Enum.KeyCode.ButtonA + end) + expect(buttonAIndex).to.be.ok() + + local buttonAInputObject = gamepadState[buttonAIndex] + + -- A quick consistency check with expected default values + expect(buttonAInputObject.UserInputType).to.equal(gamepad1) + expect(buttonAInputObject.UserInputState).to.equal(Enum.UserInputState.End) + + mockEngine:simulateInput({ + UserInputType = gamepad1, + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.Begin, + }) + + -- The engine will directly mutate the InputObjects it returns from + -- `GetGamepadState`, so we simulate the same behavior here. This + -- means we can check that the input object we already saved to + -- buttonAInputObject to mutate in response to inputs + expect(buttonAInputObject.UserInputState).to.equal(Enum.UserInputState.Begin) + end) + end) + + describe("render step simulation", function() + it("should fire subscribed events when render steps are simulated", function() + local mockEngine, engineInterface = MockEngine.new() + + local renderSteppedSpy = createSpy() + engineInterface.subscribeToRenderStepped(renderSteppedSpy.value) + + expect(renderSteppedSpy.callCount).to.equal(0) + + mockEngine:renderStep(0.03) + + expect(renderSteppedSpy.callCount).to.equal(1) + renderSteppedSpy:assertCalledWith(0.03) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/createSpy.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/createSpy.lua new file mode 100644 index 0000000..3423ab5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/createSpy.lua @@ -0,0 +1,67 @@ +--[[ + A utility used to create a function spy that can be used to robustly test + that functions are invoked the correct number of times and with the correct + number of arguments. + + This should only be used in tests. +]] +local function createSpy(inner) + local self = { + callCount = 0, + values = {}, + valuesLength = 0, + } + + self.value = function(...) + self.callCount = self.callCount + 1 + self.values = {...} + self.valuesLength = select("#", ...) + + if inner ~= nil then + return inner(...) + end + + return + end + + self.assertCalledWith = function(_, ...) + local len = select("#", ...) + + if self.valuesLength ~= len then + error(("Expected %d arguments, but was called with %d arguments"):format( + self.valuesLength, + len + ), 2) + end + + for i = 1, len do + local expected = select(i, ...) + + assert(self.values[i] == expected, "value differs; got " .. tostring(self.values[i]) .. ", expected " .. tostring(expected)) + end + end + + self.captureValues = function(_, ...) + local len = select("#", ...) + local result = {} + + assert(self.valuesLength == len, "length of expected values differs from stored values") + + for i = 1, len do + local key = select(i, ...) + result[key] = self.values[i] + end + + return result + end + + setmetatable(self, { + __index = function(_, key) + error(("%q is not a valid member of spy"):format(key)) + end, + }) + + return self +end + +return createSpy \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/asFocusable.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/asFocusable.lua new file mode 100644 index 0000000..1c48de6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/asFocusable.lua @@ -0,0 +1,298 @@ +--!nonstrict +-- Manages groups of selectable elements, reacting to selection changes for +-- individual items and triggering events for group selection changes +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local FocusContext = require(script.Parent.FocusContext) +local forwardRef = require(script.Parent.forwardRef) +local FocusNode = require(script.Parent.FocusNode) +local getEngineInterface = require(script.Parent.getEngineInterface) + +local InternalApi = require(script.Parent.FocusControllerInternalApi) + +local nonHostProps = { + parentFocusNode = Cryo.None, + parentNeighbors = Cryo.None, + focusController = Cryo.None, + + onFocusGained = Cryo.None, + onFocusLost = Cryo.None, + onFocusChanged = Cryo.None, + inputBindings = Cryo.None, + + restorePreviousChildFocus = Cryo.None, + defaultChild = Cryo.None, +} + +local function checkFocusManager(props) + if props.focusController ~= nil and props.parentFocusNode ~= nil then + return false, "Cannot attach a new focusController beneath an existing one" + end + + return true +end + +local focusableValidateProps = t.intersection(t.interface({ + parentFocusNode = t.optional(t.table), + focusController = t.optional(t.table), + [Roact.Ref] = t.table, + + restorePreviousChildFocus = t.boolean, + inputBindings = t.table, + defaultChild = t.optional(t.table), + + onFocusGained = t.optional(t.callback), + onFocusLost = t.optional(t.callback), + onFocusChanged = t.optional(t.callback), +}), checkFocusManager) + +local focusableDefaultProps = { + restorePreviousChildFocus = false, + inputBindings = {}, +} + +--[[ + Identifies an instance as a focusable element or a group of focusable + elements. Injects a navigation context object that propagates its own + navigational props to any children that are also selectable. +]] +local function asFocusable(innerComponent) + local componentName = ("Focusable(%s)"):format(tostring(innerComponent)) + + -- Selection container component; groups together children and reacts to changes + -- in GuiService.SelectedObject + local Focusable = Roact.Component:extend(componentName) + + Focusable.validateProps = focusableValidateProps + Focusable.defaultProps = focusableDefaultProps + + function Focusable:init() + self.focused = false + + local parentNeighbors = self.props.parentNeighbors or {} + self.navContext = { + focusNode = FocusNode.new(self.props), + neighbors = { + NextSelectionLeft = self.props.NextSelectionLeft or parentNeighbors.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight or parentNeighbors.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp or parentNeighbors.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown or parentNeighbors.NextSelectionDown, + } + } + + self.updateFocusedState = function(newFocusedState) + if not self.focused and newFocusedState then + self:gainFocus() + elseif self.focused and not newFocusedState then + self:loseFocus() + end + end + + if self:isRoot() then + local isRooted = false + -- If this Focusable needs to behave as a root, it is responsible for + -- initializing the FocusManager. Once it becomes a descendant of + -- `game`, we initialize the FocusManager which determines which sort of + -- PlayerGui this focus tree is contained under + self.ancestryChanged = function(instance) + if not isRooted and instance:IsDescendantOf(game) then + isRooted = true + self:getFocusControllerInternal():initialize(getEngineInterface(instance)) + end + end + + -- This function is called separately, since we don't want to falsely + -- trigger an existing callback in props when we call + -- `ancestryChanged` in didMount + self.ancestryChangedListener = function(instance) + self.ancestryChanged(instance) + + local existingCallback = self.props[Roact.Event.AncestryChanged] + if existingCallback ~= nil then + existingCallback(instance) + end + end + + self.refreshFocusOnDescendantAdded = function(descendant) + self:getFocusControllerInternal():descendantAddedRefocus() + + local existingCallback = self.props[Roact.Event.DescendantAdded] + if existingCallback ~= nil then + existingCallback(descendant) + end + end + + self.refreshFocusOnDescendantRemoved = function(descendant) + self:getFocusControllerInternal():descendantRemovedRefocus() + + local existingCallback = self.props[Roact.Event.DescendantRemoving] + if existingCallback ~= nil then + existingCallback(descendant) + end + end + end + end + + function Focusable:willUpdate(nextProps) + -- Here, we need to carefully update the navigation context according to + -- the incoming props. There are three different categories of prop + -- changes we have to deal with. + + -- 1. Apply the changes from the navigation props themselves. These only + -- affect navigation behavior for this node's ref and do not need to + -- cascade to other parts of the tree + self.navContext.focusNode:updateNavProps(nextProps) + + -- 2. If neighbors changed, we need to cascade this change through + -- context, so we make sure the value that we pass to context has a + -- _new_ identity + if + nextProps.NextSelectionLeft ~= self.navContext.neighbors.NextSelectionLeft + or nextProps.NextSelectionRight ~= self.navContext.neighbors.NextSelectionRight + or nextProps.NextSelectionDown ~= self.navContext.neighbors.NextSelectionDown + or nextProps.NextSelectionUp ~= self.navContext.neighbors.NextSelectionUp + or nextProps.parentNeighbors ~= self.props.parentNeighbors + then + local parentNeighbors = nextProps.parentNeighbors or {} + self.navContext = { + focusNode = self.navContext.focusNode, + neighbors = { + NextSelectionLeft = nextProps.NextSelectionLeft or parentNeighbors.NextSelectionLeft, + NextSelectionRight = nextProps.NextSelectionRight or parentNeighbors.NextSelectionRight, + NextSelectionUp = nextProps.NextSelectionUp or parentNeighbors.NextSelectionUp, + NextSelectionDown = nextProps.NextSelectionDown or parentNeighbors.NextSelectionDown, + } + } + end + + -- 3. Finally, if the ref changed, then for now we simply get angry and + -- throw an error; we'll likely have to manage this another way + -- anyways! + if self.navContext.focusNode.ref ~= nextProps[Roact.Ref] then + error("Cannot change the ref passed to a Focusable component", 0) + end + end + + function Focusable:gainFocus() + self.focused = true + + if self.props.onFocusGained ~= nil then + self.props.onFocusGained() + end + + if self.props.onFocusChanged ~= nil then + self.props.onFocusChanged(true) + end + end + + function Focusable:loseFocus() + self.focused = false + + if self.props.onFocusLost ~= nil then + self.props.onFocusLost() + end + + if self.props.onFocusChanged ~= nil then + self.props.onFocusChanged(false) + end + end + + -- Determines whether or not this Focusable is supposed to be the root of a + -- focusable tree, determined by whether or not it has parent or focus props + -- provided + function Focusable:isRoot() + return self.props.focusController ~= nil and self.props.parentFocusNode == nil + end + + function Focusable:getFocusControllerInternal() + return self.navContext.focusNode.focusController[InternalApi] + end + + function Focusable:render() + local ref = self.props[Roact.Ref] + local childDefaultNavProps = { + NextSelectionLeft = ref, + NextSelectionRight = ref, + NextSelectionDown = ref, + NextSelectionUp = ref, + + [Roact.Ref] = ref, + } + + local innerProps + if self:isRoot() then + local rootNavProps = { + [Roact.Event.AncestryChanged] = self.ancestryChangedListener, + [Roact.Event.DescendantAdded] = self.refreshFocusOnDescendantAdded, + [Roact.Event.DescendantRemoving] = self.refreshFocusOnDescendantRemoved, + } + + innerProps = Cryo.Dictionary.join( + childDefaultNavProps, + self.props, + rootNavProps, + nonHostProps + ) + else + innerProps = Cryo.Dictionary.join( + childDefaultNavProps, + self.props.parentNeighbors or {}, + self.props, + nonHostProps + ) + end + + -- We pass the inner component as a single child (instead of part of a + -- table of children) because it causes Roact to reuse the key provided + -- to _this_ component when naming the resulting object. This means that + -- Focusable avoids disrupting the naming of the Instance hierarchy + return Roact.createElement(FocusContext.Provider, { + value = self.navContext, + }, Roact.createElement(innerComponent, innerProps)) + end + + function Focusable:didMount() + self.navContext.focusNode:attachToTree(self.props.parentFocusNode, self.updateFocusedState) + + if self:isRoot() then + -- Ancestry change may not trigger if the UI elements we're mounting + -- to were previously mounted to the DataModel already + self.ancestryChanged(self.props[Roact.Ref]:getValue()) + end + end + + function Focusable:willUnmount() + self.navContext.focusNode:detachFromTree() + + if self:isRoot() then + self:getFocusControllerInternal():teardown() + end + end + + return forwardRef(function(props, ref) + return Roact.createElement(FocusContext.Consumer, { + render = function(navContext) + if navContext == nil and props.focusController == nil then + -- If this component can't be the root, and there's no + -- parent, behave like the underlying component and ignore + -- all focus logic + local hostPropsOnly = Cryo.Dictionary.join(props, nonHostProps) + return Roact.createElement(innerComponent, hostPropsOnly) + end + + local propsWithNav = Cryo.Dictionary.join(props, { + parentFocusNode = navContext and navContext.focusNode or nil, + parentNeighbors = navContext and navContext.neighbors or nil, + [Roact.Ref] = ref, + }) + + return Roact.createElement(Focusable, propsWithNav) + end, + }) + end) +end + +return asFocusable \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/asFocusable.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/asFocusable.spec.lua new file mode 100644 index 0000000..acae647 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/asFocusable.spec.lua @@ -0,0 +1,801 @@ +return function() + local Players = game:GetService("Players") + + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + + local asFocusable = require(script.Parent.asFocusable) + local createRefCache = require(script.Parent.createRefCache) + local FocusContext = require(script.Parent.FocusContext) + local FocusNode = require(script.Parent.FocusNode) + local Input = require(script.Parent.Input) + local FocusController = require(script.Parent.FocusController) + local InternalApi = require(script.Parent.FocusControllerInternalApi) + local MockEngine = require(script.Parent.Test.MockEngine) + local createSpy = require(script.Parent.Test.createSpy) + + local function createRootNode(ref) + local node = FocusNode.new({ + focusController = FocusController.createPublicApiWrapper(), + [Roact.Ref] = ref, + }) + + node:attachToTree(nil, function() end) + + return node + end + + local function createTestContainer() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local focusNode = createRootNode(rootRef) + local mockEngine, engineInterface = MockEngine.new() + focusNode.focusController[InternalApi]:initialize(engineInterface) + + return { + rootRef = rootRef, + rootFocusNode = focusNode, + focusController = focusNode.focusController[InternalApi], + + mockEngine = mockEngine, + engineInterface = engineInterface, + getNode = function(ref) + return focusNode.focusController[InternalApi].allNodes[ref] + end, + FocusProvider = function(props) + return Roact.createElement(FocusContext.Provider, { + value = { + focusNode = focusNode, + } + }, props[Roact.Children]) + end + } + end + + describe("Focusable component basics", function() + it("adds a new node to the focus tree when it mounts", function() + local testContainer = createTestContainer() + + local FocusableFrame = asFocusable("Frame") + + local injectedRef = Roact.createRef() + local tree = Roact.mount(Roact.createElement(testContainer.FocusProvider, {}, { + FocusChild = Roact.createElement(FocusableFrame, { + [Roact.Ref] = injectedRef, + }), + })) + + local focusController = testContainer.focusController + + expect(injectedRef:getValue()).to.be.ok() + expect(focusController.allNodes[injectedRef]).to.be.ok() + + local children = focusController:getChildren(testContainer.rootFocusNode) + expect(children[injectedRef]).to.be.ok() + + Roact.unmount(tree) + end) + + it("removes nodes from the focus tree when the component unmounts", function() + local testContainer = createTestContainer() + + local FocusableFrame = asFocusable("Frame") + + local injectedRef = Roact.createRef() + local tree = Roact.mount(Roact.createElement(testContainer.FocusProvider, {}, { + FocusChild = Roact.createElement(FocusableFrame, { + [Roact.Ref] = injectedRef, + }), + })) + + -- Update the tree with the child focusable frame absent, which will + -- unmount it from the tree + Roact.update(tree, Roact.createElement(testContainer.FocusProvider)) + + local focusController = testContainer.focusController + + expect(injectedRef:getValue()).to.equal(nil) + expect(focusController.allNodes[injectedRef]).to.equal(nil) + + local children = focusController:getChildren(testContainer.rootFocusNode) + expect(children[injectedRef]).to.equal(nil) + + Roact.unmount(tree) + end) + + it("triggers callbacks when focus changes", function() + local testContainer = createTestContainer() + local FocusableFrame = asFocusable("Frame") + + local focusGainedSpy = createSpy() + local focusLostSpy = createSpy() + local focusChangedSpy = createSpy() + + local childRefA = Roact.createRef() + local childRefB = Roact.createRef() + local tree = Roact.mount(Roact.createElement(testContainer.FocusProvider, {}, { + FocusChildA = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefA, + + onFocusGained = focusGainedSpy.value, + onFocusLost = focusLostSpy.value, + onFocusChanged = focusChangedSpy.value, + }), + FocusChildB = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefB, + }) + }), testContainer.rootRef:getValue()) + + testContainer.focusController:moveFocusTo(childRefA) + + expect(focusGainedSpy.callCount).to.equal(1) + expect(focusChangedSpy.callCount).to.equal(1) + focusChangedSpy:assertCalledWith(true) + + testContainer.focusController:moveFocusTo(childRefB) + + expect(focusLostSpy.callCount).to.equal(1) + expect(focusChangedSpy.callCount).to.equal(2) + focusChangedSpy:assertCalledWith(false) + + Roact.unmount(tree) + end) + + it("triggers callbacks when focus is released", function() + local testContainer = createTestContainer() + local FocusableFrame = asFocusable("Frame") + + local focusLostSpy = createSpy() + local focusChangedSpy = createSpy() + + local childRefA = Roact.createRef() + local tree = Roact.mount(Roact.createElement(testContainer.FocusProvider, {}, { + FocusChildA = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefA, + + onFocusLost = focusLostSpy.value, + onFocusChanged = focusChangedSpy.value, + }), + }), testContainer.rootRef:getValue()) + + testContainer.focusController:moveFocusTo(childRefA) + expect(focusChangedSpy.callCount).to.equal(1) + focusChangedSpy:assertCalledWith(true) + + testContainer.focusController:releaseFocus() + + expect(focusLostSpy.callCount).to.equal(1) + expect(focusChangedSpy.callCount).to.equal(2) + focusChangedSpy:assertCalledWith(false) + + Roact.unmount(tree) + end) + end) + + describe("Root vs non-root Focusable", function() + it("should ignore focusable logic if no parent or controller is provided", function() + local FocusableFrame = asFocusable("Frame") + local focusGainedSpy = createSpy() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + onFocusGained = focusGainedSpy.value, + })) + + expect(focusGainedSpy.callCount).to.equal(0) + + Roact.unmount(tree) + end) + + it("should initialize and teardown focusController when it's the root", function() + local FocusableFrame = asFocusable("Frame") + + -- This test is testing the automatic initialization of the internal + -- focusController based on the instance tree it's attached to. To + -- do this, we depend on the using a PlayerGui instance to avoid + -- simulate the real-world use case instead of the mock engine + expect(Players.LocalPlayer.PlayerGui).to.be.ok() + + local focusController = FocusController.createPublicApiWrapper() + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + }), Players.LocalPlayer.PlayerGui) + + expect(focusController[InternalApi].engineInterface).never.to.equal(nil) + + Roact.unmount(tree) + + expect(focusController[InternalApi].engineInterface).to.equal(nil) + end) + + it("should inherit parent neighbors through multiple layers", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + local function getNode(ref) + return focusController[InternalApi].allNodes[ref] + end + + local refs = createRefCache() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + [Roact.Ref] = refs.root, + }, { + TopSelectionTarget = Roact.createElement(FocusableFrame, { + NextSelectionDown = refs.bottomFocusable, + [Roact.Ref] = refs.topFocusable, + }), + BottomSelectionTarget = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.bottomFocusable, + NextSelectionUp = refs.topFocusable, + }, { + IntermediateChild = Roact.createElement(FocusableFrame, {}, { + -- This focusable child should be able to inherit + -- neighbors from its grandparent + LeafChild = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.bottomLeaf, + }) + }), + }), + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local mockEngine, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + -- Initialize gamepad focus to the top element + local topNode = getNode(refs.topFocusable) + topNode:focus() + expect(focusControllerInternal:isNodeFocused(topNode)).to.equal(true) + + -- Move focus down; this should work without neighbor propagation + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.DPadDown, + }) + + local bottomLeafNode = getNode(refs.bottomLeaf) + expect(focusControllerInternal:isNodeFocused(bottomLeafNode)).to.equal(true) + + -- Move focus up; this only works if grandparents' neighbors get + -- passed down correctly + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.DPadUp, + }) + expect(focusControllerInternal:isNodeFocused(topNode)).to.equal(true) + + Roact.unmount(tree) + end) + end) + + -- These tests rely on the fact that a FocusController passed to a Focusable + -- component will _not_ be automatically initialized if it's not mounted + -- under a PlayerGui. We leverage this technicality to initialize it + -- ourselves with the mock engine interface. + describe("Refresh focus logic", function() + it("should redirect focus to the parent when a focused child is detached", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + local function getNode(ref) + return focusController[InternalApi].allNodes[ref] + end + + local refs = createRefCache() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + [Roact.Ref] = refs.root, + }, { + FocusChild = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.child, + }), + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local _, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + local childNode = getNode(refs.child) + childNode:focus() + expect(focusControllerInternal:isNodeFocused(childNode)).to.equal(true) + + tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + [Roact.Ref] = refs.root, + })) + + local rootNode = getNode(refs.root) + expect(focusControllerInternal:isNodeFocused(rootNode)).to.equal(true) + + Roact.unmount(tree) + end) + + it("should trigger parent focus logic when a focused child is detached", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + local function getNode(ref) + return focusController[InternalApi].allNodes[ref] + end + + local refs = createRefCache() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + FocusChildA = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.childA, + }), + FocusChildB = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.childB, + }), + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local _, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + local childNodeA = getNode(refs.childA) + childNodeA:focus() + expect(focusControllerInternal:isNodeFocused(childNodeA)).to.equal(true) + + tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + FocusChildB = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.childB, + }), + })) + + local childNodeB = getNode(refs.childB) + expect(focusControllerInternal:isNodeFocused(childNodeB)).to.equal(true) + + Roact.unmount(tree) + end) + + it("should trigger parent focus logic when a node has children added to it", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + local function getNode(ref) + return focusController[InternalApi].allNodes[ref] + end + + local refs = createRefCache() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + [Roact.Ref] = refs.root, + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local _, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + focusController.captureFocus() + local rootNode = getNode(refs.root) + expect(focusControllerInternal:isNodeFocused(rootNode)).to.equal(true) + + tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + [Roact.Ref] = refs.root, + }, { + FocusChild = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.child, + }), + })) + + local childNode = getNode(refs.child) + expect(focusControllerInternal:isNodeFocused(childNode)).to.equal(true) + + Roact.unmount(tree) + end) + + it("should not refocus when adding children to a parent that already has at least one child", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + local function getNode(ref) + return focusController[InternalApi].allNodes[ref] + end + + local childRefA = Roact.createRef() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + FocusChildA = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefA, + }), + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local _, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + local childNodeA = getNode(childRefA) + childNodeA:focus() + expect(focusControllerInternal:isNodeFocused(childNodeA)).to.equal(true) + + tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + FocusChildA = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefA, + }), + FocusChildB = Roact.createElement(FocusableFrame), + })) + + -- Focus should not have moved as a result of the above change + expect(focusControllerInternal:isNodeFocused(childNodeA)).to.equal(true) + + Roact.unmount(tree) + end) + + it("should clean up input event subscriptions when the Focusable they're bound to is detached", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + + local beginCallbackSpy, moveStepCallbackSpy = createSpy(), createSpy() + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + FocusChildA = Roact.createElement(FocusableFrame, { + inputBindings = { + Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, beginCallbackSpy.value), + Input.PublicInterface.onMoveStep(moveStepCallbackSpy.value), + } + }), + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local mockEngine, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + focusController.captureFocus() + + expect(beginCallbackSpy.callCount).to.equal(0) + expect(moveStepCallbackSpy.callCount).to.equal(0) + + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + mockEngine:renderStep() + expect(beginCallbackSpy.callCount).to.equal(1) + expect(moveStepCallbackSpy.callCount).to.equal(1) + + -- Remove the child from the tree, which will also nil its parents + -- and trigger any auto-refocusing logic + local tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + -- Child was removed + }), nil) + + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + mockEngine:renderStep() + expect(beginCallbackSpy.callCount).to.equal(1) + expect(moveStepCallbackSpy.callCount).to.equal(1) + + Roact.unmount(tree) + end) + + it("should clean up input event subscriptions when the whole tree is cleaned up", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + + local beginCallbackSpy, moveStepCallbackSpy = createSpy(), createSpy() + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + inputBindings = { + Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, beginCallbackSpy.value), + Input.PublicInterface.onMoveStep(moveStepCallbackSpy.value), + } + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local mockEngine, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + focusController.captureFocus() + expect(beginCallbackSpy.callCount).to.equal(0) + expect(moveStepCallbackSpy.callCount).to.equal(0) + + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + mockEngine:renderStep() + expect(beginCallbackSpy.callCount).to.equal(1) + expect(moveStepCallbackSpy.callCount).to.equal(1) + + -- Remove the child from the tree, which will also nil its parents + -- and trigger any auto-refocusing logic + Roact.unmount(tree) + + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + mockEngine:renderStep() + expect(beginCallbackSpy.callCount).to.equal(1) + expect(moveStepCallbackSpy.callCount).to.equal(1) + end) + end) + + describe("Component behavior", function() + it("should not replace any provided event handlers", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + + local ancestryChangedSpy = createSpy() + local descendantAddedSpy = createSpy() + local descendantRemovedSpy = createSpy() + local rootRef = Roact.createRef() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + [Roact.Ref] = rootRef, + + [Roact.Event.AncestryChanged] = ancestryChangedSpy.value, + [Roact.Event.DescendantAdded] = descendantAddedSpy.value, + [Roact.Event.DescendantRemoving] = descendantRemovedSpy.value, + }), nil) + + expect(ancestryChangedSpy.callCount).to.equal(0) + expect(descendantAddedSpy.callCount).to.equal(0) + expect(descendantRemovedSpy.callCount).to.equal(0) + + local newParentFrame = Instance.new("Frame") + rootRef:getValue().Parent = newParentFrame + expect(ancestryChangedSpy.callCount).to.equal(1) + + local newChildFrame = Instance.new("Frame") + newChildFrame.Parent = rootRef:getValue() + expect(descendantAddedSpy.callCount).to.equal(1) + + newChildFrame.Parent = nil + expect(descendantRemovedSpy.callCount).to.equal(1) + + Roact.unmount(tree) + end) + + it("should update navigation logic correctly when props update", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + + local xBindingSpy = createSpy() + local yBindingSpy = createSpy() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + inputBindings = { + onXButton = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, xBindingSpy.value), + } + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local mockEngine, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + focusController.captureFocus() + expect(xBindingSpy.callCount).to.equal(0) + expect(yBindingSpy.callCount).to.equal(0) + + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + expect(xBindingSpy.callCount).to.equal(1) + expect(yBindingSpy.callCount).to.equal(0) + + tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + inputBindings = { + onYButton = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonY, yBindingSpy.value), + } + })) + + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonY, + }) + expect(xBindingSpy.callCount).to.equal(1) + expect(yBindingSpy.callCount).to.equal(1) + + -- Pressing X again does not trigger the old callback + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + expect(xBindingSpy.callCount).to.equal(1) + + Roact.unmount(tree) + end) + + it("should propagate updates to inherited parent neighbor relationships", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + local function getNode(ref) + return focusController[InternalApi].allNodes[ref] + end + + local parentRefA = Roact.createRef() + local parentRefB = Roact.createRef() + local childRefA = Roact.createRef() + local childRefB = Roact.createRef() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + ParentA = Roact.createElement(FocusableFrame, { + NextSelectionDown = parentRefB, + [Roact.Ref] = parentRefA, + }, { + ChildA = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefA, + }) + }), + ParentB = Roact.createElement(FocusableFrame, { + -- No neighbors set + [Roact.Ref] = parentRefB, + }, { + ChildB = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefB, + }) + }), + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local mockEngine, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + local childNodeA = getNode(childRefA) + local childNodeB = getNode(childRefB) + + childNodeA:focus() + expect(focusControllerInternal:isNodeFocused(childNodeA)).to.equal(true) + + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.DPadDown, + }) + expect(focusControllerInternal:isNodeFocused(childNodeB)).to.equal(true) + + -- Try moving back up; no neighbors are set, and none are inherited + -- from the parents, so this shouldn't cause the selection to move + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.DPadUp, + }) + expect(focusControllerInternal:isNodeFocused(childNodeB)).to.equal(true) + + -- Update the tree to introduce an upward neighbor + tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + ParentA = Roact.createElement(FocusableFrame, { + NextSelectionDown = parentRefB, + [Roact.Ref] = parentRefA, + }, { + ChildA = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefA, + }) + }), + ParentB = Roact.createElement(FocusableFrame, { + NextSelectionUp = parentRefA, + [Roact.Ref] = parentRefB, + }, { + ChildB = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefB, + }) + }), + })) + + -- Make sure we're still on B like before + expect(focusControllerInternal:isNodeFocused(childNodeB)).to.equal(true) + + -- This time, moving back up should work as expected + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.DPadUp, + }) + expect(focusControllerInternal:isNodeFocused(childNodeA)).to.equal(true) + + Roact.unmount(tree) + end) + + it("should propagate updates to inherited grandparent neighbor relationships", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + local function getNode(ref) + return focusController[InternalApi].allNodes[ref] + end + + local refs = createRefCache() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + TopSelectionTarget = Roact.createElement(FocusableFrame, { + NextSelectionDown = refs.bottomFocusable, + [Roact.Ref] = refs.topFocusable, + }), + BottomSelectionTarget = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.bottomFocusable, + }, { + IntermediateChild = Roact.createElement(FocusableFrame, {}, { + LeafChild = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.bottomLeaf, + }) + }), + }), + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local mockEngine, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + local bottomLeafNode = getNode(refs.bottomLeaf) + bottomLeafNode:focus() + expect(focusControllerInternal:isNodeFocused(bottomLeafNode)).to.equal(true) + + -- This upward input will not work on this tree, since there's no + -- upward neighbor defined just yet + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.DPadUp, + }) + expect(focusControllerInternal:isNodeFocused(bottomLeafNode)).to.equal(true) + + -- Update the tree to introduce an upward neighbor + tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + TopSelectionTarget = Roact.createElement(FocusableFrame, { + NextSelectionDown = refs.bottomFocusable, + [Roact.Ref] = refs.topFocusable, + }), + BottomSelectionTarget = Roact.createElement(FocusableFrame, { + NextSelectionUp = refs.topFocusable, + [Roact.Ref] = refs.bottomFocusable, + }, { + IntermediateChild = Roact.createElement(FocusableFrame, {}, { + -- This focusable child should be able to inherit + -- neighbors from its grandparent + LeafChild = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.bottomLeaf, + }) + }), + }), + })) + + -- Make sure we're still on B like before + expect(focusControllerInternal:isNodeFocused(bottomLeafNode)).to.equal(true) + + -- This time, moving up should work as expected + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.DPadUp, + }) + local topNode = getNode(refs.topFocusable) + expect(focusControllerInternal:isNodeFocused(topNode)).to.equal(true) + + Roact.unmount(tree) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createFocusableCache.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createFocusableCache.lua new file mode 100644 index 0000000..a13d106 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createFocusableCache.lua @@ -0,0 +1,51 @@ +local asFocusable = require(script.Parent.asFocusable) + +local function checkHostProperties(component) + local instance = Instance.new(component) + + assert(instance:IsA("GuiObject")) +end + +local function isValidFocusable(component) + local componentType = typeof(component) + if componentType == "string" then + local hasHostProps, _ = pcall(checkHostProperties, component) + return hasHostProps + elseif componentType == "function" or componentType == "table" then + -- Not much else we can do here right now + return true + end + + -- All other types are invalid components anyways + return false +end + +-- Returns a table that dynamically instantiates Focusable components whenever a +-- new key is accessed. This means that any valid component can be Focusable +local function createFocusableCache() + local focusableComponentCache = {} + + setmetatable(focusableComponentCache, { + __index = function(_, key) + if not isValidFocusable(key) then + error("Component " .. tostring(key) .. " (" .. typeof(key) .. ") is not a valid focusable component", 2) + end + + local newComponent = asFocusable(key) + focusableComponentCache[key] = newComponent + + return newComponent + end, + __tostring = function(self) + local result = "{" + for key, componentClass in pairs(self) do + result = ("%s\n\t%s -> %s"):format(result, tostring(key), tostring(componentClass)) + end + return result .. "\n}" + end + }) + + return focusableComponentCache +end + +return createFocusableCache \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createFocusableCache.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createFocusableCache.spec.lua new file mode 100644 index 0000000..28b982b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createFocusableCache.spec.lua @@ -0,0 +1,42 @@ +return function() + local createFocusableCache = require(script.Parent.createFocusableCache) + + it("should always return Roact components", function() + local focusableCache = createFocusableCache() + + local keys = { "TextLabel", "Frame", "ImageLabel" } + for _, key in ipairs(keys) do + local focusableComponent = focusableCache[key] + expect(focusableComponent).never.to.equal(nil) + -- We don't really have a good way to verify types right now + expect(typeof(focusableComponent.render)).to.equal("function") + end + end) + + it("should return the same object for the same key", function() + local focusableCache = createFocusableCache() + local TextLabel = focusableCache.TextLabel + local Frame = focusableCache.Frame + + expect(TextLabel).to.equal(focusableCache.TextLabel) + expect(Frame).to.equal(focusableCache.Frame) + end) + + it("should verify (to the best of its ability) that the provided component is a viable focusable component", function() + local focusableCache = createFocusableCache() + + expect(function() + return focusableCache[1] + end).to.throw() + expect(function() + return focusableCache.Folder + end).to.throw() + + local function myFunctionComponent(props) + return nil + end + expect(function() + return focusableCache[myFunctionComponent] + end).never.to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createRefCache.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createRefCache.lua new file mode 100644 index 0000000..e1c1548 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createRefCache.lua @@ -0,0 +1,32 @@ +--[[ + This is a handy trick to allow us to reference refs before we've actually + rendered anything, and without duplicating rendering logic! +]] +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) + +-- Returns a table that dynamically instantiates refs whenever a new key is +-- accessed; helpful for building dynamic lists of elements +local function createRefCache() + local refCache = {} + + setmetatable(refCache, { + __index = function(_, key) + local newRef = Roact.createRef() + refCache[key] = newRef + + return newRef + end, + __tostring = function(self) + local result = "{" + for key, ref in pairs(self) do + result = ("%s\n\t%s -> %s"):format(result, tostring(key), tostring(ref)) + end + return result .. "\n}" + end + }) + + return refCache +end + +return createRefCache \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createRefCache.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createRefCache.spec.lua new file mode 100644 index 0000000..35adf69 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createRefCache.spec.lua @@ -0,0 +1,23 @@ +return function() + local createRefCache = require(script.Parent.createRefCache) + + it("should always return valid ref objects", function() + local refCache = createRefCache() + + local keys = { "test", "whatever", "some key" } + for _, key in ipairs(keys) do + local ref = refCache[key] + expect(ref).never.to.equal(nil) + expect(typeof(ref.getValue)).to.equal("function") + end + end) + + it("should return the same object for the same key", function() + local refCache = createRefCache() + local firstRef = refCache.firstRef + local secondRef = refCache.secondRef + + expect(firstRef).to.equal(refCache.firstRef) + expect(secondRef).to.equal(refCache.secondRef) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createSignal.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createSignal.lua new file mode 100644 index 0000000..3db6354 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createSignal.lua @@ -0,0 +1,75 @@ +--[[ + This is a simple signal implementation that has a dead-simple API. + + local signal = createSignal() + + local disconnect = signal:subscribe(function(foo) + print("Cool foo:", foo) + end) + + signal:fire("something") + + disconnect() +]] + +local function addToMap(map, addKey, addValue) + local new = {} + + for key, value in pairs(map) do + new[key] = value + end + + new[addKey] = addValue + + return new +end + +local function removeFromMap(map, removeKey) + local new = {} + + for key, value in pairs(map) do + if key ~= removeKey then + new[key] = value + end + end + + return new +end + +local function createSignal() + local connections = {} + + local function subscribe(self, callback) + assert(typeof(callback) == "function", "Can only subscribe to signals with a function.") + + local connection = { + callback = callback, + } + + connections = addToMap(connections, callback, connection) + + local function disconnect() + assert(not connection.disconnected, "Listeners can only be disconnected once.") + + connection.disconnected = true + connections = removeFromMap(connections, callback) + end + + return disconnect + end + + local function fire(self, ...) + for callback, connection in pairs(connections) do + if not connection.disconnected then + callback(...) + end + end + end + + return { + subscribe = subscribe, + fire = fire, + } +end + +return createSignal \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/debugPrint.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/debugPrint.lua new file mode 100644 index 0000000..1da7b49 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/debugPrint.lua @@ -0,0 +1,9 @@ +local DEBUG = false + +local function debugPrint(...) + if DEBUG then + print(...) + end +end + +return debugPrint \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/forwardRef.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/forwardRef.lua new file mode 100644 index 0000000..b26f139 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/forwardRef.lua @@ -0,0 +1,24 @@ +-- TODO: Consider contributing this to Roact itself +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) + +--[[ + Passed a provided ref to given render callback. Can be used to treat class + components as host components and assign the passed-in ref to the underlying + host component +]] +local function forwardRef(render) + local ForwardRefComponent = Roact.Component:extend("ForwardRefContainer") + + function ForwardRefComponent:init() + self.defaultRef = Roact.createRef() + end + + function ForwardRefComponent:render() + return render(self.props, self.props[Roact.Ref] or self.defaultRef) + end + + return ForwardRefComponent +end + +return forwardRef \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/forwardRef.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/forwardRef.spec.lua new file mode 100644 index 0000000..39dd1b1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/forwardRef.spec.lua @@ -0,0 +1,54 @@ +return function() + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + + local forwardRef = require(script.Parent.forwardRef) + local createSpy = require(script.Parent.Test.createSpy) + + it("should provide a valid ref to the given function when none is passed in", function() + local internalComponentSpy = createSpy(function(props, ref) + return nil + end) + + local Component = forwardRef(internalComponentSpy.value) + + expect(internalComponentSpy.callCount).to.equal(0) + + local tree = Roact.mount(Roact.createElement(Component, { + text = "Hello", + }), nil) + + expect(internalComponentSpy.callCount).to.equal(1) + + local args = internalComponentSpy:captureValues("props", "ref") + expect(args.props.text).to.equal("Hello") + expect(args.ref).to.be.ok() + expect(typeof(args.ref.getValue)).to.equal("function") + + Roact.unmount(tree) + end) + + it("should forward a provided ref if present", function() + local internalComponentSpy = createSpy(function(props, ref) + return nil + end) + + local Component = forwardRef(internalComponentSpy.value) + + expect(internalComponentSpy.callCount).to.equal(0) + + local providedRef = Roact.createRef() + local tree = Roact.mount(Roact.createElement(Component, { + text = "Hello", + [Roact.Ref] = providedRef, + }), nil) + + expect(internalComponentSpy.callCount).to.equal(1) + + local args = internalComponentSpy:captureValues("props", "ref") + expect(args.props.text).to.equal("Hello") + expect(args.ref).to.equal(providedRef) + + Roact.unmount(tree) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/getEngineInterface.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/getEngineInterface.lua new file mode 100644 index 0000000..19f3e70 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/getEngineInterface.lua @@ -0,0 +1,98 @@ +--!strict +local GuiService = game:GetService("GuiService") +local RunService = game:GetService("RunService") +local UserInputService = game:GetService("UserInputService") +local CoreGui = game:GetService("CoreGui") + +--[[ + The CoreInterface will be used for any focus trees mounted under the + `CoreGui` service in the DataModel. +]] +local CoreInterface = {} + +function CoreInterface.getGamepadConnected(gamepadNum) + return UserInputService:GetGamepadConnected(gamepadNum) +end + +function CoreInterface.getGamepadState(gamepadNum) + return UserInputService:GetGamepadState(gamepadNum) +end + +function CoreInterface.getNavigationGamepads() + return UserInputService:GetNavigationGamepads() +end + +function CoreInterface.getSelection() + return GuiService.SelectedCoreObject +end + +function CoreInterface.setSelection(selectionTarget) + GuiService.SelectedCoreObject = selectionTarget +end + +function CoreInterface.subscribeToSelectionChanged(callback) + return GuiService:GetPropertyChangedSignal("SelectedCoreObject"):Connect(callback) +end + +function CoreInterface.subscribeToRenderStepped(callback) + return RunService.RenderStepped:Connect(callback) +end + +function CoreInterface.subscribeToGamepadConnected(callback) + return UserInputService.GamepadConnected:Connect(callback) +end + +function CoreInterface.subscribeToGamepadDisconnected(callback) + return UserInputService.GamepadDisconnected:Connect(callback) +end + +function CoreInterface.subscribeToInputBegan(callback) + return UserInputService.InputBegan:Connect(callback) +end + +function CoreInterface.subscribeToInputEnded(callback) + return UserInputService.InputEnded:Connect(callback) +end + +--[[ + The PlayerGuiInterface will be used for focus trees mounted anywhere under a + `PlayerGui` instance +]] +local PlayerGuiInterface = {} + +function PlayerGuiInterface.getSelection() + return GuiService.SelectedObject +end + +function PlayerGuiInterface.setSelection(selectionTarget) + GuiService.SelectedObject = selectionTarget +end + +function PlayerGuiInterface.subscribeToSelectionChanged(callback) + return GuiService:GetPropertyChangedSignal("SelectedObject"):Connect(callback) +end + +-- These functions aren't distinct from their core counterparts, but are still +-- very useful to mock. For the PlayerGuiInterface, we simply reuse the same +-- function as the CoreInterface +PlayerGuiInterface.getGamepadConnected = CoreInterface.getGamepadConnected +PlayerGuiInterface.getGamepadState = CoreInterface.getGamepadState +PlayerGuiInterface.getNavigationGamepads = CoreInterface.getNavigationGamepads +PlayerGuiInterface.subscribeToRenderStepped = CoreInterface.subscribeToRenderStepped +PlayerGuiInterface.subscribeToGamepadConnected = CoreInterface.subscribeToGamepadConnected +PlayerGuiInterface.subscribeToGamepadDisconnected = CoreInterface.subscribeToGamepadDisconnected +PlayerGuiInterface.subscribeToInputBegan = CoreInterface.subscribeToInputBegan +PlayerGuiInterface.subscribeToInputEnded = CoreInterface.subscribeToInputEnded + +return function(instance) + if instance:IsDescendantOf(CoreGui) then + return CoreInterface + else + local playerGui = instance:FindFirstAncestorWhichIsA("PlayerGui") + if playerGui == nil then + error("Gamepad navigation not supported. Must be a child of CoreGui or a PlayerGui") + end + + return PlayerGuiInterface + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/init.lua new file mode 100644 index 0000000..0e95691 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/init.lua @@ -0,0 +1,13 @@ +local createFocusableCache = require(script.createFocusableCache) + +local api = { + createRefCache = require(script.createRefCache), + + Focusable = createFocusableCache(), + Input = require(script.Input).PublicInterface, + + withFocusController = require(script.withFocusController), + createFocusController = require(script.FocusController).createPublicApiWrapper, +} + +return api \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/withFocusController.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/withFocusController.lua new file mode 100644 index 0000000..760688f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/withFocusController.lua @@ -0,0 +1,30 @@ +--!strict +-- Manages groups of selectable elements, reacting to selection changes for +-- individual items and triggering events for group selection changes +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) + +local FocusContext = require(script.Parent.FocusContext) + +local function FocusControllerConsumer(props) + return Roact.createElement(FocusContext.Consumer, { + render = function(navContext) + local focusController + if navContext then + focusController = navContext.focusNode.focusController + else + focusController = nil + end + + return props.render(focusController) + end, + }) +end + +local function withFocusController(render) + return Roact.createElement(FocusControllerConsumer, { + render = render + }) +end + +return withFocusController \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/withFocusController.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/withFocusController.spec.lua new file mode 100644 index 0000000..f04df87 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/withFocusController.spec.lua @@ -0,0 +1,19 @@ +return function() + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + local asFocusable = require(script.Parent.asFocusable) + local withFocusController = require(script.Parent.withFocusController) + + describe("withFocusController", function() + it("should give a nil focusController if no focusRoot exists in the tree", function() + local FocusableFrame = asFocusable("Frame") + + local tree = Roact.mount(withFocusController(function(focusController) + expect(focusController).to.equal(nil) + return Roact.createElement(FocusableFrame) + end)) + + Roact.unmount(tree) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/t.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Otter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Roact.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/StateTable.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/StateTable.lua new file mode 100644 index 0000000..f698d9c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/StateTable.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_state-table"]["state-table"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/lock.toml new file mode 100644 index 0000000..759c689 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/lock.toml @@ -0,0 +1,11 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/roact-navigation" +version = "0.2.8" +commit = "6b5c6975e75ab68048a12f6f3b84ab4061afe496" +source = "url+https://github.com/roblox/roact-navigation" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "StateTable roblox/state-table 0.1.0 url+https://github.com/roblox/state-table", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/BackBehavior.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/BackBehavior.lua new file mode 100644 index 0000000..5fe5b20 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/BackBehavior.lua @@ -0,0 +1,21 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +local NONE_TOKEN = NavigationSymbol("NONE") +local INITIAL_ROUTE_TOKEN = NavigationSymbol("INITIAL_ROUTE") +local ORDER_TOKEN = NavigationSymbol("ORDER") +local HISTORY_TOKEN = NavigationSymbol("HISTORY") + +--[[ + BackBehavior provides shared constants that are used to configure back + action styles for different navigators. Note that not all routers support + all BackBehaviors and they will fall back to appropriate defaults for + those cases. +]] +local BackBehavior = { + None = NONE_TOKEN, + InitialRoute = INITIAL_ROUTE_TOKEN, + Order = ORDER_TOKEN, + History = HISTORY_TOKEN, +} + +return BackBehavior diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NavigationActions.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NavigationActions.lua new file mode 100644 index 0000000..5719d57 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NavigationActions.lua @@ -0,0 +1,76 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +local BACK_TOKEN = NavigationSymbol("BACK") +local INIT_TOKEN = NavigationSymbol("INIT") +local NAVIGATE_TOKEN = NavigationSymbol("NAVIGATE") +local SET_PARAMS_TOKEN = NavigationSymbol("SET_PARAMS") +local COMPLETE_TRANSITION_TOKEN = NavigationSymbol("COMPLETE_TRANSITION") + +--[[ + NavigationActions provides shared constants and methods to construct + actions that are dispatched to routers to cause a change in the route. +]] +local NavigationActions = { + Back = BACK_TOKEN, + Init = INIT_TOKEN, + Navigate = NAVIGATE_TOKEN, + SetParams = SET_PARAMS_TOKEN, + CompleteTransition = COMPLETE_TRANSITION_TOKEN, +} + +NavigationActions.__index = NavigationActions + +-- Navigate back in the history (temporally). +function NavigationActions.back(payload) + local data = payload or {} + return { + type = BACK_TOKEN, + key = data.key, + immediate = data.immediate, + } +end + +-- Initialize the navigation history if not already defined. +function NavigationActions.init(payload) + local data = payload or {} + return { + type = INIT_TOKEN, + params = data.params, + } +end + +-- Navigate to an existing or new route. +function NavigationActions.navigate(payload) + local data = payload or {} + return { + type = NAVIGATE_TOKEN, + routeName = data.routeName, + params = data.params, + action = data.action, + key = data.key, + } +end + +-- Swap out the params for an existing route, matched by the given key. +function NavigationActions.setParams(payload) + local data = payload or {} + return { + type = SET_PARAMS_TOKEN, + key = data.key, + params = data.params, + } +end + +-- For internal use. Triggers completion of a transition animation, if needed by the router. +-- This would be sent on e.g. didMount of the new page, so the router knows that the new screen +-- is ready to be displayed before it animates it in place. +function NavigationActions.completeTransition(payload) + local data = payload or {} + return { + type = COMPLETE_TRANSITION_TOKEN, + key = data.key, + toChildKey = data.toChildKey, + } +end + +return NavigationActions diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NavigationEvents.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NavigationEvents.lua new file mode 100644 index 0000000..a60610b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NavigationEvents.lua @@ -0,0 +1,21 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +local WILL_FOCUS_TOKEN = NavigationSymbol("WILL_FOCUS") +local DID_FOCUS_TOKEN = NavigationSymbol("DID_FOCUS") +local WILL_BLUR_TOKEN = NavigationSymbol("WILL_BLUR") +local DID_BLUR_TOKEN = NavigationSymbol("DID_BLUR") +local ACTION_TOKEN = NavigationSymbol("ACTION") +local REFOCUS_TOKEN = NavigationSymbol("REFOCUS") + +--[[ + NavigationEvents provides shared constants that are used to register + listeners for different RoactNavigation UI state changes. +]] +return { + WillFocus = WILL_FOCUS_TOKEN, + DidFocus = DID_FOCUS_TOKEN, + WillBlur = WILL_BLUR_TOKEN, + DidBlur = DID_BLUR_TOKEN, + Action = ACTION_TOKEN, + Refocus = REFOCUS_TOKEN, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NavigationSymbol.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NavigationSymbol.lua new file mode 100644 index 0000000..90762dc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NavigationSymbol.lua @@ -0,0 +1,15 @@ +-- Taken from Roact.Symbol and modified to produce exact string names +-- to allow for serialization/pathing. + +return function (name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + -- Unlike Symbols in Roact, we need the exact names. + getmetatable(self).__tostring = function() + return name + end + + return self +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NoneSymbol.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NoneSymbol.lua new file mode 100644 index 0000000..10acee1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NoneSymbol.lua @@ -0,0 +1,11 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +--[[ + RoactNavigation.None allows us to declare that certain values should be explicitly + removed from navigation props, e.g. for stack navigation options where we want to + remove the default header from a screen when drawing in a stack navigator with + headerMode == StackHeaderMode.Screen. +]] +local NONE_SYMBOL = NavigationSymbol("NONE") + +return NONE_SYMBOL diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/StackActions.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/StackActions.lua new file mode 100644 index 0000000..387ed44 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/StackActions.lua @@ -0,0 +1,80 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +local POP_TOKEN = NavigationSymbol("POP") +local POP_TO_TOP_TOKEN = NavigationSymbol("POP_TO_TOP") +local PUSH_TOKEN = NavigationSymbol("PUSH") +local RESET_TOKEN = NavigationSymbol("RESET") +local REPLACE_TOKEN = NavigationSymbol("REPLACE") + +--[[ + StackActions provides shared constants and methods to construct + actions that are dispatched to routers to cause a change in the route. + These actions are specific to Stack navigation. See NavigationActions + if you need to use more general APIs. +]] +local StackActions = { + Pop = POP_TOKEN, + PopToTop = POP_TO_TOP_TOKEN, + Push = PUSH_TOKEN, + Reset = RESET_TOKEN, + Replace = REPLACE_TOKEN, +} + +StackActions.__index = StackActions + +-- Pop the top-most item off the route stack, if any. +function StackActions.pop(payload) + local data = payload or {} + return { + type = POP_TOKEN, + n = data.n, + } +end + +-- Pop all the items except the last one off the route stack. +function StackActions.popToTop(payload) + local data = payload or {} + return { + type = POP_TO_TOP_TOKEN, + key = data.key, + } +end + +-- Push a new item onto the route stack. +function StackActions.push(payload) + local data = payload or {} + return { + type = PUSH_TOKEN, + routeName = data.routeName, + params = data.params, + action = data.action, + } +end + +-- Reset the route stack and replace it with a new stack, +-- specified by a list of actions to be applied. +function StackActions.reset(payload) + local data = payload or {} + return { + type = RESET_TOKEN, + index = data.index, + actions = data.actions, + key = data.key, + } +end + +-- Replace the route for the given key with a new route. +function StackActions.replace(payload) + local data = payload or {} + return { + type = REPLACE_TOKEN, + key = data.key, + newKey = data.newKey, + routeName = data.routeName, + params = data.params, + action = data.action, + immediate = data.immediate, + } +end + +return StackActions diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/StateUtils.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/StateUtils.lua new file mode 100644 index 0000000..415a6a6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/StateUtils.lua @@ -0,0 +1,262 @@ +local Cryo = require(script.Parent.Parent.Cryo) + +--[[ + StateUtils provides utilities to read and write standard route data. + Routes have the following general structure: + { + index = , + routes = [ + { + routeName = , + key = , + params = , + action = , + }, + ... + ] + } + + This structure is independent of the notion of stack, tab, drawer, or any + other kind of navigation. It simply represents a list of pages and their + parameters. Different kinds of routers can treat the data in their own way. +]] +local StateUtils = {} +StateUtils.__index = StateUtils + +-- Get the route matching the given key. Returns nil if no match is found. +function StateUtils.get(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + for _, route in ipairs(state.routes) do + if route.key == key then + return route + end + end + + return nil +end + +-- Get the route at the given index. Returns nil if no match is found. +function StateUtils.getAtIndex(state, index) + assert(type(state) == "table", "state must be a table") + assert(type(index) == "number", "index must be a number") + assert(index >= 0, "index must be non-negative") + + return state.routes[index] +end + +-- Get the active route from state. Returns nil if no routes. +function StateUtils.getActiveRoute(state) + assert(type(state) == "table", "state must be a table") + + local index = state.index + if index <= 0 then + return nil + end + + return state.routes[index] +end + +-- Get the index of the route matching the given key. Returns nil if no match is found. +function StateUtils.indexOf(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + for index, route in ipairs(state.routes) do + if route.key == key then + return index + end + end + + return nil +end + +-- Returns true if a route exists matching the given key, false otherwise. +function StateUtils.has(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + for _, route in ipairs(state.routes) do + if route.key == key then + return true + end + end + + return false +end + +-- Push a new route into the navigation state. Makes the pushed route active. +function StateUtils.push(state, route) + assert(type(state) == "table", "state must be a table") + assert(type(route) == "table", "route must be a table") + + assert(StateUtils.indexOf(state, route.key) == nil, + string.format("route with key '%s' already exists", route.key)) + + local routes = Cryo.List.join(state.routes, { route }) + return Cryo.Dictionary.join(state, { + index = #routes, + routes = routes, + }) +end + +-- Pop the top-most route from the navigation state (NOT the active route). +-- Makes the new top-most route active. +function StateUtils.pop(state) + assert(type(state) == "table", "state must be a table") + + if #state.routes == 0 then + -- NOTE: Popping empty state is a no-op + return state + end + + local routes = Cryo.List.removeIndex(state.routes, #state.routes) + return Cryo.Dictionary.join(state, { + index = #routes, + routes = routes, + }) +end + +-- Sets the active route to match the given index. +function StateUtils.jumpToIndex(state, index) + assert(type(state) == "table", "state must be a table") + assert(type(index) == "number", "index must be a number") + + if index == state.index then + return state + end + + assert(state.routes[index] ~= nil, + string.format("cannot jump to out-of-range index '%d'", index)) + + return Cryo.Dictionary.join(state, { + index = index, + }) +end + +-- Sets the active route to match the given key. +function StateUtils.jumpTo(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + local index = StateUtils.indexOf(state, key) + return StateUtils.jumpToIndex(state, index) +end + +-- Sets the active route to the previous route in the list. +function StateUtils.back(state) + assert(type(state) == "table", "state must be a table") + + local index = state.index - 1 + if not state.routes[index] then + return state + end + + return StateUtils.jumpToIndex(state, index) +end + +-- Sets the active route to the next route in the list. +function StateUtils.forward(state) + assert(type(state) == "table", "state must be a table") + + local index = state.index + 1 + if not state.routes[index] then + return state + end + + return StateUtils.jumpToIndex(state, index) +end + +-- Replace the route matching the given key. Sets the active route to the +-- newly replaced entry. Prunes the old entries that follow the replaced one. +function StateUtils.replaceAndPrune(state, key, route) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + assert(type(route) == "table", "route must be a table") + + local index = StateUtils.indexOf(state, key) + local replaced = StateUtils.replaceAtIndex(state, index, route) + + return Cryo.Dictionary.join(replaced, { + routes = { unpack(replaced.routes, 1, index) } + }) +end + +-- Replace the route matching the given key without pruning the following routes. +-- The active route will be updated to match the newly replaced one unless +-- preserveIndex is true. +function StateUtils.replaceAt(state, key, route, preserveIndex) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + assert(type(route) == "table", "route must be a table") + assert(preserveIndex == nil or type(preserveIndex) == "boolean", + "preserveIndex must be nil or a boolean") + + local index = StateUtils.indexOf(state, key) + local nextIndex = preserveIndex and state.index or index + local nextState = StateUtils.replaceAtIndex(state, index, route) + nextState.index = nextIndex + return nextState +end + +-- Replace the route at the given index. Updates the active route to point to +-- the replaced entry. +function StateUtils.replaceAtIndex(state, index, route) + assert(type(state) == "table", "state must be a table") + assert(type(index) == "number", "index must be a number") + assert(type(route) == "table", "route must be a table") + + assert(state.routes[index] ~= nil, + string.format("index '%d' does not exist in route '%s'", index, route.key)) + + if state.routes[index] == route and index == state.index then + return state + end + + local routes = Cryo.List.join(state.routes) + routes[index] = route + + return Cryo.Dictionary.join(state, { + index = index, + routes = routes, + }) +end + +-- Wipe away the existing routes and replace them with new routes. +-- Sets the active route to the provided index (if provided), otherwise +-- sets the active route to the last one in the list. +function StateUtils.reset(state, routes, index) + assert(type(state) == "table", "state must be a table") + assert(type(routes) == "table" and #routes > 0, + "routes must be a list with at least one element") + assert(index == nil or type(index) == "number", + "index must be a number or nil") + + local nextIndex = not index and #routes or index + + -- Bail out without replacing IFF index and routes all match + if #state.routes == #routes and state.index == nextIndex then + local routesAreEqual = true + for i = 1, #routes, 1 do + if state.routes[i] ~= routes[i] then + routesAreEqual = false + break + end + end + + if routesAreEqual then + return state + end + end + + assert(routes[nextIndex] ~= nil, + string.format("cannot reset index '%d' that does not exist", nextIndex)) + + return Cryo.Dictionary.join(state, { + index = nextIndex, + routes = routes, + }) +end + +return StateUtils diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/BackBehavior.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/BackBehavior.spec.lua new file mode 100644 index 0000000..cec6ddd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/BackBehavior.spec.lua @@ -0,0 +1,19 @@ +return function() + local BackBehavior = require(script.Parent.Parent.BackBehavior) + + describe("BackBehavior token tests", function() + it("should return same object for each token for multiple calls", function() + expect(BackBehavior.None).to.equal(BackBehavior.None) + expect(BackBehavior.InitialRoute).to.equal(BackBehavior.InitialRoute) + expect(BackBehavior.Order).to.equal(BackBehavior.Order) + expect(BackBehavior.History).to.equal(BackBehavior.History) + end) + + it("should return matching string names for symbols", function() + expect(tostring(BackBehavior.None)).to.equal("NONE") + expect(tostring(BackBehavior.InitialRoute)).to.equal("INITIAL_ROUTE") + expect(tostring(BackBehavior.Order)).to.equal("ORDER") + expect(tostring(BackBehavior.History)).to.equal("HISTORY") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationActions.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationActions.spec.lua new file mode 100644 index 0000000..03d08db --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationActions.spec.lua @@ -0,0 +1,80 @@ +return function() + local NavigationActions = require(script.Parent.Parent.NavigationActions) + + describe("NavigationActions token tests", function() + it("should return same object for each token for multiple calls", function() + expect(NavigationActions.Back).to.equal(NavigationActions.Back) + expect(NavigationActions.Init).to.equal(NavigationActions.Init) + expect(NavigationActions.Navigate).to.equal(NavigationActions.Navigate) + expect(NavigationActions.SetParams).to.equal(NavigationActions.SetParams) + expect(NavigationActions.CompleteTransition).to.equal(NavigationActions.CompleteTransition) + end) + + it("should return matching string names for symbols", function() + expect(tostring(NavigationActions.Back)).to.equal("BACK") + expect(tostring(NavigationActions.Init)).to.equal("INIT") + expect(tostring(NavigationActions.Navigate)).to.equal("NAVIGATE") + expect(tostring(NavigationActions.SetParams)).to.equal("SET_PARAMS") + expect(tostring(NavigationActions.CompleteTransition)).to.equal("COMPLETE_TRANSITION") + end) + end) + + describe("NavigationActions function tests", function() + it("should return a back action with matching data for a call to back()", function() + local backTable = NavigationActions.back({ + key = "the_key", + immediate = true, + }) + + expect(backTable.type).to.equal(NavigationActions.Back) + expect(backTable.key).to.equal("the_key") + expect(backTable.immediate).to.equal(true) + end) + + it("should return an init action with matching data for call to init()", function() + local initTable = NavigationActions.init({ + params = "foo", + }) + + expect(initTable.type).to.equal(NavigationActions.Init) + expect(initTable.params).to.equal("foo") + end) + + it("should return a navigate action with matching data for call to navigate()", function() + local navigateTable = NavigationActions.navigate({ + routeName = "routeName", + params = "foo", + action = "action", + key = "key", + }) + + expect(navigateTable.type).to.equal(NavigationActions.Navigate) + expect(navigateTable.routeName).to.equal("routeName") + expect(navigateTable.params).to.equal("foo") + expect(navigateTable.action).to.equal("action") + expect(navigateTable.key).to.equal("key") + end) + + it("should return a set params action with matching data for call to setParams()", function() + local setParamsTable = NavigationActions.setParams({ + key = "key", + params = "foo", + }) + + expect(setParamsTable.type).to.equal(NavigationActions.SetParams) + expect(setParamsTable.key).to.equal("key") + expect(setParamsTable.params).to.equal("foo") + end) + + it("should return a complete transition action with matching data for call to completeTransition()", function() + local completeTransitionTable = NavigationActions.completeTransition({ + key = "key", + toChildKey = "toChildKey", + }) + + expect(completeTransitionTable.type).to.equal(NavigationActions.CompleteTransition) + expect(completeTransitionTable.key).to.equal("key") + expect(completeTransitionTable.toChildKey).to.equal("toChildKey") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationEvents.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationEvents.spec.lua new file mode 100644 index 0000000..ed776d7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationEvents.spec.lua @@ -0,0 +1,23 @@ +return function() + local NavigationEvents = require(script.Parent.Parent.NavigationEvents) + + describe("NavigationEvents token tests", function() + it("should return same object for each token for multiple calls", function() + expect(NavigationEvents.WillFocus).to.equal(NavigationEvents.WillFocus) + expect(NavigationEvents.DidFocus).to.equal(NavigationEvents.DidFocus) + expect(NavigationEvents.WillBlur).to.equal(NavigationEvents.WillBlur) + expect(NavigationEvents.DidBlur).to.equal(NavigationEvents.DidBlur) + expect(NavigationEvents.Action).to.equal(NavigationEvents.Action) + expect(NavigationEvents.Refocus).to.equal(NavigationEvents.Refocus) + end) + + it("should return matching string names for symbols", function() + expect(tostring(NavigationEvents.WillFocus)).to.equal("WILL_FOCUS") + expect(tostring(NavigationEvents.DidFocus)).to.equal("DID_FOCUS") + expect(tostring(NavigationEvents.WillBlur)).to.equal("WILL_BLUR") + expect(tostring(NavigationEvents.DidBlur)).to.equal("DID_BLUR") + expect(tostring(NavigationEvents.Action)).to.equal("ACTION") + expect(tostring(NavigationEvents.Refocus)).to.equal("REFOCUS") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationSymbol.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationSymbol.spec.lua new file mode 100644 index 0000000..a43690b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationSymbol.spec.lua @@ -0,0 +1,22 @@ +return function() + local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol) + + it("should give an opaque object", function() + local symbol = NavigationSymbol("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = NavigationSymbol("foo") + + expect(tostring(symbol)).to.equal("foo") + end) + + it("should be unique when constructed", function() + local symbolA = NavigationSymbol("abc") + local symbolB = NavigationSymbol("abc") + + expect(symbolA).never.to.equal(symbolB) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NoneSymbol.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NoneSymbol.spec.lua new file mode 100644 index 0000000..afcbced --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NoneSymbol.spec.lua @@ -0,0 +1,11 @@ +return function() + local NoneSymbol = require(script.Parent.Parent.NoneSymbol) + + it("should return same object for each token for multiple calls", function() + expect(NoneSymbol).to.equal(NoneSymbol) + end) + + it("should return matching string names for symbols", function() + expect(tostring(NoneSymbol)).to.equal("NONE") + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/StackActions.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/StackActions.spec.lua new file mode 100644 index 0000000..7baf9cc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/StackActions.spec.lua @@ -0,0 +1,82 @@ +return function() + local StackActions = require(script.Parent.Parent.StackActions) + + describe("StackActions token tests", function() + it("should return same object for each token for multiple calls", function() + expect(StackActions.Pop).to.equal(StackActions.Pop) + expect(StackActions.PopToTop).to.equal(StackActions.PopToTop) + expect(StackActions.Push).to.equal(StackActions.Push) + expect(StackActions.Reset).to.equal(StackActions.Reset) + expect(StackActions.Replace).to.equal(StackActions.Replace) + end) + + it("should return matching string names for symbols", function() + expect(tostring(StackActions.Pop)).to.equal("POP") + expect(tostring(StackActions.PopToTop)).to.equal("POP_TO_TOP") + expect(tostring(StackActions.Push)).to.equal("PUSH") + expect(tostring(StackActions.Reset)).to.equal("RESET") + expect(tostring(StackActions.Replace)).to.equal("REPLACE") + end) + end) + + describe("StackActions function tests", function() + it("should return a pop action for pop()", function() + local popTable = StackActions.pop({ + n = "n", + }) + + expect(popTable.type).to.equal(StackActions.Pop) + expect(popTable.n).to.equal("n") + end) + + it("should return a pop to top action for popToTop()", function() + local popToTopTable = StackActions.popToTop() + + expect(popToTopTable.type).to.equal(StackActions.PopToTop) + end) + + it("should return a push action for push()", function() + local pushTable = StackActions.push({ + routeName = "routeName", + params = "params", + action = "action", + }) + + expect(pushTable.type).to.equal(StackActions.Push) + expect(pushTable.routeName).to.equal("routeName") + expect(pushTable.params).to.equal("params") + expect(pushTable.action).to.equal("action") + end) + + it("should return a reset action for reset()", function() + local resetTable = StackActions.reset({ + index = "index", + actions = "actions", + key = "key", + }) + + expect(resetTable.type).to.equal(StackActions.Reset) + expect(resetTable.index).to.equal("index") + expect(resetTable.key).to.equal("key") + end) + + it("should return a replace action for replace()", function() + local replaceTable = StackActions.replace({ + key = "key", + newKey = "newKey", + routeName = "routeName", + params = "params", + action = "action", + immediate = "immediate", + }) + + expect(replaceTable.type).to.equal(StackActions.Replace) + expect(replaceTable.key).to.equal("key") + expect(replaceTable.newKey).to.equal("newKey") + expect(replaceTable.routeName).to.equal("routeName") + expect(replaceTable.params).to.equal("params") + expect(replaceTable.action).to.equal("action") + expect(replaceTable.immediate).to.equal("immediate") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/StateUtils.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/StateUtils.spec.lua new file mode 100644 index 0000000..279873f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/StateUtils.spec.lua @@ -0,0 +1,725 @@ +return function() + local StateUtils = require(script.Parent.Parent.StateUtils) + + describe("StateUtils.get tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.get(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.get({}, 5) + end).to.throw() + end) + + it("should return nil if key is not found in routes", function() + local result = StateUtils.get({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + }, + }, + }, "key") + + expect(result).to.equal(nil) + end) + + it("should return route if key is found in routes", function() + local result = StateUtils.get({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + }, + }, "foo-1") + + expect(result.routeName).to.equal("foo") + expect(result.key).to.equal("foo-1") + end) + end) + + describe("StateUtils.getAtIndex tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.getAtIndex(nil, 0) + end).to.throw() + end) + + it("should assert if index is negative", function() + expect(function() + StateUtils.getAtIndex({}, -1) + end).to.throw() + end) + + it("should return nil if index is not found", function() + local result = StateUtils.getAtIndex({ + index = 1, + routes = { + { + routeName = "foo1", + key = "foo-1", + }, + { + routeName = "foo2", + key = "foo-2", + } + } + }, 5) + + expect(result).to.equal(nil) + end) + + it("should return a matching route", function() + local result = StateUtils.getAtIndex({ + index = 1, + routes = { + { + routeName = "foo1", + key = "foo-1", + }, + { + routeName = "foo2", + key = "foo-2", + } + } + }, 2) + + expect(result.routeName).to.equal("foo2") + expect(result.key).to.equal("foo-2") + end) + end) + + describe("StateUtils.getActiveRoute tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.getActiveRoute(nil) + end).to.throw() + end) + + it("should return nil if no routes", function() + local result = StateUtils.getActiveRoute({ + index = 0, + routes = {}, + }) + + expect(result).to.equal(nil) + end) + + it("should return active route", function() + local result = StateUtils.getActiveRoute({ + index = 1, + routes = { + { + routeName = "active", + key = "active-1", + } + }, + }) + + expect(result.routeName).to.equal("active") + expect(result.key).to.equal("active-1") + end) + end) + + describe("StateUtils.indexOf tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.indexOf(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.indexOf({}, 5) + end).to.throw() + end) + + it("should return nil if key is not found in routes", function() + local result = StateUtils.indexOf({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + }, + }, "key") + + expect(result).to.equal(nil) + end) + + it("should return index if key is found in routes", function() + local result = StateUtils.indexOf({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + }, + { + routeName = "foo2", + key = "foo-2", + } + }, + }, "foo-2") + + expect(result).to.equal(2) + end) + end) + + describe("StateUtils.has tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.has(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.has({}, 5) + end).to.throw() + end) + + it("should return false if key is not in routes", function() + local result = StateUtils.has({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + } + }, "key") + + expect(result).to.equal(false) + end) + + it("should return true if key is found in routes", function() + local result = StateUtils.has({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + } + }, "foo-1") + + expect(result).to.equal(true) + end) + end) + + describe("StateUtils.push tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.push(nil, {}) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.push({}, 5) + end).to.throw() + end) + + it("should assert if route.key is already present", function() + expect(function() + StateUtils.push({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + } + }, { + routeName = "foo", + key = "foo-1", + }) + end).to.throw() + end) + + it("should insert new route if it doesn't exist", function() + local newState = StateUtils.push({ + index = 1, + routes = { + { + routeName = "first", + key = "foo-1", + }, + }, + }, { + routeName = "second", + key = "foo-2", + }) + + expect(newState.index).to.equal(2) + expect(#newState.routes).to.equal(2) + expect(newState.routes[newState.index].key).to.equal("foo-2") + expect(newState.routes[newState.index].routeName).to.equal("second") + end) + end) + + describe("StateUtils.pop tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.pop(nil) + end).to.throw() + end) + + it("should return existing state if routes is empty", function() + local initialState = { + index = 0, + routes = {}, + } + + local newState = StateUtils.pop(initialState) + expect(newState).to.equal(initialState) + end) + + it("should return empty state if popping one route", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "route-1", }, + }, + } + + local newState = StateUtils.pop(initialState) + expect(newState.index).to.equal(0) + expect(#newState.routes).to.equal(0) + end) + + it("should remove top route if popping with more than one route", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "route-1", }, + { routeName = "route", key = "route-2", }, + }, + } + + local newState = StateUtils.pop(initialState) + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(1) + expect(newState.routes[1].key).to.equal("route-1") + end) + end) + + describe("StateUtils.jumpToIndex tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.jumpToIndex(nil, 0) + end).to.throw() + end) + + it("should assert if index is not a number", function() + expect(function() + StateUtils.jumpToIndex({}, "foo") + end).to.throw() + end) + + it("should assert if index does not match a route", function() + expect(function() + StateUtils.jumpToIndex({ + index = 1, + routes = { { routeName = "first", key = "first-1" } } + }, 5) + end).to.throw() + end) + + it("should return original state if index matches current", function() + local initialState = { + index = 1, + routes = { { routeName = "one", key = "1" } } + } + + local newState = StateUtils.jumpToIndex(initialState, 1) + expect(newState).to.equal(initialState) + end) + + it("should return updated state if index differs", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "route-1" }, + { routeName = "route", key = "route-2" }, + }, + } + + local newState = StateUtils.jumpToIndex(initialState, 2) + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.jumpTo tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.jumpTo(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.jumpTo({}, 0) + end).to.throw() + end) + + it("should return original state if key is already active route", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.jumpTo(initialState, "key-1") + expect(newState).to.equal(initialState) + end) + + it("should return state with new active route if key is not active", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.jumpTo(initialState, "key-2") + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.back tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.back(nil) + end).to.throw() + end) + + it("should return original state if route for new index does not exist", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + } + } + + local newState = StateUtils.back(initialState) + expect(newState).to.equal(initialState) + end) + + it("should remove top state if there is somewhere to go", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.back(initialState) + expect(newState.index).to.equal(1) + end) + end) + + describe("StateUtils.forward tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.forward(nil) + end).to.throw() + end) + + it("should not walk off the end of the route list", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + } + } + + local newState = StateUtils.forward(initialState) + expect(newState).to.equal(initialState) + end) + + it("should move to next route if available", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.forward(initialState) + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.replaceAndPrune tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.replaceAndPrune(nil, "key", {}) + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.replaceAndPrune({}, 0, {}) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.replaceAndPrune({}, "key", 0) + end).to.throw() + end) + + it("should replace matching route and prune following routes", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.replaceAndPrune(initialState, "key-1", { + routeName = "newRoute", key = "key-3" + }) + + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(1) + expect(newState.routes[1].routeName).to.equal("newRoute") + expect(newState.routes[1].key).to.equal("key-3") + end) + end) + + describe("StateUtils.replaceAt tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.replaceAt(nil, "key", {}, false) + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.replaceAt({}, 0, {}, false) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.replaceAt({}, "key", 0, false) + end).to.throw() + end) + + it("should assert if preserveIndex is not a boolean", function() + expect(function() + StateUtils.replaceAt({}, "key", {}, 0) + end).to.throw() + end) + + it("should replace matching route, not prune, and update index", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.replaceAt(initialState, "key-1", { + routeName = "newRoute", key = "key-3" + }, false) + + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(2) + expect(newState.routes[1].routeName).to.equal("newRoute") + expect(newState.routes[1].key).to.equal("key-3") + end) + + it("should replace matching route, not prune, and preserve existing index", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.replaceAt(initialState, "key-1", { + routeName = "newRoute", key = "key-3" + }, true) + + expect(newState.index).to.equal(2) + expect(#newState.routes).to.equal(2) + expect(newState.routes[1].routeName).to.equal("newRoute") + expect(newState.routes[1].key).to.equal("key-3") + end) + end) + + describe("StateUtils.replaceAtIndex tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.replaceAtIndex(nil, 0, {}) + end).to.throw() + end) + + it("should assert if index is not a number", function() + expect(function() + StateUtils.replaceAtIndex({}, nil, {}) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.replaceAtIndex({}, 5, nil) + end).to.throw() + end) + + it("should assert if index does not exist", function() + expect(function() + StateUtils.replaceAtIndex({ + index = 0, + routes = {} + }, 5, { routeName = "name", key = "key" }) + end).to.throw() + end) + + it("should return original state if inputs are same", function() + local testRoute = { routeName = "name", key = "key" } + local initialState = { + index = 1, + routes = { testRoute }, + } + + local newState = StateUtils.replaceAtIndex(initialState, 1, testRoute) + expect(newState).to.equal(initialState) + end) + + it("should replace route at index if route is not equal", function() + local initialState = { + index = 1, + routes = { + { routeName = "name", key = "key" } + }, + } + + local newState = StateUtils.replaceAtIndex(initialState, 1, { + routeName = "newName", + key = "key", + }) + + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(1) + expect(newState.routes[1].routeName).to.equal("newName") + expect(newState.routes[1].key).to.equal("key") + end) + + it("should update index, if new index differs but route does not", function() + local testRoute = { routeName = "name", key = "key-2" } + local initialState = { + index = 1, + routes = { + { routeName = "name", key = "key-1" }, + testRoute, + } + } + + local newState = StateUtils.replaceAtIndex(initialState, 2, testRoute) + expect(newState).never.to.equal(initialState) + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.reset tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.reset(nil, {}, 0) + end).to.throw() + end) + + it("should assert if routes is not a table", function() + expect(function() + StateUtils.reset({}, nil, 0) + end).to.throw() + end) + + it("should assert if index is not a number", function() + expect(function() + StateUtils.reset({}, {}, "foo") + end).to.throw() + end) + + it("should NOT assert if index is nil", function() + expect(function() + StateUtils.reset({}, {}) + end).to.throw() + end) + + it("should return original state if index matches and all routes are same objects", function() + local route1 = { routeName = "route1", key = "route-1" } + local route2 = { routeName = "route2", key = "route-2" } + + local initialState = { + index = 2, + routes = { route1, route2 }, + } + + local newState = StateUtils.reset(initialState, { + route1, + route2, + }, 2) + + expect(newState).to.equal(initialState) + end) + + it("should update state if index is not specified and old index is not last route", function() + local route1 = { routeName = "route1", key = "route-1" } + local route2 = { routeName = "route2", key = "route-2" } + + local initialState = { + index = 1, + routes = { route1, route2 }, + } + + local newState = StateUtils.reset(initialState, { + route1, + route2, + }) + + expect(newState).never.to.equal(initialState) + expect(newState.index).to.equal(2) + end) + + it("should update state if index matches but routes differ", function() + local route1 = { routeName = "route1", key = "route-1" } + local route2 = { routeName = "route2", key = "route-2" } + + local initialState = { + index = 1, + routes = { route1, route2 }, + } + + local newState = StateUtils.reset(initialState, { + route1, + { routeName = "route3", key = "route-3" }, + }, 1) + + expect(newState).never.to.equal(initialState) + expect(#newState.routes).to.equal(2) + expect(newState.index).to.equal(1) + expect(newState.routes[2].routeName).to.equal("route3") + expect(newState.routes[2].key).to.equal("route-3") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/createAppContainer.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/createAppContainer.spec.lua new file mode 100644 index 0000000..1e53bc9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/createAppContainer.spec.lua @@ -0,0 +1,143 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Roact) + local NavigationActions = require(script.Parent.Parent.NavigationActions) + local createAppContainer = require(script.Parent.Parent.createAppContainer) + local createSwitchNavigator = require(script.Parent.Parent.navigators.createSwitchNavigator) + + it("should be a function", function() + expect(type(createAppContainer)).to.equal("function") + end) + + it("should return a valid component when mounting a switch navigator", function() + local TestNavigator = createSwitchNavigator({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local TestApp = createAppContainer(TestNavigator) + local element = Roact.createElement(TestApp) + local instance = Roact.mount(element) + + Roact.unmount(instance) + end) + + it("should throw when navigator has both navigation and container props", function() + local TestAppComponent = Roact.Component:extend("TestAppComponent") + TestAppComponent.router = {} + function TestAppComponent:render() end + + local element = Roact.createElement(createAppContainer(TestAppComponent), { + navigation = {}, + somePropThatShouldNotBeHere = true, + }) + + local status, err = pcall(function() + Roact.mount(element) + end) + + expect(status).to.equal(false) + expect(string.find(err, "This navigator has both 'navigation' and container props.")).to.never.equal(nil) + end) + + it("should throw when not passed a table for AppComponent", function() + local TestAppComponent = 5 + + local status, err = pcall(function() + createAppContainer(TestAppComponent) + end) + + expect(status).to.equal(false) + expect(string.find(err, "AppComponent must be a navigator or a stateful Roact " .. + "component with a 'router' field")).to.never.equal(nil) + end) + + it("should throw when passed a stateful component without router field", function() + local TestAppComponent = Roact.Component:extend("TestAppComponent") + + local status, err = pcall(function() + createAppContainer(TestAppComponent) + end) + + expect(status).to.equal(false) + expect(string.find(err, "AppComponent must be a navigator or a stateful Roact " .. + "component with a 'router' field")).to.never.equal(nil) + end) + + it("should accept actions from externalDispatchConnector", function() + local TestNavigator = createSwitchNavigator({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local registeredCallback = nil + local externalDispatchConnector = function(rnCallback) + registeredCallback = rnCallback + return function() + registeredCallback = nil + end + end + + local element = Roact.createElement(createAppContainer(TestNavigator), { + externalDispatchConnector = externalDispatchConnector, + }) + + local instance = Roact.mount(element) + expect(type(registeredCallback)).to.equal("function") + + -- Make sure it processes action + local result = registeredCallback(NavigationActions.navigate({ + routeName = "Foo", + })) + expect(result).to.equal(true) + + local failResult = registeredCallback(NavigationActions.navigate({ + routeName = "Bar", -- should fail because not a valid route + })) + expect(failResult).to.equal(false) + + Roact.unmount(instance) + expect(registeredCallback).to.equal(nil) + end) + + it("should correctly pass screenProps to pages", function() + local passedScreenProps = nil + local extractedValue1 = nil + local extractedMissingValue1 = nil + local extractedMissingValue2 = nil + + local testScreenProps = { + MyKey1 = "MyValue1", + } + + local TestNavigator = createSwitchNavigator({ + routes = { + Foo = function(props) + -- doing this in render is an abuse, but it's just a test + passedScreenProps = props.navigation.getScreenProps() + extractedValue1 = props.navigation.getScreenProps("MyKey1") + extractedMissingValue1 = props.navigation.getScreenProps("MyMissingKey", 5) + extractedMissingValue2 = props.navigation.getScreenProps("MyMissingKey") + end, + }, + initialRouteName = "Foo", + }) + + local TestApp = createAppContainer(TestNavigator) + local element = Roact.createElement(TestApp, { + screenProps = testScreenProps, + }) + local instance = Roact.mount(element) + + expect(passedScreenProps).to.equal(testScreenProps) + expect(extractedValue1).to.equal("MyValue1") + expect(extractedMissingValue1).to.equal(5) + expect(extractedMissingValue2).to.equal(nil) + + Roact.unmount(instance) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildEventSubscriber.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildEventSubscriber.spec.lua new file mode 100644 index 0000000..45ca5bc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildEventSubscriber.spec.lua @@ -0,0 +1,498 @@ +return function() + local NavigationEvents = require(script.Parent.Parent.NavigationEvents) + local getChildEventSubscriber = require(script.Parent.Parent.getChildEventSubscriber) + + local function dummyAddListener() end + + local function makeListenerBundle() + local testUpstreamListenerMap = {} + local function testAddUpstreamListener(eventType, callback) + testUpstreamListenerMap[eventType] = callback + + return { + disconnect = function() + testUpstreamListenerMap[eventType] = nil + end + } + end + + return { + listenerMap = testUpstreamListenerMap, + addListener = testAddUpstreamListener, + } + end + + local SIMPLE_TEST_KEY = "Foo" + + local SIMPLE_TEST_STATE = { + state = { + routes = { + { key = SIMPLE_TEST_KEY } + }, + index = 1, + }, + lastState = { + routes = { + { key = SIMPLE_TEST_KEY } + }, + index = 1, + }, + action = { + type = "SomeAction" + }, + } + + it("should return a table with correct members", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + expect(type(childSubscriber.addListener)).to.equal("function") + expect(type(childSubscriber.emit)).to.equal("function") + end) + + describe("addListener tests", function() + it("should throw on invalid eventType", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + expect(function() + childSubscriber.addListener("BadSymbol", function() end) + end).to.throw() + end) + + it("should throw on invalid eventHandler", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + expect(function() + childSubscriber.addListener(NavigationEvents.Action, 5) + end).to.throw() + end) + + it("should allow disconnect of listener", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + local connection = childSubscriber.addListener(NavigationEvents.Refocus, function() end) + connection.disconnect() + end) + end) + + describe("emit tests", function() + it("should throw when trying to emit any event besides Refocus", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + expect(function() + childSubscriber.emit(NavigationEvents.WillFocus) + end).to.throw() + + expect(function() + childSubscriber.emit(NavigationEvents.DidFocus) + end).to.throw() + + expect(function() + childSubscriber.emit(NavigationEvents.WillBlur) + end).to.throw() + + expect(function() + childSubscriber.emit(NavigationEvents.DidBlur) + end).to.throw() + + expect(function() + childSubscriber.emit(NavigationEvents.Action) + end).to.throw() + end) + + it("should throw when payload is not a table", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + expect(function() + childSubscriber.emit(NavigationEvents.Refocus, 5) + end).to.throw() + end) + + it("should allow external caller to emit a refocus event with valid payload", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + local testPayload = { a = 1 } + local outputPayload = nil + + childSubscriber.addListener(NavigationEvents.Refocus, function(payload) + outputPayload = payload + end) + + childSubscriber.emit(NavigationEvents.Refocus, testPayload) + expect(outputPayload.a).to.equal(1) + expect(outputPayload.type).to.equal(NavigationEvents.Refocus) + end) + + it("should allow external caller to emit a refocus event with nil payload", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + local outputPayload = nil + + childSubscriber.addListener(NavigationEvents.Refocus, function(payload) + outputPayload = payload + end) + + childSubscriber.emit(NavigationEvents.Refocus) + expect(outputPayload.type).to.equal(NavigationEvents.Refocus) + end) + end) + + describe("upstream event handling tests", function() + it("should register subscriptions for supported event types", function() + local testUpstreamListenerMap = {} + local function testAddUpstreamListener(eventType, callback) + expect(testUpstreamListenerMap[eventType]).to.equal(nil) + testUpstreamListenerMap[eventType] = true + + return { + disconnect = function() end + } + end + + getChildEventSubscriber(testAddUpstreamListener, SIMPLE_TEST_KEY) + + expect(testUpstreamListenerMap[NavigationEvents.Action]).to.equal(true) + expect(testUpstreamListenerMap[NavigationEvents.WillFocus]).to.equal(true) + expect(testUpstreamListenerMap[NavigationEvents.DidFocus]).to.equal(true) + expect(testUpstreamListenerMap[NavigationEvents.WillBlur]).to.equal(true) + expect(testUpstreamListenerMap[NavigationEvents.DidBlur]).to.equal(true) + expect(testUpstreamListenerMap[NavigationEvents.Refocus]).to.equal(true) + end) + + it("should disconnect subscriptions on DidBlur when there is no new route", function() + local testUpstreamListenerMap = {} + local function testAddUpstreamListener(eventType, callback) + testUpstreamListenerMap[eventType] = callback + + return { + disconnect = function() + testUpstreamListenerMap[eventType] = false + end + } + end + + local childSubscriber = getChildEventSubscriber( + testAddUpstreamListener, SIMPLE_TEST_KEY, "Blurred") + + childSubscriber.addListener(NavigationEvents.Action, function() end) + + testUpstreamListenerMap[NavigationEvents.DidBlur]({ + state = {}, + action = { + type = "SomeAction" + } + }) + + expect(testUpstreamListenerMap[NavigationEvents.Action]).to.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.WillFocus]).to.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.DidFocus]).to.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.WillBlur]).to.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.DidBlur]).to.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.Refocus]).to.equal(false) + end) + + it("should NOT disconnect subscriptions on DidBlur when there is a new route", function() + local testUpstreamListenerMap = {} + local function testAddUpstreamListener(eventType, callback) + testUpstreamListenerMap[eventType] = callback + + return { + disconnect = function() + testUpstreamListenerMap[eventType] = false + end + } + end + + local childSubscriber = getChildEventSubscriber(testAddUpstreamListener, SIMPLE_TEST_KEY, "Blurred") + + childSubscriber.addListener(NavigationEvents.Action, function() end) + + testUpstreamListenerMap[NavigationEvents.DidBlur](SIMPLE_TEST_STATE) + + expect(testUpstreamListenerMap[NavigationEvents.Action]).to.never.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.WillFocus]).to.never.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.DidFocus]).to.never.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.WillBlur]).to.never.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.DidBlur]).to.never.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.Refocus]).to.never.equal(false) + end) + + it("should propagate refocus event from upstream", function() + local bundle = makeListenerBundle() + + local outputPayload = nil + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY) + childSubscriber.addListener(NavigationEvents.Refocus, function(payload) + outputPayload = payload + end) + + bundle.listenerMap[NavigationEvents.Refocus]({ a = 1 }) + + expect(outputPayload.a).to.equal(1) + expect(outputPayload.type).to.equal(NavigationEvents.Refocus) + end) + + it("should emit WillFocus on WillFocus event when previously blurred and child is current index", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY) + + local willFocusPayload = nil + childSubscriber.addListener(NavigationEvents.WillFocus, function(payload) + willFocusPayload = payload + end) + + bundle.listenerMap[NavigationEvents.WillFocus](SIMPLE_TEST_STATE) + + -- Detailed analysis of generated payload. Further tests will just check that functor was called. + expect(willFocusPayload).to.never.equal(nil) + expect(willFocusPayload.state).to.never.equal(nil) + expect(willFocusPayload.lastState).to.never.equal(nil) + expect(willFocusPayload.action.type).to.equal("SomeAction") + expect(willFocusPayload.type).to.equal(NavigationEvents.WillFocus) + end) + + it("should emit WillFocus AND DidFocus on Action event when previously blurred and child is current index", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY) + + local willFocusCalled = false + childSubscriber.addListener(NavigationEvents.WillFocus, function() + willFocusCalled = true + end) + + local didFocusCalled = false + childSubscriber.addListener(NavigationEvents.DidFocus, function() + didFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action](SIMPLE_TEST_STATE) + expect(willFocusCalled).to.equal(true) + expect(didFocusCalled).to.equal(true) + end) + + it("should NOT emit WillFocus or DidFocus on Action event when previously blurred and child is NOT current index", + function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY) + + local willFocusCalled = false + childSubscriber.addListener(NavigationEvents.WillFocus, function() + willFocusCalled = true + end) + + local didFocusCalled = false + childSubscriber.addListener(NavigationEvents.DidFocus, function() + didFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action]({ + state = { + routes = { + { key = SIMPLE_TEST_KEY }, + { key = "NOT_SIMPLE_TEST_KEY" }, + }, + index = 2, + }, + action = { + "SomeAction" + }, + }) + + expect(willFocusCalled).to.equal(false) + expect(didFocusCalled).to.equal(false) + end) + + it("should emit DidFocus on DidFocus event when previous event was WillFocus and child is current index", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, "Focusing") + + local didFocusCalled = false + childSubscriber.addListener(NavigationEvents.DidFocus, function() + didFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.DidFocus](SIMPLE_TEST_STATE) + + expect(didFocusCalled).to.equal(true) + end) + + it("should emit DidFocus on Action event when previous event was WillFocus and child is current index", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, "Focusing") + + local didFocusCalled = false + childSubscriber.addListener(NavigationEvents.DidFocus, function() + didFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action](SIMPLE_TEST_STATE) + + expect(didFocusCalled).to.equal(true) + end) + + it("should NOT emit DidFocus on DidFocus event when previous event was WillFocus while transitioning", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, "Focusing") + + local didFocusCalled = false + childSubscriber.addListener(NavigationEvents.DidFocus, function() + didFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.DidFocus]({ + state = { + routes = { + { key = SIMPLE_TEST_KEY } + }, + index = 1, + isTransitioning = true, + }, + action = { + "SomeAction" + }, + }) + + expect(didFocusCalled).to.equal(false) + end) + + it("should emit WillBlur on WillBlur event when previous event was DidFocus", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, "Focused") + + local willBlurCalled = false + childSubscriber.addListener(NavigationEvents.WillBlur, function() + willBlurCalled = true + end) + + bundle.listenerMap[NavigationEvents.WillBlur](SIMPLE_TEST_STATE) + + expect(willBlurCalled).to.equal(true) + end) + + it("should emit Action on Action event when previous event was DidFocus", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, "Focused") + + local actionCalled = false + childSubscriber.addListener(NavigationEvents.Action, function() + actionCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action](SIMPLE_TEST_STATE) + + expect(actionCalled).to.equal(true) + end) + + it("should emit DidBlur on DidBlur event when previous event was WillBlur", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, "Blurring") + + local didBlurCalled = false + childSubscriber.addListener(NavigationEvents.DidBlur, function() + didBlurCalled = true + end) + + bundle.listenerMap[NavigationEvents.DidBlur](SIMPLE_TEST_STATE) + + expect(didBlurCalled).to.equal(true) + end) + + it("should emit DidBlur on Action event when previous event was WillBlur and we've finished transitioning", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, "Foo", "Blurring") + + local didBlurCalled = false + childSubscriber.addListener(NavigationEvents.DidBlur, function() + didBlurCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action]({ + state = { + routes = { + { key = "Foo" }, -- Transitioned away from this route! + { key = "Bar" }, + }, + index = 2, + }, + lastState = { + routes = { + { key = "Foo" }, + { key = "Bar" }, + }, + index = 1, + }, + action = { + type = "SomeAction" + }, + }) + + expect(didBlurCalled).to.equal(true) + end) + + it("should emit WillFocus on Action event when previois event was WillBlur, while transitioning to child", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, "Bar", "Blurring") + + local willFocusCalled = false + childSubscriber.addListener(NavigationEvents.WillFocus, function() + willFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action]({ + state = { + routes = { + { key = "Foo" }, -- Transitioned away from this route! + { key = "Bar" }, + }, + index = 2, + isTransitioning = true, + }, + lastState = { + routes = { + { key = "Foo" }, + { key = "Bar" }, + }, + index = 1, + }, + action = { + type = "SomeAction" + }, + }) + + expect(willFocusCalled).to.equal(true) + end) + + it("should disconnect from input events after finalizing blur if removed from nav state", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, "Bar", "Blurring") + + local didBlurCalled = false + childSubscriber.addListener(NavigationEvents.DidBlur, function() + didBlurCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action]({ + state = { + routes = { + { key = "Foo" }, + }, + index = 1, + isTransitioning = false, + }, + lastState = { + routes = { + { key = "Foo" }, + { key = "Bar" }, -- Transitioning away from this route. + }, + index = 1, + isTransitioning = true, + }, + action = { + type = "SomeAction" + }, + }) + + expect(bundle.listenerMap[NavigationEvents.Action]).to.equal(nil) + expect(bundle.listenerMap[NavigationEvents.Refocus]).to.equal(nil) + expect(didBlurCalled).to.equal(true) -- Event should still be propagated! + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildNavigation.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildNavigation.spec.lua new file mode 100644 index 0000000..1ebdf8a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildNavigation.spec.lua @@ -0,0 +1,119 @@ +return function() + local getChildNavigation = require(script.Parent.Parent.getChildNavigation) + + it("should return nil if there is no route matching requested key", function() + local testNavigation = { + state = { + routes = { + { key = "a" } + } + } + } + + local childNav = getChildNavigation(testNavigation, "invalid_child", function() + return testNavigation + end) + + expect(childNav).to.equal(nil) + end) + + it("should return cached child if its state is a top-level route", function() + local testNavigation = { + state = { + routes = { + { key = "a" } + }, + + }, + } + + testNavigation._childrenNavigation = { + a = { + state = testNavigation.state.routes[1] + } + } + + local childNav = getChildNavigation(testNavigation, "a", function() + return testNavigation + end) + + expect(childNav).to.equal(testNavigation._childrenNavigation.a) + end) + + it("should update cache and return new data when child's state has changed", function() + local testNavigation = { + state = { + routes = { + { key = "a", routeName = "a" }, + { key = "b", routeName = "b" }, + }, + index = 1, + }, + router = { + getComponentForRouteName = function(routeName) + return function() end + end, + getActionCreators = function() end + } + } + + local oldStateA = { + state = { + routes = { + { key = "a", routeName = "a" }, + { key = "b", routeName = "b" }, + }, + index = 2, + }, + } + + testNavigation._childrenNavigation = { + a = oldStateA + } + + local childNav = getChildNavigation(testNavigation, "a", function() + return testNavigation + end) + + expect(childNav).to.equal(testNavigation._childrenNavigation["a"]) + expect(childNav.state).to.equal(testNavigation.state.routes[1]) + expect(type(childNav.getParam)).to.equal("function") + end) + + it("should create a new entry if cached child does not exist yet", function() + local testNavigation = { + state = { + routes = { + { key = "a", routeName = "a", params = { a = 1 } }, + { key = "b", routeName = "b" }, + }, + index = 1, + }, + router = { + getComponentForRouteName = function(routeName) + return function() end + end, + getActionCreators = function() end, + }, + addListener = function() + return { + disconnect = function() end + } + end, + isFocused = function() + return true + end, + } + + local childNav = getChildNavigation(testNavigation, "a", function() + return testNavigation + end) + + expect(testNavigation._childrenNavigation["a"]).to.never.equal(nil) + expect(childNav).to.equal(testNavigation._childrenNavigation["a"]) + expect(childNav.isFocused()).to.equal(true) + + expect(childNav.getParam("a", 0)).to.equal(1) + expect(childNav.getParam("b", 0)).to.equal(0) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildrenNavigationCache.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildrenNavigationCache.spec.lua new file mode 100644 index 0000000..b96a218 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildrenNavigationCache.spec.lua @@ -0,0 +1,63 @@ +return function() + local getChildrenNavigationCache = require(script.Parent.Parent.getChildrenNavigationCache) + + it("should return empty table if navigation arg not provided", function() + expect(getChildrenNavigationCache()._childrenNavigation).to.equal(nil) + end) + + it("should populate navigation._childrenNavigation as a side-effect", function() + local navigation = { state = {} } + local result = getChildrenNavigationCache(navigation) + expect(result).to.never.equal(nil) + expect(navigation._childrenNavigation).to.equal(result) + end) + + it("should delete children cache keys that are no longer valid", function() + local navigation = { + state = { + routes = { + { key = "one" }, + { key = "two" }, + { key = "three" }, + } + }, + _childrenNavigation = { + one = {}, + two = {}, + three = {}, + four = {}, + } + } + + local result = getChildrenNavigationCache(navigation) + expect(result.one).to.never.equal(nil) + expect(result.two).to.never.equal(nil) + expect(result.three).to.never.equal(nil) + expect(result.four).to.equal(nil) + end) + + it("should not delete children cache keys if in transitioning state", function() + local navigation = { + state = { + routes = { + { key = "one" }, + { key = "two" }, + { key = "three" }, + }, + isTransitioning = true, + }, + _childrenNavigation = { + one = {}, + two = {}, + three = {}, + four = {}, + } + } + + local result = getChildrenNavigationCache(navigation) + expect(result.one).to.never.equal(nil) + expect(result.two).to.never.equal(nil) + expect(result.three).to.never.equal(nil) + expect(result.four).to.never.equal(nil) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getNavigation.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getNavigation.spec.lua new file mode 100644 index 0000000..bfd1a59 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getNavigation.spec.lua @@ -0,0 +1,94 @@ +return function() + local NavigationEvents = require(script.Parent.Parent.NavigationEvents) + local getNavigation = require(script.Parent.Parent.getNavigation) + + local function makeTestBundle(testState) + testState = testState or { + routes = { + { key = "a" } + }, + index = 1, + } + + local testActions = {} + local bundle = { + testActions = testActions, + testState = testState, + testRouter = { + getActionCreators = function() + return testActions + end + }, + testDispatch = function() end, + testActionSubscribers = {}, + testGetScreenProps = function() end, + } + + function bundle.testGetCurrentNavigation() + return bundle.navigation + end + + bundle.navigation = getNavigation( + bundle.testRouter, + bundle.testState, + bundle.testDispatch, + bundle.testActionSubscribers, + bundle.testGetScreenProps, + bundle.testGetCurrentNavigation + ) + + return bundle + end + + it("should build out correct public props", function() + local bundle = makeTestBundle() + + expect(bundle.navigation.actions).to.equal(bundle.testActions) + expect(bundle.navigation.router).to.equal(bundle.testRouter) + expect(bundle.navigation.state).to.equal(bundle.testState) + expect(bundle.navigation.dispatch).to.equal(bundle.testDispatch) + expect(bundle.navigation.getScreenProps).to.equal(bundle.testGetScreenProps) + expect(#bundle.navigation._childrenNavigation).to.equal(0) + end) + + describe("isFocused tests", function() + it("should return focused=true for child key matching index", function() + local bundle = makeTestBundle() + expect(bundle.navigation.isFocused("a")).to.equal(true) + end) + + it("should return focused=false for child key not matching index", function() + local bundle = makeTestBundle({ + routes = { + { key = "a" }, + { key = "b" }, + }, + index = 2, + }) + expect(bundle.navigation.isFocused("a")).to.equal(false) + end) + + it("should return focused=true if no child key provided (parent always focused)", function() + local bundle = makeTestBundle() + expect(bundle.navigation.isFocused()).to.equal(true) + end) + end) + + describe("addListener tests", function() + it("should short-circuit subscriptions for non-Action events", function() + local bundle = makeTestBundle() + + local testHandler = function() end + bundle.navigation.addListener(NavigationEvents.WillFocus, testHandler) + expect(bundle.testActionSubscribers[testHandler]).to.equal(nil) + end) + + it("should add Action event handlers to actionSubscribers set", function() + local bundle = makeTestBundle() + + local testHandler = function() end + bundle.navigation.addListener(NavigationEvents.Action, testHandler) + expect(bundle.testActionSubscribers[testHandler]).to.equal(true) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/init.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/init.spec.lua new file mode 100644 index 0000000..fc6f7ae --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/init.spec.lua @@ -0,0 +1,156 @@ +return function() + local RoactNavigation = require(script.Parent.Parent) + local Roact = require(script.Parent.Parent.Parent.Roact) + local StackPresentationStyle = require(script.Parent.Parent.views.StackView.StackPresentationStyle) + local NoneSymbol = require(script.Parent.Parent.NoneSymbol) + + it("should load", function() + require(script.Parent.Parent) + end) + + it("should return a function for createAppContainer", function() + expect(type(RoactNavigation.createAppContainer)).to.equal("function") + end) + + it("should return a function for getNavigation", function() + expect(type(RoactNavigation.getNavigation)).to.equal("function") + end) + + it("should return an appropriate table for Context", function() + expect(type(RoactNavigation.Context)).to.equal("table") + expect(type(RoactNavigation.Context.Provider)).to.equal("table") + expect(type(RoactNavigation.Context.Consumer)).to.equal("table") + expect(type(RoactNavigation.Context.connect)).to.equal("function") + end) + + it("should return a table for Provider", function() + expect(type(RoactNavigation.Provider)).to.equal("table") + end) + + it("should return a table for Consumer", function() + expect(type(RoactNavigation.Consumer)).to.equal("table") + end) + + it("should return a function for connect", function() + expect(type(RoactNavigation.connect)).to.equal("function") + end) + + it("should return a function for withNavigation", function() + expect(type(RoactNavigation.withNavigation)).to.equal("function") + end) + + it("should return a function for withNavigationFocus", function() + expect(type(RoactNavigation.withNavigationFocus)).to.equal("function") + end) + + it("should return a function for createSwitchNavigator", function() + expect(type(RoactNavigation.createSwitchNavigator)).to.equal("function") + end) + + it("should return a function for createStackNavigator", function() + expect(type(RoactNavigation.createStackNavigator)).to.equal("function") + end) + + it("should return a function for createNavigator", function() + expect(type(RoactNavigation.createNavigator)).to.equal("function") + end) + + it("should return a function for StackRouter", function() + expect(type(RoactNavigation.StackRouter)).to.equal("function") + end) + + it("should return a function for SwitchRouter", function() + expect(type(RoactNavigation.SwitchRouter)).to.equal("function") + end) + + it("should return a function for TabRouter", function() + expect(type(RoactNavigation.TabRouter)).to.equal("function") + end) + + it("should return a table for Actions", function() + expect(type(RoactNavigation.Actions)).to.equal("table") + end) + + it("should return a table for StackActions", function() + expect(type(RoactNavigation.StackActions)).to.equal("table") + end) + + it("should return a table for BackBehavior", function() + expect(type(RoactNavigation.BackBehavior)).to.equal("table") + end) + + it("should return a table for Events", function() + expect(type(RoactNavigation.Events)).to.equal("table") + end) + + it("should return a valid component for EventsAdapter", function() + expect(RoactNavigation.EventsAdapter.render).never.to.equal(nil) + local instance = Roact.mount(Roact.createElement(RoactNavigation.EventsAdapter, { + navigation = { + addListener = function() + return { disconnect = function() end } + end + } + })) + Roact.unmount(instance) + end) + + it("should return StackPresentationStyle", function() + expect(RoactNavigation.StackPresentationStyle).to.equal(StackPresentationStyle) + end) + + it("should return NoneSymbol", function() + expect(RoactNavigation.None).to.equal(NoneSymbol) + end) + + it("should return a valid component for SceneView", function() + expect(RoactNavigation.SceneView.render).never.to.equal(nil) + local instance = Roact.mount(Roact.createElement(RoactNavigation.SceneView, { + navigation = {}, + component = function() end, + })) + Roact.unmount(instance) + end) + + it("should return a valid component for SwitchView", function() + expect(RoactNavigation.SwitchView.render).never.to.equal(nil) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo", } + }, + index = 1, + } + } + + local instance = Roact.mount(Roact.createElement(RoactNavigation.SwitchView, { + descriptors = { + Foo = { + getComponent = function() + return function() end + end, + navigation = testNavigation, + } + }, + navigation = testNavigation, + })) + Roact.unmount(instance) + end) + + it("should return a function for createConfigGetter", function() + expect(type(RoactNavigation.createConfigGetter)).to.equal("function") + end) + + it("should return a function for getScreenForRouteName", function() + expect(type(RoactNavigation.getScreenForRouteName)).to.equal("function") + end) + + it("should return a function for validateRouteConfigMap", function() + expect(type(RoactNavigation.validateRouteConfigMap)).to.equal("function") + end) + + it("should return a function for getActiveChildNavigationOptions", function() + expect(type(RoactNavigation.getActiveChildNavigationOptions)).to.equal("function") + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/createAppContainer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/createAppContainer.lua new file mode 100644 index 0000000..9864779 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/createAppContainer.lua @@ -0,0 +1,300 @@ +local Roact = require(script.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Cryo) +local NavigationActions = require(script.Parent.NavigationActions) +local NavigationEvents = require(script.Parent.NavigationEvents) +local AppNavigationContext = require(script.Parent.views.AppNavigationContext) +local getNavigation = require(script.Parent.getNavigation) +local validate = require(script.Parent.utils.validate) + +local function validateProps(props) + if not props.navigation then + return + end + + local errStr = + "This navigator has both 'navigation' and container props. " .. + "It is unclear if it should own its own state. Remove the " .. + "container props or don't pass a 'navigation' prop." + + for key in pairs(props) do + validate(key == "screenProps" or key == "navigation", errStr) + end +end + +--[[ + Construct a container Roact component that will host the navigation hierarchy + specified by your main AppComponent. AppComponent must be a navigator created by + a Roact-Navigation helper function, or a stateful Roact component + + If you are using a custom stateful Roact component, make sure to set the 'router' + field so that it can be hooked into the navigation system. You must also pass your + 'navigation' prop to any child navigators. + + Additional props: + renderLoading - Roact component to render while the app is loading. + externalDispatchConnector - Function that Roact Navigation can use to connect to + externally triggered navigation Actions. This is useful + for external UI or handling of the Android back button. + + Ex: + local connector = function(rnDispatch) + -- You store rnDispatch and call it when you want to inject + -- an event from outside RN. + return function() + -- You disconnect rnDispatch when RN calls this. + end + end + + ... + Roact.createElement(MyRNAppContainer, { + externalDispatchConnector = connector, + }) +]] +return function(AppComponent) + validate(type(AppComponent) == "table" and AppComponent.router ~= nil, + "AppComponent must be a navigator or a stateful Roact component with a 'router' field") + + local containerName = string.format("NavigationContainer(%s)", tostring(AppComponent)) + local NavigationContainer = Roact.Component:extend(containerName) + + function NavigationContainer.getDerivedStateFromProps(nextProps) + validateProps(nextProps) + return nil + end + + function NavigationContainer:init() + validateProps(self.props) + + self._actionEventSubscribers = {} + self._initialAction = NavigationActions.init() + + local initialNav = nil + local containerIsStateful = self:_isStateful() + if containerIsStateful and not self.props.persistenceKey then + initialNav = AppComponent.router.getStateForAction(self._initialAction) + end + + self.state = { + nav = initialNav, + } + end + + function NavigationContainer:_updateExternalDispatchConnector() + local externalDispatchConnector = self.props.externalDispatchConnector + if self._subs then + self._subs() + self._subs = nil + end + + if externalDispatchConnector ~= nil then + self._subs = externalDispatchConnector(function(...) + if self._isMounted then + return self:dispatch(...) + end + + -- External dispatch while we're not mounted gets dropped on floor. + return false + end) + end + end + + function NavigationContainer:_renderLoading() + local renderLoading = self.props.renderLoading + if renderLoading then + return renderLoading() + else + return nil + end + end + + function NavigationContainer:render() + local navigation = self.props.navigation + + if self:_isStateful() then + local navState = self.state.nav + if not navState then + return self:_renderLoading() + end + + if not self._navigation or self._navigation.state ~= navState then + self._navigation = getNavigation( + AppComponent.router, + navState, + function(...) + return self:dispatch(...) + end, + self._actionEventSubscribers, + function(...) + return self:_getScreenProps(...) + end, + function() + return self._navigation + end + ) + end + + navigation = self._navigation + end + + validate(navigation ~= nil, "failed to get navigation") + + return Roact.createElement(AppNavigationContext.Provider, { + navigation = navigation, + }, { + -- Provide navigation prop for top-level component so it doesn't have to connect. + AppComponent = Roact.createElement(AppComponent, Cryo.Dictionary.join(self.props, { + navigation = navigation, + })) + }) + end + + function NavigationContainer:didMount() + self._isMounted = true + + self:_updateExternalDispatchConnector() + + if not self:_isStateful() then + return + end + + local action = self._initialAction + local startupState = self.state.nav + + if not startupState then + startupState = AppComponent.router.getStateForAction(action) + end + + local function dispatchActionEvents() + -- _actionEventSubscribers is a table(handler, true), e.g. a Set container + for subscriber in pairs(self._actionEventSubscribers) do + subscriber({ + type = NavigationEvents.Action, + action = action, + state = self.state.nav, + -- there is no lastState for initial mounting + }) + end + end + + if startupState ~= self.state.nav then + self:setState({ + nav = startupState + }) + end + + -- This must be spawned until we get async setState callback handler in Roact + spawn(dispatchActionEvents) + end + + function NavigationContainer:willUnmount() + self._isMounted = false + + -- TODO: Disconnect from from URL listener once implemented + + if self._subs then + self._subs() + self._subs = nil + end + end + + function NavigationContainer:didUpdate(oldProps) + -- Clear cached _navState every time we update. + if self._navState == self.state.nav then + self._navState = nil + end + + if self.props.externalDispatchConnector ~= oldProps.externalDispatchConnector then + self:_updateExternalDispatchConnector() + end + end + + function NavigationContainer:_isStateful() + return not self.props.navigation + end + + -- NOTE: Not implementing _validateProps; it is duplicate + + -- NOTE: Not implementing _handleOpenURL; app should have a component + -- that transforms URLs into paths for AppContainer instead. + + function NavigationContainer:_onNavigationStateChange(prevNav, nextNav, action) + local onNavigationStateChange = self.props.onNavigationStateChange + + if type(onNavigationStateChange) == "function" then + onNavigationStateChange(prevNav, nextNav, action) + end + end + + function NavigationContainer:_getScreenProps(propKey, defaultValue) + local screenProps = self.props.screenProps or {} + if propKey ~= nil then + return screenProps[propKey] or defaultValue + end + + -- Legacy: return original table if no args provided + return screenProps + end + + function NavigationContainer:dispatch(action) + if self.props.navigation then + return self.props.navigation.dispatch(action) + end + + self._navState = self._navState or self.state.nav + + local lastNavState = self._navState + validate(lastNavState ~= nil, "navState should be set in constructor if stateful") + + local reducedState = AppComponent.router.getStateForAction(action, lastNavState) + local navState = reducedState + if not navState then + navState = lastNavState + end + + local function dispatchActionEvents() + -- _actionEventSubscribers is a table(handler, true), e.g. a Set container + for subscriber in pairs(self._actionEventSubscribers) do + subscriber({ + type = NavigationEvents.Action, + action = action, + state = navState, + lastState = lastNavState, + }) + end + end + + if reducedState == nil then + -- Router returns nil when action has been handled and there is no state change. + -- dispatch() must return true whenever something has been handled. + dispatchActionEvents() + return true + end + + if navState ~= lastNavState then + -- Update cache to ensure that subsequent calls do not discard this change + self._navState = navState + + -- TODO: We have to dispatch events before or after setState (which mounts/unmounts components) + -- based upon the specific event type, to ensure that pages get them in the correct order... + + self:setState({ + nav = navState + }) + + -- Must be spawned until we get async setState callback handler in Roact. + spawn(function() + self:_onNavigationStateChange(lastNavState, navState, action) + dispatchActionEvents() + -- TODO: Add call to persist navigation state here, if we ever implement it. + end) + + return true + end + + spawn(dispatchActionEvents) + + return false + end + + return NavigationContainer +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildEventSubscriber.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildEventSubscriber.lua new file mode 100644 index 0000000..5bdb0e0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildEventSubscriber.lua @@ -0,0 +1,150 @@ +local Cryo = require(script.Parent.Parent.Cryo) +local NavigationEvents = require(script.Parent.NavigationEvents) +local createSubscriberEventsStateTable = require(script.Parent.utils.createSubscriberEventsStateTable) +local validate = require(script.Parent.utils.validate) + +--[[ + This utility will fire focus and blur events for the child based upon action events + and the current navigation state. + + Args: + addListener - Functor to add an event listener to navigation dispatch system. + key - The key for the subscribing child. + initialState - Initial state to set for the child event monitor (optional). +]] +return function(addListener, key, initialState) + initialState = initialState or "Blurred" + + local upstreamSubscribers = {} + + local subscriberMap = { + [NavigationEvents.Action] = {}, + [NavigationEvents.WillFocus] = {}, + [NavigationEvents.DidFocus] = {}, + [NavigationEvents.WillBlur] = {}, + [NavigationEvents.DidBlur] = {}, + [NavigationEvents.Refocus] = {}, + } + + local function emit(subscriberType, payload) + local payloadWithType = Cryo.Dictionary.join(payload or {}, { type = subscriberType }) + local subscribers = subscriberMap[subscriberType] + if subscribers then + for _, subs in ipairs(subscribers) do + subs(payloadWithType) + end + end + end + + local function disconnectAll() + for _, subscriberList in pairs(subscriberMap) do + for x in pairs(subscriberList) do + subscriberList[x] = nil + end + end + + for _, subs in pairs(upstreamSubscribers) do + if subs then + subs.disconnect() + end + end + end + + -- Each event subscriber (e.g. screen) has its own state table that tracks the events which + -- drive focused/unfocused transitions. Screens all start life blurred, including the initial pages + -- displayed by a navigator. The latter need special treatment so they still receive their + -- willFocus+didFocus events. + local eventStateTable = createSubscriberEventsStateTable(key, initialState, emit, disconnectAll) + + for eventType in pairs(subscriberMap) do + upstreamSubscribers[eventType] = addListener(eventType, function(payload) + -- Refocus events don't use a specialized child payload or T/A event key marks. Propagate immediately. + -- We also allow arbitrary payloads with Refocus so we do not want to try to parse the table. + if eventType == NavigationEvents.Refocus then + eventStateTable:handleEvent(tostring(NavigationEvents.Refocus), payload) + return + end + + local state = payload.state + local lastState = payload.lastState + local action = payload.action + + local routes = state and state.routes + local lastRoutes = lastState and lastState.routes + + local focusKey = routes and routes[state.index].key or nil + local isChildFocused = focusKey == key + local isTransitioning = state and state.isTransitioning or false + + local lastRoute = nil + if lastRoutes then + for _, route in ipairs(lastRoutes) do + if route.key == key then + lastRoute = route + break + end + end + end + + local newRoute = nil + if routes then + for _, route in ipairs(routes) do + if route.key == key then + newRoute = route + break + end + end + end + + local childPayload = { + context = string.format("%s:%s_%s", key, tostring(action.type), payload.context or 'Root'), + state = newRoute, + lastState = lastRoute, + action = action, + type = eventType, + } + + -- State table figures out all the details of what conditions lead to event propagation based + -- upon a string eventKey. + local activeKey = isChildFocused and "A" or "" + local transitioningKey = isTransitioning and "T" or "" + local eventKey = tostring(eventType) .. activeKey .. transitioningKey + + eventStateTable:handleEvent(eventKey, childPayload) + + -- Regardless of what state transition we've propagated earlier, shut down the state machine + -- if the route has been removed from the nav history because that means the page is gone. + -- (This also disconnects the event listeners.) + if not newRoute then + eventStateTable.events.shutdown() + end + end) + end + + return { + addListener = function(eventType, eventHandler) + local subscribers = subscriberMap[eventType] + validate(subscribers ~= nil, "Invalid event type '%s'", tostring(eventType)) + validate(type(eventHandler) == "function", + "eventHandler for '%s' must be a function", tostring(eventType)) + table.insert(subscribers, eventHandler) + return { + disconnect = function() + for idx, subs in ipairs(subscribers) do + if subs == eventHandler then + table.remove(subscribers, idx) + break + end + end + end + } + end, + emit = function(eventType, payload) + validate(eventType == NavigationEvents.Refocus, + "navigation.emit only supports NavigationEvents.Refocus currently.") + validate(payload == nil or type(payload) == "table", + "navigation.emit payloads must be a table or nil") + emit(eventType, payload) + end, + } +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildNavigation.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildNavigation.lua new file mode 100644 index 0000000..b4afb1b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildNavigation.lua @@ -0,0 +1,115 @@ +local Cryo = require(script.Parent.Parent.Cryo) +local getChildEventSubscriber = require(script.Parent.getChildEventSubscriber) +local getChildRouter = require(script.Parent.routers.getChildRouter) +local getNavigationActionCreators = require(script.Parent.routers.getNavigationActionCreators) +local getChildrenNavigationCache = require(script.Parent.getChildrenNavigationCache) + +local function createParamGetter(route) + return function(paramName, defaultValue) + local params = route.params + return params and params[paramName] or defaultValue + end +end + +local function getChildNavigation(navigation, childKey, getCurrentParentNavigation) + local children = getChildrenNavigationCache(navigation) + + local childRoute = nil + for _, route in ipairs(navigation.state.routes) do + if route.key == childKey then + childRoute = route + break + end + end + + if not childRoute then + return nil + end + + local requestedChild = children[childKey] + + if requestedChild and requestedChild.state == childRoute then + return requestedChild + end + + local childRouter = getChildRouter(navigation.router, childRoute.routeName) + + -- If the route has children that match our routes schema then get a reference + -- to the focused grandchild so we can pass the correct action creators to the + -- child router so that any action that depends on the child route will behave + -- as expected. + local focusedGrandChildRoute = nil + if childRoute.routes and type(childRoute.index) == "number" then + focusedGrandChildRoute = childRoute.routes[childRoute.index] + end + + local childRouterActionCreators = childRouter and + childRouter.getActionCreators(focusedGrandChildRoute, childRoute.key) or {} + + local actionCreators = Cryo.Dictionary.join( + navigation.actions or {}, + navigation.router.getActionCreators(childRoute, navigation.state.key) or {}, + childRouterActionCreators or {}, + getNavigationActionCreators(childRoute) or {}) + + local actionHelpers = {} + for key, creator in pairs(actionCreators) do + actionHelpers[key] = function(...) + local action = creator(...) + return navigation.dispatch(action) + end + end + + if requestedChild then + -- Update cache value for requestedChild because child's state has changed + children[childKey] = Cryo.Dictionary.join(requestedChild, actionHelpers, { + state = childRoute, + router = childRouter, + actions = actionCreators, + getParam = createParamGetter(childRoute), + }) + + return children[childKey] + else + -- No cached value for requestedChild. Create a new entry. + local childSubscriber = getChildEventSubscriber(navigation.addListener, childKey) + + children[childKey] = Cryo.Dictionary.join(actionHelpers, { + state = childRoute, + router = childRouter, + actions = actionCreators, + getParam = createParamGetter(childRoute), + getChildNavigation = function(grandChildKey) + return getChildNavigation(children[childKey], grandChildKey, function() + local nav = getCurrentParentNavigation() + return nav and nav.getChildNavigation(childKey) or nil + end) + end, + isFocused = function() + local currentNavigation = getCurrentParentNavigation() + if not currentNavigation then + return false + end + + local state = currentNavigation.state + local routes = state.routes + local index = state.index + + if not currentNavigation.isFocused() then + return false + end + + -- If we're transitioning to this state then we are NOT focused until the transition is over. + return (routes[index].key == childKey and state.isTransitioning ~= true) or false + end, + dispatch = navigation.dispatch, + getScreenProps = navigation.getScreenProps, + addListener = childSubscriber.addListener, + emit = childSubscriber.emit, + }) + + return children[childKey] + end +end + +return getChildNavigation diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildrenNavigationCache.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildrenNavigationCache.lua new file mode 100644 index 0000000..b9271da --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildrenNavigationCache.lua @@ -0,0 +1,26 @@ +return function(navigation) + if not navigation then + return {} + end + + if not navigation._childrenNavigation then + navigation._childrenNavigation = {} + end + + local childrenNavigationCache = navigation._childrenNavigation + + local childKeys = {} + for _, route in ipairs(navigation.state.routes or {}) do + childKeys[route.key] = true + end + + if not navigation.state.isTransitioning then + for cacheKey, _ in pairs(childrenNavigationCache) do + if not childKeys[cacheKey] then + childrenNavigationCache[cacheKey] = nil + end + end + end + + return navigation._childrenNavigation +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getNavigation.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getNavigation.lua new file mode 100644 index 0000000..2b8568f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getNavigation.lua @@ -0,0 +1,52 @@ +local Cryo = require(script.Parent.Parent.Cryo) +local NavigationEvents = require(script.Parent.NavigationEvents) +local getNavigationActionCreators = require(script.Parent.routers.getNavigationActionCreators) +local getChildNavigation = require(script.Parent.getChildNavigation) +local getChildrenNavigationCache = require(script.Parent.getChildrenNavigationCache) + +return function(router, state, dispatch, actionSubscribers, getScreenProps, getCurrentNavigation) + local actions = router.getActionCreators(state, nil) + + local navigation = { + actions = actions, + router = router, + state = state, + dispatch = dispatch, + getScreenProps = getScreenProps, + _childrenNavigation = getChildrenNavigationCache(getCurrentNavigation()), + } + + function navigation.getChildNavigation(childKey) + return getChildNavigation(navigation, childKey, getCurrentNavigation) + end + + function navigation.isFocused(childKey) + local routes = getCurrentNavigation().state.routes + local index = getCurrentNavigation().state.index + + return not childKey or routes[index].key == childKey + end + + function navigation.addListener(event, handler) + if event ~= NavigationEvents.Action then + return { disconnect = function() end } + else + actionSubscribers[handler] = true + return { + disconnect = function() + actionSubscribers[handler] = nil + end + } + end + end + + local actionCreators = Cryo.Dictionary.join(getNavigationActionCreators(navigation.state), actions) + + for actionName, _ in pairs(actionCreators) do + navigation[actionName] = function(...) + navigation.dispatch(actionCreators[actionName](...)) + end + end + + return navigation +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/init.lua new file mode 100644 index 0000000..c30c4b5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/init.lua @@ -0,0 +1,53 @@ +-- Generator information: +-- Human name: Roact Navigation +-- Variable name: RoactNavigation +-- Repo name: roact-navigation + +return { + -- Navigation container construction + createAppContainer = require(script.createAppContainer), + getNavigation = require(script.getNavigation), + + -- Context Access + Context = require(script.views.AppNavigationContext), + Provider = require(script.views.AppNavigationContext).Provider, + Consumer = require(script.views.AppNavigationContext).Consumer, + connect = require(script.views.AppNavigationContext).connect, + + withNavigation = require(script.views.withNavigation), + withNavigationFocus = require(script.views.withNavigationFocus), + + -- Navigators + createStackNavigator = require(script.navigators.createStackNavigator), + createSwitchNavigator = require(script.navigators.createSwitchNavigator), + createNavigator = require(script.navigators.createNavigator), + + -- Routers + StackRouter = require(script.routers.StackRouter), + SwitchRouter = require(script.routers.SwitchRouter), + TabRouter = require(script.routers.TabRouter), + + -- Navigation Actions + Actions = require(script.NavigationActions), + StackActions = require(script.StackActions), + BackBehavior = require(script.BackBehavior), + + -- Navigation Events + Events = require(script.NavigationEvents), + EventsAdapter = require(script.views.NavigationEventsAdapter), + + -- Additional Types + StackPresentationStyle = require(script.views.StackView.StackPresentationStyle), + None = require(script.NoneSymbol), + + -- Screen Views + SceneView = require(script.views.SceneView), + SwitchView = require(script.views.SwitchView), + StackView = require(script.views.StackView.StackView), + + -- Utilities + createConfigGetter = require(script.routers.createConfigGetter), + getScreenForRouteName = require(script.routers.getScreenForRouteName), + validateRouteConfigMap = require(script.routers.validateRouteConfigMap), + getActiveChildNavigationOptions = require(script.utils.getActiveChildNavigationOptions), +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createNavigator.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createNavigator.spec.lua new file mode 100644 index 0000000..e890099 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createNavigator.spec.lua @@ -0,0 +1,78 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local createNavigator = require(script.Parent.Parent.createNavigator) + + local testRouter = { + getScreenOptions = function() return nil end, + } + + it("should return a Roact component that exposes navigator fields", function() + local testComponentMounted = nil + local TestViewComponent = Roact.Component:extend("TestViewComponent") + function TestViewComponent:render() end + function TestViewComponent:didMount() testComponentMounted = true end + function TestViewComponent:willUnmount() testComponentMounted = false end + + local testNavOptions = {} + + local navigator = createNavigator(TestViewComponent, testRouter, { + navigationOptions = testNavOptions, + }) + + expect(type(navigator.render)).to.equal("function") + expect(navigator.router).to.equal(testRouter) + expect(navigator.navigationOptions).to.equal(testNavOptions) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + }, + index = 1 + }, + getChildNavigation = function() return nil end, -- stub + } + + -- Try to mount it + local instance = Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + + expect(testComponentMounted).to.equal(true) + Roact.unmount(instance) + expect(testComponentMounted).to.equal(false) + end) + + it("should throw when trying to mount without navigation prop", function() + local TestViewComponent = function() end + + local navigator = createNavigator(TestViewComponent, testRouter, { + navigationOptions = {} + }) + + expect(function() + Roact.mount(Roact.createElement(navigator)) + end).to.throw() + end) + + it("should throw when trying to mount without routes", function() + local TestViewComponent = function() end + + local navigator = createNavigator(TestViewComponent, testRouter, { + navigationOptions = {} + }) + + local testNavigation = { + state = { + index = 1 + }, + getChildNavigation = function() return nil end, -- stub + } + + expect(function() + Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + end).to.throw() + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createStackNavigator.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createStackNavigator.spec.lua new file mode 100644 index 0000000..73d7f5f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createStackNavigator.spec.lua @@ -0,0 +1,43 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local createStackNavigator = require(script.Parent.Parent.createStackNavigator) + local getChildNavigation = require(script.Parent.Parent.Parent.getChildNavigation) + + it("should return a mountable Roact component", function() + local navigator = createStackNavigator({ + routes = { + Foo = function() end + }, + initialRouteName = "Foo", + }) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + }, + index = 1 + }, + router = navigator.router + } + + function testNavigation.getChildNavigation(childKey) + return getChildNavigation(testNavigation, childKey, function() + return testNavigation + end) + end + + function testNavigation.addListener(symbol, callback) + return { + disconnect = function() end + } + end + + local instance = Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + + Roact.unmount(instance) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createSwitchNavigator.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createSwitchNavigator.spec.lua new file mode 100644 index 0000000..c331c26 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createSwitchNavigator.spec.lua @@ -0,0 +1,43 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local createSwitchNavigator = require(script.Parent.Parent.createSwitchNavigator) + local getChildNavigation = require(script.Parent.Parent.Parent.getChildNavigation) + + it("should return a mountable Roact component", function() + local navigator = createSwitchNavigator({ + routes = { + Foo = function() end + }, + initialRouteName = "Foo", + }) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + }, + index = 1 + }, + router = navigator.router + } + + function testNavigation.getChildNavigation(childKey) + return getChildNavigation(testNavigation, childKey, function() + return testNavigation + end) + end + + function testNavigation.addListener(symbol, callback) + return { + disconnect = function() end + } + end + + local instance = Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + + Roact.unmount(instance) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createNavigator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createNavigator.lua new file mode 100644 index 0000000..5d60055 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createNavigator.lua @@ -0,0 +1,78 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local validate = require(script.Parent.Parent.utils.validate) + +return function(navigatorViewComponent, router, navigationConfig) + local Navigator = Roact.Component:extend("Navigator") + + -- These statics need to be accessible to routers + Navigator.router = router + Navigator.navigationOptions = navigationConfig.navigationOptions + + function Navigator:init() + local screenProps = self.props.screenProps + + self.state = { + descriptors = {}, + screenProps = screenProps + } + end + + function Navigator.getDerivedStateFromProps(nextProps, prevState) + local prevDescriptors = prevState.descriptors + local navigation = nextProps.navigation + local screenProps = nextProps.screenProps + + validate(navigation ~= nil, "The navigation prop is missing for this navigator") + + local routes = navigation.state.routes + + validate(type(routes) == "table", "No 'routes' found in navigation state. " .. + "Don't try to pass the navigation prop from a Roact component to a Navigator child.") + + local descriptors = {} + + for _, route in ipairs(routes) do + if prevDescriptors and prevDescriptors[route.key] and + route == prevDescriptors[route.key].state and + screenProps == prevState.screenProps then + descriptors[route.key] = prevDescriptors[route.key] + else + local getComponent = function() + return router.getComponentForRouteName(route.routeName) + end + + local childNavigation = navigation.getChildNavigation(route.key) + local options = router.getScreenOptions(childNavigation, screenProps) + + descriptors[route.key] = { + key = route.key, + getComponent = getComponent, + options = options, + state = route, + navigation = childNavigation, + } + end + end + + return { + descriptors = descriptors, + screenProps = screenProps, + } + end + + function Navigator:render() + local navigation = self.props.navigation + local screenProps = self.state.screenProps + local descriptors = self.state.descriptors + + return Roact.createElement(navigatorViewComponent, Cryo.Dictionary.join(self.props, { + screenProps = screenProps, + navigation = navigation, + navigationConfig = navigationConfig, + descriptors = descriptors, + })) + end + + return Navigator +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createStackNavigator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createStackNavigator.lua new file mode 100644 index 0000000..35697c2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createStackNavigator.lua @@ -0,0 +1,12 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local createNavigator = require(script.Parent.createNavigator) +local StackRouter = require(script.Parent.Parent.routers.StackRouter) +local StackView = require(script.Parent.Parent.views.StackView.StackView) + +return function(config) + local router = StackRouter(config) + return createNavigator(StackView, router, Cryo.Dictionary.join(config, { + routes = Cryo.None, -- navigator config doesn't need routes + })) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createSwitchNavigator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createSwitchNavigator.lua new file mode 100644 index 0000000..05e525c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createSwitchNavigator.lua @@ -0,0 +1,27 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local createNavigator = require(script.Parent.createNavigator) +local SwitchRouter = require(script.Parent.Parent.routers.SwitchRouter) +local SwitchView = require(script.Parent.Parent.views.SwitchView) + +--[[ + Creates a navigator component that provides simple screen "switcher" behavior. + Each page is mutually exclusive and no transition animation is used. + + Additional config options: + order : List + Specifies the index order for page components, e.g. for use with tab bars. + keepVisitedScreensMounted : Boolean (false) + Set to true if you want to keep previously visited screens mounted for better performance. + resetOnBlur : Boolean (true) + Set to false if you want to preserve existing state for child navigators. + backBehavior : BackBehavior (None) + Set to BackBehavior.InitialRoute to allow a goBack() operation to return to the + initial route name. By default, the SwitchNavigator will not do anything on a back action. +]] +return function(config) + local router = SwitchRouter(config) + return createNavigator(SwitchView, router, Cryo.Dictionary.join(config, { + routes = Cryo.None, -- navigator config doesn't need routes, remove from props + })) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/StackRouter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/StackRouter.lua new file mode 100644 index 0000000..021ad56 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/StackRouter.lua @@ -0,0 +1,565 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local NavigationActions = require(script.Parent.Parent.NavigationActions) +local StackActions = require(script.Parent.Parent.StackActions) +local KeyGenerator = require(script.Parent.Parent.utils.KeyGenerator) +local StateUtils = require(script.Parent.Parent.StateUtils) +local getScreenForRouteName = require(script.Parent.getScreenForRouteName) +local createConfigGetter = require(script.Parent.createConfigGetter) +local validateRouteConfigMap = require(script.Parent.validateRouteConfigMap) +local validate = require(script.Parent.Parent.utils.validate) +local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol) +local NoneSymbol = require(script.Parent.Parent.NoneSymbol) + +local STACK_ROUTER_ROOT_KEY = "StackRouterRoot" +local CHILD_IS_SCREEN = NavigationSymbol("CHILD_IS_SCREEN") + +local defaultActionCreators = function() return {} end + +local function behavesLikePushAction(action) + return action.type == NavigationActions.Navigate or + action.type == StackActions.Push +end + +local function isResetToRootStack(action) + return action.type == StackActions.Reset and action.key == NoneSymbol +end + +return function(config) + validate(type(config) == "table", "config must be a table") + + local routeConfigs = validateRouteConfigMap(config.routes) + local routeNames = Cryo.Dictionary.keys(routeConfigs) + + -- find child child routers + local childRouters = {} + for _, routeName in ipairs(routeNames) do + local screen = getScreenForRouteName(routeConfigs, routeName) + if type(screen) == "table" and screen.router then + -- if it has a router then it's a navigator + childRouters[routeName] = screen.router + else + -- TODO: This is a hack to make this code behave like React-Navigation's usage of + -- null and undefined values for childRouters. We should come up with a better approach. + childRouters[routeName] = CHILD_IS_SCREEN + end + end + + local getCustomActionCreators = config.getCustomActionCreators or defaultActionCreators + + local initialRouteParams = config.initialRouteParams or {} + local initialRouteName = validate(config.initialRouteName, "initialRouteName must be provided") + + local initialRouteIndex = Cryo.List.find(routeNames, initialRouteName) + + -- dump an error if initialRouteName is not in routes. + if initialRouteIndex == nil then + local availableRouteStr = "" + for _, name in ipairs(routeNames) do + availableRouteStr = availableRouteStr .. name .. "," + end + + error(string.format("Invalid initialRouteName '%s'. Must be one of [%s]", initialRouteName, availableRouteStr), 2) + end + + local initialChildRouter = childRouters[initialRouteName] + + local function getInitialState() + local route = {} + + if initialChildRouter ~= nil and initialChildRouter ~= CHILD_IS_SCREEN then + route = initialChildRouter.getStateForAction(NavigationActions.init({ + params = initialRouteParams, + })) + end + + local initialRouteConfig = routeConfigs[initialRouteName] + local initialRouteConfigParams = type(initialRouteConfig) == "table" and initialRouteConfig.params or {} + + local params = Cryo.Dictionary.join( + initialRouteConfigParams, -- params set in routes table! + route.params or {}, + initialRouteParams or {} -- params provided at top level + ) + + local initialRouteKey = config.initialRouteKey + route = Cryo.Dictionary.join(route, params, { + routeName = initialRouteName, + key = initialRouteKey or KeyGenerator.generateKey() + }) + + return { + key = STACK_ROUTER_ROOT_KEY, + isTransitioning = false, + index = 1, + routes = { route } + } + end + + local function getParamsForRouteAndAction(routeName, action) + local routeConfig = routeConfigs[routeName] + if type(routeConfig) == "table" and routeConfig.params then + return Cryo.Dictionary.join(routeConfig.params, action.params) + else + return action.params + end + end + + -- Strip out the CHILD_IS_SCREEN hacked elements before exposing publicly. + local strippedChildRouters = {} + for routerName, router in pairs(childRouters) do + if router ~= CHILD_IS_SCREEN then + strippedChildRouters[routerName] = router + end + end + + local StackRouter = { + childRouters = strippedChildRouters, + getScreenOptions = createConfigGetter(routeConfigs, config.defaultNavigationOptions), + _CHILD_IS_SCREEN = CHILD_IS_SCREEN, -- expose symbol for testing purposes + } + + function StackRouter.getComponentForState(state) + local activeChildRoute = state.routes[state.index] or {} + local routeName = activeChildRoute.routeName + validate(routeName, "There is no route defined for index '%d'. " .. + "Make sure that you passed in a navigation state with a " .. + "valid stack index.", state.index) + + local childRouter = childRouters[routeName] + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + return childRouters[routeName].getComponentForState(activeChildRoute) + end + + return getScreenForRouteName(routeConfigs, routeName) + end + + function StackRouter.getComponentForRouteName(routeName) + return getScreenForRouteName(routeConfigs, routeName) + end + + function StackRouter.getActionCreators(route, navStateKey) + return Cryo.Dictionary.join(getCustomActionCreators(route, navStateKey), { + pop = function(n, params) + return StackActions.pop(Cryo.Dictionary.join({ + n = n, + }, params or {})) + end, + popToTop = function(params) + return StackActions.popToTop(params) + end, + push = function(routeName, params, action) + return StackActions.push({ + routeName = routeName, + params = params, + action = action, + }) + end, + replace = function(replaceWith, params, action, newKey) + if type(replaceWith) == "string" then + return StackActions.replace({ + routeName = replaceWith, + params = params, + action = action, + key = route.key, + newKey = newKey, + }) + end + + validate(type(replaceWith) == "table", "replaceWith must be a table or string") + validate(params == nil, "params cannot be provided to .replace() when specifying a table") + validate(action == nil, "Child action cannot be provided to .replace() when specifying a table") + validate(newKey == nil, "newKey cannot be provided to .replace() when specifying a table") + + return StackActions.replace(replaceWith) + end, + reset = function(actions, index) + local resetIndex = index + if index == nil then + resetIndex = #actions + end + + return StackActions.reset({ + actions = actions, + index = resetIndex, + key = navStateKey, + }) + end, + dismiss = function() + return NavigationActions.back({ + key = navStateKey, + }) + end, + }) + end + + function StackRouter.getStateForAction(action, state) + -- Set up initial state if needed + state = state or getInitialState() + + local activeChildRoute = state.routes[state.index] + + if not isResetToRootStack(action) and action.type ~= NavigationActions.Navigate then + local activeChildRouter = childRouters[activeChildRoute.routeName] + if activeChildRouter ~= nil and activeChildRouter ~= CHILD_IS_SCREEN then + local route = activeChildRouter.getStateForAction(action, activeChildRoute) + if route ~= nil and route ~= activeChildRoute then + return StateUtils.replaceAt( + state, + activeChildRoute.key, + route, + action.type == NavigationActions.SetParams -- don't change index for setParam action + ) + end + end + elseif action.type == NavigationActions.Navigate then + -- Traverse routes from top of the stack to the bottom; active route has first opportunity + for i = #state.routes, 1, -1 do + local childRoute = state.routes[i] + local childRouter = childRouters[childRoute.routeName] + local childAction = action + if action.routeName == childRoute.routeName and action.action then + childAction = action.action + end + + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + local nextRouteState = childRouter.getStateForAction(childAction, childRoute) + if nextRouteState == nil or nextRouteState ~= childRoute then + local newState = StateUtils.replaceAndPrune( + state, + nextRouteState and nextRouteState.key or childRoute.key, + nextRouteState and nextRouteState or childRoute + ) + + local newTransitioning = state.isTransitioning + if state.index ~= newState.index then + newTransitioning = action.immediate ~= true + end + + return Cryo.Dictionary.join(newState, { + isTransitioning = newTransitioning, + }) + end + end + end + end + + -- Handle push and navigation actions. This must happen after focused child router + -- has had its chance to handle the action. + if behavesLikePushAction(action) and childRouters[action.routeName] ~= nil then + local childRouter = childRouters[action.routeName] + validate(action.type ~= StackActions.Push or action.key == nil, + "StackRouter does not support key on the push action") + + -- Before pushing new route, try to find existing one in the stack. + local lastRouteIndex = nil + for idx, route in ipairs(state.routes) do + if (action.key and route.key == action.key) or (route.routeName == action.routeName) then + lastRouteIndex = idx + break + end + end + + -- An instance of this route exists already and we're dealing with a Navigate action. + if action.type ~= StackActions.Push and lastRouteIndex ~= nil then + -- If index or params have not changed, leave state alone + if state.index == lastRouteIndex and not action.params then + return nil + end + + -- Remove unused routes at tail + local tailIndex = state.index == lastRouteIndex and lastRouteIndex or lastRouteIndex + 1 + local routes = Cryo.List.removeRange(state.routes, tailIndex, #state.routes) + + -- Apply params if provided + if action.params then + local route = state.routes[lastRouteIndex] + routes[lastRouteIndex] = Cryo.Dictionary.join(route, { + params = Cryo.Dictionary.join(route.params or {}, action.params) + }) + end + + -- Return state with new index, changing isTransitioning only if index has changed + local newIsTransitioning = state.isTransitioning + if state.index ~= lastRouteIndex then + newIsTransitioning = action.immediate ~= true + end + + return Cryo.Dictionary.join(state, { + isTransitioning = newIsTransitioning, + index = lastRouteIndex, + routes = routes, + }) + end + + local route + if childRouter ~= CHILD_IS_SCREEN then + -- Delegate to child router + local childAction = action.action or NavigationActions.init({ + params = getParamsForRouteAndAction(action.routeName, action) + }) + + route = Cryo.Dictionary.join({ + -- TODO: Does it make sense to wipe out the params here, or to incorporate params at all? + params = getParamsForRouteAndAction(action.routeName, action), + }, childRouter.getStateForAction(childAction), { + routeName = action.routeName, + key = action.key or KeyGenerator.generateKey(), + }) + else + -- Create new route from scratch + route = { + params = getParamsForRouteAndAction(action.routeName, action), + routeName = action.routeName, + key = action.key or KeyGenerator.generateKey(), + } + + end + + return Cryo.Dictionary.join(StateUtils.push(state, route), { + isTransitioning = action.immediate ~= true, + }) + elseif action.type == StackActions.Push and childRouters[action.routeName] == nil then + -- Return original state to bubble the action up + return state + end + + -- Handle navigation to other child routers that are not pushed yet. + if behavesLikePushAction(action) then + local childRouterNames = Cryo.Dictionary.keys(childRouters) + for _, childRouterName in ipairs(childRouterNames) do + local childRouter = childRouters[childRouterName] + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + -- Start with blank state for each child router + local initChildRoute = childRouter.getStateForAction(NavigationActions.init()) + + -- Check to see if it handles our action + local navigatedChildRoute = childRouter.getStateForAction(action, initChildRoute) + + local routeToPush = nil + if navigatedChildRoute == nil then + -- Push initial route if the router returned nil when handling action + routeToPush = initChildRoute + elseif navigatedChildRoute ~= initChildRoute then + -- Push new route if state changed in response to this action + routeToPush = navigatedChildRoute + end + + if routeToPush then + local route = Cryo.Dictionary.join(routeToPush, { + routeName = childRouterName, + key = action.key or KeyGenerator.generateKey(), + }) + + return Cryo.Dictionary.join(StateUtils.push(state, route), { + isTransitioning = action.immediate ~= true, + }) + end + end + end + end + + -- Handle pop-to-top behavior. This must happen after children have had a chance to handle + -- the action, so that the inner stack always pops first. + if action.type == StackActions.PopToTop then + -- Refuse to handle pop to top if a key is given that does not correspond to this router + if action.key and state.key ~= action.key then + return state + end + + -- If we're already at the top then return current state to allow action to bubble up. + if state.index <= 1 then + return state + end + + return Cryo.Dictionary.join(state, { + isTransitioning = action.immediate ~= true, + index = 1, + routes = { state.routes[1] } + }) + end + + if action.type == StackActions.Replace then + local routeIndex = nil + + -- If there is no key, set index to last route in stack + if not action.key and #state.routes > 0 then + routeIndex = #state.routes + else + for idx, route in ipairs(state.routes) do + if route.key == action.key then + routeIndex = idx + break + end + end + end + + if routeIndex then + local childRouter = childRouters[action.routeName] + local childState = {} + + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + local childAction = action.action or NavigationActions.init({ + params = getParamsForRouteAndAction(action.routeName, action) + }) + + childState = childRouter.getStateForAction(childAction) + end + + -- shallow copy and update routes + local routes = Cryo.List.join(state.routes) + routes[routeIndex] = Cryo.Dictionary.join({ + params = getParamsForRouteAndAction(action.routeName, action), + }, childState, { + routeName = action.routeName, + key = action.newKey or KeyGenerator.generateKey(), + }) + + return Cryo.Dictionary.join(state, { + routes = routes, + }) + end + end + + if action.type == NavigationActions.CompleteTransition and + (action.key == nil or action.key == state.key) and + action.toChildKey == state.routes[state.index].key and + state.isTransitioning then + return Cryo.Dictionary.join(state, { + isTransitioning = false, + }) + end + + if action.type == NavigationActions.SetParams then + local key = action.key + + local lastRouteIndex = nil + local lastRoute = nil + for idx, route in ipairs(state.routes) do + if route.key == key then + lastRouteIndex = idx + lastRoute = route + break + end + end + + if lastRoute then + local params = Cryo.Dictionary.join(lastRoute.params or {}, action.params or {}) + -- shallow copy and update routes + local routes = Cryo.List.join(state.routes) + routes[lastRouteIndex] = Cryo.Dictionary.join(lastRoute, { + params = params, + }) + + return Cryo.Dictionary.join(state, { + routes = routes, + }) + end + end + + if action.type == StackActions.Reset then + -- Only handle reset actions with matching key (or none) + if action.key ~= nil and action.key ~= state.key then + return state + end + + local specifiedActions = action.actions or {} + + local newRoutes = {} + for _, newStackAction in ipairs(specifiedActions) do + local router = childRouters[newStackAction.routeName] + + local childState = {} + if router ~= nil and router ~= CHILD_IS_SCREEN then + local childAction = newStackAction.action or NavigationActions.init({ + params = getParamsForRouteAndAction(newStackAction.routeName, newStackAction), + }) + + childState = router.getStateForAction(childAction) + end + + table.insert(newRoutes, Cryo.Dictionary.join({ + params = getParamsForRouteAndAction(newStackAction.routeName, newStackAction) + }, childState, { + routeName = newStackAction.routeName, + key = newStackAction.key or KeyGenerator.generateKey(), + })) + end + + return Cryo.Dictionary.join(state, { + routes = newRoutes, + index = action.index or #specifiedActions, + }) + end + + if action.type == NavigationActions.Back or + action.type == StackActions.Pop then + local key = action.key + local n = action.n + local immediate = action.immediate + + local backRouteIndex = state.index -- index to go back *FROM* + if action.type == StackActions.Pop and n ~= nil then + backRouteIndex = math.max(1, state.index - n + 1) + elseif key and key ~= NoneSymbol then + -- If key is specified and is not ours, we should NOT try to navigate back + -- because it might be intended for our parent! (So clear the backRouteIndex.) + backRouteIndex = 0 + for idx, route in ipairs(state.routes) do + if route.key == key then + backRouteIndex = idx + break + end + end + end + + if backRouteIndex > 1 then + return Cryo.Dictionary.join(state, { + routes = Cryo.List.removeRange(state.routes, backRouteIndex, #state.routes), + index = backRouteIndex - 1, + isTransitioning = immediate ~= true, + }) + end + end + + -- At this point, we've handled the behavior of the active route and any + -- stack actions. Now we allow non-active child routers to try to process the action, + -- and switch to them if they can handle it. + local keyIndex = action.key and StateUtils.indexOf(state, action.key) or nil + + -- Traverse from top of stack to bottom. + for i = #state.routes, 1, -1 do + local childRoute = state.routes[i] + -- Skip over the active route since we already let it try. + -- Also, skip calling getStateForAction on other child routers + -- if the provided key is in the route's state + if (childRoute.key ~= activeChildRoute.key) and + (not keyIndex or childRoute.key == action.key) then + local childRouter = childRouters[childRoute.routeName] + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + local route = childRouter.getStateForAction(action, childRoute) + if not route then + return state + end + + if route ~= childRoute then + return StateUtils.replaceAt( + state, + childRoute.key, + route, + -- don't change index for these action types + action.type == NavigationActions.SetParams or + action.type == NavigationActions.CompleteTransition + ) + end + end + end + end + + return state + end + + -- TODO: Implement StackRouter.getPathAndParamsForState after we add path expression support + -- TODO: Implement StackRouter.getActionForPathAndParams after we add path expression support + + return StackRouter +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/SwitchRouter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/SwitchRouter.lua new file mode 100644 index 0000000..c7675fd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/SwitchRouter.lua @@ -0,0 +1,331 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local NavigationActions = require(script.Parent.Parent.NavigationActions) +local BackBehavior = require(script.Parent.Parent.BackBehavior) +local getScreenForRouteName = require(script.Parent.getScreenForRouteName) +local createConfigGetter = require(script.Parent.createConfigGetter) +local validateRouteConfigMap = require(script.Parent.validateRouteConfigMap) +local validate = require(script.Parent.Parent.utils.validate) + +local defaultActionCreators = function() return {} end + +-- Until Cryo has a List function to do this, provide shallow copy+replace index +local function immutableReplaceListIndex(list, index, value) + local result = {} + for i, ival in ipairs(list) do + result[i] = ival + end + + result[index] = value + return result +end + +local function childrenUpdateWithoutSwitchingIndex(actionType) + return actionType == NavigationActions.SetParams or + actionType == NavigationActions.CompleteTransition +end + +local function collectChildRouters(routeConfigs) + local childRouters = {} + for routeName, _ in pairs(routeConfigs) do + local screen = getScreenForRouteName(routeConfigs, routeName) + if type(screen) == "table" and screen.router then + childRouters[routeName] = screen.router + end + end + + return childRouters +end + +local function getParamsForRoute(routeConfigs, routeName, initialParams) + local routeConfig = routeConfigs[routeName] + if type(routeConfig) == "table" and routeConfig.params then + return Cryo.Dictionary.join(routeConfig.params, initialParams) + else + return initialParams + end +end + + +return function(config) + validate(type(config) == "table", "config must be a table") + + local routeConfigs = validateRouteConfigMap(config.routes) + + -- Order is how we map the active index into the list of possible routes. + -- Lua does not guarantee any sense of order of table keys in dictionaries, so + -- we have to require the initialRouteName parameter instead defaulting to the + -- first route in the map. + local order = config.order or Cryo.Dictionary.keys(routeConfigs) + + local getCustomActionCreators = config.getCustomActionCreators or defaultActionCreators + local initialRouteParams = config.initialRouteParams or {} + + local initialRouteName = validate(config.initialRouteName, + "initialRouteName must be provided") + + local backBehavior = config.backBehavior or BackBehavior.None + local backShouldNavigateToInitialRoute = backBehavior == BackBehavior.InitialRoute + + local resetOnBlur = true + if type(config.resetOnBlur) == "boolean" then + resetOnBlur = config.resetOnBlur + end + + local initialRouteIndex = Cryo.List.find(order, initialRouteName) + if initialRouteIndex == nil then + local availableRouteStr = "" + for _, name in ipairs(order) do + availableRouteStr = availableRouteStr .. name .. "," + end + + error(string.format("Invalid initialRouteName '%s'. Must be one of [%s]", initialRouteName, availableRouteStr), 2) + end + + local childRouters = collectChildRouters(routeConfigs) + + local function resetChildRoute(routeName) + -- TODO: Do we want to merge initialRouteParams on TOP of route-specific params? + -- There is a comment in RoactNavigation that this is incorrect behavior, but they + -- do it to be consistent with their StackRouter. Do we even need this feature? + local initialParams = routeName == initialRouteName and initialRouteParams or {} + local params = getParamsForRoute(routeConfigs, routeName, initialParams) + local childRouter = childRouters[routeName] + if childRouter then + local childAction = NavigationActions.init() + return Cryo.Dictionary.join(childRouter.getStateForAction(childAction), { + key = routeName, + routeName = routeName, + params = params, + }) + else + return { + key = routeName, + routeName = routeName, + params = params, + } + end + end + + local function getNextState(prevState, possibleNextState) + if not prevState then + return possibleNextState + end + + if prevState.index ~= possibleNextState.index and resetOnBlur then + local prevRouteName = prevState.routes[prevState.index].routeName + local nextRoutes = immutableReplaceListIndex( + possibleNextState.routes, + prevState.index, + resetChildRoute(prevRouteName)) + + return Cryo.Dictionary.join(possibleNextState, { + routes = nextRoutes, + }) + else + return possibleNextState + end + end + + local function getInitialState() + return { + routes = Cryo.List.map(order, resetChildRoute), + index = initialRouteIndex, + isTransitioning = false, + } + end + + local SwitchRouter = { + childRouters = childRouters, + getScreenOptions = createConfigGetter(routeConfigs, config.defaultNavigationOptions) + } + + function SwitchRouter.getActionCreators(route, stateKey) + return getCustomActionCreators(route, stateKey) + end + + function SwitchRouter.getStateForAction(action, inputState) + local prevState = inputState and Cryo.Dictionary.join(inputState) or nil + local state = inputState or getInitialState() + local activeChildIndex = state.index + + if action.type == NavigationActions.Init then + -- TODO: React-Navigation has a comment that wonders why we merge params into child routes. + -- Need to understand if we really want to do this. + local params = action.params + if params then + state.routes = Cryo.List.map(state.routes, function(route) + local initialParams = route.routeName == initialRouteName and initialRouteParams or {} + return Cryo.Dictionary.join(route, { + params = Cryo.Dictionary.join(route.params, params, initialParams) + }) + end) + end + end + + -- Let the active child try to handle the action first. + local activeChildLastState = state.routes[state.index] + local activeChildRouter = childRouters[order[state.index]] + if activeChildRouter then + local activeChildState = activeChildRouter.getStateForAction(action, activeChildLastState) + if not activeChildState and inputState then + -- Child ran into error with known inputState. Propagate to caller. + return nil + end + + if activeChildState and activeChildState ~= activeChildLastState then + local routes = immutableReplaceListIndex(state.routes, state.index, activeChildState) + return getNextState(prevState, Cryo.Dictionary.join(state, { + routes = routes + })) + end + end + + -- Child did not handle it, so try to process the action ourselves. + local isBackEligible = not action.key or action.key == activeChildLastState.key + if action.type == NavigationActions.Back then + if isBackEligible and backShouldNavigateToInitialRoute then + activeChildIndex = initialRouteIndex + else + return state + end + end + + local didNavigate = false + if action.type == NavigationActions.Navigate then + for index, childId in ipairs(order) do + if childId == action.routeName then + activeChildIndex = index + didNavigate = true + break + end + end + + if didNavigate then + local childState = state.routes[activeChildIndex] + local childRouter = childRouters[action.routeName] + local newChildState = childState + + if action.action and childRouter then + local childStateUpdate = childRouter.getStateForAction(action.action, childState) + if childStateUpdate then + newChildState = childStateUpdate + end + end + + if action.params then + newChildState = Cryo.Dictionary.join(newChildState, { + params = Cryo.Dictionary.join(newChildState.params or {}, action.params) + }) + end + + if newChildState ~= childState then + local routes = immutableReplaceListIndex(state.routes, activeChildIndex, newChildState) + local nextState = Cryo.Dictionary.join(state, { + routes = routes, + index = activeChildIndex, + }) + + return getNextState(prevState, nextState) + elseif newChildState == childState and state.index == activeChildIndex and prevState then + return nil + end + end + end + + if action.type == NavigationActions.SetParams then + local key = action.key + local lastIndex, lastRoute + for index, route in ipairs(state.routes) do + if route.key == key then + lastIndex = index + lastRoute = route + break + end + end + + if lastRoute then + local params = Cryo.Dictionary.join(lastRoute.params or {}, action.params) + local mergedRoute = Cryo.Dictionary.join(lastRoute, { + params = params + }) + + local routes = immutableReplaceListIndex(state.routes, lastIndex, mergedRoute) + return getNextState(prevState, Cryo.Dictionary.join(state, { + routes = routes + })) + end + end + + if activeChildIndex ~= state.index then + return getNextState(prevState, Cryo.Dictionary.join(state, { index = activeChildIndex })) + elseif didNavigate and not inputState then + return state + elseif didNavigate then + return Cryo.Dictionary.join(state) + end + + -- Let other children handle it and switch to first child that returns a new state + local index = state.index + local routes = state.routes + + for i, childId in ipairs(order) do + if i ~= index then + local childRouter = childRouters[childId] + local childState = routes[i] + if childRouter then + childState = childRouter.getStateForAction(action, childState) + end + + if not childState then + index = i + break + end + + if childState ~= routes[i] then + routes = immutableReplaceListIndex(routes, i, childState) + index = i + break + end + end + end + + -- Nested routers can be updated after switching children with actions such as SetParams + -- and CompleteTransition + if childrenUpdateWithoutSwitchingIndex(action.type) then + index = state.index + end + + if index ~= state.index or routes ~= state.routes then + return getNextState(prevState, Cryo.Dictionary.join(state, { + index = index, + routes = routes, + })) + end + + return state + end + + function SwitchRouter.getComponentForState(state) + local activeRoute = state.routes[state.index] or {} + local routeName = activeRoute.routeName + validate(routeName, "There is no route defined for index '%d'. " .. + "Make sure that you passed in a navigation state with a " .. + "valid tab/screen index.", state.index) + + local childRouter = childRouters[routeName] + if childRouter then + return childRouter.getComponentForState(state.routes[state.index]) + else + return getScreenForRouteName(routeConfigs, routeName) + end + end + + function SwitchRouter.getComponentForRouteName(routeName) + return getScreenForRouteName(routeConfigs, routeName) + end + + -- TODO: Implement SwitchRouter.getPathAndParamsForState after we add path expression support + -- TODO: Implement SwitchRouter.getActionForPathAndParams after we add path expression support + + return SwitchRouter +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/TabRouter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/TabRouter.lua new file mode 100644 index 0000000..052d391 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/TabRouter.lua @@ -0,0 +1,13 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local SwitchRouter = require(script.Parent.SwitchRouter) +local BackBehavior = require(script.Parent.Parent.BackBehavior) + +return function(config) + -- Provide defaults suitable for tab routing. + local modifiedConfig = Cryo.Dictionary.join({ + resetOnBlur = false, + backBehavior = BackBehavior.InitialRoute, + }, config) + + return SwitchRouter(modifiedConfig) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/StackRouter.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/StackRouter.spec.lua new file mode 100644 index 0000000..e26a252 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/StackRouter.spec.lua @@ -0,0 +1,1435 @@ +return function() + local StackRouter = require(script.Parent.Parent.StackRouter) + local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) + local StackActions = require(script.Parent.Parent.Parent.StackActions) + + -- local TableUtilities = require(script.Parent.Parent.Parent.utils.TableUtilities) + + local function expectError(functor, msg) + local status, err = pcall(functor) + + if status ~= false then + error("expectError: Test function should have thrown error, but it passed", 2) + end + if string.find(err, msg) == nil then + error(string.format("expectError: Expected error message '%s' not found in actual message: '%s'", msg, err), 2) + end + end + + it("should be a function", function() + expect(type(StackRouter)).to.equal("function") + end) + + it("should throw when passed a non-table", function() + expectError(function() + StackRouter(5) + end, "config must be a table") + end) + + it("should throw for invalid routes config", function() + expect(function() + StackRouter({ routes = 5 }) + end).to.throw() + end) + + it("should throw if initialRouteName is not provided", function() + expectError(function() + StackRouter({ routes = { + Foo = function() end, + }}) + end, "initialRouteName must be provided") + end) + + it("should throw if initialRouteName is not found in routes table", function() + expectError(function() + StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "MyRoute", + }) + end, "Invalid initialRouteName 'MyRoute'. Must be one of %[Bar,Foo,%]") + end) + + it("should expose childRouters as a member", function() + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + router = "A", + }, + }, + Bar = { + screen = { + render = function() end, + router = "B", + }, + }, + }, + initialRouteName = "Foo", + }) + + expect(router.childRouters.Foo).to.equal("A") + expect(router.childRouters.Bar).to.equal("B") + end) + + it("should not expose childRouters list members if they are CHILD_IS_SCREEN", function() + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + router = "A", + }, + }, + Bar = { + screen = { + render = function() end, + }, + }, + }, + initialRouteName = "Foo", + }) + + expect(router.childRouters.Foo).to.equal("A") + + expect(router._CHILD_IS_SCREEN).to.never.equal(nil) + for _, childRouter in pairs(router.childRouters) do + expect(childRouter).to.never.equal(router._CHILD_IS_SCREEN) + end + end) + + describe("getScreenOptions tests", function() + it("should correctly configure default screen options", function() + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + } + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + } + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("FooTitle") + end) + + it("should correctly configure route-specified screen options", function() + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + }, + navigationOptions = { title = "RouteFooTitle" }, + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("RouteFooTitle") + end) + + it("should correctly configure component-specified screen options", function() + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentFooTitle" }, + }, + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("ComponentFooTitle") + end) + end) + + describe("getActionCreators tests", function() + it("should return basic action creators table if none are provided", function() + local router = StackRouter({ + routes = { + Foo = { render = function() end }, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + + local fieldCount = 0 + for _ in pairs(actionCreators) do + fieldCount = fieldCount + 1 + end + + expect(fieldCount).to.equal(6) + expect(type(actionCreators.pop)).to.equal("function") + expect(type(actionCreators.popToTop)).to.equal("function") + expect(type(actionCreators.push)).to.equal("function") + expect(type(actionCreators.replace)).to.equal("function") + expect(type(actionCreators.reset)).to.equal("function") + expect(type(actionCreators.dismiss)).to.equal("function") + end) + + it("should call custom action creators function if provided", function() + local router = StackRouter({ + routes = { + Foo = { render = function() end }, + }, + initialRouteName = "Foo", + getCustomActionCreators = function() + return { a = 1, popToTop = 2 } + end, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.a).to.equal(1) + + -- make sure that we merged the default ones on top! + expect(type(actionCreators.pop)).to.equal("function") + expect(type(actionCreators.popToTop)).to.equal("function") + end) + + it("should build a pop action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.pop(1).type).to.equal(StackActions.Pop) + end) + + it("should build a pop to top action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.popToTop().type).to.equal(StackActions.PopToTop) + end) + + it("should build a push action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.push("Foo").type).to.equal(StackActions.Push) + end) + + it("should build a replace action with a string replaceWith arg", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo", key = "Foo" }, "key") + expect(actionCreators.replace("Foo").type).to.equal(StackActions.Replace) + end) + + it("should build a replace action with a table replaceWith arg", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.replace({ routeName = "Foo" }).type).to.equal(StackActions.Replace) + end) + + it("should build a reset action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.reset({ + actions = { NavigationActions.navigate({ routeName = "Foo" }) }, + }).type).to.equal(StackActions.Reset) + end) + + it("should build a dismiss action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.dismiss().type).to.equal(NavigationActions.Back) + end) + end) + + describe("getComponentForState tests", function() + it("should return component matching requested state", function() + local testComponent = function() end + local router = StackRouter({ + routes = { + Foo = { screen = testComponent }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForState({ + routes = { + { routeName = "Foo" }, + }, + index = 1, + }) + expect(component).to.equal(testComponent) + end) + + it("should throw if there is no route matching active index", function() + local router = StackRouter({ + routes = { + Foo = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + expectError(function() + router.getComponentForState({ + routes = { + Foo = { screen = function() end }, + }, + index = 2, + }) + end, "There is no route defined for index '2'. " .. + "Make sure that you passed in a navigation state with a " .. + "valid stack index.") + + end) + + it("should descend child router for requested route", function() + local testComponent = function() end + local childRouter = StackRouter({ + routes = { + Bar = { screen = testComponent } + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + router = childRouter, + } + }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForState({ + routes = { + { + routeName = "Foo", + routes = { -- Child router's routes + { routeName = "Bar" }, + }, + index = 1 + }, + }, + index = 1, + }) + expect(component).to.equal(testComponent) + end) + end) + + describe("getComponentForRouteName tests", function() + it("should return a component that matches the given route name", function() + local testComponent = function() end + local router = StackRouter({ + routes = { + Foo = testComponent, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForRouteName("Foo") + expect(component).to.equal(testComponent) + end) + + it("should return a component that matches the given route name from accessed childRouter", function() + local testComponent = function() end + local childRouter = StackRouter({ + routes = { + Bar = testComponent, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + }, + initialRouteName = "Foo", + }) + + local component = router.childRouters.Foo.getComponentForRouteName("Bar") + expect(component).to.equal(testComponent) + end) + end) + + describe("getStateForAction tests", function() + it("should return initial state for init action", function() + local router = StackRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(#state.routes).to.equal(1) + expect(state.routes[state.index].routeName).to.equal("Foo") + expect(state.isTransitioning).to.equal(false) + end) + + it("should adjust initial state index to match initialRouteName's index", function() + local router = StackRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(state.routes[state.index].routeName).to.equal("Foo") + + local router2 = StackRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local state2 = router2.getStateForAction(NavigationActions.init(), nil) + expect(state2.routes[state2.index].routeName).to.equal("Bar") + end) + + it("should incorporate child router state", function() + local childRouter = StackRouter({ + routes = { + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + City = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + local activeState = state.routes[state.index] + expect(activeState.routeName).to.equal("Foo") -- parent's tracking uses parent's route name + expect(activeState.routes[activeState.index].routeName).to.equal("Bar") + end) + + it("should let active child handle non-init action first", function() + local childRouter = StackRouter({ + routes = { + Bar = { screen = function() end }, + City = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "City" })) + + expect(state.routes[1].routes[2].routeName).to.equal("City") + expect(state.index).to.equal(1) + expect(state.routes[1].index).to.equal(2) + end) + + it("should make historical inactive child router active if it handles action", function() + local childRouter = StackRouter({ + routes = { + City = function() end, + State = function() end, + }, + initialRouteName = "City", + }) + + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = { + render = function() end, + router = childRouter, + } + }, + initialRouteName = "Foo", + }) + + local initialState = { + routes = { + [1] = { routeName = "Foo", key = "Foo1" }, + [2] = { + routeName = "Bar", + key = "Bar", + routes = { + [1] = { routeName = "City", key = "City", }, + }, + index = 1 + }, + [3] = { routeName = "Foo", key = "Foo2" }, + }, + index = 3, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "State" }), initialState) + expect(resultState.routes[2].index).to.equal(2) + expect(resultState.routes[2].routes[2].routeName).to.equal("State") + expect(#resultState.routes[2].routes).to.equal(2) + expect(resultState.index).to.equal(2) + end) + + it("should go back to previous stack entry on back action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar", + } + }, + index = 2, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Foo") + expect(#resultState.routes).to.equal(1) -- it should delete top entry! + end) + + it("should not go back if at root of stack", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(resultState).to.equal(initialState) + end) + + it("should go back out of child stack if on root of child", function() + local childRouter = StackRouter({ + routes = { + Bar = { screen = function() end }, + City = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + Cat = function() end, + }, + initialRouteName = "Cat", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Cat", + key = "Cat", + }, + [2] = { + routeName = "Foo", + key = "Foo", + routes = { + [1] = { + routeName = "Bar", + key = "Bar", + } + }, + index = 1, + } + }, + index = 2, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(resultState.index).to.equal(1) + expect(#resultState.routes).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Cat") + end) + + it("should go back within active child if not on root of child", function() + local childRouter = StackRouter({ + routes = { + Bar = { screen = function() end }, + City = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + Cat = function() end, + }, + initialRouteName = "Cat", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Cat", + key = "Cat", + }, + [2] = { + routeName = "Foo", + key = "Foo", + routes = { + [1] = { + routeName = "Bar", + key = "Bar", + }, + [2] = { + routeName = "City", + key = "City", + }, + }, + index = 2, + } + }, + index = 2, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[1].routeName).to.equal("Cat") + expect(resultState.routes[2].routeName).to.equal("Foo") + + expect(#resultState.routes[2].routes).to.equal(1) + expect(resultState.routes[2].index).to.equal(1) + expect(resultState.routes[2].routes[1].routeName).to.equal("Bar") + end) + + it("should pop to top", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar1", + }, + [3] = { + routeName = "Bar", + key = "Bar2", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(StackActions.popToTop(), initialState) + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Foo") + end) + + it("should pop to top through child router", function() + local childRouter = StackRouter({ + routes = { + Bar = function() end, + City = function() end, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + screen = function() end, + router = childRouter, + }, + Crazy = function() end, + }, + initialRouteName = "Crazy", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Crazy", + key = "Crazy", + }, + [2] = { + routeName = "Foo", + key = "Foo", + routes = { + [1] = { + routeName = "Bar", + key = "Bar", + }, + [2] = { + routeName = "City", + key = "City", + }, + }, + index = 2, + } + }, + index = 2, + } + + local resultState = router.getStateForAction(StackActions.popToTop(), initialState) + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Crazy") + end) + + it("should push a new entry on navigate without instance of that screen", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + } + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + end) + + it("should jump to existing entry in stack if one exists already, on navigate", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + City = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar", + }, + [3] = { + routeName = "City", + key = "City", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + params = { a = 1 }, + }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + expect(resultState.routes[2].params.a).to.equal(1) + end) + + it("should jump to existing entry in stack if one exists already, on navigate, with empty params", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + City = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar", + }, + [3] = { + routeName = "City", + key = "City", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + expect(resultState.routes[2].params).to.equal(nil) + end) + + it("should jump to existing entry in stack with existing params if params is not provided, on navigate", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + City = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar", + params = { a = 1 }, + }, + [3] = { + routeName = "City", + key = "City", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + expect(resultState.routes[2].params.a).to.equal(1) + end) + + it("should jump to existing entry in stack with updated params if params is provided, on navigate", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + City = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar", + params = { a = 1 }, + }, + [3] = { + routeName = "City", + key = "City", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + params = { a = 2 }, + }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + expect(resultState.routes[2].params.a).to.equal(2) + end) + + it("should stay at current route in stack if navigate with different params", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Foo", + params = { a = 1 }, + }), initialState) + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Foo") + expect(resultState.routes[1].params.a).to.equal(1) + end) + + it("should stay at current route with existing params if navigate with empty params", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + params = { a = 1 }, + }, + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Foo", + params = {}, + }), initialState) + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Foo") + expect(resultState.routes[1].params.a).to.equal(1) + end) + + it("should always push new entry on push action even with pre-existing instance of that screen", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + City = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar", + }, + [3] = { + routeName = "City", + key = "City", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(StackActions.push({ routeName = "Foo" }), initialState) + expect(#resultState.routes).to.equal(4) + expect(resultState.index).to.equal(4) + expect(resultState.routes[4].routeName).to.equal("Foo") + end) + + it("should navigate to inactive child if route not present elsewhere", function() + local childRouter = StackRouter({ + routes = { + Bar = { screen = function() end }, + City = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + Cat = function() end, + }, + initialRouteName = "Cat", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Cat", + key = "Cat", + }, + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "City" }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Foo") + + expect(#resultState.routes[2].routes).to.equal(2) + expect(resultState.routes[2].index).to.equal(2) + expect(resultState.routes[2].routes[2].routeName).to.equal("City") + end) + + it("should set params on route for setParams action", function() + local router = StackRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + initialRouteName = "Foo", + initialRouteKey = "FooKey", + }) + + local newState = router.getStateForAction(NavigationActions.setParams({ + key = "FooKey", + params = { a = 1 }, + })) + + expect(newState.routes[newState.index].params.a).to.equal(1) + end) + + it("should set params on route for setParams action with empty params", function() + local router = StackRouter({ + routes = { + Foo = { + screen = function() end, + params = { a = 1 }, + }, + Bar = { render = function() end }, + }, + initialRouteName = "Foo", + initialRouteKey = "FooKey", + }) + + local newState = router.getStateForAction(NavigationActions.setParams({ + key = "FooKey", + params = {}, + })) + + expect(newState.routes[newState.index].params.a).to.equal(nil) + end) + + it("should combine params from action and route config", function() + local router = StackRouter({ + routes = { + Foo = { render = function() end }, + Bar = { + screen = function() end, + params = { a = 1 }, + }, + }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + params = { b = 2 }, + })) + + expect(newState.routes[2].params.a).to.equal(1) + expect(newState.routes[2].params.b).to.equal(2) + end) + + it("should init and then replace initial route if prior state is not provided", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(StackActions.replace({ + routeName = "Bar", + })) + + expect(#newState.routes).to.equal(1) + expect(newState.index).to.equal(1) + expect(newState.routes[1].routeName).to.equal("Bar") + end) + + it("should replace top route if no key is provided", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + } + }, + index = 1, + } + + local newState = router.getStateForAction(StackActions.replace({ + routeName = "Bar", + }), initialState) + + expect(#newState.routes).to.equal(1) + expect(newState.index).to.equal(1) + expect(newState.routes[1].routeName).to.equal("Bar") + end) + + it("should replace keyed route if provided", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar", + } + }, + index = 2, + } + + local newState = router.getStateForAction(StackActions.replace({ + routeName = "Foo", + key = "Bar", + newKey = "NewFoo", + }), initialState) + + expect(#newState.routes).to.equal(2) + expect(newState.index).to.equal(2) + expect(newState.routes[2].routeName).to.equal("Foo") + expect(newState.routes[2].key).to.equal("NewFoo") + end) + + it("should reset top-level routes if not given a key", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo1", + }, + [2] = { + routeName = "Foo", + key = "Foo2", + }, + }, + index = 2, + } + + local resultState = router.getStateForAction(StackActions.reset({ + actions = { + NavigationActions.navigate({ routeName = "Bar" }) + } + }), initialState) + + -- "actions" array replaces entire state, bypassing initial route config! + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Bar") + end) + + it("should reset keyed route if provided", function() + local childRouter = StackRouter({ + routes = { + City = function() end, + State = function() end, + }, + initialRouteName = "City", + }) + + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = { + screen = function() end, + router = childRouter, + }, + }, + initialRouteName = "Bar", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo1", + }, + [2] = { + routeName = "Bar", + key = "Bar", + routes = { + [1] = { + routeName = "City", + key = "City", + } + }, + index = 1, + }, + }, + index = 2, + } + + local resultState = router.getStateForAction(StackActions.reset({ + actions = { + NavigationActions.navigate({ routeName = "State" }) + }, + key = "Bar", + }), initialState) + + -- "actions" array replaces entire state, bypassing initial route config! + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + expect(resultState.routes[2].routes[1].routeName).to.equal("City") + end) + + it("should mark state as transitioning, then clear it on CompleteTransition action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + } + }, + index = 1, + } + + local transitioningState = router.getStateForAction(StackActions.push({ routeName = "Bar" }), initialState) + expect(transitioningState.isTransitioning).to.equal(true) + + local completedState = router.getStateForAction(NavigationActions.completeTransition({ + toChildKey = transitioningState.routes[2].key, -- Need actual key to identify target + }), transitioningState) + + expect(completedState.isTransitioning).to.equal(false) + end) + + it("should mark root and child states as transitioning, then separately clear them on CompleteTransition", function() + local childRouter = StackRouter({ + routes = { + BarA = function() end, + BarB = function() end, + }, + initialRouteName = "BarA", + }) + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = { + screen = { + render = function() end, + router = childRouter, + }, + }, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + }, + index = 1, + } + + local transitioningState = router.getStateForAction(NavigationActions.navigate({ routeName = "BarB" }), initialState) + expect(transitioningState).to.be.ok() + expect(transitioningState.isTransitioning).to.equal(true) + expect(transitioningState.routes[2].isTransitioning).to.equal(true) + expect(transitioningState.routes[2].routes[2].routeName).to.equal("BarB") + + local childOnlyCompletedState = router.getStateForAction(NavigationActions.completeTransition({ + toChildKey = transitioningState.routes[2].routes[2].key, + }), transitioningState) + expect(childOnlyCompletedState.isTransitioning).to.equal(true) -- *** parent needs its own completeTransition call *** + expect(childOnlyCompletedState.routes[2].isTransitioning).to.equal(false) + + local completedState = router.getStateForAction(NavigationActions.completeTransition({ + toChildKey = transitioningState.routes[2].key, + }), childOnlyCompletedState) + expect(completedState.isTransitioning).to.equal(false) + expect(completedState.routes[2].isTransitioning).to.equal(false) + end) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/SwitchRouter.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/SwitchRouter.spec.lua new file mode 100644 index 0000000..2548212 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/SwitchRouter.spec.lua @@ -0,0 +1,691 @@ +return function() + local SwitchRouter = require(script.Parent.Parent.SwitchRouter) + local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) + local BackBehavior = require(script.Parent.Parent.Parent.BackBehavior) + + local function expectError(functor, msg) + local status, err = pcall(functor) + + if status ~= false then + error("expectError: Test function should have thrown error, but it passed", 2) + end + if string.find(err, msg) == nil then + error(string.format("expectError: Expected error message '%s' not found in actual message: '%s'", msg, err), 2) + end + end + + it("should be a function", function() + expect(type(SwitchRouter)).to.equal("function") + end) + + it("should throw when passed a non-table", function() + expectError(function() + SwitchRouter(5) + end, "config must be a table") + end) + + it("should throw for invalid routes config", function() + expect(function() + SwitchRouter({ routes = 5 }) + end).to.throw() -- throw is from validateRouteConfigs, so do not depend on message + end) + + it("should throw if initialRouteName is not provided", function() + expectError(function() + SwitchRouter({ routes = { + Foo = function() end, + } }) + end, "initialRouteName must be provided") + end) + + it("should throw if initialRouteName is not found in routes table", function() + expectError(function() + SwitchRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "MyRoute", + }) + end, "Invalid initialRouteName 'MyRoute'. Must be one of %[Bar,Foo,%]") + end) + + it("should expose childRouters as a member", function() + local router = SwitchRouter({ + routes = { + Foo = { + screen = { + render = function() end, + router = "A", + }, + }, + Bar = { + screen = { + render = function() end, + router = "B", + }, + }, + }, + initialRouteName = "Foo", + }) + + expect(router.childRouters.Foo).to.equal("A") + expect(router.childRouters.Bar).to.equal("B") + end) + + describe("getScreenOptions tests", function() + it("should correctly configure default screen options", function() + local router = SwitchRouter({ + routes = { + Foo = { + screen = { + render = function() end, + } + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("FooTitle") + end) + + it("should correctly configure route-specified screen options", function() + local router = SwitchRouter({ + routes = { + Foo = { + screen = { + render = function() end, + }, + navigationOptions = { title = "RouteFooTitle" }, + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("RouteFooTitle") + end) + + it("should correctly configure component-specified screen options", function() + local router = SwitchRouter({ + routes = { + Foo = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentFooTitle" }, + }, + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("ComponentFooTitle") + end) + end) + + describe("getActionCreators tests", function() + it("should return empty action creators table if none are provided", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + + local fieldCount = 0 + for _ in pairs(actionCreators) do + fieldCount = fieldCount + 1 + end + + expect(fieldCount).to.equal(0) + end) + + it("should call custom action creators function if provided", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + }, + initialRouteName = "Foo", + getCustomActionCreators = function() + return { a = 1 } + end, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.a).to.equal(1) + end) + end) + + describe("getComponentForState tests", function() + it("should return component matching requested state", function() + local testComponent = function() end + local router = SwitchRouter({ + routes = { + Foo = { screen = testComponent }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForState({ + routes = { + { routeName = "Foo" }, + }, + index = 1, + }) + expect(component).to.equal(testComponent) + end) + + it("should throw if there is no route matching active index", function() + local router = SwitchRouter({ + routes = { + Foo = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + expectError(function() + router.getComponentForState({ + routes = { + Foo = { screen = function() end }, + }, + index = 2, + }) + end, "There is no route defined for index '2'. " .. + "Make sure that you passed in a navigation state with a " .. + "valid tab/screen index.") + + end) + + it("should descend child router for requested route", function() + local testComponent = function() end + local childRouter = SwitchRouter({ + routes = { + Bar = { screen = testComponent } + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + screen = { + render = function() end, + router = childRouter, + } + }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForState({ + routes = { + { + routeName = "Foo", + routes = { -- Child router's routes + { routeName = "Bar" }, + }, + index = 1 + }, + }, + index = 1, + }) + expect(component).to.equal(testComponent) + end) + end) + + describe("getComponentForRouteName tests", function() + it("should return a component that matches the given route name", function() + local testComponent = function() end + local router = SwitchRouter({ + routes = { + Foo = { screen = testComponent }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForRouteName("Foo") + expect(component).to.equal(testComponent) + end) + end) + + describe("getStateForAction tests", function() + it("should return initial state for init action", function() + local router = SwitchRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(#state.routes).to.equal(2) + expect(state.routes[state.index].routeName).to.equal("Foo") + expect(state.isTransitioning).to.equal(false) + end) + + it("should adjust initial state index to match initialRouteName's index", function() + local router = SwitchRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(state.routes[state.index].routeName).to.equal("Foo") + + local router2 = SwitchRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local state2 = router2.getStateForAction(NavigationActions.init(), nil) + expect(state2.routes[state2.index].routeName).to.equal("Bar") + end) + + it("should respect optional order property", function() + local router = SwitchRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(state.routes[1].routeName).to.equal("Foo") + expect(state.routes[2].routeName).to.equal("Bar") + end) + + it("should incorporate child router state", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + City = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + local activeState = state.routes[state.index] + expect(activeState.routeName).to.equal("Foo") -- parent's tracking uses parent's route name + expect(activeState.routes[activeState.index].routeName).to.equal("Bar") + end) + + it("should let active child handle non-init action first", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { screen = function() end }, + City = { screen = function() end }, + }, + order = { "Bar", "City" }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + State = { render = function() end }, + }, + order = { "Foo", "State" }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "City" })) + expect(state.routes[1].index).to.equal(2) + expect(state.index).to.equal(1) + end) + + it("should go back to initial route index if BackBehavior.InitialRoute", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + backBehavior = BackBehavior.InitialRoute, + initialRouteName = "Foo", + }) + + local prevState = { + routes = { + { routeName = "Foo" }, + { routeName = "Bar" }, + }, + index = 2, + } + + local newState = router.getStateForAction(NavigationActions.back(), prevState) + expect(newState.index).to.equal(1) + end) + + it("should not change state on back action if BackBehavior.None", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local prevState = { + routes = { + { routeName = "Foo" }, + { routeName = "Bar" }, + }, + index = 2, + } + + local newState = router.getStateForAction(NavigationActions.back(), prevState) + expect(newState).to.equal(prevState) + end) + + it("should change active route on navigate", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" })) + expect(newState.index).to.equal(2) + expect(newState.routes[newState.index].routeName).to.equal("Bar") + end) + + it("should pass sub-action to child router on navigate", function() + local childRouter = SwitchRouter({ + routes = { + City = { screen = function() end }, + State = { screen = function() end }, + }, + initialRouteName = "City", + }) + + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { + render = function() end, + router = childRouter, + }, + }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + action = NavigationActions.navigate({ routeName = "State" }), + })) + + local activeRoute = newState.routes[newState.index] + expect(activeRoute.routeName).to.equal("Bar") + expect(activeRoute.routes[activeRoute.index].routeName).to.equal("State") + end) + + it("should return initial state if navigating to active child without previous state", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + City = { render = function() end }, + }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Foo", + })) + + expect(newState.routes[newState.index].routeName).to.equal("Foo") + expect(newState.isTransitioning).to.equal(false) + end) + + it("should reset state for deactivated route by default", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local initialState = { + routes = { + { routeName = "Foo", params = { a = 1 } }, + { routeName = "Bar" }, + }, + index = 1, + } + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(state.routes[1].params.a).to.equal(nil) -- should be empty + end) + + it("should not reset state for deactivated route if resetOnBlur is false", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + resetOnBlur = false, + }) + + local testParams = { a = 1 } + + local initialState = { + routes = { + { routeName = "Foo", params = testParams }, + { routeName = "Bar" }, + }, + index = 1, + } + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(state.routes[1].params).to.equal(testParams) + end) + + it("should set params on route for setParams action", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(NavigationActions.setParams({ + key = "Foo", -- By default, key == routeName + params = { a = 1 }, + })) + + expect(newState.routes[newState.index].params.a).to.equal(1) + end) + + it("should preserve route configured params for child router", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { + screen = function() end, + params = { a = 2 }, + }, + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + params = { a = 1 }, + router = childRouter, + }, + City = { render = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init()) + expect(state.routes[state.index].params.a).to.equal(1) + end) + + it("should merge initialRouteParams with initial route's own params", function() + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + params = { a = 1 }, + }, + Bar = { + render = function() end, + params = { a = 1 }, + }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + initialRouteParams = { a = 2, b = 3 }, + }) + + local state = router.getStateForAction(NavigationActions.init()) + expect(state.routes[1].params.a).to.equal(2) + expect(state.routes[1].params.b).to.equal(3) + expect(state.routes[2].params.a).to.equal(1) + expect(state.routes[2].params.b).to.equal(nil) + end) + + it("should merge init action params with initial route's own params and initialRouteParams", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end, params = { a = 1 } } + }, + initialRouteName = "Foo", + initialRouteParams = { c = 3 }, + }) + + local state = router.getStateForAction(NavigationActions.init({ params = { b = 2 } })) + expect(state.routes[1].params.a).to.equal(1) + expect(state.routes[1].params.b).to.equal(2) + expect(state.routes[1].params.c).to.equal(3) + end) + + it("should merge navigate action params for child router", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { + screen = function() end, + params = { a = 2 }, + }, + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + params = { b = 3 }, + })) + + expect(state.routes[1].routes[1].params.a).to.equal(2) + expect(state.routes[1].routes[1].params.b).to.equal(3) + end) + + it("should propagate a child router getStateForAction failure to caller", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + }, + initialRouteName = "Foo", + }) + + -- need to properly initialize state because we're being abusive of getStateForAction + local initialState = router.getStateForAction(NavigationActions.init()) + + childRouter.getStateForAction = function() return nil end + + local state = router.getStateForAction(NavigationActions.navigate("Bar"), initialState) + expect(state).to.equal(nil) + end) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/TabRouter.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/TabRouter.spec.lua new file mode 100644 index 0000000..f18a109 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/TabRouter.spec.lua @@ -0,0 +1,67 @@ +return function() + local TabRouter = require(script.Parent.Parent.TabRouter) + local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) + + -- NOTE: Most functional tests are covered by SwitchRouter.spec.lua + -- We just check that we can mount a basic case, and check that our custom + -- defaults for resetOnBlur and backBehavior work as expected. + + it("should return a component that matches the given route name", function() + local testComponent = function() end + local router = TabRouter({ + routes = { + Foo = { screen = testComponent }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForRouteName("Foo") + expect(component).to.equal(testComponent) + end) + + it("should not reset state for deactivated route", function() + local router = TabRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local testParams = { a = 1 } + + local initialState = { + routes = { + { routeName = "Foo", params = testParams }, + { routeName = "Bar" }, + }, + index = 1, + } + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(state.routes[1].params).to.equal(testParams) + end) + + it("should go back to initial route index", function() + local router = TabRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local prevState = { + routes = { + { routeName = "Foo" }, + { routeName = "Bar" }, + }, + index = 2, + } + + local newState = router.getStateForAction(NavigationActions.back(), prevState) + expect(newState.index).to.equal(1) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/createConfigGetter.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/createConfigGetter.spec.lua new file mode 100644 index 0000000..6cb2e53 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/createConfigGetter.spec.lua @@ -0,0 +1,115 @@ +return function() + local createConfigGetter = require(script.Parent.Parent.createConfigGetter) + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + + it("should return a function", function() + local result = createConfigGetter({}, {}) + expect(type(result)).to.equal("function") + end) + + it("should return a screen config when called", function() + local HomeScreen = Roact.Component:extend("HomeScreen") + HomeScreen.navigationOptions = function(props) + local username = props.navigation.state.params and + props.navigation.state.params.user or "anonymous" + + return { + title = string.format("Welcome %s", username), + gesturesEnabled = true, + } + end + function HomeScreen:render() return nil end + + local SettingsScreen = Roact.Component:extend("SettingsScreen") + SettingsScreen.navigationOptions = { + title = "Settings!!!", + gesturesEnabled = false, + } + function SettingsScreen:render() return nil end + + local NotificationScreen = Roact.Component:extend("NotificationScreen") + NotificationScreen.navigationOptions = function(props) + local gesturesEnabled = true + if props.navigation.state.params then + gesturesEnabled = not props.navigation.state.params.fullscreen + end + + return { + title = "42", + gesturesEnabled = gesturesEnabled + } + end + + local getScreenOptions = createConfigGetter({ + Home = { screen = HomeScreen }, + Settings = { screen = SettingsScreen }, + Notifications = { + screen = NotificationScreen, + navigationOptions = { + title = "10 new notifications", + } + } + }) + + local routes = { + { key = "A", routeName = "Home", }, + { key = "B", routeName = "Home", params = { user = "jane"} }, + { key = "C", routeName = "Settings", }, + { key = "D", routeName = "Notifications", }, + { key = "E", routeName = "Notifications", params = { fullscreen = true } }, + } + + expect(getScreenOptions({ state = routes[1] }, {}).title + ).to.equal("Welcome anonymous") + + expect(getScreenOptions({ state = routes[2] }, {}).title + ).to.equal("Welcome jane") + + expect(getScreenOptions({ state = routes[1] }, {}).gesturesEnabled + ).to.equal(true) + + expect(getScreenOptions({ state = routes[3] }, {}).title + ).to.equal("Settings!!!") + + expect(getScreenOptions({ state = routes[3] }, {}).gesturesEnabled + ).to.equal(false) + + expect(getScreenOptions({ state = routes[4] }, {}).title + ).to.equal("10 new notifications") + + expect(getScreenOptions({ state = routes[4] }, {}).gesturesEnabled + ).to.equal(true) + + expect(getScreenOptions({ state = routes[5] }, {}).gesturesEnabled + ).to.equal(false) + end) + + it("should override default config with component-specific config", function() + local getScreenOptions = createConfigGetter({ + Home = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentHome" }, + }, + }, + defaultNavigationOptions = { title = "DefaultTitle" }, + }) + + expect(getScreenOptions({ state = { routeName = "Home" } }).title).to.equal("ComponentHome") + end) + + it("should override component-specific config with route-specific config", function() + local getScreenOptions = createConfigGetter({ + Home = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentHome" }, + }, + navigationOptions = { title = "RouteHome" }, + }, + defaultNavigationOptions = { title = "DefaultTitle" }, + }) + + expect(getScreenOptions({ state = { routeName = "Home" } }).title).to.equal("RouteHome") + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/getChildRouter.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/getChildRouter.spec.lua new file mode 100644 index 0000000..dec6a68 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/getChildRouter.spec.lua @@ -0,0 +1,60 @@ +return function() + local getChildRouter = require(script.Parent.Parent.getChildRouter) + + it("should throw if router is not a table", function() + local status, err = pcall(function() + getChildRouter(5, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "router must be a table")).to.never.equal(nil) + end) + + it("should throw if routeName is not a string", function() + local status, err = pcall(function() + getChildRouter({}, 5) + end) + + expect(status).to.equal(false) + expect(string.find(err, "routeName must be a string")).to.never.equal(nil) + end) + + it("should return child router if found", function() + local childRouter = {} + local result = getChildRouter({ + childRouters = { + myRoute = childRouter, + } + }, "myRoute") + + expect(result).to.equal(childRouter) + end) + + it("should look up component router if no child router is found", function() + local component = { router = {} } + + local result = getChildRouter({ + getComponentForRouteName = function(routeName) + if routeName == "myRoute" then + return component + else + return nil + end + end + }, "myRoute") + + expect(result).to.equal(component.router) + end) + + it("should throw if no child routers are specified and getComponentForRouteName is not a function", function() + local status, err = pcall(function() + getChildRouter({ + getComponentForRouteName = 5 + }, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "router.getComponentForRouteName must be a function if no child routers are specified") + ).to.never.equal(nil) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/getNavigationActionCreators.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/getNavigationActionCreators.spec.lua new file mode 100644 index 0000000..bafe6b7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/getNavigationActionCreators.spec.lua @@ -0,0 +1,101 @@ +return function() + local getNavigationActionCreators = require(script.Parent.Parent.getNavigationActionCreators) + local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) + + local function expectError(functor, msg) + local status, err = pcall(functor) + expect(status).to.equal(false) + expect(string.find(err, msg)).to.never.equal(nil) + end + + it("should return a table with correct functions when called", function() + local result = getNavigationActionCreators() + expect(type(result.goBack)).to.equal("function") + expect(type(result.navigate)).to.equal("function") + expect(type(result.setParams)).to.equal("function") + end) + + describe("goBack tests", function() + it("should return a Back action when called", function() + local result = getNavigationActionCreators().goBack("theKey") + expect(result.type).to.equal(NavigationActions.Back) + expect(result.key).to.equal("theKey") + end) + + it("should throw when route.key is not a string", function() + expectError(function() + getNavigationActionCreators({ key = 5 }).goBack() + end, "%.goBack%(%): key should be a string") + end) + + it("should fall back to route.key if key is not provided", function() + local result = getNavigationActionCreators({ key = "routeKey" }).goBack() + expect(result.key).to.equal("routeKey") + end) + + it("should override route.key if key is provided", function() + local result = getNavigationActionCreators({ key = "routeKey" }).goBack("theKey") + expect(result.key).to.equal("theKey") + end) + end) + + describe("navigate tests", function() + it("should return a Navigate action when called", function() + local theParams = {} + local childAction = {} + local result = getNavigationActionCreators().navigate("theRoute", theParams, childAction) + expect(result.type).to.equal(NavigationActions.Navigate) + expect(result.routeName).to.equal("theRoute") + expect(result.params).to.equal(theParams) + expect(result.action).to.equal(childAction) + end) + + it("should return a navigate action with matching properties when called with a table", function() + local testNavigateTo = { + routeName = "theRoute", + params = {}, + action = {}, + } + + local result = getNavigationActionCreators().navigate(testNavigateTo) + expect(result.type).to.equal(NavigationActions.Navigate) + expect(result.routeName).to.equal("theRoute") + expect(result.params).to.equal(testNavigateTo.params) + expect(result.action).to.equal(testNavigateTo.action) + end) + + it("should throw when navigateTo is not a valid type", function() + expectError(function() + getNavigationActionCreators().navigate(5) + end, "%.navigate%(%): navigateTo must be a string or table") + end) + + it("should throw when params is provided with a table navigateTo", function() + expectError(function() + getNavigationActionCreators().navigate({}, {}) + end, "%.navigate%(%): params can only be provided with a string navigateTo value") + end) + + it("should throw when action is provided with a table navigateTo", function() + expectError(function() + getNavigationActionCreators().navigate({}, nil, {}) + end, "%.navigate%(%): child action can only be provided with a string navigateTo value") + end) + end) + + describe("setParams tests", function() + it("should return a SetParams action when called", function() + local theParams = {} + local result = getNavigationActionCreators({ key = "theKey" }).setParams(theParams) + expect(result.type).to.equal(NavigationActions.SetParams) + expect(result.key).to.equal("theKey") + expect(result.params).to.equal(theParams) + end) + + it("should throw when called by a root navigator", function() + expectError(function() + getNavigationActionCreators({}).setParams({}) + end, "%.setParams%(%): cannot be called by the root navigator") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/getScreenForRouteName.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/getScreenForRouteName.spec.lua new file mode 100644 index 0000000..3ad8af1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/getScreenForRouteName.spec.lua @@ -0,0 +1,89 @@ +return function() + local getScreenForRouteName = require(script.Parent.Parent.getScreenForRouteName) + + it("should throw for invalid arg types", function() + local status, err = pcall(function() + getScreenForRouteName("", "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "routeConfigs must be a table")).to.never.equal(nil) + + status, err = pcall(function() + getScreenForRouteName({}, 5) + end) + + expect(status).to.equal(false) + expect(string.find(err, "routeName must be a string")).to.never.equal(nil) + end) + + it("should throw if requested route is not present within table", function() + local status, err = pcall(function() + getScreenForRouteName({ + notMyRoute = function() return "foo" end + }, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "There is no route defined for key 'myRoute'.")).to.never.equal(nil) + end) + + it("should return raw table if screen and getScreen are not props", function() + local screenComponent = { render = function() return nil end } + local result = getScreenForRouteName({ + myRoute = screenComponent + }, "myRoute") + + expect(result).to.equal(screenComponent) + end) + + it("should return screen prop if it is set in route data table", function() + local screenComponent = { render = function() return nil end } + local result = getScreenForRouteName({ + myRoute = { + screen = screenComponent + } + }, "myRoute") + + expect(result).to.equal(screenComponent) + end) + + it("should return object returned by getScreen function if object is valid Roact element", function() + local screenComponent = { render = function() return nil end } + local result = getScreenForRouteName({ + myRoute = { + getScreen = function() return screenComponent end + } + }, "myRoute") + + expect(result).to.equal(screenComponent) + end) + + it("should throw if getScreen does not return a valid Roact element", function() + local status, err = pcall(function() + getScreenForRouteName({ + myRoute = { + getScreen = function() return nil end + } + }, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "The getScreen function defined for route 'myRoute'" .. + " did not return a valid screen or navigator")).to.never.equal(nil) + end) + + it("should throw if screen is not a valid Roact element", function() + local status, err = pcall(function() + getScreenForRouteName({ + myRoute = { + screen = 5, + } + }, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "screen param for key 'myRoute' must be a valid Roact component.")).to.never.equal(nil) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/validateRouteConfigMap.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/validateRouteConfigMap.spec.lua new file mode 100644 index 0000000..3003aa8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/validateRouteConfigMap.spec.lua @@ -0,0 +1,119 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local validateRouteConfigMap = require(script.Parent.Parent.validateRouteConfigMap) + + local function expectError(functor, msg) + local status, err = pcall(functor) + expect(status).to.equal(false) + expect(string.find(err, msg)).to.never.equal(nil) + end + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + return nil + end + + + it("should throw if routeConfigs is not a table", function() + expectError(function() + validateRouteConfigMap(5) + end, "routeConfigs must be a table") + end) + + it("should throw if routeConfigs is empty", function() + expectError(function() + validateRouteConfigMap({}) + end, "Please specify at least one route when configuring a navigator%.") + end) + + it("should throw if routeConfigs contains an invalid Roact element", function() + expectError(function() + validateRouteConfigMap({ + myRoute = 5, + }) + end, "The component for route 'myRoute' must be a Roact Function/Stateful component or table with 'getScreen'." .. + "getScreen function must return Roact Function/Stateful component.") + end) + + it("should throw if getScreen returns invalid Roact element", function() + expectError(function() + validateRouteConfigMap({ + myRoute = { + getScreen = function() end, + }, + }) + end, "The component for route 'myRoute' must be a Roact Function/Stateful component or table with 'getScreen'." .. + "getScreen function must return Roact Function/Stateful component.") + end) + + it("should throw when both screen and getScreen are provided for same component", function() + expectError(function() + validateRouteConfigMap({ + myRoute = { + screen = "TheScreen", + getScreen = function() return TestComponent end, + } + }) + end, "Route 'myRoute' should provide 'screen' or 'getScreen', but not both%.") + end) + + it("should throw for a simple table where screen is not a Roact Function/Stateful component", function() + expectError(function() + validateRouteConfigMap({ + myRoute = { + screen = {}, + } + }) + end, "The component for route 'myRoute' must be a Roact Function/Stateful component or table with 'getScreen'." .. + "getScreen function must return Roact Function/Stateful component.") + end) + + it("should throw for a non-function getScreen", function() + expectError(function() + validateRouteConfigMap({ + myRoute = { + getScreen = 5 + } + }) + end, "The component for route 'myRoute' must be a Roact Function/Stateful component or table with 'getScreen'." .. + "getScreen function must return Roact Function/Stateful component.") + end) + + it("should throw for a Host Component", function() + expectError(function() + validateRouteConfigMap({ + myRoute = { + aFrame = "Frame" + } + }) + end, "The component for route 'myRoute' must be a Roact Function/Stateful component or table with 'getScreen'." .. + "getScreen function must return Roact Function/Stateful component.") + end) + + + it("should pass for valid basic routeConfigs", function() + validateRouteConfigMap({ + basicComponentRoute = TestComponent, + functionalComponentRoute = function() end, + }) + end) + + it("should pass for valid screen prop type routeConfigs", function() + validateRouteConfigMap({ + basicComponentRoute = { + screen = TestComponent, + }, + functionalComponentRoute = { + screen = function() end, + }, + }) + end) + + it("should pass for valid getScreen route configs", function() + validateRouteConfigMap({ + getScreenRoute = { + getScreen = function() return TestComponent end, + } + }) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/validateScreenOptions.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/validateScreenOptions.spec.lua new file mode 100644 index 0000000..c1bb8ee --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/validateScreenOptions.spec.lua @@ -0,0 +1,25 @@ +return function() + local validateScreenOptions = require(script.Parent.Parent.validateScreenOptions) + + it("should not throw when there are no problems", function() + validateScreenOptions({ title = "foo" }, { routeName = "foo" }) + end) + + it("should throw error if no routeName is provided", function() + local status, err = pcall(function() + validateScreenOptions({ title = "bar" }, {}) + end) + + expect(status).to.equal(false) + expect(string.find(err, "route.routeName must be a string")).to.never.equal(nil) + end) + + it("should throw error for options with function for title", function() + expect(function() + validateScreenOptions({ + title = function() end, + }, { routeName = "foo" }) + end).to.throw() + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/createConfigGetter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/createConfigGetter.lua new file mode 100644 index 0000000..627f478 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/createConfigGetter.lua @@ -0,0 +1,53 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local getScreenForRouteName = require(script.Parent.getScreenForRouteName) +local validateScreenOptions = require(script.Parent.validateScreenOptions) +local validate = require(script.Parent.Parent.utils.validate) + +local function applyConfig(configurer, navigationOptions, configProps) + navigationOptions = navigationOptions or {} + + local configurerType = type(configurer) + if configurerType == "function" then + return Cryo.Dictionary.join(navigationOptions, + configurer(Cryo.Dictionary.join(configProps or {}, { + navigationOptions = navigationOptions + }))) + elseif configurerType == "table" then + return Cryo.Dictionary.join(navigationOptions, configurer) + else + return navigationOptions + end +end + +return function(routeConfigs, navigatorScreenConfig) + return function(navigation, screenProps) + screenProps = screenProps or {} + local route = navigation.state + + validate(type(route) == "table", "navigation.state must be a table") + validate(type(route.routeName == "string"), "routeName must be a string") + + local component = getScreenForRouteName(routeConfigs, route.routeName) + local routeConfig = routeConfigs[route.routeName] + + local routeScreenConfig = nil + if routeConfig ~= component then + routeScreenConfig = routeConfig.navigationOptions + end + + local componentScreenConfig = type(component) == "table" + and component.navigationOptions or {} + + local configOptions = { + navigation = navigation, + screenProps = screenProps, + } + + local outputConfig = applyConfig(navigatorScreenConfig, {}, configOptions) + outputConfig = applyConfig(componentScreenConfig, outputConfig, configOptions) + outputConfig = applyConfig(routeScreenConfig, outputConfig, configOptions) + + validateScreenOptions(outputConfig, route) + return outputConfig + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/getChildRouter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/getChildRouter.lua new file mode 100644 index 0000000..fc50799 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/getChildRouter.lua @@ -0,0 +1,20 @@ +local validate = require(script.Parent.Parent.utils.validate) + +return function(router, routeName) + validate(type(router) == "table", "router must be a table") + validate(type(routeName) == "string", "routeName must be a string") + + if router.childRouters and router.childRouters[routeName] then + return router.childRouters[routeName] + end + + validate(type(router.getComponentForRouteName) == "function", + "router.getComponentForRouteName must be a function if no child routers are specified") + local component = router.getComponentForRouteName(routeName) + if type(component) == "table" then + return component.router + else + return nil + end +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/getNavigationActionCreators.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/getNavigationActionCreators.lua new file mode 100644 index 0000000..106c266 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/getNavigationActionCreators.lua @@ -0,0 +1,42 @@ +local NavigationActions = require(script.Parent.Parent.NavigationActions) +local validate = require(script.Parent.Parent.utils.validate) + +return function(route) + local result = {} + + -- Go back FROM screen identified by 'key', or the default for current route. + function result.goBack(key) + if key == nil and route.key then + validate(type(route.key) == "string", ".goBack(): key should be a string") + key = route.key + end + + return NavigationActions.back({ key = key }) + end + + -- Navigate to a different screen, either by route name+params+action, or + -- by passing a raw navigation table. + function result.navigate(navigateTo, params, action) + if type(navigateTo) == "string" then + return NavigationActions.navigate({ + routeName = navigateTo, + params = params, + action = action, + }) + else + validate(type(navigateTo) == "table", ".navigate(): navigateTo must be a string or table") + validate(params == nil, ".navigate(): params can only be provided with a string navigateTo value") + validate(action == nil, ".navigate(): child action can only be provided with a string navigateTo value") + + return NavigationActions.navigate(navigateTo) + end + end + + -- Change navigation params for current route + function result.setParams(params) + validate(type(route.key) == "string", ".setParams(): cannot be called by the root navigator") + return NavigationActions.setParams({ params = params, key = route.key }) + end + + return result +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/getScreenForRouteName.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/getScreenForRouteName.lua new file mode 100644 index 0000000..a114ee0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/getScreenForRouteName.lua @@ -0,0 +1,32 @@ +local validate = require(script.Parent.Parent.utils.validate) +local isValidScreenComponent = require(script.Parent.Parent.utils.isValidScreenComponent) + +-- Extract a single screen Roact component/navigator from +-- a navigator's config. +return function(routeConfigs, routeName) + validate(type(routeConfigs) == "table", "routeConfigs must be a table") + validate(type(routeName) == "string", "routeName must be a string") + + local routeConfig = routeConfigs[routeName] + validate(routeConfig ~= nil, "There is no route defined for key '%s'.", routeName) + + local routeConfigType = type(routeConfig) + + if routeConfigType == "table" then + if routeConfig.screen ~= nil then + validate(isValidScreenComponent(routeConfig.screen), + "screen param for key '%s' must be a valid Roact component.", routeName) + return routeConfig.screen + elseif type(routeConfig.getScreen) == "function" then + local screen = routeConfig.getScreen() + validate(isValidScreenComponent(screen), + "The getScreen function defined for route '%s' did not return a valid screen or navigator", routeName) + return screen + end + end + + validate(isValidScreenComponent(routeConfig), + "Value for key '%s' must be a route config table or a valid Roact component.", routeName) + + return routeConfig +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/validateRouteConfigMap.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/validateRouteConfigMap.lua new file mode 100644 index 0000000..8cbc7cf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/validateRouteConfigMap.lua @@ -0,0 +1,44 @@ +local validate = require(script.Parent.Parent.utils.validate) +local isValidScreenComponent = require(script.Parent.Parent.utils.isValidScreenComponent) + +--[[ + This utility checks to make sure that configs passed to a + router are in the correct format. + + Example: + routeConfigs = { + routeNameEx1 = Roact.Function/Stateful_Component, + routeNameEx2 = { + screen = Roact.Function/Stateful_Component, + }, + routeNameEx3 = { + getScreen = function() + return Roact.Function/Stateful_Component + end + } + routeNameEx4 = AnotherRoactNavigator -- this is a Stateful Component + } +]] +return function(routeConfigs) + validate(type(routeConfigs) == "table", "routeConfigs must be a table") + + local atLeastOne = false + for routeName, routeConfig in pairs(routeConfigs) do + local configIsTable = type(routeConfig) == "table" or false + local screenConfig = configIsTable and routeConfig or {} -- easy index .screen/.getScreen + local screenComponent = configIsTable and routeConfig.screen or routeConfig + validate(isValidScreenComponent(screenComponent) or + (type(screenConfig.getScreen) == "function" and isValidScreenComponent(screenConfig.getScreen())), + "The component for route '%s' must be a Roact Function/Stateful component or table with 'getScreen'." .. + "getScreen function must return Roact Function/Stateful component.", + routeName) + + validate(screenConfig.screen == nil or screenConfig.getScreen == nil, + "Route '%s' should provide 'screen' or 'getScreen', but not both.", routeName) + atLeastOne = true + end + + validate(atLeastOne, "Please specify at least one route when configuring a navigator.") + + return routeConfigs +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/validateScreenOptions.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/validateScreenOptions.lua new file mode 100644 index 0000000..b4354b6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/validateScreenOptions.lua @@ -0,0 +1,10 @@ +local validate = require(script.Parent.Parent.utils.validate) + +return function(screenOptions, route) + validate(type(screenOptions) == "table", "screenOptions must be a table") + validate(type(route) == "table", "route must be a table") + validate(type(route.routeName) == "string", "route.routeName must be a string") + validate(type(screenOptions.title) ~= "function", + "title cannot be defined as a function in navigation options for screen '%s'", + route.routeName) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/KeyGenerator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/KeyGenerator.lua new file mode 100644 index 0000000..b21abea --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/KeyGenerator.lua @@ -0,0 +1,19 @@ +local uniqueBaseId = "id-" .. tostring(math.random(100000, 1000000)) +local uuidCount = 0 + +local KeyGenerator = {} + +-- NOTE: FOR TESTING ONLY. +-- Normalize keys so that tests can be consistent. +function KeyGenerator.normalizeKeys() + uniqueBaseId = "id-test-" + uuidCount = 0 +end + +-- Get a string key that is unique for this session. +function KeyGenerator.generateKey() + uuidCount = uuidCount + 1 + return uniqueBaseId .. tostring(uuidCount) +end + +return KeyGenerator diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/PageNavigationEvent.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/PageNavigationEvent.lua new file mode 100644 index 0000000..e225ab7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/PageNavigationEvent.lua @@ -0,0 +1,36 @@ +local validate = require(script.Parent.validate) + + +local PageNavigationEvent = {} +PageNavigationEvent.__index = PageNavigationEvent + +function PageNavigationEvent.new(pageName, event) + validate(typeof(pageName) == "string", "pageName should be string") + validate(typeof(event) == "userdata", "event should be RoactNavigation.Event") + local self = { + event = event, + pageName = pageName, + } + + setmetatable(self, PageNavigationEvent) + + return self +end + +function PageNavigationEvent.isPageNavigationEvent(instance) + return getmetatable(instance).__index == PageNavigationEvent +end + + +PageNavigationEvent.__tostring = function (pageNavigationEvent) + return string.format( "%-15s - %s", tostring(pageNavigationEvent.event), pageNavigationEvent.pageName) +end + + +function PageNavigationEvent:equalTo(anotherPageNavigationEvent) + validate(PageNavigationEvent.isPageNavigationEvent(anotherPageNavigationEvent), "should be PageNavigationEvent") + return self.pageName == anotherPageNavigationEvent.pageName and self.event == anotherPageNavigationEvent.event +end + + +return PageNavigationEvent \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/TableUtilities.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/TableUtilities.lua new file mode 100644 index 0000000..5922e53 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/TableUtilities.lua @@ -0,0 +1,255 @@ +--[[ + Provides functions for comparing and printing lua tables. +]] + +local TableUtilities = {} +local defaultIgnore = {} + +local function makeKeyString(key) + if type(key) == "string" then + return string.format("%s", key) + else + return string.format("[%s]", tostring(key)) + end +end + +local function makeValueString(value) + local valueType = type(value) + if valueType == "string" then + return string.format("%q", value) + elseif valueType == "function" or valueType == "table" then + return string.format("<%s>", tostring(value)) + else + return string.format("%s", tostring(value)) + end +end + +local function printKeypair(key, value, indentStr, comment) + local keyString = makeKeyString(key) + local valueString = makeValueString(value) + + local commentStr = comment and string.format(" -- %s", comment) or "" + print(string.format("%s%s = %s,%s", indentStr, keyString, valueString, commentStr)) +end + +--[[ + Takes two tables A and B, returns if they have the same key-value pairs + Except ignored keys +]] +function TableUtilities.ShallowEqual(A, B, ignore) + if not A or not B then + return false + elseif A == B then + return true + end + + if not ignore then + ignore = defaultIgnore + end + + for key, value in pairs(A) do + if B[key] ~= value and not ignore[key] then + return false + end + end + for key, value in pairs(B) do + if A[key] ~= value and not ignore[key] then + return false + end + end + + return true +end + +--[[ + Takes two tables A, B and a key, returns if two tables have the same value at key +]] +function TableUtilities.EqualKey(A, B, key) + if A and B and key and key ~= "" and A[key] and B[key] and A[key] == B[key] then + return true + end + return false +end + +--[[ + Takes two tables A and B, returns a new table with elements of A + which are either not keys in B or have a different value in B +]] +function TableUtilities.TableDifference(A, B) + local new = {} + + for key, value in pairs(A) do + if B[key] ~= A[key] then + new[key] = value + end + end + + return new +end + + +--[[ + Takes a list and returns a table whose + keys are elements of the list and whose + values are all true +]] +local function membershipTable(list) + local result = {} + for i = 1, #list do + result[list[i]] = true + end + return result +end + + +--[[ + Takes a table and returns a list of keys in that table +]] +local function listOfKeys(t) + local result = {} + for key,_ in pairs(t) do + table.insert(result, key) + end + return result +end + + +--[[ + Takes two lists A and B, returns a new list of elements of A + which are not in B +]] +function TableUtilities.ListDifference(A, B) + return listOfKeys(TableUtilities.TableDifference(membershipTable(A), membershipTable(B))) +end + + +--[[ + For debugging. Returns false if the given table has any of the following: + - a key that is neither a number or a string + - a mix of number and string keys + - number keys which are not exactly 1..#t +]] +function TableUtilities.CheckListConsistency(t) + local containsNumberKey = false + local containsStringKey = false + local numberConsistency = true + + local index = 1 + for x, _ in pairs(t) do + if type(x) == 'string' then + containsStringKey = true + elseif type(x) == 'number' then + if index ~= x then + numberConsistency = false + end + containsNumberKey = true + else + return false + end + + if containsStringKey and containsNumberKey then + return false + end + + index = index + 1 + end + + if containsNumberKey then + return numberConsistency + end + + return true +end + + +--[[ + For debugging, serializes the given table to a reasonable string that might even interpret as lua. +]] +function TableUtilities.RecursiveToString(t, indent) + indent = indent or '' + + if type(t) == 'table' then + local result = "" + if not TableUtilities.CheckListConsistency(t) then + result = result .. "-- WARNING: this table fails the list consistency test\n" + end + result = result .. "{\n" + for k,v in pairs(t) do + if type(k) == 'string' then + result = result + .. " " + .. indent + .. tostring(k) + .. " = " + .. TableUtilities.RecursiveToString(v, " "..indent) + ..";\n" + end + if type(k) == 'number' then + result = result .. " " .. indent .. TableUtilities.RecursiveToString(v, " "..indent)..",\n" + end + end + result = result .. indent .. "}" + return result + else + return tostring(t) + end +end + +--[[ + For debugging. Prints the table on multiple lines to overcome log-line length + limitations which are otherwise necessary for performance. Use sparingly. +]] +function TableUtilities.Print(t, indent) + indent = indent or ' ' + + if type(t) ~= "table" then + error("TableUtilities.Print must be passed a table", 2) + end + + -- For cycle detection + local printedTables = {} + + local function recurse(subTable, tableKey, level) + -- Prevent cycles by keeping track of what tables we have printed + printedTables[subTable] = true + + local indentStr = string.rep(indent, level) + local valueIndentStr = string.rep(indent, level + 1) + + if tableKey then + print(string.format("%s%s = %s {", indentStr, makeKeyString(tableKey), makeValueString(subTable))) + else + print(string.format("%s%s {", indentStr, makeValueString(subTable))) + end + + for key, value in pairs(subTable) do + if type(value) == "table" then + if printedTables[value] then + printKeypair(key, value, valueIndentStr, "Possible cycle") + else + recurse(value, key, level + 1) + end + else + printKeypair(key, value, valueIndentStr) + end + end + + print(string.format("%s}%s", indentStr, (level > 0 and "," or ""))) + end + + recurse(t, nil, 0) +end + +--[[ + Takes a table and returns the field count +]] +function TableUtilities.FieldCount(t) + local fieldCount = 0 + for _ in pairs(t) do + fieldCount = fieldCount + 1 + end + return fieldCount +end + +return TableUtilities + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/TrackNavigationEvents.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/TrackNavigationEvents.lua new file mode 100644 index 0000000..4974cd4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/TrackNavigationEvents.lua @@ -0,0 +1,72 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local RoactNavigation = require(script.Parent.Parent) +local NavigationEventsAdapter = require(script.Parent.Parent.views.NavigationEventsAdapter) +local validate = require(script.Parent.validate) +local PageNavigationEvent = require(script.Parent.PageNavigationEvent) + +local TrackNavigationEvents = {} +TrackNavigationEvents.__index = TrackNavigationEvents + +function TrackNavigationEvents.new() + local self = { + navigationEvents = {}, + } + + setmetatable(self, TrackNavigationEvents) + + return self +end + +function TrackNavigationEvents:getNavigationEvents() + return self.navigationEvents +end + +function TrackNavigationEvents:printNavigationEvents() + print("Total Events: ", #self.navigationEvents) + for _, navigationEvent in ipairs(self.navigationEvents) do + print(navigationEvent) + end +end + +function TrackNavigationEvents:waitForNumberEventsMaxWaitTime(numberOfEvents, maxWaitTimeInSeconds) + local secondsWaitedFor = 0 + local waitDurationPerIteration = 0.33 + while #self.navigationEvents < numberOfEvents + and secondsWaitedFor <= maxWaitTimeInSeconds + do + wait(waitDurationPerIteration) + -- print("waiting for number of events to reach:", numberOfEvents, "waited for:", secondsWaitedFor) + secondsWaitedFor = secondsWaitedFor + waitDurationPerIteration + end +end + +function TrackNavigationEvents:resetNavigationEvents() + self.navigationEvents = {} +end + +function TrackNavigationEvents:createNavigationAdapter(pageName, components) + local events = {} + for _, event in pairs(RoactNavigation.Events) do + events[event] = function() + PageNavigationEvent.new(pageName, event) + table.insert(self.navigationEvents, PageNavigationEvent.new(pageName, event)) + end + end + + return Roact.createElement(NavigationEventsAdapter, events, components) +end + +function TrackNavigationEvents:equalTo(pageNavigationEventList) + validate(typeof(pageNavigationEventList) == "table", "should be a list") + local numberOfEvents = #self.navigationEvents + local equal = numberOfEvents == #pageNavigationEventList + local eventIndex = 1 + while eventIndex <= numberOfEvents and equal do + equal = self.navigationEvents[eventIndex]:equalTo(pageNavigationEventList[eventIndex]) + eventIndex = eventIndex + 1 + end + + return equal +end + +return TrackNavigationEvents diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/KeyGenerator.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/KeyGenerator.spec.lua new file mode 100644 index 0000000..2e414ac --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/KeyGenerator.spec.lua @@ -0,0 +1,14 @@ +return function() + local KeyGenerator = require(script.Parent.Parent.KeyGenerator) + + it("should generate a new string key when called", function() + KeyGenerator.normalizeKeys() + + expect(KeyGenerator.generateKey()).to.equal("id-test-1") + expect(KeyGenerator.generateKey()).to.equal("id-test-2") + end) + + it("should generate unique string keys without being normalized", function() + expect(KeyGenerator.generateKey()).to.never.equal(KeyGenerator.generateKey()) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/PageNavigationEvent.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/PageNavigationEvent.spec.lua new file mode 100644 index 0000000..82551b2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/PageNavigationEvent.spec.lua @@ -0,0 +1,43 @@ +local RoactNavigation = require(script.Parent.Parent.Parent) +local PageNavigationEvent = require(script.Parent.Parent.PageNavigationEvent) + +return function() + local testPage = "TEST PAGE" + local willFocusEvent = RoactNavigation.Events.WillFocus + + it("should validate constructor inputs", function() + expect(function () PageNavigationEvent.new(testPage, willFocusEvent) end).never.to.throw() + + expect(function () PageNavigationEvent.new(testPage, 1) end).to.throw() + expect(function () PageNavigationEvent.new(testPage, "event") end).to.throw() + expect(function () PageNavigationEvent.new(testPage, nil) end).to.throw() + expect(function () PageNavigationEvent.new(testPage, {some = "junk"}) end).to.throw() + expect(function () PageNavigationEvent.new(1, willFocusEvent) end).to.throw() + expect(function () PageNavigationEvent.new(nil, willFocusEvent) end).to.throw() + expect(function () PageNavigationEvent.new({"bogus"}, willFocusEvent) end).to.throw() + end) + + it("should be constructed from page name and RoactNavigation.Events", function() + for _, event in pairs(RoactNavigation.Events) do + local pageName = testPage .. tostring(event) + local testPageNavigationEvent = PageNavigationEvent.new(pageName, event) + expect(testPageNavigationEvent.pageName).to.be.equal(pageName) + expect(testPageNavigationEvent.event).to.be.equal(event) + expect(PageNavigationEvent.isPageNavigationEvent(testPageNavigationEvent)).to.be.equal(true) + end + end) + + it("should implement tostring and eq", function() + for _, event in pairs(RoactNavigation.Events) do + local pageName = testPage .. tostring(event) + local testPageNavigationEvent = PageNavigationEvent.new(pageName, event) + expect(testPageNavigationEvent:equalTo(PageNavigationEvent.new(pageName, event))).to.be.equal(true) + expect(tostring(testPageNavigationEvent)).to.be.equal(string.format("%-15s - %s",tostring(event), pageName)) + end + + local testPageNavigationEvent = PageNavigationEvent.new(testPage, willFocusEvent) + expect(testPageNavigationEvent:equalTo(PageNavigationEvent.new(testPage, willFocusEvent))).to.be.equal(true) + expect(testPageNavigationEvent:equalTo(PageNavigationEvent.new(testPage .. "bogus", willFocusEvent))).to.be.equal(false) + expect(testPageNavigationEvent:equalTo(PageNavigationEvent.new(testPage, RoactNavigation.Events.WillBlur))).to.be.equal(false) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/TrackNavigationEvents.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/TrackNavigationEvents.spec.lua new file mode 100644 index 0000000..1491d9b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/TrackNavigationEvents.spec.lua @@ -0,0 +1,36 @@ +local RoactNavigation = require(script.Parent.Parent.Parent) +local TrackNavigationEvents = require(script.Parent.Parent.TrackNavigationEvents) +local PageNavigationEvent = require(script.Parent.Parent.PageNavigationEvent) + + +return function() + local testPage = "TEST PAGE" + local testPageWillFocus = PageNavigationEvent.new(testPage, RoactNavigation.Events.WillFocus) + local testPageWillBlur = PageNavigationEvent.new(testPage, RoactNavigation.Events.WillBlur) + + local trackNavigationEvents = TrackNavigationEvents.new() + it("should implement equalTo function", function() + expect(trackNavigationEvents:equalTo({})).to.be.equal(true) + + local navigationEvents = trackNavigationEvents:getNavigationEvents() + table.insert(navigationEvents, testPageWillFocus) + expect(trackNavigationEvents:equalTo({testPageWillFocus})).to.be.equal(true) + expect(trackNavigationEvents:equalTo({})).to.be.equal(false) + + table.insert(navigationEvents, testPageWillBlur) + expect(trackNavigationEvents:equalTo({testPageWillFocus, testPageWillBlur})).to.be.equal(true) + + table.insert(navigationEvents, testPageWillFocus) + expect(trackNavigationEvents:equalTo({testPageWillFocus, testPageWillBlur})).to.be.equal(false) + expect(trackNavigationEvents:equalTo({testPageWillFocus, testPageWillBlur, testPageWillFocus})).to.be.equal(true) + end) + + it("should be empty after reset", function() + trackNavigationEvents:resetNavigationEvents() + local navigationEvents = trackNavigationEvents:getNavigationEvents() + expect(typeof(navigationEvents)).to.be.equal('table') + expect(#navigationEvents).to.be.equal(0) + expect(trackNavigationEvents:equalTo({})).to.be.equal(true) + end) + +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/getActiveChildNavigationOptions.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/getActiveChildNavigationOptions.spec.lua new file mode 100644 index 0000000..99c57a5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/getActiveChildNavigationOptions.spec.lua @@ -0,0 +1,47 @@ +return function() + local getActiveChildNavigationOptions = require(script.Parent.Parent.getActiveChildNavigationOptions) + + it("should return a function", function() + expect(type(getActiveChildNavigationOptions)).to.equal("function") + end) + + it("should ask router for current screen options and return them", function() + local testInputScreenOpts = {} + local testScreenOpts = {} + + local navigation = { + state = { + routes = { + { key = "123" } + }, + index = 1, + + }, + router = {}, -- stub + } + + function navigation.getChildNavigation(key) + if key == "123" then + return navigation + else + return nil + end + end + + local testOutputScreenOpts = nil + function navigation.router.getScreenOptions(activeNav, screenProps) + testOutputScreenOpts = screenProps + + if activeNav == navigation then + return testScreenOpts + else + return nil + end + end + + expect(getActiveChildNavigationOptions(navigation, testInputScreenOpts)) + .to.equal(testScreenOpts) + expect(testOutputScreenOpts).to.equal(testInputScreenOpts) + end) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/isValidScreenComponent.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/isValidScreenComponent.spec.lua new file mode 100644 index 0000000..e1bbffc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/isValidScreenComponent.spec.lua @@ -0,0 +1,25 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local isValidScreenComponent = require(script.Parent.Parent.isValidScreenComponent) + local TestComponent = Roact.Component:extend("TestFoo") + it("should return true for valid element types", function() + -- Function Component is valid + expect(isValidScreenComponent(function() end)).to.equal(true) + -- Stateful Component is valid + expect(isValidScreenComponent(TestComponent)).to.equal(true) + expect(isValidScreenComponent( + { render = function() return TestComponent end })).to.equal(true) + expect(isValidScreenComponent( -- we do not test if render function returns valid component + { render = function() end })).to.equal(true) + end) + + it("should return false for invalid element types", function() + expect(isValidScreenComponent("foo")).to.equal(false) + expect(isValidScreenComponent(Roact.createElement("Frame"))).to.equal(false) + expect(isValidScreenComponent(5)).to.equal(false) + expect(isValidScreenComponent(Roact.Portal)).to.equal(false) + expect(isValidScreenComponent({ render = "bad" })).to.equal(false) + expect(isValidScreenComponent( + { notRender = function() return "foo" end })).to.equal(false) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/lerp.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/lerp.spec.lua new file mode 100644 index 0000000..6996e7a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/lerp.spec.lua @@ -0,0 +1,21 @@ +return function() + local lerp = require(script.Parent.Parent.lerp) + + it("should return bottom of range for bottom input", function() + expect(lerp(0, 1, 0)).to.equal(0) + expect(lerp(1, 0, 0)).to.equal(1) + expect(lerp(-1, 0, 0)).to.equal(-1) + end) + + it("should return middle of range for middle input", function() + expect(lerp(0, 1, 0.5)).to.equal(0.5) + expect(lerp(1, 0, 0.5)).to.equal(0.5) + expect(lerp(-1, 0, 0.5)).to.equal(-0.5) + end) + + it("should return top of range for top input", function() + expect(lerp(0, 1, 1)).to.equal(1) + expect(lerp(1, 0, 1)).to.equal(0) + expect(lerp(-1, 0, 1)).to.equal(0) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/createSubscriberEventsStateTable.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/createSubscriberEventsStateTable.lua new file mode 100644 index 0000000..7a65f0a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/createSubscriberEventsStateTable.lua @@ -0,0 +1,202 @@ +local StateTable = require(script.Parent.Parent.Parent.StateTable) +local Events = require(script.Parent.Parent.NavigationEvents) +local validate = require(script.Parent.validate) + +local WILL_FOCUS_KEY = tostring(Events.WillFocus) +local DID_FOCUS_KEY = tostring(Events.DidFocus) +local WILL_BLUR_KEY = tostring(Events.WillBlur) +local DID_BLUR_KEY = tostring(Events.DidBlur) +local ACTION_KEY = tostring(Events.Action) +local REFOCUS_KEY = tostring(Events.Refocus) + +--[[ + Provides a StateTable for sending events to user screens on screen state changes. + + Args: + name - Name for state table debugging. + emit(type, payload) - Function to emit a single raw event. + disconnectAll() - Function to disconnect all listeners. + + States: + Blurred - Screen is not visible, but was at least animated on/off before. + Focusing - Screen is moving to being visible. + Focused - Screen is visible. + Blurring - Screen is moving to being hidden. + Disconnected - Screen is no longer part of the hierarchy and will not propagate events to user. + + Accepted Event Types (always sent using Normal Events Pattern): + NavigationEvents.WillFocus + NavigationEvents.DidFocus + NavigationEvents.WillBlur + NavigationEvents.DidBlur + NavigationEvents.Action + NavigationEvents.Refocus - Note that Refocus events do NOT support the pattern. Send them without adornment! + + Normal Events Pattern: + [ .. "A"] - Received event while screen is active child and not currently transitioning. + [ .. "T"] - Received event while screen is not active child, but is transitioning. + [ .. "AT"] - Received event while screen is active child and currently transitioning. + [] - Received event while screen is not active child, and not currently transitioning. + + Special Events: + shutdown - Page has been removed from hierarchy, so shut down further event handling. +]] +return function(name, initialState, emit, disconnectAll) + validate(type(name) == "string", "name must be a string") + validate(type(initialState) == "string", "initialState must be a string") + validate(type(emit) == "function", "emitAction must be a function") + validate(type(disconnectAll) == "function", "disconnectAllAction must be a function") + + local function doEmit(...) + local eventList = {...} + return function(_, _, payload) + for _, event in ipairs(eventList) do + emit(event, payload) + end + end + end + + return StateTable.new("SubscriberEventsTable(" .. name .. ")", initialState, nil, { + Blurred = { + -- If child is active and not transitioning then we jump immediately to Focused state + -- while sending the complete event sequence. + [WILL_FOCUS_KEY .. "A"] = { + nextState = "Focused", + action = doEmit(Events.WillFocus, Events.DidFocus), + }, + [ACTION_KEY .. "A"] = { + nextState = "Focused", + action = doEmit(Events.WillFocus, Events.DidFocus), + }, + -- If child is active and we're in transition, we are visibly moving to it and + -- a completion event will fire, so move to Focusing state. + [WILL_FOCUS_KEY .. "AT"] = { + nextState = "Focusing", + action = doEmit(Events.WillFocus), + }, + [ACTION_KEY .. "AT"] = { + nextState = "Focusing", + action = doEmit(Events.WillFocus), + }, + + -- Shutdown event can occur after any other event, but only has teeth in blurred state. + -- TODO: Do we need to support this from any state and broadcast all of the events that + -- would lead it to Blurred from the screen's current state? + shutdown = { + nextState = "Disconnected", + action = disconnectAll, + }, + + -- All Refocus events are simply passed through. + [REFOCUS_KEY] = { + action = doEmit(Events.Refocus), + }, + }, + + Focusing = { + -- Note that DidFocus is only propagated if child is active and we're NOT transitioning, since + -- a child that is still transitioning is not finished yet. + [DID_FOCUS_KEY .. "A"] = { + nextState = "Focused", + action = doEmit(Events.DidFocus), + }, + [ACTION_KEY .. "A"] = { + nextState = "Focused", + action = doEmit(Events.DidFocus), + }, + -- It's possible to blur while still transitioning on screen due to multiple user taps. Note that + -- in this case, this child would not be active, and navigation state obviously must still be + -- transitioning to the new screen. + [WILL_BLUR_KEY .. "T"] = { + nextState = "Blurring", + action = doEmit(Events.WillBlur), + }, + + -- All Refocus events are simply passed through. + [REFOCUS_KEY] = { + action = doEmit(Events.Refocus), + }, + }, + Focused = { + -- Any WillBlur event gets propagated, regardless of active or transitioning states. + [WILL_BLUR_KEY] = { + nextState = "Blurring", + action = doEmit(Events.WillBlur), + }, + [WILL_BLUR_KEY .. "T"] = { + nextState = "Blurring", + action = doEmit(Events.WillBlur), + }, + [WILL_BLUR_KEY .. "A"] = { + nextState = "Blurring", + action = doEmit(Events.WillBlur), + }, + [WILL_BLUR_KEY .. "AT"] = { + nextState = "Blurring", + action = doEmit(Events.WillBlur), + }, + -- Action events drive blurring. When the screen is Focused but loses its active mark in + -- navigation state, it becomes blurred. + [ACTION_KEY] = { + nextState = "Blurring", + action = doEmit(Events.WillBlur), + }, + [ACTION_KEY .. "T"] = { + nextState = "Blurring", + action = doEmit(Events.WillBlur), + }, + -- Any Action event while screen is focused needs to be passed along to the children. + [ACTION_KEY .. "A"] = { + action = doEmit(Events.Action), + }, + [ACTION_KEY .. "AT"] = { + action = doEmit(Events.Action), + }, + + -- All Refocus events are simply passed through. + [REFOCUS_KEY] = { + action = doEmit(Events.Refocus), + }, + }, + Blurring = { + -- We're done blurring on any action event once we are not focused and no longer transitioning. + [ACTION_KEY] = { + nextState = "Blurred", + action = doEmit(Events.DidBlur), + }, + -- If parent is fully blurred, then so are we, and so are our children. + [DID_BLUR_KEY] = { + nextState = "Blurred", + action = doEmit(Events.DidBlur), + }, + [DID_BLUR_KEY .. "A"] = { + nextState = "Blurred", + action = doEmit(Events.DidBlur), + }, + [DID_BLUR_KEY .. "T"] = { + nextState = "Blurred", + action = doEmit(Events.DidBlur), + }, + [DID_BLUR_KEY .. "AT"] = { + nextState = "Blurred", + action = doEmit(Events.DidBlur), + }, + -- Action that results in screen being active and there is no transition... We go straight back to Focused state! + [ACTION_KEY .. "A"] = { + nextState = "Focused", + action = doEmit(Events.DidFocus) + }, + -- If we become active and nav state is transitioning then user took an action to refocus this screen. + [ACTION_KEY .. "AT"] = { + nextState = "Focusing", + action = doEmit(Events.WillFocus), + }, + + -- All Refocus events are simply passed through. + [REFOCUS_KEY] = { + action = doEmit(Events.Refocus), + }, + }, + Disconnected = {}, + }) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/getActiveChildNavigationOptions.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/getActiveChildNavigationOptions.lua new file mode 100644 index 0000000..a036af6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/getActiveChildNavigationOptions.lua @@ -0,0 +1,11 @@ +return function(navigation, screenProps) + local state = navigation.state + local router = navigation.router + local getChildNavigation = navigation.getChildNavigation + + local activeRoute = state.routes[state.index] + local activeNavigation = getChildNavigation(activeRoute.key) + + return router.getScreenOptions(activeNavigation, screenProps) +end + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/getSceneIndicesForInterpolationInputRange.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/getSceneIndicesForInterpolationInputRange.lua new file mode 100644 index 0000000..a2e5c7c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/getSceneIndicesForInterpolationInputRange.lua @@ -0,0 +1,44 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) + +return function(props) + local scene = props.scene + local scenes = props.scenes + + local index = scene.index + local lastSceneIndexInScenes = #scenes + local isBack = not scenes[lastSceneIndexInScenes].isActive + + if isBack then + local currentSceneIndexInScenes = Cryo.List.find(scenes, scene) + + local targetSceneIndexInScenes = nil + for i, iScene in ipairs(scenes) do + if iScene.isActive then + targetSceneIndexInScenes = i + break + end + end + + local targetSceneIndex = scenes[targetSceneIndexInScenes].index + local lastSceneIndex = scenes[lastSceneIndexInScenes].index + + if index ~= targetSceneIndex and currentSceneIndexInScenes == lastSceneIndexInScenes then + return { + first = math.min(targetSceneIndex, index - 1), + last = index + 1, + } + elseif index == targetSceneIndex and currentSceneIndexInScenes == targetSceneIndexInScenes then + return { + first = index - 1, + last = math.max(lastSceneIndex, index + 1) + } + elseif index == targetSceneIndex or currentSceneIndexInScenes > targetSceneIndexInScenes then + return nil + end + end + + return { + first = index - 1, + last = index + 1 + } +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/isValidScreenComponent.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/isValidScreenComponent.lua new file mode 100644 index 0000000..66f9b26 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/isValidScreenComponent.lua @@ -0,0 +1,10 @@ +-- We have to do this using type because ElementKind is not exported by Roact +return function (screenComponent) + local componentType = type(screenComponent) + local valid = componentType == "function" or -- Function Component + (componentType == "table" and type(screenComponent.render) == "function") -- Stateful Component + return valid +end + + + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/lerp.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/lerp.lua new file mode 100644 index 0000000..b06b11d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/lerp.lua @@ -0,0 +1,6 @@ +-- Helper interpolates t with range [0,1] into the range [a,b]. +local function lerp(a, b, t) + return a * (1 - t) + b * t +end + +return lerp diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/validate.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/validate.lua new file mode 100644 index 0000000..a31a4ce --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/validate.lua @@ -0,0 +1,19 @@ +--[[ + Validate() provides a mechanism to validate input arguments and + internal state for your components. You can call it like this: + + function myFunc(arg1, arg2) + validate(arg1 ~= arg2, "arg1 (%s) and arg2 (%s) must be different!", + tostring(arg1), tostring(arg2)) + return doSomething(arg1, arg2) + end + + The error will be surfaced at the *call site* of your function. +]] +return function(result, ...) + if not result then + error(string.format(...), 3) + end + + return result +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/AppNavigationContext.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/AppNavigationContext.lua new file mode 100644 index 0000000..ac91d5c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/AppNavigationContext.lua @@ -0,0 +1,60 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol) +local validate = require(script.Parent.Parent.utils.validate) + +local APP_NAVIGATION_CONTEXT = NavigationSymbol("APP_NAVIGATION_CONTEXT") + +-- Provider +local NavigationProvider = Roact.Component:extend("NavigationProvider") + +function NavigationProvider:init() + local navigation = self.props.navigation + validate(navigation ~= nil, "AppNavigationContext.Provider requires a 'navigation' prop.") + self._context[APP_NAVIGATION_CONTEXT] = { navigation = navigation } +end + +function NavigationProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +-- Consumer +local NavigationConsumer = Roact.Component:extend("NavigationConsumer") + +function NavigationConsumer:render() + local renderProp = self.props.render + local context = self._context[APP_NAVIGATION_CONTEXT] or {} + local navigation = self.props.navigation or context.navigation + + validate(renderProp ~= nil, "AppNavigationContext.Consumer requires 'render' prop.") + validate(navigation ~= nil, "AppNavigationContext.Consumer requires a navigation prop or context entry.") + + return renderProp(navigation) +end + +-- Static connector +local function connect(innerComponent) + local componentName = string.format("NavigationConnection(%s)", tostring(innerComponent)) + local Connection = Roact.Component:extend(componentName) + + function Connection:render() + local props = self.props + + return Roact.createElement(NavigationConsumer, { + navigation = props.navigation, -- can be passed directly to wrapper + render = function(navigation) + return Roact.createElement(innerComponent, Cryo.Dictionary.join({ + navigation = navigation + }, props)) -- join other props last so someone can manually pass in 'navigation' + end + }) + end + + return Connection +end + +return { + Provider = NavigationProvider, + Consumer = NavigationConsumer, + connect = connect, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/NavigationEventsAdapter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/NavigationEventsAdapter.lua new file mode 100644 index 0000000..76c8c1b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/NavigationEventsAdapter.lua @@ -0,0 +1,91 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local AppNavigationContext = require(script.Parent.AppNavigationContext) +local NavigationEvents = require(script.Parent.Parent.NavigationEvents) +local validate = require(script.Parent.Parent.utils.validate) + +--[[ + NavigationEventsAdapter providers a wrapper component that allows you to subscribe + to the navigation lifecycle events without having to explicitly manage your own + listener subscriptions. + + Usage: + + function MyComponent:init() + self.willFocus = function() + -- Do tasks that need to happen right before the component will appear on screen. + end + + self.didFocus = function() + -- Do tasks that need to happen right after the component appears on screen. + end + end + + function MyComponent:render() + -- Note that you must capture the self reference lexically, if you need it. + return Roact.createElement(RoactNavigation.EventsAdapter, { + [RoactNavigation.Events.WillFocus] = self.willFocus, + [RoactNavigation.Events.DidFocus] = self.didFocus, + [RoactNavigation.Events.WillBlur] = self.willBlur, + [RoactNavigation.Events.DidBlur] = self.didBlur, + }, ) + end + + Remember that focus and blur events may be called more than once in the lifetime of a + component. If you navigate away from a component and then come back later, it will receive + willBlur/didBlur and then willFocus/didFocus events. + + Also remember that your event handlers must capture any self reference lexically, if necessary. +]] +local NavigationEventsAdapter = Roact.Component:extend("NavigationEventsAdapter") + +function NavigationEventsAdapter:init() + self.subscriptions = {} +end + +function NavigationEventsAdapter:_subscribeAll() + local navigation = self.props.navigation + assert(navigation ~= nil, "NavigationEventsAdapter can only be used within the view hierarchy of a navigator.") + + for _, symbol in pairs(NavigationEvents) do + self.subscriptions[symbol] = navigation.addListener(symbol, function(...) + -- Retrieve callback from props each time, in case props change. + local callback = self.props[symbol] or nil + if callback then + validate(type(callback) == "function", "Value for event '%s' must be a function callback", tostring(symbol)) + callback(...) + end + end) + end +end + +function NavigationEventsAdapter:_disconnectAll() + for _, symbol in pairs(NavigationEvents) do + local sub = self.subscriptions[symbol] + if sub then + sub.disconnect() + self.subscriptions[symbol] = nil + end + end +end + +function NavigationEventsAdapter:didMount() + self:_subscribeAll() +end + +function NavigationEventsAdapter:willUnmount() + self:_disconnectAll() +end + +function NavigationEventsAdapter:didUpdate(prevProps) + if self.props.navigation ~= prevProps.navigation then + -- This component might get reused for different state, so we need to hook back up to events + self:_disconnectAll() + self:_subscribeAll() + end +end + +function NavigationEventsAdapter:render() + return Roact.createElement("Folder", nil, self.props[Roact.Children]) +end + +return AppNavigationContext.connect(NavigationEventsAdapter) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/SceneView.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/SceneView.lua new file mode 100644 index 0000000..4e8fc00 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/SceneView.lua @@ -0,0 +1,21 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local AppNavigationContext = require(script.Parent.AppNavigationContext) + +local SceneView = Roact.PureComponent:extend("SceneView") + +function SceneView:render() + local screenProps = self.props.screenProps + local component = self.props.component + local navigation = self.props.navigation + + return Roact.createElement(AppNavigationContext.Provider, { + navigation = navigation, + }, { + Scene = Roact.createElement(component, { + screenProps = screenProps, + navigation = navigation, + }) + }) +end + +return SceneView diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/ScenesReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/ScenesReducer.lua new file mode 100644 index 0000000..8d8ace9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/ScenesReducer.lua @@ -0,0 +1,201 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local TableUtilities = require(script.Parent.Parent.utils.TableUtilities) +local validate = require(script.Parent.Parent.utils.validate) + +local SCENE_KEY_PREFIX = "scene_" + +-- Compare two scenes based upon index and view key. +local function compareScenes(a, b) + if a.index == b.index then + -- compare the route keys + local delta = #a.key - #b.key + if delta == 0 then + return a.key < b.key + else + return delta < 0 + end + else + -- rank by index first + return a.index < b.index + end +end + +local function routesAreShallowEqual(a, b) + if not a or not b then + return a == b + end + + if a.key ~= b.key then + return false + end + + return TableUtilities.ShallowEqual(a, b) +end + +local function scenesAreShallowEqual(a, b) + return + a.key == b.key and + a.index == b.index and + a.isStale == b.isStale and + a.isActive == b.isActive and + routesAreShallowEqual(a, b) +end + +return function(scenes, nextState, prevState, descriptors) + -- Always update descriptors. See react-navigation's bug, here: + -- https://github.com/react-navigation/react-navigation/issues/4271 + -- TODO: Do we need this? Can we do a real fix? + for _, scene in ipairs(scenes) do + local route = scene.route + if descriptors and descriptors[route.key] then + scene.descriptor = descriptors[route.key] + end + end + + -- Bail out early if state is not updated + if prevState == nextState then + return scenes + end + + local prevScenes = {} + local freshScenes = {} + local staleScenes = {} + + -- previously stale scenes should be marked stale + for _, scene in ipairs(scenes) do + local key = scene.key + if scene.isStale then + staleScenes[key] = scene + end + + prevScenes[key] = scene + end + + local nextKeys = {} -- fake set! + local nextRoutes = nextState.routes + local nextRoutesLength = #nextRoutes + + -- Clip nextRoutes to stop at index because index is top of stack! + if nextRoutesLength > nextState.index then + print("Warning: StackRouter provided invalid state. Index should always be the top route") + nextRoutes = Cryo.List.removeRange(nextRoutes, nextState.index, nextRoutesLength) + end + + for index, route in ipairs(nextRoutes) do + local key = SCENE_KEY_PREFIX .. route.key + local descriptor = descriptors and descriptors[route.key] or nil + + local scene = { + index = index, + isActive = false, + isStale = false, + key = key, + route = route, + descriptor = descriptor, + } + + validate(not nextKeys[key], "navigation.state.routes[%d].key '%s' conflicts with another route!", index, key) + nextKeys[key] = true + + if staleScenes[key] then + -- Previously stale scene was added to nextState, so we remove it from + -- the map of stale scenes. + staleScenes[key] = nil + end + + freshScenes[key] = scene + end + + if prevState then + local prevRoutes = prevState.routes + local prevRoutesLength = #prevRoutes + if prevRoutesLength > prevState.index then + print("StackRouter provided invalid state. Index should always be the top route.") + prevRoutes = Cryo.List.removeRange(prevRoutes, prevState.index, prevRoutesLength) + end + + -- Search previous routes and mark any removed scenes as stale + for index, route in ipairs(prevRoutes) do + local key = SCENE_KEY_PREFIX .. route.key + -- Skip any refreshed scenes + if not freshScenes[key] then + local lastScene = nil + for _, scene in ipairs(scenes) do + if scene.route.key == route.key then + lastScene = scene + break + end + end + + local descriptor = descriptors[route.key] + if lastScene then + descriptor = lastScene.descriptor + end + + if descriptor then + staleScenes[key] = { + index = index, + isActive = false, + isStale = true, + key = key, + route = route, + descriptor = descriptor, + } + end + end + end + end + + local nextScenes = {} + + local function mergeScene(nextScene) + local key = nextScene.key + local prevScene = prevScenes[key] or nil + if prevScene and scenesAreShallowEqual(prevScene, nextScene) then + -- reuse prevScene to avoid re-render + table.insert(nextScenes, prevScene) + else + table.insert(nextScenes, nextScene) + end + end + + for _, scene in pairs(staleScenes) do + mergeScene(scene) + end + + for _, scene in pairs(freshScenes) do + mergeScene(scene) + end + + table.sort(nextScenes, compareScenes) + + local activeScenesCount = 0 + for index, scene in ipairs(nextScenes) do + local isActive = not scene.isStale and scene.index == nextState.index + if isActive ~= scene.isActive then + nextScenes[index] = Cryo.Dictionary.join(scene, { + isActive = isActive, + }) + end + + if isActive then + activeScenesCount = activeScenesCount + 1 + end + end + + validate(activeScenesCount == 1, "There should only be one active scene, not %d", activeScenesCount) + + -- Conditionally return nextScenes based upon shallow comparison, for performance + if #nextScenes ~= #scenes then + return nextScenes + end + + for index, scene in ipairs(nextScenes) do + if not scenesAreShallowEqual(scenes[index], scene) then + return nextScenes + end + end + + -- Scenes have not changed + return scenes +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/ScreenGuiWrapper.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/ScreenGuiWrapper.lua new file mode 100644 index 0000000..1bb98fa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/ScreenGuiWrapper.lua @@ -0,0 +1,36 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Roact) + +local ScreenGuiWrapper = Roact.PureComponent:extend("ScreenGuiWrapper") + +ScreenGuiWrapper.defaultProps = { + DisplayOrder = 0, + OnTopOfCoreBlur = false, + visible = true, +} + +function ScreenGuiWrapper:render() + local props = self.props + local component = props.component + local visible = props.visible + local displayOrder = props.DisplayOrder + local onTopOfCoreBlur = props.OnTopOfCoreBlur + + local filteredProps = Cryo.Dictionary.join(props, { + component = Cryo.None, + DisplayOrder = Cryo.None, + OnTopOfCoreBlur = Cryo.None, + -- visible prop is passed down for convenience of inner component. + }) + + return Roact.createElement("ScreenGui", { + Enabled = visible, + ZIndexBehavior = Enum.ZIndexBehavior.Sibling, + DisplayOrder = displayOrder, + OnTopOfCoreBlur = onTopOfCoreBlur, + }, { + InnerComponent = Roact.createElement(component, filteredProps) + }) +end + +return ScreenGuiWrapper diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackPresentationStyle.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackPresentationStyle.lua new file mode 100644 index 0000000..d11af5e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackPresentationStyle.lua @@ -0,0 +1,43 @@ +local NavigationSymbol = require(script.Parent.Parent.Parent.NavigationSymbol) + +local DEFAULT_SYMBOL = NavigationSymbol("DEFAULT") +local MODAL_SYMBOL = NavigationSymbol("MODAL") +local OVERLAY_SYMBOL = NavigationSymbol("OVERLAY") + +--[[ + StackPresentationStyle is used with stack navigators/views to determine + the behavior of a given screen when it is pushed/popped from the stack, as + well as the visual effects/transitions applied while the view is on screen. +]] +return { + --[[ + The Default presentation style draws stack cards so that they will + slide in and out from the right side of the screen one at a time. No + special visual effects are applied, and cards always fill the entire space + available. Your screen content is rendered over an opaque background color + by default, but you have the option to draw the cards with a semi- or fully + transparent background via navigationOptions. Cards always prevent + tap-through of the content underneath them (in case your navigation + container is transparent). + ]] + Default = DEFAULT_SYMBOL, + + --[[ + The Modal presentation style causes screens to animate up/down from the + bottom of the navigation container and visually stack on top of each + other. Cards are opaque by default, but you may set navigationOptions + to make them semi- or fully transparent so that you can see the underlying + cards. Modal cards always prevent tap-through of any underlying UI, including + other cards in the same stack. + ]] + Modal = MODAL_SYMBOL, + + --[[ + The Overlay presentation style causes screens to pop in (later they will fade in) + on top of the underlying screens. Like modals, they visually stack on top of each + other. Cards are opaque by default, but you may set navigationOptions to make + them semi- or fully transparent. Overlay cards always prevent tap-through of any + underlying UI, including other cards in the same stack. + ]] + Overlay = OVERLAY_SYMBOL +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackView.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackView.lua new file mode 100644 index 0000000..e2f01b3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackView.lua @@ -0,0 +1,134 @@ +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) +local StackViewLayout = require(script.Parent.StackViewLayout) +local Transitioner = require(script.Parent.Parent.Transitioner) +local StackViewTransitionConfigs = require(script.Parent.StackViewTransitionConfigs) +local StackPresentationStyle = require(script.Parent.StackPresentationStyle) + +local defaultNavigationConfig = { + mode = StackPresentationStyle.Default, +} + + +local StackView = Roact.Component:extend("StackView") + +function StackView:init() + self._doRender = function(...) + return self:_render(...) + end + + self._doConfigureTransition = function(...) + return self:_configureTransition(...) + end + + self._doOnTransitionStart = function(...) + return self:_onTransitionStart(...) + end + + self._doOnTransitionEnd = function(...) + return self:_onTransitionEnd(...) + end + + self._doOnTransitionStep = function(...) + return self:_onTransitionStep(...) + end +end + +function StackView:render() + local screenProps = self.props.screenProps + local navigation = self.props.navigation + local descriptors = self.props.descriptors + + -- Transitioner handles setting up the animation motors and making that data + -- available to the lower layer. + return Roact.createElement(Transitioner, { + render = self._doRender, + configureTransition = self._doConfigureTransition, + screenProps = screenProps, + navigation = navigation, + descriptors = descriptors, + onTransitionStart = self._doOnTransitionStart, + onTransitionEnd = self._doOnTransitionEnd, + onTransitionStep = self._doOnTransitionStep, + }) +end + +function StackView:didMount() + local navigation = self.props.navigation + if navigation.state.isTransitioning then + navigation.dispatch(NavigationActions.completeTransition({ + key = navigation.state.key, + })) + end +end + +function StackView:_render(transition, lastTransition) + local screenProps = self.props.screenProps + local navigationConfig = Cryo.Dictionary.join(defaultNavigationConfig, self.props.navigationConfig) + local descriptors = self.props.descriptors + + return Roact.createElement(StackViewLayout, Cryo.Dictionary.join(navigationConfig, { + screenProps = screenProps, + descriptors = descriptors, + transitionProps = transition, + lastTransitionProps = lastTransition, + })) +end + +function StackView:_configureTransition(transition, lastTransition) + return StackViewTransitionConfigs.getTransitionConfig( + self.props.navigationConfig.transitionConfig, + transition, + lastTransition, + self.props.navigationConfig.mode + ).transitionSpec +end + +function StackView:_onTransitionStart(transition, lastTransition) + local onTransitionStart = self.props.onTransitionStart + or self.props.navigationConfig.onTransitionStart + + -- Only propagate transition changes to caller for transitions where the actual + -- index has changed. Transitioner sends updates for _all_ transitions, including + -- those to the same screen that result from animation completion events. + if onTransitionStart and transition.index ~= lastTransition.index then + onTransitionStart(transition.navigation, lastTransition.navigation) + end +end + +function StackView:_onTransitionEnd(transition, lastTransition) + local navigationConfig = self.props.navigationConfig + local navigation = self.props.navigation + local onTransitionEnd = self.props.onTransitionEnd or navigationConfig.onTransitionEnd + local transitionDestKey = transition.scene.route.key + local isCurrentKey = navigation.state.routes[navigation.state.index].key == transitionDestKey + + if transition.navigation.state.isTransitioning and isCurrentKey then + navigation.dispatch(NavigationActions.completeTransition({ + key = navigation.state.key, + toChildKey = transitionDestKey, + })) + end + + -- Only propagate transition changes to caller for transitions where the actual + -- index has changed. Transitioner sends updates for _all_ transitions, including + -- those to the same screen that result from animation completion events. + if onTransitionEnd and transition.index ~= lastTransition.index then + onTransitionEnd(transition.navigation, lastTransition.navigation) + end +end + +function StackView:_onTransitionStep(transition, lastTransition, value) + local onTransitionStep = self.props.onTransitionStep + or self.props.navigationConfig.onTransitionStep + + -- Only propagate transition changes to caller for transitions where the actual + -- index has changed. Transitioner sends updates for _all_ transitions, including + -- those to the same screen that result from animation completion events. + if onTransitionStep and transition.index ~= lastTransition.index then + onTransitionStep(transition.navigation, lastTransition.navigation, value) + end +end + +return StackView diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackViewCard.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackViewCard.lua new file mode 100644 index 0000000..b7b2046 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackViewCard.lua @@ -0,0 +1,116 @@ +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local validate = require(script.Parent.Parent.Parent.utils.validate) + +--[[ + Render a scene as a card for use in a StackView. This component is + responsible for correctly positioning the scene content in relation + to the other scenes. The content will be rendered inside a Frame + whose position and size are controlled by the transition logic. The + frame may either be transparent or a solid color, depending upon props. + Any additional visual effects must be supplied by the container or the + child element created by renderScene(). + + Props: + renderScene(scene) -- Render prop to draw the scene inside the card. + initialPosition -- Starting position for the card. (Animated by Otter from there). + positionStep -- Stepper function from StackViewInterpolator. + position -- Otter motor for the position of the card. + scene -- Scene that the card is to render. + forceHidden -- Forcibly disable card rendering (e.g. animated off-screen). + transparent -- Card allows underlying content to show through (default: false). + cardColor3 -- Color of the card background if it's not transparent (default: white). +]] +local StackViewCard = Roact.Component:extend("StackViewCard") + +StackViewCard.defaultProps = { + transparent = false, + cardColor3 = Color3.new(1, 1, 1), +} + +function StackViewCard:init() + local currentNavIndex = self.props.navigation.state.index + + self._isMounted = false + self._positionLastValue = currentNavIndex + + local selfRef = Roact.createRef() + self._getRef = function() + return self.props[Roact.Ref] or selfRef + end +end + +function StackViewCard:render() + local forceHidden = self.props.forceHidden + local cardColor3 = self.props.cardColor3 + local transparent = self.props.transparent + local initialPosition = self.props.initialPosition + local renderScene = self.props.renderScene + local scene = self.props.scene + + validate(type(renderScene) == "function", "renderScene must be a function") + + return Roact.createElement("Frame", { + Position = initialPosition, + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = cardColor3, + BackgroundTransparency = transparent and 1 or nil, + BorderSizePixel = 0, + ClipsDescendants = true, + Visible = not forceHidden, + [Roact.Ref] = self:_getRef(), + }, { + Content = renderScene(scene), + }) +end + +function StackViewCard:didMount() + self._isMounted = true + + local position = self.props.position + self._positionDisconnector = position:onStep(function(...) + self:_onPositionStep(...) + end) +end + +function StackViewCard:willUnmount() + self._isMounted = false + + if self._positionDisconnector then + self._positionDisconnector() + self._positionDisconnector = nil + end +end + +function StackViewCard:didUpdate(oldProps) + local position = self.props.position + local positionStep = self.props.positionStep + + if position ~= oldProps.position then + self._positionDisconnector() + self._positionDisconnector = position:onStep(function(...) + self:_onPositionStep(...) + end) + end + + if positionStep ~= oldProps.positionStep then + -- The motor won't fire just because stepper function has changed. We have to + -- update the position to match new requirements based upon last motor value. + self:_onPositionStep(self._positionLastValue) + end +end + +function StackViewCard:_onPositionStep(value) + if not self._isMounted then + return + end + + local positionStep = self.props.positionStep + + if positionStep then + positionStep(self:_getRef(), value) + end + + self._positionLastValue = value +end + +return StackViewCard diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackViewInterpolator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackViewInterpolator.lua new file mode 100644 index 0000000..0ca0a09 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackViewInterpolator.lua @@ -0,0 +1,216 @@ +--[[ + Provides builders to create functions that interpolate the current Otter motor + position into the correct translation for stack cards based upon their associated + scene. + + Interpolator builders expect the following props as input: + { + initialPositionValue = , + scene = , + layout = { + initWidth = , + initHeight = , + isMeasured = , + } + } + + Each builder returns a props table to be merged onto your other StackViewCard props, ex: + { + positionStep = , + initialPosition = , + forceHidden = true, -- May disable card visibility if it's outside interpolating range. + } + + The props table may contain other changes, depending on the requirements of the animation. +]] +local getSceneIndicesForInterpolationInputRange = require( + script.Parent.Parent.Parent.utils.getSceneIndicesForInterpolationInputRange) +local lerp = require(script.Parent.Parent.Parent.utils.lerp) + +-- Render initial style when layout hasn't been measured yet. +local function forInitial(props) + local initialPositionValue = props.initialPositionValue + local scene = props.scene + + local forceHidden = initialPositionValue ~= scene.index + local translate = forceHidden and 1000000 or 0 + + return { + forceHidden = forceHidden, + initialPosition = UDim2.new(0, translate, 0, translate), + positionStep = nil, + } +end + +-- Slide-in from right style (e.g. navigation stack view). +local function forHorizontal(props) + local initialPositionValue = props.initialPositionValue + local layout = props.layout + local scene = props.scene + + if not layout.isMeasured then + return forInitial(props) + end + + local interpolate = getSceneIndicesForInterpolationInputRange(props) + + -- getSceneIndices* returns nil if card is not visible and need not be + -- considered for the animation until state changes. + if not interpolate then + return { + forceHidden = true, + initialPosition = UDim2.new(0, 100000, 0, 100000), + positionStep = nil, + } + end + + local first = interpolate.first + local last = interpolate.last + local index = scene.index + + local width = layout.initWidth + + local function calculate(positionValue) + -- 3 range LERP + if positionValue < first then + return width + elseif positionValue < index then + return lerp(width, 0, (positionValue - first) / (index - first)) + elseif positionValue == index then + return 0 + elseif positionValue < last then + return lerp(0, -width, (positionValue - index) / (last - index)) + else + return -width + end + end + + local function stepper(cardRef, positionValue) + local cardInstance = cardRef.current + if not cardInstance then + return + end + + local oldPosition = cardInstance.Position + cardInstance.Position = UDim2.new( + oldPosition.X.Scale, + calculate(positionValue), + oldPosition.Y.Scale, + oldPosition.Y.Offset + ) + end + + local initialPosition = UDim2.new(0, calculate(initialPositionValue), 0, 0) + + return { + initialPosition = initialPosition, + positionStep = stepper, + } +end + +-- Slide-in from bottom style (e.g. modals). +local function forVertical(props) + local initialPositionValue = props.initialPositionValue + local layout = props.layout + local scene = props.scene + + if not layout.isMeasured then + return forInitial(props) + end + + local interpolate = getSceneIndicesForInterpolationInputRange(props) + + if not interpolate then + return { + forceHidden = true, + initialPosition = UDim2.new(0, 100000, 0, 100000), + positionStep = nil, + } + end + + local first = interpolate.first + local index = scene.index + local height = layout.initHeight + + local function calculate(positionValue) + -- 2 range LERP + if positionValue < first then + return height + elseif positionValue < index then + return lerp(height, 0, (positionValue - first) / (index - first)) + else + return 0 + end + end + + local function stepper(cardRef, positionValue) + local cardInstance = cardRef.current + if not cardInstance then + return + end + + local oldPosition = cardInstance.Position + cardInstance.Position = UDim2.new( + oldPosition.X.Scale, + oldPosition.X.Offset, + oldPosition.Y.Scale, + calculate(positionValue) + ) + end + + local initialPosition = UDim2.new(0, 0, 0, calculate(initialPositionValue)) + + return { + initialPosition = initialPosition, + positionStep = stepper, + } +end + +-- Fade in place animation (e.g. popovers and toasts). Note that since we don't currently have +-- group transparency, this 'animation' just pops the views in for now. +local function forFade(props) + local initialPositionValue = props.initialPositionValue + local layout = props.layout + local scene = props.scene + + if not layout.isMeasured then + return forInitial(props) + end + + local interpolate = getSceneIndicesForInterpolationInputRange(props) + + if not interpolate then + return { + forceHidden = true, + initialPosition = UDim2.new(0, 100000, 0, 100000), + positionStep = nil, + } + end + + local index = scene.index + + local function calculate(positionValue) + return positionValue >= index - 0.5 + end + + local function stepper(cardRef, positionValue) + local cardInstance = cardRef.current + if not cardInstance then + return + end + + cardInstance.Visible = calculate(positionValue) + end + + return { + forceHidden = not calculate(initialPositionValue), + initialPosition = UDim2.new(0, 0, 0, 0), + positionStep = stepper, + } +end + +return { + forHorizontal = forHorizontal, + forVertical = forVertical, + forFade = forFade, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackViewLayout.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackViewLayout.lua new file mode 100644 index 0000000..342e38e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackViewLayout.lua @@ -0,0 +1,265 @@ +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local StackPresentationStyle = require(script.Parent.StackPresentationStyle) +local StackViewTransitionConfigs = require(script.Parent.StackViewTransitionConfigs) +local StackViewOverlayFrame = require(script.Parent.StackViewOverlayFrame) +local StackViewCard = require(script.Parent.StackViewCard) +local SceneView = require(script.Parent.Parent.SceneView) + +local defaultScreenOptions = { + absorbInput = true, + overlayEnabled = false, + overlayColor3 = Color3.new(0, 0, 0), + overlayTransparency = 0.7, + -- cardColor3 default is provided by StackViewCard + renderOverlay = function(navigationOptions, initialTransitionValue, transitionChangedSignal) + -- NOTE: renderOverlay will not be called if sceneOptions.overlayEnabled evaluates false + return Roact.createElement(StackViewOverlayFrame, { + navigationOptions = navigationOptions, + initialTransitionValue = initialTransitionValue, + transitionChangedSignal = transitionChangedSignal, + }) + end, +} + +local function calculateTransitionValue(index, position) + return math.max(math.min(1 + position - index, 1), 0) +end + + +local StackViewLayout = Roact.Component:extend("StackViewLayout") + +function StackViewLayout:init() + local startingIndex = self.props.transitionProps.navigation.state.index + + self._isMounted = false + self._positionLastValue = startingIndex + + self._renderScene = function(scene) + return self:_renderInnerScene(scene) + end + + self._subscribeToOverlayUpdates = function(callback) + local position = self.props.transitionProps.position + local index = self.props.transitionProps.scene.index + + return position:onStep(function(value) + callback(calculateTransitionValue(index, value)) + end) + end +end + +function StackViewLayout:_renderCard(scene, navigationOptions) + local transitionProps = self.props.transitionProps -- Core animation info from Transitioner. + local lastTransitionProps = self.props.lastTransitionProps -- Previous transition info. + local transitionConfig = self.state.transitionConfig -- State based info from scene config. + + local cardColor3 = navigationOptions.cardColor3 + local overlayEnabled = navigationOptions.overlayEnabled + + local initialPositionValue = transitionProps.scene.index + if lastTransitionProps then + initialPositionValue = lastTransitionProps.scene.index + end + + local cardInterpolationProps = {} + local screenInterpolator = transitionConfig.screenInterpolator + if screenInterpolator then + cardInterpolationProps = screenInterpolator( + Cryo.Dictionary.join(transitionProps, { + initialPositionValue = initialPositionValue, + scene = scene, + }) + ) + end + + -- Merge down the various prop packages to be applied to StackViewCard. + return Roact.createElement(StackViewCard, Cryo.Dictionary.join( + transitionProps, cardInterpolationProps, { + key = "card_" .. tostring(scene.key), + scene = scene, + renderScene = self._renderScene, + transparent = overlayEnabled, + cardColor3 = cardColor3, + }) + ) +end + +function StackViewLayout:_renderInnerScene(scene) + local navigation = scene.descriptor.navigation + + local sceneComponent = scene.descriptor.getComponent() + local screenProps = self.props.screenProps + + return Roact.createElement(SceneView, { + screenProps = screenProps, + navigation = navigation, + component = sceneComponent, + }) +end + +function StackViewLayout:render() + local transitionProps = self.props.transitionProps + local topMostOpaqueSceneIndex = self.state.topMostOpaqueSceneIndex + local scenes = transitionProps.scenes + + local renderedScenes = Cryo.List.map(scenes, function(scene) + -- The card is obscured if: + -- It's not the active card (e.g. we're transitioning TO it). + -- It's hidden underneath an opaque card that is NOT currently transitioning. + -- It's completely off-screen. + local cardObscured = scene.index < topMostOpaqueSceneIndex and not scene.isActive + + local screenOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {}) + local overlayEnabled = screenOptions.overlayEnabled + local absorbInput = screenOptions.absorbInput + local renderOverlay = screenOptions.renderOverlay + + local stationaryContent = nil + if overlayEnabled then + stationaryContent = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + ZIndex = 1, + }, { + Overlay = renderOverlay( + screenOptions, + calculateTransitionValue(scene.index, self._positionLastValue), + self._subscribeToOverlayUpdates) + }) + end + + -- Wrapper frame holds default/custom card background and the card content. + -- It MUST be a Frame when absorbInput=false because of legacy behavior on desktop + -- for GuiObject.Active=false blocking mouse clicks from falling through. + -- (Active=false DOES work on mobile, but not desktop). + -- When absorbInput=true, we add a TextButton behind the frame that will catch + -- mouse clicks + local absorbInputElement = nil + if not cardObscured and absorbInput then + absorbInputElement = Roact.createElement("TextButton", { + Active = true, + AutoButtonColor = false, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = true, + Size = UDim2.new(1, 0, 1, 0), + Text = " ", + ZIndex = 2 * scene.index - 1, + }) + end + + return Roact.createFragment({ + AbsorbInput = absorbInputElement, + -- use scene index for key, it makes testing with Rhodium easier + [tostring(scene.index)] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = true, + ZIndex = 2 * scene.index, + Visible = not cardObscured, + }, { + StationaryContent = stationaryContent, + DynamicContent = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + ZIndex = 2, + }, { + -- Cards need to have unique keys so that instances of the same components are not + -- reused for different scenes. (Could lead to unanticipated lifecycle problems). + ["card_" .. scene.key] = self:_renderCard(scene, screenOptions), + }) + }), + }) + end) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + }, renderedScenes) +end + +function StackViewLayout.getDerivedStateFromProps(nextProps, lastState) + local transitionProps = nextProps.transitionProps + local scenes = transitionProps.scenes + local state = transitionProps.navigation.state + local isTransitioning = state.isTransitioning + local topMostIndex = #scenes + + local isOverlayMode = nextProps.mode == StackPresentationStyle.Modal or + nextProps.mode == StackPresentationStyle.Overlay + + -- Find the last opaque scene in a modal stack so that we can optimize rendering. + local topMostOpaqueSceneIndex = 0 + if isOverlayMode then + for idx = topMostIndex, 1, -1 do + local scene = scenes[idx] + local navigationOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {}) + + -- Card covers other pages if it's not an overlay and it's not the top-most index while transitioning. + if not navigationOptions.overlayEnabled and not (isTransitioning and idx == topMostIndex) then + topMostOpaqueSceneIndex = idx + break + end + end + else + for idx = topMostIndex, 1, -1 do + if not (isTransitioning and idx == topMostIndex) then + topMostOpaqueSceneIndex = idx + break + end + end + end + + return { + topMostOpaqueSceneIndex = topMostOpaqueSceneIndex, + transitionConfig = StackViewTransitionConfigs.getTransitionConfig( + nextProps.transitionConfig, + nextProps.transitionProps, + nextProps.lastTransitionProps, + nextProps.mode), + } +end + +function StackViewLayout:didMount() + self._isMounted = true + + self._positionDisconnector = self.props.transitionProps.position:onStep(function(...) + self:_onPositionStep(...) + end) +end + +function StackViewLayout:willUnmount() + self._isMounted = false + + if self._positionDisconnector then + self._positionDisconnector() + self._positionDisconnector = nil + end +end + +function StackViewLayout:didUpdate(oldProps) + local position = self.props.transitionProps.position + + if position ~= oldProps.transitionProps.position then + self._positionDisconnector() + self._positionDisconnector = position:onStep(function(...) + self:_onPositionStep(...) + end) + end +end + +function StackViewLayout:_onPositionStep(value) + if self._isMounted then + self._positionLastValue = value + end +end + +return StackViewLayout diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackViewOverlayFrame.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackViewOverlayFrame.lua new file mode 100644 index 0000000..6d293e3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackViewOverlayFrame.lua @@ -0,0 +1,71 @@ +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local lerp = require(script.Parent.Parent.Parent.utils.lerp) + +local StackViewOverlayFrame = Roact.Component:extend("StackViewOverlayFrame") + +function StackViewOverlayFrame:init() + self._signalDisconnect = nil + + local selfRef = Roact.createRef() + self._getRef = function() + return self.props[Roact.Ref] or selfRef + end +end + +function StackViewOverlayFrame:render() + local navigationOptions = self.props.navigationOptions + local initialTransitionValue = self.props.initialTransitionValue + + local overlayTransparency = lerp(1, navigationOptions.overlayTransparency, initialTransitionValue) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = navigationOptions.overlayColor3, + BackgroundTransparency = overlayTransparency, + BorderSizePixel = 0, + [Roact.Ref] = self:_getRef(), + }) +end + +function StackViewOverlayFrame:didUpdate(oldProps) + local transitionChangedSignal = self.props.transitionChangedSignal + + if transitionChangedSignal ~= oldProps.transitionChangedSignal then + if self._signalDisconnect then + self._signalDisconnect() + end + + self._signalDisconnect = transitionChangedSignal(function(...) + self:_transitionChanged(...) + end) + end +end + +function StackViewOverlayFrame:didMount() + self._isMounted = true + self._signalDisconnect = self.props.transitionChangedSignal(function(...) + self:_transitionChanged(...) + end) +end + +function StackViewOverlayFrame:willUnmount() + self._isMounted = false + if self._signalDisconnect then + self._signalDisconnect() + end +end + +function StackViewOverlayFrame:_transitionChanged(value) + if not self._isMounted then + return + end + + local myRef = self:_getRef() + if myRef.current then + local navigationOptions = self.props.navigationOptions + local overlayTransparency = lerp(1, navigationOptions.overlayTransparency, value) + myRef.current.BackgroundTransparency = overlayTransparency + end +end + +return StackViewOverlayFrame diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackViewTransitionConfigs.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackViewTransitionConfigs.lua new file mode 100644 index 0000000..83e7a33 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/StackViewTransitionConfigs.lua @@ -0,0 +1,53 @@ +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) +local StackViewInterpolator = require(script.Parent.StackViewInterpolator) +local StackPresentationStyle = require(script.Parent.StackPresentationStyle) + +local DefaultTransitionSpec = { + frequency = 3, -- Hz + dampingRatio = 1, +} + +local SlideFromRight = { + transitionSpec = DefaultTransitionSpec, + screenInterpolator = StackViewInterpolator.forHorizontal, +} + +local ModalSlideFromBottom = { + transitionSpec = DefaultTransitionSpec, + screenInterpolator = StackViewInterpolator.forVertical, +} + +local FadeInPlace = { + transitionSpec = DefaultTransitionSpec, + screenInterpolator = StackViewInterpolator.forFade, +} + +local function getDefaultTransitionConfig(transitionProps, prevTransitionProps, presentationStyle) + if presentationStyle == StackPresentationStyle.Modal then + return ModalSlideFromBottom + elseif presentationStyle == StackPresentationStyle.Overlay then + return FadeInPlace + else + return SlideFromRight + end +end + +local function getTransitionConfig(transitionConfigurer, transitionProps, prevTransitionProps, presentationStyle) + local defaultConfig = getDefaultTransitionConfig(transitionProps, prevTransitionProps, presentationStyle) + if transitionConfigurer then + return Cryo.Dictionary.join( + defaultConfig, + transitionConfigurer(transitionProps, prevTransitionProps, presentationStyle) + ) + end + + return defaultConfig +end + +return { + getDefaultTransitionConfig = getDefaultTransitionConfig, + getTransitionConfig = getTransitionConfig, + SlideFromRight = SlideFromRight, + ModalSlideFromBottom = ModalSlideFromBottom, + FadeInPlace = FadeInPlace, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackPresentationStyle.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackPresentationStyle.spec.lua new file mode 100644 index 0000000..0a64c09 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackPresentationStyle.spec.lua @@ -0,0 +1,17 @@ +return function() + local StackPresentationStyle = require(script.Parent.Parent.StackPresentationStyle) + + describe("StackPresentationStyle token tests", function() + it("should return same object for each token for multiple calls", function() + expect(StackPresentationStyle.Default).to.equal(StackPresentationStyle.Default) + expect(StackPresentationStyle.Modal).to.equal(StackPresentationStyle.Modal) + expect(StackPresentationStyle.Overlay).to.equal(StackPresentationStyle.Overlay) + end) + + it("should return matching string names for symbols", function() + expect(tostring(StackPresentationStyle.Default)).to.equal("DEFAULT") + expect(tostring(StackPresentationStyle.Modal)).to.equal("MODAL") + expect(tostring(StackPresentationStyle.Overlay)).to.equal("OVERLAY") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackView.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackView.spec.lua new file mode 100644 index 0000000..cd1a24e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackView.spec.lua @@ -0,0 +1,8 @@ +return function() + -- local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + -- local StackView = require(script.Parent.Parent.StackView) + + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackViewCard.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackViewCard.spec.lua new file mode 100644 index 0000000..1b3e68f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackViewCard.spec.lua @@ -0,0 +1,36 @@ +return function() + local Otter = require(script.Parent.Parent.Parent.Parent.Parent.Otter) + local Roact = require(script.Parent.Parent.Parent.Parent.Parent.Roact) + local StackViewCard = require(script.Parent.Parent.StackViewCard) + + it("should mount its renderProp and pass it scene", function() + local didRender = false + local testScene = { + isActive = true, + index = 1, + } + + local renderedScene = nil + local element = Roact.createElement(StackViewCard, { + renderScene = function(theScene) + renderedScene = theScene + return Roact.createElement(function() + didRender = true -- verifies component is attached to tree + end) + end, + scene = testScene, + position = Otter.createSingleMotor(1), + navigation = { + state = { + index = 1, + } + } + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(renderedScene).to.equal(testScene) + expect(didRender).to.equal(true) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackViewInterpolator.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackViewInterpolator.spec.lua new file mode 100644 index 0000000..b3092a6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackViewInterpolator.spec.lua @@ -0,0 +1,7 @@ +return function() + local StackViewInterpolator = require(script.Parent.Parent.StackViewInterpolator) + + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackViewLayout.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackViewLayout.spec.lua new file mode 100644 index 0000000..9882e6d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackViewLayout.spec.lua @@ -0,0 +1,8 @@ +return function() + -- local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + -- local StackViewLayout = require(script.Parent.Parent.StackViewLayout) + + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackViewTransitionConfigs.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackViewTransitionConfigs.spec.lua new file mode 100644 index 0000000..46e4e1e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/StackView/_tests_/StackViewTransitionConfigs.spec.lua @@ -0,0 +1,5 @@ +return function() + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/SwitchView.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/SwitchView.lua new file mode 100644 index 0000000..29b443a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/SwitchView.lua @@ -0,0 +1,68 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Roact) +local SceneView = require(script.Parent.SceneView) + +local defaultNavigationConfig = { + keepVisitedScreensMounted = false, +} + +local SwitchView = Roact.Component:extend("SwitchView") + +function SwitchView.getDerivedStateFromProps(nextProps, prevState) + local navState = nextProps.navigation.state + local activeKey = navState.routes[navState.index].key + local descriptors = nextProps.descriptors + + local navigationConfig = Cryo.Dictionary.join(defaultNavigationConfig, nextProps.navigationConfig or {}) + local keepVisitedScreensMounted = navigationConfig.keepVisitedScreensMounted + + local visitedScreenKeys = { + [activeKey] = true + } + + if keepVisitedScreensMounted then + -- prune visited screen keys if they are not included in incoming descriptors + for prevKey in pairs(prevState.visitedScreenKeys or {}) do + if descriptors[prevKey] ~= nil then + visitedScreenKeys[prevKey] = true + end + end + end + + return { + visitedScreenKeys = visitedScreenKeys, + } +end + +function SwitchView:render() + local navState = self.props.navigation.state + local screenProps = self.props.screenProps + local descriptors = self.props.descriptors + local visitedScreenKeys = self.state.visitedScreenKeys + local activeKey = navState.routes[navState.index].key + + local screenElements = {} + for key, descriptor in pairs(descriptors) do + local isActiveKey = (key == activeKey) + + if visitedScreenKeys[key] == true then + screenElements["card_" .. key] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + Visible = isActiveKey, + }, { + Content = Roact.createElement(SceneView, { + component = descriptor.getComponent(), + navigation = descriptor.navigation, + screenProps = screenProps, + }) + }) + end + end + + return Roact.createElement("Folder", nil, screenElements) +end + +return SwitchView diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/Transitioner.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/Transitioner.lua new file mode 100644 index 0000000..bf6c9cc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/Transitioner.lua @@ -0,0 +1,310 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Roact) +local Otter = require(script.Parent.Parent.Parent.Otter) +local ScenesReducer = require(script.Parent.ScenesReducer) +local validate = require(script.Parent.Parent.utils.validate) + +local DEFAULT_TRANSITION_SPEC = { + frequency = 4 -- Hz +} + +local function buildTransitionProps(props, state) + local navigation = props.navigation + local options = props.options + + local layout = state.layout + local position = state.position + local scenes = state.scenes + + local activeScene + for _, x in ipairs(scenes) do + if x.isActive then + activeScene = x + break + end + end + + validate(activeScene, "Could not find active scene") + + return { + layout = layout, + navigation = navigation, + position = position, + scenes = scenes, + scene = activeScene, + options = options, + index = activeScene.index, + } +end + +local function filterStale(scenes) + local filtered = Cryo.List.filter(scenes, function(scene) + return not scene.isStale + end) + + if #filtered == #scenes then + return scenes + else + return filtered + end +end + +local Transitioner = Roact.Component:extend("Transitioner") + +function Transitioner:init() + local navigationState = self.props.navigation.state + local descriptors = self.props.descriptors + + self.state = { + -- Layout is passed to StackViewLayout in order to allow it to + -- sync animations. + layout = { + initWidth = 0, + initHeight = 0, + isMeasured = false, + }, + position = Otter.createSingleMotor(navigationState.index), + scenes = ScenesReducer({}, navigationState, nil, descriptors), + } + + self._doOnAbsoluteSizeChanged = function(...) + return self:_onAbsoluteSizeChanged(...) + end + + self._positionLastValue = navigationState.index + + self._prevTransitionProps = nil + self._transitionProps = buildTransitionProps(self.props, self.state) + + self._isMounted = false + self._isTransitionRunning = false + self._transitionQueue = {} + + self._completeSignalDisconnector = self.state.position:onComplete(function() + -- This spawn is required because of this Otter bug: https://github.com/Roblox/otter/issues/26 + -- Otter.SingleMotor's step function calls onComplete before it calls stop(). This leaves their + -- __running=true, and the setGoal() in our _onTransitionEnd() does nothing. So the whole queue + -- handling just stops cold, and the transition never actually happens! + spawn(function() + if self._isMounted then + self:_onTransitionEnd() + end + end) + end) + + self._stepSignalDisconnector = self.state.position:onStep(function(value) + if self._isMounted then + self:_onPositionStep(value) + end + end) +end + +function Transitioner:didMount() + self._isMounted = true +end + +function Transitioner:willUnmount() + self._isMounted = false + + if self._completeSignalDisconnector then + self._completeSignalDisconnector() + self._completeSignalDisconnector = nil + end + + if self._stepSignalDisconnector then + self._stepSignalDisconnector() + self._stepSignalDisconnector = nil + end +end + +function Transitioner:didUpdate(prevProps) + -- React-navigation uses componentWillReceiveProps that is only called when Parent + -- re-renders or when this component is actually being given new props, so we need to + -- filter here. If not, this would trigger on setState and enter an infinite loop. + if self.props ~= prevProps then + if self._isTransitionRunning then + local mostRecentTransition = self._transitionQueue[#self._transitionQueue] or {} + -- don't enqueue spurious extra copies of same transition props + if mostRecentTransition.prevProps ~= prevProps then + table.insert(self._transitionQueue, { prevProps = prevProps }) + end + + return + end + + self:_startTransition(prevProps, self.props) + end +end + +function Transitioner:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = true, + [Roact.Change.AbsoluteSize] = self._doOnAbsoluteSizeChanged, + }, { + ["TransitionerScenes"] = self.props.render( + self._transitionProps, self._prevTransitionProps), + }) +end + +-- equivalent to React-Nav's Transitioner._onLayout +function Transitioner:_onAbsoluteSizeChanged(rbx) + local width = rbx.AbsoluteSize.X + local height = rbx.AbsoluteSize.Y + + if width == self.state.layout.initWidth and + height == self.state.layout.initHeight then + return + end + + local layout = Cryo.Dictionary.join(self.state.layout, { + initWidth = width, + initHeight = height, + isMeasured = true, + }) + + local nextState = Cryo.Dictionary.join(self.state, { + layout = layout, + }) + + self._transitionProps = buildTransitionProps(self.props, nextState) + + self:setState({ + layout = layout, + }) +end + +function Transitioner:_computeScenes(props, nextProps) + local nextScenes = ScenesReducer( + self.state.scenes, + nextProps.navigation.state, + props.navigation.state, + nextProps.descriptors) + + if not nextProps.navigation.state.isTransitioning then + nextScenes = filterStale(nextScenes) + end + + if nextScenes == self.state.scenes then + return nil + end + + return nextScenes +end + +function Transitioner:_startTransition(props, nextProps) + local indexHasChanged = props.navigation.state.index ~= nextProps.navigation.state.index + local nextScenes = self:_computeScenes(props, nextProps) + + if not nextScenes then + -- If nextScenes is nil, nothing has changed, so report transition end, then bail + self._prevTransitionProps = self._transitionProps + + -- Trigger end transition logic if we daisy-chained from queued transitions via _onTransitionEnd. + if self._isTransitionRunning then + self:_onTransitionEnd() + end + + return + end + + local nextState = Cryo.Dictionary.join(self.state, { + scenes = nextScenes, + }) + + local position = nextState.position + local toValue = nextProps.navigation.state.index + + -- compute transitionProps + self._prevTransitionProps = self._transitionProps + self._transitionProps = buildTransitionProps(nextProps, nextState) + local isTransitioning = self._transitionProps.navigation.state.isTransitioning + + if not isTransitioning or not indexHasChanged then + -- If state is not transitioning, then we go immediately to new index. + -- Likewise, if the index has not changed then we still need to set up initial + -- positions via setState. + self:setState(nextState) + + if nextProps.onTransitionStart then + nextProps.onTransitionStart(self._transitionProps, self._prevTransitionProps) + end + + -- motor will call _endTransition for us + position:setGoal(Otter.instant(toValue)) + elseif isTransitioning then + self._isTransitionRunning = true + self:setState(nextState) + + if nextProps.onTransitionStart then + nextProps.onTransitionStart(self._transitionProps, self._prevTransitionProps) + end + + local positionHasChanged = self._positionLastValue ~= toValue + if indexHasChanged and positionHasChanged then + -- get transition spec + local transitionUserSpec = {} + if nextProps.configureTransition then + transitionUserSpec = nextProps.configureTransition( + self._transitionProps, self._prevTransitionProps) or {} + end + + local transitionSpec = Cryo.Dictionary.join(DEFAULT_TRANSITION_SPEC, transitionUserSpec) + + -- motor will call _endTransition for us + position:setGoal(Otter.spring(nextProps.navigation.state.index, transitionSpec)) + else + -- Set motor to current state to trigger _endTransition call with correct sequencing. + position:setGoal(Otter.instant(nextProps.navigation.state.index)) + end + end +end + +function Transitioner:_onTransitionEnd() + local prevTransitionProps = self._prevTransitionProps + self._prevTransitionProps = nil + + local scenes = filterStale(self.state.scenes) + + local nextState = Cryo.Dictionary.join(self.state, { + scenes = scenes, + }) + + self._transitionProps = buildTransitionProps(self.props, nextState) + + self:setState(nextState) + + if self.props.onTransitionEnd then + self.props.onTransitionEnd(self._transitionProps, prevTransitionProps) + end + + local firstQueuedTransition = self._transitionQueue[1] + if firstQueuedTransition then + local prevProps = firstQueuedTransition.prevProps + self._transitionQueue = Cryo.List.removeIndex(self._transitionQueue, 1) + self:_startTransition(prevProps, self.props) + else + self._isTransitionRunning = false + end +end + +function Transitioner:_onPositionStep(value) + self._positionLastValue = value + + local targetIndex = self._transitionProps.index + + -- _prevTransitionProps can be nil, so guard against it. + local startingIndex = targetIndex + if self._prevTransitionProps then + startingIndex = self._prevTransitionProps.index + end + + if self.props.onTransitionStep and startingIndex ~= targetIndex then + local transitionValue = (value - startingIndex) / (targetIndex - startingIndex) + self.props.onTransitionStep(self._transitionProps, self._prevTransitionProps, transitionValue) + end +end + +return Transitioner diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/AppNavigationContext.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/AppNavigationContext.spec.lua new file mode 100644 index 0000000..814b205 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/AppNavigationContext.spec.lua @@ -0,0 +1,91 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local AppNavigationContext = require(script.Parent.Parent.AppNavigationContext) + + it("should propagate navigation prop from provider to consumer", function() + local testNavigationContextProp = {} + local testComponentNavContext = nil + + local provider = Roact.createElement(AppNavigationContext.Provider, { + navigation = testNavigationContextProp + }, { + Child = Roact.createElement(AppNavigationContext.Consumer, { + render = function(navigation) + testComponentNavContext = navigation + end + }) + }) + + local instance = Roact.mount(provider) + expect(testComponentNavContext).to.equal(testNavigationContextProp) + Roact.unmount(instance) + end) + + it("should override context navigation prop if custom prop is set on consumer", function() + local testCustomNavigationProp = {} + local testComponentNavContext = nil + + local provider = Roact.createElement(AppNavigationContext.Provider, { + navigation = {} + }, { + Child = Roact.createElement(AppNavigationContext.Consumer, { + navigation = testCustomNavigationProp, + render = function(navigation) + testComponentNavContext = navigation + end + }) + }) + + local instance = Roact.mount(provider) + expect(testComponentNavContext).to.equal(testCustomNavigationProp) + Roact.unmount(instance) + end) + + it("should pass navigation prop to statically wrapped components", function() + local testNavigationContextProp = {} + local passedNavigationProp = nil + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + passedNavigationProp = self.props.navigation + end + + local WrappedComponent = AppNavigationContext.connect(TestComponent) + + local element = Roact.createElement(AppNavigationContext.Provider, { + navigation = testNavigationContextProp + }, { + Child = Roact.createElement(WrappedComponent) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(passedNavigationProp).to.equal(testNavigationContextProp) + end) + + it("should override static wrapper navigation prop when navigation is directly set", function() + local testNavigationProp = {} + local passedNavigationProp = nil + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + passedNavigationProp = self.props.navigation + end + + local WrappedComponent = AppNavigationContext.connect(TestComponent) + + local element = Roact.createElement(AppNavigationContext.Provider, { + navigation = {} + }, { + Child = Roact.createElement(WrappedComponent, { + navigation = testNavigationProp + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(passedNavigationProp).to.equal(testNavigationProp) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/NavigationEventsAdapter.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/NavigationEventsAdapter.spec.lua new file mode 100644 index 0000000..75a3dbd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/NavigationEventsAdapter.spec.lua @@ -0,0 +1,61 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local NavigationEvents = require(script.Parent.Parent.Parent.NavigationEvents) + local NavigationEventsAdapter = require(script.Parent.Parent.NavigationEventsAdapter) + + it("should subscribe to events for each registered handler", function() + local mockNavContext = { + subscribedHandlers = {}, + calledHandlers = {}, + } + + function mockNavContext:makeHandler(symbol) + return function() + self.calledHandlers[symbol] = true + end + end + + function mockNavContext.addListener(symbol, callback) + mockNavContext.subscribedHandlers[symbol] = callback + return { + disconnect = function() + mockNavContext.subscribedHandlers[symbol] = nil + end + } + end + + local adapter = Roact.createElement(NavigationEventsAdapter, { + navigation = mockNavContext, + [NavigationEvents.WillFocus] = mockNavContext:makeHandler(NavigationEvents.WillFocus), + [NavigationEvents.DidFocus] = mockNavContext:makeHandler(NavigationEvents.DidFocus), + [NavigationEvents.WillBlur] = mockNavContext:makeHandler(NavigationEvents.WillBlur), + [NavigationEvents.DidBlur] = mockNavContext:makeHandler(NavigationEvents.DidBlur), + }) + + local instance = Roact.mount(adapter) + + expect(type(mockNavContext.subscribedHandlers[NavigationEvents.WillFocus])).to.equal("function") + expect(type(mockNavContext.subscribedHandlers[NavigationEvents.DidFocus])).to.equal("function") + expect(type(mockNavContext.subscribedHandlers[NavigationEvents.WillBlur])).to.equal("function") + expect(type(mockNavContext.subscribedHandlers[NavigationEvents.DidBlur])).to.equal("function") + + mockNavContext.subscribedHandlers[NavigationEvents.WillFocus]() + expect(mockNavContext.calledHandlers[NavigationEvents.WillFocus]).to.equal(true) + + mockNavContext.subscribedHandlers[NavigationEvents.DidFocus]() + expect(mockNavContext.calledHandlers[NavigationEvents.DidFocus]).to.equal(true) + + mockNavContext.subscribedHandlers[NavigationEvents.WillBlur]() + expect(mockNavContext.calledHandlers[NavigationEvents.WillBlur]).to.equal(true) + + mockNavContext.subscribedHandlers[NavigationEvents.DidBlur]() + expect(mockNavContext.calledHandlers[NavigationEvents.DidBlur]).to.equal(true) + + Roact.unmount(instance) + + expect(mockNavContext.subscribedHandlers[NavigationEvents.WillFocus]).to.equal(nil) + expect(mockNavContext.subscribedHandlers[NavigationEvents.DidFocus]).to.equal(nil) + expect(mockNavContext.subscribedHandlers[NavigationEvents.WillBlur]).to.equal(nil) + expect(mockNavContext.subscribedHandlers[NavigationEvents.DidBlur]).to.equal(nil) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/SceneView.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/SceneView.spec.lua new file mode 100644 index 0000000..8cd3941 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/SceneView.spec.lua @@ -0,0 +1,36 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local SceneView = require(script.Parent.Parent.SceneView) + local withNavigation = require(script.Parent.Parent.withNavigation) + + it("should mount inner component and pass down required props+context.navigation", function() + local testComponentNavigationFromProp = nil + local testComponentScreenProps = nil + local testComponentNavigationFromContext = nil + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + testComponentNavigationFromProp = self.props.navigation + testComponentScreenProps = self.props.screenProps + + return withNavigation(function(navigation) + testComponentNavigationFromContext = navigation + end) + end + + local testScreenProps = {} + local testNav = {} + local element = Roact.createElement(SceneView, { + screenProps = testScreenProps, + navigation = testNav, + component = TestComponent, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(testComponentScreenProps).to.equal(testScreenProps) + expect(testComponentNavigationFromProp).to.equal(testNav) + expect(testComponentNavigationFromContext).to.equal(testNav) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/ScenesReducer.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/ScenesReducer.spec.lua new file mode 100644 index 0000000..1b389e3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/ScenesReducer.spec.lua @@ -0,0 +1,266 @@ +return function() + -- ScenesReducer(scenes, nextState, prevState, descriptors) + local ScenesReducer = require(script.Parent.Parent.ScenesReducer) + + local initialRouteKey = "id-1" + local initialRouteName = "First Route" + local initialRoute = { + routeName = initialRouteName, + key = initialRouteKey, + } + + local initialState = { + index = 1, + key = "StackRouterRoot", + isTransitioning = false, + routes = { + initialRoute, + }, + } + + local initialDescriptors = { + [initialRouteKey] = { + key = initialRouteKey, + navigation = { + state = initialRoute, + }, + state = initialRoute, + }, + } + + local initialScenes = nil + + it("should generate valid initial scene", function() + local scenes = ScenesReducer({}, initialState, nil, initialDescriptors) + local sceneOne = scenes[1] + expect(#scenes).to.be.equal(1) + expect(sceneOne.route).to.be.equal(initialRoute) + expect(sceneOne.index).to.be.equal(1) + expect(sceneOne.isActive).to.be.equal(true) + expect(sceneOne.isStale).to.be.equal(false) + initialScenes = scenes + end) + + it("should update descriptor", function() + local scenes = ScenesReducer({}, initialState, nil, initialDescriptors) + local dummyDescriptor = { key = "this is a dummy descriptor" } + scenes[1].descriptor = dummyDescriptor + local updatedScenes = ScenesReducer(scenes, initialState, initialState, initialDescriptors) + expect(#updatedScenes).to.be.equal(1) + local sceneOne = updatedScenes[1] + expect(sceneOne.descriptor).to.never.be.equal(dummyDescriptor) + expect(sceneOne.descriptor).to.be.equal(initialDescriptors[initialRouteKey]) + expect(sceneOne.route).to.be.equal(initialRoute) + expect(sceneOne.index).to.be.equal(1) + expect(sceneOne.isActive).to.be.equal(true) + expect(sceneOne.isStale).to.be.equal(false) + end) + + it("should bail out early", function() + expect(initialScenes).to.never.be.equal(nil) + local scenes = ScenesReducer(initialScenes, nil, nil, nil) + expect(scenes).to.be.equal(initialScenes) + scenes = ScenesReducer(initialScenes, initialState, initialState, initialDescriptors) + expect(scenes).to.be.equal(initialScenes) + end) + + local secondRouteKey = "id-2" + local secondRouteName = "Second Route" + local secondRoute = { + key = secondRouteKey, + routeName = secondRouteName, + } + + local secondState = { + index = 2, + key = "StackRouterRoot", + isTransitioning = true, + routes = { + initialRoute, + secondRoute, + }, + } + + local secondDescriptors = { + [initialRouteKey] = initialDescriptors[initialRouteKey], + [secondRouteKey] ={ + key = secondRouteKey, + navigation = { + state = secondRoute, + }, + state = secondRoute, + }, + } + + local secondScenes = nil + + it("should add second scene", function() + expect(initialScenes).to.never.be.equal(nil) + local scenes = ScenesReducer(initialScenes, secondState, initialState, secondDescriptors) + expect(scenes).to.never.be.equal(initialScenes) + expect(#scenes).to.be.equal(2) + + local sceneOne = scenes[1] + expect(sceneOne.route).to.be.equal(initialRoute) + expect(sceneOne.index).to.be.equal(1) + expect(sceneOne.isActive).to.be.equal(false) + expect(sceneOne.isStale).to.be.equal(false) + + local sceneTwo = scenes[2] + expect(sceneTwo.route).to.be.equal(secondRoute) + expect(sceneTwo.index).to.be.equal(2) + expect(sceneTwo.isActive).to.be.equal(true) + expect(sceneTwo.isStale).to.be.equal(false) + + secondScenes = scenes + end) + + local thirdRouteKey = "id-3" + local thirdRouteName = "Third Route" + local thirdRoute = { + key = thirdRouteKey, + routeName = thirdRouteName, + } + + local thirdState = { + index = 3, + key = "StackRouterRoot", + isTransitioning = true, + routes = { + initialRoute, + secondRoute, + thirdRoute, + }, + } + + local thirdDescriptors = { + [initialRouteKey] = initialDescriptors[initialRouteKey], + [secondRouteKey] = secondDescriptors[secondRouteKey], + [thirdRouteKey] = { + key = thirdRouteKey, + navigation = { + state = thirdRoute, + }, + state = thirdRoute, + }, + } + + local thirdScenes = nil + + it("should add third scene", function() + expect(initialScenes).to.never.be.equal(nil) + local scenes = ScenesReducer(secondScenes, thirdState, secondState, thirdDescriptors) + expect(scenes).to.never.be.equal(initialScenes) + expect(#scenes).to.be.equal(3) + + local sceneOne = scenes[1] + expect(sceneOne.route).to.be.equal(initialRoute) + expect(sceneOne.index).to.be.equal(1) + expect(sceneOne.isActive).to.be.equal(false) + expect(sceneOne.isStale).to.be.equal(false) + + local sceneTwo = scenes[2] + expect(sceneTwo.route).to.be.equal(secondRoute) + expect(sceneTwo.index).to.be.equal(2) + expect(sceneTwo.isActive).to.be.equal(false) + expect(sceneTwo.isStale).to.be.equal(false) + + local sceneTwo = scenes[3] + expect(sceneTwo.route).to.be.equal(thirdRoute) + expect(sceneTwo.index).to.be.equal(3) + expect(sceneTwo.isActive).to.be.equal(true) + expect(sceneTwo.isStale).to.be.equal(false) + + thirdScenes = scenes + end) + + it("should mark removed scenes as stale", function() + expect(secondState).to.never.be.equal(nil) + local scenes = ScenesReducer(thirdScenes, initialState, thirdState, initialDescriptors) + expect(scenes).to.never.be.equal(initialScenes) + expect(#scenes).to.be.equal(3) + + local sceneOne = scenes[1] + expect(sceneOne.route).to.be.equal(initialRoute) + expect(sceneOne.index).to.be.equal(1) + expect(sceneOne.isActive).to.be.equal(true) + expect(sceneOne.isStale).to.be.equal(false) + + local sceneTwo = scenes[2] + expect(sceneTwo.route).to.be.equal(secondRoute) + expect(sceneTwo.index).to.be.equal(2) + expect(sceneTwo.isActive).to.be.equal(false) + expect(sceneTwo.isStale).to.be.equal(true) + + local sceneThree = scenes[3] + expect(sceneThree.route).to.be.equal(thirdRoute) + expect(sceneThree.index).to.be.equal(3) + expect(sceneThree.isActive).to.be.equal(false) + expect(sceneThree.isStale).to.be.equal(true) + end) + + local secondScreenReplacementKey = "id-22" + local secondScreenReplacementName = "Second Route Replacement" + local secondScreenReplacementRoute = { + key = secondScreenReplacementKey, + routeName = secondScreenReplacementName, + } + + local replacedSecondSceneState = { + index = 3, + key = "StackRouterRoot", + isTransitioning = true, + routes = { + initialRoute, + secondScreenReplacementRoute, + thirdRoute, + }, + } + + local replacedSceneDescriptors = { + [initialRouteKey] = initialDescriptors[initialRouteKey], + [secondRouteKey] = { + key = secondScreenReplacementKey, + navigation = { + state = secondScreenReplacementRoute + }, + state = secondScreenReplacementRoute, + }, + [thirdRouteKey] = thirdDescriptors[thirdRouteKey], + } + + it("should mark replaced scene as stale", function() + expect(secondState).to.never.be.equal(nil) + local scenes = ScenesReducer(thirdScenes, replacedSecondSceneState, thirdState, replacedSceneDescriptors) + expect(scenes).to.never.be.equal(initialScenes) + expect(#scenes).to.be.equal(4) -- replaced scene is marked stale, it is not removed + + local sceneOne = scenes[1] + expect(sceneOne.route).to.be.equal(initialRoute) + expect(sceneOne.index).to.be.equal(1) + expect(sceneOne.isActive).to.be.equal(false) + expect(sceneOne.isStale).to.be.equal(false) + + local sceneTwo = scenes[2] + expect(sceneTwo.route).to.be.equal(secondRoute) + expect(sceneTwo.index).to.be.equal(2) + expect(sceneTwo.isActive).to.be.equal(false) + expect(sceneTwo.isStale).to.be.equal(true) + + -- because of comparison algorithm in SceneReducer.lua compareScenes + -- the replacement scene is after the scene it replaced because id-2 < id-22 + local sceneTwoReplacement = scenes[3] + expect(sceneTwoReplacement.route).to.be.equal(secondScreenReplacementRoute) + -- index is still 2 because the scene index come from route index in the nextState + -- this is ok because filterStale in Transitioner.lua will remove the stale scene + expect(sceneTwoReplacement.index).to.be.equal(2) + expect(sceneTwoReplacement.isActive).to.be.equal(false) + expect(sceneTwoReplacement.isStale).to.be.equal(false) + + local sceneThree = scenes[4] + expect(sceneThree.route).to.be.equal(thirdRoute) + expect(sceneThree.index).to.be.equal(3) + expect(sceneThree.isActive).to.be.equal(true) + expect(sceneThree.isStale).to.be.equal(false) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/ScreenGuiWrapper.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/ScreenGuiWrapper.spec.lua new file mode 100644 index 0000000..1d81665 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/ScreenGuiWrapper.spec.lua @@ -0,0 +1,50 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local ScreenGuiWrapper = require(script.Parent.Parent.ScreenGuiWrapper) + + it("should mount the inner component if visible", function() + local innerComponentProps = nil + + local innerComponent = function(props) + innerComponentProps = props + return Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50) + }) + end + + local instance = Roact.mount(Roact.createElement(ScreenGuiWrapper, { + component = innerComponent, + visible = true, + myPassedInValue = 5, + })) + + expect(innerComponentProps).to.never.equal(nil) + expect(innerComponentProps.visible).to.equal(true) + expect(innerComponentProps.DisplayOrder).to.equal(nil) + expect(innerComponentProps.component).to.equal(nil) + expect(innerComponentProps.myPassedInValue).to.equal(5) + + Roact.unmount(instance) + end) + + it("should still mount the inner component if ScreenGui is not visible", function() + local innerComponentProps = nil + + local innerComponent = function(props) + innerComponentProps = props + return Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50) + }) + end + + local instance = Roact.mount(Roact.createElement(ScreenGuiWrapper, { + component = innerComponent, + visible = false, + })) + + expect(innerComponentProps).to.never.equal(nil) + expect(innerComponentProps.visible).to.equal(false) + + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/SwitchView.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/SwitchView.spec.lua new file mode 100644 index 0000000..facd12f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/SwitchView.spec.lua @@ -0,0 +1,274 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local SwitchView = require(script.Parent.Parent.SwitchView) + local withNavigation = require(script.Parent.Parent.withNavigation) + + it("should mount and pass required props and context", function() + local testScreenProps = {} + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" } + }, + index = 1, + }, + } + + local testComponentNavigationFromProp = nil + local testComponentScreenProps = nil + local testComponentNavigationFromContext = nil + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + testComponentNavigationFromProp = self.props.navigation + testComponentScreenProps = self.props.screenProps + + return withNavigation(function(navigation) + testComponentNavigationFromContext = navigation + end) + end + + local testDescriptors = { + Foo = { + getComponent = function() + return TestComponent + end, + navigation = testNavigation, + } + } + + local element = Roact.createElement(SwitchView, { + screenProps = testScreenProps, + navigation = testNavigation, + descriptors = testDescriptors, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(testComponentNavigationFromProp).to.equal(testNavigation) + expect(testComponentScreenProps).to.equal(testScreenProps) + expect(testComponentNavigationFromContext).to.equal(testNavigation) + end) + + it("should unmount inactive pages when keepVisitedScreensMounted is false", function() + local fooUnmounted = false + local TestComponentFoo = Roact.Component:extend("TestComponentFoo") + function TestComponentFoo:render() end + function TestComponentFoo:willUnmount() + fooUnmounted = true + end + + local TestComponentBar = Roact.Component:extend("TestComponentBar") + function TestComponentBar:render() end + + local function makeDescriptors(navProp) + return { + Foo = { + getComponent = function() return TestComponentFoo end, + navigation = navProp, + }, + Bar = { + getComponent = function() return TestComponentBar end, + navigation = navProp, + }, + } + end + + local testNavigation1 = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + { routeName = "Bar", key = "Bar" }, + }, + index = 1, + }, + } + + local element = Roact.createElement(SwitchView, { + screenProps = {}, + navigation = testNavigation1, + descriptors = makeDescriptors(testNavigation1), + navigationConfig = { + keepVisitedScreensMounted = false, + }, + }) + + local instance = Roact.mount(element) + expect(fooUnmounted).to.equal(false) + + local testNavigation2 = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + { routeName = "Bar", key = "Bar" }, + }, + index = 2, + }, + } + + instance = Roact.update(instance, Roact.createElement(SwitchView, { + screenProps = {}, + navigation = testNavigation2, + descriptors = makeDescriptors(testNavigation2), + navigationConfig = { + keepVisitedScreensMounted = false, + } + })) + + expect(fooUnmounted).to.equal(true) + Roact.unmount(instance) + end) + + it("should not unmount inactive pages when keepVisitedScreensMounted is true", function() + local fooUnmounted = false + local TestComponentFoo = Roact.Component:extend("TestComponentFoo") + function TestComponentFoo:render() end + function TestComponentFoo:willUnmount() + fooUnmounted = true + end + + local TestComponentBar = Roact.Component:extend("TestComponentBar") + function TestComponentBar:render() end + + local function makeDescriptors(navProp) + return { + Foo = { + getComponent = function() return TestComponentFoo end, + navigation = navProp, + }, + Bar = { + getComponent = function() return TestComponentBar end, + navigation = navProp, + }, + } + end + + local testNavigation1 = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + { routeName = "Bar", key = "Bar" }, + }, + index = 1, + }, + } + + local element = Roact.createElement(SwitchView, { + screenProps = {}, + navigation = testNavigation1, + descriptors = makeDescriptors(testNavigation1), + navigationConfig = { + keepVisitedScreensMounted = true, + }, + }) + + local instance = Roact.mount(element) + expect(fooUnmounted).to.equal(false) + + local testNavigation2 = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + { routeName = "Bar", key = "Bar" }, + }, + index = 2, + }, + } + + instance = Roact.update(instance, Roact.createElement(SwitchView, { + screenProps = {}, + navigation = testNavigation2, + descriptors = makeDescriptors(testNavigation2), + navigationConfig = { + keepVisitedScreensMounted = true, + } + })) + + expect(fooUnmounted).to.equal(false) + + Roact.unmount(instance) + expect(fooUnmounted).to.equal(true) + end) + + it("should unmount inactive pages when keepVisitedScreensMounted switches from true to false", function() + local fooUnmounted = false + local TestComponentFoo = Roact.Component:extend("TestComponentFoo") + function TestComponentFoo:render() end + function TestComponentFoo:willUnmount() + fooUnmounted = true + end + + local TestComponentBar = Roact.Component:extend("TestComponentBar") + function TestComponentBar:render() end + + local function makeDescriptors(navProp) + return { + Foo = { + getComponent = function() return TestComponentFoo end, + navigation = navProp, + }, + Bar = { + getComponent = function() return TestComponentBar end, + navigation = navProp, + }, + } + end + + local testNavigation1 = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + { routeName = "Bar", key = "Bar" }, + }, + index = 1, + }, + } + + local element = Roact.createElement(SwitchView, { + screenProps = {}, + navigation = testNavigation1, + descriptors = makeDescriptors(testNavigation1), + navigationConfig = { + keepVisitedScreensMounted = true, + }, + }) + + local instance = Roact.mount(element) + + local testNavigation2 = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + { routeName = "Bar", key = "Bar" }, + }, + index = 2, + }, + } + + -- We must update tree to make sure active screens list gets updated first! + instance = Roact.update(instance, Roact.createElement(SwitchView, { + screenProps = {}, + navigation = testNavigation2, + descriptors = makeDescriptors(testNavigation2), + navigationConfig = { + keepVisitedScreensMounted = true, + } + })) + + expect(fooUnmounted).to.equal(false) + + instance = Roact.update(instance, Roact.createElement(SwitchView, { + screenProps = {}, + navigation = testNavigation2, + descriptors = makeDescriptors(testNavigation2), + navigationConfig = { + keepVisitedScreensMounted = false, + } + })) + + expect(fooUnmounted).to.equal(true) + + Roact.unmount(instance) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/Transitioner.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/Transitioner.spec.lua new file mode 100644 index 0000000..ddbe63a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/Transitioner.spec.lua @@ -0,0 +1,8 @@ +return function() + -- local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + -- local Transitioner = require(script.Parent.Parent.Transitioner) + + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/withNavigation.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/withNavigation.spec.lua new file mode 100644 index 0000000..7cf9988 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/withNavigation.spec.lua @@ -0,0 +1,34 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local withNavigation = require(script.Parent.Parent.withNavigation) + local AppNavigationContext = require(script.Parent.Parent.AppNavigationContext) + + it("should throw if no renderProp is provided", function() + local status, err = pcall(function() + withNavigation(nil) + end) + + expect(status).to.equal(false) + expect(string.find(err, "withNavigation must be passed a render prop")).to.never.equal(nil) + end) + + it("should extract navigation object from provider and pass it through", function() + local testNavigation = {} + local extractedNavigation = nil + + local rootElement = Roact.createElement(AppNavigationContext.Provider, { + navigation = testNavigation, + }, { + Child = Roact.createElement(function() + return withNavigation(function(nav) + extractedNavigation = nav + end) + end) + }) + + local rootInstance = Roact.mount(rootElement) + Roact.unmount(rootInstance) + + expect(extractedNavigation).to.equal(testNavigation) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/withNavigationFocus.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/withNavigationFocus.spec.lua new file mode 100644 index 0000000..88b5696 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/withNavigationFocus.spec.lua @@ -0,0 +1,147 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local AppNavigationContext = require(script.Parent.Parent.AppNavigationContext) + local NavigationEvents = require(script.Parent.Parent.Parent.NavigationEvents) + local withNavigationFocus = require(script.Parent.Parent.withNavigationFocus) + + it("should pass focused=true when initially focused", function() + local testNavigation, testFocused + local component = function() + return withNavigationFocus(function(navigation, focused) + testNavigation = navigation + testFocused = focused + return nil + end) + end + + local navigationProp = { + isFocused = function() + return true + end, + addListener = function() + return { + disconnect = function() end + } + end + } + + local rootElement = Roact.createElement(AppNavigationContext.Provider, { + navigation = navigationProp, + }, { + child = Roact.createElement(component) + }) + + local instance = Roact.mount(rootElement) + expect(testNavigation).to.equal(navigationProp) + expect(testFocused).to.equal(true) + + Roact.unmount(instance) + end) + + it("should pass focused=false when initially unfocused", function() + local testNavigation, testFocused + local component = function() + return withNavigationFocus(function(navigation, focused) + testNavigation = navigation + testFocused = focused + return nil + end) + end + + local navigationProp = { + isFocused = function() + return false + end, + addListener = function() + return { + disconnect = function() end + } + end + } + + local rootElement = Roact.createElement(AppNavigationContext.Provider, { + navigation = navigationProp, + }, { + child = Roact.createElement(component) + }) + + local instance = Roact.mount(rootElement) + expect(testNavigation).to.equal(navigationProp) + expect(testFocused).to.equal(false) + + Roact.unmount(instance) + end) + + it("should re-render and set focused status for events", function() + local testListeners = {} + local testFocused = false + local component = function() + return withNavigationFocus(function(navigation, focused) + testFocused = focused + return nil + end) + end + + local navigationProp = { + isFocused = function() + return false + end, + addListener = function(event, listener) + testListeners[event] = listener + return { + disconnect = function() + testListeners[event] = nil + end + } + end + } + + local rootElement = Roact.createElement(AppNavigationContext.Provider, { + navigation = navigationProp, + }, { + child = Roact.createElement(component) + }) + + local instance = Roact.mount(rootElement) + expect(testFocused).to.equal(false) + expect(type(testListeners[NavigationEvents.DidFocus])).to.equal("function") + expect(type(testListeners[NavigationEvents.WillBlur])).to.equal("function") + + testListeners[NavigationEvents.DidFocus]() + expect(testFocused).to.equal(true) + + testListeners[NavigationEvents.WillBlur]() + expect(testFocused).to.equal(false) + + Roact.unmount(instance) + expect(testListeners[NavigationEvents.DidFocus]).to.equal(nil) + expect(testListeners[NavigationEvents.WillBlur]).to.equal(nil) + end) + + it("should throw when renderProp is not provided", function() + local success, err = pcall(function() + withNavigationFocus(nil) + end) + + expect(success).to.equal(false) + expect(string.find(err, + "withNavigationFocus must be passed a render prop")).to.never.equal(nil) + end) + + it("should throw when used outside of a navigation provider", function() + local component = function() + return withNavigationFocus(function(navigation, focused) + + end) + end + + local element = Roact.createElement(component) + + local success, _ = pcall(function() + Roact.unmount(Roact.mount(element)) + end) + + expect(success).to.equal(false) + -- We do not test the message because NavigationConsumer gets in the way here. + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/withNavigation.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/withNavigation.lua new file mode 100644 index 0000000..4590801 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/withNavigation.lua @@ -0,0 +1,24 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local AppNavigationContext = require(script.Parent.AppNavigationContext) +local validate = require(script.Parent.Parent.utils.validate) + +--[[ + withNavigation() is a convenience function that you can use in your component's + render function to access the navigation context object. For example: + + function MyComponent:render() + return withNavigation(function(navigation) + return Roact.createElement("TextButton", { + [Roact.Activated] = function() + navigation.navigate("DetailPage") + end + }) + end) + end +]] +return function(renderProp) + validate(renderProp ~= nil, "withNavigation must be passed a render prop") + return Roact.createElement(AppNavigationContext.Consumer, { + render = renderProp + }) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/withNavigationFocus.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/withNavigationFocus.lua new file mode 100644 index 0000000..cf9d96b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/withNavigationFocus.lua @@ -0,0 +1,89 @@ +--[[ + withNavigationFocus() is a convenience function that extends withNavigation(), + allowing your render function (and therefor your subgraph) to access the + navigation context object AND an additional boolean that indicates whether or + not the containing screen component is in focus. For example: + + function MyButtonComponent:render() + return withNavigationFocus(function(navigation, focused) + return Roact.createElement("TextButton", { + Enabled = focused, + [Roact.Event.Activated] = function() + navigation.navigate("DetailPage") + end, + }) + end) + end + + This is very useful when writing generic components that need to work with + the navigation system (e.g. preventing buttons from navigating when a screen + is not in focus so you don't cause double-navigation). + + Note that if you ONLY need the 'navigation' context object, it is recommended + that you use withNavigation() for performance reasons. +]] +local Roact = require(script.Parent.Parent.Parent.Roact) +local NavigationEvents = require(script.Parent.Parent.NavigationEvents) +local AppNavigationContext = require(script.Parent.AppNavigationContext) +local validate = require(script.Parent.Parent.utils.validate) + + +local NavigationFocusComponent = Roact.Component:extend("NavigationFocusComponent") + +function NavigationFocusComponent:init() + local navigation = self.props.navigation + self.state = { + isFocused = navigation and navigation.isFocused() or false + } +end + +function NavigationFocusComponent:didMount() + local navigation = self.props.navigation + validate(navigation ~= nil, + "withNavigationFocus can only be used within the view hierarchy of a navigator. " .. + "The wrapped component cannot access 'navigation' from props or context.") + + self._didFocusListener = navigation.addListener(NavigationEvents.DidFocus, function() + -- no spawn because we expect this to be called directly from safe paths + self:setState({ + isFocused = true, + }) + end) + + self._willBlurListener = navigation.addListener(NavigationEvents.WillBlur, function() + -- no spawn because we expect this to be called directly from safe paths + self:setState({ + isFocused = false, + }) + end) +end + +function NavigationFocusComponent:willUnmount() + if self._didFocusListener then + self._didFocusListener:disconnect() + self._didFocusListener = nil + end + + if self._willBlurListener then + self._willBlurListener:disconnect() + self._willBlurListener = nil + end +end + +function NavigationFocusComponent:render() + local isFocused = self.state.isFocused + local navigation = self.props.navigation + local render = self.props.render + + return render(navigation, isFocused) +end + +NavigationFocusComponent = AppNavigationContext.connect(NavigationFocusComponent) + +return function(renderProp) + validate(renderProp ~= nil, "withNavigationFocus must be passed a render prop") + + return Roact.createElement(NavigationFocusComponent, { + render = renderProp + }) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/Roact.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/Rodux.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/Rodux.lua new file mode 100644 index 0000000..96b67df --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/Rodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_rodux"]["rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/lock.toml new file mode 100644 index 0000000..f7bd68d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/lock.toml @@ -0,0 +1,9 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/roact-rodux" +version = "0.2.2" +commit = "78e2f5ac8c4679ef7216fbb9eedbcae31122545a" +source = "url+https://github.com/roblox/roact-rodux" +dependencies = [ + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/StoreProvider.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/StoreProvider.lua new file mode 100644 index 0000000..a226935 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/StoreProvider.lua @@ -0,0 +1,21 @@ +local Roact = require(script.Parent.Parent.Roact) + +local storeKey = require(script.Parent.storeKey) + +local StoreProvider = Roact.Component:extend("StoreProvider") + +function StoreProvider:init(props) + local store = props.store + + if store == nil then + error("Error initializing StoreProvider. Expected a `store` prop to be a Rodux store.") + end + + self._context[storeKey] = store +end + +function StoreProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +return StoreProvider \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/StoreProvider.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/StoreProvider.spec.lua new file mode 100644 index 0000000..e7236f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/StoreProvider.spec.lua @@ -0,0 +1,30 @@ +return function() + local StoreProvider = require(script.Parent.StoreProvider) + + local Roact = require(script.Parent.Parent.Roact) + local Rodux = require(script.Parent.Parent.Rodux) + + it("should be instantiable as a component", function() + local store = Rodux.Store.new(function() + return 0 + end) + local element = Roact.createElement(StoreProvider, { + store = store + }) + + expect(element).to.be.ok() + + local handle = Roact.mount(element, nil, "StoreProvider-test") + + Roact.unmount(handle) + store:destruct() + end) + + it("should expect a 'store' prop", function() + local element = Roact.createElement(StoreProvider) + + expect(function() + Roact.mount(element) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/Symbol.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/Symbol.lua new file mode 100644 index 0000000..8b6adaf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/Symbol.lua @@ -0,0 +1,43 @@ +--[[ + A 'Symbol' is an opaque marker type that can be used to signify unique + statuses. Symbols have the type 'userdata', but when printed to the console, + the name of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +--[[ + Create an unnamed Symbol. Usually, you should create a named Symbol using + Symbol.named(name) +]] +function Symbol.unnamed() + local self = newproxy(true) + + getmetatable(self).__tostring = function() + return "Unnamed Symbol" + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/Symbol.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/Symbol.spec.lua new file mode 100644 index 0000000..cde9be0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/Symbol.spec.lua @@ -0,0 +1,45 @@ +return function() + local Symbol = require(script.Parent.Symbol) + + describe("named", function() + it("should give an opaque object", function() + local symbol = Symbol.named("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = Symbol.named("foo") + + expect(tostring(symbol):find("foo")).to.be.ok() + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.named("abc") + local symbolB = Symbol.named("abc") + + expect(symbolA).never.to.equal(symbolB) + end) + end) + + describe("unnamed", function() + it("should give an opaque object", function() + local symbol = Symbol.unnamed() + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to some string", function() + local symbol = Symbol.unnamed() + + expect(tostring(symbol)).to.be.a("string") + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.unnamed() + local symbolB = Symbol.unnamed() + + expect(symbolA).never.to.equal(symbolB) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/TempConfig.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/TempConfig.lua new file mode 100644 index 0000000..a752d7d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/TempConfig.lua @@ -0,0 +1,3 @@ +return { + newConnectionOrder = true, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/connect.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/connect.lua new file mode 100644 index 0000000..7e18ca1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/connect.lua @@ -0,0 +1,201 @@ +local Roact = require(script.Parent.Parent.Roact) +local getStore = require(script.Parent.getStore) +local shallowEqual = require(script.Parent.shallowEqual) +local join = require(script.Parent.join) + +local TempConfig = require(script.Parent.TempConfig) + +--[[ + Formats a multi-line message with printf-style placeholders. +]] +local function formatMessage(lines, parameters) + return table.concat(lines, "\n"):format(unpack(parameters or {})) +end + +local function noop() + return nil +end + +--[[ + The stateUpdater accepts props when they update and computes the + complete set of props that should be passed to the wrapped component. + + Each connected component will have a stateUpdater created for it. + + stateUpdater is put into the component's state in order for + getDerivedStateFromProps to be able to access it. It is not mutated. +]] +local function makeStateUpdater(store) + return function(nextProps, prevState, mappedStoreState) + -- The caller can optionally provide mappedStoreState if it needed that + -- value beforehand. Doing so is purely an optimization. + if mappedStoreState == nil then + mappedStoreState = prevState.mapStateToProps(store:getState(), nextProps) + end + + local propsForChild = join(nextProps, mappedStoreState, prevState.mappedStoreDispatch) + + return { + mappedStoreState = mappedStoreState, + propsForChild = propsForChild, + } + end +end + +--[[ + mapStateToProps: + (storeState, props) -> partialProps + OR + () -> (storeState, props) -> partialProps + mapDispatchToProps: (dispatch) -> partialProps +]] +local function connect(mapStateToPropsOrThunk, mapDispatchToProps) + local connectTrace = debug.traceback() + + if mapStateToPropsOrThunk ~= nil then + assert(typeof(mapStateToPropsOrThunk) == "function", "mapStateToProps must be a function or nil!") + else + mapStateToPropsOrThunk = noop + end + + if mapDispatchToProps ~= nil then + assert(typeof(mapDispatchToProps) == "function", "mapDispatchToProps must be a function or nil!") + else + mapDispatchToProps = noop + end + + return function(innerComponent) + if innerComponent == nil then + local message = formatMessage({ + "connect returns a function that must be passed a component.", + "Check the connection at:", + "%s", + }, { + connectTrace, + }) + + error(message, 2) + end + + local componentName = ("RoduxConnection(%s)"):format(tostring(innerComponent)) + + local Connection = Roact.Component:extend(componentName) + + function Connection.getDerivedStateFromProps(nextProps, prevState) + if prevState.stateUpdater ~= nil then + return prevState.stateUpdater(nextProps, prevState) + end + end + + function Connection:createStoreConnection() + self.storeChangedConnection = self.store.changed:connect(function(storeState) + self:setState(function(prevState, props) + local mappedStoreState = prevState.mapStateToProps(storeState, props) + + -- We run this check here so that we only check shallow + -- equality with the result of mapStateToProps, and not the + -- other props that could be passed through the connector. + if shallowEqual(mappedStoreState, prevState.mappedStoreState) then + return nil + end + + return prevState.stateUpdater(props, prevState, mappedStoreState) + end) + end) + end + + function Connection:init() + self.store = getStore(self) + + if self.store == nil then + local message = formatMessage({ + "Cannot initialize Roact-Rodux connection without being a descendent of StoreProvider!", + "Tried to wrap component %q", + "Make sure there is a StoreProvider above this component in the tree.", + }, { + tostring(innerComponent), + }) + + error(message) + end + + local storeState = self.store:getState() + + local mapStateToProps = mapStateToPropsOrThunk + local mappedStoreState = mapStateToProps(storeState, self.props) + + -- mapStateToPropsOrThunk can return a function instead of a state + -- value. In this variant, we keep that value as mapStateToProps + -- instead of the original mapStateToProps. This matches react-redux + -- and enables connectors to keep instance-level state. + if typeof(mappedStoreState) == "function" then + mapStateToProps = mappedStoreState + mappedStoreState = mapStateToProps(storeState, self.props) + end + + if mappedStoreState ~= nil and typeof(mappedStoreState) ~= "table" then + local message = formatMessage({ + "mapStateToProps must either return a table, or return another function that returns a table.", + "Instead, it returned %q, which is of type %s.", + }, { + tostring(mappedStoreState), + typeof(mappedStoreState), + }) + + error(message) + end + + local mappedStoreDispatch = mapDispatchToProps(function(...) + return self.store:dispatch(...) + end) + + local stateUpdater = makeStateUpdater(self.store) + + self.state = { + -- Combines props, mappedStoreDispatch, and the result of + -- mapStateToProps into propsForChild. Stored in state so that + -- getDerivedStateFromProps can access it. + stateUpdater = stateUpdater, + + -- Used by the store changed connection and stateUpdater to + -- construct propsForChild. + mapStateToProps = mapStateToProps, + + -- Used by stateUpdater to construct propsForChild. + mappedStoreDispatch = mappedStoreDispatch, + + -- Passed directly into the component that Connection is + -- wrapping. + propsForChild = nil, + } + + local extraState = stateUpdater(self.props, self.state, mappedStoreState) + + for key, value in pairs(extraState) do + self.state[key] = value + end + + if TempConfig.newConnectionOrder then + self:createStoreConnection() + end + end + + function Connection:didMount() + if not TempConfig.newConnectionOrder then + self:createStoreConnection() + end + end + + function Connection:willUnmount() + self.storeChangedConnection:disconnect() + end + + function Connection:render() + return Roact.createElement(innerComponent, self.state.propsForChild) + end + + return Connection + end +end + +return connect \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/connect.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/connect.spec.lua new file mode 100644 index 0000000..453e3d5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/connect.spec.lua @@ -0,0 +1,353 @@ +return function() + local connect = require(script.Parent.connect) + + local StoreProvider = require(script.Parent.StoreProvider) + + local Roact = require(script.Parent.Parent.Roact) + local Rodux = require(script.Parent.Parent.Rodux) + + local TempConfig = require(script.Parent.TempConfig) + + local function noop() + return nil + end + + local function NoopComponent() + return nil + end + + local function countReducer(state, action) + state = state or 0 + + if action.type == "increment" then + return state + 1 + end + + return state + end + + local reducer = Rodux.combineReducers({ + count = countReducer, + }) + + describe("Argument validation", function() + it("should accept no arguments", function() + connect() + end) + + it("should accept one function", function() + connect(noop) + end) + + it("should accept two functions", function() + connect(noop, noop) + end) + + it("should accept only the second function", function() + connect(nil, function() end) + end) + + it("should throw if not passed a component", function() + local selector = function(store) + return {} + end + + expect(function() + connect(selector)(nil) + end).to.throw() + end) + end) + + it("should throw if not mounted under a StoreProvider", function() + local ConnectedSomeComponent = connect()(NoopComponent) + + expect(function() + Roact.mount(Roact.createElement(ConnectedSomeComponent)) + end).to.throw() + end) + + it("should accept a higher-order function mapStateToProps", function() + local function mapStateToProps() + return function(state) + return { + count = state.count, + } + end + end + + local ConnectedSomeComponent = connect(mapStateToProps)(NoopComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent), + }) + + local handle = Roact.mount(tree) + + Roact.unmount(handle) + end) + + it("should not accept a higher-order mapStateToProps that returns a non-table value", function() + local function mapStateToProps() + return function(state) + return "nope" + end + end + + local ConnectedSomeComponent = connect(mapStateToProps)(NoopComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent), + }) + + expect(function() + Roact.mount(tree) + end).to.throw() + end) + + it("should not accept a mapStateToProps that returns a non-table value", function() + local function mapStateToProps() + return "nah" + end + + local ConnectedSomeComponent = connect(mapStateToProps)(NoopComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent), + }) + + expect(function() + Roact.mount(tree) + end).to.throw() + end) + + it("should abort renders when mapStateToProps returns the same data", function() + local function mapStateToProps(state) + return { + count = state.count, + } + end + + local renderCount = 0 + local function SomeComponent(props) + renderCount = renderCount + 1 + end + + local ConnectedSomeComponent = connect(mapStateToProps)(SomeComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent), + }) + + local handle = Roact.mount(tree) + + expect(renderCount).to.equal(1) + + store:dispatch({ type = "an unknown action" }) + store:flush() + + expect(renderCount).to.equal(1) + + store:dispatch({ type = "increment" }) + store:flush() + + expect(renderCount).to.equal(2) + + Roact.unmount(handle) + end) + + it("should only call mapDispatchToProps once and never re-render if no mapStateToProps was passed", function() + local dispatchCount = 0 + local mapDispatchToProps = function(dispatch) + dispatchCount = dispatchCount + 1 + + return { + increment = function() + return dispatch({ type = "increment" }) + end, + } + end + + local renderCount = 0 + local function SomeComponent(props) + renderCount = renderCount + 1 + end + + local ConnectedSomeComponent = connect(nil, mapDispatchToProps)(SomeComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent), + }) + + local handle = Roact.mount(tree) + + expect(dispatchCount).to.equal(1) + expect(renderCount).to.equal(1) + + store:dispatch({ type = "an unknown action" }) + store:flush() + + expect(dispatchCount).to.equal(1) + expect(renderCount).to.equal(1) + + store:dispatch({ type = "increment" }) + store:flush() + + expect(dispatchCount).to.equal(1) + expect(renderCount).to.equal(1) + + Roact.unmount(handle) + end) + + it("should return result values from the dispatch passed to mapDispatchToProps", function() + local function reducer() + return 0 + end + + local function fiveThunk() + return 5 + end + + local dispatch + local function SomeComponent(props) + dispatch = props.dispatch + end + + local function mapDispatchToProps(dispatch) + return { + dispatch = dispatch + } + end + + local ConnectedSomeComponent = connect(nil, mapDispatchToProps)(SomeComponent) + + -- We'll use the thunk middleware, as it should always return its result + local store = Rodux.Store.new(reducer, nil, { Rodux.thunkMiddleware }) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent) + }) + + local handle = Roact.mount(tree) + + expect(dispatch).to.be.a("function") + expect(dispatch(fiveThunk)).to.equal(5) + + Roact.unmount(handle) + end) + + it("should render parent elements before children", function() + local oldNewConnectionOrder = TempConfig.newConnectionOrder + TempConfig.newConnectionOrder = true + + local function mapStateToProps(state) + return { + count = state.count, + } + end + + local childWasRenderedFirst = false + + local function ChildComponent(props) + if props.count > props.parentCount then + childWasRenderedFirst = true + end + end + + local ConnectedChildComponent = connect(mapStateToProps)(ChildComponent) + + local function ParentComponent(props) + return Roact.createElement(ConnectedChildComponent, { + parentCount = props.count, + }) + end + + local ConnectedParentComponent = connect(mapStateToProps)(ParentComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + parent = Roact.createElement(ConnectedParentComponent), + }) + + local handle = Roact.mount(tree) + + store:dispatch({ type = "increment" }) + store:flush() + + store:dispatch({ type = "increment" }) + store:flush() + + Roact.unmount(handle) + + expect(childWasRenderedFirst).to.equal(false) + + TempConfig.newConnectionOrder = oldNewConnectionOrder + end) + + it("should render child elements before children when TempConfig.newConnectionOrder is false", function() + local oldNewConnectionOrder = TempConfig.newConnectionOrder + TempConfig.newConnectionOrder = false + + local function mapStateToProps(state) + return { + count = state.count, + } + end + + local childWasRenderedFirst = false + + local function ChildComponent(props) + if props.count > props.parentCount then + childWasRenderedFirst = true + end + end + + local ConnectedChildComponent = connect(mapStateToProps)(ChildComponent) + + local function ParentComponent(props) + return Roact.createElement(ConnectedChildComponent, { + parentCount = props.count, + }) + end + + local ConnectedParentComponent = connect(mapStateToProps)(ParentComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + parent = Roact.createElement(ConnectedParentComponent), + }) + + local handle = Roact.mount(tree) + + store:dispatch({ type = "increment" }) + store:flush() + + store:dispatch({ type = "increment" }) + store:flush() + + Roact.unmount(handle) + + expect(childWasRenderedFirst).to.equal(true) + + TempConfig.newConnectionOrder = oldNewConnectionOrder + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/getStore.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/getStore.lua new file mode 100644 index 0000000..61fbd71 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/getStore.lua @@ -0,0 +1,7 @@ +local storeKey = require(script.Parent.storeKey) + +local function getStore(componentInstance) + return componentInstance._context[storeKey] +end + +return getStore \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/getStore.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/getStore.spec.lua new file mode 100644 index 0000000..c79966a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/getStore.spec.lua @@ -0,0 +1,62 @@ +return function() + local Roact = require(script.Parent.Parent.Roact) + local Rodux = require(script.Parent.Parent.Rodux) + + local StoreProvider = require(script.Parent.StoreProvider) + + local getStore = require(script.Parent.getStore) + + it("should return the store when present", function() + local function reducer() + return 0 + end + + local store = Rodux.Store.new(reducer) + local consumedStore = nil + + local StoreConsumer = Roact.Component:extend("StoreConsumer") + + function StoreConsumer:init() + consumedStore = getStore(self) + end + + function StoreConsumer:render() + return nil + end + + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + Consumer = Roact.createElement(StoreConsumer), + }) + + local handle = Roact.mount(tree) + + expect(consumedStore).to.equal(store) + + Roact.unmount(handle) + store:destruct() + end) + + it("should return nil when the store is not present", function() + -- Use a non-nil value to know for sure if StoreConsumer:init was called + local consumedStore = 6 + + local StoreConsumer = Roact.Component:extend("StoreConsumer") + + function StoreConsumer:init() + consumedStore = getStore(self) + end + + function StoreConsumer:render() + return nil + end + + local tree = Roact.createElement(StoreConsumer) + local handle = Roact.mount(tree) + + expect(consumedStore).to.equal(nil) + + Roact.unmount(handle) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/init.lua new file mode 100644 index 0000000..d7db6e6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/init.lua @@ -0,0 +1,13 @@ +local StoreProvider = require(script.StoreProvider) +local connect = require(script.connect) +local getStore = require(script.getStore) +local TempConfig = require(script.TempConfig) + +return { + StoreProvider = StoreProvider, + connect = connect, + UNSTABLE_getStore = getStore, + UNSTABLE_connect2 = connect, + + TEMP_CONFIG = TempConfig, +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/join.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/join.lua new file mode 100644 index 0000000..0e6b195 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/join.lua @@ -0,0 +1,17 @@ +local function join(...) + local result = {} + + for i = 1, select("#", ...) do + local source = select(i, ...) + + if source ~= nil then + for key, value in pairs(source) do + result[key] = value + end + end + end + + return result +end + +return join \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/shallowEqual.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/shallowEqual.lua new file mode 100644 index 0000000..8e9b68a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/shallowEqual.lua @@ -0,0 +1,23 @@ +local function shallowEqual(a, b) + if a == nil then + return b == nil + elseif b == nil then + return a == nil + end + + for key, value in pairs(a) do + if value ~= b[key] then + return false + end + end + + for key, value in pairs(b) do + if value ~= a[key] then + return false + end + end + + return true +end + +return shallowEqual \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/shallowEqual.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/shallowEqual.spec.lua new file mode 100644 index 0000000..fd0f9a2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/shallowEqual.spec.lua @@ -0,0 +1,45 @@ +return function() + local shallowEqual = require(script.Parent.shallowEqual) + + it("should compare dictionaries", function() + local a = { + a = "a", + b = {}, + c = 6, + } + + local b = { + b = a.b, + c = a.c, + a = a.a, + } + + local c = { + b = {}, + a = a.a, + c = a.c, + } + + local d = { + a = a.a, + b = a.b, + c = a.c, + d = "hello", + } + + expect(shallowEqual(a, a)).to.equal(true) + expect(shallowEqual(a, b)).to.equal(true) + expect(shallowEqual(a, c)).to.equal(false) + expect(shallowEqual(b, c)).to.equal(false) + expect(shallowEqual(a, d)).to.equal(false) + expect(shallowEqual(b, d)).to.equal(false) + end) + + it("should handle nil for either argument", function() + local a = {} + + expect(shallowEqual(nil, nil)).to.equal(true) + expect(shallowEqual(a, nil)).to.equal(false) + expect(shallowEqual(nil, a)).to.equal(false) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/storeKey.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/storeKey.lua new file mode 100644 index 0000000..bb77a74 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/storeKey.lua @@ -0,0 +1,3 @@ +local Symbol = require(script.Parent.Symbol) + +return Symbol.named("RoduxStore") \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/lock.toml new file mode 100644 index 0000000..be58a65 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/roact" +version = "1.3.0" +commit = "58af06b996061dd3ab0696fbe530e4b5070a35c4" +source = "url+https://github.com/roblox/roact" diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Binding.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Binding.lua new file mode 100644 index 0000000..b50acf6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Binding.lua @@ -0,0 +1,157 @@ +local createSignal = require(script.Parent.createSignal) +local Symbol = require(script.Parent.Symbol) +local Type = require(script.Parent.Type) + +local config = require(script.Parent.GlobalConfig).get() + +local BindingImpl = Symbol.named("BindingImpl") + +local BindingInternalApi = {} + +local bindingPrototype = {} + +function bindingPrototype:getValue() + return BindingInternalApi.getValue(self) +end + +function bindingPrototype:map(predicate) + return BindingInternalApi.map(self, predicate) +end + +local BindingPublicMeta = { + __index = bindingPrototype, + __tostring = function(self) + return string.format("RoactBinding(%s)", tostring(self:getValue())) + end, +} + +function BindingInternalApi.update(binding, newValue) + return binding[BindingImpl].update(newValue) +end + +function BindingInternalApi.subscribe(binding, callback) + return binding[BindingImpl].subscribe(callback) +end + +function BindingInternalApi.getValue(binding) + return binding[BindingImpl].getValue() +end + +function BindingInternalApi.create(initialValue) + local impl = { + value = initialValue, + changeSignal = createSignal(), + } + + function impl.subscribe(callback) + return impl.changeSignal:subscribe(callback) + end + + function impl.update(newValue) + impl.value = newValue + impl.changeSignal:fire(newValue) + end + + function impl.getValue() + return impl.value + end + + return setmetatable({ + [Type] = Type.Binding, + [BindingImpl] = impl, + }, BindingPublicMeta), impl.update +end + +function BindingInternalApi.map(upstreamBinding, predicate) + if config.typeChecks then + assert(Type.of(upstreamBinding) == Type.Binding, "Expected arg #1 to be a binding") + assert(typeof(predicate) == "function", "Expected arg #1 to be a function") + end + + local impl = {} + + function impl.subscribe(callback) + return BindingInternalApi.subscribe(upstreamBinding, function(newValue) + callback(predicate(newValue)) + end) + end + + function impl.update(newValue) + error("Bindings created by Binding:map(fn) cannot be updated directly", 2) + end + + function impl.getValue() + return predicate(upstreamBinding:getValue()) + end + + return setmetatable({ + [Type] = Type.Binding, + [BindingImpl] = impl, + }, BindingPublicMeta) +end + +function BindingInternalApi.join(upstreamBindings) + if config.typeChecks then + assert(typeof(upstreamBindings) == "table", "Expected arg #1 to be of type table") + + for key, value in pairs(upstreamBindings) do + if Type.of(value) ~= Type.Binding then + local message = ( + "Expected arg #1 to contain only bindings, but key %q had a non-binding value" + ):format( + tostring(key) + ) + error(message, 2) + end + end + end + + local impl = {} + + local function getValue() + local value = {} + + for key, upstream in pairs(upstreamBindings) do + value[key] = upstream:getValue() + end + + return value + end + + function impl.subscribe(callback) + local disconnects = {} + + for key, upstream in pairs(upstreamBindings) do + disconnects[key] = BindingInternalApi.subscribe(upstream, function(newValue) + callback(getValue()) + end) + end + + return function() + if disconnects == nil then + return + end + + for _, disconnect in pairs(disconnects) do + disconnect() + end + + disconnects = nil + end + end + + function impl.update(newValue) + error("Bindings created by joinBindings(...) cannot be updated directly", 2) + end + + function impl.getValue() + return getValue() + end + + return setmetatable({ + [Type] = Type.Binding, + [BindingImpl] = impl, + }, BindingPublicMeta) +end + +return BindingInternalApi \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Binding.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Binding.spec.lua new file mode 100644 index 0000000..f4fd03e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Binding.spec.lua @@ -0,0 +1,269 @@ +return function() + local createSpy = require(script.Parent.createSpy) + local Type = require(script.Parent.Type) + local GlobalConfig = require(script.Parent.GlobalConfig) + + local Binding = require(script.Parent.Binding) + + describe("Binding.create", function() + it("should return a Binding object and an update function", function() + local binding, update = Binding.create(1) + + expect(Type.of(binding)).to.equal(Type.Binding) + expect(typeof(update)).to.equal("function") + end) + + it("should support tostring on bindings", function() + local binding, update = Binding.create(1) + expect(tostring(binding)).to.equal("RoactBinding(1)") + + update("foo") + expect(tostring(binding)).to.equal("RoactBinding(foo)") + end) + end) + + describe("Binding object", function() + it("should provide a getter and setter", function() + local binding, update = Binding.create(1) + + expect(binding:getValue()).to.equal(1) + + update(3) + + expect(binding:getValue()).to.equal(3) + end) + + it("should let users subscribe and unsubscribe to its updates", function() + local binding, update = Binding.create(1) + + local spy = createSpy() + local disconnect = Binding.subscribe(binding, spy.value) + + expect(spy.callCount).to.equal(0) + + update(2) + + expect(spy.callCount).to.equal(1) + spy:assertCalledWith(2) + + disconnect() + update(3) + + expect(spy.callCount).to.equal(1) + end) + end) + + describe("Mapped bindings", function() + it("should be composable", function() + local word, updateWord = Binding.create("hi") + + local wordLength = word:map(string.len) + local isEvenLength = wordLength:map(function(value) + return value % 2 == 0 + end) + + expect(word:getValue()).to.equal("hi") + expect(wordLength:getValue()).to.equal(2) + expect(isEvenLength:getValue()).to.equal(true) + + updateWord("sup") + + expect(word:getValue()).to.equal("sup") + expect(wordLength:getValue()).to.equal(3) + expect(isEvenLength:getValue()).to.equal(false) + end) + + it("should cascade updates when subscribed", function() + -- base binding + local word, updateWord = Binding.create("hi") + + local wordSpy = createSpy() + local disconnectWord = Binding.subscribe(word, wordSpy.value) + + -- binding -> base binding + local length = word:map(string.len) + + local lengthSpy = createSpy() + local disconnectLength = Binding.subscribe(length, lengthSpy.value) + + -- binding -> binding -> base binding + local isEvenLength = length:map(function(value) + return value % 2 == 0 + end) + + local isEvenLengthSpy = createSpy() + local disconnectIsEvenLength = Binding.subscribe(isEvenLength, isEvenLengthSpy.value) + + expect(wordSpy.callCount).to.equal(0) + expect(lengthSpy.callCount).to.equal(0) + expect(isEvenLengthSpy.callCount).to.equal(0) + + updateWord("nice") + + expect(wordSpy.callCount).to.equal(1) + wordSpy:assertCalledWith("nice") + + expect(lengthSpy.callCount).to.equal(1) + lengthSpy:assertCalledWith(4) + + expect(isEvenLengthSpy.callCount).to.equal(1) + isEvenLengthSpy:assertCalledWith(true) + + disconnectWord() + disconnectLength() + disconnectIsEvenLength() + + updateWord("goodbye") + + expect(wordSpy.callCount).to.equal(1) + expect(isEvenLengthSpy.callCount).to.equal(1) + expect(lengthSpy.callCount).to.equal(1) + end) + + it("should throw when updated directly", function() + local source = Binding.create(1) + local mapped = source:map(function(v) + return v + end) + + expect(function() + Binding.update(mapped, 5) + end).to.throw() + end) + end) + + describe("Binding.join", function() + it("should have getValue", function() + local binding1 = Binding.create(1) + local binding2 = Binding.create(2) + local binding3 = Binding.create(3) + + local joinedBinding = Binding.join({ + binding1, + binding2, + foo = binding3, + }) + + local bindingValue = joinedBinding:getValue() + expect(bindingValue).to.be.a("table") + expect(bindingValue[1]).to.equal(1) + expect(bindingValue[2]).to.equal(2) + expect(bindingValue.foo).to.equal(3) + end) + + it("should update when any one of the subscribed bindings updates", function() + local binding1, update1 = Binding.create(1) + local binding2, update2 = Binding.create(2) + local binding3, update3 = Binding.create(3) + + local joinedBinding = Binding.join({ + binding1, + binding2, + foo = binding3, + }) + + local spy = createSpy() + Binding.subscribe(joinedBinding, spy.value) + + expect(spy.callCount).to.equal(0) + + update1(3) + expect(spy.callCount).to.equal(1) + + local args = spy:captureValues("value") + expect(args.value).to.be.a("table") + expect(args.value[1]).to.equal(3) + expect(args.value[2]).to.equal(2) + expect(args.value["foo"]).to.equal(3) + + update2(4) + expect(spy.callCount).to.equal(2) + + args = spy:captureValues("value") + expect(args.value).to.be.a("table") + expect(args.value[1]).to.equal(3) + expect(args.value[2]).to.equal(4) + expect(args.value["foo"]).to.equal(3) + + update3(8) + expect(spy.callCount).to.equal(3) + + args = spy:captureValues("value") + expect(args.value).to.be.a("table") + expect(args.value[1]).to.equal(3) + expect(args.value[2]).to.equal(4) + expect(args.value["foo"]).to.equal(8) + end) + + it("should disconnect from all upstream bindings", function() + local binding1, update1 = Binding.create(1) + local binding2, update2 = Binding.create(2) + + local joined = Binding.join({binding1, binding2}) + + local spy = createSpy() + local disconnect = Binding.subscribe(joined, spy.value) + + expect(spy.callCount).to.equal(0) + + update1(3) + expect(spy.callCount).to.equal(1) + + update2(3) + expect(spy.callCount).to.equal(2) + + disconnect() + update1(4) + expect(spy.callCount).to.equal(2) + + update2(2) + expect(spy.callCount).to.equal(2) + + local value = joined:getValue() + expect(value[1]).to.equal(4) + expect(value[2]).to.equal(2) + end) + + it("should be okay with calling disconnect multiple times", function() + local joined = Binding.join({}) + + local disconnect = Binding.subscribe(joined, function() end) + + disconnect() + disconnect() + end) + + it("should throw if updated directly", function() + local joined = Binding.join({}) + + expect(function() + Binding.update(joined, 0) + end) + end) + + it("should throw when a non-table value is passed", function() + GlobalConfig.scoped({ + typeChecks = true, + }, function() + expect(function() + Binding.join("hi") + end).to.throw() + end) + end) + + it("should throw when a non-binding value is passed via table", function() + GlobalConfig.scoped({ + typeChecks = true, + }, function() + expect(function() + local binding = Binding.create(123) + + Binding.join({ + binding, + "abcde", + }) + end).to.throw() + end) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.lua new file mode 100644 index 0000000..1dfb5fb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.lua @@ -0,0 +1,507 @@ +local assign = require(script.Parent.assign) +local ComponentLifecyclePhase = require(script.Parent.ComponentLifecyclePhase) +local Type = require(script.Parent.Type) +local Symbol = require(script.Parent.Symbol) +local invalidSetStateMessages = require(script.Parent.invalidSetStateMessages) +local internalAssert = require(script.Parent.internalAssert) + +local config = require(script.Parent.GlobalConfig).get() + +--[[ + Calling setState during certain lifecycle allowed methods has the potential + to create an infinitely updating component. Rather than time out, we exit + with an error if an unreasonable number of self-triggering updates occur +]] +local MAX_PENDING_UPDATES = 100 + +local InternalData = Symbol.named("InternalData") + +local componentMissingRenderMessage = [[ +The component %q is missing the `render` method. +`render` must be defined when creating a Roact component!]] + +local tooManyUpdatesMessage = [[ +The component %q has reached the setState update recursion limit. +When using `setState` in `didUpdate`, make sure that it won't repeat infinitely!]] + +local componentClassMetatable = {} + +function componentClassMetatable:__tostring() + return self.__componentName +end + +local Component = {} +setmetatable(Component, componentClassMetatable) + +Component[Type] = Type.StatefulComponentClass +Component.__index = Component +Component.__componentName = "Component" + +--[[ + A method called by consumers of Roact to create a new component class. + Components can not be extended beyond this point, with the exception of + PureComponent. +]] +function Component:extend(name) + if config.typeChecks then + assert(Type.of(self) == Type.StatefulComponentClass, "Invalid `self` argument to `extend`.") + assert(typeof(name) == "string", "Component class name must be a string") + end + + local class = {} + + for key, value in pairs(self) do + -- Roact opts to make consumers use composition over inheritance, which + -- lines up with React. + -- https://reactjs.org/docs/composition-vs-inheritance.html + if key ~= "extend" then + class[key] = value + end + end + + class[Type] = Type.StatefulComponentClass + class.__index = class + class.__componentName = name + + setmetatable(class, componentClassMetatable) + + return class +end + +function Component:__getDerivedState(incomingProps, incomingState) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__getDerivedState`") + end + + local internalData = self[InternalData] + local componentClass = internalData.componentClass + + if componentClass.getDerivedStateFromProps ~= nil then + local derivedState = componentClass.getDerivedStateFromProps(incomingProps, incomingState) + + if derivedState ~= nil then + if config.typeChecks then + assert(typeof(derivedState) == "table", "getDerivedStateFromProps must return a table!") + end + + return derivedState + end + end + + return nil +end + +function Component:setState(mapState) + if config.typeChecks then + assert(Type.of(self) == Type.StatefulComponentInstance, "Invalid `self` argument to `extend`.") + end + + local internalData = self[InternalData] + local lifecyclePhase = internalData.lifecyclePhase + + --[[ + When preparing to update, rendering, or unmounting, it is not safe + to call `setState` as it will interfere with in-flight updates. It's + also disallowed during unmounting + ]] + if lifecyclePhase == ComponentLifecyclePhase.ShouldUpdate or + lifecyclePhase == ComponentLifecyclePhase.WillUpdate or + lifecyclePhase == ComponentLifecyclePhase.Render or + lifecyclePhase == ComponentLifecyclePhase.WillUnmount + then + local messageTemplate = invalidSetStateMessages[internalData.lifecyclePhase] + + local message = messageTemplate:format(tostring(internalData.componentClass)) + + error(message, 2) + end + + local pendingState = internalData.pendingState + + local partialState + if typeof(mapState) == "function" then + partialState = mapState(pendingState or self.state, self.props) + + -- Abort the state update if the given state updater function returns nil + if partialState == nil then + return + end + elseif typeof(mapState) == "table" then + partialState = mapState + else + error("Invalid argument to setState, expected function or table", 2) + end + + local newState + if pendingState ~= nil then + newState = assign(pendingState, partialState) + else + newState = assign({}, self.state, partialState) + end + + if lifecyclePhase == ComponentLifecyclePhase.Init then + -- If `setState` is called in `init`, we can skip triggering an update! + local derivedState = self:__getDerivedState(self.props, newState) + self.state = assign(newState, derivedState) + + elseif lifecyclePhase == ComponentLifecyclePhase.DidMount or + lifecyclePhase == ComponentLifecyclePhase.DidUpdate or + lifecyclePhase == ComponentLifecyclePhase.ReconcileChildren + then + --[[ + During certain phases of the component lifecycle, it's acceptable to + allow `setState` but defer the update until we're done with ones in flight. + We do this by collapsing it into any pending updates we have. + ]] + local derivedState = self:__getDerivedState(self.props, newState) + internalData.pendingState = assign(newState, derivedState) + + elseif lifecyclePhase == ComponentLifecyclePhase.Idle then + -- Outside of our lifecycle, the state update is safe to make immediately + self:__update(nil, newState) + + else + local messageTemplate = invalidSetStateMessages.default + + local message = messageTemplate:format(tostring(internalData.componentClass)) + + error(message, 2) + end +end + +--[[ + Returns the stack trace of where the element was created that this component + instance's properties are based on. + + Intended to be used primarily by diagnostic tools. +]] +function Component:getElementTraceback() + return self[InternalData].virtualNode.currentElement.source +end + +--[[ + Returns a snapshot of this component given the current props and state. Must + be overridden by consumers of Roact and should be a pure function with + regards to props and state. + + TODO (#199): Accept props and state as arguments. +]] +function Component:render() + local internalData = self[InternalData] + + local message = componentMissingRenderMessage:format( + tostring(internalData.componentClass) + ) + + error(message, 0) +end + +--[[ + Retrieves the context value corresponding to the given key. Can return nil + if a requested context key is not present +]] +function Component:__getContext(key) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__getContext`") + internalAssert(key ~= nil, "Context key cannot be nil") + end + + local virtualNode = self[InternalData].virtualNode + local context = virtualNode.context + + return context[key] +end + +--[[ + Adds a new context entry to this component's context table (which will be + passed down to child components). +]] +function Component:__addContext(key, value) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__addContext`") + end + local virtualNode = self[InternalData].virtualNode + + -- Make sure we store a reference to the component's original, unmodified + -- context the virtual node. In the reconciler, we'll restore the original + -- context if we need to replace the node (this happens when a node gets + -- re-rendered as a different component) + if virtualNode.originalContext == nil then + virtualNode.originalContext = virtualNode.context + end + + -- Build a new context table on top of the existing one, then apply it to + -- our virtualNode + local existing = virtualNode.context + virtualNode.context = assign({}, existing, { [key] = value }) +end + +--[[ + Performs property validation if the static method validateProps is declared. + validateProps should follow assert's expected arguments: + (false, message: string) | true. The function may return a message in the + true case; it will be ignored. If this fails, the function will throw the + error. +]] +function Component:__validateProps(props) + if not config.propValidation then + return + end + + local validator = self[InternalData].componentClass.validateProps + + if validator == nil then + return + end + + if typeof(validator) ~= "function" then + error(("validateProps must be a function, but it is a %s.\nCheck the definition of the component %q."):format( + typeof(validator), + self.__componentName + )) + end + + local success, failureReason = validator(props) + + if not success then + failureReason = failureReason or "" + error(("Property validation failed: %s\n\n%s"):format( + tostring(failureReason), + self:getElementTraceback() or ""), + 0) + end +end + +--[[ + An internal method used by the reconciler to construct a new component + instance and attach it to the given virtualNode. +]] +function Component:__mount(reconciler, virtualNode) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentClass, "Invalid use of `__mount`") + internalAssert(Type.of(virtualNode) == Type.VirtualNode, "Expected arg #2 to be of type VirtualNode") + end + + local currentElement = virtualNode.currentElement + local hostParent = virtualNode.hostParent + + -- Contains all the information that we want to keep from consumers of + -- Roact, or even other parts of the codebase like the reconciler. + local internalData = { + reconciler = reconciler, + virtualNode = virtualNode, + componentClass = self, + lifecyclePhase = ComponentLifecyclePhase.Init, + } + + local instance = { + [Type] = Type.StatefulComponentInstance, + [InternalData] = internalData, + } + + setmetatable(instance, self) + + virtualNode.instance = instance + + local props = currentElement.props + + if self.defaultProps ~= nil then + props = assign({}, self.defaultProps, props) + end + + instance:__validateProps(props) + + instance.props = props + + local newContext = assign({}, virtualNode.legacyContext) + instance._context = newContext + + instance.state = assign({}, instance:__getDerivedState(instance.props, {})) + + if instance.init ~= nil then + instance:init(instance.props) + assign(instance.state, instance:__getDerivedState(instance.props, instance.state)) + end + + -- It's possible for init() to redefine _context! + virtualNode.legacyContext = instance._context + + internalData.lifecyclePhase = ComponentLifecyclePhase.Render + local renderResult = instance:render() + + internalData.lifecyclePhase = ComponentLifecyclePhase.ReconcileChildren + reconciler.updateVirtualNodeWithRenderResult(virtualNode, hostParent, renderResult) + + if instance.didMount ~= nil then + internalData.lifecyclePhase = ComponentLifecyclePhase.DidMount + instance:didMount() + end + + if internalData.pendingState ~= nil then + -- __update will handle pendingState, so we don't pass any new element or state + instance:__update(nil, nil) + end + + internalData.lifecyclePhase = ComponentLifecyclePhase.Idle +end + +--[[ + Internal method used by the reconciler to clean up any resources held by + this component instance. +]] +function Component:__unmount() + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__unmount`") + end + + local internalData = self[InternalData] + local virtualNode = internalData.virtualNode + local reconciler = internalData.reconciler + + if self.willUnmount ~= nil then + internalData.lifecyclePhase = ComponentLifecyclePhase.WillUnmount + self:willUnmount() + end + + for _, childNode in pairs(virtualNode.children) do + reconciler.unmountVirtualNode(childNode) + end +end + +--[[ + Internal method used by setState (to trigger updates based on state) and by + the reconciler (to trigger updates based on props) + + Returns true if the update was completed, false if it was cancelled by shouldUpdate +]] +function Component:__update(updatedElement, updatedState) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__update`") + internalAssert( + Type.of(updatedElement) == Type.Element or updatedElement == nil, + "Expected arg #1 to be of type Element or nil" + ) + internalAssert( + typeof(updatedState) == "table" or updatedState == nil, + "Expected arg #2 to be of type table or nil" + ) + end + + local internalData = self[InternalData] + local componentClass = internalData.componentClass + + local newProps = self.props + if updatedElement ~= nil then + newProps = updatedElement.props + + if componentClass.defaultProps ~= nil then + newProps = assign({}, componentClass.defaultProps, newProps) + end + + self:__validateProps(newProps) + end + + local updateCount = 0 + repeat + local finalState + local pendingState = nil + + -- Consume any pending state we might have + if internalData.pendingState ~= nil then + pendingState = internalData.pendingState + internalData.pendingState = nil + end + + -- Consume a standard update to state or props + if updatedState ~= nil or newProps ~= self.props then + if pendingState == nil then + finalState = updatedState or self.state + else + finalState = assign(pendingState, updatedState) + end + + local derivedState = self:__getDerivedState(newProps, finalState) + + if derivedState ~= nil then + finalState = assign({}, finalState, derivedState) + end + + updatedState = nil + else + finalState = pendingState + end + + if not self:__resolveUpdate(newProps, finalState) then + -- If the update was short-circuited, bubble the result up to the caller + return false + end + + updateCount = updateCount + 1 + + if updateCount > MAX_PENDING_UPDATES then + error(tooManyUpdatesMessage:format(tostring(internalData.componentClass)), 3) + end + until internalData.pendingState == nil + + return true +end + +--[[ + Internal method used by __update to apply new props and state + + Returns true if the update was completed, false if it was cancelled by shouldUpdate +]] +function Component:__resolveUpdate(incomingProps, incomingState) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__resolveUpdate`") + end + + local internalData = self[InternalData] + local virtualNode = internalData.virtualNode + local reconciler = internalData.reconciler + + local oldProps = self.props + local oldState = self.state + + if incomingProps == nil then + incomingProps = oldProps + end + if incomingState == nil then + incomingState = oldState + end + + if self.shouldUpdate ~= nil then + internalData.lifecyclePhase = ComponentLifecyclePhase.ShouldUpdate + local continueWithUpdate = self:shouldUpdate(incomingProps, incomingState) + + if not continueWithUpdate then + internalData.lifecyclePhase = ComponentLifecyclePhase.Idle + return false + end + end + + if self.willUpdate ~= nil then + internalData.lifecyclePhase = ComponentLifecyclePhase.WillUpdate + self:willUpdate(incomingProps, incomingState) + end + + internalData.lifecyclePhase = ComponentLifecyclePhase.Render + + self.props = incomingProps + self.state = incomingState + + local renderResult = virtualNode.instance:render() + + internalData.lifecyclePhase = ComponentLifecyclePhase.ReconcileChildren + reconciler.updateVirtualNodeWithRenderResult(virtualNode, virtualNode.hostParent, renderResult) + + if self.didUpdate ~= nil then + internalData.lifecyclePhase = ComponentLifecyclePhase.DidUpdate + self:didUpdate(oldProps, oldState) + end + + internalData.lifecyclePhase = ComponentLifecyclePhase.Idle + return true +end + +return Component \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/context.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/context.spec.lua new file mode 100644 index 0000000..1346a33 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/context.spec.lua @@ -0,0 +1,297 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local oneChild = require(script.Parent.Parent.oneChild) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be provided as an internal api on Component", function() + local Provider = Component:extend("Provider") + + function Provider:init() + self:__addContext("foo", "bar") + end + + function Provider:render() + end + + local element = createElement(Provider) + local hostParent = nil + local hostKey = "Provider" + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContext = { + foo = "bar", + } + + assertDeepEqual(node.context, expectedContext) + end) + + it("should be inherited from parent stateful nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + hello = self:__getContext("hello"), + value = self:__getContext("value"), + } + end + + function Consumer:render() + end + + local Parent = Component:extend("Parent") + + function Parent:render() + return createElement(Consumer) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local context = { + hello = "world", + value = 6, + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, context) + + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.context) + assertDeepEqual(node.context, context) + assertDeepEqual(capturedContext, context) + end) + + it("should be inherited from parent function nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + hello = self:__getContext("hello"), + value = self:__getContext("value"), + } + end + + function Consumer:render() + end + + local function Parent() + return createElement(Consumer) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local context = { + hello = "world", + value = 6, + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, context) + + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.context) + assertDeepEqual(node.context, context) + assertDeepEqual(capturedContext, context) + end) + + it("should not copy the context table if it doesn't need to", function() + local Parent = Component:extend("Parent") + + function Parent:init() + self:__addContext("parent", "I'm here!") + end + + function Parent:render() + -- Create some child element + return createElement(function() end) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local parentNode = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContext = { + parent = "I'm here!", + } + + assertDeepEqual(parentNode.context, expectedContext) + + local childNode = oneChild(parentNode.children) + + -- Parent and child should have the same context table + expect(parentNode.context).to.equal(childNode.context) + end) + + it("should not allow context to move up the tree", function() + local ChildProvider = Component:extend("ChildProvider") + + function ChildProvider:init() + self:__addContext("child", "I'm here too!") + end + + function ChildProvider:render() + end + + local ParentProvider = Component:extend("ParentProvider") + + function ParentProvider:init() + self:__addContext("parent", "I'm here!") + end + + function ParentProvider:render() + return createElement(ChildProvider) + end + + local element = createElement(ParentProvider) + local hostParent = nil + local hostKey = "Parent" + + local parentNode = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + local childNode = oneChild(parentNode.children) + + local expectedParentContext = { + parent = "I'm here!", + -- Context does not travel back up + } + + local expectedChildContext = { + parent = "I'm here!", + child = "I'm here too!" + } + + assertDeepEqual(parentNode.context, expectedParentContext) + assertDeepEqual(childNode.context, expectedChildContext) + end) + + it("should contain values put into the tree by parent nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + dont = self:__getContext("dont"), + frob = self:__getContext("frob"), + } + end + + function Consumer:render() + end + + local Provider = Component:extend("Provider") + + function Provider:init() + self:__addContext("frob", "ulator") + end + + function Provider:render() + return createElement(Consumer) + end + + local element = createElement(Provider) + local hostParent = nil + local hostKey = "Consumer" + local context = { + dont = "try it", + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, context) + + local initialContext = { + dont = "try it", + } + + local expectedContext = { + dont = "try it", + frob = "ulator", + } + + -- Because components mutate context, we're careful with equality + expect(node.context).never.to.equal(context) + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.context) + + assertDeepEqual(context, initialContext) + assertDeepEqual(node.context, expectedContext) + assertDeepEqual(capturedContext, expectedContext) + end) + + it("should transfer context to children that are replaced", function() + local ConsumerA = Component:extend("ConsumerA") + + local function captureAllContext(component) + return { + A = component:__getContext("A"), + B = component:__getContext("B"), + frob = component:__getContext("frob"), + } + end + + local capturedContextA + function ConsumerA:init() + self:__addContext("A", "hello") + + capturedContextA = captureAllContext(self) + end + + function ConsumerA:render() + end + + local ConsumerB = Component:extend("ConsumerB") + + local capturedContextB + function ConsumerB:init() + self:__addContext("B", "hello") + + capturedContextB = captureAllContext(self) + end + + function ConsumerB:render() + end + + local Provider = Component:extend("Provider") + + function Provider:init() + self:__addContext("frob", "ulator") + end + + function Provider:render() + local useConsumerB = self.props.useConsumerB + + if useConsumerB then + return createElement(ConsumerB) + else + return createElement(ConsumerA) + end + end + + local hostParent = nil + local hostKey = "Consumer" + + local element = createElement(Provider) + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContextA = { + frob = "ulator", + A = "hello", + } + + assertDeepEqual(capturedContextA, expectedContextA) + + local expectedContextB = { + frob = "ulator", + B = "hello", + } + + local replacedElement = createElement(Provider, { + useConsumerB = true, + }) + noopReconciler.updateVirtualNode(node, replacedElement) + + assertDeepEqual(capturedContextB, expectedContextB) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/defaultProps.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/defaultProps.spec.lua new file mode 100644 index 0000000..40edca0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/defaultProps.spec.lua @@ -0,0 +1,126 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local None = require(script.Parent.Parent.None) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should fill in when mounting before init", function() + local defaultProps = { + a = 3, + b = 2, + } + + local Foo = Component:extend("Foo") + + Foo.defaultProps = defaultProps + + local capturedProps + function Foo:init() + capturedProps = self.props + end + + function Foo:render() + end + + local initialProps = { + b = 4, + c = 6, + } + + local element = createElement(Foo, initialProps) + local hostParent = nil + local key = "Some Foo" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + local expectedProps = { + a = defaultProps.a, + b = initialProps.b, + c = initialProps.c, + } + + assertDeepEqual(capturedProps, expectedProps) + end) + + it("should fill in when updating via props", function() + local defaultProps = { + a = 3, + b = 2, + } + + local Foo = Component:extend("Foo") + + Foo.defaultProps = defaultProps + + local capturedProps + function Foo:render() + capturedProps = self.props + end + + local initialProps = { + b = 4, + c = 6, + } + + local element = createElement(Foo, initialProps) + local hostParent = nil + local key = "Some Foo" + + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + + local updatedProps = { + c = 5, + } + local updatedElement = createElement(Foo, updatedProps) + + noopReconciler.updateVirtualNode(node, updatedElement) + + local expectedProps = { + a = defaultProps.a, + b = defaultProps.b, + c = updatedProps.c, + } + + assertDeepEqual(capturedProps, expectedProps) + end) + + it("should respect None to override a default prop with nil", function() + local defaultProps = { + a = 3, + b = 2, + } + + local Foo = Component:extend("Foo") + + Foo.defaultProps = defaultProps + + local capturedProps + function Foo:render() + capturedProps = self.props + end + + local initialProps = { + b = None, + c = 4, + } + + local element = createElement(Foo, initialProps) + local hostParent = nil + local key = "Some Foo" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + local expectedProps = { + a = defaultProps.a, + b = nil, + c = initialProps.c, + } + + assertDeepEqual(capturedProps, expectedProps) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/didMount.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/didMount.spec.lua new file mode 100644 index 0000000..b728629 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/didMount.spec.lua @@ -0,0 +1,35 @@ +return function() + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked when mounted", function() + local MyComponent = Component:extend("MyComponent") + + local didMountSpy = createSpy() + + MyComponent.didMount = didMountSpy.value + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(didMountSpy.callCount).to.equal(1) + + local values = didMountSpy:captureValues("self") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/didUpdate.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/didUpdate.spec.lua new file mode 100644 index 0000000..269c6f4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/didUpdate.spec.lua @@ -0,0 +1,92 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked when updated via updateVirtualNode", function() + local MyComponent = Component:extend("MyComponent") + + local didUpdateSpy = createSpy() + MyComponent.didUpdate = didUpdateSpy.value + + function MyComponent:render() + return nil + end + + local initialProps = { + a = 5, + } + local initialElement = createElement(MyComponent, initialProps) + local hostParent = nil + local key = "Test" + + local virtualNode = noopReconciler.mountVirtualNode(initialElement, hostParent, key) + + expect(didUpdateSpy.callCount).to.equal(0) + + local newProps = { + a = 6, + b = 2, + } + local newElement = createElement(MyComponent, newProps) + noopReconciler.updateVirtualNode(virtualNode, newElement) + + expect(didUpdateSpy.callCount).to.equal(1) + + local values = didUpdateSpy:captureValues("self", "oldProps", "oldState") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + assertDeepEqual(values.oldProps, initialProps) + assertDeepEqual(values.oldState, {}) + end) + + it("should be invoked when updated via setState", function() + local MyComponent = Component:extend("MyComponent") + + local didUpdateSpy = createSpy() + MyComponent.didUpdate = didUpdateSpy.value + + local initialState = { + a = 4, + } + + local setState + function MyComponent:init() + setState = function(...) + return self:setState(...) + end + + self:setState(initialState) + end + + function MyComponent:render() + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(didUpdateSpy.callCount).to.equal(0) + + setState({ + a = 5, + }) + + expect(didUpdateSpy.callCount).to.equal(1) + + local values = didUpdateSpy:captureValues("self", "oldProps", "oldState") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + assertDeepEqual(values.oldProps, {}) + assertDeepEqual(values.oldState, initialState) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/extend.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/extend.spec.lua new file mode 100644 index 0000000..04a433a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/extend.spec.lua @@ -0,0 +1,29 @@ +return function() + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + it("should be extendable", function() + local MyComponent = Component:extend("The Senate") + + expect(MyComponent).to.be.ok() + expect(Type.of(MyComponent)).to.equal(Type.StatefulComponentClass) + end) + + it("should prevent extending a user component", function() + local MyComponent = Component:extend("Sheev") + + expect(function() + MyComponent:extend("Frank") + end).to.throw() + end) + + it("should use a given name", function() + local MyComponent = Component:extend("FooBar") + + local name = tostring(MyComponent) + + expect(name).to.be.a("string") + expect(name:find("FooBar")).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/getDerivedStateFromProps.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/getDerivedStateFromProps.spec.lua new file mode 100644 index 0000000..1f04cc8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/getDerivedStateFromProps.spec.lua @@ -0,0 +1,279 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createSpy = require(script.Parent.Parent.createSpy) + local createElement = require(script.Parent.Parent.createElement) + local createFragment = require(script.Parent.Parent.createFragment) + local createReconciler = require(script.Parent.Parent.createReconciler) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked on initial mount", function() + local getDerivedSpy = createSpy() + local WithDerivedState = Component:extend("WithDerivedState") + + WithDerivedState.getDerivedStateFromProps = getDerivedSpy.value + + function WithDerivedState:render() + return nil + end + + local element = createElement(WithDerivedState, { + someProp = 1, + }) + local hostParent = nil + local hostKey = "WithDerivedState" + + noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + expect(getDerivedSpy.callCount).to.equal(1) + + local values = getDerivedSpy:captureValues("props", "state") + + assertDeepEqual(values.props, { someProp = 1 }) + assertDeepEqual(values.state, {}) + end) + + it("should be invoked when updated via props", function() + local getDerivedSpy = createSpy() + local WithDerivedState = Component:extend("WithDerivedState") + + WithDerivedState.getDerivedStateFromProps = getDerivedSpy.value + + function WithDerivedState:render() + return nil + end + + local hostParent = nil + local hostKey = "WithDerivedState" + + local node = noopReconciler.mountVirtualNode(createElement(WithDerivedState, { + someProp = 1, + }), hostParent, hostKey) + + noopReconciler.updateVirtualNode(node, createElement(WithDerivedState, { + someProp = 2, + })) + + expect(getDerivedSpy.callCount).to.equal(2) + + local values = getDerivedSpy:captureValues("props", "state") + + assertDeepEqual(values.props, { someProp = 2 }) + assertDeepEqual(values.state, {}) + end) + + it("should be invoked when updated via state", function() + local getDerivedSpy = createSpy() + local WithDerivedState = Component:extend("WithDerivedState") + + WithDerivedState.getDerivedStateFromProps = getDerivedSpy.value + + function WithDerivedState:init() + self:setState({ + someState = 1, + }) + end + + function WithDerivedState:render() + return nil + end + + local element = createElement(WithDerivedState) + local hostParent = nil + local hostKey = "WithDerivedState" + + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + noopReconciler.updateVirtualNode(node, element, { + someState = 2, + }) + + -- getDerivedStateFromProps will be called: + -- * Once on empty props + -- * Once during the self:setState in init + -- * Once more, defensively, on the resulting state AFTER init + -- * On updating with new state via updateVirtualNode + expect(getDerivedSpy.callCount).to.equal(4) + + local values = getDerivedSpy:captureValues("props", "state") + + assertDeepEqual(values.props, {}) + assertDeepEqual(values.state, { someState = 2 }) + end) + + it("should be invoked when updating via state in init (which skips reconciliation)", function() + local getDerivedSpy = createSpy() + local WithDerivedState = Component:extend("WithDerivedState") + + WithDerivedState.getDerivedStateFromProps = getDerivedSpy.value + + function WithDerivedState:init() + self:setState({ + stateFromInit = 1, + }) + end + + function WithDerivedState:render() + return nil + end + + local element = createElement(WithDerivedState, { + someProp = 1, + }) + local hostParent = nil + local hostKey = "WithDerivedState" + + noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + -- getDerivedStateFromProps will be called: + -- * Once on empty props + -- * Once during the self:setState in init + -- * Once more, defensively, on the resulting state AFTER init + expect(getDerivedSpy.callCount).to.equal(3) + + local values = getDerivedSpy:captureValues("props", "state") + + assertDeepEqual(values.props, { + someProp = 1, + }) + assertDeepEqual(values.state, { + stateFromInit = 1, + }) + end) + + it("should receive defaultProps", function() + local getDerivedSpy = createSpy() + local WithDerivedState = Component:extend("WithDerivedState") + + WithDerivedState.defaultProps = { + someDefaultProp = "foo", + } + + WithDerivedState.getDerivedStateFromProps = getDerivedSpy.value + + function WithDerivedState:render() + return nil + end + + local element = createElement(WithDerivedState, { + someProp = 1, + }) + local hostParent = nil + local hostKey = "WithDerivedState" + + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + expect(getDerivedSpy.callCount).to.equal(1) + + local values = getDerivedSpy:captureValues("props", "state") + + assertDeepEqual(values.props, { + someDefaultProp = "foo", + someProp = 1, + }) + + -- Update via props, confirm that defaultProp is still present + element = createElement(WithDerivedState, { + someProp = 2, + }) + + noopReconciler.updateVirtualNode(node, element) + + expect(getDerivedSpy.callCount).to.equal(2) + + values = getDerivedSpy:captureValues("props", "state") + + assertDeepEqual(values.props, { + someDefaultProp = "foo", + someProp = 2, + }) + end) + + it("should derive state for all setState updates, even when deferred", function() + local Child = Component:extend("Child") + local stateUpdaterSpy = createSpy(function() + return {} + end) + local stateDerivedSpy = createSpy() + + function Child:render() + return nil + end + + function Child:didMount() + self.props.callback() + end + + local Parent = Component:extend("Parent") + + Parent.getDerivedStateFromProps = stateDerivedSpy.value + + function Parent:render() + local callback = function() + self:setState(stateUpdaterSpy.value) + end + + return createFragment({ + ChildA = createElement(Child, { + callback = callback, + }), + ChildB = createElement(Child, { + callback = callback, + }), + }) + end + + local element = createElement(Parent) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(stateUpdaterSpy.callCount).to.equal(2) + + -- getDerivedStateFromProps is always called on initial state + expect(stateDerivedSpy.callCount).to.equal(3) + end) + + it("should have derived state after assigning to state in init", function() + local getStateCallback + local getDerivedSpy = createSpy(function() + return { + derived = true, + } + end) + local WithDerivedState = Component:extend("WithDerivedState") + + WithDerivedState.getDerivedStateFromProps = getDerivedSpy.value + + function WithDerivedState:init() + self.state = { + init = true, + } + + getStateCallback = function() + return self.state + end + end + + function WithDerivedState:render() + return nil + end + + local hostParent = nil + local hostKey = "WithDerivedState" + local element = createElement(WithDerivedState) + + noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + expect(getDerivedSpy.callCount).to.equal(2) + + assertDeepEqual(getStateCallback(), { + init = true, + derived = true, + }) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/getElementTraceback.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/getElementTraceback.spec.lua new file mode 100644 index 0000000..1d3e0ac --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/getElementTraceback.spec.lua @@ -0,0 +1,67 @@ +return function() + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local GlobalConfig = require(script.Parent.Parent.GlobalConfig) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should return stack traces in initial renders", function() + local TestComponent = Component:extend("TestComponent") + + local stackTrace + function TestComponent:init() + stackTrace = self:getElementTraceback() + end + + function TestComponent:render() + return nil + end + + local config = { + elementTracing = true, + } + + GlobalConfig.scoped(config, function() + local element = createElement(TestComponent) + local hostParent = nil + local key = "Some key" + + noopReconciler.mountVirtualNode(element, hostParent, key) + end) + + expect(stackTrace).to.be.a("string") + end) + + itSKIP("it should return an updated stack trace after an update", function() end) + + it("should return nil when elementTracing is off", function() + local stackTrace = nil + + local config = { + elementTracing = false, + } + + local TestComponent = Component:extend("TestComponent") + + function TestComponent:init() + stackTrace = self:getElementTraceback() + end + + function TestComponent:render() + return nil + end + + GlobalConfig.scoped(config, function() + local element = createElement(TestComponent) + local hostParent = nil + local key = "Some key" + + noopReconciler.mountVirtualNode(element, hostParent, key) + end) + + expect(stackTrace).to.equal(nil) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/init.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/init.spec.lua new file mode 100644 index 0000000..af50997 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/init.spec.lua @@ -0,0 +1,41 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked with props when mounted", function() + local MyComponent = Component:extend("MyComponent") + + local initSpy = createSpy() + + MyComponent.init = initSpy.value + + function MyComponent:render() + return nil + end + + local props = { + a = 5, + } + local element = createElement(MyComponent, props) + local hostParent = nil + local key = "Some Component Key" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(initSpy.callCount).to.equal(1) + + local values = initSpy:captureValues("self", "props") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + expect(typeof(values.props)).to.equal("table") + assertDeepEqual(values.props, props) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/legacyContext.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/legacyContext.spec.lua new file mode 100644 index 0000000..e1014f2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/legacyContext.spec.lua @@ -0,0 +1,209 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be provided as a mutable self._context in Component:init", function() + local Provider = Component:extend("Provider") + + function Provider:init() + self._context.foo = "bar" + end + + function Provider:render() + end + + local element = createElement(Provider) + local hostParent = nil + local hostKey = "Provider" + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContext = { + foo = "bar", + } + + assertDeepEqual(node.legacyContext, expectedContext) + end) + + it("should be inherited from parent stateful nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = self._context + end + + function Consumer:render() + end + + local Parent = Component:extend("Parent") + + function Parent:render() + return createElement(Consumer) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local context = { + hello = "world", + value = 6, + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) + + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.legacyContext) + assertDeepEqual(node.legacyContext, context) + assertDeepEqual(capturedContext, context) + end) + + it("should be inherited from parent function nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = self._context + end + + function Consumer:render() + end + + local function Parent() + return createElement(Consumer) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local context = { + hello = "world", + value = 6, + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) + + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.legacyContext) + assertDeepEqual(node.legacyContext, context) + assertDeepEqual(capturedContext, context) + end) + + it("should contain values put into the tree by parent nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = self._context + end + + function Consumer:render() + end + + local Provider = Component:extend("Provider") + + function Provider:init() + self._context.frob = "ulator" + end + + function Provider:render() + return createElement(Consumer) + end + + local element = createElement(Provider) + local hostParent = nil + local hostKey = "Consumer" + local context = { + dont = "try it", + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) + + local initialContext = { + dont = "try it", + } + + local expectedContext = { + dont = "try it", + frob = "ulator", + } + + -- Because components mutate context, we're careful with equality + expect(node.legacyContext).never.to.equal(context) + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.legacyContext) + + assertDeepEqual(context, initialContext) + assertDeepEqual(node.legacyContext, expectedContext) + assertDeepEqual(capturedContext, expectedContext) + end) + + it("should transfer context to children that are replaced", function() + local ConsumerA = Component:extend("ConsumerA") + + local capturedContextA + function ConsumerA:init() + self._context.A = "hello" + + capturedContextA = self._context + end + + function ConsumerA:render() + end + + local ConsumerB = Component:extend("ConsumerB") + + local capturedContextB + function ConsumerB:init() + self._context.B = "hello" + + capturedContextB = self._context + end + + function ConsumerB:render() + end + + local Provider = Component:extend("Provider") + + function Provider:init() + self._context.frob = "ulator" + end + + function Provider:render() + local useConsumerB = self.props.useConsumerB + + if useConsumerB then + return createElement(ConsumerB) + else + return createElement(ConsumerA) + end + end + + local hostParent = nil + local hostKey = "Consumer" + + local element = createElement(Provider) + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContextA = { + frob = "ulator", + A = "hello", + } + + assertDeepEqual(capturedContextA, expectedContextA) + + local expectedContextB = { + frob = "ulator", + B = "hello", + } + + local replacedElement = createElement(Provider, { + useConsumerB = true, + }) + noopReconciler.updateVirtualNode(node, replacedElement) + + assertDeepEqual(capturedContextB, expectedContextB) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/render.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/render.spec.lua new file mode 100644 index 0000000..8dac00a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/render.spec.lua @@ -0,0 +1,150 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should throw on mount if not overridden", function() + local MyComponent = Component:extend("MyComponent") + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + local success, result = pcall(function() + noopReconciler.mountVirtualNode(element, hostParent, key) + end) + + expect(success).to.equal(false) + expect(result:match("MyComponent")).to.be.ok() + expect(result:match("render")).to.be.ok() + end) + + it("should be invoked when a component is mounted", function() + local Foo = Component:extend("Foo") + + local capturedProps + local capturedState + local renderSpy = createSpy(function(self) + capturedProps = self.props + capturedState = self.state + end) + Foo.render = renderSpy.value + + local element = createElement(Foo) + local hostParent = nil + local key = "Foo Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(renderSpy.callCount).to.equal(1) + + local renderArguments = renderSpy:captureValues("self") + + expect(Type.of(renderArguments.self)).to.equal(Type.StatefulComponentInstance) + assertDeepEqual(capturedProps, {}) + assertDeepEqual(capturedState, {}) + end) + + it("should be invoked when a component is updated via props", function() + local Foo = Component:extend("Foo") + + local capturedProps + local capturedState + local renderSpy = createSpy(function(self) + capturedProps = self.props + capturedState = self.state + end) + Foo.render = renderSpy.value + + local initialProps = { + a = 2, + } + local element = createElement(Foo, initialProps) + local hostParent = nil + local key = "Foo Test" + + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(renderSpy.callCount).to.equal(1) + + local firstRenderArguments = renderSpy:captureValues("self") + local firstProps = capturedProps + local firstState = capturedState + + expect(Type.of(firstRenderArguments.self)).to.equal(Type.StatefulComponentInstance) + assertDeepEqual(firstProps, initialProps) + assertDeepEqual(firstState, {}) + + local updatedProps = { + a = 3, + } + local newElement = createElement(Foo, updatedProps) + + noopReconciler.updateVirtualNode(node, newElement) + + expect(renderSpy.callCount).to.equal(2) + + local secondRenderArguments = renderSpy:captureValues("self") + local secondProps = capturedProps + local secondState = capturedState + + expect(Type.of(secondRenderArguments.self)).to.equal(Type.StatefulComponentInstance) + expect(secondProps).never.to.equal(firstProps) + assertDeepEqual(secondProps, updatedProps) + expect(secondState).to.equal(firstState) + end) + + it("should be invoked when a component is updated via state", function() + local Foo = Component:extend("Foo") + + local setState + function Foo:init() + setState = function(...) + return self:setState(...) + end + end + + local capturedProps + local capturedState + local renderSpy = createSpy(function(self) + capturedProps = self.props + capturedState = self.state + end) + Foo.render = renderSpy.value + + local element = createElement(Foo) + local hostParent = nil + local key = "Foo Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(renderSpy.callCount).to.equal(1) + + local firstRenderArguments = renderSpy:captureValues("self") + local firstProps = capturedProps + local firstState = capturedState + + expect(Type.of(firstRenderArguments.self)).to.equal(Type.StatefulComponentInstance) + + setState({}) + + expect(renderSpy.callCount).to.equal(2) + + local renderArguments = renderSpy:captureValues("self") + + expect(Type.of(renderArguments.self)).to.equal(Type.StatefulComponentInstance) + expect(capturedProps).to.equal(firstProps) + expect(capturedState).never.to.equal(firstState) + end) + + itSKIP("Test defaultProps on initial render", function() end) + itSKIP("Test defaultProps on prop update", function() end) + itSKIP("Test defaultProps on state update", function() end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/setState.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/setState.spec.lua new file mode 100644 index 0000000..88ae9f5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/setState.spec.lua @@ -0,0 +1,602 @@ +return function() + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local None = require(script.Parent.Parent.None) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + describe("setState", function() + it("should not trigger an extra update when called in init", function() + local renderCount = 0 + local updateCount = 0 + local capturedState + + local InitComponent = Component:extend("InitComponent") + + function InitComponent:init() + self:setState({ + a = 1 + }) + end + + function InitComponent:willUpdate() + updateCount = updateCount + 1 + end + + function InitComponent:render() + renderCount = renderCount + 1 + capturedState = self.state + return nil + end + + local initElement = createElement(InitComponent) + + noopReconciler.mountVirtualTree(initElement) + + expect(renderCount).to.equal(1) + expect(updateCount).to.equal(0) + expect(capturedState.a).to.equal(1) + end) + + it("should throw when called in render", function() + local TestComponent = Component:extend("TestComponent") + + function TestComponent:render() + self:setState({ + a = 1 + }) + end + + local renderElement = createElement(TestComponent) + + local success, result = pcall(noopReconciler.mountVirtualTree, renderElement) + + expect(success).to.equal(false) + expect(result:match("render")).to.be.ok() + expect(result:match("TestComponent")).to.be.ok() + end) + + it("should throw when called in shouldUpdate", function() + local TestComponent = Component:extend("TestComponent") + + function TestComponent:render() + return nil + end + + function TestComponent:shouldUpdate() + self:setState({ + a = 1 + }) + end + + local initialElement = createElement(TestComponent) + local updatedElement = createElement(TestComponent) + + local tree = noopReconciler.mountVirtualTree(initialElement) + + local success, result = pcall(noopReconciler.updateVirtualTree, tree, updatedElement) + + expect(success).to.equal(false) + expect(result:match("shouldUpdate")).to.be.ok() + expect(result:match("TestComponent")).to.be.ok() + end) + + it("should throw when called in willUpdate", function() + local TestComponent = Component:extend("TestComponent") + + function TestComponent:render() + return nil + end + + function TestComponent:willUpdate() + self:setState({ + a = 1 + }) + end + + local initialElement = createElement(TestComponent) + local updatedElement = createElement(TestComponent) + local tree = noopReconciler.mountVirtualTree(initialElement) + + local success, result = pcall(noopReconciler.updateVirtualTree, tree, updatedElement) + + expect(success).to.equal(false) + expect(result:match("willUpdate")).to.be.ok() + expect(result:match("TestComponent")).to.be.ok() + end) + + it("should throw when called in willUnmount", function() + local TestComponent = Component:extend("TestComponent") + + function TestComponent:render() + return nil + end + + function TestComponent:willUnmount() + self:setState({ + a = 1 + }) + end + + local element = createElement(TestComponent) + local tree = noopReconciler.mountVirtualTree(element) + + local success, result = pcall(noopReconciler.unmountVirtualTree, tree) + + expect(success).to.equal(false) + expect(result:match("willUnmount")).to.be.ok() + expect(result:match("TestComponent")).to.be.ok() + end) + + it("should remove values from state when the value is None", function() + local TestComponent = Component:extend("TestComponent") + local setStateCallback, getStateCallback + + function TestComponent:init() + setStateCallback = function(newState) + self:setState(newState) + end + + getStateCallback = function() + return self.state + end + + self:setState({ + value = 0 + }) + end + + function TestComponent:render() + return nil + end + + local element = createElement(TestComponent) + local instance = noopReconciler.mountVirtualNode(element, nil, "Test") + + expect(getStateCallback().value).to.equal(0) + + setStateCallback({ + value = None + }) + + expect(getStateCallback().value).to.equal(nil) + + noopReconciler.unmountVirtualNode(instance) + end) + + it("should invoke functions to compute a partial state", function() + local TestComponent = Component:extend("TestComponent") + local setStateCallback, getStateCallback, getPropsCallback + + function TestComponent:init() + setStateCallback = function(newState) + self:setState(newState) + end + + getStateCallback = function() + return self.state + end + + getPropsCallback = function() + return self.props + end + + self:setState({ + value = 0 + }) + end + + function TestComponent:render() + return nil + end + + local element = createElement(TestComponent) + local instance = noopReconciler.mountVirtualNode(element, nil, "Test") + + expect(getStateCallback().value).to.equal(0) + + setStateCallback(function(state, props) + expect(state).to.equal(getStateCallback()) + expect(props).to.equal(getPropsCallback()) + + return { + value = state.value + 1 + } + end) + + expect(getStateCallback().value).to.equal(1) + + noopReconciler.unmountVirtualNode(instance) + end) + + it("should cancel rendering if the function returns nil", function() + local TestComponent = Component:extend("TestComponent") + local setStateCallback + local renderCount = 0 + + function TestComponent:init() + setStateCallback = function(newState) + self:setState(newState) + end + + self:setState({ + value = 0 + }) + end + + function TestComponent:render() + renderCount = renderCount + 1 + return nil + end + + local element = createElement(TestComponent) + local instance = noopReconciler.mountVirtualNode(element, nil, "Test") + expect(renderCount).to.equal(1) + + setStateCallback(function(state, props) + return nil + end) + + expect(renderCount).to.equal(1) + + noopReconciler.unmountVirtualNode(instance) + end) + end) + + describe("setState suspension", function() + it("should defer setState triggered while reconciling", function() + local Child = Component:extend("Child") + local getParentStateCallback + + function Child:render() + return nil + end + + function Child:didMount() + self.props.callback() + end + + local Parent = Component:extend("Parent") + + function Parent:init() + getParentStateCallback = function() + return self.state + end + end + + function Parent:render() + return createElement(Child, { + callback = function() + self:setState({ + foo = "bar" + }) + end, + }) + end + + local element = createElement(Parent) + local hostParent = nil + local key = "Test" + + local result = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(result).to.be.ok() + expect(getParentStateCallback().foo).to.equal("bar") + end) + + it("should defer setState triggered while reconciling during an update", function() + local Child = Component:extend("Child") + local getParentStateCallback + + function Child:render() + return nil + end + + function Child:didUpdate() + self.props.callback() + end + + local Parent = Component:extend("Parent") + + function Parent:init() + getParentStateCallback = function() + return self.state + end + end + + function Parent:render() + return createElement(Child, { + callback = function() + -- This guards against a stack overflow that would be OUR fault + if not self.state.foo then + self:setState({ + foo = "bar" + }) + end + end, + }) + end + + local element = createElement(Parent) + local hostParent = nil + local key = "Test" + + local result = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(result).to.be.ok() + expect(getParentStateCallback().foo).to.equal(nil) + + result = noopReconciler.updateVirtualNode(result, createElement(Parent)) + + expect(result).to.be.ok() + expect(getParentStateCallback().foo).to.equal("bar") + + noopReconciler.unmountVirtualNode(result) + end) + + it("should combine pending state changes properly", function() + local Child = Component:extend("Child") + local getParentStateCallback + + function Child:render() + return nil + end + + function Child:didMount() + self.props.callback("foo", 1) + self.props.callback("bar", 3) + end + + local Parent = Component:extend("Parent") + + function Parent:init() + getParentStateCallback = function() + return self.state + end + end + + function Parent:render() + return createElement(Child, { + callback = function(key, value) + self:setState({ + [key] = value, + }) + end, + }) + end + + local element = createElement(Parent) + local hostParent = nil + local key = "Test" + + local result = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(result).to.be.ok() + expect(getParentStateCallback().foo).to.equal(1) + expect(getParentStateCallback().bar).to.equal(3) + + noopReconciler.unmountVirtualNode(result) + end) + + it("should abort properly when functional setState returns nil while deferred", function() + local Child = Component:extend("Child") + + function Child:render() + return nil + end + + function Child:didMount() + self.props.callback() + end + + local Parent = Component:extend("Parent") + + local renderSpy = createSpy(function(self) + return createElement(Child, { + callback = function() + self:setState(function() + -- abort the setState + return nil + end) + end, + }) + end) + + Parent.render = renderSpy.value + + local element = createElement(Parent) + local hostParent = nil + local key = "Test" + + local result = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(result).to.be.ok() + expect(renderSpy.callCount).to.equal(1) + + noopReconciler.unmountVirtualNode(result) + end) + + it("should still apply pending state if a subsequent state update was aborted", function() + local Child = Component:extend("Child") + local getParentStateCallback + + function Child:render() + return nil + end + + function Child:didMount() + self.props.callback(function() + return { + foo = 1, + } + end) + self.props.callback(function() + return nil + end) + end + + local Parent = Component:extend("Parent") + + function Parent:init() + getParentStateCallback = function() + return self.state + end + end + + function Parent:render() + return createElement(Child, { + callback = function(stateUpdater) + self:setState(stateUpdater) + end, + }) + end + + local element = createElement(Parent) + local hostParent = nil + local key = "Test" + + local result = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(result).to.be.ok() + expect(getParentStateCallback().foo).to.equal(1) + + noopReconciler.unmountVirtualNode(result) + end) + + it("should not re-process new state when pending state is present after update", function() + local setComponentState + local getComponentState + + local MyComponent = Component:extend("MyComponent") + + function MyComponent:init() + self:setState({ + hasUpdatedOnce = false, + counter = 0, + }) + + setComponentState = function(mapState) + self:setState(mapState) + end + + getComponentState = function() + return self.state + end + end + + function MyComponent:render() + return nil + end + + function MyComponent:didUpdate() + if self.state.hasUpdatedOnce == false then + self:setState({ + hasUpdatedOnce = true, + }) + end + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(getComponentState().hasUpdatedOnce).to.equal(false) + expect(getComponentState().counter).to.equal(0) + + setComponentState(function(state) + return { + counter = state.counter + 1 + } + end) + + expect(getComponentState().hasUpdatedOnce).to.equal(true) + expect(getComponentState().counter).to.equal(1) + end) + + it("should throw when an infinite update is triggered", function() + local InfiniteUpdater = Component:extend("InfiniteUpdater") + + function InfiniteUpdater:render() + return nil + end + + function InfiniteUpdater:didMount() + self:setState({}) + end + + function InfiniteUpdater:didUpdate() + self:setState({}) + end + + local element = createElement(InfiniteUpdater) + local hostParent = nil + local key = "Test" + + local success, result = pcall(noopReconciler.mountVirtualNode, element, hostParent, key) + + expect(success).to.equal(false) + expect(result:find("InfiniteUpdater")).to.be.ok() + expect(result:find("reached the setState update recursion limit")).to.be.ok() + end) + + itSKIP("should process single updates with both new and pending state", function() + --[[ + This situation shouldn't be possible currently, but the implementation + should support it for future update de-duplication + ]] + end) + + it("should call trigger update after didMount when setting state in didMount", function() + --[[ + Before setState suspension, it was possible to call setState in didMount but it would + not actually finish resolving didMount until after the entire update. + + This is theoretically problematic, as it means that lifecycle methods like didUpdate + could be called before didMount is finished. setState suspension resolves this by + suspending state updates made in didMount and didUpdate as well as reconciliation + ]] + local MyComponent = Component:extend("MyComponent") + + function MyComponent:init() + self:setState({ + status = "initial mount" + }) + + self.isMounted = false + end + + function MyComponent:render() + return nil + end + + function MyComponent:didMount() + self:setState({ + status = "mounted" + }) + + self.isMounted = true + end + + function MyComponent:didUpdate(oldProps, oldState) + expect(oldState.status).to.equal("initial mount") + expect(self.state.status).to.equal("mounted") + + expect(self.isMounted).to.equal(true) + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + local result = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(result).to.be.ok() + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/shouldUpdate.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/shouldUpdate.spec.lua new file mode 100644 index 0000000..9d53b98 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/shouldUpdate.spec.lua @@ -0,0 +1,175 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked when props update", function() + local MyComponent = Component:extend("MyComponent") + + local capturedProps + local capturedState + local shouldUpdateSpy = createSpy(function(self) + capturedProps = self.props + capturedState = self.state + + return true + end) + + MyComponent.shouldUpdate = shouldUpdateSpy.value + + function MyComponent:render() + return nil + end + + local initialProps = { + a = 5, + } + local initialElement = createElement(MyComponent, initialProps) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(initialElement, hostParent, key) + + expect(shouldUpdateSpy.callCount).to.equal(0) + + local newProps = { + a = 6, + b = 2, + } + local newElement = createElement(MyComponent, newProps) + noopReconciler.updateVirtualNode(node, newElement) + + expect(shouldUpdateSpy.callCount).to.equal(1) + + local values = shouldUpdateSpy:captureValues("self", "newProps", "newState") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + + assertDeepEqual(values.newProps, newProps) + + assertDeepEqual(capturedProps, initialProps) + + expect(values.newState).to.equal(capturedState) + assertDeepEqual(capturedState, {}) + end) + + it("should be invoked when state is updated", function() + local MyComponent = Component:extend("MyComponent") + + local initialState = { + a = 1, + } + + local setState + local initState + function MyComponent:init() + setState = function(...) + return self:setState(...) + end + + self:setState(initialState) + + initState = self.state + end + + local capturedProps + local capturedState + local shouldUpdateSpy = createSpy(function(self) + capturedProps = self.props + capturedState = self.state + + return true + end) + + MyComponent.shouldUpdate = shouldUpdateSpy.value + + function MyComponent:render() + return nil + end + + local initialElement = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(initialElement, hostParent, key) + + expect(shouldUpdateSpy.callCount).to.equal(0) + + local newState = { + a = 2, + b = 3, + } + + setState(newState) + + expect(shouldUpdateSpy.callCount).to.equal(1) + + local values = shouldUpdateSpy:captureValues("self", "newProps", "newState") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + + expect(values.newProps).to.equal(capturedProps) + assertDeepEqual(capturedProps, {}) + + assertDeepEqual(capturedState, initialState) + expect(capturedState).to.equal(initState) + assertDeepEqual(values.newState, newState) + end) + + it("should not abort an update when returning true", function() + local MyComponent = Component:extend("MyComponent") + + function MyComponent:shouldUpdate() + return true + end + + local renderSpy = createSpy() + + MyComponent.render = renderSpy.value + + local initialElement = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(initialElement, hostParent, key) + + expect(renderSpy.callCount).to.equal(1) + + local newElement = createElement(MyComponent) + noopReconciler.updateVirtualNode(node, newElement) + + expect(renderSpy.callCount).to.equal(2) + end) + + it("should abort an update when retuning false", function() + local MyComponent = Component:extend("MyComponent") + + function MyComponent:shouldUpdate() + return false + end + + local renderSpy = createSpy() + + MyComponent.render = renderSpy.value + + local initialElement = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(initialElement, hostParent, key) + + expect(renderSpy.callCount).to.equal(1) + + local newElement = createElement(MyComponent) + noopReconciler.updateVirtualNode(node, newElement) + + expect(renderSpy.callCount).to.equal(1) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/validateProps.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/validateProps.spec.lua new file mode 100644 index 0000000..bcc8abb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/validateProps.spec.lua @@ -0,0 +1,240 @@ +return function() + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local GlobalConfig = require(script.Parent.Parent.GlobalConfig) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked when mounted", function() + local config = { + propValidation = true, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + + local validatePropsSpy = createSpy(function() + return true + end) + + MyComponent.validateProps = validatePropsSpy.value + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + expect(validatePropsSpy.callCount).to.equal(1) + end) + end) + + it("should be invoked when props change", function() + local config = { + propValidation = true, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + + local validatePropsSpy = createSpy(function() + return true + end) + + MyComponent.validateProps = validatePropsSpy.value + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent, { a = 1 }) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + expect(validatePropsSpy.callCount).to.equal(1) + validatePropsSpy:assertCalledWithDeepEqual({ + a = 1, + }) + + local newElement = createElement(MyComponent, { a = 2 }) + noopReconciler.updateVirtualNode(node, newElement) + expect(validatePropsSpy.callCount).to.equal(2) + validatePropsSpy:assertCalledWithDeepEqual({ + a = 2, + }) + end) + end) + + it("should not be invoked when state changes", function() + local config = { + propValidation = true, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + + local setStateCallback = nil + local validatePropsSpy = createSpy(function() + return true + end) + + MyComponent.validateProps = validatePropsSpy.value + + function MyComponent:init() + setStateCallback = function(newState) + self:setState(newState) + end + end + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent, { a = 1 }) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + expect(validatePropsSpy.callCount).to.equal(1) + validatePropsSpy:assertCalledWithDeepEqual({ + a = 1 + }) + + setStateCallback({ + b = 1 + }) + + expect(validatePropsSpy.callCount).to.equal(1) + end) + end) + + it("should throw if validateProps is not a function", function() + local config = { + propValidation = true, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + MyComponent.validateProps = 1 + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + expect(function() + noopReconciler.mountVirtualNode(element, hostParent, key) + end).to.throw() + end) + end) + + it("should throw if validateProps returns false", function() + local config = { + propValidation = true, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + MyComponent.validateProps = function() + return false + end + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + expect(function() + noopReconciler.mountVirtualNode(element, hostParent, key) + end).to.throw() + end) + end) + + it("should be invoked after defaultProps are applied", function() + local config = { + propValidation = true, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + + local validatePropsSpy = createSpy(function() + return true + end) + + MyComponent.validateProps = validatePropsSpy.value + + function MyComponent:render() + return nil + end + + MyComponent.defaultProps = { + b = 2, + } + + local element = createElement(MyComponent, { a = 1 }) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + expect(validatePropsSpy.callCount).to.equal(1) + validatePropsSpy:assertCalledWithDeepEqual({ + a = 1, + b = 2, + }) + + local newElement = createElement(MyComponent, { a = 2 }) + noopReconciler.updateVirtualNode(node, newElement) + expect(validatePropsSpy.callCount).to.equal(2) + validatePropsSpy:assertCalledWithDeepEqual({ + a = 2, + b = 2, + }) + end) + end) + + it("should not be invoked if the flag is off", function() + local config = { + propValidation = false, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + + local validatePropsSpy = createSpy(function() + return true + end) + + MyComponent.validateProps = validatePropsSpy.value + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent, { a = 1 }) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + expect(validatePropsSpy.callCount).to.equal(0) + + local newElement = createElement(MyComponent, { a = 2 }) + noopReconciler.updateVirtualNode(node, newElement) + expect(validatePropsSpy.callCount).to.equal(0) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/willUnmount.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/willUnmount.spec.lua new file mode 100644 index 0000000..590b61d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/willUnmount.spec.lua @@ -0,0 +1,36 @@ +return function() + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked when unmounted", function() + local MyComponent = Component:extend("MyComponent") + + local willUnmountSpy = createSpy() + + MyComponent.willUnmount = willUnmountSpy.value + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + noopReconciler.unmountVirtualNode(node) + + expect(willUnmountSpy.callCount).to.equal(1) + + local values = willUnmountSpy:captureValues("self") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/willUpdate.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/willUpdate.spec.lua new file mode 100644 index 0000000..b83937f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/willUpdate.spec.lua @@ -0,0 +1,93 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked when updated via updateVirtualNode", function() + local MyComponent = Component:extend("MyComponent") + + local willUpdateSpy = createSpy() + + MyComponent.willUpdate = willUpdateSpy.value + + function MyComponent:render() + return nil + end + + local initialProps = { + a = 5, + } + local initialElement = createElement(MyComponent, initialProps) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(initialElement, hostParent, key) + + local newProps = { + a = 6, + b = 2, + } + local newElement = createElement(MyComponent, newProps) + noopReconciler.updateVirtualNode(node, newElement) + + expect(willUpdateSpy.callCount).to.equal(1) + + local values = willUpdateSpy:captureValues("self", "newProps", "newState") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + assertDeepEqual(values.newProps, newProps) + assertDeepEqual(values.newState, {}) + end) + + it("it should be invoked when updated via setState", function() + local MyComponent = Component:extend("MyComponent") + local setComponentState + + local willUpdateSpy = createSpy() + + MyComponent.willUpdate = willUpdateSpy.value + + function MyComponent:init() + setComponentState = function(state) + self:setState(state) + end + + self:setState({ + foo = 1 + }) + end + + function MyComponent:render() + return nil + end + + local initialElement = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(initialElement, hostParent, key) + + expect(willUpdateSpy.callCount).to.equal(0) + + setComponentState({ + foo = 2 + }) + + expect(willUpdateSpy.callCount).to.equal(1) + + local values = willUpdateSpy:captureValues("self", "newProps", "newState") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + assertDeepEqual(values.newProps, {}) + assertDeepEqual(values.newState, { + foo = 2 + }) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ComponentLifecyclePhase.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ComponentLifecyclePhase.lua new file mode 100644 index 0000000..dd23963 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ComponentLifecyclePhase.lua @@ -0,0 +1,19 @@ +local Symbol = require(script.Parent.Symbol) +local strict = require(script.Parent.strict) + +local ComponentLifecyclePhase = strict({ + -- Component methods + Init = Symbol.named("init"), + Render = Symbol.named("render"), + ShouldUpdate = Symbol.named("shouldUpdate"), + WillUpdate = Symbol.named("willUpdate"), + DidMount = Symbol.named("didMount"), + DidUpdate = Symbol.named("didUpdate"), + WillUnmount = Symbol.named("willUnmount"), + + -- Phases describing reconciliation status + ReconcileChildren = Symbol.named("reconcileChildren"), + Idle = Symbol.named("idle"), +}, "ComponentLifecyclePhase") + +return ComponentLifecyclePhase \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Config.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Config.lua new file mode 100644 index 0000000..6884bc4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Config.lua @@ -0,0 +1,123 @@ +--[[ + Exposes an interface to set global configuration values for Roact. + + Configuration can only occur once, and should only be done by an application + using Roact, not a library. + + Any keys that aren't recognized will cause errors. Configuration is only + intended for configuring Roact itself, not extensions or libraries. + + Configuration is expected to be set immediately after loading Roact. Setting + configuration values after an application starts may produce unpredictable + behavior. +]] + +-- Every valid configuration value should be non-nil in this table. +local defaultConfig = { + -- Enables asserts for internal Roact APIs. Useful for debugging Roact itself. + ["internalTypeChecks"] = false, + -- Enables stricter type asserts for Roact's public API. + ["typeChecks"] = false, + -- Enables storage of `debug.traceback()` values on elements for debugging. + ["elementTracing"] = false, + -- Enables validation of component props in stateful components. + ["propValidation"] = false, +} + +-- Build a list of valid configuration values up for debug messages. +local defaultConfigKeys = {} +for key in pairs(defaultConfig) do + table.insert(defaultConfigKeys, key) +end + +local Config = {} + +function Config.new() + local self = {} + + self._currentConfig = setmetatable({}, { + __index = function(_, key) + local message = ( + "Invalid global configuration key %q. Valid configuration keys are: %s" + ):format( + tostring(key), + table.concat(defaultConfigKeys, ", ") + ) + + error(message, 3) + end + }) + + -- We manually bind these methods here so that the Config's methods can be + -- used without passing in self, since they eventually get exposed on the + -- root Roact object. + self.set = function(...) + return Config.set(self, ...) + end + + self.get = function(...) + return Config.get(self, ...) + end + + self.scoped = function(...) + return Config.scoped(self, ...) + end + + self.set(defaultConfig) + + return self +end + +function Config:set(configValues) + -- Validate values without changing any configuration. + -- We only want to apply this configuration if it's valid! + for key, value in pairs(configValues) do + if defaultConfig[key] == nil then + local message = ( + "Invalid global configuration key %q (type %s). Valid configuration keys are: %s" + ):format( + tostring(key), + typeof(key), + table.concat(defaultConfigKeys, ", ") + ) + + error(message, 3) + end + + -- Right now, all configuration values must be boolean. + if typeof(value) ~= "boolean" then + local message = ( + "Invalid value %q (type %s) for global configuration key %q. Valid values are: true, false" + ):format( + tostring(value), + typeof(value), + tostring(key) + ) + + error(message, 3) + end + + self._currentConfig[key] = value + end +end + +function Config:get() + return self._currentConfig +end + +function Config:scoped(configValues, callback) + local previousValues = {} + for key, value in pairs(self._currentConfig) do + previousValues[key] = value + end + + self.set(configValues) + + local success, result = pcall(callback) + + self.set(previousValues) + + assert(success, result) +end + +return Config \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Config.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Config.spec.lua new file mode 100644 index 0000000..08a884f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Config.spec.lua @@ -0,0 +1,52 @@ +return function() + local Config = require(script.Parent.Config) + + it("should accept valid configuration", function() + local config = Config.new() + local values = config.get() + + expect(values.elementTracing).to.equal(false) + + config.set({ + elementTracing = true, + }) + + expect(values.elementTracing).to.equal(true) + end) + + it("should reject invalid configuration keys", function() + local config = Config.new() + + local badKey = "garblegoop" + + local ok, err = pcall(function() + config.set({ + [badKey] = true, + }) + end) + + expect(ok).to.equal(false) + + -- The error should mention our bad key somewhere. + expect(err:find(badKey)).to.be.ok() + end) + + it("should reject invalid configuration values", function() + local config = Config.new() + + local goodKey = "elementTracing" + local badValue = "Hello there!" + + local ok, err = pcall(function() + config.set({ + [goodKey] = badValue, + }) + end) + + expect(ok).to.equal(false) + + -- The error should mention both our key and value + expect(err:find(goodKey)).to.be.ok() + expect(err:find(badValue)).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementKind.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementKind.lua new file mode 100644 index 0000000..22e1e53 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementKind.lua @@ -0,0 +1,51 @@ +--[[ + Contains markers for annotating the type of an element. + + Use `ElementKind` as a key, and values from it as the value. + + local element = { + [ElementKind] = ElementKind.Host, + } +]] + +local Symbol = require(script.Parent.Symbol) +local strict = require(script.Parent.strict) +local Portal = require(script.Parent.Portal) + +local ElementKind = newproxy(true) + +local ElementKindInternal = { + Portal = Symbol.named("Portal"), + Host = Symbol.named("Host"), + Function = Symbol.named("Function"), + Stateful = Symbol.named("Stateful"), + Fragment = Symbol.named("Fragment"), +} + +function ElementKindInternal.of(value) + if typeof(value) ~= "table" then + return nil + end + + return value[ElementKind] +end + +local componentTypesToKinds = { + ["string"] = ElementKindInternal.Host, + ["function"] = ElementKindInternal.Function, + ["table"] = ElementKindInternal.Stateful, +} + +function ElementKindInternal.fromComponent(component) + if component == Portal then + return ElementKind.Portal + else + return componentTypesToKinds[typeof(component)] + end +end + +getmetatable(ElementKind).__index = ElementKindInternal + +strict(ElementKindInternal, "ElementKind") + +return ElementKind \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementKind.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementKind.spec.lua new file mode 100644 index 0000000..80f8c4e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementKind.spec.lua @@ -0,0 +1,54 @@ +return function() + local Portal = require(script.Parent.Portal) + local Component = require(script.Parent.Component) + + local ElementKind = require(script.Parent.ElementKind) + + describe("of", function() + it("should return nil for non-table values", function() + expect(ElementKind.of(nil)).to.equal(nil) + expect(ElementKind.of(5)).to.equal(nil) + expect(ElementKind.of(newproxy(true))).to.equal(nil) + end) + + it("should return nil for table values without an ElementKind key", function() + expect(ElementKind.of({})).to.equal(nil) + end) + + it("should return the ElementKind from a table", function() + local value = { + [ElementKind] = ElementKind.Stateful, + } + + expect(ElementKind.of(value)).to.equal(ElementKind.Stateful) + end) + end) + + describe("fromComponent", function() + it("should handle host components", function() + expect(ElementKind.fromComponent("foo")).to.equal(ElementKind.Host) + end) + + it("should handle function components", function() + local function foo() + end + + expect(ElementKind.fromComponent(foo)).to.equal(ElementKind.Function) + end) + + it("should handle stateful components", function() + local Foo = Component:extend("Foo") + + expect(ElementKind.fromComponent(Foo)).to.equal(ElementKind.Stateful) + end) + + it("should handle portals", function() + expect(ElementKind.fromComponent(Portal)).to.equal(ElementKind.Portal) + end) + + it("should return nil for invalid inputs", function() + expect(ElementKind.fromComponent(5)).to.equal(nil) + expect(ElementKind.fromComponent(newproxy(true))).to.equal(nil) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementUtils.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementUtils.lua new file mode 100644 index 0000000..971b6b1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementUtils.lua @@ -0,0 +1,99 @@ +local Type = require(script.Parent.Type) +local Symbol = require(script.Parent.Symbol) + +local function noop() + return nil +end + +local ElementUtils = {} + +--[[ + A signal value indicating that a child should use its parent's key, because + it has no key of its own. + + This occurs when you return only one element from a function component or + stateful render function. +]] +ElementUtils.UseParentKey = Symbol.named("UseParentKey") + +--[[ + Returns an iterator over the children of an element. + `elementOrElements` may be one of: + * a boolean + * nil + * a single element + * a fragment + * a table of elements + + If `elementOrElements` is a boolean or nil, this will return an iterator with + zero elements. + + If `elementOrElements` is a single element, this will return an iterator with + one element: a tuple where the first value is ElementUtils.UseParentKey, and + the second is the value of `elementOrElements`. + + If `elementOrElements` is a fragment or a table, this will return an iterator + over all the elements of the array. + + If `elementOrElements` is none of the above, this function will throw. +]] +function ElementUtils.iterateElements(elementOrElements) + local richType = Type.of(elementOrElements) + + -- Single child + if richType == Type.Element then + local called = false + + return function() + if called then + return nil + else + called = true + return ElementUtils.UseParentKey, elementOrElements + end + end + end + + local regularType = typeof(elementOrElements) + + if elementOrElements == nil or regularType == "boolean" then + return noop + end + + if regularType == "table" then + return pairs(elementOrElements) + end + + error("Invalid elements") +end + +--[[ + Gets the child corresponding to a given key, respecting Roact's rules for + children. Specifically: + * If `elements` is nil or a boolean, this will return `nil`, regardless of + the key given. + * If `elements` is a single element, this will return `nil`, unless the key + is ElementUtils.UseParentKey. + * If `elements` is a table of elements, this will return `elements[key]`. +]] +function ElementUtils.getElementByKey(elements, hostKey) + if elements == nil or typeof(elements) == "boolean" then + return nil + end + + if Type.of(elements) == Type.Element then + if hostKey == ElementUtils.UseParentKey then + return elements + end + + return nil + end + + if typeof(elements) == "table" then + return elements[hostKey] + end + + error("Invalid elements") +end + +return ElementUtils \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementUtils.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementUtils.spec.lua new file mode 100644 index 0000000..3457abb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementUtils.spec.lua @@ -0,0 +1,95 @@ +return function() + local ElementUtils = require(script.Parent.ElementUtils) + local createElement = require(script.Parent.createElement) + local createFragment = require(script.Parent.createFragment) + local Type = require(script.Parent.Type) + + describe("iterateElements", function() + it("should iterate once for a single child", function() + local child = createElement("TextLabel") + local iterator = ElementUtils.iterateElements(child) + local iteratedKey, iteratedChild = iterator() + -- For single elements, the key should be UseParentKey + expect(iteratedKey).to.equal(ElementUtils.UseParentKey) + expect(iteratedChild).to.equal(child) + + iteratedKey = iterator() + expect(iteratedKey).to.equal(nil) + end) + + it("should iterate over tables", function() + local children = { + a = createElement("TextLabel"), + b = createElement("TextLabel"), + } + + local seenChildren = {} + local count = 0 + + for key, child in ElementUtils.iterateElements(children) do + expect(typeof(key)).to.equal("string") + expect(Type.of(child)).to.equal(Type.Element) + seenChildren[child] = key + count = count + 1 + end + + expect(count).to.equal(2) + expect(seenChildren[children.a]).to.equal("a") + expect(seenChildren[children.b]).to.equal("b") + end) + + it("should return a zero-element iterator for booleans", function() + local booleanIterator = ElementUtils.iterateElements(false) + expect(booleanIterator()).to.equal(nil) + end) + + it("should return a zero-element iterator for nil", function() + local nilIterator = ElementUtils.iterateElements(nil) + expect(nilIterator()).to.equal(nil) + end) + + it("should throw if given an illegal value", function() + expect(function() + ElementUtils.iterateElements(1) + end).to.throw() + end) + end) + + describe("getElementByKey", function() + it("should return nil for booleans", function() + expect(ElementUtils.getElementByKey(true, "test")).to.equal(nil) + end) + + it("should return nil for nil", function() + expect(ElementUtils.getElementByKey(nil, "test")).to.equal(nil) + end) + + describe("single elements", function() + local element = createElement("TextLabel") + + it("should return the element if the key is UseParentKey", function() + expect(ElementUtils.getElementByKey(element, ElementUtils.UseParentKey)).to.equal(element) + end) + + it("should return nil if the key is not UseParentKey", function() + expect(ElementUtils.getElementByKey(element, "test")).to.equal(nil) + end) + end) + + it("should return the corresponding element from a table", function() + local children = { + a = createElement("TextLabel"), + b = createElement("TextLabel"), + } + + expect(ElementUtils.getElementByKey(children, "a")).to.equal(children.a) + expect(ElementUtils.getElementByKey(children, "b")).to.equal(children.b) + end) + + it("should return nil if the key does not exist", function() + local children = createFragment({}) + + expect(ElementUtils.getElementByKey(children, "a")).to.equal(nil) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/GlobalConfig.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/GlobalConfig.lua new file mode 100644 index 0000000..3219835 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/GlobalConfig.lua @@ -0,0 +1,7 @@ +--[[ + Exposes a single instance of a configuration as Roact's GlobalConfig. +]] + +local Config = require(script.Parent.Config) + +return Config.new() \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/GlobalConfig.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/GlobalConfig.spec.lua new file mode 100644 index 0000000..760a2a3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/GlobalConfig.spec.lua @@ -0,0 +1,9 @@ +return function() + local GlobalConfig = require(script.Parent.GlobalConfig) + + it("should have the correct methods", function() + expect(GlobalConfig).to.be.ok() + expect(GlobalConfig.set).to.be.ok() + expect(GlobalConfig.get).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Logging.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Logging.lua new file mode 100644 index 0000000..17a9d6d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Logging.lua @@ -0,0 +1,159 @@ +--[[ + Centralized place to handle logging. Lets us: + - Unit test log output via `Logging.capture` + - Disable verbose log messages when not debugging Roact + + This should be broken out into a separate library with the addition of + scoping and logging configuration. +]] + +-- Determines whether log messages will go to stdout/stderr +local outputEnabled = true + +-- A set of LogInfo objects that should have messages inserted into them. +-- This is a set so that nested calls to Logging.capture will behave. +local collectors = {} + +-- A set of all stack traces that have called warnOnce. +local onceUsedLocations = {} + +--[[ + Indent a potentially multi-line string with the given number of tabs, in + addition to any indentation the string already has. +]] +local function indent(source, indentLevel) + local indentString = ("\t"):rep(indentLevel) + + return indentString .. source:gsub("\n", "\n" .. indentString) +end + +--[[ + Indents a list of strings and then concatenates them together with newlines + into a single string. +]] +local function indentLines(lines, indentLevel) + local outputBuffer = {} + + for _, line in ipairs(lines) do + table.insert(outputBuffer, indent(line, indentLevel)) + end + + return table.concat(outputBuffer, "\n") +end + +local logInfoMetatable = {} + +--[[ + Automatic coercion to strings for LogInfo objects to enable debugging them + more easily. +]] +function logInfoMetatable:__tostring() + local outputBuffer = {"LogInfo {"} + + local errorCount = #self.errors + local warningCount = #self.warnings + local infosCount = #self.infos + + if errorCount + warningCount + infosCount == 0 then + table.insert(outputBuffer, "\t(no messages)") + end + + if errorCount > 0 then + table.insert(outputBuffer, ("\tErrors (%d) {"):format(errorCount)) + table.insert(outputBuffer, indentLines(self.errors, 2)) + table.insert(outputBuffer, "\t}") + end + + if warningCount > 0 then + table.insert(outputBuffer, ("\tWarnings (%d) {"):format(warningCount)) + table.insert(outputBuffer, indentLines(self.warnings, 2)) + table.insert(outputBuffer, "\t}") + end + + if infosCount > 0 then + table.insert(outputBuffer, ("\tInfos (%d) {"):format(infosCount)) + table.insert(outputBuffer, indentLines(self.infos, 2)) + table.insert(outputBuffer, "\t}") + end + + table.insert(outputBuffer, "}") + + return table.concat(outputBuffer, "\n") +end + +local function createLogInfo() + local logInfo = { + errors = {}, + warnings = {}, + infos = {}, + } + + setmetatable(logInfo, logInfoMetatable) + + return logInfo +end + +local Logging = {} + +--[[ + Invokes `callback`, capturing all output that happens during its execution. + + Output will not go to stdout or stderr and will instead be put into a + LogInfo object that is returned. If `callback` throws, the error will be + bubbled up to the caller of `Logging.capture`. +]] +function Logging.capture(callback) + local collector = createLogInfo() + + local wasOutputEnabled = outputEnabled + outputEnabled = false + collectors[collector] = true + + local success, result = pcall(callback) + + collectors[collector] = nil + outputEnabled = wasOutputEnabled + + assert(success, result) + + return collector +end + +--[[ + Issues a warning with an automatically attached stack trace. +]] +function Logging.warn(messageTemplate, ...) + local message = messageTemplate:format(...) + + for collector in pairs(collectors) do + table.insert(collector.warnings, message) + end + + -- debug.traceback inserts a leading newline, so we trim it here + local trace = debug.traceback("", 2):sub(2) + local fullMessage = ("%s\n%s"):format(message, indent(trace, 1)) + + if outputEnabled then + warn(fullMessage) + end +end + +--[[ + Issues a warning like `Logging.warn`, but only outputs once per call site. + + This is useful for marking deprecated functions that might be called a lot; + using `warnOnce` instead of `warn` will reduce output noise while still + correctly marking all call sites. +]] +function Logging.warnOnce(messageTemplate, ...) + local trace = debug.traceback() + + if onceUsedLocations[trace] then + return + end + + onceUsedLocations[trace] = true + Logging.warn(messageTemplate, ...) +end + +return Logging \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/None.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/None.lua new file mode 100644 index 0000000..9f25d3a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/None.lua @@ -0,0 +1,7 @@ +local Symbol = require(script.Parent.Symbol) + +-- Marker used to specify that the value is nothing, because nil cannot be +-- stored in tables. +local None = Symbol.named("None") + +return None \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/NoopRenderer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/NoopRenderer.lua new file mode 100644 index 0000000..8d19157 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/NoopRenderer.lua @@ -0,0 +1,24 @@ +--[[ + Reference renderer intended for use in tests as well as for documenting the + minimum required interface for a Roact renderer. +]] + +local NoopRenderer = {} + +function NoopRenderer.isHostObject(target) + -- Attempting to use NoopRenderer to target a Roblox instance is almost + -- certainly a mistake. + return target == nil +end + +function NoopRenderer.mountHostNode(reconciler, node) +end + +function NoopRenderer.unmountHostNode(reconciler, node) +end + +function NoopRenderer.updateHostNode(reconciler, node, newElement) + return node +end + +return NoopRenderer \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Portal.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Portal.lua new file mode 100644 index 0000000..4db0a37 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Portal.lua @@ -0,0 +1,5 @@ +local Symbol = require(script.Parent.Symbol) + +local Portal = Symbol.named("Portal") + +return Portal \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Change.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Change.lua new file mode 100644 index 0000000..2a20adb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Change.lua @@ -0,0 +1,38 @@ +--[[ + Change is used to generate special prop keys that can be used to connect to + GetPropertyChangedSignal. + + Generally, Change is indexed by a Roblox property name: + + Roact.createElement("TextBox", { + [Roact.Change.Text] = function(rbx) + print("The TextBox", rbx, "changed text to", rbx.Text) + end, + }) +]] + +local Type = require(script.Parent.Parent.Type) + +local Change = {} + +local changeMetatable = { + __tostring = function(self) + return ("RoactHostChangeEvent(%s)"):format(self.name) + end, +} + +setmetatable(Change, { + __index = function(self, propertyName) + local changeListener = { + [Type] = Type.HostChangeEvent, + name = propertyName, + } + + setmetatable(changeListener, changeMetatable) + Change[propertyName] = changeListener + + return changeListener + end, +}) + +return Change diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Change.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Change.spec.lua new file mode 100644 index 0000000..903099d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Change.spec.lua @@ -0,0 +1,19 @@ +return function() + local Type = require(script.Parent.Parent.Type) + + local Change = require(script.Parent.Change) + + it("should yield change listener objects when indexed", function() + expect(Type.of(Change.Text)).to.equal(Type.HostChangeEvent) + expect(Type.of(Change.Selected)).to.equal(Type.HostChangeEvent) + end) + + it("should yield the same object when indexed again", function() + local a = Change.Text + local b = Change.Text + local c = Change.Selected + + expect(a).to.equal(b) + expect(a).never.to.equal(c) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Children.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Children.lua new file mode 100644 index 0000000..8c320dd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Children.lua @@ -0,0 +1,5 @@ +local Symbol = require(script.Parent.Parent.Symbol) + +local Children = Symbol.named("Children") + +return Children \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Event.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Event.lua new file mode 100644 index 0000000..f9aba02 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Event.lua @@ -0,0 +1,41 @@ +--[[ + Index into `Event` to get a prop key for attaching to an event on a Roblox + Instance. + + Example: + + Roact.createElement("TextButton", { + Text = "Hello, world!", + + [Roact.Event.MouseButton1Click] = function(rbx) + print("Clicked", rbx) + end + }) +]] + +local Type = require(script.Parent.Parent.Type) + +local Event = {} + +local eventMetatable = { + __tostring = function(self) + return ("RoactHostEvent(%s)"):format(self.name) + end, +} + +setmetatable(Event, { + __index = function(self, eventName) + local event = { + [Type] = Type.HostEvent, + name = eventName, + } + + setmetatable(event, eventMetatable) + + Event[eventName] = event + + return event + end, +}) + +return Event diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Event.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Event.spec.lua new file mode 100644 index 0000000..fc34e91 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Event.spec.lua @@ -0,0 +1,19 @@ +return function() + local Type = require(script.Parent.Parent.Type) + + local Event = require(script.Parent.Event) + + it("should yield event objects when indexed", function() + expect(Type.of(Event.MouseButton1Click)).to.equal(Type.HostEvent) + expect(Type.of(Event.Touched)).to.equal(Type.HostEvent) + end) + + it("should yield the same object when indexed again", function() + local a = Event.MouseButton1Click + local b = Event.MouseButton1Click + local c = Event.Touched + + expect(a).to.equal(b) + expect(a).never.to.equal(c) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Ref.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Ref.lua new file mode 100644 index 0000000..a86e4c2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Ref.lua @@ -0,0 +1,5 @@ +local Symbol = require(script.Parent.Parent.Symbol) + +local Ref = Symbol.named("Ref") + +return Ref \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PureComponent.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PureComponent.lua new file mode 100644 index 0000000..0283298 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PureComponent.lua @@ -0,0 +1,41 @@ +--[[ + A version of Component with a `shouldUpdate` method that forces the + resulting component to be pure. +]] + +local Component = require(script.Parent.Component) + +local PureComponent = Component:extend("PureComponent") + +-- When extend()ing a component, you don't get an extend method. +-- This is to promote composition over inheritance. +-- PureComponent is an exception to this rule. +PureComponent.extend = Component.extend + +function PureComponent:shouldUpdate(newProps, newState) + -- In a vast majority of cases, if state updated, something has updated. + -- We don't bother checking in this case. + if newState ~= self.state then + return true + end + + if newProps == self.props then + return false + end + + for key, value in pairs(newProps) do + if self.props[key] ~= value then + return true + end + end + + for key, value in pairs(self.props) do + if newProps[key] ~= value then + return true + end + end + + return false +end + +return PureComponent \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PureComponent.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PureComponent.spec.lua new file mode 100644 index 0000000..b164437 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PureComponent.spec.lua @@ -0,0 +1,75 @@ +return function() + local createElement = require(script.Parent.createElement) + local NoopRenderer = require(script.Parent.NoopRenderer) + local createReconciler = require(script.Parent.createReconciler) + + local PureComponent = require(script.Parent.PureComponent) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be extendable", function() + local MyComponent = PureComponent:extend("MyComponent") + + expect(MyComponent).to.be.ok() + end) + + it("should skip updates for shallow-equal props", function() + local updateCount = 0 + local setValue + + local PureChild = PureComponent:extend("PureChild") + + function PureChild:willUpdate() + updateCount = updateCount + 1 + end + + function PureChild:render() + return nil + end + + local PureContainer = PureComponent:extend("PureContainer") + + function PureContainer:init() + self.state = { + value = 0, + } + end + + function PureContainer:didMount() + setValue = function(value) + self:setState({ + value = value, + }) + end + end + + function PureContainer:render() + return createElement(PureChild, { + value = self.state.value, + }) + end + + local element = createElement(PureContainer) + local tree = noopReconciler.mountVirtualTree(element, nil, "PureComponent Tree") + + expect(updateCount).to.equal(0) + + setValue(1) + + expect(updateCount).to.equal(1) + + setValue(1) + + expect(updateCount).to.equal(1) + + setValue(2) + + expect(updateCount).to.equal(2) + + setValue(1) + + expect(updateCount).to.equal(3) + + noopReconciler.unmountVirtualTree(tree) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/RobloxRenderer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/RobloxRenderer.lua new file mode 100644 index 0000000..4f528ad --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/RobloxRenderer.lua @@ -0,0 +1,283 @@ +--[[ + Renderer that deals in terms of Roblox Instances. This is the most + well-supported renderer after NoopRenderer and is currently the only + renderer that does anything. +]] + +local Binding = require(script.Parent.Binding) +local Children = require(script.Parent.PropMarkers.Children) +local ElementKind = require(script.Parent.ElementKind) +local SingleEventManager = require(script.Parent.SingleEventManager) +local getDefaultInstanceProperty = require(script.Parent.getDefaultInstanceProperty) +local Ref = require(script.Parent.PropMarkers.Ref) +local Type = require(script.Parent.Type) +local internalAssert = require(script.Parent.internalAssert) + +local config = require(script.Parent.GlobalConfig).get() + +local applyPropsError = [[ +Error applying props: + %s +In element: +%s +]] + +local updatePropsError = [[ +Error updating props: + %s +In element: +%s +]] + +local function identity(...) + return ... +end + +local function applyRef(ref, newHostObject) + if ref == nil then + return + end + + if typeof(ref) == "function" then + ref(newHostObject) + elseif Type.of(ref) == Type.Binding then + Binding.update(ref, newHostObject) + else + -- TODO (#197): Better error message + error(("Invalid ref: Expected type Binding but got %s"):format( + typeof(ref) + )) + end +end + +local function setRobloxInstanceProperty(hostObject, key, newValue) + if newValue == nil then + local hostClass = hostObject.ClassName + local _, defaultValue = getDefaultInstanceProperty(hostClass, key) + newValue = defaultValue + end + + -- Assign the new value to the object + hostObject[key] = newValue + + return +end + +local function removeBinding(virtualNode, key) + local disconnect = virtualNode.bindings[key] + disconnect() + virtualNode.bindings[key] = nil +end + +local function attachBinding(virtualNode, key, newBinding) + local function updateBoundProperty(newValue) + local success, errorMessage = xpcall(function() + setRobloxInstanceProperty(virtualNode.hostObject, key, newValue) + end, identity) + + if not success then + local source = virtualNode.currentElement.source + + if source == nil then + source = "" + end + + local fullMessage = updatePropsError:format(errorMessage, source) + error(fullMessage, 0) + end + end + + if virtualNode.bindings == nil then + virtualNode.bindings = {} + end + + virtualNode.bindings[key] = Binding.subscribe(newBinding, updateBoundProperty) + + updateBoundProperty(newBinding:getValue()) +end + +local function detachAllBindings(virtualNode) + if virtualNode.bindings ~= nil then + for _, disconnect in pairs(virtualNode.bindings) do + disconnect() + end + end +end + +local function applyProp(virtualNode, key, newValue, oldValue) + if newValue == oldValue then + return + end + + if key == Ref or key == Children then + -- Refs and children are handled in a separate pass + return + end + + local internalKeyType = Type.of(key) + + if internalKeyType == Type.HostEvent or internalKeyType == Type.HostChangeEvent then + if virtualNode.eventManager == nil then + virtualNode.eventManager = SingleEventManager.new(virtualNode.hostObject) + end + + local eventName = key.name + + if internalKeyType == Type.HostChangeEvent then + virtualNode.eventManager:connectPropertyChange(eventName, newValue) + else + virtualNode.eventManager:connectEvent(eventName, newValue) + end + + return + end + + local newIsBinding = Type.of(newValue) == Type.Binding + local oldIsBinding = Type.of(oldValue) == Type.Binding + + if oldIsBinding then + removeBinding(virtualNode, key) + end + + if newIsBinding then + attachBinding(virtualNode, key, newValue) + else + setRobloxInstanceProperty(virtualNode.hostObject, key, newValue) + end +end + +local function applyProps(virtualNode, props) + for propKey, value in pairs(props) do + applyProp(virtualNode, propKey, value, nil) + end +end + +local function updateProps(virtualNode, oldProps, newProps) + -- Apply props that were added or updated + for propKey, newValue in pairs(newProps) do + local oldValue = oldProps[propKey] + + applyProp(virtualNode, propKey, newValue, oldValue) + end + + -- Clean up props that were removed + for propKey, oldValue in pairs(oldProps) do + local newValue = newProps[propKey] + + if newValue == nil then + applyProp(virtualNode, propKey, nil, oldValue) + end + end +end + +local RobloxRenderer = {} + +function RobloxRenderer.isHostObject(target) + return typeof(target) == "Instance" +end + +function RobloxRenderer.mountHostNode(reconciler, virtualNode) + local element = virtualNode.currentElement + local hostParent = virtualNode.hostParent + local hostKey = virtualNode.hostKey + + if config.internalTypeChecks then + internalAssert(ElementKind.of(element) == ElementKind.Host, "Element at given node is not a host Element") + end + if config.typeChecks then + assert(element.props.Name == nil, "Name can not be specified as a prop to a host component in Roact.") + assert(element.props.Parent == nil, "Parent can not be specified as a prop to a host component in Roact.") + end + + local instance = Instance.new(element.component) + virtualNode.hostObject = instance + + local success, errorMessage = xpcall(function() + applyProps(virtualNode, element.props) + end, identity) + + if not success then + local source = element.source + + if source == nil then + source = "" + end + + local fullMessage = applyPropsError:format(errorMessage, source) + error(fullMessage, 0) + end + + instance.Name = tostring(hostKey) + + local children = element.props[Children] + + if children ~= nil then + reconciler.updateVirtualNodeWithChildren(virtualNode, virtualNode.hostObject, children) + end + + instance.Parent = hostParent + virtualNode.hostObject = instance + + applyRef(element.props[Ref], instance) + + if virtualNode.eventManager ~= nil then + virtualNode.eventManager:resume() + end +end + +function RobloxRenderer.unmountHostNode(reconciler, virtualNode) + local element = virtualNode.currentElement + + applyRef(element.props[Ref], nil) + + for _, childNode in pairs(virtualNode.children) do + reconciler.unmountVirtualNode(childNode) + end + + detachAllBindings(virtualNode) + + virtualNode.hostObject:Destroy() +end + +function RobloxRenderer.updateHostNode(reconciler, virtualNode, newElement) + local oldProps = virtualNode.currentElement.props + local newProps = newElement.props + + if virtualNode.eventManager ~= nil then + virtualNode.eventManager:suspend() + end + + -- If refs changed, detach the old ref and attach the new one + if oldProps[Ref] ~= newProps[Ref] then + applyRef(oldProps[Ref], nil) + applyRef(newProps[Ref], virtualNode.hostObject) + end + + local success, errorMessage = xpcall(function() + updateProps(virtualNode, oldProps, newProps) + end, identity) + + if not success then + local source = newElement.source + + if source == nil then + source = "" + end + + local fullMessage = updatePropsError:format(errorMessage, source) + error(fullMessage, 0) + end + + local children = newElement.props[Children] + if children ~= nil or oldProps[Children] ~= nil then + reconciler.updateVirtualNodeWithChildren(virtualNode, virtualNode.hostObject, children) + end + + if virtualNode.eventManager ~= nil then + virtualNode.eventManager:resume() + end + + return virtualNode +end + +return RobloxRenderer diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/RobloxRenderer.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/RobloxRenderer.spec.lua new file mode 100644 index 0000000..1ea04cb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/RobloxRenderer.spec.lua @@ -0,0 +1,949 @@ +return function() + local assertDeepEqual = require(script.Parent.assertDeepEqual) + local Binding = require(script.Parent.Binding) + local Children = require(script.Parent.PropMarkers.Children) + local Component = require(script.Parent.Component) + local createElement = require(script.Parent.createElement) + local createFragment = require(script.Parent.createFragment) + local createReconciler = require(script.Parent.createReconciler) + local createRef = require(script.Parent.createRef) + local createSpy = require(script.Parent.createSpy) + local GlobalConfig = require(script.Parent.GlobalConfig) + local Portal = require(script.Parent.Portal) + local Ref = require(script.Parent.PropMarkers.Ref) + + local RobloxRenderer = require(script.Parent.RobloxRenderer) + + local reconciler = createReconciler(RobloxRenderer) + + describe("mountHostNode", function() + it("should create instances with correct props", function() + local parent = Instance.new("Folder") + local value = "Hello!" + local key = "Some Key" + + local element = createElement("StringValue", { + Value = value, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local root = parent:GetChildren()[1] + + expect(root.ClassName).to.equal("StringValue") + expect(root.Value).to.equal(value) + expect(root.Name).to.equal(key) + end) + + it("should create children with correct names and props", function() + local parent = Instance.new("Folder") + local rootValue = "Hey there!" + local childValue = 173 + local key = "Some Key" + + local element = createElement("StringValue", { + Value = rootValue, + }, { + ChildA = createElement("IntValue", { + Value = childValue, + }), + + ChildB = createElement("Folder"), + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local root = parent:GetChildren()[1] + + expect(root.ClassName).to.equal("StringValue") + expect(root.Value).to.equal(rootValue) + expect(root.Name).to.equal(key) + + expect(#root:GetChildren()).to.equal(2) + + local childA = root.ChildA + local childB = root.ChildB + + expect(childA).to.be.ok() + expect(childB).to.be.ok() + + expect(childA.ClassName).to.equal("IntValue") + expect(childA.Value).to.equal(childValue) + + expect(childB.ClassName).to.equal("Folder") + end) + + it("should attach Bindings to Roblox properties", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local binding, update = Binding.create(10) + local element = createElement("IntValue", { + Value = binding, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local instance = parent:GetChildren()[1] + + expect(instance.ClassName).to.equal("IntValue") + expect(instance.Value).to.equal(10) + + update(20) + + expect(instance.Value).to.equal(20) + + RobloxRenderer.unmountHostNode(reconciler, node) + end) + + it("should connect Binding refs", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local ref = createRef() + local element = createElement("Frame", { + [Ref] = ref, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local instance = parent:GetChildren()[1] + + expect(ref.current).to.be.ok() + expect(ref.current).to.equal(instance) + + RobloxRenderer.unmountHostNode(reconciler, node) + end) + + it("should call function refs", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local spyRef = createSpy() + local element = createElement("Frame", { + [Ref] = spyRef.value, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local instance = parent:GetChildren()[1] + + expect(spyRef.callCount).to.equal(1) + spyRef:assertCalledWith(instance) + + RobloxRenderer.unmountHostNode(reconciler, node) + end) + + it("should throw if setting invalid instance properties", function() + local configValues = { + elementTracing = true, + } + + GlobalConfig.scoped(configValues, function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local element = createElement("Frame", { + Frob = 6, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + local success, message = pcall(RobloxRenderer.mountHostNode, reconciler, node) + assert(not success, "Expected call to fail") + + expect(message:find("Frob")).to.be.ok() + expect(message:find("Frame")).to.be.ok() + expect(message:find("RobloxRenderer%.spec")).to.be.ok() + end) + end) + end) + + describe("updateHostNode", function() + it("should update node props and children", function() + -- TODO: Break up test + + local parent = Instance.new("Folder") + local key = "updateHostNodeTest" + local firstValue = "foo" + local newValue = "bar" + + local defaultStringValue = Instance.new("StringValue").Value + + local element = createElement("StringValue", { + Value = firstValue + }, { + ChildA = createElement("IntValue", { + Value = 1 + }), + ChildB = createElement("BoolValue", { + Value = true, + }), + ChildC = createElement("StringValue", { + Value = "test", + }), + ChildD = createElement("StringValue", { + Value = "test", + }) + }) + + local node = reconciler.createVirtualNode(element, parent, key) + RobloxRenderer.mountHostNode(reconciler, node) + + -- Not testing mountHostNode's work here, only testing that the + -- node is properly updated. + + local newElement = createElement("StringValue", { + Value = newValue, + }, { + -- ChildA changes element type. + ChildA = createElement("StringValue", { + Value = "test" + }), + -- ChildB changes child properties. + ChildB = createElement("BoolValue", { + Value = false, + }), + -- ChildC should reset its Value property back to the default. + ChildC = createElement("StringValue", {}), + -- ChildD is deleted. + -- ChildE is added. + ChildE = createElement("Folder", {}), + }) + + RobloxRenderer.updateHostNode(reconciler, node, newElement) + + local root = parent[key] + expect(root.ClassName).to.equal("StringValue") + expect(root.Value).to.equal(newValue) + expect(#root:GetChildren()).to.equal(4) + + local childA = root.ChildA + expect(childA.ClassName).to.equal("StringValue") + expect(childA.Value).to.equal("test") + + local childB = root.ChildB + expect(childB.ClassName).to.equal("BoolValue") + expect(childB.Value).to.equal(false) + + local childC = root.ChildC + expect(childC.ClassName).to.equal("StringValue") + expect(childC.Value).to.equal(defaultStringValue) + + local childE = root.ChildE + expect(childE.ClassName).to.equal("Folder") + end) + + it("should update Bindings", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local bindingA, updateA = Binding.create(10) + local element = createElement("IntValue", { + Value = bindingA, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + local instance = parent:GetChildren()[1] + + expect(instance.Value).to.equal(10) + + local bindingB, updateB = Binding.create(99) + local newElement = createElement("IntValue", { + Value = bindingB, + }) + + RobloxRenderer.updateHostNode(reconciler, node, newElement) + + expect(instance.Value).to.equal(99) + + updateA(123) + + expect(instance.Value).to.equal(99) + + updateB(123) + + expect(instance.Value).to.equal(123) + + RobloxRenderer.unmountHostNode(reconciler, node) + end) + + it("should update Binding refs", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local refA = createRef() + local refB = createRef() + + local element = createElement("Frame", { + [Ref] = refA, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local instance = parent:GetChildren()[1] + + expect(refA.current).to.equal(instance) + expect(refB.current).never.to.be.ok() + + local newElement = createElement("Frame", { + [Ref] = refB, + }) + + RobloxRenderer.updateHostNode(reconciler, node, newElement) + + expect(refA.current).never.to.be.ok() + expect(refB.current).to.equal(instance) + + RobloxRenderer.unmountHostNode(reconciler, node) + end) + + it("should call old function refs with nil and new function refs with a valid rbx", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local spyRefA = createSpy() + local spyRefB = createSpy() + + local element = createElement("Frame", { + [Ref] = spyRefA.value, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local instance = parent:GetChildren()[1] + + expect(spyRefA.callCount).to.equal(1) + spyRefA:assertCalledWith(instance) + expect(spyRefB.callCount).to.equal(0) + + local newElement = createElement("Frame", { + [Ref] = spyRefB.value, + }) + + RobloxRenderer.updateHostNode(reconciler, node, newElement) + + expect(spyRefA.callCount).to.equal(2) + spyRefA:assertCalledWith(nil) + expect(spyRefB.callCount).to.equal(1) + spyRefB:assertCalledWith(instance) + + RobloxRenderer.unmountHostNode(reconciler, node) + end) + + it("should not call function refs again if they didn't change", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local spyRef = createSpy() + + local element = createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + [Ref] = spyRef.value, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local instance = parent:GetChildren()[1] + + expect(spyRef.callCount).to.equal(1) + spyRef:assertCalledWith(instance) + + local newElement = createElement("Frame", { + Size = UDim2.new(0.5, 0, 0.5, 0), + [Ref] = spyRef.value, + }) + + RobloxRenderer.updateHostNode(reconciler, node, newElement) + + -- Not called again + expect(spyRef.callCount).to.equal(1) + end) + + it("should throw if setting invalid instance properties", function() + local configValues = { + elementTracing = true, + } + + GlobalConfig.scoped(configValues, function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local firstElement = createElement("Frame") + local secondElement = createElement("Frame", { + Frob = 6, + }) + + local node = reconciler.createVirtualNode(firstElement, parent, key) + RobloxRenderer.mountHostNode(reconciler, node) + + local success, message = pcall(RobloxRenderer.updateHostNode, reconciler, node, secondElement) + assert(not success, "Expected call to fail") + + expect(message:find("Frob")).to.be.ok() + expect(message:find("Frame")).to.be.ok() + expect(message:find("RobloxRenderer%.spec")).to.be.ok() + end) + end) + + it("should delete instances when reconciling to nil children", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local element = createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + child = createElement("Frame"), + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local instance = parent:GetChildren()[1] + expect(#instance:GetChildren()).to.equal(1) + + local newElement = createElement("Frame", { + Size = UDim2.new(0.5, 0, 0.5, 0), + }) + + RobloxRenderer.updateHostNode(reconciler, node, newElement) + expect(#instance:GetChildren()).to.equal(0) + end) + end) + + describe("unmountHostNode", function() + it("should delete instances from the inside-out", function() + local parent = Instance.new("Folder") + local key = "Root" + local element = createElement("Folder", nil, { + Child = createElement("Folder", nil, { + Grandchild = createElement("Folder"), + }), + }) + + local node = reconciler.mountVirtualNode(element, parent, key) + + expect(#parent:GetChildren()).to.equal(1) + + local root = parent:GetChildren()[1] + expect(#root:GetChildren()).to.equal(1) + + local child = root:GetChildren()[1] + expect(#child:GetChildren()).to.equal(1) + + local grandchild = child:GetChildren()[1] + + RobloxRenderer.unmountHostNode(reconciler, node) + + expect(grandchild.Parent).to.equal(nil) + expect(child.Parent).to.equal(nil) + expect(root.Parent).to.equal(nil) + end) + + it("should unsubscribe from any Bindings", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local binding, update = Binding.create(10) + local element = createElement("IntValue", { + Value = binding, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + local instance = parent:GetChildren()[1] + + expect(instance.Value).to.equal(10) + + RobloxRenderer.unmountHostNode(reconciler, node) + update(56) + + expect(instance.Value).to.equal(10) + end) + + it("should clear Binding refs", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local ref = createRef() + local element = createElement("Frame", { + [Ref] = ref, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(ref.current).to.be.ok() + + RobloxRenderer.unmountHostNode(reconciler, node) + + expect(ref.current).never.to.be.ok() + end) + + it("should call function refs with nil", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local spyRef = createSpy() + local element = createElement("Frame", { + [Ref] = spyRef.value, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(spyRef.callCount).to.equal(1) + + RobloxRenderer.unmountHostNode(reconciler, node) + + expect(spyRef.callCount).to.equal(2) + spyRef:assertCalledWith(nil) + end) + end) + + describe("Portals", function() + it("should create and destroy instances as children of `target`", function() + local target = Instance.new("Folder") + + local function FunctionComponent(props) + return createElement("IntValue", { + Value = props.value, + }) + end + + local element = createElement(Portal, { + target = target, + }, { + folderOne = createElement("Folder"), + folderTwo = createElement("Folder"), + intValueOne = createElement(FunctionComponent, { + value = 42, + }), + }) + local hostParent = nil + local hostKey = "Some Key" + local node = reconciler.mountVirtualNode(element, hostParent, hostKey) + + expect(#target:GetChildren()).to.equal(3) + + expect(target:FindFirstChild("folderOne")).to.be.ok() + expect(target:FindFirstChild("folderTwo")).to.be.ok() + expect(target:FindFirstChild("intValueOne")).to.be.ok() + expect(target:FindFirstChild("intValueOne").Value).to.equal(42) + + reconciler.unmountVirtualNode(node) + + expect(#target:GetChildren()).to.equal(0) + end) + + it("should pass prop updates through to children", function() + local target = Instance.new("Folder") + + local firstElement = createElement(Portal, { + target = target, + }, { + ChildValue = createElement("IntValue", { + Value = 1, + }), + }) + + local secondElement = createElement(Portal, { + target = target, + }, { + ChildValue = createElement("IntValue", { + Value = 2, + }), + }) + + local hostParent = nil + local hostKey = "A Host Key" + local node = reconciler.mountVirtualNode(firstElement, hostParent, hostKey) + + expect(#target:GetChildren()).to.equal(1) + + local firstValue = target.ChildValue + expect(firstValue.Value).to.equal(1) + + node = reconciler.updateVirtualNode(node, secondElement) + + expect(#target:GetChildren()).to.equal(1) + + local secondValue = target.ChildValue + expect(firstValue).to.equal(secondValue) + expect(secondValue.Value).to.equal(2) + + reconciler.unmountVirtualNode(node) + + expect(#target:GetChildren()).to.equal(0) + end) + + it("should throw if `target` is nil", function() + -- TODO: Relax this restriction? + local element = createElement(Portal) + local hostParent = nil + local hostKey = "Keys for Everyone" + + expect(function() + reconciler.mountVirtualNode(element, hostParent, hostKey) + end).to.throw() + end) + + it("should throw if `target` is not a Roblox instance", function() + local element = createElement(Portal, { + target = {}, + }) + local hostParent = nil + local hostKey = "Unleash the keys!" + + expect(function() + reconciler.mountVirtualNode(element, hostParent, hostKey) + end).to.throw() + end) + + it("should recreate instances if `target` changes in an update", function() + local firstTarget = Instance.new("Folder") + local secondTarget = Instance.new("Folder") + + local firstElement = createElement(Portal, { + target = firstTarget, + }, { + ChildValue = createElement("IntValue", { + Value = 1, + }), + }) + + local secondElement = createElement(Portal, { + target = secondTarget, + }, { + ChildValue = createElement("IntValue", { + Value = 2, + }), + }) + + local hostParent = nil + local hostKey = "Some Key" + local node = reconciler.mountVirtualNode(firstElement, hostParent, hostKey) + + expect(#firstTarget:GetChildren()).to.equal(1) + expect(#secondTarget:GetChildren()).to.equal(0) + + local firstChild = firstTarget.ChildValue + expect(firstChild.Value).to.equal(1) + + node = reconciler.updateVirtualNode(node, secondElement) + + expect(#firstTarget:GetChildren()).to.equal(0) + expect(#secondTarget:GetChildren()).to.equal(1) + + local secondChild = secondTarget.ChildValue + expect(secondChild.Value).to.equal(2) + + reconciler.unmountVirtualNode(node) + + expect(#firstTarget:GetChildren()).to.equal(0) + expect(#secondTarget:GetChildren()).to.equal(0) + end) + end) + + describe("Fragments", function() + it("should parent the fragment's elements into the fragment's parent", function() + local hostParent = Instance.new("Folder") + + local fragment = createFragment({ + key = createElement("IntValue", { + Value = 1, + }), + key2 = createElement("IntValue", { + Value = 2, + }), + }) + + local node = reconciler.mountVirtualNode(fragment, hostParent, "test") + + expect(hostParent:FindFirstChild("key")).to.be.ok() + expect(hostParent.key.ClassName).to.equal("IntValue") + expect(hostParent.key.Value).to.equal(1) + + expect(hostParent:FindFirstChild("key2")).to.be.ok() + expect(hostParent.key2.ClassName).to.equal("IntValue") + expect(hostParent.key2.Value).to.equal(2) + + reconciler.unmountVirtualNode(node) + + expect(#hostParent:GetChildren()).to.equal(0) + end) + + it("should allow sibling fragment to have common keys", function() + local hostParent = Instance.new("Folder") + local hostKey = "Test" + + local function parent(props) + return createElement("IntValue", {}, { + fragmentA = createFragment({ + key = createElement("StringValue", { + Value = "A", + }), + key2 = createElement("StringValue", { + Value = "B", + }), + }), + fragmentB = createFragment({ + key = createElement("StringValue", { + Value = "C", + }), + key2 = createElement("StringValue", { + Value = "D", + }), + }), + }) + end + + local node = reconciler.mountVirtualNode(createElement(parent), hostParent, hostKey) + local parentChildren = hostParent[hostKey]:GetChildren() + + expect(#parentChildren).to.equal(4) + + local childValues = {} + + for _, child in pairs(parentChildren) do + expect(child.ClassName).to.equal("StringValue") + childValues[child.Value] = 1 + (childValues[child.Value] or 0) + end + + -- check if the StringValues have not collided + expect(childValues.A).to.equal(1) + expect(childValues.B).to.equal(1) + expect(childValues.C).to.equal(1) + expect(childValues.D).to.equal(1) + + reconciler.unmountVirtualNode(node) + + expect(#hostParent:GetChildren()).to.equal(0) + end) + + it("should render nested fragments", function() + local hostParent = Instance.new("Folder") + + local fragment = createFragment({ + key = createFragment({ + TheValue = createElement("IntValue", { + Value = 1, + }), + TheOtherValue = createElement("IntValue", { + Value = 2, + }) + }) + }) + + local node = reconciler.mountVirtualNode(fragment, hostParent, "Test") + + expect(hostParent:FindFirstChild("TheValue")).to.be.ok() + expect(hostParent.TheValue.ClassName).to.equal("IntValue") + expect(hostParent.TheValue.Value).to.equal(1) + + expect(hostParent:FindFirstChild("TheOtherValue")).to.be.ok() + expect(hostParent.TheOtherValue.ClassName).to.equal("IntValue") + expect(hostParent.TheOtherValue.Value).to.equal(2) + + reconciler.unmountVirtualNode(node) + + expect(#hostParent:GetChildren()).to.equal(0) + end) + + it("should not add any instances if the fragment is empty", function() + local hostParent = Instance.new("Folder") + + local node = reconciler.mountVirtualNode(createFragment({}), hostParent, "test") + + expect(#hostParent:GetChildren()).to.equal(0) + + reconciler.unmountVirtualNode(node) + + expect(#hostParent:GetChildren()).to.equal(0) + end) + end) + + describe("Context", function() + it("should pass context values through Roblox host nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + hello = self:__getContext("hello") + } + end + + function Consumer:render() + end + + local element = createElement("Folder", nil, { + Consumer = createElement(Consumer) + }) + local hostParent = nil + local hostKey = "Context Test" + local context = { + hello = "world", + } + local node = reconciler.mountVirtualNode(element, hostParent, hostKey, context) + + expect(capturedContext).never.to.equal(context) + assertDeepEqual(capturedContext, context) + + reconciler.unmountVirtualNode(node) + end) + + it("should pass context values through portal nodes", function() + local target = Instance.new("Folder") + + local Provider = Component:extend("Provider") + + function Provider:init() + self:__addContext("foo", "bar") + end + + function Provider:render() + return createElement("Folder", nil, self.props[Children]) + end + + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + foo = self:__getContext("foo"), + } + end + + function Consumer:render() + return nil + end + + local element = createElement(Provider, nil, { + Portal = createElement(Portal, { + target = target, + }, { + Consumer = createElement(Consumer), + }) + }) + local hostParent = nil + local hostKey = "Some Key" + reconciler.mountVirtualNode(element, hostParent, hostKey) + + assertDeepEqual(capturedContext, { + foo = "bar" + }) + end) + end) + + describe("Legacy context", function() + it("should pass context values through Roblox host nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = self._context + end + + function Consumer:render() + end + + local element = createElement("Folder", nil, { + Consumer = createElement(Consumer) + }) + local hostParent = nil + local hostKey = "Context Test" + local context = { + hello = "world", + } + local node = reconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) + + expect(capturedContext).never.to.equal(context) + assertDeepEqual(capturedContext, context) + + reconciler.unmountVirtualNode(node) + end) + + it("should pass context values through portal nodes", function() + local target = Instance.new("Folder") + + local Provider = Component:extend("Provider") + + function Provider:init() + self._context.foo = "bar" + end + + function Provider:render() + return createElement("Folder", nil, self.props[Children]) + end + + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = self._context + end + + function Consumer:render() + return nil + end + + local element = createElement(Provider, nil, { + Portal = createElement(Portal, { + target = target, + }, { + Consumer = createElement(Consumer), + }) + }) + local hostParent = nil + local hostKey = "Some Key" + reconciler.mountVirtualNode(element, hostParent, hostKey) + + assertDeepEqual(capturedContext, { + foo = "bar" + }) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/SingleEventManager.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/SingleEventManager.lua new file mode 100644 index 0000000..bb579c7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/SingleEventManager.lua @@ -0,0 +1,147 @@ +--[[ + A manager for a single host virtual node's connected events. +]] + +local Logging = require(script.Parent.Logging) + +local CHANGE_PREFIX = "Change." + +local EventStatus = { + -- No events are processed at all; they're silently discarded + Disabled = "Disabled", + + -- Events are stored in a queue; listeners are invoked when the manager is resumed + Suspended = "Suspended", + + -- Event listeners are invoked as the events fire + Enabled = "Enabled", +} + +local SingleEventManager = {} +SingleEventManager.__index = SingleEventManager + +function SingleEventManager.new(instance) + local self = setmetatable({ + -- The queue of suspended events + _suspendedEventQueue = {}, + + -- All the event connections being managed + -- Events are indexed by a string key + _connections = {}, + + -- All the listeners being managed + -- These are stored distinctly from the connections + -- Connections can have their listeners replaced at runtime + _listeners = {}, + + -- The suspension status of the manager + -- Managers start disabled and are "resumed" after the initial render + _status = EventStatus.Disabled, + + -- If true, the manager is processing queued events right now. + _isResuming = false, + + -- The Roblox instance the manager is managing + _instance = instance, + }, SingleEventManager) + + return self +end + +function SingleEventManager:connectEvent(key, listener) + self:_connect(key, self._instance[key], listener) +end + +function SingleEventManager:connectPropertyChange(key, listener) + local success, event = pcall(function() + return self._instance:GetPropertyChangedSignal(key) + end) + + if not success then + error(("Cannot get changed signal on property %q: %s"):format( + tostring(key), + event + ), 0) + end + + self:_connect(CHANGE_PREFIX .. key, event, listener) +end + +function SingleEventManager:_connect(eventKey, event, listener) + -- If the listener doesn't exist we can just disconnect the existing connection + if listener == nil then + if self._connections[eventKey] ~= nil then + self._connections[eventKey]:Disconnect() + self._connections[eventKey] = nil + end + + self._listeners[eventKey] = nil + else + if self._connections[eventKey] == nil then + self._connections[eventKey] = event:Connect(function(...) + if self._status == EventStatus.Enabled then + self._listeners[eventKey](self._instance, ...) + elseif self._status == EventStatus.Suspended then + -- Store this event invocation to be fired when resume is + -- called. + + local argumentCount = select("#", ...) + table.insert(self._suspendedEventQueue, { eventKey, argumentCount, ... }) + end + end) + end + + self._listeners[eventKey] = listener + end +end + +function SingleEventManager:suspend() + self._status = EventStatus.Suspended +end + +function SingleEventManager:resume() + -- If we're already resuming events for this instance, trying to resume + -- again would cause a disaster. + if self._isResuming then + return + end + + self._isResuming = true + + local index = 1 + + -- More events might be added to the queue when evaluating events, so we + -- need to be careful in order to preserve correct evaluation order. + while index <= #self._suspendedEventQueue do + local eventInvocation = self._suspendedEventQueue[index] + local listener = self._listeners[eventInvocation[1]] + local argumentCount = eventInvocation[2] + + -- The event might have been disconnected since suspension started; in + -- this case, we drop the event. + if listener ~= nil then + -- Wrap the listener in a coroutine to catch errors and handle + -- yielding correctly. + local listenerCo = coroutine.create(listener) + local success, result = coroutine.resume( + listenerCo, + self._instance, + unpack(eventInvocation, 3, 2 + argumentCount)) + + -- If the listener threw an error, we log it as a warning, since + -- there's no way to write error text in Roblox Lua without killing + -- our thread! + if not success then + Logging.warn("%s", result) + end + end + + index = index + 1 + end + + self._isResuming = false + self._status = EventStatus.Enabled + self._suspendedEventQueue = {} +end + +return SingleEventManager \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/SingleEventManager.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/SingleEventManager.spec.lua new file mode 100644 index 0000000..9d87e27 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/SingleEventManager.spec.lua @@ -0,0 +1,239 @@ +return function() + local assertDeepEqual = require(script.Parent.assertDeepEqual) + local createSpy = require(script.Parent.createSpy) + local Logging = require(script.Parent.Logging) + + local SingleEventManager = require(script.Parent.SingleEventManager) + + describe("new", function() + it("should create a SingleEventManager", function() + local manager = SingleEventManager.new() + + expect(manager).to.be.ok() + end) + end) + + describe("connectEvent", function() + it("should connect to events", function() + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + local eventSpy = createSpy() + + manager:connectEvent("Event", eventSpy.value) + manager:resume() + + instance:Fire("foo") + expect(eventSpy.callCount).to.equal(1) + eventSpy:assertCalledWith(instance, "foo") + + instance:Fire("bar") + expect(eventSpy.callCount).to.equal(2) + eventSpy:assertCalledWith(instance, "bar") + + manager:connectEvent("Event", nil) + + instance:Fire("baz") + expect(eventSpy.callCount).to.equal(2) + end) + + it("should drop events until resumed initially", function() + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + local eventSpy = createSpy() + + manager:connectEvent("Event", eventSpy.value) + + instance:Fire("foo") + expect(eventSpy.callCount).to.equal(0) + + manager:resume() + + instance:Fire("bar") + expect(eventSpy.callCount).to.equal(1) + eventSpy:assertCalledWith(instance, "bar") + end) + + it("should invoke suspended events when resumed", function() + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + local eventSpy = createSpy() + + manager:connectEvent("Event", eventSpy.value) + manager:resume() + + instance:Fire("foo") + expect(eventSpy.callCount).to.equal(1) + eventSpy:assertCalledWith(instance, "foo") + + manager:suspend() + + instance:Fire("bar") + expect(eventSpy.callCount).to.equal(1) + + manager:resume() + expect(eventSpy.callCount).to.equal(2) + eventSpy:assertCalledWith(instance, "bar") + end) + + it("should invoke events triggered during resumption in the correct order", function() + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + + local recordedValues = {} + local eventSpy = createSpy(function(_, value) + table.insert(recordedValues, value) + + if value == 2 then + instance:Fire(3) + elseif value == 3 then + instance:Fire(4) + end + end) + + manager:connectEvent("Event", eventSpy.value) + manager:suspend() + + instance:Fire(1) + instance:Fire(2) + + manager:resume() + expect(eventSpy.callCount).to.equal(4) + assertDeepEqual(recordedValues, {1, 2, 3, 4}) + end) + + it("should not invoke events fired during suspension but disconnected before resumption", function() + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + local eventSpy = createSpy() + + manager:connectEvent("Event", eventSpy.value) + manager:suspend() + + instance:Fire(1) + + manager:connectEvent("Event", nil) + + manager:resume() + expect(eventSpy.callCount).to.equal(0) + end) + + it("should not yield events through the SingleEventManager when resuming", function() + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + + manager:connectEvent("Event", function() + coroutine.yield() + end) + + manager:resume() + + local co = coroutine.create(function() + instance:Fire(5) + end) + + assert(coroutine.resume(co)) + expect(coroutine.status(co)).to.equal("dead") + + manager:suspend() + instance:Fire(5) + + co = coroutine.create(function() + manager:resume() + end) + + assert(coroutine.resume(co)) + expect(coroutine.status(co)).to.equal("dead") + end) + + it("should not throw errors through SingleEventManager when resuming", function() + local errorText = "Error from SingleEventManager test" + + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + + manager:connectEvent("Event", function() + error(errorText) + end) + + manager:resume() + + -- If we call instance:Fire() here, the error message will leak to + -- the console since the thread's resumption will be handled by + -- Roblox's scheduler. + + manager:suspend() + instance:Fire(5) + + local logInfo = Logging.capture(function() + manager:resume() + end) + + expect(#logInfo.errors).to.equal(0) + expect(#logInfo.warnings).to.equal(1) + expect(#logInfo.infos).to.equal(0) + + expect(logInfo.warnings[1]:find(errorText)).to.be.ok() + end) + + it("should not overflow with events if manager:resume() is invoked when resuming a suspended event", function() + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + + -- This connection emulates what happens if reconciliation is + -- triggered again in response to reconciliation. Without + -- appropriate guards, the inner resume() call will process the + -- Fire(1) event again, causing a nasty stack overflow. + local eventSpy = createSpy(function(_, value) + if value == 1 then + manager:suspend() + instance:Fire(2) + manager:resume() + end + end) + + manager:connectEvent("Event", eventSpy.value) + + manager:suspend() + instance:Fire(1) + manager:resume() + + expect(eventSpy.callCount).to.equal(2) + end) + end) + + describe("connectPropertyChange", function() + -- Since property changes utilize the same mechanisms as other events, + -- the tests here are slimmed down to reduce redundancy. + + it("should connect to property changes", function() + local instance = Instance.new("Folder") + local manager = SingleEventManager.new(instance) + local eventSpy = createSpy() + + manager:connectPropertyChange("Name", eventSpy.value) + manager:resume() + + instance.Name = "foo" + expect(eventSpy.callCount).to.equal(1) + eventSpy:assertCalledWith(instance) + + instance.Name = "bar" + expect(eventSpy.callCount).to.equal(2) + eventSpy:assertCalledWith(instance) + + manager:connectPropertyChange("Name") + + instance.Name = "baz" + expect(eventSpy.callCount).to.equal(2) + end) + + it("should throw an error if the property is invalid", function() + local instance = Instance.new("Folder") + local manager = SingleEventManager.new(instance) + + expect(function() + manager:connectPropertyChange("foo", function() end) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Symbol.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Symbol.lua new file mode 100644 index 0000000..305d66a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Symbol.lua @@ -0,0 +1,30 @@ +--[[ + A 'Symbol' is an opaque marker type. + + Symbols have the type 'userdata', but when printed to the console, the name + of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Symbol.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Symbol.spec.lua new file mode 100644 index 0000000..e05061d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Symbol.spec.lua @@ -0,0 +1,24 @@ +return function() + local Symbol = require(script.Parent.Symbol) + + describe("named", function() + it("should give an opaque object", function() + local symbol = Symbol.named("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = Symbol.named("foo") + + expect(tostring(symbol):find("foo")).to.be.ok() + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.named("abc") + local symbolB = Symbol.named("abc") + + expect(symbolA).never.to.equal(symbolB) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Type.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Type.lua new file mode 100644 index 0000000..156ee0e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Type.lua @@ -0,0 +1,48 @@ +--[[ + Contains markers for annotating objects with types. + + To set the type of an object, use `Type` as a key and the actual marker as + the value: + + local foo = { + [Type] = Type.Foo, + } +]] + +local Symbol = require(script.Parent.Symbol) +local strict = require(script.Parent.strict) + +local Type = newproxy(true) + +local TypeInternal = {} + +local function addType(name) + TypeInternal[name] = Symbol.named("Roact" .. name) +end + +addType("Binding") +addType("Element") +addType("HostChangeEvent") +addType("HostEvent") +addType("StatefulComponentClass") +addType("StatefulComponentInstance") +addType("VirtualNode") +addType("VirtualTree") + +function TypeInternal.of(value) + if typeof(value) ~= "table" then + return nil + end + + return value[Type] +end + +getmetatable(Type).__index = TypeInternal + +getmetatable(Type).__tostring = function() + return "RoactType" +end + +strict(TypeInternal, "Type") + +return Type \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Type.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Type.spec.lua new file mode 100644 index 0000000..f247709 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Type.spec.lua @@ -0,0 +1,24 @@ +return function() + local Type = require(script.Parent.Type) + + describe("of", function() + it("should return nil if the value is not a table", function() + expect(Type.of(1)).to.equal(nil) + expect(Type.of(true)).to.equal(nil) + expect(Type.of("test")).to.equal(nil) + expect(Type.of(print)).to.equal(nil) + end) + + it("should return nil if the table has no type", function() + expect(Type.of({})).to.equal(nil) + end) + + it("should return the assigned type", function() + local test = { + [Type] = Type.Element + } + + expect(Type.of(test)).to.equal(Type.Element) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assertDeepEqual.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assertDeepEqual.lua new file mode 100644 index 0000000..3f422d8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assertDeepEqual.lua @@ -0,0 +1,73 @@ +--[[ + A utility used to assert that two objects are value-equal recursively. It + outputs fairly nicely formatted messages to help diagnose why two objects + would be different. + + This should only be used in tests. +]] + +local function deepEqual(a, b) + if typeof(a) ~= typeof(b) then + local message = ("{1} is of type %s, but {2} is of type %s"):format( + typeof(a), + typeof(b) + ) + return false, message + end + + if typeof(a) == "table" then + local visitedKeys = {} + + for key, value in pairs(a) do + visitedKeys[key] = true + + local success, innerMessage = deepEqual(value, b[key]) + if not success then + local message = innerMessage + :gsub("{1}", ("{1}[%s]"):format(tostring(key))) + :gsub("{2}", ("{2}[%s]"):format(tostring(key))) + + return false, message + end + end + + for key, value in pairs(b) do + if not visitedKeys[key] then + local success, innerMessage = deepEqual(value, a[key]) + + if not success then + local message = innerMessage + :gsub("{1}", ("{1}[%s]"):format(tostring(key))) + :gsub("{2}", ("{2}[%s]"):format(tostring(key))) + + return false, message + end + end + end + + return true + end + + if a == b then + return true + end + + local message = "{1} ~= {2}" + return false, message +end + +local function assertDeepEqual(a, b) + local success, innerMessageTemplate = deepEqual(a, b) + + if not success then + local innerMessage = innerMessageTemplate + :gsub("{1}", "first") + :gsub("{2}", "second") + + local message = ("Values were not deep-equal.\n%s"):format(innerMessage) + + error(message, 2) + end +end + +return assertDeepEqual \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assertDeepEqual.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assertDeepEqual.spec.lua new file mode 100644 index 0000000..bece8d7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assertDeepEqual.spec.lua @@ -0,0 +1,99 @@ +return function() + local assertDeepEqual = require(script.Parent.assertDeepEqual) + + it("should fail with a message when args are not equal", function() + local success, message = pcall(assertDeepEqual, 1, 2) + + expect(success).to.equal(false) + expect(message:find("first ~= second")).to.be.ok() + + success, message = pcall(assertDeepEqual, { + foo = 1, + }, { + foo = 2, + }) + + expect(success).to.equal(false) + expect(message:find("first%[foo%] ~= second%[foo%]")).to.be.ok() + end) + + it("should compare non-table values using standard '==' equality", function() + assertDeepEqual(1, 1) + assertDeepEqual("hello", "hello") + assertDeepEqual(nil, nil) + + local someFunction = function() end + local theSameFunction = someFunction + + assertDeepEqual(someFunction, theSameFunction) + + local A = { + foo = someFunction + } + local B = { + foo = theSameFunction + } + + assertDeepEqual(A, B) + end) + + it("should fail when types differ", function() + local success, message = pcall(assertDeepEqual, 1, "1") + + expect(success).to.equal(false) + expect(message:find("first is of type number, but second is of type string")).to.be.ok() + end) + + it("should compare (and report about) nested tables", function() + local A = { + foo = "bar", + nested = { + foo = 1, + bar = 2, + } + } + local B = { + foo = "bar", + nested = { + foo = 1, + bar = 2, + } + } + + assertDeepEqual(A, B) + + local C = { + foo = "bar", + nested = { + foo = 1, + bar = 3, + } + } + + local success, message = pcall(assertDeepEqual, A, C) + + expect(success).to.equal(false) + expect(message:find("first%[nested%]%[bar%] ~= second%[nested%]%[bar%]")).to.be.ok() + end) + + it("should be commutative", function() + local equalArgsA = { + foo = "bar", + hello = "world", + } + local equalArgsB = { + foo = "bar", + hello = "world", + } + + assertDeepEqual(equalArgsA, equalArgsB) + assertDeepEqual(equalArgsB, equalArgsA) + + local nonEqualArgs = { + foo = "bar", + } + + expect(function() assertDeepEqual(equalArgsA, nonEqualArgs) end).to.throw() + expect(function() assertDeepEqual(nonEqualArgs, equalArgsA) end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assign.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assign.lua new file mode 100644 index 0000000..704c165 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assign.lua @@ -0,0 +1,27 @@ +local None = require(script.Parent.None) + +--[[ + Merges values from zero or more tables onto a target table. If a value is + set to None, it will instead be removed from the table. + + This function is identical in functionality to JavaScript's Object.assign. +]] +local function assign(target, ...) + for index = 1, select("#", ...) do + local source = select(index, ...) + + if source ~= nil then + for key, value in pairs(source) do + if value == None then + target[key] = nil + else + target[key] = value + end + end + end + end + + return target +end + +return assign \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assign.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assign.spec.lua new file mode 100644 index 0000000..24784a1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assign.spec.lua @@ -0,0 +1,68 @@ +return function() + local None = require(script.Parent.None) + + local assign = require(script.Parent.assign) + + it("should accept zero additional tables", function() + local input = {} + local result = assign(input) + + expect(input).to.equal(result) + end) + + it("should merge multiple tables onto the given target table", function() + local target = { + a = 5, + b = 6, + } + + local source1 = { + b = 7, + c = 8, + } + + local source2 = { + b = 8, + } + + assign(target, source1, source2) + + expect(target.a).to.equal(5) + expect(target.b).to.equal(source2.b) + expect(target.c).to.equal(source1.c) + end) + + it("should remove keys if specified as None", function() + local target = { + foo = 2, + bar = 3, + } + + local source = { + foo = None, + } + + assign(target, source) + + expect(target.foo).to.equal(nil) + expect(target.bar).to.equal(3) + end) + + it("should re-add keys if specified after None", function() + local target = { + foo = 2, + } + + local source1 = { + foo = None, + } + + local source2 = { + foo = 3, + } + + assign(target, source1, source2) + + expect(target.foo).to.equal(source2.foo) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createContext.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createContext.lua new file mode 100644 index 0000000..b21635e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createContext.lua @@ -0,0 +1,151 @@ +local Symbol = require(script.Parent.Symbol) +local createFragment = require(script.Parent.createFragment) +local createSignal = require(script.Parent.createSignal) +local Children = require(script.Parent.PropMarkers.Children) +local Component = require(script.Parent.Component) + +--[[ + Construct the value that is assigned to Roact's context storage. +]] +local function createContextEntry(currentValue) + return { + value = currentValue, + onUpdate = createSignal(), + } +end + +local function createProvider(context) + local Provider = Component:extend("Provider") + + function Provider:init(props) + self.contextEntry = createContextEntry(props.value) + self:__addContext(context.key, self.contextEntry) + end + + function Provider:willUpdate(nextProps) + -- If the provided value changed, immediately update the context entry. + -- + -- During this update, any components that are reachable will receive + -- this updated value at the same time as any props and state updates + -- that are being applied. + if nextProps.value ~= self.props.value then + self.contextEntry.value = nextProps.value + end + end + + function Provider:didUpdate(prevProps) + -- If the provided value changed, after we've updated every reachable + -- component, fire a signal to update the rest. + -- + -- This signal will notify all context consumers. It's expected that + -- they will compare the last context value they updated with and only + -- trigger an update on themselves if this value is different. + -- + -- This codepath will generally only update consumer components that has + -- a component implementing shouldUpdate between them and the provider. + if prevProps.value ~= self.props.value then + self.contextEntry.onUpdate:fire(self.props.value) + end + end + + function Provider:render() + return createFragment(self.props[Children]) + end + + return Provider +end + +local function createConsumer(context) + local Consumer = Component:extend("Consumer") + + function Consumer.validateProps(props) + if type(props.render) ~= "function" then + return false, "Consumer expects a `render` function" + else + return true + end + end + + function Consumer:init(props) + -- This value may be nil, which indicates that our consumer is not a + -- descendant of a provider for this context item. + self.contextEntry = self:__getContext(context.key) + end + + function Consumer:render() + -- Render using the latest available for this context item. + -- + -- We don't store this value in state in order to have more fine-grained + -- control over our update behavior. + local value + if self.contextEntry ~= nil then + value = self.contextEntry.value + else + value = context.defaultValue + end + + return self.props.render(value) + end + + function Consumer:didUpdate() + -- Store the value that we most recently updated with. + -- + -- This value is compared in the contextEntry onUpdate hook below. + if self.contextEntry ~= nil then + self.lastValue = self.contextEntry.value + end + end + + function Consumer:didMount() + if self.contextEntry ~= nil then + -- When onUpdate is fired, a new value has been made available in + -- this context entry, but we may have already updated in the same + -- update cycle. + -- + -- To avoid sending a redundant update, we compare the new value + -- with the last value that we updated with (set in didUpdate) and + -- only update if they differ. This may happen when an update from a + -- provider was blocked by an intermediate component that returned + -- false from shouldUpdate. + self.disconnect = self.contextEntry.onUpdate:subscribe(function(newValue) + if newValue ~= self.lastValue then + -- Trigger a dummy state update. + self:setState({}) + end + end) + end + end + + function Consumer:willUnmount() + if self.disconnect ~= nil then + self.disconnect() + end + end + + return Consumer +end + +local Context = {} +Context.__index = Context + +function Context.new(defaultValue) + return setmetatable({ + defaultValue = defaultValue, + key = Symbol.named("ContextKey"), + }, Context) +end + +function Context:__tostring() + return "RoactContext" +end + +local function createContext(defaultValue) + local context = Context.new(defaultValue) + + return { + Provider = createProvider(context), + Consumer = createConsumer(context), + } +end + +return createContext diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createContext.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createContext.spec.lua new file mode 100644 index 0000000..432d39d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createContext.spec.lua @@ -0,0 +1,304 @@ +return function() + local Component = require(script.Parent.Component) + local NoopRenderer = require(script.Parent.NoopRenderer) + local Children = require(script.Parent.PropMarkers.Children) + local createContext = require(script.Parent.createContext) + local createElement = require(script.Parent.createElement) + local createFragment = require(script.Parent.createFragment) + local createReconciler = require(script.Parent.createReconciler) + local createSpy = require(script.Parent.createSpy) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should return a table", function() + local context = createContext("Test") + expect(context).to.be.ok() + expect(type(context)).to.equal("table") + end) + + it("should contain a Provider and a Consumer", function() + local context = createContext("Test") + expect(context.Provider).to.be.ok() + expect(context.Consumer).to.be.ok() + end) + + describe("Provider", function() + it("should render its children", function() + local context = createContext("Test") + + local Listener = createSpy(function() + return nil + end) + + local element = createElement(context.Provider, { + value = "Test", + }, { + Listener = createElement(Listener.value), + }) + + local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + noopReconciler.unmountVirtualTree(tree) + + expect(Listener.callCount).to.equal(1) + end) + end) + + describe("Consumer", function() + it("should expect a render function", function() + local context = createContext("Test") + local element = createElement(context.Consumer) + + expect(function() + noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + end).to.throw() + end) + + it("should return the default value if there is no Provider", function() + local valueSpy = createSpy() + local context = createContext("Test") + + local element = createElement(context.Consumer, { + render = valueSpy.value, + }) + + local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + noopReconciler.unmountVirtualTree(tree) + + valueSpy:assertCalledWith("Test") + end) + + it("should pass the value to the render function", function() + local valueSpy = createSpy() + local context = createContext("Test") + + local function Listener() + return createElement(context.Consumer, { + render = valueSpy.value, + }) + end + + local element = createElement(context.Provider, { + value = "NewTest", + }, { + Listener = createElement(Listener), + }) + + local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + noopReconciler.unmountVirtualTree(tree) + + valueSpy:assertCalledWith("NewTest") + end) + + it("should update when the value updates", function() + local valueSpy = createSpy() + local context = createContext("Test") + + local function Listener() + return createElement(context.Consumer, { + render = valueSpy.value, + }) + end + + local element = createElement(context.Provider, { + value = "NewTest", + }, { + Listener = createElement(Listener), + }) + + local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + + expect(valueSpy.callCount).to.equal(1) + valueSpy:assertCalledWith("NewTest") + + noopReconciler.updateVirtualTree(tree, createElement(context.Provider, { + value = "ThirdTest", + }, { + Listener = createElement(Listener), + })) + + expect(valueSpy.callCount).to.equal(2) + valueSpy:assertCalledWith("ThirdTest") + + noopReconciler.unmountVirtualTree(tree) + end) + + --[[ + This test is the same as the one above, but with a component that + always blocks updates in the middle. We expect behavior to be the + same. + ]] + it("should update when the value updates through an update blocking component", function() + local valueSpy = createSpy() + local context = createContext("Test") + + local UpdateBlocker = Component:extend("UpdateBlocker") + + function UpdateBlocker:render() + return createFragment(self.props[Children]) + end + + function UpdateBlocker:shouldUpdate() + return false + end + + local function Listener() + return createElement(context.Consumer, { + render = valueSpy.value, + }) + end + + local element = createElement(context.Provider, { + value = "NewTest", + }, { + Blocker = createElement(UpdateBlocker, nil, { + Listener = createElement(Listener), + }), + }) + + local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + + expect(valueSpy.callCount).to.equal(1) + valueSpy:assertCalledWith("NewTest") + + noopReconciler.updateVirtualTree(tree, createElement(context.Provider, { + value = "ThirdTest", + }, { + Blocker = createElement(UpdateBlocker, nil, { + Listener = createElement(Listener), + }), + })) + + expect(valueSpy.callCount).to.equal(2) + valueSpy:assertCalledWith("ThirdTest") + + noopReconciler.unmountVirtualTree(tree) + end) + + it("should behave correctly when the default value is nil", function() + local context = createContext(nil) + + local valueSpy = createSpy() + local function Listener() + return createElement(context.Consumer, { + render = valueSpy.value, + }) + end + + local tree = noopReconciler.mountVirtualTree(createElement(Listener), nil, "Provide Tree") + expect(valueSpy.callCount).to.equal(1) + valueSpy:assertCalledWith(nil) + + tree = noopReconciler.updateVirtualTree(tree, createElement(Listener)) + noopReconciler.unmountVirtualTree(tree) + + expect(valueSpy.callCount).to.equal(2) + valueSpy:assertCalledWith(nil) + end) + end) + + describe("Update order", function() + --[[ + This test ensures that there is no scenario where we can observe + 'update tearing' when props and context are updated at the same + time. + + Update tearing is scenario where a single update is partially + applied in multiple steps instead of atomically. This is observable + by components and can lead to strange bugs or errors. + + This instance of update tearing happens when updating a prop and a + context value in the same update. Image we represent our tree's + state as the current prop and context versions. Our initial state + is: + + (prop_1, context_1) + + The next state we would like to update to is: + + (prop_2, context_2) + + Under the bug reported in issue 259, Roact reaches three different + states in sequence: + + 1: (prop_1, context_1) - the initial state + 2: (prop_2, context_1) - woops! + 3: (prop_2, context_2) - correct end state + + In state 2, a user component was added that tried to access the + current context value, which was not set at the time. This raised an + error, because this state is not valid! + + The first proposed solution was to move the context update to happen + before the props update. It is easy to show that this will still + result in update tearing: + + 1: (prop_1, context_1) + 2: (prop_1, context_2) + 3: (prop_2, context_2) + + Although the initial concern about newly added components observing + old context values is fixed, there is still a state + desynchronization between props and state. + + We would instead like the following update sequence: + + 1: (prop_1, context_1) + 2: (prop_2, context_2) + + This test tries to ensure that is the case. + + The initial bug report is here: + https://github.com/Roblox/roact/issues/259 + ]] + it("should update context at the same time as props", function() + -- These values are used to make sure we reach both the first and + -- second state combinations we want to visit. + local observedA = false + local observedB = false + local updateCount = 0 + + local context = createContext("default") + + local function Listener(props) + return createElement(context.Consumer, { + render = function(value) + updateCount = updateCount + 1 + + if value == "context_1" then + expect(props.someProp).to.equal("prop_1") + observedA = true + elseif value == "context_2" then + expect(props.someProp).to.equal("prop_2") + observedB = true + else + error("Unexpected context value") + end + end, + }) + end + + local element1 = createElement(context.Provider, { + value = "context_1", + }, { + Child = createElement(Listener, { + someProp = "prop_1", + }), + }) + + local element2 = createElement(context.Provider, { + value = "context_2", + }, { + Child = createElement(Listener, { + someProp = "prop_2", + }), + }) + + local tree = noopReconciler.mountVirtualTree(element1, nil, "UpdateObservationIsFun") + noopReconciler.updateVirtualTree(tree, element2) + + expect(updateCount).to.equal(2) + expect(observedA).to.equal(true) + expect(observedB).to.equal(true) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createElement.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createElement.lua new file mode 100644 index 0000000..b902219 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createElement.lua @@ -0,0 +1,74 @@ +local Children = require(script.Parent.PropMarkers.Children) +local ElementKind = require(script.Parent.ElementKind) +local Logging = require(script.Parent.Logging) +local Type = require(script.Parent.Type) + +local config = require(script.Parent.GlobalConfig).get() + +local multipleChildrenMessage = [[ +The prop `Roact.Children` was defined but was overriden by the third parameter to createElement! +This can happen when a component passes props through to a child element but also uses the `children` argument: + + Roact.createElement("Frame", passedProps, { + child = ... + }) + +Instead, consider using a utility function to merge tables of children together: + + local children = mergeTables(passedProps[Roact.Children], { + child = ... + }) + + local fullProps = mergeTables(passedProps, { + [Roact.Children] = children + }) + + Roact.createElement("Frame", fullProps)]] + +--[[ + Creates a new element representing the given component. + + Elements are lightweight representations of what a component instance should + look like. + + Children is a shorthand for specifying `Roact.Children` as a key inside + props. If specified, the passed `props` table is mutated! +]] +local function createElement(component, props, children) + if config.typeChecks then + assert(component ~= nil, "`component` is required") + assert(typeof(props) == "table" or props == nil, "`props` must be a table or nil") + assert(typeof(children) == "table" or children == nil, "`children` must be a table or nil") + end + + if props == nil then + props = {} + end + + if children ~= nil then + if props[Children] ~= nil then + Logging.warnOnce(multipleChildrenMessage) + end + + props[Children] = children + end + + local elementKind = ElementKind.fromComponent(component) + + local element = { + [Type] = Type.Element, + [ElementKind] = elementKind, + component = component, + props = props, + } + + if config.elementTracing then + -- We trim out the leading newline since there's no way to specify the + -- trace level without also specifying a message. + element.source = debug.traceback("", 2):sub(2) + end + + return element +end + +return createElement \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createElement.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createElement.spec.lua new file mode 100644 index 0000000..6e05709 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createElement.spec.lua @@ -0,0 +1,110 @@ +return function() + local Component = require(script.Parent.Component) + local ElementKind = require(script.Parent.ElementKind) + local GlobalConfig = require(script.Parent.GlobalConfig) + local Logging = require(script.Parent.Logging) + local Type = require(script.Parent.Type) + local Portal = require(script.Parent.Portal) + local Children = require(script.Parent.PropMarkers.Children) + + local createElement = require(script.Parent.createElement) + + it("should create new primitive elements", function() + local element = createElement("Frame") + + expect(element).to.be.ok() + expect(Type.of(element)).to.equal(Type.Element) + expect(ElementKind.of(element)).to.equal(ElementKind.Host) + end) + + it("should create new functional elements", function() + local element = createElement(function() + end) + + expect(element).to.be.ok() + expect(Type.of(element)).to.equal(Type.Element) + expect(ElementKind.of(element)).to.equal(ElementKind.Function) + end) + + it("should create new stateful components", function() + local Foo = Component:extend("Foo") + + local element = createElement(Foo) + + expect(element).to.be.ok() + expect(Type.of(element)).to.equal(Type.Element) + expect(ElementKind.of(element)).to.equal(ElementKind.Stateful) + end) + + it("should create new portal elements", function() + local element = createElement(Portal) + + expect(element).to.be.ok() + expect(Type.of(element)).to.equal(Type.Element) + expect(ElementKind.of(element)).to.equal(ElementKind.Portal) + end) + + it("should accept props", function() + local element = createElement("StringValue", { + Value = "Foo", + }) + + expect(element).to.be.ok() + expect(element.props.Value).to.equal("Foo") + end) + + it("should accept props and children", function() + local child = createElement("IntValue") + + local element = createElement("StringValue", { + Value = "Foo", + }, { + Child = child, + }) + + expect(element).to.be.ok() + expect(element.props.Value).to.equal("Foo") + expect(element.props[Children]).to.be.ok() + expect(element.props[Children].Child).to.equal(child) + end) + + it("should accept children with without props", function() + local child = createElement("IntValue") + + local element = createElement("StringValue", nil, { + Child = child, + }) + + expect(element).to.be.ok() + expect(element.props[Children]).to.be.ok() + expect(element.props[Children].Child).to.equal(child) + end) + + it("should warn once if children is specified in two different ways", function() + local logInfo = Logging.capture(function() + -- Using a loop here to ensure that multiple occurences of the same + -- warning only cause output once. + for _ = 1, 2 do + createElement("Frame", { + [Children] = {}, + }, {}) + end + end) + + expect(#logInfo.warnings).to.equal(1) + expect(logInfo.warnings[1]:find("createElement")).to.be.ok() + expect(logInfo.warnings[1]:find("Children")).to.be.ok() + end) + + it("should have a `source` member if elementTracing is set", function() + local config = { + elementTracing = true, + } + + GlobalConfig.scoped(config, function() + local element = createElement("StringValue") + + expect(element.source).to.be.a("string") + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createFragment.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createFragment.lua new file mode 100644 index 0000000..91554f3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createFragment.lua @@ -0,0 +1,12 @@ +local ElementKind = require(script.Parent.ElementKind) +local Type = require(script.Parent.Type) + +local function createFragment(elements) + return { + [Type] = Type.Element, + [ElementKind] = ElementKind.Fragment, + elements = elements, + } +end + +return createFragment \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createFragment.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createFragment.spec.lua new file mode 100644 index 0000000..45de6c7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createFragment.spec.lua @@ -0,0 +1,21 @@ +return function() + local ElementKind = require(script.Parent.ElementKind) + local Type = require(script.Parent.Type) + + local createFragment = require(script.Parent.createFragment) + + it("should create new primitive elements", function() + local fragment = createFragment({}) + + expect(fragment).to.be.ok() + expect(Type.of(fragment)).to.equal(Type.Element) + expect(ElementKind.of(fragment)).to.equal(ElementKind.Fragment) + end) + + it("should accept children", function() + local subFragment = createFragment({}) + local fragment = createFragment({key = subFragment}) + + expect(fragment.elements.key).to.equal(subFragment) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconciler.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconciler.lua new file mode 100644 index 0000000..e4e43c6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconciler.lua @@ -0,0 +1,456 @@ +local Type = require(script.Parent.Type) +local ElementKind = require(script.Parent.ElementKind) +local ElementUtils = require(script.Parent.ElementUtils) +local Children = require(script.Parent.PropMarkers.Children) +local Symbol = require(script.Parent.Symbol) +local internalAssert = require(script.Parent.internalAssert) + +local config = require(script.Parent.GlobalConfig).get() + +local InternalData = Symbol.named("InternalData") + +--[[ + The reconciler is the mechanism in Roact that constructs the virtual tree + that later gets turned into concrete objects by the renderer. + + Roact's reconciler is constructed with the renderer as an argument, which + enables switching to different renderers for different platforms or + scenarios. + + When testing the reconciler itself, it's common to use `NoopRenderer` with + spies replacing some methods. The default (and only) reconciler interface + exposed by Roact right now uses `RobloxRenderer`. +]] +local function createReconciler(renderer) + local reconciler + local mountVirtualNode + local updateVirtualNode + local unmountVirtualNode + + --[[ + Unmount the given virtualNode, replacing it with a new node described by + the given element. + + Preserves host properties, depth, and legacyContext from parent. + ]] + local function replaceVirtualNode(virtualNode, newElement) + local hostParent = virtualNode.hostParent + local hostKey = virtualNode.hostKey + local depth = virtualNode.depth + + -- If the node that is being replaced has modified context, we need to + -- use the original *unmodified* context for the new node + -- The `originalContext` field will be nil if the context was unchanged + local context = virtualNode.originalContext or virtualNode.context + local parentLegacyContext = virtualNode.parentLegacyContext + + unmountVirtualNode(virtualNode) + local newNode = mountVirtualNode(newElement, hostParent, hostKey, context, parentLegacyContext) + + -- mountVirtualNode can return nil if the element is a boolean + if newNode ~= nil then + newNode.depth = depth + end + + return newNode + end + + --[[ + Utility to update the children of a virtual node based on zero or more + updated children given as elements. + ]] + local function updateChildren(virtualNode, hostParent, newChildElements) + if config.internalTypeChecks then + internalAssert(Type.of(virtualNode) == Type.VirtualNode, "Expected arg #1 to be of type VirtualNode") + end + + local removeKeys = {} + + -- Changed or removed children + for childKey, childNode in pairs(virtualNode.children) do + local newElement = ElementUtils.getElementByKey(newChildElements, childKey) + local newNode = updateVirtualNode(childNode, newElement) + + if newNode ~= nil then + virtualNode.children[childKey] = newNode + else + removeKeys[childKey] = true + end + end + + for childKey in pairs(removeKeys) do + virtualNode.children[childKey] = nil + end + + -- Added children + for childKey, newElement in ElementUtils.iterateElements(newChildElements) do + local concreteKey = childKey + if childKey == ElementUtils.UseParentKey then + concreteKey = virtualNode.hostKey + end + + if virtualNode.children[childKey] == nil then + local childNode = mountVirtualNode( + newElement, + hostParent, + concreteKey, + virtualNode.context, + virtualNode.legacyContext + ) + + -- mountVirtualNode can return nil if the element is a boolean + if childNode ~= nil then + childNode.depth = virtualNode.depth + 1 + virtualNode.children[childKey] = childNode + end + end + end + end + + local function updateVirtualNodeWithChildren(virtualNode, hostParent, newChildElements) + updateChildren(virtualNode, hostParent, newChildElements) + end + + local function updateVirtualNodeWithRenderResult(virtualNode, hostParent, renderResult) + if Type.of(renderResult) == Type.Element + or renderResult == nil + or typeof(renderResult) == "boolean" + then + updateChildren(virtualNode, hostParent, renderResult) + else + error(("%s\n%s"):format( + "Component returned invalid children:", + virtualNode.currentElement.source or "" + ), 0) + end + end + + --[[ + Unmounts the given virtual node and releases any held resources. + ]] + function unmountVirtualNode(virtualNode) + if config.internalTypeChecks then + internalAssert(Type.of(virtualNode) == Type.VirtualNode, "Expected arg #1 to be of type VirtualNode") + end + + local kind = ElementKind.of(virtualNode.currentElement) + + if kind == ElementKind.Host then + renderer.unmountHostNode(reconciler, virtualNode) + elseif kind == ElementKind.Function then + for _, childNode in pairs(virtualNode.children) do + unmountVirtualNode(childNode) + end + elseif kind == ElementKind.Stateful then + virtualNode.instance:__unmount() + elseif kind == ElementKind.Portal then + for _, childNode in pairs(virtualNode.children) do + unmountVirtualNode(childNode) + end + elseif kind == ElementKind.Fragment then + for _, childNode in pairs(virtualNode.children) do + unmountVirtualNode(childNode) + end + else + error(("Unknown ElementKind %q"):format(tostring(kind), 2)) + end + end + + local function updateFunctionVirtualNode(virtualNode, newElement) + local children = newElement.component(newElement.props) + + updateVirtualNodeWithRenderResult(virtualNode, virtualNode.hostParent, children) + + return virtualNode + end + + local function updatePortalVirtualNode(virtualNode, newElement) + local oldElement = virtualNode.currentElement + local oldTargetHostParent = oldElement.props.target + + local targetHostParent = newElement.props.target + + assert(renderer.isHostObject(targetHostParent), "Expected target to be host object") + + if targetHostParent ~= oldTargetHostParent then + return replaceVirtualNode(virtualNode, newElement) + end + + local children = newElement.props[Children] + + updateVirtualNodeWithChildren(virtualNode, targetHostParent, children) + + return virtualNode + end + + local function updateFragmentVirtualNode(virtualNode, newElement) + updateVirtualNodeWithChildren(virtualNode, virtualNode.hostParent, newElement.elements) + + return virtualNode + end + + --[[ + Update the given virtual node using a new element describing what it + should transform into. + + `updateVirtualNode` will return a new virtual node that should replace + the passed in virtual node. This is because a virtual node can be + updated with an element referencing a different component! + + In that case, `updateVirtualNode` will unmount the input virtual node, + mount a new virtual node, and return it in this case, while also issuing + a warning to the user. + ]] + function updateVirtualNode(virtualNode, newElement, newState) + if config.internalTypeChecks then + internalAssert(Type.of(virtualNode) == Type.VirtualNode, "Expected arg #1 to be of type VirtualNode") + end + if config.typeChecks then + assert( + Type.of(newElement) == Type.Element or typeof(newElement) == "boolean" or newElement == nil, + "Expected arg #2 to be of type Element, boolean, or nil" + ) + end + + -- If nothing changed, we can skip this update + if virtualNode.currentElement == newElement and newState == nil then + return virtualNode + end + + if typeof(newElement) == "boolean" or newElement == nil then + unmountVirtualNode(virtualNode) + return nil + end + + if virtualNode.currentElement.component ~= newElement.component then + return replaceVirtualNode(virtualNode, newElement) + end + + local kind = ElementKind.of(newElement) + + local shouldContinueUpdate = true + + if kind == ElementKind.Host then + virtualNode = renderer.updateHostNode(reconciler, virtualNode, newElement) + elseif kind == ElementKind.Function then + virtualNode = updateFunctionVirtualNode(virtualNode, newElement) + elseif kind == ElementKind.Stateful then + shouldContinueUpdate = virtualNode.instance:__update(newElement, newState) + elseif kind == ElementKind.Portal then + virtualNode = updatePortalVirtualNode(virtualNode, newElement) + elseif kind == ElementKind.Fragment then + virtualNode = updateFragmentVirtualNode(virtualNode, newElement) + else + error(("Unknown ElementKind %q"):format(tostring(kind), 2)) + end + + -- Stateful components can abort updates via shouldUpdate. If that + -- happens, we should stop doing stuff at this point. + if not shouldContinueUpdate then + return virtualNode + end + + virtualNode.currentElement = newElement + + return virtualNode + end + + --[[ + Constructs a new virtual node but not does mount it. + ]] + local function createVirtualNode(element, hostParent, hostKey, context, legacyContext) + if config.internalTypeChecks then + internalAssert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") + internalAssert(typeof(context) == "table" or context == nil, "Expected arg #4 to be of type table or nil") + internalAssert( + typeof(legacyContext) == "table" or legacyContext == nil, + "Expected arg #5 to be of type table or nil" + ) + end + if config.typeChecks then + assert(hostKey ~= nil, "Expected arg #3 to be non-nil") + assert( + Type.of(element) == Type.Element or typeof(element) == "boolean", + "Expected arg #1 to be of type Element or boolean" + ) + end + + return { + [Type] = Type.VirtualNode, + currentElement = element, + depth = 1, + children = {}, + hostParent = hostParent, + hostKey = hostKey, + + -- Legacy Context API + -- A table of context values inherited from the parent node + legacyContext = legacyContext, + + -- A saved copy of the parent context, used when replacing a node + parentLegacyContext = legacyContext, + + -- Context API + -- A table of context values inherited from the parent node + context = context or {}, + + -- A saved copy of the unmodified context; this will be updated when + -- a component adds new context and used when a node is replaced + originalContext = nil, + } + end + + local function mountFunctionVirtualNode(virtualNode) + local element = virtualNode.currentElement + + local children = element.component(element.props) + + updateVirtualNodeWithRenderResult(virtualNode, virtualNode.hostParent, children) + end + + local function mountPortalVirtualNode(virtualNode) + local element = virtualNode.currentElement + + local targetHostParent = element.props.target + local children = element.props[Children] + + assert(renderer.isHostObject(targetHostParent), "Expected target to be host object") + + updateVirtualNodeWithChildren(virtualNode, targetHostParent, children) + end + + local function mountFragmentVirtualNode(virtualNode) + local element = virtualNode.currentElement + local children = element.elements + + updateVirtualNodeWithChildren(virtualNode, virtualNode.hostParent, children) + end + + --[[ + Constructs a new virtual node and mounts it, but does not place it into + the tree. + ]] + function mountVirtualNode(element, hostParent, hostKey, context, legacyContext) + if config.internalTypeChecks then + internalAssert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") + internalAssert( + typeof(legacyContext) == "table" or legacyContext == nil, + "Expected arg #5 to be of type table or nil" + ) + end + if config.typeChecks then + assert(hostKey ~= nil, "Expected arg #3 to be non-nil") + assert( + Type.of(element) == Type.Element or typeof(element) == "boolean", + "Expected arg #1 to be of type Element or boolean" + ) + end + + -- Boolean values render as nil to enable terse conditional rendering. + if typeof(element) == "boolean" then + return nil + end + + local kind = ElementKind.of(element) + + local virtualNode = createVirtualNode(element, hostParent, hostKey, context, legacyContext) + + if kind == ElementKind.Host then + renderer.mountHostNode(reconciler, virtualNode) + elseif kind == ElementKind.Function then + mountFunctionVirtualNode(virtualNode) + elseif kind == ElementKind.Stateful then + element.component:__mount(reconciler, virtualNode) + elseif kind == ElementKind.Portal then + mountPortalVirtualNode(virtualNode) + elseif kind == ElementKind.Fragment then + mountFragmentVirtualNode(virtualNode) + else + error(("Unknown ElementKind %q"):format(tostring(kind), 2)) + end + + return virtualNode + end + + --[[ + Constructs a new Roact virtual tree, constructs a root node for + it, and mounts it. + ]] + local function mountVirtualTree(element, hostParent, hostKey) + if config.typeChecks then + assert(Type.of(element) == Type.Element, "Expected arg #1 to be of type Element") + assert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") + end + + if hostKey == nil then + hostKey = "RoactTree" + end + + local tree = { + [Type] = Type.VirtualTree, + [InternalData] = { + -- The root node of the tree, which starts into the hierarchy of + -- Roact component instances. + rootNode = nil, + mounted = true, + }, + } + + tree[InternalData].rootNode = mountVirtualNode(element, hostParent, hostKey) + + return tree + end + + --[[ + Unmounts the virtual tree, freeing all of its resources. + + No further operations should be done on the tree after it's been + unmounted, as indicated by its the `mounted` field. + ]] + local function unmountVirtualTree(tree) + local internalData = tree[InternalData] + if config.typeChecks then + assert(Type.of(tree) == Type.VirtualTree, "Expected arg #1 to be a Roact handle") + assert(internalData.mounted, "Cannot unmounted a Roact tree that has already been unmounted") + end + + internalData.mounted = false + + if internalData.rootNode ~= nil then + unmountVirtualNode(internalData.rootNode) + end + end + + --[[ + Utility method for updating the root node of a virtual tree given a new + element. + ]] + local function updateVirtualTree(tree, newElement) + local internalData = tree[InternalData] + if config.typeChecks then + assert(Type.of(tree) == Type.VirtualTree, "Expected arg #1 to be a Roact handle") + assert(Type.of(newElement) == Type.Element, "Expected arg #2 to be a Roact Element") + end + + internalData.rootNode = updateVirtualNode(internalData.rootNode, newElement) + + return tree + end + + reconciler = { + mountVirtualTree = mountVirtualTree, + unmountVirtualTree = unmountVirtualTree, + updateVirtualTree = updateVirtualTree, + + createVirtualNode = createVirtualNode, + mountVirtualNode = mountVirtualNode, + unmountVirtualNode = unmountVirtualNode, + updateVirtualNode = updateVirtualNode, + updateVirtualNodeWithChildren = updateVirtualNodeWithChildren, + updateVirtualNodeWithRenderResult = updateVirtualNodeWithRenderResult, + } + + return reconciler +end + +return createReconciler diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconciler.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconciler.spec.lua new file mode 100644 index 0000000..193dd25 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconciler.spec.lua @@ -0,0 +1,326 @@ +return function() + local assign = require(script.Parent.assign) + local createElement = require(script.Parent.createElement) + local createFragment = require(script.Parent.createFragment) + local createSpy = require(script.Parent.createSpy) + local NoopRenderer = require(script.Parent.NoopRenderer) + local Type = require(script.Parent.Type) + local ElementKind = require(script.Parent.ElementKind) + + local createReconciler = require(script.Parent.createReconciler) + + local noopReconciler = createReconciler(NoopRenderer) + + describe("tree operations", function() + it("should mount and unmount", function() + local tree = noopReconciler.mountVirtualTree(createElement("StringValue")) + + expect(tree).to.be.ok() + + noopReconciler.unmountVirtualTree(tree) + end) + + it("should mount, update, and unmount", function() + local tree = noopReconciler.mountVirtualTree(createElement("StringValue")) + + expect(tree).to.be.ok() + + noopReconciler.updateVirtualTree(tree, createElement("StringValue")) + + noopReconciler.unmountVirtualTree(tree) + end) + end) + + describe("booleans", function() + it("should mount booleans as nil", function() + local node = noopReconciler.mountVirtualNode(false, nil, "test") + expect(node).to.equal(nil) + end) + + it("should unmount nodes if they are updated to a boolean value", function() + local node = noopReconciler.mountVirtualNode(createElement("StringValue"), nil, "test") + + expect(node).to.be.ok() + + node = noopReconciler.updateVirtualNode(node, true) + + expect(node).to.equal(nil) + end) + end) + + describe("invalid elements", function() + it("should throw errors when attempting to mount invalid elements", function() + -- These function components return values with incorrect types + local returnsString = function() + return "Hello" + end + local returnsNumber = function() + return 1 + end + local returnsFunction = function() + return function() end + end + local returnsTable = function() + return {} + end + + local hostParent = nil + local key = "Some Key" + + expect(function() + noopReconciler.mountVirtualNode(createElement(returnsString), hostParent, key) + end).to.throw() + + expect(function() + noopReconciler.mountVirtualNode(createElement(returnsNumber), hostParent, key) + end).to.throw() + + expect(function() + noopReconciler.mountVirtualNode(createElement(returnsFunction), hostParent, key) + end).to.throw() + + expect(function() + noopReconciler.mountVirtualNode(createElement(returnsTable), hostParent, key) + end).to.throw() + end) + end) + + describe("Host components", function() + it("should invoke the renderer to mount host nodes", function() + local mountHostNode = createSpy(NoopRenderer.mountHostNode) + + local renderer = assign({}, NoopRenderer, { + mountHostNode = mountHostNode.value, + }) + + local reconciler = createReconciler(renderer) + + local element = createElement("StringValue") + local hostParent = nil + local key = "Some Key" + local node = reconciler.mountVirtualNode(element, hostParent, key) + + expect(Type.of(node)).to.equal(Type.VirtualNode) + + expect(mountHostNode.callCount).to.equal(1) + + local values = mountHostNode:captureValues("reconciler", "node") + + expect(values.reconciler).to.equal(reconciler) + expect(values.node).to.equal(node) + end) + + it("should invoke the renderer to update host nodes", function() + local updateHostNode = createSpy(NoopRenderer.updateHostNode) + + local renderer = assign({}, NoopRenderer, { + mountHostNode = NoopRenderer.mountHostNode, + updateHostNode = updateHostNode.value, + }) + + local reconciler = createReconciler(renderer) + + local element = createElement("StringValue") + local hostParent = nil + local key = "Key" + local node = reconciler.mountVirtualNode(element, hostParent, key) + + expect(Type.of(node)).to.equal(Type.VirtualNode) + + local newElement = createElement("StringValue") + local newNode = reconciler.updateVirtualNode(node, newElement) + + expect(newNode).to.equal(node) + + expect(updateHostNode.callCount).to.equal(1) + + local values = updateHostNode:captureValues("reconciler", "node", "newElement") + + expect(values.reconciler).to.equal(reconciler) + expect(values.node).to.equal(node) + expect(values.newElement).to.equal(newElement) + end) + + it("should invoke the renderer to unmount host nodes", function() + local unmountHostNode = createSpy(NoopRenderer.unmountHostNode) + + local renderer = assign({}, NoopRenderer, { + mountHostNode = NoopRenderer.mountHostNode, + unmountHostNode = unmountHostNode.value, + }) + + local reconciler = createReconciler(renderer) + + local element = createElement("StringValue") + local hostParent = nil + local key = "Key" + local node = reconciler.mountVirtualNode(element, hostParent, key) + + expect(Type.of(node)).to.equal(Type.VirtualNode) + + reconciler.unmountVirtualNode(node) + + expect(unmountHostNode.callCount).to.equal(1) + + local values = unmountHostNode:captureValues("reconciler", "node") + + expect(values.reconciler).to.equal(reconciler) + expect(values.node).to.equal(node) + end) + end) + + describe("Function components", function() + it("should mount and unmount function components", function() + local componentSpy = createSpy(function(props) + return nil + end) + + local element = createElement(componentSpy.value, { + someValue = 5, + }) + local hostParent = nil + local key = "A Key" + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(Type.of(node)).to.equal(Type.VirtualNode) + + expect(componentSpy.callCount).to.equal(1) + + local calledWith = componentSpy:captureValues("props") + + expect(calledWith.props).to.be.a("table") + expect(calledWith.props.someValue).to.equal(5) + + noopReconciler.unmountVirtualNode(node) + + expect(componentSpy.callCount).to.equal(1) + end) + + it("should mount single children of function components", function() + local childComponentSpy = createSpy(function(props) + return nil + end) + + local parentComponentSpy = createSpy(function(props) + return createElement(childComponentSpy.value, { + value = props.value + 1, + }) + end) + + local element = createElement(parentComponentSpy.value, { + value = 13, + }) + local hostParent = nil + local key = "A Key" + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(Type.of(node)).to.equal(Type.VirtualNode) + + expect(parentComponentSpy.callCount).to.equal(1) + expect(childComponentSpy.callCount).to.equal(1) + + local parentCalledWith = parentComponentSpy:captureValues("props") + local childCalledWith = childComponentSpy:captureValues("props") + + expect(parentCalledWith.props).to.be.a("table") + expect(parentCalledWith.props.value).to.equal(13) + + expect(childCalledWith.props).to.be.a("table") + expect(childCalledWith.props.value).to.equal(14) + + noopReconciler.unmountVirtualNode(node) + + expect(parentComponentSpy.callCount).to.equal(1) + expect(childComponentSpy.callCount).to.equal(1) + end) + + it("should mount fragments returned by function components", function() + local childAComponentSpy = createSpy(function(props) + return nil + end) + + local childBComponentSpy = createSpy(function(props) + return nil + end) + + local parentComponentSpy = createSpy(function(props) + return createFragment({ + A = createElement(childAComponentSpy.value, { + value = props.value + 1, + }), + B = createElement(childBComponentSpy.value, { + value = props.value + 5, + }), + }) + end) + + local element = createElement(parentComponentSpy.value, { + value = 17, + }) + local hostParent = nil + local key = "A Key" + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(Type.of(node)).to.equal(Type.VirtualNode) + + expect(parentComponentSpy.callCount).to.equal(1) + expect(childAComponentSpy.callCount).to.equal(1) + expect(childBComponentSpy.callCount).to.equal(1) + + local parentCalledWith = parentComponentSpy:captureValues("props") + local childACalledWith = childAComponentSpy:captureValues("props") + local childBCalledWith = childBComponentSpy:captureValues("props") + + expect(parentCalledWith.props).to.be.a("table") + expect(parentCalledWith.props.value).to.equal(17) + + expect(childACalledWith.props).to.be.a("table") + expect(childACalledWith.props.value).to.equal(18) + + expect(childBCalledWith.props).to.be.a("table") + expect(childBCalledWith.props.value).to.equal(22) + + noopReconciler.unmountVirtualNode(node) + + expect(parentComponentSpy.callCount).to.equal(1) + expect(childAComponentSpy.callCount).to.equal(1) + expect(childBComponentSpy.callCount).to.equal(1) + end) + end) + + describe("Fragments", function() + it("should mount fragments", function() + local fragment = createFragment({}) + local node = noopReconciler.mountVirtualNode(fragment, nil, "test") + + expect(node).to.be.ok() + expect(ElementKind.of(node.currentElement)).to.equal(ElementKind.Fragment) + end) + + it("should mount an empty fragment", function() + local emptyFragment = createFragment({}) + local node = noopReconciler.mountVirtualNode(emptyFragment, nil, "test") + + expect(node).to.be.ok() + expect(next(node.children)).to.never.be.ok() + end) + + it("should mount all fragment's children", function() + local childComponentSpy = createSpy(function(props) + return nil + end) + local elements = {} + local totalElements = 5 + + for i=1, totalElements do + elements["key"..tostring(i)] = createElement(childComponentSpy.value, {}) + end + + local fragments = createFragment(elements) + local node = noopReconciler.mountVirtualNode(fragments, nil, "test") + + expect(node).to.be.ok() + expect(childComponentSpy.callCount).to.equal(totalElements) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconcilerCompat.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconcilerCompat.lua new file mode 100644 index 0000000..e79cf5a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconcilerCompat.lua @@ -0,0 +1,47 @@ +--[[ + Contains deprecated methods from Reconciler. Broken out so that removing + this shim is easy -- just delete this file and remove it from init. +]] + +local Logging = require(script.Parent.Logging) + +local reifyMessage = [[ +Roact.reify has been renamed to Roact.mount and will be removed in a future release. +Check the call to Roact.reify at: +]] + +local teardownMessage = [[ +Roact.teardown has been renamed to Roact.unmount and will be removed in a future release. +Check the call to Roact.teardown at: +]] + +local reconcileMessage = [[ +Roact.reconcile has been renamed to Roact.update and will be removed in a future release. +Check the call to Roact.reconcile at: +]] + +local function createReconcilerCompat(reconciler) + local compat = {} + + function compat.reify(...) + Logging.warnOnce(reifyMessage) + + return reconciler.mountVirtualTree(...) + end + + function compat.teardown(...) + Logging.warnOnce(teardownMessage) + + return reconciler.unmountVirtualTree(...) + end + + function compat.reconcile(...) + Logging.warnOnce(reconcileMessage) + + return reconciler.updateVirtualTree(...) + end + + return compat +end + +return createReconcilerCompat \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconcilerCompat.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconcilerCompat.spec.lua new file mode 100644 index 0000000..ea4d078 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconcilerCompat.spec.lua @@ -0,0 +1,82 @@ +return function() + local createElement = require(script.Parent.createElement) + local createReconciler = require(script.Parent.createReconciler) + local Logging = require(script.Parent.Logging) + local NoopRenderer = require(script.Parent.NoopRenderer) + + local createReconcilerCompat = require(script.Parent.createReconcilerCompat) + + local noopReconciler = createReconciler(NoopRenderer) + local compatReconciler = createReconcilerCompat(noopReconciler) + + it("reify should only warn once per call site", function() + local logInfo = Logging.capture(function() + -- We're using a loop so that we get the same stack trace and only one + -- warning hopefully. + for _ = 1, 2 do + local handle = compatReconciler.reify(createElement("StringValue")) + noopReconciler.unmountVirtualTree(handle) + end + end) + + expect(#logInfo.warnings).to.equal(1) + expect(logInfo.warnings[1]:find("reify")).to.be.ok() + + logInfo = Logging.capture(function() + -- This is a different call site, which should trigger another warning. + local handle = compatReconciler.reify(createElement("StringValue")) + noopReconciler.unmountVirtualTree(handle) + end) + + expect(#logInfo.warnings).to.equal(1) + expect(logInfo.warnings[1]:find("reify")).to.be.ok() + end) + + it("teardown should only warn once per call site", function() + local logInfo = Logging.capture(function() + -- We're using a loop so that we get the same stack trace and only one + -- warning hopefully. + for _ = 1, 2 do + local handle = noopReconciler.mountVirtualTree(createElement("StringValue")) + compatReconciler.teardown(handle) + end + end) + + expect(#logInfo.warnings).to.equal(1) + expect(logInfo.warnings[1]:find("teardown")).to.be.ok() + + logInfo = Logging.capture(function() + -- This is a different call site, which should trigger another warning. + local handle = noopReconciler.mountVirtualTree(createElement("StringValue")) + compatReconciler.teardown(handle) + end) + + expect(#logInfo.warnings).to.equal(1) + expect(logInfo.warnings[1]:find("teardown")).to.be.ok() + end) + + it("update should only warn once per call site", function() + local logInfo = Logging.capture(function() + -- We're using a loop so that we get the same stack trace and only one + -- warning hopefully. + for _ = 1, 2 do + local handle = noopReconciler.mountVirtualTree(createElement("StringValue")) + compatReconciler.reconcile(handle, createElement("StringValue")) + noopReconciler.unmountVirtualTree(handle) + end + end) + + expect(#logInfo.warnings).to.equal(1) + expect(logInfo.warnings[1]:find("reconcile")).to.be.ok() + + logInfo = Logging.capture(function() + -- This is a different call site, which should trigger another warning. + local handle = noopReconciler.mountVirtualTree(createElement("StringValue")) + compatReconciler.reconcile(handle, createElement("StringValue")) + noopReconciler.unmountVirtualTree(handle) + end) + + expect(#logInfo.warnings).to.equal(1) + expect(logInfo.warnings[1]:find("reconcile")).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createRef.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createRef.lua new file mode 100644 index 0000000..c13e1b5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createRef.lua @@ -0,0 +1,38 @@ +--[[ + A ref is nothing more than a binding with a special field 'current' + that maps to the getValue method of the binding +]] +local Binding = require(script.Parent.Binding) + +local function createRef() + local binding, _ = Binding.create(nil) + + local ref = {} + + --[[ + A ref is just redirected to a binding via its metatable + ]] + setmetatable(ref, { + __index = function(self, key) + if key == "current" then + return binding:getValue() + else + return binding[key] + end + end, + __newindex = function(self, key, value) + if key == "current" then + error("Cannot assign to the 'current' property of refs", 2) + end + + binding[key] = value + end, + __tostring = function(self) + return ("RoactRef(%s)"):format(tostring(binding:getValue())) + end, + }) + + return ref +end + +return createRef \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createRef.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createRef.spec.lua new file mode 100644 index 0000000..553e79d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createRef.spec.lua @@ -0,0 +1,55 @@ +return function() + local Binding = require(script.Parent.Binding) + local Type = require(script.Parent.Type) + + local createRef = require(script.Parent.createRef) + + it("should create refs, which are specialized bindings", function() + local ref = createRef() + + expect(Type.of(ref)).to.equal(Type.Binding) + expect(ref.current).to.equal(nil) + end) + + it("should have a 'current' field that is the same as the internal binding's value", function() + local ref = createRef() + + expect(ref.current).to.equal(nil) + + Binding.update(ref, 10) + expect(ref.current).to.equal(10) + end) + + it("should support tostring on refs", function() + local ref = createRef() + + expect(ref.current).to.equal(nil) + expect(tostring(ref)).to.equal("RoactRef(nil)") + + Binding.update(ref, 10) + expect(tostring(ref)).to.equal("RoactRef(10)") + end) + + it("should not allow assignments to the 'current' field", function() + local ref = createRef() + + expect(ref.current).to.equal(nil) + + Binding.update(ref, 99) + expect(ref.current).to.equal(99) + + expect(function() + ref.current = 77 + end).to.throw() + + expect(ref.current).to.equal(99) + end) + + it("should return the same thing from getValue as its current field", function() + local ref = createRef() + Binding.update(ref, 10) + + expect(ref:getValue()).to.equal(10) + expect(ref:getValue()).to.equal(ref.current) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSignal.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSignal.lua new file mode 100644 index 0000000..3db6354 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSignal.lua @@ -0,0 +1,75 @@ +--[[ + This is a simple signal implementation that has a dead-simple API. + + local signal = createSignal() + + local disconnect = signal:subscribe(function(foo) + print("Cool foo:", foo) + end) + + signal:fire("something") + + disconnect() +]] + +local function addToMap(map, addKey, addValue) + local new = {} + + for key, value in pairs(map) do + new[key] = value + end + + new[addKey] = addValue + + return new +end + +local function removeFromMap(map, removeKey) + local new = {} + + for key, value in pairs(map) do + if key ~= removeKey then + new[key] = value + end + end + + return new +end + +local function createSignal() + local connections = {} + + local function subscribe(self, callback) + assert(typeof(callback) == "function", "Can only subscribe to signals with a function.") + + local connection = { + callback = callback, + } + + connections = addToMap(connections, callback, connection) + + local function disconnect() + assert(not connection.disconnected, "Listeners can only be disconnected once.") + + connection.disconnected = true + connections = removeFromMap(connections, callback) + end + + return disconnect + end + + local function fire(self, ...) + for callback, connection in pairs(connections) do + if not connection.disconnected then + callback(...) + end + end + end + + return { + subscribe = subscribe, + fire = fire, + } +end + +return createSignal \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSignal.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSignal.spec.lua new file mode 100644 index 0000000..ed9cf97 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSignal.spec.lua @@ -0,0 +1,89 @@ +return function() + local createSignal = require(script.Parent.createSignal) + + local createSpy = require(script.Parent.createSpy) + + it("should fire subscribers and disconnect them", function() + local signal = createSignal() + + local spy = createSpy() + local disconnect = signal:subscribe(spy.value) + + expect(spy.callCount).to.equal(0) + + local a = 1 + local b = {} + local c = "hello" + signal:fire(a, b, c) + + expect(spy.callCount).to.equal(1) + spy:assertCalledWith(a, b, c) + + disconnect() + + signal:fire() + + expect(spy.callCount).to.equal(1) + end) + + it("should handle multiple subscribers", function() + local signal = createSignal() + + local spyA = createSpy() + local spyB = createSpy() + + local disconnectA = signal:subscribe(spyA.value) + local disconnectB = signal:subscribe(spyB.value) + + expect(spyA.callCount).to.equal(0) + expect(spyB.callCount).to.equal(0) + + local a = {} + local b = 67 + signal:fire(a, b) + + expect(spyA.callCount).to.equal(1) + spyA:assertCalledWith(a, b) + + expect(spyB.callCount).to.equal(1) + spyB:assertCalledWith(a, b) + + disconnectA() + + signal:fire(b, a) + + expect(spyA.callCount).to.equal(1) + + expect(spyB.callCount).to.equal(2) + spyB:assertCalledWith(b, a) + + disconnectB() + end) + + it("should stop firing a connection if disconnected mid-fire", function() + local signal = createSignal() + + -- In this test, we'll connect two listeners that each try to disconnect + -- the other. Because the order of listeners firing isn't defined, we + -- have to be careful to handle either case. + + local disconnectA + local disconnectB + + local spyA = createSpy(function() + disconnectB() + end) + + local spyB = createSpy(function() + disconnectA() + end) + + disconnectA = signal:subscribe(spyA.value) + disconnectB = signal:subscribe(spyB.value) + + signal:fire() + + -- Exactly once listener should have been called. + expect(spyA.callCount + spyB.callCount).to.equal(1) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSpy.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSpy.lua new file mode 100644 index 0000000..baeba1c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSpy.lua @@ -0,0 +1,85 @@ +--[[ + A utility used to create a function spy that can be used to robustly test + that functions are invoked the correct number of times and with the correct + number of arguments. + + This should only be used in tests. +]] + +local assertDeepEqual = require(script.Parent.assertDeepEqual) + +local function createSpy(inner) + local self = { + callCount = 0, + values = {}, + valuesLength = 0, + } + + self.value = function(...) + self.callCount = self.callCount + 1 + self.values = {...} + self.valuesLength = select("#", ...) + + if inner ~= nil then + return inner(...) + end + end + + self.assertCalledWith = function(_, ...) + local len = select("#", ...) + + if self.valuesLength ~= len then + error(("Expected %d arguments, but was called with %d arguments"):format( + self.valuesLength, + len + ), 2) + end + + for i = 1, len do + local expected = select(i, ...) + + assert(self.values[i] == expected, "value differs") + end + end + + self.assertCalledWithDeepEqual = function(_, ...) + local len = select("#", ...) + + if self.valuesLength ~= len then + error(("Expected %d arguments, but was called with %d arguments"):format( + self.valuesLength, + len + ), 2) + end + + for i = 1, len do + local expected = select(i, ...) + + assertDeepEqual(self.values[i], expected) + end + end + + self.captureValues = function(_, ...) + local len = select("#", ...) + local result = {} + + assert(self.valuesLength == len, "length of expected values differs from stored values") + + for i = 1, len do + local key = select(i, ...) + result[key] = self.values[i] + end + + return result + end + + setmetatable(self, { + __index = function(_, key) + error(("%q is not a valid member of spy"):format(key)) + end, + }) + + return self +end + +return createSpy \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSpy.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSpy.spec.lua new file mode 100644 index 0000000..8693693 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSpy.spec.lua @@ -0,0 +1,90 @@ +return function() + local createSpy = require(script.Parent.createSpy) + + describe("createSpy", function() + it("should create spies", function() + local spy = createSpy(function() end) + + expect(spy).to.be.ok() + end) + + it("should throw if spies are indexed by an invalid key", function() + local spy = createSpy(function() end) + + expect(function() + return spy.test + end).to.throw() + end) + end) + + describe("value", function() + it("should increment callCount when called", function() + local spy = createSpy(function() end) + spy.value() + + expect(spy.callCount).to.equal(1) + end) + + it("should store all values passed", function() + local spy = createSpy(function() end) + spy.value(1, true, "3") + + expect(spy.valuesLength).to.equal(3) + expect(spy.values[1]).to.equal(1) + expect(spy.values[2]).to.equal(true) + expect(spy.values[3]).to.equal("3") + end) + + it("should return the value of the inner function", function() + local spy = createSpy(function() + return true + end) + + expect(spy.value()).to.equal(true) + end) + end) + + describe("assertCalledWith", function() + it("should throw if the number of values differs", function() + local spy = createSpy(function() end) + spy.value(1, 2) + + expect(function() + spy:assertCalledWith(1) + end).to.throw() + end) + + it("should throw if any value differs", function() + local spy = createSpy(function() end) + spy.value(1, 2) + + expect(function() + spy:assertCalledWith(1, 3) + end).to.throw() + + expect(function() + spy:assertCalledWith(2, 3) + end).to.throw() + end) + end) + + describe("captureValues", function() + it("should throw if the number of values differs", function() + local spy = createSpy(function() end) + spy.value(1, 2) + + expect(function() + spy:captureValues("a") + end).to.throw() + end) + + it("should capture all values in a table", function() + local spy = createSpy(function() end) + spy.value(1, 2) + + local captured = spy:captureValues("a", "b") + expect(captured.a).to.equal(1) + expect(captured.b).to.equal(2) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/getDefaultInstanceProperty.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/getDefaultInstanceProperty.lua new file mode 100644 index 0000000..9a6a095 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/getDefaultInstanceProperty.lua @@ -0,0 +1,54 @@ +--[[ + Attempts to get the default value of a given property on a Roblox instance. + + This is used by the reconciler in cases where a prop was previously set on a + primitive component, but is no longer present in a component's new props. + + Eventually, Roblox might provide a nicer API to query the default property + of an object without constructing an instance of it. +]] + +local Symbol = require(script.Parent.Symbol) + +local Nil = Symbol.named("Nil") +local _cachedPropertyValues = {} + +local function getDefaultInstanceProperty(className, propertyName) + local classCache = _cachedPropertyValues[className] + + if classCache then + local propValue = classCache[propertyName] + + -- We have to use a marker here, because Lua doesn't distinguish + -- between 'nil' and 'not in a table' + if propValue == Nil then + return true, nil + end + + if propValue ~= nil then + return true, propValue + end + else + classCache = {} + _cachedPropertyValues[className] = classCache + end + + local created = Instance.new(className) + local ok, defaultValue = pcall(function() + return created[propertyName] + end) + + created:Destroy() + + if ok then + if defaultValue == nil then + classCache[propertyName] = Nil + else + classCache[propertyName] = defaultValue + end + end + + return ok, defaultValue +end + +return getDefaultInstanceProperty \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/getDefaultInstanceProperty.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/getDefaultInstanceProperty.spec.lua new file mode 100644 index 0000000..a126820 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/getDefaultInstanceProperty.spec.lua @@ -0,0 +1,33 @@ +return function() + local getDefaultInstanceProperty = require(script.Parent.getDefaultInstanceProperty) + + it("should get default name string values", function() + local _, defaultName = getDefaultInstanceProperty("StringValue", "Name") + + expect(defaultName).to.equal("Value") + end) + + it("should get default empty string values", function() + local _, defaultValue = getDefaultInstanceProperty("StringValue", "Value") + + expect(defaultValue).to.equal("") + end) + + it("should get default number values", function() + local _, defaultValue = getDefaultInstanceProperty("IntValue", "Value") + + expect(defaultValue).to.equal(0) + end) + + it("should get nil default values", function() + local _, defaultValue = getDefaultInstanceProperty("ObjectValue", "Value") + + expect(defaultValue).to.equal(nil) + end) + + it("should get bool default values", function() + local _, defaultValue = getDefaultInstanceProperty("BoolValue", "Value") + + expect(defaultValue).to.equal(false) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/init.lua new file mode 100644 index 0000000..0bac301 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/init.lua @@ -0,0 +1,48 @@ +--[[ + Packages up the internals of Roact and exposes a public API for it. +]] + +local GlobalConfig = require(script.GlobalConfig) +local createReconciler = require(script.createReconciler) +local createReconcilerCompat = require(script.createReconcilerCompat) +local RobloxRenderer = require(script.RobloxRenderer) +local strict = require(script.strict) +local Binding = require(script.Binding) + +local robloxReconciler = createReconciler(RobloxRenderer) +local reconcilerCompat = createReconcilerCompat(robloxReconciler) + +local Roact = strict { + Component = require(script.Component), + createElement = require(script.createElement), + createFragment = require(script.createFragment), + oneChild = require(script.oneChild), + PureComponent = require(script.PureComponent), + None = require(script.None), + Portal = require(script.Portal), + createRef = require(script.createRef), + createBinding = Binding.create, + joinBindings = Binding.join, + createContext = require(script.createContext), + + Change = require(script.PropMarkers.Change), + Children = require(script.PropMarkers.Children), + Event = require(script.PropMarkers.Event), + Ref = require(script.PropMarkers.Ref), + + mount = robloxReconciler.mountVirtualTree, + unmount = robloxReconciler.unmountVirtualTree, + update = robloxReconciler.updateVirtualTree, + + reify = reconcilerCompat.reify, + teardown = reconcilerCompat.teardown, + reconcile = reconcilerCompat.reconcile, + + setGlobalConfig = GlobalConfig.set, + + -- APIs that may change in the future without warning + UNSTABLE = { + }, +} + +return Roact \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/init.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/init.spec.lua new file mode 100644 index 0000000..652ee19 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/init.spec.lua @@ -0,0 +1,64 @@ +return function() + local Roact = require(script.Parent) + + it("should load with all public APIs", function() + local publicApi = { + createElement = "function", + createFragment = "function", + createRef = "function", + createBinding = "function", + joinBindings = "function", + mount = "function", + unmount = "function", + update = "function", + oneChild = "function", + setGlobalConfig = "function", + createContext = "function", + + -- These functions are deprecated and throw warnings! + reify = "function", + teardown = "function", + reconcile = "function", + + Component = true, + PureComponent = true, + Portal = true, + Children = true, + Event = true, + Change = true, + Ref = true, + None = true, + UNSTABLE = true, + } + + expect(Roact).to.be.ok() + + for key, valueType in pairs(publicApi) do + local success + if typeof(valueType) == "string" then + success = typeof(Roact[key]) == valueType + else + success = Roact[key] ~= nil + end + + if not success then + local existence = typeof(valueType) == "boolean" and "present" or "of type " .. valueType + local message = ( + "Expected public API member %q to be %s, but instead it was of type %s" + ):format(tostring(key), existence, typeof(Roact[key])) + + error(message) + end + end + + for key in pairs(Roact) do + if publicApi[key] == nil then + local message = ( + "Found unknown public API key %q!" + ):format(tostring(key)) + + error(message) + end + end + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/internalAssert.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/internalAssert.lua new file mode 100644 index 0000000..87c5dfc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/internalAssert.lua @@ -0,0 +1,7 @@ +local function internalAssert(condition, message) + if not condition then + error(message .. " (This is probably a bug in Roact!)", 3) + end +end + +return internalAssert \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/invalidSetStateMessages.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/invalidSetStateMessages.lua new file mode 100644 index 0000000..34571ce --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/invalidSetStateMessages.lua @@ -0,0 +1,44 @@ +--[[ + These messages are used by Component to help users diagnose when they're + calling setState in inappropriate places. + + The indentation may seem odd, but it's necessary to avoid introducing extra + whitespace into the error messages themselves. +]] +local ComponentLifecyclePhase = require(script.Parent.ComponentLifecyclePhase) + +local invalidSetStateMessages = {} + +invalidSetStateMessages[ComponentLifecyclePhase.WillUpdate] = [[ +setState cannot be used in the willUpdate lifecycle method. +Consider using the didUpdate method instead, or using getDerivedStateFromProps. + +Check the definition of willUpdate in the component %q.]] + +invalidSetStateMessages[ComponentLifecyclePhase.WillUnmount] = [[ +setState cannot be used in the willUnmount lifecycle method. +A component that is being unmounted cannot be updated! + +Check the definition of willUnmount in the component %q.]] + +invalidSetStateMessages[ComponentLifecyclePhase.ShouldUpdate] = [[ +setState cannot be used in the shouldUpdate lifecycle method. +shouldUpdate must be a pure function that only depends on props and state. + +Check the definition of shouldUpdate in the component %q.]] + +invalidSetStateMessages[ComponentLifecyclePhase.Render] = [[ +setState cannot be used in the render method. +render must be a pure function that only depends on props and state. + +Check the definition of render in the component %q.]] + +invalidSetStateMessages["default"] = [[ +setState can not be used in the current situation, because Roact doesn't know +which part of the lifecycle this component is in. + +This is a bug in Roact. +It was triggered by the component %q. +]] + +return invalidSetStateMessages \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/oneChild.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/oneChild.lua new file mode 100644 index 0000000..285d519 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/oneChild.lua @@ -0,0 +1,28 @@ +--[[ + Retrieves at most one child from the children passed to a component. + + If passed nil or an empty table, will return nil. + + Throws an error if passed more than one child. +]] +local function oneChild(children) + if not children then + return nil + end + + local key, child = next(children) + + if not child then + return nil + end + + local after = next(children, key) + + if after then + error("Expected at most child, had more than one child.", 2) + end + + return child +end + +return oneChild \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/oneChild.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/oneChild.spec.lua new file mode 100644 index 0000000..6540ce2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/oneChild.spec.lua @@ -0,0 +1,35 @@ +return function() + local createElement = require(script.Parent.createElement) + + local oneChild = require(script.Parent.oneChild) + + it("should get zero children from a table", function() + local children = {} + + expect(oneChild(children)).to.equal(nil) + end) + + it("should get exactly one child", function() + local child = createElement("Frame") + local children = { + foo = child, + } + + expect(oneChild(children)).to.equal(child) + end) + + it("should error with more than one child", function() + local children = { + a = createElement("Frame"), + b = createElement("Frame"), + } + + expect(function() + oneChild(children) + end).to.throw() + end) + + it("should handle being passed nil", function() + expect(oneChild(nil)).to.equal(nil) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/strict.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/strict.lua new file mode 100644 index 0000000..c1d21a5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/strict.lua @@ -0,0 +1,27 @@ +local function strict(t, name) + name = name or tostring(t) + + return setmetatable(t, { + __index = function(self, key) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + + __newindex = function(self, key, value) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + }) +end + +return strict \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/strict.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/strict.spec.lua new file mode 100644 index 0000000..fc44bff --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/strict.spec.lua @@ -0,0 +1,25 @@ +return function() + local strict = require(script.Parent.strict) + + it("should error when getting a nonexistent key", function() + local t = strict({ + a = 1, + b = 2, + }) + + expect(function() + return t.c + end).to.throw() + end) + + it("should error when setting a nonexistent key", function() + local t = strict({ + a = 1, + b = 2, + }) + + expect(function() + t.c = 3 + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/lock.toml new file mode 100644 index 0000000..ac51832 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/rodux" +version = "1.0.0" +commit = "416e8042bd6eac7a385cb0955079501fd44f11e1" +source = "url+https://github.com/roblox/rodux" diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/NoYield.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/NoYield.lua new file mode 100644 index 0000000..f9519f1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/NoYield.lua @@ -0,0 +1,29 @@ +--[[ + Calls a function and throws an error if it attempts to yield. + + Pass any number of arguments to the function after the callback. + + This function supports multiple return; all results returned from the + given function will be returned. +]] + +local function resultHandler(co, ok, ...) + if not ok then + local message = (...) + error(debug.traceback(co, message), 2) + end + + if coroutine.status(co) ~= "dead" then + error(debug.traceback(co, "Attempted to yield inside changed event!"), 2) + end + + return ... +end + +local function NoYield(callback, ...) + local co = coroutine.create(callback) + + return resultHandler(co, coroutine.resume(co, ...)) +end + +return NoYield \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/NoYield.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/NoYield.spec.lua new file mode 100644 index 0000000..f1e7cf0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/NoYield.spec.lua @@ -0,0 +1,56 @@ +return function() + local NoYield = require(script.Parent.NoYield) + + it("should call functions normally", function() + local callCount = 0 + + local function test(a, b) + expect(a).to.equal(5) + expect(b).to.equal(6) + + callCount = callCount + 1 + + return 11, "hello" + end + + local a, b = NoYield(test, 5, 6) + + expect(a).to.equal(11) + expect(b).to.equal("hello") + end) + + it("should throw on yield", function() + local preCount = 0 + local postCount = 0 + + local function testMethod() + preCount = preCount + 1 + wait() + postCount = postCount + 1 + end + + local ok, err = pcall(NoYield, testMethod) + + expect(preCount).to.equal(1) + expect(postCount).to.equal(0) + + expect(ok).to.equal(false) + expect(err:find("Attempted to yield inside changed event!")).to.be.ok() + expect(err:find("NoYield.spec")).to.be.ok() + end) + + it("should propagate error messages", function() + local count = 0 + + local function test() + count = count + 1 + error("foo") + end + + local ok, err = pcall(NoYield, test) + + expect(ok).to.equal(false) + expect(err:find("foo")).to.be.ok() + expect(err:find("NoYield.spec")).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Signal.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Signal.lua new file mode 100644 index 0000000..dc4d041 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Signal.lua @@ -0,0 +1,75 @@ +--[[ + A limited, simple implementation of a Signal. + + Handlers are fired in order, and (dis)connections are properly handled when + executing an event. +]] + +local function immutableAppend(list, ...) + local new = {} + local len = #list + + for key = 1, len do + new[key] = list[key] + end + + for i = 1, select("#", ...) do + new[len + i] = select(i, ...) + end + + return new +end + +local function immutableRemoveValue(list, removeValue) + local new = {} + + for i = 1, #list do + if list[i] ~= removeValue then + table.insert(new, list[i]) + end + end + + return new +end + +local Signal = {} + +Signal.__index = Signal + +function Signal.new() + local self = { + _listeners = {} + } + + setmetatable(self, Signal) + + return self +end + +function Signal:connect(callback) + local listener = { + callback = callback, + disconnected = false, + } + + self._listeners = immutableAppend(self._listeners, listener) + + local function disconnect() + listener.disconnected = true + self._listeners = immutableRemoveValue(self._listeners, listener) + end + + return { + disconnect = disconnect + } +end + +function Signal:fire(...) + for _, listener in ipairs(self._listeners) do + if not listener.disconnected then + listener.callback(...) + end + end +end + +return Signal \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Signal.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Signal.spec.lua new file mode 100644 index 0000000..f00f947 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Signal.spec.lua @@ -0,0 +1,114 @@ +return function() + local Signal = require(script.Parent.Signal) + + it("should construct from nothing", function() + local signal = Signal.new() + + expect(signal).to.be.ok() + end) + + it("should fire connected callbacks", function() + local callCount = 0 + local value1 = "Hello World" + local value2 = 7 + + local callback = function(arg1, arg2) + expect(arg1).to.equal(value1) + expect(arg2).to.equal(value2) + callCount = callCount + 1 + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + + connection:disconnect() + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + end) + + it("should disconnect handlers", function() + local callback = function() + error("Callback was called after disconnect!") + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + connection:disconnect() + + signal:fire() + end) + + it("should fire handlers in order", function() + local signal = Signal.new() + local x = 0 + local y = 0 + + local callback1 = function() + expect(x).to.equal(0) + expect(y).to.equal(0) + x = x + 1 + end + + local callback2 = function() + expect(x).to.equal(1) + expect(y).to.equal(0) + y = y + 1 + end + + signal:connect(callback1) + signal:connect(callback2) + signal:fire() + + expect(x).to.equal(1) + expect(y).to.equal(1) + end) + + it("should continue firing despite mid-event disconnection", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionA + connectionA = signal:connect(function() + connectionA:disconnect() + countA = countA + 1 + end) + + signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(1) + end) + + it("should skip listeners that were disconnected during event evaluation", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionB + + signal:connect(function() + countA = countA + 1 + connectionB:disconnect() + end) + + connectionB = signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(0) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Store.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Store.lua new file mode 100644 index 0000000..90aa02f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Store.lua @@ -0,0 +1,131 @@ +local RunService = game:GetService("RunService") + +local Signal = require(script.Parent.Signal) +local NoYield = require(script.Parent.NoYield) + +local Store = {} + +-- This value is exposed as a private value so that the test code can stay in +-- sync with what event we listen to for dispatching the Changed event. +-- It may not be Heartbeat in the future. +Store._flushEvent = RunService.Heartbeat + +Store.__index = Store + +--[[ + Create a new Store whose state is transformed by the given reducer function. + + Each time an action is dispatched to the store, the new state of the store + is given by: + + state = reducer(state, action) + + Reducers do not mutate the state object, so the original state is still + valid. +]] +function Store.new(reducer, initialState, middlewares) + assert(typeof(reducer) == "function", "Bad argument #1 to Store.new, expected function.") + assert(middlewares == nil or typeof(middlewares) == "table", "Bad argument #3 to Store.new, expected nil or table.") + + local self = {} + + self._reducer = reducer + self._state = reducer(initialState, { + type = "@@INIT", + }) + self._lastState = self._state + + self._mutatedSinceFlush = false + self._connections = {} + + self.changed = Signal.new() + + setmetatable(self, Store) + + local connection = self._flushEvent:Connect(function() + self:flush() + end) + table.insert(self._connections, connection) + + if middlewares then + local unboundDispatch = self.dispatch + local dispatch = function(...) + return unboundDispatch(self, ...) + end + + for i = #middlewares, 1, -1 do + local middleware = middlewares[i] + dispatch = middleware(dispatch, self) + end + + self.dispatch = function(self, ...) + return dispatch(...) + end + end + + return self +end + +--[[ + Get the current state of the Store. Do not mutate this! +]] +function Store:getState() + return self._state +end + +--[[ + Dispatch an action to the store. This allows the store's reducer to mutate + the state of the application by creating a new copy of the state. + + Listeners on the changed event of the store are notified when the state + changes, but not necessarily on every Dispatch. +]] +function Store:dispatch(action) + if typeof(action) == "table" then + if action.type == nil then + error("action does not have a type field", 2) + end + + self._state = self._reducer(self._state, action) + self._mutatedSinceFlush = true + else + error(("actions of type %q are not permitted"):format(typeof(action)), 2) + end +end + +--[[ + Marks the store as deleted, disconnecting any outstanding connections. +]] +function Store:destruct() + for _, connection in ipairs(self._connections) do + connection:Disconnect() + end + + self._connections = nil +end + +--[[ + Flush all pending actions since the last change event was dispatched. +]] +function Store:flush() + if not self._mutatedSinceFlush then + return + end + + self._mutatedSinceFlush = false + + -- On self.changed:fire(), further actions may be immediately dispatched, in + -- which case self._lastState will be set to the most recent self._state, + -- unless we cache this value first + local state = self._state + + -- If a changed listener yields, *very* surprising bugs can ensue. + -- Because of that, changed listeners cannot yield. + NoYield(function() + self.changed:fire(state, self._lastState) + end) + + self._lastState = state +end + +return Store diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Store.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Store.spec.lua new file mode 100644 index 0000000..5e7a5fd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Store.spec.lua @@ -0,0 +1,342 @@ +return function() + local Store = require(script.Parent.Store) + + describe("new", function() + it("should instantiate with a reducer", function() + local store = Store.new(function(state, action) + return "hello, world" + end) + + expect(store).to.be.ok() + expect(store:getState()).to.equal("hello, world") + + store:destruct() + end) + + it("should instantiate with a reducer and an initial state", function() + local store = Store.new(function(state, action) + return state + end, "initial state") + + expect(store).to.be.ok() + expect(store:getState()).to.equal("initial state") + + store:destruct() + end) + + it("should instantiate with a reducer, initial state, and middlewares", function() + local store = Store.new(function(state, action) + return state + end, "initial state", {}) + + expect(store).to.be.ok() + expect(store:getState()).to.equal("initial state") + + store:destruct() + end) + + it("should modify the dispatch method when middlewares are passed", function() + local middlewareInstantiateCount = 0 + local middlewareInvokeCount = 0 + local passedDispatch + local passedStore + local passedAction + + local function reducer(state, action) + if action.type == "test" then + return "test state" + end + + return state + end + + local function testMiddleware(nextDispatch, store) + middlewareInstantiateCount = middlewareInstantiateCount + 1 + passedDispatch = nextDispatch + passedStore = store + + return function(action) + middlewareInvokeCount = middlewareInvokeCount + 1 + passedAction = action + + nextDispatch(action) + end + end + + local store = Store.new(reducer, "initial state", { testMiddleware }) + + expect(middlewareInstantiateCount).to.equal(1) + expect(middlewareInvokeCount).to.equal(0) + expect(passedDispatch).to.be.a("function") + expect(passedStore).to.equal(store) + + store:dispatch({ + type = "test", + }) + + expect(middlewareInstantiateCount).to.equal(1) + expect(middlewareInvokeCount).to.equal(1) + expect(passedAction.type).to.equal("test") + + store:flush() + + expect(store:getState()).to.equal("test state") + + store:destruct() + end) + + it("should execute middleware left-to-right", function() + local events = {} + + local function reducer(state) + return state + end + + local function middlewareA(nextDispatch, store) + table.insert(events, "instantiate a") + return function(action) + table.insert(events, "execute a") + return nextDispatch(action) + end + end + + local function middlewareB(nextDispatch, store) + table.insert(events, "instantiate b") + return function(action) + table.insert(events, "execute b") + return nextDispatch(action) + end + end + + local store = Store.new(reducer, 5, { middlewareA, middlewareB }) + + expect(#events).to.equal(2) + expect(events[1]).to.equal("instantiate b") + expect(events[2]).to.equal("instantiate a") + + store:dispatch({ + type = "test", + }) + + expect(#events).to.equal(4) + expect(events[3]).to.equal("execute a") + expect(events[4]).to.equal("execute b") + end) + + it("should send an initial action with a 'type' field", function() + local lastAction + local callCount = 0 + + local store = Store.new(function(state, action) + lastAction = action + callCount = callCount + 1 + + return state + end) + + expect(callCount).to.equal(1) + expect(lastAction).to.be.a("table") + expect(lastAction.type).to.be.ok() + + store:destruct() + end) + end) + + describe("getState", function() + it("should get the current state", function() + local store = Store.new(function(state, action) + return "foo" + end) + + local state = store:getState() + + expect(state).to.equal("foo") + + store:destruct() + end) + end) + + describe("dispatch", function() + it("should be sent through the reducer", function() + local store = Store.new(function(state, action) + state = state or "foo" + + if action.type == "act" then + return "bar" + end + + return state + end) + + expect(store).to.be.ok() + expect(store:getState()).to.equal("foo") + + store:dispatch({ + type = "act", + }) + + store:flush() + + expect(store:getState()).to.equal("bar") + + store:destruct() + end) + + it("should trigger the changed event after a flush", function() + local store = Store.new(function(state, action) + state = state or 0 + + if action.type == "increment" then + return state + 1 + end + + return state + end) + + local callCount = 0 + + store.changed:connect(function(state, oldState) + expect(oldState).to.equal(0) + expect(state).to.equal(1) + + callCount = callCount + 1 + end) + + store:dispatch({ + type = "increment", + }) + + store:flush() + + expect(callCount).to.equal(1) + + store:destruct() + end) + + it("should handle actions dispatched within the changed event", function() + local store = Store.new(function(state, action) + state = state or { + value = 0, + } + + if action.type == "increment" then + return { + value = state.value + 1, + } + elseif action.type == "decrement" then + return { + value = state.value - 1, + } + end + + return state + end) + + local changeCount = 0 + + store.changed:connect(function(state, oldState) + expect(state).never.to.equal(oldState) + + if state.value > 0 then + store:dispatch({ + type = "decrement", + }) + end + + changeCount = changeCount + 1 + end) + + store:dispatch({ + type = "increment", + }) + store:flush() + store:flush() + + expect(changeCount).to.equal(2) + + store:destruct() + end) + + it("should prevent yielding from changed handler", function() + local preCount = 0 + local postCount = 0 + + local store = Store.new(function(state, action) + state = state or 0 + return state + 1 + end) + + store.changed:connect(function(state, oldState) + preCount = preCount + 1 + wait() + postCount = postCount + 1 + end) + + store:dispatch({ + type = "increment", + }) + + expect(function() + store:flush() + end).to.throw() + + expect(preCount).to.equal(1) + expect(postCount).to.equal(0) + + store:destruct() + end) + + it("should throw if an action is dispatched without a type field", function() + local store = Store.new(function(state, action) + return state + end) + + expect(function() + store:dispatch({}) + end).to.throw() + + store:destruct() + end) + + it("should throw if the action is not a function or table", function() + local store = Store.new(function(state, action) + return state + end) + + expect(function() + store:dispatch(1) + end).to.throw() + + store:destruct() + end) + end) + + describe("flush", function() + it("should not fire a changed event if there were no dispatches", function() + local store = Store.new(function() + end) + + local count = 0 + store.changed:connect(function() + count = count + 1 + end) + + store:flush() + + expect(count).to.equal(0) + + store:dispatch({ + type = "increment", + }) + store:flush() + + expect(count).to.equal(1) + + store:flush() + + expect(count).to.equal(1) + + store:destruct() + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/combineReducers.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/combineReducers.lua new file mode 100644 index 0000000..f3023c4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/combineReducers.lua @@ -0,0 +1,22 @@ +--[[ + Create a composite reducer from a map of keys and sub-reducers. +]] +local function combineReducers(map) + return function(state, action) + -- If state is nil, substitute it with a blank table. + if state == nil then + state = {} + end + + local newState = {} + + for key, reducer in pairs(map) do + -- Each reducer gets its own state, not the entire state table + newState[key] = reducer(state[key], action) + end + + return newState + end +end + +return combineReducers diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/combineReducers.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/combineReducers.spec.lua new file mode 100644 index 0000000..a3a85af --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/combineReducers.spec.lua @@ -0,0 +1,52 @@ +return function() + local combineReducers = require(script.Parent.combineReducers) + + it("should invoke each sub-reducer for every action", function() + local aCount = 0 + local bCount = 0 + + local reducer = combineReducers({ + a = function(state, action) + aCount = aCount + 1 + end, + b = function(state, action) + bCount = bCount + 1 + end, + }) + + -- Mock reducer invocation + reducer({}, {}) + expect(aCount).to.equal(1) + expect(bCount).to.equal(1) + end) + + it("should assign each sub-reducer's value to the new state", function() + local reducer = combineReducers({ + a = function(state, action) + return (state or 0) + 1 + end, + b = function(state, action) + return (state or 0) + 3 + end, + }) + + local newState = reducer({}, {}) + expect(newState.a).to.equal(1) + expect(newState.b).to.equal(3) + end) + + it("should not throw when state is nil", function() + local reducer = combineReducers({ + a = function(state, action) + return (state or 0) + 1 + end, + b = function(state, action) + return (state or 0) + 3 + end, + }) + + expect(function() + reducer(nil, {}) + end).to.never.throw() + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/createReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/createReducer.lua new file mode 100644 index 0000000..5560b59 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/createReducer.lua @@ -0,0 +1,15 @@ +return function(initialState, handlers) + return function(state, action) + if state == nil then + state = initialState + end + + local handler = handlers[action.type] + + if handler then + return handler(state, action) + end + + return state + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/createReducer.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/createReducer.spec.lua new file mode 100644 index 0000000..a704944 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/createReducer.spec.lua @@ -0,0 +1,106 @@ +return function() + local createReducer = require(script.Parent.createReducer) + + it("should handle actions", function() + local reducer = createReducer({ + a = 0, + b = 0, + }, { + a = function(state, action) + return { + a = state.a + 1, + b = state.b, + } + end, + b = function(state, action) + return { + a = state.a, + b = state.b + 2, + } + end, + }) + + local newState = reducer({ + a = 0, + b = 0, + }, { + type = "a", + }) + + expect(newState.a).to.equal(1) + + newState = reducer(newState, { + type = "b", + }) + + expect(newState.b).to.equal(2) + end) + + it("should return the initial state if the state is nil", function() + local reducer = createReducer({ + a = 0, + b = 0, + -- We don't care about the actions here + }, {}) + + local newState = reducer(nil, {}) + expect(newState).to.be.ok() + expect(newState.a).to.equal(0) + expect(newState.b).to.equal(0) + end) + + it("should still run action handlers if the state is nil", function() + local callCount = 0 + + local reducer = createReducer(0, { + foo = function(state, action) + callCount = callCount + 1 + return nil + end + }) + + expect(callCount).to.equal(0) + + local newState = reducer(nil, { + type = "foo", + }) + + expect(callCount).to.equal(1) + expect(newState).to.equal(nil) + + newState = reducer(newState, { + type = "foo", + }) + + expect(callCount).to.equal(2) + expect(newState).to.equal(nil) + end) + + it("should return the same state if the action is not handled", function() + local initialState = { + a = 0, + b = 0, + } + + local reducer = createReducer(initialState, { + a = function(state, action) + return { + a = state.a + 1, + b = state.b, + } + end, + b = function(state, action) + return { + a = state.a, + b = state.b + 2, + } + end, + }) + + local newState = reducer(initialState, { + type = "c", + }) + + expect(newState).to.equal(initialState) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/init.lua new file mode 100644 index 0000000..acef1df --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/init.lua @@ -0,0 +1,13 @@ +local Store = require(script.Store) +local createReducer = require(script.createReducer) +local combineReducers = require(script.combineReducers) +local loggerMiddleware = require(script.loggerMiddleware) +local thunkMiddleware = require(script.thunkMiddleware) + +return { + Store = Store, + createReducer = createReducer, + combineReducers = combineReducers, + loggerMiddleware = loggerMiddleware.middleware, + thunkMiddleware = thunkMiddleware, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/init.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/init.spec.lua new file mode 100644 index 0000000..14a24ef --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/init.spec.lua @@ -0,0 +1,9 @@ +return function() + describe("Rodux", function() + it("should load", function() + local Rodux = require(script.Parent) + + expect(Rodux.Store).to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/loggerMiddleware.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/loggerMiddleware.lua new file mode 100644 index 0000000..9bc922b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/loggerMiddleware.lua @@ -0,0 +1,55 @@ +local indent = " " + +local function prettyPrint(value, indentLevel) + indentLevel = indentLevel or 0 + local output = {} + + if typeof(value) == "table" then + table.insert(output, "{\n") + + for key, value in pairs(value) do + table.insert(output, indent:rep(indentLevel + 1)) + table.insert(output, tostring(key)) + table.insert(output, " = ") + + table.insert(output, prettyPrint(value, indentLevel + 1)) + table.insert(output, "\n") + end + + table.insert(output, indent:rep(indentLevel)) + table.insert(output, "}") + elseif typeof(value) == "string" then + table.insert(output, string.format("%q", value)) + table.insert(output, " (string)") + else + table.insert(output, tostring(value)) + table.insert(output, " (") + table.insert(output, typeof(value)) + table.insert(output, ")") + end + + return table.concat(output, "") +end + +-- We want to be able to override outputFunction in tests, so the shape of this +-- module is kind of unconventional. +-- +-- We fix it this weird shape in init.lua. +local loggerMiddleware = { + outputFunction = print, +} + +function loggerMiddleware.middleware(nextDispatch, store) + return function(action) + local result = nextDispatch(action) + + loggerMiddleware.outputFunction(("Action dispatched: %s\nState changed to: %s"):format( + prettyPrint(action), + prettyPrint(store:getState()) + )) + + return result + end +end + +return loggerMiddleware diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/loggerMiddleware.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/loggerMiddleware.spec.lua new file mode 100644 index 0000000..2ec3ea3 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/loggerMiddleware.spec.lua @@ -0,0 +1,39 @@ +return function() + local Store = require(script.Parent.Store) + local loggerMiddleware = require(script.Parent.loggerMiddleware) + + it("should print whenever an action is dispatched", function() + local outputCount = 0 + local outputMessage + + local function reducer(state, action) + return state + end + + local store = Store.new(reducer, { + fooValue = 12345, + barValue = { + bazValue = "hiBaz", + }, + }, { loggerMiddleware.middleware }) + + loggerMiddleware.outputFunction = function(message) + outputCount = outputCount + 1 + outputMessage = message + end + + store:dispatch({ + type = "testActionType", + }) + + expect(outputCount).to.equal(1) + expect(outputMessage:find("testActionType")).to.be.ok() + expect(outputMessage:find("fooValue")).to.be.ok() + expect(outputMessage:find("12345")).to.be.ok() + expect(outputMessage:find("barValue")).to.be.ok() + expect(outputMessage:find("bazValue")).to.be.ok() + expect(outputMessage:find("hiBaz")).to.be.ok() + + loggerMiddleware.outputFunction = print + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/thunkMiddleware.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/thunkMiddleware.lua new file mode 100644 index 0000000..08c676b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/thunkMiddleware.lua @@ -0,0 +1,17 @@ +--[[ + A middleware that allows for functions to be dispatched. + Functions will receive a single argument, the store itself. + This middleware consumes the function; middleware further down the chain + will not receive it. +]] +local function thunkMiddleware(nextDispatch, store) + return function(action) + if typeof(action) == "function" then + return action(store) + else + return nextDispatch(action) + end + end +end + +return thunkMiddleware diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/thunkMiddleware.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/thunkMiddleware.spec.lua new file mode 100644 index 0000000..8f717e4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/thunkMiddleware.spec.lua @@ -0,0 +1,58 @@ +return function() + local Store = require(script.Parent.Store) + local thunkMiddleware = require(script.Parent.thunkMiddleware) + + it("should dispatch thunks", function() + local function reducer(state, action) + return state + end + + local store = Store.new(reducer, {}, { thunkMiddleware }) + local thunkCount = 0 + + local function thunk(store) + thunkCount = thunkCount + 1 + end + + store:dispatch(thunk) + + expect(thunkCount).to.equal(1) + end) + + it("should allow normal actions to pass through", function() + local reducerCount = 0 + + local function reducer(state, action) + reducerCount = reducerCount + 1 + return state + end + + local store = Store.new(reducer, {}, { thunkMiddleware }) + + store:dispatch({ + type = "test", + }) + + -- Reducer will be invoked twice: + -- Once when creating the store (@@INIT action) + -- Once when the test action is dispatched + expect(reducerCount).to.equal(2) + end) + + it("should return the value from the thunk", function() + local function reducer(state, action) + return state + end + + local store = Store.new(reducer, {}, { thunkMiddleware }) + local thunkValue = "test" + + local function thunk(store) + return thunkValue + end + + local result = store:dispatch(thunk) + + expect(result).to.equal(thunkValue) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_state-table/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_state-table/lock.toml new file mode 100644 index 0000000..793b277 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_state-table/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/state-table" +version = "0.1.0" +commit = "aa0641127efcb15fcabb26f5ba46e44815f42d4a" +source = "url+https://github.com/roblox/state-table" diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_state-table/state-table/StateTable.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_state-table/state-table/StateTable.lua new file mode 100644 index 0000000..7e88902 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_state-table/state-table/StateTable.lua @@ -0,0 +1,202 @@ +local StateTable = {} + +StateTable.__index = StateTable + +local function validateStateTableItem(item, qualifier) + local type = typeof(item) + local isValid = type == "string" or type == "userdata" + assert(isValid, string.format("%s '%s' must be a string or userdata but is a %s", qualifier, tostring(item), type)) +end + +--[[ + This class method creates a new StateTable instance that you can use to control complicated + logic that is based upon your state machine design. Ex: + + self.stateTable = StateTable.new(name, initialState, initialContext, { + InitialState = { + EventName1 = { nextState = "StateOne", action = self.actionDoSomething }, + EventName2 = { nextState = "FinalState" } -- actions are optional + }, + StateOne = { + EventName1 = { nextState = "FinalState", action = self.actionDoSomethingAtLast }, + EventName3 = { action = self.actionDoSomethingElse } -- will maintain current state + }, + FinalState = {} -- transitions are optional + }) + + Arguments: + name - A debug name for this StateTable. (String) + initialState - Name of the beginning state for this StateTable. (String) + initialContext - A reference to an existing table where you hold all sidecar contextual data + that needs to be manipulated by this StateTable's actions. (Table) + transitionTable - Description of the state machine structure. (Table) + + The outermost keys in "transitionTable" represent individual states in your design, each of which + contains a description of the events that can be called while in that state. Calling an event + triggers a transition to a new state while also (optionally) running an action functor. + + (All states and events must be simple strings or userdata.) + (If using userdata for states and events, implement a tostring metamethod for ease of debugging.) + + Named events in your state table will be converted into functions that you can call directly. + Calling these event functions will transition the StateTable to the appropriate nextState and + call the registered action handler, if any. You may pass arguments to your actions through the + event function by passing them as a table. Ex: + + self.stateTable.events.EventName1(args) + + The combination of named states and events in StateTable make up the control flow portion of your + state machine. To run business logic, you need to implement Actions. + + Each action functor accepts four arguments: the current state, the next state, event arguments, + and the contextual data table that you passed in when you called the event function. Your action + functor should return a table containing the keys that need to be updated from currentContext. + The returned table will be merged into currentContext and passed back to your onStateChange + callback. Ex: + + function actionDoSomething(currentState, nextState, args, currentContext) + local contextDiff = doSomething(args, currentContext) + return contextDiff + end + + Do NOT update your own copy of the StateTable's internal state variable or your context in actions. + If this is an action-less transition, you'll fail to update it! + + To update your context and own tracking of the current state at the same time, listen to changes + via StateTable:onStateChange. See the documentation of that method for more details. +]] +function StateTable.new(name, initialState, initialContext, transitionTable) + assert(typeof(name) == "string", "name must be a string") + assert(#name > 0, "name must not be an empty string") + + validateStateTableItem(initialState, "initialState") + assert(initialContext == nil or typeof(initialContext) == "table", "initialContext must be a table or nil") + + assert(typeof(transitionTable) == "table", "transitionTable must be a table") + assert(typeof(transitionTable[initialState]) == "table", "initialState must be present in transitionTable") + + local self = {} + setmetatable(self, StateTable) + + self.name = name + self.currentState = initialState + self.currentContext = initialContext or {} + self.transitionTable = {} + self.events = {} + + for state, eventTable in pairs(transitionTable) do + validateStateTableItem(state, "state") + assert(typeof(eventTable) == "table", string.format("state '%s' must map to a table", tostring(state))) + + local parsedEventTable = {} + for event, eventData in pairs(eventTable) do + validateStateTableItem(event, "event") + assert(typeof(eventData) == "table", string.format("event '%s' must map to a table", tostring(event))) + + local nextState = eventData.nextState + local action = eventData.action + + if nextState ~= nil then + validateStateTableItem(nextState, "nextState") + + -- Check that the transition lands on a known state + assert(transitionTable[nextState] ~= nil, + string.format("nextState '%s' does not exist in transitionTable", tostring(nextState))) + end + + assert(action == nil or typeof(action) == "function", "action must be a function") + + parsedEventTable[event] = eventData + + -- Create a function to make it easy to call this event + if self.events[event] == nil then + self.events[event] = function(args) + return self:handleEvent(event, args) + end + end + end + + self.transitionTable[state] = parsedEventTable + end + + -- catch calls to invalid events earlier + setmetatable(self.events, { + __index = function(_, event) + error(string.format("'%s' is not a valid event in StateTable '%s'", + tostring(event), self.name), 2) + end + }) + + return self +end + +--[[ + It is recommended that you use the auto-generated event functions instead + of calling this method; see StateTable.new. + + Process an event through this StateTable instance. Pass in the name + of the event, and optional arguments. The arguments will be passed to the + registered action handler for the state/event transition, if any. + + This function does not return anything. Listen to changes using + StateTable:onStateChange if you need to store the current state or use the + results of an action. +]] +function StateTable:handleEvent(event, args) + validateStateTableItem(event, "event") + assert(args == nil or typeof(args) == "table", "args must be nil or valid table") + + local currentState = self.currentState + local eventMap = self.transitionTable[currentState] + + assert(eventMap ~= nil, "no transition events for current state") + + if eventMap[event] ~= nil then + local eventData = eventMap[event] + local nextState = eventData.nextState or currentState + local action = eventData.action + + local updatedContext = self.currentContext + if action ~= nil then + local contextDiff = action(currentState, nextState, args, self.currentContext) or {} + + -- Immutable merge dictionaries (without Cryo) + updatedContext = {} + for key, value in pairs(self.currentContext) do + updatedContext[key] = value + end + + for key, value in pairs(contextDiff) do + updatedContext[key] = value + end + + self.currentContext = updatedContext + end + + self.currentState = nextState + + if self.stateChangeHandler ~= nil then + self.stateChangeHandler(currentState, nextState, updatedContext) + end + end +end + +--[[ + Register a function to process changes in state. Your function should have + the following signature and return nothing: + + function handleStateChange(oldState, newState, updatedContext) + self.currentState = newState + self.currentContext = updatedContext + end + + The updatedContext parameter contains the table that was returned by the action + handler associated with the event transition. +]] +function StateTable:onStateChange(stateChangeHandler) + assert(stateChangeHandler == nil or typeof(stateChangeHandler) == "function", + "stateChangeHandler must be nil or a function") + self.stateChangeHandler = stateChangeHandler +end + +return StateTable diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_state-table/state-table/StateTable.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_state-table/state-table/StateTable.spec.lua new file mode 100644 index 0000000..df751d9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_state-table/state-table/StateTable.spec.lua @@ -0,0 +1,525 @@ +return function() + local StateTable = require(script.Parent.StateTable) + + local TEST_NAME = "test_state_table_name" + local TEST_INITIAL_STATE = "Initial" + local TEST_DATA = { foo = 1 } + + local DUMMY_ACTION = function(_, _, data) + return data + end + + local function FieldCount(t) + local fieldCount = 0 + for _ in pairs(t) do + fieldCount = fieldCount + 1 + end + return fieldCount + end + + local function ShallowEqual(A, B) + if not A or not B then + return false + elseif A == B then + return true + end + + for key, value in pairs(A) do + if B[key] ~= value then + return false + end + end + for key, value in pairs(B) do + if A[key] ~= value then + return false + end + end + + return true + end + + describe("StateTable.new validation throw tests", function() + it("should throw if name is nil", function() + expect(function() + StateTable.new(nil, TEST_INITIAL_STATE, {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if name is not a string", function() + expect(function() + StateTable.new(5, TEST_INITIAL_STATE, {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if name is empty", function() + expect(function() + StateTable.new("", TEST_INITIAL_STATE, {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if initialState is nil", function() + expect(function() + StateTable.new(TEST_NAME, nil, {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if initialState is not a string", function() + expect(function() + StateTable.new(TEST_NAME, 5, {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if initialState is empty", function() + expect(function() + StateTable.new(TEST_NAME, "", {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if initialContext is wrong type", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, 5, { Initial = {} }) + end).to.throw() + end) + + it("should throw if transitionTable is nil", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, nil) + end).to.throw() + end) + + it("should throw if empty transitionTable is provided", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, {}) + end).to.throw() + end) + + it("should throw if non-table transitionTable is provided", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, 1) + end).to.throw() + end) + + it("should throw if no arguments are provided", function() + expect(function() + StateTable.new() + end).to.throw() + end) + + it("should throw if non-string/userdata used for state name", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { [1] = {} }) + end).to.throw() + end) + + it("should throw if non-table is used for state table", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = 1 }) + end).to.throw() + end) + + it("should throw if non-string/userdata is used for event name", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { [1] = {} } }) + end).to.throw() + end) + + it("should throw if non-table is used for event data table", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = 1 } }) + end).to.throw() + end) + + it("should throw if non-string/userdata used for nextState", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { nextState = 1 } } }) + end).to.throw() + end) + + it("should throw if non-function used for action", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { action = 1 } } }) + end).to.throw() + end) + + it("should throw if initial state not in transitionTable", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { NotInitial = {} }) + end).to.throw() + end) + end) + + describe("StateTable.new validation success tests", function() + it("should not throw if empty event table is provided", function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} }) + end) + + it("should not throw if empty event data table is provided", function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = {} } }) + end) + + it("should not throw if action is missing", function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Next" } + }, + Next = {} + }) + end) + + it("should not throw if nextState is missing", function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { action = function() end } } }) + end) + end) + + describe("StateTable.new event function creation tests", function() + it("should create an event function for a single event", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { } } }) + expect(FieldCount(st.events)).to.equal(1) + expect(typeof(st.events.Event1)).to.equal("function") + end) + + it("should create different event functions for multiple events", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { }, Event2 = { } } }) + expect(FieldCount(st.events)).to.equal(2) + expect(typeof(st.events.Event1)).to.equal("function") + expect(typeof(st.events.Event2)).to.equal("function") + end) + + it("should not create event functions without at least one event", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} }) + expect(FieldCount(st.events)).to.equal(0) + end) + end) + + describe("StateTable onStateChange registration tests", function() + it("should assert when non-function provided", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} }) + expect(function() + st:onStateChange(5) + end).to.throw() + end) + + it("should not assert when function provided", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} }) + st:onStateChange(function() end) + end) + + it("should not assert when nil provided", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} }) + st:onStateChange(nil) + end) + + it("should call onStateChange handler when transition occurs", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = {} + } + }) + + local called = false + st:onStateChange(function() + called = true + end) + + st.events.Event1(nil) + expect(called).to.equal(true) + end) + + it("should not call onStateChange handler after it has been de-registered", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = {} + } + }) + + local called = false + st:onStateChange(function() + called = true + end) + + st:onStateChange(nil) + + st.events.Event1(nil) + expect(called).to.equal(false) + end) + end) + + describe("StateTable event call tests", function() + it("should change state when handleEvent is called", function() + local action1Called = false + local testAction1 = function() action1Called = true end + + local action2Called = false + local testAction2 = function() action2Called = true end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two", action = testAction1 } + }, + Two = { + Event2 = { action = testAction2 } + } + }) + + st:handleEvent("Event1", nil) + expect(action1Called).to.equal(true) + expect(action2Called).to.equal(false) + + action1Called = false + action2Called = false + + st:handleEvent("Event2", nil) + expect(action1Called).to.equal(false) + expect(action2Called).to.equal(true) + end) + + it("should call mapped action when handleEvent is called", function() + local actionOldState, actionNewState, actionData + local testAction = function(oldState, newState, data) + actionOldState = oldState + actionNewState = newState + actionData = data + end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two", action = testAction } + }, + Two = {} + }) + + st:handleEvent("Event1", TEST_DATA) + expect(actionOldState).to.equal("Initial") + expect(actionNewState).to.equal("Two") + expect(ShallowEqual(actionData, TEST_DATA)).to.equal(true) + end) + + it("should return expected nextState and data in onStateChange callback when handleEvent is called", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two", action = DUMMY_ACTION } + }, + Two = {} + }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st:handleEvent("Event1", TEST_DATA) + + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Two") + expect(ShallowEqual(updatedContext, TEST_DATA)).to.equal(true) + end) + + it("should call mapped action when event functor is called", function() + local actionOldState, actionNewState, actionData + local testAction = function(oldState, newState, data) + actionOldState = oldState + actionNewState = newState + actionData = data + end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two", action = testAction } + }, + Two = {} + }) + + st.events.Event1(TEST_DATA) + expect(actionOldState).to.equal("Initial") + expect(actionNewState).to.equal("Two") + expect(ShallowEqual(actionData, TEST_DATA)).to.equal(true) + end) + + it("should return expected nextState and data in onStateChange callback when event functor is called", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two", action = DUMMY_ACTION } + }, + Two = {} + }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st.events.Event1(TEST_DATA) + + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Two") + expect(ShallowEqual(updatedContext, TEST_DATA)).to.equal(true) + end) + + it("should still return nextState in onStateChange callback when no action handler is provided", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two" } + }, + Two = {} + }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st.events.Event1(TEST_DATA) + + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Two") + expect(typeof(updatedContext)).to.equal("table") + expect(FieldCount(updatedContext)).to.equal(0) + end) + + it("should return current state and empty data if new state and action handler are not specified", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { } } }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st.events.Event1(TEST_DATA) + + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Initial") + expect(FieldCount(updatedContext)).to.equal(0) + end) + + it("should return current state and matching data if only action handler is provided", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { action = DUMMY_ACTION } } }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st.events.Event1(TEST_DATA) + + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Initial") + expect(ShallowEqual(updatedContext, TEST_DATA)).to.equal(true) + end) + + it("should return empty data in onStateChange callback when nil data is provided to no-action event", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = {} } }) + + local updatedContext + st:onStateChange(function(_, _, uc) + updatedContext = uc + end) + + st.events.Event1(nil) + + expect(FieldCount(updatedContext)).to.equal(0) + end) + + it("should merge context updates with old context", function() + local initialContext = { foo = 1 } + + local function action1() + return { bar = 2 } + end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, initialContext, { + Initial = { + Event1 = { action = action1 } + } + }) + + local updatedContext + st:onStateChange(function(_, _, uc) + updatedContext = uc + end) + + st.events.Event1() + + expect(ShallowEqual(updatedContext, { foo = 1, bar = 2 })).to.equal(true) + end) + + it("should pass args to actions", function() + local passedArgs + local function action1(_, _, args) + passedArgs = args + end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { Event1 = { action = action1 } } + }) + + local theArgs = { argsAreHere = true } + st.events.Event1(theArgs) + + expect(ShallowEqual(theArgs, passedArgs)).to.equal(true) + end) + + it("should call actions independently for different events", function() + local testData1 = TEST_DATA + local testData2 = { foo = 2 } + + local action1OldState, action1NewState, action1Data + local testAction1 = function(oldState, newState, data) + action1OldState = oldState + action1NewState = newState + action1Data = data + return data + end + + local action2OldState, action2NewState, action2Data + local testAction2 = function(oldState, newState, data) + action2OldState = oldState + action2NewState = newState + action2Data = data + return data + end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { action = testAction1 }, + Event2 = { nextState = "Two", action = testAction2 }, + }, + Two = {} + }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st.events.Event1(testData1) + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Initial") + expect(ShallowEqual(updatedContext, testData1)).to.equal(true) + + st.events.Event2(testData2) + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Two") + expect(ShallowEqual(updatedContext, testData2)).to.equal(true) + + expect(action1OldState).to.equal("Initial") + expect(action2OldState).to.equal("Initial") + expect(action1NewState).to.equal("Initial") + expect(action2NewState).to.equal("Two") + + expect(ShallowEqual(action1Data, testData1)).to.equal(true) + expect(ShallowEqual(action2Data, testData2)).to.equal(true) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_state-table/state-table/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_state-table/state-table/init.lua new file mode 100644 index 0000000..6fce9eb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_state-table/state-table/init.lua @@ -0,0 +1,9 @@ +-- Generator information: +-- Human name: State Table +-- Variable name: StateTable +-- Repo name: state-table + +local StateTable = require(script.StateTable) + +return StateTable + diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/lock.toml new file mode 100644 index 0000000..080eca2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/string-utilities" +version = "1.0.0" +commit = "7cbef7885ca023d71dd93f02ffa32cbda65faa9c" +source = "url+https://github.com/roblox/string-utilities" diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/ParseQuery.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/ParseQuery.lua new file mode 100644 index 0000000..e3e0d8b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/ParseQuery.lua @@ -0,0 +1,84 @@ +--!nocheck +local RunService = game:GetService("RunService") +local StringSplit = require(script.Parent.StringSplit) + +function assertIsType(value, expectedType, name) + if RunService:IsStudio() or _G.__TESTEZ_RUNNING_TEST__ then + assert( + typeof(value) == expectedType, + string.format("expects %s to be a %s! it was: %s", name, expectedType, typeof(value)) + ) + end +end + +local function urlDecode(input) + assertIsType(input, "string", "input") + local decoded = string.gsub(input, '%%(%x%x)', function(charCode) + return string.char(tonumber(charCode, 16)) + end) + decoded = string.gsub(decoded, "+", " ") + return decoded; +end + +--[[ + Parses a query string into a lua table. + * supports both "&"" and ";" as separator (and "=" separates names/values) + * empty names or values are returned as "" (eg "k1=&=v2&k3") + * completely empty pairs are ignored (eg "k1=v1&&k3=v3") + * url decodes all names and values + * supports multiple values + * by default, all values are returned in tables + * if listKeyMapper (name => listKey) is provided: + * only the last value for a name is returned as that key (ie name) + * the list of values is returned at the key provided by listKeyMapper + * listKeyMapper can return the same input key (ie name), or nil (same effect) +]] +local function ParseQuery(input, listKeyMapper) + if input ~= nil then + assertIsType(input, "string", "input") + end + if listKeyMapper ~= nil then + assertIsType(listKeyMapper, "function", "listKeyMapper") + end + + local useListKeys = type(listKeyMapper) == "function" + local parsed = {} + if input and #input > 0 then + local items = StringSplit(input, "[&;]") + for _, item in ipairs(items) do + if item and #item > 0 then + local key, value = unpack(string.split(item, "=")) + if value == nil then + value = "" + end + key = urlDecode(key) + value = urlDecode(value) + if useListKeys then + if parsed[key] ~= nil then + local listKey = listKeyMapper(key) + if listKey == nil then + listKey = key + end + if type(parsed[listKey]) ~= "table" then + parsed[listKey] = { parsed[key] } + end + table.insert(parsed[listKey], value) + if key ~= listKey then + parsed[key] = value + end + else + parsed[key] = value + end + else + if parsed[key] == nil then + parsed[key] = {} + end + table.insert(parsed[key], value) + end + end + end + end + return parsed +end + +return ParseQuery diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/ParseQuery.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/ParseQuery.spec.lua new file mode 100644 index 0000000..79014d0 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/ParseQuery.spec.lua @@ -0,0 +1,74 @@ +return function() + local ParseQuery = require(script.Parent.ParseQuery) + + describe("ParseQuery normal usage", function() + it("should parse a normal query string", function() + local parsed = ParseQuery("name1=value1&name2=value2") + + expect(parsed.name1[1]).to.equal("value1") + expect(parsed.name2[1]).to.equal("value2") + end) + + it("should parse multiple values for the same name", function() + local parsed = ParseQuery("name1=value1&name1=value2") + + expect(#parsed.name1).to.equal(2) + expect(parsed.name1[1]).to.equal("value1") + expect(parsed.name1[2]).to.equal("value2") + end) + + it("should allow custom keys for multiple values", function() + local parsed = ParseQuery("name1=value1&name2=value2&name2=value3", function(key) + return "list_of_" .. key + end) + + expect(parsed.name1).to.equal("value1") + expect(parsed.name2).to.equal("value3") + + expect(#parsed.list_of_name2).to.equal(2) + expect(parsed.list_of_name2[1]).to.equal("value2") + expect(parsed.list_of_name2[2]).to.equal("value3") + end) + + it("should url decode names and values", function() + local parsed = ParseQuery("name1=value+1&%5Fname2=value2") + + expect(parsed.name1[1]).to.equal("value 1") + expect(parsed._name2[1]).to.equal("value2") + end) + end) + + describe("ParseQuery edge cases", function() + it("should return an empty table on empty or nil input", function() + local parsed1 = ParseQuery("") + local parsed2 = ParseQuery(nil) + + expect(#parsed1).to.equal(0) + expect(#parsed2).to.equal(0) + end) + + it("should support listKeyMapper returning key or nil", function() + local parsed = ParseQuery("name1=value1&name2=value2&name2=value3", function()end) + + expect(parsed.name1).to.equal("value1") + expect(#parsed.name2).to.equal(2) + expect(parsed.name2[1]).to.equal("value2") + expect(parsed.name2[2]).to.equal("value3") + end) + + it("should use empty strings for unavailable values", function() + local parsed = ParseQuery("name1=&=value2&name3", function()end) + + expect(parsed.name1).to.equal("") + expect(parsed[""]).to.equal("value2") + expect(parsed.name3).to.equal("") + end) + + it("should support `;` as separator", function() + local parsed = ParseQuery("name1=value1;name2=value2", function()end) + + expect(parsed.name1).to.equal("value1") + expect(parsed.name2).to.equal("value2") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringReplaceAll.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringReplaceAll.lua new file mode 100644 index 0000000..1f6560c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringReplaceAll.lua @@ -0,0 +1,26 @@ +-- Performs multiple replacements on the given string. +-- replacements is a table where the keys are patterns to be replaced, and the +-- values are the replacements to be made. For example: +-- StringReplaceAll( +-- "key1 key2 key3", +-- {["%w+1"] = "value1", ["%w+2"] = "value2"} +-- ) +-- becomes "value1 value2 key3". +return function(str, replacements) + if type(str) ~= "string" then + return "" + end + + if type(replacements) ~= "table" then + return str + end + + local result = str + for piiStr, replaceStr in pairs(replacements) do + if type(piiStr) == "string" and type(replaceStr) == "string" then + result = string.gsub(result, piiStr, replaceStr) + end + end + + return result +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringReplaceAll.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringReplaceAll.spec.lua new file mode 100644 index 0000000..0a8cf74 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringReplaceAll.spec.lua @@ -0,0 +1,78 @@ +return function() + local StringReplaceAll = require(script.Parent.StringReplaceAll) + + it("should return empty string if str is not a string", function() + local result = StringReplaceAll(1, { "a" }) + expect(result).to.equal("") + + result = StringReplaceAll({ 1, 2, 3 }, { "a" }) + expect(result).to.equal("") + + result = StringReplaceAll(nil, { "a" }) + expect(result).to.equal("") + end) + + it("should return original string if replacements is not a table", function() + local result = StringReplaceAll("abc", "a") + expect(result).to.equal("abc") + + result = StringReplaceAll("abc", 1) + expect(result).to.equal("abc") + + result = StringReplaceAll("abc", nil) + expect(result).to.equal("abc") + end) + + it("should replace a PII string if provided", function() + local result = StringReplaceAll("abc", { a = "" }) + expect(result).to.equal("bc") + + result = StringReplaceAll("abc", { b = "" }) + expect(result).to.equal("ac") + + result = StringReplaceAll("abc-123-a2c", { ["2"] = "" }) + expect(result).to.equal("abc-13-ac") + + result = StringReplaceAll("abc-123-a2c", { a = "A" }) + expect(result).to.equal("Abc-123-A2c") + + -- special character + result = StringReplaceAll("hello, said Noob_123", { ["Noob_123"] = "Username" }) + expect(result).to.equal("hello, said Username") + end) + + it("should replace all PII strings provided", function() + local result = StringReplaceAll("abc", { a = "A", c = "d"}) + expect(result).to.equal("Abd") + + result = StringReplaceAll("abc", { a = "A", bc = "" }) + expect(result).to.equal("A") + + result = StringReplaceAll("abc-123-a23", { abc = "user", ["123"] = "id" }) + expect(result).to.equal("user-id-a23") + end) + + it("should return original string if replacements is an empty table or no matches are found", function() + local result = StringReplaceAll("abc", {}) + expect(result).to.equal("abc") + + result = StringReplaceAll("abc", { d = "e" }) + expect(result).to.equal("abc") + + result = StringReplaceAll("abc", { d = "e", e = "f" }) + expect(result).to.equal("abc") + end) + + it("should ignore PIIs if they are not strings", function() + local result = StringReplaceAll("abc", { a = 1 }) + expect(result).to.equal("abc") + + result = StringReplaceAll("abc", { a = 1, bc = "" }) + expect(result).to.equal("a") + end) + + it("should match the example given in the doc string", function() + local result = StringReplaceAll("key1 key2 key3", {["%w+1"] = "value1", ["%w+2"] = "value2"}) + expect(result).to.equal("value1 value2 key3") + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringSplit.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringSplit.lua new file mode 100644 index 0000000..12d8a1c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringSplit.lua @@ -0,0 +1,45 @@ +-- Split the input string according to the separator pattern into at most limit +-- pieces. If the separator is nil, it defaults to consecutive whitespace (%s+). +-- If the limit isn't defined, it will split the string into as many pieces as +-- it finds. +-- Note: There is now a string.split function built into Luau though it isn't an +-- exact replacement for this. +local function StringSplit(input, separator, limit) + if #input == 0 then + if string.find(input, separator) then + return {} + else + return {""} + end + end + if not limit then + limit = -1 + end + if limit == 1 then + return {input} + end + if not separator then + separator = "%s+" + end + local start, stop = string.find(input, separator) + if not start then + return {input} + end + -- special case, delimiter resolved to "" + if stop < 1 then + start, stop = string.find(string.sub(input, 2), separator) + start = start + 1 + stop = stop + 1 + end + local first = string.sub(input, 1, start - 1) + local rest = string.sub(input, stop + 1) + -- special case, non empty pattern found at the end + if #rest == 0 and stop >= start then + return {first, rest} + end + local items = StringSplit(rest, separator, limit - 1) + table.insert(items, 1, first) + return items +end + +return StringSplit diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringSplit.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringSplit.spec.lua new file mode 100644 index 0000000..48d0bcc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringSplit.spec.lua @@ -0,0 +1,108 @@ +return function() + local StringSplit = require(script.Parent.StringSplit) + + describe("Normal usage", function() + + it("should split a string by a one character separator", function() + local str = "Roblox Powering Imagination" + local words = StringSplit(str, " ") + + expect(#words).to.equal(3) + expect(words[1]).to.equal("Roblox") + expect(words[2]).to.equal("Powering") + expect(words[3]).to.equal("Imagination") + end) + + it("should split a string by a complex regex", function() + local str = "https://corp.roblox.com/technology" + local words = StringSplit(str, "[^a-z]+") + + expect(#words).to.equal(5) + expect(words[1]).to.equal("https") + expect(words[2]).to.equal("corp") + expect(words[3]).to.equal("roblox") + expect(words[4]).to.equal("com") + expect(words[5]).to.equal("technology") + end) + + it("should split on blank spaces by default", function() + local str = "together through\n\tplay" + local words = StringSplit(str) + + expect(#words).to.equal(3) + expect(words[1]).to.equal("together") + expect(words[2]).to.equal("through") + expect(words[3]).to.equal("play") + end) + + it("should not exceed the provided limit", function() + local str = "Modules/Common/StringUtilities/StringSplit" + local words = StringSplit(str, "/", 3) + + expect(#words).to.equal(3) + expect(words[1]).to.equal("Modules") + expect(words[2]).to.equal("Common") + expect(words[3]).to.equal("StringUtilities/StringSplit") + end) + + it("should give en empty string on leading, repeated and trailing separators", function() + local str = "/var//www/" + local words = StringSplit(str, "/") + + expect(#words).to.equal(5) + expect(words[1]).to.equal("") + expect(words[2]).to.equal("var") + expect(words[3]).to.equal("") + expect(words[4]).to.equal("www") + expect(words[5]).to.equal("") + end) + + end) + + describe("Edge case:", function() + + it("an empty string results in one empty result", function() + local str = "" + local words = StringSplit(str, "/") + + expect(#words).to.equal(1) + expect(words[1]).to.equal("") + end) + + it("an empty separator splits to characters, no leading/trailing empty strings", function() + local str = "lua" + local words = StringSplit(str, "") + + expect(#words).to.equal(3) + expect(words[1]).to.equal("l") + expect(words[2]).to.equal("u") + expect(words[3]).to.equal("a") + end) + + it("empty string AND separator should get an empty array", function() + local str = "" + local words = StringSplit(str, "") + + expect(#words).to.equal(0) + end) + + it("regex separators can resolve to empty and non empty in same operation", function() + local str = "//r#blox" + local words = StringSplit(str, "[^a-z]*") + + expect(#words).to.equal(6) + -- pattern resolved to "//", creating a leading empty string + expect(words[1]).to.equal("") + -- pattern resolved to "#" + expect(words[2]).to.equal("r") + -- pattern resolved to "", splitting by character + expect(words[3]).to.equal("b") + expect(words[4]).to.equal("l") + expect(words[5]).to.equal("o") + expect(words[6]).to.equal("x") + -- pattern resolved to "", NO trailing empty string + end) + + end) + +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringTrim.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringTrim.lua new file mode 100644 index 0000000..c460b47 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringTrim.lua @@ -0,0 +1,34 @@ +-- Trim the given set of characters from both ends of the input string. `sides` +-- can be set to {left = true} or {right = true} to trim from just one side. +local function StringTrim(input, chars, sides) + if not chars then + chars = "%s" + end + if #chars == 0 then + return input + end + if not sides then + sides = { + left = true, + right = true, + } + end + local trimmed = input + if sides.left then + local start = string.find(trimmed, "[^" .. chars .. "]") + if not start then + return "" + end + trimmed = string.sub(trimmed, start) + end + if sides.right then + local stop = string.find(trimmed, "[" .. chars .. "]+$") + if not stop then + return trimmed + end + trimmed = string.sub(trimmed, 1, stop-1) + end + return trimmed +end + +return StringTrim diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringTrim.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringTrim.spec.lua new file mode 100644 index 0000000..e3f85aa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringTrim.spec.lua @@ -0,0 +1,54 @@ +return function() + local StringTrim = require(script.Parent.StringTrim) + + describe("Normal usage", function() + + it("should trim the specified character", function() + local str = "/Modules/Common/StringUtilities/" + local trimmed = StringTrim(str, "/") + + expect(trimmed).to.equal("Modules/Common/StringUtilities") + end) + + it("should accept multiple charcters", function() + local str = "(Roblox Powering Imagination){}" + local trimmed = StringTrim(str, "(){}") + + expect(trimmed).to.equal("Roblox Powering Imagination") + end) + + it("should be able to trim only on the right side", function() + local str = "(Roblox Powering Imagination){}" + local trimmed = StringTrim(str, "(){}", {right = true}) + + expect(trimmed).to.equal("(Roblox Powering Imagination") + end) + + it("should be able to trim only on the left side", function() + local str = "(Roblox Powering Imagination){}" + local trimmed = StringTrim(str, "(){}", {left = true}) + + expect(trimmed).to.equal("Roblox Powering Imagination){}") + end) + + it("should default to trimming blanks on both sides", function() + local str = "\tRoblox Powering Imagination \n" + local trimmed = StringTrim(str) + + expect(trimmed).to.equal("Roblox Powering Imagination") + end) + + end) + + describe("Edge case:", function() + + it("an empty character list is a no-op", function() + local str = " Roblox Powering Imagination " + local trimmed = StringTrim(str, "") + + expect(trimmed).to.equal(str) + end) + + end) + +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/init.lua new file mode 100644 index 0000000..7a39fc7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/init.lua @@ -0,0 +1,6 @@ +return { + ParseQuery = require(script.ParseQuery), + StringReplaceAll = require(script.StringReplaceAll), + StringSplit = require(script.StringSplit), + StringTrim = require(script.StringTrim), +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/init.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/init.spec.lua new file mode 100644 index 0000000..a0faf51 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/init.spec.lua @@ -0,0 +1,5 @@ +return function() + it("should import without error", function() + require(script.Parent) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_t/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_t/lock.toml new file mode 100644 index 0000000..5663f1a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_t/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/t" +version = "1.2.5" +commit = "5d6ee3e23a658b12bc433b0837184215618e233e" +source = "url+https://github.com/roblox/t" diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/init.lua new file mode 100644 index 0000000..fb7248e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/init.lua @@ -0,0 +1,1109 @@ +-- t: a runtime typechecker for Roblox + +-- regular lua compatibility +local typeof = typeof or type + +local function primitive(typeName) + return function(value) + local valueType = typeof(value) + if valueType == typeName then + return true + else + return false, string.format("%s expected, got %s", typeName, valueType) + end + end +end + +local t = {} + +--[[** + matches any type except nil + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.any(value) + if value ~= nil then + return true + else + return false, "any expected, got nil" + end +end + +--Lua primitives + +--[[** + ensures Lua primitive boolean type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.boolean = primitive("boolean") + +--[[** + ensures Lua primitive thread type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.thread = primitive("thread") + +--[[** + ensures Lua primitive callback type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.callback = primitive("function") + +--[[** + ensures Lua primitive none type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.none = primitive("nil") + +--[[** + ensures Lua primitive string type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.string = primitive("string") + +--[[** + ensures Lua primitive table type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.table = primitive("table") + +--[[** + ensures Lua primitive userdata type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.userdata = primitive("userdata") + +--[[** + ensures value is a number and non-NaN + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.number(value) + local valueType = typeof(value) + if valueType == "number" then + if value == value then + return true + else + return false, "unexpected NaN value" + end + else + return false, string.format("number expected, got %s", valueType) + end +end + +--[[** + ensures value is NaN + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.nan(value) + if value ~= value then + return true + else + return false, "unexpected non-NaN value" + end +end + +-- roblox types + +--[[** + ensures Roblox Axes type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Axes = primitive("Axes") + +--[[** + ensures Roblox BrickColor type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.BrickColor = primitive("BrickColor") + +--[[** + ensures Roblox CFrame type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.CFrame = primitive("CFrame") + +--[[** + ensures Roblox Color3 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Color3 = primitive("Color3") + +--[[** + ensures Roblox ColorSequence type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.ColorSequence = primitive("ColorSequence") + +--[[** + ensures Roblox ColorSequenceKeypoint type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.ColorSequenceKeypoint = primitive("ColorSequenceKeypoint") + +--[[** + ensures Roblox DockWidgetPluginGuiInfo type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.DockWidgetPluginGuiInfo = primitive("DockWidgetPluginGuiInfo") + +--[[** + ensures Roblox Faces type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Faces = primitive("Faces") + +--[[** + ensures Roblox Instance type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Instance = primitive("Instance") + +--[[** + ensures Roblox NumberRange type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.NumberRange = primitive("NumberRange") + +--[[** + ensures Roblox NumberSequence type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.NumberSequence = primitive("NumberSequence") + +--[[** + ensures Roblox NumberSequenceKeypoint type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.NumberSequenceKeypoint = primitive("NumberSequenceKeypoint") + +--[[** + ensures Roblox PathWaypoint type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.PathWaypoint = primitive("PathWaypoint") + +--[[** + ensures Roblox PhysicalProperties type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.PhysicalProperties = primitive("PhysicalProperties") + +--[[** + ensures Roblox Random type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Random = primitive("Random") + +--[[** + ensures Roblox Ray type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Ray = primitive("Ray") + +--[[** + ensures Roblox Rect type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Rect = primitive("Rect") + +--[[** + ensures Roblox Region3 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Region3 = primitive("Region3") + +--[[** + ensures Roblox Region3int16 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Region3int16 = primitive("Region3int16") + +--[[** + ensures Roblox TweenInfo type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.TweenInfo = primitive("TweenInfo") + +--[[** + ensures Roblox UDim type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.UDim = primitive("UDim") + +--[[** + ensures Roblox UDim2 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.UDim2 = primitive("UDim2") + +--[[** + ensures Roblox Vector2 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Vector2 = primitive("Vector2") + +--[[** + ensures Roblox Vector3 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Vector3 = primitive("Vector3") + +--[[** + ensures Roblox Vector3int16 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Vector3int16 = primitive("Vector3int16") + +-- roblox enum types + +--[[** + ensures Roblox Enum type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Enum = primitive("Enum") + +--[[** + ensures Roblox EnumItem type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.EnumItem = primitive("EnumItem") + +--[[** + ensures Roblox RBXScriptSignal type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.RBXScriptSignal = primitive("RBXScriptSignal") + +--[[** + ensures Roblox RBXScriptConnection type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.RBXScriptConnection = primitive("RBXScriptConnection") + +--[[** + ensures value is a given literal value + + @param literal The literal to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.literal(...) + local size = select("#", ...) + if size == 1 then + local literal = ... + return function(value) + if value ~= literal then + return false, string.format("expected %s, got %s", tostring(literal), tostring(value)) + end + return true + end + else + local literals = {} + for i = 1, size do + local value = select(i, ...) + literals[i] = t.literal(value) + end + return t.union(unpack(literals)) + end +end + +--[[** + DEPRECATED + Please use t.literal +**--]] +t.exactly = t.literal + +--[[** + Returns a t.union of each key in the table as a t.literal + + @param keyTable The table to get keys from + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.keyOf(keyTable) + local keys = {} + for key in pairs(keyTable) do + keys[#keys + 1] = key + end + return t.literal(unpack(keys)) +end + +--[[** + Returns a t.union of each value in the table as a t.literal + + @param valueTable The table to get values from + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.valueOf(valueTable) + local values = {} + for _, value in pairs(valueTable) do + values[#values + 1] = value + end + return t.literal(unpack(values)) +end + +--[[** + ensures value is an integer + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.integer(value) + local success, errMsg = t.number(value) + if not success then + return false, errMsg or "" + end + if value%1 == 0 then + return true + else + return false, string.format("integer expected, got %s", value) + end +end + +--[[** + ensures value is a number where min <= value + + @param min The minimum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberMin(min) + return function(value) + local success, errMsg = t.number(value) + if not success then + return false, errMsg or "" + end + if value >= min then + return true + else + return false, string.format("number >= %s expected, got %s", min, value) + end + end +end + +--[[** + ensures value is a number where value <= max + + @param max The maximum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberMax(max) + return function(value) + local success, errMsg = t.number(value) + if not success then + return false, errMsg + end + if value <= max then + return true + else + return false, string.format("number <= %s expected, got %s", max, value) + end + end +end + +--[[** + ensures value is a number where min < value + + @param min The minimum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberMinExclusive(min) + return function(value) + local success, errMsg = t.number(value) + if not success then + return false, errMsg or "" + end + if min < value then + return true + else + return false, string.format("number > %s expected, got %s", min, value) + end + end +end + +--[[** + ensures value is a number where value < max + + @param max The maximum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberMaxExclusive(max) + return function(value) + local success, errMsg = t.number(value) + if not success then + return false, errMsg or "" + end + if value < max then + return true + else + return false, string.format("number < %s expected, got %s", max, value) + end + end +end + +--[[** + ensures value is a number where value > 0 + + @returns A function that will return true iff the condition is passed +**--]] +t.numberPositive = t.numberMinExclusive(0) + +--[[** + ensures value is a number where value < 0 + + @returns A function that will return true iff the condition is passed +**--]] +t.numberNegative = t.numberMaxExclusive(0) + +--[[** + ensures value is a number where min <= value <= max + + @param min The minimum to use + @param max The maximum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberConstrained(min, max) + assert(t.number(min) and t.number(max)) + local minCheck = t.numberMin(min) + local maxCheck = t.numberMax(max) + return function(value) + local minSuccess, minErrMsg = minCheck(value) + if not minSuccess then + return false, minErrMsg or "" + end + + local maxSuccess, maxErrMsg = maxCheck(value) + if not maxSuccess then + return false, maxErrMsg or "" + end + + return true + end +end + +--[[** + ensures value is a number where min < value < max + + @param min The minimum to use + @param max The maximum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberConstrainedExclusive(min, max) + assert(t.number(min) and t.number(max)) + local minCheck = t.numberMinExclusive(min) + local maxCheck = t.numberMaxExclusive(max) + return function(value) + local minSuccess, minErrMsg = minCheck(value) + if not minSuccess then + return false, minErrMsg or "" + end + + local maxSuccess, maxErrMsg = maxCheck(value) + if not maxSuccess then + return false, maxErrMsg or "" + end + + return true + end +end + +--[[** + ensures value matches string pattern + + @param string pattern to check against + + @returns A function that will return true iff the condition is passed +**--]] +function t.match(pattern) + assert(t.string(pattern)) + return function(value) + local stringSuccess, stringErrMsg = t.string(value) + if not stringSuccess then + return false, stringErrMsg + end + + if string.match(value, pattern) == nil then + return false, string.format("\"%s\" failed to match pattern \"%s\"", value, pattern) + end + + return true + end +end + +--[[** + ensures value is either nil or passes check + + @param check The check to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.optional(check) + assert(t.callback(check)) + return function(value) + if value == nil then + return true + end + local success, errMsg = check(value) + if success then + return true + else + return false, string.format("(optional) %s", errMsg or "") + end + end +end + +--[[** + matches given tuple against tuple type definition + + @param ... The type definition for the tuples + + @returns A function that will return true iff the condition is passed +**--]] +function t.tuple(...) + local checks = {...} + return function(...) + local args = {...} + for i = 1, #checks do + local success, errMsg = checks[i](args[i]) + if success == false then + return false, string.format("Bad tuple index #%s:\n\t%s", i, errMsg or "") + end + end + return true + end +end + +--[[** + ensures all keys in given table pass check + + @param check The function to use to check the keys + + @returns A function that will return true iff the condition is passed +**--]] +function t.keys(check) + assert(t.callback(check)) + return function(value) + local tableSuccess, tableErrMsg = t.table(value) + if tableSuccess == false then + return false, tableErrMsg or "" + end + + for key in pairs(value) do + local success, errMsg = check(key) + if success == false then + return false, string.format("bad key %s:\n\t%s", tostring(key), errMsg or "") + end + end + + return true + end +end + +--[[** + ensures all values in given table pass check + + @param check The function to use to check the values + + @returns A function that will return true iff the condition is passed +**--]] +function t.values(check) + assert(t.callback(check)) + return function(value) + local tableSuccess, tableErrMsg = t.table(value) + if tableSuccess == false then + return false, tableErrMsg or "" + end + + for key, val in pairs(value) do + local success, errMsg = check(val) + if success == false then + return false, string.format("bad value for key %s:\n\t%s", tostring(key), errMsg or "") + end + end + + return true + end +end + +--[[** + ensures value is a table and all keys pass keyCheck and all values pass valueCheck + + @param keyCheck The function to use to check the keys + @param valueCheck The function to use to check the values + + @returns A function that will return true iff the condition is passed +**--]] +function t.map(keyCheck, valueCheck) + assert(t.callback(keyCheck), t.callback(valueCheck)) + local keyChecker = t.keys(keyCheck) + local valueChecker = t.values(valueCheck) + return function(value) + local keySuccess, keyErr = keyChecker(value) + if not keySuccess then + return false, keyErr or "" + end + + local valueSuccess, valueErr = valueChecker(value) + if not valueSuccess then + return false, valueErr or "" + end + + return true + end +end + +do + local arrayKeysCheck = t.keys(t.integer) + --[[** + ensures value is an array and all values of the array match check + + @param check The check to compare all values with + + @returns A function that will return true iff the condition is passed + **--]] + function t.array(check) + assert(t.callback(check)) + local valuesCheck = t.values(check) + return function(value) + local keySuccess, keyErrMsg = arrayKeysCheck(value) + if keySuccess == false then + return false, string.format("[array] %s", keyErrMsg or "") + end + + -- # is unreliable for sparse arrays + -- Count upwards using ipairs to avoid false positives from the behavior of # + local arraySize = 0 + + for _, _ in ipairs(value) do + arraySize = arraySize + 1 + end + + for key in pairs(value) do + if key < 1 or key > arraySize then + return false, string.format("[array] key %s must be sequential", tostring(key)) + end + end + + local valueSuccess, valueErrMsg = valuesCheck(value) + if not valueSuccess then + return false, string.format("[array] %s", valueErrMsg or "") + end + + return true + end + end +end + +do + local callbackArray = t.array(t.callback) + --[[** + creates a union type + + @param ... The checks to union + + @returns A function that will return true iff the condition is passed + **--]] + function t.union(...) + local checks = {...} + assert(callbackArray(checks)) + return function(value) + for _, check in pairs(checks) do + if check(value) then + return true + end + end + return false, "bad type for union" + end + end + + --[[** + Alias for t.union + **--]] + t.some = t.union + + --[[** + creates an intersection type + + @param ... The checks to intersect + + @returns A function that will return true iff the condition is passed + **--]] + function t.intersection(...) + local checks = {...} + assert(callbackArray(checks)) + return function(value) + for _, check in pairs(checks) do + local success, errMsg = check(value) + if not success then + return false, errMsg or "" + end + end + return true + end + end + + --[[** + Alias for t.intersection + **--]] + t.every = t.intersection +end + +do + local checkInterface = t.map(t.any, t.callback) + --[[** + ensures value matches given interface definition + + @param checkTable The interface definition + + @returns A function that will return true iff the condition is passed + **--]] + function t.interface(checkTable) + assert(checkInterface(checkTable)) + return function(value) + local tableSuccess, tableErrMsg = t.table(value) + if tableSuccess == false then + return false, tableErrMsg or "" + end + + for key, check in pairs(checkTable) do + local success, errMsg = check(value[key]) + if success == false then + return false, string.format("[interface] bad value for %s:\n\t%s", tostring(key), errMsg or "") + end + end + return true + end + end + + --[[** + ensures value matches given interface definition strictly + + @param checkTable The interface definition + + @returns A function that will return true iff the condition is passed + **--]] + function t.strictInterface(checkTable) + assert(checkInterface(checkTable)) + return function(value) + local tableSuccess, tableErrMsg = t.table(value) + if tableSuccess == false then + return false, tableErrMsg or "" + end + + for key, check in pairs(checkTable) do + local success, errMsg = check(value[key]) + if success == false then + return false, string.format("[interface] bad value for %s:\n\t%s", tostring(key), errMsg or "") + end + end + + for key in pairs(value) do + if not checkTable[key] then + return false, string.format("[interface] unexpected field '%s'", tostring(key)) + end + end + + return true + end + end +end + +--[[** + ensure value is an Instance and it's ClassName matches the given ClassName + + @param className The class name to check for + + @returns A function that will return true iff the condition is passed +**--]] +function t.instanceOf(className, childTable) + assert(t.string(className)) + + local childrenCheck + if childTable ~= nil then + childrenCheck = t.children(childTable) + end + + return function(value) + local instanceSuccess, instanceErrMsg = t.Instance(value) + if not instanceSuccess then + return false, instanceErrMsg or "" + end + + if value.ClassName ~= className then + return false, string.format("%s expected, got %s", className, value.ClassName) + end + + if childrenCheck then + local childrenSuccess, childrenErrMsg = childrenCheck(value) + if not childrenSuccess then + return false, childrenErrMsg + end + end + + return true + end +end +t.instance = t.instanceOf + +--[[** + ensure value is an Instance and it's ClassName matches the given ClassName by an IsA comparison + + @param className The class name to check for + + @returns A function that will return true iff the condition is passed +**--]] +function t.instanceIsA(className, childTable) + assert(t.string(className)) + + local childrenCheck + if childTable ~= nil then + childrenCheck = t.children(childTable) + end + + return function(value) + local instanceSuccess, instanceErrMsg = t.Instance(value) + if not instanceSuccess then + return false, instanceErrMsg or "" + end + + if not value:IsA(className) then + return false, string.format("%s expected, got %s", className, value.ClassName) + end + + if childrenCheck then + local childrenSuccess, childrenErrMsg = childrenCheck(value) + if not childrenSuccess then + return false, childrenErrMsg + end + end + + return true + end +end + +--[[** + ensures value is an enum of the correct type + + @param enum The enum to check + + @returns A function that will return true iff the condition is passed +**--]] +function t.enum(enum) + assert(t.Enum(enum)) + return function(value) + local enumItemSuccess, enumItemErrMsg = t.EnumItem(value) + if not enumItemSuccess then + return false, enumItemErrMsg + end + + if value.EnumType == enum then + return true + else + return false, string.format("enum of %s expected, got enum of %s", tostring(enum), tostring(value.EnumType)) + end + end +end + +do + local checkWrap = t.tuple(t.callback, t.callback) + + --[[** + wraps a callback in an assert with checkArgs + + @param callback The function to wrap + @param checkArgs The functon to use to check arguments in the assert + + @returns A function that first asserts using checkArgs and then calls callback + **--]] + function t.wrap(callback, checkArgs) + assert(checkWrap(callback, checkArgs)) + return function(...) + assert(checkArgs(...)) + return callback(...) + end + end +end + +--[[** + asserts a given check + + @param check The function to wrap with an assert + + @returns A function that simply wraps the given check in an assert +**--]] +function t.strict(check) + return function(...) + assert(check(...)) + end +end + +do + local checkChildren = t.map(t.string, t.callback) + + --[[** + Takes a table where keys are child names and values are functions to check the children against. + Pass an instance tree into the function. + If at least one child passes each check, the overall check passes. + + Warning! If you pass in a tree with more than one child of the same name, this function will always return false + + @param checkTable The table to check against + + @returns A function that checks an instance tree + **--]] + function t.children(checkTable) + assert(checkChildren(checkTable)) + + return function(value) + local instanceSuccess, instanceErrMsg = t.Instance(value) + if not instanceSuccess then + return false, instanceErrMsg or "" + end + + local childrenByName = {} + for _, child in pairs(value:GetChildren()) do + local name = child.Name + if checkTable[name] then + if childrenByName[name] then + return false, string.format("Cannot process multiple children with the same name \"%s\"", name) + end + childrenByName[name] = child + end + end + + for name, check in pairs(checkTable) do + local success, errMsg = check(childrenByName[name]) + if not success then + return false, string.format("[%s.%s] %s", value:GetFullName(), name, errMsg or "") + end + end + + return true + end + end +end + +return t \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/init.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/init.spec.lua new file mode 100644 index 0000000..19d6ff5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/init.spec.lua @@ -0,0 +1,560 @@ +return function() + local t = require(script.Parent) + + it("should support basic types", function() + assert(t.any("")) + assert(t.boolean(true)) + assert(t.none(nil)) + assert(t.number(1)) + assert(t.string("foo")) + assert(t.table({})) + + assert(not (t.any(nil))) + assert(not (t.boolean("true"))) + assert(not (t.none(1))) + assert(not (t.number(true))) + assert(not (t.string(true))) + assert(not (t.table(82))) + end) + + it("should support special number types", function() + local maxTen = t.numberMax(10) + local minTwo = t.numberMin(2) + local maxTenEx = t.numberMaxExclusive(10) + local minTwoEx = t.numberMinExclusive(2) + local constrainedEightToEleven = t.numberConstrained(8, 11) + local constrainedEightToElevenEx = t.numberConstrainedExclusive(8, 11) + + assert(maxTen(5)) + assert(maxTen(10)) + assert(not (maxTen(11))) + assert(not (maxTen())) + + assert(minTwo(5)) + assert(minTwo(2)) + assert(not (minTwo(1))) + assert(not (minTwo())) + + assert(maxTenEx(5)) + assert(maxTenEx(9)) + assert(not (maxTenEx(10))) + assert(not (maxTenEx())) + + assert(minTwoEx(5)) + assert(minTwoEx(3)) + assert(not (minTwoEx(2))) + assert(not (minTwoEx())) + + assert(not (constrainedEightToEleven(7))) + assert(constrainedEightToEleven(8)) + assert(constrainedEightToEleven(9)) + assert(constrainedEightToEleven(11)) + assert(not (constrainedEightToEleven(12))) + assert(not (constrainedEightToEleven())) + + assert(not (constrainedEightToElevenEx(7))) + assert(not (constrainedEightToElevenEx(8))) + assert(constrainedEightToElevenEx(9)) + assert(not (constrainedEightToElevenEx(11))) + assert(not (constrainedEightToElevenEx(12))) + assert(not (constrainedEightToElevenEx())) + end) + + it("should support optional types", function() + local check = t.optional(t.string) + assert(check("")) + assert(check()) + assert(not (check(1))) + end) + + it("should support tuple types", function() + local myTupleCheck = t.tuple(t.number, t.string, t.optional(t.number)) + assert(myTupleCheck(1, "2", 3)) + assert(myTupleCheck(1, "2")) + assert(not (myTupleCheck(1, "2", "3"))) + end) + + it("should support union types", function() + local numberOrString = t.union(t.number, t.string) + assert(numberOrString(1)) + assert(numberOrString("1")) + assert(not (numberOrString(nil))) + end) + + it("should support literal types", function() + local checkSingle = t.literal("foo") + local checkUnion = t.union(t.literal("foo"), t.literal("bar"), t.literal("oof")) + + assert(checkSingle("foo")) + assert(checkUnion("foo")) + assert(checkUnion("bar")) + assert(checkUnion("oof")) + + assert(not (checkSingle("FOO"))) + assert(not (checkUnion("FOO"))) + assert(not (checkUnion("BAR"))) + assert(not (checkUnion("OOF"))) + end) + + it("should support multiple literal types", function() + local checkSingle = t.literal("foo") + local checkUnion = t.literal("foo", "bar", "oof") + + assert(checkSingle("foo")) + assert(checkUnion("foo")) + assert(checkUnion("bar")) + assert(checkUnion("oof")) + + assert(not (checkSingle("FOO"))) + assert(not (checkUnion("FOO"))) + assert(not (checkUnion("BAR"))) + assert(not (checkUnion("OOF"))) + end) + + it("should support intersection types", function() + local integerMax5000 = t.intersection(t.integer, t.numberMax(5000)) + assert(integerMax5000(1)) + assert(not (integerMax5000(5001))) + assert(not (integerMax5000(1.1))) + assert(not (integerMax5000("1"))) + end) + + describe("array", function() + it("should support array types", function() + local stringArray = t.array(t.string) + local anyArray = t.array(t.any) + local stringValues = t.values(t.string) + assert(not (anyArray("foo"))) + assert(anyArray({1, "2", 3})) + assert(not (stringArray({1, "2", 3}))) + assert(not (stringArray())) + assert(not (stringValues())) + assert(anyArray({"1", "2", "3"}, t.string)) + assert(not (anyArray({ + foo = "bar" + }))) + assert(not (anyArray({ + [1] = "non", + [5] = "sequential" + }))) + end) + + it("should not be fooled by sparse arrays", function() + local anyArray = t.array(t.any) + + assert(not (anyArray({ + [1] = 1, + [2] = 2, + [4] = 4, + }))) + end) + end) + + it("should support map types", function() + local stringNumberMap = t.map(t.string, t.number) + assert(stringNumberMap({})) + assert(stringNumberMap({a = 1})) + assert(not (stringNumberMap({[1] = "a"}))) + assert(not (stringNumberMap({a = "a"}))) + assert(not (stringNumberMap())) + end) + + it("should support interface types", function() + local IVector3 = t.interface({ + x = t.number, + y = t.number, + z = t.number, + }) + + assert(IVector3({ + w = 0, + x = 1, + y = 2, + z = 3, + })) + + assert(not (IVector3({ + w = 0, + x = 1, + y = 2, + }))) + end) + + it("should support strict interface types", function() + local IVector3 = t.strictInterface({ + x = t.number, + y = t.number, + z = t.number, + }) + + assert(not (IVector3(0))) + + assert(not (IVector3({ + w = 0, + x = 1, + y = 2, + z = 3, + }))) + + assert(not (IVector3({ + w = 0, + x = 1, + y = 2, + }))) + + assert(IVector3({ + x = 1, + y = 2, + z = 3, + })) + end) + + it("should support deep interface types", function() + local IPlayer = t.interface({ + name = t.string, + inventory = t.interface({ + size = t.number + }) + }) + + assert(IPlayer({ + name = "TestPlayer", + inventory = { + size = 1 + } + })) + + assert(not (IPlayer({ + inventory = { + size = 1 + } + }))) + + assert(not (IPlayer({ + name = "TestPlayer", + inventory = { + } + }))) + + assert(not (IPlayer({ + name = "TestPlayer", + }))) + end) + + it("should support deep optional interface types", function() + local IPlayer = t.interface({ + name = t.string, + inventory = t.optional(t.interface({ + size = t.number + })) + }) + + assert(IPlayer({ + name = "TestPlayer" + })) + + assert(not (IPlayer({ + name = "TestPlayer", + inventory = { + } + }))) + + assert(IPlayer({ + name = "TestPlayer", + inventory = { + size = 1 + } + })) + end) + + it("should support Roblox Instance types", function() + local stringValueCheck = t.instanceOf("StringValue") + local stringValue = Instance.new("StringValue") + local boolValue = Instance.new("BoolValue") + + assert(stringValueCheck(stringValue)) + assert(not (stringValueCheck(boolValue))) + assert(not (stringValueCheck())) + end) + + it("should support Roblox Instance types inheritance", function() + local guiObjectCheck = t.instanceIsA("GuiObject") + local frame = Instance.new("Frame") + local textLabel = Instance.new("TextLabel") + local stringValue = Instance.new("StringValue") + + assert(guiObjectCheck(frame)) + assert(guiObjectCheck(textLabel)) + assert(not (guiObjectCheck(stringValue))) + assert(not (guiObjectCheck())) + end) + + it("should support Roblox Enum types", function() + local sortOrderEnumCheck = t.enum(Enum.SortOrder) + assert(t.Enum(Enum.SortOrder)) + assert(not (t.Enum("Enum.SortOrder"))) + + assert(t.EnumItem(Enum.SortOrder.Name)) + assert(not (t.EnumItem("Enum.SortOrder.Name"))) + + assert(sortOrderEnumCheck(Enum.SortOrder.Name)) + assert(sortOrderEnumCheck(Enum.SortOrder.Custom)) + assert(not (sortOrderEnumCheck(Enum.EasingStyle.Linear))) + assert(not (sortOrderEnumCheck())) + end) + + it("should support Roblox RBXScriptSignal", function() + assert(t.RBXScriptSignal(game.ChildAdded)) + assert(not (t.RBXScriptSignal(nil))) + assert(not (t.RBXScriptSignal(Vector3.new()))) + end) + + -- TODO: Add this back when Lemur supports it + -- it("should support Roblox RBXScriptConnection", function() + -- local conn = game.ChildAdded:Connect(function() end) + -- assert(t.RBXScriptConnection(conn)) + -- assert(not (t.RBXScriptConnection(nil))) + -- assert(not (t.RBXScriptConnection(Vector3.new()))) + -- end) + + it("should support wrapping function types", function() + local checkFoo = t.tuple(t.string, t.number, t.optional(t.string)) + local foo = t.wrap(function(a, b, c) + local result = string.format("%s %d", a, b) + if c then + result = result .. " " .. c + end + return result + end, checkFoo) + + assert(not (pcall(foo))) + assert(not (pcall(foo, "a"))) + assert(not (pcall(foo, 2))) + assert(pcall(foo, "a", 1)) + assert(pcall(foo, "a", 1, "b")) + end) + + it("should support strict types", function() + local myType = t.strict(t.tuple(t.string, t.number)) + assert(not (pcall(function() + myType("a", "b") + end))) + assert(pcall(function() + myType("a", 1) + end)) + end) + + it("should support common OOP types", function() + local MyClass = {} + MyClass.__index = MyClass + + function MyClass.new() + local self = setmetatable({}, MyClass) + return self + end + + local function instanceOfClass(class) + return function(value) + local tableSuccess, tableErrMsg = t.table(value) + if not tableSuccess then + return false, tableErrMsg or "" + end + + local mt = getmetatable(value) + if not mt or mt.__index ~= class then + return false, "bad member of class" + end + + return true + end + end + + local instanceOfMyClass = instanceOfClass(MyClass) + + local myObject = MyClass.new() + assert(instanceOfMyClass(myObject)) + assert(not (instanceOfMyClass({}))) + assert(not (instanceOfMyClass())) + end) + + it("should not treat NaN as numbers", function() + assert(t.number(1)) + assert(not (t.number(0/0))) + assert(not (t.number("1"))) + end) + + it("should not treat numbers as NaN", function() + assert(not (t.nan(1))) + assert(t.nan(0/0)) + assert(not (t.nan("1"))) + end) + + it("should allow union of number and NaN", function() + local numberOrNaN = t.union(t.number, t.nan) + assert(numberOrNaN(1)) + assert(numberOrNaN(0/0)) + assert(not (numberOrNaN("1"))) + end) + + it("should support non-string keys for interfaces", function() + local key = {} + local myInterface = t.interface({ [key] = t.number }) + assert(myInterface({ [key] = 1 })) + assert(not (myInterface({ [key] = "1" }))) + end) + + it("should support failing on non-string keys for strict interfaces", function() + local myInterface = t.strictInterface({ a = t.number }) + assert(not (myInterface({ a = 1, [{}] = 2 }))) + end) + + it("should support children", function() + local myInterface = t.interface({ + buttonInFrame = t.intersection(t.instanceOf("Frame"), t.children({ + MyButton = t.instanceOf("ImageButton") + })) + }) + + assert(not (t.children({})(5))) + assert(not (myInterface({ buttonInFrame = Instance.new("Frame") }))) + + do + local frame = Instance.new("Frame") + local button = Instance.new("ImageButton", frame) + button.Name = "MyButton" + assert(myInterface({ buttonInFrame = frame })) + end + + do + local frame = Instance.new("Frame") + local button = Instance.new("ImageButton", frame) + button.Name = "NotMyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + + do + local frame = Instance.new("Frame") + local button = Instance.new("TextButton", frame) + button.Name = "MyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + + do + local frame = Instance.new("Frame") + local button1 = Instance.new("ImageButton", frame) + button1.Name = "MyButton" + local button2 = Instance.new("ImageButton", frame) + button2.Name = "MyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + end) + + it("should support t.instanceOf shorthand", function() + local myInterface = t.interface({ + buttonInFrame = t.instanceOf("Frame", { + MyButton = t.instanceOf("ImageButton") + }) + }) + + assert(not (t.children({})(5))) + assert(not (myInterface({ buttonInFrame = Instance.new("Frame") }))) + + do + local frame = Instance.new("Frame") + local button = Instance.new("ImageButton", frame) + button.Name = "MyButton" + assert(myInterface({ buttonInFrame = frame })) + end + + do + local frame = Instance.new("Frame") + local button = Instance.new("ImageButton", frame) + button.Name = "NotMyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + + do + local frame = Instance.new("Frame") + local button = Instance.new("TextButton", frame) + button.Name = "MyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + + do + local frame = Instance.new("Frame") + local button1 = Instance.new("ImageButton", frame) + button1.Name = "MyButton" + local button2 = Instance.new("ImageButton", frame) + button2.Name = "MyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + end) + + it("should support t.instanceIsA shorthand", function() + local myInterface = t.interface({ + buttonInFrame = t.instanceIsA("Frame", { + MyButton = t.instanceIsA("ImageButton") + }) + }) + + assert(not (t.children({})(5))) + assert(not (myInterface({ buttonInFrame = Instance.new("Frame") }))) + + do + local frame = Instance.new("Frame") + local button = Instance.new("ImageButton", frame) + button.Name = "MyButton" + assert(myInterface({ buttonInFrame = frame })) + end + + do + local frame = Instance.new("Frame") + local button = Instance.new("ImageButton", frame) + button.Name = "NotMyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + + do + local frame = Instance.new("Frame") + local button = Instance.new("TextButton", frame) + button.Name = "MyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + + do + local frame = Instance.new("Frame") + local button1 = Instance.new("ImageButton", frame) + button1.Name = "MyButton" + local button2 = Instance.new("ImageButton", frame) + button2.Name = "MyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + end) + + it("should support t.match", function() + local check = t.match("%d+") + assert(check("123")) + assert(not (check("abc"))) + assert(not (check())) + end) + + it("should support t.keyOf", function() + local myNewEnum = { + OptionA = {}, + OptionB = {}, + } + local check = t.keyOf(myNewEnum) + assert(check("OptionA")) + assert(not (check("OptionC"))) + end) + + it("should support t.valueOf", function() + local myNewEnum = { + OptionA = {}, + OptionB = {}, + } + local check = t.valueOf(myNewEnum) + assert(check(myNewEnum.OptionA)) + assert(not (check(1010))) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/t.d.ts b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/t.d.ts new file mode 100644 index 0000000..b0b7d2c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/t.d.ts @@ -0,0 +1,214 @@ +/** checks to see if `value` is a T */ +type check = (value: unknown) => value is T; + +interface t { + // lua types + /** checks to see if `value` is an any */ + any: check; + /** checks to see if `value` is a boolean */ + boolean: check; + /** checks to see if `value` is a thread */ + thread: check; + /** checks to see if `value` is a Function */ + callback: check; + /** checks to see if `value` is undefined */ + none: check; + /** checks to see if `value` is a number, will _not_ match NaN */ + number: check; + /** checks to see if `value` is NaN */ + nan: check; + /** checks to see if `value` is a string */ + string: check; + /** checks to see if `value` is an object */ + table: check; + /** checks to see if `value` is a userdata */ + userdata: check; + + // roblox types + /** checks to see if `value` is an Axes */ + Axes: check; + /** checks to see if `value` is a BrickColor */ + BrickColor: check; + /** checks to see if `value` is a CFrame */ + CFrame: check; + /** checks to see if `value` is a Color3 */ + Color3: check; + /** checks to see if `value` is a ColorSequence */ + ColorSequence: check; + /** checks to see if `value` is a ColorSequenceKeypoint */ + ColorSequenceKeypoint: check; + /** checks to see if `value` is a DockWidgetPluginGuiInfo */ + DockWidgetPluginGuiInfo: check; + /** checks to see if `value` is a Faces */ + Faces: check; + /** checks to see if `value` is an Instance */ + Instance: check; + /** checks to see if `value` is a NumberRange */ + NumberRange: check; + /** checks to see if `value` is a NumberSequence */ + NumberSequence: check; + /** checks to see if `value` is a NumberSequenceKeypoint */ + NumberSequenceKeypoint: check; + /** checks to see if `value` is a PathWaypoint */ + PathWaypoint: check; + /** checks to see if `value` is a PhysicalProperties */ + PhysicalProperties: check; + /** checks to see if `value` is a Random */ + Random: check; + /** checks to see if `value` is a Ray */ + Ray: check; + /** checks to see if `value` is a Rect */ + Rect: check; + /** checks to see if `value` is a Region3 */ + Region3: check; + /** checks to see if `value` is a Region3int16 */ + Region3int16: check; + /** checks to see if `value` is a TweenInfo */ + TweenInfo: check; + /** checks to see if `value` is a UDim */ + UDim: check; + /** checks to see if `value` is a UDim2 */ + UDim2: check; + /** checks to see if `value` is a Vector2 */ + Vector2: check; + /** checks to see if `value` is a Vector3 */ + Vector3: check; + /** checks to see if `value` is a Vector3int16 */ + Vector3int16: check; + /** checks to see if `value` is a RBXScriptSignal */ + RBXScriptSignal: check; + /** checks to see if `value` is a RBXScriptConnection */ + RBXScriptConnection: check; + + /** + * checks to see if `value == literalValue` + */ + literal(this: void, literalValue: T): check; + literal>( + this: void, + ...args: T + ): T extends [infer A] + ? (value: unknown) => value is A + : T extends [infer A, infer B] + ? check + : T extends [infer A, infer B, infer C] + ? check + : T extends [infer A, infer B, infer C, infer D] + ? check + : T extends [infer A, infer B, infer C, infer D, infer E] + ? check + : T extends [infer A, infer B, infer C, infer D, infer E, infer F] + ? check + : never; + literal(this: void, literalValue: T): (value: unknown) => value is T; + + /** Returns a t.union of each key in the table as a t.literal */ + keyOf: (valueTable: T) => check; + + /** Returns a t.union of each value in the table as a t.literal */ + valueOf: (valueTable: T) => T extends { [P in keyof T]: infer U } ? check : never; + + /** checks to see if `value` is an integer */ + integer: (value: unknown) => value is number; + /** checks to see if `value` is a number and is more than or equal to `min` */ + numberMin: (min: number) => (value: unknown) => value is number; + /** checks to see if `value` is a number and is less than or equal to `max` */ + numberMax: (max: number) => (value: unknown) => value is number; + /** checks to see if `value` is a number and is more than `min` */ + numberMinExclusive: (min: number) => (value: unknown) => value is number; + /** checks to see if `value` is a number and is less than `max` */ + numberMaxExclusive: (max: number) => (value: unknown) => value is number; + /** checks to see if `value` is a number and is more than 0 */ + numberPositive: (value: unknown) => value is number; + /** checks to see if `value` is a number and is less than 0 */ + numberNegative: (value: unknown) => value is number; + /** checks to see if `value` is a number and `min <= value <= max` */ + numberConstrained: (min: number, max: number) => (value: unknown) => value is number; + /** checks to see if `value` is a number and `min < value < max` */ + numberConstrainedExclusive: (min: number, max: number) => (value: unknown) => value is number; + /** checks `t.string` and determines if value matches the pattern via `string.match(value, pattern)` */ + match: (pattern: string) => check; + /** checks to see if `value` is either nil or passes `check` */ + optional: (check: (value: unknown) => value is T) => check; + /** checks to see if `value` is a table and if its keys match against `check */ + keys: (check: (value: unknown) => value is T) => check>; + /** checks to see if `value` is a table and if its values match against `check` */ + values: (check: (value: unknown) => value is T) => check>; + /** checks to see if `value` is a table and all of its keys match against `keyCheck` and all of its values match against `valueCheck` */ + map: ( + keyCheck: (value: unknown) => value is K, + valueCheck: (value: unknown) => value is V + ) => check>; + /** checks to see if `value` is an array and all of its keys are sequential integers and all of its values match `check` */ + array: (check: (value: unknown) => value is T) => check>; + + /** checks to see if `value` matches any given check */ + union: >( + ...args: T + ) => T extends [check] + ? (value: unknown) => value is A + : T extends [check, check] + ? check + : T extends [check, check, check] + ? check + : T extends [check, check, check, check] + ? check + : T extends [check, check, check, check, check] + ? check + : T extends [check, check, check, check, check, check] + ? check + : never; + + /** checks to see if `value` matches all given checks */ + intersection: >( + ...args: T + ) => T extends [check] + ? (value: unknown) => value is A + : T extends [check, check] + ? check + : T extends [check, check, check] + ? check + : T extends [check, check, check, check] + ? check + : T extends [check, check, check, check, check] + ? check + : T extends [check, check, check, check, check, check] + ? check + : never; + + /** checks to see if `value` matches a given interface definition */ + interface: value is any }>( + checkTable: T + ) => check<{ [P in keyof T]: t.static }>; + + /** checks to see if `value` matches a given interface definition with no extra members */ + strictInterface: value is any }>( + checkTable: T + ) => check<{ [P in keyof T]: t.static }>; + + instanceOf(this: void, className: S): check; + instanceOf value is any }>( + this: void, + className: S, + checkTable: T + ): check }>; + + instanceIsA(this: void, className: S): check; + instanceIsA value is any }>( + this: void, + className: S, + checkTable: T + ): check }>; + + children: value is any }>( + checkTable: T + ) => check }>; +} + +declare namespace t { + /** creates a static type from a t-defined type */ + export type static = T extends check ? U : never; +} + +declare const t: t; +export = t; diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/ts.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/ts.lua new file mode 100644 index 0000000..7e7df9b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/ts.lua @@ -0,0 +1,554 @@ +-- t: a runtime typechecker for Roblox + +-- regular lua compatibility +local typeof = typeof or type + +local function primitive(typeName) + return function(value) + local valueType = typeof(value) + if valueType == typeName then + return true + else + return false + end + end +end + +local t = {} + +function t.any(value) + if value ~= nil then + return true + else + return false + end +end + +--Lua primitives + +t.boolean = primitive("boolean") +t.thread = primitive("thread") +t.callback = primitive("function") +t.none = primitive("nil") +t.string = primitive("string") +t.table = primitive("table") +t.userdata = primitive("userdata") + +function t.number(value) + local valueType = typeof(value) + if valueType == "number" then + if value == value then + return true + else + return false + end + else + return false + end +end + +function t.nan(value) + if value ~= value then + return true + else + return false + end +end + +-- roblox types + +t.Axes = primitive("Axes") +t.BrickColor = primitive("BrickColor") +t.CFrame = primitive("CFrame") +t.Color3 = primitive("Color3") +t.ColorSequence = primitive("ColorSequence") +t.ColorSequenceKeypoint = primitive("ColorSequenceKeypoint") +t.DockWidgetPluginGuiInfo = primitive("DockWidgetPluginGuiInfo") +t.Faces = primitive("Faces") +t.Instance = primitive("Instance") +t.NumberRange = primitive("NumberRange") +t.NumberSequence = primitive("NumberSequence") +t.NumberSequenceKeypoint = primitive("NumberSequenceKeypoint") +t.PathWaypoint = primitive("PathWaypoint") +t.PhysicalProperties = primitive("PhysicalProperties") +t.Random = primitive("Random") +t.Ray = primitive("Ray") +t.Rect = primitive("Rect") +t.Region3 = primitive("Region3") +t.Region3int16 = primitive("Region3int16") +t.TweenInfo = primitive("TweenInfo") +t.UDim = primitive("UDim") +t.UDim2 = primitive("UDim2") +t.Vector2 = primitive("Vector2") +t.Vector3 = primitive("Vector3") +t.Vector3int16 = primitive("Vector3int16") +t.Enum = primitive("Enum") +t.EnumItem = primitive("EnumItem") +t.RBXScriptSignal = primitive("RBXScriptSignal") +t.RBXScriptConnection = primitive("RBXScriptConnection") + +function t.literal(...) + local size = select("#", ...) + if size == 1 then + local literal = ... + return function(value) + if value ~= literal then + return false + end + return true + end + else + local literals = {} + for i = 1, size do + local value = select(i, ...) + literals[i] = t.literal(value) + end + return t.union(unpack(literals)) + end +end + +t.exactly = t.literal + +function t.keyOf(keyTable) + local keys = {} + for key in pairs(keyTable) do + keys[#keys + 1] = key + end + return t.literal(unpack(keys)) +end + +function t.valueOf(valueTable) + local values = {} + for _, value in pairs(valueTable) do + values[#values + 1] = value + end + return t.literal(unpack(values)) +end + +function t.integer(value) + local success = t.number(value) + if not success then + return false + end + if value%1 == 0 then + return true + else + return false + end +end + +function t.numberMin(min) + return function(value) + local success = t.number(value) + if not success then + return false + end + if value >= min then + return true + else + return false + end + end +end + +function t.numberMax(max) + return function(value) + local success = t.number(value) + if not success then + return false + end + if value <= max then + return true + else + return false + end + end +end + +function t.numberMinExclusive(min) + return function(value) + local success = t.number(value) + if not success then + return false + end + if min < value then + return true + else + return false + end + end +end + +function t.numberMaxExclusive(max) + return function(value) + local success = t.number(value) + if not success then + return false + end + if value < max then + return true + else + return false + end + end +end + +t.numberPositive = t.numberMinExclusive(0) +t.numberNegative = t.numberMaxExclusive(0) + +function t.numberConstrained(min, max) + assert(t.number(min) and t.number(max)) + local minCheck = t.numberMin(min) + local maxCheck = t.numberMax(max) + return function(value) + local minSuccess = minCheck(value) + if not minSuccess then + return false + end + + local maxSuccess = maxCheck(value) + if not maxSuccess then + return false + end + + return true + end +end + +function t.numberConstrainedExclusive(min, max) + assert(t.number(min) and t.number(max)) + local minCheck = t.numberMinExclusive(min) + local maxCheck = t.numberMaxExclusive(max) + return function(value) + local minSuccess = minCheck(value) + if not minSuccess then + return false + end + + local maxSuccess = maxCheck(value) + if not maxSuccess then + return false + end + + return true + end +end + +function t.match(pattern) + assert(t.string(pattern)) + return function(value) + local stringSuccess = t.string(value) + if not stringSuccess then + return false + end + + if string.match(value, pattern) == nil then + return false + end + + return true + end +end + +function t.optional(check) + assert(t.callback(check)) + return function(value) + if value == nil then + return true + end + local success = check(value) + if success then + return true + else + return false + end + end +end + +function t.tuple(...) + local checks = {...} + return function(...) + local args = {...} + for i = 1, #checks do + local success = checks[i](args[i]) + if success == false then + return false + end + end + return true + end +end + +function t.keys(check) + assert(t.callback(check)) + return function(value) + local tableSuccess = t.table(value) + if tableSuccess == false then + return false + end + + for key in pairs(value) do + local success = check(key) + if success == false then + return false + end + end + + return true + end +end + +function t.values(check) + assert(t.callback(check)) + return function(value) + local tableSuccess = t.table(value) + if tableSuccess == false then + return false + end + + for _, val in pairs(value) do + local success = check(val) + if success == false then + return false + end + end + + return true + end +end + +function t.map(keyCheck, valueCheck) + assert(t.callback(keyCheck), t.callback(valueCheck)) + local keyChecker = t.keys(keyCheck) + local valueChecker = t.values(valueCheck) + return function(value) + local keySuccess = keyChecker(value) + if not keySuccess then + return false + end + + local valueSuccess = valueChecker(value) + if not valueSuccess then + return false + end + + return true + end +end + +do + local arrayKeysCheck = t.keys(t.integer) + + function t.array(check) + assert(t.callback(check)) + local valuesCheck = t.values(check) + return function(value) + local keySuccess = arrayKeysCheck(value) + if keySuccess == false then + return false + end + + -- # is unreliable for sparse arrays + -- Count upwards using ipairs to avoid false positives from the behavior of # + local arraySize = 0 + + for _, _ in ipairs(value) do + arraySize = arraySize + 1 + end + + for key in pairs(value) do + if key < 1 or key > arraySize then + return false + end + end + + local valueSuccess = valuesCheck(value) + if not valueSuccess then + return false + end + + return true + end + end +end + +do + local callbackArray = t.array(t.callback) + + function t.union(...) + local checks = {...} + assert(callbackArray(checks)) + return function(value) + for _, check in pairs(checks) do + if check(value) then + return true + end + end + return false + end + end + + function t.intersection(...) + local checks = {...} + assert(callbackArray(checks)) + return function(value) + for _, check in pairs(checks) do + local success = check(value) + if not success then + return false + end + end + return true + end + end +end + +do + local checkInterface = t.map(t.any, t.callback) + + function t.interface(checkTable) + assert(checkInterface(checkTable)) + return function(value) + local tableSuccess = t.table(value) + if tableSuccess == false then + return false + end + + for key, check in pairs(checkTable) do + local success = check(value[key]) + if success == false then + return false + end + end + return true + end + end + + function t.strictInterface(checkTable) + assert(checkInterface(checkTable)) + return function(value) + local tableSuccess = t.table(value) + if tableSuccess == false then + return false + end + + for key, check in pairs(checkTable) do + local success = check(value[key]) + if success == false then + return false + end + end + + for key in pairs(value) do + if not checkTable[key] then + return false + end + end + + return true + end + end +end + +function t.instanceOf(className) + assert(t.string(className)) + return function(value) + local instanceSuccess = t.Instance(value) + if not instanceSuccess then + return false + end + + if value.ClassName ~= className then + return false + end + + return true + end +end +t.instance = t.instanceOf + +function t.instanceIsA(className) + assert(t.string(className)) + return function(value) + local instanceSuccess = t.Instance(value) + if not instanceSuccess then + return false + end + + if not value:IsA(className) then + return false + end + + return true + end +end + +function t.enum(enum) + assert(t.Enum(enum)) + return function(value) + local enumItemSuccess = t.EnumItem(value) + if not enumItemSuccess then + return false + end + + if value.EnumType == enum then + return true + else + return false + end + end +end + +do + local checkWrap = t.tuple(t.callback, t.callback) + + function t.wrap(callback, checkArgs) + assert(checkWrap(callback, checkArgs)) + return function(...) + assert(checkArgs(...)) + return callback(...) + end + end +end + +function t.strict(check) + return function(...) + assert(check(...)) + end +end + +do + local checkChildren = t.map(t.string, t.callback) + function t.children(checkTable) + assert(checkChildren(checkTable)) + + return function(value) + local instanceSuccess = t.Instance(value) + if not instanceSuccess then + return false + end + + local childrenByName = {} + for _, child in pairs(value:GetChildren()) do + local name = child.Name + if checkTable[name] then + if childrenByName[name] then + return false + end + childrenByName[name] = child + end + end + + for name, check in pairs(checkTable) do + local success = check(childrenByName[name]) + if not success then + return false + end + end + + return true + end + end +end + +return t \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/lock.toml new file mode 100644 index 0000000..bc7efec --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/testez" +version = "0.3.1" +commit = "42f0b3772c90ed9c0cff187fb56d508aca7b6641" +source = "url+https://github.com/roblox/testez" diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Context.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Context.lua new file mode 100644 index 0000000..efd4993 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Context.lua @@ -0,0 +1,26 @@ +--[[ + The Context object implements a write-once key-value store. It also allows + for a new Context object to inherit the entries from an existing one. +]] +local Context = {} + +function Context.new(parent) + local meta = {} + local index = {} + meta.__index = index + + if parent then + for key, value in pairs(getmetatable(parent).__index) do + index[key] = value + end + end + + function meta.__newindex(_obj, key, value) + assert(index[key] == nil, string.format("Cannot reassign %s in context", tostring(key))) + index[key] = value + end + + return setmetatable({}, meta) +end + +return Context diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Expectation.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Expectation.lua new file mode 100644 index 0000000..675c33a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Expectation.lua @@ -0,0 +1,252 @@ +--[[ + Allows creation of expectation statements designed for behavior-driven + testing (BDD). See Chai (JS) or RSpec (Ruby) for examples of other BDD + frameworks. + + The Expectation class is exposed to tests as a function called `expect`: + + expect(5).to.equal(5) + expect(foo()).to.be.ok() + + Expectations can be negated using .never: + + expect(true).never.to.equal(false) + + Expectations throw errors when their conditions are not met. +]] + +local Expectation = {} + +--[[ + These keys don't do anything except make expectations read more cleanly +]] +local SELF_KEYS = { + to = true, + be = true, + been = true, + have = true, + was = true, + at = true, +} + +--[[ + These keys invert the condition expressed by the Expectation. +]] +local NEGATION_KEYS = { + never = true, +} + +--[[ + Extension of Lua's 'assert' that lets you specify an error level. +]] +local function assertLevel(condition, message, level) + message = message or "Assertion failed!" + level = level or 1 + + if not condition then + error(message, level + 1) + end +end + +--[[ + Returns a version of the given method that can be called with either . or : +]] +local function bindSelf(self, method) + return function(firstArg, ...) + if firstArg == self then + return method(self, ...) + else + return method(self, firstArg, ...) + end + end +end + +local function formatMessage(result, trueMessage, falseMessage) + if result then + return trueMessage + else + return falseMessage + end +end + +--[[ + Create a new expectation +]] +function Expectation.new(value) + local self = { + value = value, + successCondition = true, + condition = false + } + + setmetatable(self, Expectation) + + self.a = bindSelf(self, self.a) + self.an = self.a + self.ok = bindSelf(self, self.ok) + self.equal = bindSelf(self, self.equal) + self.throw = bindSelf(self, self.throw) + self.near = bindSelf(self, self.near) + + return self +end + +function Expectation.__index(self, key) + -- Keys that don't do anything except improve readability + if SELF_KEYS[key] then + return self + end + + -- Invert your assertion + if NEGATION_KEYS[key] then + local newExpectation = Expectation.new(self.value) + newExpectation.successCondition = not self.successCondition + + return newExpectation + end + + -- Fall back to methods provided by Expectation + return Expectation[key] +end + +--[[ + Called by expectation terminators to reset modifiers in a statement. + + This makes chains like: + + expect(5) + .never.to.equal(6) + .to.equal(5) + + Work as expected. +]] +function Expectation:_resetModifiers() + self.successCondition = true +end + +--[[ + Assert that the expectation value is the given type. + + expect(5).to.be.a("number") +]] +function Expectation:a(typeName) + local result = (type(self.value) == typeName) == self.successCondition + + local message = formatMessage(self.successCondition, + ("Expected value of type %q, got value %q of type %s"):format( + typeName, + tostring(self.value), + type(self.value) + ), + ("Expected value not of type %q, got value %q of type %s"):format( + typeName, + tostring(self.value), + type(self.value) + ) + ) + + assertLevel(result, message, 3) + self:_resetModifiers() + + return self +end + +--[[ + Assert that our expectation value is truthy +]] +function Expectation:ok() + local result = (self.value ~= nil) == self.successCondition + + local message = formatMessage(self.successCondition, + ("Expected value %q to be non-nil"):format( + tostring(self.value) + ), + ("Expected value %q to be nil"):format( + tostring(self.value) + ) + ) + + assertLevel(result, message, 3) + self:_resetModifiers() + + return self +end + +--[[ + Assert that our expectation value is equal to another value +]] +function Expectation:equal(otherValue) + local result = (self.value == otherValue) == self.successCondition + + local message = formatMessage(self.successCondition, + ("Expected value %q (%s), got %q (%s) instead"):format( + tostring(otherValue), + type(otherValue), + tostring(self.value), + type(self.value) + ), + ("Expected anything but value %q (%s)"):format( + tostring(otherValue), + type(otherValue) + ) + ) + + assertLevel(result, message, 3) + self:_resetModifiers() + + return self +end + +--[[ + Assert that our expectation value is equal to another value within some + inclusive limit. +]] +function Expectation:near(otherValue, limit) + assert(type(self.value) == "number", "Expectation value must be a number to use 'near'") + assert(type(otherValue) == "number", "otherValue must be a number") + assert(type(limit) == "number" or limit == nil, "limit must be a number or nil") + + limit = limit or 1e-7 + + local result = (math.abs(self.value - otherValue) <= limit) == self.successCondition + + local message = formatMessage(self.successCondition, + ("Expected value to be near %f (within %f) but got %f instead"):format( + otherValue, + limit, + self.value + ), + ("Expected value to not be near %f (within %f) but got %f instead"):format( + otherValue, + limit, + self.value + ) + ) + + assertLevel(result, message, 3) + self:_resetModifiers() + + return self +end + +--[[ + Assert that our functoid expectation value throws an error when called +]] +function Expectation:throw() + local ok, err = pcall(self.value) + local result = ok ~= self.successCondition + + local message = formatMessage(self.successCondition, + "Expected function to throw an error, but it did not.", + ("Expected function to succeed, but it threw an error: %s"):format( + tostring(err) + ) + ) + + assertLevel(result, message, 3) + self:_resetModifiers() + + return self +end + +return Expectation \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/LifecycleHooks.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/LifecycleHooks.lua new file mode 100644 index 0000000..c60b497 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/LifecycleHooks.lua @@ -0,0 +1,89 @@ +local TestEnum = require(script.Parent.TestEnum) + +local LifecycleHooks = {} +LifecycleHooks.__index = LifecycleHooks + +function LifecycleHooks.new() + local self = { + _stack = {}, + } + return setmetatable(self, LifecycleHooks) +end + +--[[ + Returns an array of `beforeEach` hooks in FIFO order +]] +function LifecycleHooks:getBeforeEachHooks() + local key = TestEnum.NodeType.BeforeEach + local hooks = {} + + for _, level in ipairs(self._stack) do + for _, hook in ipairs(level[key]) do + table.insert(hooks, hook) + end + end + + return hooks +end + +--[[ + Returns an array of `afterEach` hooks in FILO order +]] +function LifecycleHooks:getAfterEachHooks() + local key = TestEnum.NodeType.AfterEach + local hooks = {} + + for _, level in ipairs(self._stack) do + for _, hook in ipairs(level[key]) do + table.insert(hooks, 1, hook) + end + end + + return hooks +end + +--[[ + Pushes uncalled beforeAll and afterAll hooks back up the stack +]] +function LifecycleHooks:popHooks() + table.remove(self._stack, #self._stack) +end + +function LifecycleHooks:pushHooksFrom(planNode) + assert(planNode ~= nil) + + table.insert(self._stack, { + [TestEnum.NodeType.BeforeAll] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.BeforeAll), + [TestEnum.NodeType.AfterAll] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.AfterAll), + [TestEnum.NodeType.BeforeEach] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.BeforeEach), + [TestEnum.NodeType.AfterEach] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.AfterEach), + }) +end + +--[[ + Get the beforeAll hooks from the current level. +]] +function LifecycleHooks:getBeforeAllHooks() + return self._stack[#self._stack][TestEnum.NodeType.BeforeAll] +end + +--[[ + Get the afterAll hooks from the current level. +]] +function LifecycleHooks:getAfterAllHooks() + return self._stack[#self._stack][TestEnum.NodeType.AfterAll] +end + +function LifecycleHooks:_getHooksOfType(nodes, key) + local hooks = {} + + for _, node in ipairs(nodes) do + if node.type == key then + table.insert(hooks, node.callback) + end + end + + return hooks +end + +return LifecycleHooks diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TeamCityReporter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TeamCityReporter.lua new file mode 100644 index 0000000..bab37e5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TeamCityReporter.lua @@ -0,0 +1,102 @@ +local TestService = game:GetService("TestService") + +local TestEnum = require(script.Parent.Parent.TestEnum) + +local TeamCityReporter = {} + +local function teamCityEscape(str) + str = string.gsub(str, "([]|'[])","|%1") + str = string.gsub(str, "\r", "|r") + str = string.gsub(str, "\n", "|n") + return str +end + +local function teamCityEnterSuite(suiteName) + return string.format("##teamcity[testSuiteStarted name='%s']", teamCityEscape(suiteName)) +end + +local function teamCityLeaveSuite(suiteName) + return string.format("##teamcity[testSuiteFinished name='%s']", teamCityEscape(suiteName)) +end + +local function teamCityEnterCase(caseName) + return string.format("##teamcity[testStarted name='%s']", teamCityEscape(caseName)) +end + +local function teamCityLeaveCase(caseName) + return string.format("##teamcity[testFinished name='%s']", teamCityEscape(caseName)) +end + +local function teamCityFailCase(caseName, errorMessage) + return string.format("##teamcity[testFailed name='%s' message='%s']", + teamCityEscape(caseName), teamCityEscape(errorMessage)) +end + +local function reportNode(node, buffer, level) + buffer = buffer or {} + level = level or 0 + if node.status == TestEnum.TestStatus.Skipped then + return buffer + end + if node.planNode.type == TestEnum.NodeType.Describe then + table.insert(buffer, teamCityEnterSuite(node.planNode.phrase)) + for _, child in ipairs(node.children) do + reportNode(child, buffer, level + 1) + end + table.insert(buffer, teamCityLeaveSuite(node.planNode.phrase)) + else + table.insert(buffer, teamCityEnterCase(node.planNode.phrase)) + if node.status == TestEnum.TestStatus.Failure then + table.insert(buffer, teamCityFailCase(node.planNode.phrase, table.concat(node.errors,"\n"))) + end + table.insert(buffer, teamCityLeaveCase(node.planNode.phrase)) + end +end + +local function reportRoot(node) + local buffer = {} + + for _, child in ipairs(node.children) do + reportNode(child, buffer, 0) + end + + return buffer +end + +local function report(root) + local buffer = reportRoot(root) + + return table.concat(buffer, "\n") +end + +function TeamCityReporter.report(results) + local resultBuffer = { + "Test results:", + report(results), + ("%d passed, %d failed, %d skipped"):format( + results.successCount, + results.failureCount, + results.skippedCount + ) + } + + print(table.concat(resultBuffer, "\n")) + + if results.failureCount > 0 then + print(("%d test nodes reported failures."):format(results.failureCount)) + end + + if #results.errors > 0 then + print("Errors reported by tests:") + print("") + + for _, message in ipairs(results.errors) do + TestService:Error(message) + + -- Insert a blank line after each error + print("") + end + end +end + +return TeamCityReporter \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TextReporter.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TextReporter.lua new file mode 100644 index 0000000..e40d858 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TextReporter.lua @@ -0,0 +1,106 @@ +--[[ + The TextReporter uses the results from a completed test to output text to + standard output and TestService. +]] + +local TestService = game:GetService("TestService") + +local TestEnum = require(script.Parent.Parent.TestEnum) + +local INDENT = (" "):rep(3) +local STATUS_SYMBOLS = { + [TestEnum.TestStatus.Success] = "+", + [TestEnum.TestStatus.Failure] = "-", + [TestEnum.TestStatus.Skipped] = "~" +} +local UNKNOWN_STATUS_SYMBOL = "?" + +local TextReporter = {} + +local function compareNodes(a, b) + return a.planNode.phrase:lower() < b.planNode.phrase:lower() +end + +local function reportNode(node, buffer, level) + buffer = buffer or {} + level = level or 0 + + if node.status == TestEnum.TestStatus.Skipped then + return buffer + end + + local line + + if node.status then + local symbol = STATUS_SYMBOLS[node.status] or UNKNOWN_STATUS_SYMBOL + + line = ("%s[%s] %s"):format( + INDENT:rep(level), + symbol, + node.planNode.phrase + ) + else + line = ("%s%s"):format( + INDENT:rep(level), + node.planNode.phrase + ) + end + + table.insert(buffer, line) + table.sort(node.children, compareNodes) + + for _, child in ipairs(node.children) do + reportNode(child, buffer, level + 1) + end + + return buffer +end + +local function reportRoot(node) + local buffer = {} + table.sort(node.children, compareNodes) + + for _, child in ipairs(node.children) do + reportNode(child, buffer, 0) + end + + return buffer +end + +local function report(root) + local buffer = reportRoot(root) + + return table.concat(buffer, "\n") +end + +function TextReporter.report(results) + local resultBuffer = { + "Test results:", + report(results), + ("%d passed, %d failed, %d skipped"):format( + results.successCount, + results.failureCount, + results.skippedCount + ) + } + + print(table.concat(resultBuffer, "\n")) + + if results.failureCount > 0 then + print(("%d test nodes reported failures."):format(results.failureCount)) + end + + if #results.errors > 0 then + print("Errors reported by tests:") + print("") + + for _, message in ipairs(results.errors) do + TestService:Error(message) + + -- Insert a blank line after each error + print("") + end + end +end + +return TextReporter \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TextReporterQuiet.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TextReporterQuiet.lua new file mode 100644 index 0000000..cbbb1b4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TextReporterQuiet.lua @@ -0,0 +1,97 @@ +--[[ + Copy of TextReporter that doesn't output successful tests. + + This should be temporary, it's just a workaround to make CI environments + happy in the short-term. +]] + +local TestService = game:GetService("TestService") + +local TestEnum = require(script.Parent.Parent.TestEnum) + +local INDENT = (" "):rep(3) +local STATUS_SYMBOLS = { + [TestEnum.TestStatus.Success] = "+", + [TestEnum.TestStatus.Failure] = "-", + [TestEnum.TestStatus.Skipped] = "~" +} +local UNKNOWN_STATUS_SYMBOL = "?" + +local TextReporterQuiet = {} + +local function reportNode(node, buffer, level) + buffer = buffer or {} + level = level or 0 + + if node.status == TestEnum.TestStatus.Skipped then + return buffer + end + + local line + + if node.status ~= TestEnum.TestStatus.Success then + local symbol = STATUS_SYMBOLS[node.status] or UNKNOWN_STATUS_SYMBOL + + line = ("%s[%s] %s"):format( + INDENT:rep(level), + symbol, + node.planNode.phrase + ) + end + + table.insert(buffer, line) + + for _, child in ipairs(node.children) do + reportNode(child, buffer, level + 1) + end + + return buffer +end + +local function reportRoot(node) + local buffer = {} + + for _, child in ipairs(node.children) do + reportNode(child, buffer, 0) + end + + return buffer +end + +local function report(root) + local buffer = reportRoot(root) + + return table.concat(buffer, "\n") +end + +function TextReporterQuiet.report(results) + local resultBuffer = { + "Test results:", + report(results), + ("%d passed, %d failed, %d skipped"):format( + results.successCount, + results.failureCount, + results.skippedCount + ) + } + + print(table.concat(resultBuffer, "\n")) + + if results.failureCount > 0 then + print(("%d test nodes reported failures."):format(results.failureCount)) + end + + if #results.errors > 0 then + print("Errors reported by tests:") + print("") + + for _, message in ipairs(results.errors) do + TestService:Error(message) + + -- Insert a blank line after each error + print("") + end + end +end + +return TextReporterQuiet \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestBootstrap.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestBootstrap.lua new file mode 100644 index 0000000..e3641a5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestBootstrap.lua @@ -0,0 +1,147 @@ +--[[ + Provides an interface to quickly run and report tests from a given object. +]] + +local TestPlanner = require(script.Parent.TestPlanner) +local TestRunner = require(script.Parent.TestRunner) +local TextReporter = require(script.Parent.Reporters.TextReporter) + +local TestBootstrap = {} + +local function stripSpecSuffix(name) + return (name:gsub("%.spec$", "")) +end +local function isSpecScript(aScript) + return aScript:IsA("ModuleScript") and aScript.Name:match("%.spec$") +end + +local function getPath(module, root) + root = root or game + + local path = {} + local last = module + + if last.Name == "init.spec" then + -- Use the directory's node for init.spec files. + last = last.Parent + end + + while last ~= nil and last ~= root do + table.insert(path, stripSpecSuffix(last.Name)) + last = last.Parent + end + table.insert(path, stripSpecSuffix(root.Name)) + + return path +end + +local function toStringPath(tablePath) + local stringPath = "" + local first = true + for _, element in ipairs(tablePath) do + if first then + stringPath = element + first = false + else + stringPath = element .. " " .. stringPath + end + end + return stringPath +end + +function TestBootstrap:getModulesImpl(root, modules, current) + modules = modules or {} + current = current or root + + if isSpecScript(current) then + local method = require(current) + local path = getPath(current, root) + local pathString = toStringPath(path) + + table.insert(modules, { + method = method, + path = path, + pathStringForSorting = pathString:lower() + }) + end +end + +--[[ + Find all the ModuleScripts in this tree that are tests. +]] +function TestBootstrap:getModules(root) + local modules = {} + + self:getModulesImpl(root, modules) + + for _, child in ipairs(root:GetDescendants()) do + self:getModulesImpl(root, modules, child) + end + + return modules +end + +--[[ + Runs all test and reports the results using the given test reporter. + + If no reporter is specified, a reasonable default is provided. + + This function demonstrates the expected workflow with this testing system: + 1. Locate test modules + 2. Generate test plan + 3. Run test plan + 4. Report test results + + This means we could hypothetically present a GUI to the developer that shows + the test plan before we execute it, allowing them to toggle specific tests + before they're run, but after they've been identified! +]] +function TestBootstrap:run(roots, reporter, otherOptions) + reporter = reporter or TextReporter + + otherOptions = otherOptions or {} + local showTimingInfo = otherOptions["showTimingInfo"] or false + local testNamePattern = otherOptions["testNamePattern"] + local extraEnvironment = otherOptions["extraEnvironment"] or {} + + if type(roots) ~= "table" then + error(("Bad argument #1 to TestBootstrap:run. Expected table, got %s"):format(typeof(roots)), 2) + end + + local startTime = tick() + + local modules = {} + for _, subRoot in ipairs(roots) do + local newModules = self:getModules(subRoot) + + for _, newModule in ipairs(newModules) do + table.insert(modules, newModule) + end + end + + local afterModules = tick() + + local plan = TestPlanner.createPlan(modules, testNamePattern, extraEnvironment) + local afterPlan = tick() + + local results = TestRunner.runPlan(plan) + local afterRun = tick() + + reporter.report(results) + local afterReport = tick() + + if showTimingInfo then + local timing = { + ("Took %f seconds to locate test modules"):format(afterModules - startTime), + ("Took %f seconds to create test plan"):format(afterPlan - afterModules), + ("Took %f seconds to run tests"):format(afterRun - afterPlan), + ("Took %f seconds to report tests"):format(afterReport - afterRun), + } + + print(table.concat(timing, "\n")) + end + + return results +end + +return TestBootstrap \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestEnum.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestEnum.lua new file mode 100644 index 0000000..d8d31b7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestEnum.lua @@ -0,0 +1,28 @@ +--[[ + Constants used throughout the testing framework. +]] + +local TestEnum = {} + +TestEnum.TestStatus = { + Success = "Success", + Failure = "Failure", + Skipped = "Skipped" +} + +TestEnum.NodeType = { + Describe = "Describe", + It = "It", + BeforeAll = "BeforeAll", + AfterAll = "AfterAll", + BeforeEach = "BeforeEach", + AfterEach = "AfterEach" +} + +TestEnum.NodeModifier = { + None = "None", + Skip = "Skip", + Focus = "Focus" +} + +return TestEnum \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestPlan.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestPlan.lua new file mode 100644 index 0000000..6529ec2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestPlan.lua @@ -0,0 +1,291 @@ +--[[ + Represents a tree of tests that have been loaded but not necessarily + executed yet. + + TestPlan objects are produced by TestPlanner. +]] + +local TestEnum = require(script.Parent.TestEnum) +local Expectation = require(script.Parent.Expectation) + +local function newEnvironment(currentNode, extraEnvironment) + local env = {} + + if extraEnvironment then + if type(extraEnvironment) ~= "table" then + error(("Bad argument #2 to newEnvironment. Expected table, got %s"):format( + typeof(extraEnvironment)), 2) + end + + for key, value in pairs(extraEnvironment) do + env[key] = value + end + end + + local function addChild(phrase, callback, nodeType, nodeModifier) + local node = currentNode:addChild(phrase, nodeType, nodeModifier) + node.callback = callback + if nodeType == TestEnum.NodeType.Describe then + node:expand() + end + return node + end + + function env.describeFOCUS(phrase, callback) + addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.Focus) + end + + function env.describeSKIP(phrase, callback) + addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.Skip) + end + + function env.describe(phrase, callback, nodeModifier) + addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.None) + end + + function env.itFOCUS(phrase, callback) + addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Focus) + end + + function env.itSKIP(phrase, callback) + addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip) + end + + function env.itFIXME(phrase, callback) + local node = addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip) + warn("FIXME: broken test", node:getFullName()) + end + + function env.it(phrase, callback, nodeModifier) + addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.None) + end + + -- Incrementing counter used to ensure that beforeAll, afterAll, beforeEach, afterEach have unique phrases + local lifecyclePhaseId = 0 + + local lifecycleHooks = { + [TestEnum.NodeType.BeforeAll] = "beforeAll", + [TestEnum.NodeType.AfterAll] = "afterAll", + [TestEnum.NodeType.BeforeEach] = "beforeEach", + [TestEnum.NodeType.AfterEach] = "afterEach" + } + + for nodeType, name in pairs(lifecycleHooks) do + env[name] = function(callback) + addChild(name .. "_" .. tostring(lifecyclePhaseId), callback, nodeType, TestEnum.NodeModifier.None) + lifecyclePhaseId = lifecyclePhaseId + 1 + end + end + + function env.FIXME(optionalMessage) + warn("FIXME: broken test", currentNode:getFullName(), optionalMessage or "") + + currentNode.modifier = TestEnum.NodeModifier.Skip + end + + function env.FOCUS() + currentNode.modifier = TestEnum.NodeModifier.Focus + end + + function env.SKIP() + currentNode.modifier = TestEnum.NodeModifier.Skip + end + + --[[ + This function is deprecated. Calling it is a no-op beyond generating a + warning. + ]] + function env.HACK_NO_XPCALL() + warn("HACK_NO_XPCALL is deprecated. It is now safe to yield in an " .. + "xpcall, so this is no longer necessary. It can be safely deleted.") + end + + env.fit = env.itFOCUS + env.xit = env.itSKIP + env.fdescribe = env.describeFOCUS + env.xdescribe = env.describeSKIP + + env.expect = Expectation.new + + return env +end + +local TestNode = {} +TestNode.__index = TestNode + +--[[ + Create a new test node. A pointer to the test plan, a phrase to describe it + and the type of node it is are required. The modifier is optional and will + be None if left blank. +]] +function TestNode.new(plan, phrase, nodeType, nodeModifier) + nodeModifier = nodeModifier or TestEnum.NodeModifier.None + + local node = { + plan = plan, + phrase = phrase, + type = nodeType, + modifier = nodeModifier, + children = {}, + callback = nil, + parent = nil, + } + + node.environment = newEnvironment(node, plan.extraEnvironment) + return setmetatable(node, TestNode) +end + +local function getModifier(name, pattern, modifier) + if pattern and (modifier == nil or modifier == TestEnum.NodeModifier.None) then + if name:match(pattern) then + return TestEnum.NodeModifier.Focus + else + return TestEnum.NodeModifier.Skip + end + end + return modifier +end + +function TestNode:addChild(phrase, nodeType, nodeModifier) + if nodeType == TestEnum.NodeType.It then + for _, child in pairs(self.children) do + if child.phrase == phrase then + error("Duplicate it block found: " .. child:getFullName()) + end + end + end + + local childName = self:getFullName() .. " " .. phrase + nodeModifier = getModifier(childName, self.plan.testNamePattern, nodeModifier) + local child = TestNode.new(self.plan, phrase, nodeType, nodeModifier) + child.parent = self + table.insert(self.children, child) + return child +end + +--[[ + Join the names of all the nodes back to the parent. +]] +function TestNode:getFullName() + if self.parent then + local parentPhrase = self.parent:getFullName() + if parentPhrase then + return parentPhrase .. " " .. self.phrase + end + end + return self.phrase +end + +--[[ + Expand a node by setting its callback environment and then calling it. Any + further it and describe calls within the callback will be added to the tree. +]] +function TestNode:expand() + local originalEnv = getfenv(self.callback) + local callbackEnv = setmetatable({}, { __index = originalEnv }) + for key, value in pairs(self.environment) do + callbackEnv[key] = value + end + setfenv(self.callback, callbackEnv) + + local success, result = xpcall(self.callback, debug.traceback) + + if not success then + self.loadError = result + end +end + +local TestPlan = {} +TestPlan.__index = TestPlan + +--[[ + Create a new, empty TestPlan. +]] +function TestPlan.new(testNamePattern, extraEnvironment) + local plan = { + children = {}, + testNamePattern = testNamePattern, + extraEnvironment = extraEnvironment, + } + + return setmetatable(plan, TestPlan) +end + +--[[ + Add a new child under the test plan's root node. +]] +function TestPlan:addChild(phrase, nodeType, nodeModifier) + nodeModifier = getModifier(phrase, self.testNamePattern, nodeModifier) + local child = TestNode.new(self, phrase, nodeType, nodeModifier) + table.insert(self.children, child) + return child +end + +--[[ + Add a new describe node with the given method as a callback. Generates or + reuses all the describe nodes along the path. +]] +function TestPlan:addRoot(path, method) + local curNode = self + for i = #path, 1, -1 do + local nextNode = nil + + for _, child in ipairs(curNode.children) do + if child.phrase == path[i] then + nextNode = child + break + end + end + + if nextNode == nil then + nextNode = curNode:addChild(path[i], TestEnum.NodeType.Describe) + end + + curNode = nextNode + end + + curNode.callback = method + curNode:expand() +end + +--[[ + Calls the given callback on all nodes in the tree, traversed depth-first. +]] +function TestPlan:visitAllNodes(callback, root, level) + root = root or self + level = level or 0 + + for _, child in ipairs(root.children) do + callback(child, level) + + self:visitAllNodes(callback, child, level + 1) + end +end + +--[[ + Visualizes the test plan in a simple format, suitable for debugging the test + plan's structure. +]] +function TestPlan:visualize() + local buffer = {} + self:visitAllNodes(function(node, level) + table.insert(buffer, (" "):rep(3 * level) .. node.phrase) + end) + return table.concat(buffer, "\n") +end + +--[[ + Gets a list of all nodes in the tree for which the given callback returns + true. +]] +function TestPlan:findNodes(callback) + local results = {} + self:visitAllNodes(function(node) + if callback(node) then + table.insert(results, node) + end + end) + return results +end + +return TestPlan diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestPlanner.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestPlanner.lua new file mode 100644 index 0000000..6612ff5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestPlanner.lua @@ -0,0 +1,40 @@ +--[[ + Turns a series of specification functions into a test plan. + + Uses a TestPlanBuilder to keep track of the state of the tree being built. +]] +local TestPlan = require(script.Parent.TestPlan) + +local TestPlanner = {} + +--[[ + Create a new TestPlan from a list of specification functions. + + These functions should call a combination of `describe` and `it` (and their + variants), which will be turned into a test plan to be executed. + + Parameters: + - modulesList - list of tables describing test modules { + method, -- specification function described above + path, -- array of parent entires, first element is the leaf that owns `method` + pathStringForSorting -- a string representation of `path`, used for sorting of the test plan + } + - testNamePattern - Only tests matching this Lua pattern string will run. Pass empty or nil to run all tests + - extraEnvironment - Lua table holding additional functions and variables to be injected into the specification + function during execution +]] +function TestPlanner.createPlan(modulesList, testNamePattern, extraEnvironment) + local plan = TestPlan.new(testNamePattern, extraEnvironment) + + table.sort(modulesList, function(a, b) + return a.pathStringForSorting < b.pathStringForSorting + end) + + for _, module in ipairs(modulesList) do + plan:addRoot(module.path, module.method) + end + + return plan +end + +return TestPlanner \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestResults.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestResults.lua new file mode 100644 index 0000000..c39c829 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestResults.lua @@ -0,0 +1,112 @@ +--[[ + Represents a tree of test results. + + Each node in the tree corresponds directly to a node in a corresponding + TestPlan, accessible via the 'planNode' field. + + TestResults objects are produced by TestRunner using TestSession as state. +]] + +local TestEnum = require(script.Parent.TestEnum) + +local STATUS_SYMBOLS = { + [TestEnum.TestStatus.Success] = "+", + [TestEnum.TestStatus.Failure] = "-", + [TestEnum.TestStatus.Skipped] = "~" +} + +local TestResults = {} + +TestResults.__index = TestResults + +--[[ + Create a new TestResults tree that's linked to the given TestPlan. +]] +function TestResults.new(plan) + local self = { + successCount = 0, + failureCount = 0, + skippedCount = 0, + planNode = plan, + children = {}, + errors = {} + } + + setmetatable(self, TestResults) + + return self +end + +--[[ + Create a new result node that can be inserted into a TestResult tree. +]] +function TestResults.createNode(planNode) + local node = { + planNode = planNode, + children = {}, + errors = {}, + status = nil + } + + return node +end + +--[[ + Visit all test result nodes, depth-first. +]] +function TestResults:visitAllNodes(callback, root) + root = root or self + + for _, child in ipairs(root.children) do + callback(child) + + self:visitAllNodes(callback, child) + end +end + +--[[ + Creates a debug visualization of the test results. +]] +function TestResults:visualize(root, level) + root = root or self + level = level or 0 + + local buffer = {} + + for _, child in ipairs(root.children) do + if child.planNode.type == TestEnum.NodeType.It then + local symbol = STATUS_SYMBOLS[child.status] or "?" + local str = ("%s[%s] %s"):format( + (" "):rep(3 * level), + symbol, + child.planNode.phrase + ) + + if child.messages and #child.messages > 0 then + str = str .. "\n " .. (" "):rep(3 * level) .. table.concat(child.messages, "\n " .. (" "):rep(3 * level)) + end + + table.insert(buffer, str) + else + local str = ("%s%s"):format( + (" "):rep(3 * level), + child.planNode.phrase or "" + ) + + if child.status then + str = str .. (" (%s)"):format(child.status) + end + + table.insert(buffer, str) + + if #child.children > 0 then + local text = self:visualize(child, level + 1) + table.insert(buffer, text) + end + end + end + + return table.concat(buffer, "\n") +end + +return TestResults \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestRunner.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestRunner.lua new file mode 100644 index 0000000..c3f4467 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestRunner.lua @@ -0,0 +1,177 @@ +--[[ + Contains the logic to run a test plan and gather test results from it. + + TestRunner accepts a TestPlan object, executes the planned tests, and + produces a TestResults object. While the tests are running, the system's + state is contained inside a TestSession object. +]] + +local Expectation = require(script.Parent.Expectation) +local TestEnum = require(script.Parent.TestEnum) +local TestSession = require(script.Parent.TestSession) +local LifecycleHooks = require(script.Parent.LifecycleHooks) + +local RUNNING_GLOBAL = "__TESTEZ_RUNNING_TEST__" + +local TestRunner = { + environment = {} +} + +function TestRunner.environment.expect(...) + return Expectation.new(...) +end + +--[[ + Runs the given TestPlan and returns a TestResults object representing the + results of the run. +]] +function TestRunner.runPlan(plan) + local session = TestSession.new(plan) + local lifecycleHooks = LifecycleHooks.new() + + local exclusiveNodes = plan:findNodes(function(node) + return node.modifier == TestEnum.NodeModifier.Focus + end) + + session.hasFocusNodes = #exclusiveNodes > 0 + + TestRunner.runPlanNode(session, plan, lifecycleHooks) + + return session:finalize() +end + +--[[ + Run the given test plan node and its descendants, using the given test + session to store all of the results. +]] +function TestRunner.runPlanNode(session, planNode, lifecycleHooks) + local function runCallback(callback, messagePrefix) + local success = true + local errorMessage + -- Any code can check RUNNING_GLOBAL to fork behavior based on + -- whether a test is running. We use this to avoid accessing + -- protected APIs; it's a workaround that will go away someday. + _G[RUNNING_GLOBAL] = true + + messagePrefix = messagePrefix or "" + + local testEnvironment = getfenv(callback) + + for key, value in pairs(TestRunner.environment) do + testEnvironment[key] = value + end + + testEnvironment.fail = function(message) + if message == nil then + message = "fail() was called." + end + + success = false + errorMessage = messagePrefix .. message .. "\n" .. debug.traceback() + end + + local context = session:getContext() + + local nodeSuccess, nodeResult = xpcall( + function() + callback(context) + end, + function(message) + return messagePrefix .. message .. "\n" .. debug.traceback() + end + ) + + -- If a node threw an error, we prefer to use that message over + -- one created by fail() if it was set. + if not nodeSuccess then + success = false + errorMessage = nodeResult + end + + _G[RUNNING_GLOBAL] = nil + + return success, errorMessage + end + + local function runNode(childPlanNode) + -- Errors can be set either via `error` propagating upwards or + -- by a test calling fail([message]). + + for _, hook in ipairs(lifecycleHooks:getBeforeEachHooks()) do + local success, errorMessage = runCallback(hook, "beforeEach hook: ") + if not success then + return false, errorMessage + end + end + + do + local success, errorMessage = runCallback(childPlanNode.callback) + if not success then + return false, errorMessage + end + end + + for _, hook in ipairs(lifecycleHooks:getAfterEachHooks()) do + local success, errorMessage = runCallback(hook, "afterEach hook: ") + if not success then + return false, errorMessage + end + end + + return true, nil + end + + lifecycleHooks:pushHooksFrom(planNode) + + local halt = false + for _, hook in ipairs(lifecycleHooks:getBeforeAllHooks()) do + local success, errorMessage = runCallback(hook, "beforeAll hook: ") + if not success then + session:addDummyError("beforeAll", errorMessage) + halt = true + end + end + + if not halt then + for _, childPlanNode in ipairs(planNode.children) do + session:pushNode(childPlanNode) + + if childPlanNode.type == TestEnum.NodeType.It then + if session:shouldSkip() then + session:setSkipped() + else + local success, errorMessage = runNode(childPlanNode) + + if success then + session:setSuccess() + else + session:setError(errorMessage) + end + end + elseif childPlanNode.type == TestEnum.NodeType.Describe then + TestRunner.runPlanNode(session, childPlanNode, lifecycleHooks) + + -- Did we have an error trying build a test plan? + if childPlanNode.loadError then + local message = "Error during planning: " .. childPlanNode.loadError + session:setError(message) + else + session:setStatusFromChildren() + end + end + + session:popNode() + end + end + + for _, hook in ipairs(lifecycleHooks:getAfterAllHooks()) do + local success, errorMessage = runCallback(hook, "afterAll hook: ") + if not success then + session:addDummyError("afterAll", errorMessage) + end + end + + lifecycleHooks:popHooks() +end + +return TestRunner \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestSession.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestSession.lua new file mode 100644 index 0000000..1094285 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestSession.lua @@ -0,0 +1,230 @@ +--[[ + Represents the state relevant while executing a test plan. + + Used by TestRunner to produce a TestResults object. + + Uses the same tree building structure as TestPlanBuilder; TestSession keeps + track of a stack of nodes that represent the current path through the tree. +]] + +local TestEnum = require(script.Parent.TestEnum) +local TestResults = require(script.Parent.TestResults) +local Context = require(script.Parent.Context) + +local TestSession = {} + +TestSession.__index = TestSession + +--[[ + Create a TestSession related to the given TestPlan. + + The resulting TestResults object will be linked to this TestPlan. +]] +function TestSession.new(plan) + local self = { + results = TestResults.new(plan), + nodeStack = {}, + contextStack = {}, + hasFocusNodes = false + } + + setmetatable(self, TestSession) + + return self +end + +--[[ + Calculate success, failure, and skipped test counts in the tree at the + current point in the execution. +]] +function TestSession:calculateTotals() + local results = self.results + + results.successCount = 0 + results.failureCount = 0 + results.skippedCount = 0 + + results:visitAllNodes(function(node) + local status = node.status + local nodeType = node.planNode.type + + if nodeType == TestEnum.NodeType.It then + if status == TestEnum.TestStatus.Success then + results.successCount = results.successCount + 1 + elseif status == TestEnum.TestStatus.Failure then + results.failureCount = results.failureCount + 1 + elseif status == TestEnum.TestStatus.Skipped then + results.skippedCount = results.skippedCount + 1 + end + end + end) +end + +--[[ + Gathers all of the errors reported by tests and puts them at the top level + of the TestResults object. +]] +function TestSession:gatherErrors() + local results = self.results + + results.errors = {} + + results:visitAllNodes(function(node) + if #node.errors > 0 then + for _, message in ipairs(node.errors) do + table.insert(results.errors, message) + end + end + end) +end + +--[[ + Calculates test totals, verifies the tree is valid, and returns results. +]] +function TestSession:finalize() + if #self.nodeStack ~= 0 then + error("Cannot finalize TestResults with nodes still on the stack!", 2) + end + + self:calculateTotals() + self:gatherErrors() + + return self.results +end + +--[[ + Create a new test result node and push it onto the navigation stack. +]] +function TestSession:pushNode(planNode) + local node = TestResults.createNode(planNode) + local lastNode = self.nodeStack[#self.nodeStack] or self.results + local lastContext = self.contextStack[#self.contextStack] + local context = Context.new(lastContext) + + table.insert(lastNode.children, node) + table.insert(self.nodeStack, node) + table.insert(self.contextStack, context) +end + +--[[ + Pops a node off of the navigation stack. +]] +function TestSession:popNode() + assert(#self.nodeStack > 0, "Tried to pop from an empty node stack!") + table.remove(self.nodeStack, #self.nodeStack) + table.remove(self.contextStack, #self.contextStack) +end + +--[[ + Gets the Context object for the current node. +]] +function TestSession:getContext() + assert(#self.contextStack > 0, "Tried to get context from an empty stack!") + return self.contextStack[#self.contextStack] +end + +--[[ + Tells whether the current test we're in should be skipped. +]] +function TestSession:shouldSkip() + -- If our test tree had any exclusive tests, then normal tests are skipped! + if self.hasFocusNodes then + for i = #self.nodeStack, 1, -1 do + local node = self.nodeStack[i] + + -- Skipped tests are still skipped + if node.planNode.modifier == TestEnum.NodeModifier.Skip then + return true + end + + -- Focused tests are the only ones that aren't skipped + if node.planNode.modifier == TestEnum.NodeModifier.Focus then + return false + end + end + + return true + else + for i = #self.nodeStack, 1, -1 do + local node = self.nodeStack[i] + + if node.planNode.modifier == TestEnum.NodeModifier.Skip then + return true + end + end + end + + return false +end + +--[[ + Set the current node's status to Success. +]] +function TestSession:setSuccess() + assert(#self.nodeStack > 0, "Attempting to set success status on empty stack") + self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Success +end + +--[[ + Set the current node's status to Skipped. +]] +function TestSession:setSkipped() + assert(#self.nodeStack > 0, "Attempting to set skipped status on empty stack") + self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Skipped +end + +--[[ + Set the current node's status to Failure and adds a message to its list of + errors. +]] +function TestSession:setError(message) + assert(#self.nodeStack > 0, "Attempting to set error status on empty stack") + local last = self.nodeStack[#self.nodeStack] + last.status = TestEnum.TestStatus.Failure + table.insert(last.errors, message) +end + +--[[ + Add a dummy child node to the current node to hold the given error. This + allows an otherwise empty describe node to report an error in a more natural + way. +]] +function TestSession:addDummyError(phrase, message) + self:pushNode({type = TestEnum.NodeType.It, phrase = phrase}) + self:setError(message) + self:popNode() + self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Failure +end + +--[[ + Set the current node's status based on that of its children. If all children + are skipped, mark it as skipped. If any are fails, mark it as failed. + Otherwise, mark it as success. +]] +function TestSession:setStatusFromChildren() + assert(#self.nodeStack > 0, "Attempting to set status from children on empty stack") + + local last = self.nodeStack[#self.nodeStack] + local status = TestEnum.TestStatus.Success + local skipped = true + + -- If all children were skipped, then we were skipped + -- If any child failed, then we failed! + for _, child in ipairs(last.children) do + if child.status ~= TestEnum.TestStatus.Skipped then + skipped = false + + if child.status == TestEnum.TestStatus.Failure then + status = TestEnum.TestStatus.Failure + end + end + end + + if skipped then + status = TestEnum.TestStatus.Skipped + end + + last.status = status +end + +return TestSession diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/init.lua new file mode 100644 index 0000000..9c702a1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/init.lua @@ -0,0 +1,40 @@ +local Expectation = require(script.Expectation) +local TestBootstrap = require(script.TestBootstrap) +local TestEnum = require(script.TestEnum) +local TestPlan = require(script.TestPlan) +local TestPlanner = require(script.TestPlanner) +local TestResults = require(script.TestResults) +local TestRunner = require(script.TestRunner) +local TestSession = require(script.TestSession) +local TextReporter = require(script.Reporters.TextReporter) +local TextReporterQuiet = require(script.Reporters.TextReporterQuiet) +local TeamCityReporter = require(script.Reporters.TeamCityReporter) + +local function run(testRoot, callback) + local modules = TestBootstrap:getModules(testRoot) + local plan = TestPlanner.createPlan(modules) + local results = TestRunner.runPlan(plan) + + callback(results) +end + +local TestEZ = { + run = run, + + Expectation = Expectation, + TestBootstrap = TestBootstrap, + TestEnum = TestEnum, + TestPlan = TestPlan, + TestPlanner = TestPlanner, + TestResults = TestResults, + TestRunner = TestRunner, + TestSession = TestSession, + + Reporters = { + TextReporter = TextReporter, + TextReporterQuiet = TextReporterQuiet, + TeamCityReporter = TeamCityReporter, + }, +} + +return TestEZ \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/StringUtilities.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/StringUtilities.lua new file mode 100644 index 0000000..d7885b1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/StringUtilities.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_string-utilities"]["string-utilities"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/lock.toml new file mode 100644 index 0000000..27a3d54 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/lock.toml @@ -0,0 +1,9 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/url-builder" +version = "1.0.1" +commit = "9ae0b3705f07b75f259ffa19e7e363ef4eb02ebe" +source = "url+https://github.com/roblox/url-builder" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "StringUtilities roblox/string-utilities 1.0.0 url+https://github.com/roblox/string-utilities", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBase.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBase.lua new file mode 100644 index 0000000..7f3df53 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBase.lua @@ -0,0 +1,93 @@ + +local ContentProvider = game:GetService("ContentProvider") + +--- base configuration ----------------------------- +local baseUrl = ContentProvider.BaseUrl +baseUrl = string.gsub(baseUrl, ".*://", "") +baseUrl = string.gsub(baseUrl, "/.*", "") +baseUrl = string.gsub(baseUrl, "^www%.", "") + +local UrlBase = {} + +--[[ + builds the base URL for an API + + name (string): base name of the API, eg: "games" + params (optional table): a configuration table with any of the following: + * proto (optional string): the URL protocol, eg "http", "ftp", defaults to "https" + * version (optional int, string): an optional version, eg "1" will append "/v1" to the URL + * path (optional string): a subpath to append to the URL, no leading slash + * secure (optional boolean): equivalent to proto = "https" + all strings can be empty (including name), output will adapt accordingly + + alternatively, version can be provided as second argument eg ("games", 1) +]] +function UrlBase.new(name, params) + assert(type(name) == "string", "UrlBase.new: `name` should be a string") + if params == nil then + params = {} + end + if type(params) == "number" or type(params) == "string" then + params = {version = params} + end + assert(type(params) == "table", "UrlBase.new: `params` should be a table") + local proto = params.proto + local version = params.version + local path = params.path + if proto == nil then + if params.secure == false then + proto = "http" + else + proto = "https" + end + end + local urlbase = proto + if #proto > 0 then + urlbase = urlbase .. "://" + end + urlbase = urlbase .. name + if #name > 0 then + urlbase = urlbase .. "." + end + urlbase = urlbase .. baseUrl + if version ~= nil and #tostring(version) > 0 then + urlbase = urlbase .. "/v" .. tostring(version) + end + if path ~= nil and #path > 0 then + urlbase = urlbase .. "/" .. path + end + return urlbase +end + +local isQQ = string.sub(baseUrl, -6) == "qq.com" + +-- from Url.lua +UrlBase.API = UrlBase.new("api") +UrlBase.APIS = UrlBase.new("apis") +UrlBase.AUTH = UrlBase.new("auth") +UrlBase.CHAT = UrlBase.new("chat") +UrlBase.FRIENDS = UrlBase.new("friends", 1) +UrlBase.ASSETGAME = UrlBase.new("assetgame") +UrlBase.GAMES = UrlBase.new("games", 1) +UrlBase.NOTIFICATION = UrlBase.new("notification", 2) +UrlBase.PRESENCE = UrlBase.new("presence", 1) +UrlBase.REALTIME = UrlBase.new("realtime") +UrlBase.WEB = UrlBase.new("web") +UrlBase.WWW = UrlBase.new("www") +UrlBase.ASD = UrlBase.new("ads", 1) +UrlBase.FOLLOWINGS = UrlBase.new("followings", 1) +UrlBase.PREMIUM = UrlBase.new("premiumfeatures", 1) +UrlBase.BLOG = "https://blog.roblox.com" +UrlBase.CORP = isQQ and "https://roblox.qq.com" or "https://corp.roblox.com" +-- from Http.lua +UrlBase.ACCOUNTSETTINGS = UrlBase.new("accountsettings") +UrlBase.BADGES = UrlBase.new("badges", 1) +UrlBase.INVENTORY = UrlBase.new("inventory", 1) +UrlBase.CATALOG = UrlBase.new("catalog", 1) +-- from AEWebApi.lua +UrlBase.AVATAR = UrlBase.new("avatar", 1) + +UrlBase.MOBILENAV = "robloxmobile://navigation" +UrlBase.APPSFLYER = "https://ro.blox.com" + +return UrlBase diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBuilder.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBuilder.lua new file mode 100644 index 0000000..126d363 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBuilder.lua @@ -0,0 +1,390 @@ +local Packages = script.Parent.Parent + +local Cryo = require(Packages.Cryo) +local StringUtilities = require(Packages.StringUtilities) +local StringTrim = StringUtilities.StringTrim +local StringSplit = StringUtilities.StringSplit +local encodeURIComponent = require(script.Parent.encodeURIComponent) +local UrlBase = require(script.Parent.UrlBase) + +local GameUrlPatterns = require(script.Parent.UrlPatterns.GameUrlPatterns) +local UserUrlPatterns = require(script.Parent.UrlPatterns.UserUrlPatterns) +local StaticUrlPatterns = require(script.Parent.UrlPatterns.StaticUrlPatterns) +local CatalogUrlPatterns = require(script.Parent.UrlPatterns.CatalogUrlPatterns) + +local UrlBuilder = {} + +--[[ + UTILITY FUNCTIONS +]] + +-- splits by separator, trims whitspaces from each part and filters out empty ones +local function splitAndTrim(input, separator, limit) + local list = StringSplit(input, separator, limit) + list = Cryo.List.map(list, function(item) + return StringTrim(item) + end) + list = Cryo.List.filter(list, function(item) + return #item > 0 + end) + return list +end + +--[[ + PATTERN VALIDATION +]] + +-- a value should be a string, number, or a table of strings and numbers only +local function validateValueType(value) + local valueType = type(value) + if valueType == "string" then + return true + elseif valueType == "number" then + return true + elseif valueType == "table" then + for _, item in ipairs(value) do + local itemType = type(item) + if itemType ~= "string" and itemType ~= "number" then + return false + end + end + return true + else + return false + end +end + +-- validate that item is either a literal (no check needed), or a valid placeholder +local function assertPlaceholder(element) + if string.sub(element, 1, 1) == "{" then + assert(string.sub(element, -1, -1) == "}", "invalid pattern: placeholder items should end with `}`") + end +end + +-- validate a pattern's path or query element +local function assertElementIsValid(element, inQuery) + assert(type(element) == "table", "invalid pattern: elements should all be tables") + if inQuery then + assert(type(element.name) == "string", "invalid pattern: element name should be a string") + assert(#element.name > 0, "invalid pattern: element name should not be empty") + end + assert( + type(element.value) == "string" or type(element.value) == "number", + "invalid pattern: element value should be a string or number" + ) + if type(element.value) == "string" then + assertPlaceholder(element.value) + end + if element.optional ~= nil then + assert(type(element.optional) == "boolean", "invalid pattern: element optional should be a boolean") + end + if element.default ~= nil then + assert( + validateValueType(element.default), + "invalid pattern: element default should be a string, number, or a table of strings and numbers only" + ) + end + if element.collect ~= nil then + assert( + element.collect == "multi" or element.collect == "csv", + "invalid pattern: element optional should be one of `multi`, `csv`" + ) + end +end + +-- validate a full pattern object, could use a validator library in the future +local function assertPatternIsValid(pattern) + assert(type(pattern.base) == "string", "invalid pattern: base should be a string") + assert(#pattern.base > 0, "invalid pattern: base should not be empty") + assert(type(pattern.path) == "table", "invalid pattern: path should be a table") + for _, element in ipairs(pattern.path) do + assertElementIsValid(element, false) + end + if pattern.query ~= nil then + assert(type(pattern.query) == "table", "invalid pattern: query should be a table") + end + if type(pattern.query) == "table" then + for _, element in ipairs(pattern.query) do + assertElementIsValid(element, true) + end + end + if pattern.hash ~= nil then + assert(type(pattern.hash) == "string", "invalid pattern: hash should be a string") + end +end + +--[[ + STRING PATTERNS +]] + +-- converts a "/" or "&" delimited string into a list or path/query elements +local function buildElementsFromString(elements, inQuery) + local separator = inQuery and "&" or "/" + local elements = splitAndTrim(elements, separator) + elements = Cryo.List.map(elements, function(element) + local elementName = nil + local elementValue = StringTrim(element) + local elementOptional = nil + local elementDefault = nil + local elementCollect = nil + if inQuery then + local queryitems = StringSplit(elementValue, "=", 2) + elementName = StringTrim(queryitems[1]) + elementValue = queryitems[2] + if string.sub(elementName, 1, 1) == "{" then + elementName = StringTrim(elementName, "{}") + elementName = StringSplit(elementName, "|", 2)[1] + if elementValue == nil then + elementValue = queryitems[1] + end + elementName = StringTrim(elementName) + end + elementCollect = "multi" + end + if elementValue ~= nil and string.find(elementValue, "^{.*}$") then + elementValue = StringTrim(elementValue, "{}") + local valueitems = StringSplit(elementValue, "|", 2) + elementValue = StringTrim(valueitems[1]) + if #valueitems > 1 then + elementOptional = true + if #(valueitems[2]) > 1 then + elementDefault = valueitems[2] + end + end + elementValue = "{" .. elementValue .. "}" + end + return { + name = elementName, + value = elementValue, + optional = elementOptional, + default = elementDefault, + collect = elementCollect, + } + end) + return elements +end + +local function buildQueryStringFromTable(elements) + local stringpattern = {} + for name, value in pairs(elements) do + table.insert(stringpattern, name .. "=" .. value) + end + return table.concat(stringpattern, "&") +end + +-- expands a pattern, replacing string parts with element tables +local function simplifyPattern(pattern) + local patternbase = pattern.base + local patternpath = pattern.path + local patternquery = pattern.query + local patternhash = pattern.hash + if patternbase ~= nil and UrlBase[string.upper(patternbase)] ~= nil then + patternbase = UrlBase[string.upper(patternbase)] + end + if type(patternpath) == "string" then + patternpath = buildElementsFromString(patternpath, false) + end + if type(patternquery) == "table" and patternquery[1] == nil then + patternquery = buildQueryStringFromTable(patternquery) + end + if type(patternquery) == "string" then + patternquery = buildElementsFromString(patternquery, true) + end + return { + base = patternbase, + path = patternpath, + query = patternquery, + hash = patternhash, + } +end + +--[[ + PATTERN RESOLUTION +]] + +local function concatValues(elementValues, inQuery) + -- suppress all empty values, avoids double slashes and empty query params + elementValues = Cryo.List.filter(elementValues, function(value) + if inQuery then + return #splitAndTrim(value, "=") > 1 + end + return #value > 0 + end) + local separator = inQuery and "&" or "/" + return table.concat(elementValues, separator) +end + +-- resolves the element to a string, from the given input table +local function resolveElement(element, input, inQuery) + if input == nil then + input = {} + end + local elementValues + if type(element.value) == "string" and string.sub(element.value, 1, 1) == "{" then + elementValues = input[string.sub(element.value, 2, -2)] + if elementValues == nil then + elementValues = element.default + end + if not element.optional then + assert(elementValues ~= nil, "UrlBuilder: missing parameter: `" .. element.value .. "`") + end + if elementValues == nil then + -- at this point optional == true, so we remove the element from output + elementValues = {} + end + assert( + validateValueType(elementValues), + "UrlBuilder: invalid parameter: `" .. element.value .. "`, " .. + "should be a string, number, or a table of strings and numbers only" + ) + else + elementValues = element.value + end + if type(elementValues) ~= "table" then + elementValues = {elementValues} + end + elementValues = Cryo.List.map(elementValues, function(value) + return encodeURIComponent(tostring(value)) + end) + if inQuery then + -- we are resolving a query parameter + if element.collect == "csv" then + elementValues = {table.concat(elementValues, "%2C")} -- "," + end + elementValues = Cryo.List.map(elementValues, function(value) + return encodeURIComponent(element.name) .. "=" .. value + end) + end + return concatValues(elementValues, inQuery) +end + +local function resolveElementList(elementList, input, inQuery) + local elementValues = Cryo.List.map(elementList or {}, function(element) + return resolveElement(element, input, inQuery) + end) + return concatValues(elementValues, inQuery) +end + +--[[ + PATTERN CONSTRUCTION +]] + +--[[ + creates a new URL builder function for the given pattern + the function can then be called with an input (table) to generate a URL + + pattern (table): pattern specification with the following: + * base (string): the domain and base path for the URL, eg "http://static.roblox.com" + * the UrlBase module exposes a list of available APIs, or can be used to properly build a new base + * path (string or table): list of path elements, one of + * (string): "/" delimited string, each element can be a literal or a "{}" enclosed placeholder + * (table): list of full path elements, see details below + * query (optional table): list of querystring elements, one of + * (string) : query string, with optional placeholders, eg "imageId={images}&size=140" + * (table): dictionary of {name = value, ...} pairs, eg {imageId = "{images}", size = 140} + * (table): list of full query elements, see details below + * hash (optional string): will be appended AS IS (no placeholders or UrlEncode), separated by "#" + + path and query elements are tables with the following (some only apply to query elements): + * name (string): *query only* the name of the query parameter + * value (string or number): value of the element, can be a literal or a "{}" enclosed placeholder + * optional (optional boolean): marks the element as optional, if placeholder value can't be found + * default (optional string): value if placeholder can't be found, implies "optional = true" + * collect (optional string): *query only* how to resolve table values, one of + * "multi": param and value will be repeated, eg "p=v1&p=v2&p=v3", this is the default + * "csv": one param, values will be concatenated, eg "p=v1,v2,v3" +]] +function UrlBuilder.new(pattern) + pattern = simplifyPattern(pattern) + assertPatternIsValid(pattern) + return function(input, expected) + local url = StringTrim(pattern.base, "/", {right = true}) + local path = resolveElementList(pattern.path, input, false) + if #path > 0 then + url = url .. "/" .. path + end + -- append slash if URL only consists of "proto://domain" + if string.match(url, "[^/]/[^/]") == nil then + url = url .. "/" + end + local query = resolveElementList(pattern.query, input, true) + if #query > 0 then + url = url .. "?" .. query + end + if pattern.hash and #pattern.hash > 0 then + url = url .. "#" .. pattern.hash + end + -- testing, for development use only + if expected then + if url ~= expected then + warn("UrlBuilder: unexpected output for pattern:") + warn("UrlBuilder: expected `" .. expected .. "`") + warn("UrlBuilder: actual `" .. url .. "`") + end + return expected + end + return url + end +end + +--[[ + creates a new URL builder function from a string pattern + + pattern format: "base:path/to/{endpoint}?param1={value1}&{param2}" +]] +function UrlBuilder.fromString(pattern) + local patternitems = StringSplit(pattern, ":", 2) + if #patternitems < 2 then + patternitems = {"", patternitems[1]} + end + local patternbase = patternitems[1] + patternitems = StringSplit(patternitems[2], "%#", 2) + local patternhash = patternitems[2] or "" + patternitems = StringSplit(patternitems[1], "%?", 2) + local patternpath = patternitems[1] or "" + local patternquery = patternitems[2] or "" + -- in case ":" was the protocol delimiter of a full url (eg http://domain/...) + if string.sub(patternpath, 1, 2) == "//" then + patternitems = StringSplit(string.sub(patternpath, 3), "/", 2) + patternbase = patternbase .. "://" .. patternitems[1] + patternpath = patternitems[2] or "/" + end + return UrlBuilder.new({ + base = StringTrim(patternbase), + path = StringTrim(patternpath), + query = StringTrim(patternquery), + hash = patternhash, + }) +end + +--[[ + CONVENIENCE SHORTHANDS +]] + +function UrlBuilder.addQueryString(url, query) + local pattern = StringTrim(url) + local queryindex = string.find(pattern, "%?") + if queryindex == nil then + pattern = pattern .. "?" + elseif queryindex < #pattern then + pattern = pattern .. "&" + end + local queryitems = Cryo.Dictionary.keys(query) + queryitems = Cryo.List.map(queryitems, function(param) + return "{" .. param .. "}" + end) + queryitems = table.concat(queryitems, "&") + pattern = pattern .. queryitems + return UrlBuilder.fromString(pattern)(query) +end + +--[[ + PATTERN REGISTRATION +]] + +UrlBuilder.game = GameUrlPatterns(UrlBuilder) +UrlBuilder.user = UserUrlPatterns(UrlBuilder) +UrlBuilder.catalog = CatalogUrlPatterns(UrlBuilder) +UrlBuilder.static = StaticUrlPatterns(UrlBuilder) + +return UrlBuilder diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBuilder.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBuilder.spec.lua new file mode 100644 index 0000000..b2e39f5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBuilder.spec.lua @@ -0,0 +1,85 @@ + +return function() + local UrlBuilder = require(script.Parent.UrlBuilder) + + describe("simple", function() + local pattern = UrlBuilder.fromString("api:game/{universeId}/thumbnail?size={pxWidth|150}") + + it("should generate proper url", function() + local url = pattern({ + universeId = "1356984689", + pxWidth = 720, + }) + expect(url).to.equal("https://api.roblox.com/game/1356984689/thumbnail?size=720") + end) + + it("table values duplicate url parts", function() + local url = pattern({ + universeId = {"1356984689", "8745654337"}, + pxWidth = {720, 320}, + }) + expect(url).to.equal("https://api.roblox.com/game/1356984689/8745654337/thumbnail?size=720&size=320") + end) + + it("empty table should remove url part", function() + local url = pattern({ + universeId = "1356984689", + pxWidth = {}, + }) + expect(url).to.equal("https://api.roblox.com/game/1356984689/thumbnail") + end) + + it("missing values should throw", function() + local function getUrl() + pattern({pxWidth = 720}) + end + expect(getUrl).to.throw() + end) + + it("missing optional values should not throw", function() + local url = pattern({universeId = "1356984689"}) + expect(url).to.equal("https://api.roblox.com/game/1356984689/thumbnail?size=150") + end) + + end) + + describe("complex", function() + local pattern = UrlBuilder.fromString(" https://roblox.com/ test/{special}/path / // {empty}/{multiple}/ x ? {num}¶m1=static&dupl={multiple}&") + + it("complex pattern/values with nultiple edge cases", function() + local url = pattern({ + special = "$pec!al", + num = 568, + multiple = {"ab/", "c d"}, + empty = "", + }) + expect(url).to.equal("https://roblox.com/test/%24pec!al/path/ab%2F/c%20d/x?num=568¶m1=static&dupl=ab%2F&dupl=c%20d") + end) + end) + + describe("invalid patterns", function() + + it("should throw on malformed placeholder", function() + local function invalidPattern() + UrlBuilder.fromString("api:game/{universeI}d/thumbnail?size={pxWidth|150}") + end + expect(invalidPattern).to.throw() + end) + + it("should throw on missing base", function() + local function invalidPattern() + UrlBuilder.fromString("/game/{universeId}/thumbnail?size={pxWidth|150}") + end + expect(invalidPattern).to.throw() + end) + + it("should throw on missing parameter name", function() + local function invalidPattern() + UrlBuilder.fromString("api:game/{universeId}/thumbnail?={pxWidth|150}") + end + expect(invalidPattern).to.throw() + end) + + end) + +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/.robloxrc b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/.robloxrc new file mode 100644 index 0000000..e7aa2a4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/.robloxrc @@ -0,0 +1,7 @@ +{ + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/CatalogUrlPatterns.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/CatalogUrlPatterns.lua new file mode 100644 index 0000000..5e116bf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/CatalogUrlPatterns.lua @@ -0,0 +1,29 @@ +--!nocheck + +return function(UrlBuilder) + + local CatalogUrlPatterns = {} + + CatalogUrlPatterns.info = { + webbundle = UrlBuilder.fromString("www:bundles/{assetId}"), + webasset = UrlBuilder.fromString("www:catalog/{assetId}"), + webpage = function(params) + if params.assetType == "Bundle" then + return CatalogUrlPatterns.info.webbundle(params) + elseif params.assetType == "Asset" then + return CatalogUrlPatterns.info.webasset(params) + else + warn(string.format("%s - unknown assetType of %s", tostring(script.name), tostring(params.assetType))) + return nil + end + end, + appsflyer = function(params) + return UrlBuilder.fromString("appsflyer:Ebh5?pid=share&is_retargeting=true&af_dp={mobileUrl}&af_web_dp={webUrl}")({ + mobileUrl = UrlBuilder.fromString("mobilenav:item_details?itemId={assetId}&itemType={assetType}")(params), + webUrl = CatalogUrlPatterns.info.webpage(params), + }) + end, + } + + return CatalogUrlPatterns +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/CatalogUrlPatterns.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/CatalogUrlPatterns.spec.lua new file mode 100644 index 0000000..adb1d17 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/CatalogUrlPatterns.spec.lua @@ -0,0 +1,25 @@ +return function() + local UrlBuilder = require(script.Parent.Parent.UrlBuilder) + + describe("appsflyer link", function() + it("should generate proper bundle url", function() + local url = UrlBuilder.catalog.info.appsflyer({ + assetId = "1356984689", + assetType = "Bundle", + }) + expect(url).to.equal("https://ro.blox.com/Ebh5?pid=share&is_retargeting=true" .. + "&af_dp=robloxmobile%3A%2F%2Fnavigation%2Fitem_details%3FitemId%3D1356984689%26itemType%3DBundle" .. + "&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fbundles%2F1356984689") + end) + + it("should generate proper asset url", function() + local url = UrlBuilder.catalog.info.appsflyer({ + assetId = "1356984689", + assetType = "Asset", + }) + expect(url).to.equal("https://ro.blox.com/Ebh5?pid=share&is_retargeting=true" .. + "&af_dp=robloxmobile%3A%2F%2Fnavigation%2Fitem_details%3FitemId%3D1356984689%26itemType%3DAsset" .. + "&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fcatalog%2F1356984689") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/GameUrlPatterns.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/GameUrlPatterns.lua new file mode 100644 index 0000000..f0b46f8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/GameUrlPatterns.lua @@ -0,0 +1,59 @@ +--!nocheck + +return function(UrlBuilder) + + local GameUrlPatterns = {} + + --- from GameInfoList.lua + GameUrlPatterns.info = { + webpage = UrlBuilder.fromString("www:games/{placeId}"), + store = UrlBuilder.fromString("www:games/store-section/{universeId}"), + badges = UrlBuilder.fromString("www:games/badges-section/{universeId}"), + servers = UrlBuilder.fromString("www:games/servers-section/{universeId}"), + serversPreopenCreateVip = UrlBuilder.fromString("www:games/servers-section-preopen-create-vip/{universeId}"), + group = UrlBuilder.fromString("www:groups/{creatorId}"), + user = UrlBuilder.fromString("www:users/{creatorId}/profile"), + -- {creatorType=Group|User, creatorId} + creator = function(params) + if params.creatorType == "Group" then + return GameUrlPatterns.info.group(params) + elseif params.creatorType == "User" then + return GameUrlPatterns.info.user(params) + end + warn(string.format("%s - unknown creatorType of %s", tostring(script.name), tostring(params.creatorType))) + return nil + end, + appsflyer = function(params) + return UrlBuilder.fromString("appsflyer:Ebh5?pid=share&is_retargeting=true&af_dp={mobileUrl}&af_web_dp={webUrl}")({ + mobileUrl = UrlBuilder.fromString("mobilenav:game_details?gameId={universeId}")(params), + webUrl = GameUrlPatterns.info.webpage(params), + }) + end, + } + + --- from Http/Requests/* + GameUrlPatterns.details = UrlBuilder.fromString("games:games?{universeIds}") + GameUrlPatterns.playability = UrlBuilder.fromString("games:games/multiget-playability-status?{universeIds}") + GameUrlPatterns.media = UrlBuilder.fromString("games:games/{universeId}/media") + GameUrlPatterns.favorite = UrlBuilder.fromString("games:games/{universeId}/favorites") + GameUrlPatterns.social = UrlBuilder.fromString("games:games/{universeId}/social-links/list") + GameUrlPatterns.recommended = UrlBuilder.fromString("games:games/recommendations/game/{universeId}?{paginationKey|}&{maxRows|6}") + GameUrlPatterns.thumbnail = UrlBuilder.fromString("games:games/game-thumbnails?{height|150}&{width|150}&{imageTokens}") + GameUrlPatterns.vote = { + -- votes for all users + all = UrlBuilder.fromString("games:games/{universeId}/votes"), + -- current user vote status + get = UrlBuilder.fromString("games:games/{universeId}/votes/user"), + set = UrlBuilder.fromString("games:games/{universeId}/user-votes"), + } + GameUrlPatterns.follow = { + get = UrlBuilder.fromString("followings:users/{userId}/universes/{universeId}/status"), + set = UrlBuilder.fromString("followings:users/{userId}/universes/{universeId}"), + } + GameUrlPatterns.report = UrlBuilder.fromString("www:abusereport/asset?id={placeId}") + + GameUrlPatterns.place = UrlBuilder.fromString("games:games/multiget-place-details?{placeIds}") + + return GameUrlPatterns + +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/GameUrlPatterns.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/GameUrlPatterns.spec.lua new file mode 100644 index 0000000..9f69078 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/GameUrlPatterns.spec.lua @@ -0,0 +1,15 @@ +return function() + local UrlBuilder = require(script.Parent.Parent.UrlBuilder) + + describe("appsflyer link", function() + it("should generate proper url", function() + local url = UrlBuilder.game.info.appsflyer({ + universeId = "1356984689", + placeId = "123456789", + }) + expect(url).to.equal("https://ro.blox.com/Ebh5?pid=share&is_retargeting=true" .. + "&af_dp=robloxmobile%3A%2F%2Fnavigation%2Fgame_details%3FgameId%3D1356984689" .. + "&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fgames%2F123456789") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/StaticUrlPatterns.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/StaticUrlPatterns.lua new file mode 100644 index 0000000..825c4bb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/StaticUrlPatterns.lua @@ -0,0 +1,49 @@ +return function(UrlBuilder) + local function isQQ() + return string.find(UrlBuilder.fromString("corp:")(), "qq.com") + end + + return { + catalog = UrlBuilder.fromString("www:catalog"), + buildersClub = UrlBuilder.fromString("www:mobile-app-upgrades/native-ios/bc"), + trades = UrlBuilder.fromString("www:trades"), + profile = UrlBuilder.fromString("www:users/profile"), + friends = UrlBuilder.fromString("www:users/friends"), + groups = UrlBuilder.fromString("www:my/groups"), + inventory = UrlBuilder.fromString("www:users/inventory"), + messages = UrlBuilder.fromString("www:my/messages"), + feed = UrlBuilder.fromString("www:feeds/inapp"), + develop = UrlBuilder.fromString("www:develop/landing"), + blog = UrlBuilder.fromString("blog:"), + help = UrlBuilder.fromString(isQQ() and "corp:faq" or "www:help"), + about = { + us = UrlBuilder.fromString("corp:"), + careers = UrlBuilder.fromString(isQQ() and "corp:careers.html" or "corp:careers"), + parents = UrlBuilder.fromString("corp:parents"), + terms = function(params) + if isQQ() and params.useGameQQUrls then + return UrlBuilder.fromString("https://game.qq.com/contract.shtml")() + else + return UrlBuilder.fromString("www:info/terms")() + end + end, + privacy = function(params) + if isQQ() and params.useGameQQUrls then + return UrlBuilder.fromString("https://game.qq.com/privacy_guide.shtml")() + else + return UrlBuilder.fromString("www:info/privacy")() + end + end, + }, + settings = { + account = UrlBuilder.fromString("www:my/account#!/info"), + security = UrlBuilder.fromString("www:my/account#!/security"), + privacy = UrlBuilder.fromString("www:my/account#!/privacy"), + billing = UrlBuilder.fromString("www:my/account#!/billing"), + notifications = UrlBuilder.fromString("www:my/account#!/notifications"), + }, + tencent = { + reputationInfo = UrlBuilder.fromString("https://gamecredit.qq.com/static/games/index.htm") + } + } +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/UserUrlPatterns.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/UserUrlPatterns.lua new file mode 100644 index 0000000..b4368bf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/UserUrlPatterns.lua @@ -0,0 +1,21 @@ + +return function(UrlBuilder) + return { + profile = UrlBuilder.fromString("www:users/{userId}/profile"), + friends = UrlBuilder.fromString("www:users/{userId}/friends"), + inventory = UrlBuilder.fromString("www:users/{userId}/inventory"), + search = UrlBuilder.fromString("www:search/users?{keyword}"), + + report = function(params) + -- Web is fixing a bug that requires actionName and redirectUrl for this page to work + -- once fixed, this pattern function can be replaced with + -- UrlBuilder.fromString("www:abusereport/embedded/chat?id={userId}&{conversationId}"), + return UrlBuilder.fromString("www:abusereport/embedded/chat?id={userId}&{actionName}&{conversationId}&{redirecturl}")({ + userId = params.userId, + conversationId = params.conversationId, + actionName = "chat", + redirecturl = UrlBuilder.fromString("www:home")(), + }) + end + } +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/encodeURIComponent.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/encodeURIComponent.lua new file mode 100644 index 0000000..ec2079e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/encodeURIComponent.lua @@ -0,0 +1,7 @@ +local function formatCharacter(character) + return string.format("%%%02X", character:byte(1,1)) +end + +return function(stringToEncode) + return stringToEncode:gsub("[^%w_%-%!%.%~%*%'%(%)]", formatCharacter) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/encodeURIComponent.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/encodeURIComponent.spec.lua new file mode 100644 index 0000000..b37bbce --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/encodeURIComponent.spec.lua @@ -0,0 +1,18 @@ +return function() + local encodeURIComponent = require(script.Parent.encodeURIComponent) + + it('should not filter alphanumerics', function() + local str = 'abcXYZ1230' + expect(encodeURIComponent(str)).to.equal(str) + end) + + it('should not filter allowed special characters', function() + local str = 'abcABC123-_.!~*\'()' + expect(encodeURIComponent(str)).to.equal(str) + end) + + it('should filter other non-alphanumeric characters', function() + local str = 'hello world&result=true' + expect(encodeURIComponent(str)).to.equal('hello%20world%26result%3Dtrue') + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/init.lua new file mode 100644 index 0000000..21abeb9 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/init.lua @@ -0,0 +1,5 @@ +return { + encodeURIComponent = require(script.encodeURIComponent), + UrlBase = require(script.UrlBase), + UrlBuilder = require(script.UrlBuilder), +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Freeze.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Freeze.lua new file mode 100644 index 0000000..bdd302b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Freeze.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["freeze"]["freeze"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Promise.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Promise.lua new file mode 100644 index 0000000..5dea2b4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Promise.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["lua-promise"]["lua-promise"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Rodux.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Rodux.lua new file mode 100644 index 0000000..96b67df --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Rodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_rodux"]["rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/lock.toml new file mode 100644 index 0000000..9f7281f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/lock.toml @@ -0,0 +1,12 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "rodux-networking" +version = "1.0.0" +commit = "ea19cfe3c71e5747fa3ab4235beed4fc21d63e4d" +source = "git+https://github.rbx.com/roblox/rodux-networking#v1.0.1" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Freeze freeze ef89f9d0 git+https://github.rbx.com/roblox/freeze#master", + "Promise lua-promise bbb9e162 git+https://github.rbx.com/roblox/lua-promise#master", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "tutils tutils 0be577db git+https://github.rbx.com/roblox/tutils#master", +] diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Action.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Action.lua new file mode 100644 index 0000000..2760513 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Action.lua @@ -0,0 +1,68 @@ +--[[ + A helper function to define a Rodux action creator with an associated name. + + Normally when creating a Rodux action, you can just create a function: + + return function(value) + return { + type = "MyAction", + value = value, + } + end + + And then when you check for it in your reducer, you either use a constant, + or type out the string name: + + if action.type == "MyAction" then + -- change some state + end + + Typos here are a remarkably common bug. We also have the issue that there's + no link between reducers and the actions that they respond to! + + `Action` (this helper) provides a utility that makes this a bit cleaner. + + Instead, define your Rodux action like this: + + return Action("MyAction", function(value) + return { + value = value, + } + end) + + We no longer need to add the `type` field manually. + + Additionally, the returned action creator now has a 'name' property that can + be checked by your reducer: + + local MyAction = require(Reducers.MyAction) + + ... + + if action.type == MyAction.name then + -- change some state! + end + + Now we have a clear link between our reducers and the actions they use, and + if we ever typo a name, we'll get a warning in LuaCheck as well as an error + at runtime! +]] + +return function(name, fn) + assert(type(name) == "string", "A name must be provided to create an Action") + assert(type(fn) == "function", "A function must be provided to create an Action") + + return setmetatable({ + name = name, + }, { + __call = function(self, ...) + local result = fn(...) + + assert(type(result) == "table", "An action must return a table") + + result.type = name + + return result + end + }) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Cryo.lua new file mode 100644 index 0000000..ddd769d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Cryo.lua @@ -0,0 +1,4 @@ +local root = script.Parent +local Packages = root.Parent + +return require(Packages.Cryo) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Freeze.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Freeze.lua new file mode 100644 index 0000000..e4c6423 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Freeze.lua @@ -0,0 +1,4 @@ +local root = script.Parent +local Packages = root.Parent + +return require(Packages.Freeze) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/GET.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/GET.lua new file mode 100644 index 0000000..1f76609 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/GET.lua @@ -0,0 +1,6 @@ +local root = script.Parent +local makeRequestApi = require(root.makeRequestApi) + +return function(options) + return makeRequestApi(options, "GET") +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/GET.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/GET.spec.lua new file mode 100644 index 0000000..179af61 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/GET.spec.lua @@ -0,0 +1,24 @@ +return function() + local root = script.Parent + local mockNetworkImpl = function() + return nil + end + + local GET = require(root.GET)({ + keyPath = "hello.world", + networkImpl = mockNetworkImpl, + }) + + describe("WHEN invoked", function() + local endpoint = GET(script, function(requestBuilder) + return requestBuilder("example.com") + end) + + it("SHOULD return an object have all expected fields", function() + expect(endpoint.API).to.be.ok() + expect(endpoint.getStatus).to.be.ok() + expect(endpoint.Succeeded).to.be.ok() + expect(endpoint.Failed).to.be.ok() + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/EnumNetworkStatus.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/EnumNetworkStatus.lua new file mode 100644 index 0000000..d8db68e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/EnumNetworkStatus.lua @@ -0,0 +1,20 @@ +local EnumNetworkStatus = {} + +local EnumValues = { + NotStarted = "NotStarted", + Fetching = "Fetching", + Done = "Done", + Failed = "Failed", +} + +setmetatable(EnumNetworkStatus, + { + __newindex = function(t, key, index) + end, + __index = function(t, index) + assert(EnumValues[index] ~= nil, ("EnumNetworkStatus has no value: " .. tostring(index))) + return EnumValues[index] + end + }) + +return EnumNetworkStatus diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Freeze.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Freeze.lua new file mode 100644 index 0000000..98dc4b4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Freeze.lua @@ -0,0 +1,4 @@ +local root = script.Parent.Parent +local Packages = root.Parent + +return require(Packages.Freeze) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Promise.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Promise.lua new file mode 100644 index 0000000..a7444c1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Promise.lua @@ -0,0 +1,4 @@ +local root = script.Parent.Parent +local Packages = root.Parent + +return require(Packages.Promise) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Rodux.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Rodux.lua new file mode 100644 index 0000000..b448060 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Rodux.lua @@ -0,0 +1,4 @@ +local root = script.Parent.Parent +local Packages = root.Parent + +return require(Packages.Rodux) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/buildActionName.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/buildActionName.lua new file mode 100644 index 0000000..6190b72 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/buildActionName.lua @@ -0,0 +1,3 @@ +return function(options) + return "networkStatus:" .. tostring(options.keyPath) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/buildActionName.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/buildActionName.spec.lua new file mode 100644 index 0000000..feb7a5a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/buildActionName.spec.lua @@ -0,0 +1,17 @@ +return function() + local buildActionName = require(script.Parent.buildActionName) + + it("SHOULD concat networkStatus: to keyPath", function() + local result = buildActionName({ + keyPath = "Hello", + }) + expect(result).to.equal("networkStatus:Hello") + end) + + it("SHOULD concat networkStatus: to keyPath with depth", function() + local result = buildActionName({ + keyPath = "Hello.World", + }) + expect(result).to.equal("networkStatus:Hello.World") + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getDeepValue.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getDeepValue.lua new file mode 100644 index 0000000..dd449b6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getDeepValue.lua @@ -0,0 +1,12 @@ +return function(tab, keyPath) + local currentNode = tab + for _, key in ipairs(keyPath:split(".")) do + if not currentNode[key] then + return nil + end + + currentNode = currentNode[key] + end + + return currentNode +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getDeepValue.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getDeepValue.spec.lua new file mode 100644 index 0000000..0d463a2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getDeepValue.spec.lua @@ -0,0 +1,54 @@ +return function() + local getDeepValue = require(script.Parent.getDeepValue) + + describe("GIVEN an empty array", function() + local tab = {} + it("SHOULD return nil", function() + expect(getDeepValue(tab, "")).to.equal(nil) + expect(getDeepValue(tab, "hello.world")).to.equal(nil) + end) + end) + + describe("GIVEN a dictionary with hello.world", function() + local tab = { + hello = { + world = 100, + }, + } + describe("GIVEN an empty string as the second argument", function() + it("SHOULD return nil", function() + expect(getDeepValue(tab, "")).to.equal(nil) + end) + end) + + describe("GIVEN `goodbye.world` as the second argument", function() + it("SHOULD return nil", function() + expect(getDeepValue(tab, "goodbye.world")).to.equal(nil) + end) + end) + + describe("GIVEN `hello.there` as the second argument", function() + it("SHOULD return nil", function() + expect(getDeepValue(tab, "hello.there")).to.equal(nil) + end) + end) + + describe("GIVEN `hello.there` as the second argument", function() + it("SHOULD return nil", function() + expect(getDeepValue(tab, "hello.there")).to.equal(nil) + end) + end) + + describe("GIVEN `hello` as the second argument", function() + it("SHOULD return the hello table", function() + expect(getDeepValue(tab, "hello")).to.equal(tab.hello) + end) + end) + + describe("GIVEN `hello.world` as the second argument", function() + it("SHOULD return the 100 (the value mapped to hello.world)", function() + expect(getDeepValue(tab, "hello.world")).to.equal(100) + end) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getStatus.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getStatus.lua new file mode 100644 index 0000000..2f8fd73 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getStatus.lua @@ -0,0 +1,20 @@ +local root = script.Parent +local getDeepValue = require(root.getDeepValue) +local EnumNetworkStatus = require(root.EnumNetworkStatus) + +return function(options) + local keyPath = options.keyPath + + return function(state, fetchStatusKey) + assert(typeof(state) == "table") + assert(typeof(fetchStatusKey) == "string") + assert(#fetchStatusKey > 0) + local reducerValue = getDeepValue(state, keyPath) + assert(reducerValue, string.format( + "Reducer not found for keyPath: %s. Did you forget to call `installReducer`?", + keyPath + )) + + return reducerValue:get(fetchStatusKey) or EnumNetworkStatus.NotStarted + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getStatus.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getStatus.spec.lua new file mode 100644 index 0000000..da1d223 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getStatus.spec.lua @@ -0,0 +1,35 @@ +local Freeze = require(script.Parent.Freeze) + +return function() + local getStatus = require(script.Parent.getStatus)({ + keyPath = "testingKeyPathStatus", + }) + local EnumNetworkStatus = require(script.Parent.EnumNetworkStatus) + local TEST_KEY_1 = "item_key" + + it("should return NotStarted for missing key", function() + local state = { testingKeyPathStatus = Freeze.UnorderedMap.new({}) } + local status = getStatus(state, TEST_KEY_1) + + expect(status).to.equal(EnumNetworkStatus.NotStarted) + end) + + it("should return matching status for state in store", function() + local statusesToTest = { + EnumNetworkStatus.NotStarted, + EnumNetworkStatus.Fetching, + EnumNetworkStatus.Done, + EnumNetworkStatus.Failed + } + + for _, testStatus in ipairs(statusesToTest) do + local state = { + testingKeyPathStatus = Freeze.UnorderedMap.new({ + [TEST_KEY_1] = testStatus + }) + } + + expect(getStatus(state, TEST_KEY_1)).to.equal(testStatus) + end + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/init.lua new file mode 100644 index 0000000..f9937bc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/init.lua @@ -0,0 +1,11 @@ +return function(options) + return { + getStatus = require(script.getStatus)(options), + setStatus = require(script.setStatus)(options), + installReducer = require(script.installReducer)(options), + + Enum = { + Status = require(script.EnumNetworkStatus), + }, + } +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/installReducer.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/installReducer.lua new file mode 100644 index 0000000..7478980 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/installReducer.lua @@ -0,0 +1,23 @@ +local Rodux = require(script.Parent.Rodux) +local Freeze = require(script.Parent.Freeze) +local buildActionName = require(script.Parent.buildActionName) + +return function(options) + return function() + return Rodux.createReducer(Freeze.UnorderedMap.new({}), { + [buildActionName(options)] = function(state, action) + local updatedStatus = {} + if #action.ids == 0 then + updatedStatus[action.keymapper()] = action.status + else + for _, id in ipairs(action.ids) do + local mappedId = action.keymapper(id) + updatedStatus[mappedId] = action.status + end + end + + return state:batchSet(updatedStatus) + end, + }) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/installReducer.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/installReducer.spec.lua new file mode 100644 index 0000000..b3c6a1e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/installReducer.spec.lua @@ -0,0 +1,107 @@ +return function() + local options = { + keyPath = "networkStatus" + } + local installReducer = require(script.Parent.installReducer)(options) + local buildActionName = require(script.Parent.buildActionName) + local getStatus = require(script.Parent.getStatus)(options) + local reducer = installReducer() + + describe("GIVEN an action with no id", function() + local initialAction = { + type = buildActionName(options), + ids = {}, + keymapper = function() return "key" end, + status = "test", + } + + it("SHOULD return a new UnorderedMap with the key mapped properly", function() + local result = reducer(nil, initialAction) + + expect(result).to.be.ok() + expect(result:get("key")).to.equal("test") + end) + + describe("GIVEN another action that rewrites the previous key", function() + local overwriteAction = { + type = buildActionName(options), + ids = {}, + keymapper = function() return "key" end, + status = "next-best-thing", + } + + it("SHOULD update the key accordingly", function() + local result = reducer(reducer(nil, initialAction), overwriteAction) + + expect(result).to.be.ok() + expect(result:get("key")).to.equal("next-best-thing") + end) + end) + + it("SHOULD be retrievable with getStatus", function() + local state = { + networkStatus = reducer(nil, initialAction), + } + local result = getStatus(state, "key") + expect(result).to.equal("test") + end) + end) + + describe("GIVEN an action with one id", function() + local initialAction = { + type = buildActionName(options), + ids = { "123" }, + keymapper = function(id) return "key:" .. id end, + status = "test", + } + + it("SHOULD return a new UnorderedMap with the key mapped properly", function() + local result = reducer(nil, initialAction) + + expect(result).to.be.ok() + expect(result:get("key:123")).to.equal("test") + end) + + describe("GIVEN another action that rewrites the previous key", function() + local overwriteAction = { + type = buildActionName(options), + ids = { "123" }, + keymapper = function(id) return "key:" .. id end, + status = "next-best-thing", + } + + it("SHOULD update the key accordingly", function() + local result = reducer(reducer(nil, initialAction), overwriteAction) + + expect(result).to.be.ok() + expect(result:get("key:123")).to.equal("next-best-thing") + end) + end) + + it("SHOULD be retrievable with getStatus", function() + local state = { + networkStatus = reducer(nil, initialAction), + } + local result = getStatus(state, "key:123") + expect(result).to.equal("test") + end) + end) + + describe("GIVEN an action with multiple ids", function() + local batchAction = { + type = buildActionName(options), + ids = { "123", "456", "789" }, + keymapper = function(id) return "key:" .. id end, + status = "the-same-status", + } + + it("SHOULD update all keys to the same status", function() + local result = reducer(nil, batchAction) + + expect(result).to.be.ok() + expect(result:get("key:123")).to.equal("the-same-status") + expect(result:get("key:456")).to.equal("the-same-status") + expect(result:get("key:789")).to.equal("the-same-status") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/setStatus.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/setStatus.lua new file mode 100644 index 0000000..979a7da --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/setStatus.lua @@ -0,0 +1,41 @@ +local EnumNetworkStatus = require(script.Parent.EnumNetworkStatus) +local buildActionName = require(script.Parent.buildActionName) + +return function(options) + local getStatus = require(script.Parent.getStatus)(options) + + local function actionCreator(ids, keymapper, status) + return { + ids = ids, + keymapper = keymapper, + status = status, + type = buildActionName(options), + } + end + + local function filter(state, ids, keymapper) + local filteredIds = {} + for _, id in ipairs(ids) do + local status = getStatus(state, keymapper(id)) + if status ~= EnumNetworkStatus.Fetching then + table.insert(filteredIds, id) + end + end + return filteredIds + end + + return function(store, ids, keymapper, promiseFunction) + local filteredIds = filter(store:getState(), ids, keymapper) + + store:dispatch(actionCreator(filteredIds, keymapper, EnumNetworkStatus.Fetching)) + + return promiseFunction(store, filteredIds):andThen(function(result) + store:dispatch(actionCreator(filteredIds, keymapper, EnumNetworkStatus.Done)) + return result + end, + function(errorString) + store:dispatch(actionCreator(filteredIds, keymapper, EnumNetworkStatus.Failed)) + error(errorString) + end) + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/setStatus.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/setStatus.spec.lua new file mode 100644 index 0000000..727e0c2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/setStatus.spec.lua @@ -0,0 +1,85 @@ +return function() + local Freeze = require(script.Parent.Freeze) + local EnumNetworkStatus = require(script.Parent.EnumNetworkStatus) + local Promise = require(script.Parent.Promise) + local setStatus = require(script.Parent.setStatus)({ + keyPath = "testingKeyPath", + }) + local mockStore = require(script.Parent.Parent.mockStore) + + describe("GIVEN a store", function() + local actionHistory = {} + local roduxStore = mockStore.config({ + dispatch = function(self, action) + if type(action) == "function" then + action(self) + else + table.insert(actionHistory, action) + end + end, + }) + + describe("GIVEN multiple ids with a valid keymapper and promise function", function() + local ids = { "123", "456", "789" } + local keymapper = function(id) + return "key" .. tostring(id) + end + + describe("WHEN there are no ongoing requests", function() + it("SHOULD not filter any ids", function() + roduxStore:setState({ + testingKeyPath = Freeze.UnorderedMap.new({}), + }) + + local promiseFunction = function(store, filteredIds) + expect(store).to.be.ok() + expect(#filteredIds).to.equal(#ids) + return Promise.resolve(filteredIds) + end + + setStatus(roduxStore, ids, keymapper, promiseFunction) + end) + + it("SHOULD only dispatch relevant actions", function() + expect(#actionHistory).to.equal(2) + + local secondToLastAction = actionHistory[#actionHistory - 1] + expect(secondToLastAction).to.be.ok() + expect(type(secondToLastAction)).to.equal("table") + expect(secondToLastAction.type).to.equal("networkStatus:testingKeyPath") + expect(secondToLastAction.status).to.equal(EnumNetworkStatus.Fetching) + + local lastAction = actionHistory[#actionHistory] + expect(lastAction).to.be.ok() + expect(type(lastAction)).to.equal("table") + expect(lastAction.type).to.equal("networkStatus:testingKeyPath") + expect(lastAction.status).to.equal(EnumNetworkStatus.Done) + end) + end) + + describe("WHEN there is already an ongoing request", function() + it("SHOULD filter the ids that are currently ongoing", function() + actionHistory = {} + roduxStore:setState({ + testingKeyPath = Freeze.UnorderedMap.new({ + [keymapper("123")] = EnumNetworkStatus.Fetching, + [keymapper("456")] = EnumNetworkStatus.Fetching, + [keymapper("789")] = EnumNetworkStatus.Fetching, + }), + }) + local promiseFunction = function(store, filteredIds) + expect(store).to.be.ok() + expect(#filteredIds).to.equal(0) + return Promise.resolve(filteredIds) + end + + setStatus(roduxStore, ids, keymapper, promiseFunction) + end) + + it("SHOULD still fire any actions if there are no ids", function() + expect(#actionHistory).to.equal(2) + end) + end) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/POST.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/POST.lua new file mode 100644 index 0000000..d5d74e5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/POST.lua @@ -0,0 +1,6 @@ +local root = script.Parent +local makeRequestApi = require(root.makeRequestApi) + +return function(options) + return makeRequestApi(options, "POST") +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/POST.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/POST.spec.lua new file mode 100644 index 0000000..a09d6d2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/POST.spec.lua @@ -0,0 +1,24 @@ +return function() + local root = script.Parent + local mockNetworkImpl = function() + return nil + end + + local POST = require(root.POST)({ + keyPath = "hello.world", + networkImpl = mockNetworkImpl, + }) + + describe("WHEN invoked", function() + local endpoint = POST(script, function(requestBuilder) + return requestBuilder("example.com") + end) + + it("SHOULD return an object have all expected fields", function() + expect(endpoint.API).to.be.ok() + expect(endpoint.getStatus).to.be.ok() + expect(endpoint.Succeeded).to.be.ok() + expect(endpoint.Failed).to.be.ok() + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Promise.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Promise.lua new file mode 100644 index 0000000..cbb818f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Promise.lua @@ -0,0 +1,4 @@ +local root = script.Parent +local Packages = root.Parent + +return require(Packages.Promise) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/Cryo.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/Cryo.lua new file mode 100644 index 0000000..834f875 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/Cryo.lua @@ -0,0 +1,4 @@ +local root = script.Parent.Parent +local Packages = root.Parent + +return require(Packages.Cryo) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/RequestBuilder.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/RequestBuilder.lua new file mode 100644 index 0000000..ac0d91f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/RequestBuilder.lua @@ -0,0 +1,139 @@ +local root = script.Parent +local Cryo = require(root.Cryo) + +local UriIds = {} +UriIds.__index = UriIds + +function UriIds:new(ids, delimiter) + return setmetatable({ + ids = UriIds.makeArray(ids), + delimiter = delimiter + }, self) +end + +function UriIds:setIds(ids) + self.ids = UriIds.makeArray(ids) +end + +function UriIds.makeArray(ids) + if type(ids) == "table" then + return ids + end + return {ids} +end + +function UriIds:__tostring() + return table.concat(self.ids, self.delimiter) +end + +local RequestBuilder = {} +RequestBuilder.__index = RequestBuilder + +function RequestBuilder:new(baseUrl) + return setmetatable({ + baseUrl = baseUrl, + keyMapper = nil, + args = {}, + pathElements = {}, + configurableIds = nil, + namedIds = {}, + idsDelimiter = ";", + options = {}, + }, self) +end + +function RequestBuilder:path(path) + table.insert(self.pathElements, path) + return self +end + +function RequestBuilder:id(ids, key) + if not key and #self.pathElements < 1 then + warn("Cannot name id or ids because there is no leading path segment and no name is provided") + end + local name = key or self.pathElements[#self.pathElements] + self.namedIds[name] = ids + + self.configurableIds = UriIds:new(ids, self.idsDelimiter) + table.insert(self.pathElements, self.configurableIds) + return self +end + +function RequestBuilder:queryArgWithIds(argName, ids) + self.namedIds[argName] = ids + self.configurableIds = UriIds:new(ids, self.idsDelimiter) + self:queryArgs({ + [argName] = self.configurableIds + }) + return self +end + +function RequestBuilder:queryArgs(args) + self.args = Cryo.Dictionary.join(self.args, args) + return self +end + +function RequestBuilder:body(dictionary) + self.options.postBody = dictionary + return self +end + +function RequestBuilder:makeKeyMapper() + return function(someId) + return self:makeUrl(someId) + end +end + +function RequestBuilder:makeUri(ids) + local fullPath = "" + for _, element in ipairs(self.pathElements) do + fullPath = fullPath .. "/" .. tostring(element) + end + return fullPath +end + +function RequestBuilder:makeQueryArgs(ids) + self:_plugInConfigurableIds(ids) + local argsString = "" + for k,v in pairs(self.args) do + local arg = tostring(k) .. "=" .. tostring(v) + if argsString:len() > 1 then + argsString = argsString .. "&" .. arg + else + argsString = arg + end + end + if argsString:len() > 1 then + return "?" .. argsString + end + return "" +end + +function RequestBuilder:makeUrl(ids) + self:_plugInConfigurableIds(ids) + local fullUrl = self.baseUrl .. self:makeUri(ids) .. self:makeQueryArgs(ids) + return fullUrl +end + +function RequestBuilder:makeOptions() + return self.options +end + +function RequestBuilder:_plugInConfigurableIds(ids) + if ids ~= nil and self.configurableIds then + self.configurableIds:setIds(ids) + end +end + +function RequestBuilder:getIds() + if self.configurableIds and self.configurableIds.ids then + return self.configurableIds.ids + end + return {} +end + +function RequestBuilder:getNamedIds() + return self.namedIds +end + +return RequestBuilder diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/RequestBuilder.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/RequestBuilder.spec.lua new file mode 100644 index 0000000..0d87d15 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/RequestBuilder.spec.lua @@ -0,0 +1,177 @@ +return function() + local root = script.Parent + local RequestBuilder = require(root.RequestBuilder) + local tutils = require(root.tutils) + + local baseUrl = "https://example.com" + describe("RequestBuilder basics", function() + it("builder functions should return self", function() + local builder = RequestBuilder:new() + + expect(builder).to.equal(builder:path("test")) + expect(builder).to.equal(builder:id("test")) + expect(builder).to.equal(builder:queryArgs({ "test" })) + expect(builder).to.equal(builder:body({ "test" })) + end) + + it("should be constructible from base URL", function() + + local builder = RequestBuilder:new(baseUrl) + expect(builder).to.be.ok() + expect(builder:makeUrl()).to.equal(baseUrl) + end) + + it("should be constructible from successive path calls", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("some"):path("element") + expect(builder:makeUrl()).to.equal(baseUrl .. "/some/element") + expect(tutils.shallowEqual(builder:getIds(), {})).to.equal(true) + end) + + it("should allow building query args", function() + local builder = RequestBuilder:new(baseUrl) + builder:queryArgs({ + arg = "value" + }) + expect(builder:makeUrl()).to.equal(baseUrl .. "?arg=value") + expect(tutils.shallowEqual(builder:getIds(), {})).to.equal(true) + end) + + it("should allow building multiple query args", function() + local builder = RequestBuilder:new(baseUrl) + builder:queryArgs({ + arg = "value" + }) + builder:queryArgs({ + arg2 = "value2" + }) + expect(builder:makeUrl()).to.equal(baseUrl .. "?arg2=value2&arg=value") + expect(tutils.shallowEqual(builder:getIds(), {})).to.equal(true) + end) + end) + + describe("RequestBuilder path ids", function() + it("should be constructible from path and single id", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("some/element"):id(123) + + local expectedUrl = baseUrl .. "/some/element/123" + expect(builder:makeUrl()).to.equal(expectedUrl) + + expect(builder:makeKeyMapper()).to.be.ok() + expect(builder:makeKeyMapper()()).to.equal(expectedUrl) + + expect(tutils.shallowEqual(builder:getIds(), {123})).to.equal(true) + end) + + it("should be constructible from path and ids array", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("some/element"):id({123, 321}) + + local staticUrlPart = baseUrl .. "/some/element/" + expect(builder:makeUrl()).to.equal(staticUrlPart .. "123;321") + + expect(builder:makeKeyMapper()(123)).to.equal(staticUrlPart .. "123") + expect(builder:makeKeyMapper()(321)).to.equal(staticUrlPart .. "321") + end) + + it("should allow swapping ids in makeUrl call", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("some/element"):id({}) + expect(builder:makeUrl({567, 789})).to.equal(baseUrl .. "/some/element/567;789") + end) + + it("should allow swapping ids in makeUrl call, but only for last ids group", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("some/element"):id(123):path("other"):id({}) + + local staticUrlPart = baseUrl .. "/some/element/123/other" + expect(builder:makeUrl(567)).to.equal(staticUrlPart .. "/567") + expect(builder:makeUrl({567})).to.equal(staticUrlPart .. "/567") + expect(builder:makeUrl({567, 789})).to.equal(staticUrlPart .. "/567;789") + + expect(builder:makeKeyMapper()(567)).to.equal(staticUrlPart .. "/567") + expect(builder:makeKeyMapper()(789)).to.equal(staticUrlPart .. "/789") + end) + + it("should map previous path segment to id", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("pathexample"):id(444) + local namedIds = builder:getNamedIds() + expect(namedIds["pathexample"]).to.be.ok() + expect(namedIds["pathexample"]).to.equal(444) + end) + + it("should map previous path segment to id with multiple path segments and ids", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("firstpathexample"):id(444):path("anotherpathexample"):path("yetanotherpathexample"):id(555) + local namedIds = builder:getNamedIds() + expect(namedIds["firstpathexample"]).to.be.ok() + expect(namedIds["anotherpathexample"]).to.never.be.ok() + expect(namedIds["yetanotherpathexample"]).to.be.ok() + + expect(namedIds["firstpathexample"]).to.equal(444) + expect(namedIds["yetanotherpathexample"]).to.equal(555) + end) + + it("should map previous path segment to id with multiple path segments and ids, with multiple ids given to function", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("firstpathexample"):id({ 333, 444 }):path("anotherpathexample"):path("yetanotherpathexample"):id(555) + local namedIds = builder:getNamedIds() + expect(namedIds["firstpathexample"]).to.be.ok() + expect(namedIds["anotherpathexample"]).to.never.be.ok() + expect(namedIds["yetanotherpathexample"]).to.be.ok() + + expect(tutils.deepEqual(namedIds["firstpathexample"], { 333, 444 })).to.equal(true) + expect(namedIds["yetanotherpathexample"]).to.equal(555) + end) + + it("should allow one to override the default key", function() + local builder = RequestBuilder:new(baseUrl) + local KEY1 = "KEY1" + builder:path("firstpathexample"):id({ 333, 444 }, KEY1):path("anotherpathexample"):path("yetanotherpathexample"):id(555) + local namedIds = builder:getNamedIds() + expect(namedIds["firstpathexample"]).to.never.be.ok() + expect(namedIds["anotherpathexample"]).to.never.be.ok() + expect(namedIds["yetanotherpathexample"]).to.be.ok() + + expect(tutils.deepEqual(namedIds[KEY1], { 333, 444 })).to.equal(true) + expect(namedIds["yetanotherpathexample"]).to.equal(555) + end) + end) + + describe("RequestBuilder query args and ids", function() + it("should allow swapping query argument ids in makeUrl call", function() + local builder = RequestBuilder:new(baseUrl) + builder:queryArgWithIds("arg", {123}) + expect(builder:makeUrl()).to.equal(baseUrl .. "?arg=123") + end) + + it("should allow swapping multiple query argument ids in makeUrl call", function() + local builder = RequestBuilder:new(baseUrl) + builder:queryArgWithIds("arg", {}) + expect(builder:makeUrl(123)).to.equal(baseUrl .. "?arg=123") + expect(builder:makeUrl({345, 456})).to.equal(baseUrl .. "?arg=345;456") + + expect(builder:makeKeyMapper()(567)).to.equal(baseUrl .. "?arg=567") + expect(builder:makeKeyMapper()({567, 321})).to.equal(baseUrl .. "?arg=567;321") + end) + + it("should allow swapping query argument ids in makeUrl call and not affect path ids", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("some/element"):id(999):queryArgWithIds("arg", {}) + expect(builder:makeUrl(123)).to.equal(baseUrl .. "/some/element/999?arg=123") + expect(builder:makeUrl({345, 456})).to.equal(baseUrl .. "/some/element/999?arg=345;456") + end) + end) + + describe("RequestBuilder body", function() + it("should replace postBody", function() + local myBody = { hi = "there" } + local builder = RequestBuilder:new(baseUrl):body(myBody) + + expect(builder:makeOptions()).to.be.ok() + expect(builder:makeOptions().postBody.hi).to.equal("there") + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/init.lua new file mode 100644 index 0000000..7a3f212 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/init.lua @@ -0,0 +1 @@ +return require(script.RequestBuilder) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/tutils.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/tutils.lua new file mode 100644 index 0000000..8203b43 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/tutils.lua @@ -0,0 +1,4 @@ +local root = script.Parent.Parent +local Packages = root.Parent + +return require(Packages.tutils) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RoduxNetworking.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RoduxNetworking.lua new file mode 100644 index 0000000..b4cf27d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RoduxNetworking.lua @@ -0,0 +1,37 @@ +local GET = require(script.Parent.GET) +local POST = require(script.Parent.POST) + +local RoduxNetworking = {} +RoduxNetworking.__index = RoduxNetworking + +function RoduxNetworking.new(options) + assert(options, "Expected options to be passed into RoduxNetworking") + assert(options.keyPath, "Expected options.keyPath to be passed into RoduxNetworking") + assert(options.networkImpl, "Expected options.networkImpl to be passed into RoduxNetworking") + + local self = { + options = options, + } + + return setmetatable(self, RoduxNetworking) +end + +function RoduxNetworking:GET(moduleScript, constructBuilderFunction) + assert(moduleScript, "RoduxNetworking:GET expects moduleScript argument") + assert(moduleScript, "RoduxNetworking:GET expects constructBuilderFunction argument") + return GET(self.options)(moduleScript, constructBuilderFunction) +end + +function RoduxNetworking:POST(...) + return POST(self.options)(...) +end + +function RoduxNetworking:getNetworkImpl() + return self.options.networkImpl +end + +function RoduxNetworking:setNetworkImpl(networkImpl) + self.options.networkImpl = networkImpl +end + +return RoduxNetworking \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RoduxNetworking.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RoduxNetworking.spec.lua new file mode 100644 index 0000000..8e98b28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RoduxNetworking.spec.lua @@ -0,0 +1,50 @@ +return function() + local RoduxNetworking = require(script.Parent.RoduxNetworking) + local Promise = require(script.Parent.Promise) + local noOpt = function() + return Promise.resolve() + end + local moduleScript = { + Name = "moduleScript", + } + local builderConstructorFunction = function() + end + + describe("GIVEN an options configuration", function() + local options = { + keyPath = "", + networkImpl = noOpt + } + it("SHOULD return a unique object when .new is called", function() + local instance1 = RoduxNetworking.new(options) + local instance2 = RoduxNetworking.new(options) + + expect(instance1).to.be.ok() + expect(instance2).to.be.ok() + expect(instance1).to.never.equal(instance2) + end) + + describe("API", function() + local instance = RoduxNetworking.new(options) + it("SHOULD return an object when GET is called", function() + local result = instance:GET(moduleScript, builderConstructorFunction) + expect(result).to.be.ok() + end) + + it("SHOULD return an object when POST is called", function() + local result = instance:POST(moduleScript, builderConstructorFunction) + expect(result).to.be.ok() + end) + + describe("WHEN setNetworkImpl is called", function() + local newNetworkImpl = function() + return Promise.reject() + end + instance:setNetworkImpl(newNetworkImpl) + it("SHOULD return the given value when getNetworkImpl is called", function() + expect(instance:getNetworkImpl()).to.equal(newNetworkImpl) + end) + end) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/UrlBuilder.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/UrlBuilder.lua new file mode 100644 index 0000000..a732b95 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/UrlBuilder.lua @@ -0,0 +1,4 @@ +local root = script.Parent +local Packages = root.Parent + +return require(Packages.UrlBuilder) diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/init.lua new file mode 100644 index 0000000..70d797b --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/init.lua @@ -0,0 +1,29 @@ +local RoduxNetworking = require(script.RoduxNetworking) +local NetworkStatus = require(script.NetworkStatus) + +return { + config = function(options) + local roduxNetworkingInstance = RoduxNetworking.new(options) + local networkStatusInstance = NetworkStatus(options) + + return { + GET = function(...) + return roduxNetworkingInstance:GET(...) + end, + POST = function(...) + return roduxNetworkingInstance:POST(...) + end, + getNetworkImpl = function() + return roduxNetworkingInstance:getNetworkImpl() + end, + setNetworkImpl = function(...) + roduxNetworkingInstance:setNetworkImpl(...) + end, + + installReducer = networkStatusInstance.installReducer, + Enum = { + NetworkStatus = networkStatusInstance.Enum.Status, + }, + } + end, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/init.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/init.spec.lua new file mode 100644 index 0000000..7618438 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/init.spec.lua @@ -0,0 +1,21 @@ +return function() + local init = require(script.Parent) + local noOpt = function() + end + + describe("GIVEN an options configuration", function() + local instance = init.config({ + keyPath = "hello.world", + networkImpl = noOpt, + }) + + it("SHOULD have all expected fields", function() + expect(instance.GET).to.be.ok() + expect(instance.POST).to.be.ok() + expect(instance.Enum).to.be.ok() + expect(instance.installReducer).to.be.ok() + expect(instance.getNetworkImpl).to.be.ok() + expect(instance.setNetworkImpl).to.be.ok() + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeActionCreator.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeActionCreator.lua new file mode 100644 index 0000000..8121ecc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeActionCreator.lua @@ -0,0 +1,21 @@ +local root = script.Parent +local Action = require(root.Action) + +return function(networkRequestScript) + return { + Succeeded = Action(networkRequestScript.Name .. "_Succeeded", function(ids, responseBody, namedIds) + return { + ids = ids, + responseBody = responseBody, + namedIds = namedIds, + } + end), + Failed = Action(networkRequestScript.Name .. "_Failed", function(ids, error, namedIds) + return { + ids = ids, + error = error, + namedIds = namedIds, + } + end), + } +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeActionCreator.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeActionCreator.spec.lua new file mode 100644 index 0000000..9afe984 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeActionCreator.spec.lua @@ -0,0 +1,13 @@ +return function() + local makeActionCreator = require(script.Parent.makeActionCreator) + + describe("GIVEN a script", function() + local myScript = Instance.new("ModuleScript") + local result = makeActionCreator(myScript) + + it("SHOULD return an object with a success field and failed field", function() + expect(result.Succeeded).to.be.ok() + expect(result.Failed).to.be.ok() + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApi.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApi.lua new file mode 100644 index 0000000..9ecf0a4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApi.lua @@ -0,0 +1,51 @@ +local root = script.Parent +local makeActionCreator = require(root.makeActionCreator) +local RequestBuilder = require(root.RequestBuilder) +local NetworkStatus = require(root.NetworkStatus) + +return function(options, methodType) + local keyPath = options.keyPath + + local myNetworkStatus = NetworkStatus({ + keyPath = keyPath, + }) + + return function(moduleScript, constructBuilderFunction) + local self = makeActionCreator(moduleScript) + self.API = function(...) + local userRequestBuilder = constructBuilderFunction(function(...) + return RequestBuilder:new(...) + end, ...) + + return function(store) + return myNetworkStatus.setStatus(store, userRequestBuilder:getIds(), userRequestBuilder:makeKeyMapper(), function(store, filteredIds) + local networkImpl = options.networkImpl + return networkImpl(userRequestBuilder:makeUrl(filteredIds), methodType, userRequestBuilder:makeOptions()):andThen( + function(payload) + store:dispatch(self.Succeeded(filteredIds, payload.responseBody, userRequestBuilder:getNamedIds())) + return payload + end, + function(errorString) + store:dispatch(self.Failed(filteredIds, error, userRequestBuilder:getNamedIds())) + -- Throw again so we can catch it outside of library + error(errorString) + end + ) + end) + end + end + + self.getStatus = (methodType == "GET") and function(state, key) + local userRequestBuilder = constructBuilderFunction(function(...) + return RequestBuilder:new(...) + end, key) + + local keymapper = userRequestBuilder:makeKeyMapper() + local mappedKey = keymapper(key) + + return myNetworkStatus.getStatus(state, mappedKey) + end + + return self + end +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApi.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApi.spec.lua new file mode 100644 index 0000000..9efc56f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApi.spec.lua @@ -0,0 +1,121 @@ +return function() + local root = script.Parent + local Freeze = require(root.Freeze) + local EnumNetworkStatus = require(root.NetworkStatus.EnumNetworkStatus) + local Promise = require(root.Promise) + local mockNetworkImpl = function(url, method, options) + return Promise.resolve({ + responseBody = "benj", + }) + end + local mockStore = require(root.mockStore) + + local GET = require(root.makeRequestApi)({ + keyPath = "hello.world", + networkImpl = mockNetworkImpl, + }, "GET") + + describe("GIVEN a store", function() + local actionHistory = {} + local roduxStore = mockStore.config({ + dispatch = function(self, action) + if type(action) == "function" then + action(self) + else + table.insert(actionHistory, action) + end + end, + + state = { + hello = { + world = Freeze.UnorderedMap.new({}), + } + } + }) + + describe("GIVEN a module script and builderFunction w/out parameters", function() + local hasBuilderFunctionRun = false + local receivedChannelArgument = nil + local mockGETChannels = GET(script, function(requestBuilder, channel) + hasBuilderFunctionRun = true + receivedChannelArgument = channel + + return requestBuilder("example.com"):path("v1"):path("channels"):id(channel):path("messages") + end) + + describe("GIVEN no parameters for a url", function() + it("SHOULD return a valid thunk", function() + local thunk = mockGETChannels.API("weather-channel") + expect(type(thunk)).to.equal("function") + end) + + describe("WHEN thunk is dispatched", function() + + it("SHOULD invoke builder function given to GET constructor", function() + roduxStore:dispatch(mockGETChannels.API("announcements")) + expect(hasBuilderFunctionRun).to.equal(true) + expect(receivedChannelArgument).to.equal("announcements") + end) + + it("SHOULD dispatch network status actions and payload action", function() + actionHistory = {} + roduxStore:dispatch(mockGETChannels.API("announcements")) + local action1 = actionHistory[1] + expect(action1).to.be.ok() + expect(action1.type).to.equal("networkStatus:hello.world") + expect(action1.status).to.equal(EnumNetworkStatus.Fetching) + end) + end) + end) + + describe("Action Creators", function() + it("SHOULD have a Succeeded action creator", function() + expect(mockGETChannels.Succeeded).to.be.ok() + local action = mockGETChannels.Succeeded({}, {}, {}) + expect(action).to.be.ok() + expect(action.ids).to.be.ok() + expect(action.responseBody).to.be.ok() + expect(action.namedIds).to.be.ok() + end) + + it("SHOULD have a Failed action creator", function() + expect(mockGETChannels.Failed).to.be.ok() + local action = mockGETChannels.Failed({}, {}, {}) + expect(action).to.be.ok() + expect(action.ids).to.be.ok() + expect(action.error).to.be.ok() + expect(action.namedIds).to.be.ok() + end) + end) + + describe("getStatus", function() + it("SHOULD return Done for successful network responses", function() + local store = mockStore.config({ + state = { + hello = { + world = Freeze.UnorderedMap.new({ + -- key mapper would usually generate this + ["example.com/v1/channels/faq/messages"] = "testingStatus", + }), + } + } + }) + + local status = mockGETChannels.getStatus(store:getState(), "faq") + expect(status).to.equal("testingStatus") + end) + + it("SHOULD throw for non-GET request types", function() + local POST = require(root.makeRequestApi)({ + keyPath = "hello.world", + networkImpl = mockNetworkImpl, + }, "POST") + + expect(function() + POST.getStatus({}, "testing") + end).to.throw() + end) + end) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApiThrows.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApiThrows.spec.lua new file mode 100644 index 0000000..a9eba5e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApiThrows.spec.lua @@ -0,0 +1,84 @@ +return function() + local root = script.Parent + local Freeze = require(root.Freeze) + local EnumNetworkStatus = require(root.NetworkStatus.EnumNetworkStatus) + local Promise = require(root.Promise) + local mockNetworkImpl = function(url, method, options) + return Promise.reject("networkError") + end + local mockStore = require(root.mockStore) + + local GET = require(root.makeRequestApi)({ + methodType = "GET", + keyPath = "hello.world", + networkImpl = mockNetworkImpl, + }) + + local function noOpt() + end + + describe("GIVEN a store", function() + local actionHistory = {} + local roduxStore = mockStore.config({ + dispatch = function(self, action) + if type(action) == "function" then + return action(self) + else + table.insert(actionHistory, action) + end + end, + + state = { + hello = { + world = Freeze.UnorderedMap.new({}), + } + } + }) + + describe("GIVEN a module script and builderFunction w/out parameters", function() + local hasBuilderFunctionRun = false + local receivedChannelArgument = nil + local mockGETChannels = GET(script, function(requestBuilder, channel) + hasBuilderFunctionRun = true + receivedChannelArgument = channel + + return requestBuilder("example.com"):path("v1"):path("channels"):id(channel):path("messages") + end) + + describe("WHEN thunk is dispatched", function() + it("SHOULD invoke builder function given to GET constructor", function() + roduxStore:dispatch(mockGETChannels.API("announcements")):catch(noOpt) + expect(hasBuilderFunctionRun).to.equal(true) + expect(receivedChannelArgument).to.equal("announcements") + end) + + it("SHOULD dispatch network status actions and payload action", function() + actionHistory = {} + roduxStore:dispatch(mockGETChannels.API("announcements")):catch(noOpt) + local action1 = actionHistory[1] + expect(action1).to.be.ok() + expect(action1.type).to.equal("networkStatus:hello.world") + expect(action1.status).to.equal(EnumNetworkStatus.Fetching) + end) + + it("SHOULD dispatch a failed network status action", function() + actionHistory = {} + roduxStore:dispatch(mockGETChannels.API("announcements")):catch(noOpt) + local action2 = actionHistory[2] + expect(action2).to.be.ok() + local isFound = string.find(action2.type, "Failed") + expect(isFound).to.be.ok() + end) + + it("SHOULD allow the promise to be caught", function() + local itWasCaught = false + roduxStore:dispatch(mockGETChannels.API("announcements")):catch(function() + itWasCaught = true + end) + + assert(itWasCaught, "Promise was not able to be caught when network fails") + end) + end) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/mockStore.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/mockStore.lua new file mode 100644 index 0000000..d67a0a8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/mockStore.lua @@ -0,0 +1,23 @@ +return { + config = function(options) + options = options or {} + + local state = options.state or {} + return { + dispatch = options.dispatch or function(self, thunk) + if type(thunk) == "function" then + thunk(self) + end + end, + + getState = function() + return state + end, + + setState = function(_, newState) + state = newState + end, + } + + end, +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/tutils.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/tutils.lua new file mode 100644 index 0000000..cb6c720 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/tutils.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["tutils"]["tutils"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/lock.toml b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/lock.toml new file mode 100644 index 0000000..cd90d99 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "tutils" +version = "0.1.0" +commit = "0be577dbb47de6c7821d1e3d1acf0b40a292689f" +source = "git+https://github.rbx.com/roblox/tutils#master" diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/checkListConsistency.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/checkListConsistency.lua new file mode 100644 index 0000000..0f71828 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/checkListConsistency.lua @@ -0,0 +1,37 @@ +--[[ + Returns false if the given table has any of the following: + - a key that is neither a number or a string + - a mix of number and string keys + - number keys which are not exactly 1..#t +]] +return function(t) + local containsNumberKey = false + local containsStringKey = false + local numberConsistency = true + + local index = 1 + for x, _ in pairs(t) do + if type(x) == 'string' then + containsStringKey = true + elseif type(x) == 'number' then + if index ~= x then + numberConsistency = false + end + containsNumberKey = true + else + return false + end + + if containsStringKey and containsNumberKey then + return false + end + + index = index + 1 + end + + if containsNumberKey then + return numberConsistency + end + + return true +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/checkListConsistency.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/checkListConsistency.spec.lua new file mode 100644 index 0000000..705435f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/checkListConsistency.spec.lua @@ -0,0 +1,41 @@ +return function() + local checkListConsistency = require(script.Parent.checkListConsistency) + + describe("WHEN given a valid table", function() + it("SHOULD return true for lists", function() + expect(checkListConsistency({ 1, 2, 3 })).to.equal(true) + end) + + it("SHOULD return false for lists with holes", function() + local list = { + [1] = true, + [2] = nil, + [3] = true, + } + expect(checkListConsistency(list)).to.equal(false) + end) + + it("SHOULD return true for dictionary with string keys", function() + local dictionary = { + foo = "bar", + hello = "world", + } + expect(checkListConsistency(dictionary)).to.equal(true) + end) + + it("SHOULD return false for dictionary with mixed keys", function() + local dictionary = { + foo = "bar", + [100] = "hundred", + } + expect(checkListConsistency(dictionary)).to.equal(false) + end) + + it("SHOULD return false for dictionary with keys that are not a string or number", function() + local dictionary = { + [{}] = true, + } + expect(checkListConsistency(dictionary)).to.equal(false) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepCopy.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepCopy.lua new file mode 100644 index 0000000..9145b3e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepCopy.lua @@ -0,0 +1,27 @@ +--[[ + Returns a deep copy of the given table. + Both the keys and the values of the table are deep copied. + Metatable of the table is copied. + If table is used as key in the input table, + the deep copy will not be deepEqual to the original. +]] + +local function deepCopy(A, seen) + if type(A) ~= 'table' then + return A + end + + if seen and seen[A] then + return seen[A] + end + + local alreadySeen = seen or {} + local newTable = setmetatable({}, getmetatable(A)) + alreadySeen[A] = newTable + for key, value in pairs(A) do + newTable[deepCopy(key, alreadySeen)] = deepCopy(value, alreadySeen) + end + return newTable +end + +return deepCopy diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepCopy.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepCopy.spec.lua new file mode 100644 index 0000000..2b276ac --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepCopy.spec.lua @@ -0,0 +1,66 @@ +return function() + local deepCopy = require(script.Parent.deepCopy) + local deepEqual = require(script.Parent.deepEqual) + + local function deepCopyAndCompare(value) + local aDeepCopyOfValue = deepCopy(value) + expect(deepEqual(value, aDeepCopyOfValue)).to.equal(true) + end + + it("SHOULD work for primitive data types", function() + deepCopyAndCompare(true) + deepCopyAndCompare(false) + deepCopyAndCompare(nil) + deepCopyAndCompare(100) + deepCopyAndCompare("Deep Copy") + end) + + local table1 = { + num = 1, + innerTable = { + innerString = "str" + } + } + local table2 = { + num = 1, + innerTable = { + innerString = "str", + innerInnerTable = table1, + }, + } + it("SHOULD correctly copy table without table as key ", function() + deepCopyAndCompare(table2) + + local deepCopyOfTable2 = deepCopy(table2) + + expect(table2.innerTable).to.never.equal(deepCopyOfTable2.innerTable) + expect(deepEqual(table2.innerTable, deepCopyOfTable2.innerTable)).to.equal(true) + + expect(table2.innerTable.innerInnerTable).to.never.equal(deepCopyOfTable2.innerTable.innerInnerTable) + expect(deepEqual(table2.innerTable.innerInnerTable, deepCopyOfTable2.innerTable.innerInnerTable)).to.equal(true) + end) + + local table3 = { + [table1] = table2, + } + it("SHOULD correctly copy table with table as key", function() + local deepCopyOfTable3 = deepCopy(table3) + expect(deepEqual(table3, deepCopyOfTable3)).to.equal(false) + + local table3Key, table3Value = next(table3) + local deepCopyOfTable3Key, deepCopyOfTable3Value = next(deepCopyOfTable3) + expect(table3Key).to.never.equal(deepCopyOfTable3Key) + expect(deepEqual(table3Key, deepCopyOfTable3Key)).to.equal(true) + expect(table3Value).to.never.equal(deepCopyOfTable3Value) + expect(deepEqual(table3Value, deepCopyOfTable3Value)).to.equal(true) + end) + + it("SHOULD create only one copy of a table in the table", function() + local deepCopyOfTable3 = deepCopy(table3) + local key, value = next(deepCopyOfTable3) + + expect(key).to.never.be.equal(table1) + expect(key).to.be.equal(value.innerTable.innerInnerTable) + expect(deepEqual(key, value.innerTable.innerInnerTable)).to.equal(true) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepEqual.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepEqual.lua new file mode 100644 index 0000000..4f11317 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepEqual.lua @@ -0,0 +1,46 @@ +--[[ + Takes two tables A and B, returns if they are deeply equal. ignoreMetatables specifies if metatables should be ignored + in the deep compare + Assumes tables do not have self-references +]] + +local function deepEqual(A, B, ignoreMetatables) + if A == B then + return true + end + local AType = type(A) + local BType = type(B) + if AType ~= BType then + return false + end + if AType ~= "table" then + return false + end + + if not ignoreMetatables then + local mt1 = getmetatable(A) + if mt1 and mt1.__eq then + --compare using built in method + return A == B + end + end + + local keySet = {} + + for key1, value1 in pairs(A) do + local value2 = B[key1] + if value2 == nil or not deepEqual(value1, value2, ignoreMetatables) then + return false + end + keySet[key1] = true + end + + for key2, _ in pairs(B) do + if not keySet[key2] then + return false + end + end + return true +end + +return deepEqual diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepEqual.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepEqual.spec.lua new file mode 100644 index 0000000..c45bb8e --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepEqual.spec.lua @@ -0,0 +1,55 @@ +return function() + local deepEqual = require(script.Parent.deepEqual) + + it("SHOULD work for primitive data types", function() + expect(deepEqual(1, 1)).to.equal(true) + expect(deepEqual("str1", "str1")).to.equal(true) + expect(deepEqual(1, 2)).to.equal(false) + expect(deepEqual("str1", "str2")).to.equal(false) + end) + + it("SHOULD correctly identifies deeply-equal tables", function() + local table1 = { + num = 1, + innerTable = { + innerString = "str" + } + } + local table2 = { + num = 1, + innerTable = { + innerString = "str" + } + } + expect(deepEqual(table1, table2)).to.equal(true) + end) + + it("SHOULD correctly rejects non-deeply-equal tables", function() + local table1 = { + num = 1, + innerTable = { + innerString = "str" + } + } + local table2 = { + num = 1, + innerTable = { + innerString = "differentStr" + } + } + expect(deepEqual(table1, table2)).to.equal(false) + local table3 = { + num = 1, + innerTable = { + innerString = "str" + } + } + local table4 = { + num = 1, + innerTableWithDifferentKey = { + innerString = "str" + } + } + expect(deepEqual(table3, table4)).to.equal(false) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/equalKey.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/equalKey.lua new file mode 100644 index 0000000..0c9c702 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/equalKey.lua @@ -0,0 +1,10 @@ +--[[ + Takes two tables A, B and a key, returns if two tables have the same value at key +]] + +return function(A, B, key) + if A and B and key and key ~= "" and A[key] and B[key] and A[key] == B[key] then + return true + end + return false +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/equalKey.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/equalKey.spec.lua new file mode 100644 index 0000000..89a887a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/equalKey.spec.lua @@ -0,0 +1,78 @@ +return function() + local equalKey = require(script.Parent.equalKey) + + describe("WHEN given nil values", function() + it("SHOULD return false", function() + local testCase = function(tableA, tableB) + expect(equalKey(tableA, tableB)).to.equal(false) + expect(equalKey(tableA, tableB, "")).to.equal(false) + expect(equalKey(tableA, tableB, "key1")).to.equal(false) + end + + testCase(nil, nil) + testCase(nil, {}) + testCase({}, nil) + end) + end) + + describe("WHEN given table values", function() + it("SHOULD return false if key does not exist in either table (empty tables)", function() + local tableA, tableB = {}, {} + expect(equalKey(tableA, tableB)).to.equal(false) + expect(equalKey(tableA, tableB, "")).to.equal(false) + expect(equalKey(tableA, tableB, "key1")).to.equal(false) + end) + + it("SHOULD return false if key does not exist in either table (single keys)", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key2 = "value1", + } + expect(equalKey(tableA, tableB)).to.equal(false) + expect(equalKey(tableA, tableB, "")).to.equal(false) + expect(equalKey(tableA, tableB, "key1")).to.equal(false) + end) + + describe("WHEN key exists in both tables", function() + it("SHOULD return true if value of key is the same", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key1 = "value1", + } + expect(equalKey(tableA, tableB)).to.equal(false) + expect(equalKey(tableA, tableB, "")).to.equal(false) + expect(equalKey(tableA, tableB, "key1")).to.equal(true) + end) + + it("SHOULD return false if value of key is not the same", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key1 = "value2", + } + expect(equalKey(tableA, tableB)).to.equal(false) + expect(equalKey(tableA, tableB, "")).to.equal(false) + expect(equalKey(tableA, tableB, "key1")).to.equal(false) + end) + end) + + it("should return whether tables are equal to each other at key", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key1 = "value1", + key2 = "value2", + } + expect(equalKey(tableA, tableB)).to.equal(false) + expect(equalKey(tableA, tableB, "")).to.equal(false) + expect(equalKey(tableA, tableB, "key1")).to.equal(true) + expect(equalKey(tableA, tableB, "key2")).to.equal(false) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/fieldCount.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/fieldCount.lua new file mode 100644 index 0000000..c8ace7a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/fieldCount.lua @@ -0,0 +1,10 @@ +--[[ + Takes a table and returns the field count +]] +return function(t) + local fieldCount = 0 + for _ in pairs(t) do + fieldCount = fieldCount + 1 + end + return fieldCount +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/fieldCount.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/fieldCount.spec.lua new file mode 100644 index 0000000..9ef2bdb --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/fieldCount.spec.lua @@ -0,0 +1,24 @@ +return function() + local fieldCount = require(script.Parent.fieldCount) + + describe("WHEN given empty tables", function() + it("SHOULD return zero", function() + expect(fieldCount({})).to.equal(0) + end) + end) + + describe("WHEN given a valid dictionary", function() + it("should return table's field count", function() + local table1 = { + key1 = "value1", + } + expect(fieldCount(table1)).to.equal(1) + + local table2 = { + key1 = "value1", + key2 = "value2", + } + expect(fieldCount(table2)).to.equal(2) + end) + end) +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/init.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/init.lua new file mode 100644 index 0000000..de9e651 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/init.lua @@ -0,0 +1,16 @@ +--[[ + Provides functions for comparing and printing lua tables. +]] + +return { + checkListConsistency = require(script.checkListConsistency), + deepEqual = require(script.deepEqual), + deepCopy = require(script.deepCopy), + equalKey = require(script.equalKey), + fieldCount = require(script.fieldCount), + listDifferences = require(script.listDifferences), + print = require(script.print)(print), + shallowEqual = require(script.shallowEqual), + tableDifference = require(script.tableDifference), + toString = require(script.toString), +} diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/listDifferences.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/listDifferences.lua new file mode 100644 index 0000000..4ae41b7 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/listDifferences.lua @@ -0,0 +1,35 @@ +local tableDifference = require(script.Parent.tableDifference) + +--[[ + Takes a list and returns a table whose + keys are elements of the list and whose + values are all true +]] +local function membershipTable(list) + local result = {} + for i = 1, #list do + result[list[i]] = true + end + return result +end + + +--[[ + Takes a table and returns a list of keys in that table +]] +local function listOfKeys(t) + local result = {} + for key,_ in pairs(t) do + table.insert(result, key) + end + return result +end + + +--[[ + Takes two lists A and B, returns a new list of elements of A + which are not in B +]] +return function(A, B) + return listOfKeys(tableDifference(membershipTable(A), membershipTable(B))) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/listDifferences.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/listDifferences.spec.lua new file mode 100644 index 0000000..56a517a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/listDifferences.spec.lua @@ -0,0 +1,46 @@ +return function() + local listDifferences = require(script.Parent.listDifferences) + local expectTable = require(script.Parent.unitTests.expectTable) + + describe("GIVEN two tables", function() + describe("WHEN the tables are lists", function() + it("SHOULD return an empty table if the lists share the same values", function() + local listA = { 1, 2, 3 } + local listB = { 1, 2, 3 } + expectTable(listDifferences(listA, listB)).toEqual({}) + end) + + it("SHOULD return a list of delta values if first parameter has extra values", function() + local listA = { 1, 2, 3, 4 } + local listB = { 1, 2, 3 } + expectTable(listDifferences(listA, listB)).toEqual({ 4 }) + end) + + it("SHOULD return an empty table if the second parameter has extra values", function() + local listA = { 1, 2, 3 } + local listB = { 1, 2, 3, 4 } + expectTable(listDifferences(listA, listB)).toEqual({}) + end) + end) + + describe("WHEN the tables are dictionaries", function() + it("SHOULD always return an empty table (same dictionaries)", function() + local dictionaryA = { foo = "bar" } + local dictionaryB = { foo = "bar" } + expectTable(listDifferences(dictionaryA, dictionaryB)).toEqual({}) + end) + + it("SHOULD always return an empty table (second has extra keys)", function() + local dictionaryA = { foo = "bar" } + local dictionaryB = { foo = "bar", hello = "world" } + expectTable(listDifferences(dictionaryA, dictionaryB)).toEqual({}) + end) + + it("SHOULD always return an empty table (first has extra keys)", function() + local dictionaryA = { foo = "bar" } + local dictionaryB = { foo = "bar", hello = "world" } + expectTable(listDifferences(dictionaryA, dictionaryB)).toEqual({}) + end) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/print.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/print.lua new file mode 100644 index 0000000..43b4cee --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/print.lua @@ -0,0 +1,73 @@ +return function(print) + local function makeKeyString(key) + if type(key) == "string" then + return string.format("%s", key) + else + return string.format("[%s]", tostring(key)) + end + end + + local function makeValueString(value) + local valueType = type(value) + if valueType == "string" then + return string.format("%q", value) + elseif valueType == "function" or valueType == "table" then + return string.format("<%s>", tostring(value)) + else + return string.format("%s", tostring(value)) + end + end + + local function printKeypair(key, value, indentStr, comment) + local keyString = makeKeyString(key) + local valueString = makeValueString(value) + + local commentStr = comment and string.format(" -- %s", comment) or "" + print(string.format("%s%s = %s,%s", indentStr, keyString, valueString, commentStr)) + end + + --[[ + For debugging. Prints the table on multiple lines to overcome log-line length + limitations which are otherwise necessary for performance. Use sparingly. + ]] + return function(t, indent) + indent = indent or ' ' + + if type(t) ~= "table" then + error("tutils.Print must be passed a table", 2) + end + + -- For cycle detection + local printedTables = {} + + local function recurse(subTable, tableKey, level) + -- Prevent cycles by keeping track of what tables we have printed + printedTables[subTable] = true + + local indentStr = string.rep(indent, level) + local valueIndentStr = string.rep(indent, level + 1) + + if tableKey then + print(string.format("%s%s = %s {", indentStr, makeKeyString(tableKey), makeValueString(subTable))) + else + print(string.format("%s%s {", indentStr, makeValueString(subTable))) + end + + for key, value in pairs(subTable) do + if type(value) == "table" then + if printedTables[value] then + printKeypair(key, value, valueIndentStr, "Possible cycle") + else + recurse(value, key, level + 1) + end + else + printKeypair(key, value, valueIndentStr) + end + end + + print(string.format("%s}%s", indentStr, (level > 0 and "," or ""))) + end + + recurse(t, nil, 0) + end +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/print.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/print.spec.lua new file mode 100644 index 0000000..53e984c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/print.spec.lua @@ -0,0 +1,92 @@ +return function() + local history = {} + local mockPrint = function(...) + local str = table.concat({ ... }, " " ) + table.insert(history, str) + end + local clearHistory = function() + history = {} + end + + local print = require(script.Parent.print)(mockPrint) + + describe("GIVEN a table", function() + it("SHOULD handle an empty table appropriately", function() + print({}) + + local firstLine = history[1] + expect(firstLine:sub(-1)).to.equal("{") + local secondLine = history[2] + expect(secondLine).to.equal("}") + + clearHistory() + end) + + it("SHOULD handle a simple list appropriately", function() + print({ 1, 2, 3 }) + + local firstLine = history[1] + expect(firstLine:sub(-1)).to.equal("{") + local lastLine = history[#history] + expect(lastLine).to.equal("}") + + local firstElement = history[2] + expect(firstElement).to.equal(" [1] = 1,") + local secondElement = history[3] + expect(secondElement).to.equal(" [2] = 2,") + local thirdElement = history[4] + expect(thirdElement).to.equal(" [3] = 3,") + + clearHistory() + end) + + it("SHOULD handle a simple dictionary appropriately", function() + print({ foo = "bar", hello = "world" }) + + local firstLine = history[1] + expect(firstLine:sub(-1)).to.equal("{") + local lastLine = history[#history] + expect(lastLine).to.equal("}") + + local firstElement = history[2] + expect(firstElement).to.equal(" hello = \"world\",") + local secondElement = history[3] + expect(secondElement).to.equal(" foo = \"bar\",") + + clearHistory() + end) + + it("SHOULD handle a cyclic dictionary by printing a warning", function() + local tab = {} + tab.element1 = tab + print(tab) + + local firstLine = history[1] + expect(firstLine:sub(-1)).to.equal("{") + local lastLine = history[#history] + expect(lastLine).to.equal("}") + + local firstElement = history[2] + local isFound = firstElement:find("Possible cycle$") + expect(isFound).to.be.ok() + + clearHistory() + end) + end) + + describe("GIVEN anything else", function() + it("SHOULD throw", function() + expect(function() + print(1) + end).to.throw() + + expect(function() + print(true) + end).to.throw() + + expect(function() + print("hello world") + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/shallowEqual.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/shallowEqual.lua new file mode 100644 index 0000000..f20eb41 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/shallowEqual.lua @@ -0,0 +1,27 @@ +--[[ + Takes two tables A and B, returns if they have the same key-value pairs + Except ignored keys +]] +return function(A, B, ignore) + if not A or not B then + return false + elseif A == B then + return true + end + if not ignore then + ignore = {} + end + + for key, value in pairs(A) do + if B[key] ~= value and not ignore[key] then + return false + end + end + for key, value in pairs(B) do + if A[key] ~= value and not ignore[key] then + return false + end + end + + return true +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/shallowEqual.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/shallowEqual.spec.lua new file mode 100644 index 0000000..724eb26 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/shallowEqual.spec.lua @@ -0,0 +1,74 @@ +return function() + local shallowEqual = require(script.Parent.shallowEqual) + + describe("WHEN given nil values", function() + it("SHOULD return false if both values are nil", function() + expect(shallowEqual(nil, nil)).to.equal(false) + end) + + it("SHOULD return false if either value is nil", function() + expect(shallowEqual(nil, {})).to.equal(false) + expect(shallowEqual({}, nil)).to.equal(false) + end) + end) + + describe("WHEN given similar table values", function() + it("SHOULD return true for two empty tables", function() + expect(shallowEqual({}, {})).to.equal(true) + end) + + it("SHOULD return true for one key dictionaries", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key1 = "value1", + } + expect(shallowEqual(tableA, tableB)).to.equal(true) + end) + end) + + describe("WHEN given dissimilar table values", function() + it("SHOULD return false for same key, different values", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key1 = "value2", + } + expect(shallowEqual(tableA, tableB)).to.equal(false) + end) + + it("SHOULD return false for different keys, same values", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key2 = "value1", + } + expect(shallowEqual(tableA, tableB)).to.equal(false) + end) + + it("SHOULD return false for different keys, different values", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key2 = "value2", + } + expect(shallowEqual(tableA, tableB)).to.equal(false) + end) + + it("SHOULD return false for extra keys", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key1 = "value1", + key2 = "value2", + } + expect(shallowEqual(tableA, tableB)).to.equal(false) + end) + end) + +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/tableDifference.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/tableDifference.lua new file mode 100644 index 0000000..9f4678a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/tableDifference.lua @@ -0,0 +1,15 @@ +--[[ + Takes two tables A and B, returns a new table with elements of A + which are either not keys in B or have a different value in B +]] +return function(A, B) + local new = {} + + for key, value in pairs(A) do + if B[key] ~= A[key] then + new[key] = value + end + end + + return new +end diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/tableDifference.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/tableDifference.spec.lua new file mode 100644 index 0000000..e112b41 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/tableDifference.spec.lua @@ -0,0 +1,27 @@ +return function() + local tableDifference = require(script.Parent.tableDifference) + local expectTable = require(script.Parent.unitTests.expectTable) + + describe("WHEN given two tables", function() + it("SHOULD return an empty table if lists are the same", function() + local listA = {1, 2, 3} + local listB = {1, 2, 3} + + expectTable(tableDifference(listA, listB)).toEqual({}) + end) + + it("SHOULD return a dictionary of the key value if a list has an extra value", function() + local listA = {1, 2, 3, 4} + local listB = {1, 2, 3} + + expectTable(tableDifference(listA, listB)).toEqual({ [4] = 4 }) + end) + + it("SHOULD return a dictionary of the key value if a dictionary has an extra value", function() + local dictionaryA = { foo = "bar", hello = "world" } + local dictionaryB = { foo = "bar" } + + expectTable(tableDifference(dictionaryA, dictionaryB)).toEqual({ hello = "world" }) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/toString.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/toString.lua new file mode 100644 index 0000000..f6a5a24 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/toString.lua @@ -0,0 +1,36 @@ +local checkListConsistency = require(script.Parent.checkListConsistency) + +--[[ + Serializes the given table to a reasonable string that might even interpret as lua. +]] +local function recursiveToString(t, indent) + indent = indent or '' + + if type(t) == 'table' then + local result = "" + if not checkListConsistency(t) then + result = result .. "-- WARNING: this table fails the list consistency test\n" + end + result = result .. "{\n" + for k,v in pairs(t) do + if type(k) == 'string' then + result = result + .. " " + .. indent + .. tostring(k) + .. " = " + .. recursiveToString(v, " "..indent) + ..";\n" + end + if type(k) == 'number' then + result = result .. " " .. indent .. recursiveToString(v, " "..indent)..",\n" + end + end + result = result .. indent .. "}" + return result + else + return tostring(t) + end +end + +return recursiveToString \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/toString.spec.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/toString.spec.lua new file mode 100644 index 0000000..e52104a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/toString.spec.lua @@ -0,0 +1,40 @@ +return function() + local toString = require(script.Parent.toString) + + describe("WHEN given a table", function() + it("SHOULD handle simple lists", function() + local indent = "." + local result = toString({ 1, 2, 3 }, indent) + + expect(result).to.equal("{\n .1,\n .2,\n .3,\n.}") + end) + + it("SHOULD handle simple dictionaries", function() + local indent = "." + local result = toString({ hello = "world" }, indent) + + expect(result).to.equal("{\n .hello = world;\n.}") + end) + + it("SHOULD handle tables within tables", function() + local indent = "." + local result = toString({ {} }, indent) + + expect(result).to.equal("{\n .{\n .},\n.}") + end) + + it("SHOULD show a warning for mixed tables", function() + local result = toString({ 1, 2, hello = "world" }) + local findResult = result:find("WARNING: this table fails the list consistency test") + expect(findResult).to.be.ok() + end) + end) + + describe("WHEN given anything else", function() + it("SHOULD return the tostring equivalent", function() + expect(toString(1)).to.equal(tostring(1)) + expect(toString(true)).to.equal(tostring(true)) + expect(toString("hello")).to.equal(tostring("hello")) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/unitTests/expectTable.lua b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/unitTests/expectTable.lua new file mode 100644 index 0000000..a25b85a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/unitTests/expectTable.lua @@ -0,0 +1,15 @@ +local shallowEqual = require(script.Parent.Parent.shallowEqual) +local toString = require(script.Parent.Parent.toString) + +local function expectTable(tab) + return { + toEqual = function(value) + assert( + shallowEqual(tab, value), + string.format("expected: %s\ninstead got: %s", toString(value), toString(tab)) + ) + end, + } +end + +return expectTable \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/enumerate.lua b/Client2020/ExtraContent/LuaPackages/Packages/enumerate.lua new file mode 100644 index 0000000..73eb2b2 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/enumerate.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_enumerate"]["enumerate"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/t.lua b/Client2020/ExtraContent/LuaPackages/Packages/t.lua new file mode 100644 index 0000000..ef185ec --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Packages/tutils.lua b/Client2020/ExtraContent/LuaPackages/Packages/tutils.lua new file mode 100644 index 0000000..60f9805 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Packages/tutils.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["tutils"]["tutils"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/PolicyProvider.lua b/Client2020/ExtraContent/LuaPackages/PolicyProvider.lua new file mode 100644 index 0000000..78cc600 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/PolicyProvider.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.PolicyProvider) diff --git a/Client2020/ExtraContent/LuaPackages/PremiumUpsellDeps.lua b/Client2020/ExtraContent/LuaPackages/PremiumUpsellDeps.lua new file mode 100644 index 0000000..40d4418 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/PremiumUpsellDeps.lua @@ -0,0 +1,12 @@ + +--[[ + Proxy package for dependencies for PremiumUpsellDeps. +]] + +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.PremiumUpsellDeps) diff --git a/Client2020/ExtraContent/LuaPackages/Promise.lua b/Client2020/ExtraContent/LuaPackages/Promise.lua new file mode 100644 index 0000000..be7c277 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Promise.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Promise) diff --git a/Client2020/ExtraContent/LuaPackages/PurchasePrompt.lua b/Client2020/ExtraContent/LuaPackages/PurchasePrompt.lua new file mode 100644 index 0000000..e90a85d --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/PurchasePrompt.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.PurchasePrompt) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Result.lua b/Client2020/ExtraContent/LuaPackages/Result.lua new file mode 100644 index 0000000..bcdcb3c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Result.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Result) diff --git a/Client2020/ExtraContent/LuaPackages/Rhodium.lua b/Client2020/ExtraContent/LuaPackages/Rhodium.lua new file mode 100644 index 0000000..7e41283 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Rhodium.lua @@ -0,0 +1,7 @@ +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Dev.Rhodium) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Roact.lua b/Client2020/ExtraContent/LuaPackages/Roact.lua new file mode 100644 index 0000000..abc1a07 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Roact.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Roact) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/RoactNavigation.lua b/Client2020/ExtraContent/LuaPackages/RoactNavigation.lua new file mode 100644 index 0000000..cfebc1a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/RoactNavigation.lua @@ -0,0 +1,6 @@ +local CorePackages = game:GetService("CorePackages") +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.RoactNavigation) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/RoactRodux.lua b/Client2020/ExtraContent/LuaPackages/RoactRodux.lua new file mode 100644 index 0000000..117fa09 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/RoactRodux.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.RoactRodux) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/RoactUtilities/.robloxrc b/Client2020/ExtraContent/LuaPackages/RoactUtilities/.robloxrc new file mode 100644 index 0000000..321fd28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/RoactUtilities/.robloxrc @@ -0,0 +1,12 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "ImplicitReturn": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/RoactUtilities/ExternalEventConnection.lua b/Client2020/ExtraContent/LuaPackages/RoactUtilities/ExternalEventConnection.lua new file mode 100644 index 0000000..0c1bc58 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/RoactUtilities/ExternalEventConnection.lua @@ -0,0 +1,51 @@ +--[[ + A component that establishes a connection to a Roblox event when it is rendered. +]] + +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) +local ExternalEventConnection = Roact.Component:extend("ExternalEventConnection") + +function ExternalEventConnection:init() + self.connection = nil +end + +--[[ + Render the child component so that ExternalEventConnections can be nested like so: + + Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputBegan, + callback = inputBeganCallback, + }, { + Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputEnded, + callback = inputChangedCallback, + }) + }) +]] +function ExternalEventConnection:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +function ExternalEventConnection:didMount() + local event = self.props.event + local callback = self.props.callback + + self.connection = event:Connect(callback) +end + +function ExternalEventConnection:didUpdate(oldProps) + if self.props.event ~= oldProps.event or self.props.callback ~= oldProps.callback then + self.connection:Disconnect() + + self.connection = self.props.event:Connect(self.props.callback) + end +end + +function ExternalEventConnection:willUnmount() + self.connection:Disconnect() + + self.connection = nil +end + +return ExternalEventConnection \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/RoactUtilities/ExternalEventConnection.spec.lua b/Client2020/ExtraContent/LuaPackages/RoactUtilities/ExternalEventConnection.spec.lua new file mode 100644 index 0000000..7a80b7f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/RoactUtilities/ExternalEventConnection.spec.lua @@ -0,0 +1,91 @@ +return function () + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local ExternalEventConnection = require(script.Parent.ExternalEventConnection) + + it("if mounted, should call the callback when the event is triggered", function() + local event = Instance.new("BindableEvent") + local count = 0 + + local element = Roact.createElement(ExternalEventConnection, { + event = event.Event, + callback = function() + count = count + 1 + end, + }) + + local RoactInstance = Roact.mount(element) + event:Fire() + + expect(count).to.equal(1) + + Roact.unmount(RoactInstance) + event:Fire() + + expect(count).to.equal(1) + + event:Destroy() + end) + + it("should handle updating the callback or event", function() + local firstEvent = Instance.new("BindableEvent") + local secondEvent = Instance.new("BindableEvent") + local count = 0 + local changeState + + local EventContainer = Roact.Component:extend("EventContainer") + + function EventContainer:init() + self.state = { + event = firstEvent.Event, + callback = function() + count = count + 1 + end, + } + end + + function EventContainer:render() + return Roact.createElement(ExternalEventConnection, { + event = self.state.event, + callback = self.state.callback, + }) + end + + function EventContainer:didMount() + changeState = function(newState) + self:setState(newState) + end + end + + function EventContainer:willUnmount() + changeState = nil + end + + Roact.mount(Roact.createElement(EventContainer)) + firstEvent:Fire() + + expect(count).to.equal(1) + + changeState({ + event = secondEvent.Event, + }) + firstEvent:Fire() + + expect(count).to.equal(1) + + secondEvent:Fire() + + expect(count).to.equal(2) + + changeState({ + callback = function() + -- this is intentionally blank + end, + }) + secondEvent:Fire() + + expect(count).to.equal(2) + firstEvent:Destroy() + secondEvent:Destroy() + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Rodux.lua b/Client2020/ExtraContent/LuaPackages/Rodux.lua new file mode 100644 index 0000000..c300b31 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Rodux.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Rodux) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/Symbol.lua b/Client2020/ExtraContent/LuaPackages/Symbol.lua new file mode 100644 index 0000000..16d4a1a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/Symbol.lua @@ -0,0 +1,7 @@ +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.SymbolImpl) + +return require(CorePackages.SymbolImpl) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/SymbolImpl/.robloxrc b/Client2020/ExtraContent/LuaPackages/SymbolImpl/.robloxrc new file mode 100644 index 0000000..321fd28 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/SymbolImpl/.robloxrc @@ -0,0 +1,12 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "ImplicitReturn": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/SymbolImpl/Symbol.lua b/Client2020/ExtraContent/LuaPackages/SymbolImpl/Symbol.lua new file mode 100644 index 0000000..8b6adaf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/SymbolImpl/Symbol.lua @@ -0,0 +1,43 @@ +--[[ + A 'Symbol' is an opaque marker type that can be used to signify unique + statuses. Symbols have the type 'userdata', but when printed to the console, + the name of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +--[[ + Create an unnamed Symbol. Usually, you should create a named Symbol using + Symbol.named(name) +]] +function Symbol.unnamed() + local self = newproxy(true) + + getmetatable(self).__tostring = function() + return "Unnamed Symbol" + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/SymbolImpl/Symbol.spec.lua b/Client2020/ExtraContent/LuaPackages/SymbolImpl/Symbol.spec.lua new file mode 100644 index 0000000..03440f6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/SymbolImpl/Symbol.spec.lua @@ -0,0 +1,45 @@ +return function() + local Symbol = require(script.Parent.Symbol) + + describe("named", function() + it("should give an opaque object", function() + local symbol = Symbol.named("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = Symbol.named("foo") + local location = tostring(symbol):find("foo") + expect(location).to.be.ok() + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.named("abc") + local symbolB = Symbol.named("abc") + + expect(symbolA).never.to.equal(symbolB) + end) + end) + + describe("unnamed", function() + it("should give an opaque object", function() + local symbol = Symbol.unnamed() + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to some string", function() + local symbol = Symbol.unnamed() + + expect(tostring(symbol)).to.be.a("string") + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.unnamed() + local symbolB = Symbol.unnamed() + + expect(symbolA).never.to.equal(symbolB) + end) + end) +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/SymbolImpl/init.lua b/Client2020/ExtraContent/LuaPackages/SymbolImpl/init.lua new file mode 100644 index 0000000..6aaa8ef --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/SymbolImpl/init.lua @@ -0,0 +1 @@ +return require(script.Symbol) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/TestEZ.lua b/Client2020/ExtraContent/LuaPackages/TestEZ.lua new file mode 100644 index 0000000..d49f69c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/TestEZ.lua @@ -0,0 +1,7 @@ +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Dev.TestEZ) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidation.lua b/Client2020/ExtraContent/LuaPackages/UGCValidation.lua new file mode 100644 index 0000000..c2295dd --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidation.lua @@ -0,0 +1,9 @@ +-- Shared code used by Toolbox and RCC for validating UGC catalog uploads + +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.UGCValidationImpl) + +return require(CorePackages.UGCValidationImpl) \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/.robloxrc b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/.robloxrc new file mode 100644 index 0000000..81a5da6 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/.robloxrc @@ -0,0 +1,12 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "DeprecatedGlobal": "fatal", + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/Constants.lua b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/Constants.lua new file mode 100644 index 0000000..685d103 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/Constants.lua @@ -0,0 +1,247 @@ +local CorePackages = game:GetService("CorePackages") + +local Cryo = require(CorePackages.Cryo) + +-- switch this to Cryo.List.toSet when available +local function convertArrayToTable(array) + local result = {} + for _, v in pairs(array) do + result[v] = true + end + return result +end + +local Constants = {} + +Constants.MAX_HAT_TRIANGLES = 4000 + +Constants.MAX_TEXTURE_SIZE = 256 + +Constants.MATERIAL_WHITELIST = convertArrayToTable({ + Enum.Material.Plastic, +}) + +Constants.BANNED_CLASS_NAMES = { + "Script", + "LocalScript", + "ModuleScript", + "ParticleEmitter", + "Fire", + "Smoke", + "Sparkles", +} + +Constants.R6_BODY_PARTS = { + "Torso", + "Left Leg", + "Right Leg", + "Left Arm", + "Right Arm", +} + +Constants.R15_BODY_PARTS = { + "UpperTorso", + "LowerTorso", + + "LeftUpperLeg", + "LeftLowerLeg", + "LeftFoot", + + "RightUpperLeg", + "RightLowerLeg", + "RightFoot", + + "LeftUpperArm", + "LeftLowerArm", + "LeftHand", + + "RightUpperArm", + "RightLowerArm", + "RightHand", +} + +Constants.EXTRA_BANNED_NAMES = { + "Head", + "HumanoidRootPart", + "Humanoid", +} + +if game:GetFastFlag("UGCExtraBannedNames") then + local extraBannedNames = { + "Body Colors", + "Shirt Graphic", + "Shirt", + "Pants", + "Health", + "Animate", + } + for _, name in ipairs(extraBannedNames) do + table.insert(Constants.EXTRA_BANNED_NAMES, name) + end +end + +Constants.BANNED_NAMES = convertArrayToTable(Cryo.Dictionary.join( + Constants.R6_BODY_PARTS, + Constants.R15_BODY_PARTS, + Constants.EXTRA_BANNED_NAMES +)) + +Constants.ASSET_STATUS = { + UNKNOWN = "Unknown", + REVIEW_PENDING = "ReviewPending", + MODERATED = "Moderated", +} + +-- https://confluence.rbx.com/display/AVATAR/UGC+Accessory+Max+Sizes +-- Measurements are doubled to account full size +-- boundsOffset is used when measurements are non-symmetrical +-- i.e. WaistAccessory is 3 behind, 2.5 front +Constants.ASSET_TYPE_INFO = {} + +Constants.ASSET_TYPE_INFO[Enum.AssetType.Hat] = { + attachmentNames = { "HatAttachment" }, + bounds = { + HatAttachment = { + size = Vector3.new(3, 4, 3), + }, + }, +} + +Constants.ASSET_TYPE_INFO[Enum.AssetType.HairAccessory] = { + attachmentNames = { "HairAttachment" }, + bounds = { + HairAttachment = { + size = Vector3.new(3, 5, 3.5), + offset = Vector3.new(0, -0.5, 0.25), + }, + }, +} + +local FACE_BOUNDS = { size = Vector3.new(3, 2, 2) } +Constants.ASSET_TYPE_INFO[Enum.AssetType.FaceAccessory] = { + attachmentNames = { "FaceFrontAttachment", "FaceCenterAttachment" }, + bounds = { + FaceFrontAttachment = FACE_BOUNDS, + FaceCenterAttachment = FACE_BOUNDS, + }, +} + +Constants.ASSET_TYPE_INFO[Enum.AssetType.NeckAccessory] = { + attachmentNames = { "NeckAttachment" }, + bounds = { + NeckAttachment = { size = Vector3.new(3, 3, 2) }, + }, +} + +local SHOULDER_BOUNDS = { size = Vector3.new(3, 3, 3) } +Constants.ASSET_TYPE_INFO[Enum.AssetType.ShoulderAccessory] = { + attachmentNames = { + "NeckAttachment", + "LeftCollarAttachment", + "RightCollarAttachment", + "LeftShoulderAttachment", + "RightShoulderAttachment", + }, + bounds = { + NeckAttachment = { size = Vector3.new(7, 3, 3) }, + LeftCollarAttachment = SHOULDER_BOUNDS, + RightCollarAttachment = SHOULDER_BOUNDS, + LeftShoulderAttachment = SHOULDER_BOUNDS, + RightShoulderAttachment = SHOULDER_BOUNDS, + }, +} + +Constants.ASSET_TYPE_INFO[Enum.AssetType.FrontAccessory] = { + attachmentNames = { "BodyFrontAttachment" }, + bounds = { + BodyFrontAttachment = { size = Vector3.new(3, 3, 3) }, + }, +} + +Constants.ASSET_TYPE_INFO[Enum.AssetType.BackAccessory] = { + attachmentNames = { "BodyBackAttachment" }, + bounds = { + BodyBackAttachment = { + size = Vector3.new(10, 7, 4.5), + offset = Vector3.new(0, 0, 0.75), + }, + }, +} + +local WAIST_BOUNDS = { + size = Vector3.new(4, 3.5, 7), + offset = Vector3.new(0, -0.25, 0), +} +Constants.ASSET_TYPE_INFO[Enum.AssetType.WaistAccessory] = { + attachmentNames = { + "WaistBackAttachment", + "WaistFrontAttachment", + "WaistCenterAttachment", + }, + bounds = { + WaistBackAttachment = WAIST_BOUNDS, + WaistFrontAttachment = WAIST_BOUNDS, + WaistCenterAttachment = WAIST_BOUNDS, + } +} + +Constants.PROPERTIES = { + Instance = { + Archivable = true, + }, + Attachment = { + Visible = false, + }, + SpecialMesh = { + MeshType = Enum.MeshType.FileMesh, + Offset = Vector3.new(0, 0, 0), + VertexColor = Vector3.new(1, 1, 1), + }, + BasePart = { + Anchored = false, + Color = BrickColor.new("Medium stone grey").Color, -- luacheck: ignore BrickColor + CollisionGroupId = 0, -- collision groups can change by place + CustomPhysicalProperties = Cryo.None, -- ensure CustomPhysicalProperties is _not_ defined + Elasticity = 0.5, + Friction = 0.3, + LocalTransparencyModifier = 0, + Massless = false, -- this is already done by accessories internally + Reflectance = 0, + RootPriority = 0, + RotVelocity = Vector3.new(0, 0, 0), + Transparency = 0, + Velocity = Vector3.new(0, 0, 0), + + -- surface properties + BackParamA = -0.5, + BackParamB = 0.5, + BackSurfaceInput = Enum.InputType.NoInput, + BottomParamA = -0.5, + BottomParamB = 0.5, + BottomSurfaceInput = Enum.InputType.NoInput, + FrontParamA = -0.5, + FrontParamB = 0.5, + FrontSurfaceInput = Enum.InputType.NoInput, + LeftParamA = -0.5, + LeftParamB = 0.5, + LeftSurfaceInput = Enum.InputType.NoInput, + RightParamA = -0.5, + RightParamB = 0.5, + RightSurfaceInput = Enum.InputType.NoInput, + TopParamA = -0.5, + TopParamB = 0.5, + TopSurfaceInput = Enum.InputType.NoInput, + + BackSurface = Enum.SurfaceType.Smooth, + BottomSurface = Enum.SurfaceType.Smooth, + FrontSurface = Enum.SurfaceType.Smooth, + LeftSurface = Enum.SurfaceType.Smooth, + RightSurface = Enum.SurfaceType.Smooth, + TopSurface = Enum.SurfaceType.Smooth, + }, + Part = { + Shape = Enum.PartType.Block, + }, +} + +return Constants diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/init.lua b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/init.lua new file mode 100644 index 0000000..9421ccc --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/init.lua @@ -0,0 +1,96 @@ +game:DefineFastFlag("UGCValidateMeshBounds", false) +game:DefineFastFlag("UGCValidateHandleSize", false) +game:DefineFastFlag("UGCExtraBannedNames", false) + +local root = script + +local validateInstanceTree = require(root.validation.validateInstanceTree) +local validateMeshTriangles = require(root.validation.validateMeshTriangles) +local validateModeration = require(root.validation.validateModeration) +local validateMaterials = require(root.validation.validateMaterials) +local validateTags = require(root.validation.validateTags) +local validateMeshBounds = require(root.validation.validateMeshBounds) +local validateTextureSize = require(root.validation.validateTextureSize) +local validateHandleSize = require(root.validation.validateHandleSize) +local validateProperties = require(root.validation.validateProperties) + +local function validateInternal(isAsync, instances, assetTypeEnum, noModeration) + -- validate that only one instance was selected + if #instances == 0 then + return false, { "No instances selected" } + elseif #instances > 1 then + return false, { "More than one instance selected" } + end + + local instance = instances[1] + + local success, reasons + + success, reasons = validateInstanceTree(instance, assetTypeEnum) + if not success then + return false, reasons + end + + success, reasons = validateMaterials(instance) + if not success then + return false, reasons + end + + success, reasons = validateProperties(instance) + if not success then + return false, reasons + end + + success, reasons = validateTags(instance) + if not success then + return false, reasons + end + + if game:GetFastFlag("UGCValidateMeshBounds") then + success, reasons = validateMeshBounds(isAsync, instance, assetTypeEnum) + if not success then + return false, reasons + end + end + + success, reasons = validateTextureSize(isAsync, instance) + if not success then + return false, reasons + end + + if game:GetFastFlag("UGCValidateHandleSize") then + success, reasons = validateHandleSize(isAsync, instance) + if not success then + return false, reasons + end + end + + success, reasons = validateMeshTriangles(isAsync, instance) + if not success then + return false, reasons + end + + if not noModeration then + success, reasons = validateModeration(isAsync, instance) + if not success then + return false, reasons + end + end + + return true +end + +local UGCValidation = {} + +function UGCValidation.validate(instances, assetTypeEnum, noModeration) + local success, reasons = validateInternal(--[[ isAsync = ]] false, instances, assetTypeEnum, noModeration) + return success, reasons +end + +function UGCValidation.validateAsync(instances, assetTypeEnum, callback, noModeration) + coroutine.wrap(function() + callback(validateInternal(--[[ isAsync = ]] true, instances, assetTypeEnum, noModeration)) + end)() +end + +return UGCValidation diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/util/createAccessorySchema.lua b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/util/createAccessorySchema.lua new file mode 100644 index 0000000..ef05835 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/util/createAccessorySchema.lua @@ -0,0 +1,47 @@ +local function createAccessorySchema(attachmentName) + assert(attachmentName, "attachmentName cannot be nil") + return { + ClassName = "Accessory", + _children = { + { + Name = "ThumbnailConfiguration", + ClassName = "Configuration", + _optional = true, + _children = { + { + Name = "ThumbnailCameraTarget", + ClassName = "ObjectValue", + }, + { + Name = "ThumbnailCameraValue", + ClassName = "CFrameValue", + }, + }, + }, + { + Name = "Handle", + ClassName = "Part", + _children = { + { + Name = attachmentName, + ClassName = "Attachment", + }, + { + ClassName = "SpecialMesh", + }, + { + ClassName = "StringValue", + Name = "AvatarPartScaleType", + _optional = true, + }, + { + ClassName = "TouchTransmitter", + _optional = true, + }, + } + }, + }, + } +end + +return createAccessorySchema diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/util/getAssetCreationDetails.lua b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/util/getAssetCreationDetails.lua new file mode 100644 index 0000000..4fe76da --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/util/getAssetCreationDetails.lua @@ -0,0 +1,56 @@ +local HttpService = game:GetService("HttpService") +local HttpRbxApiService = game:GetService("HttpRbxApiService") +local ContentProvider = game:GetService("ContentProvider") + +local function getBaseDomain() + local baseUrl = ContentProvider.BaseUrl + if string.sub(baseUrl, #baseUrl) ~= "/" then + baseUrl = baseUrl .. "/" + end + local _, schemeEnd = string.find(baseUrl, "://") + local _, prefixEnd = string.find(baseUrl, "%.", schemeEnd + 1) + return string.sub(baseUrl, prefixEnd + 1) +end + +local MAX_RETRIES = 5 + +local function requestAndRetry(apiUrl, data, attempt) + if attempt == nil then + attempt = 0 + end + + local success, response = pcall(function() + return HttpRbxApiService:PostAsyncFullUrl(apiUrl, data) + end) + + if success then + return true, response + elseif attempt >= MAX_RETRIES then + return false, response + else + local timeToWait = 2^(attempt - 1) + wait(timeToWait) + return requestAndRetry(apiUrl, data, attempt + 1) + end +end + +local BASE_DOMAIN = getBaseDomain() +local ITEM_CONFIGURATION_URL = string.format("https://itemconfiguration.%s", BASE_DOMAIN) +local GET_ASSET_CREATION_DETAILS_URL = ITEM_CONFIGURATION_URL .. "v1/creations/get-asset-details" + +local function getAssetCreationDetails(isAsync, assetIds) + local success, response = requestAndRetry( + GET_ASSET_CREATION_DETAILS_URL, + HttpService:JSONEncode({ assetIds = assetIds }) + ) + + -- TODO: isAsync + + if success then + return true, HttpService:JSONDecode(response) + else + return false, response + end +end + +return getAssetCreationDetails \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/util/validateWithSchema.lua b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/util/validateWithSchema.lua new file mode 100644 index 0000000..0f8b7bf --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/util/validateWithSchema.lua @@ -0,0 +1,100 @@ +local function checkName(nameList, instanceName) + if type(nameList) == "table" then + for _, name in pairs(nameList) do + if name == instanceName then + return true + end + end + elseif type(nameList) == "string" then + return nameList == instanceName + end + return false +end + +local function getReadableName(nameList) + if type(nameList) == "table" then + return table.concat(nameList, " or ") + elseif type(nameList) == "string" then + return nameList + end + return "*" +end + +local function validateWithSchemaHelper(schema, instance, authorizedSet) + -- validate + if instance.ClassName ~= schema.ClassName or (schema.Name ~= nil and not checkName(schema.Name, instance.Name)) then + return { success = false } + end + + -- validate children + if schema._children then + for _, childSchema in pairs(schema._children) do + local found = false + local mostRecentFailure + for _, child in pairs(instance:GetChildren()) do + local result = validateWithSchemaHelper(childSchema, child, authorizedSet) + if result.success then + found = true + break + elseif result.message then + mostRecentFailure = result + end + end + if not found and not childSchema._optional then + if mostRecentFailure then + return mostRecentFailure + else + return { + success = false, + message = "Could not find a " + .. childSchema.ClassName + .. " called " + .. getReadableName(childSchema.Name) + .. " inside " + .. instance.Name + } + end + end + end + end + + authorizedSet[instance] = true + + return { success = true } +end + +local function validateWithSchema(schema, instance) + + if instance.ClassName ~= schema.ClassName or (schema.Name ~= nil and schema.Name ~= instance.Name) then + return { + success = false, + message = "Expected top-level instance to be a " .. schema.ClassName, + } + end + + local authorizedSet = {} + local result = validateWithSchemaHelper(schema, instance, authorizedSet) + + if not result.success then + return result + end + + -- check for extra descendants + local unauthorizedDescendantPaths = {} + for _, descendant in pairs(instance:GetDescendants()) do + if authorizedSet[descendant] == nil then + unauthorizedDescendantPaths[#unauthorizedDescendantPaths + 1] = descendant:GetFullName() + end + end + + if #unauthorizedDescendantPaths > 0 then + return { + success = false, + message = "Unexpected Descendants:\n" .. table.concat(unauthorizedDescendantPaths, "\n") + } + end + + return { success = true } +end + +return validateWithSchema \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/util/valueToString.lua b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/util/valueToString.lua new file mode 100644 index 0000000..29aed14 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/util/valueToString.lua @@ -0,0 +1,33 @@ +local CorePackages = game:GetService("CorePackages") + +local Cryo = require(CorePackages.Cryo) + +local function round(num, numDecimalPlaces) + local mult = 10^(numDecimalPlaces or 0) + return math.floor(num * mult + 0.5) / mult +end + +local function valueToString(propValue) + local valueType = typeof(propValue) + if propValue == Cryo.None then + return "not defined" + elseif valueType == "Vector3" then + return string.format( + "%d, %d, %d", + round(propValue.X, 2), + round(propValue.Y, 2), + round(propValue.Z, 2) + ) + elseif valueType == "Color3" then + return string.format( + "%d, %d, %d", + math.floor(propValue.r * 255), + math.floor(propValue.g * 255), + math.floor(propValue.b * 255) + ) + else + return tostring(propValue) + end +end + +return valueToString diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateHandleSize.lua b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateHandleSize.lua new file mode 100644 index 0000000..a2ec741 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateHandleSize.lua @@ -0,0 +1,59 @@ +local UGCValidationService = game:GetService("UGCValidationService") + +local root = script.Parent.Parent +local valueToString = require(root.util.valueToString) + +local MARGIN_OF_ERROR = 0.1 + +local function validateHandleSize(isAsync, instance) + -- these are guaranteed to exist thanks to validateInstanceTree being called beforehand + local handle = instance.Handle + local mesh = handle:FindFirstChildOfClass("SpecialMesh") + + local success, verts = pcall(function() + if isAsync then + return UGCValidationService:GetMeshVerts(mesh.MeshId) + else + return UGCValidationService:GetMeshVertsSync(mesh.MeshId) + end + end) + + if not success then + return false, { "Failed to read mesh" } + end + + local minX, maxX = math.huge, 0 + local minY, maxY = math.huge, 0 + local minZ, maxZ = math.huge, 0 + + for i = 1, #verts do + local vert = verts[i] * mesh.Scale + minX = math.min(minX, vert.X) + minY = math.min(minY, vert.Y) + minZ = math.min(minZ, vert.Z) + maxX = math.max(maxX, vert.X) + maxY = math.max(maxY, vert.Y) + maxZ = math.max(maxZ, vert.Z) + end + + local meshSize = Vector3.new( + maxX - minX, + maxY - minY, + maxZ - minZ + ) + + -- allow handle.Size to be within MARGIN_OF_ERROR of meshSize or larger + -- this is necessary since we're comparing floats + -- the size only needs to be a rough equivalent for thumbnailing + if handle.Size.X + MARGIN_OF_ERROR < meshSize.X + or handle.Size.Y + MARGIN_OF_ERROR < meshSize.Y + or handle.Size.Z + MARGIN_OF_ERROR < meshSize.Z then + return false, { + string.format("Accessory Handle size should be at least the size of the mesh ( %s )", valueToString(meshSize)) + } + end + + return true +end + +return validateHandleSize \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateInstanceTree.lua b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateInstanceTree.lua new file mode 100644 index 0000000..82b3ca8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateInstanceTree.lua @@ -0,0 +1,53 @@ +local root = script.Parent.Parent + +local Constants = require(root.Constants) +local createAccessorySchema = require(root.util.createAccessorySchema) +local validateWithSchema = require(root.util.validateWithSchema) + +-- validates a given instance based on a schema +local function validateInstanceTree(instance, assetTypeEnum) + local assetInfo = Constants.ASSET_TYPE_INFO[assetTypeEnum] + if not assetInfo then + return false, { "Could not validate" } + end + + local schema = createAccessorySchema(assetInfo.attachmentNames) + + -- validate using hat schema + local validationResult = validateWithSchema(schema, instance) + if validationResult.success == false then + return false, { validationResult.message } + end + + -- fallback case for if validateWithSchema breaks + local invalidDescendantsReasons = {} + if Constants.BANNED_NAMES[instance.Name] then + local reason = string.format("%s has an invalid name", instance:GetFullName()) + invalidDescendantsReasons[#invalidDescendantsReasons + 1] = reason + end + + for _, descendant in pairs(instance:GetDescendants()) do + for _, className in pairs(Constants.BANNED_CLASS_NAMES) do + if descendant:IsA(className) then + local reason = string.format( + "%s is of type %s which is not allowed", + descendant:GetFullName(), + className + ) + invalidDescendantsReasons[#invalidDescendantsReasons + 1] = reason + end + end + if Constants.BANNED_NAMES[descendant.Name] then + local reason = string.format("%s has an invalid name", descendant:GetFullName()) + invalidDescendantsReasons[#invalidDescendantsReasons + 1] = reason + end + end + + if #invalidDescendantsReasons > 0 then + return false, invalidDescendantsReasons + end + + return true +end + +return validateInstanceTree \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMaterials.lua b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMaterials.lua new file mode 100644 index 0000000..81408d4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMaterials.lua @@ -0,0 +1,30 @@ +local root = script.Parent.Parent + +local Constants = require(root.Constants) + +-- ensures no descendant of instance has a material that does not exist in Constants.MATERIAL_WHITELIST +local function validateMaterials(instance) + local materialFailures = {} + for _, descendant in pairs(instance:GetDescendants()) do + if descendant:IsA("BasePart") and not Constants.MATERIAL_WHITELIST[descendant.Material] then + materialFailures[#materialFailures + 1] = descendant:GetFullName() + end + end + if #materialFailures > 0 then + local reasons = {} + local acceptedMaterialNames = {} + for material in pairs(Constants.MATERIAL_WHITELIST) do + acceptedMaterialNames[#acceptedMaterialNames + 1] = material.Name + end + reasons[#reasons + 1] = "Invalid materials for" + for _, name in pairs(materialFailures) do + reasons[#reasons + 1] = name + end + reasons[#reasons + 1] = "Accepted materials are " .. table.concat(acceptedMaterialNames, ", ") + return false, reasons + end + + return true +end + +return validateMaterials \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMeshBounds.lua b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMeshBounds.lua new file mode 100644 index 0000000..4d7f5f8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMeshBounds.lua @@ -0,0 +1,72 @@ +local UGCValidationService = game:GetService("UGCValidationService") + +local root = script.Parent.Parent + +local Constants = require(root.Constants) + +local DEFAULT_OFFSET = Vector3.new(0, 0, 0) + +local function pointInBounds(worldPos, boundsCF, boundsSize) + local objectPos = boundsCF:pointToObjectSpace(worldPos) + return objectPos.X >= -boundsSize.X/2 + and objectPos.X <= boundsSize.X/2 + and objectPos.Y >= -boundsSize.Y/2 + and objectPos.Y <= boundsSize.Y/2 + and objectPos.Z >= -boundsSize.Z/2 + and objectPos.Z <= boundsSize.Z/2 +end + +local function getAttachment(parent, names) + for _, name in pairs(names) do + local result = parent:FindFirstChild(name) + if result then + return result + end + end + return nil +end + +local function validateMeshBounds(isAsync, instance, assetTypeEnum) + local assetInfo = Constants.ASSET_TYPE_INFO[assetTypeEnum] + + -- these are guaranteed to exist thanks to validateInstanceTree being called beforehand + local handle = instance.Handle + local mesh = handle:FindFirstChildOfClass("SpecialMesh") + local attachment = getAttachment(handle, assetInfo.attachmentNames) + + if mesh.MeshId == "" then + return false, { "Mesh must contain valid MeshId" } + end + + local success, verts = pcall(function() + if isAsync then + return UGCValidationService:GetMeshVerts(mesh.MeshId) + else + return UGCValidationService:GetMeshVertsSync(mesh.MeshId) + end + end) + + if not success then + return false, { "Failed to read mesh" } + end + + local boundsInfo = assert(assetInfo.bounds[attachment.Name], "Could not find bounds for " .. attachment.Name) + local boundsSize = boundsInfo.size + local boundsOffset = boundsInfo.offset or DEFAULT_OFFSET + local boundsCF = handle.CFrame * attachment.CFrame * CFrame.new(boundsOffset) + + for _, vertPos in pairs(verts) do + local worldPos = handle.CFrame:pointToWorldSpace(vertPos * mesh.Scale) + if not pointInBounds(worldPos, boundsCF, boundsSize) then + return false, { + "Mesh is too large!", + string.format("Max size for type %s is ( %s )", assetTypeEnum.Name, tostring(boundsSize)), + "Use SpecialMesh.Scale if needed" + } + end + end + + return true +end + +return validateMeshBounds diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMeshTriangles.lua b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMeshTriangles.lua new file mode 100644 index 0000000..eb2a47f --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMeshTriangles.lua @@ -0,0 +1,40 @@ +local UGCValidationService = game:GetService("UGCValidationService") + +local root = script.Parent.Parent + +local Constants = require(root.Constants) + +-- ensures accessory mesh does not have more triangles than Constants.MAX_HAT_TRIANGLES +local function validateMeshTriangles(isAsync, instance) + -- check mesh triangles + -- this is guaranteed to exist thanks to validateInstanceTree being called beforehand + local mesh = instance.Handle:FindFirstChildOfClass("SpecialMesh") + + if mesh.MeshId == "" then + return false, { "Mesh must contain valid MeshId" } + end + + local success, triangles = pcall(function() + if isAsync then + return UGCValidationService:GetMeshTriCount(mesh.MeshId) + else + return UGCValidationService:GetMeshTriCountSync(mesh.MeshId) + end + end) + + if not success then + return false, { "Failed to load mesh data" } + elseif triangles > Constants.MAX_HAT_TRIANGLES then + return false, { + string.format( + "Mesh has %d triangles, but the limit is %d", + triangles, + Constants.MAX_HAT_TRIANGLES + ) + } + end + + return true +end + +return validateMeshTriangles \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateModeration.lua b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateModeration.lua new file mode 100644 index 0000000..aa3bea8 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateModeration.lua @@ -0,0 +1,97 @@ +local root = script.Parent.Parent + +local Constants = require(root.Constants) +local getAssetCreationDetails = require(root.util.getAssetCreationDetails) + +local function parseContentId(contentIds, contentIdMap, object, fieldName) + local contentId = object[fieldName] + + -- map to ending digits + -- rbxassetid://1234 -> 1234 + -- http://www.roblox.com/asset/?id=1234 -> 1234 + local id = tonumber(string.match(contentId, "%d+$")) + if id == nil then + return false, { + "Could not parse ContentId", + contentId, + } + end + contentIdMap[id] = { + fieldName = fieldName, + instance = object, + } + table.insert(contentIds, id) + + return true +end + +local function parseDescendantContentIds(contentIds, contentIdMap, object) + for _, descendant in pairs(object:GetDescendants()) do + if descendant:IsA("SpecialMesh") then + local success, reasons + success, reasons = parseContentId(contentIds, contentIdMap, descendant, "MeshId") + if not success then + return false, reasons + end + success, reasons = parseContentId(contentIds, contentIdMap, descendant, "TextureId") + if not success then + return false, reasons + end + end + end + + return true +end + +-- ensures accessory content ids have all passed moderation review +local function validateModeration(isAsync, instance) + local contentIdMap = {} + local contentIds = {} + + local parseSuccess, parseReasons = parseDescendantContentIds(contentIds, contentIdMap, instance) + if not parseSuccess then + return false, parseReasons + end + + local moderatedIds = {} + + local success, response = getAssetCreationDetails(isAsync, contentIds) + + if not success or #response ~= #contentIds then + return false, { "Could not fetch details for assets" } + end + + for _, details in pairs(response) do + if details.status == Constants.ASSET_STATUS.UNKNOWN + or details.status == Constants.ASSET_STATUS.REVIEW_PENDING + or details.status == Constants.ASSET_STATUS.MODERATED + then + table.insert(moderatedIds, details.assetId) + end + end + + if #moderatedIds > 0 then + local moderationMessages = {} + for idx, id in pairs(moderatedIds) do + local mapped = contentIdMap[id] + if mapped then + moderationMessages[idx] = string.format( + "%s.%s ( %s )", + mapped.instance:GetFullName(), + mapped.fieldName, + id + ) + else + moderationMessages[idx] = id + end + end + return false, { + "The following asset IDs have not passed moderation:", + unpack(moderationMessages), + } + end + + return true +end + +return validateModeration diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateProperties.lua b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateProperties.lua new file mode 100644 index 0000000..cd87950 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateProperties.lua @@ -0,0 +1,71 @@ +local CorePackages = game:GetService("CorePackages") + +local Cryo = require(CorePackages.Cryo) + +local root = script.Parent.Parent + +local Constants = require(root.Constants) +local valueToString = require(root.util.valueToString) + +local EPSILON = 1e-5 + +local function floatEq(a, b) + return math.abs(a - b) <= EPSILON +end + +local function v3FloatEq(a, b) + return floatEq(a.X, b.X) and floatEq(a.Y, b.Y) and floatEq(a.Z, b.Z) +end + +local function c3FloatEq(a, b) + return floatEq(a.r, b.r) and floatEq(a.g, b.g) and floatEq(a.b, b.b) +end + +local function propEq(propValue, expectedValue) + local valueType = typeof(expectedValue) + if expectedValue == Cryo.None then + return propValue == nil + elseif valueType == "number" then + return floatEq(propValue, expectedValue) + elseif valueType == "Vector3" then + return v3FloatEq(propValue, expectedValue) + elseif valueType == "Color3" then + return c3FloatEq(propValue, expectedValue) + else + return propValue == expectedValue + end +end + +local function validateProperties(instance) + + -- full tree of instance + descendants + local objects = instance:GetDescendants() + table.insert(objects, instance) + + for _, object in pairs(objects) do + for className, properties in pairs(Constants.PROPERTIES) do + if object:IsA(className) then + for propName, expectedValue in pairs(properties) do + -- ensure property exists first + local propExists, propValue = pcall(function() return object[propName] end) + + if not propExists then + return false, { + string.format("Property %s does not exist on type %s", propName, object.ClassName) + } + end + + if not propEq(propValue, expectedValue) then + return false, { + string.format("Expected %s.%s to be %s", object:GetFullName(), propName, valueToString(expectedValue)) + } + end + end + end + end + end + + return true +end + +return validateProperties \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateTags.lua b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateTags.lua new file mode 100644 index 0000000..50c8c00 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateTags.lua @@ -0,0 +1,25 @@ +local CollectionService = game:GetService("CollectionService") + +local function validateTags(instance) + local objects = instance:GetDescendants() + table.insert(objects, instance) + + local hasTags = {} + for _, obj in pairs(objects) do + if #CollectionService:GetTags(obj) > 0 then + table.insert(hasTags, obj) + end + end + + if #hasTags > 0 then + local reasons = { "The following objects contain CollectionService tags:" } + for _, obj in pairs(hasTags) do + table.insert(reasons, obj:GetFullName()) + end + return false, reasons + end + + return true +end + +return validateTags \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateTextureSize.lua b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateTextureSize.lua new file mode 100644 index 0000000..aa50c53 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateTextureSize.lua @@ -0,0 +1,40 @@ +local UGCValidationService = game:GetService("UGCValidationService") + +local root = script.Parent.Parent + +local Constants = require(root.Constants) + +local function validateTextureSize(isAsync, instance) + -- this is guaranteed to exist thanks to validateInstanceTree being called beforehand + local mesh = instance.Handle:FindFirstChildOfClass("SpecialMesh") + + if mesh.TextureId == "" then + return false, { "Mesh must contain valid TextureId" } + end + + local success, imageSize = pcall(function() + if isAsync then + return UGCValidationService:GetTextureSize(mesh.TextureId) + else + return UGCValidationService:GetTextureSizeSync(mesh.TextureId) + end + end) + + if not success then + return false, { "Failed to load texture data", imageSize } + elseif imageSize.X > Constants.MAX_TEXTURE_SIZE or imageSize.Y > Constants.MAX_TEXTURE_SIZE then + return false, { + string.format( + "Texture size is %dx%d px, but the limit is %dx%d px", + imageSize.X, + imageSize.Y, + Constants.MAX_TEXTURE_SIZE, + Constants.MAX_TEXTURE_SIZE + ) + } + end + + return true +end + +return validateTextureSize \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UIBlox.lua b/Client2020/ExtraContent/LuaPackages/UIBlox.lua new file mode 100644 index 0000000..adc8236 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UIBlox.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +local UIBlox = require(CorePackages.Packages.UIBlox) + +return UIBlox \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaAppUseUIBloxToasts.lua b/Client2020/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaAppUseUIBloxToasts.lua new file mode 100644 index 0000000..38d6ffa --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaAppUseUIBloxToasts.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("LuaAppUseUIBloxToasts2", false) + +return function() + return game:GetFastFlag("LuaAppUseUIBloxToasts2") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaFixItemTilePremiumIcon.lua b/Client2020/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaFixItemTilePremiumIcon.lua new file mode 100644 index 0000000..654c1f5 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaFixItemTilePremiumIcon.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("LuaPremiumCatalogTileFix", false) + +return function() + return game:GetFastFlag("LuaPremiumCatalogTileFix") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaUIBloxModalWindowAnchorPoint.lua b/Client2020/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaUIBloxModalWindowAnchorPoint.lua new file mode 100644 index 0000000..cf5f2e4 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaUIBloxModalWindowAnchorPoint.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("LuaUIBloxModalWindowAnchorPoint", false) + +return function() + return game:GetFastFlag("LuaUIBloxModalWindowAnchorPoint") +end \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/UIBloxUniversalAppConfig.lua b/Client2020/ExtraContent/LuaPackages/UIBloxUniversalAppConfig.lua new file mode 100644 index 0000000..341f817 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/UIBloxUniversalAppConfig.lua @@ -0,0 +1,13 @@ +-- See https://confluence.rbx.com/display/MOBAPP/UIBlox+Flagging +-- for more info on how to add values here +local CorePackages = game:GetService("CorePackages") +local GetFFlagLuaAppUseUIBloxToasts = require(CorePackages.UIBloxFlags.GetFFlagLuaAppUseUIBloxToasts) +local GetFFlagLuaUIBloxModalWindowAnchorPoint = require(CorePackages.UIBloxFlags.GetFFlagLuaUIBloxModalWindowAnchorPoint) +local GetFFlagLuaFixItemTilePremiumIcon = require(CorePackages.UIBloxFlags.GetFFlagLuaFixItemTilePremiumIcon) + +return { + fixToastResizeConfig = GetFFlagLuaAppUseUIBloxToasts(), + expandableTextAutomaticResizeConfig = true, + modalWindowAnchorPoint = GetFFlagLuaUIBloxModalWindowAnchorPoint(), + fixItemTilePremiumIcon = GetFFlagLuaFixItemTilePremiumIcon(), +} \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/enumerate.lua b/Client2020/ExtraContent/LuaPackages/enumerate.lua new file mode 100644 index 0000000..0468de1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/enumerate.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.enumerate) diff --git a/Client2020/ExtraContent/LuaPackages/initify.lua b/Client2020/ExtraContent/LuaPackages/initify.lua new file mode 100644 index 0000000..f6334a1 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/initify.lua @@ -0,0 +1,40 @@ +--[[ + Restructures a tree of ModuleScript objects to emulate the behavior of stock + Lua and Rojo's `init.lua` mechanism, which essentially lets you load folders + as modules. + + A file structure like this: + + foo (directory) + `-- bar (directory) + `-- init.lua (file) + + Is turned into: + + foo (Folder) + `-- bar (ModuleScript) +]] + +local function initify(rbx) + local init = rbx:FindFirstChild("init") + + if init then + init.Name = rbx.Name + init.Parent = rbx.Parent + + for _, child in ipairs(rbx:GetChildren()) do + child.Parent = init + end + + rbx:Destroy() + rbx = init + end + + for _, child in ipairs(rbx:GetChildren()) do + initify(child) + end + + return rbx +end + +return initify \ No newline at end of file diff --git a/Client2020/ExtraContent/LuaPackages/rotriever.lock b/Client2020/ExtraContent/LuaPackages/rotriever.lock new file mode 100644 index 0000000..f498599 --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/rotriever.lock @@ -0,0 +1,379 @@ +# This file is automatically @generated by rotriever. +# It is not intended for manual editing. +lockfile_format_version = 4 +proxy = "https://github.com/roblox/rotriever-proxy-index" + +[[package]] +name = "AvatarExperienceDeps" +version = "0.0.1" +commit = "5a31b41867d2c5ebf0b5670d9be27f3edd403545" +source = "git+https://github.com/roblox/avatar-experience-deps#master" +dependencies = ["RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components"] + +[[package]] +name = "CorePackages" +version = "0.1.0" +dependencies = [ + "AvatarExperienceDeps AvatarExperienceDeps 5a31b418 git+https://github.com/roblox/avatar-experience-deps#master", + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "InGameMenuDependencies InGameMenuDependencies aecc56c1 git+https://github.rbx.com/roblox/in-game-menu-dependencies#master", + "LuaChatDeps LuaChatDeps 6144362f git+https://github.rbx.com/roblox/lua-chat-deps#master", + "LuaDiscussionsDeps LuaDiscussionsDeps dfba285e git+https://github.rbx.com/roblox/lua-discussions-deps#master", + "LuaSocialLibrariesDeps LuaSocialLibrariesDeps 6d421330 git+https://github.rbx.com/roblox/lua-social-libraries-deps#master", + "Lumberyak roblox/lumberyak 47552268 git+https://github.rbx.com/roblox/lumberyak#master", + "Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter", + "PolicyProvider roblox/lua-roact-policy-provider 1d595dae git+https://github.com/roblox/lua-roact-policy-provider#master", + "PremiumUpsellDeps PremiumUpsellDeps c51030b7 git+https://github.com/roblox/premium-upsell-deps#master", + "Promise lua-promise bbb9e162 git+https://github.rbx.com/roblox/lua-promise#master", + "PurchasePrompt roblox/purchase-prompt ad4ca756 git+https://github.com/roblox/purchasepromptscript-roact#master", + "Result roblox/lua-result c6817c84 git+https://github.rbx.com/roblox/lua-result#master", + "Rhodium roblox/rhodium 0.2.5 url+https://github.com/roblox/rhodium", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "RoactGamepad roblox/roact-gamepad 0.4.4 url+https://github.com/roblox/roact-gamepad", + "RoactNavigation roblox/roact-navigation 0.2.8 url+https://github.com/roblox/roact-navigation", + "RoactRodux roblox/roact-rodux 0.2.2 url+https://github.com/roblox/roact-rodux", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "StringUtilities roblox/string-utilities 1.0.0 url+https://github.com/roblox/string-utilities", + "TestEZ roblox/testez 0.3.1 url+https://github.com/roblox/testez", + "UIBlox UIBlox a253d523 git+https://github.com/roblox/uiblox#master", + "UrlBuilder roblox/url-builder 1.0.1 url+https://github.com/roblox/url-builder", + "enumerate roblox/enumerate 1.0.0 url+https://github.com/roblox/enumerate", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", + "tutils tutils 0be577db git+https://github.rbx.com/roblox/tutils#master", +] + +[[package]] +name = "InGameMenuDependencies" +version = "0.1.0" +commit = "aecc56c1fa6348886a1073785dbb196b655a0a90" +source = "git+https://github.rbx.com/roblox/in-game-menu-dependencies#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter", + "PolicyProvider roblox/lua-roact-policy-provider 1d595dae git+https://github.com/roblox/lua-roact-policy-provider#master", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "RoactRodux roblox/roact-rodux 0.2.2 url+https://github.com/roblox/roact-rodux", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "UIBlox UIBlox a253d523 git+https://github.com/roblox/uiblox#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "LuaChatDeps" +version = "0.1.3" +commit = "6144362fd181fe94fd213ec8569ba95f0a61faed" +source = "git+https://github.rbx.com/roblox/lua-chat-deps#master" +dependencies = [ + "AssetCard asset-card c7b683fb git+https://github.com/roblox/asset-card#v1.0.2", + "InfiniteScroller roblox/infinite-scroller 0.5.6 url+https://github.com/roblox/infinite-scroller", + "RoduxNetworking rodux-networking ea19cfe3 git+https://github.rbx.com/roblox/rodux-networking#v1.0.1", + "UIBlox UIBlox a253d523 git+https://github.com/roblox/uiblox#master", +] + +[[package]] +name = "LuaDiscussionsDeps" +version = "0.1.2" +commit = "dfba285e3dd4e0fe5059b93d52880666e2924612" +source = "git+https://github.rbx.com/roblox/lua-discussions-deps#master" +dependencies = [ + "InfiniteScroll roblox/infinite-scroller 0.3.4 url+https://github.com/roblox/infinite-scroller", + "RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "RoactNavigation roact-navigation 0.1.1-linter-fix url+https://github.com/roblox/roact-navigation", + "RoduxNetworking rodux-networking ea19cfe3 git+https://github.rbx.com/roblox/rodux-networking#v1.0.1", +] + +[[package]] +name = "LuaSocialLibrariesDeps" +version = "0.1.1" +commit = "6d4213307f9d059e422026c5f8ad1a37a3489a9b" +source = "git+https://github.rbx.com/roblox/lua-social-libraries-deps#master" +dependencies = [ + "GenericPagination roblox/genericpagination 6cc56178 git+https://github.rbx.com/roblox/genericpagination#master", + "Mock jtaylor/mock d2c4005c git+https://github.rbx.com/roblox/mock#master", + "RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", +] + +[[package]] +name = "PremiumUpsellDeps" +version = "0.0.0" +commit = "c51030b70236f2e9d69bb34348b7e045c2c437f6" +source = "git+https://github.com/roblox/premium-upsell-deps#master" +dependencies = ["RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components"] + +[[package]] +name = "UIBlox" +version = "0.1.1" +commit = "a253d52373c4ce1611090c04ae2a02994274f1f2" +source = "git+https://github.com/roblox/uiblox#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "InfiniteScroller roblox/infinite-scroller 0.5.6 url+https://github.com/roblox/infinite-scroller", + "Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "RoactGamepad roblox/roact-gamepad 0.4.4 url+https://github.com/roblox/roact-gamepad", + "enumerate roblox/enumerate 1.0.0 url+https://github.com/roblox/enumerate", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "asset-card" +version = "1.0.1" +commit = "c7b683fb0c31888433127311ce45b691b9cf6086" +source = "git+https://github.com/roblox/asset-card#v1.0.2" +dependencies = [ + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "UIBlox UIBlox a253d523 git+https://github.com/roblox/uiblox#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "freeze" +version = "0.1.0" +commit = "ef89f9d0444a2a7f63d5fd913a38824f3edba69f" +source = "git+https://github.rbx.com/roblox/freeze#master" +dependencies = ["Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo"] + +[[package]] +name = "jtaylor/mock" +version = "0.1.0" +commit = "d2c4005c863fd2f9fa74b7391ada64906d242847" +source = "git+https://github.rbx.com/roblox/mock#master" + +[[package]] +name = "lua-promise" +version = "0.1.0" +commit = "bbb9e1628901e29b8fad444ed1c9743ccbb3280b" +source = "git+https://github.rbx.com/roblox/lua-promise#master" +dependencies = ["tutils tutils 0be577db git+https://github.rbx.com/roblox/tutils#master"] + +[[package]] +name = "roact-navigation" +version = "0.1.1-linter-fix" +commit = "943c1c661c396a39ef3ca8f45927d4242732581d" +source = "url+https://github.com/roblox/roact-navigation" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", +] + +[[package]] +name = "roblox/cryo" +version = "1.0.0" +commit = "272caa8f3f3b3b29296b462f80b65cc9b1c92f1e" +source = "url+https://github.com/roblox/cryo" + +[[package]] +name = "roblox/enumerate" +version = "1.0.0" +commit = "48daaf0df47eaf36c154d691a8320239e4a1312e" +source = "url+https://github.com/roblox/enumerate" + +[[package]] +name = "roblox/genericpagination" +version = "0.1.0" +commit = "6cc56178d99b731a71f6920ace8b3f3d4aaf5235" +source = "git+https://github.rbx.com/roblox/genericpagination#master" +dependencies = [ + "Promise lua-promise bbb9e162 git+https://github.rbx.com/roblox/lua-promise#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "roblox/infinite-scroller" +version = "0.3.4" +commit = "43d4eedf0d7a0a97d2dd55e7f066ac617e7fde9f" +source = "url+https://github.com/roblox/infinite-scroller" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "roblox/infinite-scroller" +version = "0.5.6" +commit = "d622d74bec4a599c5f8ef642194fa9eaf973b5c0" +source = "url+https://github.com/roblox/infinite-scroller" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "roblox/lua-result" +version = "0.1.0" +commit = "c6817c8455aa1f5922d83dd44c8bedbc7726e51d" +source = "git+https://github.rbx.com/roblox/lua-result#master" + +[[package]] +name = "roblox/lua-roact-policy-provider" +version = "0.1.0" +commit = "1d595dae48c654c54a76f07a2ddb95bfad53dc43" +source = "git+https://github.com/roblox/lua-roact-policy-provider#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Lumberyak roblox/lumberyak 47552268 git+https://github.rbx.com/roblox/lumberyak#master", + "Mock jtaylor/mock d2c4005c git+https://github.rbx.com/roblox/mock#master", + "Promise lua-promise bbb9e162 git+https://github.rbx.com/roblox/lua-promise#master", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "Symbol roblox/lua-symbol 139fdfe6 git+https://github.rbx.com/roblox/lua-symbol#master", + "tutils tutils 0be577db git+https://github.rbx.com/roblox/tutils#master", +] + +[[package]] +name = "roblox/lua-symbol" +version = "0.1.0" +commit = "139fdfe6e4d4eca690887d3beb80e1514af501bf" +source = "git+https://github.rbx.com/roblox/lua-symbol#master" + +[[package]] +name = "roblox/lumberyak" +version = "0.1.0" +commit = "47552268e68c899226295e82936d3b68dfd53565" +source = "git+https://github.rbx.com/roblox/lumberyak#master" +dependencies = ["Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo"] + +[[package]] +name = "roblox/otter" +version = "0.1.2" +commit = "572480526578e1bbd583959e959f0d10118c05a5" +source = "url+https://github.com/roblox/otter" + +[[package]] +name = "roblox/purchase-prompt" +version = "0.1.6" +commit = "ad4ca756b1cee3e65122ca3459e2ae38668fcd54" +source = "git+https://github.com/roblox/purchasepromptscript-roact#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "RoactRodux roblox/roact-rodux 0.2.2 url+https://github.com/roblox/roact-rodux", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "UIBlox UIBlox a253d523 git+https://github.com/roblox/uiblox#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "roblox/rhodium" +version = "0.2.5" +commit = "7e623fc1d95af129f8fe9ca959160969e469e2ef" +source = "url+https://github.com/roblox/rhodium" + +[[package]] +name = "roblox/roact" +version = "1.3.0" +commit = "58af06b996061dd3ab0696fbe530e4b5070a35c4" +source = "url+https://github.com/roblox/roact" + +[[package]] +name = "roblox/roact-fit-components" +version = "1.2.5" +commit = "b784928fdef64215d38e2febae28412a3b2a520b" +source = "url+https://github.com/roblox/roact-fit-components" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", +] + +[[package]] +name = "roblox/roact-gamepad" +version = "0.4.4" +commit = "e5eb28c3a4981f971378372570ce9cd4ee023137" +source = "url+https://github.com/roblox/roact-gamepad" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "enumerate roblox/enumerate 1.0.0 url+https://github.com/roblox/enumerate", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "roblox/roact-navigation" +version = "0.2.8" +commit = "6b5c6975e75ab68048a12f6f3b84ab4061afe496" +source = "url+https://github.com/roblox/roact-navigation" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "StateTable roblox/state-table 0.1.0 url+https://github.com/roblox/state-table", +] + +[[package]] +name = "roblox/roact-rodux" +version = "0.2.2" +commit = "78e2f5ac8c4679ef7216fbb9eedbcae31122545a" +source = "url+https://github.com/roblox/roact-rodux" +dependencies = [ + "Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", +] + +[[package]] +name = "roblox/rodux" +version = "1.0.0" +commit = "416e8042bd6eac7a385cb0955079501fd44f11e1" +source = "url+https://github.com/roblox/rodux" + +[[package]] +name = "roblox/state-table" +version = "0.1.0" +commit = "aa0641127efcb15fcabb26f5ba46e44815f42d4a" +source = "url+https://github.com/roblox/state-table" + +[[package]] +name = "roblox/string-utilities" +version = "1.0.0" +commit = "7cbef7885ca023d71dd93f02ffa32cbda65faa9c" +source = "url+https://github.com/roblox/string-utilities" + +[[package]] +name = "roblox/t" +version = "1.2.5" +commit = "5d6ee3e23a658b12bc433b0837184215618e233e" +source = "url+https://github.com/roblox/t" + +[[package]] +name = "roblox/testez" +version = "0.3.1" +commit = "42f0b3772c90ed9c0cff187fb56d508aca7b6641" +source = "url+https://github.com/roblox/testez" + +[[package]] +name = "roblox/url-builder" +version = "1.0.1" +commit = "9ae0b3705f07b75f259ffa19e7e363ef4eb02ebe" +source = "url+https://github.com/roblox/url-builder" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "StringUtilities roblox/string-utilities 1.0.0 url+https://github.com/roblox/string-utilities", +] + +[[package]] +name = "rodux-networking" +version = "1.0.0" +commit = "ea19cfe3c71e5747fa3ab4235beed4fc21d63e4d" +source = "git+https://github.rbx.com/roblox/rodux-networking#v1.0.1" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Freeze freeze ef89f9d0 git+https://github.rbx.com/roblox/freeze#master", + "Promise lua-promise bbb9e162 git+https://github.rbx.com/roblox/lua-promise#master", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "tutils tutils 0be577db git+https://github.rbx.com/roblox/tutils#master", +] + +[[package]] +name = "tutils" +version = "0.1.0" +commit = "0be577dbb47de6c7821d1e3d1acf0b40a292689f" +source = "git+https://github.rbx.com/roblox/tutils#master" diff --git a/Client2020/ExtraContent/LuaPackages/rotriever.toml b/Client2020/ExtraContent/LuaPackages/rotriever.toml new file mode 100644 index 0000000..8b0e26c --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/rotriever.toml @@ -0,0 +1,65 @@ +[package] +name = "CorePackages" +author = "Roblox" +license = "" +version = "0.1.0" +proxy = "https://github.com/roblox/rotriever-proxy-index" + +[dependencies] +Roact = "github.com/roblox/roact@1.2" +Rodux = "github.com/roblox/rodux@1.0" +RoactRodux = "github.com/roblox/roact-rodux@0.2" +RoactNavigation = "github.com/roblox/roact-navigation@0.2" +Cryo = "github.com/roblox/cryo@1.0" +LuaChatDeps = { git = "https://github.rbx.com/Roblox/lua-chat-deps" } +LuaDiscussionsDeps = { git = "https://github.rbx.com/Roblox/lua-discussions-deps" } +LuaSocialLibrariesDeps = { git = "https://github.rbx.com/Roblox/lua-social-libraries-deps" } +PremiumUpsellDeps = { git = "https://github.com/Roblox/premium-upsell-deps" } +AvatarExperienceDeps = { git = "https://github.com/Roblox/avatar-experience-deps" } +UIBlox = { git = "https://github.com/Roblox/uiblox", rev = "master" } +Otter = "github.com/roblox/otter@0.1" +t = "github.com/roblox/t@1.0" +enumerate = "github.com/roblox/enumerate@1.0.0" +PolicyProvider = { git = "https://github.com/Roblox/lua-roact-policy-provider", rev = "master" } +Lumberyak = { git = "https://github.rbx.com/Roblox/lumberyak" } +StringUtilities = "github.com/Roblox/string-utilities@1.0.0" +UrlBuilder = "github.com/Roblox/url-builder@1.0.1" + +# The following packages were ported from AppTempCommon +Promise = { git = "https://github.rbx.com/Roblox/lua-promise" } +Result = { git = "https://github.rbx.com/Roblox/lua-result" } +tutils = { git = "https://github.rbx.com/Roblox/tutils" } + +PurchasePrompt = { git = "https://github.com/Roblox/PurchasePromptScript-Roact", rev = "master" } + +InGameMenuDependencies = { git = "https://github.rbx.com/Roblox/in-game-menu-dependencies", rev = "master" } + +RoactGamepad = "github.com/roblox/roact-gamepad@0.4.4" + +[dev_dependencies] +TestEZ = "github.com/roblox/testez@0.3.1" +Rhodium = "github.com/roblox/rhodium@0.2.5" + +[patch."https://github.rbx.com/roblox/uiblox"] +git = "https://github.com/roblox/uiblox" +rev = "master" + +[patch."https://github.com/roblox/roact"] +target = "github.com/roblox/roact" +version = "1.2" + +[patch."https://github.com/roblox/cryo"] +target = "github.com/roblox/cryo" +version = "1.0" + +[patch."https://github.com/roblox/t"] +target = "github.com/roblox/t" +version = "1.0" + +[patch."https://github.com/roblox/rodux"] +target = "github.com/roblox/rodux" +version = "1.0" + +[patch."https://github.com/roblox/otter"] +target = "github.com/roblox/otter" +version = "0.1" diff --git a/Client2020/ExtraContent/LuaPackages/tutils.lua b/Client2020/ExtraContent/LuaPackages/tutils.lua new file mode 100644 index 0000000..1e69d3a --- /dev/null +++ b/Client2020/ExtraContent/LuaPackages/tutils.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.tutils) diff --git a/Client2020/ExtraContent/models/AvatarContextMenu/AvatarContextArrow.rbxm b/Client2020/ExtraContent/models/AvatarContextMenu/AvatarContextArrow.rbxm new file mode 100644 index 0000000..f8cc772 Binary files /dev/null and b/Client2020/ExtraContent/models/AvatarContextMenu/AvatarContextArrow.rbxm differ diff --git a/Client2020/ExtraContent/models/DataModelPatch/DataModelPatch.rbxm b/Client2020/ExtraContent/models/DataModelPatch/DataModelPatch.rbxm new file mode 100644 index 0000000..7918c55 Binary files /dev/null and b/Client2020/ExtraContent/models/DataModelPatch/DataModelPatch.rbxm differ diff --git a/Client2020/ExtraContent/places/InGameMenu.rbxl b/Client2020/ExtraContent/places/InGameMenu.rbxl new file mode 100644 index 0000000..fdd9a07 Binary files /dev/null and b/Client2020/ExtraContent/places/InGameMenu.rbxl differ diff --git a/Client2020/ExtraContent/places/Mobile.rbxl b/Client2020/ExtraContent/places/Mobile.rbxl new file mode 100644 index 0000000..1876579 Binary files /dev/null and b/Client2020/ExtraContent/places/Mobile.rbxl differ diff --git a/Client2020/ExtraContent/places/MobileChatPlace.rbxl b/Client2020/ExtraContent/places/MobileChatPlace.rbxl new file mode 100644 index 0000000..f087511 Binary files /dev/null and b/Client2020/ExtraContent/places/MobileChatPlace.rbxl differ diff --git a/Client2020/ExtraContent/places/RhodiumUnitTest.rbxl b/Client2020/ExtraContent/places/RhodiumUnitTest.rbxl new file mode 100644 index 0000000..5a813eb Binary files /dev/null and b/Client2020/ExtraContent/places/RhodiumUnitTest.rbxl differ diff --git a/Client2020/ExtraContent/textures/ui/AvatarExperience/AvatarExperienceSkyboxDarkTheme.png b/Client2020/ExtraContent/textures/ui/AvatarExperience/AvatarExperienceSkyboxDarkTheme.png new file mode 100644 index 0000000..19607f5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/AvatarExperience/AvatarExperienceSkyboxDarkTheme.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_1x_1.png b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_1x_1.png new file mode 100644 index 0000000..598b460 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_1x_1.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_1x_2.png b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_1x_2.png new file mode 100644 index 0000000..05d488f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_1x_2.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_1.png b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_1.png new file mode 100644 index 0000000..e4909e5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_1.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_2.png b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_2.png new file mode 100644 index 0000000..9b4d115 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_2.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_3.png b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_3.png new file mode 100644 index 0000000..418b1f4 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_3.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_4.png b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_4.png new file mode 100644 index 0000000..92a6ddd Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_4.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_5.png b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_5.png new file mode 100644 index 0000000..5071046 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_5.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_1.png b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_1.png new file mode 100644 index 0000000..fa03dbb Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_1.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_2.png b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_2.png new file mode 100644 index 0000000..3b42e87 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_2.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_3.png b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_3.png new file mode 100644 index 0000000..32d8877 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_3.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_1x_1.png b/Client2020/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_1x_1.png new file mode 100644 index 0000000..5bea3a6 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_1x_1.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_2x_1.png b/Client2020/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_2x_1.png new file mode 100644 index 0000000..2cee21b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_2x_1.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_3x_1.png b/Client2020/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_3x_1.png new file mode 100644 index 0000000..bb96425 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_3x_1.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_1x_1.png b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_1x_1.png new file mode 100644 index 0000000..b2ac46b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_1x_1.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_1x_2.png b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_1x_2.png new file mode 100644 index 0000000..23377e0 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_1x_2.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_1.png b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_1.png new file mode 100644 index 0000000..32c6cfc Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_1.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_2.png b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_2.png new file mode 100644 index 0000000..fd94dde Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_2.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_3.png b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_3.png new file mode 100644 index 0000000..31a6c27 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_3.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_4.png b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_4.png new file mode 100644 index 0000000..3bc2fa1 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_4.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_1.png b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_1.png new file mode 100644 index 0000000..cfbb87d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_1.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_2.png b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_2.png new file mode 100644 index 0000000..bd94c36 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_2.png differ diff --git a/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_3.png b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_3.png new file mode 100644 index 0000000..2b6a80e Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_3.png differ diff --git a/Client2020/ExtraContent/textures/ui/InGameChat/Caret.png b/Client2020/ExtraContent/textures/ui/InGameChat/Caret.png new file mode 100644 index 0000000..b16bad7 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/InGameChat/Caret.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px.png b/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px.png new file mode 100644 index 0000000..9219b54 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@2x.png new file mode 100644 index 0000000..74ee0db Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@3x.png new file mode 100644 index 0000000..63a2428 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator.png b/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator.png new file mode 100644 index 0000000..3f2bed0 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator@2x.png new file mode 100644 index 0000000..92be6e5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator@3x.png new file mode 100644 index 0000000..cc157ad Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-featured.png b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-featured.png new file mode 100644 index 0000000..716f358 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-featured.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-featured@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-featured@2x.png new file mode 100644 index 0000000..a03cced Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-featured@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-featured@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-featured@3x.png new file mode 100644 index 0000000..712b018 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-featured@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-popular.png b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-popular.png new file mode 100644 index 0000000..74becef Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-popular.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-popular@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-popular@2x.png new file mode 100644 index 0000000..bcf1fa1 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-popular@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-popular@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-popular@3x.png new file mode 100644 index 0000000..cd02edf Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-popular@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-top rated.png b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-top rated.png new file mode 100644 index 0000000..1b6f08a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-top rated.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-top rated@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-top rated@2x.png new file mode 100644 index 0000000..373be35 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-top rated@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-top rated@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-top rated@3x.png new file mode 100644 index 0000000..6a7dde2 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/category/ic-top rated@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up.png b/Client2020/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up.png new file mode 100644 index 0000000..73f833a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up@2x.png new file mode 100644 index 0000000..62314dd Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up@3x.png new file mode 100644 index 0000000..a9c91b2 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/CharacterShadow.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/CharacterShadow.png new file mode 100644 index 0000000..381c17c Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/CharacterShadow.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/DatePickerDivider.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/DatePickerDivider.png new file mode 100644 index 0000000..3d2702a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/DatePickerDivider.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/GridBackground.jpg b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/GridBackground.jpg new file mode 100644 index 0000000..e3ace0e Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/GridBackground.jpg differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/Vignette.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/Vignette.png new file mode 100644 index 0000000..1fdf8d9 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/Vignette.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/builderman.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/builderman.png new file mode 100644 index 0000000..00dfb9d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/builderman.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/gradient_bg.jpg b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/gradient_bg.jpg new file mode 100644 index 0000000..6585db8 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/gradient_bg.jpg differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/logo_white_1x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/logo_white_1x.png new file mode 100644 index 0000000..e4cb43a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/logo_white_1x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/reversevignette.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/reversevignette.png new file mode 100644 index 0000000..10ec0a9 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/Auth/reversevignette.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/CityBackground.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/CityBackground.png new file mode 100644 index 0000000..820d79a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/CityBackground.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/CompactView_purplelayer.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/CompactView_purplelayer.png new file mode 100644 index 0000000..5b3f9a8 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/CompactView_purplelayer.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/EducationalBackground.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/EducationalBackground.png new file mode 100644 index 0000000..2f20b00 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/EducationalBackground.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/GameDetailsBackground/abkg_general.jpg b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/GameDetailsBackground/abkg_general.jpg new file mode 100644 index 0000000..d143f4c Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/GameDetailsBackground/abkg_general.jpg differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/GameDetailsBackground/loadingBkg_base.jpg b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/GameDetailsBackground/loadingBkg_base.jpg new file mode 100644 index 0000000..534edd2 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/GameDetailsBackground/loadingBkg_base.jpg differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/TopBottomBorder.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/TopBottomBorder.png new file mode 100644 index 0000000..02506d4 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/TopBottomBorder.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/WideView_purpleLayer.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/WideView_purpleLayer.png new file mode 100644 index 0000000..4414da7 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/WideView_purpleLayer.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-add.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-add.png new file mode 100644 index 0000000..c863bce Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-add.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-add@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-add@2x.png new file mode 100644 index 0000000..7f8368f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-add@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-add@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-add@3x.png new file mode 100644 index 0000000..b8a5b30 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-add@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84.png new file mode 100644 index 0000000..1ac156d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@2x.png new file mode 100644 index 0000000..97f6bda Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@3x.png new file mode 100644 index 0000000..a167ba0 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90.png new file mode 100644 index 0000000..ac423bb Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@2x.png new file mode 100644 index 0000000..2e44709 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@3x.png new file mode 100644 index 0000000..1cf1cfa Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36.png new file mode 100644 index 0000000..3879628 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@2x.png new file mode 100644 index 0000000..952a375 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@3x.png new file mode 100644 index 0000000..baa0910 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle.png new file mode 100644 index 0000000..5a41676 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle@2x.png new file mode 100644 index 0000000..e4347f1 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle@3x.png new file mode 100644 index 0000000..437fd5c Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px.png new file mode 100644 index 0000000..3608a54 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px@2x.png new file mode 100644 index 0000000..3ca1bc2 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px@3x.png new file mode 100644 index 0000000..cadf11a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100.png new file mode 100644 index 0000000..19be24b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100@2x.png new file mode 100644 index 0000000..8f56820 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100@3x.png new file mode 100644 index 0000000..6cbb155 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/itemcardbkg_dark.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/itemcardbkg_dark.png new file mode 100644 index 0000000..e0c2211 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/itemcardbkg_dark.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection.png new file mode 100644 index 0000000..9c270b2 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection@2x.png new file mode 100644 index 0000000..03c37c8 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection@3x.png new file mode 100644 index 0000000..9379cf3 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noconnection.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noconnection.png new file mode 100644 index 0000000..4f5485f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noconnection.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noconnection@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noconnection@2x.png new file mode 100644 index 0000000..46004dc Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noconnection@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noconnection@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noconnection@3x.png new file mode 100644 index 0000000..4f9bb99 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/noconnection@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait.png new file mode 100644 index 0000000..d3091db Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait@2x.png new file mode 100644 index 0000000..a537692 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait@3x.png new file mode 100644 index 0000000..6c3298f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/playBtnBackground.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/playBtnBackground.png new file mode 100644 index 0000000..698bee4 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/playBtnBackground.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask.png new file mode 100644 index 0000000..ae751a4 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask@2x.png new file mode 100644 index 0000000..0cd07f6 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask@3x.png new file mode 100644 index 0000000..7145a4d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36.png new file mode 100644 index 0000000..3879628 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36@2x.png new file mode 100644 index 0000000..952a375 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36@3x.png new file mode 100644 index 0000000..baa0910 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer.png new file mode 100644 index 0000000..10baae3 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer@2x.png new file mode 100644 index 0000000..76bacac Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer_darkTheme.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer_darkTheme.png new file mode 100644 index 0000000..10baae3 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer_darkTheme.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer_darkTheme@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer_darkTheme@2x.png new file mode 100644 index 0000000..76bacac Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer_darkTheme@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer_lightTheme.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer_lightTheme.png new file mode 100644 index 0000000..10baae3 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer_lightTheme.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer_lightTheme@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer_lightTheme@2x.png new file mode 100644 index 0000000..76bacac Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/graphic/shimmer_lightTheme@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large.png new file mode 100644 index 0000000..55bd21a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large@2x.png new file mode 100644 index 0000000..a93c593 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large@3x.png new file mode 100644 index 0000000..a53b28c Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX.png new file mode 100644 index 0000000..7d690e8 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX@2x.png new file mode 100644 index 0000000..6d88c27 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX@3x.png new file mode 100644 index 0000000..16efb3c Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add-down.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add-down.png new file mode 100644 index 0000000..0a3bf2b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add-down.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add-down@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add-down@2x.png new file mode 100644 index 0000000..5dcd81b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add-down@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add-down@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add-down@3x.png new file mode 100644 index 0000000..80ca4f4 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add-down@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add.png new file mode 100644 index 0000000..649ae8f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add@2x.png new file mode 100644 index 0000000..77e1f58 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add@3x.png new file mode 100644 index 0000000..2b12c24 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-add@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right.png new file mode 100644 index 0000000..c490b34 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right@2x.png new file mode 100644 index 0000000..5ceb151 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right@3x.png new file mode 100644 index 0000000..256287d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot.png new file mode 100644 index 0000000..2b1bbec Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot@2x.png new file mode 100644 index 0000000..f7987d6 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot@3x.png new file mode 100644 index 0000000..ffe172d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20.png new file mode 100644 index 0000000..577160d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20@2x.png new file mode 100644 index 0000000..47429b9 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20@3x.png new file mode 100644 index 0000000..c5643d2 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-favorite-filled.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-favorite-filled.png new file mode 100644 index 0000000..8fa0b77 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-favorite-filled.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-favorite-filled@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-favorite-filled@2x.png new file mode 100644 index 0000000..9419d89 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-favorite-filled@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-favorite.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-favorite.png new file mode 100644 index 0000000..fd51ec1 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-favorite.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-favorite@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-favorite@2x.png new file mode 100644 index 0000000..ef6f41b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-favorite@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-game.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-game.png new file mode 100644 index 0000000..b1b1b9a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-game.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-game@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-game@2x.png new file mode 100644 index 0000000..253963b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-game@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-game@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-game@3x.png new file mode 100644 index 0000000..e488025 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-game@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-games.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-games.png new file mode 100644 index 0000000..8d60528 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-games.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-games@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-games@2x.png new file mode 100644 index 0000000..5efd3a2 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-games@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-games@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-games@3x.png new file mode 100644 index 0000000..e27091b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-games@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-about.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-about.png new file mode 100644 index 0000000..9f15260 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-about.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-about@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-about@2x.png new file mode 100644 index 0000000..27a89a7 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-about@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-about@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-about@3x.png new file mode 100644 index 0000000..ba4742d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-about@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog.png new file mode 100644 index 0000000..cabd391 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog@2x.png new file mode 100644 index 0000000..84d3181 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog@3x.png new file mode 100644 index 0000000..9a71c4f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club.png new file mode 100644 index 0000000..a729363 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club@2x.png new file mode 100644 index 0000000..15ab42e Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club@3x.png new file mode 100644 index 0000000..ecbfe67 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog.png new file mode 100644 index 0000000..d6d740c Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog@2x.png new file mode 100644 index 0000000..10f0c09 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog@3x.png new file mode 100644 index 0000000..a2b13c6 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-create.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-create.png new file mode 100644 index 0000000..7db7b0a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-create.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-create@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-create@2x.png new file mode 100644 index 0000000..ad53a89 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-create@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-create@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-create@3x.png new file mode 100644 index 0000000..05498c8 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-create@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-events.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-events.png new file mode 100644 index 0000000..a586238 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-events.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-events@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-events@2x.png new file mode 100644 index 0000000..0940fc0 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-events@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-events@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-events@3x.png new file mode 100644 index 0000000..a76e24f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-events@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends.png new file mode 100644 index 0000000..75366b7 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends@2x.png new file mode 100644 index 0000000..3629480 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends@3x.png new file mode 100644 index 0000000..91e86f5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups.png new file mode 100644 index 0000000..b07c7b9 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups@2x.png new file mode 100644 index 0000000..1e111ac Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups@3x.png new file mode 100644 index 0000000..412bf34 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-help.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-help.png new file mode 100644 index 0000000..bbe33d6 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-help.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-help@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-help@2x.png new file mode 100644 index 0000000..8529801 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-help@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-help@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-help@3x.png new file mode 100644 index 0000000..6f569db Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-help@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory.png new file mode 100644 index 0000000..6a7a91d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory@2x.png new file mode 100644 index 0000000..df440ed Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory@3x.png new file mode 100644 index 0000000..b622838 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-message.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-message.png new file mode 100644 index 0000000..deaef34 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-message.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-message@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-message@2x.png new file mode 100644 index 0000000..676fa23 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-message@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-message@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-message@3x.png new file mode 100644 index 0000000..0b3e1cb Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-message@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed.png new file mode 100644 index 0000000..d3260bc Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed@2x.png new file mode 100644 index 0000000..1c99d76 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed@3x.png new file mode 100644 index 0000000..dc67f5d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile.png new file mode 100644 index 0000000..86450b1 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile@2x.png new file mode 100644 index 0000000..d204bff Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile@3x.png new file mode 100644 index 0000000..c570d88 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings.png new file mode 100644 index 0000000..7a0f9d1 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings@2x.png new file mode 100644 index 0000000..03a254b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings@3x.png new file mode 100644 index 0000000..375b1e5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more.png new file mode 100644 index 0000000..bad7231 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more@2x.png new file mode 100644 index 0000000..2f90207 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more@3x.png new file mode 100644 index 0000000..2bc8325 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-more@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20.png new file mode 100644 index 0000000..b8ec87e Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20@2x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20@2x.png new file mode 100644 index 0000000..b5fa7e8 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20@3x.png b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20@3x.png new file mode 100644 index 0000000..f5f12f5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/btn-control-sm.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/btn-control-sm.png new file mode 100644 index 0000000..0623973 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/btn-control-sm.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right.png new file mode 100644 index 0000000..b0a79f4 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right@2x.png new file mode 100644 index 0000000..409dbfb Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right@3x.png new file mode 100644 index 0000000..22ed017 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip.png new file mode 100644 index 0000000..e00403f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@2x.png new file mode 100644 index 0000000..cb4b777 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@3x.png new file mode 100644 index 0000000..4f2ad91 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self.png new file mode 100644 index 0000000..e6c833d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2.png new file mode 100644 index 0000000..b6115fc Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2@2x.png new file mode 100644 index 0000000..5bacef2 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2@3x.png new file mode 100644 index 0000000..96fb44d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self@2x.png new file mode 100644 index 0000000..d1128bb Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self@3x.png new file mode 100644 index 0000000..9f824af Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right.png new file mode 100644 index 0000000..517b79a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@2x.png new file mode 100644 index 0000000..49fed32 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@3x.png new file mode 100644 index 0000000..96e8a1e Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip.png new file mode 100644 index 0000000..c3e7bf6 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip@2x.png new file mode 100644 index 0000000..e0ef139 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip@3x.png new file mode 100644 index 0000000..1701d5a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble.png new file mode 100644 index 0000000..7c6ec32 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2.png new file mode 100644 index 0000000..fc86247 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2@2x.png new file mode 100644 index 0000000..f7b7d40 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2@3x.png new file mode 100644 index 0000000..bd0302d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble@2x.png new file mode 100644 index 0000000..8800522 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble@3x.png new file mode 100644 index 0000000..b627cb4 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/error-toast.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/error-toast.png new file mode 100644 index 0000000..3fcdffd Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/error-toast.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/error-toast@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/error-toast@2x.png new file mode 100644 index 0000000..b514105 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/error-toast@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/error-toast@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/error-toast@3x.png new file mode 100644 index 0000000..0cf667d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/error-toast@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon.png new file mode 100644 index 0000000..8e2ae96 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon@2x.png new file mode 100644 index 0000000..c7020c7 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon@3x.png new file mode 100644 index 0000000..0025b77 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/hello-button.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/hello-button.png new file mode 100644 index 0000000..22acfff Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/hello-button.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/hello-button@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/hello-button@2x.png new file mode 100644 index 0000000..415d1f5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/hello-button@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/hello-button@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/hello-button@3x.png new file mode 100644 index 0000000..5935ab4 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/hello-button@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-default.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-default.png new file mode 100644 index 0000000..a1e3247 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-default.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-default@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-default@2x.png new file mode 100644 index 0000000..bad7c52 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-default@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-default@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-default@3x.png new file mode 100644 index 0000000..e92e175 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-default@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message.png new file mode 100644 index 0000000..0384815 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message@2x.png new file mode 100644 index 0000000..ea0a875 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message@3x.png new file mode 100644 index 0000000..19d5308 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/modal.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/modal.png new file mode 100644 index 0000000..b127799 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/modal.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/modal@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/modal@2x.png new file mode 100644 index 0000000..1ffc6a9 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/modal@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/modal@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/modal@3x.png new file mode 100644 index 0000000..12d0f90 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/modal@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator.png new file mode 100644 index 0000000..38dd53a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator@2x.png new file mode 100644 index 0000000..3aa02fd Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator@3x.png new file mode 100644 index 0000000..1287b87 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar.png new file mode 100644 index 0000000..c47871b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar@2x.png new file mode 100644 index 0000000..bb7c513 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar@3x.png new file mode 100644 index 0000000..3e15a0d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/search.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/search.png new file mode 100644 index 0000000..6fdddea Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/search.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/search@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/search@2x.png new file mode 100644 index 0000000..857edd9 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/search@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/search@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/search@3x.png new file mode 100644 index 0000000..eb8a142 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/search@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/system-message.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/system-message.png new file mode 100644 index 0000000..d26555b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/system-message.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/system-message@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/system-message@2x.png new file mode 100644 index 0000000..3d85f6f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/system-message@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/system-message@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/system-message@3x.png new file mode 100644 index 0000000..3e8c804 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/system-message@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble.png new file mode 100644 index 0000000..75f482a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble@2x.png new file mode 100644 index 0000000..c1a412f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble@3x.png new file mode 100644 index 0000000..7ba23b8 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/friendmask.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/friendmask.png new file mode 100644 index 0000000..a42205f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/friendmask.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24.png new file mode 100644 index 0000000..35db759 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24@2x.png new file mode 100644 index 0000000..4886ac6 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24@3x.png new file mode 100644 index 0000000..b444c76 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60.png new file mode 100644 index 0000000..0deee67 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60@2x.png new file mode 100644 index 0000000..34a58a7 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60@3x.png new file mode 100644 index 0000000..8daff78 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52.png new file mode 100644 index 0000000..d3725fe Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52@2x.png new file mode 100644 index 0000000..c775cab Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10.png new file mode 100644 index 0000000..8d7fab3 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@2x.png new file mode 100644 index 0000000..07f3a24 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@3x.png new file mode 100644 index 0000000..a5de74f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12.png new file mode 100644 index 0000000..8b59d6a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@2x.png new file mode 100644 index 0000000..80c2bd4 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@3x.png new file mode 100644 index 0000000..e9eb973 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14.png new file mode 100644 index 0000000..1200f62 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@2x.png new file mode 100644 index 0000000..6f214d9 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@3x.png new file mode 100644 index 0000000..8afb3a6 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6.png new file mode 100644 index 0000000..b9d4a49 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@2x.png new file mode 100644 index 0000000..8b59d6a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@3x.png new file mode 100644 index 0000000..1f90cfd Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8.png new file mode 100644 index 0000000..be8d8c5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@2x.png new file mode 100644 index 0000000..c9632ff Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@3x.png new file mode 100644 index 0000000..0d194b0 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame.png new file mode 100644 index 0000000..f622033 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame@2x.png new file mode 100644 index 0000000..24ff925 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame@3x.png new file mode 100644 index 0000000..6866f33 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10.png new file mode 100644 index 0000000..70bef5b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@2x.png new file mode 100644 index 0000000..b7d0d3f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@3x.png new file mode 100644 index 0000000..1daa6c7 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12.png new file mode 100644 index 0000000..32b5368 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12@3x.png new file mode 100644 index 0000000..7c38197 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x2@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x2@2x.png new file mode 100644 index 0000000..6a0afba Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x2@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14.png new file mode 100644 index 0000000..bb74b97 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@2x.png new file mode 100644 index 0000000..1015a19 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@3x.png new file mode 100644 index 0000000..eeb8cee Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8.png new file mode 100644 index 0000000..3d1a10a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@2x.png new file mode 100644 index 0000000..f2d5ec5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@3x.png new file mode 100644 index 0000000..fb9489f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio.png new file mode 100644 index 0000000..94c64f4 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio@2x.png new file mode 100644 index 0000000..0c6d285 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio@3x.png new file mode 100644 index 0000000..db89806 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6.png new file mode 100644 index 0000000..afd86c0 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@2x.png new file mode 100644 index 0000000..32b5368 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@3x.png new file mode 100644 index 0000000..970a691 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10.png new file mode 100644 index 0000000..6dd796d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@2x.png new file mode 100644 index 0000000..6632f9b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@3x.png new file mode 100644 index 0000000..2c39a06 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12.png new file mode 100644 index 0000000..e8b271d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@2x.png new file mode 100644 index 0000000..6c3645b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@3x.png new file mode 100644 index 0000000..3b76e80 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14.png new file mode 100644 index 0000000..4ee79a3 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@2x.png new file mode 100644 index 0000000..2673de7 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@3x.png new file mode 100644 index 0000000..26b4071 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6.png new file mode 100644 index 0000000..d9f9181 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@2x.png new file mode 100644 index 0000000..e8b271d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@3x.png new file mode 100644 index 0000000..e3e9c60 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8.png new file mode 100644 index 0000000..d26c309 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@2x.png new file mode 100644 index 0000000..b3428ea Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@3x.png new file mode 100644 index 0000000..6a14d5c Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online.png new file mode 100644 index 0000000..a8efbec Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online@2x.png new file mode 100644 index 0000000..1133089 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online@3x.png new file mode 100644 index 0000000..5c61715 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48.png new file mode 100644 index 0000000..47bd340 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48@2x.png new file mode 100644 index 0000000..578a10a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers.png new file mode 100644 index 0000000..f59ead5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers@2x.png new file mode 100644 index 0000000..b4b9fbe Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers@3x.png new file mode 100644 index 0000000..4f3279f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow.png new file mode 100644 index 0000000..4c1fa77 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow@2x.png new file mode 100644 index 0000000..42e5a34 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow@3x.png new file mode 100644 index 0000000..64615ab Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36.png new file mode 100644 index 0000000..ecab811 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36@2x.png new file mode 100644 index 0000000..03b94cc Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36@3x.png new file mode 100644 index 0000000..b0d22fe Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted.png new file mode 100644 index 0000000..5917d8d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@2x.png new file mode 100644 index 0000000..911d0f7 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@3x.png new file mode 100644 index 0000000..c27bf49 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48.png new file mode 100644 index 0000000..e2f28f8 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48@2x.png new file mode 100644 index 0000000..b1cd13b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48@3x.png new file mode 100644 index 0000000..9a0a00a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on.png new file mode 100644 index 0000000..59255c1 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on@2x.png new file mode 100644 index 0000000..407fa57 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on@3x.png new file mode 100644 index 0000000..ed4ad48 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send.png new file mode 100644 index 0000000..b5f28b2 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send@2x.png new file mode 100644 index 0000000..d647c1b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send@3x.png new file mode 100644 index 0000000..aced5ec Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/gr-send@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on.png new file mode 100644 index 0000000..0390cb4 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on@2x.png new file mode 100644 index 0000000..9ee440a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on@3x.png new file mode 100644 index 0000000..b897b38 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox.png new file mode 100644 index 0000000..cfd7260 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox@2x.png new file mode 100644 index 0000000..7a04603 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox@3x.png new file mode 100644 index 0000000..09fd802 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/indicator-background.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/indicator-background.png new file mode 100644 index 0000000..c36fb92 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/indicator-background.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/send-white.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/send-white.png new file mode 100644 index 0000000..e9df684 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/send-white.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/send-white@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/send-white@2x.png new file mode 100644 index 0000000..030eeee Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/send-white@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/graphic/send-white@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/send-white@3x.png new file mode 100644 index 0000000..af160d0 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/graphic/send-white@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends.png new file mode 100644 index 0000000..a68367b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends@2x.png new file mode 100644 index 0000000..6e1b632 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends@3x.png new file mode 100644 index 0000000..b2a1be0 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-alert.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-alert.png new file mode 100644 index 0000000..85d6d39 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-alert.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-alert@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-alert@2x.png new file mode 100644 index 0000000..7fb786a Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-alert@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-alert@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-alert@3x.png new file mode 100644 index 0000000..ab44ae0 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-alert@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-back-android.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-back-android.png new file mode 100644 index 0000000..2fb40d1 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-back-android.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-back-android@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-back-android@2x.png new file mode 100644 index 0000000..af6490d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-back-android@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-back.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-back.png new file mode 100644 index 0000000..42a2433 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-back.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-back@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-back@2x.png new file mode 100644 index 0000000..e2b4462 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-back@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-bc.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-bc.png new file mode 100644 index 0000000..acfbdb5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-bc.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-bc@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-bc@2x.png new file mode 100644 index 0000000..be1c042 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-bc@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-bc@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-bc@3x.png new file mode 100644 index 0000000..85d2fbd Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-bc@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large.png new file mode 100644 index 0000000..5a7141d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large@2x.png new file mode 100644 index 0000000..5673f80 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large@3x.png new file mode 100644 index 0000000..f07c89e Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-check.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-check.png new file mode 100644 index 0000000..26d3557 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-check.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-check@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-check@2x.png new file mode 100644 index 0000000..7791da1 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-check@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-check@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-check@3x.png new file mode 100644 index 0000000..38cc530 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-check@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy.png new file mode 100644 index 0000000..511e290 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy@2x.png new file mode 100644 index 0000000..d2aa085 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy@3x.png new file mode 100644 index 0000000..fcd44c7 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray.png new file mode 100644 index 0000000..f39bdfc Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray@2x.png new file mode 100644 index 0000000..18e07bd Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray@3x.png new file mode 100644 index 0000000..c746437 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid.png new file mode 100644 index 0000000..5839bd5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid@2x.png new file mode 100644 index 0000000..48b7bbf Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid@3x.png new file mode 100644 index 0000000..5ec68b8 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2.png new file mode 100644 index 0000000..1dd1e05 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2@2x.png new file mode 100644 index 0000000..3a845ff Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2@3x.png new file mode 100644 index 0000000..3326324 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-close-white.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-close-white.png new file mode 100644 index 0000000..af8f7b8 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-close-white.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-create-group.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-create-group.png new file mode 100644 index 0000000..72c902c Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-create-group.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-create-group@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-create-group@2x.png new file mode 100644 index 0000000..6041c11 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-create-group@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-create-group@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-create-group@3x.png new file mode 100644 index 0000000..569e60d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-create-group@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24.png new file mode 100644 index 0000000..ad3c8f9 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24@2x.png new file mode 100644 index 0000000..2dfef75 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24@3x.png new file mode 100644 index 0000000..73ee9b8 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-friends.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-friends.png new file mode 100644 index 0000000..c2dddec Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-friends.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-friends@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-friends@2x.png new file mode 100644 index 0000000..aafb10e Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-friends@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-friends@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-friends@3x.png new file mode 100644 index 0000000..c2610a8 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-friends@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24.png new file mode 100644 index 0000000..a04a67c Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24@2x.png new file mode 100644 index 0000000..a8ee17d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24@3x.png new file mode 100644 index 0000000..9276f55 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game.png new file mode 100644 index 0000000..aeb09dc Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game@2x.png new file mode 100644 index 0000000..233dbe2 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game@3x.png new file mode 100644 index 0000000..4334107 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-game@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16.png new file mode 100644 index 0000000..4be38ac Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16@2x.png new file mode 100644 index 0000000..2d02bf0 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16@3x.png new file mode 100644 index 0000000..a3204eb Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group.png new file mode 100644 index 0000000..db3b1b7 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group@2x.png new file mode 100644 index 0000000..48ec989 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group@3x.png new file mode 100644 index 0000000..38a5549 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-group@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-info.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-info.png new file mode 100644 index 0000000..0a9e434 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-info.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-info@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-info@2x.png new file mode 100644 index 0000000..2801509 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-info@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-info@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-info@3x.png new file mode 100644 index 0000000..2fb4765 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-info@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-leave.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-leave.png new file mode 100644 index 0000000..136f273 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-leave.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-leave@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-leave@2x.png new file mode 100644 index 0000000..53f544f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-leave@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-leave@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-leave@3x.png new file mode 100644 index 0000000..57966c6 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-leave@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-more.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-more.png new file mode 100644 index 0000000..718010b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-more.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-more@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-more@2x.png new file mode 100644 index 0000000..d107f2f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-more@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-more@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-more@3x.png new file mode 100644 index 0000000..7d871a6 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-more@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-nametag.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-nametag.png new file mode 100644 index 0000000..eee203e Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-nametag.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-nametag@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-nametag@2x.png new file mode 100644 index 0000000..1dc795d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-nametag@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-nametag@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-nametag@3x.png new file mode 100644 index 0000000..ad7710f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-nametag@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-notification.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-notification.png new file mode 100644 index 0000000..7cc3f5d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-notification.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-notification@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-notification@2x.png new file mode 100644 index 0000000..7db1065 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-notification@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-notification@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-notification@3x.png new file mode 100644 index 0000000..c49aa2b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-notification@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pin.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pin.png new file mode 100644 index 0000000..f7a8d92 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pin.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pin@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pin@2x.png new file mode 100644 index 0000000..9befe4b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pin@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pin@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pin@3x.png new file mode 100644 index 0000000..7387c33 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pin@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed.png new file mode 100644 index 0000000..b893f0b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed@2x.png new file mode 100644 index 0000000..1103fa5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed@3x.png new file mode 100644 index 0000000..37bfbc5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-profile.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-profile.png new file mode 100644 index 0000000..fdf6f87 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-profile.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-profile@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-profile@2x.png new file mode 100644 index 0000000..8649c34 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-profile@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-profile@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-profile@3x.png new file mode 100644 index 0000000..bd3fe06 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-profile@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-remove.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-remove.png new file mode 100644 index 0000000..089dba9 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-remove.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-remove@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-remove@2x.png new file mode 100644 index 0000000..a43d237 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-remove@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-remove@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-remove@3x.png new file mode 100644 index 0000000..0185b15 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-remove@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-resend.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-resend.png new file mode 100644 index 0000000..464bc87 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-resend.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-resend@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-resend@2x.png new file mode 100644 index 0000000..065d260 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-resend@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-resend@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-resend@3x.png new file mode 100644 index 0000000..c93f1d8 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-resend@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-robux.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-robux.png new file mode 100644 index 0000000..30e32cf Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-robux.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-robux@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-robux@2x.png new file mode 100644 index 0000000..b181a12 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-robux@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-robux@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-robux@3x.png new file mode 100644 index 0000000..67f39f5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-robux@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray.png new file mode 100644 index 0000000..f2e0f74 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray@2x.png new file mode 100644 index 0000000..52661b5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray@3x.png new file mode 100644 index 0000000..365865f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search.png new file mode 100644 index 0000000..fefae0c Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search@2x.png new file mode 100644 index 0000000..8585a75 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search@3x.png new file mode 100644 index 0000000..ed9ff2f Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-search@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-send.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-send.png new file mode 100644 index 0000000..c6951c1 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-send.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-send@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-send@2x.png new file mode 100644 index 0000000..d0afa8b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-send@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-send@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-send@3x.png new file mode 100644 index 0000000..41a0658 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-send@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20.png new file mode 100644 index 0000000..61f6d2e Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20@2x.png new file mode 100644 index 0000000..6661058 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20@3x.png new file mode 100644 index 0000000..be0e5a8 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20.png new file mode 100644 index 0000000..b8ec87e Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20@2x.png new file mode 100644 index 0000000..b5fa7e8 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20@3x.png new file mode 100644 index 0000000..f5f12f5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24.png new file mode 100644 index 0000000..119547d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24@2x.png new file mode 100644 index 0000000..47ecc6b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24@3x.png new file mode 100644 index 0000000..0bc59ee Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24.png new file mode 100644 index 0000000..e72edaa Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@2x.png new file mode 100644 index 0000000..3691a33 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@3x.png new file mode 100644 index 0000000..bf9e31b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack.png new file mode 100644 index 0000000..b567c45 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack@2x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack@2x.png new file mode 100644 index 0000000..890f7f9 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack@3x.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack@3x.png new file mode 100644 index 0000000..a0b42ad Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChat/icons/share-game-thumbnail.png b/Client2020/ExtraContent/textures/ui/LuaChat/icons/share-game-thumbnail.png new file mode 100644 index 0000000..1c4b30e Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChat/icons/share-game-thumbnail.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_checkbox.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_checkbox.png new file mode 100644 index 0000000..8bf4c14 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_checkbox.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose.png new file mode 100644 index 0000000..b87f16d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose@2x.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose@2x.png new file mode 100644 index 0000000..690d057 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose@3x.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose@3x.png new file mode 100644 index 0000000..634ee57 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff.png new file mode 100644 index 0000000..6e2cd7b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff@2x.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff@2x.png new file mode 100644 index 0000000..8f7339b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff@3x.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff@3x.png new file mode 100644 index 0000000..07422ba Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn.png new file mode 100644 index 0000000..7420fc4 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn@2x.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn@2x.png new file mode 100644 index 0000000..6e710f5 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn@3x.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn@3x.png new file mode 100644 index 0000000..ac4501c Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/common_search.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/common_search.png new file mode 100644 index 0000000..cd381bf Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/common_search.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/common_search@2x.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/common_search@2x.png new file mode 100644 index 0000000..3eb8a4d Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/common_search@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/common_search@3x.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/common_search@3x.png new file mode 100644 index 0000000..2638ca1 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/common_search@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/ic-add-friends.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/ic-add-friends.png new file mode 100644 index 0000000..199de94 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/ic-add-friends.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/ic-friend-empty-border.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/ic-friend-empty-border.png new file mode 100644 index 0000000..2bcf309 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/ic-friend-empty-border.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack.png new file mode 100644 index 0000000..b567c45 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack@2x.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack@2x.png new file mode 100644 index 0000000..890f7f9 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack@3x.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack@3x.png new file mode 100644 index 0000000..a0b42ad Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight.png new file mode 100644 index 0000000..4f88566 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight@2x.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight@2x.png new file mode 100644 index 0000000..fce85d6 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight@3x.png b/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight@3x.png new file mode 100644 index 0000000..e4e78de Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonFill.png b/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonFill.png new file mode 100644 index 0000000..afae8bb Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonFill.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonFill@2x.png b/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonFill@2x.png new file mode 100644 index 0000000..0f8157c Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonFill@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonFill@3x.png b/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonFill@3x.png new file mode 100644 index 0000000..72a5371 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonFill@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonStroke.png b/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonStroke.png new file mode 100644 index 0000000..70b102b Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonStroke.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonStroke@2x.png b/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonStroke@2x.png new file mode 100644 index 0000000..cee0a86 Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonStroke@2x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonStroke@3x.png b/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonStroke@3x.png new file mode 100644 index 0000000..0ac6f5c Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaDiscussions/buttonStroke@3x.png differ diff --git a/Client2020/ExtraContent/textures/ui/LuaDiscussions/search.png b/Client2020/ExtraContent/textures/ui/LuaDiscussions/search.png new file mode 100644 index 0000000..cd381bf Binary files /dev/null and b/Client2020/ExtraContent/textures/ui/LuaDiscussions/search.png differ diff --git a/Client2020/ExtraContent/translations/CoreScriptLocalization.csv b/Client2020/ExtraContent/translations/CoreScriptLocalization.csv new file mode 100644 index 0000000..66e496a --- /dev/null +++ b/Client2020/ExtraContent/translations/CoreScriptLocalization.csv @@ -0,0 +1,721 @@ +Key,Context,Example,Source,de,en-us,es,es-es,fr,it,ja,ko,pt,pt-br,ru,zh-cn,zh-tw,zh-cjv +Authentication.Login.WeChat.AntiAddictionText,,,"Boycott bad games, refuse pirated games. Be aware of self-defense and being deceived. Playing games is good for your brain, but too much game play can harm your health. Manage your time well and enjoy a healthy lifestyle.","Boykottiere schlechte Spiele und lehne Raubkopien ab. Sei dir über Selbstverteidigung und Täuschungsversuche im Klaren. Spielen ist gut für dein Gehirn, aber zu viel des Guten kann deine Gesundheit beeinträchtigen. Teile dir deine Zeit gut ein und führe einen gesunden Lebensstil.","Boycott bad games, refuse pirated games. Be aware of self-defense and being deceived. Playing games is good for your brain, but too much game play can harm your health. Manage your time well and enjoy a healthy lifestyle.","Boicotea juegos con contenido inapropiado y di no a la piratería. Protégete y no dejes que te engañen. Jugar a videojuegos es bueno, pero jugar demasiado puede perjudicar tu salud. Gestiona bien tu tiempo para poder vivir una vida saludable.","Boicotea juegos con contenido inapropiado y di no a la piratería. Protégete y no dejes que te engañen. Jugar a videojuegos es bueno, pero jugar demasiado puede perjudicar tu salud. Gestiona bien tu tiempo para poder vivir una vida saludable.","Boycottez les mauvais jeux, évitez les jeux piratés. Faites attention à votre sécurité et ne vous faites pas berner. Jouer a des effets positifs sur le cerveau, mais en abuser risque de nuire à votre santé. Gérez votre temps convenablement et menez une vie saine.","Boicotta i giochi scadenti, rifiuta i giochi pirata. Difenditi e stai attento a non farti ingannare. I videogiochi sono un ottimo esercizio per la mente, ma esagerando diventano dannosi per la salute. Gestisci bene il tuo tempo e goditi uno stile di vita sano.",不適切なゲームには参加しないように心掛け、海賊版は拒否しましょう。自衛意識を高め、騙されないようにしましょう。ゲームをプレイすることは脳の働きを高めてくれますが、プレイしすぎると健康を害する恐れがあります。時間管理をきちんと行い、健康的なライフスタイルをお楽しみください。,"건전하지 않은 게임과 저작권 문제가 있는 게임은 플레이하지 마시고, 사기 행위에 연루되지 않도록 스스로를 보호하세요. 게임은 두뇌 발달에 도움이 되지만, 지나친 게임 플레이는 건강에 좋지 않습니다. 플레이 시간을 잘 조절해서 건강하게 게임을 즐기세요.","Boicote jogos ruins, recuse jogos pirateados. Saiba se defender e lidar com fraudes. Jogar jogos é bom para o cérebro, mas jogar demais pode prejudicar sua saúde. Administre bem o seu tempo e desfrute de um estilo de vida saudável.","Boicote jogos ruins, recuse jogos pirateados. Saiba se defender e lidar com fraudes. Jogar jogos é bom para o cérebro, mas jogar demais pode prejudicar sua saúde. Administre bem o seu tempo e desfrute de um estilo de vida saudável.","Избегайте плохих и пиратских игр. Будьте осторожны, и вас не обманут. Игры помогают развивать мозг, однако не надо с этим злоупотреблять – это может нанести вред здоровью. Разумно распределяйте свое время и наслаждайтесь правильным образом жизни!",抵制不良游戏,拒绝盗版游戏。注意自我保护,谨防受骗上当。适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。,抵制劣質與抄襲遊戲!玩遊戲有益身心,但過度沉迷會對身體造成影響。控制遊戲時間,享受健康人生!,抵制不良游戏,拒绝盗版游戏。注意自我保护,谨防受骗上当。适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。 +Authentication.Login.Label.WeChatAntiAddictionText,,,"Boycott bad games, refuse pirated games. Be aware of self-defense and being deceived. Playing games is good for your brain, but too much game play can harm your health. Manage your time well and enjoy a healthy lifestyle.","Boykottiere schlechte Spiele und lehne Raubkopien ab. Sei dir über Selbstverteidigung und Täuschungsversuche im Klaren. Spielen ist gut für dein Gehirn, aber zu viel des Guten kann deine Gesundheit beeinträchtigen. Teile dir deine Zeit gut ein und führe einen gesunden Lebensstil.","Boycott bad games, refuse pirated games. Be aware of self-defense and being deceived. Playing games is good for your brain, but too much game play can harm your health. Manage your time well and enjoy a healthy lifestyle.","Boicotea juegos con contenido inapropiado y di no a la piratería. No permitas que te engañen. Jugar a videojuegos es bueno, pero jugar demasiado puede perjudicar tu salud. Gestiona bien tu tiempo para vivir una vida balanceada y saludable.","Boicotea juegos con contenido inapropiado y di no a la piratería. No permitas que te engañen. Jugar a videojuegos es bueno, pero jugar demasiado puede perjudicar tu salud. Gestiona bien tu tiempo para vivir una vida balanceada y saludable.","Boycottez les mauvais jeux, évitez les jeux piratés. Faites attention à votre sécurité et ne vous faites pas berner. Jouer a des effets positifs sur le cerveau, mais en abuser risque de nuire à votre santé. Gérez votre temps convenablement et menez une vie saine.","Boicotta i giochi scadenti, rifiuta i giochi pirata. Difenditi e stai attento a non farti ingannare. I videogiochi sono un ottimo esercizio per la mente, ma esagerando diventano dannosi per la salute. Gestisci bene il tuo tempo e goditi uno stile di vita sano.",不適切なゲームには参加しないように心掛け、海賊版は拒否しましょう。自衛意識を高め、騙されないようにしましょう。ゲームをプレイすることは脳の働きを高めてくれますが、プレイしすぎると健康を害する恐れがあります。時間管理をきちんと行い、健康的なライフスタイルをお楽しみください。,"건전하지 않은 게임과 저작권 문제가 있는 게임은 플레이하지 마시고, 사기 행위에 연루되지 않도록 스스로를 보호하세요. 게임은 두뇌 발달에 도움이 되지만, 지나친 게임 플레이는 건강에 좋지 않습니다. 플레이 시간을 잘 조절해서 건강하게 게임을 즐기세요.","Boicote jogos ruins, recuse jogos pirateados. Saiba se defender e lidar com fraudes. Jogar jogos é bom para o cérebro, mas jogar demais pode prejudicar a saúde. Administre bem o seu tempo e desfrute de um estilo de vida saudável.","Boicote jogos ruins, recuse jogos pirateados. Saiba se defender e lidar com fraudes. Jogar jogos é bom para o cérebro, mas jogar demais pode prejudicar a saúde. Administre bem o seu tempo e desfrute de um estilo de vida saudável.","Избегайте плохих и пиратских игр. Будьте осторожны, и вас не обманут. Игры помогают развивать мозг, однако не надо ими злоупотреблять – это может нанести вред здоровью. Разумно распределяйте свое время и наслаждайтесь правильным образом жизни!",抵制不良游戏,拒绝盗版游戏。注意自我保护,谨防受骗上当。适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。,抵制劣質與抄襲遊戲!玩遊戲有益身心,但過度沉迷會對身體造成影響。控制遊戲時間,享受健康人生!,抵制不良游戏,拒绝盗版游戏。注意自我保护,谨防受骗上当。适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。 +Corescripts.AvatarContextMenu.FriendRequestPending,,,Friend Request Pending,Freundesanfrage ausstehend,Friend Request Pending,Solicitud de amistad pendiente,Solicitud de amistad pendiente,Demande d'ami en attente,Richiesta di amicizia in sospeso,友達リクエスト中,친구 요청 대기 중,Pedido de amizade pendente,Pedido de amizade pendente,Запрос о дружбе на рассмотрении,好友邀请待处理,好友邀請待處理,好友邀请待处理 +Corescripts.AvatarContextMenu.AddFriend,,,Add Friend,Freund hinzufügen,Add Friend,Añadir amigo,Añadir amigo,Ajouter un ami,Aggiungi amico,友達を追加,친구 추가,Adicionar amigo,Adicionar amigo,Добавить друга,添加好友,新增好友,添加好友 +Corescripts.AvatarContextMenu.Friends,,,Friends,Freunde,Friends,Amigos,Amigos,Amis,Amici,友達,친구,Amigos,Amigos,Друзья,好友,好友,好友 +Corescripts.AvatarContextMenu.AcceptFriendRequest,,,Accept Friend Request,Freundesanfrage annehmen,Accept Friend Request,Aceptar solicitud de amistad,Aceptar solicitud de amistad,Accepter l'invitation d'un ami,Accetta richiesta di amicizia,友達リクエストを承認する,친구 요청 수락,Aceitar pedido de amizade,Aceitar pedido de amizade,Принять запрос дружбы,接受好友邀请,接受好友邀請,接受好友邀请 +Corescripts.AvatarContextMenu.PlayerBlocked,,,Player Blocked,Spieler wurde gesperrt,Player Blocked,Jugador bloqueado,Jugador bloqueado,Joueur bloqué,Giocatore bloccato,プレイヤーをブロックしました,플레이어 차단됨,Jogador bloqueado,Jogador bloqueado,Заблокированные игроки,已屏蔽此玩家,已封鎖此玩家,已屏蔽此玩家 +Corescripts.AvatarContextMenu.Chat,,,Chat,Chat,Chat,Chat,Chat,Chat,Chat,チャット,채팅,Chat,Chat,Чат,聊天,聊天,聊天 +Corescripts.AvatarContextMenu.ChatDisabled,,,Chat Disabled,Chat deaktiviert,Chat Disabled,Chat desactivado,Chat desactivado,Chat désactivé,Chat disattivata,チャットを無効にしました,채팅 비활성화,Chat desabilitado,Chat desabilitado,Чат отключен,聊天已停用,聊天停用中,聊天已停用 +Corescripts.AvatarContextMenu.Wave,,,Wave,Winken,Wave,Saludar,Saludar,Saluer,Onda,手を振って挨拶,손 흔들기,Acenar,Acenar,Волна,招手,揮手,招手 +CoreScripts.PremiumModal.Title.PremiumRequired,,,Premium Required,Premium erforderlich,Premium Required,Se requiere Premium,Se requiere Premium,Niveau Premium requis,Premium necessario,Premiumが必要です,Premium 필요,Requer Premium,Requer Premium,Требуется премиум-подписка,需要 Premium 会员,需要 Premium,需要 Premium 会员 +CoreScripts.PremiumModal.Body.Description,,,Roblox Premium will get you:,Durch Roblox Premium erhältst du:,Roblox Premium will get you:,Con Roblox Premium también obtienes:,Con Roblox Premium también obtienes:,"Avec Roblox Premium, vous avez droit à :",Con Roblox Premium otterrai:,Roblox Premiumには以下が含まれています:,Roblox Premium 회원을 위한 다양한 혜택:,O Roblox Premium dá direito a:,O Roblox Premium dá direito a:,Преимущества премиум-подписки Roblox:,Roblox Premium 将给你:,Roblox Premium 將會給您:,Roblox Premium 将给你: +CoreScripts.PremiumModal.Body.RobuxMonthly,,,450 Robux per month,450 Robux pro Monat,450 Robux per month,450 Robux al mes,450 Robux al mes,450 Robux par mois,450 Robux al mese,1ヶ月に450Robux,매달 450 Robux 획득,450 Robux por mês,450 Robux por mês,450 Robux в месяц;,每月 450 Robux,每個月 450 Robux,每月 450 Robux +CoreScripts.PremiumModal.Body.PremiumOnlyAreas,,,Access to Premium only benefits,Zugriff auf exklusive Premium-Vorteile,Access to Premium only benefits,Acceso a beneficios exclusivos para suscriptores de Premium,Acceso a beneficios exclusivos para suscriptores de Premium,Accédez à des avantages réservés aux membres Premium,Accesso a vantaggi esclusivi Premium,Premium限定特典にアクセス,Premium 전용 혜택 이용,Acesso a benefícios exclusivos,Acesso a benefícios exclusivos,Доступ к возможностям только для премиум-подписчиков,Premium 会员限定福利,獲得 Premium 限定福利,Premium 会员限定福利 +CoreScripts.PremiumModal.Body.RobuxDiscount,,,10% bonus when buying Robux,10% Bonus beim Kauf von Robux,10% bonus when buying Robux,10% de Robux extra en la compra de nuestra moneda virtual,10% de Robux extra en la compra de nuestra moneda virtual,10 % de bonus lors de l'achat de Robux,Bonus del 10% quando acquisti Robux,Robux購入時に10%のボーナス,Robux 구입 시 10% 추가 보너스 증정,Bônus de 10% ao comprar Robux,Bônus de 10% ao comprar Robux,бонус в 10% при покупке Robux.,购买 Robux 时额外增送 10%,購買 Robux 時獲得 10% 額外 Robux,购买 Robux 时额外增送 10% +CoreScripts.PremiumModal.Action.PricePerMonth,,,{price}/month,{price}/Monat,{price}/month,{price} al mes,{price} al mes,{price} par mois,{price}/mese,{price}/月額,{price}/월,{price}/mês,{price}/mês,{price}/месяц,{price} / 月,{price} / 月,{price} / 月 +CoreScripts.PremiumModal.Error.PlatformUnavailable,,,Purchasing Roblox Premium is not supported on your platform. Please use your desktop to purchase Premium.,"Der Kauf von Roblox Premium wird auf deiner Plattform nicht unterstützt. Bitte verwende deinen Desktop-Computer, um Premium zu kaufen.",Purchasing Roblox Premium is not supported on your platform. Please use your desktop to purchase Premium.,La compra de Roblox Premium no es compatible con tu plataforma. Usa un equipo de escritorio para suscribirte.,La compra de Roblox Premium no es compatible con tu plataforma. Usa un equipo de escritorio para suscribirte.,L'achat de Roblox Premium n'est pas pris en charge sur votre plateforme. Veuillez utiliser un ordinateur pour l'acheter.,Non è possibile acquistare Roblox Premium sulla tua piattaforma. Acquista la versione Premium da un computer desktop.,お使いのプラットフォームはRoblox Premiumの購入に対応していません。Premiumを購入するにはデスクトップをお使いください。,Roblox Premium 구매가 지원되지 않는 플랫폼입니다. Premium을 구매하려면 데스크톱을 사용하세요.,A compra do Roblox Premium não está disponível na sua plataforma. Use o computador para comprar a assinatura Premium.,A compra do Roblox Premium não está disponível na sua plataforma. Use o computador para comprar a assinatura Premium.,Покупка премиум-подписки Roblox не поддерживается на вашей платформе. Используйте ПК для покупки.,你的平台不支持购买 Roblox Premium。请使用电脑购买 Premium 会员。,您的平台不支援購買 Roblox Premium,請使用電腦購買 Premium。,你的平台不支持购买 Roblox Premium。请使用电脑购买 Premium 会员。 +CoreScripts.PremiumModal.Error.AlreadyPremium,,,"Looks like the developer is trying to prompt you to purchase Roblox Premium, but you are already a Premium member!","Der Entwickler versucht, dich dazu aufzufordern, Roblox Premium zu kaufen, aber du bist bereits ein Premium-Mitglied!","Looks like the developer is trying to prompt you to purchase Roblox Premium, but you are already a Premium member!","Parece que el desarrollador te quiere motivar a que te suscribas a Premium, pero tú ya tienes una suscripción a Premium.","Parece que el desarrollador te quiere motivar a que te suscribas a Premium, pero tú ya tienes una suscripción a Premium.",Il semble que le dévelopeur essaie de vous inviter à acheter Roblox Premium mais vous êtes déjà une membre Premium !,"Sembra che lo sviluppatore stia cercando di farti acquistare Roblox Premium, ma sei già un membro Premium!",開発者がRoblox Premiumを購入するように促しているようですが、すでにPremiumメンバーです!,개발자가 Roblox Premium 구매를 도와드리려 한 것 같네요. 그런데 이미 Premium 회원이세요!,"Parece que o desenvolvedor quer te convidar para comprar uma inscrição do Roblox Premium, mas você já se inscreveu!","Parece que o desenvolvedor quer te convidar para comprar uma inscrição do Roblox Premium, mas você já se inscreveu!","Похоже, разработчик пытается убедить вас купить премиум-подписку Roblox, а она у вас уже есть!",开发者似乎想给你发送购买 Roblox Premium 的提示,但你已经是 Premium 会员了!,開發人員似乎想給您購買 Roblox Premium 的提示,但您已經是 Premium 會員了!,开发者似乎想给你发送购买 Roblox Premium 的提示,但你已经是 Premium 会员了! +CoreScripts.PremiumModal.Error.Unavailable,,,Premium is unavailable at the moment. Please again try later!,Premium ist derzeit nicht verfügbar. Bitte versuche es später wieder!,Premium is unavailable at the moment. Please again try later!,Premium no está disponible en este momento. Inténtalo más tarde.,Premium no está disponible en este momento. Inténtalo más tarde.,Le niveau Premium n'est pas disponible pour le moment. Réessayez plus tard !,Premium non è al momento disponibile. Riprova più tardi!,現在、Premiumが利用できません。あとでお試しください!,Premium이 일시적으로 이용 불가능합니다. 나중에 다시 시도하세요!,O Roblox Premium não está disponível no momento. Tente mais tarde!,O Roblox Premium não está disponível no momento. Tente mais tarde!,В настоящий момент премиум-подписка недоступна. Повторите попытку позже!,当前无法购买 Premium,请稍后重试。,目前無法購買 Premium,請稍後再試!,当前无法购买 Premium,请稍后重试。 +CoreScripts.PremiumModal.Body.RobuxMonthlyV2,,,{robux} Robux per month,{robux} Robux im Monat,{robux} Robux per month,{robux} Robux al mes,{robux} Robux al mes,{robux} Robux par mois,{robux} Robux al mese,1ヶ月につき{robux} Robux,매월 {robux} Robux 지급,{robux} Robux por mês,{robux} Robux por mês,{robux} Robux в месяц;,每月 {robux} Robux,每月 {robux} Robux,每月 {robux} Robux +CoreScripts.PremiumModal.Error.FailedNativePurchase,,,"Purchase was not complete, please try again.","Kauf wurde nicht abgeschlossen, bitte versuche es erneut.","Purchase was not complete, please try again.",No se ha finalizado la compra. Inténtalo de nuevo.,No se ha finalizado la compra. Inténtalo de nuevo.,"L'achat n'est pas finalisé, réessaie s'il te plaît.","L'acquisto non è stato completato, riprova.",購入は完了していません。もう一度お試しください。,구매를 완료하지 못했습니다. 다시 시도하세요.,A compra não foi concluída; tente novamente.,A compra não foi concluída; tente novamente.,"Покупка не удалась, попробуйте еще раз.",购买未完成,请重试。,購買未完成,請重新嘗試。,购买未完成,请重试。 +CoreScripts.PremiumModal.Title.Error,,,Error,Fehler,Error,Error,Error,Erreur,Errore,エラー,오류,Erro,Erro,Ошибка,错误,錯誤,错误 +CoreScripts.TopBar.Leaderboard,,,Leaderboard,Bestenliste,Leaderboard,Clasificación,Clasificación,Tableau des scores,Classifica,リーダーボード,리더보드,Classificação,Classificação,Список лидеров,排行榜,排行榜,排行榜 +CoreScripts.TopBar.Emotes,,,Emotes,Emotes,Emotes,Emotes,Emotes,Emotes,Emoticon,エモート,감정 표현,Emotes,Emotes,Эмоции,动作,動作,动作 +CoreScripts.TopBar.Inventory,,,Inventory,Inventar,Inventory,Inventario,Inventario,Inventaire,Inventario,インベントリ,인벤토리,Inventário,Inventário,Инвентарь,道具库,道具欄,道具库 +CoreScripts.TopBar.Leave,,,Leave,Verlassen,Leave,Salir,Salir,Quitter,Esci,終了,게임 종료,Sair,Sair,Выйти,离开,離開,离开 +CoreScripts.TopBar.Chat,,,Chat,Chat,Chat,Chat,Chat,Chat,Chat,チャット,채팅,Chat,Chat,Чат,聊天,聊天,聊天 +CoreScripts.TopBar.QuickMenu,,,Quick Menu,Schnellmenü,Quick Menu,Menú rápido,Menú rápido,Menu rapide,Menu rapido,クイックメニュー,퀵 메뉴,Menu rápido,Menu rápido,Быстрое меню,快捷菜单,快捷選單,快捷菜单 +CoreScripts.TopBar.Menu,,,Menu,Menü,Menu,Menú,Menú,Menu,Menu,メニュー,메뉴,Menu,Menu,Меню,菜单,選單,菜单 +CoreScripts.TopBar.Respawn,,,Respawn Character,Charakter respawnen,Respawn Character,Regenerar personaje,Regenerar personaje,Reproduction du personnage,Rigenera il personaggio,キャラクターをリスポーンする,캐릭터 리스폰,Regenerar personagem,Regenerar personagem,Возродить персонажа,重生角色,重生角色,重生角色 +CoreScripts.TopBar.Back,,,Back,Zurück,Back,Volver,Volver,Retour,Indietro,戻る,뒤로,Voltar,Voltar,Назад,返回,返回,返回 +CoreScripts.TopBar.GameNamePlaceHolder,,,Game,Spiel,Game,Juego,Juego,Jeu,Gioco,ゲーム,게임,Jogo,Jogo,Игра,游戏,遊戲,游戏 +Feature.SettingsHub.Label.LoadingFriendsListFailed,,,"An error occurred, please try again later.",Ein Fehler ist aufgetreten. Bitte versuche es später erneut.,"An error occurred, please try again later.",Se ha producido un error. Inténtalo de nuevo más tarde.,Se ha producido un error. Inténtalo de nuevo más tarde.,"Une erreur s'est produite, veuillez reéssayer plus tard.",Si è verificato un errore. Riprova più tardi.,エラーが発生しました。後でもう一度お試しください。,오류가 발생했어요. 나중에 다시 시도하세요.,"Ocorreu um erro, tente novamente mais tarde.","Ocorreu um erro, tente novamente mais tarde.",Произошла ошибка. Повторите попытку позже.,发生错误,请稍候重试。,發生錯誤,請稍後再試。,发生错误,请稍候重试。 +Feature.SettingsHub.Label.LeaveGame,,,Are you sure you want to leave the game?,Möchtest du das Spiel wirklich verlassen?,Are you sure you want to leave the game?,¿Seguro que deseas salir del juego?,¿Seguro que deseas salir del juego?,Voulez-vous vraiment quitter le jeu ?,Vuoi davvero uscire dal gioco?,ゲームを終了しますか?,게임을 종료하시겠습니까?,Quer mesmo sair do jogo?,Quer mesmo sair do jogo?,Выйти из игры?,是否确定要离开游戏?,確定離開遊戲?,是否确定要离开游戏? +Feature.SettingsHub.Action.CancelSearch,,,Cancel,Abbrechen,Cancel,Cancelar,Cancelar,Annuler,Annulla,キャンセル,취소,Cancelar,Cancelar,Отмена,取消,取消,取消 +Feature.SettingsHub.Label.DontLeaveButton,,,Don't Leave,Nicht verlassen,Don't Leave,No salir,No salir,Ne pas quitter,Non uscire,終了しない,계속하기,Não sair,Não sair,Не выходить,不要离开,不要離開,不要离开 +Feature.SettingsHub.Label.GameInviteError,,,Game invite could not be sent. Please try again later.,Spieleinladung konnte nicht gesendet werden. Bitte versuche es später erneut.,Game invite could not be sent. Please try again later.,No se ha podido enviar la invitación al juego. Inténtalo de nuevo más tarde.,No se ha podido enviar la invitación al juego. Inténtalo de nuevo más tarde.,L'invitation au jeu n'a pas pu être envoyée. Veuillez réessayer plus tard.,Non è stato possibile inviare l'invito di gioco. Riprova più tardi.,ゲームへの招待を送信できませんでした。しばらくしてからやり直してください。,게임 초대를 보내지 못했습니다. 나중에 다시 시도하세요.,O convite de jogo não pôde ser enviado. Tente de novo mais tarde.,O convite de jogo não pôde ser enviado. Tente de novo mais tarde.,"Приглашение от игры не было послано. Пожалуйста, повторите попытку.",无法发送游戏邀请。请稍后重试。,無法傳送遊戲邀請,請稍後再試。,无法发送游戏邀请。请稍后重试。 +Feature.SettingsHub.Label.ModeratedInviteError,,,Game invite was moderated and not sent.,Spieleinladung wurde von einem Moderator angepasst und nicht gesendet.,Game invite was moderated and not sent.,La invitación al juego ha sido moderada y no se ha enviado.,La invitación al juego ha sido moderada y no se ha enviado.,L'invitation au jeu a été modérée et n'a pu être envoyée.,L'invito di gioco è stato moderato e non inviato.,ゲームへの招待は、規制により送信されませんでした。,게임 초대가 검토 요청을 받아 전송되지 않았어요.,O convite de jogo foi moderado e não foi enviado.,O convite de jogo foi moderado e não foi enviado.,Приглашение в игру было проверено и не было отправлено.,游戏邀请已被过滤,未能发送。,遊戲邀請遭到過濾,無法傳送。,游戏邀请已被过滤,未能发送。 +Feature.SettingsHub.Action.InviteFriend,,,Invite,Einladen,Invite,Invitar,Invitar,Inviter,Invita,招待,초대,Convidar,Convidar,Пригласить,邀请,邀請,邀请 +Feature.SettingsHub.Heading.InviteFriends,,,Invite Friends,Freunde einladen,Invite Friends,Invitar amigos,Invitar amigos,Inviter des amis,Invita amici,友達を招待,친구 초대,Convidar amigos,Convidar amigos,Пригласить друзей,邀请好友,邀請好友,邀请好友 +Feature.SettingsHub.Action.InviteFriendsToPlay,,,Invite friends to play,Lade Freunde zum Spielen ein,Invite friends to play,Invitar amigos al juego,Invitar amigos al juego,Inviter des amis à jouer,Invita gli amici a giocare,友達を招待してプレイ,함께 플레이할 친구 초대,Convidar amigos para o jogo,Convidar amigos para o jogo,Пригласить друзей в игру,邀请好友加入游戏,邀請好友,邀请好友加入游戏 +Feature.SettingsHub.Label.Invited,,,Invited,Eingeladen,Invited,Invitado,Invitado,Invité,Invitato,招待済み,초대 완료,Convidado,Convidado,Приглашено,已邀请,已邀請,已邀请 +Feature.SettingsHub.Label.LeaveButton,,,Leave,Verlassen,Leave,Salir,Salir,Quitter,Esci,終了,게임 종료,Sair,Sair,Выйти,离开,離開,离开 +Feature.SettingsHub.Label.NoFriendsScreen,,,Make friends so you can invite them to play with you!,"Finde Freunde und lade sie ein, mit dir zu spielen!",Make friends so you can invite them to play with you!,¡Haz amigos para que los invites a jugar contigo!,¡Haz amigos para que los invites a jugar contigo!,Faites des amis pour que vous puissiez les inviter à jouer!,"Trova nuovi amici, così potrai invitarli a giocare con te!",ゲームで一緒にプレイするために友達を招待しましょう!,친구를 사귄 후 초대하여 함께 플레이하세요!,Faça amigos e os convide para jogar,Faça amigos e os convide para jogar,"Найдите друзей, чтобы играть вместе с ними!",认识更多朋友,邀请他们和你一起玩游戏!,結交好友,開始同樂!,认识更多朋友,邀请他们和你一起玩游戏! +Feature.SettingsHub.Label.Moderated,,,Moderated,Von einem Moderator angepasst,Moderated,Moderado,Moderado,Modéré,Moderato,規制対象,검토 요청됨,Moderado,Moderado,Проверено,已过滤,已遭過濾,已过滤 +Feature.SettingsHub.Label.InviteSearchNoResults,,,No results found,Keine Suchergebnisse,No results found,Sin resultados,Sin resultados,Aucun résultat trouvé,Nessun risultato trovato,結果が見つかりませんでした,검색 결과가 없습니다,Nenhum resultado,Nenhum resultado,Нет результатов,未找到搜索结果,找不到結果,未找到搜索结果 +Feature.SettingsHub.Label.RecentPlayed,,,Recently Played,Recently Played,Recently Played,Recently Played,Recently Played,Recently Played,Recently Played,最近プレイしたもの,최근 플레이,Recently Played,Recently Played,Recently Played,Recently Played,最近玩過,Recently Played +Feature.SettingsHub.Label.SearchForFriendsPlaceholder,,,Search for friends,Suche nach Freunden,Search for friends,Buscar amigos,Buscar amigos,Chercher des amis,Cerca amici,友達を検索,친구 검색,Procurar amigos,Procurar amigos,Поиск друзей,搜索好友,搜尋好友,搜索好友 +Feature.SettingsHub.Label.Sending,,,Sending,Wird gesendet,Sending,Enviando,Enviando,En cours d'envoi,Invio...,送信しています,전송 중,Enviando,Enviando,Отправка,正在发送,正在傳送,正在发送 +Feature.SettingsHub.Action.InviteFriendsBack,,,Back,Zurück,Back,Volver,Volver,Arrière,Indietro,戻る,뒤로,Voltar,Voltar,Назад,返回,返回,返回 +Feature.SettingsHub.Message.InviteToGameTitle,,,Come join me in {PLACENAME},Komm mit mir zu {PLACENAME},Come join me in {PLACENAME},Únete a mí en {PLACENAME},Únete a mí en {PLACENAME},Rejoins-moi ici : {PLACENAME},Raggiungimi qui: {PLACENAME},{PLACENAME} で一緒にゲームしましょう。,{PLACENAME}에서 함께 즐겨요,Junte-se a mim em {PLACENAME},Junte-se a mim em {PLACENAME},Присоединяйтесь ко мне в: {PLACENAME},来和我一起加入“{PLACENAME} ”吧!,來 {PLACENAME} 和我一起同樂吧!,来和我一起加入“{PLACENAME} ”吧! +Feature.SettingsHub.TouchMovementMode.DPad,,,DPad,Steuerkreuz,DPad,Cruceta,Cruceta,DPad,Croce direzionale,DPad,D패드,Direcional,Direcional,Крестовина,十字键,方向鍵,十字键 +Feature.SettingsHub.Default.DynamicThumbstick,,,Default (Dynamic Thumbstick),Standard (Dynamischer Daumenstick),Default (Dynamic Thumbstick),Predeterminado (stick dinámico),Predeterminado (stick dinámico),Par défaut (Joystick),Predefinita (Levetta dinamica),デフォルト (サムスティック),기본값(다이내믹 엄지스틱),Padrão (thumbstick dinâmico),Padrão (thumbstick dinâmico),По умолчанию (динамический аналоговый стик),默认(动态摇杆),預設(動態類比搖桿),默认(动态摇杆) +Feature.SettingsHub.DefaultKeyboard,,,Default (Keyboard),Standard (Tastatur),Default (Keyboard),Predeterminado (teclado),Predeterminado (teclado),Par défaut (Clavier),Predefinita (Tastiera),デフォルト (キーボード),기본값(키보드),Padrão (teclado),Padrão (teclado),По умолчанию (клавиатура),默认(键盘),預設(鍵盤),默认(键盘) +InGame.CommonUI.Title.Warning,,,Warning,Warnung,Warning,Advertencia,Advertencia,Attention,Attenzione,警告,경고,Aviso,Aviso,Предупреждение,警告,警告,警告 +InGame.CommonUI.Title.Error,,,Error,Fehler,Error,Error,Error,Erreur,Errore,エラー,오류,Erro,Erro,Ошибка,错误,錯誤,错误 +InGame.CommonUI.Button.Retry,,,Retry,Erneut versuchen,Retry,Reintentar,Reintentar,Réessaye,Riprova,再試行,다시 시도,Tentar de novo,Tentar de novo,Повторить,重试,重試,重试 +InGame.CommonUI.Button.Ok,,,Ok,Okay,Ok,Aceptar,Aceptar,Ok,Ok,OK,확인,Ok,Ok,OK,确定,確定,确定 +InGame.CommonUI.Title.Alert,,,Alert,Warnung,Alert,Alerta,Alerta,Alerte,Avviso,注意,경고,Alerta,Alerta,Тревога,警告,注意,警告 +InGame.CommonUI.Label.Default,,,Default,Standard,Default,Predeterminado,Predeterminado,Par défaut,Predefinito,デフォルト,기본,Padrão,Padrão,По умолчанию,默认,預設,默认 +InGame.CommonUI.Label.Custom,,,Custom,Individualisiert,Custom,Personalizado,Personalizado,Personnalisé,Personalizzato,カスタム,사용자 정의,Personalizado,Personalizado,Особенное,自定义,自訂,自定义 +InGame.CommonUI.Label.PossiblyCustom,,,Possibly Custom,Möglicherweise benutzerdefiniert,Possibly Custom,Posiblemente una versión personalizada,Posiblemente una versión personalizada,Peut être personnalisé,Forse personalizzato,カスタムの可能性あり,사용자 정의일 수 있음,Possivelmente personalizado,Possivelmente personalizado,Возможно особенное,可能自定义,可能自訂,可能自定义 +InGame.CommonUI.Label.CustomOld,,,Custom Old,Benutzerdefiniert alt,Custom Old,Posiblemente un versión antigua personalizada,Posiblemente un versión antigua personalizada,Peut être une ancienne version personnalisée,Vecchia personalizzazione,旧カスタム,이전 버전의 사용자 정의,Personalizado antigo,Personalizado antigo,Старое особенное,自定义旧版,舊版自訂,自定义旧版 +InGame.ConnectionError.DisconnectCloudEditKick,,,Lost connection to Team Create. Please reconnect. ,Verbindung zu Team Create verloren. Bitte wieder verbinden. ,Lost connection to Team Create. Please reconnect. ,Se ha perdido la conexión a Creación en equipo. Vuelve a conectarte. ,Se ha perdido la conexión a Creación en equipo. Vuelve a conectarte. ,Connexion perdue lors de la création d'équipe. Veuillez vous reconnecter. ,Connessione persa con la creazione della squadra. È necessario riconnettersi. ,チームクリエイトへの接続が切断されました。再接続してください。 ,팀 만들기 중 접속이 끊어졌습니다. 다시 접속하세요.,Conexão perdida com Team Create. Reconecte-se. ,Conexão perdida com Team Create. Reconecte-se. ,"Потеряно соединение с командой создателей. Пожалуйста, установите соединение заново. ",与团队创作(Team Create)建立的连接被中断。请重新连接。,中斷與集體創作的連線,請重新連線。,与团队创作(Team Create)建立的连接被中断。请重新连接。 +InGame.ConnectionError.DisconnectWrongVersion,,,Your version of Roblox may be out of date. Please update Roblox and try again.,Deine Version von Roblox ist eventuell veraltet. Bitte aktualisiere Roblox und versuche es erneut.,Your version of Roblox may be out of date. Please update Roblox and try again.,Tu versión de Roblox puede que esté obsoleta. Actualízala e inténtalo de nuevo.,Tu versión de Roblox puede que esté obsoleta. Actualízala e inténtalo de nuevo.,Votre version de Roblox n'est plus à jour. Veuillez mettre Roblox à jour et réessayer.,La tua versione di Roblox potrebbe essere obsoleta. Aggiorna Roblox e prova di nuovo.,Robloxのバージョンが最新ではありません。Robloxをアップデートして、もう一度お試しください。,회원님이 사용 중인 Roblox 버전이 오래되었습니다. 업데이트 후 다시 시도하세요.,Sua versão do Roblox pode estar desatualizada. Atualize o Roblox e tente novamente.,Sua versão do Roblox pode estar desatualizada. Atualize o Roblox e tente novamente.,"Ваша версия Roblox, возможно, устарела. Пожалуйста, установите обновление для Roblox и попробуйте снова.",你的 Roblox 可能仍是老版本。请更新 Roblox 并重试。,您的 Roblox 版本可能過時,請更新 Roblox 後重新嘗試。,你的 Roblox 可能仍是老版本。请更新 Roblox 并重试。 +InGame.ConnectionError.TeleportFailure,,,Teleport failed due to an unexpected error.,Das Teleportieren ist aufgrund eines unerwarteten Fehlers fehlgeschlagen.,Teleport failed due to an unexpected error.,Error en la teletransportación debido a un problema inesperado.,Error en la teletransportación debido a un problema inesperado.,Échec de la téléportation en raison d'une erreur inattendue.,Teletrasporto non riuscito a causa di un errore imprevisto.,予期せぬエラーによりテレポートできませんでした。,예기치 못한 오류로 텔레포트 실패.,O teleporte falhou devido a um erro inesperado.,O teleporte falhou devido a um erro inesperado.,Произошла непредвиденная ошибка телепорта.,由于未知错误,传送失败。,發生錯誤,無法傳送。,由于未知错误,传送失败。 +InGame.ConnectionError.DisconnectDuplicatePlayer,,,Same account launched game from different device. Reconnect if you prefer to use this device.,"Das Spiel wurde mit demselben Konto auf einem anderen Gerät gestartet. Verbinde dich wieder, wenn du dieses Gerät benutzen möchtest.",Same account launched game from different device. Reconnect if you prefer to use this device.,Se ha utilizado la misma cuenta para lanzar el juego en un dispositivo diferente. Vuelve a conectarte si prefieres utilizar este dispositivo.,Se ha utilizado la misma cuenta para lanzar el juego en un dispositivo diferente. Vuelve a conectarte si prefieres utilizar este dispositivo.,Ce compte a lancé le jeu depuis un autre appareil. Veuillez vous reconnecter si vous préférez utiliser celui-ci.,Lo stesso account ha avviato il gioco da un altro dispositivo. Riconnettiti se preferisci usare questo dispositivo.,同じアカウントで別のデバイスからゲームを起動しました。このデバイスを使う場合は再接続してください。,다른 기기에서 같은 계정으로 게임에 접속했습니다. 본 기기를 사용하기를 원하시면 다시 접속하세요.,A mesma conta iniciou um jogo de um dispositivo diferente. Reconecte-se se você prefere usar este dispositivo.,A mesma conta iniciou um jogo de um dispositivo diferente. Reconecte-se se você prefere usar este dispositivo.,"Игра была запущена с той же учетной записи, но с другого устройства. Выберите нужное для вас устройство.",同一账号已在另一设备上运行游戏。如果你偏向于使用此设备,请重新连接。,此帳號已從其它裝置開啟此遊戲。若您想使用此裝置,請重新連線。,同一账号已在另一设备上运行游戏。如果你偏向于使用此设备,请重新连接。 +InGame.ConnectionError.DisconnectNewSecurityKeyMismatch,,,Lost connection due to an error.,Die Verbindung ist aufgrund eines Fehlers abgebrochen.,Lost connection due to an error.,Se ha perdido la conexión debido a un error.,Se ha perdido la conexión debido a un error.,Connexion perdue suite à une erreur.,Connessione persa a causa di un errore.,エラーにより接続が切断されました。,오류로 인해 연결 끊김,Conexão perdida devido a um erro.,Conexão perdida devido a um erro.,Из-за ошибки пропало соединение.,由于某项错误,连接被中断。,發生錯誤,連線中斷。,由于某项错误,连接被中断。 +InGame.ConnectionError.PlacelaunchUserLeft,,,The user you attempted to join has left the game.,"Der Benutzer, dem du beitreten wolltest, hat das Spiel verlassen.",The user you attempted to join has left the game.,El usuario al que querías unirte ha salido del juego.,El usuario al que querías unirte ha salido del juego.,L'utilisateur que vous avez essayé de rejoindre a quitté le jeu.,L'utente che hai provato a raggiungere ha abbandonato il gioco.,参加しようとしていたユーザーがゲームから退出しました。,함께 플레이하려던 사용자가 게임을 나갔습니다.,O usuário ao qual você estava tentando se juntar saiu do jogo.,O usuário ao qual você estava tentando se juntar saiu do jogo.,Соединяющийся с вами игрок вышел из игры,你尝试加入的用户已离开游戏。,您嘗試跟隨的使用者已離開遊戲。,你尝试加入的用户已离开游戏。 +InGame.ConnectionError.TeleportGameFull,,,"Teleport failed, server is full.","Teleportieren fehlgeschlagen, der Server ist voll.","Teleport failed, server is full.",Error en la teletransportación. El servidor está lleno.,Error en la teletransportación. El servidor está lleno.,Échec de la téléportation ; le serveur est complet.,"Teletrasporto non riuscito, il server è al completo.",テレポートできませんでした。サーバーが満員です。,텔레포트 실패. 서버가 만원입니다.,Falha no teleporte. Servidor lotado.,Falha no teleporte. Servidor lotado.,"Сервер переполнен, телепорт не удался.",服务器已满,传送失败。,伺服器額滿,無法傳送。,服务器已满,传送失败。 +InGame.ConnectionError.PlacelaunchHashExpired,,,This game is currently unavailable. Please try again later.,Dieses Spiel ist derzeit nicht verfügbar. Bitte versuche es später erneut.,This game is currently unavailable. Please try again later.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,"Ce jeu est indisponible pour le moment, veuillez réessayer plus tard.",Questo gioco non è al momento disponibile. Riprova più tardi.,このゲームは現在利用できません。後でもう一度お試しください。,이 게임은 현재 플레이할 수 없습니다. 나중에 다시 시도하세요.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Эта игра сейчас недоступна. Повторите попытку позже.,此游戏目前不可用,请稍后重试。,此遊戲目前不開放,請稍後再試。,此游戏目前不可用,请稍后重试。 +InGame.ConnectionError.DisconnectPlayerless,,,Server was shutting down as you tried to connect. Please try again.,Der Server wurde während deines Verbindungsversuchs heruntergefahren. Bitte versuche es erneut.,Server was shutting down as you tried to connect. Please try again.,El servidor se estaba cerrando mientras tratabas de conectarte. Inténtalo de nuevo.,El servidor se estaba cerrando mientras tratabas de conectarte. Inténtalo de nuevo.,Le serveur a fermé lors de votre tentative de connexion. Veuillez réessayer.,Il server è stato chiuso mentre provavi a connetterti. Riprova più tardi.,接続中にサーバーがシャットダウンしました。もう一度お試しください。,회원님이 연결하려고 하는 서버가 종료되었습니다. 다시 시도하세요. ,O servidor estava sendo desligado quando você tentou se conectar. Tente de novo mais tarde.,O servidor estava sendo desligado quando você tentou se conectar. Tente de novo mais tarde.,"В попытке соединения с сервером отказано. Пожалуйста, повторите ее позже.",服务器在你尝试连接时被关闭。请重试。,伺服器在連線過程關閉,請重新嘗試。,服务器在你尝试连接时被关闭。请重试。 +InGame.ConnectionError.TeleportGameNotFound,,,Attempted to teleport to a place that does not exist.,"Du hast versucht, dich an einen nicht existierenden Ort zu teleportieren.",Attempted to teleport to a place that does not exist.,Se ha intentado teletransportarse a un lugar que no existe.,Se ha intentado teletransportarse a un lugar que no existe.,Vous avez tenté de vous téléporter dans un emplacement qui n'existe pas.,Tentativo di teletrasporto verso una località che non esiste.,存在しないプレースへのテレポートを試みました。,존재하지 않는 장소로 텔레포트를 시도했습니다.,Tentativa de se teleportar para um local que não existe.,Tentativa de se teleportar para um local que não existe.,Попытка телепорта в несуществующее место.,尝试传送至一个不存在的场景。,您嘗試傳送的空間不存在。,尝试传送至一个不存在的场景。 +InGame.ConnectionError.DisconnectErrors,,,Lost connection due to an unknown error. Please reconnect.,Die Verbindung ist aufgrund eines unbekannten Fehlers abgebrochen. Bitte wieder verbinden.,Lost connection due to an unknown error. Please reconnect.,Se ha perdido la conexión debido a un error desconocido. Vuelve a conectarte.,Se ha perdido la conexión debido a un error desconocido. Vuelve a conectarte.,Connexion perdue suite à une erreur inconnue. Veuillez vous reconnecter.,Connessione persa a causa di un errore sconosciuto. Riconnettiti.,予期せぬエラーにより接続が切断されました。再接続してください。,알 수 없는 오류로 연결 끊김. 다시 접속하세요.,Conexão perdida devido a um erro desconhecido. Reconecte-se.,Conexão perdida devido a um erro desconhecido. Reconecte-se.,"Пропало соединение из-за неизвестной ошибки. Пожалуйста, установите соединение снова.",由于未知错误,连接被中断。请重新连接。,發生錯誤,連線中斷。請重新連線。,由于未知错误,连接被中断。请重新连接。 +InGame.ConnectionError.PlacelaunchUnauthorized,,,You do not have permission to join this game. ,"Du hast keine Berechtigung, diesem Spiel beizutreten. ",You do not have permission to join this game. ,No tienes permiso para unirte a este juego. ,No tienes permiso para unirte a este juego. ,Vous n'avez pas la permission de rejoindre ce jeu. ,Non hai i permessi necessari per partecipare a questa partita. ,このゲームに参加する権限がありません。 ,본 게임에 참가할 권한이 없습니다.,Você não tem permissão para entrar neste jogo. ,Você não tem permissão para entrar neste jogo. ,Не разрешено присоединяться к этой игре. ,你没有加入此游戏的权限。,您沒有加入此遊戲的權限。,你没有加入此游戏的权限。 +InGame.ConnectionError.DisconnectDuplicateTicket,,,Lost connection due to an error. Multiple connections detected from same account.,Die Verbindung ist aufgrund eines Fehlers abgebrochen. Mehrere Verbindungen desselben Kontos erkannt.,Lost connection due to an error. Multiple connections detected from same account.,Se ha perdido la conexión debido a un error. Se han detectado múltiples conexiones provenientes de la misma cuenta.,Se ha perdido la conexión debido a un error. Se han detectado múltiples conexiones provenientes de la misma cuenta.,Connexion perdue suite à une erreur ; plusieurs connexions détectées depuis le même compte.,Connessione persa a causa di un errore. Sono state rilevate diverse connessioni dallo stesso account.,エラーにより接続が切断されました。同じアカウントからの複数の接続を検出しました。,오류로 인해 연결이 끊어졌습니다. 동일 계정으로 다수의 접속이 발견되었습니다.,Conexão perdida devido a um erro. Várias conexões detectadas da mesma conta.,Conexão perdida devido a um erro. Várias conexões detectadas da mesma conta.,Из-за ошибки пропало соединение. С этой учетной записи следуют многочисленные попытки входа.,由于某项错误,连接被中断。检测到来自同一帐户的多个连接。,發生錯誤,連線中斷。偵測到多條來自同一帳號的連線。,由于某项错误,连接被中断。检测到来自同一帐户的多个连接。 +InGame.ConnectionError.DisconnectBadhash,,,Lost connection due to an error.,Die Verbindung ist aufgrund eines Fehlers abgebrochen.,Lost connection due to an error.,Se ha perdido la conexión debido a un error.,Se ha perdido la conexión debido a un error.,Connexion perdue suite à une erreur.,Connessione persa a causa di un errore.,エラーにより接続が切断されました。,오류로 인해 접속이 끊김,Conexão perdida devido a um erro.,Conexão perdida devido a um erro.,Из-за ошибки пропало соединение.,由于某项错误,连接被中断。,發生錯誤,連線中斷。,由于某项错误,连接被中断。 +InGame.ConnectionError.DisconnectSendPacketError,,,"There was a problem sending data, please reconnect.","Beim Senden der Daten ist ein Problem aufgetreten, bitte wieder verbinden.","There was a problem sending data, please reconnect.",Ha habido un problema al enviar los datos. Vuelve a conectarte.,Ha habido un problema al enviar los datos. Vuelve a conectarte.,"Une erreur s'est produite lors de l'envoi des données, veuillez vous reconnecter.","C'è stato un problema durante l'invio dei dati, è necessario riconnettersi.",データを送信中に問題が発生しました。再接続してください,데이터 전송 중 오류가 발생했어요. 다시 접속하세요.,Ocorreu um erro ao enviar os dados. Reconecte-se.,Ocorreu um erro ao enviar os dados. Reconecte-se.,"Проблемы с отправкой данных, пожалуйста, установите соединение повторно.",发送数据时出现问题,请重新连接。,傳送資料時發生錯誤,請重新連線。,发送数据时出现问题,请重新连接。 +InGame.ConnectionError.DisconnectOnRemoteSysStats,,,You have been kicked due to unexpected client behavior.,Du wurdest aufgrund unerwartetem Verhalten des Clients entfernt.,You have been kicked due to unexpected client behavior.,Has sido expulsado debido a un comportamiento inesperado del cliente.,Has sido expulsado debido a un comportamiento inesperado del cliente.,Vous avez été expulsé(e) à cause d'un comportement inattendu du client.,Sei stato espulso a causa di un comportamento imprevisto del client.,クライアントの予期せぬ動作により、ゲームから外されました。,예기치 못한 고객 행동으로 강제 퇴장당하셨습니다.,Você foi expulso devido a um comportamento inesperado do cliente.,Você foi expulso devido a um comportamento inesperado do cliente.,Вас исключили из игры за неподобающее поведение.,由于未知客户端行为,你已被踢出。,用戶端異常,您已被踢出。,由于未知客户端行为,你已被踢出。 +InGame.ConnectionError.DisconnectTimeout,,,Your connection timed out. Check your internet connection and try again.,Zeitüberschreitung der Verbindung. Bitte überprüfe deine Internetverbindung und versuche es erneut.,Your connection timed out. Check your internet connection and try again.,Se ha agotado el tiempo de la conexión. Comprueba tu conexión a internet e inténtalo de nuevo.,Se ha agotado el tiempo de la conexión. Comprueba tu conexión a internet e inténtalo de nuevo.,Votre connexion a été perdue. Vérifiez que vous disposez d'un accès à Internet et réessayez.,La tua connessione è scaduta. Controlla la tua connessione a Internet e riprova.,接続がタイムアウトしました。インターネット接続をチェックしてもう一度お試しください。,연결 대기 시간 초과. 인터넷 연결을 확인하고 다시 시도하세요.,Sua conexão expirou. Verifique sua conexão de internet e tente novamente.,Sua conexão expirou. Verifique sua conexão de internet e tente novamente.,Время для подключения истекло. Проверьте свое подключение к сети Интернет и повторите попытку.,你的连接已超时。请检查你的网络连接并重试。,連線逾時,請檢查您的網路連線後重新嘗試。,你的连接已超时。请检查你的网络连接并重试。 +InGame.ConnectionError.PlacelaunchHttpError,,,We are experiencing technical difficulties. Please try again later.,Wir haben technische Schwierigkeiten. Bitte versuche es später erneut.,We are experiencing technical difficulties. Please try again later.,Tenemos dificultades técnicas. Inténtalo de nuevo más tarde.,Tenemos dificultades técnicas. Inténtalo de nuevo más tarde.,Nous rencontrons des problèmes techniques. Veuillez réessayer plus tard.,Stiamo riscontrando delle difficoltà tecniche. Riprova più tardi.,技術的な問題が発生しています。後でもう一度お試しください。,기술적인 문제가 발생했습니다. 나중에 다시 시도하세요.,Estamos tendo dificuldades técnicas. Tente de novo mais tarde.,Estamos tendo dificuldades técnicas. Tente de novo mais tarde.,Сейчас технические проблемы. Повторите попытку позже.,我们遇到技术困难 。请稍后重试。,我們遇到技術困難,請稍後再試。,我们遇到技术困难 。请稍后重试。 +InGame.ConnectionError.DisconnectSecurityKeyMismatch,,,Lost connection due to an error.,Die Verbindung ist aufgrund eines Fehlers abgebrochen.,Lost connection due to an error.,Se ha perdido la conexión debido a un error.,Se ha perdido la conexión debido a un error.,Connexion perdue suite à une erreur.,Connessione persa a causa di un errore.,エラーにより接続が切断されました。,오류로 인해 접속이 끊겼습니다.,Conexão perdida devido a um erro.,Conexão perdida devido a um erro.,Из-за ошибки пропало соединение.,由于某项错误,连接被中断。,發生錯誤,連線中斷。,由于某项错误,连接被中断。 +InGame.ConnectionError.TeleportErrors,,,Teleport failed due to an unknown error.,Das Teleportieren ist aufgrund eines unbekannten Fehlers fehlgeschlagen.,Teleport failed due to an unknown error.,Error en la teletransportación debido a un problema inesperado.,Error en la teletransportación debido a un problema inesperado.,Échec de la téléportation en raison d'une erreur inconnue.,Teletrasporto non riuscito a causa di un errore sconosciuto.,不明なエラーによりテレポートできませんでした。,알 수 없는 오류로 인해 텔레포트 실패.,O teleporte falhou devido a um erro desconhecido.,O teleporte falhou devido a um erro desconhecido.,Произошла неопознанная ошибка телепорта.,由于未知错误,传送失败。,發生錯誤,無法傳送。,由于未知错误,传送失败。 +InGame.ConnectionError.PlacelaunchDisabled,,,This game is currently unavailable. Please try again later.,Dieses Spiel ist derzeit nicht verfügbar. Bitte versuche es später erneut.,This game is currently unavailable. Please try again later.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,"Ce jeu est indisponible pour le moment, veuillez réessayer plus tard.",Questo gioco non è al momento disponibile. Riprova più tardi.,このゲームは現在利用できません。後でもう一度お試しください。,이 게임은 현재 플레이할 수 없습니다. 나중에 다시 시도하세요.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Эта игра сейчас недоступна. Повторите попытку позже.,此游戏目前不可用,请稍后重试。,此遊戲目前不開放,請稍後再試。,此游戏目前不可用,请稍后重试。 +InGame.ConnectionError.TeleportGameEnded,,,"Teleport failed, server is no longer available.","Teleportieren fehlgeschlagen, der Server ist nicht mehr verfügbar.","Teleport failed, server is no longer available.",Error en la teletransportación. El servidor ya no está disponible.,Error en la teletransportación. El servidor ya no está disponible.,Échec de la téléportation ; le serveur n'est plus disponible.,"Teletrasporto non riuscito, il server non è più disponibile.",テレポートできませんでした。サーバーが利用できません。,텔레포트 실패. 서버를 더 이상 이용할 수 없습니다.,Falha no teleporte. O servidor não está mais disponível.,Falha no teleporte. O servidor não está mais disponível.,"Телепорт не удался, сервер недоступен.",服务器已不可用,传送失败。,伺服器已關閉,無法傳送。,服务器已不可用,传送失败。 +InGame.ConnectionError.DisconnectRejoin,,,Same account launched game from different device. Reconnect if you prefer to use this device.,"Das Spiel wurde mit demselben Konto auf einem anderen Gerät gestartet. Verbinde dich wieder, wenn du dieses Gerät benutzen möchtest.",Same account launched game from different device. Reconnect if you prefer to use this device.,Se ha utilizado la misma cuenta para lanzar el juego en un dispositivo diferente. Vuelve a conectarte si prefieres utilizar este dispositivo.,Se ha utilizado la misma cuenta para lanzar el juego en un dispositivo diferente. Vuelve a conectarte si prefieres utilizar este dispositivo.,Ce compte a lancé le jeu depuis un autre appareil. Veuillez vous reconnecter si vous préférez utiliser celui-ci.,Lo stesso account ha avviato il gioco da un altro dispositivo. Riconnettiti se preferisci usare questo dispositivo.,同じアカウントで別のデバイスからゲームを起動しました。このデバイスを使う場合は再接続してください。,다른 기기에서 같은 계정으로 게임에 접속했습니다. 본 기기를 이용하기를 원하시면 다시 접속하세요. ,A mesma conta iniciou um jogo de um dispositivo diferente. Reconecte-se se você prefere usar este dispositivo.,A mesma conta iniciou um jogo de um dispositivo diferente. Reconecte-se se você prefere usar este dispositivo.,"Игра была запущена с той же учетной записи, но с другого устройства. Выберите нужное для вас устройство.",同一账号已在另一设备上运行游戏。如果你偏向于使用此设备,请重新连接。,此帳號已從其它裝置開啟此遊戲。若您想使用此裝置,請重新連線。,同一账号已在另一设备上运行游戏。如果你偏向于使用此设备,请重新连接。 +InGame.ConnectionError.PlacelaunchFlooded,,,The server is currently busy. Please try again.,Der Server ist momentan ausgelastet. Bitte versuche es erneut.,The server is currently busy. Please try again.,El servidor está ocupado en este momento. Inténtelo de nuevo.,El servidor está ocupado en este momento. Inténtelo de nuevo.,Le serveur est occupé. Veuillez réessayer plus tard.,Il server è al momento occupato. Riprova.,現在サーバーが混雑しています。もう一度お試しください。,현재 서버 사용량이 많습니다. 다시 시도하세요.,O servidor está ocupado no momento. Tente de novo.,O servidor está ocupado no momento. Tente de novo.,Сейчас сервер занят. Повторите попытку.,服务器目前繁忙,请稍后重试。,伺服器忙碌中,請稍後再試。,服务器目前繁忙,请稍后重试。 +InGame.ConnectionError.DisconnectProtocolMismatch,,,Your version of Roblox may be out of date. Please update Roblox and try again.,Deine Version von Roblox ist eventuell veraltet. Bitte aktualisiere Roblox und versuche es erneut.,Your version of Roblox may be out of date. Please update Roblox and try again.,Tu versión de Roblox puede que esté obsoleta. Actualízala e inténtalo de nuevo.,Tu versión de Roblox puede que esté obsoleta. Actualízala e inténtalo de nuevo.,Votre version de Roblox n'est plus à jour. Veuillez mettre Roblox à jour et réessayer.,La tua versione di Roblox potrebbe essere obsoleta. Aggiorna Roblox e prova di nuovo.,Robloxのバージョンが最新ではありません。Robloxをアップデートして、もう一度お試しください。,회원님이 사용 중인 Roblox 버전이 오래되었습니다. 업데이트 후 다시 시도하세요.,Sua versão do Roblox pode estar desatualizada. Atualize o Roblox e tente novamente.,Sua versão do Roblox pode estar desatualizada. Atualize o Roblox e tente novamente.,"Ваша версия Roblox, возможно, устарела. Пожалуйста, установите обновление для Roblox и попробуйте снова.",你的 Roblox 可能仍是老版本。请更新 Roblox 并重试。,您的 Roblox 版本可能過時,請更新 Roblox 後重新嘗試。,你的 Roblox 可能仍是老版本。请更新 Roblox 并重试。 +InGame.ConnectionError.DisconnectRobloxMaintenance,,,Roblox has shut down the server for maintenance. Please try again.,Roblox hat den Server zur Wartung heruntergefahren. Bitte versuche es erneut.,Roblox has shut down the server for maintenance. Please try again.,Roblox ha cerrado el servidor por mantenimiento. Inténtalo de nuevo.,Roblox ha cerrado el servidor por mantenimiento. Inténtalo de nuevo.,Roblox a fermé ce serveur de jeu pour en effectuer la maintenance. Veuillez réessayer.,Roblox ha chiuso questo server per manutenzione. Riprova più tardi.,メンテナンスのため、Robloxによってサーバーがシャットダウンされています。もう一度お試しください。,Roblox가 유지보수를 위해 서버를 강제 종료하였습니다. 다시 시도하세요.,Roblox desligou o servidor para realizar uma manutenção. Tente novamente.,Roblox desligou o servidor para realizar uma manutenção. Tente novamente.,Roblox отключил игровой сервер для техобслуживания. Повторите попытку.,由于系统维护,Roblox 已关闭服务器。请重试。,即將進行維修,伺服器已關閉。請重新嘗試。,由于系统维护,Roblox 已关闭服务器。请重试。 +InGame.ConnectionError.DisconnectHashTimeout,,,Your connection timed out. Check your internet connection and try again.,Zeitüberschreitung der Verbindung. Bitte überprüfe deine Internetverbindung und versuche es erneut.,Your connection timed out. Check your internet connection and try again.,Se ha agotado el tiempo de la conexión. Comprueba tu conexión a internet e inténtalo de nuevo.,Se ha agotado el tiempo de la conexión. Comprueba tu conexión a internet e inténtalo de nuevo.,Votre connexion a été perdue. Vérifiez que vous disposez d'un accès à Internet et réessayez.,La tua connessione è scaduta. Controlla la tua connessione a Internet e riprova.,接続がタイムアウトしました。インターネット接続をチェックしてもう一度お試しください。,연결 대기 시간 초과. 인터넷 연결 상태를 확인 후 다시 시도하세요.,Sua conexão expirou. Verifique sua conexão de internet e tente novamente.,Sua conexão expirou. Verifique sua conexão de internet e tente novamente.,Время для подключения истекло. Проверьте свое подключение к сети Интернет и повторите попытку.,你的连接已超时。请检查你的网络连接并重试。,連線逾時,請檢查您的網路連線後重新嘗試。,你的连接已超时。请检查你的网络连接并重试。 +InGame.ConnectionError.DisconnectDevMaintenance,,,The game's developer has temporarily shut down the game server. Please try again.,Der Entwickler des Spiels hat den Spielserver vorübergehend heruntergefahren. Bitte versuche es erneut.,The game's developer has temporarily shut down the game server. Please try again.,El desarrollador ha cerrado temporalmente el servidor del juego. Inténtalo de nuevo.,El desarrollador ha cerrado temporalmente el servidor del juego. Inténtalo de nuevo.,Le développeur a fermé le serveur du jeu temporairement. Veuillez réessayer plus tard.,Lo sviluppatore ha temporaneamente chiuso il server di gioco. Riprova più tardi.,ゲームの開発者が一時的にゲームサーバーをシャットダウンしました。もう一度お試しください。,본 게임의 개발자들이 일시적으로 게임 서버를 강제 종료하였습니다. 다시 시도하세요.,O desenvolvedor do jogo desligou o servidor do jogo temporariamente. Tente novamente.,O desenvolvedor do jogo desligou o servidor do jogo temporariamente. Tente novamente.,"Разработчик временно отключил игровой сервер. Пожалуйста, повторите попытку.",游戏开发者已暂时关闭游戏服务器。请重试。,遊戲開發人員已暫時關閉遊戲伺服器,請重新嘗試。,游戏开发者已暂时关闭游戏服务器。请重试。 +InGame.ConnectionError.DisconnectReceivePacketError,,,"There was a problem receiving data, please reconnect.","Beim Empfangen der Daten ist ein Problem aufgetreten, bitte wieder verbinden.","There was a problem receiving data, please reconnect.",Ha habido un problema al recibir los datos. Vuelve a conectarte.,Ha habido un problema al recibir los datos. Vuelve a conectarte.,"Une erreur s'est produite lors de la réception des données, veuillez vous reconnecter.","C'è stato un problema durante la ricezione dei dati, è necessario riconnettersi.",データを受信中に問題が発生しました。再接続してください,데이터 수신 오류가 발생했어요. 다시 접속하세요.,Ocorreu um erro ao receber os dados. Reconecte-se.,Ocorreu um erro ao receber os dados. Reconecte-se.,"Проблемы с получением данных, пожалуйста, установите соединение повторно.",接收数据时出现问题,请重新连接。,接收資料時發生錯誤,請重新連線。,接收数据时出现问题,请重新连接。 +InGame.ConnectionError.PlacelaunchOtherError,,,We are experiencing technical difficulties. Please try again later.,Wir haben technische Schwierigkeiten. Bitte versuche es später erneut.,We are experiencing technical difficulties. Please try again later.,Tenemos dificultades técnicas. Inténtalo de nuevo más tarde.,Tenemos dificultades técnicas. Inténtalo de nuevo más tarde.,Nous rencontrons des problèmes techniques. Veuillez réessayer plus tard.,Stiamo riscontrando delle difficoltà tecniche. Riprova più tardi.,技術的な問題が発生しています。後でもう一度お試しください。,기술적인 문제가 발생했습니다. 나중에 다시 시도하세요.,Estamos tendo dificuldades técnicas. Tente de novo mais tarde.,Estamos tendo dificuldades técnicas. Tente de novo mais tarde.,Сейчас технические проблемы. Повторите попытку позже.,我们遇到技术困难 。请稍后重试。,我們遇到技術困難,請稍後再試。,我们遇到技术困难 。请稍后重试。 +InGame.ConnectionError.PlacelaunchGameFull,,,The server you are attempting to join is currently full. Please try again.,"Der Server, dem du beitreten möchtest, ist derzeit voll. Bitte versuche es erneut.",The server you are attempting to join is currently full. Please try again.,El servidor al que estás tratando de unirte está lleno en este momento. Inténtalo de nuevo.,El servidor al que estás tratando de unirte está lleno en este momento. Inténtalo de nuevo.,Le serveur que vous tentez de rejoindre est actuellement complet. Veuillez réessayer.,Il server a cui stai cercando di unirti è attualmente al completo. Riprova più tardi.,参加しようとしているサーバーは現在満員です。もう一度試してください。,참가하려는 서버가 현재 만원입니다. 나중에 시도하세요.,O servidor no qual você está tentando entrar está cheio. Tente novamente.,O servidor no qual você está tentando entrar está cheio. Tente novamente.,"Выбранный вами сервер переполнен и соединение невозможно. Пожалуйста, повторите попытку.",你尝试加入的服务器目前已满。请重试。,您嘗試加入的伺服器已額滿,請重新嘗試。,你尝试加入的服务器目前已满。请重试。 +InGame.ConnectionError.TeleportFlooded,,,Too many teleport requests received.,Zu viele Anfragen zum Teleportieren erhalten.,Too many teleport requests received.,Se han recibido demasiadas solicitudes de teletransportación.,Se han recibido demasiadas solicitudes de teletransportación.,Trop de demandes de téléportation reçues.,Troppe richieste di teletrasporto ricevute.,テレポートのリクエスト受信が多すぎます。,텔레포트 요청 횟수 초과.,Excesso de solicitações de teleporte recebidas.,Excesso de solicitações de teleporte recebidas.,Получено слишком много запросов на телепорт.,已接收过多传送请求。,傳送請求過多。,已接收过多传送请求。 +InGame.ConnectionError.DisconnectIllegalTeleport,,,Lost connection due to an invalid teleport. ,Die Verbindung ist aufgrund einer ungültigen Teleportation abgebrochen. ,Lost connection due to an invalid teleport. ,Se ha perdido la conexión debido a una teletransportación no válida. ,Se ha perdido la conexión debido a una teletransportación no válida. ,Connexion perdue en raison d'une téléportation invalide. ,Connessione persa a causa di un teletrasporto non valido. ,無効なテレポートにより接続が切断されました。 ,유효하지 못한 텔레포트로 인해 연결 끊김,Conexão perdida devido a um teleporte inválido. ,Conexão perdida devido a um teleporte inválido. ,Пропало соединение из-за неправильного телепорта. ,由于无效传送,连接被中断。,傳送無效,連線中斷。,由于无效传送,连接被中断。 +InGame.ConnectionError.DisconnectEvicted,,,Same account launched game from different device. Reconnect if you prefer to use this device.,"Das Spiel wurde mit demselben Konto auf einem anderen Gerät gestartet. Verbinde dich wieder, wenn du dieses Gerät benutzen möchtest.",Same account launched game from different device. Reconnect if you prefer to use this device.,Se ha utilizado la misma cuenta para lanzar el juego en un dispositivo diferente. Vuelve a conectarte si prefieres utilizar este dispositivo.,Se ha utilizado la misma cuenta para lanzar el juego en un dispositivo diferente. Vuelve a conectarte si prefieres utilizar este dispositivo.,Ce compte a lancé le jeu depuis un autre appareil. Veuillez vous reconnecter si vous préférez utiliser celui-ci.,Lo stesso account ha avviato il gioco da un altro dispositivo. Riconnettiti se preferisci usare questo dispositivo.,同じアカウントで別のデバイスからゲームを起動しました。このデバイスを使う場合は再接続してください。,다른 기기에서 같은 계정으로 게임에 접속했습니다. 본 기기를 이용하기를 원하시면 다시 접속하세요. ,A mesma conta iniciou um jogo de um dispositivo diferente. Reconecte-se se você prefere usar este dispositivo.,A mesma conta iniciou um jogo de um dispositivo diferente. Reconecte-se se você prefere usar este dispositivo.,"Игра была запущена с той же учетной записи, но с другого устройства. Выберите нужное для вас устройство.",同一账号已在另一设备上运行游戏。如果你偏向于使用此设备,请重新连接。,此帳號已從其它裝置開啟此遊戲。若您想使用此裝置,請重新連線。,同一账号已在另一设备上运行游戏。如果你偏向于使用此设备,请重新连接。 +InGame.ConnectionError.PlacelaunchPartyCannotFit,,,Your party is too large to join this game. Try joining a different game.,"Dein Team ist zu groß, um diesem Spiel beizutreten. Versuche, einem anderen Spiel beizutreten.",Your party is too large to join this game. Try joining a different game.,Tu equipo es demasiado grande para unirse a este juego. Prueba un juego diferente.,Tu equipo es demasiado grande para unirse a este juego. Prueba un juego diferente.,Votre groupe comporte trop de membres pour rejoindre ce jeu. Essayez d'en rejoindre un autre.,Il tuo gruppo è troppo grande per partecipare a questa partita. Prova con un'altra partita.,人数が多すぎて、このゲームに参加できません。別のゲームに参加してください。,파티 인원이 너무 많아 게임에 참가할 수 없습니다. 다른 게임에 참가해 보세요.,Seu grupo é grande demais para entrar neste jogo. Tente entrar em outro jogo.,Seu grupo é grande demais para entrar neste jogo. Tente entrar em outro jogo.,Ваша группа слишком велика для присоединения к игре. Попробуйте присоединиться к другой.,你的队伍人数过多,无法加入此游戏。请尝试加入其他游戏。,您的隊伍人數過多,無法加入此遊戲。請嘗試加入其它遊戲。,你的队伍人数过多,无法加入此游戏。请尝试加入其他游戏。 +InGame.ConnectionError.PlacelaunchRestricted,,,The status of the game has changed and you no longer have access. Please try again later.,Der Status des Spiels hat sich geändert und du hast keinen Zugang mehr. Bitte versuche es später erneut.,The status of the game has changed and you no longer have access. Please try again later.,El estado de este juego ha cambiado y ya no tienes acceso. Inténtalo de nuevo más tarde.,El estado de este juego ha cambiado y ya no tienes acceso. Inténtalo de nuevo más tarde.,Le statut du jeu a changé et vous n'y avez plus accès. Veuillez réessayer plus tard.,Lo stato del gioco è cambiato e non hai più l'accesso. Riprova più tardi.,ゲームのステータスが変更されたため、アクセスできなくなりました。後でもう一度お試しください。,게임 상황이 변경되어 더 이상 접속하실 수 없습니다. 나중에 다시 시도하세요.,O status deste jogo mudou e você não tem mais acesso. Tente de novo mais tarde.,O status deste jogo mudou e você não tem mais acesso. Tente de novo mais tarde.,"Статус этой игры был изменен, и вы больше не имеете к ней доступа. Пожалуйста, повторите попытку.",游戏状态已更改,你已经失去访问权限。请稍后重试。,遊戲狀態已變更,您已失去此遊戲的通行權。請稍後再試。,游戏状态已更改,你已经失去访问权限。请稍后重试。 +InGame.ConnectionError.PlacelaunchGameEnded,,,This game is currently unavailable. Please try again later.,Dieses Spiel ist derzeit nicht verfügbar. Bitte versuche es später erneut.,This game is currently unavailable. Please try again later.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,"Ce jeu est indisponible pour le moment, veuillez réessayer plus tard.",Questo gioco non è al momento disponibile. Riprova più tardi.,このゲームは現在利用できません。後でもう一度お試しください。,이 게임은 현재 플레이할 수 없습니다. 나중에 다시 시도하세요.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Эта игра сейчас недоступна. Повторите попытку позже.,此游戏目前不可用,请稍后重试。,此遊戲目前不開放,請稍後再試。,此游戏目前不可用,请稍后重试。 +InGame.ConnectionError.PlacelaunchErrors,,,An error occured while joining the game. Please try again.,Beim Beitritt zum Spiel ist ein Fehler aufgetreten. Bitte versuche es erneut.,An error occured while joining the game. Please try again.,Se ha producido un error al unirse al juego. Inténtalo de nuevo.,Se ha producido un error al unirse al juego. Inténtalo de nuevo.,Une erreur est survenue lors de la connexion au jeu. Veuillez réessayer plus tard.,Si è verificato un errore mentre cercavi di unirti al gioco. Riprova più tardi.,ゲームの参加中にエラーが発生しました。もう一度お試しください。,게임 참가 중 오류가 발생했어요. 다시 시도하세요.,Ocorreu um erro ao tentar entrar no jogo. Tente novamente.,Ocorreu um erro ao tentar entrar no jogo. Tente novamente.,Произошла ошибка при входе в игру. Повторите попытку позже.,加入游戏时发生错误。请稍后重试。,加入遊戲時發生錯誤,請重新嘗試。,加入游戏时发生错误。请稍后重试。 +InGame.ConnectionError.DisconnectLuaKick,,,You have been kicked from the game. ,Du wurdest aus dem Spiel entfernt. ,You have been kicked from the game. ,Has sido expulsado del juego. ,Has sido expulsado del juego. ,Vous avez été expulsé(e) du jeu. ,Sei stato espulso dal gioco. ,ゲームから外されました。 ,게임에서 강제 퇴장당하셨어요.,Você foi removido do jogo. ,Você foi removido do jogo. ,Вас исключили из игры. ,你已被踢出游戏。,您已被踢出此遊戲。,你已被踢出游戏。 +InGame.ConnectionError.PlacelaunchHashException,,,This game is currently unavailable. Please try again later.,Dieses Spiel ist derzeit nicht verfügbar. Bitte versuche es später erneut.,This game is currently unavailable. Please try again later.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,"Ce jeu est indisponible pour le moment, veuillez réessayer plus tard.",Questo gioco non è al momento disponibile. Riprova più tardi.,このゲームは現在利用できません。後でもう一度お試しください。,이 게임은 현재 플레이할 수 없습니다. 나중에 다시 시도하세요.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Эта игра сейчас недоступна. Повторите попытку позже.,此游戏目前不可用,请稍后重试。,此遊戲目前不開放,請稍後再試。,此游戏目前不可用,请稍后重试。 +InGame.ConnectionError.DisconnectReceivePacketStreamError,,,"There was a problem streaming data, please reconnect.","Beim Streamen der Daten ist ein Problem aufgetreten, bitte wieder verbinden.","There was a problem streaming data, please reconnect.",Ha habido un problema al transmitir los datos. Vuelve a conectarte.,Ha habido un problema al transmitir los datos. Vuelve a conectarte.,"Une erreur s'est produite lors de la diffusion des données, veuillez vous reconnecter.","C'è stato un problema durante lo streaming dei dati, è necessario riconnettersi.",データをストリーミング中にエラーが発生しました。再接続してください。,데이터 스트리밍 중 오류가 발생했어요. 다시 연결하세요.,Ocorreu um erro ao transmitir os dados. Reconecte-se.,Ocorreu um erro ao transmitir os dados. Reconecte-se.,"Проблемы с транслированием данных, пожалуйста, установите соединение повторно.",流式处理数据时出现问题,请重新连接。,資料串流發生錯誤,請重新連線。,流式处理数据时出现问题,请重新连接。 +InGame.ConnectionError.PlacelaunchError,,,Unable to find the server you are attempting to join. Please try again later.,"Der Server, dem du beitreten möchtest, kann nicht gefunden werden. Bitte versuche es später erneut.",Unable to find the server you are attempting to join. Please try again later.,No se ha podido encontrar el servidor al que estás tratando de unirte. Inténtalo de nuevo más tarde.,No se ha podido encontrar el servidor al que estás tratando de unirte. Inténtalo de nuevo más tarde.,Impossible de trouver le serveur que vous tentez de rejoindre. Veuillez réessayer plus tard.,Impossibile trovare il server a cui stai cercando di unirti. Riprova più tardi.,参加しようとしているサーバーが見つかりません。後でもう一度試してください。,참가하려는 서버를 찾을 수 없습니다. 나중에 다시 시도하세요.,Impossível encontrar o servidor no qual você está tentando entrar. Tente novamente.,Impossível encontrar o servidor no qual você está tentando entrar. Tente novamente.,"Невозможно найти выбранный вами сервер. Пожалуйста, повторите попытку.",无法找到你尝试加入的服务器。请稍后重试。,找不到您嘗試加入的伺服器,請稍後再試。,无法找到你尝试加入的服务器。请稍后重试。 +InGame.ConnectionError.TeleportUnauthorized,,,Attempted to teleport to a place that is restricted.,"Du hast versucht, dich an einen zugangsbeschränkten Ort zu teleportieren.",Attempted to teleport to a place that is restricted.,Se ha intentado teletransportarse a un lugar que está restringido.,Se ha intentado teletransportarse a un lugar que está restringido.,Vous avez tenté de vous téléporter dans un emplacement à accès restreint.,Tentativo di teletrasporto verso una località riservata.,制限されたプレースへのテレポートを試みました。,금지된 장소로 텔레포트를 시도하였습니다.,Tentativa de se teleportar para um local restrito.,Tentativa de se teleportar para um local restrito.,Попытка телепорта в запрещенное место.,尝试传送至受限场景。,您嘗試傳送的空間受到限制。,尝试传送至受限场景。 +InGame.ConnectionError.Title.JoinError,,,Join Error,Verbindungsfehler,Join Error,Error al unirse,Error al unirse,Erreur de connexion,Errore di accesso,参加でエラーが発生,참가 오류 발생,Erro ao entrar,Erro ao entrar,Ошибка присоединения,加入错误,加入時發生錯誤,加入错误 +InGame.ConnectionError.Title.Disconnected,,,Disconnected,Verbindung getrennt,Disconnected,Desconectado,Desconectado,Déconnecté,Disconnesso,切断されました,연결 끊김,Desconectado,Desconectado,Отключено,连接已断开,連線中斷,连接已断开 +InGame.ConnectionError.Title.TeleportFailed,,,Teleport Failed,Teleportation gescheitert,Teleport Failed,Error en la teletransportación,Error en la teletransportación,Erreur de téléportation,Teletrasporto non riuscito,テレポートできませんでした,텔레포트 실패,Falha no teleporte,Falha no teleporte,Телепорт неудачен,传送失败,傳送失敗,传送失败 +InGame.ConnectionError.Message.ErrorCode,,,Error Code: {ERROR_CODE},Fehlercode: {ERROR_CODE},Error Code: {ERROR_CODE},Código del error: {ERROR_CODE},Código del error: {ERROR_CODE},Code Erreur : {ERROR_CODE},Codice di errore: {ERROR_CODE},エラーコード: {ERROR_CODE},오류 코드: {ERROR_CODE},Código de erro: {ERROR_CODE},Código de erro: {ERROR_CODE},Код ошибки: {ERROR_CODE},错误代码: {ERROR_CODE},錯誤代碼:{ERROR_CODE},错误代码: {ERROR_CODE} +InGame.ConnectionError.Button.Reconnect,,,Reconnect,Erneut verbinden,Reconnect,Reconectar,Reconectar,Reconnexion,Riconnetti,再接続,재연결,Reconectar,Reconectar,Повторное соединение,重新连接,重新連線,重新连接 +InGame.ConnectionError.DisconnectConnectionLost,,,Please check your internet connection and try again.,Bitte überprüfe deine Internetverbindung und versuche es erneut.,Please check your internet connection and try again.,Comprueba tu conexión a internet e inténtelo de nuevo.,Comprueba tu conexión a internet e inténtelo de nuevo.,Vérifie ta connexion internet et réessaye.,Controlla la tua connessione a Internet e riprova.,インターネット接続をチェックしてやり直してください。,인터넷 연결을 확인하고 다시 시도하세요.,Verifique sua conexão de internet e tente novamente.,Verifique sua conexão de internet e tente novamente.,Проверьте свое подключение к сети Интернет и повторите попытку.,请检查你的网络连接并重试。,請檢查網路連線再重新嘗試。,请检查你的网络连接并重试。 +InGame.ConnectionError.DisconnectIdle,,,You were disconnected for being idle {RBX_NUM} minutes,"Deine Verbindung wurde getrennt, da du {RBX_NUM} Minuten lang untätig warst",You were disconnected for being idle {RBX_NUM} minutes,Se te ha desconectado por no mostrar actividad durante {RBX_NUM} minutos.,Se te ha desconectado por no mostrar actividad durante {RBX_NUM} minutos.,Tu as été déconnecté.e car tu étais en mode inactif pendant {RBX_NUM} minutes,Sei stato disconnesso perché inattivo per {RBX_NUM} minuti,{RBX_NUM} 分間、アイドル状態だったので切断されました。,{RBX_NUM}분 동안 기본 상태로 있어 연결이 끊겼어요,Você perdeu a conexão por ficar inativo(a) por {RBX_NUM} minutos,Você perdeu a conexão por ficar inativo(a) por {RBX_NUM} minutos,"Вы отключены, так как не были активны в течение {RBX_NUM} мин.",已闲置 {RBX_NUM} 分钟,你的连接已断开。,您已閒置 {RBX_NUM} 分鐘,連線中斷,已闲置 {RBX_NUM} 分钟,你的连接已断开。 +InGame.ConnectionError.UnknownError,,,Unknown error.,Unbekannter Fehler.,Unknown error.,Error desconocido.,Error desconocido.,Erreur inconnue.,Errore imprevisto.,不明なエラーです。,알 수 없는 오류.,Erro desconhecido.,Erro desconhecido.,Неизвестная ошибка.,未知错误。,未知錯誤。,未知错误。 +InGame.ConnectionError.ReconnectFailed,,,Reconnect was unsuccessful. Please try again.,Wiederverbindung war nicht erfolgreich. Bitte versuche es erneut.,Reconnect was unsuccessful. Please try again.,Error en la reconexión. Inténtalo de nuevo.,Error en la reconexión. Inténtalo de nuevo.,Ta tentative de connexion a échoué. Réessaye.,Riconnessione non riuscita. Riprova.,再接続できませんでした。もう一度やり直してください。,재연결에 실패했어요. 다시 시도하세요,Falha ao reconectar. Tente novamente.,Falha ao reconectar. Tente novamente.,Повторное соединение неудачно. Повторите попытку.,重新连接未成功。请重试。,重新連線失敗,請再試一次。,重新连接未成功。请重试。 +InGame.EmotesMenu.SelectAnEmote,,,Select an Emote,Emote auswählen,Select an Emote,Seleccionar un emoticono,Seleccionar un emoticono,Choisis une emote,Seleziona un'emoticon,エモートを選ぶ,감정 표현 선택,Selecione um emote,Selecione um emote,Выбрать эмоцию,选择动作,選擇動作,选择动作 +InGame.EmotesMenu.NoEmotesEquipped,,,No Emotes equipped,Keine Emotes ausgerüstet,No Emotes equipped,No hay emoticonos equipados,No hay emoticonos equipados,Aucune Emote équipée,Nessuna emoticon equipaggiata,エモートをつけていません,장착된 감정 표현 없음,Nenhum emote equipado,Nenhum emote equipado,Эмоция не выбрана,未装备动作,未裝備動作,未装备动作 +InGame.EmotesMenu.ErrorMessageNotSupported,,,You can't use Emotes here.,Du kannst hier keine Emotes verwenden.,You can't use Emotes here.,No puedes usar emoticonos aquí.,No puedes usar emoticonos aquí.,Tu ne peux pas utiliser les Emotes ici.,Qui non puoi usare le emoticon.,ここではエモートは使えません。,여기서는 감정 표현을 사용할 수 없어요.,Você não pode usar emotes aqui.,Você não pode usar emotes aqui.,Здесь нельзя использовать эмоции.,无法在此游戏中使用动作。,無法在此遊戲使用動作。,无法在此游戏中使用动作。 +InGame.EmotesMenu.ErrorMessageR15Only,,,Only R15 avatars can use Emotes.,Nur R15 Avatars können Emotes verwenden.,Only R15 avatars can use Emotes.,Solo los avatares R15 pueden usar emoticonos.,Solo los avatares R15 pueden usar emoticonos.,Seuls les avatars R15 peuvent utiliser les Emotes.,Solo gli avatar R15 possono usare le emoticon.,R15指定のアバターのみエモートを使えます。,R15 아바타만 감정 표현을 사용할 수 있어요.,Apenas avatares R15 podem usar emotes.,Apenas avatares R15 podem usar emotes.,Только аватары R15 могут использовать эмоции.,只有 R15 虚拟形象可以使用动作。,只有 R15 虛擬人偶可以使用動作。,只有 R15 虚拟形象可以使用动作。 +InGame.EmotesMenu.ErrorMessageNoMatchingEmote,,,You can't use that Emote.,Du kannst dieses Emote nicht benutzen.,You can't use that Emote.,No puedes usar ese emoticono.,No puedes usar ese emoticono.,Tu ne peux pas utiliser cette Emote.,Non puoi usare quell'emoticon.,そのエモートは使えません。,이 감정 표현은 사용할 수 없어요.,Você não pode usar esse emote.,Você não pode usar esse emote.,Вы не можете использовать эту эмоцию.,无法使用该动作。,無法使用該動作。,无法使用该动作。 +InGame.EmotesMenu.ErrorMessageTemporarilyUnavailable,,,You can't use Emotes right now.,Du kannst Emotes derzeit nicht verwenden.,You can't use Emotes right now.,No puedes usar emoticonos en este momento.,No puedes usar emoticonos en este momento.,Tu ne peux pas utiliser d'Emote maintenant.,Adesso non puoi usare le emoticon.,今はエモートを使えません。,지금은 감정 표현을 사용할 수 없어요.,Você não pode usar emotes no momento.,Você não pode usar emotes no momento.,Вы не можете использовать эмоции прямо сейчас.,当前无法使用动作。,現在無法使用動作。,当前无法使用动作。 +InGame.EmotesMenu.EmotesDisabled,,,Emotes are disabled in this game.,Emotes sind in diesem Spiel deaktiviert.,Emotes are disabled in this game.,Los emoticonos no están activados en este juego.,Los emoticonos no están activados en este juego.,Les emotes sont désactivés dans ce jeu.,In questo gioco sono state disattivate le emoticon.,このゲームではエモートが無効化されています。,이 게임에서는 감정 표현을 이용할 수 없어요.,O emotes estão desabilitados neste jogo.,O emotes estão desabilitados neste jogo.,Выражение эмоций отключено в этой игре.,此游戏已停用动作。,此遊戲已停用動作。,此游戏已停用动作。 +FriendPlayerPrompt.promptCompletedCallback.UnknownError,,,An error occurred while sending {RBX_NAME} a friend request. Please try again later.,Beim Senden der Freundesanfrage an {RBX_NAME} ist ein Fehler aufgetreten. Bitte versuche es später erneut.,An error occurred while sending {RBX_NAME} a friend request. Please try again later.,Se ha producido un error al enviar una solicitud de amistad a {RBX_NAME}. Inténtalo de nuevo más tarde.,Se ha producido un error al enviar una solicitud de amistad a {RBX_NAME}. Inténtalo de nuevo más tarde.,Une erreur est survenue lors de l'envoi de la demande d'ami à {RBX_NAME}. Veuillez réessayer plus tard.,Si è verificato un errore nell'invio della richiesta di amicizia a {RBX_NAME}. Riprova più tardi.,{RBX_NAME} さんへの友達リクエスト中にエラーが発生しました。しばらくしてからやり直してください。,{RBX_NAME}님에게 친구 요청을 보내는 중 오류가 발생했어요. 나중에 다시 시도하세요.,Um erro ocorreu ao enviar um pedido de amizade para {RBX_NAME}. Tente de novo mais tarde.,Um erro ocorreu ao enviar um pedido de amizade para {RBX_NAME}. Tente de novo mais tarde.,При отправлении запроса дружбы пользователю {RBX_NAME} произошла ошибка. Повторите попытку позже.,向“{RBX_NAME}”发送好友邀请时发生错误。请稍候重试。,向 {RBX_NAME} 傳送好友邀請時發生錯誤,請稍後再試。,向“{RBX_NAME}”发送好友邀请时发生错误。请稍候重试。 +FriendPlayerPrompt.DoPromptUnfriendPlayer,,,Would you like to remove {RBX_NAME} from your friends list?,Möchtest du {RBX_NAME} von deiner Freundesliste entfernen?,Would you like to remove {RBX_NAME} from your friends list?,¿Quieres eliminar a {RBX_NAME} de tu lista de amigos?,¿Quieres eliminar a {RBX_NAME} de tu lista de amigos?,Souhaitez-vous retirer {RBX_NAME} de votre liste d'amis ?,Vuoi rimuovere {RBX_NAME} dalla tua lista amici?,{RBX_NAME} さんを友達リストから削除しますか?,{RBX_NAME}님을 친구 목록에서 삭제할까요?,Gostaria de remover {RBX_NAME} da sua lista de amigos?,Gostaria de remover {RBX_NAME} da sua lista de amigos?,Удалить пользователя {RBX_NAME} из списка друзей?,你想要将“{RBX_NAME}”从你的好友名单中移除吗?,您要將 {RBX_NAME} 從好友名單移除嗎?,你想要将“{RBX_NAME}”从你的好友名单中移除吗? +FriendPlayerPrompt.DoPromptRequestFriendPlayer,,,Would you like to send {RBX_NAME} a Friend Request?,Möchtest du {RBX_NAME} eine Freundesanfrage senden?,Would you like to send {RBX_NAME} a Friend Request?,¿Quieres enviar a {RBX_NAME} una solicitud de amistad?,¿Quieres enviar a {RBX_NAME} una solicitud de amistad?,Souhaitez-vous envoyer une demande d'ami à {RBX_NAME} ?,Vuoi inviare una richiesta di amicizia a {RBX_NAME}?,{RBX_NAME} さんに友達リクエストを送りますか?,{RBX_NAME}님에게 친구 요청을 보낼까요?,Gostaria de enviar um pedido de amizade para {RBX_NAME}?,Gostaria de enviar um pedido de amizade para {RBX_NAME}?,Отправить пользователю {RBX_NAME} запрос дружбы?,你想要向“{RBX_NAME}”发送好友邀请吗?,您要向 {RBX_NAME} 傳送好友邀請嗎?,你想要向“{RBX_NAME}”发送好友邀请吗? +FriendPlayerPrompt.promptCompletedCallback.AtFriendLimit,,,You can not send a friend request to {RBX_NAME} because they are at the max friend limit.,"Du kannst {RBX_NAME} keine Freundesanfrage senden, da dieser Spieler die max. Anzahl an Freunden erreicht hat.",You can not send a friend request to {RBX_NAME} because they are at the max friend limit.,No puedes enviar una solicitud de amistad a {RBX_NAME} porque ha alcanzado el límite máximo de amigos.,No puedes enviar una solicitud de amistad a {RBX_NAME} porque ha alcanzado el límite máximo de amigos.,"Vous ne pouvez pas envoyer une demande d'ami à {RBX_NAME}, car il a atteint sa limite maximale d'amis.",Non puoi inviare una richiesta di amicizia a {RBX_NAME} perché ha raggiunto il limite massimo di amici.,{RBX_NAME} さんの友達が上限に達しているため友達リクエストが送れませんでした。,{RBX_NAME}의 친구 수가 너무 많아 더 이상 친구 요청을 보낼 수 없습니다.,Você não pode enviar um pedido de amizade para {RBX_NAME} pois ele(a) alcançou o limite máximo de amigos.,Você não pode enviar um pedido de amizade para {RBX_NAME} pois ele(a) alcançou o limite máximo de amigos.,Нельзя отправить запрос дружбы пользователю {RBX_NAME}: количество его друзей достигло максимума.,由于对方已达到好友数量上限,你无法向“{RBX_NAME}”发送好友邀请。,無法傳送好友邀請,{RBX_NAME} 的好友人數已達上限。,由于对方已达到好友数量上限,你无法向“{RBX_NAME}”发送好友邀请。 +FriendPlayerPrompt.Title.SendFriendRequest,,,Send Friend Request?,Freundesanfrage senden?,Send Friend Request?,¿Enviar una solicitud de amistad?,¿Enviar una solicitud de amistad?,Envoyer demande d'ami?,Invia richiesta di amicizia?,友達リスエストを送信しますか。,친구 요청 전송?,Enviar pedido de amizade?,Enviar pedido de amizade?,Отправить запрос дружбы?,发送好友邀请?,傳送好友邀請?,发送好友邀请? +FriendPlayerPrompt.Title.UnfriendPlayer,,,Unfriend Player?,Spieler von der Freundesliste entfernen?,Unfriend Player?,¿Cancelar amistad con jugador?,¿Cancelar amistad con jugador?,Ne plus être ami du joueur?,Togli amicizia a giocatore?,プレイヤーを友達解除しますか。,친구를 끊을까요?,Cancelar amizade com jogador?,Cancelar amizade com jogador?,Недружественный игрок?,与玩家解除好友关系?,刪除此好友?,与玩家解除好友关系? +FriendPlayerPrompt.Label.Unfriend,,,Unfriend,Von der Freundesliste entfernen,Unfriend,Cancelar amistad,Cancelar amistad,Ne plus être ami,Togli amicizia,友達解除,친구 끊기,Remover amigo,Remover amigo,Недруг,解除好友关系,刪除好友,解除好友关系 +InGame.GameplayPaused.Title,,,Gameplay Paused,Spiel pausiert,Gameplay Paused,Juego pausado,Juego pausado,Jeu en pause,Gioco in pausa,ゲームプレイが一時停止しました,플레이가 일시 중지됨,Jogo pausado,Jogo pausado,Игра приостановлена,游戏已暂停,遊戲已暫停,游戏已暂停 +InGame.GameplayPaused.Body,,,Gameplay has been paused: please wait while the game content loads.,"Spiel pausiert: Bitte warte, solange der Spieleinhalt lädt.",Gameplay has been paused: please wait while the game content loads.,Se ha pausado el juego. Espera mientras se carga el contenido del juego.,Se ha pausado el juego. Espera mientras se carga el contenido del juego.,Le jeu est en pause : attends quelques instants pendant que le contenu du jeu se charge.,Il gioco è stato messo in pausa: attendi durante il caricamento del contenuto di gioco.,ゲームプレイが一時停止しました: ゲームコンテンツを読み込んでいる間、お待ちください。,플레이가 일시 중지되었습니다. 게임 콘텐츠를 불러오고 있으니 잠시만 기다리세요.,O jogo foi pausado: espere o carregamento do conteúdo.,O jogo foi pausado: espere o carregamento do conteúdo.,Игра приостановлена: дождитесь загрузки игровых данных,游戏已暂停,请耐心等待游戏内容加载完成。,遊戲已暫停,請等候遊戲內容完成載入。,游戏已暂停,请耐心等待游戏内容加载完成。 +InGame.HelpMenu.UpArrow,,,Up Arrow,Aufwärtspfeil,Up Arrow,Flecha hacia arriba,Flecha hacia arriba,Flèche haut,Freccia SU,上カーソル,위쪽 화살표,Seta para cima,Seta para cima,Стрелка вверх,上箭头,↑,上箭头 +InGame.HelpMenu.DownArrow,,,Down Arrow,Abwärtspfeil,Down Arrow,Flecha hacia abajo,Flecha hacia abajo,Flèche bas,Freccia GIÙ,下カーソル,아래쪽 화살표,Seta para baixo,Seta para baixo,Стрелка вниз,下箭头,↓,下箭头 +InGame.HelpMenu.LeftArrow,,,Left Arrow,Linkspfeil,Left Arrow,Flecha hacia la izquierda,Flecha hacia la izquierda,Flèche gauche,Freccia SINISTRA,左カーソル,왼쪽 화살표,Seta esquerda,Seta esquerda,Стрелка влево,左箭头,←,左箭头 +InGame.HelpMenu.RightArrow,,,Right Arrow,Rechtspfeil,Right Arrow,Flecha hacia la derecha,Flecha hacia la derecha,Flèche droite,Freccia DESTRA,右カーソル,오른쪽 화살표,Seta direita,Seta direita,Стрелка вправо,右箭头,→,右箭头 +InGame.HelpMenu.Label.ServerVersion,,,Server Version:,Serverversion:,Server Version:,Versión del servidor:,Versión del servidor:,Version du serveur :,Versione del server:,サーバーバージョン:,서버 버전:,Versão de servidor:,Versão de servidor:,Версия сервера:,服务器版本:,伺服器版本:,服务器版本: +InGame.HelpMenu.Label.ClientVersion,,,Client Version:,Client-Version:,Client Version:,Versión del cliente:,Versión del cliente:,Version du client :,Versione del client:,クライアントバージョン:,클라이언트 버전:,Versão de cliente:,Versão de cliente:,Версия клиента:,客户端版本:,客戶端版本:,客户端版本: +InGame.HelpMenu.Label.PlayerScripts,,,PlayerScripts:,SpielerScripts:,PlayerScripts:,PlayerScripts:,PlayerScripts:,PlayerScripts :,PlayerScripts:,PlayerScripts:,PlayerScripts:,Scripts de jogador:,Scripts de jogador:,СкриптИгрока:,玩家脚本:,PlayerScripts:,玩家脚本: +InGame.HelpMenu.Label.PlaceVersion,,,Place Version:,Orts-Version:,Place Version:,Versión del lugar:,Versión del lugar:,Version de la location :,Versione della località:,プレースバージョン:,장소 버전:,Versão do local:,Versão do local:,Версия расположения:,场景版本:,空間版本:,场景版本: +InGame.HelpMenu.Label.ClientCoreScriptVersion,,,Client CoreScript Version:,Client-CoreScript-Version:,Client CoreScript Version:,Versión CoreScript del cliente:,Versión CoreScript del cliente:,Version CoreScript du client :,Versione CoreScript del client:,クライアントCoreScriptバージョン:,클라이언트 CoreScript 버전:,Versão de cliente CoreScript:,Versão de cliente CoreScript:,Клиентская версия CoreScript:,客户端 CoreScript 版本:,客戶端 CoreScript 版本:,客户端 CoreScript 版本: +InGame.HelpMenu.Message.ClientVersion,,,Client Version: {RBX_NUM},Client-Version: {RBX_NUM},Client Version: {RBX_NUM},Versión del cliente: {RBX_NUM},Versión del cliente: {RBX_NUM},Version du client : {RBX_NUM},Versione del client: {RBX_NUM},クライアントバージョン: {RBX_NUM},클라이언트 버전: {RBX_NUM},Versão de cliente: {RBX_NUM},Versão de cliente: {RBX_NUM},Версия клиента: {RBX_NUM},客户端版本: {RBX_NUM},客戶端版本:{RBX_NUM},客户端版本: {RBX_NUM} +InGame.HelpMenu.Message.ServerVersion,,,Server Version: {RBX_NUM},Serverversion: {RBX_NUM},Server Version: {RBX_NUM},Versión del servidor: {RBX_NUM},Versión del servidor: {RBX_NUM},Version du serveur : {RBX_NUM},Versione del server: {RBX_NUM},サーバーバージョン: {RBX_NUM},서버 버전: {RBX_NUM},Versão de servidor: {RBX_NUM},Versão de servidor: {RBX_NUM},Версия сервера: {RBX_NUM},服务器版本: {RBX_NUM},伺服器版本:{RBX_NUM},服务器版本: {RBX_NUM} +InGame.HelpMenu.Message.PlayerScripts,,,PlayerScripts:{RBX_STR},SpielerScripts:{RBX_STR},PlayerScripts:{RBX_STR},PlayerScripts:{RBX_STR},PlayerScripts:{RBX_STR},PlayerScripts : {RBX_STR},PlayerScripts:{RBX_STR},PlayerScripts:{RBX_STR},PlayerScripts: {RBX_STR},Scripts de jogador: {RBX_STR},Scripts de jogador: {RBX_STR},СкриптИгрока: {RBX_STR},玩家脚本:{RBX_STR},PlayerScripts:{RBX_STR},玩家脚本:{RBX_STR} +InGame.HelpMenu.Message.PlaceVersion,,,Place Version: {RBX_NUM},Orts-Version: {RBX_NUM},Place Version: {RBX_NUM},Versión del lugar: {RBX_NUM},Versión del lugar: {RBX_NUM},Version de la location : {RBX_NUM},Versione della località: {RBX_NUM},プレースバージョン: {RBX_NUM},장소 버전: {RBX_NUM},Versão do local: {RBX_NUM},Versão do local: {RBX_NUM},Версия расположения: {RBX_NUM},场景版本:{RBX_NUM},空間版本:{RBX_NUM},场景版本:{RBX_NUM} +InGame.HelpMenu.Leave,,,Leave,Verlassen,Leave,Salir,Salir,Quitter,Esci,終了,나가기,Sair,Sair,Выйти,离开,離開,离开 +InGame.HelpMenu.Resume,,,Resume,Fortsetzen,Resume,Reanudar,Reanudar,Reprendre,Riprendi,再開,다시 시작,Continuar,Continuar,Вернуться,继续,繼續,继续 +InGame.HelpMenu.ConfirmLeaveGame,,,Are you sure you want to leave?,Möchtest du das Spiel wirklich verlassen?,Are you sure you want to leave?,¿Seguro que quieres salir del juego?,¿Seguro que quieres salir del juego?,Voulez-vous vraiment quitter ?,Vuoi davvero uscire?,終了してよろしいですか?,정말로 나갈까요?,Quer mesmo sair?,Quer mesmo sair?,Выйти из игры?,是否确定要离开?,確定離開?,是否确定要离开? +InGame.InspectMenu.Description.NoInventoryNotice,,,This user isn't wearing any items you can inspect.,"Dieser Benutzer trägt keine Gegenstände, die du beschauen kannst.",This user isn't wearing any items you can inspect.,No puedes inspeccionar la ropa de este usuario porque no lleva nada puesto.,No puedes inspeccionar la ropa de este usuario porque no lleva nada puesto.,Cette utilisateur ne porte aucun objet possible d'être inspecté.,Questo utente non indossa alcun oggetto che puoi esaminare.,このユーザーはあなたが確認できるアイテムをつけていません。,이 사용자는 내가 확인할 수 있는 아이템을 갖추고 있지 않습니다.,Este usuário não está usando nenhum item que você possa inspecionar.,Este usuário não está usando nenhum item que você possa inspecionar.,Этот пользователь не носит интересующих вас предметов.,此用户未穿戴你可以检视的物品。,此使用者未穿戴可檢視的道具。,此用户未穿戴你可以检视的物品。 +InGame.InspectMenu.Description.MultipleBundlesNotice,,,This item is part of multiple bundles.,Dieser Artikel ist Teil mehrerer Pakete.,This item is part of multiple bundles.,Este objeto forma parte de varios paquetes.,Este objeto forma parte de varios paquetes.,Cet article fait partie de plusieurs paquets.,Questo oggetto fa parte di bundle multipli.,このアイテムは複数のバンドルの一部です。,이 아이템은 여러 번들의 일부입니다.,Este item faz parte de vários pacotes.,Este item faz parte de vários pacotes.,Этот предмет является частью нескольких наборов.,此物品为多个套装的一部分。,此道具為多個組合的一部分。,此物品为多个套装的一部分。 +InGame.InspectMenu.Description.SingleBundleNotice,,,This item is part of a bundle.,Dieser Artikel ist Teil eines Pakets.,This item is part of a bundle.,Este objeto forma parte de un paquete.,Este objeto forma parte de un paquete.,Cet article fait partie d'un paquet.,Questo oggetto fa parte di un bundle.,このアイテムは、バンドルの一部です。,이 아이템은 번들의 일부입니다.,Este item faz parte de um pacote.,Este item faz parte de um pacote.,Этот предмет является частью набора.,此物品为套装的一部分。,此道具為組合的一部分。,此物品为套装的一部分。 +InGame.InspectMenu.Action.TakeOff,,,Take Off,Abnehmen,Take Off,Quitar,Quitar,Enlever,Togli,外す,삭제,Remover,Remover,Снять,脱下,脫下,脱下 +InGame.InspectMenu.Action.TryOn,,,Try On,Anprobieren,Try On,Probar,Probar,Essaie,Prova,つける,해 보기,Experimentar,Experimentar,Примерить,试穿,試穿,试穿 +InGame.InspectMenu.Action.Buy,,,Buy,Kaufen,Buy,Comprar,Comprar,Acheter,Compra,買う,구매,Comprar,Comprar,Купить,购买,購買,购买 +InGame.InspectMenu.Action.Inspect,,,Inspect,Beschauen,Inspect,Inspeccionar,Inspeccionar,Inspecter,Esamina,確認,확인,Inspecionar,Inspecionar,Осмотреть,检视,檢視,检视 +InGame.InspectMenu.Action.View,,,View,Ansehen,View,Ver,Ver,Voir,Osserva,表示,보기,Ver,Ver,Осмотр,查看,查看,查看 +InGame.InspectMenu.Label.Owned,,,Owned,Im Besitz,Owned,En posesión,En posesión,Possédé,Posseduto,所有済み,소유함,Possuído,Possuído,В наличии,已拥有,已擁有,已拥有 +InGame.InspectMenu.Label.Offsale,,,Offsale,Offsale,Offsale,Fuera de venta,Fuera de venta,Pas en vente,Non in vendita,非売品,오프세일,Indisponível,Indisponível,Распродано,下架,下架,下架 +InGame.InspectMenu.Label.Limited,,,Limited,In limitierter Auflage,Limited,Limitado,Limitado,Limité-e,Limitato,限定,한정품,Limitado,Limitado,Ограничено,限量,限定,限量 +InGame.InspectMenu.Label.Inventory,,,{PLAYER_NAME}'s Inventory,{PLAYER_NAME}s Inventar,{PLAYER_NAME}'s Inventory,Inventario de {PLAYER_NAME},Inventario de {PLAYER_NAME},Inventaire de {PLAYER_NAME},Inventario di {PLAYER_NAME},{PLAYER_NAME} さんのインベントリ,{PLAYER_NAME}님의 인벤토리,Inventário de {PLAYER_NAME},Inventário de {PLAYER_NAME},Инвентарь игрока {PLAYER_NAME},“{PLAYER_NAME}”的道具库,{PLAYER_NAME} 的道具欄,“{PLAYER_NAME}”的道具库 +InGame.InspectMenu.Label.CurrentlyWearing,,,Currently Wearing,Derzeit getragen,Currently Wearing,Ropa actual,Ropa actual,Porté-e actuellement,Indossato attualmente,今つけているもの,현재 장착 중,Vestindo atualmente,Vestindo atualmente,Сейчас одето,目前穿戴,目前穿戴,目前穿戴 +InGame.InspectMenu.Label.By,,,By {CREATOR},Von {CREATOR},By {CREATOR},De {CREATOR},De {CREATOR},Par {CREATOR},Da {CREATOR},{CREATOR} 作,{CREATOR} 제작,De {CREATOR},De {CREATOR},Создатель: {CREATOR},创作者:{CREATOR},創作者:{CREATOR},创作者:{CREATOR} +InGame.InspectMenu.Description.LimitedNotice,,,This item can only be purchased from resellers in the Catalog.,Dieser Artikel kann nur von Fachhändlern im Katalog gekauft werden.,This item can only be purchased from resellers in the Catalog.,Este objeto solo puede comprarse de revendedores en el catálogo.,Este objeto solo puede comprarse de revendedores en el catálogo.,Objet disponible à l'achat uniquement à des revendeurs dans le Catalogue. ,Questo oggetto può essere acquistato solo dai rivenditori presenti nel catalogo.,このアイテムはカタログ内の再販者からのみ購入できます。,이 아이템은 카탈로그의 리셀러에게서만 구입할 수 있습니다.,Este item só pode ser comprado de revendedores no Catálogo.,Este item só pode ser comprado de revendedores no Catálogo.,Этот предмет можно приобрести только у продавцов из каталога.,此项目只能在商店中从转售者处购买。,此道具只能在型錄裡透過轉賣者購買。,此项目只能在商店中从转售者处购买。 +InGame.InspectMenu.Label.Costume,,,{PLAYER_NAME}'s Avatar,{PLAYER_NAME}s Avatar,{PLAYER_NAME}'s Avatar,Avatar de {PLAYER_NAME},Avatar de {PLAYER_NAME},L'avatar de {PLAYER_NAME},Avatar di {PLAYER_NAME},{PLAYER_NAME} さんのアバター,{PLAYER_NAME}님의 아바타,Avatar de {PLAYER_NAME},Avatar de {PLAYER_NAME},Аватар игрока {PLAYER_NAME},{PLAYER_NAME}的虚拟形象,{PLAYER_NAME} 的虛擬人偶,{PLAYER_NAME}的虚拟形象 +InGame.InspectMenu.Label.Avatar,,,{PLAYER_NAME}'s Avatar,{PLAYER_NAME}s Avatar,{PLAYER_NAME}'s Avatar,Avatar de {PLAYER_NAME},Avatar de {PLAYER_NAME},L'avatar de {PLAYER_NAME},Avatar di {PLAYER_NAME},{PLAYER_NAME} さんのアバター,{PLAYER_NAME}님의 아바타,Avatar de {PLAYER_NAME},Avatar de {PLAYER_NAME},Аватар игрока {PLAYER_NAME},{PLAYER_NAME}的虚拟形象,{PLAYER_NAME} 的虛擬人偶,{PLAYER_NAME}的虚拟形象 +InGame.InspectMenu.Label.NoResellers,,,No Resellers,Keine Fachhändler,No Resellers,No hay revendedores,No hay revendedores,Aucun revendeur,Nessun rivenditore,再販者がいません,재판매자가 없어요,Nenhum revendedor,Nenhum revendedor,Нет посредников,无人转售,沒有轉賣者,无人转售 +InGame.InspectMenu.Label.PremiumOnly,,,Premium Only,Nur Premium,Premium Only,Solo Premium,Solo Premium,Premium uniquement,Solo Premium,Premium限定,Premium 전용,Apenas Premium,Apenas Premium,Только премиум-подписка,仅限 Premium,Premium 限定,仅限 Premium +NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Decreased,,,Decreased to {RBX_NUMBER},Verringert auf {RBX_NUMBER},Decreased to {RBX_NUMBER},Reducción a {RBX_NUMBER},Reducción a {RBX_NUMBER},Réduit jusqu'à {RBX_NUMBER},Sceso a {RBX_NUMBER},{RBX_NUMBER}に減少,{RBX_NUMBER}(으)로 감소,Reduzido para {RBX_NUMBER},Reduzido para {RBX_NUMBER},Уменьшено до {RBX_NUMBER},减至 {RBX_NUMBER},減少到 {RBX_NUMBER},减至 {RBX_NUMBER} +NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Increased,,,Increased to {RBX_NUMBER},Erhöht auf {RBX_NUMBER},Increased to {RBX_NUMBER},Aumento hasta {RBX_NUMBER},Aumento hasta {RBX_NUMBER},Augmenté jusqu'à {RBX_NUMBER},Salito a {RBX_NUMBER},{RBX_NUMBER}に増加,{RBX_NUMBER}(으)로 증가,Aumentado para {RBX_NUMBER},Aumentado para {RBX_NUMBER},Увеличено до {RBX_NUMBER},增至 {RBX_NUMBER},增加到 {RBX_NUMBER},增至 {RBX_NUMBER} +NotificationScript2.NewFollower,,,New Follower {RBX_NAME} is now following you!,Neuer Follower {RBX_NAME} folgt dir jetzt!,New Follower {RBX_NAME} is now following you!,Seguidor nuevo: ¡{RBX_NAME} te está siguiendo!,Seguidor nuevo: ¡{RBX_NAME} te está siguiendo!,"Désormais, le nouvel abonné, {RBX_NAME}, vous suit !",Il nuovo follower {RBX_NAME} ha cominciato a seguirti!,新規フォロワー {RBX_NAME} さんがあなたをフォローしました!,새 팔로워 {RBX_NAME}님이 회원님을 팔로우합니다!,"O novo seguidor, {RBX_NAME}, está seguindo você agora!","O novo seguidor, {RBX_NAME}, está seguindo você agora!",На вас подписался пользователь {RBX_NAME}!,新粉丝“{RBX_NAME}”现在关注了你!,{RBX_NAME} 正在追蹤您!,新粉丝“{RBX_NAME}”现在关注了你! +NotificationScript2.FriendRequestEvent.Accept,,,You are now friends with {RBX_NAME}!,Du bist jetzt Freunde mit {RBX_NAME}!,You are now friends with {RBX_NAME}!,Ahora eres amigo de {RBX_NAME}.,Ahora eres amigo de {RBX_NAME}.,Vous êtes maintenant ami avec {RBX_NAME}!,Sei ora amici di {RBX_NAME}!,{RBX_NAME} さんと友達になりました!,이제 {RBX_NAME}님과 친구예요!,You are now friends with {RBX_NAME}!,You are now friends with {RBX_NAME}!,Вы теперь друзья с {RBX_NAME}!,你现在与 {RBX_NAME} 成为好友了!,您已與 {RBX_NAME} 成為好友!,你现在与 {RBX_NAME} 成为好友了! +NotificationScript2.onPointsRewarded.negative,,,You lost {RBX_NUMBER} points!,Du hast {RBX_NUMBER} Punkte verloren!,You lost {RBX_NUMBER} points!,¡Has perdido {RBX_NUMBER} puntos!,¡Has perdido {RBX_NUMBER} puntos!,Vous avez perdu {RBX_NUMBER} points !,Hai perso {RBX_NUMBER} punti!,{RBX_NUMBER} ポイント失いました!,{RBX_NUMBER} 포인트를 잃었어요!,Você perdeu {RBX_NUMBER} pontos!,Você perdeu {RBX_NUMBER} pontos!,Вы потеряли {RBX_NUMBER} очк.!,你丢失了 {RBX_NUMBER} 点!,您失去 {RBX_NUMBER} 點!,你丢失了 {RBX_NUMBER} 点! +NotificationScript2.onPointsAwarded.single,,,You received {RBX_NUMBER} point!,Du hast {RBX_NUMBER} Punkt erhalten!,You received {RBX_NUMBER} point!,¡Has recibido {RBX_NUMBER} punto!,¡Has recibido {RBX_NUMBER} punto!,Vous avez reçu {RBX_NUMBER} point !,Hai ricevuto {RBX_NUMBER} punto!,{RBX_NUMBER} ポイントをゲットしました!,{RBX_NUMBER} 포인트를 받았어요!,Você recebeu {RBX_NUMBER} ponto!,Você recebeu {RBX_NUMBER} ponto!,Вы получили {RBX_NUMBER} очк.!,你得到了 {RBX_NUMBER} 点!,您得到 {RBX_NUMBER} 點!,你得到了 {RBX_NUMBER} 点! +NotificationScript2.onPointsAwarded.multiple,,,You received {RBX_NUMBER} points!,Du hast {RBX_NUMBER} Punkte erhalten!,You received {RBX_NUMBER} points!,¡Has recibido {RBX_NUMBER} puntos!,¡Has recibido {RBX_NUMBER} puntos!,Vous avez reçu {RBX_NUMBER} points !,Hai ricevuto {RBX_NUMBER} punti!,{RBX_NUMBER} ポイントをゲットしました!,{RBX_NUMBER} 포인트를 받았어요!,Você recebeu {RBX_NUMBER} pontos!,Você recebeu {RBX_NUMBER} pontos!,Вы получили {RBX_NUMBER} очк.!,你得到了 {RBX_NUMBER} 点!,您得到 {RBX_NUMBER} 點!,你得到了 {RBX_NUMBER} 点! +NotificationScript2.onBadgeAwardedTitle,,,Badge Awarded,Abzeichen verliehen,Badge Awarded,Emblema concedido,Emblema concedido,Badge accordé,Contrassegno conferito,授与されたバッジ,배지 획득,Emblema concedido,Emblema concedido,Получен значок,已获得徽章,已獲得徽章,已获得徽章 +NotificationScript2.onBadgeAwardedDetail,,,"{RBX_NAME} won {CREATOR_NAME}'s ""{BADGE_NAME}"" award!",{RBX_NAME} hat das „{BADGE_NAME}“ von {CREATOR_NAME} gewonnen!,"{RBX_NAME} won {CREATOR_NAME}'s ""{BADGE_NAME}"" award!","¡{RBX_NAME} ha ganado el premio ""{BADGE_NAME}"" de {CREATOR_NAME}!","¡{RBX_NAME} ha ganado el premio ""{BADGE_NAME}"" de {CREATOR_NAME}!",{RBX_NAME} a gagné la récompense « {BADGE_NAME} » de {CREATOR_NAME} !,"{RBX_NAME} ha vinto il premio ""{BADGE_NAME}"" di {CREATOR_NAME}!",{RBX_NAME} が {CREATOR_NAME} の「{BADGE_NAME}」を受賞!,{RBX_NAME}님이 보상으로 {CREATOR_NAME}의 '{BADGE_NAME}'을(를) 획득했어요!,"{RBX_NAME} ganhou o prêmio ""{BADGE_NAME}"" de {CREATOR_NAME}!","{RBX_NAME} ganhou o prêmio ""{BADGE_NAME}"" de {CREATOR_NAME}!",{RBX_NAME} получает награду {CREATOR_NAME} «{BADGE_NAME}»!,{RBX_NAME}贏得了{CREATOR_NAME}的“{BADGE_NAME}”徽章!,{RBX_NAME} 贏得了 {CREATOR_NAME} 的「{BADGE_NAME}」徽章!,{RBX_NAME}贏得了{CREATOR_NAME}的“{BADGE_NAME}”徽章! +NotificationScript2.Screenshot.Title,,,Screenshot Taken,Screenshot erstellt,Screenshot Taken,Captura tomada,Captura tomada,Capture d'écran enregistrée,Screenshot catturato,撮影したスクリーンショット,스크린숏 찍기 완료,Captura de tela salva,Captura de tela salva,Снимок экрана сделан,已截取屏幕快照,已截圖,已截取屏幕快照 +NotificationScript2.Screenshot.Description,,,Check out your screenshots folder to see it.,"Schau dir deinen Screenshots-Ordner an, um ihn zu sehen.",Check out your screenshots folder to see it.,Puedes verla en la carpeta de capturas de pantalla.,Puedes verla en la carpeta de capturas de pantalla.,Ouvrez le dossier des captures d'écran pour la visualiser.,Vai nella cartella screenshot per vederlo.,見るにはスクリーンショットフォルダをチェック。,스크린숏 폴더에서 확인하세요.,Confira sua pasta de capturas de tela para vê-la.,Confira sua pasta de capturas de tela para vê-la.,"Откройте папку со снимками экрана, чтобы его увидеть.",请检查你的屏幕快照文件夹以查看。,前往您的截圖資料夾查看。,请检查你的屏幕快照文件夹以查看。 +NotificationScript2.Screenshot.ButtonText,,,Open Folder,Ordner öffnen,Open Folder,Abrir carpeta,Abrir carpeta,Ouvrir le dossier,Apri cartella,フォルダを開く,폴더 열기,Abrir pasta,Abrir pasta,Открыть папку,打开文件夹,開啟資料夾,打开文件夹 +NotificationScript2.Video.Title,,,Video Recorded,Video aufgenommen,Video Recorded,Vídeo grabado,Vídeo grabado,Vidéo enregistrée,Video registrato,録画したビデオ,녹화 비디오 완료,Vídeo gravado,Vídeo gravado,Видео записано,视频已录制,已錄製影片,视频已录制 +NotificationScript2.Video.Description,,,Check out your videos folder to see it.,"Schau dir deinen Video-Ordner an, um es zu sehen.",Check out your videos folder to see it.,Puedes verlo en la carpeta de vídeos.,Puedes verlo en la carpeta de vídeos.,Ouvrez le dossier des vidéos pour la visualiser.,Vai nella cartella video per vederlo.,見るにはビデオフォルダをチェック。,비디오 폴더에서 확인하세요.,Confira sua pasta de vídeos para vê-lo.,Confira sua pasta de vídeos para vê-lo.,"Откройте папку с видеозаписями, чтобы его увидеть.",检查你的视频文件夹以查看。,前往您的影片資料夾查看。,检查你的视频文件夹以查看。 +NotificationScript2.Video.ButtonText,,,Open Folder,Ordner öffnen,Open Folder,Abrir carpeta,Abrir carpeta,Ouvrir le dossier,Apri cartella,フォルダを開く,폴더 열기,Abrir pasta,Abrir pasta,Открыть папку,打开文件夹,開啟資料夾,打开文件夹 +PlayerDropDown.onUnfollowButtonPress.success,,,No longer following {RBX_NAME}.,Folgst nicht mehr {RBX_NAME},No longer following {RBX_NAME}.,Ya no estás siguiendo a {RBX_NAME}.,Ya no estás siguiendo a {RBX_NAME}.,Vous ne suivez plus {RBX_NAME}.,Non stai seguendo ancora {RBX_NAME}.,{RBX_NAME} さんをフォローしていません。,{RBX_NAME}님 팔로우 취소.,Você não está mais seguindo {RBX_NAME}.,Você não está mais seguindo {RBX_NAME}.,Вы перестали следовать {RBX_NAME}.,已取消关注“{RBX_NAME}”,已取消追蹤 {RBX_NAME}。,已取消关注“{RBX_NAME}” +PlayerDropDown.onFollowButtonPress.success,,,now following {RBX_NAME},folgst jetzt {RBX_NAME},now following {RBX_NAME},Estás siguiendo a {RBX_NAME},Estás siguiendo a {RBX_NAME},Vous suivez désormais {RBX_NAME}.,stai seguendo {RBX_NAME},{RBX_NAME} さんをフォロー中,{RBX_NAME} 팔로우 중,agora seguindo {RBX_NAME},agora seguindo {RBX_NAME},подписались на {RBX_NAME},现正关注“{RBX_NAME}”,你正在追蹤 {RBX_NAME},现正关注“{RBX_NAME}” +PlayerDropDown.CannotSendFriendRequest,,,Cannot send friend request,Freundesanfrage konnte nicht gesendet werden,Cannot send friend request,No se puede enviar una solicitud de amistad,No se puede enviar una solicitud de amistad,Tu ne peux pas envoyer de demande d’ami,Impossibile inviare richiesta di amicizia,友達リクエストを送信できません,친구 요청 전송 불가,Impossível enviar pedido de amizade,Impossível enviar pedido de amizade,Нельзя отправить запрос на приглашение в друзья,无法发送好友邀请,無法傳送好友邀請,无法发送好友邀请 +PlayerDropDown.FriendLimit,,,You are at the max friends limit.,Du hast die max. Anzahl an Freunden erreicht.,You are at the max friends limit.,Has llegado al número máximo de amigos permitido.,Has llegado al número máximo de amigos permitido.,Tu as atteint la limite d'amis.,Hai raggiunto il limite massimo di amici.,友達の数が上限に達しました。,친구 수가 최대 한도에 도달했어요.,Você alcançou o limite máximo de amigos.,Você alcançou o limite máximo de amigos.,Количество ваших друзей достигло максимума.,你的好友人数已达上限。,您已達好友人數上限。,你的好友人数已达上限。 +PlayerDropDown.OtherPlayerFriendLimit,,,{RBX_NAME} is at the max friends limit.,{RBX_NAME} hat die max. Anzahl an Freunden erreicht.,{RBX_NAME} is at the max friends limit.,{RBX_NAME} ha llegado al número máximo de amigos permitido.,{RBX_NAME} ha llegado al número máximo de amigos permitido.,{RBX_NAME} a atteint la limite d'amis.,{RBX_NAME} ha raggiunto il limite massimo di amici.,{RBX_NAME} さんで友達の数が上限に達しました。,{RBX_NAME}님의 친구 수가 최대 한도에 도달했어요.,{RBX_NAME} alcançou o limite máximo de amigos.,{RBX_NAME} alcançou o limite máximo de amigos.,Максимальное количество ваших друзей: {RBX_NAME},{RBX_NAME} 的好友人数已达上限。,{RBX_NAME} 已達好友人數上限。,{RBX_NAME} 的好友人数已达上限。 +PlayerDropDown.UnFollow,,,Unfollow,Nicht mehr folgen,Unfollow,Dejar de seguir,Dejar de seguir,Ne plus suivre,Non seguire più,フォローをやめる,팔로우 취소,Deixar de seguir,Deixar de seguir,Отписаться,取消关注,取消追蹤,取消关注 +PlayerDropDown.Follow,,,Follow,Folgen,Follow,Seguir,Seguir,Suivre,Segui,フォロー,팔로우,Seguir,Seguir,Подписаться,关注,追蹤,关注 +PlayerDropDown.UnBlock,,,Unblock Player,Spieler nicht mehr sperren,Unblock Player,Desbloquear jugador,Desbloquear jugador,Débloquer joueur,Sblocca giocatore,プレイヤーのブロックを解除,플레이어 차단 해제,Desbloquear jogador,Desbloquear jogador,Разблокировать игрока,取消屏蔽玩家,解除封鎖玩家,取消屏蔽玩家 +PlayerDropDown.Block,,,Block Player,Spieler sperren,Block Player,Bloquear jugador,Bloquear jugador,Bloquer joueur,Blocca giocatore,プレイヤーをブロック,플레이어 차단,Bloquear jogador,Bloquear jogador,Заблокировать игрока,屏蔽玩家,封鎖玩家,屏蔽玩家 +PlayerDropDown.Report,,,Report Abuse,Verstoß melden,Report Abuse,Denunciar abuso,Denunciar abuso,Signaler une infraction,Segnala un abuso,規約違反を報告,신고하기,Denunciar abuso,Denunciar abuso,Сообщить о нарушении,举报滥用,檢舉濫用,举报滥用 +PlayerDropDown.Examine,,,Examine Avatar,Avatar inspizieren,Examine Avatar,Inspeccionar avatar,Inspeccionar avatar,Examiner l'avatar,Esamina avatar,アバターをチェック,아바타 검토,Examinar avatar,Examinar avatar,Посмотреть аватар,查看虚拟形象,檢視人偶,查看虚拟形象 +PlayerDropDown.Unfriend,,,Unfriend,Von der Freundesliste entfernen,Unfriend,Cancelar amistad,Cancelar amistad,Retirer des amis,Togli amicizia,友達解除,친구 끊기,Remover amigo,Remover amigo,Удалить из друзей,解除好友关系,刪除好友,解除好友关系 +PlayerDropDown.FriendRequest,,,Friend Request,Anfrage senden,Friend Request,Enviar solicitud de amistad,Enviar solicitud de amistad,Demande d'amitié,Richiesta di amicizia,友達リクエスト,친구 요청,Solicitar amizade,Solicitar amizade,Запрос дружбы,好友请求,好友邀請,好友请求 +PlayerDropDown.CancelRequest,,,Cancel Request,Anfrage abbrechen,Cancel Request,Cancelar solicitud,Cancelar solicitud,Annuler la demande,Annulla richiesta,リクエストをキャンセル,요청 취소,Cancelar pedido,Cancelar pedido,Отменить запрос,取消请求,取消邀請,取消请求 +PlayerDropDown.Accept,,,Accept,Annehmen,Accept,Aceptar,Aceptar,Accepter,Accetta,承認する,수락,Aceitar,Aceitar,Принять,接受,接受,接受 +InGame.PlayerList.Players,,,Players,Spieler,Players,Jugadores,Jugadores,Joueurs,Giocatori,プレイヤー,플레이어,Jogadores,Jogadores,Игроки,玩家,玩家,玩家 +InGame.Presence.Label.InGame,,,In Game,Im Spiel,In Game,En el juego,En el juego,En Jeu,Nel gioco,ゲーム内,게임 이용 중,No Jogo,No Jogo,В игре,正在游戏中,遊戲中,正在游戏中 +InGame.Presence.Label.InStudio,,,In Studio,In Studio,In Studio,En Studio,En Studio,En Studio,In Studio,Studio内,Studio 사용 중,No Studio,No Studio,В студии,正在 Studio 中,在 Studio 中,正在 Studio 中 +InGame.Presence.Label.Offline,,,Offline,Offline,Offline,Sin conexión,Sin conexión,Hors ligne,Offline,オフライン,오프라인,Off-line,Off-line,Не в сети,离线,離線,离线 +InGame.Presence.Label.Online,,,Online,Online,Online,En línea,En línea,En ligne,Online,オンライン,온라인,On-line,On-line,В сети,在线,在線,在线 +PurchasePromptScript.PURCHASE_FAILED.CANNOT_GET_BALANCE,,,Cannot retrieve your balance at this time. Your account has not been charged. Please try again later.,Dein Guthaben kann derzeit nicht abgerufen werden. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.,Cannot retrieve your balance at this time. Your account has not been charged. Please try again later.,En este momento no es posible acceder a tu saldo. No se ha llevado a cabo ningún cobro en tu cuenta. Inténtalo de nuevo más tarde.,En este momento no es posible acceder a tu saldo. No se ha llevado a cabo ningún cobro en tu cuenta. Inténtalo de nuevo más tarde.,Impossible d'obtenir votre solde pour l'instant. Votre compte n'a pas été débité. Veuillez réessayer plus tard.,Impossibile recuperare i tuoi fondi in questo momento. Il tuo account non ha subito addebiti. Riprova più tardi.,現在は残額を参照できません。アカウントがチャージされませんでした。しばらくしてからやり直してください。,현재 잔액을 불러올 수 없어요. 계정에 비용이 청구되지 않습니다. 나중에 다시 시도하세요.,Impossível obter seu saldo no momento. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Impossível obter seu saldo no momento. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Не удалось получить доступ к вашему балансу. Средства с вашего счета не списаны. Повторите попытку позже.,当前无法读取你的余额。你的帐户未被扣款。请稍候重试。,目前無法取得您的餘額。您的帳號餘額維持不變,請稍後再試。,当前无法读取你的余额。你的帐户未被扣款。请稍候重试。 +PurchasePromptScript.ERROR_MSG.PURCHASE_DISABLED,,,In-game purchases are temporarily disabled,Käufe im Spiel derzeit deaktiviert sind,In-game purchases are temporarily disabled,Las compras dentro de la aplicación están desactivadas temporalmente,Las compras dentro de la aplicación están desactivadas temporalmente,Les achats intégrés sont temporairement désactivés,Gli acquisti all'interno del gioco sono temporaneamente disattivati,ゲーム内購入は一時的に無効になっています。,일시적으로 게임 내 구매를 이용할 수 없어요,As compras no jogo estão temporariamente desabilitadas,As compras no jogo estão temporariamente desabilitadas,Внутриигровые покупки временно недоступны,游戏内购买功能暂时停用,遊戲中購買暫時停用,游戏内购买功能暂时停用 +PurchasePromptScript.ERROR_MSG.MAINTENANCE,,,ROBLOX is performing maintenance,ROBLOX Wartungsarbeiten durchführt,ROBLOX is performing maintenance,ROBLOX está efectuando tareas de mantenimiento,ROBLOX está efectuando tareas de mantenimiento,ROBLOX est en cours de maintenance,ROBLOX sta effettuando la manutenzione,ROBLOXはメンテナンス中です。,ROBLOX 유지보수 중입니다,ROBLOX está em manutenção,ROBLOX está em manutenção,Проводится техническое обслуживание ROBLOX,Roblox 正在维护中,ROBLOX 正在進行維修,Roblox 正在维护中 +PurchasePromptScript.setBuyMoreRobuxDialog.PostBalanceText,,,The remaining {RBX_NUMBER} ROBUX will be credited to your balance.,Die verbleibenden {RBX_NUMBER} ROBUX werden deinem Guthaben gutgeschrieben.,The remaining {RBX_NUMBER} ROBUX will be credited to your balance.,Los {RBX_NUMBER} ROBUX restantes se cargarán a tu saldo.,Los {RBX_NUMBER} ROBUX restantes se cargarán a tu saldo.,Les {RBX_NUMBER} ROBUX restants seront portés à votre solde.,I rimanenti {RBX_NUMBER} ROBUX verranno accreditati ai tuoi fondi.,残りの {RBX_NUMBER} ROBUX があなたのアカウントの残高に戻ります。,남은 {RBX_NUMBER} ROBUX는 회원님의 계정에 적립됩니다.,Os {RBX_NUMBER} ROBUX restantes serão somados ao seu saldo.,Os {RBX_NUMBER} ROBUX restantes serão somados ao seu saldo.,Оставшиеся {RBX_NUMBER} ROBUX будут перечислены на ваш счет.,剩余的 {RBX_NUMBER} ROBUX 将计入你的余额。,剩餘的 {RBX_NUMBER} ROBUX 將會加入您的餘額。,剩余的 {RBX_NUMBER} ROBUX 将计入你的余额。 +PurchasePromptScript.PURCHASE_FAILED.THIRD_PARTY_DISABLED,,,Third-party item sales have been disabled for this place. Your account has not been charged.,Gegenstände von Drittanbietern können an diesem Ort nicht verkauft werden. Dein Konto wurde nicht belastet.,Third-party item sales have been disabled for this place. Your account has not been charged.,Las ventas de objetos de terceros están desactivadas para este lugar. No se ha llevado a cabo ningún cobro en tu cuenta.,Las ventas de objetos de terceros están desactivadas para este lugar. No se ha llevado a cabo ningún cobro en tu cuenta.,Les ventes d'objets par des tiers ont été désactivées pour cet emplacement. Votre compte n'a pas été débité.,Le vendite di oggetti di terzi sono state disattivate per questa località. Il tuo account non ha subito addebiti.,サードパーティ製のアイテムの販売はここでは禁止されています。アカウントがチャージされませんでした。,본 장소에 대한 제삼자 아이템 판매가 비활성화되었습니다. 계정에 비용이 청구되지 않아요.,Vendas de itens de terceiros foram desabilitadas para este local. Nada foi cobrado da sua conta.,Vendas de itens de terceiros foram desabilitadas para este local. Nada foi cobrado da sua conta.,Продажа сторонних товаров в этом месте запрещена. Средства с вашего счета не списаны.,此地点的第三方物品拍卖已停用。你的帐户未被扣款。,此空間的第三方買賣目前停用,您的帳號餘額維持不變。,此地点的第三方物品拍卖已停用。你的帐户未被扣款。 +PurchasePromptScript.PURCHASE_FAILED.NOT_ENOUGH_TIX,,,This item cost more tickets than you currently have. Try trading currency on www.roblox.com to get more tickets.,"Dieser Gegenstand kostet mehr Tickets, als du derzeit besitzt. Auf www.roblox.com kannst du Währungen tauschen, um mehr Tickets zu erhalten.",This item cost more tickets than you currently have. Try trading currency on www.roblox.com to get more tickets.,Este objeto cuesta más tiques de los que tienes en este momento. Intenta convertir tipos de moneda en www.roblox.com para obtener más tiques.,Este objeto cuesta más tiques de los que tienes en este momento. Intenta convertir tipos de moneda en www.roblox.com para obtener más tiques.,Cet objet coûte plus de tickets que vous n'en avez. Essayez d'échanger des devises sur www.roblox.com pour obtenir plus de tickets.,Questo oggetto costa più ticket di quanti tu ne abbia. Prova a scambiare valuta sul sito www.roblox.com per ottenere più ticket.,このアイテムの入手には現在お持ちのチケットでは足りません。www.roblox.com でお金のトレーディングをしてチケットを手に入れよう。,본 아이템은 현재 소지한 티켓보다 비싸요. 더 많은 티켓을 받으려면 www.roblox.com에서 통화를 거래하세요.,Este item custa mais bilhetes do que você tem no momento. Experimente trocar moeda do jogo em www.roblox.com para obter mais.,Este item custa mais bilhetes do que você tem no momento. Experimente trocar moeda do jogo em www.roblox.com para obter mais.,"Цена товара превышает имеющееся у вас количество купонов. Обменяйте валюту на странице www.roblox.com, чтобы получить больше купонов.",此物品的价格超过你当前拥有的票单。在 www.roblox.com 上尝试交易货币以获取更多票单。,您的票券不足,無法購買此道具。請在 www.roblox.com 交易貨幣,取得更多票券。,此物品的价格超过你当前拥有的票单。在 www.roblox.com 上尝试交易货币以获取更多票单。 +PurchasePromptScript.PURCHASE_FAILED.NOT_FOR_SALE,,,This item is not currently for sale. Your account has not been charged.,Dieser Gegenstand steht derzeit nicht zum Verkauf. Dein Konto wurde nicht belastet.,This item is not currently for sale. Your account has not been charged.,Este objeto no está a la venta en este momento. No se ha llevado a cabo ningún cobro en tu cuenta.,Este objeto no está a la venta en este momento. No se ha llevado a cabo ningún cobro en tu cuenta.,Cet objet n'est pas en vente pour l'instant. Votre compte n'a pas été débité.,Questo oggetto non è attualmente in vendita. Il tuo account non ha subito addebiti.,このアイテムは現在売られていません。アカウントがチャージされませんでした。,판매 중인 아이템이 아닙니다. 계정에 비용이 청구되지 않아요.,Este item não está disponível para compra no momento. Nada foi cobrado da sua conta.,Este item não está disponível para compra no momento. Nada foi cobrado da sua conta.,Этот товар в настоящее время не продается. Средства с вашего счета не списаны.,此物品当前为非卖品。你的帐户未被扣款。,此道具目前為非賣品。您的帳號餘額維持不變。,此物品当前为非卖品。你的帐户未被扣款。 +PurchasePromptScript.setPurchaseDataInGui.invalidBC,,,This item requires {RBX_NAME1}. Click 'Upgrade' to upgrade your Builders Club!,"Dieser Gegenstand erfordert {RBX_NAME1}. Klicke auf „Aufwerten“, um deinen Builders-Club-Status zu verbessern!",This item requires {RBX_NAME1}. Click 'Upgrade' to upgrade your Builders Club!,"Este objeto requiere {RBX_NAME1}. Dale a ""Mejorar"" para mejorar el Builders Club.","Este objeto requiere {RBX_NAME1}. Dale a ""Mejorar"" para mejorar el Builders Club.",Cet objet nécessite {RBX_NAME1}. Cliquez sur « Améliorer » pour améliorer votre Builders Club !,"Questo oggetto richiede: {RBX_NAME1}. Clicca su ""Migliora"" per potenziare il tuo Builders Club!",このアイテムには{RBX_NAME1} が必要です。「アップグレード」をクリックしてBuilders Clubをアップグレード!,본 아이템은 {RBX_NAME1}이(가) 필요합니다. '업그레이드' 버튼을 클릭해 Builders Club을 업그레이드하세요!,Este item requer {RBX_NAME1}. Clique em 'Melhorar' para melhorar seu Builders Club!,Este item requer {RBX_NAME1}. Clique em 'Melhorar' para melhorar seu Builders Club!,"Для этого предмета требуется: {RBX_NAME1}. Нажмите кнопку «Улучшение», чтобы улучшить клуб создателей!",此物品需要 {RBX_NAME1}。点按“升级”以升级你的 Builders Club !,此道具需要 {RBX_NAME1}。按下「升級」來升級您的 Builders Club!,此物品需要 {RBX_NAME1}。点按“升级”以升级你的 Builders Club ! +PurchasePromptScript.PURCHASE_FAILED.LIMITED,,,This limited item has no more copies. Try buying from another user on www.roblox.com. Your account has not been charged.,Von diesem limitierten Gegenstand gibt es keine weiteren Exemplare. Such auf www.roblox.com nach einem anderen Verkäufer. Dein Konto wurde nicht belastet.,This limited item has no more copies. Try buying from another user on www.roblox.com. Your account has not been charged.,Este objeto limitado no tiene más copias. Intenta comprárselo a otro usuario en www.roblox.com. No se ha llevado a cabo ningún cobro en tu cuenta.,Este objeto limitado no tiene más copias. Intenta comprárselo a otro usuario en www.roblox.com. No se ha llevado a cabo ningún cobro en tu cuenta.,Il n'y a plus d'exemplaires de cet objet en série limitée. Essayez de l'acheter à un autre utilisateur sur www.roblox.com. Votre compte n'a pas été débité.,Questo oggetto limitato non è più disponibile. Prova ad acquistarlo da un altro utente sul sito www.roblox.com. Il tuo account non ha subito addebiti.,この限定アイテムはもう残っていません。www.roblox.com で他のユーザから購入してみてください。アカウントがチャージされませんでした。,본 한정 아이템은 더 이상 재고가 없어요. www.roblox.com에서 다른 사용자에게 구매해보세요. 계정에 비용이 청구되지 않아요.,Esgotaram-se as cópias deste item limitado. Tente comprar de outro usuário em www.roblox.com. Nada foi cobrado da sua conta.,Esgotaram-se as cópias deste item limitado. Tente comprar de outro usuário em www.roblox.com. Nada foi cobrado da sua conta.,Этого ограниченного товара больше нет в продаже. Попробуйте купить его у другого пользователя на сайте www.roblox.com. Средства с вашего счета не списаны.,此限量物品已售完。请尝试在 www.roblox.com 上从别的用户手中购买。你的帐户未被扣款。,此限量道具已售完,請前往 www.roblox.com 向其他使用者購買。您的帳號餘額維持不變。,此限量物品已售完。请尝试在 www.roblox.com 上从别的用户手中购买。你的帐户未被扣款。 +PurchasePromptScript.PURCHASE_MSG.PURCHASE,,,Want to buy the {RBX_NAME1} {RBX_NAME2} for,Möchtest du {RBX_NAME1} {RBX_NAME2} kaufen für:,Want to buy the {RBX_NAME1} {RBX_NAME2} for,¿Quieres comprar {RBX_NAME1} {RBX_NAME2} por,¿Quieres comprar {RBX_NAME1} {RBX_NAME2} por,Vous voulez acheter le {RBX_NAME1} {RBX_NAME2} pour,Vuoi comprare {RBX_NAME1} {RBX_NAME2} per,{RBX_NAME1} {RBX_NAME2} を以下の価格で購入希望:,다음 금액으로 {RBX_NAME1} {RBX_NAME2}을(를) 구매할까요,Deseja comprar {RBX_NAME1} {RBX_NAME2} por,Deseja comprar {RBX_NAME1} {RBX_NAME2} por,Купить {RBX_NAME1} {RBX_NAME2} за,你想要以如下价格购买“{RBX_NAME1} - {RBX_NAME2}”:,購買{RBX_NAME1} {RBX_NAME2}?價格為,你想要以如下价格购买“{RBX_NAME1} - {RBX_NAME2}”: +PurchasePromptScript.PURCHASE_FAILED.CANNOT_GET_ITEM_PRICE,,,We couldn't retrieve the price of the item at this time. Your account has not been charged. Please try again later.,Der Preis des Gegenstands kann derzeit nicht abgerufen werden. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.,We couldn't retrieve the price of the item at this time. Your account has not been charged. Please try again later.,En este momento no podemos acceder al precio de este objeto. No se ha llevado a cabo ningún cobro en tu cuenta. Inténtalo de nuevo más tarde.,En este momento no podemos acceder al precio de este objeto. No se ha llevado a cabo ningún cobro en tu cuenta. Inténtalo de nuevo más tarde.,Nous ne pouvons pas récupérer le prix de cet objet pour l'instant. Votre compte n'a pas été débité. Veuillez réessayer plus tard.,Impossibile recuperare il prezzo dell'oggetto in questo momento. Il tuo account non ha subito addebiti. Riprova più tardi.,現在はアイテムの価格が参照できません。アカウントがチャージされませんでした。しばらくしてからやり直してください。,일시적으로 아이템 가격을 불러올 수 없습니다. 계정에 비용이 청구되지 않아요. 나중에 다시 시도하세요.,Não conseguimos obter o preço do item no momento. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Não conseguimos obter o preço do item no momento. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Не удалось получить данные о цене товара. Средства с вашего счета не списаны. Повторите попытку позже.,当前无法读取物品的价格。你的帐户未被扣款。请稍候重试。,目前無法取得道具價格。您的帳號餘額維持不變,請稍後再試。,当前无法读取物品的价格。你的帐户未被扣款。请稍候重试。 +PurchasePromptScript.PURCHASE_MSG.FREE,,,Would you like to take {RBX_NAME2} for FREE?,Möchtest du „{RBX_NAME2}“ gerne GRATIS nehmen?,Would you like to take {RBX_NAME2} for FREE?,¿Quieres obtener {RBX_NAME2} GRATIS?,¿Quieres obtener {RBX_NAME2} GRATIS?,Souhaitez-vous prendre l'objet {RBX_NAME2} GRATUITEMENT ?,Vuoi prendere l'oggetto {RBX_NAME2} GRATIS?,アイテム名{RBX_NAME2} を無料で入手したいですか?,무료로 {RBX_NAME2}을(를) 받을까요?,Gostaria de obter {RBX_NAME2} GRÁTIS?,Gostaria de obter {RBX_NAME2} GRÁTIS?,Хотите получить товар {RBX_NAME2} БЕСПЛАТНО?,你想要免费拿到“{RBX_NAME2}”吗?,您要免費領取 {RBX_NAME2} 嗎?,你想要免费拿到“{RBX_NAME2}”吗? +PurchasePromptScript.PURCHASE_FAILED.PROMPT_PURCHASE_ON_GUEST,,,"You need to create a ROBLOX account to buy items, visit www.roblox.com for more info.","Du musst ein ROBLOX-Konto erstellen, um Gegenstände zu kaufen. Auf www.roblox.com findest du weitere Infos.","You need to create a ROBLOX account to buy items, visit www.roblox.com for more info.",Tienes que crear una cuenta de ROBLOX para comprar objetos. Visita www.roblox.com para obtener más información.,Tienes que crear una cuenta de ROBLOX para comprar objetos. Visita www.roblox.com para obtener más información.,"Vous devez créer un compte ROBLOX pour acheter des objets, rendez-vous sur www.roblox.com pour plus d'informations.","Per poter comprare oggetti, devi creare un account ROBLOX. Visita il sito www.roblox.com per maggiori informazioni.",アイテムを購入するにはROBLOX アカウントを作る必要があります。詳しくは www.roblox.com で。,아이템을 구매하려면 ROBLOX 계정을 만들어야 해요. 자세한 정보는 www.roblox.com을 방문하세요.,Você precisa criar uma conta ROBLOX para comprar itens. Visite www.roblox.com para mais informações.,Você precisa criar uma conta ROBLOX para comprar itens. Visite www.roblox.com para mais informações.,"Создайте учетную запись ROBLOX, чтобы покупать предметы. Посетите сайт www.roblox.com.",你需要创建 ROBLOX 帐户以购买物品,访问 www.roblox.com 以获取更多信息。,若要購買道具,請前往 www.roblox.com 建立 Roblox 帳號。,你需要创建 ROBLOX 帐户以购买物品,访问 www.roblox.com 以获取更多信息。 +PurchasePromptScript.setBuyMoreRobuxDialog.descriptionText,,,You need {RBX_NUMBER} more ROBUX to buy the {RBX_NAME1} {RBX_NAME2}. Would you like to buy more ROBUX?,"Du brauchst noch {RBX_NUMBER} ROBUX, um {RBX_NAME1} {RBX_NAME2} zu kaufen. Möchtest du mehr ROBUX kaufen?",You need {RBX_NUMBER} more ROBUX to buy the {RBX_NAME1} {RBX_NAME2}. Would you like to buy more ROBUX?,Necesitas {RBX_NUMBER} ROBUX más para comprar {RBX_NAME1} {RBX_NAME2}. ¿Quieres comprar más ROBUX?,Necesitas {RBX_NUMBER} ROBUX más para comprar {RBX_NAME1} {RBX_NAME2}. ¿Quieres comprar más ROBUX?,Vous avez besoin de {RBX_NUMBER} ROBUX de plus pour acheter le {RBX_NAME1} {RBX_NAME2}. Souhaitez-vous acheter plus de ROBUX ?,Hai bisogno di {RBX_NUMBER} ROBUX in più per comprare: {RBX_NAME1} {RBX_NAME2}. Vuoi acquistare altri ROBUX?,{RBX_NAME1} {RBX_NAME2}.を買うにはあと{RBX_NUMBER}のROBUXが必要です。もっとROBUXを買いますか?,{RBX_NUMBER} ROBUX가 부족하여 {RBX_NAME1} {RBX_NAME2}을(를) 구매할 수 없습니다. ROBUX를 구매하시겠습니까?,Você precisa de mais {RBX_NUMBER} ROBUX para comprar o(a) {RBX_NAME2} {RBX_NAME1}. Gostaria de comprar mais ROBUX?,Você precisa de mais {RBX_NUMBER} ROBUX para comprar o(a) {RBX_NAME2} {RBX_NAME1}. Gostaria de comprar mais ROBUX?,Для покупки товара {RBX_NAME1} {RBX_NAME2} требуется еще {RBX_NUMBER} ROBUX. Приобрести больше ROBUX?,你还需要 {RBX_NUMBER} ROBUX 才能购买 {RBX_NAME1} {RBX_NAME2}。你想要购买更多 ROBUX 吗?,您還需要 {RBX_NUMBER} Robux 才能購買{RBX_NAME1} {RBX_NAME2}。您要加購 Robux 嗎?,你还需要 {RBX_NUMBER} ROBUX 才能购买 {RBX_NAME1} {RBX_NAME2}。你想要购买更多 ROBUX 吗? +PurchasePromptScript.PURCHASE_FAILED.UNDER_13,,,Your account is under 13. Purchase of this item is not allowed. Your account has not been charged.,Dein Konto ist für Spieler unter 13 Jahren. Der Kauf dieses Gegenstands ist nicht gestattet. Dein Konto wurde nicht belastet.,Your account is under 13. Purchase of this item is not allowed. Your account has not been charged.,Tu cuenta es para menores de 13 años. La compra de este objeto no está permitida. No se ha llevado a cabo ningún cobro en tu cuenta.,Tu cuenta es para menores de 13 años. La compra de este objeto no está permitida. No se ha llevado a cabo ningún cobro en tu cuenta.,Votre compte est Moins de 13 ans. L'achat de cet objet n'est pas permis. Votre compte n'a pas été débité.,Il tuo account è per giocatori con meno di 13 anni. L'acquisto di questo oggetto non è permesso. Il tuo account non ha subito addebiti.,あなたのアカウントは13才以下です。このアイテムの購入は認められていません。アカウントがチャージされませんでした。,만 13세 미만의 계정으로 본 아이템을 구매할 수 없어요. 계정에 비용이 청구되지 않아요.,Sua conta é para menor de 13 anos. A compra deste item não é permitida. Nada foi cobrado da sua conta.,Sua conta é para menor de 13 anos. A compra deste item não é permitida. Nada foi cobrado da sua conta.,Возраст владельца учетной записи не превышает 13 лет. Вы не можете купить этот товар. Средства с вашего счета не списаны.,此帐户的用户未满 13 岁,不允许购买此物品。你的帐户未被扣款。,您的帳號為 13 歲以下,不允許購買此道具。您的帳號餘額維持不變。,此帐户的用户未满 13 岁,不允许购买此物品。你的帐户未被扣款。 +PurchasePromptScript.PURCHASE_MSG.BALANCE_FUTURE,,,Your balance after this transaction will be {RBX_NUMBER}.,Nach dieser Transaktion wird dein Guthaben {RBX_NUMBER} betragen.,Your balance after this transaction will be {RBX_NUMBER}.,Tu saldo después de esta transacción será de {RBX_NUMBER}.,Tu saldo después de esta transacción será de {RBX_NUMBER}.,Votre solde après cette transaction sera de {RBX_NUMBER}.,"Dopo la transazione, i tuoi fondi saranno {RBX_NUMBER}.",取引後の残高は{RBX_NUMBER}になります。,본 거래 후의 예상 잔액은 {RBX_NUMBER}입니다.,Seu saldo depois desta transação será de {RBX_NUMBER}.,Seu saldo depois desta transação será de {RBX_NUMBER}.,После этой операции ваш баланс составит {RBX_NUMBER},此次交易后,你的余额将为 {RBX_NUMBER}。,您在此交易後的餘額將為 {RBX_NUMBER}。,此次交易后,你的余额将为 {RBX_NUMBER}。 +PurchasePromptScript.PURCHASE_MSG.BALANCE_NOW,,,Your balance is now {RBX_NUMBER}.,Dein Guthaben beträgt nun {RBX_NUMBER}.,Your balance is now {RBX_NUMBER}.,Tu saldo es ahora de {RBX_NUMBER}.,Tu saldo es ahora de {RBX_NUMBER}.,Votre solde est désormais de {RBX_NUMBER}.,Ora i tuoi fondi sono {RBX_NUMBER}.,残高は現在 {RBX_NUMBER} です。,현재 잔액은 {RBX_NUMBER}입니다.,Seu saldo agora é {RBX_NUMBER}.,Seu saldo agora é {RBX_NUMBER}.,Сейчас ваш баланс составляет {RBX_NUMBER}.,你的当前余额为 {RBX_NUMBER}。,您目前的餘額為 {RBX_NUMBER}。,你的当前余额为 {RBX_NUMBER}。 +PurchasePromptScript.ERROR_MSG.FAILED_NO_ITEM_NAME,,,Your purchase failed because {ERROR_REASON}. Your account has not been charged. Please try again later.,Dein Kauf ist fehlgeschlagen. Grund: {ERROR_REASON}. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.,Your purchase failed because {ERROR_REASON}. Your account has not been charged. Please try again later.,Se ha producido un error con tu compra debido a {ERROR_REASON}. No se te ha cobrado. Inténtalo de nuevo más tarde. ,Se ha producido un error con tu compra debido a {ERROR_REASON}. No se te ha cobrado. Inténtalo de nuevo más tarde. ,Échec de la transaction. Motif : {ERROR_REASON}. Votre compte n'a pas été débité. Veuillez réessayer plus tard.,L'acquisto non è andato a buon fine a causa del seguente errore: {ERROR_REASON}. Il tuo account non ha subito addebiti. Riprova più tardi.,{ERROR_REASON}のため購入を完了できませんでした。アカウントへの請求は行われていません。後でもう一度お試し下さい。,{ERROR_REASON} 때문에 구매에 실패했어요. 계정에 비용이 청구되지 않았어요. 나중에 다시 시도하세요.,Sua compra falhou. Motivo: {ERROR_REASON}. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Sua compra falhou. Motivo: {ERROR_REASON}. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Произвести покупку не удалось: {ERROR_REASON}. Средства с вашей учетной записи не списаны. Повторите попытку позже.,由于{ERROR_REASON},你未能成功购买。你的帐户未被扣款。请稍候重试。,{ERROR_REASON},無法購買。您的帳號餘額維持不變,請稍後再試。,由于{ERROR_REASON},你未能成功购买。你的帐户未被扣款。请稍候重试。 +PurchasePromptScript.PURCHASE_MSG.FAILED,,,Your purchase of {RBX_NAME1} failed because {RBX_NAME2}. Your account has not been charged. Please try again later.,"Du konntest „{RBX_NAME1}“ nicht kaufen, da {RBX_NAME2}. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.",Your purchase of {RBX_NAME1} failed because {RBX_NAME2}. Your account has not been charged. Please try again later.,Tu compra de {RBX_NAME1} no ha funcionado porque {RBX_NAME2}. No se ha realizado ningún cobro. Inténtalo de nuevo más tarde.,Tu compra de {RBX_NAME1} no ha funcionado porque {RBX_NAME2}. No se ha realizado ningún cobro. Inténtalo de nuevo más tarde.,Votre achat de {RBX_NAME1} a échoué à cause de {RBX_NAME2}. Votre compte n'a pas été débité. Veuillez réessayer plus tard.,Il tuo acquisto di {RBX_NAME1} non è riuscito perché: {RBX_NAME2}. Il tuo account non ha subito addebiti. Riprova più tardi.,{RBX_NAME1} のため、 {RBX_NAME2} の購入に失敗しました。アカウントがチャージされませんでした。しばらくしてからやり直してください。,{RBX_NAME2} 때문에 {RBX_NAME1}에 실패했어요. 계정에 비용이 청구되지 않았어요. 나중에 다시 시도하세요.,Sua compra de {RBX_NAME1} fracassou. Motivo: {RBX_NAME2}. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Sua compra de {RBX_NAME1} fracassou. Motivo: {RBX_NAME2}. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Не удалось купить товар {RBX_NAME1}: {RBX_NAME2}. Средства с вашего счета не списаны. Повторите попытку позже.,由于“{RBX_NAME2}”,未能成功购买“{RBX_NAME1}”。你的帐户未被扣款。请稍候重试。,{RBX_NAME2},無法購買 {RBX_NAME1}。您的帳號餘額維持不變,請稍後再試。,由于“{RBX_NAME2}”,未能成功购买“{RBX_NAME1}”。你的帐户未被扣款。请稍候重试。 +PurchasePromptScript.PURCHASE_MSG.SUCCEEDED,,,Your purchase of {RBX_NAME1} succeeded!,Du hast „{RBX_NAME1}“ gekauft!,Your purchase of {RBX_NAME1} succeeded!,¡Tu compra de {RBX_NAME1} ha funcionado!,¡Tu compra de {RBX_NAME1} ha funcionado!,Votre achat de {RBX_NAME1} a réussi !,Il tuo acquisto di {RBX_NAME1} è andato a buon fine!,{RBX_NAME1} の購入に成功しました!,{RBX_NAME1} 구매를 완료했어요!,Sua compra de {RBX_NAME1} foi bem-sucedida!,Sua compra de {RBX_NAME1} foi bem-sucedida!,Вы купили товар {RBX_NAME1}!,购买“{RBX_NAME1}”成功!,成功購買 {RBX_NAME1}!,购买“{RBX_NAME1}”成功! +PurchasePromptScript.ERROR_MSG.UNKNOWN,,,something went wrong,ein Problem aufgetreten ist,something went wrong,algo ha ido mal,algo ha ido mal,quelque chose s'est mal passé,qualcosa è andato storto,エラーが起きました,오류가 발생했어요,algo deu errado,algo deu errado,возникли проблемы,有地方出错,發生錯誤,有地方出错 +PurchasePromptScript.ERROR_MSG.INVALID_FUNDS,,,your account does not have enough ROBUX,dein Konto nicht über genügend ROBUX verfügt,your account does not have enough ROBUX,tu cuenta no tiene suficientes ROBUX,tu cuenta no tiene suficientes ROBUX,votre compte ne possède pas assez de ROBUX,il tuo account non ha abbastanza ROBUX,アカウントのROBUXが足りません。,계정에 ROBUX가 부족합니다,sua conta não tem ROBUX suficientes,sua conta não tem ROBUX suficientes,у вас недостаточно ROBUX,你的帐户没有足够的 Robux,您的 Robux 不足,你的帐户没有足够的 Robux +StatsUtil.KBps,,,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} Ko/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} kB/s,{RBX_NUMBER} kB/s,{RBX_NUMBER} кБ/с,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s +StatsUtil.MB,,,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} Mo,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} МБ,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB +StatsUtil.ms,,,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} мс,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms +,,,/join or /j : join channel.,/join oder /j : Kanal beitreten,/join or /j : join channel.,/join o /j : unirse al canal.,/join o /j : unirse al canal.,/join ou /j  : rejoindre le canal.,/join o /j : accedi al canale.,/join <チャンネル> または /j <チャンネル> : で、チャンネルに参加。,/join <채널> 또는 /j <채널> : 채널 가입.,/join ou /j : juntar-se a um canal.,/join ou /j : juntar-se a um canal.,/join <канал> или /j <канал> : подключиться к каналу.,/join or /j : 加入频道。,/join <頻道名稱> or /j <頻道名稱>:加入頻道。,/join or /j : 加入频道。 +,,,/leave or /l : leave channel. (leaves current if none specified),/leave oder /l : Kanal verlassen. (Ohne Angabe des Kanals wird der aktuelle Kanal verlassen.),/leave or /l : leave channel. (leaves current if none specified),/leave o /l : salir del canal (sale del canal actual si no se especifica ninguno).,/leave o /l : salir del canal (sale del canal actual si no se especifica ninguno).,/leave ou /l  : quitter canal. (reste sur le même si aucun n'est spécifié),/leave o /l : lascia il canale (quello attuale se non specificato).,/leave <チャンネル> または /l <チャンネル> : でチャンネルを終了。 (指定しなければ現在のチャネルを終了),/leave <채널> 또는 /l <채널> : 채널 나가기. (미지정 시 현재 채널에서 나가기),/leave ou /l : sair do canal. (sai do atual se não especificado),/leave ou /l : sair do canal. (sai do atual se não especificado),"/leave <канал> или /l <канал> : покинуть канал. (Вы покинете текущий канал, если не укажете другой.)",/leave 或 /l : 离开频道。(若未指定,离开当前频道),/leave <頻道名稱> 或 /l <頻道名稱>:離開頻道。(若未指定頻道名稱,您將離開目前頻道),/leave 或 /l : 离开频道。(若未指定,离开当前频道) +,,,"1,2,3...","1,2,3...","1,2,3...","1,2,3...","1,2,3...","1, 2, 3...","1, 2, 3...",1、2、3...,"1,2,3...","1,2,3...","1,2,3...","1,2,3...","1,2,3...",1、2、3…,"1,2,3..." +,,,A Http error has occured. Please close the client and try again.,Ein HTTP-Fehler ist aufgetreten. Schließe den Client und versuche es erneut.,A Http error has occured. Please close the client and try again.,Error en el http. Cierra el cliente e inténtalo de nuevo.,Error en el http. Cierra el cliente e inténtalo de nuevo.,"Une erreur HTTP s'est produite. Veuillez fermer le client, puis réessayez.",Si è verificato un errore http. Chiudi il client e riprova.,Httpエラーが発生しました。クライアントを閉じて、もう一度試してください。,Http 오류가 발생했어요. 클라이언트를 닫고 다시 시도하세요.,Erro no http. Feche o cliente e tente novamente.,Erro no http. Feche o cliente e tente novamente.,"Произошла ошибка Http. Пожалуйста, закройте клиент и попробуйте снова.",发生 Http 错误。请关闭客户端并重试。,發生 HTTP 錯誤,請關閉客戶端並重新嘗試。,发生 Http 错误。请关闭客户端并重试。 +,,,A Http error has occured. Please close the client and try again.. ({COUNT:int}),Ein HTTP-Fehler ist aufgetreten. Schließe den Client und versuche es erneut.. ({COUNT:int}),A Http error has occured. Please close the client and try again.. ({COUNT:int}),Error en el http. Cierra el cliente e inténtalo de nuevo.. ({COUNT:int}),Error en el http. Cierra el cliente e inténtalo de nuevo.. ({COUNT:int}),"Une erreur HTTP s'est produite. Veuillez fermer le client, puis réessayez.. ({COUNT:int})",Si è verificato un errore http. Chiudi il client e riprova.. ({COUNT:int}),HTTPエラーが発生しました。クライアントを閉じて、もう一度試してください。 ({COUNT:int}),Http 오류가 발생했어요. 클라이언트를 닫고 다시 시도하세요. ({COUNT:int}),Erro no http. Feche o cliente e tente novamente. ({COUNT:int}),Erro no http. Feche o cliente e tente novamente. ({COUNT:int}),"Произошла ошибка Http. Пожалуйста, закройте клиент и попробуйте снова. ({COUNT:int})",发生 Http 错误。请关闭客户端并重试。 ({COUNT:int}),發生 HTTP 錯誤,請關閉客戶端並重新嘗試。({COUNT:int}),发生 Http 错误。请关闭客户端并重试。 ({COUNT:int}) +,,,A/Left Arrow,A/Linkspfeil,A/Left Arrow,A/Cursor izquierdo,A/Cursor izquierdo,A/Flèche gauche,A/Freccia SINISTRA,A/左カーソル,A/왼쪽 화살표,A/seta esquerda,A/seta esquerda,A/стрелка влево,A/左箭头,A、←,A/左箭头 +,,,Abuse Description,Beschreibung des Verstoßes,Abuse Description,Descripción del abuso,Descripción del abuso,Description de l'infraction,Descrizione abuso,規約違反の詳細,욕설 내용,Descrição do abuso,Descrição do abuso,Описание нарушения,滥用描述,濫用說明,滥用描述 +,,,Accept,Annehmen,Accept,Aceptar,Aceptar,Accepter,Accetta,同意する,수락,Aceitar,Aceitar,Принять,接受,接受,接受 +,,,Accept Friend Request,Freundschaftsanfrage akzeptieren,Accept Friend Request,Aceptar solicitud de amistad,Aceptar solicitud de amistad,Accepter l'invitation d'un ami,Accetta la richiesta di amicizia,友達リクエストを承認する,친구 요청 수락,Aceitar solicitação de amizade,Aceitar solicitação de amizade,Принять запрос друга,接受好友邀请,接受好友邀請,接受好友邀请 +,,,Accessories,Accessoires,Accessories,Accesorios,Accesorios,Accessoires,Accessori,アクセサリ,장신구,Acessórios,Acessórios,Аксессуары,配饰,飾品,配饰 +,,,Account: 13+,Konto: 13+,Account: 13+,Cuenta: 13+,Cuenta: 13+,Compte : 13+,Account: più di 13 anni,アカウント:13+,계정: 만 13세 이상,Conta: 13+,Conta: 13+,Учетная запись: 13+,帐户:13+,帳號:13+,帐户:13+ +,,,Account: <13,Konto: <13,Account: <13,Cuenta: <13,Cuenta: <13,Compte : <13,Account: <13,アカウント: <13,계정: 만 13세 미만,Conta: <13,Conta: <13,Учетная запись: <13,帐户:<13,帳號:<13,帐户:<13 +,,,Account: Over 13 yrs,Konto: Über 13 J.,Account: Over 13 yrs,Cuenta: Mayor de 13 años,Cuenta: Mayor de 13 años,Compte : Plus de 13 ans,Account: più di 13 anni,アカウント:13才以上,계정: 만 13세 이상,Conta: Mais de 13 anos,Conta: Mais de 13 anos,Учетная запись: cтарше 13 лет,帐户:超过 13 岁,帳號:13 歲以上,帐户:超过 13 岁 +,,,Account: Under 13 yrs,Konto: Unter 13 J.,Account: Under 13 yrs,Cuenta: Menor de 13 años,Cuenta: Menor de 13 años,Compte : Moins de 13 ans,Account: meno di 13 anni,アカウント:13才以下,계정: 만 13세 미만,Conta: Menor de 13 anos,Conta: Menor de 13 anos,Учетная запись: не старше 13 лет,帐户:13 岁以下,帳號:13 歲以下,帐户:13 岁以下 +,,,Add Friend,Freund hinzufügen,Add Friend,Añadir amigo,Añadir amigo,Ajouter ami,Aggiungi amico,友達を追加,친구 추가,Adicionar amigo,Adicionar amigo,Добавить друга,添加好友,新增好友,添加好友 +,,,Adjust,Anpassen,Adjust,Ajustar,Ajustar,Ajuster,Modifica,調節,조정,Ajustar,Ajustar,Настроить,调整,調整,调整 +,,,An error occurred while unblocking {RBX_NAME}. Please try again later.,Bei der Aufhebung der Sperre von {RBX_NAME} ist ein Fehler aufgetreten. Bitte versuche es später erneut.,An error occurred while unblocking {RBX_NAME}. Please try again later.,Se ha producido un error al desbloquear a {RBX_NAME}. Inténtalo de nuevo más tarde.,Se ha producido un error al desbloquear a {RBX_NAME}. Inténtalo de nuevo más tarde.,Une erreur est survenue lors de la levée du blocage de {RBX_NAME}. Veuillez réessayer plus tard.,Si è verificato un errore nello sblocco di {RBX_NAME}. Riprova più tardi.,{RBX_NAME} さんのブロック解除でエラーが発生しました。しばらくしてからやり直してください。,{RBX_NAME}님의 차단을 해제하는 중 오류가 발생했어요. 나중에 다시 시도하세요.,Um erro ocorreu ao desbloquear {RBX_NAME}. Tente de novo mais tarde.,Um erro ocorreu ao desbloquear {RBX_NAME}. Tente de novo mais tarde.,При разблокировке пользователя {RBX_NAME} произошла ошибка. Повторите попытку позже.,取消屏蔽“{RBX_NAME}”时出错。请稍候重试。,解除封鎖 {RBX_NAME} 時發生錯誤,請稍後再試。,取消屏蔽“{RBX_NAME}”时出错。请稍候重试。 +,,,An error occurred while unfriending {RBX_NAME}. Please try again later.,Bei der Entfernung von {RBX_NAME} als Freund ist ein Fehler aufgetreten. Bitte versuche es später erneut.,An error occurred while unfriending {RBX_NAME}. Please try again later.,Se ha producido un error al cancelar la amistad con {RBX_NAME}. Inténtalo de nuevo más tarde.,Se ha producido un error al cancelar la amistad con {RBX_NAME}. Inténtalo de nuevo más tarde.,Une erreur est survenue lors de la suppression de l'ami {RBX_NAME}. Veuillez réessayer plus tard.,Si è verificato un errore nel togliere l'amicizia a {RBX_NAME}. Riprova più tardi.,{RBX_NAME} さんの友達解除でエラーが発生しました。しばらくしてからやり直してください。,{RBX_NAME}님을 친구 취소하는 중 오류가 발생했어요. 나중에 다시 시도하세요.,Um erro ocorreu ao cancelar amizade com {RBX_NAME}. Tente de novo mais tarde.,Um erro ocorreu ao cancelar amizade com {RBX_NAME}. Tente de novo mais tarde.,При удалении пользователя {RBX_NAME} из друзей произошла ошибка. Повторите попытку позже.,与“{RBX_NAME}”解除好友关系时出错。请稍候重试。,與 {RBX_NAME} 解除好友關係時發生錯誤,請稍後再試。,与“{RBX_NAME}”解除好友关系时出错。请稍候重试。 +,,,Animation,Animation,Animation,Animación,Animación,Animation,Animazione,アニメーション,애니메이션,Animação,Animação,Анимация,动画,動畫,动画 +,,,Are you sure you want to reset your character?,Möchtest du deinen Charakter wirklich zurücksetzen?,Are you sure you want to reset your character?,¿Seguro que deseas reiniciar tu personaje?,¿Seguro que deseas reiniciar tu personaje?,Voulez-vous vraiment réinitialiser le personnage ?,Vuoi davvero azzerare il tuo personaggio?,キャラクターをリセットしてよろしいですか?,캐릭터를 재설정하시겠습니까?,Quer mesmo reiniciar o personagem?,Quer mesmo reiniciar o personagem?,Сбросить персонажа?,是否确定要重置人物?,確定重置人偶?,是否确定要重置人物? +,,,Arms,Arme,Arms,Brazos,Brazos,Bras,Braccia,腕,팔,Braços,Braços,hua ,手臂,手臂,手臂 +,,,Audio,Audio,Audio,Audio,Audio,Audio,Audio,オーディオ,오디오,Áudio,Áudio,Звук,音频,音訊,音频 +,,,Automatic,Automatisch,Automatic,Automático,Automático,Automatique,Automatico,自動,자동,Automático,Automático,Автоматический,自动,自動,自动 +,,,Avatar,Avatar,Avatar,Avatar,Avatar,Avatar,Avatar,アバター,아바타,Avatar,Avatar,Аватар,虚拟形象,虛擬人偶,虚拟形象 +,,,Back Accessory,Rückseiten-Accessoire,Back Accessory,Accesorio trasero,Accesorio trasero,Accessoire arrière,Accessorio posteriore,背面アクセサリ,등 장신구,Acessório de trás,Acessório de trás,Задний аксессуар,背面配饰,背面飾品,背面配饰 +,,,Backpack,Rucksack,Backpack,Mochila,Mochila,Sac à dos,Zaino,バックパック,배낭,Mochila,Mochila,Рюкзак,背包,背包,背包 +,,,Backspace,Rücktaste,Backspace,Retroceso,Retroceso,Retour,BACKSPACE,バックスペース,백스페이스,Backspace,Backspace,Backspace,回退,退格鍵,回退 +,,,Badge,Abzeichen,Badge,Emblema,Emblema,Badge,Contrassegno,バッジ,배지,Emblema,Emblema,Значок,徽章,徽章,徽章 +,,,Badge Awarded,Abzeichen verliehen,Badge Awarded,Emblema concedido,Emblema concedido,Badge accordé,Contrassegno conferito,授与されたバッジ,배지 획득,Emblema concedido,Emblema concedido,Получен значок,已获得徽章,獲得徽章,已获得徽章 +,,,Block Player,Spieler sperren,Block Player,Bloquear jugador,Bloquear jugador,Bloquer joueur,Blocca giocatore,プレイヤーをブロック,플레이어 차단,Bloquear jogador,Bloquear jogador,Заблокировать игрока,取消屏蔽玩家,封鎖玩家,取消屏蔽玩家 +,,,Builders Club,Builders Club,Builders Club,Builders Club,Builders Club,Builders Club,Builders Club,Builders Club,Builders Club,Builders Club,Builders Club,Клуб создателей,Builders Club,Builders Club,Builders Club +,,,Bullying,Mobbing,Bullying,Abusos,Abusos,Harcèlement,Bullismo,いじめ,괴롭힘,Bullying,Bullying,Агрессивное поведение,欺凌,霸凌,欺凌 +,,,Button,Schaltfläche,Button,Botón,Botón,Bouton,Pulsante,ボタン,버튼,Botão,Botão,Кнопка,按钮,按鈕,按钮 +,,,Buy,Kaufen,Buy,Comprar,Comprar,Acheter,Compra,買う,구매,Comprar,Comprar,Купить,买,購買,买 +,,,Buy Now,Jetzt kaufen,Buy Now,Comprar ahora,Comprar ahora,Acheter maintenant,Compra ora,今すぐ買う,지금 구매하기,Comprar agora,Comprar agora,Купить,立即购买,現在購買,立即购买 +,,,Buy R$,R$ kaufen,Buy R$,Comprar R$,Comprar R$,Acheter des R$,Compra R$,R$ を買う,R$ 구매,Comprar R$,Comprar R$,Купить R$,购买 R$,購買 R$,购买 R$ +,,,"By clicking the 'Record Video' button, the menu will close and start recording your screen.","Wenn du auf die Schaltfläche „Video aufnehmen“ klickst, schließt sich das Menü und dein Bildschirm wird aufgezeichnet.","By clicking the 'Record Video' button, the menu will close and start recording your screen.","Al hacer clic en el botón ""Grabar vídeo"", el menú se cerrará y se empezará a grabar tu pantalla.","Al hacer clic en el botón ""Grabar vídeo"", el menú se cerrará y se empezará a grabar tu pantalla.","En cliquant sur le bouton Enregistrer vidéo, le menu se fermera et l'enregistrement de votre écran débutera.","Clicca sul pulsante ""Registra video"" per chiudere il menu e iniziare a registrare quanto avviene sullo schermo.",「ビデオの録画」ボタンをクリックするとメニューが閉じて画面の録画が始まります。,"""비디오 녹화"" 버튼을 클릭하면 본 화면이 종료되고 녹화가 시작됩니다.","Ao clicar no botão ‘Gravar vídeo’, o menu será fechado e a tela começará a ser gravada.","Ao clicar no botão ‘Gravar vídeo’, o menu será fechado e a tela começará a ser gravada.","После нажатия на кнопку «Запись видео» это меню закроется, и начнется запись видео с экрана.",点按“录制视频”按钮,目录会关闭,并开始录制你的屏幕。,按下「錄影」按鈕後,選單會關閉並開始錄下您的畫面。,点按“录制视频”按钮,目录会关闭,并开始录制你的屏幕。 +,,,"By clicking the 'Take Screenshot' button, the menu will close and take a screenshot and save it to your computer.","Wenn du auf die Schaltfläche „Screenshot machen“ klickst, schließt sich das Menü und ein Screenshot wird auf deinem Computer gespeichert.","By clicking the 'Take Screenshot' button, the menu will close and take a screenshot and save it to your computer.","Al hacer clic en el botón ""Hacer captura de pantalla"", el menú se cerrará y se hará una captura de pantalla que se guardará en tu ordenador.","Al hacer clic en el botón ""Hacer captura de pantalla"", el menú se cerrará y se hará una captura de pantalla que se guardará en tu ordenador.","En cliquant sur le bouton Capture d'écran, le menu se fermera et une capture d'écran sera sauvegardée dans votre ordinateur.","Clicca sul pulsante ""Cattura screenshot"" per chiudere il menu, catturare uno screenshot e salvarlo sul computer.",「スクリーンショットを撮る」ボタンをクリックするとメニューが閉じてスクリーンショットがパソコンに保存されます。,'스크린숏 찍기' 버튼을 클릭하면 본 화면 종료 후 스크린숏을 찍어 컴퓨터에 저장합니다.,"Ao clicar no botão ‘Captura de tela’, o menu será fechado e a tela será capturada e salva no seu computador.","Ao clicar no botão ‘Captura de tela’, o menu será fechado e a tela será capturada e salva no seu computador.","После нажатия на кнопку «Сделать снимок экрана» это меню закроется, и снимок сохранится на вашем компьютере.",点按“截取屏幕快照”按钮,目录会关闭,截取屏幕快照并保存至你的电脑。,按下「截圖」按鈕,選單會關閉並截圖儲存到您的電腦。,点按“截取屏幕快照”按钮,目录会关闭,截取屏幕快照并保存至你的电脑。 +,,,CPU,CPU,CPU,CPU,CPU,UCT,CPU,CPU,CPU,CPU,CPU,Процессор,CPU,中央處理器,CPU +,,,Camera Inverted,Kamera invertiert,Camera Inverted,Cámara invertida,Cámara invertida,Caméra inversée,Macchina fotografica invertita,カメラの反転,카메라 반전,Câmera Invertida,Câmera Invertida,Инвертированная камера,镜头反转,相機反轉,镜头反转 +,,,Camera Mode,Kameramodus,Camera Mode,Modo de cámara,Modo de cámara,Mode caméra,Modalità visuale,カメラのモード,카메라 모드,Modo da câmera,Modo da câmera,Режим камеры,镜头模式,相機模式,镜头模式 +,,,Camera Movement,Kamerabewegung,Camera Movement,Movimiento de la cámara,Movimiento de la cámara,Déplacement caméra,Movimento visuale,カメラの動き,카메라 이동,Movimento da câmera,Movimento da câmera,Движение камеры,镜头移动模式,相機移動,镜头移动模式 +,,,Camera Sensitivity,Kameraempfindlichkeit,Camera Sensitivity,Sensibilidad de la cámara,Sensibilidad de la cámara,Sensibilité de la caméra,Precisione telecamera,カメラ感度,카메라 민감도,Sensitividade da câmera,Sensitividade da câmera,Чувствительность камеры,镜头敏感度,相機靈敏度,镜头敏感度 +,,,Camera Zoom,Kamerazoom,Camera Zoom,Distancia de la cámara,Distancia de la cámara,Zoom caméra,Zoom visuale,カメラズーム,카메라 줌,Zoom da câmera,Zoom da câmera,Масштаб камеры,镜头缩放,相機縮放,镜头缩放 +,,,Can't follow user: {ERROR_REASON:translate},Folgen von Benutzer nicht möglich: {ERROR_REASON:translate},Can't follow user: {ERROR_REASON:translate},No es posible seguir al usuario: {ERROR_REASON:translate},No es posible seguir al usuario: {ERROR_REASON:translate},Impossible de suivre l'utilisateur : {ERROR_REASON:translate},Impossibile seguire l'utente: {ERROR_REASON:translate},ユーザーをフォローできません:{ERROR_REASON:translate},사용자를 팔로우할 수 없음: {ERROR_REASON:translate},Não é possível seguir o usuário: {ERROR_REASON:translate},Não é possível seguir o usuário: {ERROR_REASON:translate},Нельзя следить за пользователем: {ERROR_REASON:translate},由于{ERROR_REASON:translate},无法关注用户:,無法追蹤使用者:{ERROR_REASON:translate},由于{ERROR_REASON:translate},无法关注用户: +,,,Can't follow user: {ERROR_REASON:translate}. ({COUNT:int}),Folgen von Benutzer nicht möglich: {ERROR_REASON:translate}. ({COUNT:int}),Can't follow user: {ERROR_REASON:translate}. ({COUNT:int}),No es posible seguir al usuario: {ERROR_REASON:translate}. ({COUNT:int}),No es posible seguir al usuario: {ERROR_REASON:translate}. ({COUNT:int}),Impossible de suivre l'utilisateur : {ERROR_REASON:translate}. ({COUNT:int}),Impossibile seguire l'utente: {ERROR_REASON:translate}. ({COUNT:int}),ユーザーをフォローできません:{ERROR_REASON:translate}. ({COUNT:int}),사용자를 팔로우할 수 없음: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível seguir o usuário: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível seguir o usuário: {ERROR_REASON:translate}. ({COUNT:int}),Нельзя следить за пользователем: {ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},无法关注用户:. ({COUNT:int}),無法追蹤使用者:{ERROR_REASON:translate}。({COUNT:int}),由于{ERROR_REASON:translate},无法关注用户:. ({COUNT:int}) +,,,Can't follow user: {ERROR_REASON},Folgen von Benutzer nicht möglich: {ERROR_REASON},Can't follow user: {ERROR_REASON},No es posible seguir al usuario: {ERROR_REASON},No es posible seguir al usuario: {ERROR_REASON},Impossible de suivre l'utilisateur : {ERROR_REASON},Impossibile seguire l'utente: {ERROR_REASON},ユーザーをフォローできません:{ERROR_REASON},사용자를 팔로우할 수 없음: {ERROR_REASON},Não é possível seguir o usuário: {ERROR_REASON},Não é possível seguir o usuário: {ERROR_REASON},Нельзя следить за пользователем: {ERROR_REASON},由于{ERROR_REASON},无法关注用户:,無法追蹤使用者:{ERROR_REASON},由于{ERROR_REASON},无法关注用户: +,,,Can't join place {PLACEID:int}: {ERROR_REASON:translate},Beitritt zu {PLACEID:int} nicht möglich: {ERROR_REASON:translate},Can't join place {PLACEID:int}: {ERROR_REASON:translate},No es posible unirse al lugar {PLACEID:int}: {ERROR_REASON:translate},No es posible unirse al lugar {PLACEID:int}: {ERROR_REASON:translate},Impossible de rejoindre l'emplacement {PLACEID:int} : {ERROR_REASON:translate},Impossibile raggiungere la località {PLACEID:int}: {ERROR_REASON:translate},{PLACEID:int}のプレースに参加できません:{ERROR_REASON:translate},장소 {PLACEID:int}에 참여할 수 없음: {ERROR_REASON:translate},Não é possível entrar em {PLACEID:int}: {ERROR_REASON:translate},Não é possível entrar em {PLACEID:int}: {ERROR_REASON:translate},Нельзя присоединиться ({PLACEID:int}): {ERROR_REASON:translate},由于{ERROR_REASON:translate},你无法加入{PLACEID:int}:,無法加入空間 {PLACEID:int}:{ERROR_REASON:translate},由于{ERROR_REASON:translate},你无法加入{PLACEID:int}: +,,,Can't join place {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),Beitritt zu {PLACEID:int} nicht möglich: {ERROR_REASON:translate}. ({COUNT:int}),Can't join place {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),No es posible unirse al lugar {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),No es posible unirse al lugar {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),Impossible de rejoindre l'emplacement {PLACEID:int} : {ERROR_REASON:translate}. ({COUNT:int}),Impossibile raggiungere la località {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),{PLACEID:int}のプレースに参加できません:{ERROR_REASON:translate}. ({COUNT:int}),장소 {PLACEID:int}에 참여할 수 없음: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível entrar em {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível entrar em {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),Нельзя присоединиться ({PLACEID:int}): {ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},你无法加入{PLACEID:int}:. ({COUNT:int}),無法加入空間 {PLACEID:int}:{ERROR_REASON:translate}。({COUNT:int}),由于{ERROR_REASON:translate},你无法加入{PLACEID:int}:. ({COUNT:int}) +,,,Can't join place {PLACEID}: {ERROR_REASON},Beitritt zu {PLACEID} nicht möglich: {ERROR_REASON},Can't join place {PLACEID}: {ERROR_REASON},No es posible unirse al lugar {PLACEID}: {ERROR_REASON},No es posible unirse al lugar {PLACEID}: {ERROR_REASON},Impossible de rejoindre l'emplacement {PLACEID} : {ERROR_REASON},Impossibile raggiungere la località {PLACEID}: {ERROR_REASON},{PLACEID} のプレースに参加できません:{ERROR_REASON},장소 {PLACEID}에 참여할 수 없음: {ERROR_REASON},Não é possível entrar em {PLACEID}: {ERROR_REASON},Não é possível entrar em {PLACEID}: {ERROR_REASON},Нельзя присоединиться ({PLACEID}): {ERROR_REASON},由于{ERROR_REASON},你无法加入{PLACEID}:,無法加入空間 {PLACEID}:{ERROR_REASON},由于{ERROR_REASON},你无法加入{PLACEID}: +,,,Canceling...,Abbrechen ...,Canceling...,Cancelando...,Cancelando...,Annulation...,Annullamento...,キャンセル中...,취소 중...,Cancelando...,Cancelando...,Отмена...,正在取消...,正在取消…,正在取消... +,,,Cannot find game server,Spielserver kann nicht gefunden werden,Cannot find game server,No se ha encontrado el servidor del juego,No se ha encontrado el servidor del juego,Serveur de jeu introuvable.,Impossibile trovare il server di gioco,ゲームサーバーが見つかりません,게임 서버를 찾을 수 없습니다,Não foi possível encontrar o servidor do jogo,Não foi possível encontrar o servidor do jogo,Не могу найти игровой сервер,无法找到游戏服务器,找不到遊戲伺服器,无法找到游戏服务器 +,,,Cannot find game server. ({COUNT:int}),Spielserver kann nicht gefunden werden. ({COUNT:int}),Cannot find game server. ({COUNT:int}),No se ha encontrado el servidor del juego. ({COUNT:int}),No se ha encontrado el servidor del juego. ({COUNT:int}),Serveur de jeu introuvable.. ({COUNT:int}),Impossibile trovare il server di gioco. ({COUNT:int}),ゲームサーバーが見つかりません ({COUNT:int}),게임 서버를 찾을 수 없습니다 ({COUNT:int}),Não foi possível encontrar o servidor do jogo ({COUNT:int}),Não foi possível encontrar o servidor do jogo ({COUNT:int}),Не могу найти игровой сервер ({COUNT:int}),无法找到游戏服务器 ({COUNT:int}),找不到遊戲伺服器。({COUNT:int}),无法找到游戏服务器 ({COUNT:int}) +,,,Cannot find game server. {RET:translate}...({COUNT:int}),Spielserver kann nicht gefunden werden. {RET:translate}...({COUNT:int}),Cannot find game server. {RET:translate}...({COUNT:int}),No se ha encontrado el servidor del juego. {RET:translate}...({COUNT:int}),No se ha encontrado el servidor del juego. {RET:translate}...({COUNT:int}),Serveur de jeu introuvable.. {RET:translate}...({COUNT:int}),Impossibile trovare il server di gioco. {RET:translate}...({COUNT:int}),ゲームサーバーが見つかりません。 {RET:translate}...({COUNT:int}),게임 서버를 찾을 수 없습니다. {RET:translate}...({COUNT:int}),Não foi possível encontrar o servidor do jogo. {RET:translate}...({COUNT:int}),Não foi possível encontrar o servidor do jogo. {RET:translate}...({COUNT:int}),Не могу найти игровой сервер. {RET:translate}...({COUNT:int}),无法找到游戏服务器. {RET:translate}...({COUNT:int}),找不到遊戲伺服器。{RET:translate}…({COUNT:int}),无法找到游戏服务器. {RET:translate}...({COUNT:int}) +,,,Cannot join game instance: {ERROR_REASON:translate},Beitritt zu Spielinstanz nicht möglich: {ERROR_REASON:translate},Cannot join game instance: {ERROR_REASON:translate},No es posible unirse a una instancia del juego: {ERROR_REASON:translate},No es posible unirse a una instancia del juego: {ERROR_REASON:translate},Impossible de rejoindre l'instance de jeu : {ERROR_REASON:translate},Impossibile accedere all'area di gioco: {ERROR_REASON:translate},ゲームインスタンスに参加できません:{ERROR_REASON:translate},게임 인스턴스에 참여할 수 없음: {ERROR_REASON:translate},Não é possível juntar-se ao jogo: {ERROR_REASON:translate},Não é possível juntar-se ao jogo: {ERROR_REASON:translate},Нельзя войти в локацию игры: {ERROR_REASON:translate},由于{ERROR_REASON:translate},无法加入游戏:,無法加入遊戲: {ERROR_REASON:translate},由于{ERROR_REASON:translate},无法加入游戏: +,,,Cannot join game instance: {ERROR_REASON:translate}. ({COUNT:int}),Beitritt zu Spielinstanz nicht möglich: {ERROR_REASON:translate}. ({COUNT:int}),Cannot join game instance: {ERROR_REASON:translate}. ({COUNT:int}),No es posible unirse a una instancia del juego: {ERROR_REASON:translate}. ({COUNT:int}),No es posible unirse a una instancia del juego: {ERROR_REASON:translate}. ({COUNT:int}),Impossible de rejoindre l'instance de jeu : {ERROR_REASON:translate}. ({COUNT:int}),Impossibile accedere all'area di gioco: {ERROR_REASON:translate}. ({COUNT:int}),ゲームインスタンスに参加できません:{ERROR_REASON:translate}. ({COUNT:int}),게임 인스턴스에 참여할 수 없음: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível juntar-se ao jogo: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível juntar-se ao jogo: {ERROR_REASON:translate}. ({COUNT:int}),Нельзя войти в локацию игры: {ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},无法加入游戏:. ({COUNT:int}),無法加入遊戲: {ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},无法加入游戏:. ({COUNT:int}) +,,,Cannot join game instance: {ERROR_REASON},Beitritt zu Spielinstanz nicht möglich: {ERROR_REASON},Cannot join game instance: {ERROR_REASON},No es posible unirse a una instancia del juego: {ERROR_REASON},No es posible unirse a una instancia del juego: {ERROR_REASON},Impossible de rejoindre l'instance de jeu : {ERROR_REASON},Impossibile accedere all'area di gioco: {ERROR_REASON},ゲームインスタンスに参加できません:{ERROR_REASON},게임 인스턴스에 참여할 수 없음: {ERROR_REASON},Não é possível juntar-se ao jogo: {ERROR_REASON},Não é possível juntar-se ao jogo: {ERROR_REASON},Нельзя войти в локацию игры: {ERROR_REASON},由于{ERROR_REASON},无法加入游戏:,無法加入遊戲: {ERROR_REASON},由于{ERROR_REASON},无法加入游戏: +,,,Cannot join private server: {ERROR_REASON:translate},Beitritt zu privatem Server nicht möglich: {ERROR_REASON:translate},Cannot join private server: {ERROR_REASON:translate},No es posible unirse al servidor privado: {ERROR_REASON:translate},No es posible unirse al servidor privado: {ERROR_REASON:translate},Impossible de rejoindre le serveur privé : {ERROR_REASON:translate},Impossibile accedere al server privato: {ERROR_REASON:translate},プライベートサーバーに参加できません:{ERROR_REASON:translate},비공개 서버에 참여할 수 없습니다: {ERROR_REASON:translate},Não é possível juntar-se ao servidor privado: {ERROR_REASON:translate},Não é possível juntar-se ao servidor privado: {ERROR_REASON:translate},Нельзя присоединиться к частному серверу: {ERROR_REASON:translate},由于{ERROR_REASON:translate},无法加入私人服务器:,無法加入私人伺服器:{ERROR_REASON:translate},由于{ERROR_REASON:translate},无法加入私人服务器: +,,,Cannot join private server: {ERROR_REASON:translate}. ({COUNT:int}),Beitritt zu privatem Server nicht möglich: {ERROR_REASON:translate}. ({COUNT:int}),Cannot join private server: {ERROR_REASON:translate}. ({COUNT:int}),No es posible unirse al servidor privado: {ERROR_REASON:translate}. ({COUNT:int}),No es posible unirse al servidor privado: {ERROR_REASON:translate}. ({COUNT:int}),Impossible de rejoindre le serveur privé : {ERROR_REASON:translate}. ({COUNT:int}),Impossibile accedere al server privato: {ERROR_REASON:translate}. ({COUNT:int}),プライベートサーバーに参加できません:{ERROR_REASON:translate}. ({COUNT:int}),비공개 서버에 참여할 수 없습니다: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível juntar-se ao servidor privado: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível juntar-se ao servidor privado: {ERROR_REASON:translate}. ({COUNT:int}),Нельзя присоединиться к частному серверу: {ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},无法加入私人服务器:. ({COUNT:int}),無法加入私人伺服器:{ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},无法加入私人服务器:. ({COUNT:int}) +,,,Cannot join private server: {ERROR_REASON},Beitritt zu privatem Server nicht möglich: {ERROR_REASON},Cannot join private server: {ERROR_REASON},No es posible unirse al servidor privado: {ERROR_REASON},No es posible unirse al servidor privado: {ERROR_REASON},Impossible de rejoindre le serveur privé : {ERROR_REASON},Impossibile accedere al server privato: {ERROR_REASON},プライベートサーバーに参加できません:{ERROR_REASON},비공개 서버에 참여할 수 없습니다: {ERROR_REASON},Não é possível juntar-se ao servidor privado: {ERROR_REASON},Não é possível juntar-se ao servidor privado: {ERROR_REASON},Нельзя присоединиться к частному серверу: {ERROR_REASON},由于{ERROR_REASON},无法加入私人服务器:,無法加入私人伺服器:{ERROR_REASON},由于{ERROR_REASON},无法加入私人服务器: +,,,Character Movement,Charakterbewegung,Character Movement,Movimiento del personaje,Movimiento del personaje,Déplacement personnage,Movimento personaggio,キャラクターの動き,캐릭터 이동,Movimento de personagem,Movimento de personagem,Движение персонажа,人物移动模式,人偶移動,人物移动模式 +,,,Chat,Chat,Chat,Chat,Chat,Chat,Chat,チャット,채팅,Chat,Chat,Чат,聊天,聊天,聊天 +,,,Chat ended because you didn't reply,"Chat wurde beendet, da du nicht geantwortet hast.",Chat ended because you didn't reply,El chat ha finalizado porque no has contestado.,El chat ha finalizado porque no has contestado.,Le chat a pris fin car vous n'avez pas répondu,"La chat terminata, non hai risposto",返事をしなかったのでチャットが終了しました,회원님이 대답을 하지 않아 채팅이 종료되었어요,O chat terminou porque você não respondeu,O chat terminou porque você não respondeu,Чат завершен: вы не ответили.,你没有回答,聊天结束,您沒有回覆,聊天已結束,你没有回答,聊天结束 +,,,Chat ended because you walked away,"Chat wurde beendet, da du dich entfernt hast.",Chat ended because you walked away,El chat ha finalizado porque te has alejado.,El chat ha finalizado porque te has alejado.,Le chat a pris fin car vous êtes parti,"La chat terminata, ti sei allontanato",立ち去ったのでチャットが終了しました,회원님이 채팅방을 나가 채팅이 종료되었어요,O chat terminou porque você se afastou,O chat terminou porque você se afastou,Чат завершен: вы ушли.,你已走开,聊天结束,您在範圍外,聊天已結束,你已走开,聊天结束 +,,,Cheating/Exploiting,Schummeln/Ausnutzen von Spielfehlern,Cheating/Exploiting,Trampas/abuso de errores,Trampas/abuso de errores,Triche/Exploitation,Inganno/Sfruttamento,チート/悪用,사기/악용,Trapaça/abuso,Trapaça/abuso,Жульничество,作弊/开挂,作弊 / 外掛,作弊/开挂 +,,,Check out your screenshots folder to see it.,Sieh ihn dir im Ordner mit den Screenshots an.,Check out your screenshots folder to see it.,Puedes verla en la carpeta de capturas de pantalla.,Puedes verla en la carpeta de capturas de pantalla.,Ouvrez le dossier des captures d'écran pour la visualiser.,Vai nella cartella screenshot per vederlo.,見るにはスクリーンショットフォルダをチェック。,스크린숏 폴더에서 확인하세요.,Confira sua pasta de capturas de tela para vê-la.,Confira sua pasta de capturas de tela para vê-la.,"Откройте папку со снимками экрана, чтобы его увидеть.",请检查你的屏幕快照文件夹以查看。,前往您的截圖資料夾查看。,请检查你的屏幕快照文件夹以查看。 +,,,Check out your videos folder to see it.,Sieh es dir im Ordner mit den Videos an.,Check out your videos folder to see it.,Puedes verlo en la carpeta de vídeos.,Puedes verlo en la carpeta de vídeos.,Ouvrez le dossier des vidéos pour la visualiser.,Vai nella cartella video per vederlo.,見るにはビデオフォルダをチェック。,비디오 폴더에서 확인하세요.,Confira sua pasta de vídeos para vê-lo.,Confira sua pasta de vídeos para vê-lo.,"Откройте папку с видеозаписями, чтобы его увидеть.",检查你的视频文件夹以查看。,前往您的影片資料夾查看。,检查你的视频文件夹以查看。 +,,,Choose One,Wähle eine der folgenden Optionen,Choose One,Elige uno,Elige uno,En choisir un(e),Scegli,ひとつ選ぶ,한 가지 선택,Escolha um,Escolha um,Выберите один вариант,选择一项,選擇,选择一项 +,,,Classic,Klassisch,Classic,Clásico,Clásico,Classique,Classica,クラシック,Classic,Clássico,Clássico,Классика,经典,經典,经典 +,,,Click to Move,Zum Bewegen klicken,Click to Move,Clic para moverse,Clic para moverse,Cliquer pour se déplacer,Clicca per muovere,クリックして移動,마우스 클릭으로 이동,Clique para mover,Clique para mover,Движение по нажатию,点按以移动,點擊移動,点按以移动 +,,,Climb Animation,Kletteranimation,Climb Animation,Animación de escalada,Animación de escalada,Animation d'escalade,Animazione scalata,登りアニメーション,오르기 애니메이션,Animação de escalada,Animação de escalada,Анимация подъема,攀爬动画,攀爬動畫,攀爬动画 +,,,Close Backpack,Rucksack schließen,Close Backpack,Cerrar mochila,Cerrar mochila,Fermer le sac à dos,Chiudi zaino,バックパックを閉じる,배낭 닫기,Fechar mochila,Fechar mochila,Закрыть рюкзак,关闭背包,關閉背包,关闭背包 +,,,Confirm Block,Sperre bestätigen,Confirm Block,Confirmar bloqueo,Confirmar bloqueo,Confirmer le blocage,Conferma blocco,ブロックを確認,차단 확인,Confirmar bloqueio,Confirmar bloqueio,Подтверждение блокировки,确认屏蔽,確認封鎖,确认屏蔽 +,,,Confirm Unblock,Aufhebung der Sperre bestätigen,Confirm Unblock,Confirmar desbloqueo,Confirmar desbloqueo,Confirmer le déblocage,Conferma sblocco,ブロック解除を確認,차단 해제 확인,Confirmar desbloqueio,Confirmar desbloqueio,Подтверждение разблокировки,确认取消屏蔽,確認解除封鎖,确认取消屏蔽 +,,,Connection attempt failed.,Verbindungsaufbau fehlgeschlagen.,Connection attempt failed.,Ha fallado el intento de conexión.,Ha fallado el intento de conexión.,Échec de la tentative de connexion.,Tentativo di connessione non riuscito.,接続に失敗しました。,연결 시도 실패.,Tentativa de conexão fracassada.,Tentativa de conexão fracassada.,Не удалось подключиться.,尝试连接失败。,連線失敗。,尝试连接失败。 +,,,Could not connect to game because game failed to start,"Verbindung zum Spiel konnte nicht hergestellt werden, da das Spiel nicht gestartet werden konnte.",Could not connect to game because game failed to start,No se ha podido conectar al juego porque no se ha podido iniciar.,No se ha podido conectar al juego porque no se ha podido iniciar.,Impossible de se connecter au jeu car il n'a pas réussi à être lancé,Impossibile connettersi al gioco perché l'avvio non è riuscito,ゲームの起動に失敗したため接続できませんでした。,게임 시작에 실패하여 접속할 수 없어요,Impossível conectar pois o jogo não conseguiu iniciar,Impossível conectar pois o jogo não conseguiu iniciar,"Не удалось подключиться к игре, так как она не запустилась.",开始此游戏失败,无法连接。,遊戲無法啟動,無法連線到遊戲,开始此游戏失败,无法连接。 +,,,Could not connect to game because game has ended,"Verbindung zum Spiel konnte nicht hergestellt werden, da das Spiel beendet wurde.",Could not connect to game because game has ended,No se ha podido conectar al juego porque ha terminado.,No se ha podido conectar al juego porque ha terminado.,Impossible de se connecter au jeu car il est terminé,Impossibile connettersi al gioco perché è terminato,ゲームが終了したため接続できませんでした。,게임이 종료되어 접속할 수 없어요,Impossível conectar pois o jogo já acabou,Impossível conectar pois o jogo já acabou,"Не удалось подключиться к игре, так как она завершилась.",此游戏已结束,无法连接。,遊戲已結束,無法連線到遊戲,此游戏已结束,无法连接。 +,,,Could not connect to game because game is disabled,"Verbindung zum Spiel konnte nicht hergestellt werden, da das Spiel deaktiviert ist.",Could not connect to game because game is disabled,No se ha podido conectar al juego porque está desactivado.,No se ha podido conectar al juego porque está desactivado.,Impossible de se connecter au jeu car il a été désactivé,Impossibile connettersi al gioco perché è disattivato,ゲームが無効なため接続できませんでした。,게임이 비활성화되어 접속할 수 없어요,Impossível conectar pois o jogo está desabilitado,Impossível conectar pois o jogo está desabilitado,"Не удалось подключиться к игре, так как она недоступна.",此游戏已停用,无法连接。,遊戲已停用,無法連線到遊戲,此游戏已停用,无法连接。 +,,,Could not connect to game because game is full,"Verbindung zum Spiel konnte nicht hergestellt werden, da das Spiel voll ist.",Could not connect to game because game is full,No se ha podido conectar al juego porque está lleno.,No se ha podido conectar al juego porque está lleno.,Impossible de se connecter au jeu car il est complet,Impossibile connettersi al gioco perché è al completo,ゲームが満員なため接続できませんでした。,게임이 가득 차서 접속할 수 없어요,Impossível conectar pois o jogo está cheio,Impossível conectar pois o jogo está cheio,"Не удалось подключиться к игре, так как она переполнена.",此游戏已满员,无法连接。,此遊戲已額滿,無法連線到遊戲,此游戏已满员,无法连接。 +,,,Could not connect to game because it is not available for your platform,"Verbindung zum Spiel konnte nicht hergestellt werden, da es für deine Plattform nicht verfügbar ist.",Could not connect to game because it is not available for your platform,No se ha podido conectar al juego porque no está disponible para tu plataforma.,No se ha podido conectar al juego porque no está disponible para tu plataforma.,Impossible de se connecter au jeu car il n'est pas disponible sur votre plateforme,Impossibile connettersi al gioco perché non è disponibile per la tua piattaforma,プラットフォームに対応していないためゲームに接続できませんでした。,플랫폼에서 이용할 수 없는 게임이므로 접속할 수 없어요,Impossível conectar pois o jogo não está disponível para a sua plataforma,Impossível conectar pois o jogo não está disponível para a sua plataforma,"Не удалось подключиться к игре, так как она недоступна на вашей платформе.",此游戏在你的平台上不可用,因此无法连接。,您的平台不支援此遊戲,無法連線到遊戲,此游戏在你的平台上不可用,因此无法连接。 +,,,Could not connect to game because user you were following has left the game,"Verbindung zum Spiel konnte nicht hergestellt werden, da der Benutzer, dem du gefolgt bist, das Spiel verlassen hat.",Could not connect to game because user you were following has left the game,No se ha podido conectar al juego porque el jugador al que seguías ha salido del mismo.,No se ha podido conectar al juego porque el jugador al que seguías ha salido del mismo.,Impossible de se connecter au jeu car l'utilisateur que vous suiviez l'a quitté,Impossibile connettersi al gioco perché l'utente che stavi seguendo è uscito,フォロー中のユーザーがゲームから退出したため接続できませんでした。,팔로우 중인 사용자가 게임에서 나가서 게임에 접속할 수 없어요,Impossível conectar pois o usuário que você estava seguindo saiu do jogo,Impossível conectar pois o usuário que você estava seguindo saiu do jogo,"Не удалось подключиться к игре, так как игрок, за которым вы последовали, ее покинул.",你关注的用户已离开游戏,因此无法连接。,您跟隨的使用者已離開遊戲,無法連線到遊戲,你关注的用户已离开游戏,因此无法连接。 +,,,Could not connect to game due to join script failure,"Verbindung zum Spiel konnte nicht hergestellt werden, da ein Problem mit dem Beitrittsskript aufgetreten ist.",Could not connect to game due to join script failure,No se ha podido conectar al juego a causa de un error en el script de unión.,No se ha podido conectar al juego a causa de un error en el script de unión.,Impossible de se connecter au jeu du fait d'un échec du script pour le rejoindre,Impossibile connettersi al gioco per un errore dello script di accesso,参加スクリプトの失敗により接続できませんでした。,참가 스크립트를 가져오지 못해 게임에 접속할 수 없어요,Impossível conectar devido a uma falha no script de entrada,Impossível conectar devido a uma falha no script de entrada,Не удалось подключиться к игре из-за ошибки в сценарии подключения.,由于加入脚本失败,无法连接至游戏,加入腳本發生錯誤,無法連線到遊戲,由于加入脚本失败,无法连接至游戏 +,,,"Could not connect to game, please try again later.",Verbindung zum Spiel konnte nicht hergestellt werden. Bitte versuche es später erneut.,"Could not connect to game, please try again later.",No se ha podido conectar al juego. Inténtalo de nuevo más tarde.,No se ha podido conectar al juego. Inténtalo de nuevo más tarde.,"Impossible de se connecter au jeu, veuillez réessayer plus tard.",Impossibile connettersi al gioco. Riprova più tardi.,ゲームに接続できませんでした。しばらくしてからやり直してください。,게임에 접속할 수 없어요. 나중에 다시 시도하세요.,Impossível conectar ao jogo. Tente de novo mais tarde.,Impossível conectar ao jogo. Tente de novo mais tarde.,Не удалось подключиться к игре. Повторите попытку позже.,无法连接至游戏,请稍候重试。,無法連線到遊戲,請稍後再試。,无法连接至游戏,请稍候重试。 +,,,Ctrl-Shift-C again to restore.,Zum Wiederherstellen erneut Strg-Umschalt-C drücken.,Ctrl-Shift-C again to restore.,Vuelve a pulsar Ctrl-Mayús-C para restablecerla.,Vuelve a pulsar Ctrl-Mayús-C para restablecerla.,Ctrl-Maj-C pour la restaurer.,Ancora CTRL-MAIUSC-C per ripristinare.,Ctrl-Shift-Cをもう一度押して復元。,복구하려면 Ctrl-Shift-C를 클릭하세요.,Ctrl-Shift-C de novo para restaurar.,Ctrl-Shift-C de novo para restaurar.,"Снова нажмите Ctrl-Shift-C, чтобы восстановить.",重按 Ctrl-Shift-C 以还原。,重按 Ctrl-Shift-C 復原。,重按 Ctrl-Shift-C 以还原。 +,,,Ctrl-Shift-G again to restore.,Zum Wiederherstellen erneut Strg-Umschalt-G drücken.,Ctrl-Shift-G again to restore.,Vuelve a pulsar Ctrl-Mayús-G para restablecerla.,Vuelve a pulsar Ctrl-Mayús-G para restablecerla.,Ctrl-Maj-G pour la restaurer.,Ancora CTRL-MAIUSC-G per ripristinare.,Ctrl-Shift-G をもう一度押して復元。,복원하려면 Ctrl-Shift-G를 클릭하세요.,Ctrl-Shift-G de novo para restaurar.,Ctrl-Shift-G de novo para restaurar.,"Снова нажмите Ctrl-Shift-G, чтобы восстановить.",重按 Ctrl-Shift-G 以还原。,重按 Ctrl-Shift-G 復原。,重按 Ctrl-Shift-G 以还原。 +,,,D/Right Arrow,D/Rechtspfeil,D/Right Arrow,D/Cursor derecho,D/Cursor derecho,D/Flèche droite,D/Freccia DESTRA,D/右カーソル,D/오른쪽 화살표,D/seta direita,D/seta direita,D/стрелка вправо,D/右箭头,D、→,D/右箭头 +,,,DPad,Steuerkreuz,DPad,Cruceta,Cruceta,Croix directionnelle,Croce direzionale,DPad,DPad,Direcional,Direcional,Крестовина,十字键,十字鍵,十字键 +,,,Dating,Dating,Dating,Solicitud de cita,Solicitud de cita,Drague,Appuntamento,デートの誘い,이성 교제,Namoro,Namoro,Флирт,约会,約會,约会 +,,,Decal,Decal,Decal,Adhesivo,Adhesivo,Décalcomanie,Decalcomania,デカール,데칼,Decalque,Decalque,Наклейка,贴纸,貼花,贴纸 +,,,Decline,Ablehnen,Decline,Rechazar,Rechazar,Décliner,Declina,拒否する,거절,Recusar,Recusar,Отклонить,拒绝,拒絕,拒绝 +,,,Decline Friend Request,Freundschaftsanfrage ablehnen,Decline Friend Request,Rechazar solicitud de amistad,Rechazar solicitud de amistad,Refuser la demande d'ami,Declinare la richiesta di un amico,友達リクエストを拒否する,친구 요청 거절,Rejeitar pedido de amizade,Rejeitar pedido de amizade,Отклонить запрос друга,拒绝好友邀请,拒絕好友邀請,拒绝好友邀请 +,,,Default (Classic),Standard (Klassisch),Default (Classic),Predeterminado (clásico),Predeterminado (clásico),Par défaut (Classique),Predefinita (Classica),デフォルト (クラシック),기본값 (Classic),Padrão (clássico),Padrão (clássico),По умолчанию (классика),默认(经典),預設(經典),默认(经典) +,,,Default (Follow),Standard (Folgen),Default (Follow),Predeterminado (seguir),Predeterminado (seguir),Par défaut (Suivre),Predefinita (Segui),デフォルト (フォロー),기본값 (팔로우),Padrão (seguir),Padrão (seguir),По умолчанию (следование),默认(追随),預設(追蹤),默认(追随) +,,,Default (Keyboard),Standard (Tastatur),Default (Keyboard),Predeterminado (teclado),Predeterminado (teclado),Par défaut (Clavier),Predefinita (Tastiera),デフォルト (キーボード),기본값 (키보드),Padrão (teclado),Padrão (teclado),По умолчанию (клавиатура),默认(键盘),預設(鍵盤),默认(键盘) +,,,Default (Thumbstick),Standard (Daumenstick),Default (Thumbstick),Predeterminado (stick),Predeterminado (stick),Par défaut (Joystick),Predefinita (Levetta),デフォルト (サムスティック),기본값 (엄지스틱),Padrão (thumbstick),Padrão (thumbstick),По умолчанию (аналоговый стик),默认(摇杆),預設(類比搖桿),默认(摇杆) +,,,Dev Console,Entwicklerkonsole,Dev Console,Consola de desarrollo,Consola de desarrollo,Console de dév.,Console svilup.,Dev コンソール,개발자 콘솔,Console de dev.,Console de dev.,Консоль разработчика,开发控制台,開發人員控制台,开发控制台 +,,,Developer Console,Entwicklerkonsole,Developer Console,Consola de desarrollo,Consola de desarrollo,Console de développement,Console sviluppatore,デベロッパーコンソール,개발자 콘솔,Console de desenvolvimento,Console de desenvolvimento,Консоль разработчика,开发人员控制台,開發人員控制台,开发人员控制台 +,,,"Developer has shut down all game servers or game server has shut down for other reasons, please reconnect",Der Entwickler hat alle Spielserver abgeschaltet oder die Verbindung zum Spielserver wurde aus anderen Gründen unterbrochen. Bitte erneut verbinden.,"Developer has shut down all game servers or game server has shut down for other reasons, please reconnect",El desarrollador ha cerrado todos los servidores del juego o el servidor del juego se ha cerrado por otras razones. Conéctate de nuevo.,El desarrollador ha cerrado todos los servidores del juego o el servidor del juego se ha cerrado por otras razones. Conéctate de nuevo.,"Le développeur a arrêté tous les serveurs du jeu, ou le serveur s'est arrêté pour d'autres raisons. Veuillez vous reconnecter.",Gli sviluppatori hanno chiuso tutti i server di gioco o il server si è chiuso per altri motivi. È necessario riconnettersi,開発者がすべてのゲームサーバーをシャットダウンしたか、他の理由でをシャットダウンしたようです。再接続してください,개발자가 모든 게임 서버를 종료했거나 혹은 기타 이유로 인해 게임 서버가 종료되었습니다. 다시 연결하세요.,O desenvolvedor encerrou todos os servidores do jogo ou o servidor do jogo foi encerrado por outros motivos. Conecte-se novamente.,O desenvolvedor encerrou todos os servidores do jogo ou o servidor do jogo foi encerrado por outros motivos. Conecte-se novamente.,"Разработчик отключил все сервера игры или они не работают по какой-либо другой причине. Пожалуйста, установите соединение повторно.",开发人员已经关闭所有游戏服务器,或由于其他原因服务器已关闭,请重新连接,開發人員控制台已關閉所有伺服器,或伺服器因其它原因關閉。請重新連線。,开发人员已经关闭所有游戏服务器,或由于其他原因服务器已关闭,请重新连接 +,,,Developer has shut down this game server for maintenance,Der Entwickler hat diesen Spielserver aufgrund von Wartungsarbeiten abgeschaltet,Developer has shut down this game server for maintenance,El desarrollador ha cerrado el servidor de este juego por mantenimiento,El desarrollador ha cerrado el servidor de este juego por mantenimiento,Le développeur a arrêté ce serveur de jeu pour effectuer la maintenance.,Lo sviluppatore ha chiuso questo server di gioco per manutenzione,メンテナンスのため、ディベロッパーによってゲームサーバーがシャットダウンされています。,유지보수를 위해 개발자가 본 게임 서버를 종료했습니다,O servidor deste jogo foi encerrado pelo desenvolvedor para manutenção,O servidor deste jogo foi encerrado pelo desenvolvedor para manutenção,Разработчик отключил игровой сервер для техобслуживания.,维护期间,开发人员已关闭此游戏服务器,開發人員已關閉此遊戲伺服器,维护期间,开发人员已关闭此游戏服务器 +,,,Disconnected due to Security Key Mismatch,Verbindung aufgrund einer Sicherheitsschlüssel-Diskrepanz unterbrochen,Disconnected due to Security Key Mismatch,Desconexión por incompatibilidad de la clave de seguridad ,Desconexión por incompatibilidad de la clave de seguridad ,"Vous avez été déconnecté(e), car la clé de sécurité ne correspond pas.",Disconnessione a causa di una mancata corrispondenza della chiave di sicurezza,セキュリティキーの不一致により接続が切断されました,보안키가 일치하지 않아 연결이 끊어졌습니다,Desconexão devido à incompatibilidade da Chave de Segurança,Desconexão devido à incompatibilidade da Chave de Segurança,Соединение прервано из-за несовпадения ключей безопасности.,由于安全密钥不匹配,连接中断,安全金鑰不符,連線中斷,由于安全密钥不匹配,连接中断 +,,,Disconnected due to a bad hash,Verbindung aufgrund eines fehlerhaften Hashs unterbrochen,Disconnected due to a bad hash,Desconexión por un error de hash,Desconexión por un error de hash,Vous avez été déconnecté(e) en raison d'un mauvais hachage.,Disconnessione a causa di un cattivo funzionamento dell'hash,ハッシュが正しくないため接続が切断されました,잘못된 해시로 인해 연결이 끊어졌습니다,Desconexão devido a um erro de hash,Desconexão devido a um erro de hash,Соединение прервано из-за ошибок хеширования.,由于错误的哈希码,连接中断,特徵碼錯誤,連線中斷,由于错误的哈希码,连接中断 +,,,"Disconnected due to timeout, please reconnect",Aufgrund einer Zeitüberschreitung unterbrochen. Bitte erneut verbinden.,"Disconnected due to timeout, please reconnect",Desconexión por agotamiento del tiempo de espera. Conéctate de nuevo.,Desconexión por agotamiento del tiempo de espera. Conéctate de nuevo.,"Vous avez été déconnecté(e), car votre session a expiré. Veuillez vous reconnecter.",Disconnessione a causa del time-out; è necessario riconnettersi,タイムアウトにより接続が失われました。再接続してください,시간 초과로 인해 연결이 끊어졌습니다. 다시 연결하세요.,Desconexão devido ao limite de tempo de espera ter sido atingido. Conecte-se novamente.,Desconexão devido ao limite de tempo de espera ter sido atingido. Conecte-se novamente.,"Разрыв соединения из-за превышения времени ожидания, пожалуйста, установите соединение повторно.",由于超时,连接已断开。请重新连接,連線逾時,請重新連線,由于超时,连接已断开。请重新连接 +,,,"Disconnected from game, please reconnect",Verbindung zum Spiel unterbrochen. Bitte erneut verbinden.,"Disconnected from game, please reconnect",Desconectado del juego. Conéctate de nuevo.,Desconectado del juego. Conéctate de nuevo.,Vous avez été déconnecté(e) du jeu. Veuillez vous reconnecter.,Disconnesso dal gioco; è necessario riconnettersi,ゲームへの接続が切断されました。再接続してください,게임 연결이 끊어졌습니다. 다시 연결하세요.,Você foi desconectado do jogo. Conecte-se novamente.,Você foi desconectado do jogo. Conecte-se novamente.,"Соединение и игрой прервано, пожалуйста, установите соединение повторно.",与游戏连接已断开,请重新连接,連線中斷,請重新連線,与游戏连接已断开,请重新连接 +,,,"Disconnected from game, please reconnect. {RET:translate}...({COUNT:int})",Verbindung zum Spiel unterbrochen. Bitte erneut verbinden.. {RET:translate}...({COUNT:int}),"Disconnected from game, please reconnect. {RET:translate}...({COUNT:int})",Desconectado del juego. Conéctate de nuevo.. {RET:translate}...({COUNT:int}),Desconectado del juego. Conéctate de nuevo.. {RET:translate}...({COUNT:int}),Vous avez été déconnecté(e) du jeu. Veuillez vous reconnecter.. {RET:translate}...({COUNT:int}),Disconnesso dal gioco; è necessario riconnettersi. {RET:translate}...({COUNT:int}),ゲームへの接続が切断されました。再接続してください. {RET:translate}...({COUNT:int}),게임 연결이 끊어졌습니다. 다시 연결하세요.. {RET:translate}...({COUNT:int}),Você foi desconectado do jogo. Conecte-se novamente.. {RET:translate}...({COUNT:int}),Você foi desconectado do jogo. Conecte-se novamente.. {RET:translate}...({COUNT:int}),"Соединение и игрой прервано, пожалуйста, установите соединение повторно.. {RET:translate}...({COUNT:int})",与游戏连接已断开,请重新连接. {RET:translate}...({COUNT:int}),連線中斷,請重新連線。{RET:translate}…({COUNT:int}),与游戏连接已断开,请重新连接. {RET:translate}...({COUNT:int}) +,,,"Disconnected from game, possibly due to game joined from another device",Verbindung zum Spiel wurde unterbochen. Möglicher Grund: Spielbeitritt von einem anderen Gerät.,"Disconnected from game, possibly due to game joined from another device","Desconectado del juego, posiblemente por conectarse al juego desde otro dispositivo.","Desconectado del juego, posiblemente por conectarse al juego desde otro dispositivo.",Vous avez été déconnecté(e) du jeu ; il se peut que vous ayez rejoint le jeu depuis un autre appareil.,Sei stato disconnesso dal gioco forse perché hai giocato una partita con un altro dispositivo,他のデバイスから参加したゲームのために、ゲームへの接続から切断された可能性があります,게임 연결이 끊어졌습니다. 다른 장치에서 게임에 참여한 것 같아요.,"Você foi desconectado do jogo, provavelmente porque entrou em um jogo através de outro dispositivo","Você foi desconectado do jogo, provavelmente porque entrou em um jogo através de outro dispositivo","Соединение с игрой прервано, возможно, из-за подключения к игре с другого устройства.",已断开与游戏的连接,可能是由于该游戏同时从另一个设备加入,連線中斷,您可能從其它裝置加入遊戲,已断开与游戏的连接,可能是由于该游戏同时从另一个设备加入 +,,,Dismiss,Verwerfen,Dismiss,Descartar,Descartar,Rejeter,Ignora,却下,취소,Dispensar,Dispensar,Отклонить,关闭,關閉,关闭 +,,,Do you allow game to create new place in your inventory?,"Erlaubst du dem Spiel, mehr Platz in deinem Inventar zu schaffen?",Do you allow game to create new place in your inventory?,¿Quieres permitir que el juego cree un espacio nuevo en tu inventario?,¿Quieres permitir que el juego cree un espacio nuevo en tu inventario?,Autorisez-vous le jeu à créer un nouvel emplacement dans votre inventaire ?,Vuoi permettere al gioco di creare nuovo spazio nel tuo inventario?,あなたのインベントリに新しい場所を作成しても良いですか?,게임 인벤토리에 새로운 장소를 만들까요?,Você permite que o jogo crie um novo espaço em seu inventário?,Você permite que o jogo crie um novo espaço em seu inventário?,Разрешить игре создать новое место в вашем инвентаре?,你是否允许游戏在你的道具中创建新的地点?,您要允許遊戲在您的道具欄創作新空間嗎?,你是否允许游戏在你的道具中创建新的地点? +,,,Don't Reset,Nicht zurücksetzen,Don't Reset,No reiniciar,No reiniciar,Ne pas réinitialiser,Non azzerare,リセットしない,재설정 취소,Não reiniciar,Não reiniciar,Не сбрасывать,不要重置,不要重置,不要重置 +,,,Drop Tool,Werkzeug ablegen,Drop Tool,Soltar herramienta,Soltar herramienta,Lâcher outil,Lascia strumento,ツールを手放す,도구 드롭,Largar,Largar,Выбросить,丢弃工具,捨棄工具,丢弃工具 +,,,ESC,ESC,ESC,ESC,ESC,ÉCHAP,ESC,ESC,ESC,ESC,ESC,ESC,ESC,ESC,ESC +,,,English text,English text,English text,English text,English text,Texte en français,Italian text,英文テキスト,한국어 텍스트,Portuguese,Portuguese,Текст на русском,English text,English text,English text +,,,Equip Tools,Werkzeuge ausrüsten,Equip Tools,Equipar herramientas,Equipar herramientas,Prendre outils,Equipaggia strum.,ツールをつける,도구 장착,Equipar,Equipar,Назначить,配备工具,裝上工具,配备工具 +,,,Equip/Unequip Tools,Werkzeug-Ausrüstung hinzufügen/abnehmen,Equip/Unequip Tools,Equipar/desequipar herramientas,Equipar/desequipar herramientas,Prendre/Lâcher outils,Equipaggia/Rimuovi,ツールをつける/外す,도구 장착/해제,Trocar ferramentas,Trocar ferramentas,Назначить/убрать предметы,配备/取消配备工具,裝上 / 卸下工具,配备/取消配备工具 +,,,Error Blocking Player,Fehler beim Sperren des Spielers,Error Blocking Player,Error al bloquear al jugador,Error al bloquear al jugador,Erreur en bloquant le joueur,Errore di blocco giocatore,プレイヤーのブロックエラー,플레이어 차단 오류,Erro ao bloquear jogador,Erro ao bloquear jogador,Ошибка блокировки игрока,屏蔽玩家时出错,封鎖玩家時發生錯誤,屏蔽玩家时出错 +,,,Error Unblocking Player,Fehler beim Aufheben der Sperre des Spielers,Error Unblocking Player,Error al desbloquear al jugador,Error al desbloquear al jugador,Erreur en levant le blocage du joueur,Errore di sblocco giocatore,プレーヤーブロック解除エラー,플레이어 차단 해제 오류,Erro ao desbloquear jogador,Erro ao desbloquear jogador,Ошибка разблокировки игрока,取消屏蔽玩家时出错,解除封鎖玩家時發生錯誤,取消屏蔽玩家时出错 +,,,"Error processing ticket, please reconnect",Fehler bei der Ticketbearbeitung. Bitte erneut verbinden.,"Error processing ticket, please reconnect",Error al procesar un tique. Conéctate de nuevo.,Error al procesar un tique. Conéctate de nuevo.,"Erreur lors du traitement du ticket, veuillez vous reconnecter.",Errore di elaborazione della richiesta; è necessario riconnettersi,チケットの処理中にエラーが発生しました。再接続してください,티켓 처리 중 오류가 발생했어요. 다시 연결하세요.,Erro ao processar o tíquete. Conecte-se novamente.,Erro ao processar o tíquete. Conecte-se novamente.,"Сообщение об ошибке процесса, пожалуйста, установите соединение повторно.",处理票单时出错,请重新连接,處理票券時發生錯誤,請重新連線。,处理票单时出错,请重新连接 +,,,"Error while receiving data, please reconnect",Fehler beim Datenempfang. Bitte erneut verbinden,"Error while receiving data, please reconnect",Error al recibir los datos. Conéctate de nuevo.,Error al recibir los datos. Conéctate de nuevo.,"Erreur lors de la réception des données, veuillez vous reconnecter.",Errore di ricezione dei dati; è necessario riconnettersi,データを受信中にエラーが発生しました。再接続してください,데이터 수신 중 오류가 발생했어요. 다시 연결하세요.,Erro ao receber os dados. Conecte-se novamente.,Erro ao receber os dados. Conecte-se novamente.,"Ошибка при получении данных, пожалуйста, установите соединение повторно.",接收数据时出错,请重新连接,接收資料時發生錯誤,請重新連線。,接收数据时出错,请重新连接 +,,,"Error while sending data, please reconnect",Fehler beim Senden der Daten. Bitte erneut verbinden.,"Error while sending data, please reconnect",Error al enviar los datos. Conéctate de nuevo.,Error al enviar los datos. Conéctate de nuevo.,"Erreur lors de l'envoi des données, veuillez vous reconnecter.",Errore durante l'invio dei dati; è necessario riconnettersi,データを送信中にエラーが発生しました。再接続してください,데이터 전송 중 오류가 발생했어요. 다시 연결하세요.,Erro ao enviar os dados. Conecte-se novamente.,Erro ao enviar os dados. Conecte-se novamente.,"Ошибка при отправке данных, пожалуйста, установите соединение повторно.",发送数据时出错,请重新连接,傳送資料時發生錯誤,請重新連線。,发送数据时出错,请重新连接 +,,,"Error while streaming data, please reconnect",Fehler bei der Datenübertragung. Bitte erneut verbinden.,"Error while streaming data, please reconnect",Error al transmitir los datos. Conéctate de nuevo.,Error al transmitir los datos. Conéctate de nuevo.,"Erreur lors de la diffusion des données, veuillez vous reconnecter.",Errore durante lo streaming dei dati; è necessario riconnettersi,データをストリーミング中にエラーが発生しました。再接続してください,데이터 스트리밍 중 오류가 발생했어요. 다시 연결하세요.,Erro ao transmitir os dados. Conecte-se novamente.,Erro ao transmitir os dados. Conecte-se novamente.,"Ошибка при передаче данных, пожалуйста, установите соединение повторно.",流式处理数据时出错,请重新连接,資料串流發生錯誤,請重新連線。,流式处理数据时出错,请重新连接 +,,,Face,Gesicht,Face,Cara,Cara,Visage,Faccia,顔,얼굴,Rosto,Rosto,Лицо,脸部,臉部,脸部 +,,,Face Accessory,Gesichts-Accessoire,Face Accessory,Accesorio para la cara,Accesorio para la cara,Accessoire de visage,Accessorio faccia,顔アクセサリ,얼굴 장신구,Acessório de rosto,Acessório de rosto,Аксессуар для лица,脸部配饰,臉部飾品,脸部配饰 +,,,Failed to connect to the Game. (ID = {RBX_NUMBER}: {RBX_NAME}),Verbindung zum Spiel fehlgeschlagen. (ID = {RBX_NUMBER}: {RBX_NAME}),Failed to connect to the Game. (ID = {RBX_NUMBER}: {RBX_NAME}),No se ha podido conectar con el juego. (ID = {RBX_NUMBER}: {RBX_NAME}),No se ha podido conectar con el juego. (ID = {RBX_NUMBER}: {RBX_NAME}),Échec de la connexion au jeu. (ID = {RBX_NUMBER} : {RBX_NAME}),Connessione al gioco non riuscita. (ID = {RBX_NUMBER}: {RBX_NAME}),ゲームに接続できませんでした。(ID = {RBX_NUMBER}: {RBX_NAME}),게임 연결에 실패했어요. (ID = {RBX_NUMBER}: {RBX_NAME}),Falha ao conectar no jogo. (ID = {RBX_NUMBER}: {RBX_NAME}),Falha ao conectar no jogo. (ID = {RBX_NUMBER}: {RBX_NAME}),Не удалось подключиться к игре. (ID = {RBX_NUMBER}: {RBX_NAME}),无法连接至游戏。(ID = {RBX_NUMBER}: {RBX_NAME}),無法連線到遊戲。(ID = {RBX_NUMBER}: {RBX_NAME}),无法连接至游戏。(ID = {RBX_NUMBER}: {RBX_NAME}) +,,,Fall Animation,Fallanimation,Fall Animation,Animación de caída,Animación de caída,Animation de chute,Animazione caduta,落下アニメーション,낙하 애니메이션,Animação de queda,Animação de queda,Анимация падения,下降动画,跌落動畫,下降动画 +,,,Follow,Folgen,Follow,Seguir,Seguir,Suivre,Segui,フォロー,팔로우,Seguir,Seguir,Слежение,跟随,追蹤,跟随 +,,,Follow Player,Spieler folgen,Follow Player,Seguir a jugador,Seguir a jugador,Suivre joueur,Segui giocatore,プレイヤーをフォロー,플레이어 팔로우,Seguir jogador,Seguir jogador,Подписаться на игрока,关注玩家,追蹤玩家,关注玩家 +,,,Followed user has left the game,"Der Benutzer, dem du folgst, hat das Spiel verlassen",Followed user has left the game,El usuario al que sigues ha abandonado el juego,El usuario al que sigues ha abandonado el juego,L'utilisateur que vous suiviez a quitté la partie.,L'utente seguito ha abbandonato il gioco,フォローしたユーザーがゲームを終了しました,팔로우 중인 사용자가 게임에서 나갔어요.,O usuário que você segue saiu do jogo,O usuário que você segue saiu do jogo,Отслеживаемый игрок вышел из игры,关注用户已离开游戏,您追蹤的使用者已離開遊戲,关注用户已离开游戏 +,,,Followed user has left the game. ({COUNT:int}),"Der Benutzer, dem du folgst, hat das Spiel verlassen. ({COUNT:int})",Followed user has left the game. ({COUNT:int}),El usuario al que sigues ha abandonado el juego. ({COUNT:int}),El usuario al que sigues ha abandonado el juego. ({COUNT:int}),L'utilisateur que vous suiviez a quitté la partie.. ({COUNT:int}),L'utente seguito ha abbandonato il gioco. ({COUNT:int}),フォローしたユーザーがゲームを終了しました。 ({COUNT:int}),팔로우 중인 사용자가 게임에서 나갔어요. ({COUNT:int}),O usuário que você segue saiu do jogo ({COUNT:int}),O usuário que você segue saiu do jogo ({COUNT:int}),Отслеживаемый игрок вышел из игры ({COUNT:int}),关注用户已离开游戏 ({COUNT:int}),您追蹤的使用者已離開遊戲。({COUNT:int}),关注用户已离开游戏 ({COUNT:int}) +,,,Followed user has left the game. {RET:translate}...({COUNT:int}),"Der Benutzer, dem du folgst, hat das Spiel verlassen. {RET:translate}...({COUNT:int})",Followed user has left the game. {RET:translate}...({COUNT:int}),El usuario al que sigues ha abandonado el juego. {RET:translate}...({COUNT:int}),El usuario al que sigues ha abandonado el juego. {RET:translate}...({COUNT:int}),L'utilisateur que vous suiviez a quitté la partie.. {RET:translate}...({COUNT:int}),L'utente seguito ha abbandonato il gioco. {RET:translate}...({COUNT:int}),フォローしたユーザーがゲームを終了しました。 {RET:translate}...({COUNT:int}),팔로우 중인 사용자가 게임에서 나갔어요.. {RET:translate}...({COUNT:int}),O usuário que você segue saiu do jogo. {RET:translate}...({COUNT:int}),O usuário que você segue saiu do jogo. {RET:translate}...({COUNT:int}),Отслеживаемый игрок вышел из игры. {RET:translate}...({COUNT:int}),关注用户已离开游戏. {RET:translate}...({COUNT:int}),您追蹤的使用者已離開遊戲。{RET:translate}…({COUNT:int}),关注用户已离开游戏. {RET:translate}...({COUNT:int}) +,,,Friend,Freund,Friend,Amigo,Amigo,Ami,Amico,友達,친구,Amigo,Amigo,Друг,好友,好友,好友 +,,,Friend Limit Reached,Max. Anzahl an Freunden erreicht,Friend Limit Reached,Has alcanzado el límite de amigos,Has alcanzado el límite de amigos,Limite d'amis atteinte,Limiti di amici raggiunto,友達の数が上限に達しました,최대 친구 수 도달,Limite de amigos alcançado,Limite de amigos alcançado,Достигнуто максимальное количество друзей,已达好友数量上限,已達好友上限,已达好友数量上限 +,,,From {RBX_NAME},Von {RBX_NAME},From {RBX_NAME},De {RBX_NAME},De {RBX_NAME},De {RBX_NAME},Da {RBX_NAME},送信者: {RBX_NAME},발신: {RBX_NAME},De {RBX_NAME},De {RBX_NAME},Отправитель: {RBX_NAME},自“{RBX_NAME}”,自 {RBX_NAME},自“{RBX_NAME}” +,,,Front Accessory,Vorderseiten-Accessoire,Front Accessory,Accesorio frontal,Accesorio frontal,Accessoire avant,Accessorio anteriore,前面アクセサリ,가슴 장신구,Acessório da frente,Acessório da frente,Передний аксессуар,正面配饰,正面飾品,正面配饰 +,,,Fullscreen,Vollbild,Fullscreen,Pantalla completa,Pantalla completa,Plein écran,Schermo intero,フルスクリーン,전체 화면,Tela cheia,Tela cheia,Полный экран,全屏,全螢幕,全屏 +,,,GPU,GPU,GPU,GPU,GPU,Processeur Graphique,GPU,GPU,GPU,GPU,GPU,Графический процессор,GPU,圖形處理器,GPU +,,,Game,Spiel,Game,Juego,Juego,Jeu,Gioco,ゲーム,게임,Jogo,Jogo,Игра,游戏,遊戲,游戏 +,,,Game Menu Toggle,Spielmenü umschalten,Game Menu Toggle,Alternar menú del juego,Alternar menú del juego,Menu du jeu (act./désact.),Attiva/Disattiva menu di gioco,ゲームメニュー切り換え,게임 메뉴 전환,Ativar menu do jogo,Ativar menu do jogo,Вызов игрового меню,游戏菜单切换,遊戲選單切換,游戏菜单切换 +,,,Game Pass,Spielpass,Game Pass,Pase de juego,Pase de juego,Passe de jeu,Pass di gioco,ゲームパス,게임패스,Passe de jogo,Passe de jogo,Игровой пропуск,游戏通行证,遊戲證,游戏通行证 +,,,"Game join request expired or invalid, please try again.",Anfrage zum Spielbeitritt abgelaufen oder ungültig. Bitte versuche es erneut.,"Game join request expired or invalid, please try again.",La solicitud para unirse al juego ha caducado o no es válida. Inténtalo de nuevo.,La solicitud para unirse al juego ha caducado o no es válida. Inténtalo de nuevo.,"La demande pour rejoindre le jeu a expirée ou est invalide, veuillez réessayer.",Richiesta di accesso al gioco scaduta o non valida. Riprova.,ゲーム参加リクエストが期限切れか無効です。やり直してください。,게임 가입 요청이 만료되었거나 유효하지 않습니다. 다시 시도하세요.,Pedido de entrada no jogo expirado ou inválido. Tente novamente.,Pedido de entrada no jogo expirado ou inválido. Tente novamente.,"Срок приглашения в игру истек, или оно является недействительным. Повторите попытку.",加入游戏的请求已过期或无效,请重试。,遊戲加入請求過期或無效,請重新嘗試。,加入游戏的请求已过期或无效,请重试。 +,,,Game or Player?,Spiel oder Spieler?,Game or Player?,¿Juego o jugador?,¿Juego o jugador?,Jeu ou joueur ?,Gioco o giocatore?,ゲームですか、プレイヤーですか?,게임 혹은 플레이어?,Jogo ou jogador?,Jogo ou jogador?,Игра или игрок?,游戏或玩家?,遊戲或玩家?,游戏或玩家? +,,,Gear,Ausrüstung,Gear,Equipamiento,Equipamiento,Équipement,Attrezzatura,ギア,기어,Equipamento,Equipamento,Снаряжение,装备,裝備,装备 +,,,Goodbye!,Tschüss!,Goodbye!,¡Hasta luego!,¡Hasta luego!,Au revoir !,Addio!,さようなら!,안녕히 가세요!,Tchau!,Tchau!,До свидания!,再见!,再見!,再见! +,,,Graphics Level,Grafikstufe,Graphics Level,Nivel de gráficos,Nivel de gráficos,Niveau graphismes,Livello grafica,グラフィックレベル,그래픽 수준,Nível dos gráficos,Nível dos gráficos,Уровень графики,图形级别,圖形層級,图形级别 +,,,Graphics Mode,Grafikmodus,Graphics Mode,Modo de gráficos,Modo de gráficos,Mode graphismes,Modalità grafica,グラフィックモード,그래픽 모드,Modo de gráficos,Modo de gráficos,Режим графики,图形模式,圖形模式,图形模式 +,,,Graphics Quality,Grafikqualität,Graphics Quality,Calidad gráfica,Calidad gráfica,Qualité graphismes,Qualità grafica,グラフィック品質,그래픽 품질,Qualidade dos gráficos,Qualidade dos gráficos,Качество графики,图形画质,圖形畫質,图形画质 +,,,Group Emblem,Gruppenemblem,Group Emblem,Emblema de grupo,Emblema de grupo,Emblème de groupe,Emblema gruppo,グループエンブレム,그룹 엠블렘,Emblema de grupo,Emblema de grupo,Эмблема группы,群组徽章,群組圖像,群组徽章 +,,,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML +,,,Hair Accessory,Haar-Accessoire,Hair Accessory,Accesorio para el pelo,Accesorio para el pelo,Accessoire de cheveux,Accessorio capelli,ヘアアクセサリ,헤어 장신구,Acessório de cabelo,Acessório de cabelo,Аксессуар для волос,头发配饰,髮型飾品,头发配饰 +,,,Hash Exception,Hash-Ausnahme,Hash Exception,Excepción del hash,Excepción del hash,Exception de hachage.,Eccezione hash,ハッシュが例外です,해시 예외,Exceção de hash,Exceção de hash,Исключение хеширования,哈希码例外,特徵碼例外,哈希码例外 +,,,Hash Exception. ({COUNT:int}),Hash-Ausnahme. ({COUNT:int}),Hash Exception. ({COUNT:int}),Excepción del hash. ({COUNT:int}),Excepción del hash. ({COUNT:int}),Exception de hachage.. ({COUNT:int}),Eccezione hash. ({COUNT:int}),ハッシュが例外です。 ({COUNT:int}),해시 예외 ({COUNT:int}),Exceção de hash ({COUNT:int}),Exceção de hash ({COUNT:int}),Исключение хеширования ({COUNT:int}),哈希码例外 ({COUNT:int}),特徵碼例外。({COUNT:int}),哈希码例外 ({COUNT:int}) +,,,Hash Expired,Hash abgelaufen,Hash Expired,El hash ha caducado,El hash ha caducado,Expiration du hachage.,Hash scaduto,ハッシュが期限切れです,해시 만료됨,Hash expirado,Hash expirado,Хеширование истекло,哈希码失效,特徵碼過期。,哈希码失效 +,,,Hash Expired. ({COUNT:int}),Hash abgelaufen. ({COUNT:int}),Hash Expired. ({COUNT:int}),El hash ha caducado. ({COUNT:int}),El hash ha caducado. ({COUNT:int}),Expiration du hachage.. ({COUNT:int}),Hash scaduto. ({COUNT:int}),ハッシュが期限切れです。 ({COUNT:int}),해시 만료됨 ({COUNT:int}),Hash expirado ({COUNT:int}),Hash expirado ({COUNT:int}),Хеширование истекло ({COUNT:int}),哈希码失效 ({COUNT:int}),特徵碼過期。({COUNT:int}),哈希码失效 ({COUNT:int}) +,,,Hash Expired. {RET:translate}...({COUNT:int}),Hash abgelaufen. {RET:translate}...({COUNT:int}),Hash Expired. {RET:translate}...({COUNT:int}),El hash ha caducado. {RET:translate}...({COUNT:int}),El hash ha caducado. {RET:translate}...({COUNT:int}),Expiration du hachage.. {RET:translate}...({COUNT:int}),Hash scaduto. {RET:translate}...({COUNT:int}),ハッシュが期限切れです。 {RET:translate}...({COUNT:int}),해시 만료됨. {RET:translate}...({COUNT:int}),Hash expirado. {RET:translate}...({COUNT:int}),Hash expirado. {RET:translate}...({COUNT:int}),Хеширование истекло. {RET:translate}...({COUNT:int}),哈希码失效. {RET:translate}...({COUNT:int}),特徵碼過期。{RET:translate}…({COUNT:int}),哈希码失效. {RET:translate}...({COUNT:int}) +,,,Hat,Hut,Hat,Sombrero,Sombrero,Chapeau,Cappello,帽子,모자,Chapéu,Chapéu,Головной убор,帽子,帽子,帽子 +,,,Head,Kopf,Head,Cabeza,Cabeza,Tête,Testa,頭,머리,Cabeça,Cabeça,Голова,头部,頭部,头部 +,,,Help,Hilfe,Help,Ayuda,Ayuda,Aide,Guida,ヘルプ,도움말,Ajuda,Ajuda,Справка,帮助,協助,帮助 +,,,Hiding Core GUI,Grafische Standard-Benutzeroberfläche verbergen,Hiding Core GUI,Ocultando la interfaz básica,Ocultando la interfaz básica,Cacher l'IGU principale,Nascondere interfaccia base,コア GUI を隠す。,코어 GUI 숨기기,Escondendo interface básica,Escondendo interface básica,Скрытие элементов основного интерфейса,隐藏核心 GUI,隱藏標準介面,隐藏核心 GUI +,,,Hiding Custom GUI,Individuelle grafische Benutzeroberfläche verbergen,Hiding Custom GUI,Ocultando la interfaz básica personalizada,Ocultando la interfaz básica personalizada,Cacher l'IGU personnalisée,Nascondere interfaccia personalizzata,カスタムGUIを隠す。,사용자 정의 GUI 숨기기,Escondendo interface personalizada,Escondendo interface personalizada,Скрытие элементов пользовательского интерфейса,隐藏自定义 GUI,隱藏自訂介面,隐藏自定义 GUI +,,,Idle Animation,Untätigkeitsanimation,Idle Animation,Animación de inactividad,Animación de inactividad,Animation oisif,Animazione inattività,待機アニメーション,대기 애니메이션,Animação de inatividade,Animação de inatividade,Анимация ожидания,闲置动画,閒置動畫,闲置动画 +,,,Image,Bild,Image,Imagen,Imagen,Image,Immagine,イメージ,이미지,Imagem,Imagem,Изображение,图像,圖像,图像 +,,,Inappropriate Content,Unangemessener Inhalt,Inappropriate Content,Contenido inadecuado,Contenido inadecuado,Contenu inapproprié,Contenuto non appropriato,不適切なコンテンツ,부적절한 콘텐츠,Conteúdo inapropriado,Conteúdo inapropriado,Неприличное содержимое,内容不当,內容不當,内容不当 +,,,Inappropriate Username,Unangemessener Benutzername,Inappropriate Username,Nombre de usuario inadecuado,Nombre de usuario inadecuado,Nom d'utilisateur inapproprié,Nome utente non appropriato,不適切なユーザーネーム,부적절한 사용자 이름,Nome de usuário inapropriado,Nome de usuário inapropriado,Неприличное имя пользователя,用户名不当,使用者名稱不當,用户名不当 +,,,Invalid JSON response received,Ungültige JSON-Antwort erhalten,Invalid JSON response received,Se ha recibido una respuesta JSON no válida,Se ha recibido una respuesta JSON no válida,Réponse JSON invalide reçue.,Ricevuta risposta JSON non valida,無効なJSON応答を受信しました,유효하지 않은 JSON 응답 수신됨,Resposta do JSON inválida,Resposta do JSON inválida,Получен некорректный ответ JSON,接收到无效的 JSON ,接收到無效的 JSON 回應,接收到无效的 JSON +,,,Invalid teleport destination,Ungültiger Teleport-Bestimmungsort,Invalid teleport destination,Destino del teleport no válido,Destino del teleport no válido,Destination de téléportation non valide.,Destinazione del teletrasporto non valida,テレポートの移動先が無効です,유효하지 않은 텔레포트 목적지,Destino de teleport inválido,Destino de teleport inválido,Некорректное назначение телепортирования.,传送目的地无效,傳送目的地無效,传送目的地无效 +,,,Joining server. ({COUNT:int}),Verbindung zum Server wird hergestellt. ({COUNT:int}),Joining server. ({COUNT:int}),Uniéndose a un servidor. ({COUNT:int}),Uniéndose a un servidor. ({COUNT:int}),Connexion au serveur.... ({COUNT:int}),Connessione al server. ({COUNT:int}),サーバーに接続中。 ({COUNT:int}),서버 입장 중. ({COUNT:int}),Entrando no servidor ({COUNT:int}),Entrando no servidor ({COUNT:int}),Подключение к серверу ({COUNT:int}),正在加入服务器 ({COUNT:int}),正在加入伺服器。({COUNT:int}),正在加入服务器 ({COUNT:int}) +,,,Joining server. {RET:translate}...({COUNT:int}),Verbindung zum Server wird hergestellt. {RET:translate}...({COUNT:int}),Joining server. {RET:translate}...({COUNT:int}),Uniéndose a un servidor. {RET:translate}...({COUNT:int}),Uniéndose a un servidor. {RET:translate}...({COUNT:int}),Connexion au serveur.... {RET:translate}...({COUNT:int}),Connessione al server. {RET:translate}...({COUNT:int}),サーバーに接続中。 {RET:translate}...({COUNT:int}),서버 입장 중. {RET:translate}...({COUNT:int}),Entrando no servidor. {RET:translate}...({COUNT:int}),Entrando no servidor. {RET:translate}...({COUNT:int}),Подключение к серверу. {RET:translate}...({COUNT:int}),正在加入服务器. {RET:translate}...({COUNT:int}),正在加入伺服器。{RET:translate}…({COUNT:int}),正在加入服务器. {RET:translate}...({COUNT:int}) +,,,Jump,Springen,Jump,Saltar,Saltar,Sauter,Salta,ジャンプ,점프,Pular,Pular,Прыжок,跳跃,跳躍,跳跃 +,,,Jump Animation,Springanimation,Jump Animation,Animación de salto,Animación de salto,Animation de saut,Animazione salto,ジャンプアニメーション,점프 애니메이션,Animação de salto,Animação de salto,Анимация прыжка,跳跃动画,跳躍動畫,跳跃动画 +,,,KOs,K.o.,KOs,N.º de KO,N.º de KO,Ko,KO,KO回数,KOs,Nocautes,Nocautes,Нокауты,击倒数,擊倒數,击倒数 +,,,Keyboard + Mouse,Tastatur + Maus,Keyboard + Mouse,Teclado + ratón,Teclado + ratón,Clavier + souris,Tastiera e mouse,キーボード + マウス,키보드 + 마우스,Teclado + mouse,Teclado + mouse,Клавиатура + мышь,键盘+鼠标,鍵盤+滑鼠,键盘+鼠标 +,,,Kicked by server. Please close and rejoin another game,Vom Sever rausgeworfen. Bitte schließen und einem anderen Spiel beitreten.,Kicked by server. Please close and rejoin another game,Expulsado del servidor. Cierra el juego y únete a otro.,Expulsado del servidor. Cierra el juego y únete a otro.,Le serveur vous a expulsé(e). Veuillez quitter le jeu et rejoindre une autre partie.,Espulso dal server. Chiudi e ricomincia una nuova partita,ゲームから外されました。閉じてから別のゲームに参加してください,서버에서 강퇴 퇴장되었습니다. 게임 종료 후 다른 게임에 참가하세요.,Você foi expulso pelo servidor. Feche a tela e entre em outro jogo.,Você foi expulso pelo servidor. Feche a tela e entre em outro jogo.,"Вы были исключены сервером. Пожалуйста, закройте игру и зайдите снова.",已被服务器踢出。请关闭并重新加入其他游戏,您被伺服器踢出,請關閉程式並加入新的遊戲。,已被服务器踢出。请关闭并重新加入其他游戏 +,,,Label,Label,Label,Etiqueta,Etiqueta,Étiquette,Etichetta,ラベル,라벨,Rótulo,Rótulo,Метка,标签,標籤,标签 +,,,Leave Game,Spiel verlassen,Leave Game,Salir del juego,Salir del juego,Quitter le jeu,Esci dal gioco,ゲームを終了,게임 종료,Sair do jogo,Sair do jogo,Выйти из игры,离开游戏,離開遊戲,离开游戏 +,,,Left Arm,Linker Arm,Left Arm,Brazo izquierdo,Brazo izquierdo,Bras gauche,Braccio sinistro,左腕,왼팔,Braço esquerdo,Braço esquerdo,Левая рука,左臂,左臂,左臂 +,,,Left Leg,Linkes Bein,Left Leg,Pierna izquierda,Pierna izquierda,Jambe gauche,Gamba sinistra,左脚,왼다리,Perna esquerda,Perna esquerda,Левая нога,左腿,左腿,左腿 +,,,Left Mouse Button,Linke Maustaste,Left Mouse Button,Botón izquierdo del ratón,Botón izquierdo del ratón,Bouton gauche de la souris,Sinistro/Mouse,左マウスボタン,마우스 왼쪽 버튼,Mouse (esquerdo),Mouse (esquerdo),ЛКМ,鼠标左键,左滑鼠鍵,鼠标左键 +,,,Legs,Beine,Legs,Piernas,Piernas,Jambes,Gambe,脚,다리,Pernas,Pernas,Ноги,腿部,腿部,腿部 +,,,Loading,Laden,Loading,Cargando,Cargando,En cours,Caricamento,読み込み中,불러오는 중,Carregando,Carregando,Загрузка,正在载入,正在載入,正在载入 +,,,Loading.,Laden.,Loading.,Cargando.,Cargando.,En cours.,Caricamento.,読み込み中。,불러오는 중.,Carregando.,Carregando.,Загрузка,正在载入。,正在載入.,正在载入。 +,,,Loading..,Laden ...,Loading..,Cargando..,Cargando..,En cours..,Caricamento..,読み込み中..,불러오는 중..,Carregando..,Carregando..,Загрузка,正在载入..,正在載入..,正在载入.. +,,,Loading...,Laden ...,Loading...,Cargando...,Cargando...,En cours...,Caricam...,読み込み中...,불러오는 중...,Carregando...,Carregando...,Загрузка,正在载入...,正在載入...,正在载入... +,,,Lost connection to server due to timeout,Die Verbindung zum Server wurde aufgrund einer Zeitüberschreitung unterbrochen.,Lost connection to server due to timeout,Se ha perdido la conexión con el servidor por agotamiento del tiempo de espera,Se ha perdido la conexión con el servidor por agotamiento del tiempo de espera,Connexion au serveur perdue ; la session a expiré.,Connessione persa con il server per un errore di time-out,タイムアウトでサーバーへの接続が失われました,시간 초과로 인해 연결 해제됨,A conexão com o servidor foi perdida pois o limite de tempo de espera foi atingido,A conexão com o servidor foi perdida pois o limite de tempo de espera foi atingido,Разрыв соединения с сервером из-за превышения времени ожидания.,由于超时,与服务器的连接已断开,連線逾時,與伺服器連線中斷,由于超时,与服务器的连接已断开 +,,,Lua,Lua,Lua,Lua,Lua,Lua,Lua,Lua,Lua,Lua,Lua,Lua,Lua ,Lua,Lua +,,,Manual,Manuell,Manual,Manual,Manual,Manuel,Manuale,マニュアル,수동,Manual,Manual,Ручной,手动,手動,手动 +,,,Mem,Speicher,Mem,Mem.,Mem.,Mém,Mem,メモリー,메모리,Mem.,Mem.,Память,内存,記憶體,内存 +,,,Memory,Speicher,Memory,Memoria,Memoria,Mémoire,Memoria,メモリー,메모리,Memória,Memória,Память,内存,記憶體,内存 +,,,Menu Items,Menüobjekte,Menu Items,Objetos del menú,Objetos del menú,Objets du menu,Oggetti menu,メニューアイテム,메뉴 아이템,Itens de menu,Itens de menu,Пункты меню,菜单项目,選單項目,菜单项目 +,,,Menu Navigation,Menünavigation,Menu Navigation,Navegación del menú,Navegación del menú,Navigation du menu,Menu di navigazione,メニューナビゲーション,메뉴 내비게이션,Navegação do menu,Navegação do menu,Навигация в меню,菜单导航,選單導覽,菜单导航 +,,,Mesh,Mesh,Mesh,Mesh,Mesh,Maillage,Mesh,メッシュ,메시,Malha,Malha,Сетка,网格,模組,网格 +,,,MeshPart,MeshPart,MeshPart,MeshPart,MeshPart,MeshPart,Parte mesh,MeshPart,MeshPart,SolidModel,SolidModel,Полигональная часть,MeshPart,MeshPart,MeshPart +,,,Misc,Verschiedenes,Misc,Misc.,Misc.,Divers,Vari,その他,기타,Diversos,Diversos,Разное,杂项,其它,杂项 +,,,Model,Modell,Model,Modelo,Modelo,Modèle,Modello,モデル,모델,Modelo,Modelo,Модель,模型,模型,模型 +,,,Mouse Sensitivity,Mausempfindlichkeit,Mouse Sensitivity,Sensibilidad del ratón,Sensibilidad del ratón,Sensibilité de la souris,Precisione mouse,マウス感度,마우스 감도,Sensibilidade do mouse,Sensibilidade do mouse,Чувствительность мыши,鼠标灵敏度,滑鼠靈敏度,鼠标灵敏度 +,,,Mouse Wheel,Mausrad,Mouse Wheel,Rueda del ratón,Rueda del ratón,Molette de la souris,Rotellina mouse,マウスホイール,마우스 휠,Roda do mouse,Roda do mouse,Колесо мыши,鼠标滚轮,滑鼠滾輪,鼠标滚轮 +,,,Mouselock,Maussperre,Mouselock,Bloqueo del ratón,Bloqueo del ratón,Verrouillage souris,Blocco mouse,マウスロック,마우스 잠금,Travar mouse,Travar mouse,Фиксация мыши,鼠标锁定,滑鼠鎖定,鼠标锁定 +,,,Move,Bewegen,Move,Mover,Mover,Se déplacer,Muovi,移動,이동,Mover,Mover,Передвижение,移动,移動,移动 +,,,Move Backward,Rückwärts bewegen,Move Backward,Moverse hacia atrás,Moverse hacia atrás,Reculer,Muovi indietro,後ろに移動,뒤로 이동,Mover (trás),Mover (trás),Назад,后退,向後,后退 +,,,Move Forward,Vorwärts bewegen,Move Forward,Moverse hacia delante,Moverse hacia delante,Avancer,Muovi in avanti,前に移動,앞으로 이동,Mover (frente),Mover (frente),Вперед,向前,向前,向前 +,,,Move Left,Nach links bewegen,Move Left,Moverse a la izquierda,Moverse a la izquierda,Aller à gauche,Muovi a sinistra,左に移動,왼쪽으로 이동,Mover (esquerda),Mover (esquerda),Влево,左移,向左,左移 +,,,Move Right,Nach rechts bewegen,Move Right,Moverse a la derecha,Moverse a la derecha,Aller à droite,Muovi a destra,右に移動,오른쪽으로 이동,Mover (direita),Mover (direita),Вправо,右移,向右,右移 +,,,Movement Mode,Bewegungsmodus,Movement Mode,Modo de movimiento,Modo de movimiento,Mode déplacement,Modalità movimento,動作モード,이동 모드,Modo de movimento,Modo de movimento,Режим передвижения,移动模式,移動模式,移动模式 +,,,Neck Accessory,Hals-Accessoire,Neck Accessory,Accesorio para el cuello,Accesorio para el cuello,Accessoire de cou,Accessorio collo,首アクセサリ,목 장신구,Acessório de pescoço,Acessório de pescoço,Аксессуар для шеи,颈部配饰,頸部飾品,颈部配饰 +,,,Network error {RBX_NUMBER},Netzwerkfehler {RBX_NUMBER},Network error {RBX_NUMBER},Error de red {RBX_NUMBER},Error de red {RBX_NUMBER},Erreur réseau {RBX_NUMBER},Errore di rete {RBX_NUMBER},ネットワークエラー {RBX_NUMBER},네트워크 오류 {RBX_NUMBER},Erro de rede {RBX_NUMBER},Erro de rede {RBX_NUMBER},Ошибка сети {RBX_NUMBER},网络错误 {RBX_NUMBER},網路錯誤 {RBX_NUMBER},网络错误 {RBX_NUMBER} +,,,New Follower,Neuer Follower,New Follower,Seguidor nuevo,Seguidor nuevo,Nouvel abonné,Nuovo follower,新規フォロワー,새 팔로워,Novo seguidor,Novo seguidor,Новый подписчик,新粉丝,新追蹤者,新粉丝 +,,,New Friend,Neuer Freund,New Friend,Amigo nuevo,Amigo nuevo,Nouvel ami,Nuovo amico,新しい友達,새 친구,Novo amigo,Novo amigo,Новый друг,新好友,新好友,新好友 +,,,Not authorized to join this game,"Du bist nicht befugt, diesem Spiel beizutreten",Not authorized to join this game,No tienes autorización para unirte a este juego,No tienes autorización para unirte a este juego,Vous n'avez pas l'autorisation de rejoindre ce jeu.,Non hai l'autorizzazione per partecipare a questa partita,このゲームに参加する権限がありません,게임 참여 권한 없음,Você não tem autorização para entrar neste jogo,Você não tem autorização para entrar neste jogo,Не разрешено присоединяться к этой игре,没有加入此游戏的权限,權限不足,無法加入此遊戲。,没有加入此游戏的权限 +,,,Not authorized to join this game. ({COUNT:int}),"Du bist nicht befugt, diesem Spiel beizutreten. ({COUNT:int})",Not authorized to join this game. ({COUNT:int}),No tienes autorización para unirte a este juego. ({COUNT:int}),No tienes autorización para unirte a este juego. ({COUNT:int}),Vous n'avez pas l'autorisation de rejoindre ce jeu.. ({COUNT:int}),Non hai l'autorizzazione per partecipare a questa partita. ({COUNT:int}),このゲームに参加する権限がありません。 ({COUNT:int}),게임 참여 권한 없음 ({COUNT:int}),Você não tem autorização para entrar neste jogo ({COUNT:int}),Você não tem autorização para entrar neste jogo ({COUNT:int}),Не разрешено присоединяться к этой игре ({COUNT:int}),没有加入此游戏的权限 ({COUNT:int}),權限不足,無法加入此遊戲。({COUNT:int}),没有加入此游戏的权限 ({COUNT:int}) +,,,Not authorized to join this game. {RET:translate}...({COUNT:int}),"Du bist nicht befugt, diesem Spiel beizutreten. {RET:translate}...({COUNT:int})",Not authorized to join this game. {RET:translate}...({COUNT:int}),No tienes autorización para unirte a este juego. {RET:translate}...({COUNT:int}),No tienes autorización para unirte a este juego. {RET:translate}...({COUNT:int}),Vous n'avez pas l'autorisation de rejoindre ce jeu.. {RET:translate}...({COUNT:int}),Non hai l'autorizzazione per partecipare a questa partita. {RET:translate}...({COUNT:int}),このゲームに参加する権限がありません。 {RET:translate}...({COUNT:int}),게임 참여 권한 없음. {RET:translate}...({COUNT:int}),Você não tem autorização para entrar neste jogo. {RET:translate}...({COUNT:int}),Você não tem autorização para entrar neste jogo. {RET:translate}...({COUNT:int}),Не разрешено присоединяться к этой игре. {RET:translate}...({COUNT:int}),没有加入此游戏的权限. {RET:translate}...({COUNT:int}),權限不足,無法加入此遊戲。{RET:translate}…({COUNT:int}),没有加入此游戏的权限. {RET:translate}...({COUNT:int}) +,,,Notifications,Benachrichtigungen,Notifications,Notificaciones,Notificaciones,Notifications,Notifiche,お知らせ,알림,Notificações,Notificações,Уведомления,通知,通知,通知 +,,,OK,Okay,OK,Aceptar,Aceptar,Ok,OK,OK,확인,OK,OK,OK,好,確定,好 +,,,Off,Aus,Off,Desactivado,Desactivado,Arrêt,Disattivato,オフ,끄기,Desligado,Desligado,Выкл.,关闭,關閉,关闭 +,,,Offsite Links,Externe Links,Offsite Links,Enlaces externos,Enlaces externos,Liens hors site,Collegamenti ad altri siti,外部リンク,외부 링크,Links externos,Links externos,Внешние ссылки,离线链接,站外連結,离线链接 +,,,Okay,Okay,Okay,Aceptar,Aceptar,D'accord,OK,OK,확인,Ok,Ok,ОК,好,好,好 +,,,On,An,On,Activado,Activado,Marche,Attivato,オン,켜기,Ligado,Ligado,Вкл.,开启,開啟,开启 +,,,Open,Öffnen,Open,Abrir,Abrir,Ouvert(e),Apri,開く,열기,Abrir,Abrir,Открыть,打开,開啟,打开 +,,,Open Folder,Ordner öffnen,Open Folder,Abrir carpeta,Abrir carpeta,Ouvrir le dossier,Apri cartella,フォルダを開く,폴더 열기,Abrir pasta,Abrir pasta,Открыть папку,打开文件夹,開啟資料夾,打开文件夹 +,,,Outrageous Builders Club,Outrageous Builders Club,Outrageous Builders Club,Outrageous Builders Club,Outrageous Builders Club,Outrageous Builders Club,Turbo Builders Club,Outrageous Builders Club,Outrageous Builders Club,Outrageous Builders Club,Outrageous Builders Club,Невероятный клуб создателей,Outrageous Builders Club,Outrageous Builders Club,Outrageous Builders Club +,,,Package,Paket,Package,Paquete,Paquete,Pack,Pacchetto,パッケージ,패키지,Pacote,Pacote,Набор,套装,套裝,套装 +,,,Pants,Hose,Pants,Pantalones,Pantalones,Pantalon,Pantaloni,  ズボン,바지,Calças,Calças,Штаны,裤子,褲子,裤子 +,,,Perf. Stats,Leistungsw.,Perf. Stats,Est. de rend.,Est. de rend.,Stats perf.,Stat. prest.,パフォーマンス解析,성능 통계,Estat. Des.,Estat. Des.,Показатели произ.,表现统计,效能數據,表现统计 +,,,Performance Stats,Leistungswerte,Performance Stats,Estad. de rendimiento,Estad. de rendimiento,Stats de performance,Statistiche prestazioni,パフォーマンス解析,성능 통계,Estat. de desempenho,Estat. de desempenho,Показатели производительности,表现统计,效能數據,表现统计 +,,,Personal Question,Persönliche Frage,Personal Question,Pregunta personal,Pregunta personal,Question personnelle,Domanda personale,プライベートな質問,개인 질문,Pergunta pessoal,Pergunta pessoal,Личный вопрос,私人问题,私人問題,私人问题 +,,,Phys,Phys.,Phys,Fís.,Fís.,Phys.,Fisico,物理,물리,Fís.,Fís.,Физ.,物理,物理,物理 +,,,Physics,Physik,Physics,Física,Física,Physique,Fisica,物理,물리,Física,Física,Физика,物理,物理,物理 +,,,Place,Ort,Place,Lugar,Lugar,Emplacement,Località,プレース,장소,Local,Local,Место,地点,空間,地点 +,,,Player,Spieler,Player,Jugador,Jugador,Joueur,Giocatore,プレイヤー,플레이어,Jogador,Jogador,Игрок,玩家,玩家,玩家 +,,,Playerlist,Spielerliste,Playerlist,Lista de jugadores,Lista de jugadores,Liste de joueurs,Lista giocatori,プレイヤーリスト,플레이어 목록,Jogadores,Jogadores,Список игроков,玩家名单,玩家名單,玩家名单 +,,,Players,Spieler,Players,Jugadores,Jugadores,Joueurs,Giocatori,プレイヤー,플레이어,Jogadores,Jogadores,Игроки,玩家,玩家,玩家 +,,,Plugin,Plug-in,Plugin,Plugin,Plugin,Plugin,Plug-in,プラグイン,플러그인,Plugin,Plugin,Расширение,插件,外掛程式,插件 +,,,Point Awarded,Punkt verliehen,Point Awarded,Punto concedido,Punto concedido,Point accordé,Punto attribuito,ポイント授与,획득한 포인트,Ponto concedido,Ponto concedido,Получено очко,已获得点数,已獲點數,已获得点数 +,,,Points Awarded,Punkte verliehen,Points Awarded,Puntos concedidos,Puntos concedidos,Points accordés,Punti attribuiti,ポイント授与,획득한 포인트,Pontos concedidos,Pontos concedidos,Получены очки,已获得点数,已獲點數,已获得点数 +,,,Print Screen,Drucktaste,Print Screen,Imprimir pantalla,Imprimir pantalla,Impression écran,STAMP,画面プリント,화면 캡쳐,Captura de Tela,Captura de Tela,Print Screen,屏幕截图,截圖,屏幕截图 +,,,"Protocol mismatch, please reconnect",Protokoll-Diskrepanz. Bitte erneut verbinden.,"Protocol mismatch, please reconnect",Incompatibilidad del protocolo. Conéctate de nuevo.,Incompatibilidad del protocolo. Conéctate de nuevo.,"Le protocole ne correspond pas, veuillez vous reconnecter.",Mancata corrispondenza del protocollo; è necessario riconnettersi,プロトコルの不一致です。再接続してください,프로토콜이 일치하지 않아요. 다시 연결하세요.,Incompatibilidade do Protocolo. Conecte-se novamente.,Incompatibilidade do Protocolo. Conecte-se novamente.,"Несовпадение протокола, пожалуйста, установите соединение повторно.",协议不匹配,请重新连接,通訊協定不符,請重新連線,协议不匹配,请重新连接 +,,,Purchasing,Wird gekauft ...,Purchasing,Compras,Compras,Achat,Acquisto,購入中,구매 중,Comprando,Comprando,Покупка,正在购买,正在購買,正在购买 +,,,ROBLOX version is out of date. Please uninstall and try again.,Die ROBLOX-Version ist nicht aktuell. Bitte deinstallieren und erneut versuchen.,ROBLOX version is out of date. Please uninstall and try again.,"Esta versión de ROBLOX es obsoleta. Por favor, desinstálala y vuelve a intentarlo.","Esta versión de ROBLOX es obsoleta. Por favor, desinstálala y vuelve a intentarlo.",Cette version de ROBLOX est obsolète. Veuillez désinstaller et réessayer.,Versione di ROBLOX obsoleta. Disinstalla e riprova.,古いバージョンのROBLOXです。アンインストールしてリトライしてください。,ROBLOX 버전이 오래되었습니다. 프로그램 제거 후 다시 시도하세요.,Sua versão do ROBLOX não está atualizada. Desinstale e tente novamente.,Sua versão do ROBLOX não está atualizada. Desinstale e tente novamente.,Эта версия ROBLOX устарела. Удалите приложение и повторите попытку.,Roblox 版本已过期。请解除安装并重试。,ROBLOX 版本過時,請重新安裝 ROBLOX。,Roblox 版本已过期。请解除安装并重试。 +,,,Record,Aufnehmen,Record,Grabar,Grabar,Enregistrer,Registra,録画,녹화,Gravar,Gravar,Запись,录制,錄影,录制 +,,,Record Video,Video aufnehmen,Record Video,Grabar vídeo,Grabar vídeo,Enregistrer vidéo,Registra video,ビデオ録画,비디오 녹화,Gravar vídeo,Gravar vídeo,Запись видео,录制视频,錄影,录制视频 +,,,Recv,Empfangen,Recv,Recib.,Recib.,Reçu,Ricev.,受信,받음,Receb.,Receb.,Получ.,收到,收到,收到 +,,,Remove From Hotbar,Schnellzugriff entfernen,Remove From Hotbar,Eliminar de la barra de acceso rápido,Eliminar de la barra de acceso rápido,Retirer de la barre de raccourcis,Togli da barra scelta rap.,ホットバーから削除,핫바 창에서 제거,Remover da barra,Remover da barra,Удалить из панели быстрого доступа,从快捷栏删除,從快捷列移除,从快捷栏删除 +,,,Report,Melden,Report,Denunciar,Denunciar,Signaler,Segnala,報告する,신고,Denunciar,Denunciar,Сообщить,举报,檢舉,举报 +,,,Report Abuse,Verstoß melden,Report Abuse,Denunciar abuso,Denunciar abuso,Signaler infraction,Segnala abuso,規約違反を報告,신고하기,Denunciar abuso,Denunciar abuso,Сообщить о нарушении,举报滥用,檢舉濫用,举报滥用 +,,,Request Sent,Anfrage gesendet,Request Sent,Solicitud enviada,Solicitud enviada,Demande envoyée,Richiesta inviata,リクエストを送信しました,요청 보냄,Pedido enviado,Pedido enviado,Запрос отправлен,请求已发送,請求已送出,请求已发送 +,,,Requested game is full,Das angefragte Spiel ist voll,Requested game is full,El juego solicitado está lleno,El juego solicitado está lleno,La partie demandée est complète.,Il gioco richiesto è al completo,リクエストしたゲームは満員です。,요청한 게임이 만원입니다.,O jogo solicitado está cheio,O jogo solicitado está cheio,Запрашиваемая игра переполнена,请求的游戏已满员,請求的遊戲已額滿,请求的游戏已满员 +,,,"Requested game is full, retrying...",Gewünschtes Spiel ist voll. Neuer Versuch ...,"Requested game is full, retrying...",El juego solicitado está lleno. Intentándolo de nuevo...,El juego solicitado está lleno. Intentándolo de nuevo...,"Le jeu demandé est complet, nouvelle tentative...",Il gioco richiesto è al completo. Nuovo tentativo...,リクエストしたゲームは満員です。リトライ中...,요청한 게임이 만원입니다. 재시도 중...,O jogo solicitado está cheio. Tentando de novo...,O jogo solicitado está cheio. Tentando de novo...,Запрашиваемая игра переполнена. Новая попытка...,已请求的游戏已满员。正在重试...,請求的遊戲已額滿,正在重試…,已请求的游戏已满员。正在重试... +,,,Requested game is full. ({COUNT:int}),Das angefragte Spiel ist voll. ({COUNT:int}),Requested game is full. ({COUNT:int}),El juego solicitado está lleno. ({COUNT:int}),El juego solicitado está lleno. ({COUNT:int}),La partie demandée est complète.. ({COUNT:int}),Il gioco richiesto è al completo. ({COUNT:int}),リクエストしたゲームは満員です。 ({COUNT:int}),요청한 게임이 만원입니다. ({COUNT:int}),O jogo solicitado está cheio ({COUNT:int}),O jogo solicitado está cheio ({COUNT:int}),Запрашиваемая игра переполнена ({COUNT:int}),请求的游戏已满员 ({COUNT:int}),請求的遊戲已額滿。({COUNT:int}),请求的游戏已满员 ({COUNT:int}) +,,,Requested game is full. {RET:translate}...({COUNT:int}),Das angefragte Spiel ist voll. {RET:translate}...({COUNT:int}),Requested game is full. {RET:translate}...({COUNT:int}),El juego solicitado está lleno. {RET:translate}...({COUNT:int}),El juego solicitado está lleno. {RET:translate}...({COUNT:int}),La partie demandée est complète.. {RET:translate}...({COUNT:int}),Il gioco richiesto è al completo. {RET:translate}...({COUNT:int}),リクエストしたゲームは満員です。 {RET:translate}...({COUNT:int}),요청한 게임이 만원입니다. {RET:translate}...({COUNT:int}),O jogo solicitado está cheio. {RET:translate}...({COUNT:int}),O jogo solicitado está cheio. {RET:translate}...({COUNT:int}),Запрашиваемая игра переполнена. {RET:translate}...({COUNT:int}),请求的游戏已满员. {RET:translate}...({COUNT:int}),請求的遊戲已額滿。{RET:translate}…({COUNT:int}),请求的游戏已满员. {RET:translate}...({COUNT:int}) +,,,Requesting Server...,Serveranfrage ...,Requesting Server...,Solicitando servidor...,Solicitando servidor...,Demande serveur...,Richiesta server...,サーバーにリクエスト中...,서버 요청 중...,Solicitando servidor...,Solicitando servidor...,Запрос сервера...,正在向服务器发送请求...,正在請求伺服器…,正在向服务器发送请求... +,,,Reset,Zurücksetzen,Reset,Reiniciar,Reiniciar,Réinitialiser,Azzera,リセット,재설정,Reiniciar,Reiniciar,Сброс,重置,重置,重置 +,,,Resume Game,Spiel fortsetzen,Resume Game,Seguir jugando,Seguir jugando,Reprendre le jeu,Riprendi gioco,ゲームを再開,게임 계속하기,Continuar jogo,Continuar jogo,Вернуться в игру,继续游戏,繼續遊戲,继续游戏 +,,,Retrying,Neuer Versuch,Retrying,Intentándolo de nuevo,Intentándolo de nuevo,Nouvelle tentative,Nuovo tentativo,リトライ中,재시도 중,Tentando de novo,Tentando de novo,Новая попытка,正在重试,正在重試,正在重试 +,,,Revoke Friend Request,Widerruf der Freundschaftsanfrage,Revoke Friend Request,Revocar solicitud de amigo,Revocar solicitud de amigo,Révoquer une demande d'ami,Revoca richiesta amico,友達リクエストを取り消す,친구 요청 취소,Revocar pedido de amigo,Revocar pedido de amigo,Отменить запрос друга,撤销好友邀请,取消好友邀請,撤销好友邀请 +,,,Right Arm,Rechter Arm,Right Arm,Brazo derecho,Brazo derecho,Bras droit,Braccio destro,右腕,오른팔,Braço direito,Braço direito,Правая рука,右臂,右臂,右臂 +,,,Right Leg,Rechtes Bein,Right Leg,Pierna derecha,Pierna derecha,Jambe droite,Gamba destra,右脚,오른다리,Perna direita,Perna direita,Правая нога,右腿,右腿,右腿 +,,,Right Mouse Button,Rechte Maustaste,Right Mouse Button,Botón derecho del ratón,Botón derecho del ratón,Bouton droit de la souris,Destro/Mouse,右マウスボタン,마우스 오른쪽 버튼,Mouse (direito),Mouse (direito),ПКМ,鼠标右键,右滑鼠鍵,鼠标右键 +,,,Roblox Menu,Roblox-Menü,Roblox Menu,Menú de Roblox,Menú de Roblox,Menu Roblox,Menu Roblox,Roblox メニュー,Roblox 메뉴,Menu Roblox,Menu Roblox,Меню Roblox,系统菜单,Roblox 選單,系统菜单 +,,,Roblox has shut down this game server for maintenance,Roblox hat diesen Spielserver aufgrund von Wartungsarbeiten abgeschaltet,Roblox has shut down this game server for maintenance,Roblox ha cerrado el servidor de este juego por mantenimiento,Roblox ha cerrado el servidor de este juego por mantenimiento,Roblox a arrêté ce serveur de jeu pour effectuer la maintenance.,Roblox ha chiuso questo server di gioco per manutenzione,メンテナンスのため、Roblox によってゲームサーバーがシャットダウンされています。,Roblox가 유지보수를 위해 본 게임 서버를 종료했습니다.,O servidor deste jogo foi encerrado pelo Roblox para manutenção,O servidor deste jogo foi encerrado pelo Roblox para manutenção,Roblox отключил игровой сервер для техобслуживания.,维护期间,Roblox 已关闭此游戏服务器,即將進行伺服器維護,Roblox 已關閉此遊戲伺服器,维护期间,Roblox 已关闭此游戏服务器 +,,,Roblox has shut down this game server for maintenance. {RET:translate}...({COUNT:int}),Roblox hat diesen Spielserver aufgrund von Wartungsarbeiten abgeschaltet. {RET:translate}...({COUNT:int}),Roblox has shut down this game server for maintenance. {RET:translate}...({COUNT:int}),Roblox ha cerrado el servidor de este juego por mantenimiento. {RET:translate}...({COUNT:int}),Roblox ha cerrado el servidor de este juego por mantenimiento. {RET:translate}...({COUNT:int}),Roblox a arrêté ce serveur de jeu pour effectuer la maintenance.. {RET:translate}...({COUNT:int}),Roblox ha chiuso questo server di gioco per manutenzione. {RET:translate}...({COUNT:int}),メンテナンスのため、Roblox によってゲームサーバーがシャットダウンされています。. {RET:translate}...({COUNT:int}),Roblox가 유지보수를 위해 본 게임 서버를 종료했습니다. {RET:translate}...({COUNT:int}),O servidor deste jogo foi encerrado pelo Roblox para manutenção. {RET:translate}...({COUNT:int}),O servidor deste jogo foi encerrado pelo Roblox para manutenção. {RET:translate}...({COUNT:int}),Roblox отключил игровой сервер для техобслуживания.. {RET:translate}...({COUNT:int}),维护期间,Roblox 已关闭此游戏服务器. {RET:translate}...({COUNT:int}),即將進行伺服器維護,Roblox 已關閉此遊戲伺服器。{RET:translate}…({COUNT:int}),维护期间,Roblox 已关闭此游戏服务器. {RET:translate}...({COUNT:int}) +,,,Rotate,Drehen,Rotate,Rotar,Rotar,Tourner,Ruota,回転,회전,Girar,Girar,Вращение,旋转,旋轉,旋转 +,,,Rotate Camera,Kamera drehen,Rotate Camera,Rotar cámara,Rotar cámara,Rotation de caméra,Ruota visuale,カメラを回転,카메라 회전,Girar câmera,Girar câmera,Вращение камеры,旋转镜头,旋轉相機,旋转镜头 +,,,Run Animation,Laufanimation,Run Animation,Animación de carrera,Animación de carrera,Animation de course,Animazione corsa,走るアニメーション,달리기 애니메이션,Animação de corrida,Animação de corrida,Анимация бега,跑步动画,奔跑動畫,跑步动画 +,,,S/Down Arrow,S/Abwärtspfeil,S/Down Arrow,S/Cursor abajo,S/Cursor abajo,S/Flèche bas,S/Freccia GIÙ,S/下カーソル,S/아래쪽 화살표,S/seta para baixo,S/seta para baixo,S/стрелка вниз,S/下箭头,S、↓,S/下箭头 +,,,Safe Zone,Sichere Zone,Safe Zone,Zona segura,Zona segura,Zone sécurisée,Zona sicura,セーフゾーン,안전 구역,Zona de segurança,Zona de segurança,Безопасная зона,安全区,安全區,安全区 +,,,Save To Disk,Auf Festplatte speichern,Save To Disk,Guardar en disco,Guardar en disco,Sauvegarder sur le disque,Salva su disco,ディスクに保存,디스크에 저장,Salvar em disco,Salvar em disco,Сохранить на диск,保存至磁盘,儲存到硬碟,保存至磁盘 +,,,Scamming,Scamming,Scamming,Estafa,Estafa,Arnaque,Truffa,詐欺,신용 범죄,Fraude,Fraude,Мошенничество,诈骗,詐騙,诈骗 +,,,Screenshot,Screenshot,Screenshot,Captura de pantalla,Captura de pantalla,Capture d'écran,Screenshot,スクリーンショット,스크린숏,Captura de tela,Captura de tela,Снимок экрана,屏幕截图,截圖,屏幕截图 +,,,Screenshot Taken,Screenshot gespeichert,Screenshot Taken,Captura tomada,Captura tomada,Capture d'écran prise,Screenshot catturato,撮影したスクリーンショット,스크린숏 찍기 완료,Captura de tela salva,Captura de tela salva,Снимок экрана сделан,已截取屏幕快照,已截圖,已截取屏幕快照 +,,,Select/Swap,Auswählen/Austauschen,Select/Swap,Seleccionar/alternar,Seleccionar/alternar,Sélection/Échange,Selez./Scambia,選択/交換,선택/교체,Selecionar/trocar,Selecionar/trocar,Выбрать/сменить,选取/切换,選擇 / 切換,选取/切换 +,,,Send Friend Request,Freundesanfrage senden,Send Friend Request,Enviar solicitud de amistad,Enviar solicitud de amistad,Envoyer demande d'ami,Invia richiesta di amicizia,友達リクエストを送信,친구 요청 보내기,Enviar pedido de amizade,Enviar pedido de amizade,Отправить запрос дружбы,发送好友邀请,傳送好友邀請,发送好友邀请 +,,,Send Game Invites,Spieleinladungen senden,Send Game Invites,Enviar invitaciones al juego,Enviar invitaciones al juego,Envoyer des invitations,Manda inviti di gioco,ゲームへの招待を送る,게임 초대 전송,Enviar convites,Enviar convites,Отправить приглашения,发送游戏邀请,傳送遊戲邀請,发送游戏邀请 +,,,Send Request,Anfrage senden,Send Request,Enviar solicitud,Enviar solicitud,Envoyer demande,Invia richiesta,リクエスト送信,요청 전송,Enviar pedido,Enviar pedido,Отправить запрос,发送请求,傳送請求,发送请求 +,,,Sent you a friend request!,hat dir eine Freundesanfrage gesendet!,Sent you a friend request!,te ha enviado una solicitud de amistad,te ha enviado una solicitud de amistad,vous a envoyé une demande d'ami !,ti ha inviato una richiesta di amicizia!,君に友達リクエストを送ったよ!,친구 요청을 보냈어요!,enviou um pedido de amizade para você!,enviou um pedido de amizade para você!,отправил вам запрос дружбы!,向你发送了好友邀请!,已向您傳送好友邀請!,向你发送了好友邀请! +,,,"Server found, loading.... ({COUNT:int})","Server gefunden, wird geladen .... ({COUNT:int})","Server found, loading.... ({COUNT:int})",Servidor encontrado. Cargando.... ({COUNT:int}),Servidor encontrado. Cargando.... ({COUNT:int}),"Serveur trouvé, chargement en cours.... ({COUNT:int})","Server trovato, caricamento.... ({COUNT:int})",サーバが見つかりました。読み込み中です... ({COUNT:int}),"서버를 찾았습니다, 불러오는 중... ({COUNT:int})","Servidor encontrado, carregando... ({COUNT:int})","Servidor encontrado, carregando... ({COUNT:int})",Сервер найден. Загрузка... ({COUNT:int}),找到服务器,正在加载... ({COUNT:int}),找到伺服器,正在載入…({COUNT:int}),找到服务器,正在加载... ({COUNT:int}) +,,,"Server found, loading.... {RET:translate}...({COUNT:int})","Server gefunden, wird geladen .... {RET:translate}...({COUNT:int})","Server found, loading.... {RET:translate}...({COUNT:int})",Servidor encontrado. Cargando.... {RET:translate}...({COUNT:int}),Servidor encontrado. Cargando.... {RET:translate}...({COUNT:int}),"Serveur trouvé, chargement en cours.... {RET:translate}...({COUNT:int})","Server trovato, caricamento.... {RET:translate}...({COUNT:int})",サーバが見つかりました。読み込み中です.... {RET:translate}...({COUNT:int}),"서버를 찾았습니다, 불러오는 중.... {RET:translate}...({COUNT:int})","Servidor encontrado, carregando.... {RET:translate}...({COUNT:int})","Servidor encontrado, carregando.... {RET:translate}...({COUNT:int})",Сервер найден. Загрузка.... {RET:translate}...({COUNT:int}),找到服务器,正在加载.... {RET:translate}...({COUNT:int}),找到伺服器,正在載入…{RET:translate}…({COUNT:int}),找到服务器,正在加载.... {RET:translate}...({COUNT:int}) +,,,Server is busy,Server ist ausgelastet,Server is busy,El servidor está ocupado,El servidor está ocupado,Le serveur est occupé.,Il server è occupato,サーバーがビジー状態です,서버 사용량이 많아요.,Servidor ocupado,Servidor ocupado,Сервер занят,服务器忙,伺服器忙碌中,服务器忙 +,,,Server is busy. ({COUNT:int}),Server ist ausgelastet. ({COUNT:int}),Server is busy. ({COUNT:int}),El servidor está ocupado. ({COUNT:int}),El servidor está ocupado. ({COUNT:int}),Le serveur est occupé.. ({COUNT:int}),Il server è occupato. ({COUNT:int}),サーバーがビジー状態です。 ({COUNT:int}),서버 사용량이 많아요. ({COUNT:int}),Servidor ocupado ({COUNT:int}),Servidor ocupado ({COUNT:int}),Сервер занят ({COUNT:int}),服务器忙 ({COUNT:int}),伺服器忙碌中。({COUNT:int}),服务器忙 ({COUNT:int}) +,,,Server is busy. {RET:translate}...({COUNT:int}),Server ist ausgelastet. {RET:translate}...({COUNT:int}),Server is busy. {RET:translate}...({COUNT:int}),El servidor está ocupado. {RET:translate}...({COUNT:int}),El servidor está ocupado. {RET:translate}...({COUNT:int}),Le serveur est occupé.. {RET:translate}...({COUNT:int}),Il server è occupato. {RET:translate}...({COUNT:int}),サーバーがビジー状態です。 {RET:translate}...({COUNT:int}),서버 사용량이 많아요.. {RET:translate}...({COUNT:int}),Servidor ocupado. {RET:translate}...({COUNT:int}),Servidor ocupado. {RET:translate}...({COUNT:int}),Сервер занят. {RET:translate}...({COUNT:int}),服务器忙. {RET:translate}...({COUNT:int}),伺服器忙碌中。{RET:translate}…({COUNT:int}),服务器忙. {RET:translate}...({COUNT:int}) +,,,Server was shutdown due to no active players,"Der Server wurde abgeschaltet, weil keine aktiven Spieler vorhanden waren",Server was shutdown due to no active players,Se ha cerrado el servidor por inactividad de los jugadores,Se ha cerrado el servidor por inactividad de los jugadores,"Le serveur a été fermé, car aucun joueur n'était actif.",Il server è stato chiuso per mancanza di giocatori attivi,アクティブなプレイヤーがいないため、サーバーはシャットダウンされました,플레이 중인 플레이어가 없어 서버가 종료되었습니다,O servidor foi encerrado pois não há jogadores ativos,O servidor foi encerrado pois não há jogadores ativos,Сервер был отключен из-за отсутствия игроков.,由于没有活跃玩家,服务器关闭,沒有活躍玩家,伺服器已關閉,由于没有活跃玩家,服务器关闭 +,,,Set by Developer,Vom Entwickler festgelegt,Set by Developer,Configurado por el desarrollador,Configurado por el desarrollador,Défini par développeur,Impostato da sviluppatore,開発者による設定,개발자 설정,Definido pelo desenvolvedor,Definido pelo desenvolvedor,Установлено разработчиком,由开发人员设置,由開發人員設定,由开发人员设置 +,,,Settings,Einstellungen,Settings,Config.,Config.,Paramètres,Impostazioni,設定,설정,Config.,Config.,Настройки,设置,設定,设置 +,,,Shift,Umschalttaste,Shift,Mayúsculas,Mayúsculas,Maj,MAIUSC,シフト,Shift,Shift,Shift,Shift,Shift,Shift,Shift +,,,Shift Lock Switch,Shift-Lock-Schalter,Shift Lock Switch,Bloqueo de mayúsculas,Bloqueo de mayúsculas,Touche Verr. Vue,BLOC MAIUSC,シフトロックスイッチ,Shift Lock 전환,Trava do shift,Trava do shift,Переключение клавишей Shift,镜头切换开关,Shift Lock 開關,镜头切换开关 +,,,Shirt,Hemd,Shirt,Camisa,Camisa,Chemise,Camicia,シャツ,셔츠,Camisa,Camisa,Рубашка,衬衫,襯衫,衬衫 +,,,Shoulder Accessory,Schulter-Accessoire,Shoulder Accessory,Accesorio para el hombro,Accesorio para el hombro,Accessoire d'épaule,Accessorio spalla,肩アクセサリ,어깨 장신구,Acessório de ombro,Acessório de ombro,Аксессуар для плеч,肩部配饰,肩膀飾品,肩部配饰 +,,,SolidModel,SolidModel,SolidModel,SolidModel,SolidModel,SolidModel,Modello solido,SolidModel,SolidModel,SolidModel,SolidModel,Твердая модель,SolidModel,SolidModel,SolidModel +,,,Space,Leertaste,Space,Espacio,Espacio,Espace,BARRA SPAZIATRICE,スペース,스페이스,Espaço,Espaço,Пробел,空格,空格,空格 +,,,Speed,Tempo,Speed,Velocidad,Velocidad,Vitesse,Velocità,速度,속도,Velocidade,Velocidade,Скорость,速度,速度,速度 +,,,Stop Recording,Aufnahme beenden,Stop Recording,Detener grabación,Detener grabación,Arrêter l'enregistrement,Interrompi registrazione,録画を停止する,녹화 중지,Parar gravação,Parar gravação,Остановить запись,停止录音,停止錄影,停止录音 +,,,Submit,Senden,Submit,Enviar,Enviar,Soumettre,Invia,送信,확인,Enviar,Enviar,Отправить,提交,提交,提交 +,,,Swearing,Fluchen,Swearing,Palabras malsonantes,Palabras malsonantes,Insultes,Turpiloquio,ののしり,욕설,Palavrões,Palavrões,Ненормативная лексика,脏话谩骂,髒話謾罵,脏话谩骂 +,,,Swim Animation,Schwimmanimation,Swim Animation,Animación de natación,Animación de natación,Animation de nage,Animazione nuotata,泳ぐアニメーション,수영 애니메이션,Animação de nado,Animação de nado,Анимация плавания,游泳动画,游泳動畫,游泳动画 +,,,Switch Tool,Werkzeug wechseln,Switch Tool,Alternar herramienta,Alternar herramienta,Outil d'échange,Cambia strumento,ツールを交換,도구 전환,Trocar ferramenta,Trocar ferramenta,Сменить предмет,切换工具,切換工具,切换工具 +,,,T-Shirt,T-Shirt,T-Shirt,Camiseta,Camiseta,Tee-shirt,Maglietta,Tシャツ,티셔츠,Camiseta,Camiseta,Футболка,T 恤,T 恤,T 恤 +,,,TAB,Tabulator,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB +,,,Take Free,Gratis nehmen,Take Free,Obtener gratis,Obtener gratis,Prendre gratuitement,Prendi gratis,無料配布,무료 획득,Obter grátis,Obter grátis,Получить бесплатно,免费获取,免費領取,免费获取 +,,,Take Screenshot,Screenshot machen,Take Screenshot,Hacer captura de pantalla,Hacer captura de pantalla,Capture d'écran,Cattura screenshot,スクリーンショットを撮る,스크린숏 찍기,Captura de tela,Captura de tela,Сделать снимок экрана,截取屏幕快照,截圖,截取屏幕快照 +,,,Tap to Move,Zum Bewegen tippen,Tap to Move,Tocar para mover,Tocar para mover,Toucher pour se déplacer,Tocca per muovere,タップして移動,눌러서 이동,Toque para mover,Toque para mover,Передвижение по касанию,轻点以移动,輕觸移動,轻点以移动 +,,,Text,Text,Text,Texto,Texto,Texte,Testo,テキスト,텍스트,Texto,Texto,Текст,文字,文字,文字 +,,,Thanks for your report! Our moderators will evaluate the username.,Danke für deine Meldung! Unsere Moderatoren werden den Benutzernamen überprüfen.,Thanks for your report! Our moderators will evaluate the username.,Gracias por denunciarlo. Nuestros moderadores analizarán el nombre de usuario.,Gracias por denunciarlo. Nuestros moderadores analizarán el nombre de usuario.,Merci pour ce signalement ! Nos modérateurs vont examiner le nom de cet utilisateur.,Grazie della segnalazione! I moderatori esamineranno il nome utente.,ご報告ありがとうございます! モデレータがユーザーネームを確認します。,신고해주셔서 감사합니다! 검열팀이 사용자 이름을 검토할 거예요.,Obrigado por sua denúncia! Nossos moderadores avaliarão o nome de usuário.,Obrigado por sua denúncia! Nossos moderadores avaliarão o nome de usuário.,Спасибо за сообщение! Наши модераторы рассмотрят имя этого пользователя.,感谢你的举报!我们的审查员将对用户名进行评估。,感謝您的檢舉,我們的管理員將會審查該使用者名稱。,感谢你的举报!我们的审查员将对用户名进行评估。 +,,,Thanks for your report! Our moderators will review the chat logs and evaluate what happened.,"Danke für deine Meldung! Unsere Moderatoren werden sich die Chatprotokolle ansehen und überprüfen, was vorgefallen ist.",Thanks for your report! Our moderators will review the chat logs and evaluate what happened.,Gracias por denunciarlo. Nuestros moderadores comprobarán el registro del chat para analizar lo sucedido.,Gracias por denunciarlo. Nuestros moderadores comprobarán el registro del chat para analizar lo sucedido.,Merci pour ce signalement ! Nos modérateurs vont examiner le journal de chat et évaluer la situation.,Grazie della segnalazione! I moderatori controlleranno i registri della chat ed esamineranno l'accaduto.,ご報告ありがとうございます! モデレータがチャットログから状況を確認します。,신고해주셔서 감사합니다! 검열팀이 채팅 기록을 검토한 후 사건을 조사할 거예요.,Obrigado por sua denúncia! Nossos moderadores revisarão o histórico de chat e avaliarão o ocorrido.,Obrigado por sua denúncia! Nossos moderadores revisarão o histórico de chat e avaliarão o ocorrido.,"Спасибо за сообщение! Наши модераторы просмотрят журнал чата, чтобы оценить ситуацию.",感谢你的举报!我们的审查员将审阅聊天记录并进行评估。,感謝您的檢舉,我們的管理員將會審查聊天紀錄。,感谢你的举报!我们的审查员将审阅聊天记录并进行评估。 +,,,Thanks for your report! Our moderators will review the place and make a determination.,Danke für deine Meldung! Unsere Moderatoren werden sich den Ort ansehen und eine Entscheidung treffen.,Thanks for your report! Our moderators will review the place and make a determination.,Gracias por denunciarlo. Nuestros moderadores analizarán el juego y tomarán una decisión.,Gracias por denunciarlo. Nuestros moderadores analizarán el juego y tomarán una decisión.,Merci pour ce signalement ! Nos modérateurs vont examiner le jeu avant de prendre une décision.,Grazie della segnalazione! I moderatori esamineranno il gioco e prenderanno una decisione.,ご報告ありがとうございます! モデレータがプレースを確認のうえ判断いたします。,신고해주셔서 감사합니다! 검열팀이 장소를 검토하고 결정을 내릴 거예요.,Obrigado por sua denúncia! Nossos moderadores verificarão o jogo e irão tomar uma decisão.,Obrigado por sua denúncia! Nossos moderadores verificarão o jogo e irão tomar uma decisão.,Спасибо за сообщение! Наши модераторы изучат это место и примут решение.,感谢你的举报!我们的审查员将查看该地点并进行评估。,感謝您的檢舉,我們的管理員將會審查該地點。,感谢你的举报!我们的审查员将查看该地点并进行评估。 +,,,Thanks for your report! We've recorded your report for evaluation.,Danke für deine Meldung! Sie wurde zur Auswertung gespeichert.,Thanks for your report! We've recorded your report for evaluation.,Gracias por denunciarlo. Hemos guardado la denuncia para analizar lo sucedido.,Gracias por denunciarlo. Hemos guardado la denuncia para analizar lo sucedido.,Merci pour ce signalement ! Nous l'avons bien pris en compte et allons l'examiner.,Grazie della segnalazione! La tua segnalazione è stata registrata per essere esaminata.,ご報告ありがとうございます! レポートは評価のため記録されました。,신고해주셔서 감사합니다! 검토를 위해 회원님의 신고를 저장했어요.,Obrigado por sua denúncia! Registramos a sua denúncia para que seja avaliada.,Obrigado por sua denúncia! Registramos a sua denúncia para que seja avaliada.,Спасибо за сообщение! Оно будет рассмотрено.,感谢你的举报!我们已记录你的举报信息以进行评估。,感謝您的檢舉,我們的管理員將會盡速審查。,感谢你的举报!我们已记录你的举报信息以进行评估。 +,,,This game has been disconnected because you have joined a game from another device,"Dieses Spiel wurde unterbrochen, weil du einem Spiel von einem anderen Gerät beigetreten bist",This game has been disconnected because you have joined a game from another device,Se ha desconectado este juego porque te has unido a un juego desde otro dispositivo,Se ha desconectado este juego porque te has unido a un juego desde otro dispositivo,"Vous avez été déconnecté(e) de ce jeu, car vous en avez rejoint un autre depuis un appareil différent.",Questo gioco è stato disconnesso perché hai giocato una partita con un altro dispositivo,別のデバイスからゲームに参加したため、このゲームへの接続が切断されました,다른 기기에서 게임에 참여하여 본 게임의 연결이 끊어졌어요.,Este jogo foi desconectado pois você entrou em um jogo através de outro dispositivo,Este jogo foi desconectado pois você entrou em um jogo através de outro dispositivo,"Соединение с этой игрой было прервано, так как вы стали играть с другого устройства.",由于你从另一设备加入游戏,此游戏已断开连接,連線中斷,您已從其它狀置加入遊戲,由于你从另一设备加入游戏,此游戏已断开连接 +,,,This game has been disconnected because you have joined a game from another device. {RET:translate}...({COUNT:int}),"Dieses Spiel wurde unterbrochen, weil du einem Spiel von einem anderen Gerät beigetreten bist. {RET:translate}...({COUNT:int})",This game has been disconnected because you have joined a game from another device. {RET:translate}...({COUNT:int}),Se ha desconectado este juego porque te has unido a un juego desde otro dispositivo. {RET:translate}...({COUNT:int}),Se ha desconectado este juego porque te has unido a un juego desde otro dispositivo. {RET:translate}...({COUNT:int}),"Vous avez été déconnecté(e) de ce jeu, car vous en avez rejoint un autre depuis un appareil différent.. {RET:translate}...({COUNT:int})",Questo gioco è stato disconnesso perché hai giocato una partita con un altro dispositivo. {RET:translate}...({COUNT:int}),別のデバイスからゲームに参加したため、このゲームへの接続が切断されました. {RET:translate}...({COUNT:int}),다른 기기에서 게임에 참여하여 본 게임의 연결이 끊어졌어요.. {RET:translate}...({COUNT:int}),Este jogo foi desconectado pois você entrou em um jogo através de outro dispositivo. {RET:translate}...({COUNT:int}),Este jogo foi desconectado pois você entrou em um jogo através de outro dispositivo. {RET:translate}...({COUNT:int}),"Соединение с этой игрой было прервано, так как вы стали играть с другого устройства.. {RET:translate}...({COUNT:int})",由于你从另一设备加入游戏,此游戏已断开连接. {RET:translate}...({COUNT:int}),連線中斷,您已從其它狀置加入遊戲。{RET:translate}…({COUNT:int}),由于你从另一设备加入游戏,此游戏已断开连接. {RET:translate}...({COUNT:int}) +,,,This game has ended,Dieses Spiel ist beendet,This game has ended,Este juego ha finalizado,Este juego ha finalizado,Cette partie est terminée.,Questo gioco è stato chiuso,このゲームは終了しました,이 게임은 종료되었어요.,Este jogo acabou,Este jogo acabou,Эта игра закончена,此游戏已结束,此遊戲已結束,此游戏已结束 +,,,This game has ended. ({COUNT:int}),Dieses Spiel ist beendet. ({COUNT:int}),This game has ended. ({COUNT:int}),Este juego ha finalizado. ({COUNT:int}),Este juego ha finalizado. ({COUNT:int}),Cette partie est terminée.. ({COUNT:int}),Questo gioco è stato chiuso. ({COUNT:int}),このゲームは終了しました。 ({COUNT:int}),이 게임은 종료되었어요. ({COUNT:int}),Este jogo acabou ({COUNT:int}),Este jogo acabou ({COUNT:int}),Эта игра закончена ({COUNT:int}),此游戏已结束 ({COUNT:int}),此遊戲已結束 ({COUNT:int}),此游戏已结束 ({COUNT:int}) +,,,This game has ended. {RET:translate}...({COUNT:int}),Dieses Spiel ist beendet. {RET:translate}...({COUNT:int}),This game has ended. {RET:translate}...({COUNT:int}),Este juego ha finalizado. {RET:translate}...({COUNT:int}),Este juego ha finalizado. {RET:translate}...({COUNT:int}),Cette partie est terminée.. {RET:translate}...({COUNT:int}),Questo gioco è stato chiuso. {RET:translate}...({COUNT:int}),このゲームは終了しました。 {RET:translate}...({COUNT:int}),이 게임은 종료되었어요.. {RET:translate}...({COUNT:int}),Este jogo acabou. {RET:translate}...({COUNT:int}),Este jogo acabou. {RET:translate}...({COUNT:int}),Эта игра закончена. {RET:translate}...({COUNT:int}),此游戏已结束. {RET:translate}...({COUNT:int}),此遊戲已結束。{RET:translate}…({COUNT:int}),此游戏已结束. {RET:translate}...({COUNT:int}) +,,,This game has shut down,Dieses Spiel wurde abgebrochen.,This game has shut down,Este juego ha sido cerrado.,Este juego ha sido cerrado.,Ce jeu a dû fermer.,Questo gioco è stato chiuso,このゲームはシャットダウン中です,게임이 종료되었습니다.,Este jogo foi encerrado,Este jogo foi encerrado,Эта игра больше не доступна.,此游戏已关闭,此遊戲已關閉,此游戏已关闭 +,,,This game is disabled,Dieses Spiel wurde deaktiviert,This game is disabled,Este juego está desactivado,Este juego está desactivado,Ce jeu est désactivé.,Questo gioco è stato disattivato,このゲームは無効です,비활성화된 게임입니다.,Este jogo está desabilitado,Este jogo está desabilitado,Эта игра отключена,游戏已禁用,此遊戲已停用,游戏已禁用 +,,,This game is disabled. ({COUNT:int}),Dieses Spiel wurde deaktiviert. ({COUNT:int}),This game is disabled. ({COUNT:int}),Este juego está desactivado. ({COUNT:int}),Este juego está desactivado. ({COUNT:int}),Ce jeu est désactivé.. ({COUNT:int}),Questo gioco è stato disattivato. ({COUNT:int}),このゲームは無効です。 ({COUNT:int}),비활성화된 게임입니다. ({COUNT:int}),Este jogo está desabilitado ({COUNT:int}),Este jogo está desabilitado ({COUNT:int}),Эта игра отключена ({COUNT:int}),游戏已禁用 ({COUNT:int}),此遊戲已停用。({COUNT:int}),游戏已禁用 ({COUNT:int}) +,,,This game is disabled. {RET:translate}...({COUNT:int}),Dieses Spiel wurde deaktiviert. {RET:translate}...({COUNT:int}),This game is disabled. {RET:translate}...({COUNT:int}),Este juego está desactivado. {RET:translate}...({COUNT:int}),Este juego está desactivado. {RET:translate}...({COUNT:int}),Ce jeu est désactivé.. {RET:translate}...({COUNT:int}),Questo gioco è stato disattivato. {RET:translate}...({COUNT:int}),このゲームは無効です。 {RET:translate}...({COUNT:int}),비활성화된 게임입니다. {RET:translate}...({COUNT:int}),Este jogo está desabilitado. {RET:translate}...({COUNT:int}),Este jogo está desabilitado. {RET:translate}...({COUNT:int}),Эта игра отключена. {RET:translate}...({COUNT:int}),游戏已禁用. {RET:translate}...({COUNT:int}),此遊戲已停用。{RET:translate}…({COUNT:int}),游戏已禁用. {RET:translate}...({COUNT:int}) +,,,This game is not available. Please try another,Dieses Spiel ist nicht verfügbar. Bitte probiere ein anderes.,This game is not available. Please try another,Este juego no está disponible. Inténtalo con otro.,Este juego no está disponible. Inténtalo con otro.,Ce jeu n'est pas disponible. Veuillez en essayer un autre,Questo gioco non è disponibile. Provane un altro,このゲームは利用できません。他を試してください。,이용할 수 없는 게임입니다. 다른 게임을 시도하세요,Este jogo não está disponível. Tente outro,Este jogo não está disponível. Tente outro,Эта игра недоступна. Попробуйте другую,此游戏不可用。请试试另一个,此遊戲無法遊玩,請嘗試其它遊戲,此游戏不可用。请试试另一个 +,,,This game is restricted,Dieses Spiel ist begrenzt,This game is restricted,Este juego está restringido,Este juego está restringido,Ce jeu est en accès limité.,Questa partita è riservata,このゲームは制限されています,제한된 게임입니다.,Este jogo é restrito,Este jogo é restrito,Эта игра запрещена,此游戏有限制,此遊戲已受限,此游戏有限制 +,,,This game is restricted. ({COUNT:int}),Dieses Spiel ist begrenzt. ({COUNT:int}),This game is restricted. ({COUNT:int}),Este juego está restringido. ({COUNT:int}),Este juego está restringido. ({COUNT:int}),Ce jeu est en accès limité.. ({COUNT:int}),Questa partita è riservata. ({COUNT:int}),このゲームは制限されています。 ({COUNT:int}),제한된 게임입니다. ({COUNT:int}),Este jogo é restrito ({COUNT:int}),Este jogo é restrito ({COUNT:int}),Эта игра запрещена ({COUNT:int}),此游戏有限制 ({COUNT:int}),此遊戲已受限。({COUNT:int}),此游戏有限制 ({COUNT:int}) +,,,This game is restricted. {RET:translate}...({COUNT:int}),Dieses Spiel ist begrenzt. {RET:translate}...({COUNT:int}),This game is restricted. {RET:translate}...({COUNT:int}),Este juego está restringido. {RET:translate}...({COUNT:int}),Este juego está restringido. {RET:translate}...({COUNT:int}),Ce jeu est en accès limité.. {RET:translate}...({COUNT:int}),Questa partita è riservata. {RET:translate}...({COUNT:int}),このゲームは制限されています。 {RET:translate}...({COUNT:int}),제한된 게임입니다. {RET:translate}...({COUNT:int}),Este jogo é restrito. {RET:translate}...({COUNT:int}),Este jogo é restrito. {RET:translate}...({COUNT:int}),Эта игра запрещена. {RET:translate}...({COUNT:int}),此游戏有限制. {RET:translate}...({COUNT:int}),此遊戲已受限。{RET:translate}…({COUNT:int}),此游戏有限制. {RET:translate}...({COUNT:int}) +,,,This is a popup,Dies ist ein Pop-up.,This is a popup,Esta es una ventana emergente.,Esta es una ventana emergente.,Ceci est une fenêtre popup,Questo è un popup,これはポップアップです,팝업 메뉴입니다,Este é um popup,Este é um popup,Всплывающее сообщение,这是弹出式窗口,此為彈出視窗,这是弹出式窗口 +,,,This is a test purchase; your account will not be charged.,Dies ist ein Testkauf. Dein Konto wird dadurch nicht belastet.,This is a test purchase; your account will not be charged.,Esta compra es una prueba; no se llevará a cabo ningún cobro en tu cuenta.,Esta compra es una prueba; no se llevará a cabo ningún cobro en tu cuenta.,Ceci est un achat test ; votre compte ne sera pas débité.,Questo è un acquisto di prova; il tuo account non subirà addebiti.,これはテスト購入です。アカウントはチャージされません。,테스트 구매입니다. 계정에 비용이 청구되지 않아요.,Esta é uma compra de teste. Nada será cobrado da sua conta.,Esta é uma compra de teste. Nada será cobrado da sua conta.,Это тестовая покупка.Средства с вашего счета списаны не будут.,此次购买为测试性质;你的帐户将不会被扣款。,此為測試性購買,您的帳號餘額將維持不變。,此次购买为测试性质;你的帐户将不会被扣款。 +,,,This item cost more ROBUX than you can purchase. Please visit www.roblox.com to purchase more ROBUX.,"Dieser Gegenstand kostet mehr ROBUX, als du kaufen kannst. Bitte besuche www.roblox.com, um mehr ROBUX zu kaufen.",This item cost more ROBUX than you can purchase. Please visit www.roblox.com to purchase more ROBUX.,Este objeto cuesta más ROBUX de los que puedes comprar. Visita www.roblox.com para comprar más ROBUX.,Este objeto cuesta más ROBUX de los que puedes comprar. Visita www.roblox.com para comprar más ROBUX.,Cet objet coûte trop de ROBUX pour que vous l'achetiez. Veuillez vous rendre sur www.roblox.com pour acheter plus de ROBUX.,Questo oggetto costa più ROBUX di quanti tu possa acquistarne. Visita il sito www.roblox.com per acquistare altri ROBUX.,このアイテムを買うにはお持ちのROBUXでは足りません。www.roblox.com にアクセスしてROBUXを追加購入してください。,ROBUX가 부족해 본 아이템을 구매할 수 없습니다. ROBUX를 구매하려면 www.roblox.com을 방문하세요.,Este item custa mais ROBUX do que você pode comprar. Visite www.roblox.com para comprar mais ROBUX.,Este item custa mais ROBUX do que você pode comprar. Visite www.roblox.com para comprar mais ROBUX.,"Цена товара превышает количество ROBUX, которое вы можете купить. Перейдите на страницу www.roblox.com, чтобы приобрести больше ROBUX.",此物品的价格超过了你拥有的 ROBUX。请前往 www.roblox.com 购买更多 ROBUX。,您的 Robux 不足,無法購買此道具。請前往 www.roblox.com 加購 Robux。,此物品的价格超过了你拥有的 ROBUX。请前往 www.roblox.com 购买更多 ROBUX。 +,,,This item cost more ROBUX than you have available. Please leave this game and go to the ROBUX screen to purchase more.,"Dieser Gegenstand kostet mehr ROBUX, als dir zur Verfügung stehen. Bitte verlasse dieses Spiel, gehe zum ROBUX-Menü und kaufe dort mehr.",This item cost more ROBUX than you have available. Please leave this game and go to the ROBUX screen to purchase more.,Este objeto cuesta más ROBUX de los que tienes disponibles. Sal de este juego y ve a la pantalla de ROBUX para comprar más.,Este objeto cuesta más ROBUX de los que tienes disponibles. Sal de este juego y ve a la pantalla de ROBUX para comprar más.,Cet objet coûte plus de ROBUX que vous n'en avez. Veuillez quitter le jeu et aller à l'écran ROBUX pour en acheter plus.,Questo oggetto costa più ROBUX di quanti tu ne abbia. Esci dal gioco e vai nella schermata dei ROBUX per acquistarne altri.,このアイテムを買うにはお持ちのROBUXでは足りません。もっと購入するにはゲームを終了してROBUX画面に行ってください。,ROBUX가 부족해 본 아이템을 구매할 수 없습니다. ROBUX를 구매하려면 게임을 종료 후 ROBUX 화면으로 이동하세요.,Este item custa mais ROBUX do que você possui disponível. Saia do jogo e vá para a tela de ROBUX para comprar mais.,Este item custa mais ROBUX do que você possui disponível. Saia do jogo e vá para a tela de ROBUX para comprar mais.,"Цена товара превышает имеющееся у вас количество ROBUX. Выйдите из этой игры и перейдите в раздел ROBUX, чтобы купить больше.",此物品的价格超过了你拥有的 ROBUX。请离开此游戏,前往 ROBUX 屏幕以购买更多。,您的 Robux 不足,無法購買此道具。請離開此遊戲,前往 Robux 畫面加購 Robux。,此物品的价格超过了你拥有的 ROBUX。请离开此游戏,前往 ROBUX 屏幕以购买更多。 +,,,This was a test purchase.,Dies war ein Testkauf.,This was a test purchase.,Esta compra ha sido una prueba.,Esta compra ha sido una prueba.,C'était un achat test.,Questo era un acquisto di prova.,テスト購入でした。,테스트 구매입니다.,Esta foi uma compra de teste.,Esta foi uma compra de teste.,Это была тестовая покупка.,此次购买为测试性质。,此為測試性購買。,此次购买为测试性质。 +,,,Thumbpad,Daumenpad,Thumbpad,Cruceta,Cruceta,Pavé directionnel,Croce direzionale,サムパッド,엄지패드,Thumbpad,Thumbpad,Кнопочная панель,拇指垫,虛擬搖桿,拇指垫 +,,,Thumbstick,Daumenstick,Thumbstick,Stick,Stick,Joystick,Levetta,サムスティック,엄지스틱,Thumbstick,Thumbstick,Аналоговый стик,摇杆,類比搖桿,摇杆 +,,,To {RBX_NAME},An {RBX_NAME},To {RBX_NAME},Para {RBX_NAME},Para {RBX_NAME},À {RBX_NAME},A {RBX_NAME},宛先: {RBX_NAME},수신: {RBX_NAME},Para {RBX_NAME},Para {RBX_NAME},Получатель: {RBX_NAME},至“{RBX_NAME}”,給 {RBX_NAME},至“{RBX_NAME}” +,,,Torso,Körper,Torso,Torso,Torso,Torse,Busto,胴体,몸통,Tronco,Tronco,Корпус,身体主干,軀幹,身体主干 +,,,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Потрясающий клуб создателей,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club +,,,Type Of Abuse,Art des Verstoßes,Type Of Abuse,Tipo de abuso,Tipo de abuso,Type d'abus,Tipo di abuso,規約違反の種類,욕설 유형,Tipo de abuso,Tipo de abuso,Тип нарушения,滥用类型,濫用類型,滥用类型 +,,,Unblock Player,Spieler nicht mehr sperren,Unblock Player,Desbloquear jugador,Desbloquear jugador,Débloquer joueur,Sblocca giocatore,プレイヤーのブロックを解除,플레이어 차단 해제,Desbloq. jogador,Desbloq. jogador,Разблокировать игрока,取消屏蔽玩家,解除封鎖玩家,取消屏蔽玩家 +,,,Unequip Tools,Werkzeug-Ausrüstung abnehmen,Unequip Tools,Desequipar herramientas,Desequipar herramientas,Outils non équipés,Rimuovi strum.,ツールを外す,도구 장착 해제,Desequipar,Desequipar,Убрать,取消配备工具,卸下工具,取消配备工具 +,,,Unfollow Player,Spieler nicht mehr folgen,Unfollow Player,Dejar de seguir a jugador,Dejar de seguir a jugador,Ne plus suivre joueur,Non seguire giocatore,プレイヤーをフォローしない,플레이어 팔로우 취소,Parar de seguir jogador,Parar de seguir jogador,Отписаться от игрока,取消关注玩家,取消追蹤玩家,取消关注玩家 +,,,Unfriend Player,Spieler von der Freundesliste entfernen,Unfriend Player,Cancelar amistad con jugador,Cancelar amistad con jugador,Ne plus être ami du joueur,Togli amicizia a giocatore,プレイヤーを友達解除,플레이어 친구 끊기,Cancelar amizade,Cancelar amizade,Удалить из друзей,与玩家解除好友关系,刪除好友,与玩家解除好友关系 +,,,Upgrade,Aufwerten,Upgrade,Mejorar,Mejorar,Améliorer,Migliora,アップグレード,업그레이드,Melhorar,Melhorar,Улучшение,升级,升級,升级 +,,,Upload to YouTube,Auf YouTube hochladen,Upload to YouTube,Cargar a YouTube,Cargar a YouTube,Télécharger sur YouTube,Carica su YouTube,YouTubeにアップロード,YouTube에 업로드,Envio para o YouTube,Envio para o YouTube,Загрузить на YouTube,上传至 Youtube,上傳到 YouTube,上传至 Youtube +,,,Use Tool,Werkzeug verwenden,Use Tool,Utilizar herramienta,Utilizar herramienta,Utiliser outil,Usa strumento,ツールを使用,도구 사용,Usar,Usar,Использовать предмет,使用工具,使用工具,使用工具 +,,,Version not compatible with server. Please uninstall and try again.,Version nicht mit Server kompatibel. Bitte deinstallieren und erneut versuchen.,Version not compatible with server. Please uninstall and try again.,"Esta versión no es compatible con el servidor. Por favor, desinstálala y vuelve a intentarlo.","Esta versión no es compatible con el servidor. Por favor, desinstálala y vuelve a intentarlo.",Version incompatible avec le serveur. Veuillez désinstaller et réessayer.,Versione non compatibile con il server. Disinstalla e riprova.,サーバと互換性のないバージョンです。アンインストールしてリトライしてください。,서버와 호환되지 않는 버전입니다. 프로그램 제거 후 다시 시도하세요.,Versão incompatível com servidor. Desinstale e tente novamente.,Versão incompatível com servidor. Desinstale e tente novamente.,Версия приложения несовместима с сервером. Удалите приложение и повторите попытку.,版本与服务器不匹配。请解除安装并重试。,版本與伺服器不相容,請解除安裝後重新嘗試。,版本与服务器不匹配。请解除安装并重试。 +,,,Video,Video,Video,Vídeo,Vídeo,Vidéo,Video,ビデオ,비디오,Vídeo,Vídeo,Видео,视频,影片,视频 +,,,Video Recorded,Video aufgezeichnet,Video Recorded,Vídeo grabado,Vídeo grabado,Vidéo enregistrée,Video registrato,録画したビデオ,녹화 비디오 완료,Vídeo gravado,Vídeo gravado,Видео записано,视频已录制,已錄製影片,视频已录制 +,,,Video Settings,Video-Einstellungen,Video Settings,Configuración de vídeo,Configuración de vídeo,Paramètres vidéo,Impostazioni video,ビデオ設定,비디오 설정,Configurações de vídeo,Configurações de vídeo,Настройки видео,视频设置,影片設定,视频设置 +,,,Volume,Lautstärke,Volume,Volumen,Volumen,Volume,Volume,ボリューム,볼륨,Volume,Volume,Громкость,音量,音量,音量 +,,,W/Up Arrow,W/Aufwärtspfeil,W/Up Arrow,W/Cursor arriba,W/Cursor arriba,W/Flèche haut,W/Freccia SU,W/上カーソル,W/위쪽 화살표,W/seta para cima,W/seta para cima,W/стрелка вверх,W/上箭头,W、↑,W/上箭头 +,,,Waist Accessory,Taillen-Accessoire,Waist Accessory,Accesorio para la cintura,Accesorio para la cintura,Accessoire de taille,Accessorio vita,ウエストアクセサリ,허리 장신구,Acessório de cintura,Acessório de cintura,Аксессуар для талии,腰部配饰,腰部飾品,腰部配饰 +,,,Waiting for an available server. ({COUNT:int}),Warten auf verfügbaren Server. ({COUNT:int}),Waiting for an available server. ({COUNT:int}),Esperando un servidor disponible. ({COUNT:int}),Esperando un servidor disponible. ({COUNT:int}),En attente d'un serveur disponible. ({COUNT:int}),In attesa di un server disponibile. ({COUNT:int}),使用可能なサーバを待機中 ({COUNT:int}),이용 가능한 서버를 기다리는 중 ({COUNT:int}),Aguardando um servidor disponível ({COUNT:int}),Aguardando um servidor disponível ({COUNT:int}),Ожидание доступного сервера ({COUNT:int}),正在等待可用服务器 ({COUNT:int}),正在等待可用伺服器。({COUNT:int}),正在等待可用服务器 ({COUNT:int}) +,,,Waiting for an available server. {RET:translate}...({COUNT:int}),Warten auf verfügbaren Server. {RET:translate}...({COUNT:int}),Waiting for an available server. {RET:translate}...({COUNT:int}),Esperando un servidor disponible. {RET:translate}...({COUNT:int}),Esperando un servidor disponible. {RET:translate}...({COUNT:int}),En attente d'un serveur disponible. {RET:translate}...({COUNT:int}),In attesa di un server disponibile. {RET:translate}...({COUNT:int}),使用可能なサーバを待機中. {RET:translate}...({COUNT:int}),이용 가능한 서버를 기다리는 중. {RET:translate}...({COUNT:int}),Aguardando um servidor disponível. {RET:translate}...({COUNT:int}),Aguardando um servidor disponível. {RET:translate}...({COUNT:int}),Ожидание доступного сервера. {RET:translate}...({COUNT:int}),正在等待可用服务器. {RET:translate}...({COUNT:int}),正在等待可用伺服器。{RET:translate}…({COUNT:int}),正在等待可用服务器. {RET:translate}...({COUNT:int}) +,,,Walk Animation,Gehanimation,Walk Animation,Animación de marcha,Animación de marcha,Animation de marche,Animazione camminata,歩くアニメーション,걷기 애니메이션,Animação de caminhada,Animação de caminhada,Анимация ходьбы,行走动画,步行動畫,行走动画 +,,,Which Player?,Welcher Spieler?,Which Player?,¿A qué jugador?,¿A qué jugador?,Quel joueur ?,Quale giocatore?,どのプレイヤーですか?,플레이어 선택,Qual jogador?,Qual jogador?,Какой игрок?,哪位玩家?,哪位玩家?,哪位玩家? +,,,Wipeouts,Auslöschungen,Wipeouts,Destrucciones,Destrucciones,Éliminations,Annientamenti,死亡回数,사망,Aniquilações,Aniquilações,Уничтожения,死亡数,死亡數,死亡数 +,,,Would you like to unblock {RBX_NAME}?,Möchtest du die Sperre von {RBX_NAME} aufheben?,Would you like to unblock {RBX_NAME}?,¿Quieres desbloquear a {RBX_NAME}?,¿Quieres desbloquear a {RBX_NAME}?,Souhaitez-vous lever le blocage de {RBX_NAME} ?,Vuoi sbloccare {RBX_NAME}?,{RBX_NAME} さんをブロック解除しますか?,{RBX_NAME}님을 차단 해제할까요?,Gostaria de desbloquear {RBX_NAME}?,Gostaria de desbloquear {RBX_NAME}?,Разблокировать пользователя {RBX_NAME}?,你想要解除对“{RBX_NAME}”的屏蔽吗?,確定解除封鎖 {RBX_NAME}?,你想要解除对“{RBX_NAME}”的屏蔽吗? +,,,You already own this item. Your account has not been charged.,Diesen Gegenstand besitzt du bereits. Dein Konto wurde nicht belastet.,You already own this item. Your account has not been charged.,Ya tienes este objeto. No se ha llevado a cabo ningún cobro en tu cuenta.,Ya tienes este objeto. No se ha llevado a cabo ningún cobro en tu cuenta.,Vous possédez déjà cet objet. Votre compte n'a pas été débité.,Possiedi già questo oggetto. Il tuo account non ha subito addebiti.,すでにこのアイテムを持っています。アカウントはチャージされていません。,이미 보유한 아이템입니다. 계정에 비용이 청구되지 않아요.,Você já possui este item. Nada foi cobrado da sua conta.,Você já possui este item. Nada foi cobrado da sua conta.,У вас уже есть этот предмет. Средства с вашего счета не списаны.,你已拥有这件物品。你的帐户未被扣款。,您已擁有此道具,您的帳號餘額維持不變。,你已拥有这件物品。你的帐户未被扣款。 +,,,You are,Du,You are,Estás,Estás,Vous,Sei,あなたは,현재,Você está,Você está,Вы,你是,您在,你是 +,,,You are already playing a game. Please shut down the other game and try again,Du spielst bereits ein Spiel. Bitte brich das andere Spiel ab und versuche es erneut.,You are already playing a game. Please shut down the other game and try again,Ya estás jugando a un juego. Cierra el otro juego e inténtalo de nuevo.,Ya estás jugando a un juego. Cierra el otro juego e inténtalo de nuevo.,"Vous êtes déjà dans un jeu. Veuillez le quitter, puis réessayez.",Stai già giocando a un gioco. Chiudi l'altro gioco e riprova,すでにゲームをプレイ中です。ゲームを終了してもう一度試してください,게임을 플레이 중에요. 다른 게임을 닫고 다시 시도하세요.,Você já está jogando um jogo. Saia do jogo atual e tente novamente.,Você já está jogando um jogo. Saia do jogo atual e tente novamente.,"У вас уже запущена игра. Пожалуйста, закройте ее и попробуйте еще раз.",你已经在游戏中。请关闭其他游戏并重试,您正在玩其它遊戲,請關閉該遊戲並重新嘗試,你已经在游戏中。请关闭其他游戏并重试 +,,,You are too far away to chat!,"Du bist zu weit entfernt, um zu chatten!",You are too far away to chat!,¡Estás demasiado lejos para chatear!,¡Estás demasiado lejos para chatear!,Vous êtes trop loin pour discuter !,Sei troppo lontano per chattare!,遠すぎてチャット出来ません!,너무 멀리 있어서 채팅할 수 없어요!,Você está longe demais para participar do chat!,Você está longe demais para participar do chat!,Вы слишком далеко и не можете общаться в чате!,距离太远,无法聊天!,距離太遠,無法聊天。,距离太远,无法聊天! +,,,You can not send a friend request because you are at the max friend limit.,"Du kannst keine Freundesanfrage senden, da du die max. Anzahl an Freunden erreicht hast.",You can not send a friend request because you are at the max friend limit.,No puedes enviar una solicitud de amistad porque has alcanzado el límite máximo de amigos.,No puedes enviar una solicitud de amistad porque has alcanzado el límite máximo de amigos.,Vous ne pouvez pas envoyer une demande d'ami car vous avez atteint votre limite maximale d'amis.,Non puoi inviare una richiesta di amicizia perché hai raggiunto il limite massimo di amici.,友達の数が上限に達したため友達リクエストが送れません。,최대 친구 목록이 초과되어 친구 요청을 보낼 수 없어요.,Você não pode enviar um pedido de amizade pois você alcançou o limite máximo de amigos.,Você não pode enviar um pedido de amizade pois você alcançou o limite máximo de amigos.,Вы не можете отправить запрос дружбы: количество ваших друзей достигло максимума.,由于你已达到好友数量上限,你无法发送好友邀请。,您的好友人數已達上限,無法傳送好友邀請。,由于你已达到好友数量上限,你无法发送好友邀请。 +,,,"You have been disconnection from Team Create, please reconnect",Verbindung zu Team Create wurde unterbrochen. Bitte erneut verbinden.,"You have been disconnection from Team Create, please reconnect",Te has desconectado de Creación en equipo. Conéctate de nuevo.,Te has desconectado de Creación en equipo. Conéctate de nuevo.,Vous avez été déconnecté(e) de la création d'équipe. Veuillez vous reconnecter.,Sei stato disconnesso dalla creazione della squadra; è necessario riconnettersi,チームクリエイトとの接続が切断されました。再接続してください,팀 만들기 연결이 끊어졌어요. 다시 연결하세요.,Você foi desconectado do Team Create. Conecte-se novamente.,Você foi desconectado do Team Create. Conecte-se novamente.,"Соединение с командой создателей было прервано, пожалуйста, установите соединение повторно.",你已断开与团队开发的连接,请重新连接,與集體創作中斷連線,請重新連線,你已断开与团队开发的连接,请重新连接 +,,,You have been kicked from the game,Du wurdest aus dem Spiel geworfen,You have been kicked from the game,Se te ha expulsado del juego,Se te ha expulsado del juego,Vous avez été expulsé(e) du jeu.,Sei stato espulso dal gioco,ゲームから外されました,게임에서 강제 퇴장되었습니다.,Você foi expulso do jogo,Você foi expulso do jogo,Вас исключили из игры.,你已被游戏踢出,您已被遊戲踢出,你已被游戏踢出 +,,,You have lost the connection to the game,Die Verbindung zum Spiel wurde unterbrochen.,You have lost the connection to the game,Has perdido la conexión con el juego.,Has perdido la conexión con el juego.,Votre connexion au jeu a été perdue.,Sei stato disconnesso dal gioco,ゲームへの接続が失われました。,게임 연결이 끊어졌어요,Você perdeu a conexão com o jogo,Você perdeu a conexão com o jogo,Прервано подключение к игре.,你已中断与游戏的连接,與遊戲的連線中斷,你已中断与游戏的连接 +,,,You have no notifications,Du hast keine Benachrichtigungen.,You have no notifications,No tienes notificaciones,No tienes notificaciones,Vous n'avez aucune notification,Non hai notifiche,お知らせはありません,알림이 없어요,Você não possui notificações,Você não possui notificações,Уведомлений нет,你没有通知,您沒有通知,你没有通知 +,,,You must wait {RBX_NUMBER} second before sending another message!,"Du musst {RBX_NUMBER} Sekunde lang warten, bevor du eine weitere Nachricht senden kannst!",You must wait {RBX_NUMBER} second before sending another message!,¡Debes esperar {RBX_NUMBER} segundo antes de enviar otro mensaje!,¡Debes esperar {RBX_NUMBER} segundo antes de enviar otro mensaje!,Vous devez attendre {RBX_NUMBER} seconde avant d'envoyer un autre message !,Devi aspettare {RBX_NUMBER} secondo prima di inviare un altro messaggio!,{RBX_NUMBER} 秒待ってから次のメッセージを送ってください!,추가 메시지를 보내기 전에 {RBX_NUMBER}초 동안 기다려야 해요!,Você precisa esperar {RBX_NUMBER} segundo antes de enviar outra mensagem!,Você precisa esperar {RBX_NUMBER} segundo antes de enviar outra mensagem!,Вы сможете отправить новое сообщение только через {RBX_NUMBER} сек.!,发送另一条消息前你必须等待 {RBX_NUMBER} 秒!,請在 {RBX_NUMBER} 秒後再傳送新的訊息。,发送另一条消息前你必须等待 {RBX_NUMBER} 秒! +,,,You need {RBX_NUMBER} more {RBX_NAME} to buy this item.,"Du benötigst noch {RBX_NUMBER} weitere {RBX_NAME}, um diesen Gegenstand zu kaufen.",You need {RBX_NUMBER} more {RBX_NAME} to buy this item.,Necesitas {RBX_NUMBER} {RBX_NAME} más para comprar este objeto.,Necesitas {RBX_NUMBER} {RBX_NAME} más para comprar este objeto.,Vous avez besoin de {RBX_NUMBER} {RBX_NAME} de plus pour acheter cet objet.,Hai bisogno di {RBX_NUMBER} {RBX_NAME} in più per comprare questo oggetto.,このアイテムを買うにはあと {RBX_NUMBER} の {RBX_NAME} が必要です。,본 아이템을 구매하려면 {RBX_NAME}이(가) {RBX_NUMBER}개 더 필요해요.,Você precisa de mais {RBX_NUMBER} {RBX_NAME} para comprar este item.,Você precisa de mais {RBX_NUMBER} {RBX_NAME} para comprar este item.,Для покупки этого товара требуется на {RBX_NUMBER} больше {RBX_NAME}.,你还需要 {RBX_NUMBER} 个 “{RBX_NAME}” 才能购买此物品。,您還需要 {RBX_NUMBER} {RBX_NAME} 才能購買此道具。,你还需要 {RBX_NUMBER} 个 “{RBX_NAME}” 才能购买此物品。 +,,,You were kicked from '{RBX_NAME}',Du wurdest aus „{RBX_NAME}“ entfernt.,You were kicked from '{RBX_NAME}',"Se te ha expulsado de ""{RBX_NAME}"".","Se te ha expulsado de ""{RBX_NAME}"".",Vous avez été expulsé de {RBX_NAME},"Sei stato rimosso da ""{RBX_NAME}""",あなたは 「{RBX_NAME}」から外されました。,'{RBX_NAME}'에서 강제 퇴장되었습니다.,Você foi expulso(a) de '{RBX_NAME}',Você foi expulso(a) de '{RBX_NAME}',Вы исключены из «{RBX_NAME}».,“{RBX_NAME}”已将你踢出,您已被踢出「{RBX_NAME}」,“{RBX_NAME}”已将你踢出 +,,,You were kicked from '{RBX_NAME}' for the following reason(s): {RBX_NAME},Du wurdest aus folgenden Gründen aus „{RBX_NAME}“ entfernt: {RBX_NAME},You were kicked from '{RBX_NAME}' for the following reason(s): {RBX_NAME},"Se te ha expulsado de ""{RBX_NAME}"" por el/los motivo/s siguiente/s: {RBX_NAME}","Se te ha expulsado de ""{RBX_NAME}"" por el/los motivo/s siguiente/s: {RBX_NAME}",Vous avez été expulsé de {RBX_NAME} pour la/les raison(s) suivante(s) : {RBX_NAME},"Sei stato rimosso da ""{RBX_NAME}"" per le seguenti motivazioni: {RBX_NAME}",あなたは 「{RBX_NAME}」から次の理由により外されました: {RBX_NAME},'{RBX_NAME}'에서 강제 퇴장되었습니다. 퇴장 이유: {RBX_NAME},Você foi expulso(a) de '{RBX_NAME}‘. Motivo(s): {RBX_NAME},Você foi expulso(a) de '{RBX_NAME}‘. Motivo(s): {RBX_NAME},Вы исключены из «{RBX_NAME}» по следующим причинам: {RBX_NAME},“{RBX_NAME}”已将你踢出,原因为:{RBX_NAME},您因下列原因被踢出「{RBX_NAME}」:{RBX_NAME},“{RBX_NAME}”已将你踢出,原因为:{RBX_NAME} +,,,YouTube Video,YouTube-Video,YouTube Video,Vídeo de YouTube,Vídeo de YouTube,Vidéo YouTube,Video su YouTube,YouTube ビデオ,YouTube 비디오,Vídeo do YouTube,Vídeo do YouTube,Видео YouTube,Youtube 视频,YouTube 影片,Youtube 视频 +,,,Your account balance will not be affected by this transaction.,Das Guthaben deines Kontos wird durch diese Transaktion nicht beeinflusst.,Your account balance will not be affected by this transaction.,Esta transacción no modificará el saldo de tu cuenta.,Esta transacción no modificará el saldo de tu cuenta.,Le solde de votre compte ne sera pas affecté par cette transaction.,I fondi del tuo account non verranno intaccati da questa transazione.,この取引であなたのアカウントは変化しません。,본 거래는 계정 잔액에 영향을 주지 않아요.,O saldo da sua conta não será afetado por esta transação.,O saldo da sua conta não será afetado por esta transação.,После этой операции средства с вашего счета списаны не будут.,你的帐户余额将不会收到此次交易的影响。,您的帳號餘額不受此交易影響。,你的帐户余额将不会收到此次交易的影响。 +,,,Your balance after this transaction will be R${RBX_NUMBER},Nach dieser Transaktion wird dein Guthaben {RBX_NUMBER} R$ betragen.,Your balance after this transaction will be R${RBX_NUMBER},Tu saldo después de esta transacción será de {RBX_NUMBER} R$.,Tu saldo después de esta transacción será de {RBX_NUMBER} R$.,Votre solde après cette transaction sera de {RBX_NUMBER} R$,"Dopo la transazione, avrai R${RBX_NUMBER}",この取引の後の残高は R${RBX_NUMBER}です,본 거래 후 예상 잔액은 R${RBX_NUMBER}입니다.,Seu saldo depois desta transação será de R$ {RBX_NUMBER},Seu saldo depois desta transação será de R$ {RBX_NUMBER},После этой операции ваш баланс составит R${RBX_NUMBER},此次交易后,你的余额将为 R${RBX_NUMBER}。,您在此交易後的餘額將為 R${RBX_NUMBER}。,此次交易后,你的余额将为 R${RBX_NUMBER}。 +,,,Your party is too large to fit,Deine Party ist zu groß,Your party is too large to fit,Tu equipo es demasiado grande,Tu equipo es demasiado grande,Votre groupe comporte trop de membres.,Il tuo gruppo è troppo grande,パーティーが大きすぎてフィットしません,파티원이 너무 많아 참가할 수 없어요,Seu time é muito grande,Seu time é muito grande,Ваша группа слишком велика,你的团队人数过多,无法加入,您的隊伍人數過多,無法加入,你的团队人数过多,无法加入 +,,,Your party is too large to fit. ({COUNT:int}),Deine Party ist zu groß. ({COUNT:int}),Your party is too large to fit. ({COUNT:int}),Tu equipo es demasiado grande. ({COUNT:int}),Tu equipo es demasiado grande. ({COUNT:int}),Votre groupe comporte trop de membres.. ({COUNT:int}),Il tuo gruppo è troppo grande. ({COUNT:int}),パーティーが大きすぎてフィットしません。 ({COUNT:int}),파티원이 너무 많아 참가할 수 없어요. ({COUNT:int}),Seu time é muito grande ({COUNT:int}),Seu time é muito grande ({COUNT:int}),Ваша группа слишком велика ({COUNT:int}),你的团队人数过多,无法加入 ({COUNT:int}),您的隊伍人數過多,無法加入。({COUNT:int}),你的团队人数过多,无法加入 ({COUNT:int}) +,,,Zoom In,Einzoomen,Zoom In,Acercar,Acercar,Zoom avant,Ingrandisci,ズームイン,확대,Aproximar zoom,Aproximar zoom,Приблизить,放大,拉近,放大 +,,,Zoom In/Out,Ein-/Auszoomen,Zoom In/Out,Acercar/alejar,Acercar/alejar,Zoom avant/arrière,Aumenta/Riduci,ズームイン/ズームアウト,확대/축소,Aprox./afastar,Aprox./afastar,Приблизить/отдалить,放大/缩小,拉近 / 拉遠,放大/缩小 +,,,Zoom Out,Auszoomen,Zoom Out,Alejar,Alejar,Zoom arrière,Riduci,ズームアウト,축소,Afastar zoom,Afastar zoom,Отдалить,缩小,拉遠,缩小 +,,,update me,aktualisiere mich,update me,actualízame,actualízame,tenez-moi au courant,aggiornami,自分をアップデート,업데이트 요청,atualize-me,atualize-me,обновите меня,向我发送更新信息,通知我,向我发送更新信息 +,,,"{RBX_NAME1} won {RBX_NAME2}'s ""{RBX_NAME3}"" award!",{RBX_NAME1} wurde von {RBX_NAME2} die Auszeichnung „{RBX_NAME3}“ verliehen!,"{RBX_NAME1} won {RBX_NAME2}'s ""{RBX_NAME3}"" award!","¡{RBX_NAME1} ha ganado el galardón ""{RBX_NAME3}"" de {RBX_NAME2}!","¡{RBX_NAME1} ha ganado el galardón ""{RBX_NAME3}"" de {RBX_NAME2}!",{RBX_NAME1} a gagné la récompense « {RBX_NAME3} » de {RBX_NAME2} !,"{RBX_NAME1} ha vinto il premio ""{RBX_NAME3}"" di {RBX_NAME2}!",{RBX_NAME1} {RBX_NAME2} の「{RBX_NAME3}」を受賞!,"{RBX_NAME1}이(가) {RBX_NAME3}의 ""{RBX_NAME2}"" 상을 획득했어요!","{RBX_NAME1} ganhou o prêmio ""{RBX_NAME3}“ de {RBX_NAME2}!","{RBX_NAME1} ganhou o prêmio ""{RBX_NAME3}“ de {RBX_NAME2}!",{RBX_NAME1} получает награду {RBX_NAME2} «{RBX_NAME3}»!,{RBX_NAME1}赢得了{RBX_NAME2}的“{RBX_NAME3}”奖!,{RBX_NAME1} 贏得 {RBX_NAME2} 的「{RBX_NAME3}」獎!,{RBX_NAME1}赢得了{RBX_NAME2}的“{RBX_NAME3}”奖! +,,,{RBX_NAME} was kicked,{RBX_NAME} wurde entfernt,{RBX_NAME} was kicked,Se ha expulsado a {RBX_NAME}.,Se ha expulsado a {RBX_NAME}.,{RBX_NAME} a été expulsé,{RBX_NAME} è stato rimosso,{RBX_NAME} は外されました。,{RBX_NAME}님이 강제 퇴장되었습니다.,{RBX_NAME} foi expulso(a),{RBX_NAME} foi expulso(a),Пользователь {RBX_NAME} исключен,“{RBX_NAME}”被踢出,{RBX_NAME} 已被踢出,“{RBX_NAME}”被踢出 +,,,{RBX_NAME} was kicked for the following reason(s): {RBX_NAME},{RBX_NAME} wurde aus folgenden Gründen entfernt: {RBX_NAME},{RBX_NAME} was kicked for the following reason(s): {RBX_NAME},Se ha expulsado a {RBX_NAME} por el/los motivo/s siguiente/s: {RBX_NAME},Se ha expulsado a {RBX_NAME} por el/los motivo/s siguiente/s: {RBX_NAME},{RBX_NAME} a été expulsé pour la/les raison(s) suivante(s) : {RBX_NAME},{RBX_NAME} è stato rimosso per le seguenti motivazioni: {RBX_NAME},{RBX_NAME} は次の理由により外されました: {RBX_NAME},{RBX_NAME}님이 강제 퇴장되었습니다. 퇴장 이유: {RBX_NAME},{RBX_NAME} foi expulso(a). Motivo(s): {RBX_NAME},{RBX_NAME} foi expulso(a). Motivo(s): {RBX_NAME},Пользователь {RBX_NAME} исключен по следующим причинам: {RBX_NAME},已将“{RBX_NAME}”踢出,原因为:{RBX_NAME},{RBX_NAME} 因下列原因被踢出:{RBX_NAME},已将“{RBX_NAME}”踢出,原因为:{RBX_NAME} +,,,{RBX_NAME} was muted for the following reason(s): {RBX_NAME},{RBX_NAME} wurde aus folgenden Gründen stummgeschaltet: {RBX_NAME},{RBX_NAME} was muted for the following reason(s): {RBX_NAME},Se ha silenciado a {RBX_NAME} por el/los motivo/s siguiente/s: {RBX_NAME},Se ha silenciado a {RBX_NAME} por el/los motivo/s siguiente/s: {RBX_NAME},{RBX_NAME} a été bâillonné pour la/les raison(s) suivante(s) : {RBX_NAME},{RBX_NAME} non ha più la parola per le seguenti motivazioni: {RBX_NAME},{RBX_NAME} は次の理由によりミュートされました: {RBX_NAME},{RBX_NAME}님이 음소거 되었어요. 음소거 이유: {RBX_NAME},{RBX_NAME} foi silenciado(a). Motivo(s): {RBX_NAME},{RBX_NAME} foi silenciado(a). Motivo(s): {RBX_NAME},Пользователь {RBX_NAME} добавлен в список игнорируемых по следующим причинам: {RBX_NAME},“{RBX_NAME}”被禁言的原因为:{RBX_NAME},{RBX_NAME} 因下列原因被靜音:{RBX_NAME},“{RBX_NAME}”被禁言的原因为:{RBX_NAME} +,,,Waiting for an available server,Warte auf verfügbaren Server,Waiting for an available server,Esperando un servidor disponible,Esperando un servidor disponible,En attente d'un serveur disponible,In attesa di un server disponibile,使用可能なサーバを待機中,이용 가능한 서버를 기다리는 중,Aguardando um servidor disponível,Aguardando um servidor disponível,Ожидание доступного сервера,正在等待可用服务器,等待可用伺服器,正在等待可用服务器 +,,,"Server found, loading...","Server gefunden, wird geladen ...","Server found, loading...",Servidor encontrado. Cargando...,Servidor encontrado. Cargando...,"Serveur trouvé, chargement en cours...","Server trovato, caricamento...",サーバが見つかりました。読み込み中です...,"서버를 찾았습니다, 불러오는 중...","Servidor encontrado, carregando...","Servidor encontrado, carregando...",Сервер найден. Загрузка...,找到服务器,正在加载...,找到伺服器,正在載入…,找到服务器,正在加载... +,,,Reset Character,Charakter zurücksetzen,Reset Character,Reiniciar personaje,Reiniciar personaje,Réinitialiser le personnage,Azzera personaggio,キャラクターをリセット,캐릭터 재설정,Reiniciar,Reiniciar,Сброс персонажа,重置人物,重置人偶,重置人物 +,,,Dynamic Thumbstick,Dynamischer Daumenstick,Dynamic Thumbstick,Pulsador dinámico,Pulsador dinámico,Joystick dynamique,Pattina dinamica,ダイナミックサムスティック,다이나믹 엄지스틱,Thumbstick Dinâmico,Thumbstick Dinâmico,Динамический джойстик,动态摇杆,動態類比搖桿,动态摇杆 +,,,Joining server,Verbindung zum Server wird hergestellt,Joining server,Uniéndose a un servidor,Uniéndose a un servidor,Connexion au serveur...,Connessione al server,サーバーに接続中,서버 입장 중,Entrando no servidor,Entrando no servidor,Подключение к серверу,正在加入服务器,加入伺服器,正在加入服务器 +,,,Sent,Gesendet,Sent,Enviado,Enviado,Envoyé,Inviato,送付済み,보냄,Enviado,Enviado,Отправленные,已发送,已傳送,已发送 +Network,,,(Network),(Netzwerk),(Network),(Red),(Red),(Réseau),(Rete),(ネットワーク),(네트워크),(Rede),(Rede),(Сеть),(网络),(網路),(网络) +GameChat_ChatCommandsTeller_SwitchChannelCommand,,,/c : switch channel menu tabs.,/c : Zum Wechseln zwischen Kanalmenüreitern.,/c : switch channel menu tabs.,/c : alternar pestañas del menú del chat.,/c : alternar pestañas del menú del chat.,/c  : échanger les onglets du menu Canal.,/c : cambia scheda nel menu dei canali.,/c <チャンネル> : チャンネルメニュータブを切り替える。,/c <채널> : 채널 메뉴 탭 전환.,/c : trocar abas de menu de canal.,/c : trocar abas de menu de canal.,/c <канал> : переключить вкладку в меню каналов.,/c : 切换频道菜单标签。,/c <頻道名稱>:切換頻道選單標籤。,/c : 切换频道菜单标签。 +GameChat_ChatCommandsTeller_MeCommand,,,/me : roleplaying command for doing actions.,/me : Rollenspielbefehl für Aktionen.,/me : roleplaying command for doing actions.,/me : comando de rol para realizar acciones.,/me : comando de rol para realizar acciones.,/me  : commande jeu de rôle pour accomplir des actions.,/me : comando per descrivere azioni e giocare di ruolo.,/me <テキスト> : アクションのためのロールプレイングコマンド。,/me <텍스트> : 작업 수행을 위한 역할 놀이 명령어.,/me : comando de RPG para realizar ações.,/me : comando de RPG para realizar ações.,/me <текст> : сообщить о выполнении какого-либо действия.,/me : 做动作的角色扮演指令。,/me <文字>:角色扮演動作的指令。,/me : 做动作的角色扮演指令。 +GameChat_ChatCommandsTeller_MuteCommand,,,/mute : mute a speaker.,/mute : Schaltet einen Teilnehmer stumm.,/mute : mute a speaker.,/mute : silenciar a un usuario.,/mute : silenciar a un usuario.,/mute  : bâillonne un interlocuteur.,/mute : togli la parola a un giocatore.,/mute <スピーカー> : 相手をミュート。,/mute <스피커> : 스피커 음소거.,/mute : silenciar uma pessoa.,/mute : silenciar uma pessoa.,/mute <пользователь> : игнорировать пользователя.,/mute :将发言者禁言。,/mute <使用者名稱> : 將此使用者靜音。,/mute :将发言者禁言。 +GameChat_ChatCommandsTeller_TeamCommand,,,/team or /t : send a team chat to players on your team.,/team oder /t : Sendet eine Teamnachricht an Spieler deines Teams.,/team or /t : send a team chat to players on your team.,/team o /t : enviar un mensaje de chat de equipo a los jugadores de tu equipo.,/team o /t : enviar un mensaje de chat de equipo a los jugadores de tu equipo.,/team ou /t  : envoyer un message aux joueurs de votre équipe.,/team o /t : invia un messaggio a tutti i giocatori della tua squadra.,/team <メッセージ> または /t <メッセージ> : 自分のチームメンバーにチームチャットを送る。,/team <메시지> 또는 /t <메시지> : 팀 내 플레이어에게 팀 채팅 전송.,/team ou /t : enviar um chat de equipe aos jogadores da sua equipe.,/team ou /t : enviar um chat de equipe aos jogadores da sua equipe.,/team <сообщение> или /t <сообщение> : отправить сообщение игрокам из вашей команды.,/team 或 /t : 向你团队的玩家发送团队聊天。,/team <訊息> 或 /t <訊息> : 傳送訊息給隊伍中的玩家。,/team 或 /t : 向你团队的玩家发送团队聊天。 +GameChat_ChatCommandsTeller_UnMuteCommand,,,/unmute : unmute a speaker.,/unmute : Hebt Stummschaltung eines Teilnehmers auf.,/unmute : unmute a speaker.,/unmute : cancelar silencio de un usuario.,/unmute : cancelar silencio de un usuario.,/unmute  : retire le bâillon d'un interlocuteur.,/unmute : restituisci la parola a un giocatore.,/unmute <スピーカー> : 相手のミュートを解除.,/unmute <스피커> : 스피커 음소거 해제.,/unmute : remover silêncio de uma pessoa.,/unmute : remover silêncio de uma pessoa.,/unmute <пользователь> : перестать игнорировать пользователя.,/unmute : 取消发言者禁言。,/unmute <使用者名稱> : 將此使用者解除靜音。,/unmute : 取消发言者禁言。 +GameChat_ChatCommandsTeller_WhisperCommand,,,/whisper or /w : open private message channel with speaker.,/whisper oder /w : Öffnet privaten Nachrichtenkanal mit Teilnehmer.,/whisper or /w : open private message channel with speaker.,/whisper o /w : abrir canal de mensajes privados con un usuario.,/whisper o /w : abrir canal de mensajes privados con un usuario.,/whisper ou /w  : ouvre un canal de discussion privé avec l'interlocuteur.,/whisper o /w : apri un canale privato con un giocatore.,/whisper <スピーカー> または /w <スピーカー> : プライベートメッセージチャンネルを開く,/whisper <스피커> 또는 /w <스피커> : 스피커 채널에서 비공개 메시지 열기.,/whisper ou /w : abrir um canal de mensagem privada com uma pessoa.,/whisper ou /w : abrir um canal de mensagem privada com uma pessoa.,/whisper <пользователь> или /w <пользователь> : открыть канал для личной переписки с пользователем.,/whisper 或 /w : 与此发言者开启私人消息频道。,/whisper <使用者名稱> 或 /w <使用者名稱> : 與此使用者開啟私人訊息頻道。,/whisper 或 /w : 与此发言者开启私人消息频道。 +Average,,,Average,Durchschnitt,Average,Promedio,Promedio,Moyen,Media,平均,평균,Média,Média,Среднее,平均,平均,平均 +GameChat_ChatServiceRunner_ChannelDoesNotExist,,,Channel {RBX_NAME} does not exist.,Kanal „{RBX_NAME}“ existiert nicht.,Channel {RBX_NAME} does not exist.,El canal {RBX_NAME} no existe.,El canal {RBX_NAME} no existe.,Le canal {RBX_NAME} n'existe pas.,Il canale {RBX_NAME} non esiste.,チャンネル {RBX_NAME} は存在しません。,{RBX_NAME} 채널이 없어요.,O canal {RBX_NAME} não existe.,O canal {RBX_NAME} não existe.,Канала «{RBX_NAME}» не существует.,频道“{RBX_NAME}”不存在。,頻道「{RBX_NAME}」不存在。,频道“{RBX_NAME}”不存在。 +GameChat_ChatCommandsTeller_AllChannelWelcomeMessage,,,Chat '/?' or '/help' for a list of chat commands.,"Gib „/?“ oder „/help“ im Chat ein, um eine Liste der Chatbefehle zu erhalten.",Chat '/?' or '/help' for a list of chat commands.,"Envía ""/?"" o ""/help"" para obtener la lista de comandos del chat.","Envía ""/?"" o ""/help"" para obtener la lista de comandos del chat.","Dans le chat, « /? » ou « /help » pour la liste des commandes du chat.","Scrivi ""/?"" o ""/help"" per avere l'elenco dei comandi della chat.",チャットで 「/?」 または 「/help」を入力するとチャットコマンドの一覧を表示します。,채팅창에 '/?' 또는 '/도움말'을 입력하면 채팅 명령어 목록을 볼 수 있어요.,Digite '/?' ou '/help' no chat para ver uma lista de comandos.,Digite '/?' ou '/help' no chat para ver uma lista de comandos.,"Введите «/?» или «/help», чтобы увидеть список команд чата.",Chat '/?' 或 '/help' 可获取聊天指令清单。,輸入「/?」或「/help」取得聊天指令清單。,Chat '/?' 或 '/help' 可获取聊天指令清单。 +GameChat_SwallowGuestChat_Message,,,Create a free account to get access to chat permissions!,"Erstelle ein kostenloses Konto, um Zugriff auf Chatberechtigungen zu erhalten!",Create a free account to get access to chat permissions!,¡Crea una cuenta gratuita para obtener los permisos de acceso al chat!,¡Crea una cuenta gratuita para obtener los permisos de acceso al chat!,Créez un compte gratuit pour accéder aux permissions de chat !,Crea un account gratuito per avere accesso ai permessi della chat!,フリーアカウントを作ってチャット権限にアクセス!,무료 계정을 생성해 채팅 권한을 이용하세요!,Crie uma conta grátis para ter acesso a permissões de chat!,Crie uma conta grátis para ter acesso a permissões de chat!,"Создайте бесплатную учетную запись, чтобы настроить права доступа в чате!",创建免费帐户以获取聊天权限!,若要使用聊天功能,請建立免費帳號。,创建免费帐户以获取聊天权限! +Current,,,Current,Aktuell,Current,Actual,Actual,Actuel,Attuale,現在,현재,Atual,Atual,Текущее,当前,目前,当前 +DisableVoiceKey,,,Disable Voice Chat,Sprachchat deaktivieren,Disable Voice Chat,Desactivar chat de voz,Desactivar chat de voz,Désactiver le chat vocal,Disattiva chat vocale,ボイスチャットを無効にする,음성 채팅 비활성화,Desativar conversa por chat,Desativar conversa por chat,Отключить голосовой чат,停用语音聊天,停用語音聊天,停用语音聊天 +EnableVoiceKey,,,Enable Voice Chat,Sprachchat aktivieren,Enable Voice Chat,Activar chat de voz,Activar chat de voz,Activer le chat vocal,Attiva chat vocale,ボイスチャットを有効にする,음성 채팅 활성화,Ativar conversa por chat,Ativar conversa por chat,Включить голосовой чат,启用语音聊天,啟用語音聊天,启用语音聊天 +key_142,,,New Friend {RBX_NAME},Neuer Freund {RBX_NAME},New Friend {RBX_NAME},Amigo nuevo {RBX_NAME},Amigo nuevo {RBX_NAME},Nouvel ami {RBX_NAME},Nuovo amico {RBX_NAME},新しい友達 {RBX_NAME} さん,새 친구 {RBX_NAME},Novo amigo {RBX_NAME},Novo amigo {RBX_NAME},Новый друг {RBX_NAME},新好友{RBX_NAME},新好友 {RBX_NAME},新好友{RBX_NAME} +KEY_DESCRIPTION_OPTIONAL,,,Optional,Optional,Optional,Opcional,Opcional,Optionnel,Opzionale,オプション,선택 항목,Opcional,Opcional,По усмотрению,可选,可選填,可选 +key_145,,,Points Awarded You received {RBX_NUMBER} points!,Punkte verliehen Du hast {RBX_NUMBER} Punkte erhalten!,Points Awarded You received {RBX_NUMBER} points!,Puntos concedidos ¡Has recibido {RBX_NUMBER} puntos!,Puntos concedidos ¡Has recibido {RBX_NUMBER} puntos!,Points accordés Vous avez reçu {RBX_NUMBER} points !,Punti attribuiti Hai ricevuto {RBX_NUMBER} punti!,ポイント授与 ポイントを {RBX_NUMBER}ゲットしました !,획득한 포인트 {RBX_NUMBER}포인트를 받았어요!,Pontos concedidos Você recebeu {RBX_NUMBER} pontos!,Pontos concedidos Você recebeu {RBX_NUMBER} pontos!,Получены очки Вы получили {RBX_NUMBER} очк.!,已获得点数 你得到了 {RBX_NUMBER} 点!,獲得點數:您得到 {RBX_NUMBER} 點!,已获得点数 你得到了 {RBX_NUMBER} 点! +Received,,,Received,Empfangen,Received,Recibido,Recibido,Reçus,Ricevuti,受信済み,받음,Recebido,Recebido,Принято,已收到,收到,已收到 +BACKPACK_SEARCH,,,Search,Suchen,Search,Buscar,Buscar,Rechercher,Cerca,検索,검색,Pesquisar,Pesquisar,Поиск,搜索,搜尋,搜索 +KEY_DESCRIPTION_SHORT_DECRIPTION_OPTIONAL,,,Short Description (Optional),Kurzbeschreibung (optional),Short Description (Optional),Descripción breve (opcional),Descripción breve (opcional),Description brève (facultatif),Descrizione breve (opzionale),簡単な説明(オプション),신고 내용 입력 (선택),Breve descrição (opcional),Breve descrição (opcional),Короткое описание (необязательно),短描述(可选),簡介(可選填),短描述(可选) +GameChat_MuteSpeaker_SpeakerDoesNotExist,,,Speaker '{RBX_NAME}' does not exist.,Teilnehmer „{RBX_NAME}“ existiert nicht.,Speaker '{RBX_NAME}' does not exist.,"El usuario ""{RBX_NAME}"" no existe.","El usuario ""{RBX_NAME}"" no existe.",L'interlocuteur {RBX_NAME} n'existe pas.,"Il giocatore ""{RBX_NAME}"" non esiste.",「{RBX_NAME}」は存在しません。,스피커 '{RBX_NAME}'이(가) 없어요.,{RBX_NAME}' não existe.,{RBX_NAME}' não existe.,Пользователя {RBX_NAME} не существует.,发言者“{RBX_NAME}”不存在。,使用者「{RBX_NAME}」不存在。,发言者“{RBX_NAME}”不存在。 +GameChat_ChatMain_SpeakerHasBeenBlocked,,,Speaker '{RBX_NAME}' has been blocked.,Teilnehmer „{RBX_NAME}“ wurde gesperrt.,Speaker '{RBX_NAME}' has been blocked.,"Se ha bloqueado al usuario ""{RBX_NAME}"".","Se ha bloqueado al usuario ""{RBX_NAME}"".",L'interlocuteur {RBX_NAME} a été bloqué.,"Hai bloccato il giocatore ""{RBX_NAME}"".",「{RBX_NAME}」はブロック中です。,스피커 '{RBX_NAME}'님을 차단했어요.,{RBX_NAME}' foi bloqueado.,{RBX_NAME}' foi bloqueado.,Пользователь {RBX_NAME} заблокирован.,发言者“{RBX_NAME}”已被屏蔽。,已封鎖使用者「{RBX_NAME}」。,发言者“{RBX_NAME}”已被屏蔽。 +GameChat_ChatMain_SpeakerHasBeenMuted,,,Speaker '{RBX_NAME}' has been muted.,Teilnehmer „{RBX_NAME}“ wurde stummgeschaltet.,Speaker '{RBX_NAME}' has been muted.,"Se ha silenciado al usuario ""{RBX_NAME}"".","Se ha silenciado al usuario ""{RBX_NAME}"".",L'interlocuteur {RBX_NAME} a été bâillonné.,"Hai tolto la parola al giocatore ""{RBX_NAME}"".",「{RBX_NAME}」をミュートしました。,스피커 '{RBX_NAME}'이(가) 음소거되었어요.,{RBX_NAME}' foi silenciado(a).,{RBX_NAME}' foi silenciado(a).,Пользователь {RBX_NAME} добавлен в список игнорируемых.,发言者“{RBX_NAME}”已被禁言。,已將使用者「{RBX_NAME}」靜音。,发言者“{RBX_NAME}”已被禁言。 +GameChat_ChatMain_SpeakerHasBeenUnBlocked,,,Speaker '{RBX_NAME}' has been unblocked.,Sperrung von Teilnehmer „{RBX_NAME}“ wurde aufgehoben.,Speaker '{RBX_NAME}' has been unblocked.,"Se ha desbloqueado al usuario ""{RBX_NAME}"".","Se ha desbloqueado al usuario ""{RBX_NAME}"".",L'interlocuteur {RBX_NAME} n'est plus bloqué.,"Hai sbloccato il giocatore ""{RBX_NAME}"".",「{RBX_NAME}」のブロックが解除されました。,스피커 '{RBX_NAME}'님 차단을 해제했어요.,{RBX_NAME}' foi desbloqueado.,{RBX_NAME}' foi desbloqueado.,Пользователь {RBX_NAME} разблокирован.,发言者“{RBX_NAME}”已被取消屏蔽。,已解除封鎖使用者「{RBX_NAME}」。,发言者“{RBX_NAME}”已被取消屏蔽。 +GameChat_ChatMain_SpeakerHasBeenUnMuted,,,Speaker '{RBX_NAME}' has been unmuted.,Stummschaltung von Teilnehmer „{RBX_NAME}“ wurde aufgehoben.,Speaker '{RBX_NAME}' has been unmuted.,"Se ha cancelado el silencio del usuario ""{RBX_NAME}"".","Se ha cancelado el silencio del usuario ""{RBX_NAME}"".",L'interlocuteur {RBX_NAME} n'est plus bâillonné.,"Hai restituito la parola al giocatore ""{RBX_NAME}"".",「{RBX_NAME}」のミュートを解除しました。,스피커 '{RBX_NAME}'의 음소거가 해제되었어요.,O silêncio de '{RBX_NAME}' foi removido.,O silêncio de '{RBX_NAME}' foi removido.,Пользователь {RBX_NAME} удален из списка игнорируемых.,发言者“{RBX_NAME}”已被取消禁言。,已將使用者「{RBX_NAME}」解除靜音。,发言者“{RBX_NAME}”已被取消禁言。 +GameChat_ChatMain_ChatBarTextTouch,,,Tap here to chat,Tippe zum Chatten hier.,Tap here to chat,Toca aquí para chatear,Toca aquí para chatear,Touchez ici pour discuter,Tocca qui per chattare,ここをタップしてチャットする,여기를 클릭한 후 내용을 입력하세요,Toque aqui para escrever,Toque aqui para escrever,"Коснитесь здесь, чтобы общаться в чате",轻点此处以聊天,按下此處聊天,轻点此处以聊天 +Target,,,Target,Ziel,Target,Objetivo,Objetivo,Cible,Scopo,目標,대상,Alvo,Alvo,Цель,目标,目標,目标 +GameChat_ChatService_ChatFilterIssues,,,The chat filter is currently experiencing issues and messages may be slow to appear.,Es gibt derzeit Probleme mit dem Chatfilter. Nachrichten können deshalb mit Verzögerung angezeigt werden.,The chat filter is currently experiencing issues and messages may be slow to appear.,El filtro del chat sufre problemas en este momento y es posible que los mensajes tarden un poco en aparecer.,El filtro del chat sufre problemas en este momento y es posible que los mensajes tarden un poco en aparecer.,Le filtre de chat connaît actuellement des problèmes et les messages pourraient mettre du temps à apparaître.,Il filtro della chat sta riscontrando dei problemi e i messaggi potrebbero apparire in ritardo.,現在、チャットフィルターに問題があるためメッセージの表示が遅れています。,현재 채팅 필터에 문제가 있어 메시지 표시가 느릴 수 있어요.,O filtro de chat está com problemas no momento e as mensagens podem demorar para aparecer.,O filtro de chat está com problemas no momento e as mensagens podem demorar para aparecer.,Могут возникать задержки в передаче сообщений из-за проблем с фильтром чата.,聊天过滤器当前遇到问题,消息显示可能出现延迟。,文字過濾系統發生問題,訊息可能會延遲顯示。,聊天过滤器当前遇到问题,消息显示可能出现延迟。 +GameChat_ChatCommandsTeller_Desc,,,These are the basic chat commands.,Das sind die grundlegenden Chatbefehle.,These are the basic chat commands.,Estos son los comandos básicos del chat.,Estos son los comandos básicos del chat.,Voici les commandes de chat basiques.,Questi sono i comandi base della chat.,これらは基本的なチャットコマンドです。,기본 채팅 명령어에요.,Esses são os comandos de chat básicos.,Esses são os comandos de chat básicos.,Это основные команды чата.,这些是基本聊天指令。,以下是基本聊天室指令。,这些是基本聊天指令。 +GameChat_ChatServiceRunner_SystemChannelWelcomeMessage,,,This channel is for system and game notifications.,Dieser Kanal ist für System- und Spielbenachrichtigungen.,This channel is for system and game notifications.,Este canal es para notificaciones del sistema y del juego.,Este canal es para notificaciones del sistema y del juego.,Ce canal est réservé aux notifications système et de jeu.,Questo canale è per le notifiche di gioco e del sistema.,このチャンネルはシステムとゲーム通知のためのものです。,이 채널은 시스템 및 게임 알림용이에요.,Este canal é destinado a notificações do sistema e jogo.,Este canal é destinado a notificações do sistema e jogo.,Этот канал предназначен для системных и игровых уведомлений.,此频道用于发送系统及游戏通知。,此頻道為系統及遊戲通知專用。,此频道用于发送系统及游戏通知。 +GameChat_GetVersion_Message,,,This game is running chat version [{RBX_NUMBER}.{RBX_NUMBER}].,Die Chatversion dieses Spiels ist [{RBX_NUMBER}.{RBX_NUMBER}].,This game is running chat version [{RBX_NUMBER}.{RBX_NUMBER}].,Este juego utiliza la versión del chat [{RBX_NUMBER} {RBX_NUMBER}].,Este juego utiliza la versión del chat [{RBX_NUMBER} {RBX_NUMBER}].,Le jeu utilise la version de chat [{RBX_NUMBER} {RBX_NUMBER}].,Questo gioco usa la versione [{RBX_NUMBER}.{RBX_NUMBER}] della chat.,このゲームはチャットバージョン [{RBX_NUMBER}.{RBX_NUMBER}]を実行しています。,이 게임은 채팅 버전 [{RBX_NUMBER}. {RBX_NUMBER}]을(를) 실행합니다.,Este jogo está rodando a versão de chat [{RBX_NUMBER}.{RBX_NUMBER}].,Este jogo está rodando a versão de chat [{RBX_NUMBER}.{RBX_NUMBER}].,Эта игра поддерживает чат версии [{RBX_NUMBER}.{RBX_NUMBER}].,此游戏正在运行聊天版本 [{RBX_NUMBER}.{RBX_NUMBER}]。,此遊戲正在使用聊天室版本 [{RBX_NUMBER}.{RBX_NUMBER}]。,此游戏正在运行聊天版本 [{RBX_NUMBER}.{RBX_NUMBER}]。 +GameChat_TeamChat_WelcomeMessage,,,This is a private channel between you and your team members.,Dies ist ein privater Kanal für dich und deine Teammitglieder.,This is a private channel between you and your team members.,Este es un canal privado entre tú y los miembros de tu equipo.,Este es un canal privado entre tú y los miembros de tu equipo.,Ceci est un canal privé entre les membres de votre équipe et vous.,Questo è un canale privato tra te e i membri della tua squadra.,これはあなたとあなたのチームメンバーとのプライベートチャンネルです。,회원님과 팀원 간의 비공개 채널이에요.,Este é um canal privado entre você e os membros da sua equipe.,Este é um canal privado entre você e os membros da sua equipe.,Это канал для личного общения участников вашей команды.,这是你与团队成员之间的私人频道。,這是您與隊伍成員的私人頻道。,这是你与团队成员之间的私人频道。 +GameChat_ChatMain_ChatBarText,,,"To chat click here or press ""/"" key",Klicke zum Chatten hier oder drücke die „/“-Taste.,"To chat click here or press ""/"" key","Para chatear, haz clic aquí o pulsa la tecla ""/"".","Para chatear, haz clic aquí o pulsa la tecla ""/"".","Pour discuter, cliquez ici ou sur la touche « / »","Per chattare, clicca qui o premi il tasto ""/""",チャットするにはここをクリックするか 「 / 」 キーを押します。,"여기를 클릭하거나 ""/"" 키를 누른 후 채팅을 시작하세요","Para escrever clique aqui ou aperte a tecla ""/""","Para escrever clique aqui ou aperte a tecla ""/""","Чтобы общаться в чате, нажмите здесь или на клавишу «/»",若要聊天,请点按此处或按下“/”键,若要聊天,請按下此處或「/」鍵,若要聊天,请点按此处或按下“/”键 +GameChat_ChatChannel_MutedInChannel,,,You are muted and cannot talk in this channel,Du wurdest stummgeschaltet und kannst in diesem Kanal nicht kommunizieren.,You are muted and cannot talk in this channel,Se te ha silenciado y no puedes hablar en este canal.,Se te ha silenciado y no puedes hablar en este canal.,Vous êtes bâillonné et ne pouvez pas parler sur ce canal,Non hai più la parola e non puoi chattare in questo canale,あなたはミュートされこのチャンネルで話すことは出来ません。,이 채널에서 음소거되어 이야기할 수 없어요,Você está silenciado(a) e não pode falar neste canal,Você está silenciado(a) e não pode falar neste canal,Вы добавлены в список игнорируемых и не можете общаться на этом канале,你已被禁言,无法在此频道聊天,您遭到靜音,無法在此頻道聊天,你已被禁言,无法在此频道聊天 +GameChat_PrivateMessaging_CannotChat,,,You are not able to chat with this player.,Du kannst mit diesem Spieler nicht chatten.,You are not able to chat with this player.,No puedes chatear con este jugador.,No puedes chatear con este jugador.,Vous ne pouvez pas discuter avec ce joueur.,Non puoi chattare con questo giocatore.,このプレイヤーとチャットすることは出来ません。,이 플레이어와 채팅할 수 없어요.,Você não pode participar de chat com este jogador.,Você não pode participar de chat com este jogador.,Вы не можете общаться в чате с этим игроком.,你无法与此玩家聊天。,您無法與此玩家聊天。,你无法与此玩家聊天。 +GameChat_ChatServiceRunner_YouAreNotInChannel,,,You are not in channel {RBX_NAME},Du befindest dich nicht in Kanal „{RBX_NAME}“.,You are not in channel {RBX_NAME},No estás en el canal {RBX_NAME}.,No estás en el canal {RBX_NAME}.,Vous n'êtes pas sur le canal {RBX_NAME},Non ti trovi nel canale {RBX_NAME},あなたはチャンネル {RBX_NAME} にいません。,{RBX_NAME} 채널에 있지 않아요,Você não está no canal {RBX_NAME},Você não está no canal {RBX_NAME},Вы не на канале «{RBX_NAME}»,你不在频道“{RBX_NAME}”,您不在 {RBX_NAME} 頻道,你不在频道“{RBX_NAME}” +GameChat_SwitchChannel_NotInChannel,,,You are not in channel: '{RBX_NAME}',Du befindest dich nicht in Kanal „{RBX_NAME}“.,You are not in channel: '{RBX_NAME}',"No estás en el canal ""{RBX_NAME}"".","No estás en el canal ""{RBX_NAME}"".",Vous n'êtes pas sur le canal : {RBX_NAME},"Non ti trovi nel canale: ""{RBX_NAME}""",あなたはチャネル: 「{RBX_NAME}」にいません。,{RBX_NAME}' 채널에 있지 않아요,Você não está no canal: '{RBX_NAME}',Você não está no canal: '{RBX_NAME}',Вы не на канале «{RBX_NAME}»,你不在频道:“{RBX_NAME}”,您不在「{RBX_NAME}」頻道,你不在频道:“{RBX_NAME}” +GameChat_TeamChat_NowInTeam,,,You are now on the '{RBX_NAME}' team.,Du bist nun Mitglied im Team „{RBX_NAME}“.,You are now on the '{RBX_NAME}' team.,"Ahora formas parte del equipo ""{RBX_NAME}"".","Ahora formas parte del equipo ""{RBX_NAME}"".",Vous êtes désormais dans l'équipe {RBX_NAME}.,"Ora sei nella squadra ""{RBX_NAME}"".",あなたは現在「{RBX_NAME}」チームに所属しています。,현재 '{RBX_NAME}'팀에 속해 있어요.,Você agora está na equipe '{RBX_NAME}'.,Você agora está na equipe '{RBX_NAME}'.,Вы вступили в команду {RBX_NAME}.,你正在团队“{RBX_NAME}”中。,您在「{RBX_NAME}」隊伍。,你正在团队“{RBX_NAME}”中。 +GameChat_PrivateMessaging_NowChattingWith,,,You are now privately chatting with {RBX_NAME},Du unterhältst dich nun privat mit {RBX_NAME}.,You are now privately chatting with {RBX_NAME},Estás chateando en privado con {RBX_NAME}.,Estás chateando en privado con {RBX_NAME}.,"Maintenant, vous discutez en privé avec {RBX_NAME}",Ora stai chattando in privato con {RBX_NAME},あなたは現在、{RBX_NAME} さんとプライベートチャット中です。,현재 {RBX_NAME}님과 비공개 채팅 중이에요,Você agora está em um chat privado com {RBX_NAME},Você agora está em um chat privado com {RBX_NAME},Открыт канал для личного общения с пользователем {RBX_NAME},你正在与“{RBX_NAME}”私聊,您正在與{RBX_NAME}私聊,你正在与“{RBX_NAME}”私聊 +GameChat_ChatServiceRunner_YouCannotJoinChannel,,,You cannot join channel {RBX_NAME},Du kannst Kanal „{RBX_NAME}“ nicht beitreten.,You cannot join channel {RBX_NAME},No puedes unirte al canal {RBX_NAME}.,No puedes unirte al canal {RBX_NAME}.,Vous ne pouvez pas rejoindre le canal {RBX_NAME},Non puoi accedere al canale {RBX_NAME},チャンネル {RBX_NAME} に参加することは出来ません。,{RBX_NAME} 채널에 가입할 수 없어요,Você não pode entrar no canal {RBX_NAME},Você não pode entrar no canal {RBX_NAME},Вы не можете подключиться к каналу «{RBX_NAME}»,你无法加入频道“{RBX_NAME}”,您無法加入{RBX_NAME} 頻道,你无法加入频道“{RBX_NAME}” +GameChat_ChatServiceRunner_YouCannotLeaveChannel,,,You cannot leave channel {RBX_NAME},Du kannst Kanal „{RBX_NAME}“ nicht verlassen.,You cannot leave channel {RBX_NAME},No puedes salir del canal {RBX_NAME}.,No puedes salir del canal {RBX_NAME}.,Vous ne pouvez pas quitter le canal {RBX_NAME},Non puoi lasciare il canale {RBX_NAME},チャンネル {RBX_NAME} を終了できません。,{RBX_NAME} 채널에서 나갈 수 없어요,Você não pode sair do canal {RBX_NAME},Você não pode sair do canal {RBX_NAME},Вы не можете покинуть канал «{RBX_NAME}».,你无法离开频道“{RBX_NAME}”,您無法離開 {RBX_NAME} 頻道,你无法离开频道“{RBX_NAME}” +GameChat_ChatService_CannotLeaveChannel,,,You cannot leave this channel.,Du kannst diesen Kanal nicht verlassen.,You cannot leave this channel.,No puedes salir de este canal.,No puedes salir de este canal.,Vous ne pouvez pas quitter ce canal.,Non puoi lasciare questo canale.,このチャンネルを終了できません。,채널에서 나갈 수 없어요.,Você não pode sair deste canal.,Você não pode sair deste canal.,Вы не можете покинуть этот канал.,你无法离开此频道。,您無法離開此頻道。,你无法离开此频道。 +GameChat_DoMuteCommand_CannotMuteSelf,,,You cannot mute yourself.,Du kannst dich nicht selbst stummschalten.,You cannot mute yourself.,No puedes silenciarte a ti mismo.,No puedes silenciarte a ti mismo.,Vous ne pouvez pas vous bâillonner.,Non puoi togliere la parola a te stesso.,自分をミュートすることは出来ません。,자신을 음소거할 수 없어요.,Você não pode silenciar a si mesmo.,Você não pode silenciar a si mesmo.,Невозможно добавить себя в список игнорируемых.,你无法将自己禁言。,您無法將自己靜音。,你无法将自己禁言。 +GameChat_TeamChat_CannotTeamChatIfNotInTeam,,,You cannot team chat if you are not on a team!,"Teamchat ist nur verfügbar, wenn du Mitglied eines Teams bist!",You cannot team chat if you are not on a team!,¡No puedes chatear con tu equipo si no formas parte de un equipo!,¡No puedes chatear con tu equipo si no formas parte de un equipo!,Vous ne pouvez pas avoir de discussion d'équipe si vous n'appartenez pas à une équipe !,Non puoi chattare con la squadra se non sei in una squadra!,チームに所属していなければチームチャットは出来ません。,팀에 속하지 않으면 팀 채팅을 이용할 수 없어요!,Você não pode participar de chat de equipe se não estiver em uma!,Você não pode participar de chat de equipe se não estiver em uma!,"Вы не можете общаться в командном чате, если не состоите в команде!",如果你不在该团队,则无法进行团队聊天。,若您不在隊伍中,您無法使用隊伍頻道。,如果你不在该团队,则无法进行团队聊天。 +GameChat_PrivateMessaging_CannotWhisperToSelf,,,You cannot whisper to yourself.,Du kannst dir nicht selbst etwas zuflüstern.,You cannot whisper to yourself.,No puedes enviarte mensajes privados a ti mismo.,No puedes enviarte mensajes privados a ti mismo.,Vous ne pouvez pas murmurer à votre propre oreille.,Non puoi aprire un canale privato con te stesso.,自分自身に話しかけることは出来ません。,자신에게 귓속말할 수 없어요.,Você não pode sussurrar para si mesmo.,Você não pode sussurrar para si mesmo.,Нельзя отправлять себе личные сообщения.,你无法与自己开启私人频道。,您無法與自己開啟私人頻道。,你无法与自己开启私人频道。 +GameChat_ChatService_YouHaveLeftChannel,,,You have left channel '{RBX_NAME}',Du hast Kanal „{RBX_NAME}“ verlassen.,You have left channel '{RBX_NAME}',"Has salido del canal ""{RBX_NAME}"".","Has salido del canal ""{RBX_NAME}"".",Vous avez quitté le canal {RBX_NAME},"Hai lasciato il canale ""{RBX_NAME}""",チャンネル 「{RBX_NAME}」を退出しました。,{RBX_NAME}' 채널에서 나왔어요,Você saiu do canal '{RBX_NAME}',Você saiu do canal '{RBX_NAME}',Вы покинули канал «{RBX_NAME}»,你已离开频道“{RBX_NAME}”,您已離開「{RBX_NAME}」頻道,你已离开频道“{RBX_NAME}” +GameChat_ChatFloodDetector_Message,,,You must wait before sending another message!,"Du musst warten, bevor du eine weitere Nachricht senden kannst!",You must wait before sending another message!,¡Debes esperar antes de enviar otro mensaje!,¡Debes esperar antes de enviar otro mensaje!,Vous devez attendre avant d'envoyer un autre message !,Devi aspettare prima di inviare un altro messaggio!,少し待ってから次のメッセージを送ってください!,추가 메시지를 보내기 전에 기다려야 해요!,Você precisa esperar antes de enviar outra mensagem!,Você precisa esperar antes de enviar outra mensagem!,"Необходимо подождать, прежде чем отправлять новое сообщение!",发送另一条消息前你必须等待!,請稍後再傳送訊息。,发送另一条消息前你必须等待! +GameChat_ChatFloodDetector_MessageDisplaySeconds,,,You must wait {RBX_NUMBER} seconds before sending another message!,"Du musst {RBX_NUMBER} Sekunden lang warten, bevor du eine weitere Nachricht senden kannst!",You must wait {RBX_NUMBER} seconds before sending another message!,¡Debes esperar {RBX_NUMBER} segundos antes de enviar otro mensaje!,¡Debes esperar {RBX_NUMBER} segundos antes de enviar otro mensaje!,Vous devez attendre {RBX_NUMBER} secondes avant d'envoyer un autre message !,Devi aspettare {RBX_NUMBER} secondi prima di inviare un altro messaggio!,{RBX_NUMBER} 秒待ってから次のメッセージを送ってください!,추가 메시지를 보내기 전에 {RBX_NUMBER}초 동안 기다려야 해요!,Você precisa esperar {RBX_NUMBER} segundos antes de enviar outra mensagem!,Você precisa esperar {RBX_NUMBER} segundos antes de enviar outra mensagem!,Вы сможете отправить новое сообщение только через {RBX_NUMBER} сек.!,发送另一条消息前你必须等待 {RBX_NUMBER} 秒!,請 {RBX_NUMBER} 秒後再傳送訊息。,发送另一条消息前你必须等待 {RBX_NUMBER} 秒! +KEY_PLAYER_IDLE_DISCONNECT,,,You were disconnected for being idle {RBX_NUMBER} minutes,"Deine Verbindung wurde getrennt, da du {RBX_NUMBER} Minuten lang untätig warst.",You were disconnected for being idle {RBX_NUMBER} minutes,Se te ha desconectado por no mostrar actividad durante {RBX_NUMBER} minutos.,Se te ha desconectado por no mostrar actividad durante {RBX_NUMBER} minutos.,Vous avez été déconnecté car vous êtes resté inactif durant {RBX_NUMBER} minutes.,Sei stato disconnesso perché inattivo per {RBX_NUMBER} minuti,{RBX_NUMBER} 分間アイドル状態だったので切断されました。,{RBX_NUMBER}분 동안 기본 상태로 있어 연결이 끊겼어요,Você perdeu a conexão por ficar inativo(a) por {RBX_NUMBER} minutos,Você perdeu a conexão por ficar inativo(a) por {RBX_NUMBER} minutos,"Вы отключены, так как не были активны в течение {RBX_NUMBER} мин.",由于闲置超过 {RBX_NUMBER} 分钟,你已被断开,您已閒置 {RBX_NUMBER} 分鐘,連線中斷,由于闲置超过 {RBX_NUMBER} 分钟,你已被断开 +GameChat_ChatMessageValidator_SettingsError,,,Your chat settings prevent you from sending messages.,Aufgrund deiner Chateinstellungen kannst du keine Nachrichten senden.,Your chat settings prevent you from sending messages.,Tu configuración de chat te impide enviar mensajes.,Tu configuración de chat te impide enviar mensajes.,Vos paramètres de chat vous empêchent d'envoyer des messages.,Non puoi inviare messaggi per le impostazioni della tua chat.,メッセージが送れないチャット設定です。,채팅 설정 때문에 메시지를 보낼 수 없어요.,Suas configurações de chat impedem que você envie mensagens.,Suas configurações de chat impedem que você envie mensagens.,В ваших настройках чата заблокирована возможность отправлять сообщения.,你的聊天设置禁止你发送消息。,您的聊天設定禁止您傳送訊息。,你的聊天设置禁止你发送消息。 +GameChat_FriendChatNotifier_JoinMessage,,,Your friend {RBX_NAME} has joined the game.,Dein Freund {RBX_NAME} ist dem Spiel beigetreten.,Your friend {RBX_NAME} has joined the game.,Tu amigo {RBX_NAME} se ha unido al juego.,Tu amigo {RBX_NAME} se ha unido al juego.,Votre ami {RBX_NAME} a rejoint le jeu.,Il tuo amico {RBX_NAME} è entrato nel gioco.,あなたの友人、{RBX_NAME} さんがゲームに参加しました。,친구 {RBX_NAME}님이 게임에 가입했어요.,Seu amigo {RBX_NAME} juntou-se ao jogo.,Seu amigo {RBX_NAME} juntou-se ao jogo.,Ваш друг {RBX_NAME} присоединился к игре.,你的朋友“{RBX_NAME}”已加入游戏。,您的好友 {RBX_NAME} 已加入遊戲。,你的朋友“{RBX_NAME}”已加入游戏。 +GameChat_ChatMessageValidator_WhitespaceError,,,Your message contains whitespace that is not allowed.,Deine Nachricht enthält unzulässige Leerräume.,Your message contains whitespace that is not allowed.,Tu mensaje contiene espacios vacíos que no se permiten.,Tu mensaje contiene espacios vacíos que no se permiten.,Votre message contient des espaces blancs qui sont interdits.,Il tuo messaggio contiene spazi vuoti non consentiti.,メッセージに許可されていないスペースが含まれています。,메시지에 허용되지 않는 여백이 있어요.,"Sua mensagem contém um espaço em branco, que não é permitido.","Sua mensagem contém um espaço em branco, que não é permitido.",Ваше сообщение содержит недопустимый пробел.,你的消息包含不被允许的空格。,訊息禁止使用空白字元。,你的消息包含不被允许的空格。 +GameChat_ChatMessageValidator_MaxLengthError,,,Your message exceeds the maximum message length.,Deine Nachricht überschreitet die zulässige Nachrichtenlänge.,Your message exceeds the maximum message length.,Tu mensaje supera la longitud máxima permitida.,Tu mensaje supera la longitud máxima permitida.,Votre message dépasse la longueur maximale.,Il tuo messaggio supera la lunghezza massima consentita.,メッセージが最大文字数を超えています。,메시지 길이 한도를 초과했어요.,Sua mensagem ultrapassa o tamanho máximo de mensagem.,Sua mensagem ultrapassa o tamanho máximo de mensagem.,Превышено максимально допустимое количество символов в сообщении.,你的消息已超过最大长度限制。,您的訊息超過長度限制。,你的消息已超过最大长度限制。 +GameChat_SwitchChannel_NowInChannel,,,You are now chatting in channel: '{RBX_NAME}',Du chattest jetzt in Kanal „{RBX_NAME}“.,You are now chatting in channel: '{RBX_NAME}',"Estás chateando en el canal ""{RBX_NAME}"".","Estás chateando en el canal ""{RBX_NAME}"".","Maintenant, vous discutez sur le canal : {RBX_NAME}","Ora stai parlando nel canale: ""{RBX_NAME}""",あなたの現在のチャットチャンネルは: 「{RBX_NAME}」です。,'{RBX_NAME}' 채널에서 채팅 중이에요,Você agora está no canal de chat: '{RBX_NAME}',Você agora está no canal de chat: '{RBX_NAME}',Вы общаетесь на канале «{RBX_NAME}»,你当前的聊天频道为:“{RBX_NAME}”,您目前在「{RBX_NAME}」頻道聊天,你当前的聊天频道为:“{RBX_NAME}” +Sent,,,Sent,Gesendet,Sent,Enviados,Enviados,Envoyé,Inviati,送信しました,보냄,Enviado,Enviado,Отправлено,已发送,已傳送,已发送 +LocalizationTools.Main.RibbonBarButton,,,Tools,,Tools,Herramientas,Herramientas,Outils,,ツール,도구,,,Инструменты,工具,工具,工具 +LocalizationTools.Main.WindowTitle,,,Localization Tools,,Localization Tools,Herramientas de localización,Herramientas de localización,Outils de localisation,,多言語化ツール,로컬리제이션 도구,,,Инструменты локализации,本地化工具,本地化工具,本地化工具 +LocalizationTools.Main.ToolTipMessage,,,Hide/show the Localization Tools View,,Hide/show the Localization Tools View,Ocultar o mostrar la visualización de las herramientas de localización,Ocultar o mostrar la visualización de las herramientas de localización,Cacher/montrer la visualisation des outils de localization,,多言語化ツールの非表示/表示,로컬리제이션 도구 보기 숨기기/표시,,,Показать/скрыть обзор инструментов локализации,隐藏/显示本地化工具视图,隱藏 / 顯示本地化工具檢視模式,隐藏/显示本地化工具视图 +LocalizationTools.EmbeddedTableSection.TextCaptureStartText,,,Start untranslated text capture,,Start untranslated text capture,Empezar la captura del texto no traducido,Empezar la captura del texto no traducido,Commencer la capture du texte non traduit,,未翻訳のテキスト抽出を開始,번역되지 않은 텍스트 캡처 시작,,,Начать захват непереведенного текста,开始抓取未翻译的文本,開始擷取未翻譯文字,开始抓取未翻译的文本 +LocalizationTools.EmbeddedTableSection.TextCaptureStopText,,,Stop untranslated text capture,,Stop untranslated text capture,Detener la captura del texto no traducido,Detener la captura del texto no traducido,Arrêter la capture du text non traduit,,未翻訳のテキスト抽出を停止,번역되지 않은 텍스트 캡처 중지,,,Остановить захват непереведенного текста,停止抓取未翻译的文本,停止擷取未翻譯文字,停止抓取未翻译的文本 +LocalizationTools.EmbeddedTableSection.SectionLabel,,,Embedded Localization Table,,Embedded Localization Table,Tabla de localización insertada,Tabla de localización insertada,Tableau de localization intégré,,組み込み済み多言語化テーブル,임베디드 로컬리제이션 테이블,,,Встроенная локализационная таблица,嵌入本地化表格,內嵌本地化表格,嵌入本地化表格 +LocalizationTools.EmbeddedTableSection.TextCaptureButton,,,Text Capture,,Text Capture,Captura del texto,Captura del texto,Capture du texte,,テキスト抽出,텍스트 캡처,,,Захват текста,文本抓取,文字擷取,文本抓取 +LocalizationTools.EmbeddedTableSection.ExportButton,,,Export,,Export,Exportar,Exportar,Exporter,,書き出す,내보내기,,,Экспорт,导出,匯出,导出 +LocalizationTools.EmbeddedTableSection.ImportButton,,,Import,,Import,Importar,Importar,Importer,,インポート,가져오기,,,Импорт,导入,匯入,导入 +LocalizationTools.EmbeddedTableSection.ExportTextLabel,,,Export LocalizationTables under LocalizationService to CSV files,,Export LocalizationTables under LocalizationService to CSV files,Exportar LocalizationTables bajo LocalizationService a archivos CSV,Exportar LocalizationTables bajo LocalizationService a archivos CSV,Exporter Tableaux de Localization sous Service Localisation vers des fichiers CSV,,LocalizationService の下の LocalizationTables をCSVファイルをエクスポート,LocalizationService 아래의 LocalizationTables를 CSV 파일로 내보내기,,,Экспортировать LocalizationTables через LocalizationService в CSV-файлы,在 LocalizationService 下导出 LocalizationTables 至 CSV 文件,將 LocalizationService 底下的 LocalizationTable 匯出為 CSV 檔案,在 LocalizationService 下导出 LocalizationTables 至 CSV 文件 +LocalizationTools.EmbeddedTableSection.ImportTextLabel,,,Import CSV files to LocalizationTables under LocalizationService,,Import CSV files to LocalizationTables under LocalizationService,Importar archivos CSV a LocalizationTables bajo LocalizationService,Importar archivos CSV a LocalizationTables bajo LocalizationService,Importer des fichiers CSV vers Tableaux de Localization sous Service Localization,,LocalizationService の中の LocalizationTables にCSVファイルをインポート,CSV 파일을 LocalizationService 아래의 LocalizationTables로 가져오기,,,Импортировать CSV-файлы в LocalizationTables через LocalizationService,在 LocalizationService 下导入 LocalizationTables 至 CSV 文件,將 CSV 檔案匯入為 LocalizationService 底下的 LocalizationTable,在 LocalizationService 下导入 LocalizationTables 至 CSV 文件 +LocalizationTools.GameTableSection.CloudTablePageLinkText,,,Click here to configure your cloud localization table,,Click here to configure your cloud localization table,Haz clic aquí para configurar la tabla de localización en la nube,Haz clic aquí para configurar la tabla de localización en la nube,Cliquer ici pour configurer votre tableau de localization en cloud,,ここをクリックして、クラウドローカライゼーションテーブルを環境設定,클라우드 로컬리제이션 테이블을 구성하려면 여기를 클릭하세요.,,,"Щелкните здесь, чтобы настроить облачную локализационную таблицу",点按这里来配置你的云本地化表格,按下此處設定雲端本地化表格,点按这里来配置你的云本地化表格 +LocalizationTools.GameTableSection.DownloadButton,,,Download,,Download,Descargar,Descargar,Télécharger,,ダウンロード,다운로드,,,Загрузить,下载,下載,下载 +LocalizationTools.GameTableSection.DownloadTableLabel,,,Download table as CSV,,Download table as CSV,Descargar la tabla como CSV,Descargar la tabla como CSV,Télécharger le tableau en format CSV,,CSVとしてテーブルをダウンロード,테이블을 CSV로 다운로드,,,Загрузить таблицу как CSV,下载表格并保存至 CSV 格式,將表格下載為 CSV 檔案,下载表格并保存至 CSV 格式 +LocalizationTools.GameTableSection.UpdateButton,,,Update,,Update,Actualizar,Actualizar,Mise à jour,,アップデート,업데이트,,,Обновить,更新,更新,更新 +LocalizationTools.GameTableSection.UpdateTableLabel,,,Update with new content from CSV,,Update with new content from CSV,Actualizar con nuevo contenido de CSV,Actualizar con nuevo contenido de CSV,Mise à jour nouveau contenu du CSV,,CSVからの新しいコンテンツを入れてアップデート,CSV에서 새 콘텐츠로 업데이트,,,Обновить с использованием нового контента из CSV,更新 CSV 中的新内容,使用 CSV 檔案的新內容更新,更新 CSV 中的新内容 +LocalizationTools.GameTableSection.AdvancedButton,,,Advanced,,Advanced,Avanzado,Avanzado,Avancé,,詳細設定,고급,,,Расширенные настройки,高级,進階,高级 +LocalizationTools.GameTableSection.ReplaceButton,,,Replace,,Replace,Reemplazar,Reemplazar,Remplacer,,置き換え,바꾸기,,,Заменить,替换,取代,替换 +LocalizationTools.GameTableSection.ReplaceTableLabel,,,Replace entire cloud table with CSV,,Replace entire cloud table with CSV,Reemplazar toda la tabla en la nube con CSV,Reemplazar toda la tabla en la nube con CSV,Remplacer l'ensemble du tableau cloud par le CSV,,クラウドテーブル全体をCSVで置き換える,전체 클라우드 테이블을 CSV로 바꾸기,,,Заменить всю облачную таблицу CSV-файлом,将整个云表格替换为 CSV,將整個雲端表格取代成 CSV 檔案,将整个云表格替换为 CSV +LocalizationTools.GameTableSection.PublishPlaceMessage,,,Publish this place to upload a table,,Publish this place to upload a table,Publicar este lugar para cargar una tabla,Publicar este lugar para cargar una tabla,Publier cet espace pour télécharger un tableau,,テーブルをアップロードするには、このプレースを公開,이 플레이스를 게시하여 테이블 업로드,,,"Опубликовать место, чтобы загрузить таблицу",发布此场景来上传表格,發布地點後即可上傳表格,发布此场景来上传表格 +LocalizationTools.GameTableSection.SectionLabel,,,Cloud Localization Table,,Cloud Localization Table,Tabla de localización en la nube,Tabla de localización en la nube,Tableau de localisation cloud,,クラウド多言語化テーブル,클라우드 로컬리제이션 테이블,,,Облачная локализационная таблица,云本地化表格,雲端本地化表格,云本地化表格 +LocalizationTools.UploadDialogContent.PatchEmptyMessage,,,Patch empty. Upload anyway?,,Patch empty. Upload anyway?,Revisión vacía. ¿Quieres seguir cargando?,Revisión vacía. ¿Quieres seguir cargando?,Patch vide. Télécharger quand même ?,,パッチが空です。それでもアップロードしますか?,패치가 비어 있습니다. 업로드할까요?,,,Патч пустой. Все равно загрузить?,空补丁,仍要上传?,此更新檔空白,繼續上傳?,空补丁,仍要上传? +LocalizationTools.UploadDialogContent.UploadPatchMessage,,,Upload patch?,,Upload patch?,¿Cargar revisión?,¿Cargar revisión?,Télécharger un patch ?,,パッチをアップロードしますか?,패치를 업로드할까요?,,,Загрузить патч?,上传补丁?,上傳更新檔?,上传补丁? +LocalizationTools.UploadDialogContent.AddEntriesPreText,,,Add entries: ,,Add entries: ,Añadir entradas: ,Añadir entradas: ,Ajouter des entrées : ,,入力内容を追加: ,항목 추가:,,,Добавить записи: ,添加条目:,新增條目:,添加条目: +LocalizationTools.UploadDialogContent.AddTranslationsPretext,,,Add translations: ,,Add translations: ,Añadir traducciones: ,Añadir traducciones: ,Ajouter des traductions : ,,翻訳を追加: ,번역 추가:,,,Добавить переводы: ,添加翻译:,新增翻譯:,添加翻译: +LocalizationTools.UploadDialogContent.ChangeTranslationsPretext,,,Change translations: ,,Change translations: ,Cambiar traducciones: ,Cambiar traducciones: ,Changer des traductions : ,,翻訳を変更: ,번역 변경:,,,Изменить переводы: ,更改翻译:,變更翻譯:,更改翻译: +LocalizationTools.UploadDialogContent.DeleteEntriesPretext,,,Delete entries: ,,Delete entries: ,Eliminar entradas: ,Eliminar entradas: ,Supprimer des entrées : ,,入力内容を削除: ,항목 삭제:,,,Удалить записи: ,删除条目:,刪除條目:,删除条目: +LocalizationTools.UploadDialogContent.DeleteTranslationsPretext,,,Delete translations: ,,Delete translations: ,Eliminar traducciones: ,Eliminar traducciones: ,Supprimer des traductions : ,,翻訳を削除: ,번역 삭제:,,,Удалить переводы: ,删除翻译:,刪除翻譯:,删除翻译: +LocalizationTools.UploadDialogContent.AddLanguagesPretext,,,Add languages: ,,Add languages: ,Añadir idiomas: ,Añadir idiomas: ,Ajouter des langues : ,,言語を追加: ,언어 추가:,,,Добавить языки: ,添加语言:,新增語言:,添加语言: +LocalizationTools.UploadDialogContent.PatchContainsLabel,,,This patch contains: ,,This patch contains: ,Esta revisión contiene: ,Esta revisión contiene: ,Ce patch contient : ,,このパッチに含まれるもの: ,이 패치는 다음을 포함합니다.,,,Этот патч содержит: ,此补丁包括:,此更新檔包含:,此补丁包括: +LocalizationTools.UploadDialogContent.PatchTotalRowsLabel,,,Total rows: ,,Total rows: ,Filas totales: ,Filas totales: ,Total lignes : ,,行の合計: ,전체 행: ,,,Всего строк: ,总条数:,總列數:,总条数: +LocalizationTools.UploadDialogContent.PatchTotalTranslationsLabel,,,Total translations: ,,Total translations: ,Traducciones totales: ,Traducciones totales: ,Total traductions : ,,翻訳の合計: ,전체 번역: ,,,Всего переводов: ,总翻译数:,總翻譯數:,总翻译数: +LocalizationTools.UploadDialogContent.PatchLanguagesLabel,,,Languages: ,,Languages: ,Idiomas: ,Idiomas: ,Langues : ,,言語: ,언어:,,,Языки: ,语言:,語言:,语言: +LocalizationTools.UploadDialogContent.PatchInvalidLanguagesLabel,,,Invalid columns: ,,Invalid columns: ,Columnas no válidas: ,Columnas no válidas: ,Colonnes non valides : ,,無効な列: ,잘못된 열:,,,Столбцы с ошибками: ,无效的列:,無效行:,无效的列: +LocalizationTools.UploadDialogContent.PatchWillLabel,,,This patch will: ,,This patch will: ,Esta revisión hará lo siguiente: ,Esta revisión hará lo siguiente: ,Ce patcher sera : ,,このパッチは: ,이 패치는 다음 항목을 확인합니다.,,,Цель этого патча: ,此补丁将:,此更新將會:,此补丁将: +LocalizationTools.UploadDialogContent.CancelButton,,,Cancel,,Cancel,Cancelar,Cancelar,Annuler,,キャンセル,취소,,,Отмена,取消,取消,取消 +LocalizationTools.UploadDialogContent.ConfirmButton,,,Confirm,,Confirm,Confirmar,Confirmar,Confirmer,,確定,확인,,,Подтвердить,确认,確認,确认 +LocalizationTools.AddWebEntriesToRbxEntries.WrongFommatedWebTableMessage,,,Wrongly formatted web table.,,Wrongly formatted web table.,Tabla web formateada incorrectamente.,Tabla web formateada incorrectamente.,Tableau web mal formaté.,,ウェブテーブルの形式が間違っています。,웹 테이블 형식이 잘못되었습니다.,,,Неправильно отформатированная веб-таблица.,本地化表格格式有误。,網路表格格式錯誤。,本地化表格格式有误。 +LocalizationTools.AddWebEntriesToRbxEntries.ExpectedTableTypeMessage,,,Expected table type.,,Expected table type.,Tipo de tabla esperado.,Tipo de tabla esperado.,Type de tableau attendu.,,予測されるテーブルの種類。,테이블 유형이 필요합니다.,,,Ожидаемый тип таблицы.,表格类型缺失。,預定表格類型。,表格类型缺失。 +LocalizationTools.AddWebEntriesToRbxEntries.NoLocaleMessage,,,Web table contained translation with no locale,,Web table contained translation with no locale,La tabla web contiene una traducción sin idioma,La tabla web contiene una traducción sin idioma,Le tableau web contient une traduction sans locale,,ウェブテーブルに地域指定なしの翻訳があります,로케일이 없는 번역이 포함된 웹 테이블,,,Веб-таблица содержит перевод без языковых настроек,本地化表格包含未标明语言的翻译,網路表格包含未註明地區的翻譯,本地化表格包含未标明语言的翻译 +LocalizationTools.PageDownloader.DecodeFailedMessage,,,Downloaded table failed to decode,,Downloaded table failed to decode,Error de decodificación de la tabla descargada,Error de decodificación de la tabla descargada,Le tableau téléchargé n'a pas pu être décodé,,ダウンロードテーブルがデコードできませんでした,다운로드한 테이블을 디코딩할 수 없음,,,Ошибка декодирования загруженной таблицы,下载的表格解码失败,無法解碼下載的表格,下载的表格解码失败 +LocalizationTools.PageDownloader.FailedWithStatusCodeMessage,,,"Uploading table failed with status code: {1}, and response: {2}",,"Uploading table failed with status code: {1}, and response: {2}",Error al cargar la tabla con código de estado: {1} y respuesta: {2},Error al cargar la tabla con código de estado: {1} y respuesta: {2},Le chargement du tableau a échoué en raison du code avec statut: {1} et réponse : {2},,テーブルのアップロードができませんでした。ステータスコードは: {1}、レスポンス: {2},상태 코드: {1} 및 응답: {2} 때문에 테이블을 업로드할 수 없음,,,"Ошибка загрузки таблицы, код статуса: {1}, отклик: {2}",上传表格失败,状态码:{1},响应码:{2},表格上傳失敗,狀態碼:{1},回應:{2},上传表格失败,状态码:{1},响应码:{2} +LocalizationTools.PageDownloader.DownloadFailedMessage,,,Download failed,,Download failed,Error al descargar,Error al descargar,Le téléchargement a échoué,,ダウンロードできませんでした,다운로드 실패,,,Ошибка загрузки,下载失败,下載失敗,下载失败 +LocalizationTools.UploadDownloadFlow.UnexpectedErrorMessage,,,Unexpected error,,Unexpected error,Error inesperado,Error inesperado,Une erreur s'est produite,,予期せぬエラーが起きました,예기치 않은 오류,,,Непредвиденная ошибка,未知错误,意外錯誤,未知错误 +LocalizationTools.UploadDownloadFlow.CSVReadFailedMessage,,,CSV read failed,,CSV read failed,Error en la lectura de CSV,Error en la lectura de CSV,La lecture CSV a échoué,,CSVの読み取りができませんでした,CSV 읽기 실패,,,Ошибка чтения CSV,CSV 读取失败,CSV 讀取失敗,CSV 读取失败 +LocalizationTools.UploadDownloadFlow.OpenCSVCanceledMessage,,,Open CSV canceled,,Open CSV canceled,Apertura de CSV cancelada,Apertura de CSV cancelada,Ouverture CSV annulée,,CSVを開けるがキャンセルされました,CSV 열기가 취소됨,,,Открытие CSV отменено,打开 CSV 失败,已取消開啟 CSV,打开 CSV 失败 +LocalizationTools.UploadDownloadFlow.CSVWriteFailedMessage,,,CSV write failed,,CSV write failed,Error en la escritura de CSV,Error en la escritura de CSV,L'écriture CSV a échoué,,CSVの書き込みができませんでした,CSV 쓰기 실패,,,Ошибка записи CSV,CSV 写入失败,CSV 寫入失敗,CSV 写入失败 +LocalizationTools.UploadDownloadFlow.SaveCSVCanceledMessage,,,Save CSV canceled,,Save CSV canceled,Guardado de CSV cancelado,Guardado de CSV cancelado,La sauvegarde CSV a échoué,,CSV保存がキャンセルされました,CSV 저장이 취소됨,,,Сохранение CSV отменено,取消保存 CSV ,已取消儲存 CSV,取消保存 CSV +LocalizationTools.UploadDownloadFlow.BusyMessage,,,busy,,busy,ocupado,ocupado,occupé,,ビジー状態,사용 중,,,выполняется,系统繁忙,忙碌,系统繁忙 +LocalizationTools.UploadDownloadFlow.OpenCSVFileMessage,,,Open CSV file...,,Open CSV file...,Abrir archivo CSV...,Abrir archivo CSV...,Ouvrir fichier CSV...,,CSVファイルを開く...,CSV 파일 열기...,,,Открыть CSV-файл...,打开 CSV 文件...,開啟 CSV 檔案…,打开 CSV 文件... +LocalizationTools.UploadDownloadFlow.ComputingPatchMessage,,,Computing patch...,,Computing patch...,Calculando revisión...,Calculando revisión...,Patch informatique...,,パッチを計算中...,패치를 컴퓨팅하는 중...,,,Вычисление патча...,正在计算补丁...,正在運算更新檔…,正在计算补丁... +LocalizationTools.UploadDownloadFlow.ConfirmUploadMessage,,,Confirm upload...,,Confirm upload...,Confirmar carga...,Confirmar carga...,Confirmer le téléchargement...,,アップロードを確定...,업로드 확인...,,,Подтверждение загрузки...,确定上传...,確認上傳…,确定上传... +LocalizationTools.UploadDownloadFlow.ConfirmUploadDialogTitle,,,Comfirm Upload,,Comfirm Upload,Confirmar carga,Confirmar carga,Confirmer le téléchargement,,アップロードを確定,업로드 확인,,,Подтвердить загрузку,确认上传,確認上傳,确认上传 +LocalizationTools.UploadDownloadFlow.UploadingPatchMessage,,,Uploading patch...,,Uploading patch...,Cargando revisión...,Cargando revisión...,Téléchargement patch...,,パッチをアップロード中...,패치를 업로드하는 중...,,,Загрузка патча...,正在上传补丁...,正在上傳更新檔…,正在上传补丁... +LocalizationTools.UploadDownloadFlow.UploadCompleteMessage,,,Upload complete,,Upload complete,Carga completada,Carga completada,Téléchargement complet,,アップロード完了,업로드 완료,,,Загрузка завершена,上传完成,上傳完成,上传完成 +LocalizationTools.UploadDownloadFlow.UploadFailedMessage,,,Upload failed,,Upload failed,Error al cargar,Error al cargar,Échec du téléchargement,,アップロードできませんでした,업로드 실패,,,Ошибка загрузки,上传失败,上傳失敗,上传失败 +LocalizationTools.UploadDownloadFlow.UploadCanceledMessage,,,Upload canceled,,Upload canceled,Carga cancelada,Carga cancelada,Téléchargement annulé,,アップロードがキャンセルされました,업로드 취소됨,,,Загрузка отменена,上传已取消,上傳已取消,上传已取消 +LocalizationTools.UploadDownloadFlow.ComputePatchFailedMessage,,,Compute patch failed,,Compute patch failed,Error al calcular la revisión,Error al calcular la revisión,Échec du patch de calcul,,パッチ計算ができませんでした,패치 컴퓨팅 실패,,,Ошибка вычисления патча,计算补丁失败,更新檔運算失敗,计算补丁失败 +LocalizationTools.UploadDownloadFlow.DownloadingTableMessage,,,Downloading table...,,Downloading table...,Descargando tabla...,Descargando tabla...,Téléchargement tableau...,,テーブルをダウンロード中...,테이블을 다운로드하는 중...,,,Загружается таблица...,正在下载表格...,正在下載表格…,正在下载表格... +LocalizationTools.UploadDownloadFlow.SelectCSVFileMessage,,,Select CSV file...,,Select CSV file...,Seleccionar archivo CSV...,Seleccionar archivo CSV...,Sélectionner fichier CSV...,,CSVファイルを選択...,CSV 파일 선택...,,,Выбрать CSV-файл...,选择 CSV 文件...,選擇 CSV 檔案…,选择 CSV 文件... +LocalizationTools.UploadDownloadFlow.TableWrittenToFileMessage,,,Table written to file,,Table written to file,Tabla escrita en el archivo,Tabla escrita en el archivo,Tableau écrit au dossier,,ファイルに書かれたテーブル,파일에 기록된 테이블,,,Таблица записана в файл,表格已写入文件,表格已寫入檔案,表格已写入文件 +LocalizationTools.Main.ToolbarLabel,,,Localization,,Localization,Localización,Localización,Localisation,,多言語化,로컬리제이션,,,Локализация,本地化,本地化,本地化 +LocalizationTools.AddWebEntriesToRbxEntries.WrongFormatWebTableMessage,,,Wrongly formatted web table.,,Wrongly formatted web table.,Tabla web formateada incorrectamente.,Tabla web formateada incorrectamente.,Tableau web mal formaté.,,ウェブテーブルの形式が間違っています。,웹 테이블 형식이 잘못되었습니다.,,,Неправильно отформатирована веб-таблица.,本地化表格格式有误。,網路表格格式錯誤。,本地化表格格式有误。 diff --git a/Client2020/PlatformContent/pc/fonts/NotoSansCJKjp-Regular.otf b/Client2020/PlatformContent/pc/fonts/NotoSansCJKjp-Regular.otf new file mode 100644 index 0000000..296fbeb Binary files /dev/null and b/Client2020/PlatformContent/pc/fonts/NotoSansCJKjp-Regular.otf differ diff --git a/Client2020/PlatformContent/pc/terrain/diffuse.dds b/Client2020/PlatformContent/pc/terrain/diffuse.dds new file mode 100644 index 0000000..669a312 Binary files /dev/null and b/Client2020/PlatformContent/pc/terrain/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/terrain/diffusearray.dds b/Client2020/PlatformContent/pc/terrain/diffusearray.dds new file mode 100644 index 0000000..8ac4f78 Binary files /dev/null and b/Client2020/PlatformContent/pc/terrain/diffusearray.dds differ diff --git a/Client2020/PlatformContent/pc/terrain/materials.json b/Client2020/PlatformContent/pc/terrain/materials.json new file mode 100644 index 0000000..8bfaf32 --- /dev/null +++ b/Client2020/PlatformContent/pc/terrain/materials.json @@ -0,0 +1,191 @@ +{ + "platform": "pc", + "atlas": + { + "pc": { + "width": 2048, + "height": 2048, + "tileSize": 256, + "tileCount": 6, + + "sliceSize": 512, + + "borderSize": 42 + }, + "ios": { + "width": 2048, + "height": 2048, + "tileSize": 256, + "tileCount": 6, + + "borderSize": 42 + }, + "android": { + "width": 2048, + "height": 2048, + "tileSize": 256, + "tileCount": 6, + + "borderSize": 42 + }, + "durango": { + "width": 32, + "height": 32, + "tileSize": 4, + "tileCount": 6, + + "sliceSize": 512, + + "borderSize": 42 + } + }, + "materials": + [ + { + "name": "Air" + }, + { + "name": "Water", + "water": 0.5 + }, + { + "name": "Grass", + "base_color": [ 106, 127, 63 ], + "texture_top": { "tiling": 0.25, "detiling_legacy": 0.25, "detiling": 4, "rotation": 1.0 }, + "texture_side": { "tiling": 0.2, "detiling_legacy": 0.2, "detiling": 5}, + "texture_bottom": { "tiling": 0.45, "detiling_legacy": 0.45, "detiling": 2.222 } + }, + { + "name": "Slate", + "base_color": [ 63, 127, 107 ], + "shift": 0.1, + "texture": { "tiling": 0.55, "detiling_legacy": 0.55, "detiling": 1.818 } + }, + { + "name": "Concrete", + "base_color": [ 127, 102, 63 ], + "quantize": 0.5, + "type": "hard", + "texture_top": { "tiling": 0.5, "detiling_legacy": 0.8, "detiling": 1.25 }, + "texture_side": { "tiling": 0.4, "detiling_legacy": 0.4, "detiling": 2.5 } + }, + { + "name": "Brick", + "base_color": [ 138, 86, 62 ], + "cubify": 1, + "type": "hard", + "mapping": "cube", + "texture": { "tiling": 0.80, "detiling_legacy": 0.20, "detiling": 5.0 } + }, + { + "name": "Sand", + "base_color": [ 143, 126, 95 ], + "texture_top": { "tiling": 0.25, "detiling_legacy": 0.25, "detiling": 4.0 }, + "texture_side": { "tiling": 0.25, "detiling_legacy": 0.55, "detiling": 1.818 } + }, + { + "name": "WoodPlanks", + "base_color": [ 139, 109, 79 ], + "cubify": 1.0, + "type": "hard", + "mapping": "cube", + "texture": { "tiling": 0.8, "detiling_legacy": 0.5, "detiling": 2.0 } + }, + { + "name": "Rock", + "base_color": [ 102, 108, 111 ], + "shift": 0.3, + "type": "hardsoft", + "texture": { "tiling": 0.55, "detiling_legacy": 0.55, "detiling": 1.818, "rotation": 0.5 } + }, + { + "name": "Glacier", + "base_color": [ 101, 176, 234 ], + "texture_top": { "tiling": 0.23, "detiling_legacy": 0.55, "detiling": 1.818 }, + "texture_side": { "tiling": 0.2, "detiling_legacy": 0.9, "detiling": 1.111 }, + "texture_bottom": { "tiling": 0.23, "detiling_legacy": 0.6, "detiling": 1.666 } + }, + { + "name": "Snow", + "base_color": [ 195, 199, 218 ], + "texture": { "tiling": 0.3, "detiling_legacy": 0.3, "detiling": 0.3 } + }, + { + "name": "Sandstone", + "base_color": [ 137, 90, 71 ], + "texture_top": { "tiling": 0.5, "detiling_legacy": 0.7, "detiling": 0.7, "rotation": 0.5 }, + "texture_side": { "tiling": 0.3, "detiling_legacy": 0.5003, "detiling": 2.0 }, + "texture_bottom": { "tiling": 0.45, "detiling_legacy": 0.45, "detiling": 3.333 } + }, + { + "name": "Mud", + "base_color": [ 58, 46, 36 ], + "texture": { "tiling": 0.3, "detiling_legacy": 0.1, "detiling": 10 } + }, + { + "name": "Basalt", + "base_color": [ 30, 30, 37 ], + "shift": 0.2, + "type": "hardsoft", + "texture": { "tiling": 0.55, "detiling_legacy": 0.55, "detiling": 1.818 } + }, + { + "name": "Ground", + "base_color": [ 102, 92, 59 ], + "texture": { "tiling": 0.3, "detiling_legacy": 0.3, "detiling": 3.333, "rotation": 0.5 } + }, + { + "name": "CrackedLava", + "base_color": [ 232, 156, 74 ], + "shift": 0.1, + "texture": { "tiling": 0.28, "detiling_legacy": 0.3, "detiling": 3.333, "rotation": 0.5 } + }, + { + "name": "Asphalt", + "base_color": [ 115, 123, 107 ], + "texture_top": { "tiling": 0.25, "detiling_legacy": 0.23, "detiling": 4.34782 }, + "texture_side": { "tiling": 0.2, "detiling_legacy": 0.9, "detiling": 1.111 } + }, + { + "name": "Cobblestone", + "base_color": [ 132, 123, 90 ], + "quantize": 0.15, + "type": "hardsoft", + "texture_top": { "tiling": 0.23, "detiling_legacy": 0.25, "detiling": 4.0 }, + "texture_side": { "tiling": 0.2, "detiling_legacy": 0.9, "detiling": 1.111 } + }, + { + "name": "Ice", + "base_color": [ 129, 194, 224 ], + "shift": 0.15, + "texture_top": { "tiling": 0.25, "detiling_legacy": 0.25, "detiling": 4.0, "rotation": 0.1 }, + "texture_side": { "tiling": 0.25, "detiling_legacy": 0.25, "detiling": 4.0 } + }, + { + "name": "LeafyGrass", + "base_color": [ 115, 132, 74 ], + "texture_top": { "tiling": 0.25, "detiling_legacy": 0.33, "detiling": 3.030, "rotation": 1 }, + "texture_side": { "tiling": 0.25, "detiling_legacy": 0.25, "detiling": 4.0 } + }, + { + "name": "Salt", + "base_color": [ 198, 189, 181 ], + "texture_top": { "tiling": 0.23, "detiling_legacy": 0.55, "detiling": 1.818 }, + "texture_side": { "tiling": 0.2, "detiling_legacy": 0.9, "detiling": 1.111 } + }, + { + "name": "Limestone", + "base_color": [ 206, 173, 148 ], + "texture_top": { "tiling": 0.25, "detiling_legacy": 0.55, "detiling": 1.818, "rotation": 0.1 }, + "texture_side": { "tiling": 0.25, "detiling_legacy": 0.5, "detiling": 2.0 } + }, + { + "name": "Pavement", + "base_color": [ 148, 148, 140 ], + "quantize": 0.25, + "type": "hardsoft", + "texture_top": { "tiling": 0.46, "detiling_legacy": 0.25, "detiling": 4.0 }, + "texture_side": { "tiling": 0.46, "detiling_legacy": 0.25, "detiling": 4.0 } + } + ] +} diff --git a/Client2020/PlatformContent/pc/terrain/normal.dds b/Client2020/PlatformContent/pc/terrain/normal.dds new file mode 100644 index 0000000..df70fc9 Binary files /dev/null and b/Client2020/PlatformContent/pc/terrain/normal.dds differ diff --git a/Client2020/PlatformContent/pc/terrain/normalarray.dds b/Client2020/PlatformContent/pc/terrain/normalarray.dds new file mode 100644 index 0000000..fb75080 Binary files /dev/null and b/Client2020/PlatformContent/pc/terrain/normalarray.dds differ diff --git a/Client2020/PlatformContent/pc/terrain/reflection.dds b/Client2020/PlatformContent/pc/terrain/reflection.dds new file mode 100644 index 0000000..436cf6b Binary files /dev/null and b/Client2020/PlatformContent/pc/terrain/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/terrain/reflectionarray.dds b/Client2020/PlatformContent/pc/terrain/reflectionarray.dds new file mode 100644 index 0000000..fdef515 Binary files /dev/null and b/Client2020/PlatformContent/pc/terrain/reflectionarray.dds differ diff --git a/Client2020/PlatformContent/pc/textures/aluminum/diffuse.dds b/Client2020/PlatformContent/pc/textures/aluminum/diffuse.dds new file mode 100644 index 0000000..fee64f0 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/aluminum/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/aluminum/normal.dds b/Client2020/PlatformContent/pc/textures/aluminum/normal.dds new file mode 100644 index 0000000..89eb58b Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/aluminum/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/aluminum/normaldetail.dds b/Client2020/PlatformContent/pc/textures/aluminum/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/aluminum/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/aluminum/reflection.dds b/Client2020/PlatformContent/pc/textures/aluminum/reflection.dds new file mode 100644 index 0000000..dc4dc71 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/aluminum/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/brdfLUT.dds b/Client2020/PlatformContent/pc/textures/brdfLUT.dds new file mode 100644 index 0000000..00e8911 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/brdfLUT.dds differ diff --git a/Client2020/PlatformContent/pc/textures/brick/diffuse.dds b/Client2020/PlatformContent/pc/textures/brick/diffuse.dds new file mode 100644 index 0000000..740279f Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/brick/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/brick/normal.dds b/Client2020/PlatformContent/pc/textures/brick/normal.dds new file mode 100644 index 0000000..a3ead64 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/brick/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/brick/normaldetail.dds b/Client2020/PlatformContent/pc/textures/brick/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/brick/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/brick/reflection.dds b/Client2020/PlatformContent/pc/textures/brick/reflection.dds new file mode 100644 index 0000000..3bbc369 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/brick/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/cobblestone/diffuse.dds b/Client2020/PlatformContent/pc/textures/cobblestone/diffuse.dds new file mode 100644 index 0000000..099820b Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/cobblestone/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/cobblestone/normal.dds b/Client2020/PlatformContent/pc/textures/cobblestone/normal.dds new file mode 100644 index 0000000..feb08d2 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/cobblestone/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/cobblestone/normaldetail.dds b/Client2020/PlatformContent/pc/textures/cobblestone/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/cobblestone/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/cobblestone/reflection.dds b/Client2020/PlatformContent/pc/textures/cobblestone/reflection.dds new file mode 100644 index 0000000..0221722 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/cobblestone/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/concrete/diffuse.dds b/Client2020/PlatformContent/pc/textures/concrete/diffuse.dds new file mode 100644 index 0000000..6a96c3f Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/concrete/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/concrete/normal.dds b/Client2020/PlatformContent/pc/textures/concrete/normal.dds new file mode 100644 index 0000000..dc45805 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/concrete/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/concrete/normaldetail.dds b/Client2020/PlatformContent/pc/textures/concrete/normaldetail.dds new file mode 100644 index 0000000..3e8e359 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/concrete/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/concrete/reflection.dds b/Client2020/PlatformContent/pc/textures/concrete/reflection.dds new file mode 100644 index 0000000..98c43e5 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/concrete/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/diamondplate/diffuse.dds b/Client2020/PlatformContent/pc/textures/diamondplate/diffuse.dds new file mode 100644 index 0000000..99cb032 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/diamondplate/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/diamondplate/normal.dds b/Client2020/PlatformContent/pc/textures/diamondplate/normal.dds new file mode 100644 index 0000000..fe0024e Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/diamondplate/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/diamondplate/normaldetail.dds b/Client2020/PlatformContent/pc/textures/diamondplate/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/diamondplate/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/diamondplate/reflection.dds b/Client2020/PlatformContent/pc/textures/diamondplate/reflection.dds new file mode 100644 index 0000000..f779f95 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/diamondplate/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/fabric/diffuse.dds b/Client2020/PlatformContent/pc/textures/fabric/diffuse.dds new file mode 100644 index 0000000..289594c Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/fabric/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/fabric/normal.dds b/Client2020/PlatformContent/pc/textures/fabric/normal.dds new file mode 100644 index 0000000..ce4b735 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/fabric/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/fabric/normaldetail.dds b/Client2020/PlatformContent/pc/textures/fabric/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/fabric/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/fabric/reflection.dds b/Client2020/PlatformContent/pc/textures/fabric/reflection.dds new file mode 100644 index 0000000..ad09c84 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/fabric/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/glass/diffuse.dds b/Client2020/PlatformContent/pc/textures/glass/diffuse.dds new file mode 100644 index 0000000..e5e919b Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/glass/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/glass/normal.dds b/Client2020/PlatformContent/pc/textures/glass/normal.dds new file mode 100644 index 0000000..dfb65ba Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/glass/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/glass/normaldetail.dds b/Client2020/PlatformContent/pc/textures/glass/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/glass/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/glass/reflection.dds b/Client2020/PlatformContent/pc/textures/glass/reflection.dds new file mode 100644 index 0000000..6475721 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/glass/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/granite/diffuse.dds b/Client2020/PlatformContent/pc/textures/granite/diffuse.dds new file mode 100644 index 0000000..0b9a1a8 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/granite/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/granite/normal.dds b/Client2020/PlatformContent/pc/textures/granite/normal.dds new file mode 100644 index 0000000..5a4349f Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/granite/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/granite/normaldetail.dds b/Client2020/PlatformContent/pc/textures/granite/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/granite/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/granite/reflection.dds b/Client2020/PlatformContent/pc/textures/granite/reflection.dds new file mode 100644 index 0000000..3c8724d Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/granite/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/grass/diffuse.dds b/Client2020/PlatformContent/pc/textures/grass/diffuse.dds new file mode 100644 index 0000000..4627b89 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/grass/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/grass/normal.dds b/Client2020/PlatformContent/pc/textures/grass/normal.dds new file mode 100644 index 0000000..b596d9c Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/grass/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/grass/normaldetail.dds b/Client2020/PlatformContent/pc/textures/grass/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/grass/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/grass/reflection.dds b/Client2020/PlatformContent/pc/textures/grass/reflection.dds new file mode 100644 index 0000000..ac89c47 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/grass/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/ice/diffuse.dds b/Client2020/PlatformContent/pc/textures/ice/diffuse.dds new file mode 100644 index 0000000..fee64f0 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/ice/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/ice/normal.dds b/Client2020/PlatformContent/pc/textures/ice/normal.dds new file mode 100644 index 0000000..bde9eb0 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/ice/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/ice/normaldetail.dds b/Client2020/PlatformContent/pc/textures/ice/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/ice/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/ice/reflection.dds b/Client2020/PlatformContent/pc/textures/ice/reflection.dds new file mode 100644 index 0000000..6aff272 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/ice/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/marble/diffuse.dds b/Client2020/PlatformContent/pc/textures/marble/diffuse.dds new file mode 100644 index 0000000..e1804f0 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/marble/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/marble/normal.dds b/Client2020/PlatformContent/pc/textures/marble/normal.dds new file mode 100644 index 0000000..6e0449d Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/marble/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/marble/normaldetail.dds b/Client2020/PlatformContent/pc/textures/marble/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/marble/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/marble/reflection.dds b/Client2020/PlatformContent/pc/textures/marble/reflection.dds new file mode 100644 index 0000000..6c2b9f7 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/marble/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/metal/diffuse.dds b/Client2020/PlatformContent/pc/textures/metal/diffuse.dds new file mode 100644 index 0000000..5cd9550 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/metal/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/metal/normal.dds b/Client2020/PlatformContent/pc/textures/metal/normal.dds new file mode 100644 index 0000000..8502091 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/metal/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/metal/normaldetail.dds b/Client2020/PlatformContent/pc/textures/metal/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/metal/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/metal/reflection.dds b/Client2020/PlatformContent/pc/textures/metal/reflection.dds new file mode 100644 index 0000000..f0cb534 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/metal/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/pebble/diffuse.dds b/Client2020/PlatformContent/pc/textures/pebble/diffuse.dds new file mode 100644 index 0000000..6b385e0 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/pebble/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/pebble/normal.dds b/Client2020/PlatformContent/pc/textures/pebble/normal.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/pebble/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/pebble/normaldetail.dds b/Client2020/PlatformContent/pc/textures/pebble/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/pebble/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/pebble/reflection.dds b/Client2020/PlatformContent/pc/textures/pebble/reflection.dds new file mode 100644 index 0000000..ebf97fd Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/pebble/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/plastic/diffuse.dds b/Client2020/PlatformContent/pc/textures/plastic/diffuse.dds new file mode 100644 index 0000000..d4850dd Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/plastic/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/plastic/normal.dds b/Client2020/PlatformContent/pc/textures/plastic/normal.dds new file mode 100644 index 0000000..db1025b Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/plastic/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/plastic/normaldetail.dds b/Client2020/PlatformContent/pc/textures/plastic/normaldetail.dds new file mode 100644 index 0000000..594f51b Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/plastic/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/rust/diffuse.dds b/Client2020/PlatformContent/pc/textures/rust/diffuse.dds new file mode 100644 index 0000000..ab44658 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/rust/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/rust/normal.dds b/Client2020/PlatformContent/pc/textures/rust/normal.dds new file mode 100644 index 0000000..f98bf87 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/rust/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/rust/normaldetail.dds b/Client2020/PlatformContent/pc/textures/rust/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/rust/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/rust/reflection.dds b/Client2020/PlatformContent/pc/textures/rust/reflection.dds new file mode 100644 index 0000000..8f74ee4 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/rust/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/sand/diffuse.dds b/Client2020/PlatformContent/pc/textures/sand/diffuse.dds new file mode 100644 index 0000000..d37a7c4 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sand/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/sand/normal.dds b/Client2020/PlatformContent/pc/textures/sand/normal.dds new file mode 100644 index 0000000..2d468c1 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sand/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/sand/normaldetail.dds b/Client2020/PlatformContent/pc/textures/sand/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sand/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/sand/reflection.dds b/Client2020/PlatformContent/pc/textures/sand/reflection.dds new file mode 100644 index 0000000..756ae3d Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sand/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/sky/indoor512_bk.tex b/Client2020/PlatformContent/pc/textures/sky/indoor512_bk.tex new file mode 100644 index 0000000..49e3285 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sky/indoor512_bk.tex differ diff --git a/Client2020/PlatformContent/pc/textures/sky/indoor512_dn.tex b/Client2020/PlatformContent/pc/textures/sky/indoor512_dn.tex new file mode 100644 index 0000000..698bae2 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sky/indoor512_dn.tex differ diff --git a/Client2020/PlatformContent/pc/textures/sky/indoor512_ft.tex b/Client2020/PlatformContent/pc/textures/sky/indoor512_ft.tex new file mode 100644 index 0000000..ed84018 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sky/indoor512_ft.tex differ diff --git a/Client2020/PlatformContent/pc/textures/sky/indoor512_lf.tex b/Client2020/PlatformContent/pc/textures/sky/indoor512_lf.tex new file mode 100644 index 0000000..2fdf770 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sky/indoor512_lf.tex differ diff --git a/Client2020/PlatformContent/pc/textures/sky/indoor512_rt.tex b/Client2020/PlatformContent/pc/textures/sky/indoor512_rt.tex new file mode 100644 index 0000000..41ce094 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sky/indoor512_rt.tex differ diff --git a/Client2020/PlatformContent/pc/textures/sky/indoor512_up.tex b/Client2020/PlatformContent/pc/textures/sky/indoor512_up.tex new file mode 100644 index 0000000..f3a695f Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sky/indoor512_up.tex differ diff --git a/Client2020/PlatformContent/pc/textures/sky/sky512_bk.tex b/Client2020/PlatformContent/pc/textures/sky/sky512_bk.tex new file mode 100644 index 0000000..3f55061 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sky/sky512_bk.tex differ diff --git a/Client2020/PlatformContent/pc/textures/sky/sky512_dn.tex b/Client2020/PlatformContent/pc/textures/sky/sky512_dn.tex new file mode 100644 index 0000000..96fa75c Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sky/sky512_dn.tex differ diff --git a/Client2020/PlatformContent/pc/textures/sky/sky512_ft.tex b/Client2020/PlatformContent/pc/textures/sky/sky512_ft.tex new file mode 100644 index 0000000..bf370f1 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sky/sky512_ft.tex differ diff --git a/Client2020/PlatformContent/pc/textures/sky/sky512_lf.tex b/Client2020/PlatformContent/pc/textures/sky/sky512_lf.tex new file mode 100644 index 0000000..af7f1cd Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sky/sky512_lf.tex differ diff --git a/Client2020/PlatformContent/pc/textures/sky/sky512_rt.tex b/Client2020/PlatformContent/pc/textures/sky/sky512_rt.tex new file mode 100644 index 0000000..a78b967 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sky/sky512_rt.tex differ diff --git a/Client2020/PlatformContent/pc/textures/sky/sky512_up.tex b/Client2020/PlatformContent/pc/textures/sky/sky512_up.tex new file mode 100644 index 0000000..9c2aae7 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/sky/sky512_up.tex differ diff --git a/Client2020/PlatformContent/pc/textures/slate/diffuse.dds b/Client2020/PlatformContent/pc/textures/slate/diffuse.dds new file mode 100644 index 0000000..56137ae Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/slate/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/slate/normal.dds b/Client2020/PlatformContent/pc/textures/slate/normal.dds new file mode 100644 index 0000000..2c828ce Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/slate/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/slate/normaldetail.dds b/Client2020/PlatformContent/pc/textures/slate/normaldetail.dds new file mode 100644 index 0000000..d7fa2b2 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/slate/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/slate/reflection.dds b/Client2020/PlatformContent/pc/textures/slate/reflection.dds new file mode 100644 index 0000000..6531376 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/slate/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/studs.dds b/Client2020/PlatformContent/pc/textures/studs.dds new file mode 100644 index 0000000..6ff77fb Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/studs.dds differ diff --git a/Client2020/PlatformContent/pc/textures/wangIndex.dds b/Client2020/PlatformContent/pc/textures/wangIndex.dds new file mode 100644 index 0000000..e613ff1 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/wangIndex.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_01.dds b/Client2020/PlatformContent/pc/textures/water/normal_01.dds new file mode 100644 index 0000000..6af8a34 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_01.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_02.dds b/Client2020/PlatformContent/pc/textures/water/normal_02.dds new file mode 100644 index 0000000..e959d4c Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_02.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_03.dds b/Client2020/PlatformContent/pc/textures/water/normal_03.dds new file mode 100644 index 0000000..ae92c2a Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_03.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_04.dds b/Client2020/PlatformContent/pc/textures/water/normal_04.dds new file mode 100644 index 0000000..69438e7 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_04.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_05.dds b/Client2020/PlatformContent/pc/textures/water/normal_05.dds new file mode 100644 index 0000000..4c83614 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_05.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_06.dds b/Client2020/PlatformContent/pc/textures/water/normal_06.dds new file mode 100644 index 0000000..6fd9c61 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_06.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_07.dds b/Client2020/PlatformContent/pc/textures/water/normal_07.dds new file mode 100644 index 0000000..3a7ccad Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_07.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_08.dds b/Client2020/PlatformContent/pc/textures/water/normal_08.dds new file mode 100644 index 0000000..751be07 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_08.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_09.dds b/Client2020/PlatformContent/pc/textures/water/normal_09.dds new file mode 100644 index 0000000..43efa56 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_09.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_10.dds b/Client2020/PlatformContent/pc/textures/water/normal_10.dds new file mode 100644 index 0000000..a0cdc95 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_10.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_11.dds b/Client2020/PlatformContent/pc/textures/water/normal_11.dds new file mode 100644 index 0000000..c563269 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_11.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_12.dds b/Client2020/PlatformContent/pc/textures/water/normal_12.dds new file mode 100644 index 0000000..4051cfe Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_12.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_13.dds b/Client2020/PlatformContent/pc/textures/water/normal_13.dds new file mode 100644 index 0000000..ac7659b Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_13.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_14.dds b/Client2020/PlatformContent/pc/textures/water/normal_14.dds new file mode 100644 index 0000000..8bf2344 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_14.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_15.dds b/Client2020/PlatformContent/pc/textures/water/normal_15.dds new file mode 100644 index 0000000..9a0dc3d Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_15.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_16.dds b/Client2020/PlatformContent/pc/textures/water/normal_16.dds new file mode 100644 index 0000000..e42fa1d Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_16.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_17.dds b/Client2020/PlatformContent/pc/textures/water/normal_17.dds new file mode 100644 index 0000000..ea57939 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_17.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_18.dds b/Client2020/PlatformContent/pc/textures/water/normal_18.dds new file mode 100644 index 0000000..3f0921d Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_18.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_19.dds b/Client2020/PlatformContent/pc/textures/water/normal_19.dds new file mode 100644 index 0000000..f620846 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_19.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_20.dds b/Client2020/PlatformContent/pc/textures/water/normal_20.dds new file mode 100644 index 0000000..f369b89 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_20.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_21.dds b/Client2020/PlatformContent/pc/textures/water/normal_21.dds new file mode 100644 index 0000000..250ebcd Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_21.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_22.dds b/Client2020/PlatformContent/pc/textures/water/normal_22.dds new file mode 100644 index 0000000..d059da7 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_22.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_23.dds b/Client2020/PlatformContent/pc/textures/water/normal_23.dds new file mode 100644 index 0000000..3c1fde7 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_23.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_24.dds b/Client2020/PlatformContent/pc/textures/water/normal_24.dds new file mode 100644 index 0000000..758b2c1 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_24.dds differ diff --git a/Client2020/PlatformContent/pc/textures/water/normal_25.dds b/Client2020/PlatformContent/pc/textures/water/normal_25.dds new file mode 100644 index 0000000..767118e Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/water/normal_25.dds differ diff --git a/Client2020/PlatformContent/pc/textures/wood/diffuse.dds b/Client2020/PlatformContent/pc/textures/wood/diffuse.dds new file mode 100644 index 0000000..68a7671 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/wood/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/wood/normal.dds b/Client2020/PlatformContent/pc/textures/wood/normal.dds new file mode 100644 index 0000000..71c2e30 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/wood/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/wood/normaldetail.dds b/Client2020/PlatformContent/pc/textures/wood/normaldetail.dds new file mode 100644 index 0000000..9d8a998 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/wood/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/wood/reflection.dds b/Client2020/PlatformContent/pc/textures/wood/reflection.dds new file mode 100644 index 0000000..f7aaedf Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/wood/reflection.dds differ diff --git a/Client2020/PlatformContent/pc/textures/woodplanks/diffuse.dds b/Client2020/PlatformContent/pc/textures/woodplanks/diffuse.dds new file mode 100644 index 0000000..c7be12f Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/woodplanks/diffuse.dds differ diff --git a/Client2020/PlatformContent/pc/textures/woodplanks/normal.dds b/Client2020/PlatformContent/pc/textures/woodplanks/normal.dds new file mode 100644 index 0000000..de4d73f Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/woodplanks/normal.dds differ diff --git a/Client2020/PlatformContent/pc/textures/woodplanks/normaldetail.dds b/Client2020/PlatformContent/pc/textures/woodplanks/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/woodplanks/normaldetail.dds differ diff --git a/Client2020/PlatformContent/pc/textures/woodplanks/reflection.dds b/Client2020/PlatformContent/pc/textures/woodplanks/reflection.dds new file mode 100644 index 0000000..eed6151 Binary files /dev/null and b/Client2020/PlatformContent/pc/textures/woodplanks/reflection.dds differ diff --git a/Client2020/ReflectionMetadata.xml b/Client2020/ReflectionMetadata.xml new file mode 100644 index 0000000..9fb0877 --- /dev/null +++ b/Client2020/ReflectionMetadata.xml @@ -0,0 +1,7723 @@ + + + + + + BindableFunction + Scripting + Allow functions defined in one script to be called by another script + 40 + 66 + + + + + + Invoke + Causes the function assigned to OnInvoke to be called. Arguments passed to this function get passed to OnInvoke function. + + + + + + + OnInvoke + Should be defined as a function. This function is called when Invoke() is called. Number of arguments is variable. + + + + + + + BindableEvent + Scripting + Allow events defined in one script to be subscribed to by another script + + 50 + 67 + + + + + Fire + Used to make the custom event fire (see Event for more info). Arguments can be variable length. + + + + + + + Event + This event fires when the Fire() method is used. Receives the variable length arguments from Fire(). + + + + + + + TouchTransmitter + Used by networking and replication code to transmit touch events - no other purpose + + false + 30 + 37 + + + + + ForceField + Avatar + Prevents joint breakage from explosions, and stops Humanoids from taking damage + 30 + 37 + Model + + + + + PluginManager + + + + + + PluginManagerInterface + + + + + TeleportService + Allows players to seamlessly leave a game and join another + + + + CustomizedTeleportUI + true + Deprecated + + + + + + + Plugin + 30 + 86 + + + + + + PluginMouse + + + + + + Glue + true + BasePart + + + + + CollectionService + A service which provides collections of instances based on tags assigned to them. + + + + + ItemAdded + true + Deprecated. Use GetInstanceAddedSignal instead. + + + + + ItemRemoved + true + Deprecated. Use GetInstancedRemovedSignal instead. + + + + + + + GetCollection + true + Deprecated. Use GetTagged instead. + + + + + GetTagged + Returns an array of all of the instances in the data model which have the given tag. + + + + + AddTag + Adds a tag to an instance. + + + + + RemoveTag + Removes a tag to an instance. + + + + + GetTags + Returns a list of all the collections that an instance belongs to. + + + + + HasTag + Returns whether the given instance has the given tag. + + + + + GetInstanceAddedSignal + Returns a signal that fires when the given tag either has a new instance with that tag added to the data model or that tag is assigned to an instance within the data model. + + + + + GetInstanceRemovedSignal + Returns a signal that fires when the given tag either has an instance with that tag removed from the data model or that tag is removed from an instance within the data model. + + + + + + + JointsService + true + + + + + RunService + + + + + BadgeService + + + + + LogService + + + + + AssetService + A service used to set and get information about assets stored on the Roblox website. + + + + + RevertAsset + Reverts a given place id to the version number provided. Returns true if successful on reverting, false otherwise. + + + + + SetPlacePermissions + Sets the permissions for a placeID to the place accessType. An optional table (inviteList) can be included that will set the accessType for only the player names provided. The table should be set up as an array of usernames (strings). + + + + + GetPlacePermissions + Given a placeID, this function will return a table with the permissions of the place. Useful for determining what kind of permissions a particular user may have for a place. + + + + + GetAssetVersions + Given a placeID, this function will return a table with the version info of the place. An optional arg of page number can be used to page through all revisions (a single page may hold about 50 revisions). + + + + + GetCreatorAssetID + Given a creationID, this function will return the asset that created the creationID. If no other asset created the given creationID, 0 is returned. + + + + + + + HttpService + + + + + HttpEnabled + true + Enabling http requests from scripts + + + + + + + GetAsync + Server + + + + + PostAsync + Server + + + + + + + AnalyticsService + + + + + ApiKey + true + Set ApiKey + + + + + + + InsertService + A service used to insert objects stored on the website into the game. + + + + + AllowClientInsertModels + true + Can be set in non-filtering-enabled places to allow LoadAsset to be used in LocalScripts. + + + + + AllowInsertFreeModels + false + true + -1 + Allows free models to be inserted into place. + + + + + + + GetCollection + Returns a table for the assets stored in the category. A category is an setId from www.roblox.com that links to a set. <a href='http://wiki.roblox.com/index.php?title=API:Class/InsertService/GetCollection' target='_blank'>More info on table format</a>. <a href='http://wiki.roblox.com/index.php/Sets' target='_blank'>More info on sets</a> + + + + + Insert + Inserts the Instance into the workspace. It is recommended to use Instance.Parent = game.Workspace instead, as this can cause issues currently. + + + + + ApproveAssetId + true + Deprecated + + + + + ApproveAssetVersionId + true + Deprecated + + + + + + + + GetBaseSets + Returns a table containing a list of the various setIds that are ROBLOX approved. <a href='http://wiki.roblox.com/index.php/Sets' target='_blank'>More info on sets</a> + + + + + GetUserSets + Returns a table containing a list of the various setIds that correspond to argument 'userId'. <a href='http://wiki.roblox.com/index.php/Sets' target='_blank'>More info on sets</a> + + + + + GetBaseCategories + true + Deprecated. Use GetBaseSets() instead. + + + + + GetUserCategories + true + Deprecated. Use GetUserSets() instead. + + + + + LoadAsset + Returns a Model containing the Instance that resides at AssetId on the web. This call will also yield the script until the model is returned. Script execution can still continue, however, if you use a <a href='http://wiki.roblox.com/index.php?title=Coroutine' target='_blank'>coroutine</a>. + + + + + LoadAssetVersion + Similar to LoadAsset, but instead an AssetVersionId is passed in, which refers to a particular version of the asset which is not neccessarily the latest version. + + + + + + + Hat + Avatar + 30 + 45 + true + + + + + Accessory + Avatar + 30 + 32 + Model + + + + + LocalBackpack + + + + + LocalBackpackItem + + + + + MotorFeature + true + + + + + Attachment + Constraints + 30 + 81 + PVInstance + + + + + + Rotation + + + + + WorldRotation + true + Deprecated. Use WorldOrientation instead + + + + + Orientation + Euler angles applied in YXZ order + + + + + WorldOrientation + Euler angles applied in YXZ order + + + + + Axis + Primary axis. Corresponds to the LookVector, or the first column in the part-local Attachment CFrame rotation matrix + + + + + SecondaryAxis + Secondary axis. Corresponds to the UpVector, or the second column in the part-local Attachment CFrame rotation matrix + + + + + WorldAxis + Primary axis in world space. Corresponds to the LookVector, or the first column in the world space Attachment CFrame rotation matrix. + + + + + SecondaryWorldAxis + Secondary axis in world space. Corresponds to the UpVector, or the second column in the world space Attachment CFrame rotation matrix. + + + + + + + + Bone + Animations + 30 + 114 + PVInstance + PVInstance + Bone + + + + + + Constraint + Physics + 30 + 86 + BasePart + + + + + Enabled + Toggles whether or not this constraint is enabled. Disabled constraints will not render in game. + + + + + Color + The color of the in-game visual. + + + + + Visible + Toggles the in-game visual associated with this constraint. + + + + + Active + Read-only boolean, true if the Constraint is active in world. + + + + + + + + BallSocketConstraint + Constraints + 30 + 86 + BasePart + + + + + LimitsEnabled + Enables the angular limit between the axis of Attachment0 and the axis of Attachment1. + + + + + UpperAngle + Maximum angle between the two main axes. Value in [0, 180]. + + + + + Restitution + Restitution of the limit, or how elastic it is. Value in [0, 1]. + + + + + TwistLimitsEnabled + Enables the angular limits around the main axis of Attachment1. + + + + + TwistUpperAngle + Upper angular limit around the axis of Attachment1. Value in [-180, 180]. + + + + + TwistLowerAngle + Lower angular limit around the axis of Attachment1. Value in [-180, 180]. + + + + + Radius + Radius of the in-game visual. Value in [0, inf). + + + + + + + + RopeConstraint + Constraints + 30 + 89 + BasePart + + + + + Length + The length of the rope or the maximum distance between the two attachments. Value in [0, inf). + + + + + Restitution + Restitution of the rope, or how elastic it is. Value in [0, 1]. + + + + + CurrentDistance + Current distance between the two attachments. Value in [0, inf). + + + + + Thickness + The thickness of the in-game visual (diameter). Value in [0, inf). + + + + + + + + RodConstraint + Constraints + 30 + 90 + BasePart + + + + + Length + The length of the rod or the distance to be maintained between the two attachments. Value in [0, inf). + + + + + CurrentDistance + Current distance between the two attachments. Value in [0, inf). + + + + + Thickness + The thickness of the in-game visual (diameter). Value in [0, inf). + + + + + + + + SpringConstraint + Constraints + 30 + 91 + BasePart + + + + + LimitsEnabled + Enables limits on the length of the spring. + + + + + Stiffness + The stiffness parameter of the spring. Force is scaled based on distance from the free length. The units of this property are force / distance. Value in [0, inf). + + + + + Damping + The damping parameter of the spring. The force is scaled with respect to relative velocity. The units of this property are force / velocity. Value in [0, inf). + + + + + FreeLength + The distance (in studs) between the two attachments at which the spring exerts no stiffness force. Value in [0, inf). + + + + + MaxForce + The maximum force that the spring can apply. Useful to prevent instabilities. The units are mass * studs / seconds^2. Value in [0, inf). + + + + + MaxLength + Maximum spring length, or the maxium distance between the two attachments. Value in [0, inf). + + + + + MinLength + Minimum spring length, or the minimum distance between the two attachments. Value in [0, inf). + + + + + Radius + The radius of the in-game spring coil visual. Value in [0, inf). + + + + + Thickness + The thickness of the spring wire (diameter) in the in-game visual. Value in [0, inf). + + + + + Coils + The number of coils in the in-game visual. Value in [0, 8]. + + + + + CurrentLength + Current distance between the two attachments. Value in [0, inf). + + + + + + + + WeldConstraint + Constraints + 30 + 94 + PVInstance + + + + + Active + Read-only boolean, true if the joint is active in world. Rigid joints may be inactive if they are redundant or form cycles. + + + + + + + + NoCollisionConstraint + Constraints + 30 + 105 + PVInstance + + + + + Enabled + If true Part0 and Part1 will not collide, if false the parts will collide. + + + + + + + + HingeConstraint + Constraints + 30 + 87 + BasePart + + + + + ActuatorType + Type of the rotational actuator: None, Motor, or Servo. + + + + + LimitsEnabled + Enables the angular limits on rotations around the main axis of Attachment0. + + + + + UpperAngle + Upper limit for the angle from the SecondaryAxis of Attachment0 to the SecondaryAxis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + LowerAngle + Lower limit for the angle from the SecondaryAxis of Attachment0 to the SecondaryAxis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + AngularRestitution + Restitution of the two limits, or how elastic they are. Value in [0,1]. + + + + + AngularVelocity + The target angular velocity of the motor in radians per second around the rotation axis. Value in [0, inf). + + + + + MotorMaxTorque + The maximum torque the motor can apply to achieve the target angular velocity. Value in [0, inf). + + + + + MotorMaxAcceleration + The maximum angular acceleration of the motor in radians per second square. Value in [0, inf). + + + + + AngularSpeed + Target angular speed. This value is unsigned as the servo will always move toward its target. Value in [0, inf). + + + + + ServoMaxTorque + Maximum torque the servo motor can apply. Value in [0, inf). + + + + + TargetAngle + Target angle for the SecondaryAxis of Attachment1 from the SecondaryAxis of Attachment0 around the rotation axis. Value in [-180, 180]. + + + + + CurrentAngle + Signed angle between the SecondaryAxis of Attchement0 and the SecondaryAxis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + Radius + Radius of the in-game visual. Value in [0, inf). + + + + + + + + SlidingBallConstraint + Constraints + 30 + 88 + BasePart + + + + + ActuatorType + Type of linear actuator (along the axis of the slider): None, Motor, or Servo. + + + + + LimitsEnabled + Enables the limits on the linear motion along the axis of the slider. + + + + + LowerLimit + Lower limit for the position of Attachment1 with respect to Attachment0 along the slider axis. Value in (-inf, inf). + + + + + UpperLimit + Upper limit for the position of Attachment1 with respect to Attachment0 along the slider axis. Value in (-inf, inf). + + + + + Restitution + Restitution of the two limits, or how elastic they are. Value in [0, 1]. + + + + + Velocity + The target linear velocity of the motor in studs per second along the slider axis. Value in (-inf, inf). + + + + + MotorMaxForce + The maximum force the motor can apply to achieve the target velocity. Units are mass * studs / seconds^2. Value in [0, inf). + + + + + MotorMaxAcceleration + The maximum acceleration of the motor in studs per second squared. Value in [0, inf). + + + + + Speed + Target speed in studs per second. This value is unsigned as the servo will always move toward its target. Value in [0, inf). + + + + + ServoMaxForce + Maximum force the servo motor can apply. Units are mass * studs / seconds^2. Value in [0, inf). + + + + + TargetPosition + Target position of Attachment1 with respect to Attachment0 along the slider axis. Value in (-inf, inf). + + + + + CurrentPosition + Current position of Attachment1 with respect to Attachment0 along the slider axis. Value in (-inf, inf). + + + + + Size + Size of the in-game visual associated with this constraint. Value in [0, inf). + + + + + + + + PrismaticConstraint + Constraints + 30 + 88 + BasePart + + + + + + CylindricalConstraint + Constraints + 30 + 95 + BasePart + + + + + InclinationAngle + Direction of the rotation axis as an angle from the x-axis in the xy-plane of Attachment0. Value in [-180, 180]. + + + + + AngularActuatorType + Type of angular actuator: None, Motor, or Servo. + + + + + AngularLimitsEnabled + Enables the angular limits around the rotation axis. + + + + + UpperAngle + Upper limit for the angle (in degrees) between the reference axis and the SecondaryAxis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + LowerAngle + Lower limit for the angle (in degrees) between the reference axis and the SecondaryAxis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + AngularRestitution + Restitution of the two limits, or how elastic they are. Value in [0, 1]. + + + + + AngularVelocity + The target angular velocity of the motor in radians per second around the rotation axis. Value in [0, inf). + + + + + MotorMaxTorque + The maximum torque the motor can apply to achieve the target angular velocity. The units are mass * studs^2 / second^2. Value in [0, inf). + + + + + MotorMaxAngularAcceleration + The maximum angular acceleration of the motor in radians per second squared. Value in [0, inf). + + + + + AngularSpeed + Target angular speed. This value is unsigned as the servo will always move toward its target. In radians per second. Value in [0, inf). + + + + + ServoMaxTorque + Maximum torque the servo motor can apply. The units are mass * studs^2 / second^2. Value in [0, inf). + + + + + TargetAngle + Target angle (in degrees) between the reference axis and the secondary axis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + CurrentAngle + Signed angle (in degrees) between the reference axis and the secondary axis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + WorldRotationAxis + The unit vector direction of the rotation axis in world coordinates. + + + + + RotationAxisVisible + Enable the visibility of the rotation axis. + + + + + + + + AlignOrientation + Constraints + 30 + 100 + BasePart + + + + + AlignPosition + Constraints + 30 + 99 + BasePart + + + + + VectorForce + Constraints + 30 + 102 + Model + + + + + LineForce + Constraints + 30 + 101 + BasePart + + + + + Torque + Constraints + 30 + 103 + BasePart + + + + + AngularVelocity + Constraints + 30 + 103 + BasePart + + + + + Mouse + Used to receive input from the user. Actually tracks mouse events and keyboard events. + + + + + Hit + The CoordinateFrame of where the Mouse ray is currently hitting a 3D object in the Workspace. If the mouse is not over any 3D objects in the Workspace, this property is nil. + + + + + Icon + The current Texture of the Mouse Icon. Stored as a string, for more information on how to format the string <a href='http://wiki.roblox.com/index.php/Content' target='_blank'>go here</a> + + + + + Origin + The CoordinateFrame of where the Mouse is when the mouse is not clicking. + + + + + Origin + The CoordinateFrame of where the Mouse is when the mouse is not clicking. This CoordinateFrame will be very close to the Camera.CoordinateFrame. + + + + + Target + The Part the mouse is currently over. If the mouse is not currently over any object (on the skybox, for example) this property is nil. + + + + + TargetFilter + A Part or Model that the Mouse will ignore when trying to find the Target, TargetSurface and Hit. + + + + + TargetSurface + The NormalId (Top, Left, Down, etc.) of the face of the part the Mouse is currently over. + + + + + UnitRay + The Unit Ray from where the mouse is (Origin) to the current Mouse.Target. + + + + + ViewSizeX + The viewport's (game window) width in pixels. + + + + + ViewSizeY + The viewport's (game window) height in pixels. + + + + + X + The absolute pixel position of the Mouse along the x-axis of the viewport (game window). Values start at 0 on the left hand side of the screen and increase to the right. + + + + + Y + The absolute pixel position of the Mouse along the y-axis of the viewport (game window). Values start at 0 on the top of the screen and increase to the bottom. + + + + + + + Button1Down + Fired when the first button (usually the left, but could be another) on the mouse is depressed. + + + + + Button1Up + Fired when the first button (usually the left, but could be another) on the mouse is release. + + + + + Button2Down + This event is currently non-operational. + + + + + Button2Up + This event is currently non-operational. + + + + + Idle + Fired constantly when the mouse is not firing any other event (i.e. the mouse isn't moving, nor any buttons being pressed or depressed). + + + + + KeyDown + Fired when a user presses a key on the keyboard. Argument is a string representation of the key. If the key has no string representation (such as space), the string passed in is the keycode for that character. Keycodes are currently in ASCII. + + + + + KeyUp + Fired when a user releases a key on the keyboard. Argument is a string representation of the key. If the key has no string representation (such as space), the string passed in is the keycode for that character. Keycodes are currently in ASCII. + + + + + Move + Fired when the mouse X or Y member changes. + + + + + WheelBackward + This event is currently non-operational. + + + + + WheelForward + This event is currently non-operational. + + + + + + + ProfilingItem + + + + + ChangeHistoryService + + + + + RotateP + BasePart + + + + + RotateV + BasePart + + + + + ScriptContext + + + + + Selection + + + + + VelocityMotor + BasePart + + + + + Weld + 200 + 34 + BasePart + + + + + TaskScheduler + false + + + + + SetThreadShare + true + Deprecated + + + + + + + StatsItem + + + + + Snap + 200 + 34 + BasePart + + + + + FileMesh + BasePart + + + + + ClickDetector + 3D Interfaces + Raises mouse events for parent object + 30 + 41 + BasePart + + + + + MaxActivationDistance + The maximum distance a Player's character can be from the ClickDetector's parent Part that will allow the Player's mouse to fire events on this object. + + + + + + + MouseClick + Fired when a player clicks on the parent Part of ClickDetector. The argument provided is always of type Player. + + + + + MouseHoverEnter + Fired when a player's mouse enters on the parent Part of ClickDetector. The argument provided is always of type Player. + + + + + MouseHoverLeave + Fired when a player's mouse leaves the parent Part of ClickDetector. The argument provided is always of type Player. + + + + + + + + Clothing + 20 + + + + + + Smoke + Effects + Makes the parent part or model object emit smoke + 30 + 59 + BasePart + + + + + + Trail + Effects + Makes two attachments emit trail when moving + 30 + 93 + Model + + + + + LightEmission + 0 + 1 + + + + + + LightInfluence + 0 + 1 + + + + + + ZOffset + -1 + 1 + + + + + + Lifetime + 0 + 20 + + + + + + TextureLength + 0 + 5 + 40 + + + + + + MinLength + 0 + 1 + + + + + + + + + Beam + Effects + Makes beam between two attachments + 30 + 96 + BasePart + + + + + LightEmission + 0 + 1 + + + + + + LightInfluence + 0 + 1 + + + + + + TextureSpeed + -1 + 1 + + + + + + TextureLength + 0 + 5 + 40 + + + + + + CurveSize0 + -10 + 10 + + + + + + CurveSize1 + -10 + 10 + + + + + + ZOffset + -1 + 1 + + + + + + + + SurfaceAppearance + 3D Interfaces + Overrides the visual appearance of its parent MeshPart + 40 + 10 + + + + + AlphaMode + Determines how the ColorMap's alpha channel behaves. Not scriptable at runtime. + + + + + ColorMap + Image ID used to color the surface. If left empty, surface uses Part Color. Not scriptable at runtime. + + + + + MetalnessMap + Grayscale Image ID that determines the metalness of the surface. Not scriptable at runtime. + + + + + NormalMap + Image ID of the normal map used to control surface bumps and curvature. Not scriptable at runtime. + + + + + RoughnessMap + Grayscale Image ID that determines the roughness of the surface. Not scriptable at runtime. + + + + + TexturePack + Internal asset link + + + + + + + ParticleEmitter + Effects + A generic particle system. + 30 + 80 + BasePart + + + + + LightEmission + 0 + 1 + + + + + LightInfluence + Specifies the amount of influence lighting has on the particle emmitter. A value of 0 is unlit, 1 is fully lit. Fractional values blend from unlit to lit. + 0 + 1 + + + + + + Drag + 0 + 5 + + + + + VelocityInheritance + 0 + 1 + + + + + Rate + 0 + 100 + 100 + + + + + + Rotation + -180 + 180 + 72 + + + + + RotSpeed + -360 + 360 + 72 + + + + + Speed + 0 + 100 + 100 + + + + + Lifetime + 0 + 5 + + + + + + + + Sparkles + Effects + Makes the parent part or model object fantastic + 30 + 42 + BasePart + + + + + Explosion + Effects + 30 + 36 + Creates an Explosion! This can be used as a purely graphical effect, or can be made to damage objects. + BasePart + + + + + BlastPressure + How much force this Explosion exerts on objects within it's BlastRadius. Setting this to 0 creates a purely graphical effect. A larger number will cause Parts to fly away at higher velocities. + + + + + BlastRadius + How big the Explosion is. This is a circle starting from the center of the Explosion's Position, the larger this property the larger the circle of destruction. + + + + + Position + Where the Explosion occurs in absolute world coordinates. + + + + + ExplosionType + Defines the behavior of the Explosion. <a href='http://wiki.roblox.com/index.php/ExplosionType' target='_blank'>More info</a> + + + + + + + Fire + Effects + Makes the parent part or model object emit fire + 30 + 61 + BasePart + + + + + Color + The color of the base of the fire. See SecondaryColor for more. + + + + + Heat + How hot the fire appears to be. The flame moves quicker the higher this value is set. + + + + + SecondaryColor + The color the fire interpolates to from Color. The longer a particle exists in the fire, the close to this color it becomes. + + + + + Size + How large the fire appears to be. + + + + + + + Seat + Interaction + 30 + 35 + + + + + Platform + + Equivalent to a seat, except that the character stands up rather than sits down. + 30 + 35 + + + + + SkateboardPlatform + true + 30 + 35 + + + + + VehicleSeat + Interaction + Automatically finds and powers hinge joints in an assembly. Ignores motors. + 30 + 35 + Model + + + + + Tool + Interaction + 30 + 17 + StarterPack + + + + + Flag + true + 30 + 38 + + + + + CanBeDropped + If someone is carrying this flag, this bool determines whether or not they can drop it and run. + + + + + TeamColor + The Team this flag is for. Corresponds with the TeamColors in the Teams service. + + + + + + + FlagStand + true + 30 + 39 + + + + + BackpackItem + 20 + + + + + Decal + 3D Interfaces + 40 + 7 + Describes a texture that is placed on one of the sides of the Part it is parented to. + BasePart + + + + + Face + Describes the face of the Part the decal will be applied to. <a href='http://wiki.roblox.com/index.php/NormalId' target='_blank'>More info</a> + + + + + Shiny + How much light will appear to reflect off of the decal. + + + + + Specular + How light will react to the surface of the decal. + + + + + Transparency + How visible the decal is. 1 is completely invisible, while 0 is completely opaque + 0 + 1 + + + + + + + JointInstance + 200 + 34 + + + + + Active + Read-only boolean, true if the joint is active in world. Rigid joints may be inactive if they are redundant or form cycles. + + + + + + + Message + 110 + 33 + true + StarterGui + + + + + Hint + true + 110 + 33 + + + + + IntValue + Values + 30 + 4 + Stores a int value in it's Value member. Useful to share int information across multiple scripts. + + + + + RayValue + Values + 30 + 4 + Stores a Ray value in it's Value member. Useful to share Ray information across multiple scripts. + + + + + IntConstrainedValue + true + Values + 30 + 4 + Stores an int value in it's Value member. Value is clamped to be in range of Min and MaxValue. Useful to share int information across multiple scripts. + + + + MaxValue + The maximum we allow this Value to be set. If Value is set higher than this, it automatically gets adjusted to MaxValue + + + + + MinValue + The minimum we allow this Value to be set. If Value is set lower than this, it automatically gets adjusted to MinValue + + + + + + DoubleConstrainedValue + true + Values + 30 + 4 + Stores a double value in it's Value member. Value is clamped to be in range of Min and MaxValue. Useful to share double information across multiple scripts. + + + + + MaxValue + The maximum we allow this Value to be set. If Value is set higher than this, it automatically gets adjusted to MaxValue + + + + + MinValue + The minimum we allow this Value to be set. If Value is set lower than this, it automatically gets adjusted to MinValue + + + + + + + BoolValue + Values + 30 + 4 + Stores a boolean value in it's Value member. Useful to share boolean information across multiple scripts. + + + + + CustomEvent + 30 + true + 4 + + + + + CustomEventReceiver + 30 + true + 4 + + + + + FloorWire + true + 30 + 4 + Renders a thin cylinder than can be adorned with textures that 'flow' from one object to the next. Has basic pathing abilities and attempts to to not intersect anything. <a href='http://wiki.roblox.com/index.php/FloorWire_Guide' target='_blank'>More info</a> + + + + + CycleOffset + Controls how the decals are positioned along the wire. <a href='http://wiki.roblox.com/index.php/CycleOffset' target='_blank'>More info</a> + + + + + From + The object the FloorWire 'emits' from + + + + + StudsBetweenTextures + The space between two textures on the wire. Note: studs are relative depending on how far the camera is from the FloorWire. + + + + + Texture + The image we use to render the textures that flow from beginning to end of the FloorWire. + + + + + TextureSize + The size in studs of the Texture we use to flow from one object to the next. + + + + + To + The object the FloorWire 'emits' to + + + + + Velocity + The rate of travel that the textures flow along the wire. + + + + + WireRadius + How thick the wire is. + + + + + + + NumberValue + Values + 30 + 4 + + + + + StringValue + Values + 30 + 4 + + + + + Vector3Value + Values + 30 + 4 + + + + + CFrameValue + Values + 30 + 4 + Stores a CFrame value in it's Value member. Useful to share CFrame information across multiple scripts. + + + + + Color3Value + Values + 30 + 4 + Stores a Color3 value in it's Value member. Useful to share Color3 information across multiple scripts. + + + + + BrickColorValue + Values + 30 + 4 + Stores a BrickColor value in it's Value member. Useful to share BrickColor information across multiple scripts. + + + + + ValueBase + Values + 30 + 4 + The base class to all Value Objects. + + + + + ObjectValue + Values + 30 + 4 + + + + + SpecialMesh + Meshes + 30 + 8 + BasePart + + + + + BlockMesh + Meshes + 30 + 8 + BasePart + + + + + CylinderMesh + Meshes + 30 + 8 + BasePart + true + + + + + BevelMesh + Meshes + false + true + + + + + DataModelMesh + false + + + + + + Texture + 3D Interfaces + 40 + 10 + BasePart + + + + + Sound + Sounds + 10 + 11 + + + + + play + true + Deprecated. Use Play() instead + + + + + + + PlayOnRemove + The sound will play when it is removed from the Workspace. Looped sounds don't play + + + + + + + + EchoSoundEffect + An echo audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + Delay + 0.1 + 5 + 100 + + + + + Feedback + 0 + 1 + 100 + + + + + DryLevel + -80 + 10 + 100 + + + + + WetLevel + -80 + 100 + 100 + + + + + + + + FlangeSoundEffect + A Flanging audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + Mix + 0 + 1 + 100 + + + + + Depth + 0.01 + 1 + 100 + + + + + Rate + 0 + 20 + 100 + + + + + + + + DistortionSoundEffect + A Distortion audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + Level + 0 + 1 + 100 + + + + + + + + PitchShiftSoundEffect + A Pitch Shifting audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + Octave + 0.5 + 2 + 100 + + + + + + + + ChorusSoundEffect + A Chorus audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + Mix + 0 + 1 + 100 + + + + + Rate + 0 + 20 + 100 + + + + + Depth + 0 + 1 + 100 + + + + + + + + TremoloSoundEffect + A Tremolo audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + Frequency + 0.1 + 20 + 100 + + + + + Depth + 0 + 1 + 100 + + + + + Duty + 0 + 1 + 100 + + + + + + + + ReverbSoundEffect + A Reverb audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + DecayTime + 0.1 + 20 + 100 + + + + + Diffusion + 0 + 1 + 100 + + + + + Density + 0 + 1 + 100 + + + + + DryLevel + -80 + 20 + 100 + + + + + WetLevel + -80 + 20 + 100 + + + + + + + + EqualizerSoundEffect + An Three-band Equalizer audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + LowGain + -80 + 10 + 100 + + + + + MidGain + -80 + 10 + 100 + + + + + HighGain + -80 + 10 + 100 + + + + + + + + CompressorSoundEffect + A Compressor audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + Threshold + -80 + 0 + 100 + + + + + Attack + 0.001 + 1 + 100 + + + + + Release + 0.001 + 5 + 100 + + + + + Ratio + 1 + 50 + 100 + + + + + GainMakeup + 0 + 30 + 100 + + + + + + + + SoundGroup + Sounds + 20 + 85 + SoundService + + + + + + + + + StockSound + false + -1 + + + + + SoundService + 500 + 31 + + + + + + AmbientReverb + + The ambient sound environment. May not work when using hardware sound + + + + + DopplerScale + + The doppler scale is a general scaling factor for how much the pitch varies due to doppler shifting in 3D sound. Doppler is the pitch bending effect when a sound comes towards the listener or moves away from it, much like the effect you hear when a train goes past you with its horn sounding. With dopplerscale you can exaggerate or diminish the effect. + + + + + DistanceFactor + + the relative distance factor, compared to 1.0 meters. + + + + + RolloffScale + + Setting this value makes the sound drop off faster or slower. The higher the value, the faster volume will attenuate, and conversely the lower the value, the slower it will attenuate. For example a rolloff factor of 1 will simulate the real world, where as a value of 2 will make sounds attenuate 2 times quicker. + + + + + + + Backpack + 30 + 20 + false + + + + + StarterPack + 30 + 20 + + + + + StarterPlayer + 30 + 79 + + + + + StarterGear + 30 + 20 + false + + + + + + CoreGui + 30 + 46 + + + + + + CorePackages + 30 + 20 + + + + + + RobloxPluginGuiService + 30 + 46 + + + + + + PluginGuiService + 30 + 46 + + + + + + PluginDebugService + 30 + 46 + + + + + + Studio + + + + + Show Plugin GUI Service in Explorer + + + + + + + + UIGridStyleLayout + GUI + false + GuiBase2d + + + + + + SetCustomSortFunction + When SortOrder is set to Custom, this lua function is used to determine the ordering of elements. Function should take two arguments (each will be an Instance child to compare), and return true if a comes before b, otherwise return false. In other words, use this function the same way you would use a table.sort function. The sorting should be deterministic, otherwise sort will fail and fall back to name order. + true + + + + + ApplyLayout + Forces a relayout of all elements. Useful when sort is set to Custom. + + + + + + + + SortOrder + Determines how we decide which element to place next. Can be Name or Custom. If using Custom, make sure SetCustomSortFunction was called with an appropriate sort function. + + + + + FillDirection + Determines which direction to fill the grid. Can be Horizontal or Vertical. + + + + + HorizontalAlignment + Determines how grid is placed within it's parent's container in the x direction. Can be Left, Center, or Right. + + + + + VerticalAlignment + Determines how grid is placed within it's parent's container in the y direction. Can be Top, Center, or Bottom. + + + + + + + + UIListLayout + 30 + 26 + GUI + Sets the position of UI elements in a list. You can use a UIListLayout by parenting it to a GuiObject. The UIListLayout will then apply itself to all of its GuiObject siblings. + GuiBase2d + + + + + + Padding + Determines the amount of free space between each element. Can be set either using scale (Percentage of parent's size in the current direction) or offset (a static spacing value, similar to pixel size). + + + + + + + + UIGridLayout + 30 + 26 + GUI + Sets the position of UI elements in a 2D grid (this can be modified to 1D grid for list layout). This will also set the elements to a particular size, although this can be overridden with particular constraints on elements. You can use a UIGridLayout by parenting it to a GuiObject. The UIGridLayout will then apply itself to all of its GuiObject siblings. + GuiBase2d + + + + + + CellSize + Denotes what size each element should be. Can be overridden by elements using constraints on individual elements. + + + + + CellPadding + How much space between elements there should be. + + + + + FillDirectionMaxCells + Determines how many cells over in the FillDirection we go before starting a new row or column. Set to 0 for max cell count. Will be clamped if this is set higher than the parent container allows room for. + + + + + AbsoluteSize + Returns the current size of the grid. If more elements are added, this can increase. If elements are removed this can decrease. + + + + + StartCorner + Which corner we start laying the elements out from. Can be TopLeft, TopRight, BottomLeft, BottomRight. + + + + + + + + + UIPageLayout + 30 + 26 + GUI + Creates a paged viewing window, like the home screen of a mobile device. You can use a UIPageLayout by parenting it to a GuiObject. The UIPageLayout will then apply itself to all of its GuiObject siblings. + GuiBase2d + + + + + + CurrentPage + The page that is either currently being displayed or is the target of the current animation. + + + + + + Circular + Whether or not the page layout wraps around at the ends. + + + + + + Padding + Determines the amount that pages are separated from each other by. Can be set either using scale (Percentage of parent's size in the current direction) or offset (a static spacing value, similar to pixel size). + + + + + + Animated + Whether or not to animate transitions between pages. + + + + + + EasingStyle + The easing style to use when performing an animation. + + + + + + EasingDirection + The easing direction to use when performing an animation. + + + + + + TweenTime + The length of the animation. + + + + + + + + Next + Sets CurrentPage to the page after the current page and animates to it, or does nothing if there isn't a next page. + + + + + Previous + Sets CurrentPage to the page after the current page and animates to it, or does nothing if there isn't a next page. + + + + + JumpTo + If the instance is in the layout, then it sets CurrentPage to it and animtes to it. If circular layout is set, it will take the shortest path. + + + + + JumpToIndex + If the index is >= 0 and less than the size of the layout, acts like JumpTo. If it's out of bounds and circular is set, it will animate the full distance between the in-bounds index of CurrentPage and the new index. + + + + + + + + PageEnter + Fires when a page comes into view, and is going to be rendered. + + + + + PageLeave + Fires when a page leaves view, and will not be rendered. + + + + + Stopped + Fires when an animation to CurrentPage is completed without being cancelled, and the view stops scrolling. + + + + + + + + UITableLayout + 30 + 26 + GUI + Provides a layout of rows and columns that are sized based on the cells in them. + GuiBase2d + + + + + + Padding + The amount of padding to insert in between the cells of the table. + + + + + + FillEmptySpaceRows + Whether the table should expand to fill the available space of its container, row-wise. + + + + + + FillEmptySpaceColumns + Whether the table should expand to fill the available space of its container, column-wise. + + + + + + MajorAxis + Whether the direct siblings are considered the rows or the columns. The children of the direct siblings are the columns or rows, respectively. + + + + + + + + UISizeConstraint + 30 + 26 + GUI + Ensures a GuiObject does not become smaller or larger than the min and max size. If an element with a constraint is under the control of a layout, the constraint takes precedence in determining the element’s size, but not position. You can use a Constraint by parenting it to the element you wish to constrain. + GuiBase2d + + + + + + MinSize + The smallest size the GuiObject is allowed to be. + + + + + MaxSize + The biggest size the GuiObject is allowed to be. + + + + + + + + UITextSizeConstraint + 30 + 26 + GUI + Ensures a GuiObject with text does not allow the font size to become larger or smaller than min and max text sizes. If an element with a constraint is under the control of a layout, the constraint takes precedence in determining the element’s size, but not position. You can use a Constraint by parenting it to the element you wish to constrain. + GuiBase2d + + + + + + MinTextSize + The smallest size the font is allowed to be. + + + + + MaxTextSize + The biggest size the font is allowed to be. + + + + + + + + UIAspectRatioConstraint + 30 + 26 + GUI + Ensures a GuiObject will always have a particular aspect ratio. If an element with a constraint is under the control of a layout, the constraint takes precedence in determining the element’s size, but not position. You can use a Constraint by parenting it to the element you wish to constrain. + + + + + + AspectRatio + The aspect ratio to maintain. This is the width/height. Only positive numbers allowed. + + + + + AspectType + Describes how the aspect ratio will determine its size. Options are FitWithinMaxSize, ScaleWithParentSize. FitWithinMaxSize will make the element the maximum size it can be within the current possible AbsoluteSize of the element while maintaining the AspectRatio. ScaleWithParentSize will make the element the closest to the parent element’s maximum size while maintaining aspect ratio. + + + + + DominantAxis + Describes which axis to use when determining the new size of the element, while keeping respect to the aspect ratio. + + + + + + + + UIScale + 30 + 26 + GUI + Uniformly scales a GUI object and all its children. + GuiBase2d + + + + + + Scale + The scale factor to apply. + + + + + + + + UIPadding + 30 + 26 + GUI + Insets the children of the GuiObject this is parented to, by the specified padding. + GuiBase2d + + + + + + PaddingLeft + The padding to apply on the left side relative to the parent's normal size. + + + + + PaddingRight + The padding to apply on the right side relative to the parent's normal size. + + + + + PaddingTop + The padding to apply on the top side relative to the parent's normal size. + + + + + PaddingBottom + The padding to apply on the bottom side relative to the parent's normal size. + + + + + + + + UIGradient + 30 + 26 + GUI + Apply a gradient to the parent GuiObject. + GuiBase2d + + + + + + Color + The (sequence of) color3 of the gradient. + + + + + + Transparency + The (sequence of) transparency of the gradient. + + + + + + Rotation + Clockwise rotation in degrees. + + + + + + Offset + Offset of gradient center in scale. + + + + + + + + UICorner + 30 + 26 + GUI + Modify corner properties of parent GuiObject. + GuiBase2d + + + + + CornerRadius + Round corner with specified radius. + + + + + + + + TweenBase + false + + + + + + PlaybackState + The current state of how the tween is animating. Possible values are Begin, Playing, Paused, Completed and Cancelled. This property is modified by using functions such as Tween:Play(), Tween:Pause(), and Tween:Cancel(). Read-only. + + + + + + + + Play + Starts or resumes (if Tween.PlaybackState is Paused) the tween animation. If current PlaybackState is Cancelled, this property will reset the tween to the beginning properties and play the animations from the beginning. + + + + + Pause + Temporarily stops the tween animation. Animation can be resumed by calling Play(). + + + + + Cancel + Stops the tween animation. Animation can be restarted by calling Play(). Animation will start from the beginning values. + + + + + + + + Completed + Fires when the tween either reaches PlaybackState Completed or Cancelled. PlaybackState of one of these types is passed as the first arg to the function listening to this event. + + + + + + + + + Tween + An object linked to an instance that animates properties on the instance over a specified period of time. Useful for easily moving UI objects around, rotating objects, etc. without having to write a lot of code. To create a new tween, please use TweenService:Create. + + + + + + Instance + The object this tween is operating on. Read-only. + + + + + + TweenInfo + Specifies how the tween animates. Read-only. + + + + + + + + + TweenService + Service responsible for creating tweens on instances. + + + + + + + Create + Creates a Tween object bound to a particular Instance. The first arg is the Instance to tween. The second arg is a TweenInfo struct, which specifies how a tween should behave. The third arg is a table, which should specify the properties to tween as keys, with the end value specified as values to the keys. + + + + + GetValue + Transforms a linear alpha to a given EasingStyle and EasingDirection. + + + + + + + + + + StarterGui + 30 + 46 + + + + + + SetCoreGuiEnabled + Will stop/begin certain core gui elements being rendered. See CoreGuiType for core guis that can be modified. + + + + + GetCoreGuiEnabled + Returns a boolean describing whether a CoreGuiType is currently being rendered. + + + + + + + + GuiService + The GuiService is a special service, which currently allows developers to control what GuiObject is currently being selected by the Gamepad Gui navigator, and allows clients to check if Roblox's main menu is currently open. This service has a lot of hidden members, which are mainly used internally by Roblox's CoreScripts. + + + + + + GetGuiInset + Returns a Tuple containing two Vector2 values representing the offset of user GUIs in pixels from the top right corner of the screen and the bottom right corner of the screen respectively. + + + + + + + + ContextActionService + A service used to bind input to various lua functions. + + + + + + BindAction + Binds 'functionToBind' to fire when any 'inputTypes' happen. InputTypes can be variable in number and type. Types can be Enum.KeyCode, single character strings corresponding to keys, or Enum.UserInputType. 'actionName' is a key used by many other ContextActionService functions to query state. 'createTouchButton' if true will create a button on screen on touch devices. This button will fire 'functionToBind' with three arguments: first argument is the actionName, second argument is the UserInputState of the input, and the third is the InputObject that fired this function. If 'functionToBind' yields or returns nil or Enum.ContextActionResult.Sink, the input will be sunk. If it returns Enum.ContextActionResult.Pass, the next bound action in the stack will be invoked. + + + + + SetTitle + If 'actionName' key contains a bound action, then 'title' is set as the title of the touch button. Does nothing if a touch button was not created. No guarantees are made whether title will be set when button is manipulated. + + + + + SetDescription + If 'actionName' key contains a bound action, then 'description' is set as the description of the bound action. This description will appear for users in a listing of current actions availables. + + + + + SetImage + If 'actionName' key contains a bound action, then 'image' is set as the image of the touch button. Does nothing if a touch button was not created. No guarantees are made whether image will be set when button is manipulated. + + + + + SetPosition + If 'actionName' key contains a bound action, then 'position' is set as the position of the touch button. Does nothing if a touch button was not created. No guarantees are made whether position will be set when button is manipulated. + + + + + UnbindAction + If 'actionName' key contains a bound action, removes function from being called by all input that it was bound by (if function was also bound by a different action name as well, those bound input are still active). Will also remove any touch button created (if button was manipulated manually there is no guarantee it will be cleaned up). + + + + + UnbindAllActions + Removes all functions bound. No actionNames will remain. All touch buttons will be removed. If button was manipulated manually there is no guarantee it will be cleaned up. + + + + + GetBoundActionInfo + Returns a table with info regarding the function bound with 'actionName'. Table has the keys 'title' (current title that was set with SetTitle) 'image' (image set with SetImage) 'description' (description set with SetDescription) 'inputTypes' (tuple containing all input bound for this 'actionName') 'createTouchButton' (whether or not we created a touch button for this 'actionName'). + + + + + GetAllBoundActionInfo + Returns a table with all bound action info. Each entry is a key with 'actionName' and value being the same table you would get from ContextActionService:GetBoundActionInfo('actionName'). + + + + + + + + GetButton + If 'actionName' key contains a bound action, then this will return the touch button (if was created). Returns nil if a touch button was not created. No guarantees are made whether button will be retrievable when button is manipulated. + + + + + + + + PointsService + A service used to query and award points for Roblox users using the universal point system. + true + + + + + + PointsAwarded + Fired when points are successfully awarded 'userId'. Also returns the updated balance of points for usedId in universe via 'userBalanceInUniverse', total points via 'userTotalBalance', and the amount points that were awarded via 'pointsAwarded'. This event fires on the server and also all clients in the game that awarded the points. + + + + + + + + AwardPoints + Will attempt to award the 'amount' points to 'userId', returns 'userId' awarded to, the number of points awarded, the new point total the user has in the game, and the total number of points the user now has. Will also fire PointsService.PointsAwarded. Works with server scripts ONLY. + + + + + GetPointBalance + Returns the overall balance of points that player with userId has (the sum of all points across all games). Works with server scripts ONLY. + + + + + GetGamePointBalance + Returns the balance of points that player with userId has in the current game (all placeID points combined within the game). Works with server scripts ONLY. + + + + + GetAwardablePoints + Returns the number of points the current universe can award to players. Works with server scripts ONLY. + + + + + + + + Chat + 510 + 33 + + + + + + + + + + ChatService + 510 + 33 + + + + + + + + LocalizationTable + 30 + 97 + Localization + A database of strings used in the game and their translations. + + + + + + LocalizationService + 530 + 92 + + + + PreferredLanguage + Gets the system's preferred language (A Language enum). + + + + + GetLocaleId + Gets the system's LocaleId (Ex: "en-US"). + + + + + + + MarketplaceService + 46 + + + + + + PromptPurchase + Will prompt 'player' to purchase the item associated with 'assetId'. 'equipIfPurchased' is an optional argument that will give the item to the player immediately if they buy it (only applies to gear). 'currencyType' is also optional and will attempt to prompt the user with a specified currency if the product can be purchased with this currency, otherwise we use the default currency of the product. + + + + + + + + GetProductInfo + Takes one argument "assetId" which should be a number of an asset on www.roblox.com. Returns a table containing the product information (if this process fails, returns an empty table). + + + + + PlayerOwnsAsset + Checks to see if 'Player' owns the product associated with 'assetId'. Returns true if the player owns it, false otherwise. This call will produce a warning if called on a guest player. + + + + + + + ProcessReceipt + Callback that is executed for pending Developer Product receipts. + <p>If this function does not return Enum.ProductPurchaseDecision.PurchaseGranted, then you will not be granted the money for the purchase!</p> + <p>The callback will be invoked with a table, containing the following informational fields:</p> + <ul> + <li>PlayerId: int64 - the id of the player making the purchase.</li> + <li>PlaceIdWherePurchased: int64: - the specific place where the purchase was made.</li> + <li>PurchaseId: string - a unique identifier for the purchase, should be used to prevent granting an item multiple times for one purchase.</li> + <li>ProductId: int64 - the id of the purchased product.</li> + <li>CurrencyType: CurrencyType - the type of currency used (Tix, Robux).</li> + <li>CurrencySpent: int - the amount of currency spent on the product for this purchase.</li> + </ul> + + + + + + + + PromptPurchaseFinished + Fired when a 'player' dismisses a purchase dialog for 'assetId'. If the player purchased the item 'isPurchased' will be true, otherwise it will be false. This call will produce a warning if called on a guest player. + + + + + + + + UserInputService + + + + + TouchEnabled + Returns true if the local device accepts touch input, false otherwise. + + + + + KeyboardEnabled + Returns true if the local device accepts keyboard input, false otherwise. + + + + + MouseEnabled + Returns true if the local device accepts mouse input, false otherwise. + + + + + AccelerometerEnabled + Returns true if the local device has an accelerometer, false otherwise. + + + + + GyroscopeEnabled + Returns true if the local device has an gyroscope, false otherwise. + + + + + + + + TouchTap + Fired when a user taps their finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the tap gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchPinch + Fired when a user pinches their fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the pinch gesture. 'scale' is a float that indicates the difference from the beginning of the pinch gesture. 'velocity' is a float indicating how quickly the pinch gesture is happening. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchSwipe + Fired when a user swipes their fingers on a TouchEnabled device. 'swipeDirection' is an Enum.SwipeDirection, indicating the direction the user swiped. 'numberOfTouches' is an int that indicates how many touches were involved with the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchLongPress + Fired when a user holds at least one finger for a short amount of time on the same screen position on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchRotate + Fired when a user rotates two fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'rotation' is a float indicating how much the rotation has gone from the start of the gesture. 'velocity' is a float that indicates how quickly the gesture is being performed. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchPan + Fired when a user drags at least one finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'totalTranslation' is a Vector2, indicating how far the pan gesture has gone from its starting point. 'velocity' is a Vector2 that indicates how quickly the gesture is being performed in each dimension. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + + TouchStarted + Fired when a user places their finger on a TouchEnabled device. 'touch' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchMoved + Fired when a user moves their finger on a TouchEnabled device. 'touch' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchEnded + Fired when a user moves their finger on a TouchEnabled device. 'touch' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + + InputBegan + Fired when a user begins interacting via a Human-Computer Interface device (Mouse button down, touch begin, keyboard button down, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + InputChanged + Fired when a user changes interacting via a Human-Computer Interface device (Mouse move, touch move, mouse wheel, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + InputEnded + Fired when a user stops interacting via a Human-Computer Interface device (Mouse button up, touch end, keyboard button up, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + + TextBoxFocused + Fired when a user clicks/taps on a textbox to begin text entry. Argument is the textbox that was put in focus. This also fires if a textbox forces focus on the user. This event only fires locally. + + + + + TextBoxFocusReleased + Fired when a user stops text entry into a textbox (usually by pressing return or clicking/tapping somewhere else on the screen). Argument is the textbox that was taken out of focus. This event only fires locally. + + + + + DeviceAccelerationChanged + Fired when a user moves a device that has an accelerometer. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. This event only fires locally. + + + + + DeviceGravityChanged + Fired when the force of gravity changes on a device that has an accelerometer. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. This event only fires locally. + + + + + DeviceRotationChanged + Fired when a user rotates a device that has an gyroscope. This is fired with an InputObject, which has type Enum.InputType.Gyroscope, and position that shows total rotation in each local device axis. The delta property describes the amount of rotation that last happened. A second argument of Vector4 is the device's current quaternion rotation in reference to it's default reference frame. This event only fires locally. + + + + + + + + GetDeviceAcceleration + Returns an InputObject that describes the device's current acceleration. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. The delta property describes the amount of rotation that last happened. This event only fires locally. + + + + + GetDeviceGravity + Returns an InputObject that describes the device's current gravity vector. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. The delta property describes the amount of rotation that last happened. This event only fires locally. + + + + + GetDeviceRotation + Returns an InputObject and a Vector4 that describes the device's current rotation vector. This is fired with an InputObject, which has type Enum.InputType.Gyroscope, and position that shows total rotation in each local device axis. The delta property describes the amount of rotation that last happened. The Vector4 is the device's current quaternion rotation in reference to it's default reference frame. This event only fires locally. + + + + + + + + Atmosphere + Environment + 5 + 28 + Lighting + + + + + Density + 0 + 1 + 1000 + A value that controls the air particulates density. The proportion of airborne particles per unit of view depth between the camera and the sky. + + + + + Offset + 0 + 1 + 1000 + A value that offsets the quantity of light scattering events among particles in the atmosphere between the camera and the sky. + + + + + Height + 0 + 3 + 1000 + A value that controls the height of fog in the atmosphere above the horizon (not volumetric...yet...). + + + + + Color + A Color3 value that changes the hue of the atmosphere, mixed with the sky color. + + + + + Decay + A Color3 value for the the hue the atmosphere takes on away from the sun towards the horizon, mixed with the sky color (aka extinction). + + + + + Haze + 0 + 10 + 1000 + A value that controls the haziness of the atmosphere between the camera and the sky (aka turbidity). + + + + + Glare + 0 + 10 + 1000 + A value that increases the glow of the atmosphere around light the sun. + + + + + + + + Sky + Environment + 5 + 28 + Lighting + + + + + ColorCorrectionEffect + Post Processing Effects + 20 + 83 + + + + + Brightness + -1 + 1 + + + + + Contrast + -1 + 1 + + + + + Saturation + -1 + 1 + + + + + + + BloomEffect + Post Processing Effects + 20 + 83 + + + + + Intensity + 0 + 1 + + + + + Threshold + 0.8 + 4 + 1000 + + + + + Size + 0 + 56 + 56 + + + + + + + + BlurEffect + Post Processing Effects + 20 + 83 + + + + + Size + 0 + 56 + 56 + + + + + + + + DepthOfFieldEffect + Post Processing Effects + 20 + 83 + Depth based blur (Depth of Field) effect. + + + + + FocusDistance + 0 + 200.0 + 10000 + Controls the distance in stud units away from the camera where are in focus. + + + + + InFocusRadius + 0 + 50.0 + 10000 + Controls the distance in stud units away from the FocusDistance where both in front and behind objects can begin to blur. + + + + + NearIntensity + 0 + 1 + 10000 + Maximum intensity of the near blur -the higher the Intensity, the stronger the blur is allowed to reach. + + + + + FarIntensity + 0 + 1 + 10000 + Maximum intensity of the far blur -the higher the Intensity, the stronger the blur is allowed to reach. + + + + + + + + SunRaysEffect + Post Processing Effects + 20 + 83 + + + + + Intensity + 0 + 1 + 1000 + + + + + Spread + 0 + 1 + 1000 + + + + + + + + Motor + 20 + false + BasePart + + + + + Humanoid + Avatar + 30 + 9 + Model + + + + + + MoveTo + Attempts to move the Humanoid and it's associated character to 'part'. 'location' is used as an offset from part's origin. + + + + + Jump + + + + + Sit + + + + + TakeDamage + Decreases health by the amount. Use this instead of changing health directly to make sure weapons are filtered for things such as ForceField(s). + + + + + UnequipTools + + Takes any active gear/tools that the Humanoid is using and puts them into the backpack. This function only works on Humanoids with a corresponding Player. + + + + + EquipTool + + Takes a specified tool and equips it to the Humanoid's Character. Tool argument should be of type 'Tool'. + + + + + ReplaceBodyPartR15 + Replaces the desired bodypart on the Humanoid's Character using a specified Enum.BodyPartR15 and BasePart. Returns a success boolean. + + + + + GetBodyPartR15 + Returns a Enum.BodyPartR15 given a body part in the Humanoid's Character. + + + + + + + NameOcclusion + + Sets how to display other humanoid names to this humanoid's player. <a href='http://wiki.roblox.com/index.php/NameOcclusion' target='_blank'>More info</a> + + + Health + How many hit points the Humanoid has. When this number reaches 0 or goes below 0, the Humanoid's character falls apart and will respawn. + + + MaxHealth + The maximum number of hit points a Humanoid's health can reach. If the Humanoid's health is set over this amount, the health gets set to this value. + + + TargetPoint + The location that the Humanoid is trying to walk to. + + + Torso + Humanoid.RootPart will be the preferred way of getting a character's humanoid root part. + true + + + LeftLeg + In R6 this property get the player's left leg. In R15 this gets nothing. + true + + + RightLeg + In R6 this property get the player's right leg. In R15 this gets nothing. + true + + + CollisionType + An emum that selects the collision type for R15 and Rthro characters. InnerBox is classic style collisions for all characters, OuterBox is dynamically sized collisions based on Mesh size. + + + + + + + BodyColors + Avatar + 20 + Model + + + + + Shirt + Avatar + 20 + 43 + Model + + + + + Pants + Avatar + 20 + 44 + Model + + + + + ShirtGraphic + Avatar + 20 + 40 + Model + + + + + Skin + true + 20 + + + + + DebugSettings + false + 20 + + + + + FaceInstance + false + + + + + GameSettings + false + 20 + + + + + GlobalSettings + false + 20 + + + + + Item + false + 20 + + + + + NetworkPeer + false + + + + + NetworkSettings + false + 20 + + + + + PVInstance + false + + + + + CoordinateFrame + true + Deprecated. Use CFrame instead + + + + + + + PackageLink + 1 + 98 + false + + + + + Status + Current status of the Package + true + + + + + + + RenderSettings + false + 20 + + + + + RootInstance + false + + + + + ServiceProvider + false + + + + + service + true + Use GetService() instead + + + + + GetService + Instance:isService:0 + + + + + FindService + Instance:isService:0 + + + + + + + ProfilingItem + false + + + + + NetworkMarker + false + + + + + + Hopper + true + Use StarterPack instead + 20 + + + + + + Instance + false + + + + + + Archivable + Determines whether or not an Instance can be saved when the game closes/attempts to save the game. Note: this only applies to games that use Data Persistence, or SavePlaceAsync. + + + + + ClassName + The string name of this Instance's most derived class. + + + + + Parent + The Instance that is directly above this Instance in the tree. + + + + + + + + + GetDebugId + false + This function is for internal testing. Don't use in production code + + + + + Clone + Returns a copy of this Object and all its children. The copy's Parent is nil + + + + + clone + true + Use Clone() instead + + + + + isA + true + Use IsA() instead + + + + + IsA + Returns a boolean if this Instance is of type 'className' or a is a subclass of type 'className'. If 'className' is not a valid class type in ROBLOX, this function will always return false. <a href='http://wiki.roblox.com/index.php/IsA' target='_blank'>More info</a> + Instance:Any:0 + + + + + FindFirstChild + Returns the first child of this Instance that matches the first argument 'name'. The second argument 'recursive' is an optional boolean (defaults to false) that will force the call to traverse down thru all of this Instance's descendants until it finds an object with a name that matches the 'name' argument. The function will return nil if no Instance is found. + + + + + FindFirstChildOfClass + Returns the first child of this Instance that with a ClassName equal to 'className'. The function will return nil if no Instance is found. + Instance:isScriptCreatable:0 + + + + + FindFirstChildWhichIsA + Returns the first child of this Instance that :IsA(className). The second argument 'recursive' is an optional boolean (defaults to false) that will force the call to traverse down thru all of this Instance's descendants until it finds an object with a name that matches the 'className' argument. The function will return nil if no Instance is found. + Instance:Any:0 + + + + + FindFirstAncestor + Returns the first ancestor of this Instance that matches the first argument 'name'. The function will return nil if no Instance is found. + + + + + FindFirstAncestorOfClass + Returns the first ancestor of this Instance with a ClassName equal to 'className'. The function will return nil if no Instance is found. + Instance:isScriptCreatable:0 + + + + + FindFirstAncestorWhichIsA + Returns the first ancestor of this Instance that :IsA(className). The function will return nil if no Instance is found. + Instance:Any:0 + + + + + GetFullName + Returns a string that shows the path from the root node (DataModel) to this Instance. This string does not include the root node (DataModel). + + + + + children + true + Use GetChildren() instead + + + + + getChildren + true + Use GetChildren() instead + + + + + GetChildren + Returns a read-only table of this Object's children + + + + + GetDescendants + Returns an array containing all of the descendants of the instance. Returns in preorder traversal, or in other words, where the parents come before their children, depth first. + + + + + Remove + Deprecated. Use ClearAllChildren() to get rid of all child objects, or Destroy() to invalidate this object and its descendants + true + + + + + remove + true + Use Remove() instead + + + + + ClearAllChildren + Removes all children (but not this object) from the workspace. + + + + + Destroy + Removes object and all of its children from the workspace. Disconnects object and all children from open connections. Object and children may not be usable after calling Destroy. + + + + + findFirstChild + true + Use FindFirstChild() instead + + + + + + + + AncestryChanged + Fired when any of this object's ancestors change. First argument 'child' is the object whose parent changed. Second argument 'parent' is the first argument's new parent. + + + + + DescendantAdded + Fired after an Instance is parented to this object, or any of this object's descendants. The 'descendant' argument is the Instance that is being added. + + + + + DescendantRemoving + Fired after an Instance is unparented from this object, or any of this object's descendants. The 'descendant' argument is the Instance that is being added. + + + + + Changed + Fired after a property changes value. The property argument is the name of the property + + + + + + + + BodyGyro + Legacy Body Movers + Attempts to maintain a fixed orientation of its parent Part + 140 + 14 + BasePart + + + + + + MaxTorque + The maximum torque that will be exerted on the Part + + + + + maxTorque + true + Use MaxTorque instead + + + + + D + The dampening factor applied to this force + + + + + P + The power continually applied to this force + + + + + CFrame + The cframe that this force is trying to orient its parent Part to. Note: this force only uses the rotation of the cframe, not the position. + + + + + cframe + true + Use CFrame instead + + + + + + + BodyPosition + Legacy Body Movers + 140 + 14 + BasePart + + + + + + MaxForce + The maximum force that will be exerted on the Part + + + + + maxForce + true + Use MaxForce instead + + + + + D + The dampening factor applied to this force + + + + + P + The power factor continually applied to this force + + + + + Position + The Vector3 that this force is trying to position its parent Part to. + + + + + position + true + Use position instead + + + + + + + RocketPropulsion + Legacy Body Movers + 140 + 14 + A propulsion system that mimics a rocket + BasePart + + + + + BodyVelocity + Legacy Body Movers + 140 + 14 + BasePart + + + + + MaxForce + The maximum force that will be exerted on the Part in each axis + + + + + maxForce + true + Use MaxForce instead + + + + + P + The amount of power we add to the system. The higher the power, the quicker the force will achieve its goal. + + + + + Velocity + The velocity this system tries to achieve. How quickly the system reaches this velocity (if ever) is defined by P. + + + + + velocity + true + Use Velocity instead + + + + + + + BodyAngularVelocity + Legacy Body Movers + 140 + 14 + BasePart + + + + MaxTorque + The maximum torque that will be exerted on the Part in each axis + + + maxTorque + true + Use MaxTorque instead + + + P + The amount of power we add to the system. The higher the power, the quicker the force will achieve its goal. + + + AngularVelocity + The rotational velocity this system tries to achieve. How quickly the system reaches this velocity is defined by P. + + + angularVelocity + true + Use AngularVelocity instead + + + + + + BodyForce + Legacy Body Movers + 140 + 14 + When parented to a physical part, BodyForce will continually exert a force upon its parent object. + BasePart + + + + Force + The continual force exerted on an object, defined in each axis. + + + force + true + Use Force instead + + + + + + BodyThrust + Legacy Body Movers + 140 + 14 + BasePart + + + + + + Force + The power continually applied to this force + + + + + force + true + Use Force instead + + + + + Location + The Vector3 location of where to apply the force to. + + + + + location + true + Use Location instead + + + + + + + Hole + true + 20 + + + + + Feature + 20 + + + + + + Teams + This Service-level object is the container for all Team objects in a level. A map that supports team games must have a Teams service. <a href='http://wiki.roblox.com/index.php/Team' target='_blank'>More info</a> + 140 + 23 + Teams + + + + + GetPlayers + Returns a read-only table of players which are on this team. + + + + + + + Team + Interaction + The Team class is used to represent a faction in a team game. The only valid location for a Team object is under the Teams service. <a href='http://wiki.roblox.com/index.php/Team' target='_blank'>More info</a> + 10 + 24 + Teams + + + + + SpawnLocation + Interaction + 30 + 25 + + + + + NetworkClient + false + 30 + 16 + + + + + NetworkServer + false + 30 + 15 + + + + + LuaSourceContainer + false + + + + + CurrentEditor + The name of the player who is currently editing the script in Team Create. + true + + + + + + + Script + Scripting + 30 + 6 + + + + + + LinkedScript + + This property is under development. Do not use + + + + + + + + LocalScript + Scripting + 40 + 18 + A script that runs on clients, NOT servers. LocalScripts can only run when parented under one of the following: + 1) A player's Backpack. + 2) A player's Character model. + 3) A player's PlayerGui. + 4) A player's PlayerScripts. + 5) The ReplicatedFirst service. + + + + + + + RenderingTest + false + Scripting + 40 + dummy summary + 5 + + + + + + NetworkReplicator + 30 + 29 + + + + + + Model + 100 + 2 + A construct used to group Parts and other objects together, also allows manipulation of multiple objects. + PVInstance + + + + + BreakJoints + Breaks all surface joints contained within + + + + + GetModelCFrame + Returns a CFrame that has position of the centroid of all Parts in the Model. The rotation matrix is either the rotation matrix of the user-defined PrimaryPart, or if not specified then a part in the Model chosen by the engine. + + + + + GetModelSize + Returns a Vector3 that is union of the extents of all Parts in the model. + + + + + MakeJoints + Creates the appropriate SurfaceJoints between all touching Parts contrained within the model. Technically, this function calls MakeJoints() on all Parts inside the model. + + + + + MoveTo + Moves the centroid of the Model to the specified location, respecting all relative distances between parts in the model. + + + + + ResetOrientationToIdentity + Rotates all parts in the model to the orientation that was set using SetIdentityOrientation(). If this function has never been called, rotation is reset to GetModelCFrame()'s rotation. + + + + + SetIdentityOrientation + Takes the current rotation matrix of the model and stores it as the model's identity matrix. The rotation is applied when ResetOrientationToIdentity() is called. + + + + + TranslateBy + Similar to MoveTo(), except instead of moving to an explicit location, we use the model's current CFrame location and offset it. + + + + + GetPrimaryPartCFrame + Returns the cframe of the Model.PrimaryPart. If PrimaryPart is nil, then this function will throw an error. + + + + + SetPrimaryPartCFrame + Sets the cframe of the Model.PrimaryPart. If PrimaryPart is nil, then this function will throw an error. This also sets the cframe of all descendant Parts relative to the cframe change to PrimaryPart. + + + + + makeJoints + Use MakeJoints() instead + true + + + + + move + true + Use MoveTo() instead + + + + + + + PrimaryPart + A Part that serves as a reference for the Model's CFrame. Used in conjunction with GetModelPrimaryPartCFrame and SetModelPrimaryPartCFrame. Use this to rotate/translate all Parts relative to the PrimaryPart. + + + + + LevelOfDetail + Automatically generate impostor meshes to be rendered outside of streaming radius. + + + + + + + + Status + true + 100 + 2 + + + + + move + true + Use MoveTo() instead + + + + + + + + DataModel + The root of ROBLOX's parent-child hierarchy (commonly known as game after the global variable used to access it) + + + + + + OnClose + true + Deprecated. Use DataModel.BindToClose + + + + + + + + + + + PrivateServerId + true + + + + + PrivateServerOwnerId + true + + + + + + VIPServerId + true + + + + + VIPServerOwnerId + true + + + + + + Workspace + + + + + workspace + true + Deprecated. Use Workspace + + + + + ShowMouse + true + Deprecated. Use Workspace.IsMouseCursorVisible + + + + + IsLoaded + Returns true if the game has finished loading, false otherwise. Check this before listening to the Loaded signal to ensure a script knows when a game finishes loading. + + + + + + + + Loaded + Fires when the game finishes loading. Use this to know when to remove your custom loading gui. It is best to check IsLoaded() before connecting to this event, as the game may load before the event is connected to. + + + + + + + + SetPlaceID + true + Use SetPlaceId() instead + + + + + SetCreatorID + true + Use SetCreatorId() instead + + + + + + + + DataStoreService + Responsible for storing data across multiple user created places + + + + + + GetDataStore + Returns a data store with the given name and scope + + + + + GetGlobalDataStore + Returns the default data store + + + + + GetOrderedDataStore + Returns an ordered data store with the given name and scope + + + + + + + + GlobalDataStore + Exposes functions for saving and loading data for the DataStoreService + -1 + + + + + + OnUpdate + Sets callback as a function to be executed any time the value associated with key is changed. It is important to disconnect the connection when the subscription to the key is no longer needed. + + + + + + + + GetAsync + Returns the value of the entry in the DataStore with the given key + + + + + IncrementAsync + Increments the value of a particular key amd returns the incremented value + + + + + SetAsync + Sets the value of the key. This overwrites any existing data stored in the key + + + + + UpdateAsync + Retrieves the value of the key from the website, and updates it with a new value. The callback until the value fetched matches the value on the web. Returning nil means it will not save. + + + + + + + + OrderedDataStore + A type of DataStore where values must be positive integers. This makes OrderedDataStore suitable for leaderboard related scripting where you are required to order large amounts of data efficiently. + -1 + + + + + + GetSortedAsync + Returns a DataStorePages object. The length of each page is determined by pageSize, and the order is determined by isAscending. minValue and maxValue are optional parameters which will filter the result. + + + + + + + + HopperBin + true + 240 + 22 + + + + + + Camera + 5 + 5 + Model + + + + + CameraSubject + Where the Camera's focus is. Any rotation of the camera will be about this subject. + + + + + CameraType + Defines how the camera will behave. <a href='http://wiki.roblox.com/index.php/CameraType' target='_blank'>More info</a> + + + + + CoordinateFrame + true + The current position and rotation of the Camera. For most CameraTypes, the rotation is set such that the CoordinateFrame lookVector is pointing at the Focus. + + + + + CFrame + The current position and rotation of the Camera. For most CameraTypes, the rotation is set such that the CoordinateFrame lookVector is pointing at the Focus. + + + + + FieldOfViewMode + Determines how the field of view responds to changes of screen size and aspect ratio. + + + + + FieldOfView + Describes the view angle along the vertical viewport axis. + + + + + DiagonalFieldOfView + Describes the view angle along the diagonal viewport axis. + + + + + MaxAxisFieldOfView + Describes the view angle along the maximum-length viewport axis. + + + + + Focus + The current CoordinateFrame that the camera is looking at. Note: it is not always guaranteed that the camera is always looking here. + + + + + ViewportSize + Holds the x,y screen resolution of the viewport the camera is presenting (note: this can differ from the AbsoluteSize property of a full screen gui). + + + + + NearPlaneZ + The negative z-offset of the view frustum's near clipping plane. + + + + + + + GetRoll + Returns the camera's current roll. Roll is defined in radians, and is stored as the delta from the camera's y axis default normal vector. + + + + + WorldToScreenPoint + Takes a 3D position in the world and projects it onto x,y coordinates of screen space. Returns two values, first is a Vector3 that has x,y position and z position which is distance from camera (negative if behind camera, positive if in front). Second return value is a boolean indicating if the first argument is an on-screen coordinate. + + + + + ScreenPointToRay + Takes a 2D screen position and produces a Ray object to be used for 3D raycasting. Input is x,y screen coordinates, and a (optional, defaults to 0) z position which sets how far in the camera look vector to start the ray origin. + + + + + ViewportPointToRay + Same as ScreenPointToRay, except no GUI offsets are taken into account. Useful for things like casting a ray from the middle of the Camera.ViewportSize + + + + + WorldToViewportPoint + Same as WorldToScreenPoint, except no GUI offsets are taken into account. + + + + + SetRoll + Sets the camera's current roll. Roll is defined in radians, and is stored as the delta from the camera's y axis default normal vector. + + + + + + + + Players + 20 + 21 + + + + + CharacterAutoLoads + true + Set to true, when a player joins a game, they get a character automatically, as well as when they die. When set to false, characters do not auto load and will only load in using Player:LoadCharacter(). + + + + + + + players + true + Use GetPlayers() instead + + + + + + + + ReplicatedStorage + 30 + 70 + A container whose contents are replicated to all clients and the server. + + + + + + RobloxReplicatedStorage + false + + + + + + ReplicatedFirst + 30 + 70 + A container whose contents are replicated to all clients (but not back to the server) first before anything else. Useful for creating loading guis, tutorials, etc. + + + + + RemoveRobloxLoadingScreen + Removes the default Roblox loading screen from view. Call this when you are ready to either show your own loading gui, or when the game is ready to play. + + + + + + + + ServerStorage + 30 + 69 + A container whose contents are only on the server. + + + + + + ServerScriptService + 30 + 71 + A container whose contents should be scripts. Scripts that are added to the container are run on the server. + + + + + + ReplicatedScriptService + 30 + 70 + A container whose contents should be scripts. Scripts that are added to the container are run on the server and the client. + + + + + + StudioService + A service for interfacing with the current studio state from Lua. + + + + + + Lighting + 30 + 13 + Responsible for all lighting aspects of the world (affects how things are rendered). + + + + + GetMinutesAfterMidnight + The number of minutes that the current time is past midnight. If currently at midnight, returns 0. Will return decimal values if not at an exact minute. + + + + + GetMoonDirection + Returns the lookVector (Vector3) of the moon. If this lookVector was used in a CFrame, the Part would face the moon. + + + + + GetMoonPhase + Currently always returns 0.75. MoonPhase cannot be edited. + + + + + GetSunDirection + Returns the lookVector (Vector3) of the sun. If this lookVector was used in a CFrame, the Part would face the sun. + + + + + SetMinutesAfterMidnight + Sets the time to be a certain number of minutes after midnight. This works with integer and decimal values. + + + + + + + Ambient + The hue of the global lighting. Changing this changes the color tint of all objects in the Workspace. + + + + + Brightness + How much global light each Part in the Workspace receives. Standard range is 0 to 2 (0 being little light), but can be increased all the way to 10 (colors start to be appear very different at this value). + 0 + 10 + 1000 + + + + + EnvironmentDiffuseScale + Sets scale [0-1] of Diffuse Environment Lighting to add to Ambient. + 0 + 1 + 1000 + + + + + EnvironmentSpecularScale + Sets scale [0-1] of Specular Environment Lighting to add to Ambient. + 0 + 1 + 1000 + + + + + ExposureCompensation + Exposure compensation amount. Applies a bias to the exposure level prior to the tonemap step. +1 indicates twice as much exposure and -1 means half as much exposure. + -3 + 3 + 600 + + + + + ShadowSoftness + This property controls how blurry the shadows are. + 0 + 1 + 100 + + + + + ColorShift_Bottom + The hue of global lighting on the bottom surfaces of an object. + + + + + ColorShift_Top + The hue of global lighting on the top surfaces of an object. + + + + + GeographicLatitude + The latitude position the level is placed at. This affects sun position. <a href='http://wiki.roblox.com/index.php/GeographicLatitude' target='_blank'>More info</a> + 0 + 360 + 360 + + + + + GlobalShadows + Flag enabling shadows from sun and moon in the place + + + + + OutdoorAmbient + Effective ambient value for outdoors, effectively shadow color outdoors (requires GlobalShadows enabled) + + + + + Outlines + Flag enabling or disabling outlines on parts and terrain + + + + + ShadowColor + Color the shadows appear as. Shadows are drawn mostly for characters, but depending on the lighting will also show for Parts in the Workspace. Rendering settings can also affect if shadows are drawn. + + + + + TimeOfDay + A string that represent the current time of day. Time is in 24-hour clock format "XX::YY:ZZ", where X is hour, Y is minute, and Z is seconds. + + + + + ClockTime + 0 + 24 + 240 + + + + + FogColor + A Color3 value that changes the hue of the atmosphere, mixed with the sky color. + + + + + FogEnd + The distance at which fog completely blocks your vision. This distance is relative to the camera position. Units are in studs + + + + + FogStart + The distance at which the fog gradient begins. This distance is relative to the camera position. Units are in studs. + + + + + + + LightingChanged + Fired whenever a property of Lighting is changed, or a skybox or atmosphere is added or removed. Skyboxes are of type 'Sky' these and 'Atmosphere' should be parented directly to lighting. + + + + + + + + TestService + 1000 + 68 + + + + + + DebuggerManager + + + + + + + + ScriptDebugger + + + + + + + + DebuggerBreakpoint + + + + + + + + DebuggerWatch + + + + + + + + Debris + 30 + A service that provides utility in cleaning up objects + + + + + addItem + true + Use AddItem() instead + + + + + AddItem + Adds an Instance into the debris service that will later be destroyed. Second argument 'lifetime' is optional and specifies how long (in seconds) to wait before destroying the item. If no time is specified then the item added will automatically be destroyed in 10 seconds. + + + + + + + MaxItems + true + Deprecated. No replacement + + + + + + + + Accoutrement + 20 + 32 + false + + + + + + Player + false + 10 + 12 + + + + + + CharacterAppearance + false + Model + + + + + CameraMode + An enum that describes how a Player's camera is allowed to behave. <a href='http://wiki.roblox.com/index.php/CameraMode' target='_blank'>More info</a>. + + + + + DataReady + true + Read-only. If true, this Player's persistent data can be loaded, false otherwise. <a href='http://wiki.roblox.com/index.php/ROBLOX_Scripting_How_To:_Data_Persistence' target='_blank'>Info on Data Persistence</a>. + + + + + DataComplexity + true + + + + + + + + LoadCharacter + true + Loads in a new character for this player. This will replace the player's current character, if they have one. This should be used in conjunction with Players.CharacterAutoLoads to control spawning of characters. This function only works from a server-side script (NOT a LocalScript). + + + + + LoadData + true + + + + + SaveData + true + + + + + SaveBoolean + true + + + + + SaveInstance + true + + + + + SaveString + true + + + + + LoadBoolean + true + + + + + LoadNumber + true + + + + + LoadString + true + + + + + LoadInstance + true + + + + + SaveNumber + true + + + + + playerFromCharacter + true + Use GetPlayerFromCharacter() instead + + + + + SetUnder13 + true + + + + + + + + WaitForDataReady + true + true + Yields until the persistent data for this Player is ready to be loaded. <a href='http://wiki.roblox.com/index.php/ROBLOX_Scripting_How_To:_Data_Persistence' target='_blank'>Info on Data Persistence</a>. + + + + + + + + + Idled + Fired periodically after the user has been AFK for a while. Currently this event is only fired for the *local* Player. "time" is the time in seconds that the user has been idle. + + + + + + + + Workspace + 5 + 19 + + + + + FindPartsInRegion3 + Returns parts in the area defined by the Region3, up to specified maxCount or 100, whichever is less + + + + + FindPartsInRegion3WithIgnoreList + Returns parts in the area defined by the Region3, up to specified maxCount or 100, whichever is less + + + + + FindPartOnRay + Deprecated. Use WorldRoot:Raycast() instead + true + + + + + FindPartOnRayWithIgnoreList + Deprecated. Use WorldRoot:Raycast() instead + true + + + + + + + PGSPhysicsSolverEnabled + Boolean used to enable the new physics solver + + + + + FallenPartsDestroyHeight + Sets the height at which falling characters and parts are destroyed. This property is not scriptable and can only be set in Studio + + + + + + + + BasePart + A structural class, not creatable + 3 + false + + + + + + Color + Color3 of the part. + + + + + CFrame + Contains information regarding the Part's position and a matrix that defines the Part's rotation. Can read/write. <a href='http://wiki.roblox.com/index.php/Cframe' target='_blank'>More info</a> + + + + + CanCollide + Determines whether physical interactions with other Parts are respected. If true, will collide and react with physics to other Parts. If false, other parts will pass thru instead of colliding + + + + + Anchored + Determines whether or not physics acts upon the Part. If true, part stays 'Anchored' in space, not moving regardless of any collision/forces acting upon it. If false, physics works normally on the part. + + + + + Massless + If true the part will be massless when welded to another part that is not massless. The part will still have mass like a normal part if it is an assembly root part according to GetRootPart(). + + + + + RootPriority + An integer from -127 to 127. Compares before other all other part properties besides massless for deciding which part is the assembly root part according to GetRootPart(). + + + + + Elasticity + A float value ranging from 0.0f to 1.0f. Sets how much the Part will rebound against another. a value of 1 is like a superball, and 0 is like a lead block. + 0 + 1 + + + + + Friction + A float value ranging from 0.0f to 1.0f. Sets how much the Part will be able to slide. a value of 1 is no sliding, and 0 is no friction, so infinite sliding. + 0 + 2 + + + + + Locked + Determines whether building tools (in-game and studio) can manipulate this Part. If true, no editing allowed. If false, editing is allowed. + + + + + CastShadow + Determines whether this Part casts a shadow. + + + + + Material + Specifies the look and feel the Part should have. Note: this does not define the color the Part is, see BrickColor for that. <a href='http://wiki.roblox.com/index.php/Material' target='_blank'>More info</a> + + + + + Reflectance + Specifies how shiny the Part is. A value of 1 is completely reflective (chrome), while a value of 0 is no reflectance (concrete wall) + 0 + 1 + + + + + ResizeIncrement + Sets the value for the smallest change in size allowable by the Resize(NormalId, int) function. + + + + + ResizeableFaces + Sets the value for the faces allowed to be resized by the Resize(NormalId, int) function. + + + + + Transparency + Sets how visible an object is. A value of 1 makes the object invisible, while a value of 0 makes the object opaque. + 0 + 1 + + + + + Velocity + How fast the Part is traveling in studs/second. This property is NOT recommended to be modified directly, unless there is good reason. Otherwise, try using a BodyForce to move a Part. + + + + + PositionLocal + Position relative to parent part, or global space if there is no parent. + + + + + OrientationLocal + Orientation relative to parent part, or global space if there is no parent. + + + + + Orientation + Rotation around X, Y, and Z axis. Rotations applied in YXZ order. + + + + + Rotation + + + + + CenterOfMass + + + + + + + + makeJoints + Use MakeJoints() instead + true + + + + + MakeJoints + Creates the appropriate SurfaceJoints with all parts that are touching this Instance (including internal joints in the Instance, as in a Model). This uses the SurfaceTypes defined on the surfaces of parts to create the appropriate welds. <a href='http://wiki.roblox.com/index.php/MakeJoints' target='_blank'>More info</a> + + + + + BreakJoints + Destroys SurfaceJoints with all parts that are touching this Instance (including internal joints in the Instance, as in a Model). + + + + + GetMass + Returns a number that is the mass of this Instance. Mass of a Part is immutable, and is changed only by the size of the Part. + + + + + Resize + Resizes a Part in the direction of the face defined by 'NormalId', by the amount specified by 'deltaAmount'. If the operation will expand the part to intersect another Instance, the part will not resize at all. Return true if the call is successful, false otherwise. + + + + + getMass + Use GetMass() instead + true + + + + + + + OutfitChanged + true + + + + + LocalSimulationTouched + true + Deprecated. Use Touched instead + + + + + StoppedTouching + + Deprecated. Use TouchEnded instead + + + + + TouchEnded + Fired when the part stops touching another part + + + + + + + Part + Parts + A plastic building block - the fundamental component of ROBLOX + 110 + 1 + Workspace + + + + + TrussPart + Parts + An extendable building truss + 120 + 1 + Model + + + + + WedgePart + Parts + A Wedge Part + 120 + 1 + Model + + + + + PrismPart + A Prism Part + false + true + 120 + 1 + + + + + PyramidPart + A Pyramid Part + false + true + 120 + 1 + + + + + ParallelRampPart + A ParallelRamp Part + false + true + 120 + 1 + + + + + RightAngleRampPart + A RightAngleRamp Part + false + true + 120 + 1 + + + + + CornerWedgePart + Parts + A CornerWedge Part + 120 + 1 + Workspace + + + + + PlayerGui + A container instance that syncs data between a single player and the server. ScreenGui objects that are placed in this container will be shown to the Player parent only + 130 + 46 + + + + + SelectionImageObject + Overrides the default selection adornment (used for gamepads). For best results, this should point to a GuiObject. + + + + + + + PlayerScripts + A container instance that contains LocalScripts. LocalScript objects that are placed in this container will be execute only when a Player is the parent. + 130 + 78 + + + + + StandalonePluginScripts + A container instance that contains Scripts. Useful only for Plugins. When Studio starts, we load plugins into the 'UserPlugin' data model and execute Scripts contained in StandalonePluginScripts container. If a plugin doesn't have such a container then the plugin isn't loaded into UserPlugin data model. When a data model for a place is created (e.g. the Edit data model), we load plugins into said data model and execute only those Scripts which are not contained in StandalonePluginScripts container. + 130 + 78 + true + + + + + StarterPlayerScripts + A container instance that contains LocalScripts. LocalScript objects that are placed in this container will be copied to new Players on startup. + 130 + 78 + false + + + + + StarterCharacterScripts + A container instance that contains LocalScripts. LocalScript objects that are placed in this container will be copied to new characters on startup. + 130 + 78 + false + + + + + + GuiMain + Deprecated, please use ScreenGui + true + 140 + 47 + + + + + + LayerCollector + The base class of ScreenGui, BillboardGui, and SurfaceGui. + false + + + + Enabled + Whether or not this should be displayed. + + + ZIndexBehavior + Controls the behavior of the ZIndex property for descendants of this object. It can be set to Global (Default) or Sibling. + + + + + + + ScreenGui + GUI + The core GUI object on which tools are built. Add Frames/Labels/Buttons to this object to have them rendered as a 2D overlay + 140 + 47 + BasePlayerGui + + + + + FunctionalTest + Deprecated. Use TestService instead + true + 10 + + + + + BillboardGui + GUI + A GUI that adorns an object in the 3D world. Add Frames/Labels/Buttons to this object to have them rendered while attached to a 3D object + 140 + 64 + GuiBase2d + + + + + + Adornee + The Object the billboard gui uses as its base to render from. Currently, the only way to set this property is thru a script, and must exist in the workspace. This will only render if the object assigned derives from BasePart. + + + + + AbsolutePosition + A read-only Vector2 value that is the GuiObject's current position (x,y) in pixel space, from the top left corner of the GuiObject. + + + + + AbsoluteSize + A read-only Vector2 value that is the GuiObject's current size (width, height) in pixel space. + + + + + Active + If true, this GuiObject can fire mouse events and will pass them to any GuiObjects layered underneath, while false will do neither. + + + + + AlwaysOnTop + If true, billboard gui does not get occluded by 3D objects, but always renders on the screen. + + + + + Enabled + If true, billboard gui will render, otherwise rendering will be skipped. + + + + + ExtentsOffset + A Vector3 (x,y,z) defined in studs that will offset the gui from the extents of the 3d object it is rendering from. + + + + + PlayerToHideFrom + Specifies a Player that the BillboardGui will not render to. + + + + + StudsOffset + A Vector3 (x,y,z) defined in studs that will offset the gui from the centroid of the 3d object it is rendering from + + + + + SizeOffset + A Vector2 (x,y) defined in studs that will offset the gui size from it's current size. + + + + + Size + A UDim2 value describing the size of the BillboardGui. More information on UDim2 is available <a href='http://wiki.roblox.com/index.php/UDim2' target='_blank'>here</a>. Relative values are defined as one-to-one with studs. + + + + + LightInfluence + Specifies the amount of influence lighting has on the billboard gui. A value of 0 is unlit, 1 is fully lit. Fractional values blend from unlit to lit. + 0 + 1 + + + + + + + + SurfaceGui + GUI + Renders its contained GuiObjects flat against the face of a part. + 140 + 64 + GuiBase2d + + + + + + Adornee + The Object the surface gui uses as its base to render from. Currently, the only way to set this property is thru a script, and must exist in the workspace. This will only render if the object assigned derives from BasePart. + + + + + Active + If true, this GuiObject can fire mouse events and will pass them to any GuiObjects layered underneath, while false will do neither. + + + + + Enabled + If true, surface gui will render, otherwise rendering will be skipped. + + + + + LightInfluence + Specifies the amount of influence lighting has on the surface gui. A value of 0 is unlit, 1 is fully lit. Fractional values blend from unlit to lit. + 0 + 1 + + + + + + + + + + GuiBase2d + false + + + + + + AbsolutePosition + A read-only Vector2 value that is the GuiObject's current position (x,y) in pixel space, from the top left corner of the GuiObject. + + + + + AbsoluteSize + A read-only Vector2 value that is the GuiObject's current size (width, height) in pixel space. + + + + + + + + InputObject + An object that describes a particular user input, such as mouse movement, touches, keyboard, and more. + + + + + UserInputType + An enum that describes what kind of input this object is describing (mousebutton, touch, etc.). See Enum.UserInputType for more info. + + + + + UserInputState + An enum that describes what state of a particular input (touch began, touch moved, touch ended, etc.). See Enum.UserInputState for more info. + + + + + Position + A Vector3 value that describes a positional value of this input. For mouse and touch input, this is the screen position of the mouse/touch, described in the x and y components. For mouse wheel input, the z component describes whether the wheel was moved forward or backward. + + + + + KeyCode + An enum that describes what kind of input is being pressed. For types of input like Keyboard, this describes what key was pressed. For input like mousebutton, this provides no additional information. + + + + + + + + GuiObject + false + + + + + + TweenPosition + Smoothly moves a GuiObject from its current position to 'endPosition'. The only required argument is 'endPosition'. <a href='http://wiki.roblox.com/index.php/TweenPosition' target='_blank'>More info</a> + + + + + TweenSize + Smoothly translates a GuiObject's current size to 'endSize'. The only required argument is 'endSize'. <a href='http://wiki.roblox.com/index.php/TweenSize' target='_blank'>More info</a> + + + + + TweenSizeAndPosition + Smoothly translates a GuiObject's current size to 'endSize', and also smoothly translates the GuiObject's current position to 'endPosition'. The only required arguments are 'endSize' and 'endPosition'. <a href='http://wiki.roblox.com/index.php/TweenSizeAndPosition' target='_blank'>More info</a> + + + + + + + + Active + If true, this GuiObject can fire mouse events and will pass them to any GuiObjects layered underneath, while false will do neither. + + + + + BackgroundColor3 + A Color3 value that specifies the background color for the GuiObject. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + BackgroundTransparency + A number value that specifies how transparent the background of the GuiObject is. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + 0 + 1 + + + + + BorderColor3 + A Color3 value that specifies the color of the outline of the GuiObject. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + BorderSizePixel + A number value that specifies the thickness (in pixels) of the outline of the GuiObject. Currently this value can only be set to either 0 or 1, any other number has no effect. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + ClipsDescendants + If set to true, any descendants of this GuiObject will only render if contained within it's borders. If set to false, all descendants will render regardless of position. + + + + + Draggable + true + If true, allows a GuiObject to be dragged by the user's mouse. The events 'DragBegin' and 'DragStopped' are fired when the appropriate action happens, and only will fire on Draggable=true GuiObjects. + + + + + Size + A UDim2 value describing the size of the GuiObject on screen in both absolute and relative coordinates. More information on UDim2 is available <a href='http://wiki.roblox.com/index.php/UDim2' target='_blank'>here</a>. + + + + + Position + A UDim2 value describing the position of the top-left corner of the GuiObject on screen. More information on UDim2 is available <a href='http://wiki.roblox.com/index.php/UDim2' target='_blank'>here</a>. + + + + + SizeConstraint + The direction(s) that an object can be resized in. <a href='http://wiki.roblox.com/index.php/SizeConstraint' target='_blank'>More info</a>. + + + + + ZIndex + Describes the ordering in which overlapping GuiObjects will be drawn. A value of 1 is drawn first, while higher values are drawn in ascending order (each value draws over the last). + + + + + BackgroundColor + true + Deprecated. Use BackgroundColor3 instead + + + + + BorderColor + true + Deprecated. Use BorderColor3 instead + + + + + SelectionImageObject + Overrides the default selection adornment (used for gamepads). For best results, this should point to a GuiObject. + + + + + + + + DragBegin + true + Fired when a GuiObject with Draggable set to true starts to be dragged. 'InitialPosition' is a UDim2 value of the position of the GuiObject before any drag operation began. + + + + + DragStopped + true + Always fired after a DragBegin event, DragStopped is fired when the user releases the mouse button causing a drag operation on the GuiObject. Arguments 'x', and 'y' specify the top-left absolute position of the GuiObject when the event is fired. + + + + + MouseEnter + Fired when the mouse enters a GuiObject, as long as the GuiObject is active (see active property for more detail). Arguments 'x', and 'y' specify the absolute pixel position of the mouse. + + + + + MouseLeave + Fired when the mouse leaves a GuiObject, as long as the GuiObject is active (see active property for more detail). Arguments 'x', and 'y' specify the absolute pixel position of the mouse. + + + + + MouseMoved + Fired when the mouse is inside a GuiObject and moves, as long as the GuiObject is active (see active property for more detail). Arguments 'x', and 'y' specify the absolute pixel position of the mouse. + + + + + + TouchTap + Fired when a user taps their finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the tap gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchPinch + Fired when a user pinches their fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the pinch gesture. 'scale' is a float that indicates the difference from the beginning of the pinch gesture. 'velocity' is a float indicating how quickly the pinch gesture is happening. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. + + + + + TouchSwipe + Fired when a user swipes their fingers on a TouchEnabled device. 'swipeDirection' is an Enum.SwipeDirection, indicating the direction the user swiped. 'numberOfTouches' is an int that indicates how many touches were involved with the gesture. This event only fires locally. + + + + + TouchLongPress + Fired when a user holds at least one finger for a short amount of time on the same screen position on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. + + + + + TouchRotate + Fired when a user rotates two fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'rotation' is a float indicating how much the rotation has gone from the start of the gesture. 'velocity' is a float that indicates how quickly the gesture is being performed. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. + + + + + TouchPan + Fired when a user drags at least one finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'totalTranslation' is a Vector2, indicating how far the pan gesture has gone from its starting point. 'velocity' is a Vector2 that indicates how quickly the gesture is being performed in each dimension. 'state' indicates the Enum.UserInputState of the gesture. + + + + + + InputBegan + Fired when a user begins interacting via a Human-Computer Interface device (Mouse button down, touch begin, keyboard button down, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. + + + + + InputChanged + Fired when a user changes interacting via a Human-Computer Interface device (Mouse move, touch move, mouse wheel, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. + + + + + InputEnded + Fired when a user stops interacting via a Human-Computer Interface device (Mouse button up, touch end, keyboard button up, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. + + + + + + + + + Frame + GUI + A container object used to layout other GUI objects + 150 + 48 + GuiBase2d + + + + + Style + Determines how a frame will look. Uses Enum.FrameStyle. <a href='http://wiki.roblox.com/index.php?title=API:Enum/FrameStyle' target='_blank'>More info</a> + + + + + + + ScrollingFrame + GUI + Studio.App.RobloxRibbonMainWindow.ScrollingFrameTooltip + 150 + 48 + GuiBase2d + + + + + ScrollingEnabled + Determines whether or not scrolling is allowed on this frame. If turned off, no scroll bars will be rendered. + + + + + CanvasSize + Determines the size of the area that is scrollable. The UDim2 is calculated using the parent gui's size, similar to the regular Size property on gui objects. + + + + + CanvasPosition + The absolute position the scroll frame is in respect to the canvas size. The minimum this can be set to is (0,0), while the max is the absolute canvas size - AbsoluteWindowSize. + + + + + AbsoluteWindowSize + The size in pixels of the frame, without the scrollbars. + + + + + ScrollBarThickness + How thick the scroll bar appears. This applies to both the horizontal and vertical scroll bars. Can be set to 0 for no bars render. + + + + + TopImage + The "Up" image on the vertical scrollbar. Size of this is always ScrollBarThickness by ScrollBarThickness. This is also used as the "left" image on the horizontal scroll bar. + + + + + MidImage + The "Middle" image on the vertical scrollbar. Size of this can vary in the y direction, but is always set at ScrollBarThickness in x direction. This is also used as the "mid" image on the horizontal scroll bar. + + + + + BottomImage + The "Down" image on the vertical scrollbar. Size of this is always ScrollBarThickness by ScrollBarThickness. This is also used as the "right" image on the horizontal scroll bar. + + + + + + + ImageLabel + GUI + A GUI object containing an Image + 180 + 49 + GuiBase2d + + + + + Image + Specifies the id of the texture to display. <a href='http://wiki.roblox.com/index.php?title=API:Class/ImageLabel/Image' target='_blank'>More info</a> + + + + + ScaleType + Specifies how an image should be displayed. See ScaleType for more info. + + + + + SliceCenter + If ScaleType is set to Slice, this Rect is used to specify the central part of the image. Everything outside of this is considered to be the border. + + + + + TileSize + If ScaleType is set to Tile, this sets the size of the tile. + + + + + + + + TextLabel + GUI + A GUI object containing text + 190 + 50 + GuiBase2d + + + + + TextColor + true + Deprecated. Use TextColor3 instead + + + + + + + TextButton + GUI + A GUI button containing text + 170 + 51 + GuiBase2d + + + + + TextColor + true + Deprecated. Use TextColor3 instead + + + + + + + TextBox + GUI + A text entry box + 170 + 51 + GuiBase2d + + + + + TextColor + true + Deprecated. Use TextColor3 instead + + + + + + + GuiButton + GUI + A GUI button containing an Image + false + 160 + 52 + + + + + AutoButtonColor + Determines whether a button changes color automatically when reacting to mouse events. + + + + + Modal + Allows the mouse to be free in first person mode. If a button with this property set to true is visible, the mouse is 'free' in first person mode. + + + + + Style + Determines how a button will look, including mouse event states. Uses Enum.ButtonStyle. <a href='http://wiki.roblox.com/index.php?title=API:Class/GuiButton/Style' target='_blank'>More info</a> + + + + + + + MouseButton1Click + Fired when the mouse is over the button, and the mouse down and up events fire without the mouse leaving the button. + + + + + MouseButton1Down + Fired when the mouse button is pushed down on a button. + + + + + MouseButton1Up + Fired when the mouse button is released on a button. + + + + + MouseButton2Click + This function currently does not work :( + + + + + MouseButton2Down + This function currently does not work :( + + + + + MouseButton2Up + This function currently does not work :( + + + + + + + ViewportFrame + GUI + A GUI that can show 3D objects + 30 + 52 + GuiBase2d + + + + + CurrentCamera + Current Camera of children objects + + + + + ImageTransparency + A number value that specifies how transparent the rendered image of the ViewportFrame is + 0 + 1 + + + + + ImageColor3 + The rendered image of the ViewportFrame will be multiplied by this color + + + + + Ambient + Changing this changes the color tint of all objects in the ViewportFrame. + + + + + LightColor + Directional light color for objects in the ViewportFrame. + + + + + LightDirection + Light direction. Value will be normalized. All values valid except (0,0,0). + + + + + + + ImageButton + GUI + A GUI button containing an Image + 160 + 52 + GuiBase2d + + + + + Image + Specifies the asset id of the texture to display. <a href='http://wiki.roblox.com/index.php?title=API:Class/ImageButton/Image' target='_blank'>More info</a> + + + + + ScaleType + Specifies how an image should be displayed. See ScaleType for more info. + + + + + SliceCenter + If ScaleType is set to Slice, this Rect is used to specify the central part of the image. Everything outside of this is considered to be the border. + + + + + TileSize + If ScaleType is set to Tile, this sets the size of the tile. + + + + + + + Handles + Adornments + A 3D GUI object to represent draggable handles + + 190 + 53 + + + + + ArcHandles + Adornments + A 3D GUI object to represent draggable arc handles + + 200 + 56 + + + + + SelectionBox + Adornments + A 3D GUI object to represent the visible selection around an object + 210 + 54 + + + + + SelectionSphere + Adornments + A 3D GUI object to represent the visible selection around an object + 210 + 54 + + + + + SurfaceSelection + Adornments + A 3D GUI object to represent the visible selection around a face of an object + 210 + 55 + + + + + Configuration + An object that can be placed under parts to hold Value objects that represent that part's configuration + 220 + 58 + + + + + HumanoidDescription + An object that specifies the appearance of Humanoid characters + 22 + 104 + + + + + Folder + An object that can be created to hold and organize objects + 10 + 77 + + + + + WorldModel + true + World + An object that contains a World. Supports rigid joints. Unlike Workspace, WorldModels do not support dynamics simulation. + 22 + 19 + + + + + Motor6D + Animations + The Motor6D object is used to make movable joints between two Parts. + 200 + 106 + + + + + BoxHandleAdornment + Adornments + The BoxHandleAdornment is a rectangular prism that can be adorned to a BasePart. + 205 + 111 + + + + + ConeHandleAdornment + Adornments + A ConeHandleAdornment is a cone that can be adorned to a BasePart. + 205 + 110 + + + + + CylinderHandleAdornment + Adornments + The CylinderHandleAdornment is a cylinder that can be adorned to a BasePart. + 205 + 109 + + + + + SphereHandleAdornment + Adornments + The SphereHandleAdornment is a sphere that can be adorned to a BasePart. + 205 + 112 + + + + + LineHandleAdornment + Adornments + The LineHandleAdornment is a line that can be adorned to a BasePart. + 205 + 107 + + + + + ImageHandleAdornment + Adornments + The ImageHandleAdornment is an image that can be adorned to a BasePart. + 205 + 108 + + + + + SelectionPartLasso + true + A visual line drawn representation between two part objects + 220 + 57 + + + + + SelectionPointLasso + true + A visual line drawn representation between two positions + 220 + 57 + + + + + PartPairLasso + A visual line drawn representation between two parts. + 220 + 57 + + + + + Pose + The pose of a joint relative to it's parent part in a keyframe + 220 + 60 + false + + + + + KeyframeMarker + Represents when an event should be fired in an animation + 220 + 60 + false + + + + + Keyframe + One keyframe of an animation + 220 + 60 + false + + + + + Animation + Animations + Represents a linked animation object, containing keyframes and poses. + 220 + 60 + + + + + AnimationTrack + Returned by a call to LoadAnimation. Controls the playback of an animation on a Humanoid. + 220 + 60 + + + + + AnimationController + Animations + Allows animations to be played on joints of the parent object. + 220 + 60 + + + + + CharacterMesh + Meshes + Modifies the appearance of a body part. + 220 + 60 + Model + + + + + Dialog + 3D Interfaces + An object used to make dialog trees to converse with players + 220 + 62 + + + + + ConversationDistance + The maximum distance that the player's character can be from the dialog's parent in order to use the dialog. + + + + + GoodbyeChoiceActive + Indicates whether or not an extra choice is available for the player to exit the dialog tree at this node. + + + + + GoodbyeDialog + The prompt text for an extra choice that allows the player to exit the dialog tree at this node. + + + + + InUse + Indicates whether or not the dialog is currently being used by one or more players. + + + + + InitialPrompt + The chat message that is displayed to the player when they first activate the dialog. + + + + + Purpose + Describes the purpose of the dialog, which is used to display a relevant icon on the dialog's activation button. + + + + + Tone + Describes the tone of the dialog, which is used to display a relevant color in the dialog interface. + + + + + BehaviorType + Indicates how the dialog may be used by players. Use Enum.DialogBehaviorType.SinglePlayer if only one player should interact with the dialog at a time, otherwise use Enum.DialogBehaviorType.MultiplePlayers. + + + + + + + GetCurrentPlayers + Returns an array of the players currently conversing with this dialog. + + + + + + + DialogChoice + 3D Interfaces + An object used to make dialog trees to converse with players + 220 + 63 + + + + + UnionOperation + A UnionOperation is a union of multiple parts + true + false + 105 + 73 + + + + + UsePartColor + Override the colors of the mesh with the part color. + + + + + + + NegateOperation + A NegateOperation can be used to create holes in other parts + true + false + 104 + 72 + + + + + UsePartColor + Override the colors of the mesh with the part color. + + + + + + + MeshPart + Parts + A MeshPart is a physically simulatable mesh + true + true + 105 + 73 + Model + + + + + Terrain + Object representing a high performance bounded grid of static 4x4 parts + true + false + 5 + 65 + + + + + WaterTransparency + 0 + 1 + + + + + WaterWaveSize + 0 + 1 + + + + + WaterWaveSpeed + 0 + 100 + + + + + WaterReflectance + 0 + 1 + + + + + Decoration + Enables terrain materials decoration + + + + + + + GetCell + Returns CellMaterial, CellBlock, CellOrientation + + + + + GetWaterCell + + Returns hasAnyWater, WaterForce, WaterDirection + + + + + SetWaterCell + + + + + + + + Light + Lights + Parent of all light objects + 30 + 13 + PVInstance + + + + + Brightness + 0 + 40 + 2000 + + + + + + + PointLight + Lights + Makes the parent part emit light in a spherical shape + 30 + 13 + PVInstance + + + + + Range + 0 + 60 + + + + + + + SpotLight + Lights + Makes the parent part emit light in a conical shape + 30 + 13 + PVInstance + + + + + Range + 0 + 60 + + + + + Angle + 0 + 180 + + + + + + + SurfaceLight + Lights + Makes the parent part emit light in a frustum shape from rectangle defined by part + 30 + 13 + PVInstance + + + + + Range + 0 + 60 + + + + + Angle + 0 + 180 + + + + + + + RemoteFunction + Scripting + Allow functions defined in one script to be called by another script across client/server boundary + 40 + 74 + + + + + InvokeClient + Server + + + + + InvokeServer + Client + + + + + + + OnClientInvoke + Client + + + + + OnServerInvoke + Server + + + + + + + RemoteEvent + Scripting + Allow events defined in one script to be subscribed to by another script across client/server boundary + 50 + 75 + + + + + FireAllClients + Server + + + + + FireClient + Server + + + + + FireServer + Client + + + + + + + OnClientEvent + Client + + + + + OnServerEvent + Server + + + + + + + TerrainRegion + Object representing a snapshot of the region of terrain + true + 20 + 65 + false + + + + + ModuleScript + Scripting + A script fragment. Only runs when another script uses require() on it. + 50 + 76 + + + + + + + + ContextActionResult + + + + Sink + If 'functionToBind' from ContextActionService:BindAction() returns Enum.ContextActionResult.Sink, the input event will stop at that function and no other bound actions under it will be invoked. This is the default behavior if 'functionToBind' does not return anything or yields in any way. + + + + + Pass + If 'functionToBind' from ContextActionService:BindAction() returns Enum.ContextActionResult.Pass, the input event is considered to have not been handled by 'functionToBind' and will continue being passed to actions bound to the same input type. + + + + + + Material + + + + Air + false + + + + + Water + false + + + + + Rock + false + + + + + Glacier + false + + + + + Snow + false + + + + + Sandstone + false + + + + + Mud + false + + + + + Basalt + false + + + + + Ground + false + + + + + CrackedLava + false + + + + + Asphalt + false + + + + + LeafyGrass + false + + + + + Salt + false + + + + + Limestone + false + + + + + Pavement + false + + + + + + Status + true + + + + Poison + true + + + + + Confusion + true + + + + + + SaveFilter + true + + + + + PrivilegeType + true + + + + + Genre + true + + + + + GearGenreSetting + true + + + + + GearType + true + + + + + SortOrder + The ordering to use for sorting an array of GuiObjects. + + + + Name + Sort by alphabetical ordering of the Name property. + + + + + LayoutOrder + Sort using the less than operator on the LayoutOrder property of GuiObject. + + + + + Custom + true + + + + + + ZIndexBehavior + Controls the behavior of the ZIndex property. + + + + Global + The ZIndex property will override the default value computed from the depth in the hierarchy. + + + + + Sibling + The ZIndex property will control the order that the GuiObject will be rendered relative to its siblings. + + + + + + ScaleType + Controls how an image is displayed. + + + + Stretch + Force the image to fill the available space. + + + + + Slice + Use the SliceCenter property to stretch the middle of the image but maintain crisp borders. + + + + + Tile + Tile the image using the TileSize property. + + + + + Fit + Size the image to the largest size that will fit in the available space while maintaining aspect ratio. + + + + + Crop + Fill the available space, maintaining aspect ratio by cropping the edges if necessary. + + + + + diff --git a/Client2020/SyntaxPlayerBeta.exe b/Client2020/SyntaxPlayerBeta.exe new file mode 100644 index 0000000..7a13b96 Binary files /dev/null and b/Client2020/SyntaxPlayerBeta.exe differ diff --git a/Client2020/content/avatar/character.rbxm b/Client2020/content/avatar/character.rbxm new file mode 100644 index 0000000..248da30 --- /dev/null +++ b/Client2020/content/avatar/character.rbxm @@ -0,0 +1,952 @@ + + null + nil + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + erik.cassel + RBX1 + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 194 + + 0 + 19.5 + 22.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 1 + 1 + + + + + 2 + 2 + + 0 + Mesh + + 0 + 0 + 0 + + + 1.25 + 1.25 + 1.25 + + + + 1 + 1 + 1 + + + + + + 5 + face + rbxasset://textures/face.png + 0 + + + + + + 0 + 0.600000024 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + HairAttachment + + + + + + 0 + 0.600000024 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + HatAttachment + + + + + + 0 + 0 + -0.600000024 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + FaceFrontAttachment + + + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + FaceCenterAttachment + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 194 + + 0 + 18 + 22.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 2 + 0 + true + 256 + Torso + 0 + 0 + 0 + 2 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 2 + 1 + + + + + 5 + roblox + + 0 + + + + + + 0 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckAttachment + + + + + + 0 + 0 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyFrontAttachment + + + + + + 0 + 0 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyBackAttachment + + + + + + -1 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftCollarAttachment + + + + + + 1 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightCollarAttachment + + + + + + 0 + -1 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistFrontAttachment + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistCenterAttachment + + + + + + 0 + -1 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistBackAttachment + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 194 + + 1.5 + 18 + 22.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Left Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 1.0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderAttachment + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftGripAttachment + + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 194 + + -1.5 + 18 + 22.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Right Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 1.0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderAttachment + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightGripAttachment + + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 194 + + 0.5 + 16 + 22.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Left Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftFootAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 194 + + -0.5 + 16 + 22.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Right Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightFootAttachment + false + + + + + + 0 + 100 + 100 + 0 + 50 + 100 + 89 + Humanoid + 100 + 2 + 16 + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 194 + + 0 + 18 + 22.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 0 + 0 + true + 256 + HumanoidRootPart + 0 + 0 + 0 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 1 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 2 + 1 + + + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootAttachment + false + + + + + \ No newline at end of file diff --git a/Client2020/content/avatar/characterR15.rbxm b/Client2020/content/avatar/characterR15.rbxm new file mode 100644 index 0000000..0dc757a --- /dev/null +++ b/Client2020/content/avatar/characterR15.rbxm @@ -0,0 +1,2332 @@ + + null + nil + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + Player + RBX9909D4D409004F18956C91B88A6A32A3 + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 311 + + -8.03512478 + 2.3499999 + -12.6754742 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + HumanoidRootPart + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 1 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + 1 + 1 + + 2 + 2 + 1 + + + + + + -0 + -0 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootRigAttachment + false + + + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -9.2211628 + 2.14999962 + -11.7571526 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 0.299999982 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532219986 + LeftHand + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999762 + 0.299999982 + 0.999999881 + + + + + + 0.000478863716 + 0.149999991 + 5.96046448e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftWristRigAttachment + false + + + + + + -1.1920929e-007 + -0.149999633 + -1.46306121e-007 + 1 + 0 + -0 + 0 + 6.12323426e-017 + 1 + 0 + -1 + 6.12323426e-017 + + LeftGripAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -9.2211628 + 2.84999967 + -11.7571526 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 1.20000029 + 1 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532219991 + LeftLowerArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999762 + 1.20000029 + 1 + + + + + + 0.000478506088 + 0.25000003 + 7.64462551e-020 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftElbowRigAttachment + false + + + + + + 0.000478506088 + -0.549999952 + 7.64462551e-020 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftWristRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -9.22116375 + 3.29999995 + -11.7571526 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 1.40000033 + 0.99999994 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532219996 + LeftUpperArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999762 + 1.40000033 + 0.99999994 + + + + + + 0.250109196 + 0.449999809 + 8.94069672e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderRigAttachment + false + + + + + + 0.000479102135 + -0.200000167 + 8.94069672e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftElbowRigAttachment + false + + + + + + 2.38418579e-007 + 0.700000286 + -2.70968314e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -6.84908628 + 2.14999962 + -13.5937948 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999881 + 0.299999982 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532219997 + RightHand + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999881 + 0.299999982 + 0.999999881 + + + + + + 3.57627869e-007 + 0.149999991 + 5.96046448e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightWristRigAttachment + false + + + + + + 0 + -0.149999633 + -1.46306121e-007 + 1 + 0 + -0 + 0 + 6.12323426e-017 + 1 + 0 + -1 + 6.12323426e-017 + + RightGripAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -6.84908628 + 2.84999967 + -13.5937948 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 1.20000029 + 1 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532219999 + RightLowerArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999762 + 1.20000029 + 1 + + + + + + 1.1920929e-007 + 0.25000003 + 7.64462551e-020 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightElbowRigAttachment + false + + + + + + 1.1920929e-007 + -0.549999952 + -6.86244753e-018 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightWristRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -6.84908581 + 3.29999995 + -13.5937958 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999642 + 1.40000033 + 0.99999994 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220004 + RightUpperArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999642 + 1.40000033 + 0.99999994 + + + + + + -0.250020266 + 0.449999809 + 8.94069672e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderRigAttachment + false + + + + + + -5.96046448e-007 + -0.200000167 + 8.94069672e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightElbowRigAttachment + false + + + + + + -9.53674316e-007 + 0.700000286 + -2.70968314e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.03512478 + 3.19999981 + -12.6754742 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + true + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 2 + 1.60000014 + 1.00000036 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220007 + UpperTorso + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 2 + 1.60000014 + 1.00000036 + + + + + + -5.96046448e-008 + -0.450000018 + 1.1920929e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistRigAttachment + false + + + + + + -5.96046448e-008 + 0.799999952 + 1.1920929e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckRigAttachment + false + + + + + + -1.24989128 + 0.549999952 + 1.1920929e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderRigAttachment + false + + + + + + 1.24998045 + 0.549999952 + 1.1920929e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderRigAttachment + false + + + + + + -5.96046448e-008 + -0.200000048 + -0.499999881 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyFrontAttachment + false + + + + + + -5.96046448e-008 + -0.200000048 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyBackAttachment + false + + + + + + -0.999999881 + 0.800000191 + -7.27397378e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftCollarAttachment + false + + + + + + 0.99999994 + 0.799999952 + 4.61295997e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightCollarAttachment + false + + + + + + 0.0 + 0.8 + 0.0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.43047047 + 0.150000095 + -12.3693666 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 1 + 0.300000191 + 1 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220012 + LeftFoot + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 1 + 0.300000191 + 1 + + + + + + 0 + 0.05 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftAnkleRigAttachment + false + + + + + + 0 + -0.15 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftFootAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.43047047 + 0.950000286 + -12.3693666 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.99999994 + 1.50000036 + 1.00000012 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220017 + LeftLowerLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.99999994 + 1.50000036 + 1.00000012 + + + + + + -0 + 0.249999642 + -1.78813934e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftKneeRigAttachment + false + + + + + + -1.78813934e-007 + -0.749997616 + 6.29340548e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftAnkleRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.43047047 + 1.49999988 + -12.3693666 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 1.00000036 + 1.49999976 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220018 + LeftUpperLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 1.00000036 + 1.49999976 + 0.999999881 + + + + + + 5.96046448e-008 + 0.5 + -1.63912773e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftHipRigAttachment + false + + + + + + 5.96046448e-008 + -0.299999952 + -1.63912773e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftKneeRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -7.63977861 + 0.150000095 + -12.9815807 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.99999994 + 0.300000191 + 1 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220020 + RightFoot + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.99999994 + 0.300000191 + 1 + + + + + + 0 + 0.05 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightAnkleRigAttachment + false + + + + + + 0 + -0.15 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightFootAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -7.63977861 + 0.950000286 + -12.9815807 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.99999994 + 1.50000036 + 1.00000012 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220027 + RightLowerLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.99999994 + 1.50000036 + 1.00000012 + + + + + + -0 + 0.249999642 + 4.35260044e-005 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightKneeRigAttachment + false + + + + + + -0 + -0.750000477 + 9.82746205e-005 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightAnkleRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -7.63977861 + 1.49999988 + -12.9815807 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 1.00000048 + 1.49999976 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220031 + RightUpperLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 1.00000048 + 1.49999976 + 0.999999881 + + + + + + -0 + 0.5 + -1.04308128e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightHipRigAttachment + false + + + + + + -0 + -0.299999952 + 4.36005103e-005 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightKneeRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.03512478 + 2.19999981 + -12.6754742 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + true + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 1.99999976 + 0.399999976 + 1.00000012 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220036 + LowerTorso + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 1.99999976 + 0.399999976 + 1.00000012 + + + + + + -1.1920929e-007 + 0.150000036 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootRigAttachment + false + + + + + + -1.1920929e-007 + 0.550000072 + 7.64462551e-020 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistRigAttachment + false + + + + + + -0.500000119 + -0.199999958 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftHipRigAttachment + false + + + + + + 0.499999881 + -0.199999958 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightHipRigAttachment + false + + + + + + 0.0 + -0.2 + 0.0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistCenterAttachment + false + + + + + + 0.0 + -0.2 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistFrontAttachment + false + + + + + + 0.0 + -0.2 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistBackAttachment + false + + + + + + 0 + 100 + 100 + 1.35000002 + 50 + 100 + 89 + Humanoid + 100 + 2 + 1 + 16 + + + + Animator + + + + + BodyWidthScale + 1 + + + + + BodyHeightScale + 1 + + + + + BodyDepthScale + 1 + + + + + HeadScale + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.03495789 + 4.5 + -12.6752586 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + 1 + 1 + + 2 + 1 + 1 + + + + + 2 + 2 + + 0 + Mesh + + 0 + 0 + 0 + + + 1.25 + 1.25 + 1.25 + + + + 1 + 1 + 1 + + + + + + + 3.93568822e-009 + 0 + -0.000272244215 + 1 + 7.87137555e-009 + 3.02998127e-015 + -7.87137555e-009 + 1 + -4.1444258e-016 + -3.02998127e-015 + 4.14442554e-016 + 1 + + FaceCenterAttachment + false + + + + + + 3.93568866e-009 + 0 + -0.600272298 + 1 + 7.87137555e-009 + 3.02998127e-015 + -7.87137555e-009 + 1 + -4.1444258e-016 + -3.02998127e-015 + 4.14442554e-016 + 1 + + FaceFrontAttachment + false + + + + + + 8.65851391e-009 + 0.599999905 + -0.000272244215 + 1 + 7.87137555e-009 + 3.02998127e-015 + -7.87137555e-009 + 1 + -4.1444258e-016 + -3.02998127e-015 + 4.14442554e-016 + 1 + + HairAttachment + false + + + + + + 8.65851391e-009 + 0.599999905 + -0.000272244215 + 1 + 7.87137555e-009 + 3.02998127e-015 + -7.87137555e-009 + 1 + -4.1444258e-016 + -3.02998127e-015 + 4.14442554e-016 + 1 + + HatAttachment + false + + + + + + -0 + -0.500000119 + -0.000272244215 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckRigAttachment + false + + + + + 5 + face + rbxasset://textures/face.png + 0 + + + + + \ No newline at end of file diff --git a/Client2020/content/avatar/characterR15V2.rbxm b/Client2020/content/avatar/characterR15V2.rbxm new file mode 100644 index 0000000..b02f520 Binary files /dev/null and b/Client2020/content/avatar/characterR15V2.rbxm differ diff --git a/Client2020/content/avatar/compositing/CompositExtraSlot0.mesh b/Client2020/content/avatar/compositing/CompositExtraSlot0.mesh new file mode 100644 index 0000000..87b9d85 Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositExtraSlot0.mesh differ diff --git a/Client2020/content/avatar/compositing/CompositExtraSlot1.mesh b/Client2020/content/avatar/compositing/CompositExtraSlot1.mesh new file mode 100644 index 0000000..6874c5e Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositExtraSlot1.mesh differ diff --git a/Client2020/content/avatar/compositing/CompositExtraSlot2.mesh b/Client2020/content/avatar/compositing/CompositExtraSlot2.mesh new file mode 100644 index 0000000..423f198 Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositExtraSlot2.mesh differ diff --git a/Client2020/content/avatar/compositing/CompositExtraSlot3.mesh b/Client2020/content/avatar/compositing/CompositExtraSlot3.mesh new file mode 100644 index 0000000..de05ced Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositExtraSlot3.mesh differ diff --git a/Client2020/content/avatar/compositing/CompositExtraSlot4.mesh b/Client2020/content/avatar/compositing/CompositExtraSlot4.mesh new file mode 100644 index 0000000..ff9cb62 Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositExtraSlot4.mesh differ diff --git a/Client2020/content/avatar/compositing/CompositFullAtlasBaseTexture.mesh b/Client2020/content/avatar/compositing/CompositFullAtlasBaseTexture.mesh new file mode 100644 index 0000000..b7e6595 Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositFullAtlasBaseTexture.mesh differ diff --git a/Client2020/content/avatar/compositing/CompositFullAtlasOverlayTexture.mesh b/Client2020/content/avatar/compositing/CompositFullAtlasOverlayTexture.mesh new file mode 100644 index 0000000..eda1938 Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositFullAtlasOverlayTexture.mesh differ diff --git a/Client2020/content/avatar/compositing/CompositLeftArmBase.mesh b/Client2020/content/avatar/compositing/CompositLeftArmBase.mesh new file mode 100644 index 0000000..5bcc4ae Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositLeftArmBase.mesh differ diff --git a/Client2020/content/avatar/compositing/CompositLeftLegBase.mesh b/Client2020/content/avatar/compositing/CompositLeftLegBase.mesh new file mode 100644 index 0000000..f4712ce Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositLeftLegBase.mesh differ diff --git a/Client2020/content/avatar/compositing/CompositPantsTemplate.mesh b/Client2020/content/avatar/compositing/CompositPantsTemplate.mesh new file mode 100644 index 0000000..756ee03 Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositPantsTemplate.mesh differ diff --git a/Client2020/content/avatar/compositing/CompositQuad.mesh b/Client2020/content/avatar/compositing/CompositQuad.mesh new file mode 100644 index 0000000..192abf2 Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositQuad.mesh differ diff --git a/Client2020/content/avatar/compositing/CompositRightArmBase.mesh b/Client2020/content/avatar/compositing/CompositRightArmBase.mesh new file mode 100644 index 0000000..02f2721 Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositRightArmBase.mesh differ diff --git a/Client2020/content/avatar/compositing/CompositRightLegBase.mesh b/Client2020/content/avatar/compositing/CompositRightLegBase.mesh new file mode 100644 index 0000000..b287939 Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositRightLegBase.mesh differ diff --git a/Client2020/content/avatar/compositing/CompositShirtTemplate.mesh b/Client2020/content/avatar/compositing/CompositShirtTemplate.mesh new file mode 100644 index 0000000..75487e1 Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositShirtTemplate.mesh differ diff --git a/Client2020/content/avatar/compositing/CompositTShirt.mesh b/Client2020/content/avatar/compositing/CompositTShirt.mesh new file mode 100644 index 0000000..b39b8ac Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositTShirt.mesh differ diff --git a/Client2020/content/avatar/compositing/CompositTorsoBase.mesh b/Client2020/content/avatar/compositing/CompositTorsoBase.mesh new file mode 100644 index 0000000..0388bde Binary files /dev/null and b/Client2020/content/avatar/compositing/CompositTorsoBase.mesh differ diff --git a/Client2020/content/avatar/compositing/R15CompositLeftArmBase.mesh b/Client2020/content/avatar/compositing/R15CompositLeftArmBase.mesh new file mode 100644 index 0000000..c262d08 Binary files /dev/null and b/Client2020/content/avatar/compositing/R15CompositLeftArmBase.mesh differ diff --git a/Client2020/content/avatar/compositing/R15CompositRightArmBase.mesh b/Client2020/content/avatar/compositing/R15CompositRightArmBase.mesh new file mode 100644 index 0000000..c745b94 Binary files /dev/null and b/Client2020/content/avatar/compositing/R15CompositRightArmBase.mesh differ diff --git a/Client2020/content/avatar/compositing/R15CompositTorsoBase.mesh b/Client2020/content/avatar/compositing/R15CompositTorsoBase.mesh new file mode 100644 index 0000000..341d8b6 Binary files /dev/null and b/Client2020/content/avatar/compositing/R15CompositTorsoBase.mesh differ diff --git a/Client2020/content/avatar/defaultPants.rbxm b/Client2020/content/avatar/defaultPants.rbxm new file mode 100644 index 0000000..eb935f8 Binary files /dev/null and b/Client2020/content/avatar/defaultPants.rbxm differ diff --git a/Client2020/content/avatar/defaultShirt.rbxm b/Client2020/content/avatar/defaultShirt.rbxm new file mode 100644 index 0000000..9160556 Binary files /dev/null and b/Client2020/content/avatar/defaultShirt.rbxm differ diff --git a/Client2020/content/avatar/heads/head.mesh b/Client2020/content/avatar/heads/head.mesh new file mode 100644 index 0000000..26db950 Binary files /dev/null and b/Client2020/content/avatar/heads/head.mesh differ diff --git a/Client2020/content/avatar/heads/headA.mesh b/Client2020/content/avatar/heads/headA.mesh new file mode 100644 index 0000000..76b2e72 Binary files /dev/null and b/Client2020/content/avatar/heads/headA.mesh differ diff --git a/Client2020/content/avatar/heads/headB.mesh b/Client2020/content/avatar/heads/headB.mesh new file mode 100644 index 0000000..7705a05 Binary files /dev/null and b/Client2020/content/avatar/heads/headB.mesh differ diff --git a/Client2020/content/avatar/heads/headC.mesh b/Client2020/content/avatar/heads/headC.mesh new file mode 100644 index 0000000..4672c59 Binary files /dev/null and b/Client2020/content/avatar/heads/headC.mesh differ diff --git a/Client2020/content/avatar/heads/headD.mesh b/Client2020/content/avatar/heads/headD.mesh new file mode 100644 index 0000000..ff6cd6c Binary files /dev/null and b/Client2020/content/avatar/heads/headD.mesh differ diff --git a/Client2020/content/avatar/heads/headE.mesh b/Client2020/content/avatar/heads/headE.mesh new file mode 100644 index 0000000..d722d29 Binary files /dev/null and b/Client2020/content/avatar/heads/headE.mesh differ diff --git a/Client2020/content/avatar/heads/headF.mesh b/Client2020/content/avatar/heads/headF.mesh new file mode 100644 index 0000000..7c0b311 Binary files /dev/null and b/Client2020/content/avatar/heads/headF.mesh differ diff --git a/Client2020/content/avatar/heads/headG.mesh b/Client2020/content/avatar/heads/headG.mesh new file mode 100644 index 0000000..38db235 Binary files /dev/null and b/Client2020/content/avatar/heads/headG.mesh differ diff --git a/Client2020/content/avatar/heads/headH.mesh b/Client2020/content/avatar/heads/headH.mesh new file mode 100644 index 0000000..fe7365b Binary files /dev/null and b/Client2020/content/avatar/heads/headH.mesh differ diff --git a/Client2020/content/avatar/heads/headI.mesh b/Client2020/content/avatar/heads/headI.mesh new file mode 100644 index 0000000..8ff7ba1 Binary files /dev/null and b/Client2020/content/avatar/heads/headI.mesh differ diff --git a/Client2020/content/avatar/heads/headJ.mesh b/Client2020/content/avatar/heads/headJ.mesh new file mode 100644 index 0000000..ff76d44 Binary files /dev/null and b/Client2020/content/avatar/heads/headJ.mesh differ diff --git a/Client2020/content/avatar/heads/headK.mesh b/Client2020/content/avatar/heads/headK.mesh new file mode 100644 index 0000000..b08bffc Binary files /dev/null and b/Client2020/content/avatar/heads/headK.mesh differ diff --git a/Client2020/content/avatar/heads/headL.mesh b/Client2020/content/avatar/heads/headL.mesh new file mode 100644 index 0000000..645531b Binary files /dev/null and b/Client2020/content/avatar/heads/headL.mesh differ diff --git a/Client2020/content/avatar/heads/headM.mesh b/Client2020/content/avatar/heads/headM.mesh new file mode 100644 index 0000000..ba144e8 Binary files /dev/null and b/Client2020/content/avatar/heads/headM.mesh differ diff --git a/Client2020/content/avatar/heads/headN.mesh b/Client2020/content/avatar/heads/headN.mesh new file mode 100644 index 0000000..6fc4620 Binary files /dev/null and b/Client2020/content/avatar/heads/headN.mesh differ diff --git a/Client2020/content/avatar/heads/headO.mesh b/Client2020/content/avatar/heads/headO.mesh new file mode 100644 index 0000000..a41f85f Binary files /dev/null and b/Client2020/content/avatar/heads/headO.mesh differ diff --git a/Client2020/content/avatar/heads/headP.mesh b/Client2020/content/avatar/heads/headP.mesh new file mode 100644 index 0000000..cdb1647 Binary files /dev/null and b/Client2020/content/avatar/heads/headP.mesh differ diff --git a/Client2020/content/avatar/meshes/leftarm.mesh b/Client2020/content/avatar/meshes/leftarm.mesh new file mode 100644 index 0000000..6e8bb63 Binary files /dev/null and b/Client2020/content/avatar/meshes/leftarm.mesh differ diff --git a/Client2020/content/avatar/meshes/leftleg.mesh b/Client2020/content/avatar/meshes/leftleg.mesh new file mode 100644 index 0000000..aba3a29 Binary files /dev/null and b/Client2020/content/avatar/meshes/leftleg.mesh differ diff --git a/Client2020/content/avatar/meshes/rightarm.mesh b/Client2020/content/avatar/meshes/rightarm.mesh new file mode 100644 index 0000000..14a52a4 Binary files /dev/null and b/Client2020/content/avatar/meshes/rightarm.mesh differ diff --git a/Client2020/content/avatar/meshes/rightleg.mesh b/Client2020/content/avatar/meshes/rightleg.mesh new file mode 100644 index 0000000..dab065d Binary files /dev/null and b/Client2020/content/avatar/meshes/rightleg.mesh differ diff --git a/Client2020/content/avatar/meshes/torso.mesh b/Client2020/content/avatar/meshes/torso.mesh new file mode 100644 index 0000000..43d58d1 Binary files /dev/null and b/Client2020/content/avatar/meshes/torso.mesh differ diff --git a/Client2020/content/avatar/morpherEditorR15.rbxmx b/Client2020/content/avatar/morpherEditorR15.rbxmx new file mode 100644 index 0000000..7af6e5d --- /dev/null +++ b/Client2020/content/avatar/morpherEditorR15.rbxmx @@ -0,0 +1,4041 @@ + + false + null + nil + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + R15 + RBX081D55DA3E944921A37ACF5DF5DC888A + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 7.66999817 + 2.30299997 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + false + 256 + HumanoidRootPart + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 3 + 0 + 1 + + 0 + 0 + 0 + + 1 + 1 + + 2 + 1.96000004 + 1 + + + + + + 0 + -0.342999995 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootRigAttachment + + false + + + + OriginalPosition + + + 0 + -0.349999994 + 0 + + + + + + + OriginalSize + + + 2 + 2 + 1 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 6.16999769 + 2.10695553 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 0.300030679 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715537 + http://www.roblox.com/asset/?id=1699715537 + LeftHand + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.999999762 + 0.29403007 + 0.999999881 + + + + + + 0.000478982925 + 0.122544497 + 5.96046448e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftWristRigAttachment + + false + + + + OriginalPosition + + + 0.000478982925 + 0.125045404 + 5.96046448e-08 + + + + + + + + 0 + -0.146955431 + -1.46306121e-07 + 1 + 0 + -0 + 0 + 6.12323426e-17 + 1 + 0 + -1 + 6.12323426e-17 + + LeftGripAttachment + + false + + + + OriginalPosition + + + 0 + -0.149954513 + -1.46306121e-07 + + + + + + + + 0.000478625298 + -0.490910143 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + 0.000478982925 + 0.122544497 + 5.96046448e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + LeftWrist + RBX3F40768EF729487C8794F9B4240FB7C4 + RBX22C13C16C5FB47AABB4365E4AA62349F + + + + + + OriginalSize + + + 0.999999762 + 0.300030679 + 0.999999881 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 6.16999817 + 2.72041011 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.999999642 + 1.05191803 + 1 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715541 + http://www.roblox.com/asset/?id=1699715541 + LeftLowerArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.999999642 + 1.03087974 + 1 + + + + + + 0.000478625298 + 0.253514439 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftElbowRigAttachment + + false + + + + OriginalPosition + + + 0.000478625298 + 0.258688211 + 7.64462551e-20 + + + + + + + + 0.000478625298 + -0.490910143 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftWristRigAttachment + + false + + + + OriginalPosition + + + 0.000478625298 + -0.5009287 + 7.64462551e-20 + + + + + + + + 0.000479221344 + -0.327375263 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + 0.000478625298 + 0.253514439 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + LeftElbow + RBX65B95BED76AB4402A5C58B280CC87F73 + RBX3F40768EF729487C8794F9B4240FB7C4 + + + + + + OriginalSize + + + 0.999999642 + 1.05191803 + 1 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 6.16999769 + 3.30129981 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 1.16867065 + 0.99999994 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715550 + http://www.roblox.com/asset/?id=1699715550 + LeftUpperArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.999999762 + 1.14529729 + 0.99999994 + + + + + + 0.500000358 + 0.386440158 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderRigAttachment + + false + + + + OriginalPosition + + + 0.500000358 + 0.394326687 + 8.94069672e-08 + + + + + + + + 0.000479221344 + -0.327375263 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftElbowRigAttachment + + false + + + + OriginalPosition + + + 0.000479221344 + -0.334056377 + 8.94069672e-08 + + + + + + + + 2.38418579e-07 + 0.572640121 + -2.70968314e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderAttachment + + false + + + + OriginalPosition + + + 2.38418579e-07 + 0.584326625 + -2.70968314e-08 + + + + + + + + -1 + 0.551756799 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + 0.500000358 + 0.386440158 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + LeftShoulder + RBXA9943A145CA34B9199D5810AE7AC1AD5 + RBX65B95BED76AB4402A5C58B280CC87F73 + + + + + + OriginalSize + + + 0.999999762 + 1.16867065 + 0.99999994 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 9.16999817 + 2.10695553 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.999999881 + 0.300030679 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715557 + http://www.roblox.com/asset/?id=1699715557 + RightHand + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.999999881 + 0.29403007 + 0.999999881 + + + + + + 3.57627869e-07 + 0.122544497 + 5.96046448e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightWristRigAttachment + + false + + + + OriginalPosition + + + 3.57627869e-07 + 0.125045404 + 5.96046448e-08 + + + + + + + + 0 + -0.146955431 + -1.46306121e-07 + 1 + 0 + -0 + 0 + 6.12323426e-17 + 1 + 0 + -1 + 6.12323426e-17 + + RightGripAttachment + + false + + + + OriginalPosition + + + 0 + -0.149954513 + -1.46306121e-07 + + + + + + + + 1.1920929e-07 + -0.490910143 + -6.86244753e-18 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + 3.57627869e-07 + 0.122544497 + 5.96046448e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + RightWrist + RBX36AF3E43DBBF47F29C81B6C3A652EF28 + RBX138B2F31E7B64B03B104DB392895E6A2 + + + + + + OriginalSize + + + 0.999999881 + 0.300030679 + 0.999999881 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 9.16999817 + 2.72041011 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 1.05191803 + 1 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715562 + http://www.roblox.com/asset/?id=1699715562 + RightLowerArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.999999762 + 1.03087974 + 1 + + + + + + 1.1920929e-07 + 0.253407896 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightElbowRigAttachment + + false + + + + OriginalPosition + + + 1.1920929e-07 + 0.258579493 + 7.64462551e-20 + + + + + + + + 1.1920929e-07 + -0.490910143 + -6.86244753e-18 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightWristRigAttachment + + false + + + + OriginalPosition + + + 1.1920929e-07 + -0.5009287 + -6.86244753e-18 + + + + + + + + -5.96046448e-07 + -0.327481806 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + 1.1920929e-07 + 0.253407896 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + RightElbow + RBXE08AA1E71CF7447581F14072A70CB0ED + RBX36AF3E43DBBF47F29C81B6C3A652EF28 + + + + + + OriginalSize + + + 0.999999762 + 1.05191803 + 1 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 9.16999912 + 3.30129981 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.999999642 + 1.16867065 + 0.99999994 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715576 + http://www.roblox.com/asset/?id=1699715576 + RightUpperArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.999999642 + 1.14529729 + 0.99999994 + + + + + + -0.500000715 + 0.386440158 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderRigAttachment + + false + + + + OriginalPosition + + + -0.500000715 + 0.394326687 + 8.94069672e-08 + + + + + + + + -5.96046448e-07 + -0.327481806 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightElbowRigAttachment + + false + + + + OriginalPosition + + + -5.96046448e-07 + -0.334165096 + 8.94069672e-08 + + + + + + + + -8.34465027e-07 + 0.572640121 + -2.70968314e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderAttachment + + false + + + + OriginalPosition + + + -8.34465027e-07 + 0.584326625 + -2.70968314e-08 + + + + + + + + 0.99999994 + 0.551756799 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -0.500000715 + 0.386440158 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + RightShoulder + RBXA9943A145CA34B9199D5810AE7AC1AD5 + RBXE08AA1E71CF7447581F14072A70CB0ED + + + + + + OriginalSize + + + 0.999999642 + 1.16867065 + 0.99999994 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 7.66999817 + 3.13598323 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 2 + 1.60003424 + 1.00000036 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715593 + http://www.roblox.com/asset/?id=1699715593 + UpperTorso + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 2 + 1.56803358 + 1.00000036 + + + + + + -5.96046448e-08 + -0.783986032 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistRigAttachment + + false + + + + OriginalPosition + + + -5.96046448e-08 + -0.799985707 + 1.1920929e-07 + + + + + + + + -5.96046448e-08 + 0.784016788 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckRigAttachment + + false + + + + OriginalPosition + + + -5.96046448e-08 + 0.800017118 + 1.1920929e-07 + + + + + + + + -1 + 0.551756799 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderRigAttachment + + false + + + + OriginalPosition + + + -1 + 0.56301713 + 1.1920929e-07 + + + + + + + + 0.99999994 + 0.551756799 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderRigAttachment + + false + + + + OriginalPosition + + + 0.99999994 + 0.56301713 + 1.1920929e-07 + + + + + + + + -5.96046448e-08 + -0.195985973 + -0.499999881 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyFrontAttachment + + false + + + + OriginalPosition + + + -5.96046448e-08 + -0.199985683 + -0.499999881 + + + + + + + + -5.96046448e-08 + -0.195985973 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyBackAttachment + + false + + + + OriginalPosition + + + -5.96046448e-08 + -0.199985683 + 0.5 + + + + + + + + -0.999999881 + 0.784016669 + -7.27397378e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftCollarAttachment + + false + + + + OriginalPosition + + + -0.999999881 + 0.800016999 + -7.27397378e-08 + + + + + + + + 0.99999994 + 0.78401643 + 4.61295997e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightCollarAttachment + + false + + + + OriginalPosition + + + 0.99999994 + 0.800016761 + 4.61295997e-08 + + + + + + + + -5.05859497e-08 + 0.784016788 + 7.11172419e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckAttachment + + false + + + + OriginalPosition + + + -5.05859497e-08 + 0.800017118 + 7.11172419e-08 + + + + + + + + -1.1920929e-07 + 0.196024418 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -5.96046448e-08 + -0.783986032 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + Waist + RBXB8FFBCE768F34D8D8D966F48788528E0 + RBXA9943A145CA34B9199D5810AE7AC1AD5 + + + + + + OriginalSize + + + 2 + 1.60003424 + 1.00000036 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 7.16999817 + 0.147000074 + -6.00000095 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 1 + 0.300000191 + 1 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715602 + http://www.roblox.com/asset/?id=1699715602 + LeftFoot + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 1 + 0.294000179 + 1 + + + + + + -1.78813934e-07 + 0.0999008864 + -1.7222776e-06 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftAnkleRigAttachment + + false + + + + OriginalPosition + + + -1.78813934e-07 + 0.101939678 + -1.7222776e-06 + + + + + + + + -1.1920929e-07 + -0.536214054 + -2.21401592e-06 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -1.78813934e-07 + 0.0999008864 + -1.7222776e-06 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + LeftAnkle + RBX893C6CF7B2374480B69BBBE805825872 + RBX0FB02BF4485A49C89C7ABEF4D19DB3BE + + + + + + OriginalSize + + + 1 + 0.300000191 + 1 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 7.16999817 + 0.783115029 + -6.00000048 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.99999994 + 1.1930927 + 0.999999523 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715610 + http://www.roblox.com/asset/?id=1699715610 + LeftLowerLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.99999994 + 1.16923082 + 0.999999523 + + + + + + 2.98023224e-08 + 0.371438056 + -1.60860594e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftKneeRigAttachment + + false + + + + OriginalPosition + + + 2.98023224e-08 + 0.379018426 + -1.60860594e-07 + + + + + + + + -1.1920929e-07 + -0.536214054 + -2.21401592e-06 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftAnkleRigAttachment + + false + + + + OriginalPosition + + + -1.1920929e-07 + -0.547157168 + -2.21401592e-06 + + + + + + + + 8.94069672e-08 + -0.393080324 + -4.29081496e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + 2.98023224e-08 + 0.371438056 + -1.60860594e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + LeftKnee + RBXAF9FA9B09EA04DFEA9D919064EDE7D6F + RBX893C6CF7B2374480B69BBBE805825872 + + + + + + OriginalSize + + + 0.99999994 + 1.1930927 + 0.999999523 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 7.16999817 + 1.54763341 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 1.00000036 + 1.21656859 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715616 + http://www.roblox.com/asset/?id=1699715616 + LeftUpperLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 1.00000036 + 1.19223726 + 0.999999881 + + + + + + 5.96046448e-08 + 0.412366509 + -1.63912773e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftHipRigAttachment + + false + + + + OriginalPosition + + + 5.96046448e-08 + 0.420782149 + -1.63912773e-07 + + + + + + + + 8.94069672e-08 + -0.393080324 + -4.29081496e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftKneeRigAttachment + + false + + + + OriginalPosition + + + 8.94069672e-08 + -0.401102364 + -4.29081496e-07 + + + + + + + + -0.500000119 + -0.195972815 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + 5.96046448e-08 + 0.412366509 + -1.63912773e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + LeftHip + RBXB8FFBCE768F34D8D8D966F48788528E0 + RBXAF9FA9B09EA04DFEA9D919064EDE7D6F + + + + + + OriginalSize + + + 1.00000036 + 1.21656859 + 0.999999881 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 8.16999817 + 0.147000074 + -5.99999952 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.99999994 + 0.300000191 + 1 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715627 + http://www.roblox.com/asset/?id=1699715627 + RightFoot + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.99999994 + 0.294000179 + 1 + + + + + + -0 + 0.0999007672 + 7.64477954e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightAnkleRigAttachment + + false + + + + OriginalPosition + + + -0 + 0.101939559 + 7.64477954e-05 + + + + + + + + -0 + -0.536214054 + 7.62689815e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -0 + 0.0999007672 + 7.64477954e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + RightAnkle + RBX934B19001E8E48CD955C16029E83167A + RBXC1076273B95B402B92B9B47AC621CE81 + + + + + + OriginalSize + + + 0.99999994 + 0.300000191 + 1 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 8.16999817 + 0.78311491 + -5.99999952 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.99999994 + 1.19309282 + 1.00000012 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715632 + http://www.roblox.com/asset/?id=1699715632 + RightLowerLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.99999994 + 1.16923094 + 1.00000012 + + + + + + -0 + 0.371590823 + 2.5553607e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightKneeRigAttachment + + false + + + + OriginalPosition + + + -0 + 0.379174292 + 2.5553607e-05 + + + + + + + + -0 + -0.536214054 + 7.62689815e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightAnkleRigAttachment + + false + + + + OriginalPosition + + + -0 + -0.547157168 + 7.62689815e-05 + + + + + + + + -0 + -0.392927587 + -2.18767891e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -0 + 0.371590823 + 2.5553607e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + RightKnee + RBX1174DFBAABA34412840A7F27FF9DDCE9 + RBX934B19001E8E48CD955C16029E83167A + + + + + + OriginalSize + + + 0.99999994 + 1.19309282 + 1.00000012 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 8.16999817 + 1.54763329 + -5.99995232 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 1.00000048 + 1.21656859 + 0.99996686 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715641 + http://www.roblox.com/asset/?id=1699715641 + RightUpperLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 1.00000048 + 1.19223726 + 0.99996686 + + + + + + -0 + 0.412366629 + -6.67300628e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightHipRigAttachment + + false + + + + OriginalPosition + + + -0 + 0.420782268 + -6.67300628e-05 + + + + + + + + -0 + -0.392927587 + -2.18767891e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightKneeRigAttachment + + false + + + + OriginalPosition + + + -0 + -0.400946498 + -2.18767891e-05 + + + + + + + + 0.499999881 + -0.195972815 + -1.91208565e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -0 + 0.412366629 + -6.67300628e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + RightHip + RBXB8FFBCE768F34D8D8D966F48788528E0 + RBX1174DFBAABA34412840A7F27FF9DDCE9 + + + + + + OriginalSize + + + 1.00000048 + 1.21656859 + 0.99996686 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 7.66999817 + 2.15597272 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 1.99999976 + 0.400055438 + 1.00000012 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715652 + http://www.roblox.com/asset/?id=1699715652 + LowerTorso + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 1.99999976 + 0.392054349 + 1.00000012 + + + + + + -1.1920929e-07 + -0.195972815 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootRigAttachment + + false + + + + OriginalPosition + + + -1.1920929e-07 + -0.199972257 + -0 + + + + + + + + -1.1920929e-07 + 0.196024418 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistRigAttachment + + false + + + + OriginalPosition + + + -1.1920929e-07 + 0.200024918 + 7.64462551e-20 + + + + + + + + -0.500000119 + -0.195972815 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftHipRigAttachment + + false + + + + OriginalPosition + + + -0.500000119 + -0.199972257 + -0 + + + + + + + + 0.499999881 + -0.195972815 + -1.91208565e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightHipRigAttachment + + false + + + + OriginalPosition + + + 0.499999881 + -0.199972257 + -1.91208565e-05 + + + + + + + + -4.2200088e-07 + -0.195972815 + -1.65436123e-24 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistCenterAttachment + + false + + + + OriginalPosition + + + -4.2200088e-07 + -0.199972257 + -1.65436123e-24 + + + + + + + + -1.32219867e-07 + -0.195972815 + -0.50000006 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistFrontAttachment + + false + + + + OriginalPosition + + + -1.32219867e-07 + -0.199972257 + -0.50000006 + + + + + + + + -1.46413214e-07 + -0.195972815 + 0.50000006 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistBackAttachment + + false + + + + OriginalPosition + + + -1.46413214e-07 + -0.199972257 + 0.50000006 + + + + + + + + 0 + -0.342999995 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -1.1920929e-07 + -0.195972815 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + Root + RBX081D55DA3E944921A37ACF5DF5DC888A + RBXB8FFBCE768F34D8D8D966F48788528E0 + + + + + + OriginalSize + + + 1.99999976 + 0.400055438 + 1.00000012 + + + + + + + true + true + true + 2 + 0 + 0 + 100 + 1.32300007 + + 1 + 0.980000019 + 1 + + 1 + 50 + 100 + 89 + Humanoid + 100 + 2 + 1 + + 16 + + + + BodyDepthScale + + 1 + + + + + BodyHeightScale + + 0.98000001907348632813 + + + + + BodyProportionScale + + 0 + + + + + BodyTypeScale + + 0 + + + + + BodyWidthScale + + 1 + + + + + HeadScale + + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 7.66999817 + 4.42000008 + -5.99972773 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + false + 256 + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + 1 + 1 + + 2 + 1 + 1 + + + + + 2 + 2 + + 0 + Mesh + + 0 + 0 + 0 + + + 1.25 + 1.25 + 1.25 + + + + + 1 + 1 + 1 + + + + + OriginalSize + + + 1.25 + 1.25 + 1.25 + + + + + + + + 3.93568822e-09 + 0 + -0.000272244215 + 1 + 7.87137555e-09 + 3.02998127e-15 + -7.87137555e-09 + 1 + -4.1444258e-16 + -3.02998127e-15 + 4.14442554e-16 + 1 + + FaceCenterAttachment + + false + + + + OriginalPosition + + + 3.93568822e-09 + 0 + -0.000272244215 + + + + + + + + 3.93568866e-09 + 0 + -0.600272298 + 1 + 7.87137555e-09 + 3.02998127e-15 + -7.87137555e-09 + 1 + -4.1444258e-16 + -3.02998127e-15 + 4.14442554e-16 + 1 + + FaceFrontAttachment + + false + + + + OriginalPosition + + + 3.93568866e-09 + 0 + -0.600272298 + + + + + + + + 8.65851391e-09 + 0.599999905 + -0.000272244215 + 1 + 7.87137555e-09 + 3.02998127e-15 + -7.87137555e-09 + 1 + -4.1444258e-16 + -3.02998127e-15 + 4.14442554e-16 + 1 + + HairAttachment + + false + + + + OriginalPosition + + + 8.65851391e-09 + 0.599999905 + -0.000272244215 + + + + + + + + 8.65851391e-09 + 0.599999905 + -0.000272244215 + 1 + 7.87137555e-09 + 3.02998127e-15 + -7.87137555e-09 + 1 + -4.1444258e-16 + -3.02998127e-15 + 4.14442554e-16 + 1 + + HatAttachment + + false + + + + OriginalPosition + + + 8.65851391e-09 + 0.599999905 + -0.000272244215 + + + + + + + + -0 + -0.500000119 + -0.000272244215 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckRigAttachment + + false + + + + OriginalPosition + + + -0 + -0.500000119 + -0.000272244215 + + + + + + + 4294967295 + 5 + face + + rbxasset://textures/face.png + 0 + + + + + + -5.96046448e-08 + 0.784016788 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -0 + -0.500000119 + -0.000272244215 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + Neck + RBXA9943A145CA34B9199D5810AE7AC1AD5 + RBX3A01263AEC0C4FB289F443C3B3EEF144 + + + + + + OriginalSize + + + 2 + 1 + 1 + + + + + + + false + + Animate + {023B546E-F0DD-4EFB-A64F-BC4BC43CBC7B} + + + + + + cheer + + + + + + http://www.roblox.com/asset/?id=507770677 + CheerAnim + + + + + + + climb + + + + + + http://www.roblox.com/asset/?id=507765644 + ClimbAnim + + + + + + + dance + + + + + + http://www.roblox.com/asset/?id=507771019 + Animation1 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507771955 + Animation2 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507772104 + Animation3 + + + + + Weight + + 10 + + + + + + + dance2 + + + + + + http://www.roblox.com/asset/?id=507776043 + Animation1 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507776720 + Animation2 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507776879 + Animation3 + + + + + Weight + + 10 + + + + + + + dance3 + + + + + + http://www.roblox.com/asset/?id=507777268 + Animation1 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507777451 + Animation2 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507777623 + Animation3 + + + + + Weight + + 10 + + + + + + + fall + + + + + + http://www.roblox.com/asset/?id=507767968 + FallAnim + + + + + + + idle + + + + + + http://www.roblox.com/asset/?id=507766388 + Animation1 + + + + + Weight + + 9 + + + + + + http://www.roblox.com/asset/?id=507766666 + Animation2 + + + + + Weight + + 1 + + + + + + + jump + + + + + + http://www.roblox.com/asset/?id=507765000 + JumpAnim + + + + + + + laugh + + + + + + http://www.roblox.com/asset/?id=507770818 + LaughAnim + + + + + + + point + + + + + + http://www.roblox.com/asset/?id=507770453 + PointAnim + + + + + + + run + + + + + + http://www.roblox.com/asset/?id=507767714 + RunAnim + + + + + + + sit + + + + + + http://www.roblox.com/asset/?id=507768133 + SitAnim + + + + + + + swim + + + + + + http://www.roblox.com/asset/?id=507784897 + Swim + + + + + + + swimidle + + + + + + http://www.roblox.com/asset/?id=481825862 + SwimIdle + + + + + + + toollunge + + + + + + http://www.roblox.com/asset/?id=522638767 + ToolLungeAnim + + + + + + + toolnone + + + + + + http://www.roblox.com/asset/?id=507768375 + ToolNoneAnim + + + + + + + toolslash + + + + + + http://www.roblox.com/asset/?id=522635514 + ToolSlashAnim + + + + + + + walk + + + + + + http://www.roblox.com/asset/?id=540798782 + WalkAnim + + + + + + + wave + + + + + + http://www.roblox.com/asset/?id=507770239 + WaveAnim + + + + + + + \ No newline at end of file diff --git a/Client2020/content/avatar/morpherEditorR6.rbxmx b/Client2020/content/avatar/morpherEditorR6.rbxmx new file mode 100644 index 0000000..ca7f595 --- /dev/null +++ b/Client2020/content/avatar/morpherEditorR6.rbxmx @@ -0,0 +1,1121 @@ + + false + null + nil + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + R6 + RBX268D3553F8164BE0944302D499E3B918 + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 10.7362061 + 5.16300297 + -2.11117506 + 0.766051888 + 0 + 0.642794013 + 0 + 1 + 0 + -0.642794013 + 0 + 0.766051888 + + true + 0 + 4288914085 + + false + + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 1 + 1 + + + + + 4294967295 + 5 + face + + rbxasset://textures/face.png + 0 + + + + + 2 + 2 + + 0 + Mesh + + 0 + 0 + 0 + + + 1.25 + 1.25 + 1.25 + + + + + 1 + 1 + 1 + + + + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + FaceCenterAttachment + + false + + + + + + 0 + 0 + -0.600000024 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + FaceFrontAttachment + + false + + + + + + 0 + 0.600000024 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + HairAttachment + + false + + + + + + 0 + 0.600000024 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + HatAttachment + + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 10.7362061 + 3.66300297 + -2.11117506 + 0.766051888 + 0 + 0.642794013 + 0 + 1 + 0 + -0.642794013 + 0 + 0.766051888 + + true + 0 + 4288914085 + + false + + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 2 + 0 + true + 256 + Torso + 0 + 0 + 0 + 2 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 2 + 1 + + + + + + 1 + 0.5 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + -1 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + -1 + 0 + 0 + + 0 + false + 0.100000001 + Right Shoulder + RBX335EB02A1E034C03BDB8817CC0227D43 + RBXF656CD68CA394FF3B913BD5A491CF6F1 + + + + + + + -1 + 0.5 + 0 + 0 + 0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + + 0.5 + 0.5 + 0 + 0 + 0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + 0 + false + 0.100000001 + Left Shoulder + RBX335EB02A1E034C03BDB8817CC0227D43 + RBX6746C39D16F440CDAF23D54B9D99B38A + + + + + + + 1 + -1 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + -1 + 0 + 0 + + + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + -1 + 0 + 0 + + 0 + false + 0.100000001 + Right Hip + RBX335EB02A1E034C03BDB8817CC0227D43 + RBXF436A38E2161481E82066DF8D79AB417 + + + + + + + -1 + -1 + 0 + 0 + 0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + + -0.5 + 1 + 0 + 0 + 0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + 0 + false + 0.100000001 + Left Hip + RBX335EB02A1E034C03BDB8817CC0227D43 + RBX1515CFDCBD8E4B02B9C4053FDCB3F8B7 + + + + + + + 0 + 1 + 0 + -1 + 0 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + + + 0 + -0.5 + 0 + -1 + 0 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + + 0 + false + 0.100000001 + Neck + RBX335EB02A1E034C03BDB8817CC0227D43 + RBX268D3553F8164BE0944302D499E3B918 + + + + + + + 0 + 0 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyBackAttachment + + false + + + + + + 0 + 0 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyFrontAttachment + + false + + + + + + -1 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftCollarAttachment + + false + + + + + + 0 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckAttachment + + false + + + + + + 1 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightCollarAttachment + + false + + + + + + 0 + -1 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistBackAttachment + + false + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistCenterAttachment + + false + + + + + + 0 + -1 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistFrontAttachment + + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 9.58712769 + 3.66300297 + -1.1469841 + 0.766051888 + 0 + 0.642794013 + 0 + 1 + 0 + -0.642794013 + 0 + 0.766051888 + + true + 0 + 4288914085 + + false + + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Left Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 0.5 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderAttachment + + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 11.8852844 + 3.66300297 + -3.07536602 + 0.766051888 + 0 + 0.642794013 + 0 + 1 + 0 + -0.642794013 + 0 + 0.766051888 + + true + 0 + 4294939796 + + false + + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Right Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 0.5 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderAttachment + + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 10.3531799 + 1.66300297 + -1.78977799 + 0.766051888 + 0 + 0.642794013 + 0 + 1 + 0 + -0.642794013 + 0 + 0.766051888 + + true + 0 + 4294939796 + + false + + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Left Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 11.1192322 + 1.66300297 + -2.43257213 + 0.766051888 + 0 + 0.642794013 + 0 + 1 + 0 + -0.642794013 + 0 + 0.766051888 + + true + 0 + 4288914085 + + false + + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Right Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + true + true + true + 2 + 100 + 0 + 0 + 0 + + 1 + 1 + 1 + + 1 + 50 + 0 + 89 + Humanoid + 100 + 2 + 0 + + 16 + + + + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 10.7362061 + 3.66300297 + -2.11117506 + 0.766051888 + 2.24691483e-39 + 0.642794013 + 2.93314189e-39 + 1 + 0 + -0.642794013 + -1.88538683e-39 + 0.766051888 + + true + 0 + 4279069100 + + false + + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 0 + 0 + true + 256 + HumanoidRootPart + 0 + 0 + 0 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 1 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 2 + 1 + + + + + + 0 + 0 + 0 + -1 + 0 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + + + 0 + 0 + 0 + -1 + 0 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + + 0 + false + 0.100000001 + RootJoint + RBXC27DD24388A54334A651B4AE62D07FEC + RBX335EB02A1E034C03BDB8817CC0227D43 + + + + + + \ No newline at end of file diff --git a/Client2020/content/avatar/scripts/humanoidAnimateLocalKeyframe.rbxm b/Client2020/content/avatar/scripts/humanoidAnimateLocalKeyframe.rbxm new file mode 100644 index 0000000..ad71482 --- /dev/null +++ b/Client2020/content/avatar/scripts/humanoidAnimateLocalKeyframe.rbxm @@ -0,0 +1,664 @@ + + null + nil + + + false + + Animate + animTable[animName][idx].weight) do + roll = roll - animTable[animName][idx].weight + idx = idx + 1 + end +-- print(animName .. " " .. idx .. " [" .. origRoll .. "]") + local anim = animTable[animName][idx].anim + + -- switch animation + if (anim ~= currentAnimInstance) then + + if (currentAnimTrack ~= nil) then + currentAnimTrack:Stop(transitionTime) + currentAnimTrack:Destroy() + end + + currentAnimSpeed = 1.0 + + -- load it to the humanoid; get AnimationTrack + currentAnimTrack = humanoid:LoadAnimation(anim) + currentAnimTrack.Priority = Enum.AnimationPriority.Core + + -- play the animation + currentAnimTrack:Play(transitionTime) + currentAnim = animName + currentAnimInstance = anim + + -- set up keyframe name triggers + if (currentAnimKeyframeHandler ~= nil) then + currentAnimKeyframeHandler:disconnect() + end + currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:connect(keyFrameReachedFunc) + + end + +end + +------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------- + +local toolAnimName = "" +local toolAnimTrack = nil +local toolAnimInstance = nil +local currentToolAnimKeyframeHandler = nil + +function toolKeyFrameReachedFunc(frameName) + if (frameName == "End") then +-- print("Keyframe : ".. frameName) + playToolAnimation(toolAnimName, 0.0, Humanoid) + end +end + + +function playToolAnimation(animName, transitionTime, humanoid, priority) + + local roll = math.random(1, animTable[animName].totalWeight) + local origRoll = roll + local idx = 1 + while (roll > animTable[animName][idx].weight) do + roll = roll - animTable[animName][idx].weight + idx = idx + 1 + end +-- print(animName .. " * " .. idx .. " [" .. origRoll .. "]") + local anim = animTable[animName][idx].anim + + if (toolAnimInstance ~= anim) then + + if (toolAnimTrack ~= nil) then + toolAnimTrack:Stop() + toolAnimTrack:Destroy() + transitionTime = 0 + end + + -- load it to the humanoid; get AnimationTrack + toolAnimTrack = humanoid:LoadAnimation(anim) + if priority then + toolAnimTrack.Priority = priority + end + + -- play the animation + toolAnimTrack:Play(transitionTime) + toolAnimName = animName + toolAnimInstance = anim + + currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:connect(toolKeyFrameReachedFunc) + end +end + +function stopToolAnimations() + local oldAnim = toolAnimName + + if (currentToolAnimKeyframeHandler ~= nil) then + currentToolAnimKeyframeHandler:disconnect() + end + + toolAnimName = "" + toolAnimInstance = nil + if (toolAnimTrack ~= nil) then + toolAnimTrack:Stop() + toolAnimTrack:Destroy() + toolAnimTrack = nil + end + + + return oldAnim +end + +------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------- + + +function onRunning(speed) + if speed > 0.01 then + playAnimation("walk", 0.1, Humanoid) + if currentAnimInstance and currentAnimInstance.AnimationId == "http://www.roblox.com/asset/?id=180426354" then + setAnimationSpeed(speed / 14.5) + end + pose = "Running" + else + if emoteNames[currentAnim] == nil then + playAnimation("idle", 0.1, Humanoid) + pose = "Standing" + end + end +end + +function onDied() + pose = "Dead" +end + +function onJumping() + playAnimation("jump", 0.1, Humanoid) + jumpAnimTime = jumpAnimDuration + pose = "Jumping" +end + +function onClimbing(speed) + playAnimation("climb", 0.1, Humanoid) + setAnimationSpeed(speed / 12.0) + pose = "Climbing" +end + +function onGettingUp() + pose = "GettingUp" +end + +function onFreeFall() + if (jumpAnimTime <= 0) then + playAnimation("fall", fallTransitionTime, Humanoid) + end + pose = "FreeFall" +end + +function onFallingDown() + pose = "FallingDown" +end + +function onSeated() + pose = "Seated" +end + +function onPlatformStanding() + pose = "PlatformStanding" +end + +function onSwimming(speed) + if speed > 0 then + pose = "Running" + else + pose = "Standing" + end +end + +function getTool() + for _, kid in ipairs(Figure:GetChildren()) do + if kid.className == "Tool" then return kid end + end + return nil +end + +function getToolAnim(tool) + for _, c in ipairs(tool:GetChildren()) do + if c.Name == "toolanim" and c.className == "StringValue" then + return c + end + end + return nil +end + +function animateTool() + + if (toolAnim == "None") then + playToolAnimation("toolnone", toolTransitionTime, Humanoid, Enum.AnimationPriority.Idle) + return + end + + if (toolAnim == "Slash") then + playToolAnimation("toolslash", 0, Humanoid, Enum.AnimationPriority.Action) + return + end + + if (toolAnim == "Lunge") then + playToolAnimation("toollunge", 0, Humanoid, Enum.AnimationPriority.Action) + return + end +end + +function moveSit() + RightShoulder.MaxVelocity = 0.15 + LeftShoulder.MaxVelocity = 0.15 + RightShoulder:SetDesiredAngle(3.14 /2) + LeftShoulder:SetDesiredAngle(-3.14 /2) + RightHip:SetDesiredAngle(3.14 /2) + LeftHip:SetDesiredAngle(-3.14 /2) +end + +local lastTick = 0 + +function move(time) + local amplitude = 1 + local frequency = 1 + local deltaTime = time - lastTick + lastTick = time + + local climbFudge = 0 + local setAngles = false + + if (jumpAnimTime > 0) then + jumpAnimTime = jumpAnimTime - deltaTime + end + + if (pose == "FreeFall" and jumpAnimTime <= 0) then + playAnimation("fall", fallTransitionTime, Humanoid) + elseif (pose == "Seated") then + playAnimation("sit", 0.5, Humanoid) + return + elseif (pose == "Running") then + playAnimation("walk", 0.1, Humanoid) + elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then +-- print("Wha " .. pose) + stopAllAnimations() + amplitude = 0.1 + frequency = 1 + setAngles = true + end + + if (setAngles) then + local desiredAngle = amplitude * math.sin(time * frequency) + + RightShoulder:SetDesiredAngle(desiredAngle + climbFudge) + LeftShoulder:SetDesiredAngle(desiredAngle - climbFudge) + RightHip:SetDesiredAngle(-desiredAngle) + LeftHip:SetDesiredAngle(-desiredAngle) + end + + -- Tool Animation handling + local tool = getTool() + if tool and tool:FindFirstChild("Handle") then + + local animStringValueObject = getToolAnim(tool) + + if animStringValueObject then + toolAnim = animStringValueObject.Value + -- message recieved, delete StringValue + animStringValueObject.Parent = nil + toolAnimTime = time + .3 + end + + if time > toolAnimTime then + toolAnimTime = 0 + toolAnim = "None" + end + + animateTool() + else + stopToolAnimations() + toolAnim = "None" + toolAnimInstance = nil + toolAnimTime = 0 + end +end + +-- connect events +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Jumping:connect(onJumping) +Humanoid.Climbing:connect(onClimbing) +Humanoid.GettingUp:connect(onGettingUp) +Humanoid.FreeFalling:connect(onFreeFall) +Humanoid.FallingDown:connect(onFallingDown) +Humanoid.Seated:connect(onSeated) +Humanoid.PlatformStanding:connect(onPlatformStanding) +Humanoid.Swimming:connect(onSwimming) + +-- setup emote chat hook +game:GetService("Players").LocalPlayer.Chatted:connect(function(msg) + local emote = "" + if msg == "/e dance" then + emote = dances[math.random(1, #dances)] + elseif (string.sub(msg, 1, 3) == "/e ") then + emote = string.sub(msg, 4) + elseif (string.sub(msg, 1, 7) == "/emote ") then + emote = string.sub(msg, 8) + end + + if (pose == "Standing" and emoteNames[emote] ~= nil) then + playAnimation(emote, 0.1, Humanoid) + end + +end) + + +-- main program + +-- initialize to idle +playAnimation("idle", 0.1, Humanoid) +pose = "Standing" + +while Figure.Parent ~= nil do + local _, time = wait(0.1) + move(time) +end + + +]]> + + + + idle + + + + + http://www.roblox.com/asset/?id=180435571 + Animation1 + + + + Weight + 9 + + + + + + http://www.roblox.com/asset/?id=180435792 + Animation2 + + + + Weight + 1 + + + + + + + walk + + + + + http://www.roblox.com/asset/?id=180426354 + WalkAnim + + + + + + run + + + + + http://www.roblox.com/asset/?id=180426354 + RunAnim + + + + + + jump + + + + + http://www.roblox.com/asset/?id=125750702 + JumpAnim + + + + + + climb + + + + + http://www.roblox.com/asset/?id=180436334 + ClimbAnim + + + + + + toolnone + + + + + http://www.roblox.com/asset/?id=182393478 + ToolNoneAnim + + + + + + fall + + + + + http://www.roblox.com/asset/?id=180436148 + FallAnim + + + + + + sit + + + + + http://www.roblox.com/asset/?id=178130996 + SitAnim + + + + + \ No newline at end of file diff --git a/Client2020/content/avatar/scripts/humanoidAnimateR15.rbxm b/Client2020/content/avatar/scripts/humanoidAnimateR15.rbxm new file mode 100644 index 0000000..b3d27ce Binary files /dev/null and b/Client2020/content/avatar/scripts/humanoidAnimateR15.rbxm differ diff --git a/Client2020/content/avatar/scripts/humanoidAnimateR15ScaledV3.rbxm b/Client2020/content/avatar/scripts/humanoidAnimateR15ScaledV3.rbxm new file mode 100644 index 0000000..c68ba8b Binary files /dev/null and b/Client2020/content/avatar/scripts/humanoidAnimateR15ScaledV3.rbxm differ diff --git a/Client2020/content/avatar/scripts/humanoidHealthRegenScript.rbxmx b/Client2020/content/avatar/scripts/humanoidHealthRegenScript.rbxmx new file mode 100644 index 0000000..ece49f1 --- /dev/null +++ b/Client2020/content/avatar/scripts/humanoidHealthRegenScript.rbxmx @@ -0,0 +1,32 @@ + + null + nil + + + false + + Health + {EC3A881D-5F49-4644-A69D-FB60F2E59FF2} + + + + \ No newline at end of file diff --git a/Client2020/content/configs/DataModelPatchConfig/DataModelPatchConfig.json b/Client2020/content/configs/DataModelPatchConfig/DataModelPatchConfig.json new file mode 100644 index 0000000..1cb2d77 --- /dev/null +++ b/Client2020/content/configs/DataModelPatchConfig/DataModelPatchConfig.json @@ -0,0 +1 @@ +{"AppStorageResetId": "0", "AssetId": "5345954812", "AssetVersion": "346", "IsForcedUpdate": false} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/de-de.json b/Client2020/content/configs/DateTimeLocaleConfigs/de-de.json new file mode 100644 index 0000000..919d45f --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/de-de.json @@ -0,0 +1,85 @@ +{ + "months": [ + "Januar", + "Februar", + "März", + "April", + "Mai", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "Dezember" + ], + "monthsShort": [ + "Jan.", + "Feb.", + "März", + "Apr.", + "Mai", + "Juni", + "Juli", + "Aug.", + "Sep.", + "Okt.", + "Nov.", + "Dez." + ], + "weekdays": [ + "Sonntag", + "Montag", + "Dienstag", + "Mittwoch", + "Donnerstag", + "Freitag", + "Samstag" + ], + "weekdaysShort": [ + "So.", + "Mo.", + "Di.", + "Mi.", + "Do.", + "Fr.", + "Sa." + ], + "weekdaysMin": [ + "So", + "Mo", + "Di", + "Mi", + "Do", + "Fr", + "Sa" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "DD.MM.YYYY", + "LL": "D. MMMM YYYY", + "LLL": "D. MMMM YYYY HH:mm", + "LLLL": "dddd, D. MMMM YYYY HH:mm", + "l": "D.M.YYYY", + "ll": "D. MMM YYYY", + "lll": "D. MMM YYYY HH:mm", + "llll": "ddd, D. MMM YYYY HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/en-au.json b/Client2020/content/configs/DateTimeLocaleConfigs/en-au.json new file mode 100644 index 0000000..2d690c4 --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/en-au.json @@ -0,0 +1,85 @@ +{ + "months": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + "monthsShort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "weekdays": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "weekdaysShort": [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + ], + "weekdaysMin": [ + "Su", + "Mo", + "Tu", + "We", + "Th", + "Fr", + "Sa" + ], + "longDateFormat": { + "LT": "h:mm A", + "LTS": "h:mm:ss A", + "L": "DD/MM/YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY h:mm A", + "LLLL": "dddd, D MMMM YYYY h:mm A", + "l": "D/M/YYYY", + "ll": "D MMM YYYY", + "lll": "D MMM YYYY h:mm A", + "llll": "ddd, D MMM YYYY h:mm A" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/en-ca.json b/Client2020/content/configs/DateTimeLocaleConfigs/en-ca.json new file mode 100644 index 0000000..3cf0fde --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/en-ca.json @@ -0,0 +1,81 @@ +{ + "months": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + "monthsShort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "weekdays": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "weekdaysShort": [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + ], + "weekdaysMin": [ + "Su", + "Mo", + "Tu", + "We", + "Th", + "Fr", + "Sa" + ], + "longDateFormat": { + "LT": "h:mm A", + "LTS": "h:mm:ss A", + "L": "YYYY-MM-DD", + "LL": "MMMM D, YYYY", + "LLL": "MMMM D, YYYY h:mm A", + "LLLL": "dddd, MMMM D, YYYY h:mm A", + "l": "YYYY-M-D", + "ll": "MMM D, YYYY", + "lll": "MMM D, YYYY h:mm A", + "llll": "ddd, MMM D, YYYY h:mm A" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ] +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/en-gb.json b/Client2020/content/configs/DateTimeLocaleConfigs/en-gb.json new file mode 100644 index 0000000..e5c6378 --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/en-gb.json @@ -0,0 +1,85 @@ +{ + "months": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + "monthsShort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "weekdays": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "weekdaysShort": [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + ], + "weekdaysMin": [ + "Su", + "Mo", + "Tu", + "We", + "Th", + "Fr", + "Sa" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "DD/MM/YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY HH:mm", + "LLLL": "dddd, D MMMM YYYY HH:mm", + "l": "D/M/YYYY", + "ll": "D MMM YYYY", + "lll": "D MMM YYYY HH:mm", + "llll": "ddd, D MMM YYYY HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/en-nz.json b/Client2020/content/configs/DateTimeLocaleConfigs/en-nz.json new file mode 100644 index 0000000..2d690c4 --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/en-nz.json @@ -0,0 +1,85 @@ +{ + "months": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + "monthsShort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "weekdays": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "weekdaysShort": [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + ], + "weekdaysMin": [ + "Su", + "Mo", + "Tu", + "We", + "Th", + "Fr", + "Sa" + ], + "longDateFormat": { + "LT": "h:mm A", + "LTS": "h:mm:ss A", + "L": "DD/MM/YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY h:mm A", + "LLLL": "dddd, D MMMM YYYY h:mm A", + "l": "D/M/YYYY", + "ll": "D MMM YYYY", + "lll": "D MMM YYYY h:mm A", + "llll": "ddd, D MMM YYYY h:mm A" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/en-us.json b/Client2020/content/configs/DateTimeLocaleConfigs/en-us.json new file mode 100644 index 0000000..c866091 --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/en-us.json @@ -0,0 +1,85 @@ +{ + "months": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + "monthsShort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "weekdays": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "weekdaysShort": [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + ], + "weekdaysMin": [ + "Su", + "Mo", + "Tu", + "We", + "Th", + "Fr", + "Sa" + ], + "longDateFormat": { + "LT": "h:mm A", + "LTS": "h:mm:ss A", + "L": "MM/DD/YYYY", + "LL": "MMMM D, YYYY", + "LLL": "MMMM D, YYYY h:mm A", + "LLLL": "dddd, MMMM D, YYYY h:mm A", + "l": "M/D/YYYY", + "ll": "MMM D, YYYY", + "lll": "MMM D, YYYY h:mm A", + "llll": "ddd, MMM D, YYYY h:mm A" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/es-es.json b/Client2020/content/configs/DateTimeLocaleConfigs/es-es.json new file mode 100644 index 0000000..c627149 --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/es-es.json @@ -0,0 +1,85 @@ +{ + "months": [ + "enero", + "febrero", + "marzo", + "abril", + "mayo", + "junio", + "julio", + "agosto", + "septiembre", + "octubre", + "noviembre", + "diciembre" + ], + "monthsShort": [ + "ene.", + "feb.", + "mar.", + "abr.", + "may.", + "jun.", + "jul.", + "ago.", + "sep.", + "oct.", + "nov.", + "dic." + ], + "weekdays": [ + "domingo", + "lunes", + "martes", + "miércoles", + "jueves", + "viernes", + "sábado" + ], + "weekdaysShort": [ + "dom.", + "lun.", + "mar.", + "mié.", + "jue.", + "vie.", + "sáb." + ], + "weekdaysMin": [ + "do", + "lu", + "ma", + "mi", + "ju", + "vi", + "sá" + ], + "longDateFormat": { + "LT": "H:mm", + "LTS": "H:mm:ss", + "L": "DD/MM/YYYY", + "LL": "D [de] MMMM [de] YYYY", + "LLL": "D [de] MMMM [de] YYYY H:mm", + "LLLL": "dddd, D [de] MMMM [de] YYYY H:mm", + "l": "D/M/YYYY", + "ll": "D [de] MMM [de] YYYY", + "lll": "D [de] MMM [de] YYYY H:mm", + "llll": "ddd, D [de] MMM [de] YYYY H:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/es-mx.json b/Client2020/content/configs/DateTimeLocaleConfigs/es-mx.json new file mode 100644 index 0000000..52b9a37 --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/es-mx.json @@ -0,0 +1,85 @@ +{ + "months": [ + "enero", + "febrero", + "marzo", + "abril", + "mayo", + "junio", + "julio", + "agosto", + "septiembre", + "octubre", + "noviembre", + "diciembre" + ], + "monthsShort": [ + "ene.", + "feb.", + "mar.", + "abr.", + "may.", + "jun.", + "jul.", + "ago.", + "sep.", + "oct.", + "nov.", + "dic." + ], + "weekdays": [ + "domingo", + "lunes", + "martes", + "miércoles", + "jueves", + "viernes", + "sábado" + ], + "weekdaysShort": [ + "dom.", + "lun.", + "mar.", + "mié.", + "jue.", + "vie.", + "sáb." + ], + "weekdaysMin": [ + "do", + "lu", + "ma", + "mi", + "ju", + "vi", + "sá" + ], + "longDateFormat": { + "LT": "h:mm A", + "LTS": "h:mm:ss A", + "L": "DD/MM/YYYY", + "LL": "D [de] MMMM [de] YYYY", + "LLL": "D [de] MMMM [de] YYYY h:mm A", + "LLLL": "dddd, D [de] MMMM [de] YYYY h:mm A", + "l": "D/M/YYYY", + "ll": "D [de] MMM [de] YYYY", + "lll": "D [de] MMM [de] YYYY h:mm A", + "llll": "ddd, D [de] MMM [de] YYYY h:mm A" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "a.m.", + "upperCase": "a.m." + }, + { + "startFrom": 1200, + "lowerCase": "p.m.", + "upperCase": "p.m." + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/fr-ca.json b/Client2020/content/configs/DateTimeLocaleConfigs/fr-ca.json new file mode 100644 index 0000000..975d55c --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/fr-ca.json @@ -0,0 +1,81 @@ +{ + "months": [ + "janvier", + "février", + "mars", + "avril", + "mai", + "juin", + "juillet", + "août", + "septembre", + "octobre", + "novembre", + "décembre" + ], + "monthsShort": [ + "janv.", + "févr.", + "mars", + "avr.", + "mai", + "juin", + "juil.", + "août", + "sept.", + "oct.", + "nov.", + "déc." + ], + "weekdays": [ + "dimanche", + "lundi", + "mardi", + "mercredi", + "jeudi", + "vendredi", + "samedi" + ], + "weekdaysShort": [ + "dim.", + "lun.", + "mar.", + "mer.", + "jeu.", + "ven.", + "sam." + ], + "weekdaysMin": [ + "di", + "lu", + "ma", + "me", + "je", + "ve", + "sa" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY-MM-DD", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY HH:mm", + "LLLL": "dddd D MMMM YYYY HH:mm", + "l": "YYYY-M-D", + "ll": "D MMM YYYY", + "lll": "D MMM YYYY HH:mm", + "llll": "ddd D MMM YYYY HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ] +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/fr-fr.json b/Client2020/content/configs/DateTimeLocaleConfigs/fr-fr.json new file mode 100644 index 0000000..67fedaa --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/fr-fr.json @@ -0,0 +1,85 @@ +{ + "months": [ + "janvier", + "février", + "mars", + "avril", + "mai", + "juin", + "juillet", + "août", + "septembre", + "octobre", + "novembre", + "décembre" + ], + "monthsShort": [ + "janv.", + "févr.", + "mars", + "avr.", + "mai", + "juin", + "juil.", + "août", + "sept.", + "oct.", + "nov.", + "déc." + ], + "weekdays": [ + "dimanche", + "lundi", + "mardi", + "mercredi", + "jeudi", + "vendredi", + "samedi" + ], + "weekdaysShort": [ + "dim.", + "lun.", + "mar.", + "mer.", + "jeu.", + "ven.", + "sam." + ], + "weekdaysMin": [ + "di", + "lu", + "ma", + "me", + "je", + "ve", + "sa" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "DD/MM/YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY HH:mm", + "LLLL": "dddd D MMMM YYYY HH:mm", + "l": "D/M/YYYY", + "ll": "D MMM YYYY", + "lll": "D MMM YYYY HH:mm", + "llll": "ddd D MMM YYYY HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/it-it.json b/Client2020/content/configs/DateTimeLocaleConfigs/it-it.json new file mode 100644 index 0000000..99e998c --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/it-it.json @@ -0,0 +1,85 @@ +{ + "months": [ + "gennaio", + "febbraio", + "marzo", + "aprile", + "maggio", + "giugno", + "luglio", + "agosto", + "settembre", + "ottobre", + "novembre", + "dicembre" + ], + "monthsShort": [ + "gen", + "feb", + "mar", + "apr", + "mag", + "giu", + "lug", + "ago", + "set", + "ott", + "nov", + "dic" + ], + "weekdays": [ + "domenica", + "lunedì", + "martedì", + "mercoledì", + "giovedì", + "venerdì", + "sabato" + ], + "weekdaysShort": [ + "dom", + "lun", + "mar", + "mer", + "gio", + "ven", + "sab" + ], + "weekdaysMin": [ + "do", + "lu", + "ma", + "me", + "gi", + "ve", + "sa" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "DD/MM/YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY HH:mm", + "LLLL": "dddd D MMMM YYYY HH:mm", + "l": "D/M/YYYY", + "ll": "D MMM YYYY", + "lll": "D MMM YYYY HH:mm", + "llll": "ddd D MMM YYYY HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/ja-jp.json b/Client2020/content/configs/DateTimeLocaleConfigs/ja-jp.json new file mode 100644 index 0000000..cdc6475 --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/ja-jp.json @@ -0,0 +1,81 @@ +{ + "months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "monthsShort": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "weekdays": [ + "日曜日", + "月曜日", + "火曜日", + "水曜日", + "木曜日", + "金曜日", + "土曜日" + ], + "weekdaysShort": [ + "日", + "月", + "火", + "水", + "木", + "金", + "土" + ], + "weekdaysMin": [ + "日", + "月", + "火", + "水", + "木", + "金", + "土" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY/MM/DD", + "LL": "YYYY年M月D日", + "LLL": "YYYY年M月D日 HH:mm", + "LLLL": "YYYY年M月D日 dddd HH:mm", + "l": "YYYY/MM/DD", + "ll": "YYYY年M月D日", + "lll": "YYYY年M月D日 HH:mm", + "llll": "YYYY年M月D日(ddd) HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "午前", + "upperCase": "午前" + }, + { + "startFrom": 1200, + "lowerCase": "午後", + "upperCase": "午後" + } + ] +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/ko-kr.json b/Client2020/content/configs/DateTimeLocaleConfigs/ko-kr.json new file mode 100644 index 0000000..314ed1b --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/ko-kr.json @@ -0,0 +1,81 @@ +{ + "months": [ + "1월", + "2월", + "3월", + "4월", + "5월", + "6월", + "7월", + "8월", + "9월", + "10월", + "11월", + "12월" + ], + "monthsShort": [ + "1월", + "2월", + "3월", + "4월", + "5월", + "6월", + "7월", + "8월", + "9월", + "10월", + "11월", + "12월" + ], + "weekdays": [ + "일요일", + "월요일", + "화요일", + "수요일", + "목요일", + "금요일", + "토요일" + ], + "weekdaysShort": [ + "일", + "월", + "화", + "수", + "목", + "금", + "토" + ], + "weekdaysMin": [ + "일", + "월", + "화", + "수", + "목", + "금", + "토" + ], + "longDateFormat": { + "LT": "A h:mm", + "LTS": "A h:mm:ss", + "L": "YYYY.MM.DD.", + "LL": "YYYY년 MMMM D일", + "LLL": "YYYY년 MMMM D일 A h:mm", + "LLLL": "YYYY년 MMMM D일 dddd A h:mm", + "l": "YYYY.MM.DD.", + "ll": "YYYY년 MMMM D일", + "lll": "YYYY년 MMMM D일 A h:mm", + "llll": "YYYY년 MMMM D일 dddd A h:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "오전", + "upperCase": "오전" + }, + { + "startFrom": 1200, + "lowerCase": "오후", + "upperCase": "오후" + } + ] +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/pt-br.json b/Client2020/content/configs/DateTimeLocaleConfigs/pt-br.json new file mode 100644 index 0000000..1d4bc09 --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/pt-br.json @@ -0,0 +1,81 @@ +{ + "months": [ + "Janeiro", + "Fevereiro", + "Março", + "Abril", + "Maio", + "Junho", + "Julho", + "Agosto", + "Setembro", + "Outubro", + "Novembro", + "Dezembro" + ], + "monthsShort": [ + "Jan", + "Fev", + "Mar", + "Abr", + "Mai", + "Jun", + "Jul", + "Ago", + "Set", + "Out", + "Nov", + "Dez" + ], + "weekdays": [ + "Domingo", + "Segunda-feira", + "Terça-feira", + "Quarta-feira", + "Quinta-feira", + "Sexta-feira", + "Sábado" + ], + "weekdaysShort": [ + "Dom", + "Seg", + "Ter", + "Qua", + "Qui", + "Sex", + "Sáb" + ], + "weekdaysMin": [ + "Do", + "2ª", + "3ª", + "4ª", + "5ª", + "6ª", + "Sá" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "DD/MM/YYYY", + "LL": "D [de] MMMM [de] YYYY", + "LLL": "D [de] MMMM [de] YYYY [às] HH:mm", + "LLLL": "dddd, D [de] MMMM [de] YYYY [às] HH:mm", + "l": "D/M/YYYY", + "ll": "D [de] MMM [de] YYYY", + "lll": "D [de] MMM [de] YYYY [às] HH:mm", + "llll": "ddd, D [de] MMM [de] YYYY [às] HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ] +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/pt-pt.json b/Client2020/content/configs/DateTimeLocaleConfigs/pt-pt.json new file mode 100644 index 0000000..8b41382 --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/pt-pt.json @@ -0,0 +1,85 @@ +{ + "months": [ + "Janeiro", + "Fevereiro", + "Março", + "Abril", + "Maio", + "Junho", + "Julho", + "Agosto", + "Setembro", + "Outubro", + "Novembro", + "Dezembro" + ], + "monthsShort": [ + "Jan", + "Fev", + "Mar", + "Abr", + "Mai", + "Jun", + "Jul", + "Ago", + "Set", + "Out", + "Nov", + "Dez" + ], + "weekdays": [ + "Domingo", + "Segunda-feira", + "Terça-feira", + "Quarta-feira", + "Quinta-feira", + "Sexta-feira", + "Sábado" + ], + "weekdaysShort": [ + "Dom", + "Seg", + "Ter", + "Qua", + "Qui", + "Sex", + "Sáb" + ], + "weekdaysMin": [ + "Do", + "2ª", + "3ª", + "4ª", + "5ª", + "6ª", + "Sá" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "DD/MM/YYYY", + "LL": "D [de] MMMM [de] YYYY", + "LLL": "D [de] MMMM [de] YYYY HH:mm", + "LLLL": "dddd, D [de] MMMM [de] YYYY HH:mm", + "l": "D/M/YYYY", + "ll": "D [de] MMM [de] YYYY", + "lll": "D [de] MMM [de] YYYY HH:mm", + "llll": "ddd, D [de] MMM [de] YYYY HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/ru-ru.json b/Client2020/content/configs/DateTimeLocaleConfigs/ru-ru.json new file mode 100644 index 0000000..3293a5f --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/ru-ru.json @@ -0,0 +1,95 @@ +{ + "months": [ + "январь", + "февраль", + "март", + "апрель", + "май", + "июнь", + "июль", + "август", + "сентябрь", + "октябрь", + "ноябрь", + "декабрь" + ], + "monthsShort": [ + "янв.", + "февр.", + "март", + "апр.", + "май", + "июнь", + "июль", + "авг.", + "сент.", + "окт.", + "нояб.", + "дек." + ], + "weekdays": [ + "воскресенье", + "понедельник", + "вторник", + "среда", + "четверг", + "пятница", + "суббота" + ], + "weekdaysShort": [ + "вс", + "пн", + "вт", + "ср", + "чт", + "пт", + "сб" + ], + "weekdaysMin": [ + "вс", + "пн", + "вт", + "ср", + "чт", + "пт", + "сб" + ], + "longDateFormat": { + "LT": "H:mm", + "LTS": "H:mm:ss", + "L": "DD.MM.YYYY", + "LL": "D MMMM YYYY г.", + "LLL": "D MMMM YYYY г., H:mm", + "LLLL": "dddd, D MMMM YYYY г., H:mm", + "l": "D.M.YYYY", + "ll": "D MMM YYYY г.", + "lll": "D MMM YYYY г., H:mm", + "llll": "ddd, D MMM YYYY г., H:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "ночи", + "upperCase": "ночи" + }, + { + "startFrom": 400, + "lowerCase": "утра", + "upperCase": "утра" + }, + { + "startFrom": 1200, + "lowerCase": "дня", + "upperCase": "дня" + }, + { + "startFrom": 1700, + "lowerCase": "вечера", + "upperCase": "вечера" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/zh-cjv.json b/Client2020/content/configs/DateTimeLocaleConfigs/zh-cjv.json new file mode 100644 index 0000000..612593e --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/zh-cjv.json @@ -0,0 +1,105 @@ +{ + "months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "monthsShort": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "weekdays": [ + "星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六" + ], + "weekdaysShort": [ + "周日", + "周一", + "周二", + "周三", + "周四", + "周五", + "周六" + ], + "weekdaysMin": [ + "日", + "一", + "二", + "三", + "四", + "五", + "六" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY/MM/DD", + "LL": "YYYY年M月D日", + "LLL": "YYYY年M月D日Ah点mm分", + "LLLL": "YYYY年M月D日ddddAh点mm分", + "l": "YYYY/M/D", + "ll": "YYYY年M月D日", + "lll": "YYYY年M月D日 HH:mm", + "llll": "YYYY年M月D日dddd HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "凌晨", + "upperCase": "凌晨" + }, + { + "startFrom": 600, + "lowerCase": "早上", + "upperCase": "早上" + }, + { + "startFrom": 900, + "lowerCase": "上午", + "upperCase": "上午" + }, + { + "startFrom": 1130, + "lowerCase": "中午", + "upperCase": "中午" + }, + { + "startFrom": 1230, + "lowerCase": "下午", + "upperCase": "下午" + }, + { + "startFrom": 1800, + "lowerCase": "晚上", + "upperCase": "晚上" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/zh-cn.json b/Client2020/content/configs/DateTimeLocaleConfigs/zh-cn.json new file mode 100644 index 0000000..612593e --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/zh-cn.json @@ -0,0 +1,105 @@ +{ + "months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "monthsShort": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "weekdays": [ + "星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六" + ], + "weekdaysShort": [ + "周日", + "周一", + "周二", + "周三", + "周四", + "周五", + "周六" + ], + "weekdaysMin": [ + "日", + "一", + "二", + "三", + "四", + "五", + "六" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY/MM/DD", + "LL": "YYYY年M月D日", + "LLL": "YYYY年M月D日Ah点mm分", + "LLLL": "YYYY年M月D日ddddAh点mm分", + "l": "YYYY/M/D", + "ll": "YYYY年M月D日", + "lll": "YYYY年M月D日 HH:mm", + "llll": "YYYY年M月D日dddd HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "凌晨", + "upperCase": "凌晨" + }, + { + "startFrom": 600, + "lowerCase": "早上", + "upperCase": "早上" + }, + { + "startFrom": 900, + "lowerCase": "上午", + "upperCase": "上午" + }, + { + "startFrom": 1130, + "lowerCase": "中午", + "upperCase": "中午" + }, + { + "startFrom": 1230, + "lowerCase": "下午", + "upperCase": "下午" + }, + { + "startFrom": 1800, + "lowerCase": "晚上", + "upperCase": "晚上" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/zh-hans.json b/Client2020/content/configs/DateTimeLocaleConfigs/zh-hans.json new file mode 100644 index 0000000..612593e --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/zh-hans.json @@ -0,0 +1,105 @@ +{ + "months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "monthsShort": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "weekdays": [ + "星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六" + ], + "weekdaysShort": [ + "周日", + "周一", + "周二", + "周三", + "周四", + "周五", + "周六" + ], + "weekdaysMin": [ + "日", + "一", + "二", + "三", + "四", + "五", + "六" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY/MM/DD", + "LL": "YYYY年M月D日", + "LLL": "YYYY年M月D日Ah点mm分", + "LLLL": "YYYY年M月D日ddddAh点mm分", + "l": "YYYY/M/D", + "ll": "YYYY年M月D日", + "lll": "YYYY年M月D日 HH:mm", + "llll": "YYYY年M月D日dddd HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "凌晨", + "upperCase": "凌晨" + }, + { + "startFrom": 600, + "lowerCase": "早上", + "upperCase": "早上" + }, + { + "startFrom": 900, + "lowerCase": "上午", + "upperCase": "上午" + }, + { + "startFrom": 1130, + "lowerCase": "中午", + "upperCase": "中午" + }, + { + "startFrom": 1230, + "lowerCase": "下午", + "upperCase": "下午" + }, + { + "startFrom": 1800, + "lowerCase": "晚上", + "upperCase": "晚上" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/zh-hant.json b/Client2020/content/configs/DateTimeLocaleConfigs/zh-hant.json new file mode 100644 index 0000000..1955433 --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/zh-hant.json @@ -0,0 +1,105 @@ +{ + "months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "monthsShort": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "weekdays": [ + "星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六" + ], + "weekdaysShort": [ + "週日", + "週一", + "週二", + "週三", + "週四", + "週五", + "週六" + ], + "weekdaysMin": [ + "日", + "一", + "二", + "三", + "四", + "五", + "六" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY/MM/DD", + "LL": "YYYY年M月D日", + "LLL": "YYYY年M月D日 HH:mm", + "LLLL": "YYYY年M月D日dddd HH:mm", + "l": "YYYY/M/D", + "ll": "YYYY年M月D日", + "lll": "YYYY年M月D日 HH:mm", + "llll": "YYYY年M月D日dddd HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "凌晨", + "upperCase": "凌晨" + }, + { + "startFrom": 600, + "lowerCase": "早上", + "upperCase": "早上" + }, + { + "startFrom": 900, + "lowerCase": "上午", + "upperCase": "上午" + }, + { + "startFrom": 1130, + "lowerCase": "中午", + "upperCase": "中午" + }, + { + "startFrom": 1230, + "lowerCase": "下午", + "upperCase": "下午" + }, + { + "startFrom": 1800, + "lowerCase": "晚上", + "upperCase": "晚上" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/zh-hk.json b/Client2020/content/configs/DateTimeLocaleConfigs/zh-hk.json new file mode 100644 index 0000000..1955433 --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/zh-hk.json @@ -0,0 +1,105 @@ +{ + "months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "monthsShort": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "weekdays": [ + "星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六" + ], + "weekdaysShort": [ + "週日", + "週一", + "週二", + "週三", + "週四", + "週五", + "週六" + ], + "weekdaysMin": [ + "日", + "一", + "二", + "三", + "四", + "五", + "六" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY/MM/DD", + "LL": "YYYY年M月D日", + "LLL": "YYYY年M月D日 HH:mm", + "LLLL": "YYYY年M月D日dddd HH:mm", + "l": "YYYY/M/D", + "ll": "YYYY年M月D日", + "lll": "YYYY年M月D日 HH:mm", + "llll": "YYYY年M月D日dddd HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "凌晨", + "upperCase": "凌晨" + }, + { + "startFrom": 600, + "lowerCase": "早上", + "upperCase": "早上" + }, + { + "startFrom": 900, + "lowerCase": "上午", + "upperCase": "上午" + }, + { + "startFrom": 1130, + "lowerCase": "中午", + "upperCase": "中午" + }, + { + "startFrom": 1230, + "lowerCase": "下午", + "upperCase": "下午" + }, + { + "startFrom": 1800, + "lowerCase": "晚上", + "upperCase": "晚上" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/configs/DateTimeLocaleConfigs/zh-tw.json b/Client2020/content/configs/DateTimeLocaleConfigs/zh-tw.json new file mode 100644 index 0000000..1955433 --- /dev/null +++ b/Client2020/content/configs/DateTimeLocaleConfigs/zh-tw.json @@ -0,0 +1,105 @@ +{ + "months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "monthsShort": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "weekdays": [ + "星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六" + ], + "weekdaysShort": [ + "週日", + "週一", + "週二", + "週三", + "週四", + "週五", + "週六" + ], + "weekdaysMin": [ + "日", + "一", + "二", + "三", + "四", + "五", + "六" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY/MM/DD", + "LL": "YYYY年M月D日", + "LLL": "YYYY年M月D日 HH:mm", + "LLLL": "YYYY年M月D日dddd HH:mm", + "l": "YYYY/M/D", + "ll": "YYYY年M月D日", + "lll": "YYYY年M月D日 HH:mm", + "llll": "YYYY年M月D日dddd HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "凌晨", + "upperCase": "凌晨" + }, + { + "startFrom": 600, + "lowerCase": "早上", + "upperCase": "早上" + }, + { + "startFrom": 900, + "lowerCase": "上午", + "upperCase": "上午" + }, + { + "startFrom": 1130, + "lowerCase": "中午", + "upperCase": "中午" + }, + { + "startFrom": 1230, + "lowerCase": "下午", + "upperCase": "下午" + }, + { + "startFrom": 1800, + "lowerCase": "晚上", + "upperCase": "晚上" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2020/content/fonts/AccanthisADFStd-Regular.otf b/Client2020/content/fonts/AccanthisADFStd-Regular.otf new file mode 100644 index 0000000..b3d6f03 Binary files /dev/null and b/Client2020/content/fonts/AccanthisADFStd-Regular.otf differ diff --git a/Client2020/content/fonts/AmaticSC-Bold.ttf b/Client2020/content/fonts/AmaticSC-Bold.ttf new file mode 100644 index 0000000..0f24d11 Binary files /dev/null and b/Client2020/content/fonts/AmaticSC-Bold.ttf differ diff --git a/Client2020/content/fonts/AmaticSC-Regular.ttf b/Client2020/content/fonts/AmaticSC-Regular.ttf new file mode 100644 index 0000000..5f16090 Binary files /dev/null and b/Client2020/content/fonts/AmaticSC-Regular.ttf differ diff --git a/Client2020/content/fonts/Balthazar-Regular.ttf b/Client2020/content/fonts/Balthazar-Regular.ttf new file mode 100644 index 0000000..fff2ff7 Binary files /dev/null and b/Client2020/content/fonts/Balthazar-Regular.ttf differ diff --git a/Client2020/content/fonts/Bangers-Regular.ttf b/Client2020/content/fonts/Bangers-Regular.ttf new file mode 100644 index 0000000..027af19 Binary files /dev/null and b/Client2020/content/fonts/Bangers-Regular.ttf differ diff --git a/Client2020/content/fonts/ComicNeue-Angular-Bold.ttf b/Client2020/content/fonts/ComicNeue-Angular-Bold.ttf new file mode 100644 index 0000000..d70a258 Binary files /dev/null and b/Client2020/content/fonts/ComicNeue-Angular-Bold.ttf differ diff --git a/Client2020/content/fonts/Creepster-Regular.ttf b/Client2020/content/fonts/Creepster-Regular.ttf new file mode 100644 index 0000000..e09c147 Binary files /dev/null and b/Client2020/content/fonts/Creepster-Regular.ttf differ diff --git a/Client2020/content/fonts/DenkOne-Regular.ttf b/Client2020/content/fonts/DenkOne-Regular.ttf new file mode 100644 index 0000000..e20703d Binary files /dev/null and b/Client2020/content/fonts/DenkOne-Regular.ttf differ diff --git a/Client2020/content/fonts/Fondamento-Italic.ttf b/Client2020/content/fonts/Fondamento-Italic.ttf new file mode 100644 index 0000000..80f50c4 Binary files /dev/null and b/Client2020/content/fonts/Fondamento-Italic.ttf differ diff --git a/Client2020/content/fonts/Fondamento-Regular.ttf b/Client2020/content/fonts/Fondamento-Regular.ttf new file mode 100644 index 0000000..a2bc80c Binary files /dev/null and b/Client2020/content/fonts/Fondamento-Regular.ttf differ diff --git a/Client2020/content/fonts/FredokaOne-Regular.ttf b/Client2020/content/fonts/FredokaOne-Regular.ttf new file mode 100644 index 0000000..9b384aa Binary files /dev/null and b/Client2020/content/fonts/FredokaOne-Regular.ttf differ diff --git a/Client2020/content/fonts/GothamSSm-Black.otf b/Client2020/content/fonts/GothamSSm-Black.otf new file mode 100644 index 0000000..8faae7e Binary files /dev/null and b/Client2020/content/fonts/GothamSSm-Black.otf differ diff --git a/Client2020/content/fonts/GothamSSm-Bold.otf b/Client2020/content/fonts/GothamSSm-Bold.otf new file mode 100644 index 0000000..cc64c8b Binary files /dev/null and b/Client2020/content/fonts/GothamSSm-Bold.otf differ diff --git a/Client2020/content/fonts/GothamSSm-Book.otf b/Client2020/content/fonts/GothamSSm-Book.otf new file mode 100644 index 0000000..5cdc825 Binary files /dev/null and b/Client2020/content/fonts/GothamSSm-Book.otf differ diff --git a/Client2020/content/fonts/GothamSSm-Medium.otf b/Client2020/content/fonts/GothamSSm-Medium.otf new file mode 100644 index 0000000..27f90c0 Binary files /dev/null and b/Client2020/content/fonts/GothamSSm-Medium.otf differ diff --git a/Client2020/content/fonts/GrenzeGotisch-Bold.ttf b/Client2020/content/fonts/GrenzeGotisch-Bold.ttf new file mode 100644 index 0000000..4ca6313 Binary files /dev/null and b/Client2020/content/fonts/GrenzeGotisch-Bold.ttf differ diff --git a/Client2020/content/fonts/GrenzeGotisch-Regular.ttf b/Client2020/content/fonts/GrenzeGotisch-Regular.ttf new file mode 100644 index 0000000..3552e82 Binary files /dev/null and b/Client2020/content/fonts/GrenzeGotisch-Regular.ttf differ diff --git a/Client2020/content/fonts/Guru-Regular.otf b/Client2020/content/fonts/Guru-Regular.otf new file mode 100644 index 0000000..20d7398 Binary files /dev/null and b/Client2020/content/fonts/Guru-Regular.otf differ diff --git a/Client2020/content/fonts/HWYGOTH.ttf b/Client2020/content/fonts/HWYGOTH.ttf new file mode 100644 index 0000000..20ac3e2 Binary files /dev/null and b/Client2020/content/fonts/HWYGOTH.ttf differ diff --git a/Client2020/content/fonts/Inconsolata-Regular.ttf b/Client2020/content/fonts/Inconsolata-Regular.ttf new file mode 100644 index 0000000..bbc9647 Binary files /dev/null and b/Client2020/content/fonts/Inconsolata-Regular.ttf differ diff --git a/Client2020/content/fonts/IndieFlower-Regular.ttf b/Client2020/content/fonts/IndieFlower-Regular.ttf new file mode 100644 index 0000000..1070aac Binary files /dev/null and b/Client2020/content/fonts/IndieFlower-Regular.ttf differ diff --git a/Client2020/content/fonts/JosefinSans-Regular.ttf b/Client2020/content/fonts/JosefinSans-Regular.ttf new file mode 100644 index 0000000..89d36f8 Binary files /dev/null and b/Client2020/content/fonts/JosefinSans-Regular.ttf differ diff --git a/Client2020/content/fonts/Jura-Regular.ttf b/Client2020/content/fonts/Jura-Regular.ttf new file mode 100644 index 0000000..64ae1a3 Binary files /dev/null and b/Client2020/content/fonts/Jura-Regular.ttf differ diff --git a/Client2020/content/fonts/Kalam-Regular.ttf b/Client2020/content/fonts/Kalam-Regular.ttf new file mode 100644 index 0000000..16f1586 Binary files /dev/null and b/Client2020/content/fonts/Kalam-Regular.ttf differ diff --git a/Client2020/content/fonts/LuckiestGuy-Regular.ttf b/Client2020/content/fonts/LuckiestGuy-Regular.ttf new file mode 100644 index 0000000..8c79c87 Binary files /dev/null and b/Client2020/content/fonts/LuckiestGuy-Regular.ttf differ diff --git a/Client2020/content/fonts/Merriweather-Italic.ttf b/Client2020/content/fonts/Merriweather-Italic.ttf new file mode 100644 index 0000000..179acf3 Binary files /dev/null and b/Client2020/content/fonts/Merriweather-Italic.ttf differ diff --git a/Client2020/content/fonts/Merriweather-Regular.ttf b/Client2020/content/fonts/Merriweather-Regular.ttf new file mode 100644 index 0000000..18da9e5 Binary files /dev/null and b/Client2020/content/fonts/Merriweather-Regular.ttf differ diff --git a/Client2020/content/fonts/Michroma-Regular.ttf b/Client2020/content/fonts/Michroma-Regular.ttf new file mode 100644 index 0000000..0c35394 Binary files /dev/null and b/Client2020/content/fonts/Michroma-Regular.ttf differ diff --git a/Client2020/content/fonts/NotoSansBengaliUI-Regular.ttf b/Client2020/content/fonts/NotoSansBengaliUI-Regular.ttf new file mode 100644 index 0000000..f53a89c Binary files /dev/null and b/Client2020/content/fonts/NotoSansBengaliUI-Regular.ttf differ diff --git a/Client2020/content/fonts/NotoSansDevanagariUI-Regular.ttf b/Client2020/content/fonts/NotoSansDevanagariUI-Regular.ttf new file mode 100644 index 0000000..1f9fb2e Binary files /dev/null and b/Client2020/content/fonts/NotoSansDevanagariUI-Regular.ttf differ diff --git a/Client2020/content/fonts/NotoSansGeorgian-Regular.ttf b/Client2020/content/fonts/NotoSansGeorgian-Regular.ttf new file mode 100644 index 0000000..8a96e5e Binary files /dev/null and b/Client2020/content/fonts/NotoSansGeorgian-Regular.ttf differ diff --git a/Client2020/content/fonts/NotoSansKhmerUI-Regular.ttf b/Client2020/content/fonts/NotoSansKhmerUI-Regular.ttf new file mode 100644 index 0000000..a965f2b Binary files /dev/null and b/Client2020/content/fonts/NotoSansKhmerUI-Regular.ttf differ diff --git a/Client2020/content/fonts/NotoSansMyanmarUI-Regular.ttf b/Client2020/content/fonts/NotoSansMyanmarUI-Regular.ttf new file mode 100644 index 0000000..b015550 Binary files /dev/null and b/Client2020/content/fonts/NotoSansMyanmarUI-Regular.ttf differ diff --git a/Client2020/content/fonts/NotoSansSinhalaUI-Regular.ttf b/Client2020/content/fonts/NotoSansSinhalaUI-Regular.ttf new file mode 100644 index 0000000..4a49b3c Binary files /dev/null and b/Client2020/content/fonts/NotoSansSinhalaUI-Regular.ttf differ diff --git a/Client2020/content/fonts/NotoSansThaiUI-Regular.ttf b/Client2020/content/fonts/NotoSansThaiUI-Regular.ttf new file mode 100644 index 0000000..89bc809 Binary files /dev/null and b/Client2020/content/fonts/NotoSansThaiUI-Regular.ttf differ diff --git a/Client2020/content/fonts/Nunito-Regular.ttf b/Client2020/content/fonts/Nunito-Regular.ttf new file mode 100644 index 0000000..fdeb018 Binary files /dev/null and b/Client2020/content/fonts/Nunito-Regular.ttf differ diff --git a/Client2020/content/fonts/Oswald-Bold.ttf b/Client2020/content/fonts/Oswald-Bold.ttf new file mode 100644 index 0000000..6d99a38 Binary files /dev/null and b/Client2020/content/fonts/Oswald-Bold.ttf differ diff --git a/Client2020/content/fonts/Oswald-Regular.ttf b/Client2020/content/fonts/Oswald-Regular.ttf new file mode 100644 index 0000000..2492c44 Binary files /dev/null and b/Client2020/content/fonts/Oswald-Regular.ttf differ diff --git a/Client2020/content/fonts/PatrickHand-Regular.ttf b/Client2020/content/fonts/PatrickHand-Regular.ttf new file mode 100644 index 0000000..1e871cf Binary files /dev/null and b/Client2020/content/fonts/PatrickHand-Regular.ttf differ diff --git a/Client2020/content/fonts/PermanentMarker-Regular.ttf b/Client2020/content/fonts/PermanentMarker-Regular.ttf new file mode 100644 index 0000000..6541e9d Binary files /dev/null and b/Client2020/content/fonts/PermanentMarker-Regular.ttf differ diff --git a/Client2020/content/fonts/PressStart2P-Regular.ttf b/Client2020/content/fonts/PressStart2P-Regular.ttf new file mode 100644 index 0000000..e659e95 Binary files /dev/null and b/Client2020/content/fonts/PressStart2P-Regular.ttf differ diff --git a/Client2020/content/fonts/Roboto-Bold.ttf b/Client2020/content/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000..d998cf5 Binary files /dev/null and b/Client2020/content/fonts/Roboto-Bold.ttf differ diff --git a/Client2020/content/fonts/Roboto-Italic.ttf b/Client2020/content/fonts/Roboto-Italic.ttf new file mode 100644 index 0000000..5b390ff Binary files /dev/null and b/Client2020/content/fonts/Roboto-Italic.ttf differ diff --git a/Client2020/content/fonts/Roboto-Regular.ttf b/Client2020/content/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..2b6392f Binary files /dev/null and b/Client2020/content/fonts/Roboto-Regular.ttf differ diff --git a/Client2020/content/fonts/RobotoCondensed-Regular.ttf b/Client2020/content/fonts/RobotoCondensed-Regular.ttf new file mode 100644 index 0000000..62dd61e Binary files /dev/null and b/Client2020/content/fonts/RobotoCondensed-Regular.ttf differ diff --git a/Client2020/content/fonts/RobotoMono-Regular.ttf b/Client2020/content/fonts/RobotoMono-Regular.ttf new file mode 100644 index 0000000..7c4ce36 Binary files /dev/null and b/Client2020/content/fonts/RobotoMono-Regular.ttf differ diff --git a/Client2020/content/fonts/RomanAntique.otf b/Client2020/content/fonts/RomanAntique.otf new file mode 100644 index 0000000..dac6f3d Binary files /dev/null and b/Client2020/content/fonts/RomanAntique.otf differ diff --git a/Client2020/content/fonts/Sarpanch-Bold.ttf b/Client2020/content/fonts/Sarpanch-Bold.ttf new file mode 100644 index 0000000..f7603a2 Binary files /dev/null and b/Client2020/content/fonts/Sarpanch-Bold.ttf differ diff --git a/Client2020/content/fonts/Sarpanch-Regular.ttf b/Client2020/content/fonts/Sarpanch-Regular.ttf new file mode 100644 index 0000000..65ae057 Binary files /dev/null and b/Client2020/content/fonts/Sarpanch-Regular.ttf differ diff --git a/Client2020/content/fonts/SourceSansPro-Bold.ttf b/Client2020/content/fonts/SourceSansPro-Bold.ttf new file mode 100644 index 0000000..be46652 Binary files /dev/null and b/Client2020/content/fonts/SourceSansPro-Bold.ttf differ diff --git a/Client2020/content/fonts/SourceSansPro-It.ttf b/Client2020/content/fonts/SourceSansPro-It.ttf new file mode 100644 index 0000000..c689cd2 Binary files /dev/null and b/Client2020/content/fonts/SourceSansPro-It.ttf differ diff --git a/Client2020/content/fonts/SourceSansPro-Light.ttf b/Client2020/content/fonts/SourceSansPro-Light.ttf new file mode 100644 index 0000000..5094f90 Binary files /dev/null and b/Client2020/content/fonts/SourceSansPro-Light.ttf differ diff --git a/Client2020/content/fonts/SourceSansPro-Regular.ttf b/Client2020/content/fonts/SourceSansPro-Regular.ttf new file mode 100644 index 0000000..24962c7 Binary files /dev/null and b/Client2020/content/fonts/SourceSansPro-Regular.ttf differ diff --git a/Client2020/content/fonts/SourceSansPro-Semibold.ttf b/Client2020/content/fonts/SourceSansPro-Semibold.ttf new file mode 100644 index 0000000..96be817 Binary files /dev/null and b/Client2020/content/fonts/SourceSansPro-Semibold.ttf differ diff --git a/Client2020/content/fonts/SpecialElite-Regular.ttf b/Client2020/content/fonts/SpecialElite-Regular.ttf new file mode 100644 index 0000000..a645a5e Binary files /dev/null and b/Client2020/content/fonts/SpecialElite-Regular.ttf differ diff --git a/Client2020/content/fonts/TitilliumWeb-Bold.ttf b/Client2020/content/fonts/TitilliumWeb-Bold.ttf new file mode 100644 index 0000000..b51a4d6 Binary files /dev/null and b/Client2020/content/fonts/TitilliumWeb-Bold.ttf differ diff --git a/Client2020/content/fonts/TitilliumWeb-Regular.ttf b/Client2020/content/fonts/TitilliumWeb-Regular.ttf new file mode 100644 index 0000000..a54ad4b Binary files /dev/null and b/Client2020/content/fonts/TitilliumWeb-Regular.ttf differ diff --git a/Client2020/content/fonts/TwemojiMozilla.ttf b/Client2020/content/fonts/TwemojiMozilla.ttf new file mode 100644 index 0000000..6091c67 Binary files /dev/null and b/Client2020/content/fonts/TwemojiMozilla.ttf differ diff --git a/Client2020/content/fonts/Ubuntu-Italic.ttf b/Client2020/content/fonts/Ubuntu-Italic.ttf new file mode 100644 index 0000000..b022726 Binary files /dev/null and b/Client2020/content/fonts/Ubuntu-Italic.ttf differ diff --git a/Client2020/content/fonts/Ubuntu-Regular.ttf b/Client2020/content/fonts/Ubuntu-Regular.ttf new file mode 100644 index 0000000..dbb834a Binary files /dev/null and b/Client2020/content/fonts/Ubuntu-Regular.ttf differ diff --git a/Client2020/content/fonts/arial.ttf b/Client2020/content/fonts/arial.ttf new file mode 100644 index 0000000..729da61 Binary files /dev/null and b/Client2020/content/fonts/arial.ttf differ diff --git a/Client2020/content/fonts/arialbd.ttf b/Client2020/content/fonts/arialbd.ttf new file mode 100644 index 0000000..aac564c Binary files /dev/null and b/Client2020/content/fonts/arialbd.ttf differ diff --git a/Client2020/content/fonts/gamecontrollerdb.txt b/Client2020/content/fonts/gamecontrollerdb.txt new file mode 100644 index 0000000..dd49836 --- /dev/null +++ b/Client2020/content/fonts/gamecontrollerdb.txt @@ -0,0 +1,89 @@ +# Windows - DINPUT +8f0e1200000000000000504944564944,Acme,platform:Windows,x:b2,a:b0,b:b1,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +341a3608000000000000504944564944,Afterglow PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, +ffff0000000000000000504944564944,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, +6d0416c2000000000000504944564944,Generic DirectInput Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, +6d0419c2000000000000504944564944,Logitech F710 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, +88880803000000000000504944564944,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b9,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b0,y:b3,platform:Windows, +4c056802000000000000504944564944,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Windows, +25090500000000000000504944564944,PS3 DualShock,a:b2,b:b1,back:b9,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b0,y:b3,platform:Windows, +4c05c405000000000000504944564944,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, +6d0418c2000000000000504944564944,Logitech RumblePad 2 USB,platform:Windows,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, +36280100000000000000504944564944,OUYA Controller,platform:Windows,a:b0,b:b3,y:b2,x:b1,start:b14,guide:b15,leftstick:b6,rightstick:b7,leftshoulder:b4,rightshoulder:b5,dpup:b8,dpleft:b10,dpdown:b9,dpright:b11,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:b12,righttrigger:b13, +4f0400b3000000000000504944564944,Thrustmaster Firestorm Dual Power,a:b0,b:b2,y:b3,x:b1,start:b10,guide:b8,back:b9,leftstick:b11,rightstick:b12,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Windows, +00f00300000000000000504944564944,RetroUSB.com RetroPad,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Windows, +00f0f100000000000000504944564944,RetroUSB.com Super RetroPort,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Windows, +28040140000000000000504944564944,GamePad Pro USB,platform:Windows,a:b1,b:b2,x:b0,y:b3,back:b8,start:b9,leftshoulder:b4,rightshoulder:b5,leftx:a0,lefty:a1,lefttrigger:b6,righttrigger:b7, +ff113133000000000000504944564944,SVEN X-PAD,platform:Windows,a:b2,b:b3,y:b1,x:b0,start:b5,back:b4,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a4,lefttrigger:b8,righttrigger:b9, +8f0e0300000000000000504944564944,Piranha xtreme,platform:Windows,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +8f0e0d31000000000000504944564944,Multilaser JS071 USB,platform:Windows,a:b1,b:b2,y:b3,x:b0,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7, +10080300000000000000504944564944,PS2 USB,platform:Windows,a:b2,b:b1,y:b0,x:b3,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a4,righty:a2,lefttrigger:b4,righttrigger:b5, +79000600000000000000504944564944,G-Shark GS-GP702,a:b2,b:b1,x:b3,y:b0,back:b8,start:b9,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a4,lefttrigger:b6,righttrigger:b7,platform:Windows, +4b12014d000000000000504944564944,NYKO AIRFLO,a:b0,b:b1,x:b2,y:b3,back:b8,guide:b10,start:b9,leftstick:a0,rightstick:a2,leftshoulder:a3,rightshoulder:b5,dpup:h0.1,dpdown:h0.0,dpleft:h0.8,dpright:h0.2,leftx:h0.6,lefty:h0.12,rightx:h0.9,righty:h0.4,lefttrigger:b6,righttrigger:b7,platform:Windows, +d6206dca000000000000504944564944,PowerA Pro Ex,a:b1,b:b2,x:b0,y:b3,back:b8,guide:b12,start:b9,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.0,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7,platform:Windows, +a3060cff000000000000504944564944,Saitek P2500,a:b2,b:b3,y:b1,x:b0,start:b4,guide:b10,back:b5,leftstick:b8,rightstick:b9,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,platform:Windows, +8f0e0300000000000000504944564944,Trust GTX 28,a:b2,b:b1,y:b0,x:b3,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7,platform:Windows, +4f0415b3000000000000504944564944,Thrustmaster Dual Analog 3.2,platform:Windows,x:b1,a:b0,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, + +# OS X +0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X, +6d0400000000000016c2000000000000,Logitech F310 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, +6d0400000000000018c2000000000000,Logitech F510 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, +6d040000000000001fc2000000000000,Logitech F710 Gamepad (XInput),a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, +6d0400000000000019c2000000000000,Logitech Wireless Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, +4c050000000000006802000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Mac OS X, +4c05000000000000c405000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,Platform:Mac OS X, +5e040000000000008e02000000000000,X360 Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, +891600000000000000fd000000000000,Razer Onza Tournament,a:b0,b:b1,y:b3,x:b2,start:b8,guide:b10,back:b9,leftstick:b6,rightstick:b7,leftshoulder:b4,rightshoulder:b5,dpup:b11,dpleft:b13,dpdown:b12,dpright:b14,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Mac OS X, +4f0400000000000000b3000000000000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,y:b3,x:b1,start:b10,guide:b8,back:b9,leftstick:b11,rightstick:,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Mac OS X, +8f0e0000000000000300000000000000,Piranha xtreme,platform:Mac OS X,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +0d0f0000000000004d00000000000000,HORI Gem Pad 3,platform:Mac OS X,a:b1,b:b2,y:b3,x:b0,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7, +79000000000000000600000000000000,G-Shark GP-702,a:b2,b:b1,x:b3,y:b0,back:b8,start:b9,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:b6,righttrigger:b7,platform:Mac OS X, +4f0400000000000015b3000000000000,Thrustmaster Dual Analog 3.2,platform:Mac OS X,x:b1,a:b0,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, + +# Linux +0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, +03000000ba2200002010000001010000,Jess Technology USB Game Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux, +030000006d04000019c2000010010000,Logitech Cordless RumblePad 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, +030000006d0400001dc2000014400000,Logitech F310 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000006d0400001ec2000020200000,Logitech F510 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000006d04000019c2000011010000,Logitech F710 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, +030000006d0400001fc2000005030000,Logitech F710 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000004c0500006802000011010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux, +030000004c050000c405000011010000,Sony DualShock 4,a:b1,b:b2,y:b3,x:b0,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a5,lefttrigger:b6,righttrigger:b7,platform:Linux, +03000000de280000ff11000001000000,Valve Streaming Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e0400008e02000014010000,X360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e0400008e02000010010000,X360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e0400001907000000010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +03000000100800000100000010010000,Twin USB PS2 Adapter,a:b2,b:b1,y:b0,x:b3,start:b9,guide:,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a2,lefttrigger:b4,righttrigger:b5,platform:Linux, +03000000a306000023f6000011010000,Saitek Cyborg V.1 Game Pad,a:b1,b:b2,y:b3,x:b0,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a4,lefttrigger:b6,righttrigger:b7,platform:Linux, +030000004f04000020b3000010010000,Thrustmaster 2 in 1 DT,a:b0,b:b2,y:b3,x:b1,start:b9,guide:,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Linux, +030000004f04000023b3000000010000,Thrustmaster Dual Trigger 3-in-1,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a5, +030000008f0e00000300000010010000,GreenAsia Inc. USB Joystick ,platform:Linux,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +030000008f0e00001200000010010000,GreenAsia Inc. USB Joystick ,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +030000005e0400009102000007010000,X360 Wireless Controller,a:b0,b:b1,y:b3,x:b2,start:b7,guide:b8,back:b6,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:b13,dpleft:b11,dpdown:b14,dpright:b12,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux, +030000006d04000016c2000010010000,Logitech Logitech Dual Action,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, +03000000260900008888000000010000,GameCube {WiseGroup USB box},a:b0,b:b2,y:b3,x:b1,start:b7,leftshoulder:,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,rightstick:,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,platform:Linux, +030000006d04000011c2000010010000,Logitech WingMan Cordless RumblePad,a:b0,b:b1,y:b4,x:b3,start:b8,guide:b5,back:b2,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:b9,righttrigger:b10,platform:Linux, +030000006d04000018c2000010010000,Logitech Logitech RumblePad 2 USB,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, +05000000d6200000ad0d000001000000,Moga Pro,platform:Linux,a:b0,b:b1,y:b3,x:b2,start:b6,leftstick:b7,rightstick:b8,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a5,righttrigger:a4, +030000004f04000009d0000000010000,Thrustmaster Run N Drive Wireless PS3,platform:Linux,a:b1,b:b2,x:b0,y:b3,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7, +030000004f04000008d0000000010000,Thrustmaster Run N Drive Wireless,platform:Linux,a:b1,b:b2,x:b0,y:b3,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a5,lefttrigger:b6,righttrigger:b7, +0300000000f000000300000000010000,RetroUSB.com RetroPad,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Linux, +0300000000f00000f100000000010000,RetroUSB.com Super RetroPort,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Linux, +030000006f0e00001f01000000010000,Generic X-Box pad,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b6,guide:b8,start:b7,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,leftstick:b9,rightstick:b10,leftx:a0,lefty:a1,rightx:a3,righty:a4, +03000000280400000140000000010000,Gravis GamePad Pro USB ,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftx:a0,lefty:a1, +030000005e0400008902000021010000,Microsoft X-Box pad v2 (US),platform:Linux,x:b3,a:b0,b:b1,y:b4,back:b6,start:b7,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b5,lefttrigger:a2,rightshoulder:b2,righttrigger:a5,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a3,righty:a4, +030000006f0e00001e01000011010000,Rock Candy Gamepad for PS3,platform:Linux,a:b1,b:b2,x:b0,y:b3,back:b8,start:b9,guide:b12,leftshoulder:b4,rightshoulder:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2, +03000000250900000500000000010000,Sony PS2 pad with SmartJoy adapter,platform:Linux,a:b2,b:b1,y:b0,x:b3,start:b8,back:b9,leftstick:b10,rightstick:b11,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b4,righttrigger:b5, +030000008916000000fd000024010000,Razer Onza Tournament,a:b0,b:b1,y:b3,x:b2,start:b7,guide:b8,back:b6,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:b13,dpleft:b11,dpdown:b14,dpright:b12,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux, +030000004f04000000b3000010010000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,y:b3,x:b1,start:b10,guide:b8,back:b9,leftstick:b11,rightstick:b12,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Linux, +03000000ad1b000001f5000033050000,Hori Pad EX Turbo 2,a:b0,b:b1,y:b3,x:b2,start:b7,guide:b8,back:b6,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux, +050000004c050000c405000000010000,PS4 Controller (Bluetooth),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, +060000004c0500006802000000010000,PS3 Controller (Bluetooth),a:b14,b:b13,y:b12,x:b15,start:b3,guide:b16,back:b0,leftstick:b1,rightstick:b2,leftshoulder:b10,rightshoulder:b11,dpup:b4,dpleft:b7,dpdown:b6,dpright:b5,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b8,righttrigger:b9,platform:Linux, +03000000790000000600000010010000,DragonRise Inc. Generic USB Joystick ,platform:Linux,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a4, +03000000666600000488000000010000,Super Joy Box 5 Pro,platform:Linux,a:b2,b:b1,x:b3,y:b0,back:b9,start:b8,leftshoulder:b6,rightshoulder:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b4,righttrigger:b5,dpup:b12,dpleft:b15,dpdown:b14,dpright:b13, +05000000362800000100000002010000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,platform:Linux,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2, +05000000362800000100000003010000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,platform:Linux,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2, +030000008916000001fd000024010000,Razer Onza Classic Edition,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b6,guide:b8,start:b7,dpleft:b11,dpdown:b14,dpright:b12,dpup:b13,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,leftstick:b9,rightstick:b10,leftx:a0,lefty:a1,rightx:a3,righty:a4, +030000005e040000d102000001010000,Microsoft X-Box One pad,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b6,guide:b8,start:b7,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,leftstick:b9,rightstick:b10,leftx:a0,lefty:a1,rightx:a3,righty:a4, \ No newline at end of file diff --git a/Client2020/content/fonts/zekton_rg.ttf b/Client2020/content/fonts/zekton_rg.ttf new file mode 100644 index 0000000..8ad2dd1 Binary files /dev/null and b/Client2020/content/fonts/zekton_rg.ttf differ diff --git a/Client2020/content/models/AnimationEditor/AnimationEditorGUI.rbxm b/Client2020/content/models/AnimationEditor/AnimationEditorGUI.rbxm new file mode 100644 index 0000000..52bbda4 Binary files /dev/null and b/Client2020/content/models/AnimationEditor/AnimationEditorGUI.rbxm differ diff --git a/Client2020/content/models/RigBuilder/AnthroRigs.rbxm b/Client2020/content/models/RigBuilder/AnthroRigs.rbxm new file mode 100644 index 0000000..b10542a Binary files /dev/null and b/Client2020/content/models/RigBuilder/AnthroRigs.rbxm differ diff --git a/Client2020/content/models/RigBuilder/RigBuilderGUI.rbxm b/Client2020/content/models/RigBuilder/RigBuilderGUI.rbxm new file mode 100644 index 0000000..df189cb Binary files /dev/null and b/Client2020/content/models/RigBuilder/RigBuilderGUI.rbxm differ diff --git a/Client2020/content/models/Thumbnails/Mannequins/R15.rbxm b/Client2020/content/models/Thumbnails/Mannequins/R15.rbxm new file mode 100644 index 0000000..db99688 Binary files /dev/null and b/Client2020/content/models/Thumbnails/Mannequins/R15.rbxm differ diff --git a/Client2020/content/models/Thumbnails/Mannequins/R6.rbxmx b/Client2020/content/models/Thumbnails/Mannequins/R6.rbxmx new file mode 100644 index 0000000..757716b --- /dev/null +++ b/Client2020/content/models/Thumbnails/Mannequins/R6.rbxmx @@ -0,0 +1,970 @@ + + null + nil + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + MrGrey + RBX1 + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 208 + + 0 + 4.5 + -0.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 1 + 1 + + + + + 2 + 2 + + 0 + Mesh + + 0 + 0 + 0 + + + 1.25 + 1.25 + 1.25 + + + + 1 + 1 + 1 + + + + + + 5 + face + rbxasset://textures/face.png + 0 + + + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + FaceCenterAttachment + false + + + + + + 0 + 0 + -0.600000024 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + FaceFrontAttachment + false + + + + + + 0 + 0.600000024 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + HairAttachment + false + + + + + + 0 + 0.600000024 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + HatAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 208 + + 0 + 3 + -0.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 2 + 0 + true + 256 + Torso + 0 + 0 + 0 + 2 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 2 + 1 + + + + + + 1 + 0.5 + 0 + 0 + 0 + 1 + 0 + 1 + 0 + -1 + -0 + -0 + + + -0.5 + 0.5 + 0 + 0 + 0 + 1 + 0 + 1 + 0 + -1 + -0 + -0 + + 0 + 0.100000001 + Right Shoulder + RBX4 + RBX6 + + + + + + -1 + 0.5 + 0 + -0 + -0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + + 0.5 + 0.5 + 0 + -0 + -0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + 0 + 0.100000001 + Left Shoulder + RBX4 + RBX8 + + + + + + 1 + -1 + 0 + 0 + 0 + 1 + 0 + 1 + 0 + -1 + -0 + -0 + + + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 1 + 0 + -1 + -0 + -0 + + 0 + 0.100000001 + Right Hip + RBX4 + RBX10 + + + + + + -1 + -1 + 0 + -0 + -0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + + -0.5 + 1 + 0 + -0 + -0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + 0 + 0.100000001 + Left Hip + RBX4 + RBX12 + + + + + + 0 + 1 + 0 + -1 + -0 + -0 + 0 + 0 + 1 + 0 + 1 + 0 + + + 0 + -0.5 + 0 + -1 + -0 + -0 + 0 + 0 + 1 + 0 + 1 + 0 + + 0 + 0.100000001 + Neck + RBX4 + RBX1 + + + + + + 0 + 0 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyBackAttachment + false + + + + + + 0 + 0 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyFrontAttachment + false + + + + + + -1 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftCollarAttachment + false + + + + + + 0 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckAttachment + false + + + + + + 1 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightCollarAttachment + false + + + + + + 0 + -1 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistBackAttachment + false + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistCenterAttachment + false + + + + + + 0 + -1 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistFrontAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 208 + + 1.5 + 3 + -0.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Left Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 208 + + -1.5 + 3 + -0.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Right Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 208 + + 0.5 + 1 + -0.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Left Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 208 + + -0.5 + 1 + -0.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Right Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 100 + 100 + 0 + 50 + 100 + 89 + Humanoid + 100 + 2 + 0 + 16 + + + + \ No newline at end of file diff --git a/Client2020/content/models/Thumbnails/Mannequins/Rthro.rbxm b/Client2020/content/models/Thumbnails/Mannequins/Rthro.rbxm new file mode 100644 index 0000000..b1603cf Binary files /dev/null and b/Client2020/content/models/Thumbnails/Mannequins/Rthro.rbxm differ diff --git a/Client2020/content/models/ViewSelector/Axis.mesh b/Client2020/content/models/ViewSelector/Axis.mesh new file mode 100644 index 0000000..83cd155 Binary files /dev/null and b/Client2020/content/models/ViewSelector/Axis.mesh differ diff --git a/Client2020/content/models/ViewSelector/Basic.mesh b/Client2020/content/models/ViewSelector/Basic.mesh new file mode 100644 index 0000000..077ecb8 Binary files /dev/null and b/Client2020/content/models/ViewSelector/Basic.mesh differ diff --git a/Client2020/content/models/ViewSelector/Corner.mesh b/Client2020/content/models/ViewSelector/Corner.mesh new file mode 100644 index 0000000..291e930 Binary files /dev/null and b/Client2020/content/models/ViewSelector/Corner.mesh differ diff --git a/Client2020/content/models/ViewSelector/ViewSelector.rbxm b/Client2020/content/models/ViewSelector/ViewSelector.rbxm new file mode 100644 index 0000000..7c3b9e1 Binary files /dev/null and b/Client2020/content/models/ViewSelector/ViewSelector.rbxm differ diff --git a/Client2020/content/sky/moon.jpg b/Client2020/content/sky/moon.jpg new file mode 100644 index 0000000..247b6cd Binary files /dev/null and b/Client2020/content/sky/moon.jpg differ diff --git a/Client2020/content/sky/sun.jpg b/Client2020/content/sky/sun.jpg new file mode 100644 index 0000000..41057ac Binary files /dev/null and b/Client2020/content/sky/sun.jpg differ diff --git a/Client2020/content/sounds/action_falling.mp3 b/Client2020/content/sounds/action_falling.mp3 new file mode 100644 index 0000000..6408717 Binary files /dev/null and b/Client2020/content/sounds/action_falling.mp3 differ diff --git a/Client2020/content/sounds/action_footsteps_plastic.mp3 b/Client2020/content/sounds/action_footsteps_plastic.mp3 new file mode 100644 index 0000000..6804420 Binary files /dev/null and b/Client2020/content/sounds/action_footsteps_plastic.mp3 differ diff --git a/Client2020/content/sounds/action_get_up.mp3 b/Client2020/content/sounds/action_get_up.mp3 new file mode 100644 index 0000000..ababd77 Binary files /dev/null and b/Client2020/content/sounds/action_get_up.mp3 differ diff --git a/Client2020/content/sounds/action_jump.mp3 b/Client2020/content/sounds/action_jump.mp3 new file mode 100644 index 0000000..7159074 Binary files /dev/null and b/Client2020/content/sounds/action_jump.mp3 differ diff --git a/Client2020/content/sounds/action_jump_land.mp3 b/Client2020/content/sounds/action_jump_land.mp3 new file mode 100644 index 0000000..3d68ce1 Binary files /dev/null and b/Client2020/content/sounds/action_jump_land.mp3 differ diff --git a/Client2020/content/sounds/action_swim.mp3 b/Client2020/content/sounds/action_swim.mp3 new file mode 100644 index 0000000..95dd2d5 Binary files /dev/null and b/Client2020/content/sounds/action_swim.mp3 differ diff --git a/Client2020/content/sounds/bass.wav b/Client2020/content/sounds/bass.wav new file mode 100644 index 0000000..5f4b791 Binary files /dev/null and b/Client2020/content/sounds/bass.wav differ diff --git a/Client2020/content/sounds/impact_explosion_03.mp3 b/Client2020/content/sounds/impact_explosion_03.mp3 new file mode 100644 index 0000000..4501c83 Binary files /dev/null and b/Client2020/content/sounds/impact_explosion_03.mp3 differ diff --git a/Client2020/content/sounds/impact_water.mp3 b/Client2020/content/sounds/impact_water.mp3 new file mode 100644 index 0000000..741862b Binary files /dev/null and b/Client2020/content/sounds/impact_water.mp3 differ diff --git a/Client2020/content/sounds/snap.mp3 b/Client2020/content/sounds/snap.mp3 new file mode 100644 index 0000000..8dd8d5a Binary files /dev/null and b/Client2020/content/sounds/snap.mp3 differ diff --git a/Client2020/content/sounds/uuhhh.mp3 b/Client2020/content/sounds/uuhhh.mp3 new file mode 100644 index 0000000..81b3565 Binary files /dev/null and b/Client2020/content/sounds/uuhhh.mp3 differ diff --git a/Client2020/content/textures/AlignTool/AlignTool.png b/Client2020/content/textures/AlignTool/AlignTool.png new file mode 100644 index 0000000..278b590 Binary files /dev/null and b/Client2020/content/textures/AlignTool/AlignTool.png differ diff --git a/Client2020/content/textures/AlignTool/Center.png b/Client2020/content/textures/AlignTool/Center.png new file mode 100644 index 0000000..0f52463 Binary files /dev/null and b/Client2020/content/textures/AlignTool/Center.png differ diff --git a/Client2020/content/textures/AlignTool/Help_Dark.png b/Client2020/content/textures/AlignTool/Help_Dark.png new file mode 100644 index 0000000..49e0851 Binary files /dev/null and b/Client2020/content/textures/AlignTool/Help_Dark.png differ diff --git a/Client2020/content/textures/AlignTool/Help_Light.png b/Client2020/content/textures/AlignTool/Help_Light.png new file mode 100644 index 0000000..604e6f0 Binary files /dev/null and b/Client2020/content/textures/AlignTool/Help_Light.png differ diff --git a/Client2020/content/textures/AlignTool/Max.png b/Client2020/content/textures/AlignTool/Max.png new file mode 100644 index 0000000..e197ea8 Binary files /dev/null and b/Client2020/content/textures/AlignTool/Max.png differ diff --git a/Client2020/content/textures/AlignTool/Min.png b/Client2020/content/textures/AlignTool/Min.png new file mode 100644 index 0000000..e7a5390 Binary files /dev/null and b/Client2020/content/textures/AlignTool/Min.png differ diff --git a/Client2020/content/textures/AnchorCursor.png b/Client2020/content/textures/AnchorCursor.png new file mode 100644 index 0000000..cb00a6a Binary files /dev/null and b/Client2020/content/textures/AnchorCursor.png differ diff --git a/Client2020/content/textures/AnimationEditor/Checkmark.png b/Client2020/content/textures/AnimationEditor/Checkmark.png new file mode 100644 index 0000000..2d7a684 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/Checkmark.png differ diff --git a/Client2020/content/textures/AnimationEditor/Circle.png b/Client2020/content/textures/AnimationEditor/Circle.png new file mode 100644 index 0000000..eea192e Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/Circle.png differ diff --git a/Client2020/content/textures/AnimationEditor/Close.png b/Client2020/content/textures/AnimationEditor/Close.png new file mode 100644 index 0000000..2281cae Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/Close.png differ diff --git a/Client2020/content/textures/AnimationEditor/Pin.png b/Client2020/content/textures/AnimationEditor/Pin.png new file mode 100644 index 0000000..3b13b68 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/Pin.png differ diff --git a/Client2020/content/textures/AnimationEditor/RoundedBackground.png b/Client2020/content/textures/AnimationEditor/RoundedBackground.png new file mode 100644 index 0000000..602e90f Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/RoundedBackground.png differ diff --git a/Client2020/content/textures/AnimationEditor/RoundedBorder.png b/Client2020/content/textures/AnimationEditor/RoundedBorder.png new file mode 100644 index 0000000..8335459 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/RoundedBorder.png differ diff --git a/Client2020/content/textures/AnimationEditor/ScrollbarBottom.png b/Client2020/content/textures/AnimationEditor/ScrollbarBottom.png new file mode 100644 index 0000000..f84d082 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/ScrollbarBottom.png differ diff --git a/Client2020/content/textures/AnimationEditor/ScrollbarMiddle.png b/Client2020/content/textures/AnimationEditor/ScrollbarMiddle.png new file mode 100644 index 0000000..0e90165 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/ScrollbarMiddle.png differ diff --git a/Client2020/content/textures/AnimationEditor/ScrollbarTop.png b/Client2020/content/textures/AnimationEditor/ScrollbarTop.png new file mode 100644 index 0000000..beb8d14 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/ScrollbarTop.png differ diff --git a/Client2020/content/textures/AnimationEditor/addEvent_border.png b/Client2020/content/textures/AnimationEditor/addEvent_border.png new file mode 100644 index 0000000..cdae253 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/addEvent_border.png differ diff --git a/Client2020/content/textures/AnimationEditor/addEvent_inner.png b/Client2020/content/textures/AnimationEditor/addEvent_inner.png new file mode 100644 index 0000000..bcbfe9d Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/addEvent_inner.png differ diff --git a/Client2020/content/textures/AnimationEditor/animation_editor_32x32.png b/Client2020/content/textures/AnimationEditor/animation_editor_32x32.png new file mode 100644 index 0000000..2342f5b Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/animation_editor_32x32.png differ diff --git a/Client2020/content/textures/AnimationEditor/animation_editor_blue.png b/Client2020/content/textures/AnimationEditor/animation_editor_blue.png new file mode 100644 index 0000000..7d20402 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/animation_editor_blue.png differ diff --git a/Client2020/content/textures/AnimationEditor/btn_addEvent_border.png b/Client2020/content/textures/AnimationEditor/btn_addEvent_border.png new file mode 100644 index 0000000..cee4d3c Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/btn_addEvent_border.png differ diff --git a/Client2020/content/textures/AnimationEditor/btn_addEvent_inner.png b/Client2020/content/textures/AnimationEditor/btn_addEvent_inner.png new file mode 100644 index 0000000..ac37d56 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/btn_addEvent_inner.png differ diff --git a/Client2020/content/textures/AnimationEditor/btn_clearText.png b/Client2020/content/textures/AnimationEditor/btn_clearText.png new file mode 100644 index 0000000..a910d33 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/btn_clearText.png differ diff --git a/Client2020/content/textures/AnimationEditor/btn_collapse.png b/Client2020/content/textures/AnimationEditor/btn_collapse.png new file mode 100644 index 0000000..5bdcbeb Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/btn_collapse.png differ diff --git a/Client2020/content/textures/AnimationEditor/btn_delete.png b/Client2020/content/textures/AnimationEditor/btn_delete.png new file mode 100644 index 0000000..6a4c117 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/btn_delete.png differ diff --git a/Client2020/content/textures/AnimationEditor/btn_edit.png b/Client2020/content/textures/AnimationEditor/btn_edit.png new file mode 100644 index 0000000..49f301d Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/btn_edit.png differ diff --git a/Client2020/content/textures/AnimationEditor/btn_expand.png b/Client2020/content/textures/AnimationEditor/btn_expand.png new file mode 100644 index 0000000..35922dc Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/btn_expand.png differ diff --git a/Client2020/content/textures/AnimationEditor/btn_manage.png b/Client2020/content/textures/AnimationEditor/btn_manage.png new file mode 100644 index 0000000..7545e88 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/btn_manage.png differ diff --git a/Client2020/content/textures/AnimationEditor/btn_removeEvent.png b/Client2020/content/textures/AnimationEditor/btn_removeEvent.png new file mode 100644 index 0000000..6bd3c1b Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/btn_removeEvent.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_collapse.png b/Client2020/content/textures/AnimationEditor/button_collapse.png new file mode 100644 index 0000000..503a6f5 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_collapse.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_control_end.png b/Client2020/content/textures/AnimationEditor/button_control_end.png new file mode 100644 index 0000000..9493b62 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_control_end.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_control_next.png b/Client2020/content/textures/AnimationEditor/button_control_next.png new file mode 100644 index 0000000..e498efc Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_control_next.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_control_play.png b/Client2020/content/textures/AnimationEditor/button_control_play.png new file mode 100644 index 0000000..7e13020 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_control_play.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_control_previous.png b/Client2020/content/textures/AnimationEditor/button_control_previous.png new file mode 100644 index 0000000..95ad3cf Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_control_previous.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_control_start.png b/Client2020/content/textures/AnimationEditor/button_control_start.png new file mode 100644 index 0000000..2a13172 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_control_start.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_expand.png b/Client2020/content/textures/AnimationEditor/button_expand.png new file mode 100644 index 0000000..53d348d Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_expand.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_hierarchy_closed.png b/Client2020/content/textures/AnimationEditor/button_hierarchy_closed.png new file mode 100644 index 0000000..2a70e31 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_hierarchy_closed.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_hierarchy_opened.png b/Client2020/content/textures/AnimationEditor/button_hierarchy_opened.png new file mode 100644 index 0000000..c2d1e6d Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_hierarchy_opened.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_lock.png b/Client2020/content/textures/AnimationEditor/button_lock.png new file mode 100644 index 0000000..cf9ca95 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_lock.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_loop.png b/Client2020/content/textures/AnimationEditor/button_loop.png new file mode 100644 index 0000000..e585d2f Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_loop.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_pause_white@2x.png b/Client2020/content/textures/AnimationEditor/button_pause_white@2x.png new file mode 100644 index 0000000..7215f9b Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_pause_white@2x.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_popup_close.png b/Client2020/content/textures/AnimationEditor/button_popup_close.png new file mode 100644 index 0000000..3511f7c Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_popup_close.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_radio_background.png b/Client2020/content/textures/AnimationEditor/button_radio_background.png new file mode 100644 index 0000000..1f15a29 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_radio_background.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_radio_default.png b/Client2020/content/textures/AnimationEditor/button_radio_default.png new file mode 100644 index 0000000..5c54fd1 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_radio_default.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_radio_innercircle.png b/Client2020/content/textures/AnimationEditor/button_radio_innercircle.png new file mode 100644 index 0000000..b6a9efe Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_radio_innercircle.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_search.png b/Client2020/content/textures/AnimationEditor/button_search.png new file mode 100644 index 0000000..a08992b Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_search.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_zoom.png b/Client2020/content/textures/AnimationEditor/button_zoom.png new file mode 100644 index 0000000..966d410 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_zoom.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_zoom_default_left.png b/Client2020/content/textures/AnimationEditor/button_zoom_default_left.png new file mode 100644 index 0000000..e371819 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_zoom_default_left.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_zoom_default_left@2x.png b/Client2020/content/textures/AnimationEditor/button_zoom_default_left@2x.png new file mode 100644 index 0000000..45c7cc7 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_zoom_default_left@2x.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_zoom_default_right.png b/Client2020/content/textures/AnimationEditor/button_zoom_default_right.png new file mode 100644 index 0000000..e371819 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_zoom_default_right.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_zoom_default_right@2x.png b/Client2020/content/textures/AnimationEditor/button_zoom_default_right@2x.png new file mode 100644 index 0000000..45c7cc7 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_zoom_default_right@2x.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_zoom_hoverpressed_left.png b/Client2020/content/textures/AnimationEditor/button_zoom_hoverpressed_left.png new file mode 100644 index 0000000..4d19d86 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_zoom_hoverpressed_left.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_zoom_hoverpressed_left@2x.png b/Client2020/content/textures/AnimationEditor/button_zoom_hoverpressed_left@2x.png new file mode 100644 index 0000000..3b2f615 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_zoom_hoverpressed_left@2x.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_zoom_hoverpressed_right.png b/Client2020/content/textures/AnimationEditor/button_zoom_hoverpressed_right.png new file mode 100644 index 0000000..4d19d86 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_zoom_hoverpressed_right.png differ diff --git a/Client2020/content/textures/AnimationEditor/button_zoom_hoverpressed_right@2x.png b/Client2020/content/textures/AnimationEditor/button_zoom_hoverpressed_right@2x.png new file mode 100644 index 0000000..3b2f615 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/button_zoom_hoverpressed_right@2x.png differ diff --git a/Client2020/content/textures/AnimationEditor/eventMarker_border.png b/Client2020/content/textures/AnimationEditor/eventMarker_border.png new file mode 100644 index 0000000..6e05d59 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/eventMarker_border.png differ diff --git a/Client2020/content/textures/AnimationEditor/eventMarker_border_selected.png b/Client2020/content/textures/AnimationEditor/eventMarker_border_selected.png new file mode 100644 index 0000000..4771ef5 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/eventMarker_border_selected.png differ diff --git a/Client2020/content/textures/AnimationEditor/eventMarker_inner.png b/Client2020/content/textures/AnimationEditor/eventMarker_inner.png new file mode 100644 index 0000000..e387ed0 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/eventMarker_inner.png differ diff --git a/Client2020/content/textures/AnimationEditor/fbximportlogo.png b/Client2020/content/textures/AnimationEditor/fbximportlogo.png new file mode 100644 index 0000000..6acee80 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/fbximportlogo.png differ diff --git a/Client2020/content/textures/AnimationEditor/ic-checkbox-active.png b/Client2020/content/textures/AnimationEditor/ic-checkbox-active.png new file mode 100644 index 0000000..b897b38 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/ic-checkbox-active.png differ diff --git a/Client2020/content/textures/AnimationEditor/ic-checkbox-off.png b/Client2020/content/textures/AnimationEditor/ic-checkbox-off.png new file mode 100644 index 0000000..09fd802 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/ic-checkbox-off.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_add.png b/Client2020/content/textures/AnimationEditor/icon_add.png new file mode 100644 index 0000000..887ece1 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_add.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_checkmark.png b/Client2020/content/textures/AnimationEditor/icon_checkmark.png new file mode 100644 index 0000000..a61f1f2 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_checkmark.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_close.png b/Client2020/content/textures/AnimationEditor/icon_close.png new file mode 100644 index 0000000..04e03cf Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_close.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_dark_warning.png b/Client2020/content/textures/AnimationEditor/icon_dark_warning.png new file mode 100644 index 0000000..c8385d3 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_dark_warning.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_delete.png b/Client2020/content/textures/AnimationEditor/icon_delete.png new file mode 100644 index 0000000..f643c9b Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_delete.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_delete_disabled.png b/Client2020/content/textures/AnimationEditor/icon_delete_disabled.png new file mode 100644 index 0000000..8eac738 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_delete_disabled.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_error.png b/Client2020/content/textures/AnimationEditor/icon_error.png new file mode 100644 index 0000000..fe8e0de Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_error.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_hierarchy_end_white.png b/Client2020/content/textures/AnimationEditor/icon_hierarchy_end_white.png new file mode 100644 index 0000000..4b9fc99 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_hierarchy_end_white.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_keyIndicator.png b/Client2020/content/textures/AnimationEditor/icon_keyIndicator.png new file mode 100644 index 0000000..9d92ac4 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_keyIndicator.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_keyIndicator_selected.png b/Client2020/content/textures/AnimationEditor/icon_keyIndicator_selected.png new file mode 100644 index 0000000..ed16d3c Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_keyIndicator_selected.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_pin.png b/Client2020/content/textures/AnimationEditor/icon_pin.png new file mode 100644 index 0000000..523ed1e Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_pin.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_showmore.png b/Client2020/content/textures/AnimationEditor/icon_showmore.png new file mode 100644 index 0000000..0ecc4ad Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_showmore.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_warning.png b/Client2020/content/textures/AnimationEditor/icon_warning.png new file mode 100644 index 0000000..a563e29 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_warning.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_warning_ik.png b/Client2020/content/textures/AnimationEditor/icon_warning_ik.png new file mode 100644 index 0000000..67c98b2 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_warning_ik.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_whitetriangle_down.png b/Client2020/content/textures/AnimationEditor/icon_whitetriangle_down.png new file mode 100644 index 0000000..09c5db8 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_whitetriangle_down.png differ diff --git a/Client2020/content/textures/AnimationEditor/icon_whitetriangle_up.png b/Client2020/content/textures/AnimationEditor/icon_whitetriangle_up.png new file mode 100644 index 0000000..5bdc664 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/icon_whitetriangle_up.png differ diff --git a/Client2020/content/textures/AnimationEditor/image_keyframe_bounce_selected.png b/Client2020/content/textures/AnimationEditor/image_keyframe_bounce_selected.png new file mode 100644 index 0000000..6e8863f Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/image_keyframe_bounce_selected.png differ diff --git a/Client2020/content/textures/AnimationEditor/image_keyframe_bounce_unselected.png b/Client2020/content/textures/AnimationEditor/image_keyframe_bounce_unselected.png new file mode 100644 index 0000000..391c175 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/image_keyframe_bounce_unselected.png differ diff --git a/Client2020/content/textures/AnimationEditor/image_keyframe_constant_selected.png b/Client2020/content/textures/AnimationEditor/image_keyframe_constant_selected.png new file mode 100644 index 0000000..5d68420 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/image_keyframe_constant_selected.png differ diff --git a/Client2020/content/textures/AnimationEditor/image_keyframe_constant_unselected.png b/Client2020/content/textures/AnimationEditor/image_keyframe_constant_unselected.png new file mode 100644 index 0000000..1c976e6 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/image_keyframe_constant_unselected.png differ diff --git a/Client2020/content/textures/AnimationEditor/image_keyframe_cubic_selected.png b/Client2020/content/textures/AnimationEditor/image_keyframe_cubic_selected.png new file mode 100644 index 0000000..67a63a0 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/image_keyframe_cubic_selected.png differ diff --git a/Client2020/content/textures/AnimationEditor/image_keyframe_cubic_unselected.png b/Client2020/content/textures/AnimationEditor/image_keyframe_cubic_unselected.png new file mode 100644 index 0000000..2a9d7db Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/image_keyframe_cubic_unselected.png differ diff --git a/Client2020/content/textures/AnimationEditor/image_keyframe_elastic_selected.png b/Client2020/content/textures/AnimationEditor/image_keyframe_elastic_selected.png new file mode 100644 index 0000000..7216182 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/image_keyframe_elastic_selected.png differ diff --git a/Client2020/content/textures/AnimationEditor/image_keyframe_elastic_unselected.png b/Client2020/content/textures/AnimationEditor/image_keyframe_elastic_unselected.png new file mode 100644 index 0000000..e56ad88 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/image_keyframe_elastic_unselected.png differ diff --git a/Client2020/content/textures/AnimationEditor/image_keyframe_linear_selected.png b/Client2020/content/textures/AnimationEditor/image_keyframe_linear_selected.png new file mode 100644 index 0000000..3d4d969 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/image_keyframe_linear_selected.png differ diff --git a/Client2020/content/textures/AnimationEditor/image_keyframe_linear_unselected.png b/Client2020/content/textures/AnimationEditor/image_keyframe_linear_unselected.png new file mode 100644 index 0000000..3f3709c Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/image_keyframe_linear_unselected.png differ diff --git a/Client2020/content/textures/AnimationEditor/image_scrollbar_vertical_bot.png b/Client2020/content/textures/AnimationEditor/image_scrollbar_vertical_bot.png new file mode 100644 index 0000000..b419435 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/image_scrollbar_vertical_bot.png differ diff --git a/Client2020/content/textures/AnimationEditor/image_scrollbar_vertical_mid.png b/Client2020/content/textures/AnimationEditor/image_scrollbar_vertical_mid.png new file mode 100644 index 0000000..56558a3 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/image_scrollbar_vertical_mid.png differ diff --git a/Client2020/content/textures/AnimationEditor/image_scrollbar_vertical_top.png b/Client2020/content/textures/AnimationEditor/image_scrollbar_vertical_top.png new file mode 100644 index 0000000..e0437c5 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/image_scrollbar_vertical_top.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_dark_scalebar_arrows.png b/Client2020/content/textures/AnimationEditor/img_dark_scalebar_arrows.png new file mode 100644 index 0000000..a5b5527 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_dark_scalebar_arrows.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_dark_scalebar_bar.png b/Client2020/content/textures/AnimationEditor/img_dark_scalebar_bar.png new file mode 100644 index 0000000..b5627c7 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_dark_scalebar_bar.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_dark_scrubberhead.png b/Client2020/content/textures/AnimationEditor/img_dark_scrubberhead.png new file mode 100644 index 0000000..1d4abcb Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_dark_scrubberhead.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_dark_timetag_bg.png b/Client2020/content/textures/AnimationEditor/img_dark_timetag_bg.png new file mode 100644 index 0000000..f4b0ecb Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_dark_timetag_bg.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_eventGroupMarker_border.png b/Client2020/content/textures/AnimationEditor/img_eventGroupMarker_border.png new file mode 100644 index 0000000..c782465 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_eventGroupMarker_border.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_eventGroupMarker_border_selected.png b/Client2020/content/textures/AnimationEditor/img_eventGroupMarker_border_selected.png new file mode 100644 index 0000000..1bbc18b Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_eventGroupMarker_border_selected.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_eventGroupMarker_inner.png b/Client2020/content/textures/AnimationEditor/img_eventGroupMarker_inner.png new file mode 100644 index 0000000..c600b0c Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_eventGroupMarker_inner.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_eventMarker_border.png b/Client2020/content/textures/AnimationEditor/img_eventMarker_border.png new file mode 100644 index 0000000..af75451 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_eventMarker_border.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_eventMarker_border_selected.png b/Client2020/content/textures/AnimationEditor/img_eventMarker_border_selected.png new file mode 100644 index 0000000..ed30a94 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_eventMarker_border_selected.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_eventMarker_inner.png b/Client2020/content/textures/AnimationEditor/img_eventMarker_inner.png new file mode 100644 index 0000000..b20aa90 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_eventMarker_inner.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_eventMarker_min.png b/Client2020/content/textures/AnimationEditor/img_eventMarker_min.png new file mode 100644 index 0000000..da504be Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_eventMarker_min.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_forwardslash.png b/Client2020/content/textures/AnimationEditor/img_forwardslash.png new file mode 100644 index 0000000..ebe9092 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_forwardslash.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_key_border.png b/Client2020/content/textures/AnimationEditor/img_key_border.png new file mode 100644 index 0000000..bac1fe3 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_key_border.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_key_indicator_border.png b/Client2020/content/textures/AnimationEditor/img_key_indicator_border.png new file mode 100644 index 0000000..bb35719 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_key_indicator_border.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_key_indicator_inner.png b/Client2020/content/textures/AnimationEditor/img_key_indicator_inner.png new file mode 100644 index 0000000..10b29f5 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_key_indicator_inner.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_key_indicator_selected_border.png b/Client2020/content/textures/AnimationEditor/img_key_indicator_selected_border.png new file mode 100644 index 0000000..309841e Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_key_indicator_selected_border.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_key_indicator_selected_inner.png b/Client2020/content/textures/AnimationEditor/img_key_indicator_selected_inner.png new file mode 100644 index 0000000..10b29f5 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_key_indicator_selected_inner.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_key_inner.png b/Client2020/content/textures/AnimationEditor/img_key_inner.png new file mode 100644 index 0000000..c281f85 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_key_inner.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_key_selected_border.png b/Client2020/content/textures/AnimationEditor/img_key_selected_border.png new file mode 100644 index 0000000..84868ec Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_key_selected_border.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_key_selected_inner.png b/Client2020/content/textures/AnimationEditor/img_key_selected_inner.png new file mode 100644 index 0000000..03fd465 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_key_selected_inner.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_scalebar_arrows.png b/Client2020/content/textures/AnimationEditor/img_scalebar_arrows.png new file mode 100644 index 0000000..92c86ae Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_scalebar_arrows.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_scalebar_arrows_border.png b/Client2020/content/textures/AnimationEditor/img_scalebar_arrows_border.png new file mode 100644 index 0000000..33af34a Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_scalebar_arrows_border.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_scrubberhead.png b/Client2020/content/textures/AnimationEditor/img_scrubberhead.png new file mode 100644 index 0000000..8bed057 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_scrubberhead.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_timetag.png b/Client2020/content/textures/AnimationEditor/img_timetag.png new file mode 100644 index 0000000..99a0999 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_timetag.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_timetag_border.png b/Client2020/content/textures/AnimationEditor/img_timetag_border.png new file mode 100644 index 0000000..49a59e7 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_timetag_border.png differ diff --git a/Client2020/content/textures/AnimationEditor/img_triangle.png b/Client2020/content/textures/AnimationEditor/img_triangle.png new file mode 100644 index 0000000..1516c70 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/img_triangle.png differ diff --git a/Client2020/content/textures/AnimationEditor/menu_shadow_bottom.png b/Client2020/content/textures/AnimationEditor/menu_shadow_bottom.png new file mode 100644 index 0000000..64c5ca8 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/menu_shadow_bottom.png differ diff --git a/Client2020/content/textures/AnimationEditor/menu_shadow_side_left.png b/Client2020/content/textures/AnimationEditor/menu_shadow_side_left.png new file mode 100644 index 0000000..3e1e14b Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/menu_shadow_side_left.png differ diff --git a/Client2020/content/textures/AnimationEditor/menu_shadow_side_right.png b/Client2020/content/textures/AnimationEditor/menu_shadow_side_right.png new file mode 100644 index 0000000..dc3a41f Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/menu_shadow_side_right.png differ diff --git a/Client2020/content/textures/AnimationEditor/menu_shadow_top.png b/Client2020/content/textures/AnimationEditor/menu_shadow_top.png new file mode 100644 index 0000000..47003ca Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/menu_shadow_top.png differ diff --git a/Client2020/content/textures/AnimationEditor/rig_builder_32x32.png b/Client2020/content/textures/AnimationEditor/rig_builder_32x32.png new file mode 100644 index 0000000..336e4d4 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/rig_builder_32x32.png differ diff --git a/Client2020/content/textures/AnimationEditor/rigbuilder_blue.png b/Client2020/content/textures/AnimationEditor/rigbuilder_blue.png new file mode 100644 index 0000000..a0d7101 Binary files /dev/null and b/Client2020/content/textures/AnimationEditor/rigbuilder_blue.png differ diff --git a/Client2020/content/textures/ArrowCursor.png b/Client2020/content/textures/ArrowCursor.png new file mode 100644 index 0000000..694c26a Binary files /dev/null and b/Client2020/content/textures/ArrowCursor.png differ diff --git a/Client2020/content/textures/ArrowCursorDecalDrag.png b/Client2020/content/textures/ArrowCursorDecalDrag.png new file mode 100644 index 0000000..694c26a Binary files /dev/null and b/Client2020/content/textures/ArrowCursorDecalDrag.png differ diff --git a/Client2020/content/textures/ArrowFarCursor.png b/Client2020/content/textures/ArrowFarCursor.png new file mode 100644 index 0000000..daf5471 Binary files /dev/null and b/Client2020/content/textures/ArrowFarCursor.png differ diff --git a/Client2020/content/textures/AssetManager/explorer.png b/Client2020/content/textures/AssetManager/explorer.png new file mode 100644 index 0000000..6ecc0dc Binary files /dev/null and b/Client2020/content/textures/AssetManager/explorer.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/AvatarEditor.png b/Client2020/content/textures/AvatarEditorImages/AvatarEditor.png new file mode 100644 index 0000000..373a4bb Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/AvatarEditor.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/AvatarEditor_LightTheme.png b/Client2020/content/textures/AvatarEditorImages/AvatarEditor_LightTheme.png new file mode 100644 index 0000000..5982dce Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/AvatarEditor_LightTheme.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Catalog.png b/Client2020/content/textures/AvatarEditorImages/Catalog.png new file mode 100644 index 0000000..8b75001 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Catalog.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Catalog_LightTheme.png b/Client2020/content/textures/AvatarEditorImages/Catalog_LightTheme.png new file mode 100644 index 0000000..0405fe7 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Catalog_LightTheme.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/DarkPixel.png b/Client2020/content/textures/AvatarEditorImages/DarkPixel.png new file mode 100644 index 0000000..e562476 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/DarkPixel.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/LightPixel.png b/Client2020/content/textures/AvatarEditorImages/LightPixel.png new file mode 100644 index 0000000..e9a7e58 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/LightPixel.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Sheet.png b/Client2020/content/textures/AvatarEditorImages/Sheet.png new file mode 100644 index 0000000..efb94f4 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Sheet.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Sliders/body-type-slider-background.png b/Client2020/content/textures/AvatarEditorImages/Sliders/body-type-slider-background.png new file mode 100644 index 0000000..a9801aa Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Sliders/body-type-slider-background.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Sliders/body-type-slider-background@2x.png b/Client2020/content/textures/AvatarEditorImages/Sliders/body-type-slider-background@2x.png new file mode 100644 index 0000000..bc1e9d4 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Sliders/body-type-slider-background@2x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Sliders/body-type-slider-background@3x.png b/Client2020/content/textures/AvatarEditorImages/Sliders/body-type-slider-background@3x.png new file mode 100644 index 0000000..fd27267 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Sliders/body-type-slider-background@3x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty.png b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty.png new file mode 100644 index 0000000..cc1a2a5 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@2x.png b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@2x.png new file mode 100644 index 0000000..b9fd04b Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@2x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@3x.png b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@3x.png new file mode 100644 index 0000000..7bfe852 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@3x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill.png b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill.png new file mode 100644 index 0000000..bc50d10 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@2x.png b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@2x.png new file mode 100644 index 0000000..30c1c22 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@2x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@3x.png b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@3x.png new file mode 100644 index 0000000..5f4ab0f Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@3x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slider.png b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slider.png new file mode 100644 index 0000000..12022f4 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slider.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slider@2x.png b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slider@2x.png new file mode 100644 index 0000000..1ae093c Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slider@2x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slider@3x.png b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slider@3x.png new file mode 100644 index 0000000..6d6ef28 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Sliders/gr-slider@3x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Stretch/bar-empty-mid.png b/Client2020/content/textures/AvatarEditorImages/Stretch/bar-empty-mid.png new file mode 100644 index 0000000..c2c751e Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Stretch/bar-empty-mid.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@2x.png b/Client2020/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@2x.png new file mode 100644 index 0000000..32d55f8 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@2x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@3x.png b/Client2020/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@3x.png new file mode 100644 index 0000000..de31bc7 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@3x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Stretch/bar-full-mid.png b/Client2020/content/textures/AvatarEditorImages/Stretch/bar-full-mid.png new file mode 100644 index 0000000..5990100 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Stretch/bar-full-mid.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Stretch/bar-full-mid@2x.png b/Client2020/content/textures/AvatarEditorImages/Stretch/bar-full-mid@2x.png new file mode 100644 index 0000000..a4f9403 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Stretch/bar-full-mid@2x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Stretch/bar-full-mid@3x.png b/Client2020/content/textures/AvatarEditorImages/Stretch/bar-full-mid@3x.png new file mode 100644 index 0000000..eeb9e7e Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Stretch/bar-full-mid@3x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Stretch/gr-tail.png b/Client2020/content/textures/AvatarEditorImages/Stretch/gr-tail.png new file mode 100644 index 0000000..deb2988 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Stretch/gr-tail.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/Stretch/gr-tail@2x.png b/Client2020/content/textures/AvatarEditorImages/Stretch/gr-tail@2x.png new file mode 100644 index 0000000..1a88af0 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/Stretch/gr-tail@2x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/circle_blue.png b/Client2020/content/textures/AvatarEditorImages/circle_blue.png new file mode 100644 index 0000000..63ced88 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/circle_blue.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/circle_blue@2x.png b/Client2020/content/textures/AvatarEditorImages/circle_blue@2x.png new file mode 100644 index 0000000..3fb53a6 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/circle_blue@2x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/circle_blue@3x.png b/Client2020/content/textures/AvatarEditorImages/circle_blue@3x.png new file mode 100644 index 0000000..5cc69fa Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/circle_blue@3x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/circle_gray4.png b/Client2020/content/textures/AvatarEditorImages/circle_gray4.png new file mode 100644 index 0000000..5043d1d Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/circle_gray4.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/circle_gray4@2x.png b/Client2020/content/textures/AvatarEditorImages/circle_gray4@2x.png new file mode 100644 index 0000000..fba16ba Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/circle_gray4@2x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/circle_gray4@3x.png b/Client2020/content/textures/AvatarEditorImages/circle_gray4@3x.png new file mode 100644 index 0000000..29e91f3 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/circle_gray4@3x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/gr-selection-border.png b/Client2020/content/textures/AvatarEditorImages/gr-selection-border.png new file mode 100644 index 0000000..95d8647 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/gr-selection-border.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/gr-selection-border@2x.png b/Client2020/content/textures/AvatarEditorImages/gr-selection-border@2x.png new file mode 100644 index 0000000..1541ea9 Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/gr-selection-border@2x.png differ diff --git a/Client2020/content/textures/AvatarEditorImages/gr-selection-border@3x.png b/Client2020/content/textures/AvatarEditorImages/gr-selection-border@3x.png new file mode 100644 index 0000000..a7d560e Binary files /dev/null and b/Client2020/content/textures/AvatarEditorImages/gr-selection-border@3x.png differ diff --git a/Client2020/content/textures/AvatarImporter/button_avatarType.png b/Client2020/content/textures/AvatarImporter/button_avatarType.png new file mode 100644 index 0000000..88581b2 Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/button_avatarType.png differ diff --git a/Client2020/content/textures/AvatarImporter/button_avatarType_border.png b/Client2020/content/textures/AvatarImporter/button_avatarType_border.png new file mode 100644 index 0000000..6341f4a Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/button_avatarType_border.png differ diff --git a/Client2020/content/textures/AvatarImporter/button_close.png b/Client2020/content/textures/AvatarImporter/button_close.png new file mode 100644 index 0000000..bc0da39 Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/button_close.png differ diff --git a/Client2020/content/textures/AvatarImporter/fbximportlogo.png b/Client2020/content/textures/AvatarImporter/fbximportlogo.png new file mode 100644 index 0000000..6acee80 Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/fbximportlogo.png differ diff --git a/Client2020/content/textures/AvatarImporter/icon_AvatarImporter.png b/Client2020/content/textures/AvatarImporter/icon_AvatarImporter.png new file mode 100644 index 0000000..924f3c9 Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/icon_AvatarImporter.png differ diff --git a/Client2020/content/textures/AvatarImporter/icon_error.png b/Client2020/content/textures/AvatarImporter/icon_error.png new file mode 100644 index 0000000..db2872f Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/icon_error.png differ diff --git a/Client2020/content/textures/AvatarImporter/img_dark_R15.png b/Client2020/content/textures/AvatarImporter/img_dark_R15.png new file mode 100644 index 0000000..9939615 Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/img_dark_R15.png differ diff --git a/Client2020/content/textures/AvatarImporter/img_dark_Rthro.png b/Client2020/content/textures/AvatarImporter/img_dark_Rthro.png new file mode 100644 index 0000000..227c53d Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/img_dark_Rthro.png differ diff --git a/Client2020/content/textures/AvatarImporter/img_dark_RthroNarrow.png b/Client2020/content/textures/AvatarImporter/img_dark_RthroNarrow.png new file mode 100644 index 0000000..f8af124 Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/img_dark_RthroNarrow.png differ diff --git a/Client2020/content/textures/AvatarImporter/img_dark_custom.png b/Client2020/content/textures/AvatarImporter/img_dark_custom.png new file mode 100644 index 0000000..558eccb Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/img_dark_custom.png differ diff --git a/Client2020/content/textures/AvatarImporter/img_light_R15.png b/Client2020/content/textures/AvatarImporter/img_light_R15.png new file mode 100644 index 0000000..b6b0657 Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/img_light_R15.png differ diff --git a/Client2020/content/textures/AvatarImporter/img_light_Rthro.png b/Client2020/content/textures/AvatarImporter/img_light_Rthro.png new file mode 100644 index 0000000..5ca69d2 Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/img_light_Rthro.png differ diff --git a/Client2020/content/textures/AvatarImporter/img_light_RthroNarrow.png b/Client2020/content/textures/AvatarImporter/img_light_RthroNarrow.png new file mode 100644 index 0000000..3aa1643 Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/img_light_RthroNarrow.png differ diff --git a/Client2020/content/textures/AvatarImporter/img_light_custom.png b/Client2020/content/textures/AvatarImporter/img_light_custom.png new file mode 100644 index 0000000..d43765e Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/img_light_custom.png differ diff --git a/Client2020/content/textures/AvatarImporter/img_window_BG.png b/Client2020/content/textures/AvatarImporter/img_window_BG.png new file mode 100644 index 0000000..afae8bb Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/img_window_BG.png differ diff --git a/Client2020/content/textures/AvatarImporter/img_window_header.png b/Client2020/content/textures/AvatarImporter/img_window_header.png new file mode 100644 index 0000000..eda1219 Binary files /dev/null and b/Client2020/content/textures/AvatarImporter/img_window_header.png differ diff --git a/Client2020/content/textures/Blank.png b/Client2020/content/textures/Blank.png new file mode 100644 index 0000000..3add73f Binary files /dev/null and b/Client2020/content/textures/Blank.png differ diff --git a/Client2020/content/textures/ClassImages.PNG b/Client2020/content/textures/ClassImages.PNG new file mode 100644 index 0000000..42ee941 Binary files /dev/null and b/Client2020/content/textures/ClassImages.PNG differ diff --git a/Client2020/content/textures/CollisionGroupsEditor/ToolbarIcon.png b/Client2020/content/textures/CollisionGroupsEditor/ToolbarIcon.png new file mode 100644 index 0000000..08542c9 Binary files /dev/null and b/Client2020/content/textures/CollisionGroupsEditor/ToolbarIcon.png differ diff --git a/Client2020/content/textures/CollisionGroupsEditor/assign-hover.png b/Client2020/content/textures/CollisionGroupsEditor/assign-hover.png new file mode 100644 index 0000000..91f7db7 Binary files /dev/null and b/Client2020/content/textures/CollisionGroupsEditor/assign-hover.png differ diff --git a/Client2020/content/textures/CollisionGroupsEditor/assign.png b/Client2020/content/textures/CollisionGroupsEditor/assign.png new file mode 100644 index 0000000..fc915f6 Binary files /dev/null and b/Client2020/content/textures/CollisionGroupsEditor/assign.png differ diff --git a/Client2020/content/textures/CollisionGroupsEditor/checked-bluebg.png b/Client2020/content/textures/CollisionGroupsEditor/checked-bluebg.png new file mode 100644 index 0000000..3dddcfb Binary files /dev/null and b/Client2020/content/textures/CollisionGroupsEditor/checked-bluebg.png differ diff --git a/Client2020/content/textures/CollisionGroupsEditor/checked-whitebg.png b/Client2020/content/textures/CollisionGroupsEditor/checked-whitebg.png new file mode 100644 index 0000000..78bdb73 Binary files /dev/null and b/Client2020/content/textures/CollisionGroupsEditor/checked-whitebg.png differ diff --git a/Client2020/content/textures/CollisionGroupsEditor/delete-hover.png b/Client2020/content/textures/CollisionGroupsEditor/delete-hover.png new file mode 100644 index 0000000..a0b6401 Binary files /dev/null and b/Client2020/content/textures/CollisionGroupsEditor/delete-hover.png differ diff --git a/Client2020/content/textures/CollisionGroupsEditor/delete.png b/Client2020/content/textures/CollisionGroupsEditor/delete.png new file mode 100644 index 0000000..9437121 Binary files /dev/null and b/Client2020/content/textures/CollisionGroupsEditor/delete.png differ diff --git a/Client2020/content/textures/CollisionGroupsEditor/manage-hover.png b/Client2020/content/textures/CollisionGroupsEditor/manage-hover.png new file mode 100644 index 0000000..273f99e Binary files /dev/null and b/Client2020/content/textures/CollisionGroupsEditor/manage-hover.png differ diff --git a/Client2020/content/textures/CollisionGroupsEditor/manage.png b/Client2020/content/textures/CollisionGroupsEditor/manage.png new file mode 100644 index 0000000..81c6517 Binary files /dev/null and b/Client2020/content/textures/CollisionGroupsEditor/manage.png differ diff --git a/Client2020/content/textures/CollisionGroupsEditor/rename-hover.png b/Client2020/content/textures/CollisionGroupsEditor/rename-hover.png new file mode 100644 index 0000000..df47a4b Binary files /dev/null and b/Client2020/content/textures/CollisionGroupsEditor/rename-hover.png differ diff --git a/Client2020/content/textures/CollisionGroupsEditor/rename.png b/Client2020/content/textures/CollisionGroupsEditor/rename.png new file mode 100644 index 0000000..bd26275 Binary files /dev/null and b/Client2020/content/textures/CollisionGroupsEditor/rename.png differ diff --git a/Client2020/content/textures/CollisionGroupsEditor/unchecked.png b/Client2020/content/textures/CollisionGroupsEditor/unchecked.png new file mode 100644 index 0000000..dd8aa02 Binary files /dev/null and b/Client2020/content/textures/CollisionGroupsEditor/unchecked.png differ diff --git a/Client2020/content/textures/ConstraintCursor.png b/Client2020/content/textures/ConstraintCursor.png new file mode 100644 index 0000000..a89d0ad Binary files /dev/null and b/Client2020/content/textures/ConstraintCursor.png differ diff --git a/Client2020/content/textures/Cursors/CrossMouseIcon.png b/Client2020/content/textures/Cursors/CrossMouseIcon.png new file mode 100644 index 0000000..a18529c Binary files /dev/null and b/Client2020/content/textures/Cursors/CrossMouseIcon.png differ diff --git a/Client2020/content/textures/Cursors/Gamepad/Pointer.png b/Client2020/content/textures/Cursors/Gamepad/Pointer.png new file mode 100644 index 0000000..5c406dc Binary files /dev/null and b/Client2020/content/textures/Cursors/Gamepad/Pointer.png differ diff --git a/Client2020/content/textures/Cursors/Gamepad/Pointer@2x.png b/Client2020/content/textures/Cursors/Gamepad/Pointer@2x.png new file mode 100644 index 0000000..e896fbb Binary files /dev/null and b/Client2020/content/textures/Cursors/Gamepad/Pointer@2x.png differ diff --git a/Client2020/content/textures/Cursors/Gamepad/PointerOver.png b/Client2020/content/textures/Cursors/Gamepad/PointerOver.png new file mode 100644 index 0000000..7663ea1 Binary files /dev/null and b/Client2020/content/textures/Cursors/Gamepad/PointerOver.png differ diff --git a/Client2020/content/textures/Cursors/Gamepad/PointerOver@2x.png b/Client2020/content/textures/Cursors/Gamepad/PointerOver@2x.png new file mode 100644 index 0000000..e0dbd85 Binary files /dev/null and b/Client2020/content/textures/Cursors/Gamepad/PointerOver@2x.png differ diff --git a/Client2020/content/textures/Cursors/mouseIconCameraTrack.png b/Client2020/content/textures/Cursors/mouseIconCameraTrack.png new file mode 100644 index 0000000..7056866 Binary files /dev/null and b/Client2020/content/textures/Cursors/mouseIconCameraTrack.png differ diff --git a/Client2020/content/textures/DarkThemeLoadingCircle.png b/Client2020/content/textures/DarkThemeLoadingCircle.png new file mode 100644 index 0000000..00b086b Binary files /dev/null and b/Client2020/content/textures/DarkThemeLoadingCircle.png differ diff --git a/Client2020/content/textures/DevConsole/Arrow.png b/Client2020/content/textures/DevConsole/Arrow.png new file mode 100644 index 0000000..f55bbff Binary files /dev/null and b/Client2020/content/textures/DevConsole/Arrow.png differ diff --git a/Client2020/content/textures/DevConsole/Clear.png b/Client2020/content/textures/DevConsole/Clear.png new file mode 100644 index 0000000..9f1bf3a Binary files /dev/null and b/Client2020/content/textures/DevConsole/Clear.png differ diff --git a/Client2020/content/textures/DevConsole/Close.png b/Client2020/content/textures/DevConsole/Close.png new file mode 100644 index 0000000..31d348a Binary files /dev/null and b/Client2020/content/textures/DevConsole/Close.png differ diff --git a/Client2020/content/textures/DevConsole/Error.png b/Client2020/content/textures/DevConsole/Error.png new file mode 100644 index 0000000..470b6e5 Binary files /dev/null and b/Client2020/content/textures/DevConsole/Error.png differ diff --git a/Client2020/content/textures/DevConsole/Filter-filled.png b/Client2020/content/textures/DevConsole/Filter-filled.png new file mode 100644 index 0000000..1f43780 Binary files /dev/null and b/Client2020/content/textures/DevConsole/Filter-filled.png differ diff --git a/Client2020/content/textures/DevConsole/Filter-stroke.png b/Client2020/content/textures/DevConsole/Filter-stroke.png new file mode 100644 index 0000000..6c00c0e Binary files /dev/null and b/Client2020/content/textures/DevConsole/Filter-stroke.png differ diff --git a/Client2020/content/textures/DevConsole/Info.png b/Client2020/content/textures/DevConsole/Info.png new file mode 100644 index 0000000..d6a13bb Binary files /dev/null and b/Client2020/content/textures/DevConsole/Info.png differ diff --git a/Client2020/content/textures/DevConsole/Maximize.png b/Client2020/content/textures/DevConsole/Maximize.png new file mode 100644 index 0000000..8016be6 Binary files /dev/null and b/Client2020/content/textures/DevConsole/Maximize.png differ diff --git a/Client2020/content/textures/DevConsole/Minimize.png b/Client2020/content/textures/DevConsole/Minimize.png new file mode 100644 index 0000000..0065af8 Binary files /dev/null and b/Client2020/content/textures/DevConsole/Minimize.png differ diff --git a/Client2020/content/textures/DevConsole/Search.png b/Client2020/content/textures/DevConsole/Search.png new file mode 100644 index 0000000..b9c95cb Binary files /dev/null and b/Client2020/content/textures/DevConsole/Search.png differ diff --git a/Client2020/content/textures/DevConsole/Sort.png b/Client2020/content/textures/DevConsole/Sort.png new file mode 100644 index 0000000..152394d Binary files /dev/null and b/Client2020/content/textures/DevConsole/Sort.png differ diff --git a/Client2020/content/textures/DevConsole/Warning.png b/Client2020/content/textures/DevConsole/Warning.png new file mode 100644 index 0000000..d614a50 Binary files /dev/null and b/Client2020/content/textures/DevConsole/Warning.png differ diff --git a/Client2020/content/textures/DeveloperFramework/slider_bg.png b/Client2020/content/textures/DeveloperFramework/slider_bg.png new file mode 100644 index 0000000..ff6ff8e Binary files /dev/null and b/Client2020/content/textures/DeveloperFramework/slider_bg.png differ diff --git a/Client2020/content/textures/DeveloperFramework/slider_knob.png b/Client2020/content/textures/DeveloperFramework/slider_knob.png new file mode 100644 index 0000000..1098b7e Binary files /dev/null and b/Client2020/content/textures/DeveloperFramework/slider_knob.png differ diff --git a/Client2020/content/textures/DeveloperFramework/slider_knob_light.png b/Client2020/content/textures/DeveloperFramework/slider_knob_light.png new file mode 100644 index 0000000..94db6a2 Binary files /dev/null and b/Client2020/content/textures/DeveloperFramework/slider_knob_light.png differ diff --git a/Client2020/content/textures/DeveloperFramework/slider_knob_ouline.png b/Client2020/content/textures/DeveloperFramework/slider_knob_ouline.png new file mode 100644 index 0000000..9cced45 Binary files /dev/null and b/Client2020/content/textures/DeveloperFramework/slider_knob_ouline.png differ diff --git a/Client2020/content/textures/DraftsWidget/deletedSource.png b/Client2020/content/textures/DraftsWidget/deletedSource.png new file mode 100644 index 0000000..55e3e8b Binary files /dev/null and b/Client2020/content/textures/DraftsWidget/deletedSource.png differ diff --git a/Client2020/content/textures/DraftsWidget/newSource.png b/Client2020/content/textures/DraftsWidget/newSource.png new file mode 100644 index 0000000..a754f40 Binary files /dev/null and b/Client2020/content/textures/DraftsWidget/newSource.png differ diff --git a/Client2020/content/textures/FillCursor.png b/Client2020/content/textures/FillCursor.png new file mode 100644 index 0000000..752cebc Binary files /dev/null and b/Client2020/content/textures/FillCursor.png differ diff --git a/Client2020/content/textures/FlatCursor.png b/Client2020/content/textures/FlatCursor.png new file mode 100644 index 0000000..7d6e845 Binary files /dev/null and b/Client2020/content/textures/FlatCursor.png differ diff --git a/Client2020/content/textures/GameSettings/Arrow.png b/Client2020/content/textures/GameSettings/Arrow.png new file mode 100644 index 0000000..152394d Binary files /dev/null and b/Client2020/content/textures/GameSettings/Arrow.png differ diff --git a/Client2020/content/textures/GameSettings/ArrowLeft.png b/Client2020/content/textures/GameSettings/ArrowLeft.png new file mode 100644 index 0000000..dc1ffab Binary files /dev/null and b/Client2020/content/textures/GameSettings/ArrowLeft.png differ diff --git a/Client2020/content/textures/GameSettings/CenterPlus.png b/Client2020/content/textures/GameSettings/CenterPlus.png new file mode 100644 index 0000000..05af26f Binary files /dev/null and b/Client2020/content/textures/GameSettings/CenterPlus.png differ diff --git a/Client2020/content/textures/GameSettings/CheckedBoxDark.png b/Client2020/content/textures/GameSettings/CheckedBoxDark.png new file mode 100644 index 0000000..96f4a42 Binary files /dev/null and b/Client2020/content/textures/GameSettings/CheckedBoxDark.png differ diff --git a/Client2020/content/textures/GameSettings/CheckedBoxLight.png b/Client2020/content/textures/GameSettings/CheckedBoxLight.png new file mode 100644 index 0000000..ad084c4 Binary files /dev/null and b/Client2020/content/textures/GameSettings/CheckedBoxLight.png differ diff --git a/Client2020/content/textures/GameSettings/DottedBorder.png b/Client2020/content/textures/GameSettings/DottedBorder.png new file mode 100644 index 0000000..18ee0e3 Binary files /dev/null and b/Client2020/content/textures/GameSettings/DottedBorder.png differ diff --git a/Client2020/content/textures/GameSettings/DottedBorder_Square.png b/Client2020/content/textures/GameSettings/DottedBorder_Square.png new file mode 100644 index 0000000..97f5507 Binary files /dev/null and b/Client2020/content/textures/GameSettings/DottedBorder_Square.png differ diff --git a/Client2020/content/textures/GameSettings/Error.png b/Client2020/content/textures/GameSettings/Error.png new file mode 100644 index 0000000..701f9a7 Binary files /dev/null and b/Client2020/content/textures/GameSettings/Error.png differ diff --git a/Client2020/content/textures/GameSettings/ErrorIcon.png b/Client2020/content/textures/GameSettings/ErrorIcon.png new file mode 100644 index 0000000..e15c23e Binary files /dev/null and b/Client2020/content/textures/GameSettings/ErrorIcon.png differ diff --git a/Client2020/content/textures/GameSettings/Gradient-Border.png b/Client2020/content/textures/GameSettings/Gradient-Border.png new file mode 100644 index 0000000..7a5cca4 Binary files /dev/null and b/Client2020/content/textures/GameSettings/Gradient-Border.png differ diff --git a/Client2020/content/textures/GameSettings/ModeratedAsset.jpg b/Client2020/content/textures/GameSettings/ModeratedAsset.jpg new file mode 100644 index 0000000..40d646e Binary files /dev/null and b/Client2020/content/textures/GameSettings/ModeratedAsset.jpg differ diff --git a/Client2020/content/textures/GameSettings/RadioButton.png b/Client2020/content/textures/GameSettings/RadioButton.png new file mode 100644 index 0000000..0e6557d Binary files /dev/null and b/Client2020/content/textures/GameSettings/RadioButton.png differ diff --git a/Client2020/content/textures/GameSettings/RoundArrowButton.png b/Client2020/content/textures/GameSettings/RoundArrowButton.png new file mode 100644 index 0000000..fed6fa1 Binary files /dev/null and b/Client2020/content/textures/GameSettings/RoundArrowButton.png differ diff --git a/Client2020/content/textures/GameSettings/ScrollBarBottom.png b/Client2020/content/textures/GameSettings/ScrollBarBottom.png new file mode 100644 index 0000000..852c120 Binary files /dev/null and b/Client2020/content/textures/GameSettings/ScrollBarBottom.png differ diff --git a/Client2020/content/textures/GameSettings/ScrollBarBottom_Wide.png b/Client2020/content/textures/GameSettings/ScrollBarBottom_Wide.png new file mode 100644 index 0000000..dc04cd1 Binary files /dev/null and b/Client2020/content/textures/GameSettings/ScrollBarBottom_Wide.png differ diff --git a/Client2020/content/textures/GameSettings/ScrollBarMiddle.png b/Client2020/content/textures/GameSettings/ScrollBarMiddle.png new file mode 100644 index 0000000..c244b9e Binary files /dev/null and b/Client2020/content/textures/GameSettings/ScrollBarMiddle.png differ diff --git a/Client2020/content/textures/GameSettings/ScrollBarMiddle_Wide.png b/Client2020/content/textures/GameSettings/ScrollBarMiddle_Wide.png new file mode 100644 index 0000000..12b12e3 Binary files /dev/null and b/Client2020/content/textures/GameSettings/ScrollBarMiddle_Wide.png differ diff --git a/Client2020/content/textures/GameSettings/ScrollBarTop.png b/Client2020/content/textures/GameSettings/ScrollBarTop.png new file mode 100644 index 0000000..c0dadb9 Binary files /dev/null and b/Client2020/content/textures/GameSettings/ScrollBarTop.png differ diff --git a/Client2020/content/textures/GameSettings/ScrollBarTop_Wide.png b/Client2020/content/textures/GameSettings/ScrollBarTop_Wide.png new file mode 100644 index 0000000..210ed76 Binary files /dev/null and b/Client2020/content/textures/GameSettings/ScrollBarTop_Wide.png differ diff --git a/Client2020/content/textures/GameSettings/ToolbarIcon.png b/Client2020/content/textures/GameSettings/ToolbarIcon.png new file mode 100644 index 0000000..be1bedf Binary files /dev/null and b/Client2020/content/textures/GameSettings/ToolbarIcon.png differ diff --git a/Client2020/content/textures/GameSettings/UncheckedBox.png b/Client2020/content/textures/GameSettings/UncheckedBox.png new file mode 100644 index 0000000..60fab3d Binary files /dev/null and b/Client2020/content/textures/GameSettings/UncheckedBox.png differ diff --git a/Client2020/content/textures/GameSettings/Warning.png b/Client2020/content/textures/GameSettings/Warning.png new file mode 100644 index 0000000..6e5f9e6 Binary files /dev/null and b/Client2020/content/textures/GameSettings/Warning.png differ diff --git a/Client2020/content/textures/GameSettings/add.png b/Client2020/content/textures/GameSettings/add.png new file mode 100644 index 0000000..4e5eab4 Binary files /dev/null and b/Client2020/content/textures/GameSettings/add.png differ diff --git a/Client2020/content/textures/GameSettings/delete.PNG b/Client2020/content/textures/GameSettings/delete.PNG new file mode 100644 index 0000000..a2cde20 Binary files /dev/null and b/Client2020/content/textures/GameSettings/delete.PNG differ diff --git a/Client2020/content/textures/GameSettings/edit.png b/Client2020/content/textures/GameSettings/edit.png new file mode 100644 index 0000000..2a6fafd Binary files /dev/null and b/Client2020/content/textures/GameSettings/edit.png differ diff --git a/Client2020/content/textures/GameSettings/friendsIcon.png b/Client2020/content/textures/GameSettings/friendsIcon.png new file mode 100644 index 0000000..537c3a7 Binary files /dev/null and b/Client2020/content/textures/GameSettings/friendsIcon.png differ diff --git a/Client2020/content/textures/GameSettings/placeholder.png b/Client2020/content/textures/GameSettings/placeholder.png new file mode 100644 index 0000000..a2c2aaa Binary files /dev/null and b/Client2020/content/textures/GameSettings/placeholder.png differ diff --git a/Client2020/content/textures/GameSettings/search.png b/Client2020/content/textures/GameSettings/search.png new file mode 100644 index 0000000..4c606f7 Binary files /dev/null and b/Client2020/content/textures/GameSettings/search.png differ diff --git a/Client2020/content/textures/GameSettings/zoom.PNG b/Client2020/content/textures/GameSettings/zoom.PNG new file mode 100644 index 0000000..999a1b4 Binary files /dev/null and b/Client2020/content/textures/GameSettings/zoom.PNG differ diff --git a/Client2020/content/textures/GlueCursor.png b/Client2020/content/textures/GlueCursor.png new file mode 100644 index 0000000..fdba03c Binary files /dev/null and b/Client2020/content/textures/GlueCursor.png differ diff --git a/Client2020/content/textures/HingeCursor.png b/Client2020/content/textures/HingeCursor.png new file mode 100644 index 0000000..99700b1 Binary files /dev/null and b/Client2020/content/textures/HingeCursor.png differ diff --git a/Client2020/content/textures/Icon_Stream_Off.png b/Client2020/content/textures/Icon_Stream_Off.png new file mode 100644 index 0000000..bbda9be Binary files /dev/null and b/Client2020/content/textures/Icon_Stream_Off.png differ diff --git a/Client2020/content/textures/Icon_Stream_Off@2x.png b/Client2020/content/textures/Icon_Stream_Off@2x.png new file mode 100644 index 0000000..979ef9c Binary files /dev/null and b/Client2020/content/textures/Icon_Stream_Off@2x.png differ diff --git a/Client2020/content/textures/Icon_Stream_Off@3x.png b/Client2020/content/textures/Icon_Stream_Off@3x.png new file mode 100644 index 0000000..e365e43 Binary files /dev/null and b/Client2020/content/textures/Icon_Stream_Off@3x.png differ diff --git a/Client2020/content/textures/LightThemeLoadingCircle.png b/Client2020/content/textures/LightThemeLoadingCircle.png new file mode 100644 index 0000000..5ddc825 Binary files /dev/null and b/Client2020/content/textures/LightThemeLoadingCircle.png differ diff --git a/Client2020/content/textures/LockCursor.png b/Client2020/content/textures/LockCursor.png new file mode 100644 index 0000000..4520327 Binary files /dev/null and b/Client2020/content/textures/LockCursor.png differ diff --git a/Client2020/content/textures/MaterialCursor.png b/Client2020/content/textures/MaterialCursor.png new file mode 100644 index 0000000..f43fa99 Binary files /dev/null and b/Client2020/content/textures/MaterialCursor.png differ diff --git a/Client2020/content/textures/MorpherEditor/mainButtonIcon.png b/Client2020/content/textures/MorpherEditor/mainButtonIcon.png new file mode 100644 index 0000000..3b2f615 Binary files /dev/null and b/Client2020/content/textures/MorpherEditor/mainButtonIcon.png differ diff --git a/Client2020/content/textures/MotorCursor.png b/Client2020/content/textures/MotorCursor.png new file mode 100644 index 0000000..64efe3e Binary files /dev/null and b/Client2020/content/textures/MotorCursor.png differ diff --git a/Client2020/content/textures/MouseLockedCursor.png b/Client2020/content/textures/MouseLockedCursor.png new file mode 100644 index 0000000..2ab5527 Binary files /dev/null and b/Client2020/content/textures/MouseLockedCursor.png differ diff --git a/Client2020/content/textures/PluginManagement/allowed.png b/Client2020/content/textures/PluginManagement/allowed.png new file mode 100644 index 0000000..d59721f Binary files /dev/null and b/Client2020/content/textures/PluginManagement/allowed.png differ diff --git a/Client2020/content/textures/PluginManagement/back.png b/Client2020/content/textures/PluginManagement/back.png new file mode 100644 index 0000000..ef27a4d Binary files /dev/null and b/Client2020/content/textures/PluginManagement/back.png differ diff --git a/Client2020/content/textures/PluginManagement/checked_dark.png b/Client2020/content/textures/PluginManagement/checked_dark.png new file mode 100644 index 0000000..c91d830 Binary files /dev/null and b/Client2020/content/textures/PluginManagement/checked_dark.png differ diff --git a/Client2020/content/textures/PluginManagement/checked_light.png b/Client2020/content/textures/PluginManagement/checked_light.png new file mode 100644 index 0000000..92f6467 Binary files /dev/null and b/Client2020/content/textures/PluginManagement/checked_light.png differ diff --git a/Client2020/content/textures/PluginManagement/declined.png b/Client2020/content/textures/PluginManagement/declined.png new file mode 100644 index 0000000..2515f6e Binary files /dev/null and b/Client2020/content/textures/PluginManagement/declined.png differ diff --git a/Client2020/content/textures/PluginManagement/edit.png b/Client2020/content/textures/PluginManagement/edit.png new file mode 100644 index 0000000..afe2243 Binary files /dev/null and b/Client2020/content/textures/PluginManagement/edit.png differ diff --git a/Client2020/content/textures/PluginManagement/unchecked.png b/Client2020/content/textures/PluginManagement/unchecked.png new file mode 100644 index 0000000..0514209 Binary files /dev/null and b/Client2020/content/textures/PluginManagement/unchecked.png differ diff --git a/Client2020/content/textures/PublishPlaceAs/TransparentWhiteImagePlaceholder.png b/Client2020/content/textures/PublishPlaceAs/TransparentWhiteImagePlaceholder.png new file mode 100644 index 0000000..f22c728 Binary files /dev/null and b/Client2020/content/textures/PublishPlaceAs/TransparentWhiteImagePlaceholder.png differ diff --git a/Client2020/content/textures/PublishPlaceAs/WhiteNew.png b/Client2020/content/textures/PublishPlaceAs/WhiteNew.png new file mode 100644 index 0000000..1ac32e9 Binary files /dev/null and b/Client2020/content/textures/PublishPlaceAs/WhiteNew.png differ diff --git a/Client2020/content/textures/PublishPlaceAs/common_checkmarkCircle.png b/Client2020/content/textures/PublishPlaceAs/common_checkmarkCircle.png new file mode 100644 index 0000000..dbcf169 Binary files /dev/null and b/Client2020/content/textures/PublishPlaceAs/common_checkmarkCircle.png differ diff --git a/Client2020/content/textures/PublishPlaceAs/navigation_pushBack.png b/Client2020/content/textures/PublishPlaceAs/navigation_pushBack.png new file mode 100644 index 0000000..45a50d2 Binary files /dev/null and b/Client2020/content/textures/PublishPlaceAs/navigation_pushBack.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/button_default.png b/Client2020/content/textures/RoactStudioWidgets/button_default.png new file mode 100644 index 0000000..28eb7a7 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/button_default.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/button_hover.png b/Client2020/content/textures/RoactStudioWidgets/button_hover.png new file mode 100644 index 0000000..5184f6d Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/button_hover.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/button_pressed.png b/Client2020/content/textures/RoactStudioWidgets/button_pressed.png new file mode 100644 index 0000000..5484114 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/button_pressed.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/button_radiobutton_chosen.png b/Client2020/content/textures/RoactStudioWidgets/button_radiobutton_chosen.png new file mode 100644 index 0000000..2b1bbec Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/button_radiobutton_chosen.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/button_radiobutton_default.png b/Client2020/content/textures/RoactStudioWidgets/button_radiobutton_default.png new file mode 100644 index 0000000..fbb0cff Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/button_radiobutton_default.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/checkbox_square.png b/Client2020/content/textures/RoactStudioWidgets/checkbox_square.png new file mode 100644 index 0000000..af883a9 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/checkbox_square.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/icon_tick.png b/Client2020/content/textures/RoactStudioWidgets/icon_tick.png new file mode 100644 index 0000000..997f788 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/icon_tick.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/slider_bar_background_dark.png b/Client2020/content/textures/RoactStudioWidgets/slider_bar_background_dark.png new file mode 100644 index 0000000..d5d0335 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/slider_bar_background_dark.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/slider_bar_background_light.png b/Client2020/content/textures/RoactStudioWidgets/slider_bar_background_light.png new file mode 100644 index 0000000..bf10bdb Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/slider_bar_background_light.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/slider_bar_dark.png b/Client2020/content/textures/RoactStudioWidgets/slider_bar_dark.png new file mode 100644 index 0000000..3e1c713 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/slider_bar_dark.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/slider_bar_light.png b/Client2020/content/textures/RoactStudioWidgets/slider_bar_light.png new file mode 100644 index 0000000..bc50d10 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/slider_bar_light.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/slider_caret.png b/Client2020/content/textures/RoactStudioWidgets/slider_caret.png new file mode 100644 index 0000000..5f5e4b1 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/slider_caret.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/slider_caret_disabled.png b/Client2020/content/textures/RoactStudioWidgets/slider_caret_disabled.png new file mode 100644 index 0000000..5f5e4b1 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/slider_caret_disabled.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/slider_handle_dark.png b/Client2020/content/textures/RoactStudioWidgets/slider_handle_dark.png new file mode 100644 index 0000000..00dba99 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/slider_handle_dark.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/slider_handle_light.png b/Client2020/content/textures/RoactStudioWidgets/slider_handle_light.png new file mode 100644 index 0000000..49a1913 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/slider_handle_light.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/toggle_disable_dark.png b/Client2020/content/textures/RoactStudioWidgets/toggle_disable_dark.png new file mode 100644 index 0000000..ba3f314 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/toggle_disable_dark.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/toggle_disable_light.png b/Client2020/content/textures/RoactStudioWidgets/toggle_disable_light.png new file mode 100644 index 0000000..8b68fa7 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/toggle_disable_light.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/toggle_off _dark.png b/Client2020/content/textures/RoactStudioWidgets/toggle_off _dark.png new file mode 100644 index 0000000..88d1e15 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/toggle_off _dark.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/toggle_off_dark.png b/Client2020/content/textures/RoactStudioWidgets/toggle_off_dark.png new file mode 100644 index 0000000..88d1e15 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/toggle_off_dark.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/toggle_off_light.png b/Client2020/content/textures/RoactStudioWidgets/toggle_off_light.png new file mode 100644 index 0000000..a359e38 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/toggle_off_light.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/toggle_on_dark.png b/Client2020/content/textures/RoactStudioWidgets/toggle_on_dark.png new file mode 100644 index 0000000..9ac71b1 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/toggle_on_dark.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/toggle_on_disable_dark.png b/Client2020/content/textures/RoactStudioWidgets/toggle_on_disable_dark.png new file mode 100644 index 0000000..03d4e80 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/toggle_on_disable_dark.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/toggle_on_disable_light.png b/Client2020/content/textures/RoactStudioWidgets/toggle_on_disable_light.png new file mode 100644 index 0000000..e214d74 Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/toggle_on_disable_light.png differ diff --git a/Client2020/content/textures/RoactStudioWidgets/toggle_on_light.png b/Client2020/content/textures/RoactStudioWidgets/toggle_on_light.png new file mode 100644 index 0000000..1a1087e Binary files /dev/null and b/Client2020/content/textures/RoactStudioWidgets/toggle_on_light.png differ diff --git a/Client2020/content/textures/StudioConvertToPackagePlugin/placeholder.png b/Client2020/content/textures/StudioConvertToPackagePlugin/placeholder.png new file mode 100644 index 0000000..ec8f91e Binary files /dev/null and b/Client2020/content/textures/StudioConvertToPackagePlugin/placeholder.png differ diff --git a/Client2020/content/textures/StudioPlayerEmulator/player_emulator_32.png b/Client2020/content/textures/StudioPlayerEmulator/player_emulator_32.png new file mode 100644 index 0000000..fb386ef Binary files /dev/null and b/Client2020/content/textures/StudioPlayerEmulator/player_emulator_32.png differ diff --git a/Client2020/content/textures/StudioSharedUI/arrowSpritesheet.png b/Client2020/content/textures/StudioSharedUI/arrowSpritesheet.png new file mode 100644 index 0000000..0750c8c Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/arrowSpritesheet.png differ diff --git a/Client2020/content/textures/StudioSharedUI/avatarMask.png b/Client2020/content/textures/StudioSharedUI/avatarMask.png new file mode 100644 index 0000000..1ac156d Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/avatarMask.png differ diff --git a/Client2020/content/textures/StudioSharedUI/clear-hover.png b/Client2020/content/textures/StudioSharedUI/clear-hover.png new file mode 100644 index 0000000..fee06ad Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/clear-hover.png differ diff --git a/Client2020/content/textures/StudioSharedUI/clear.png b/Client2020/content/textures/StudioSharedUI/clear.png new file mode 100644 index 0000000..0d00fe3 Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/clear.png differ diff --git a/Client2020/content/textures/StudioSharedUI/close.png b/Client2020/content/textures/StudioSharedUI/close.png new file mode 100644 index 0000000..bd22a81 Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/close.png differ diff --git a/Client2020/content/textures/StudioSharedUI/default_group.png b/Client2020/content/textures/StudioSharedUI/default_group.png new file mode 100644 index 0000000..cd4ca17 Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/default_group.png differ diff --git a/Client2020/content/textures/StudioSharedUI/default_user.png b/Client2020/content/textures/StudioSharedUI/default_user.png new file mode 100644 index 0000000..79e92a8 Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/default_user.png differ diff --git a/Client2020/content/textures/StudioSharedUI/dot.png b/Client2020/content/textures/StudioSharedUI/dot.png new file mode 100644 index 0000000..e890040 Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/dot.png differ diff --git a/Client2020/content/textures/StudioSharedUI/dropShadow.png b/Client2020/content/textures/StudioSharedUI/dropShadow.png new file mode 100644 index 0000000..d0841b4 Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/dropShadow.png differ diff --git a/Client2020/content/textures/StudioSharedUI/folder.png b/Client2020/content/textures/StudioSharedUI/folder.png new file mode 100644 index 0000000..55259e3 Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/folder.png differ diff --git a/Client2020/content/textures/StudioSharedUI/import.png b/Client2020/content/textures/StudioSharedUI/import.png new file mode 100644 index 0000000..5cf509a Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/import.png differ diff --git a/Client2020/content/textures/StudioSharedUI/menu.png b/Client2020/content/textures/StudioSharedUI/menu.png new file mode 100644 index 0000000..fc70cb1 Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/menu.png differ diff --git a/Client2020/content/textures/StudioSharedUI/search.png b/Client2020/content/textures/StudioSharedUI/search.png new file mode 100644 index 0000000..2db1080 Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/search.png differ diff --git a/Client2020/content/textures/StudioSharedUI/spawn_withbg_24.png b/Client2020/content/textures/StudioSharedUI/spawn_withbg_24.png new file mode 100644 index 0000000..827984e Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/spawn_withbg_24.png differ diff --git a/Client2020/content/textures/StudioSharedUI/spawn_withbg_32.png b/Client2020/content/textures/StudioSharedUI/spawn_withbg_32.png new file mode 100644 index 0000000..16d53c2 Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/spawn_withbg_32.png differ diff --git a/Client2020/content/textures/StudioSharedUI/spawn_withoutbg_24.png b/Client2020/content/textures/StudioSharedUI/spawn_withoutbg_24.png new file mode 100644 index 0000000..101cb49 Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/spawn_withoutbg_24.png differ diff --git a/Client2020/content/textures/StudioSharedUI/spawn_withoutbg_32.png b/Client2020/content/textures/StudioSharedUI/spawn_withoutbg_32.png new file mode 100644 index 0000000..72dae33 Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/spawn_withoutbg_32.png differ diff --git a/Client2020/content/textures/StudioSharedUI/statusSuccess.png b/Client2020/content/textures/StudioSharedUI/statusSuccess.png new file mode 100644 index 0000000..35c806c Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/statusSuccess.png differ diff --git a/Client2020/content/textures/StudioSharedUI/statusWarning.png b/Client2020/content/textures/StudioSharedUI/statusWarning.png new file mode 100644 index 0000000..6e5f9e6 Binary files /dev/null and b/Client2020/content/textures/StudioSharedUI/statusWarning.png differ diff --git a/Client2020/content/textures/StudioToolbox/ArrowCollapsed.png b/Client2020/content/textures/StudioToolbox/ArrowCollapsed.png new file mode 100644 index 0000000..7a18bdb Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/ArrowCollapsed.png differ diff --git a/Client2020/content/textures/StudioToolbox/ArrowDownIconWhite.png b/Client2020/content/textures/StudioToolbox/ArrowDownIconWhite.png new file mode 100644 index 0000000..c5f9c2a Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/ArrowDownIconWhite.png differ diff --git a/Client2020/content/textures/StudioToolbox/ArrowExpanded.png b/Client2020/content/textures/StudioToolbox/ArrowExpanded.png new file mode 100644 index 0000000..c682205 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/ArrowExpanded.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/CenterPlus.png b/Client2020/content/textures/StudioToolbox/AssetConfig/CenterPlus.png new file mode 100644 index 0000000..05af26f Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/CenterPlus.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/copy_2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/copy_2x.png new file mode 100644 index 0000000..f46a53b Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/copy_2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/creations.png b/Client2020/content/textures/StudioToolbox/AssetConfig/creations.png new file mode 100644 index 0000000..0f1663a Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/creations.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/creations@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/creations@2x.png new file mode 100644 index 0000000..25534ed Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/creations@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/creations@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/creations@3x.png new file mode 100644 index 0000000..4725d58 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/creations@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/editlisting.png b/Client2020/content/textures/StudioToolbox/AssetConfig/editlisting.png new file mode 100644 index 0000000..c6e4874 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/editlisting.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/editlisting@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/editlisting@2x.png new file mode 100644 index 0000000..3abe57a Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/editlisting@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/editlisting@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/editlisting@3x.png new file mode 100644 index 0000000..1b69544 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/editlisting@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/gridview.png b/Client2020/content/textures/StudioToolbox/AssetConfig/gridview.png new file mode 100644 index 0000000..36b3c5c Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/gridview.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/gridview@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/gridview@2x.png new file mode 100644 index 0000000..f0d2fe1 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/gridview@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/gridview@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/gridview@3x.png new file mode 100644 index 0000000..2da3f0b Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/gridview@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/inventory.png b/Client2020/content/textures/StudioToolbox/AssetConfig/inventory.png new file mode 100644 index 0000000..10599b5 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/inventory.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/inventory@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/inventory@2x.png new file mode 100644 index 0000000..ea92214 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/inventory@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/inventory@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/inventory@3x.png new file mode 100644 index 0000000..861c888 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/inventory@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/listview.png b/Client2020/content/textures/StudioToolbox/AssetConfig/listview.png new file mode 100644 index 0000000..66cf09f Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/listview.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/listview@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/listview@2x.png new file mode 100644 index 0000000..7516e7f Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/listview@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/listview@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/listview@3x.png new file mode 100644 index 0000000..fd35d0d Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/listview@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/marketplace.png b/Client2020/content/textures/StudioToolbox/AssetConfig/marketplace.png new file mode 100644 index 0000000..114cf9c Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/marketplace.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/marketplace@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/marketplace@2x.png new file mode 100644 index 0000000..1ee0bd6 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/marketplace@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/marketplace@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/marketplace@3x.png new file mode 100644 index 0000000..b99e341 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/marketplace@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/menu_friends.png b/Client2020/content/textures/StudioToolbox/AssetConfig/menu_friends.png new file mode 100644 index 0000000..401847e Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/menu_friends.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/menu_friends@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/menu_friends@2x.png new file mode 100644 index 0000000..3a91c59 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/menu_friends@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/menu_friends@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/menu_friends@3x.png new file mode 100644 index 0000000..2fa4d99 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/menu_friends@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/offsale.png b/Client2020/content/textures/StudioToolbox/AssetConfig/offsale.png new file mode 100644 index 0000000..9191737 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/offsale.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/offsale@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/offsale@2x.png new file mode 100644 index 0000000..463a57c Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/offsale@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/offsale@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/offsale@3x.png new file mode 100644 index 0000000..97ae618 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/offsale@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/onsale.png b/Client2020/content/textures/StudioToolbox/AssetConfig/onsale.png new file mode 100644 index 0000000..d8aef73 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/onsale.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/onsale@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/onsale@2x.png new file mode 100644 index 0000000..5ccc0d3 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/onsale@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/onsale@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/onsale@3x.png new file mode 100644 index 0000000..41f34d7 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/onsale@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/pending.png b/Client2020/content/textures/StudioToolbox/AssetConfig/pending.png new file mode 100644 index 0000000..3bae1ed Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/pending.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/pending@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/pending@2x.png new file mode 100644 index 0000000..9ff8939 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/pending@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/pending@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/pending@3x.png new file mode 100644 index 0000000..597e6c6 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/pending@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/plugin_temp.png b/Client2020/content/textures/StudioToolbox/AssetConfig/plugin_temp.png new file mode 100644 index 0000000..3fd6953 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/plugin_temp.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/private.png b/Client2020/content/textures/StudioToolbox/AssetConfig/private.png new file mode 100644 index 0000000..6b64f56 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/private.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/private@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/private@2x.png new file mode 100644 index 0000000..826a07a Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/private@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/private@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/private@3x.png new file mode 100644 index 0000000..4d7b2ff Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/private@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/public.png b/Client2020/content/textures/StudioToolbox/AssetConfig/public.png new file mode 100644 index 0000000..5d5d51b Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/public.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/public@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/public@2x.png new file mode 100644 index 0000000..bcd3aac Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/public@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/public@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/public@3x.png new file mode 100644 index 0000000..88e2800 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/public@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/readyforsale.png b/Client2020/content/textures/StudioToolbox/AssetConfig/readyforsale.png new file mode 100644 index 0000000..79ff880 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/readyforsale.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/readyforsale@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/readyforsale@2x.png new file mode 100644 index 0000000..7fc5bad Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/readyforsale@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/readyforsale@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/readyforsale@3x.png new file mode 100644 index 0000000..26bd8d5 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/readyforsale@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/recent.png b/Client2020/content/textures/StudioToolbox/AssetConfig/recent.png new file mode 100644 index 0000000..873cc6d Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/recent.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/recent@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/recent@2x.png new file mode 100644 index 0000000..ca550c1 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/recent@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/recent@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/recent@3x.png new file mode 100644 index 0000000..199f1e0 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/recent@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/rejected.png b/Client2020/content/textures/StudioToolbox/AssetConfig/rejected.png new file mode 100644 index 0000000..4738d37 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/rejected.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/rejected@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/rejected@2x.png new file mode 100644 index 0000000..483a83f Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/rejected@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/rejected@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/rejected@3x.png new file mode 100644 index 0000000..64d5173 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/rejected@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/restore.png b/Client2020/content/textures/StudioToolbox/AssetConfig/restore.png new file mode 100644 index 0000000..0b414f7 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/restore.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/restore@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/restore@2x.png new file mode 100644 index 0000000..2c5dfa4 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/restore@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/restore@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/restore@3x.png new file mode 100644 index 0000000..2305a15 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/restore@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/sales.png b/Client2020/content/textures/StudioToolbox/AssetConfig/sales.png new file mode 100644 index 0000000..00d8dcc Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/sales.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/sales@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/sales@2x.png new file mode 100644 index 0000000..938b9a3 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/sales@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/sales@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/sales@3x.png new file mode 100644 index 0000000..3cc2836 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/sales@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/selected.png b/Client2020/content/textures/StudioToolbox/AssetConfig/selected.png new file mode 100644 index 0000000..ea5fd9b Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/selected.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/selected@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/selected@2x.png new file mode 100644 index 0000000..26bd8d5 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/selected@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/selected@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/selected@3x.png new file mode 100644 index 0000000..0335111 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/selected@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/version.png b/Client2020/content/textures/StudioToolbox/AssetConfig/version.png new file mode 100644 index 0000000..b212d7d Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/version.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/version@2x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/version@2x.png new file mode 100644 index 0000000..7ea5f32 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/version@2x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetConfig/version@3x.png b/Client2020/content/textures/StudioToolbox/AssetConfig/version@3x.png new file mode 100644 index 0000000..3499bc2 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetConfig/version@3x.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/Likes_Grey.png b/Client2020/content/textures/StudioToolbox/AssetPreview/Likes_Grey.png new file mode 100644 index 0000000..9209f80 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/Likes_Grey.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/Link_Arrow.png b/Client2020/content/textures/StudioToolbox/AssetPreview/Link_Arrow.png new file mode 100644 index 0000000..b480c24 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/Link_Arrow.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/OffSale.png b/Client2020/content/textures/StudioToolbox/AssetPreview/OffSale.png new file mode 100644 index 0000000..a08e3ed Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/OffSale.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/OnSale.png b/Client2020/content/textures/StudioToolbox/AssetPreview/OnSale.png new file mode 100644 index 0000000..0095136 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/OnSale.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/Pending.png b/Client2020/content/textures/StudioToolbox/AssetPreview/Pending.png new file mode 100644 index 0000000..abe6b5e Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/Pending.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/ReadyforSale.png b/Client2020/content/textures/StudioToolbox/AssetPreview/ReadyforSale.png new file mode 100644 index 0000000..0be6696 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/ReadyforSale.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/Rejected.png b/Client2020/content/textures/StudioToolbox/AssetPreview/Rejected.png new file mode 100644 index 0000000..5f5bdb0 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/Rejected.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/audioPlay_BG.png b/Client2020/content/textures/StudioToolbox/AssetPreview/audioPlay_BG.png new file mode 100644 index 0000000..779664b Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/audioPlay_BG.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/close.png b/Client2020/content/textures/StudioToolbox/AssetPreview/close.png new file mode 100644 index 0000000..1ac968b Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/close.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/fullscreen.png b/Client2020/content/textures/StudioToolbox/AssetPreview/fullscreen.png new file mode 100644 index 0000000..3014364 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/fullscreen.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/fullscreen_exit.png b/Client2020/content/textures/StudioToolbox/AssetPreview/fullscreen_exit.png new file mode 100644 index 0000000..f0ceb71 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/fullscreen_exit.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/hierarchy.png b/Client2020/content/textures/StudioToolbox/AssetPreview/hierarchy.png new file mode 100644 index 0000000..853f4e5 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/hierarchy.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/magnifier_ph.png b/Client2020/content/textures/StudioToolbox/AssetPreview/magnifier_ph.png new file mode 100644 index 0000000..6ad4ef5 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/magnifier_ph.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/more.png b/Client2020/content/textures/StudioToolbox/AssetPreview/more.png new file mode 100644 index 0000000..65abae2 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/more.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/pause_button.png b/Client2020/content/textures/StudioToolbox/AssetPreview/pause_button.png new file mode 100644 index 0000000..d6ba58e Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/pause_button.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/play_button.png b/Client2020/content/textures/StudioToolbox/AssetPreview/play_button.png new file mode 100644 index 0000000..a97bde8 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/play_button.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/preview.png b/Client2020/content/textures/StudioToolbox/AssetPreview/preview.png new file mode 100644 index 0000000..6d4e824 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/preview.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/rating_large.png b/Client2020/content/textures/StudioToolbox/AssetPreview/rating_large.png new file mode 100644 index 0000000..b816657 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/rating_large.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/rating_small.png b/Client2020/content/textures/StudioToolbox/AssetPreview/rating_small.png new file mode 100644 index 0000000..4a3100d Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/rating_small.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/star_filled.png b/Client2020/content/textures/StudioToolbox/AssetPreview/star_filled.png new file mode 100644 index 0000000..fbf2a35 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/star_filled.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/star_stroke.png b/Client2020/content/textures/StudioToolbox/AssetPreview/star_stroke.png new file mode 100644 index 0000000..366df74 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/star_stroke.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/vote_down.png b/Client2020/content/textures/StudioToolbox/AssetPreview/vote_down.png new file mode 100644 index 0000000..4bbe19e Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/vote_down.png differ diff --git a/Client2020/content/textures/StudioToolbox/AssetPreview/vote_up.png b/Client2020/content/textures/StudioToolbox/AssetPreview/vote_up.png new file mode 100644 index 0000000..35bc285 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AssetPreview/vote_up.png differ diff --git a/Client2020/content/textures/StudioToolbox/AudioPreview/pause.png b/Client2020/content/textures/StudioToolbox/AudioPreview/pause.png new file mode 100644 index 0000000..86a1d64 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AudioPreview/pause.png differ diff --git a/Client2020/content/textures/StudioToolbox/AudioPreview/pause_hover.png b/Client2020/content/textures/StudioToolbox/AudioPreview/pause_hover.png new file mode 100644 index 0000000..004e098 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AudioPreview/pause_hover.png differ diff --git a/Client2020/content/textures/StudioToolbox/AudioPreview/play.png b/Client2020/content/textures/StudioToolbox/AudioPreview/play.png new file mode 100644 index 0000000..20d4aac Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AudioPreview/play.png differ diff --git a/Client2020/content/textures/StudioToolbox/AudioPreview/play_hover.png b/Client2020/content/textures/StudioToolbox/AudioPreview/play_hover.png new file mode 100644 index 0000000..9a5b37a Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/AudioPreview/play_hover.png differ diff --git a/Client2020/content/textures/StudioToolbox/Clear.png b/Client2020/content/textures/StudioToolbox/Clear.png new file mode 100644 index 0000000..0d00fe3 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/Clear.png differ diff --git a/Client2020/content/textures/StudioToolbox/ClearHover.png b/Client2020/content/textures/StudioToolbox/ClearHover.png new file mode 100644 index 0000000..fee06ad Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/ClearHover.png differ diff --git a/Client2020/content/textures/StudioToolbox/DeleteButton.png b/Client2020/content/textures/StudioToolbox/DeleteButton.png new file mode 100644 index 0000000..05116cd Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/DeleteButton.png differ diff --git a/Client2020/content/textures/StudioToolbox/EndorsedBadge.png b/Client2020/content/textures/StudioToolbox/EndorsedBadge.png new file mode 100644 index 0000000..b3f5fb4 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/EndorsedBadge.png differ diff --git a/Client2020/content/textures/StudioToolbox/NoBackgroundIcon.png b/Client2020/content/textures/StudioToolbox/NoBackgroundIcon.png new file mode 100644 index 0000000..1c1e179 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/NoBackgroundIcon.png differ diff --git a/Client2020/content/textures/StudioToolbox/ProductOwned.png b/Client2020/content/textures/StudioToolbox/ProductOwned.png new file mode 100644 index 0000000..1e29177 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/ProductOwned.png differ diff --git a/Client2020/content/textures/StudioToolbox/RoundedBackground.png b/Client2020/content/textures/StudioToolbox/RoundedBackground.png new file mode 100644 index 0000000..602e90f Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/RoundedBackground.png differ diff --git a/Client2020/content/textures/StudioToolbox/RoundedBorder.png b/Client2020/content/textures/StudioToolbox/RoundedBorder.png new file mode 100644 index 0000000..8335459 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/RoundedBorder.png differ diff --git a/Client2020/content/textures/StudioToolbox/ScrollBarBottom.png b/Client2020/content/textures/StudioToolbox/ScrollBarBottom.png new file mode 100644 index 0000000..235055a Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/ScrollBarBottom.png differ diff --git a/Client2020/content/textures/StudioToolbox/ScrollBarMiddle.png b/Client2020/content/textures/StudioToolbox/ScrollBarMiddle.png new file mode 100644 index 0000000..ed7ca4a Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/ScrollBarMiddle.png differ diff --git a/Client2020/content/textures/StudioToolbox/ScrollBarTop.png b/Client2020/content/textures/StudioToolbox/ScrollBarTop.png new file mode 100644 index 0000000..d01c82f Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/ScrollBarTop.png differ diff --git a/Client2020/content/textures/StudioToolbox/Search.png b/Client2020/content/textures/StudioToolbox/Search.png new file mode 100644 index 0000000..f4934f7 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/Search.png differ diff --git a/Client2020/content/textures/StudioToolbox/SearchOptions.png b/Client2020/content/textures/StudioToolbox/SearchOptions.png new file mode 100644 index 0000000..6c894e1 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/SearchOptions.png differ diff --git a/Client2020/content/textures/StudioToolbox/Tabs/Inventory.png b/Client2020/content/textures/StudioToolbox/Tabs/Inventory.png new file mode 100644 index 0000000..2c21b17 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/Tabs/Inventory.png differ diff --git a/Client2020/content/textures/StudioToolbox/Tabs/MyCreations.png b/Client2020/content/textures/StudioToolbox/Tabs/MyCreations.png new file mode 100644 index 0000000..df19608 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/Tabs/MyCreations.png differ diff --git a/Client2020/content/textures/StudioToolbox/Tabs/Recent.png b/Client2020/content/textures/StudioToolbox/Tabs/Recent.png new file mode 100644 index 0000000..538e331 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/Tabs/Recent.png differ diff --git a/Client2020/content/textures/StudioToolbox/Tabs/Shop.png b/Client2020/content/textures/StudioToolbox/Tabs/Shop.png new file mode 100644 index 0000000..f1c20f7 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/Tabs/Shop.png differ diff --git a/Client2020/content/textures/StudioToolbox/ToolboxIcon.png b/Client2020/content/textures/StudioToolbox/ToolboxIcon.png new file mode 100644 index 0000000..2e99ec3 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/ToolboxIcon.png differ diff --git a/Client2020/content/textures/StudioToolbox/Voting/Thumb.png b/Client2020/content/textures/StudioToolbox/Voting/Thumb.png new file mode 100644 index 0000000..21a239b Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/Voting/Thumb.png differ diff --git a/Client2020/content/textures/StudioToolbox/Voting/thumb-down.png b/Client2020/content/textures/StudioToolbox/Voting/thumb-down.png new file mode 100644 index 0000000..d36de77 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/Voting/thumb-down.png differ diff --git a/Client2020/content/textures/StudioToolbox/Voting/thumbs-down-filled.png b/Client2020/content/textures/StudioToolbox/Voting/thumbs-down-filled.png new file mode 100644 index 0000000..1976341 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/Voting/thumbs-down-filled.png differ diff --git a/Client2020/content/textures/StudioToolbox/Voting/thumbs-up-filled.png b/Client2020/content/textures/StudioToolbox/Voting/thumbs-up-filled.png new file mode 100644 index 0000000..f97cbdd Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/Voting/thumbs-up-filled.png differ diff --git a/Client2020/content/textures/StudioToolbox/Voting/thumbup.png b/Client2020/content/textures/StudioToolbox/Voting/thumbup.png new file mode 100644 index 0000000..babd290 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/Voting/thumbup.png differ diff --git a/Client2020/content/textures/StudioToolbox/placeholder_video.png b/Client2020/content/textures/StudioToolbox/placeholder_video.png new file mode 100644 index 0000000..be98f33 Binary files /dev/null and b/Client2020/content/textures/StudioToolbox/placeholder_video.png differ diff --git a/Client2020/content/textures/StudioUIEditor/icon_resize1.png b/Client2020/content/textures/StudioUIEditor/icon_resize1.png new file mode 100644 index 0000000..23aef57 Binary files /dev/null and b/Client2020/content/textures/StudioUIEditor/icon_resize1.png differ diff --git a/Client2020/content/textures/StudioUIEditor/icon_resize2.png b/Client2020/content/textures/StudioUIEditor/icon_resize2.png new file mode 100644 index 0000000..b50268f Binary files /dev/null and b/Client2020/content/textures/StudioUIEditor/icon_resize2.png differ diff --git a/Client2020/content/textures/StudioUIEditor/icon_resize3.png b/Client2020/content/textures/StudioUIEditor/icon_resize3.png new file mode 100644 index 0000000..3258512 Binary files /dev/null and b/Client2020/content/textures/StudioUIEditor/icon_resize3.png differ diff --git a/Client2020/content/textures/StudioUIEditor/icon_resize4.png b/Client2020/content/textures/StudioUIEditor/icon_resize4.png new file mode 100644 index 0000000..2ac469f Binary files /dev/null and b/Client2020/content/textures/StudioUIEditor/icon_resize4.png differ diff --git a/Client2020/content/textures/StudioUIEditor/icon_rotate1.png b/Client2020/content/textures/StudioUIEditor/icon_rotate1.png new file mode 100644 index 0000000..849be92 Binary files /dev/null and b/Client2020/content/textures/StudioUIEditor/icon_rotate1.png differ diff --git a/Client2020/content/textures/StudioUIEditor/icon_rotate2.png b/Client2020/content/textures/StudioUIEditor/icon_rotate2.png new file mode 100644 index 0000000..ee35889 Binary files /dev/null and b/Client2020/content/textures/StudioUIEditor/icon_rotate2.png differ diff --git a/Client2020/content/textures/StudioUIEditor/icon_rotate3.png b/Client2020/content/textures/StudioUIEditor/icon_rotate3.png new file mode 100644 index 0000000..a87e5bc Binary files /dev/null and b/Client2020/content/textures/StudioUIEditor/icon_rotate3.png differ diff --git a/Client2020/content/textures/StudioUIEditor/icon_rotate4.png b/Client2020/content/textures/StudioUIEditor/icon_rotate4.png new file mode 100644 index 0000000..e6934f7 Binary files /dev/null and b/Client2020/content/textures/StudioUIEditor/icon_rotate4.png differ diff --git a/Client2020/content/textures/StudioUIEditor/icon_rotate5.png b/Client2020/content/textures/StudioUIEditor/icon_rotate5.png new file mode 100644 index 0000000..2b69bc6 Binary files /dev/null and b/Client2020/content/textures/StudioUIEditor/icon_rotate5.png differ diff --git a/Client2020/content/textures/StudioUIEditor/icon_rotate6.png b/Client2020/content/textures/StudioUIEditor/icon_rotate6.png new file mode 100644 index 0000000..70dedbd Binary files /dev/null and b/Client2020/content/textures/StudioUIEditor/icon_rotate6.png differ diff --git a/Client2020/content/textures/StudioUIEditor/icon_rotate7.png b/Client2020/content/textures/StudioUIEditor/icon_rotate7.png new file mode 100644 index 0000000..048264c Binary files /dev/null and b/Client2020/content/textures/StudioUIEditor/icon_rotate7.png differ diff --git a/Client2020/content/textures/StudioUIEditor/icon_rotate8.png b/Client2020/content/textures/StudioUIEditor/icon_rotate8.png new file mode 100644 index 0000000..33afb5d Binary files /dev/null and b/Client2020/content/textures/StudioUIEditor/icon_rotate8.png differ diff --git a/Client2020/content/textures/StudioUIEditor/resizeHandleDropShadow.png b/Client2020/content/textures/StudioUIEditor/resizeHandleDropShadow.png new file mode 100644 index 0000000..b0aaf29 Binary files /dev/null and b/Client2020/content/textures/StudioUIEditor/resizeHandleDropShadow.png differ diff --git a/Client2020/content/textures/StudioUIEditor/valueBoxRoundedRectangle.png b/Client2020/content/textures/StudioUIEditor/valueBoxRoundedRectangle.png new file mode 100644 index 0000000..602e90f Binary files /dev/null and b/Client2020/content/textures/StudioUIEditor/valueBoxRoundedRectangle.png differ diff --git a/Client2020/content/textures/SurfacesDefault.png b/Client2020/content/textures/SurfacesDefault.png new file mode 100644 index 0000000..be149ad Binary files /dev/null and b/Client2020/content/textures/SurfacesDefault.png differ diff --git a/Client2020/content/textures/TerrainTools/DownArrowButtonOpen17.png b/Client2020/content/textures/TerrainTools/DownArrowButtonOpen17.png new file mode 100644 index 0000000..a4a8e42 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/DownArrowButtonOpen17.png differ diff --git a/Client2020/content/textures/TerrainTools/EdgesSquare17x1.png b/Client2020/content/textures/TerrainTools/EdgesSquare17x1.png new file mode 100644 index 0000000..5866d7f Binary files /dev/null and b/Client2020/content/textures/TerrainTools/EdgesSquare17x1.png differ diff --git a/Client2020/content/textures/TerrainTools/UpArrowButtonOpen17.png b/Client2020/content/textures/TerrainTools/UpArrowButtonOpen17.png new file mode 100644 index 0000000..75d8bc5 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/UpArrowButtonOpen17.png differ diff --git a/Client2020/content/textures/TerrainTools/button_arrow.png b/Client2020/content/textures/TerrainTools/button_arrow.png new file mode 100644 index 0000000..31b851c Binary files /dev/null and b/Client2020/content/textures/TerrainTools/button_arrow.png differ diff --git a/Client2020/content/textures/TerrainTools/button_arrow_down.png b/Client2020/content/textures/TerrainTools/button_arrow_down.png new file mode 100644 index 0000000..f3c6616 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/button_arrow_down.png differ diff --git a/Client2020/content/textures/TerrainTools/button_default.png b/Client2020/content/textures/TerrainTools/button_default.png new file mode 100644 index 0000000..28eb7a7 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/button_default.png differ diff --git a/Client2020/content/textures/TerrainTools/button_hover.png b/Client2020/content/textures/TerrainTools/button_hover.png new file mode 100644 index 0000000..5184f6d Binary files /dev/null and b/Client2020/content/textures/TerrainTools/button_hover.png differ diff --git a/Client2020/content/textures/TerrainTools/button_pressed.png b/Client2020/content/textures/TerrainTools/button_pressed.png new file mode 100644 index 0000000..5484114 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/button_pressed.png differ diff --git a/Client2020/content/textures/TerrainTools/checkbox_square.png b/Client2020/content/textures/TerrainTools/checkbox_square.png new file mode 100644 index 0000000..af883a9 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/checkbox_square.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_flatten_both.png b/Client2020/content/textures/TerrainTools/icon_flatten_both.png new file mode 100644 index 0000000..eda6621 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_flatten_both.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_flatten_erode.png b/Client2020/content/textures/TerrainTools/icon_flatten_erode.png new file mode 100644 index 0000000..08cf742 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_flatten_erode.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_flatten_grow.png b/Client2020/content/textures/TerrainTools/icon_flatten_grow.png new file mode 100644 index 0000000..c5a1b14 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_flatten_grow.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_picker_disable.png b/Client2020/content/textures/TerrainTools/icon_picker_disable.png new file mode 100644 index 0000000..7886a65 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_picker_disable.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_picker_disable_dark.png b/Client2020/content/textures/TerrainTools/icon_picker_disable_dark.png new file mode 100644 index 0000000..88fac4f Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_picker_disable_dark.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_picker_enable.png b/Client2020/content/textures/TerrainTools/icon_picker_enable.png new file mode 100644 index 0000000..dea3f2e Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_picker_enable.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_regions_copy.png b/Client2020/content/textures/TerrainTools/icon_regions_copy.png new file mode 100644 index 0000000..d95bfa9 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_regions_copy.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_regions_delete.png b/Client2020/content/textures/TerrainTools/icon_regions_delete.png new file mode 100644 index 0000000..cb5bbc3 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_regions_delete.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_regions_fill.png b/Client2020/content/textures/TerrainTools/icon_regions_fill.png new file mode 100644 index 0000000..a5ad504 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_regions_fill.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_regions_move.png b/Client2020/content/textures/TerrainTools/icon_regions_move.png new file mode 100644 index 0000000..f1dd66f Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_regions_move.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_regions_paste.png b/Client2020/content/textures/TerrainTools/icon_regions_paste.png new file mode 100644 index 0000000..a602011 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_regions_paste.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_regions_resize.png b/Client2020/content/textures/TerrainTools/icon_regions_resize.png new file mode 100644 index 0000000..1306cc0 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_regions_resize.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_regions_rotate.png b/Client2020/content/textures/TerrainTools/icon_regions_rotate.png new file mode 100644 index 0000000..feeb0dd Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_regions_rotate.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_regions_select.png b/Client2020/content/textures/TerrainTools/icon_regions_select.png new file mode 100644 index 0000000..bb6d6e1 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_regions_select.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_shape_cube.png b/Client2020/content/textures/TerrainTools/icon_shape_cube.png new file mode 100644 index 0000000..30b4f5a Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_shape_cube.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_shape_cylinder.png b/Client2020/content/textures/TerrainTools/icon_shape_cylinder.png new file mode 100644 index 0000000..6054bf2 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_shape_cylinder.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_shape_sphere.png b/Client2020/content/textures/TerrainTools/icon_shape_sphere.png new file mode 100644 index 0000000..41458ef Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_shape_sphere.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_terrain_big.png b/Client2020/content/textures/TerrainTools/icon_terrain_big.png new file mode 100644 index 0000000..6c27ece Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_terrain_big.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_tick.png b/Client2020/content/textures/TerrainTools/icon_tick.png new file mode 100644 index 0000000..997f788 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_tick.png differ diff --git a/Client2020/content/textures/TerrainTools/icon_tick_grey.png b/Client2020/content/textures/TerrainTools/icon_tick_grey.png new file mode 100644 index 0000000..d451338 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/icon_tick_grey.png differ diff --git a/Client2020/content/textures/TerrainTools/import_delete.png b/Client2020/content/textures/TerrainTools/import_delete.png new file mode 100644 index 0000000..a4d34bf Binary files /dev/null and b/Client2020/content/textures/TerrainTools/import_delete.png differ diff --git a/Client2020/content/textures/TerrainTools/import_edit.png b/Client2020/content/textures/TerrainTools/import_edit.png new file mode 100644 index 0000000..25d52ae Binary files /dev/null and b/Client2020/content/textures/TerrainTools/import_edit.png differ diff --git a/Client2020/content/textures/TerrainTools/import_selectImg_dark.png b/Client2020/content/textures/TerrainTools/import_selectImg_dark.png new file mode 100644 index 0000000..a6c34a8 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/import_selectImg_dark.png differ diff --git a/Client2020/content/textures/TerrainTools/import_select_image.png b/Client2020/content/textures/TerrainTools/import_select_image.png new file mode 100644 index 0000000..2014a44 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/import_select_image.png differ diff --git a/Client2020/content/textures/TerrainTools/import_toggleOff.png b/Client2020/content/textures/TerrainTools/import_toggleOff.png new file mode 100644 index 0000000..407ebec Binary files /dev/null and b/Client2020/content/textures/TerrainTools/import_toggleOff.png differ diff --git a/Client2020/content/textures/TerrainTools/import_toggleOff_dark.png b/Client2020/content/textures/TerrainTools/import_toggleOff_dark.png new file mode 100644 index 0000000..43b4783 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/import_toggleOff_dark.png differ diff --git a/Client2020/content/textures/TerrainTools/import_toggleOn.png b/Client2020/content/textures/TerrainTools/import_toggleOn.png new file mode 100644 index 0000000..494336e Binary files /dev/null and b/Client2020/content/textures/TerrainTools/import_toggleOn.png differ diff --git a/Client2020/content/textures/TerrainTools/import_toggleOn_dark.png b/Client2020/content/textures/TerrainTools/import_toggleOn_dark.png new file mode 100644 index 0000000..2f83bf4 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/import_toggleOn_dark.png differ diff --git a/Client2020/content/textures/TerrainTools/locked.png b/Client2020/content/textures/TerrainTools/locked.png new file mode 100644 index 0000000..69794d5 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/locked.png differ diff --git a/Client2020/content/textures/TerrainTools/mt_add.png b/Client2020/content/textures/TerrainTools/mt_add.png new file mode 100644 index 0000000..f9e0c40 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mt_add.png differ diff --git a/Client2020/content/textures/TerrainTools/mt_convert_part.png b/Client2020/content/textures/TerrainTools/mt_convert_part.png new file mode 100644 index 0000000..941f1ac Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mt_convert_part.png differ diff --git a/Client2020/content/textures/TerrainTools/mt_erode.png b/Client2020/content/textures/TerrainTools/mt_erode.png new file mode 100644 index 0000000..5ace872 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mt_erode.png differ diff --git a/Client2020/content/textures/TerrainTools/mt_flatten.png b/Client2020/content/textures/TerrainTools/mt_flatten.png new file mode 100644 index 0000000..1ae9a75 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mt_flatten.png differ diff --git a/Client2020/content/textures/TerrainTools/mt_generate.png b/Client2020/content/textures/TerrainTools/mt_generate.png new file mode 100644 index 0000000..dffcf7d Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mt_generate.png differ diff --git a/Client2020/content/textures/TerrainTools/mt_grow.png b/Client2020/content/textures/TerrainTools/mt_grow.png new file mode 100644 index 0000000..76e9333 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mt_grow.png differ diff --git a/Client2020/content/textures/TerrainTools/mt_paint.png b/Client2020/content/textures/TerrainTools/mt_paint.png new file mode 100644 index 0000000..03096f5 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mt_paint.png differ diff --git a/Client2020/content/textures/TerrainTools/mt_regions.png b/Client2020/content/textures/TerrainTools/mt_regions.png new file mode 100644 index 0000000..f9aaae8 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mt_regions.png differ diff --git a/Client2020/content/textures/TerrainTools/mt_replace.png b/Client2020/content/textures/TerrainTools/mt_replace.png new file mode 100644 index 0000000..14fc070 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mt_replace.png differ diff --git a/Client2020/content/textures/TerrainTools/mt_sea_level.png b/Client2020/content/textures/TerrainTools/mt_sea_level.png new file mode 100644 index 0000000..a552a52 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mt_sea_level.png differ diff --git a/Client2020/content/textures/TerrainTools/mt_smooth.png b/Client2020/content/textures/TerrainTools/mt_smooth.png new file mode 100644 index 0000000..76aadbc Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mt_smooth.png differ diff --git a/Client2020/content/textures/TerrainTools/mt_subtract.png b/Client2020/content/textures/TerrainTools/mt_subtract.png new file mode 100644 index 0000000..a9b2a6a Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mt_subtract.png differ diff --git a/Client2020/content/textures/TerrainTools/mt_terrain_clear.png b/Client2020/content/textures/TerrainTools/mt_terrain_clear.png new file mode 100644 index 0000000..3ef4be7 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mt_terrain_clear.png differ diff --git a/Client2020/content/textures/TerrainTools/mt_terrain_import.png b/Client2020/content/textures/TerrainTools/mt_terrain_import.png new file mode 100644 index 0000000..5b8085c Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mt_terrain_import.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_air.png b/Client2020/content/textures/TerrainTools/mtrl_air.png new file mode 100644 index 0000000..f9eb777 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_air.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_asphalt.png b/Client2020/content/textures/TerrainTools/mtrl_asphalt.png new file mode 100644 index 0000000..7b35820 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_asphalt.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_basalt.png b/Client2020/content/textures/TerrainTools/mtrl_basalt.png new file mode 100644 index 0000000..aaccf88 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_basalt.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_brick.png b/Client2020/content/textures/TerrainTools/mtrl_brick.png new file mode 100644 index 0000000..e0feaac Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_brick.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_cobblestone.png b/Client2020/content/textures/TerrainTools/mtrl_cobblestone.png new file mode 100644 index 0000000..f36e40b Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_cobblestone.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_concrete.png b/Client2020/content/textures/TerrainTools/mtrl_concrete.png new file mode 100644 index 0000000..8786719 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_concrete.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_crackedlava.png b/Client2020/content/textures/TerrainTools/mtrl_crackedlava.png new file mode 100644 index 0000000..03dcd6c Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_crackedlava.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_glacier.png b/Client2020/content/textures/TerrainTools/mtrl_glacier.png new file mode 100644 index 0000000..bd795db Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_glacier.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_grass.png b/Client2020/content/textures/TerrainTools/mtrl_grass.png new file mode 100644 index 0000000..bb58cb5 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_grass.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_ground.png b/Client2020/content/textures/TerrainTools/mtrl_ground.png new file mode 100644 index 0000000..b605720 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_ground.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_ice.png b/Client2020/content/textures/TerrainTools/mtrl_ice.png new file mode 100644 index 0000000..535b8b2 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_ice.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_leafygrass.png b/Client2020/content/textures/TerrainTools/mtrl_leafygrass.png new file mode 100644 index 0000000..fd4a10f Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_leafygrass.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_limestone.png b/Client2020/content/textures/TerrainTools/mtrl_limestone.png new file mode 100644 index 0000000..a0c1419 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_limestone.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_mud.png b/Client2020/content/textures/TerrainTools/mtrl_mud.png new file mode 100644 index 0000000..44ccbbc Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_mud.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_pavement.png b/Client2020/content/textures/TerrainTools/mtrl_pavement.png new file mode 100644 index 0000000..4306661 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_pavement.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_rock.png b/Client2020/content/textures/TerrainTools/mtrl_rock.png new file mode 100644 index 0000000..a66785e Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_rock.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_salt.png b/Client2020/content/textures/TerrainTools/mtrl_salt.png new file mode 100644 index 0000000..00d8c17 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_salt.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_sand.png b/Client2020/content/textures/TerrainTools/mtrl_sand.png new file mode 100644 index 0000000..df3431b Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_sand.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_sandstone.png b/Client2020/content/textures/TerrainTools/mtrl_sandstone.png new file mode 100644 index 0000000..9a31639 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_sandstone.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_slate.png b/Client2020/content/textures/TerrainTools/mtrl_slate.png new file mode 100644 index 0000000..e2b4a65 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_slate.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_snow.png b/Client2020/content/textures/TerrainTools/mtrl_snow.png new file mode 100644 index 0000000..9698dda Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_snow.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_water.png b/Client2020/content/textures/TerrainTools/mtrl_water.png new file mode 100644 index 0000000..0b9595a Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_water.png differ diff --git a/Client2020/content/textures/TerrainTools/mtrl_woodplanks.png b/Client2020/content/textures/TerrainTools/mtrl_woodplanks.png new file mode 100644 index 0000000..d7a3cee Binary files /dev/null and b/Client2020/content/textures/TerrainTools/mtrl_woodplanks.png differ diff --git a/Client2020/content/textures/TerrainTools/progress_bar.png b/Client2020/content/textures/TerrainTools/progress_bar.png new file mode 100644 index 0000000..4c8be1c Binary files /dev/null and b/Client2020/content/textures/TerrainTools/progress_bar.png differ diff --git a/Client2020/content/textures/TerrainTools/radio_button_bullet.png b/Client2020/content/textures/TerrainTools/radio_button_bullet.png new file mode 100644 index 0000000..0f96b39 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/radio_button_bullet.png differ diff --git a/Client2020/content/textures/TerrainTools/radio_button_bullet_dark.png b/Client2020/content/textures/TerrainTools/radio_button_bullet_dark.png new file mode 100644 index 0000000..c846d72 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/radio_button_bullet_dark.png differ diff --git a/Client2020/content/textures/TerrainTools/radio_button_frame.png b/Client2020/content/textures/TerrainTools/radio_button_frame.png new file mode 100644 index 0000000..fbb0cff Binary files /dev/null and b/Client2020/content/textures/TerrainTools/radio_button_frame.png differ diff --git a/Client2020/content/textures/TerrainTools/radio_button_frame_dark.png b/Client2020/content/textures/TerrainTools/radio_button_frame_dark.png new file mode 100644 index 0000000..b3d9af0 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/radio_button_frame_dark.png differ diff --git a/Client2020/content/textures/TerrainTools/sliderbar_blue.png b/Client2020/content/textures/TerrainTools/sliderbar_blue.png new file mode 100644 index 0000000..0fbbf6b Binary files /dev/null and b/Client2020/content/textures/TerrainTools/sliderbar_blue.png differ diff --git a/Client2020/content/textures/TerrainTools/sliderbar_button.png b/Client2020/content/textures/TerrainTools/sliderbar_button.png new file mode 100644 index 0000000..0f8e1c7 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/sliderbar_button.png differ diff --git a/Client2020/content/textures/TerrainTools/sliderbar_grey.png b/Client2020/content/textures/TerrainTools/sliderbar_grey.png new file mode 100644 index 0000000..dd1cdaa Binary files /dev/null and b/Client2020/content/textures/TerrainTools/sliderbar_grey.png differ diff --git a/Client2020/content/textures/TerrainTools/unlocked.png b/Client2020/content/textures/TerrainTools/unlocked.png new file mode 100644 index 0000000..ee12272 Binary files /dev/null and b/Client2020/content/textures/TerrainTools/unlocked.png differ diff --git a/Client2020/content/textures/UnAnchorCursor.png b/Client2020/content/textures/UnAnchorCursor.png new file mode 100644 index 0000000..4ca5381 Binary files /dev/null and b/Client2020/content/textures/UnAnchorCursor.png differ diff --git a/Client2020/content/textures/UnlockCursor.png b/Client2020/content/textures/UnlockCursor.png new file mode 100644 index 0000000..b765c16 Binary files /dev/null and b/Client2020/content/textures/UnlockCursor.png differ diff --git a/Client2020/content/textures/ViewSelector/back.png b/Client2020/content/textures/ViewSelector/back.png new file mode 100644 index 0000000..9efb52b Binary files /dev/null and b/Client2020/content/textures/ViewSelector/back.png differ diff --git a/Client2020/content/textures/ViewSelector/back_hover.png b/Client2020/content/textures/ViewSelector/back_hover.png new file mode 100644 index 0000000..0b75db4 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/back_hover.png differ diff --git a/Client2020/content/textures/ViewSelector/back_hover_zh_cn.png b/Client2020/content/textures/ViewSelector/back_hover_zh_cn.png new file mode 100644 index 0000000..4c1cd59 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/back_hover_zh_cn.png differ diff --git a/Client2020/content/textures/ViewSelector/back_zh_cn.png b/Client2020/content/textures/ViewSelector/back_zh_cn.png new file mode 100644 index 0000000..78e0d29 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/back_zh_cn.png differ diff --git a/Client2020/content/textures/ViewSelector/background.png b/Client2020/content/textures/ViewSelector/background.png new file mode 100644 index 0000000..eda1219 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/background.png differ diff --git a/Client2020/content/textures/ViewSelector/bottom.png b/Client2020/content/textures/ViewSelector/bottom.png new file mode 100644 index 0000000..2d6ca53 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/bottom.png differ diff --git a/Client2020/content/textures/ViewSelector/bottom_hover.png b/Client2020/content/textures/ViewSelector/bottom_hover.png new file mode 100644 index 0000000..36f744c Binary files /dev/null and b/Client2020/content/textures/ViewSelector/bottom_hover.png differ diff --git a/Client2020/content/textures/ViewSelector/bottom_hover_zh_cn.png b/Client2020/content/textures/ViewSelector/bottom_hover_zh_cn.png new file mode 100644 index 0000000..1f4f17b Binary files /dev/null and b/Client2020/content/textures/ViewSelector/bottom_hover_zh_cn.png differ diff --git a/Client2020/content/textures/ViewSelector/bottom_zh_cn.png b/Client2020/content/textures/ViewSelector/bottom_zh_cn.png new file mode 100644 index 0000000..1342fef Binary files /dev/null and b/Client2020/content/textures/ViewSelector/bottom_zh_cn.png differ diff --git a/Client2020/content/textures/ViewSelector/face_arrow.png b/Client2020/content/textures/ViewSelector/face_arrow.png new file mode 100644 index 0000000..f29eb16 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/face_arrow.png differ diff --git a/Client2020/content/textures/ViewSelector/front.png b/Client2020/content/textures/ViewSelector/front.png new file mode 100644 index 0000000..a1543bf Binary files /dev/null and b/Client2020/content/textures/ViewSelector/front.png differ diff --git a/Client2020/content/textures/ViewSelector/front_hover.png b/Client2020/content/textures/ViewSelector/front_hover.png new file mode 100644 index 0000000..762fcae Binary files /dev/null and b/Client2020/content/textures/ViewSelector/front_hover.png differ diff --git a/Client2020/content/textures/ViewSelector/front_hover_zh_cn.png b/Client2020/content/textures/ViewSelector/front_hover_zh_cn.png new file mode 100644 index 0000000..ea81e16 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/front_hover_zh_cn.png differ diff --git a/Client2020/content/textures/ViewSelector/front_zh_cn.png b/Client2020/content/textures/ViewSelector/front_zh_cn.png new file mode 100644 index 0000000..9663a7e Binary files /dev/null and b/Client2020/content/textures/ViewSelector/front_zh_cn.png differ diff --git a/Client2020/content/textures/ViewSelector/left.png b/Client2020/content/textures/ViewSelector/left.png new file mode 100644 index 0000000..afe2401 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/left.png differ diff --git a/Client2020/content/textures/ViewSelector/left_hover.png b/Client2020/content/textures/ViewSelector/left_hover.png new file mode 100644 index 0000000..532ec74 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/left_hover.png differ diff --git a/Client2020/content/textures/ViewSelector/left_hover_zh_cn.png b/Client2020/content/textures/ViewSelector/left_hover_zh_cn.png new file mode 100644 index 0000000..20ab1d5 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/left_hover_zh_cn.png differ diff --git a/Client2020/content/textures/ViewSelector/left_zh_cn.png b/Client2020/content/textures/ViewSelector/left_zh_cn.png new file mode 100644 index 0000000..3402de2 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/left_zh_cn.png differ diff --git a/Client2020/content/textures/ViewSelector/right.png b/Client2020/content/textures/ViewSelector/right.png new file mode 100644 index 0000000..8965244 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/right.png differ diff --git a/Client2020/content/textures/ViewSelector/right_hover.png b/Client2020/content/textures/ViewSelector/right_hover.png new file mode 100644 index 0000000..c7b72ca Binary files /dev/null and b/Client2020/content/textures/ViewSelector/right_hover.png differ diff --git a/Client2020/content/textures/ViewSelector/right_hover_zh_cn.png b/Client2020/content/textures/ViewSelector/right_hover_zh_cn.png new file mode 100644 index 0000000..282f743 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/right_hover_zh_cn.png differ diff --git a/Client2020/content/textures/ViewSelector/right_zh_cn.png b/Client2020/content/textures/ViewSelector/right_zh_cn.png new file mode 100644 index 0000000..8b09d6a Binary files /dev/null and b/Client2020/content/textures/ViewSelector/right_zh_cn.png differ diff --git a/Client2020/content/textures/ViewSelector/top.png b/Client2020/content/textures/ViewSelector/top.png new file mode 100644 index 0000000..64a529e Binary files /dev/null and b/Client2020/content/textures/ViewSelector/top.png differ diff --git a/Client2020/content/textures/ViewSelector/top_hover.png b/Client2020/content/textures/ViewSelector/top_hover.png new file mode 100644 index 0000000..5b35738 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/top_hover.png differ diff --git a/Client2020/content/textures/ViewSelector/top_hover_zh_cn.png b/Client2020/content/textures/ViewSelector/top_hover_zh_cn.png new file mode 100644 index 0000000..083cd90 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/top_hover_zh_cn.png differ diff --git a/Client2020/content/textures/ViewSelector/top_zh_cn.png b/Client2020/content/textures/ViewSelector/top_zh_cn.png new file mode 100644 index 0000000..b582f11 Binary files /dev/null and b/Client2020/content/textures/ViewSelector/top_zh_cn.png differ diff --git a/Client2020/content/textures/WeldCursor.png b/Client2020/content/textures/WeldCursor.png new file mode 100644 index 0000000..a686be3 Binary files /dev/null and b/Client2020/content/textures/WeldCursor.png differ diff --git a/Client2020/content/textures/advClosed-hand-anchored.png b/Client2020/content/textures/advClosed-hand-anchored.png new file mode 100644 index 0000000..1d71af1 Binary files /dev/null and b/Client2020/content/textures/advClosed-hand-anchored.png differ diff --git a/Client2020/content/textures/advClosed-hand-no-weld.png b/Client2020/content/textures/advClosed-hand-no-weld.png new file mode 100644 index 0000000..d914399 Binary files /dev/null and b/Client2020/content/textures/advClosed-hand-no-weld.png differ diff --git a/Client2020/content/textures/advClosed-hand-weld.png b/Client2020/content/textures/advClosed-hand-weld.png new file mode 100644 index 0000000..2a8e734 Binary files /dev/null and b/Client2020/content/textures/advClosed-hand-weld.png differ diff --git a/Client2020/content/textures/advClosed-hand.png b/Client2020/content/textures/advClosed-hand.png new file mode 100644 index 0000000..1adb82f Binary files /dev/null and b/Client2020/content/textures/advClosed-hand.png differ diff --git a/Client2020/content/textures/advCursor-default.png b/Client2020/content/textures/advCursor-default.png new file mode 100644 index 0000000..289c415 Binary files /dev/null and b/Client2020/content/textures/advCursor-default.png differ diff --git a/Client2020/content/textures/advCursor-openedHand.png b/Client2020/content/textures/advCursor-openedHand.png new file mode 100644 index 0000000..1aea61b Binary files /dev/null and b/Client2020/content/textures/advCursor-openedHand.png differ diff --git a/Client2020/content/textures/advCursor-white.png b/Client2020/content/textures/advCursor-white.png new file mode 100644 index 0000000..0ec7adc Binary files /dev/null and b/Client2020/content/textures/advCursor-white.png differ diff --git a/Client2020/content/textures/advancedMove.png b/Client2020/content/textures/advancedMove.png new file mode 100644 index 0000000..8621f0f Binary files /dev/null and b/Client2020/content/textures/advancedMove.png differ diff --git a/Client2020/content/textures/advancedMoveResize.png b/Client2020/content/textures/advancedMoveResize.png new file mode 100644 index 0000000..a7421be Binary files /dev/null and b/Client2020/content/textures/advancedMoveResize.png differ diff --git a/Client2020/content/textures/advancedMove_joint.png b/Client2020/content/textures/advancedMove_joint.png new file mode 100644 index 0000000..76f7f0a Binary files /dev/null and b/Client2020/content/textures/advancedMove_joint.png differ diff --git a/Client2020/content/textures/advancedMove_keysOnly.png b/Client2020/content/textures/advancedMove_keysOnly.png new file mode 100644 index 0000000..1b55927 Binary files /dev/null and b/Client2020/content/textures/advancedMove_keysOnly.png differ diff --git a/Client2020/content/textures/advancedMove_noJoint.png b/Client2020/content/textures/advancedMove_noJoint.png new file mode 100644 index 0000000..6066608 Binary files /dev/null and b/Client2020/content/textures/advancedMove_noJoint.png differ diff --git a/Client2020/content/textures/blackBkg_round.png b/Client2020/content/textures/blackBkg_round.png new file mode 100644 index 0000000..2796afa Binary files /dev/null and b/Client2020/content/textures/blackBkg_round.png differ diff --git a/Client2020/content/textures/blackBkg_square.png b/Client2020/content/textures/blackBkg_square.png new file mode 100644 index 0000000..0382ce3 Binary files /dev/null and b/Client2020/content/textures/blackBkg_square.png differ diff --git a/Client2020/content/textures/blockUpperLeft.png b/Client2020/content/textures/blockUpperLeft.png new file mode 100644 index 0000000..93a8190 Binary files /dev/null and b/Client2020/content/textures/blockUpperLeft.png differ diff --git a/Client2020/content/textures/chatBubble_bot_notifyGray_dotDotDot.png b/Client2020/content/textures/chatBubble_bot_notifyGray_dotDotDot.png new file mode 100644 index 0000000..cd76f6f Binary files /dev/null and b/Client2020/content/textures/chatBubble_bot_notifyGray_dotDotDot.png differ diff --git a/Client2020/content/textures/collapsibleArrowDown.png b/Client2020/content/textures/collapsibleArrowDown.png new file mode 100644 index 0000000..9224923 Binary files /dev/null and b/Client2020/content/textures/collapsibleArrowDown.png differ diff --git a/Client2020/content/textures/collapsibleArrowRight.png b/Client2020/content/textures/collapsibleArrowRight.png new file mode 100644 index 0000000..759dcd4 Binary files /dev/null and b/Client2020/content/textures/collapsibleArrowRight.png differ diff --git a/Client2020/content/textures/explosion.png b/Client2020/content/textures/explosion.png new file mode 100644 index 0000000..5c5787d Binary files /dev/null and b/Client2020/content/textures/explosion.png differ diff --git a/Client2020/content/textures/face.png b/Client2020/content/textures/face.png new file mode 100644 index 0000000..08254c0 Binary files /dev/null and b/Client2020/content/textures/face.png differ diff --git a/Client2020/content/textures/glow.png b/Client2020/content/textures/glow.png new file mode 100644 index 0000000..886de92 Binary files /dev/null and b/Client2020/content/textures/glow.png differ diff --git a/Client2020/content/textures/gradient.png b/Client2020/content/textures/gradient.png new file mode 100644 index 0000000..540d93a Binary files /dev/null and b/Client2020/content/textures/gradient.png differ diff --git a/Client2020/content/textures/grid16.png b/Client2020/content/textures/grid16.png new file mode 100644 index 0000000..4c17cb0 Binary files /dev/null and b/Client2020/content/textures/grid16.png differ diff --git a/Client2020/content/textures/grid2.png b/Client2020/content/textures/grid2.png new file mode 100644 index 0000000..c9379c8 Binary files /dev/null and b/Client2020/content/textures/grid2.png differ diff --git a/Client2020/content/textures/grid4.png b/Client2020/content/textures/grid4.png new file mode 100644 index 0000000..55d88e3 Binary files /dev/null and b/Client2020/content/textures/grid4.png differ diff --git a/Client2020/content/textures/icon_ROBUX.png b/Client2020/content/textures/icon_ROBUX.png new file mode 100644 index 0000000..9e5c0f8 Binary files /dev/null and b/Client2020/content/textures/icon_ROBUX.png differ diff --git a/Client2020/content/textures/icon_ROBUX@2x.png b/Client2020/content/textures/icon_ROBUX@2x.png new file mode 100644 index 0000000..0b2a791 Binary files /dev/null and b/Client2020/content/textures/icon_ROBUX@2x.png differ diff --git a/Client2020/content/textures/loading/cancelButton.png b/Client2020/content/textures/loading/cancelButton.png new file mode 100644 index 0000000..77e6a39 Binary files /dev/null and b/Client2020/content/textures/loading/cancelButton.png differ diff --git a/Client2020/content/textures/loading/darkLoadingTexture.png b/Client2020/content/textures/loading/darkLoadingTexture.png new file mode 100644 index 0000000..90a0910 Binary files /dev/null and b/Client2020/content/textures/loading/darkLoadingTexture.png differ diff --git a/Client2020/content/textures/loading/loadingCircle.png b/Client2020/content/textures/loading/loadingCircle.png new file mode 100644 index 0000000..6788c78 Binary files /dev/null and b/Client2020/content/textures/loading/loadingCircle.png differ diff --git a/Client2020/content/textures/loading/loadingTexture.png b/Client2020/content/textures/loading/loadingTexture.png new file mode 100644 index 0000000..b58f420 Binary files /dev/null and b/Client2020/content/textures/loading/loadingTexture.png differ diff --git a/Client2020/content/textures/loading/loadingvignette.png b/Client2020/content/textures/loading/loadingvignette.png new file mode 100644 index 0000000..5952539 Binary files /dev/null and b/Client2020/content/textures/loading/loadingvignette.png differ diff --git a/Client2020/content/textures/loading/robloxTilt.png b/Client2020/content/textures/loading/robloxTilt.png new file mode 100644 index 0000000..068fe31 Binary files /dev/null and b/Client2020/content/textures/loading/robloxTilt.png differ diff --git a/Client2020/content/textures/loading/robloxTiltRed.png b/Client2020/content/textures/loading/robloxTiltRed.png new file mode 100644 index 0000000..8cfc292 Binary files /dev/null and b/Client2020/content/textures/loading/robloxTiltRed.png differ diff --git a/Client2020/content/textures/loading/robloxlogo.png b/Client2020/content/textures/loading/robloxlogo.png new file mode 100644 index 0000000..2d05445 Binary files /dev/null and b/Client2020/content/textures/loading/robloxlogo.png differ diff --git a/Client2020/content/textures/localizationExport.png b/Client2020/content/textures/localizationExport.png new file mode 100644 index 0000000..591c2c9 Binary files /dev/null and b/Client2020/content/textures/localizationExport.png differ diff --git a/Client2020/content/textures/localizationImport.png b/Client2020/content/textures/localizationImport.png new file mode 100644 index 0000000..10dda0e Binary files /dev/null and b/Client2020/content/textures/localizationImport.png differ diff --git a/Client2020/content/textures/localizationTargetEnglish.png b/Client2020/content/textures/localizationTargetEnglish.png new file mode 100644 index 0000000..5d528f1 Binary files /dev/null and b/Client2020/content/textures/localizationTargetEnglish.png differ diff --git a/Client2020/content/textures/localizationTargetSpanish.png b/Client2020/content/textures/localizationTargetSpanish.png new file mode 100644 index 0000000..34e1db9 Binary files /dev/null and b/Client2020/content/textures/localizationTargetSpanish.png differ diff --git a/Client2020/content/textures/localizationTestingIcon.png b/Client2020/content/textures/localizationTestingIcon.png new file mode 100644 index 0000000..dad16bd Binary files /dev/null and b/Client2020/content/textures/localizationTestingIcon.png differ diff --git a/Client2020/content/textures/localizationUIScrapingOff.png b/Client2020/content/textures/localizationUIScrapingOff.png new file mode 100644 index 0000000..ca3fc50 Binary files /dev/null and b/Client2020/content/textures/localizationUIScrapingOff.png differ diff --git a/Client2020/content/textures/localizationUIScrapingOn.png b/Client2020/content/textures/localizationUIScrapingOn.png new file mode 100644 index 0000000..e034998 Binary files /dev/null and b/Client2020/content/textures/localizationUIScrapingOn.png differ diff --git a/Client2020/content/textures/menuDownArrow.png b/Client2020/content/textures/menuDownArrow.png new file mode 100644 index 0000000..286ecd8 Binary files /dev/null and b/Client2020/content/textures/menuDownArrow.png differ diff --git a/Client2020/content/textures/meshPartFallback.png b/Client2020/content/textures/meshPartFallback.png new file mode 100644 index 0000000..1c1e179 Binary files /dev/null and b/Client2020/content/textures/meshPartFallback.png differ diff --git a/Client2020/content/textures/particles/SquareParticle.png b/Client2020/content/textures/particles/SquareParticle.png new file mode 100644 index 0000000..6c9aca8 Binary files /dev/null and b/Client2020/content/textures/particles/SquareParticle.png differ diff --git a/Client2020/content/textures/particles/common_alpha.dds b/Client2020/content/textures/particles/common_alpha.dds new file mode 100644 index 0000000..2cc9c9c Binary files /dev/null and b/Client2020/content/textures/particles/common_alpha.dds differ diff --git a/Client2020/content/textures/particles/explosion01_core_alpha.png b/Client2020/content/textures/particles/explosion01_core_alpha.png new file mode 100644 index 0000000..47ae484 Binary files /dev/null and b/Client2020/content/textures/particles/explosion01_core_alpha.png differ diff --git a/Client2020/content/textures/particles/explosion01_core_main.dds b/Client2020/content/textures/particles/explosion01_core_main.dds new file mode 100644 index 0000000..44fd908 Binary files /dev/null and b/Client2020/content/textures/particles/explosion01_core_main.dds differ diff --git a/Client2020/content/textures/particles/explosion01_implosion_color.png b/Client2020/content/textures/particles/explosion01_implosion_color.png new file mode 100644 index 0000000..2f1e9ed Binary files /dev/null and b/Client2020/content/textures/particles/explosion01_implosion_color.png differ diff --git a/Client2020/content/textures/particles/explosion01_implosion_main.dds b/Client2020/content/textures/particles/explosion01_implosion_main.dds new file mode 100644 index 0000000..a5bf1b5 Binary files /dev/null and b/Client2020/content/textures/particles/explosion01_implosion_main.dds differ diff --git a/Client2020/content/textures/particles/explosion01_shockwave_main.dds b/Client2020/content/textures/particles/explosion01_shockwave_main.dds new file mode 100644 index 0000000..7a30ca6 Binary files /dev/null and b/Client2020/content/textures/particles/explosion01_shockwave_main.dds differ diff --git a/Client2020/content/textures/particles/explosion01_smoke_alpha.dds b/Client2020/content/textures/particles/explosion01_smoke_alpha.dds new file mode 100644 index 0000000..99807c7 Binary files /dev/null and b/Client2020/content/textures/particles/explosion01_smoke_alpha.dds differ diff --git a/Client2020/content/textures/particles/explosion01_smoke_color_new.dds b/Client2020/content/textures/particles/explosion01_smoke_color_new.dds new file mode 100644 index 0000000..fd4df8f Binary files /dev/null and b/Client2020/content/textures/particles/explosion01_smoke_color_new.dds differ diff --git a/Client2020/content/textures/particles/explosion01_smoke_main.dds b/Client2020/content/textures/particles/explosion01_smoke_main.dds new file mode 100644 index 0000000..c37f99a Binary files /dev/null and b/Client2020/content/textures/particles/explosion01_smoke_main.dds differ diff --git a/Client2020/content/textures/particles/explosion_alpha.dds b/Client2020/content/textures/particles/explosion_alpha.dds new file mode 100644 index 0000000..add095f Binary files /dev/null and b/Client2020/content/textures/particles/explosion_alpha.dds differ diff --git a/Client2020/content/textures/particles/explosion_color.dds b/Client2020/content/textures/particles/explosion_color.dds new file mode 100644 index 0000000..f69e2cc Binary files /dev/null and b/Client2020/content/textures/particles/explosion_color.dds differ diff --git a/Client2020/content/textures/particles/fire_alpha.dds b/Client2020/content/textures/particles/fire_alpha.dds new file mode 100644 index 0000000..812a506 Binary files /dev/null and b/Client2020/content/textures/particles/fire_alpha.dds differ diff --git a/Client2020/content/textures/particles/fire_color.dds b/Client2020/content/textures/particles/fire_color.dds new file mode 100644 index 0000000..5d569ad Binary files /dev/null and b/Client2020/content/textures/particles/fire_color.dds differ diff --git a/Client2020/content/textures/particles/fire_main.dds b/Client2020/content/textures/particles/fire_main.dds new file mode 100644 index 0000000..22a5049 Binary files /dev/null and b/Client2020/content/textures/particles/fire_main.dds differ diff --git a/Client2020/content/textures/particles/fire_sparks_color.dds b/Client2020/content/textures/particles/fire_sparks_color.dds new file mode 100644 index 0000000..8876db1 Binary files /dev/null and b/Client2020/content/textures/particles/fire_sparks_color.dds differ diff --git a/Client2020/content/textures/particles/fire_sparks_main.dds b/Client2020/content/textures/particles/fire_sparks_main.dds new file mode 100644 index 0000000..4468e5a Binary files /dev/null and b/Client2020/content/textures/particles/fire_sparks_main.dds differ diff --git a/Client2020/content/textures/particles/forcefield_alpha.dds b/Client2020/content/textures/particles/forcefield_alpha.dds new file mode 100644 index 0000000..03fdac0 Binary files /dev/null and b/Client2020/content/textures/particles/forcefield_alpha.dds differ diff --git a/Client2020/content/textures/particles/forcefield_glow_alpha.dds b/Client2020/content/textures/particles/forcefield_glow_alpha.dds new file mode 100644 index 0000000..326310f Binary files /dev/null and b/Client2020/content/textures/particles/forcefield_glow_alpha.dds differ diff --git a/Client2020/content/textures/particles/forcefield_glow_color.dds b/Client2020/content/textures/particles/forcefield_glow_color.dds new file mode 100644 index 0000000..39dfb8e Binary files /dev/null and b/Client2020/content/textures/particles/forcefield_glow_color.dds differ diff --git a/Client2020/content/textures/particles/forcefield_glow_main.dds b/Client2020/content/textures/particles/forcefield_glow_main.dds new file mode 100644 index 0000000..d1a6472 Binary files /dev/null and b/Client2020/content/textures/particles/forcefield_glow_main.dds differ diff --git a/Client2020/content/textures/particles/forcefield_vortex_color.dds b/Client2020/content/textures/particles/forcefield_vortex_color.dds new file mode 100644 index 0000000..fe33e22 Binary files /dev/null and b/Client2020/content/textures/particles/forcefield_vortex_color.dds differ diff --git a/Client2020/content/textures/particles/forcefield_vortex_main.dds b/Client2020/content/textures/particles/forcefield_vortex_main.dds new file mode 100644 index 0000000..d67cf49 Binary files /dev/null and b/Client2020/content/textures/particles/forcefield_vortex_main.dds differ diff --git a/Client2020/content/textures/particles/legacy_fire_alpha_color.dds b/Client2020/content/textures/particles/legacy_fire_alpha_color.dds new file mode 100644 index 0000000..da0fb05 Binary files /dev/null and b/Client2020/content/textures/particles/legacy_fire_alpha_color.dds differ diff --git a/Client2020/content/textures/particles/smoke_color.dds b/Client2020/content/textures/particles/smoke_color.dds new file mode 100644 index 0000000..a4d0a4f Binary files /dev/null and b/Client2020/content/textures/particles/smoke_color.dds differ diff --git a/Client2020/content/textures/particles/smoke_main.dds b/Client2020/content/textures/particles/smoke_main.dds new file mode 100644 index 0000000..1aaef8d Binary files /dev/null and b/Client2020/content/textures/particles/smoke_main.dds differ diff --git a/Client2020/content/textures/particles/sparkles_color.dds b/Client2020/content/textures/particles/sparkles_color.dds new file mode 100644 index 0000000..4fd1f85 Binary files /dev/null and b/Client2020/content/textures/particles/sparkles_color.dds differ diff --git a/Client2020/content/textures/particles/sparkles_main.dds b/Client2020/content/textures/particles/sparkles_main.dds new file mode 100644 index 0000000..d1875fd Binary files /dev/null and b/Client2020/content/textures/particles/sparkles_main.dds differ diff --git a/Client2020/content/textures/rotationArrow.png b/Client2020/content/textures/rotationArrow.png new file mode 100644 index 0000000..5bd9eb5 Binary files /dev/null and b/Client2020/content/textures/rotationArrow.png differ diff --git a/Client2020/content/textures/shadowblurmask.png b/Client2020/content/textures/shadowblurmask.png new file mode 100644 index 0000000..ae33768 Binary files /dev/null and b/Client2020/content/textures/shadowblurmask.png differ diff --git a/Client2020/content/textures/sparkle.png b/Client2020/content/textures/sparkle.png new file mode 100644 index 0000000..510b061 Binary files /dev/null and b/Client2020/content/textures/sparkle.png differ diff --git a/Client2020/content/textures/transformFiveDegrees.png b/Client2020/content/textures/transformFiveDegrees.png new file mode 100644 index 0000000..1201cea Binary files /dev/null and b/Client2020/content/textures/transformFiveDegrees.png differ diff --git a/Client2020/content/textures/transformNinetyDegrees.png b/Client2020/content/textures/transformNinetyDegrees.png new file mode 100644 index 0000000..253ad36 Binary files /dev/null and b/Client2020/content/textures/transformNinetyDegrees.png differ diff --git a/Client2020/content/textures/transformOneDegree.png b/Client2020/content/textures/transformOneDegree.png new file mode 100644 index 0000000..79cb61f Binary files /dev/null and b/Client2020/content/textures/transformOneDegree.png differ diff --git a/Client2020/content/textures/transformTwentyTwoDegrees.png b/Client2020/content/textures/transformTwentyTwoDegrees.png new file mode 100644 index 0000000..556b024 Binary files /dev/null and b/Client2020/content/textures/transformTwentyTwoDegrees.png differ diff --git a/Client2020/content/textures/ui/AvatarContextMenu_Arrow.png b/Client2020/content/textures/ui/AvatarContextMenu_Arrow.png new file mode 100644 index 0000000..d41d3c7 Binary files /dev/null and b/Client2020/content/textures/ui/AvatarContextMenu_Arrow.png differ diff --git a/Client2020/content/textures/ui/Backpack/Backpack.png b/Client2020/content/textures/ui/Backpack/Backpack.png new file mode 100644 index 0000000..8d9048a Binary files /dev/null and b/Client2020/content/textures/ui/Backpack/Backpack.png differ diff --git a/Client2020/content/textures/ui/Backpack/Backpack@2x.png b/Client2020/content/textures/ui/Backpack/Backpack@2x.png new file mode 100644 index 0000000..9814297 Binary files /dev/null and b/Client2020/content/textures/ui/Backpack/Backpack@2x.png differ diff --git a/Client2020/content/textures/ui/Backpack/Backpack_Down.png b/Client2020/content/textures/ui/Backpack/Backpack_Down.png new file mode 100644 index 0000000..b687a19 Binary files /dev/null and b/Client2020/content/textures/ui/Backpack/Backpack_Down.png differ diff --git a/Client2020/content/textures/ui/Backpack/Backpack_Down@2x.png b/Client2020/content/textures/ui/Backpack/Backpack_Down@2x.png new file mode 100644 index 0000000..5c46d4a Binary files /dev/null and b/Client2020/content/textures/ui/Backpack/Backpack_Down@2x.png differ diff --git a/Client2020/content/textures/ui/Backpack/ScrollDownArrow.png b/Client2020/content/textures/ui/Backpack/ScrollDownArrow.png new file mode 100644 index 0000000..68e14aa Binary files /dev/null and b/Client2020/content/textures/ui/Backpack/ScrollDownArrow.png differ diff --git a/Client2020/content/textures/ui/Backpack/ScrollUpArrow.png b/Client2020/content/textures/ui/Backpack/ScrollUpArrow.png new file mode 100644 index 0000000..3824e05 Binary files /dev/null and b/Client2020/content/textures/ui/Backpack/ScrollUpArrow.png differ diff --git a/Client2020/content/textures/ui/Backpack_Close.png b/Client2020/content/textures/ui/Backpack_Close.png new file mode 100644 index 0000000..2b3679b Binary files /dev/null and b/Client2020/content/textures/ui/Backpack_Close.png differ diff --git a/Client2020/content/textures/ui/Backpack_Close@2x.png b/Client2020/content/textures/ui/Backpack_Close@2x.png new file mode 100644 index 0000000..1716f55 Binary files /dev/null and b/Client2020/content/textures/ui/Backpack_Close@2x.png differ diff --git a/Client2020/content/textures/ui/Backpack_Open.png b/Client2020/content/textures/ui/Backpack_Open.png new file mode 100644 index 0000000..24fba15 Binary files /dev/null and b/Client2020/content/textures/ui/Backpack_Open.png differ diff --git a/Client2020/content/textures/ui/Backpack_Open@2x.png b/Client2020/content/textures/ui/Backpack_Open@2x.png new file mode 100644 index 0000000..a56634c Binary files /dev/null and b/Client2020/content/textures/ui/Backpack_Open@2x.png differ diff --git a/Client2020/content/textures/ui/BottomRoundedRect8px.png b/Client2020/content/textures/ui/BottomRoundedRect8px.png new file mode 100644 index 0000000..6031e36 Binary files /dev/null and b/Client2020/content/textures/ui/BottomRoundedRect8px.png differ diff --git a/Client2020/content/textures/ui/ButtonLeft.png b/Client2020/content/textures/ui/ButtonLeft.png new file mode 100644 index 0000000..f2654bd Binary files /dev/null and b/Client2020/content/textures/ui/ButtonLeft.png differ diff --git a/Client2020/content/textures/ui/ButtonLeftDown.png b/Client2020/content/textures/ui/ButtonLeftDown.png new file mode 100644 index 0000000..67cdc7f Binary files /dev/null and b/Client2020/content/textures/ui/ButtonLeftDown.png differ diff --git a/Client2020/content/textures/ui/ButtonRight.png b/Client2020/content/textures/ui/ButtonRight.png new file mode 100644 index 0000000..8defe2e Binary files /dev/null and b/Client2020/content/textures/ui/ButtonRight.png differ diff --git a/Client2020/content/textures/ui/ButtonRightDown.png b/Client2020/content/textures/ui/ButtonRightDown.png new file mode 100644 index 0000000..218f2d2 Binary files /dev/null and b/Client2020/content/textures/ui/ButtonRightDown.png differ diff --git a/Client2020/content/textures/ui/Camera/CameraToast9Slice.png b/Client2020/content/textures/ui/Camera/CameraToast9Slice.png new file mode 100644 index 0000000..a24ad33 Binary files /dev/null and b/Client2020/content/textures/ui/Camera/CameraToast9Slice.png differ diff --git a/Client2020/content/textures/ui/Camera/CameraToastIcon.png b/Client2020/content/textures/ui/Camera/CameraToastIcon.png new file mode 100644 index 0000000..4920cbc Binary files /dev/null and b/Client2020/content/textures/ui/Camera/CameraToastIcon.png differ diff --git a/Client2020/content/textures/ui/Chat/Chat.png b/Client2020/content/textures/ui/Chat/Chat.png new file mode 100644 index 0000000..d42e4ea Binary files /dev/null and b/Client2020/content/textures/ui/Chat/Chat.png differ diff --git a/Client2020/content/textures/ui/Chat/Chat@2x.png b/Client2020/content/textures/ui/Chat/Chat@2x.png new file mode 100644 index 0000000..57ea151 Binary files /dev/null and b/Client2020/content/textures/ui/Chat/Chat@2x.png differ diff --git a/Client2020/content/textures/ui/Chat/ChatDown.png b/Client2020/content/textures/ui/Chat/ChatDown.png new file mode 100644 index 0000000..002119b Binary files /dev/null and b/Client2020/content/textures/ui/Chat/ChatDown.png differ diff --git a/Client2020/content/textures/ui/Chat/ChatDown@2x.png b/Client2020/content/textures/ui/Chat/ChatDown@2x.png new file mode 100644 index 0000000..f25f80d Binary files /dev/null and b/Client2020/content/textures/ui/Chat/ChatDown@2x.png differ diff --git a/Client2020/content/textures/ui/Chat/ChatDownFlip.png b/Client2020/content/textures/ui/Chat/ChatDownFlip.png new file mode 100644 index 0000000..9523d39 Binary files /dev/null and b/Client2020/content/textures/ui/Chat/ChatDownFlip.png differ diff --git a/Client2020/content/textures/ui/Chat/ChatDownFlip@2x.png b/Client2020/content/textures/ui/Chat/ChatDownFlip@2x.png new file mode 100644 index 0000000..dd6b15e Binary files /dev/null and b/Client2020/content/textures/ui/Chat/ChatDownFlip@2x.png differ diff --git a/Client2020/content/textures/ui/Chat/ChatFlip.png b/Client2020/content/textures/ui/Chat/ChatFlip.png new file mode 100644 index 0000000..f0b508a Binary files /dev/null and b/Client2020/content/textures/ui/Chat/ChatFlip.png differ diff --git a/Client2020/content/textures/ui/Chat/ChatFlip@2x.png b/Client2020/content/textures/ui/Chat/ChatFlip@2x.png new file mode 100644 index 0000000..1e8d6d1 Binary files /dev/null and b/Client2020/content/textures/ui/Chat/ChatFlip@2x.png differ diff --git a/Client2020/content/textures/ui/Chat/MessageCounter.png b/Client2020/content/textures/ui/Chat/MessageCounter.png new file mode 100644 index 0000000..4ba3cbd Binary files /dev/null and b/Client2020/content/textures/ui/Chat/MessageCounter.png differ diff --git a/Client2020/content/textures/ui/Chat/MessageCounter@2x.png b/Client2020/content/textures/ui/Chat/MessageCounter@2x.png new file mode 100644 index 0000000..de2cc47 Binary files /dev/null and b/Client2020/content/textures/ui/Chat/MessageCounter@2x.png differ diff --git a/Client2020/content/textures/ui/Chat/ToggleChat.png b/Client2020/content/textures/ui/Chat/ToggleChat.png new file mode 100644 index 0000000..1bc7450 Binary files /dev/null and b/Client2020/content/textures/ui/Chat/ToggleChat.png differ diff --git a/Client2020/content/textures/ui/Chat/ToggleChat@2x.png b/Client2020/content/textures/ui/Chat/ToggleChat@2x.png new file mode 100644 index 0000000..5ab133e Binary files /dev/null and b/Client2020/content/textures/ui/Chat/ToggleChat@2x.png differ diff --git a/Client2020/content/textures/ui/Chat/ToggleChatDown.png b/Client2020/content/textures/ui/Chat/ToggleChatDown.png new file mode 100644 index 0000000..2956a50 Binary files /dev/null and b/Client2020/content/textures/ui/Chat/ToggleChatDown.png differ diff --git a/Client2020/content/textures/ui/Chat/ToggleChatDown@2x.png b/Client2020/content/textures/ui/Chat/ToggleChatDown@2x.png new file mode 100644 index 0000000..065be38 Binary files /dev/null and b/Client2020/content/textures/ui/Chat/ToggleChatDown@2x.png differ diff --git a/Client2020/content/textures/ui/Chat/ToggleChatDownFlip.png b/Client2020/content/textures/ui/Chat/ToggleChatDownFlip.png new file mode 100644 index 0000000..34d7376 Binary files /dev/null and b/Client2020/content/textures/ui/Chat/ToggleChatDownFlip.png differ diff --git a/Client2020/content/textures/ui/Chat/ToggleChatDownFlip@2x.png b/Client2020/content/textures/ui/Chat/ToggleChatDownFlip@2x.png new file mode 100644 index 0000000..c9a6dff Binary files /dev/null and b/Client2020/content/textures/ui/Chat/ToggleChatDownFlip@2x.png differ diff --git a/Client2020/content/textures/ui/Chat/ToggleChatFlip.png b/Client2020/content/textures/ui/Chat/ToggleChatFlip.png new file mode 100644 index 0000000..e929cfa Binary files /dev/null and b/Client2020/content/textures/ui/Chat/ToggleChatFlip.png differ diff --git a/Client2020/content/textures/ui/Chat/ToggleChatFlip@2x.png b/Client2020/content/textures/ui/Chat/ToggleChatFlip@2x.png new file mode 100644 index 0000000..1d02265 Binary files /dev/null and b/Client2020/content/textures/ui/Chat/ToggleChatFlip@2x.png differ diff --git a/Client2020/content/textures/ui/Chat/VRChatBackground.png b/Client2020/content/textures/ui/Chat/VRChatBackground.png new file mode 100644 index 0000000..694a50f Binary files /dev/null and b/Client2020/content/textures/ui/Chat/VRChatBackground.png differ diff --git a/Client2020/content/textures/ui/CloseButton.png b/Client2020/content/textures/ui/CloseButton.png new file mode 100644 index 0000000..6ba8d7b Binary files /dev/null and b/Client2020/content/textures/ui/CloseButton.png differ diff --git a/Client2020/content/textures/ui/CloseButton_dn.png b/Client2020/content/textures/ui/CloseButton_dn.png new file mode 100644 index 0000000..ee4b9ff Binary files /dev/null and b/Client2020/content/textures/ui/CloseButton_dn.png differ diff --git a/Client2020/content/textures/ui/DPadSheet.png b/Client2020/content/textures/ui/DPadSheet.png new file mode 100644 index 0000000..046e2e7 Binary files /dev/null and b/Client2020/content/textures/ui/DPadSheet.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/Large/OrangeHighlight.png b/Client2020/content/textures/ui/Emotes/Editor/Large/OrangeHighlight.png new file mode 100644 index 0000000..21d9db4 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/Large/OrangeHighlight.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/Large/OrangeHighlight@2x.png b/Client2020/content/textures/ui/Emotes/Editor/Large/OrangeHighlight@2x.png new file mode 100644 index 0000000..0270edc Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/Large/OrangeHighlight@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/Large/OrangeHighlight@3x.png b/Client2020/content/textures/ui/Emotes/Editor/Large/OrangeHighlight@3x.png new file mode 100644 index 0000000..fb3ad04 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/Large/OrangeHighlight@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/Large/Wheel.png b/Client2020/content/textures/ui/Emotes/Editor/Large/Wheel.png new file mode 100644 index 0000000..401a3e5 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/Large/Wheel.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/Large/Wheel@2x.png b/Client2020/content/textures/ui/Emotes/Editor/Large/Wheel@2x.png new file mode 100644 index 0000000..f5bde94 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/Large/Wheel@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/Large/Wheel@3x.png b/Client2020/content/textures/ui/Emotes/Editor/Large/Wheel@3x.png new file mode 100644 index 0000000..49b2b9a Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/Large/Wheel@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/Small/OrangeHighlight.png b/Client2020/content/textures/ui/Emotes/Editor/Small/OrangeHighlight.png new file mode 100644 index 0000000..b48d3e6 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/Small/OrangeHighlight.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/Small/OrangeHighlight@2x.png b/Client2020/content/textures/ui/Emotes/Editor/Small/OrangeHighlight@2x.png new file mode 100644 index 0000000..655b95f Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/Small/OrangeHighlight@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/Small/OrangeHighlight@3x.png b/Client2020/content/textures/ui/Emotes/Editor/Small/OrangeHighlight@3x.png new file mode 100644 index 0000000..27aa8da Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/Small/OrangeHighlight@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/Small/Wheel.png b/Client2020/content/textures/ui/Emotes/Editor/Small/Wheel.png new file mode 100644 index 0000000..83ff5fb Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/Small/Wheel.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/Small/Wheel@2x.png b/Client2020/content/textures/ui/Emotes/Editor/Small/Wheel@2x.png new file mode 100644 index 0000000..f03d5eb Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/Small/Wheel@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/Small/Wheel@3x.png b/Client2020/content/textures/ui/Emotes/Editor/Small/Wheel@3x.png new file mode 100644 index 0000000..928a2b4 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/Small/Wheel@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight.png b/Client2020/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight.png new file mode 100644 index 0000000..0ae5c24 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight@2x.png b/Client2020/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight@2x.png new file mode 100644 index 0000000..5b010b7 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight@3x.png b/Client2020/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight@3x.png new file mode 100644 index 0000000..d08d6a4 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/TenFoot/Wheel.png b/Client2020/content/textures/ui/Emotes/Editor/TenFoot/Wheel.png new file mode 100644 index 0000000..73341a7 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/TenFoot/Wheel.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/TenFoot/Wheel@2x.png b/Client2020/content/textures/ui/Emotes/Editor/TenFoot/Wheel@2x.png new file mode 100644 index 0000000..23902e6 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/TenFoot/Wheel@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Editor/TenFoot/Wheel@3x.png b/Client2020/content/textures/ui/Emotes/Editor/TenFoot/Wheel@3x.png new file mode 100644 index 0000000..f998964 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Editor/TenFoot/Wheel@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/EmotesIcon.png b/Client2020/content/textures/ui/Emotes/EmotesIcon.png new file mode 100644 index 0000000..f052e6a Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/EmotesIcon.png differ diff --git a/Client2020/content/textures/ui/Emotes/EmotesIcon@2x.png b/Client2020/content/textures/ui/Emotes/EmotesIcon@2x.png new file mode 100644 index 0000000..a3ba2b8 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/EmotesIcon@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/EmotesIcon@3x.png b/Client2020/content/textures/ui/Emotes/EmotesIcon@3x.png new file mode 100644 index 0000000..4d673ad Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/EmotesIcon@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/EmotesRadialIcon.png b/Client2020/content/textures/ui/Emotes/EmotesRadialIcon.png new file mode 100644 index 0000000..b385307 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/EmotesRadialIcon.png differ diff --git a/Client2020/content/textures/ui/Emotes/EmotesRadialIcon@2x.png b/Client2020/content/textures/ui/Emotes/EmotesRadialIcon@2x.png new file mode 100644 index 0000000..930a201 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/EmotesRadialIcon@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/EmotesRadialIcon@3x.png b/Client2020/content/textures/ui/Emotes/EmotesRadialIcon@3x.png new file mode 100644 index 0000000..056cde1 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/EmotesRadialIcon@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/ErrorIcon.png b/Client2020/content/textures/ui/Emotes/ErrorIcon.png new file mode 100644 index 0000000..32ede95 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/ErrorIcon.png differ diff --git a/Client2020/content/textures/ui/Emotes/ErrorIcon@2x.png b/Client2020/content/textures/ui/Emotes/ErrorIcon@2x.png new file mode 100644 index 0000000..72b37d6 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/ErrorIcon@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/ErrorIcon@3x.png b/Client2020/content/textures/ui/Emotes/ErrorIcon@3x.png new file mode 100644 index 0000000..6107a79 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/ErrorIcon@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Large/CircleBackground.png b/Client2020/content/textures/ui/Emotes/Large/CircleBackground.png new file mode 100644 index 0000000..7cccc02 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Large/CircleBackground.png differ diff --git a/Client2020/content/textures/ui/Emotes/Large/CircleBackground@2x.png b/Client2020/content/textures/ui/Emotes/Large/CircleBackground@2x.png new file mode 100644 index 0000000..54756a6 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Large/CircleBackground@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Large/CircleBackground@3x.png b/Client2020/content/textures/ui/Emotes/Large/CircleBackground@3x.png new file mode 100644 index 0000000..2a5d729 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Large/CircleBackground@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Large/SegmentedCircle.png b/Client2020/content/textures/ui/Emotes/Large/SegmentedCircle.png new file mode 100644 index 0000000..c7352ed Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Large/SegmentedCircle.png differ diff --git a/Client2020/content/textures/ui/Emotes/Large/SegmentedCircle@2x.png b/Client2020/content/textures/ui/Emotes/Large/SegmentedCircle@2x.png new file mode 100644 index 0000000..c721a9d Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Large/SegmentedCircle@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Large/SegmentedCircle@3x.png b/Client2020/content/textures/ui/Emotes/Large/SegmentedCircle@3x.png new file mode 100644 index 0000000..78f76f4 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Large/SegmentedCircle@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Large/SelectedGradient.png b/Client2020/content/textures/ui/Emotes/Large/SelectedGradient.png new file mode 100644 index 0000000..d2b6838 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Large/SelectedGradient.png differ diff --git a/Client2020/content/textures/ui/Emotes/Large/SelectedGradient@2x.png b/Client2020/content/textures/ui/Emotes/Large/SelectedGradient@2x.png new file mode 100644 index 0000000..2af23af Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Large/SelectedGradient@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Large/SelectedGradient@3x.png b/Client2020/content/textures/ui/Emotes/Large/SelectedGradient@3x.png new file mode 100644 index 0000000..e254af6 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Large/SelectedGradient@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Large/SelectedLine.png b/Client2020/content/textures/ui/Emotes/Large/SelectedLine.png new file mode 100644 index 0000000..74fbd9f Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Large/SelectedLine.png differ diff --git a/Client2020/content/textures/ui/Emotes/Large/SelectedLine@2x.png b/Client2020/content/textures/ui/Emotes/Large/SelectedLine@2x.png new file mode 100644 index 0000000..cc073ae Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Large/SelectedLine@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Large/SelectedLine@3x.png b/Client2020/content/textures/ui/Emotes/Large/SelectedLine@3x.png new file mode 100644 index 0000000..0e7190f Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Large/SelectedLine@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Small/CircleBackground.png b/Client2020/content/textures/ui/Emotes/Small/CircleBackground.png new file mode 100644 index 0000000..141945f Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Small/CircleBackground.png differ diff --git a/Client2020/content/textures/ui/Emotes/Small/CircleBackground@2x.png b/Client2020/content/textures/ui/Emotes/Small/CircleBackground@2x.png new file mode 100644 index 0000000..c8fb422 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Small/CircleBackground@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Small/CircleBackground@3x.png b/Client2020/content/textures/ui/Emotes/Small/CircleBackground@3x.png new file mode 100644 index 0000000..c4d5dab Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Small/CircleBackground@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Small/SegmentedCircle.png b/Client2020/content/textures/ui/Emotes/Small/SegmentedCircle.png new file mode 100644 index 0000000..3f450c8 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Small/SegmentedCircle.png differ diff --git a/Client2020/content/textures/ui/Emotes/Small/SegmentedCircle@2x.png b/Client2020/content/textures/ui/Emotes/Small/SegmentedCircle@2x.png new file mode 100644 index 0000000..fc63af4 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Small/SegmentedCircle@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Small/SegmentedCircle@3x.png b/Client2020/content/textures/ui/Emotes/Small/SegmentedCircle@3x.png new file mode 100644 index 0000000..ec66f0d Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Small/SegmentedCircle@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Small/SelectedGradient.png b/Client2020/content/textures/ui/Emotes/Small/SelectedGradient.png new file mode 100644 index 0000000..04b0a60 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Small/SelectedGradient.png differ diff --git a/Client2020/content/textures/ui/Emotes/Small/SelectedGradient@2x.png b/Client2020/content/textures/ui/Emotes/Small/SelectedGradient@2x.png new file mode 100644 index 0000000..d5bcaad Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Small/SelectedGradient@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Small/SelectedGradient@3x.png b/Client2020/content/textures/ui/Emotes/Small/SelectedGradient@3x.png new file mode 100644 index 0000000..cc830c8 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Small/SelectedGradient@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Small/SelectedLine.png b/Client2020/content/textures/ui/Emotes/Small/SelectedLine.png new file mode 100644 index 0000000..022db41 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Small/SelectedLine.png differ diff --git a/Client2020/content/textures/ui/Emotes/Small/SelectedLine@2x.png b/Client2020/content/textures/ui/Emotes/Small/SelectedLine@2x.png new file mode 100644 index 0000000..07e9f98 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Small/SelectedLine@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/Small/SelectedLine@3x.png b/Client2020/content/textures/ui/Emotes/Small/SelectedLine@3x.png new file mode 100644 index 0000000..5a5aeda Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/Small/SelectedLine@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/TenFoot/CircleBackground.png b/Client2020/content/textures/ui/Emotes/TenFoot/CircleBackground.png new file mode 100644 index 0000000..c66a2a1 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/TenFoot/CircleBackground.png differ diff --git a/Client2020/content/textures/ui/Emotes/TenFoot/CircleBackground@2x.png b/Client2020/content/textures/ui/Emotes/TenFoot/CircleBackground@2x.png new file mode 100644 index 0000000..f53c980 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/TenFoot/CircleBackground@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/TenFoot/CircleBackground@3x.png b/Client2020/content/textures/ui/Emotes/TenFoot/CircleBackground@3x.png new file mode 100644 index 0000000..0732b7c Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/TenFoot/CircleBackground@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/TenFoot/SegmentedCircle.png b/Client2020/content/textures/ui/Emotes/TenFoot/SegmentedCircle.png new file mode 100644 index 0000000..c9cbcf6 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/TenFoot/SegmentedCircle.png differ diff --git a/Client2020/content/textures/ui/Emotes/TenFoot/SegmentedCircle@2x.png b/Client2020/content/textures/ui/Emotes/TenFoot/SegmentedCircle@2x.png new file mode 100644 index 0000000..bfd4caf Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/TenFoot/SegmentedCircle@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/TenFoot/SegmentedCircle@3x.png b/Client2020/content/textures/ui/Emotes/TenFoot/SegmentedCircle@3x.png new file mode 100644 index 0000000..e43514d Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/TenFoot/SegmentedCircle@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/TenFoot/SelectedGradient.png b/Client2020/content/textures/ui/Emotes/TenFoot/SelectedGradient.png new file mode 100644 index 0000000..db43475 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/TenFoot/SelectedGradient.png differ diff --git a/Client2020/content/textures/ui/Emotes/TenFoot/SelectedGradient@2x.png b/Client2020/content/textures/ui/Emotes/TenFoot/SelectedGradient@2x.png new file mode 100644 index 0000000..02c8a6c Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/TenFoot/SelectedGradient@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/TenFoot/SelectedGradient@3x.png b/Client2020/content/textures/ui/Emotes/TenFoot/SelectedGradient@3x.png new file mode 100644 index 0000000..2a214f5 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/TenFoot/SelectedGradient@3x.png differ diff --git a/Client2020/content/textures/ui/Emotes/TenFoot/SelectedLine.png b/Client2020/content/textures/ui/Emotes/TenFoot/SelectedLine.png new file mode 100644 index 0000000..96a6f70 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/TenFoot/SelectedLine.png differ diff --git a/Client2020/content/textures/ui/Emotes/TenFoot/SelectedLine@2x.png b/Client2020/content/textures/ui/Emotes/TenFoot/SelectedLine@2x.png new file mode 100644 index 0000000..e7da9af Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/TenFoot/SelectedLine@2x.png differ diff --git a/Client2020/content/textures/ui/Emotes/TenFoot/SelectedLine@3x.png b/Client2020/content/textures/ui/Emotes/TenFoot/SelectedLine@3x.png new file mode 100644 index 0000000..32c5be8 Binary files /dev/null and b/Client2020/content/textures/ui/Emotes/TenFoot/SelectedLine@3x.png differ diff --git a/Client2020/content/textures/ui/ErrorIcon.png b/Client2020/content/textures/ui/ErrorIcon.png new file mode 100644 index 0000000..420b21b Binary files /dev/null and b/Client2020/content/textures/ui/ErrorIcon.png differ diff --git a/Client2020/content/textures/ui/ErrorIconSmall.png b/Client2020/content/textures/ui/ErrorIconSmall.png new file mode 100644 index 0000000..213a1ff Binary files /dev/null and b/Client2020/content/textures/ui/ErrorIconSmall.png differ diff --git a/Client2020/content/textures/ui/ErrorPrompt/PrimaryButton.png b/Client2020/content/textures/ui/ErrorPrompt/PrimaryButton.png new file mode 100644 index 0000000..afae8bb Binary files /dev/null and b/Client2020/content/textures/ui/ErrorPrompt/PrimaryButton.png differ diff --git a/Client2020/content/textures/ui/ErrorPrompt/PrimaryButton@2x.png b/Client2020/content/textures/ui/ErrorPrompt/PrimaryButton@2x.png new file mode 100644 index 0000000..0f8157c Binary files /dev/null and b/Client2020/content/textures/ui/ErrorPrompt/PrimaryButton@2x.png differ diff --git a/Client2020/content/textures/ui/ErrorPrompt/PrimaryButton@3x.png b/Client2020/content/textures/ui/ErrorPrompt/PrimaryButton@3x.png new file mode 100644 index 0000000..72a5371 Binary files /dev/null and b/Client2020/content/textures/ui/ErrorPrompt/PrimaryButton@3x.png differ diff --git a/Client2020/content/textures/ui/ErrorPrompt/SecondaryButton.png b/Client2020/content/textures/ui/ErrorPrompt/SecondaryButton.png new file mode 100644 index 0000000..70b102b Binary files /dev/null and b/Client2020/content/textures/ui/ErrorPrompt/SecondaryButton.png differ diff --git a/Client2020/content/textures/ui/ErrorPrompt/SecondaryButton@2x.png b/Client2020/content/textures/ui/ErrorPrompt/SecondaryButton@2x.png new file mode 100644 index 0000000..cee0a86 Binary files /dev/null and b/Client2020/content/textures/ui/ErrorPrompt/SecondaryButton@2x.png differ diff --git a/Client2020/content/textures/ui/ErrorPrompt/SecondaryButton@3x.png b/Client2020/content/textures/ui/ErrorPrompt/SecondaryButton@3x.png new file mode 100644 index 0000000..0ac6f5c Binary files /dev/null and b/Client2020/content/textures/ui/ErrorPrompt/SecondaryButton@3x.png differ diff --git a/Client2020/content/textures/ui/ErrorPrompt/ShimmerOverlay.png b/Client2020/content/textures/ui/ErrorPrompt/ShimmerOverlay.png new file mode 100644 index 0000000..a38dcc8 Binary files /dev/null and b/Client2020/content/textures/ui/ErrorPrompt/ShimmerOverlay.png differ diff --git a/Client2020/content/textures/ui/ErrorPrompt/ShimmerOverlay@2x.png b/Client2020/content/textures/ui/ErrorPrompt/ShimmerOverlay@2x.png new file mode 100644 index 0000000..4747f05 Binary files /dev/null and b/Client2020/content/textures/ui/ErrorPrompt/ShimmerOverlay@2x.png differ diff --git a/Client2020/content/textures/ui/ErrorPrompt/ShimmerOverlay@3x.png b/Client2020/content/textures/ui/ErrorPrompt/ShimmerOverlay@3x.png new file mode 100644 index 0000000..923705d Binary files /dev/null and b/Client2020/content/textures/ui/ErrorPrompt/ShimmerOverlay@3x.png differ diff --git a/Client2020/content/textures/ui/ExpandArrowSheet.png b/Client2020/content/textures/ui/ExpandArrowSheet.png new file mode 100644 index 0000000..d52edc1 Binary files /dev/null and b/Client2020/content/textures/ui/ExpandArrowSheet.png differ diff --git a/Client2020/content/textures/ui/Gear.png b/Client2020/content/textures/ui/Gear.png new file mode 100644 index 0000000..56a46db Binary files /dev/null and b/Client2020/content/textures/ui/Gear.png differ diff --git a/Client2020/content/textures/ui/Gear_dn.png b/Client2020/content/textures/ui/Gear_dn.png new file mode 100644 index 0000000..38df9a3 Binary files /dev/null and b/Client2020/content/textures/ui/Gear_dn.png differ diff --git a/Client2020/content/textures/ui/GuiImagePlaceholder.png b/Client2020/content/textures/ui/GuiImagePlaceholder.png new file mode 100644 index 0000000..5a6d524 Binary files /dev/null and b/Client2020/content/textures/ui/GuiImagePlaceholder.png differ diff --git a/Client2020/content/textures/ui/Health-BKG-Center.png b/Client2020/content/textures/ui/Health-BKG-Center.png new file mode 100644 index 0000000..dea1b3b Binary files /dev/null and b/Client2020/content/textures/ui/Health-BKG-Center.png differ diff --git a/Client2020/content/textures/ui/Health-BKG-Center@2x.png b/Client2020/content/textures/ui/Health-BKG-Center@2x.png new file mode 100644 index 0000000..3fe7101 Binary files /dev/null and b/Client2020/content/textures/ui/Health-BKG-Center@2x.png differ diff --git a/Client2020/content/textures/ui/Health-BKG-Left-Cap.png b/Client2020/content/textures/ui/Health-BKG-Left-Cap.png new file mode 100644 index 0000000..9baf764 Binary files /dev/null and b/Client2020/content/textures/ui/Health-BKG-Left-Cap.png differ diff --git a/Client2020/content/textures/ui/Health-BKG-Left-Cap@2x.png b/Client2020/content/textures/ui/Health-BKG-Left-Cap@2x.png new file mode 100644 index 0000000..6a7825c Binary files /dev/null and b/Client2020/content/textures/ui/Health-BKG-Left-Cap@2x.png differ diff --git a/Client2020/content/textures/ui/Health-BKG-Right-Cap.png b/Client2020/content/textures/ui/Health-BKG-Right-Cap.png new file mode 100644 index 0000000..66af73a Binary files /dev/null and b/Client2020/content/textures/ui/Health-BKG-Right-Cap.png differ diff --git a/Client2020/content/textures/ui/Health-BKG-Right-Cap@2x.png b/Client2020/content/textures/ui/Health-BKG-Right-Cap@2x.png new file mode 100644 index 0000000..7c3f951 Binary files /dev/null and b/Client2020/content/textures/ui/Health-BKG-Right-Cap@2x.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/BackgroundGlow.png b/Client2020/content/textures/ui/InGameMenu/BackgroundGlow.png new file mode 100644 index 0000000..f437d62 Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/BackgroundGlow.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/BackgroundGlow@2x.png b/Client2020/content/textures/ui/InGameMenu/BackgroundGlow@2x.png new file mode 100644 index 0000000..f7751c7 Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/BackgroundGlow@2x.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/BackgroundGlow@3x.png b/Client2020/content/textures/ui/InGameMenu/BackgroundGlow@3x.png new file mode 100644 index 0000000..e47d7bb Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/BackgroundGlow@3x.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/CircleCutout.png b/Client2020/content/textures/ui/InGameMenu/CircleCutout.png new file mode 100644 index 0000000..621336f Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/CircleCutout.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/GenericController.png b/Client2020/content/textures/ui/InGameMenu/GenericController.png new file mode 100644 index 0000000..6951483 Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/GenericController.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/GenericController@2x.png b/Client2020/content/textures/ui/InGameMenu/GenericController@2x.png new file mode 100644 index 0000000..fe4257c Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/GenericController@2x.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/QuarterCircle.png b/Client2020/content/textures/ui/InGameMenu/QuarterCircle.png new file mode 100644 index 0000000..c7c51a8 Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/QuarterCircle.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/ScrollBottom.png b/Client2020/content/textures/ui/InGameMenu/ScrollBottom.png new file mode 100644 index 0000000..dca5b38 Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/ScrollBottom.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/ScrollBottom@2x.png b/Client2020/content/textures/ui/InGameMenu/ScrollBottom@2x.png new file mode 100644 index 0000000..6a2d21d Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/ScrollBottom@2x.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/ScrollBottom@3x.png b/Client2020/content/textures/ui/InGameMenu/ScrollBottom@3x.png new file mode 100644 index 0000000..2c242d5 Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/ScrollBottom@3x.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/ScrollMiddle.png b/Client2020/content/textures/ui/InGameMenu/ScrollMiddle.png new file mode 100644 index 0000000..330507d Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/ScrollMiddle.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/ScrollMiddle@2x.png b/Client2020/content/textures/ui/InGameMenu/ScrollMiddle@2x.png new file mode 100644 index 0000000..8f89578 Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/ScrollMiddle@2x.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/ScrollMiddle@3x.png b/Client2020/content/textures/ui/InGameMenu/ScrollMiddle@3x.png new file mode 100644 index 0000000..46db69f Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/ScrollMiddle@3x.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/ScrollTop.png b/Client2020/content/textures/ui/InGameMenu/ScrollTop.png new file mode 100644 index 0000000..2fb159a Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/ScrollTop.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/ScrollTop@2x.png b/Client2020/content/textures/ui/InGameMenu/ScrollTop@2x.png new file mode 100644 index 0000000..d2a3a61 Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/ScrollTop@2x.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/ScrollTop@3x.png b/Client2020/content/textures/ui/InGameMenu/ScrollTop@3x.png new file mode 100644 index 0000000..d3fe75c Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/ScrollTop@3x.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/WhiteSquare.png b/Client2020/content/textures/ui/InGameMenu/WhiteSquare.png new file mode 100644 index 0000000..dc4e635 Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/WhiteSquare.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/XboxController.png b/Client2020/content/textures/ui/InGameMenu/XboxController.png new file mode 100644 index 0000000..d27ffb7 Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/XboxController.png differ diff --git a/Client2020/content/textures/ui/InGameMenu/XboxController@2x.png b/Client2020/content/textures/ui/InGameMenu/XboxController@2x.png new file mode 100644 index 0000000..e86c676 Binary files /dev/null and b/Client2020/content/textures/ui/InGameMenu/XboxController@2x.png differ diff --git a/Client2020/content/textures/ui/Input/DashedLine.png b/Client2020/content/textures/ui/Input/DashedLine.png new file mode 100644 index 0000000..f9e45e3 Binary files /dev/null and b/Client2020/content/textures/ui/Input/DashedLine.png differ diff --git a/Client2020/content/textures/ui/Input/DashedLine90.png b/Client2020/content/textures/ui/Input/DashedLine90.png new file mode 100644 index 0000000..59ae6c7 Binary files /dev/null and b/Client2020/content/textures/ui/Input/DashedLine90.png differ diff --git a/Client2020/content/textures/ui/Input/Disk_padded.png b/Client2020/content/textures/ui/Input/Disk_padded.png new file mode 100644 index 0000000..ccbda80 Binary files /dev/null and b/Client2020/content/textures/ui/Input/Disk_padded.png differ diff --git a/Client2020/content/textures/ui/Input/IntroCamera.png b/Client2020/content/textures/ui/Input/IntroCamera.png new file mode 100644 index 0000000..bcf3f9c Binary files /dev/null and b/Client2020/content/textures/ui/Input/IntroCamera.png differ diff --git a/Client2020/content/textures/ui/Input/IntroCameraPinch.png b/Client2020/content/textures/ui/Input/IntroCameraPinch.png new file mode 100644 index 0000000..9521250 Binary files /dev/null and b/Client2020/content/textures/ui/Input/IntroCameraPinch.png differ diff --git a/Client2020/content/textures/ui/Input/IntroMove.png b/Client2020/content/textures/ui/Input/IntroMove.png new file mode 100644 index 0000000..a435304 Binary files /dev/null and b/Client2020/content/textures/ui/Input/IntroMove.png differ diff --git a/Client2020/content/textures/ui/Input/IntroMove@2x.png b/Client2020/content/textures/ui/Input/IntroMove@2x.png new file mode 100644 index 0000000..1ce8682 Binary files /dev/null and b/Client2020/content/textures/ui/Input/IntroMove@2x.png differ diff --git a/Client2020/content/textures/ui/Input/Ring_padded.png b/Client2020/content/textures/ui/Input/Ring_padded.png new file mode 100644 index 0000000..54cd6eb Binary files /dev/null and b/Client2020/content/textures/ui/Input/Ring_padded.png differ diff --git a/Client2020/content/textures/ui/Input/TouchControlsSheetV2.png b/Client2020/content/textures/ui/Input/TouchControlsSheetV2.png new file mode 100644 index 0000000..1999108 Binary files /dev/null and b/Client2020/content/textures/ui/Input/TouchControlsSheetV2.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/Button_outline.png b/Client2020/content/textures/ui/InspectMenu/Button_outline.png new file mode 100644 index 0000000..e80c078 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/Button_outline.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/Button_outline@2x.png b/Client2020/content/textures/ui/InspectMenu/Button_outline@2x.png new file mode 100644 index 0000000..f597ed5 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/Button_outline@2x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/Button_outline@3x.png b/Client2020/content/textures/ui/InspectMenu/Button_outline@3x.png new file mode 100644 index 0000000..e591c6c Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/Button_outline@3x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/Button_white.png b/Client2020/content/textures/ui/InspectMenu/Button_white.png new file mode 100644 index 0000000..a9d584a Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/Button_white.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/Button_white@2x.png b/Client2020/content/textures/ui/InspectMenu/Button_white@2x.png new file mode 100644 index 0000000..d95687a Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/Button_white@2x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/Button_white@3x.png b/Client2020/content/textures/ui/InspectMenu/Button_white@3x.png new file mode 100644 index 0000000..c5b0167 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/Button_white@3x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/caret-tail-left@2x.png b/Client2020/content/textures/ui/InspectMenu/caret-tail-left@2x.png new file mode 100644 index 0000000..5eb7bba Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/caret-tail-left@2x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/caret_tail_left.png b/Client2020/content/textures/ui/InspectMenu/caret_tail_left.png new file mode 100644 index 0000000..bda2423 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/caret_tail_left.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/caret_tail_left@3x.png b/Client2020/content/textures/ui/InspectMenu/caret_tail_left@3x.png new file mode 100644 index 0000000..adbf680 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/caret_tail_left@3x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/gr-item-selector-triangle.png b/Client2020/content/textures/ui/InspectMenu/gr-item-selector-triangle.png new file mode 100644 index 0000000..44cdf7e Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/gr-item-selector-triangle.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/gr-item-selector-triangle@2x.png b/Client2020/content/textures/ui/InspectMenu/gr-item-selector-triangle@2x.png new file mode 100644 index 0000000..43e60f7 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/gr-item-selector-triangle@2x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/gr-item-selector-triangle@3x.png b/Client2020/content/textures/ui/InspectMenu/gr-item-selector-triangle@3x.png new file mode 100644 index 0000000..612624d Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/gr-item-selector-triangle@3x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/gr-item-selector.png b/Client2020/content/textures/ui/InspectMenu/gr-item-selector.png new file mode 100644 index 0000000..cf8650d Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/gr-item-selector.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/gr-item-selector@2x.png b/Client2020/content/textures/ui/InspectMenu/gr-item-selector@2x.png new file mode 100644 index 0000000..0b2bdb5 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/gr-item-selector@2x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/gr-item-selector@3x.png b/Client2020/content/textures/ui/InspectMenu/gr-item-selector@3x.png new file mode 100644 index 0000000..a632c89 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/gr-item-selector@3x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_alert_tilt.png b/Client2020/content/textures/ui/InspectMenu/ico_alert_tilt.png new file mode 100644 index 0000000..7bb27f4 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_alert_tilt.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_alert_tilt@2x.png b/Client2020/content/textures/ui/InspectMenu/ico_alert_tilt@2x.png new file mode 100644 index 0000000..5d9dd77 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_alert_tilt@2x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_alert_tilt@3x.png b/Client2020/content/textures/ui/InspectMenu/ico_alert_tilt@3x.png new file mode 100644 index 0000000..263b4c0 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_alert_tilt@3x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_favorite.png b/Client2020/content/textures/ui/InspectMenu/ico_favorite.png new file mode 100644 index 0000000..fd24702 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_favorite.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_favorite@2x.png b/Client2020/content/textures/ui/InspectMenu/ico_favorite@2x.png new file mode 100644 index 0000000..e1bfd0a Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_favorite@2x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_favorite@3x.png b/Client2020/content/textures/ui/InspectMenu/ico_favorite@3x.png new file mode 100644 index 0000000..ec7b258 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_favorite@3x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_favorite_off.png b/Client2020/content/textures/ui/InspectMenu/ico_favorite_off.png new file mode 100644 index 0000000..fca7e0f Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_favorite_off.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_favorite_off@2x.png b/Client2020/content/textures/ui/InspectMenu/ico_favorite_off@2x.png new file mode 100644 index 0000000..102aac2 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_favorite_off@2x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_favorite_off@3x.png b/Client2020/content/textures/ui/InspectMenu/ico_favorite_off@3x.png new file mode 100644 index 0000000..37b23bb Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_favorite_off@3x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_inspect.png b/Client2020/content/textures/ui/InspectMenu/ico_inspect.png new file mode 100644 index 0000000..00f16d7 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_inspect.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_inspect@2x.png b/Client2020/content/textures/ui/InspectMenu/ico_inspect@2x.png new file mode 100644 index 0000000..2fcd3c1 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_inspect@2x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_inspect@3x.png b/Client2020/content/textures/ui/InspectMenu/ico_inspect@3x.png new file mode 100644 index 0000000..b7c4045 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_inspect@3x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_isnt-wearing.png b/Client2020/content/textures/ui/InspectMenu/ico_isnt-wearing.png new file mode 100644 index 0000000..28c37f0 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_isnt-wearing.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_isnt-wearing@2x.png b/Client2020/content/textures/ui/InspectMenu/ico_isnt-wearing@2x.png new file mode 100644 index 0000000..f569ddf Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_isnt-wearing@2x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_isnt-wearing@3x.png b/Client2020/content/textures/ui/InspectMenu/ico_isnt-wearing@3x.png new file mode 100644 index 0000000..27716aa Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_isnt-wearing@3x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_robux.png b/Client2020/content/textures/ui/InspectMenu/ico_robux.png new file mode 100644 index 0000000..3a32134 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_robux.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_robux@2x.png b/Client2020/content/textures/ui/InspectMenu/ico_robux@2x.png new file mode 100644 index 0000000..3a58a23 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_robux@2x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/ico_robux@3x.png b/Client2020/content/textures/ui/InspectMenu/ico_robux@3x.png new file mode 100644 index 0000000..37202cf Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/ico_robux@3x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/scroll_bar.png b/Client2020/content/textures/ui/InspectMenu/scroll_bar.png new file mode 100644 index 0000000..d8f14c4 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/scroll_bar.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/scroll_bar@2x.png b/Client2020/content/textures/ui/InspectMenu/scroll_bar@2x.png new file mode 100644 index 0000000..306cec6 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/scroll_bar@2x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/scroll_bar@3x.png b/Client2020/content/textures/ui/InspectMenu/scroll_bar@3x.png new file mode 100644 index 0000000..7714b43 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/scroll_bar@3x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/selection_regular.png b/Client2020/content/textures/ui/InspectMenu/selection_regular.png new file mode 100644 index 0000000..725b9f1 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/selection_regular.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/selection_regular@2x.png b/Client2020/content/textures/ui/InspectMenu/selection_regular@2x.png new file mode 100644 index 0000000..efd5440 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/selection_regular@2x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/selection_regular@3x.png b/Client2020/content/textures/ui/InspectMenu/selection_regular@3x.png new file mode 100644 index 0000000..5d54e43 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/selection_regular@3x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/selection_rounded.png b/Client2020/content/textures/ui/InspectMenu/selection_rounded.png new file mode 100644 index 0000000..7bb8105 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/selection_rounded.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/selection_rounded@2x.png b/Client2020/content/textures/ui/InspectMenu/selection_rounded@2x.png new file mode 100644 index 0000000..17f7a9e Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/selection_rounded@2x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/selection_rounded@3x.png b/Client2020/content/textures/ui/InspectMenu/selection_rounded@3x.png new file mode 100644 index 0000000..8009388 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/selection_rounded@3x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/x.png b/Client2020/content/textures/ui/InspectMenu/x.png new file mode 100644 index 0000000..bb8fd52 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/x@2x.png b/Client2020/content/textures/ui/InspectMenu/x@2x.png new file mode 100644 index 0000000..dc164a4 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/x@2x.png differ diff --git a/Client2020/content/textures/ui/InspectMenu/x@3x.png b/Client2020/content/textures/ui/InspectMenu/x@3x.png new file mode 100644 index 0000000..5c03866 Binary files /dev/null and b/Client2020/content/textures/ui/InspectMenu/x@3x.png differ diff --git a/Client2020/content/textures/ui/Keyboard/close_button_background.png b/Client2020/content/textures/ui/Keyboard/close_button_background.png new file mode 100644 index 0000000..ceec973 Binary files /dev/null and b/Client2020/content/textures/ui/Keyboard/close_button_background.png differ diff --git a/Client2020/content/textures/ui/Keyboard/close_button_icon.png b/Client2020/content/textures/ui/Keyboard/close_button_icon.png new file mode 100644 index 0000000..bb306ad Binary files /dev/null and b/Client2020/content/textures/ui/Keyboard/close_button_icon.png differ diff --git a/Client2020/content/textures/ui/Keyboard/close_button_selection.png b/Client2020/content/textures/ui/Keyboard/close_button_selection.png new file mode 100644 index 0000000..87bbfed Binary files /dev/null and b/Client2020/content/textures/ui/Keyboard/close_button_selection.png differ diff --git a/Client2020/content/textures/ui/Keyboard/key_selection_9slice.png b/Client2020/content/textures/ui/Keyboard/key_selection_9slice.png new file mode 100644 index 0000000..9c6fc50 Binary files /dev/null and b/Client2020/content/textures/ui/Keyboard/key_selection_9slice.png differ diff --git a/Client2020/content/textures/ui/Keyboard/mic_icon.png b/Client2020/content/textures/ui/Keyboard/mic_icon.png new file mode 100644 index 0000000..7e09484 Binary files /dev/null and b/Client2020/content/textures/ui/Keyboard/mic_icon.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/Aluminium.png b/Client2020/content/textures/ui/LegacyRbxGui/Aluminium.png new file mode 100644 index 0000000..c97ee68 Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/Aluminium.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/Asphalt.png b/Client2020/content/textures/ui/LegacyRbxGui/Asphalt.png new file mode 100644 index 0000000..a80093b Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/Asphalt.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/Cement.png b/Client2020/content/textures/ui/LegacyRbxGui/Cement.png new file mode 100644 index 0000000..a706a60 Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/Cement.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/Cinder block.png b/Client2020/content/textures/ui/LegacyRbxGui/Cinder block.png new file mode 100644 index 0000000..0d2dae7 Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/Cinder block.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/CloseButton.png b/Client2020/content/textures/ui/LegacyRbxGui/CloseButton.png new file mode 100644 index 0000000..6e01c6c Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/CloseButton.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/ComboBoxArrow.png b/Client2020/content/textures/ui/LegacyRbxGui/ComboBoxArrow.png new file mode 100644 index 0000000..529082e Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/ComboBoxArrow.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/Gold.png b/Client2020/content/textures/ui/LegacyRbxGui/Gold.png new file mode 100644 index 0000000..6597c05 Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/Gold.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/Granite .png b/Client2020/content/textures/ui/LegacyRbxGui/Granite .png new file mode 100644 index 0000000..7c69a36 Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/Granite .png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/GravelSide.png b/Client2020/content/textures/ui/LegacyRbxGui/GravelSide.png new file mode 100644 index 0000000..2abe94d Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/GravelSide.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/IronSide.png b/Client2020/content/textures/ui/LegacyRbxGui/IronSide.png new file mode 100644 index 0000000..096ae0a Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/IronSide.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/LogSide.png b/Client2020/content/textures/ui/LegacyRbxGui/LogSide.png new file mode 100644 index 0000000..732684e Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/LogSide.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/M1Side.png b/Client2020/content/textures/ui/LegacyRbxGui/M1Side.png new file mode 100644 index 0000000..93d0e5b Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/M1Side.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/PlankSide.png b/Client2020/content/textures/ui/LegacyRbxGui/PlankSide.png new file mode 100644 index 0000000..a300a3f Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/PlankSide.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/PlasticBlueTop.png b/Client2020/content/textures/ui/LegacyRbxGui/PlasticBlueTop.png new file mode 100644 index 0000000..93d9a6d Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/PlasticBlueTop.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/PlasticRedTop.png b/Client2020/content/textures/ui/LegacyRbxGui/PlasticRedTop.png new file mode 100644 index 0000000..d8b9ea3 Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/PlasticRedTop.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/StoneBlockSide.png b/Client2020/content/textures/ui/LegacyRbxGui/StoneBlockSide.png new file mode 100644 index 0000000..5d05da3 Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/StoneBlockSide.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/_preview water 03.png b/Client2020/content/textures/ui/LegacyRbxGui/_preview water 03.png new file mode 100644 index 0000000..5e44e05 Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/_preview water 03.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/brickSide.png b/Client2020/content/textures/ui/LegacyRbxGui/brickSide.png new file mode 100644 index 0000000..86a92a9 Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/brickSide.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/configIcon.png b/Client2020/content/textures/ui/LegacyRbxGui/configIcon.png new file mode 100644 index 0000000..e3687b9 Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/configIcon.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/health_greenBar.png b/Client2020/content/textures/ui/LegacyRbxGui/health_greenBar.png new file mode 100644 index 0000000..4a7c908 Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/health_greenBar.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/popup_greenCheckCircle.png b/Client2020/content/textures/ui/LegacyRbxGui/popup_greenCheckCircle.png new file mode 100644 index 0000000..1bcdabe Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/popup_greenCheckCircle.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/popup_redx.png b/Client2020/content/textures/ui/LegacyRbxGui/popup_redx.png new file mode 100644 index 0000000..e4471a6 Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/popup_redx.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/popup_warnTriangle.png b/Client2020/content/textures/ui/LegacyRbxGui/popup_warnTriangle.png new file mode 100644 index 0000000..cc98476 Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/popup_warnTriangle.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/sandside.png b/Client2020/content/textures/ui/LegacyRbxGui/sandside.png new file mode 100644 index 0000000..32a24aa Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/sandside.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/scroll.png b/Client2020/content/textures/ui/LegacyRbxGui/scroll.png new file mode 100644 index 0000000..50551ad Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/scroll.png differ diff --git a/Client2020/content/textures/ui/LegacyRbxGui/x.png b/Client2020/content/textures/ui/LegacyRbxGui/x.png new file mode 100644 index 0000000..bce3b85 Binary files /dev/null and b/Client2020/content/textures/ui/LegacyRbxGui/x.png differ diff --git a/Client2020/content/textures/ui/LoadingBKG.png b/Client2020/content/textures/ui/LoadingBKG.png new file mode 100644 index 0000000..2fc377d Binary files /dev/null and b/Client2020/content/textures/ui/LoadingBKG.png differ diff --git a/Client2020/content/textures/ui/LoadingScreen/BackgroundDark.png b/Client2020/content/textures/ui/LoadingScreen/BackgroundDark.png new file mode 100644 index 0000000..6df6d97 Binary files /dev/null and b/Client2020/content/textures/ui/LoadingScreen/BackgroundDark.png differ diff --git a/Client2020/content/textures/ui/LoadingScreen/BackgroundLight.png b/Client2020/content/textures/ui/LoadingScreen/BackgroundLight.png new file mode 100644 index 0000000..d1b2701 Binary files /dev/null and b/Client2020/content/textures/ui/LoadingScreen/BackgroundLight.png differ diff --git a/Client2020/content/textures/ui/LoadingScreen/LoadingSpinner.png b/Client2020/content/textures/ui/LoadingScreen/LoadingSpinner.png new file mode 100644 index 0000000..b577d8e Binary files /dev/null and b/Client2020/content/textures/ui/LoadingScreen/LoadingSpinner.png differ diff --git a/Client2020/content/textures/ui/Lobby/Buttons/glow_nine_slice.png b/Client2020/content/textures/ui/Lobby/Buttons/glow_nine_slice.png new file mode 100644 index 0000000..f67905e Binary files /dev/null and b/Client2020/content/textures/ui/Lobby/Buttons/glow_nine_slice.png differ diff --git a/Client2020/content/textures/ui/Lobby/Buttons/more_nine_slice_button.png b/Client2020/content/textures/ui/Lobby/Buttons/more_nine_slice_button.png new file mode 100644 index 0000000..7e287ed Binary files /dev/null and b/Client2020/content/textures/ui/Lobby/Buttons/more_nine_slice_button.png differ diff --git a/Client2020/content/textures/ui/Lobby/Buttons/nine_slice_button.png b/Client2020/content/textures/ui/Lobby/Buttons/nine_slice_button.png new file mode 100644 index 0000000..adf36ae Binary files /dev/null and b/Client2020/content/textures/ui/Lobby/Buttons/nine_slice_button.png differ diff --git a/Client2020/content/textures/ui/Lobby/Buttons/scroll_button.png b/Client2020/content/textures/ui/Lobby/Buttons/scroll_button.png new file mode 100644 index 0000000..e40ed8c Binary files /dev/null and b/Client2020/content/textures/ui/Lobby/Buttons/scroll_button.png differ diff --git a/Client2020/content/textures/ui/Lobby/Buttons/scroll_down.png b/Client2020/content/textures/ui/Lobby/Buttons/scroll_down.png new file mode 100644 index 0000000..b7ca49b Binary files /dev/null and b/Client2020/content/textures/ui/Lobby/Buttons/scroll_down.png differ diff --git a/Client2020/content/textures/ui/Lobby/Buttons/scroll_left.png b/Client2020/content/textures/ui/Lobby/Buttons/scroll_left.png new file mode 100644 index 0000000..1ad66bb Binary files /dev/null and b/Client2020/content/textures/ui/Lobby/Buttons/scroll_left.png differ diff --git a/Client2020/content/textures/ui/Lobby/Buttons/scroll_right.png b/Client2020/content/textures/ui/Lobby/Buttons/scroll_right.png new file mode 100644 index 0000000..7c2fa78 Binary files /dev/null and b/Client2020/content/textures/ui/Lobby/Buttons/scroll_right.png differ diff --git a/Client2020/content/textures/ui/Lobby/Buttons/scroll_up.png b/Client2020/content/textures/ui/Lobby/Buttons/scroll_up.png new file mode 100644 index 0000000..c411535 Binary files /dev/null and b/Client2020/content/textures/ui/Lobby/Buttons/scroll_up.png differ diff --git a/Client2020/content/textures/ui/Lobby/Icons/back_icon.png b/Client2020/content/textures/ui/Lobby/Icons/back_icon.png new file mode 100644 index 0000000..501a503 Binary files /dev/null and b/Client2020/content/textures/ui/Lobby/Icons/back_icon.png differ diff --git a/Client2020/content/textures/ui/Menu/Hamburger.png b/Client2020/content/textures/ui/Menu/Hamburger.png new file mode 100644 index 0000000..6e0c2e4 Binary files /dev/null and b/Client2020/content/textures/ui/Menu/Hamburger.png differ diff --git a/Client2020/content/textures/ui/Menu/Hamburger@2x.png b/Client2020/content/textures/ui/Menu/Hamburger@2x.png new file mode 100644 index 0000000..a5ce478 Binary files /dev/null and b/Client2020/content/textures/ui/Menu/Hamburger@2x.png differ diff --git a/Client2020/content/textures/ui/Menu/HamburgerDown.png b/Client2020/content/textures/ui/Menu/HamburgerDown.png new file mode 100644 index 0000000..aaf1032 Binary files /dev/null and b/Client2020/content/textures/ui/Menu/HamburgerDown.png differ diff --git a/Client2020/content/textures/ui/Menu/HamburgerDown@2x.png b/Client2020/content/textures/ui/Menu/HamburgerDown@2x.png new file mode 100644 index 0000000..43ab55c Binary files /dev/null and b/Client2020/content/textures/ui/Menu/HamburgerDown@2x.png differ diff --git a/Client2020/content/textures/ui/Menu/buttonActive.png b/Client2020/content/textures/ui/Menu/buttonActive.png new file mode 100644 index 0000000..f685760 Binary files /dev/null and b/Client2020/content/textures/ui/Menu/buttonActive.png differ diff --git a/Client2020/content/textures/ui/Menu/buttonBackground.png b/Client2020/content/textures/ui/Menu/buttonBackground.png new file mode 100644 index 0000000..379241a Binary files /dev/null and b/Client2020/content/textures/ui/Menu/buttonBackground.png differ diff --git a/Client2020/content/textures/ui/Menu/buttonHover.png b/Client2020/content/textures/ui/Menu/buttonHover.png new file mode 100644 index 0000000..916ddff Binary files /dev/null and b/Client2020/content/textures/ui/Menu/buttonHover.png differ diff --git a/Client2020/content/textures/ui/Menu/hamburger3D.png b/Client2020/content/textures/ui/Menu/hamburger3D.png new file mode 100644 index 0000000..a9d70c5 Binary files /dev/null and b/Client2020/content/textures/ui/Menu/hamburger3D.png differ diff --git a/Client2020/content/textures/ui/Menu/hoverPopupLeft.png b/Client2020/content/textures/ui/Menu/hoverPopupLeft.png new file mode 100644 index 0000000..484b0d2 Binary files /dev/null and b/Client2020/content/textures/ui/Menu/hoverPopupLeft.png differ diff --git a/Client2020/content/textures/ui/Menu/hoverPopupMid.png b/Client2020/content/textures/ui/Menu/hoverPopupMid.png new file mode 100644 index 0000000..0f55008 Binary files /dev/null and b/Client2020/content/textures/ui/Menu/hoverPopupMid.png differ diff --git a/Client2020/content/textures/ui/Menu/hoverPopupRight.png b/Client2020/content/textures/ui/Menu/hoverPopupRight.png new file mode 100644 index 0000000..ea0b32a Binary files /dev/null and b/Client2020/content/textures/ui/Menu/hoverPopupRight.png differ diff --git a/Client2020/content/textures/ui/Menu/rectBackground.png b/Client2020/content/textures/ui/Menu/rectBackground.png new file mode 100644 index 0000000..758edfe Binary files /dev/null and b/Client2020/content/textures/ui/Menu/rectBackground.png differ diff --git a/Client2020/content/textures/ui/Menu/rectBackgroundWhite.png b/Client2020/content/textures/ui/Menu/rectBackgroundWhite.png new file mode 100644 index 0000000..6bbb4b6 Binary files /dev/null and b/Client2020/content/textures/ui/Menu/rectBackgroundWhite.png differ diff --git a/Client2020/content/textures/ui/Modal.png b/Client2020/content/textures/ui/Modal.png new file mode 100644 index 0000000..ec08e5b Binary files /dev/null and b/Client2020/content/textures/ui/Modal.png differ diff --git a/Client2020/content/textures/ui/Motor.png b/Client2020/content/textures/ui/Motor.png new file mode 100644 index 0000000..140c5cc Binary files /dev/null and b/Client2020/content/textures/ui/Motor.png differ diff --git a/Client2020/content/textures/ui/NetworkPause/no connection.png b/Client2020/content/textures/ui/NetworkPause/no connection.png new file mode 100644 index 0000000..88b13c5 Binary files /dev/null and b/Client2020/content/textures/ui/NetworkPause/no connection.png differ diff --git a/Client2020/content/textures/ui/NetworkPause/no connection@2x.png b/Client2020/content/textures/ui/NetworkPause/no connection@2x.png new file mode 100644 index 0000000..6eecbbd Binary files /dev/null and b/Client2020/content/textures/ui/NetworkPause/no connection@2x.png differ diff --git a/Client2020/content/textures/ui/NetworkPause/no connection@3x.png b/Client2020/content/textures/ui/NetworkPause/no connection@3x.png new file mode 100644 index 0000000..f8e1393 Binary files /dev/null and b/Client2020/content/textures/ui/NetworkPause/no connection@3x.png differ diff --git a/Client2020/content/textures/ui/PerformanceStats/BackgroundRounded.png b/Client2020/content/textures/ui/PerformanceStats/BackgroundRounded.png new file mode 100644 index 0000000..763d78e Binary files /dev/null and b/Client2020/content/textures/ui/PerformanceStats/BackgroundRounded.png differ diff --git a/Client2020/content/textures/ui/PerformanceStats/OvalKey.png b/Client2020/content/textures/ui/PerformanceStats/OvalKey.png new file mode 100644 index 0000000..90678c2 Binary files /dev/null and b/Client2020/content/textures/ui/PerformanceStats/OvalKey.png differ diff --git a/Client2020/content/textures/ui/PerformanceStats/TargetFiller.png b/Client2020/content/textures/ui/PerformanceStats/TargetFiller.png new file mode 100644 index 0000000..839cc28 Binary files /dev/null and b/Client2020/content/textures/ui/PerformanceStats/TargetFiller.png differ diff --git a/Client2020/content/textures/ui/PerformanceStats/TargetKey.png b/Client2020/content/textures/ui/PerformanceStats/TargetKey.png new file mode 100644 index 0000000..9008605 Binary files /dev/null and b/Client2020/content/textures/ui/PerformanceStats/TargetKey.png differ diff --git a/Client2020/content/textures/ui/PerformanceStats/TargetLine.png b/Client2020/content/textures/ui/PerformanceStats/TargetLine.png new file mode 100644 index 0000000..c1b434a Binary files /dev/null and b/Client2020/content/textures/ui/PerformanceStats/TargetLine.png differ diff --git a/Client2020/content/textures/ui/Plastic.png b/Client2020/content/textures/ui/Plastic.png new file mode 100644 index 0000000..0dd2054 Binary files /dev/null and b/Client2020/content/textures/ui/Plastic.png differ diff --git a/Client2020/content/textures/ui/PlayerList/Accept.png b/Client2020/content/textures/ui/PlayerList/Accept.png new file mode 100644 index 0000000..17c4258 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/Accept.png differ diff --git a/Client2020/content/textures/ui/PlayerList/Accept@2x.png b/Client2020/content/textures/ui/PlayerList/Accept@2x.png new file mode 100644 index 0000000..83a4af7 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/Accept@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/Accept@3x.png b/Client2020/content/textures/ui/PlayerList/Accept@3x.png new file mode 100644 index 0000000..97af405 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/Accept@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/AcceptButton.png b/Client2020/content/textures/ui/PlayerList/AcceptButton.png new file mode 100644 index 0000000..ae223d2 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/AcceptButton.png differ diff --git a/Client2020/content/textures/ui/PlayerList/AddFriend.png b/Client2020/content/textures/ui/PlayerList/AddFriend.png new file mode 100644 index 0000000..734d61a Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/AddFriend.png differ diff --git a/Client2020/content/textures/ui/PlayerList/AddFriend@2x.png b/Client2020/content/textures/ui/PlayerList/AddFriend@2x.png new file mode 100644 index 0000000..91ad14d Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/AddFriend@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/AddFriend@3x.png b/Client2020/content/textures/ui/PlayerList/AddFriend@3x.png new file mode 100644 index 0000000..eec1ed5 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/AddFriend@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/AdminIcon.png b/Client2020/content/textures/ui/PlayerList/AdminIcon.png new file mode 100644 index 0000000..9fe796a Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/AdminIcon.png differ diff --git a/Client2020/content/textures/ui/PlayerList/AdminIcon@2x.png b/Client2020/content/textures/ui/PlayerList/AdminIcon@2x.png new file mode 100644 index 0000000..ec9ad96 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/AdminIcon@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/AdminIcon@3x.png b/Client2020/content/textures/ui/PlayerList/AdminIcon@3x.png new file mode 100644 index 0000000..4da946e Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/AdminIcon@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/AvatarBackground.png b/Client2020/content/textures/ui/PlayerList/AvatarBackground.png new file mode 100644 index 0000000..1ca8143 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/AvatarBackground.png differ diff --git a/Client2020/content/textures/ui/PlayerList/AvatarBackground@2x.png b/Client2020/content/textures/ui/PlayerList/AvatarBackground@2x.png new file mode 100644 index 0000000..907b997 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/AvatarBackground@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/AvatarBackground@3x.png b/Client2020/content/textures/ui/PlayerList/AvatarBackground@3x.png new file mode 100644 index 0000000..b7e5df2 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/AvatarBackground@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/Block.png b/Client2020/content/textures/ui/PlayerList/Block.png new file mode 100644 index 0000000..733f3db Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/Block.png differ diff --git a/Client2020/content/textures/ui/PlayerList/Block@2x.png b/Client2020/content/textures/ui/PlayerList/Block@2x.png new file mode 100644 index 0000000..5d8788a Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/Block@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/Block@3x.png b/Client2020/content/textures/ui/PlayerList/Block@3x.png new file mode 100644 index 0000000..194eba1 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/Block@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/BlockedIcon.png b/Client2020/content/textures/ui/PlayerList/BlockedIcon.png new file mode 100644 index 0000000..1ddb436 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/BlockedIcon.png differ diff --git a/Client2020/content/textures/ui/PlayerList/CharacterImageBackground.png b/Client2020/content/textures/ui/PlayerList/CharacterImageBackground.png new file mode 100644 index 0000000..2620ac5 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/CharacterImageBackground.png differ diff --git a/Client2020/content/textures/ui/PlayerList/Clear.png b/Client2020/content/textures/ui/PlayerList/Clear.png new file mode 100644 index 0000000..7ddf2a3 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/Clear.png differ diff --git a/Client2020/content/textures/ui/PlayerList/Clear@2x.png b/Client2020/content/textures/ui/PlayerList/Clear@2x.png new file mode 100644 index 0000000..4ae0fde Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/Clear@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/Clear@3x.png b/Client2020/content/textures/ui/PlayerList/Clear@3x.png new file mode 100644 index 0000000..8deff72 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/Clear@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/FollowingIcon.png b/Client2020/content/textures/ui/PlayerList/FollowingIcon.png new file mode 100644 index 0000000..31a8b11 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/FollowingIcon.png differ diff --git a/Client2020/content/textures/ui/PlayerList/FollowingIcon@2x.png b/Client2020/content/textures/ui/PlayerList/FollowingIcon@2x.png new file mode 100644 index 0000000..d8bd4fa Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/FollowingIcon@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/FollowingIcon@3x.png b/Client2020/content/textures/ui/PlayerList/FollowingIcon@3x.png new file mode 100644 index 0000000..def46a5 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/FollowingIcon@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/FriendIcon.png b/Client2020/content/textures/ui/PlayerList/FriendIcon.png new file mode 100644 index 0000000..07db059 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/FriendIcon.png differ diff --git a/Client2020/content/textures/ui/PlayerList/FriendIcon@2x.png b/Client2020/content/textures/ui/PlayerList/FriendIcon@2x.png new file mode 100644 index 0000000..acc0cfe Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/FriendIcon@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/FriendIcon@3x.png b/Client2020/content/textures/ui/PlayerList/FriendIcon@3x.png new file mode 100644 index 0000000..9db9fce Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/FriendIcon@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/NewAvatarBackground.png b/Client2020/content/textures/ui/PlayerList/NewAvatarBackground.png new file mode 100644 index 0000000..3a69036 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/NewAvatarBackground.png differ diff --git a/Client2020/content/textures/ui/PlayerList/NewAvatarBackground@2x.png b/Client2020/content/textures/ui/PlayerList/NewAvatarBackground@2x.png new file mode 100644 index 0000000..c63ed56 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/NewAvatarBackground@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/NewAvatarBackground@3x.png b/Client2020/content/textures/ui/PlayerList/NewAvatarBackground@3x.png new file mode 100644 index 0000000..af99a97 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/NewAvatarBackground@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/NewFollowing.png b/Client2020/content/textures/ui/PlayerList/NewFollowing.png new file mode 100644 index 0000000..d98c3eb Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/NewFollowing.png differ diff --git a/Client2020/content/textures/ui/PlayerList/NewFollowing@2x.png b/Client2020/content/textures/ui/PlayerList/NewFollowing@2x.png new file mode 100644 index 0000000..672b7fa Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/NewFollowing@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/NewFollowing@3x.png b/Client2020/content/textures/ui/PlayerList/NewFollowing@3x.png new file mode 100644 index 0000000..b12f374 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/NewFollowing@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/NotificationOff.png b/Client2020/content/textures/ui/PlayerList/NotificationOff.png new file mode 100644 index 0000000..6e2cd7b Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/NotificationOff.png differ diff --git a/Client2020/content/textures/ui/PlayerList/NotificationOff@2x.png b/Client2020/content/textures/ui/PlayerList/NotificationOff@2x.png new file mode 100644 index 0000000..8f7339b Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/NotificationOff@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/NotificationOff@3x.png b/Client2020/content/textures/ui/PlayerList/NotificationOff@3x.png new file mode 100644 index 0000000..07422ba Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/NotificationOff@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/NotificationOn.png b/Client2020/content/textures/ui/PlayerList/NotificationOn.png new file mode 100644 index 0000000..7420fc4 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/NotificationOn.png differ diff --git a/Client2020/content/textures/ui/PlayerList/NotificationOn@2x.png b/Client2020/content/textures/ui/PlayerList/NotificationOn@2x.png new file mode 100644 index 0000000..6e710f5 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/NotificationOn@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/NotificationOn@3x.png b/Client2020/content/textures/ui/PlayerList/NotificationOn@3x.png new file mode 100644 index 0000000..ac4501c Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/NotificationOn@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/OwnerIcon.png b/Client2020/content/textures/ui/PlayerList/OwnerIcon.png new file mode 100644 index 0000000..eb7f86b Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/OwnerIcon.png differ diff --git a/Client2020/content/textures/ui/PlayerList/OwnerIcon@2x.png b/Client2020/content/textures/ui/PlayerList/OwnerIcon@2x.png new file mode 100644 index 0000000..e419233 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/OwnerIcon@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/OwnerIcon@3x.png b/Client2020/content/textures/ui/PlayerList/OwnerIcon@3x.png new file mode 100644 index 0000000..ad6160a Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/OwnerIcon@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/PremiumIcon.png b/Client2020/content/textures/ui/PlayerList/PremiumIcon.png new file mode 100644 index 0000000..5ff348e Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/PremiumIcon.png differ diff --git a/Client2020/content/textures/ui/PlayerList/PremiumIcon@2x.png b/Client2020/content/textures/ui/PlayerList/PremiumIcon@2x.png new file mode 100644 index 0000000..3b7033d Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/PremiumIcon@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/PremiumIcon@3x.png b/Client2020/content/textures/ui/PlayerList/PremiumIcon@3x.png new file mode 100644 index 0000000..e47ec8b Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/PremiumIcon@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/Report.png b/Client2020/content/textures/ui/PlayerList/Report.png new file mode 100644 index 0000000..856168e Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/Report.png differ diff --git a/Client2020/content/textures/ui/PlayerList/Report@2x.png b/Client2020/content/textures/ui/PlayerList/Report@2x.png new file mode 100644 index 0000000..72fe895 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/Report@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/Report@3x.png b/Client2020/content/textures/ui/PlayerList/Report@3x.png new file mode 100644 index 0000000..538a17a Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/Report@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/SelectOn.png b/Client2020/content/textures/ui/PlayerList/SelectOn.png new file mode 100644 index 0000000..14bae6a Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/SelectOn.png differ diff --git a/Client2020/content/textures/ui/PlayerList/SelectOn@2x.png b/Client2020/content/textures/ui/PlayerList/SelectOn@2x.png new file mode 100644 index 0000000..57455fd Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/SelectOn@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/SelectOn@3x.png b/Client2020/content/textures/ui/PlayerList/SelectOn@3x.png new file mode 100644 index 0000000..2812bbc Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/SelectOn@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/StarIcon.png b/Client2020/content/textures/ui/PlayerList/StarIcon.png new file mode 100644 index 0000000..3b34cfd Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/StarIcon.png differ diff --git a/Client2020/content/textures/ui/PlayerList/StarIcon@2x.png b/Client2020/content/textures/ui/PlayerList/StarIcon@2x.png new file mode 100644 index 0000000..08dbc3f Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/StarIcon@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/StarIcon@3x.png b/Client2020/content/textures/ui/PlayerList/StarIcon@3x.png new file mode 100644 index 0000000..dea4e24 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/StarIcon@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/TileShadowMissingTop.png b/Client2020/content/textures/ui/PlayerList/TileShadowMissingTop.png new file mode 100644 index 0000000..d09f5fb Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/TileShadowMissingTop.png differ diff --git a/Client2020/content/textures/ui/PlayerList/UnFriend.png b/Client2020/content/textures/ui/PlayerList/UnFriend.png new file mode 100644 index 0000000..257796c Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/UnFriend.png differ diff --git a/Client2020/content/textures/ui/PlayerList/UnFriend@2x.png b/Client2020/content/textures/ui/PlayerList/UnFriend@2x.png new file mode 100644 index 0000000..5f7da30 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/UnFriend@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/UnFriend@3x.png b/Client2020/content/textures/ui/PlayerList/UnFriend@3x.png new file mode 100644 index 0000000..bfb72fc Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/UnFriend@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/ViewAvatar.png b/Client2020/content/textures/ui/PlayerList/ViewAvatar.png new file mode 100644 index 0000000..6f75c2e Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/ViewAvatar.png differ diff --git a/Client2020/content/textures/ui/PlayerList/ViewAvatar@2x.png b/Client2020/content/textures/ui/PlayerList/ViewAvatar@2x.png new file mode 100644 index 0000000..0b741cd Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/ViewAvatar@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/ViewAvatar@3x.png b/Client2020/content/textures/ui/PlayerList/ViewAvatar@3x.png new file mode 100644 index 0000000..5dee4ff Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/ViewAvatar@3x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/developer.png b/Client2020/content/textures/ui/PlayerList/developer.png new file mode 100644 index 0000000..50aadab Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/developer.png differ diff --git a/Client2020/content/textures/ui/PlayerList/developer@2x.png b/Client2020/content/textures/ui/PlayerList/developer@2x.png new file mode 100644 index 0000000..6b04624 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/developer@2x.png differ diff --git a/Client2020/content/textures/ui/PlayerList/developer@3x.png b/Client2020/content/textures/ui/PlayerList/developer@3x.png new file mode 100644 index 0000000..c1415a7 Binary files /dev/null and b/Client2020/content/textures/ui/PlayerList/developer@3x.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/LeftButton.png b/Client2020/content/textures/ui/PurchasePrompt/LeftButton.png new file mode 100644 index 0000000..bc8b4a1 Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/LeftButton.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/LeftButton@2x.png b/Client2020/content/textures/ui/PurchasePrompt/LeftButton@2x.png new file mode 100644 index 0000000..0ff72eb Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/LeftButton@2x.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/LeftButtonDown.png b/Client2020/content/textures/ui/PurchasePrompt/LeftButtonDown.png new file mode 100644 index 0000000..d97bd19 Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/LeftButtonDown.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/LeftButtonDown@2x.png b/Client2020/content/textures/ui/PurchasePrompt/LeftButtonDown@2x.png new file mode 100644 index 0000000..3796731 Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/LeftButtonDown@2x.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/LoadingBG.png b/Client2020/content/textures/ui/PurchasePrompt/LoadingBG.png new file mode 100644 index 0000000..59d572b Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/LoadingBG.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/LoadingBG@2x.png b/Client2020/content/textures/ui/PurchasePrompt/LoadingBG@2x.png new file mode 100644 index 0000000..f21b03c Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/LoadingBG@2x.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/Premium.png b/Client2020/content/textures/ui/PurchasePrompt/Premium.png new file mode 100644 index 0000000..f95800d Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/Premium.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/PurchasePromptBG.png b/Client2020/content/textures/ui/PurchasePrompt/PurchasePromptBG.png new file mode 100644 index 0000000..2feb249 Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/PurchasePromptBG.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/PurchasePromptBG@2x.png b/Client2020/content/textures/ui/PurchasePrompt/PurchasePromptBG@2x.png new file mode 100644 index 0000000..13654f8 Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/PurchasePromptBG@2x.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/RightButton.png b/Client2020/content/textures/ui/PurchasePrompt/RightButton.png new file mode 100644 index 0000000..55089ea Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/RightButton.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/RightButton@2x.png b/Client2020/content/textures/ui/PurchasePrompt/RightButton@2x.png new file mode 100644 index 0000000..ae62615 Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/RightButton@2x.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/RightButtonDown.png b/Client2020/content/textures/ui/PurchasePrompt/RightButtonDown.png new file mode 100644 index 0000000..592ff90 Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/RightButtonDown.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/RightButtonDown@2x.png b/Client2020/content/textures/ui/PurchasePrompt/RightButtonDown@2x.png new file mode 100644 index 0000000..828fe27 Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/RightButtonDown@2x.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/SingleButton.png b/Client2020/content/textures/ui/PurchasePrompt/SingleButton.png new file mode 100644 index 0000000..706a95a Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/SingleButton.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/SingleButton@2x.png b/Client2020/content/textures/ui/PurchasePrompt/SingleButton@2x.png new file mode 100644 index 0000000..0d01ba4 Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/SingleButton@2x.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/SingleButtonDown.png b/Client2020/content/textures/ui/PurchasePrompt/SingleButtonDown.png new file mode 100644 index 0000000..885b1bf Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/SingleButtonDown.png differ diff --git a/Client2020/content/textures/ui/PurchasePrompt/SingleButtonDown@2x.png b/Client2020/content/textures/ui/PurchasePrompt/SingleButtonDown@2x.png new file mode 100644 index 0000000..fe010c6 Binary files /dev/null and b/Client2020/content/textures/ui/PurchasePrompt/SingleButtonDown@2x.png differ diff --git a/Client2020/content/textures/ui/RecordDown.png b/Client2020/content/textures/ui/RecordDown.png new file mode 100644 index 0000000..9708d84 Binary files /dev/null and b/Client2020/content/textures/ui/RecordDown.png differ diff --git a/Client2020/content/textures/ui/ResetIcon.png b/Client2020/content/textures/ui/ResetIcon.png new file mode 100644 index 0000000..b16996e Binary files /dev/null and b/Client2020/content/textures/ui/ResetIcon.png differ diff --git a/Client2020/content/textures/ui/RobloxNameIcon.png b/Client2020/content/textures/ui/RobloxNameIcon.png new file mode 100644 index 0000000..360d9c2 Binary files /dev/null and b/Client2020/content/textures/ui/RobloxNameIcon.png differ diff --git a/Client2020/content/textures/ui/RobuxIcon.png b/Client2020/content/textures/ui/RobuxIcon.png new file mode 100644 index 0000000..d14fe16 Binary files /dev/null and b/Client2020/content/textures/ui/RobuxIcon.png differ diff --git a/Client2020/content/textures/ui/RoundedRect8px.png b/Client2020/content/textures/ui/RoundedRect8px.png new file mode 100644 index 0000000..245a445 Binary files /dev/null and b/Client2020/content/textures/ui/RoundedRect8px.png differ diff --git a/Client2020/content/textures/ui/Scroll/scroll-bottom.png b/Client2020/content/textures/ui/Scroll/scroll-bottom.png new file mode 100644 index 0000000..9c5695c Binary files /dev/null and b/Client2020/content/textures/ui/Scroll/scroll-bottom.png differ diff --git a/Client2020/content/textures/ui/Scroll/scroll-bottom@2x.png b/Client2020/content/textures/ui/Scroll/scroll-bottom@2x.png new file mode 100644 index 0000000..85b70e6 Binary files /dev/null and b/Client2020/content/textures/ui/Scroll/scroll-bottom@2x.png differ diff --git a/Client2020/content/textures/ui/Scroll/scroll-middle.png b/Client2020/content/textures/ui/Scroll/scroll-middle.png new file mode 100644 index 0000000..f4489d7 Binary files /dev/null and b/Client2020/content/textures/ui/Scroll/scroll-middle.png differ diff --git a/Client2020/content/textures/ui/Scroll/scroll-middle@2x.png b/Client2020/content/textures/ui/Scroll/scroll-middle@2x.png new file mode 100644 index 0000000..22d0f11 Binary files /dev/null and b/Client2020/content/textures/ui/Scroll/scroll-middle@2x.png differ diff --git a/Client2020/content/textures/ui/Scroll/scroll-top.png b/Client2020/content/textures/ui/Scroll/scroll-top.png new file mode 100644 index 0000000..bc6dcce Binary files /dev/null and b/Client2020/content/textures/ui/Scroll/scroll-top.png differ diff --git a/Client2020/content/textures/ui/Scroll/scroll-top@2x.png b/Client2020/content/textures/ui/Scroll/scroll-top@2x.png new file mode 100644 index 0000000..0ead0e8 Binary files /dev/null and b/Client2020/content/textures/ui/Scroll/scroll-top@2x.png differ diff --git a/Client2020/content/textures/ui/SearchIcon.png b/Client2020/content/textures/ui/SearchIcon.png new file mode 100644 index 0000000..58772d0 Binary files /dev/null and b/Client2020/content/textures/ui/SearchIcon.png differ diff --git a/Client2020/content/textures/ui/SelectionBox.png b/Client2020/content/textures/ui/SelectionBox.png new file mode 100644 index 0000000..290b647 Binary files /dev/null and b/Client2020/content/textures/ui/SelectionBox.png differ diff --git a/Client2020/content/textures/ui/SelectionBox@2x.png b/Client2020/content/textures/ui/SelectionBox@2x.png new file mode 100644 index 0000000..65d1711 Binary files /dev/null and b/Client2020/content/textures/ui/SelectionBox@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/DropDown/DropDown.png b/Client2020/content/textures/ui/Settings/DropDown/DropDown.png new file mode 100644 index 0000000..39e6dc8 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/DropDown/DropDown.png differ diff --git a/Client2020/content/textures/ui/Settings/DropDown/DropDown@2x.png b/Client2020/content/textures/ui/Settings/DropDown/DropDown@2x.png new file mode 100644 index 0000000..ccdd651 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/DropDown/DropDown@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/AButtonDark.png b/Client2020/content/textures/ui/Settings/Help/AButtonDark.png new file mode 100644 index 0000000..da4ec7a Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/AButtonDark.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/AButtonDark@2x.png b/Client2020/content/textures/ui/Settings/Help/AButtonDark@2x.png new file mode 100644 index 0000000..b2e9cba Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/AButtonDark@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/AButtonLight.png b/Client2020/content/textures/ui/Settings/Help/AButtonLight.png new file mode 100644 index 0000000..431e4a7 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/AButtonLight.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/AButtonLight@2x.png b/Client2020/content/textures/ui/Settings/Help/AButtonLight@2x.png new file mode 100644 index 0000000..d140dba Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/AButtonLight@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/AButtonLightSmall.png b/Client2020/content/textures/ui/Settings/Help/AButtonLightSmall.png new file mode 100644 index 0000000..50f8035 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/AButtonLightSmall.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/BButtonDark.png b/Client2020/content/textures/ui/Settings/Help/BButtonDark.png new file mode 100644 index 0000000..9b607a9 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/BButtonDark.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/BButtonDark@2x.png b/Client2020/content/textures/ui/Settings/Help/BButtonDark@2x.png new file mode 100644 index 0000000..df1a1a5 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/BButtonDark@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/BButtonLight.png b/Client2020/content/textures/ui/Settings/Help/BButtonLight.png new file mode 100644 index 0000000..598b7b9 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/BButtonLight.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/BButtonLight@2x.png b/Client2020/content/textures/ui/Settings/Help/BButtonLight@2x.png new file mode 100644 index 0000000..e2e67db Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/BButtonLight@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/EscapeIcon.png b/Client2020/content/textures/ui/Settings/Help/EscapeIcon.png new file mode 100644 index 0000000..7c53d55 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/EscapeIcon.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/GenericController.png b/Client2020/content/textures/ui/Settings/Help/GenericController.png new file mode 100644 index 0000000..db52b1f Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/GenericController.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/GenericController@2x.png b/Client2020/content/textures/ui/Settings/Help/GenericController@2x.png new file mode 100644 index 0000000..3735735 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/GenericController@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/LeaveIcon.png b/Client2020/content/textures/ui/Settings/Help/LeaveIcon.png new file mode 100644 index 0000000..6d780ae Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/LeaveIcon.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/ResetIcon.png b/Client2020/content/textures/ui/Settings/Help/ResetIcon.png new file mode 100644 index 0000000..391ac31 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/ResetIcon.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/RotateCameraGesture.png b/Client2020/content/textures/ui/Settings/Help/RotateCameraGesture.png new file mode 100644 index 0000000..c0147d6 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/RotateCameraGesture.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/UseToolGesture.png b/Client2020/content/textures/ui/Settings/Help/UseToolGesture.png new file mode 100644 index 0000000..fe3c364 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/UseToolGesture.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/XButtonDark.png b/Client2020/content/textures/ui/Settings/Help/XButtonDark.png new file mode 100644 index 0000000..cc71edc Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/XButtonDark.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/XButtonDark@2x.png b/Client2020/content/textures/ui/Settings/Help/XButtonDark@2x.png new file mode 100644 index 0000000..0d53c7a Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/XButtonDark@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/XButtonLight.png b/Client2020/content/textures/ui/Settings/Help/XButtonLight.png new file mode 100644 index 0000000..62c13ec Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/XButtonLight.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/XButtonLight@2x.png b/Client2020/content/textures/ui/Settings/Help/XButtonLight@2x.png new file mode 100644 index 0000000..1d5bc77 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/XButtonLight@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/XboxController.png b/Client2020/content/textures/ui/Settings/Help/XboxController.png new file mode 100644 index 0000000..882124a Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/XboxController.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/XboxController@2x.png b/Client2020/content/textures/ui/Settings/Help/XboxController@2x.png new file mode 100644 index 0000000..9ea335a Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/XboxController@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/YButtonDark.png b/Client2020/content/textures/ui/Settings/Help/YButtonDark.png new file mode 100644 index 0000000..131c8ca Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/YButtonDark.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/YButtonDark@2x.png b/Client2020/content/textures/ui/Settings/Help/YButtonDark@2x.png new file mode 100644 index 0000000..3b5e223 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/YButtonDark@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/YButtonLight.png b/Client2020/content/textures/ui/Settings/Help/YButtonLight.png new file mode 100644 index 0000000..56ce742 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/YButtonLight.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/YButtonLight@2x.png b/Client2020/content/textures/ui/Settings/Help/YButtonLight@2x.png new file mode 100644 index 0000000..1a3ee30 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/YButtonLight@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Help/ZoomGesture.png b/Client2020/content/textures/ui/Settings/Help/ZoomGesture.png new file mode 100644 index 0000000..68ee910 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Help/ZoomGesture.png differ diff --git a/Client2020/content/textures/ui/Settings/LeaveGame/Button_1080.png b/Client2020/content/textures/ui/Settings/LeaveGame/Button_1080.png new file mode 100644 index 0000000..54ddf21 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/LeaveGame/Button_1080.png differ diff --git a/Client2020/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow.png b/Client2020/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow.png new file mode 100644 index 0000000..5949f9a Binary files /dev/null and b/Client2020/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow.png differ diff --git a/Client2020/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow@2x.png b/Client2020/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow@2x.png new file mode 100644 index 0000000..0ff5623 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow@3x.png b/Client2020/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow@3x.png new file mode 100644 index 0000000..a157bf9 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow@3x.png differ diff --git a/Client2020/content/textures/ui/Settings/LeaveGame/gr-item selector-8px corner.png b/Client2020/content/textures/ui/Settings/LeaveGame/gr-item selector-8px corner.png new file mode 100644 index 0000000..9cb9fb2 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/LeaveGame/gr-item selector-8px corner.png differ diff --git a/Client2020/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle.png b/Client2020/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle.png new file mode 100644 index 0000000..f142181 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle.png differ diff --git a/Client2020/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle@2x.png b/Client2020/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle@2x.png new file mode 100644 index 0000000..432dece Binary files /dev/null and b/Client2020/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle@3x.png b/Client2020/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle@3x.png new file mode 100644 index 0000000..f94d265 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle@3x.png differ diff --git a/Client2020/content/textures/ui/Settings/LeaveGame/selectorWithIcon.png b/Client2020/content/textures/ui/Settings/LeaveGame/selectorWithIcon.png new file mode 100644 index 0000000..99764d2 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/LeaveGame/selectorWithIcon.png differ diff --git a/Client2020/content/textures/ui/Settings/LeaveGame/selectorWithIcon@2x.png b/Client2020/content/textures/ui/Settings/LeaveGame/selectorWithIcon@2x.png new file mode 100644 index 0000000..7dfdbcf Binary files /dev/null and b/Client2020/content/textures/ui/Settings/LeaveGame/selectorWithIcon@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/LeaveGame/selectorWithIcon@3x.png b/Client2020/content/textures/ui/Settings/LeaveGame/selectorWithIcon@3x.png new file mode 100644 index 0000000..f201559 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/LeaveGame/selectorWithIcon@3x.png differ diff --git a/Client2020/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle.png b/Client2020/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle.png new file mode 100644 index 0000000..423d326 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle.png differ diff --git a/Client2020/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle@2x.png b/Client2020/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle@2x.png new file mode 100644 index 0000000..c373e16 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle@3x.png b/Client2020/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle@3x.png new file mode 100644 index 0000000..9bcf274 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle@3x.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuBackground.png b/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuBackground.png new file mode 100644 index 0000000..4614da6 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuBackground.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuButton.png b/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuButton.png new file mode 100644 index 0000000..632c7d2 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuButton.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuButton@2x.png b/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuButton@2x.png new file mode 100644 index 0000000..a7f7a27 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuButton@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png b/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png new file mode 100644 index 0000000..4a4267e Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected@2x.png b/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected@2x.png new file mode 100644 index 0000000..3c33658 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuSelection.png b/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuSelection.png new file mode 100644 index 0000000..c0f8936 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuSelection.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuSelection@2x.png b/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuSelection@2x.png new file mode 100644 index 0000000..b3eb828 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarAssets/MenuSelection@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab.png b/Client2020/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab.png new file mode 100644 index 0000000..5b0c02c Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab@2x.png b/Client2020/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab@2x.png new file mode 100644 index 0000000..61a2380 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarIcons/HelpTab.png b/Client2020/content/textures/ui/Settings/MenuBarIcons/HelpTab.png new file mode 100644 index 0000000..a3a18c1 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarIcons/HelpTab.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarIcons/HelpTab@2x.png b/Client2020/content/textures/ui/Settings/MenuBarIcons/HelpTab@2x.png new file mode 100644 index 0000000..c55cfad Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarIcons/HelpTab@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarIcons/HomeTab.png b/Client2020/content/textures/ui/Settings/MenuBarIcons/HomeTab.png new file mode 100644 index 0000000..e1a6468 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarIcons/HomeTab.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarIcons/HomeTab@2x.png b/Client2020/content/textures/ui/Settings/MenuBarIcons/HomeTab@2x.png new file mode 100644 index 0000000..edec627 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarIcons/HomeTab@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon.png b/Client2020/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon.png new file mode 100644 index 0000000..fc253e4 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon@2x.png b/Client2020/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon@2x.png new file mode 100644 index 0000000..8ab99f7 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarIcons/RecordTab.png b/Client2020/content/textures/ui/Settings/MenuBarIcons/RecordTab.png new file mode 100644 index 0000000..37616b6 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarIcons/RecordTab.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarIcons/RecordTab@2x.png b/Client2020/content/textures/ui/Settings/MenuBarIcons/RecordTab@2x.png new file mode 100644 index 0000000..ee7db49 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarIcons/RecordTab@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab.png b/Client2020/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab.png new file mode 100644 index 0000000..69eb788 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab.png differ diff --git a/Client2020/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab@2x.png b/Client2020/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab@2x.png new file mode 100644 index 0000000..8efb061 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Players/AddFriendIcon.png b/Client2020/content/textures/ui/Settings/Players/AddFriendIcon.png new file mode 100644 index 0000000..d63e933 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Players/AddFriendIcon.png differ diff --git a/Client2020/content/textures/ui/Settings/Players/AddFriendIcon@2x.png b/Client2020/content/textures/ui/Settings/Players/AddFriendIcon@2x.png new file mode 100644 index 0000000..ddb5658 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Players/AddFriendIcon@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Players/FriendIcon.png b/Client2020/content/textures/ui/Settings/Players/FriendIcon.png new file mode 100644 index 0000000..ce78c14 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Players/FriendIcon.png differ diff --git a/Client2020/content/textures/ui/Settings/Players/FriendIcon@2x.png b/Client2020/content/textures/ui/Settings/Players/FriendIcon@2x.png new file mode 100644 index 0000000..420f532 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Players/FriendIcon@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Players/ReportFlagIcon.png b/Client2020/content/textures/ui/Settings/Players/ReportFlagIcon.png new file mode 100644 index 0000000..667ce8b Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Players/ReportFlagIcon.png differ diff --git a/Client2020/content/textures/ui/Settings/Players/ReportFlagIcon@2x.png b/Client2020/content/textures/ui/Settings/Players/ReportFlagIcon@2x.png new file mode 100644 index 0000000..e5346b0 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Players/ReportFlagIcon@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/Alert.png b/Client2020/content/textures/ui/Settings/Radial/Alert.png new file mode 100644 index 0000000..314d122 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/Alert.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/Alert@2x.png b/Client2020/content/textures/ui/Settings/Radial/Alert@2x.png new file mode 100644 index 0000000..6c3c966 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/Alert@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/Backpack.png b/Client2020/content/textures/ui/Settings/Radial/Backpack.png new file mode 100644 index 0000000..4658ae8 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/Backpack.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/Backpack@2x.png b/Client2020/content/textures/ui/Settings/Radial/Backpack@2x.png new file mode 100644 index 0000000..d222525 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/Backpack@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/Bottom.png b/Client2020/content/textures/ui/Settings/Radial/Bottom.png new file mode 100644 index 0000000..edb40d9 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/Bottom.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/BottomLeft.png b/Client2020/content/textures/ui/Settings/Radial/BottomLeft.png new file mode 100644 index 0000000..4560e63 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/BottomLeft.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/BottomLeftSelected.png b/Client2020/content/textures/ui/Settings/Radial/BottomLeftSelected.png new file mode 100644 index 0000000..f678b7d Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/BottomLeftSelected.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/BottomRight.png b/Client2020/content/textures/ui/Settings/Radial/BottomRight.png new file mode 100644 index 0000000..34ea9d2 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/BottomRight.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/BottomRightSelected.png b/Client2020/content/textures/ui/Settings/Radial/BottomRightSelected.png new file mode 100644 index 0000000..c044854 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/BottomRightSelected.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/BottomSelected.png b/Client2020/content/textures/ui/Settings/Radial/BottomSelected.png new file mode 100644 index 0000000..eed548c Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/BottomSelected.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/Chat.png b/Client2020/content/textures/ui/Settings/Radial/Chat.png new file mode 100644 index 0000000..b0bc68c Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/Chat.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/Chat@2x.png b/Client2020/content/textures/ui/Settings/Radial/Chat@2x.png new file mode 100644 index 0000000..1b9e5bc Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/Chat@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/EmptyBottom.png b/Client2020/content/textures/ui/Settings/Radial/EmptyBottom.png new file mode 100644 index 0000000..964257a Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/EmptyBottom.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/EmptyBottomLeft.png b/Client2020/content/textures/ui/Settings/Radial/EmptyBottomLeft.png new file mode 100644 index 0000000..a7b3a80 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/EmptyBottomLeft.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/EmptyBottomRight.png b/Client2020/content/textures/ui/Settings/Radial/EmptyBottomRight.png new file mode 100644 index 0000000..87e8935 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/EmptyBottomRight.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/EmptyTop.png b/Client2020/content/textures/ui/Settings/Radial/EmptyTop.png new file mode 100644 index 0000000..90da292 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/EmptyTop.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/EmptyTopLeft.png b/Client2020/content/textures/ui/Settings/Radial/EmptyTopLeft.png new file mode 100644 index 0000000..84fd9a4 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/EmptyTopLeft.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/EmptyTopRight.png b/Client2020/content/textures/ui/Settings/Radial/EmptyTopRight.png new file mode 100644 index 0000000..53be061 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/EmptyTopRight.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/Leave.png b/Client2020/content/textures/ui/Settings/Radial/Leave.png new file mode 100644 index 0000000..5f69290 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/Leave.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/Leave@2x.png b/Client2020/content/textures/ui/Settings/Radial/Leave@2x.png new file mode 100644 index 0000000..d4f66b5 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/Leave@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/Menu.png b/Client2020/content/textures/ui/Settings/Radial/Menu.png new file mode 100644 index 0000000..bff7118 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/Menu.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/Menu@2x.png b/Client2020/content/textures/ui/Settings/Radial/Menu@2x.png new file mode 100644 index 0000000..b56e6b6 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/Menu@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/PlayerList.png b/Client2020/content/textures/ui/Settings/Radial/PlayerList.png new file mode 100644 index 0000000..fae417d Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/PlayerList.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/PlayerList@2x.png b/Client2020/content/textures/ui/Settings/Radial/PlayerList@2x.png new file mode 100644 index 0000000..9a91e3e Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/PlayerList@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/RadialLabel.png b/Client2020/content/textures/ui/Settings/Radial/RadialLabel.png new file mode 100644 index 0000000..c7065c9 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/RadialLabel.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/RadialLabel@2x.png b/Client2020/content/textures/ui/Settings/Radial/RadialLabel@2x.png new file mode 100644 index 0000000..349aed3 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/RadialLabel@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/Top.png b/Client2020/content/textures/ui/Settings/Radial/Top.png new file mode 100644 index 0000000..a87d6eb Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/Top.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/TopLeft.png b/Client2020/content/textures/ui/Settings/Radial/TopLeft.png new file mode 100644 index 0000000..64b0ec1 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/TopLeft.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/TopLeftSelected.png b/Client2020/content/textures/ui/Settings/Radial/TopLeftSelected.png new file mode 100644 index 0000000..0069778 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/TopLeftSelected.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/TopRight.png b/Client2020/content/textures/ui/Settings/Radial/TopRight.png new file mode 100644 index 0000000..73831c9 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/TopRight.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/TopRightSelected.png b/Client2020/content/textures/ui/Settings/Radial/TopRightSelected.png new file mode 100644 index 0000000..9ead602 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/TopRightSelected.png differ diff --git a/Client2020/content/textures/ui/Settings/Radial/TopSelected.png b/Client2020/content/textures/ui/Settings/Radial/TopSelected.png new file mode 100644 index 0000000..6c73255 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Radial/TopSelected.png differ diff --git a/Client2020/content/textures/ui/Settings/ShareGame/icons.png b/Client2020/content/textures/ui/Settings/ShareGame/icons.png new file mode 100644 index 0000000..b99f2dc Binary files /dev/null and b/Client2020/content/textures/ui/Settings/ShareGame/icons.png differ diff --git a/Client2020/content/textures/ui/Settings/ShareGame/icons@2x.png b/Client2020/content/textures/ui/Settings/ShareGame/icons@2x.png new file mode 100644 index 0000000..202d232 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/ShareGame/icons@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/ShareGame/icons@3x.png b/Client2020/content/textures/ui/Settings/ShareGame/icons@3x.png new file mode 100644 index 0000000..a13edee Binary files /dev/null and b/Client2020/content/textures/ui/Settings/ShareGame/icons@3x.png differ diff --git a/Client2020/content/textures/ui/Settings/Slider/BarLeft.png b/Client2020/content/textures/ui/Settings/Slider/BarLeft.png new file mode 100644 index 0000000..1787ff0 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Slider/BarLeft.png differ diff --git a/Client2020/content/textures/ui/Settings/Slider/BarLeft@2x.png b/Client2020/content/textures/ui/Settings/Slider/BarLeft@2x.png new file mode 100644 index 0000000..b6bb751 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Slider/BarLeft@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Slider/BarRight.png b/Client2020/content/textures/ui/Settings/Slider/BarRight.png new file mode 100644 index 0000000..04bca90 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Slider/BarRight.png differ diff --git a/Client2020/content/textures/ui/Settings/Slider/BarRight@2x.png b/Client2020/content/textures/ui/Settings/Slider/BarRight@2x.png new file mode 100644 index 0000000..26e8a6f Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Slider/BarRight@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Slider/Left.png b/Client2020/content/textures/ui/Settings/Slider/Left.png new file mode 100644 index 0000000..c078d2c Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Slider/Left.png differ diff --git a/Client2020/content/textures/ui/Settings/Slider/Left@2x.png b/Client2020/content/textures/ui/Settings/Slider/Left@2x.png new file mode 100644 index 0000000..3ec089d Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Slider/Left@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Slider/Less.png b/Client2020/content/textures/ui/Settings/Slider/Less.png new file mode 100644 index 0000000..afbb0a6 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Slider/Less.png differ diff --git a/Client2020/content/textures/ui/Settings/Slider/More.png b/Client2020/content/textures/ui/Settings/Slider/More.png new file mode 100644 index 0000000..05edb54 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Slider/More.png differ diff --git a/Client2020/content/textures/ui/Settings/Slider/Right.png b/Client2020/content/textures/ui/Settings/Slider/Right.png new file mode 100644 index 0000000..a607724 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Slider/Right.png differ diff --git a/Client2020/content/textures/ui/Settings/Slider/Right@2x.png b/Client2020/content/textures/ui/Settings/Slider/Right@2x.png new file mode 100644 index 0000000..53ef3f8 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Slider/Right@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Slider/SelectedBarLeft.png b/Client2020/content/textures/ui/Settings/Slider/SelectedBarLeft.png new file mode 100644 index 0000000..6c98b40 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Slider/SelectedBarLeft.png differ diff --git a/Client2020/content/textures/ui/Settings/Slider/SelectedBarLeft@2x.png b/Client2020/content/textures/ui/Settings/Slider/SelectedBarLeft@2x.png new file mode 100644 index 0000000..6476f9e Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Slider/SelectedBarLeft@2x.png differ diff --git a/Client2020/content/textures/ui/Settings/Slider/SelectedBarRight.png b/Client2020/content/textures/ui/Settings/Slider/SelectedBarRight.png new file mode 100644 index 0000000..a0e0162 Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Slider/SelectedBarRight.png differ diff --git a/Client2020/content/textures/ui/Settings/Slider/SelectedBarRight@2x.png b/Client2020/content/textures/ui/Settings/Slider/SelectedBarRight@2x.png new file mode 100644 index 0000000..d5229bc Binary files /dev/null and b/Client2020/content/textures/ui/Settings/Slider/SelectedBarRight@2x.png differ diff --git a/Client2020/content/textures/ui/SingleButton.png b/Client2020/content/textures/ui/SingleButton.png new file mode 100644 index 0000000..27d2c0d Binary files /dev/null and b/Client2020/content/textures/ui/SingleButton.png differ diff --git a/Client2020/content/textures/ui/SingleButtonDown.png b/Client2020/content/textures/ui/SingleButtonDown.png new file mode 100644 index 0000000..4e50523 Binary files /dev/null and b/Client2020/content/textures/ui/SingleButtonDown.png differ diff --git a/Client2020/content/textures/ui/Slider-BKG-Center.png b/Client2020/content/textures/ui/Slider-BKG-Center.png new file mode 100644 index 0000000..7563293 Binary files /dev/null and b/Client2020/content/textures/ui/Slider-BKG-Center.png differ diff --git a/Client2020/content/textures/ui/Slider-BKG-Center@2x.png b/Client2020/content/textures/ui/Slider-BKG-Center@2x.png new file mode 100644 index 0000000..284dae0 Binary files /dev/null and b/Client2020/content/textures/ui/Slider-BKG-Center@2x.png differ diff --git a/Client2020/content/textures/ui/Slider-BKG-Left-Cap.png b/Client2020/content/textures/ui/Slider-BKG-Left-Cap.png new file mode 100644 index 0000000..ab76642 Binary files /dev/null and b/Client2020/content/textures/ui/Slider-BKG-Left-Cap.png differ diff --git a/Client2020/content/textures/ui/Slider-BKG-Left-Cap@2x.png b/Client2020/content/textures/ui/Slider-BKG-Left-Cap@2x.png new file mode 100644 index 0000000..defda45 Binary files /dev/null and b/Client2020/content/textures/ui/Slider-BKG-Left-Cap@2x.png differ diff --git a/Client2020/content/textures/ui/Slider-BKG-Right-Cap.png b/Client2020/content/textures/ui/Slider-BKG-Right-Cap.png new file mode 100644 index 0000000..82d19f3 Binary files /dev/null and b/Client2020/content/textures/ui/Slider-BKG-Right-Cap.png differ diff --git a/Client2020/content/textures/ui/Slider-BKG-Right-Cap@2x.png b/Client2020/content/textures/ui/Slider-BKG-Right-Cap@2x.png new file mode 100644 index 0000000..0dcd8be Binary files /dev/null and b/Client2020/content/textures/ui/Slider-BKG-Right-Cap@2x.png differ diff --git a/Client2020/content/textures/ui/Slider-Fill-Center.png b/Client2020/content/textures/ui/Slider-Fill-Center.png new file mode 100644 index 0000000..a9db65e Binary files /dev/null and b/Client2020/content/textures/ui/Slider-Fill-Center.png differ diff --git a/Client2020/content/textures/ui/Slider-Fill-Center@2x.png b/Client2020/content/textures/ui/Slider-Fill-Center@2x.png new file mode 100644 index 0000000..c3355cc Binary files /dev/null and b/Client2020/content/textures/ui/Slider-Fill-Center@2x.png differ diff --git a/Client2020/content/textures/ui/Slider-Fill-Left-Cap.png b/Client2020/content/textures/ui/Slider-Fill-Left-Cap.png new file mode 100644 index 0000000..5d796da Binary files /dev/null and b/Client2020/content/textures/ui/Slider-Fill-Left-Cap.png differ diff --git a/Client2020/content/textures/ui/Slider-Fill-Left-Cap@2x.png b/Client2020/content/textures/ui/Slider-Fill-Left-Cap@2x.png new file mode 100644 index 0000000..2ff6d0b Binary files /dev/null and b/Client2020/content/textures/ui/Slider-Fill-Left-Cap@2x.png differ diff --git a/Client2020/content/textures/ui/Slider-Fill-Right-Cap.png b/Client2020/content/textures/ui/Slider-Fill-Right-Cap.png new file mode 100644 index 0000000..188106b Binary files /dev/null and b/Client2020/content/textures/ui/Slider-Fill-Right-Cap.png differ diff --git a/Client2020/content/textures/ui/Slider-Fill-Right-Cap@2x.png b/Client2020/content/textures/ui/Slider-Fill-Right-Cap@2x.png new file mode 100644 index 0000000..84ae4c8 Binary files /dev/null and b/Client2020/content/textures/ui/Slider-Fill-Right-Cap@2x.png differ diff --git a/Client2020/content/textures/ui/Slider.png b/Client2020/content/textures/ui/Slider.png new file mode 100644 index 0000000..b11c5a3 Binary files /dev/null and b/Client2020/content/textures/ui/Slider.png differ diff --git a/Client2020/content/textures/ui/Slider_dn.png b/Client2020/content/textures/ui/Slider_dn.png new file mode 100644 index 0000000..2aa542e Binary files /dev/null and b/Client2020/content/textures/ui/Slider_dn.png differ diff --git a/Client2020/content/textures/ui/Slider_sel.png b/Client2020/content/textures/ui/Slider_sel.png new file mode 100644 index 0000000..2aa542e Binary files /dev/null and b/Client2020/content/textures/ui/Slider_sel.png differ diff --git a/Client2020/content/textures/ui/TixIcon.png b/Client2020/content/textures/ui/TixIcon.png new file mode 100644 index 0000000..8b08e62 Binary files /dev/null and b/Client2020/content/textures/ui/TixIcon.png differ diff --git a/Client2020/content/textures/ui/TopBar/HealthBar.png b/Client2020/content/textures/ui/TopBar/HealthBar.png new file mode 100644 index 0000000..f9d4c2f Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/HealthBar.png differ diff --git a/Client2020/content/textures/ui/TopBar/HealthBarBase.png b/Client2020/content/textures/ui/TopBar/HealthBarBase.png new file mode 100644 index 0000000..0439bc3 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/HealthBarBase.png differ diff --git a/Client2020/content/textures/ui/TopBar/HealthBarBaseTV.png b/Client2020/content/textures/ui/TopBar/HealthBarBaseTV.png new file mode 100644 index 0000000..115a95b Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/HealthBarBaseTV.png differ diff --git a/Client2020/content/textures/ui/TopBar/HealthBarTV.png b/Client2020/content/textures/ui/TopBar/HealthBarTV.png new file mode 100644 index 0000000..8476cf1 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/HealthBarTV.png differ diff --git a/Client2020/content/textures/ui/TopBar/Round.png b/Client2020/content/textures/ui/TopBar/Round.png new file mode 100644 index 0000000..a596802 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/Round.png differ diff --git a/Client2020/content/textures/ui/TopBar/WhiteOverlayAsset.png b/Client2020/content/textures/ui/TopBar/WhiteOverlayAsset.png new file mode 100644 index 0000000..87a654a Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/WhiteOverlayAsset.png differ diff --git a/Client2020/content/textures/ui/TopBar/chatOff.png b/Client2020/content/textures/ui/TopBar/chatOff.png new file mode 100644 index 0000000..d402937 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/chatOff.png differ diff --git a/Client2020/content/textures/ui/TopBar/chatOff@2x.png b/Client2020/content/textures/ui/TopBar/chatOff@2x.png new file mode 100644 index 0000000..373df97 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/chatOff@2x.png differ diff --git a/Client2020/content/textures/ui/TopBar/chatOff@3x.png b/Client2020/content/textures/ui/TopBar/chatOff@3x.png new file mode 100644 index 0000000..b30d557 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/chatOff@3x.png differ diff --git a/Client2020/content/textures/ui/TopBar/chatOn.png b/Client2020/content/textures/ui/TopBar/chatOn.png new file mode 100644 index 0000000..ff63fba Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/chatOn.png differ diff --git a/Client2020/content/textures/ui/TopBar/chatOn@2x.png b/Client2020/content/textures/ui/TopBar/chatOn@2x.png new file mode 100644 index 0000000..873c4dc Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/chatOn@2x.png differ diff --git a/Client2020/content/textures/ui/TopBar/chatOn@3x.png b/Client2020/content/textures/ui/TopBar/chatOn@3x.png new file mode 100644 index 0000000..1ad2470 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/chatOn@3x.png differ diff --git a/Client2020/content/textures/ui/TopBar/close.png b/Client2020/content/textures/ui/TopBar/close.png new file mode 100644 index 0000000..9257b02 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/close.png differ diff --git a/Client2020/content/textures/ui/TopBar/close@2x.png b/Client2020/content/textures/ui/TopBar/close@2x.png new file mode 100644 index 0000000..970d2a8 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/close@2x.png differ diff --git a/Client2020/content/textures/ui/TopBar/close@3x.png b/Client2020/content/textures/ui/TopBar/close@3x.png new file mode 100644 index 0000000..dfebebe Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/close@3x.png differ diff --git a/Client2020/content/textures/ui/TopBar/coloredlogo.png b/Client2020/content/textures/ui/TopBar/coloredlogo.png new file mode 100644 index 0000000..0dbef90 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/coloredlogo.png differ diff --git a/Client2020/content/textures/ui/TopBar/coloredlogo@2x.png b/Client2020/content/textures/ui/TopBar/coloredlogo@2x.png new file mode 100644 index 0000000..bad82f2 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/coloredlogo@2x.png differ diff --git a/Client2020/content/textures/ui/TopBar/coloredlogo@3x.png b/Client2020/content/textures/ui/TopBar/coloredlogo@3x.png new file mode 100644 index 0000000..66cc5d6 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/coloredlogo@3x.png differ diff --git a/Client2020/content/textures/ui/TopBar/dropshadow.png b/Client2020/content/textures/ui/TopBar/dropshadow.png new file mode 100644 index 0000000..0e9aa0b Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/dropshadow.png differ diff --git a/Client2020/content/textures/ui/TopBar/dropshadow@2x.png b/Client2020/content/textures/ui/TopBar/dropshadow@2x.png new file mode 100644 index 0000000..9b14c0f Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/dropshadow@2x.png differ diff --git a/Client2020/content/textures/ui/TopBar/emotesOff.png b/Client2020/content/textures/ui/TopBar/emotesOff.png new file mode 100644 index 0000000..264fccf Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/emotesOff.png differ diff --git a/Client2020/content/textures/ui/TopBar/emotesOff@2x.png b/Client2020/content/textures/ui/TopBar/emotesOff@2x.png new file mode 100644 index 0000000..b7a4964 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/emotesOff@2x.png differ diff --git a/Client2020/content/textures/ui/TopBar/emotesOff@3x.png b/Client2020/content/textures/ui/TopBar/emotesOff@3x.png new file mode 100644 index 0000000..18c8667 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/emotesOff@3x.png differ diff --git a/Client2020/content/textures/ui/TopBar/emotesOn.png b/Client2020/content/textures/ui/TopBar/emotesOn.png new file mode 100644 index 0000000..cb94c1a Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/emotesOn.png differ diff --git a/Client2020/content/textures/ui/TopBar/emotesOn@2x.png b/Client2020/content/textures/ui/TopBar/emotesOn@2x.png new file mode 100644 index 0000000..8c856ce Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/emotesOn@2x.png differ diff --git a/Client2020/content/textures/ui/TopBar/emotesOn@3x.png b/Client2020/content/textures/ui/TopBar/emotesOn@3x.png new file mode 100644 index 0000000..7e189ab Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/emotesOn@3x.png differ diff --git a/Client2020/content/textures/ui/TopBar/iconBase.png b/Client2020/content/textures/ui/TopBar/iconBase.png new file mode 100644 index 0000000..c3e6531 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/iconBase.png differ diff --git a/Client2020/content/textures/ui/TopBar/iconBase@2x.png b/Client2020/content/textures/ui/TopBar/iconBase@2x.png new file mode 100644 index 0000000..ef9cb30 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/iconBase@2x.png differ diff --git a/Client2020/content/textures/ui/TopBar/iconBase@3x.png b/Client2020/content/textures/ui/TopBar/iconBase@3x.png new file mode 100644 index 0000000..abe4210 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/iconBase@3x.png differ diff --git a/Client2020/content/textures/ui/TopBar/inventoryOff.png b/Client2020/content/textures/ui/TopBar/inventoryOff.png new file mode 100644 index 0000000..4bbd0a3 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/inventoryOff.png differ diff --git a/Client2020/content/textures/ui/TopBar/inventoryOff@2x.png b/Client2020/content/textures/ui/TopBar/inventoryOff@2x.png new file mode 100644 index 0000000..9359065 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/inventoryOff@2x.png differ diff --git a/Client2020/content/textures/ui/TopBar/inventoryOff@3x.png b/Client2020/content/textures/ui/TopBar/inventoryOff@3x.png new file mode 100644 index 0000000..001f48a Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/inventoryOff@3x.png differ diff --git a/Client2020/content/textures/ui/TopBar/inventoryOn.png b/Client2020/content/textures/ui/TopBar/inventoryOn.png new file mode 100644 index 0000000..c0eaaa9 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/inventoryOn.png differ diff --git a/Client2020/content/textures/ui/TopBar/inventoryOn@2x.png b/Client2020/content/textures/ui/TopBar/inventoryOn@2x.png new file mode 100644 index 0000000..944c3c9 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/inventoryOn@2x.png differ diff --git a/Client2020/content/textures/ui/TopBar/inventoryOn@3x.png b/Client2020/content/textures/ui/TopBar/inventoryOn@3x.png new file mode 100644 index 0000000..bab32ca Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/inventoryOn@3x.png differ diff --git a/Client2020/content/textures/ui/TopBar/leaderboardOff.png b/Client2020/content/textures/ui/TopBar/leaderboardOff.png new file mode 100644 index 0000000..b3cfeb6 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/leaderboardOff.png differ diff --git a/Client2020/content/textures/ui/TopBar/leaderboardOff@2x.png b/Client2020/content/textures/ui/TopBar/leaderboardOff@2x.png new file mode 100644 index 0000000..fd63518 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/leaderboardOff@2x.png differ diff --git a/Client2020/content/textures/ui/TopBar/leaderboardOff@3x.png b/Client2020/content/textures/ui/TopBar/leaderboardOff@3x.png new file mode 100644 index 0000000..3521087 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/leaderboardOff@3x.png differ diff --git a/Client2020/content/textures/ui/TopBar/leaderboardOn.png b/Client2020/content/textures/ui/TopBar/leaderboardOn.png new file mode 100644 index 0000000..8184527 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/leaderboardOn.png differ diff --git a/Client2020/content/textures/ui/TopBar/leaderboardOn@2x.png b/Client2020/content/textures/ui/TopBar/leaderboardOn@2x.png new file mode 100644 index 0000000..d5c2899 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/leaderboardOn@2x.png differ diff --git a/Client2020/content/textures/ui/TopBar/leaderboardOn@3x.png b/Client2020/content/textures/ui/TopBar/leaderboardOn@3x.png new file mode 100644 index 0000000..da38dea Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/leaderboardOn@3x.png differ diff --git a/Client2020/content/textures/ui/TopBar/moreOff.png b/Client2020/content/textures/ui/TopBar/moreOff.png new file mode 100644 index 0000000..b59dac4 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/moreOff.png differ diff --git a/Client2020/content/textures/ui/TopBar/moreOff@2x.png b/Client2020/content/textures/ui/TopBar/moreOff@2x.png new file mode 100644 index 0000000..488f7ed Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/moreOff@2x.png differ diff --git a/Client2020/content/textures/ui/TopBar/moreOff@3x.png b/Client2020/content/textures/ui/TopBar/moreOff@3x.png new file mode 100644 index 0000000..0342030 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/moreOff@3x.png differ diff --git a/Client2020/content/textures/ui/TopBar/moreOn.png b/Client2020/content/textures/ui/TopBar/moreOn.png new file mode 100644 index 0000000..a0440f2 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/moreOn.png differ diff --git a/Client2020/content/textures/ui/TopBar/moreOn@2x.png b/Client2020/content/textures/ui/TopBar/moreOn@2x.png new file mode 100644 index 0000000..64813d8 Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/moreOn@2x.png differ diff --git a/Client2020/content/textures/ui/TopBar/moreOn@3x.png b/Client2020/content/textures/ui/TopBar/moreOn@3x.png new file mode 100644 index 0000000..480dc3c Binary files /dev/null and b/Client2020/content/textures/ui/TopBar/moreOn@3x.png differ diff --git a/Client2020/content/textures/ui/TopRoundedRect8px.png b/Client2020/content/textures/ui/TopRoundedRect8px.png new file mode 100644 index 0000000..237d6b4 Binary files /dev/null and b/Client2020/content/textures/ui/TopRoundedRect8px.png differ diff --git a/Client2020/content/textures/ui/TouchControlsSheet.png b/Client2020/content/textures/ui/TouchControlsSheet.png new file mode 100644 index 0000000..da0293f Binary files /dev/null and b/Client2020/content/textures/ui/TouchControlsSheet.png differ diff --git a/Client2020/content/textures/ui/VR/Radial/Icons/2DUI.png b/Client2020/content/textures/ui/VR/Radial/Icons/2DUI.png new file mode 100644 index 0000000..23ce445 Binary files /dev/null and b/Client2020/content/textures/ui/VR/Radial/Icons/2DUI.png differ diff --git a/Client2020/content/textures/ui/VR/Radial/Icons/Backpack.png b/Client2020/content/textures/ui/VR/Radial/Icons/Backpack.png new file mode 100644 index 0000000..d8a6237 Binary files /dev/null and b/Client2020/content/textures/ui/VR/Radial/Icons/Backpack.png differ diff --git a/Client2020/content/textures/ui/VR/Radial/Icons/Recenter.png b/Client2020/content/textures/ui/VR/Radial/Icons/Recenter.png new file mode 100644 index 0000000..b21a586 Binary files /dev/null and b/Client2020/content/textures/ui/VR/Radial/Icons/Recenter.png differ diff --git a/Client2020/content/textures/ui/VR/Radial/SliceActive.png b/Client2020/content/textures/ui/VR/Radial/SliceActive.png new file mode 100644 index 0000000..8776b51 Binary files /dev/null and b/Client2020/content/textures/ui/VR/Radial/SliceActive.png differ diff --git a/Client2020/content/textures/ui/VR/Radial/SliceBackground.png b/Client2020/content/textures/ui/VR/Radial/SliceBackground.png new file mode 100644 index 0000000..fd9c037 Binary files /dev/null and b/Client2020/content/textures/ui/VR/Radial/SliceBackground.png differ diff --git a/Client2020/content/textures/ui/VR/Radial/SliceDisabled.png b/Client2020/content/textures/ui/VR/Radial/SliceDisabled.png new file mode 100644 index 0000000..ad926ad Binary files /dev/null and b/Client2020/content/textures/ui/VR/Radial/SliceDisabled.png differ diff --git a/Client2020/content/textures/ui/VR/VRPointerDiscBlue.png b/Client2020/content/textures/ui/VR/VRPointerDiscBlue.png new file mode 100644 index 0000000..ceab1b0 Binary files /dev/null and b/Client2020/content/textures/ui/VR/VRPointerDiscBlue.png differ diff --git a/Client2020/content/textures/ui/VR/VRPointerDiscRed.png b/Client2020/content/textures/ui/VR/VRPointerDiscRed.png new file mode 100644 index 0000000..ba88732 Binary files /dev/null and b/Client2020/content/textures/ui/VR/VRPointerDiscRed.png differ diff --git a/Client2020/content/textures/ui/VR/button.png b/Client2020/content/textures/ui/VR/button.png new file mode 100644 index 0000000..403dfde Binary files /dev/null and b/Client2020/content/textures/ui/VR/button.png differ diff --git a/Client2020/content/textures/ui/VR/buttonActive.png b/Client2020/content/textures/ui/VR/buttonActive.png new file mode 100644 index 0000000..f685760 Binary files /dev/null and b/Client2020/content/textures/ui/VR/buttonActive.png differ diff --git a/Client2020/content/textures/ui/VR/buttonBackground.png b/Client2020/content/textures/ui/VR/buttonBackground.png new file mode 100644 index 0000000..379241a Binary files /dev/null and b/Client2020/content/textures/ui/VR/buttonBackground.png differ diff --git a/Client2020/content/textures/ui/VR/buttonHover.png b/Client2020/content/textures/ui/VR/buttonHover.png new file mode 100644 index 0000000..916ddff Binary files /dev/null and b/Client2020/content/textures/ui/VR/buttonHover.png differ diff --git a/Client2020/content/textures/ui/VR/buttonSelected.png b/Client2020/content/textures/ui/VR/buttonSelected.png new file mode 100644 index 0000000..6759aea Binary files /dev/null and b/Client2020/content/textures/ui/VR/buttonSelected.png differ diff --git a/Client2020/content/textures/ui/VR/chat.png b/Client2020/content/textures/ui/VR/chat.png new file mode 100644 index 0000000..5017f20 Binary files /dev/null and b/Client2020/content/textures/ui/VR/chat.png differ diff --git a/Client2020/content/textures/ui/VR/circleWhite.png b/Client2020/content/textures/ui/VR/circleWhite.png new file mode 100644 index 0000000..728fe7b Binary files /dev/null and b/Client2020/content/textures/ui/VR/circleWhite.png differ diff --git a/Client2020/content/textures/ui/VR/closeButtonPadded.png b/Client2020/content/textures/ui/VR/closeButtonPadded.png new file mode 100644 index 0000000..0db5f8d Binary files /dev/null and b/Client2020/content/textures/ui/VR/closeButtonPadded.png differ diff --git a/Client2020/content/textures/ui/VR/hamburger.png b/Client2020/content/textures/ui/VR/hamburger.png new file mode 100644 index 0000000..a9d70c5 Binary files /dev/null and b/Client2020/content/textures/ui/VR/hamburger.png differ diff --git a/Client2020/content/textures/ui/VR/hoverPopupLeft.png b/Client2020/content/textures/ui/VR/hoverPopupLeft.png new file mode 100644 index 0000000..484b0d2 Binary files /dev/null and b/Client2020/content/textures/ui/VR/hoverPopupLeft.png differ diff --git a/Client2020/content/textures/ui/VR/hoverPopupMid.png b/Client2020/content/textures/ui/VR/hoverPopupMid.png new file mode 100644 index 0000000..0f55008 Binary files /dev/null and b/Client2020/content/textures/ui/VR/hoverPopupMid.png differ diff --git a/Client2020/content/textures/ui/VR/hoverPopupRight.png b/Client2020/content/textures/ui/VR/hoverPopupRight.png new file mode 100644 index 0000000..ea0b32a Binary files /dev/null and b/Client2020/content/textures/ui/VR/hoverPopupRight.png differ diff --git a/Client2020/content/textures/ui/VR/notifications.png b/Client2020/content/textures/ui/VR/notifications.png new file mode 100644 index 0000000..ce18573 Binary files /dev/null and b/Client2020/content/textures/ui/VR/notifications.png differ diff --git a/Client2020/content/textures/ui/VR/notifier_glow.png b/Client2020/content/textures/ui/VR/notifier_glow.png new file mode 100644 index 0000000..9483328 Binary files /dev/null and b/Client2020/content/textures/ui/VR/notifier_glow.png differ diff --git a/Client2020/content/textures/ui/VR/recenter.png b/Client2020/content/textures/ui/VR/recenter.png new file mode 100644 index 0000000..a287342 Binary files /dev/null and b/Client2020/content/textures/ui/VR/recenter.png differ diff --git a/Client2020/content/textures/ui/VR/recenterFrame.png b/Client2020/content/textures/ui/VR/recenterFrame.png new file mode 100644 index 0000000..1f02bdf Binary files /dev/null and b/Client2020/content/textures/ui/VR/recenterFrame.png differ diff --git a/Client2020/content/textures/ui/VR/rectBackground.png b/Client2020/content/textures/ui/VR/rectBackground.png new file mode 100644 index 0000000..758edfe Binary files /dev/null and b/Client2020/content/textures/ui/VR/rectBackground.png differ diff --git a/Client2020/content/textures/ui/VR/rectBackgroundWhite.png b/Client2020/content/textures/ui/VR/rectBackgroundWhite.png new file mode 100644 index 0000000..0446670 Binary files /dev/null and b/Client2020/content/textures/ui/VR/rectBackgroundWhite.png differ diff --git a/Client2020/content/textures/ui/VR/toggle2D.png b/Client2020/content/textures/ui/VR/toggle2D.png new file mode 100644 index 0000000..78def18 Binary files /dev/null and b/Client2020/content/textures/ui/VR/toggle2D.png differ diff --git a/Client2020/content/textures/ui/Vehicle/SpeedBar.png b/Client2020/content/textures/ui/Vehicle/SpeedBar.png new file mode 100644 index 0000000..1913f2b Binary files /dev/null and b/Client2020/content/textures/ui/Vehicle/SpeedBar.png differ diff --git a/Client2020/content/textures/ui/Vehicle/SpeedBar@2x.png b/Client2020/content/textures/ui/Vehicle/SpeedBar@2x.png new file mode 100644 index 0000000..3935f79 Binary files /dev/null and b/Client2020/content/textures/ui/Vehicle/SpeedBar@2x.png differ diff --git a/Client2020/content/textures/ui/Vehicle/SpeedBarBKG.png b/Client2020/content/textures/ui/Vehicle/SpeedBarBKG.png new file mode 100644 index 0000000..af55200 Binary files /dev/null and b/Client2020/content/textures/ui/Vehicle/SpeedBarBKG.png differ diff --git a/Client2020/content/textures/ui/Vehicle/SpeedBarBKG@2x.png b/Client2020/content/textures/ui/Vehicle/SpeedBarBKG@2x.png new file mode 100644 index 0000000..b85a6e5 Binary files /dev/null and b/Client2020/content/textures/ui/Vehicle/SpeedBarBKG@2x.png differ diff --git a/Client2020/content/textures/ui/Vehicle/SpeedBarEmpty.png b/Client2020/content/textures/ui/Vehicle/SpeedBarEmpty.png new file mode 100644 index 0000000..7889a14 Binary files /dev/null and b/Client2020/content/textures/ui/Vehicle/SpeedBarEmpty.png differ diff --git a/Client2020/content/textures/ui/Vehicle/SpeedBarEmpty@2x.png b/Client2020/content/textures/ui/Vehicle/SpeedBarEmpty@2x.png new file mode 100644 index 0000000..880b096 Binary files /dev/null and b/Client2020/content/textures/ui/Vehicle/SpeedBarEmpty@2x.png differ diff --git a/Client2020/content/textures/ui/account_over13.png b/Client2020/content/textures/ui/account_over13.png new file mode 100644 index 0000000..0a91b6c Binary files /dev/null and b/Client2020/content/textures/ui/account_over13.png differ diff --git a/Client2020/content/textures/ui/account_under13.png b/Client2020/content/textures/ui/account_under13.png new file mode 100644 index 0000000..8ecf24c Binary files /dev/null and b/Client2020/content/textures/ui/account_under13.png differ diff --git a/Client2020/content/textures/ui/btn_grey.png b/Client2020/content/textures/ui/btn_grey.png new file mode 100644 index 0000000..3ae3fff Binary files /dev/null and b/Client2020/content/textures/ui/btn_grey.png differ diff --git a/Client2020/content/textures/ui/btn_greyTransp.png b/Client2020/content/textures/ui/btn_greyTransp.png new file mode 100644 index 0000000..0a39a1e Binary files /dev/null and b/Client2020/content/textures/ui/btn_greyTransp.png differ diff --git a/Client2020/content/textures/ui/btn_newBlue.png b/Client2020/content/textures/ui/btn_newBlue.png new file mode 100644 index 0000000..d7b482e Binary files /dev/null and b/Client2020/content/textures/ui/btn_newBlue.png differ diff --git a/Client2020/content/textures/ui/btn_newBlue@2x.png b/Client2020/content/textures/ui/btn_newBlue@2x.png new file mode 100644 index 0000000..ca2f2e9 Binary files /dev/null and b/Client2020/content/textures/ui/btn_newBlue@2x.png differ diff --git a/Client2020/content/textures/ui/btn_newBlueGlow.png b/Client2020/content/textures/ui/btn_newBlueGlow.png new file mode 100644 index 0000000..1b49a6c Binary files /dev/null and b/Client2020/content/textures/ui/btn_newBlueGlow.png differ diff --git a/Client2020/content/textures/ui/btn_newBlueGlow@2x.png b/Client2020/content/textures/ui/btn_newBlueGlow@2x.png new file mode 100644 index 0000000..379ec28 Binary files /dev/null and b/Client2020/content/textures/ui/btn_newBlueGlow@2x.png differ diff --git a/Client2020/content/textures/ui/btn_newGrey.png b/Client2020/content/textures/ui/btn_newGrey.png new file mode 100644 index 0000000..fb6cb06 Binary files /dev/null and b/Client2020/content/textures/ui/btn_newGrey.png differ diff --git a/Client2020/content/textures/ui/btn_newGrey@2x.png b/Client2020/content/textures/ui/btn_newGrey@2x.png new file mode 100644 index 0000000..b877522 Binary files /dev/null and b/Client2020/content/textures/ui/btn_newGrey@2x.png differ diff --git a/Client2020/content/textures/ui/btn_newGreyGlow.png b/Client2020/content/textures/ui/btn_newGreyGlow.png new file mode 100644 index 0000000..fc3e25a Binary files /dev/null and b/Client2020/content/textures/ui/btn_newGreyGlow.png differ diff --git a/Client2020/content/textures/ui/btn_newGreyGlow@2x.png b/Client2020/content/textures/ui/btn_newGreyGlow@2x.png new file mode 100644 index 0000000..017b746 Binary files /dev/null and b/Client2020/content/textures/ui/btn_newGreyGlow@2x.png differ diff --git a/Client2020/content/textures/ui/btn_newWhite.png b/Client2020/content/textures/ui/btn_newWhite.png new file mode 100644 index 0000000..4e415ee Binary files /dev/null and b/Client2020/content/textures/ui/btn_newWhite.png differ diff --git a/Client2020/content/textures/ui/btn_newWhite@2x.png b/Client2020/content/textures/ui/btn_newWhite@2x.png new file mode 100644 index 0000000..82c0825 Binary files /dev/null and b/Client2020/content/textures/ui/btn_newWhite@2x.png differ diff --git a/Client2020/content/textures/ui/btn_newWhiteGlow.png b/Client2020/content/textures/ui/btn_newWhiteGlow.png new file mode 100644 index 0000000..0373243 Binary files /dev/null and b/Client2020/content/textures/ui/btn_newWhiteGlow.png differ diff --git a/Client2020/content/textures/ui/btn_newWhiteGlow@2x.png b/Client2020/content/textures/ui/btn_newWhiteGlow@2x.png new file mode 100644 index 0000000..e03f4dc Binary files /dev/null and b/Client2020/content/textures/ui/btn_newWhiteGlow@2x.png differ diff --git a/Client2020/content/textures/ui/btn_red.png b/Client2020/content/textures/ui/btn_red.png new file mode 100644 index 0000000..4ab7dec Binary files /dev/null and b/Client2020/content/textures/ui/btn_red.png differ diff --git a/Client2020/content/textures/ui/btn_redGlow.png b/Client2020/content/textures/ui/btn_redGlow.png new file mode 100644 index 0000000..5d6249e Binary files /dev/null and b/Client2020/content/textures/ui/btn_redGlow.png differ diff --git a/Client2020/content/textures/ui/btn_white.png b/Client2020/content/textures/ui/btn_white.png new file mode 100644 index 0000000..4c6d4c8 Binary files /dev/null and b/Client2020/content/textures/ui/btn_white.png differ diff --git a/Client2020/content/textures/ui/chatBubble_blue_notify_bkg.png b/Client2020/content/textures/ui/chatBubble_blue_notify_bkg.png new file mode 100644 index 0000000..a7925dc Binary files /dev/null and b/Client2020/content/textures/ui/chatBubble_blue_notify_bkg.png differ diff --git a/Client2020/content/textures/ui/chatBubble_green_notify_bkg.png b/Client2020/content/textures/ui/chatBubble_green_notify_bkg.png new file mode 100644 index 0000000..1f38d6a Binary files /dev/null and b/Client2020/content/textures/ui/chatBubble_green_notify_bkg.png differ diff --git a/Client2020/content/textures/ui/chatBubble_red_notify_bkg.png b/Client2020/content/textures/ui/chatBubble_red_notify_bkg.png new file mode 100644 index 0000000..7503309 Binary files /dev/null and b/Client2020/content/textures/ui/chatBubble_red_notify_bkg.png differ diff --git a/Client2020/content/textures/ui/chatBubble_white_notify_bkg.png b/Client2020/content/textures/ui/chatBubble_white_notify_bkg.png new file mode 100644 index 0000000..59be44c Binary files /dev/null and b/Client2020/content/textures/ui/chatBubble_white_notify_bkg.png differ diff --git a/Client2020/content/textures/ui/chat_teamButton.png b/Client2020/content/textures/ui/chat_teamButton.png new file mode 100644 index 0000000..ecceb50 Binary files /dev/null and b/Client2020/content/textures/ui/chat_teamButton.png differ diff --git a/Client2020/content/textures/ui/chat_teamButton@2x.png b/Client2020/content/textures/ui/chat_teamButton@2x.png new file mode 100644 index 0000000..d36c05e Binary files /dev/null and b/Client2020/content/textures/ui/chat_teamButton@2x.png differ diff --git a/Client2020/content/textures/ui/clb_robux_20.png b/Client2020/content/textures/ui/clb_robux_20.png new file mode 100644 index 0000000..33dba39 Binary files /dev/null and b/Client2020/content/textures/ui/clb_robux_20.png differ diff --git a/Client2020/content/textures/ui/clb_robux_20@2x.png b/Client2020/content/textures/ui/clb_robux_20@2x.png new file mode 100644 index 0000000..01caf4c Binary files /dev/null and b/Client2020/content/textures/ui/clb_robux_20@2x.png differ diff --git a/Client2020/content/textures/ui/clb_robux_20@3x.png b/Client2020/content/textures/ui/clb_robux_20@3x.png new file mode 100644 index 0000000..28f1c90 Binary files /dev/null and b/Client2020/content/textures/ui/clb_robux_20@3x.png differ diff --git a/Client2020/content/textures/ui/common/robux.png b/Client2020/content/textures/ui/common/robux.png new file mode 100644 index 0000000..ec76c30 Binary files /dev/null and b/Client2020/content/textures/ui/common/robux.png differ diff --git a/Client2020/content/textures/ui/common/robux@2x.png b/Client2020/content/textures/ui/common/robux@2x.png new file mode 100644 index 0000000..3666e3b Binary files /dev/null and b/Client2020/content/textures/ui/common/robux@2x.png differ diff --git a/Client2020/content/textures/ui/common/robux@3x.png b/Client2020/content/textures/ui/common/robux@3x.png new file mode 100644 index 0000000..303a061 Binary files /dev/null and b/Client2020/content/textures/ui/common/robux@3x.png differ diff --git a/Client2020/content/textures/ui/common/robux_color.png b/Client2020/content/textures/ui/common/robux_color.png new file mode 100644 index 0000000..1db38ba Binary files /dev/null and b/Client2020/content/textures/ui/common/robux_color.png differ diff --git a/Client2020/content/textures/ui/common/robux_color@2x.png b/Client2020/content/textures/ui/common/robux_color@2x.png new file mode 100644 index 0000000..8774a12 Binary files /dev/null and b/Client2020/content/textures/ui/common/robux_color@2x.png differ diff --git a/Client2020/content/textures/ui/common/robux_color@3x.png b/Client2020/content/textures/ui/common/robux_color@3x.png new file mode 100644 index 0000000..cec394c Binary files /dev/null and b/Client2020/content/textures/ui/common/robux_color@3x.png differ diff --git a/Client2020/content/textures/ui/common/robux_small.png b/Client2020/content/textures/ui/common/robux_small.png new file mode 100644 index 0000000..545e5cc Binary files /dev/null and b/Client2020/content/textures/ui/common/robux_small.png differ diff --git a/Client2020/content/textures/ui/common/robux_small@2x.png b/Client2020/content/textures/ui/common/robux_small@2x.png new file mode 100644 index 0000000..6619fb6 Binary files /dev/null and b/Client2020/content/textures/ui/common/robux_small@2x.png differ diff --git a/Client2020/content/textures/ui/common/robux_small@3x.png b/Client2020/content/textures/ui/common/robux_small@3x.png new file mode 100644 index 0000000..3dd1fff Binary files /dev/null and b/Client2020/content/textures/ui/common/robux_small@3x.png differ diff --git a/Client2020/content/textures/ui/dialog_blue.png b/Client2020/content/textures/ui/dialog_blue.png new file mode 100644 index 0000000..96b9adb Binary files /dev/null and b/Client2020/content/textures/ui/dialog_blue.png differ diff --git a/Client2020/content/textures/ui/dialog_blue@2x.png b/Client2020/content/textures/ui/dialog_blue@2x.png new file mode 100644 index 0000000..c40fc31 Binary files /dev/null and b/Client2020/content/textures/ui/dialog_blue@2x.png differ diff --git a/Client2020/content/textures/ui/dialog_green.png b/Client2020/content/textures/ui/dialog_green.png new file mode 100644 index 0000000..64d012c Binary files /dev/null and b/Client2020/content/textures/ui/dialog_green.png differ diff --git a/Client2020/content/textures/ui/dialog_green@2x.png b/Client2020/content/textures/ui/dialog_green@2x.png new file mode 100644 index 0000000..775b4f9 Binary files /dev/null and b/Client2020/content/textures/ui/dialog_green@2x.png differ diff --git a/Client2020/content/textures/ui/dialog_purpose_help.png b/Client2020/content/textures/ui/dialog_purpose_help.png new file mode 100644 index 0000000..311d441 Binary files /dev/null and b/Client2020/content/textures/ui/dialog_purpose_help.png differ diff --git a/Client2020/content/textures/ui/dialog_purpose_quest.png b/Client2020/content/textures/ui/dialog_purpose_quest.png new file mode 100644 index 0000000..14efd26 Binary files /dev/null and b/Client2020/content/textures/ui/dialog_purpose_quest.png differ diff --git a/Client2020/content/textures/ui/dialog_purpose_shop.png b/Client2020/content/textures/ui/dialog_purpose_shop.png new file mode 100644 index 0000000..df2bb7f Binary files /dev/null and b/Client2020/content/textures/ui/dialog_purpose_shop.png differ diff --git a/Client2020/content/textures/ui/dialog_red.png b/Client2020/content/textures/ui/dialog_red.png new file mode 100644 index 0000000..6b3c644 Binary files /dev/null and b/Client2020/content/textures/ui/dialog_red.png differ diff --git a/Client2020/content/textures/ui/dialog_red@2x.png b/Client2020/content/textures/ui/dialog_red@2x.png new file mode 100644 index 0000000..c62b873 Binary files /dev/null and b/Client2020/content/textures/ui/dialog_red@2x.png differ diff --git a/Client2020/content/textures/ui/dialog_tail.png b/Client2020/content/textures/ui/dialog_tail.png new file mode 100644 index 0000000..b9686bc Binary files /dev/null and b/Client2020/content/textures/ui/dialog_tail.png differ diff --git a/Client2020/content/textures/ui/dialog_tail@2x.png b/Client2020/content/textures/ui/dialog_tail@2x.png new file mode 100644 index 0000000..ff18256 Binary files /dev/null and b/Client2020/content/textures/ui/dialog_tail@2x.png differ diff --git a/Client2020/content/textures/ui/dialog_white.png b/Client2020/content/textures/ui/dialog_white.png new file mode 100644 index 0000000..9911129 Binary files /dev/null and b/Client2020/content/textures/ui/dialog_white.png differ diff --git a/Client2020/content/textures/ui/dialog_white@2x.png b/Client2020/content/textures/ui/dialog_white@2x.png new file mode 100644 index 0000000..f80f843 Binary files /dev/null and b/Client2020/content/textures/ui/dialog_white@2x.png differ diff --git a/Client2020/content/textures/ui/dropdown_arrow.png b/Client2020/content/textures/ui/dropdown_arrow.png new file mode 100644 index 0000000..eddc0be Binary files /dev/null and b/Client2020/content/textures/ui/dropdown_arrow.png differ diff --git a/Client2020/content/textures/ui/dropdown_arrow@2x.png b/Client2020/content/textures/ui/dropdown_arrow@2x.png new file mode 100644 index 0000000..0db921e Binary files /dev/null and b/Client2020/content/textures/ui/dropdown_arrow@2x.png differ diff --git a/Client2020/content/textures/ui/homeButton.png b/Client2020/content/textures/ui/homeButton.png new file mode 100644 index 0000000..b9ad006 Binary files /dev/null and b/Client2020/content/textures/ui/homeButton.png differ diff --git a/Client2020/content/textures/ui/homeButton@2x.png b/Client2020/content/textures/ui/homeButton@2x.png new file mode 100644 index 0000000..2c2cdc0 Binary files /dev/null and b/Client2020/content/textures/ui/homeButton@2x.png differ diff --git a/Client2020/content/textures/ui/icon_BC-16.png b/Client2020/content/textures/ui/icon_BC-16.png new file mode 100644 index 0000000..dedd8d4 Binary files /dev/null and b/Client2020/content/textures/ui/icon_BC-16.png differ diff --git a/Client2020/content/textures/ui/icon_OBC-16.png b/Client2020/content/textures/ui/icon_OBC-16.png new file mode 100644 index 0000000..e539d5d Binary files /dev/null and b/Client2020/content/textures/ui/icon_OBC-16.png differ diff --git a/Client2020/content/textures/ui/icon_TBC-16.png b/Client2020/content/textures/ui/icon_TBC-16.png new file mode 100644 index 0000000..208dde3 Binary files /dev/null and b/Client2020/content/textures/ui/icon_TBC-16.png differ diff --git a/Client2020/content/textures/ui/icon_admin-16.png b/Client2020/content/textures/ui/icon_admin-16.png new file mode 100644 index 0000000..ae240d5 Binary files /dev/null and b/Client2020/content/textures/ui/icon_admin-16.png differ diff --git a/Client2020/content/textures/ui/icon_follower-16.png b/Client2020/content/textures/ui/icon_follower-16.png new file mode 100644 index 0000000..20dd9f1 Binary files /dev/null and b/Client2020/content/textures/ui/icon_follower-16.png differ diff --git a/Client2020/content/textures/ui/icon_following-16.png b/Client2020/content/textures/ui/icon_following-16.png new file mode 100644 index 0000000..a75dd9f Binary files /dev/null and b/Client2020/content/textures/ui/icon_following-16.png differ diff --git a/Client2020/content/textures/ui/icon_friendrequestrecieved-16.png b/Client2020/content/textures/ui/icon_friendrequestrecieved-16.png new file mode 100644 index 0000000..f7972f1 Binary files /dev/null and b/Client2020/content/textures/ui/icon_friendrequestrecieved-16.png differ diff --git a/Client2020/content/textures/ui/icon_friendrequestsent_16.png b/Client2020/content/textures/ui/icon_friendrequestsent_16.png new file mode 100644 index 0000000..947a5cf Binary files /dev/null and b/Client2020/content/textures/ui/icon_friendrequestsent_16.png differ diff --git a/Client2020/content/textures/ui/icon_friends_16.png b/Client2020/content/textures/ui/icon_friends_16.png new file mode 100644 index 0000000..17c5369 Binary files /dev/null and b/Client2020/content/textures/ui/icon_friends_16.png differ diff --git a/Client2020/content/textures/ui/icon_intern-16.png b/Client2020/content/textures/ui/icon_intern-16.png new file mode 100644 index 0000000..b4afb27 Binary files /dev/null and b/Client2020/content/textures/ui/icon_intern-16.png differ diff --git a/Client2020/content/textures/ui/icon_localization-16.png b/Client2020/content/textures/ui/icon_localization-16.png new file mode 100644 index 0000000..627b160 Binary files /dev/null and b/Client2020/content/textures/ui/icon_localization-16.png differ diff --git a/Client2020/content/textures/ui/icon_mutualfollowing-16.png b/Client2020/content/textures/ui/icon_mutualfollowing-16.png new file mode 100644 index 0000000..40adece Binary files /dev/null and b/Client2020/content/textures/ui/icon_mutualfollowing-16.png differ diff --git a/Client2020/content/textures/ui/icon_placeowner.png b/Client2020/content/textures/ui/icon_placeowner.png new file mode 100644 index 0000000..30f66ff Binary files /dev/null and b/Client2020/content/textures/ui/icon_placeowner.png differ diff --git a/Client2020/content/textures/ui/icon_premium-16.png b/Client2020/content/textures/ui/icon_premium-16.png new file mode 100644 index 0000000..912d9d2 Binary files /dev/null and b/Client2020/content/textures/ui/icon_premium-16.png differ diff --git a/Client2020/content/textures/ui/icon_star-16.png b/Client2020/content/textures/ui/icon_star-16.png new file mode 100644 index 0000000..189b9ca Binary files /dev/null and b/Client2020/content/textures/ui/icon_star-16.png differ diff --git a/Client2020/content/textures/ui/mouseLock_off.png b/Client2020/content/textures/ui/mouseLock_off.png new file mode 100644 index 0000000..c0b562a Binary files /dev/null and b/Client2020/content/textures/ui/mouseLock_off.png differ diff --git a/Client2020/content/textures/ui/mouseLock_off@2x.png b/Client2020/content/textures/ui/mouseLock_off@2x.png new file mode 100644 index 0000000..3773877 Binary files /dev/null and b/Client2020/content/textures/ui/mouseLock_off@2x.png differ diff --git a/Client2020/content/textures/ui/mouseLock_on.png b/Client2020/content/textures/ui/mouseLock_on.png new file mode 100644 index 0000000..9728774 Binary files /dev/null and b/Client2020/content/textures/ui/mouseLock_on.png differ diff --git a/Client2020/content/textures/ui/mouseLock_on@2x.png b/Client2020/content/textures/ui/mouseLock_on@2x.png new file mode 100644 index 0000000..c789d46 Binary files /dev/null and b/Client2020/content/textures/ui/mouseLock_on@2x.png differ diff --git a/Client2020/content/textures/ui/move.png b/Client2020/content/textures/ui/move.png new file mode 100644 index 0000000..d081458 Binary files /dev/null and b/Client2020/content/textures/ui/move.png differ diff --git a/Client2020/content/textures/ui/newBkg_square.png b/Client2020/content/textures/ui/newBkg_square.png new file mode 100644 index 0000000..901ffc1 Binary files /dev/null and b/Client2020/content/textures/ui/newBkg_square.png differ diff --git a/Client2020/content/textures/ui/newBkg_square@2x.png b/Client2020/content/textures/ui/newBkg_square@2x.png new file mode 100644 index 0000000..6956f48 Binary files /dev/null and b/Client2020/content/textures/ui/newBkg_square@2x.png differ diff --git a/Client2020/content/textures/ui/scroll-bottom.png b/Client2020/content/textures/ui/scroll-bottom.png new file mode 100644 index 0000000..f186156 Binary files /dev/null and b/Client2020/content/textures/ui/scroll-bottom.png differ diff --git a/Client2020/content/textures/ui/scroll-bottom@2x.png b/Client2020/content/textures/ui/scroll-bottom@2x.png new file mode 100644 index 0000000..3fb978f Binary files /dev/null and b/Client2020/content/textures/ui/scroll-bottom@2x.png differ diff --git a/Client2020/content/textures/ui/scroll-middle.png b/Client2020/content/textures/ui/scroll-middle.png new file mode 100644 index 0000000..1a7cfa7 Binary files /dev/null and b/Client2020/content/textures/ui/scroll-middle.png differ diff --git a/Client2020/content/textures/ui/scroll-middle@2x.png b/Client2020/content/textures/ui/scroll-middle@2x.png new file mode 100644 index 0000000..a101d48 Binary files /dev/null and b/Client2020/content/textures/ui/scroll-middle@2x.png differ diff --git a/Client2020/content/textures/ui/scroll-top.png b/Client2020/content/textures/ui/scroll-top.png new file mode 100644 index 0000000..1c85194 Binary files /dev/null and b/Client2020/content/textures/ui/scroll-top.png differ diff --git a/Client2020/content/textures/ui/scroll-top@2x.png b/Client2020/content/textures/ui/scroll-top@2x.png new file mode 100644 index 0000000..0720737 Binary files /dev/null and b/Client2020/content/textures/ui/scroll-top@2x.png differ diff --git a/Client2020/content/textures/ui/scrollbar.png b/Client2020/content/textures/ui/scrollbar.png new file mode 100644 index 0000000..1a3f361 Binary files /dev/null and b/Client2020/content/textures/ui/scrollbar.png differ diff --git a/Client2020/content/textures/ui/scrollbuttonDown.png b/Client2020/content/textures/ui/scrollbuttonDown.png new file mode 100644 index 0000000..fc6be92 Binary files /dev/null and b/Client2020/content/textures/ui/scrollbuttonDown.png differ diff --git a/Client2020/content/textures/ui/scrollbuttonDown_dn.png b/Client2020/content/textures/ui/scrollbuttonDown_dn.png new file mode 100644 index 0000000..38f1d5f Binary files /dev/null and b/Client2020/content/textures/ui/scrollbuttonDown_dn.png differ diff --git a/Client2020/content/textures/ui/scrollbuttonDown_ds.png b/Client2020/content/textures/ui/scrollbuttonDown_ds.png new file mode 100644 index 0000000..fb7f2f3 Binary files /dev/null and b/Client2020/content/textures/ui/scrollbuttonDown_ds.png differ diff --git a/Client2020/content/textures/ui/scrollbuttonDown_ovr.png b/Client2020/content/textures/ui/scrollbuttonDown_ovr.png new file mode 100644 index 0000000..38f1d5f Binary files /dev/null and b/Client2020/content/textures/ui/scrollbuttonDown_ovr.png differ diff --git a/Client2020/content/textures/ui/scrollbuttonUp.png b/Client2020/content/textures/ui/scrollbuttonUp.png new file mode 100644 index 0000000..311b520 Binary files /dev/null and b/Client2020/content/textures/ui/scrollbuttonUp.png differ diff --git a/Client2020/content/textures/ui/scrollbuttonUp_dn.png b/Client2020/content/textures/ui/scrollbuttonUp_dn.png new file mode 100644 index 0000000..aef3474 Binary files /dev/null and b/Client2020/content/textures/ui/scrollbuttonUp_dn.png differ diff --git a/Client2020/content/textures/ui/scrollbuttonUp_ds.png b/Client2020/content/textures/ui/scrollbuttonUp_ds.png new file mode 100644 index 0000000..551dbe7 Binary files /dev/null and b/Client2020/content/textures/ui/scrollbuttonUp_ds.png differ diff --git a/Client2020/content/textures/ui/scrollbuttonUp_ovr.png b/Client2020/content/textures/ui/scrollbuttonUp_ovr.png new file mode 100644 index 0000000..aef3474 Binary files /dev/null and b/Client2020/content/textures/ui/scrollbuttonUp_ovr.png differ diff --git a/Client2020/content/textures/ui/slider_new_tab.png b/Client2020/content/textures/ui/slider_new_tab.png new file mode 100644 index 0000000..ccc9ee1 Binary files /dev/null and b/Client2020/content/textures/ui/slider_new_tab.png differ diff --git a/Client2020/content/textures/ui/slider_new_tab@2x.png b/Client2020/content/textures/ui/slider_new_tab@2x.png new file mode 100644 index 0000000..07231fc Binary files /dev/null and b/Client2020/content/textures/ui/slider_new_tab@2x.png differ diff --git a/Client2020/content/textures/ui/traildot.png b/Client2020/content/textures/ui/traildot.png new file mode 100644 index 0000000..de2c6f1 Binary files /dev/null and b/Client2020/content/textures/ui/traildot.png differ diff --git a/Client2020/content/textures/ui/vr_active.png b/Client2020/content/textures/ui/vr_active.png new file mode 100644 index 0000000..bda5347 Binary files /dev/null and b/Client2020/content/textures/ui/vr_active.png differ diff --git a/Client2020/content/textures/ui/vr_idle.png b/Client2020/content/textures/ui/vr_idle.png new file mode 100644 index 0000000..62b95c8 Binary files /dev/null and b/Client2020/content/textures/ui/vr_idle.png differ diff --git a/Client2020/content/textures/ui/waypoint.png b/Client2020/content/textures/ui/waypoint.png new file mode 100644 index 0000000..17d23aa Binary files /dev/null and b/Client2020/content/textures/ui/waypoint.png differ diff --git a/Client2020/content/textures/whiteCircle.png b/Client2020/content/textures/whiteCircle.png new file mode 100644 index 0000000..e1e5e4c Binary files /dev/null and b/Client2020/content/textures/whiteCircle.png differ diff --git a/Client2020/shaders/keepme b/Client2020/shaders/keepme new file mode 100644 index 0000000..e69de29 diff --git a/Client2020/shaders/shaders_d3d10.pack b/Client2020/shaders/shaders_d3d10.pack new file mode 100644 index 0000000..6d7e1b5 Binary files /dev/null and b/Client2020/shaders/shaders_d3d10.pack differ diff --git a/Client2020/shaders/shaders_d3d10_1.pack b/Client2020/shaders/shaders_d3d10_1.pack new file mode 100644 index 0000000..59e4330 Binary files /dev/null and b/Client2020/shaders/shaders_d3d10_1.pack differ diff --git a/Client2020/shaders/shaders_d3d11.pack b/Client2020/shaders/shaders_d3d11.pack new file mode 100644 index 0000000..f01f0c1 Binary files /dev/null and b/Client2020/shaders/shaders_d3d11.pack differ diff --git a/Client2020/shaders/shaders_d3d9.pack b/Client2020/shaders/shaders_d3d9.pack new file mode 100644 index 0000000..6f76641 Binary files /dev/null and b/Client2020/shaders/shaders_d3d9.pack differ diff --git a/Client2020/shaders/shaders_glsl.pack b/Client2020/shaders/shaders_glsl.pack new file mode 100644 index 0000000..9401092 Binary files /dev/null and b/Client2020/shaders/shaders_glsl.pack differ diff --git a/Client2020/shaders/shaders_glsl3.pack b/Client2020/shaders/shaders_glsl3.pack new file mode 100644 index 0000000..3db2eb1 Binary files /dev/null and b/Client2020/shaders/shaders_glsl3.pack differ diff --git a/Client2020/shaders/shaders_vulkan_desktop.pack b/Client2020/shaders/shaders_vulkan_desktop.pack new file mode 100644 index 0000000..1c221c8 Binary files /dev/null and b/Client2020/shaders/shaders_vulkan_desktop.pack differ diff --git a/Client2020/ssl/cacert.pem b/Client2020/ssl/cacert.pem new file mode 100644 index 0000000..f9bd706 --- /dev/null +++ b/Client2020/ssl/cacert.pem @@ -0,0 +1,3447 @@ +## +## Bundle of CA Root Certificates +## +## Certificate data from Mozilla as of: Wed Jul 22 03:12:14 2020 GMT +## +## This is a bundle of X.509 certificates of public Certificate Authorities +## (CA). These were automatically extracted from Mozilla's root certificates +## file (certdata.txt). This file can be found in the mozilla source tree: +## https://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt +## +## It contains the certificates in PEM format and therefore +## can be directly used with curl / libcurl / php_curl, or with +## an Apache+mod_ssl webserver for SSL client authentication. +## Just configure this file as the SSLCACertificateFile. +## +## Conversion done with mk-ca-bundle.pl version 1.28. +## SHA256: cc6408bd4be7fbfb8699bdb40ccb7f6de5780d681d87785ea362646e4dad5e8e +## + + +GlobalSign Root CA +================== +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx +GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds +b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV +BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD +VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa +DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc +THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb +Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP +c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX +gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF +AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj +Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG +j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH +hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC +X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +GlobalSign Root CA - R2 +======================= +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UECxMXR2xv +YmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh +bFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT +aWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6 +ErPLv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8eoLrvozp +s6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjN +S7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CL +TfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pazq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6C +ygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUm+IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5nbG9i +YWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjAN +BgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp +9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu +01yiPqFbQfXf5WRDLenVOavSot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG7 +9G+dwfCMNYxdAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +Entrust.net Premium 2048 Secure Server CA +========================================= +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u +ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp +bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV +BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx +NzUwNTFaFw0yOTA3MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3 +d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl +MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u +ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL +Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr +hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW +nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi +VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJ +KoZIhvcNAQEFBQADggEBADubj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPy +T/4xmf3IDExoU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5bu/8j72gZyxKT +J1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+bYQLCIt+jerXmCHG8+c8eS9e +nNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/ErfF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- + +Baltimore CyberTrust Root +========================= +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJRTESMBAGA1UE +ChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3li +ZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoXDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMC +SUUxEjAQBgNVBAoTCUJhbHRpbW9yZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFs +dGltb3JlIEN5YmVyVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKME +uyKrmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjrIZ3AQSsB +UnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeKmpYcqWe4PwzV9/lSEy/C +G9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSuXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9 +XbIGevOF6uvUA65ehD5f/xXtabz5OTZydc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjpr +l3RjM71oGDHweI12v/yejl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoI +VDaGezq1BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEB +BQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT929hkTI7gQCvlYpNRh +cL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3WgxjkzSswF07r51XgdIGn9w/xZchMB5 +hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsa +Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H +RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +Entrust Root Certification Authority +==================================== +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0Lm5ldC9DUFMgaXMgaW5jb3Jw +b3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMWKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsG +A1UEAxMkRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0 +MloXDTI2MTEyNzIwNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMu +MTkwNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSByZWZlcmVu +Y2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNVBAMTJEVudHJ1c3QgUm9v +dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ALaVtkNC+sZtKm9I35RMOVcF7sN5EUFoNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYsz +A9u3g3s+IIRe7bJWKKf44LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOww +Cj0Yzfv9KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGIrb68 +j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi94DkZfs0Nw4pgHBN +rziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOBsDCBrTAOBgNVHQ8BAf8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAigA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1 +MzQyWjAfBgNVHSMEGDAWgBRokORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DH +hmak8fdLQ/uEvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9tO1KzKtvn1ISM +Y/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6ZuaAGAT/3B+XxFNSRuzFVJ7yVTa +v52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTS +W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0 +tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +GeoTrust Global CA +================== +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9iYWwgQ0EwHhcNMDIwNTIxMDQw +MDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5j +LjEbMBkGA1UEAxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjo +BbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDviS2Aelet +8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU1XupGc1V3sjs0l44U+Vc +T4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagU +vTLrGAMoUgRx5aszPeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVk +DBF9qn1luMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKInZ57Q +zxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfStQWVYrmm3ok9Nns4 +d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcFPseKUgzbFbS9bZvlxrFUaKnjaZC2 +mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Unhw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6p +XE0zX5IJL4hmXXeXxx12E6nV5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvm +Mw== +-----END CERTIFICATE----- + +GeoTrust Universal CA +===================== +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN +R2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVyc2FsIENBMB4XDTA0MDMwNDA1 +MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IElu +Yy4xHjAcBgNVBAMTFUdlb1RydXN0IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAKYVVaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9t +JPi8cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTTQjOgNB0e +RXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFhF7em6fgemdtzbvQKoiFs +7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2vc7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d +8Lsrlh/eezJS/R27tQahsiFepdaVaH/wmZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7V +qnJNk22CDtucvc+081xdVHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3Cga +Rr0BHdCXteGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZf9hB +Z3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfReBi9Fi1jUIxaS5BZu +KGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+nhutxx9z3SxPGWX9f5NAEC7S8O08 +ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0 +XG0D08DYj3rWMB8GA1UdIwQYMBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fXIwjhmF7DWgh2 +qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzynANXH/KttgCJwpQzgXQQpAvvL +oJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0zuzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsK +xr2EoyNB3tZ3b4XUhRxQ4K5RirqNPnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxF +KyDuSN/n3QmOGKjaQI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2 +DFKWkoRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9ER/frslK +xfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQtDF4JbAiXfKM9fJP/P6EU +p8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/SfuvmbJxPgWp6ZKy7PtXny3YuxadIwVyQD8vI +P/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- + +GeoTrust Universal CA 2 +======================= +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN +R2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwHhcNMDQwMzA0 +MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3Qg +SW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0 +DE81WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUGFF+3Qs17 +j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdqXbboW0W63MOhBW9Wjo8Q +JqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxLse4YuU6W3Nx2/zu+z18DwPw76L5GG//a +QMJS9/7jOvdqdzXQ2o3rXhhqMcceujwbKNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2 +WP0+GfPtDCapkzj4T8FdIgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP +20gaXT73y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRthAAn +ZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgocQIgfksILAAX/8sgC +SqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4Lt1ZrtmhN79UNdxzMk+MBB4zsslG +8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2 ++/CfXGJx7Tz0RzgQKzAfBgNVHSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8E +BAMCAYYwDQYJKoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z +dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQL1EuxBRa3ugZ +4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgrFg5fNuH8KrUwJM/gYwx7WBr+ +mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSoag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpq +A1Ihn0CoZ1Dy81of398j9tx4TuaYT1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpg +Y+RdM4kX2TGq2tbzGDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiP +pm8m1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJVOCiNUW7d +FGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH6aLcr34YEoP9VhdBLtUp +gn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwXQMAJKOSLakhT2+zNVVXxxvjpoixMptEm +X36vWkzaH6byHCx+rgIW0lbQL1dTR+iS +-----END CERTIFICATE----- + +Comodo AAA Services root +======================== +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS +R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg +TGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAw +MFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hl +c3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV +BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQuaBtDFcCLNSS1UY8y2bmhG +C1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe3M/vg4aijJRPn2jymJBGhCfHdr/jzDUs +i14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszW +Y19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjH +Ypy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEK +Iz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0f +BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNl +cy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2Vz +LmNybDANBgkqhkiG9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm +7l3sAg9g1o1QGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z +8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C +12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +QuoVadis Root CA +================ +-----BEGIN CERTIFICATE----- +MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJCTTEZMBcGA1UE +ChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAz +MTkxODMzMzNaFw0yMTAzMTcxODMzMzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRp +cyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQD +EyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Ypli4kVEAkOPcahdxYTMuk +J0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2DrOpm2RgbaIr1VxqYuvXtdj182d6UajtL +F8HVj71lODqV0D1VNk7feVcxKh7YWWVJWCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeL +YzcS19Dsw3sgQUSj7cugF+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWen +AScOospUxbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCCAk4w +PQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVvdmFkaXNvZmZzaG9y +ZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREwggENMIIBCQYJKwYBBAG+WAABMIH7 +MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNlIG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmlj +YXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJs +ZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh +Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYIKwYBBQUHAgEW +Fmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3TKbkGGew5Oanwl4Rqy+/fMIGu +BgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rqy+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkw +FwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MS4wLAYDVQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6 +tlCLMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSkfnIYj9lo +fFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf87C9TqnN7Az10buYWnuul +LsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1RcHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2x +gI4JVrmcGmD+XcHXetwReNDWXcG31a0ymQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi +5upZIof4l/UO/erMkqQWxFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi +5nrQNiOKSnQ2+Q== +-----END CERTIFICATE----- + +QuoVadis Root CA 2 +================== +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT +EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMjAeFw0wNjExMjQx +ODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCaGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6 +XJxgFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55JWpzmM+Yk +lvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bBrrcCaoF6qUWD4gXmuVbB +lDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp+ARz8un+XJiM9XOva7R+zdRcAitMOeGy +lZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt +66/3FsvbzSUr5R/7mp/iUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1Jdxn +wQ5hYIizPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og/zOh +D7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UHoycR7hYQe7xFSkyy +BNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuIyV77zGHcizN300QyNQliBJIWENie +J0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1Ud +DgQWBBQahGK8SEwzJQTU7tD2A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGU +a6FJpEcwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2fBluornFdLwUv +Z+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzng/iN/Ae42l9NLmeyhP3ZRPx3 +UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2BlfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodm +VjB3pjd4M1IQWK4/YY7yarHvGH5KWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK ++JDSV6IZUaUtl0HaB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrW +IozchLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPRTUIZ3Ph1 +WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWDmbA4CD/pXvk1B+TJYm5X +f6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0ZohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II +4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8 +VCLAAVBpQ570su9t+Oza8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +QuoVadis Root CA 3 +================== +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT +EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0wNjExMjQx +OTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNgg +DhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUrH556VOij +KTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd8lyyBTNvijbO0BNO/79K +DDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9CabwvvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbv +BNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwp +p5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8 +nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEX +MJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyM +Gf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclz +uD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHT +BgkrBgEEAb5YAAMwgcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmlj +YXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYB +BQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD +VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4 +ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UE +AxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZV +qyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSemd1o417+s +hvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd+LJ2w/w4E6oM3kJpK27z +POuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2 +Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp +8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBC +bjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXu +g/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91p +vGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbr +qZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +Security Communication Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP +U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw +HhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP +U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw +8yl89f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJDKaVv0uM +DPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9Ms+k2Y7CI9eNqPPYJayX +5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/NQV3Is00qVUarH9oe4kA92819uZKAnDfd +DJZkndwi92SL32HeFZRSFaB9UslLqCHJxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2 +JChzAgMBAAGjPzA9MB0GA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vGkl3g +0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfrUj94nK9NrvjVT8+a +mCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5Bw+SUEmK3TGXX8npN6o7WWWXlDLJ +s58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJUJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ +6rBK+1YWc26sTfcioU+tHXotRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAi +FL39vmwLAw== +-----END CERTIFICATE----- + +Sonera Class 2 Root CA +====================== +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEPMA0GA1UEChMG +U29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAxMDQwNjA3Mjk0MFoXDTIxMDQw +NjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNVBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJh +IENsYXNzMiBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3 +/Ei9vX+ALTU74W+oZ6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybT +dXnt5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s3TmVToMG +f+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2EjvOr7nQKV0ba5cTppCD8P +tOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu8nYybieDwnPz3BjotJPqdURrBGAgcVeH +nfO+oJAjPYok4doh28MCAwEAAaMzMDEwDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITT +XjwwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt +0jSv9zilzqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/3DEI +cbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvDFNr450kkkdAdavph +Oe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6Tk6ezAyNlNzZRZxe7EJQY670XcSx +EtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLH +llpwrN9M +-----END CERTIFICATE----- + +XRamp Global CA Root +==================== +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UE +BhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2Vj +dXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDQxMTAxMTcxNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMx +HjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkg +U2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS638eMpSe2OAtp87ZOqCwu +IR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCPKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMx +foArtYzAQDsRhtDLooY2YKTVMIJt2W7QDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FE +zG+gSqmUsE3a56k0enI4qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqs +AxcZZPRaJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNViPvry +xS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASsjVy16bYbMDYGA1UdHwQvMC0wK6Ap +oCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMC +AQEwDQYJKoZIhvcNAQEFBQADggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc +/Kh4ZzXxHfARvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLaIR9NmXmd4c8n +nxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSyi6mx5O+aGtA9aZnuqCij4Tyz +8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQO+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +Go Daddy Class 2 CA +=================== +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMY +VGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkG +A1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g +RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQAD +ggENADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv +2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+qN1j3hybX2C32 +qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6j +YGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmY +vLEHZ6IVDd2gWMZEewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0O +BBYEFNLEsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h/t2o +atTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMu +MTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wim +PQoZ+YeAEW5p5JYXMP80kWNyOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKt +I3lpjbi2Tc7PTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mERdEr/VxqHD3VI +Ls9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5CufReYNnyicsbkqWletNw+vHX/b +vZ8= +-----END CERTIFICATE----- + +Starfield Class 2 CA +==================== +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzElMCMGA1UEChMc +U3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZpZWxkIENsYXNzIDIg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQwNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBo +MQswCQYDVQQGEwJVUzElMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAG +A1UECxMpU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqG +SIb3DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf8MOh2tTY +bitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN+lq2cwQlZut3f+dZxkqZ +JRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVm +epsZGD3/cVE8MC5fvj13c7JdBmzDI1aaK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSN +F4Azbl5KXZnJHoe0nRrA1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HF +MIHCMB0GA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fRzt0f +hvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNo +bm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24g +QXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGs +afPzWdqbAYcaT1epoXkJKtv3L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLM +PUxA2IGvd56Deruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynpVSJYACPq4xJD +KVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEYWQPJIrSPnNVeKtelttQKbfi3 +QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +Taiwan GRCA +=========== +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/MQswCQYDVQQG +EwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4X +DTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1owPzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dv +dmVybm1lbnQgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qN +w8XRIePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1qgQdW8or5 +BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKyyhwOeYHWtXBiCAEuTk8O +1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAtsF/tnyMKtsc2AtJfcdgEWFelq16TheEfO +htX7MfP6Mb40qij7cEwdScevLJ1tZqa2jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wov +J5pGfaENda1UhhXcSTvxls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7 +Q3hub/FCVGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHKYS1t +B6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoHEgKXTiCQ8P8NHuJB +O9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThNXo+EHWbNxWCWtFJaBYmOlXqYwZE8 +lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1UdDgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNV +HRMEBTADAQH/MDkGBGcqBwAEMTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg2 +09yewDL7MTqKUWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ +TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyfqzvS/3WXy6Tj +Zwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaKZEk9GhiHkASfQlK3T8v+R0F2 +Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFEJPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlU +D7gsL0u8qV1bYH+Mh6XgUmMqvtg7hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6Qz +DxARvBMB1uUO07+1EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+Hbk +Z6MmnD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WXudpVBrkk +7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44VbnzssQwmSNOXfJIoRIM3BKQ +CZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDeLMDDav7v3Aun+kbfYNucpllQdSNpc5Oy ++fwC00fmcc4QAu4njIT/rEUNE1yDMuAlpYYsfPQS +-----END CERTIFICATE----- + +DigiCert Assured ID Root CA +=========================== +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzEx +MTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0Ew +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7cJpSIqvTO +9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYPmDI2dsze3Tyoou9q+yHy +UmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW +/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpy +oeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf +GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF +66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkq +hkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2Bc +EkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38Fn +SbNd67IJKusm7Xi+fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i +8b5QZ7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +DigiCert Global Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw +MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn +TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5 +BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H +4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y +7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB +o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm +8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF +BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr +EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt +tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886 +UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +DigiCert High Assurance EV Root CA +================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSsw +KQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAw +MFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ +MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFu +Y2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm+9S75S0t +Mqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTWPNt0OKRKzE0lgvdKpVMS +OO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEMxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3 +MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQ +NAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUe +h10aUAsgEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY +JhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQ +V8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFp +myPInngiK3BD41VHMWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkK +mNEVX58Svnw2Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K +-----END CERTIFICATE----- + +DST Root CA X3 +============== +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQK +ExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4X +DTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1 +cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmT +rE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9 +UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRy +xXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40d +utolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQ +MA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikug +dB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjE +GB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bw +RLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubS +fZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- + +SwissSign Gold CA - G2 +====================== +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNIMRUw +EwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0gRzIwHhcN +MDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dp +c3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUq +t2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+bbqBHH5C +jCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c6bM8K8vzARO/Ws/BtQpg +vd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqEemA8atufK+ze3gE/bk3lUIbLtK/tREDF +ylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvR +AiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuend +jIj3o02yMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkO +peUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR +7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGi +GqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64 +OfPAeGZe6Drn8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm +5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr +44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOf +Mke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6m +Gu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxp +mo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCChdiDyyJk +vC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid392qgQmwLOM7XdVAyksLf +KzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG2mqeSz53OiATIgHQv2ieY2Br +NU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6Lqj +viOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +SwissSign Silver CA - G2 +======================== +-----BEGIN CERTIFICATE----- +MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ0gxFTAT +BgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMB4X +DTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0NlowRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3 +aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644 +N0MvFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7brYT7QbNHm ++/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieFnbAVlDLaYQ1HTWBCrpJH +6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH6ATK72oxh9TAtvmUcXtnZLi2kUpCe2Uu +MGoM9ZDulebyzYLs2aFK7PayS+VFheZteJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5h +qAaEuSh6XzjZG6k4sIN/c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5 +FZGkECwJMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRHHTBs +ROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTfjNFusB3hB48IHpmc +celM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb65i/4z3GcRm25xBWNOHkDRUjvxF3X +CO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRB +tjpbO8tFnb0cwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 +cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBAHPGgeAn0i0P +4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShpWJHckRE1qTodvBqlYJ7YH39F +kWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L +3XWgwF15kIwb4FDm3jH+mHtwX6WQ2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx +/uNncqCxv1yL5PqZIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFa +DGi8aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2Xem1ZqSqP +e97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQRdAtq/gsD/KNVV4n+Ssuu +WxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJ +DIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ub +DgEj8Z+7fNzcbBGXJbLytGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u +-----END CERTIFICATE----- + +GeoTrust Primary Certification Authority +======================================== +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMoR2VvVHJ1c3QgUHJpbWFyeSBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgx +CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQ +cmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9AWbK7hWN +b6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjAZIVcFU2Ix7e64HXprQU9 +nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE07e9GceBrAqg1cmuXm2bgyxx5X9gaBGge +RwLmnWDiNpcB3841kt++Z8dtd1k7j53WkBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGt +tm/81w7a4DSwDRp35+MImO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJKoZI +hvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ16CePbJC/kRYkRj5K +Ts4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl4b7UVXGYNTq+k+qurUKykG/g/CFN +NWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6KoKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHa +Floxt/m0cYASSJlyc1pZU8FjUjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG +1riR/aYNKxoUAT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- + +thawte Primary Root CA +====================== +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCBqTELMAkGA1UE +BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2 +aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3 +MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwg +SW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMv +KGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMT +FnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCs +oPD7gFnUnMekz52hWXMJEEUMDSxuaPFsW0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ +1CRfBsDMRJSUjQJib+ta3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGc +q/gcfomk6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6Sk/K +aAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94JNqR32HuHUETVPm4p +afs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XPr87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUF +AAOCAQEAeRHAS7ORtvzw6WfUDW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeE +uzLlQRHAd9mzYJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2/qxAeeWsEG89 +jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/LHbTY5xZ3Y+m4Q6gLkH3LpVH +z7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7jVaMaA== +-----END CERTIFICATE----- + +VeriSign Class 3 Public Primary Certification Authority - G5 +============================================================ +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCByjELMAkGA1UE +BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO +ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk +IHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2ln +biBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBh +dXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCvJAgIKXo1nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKz +j/i5Vbext0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIzSdhD +Y2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQGBO+QueQA5N06tRn/ +Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+rCpSx4/VBEnkjWNHiDxpg8v+R70r +fk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/ +BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2Uv +Z2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKvMzEzMA0GCSqG +SIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzEp6B4Eq1iDkVwZMXnl2YtmAl+ +X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKE +KQsTb47bDN0lAtukixlE0kF6BWlKWE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiC +Km0oHw0LxOXnGiYZ4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vE +ZV8NhnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +SecureTrust CA +============== +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNlY3VyZVRy +dXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAe +BgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQX +OZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO0gMdA+9t +DWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIaowW8xQmxSPmjL8xk037uH +GFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b +01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmH +ursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYj +aHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ +KoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSu +SceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHf +mbx8IVQr5Fiiu1cprp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZ +nMUFdAvnZyPSCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +Secure Global CA +================ +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBH +bG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEg +MB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwg +Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jx +YDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa/FHtaMbQ +bqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJjnIFHovdRIWCQtBJwB1g +8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnIHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYV +HDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi +0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAn +oCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEA +MA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+ +OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cn +CDpOGR86p1hcF895P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/5 +3CYNv6ZHdAbYiNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +COMODO Certification Authority +============================== +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEb +MBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD +T01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH ++7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTww +xHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV +4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA +1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVI +rLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9k +b2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOC +AQEAPpiem/Yb6dc5t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CP +OGEIqB6BCsAvIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmc +IGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN ++8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ== +-----END CERTIFICATE----- + +Network Solutions Certificate Authority +======================================= +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQG +EwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydOZXR3b3Jr +IFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMx +MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwzc7MEL7xx +jOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPPOCwGJgl6cvf6UDL4wpPT +aaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rlmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXT +crA/vGp97Eh/jcOrqnErU2lBUzS1sLnFBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc +/Qzpf14Dl847ABSHJ3A4qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMB +AAGjgZcwgZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwubmV0c29sc3NsLmNv +bS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3JpdHkuY3JsMA0GCSqGSIb3DQEBBQUA +A4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc86fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q +4LqILPxFzBiwmZVRDuwduIj/h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/ +GGUsyfJj4akH/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHNpGxlaKFJdlxD +ydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- + +COMODO ECC Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix +GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X +4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni +wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG +FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA +U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GA CA +=============================== +-----BEGIN CERTIFICATE----- +MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCBijELMAkGA1UE +BhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHlyaWdodCAoYykgMjAwNTEiMCAG +A1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBH +bG9iYWwgUm9vdCBHQSBDQTAeFw0wNTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYD +VQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIw +IAYDVQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5 +IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy0+zAJs9 +Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxRVVuuk+g3/ytr6dTqvirdqFEr12bDYVxg +Asj1znJ7O7jyTmUIms2kahnBAbtzptf2w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbD +d50kc3vkDIzh2TbhmYsFmQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ +/yxViJGg4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t94B3R +LoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ +KoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOxSPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vIm +MMkQyh2I+3QZH4VFvbBsUfk2ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4 ++vg1YFkCExh8vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa +hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZiFj4A4xylNoEY +okxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ/L7fCg0= +-----END CERTIFICATE----- + +Certigna +======== +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZSMRIw +EAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMwNVoXDTI3 +MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwI +Q2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7q +XOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyH +GxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbwzBfsV1/p +ogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q130yGLMLLGq/jj8UEYkg +DncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKf +Irjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQ +tCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJ +BgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/J +SP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEA +hQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+ +ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1klu +PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY +1gkIl2PlwS6wt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +Cybertrust Global Root +====================== +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYGA1UEChMPQ3li +ZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBSb290MB4XDTA2MTIxNTA4 +MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQD +ExZDeWJlcnRydXN0IEdsb2JhbCBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA ++Mi8vRRQZhP/8NN57CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW +0ozSJ8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2yHLtgwEZL +AfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iPt3sMpTjr3kfb1V05/Iin +89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNzFtApD0mpSPCzqrdsxacwOUBdrsTiXSZT +8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAYXSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2 +MDSgMqAwhi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3JsMB8G +A1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUAA4IBAQBW7wojoFRO +lZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMjWqd8BfP9IjsO0QbE2zZMcwSO5bAi +5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUxXOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2 +hO0j9n0Hq0V+09+zv+mKts2oomcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+T +X3EJIrduPuocA06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +ePKI Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQG +EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsMIWVQS0kg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMx +MjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEq +MCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAHSyZbCUNs +IZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAhijHyl3SJCRImHJ7K2RKi +lTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3XDZoTM1PRYfl61dd4s5oz9wCGzh1NlDiv +qOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX +12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0O +WQqraffAsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+ +ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnao +lQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/ +vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXi +Zo1jDiVN1Rmy5nk3pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/Qkqi +MAwGA1UdEwQFMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B0 +1GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq +KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdV +xrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEP +NXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+r +GNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUBo2M3IUxE +xJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS/jQ6fbjpKdx2qcgw+BRx +gMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C6pSe3VkQw63d4k3jMdXH7Ojy +sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD +BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +certSIGN ROOT CA +================ +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREwDwYD +VQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQxNzIwMDRa +Fw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UE +CxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7I +JUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHH +rfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5dRdY4zTW2 +ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQOA7+j0xbm0bqQfWwCHTD +0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwvJoIQ4uNllAoEwF73XVv4EOLQunpL+943 +AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B +Af8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IB +AQA+0hyJLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8 +SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0 +x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIlt +vBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7Nz +TogVZ96edhBiIL5VaZVDADlN9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +GeoTrust Primary Certification Authority - G3 +============================================= +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA4IEdlb1RydXN0 +IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFy +eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIz +NTk1OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAo +YykgMjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMT +LUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz+uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5j +K/BGvESyiaHAKAxJcCGVn2TAppMSAmUmhsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdE +c5IiaacDiGydY8hS2pgn5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3C +IShwiP/WJmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exALDmKu +dlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZChuOl1UcCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMR5yo6hTgMdHNxr +2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IBAQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9 +cr5HqQ6XErhK8WTTOd8lNNTBzU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbE +Ap7aDHdlDkQNkv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD +AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUHSJsMC8tJP33s +t/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2Gspki4cErx5z481+oghLrGREt +-----END CERTIFICATE----- + +thawte Primary Root CA - G2 +=========================== +-----BEGIN CERTIFICATE----- +MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDELMAkGA1UEBhMC +VVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMpIDIwMDcgdGhhd3RlLCBJbmMu +IC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3Qg +Q0EgLSBHMjAeFw0wNzExMDUwMDAwMDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEV +MBMGA1UEChMMdGhhd3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBG +b3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAt +IEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/BebfowJPDQfGAFG6DAJS +LSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6papu+7qzcMBniKI11KOasf2twu8x+qi5 +8/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU +mtgAMADna3+FGO6Lts6KDPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUN +G4k8VIZ3KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41oxXZ3K +rr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== +-----END CERTIFICATE----- + +thawte Primary Root CA - G3 +=========================== +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCBrjELMAkGA1UE +BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2 +aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0w +ODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh +d3RlLCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9uMTgwNgYD +VQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIG +A1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAsr8nLPvb2FvdeHsbnndmgcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2At +P0LMqmsywCPLLEHd5N/8YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC ++BsUa0Lfb1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS99irY +7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2SzhkGcuYMXDhpxwTW +vGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUkOQIDAQABo0IwQDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJ +KoZIhvcNAQELBQADggEBABpA2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweK +A3rD6z8KLFIWoCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu +t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7cKUGRIjxpp7sC +8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fMm7v/OeZWYdMKp8RcTGB7BXcm +er/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZuMdRAGmI0Nj81Aa6sY6A= +-----END CERTIFICATE----- + +GeoTrust Primary Certification Authority - G2 +============================================= +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA3IEdlb1RydXN0IElu +Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1 +OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg +MjAwNyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMTLUdl +b1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjB2MBAGByqGSM49AgEG +BSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcLSo17VDs6bl8VAsBQps8lL33KSLjHUGMc +KiEIfJo22Av+0SbFWDEwKCXzXV2juLaltJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+ +EVXVMAoGCCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGTqQ7m +ndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBuczrD6ogRLQy7rQkgu2 +npaqBA+K +-----END CERTIFICATE----- + +VeriSign Universal Root Certification Authority +=============================================== +-----BEGIN CERTIFICATE----- +MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCBvTELMAkGA1UE +BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO +ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk +IHVzZSBvbmx5MTgwNgYDVQQDEy9WZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTAeFw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJV +UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv +cmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNhbCBSb290IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj +1mCOkdeQmIN65lgZOIzF9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGP +MiJhgsWHH26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+HLL72 +9fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN/BMReYTtXlT2NJ8I +AfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPTrJ9VAMf2CGqUuV/c4DPxhGD5WycR +tPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0G +CCsGAQUFBwEMBGEwX6FdoFswWTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2O +a8PPgGrUSBgsexkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud +DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4sAPmLGd75JR3 +Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+seQxIcaBlVZaDrHC1LGmWazx +Y8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTx +P/jgdFcrGJ2BtMQo2pSXpXDrrB2+BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+P +wGZsY6rp2aQW9IHRlRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4 +mJO37M2CYfE45k+XmCpajQ== +-----END CERTIFICATE----- + +VeriSign Class 3 Public Primary Certification Authority - G4 +============================================================ +-----BEGIN CERTIFICATE----- +MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjELMAkGA1UEBhMC +VVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3 +b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVz +ZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBU +cnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRo +b3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5 +IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8 +Utpkmw4tXNherJI9/gHmGUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGz +rl0Bp3vefLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEw +HzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVyaXNpZ24u +Y29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMWkf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMD +A2gAMGUCMGYhDBgmYFo4e1ZC4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIx +AJw9SDkjOVgaFRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== +-----END CERTIFICATE----- + +NetLock Arany (Class Gold) Főtanúsítvány +======================================== +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G +A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610 +dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB +cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx +MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO +ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6 +c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu +0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw +/HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk +H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw +fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1 +neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW +qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta +YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna +NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu +dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +Hongkong Post Root CA 1 +======================= +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoT +DUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMB4XDTAzMDUx +NTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25n +IFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1 +ApzQjVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEnPzlTCeqr +auh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjhZY4bXSNmO7ilMlHIhqqh +qZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9nnV0ttgCXjqQesBCNnLsak3c78QA3xMY +V18meMjWCnl3v/evt3a5pQuEF10Q6m/hq5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNV +HRMBAf8ECDAGAQH/AgEDMA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7i +h9legYsCmEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI37pio +l7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clBoiMBdDhViw+5Lmei +IAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJsEhTkYY2sEJCehFC78JZvRZ+K88ps +T/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpOfMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilT +c4afU9hDDl3WY4JxHYB0yvbiAmvZWg== +-----END CERTIFICATE----- + +SecureSign RootCA11 +=================== +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDErMCkGA1UEChMi +SmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoGA1UEAxMTU2VjdXJlU2lnbiBS +b290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSsw +KQYDVQQKEyJKYXBhbiBDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1 +cmVTaWduIFJvb3RDQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvL +TJszi1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8h9uuywGO +wvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOVMdrAG/LuYpmGYz+/3ZMq +g6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rP +O7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitA +bpSACW22s293bzUIUPsCh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZX +t94wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKCh +OBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xmKbabfSVSSUOrTC4r +bnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQX5Ucv+2rIrVls4W6ng+4reV6G4pQ +Oh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWrQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01 +y8hSyn+B/tlr0/cR7SXf+Of5pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061 +lgeLKBObjBmNQSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- + +Microsec e-Szigno Root CA 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER +MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv +c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE +BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt +U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA +fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG +0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA +pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm +1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC +AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf +QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE +FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o +lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX +I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02 +yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi +LXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +GlobalSign Root CA - R3 +======================= +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv +YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh +bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT +aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt +iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ +0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3 +rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl +OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2 +xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7 +lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8 +EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E +bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18 +YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r +kpeDMdmztcpHWD9f +-----END CERTIFICATE----- + +Autoridad de Certificacion Firmaprofesional CIF A62634068 +========================================================= +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UEBhMCRVMxQjBA +BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 +MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEyMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIw +QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB +NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD +Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P +B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY +7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH +ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI +plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX +MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX +LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK +bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU +vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1Ud +EwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNH +DhpkLzCBpgYDVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBvACAAZABlACAA +bABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBlAGwAbwBuAGEAIAAwADgAMAAx +ADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx +51tkljYyGOylMnfX40S2wBEqgLk9am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qk +R71kMrv2JYSiJ0L1ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaP +T481PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS3a/DTg4f +Jl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5kSeTy36LssUzAKh3ntLFl +osS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF3dvd6qJ2gHN99ZwExEWN57kci57q13XR +crHedUTnQn3iV2t93Jm8PYMo6oCTjcVMZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoR +saS8I8nkvof/uZS2+F0gStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTD +KCOM/iczQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQBjLMi +6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- + +Izenpe.com +========== +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYDVQQG +EwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcNMDcxMjEz +MTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMu +QS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ +03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAK +ClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6HLmYRY2xU ++zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFXuaOKmMPsOzTFlUFpfnXC +PCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQDyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxT +OTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbK +F7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK +0GqfvEyNBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+ +0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbB +leStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwID +AQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+ +SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBG +NjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l +Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbga +kEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8q +hT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Cs +g1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCTVyvehQP5 +aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGkLhObNA5me0mrZJfQRsN5 +nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqtujWTI6cfSN01RpiyEGjkpTHC +ClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZo +Q0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1Z +WrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +Chambers of Commerce Root - 2008 +================================ +-----BEGIN CERTIFICATE----- +MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYDVQQGEwJFVTFD +MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv +bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu +QS4xKTAnBgNVBAMTIENoYW1iZXJzIG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEy +Mjk1MFoXDTM4MDczMTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNl +ZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQF +EwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJl +cnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW928sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKA +XuFixrYp4YFs8r/lfTJqVKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorj +h40G072QDuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR5gN/ +ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfLZEFHcpOrUMPrCXZk +NNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05aSd+pZgvMPMZ4fKecHePOjlO+Bd5g +D2vlGts/4+EhySnB8esHnFIbAURRPHsl18TlUlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331 +lubKgdaX8ZSD6e2wsWsSaR6s+12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ +0wlf2eOKNcx5Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj +ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAxhduub+84Mxh2 +EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNVHQ4EFgQU+SSsD7K1+HnA+mCI +G8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1+HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJ +BgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNh +bWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENh +bWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDiC +CQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUH +AgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAJASryI1 +wqM58C7e6bXpeHxIvj99RZJe6dqxGfwWPJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH +3qLPaYRgM+gQDROpI9CF5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbU +RWpGqOt1glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaHFoI6 +M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2pSB7+R5KBWIBpih1 +YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MDxvbxrN8y8NmBGuScvfaAFPDRLLmF +9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QGtjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcK +zBIKinmwPQN/aUv0NCB9szTqjktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvG +nrDQWzilm1DefhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg +OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZd0jQ +-----END CERTIFICATE----- + +Global Chambersign Root - 2008 +============================== +-----BEGIN CERTIFICATE----- +MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYDVQQGEwJFVTFD +MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv +bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu +QS4xJzAlBgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMx +NDBaFw0zODA3MzExMjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUg +Y3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJ +QTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD +aGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDf +VtPkOpt2RbQT2//BthmLN0EYlVJH6xedKYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXf +XjaOcNFccUMd2drvXNL7G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0 +ZJJ0YPP2zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4ddPB +/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyGHoiMvvKRhI9lNNgA +TH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2Id3UwD2ln58fQ1DJu7xsepeY7s2M +H/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3VyJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfe +Ox2YItaswTXbo6Al/3K1dh3ebeksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSF +HTynyQbehP9r6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh +wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsogzCtLkykPAgMB +AAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQWBBS5CcqcHtvTbDprru1U8VuT +BjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDprru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UE +BhMCRVUxQzBBBgNVBAcTOk1hZHJpZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJm +aXJtYS5jb20vYWRkcmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJm +aXJtYSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiCCQDJzdPp +1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0 +dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAICIf3DekijZBZRG +/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZUohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6 +ReAJ3spED8IXDneRRXozX1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/s +dZ7LoR/xfxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVza2Mg +9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yydYhz2rXzdpjEetrHH +foUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMdSqlapskD7+3056huirRXhOukP9Du +qqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9OAP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETr +P3iZ8ntxPjzxmKfFGBI/5rsoM0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVq +c5iJWzouE4gev8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z +09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B +-----END CERTIFICATE----- + +Go Daddy Root Certificate Authority - G2 +======================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu +MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G +A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq +9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD ++qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd +fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl +NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9 +BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac +vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r +5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV +N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1 +-----END CERTIFICATE----- + +Starfield Root Certificate Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0 +eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw +DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg +VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB +dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv +W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs +bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk +N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf +ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU +JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol +TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx +4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw +F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ +c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +Starfield Services Root Certificate Authority - G2 +================================================== +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl +IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT +dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2 +h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa +hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP +LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB +rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG +SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP +E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy +xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza +YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6 +-----END CERTIFICATE----- + +AffirmTrust Commercial +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMB4XDTEw +MDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6Eqdb +DuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yrba0F8PrV +C8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPALMeIrJmqbTFeurCA+ukV6 +BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1yHp52UKqK39c/s4mT6NmgTWvRLpUHhww +MmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNV +HQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPG +hi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDi +qX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv +0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0kh +sUlHRUe072o0EclNmsxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +AffirmTrust Networking +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMB4XDTEw +MDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SE +Hi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbuakCNrmreI +dIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRLQESxG9fhwoXA3hA/Pe24 +/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gb +h+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNV +HQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIu +UFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF6 +12S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23 +WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9 +/ZFvgrG+CJPbFEfxojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +AffirmTrust Premium +=================== +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4XDTEwMDEy +OTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRy +dXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtn +BKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ+jjeRFcV +5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrSs8PhaJyJ+HoAVt70VZVs ++7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmd +GPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5R +p9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NI +S+LI+H+SqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u04 +6uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5 +/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo ++Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByv +MiPIs0laUZx2KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC +6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S +L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK ++4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmV +BtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFg +IxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8GKa1qF60 +g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaORtGdFNrHF+QFlozEJLUb +zxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAloGRwYQw== +-----END CERTIFICATE----- + +AffirmTrust Premium ECC +======================= +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDASBgNV +BAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAeFw0xMDAx +MjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1U +cnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQ +N8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0GA1UdDgQW +BBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAK +BggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/VsaobgxCd05DhT1wV/GzTjxi+zygk8N53X +57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKM +eQ== +-----END CERTIFICATE----- + +Certum Trusted Network CA +========================= +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK +ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy +MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU +ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC +l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J +J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4 +fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0 +cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw +DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj +jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1 +mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj +Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +TWCA Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ +VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG +EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB +IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx +QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC +oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP +4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r +y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG +9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC +mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW +QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY +T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny +Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +Security Communication RootCA2 +============================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc +U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh +dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC +SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy +aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++ ++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R +3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV +spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K +EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8 +QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB +CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj +u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk +3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q +tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29 +mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +EC-ACC +====== +-----BEGIN CERTIFICATE----- +MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB8zELMAkGA1UE +BhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2VydGlmaWNhY2lvIChOSUYgUS0w +ODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYD +VQQLEyxWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UE +CxMsSmVyYXJxdWlhIEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMT +BkVDLUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQGEwJFUzE7 +MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8gKE5JRiBRLTA4MDExNzYt +SSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBDZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZl +Z2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQubmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJh +cnF1aWEgRW50aXRhdHMgZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUND +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R85iK +w5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm4CgPukLjbo73FCeT +ae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaVHMf5NLWUhdWZXqBIoH7nF2W4onW4 +HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNdQlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0a +E9jD2z3Il3rucO2n5nzbcc8tlGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw +0JDnJwIDAQABo4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4opvpXY0wfwYD +VR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBodHRwczovL3d3dy5jYXRjZXJ0 +Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5l +dC92ZXJhcnJlbCAwDQYJKoZIhvcNAQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJ +lF7W2u++AVtd0x7Y/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNa +Al6kSBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhyRp/7SNVe +l+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOSAgu+TGbrIP65y7WZf+a2 +E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xlnJ2lYJU6Un/10asIbvPuW/mIPX64b24D +5EI= +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions RootCA 2011 +======================================================= +-----BEGIN CERTIFICATE----- +MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1IxRDBCBgNVBAoT +O0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9y +aXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z +IFJvb3RDQSAyMDExMB4XDTExMTIwNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYT +AkdSMUQwQgYDVQQKEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z +IENlcnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNo +IEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPzdYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI +1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa +71HFK9+WXesyHgLacEnsbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u +8yBRQlqD75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSPFEDH +3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNVHRMBAf8EBTADAQH/ +MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp5dgTBCPuQSUwRwYDVR0eBEAwPqA8 +MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQub3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQu +b3JnMA0GCSqGSIb3DQEBBQUAA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVt +XdMiKahsog2p6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 +TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7dIsXRSZMFpGD +/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8AcysNnq/onN694/BtZqhFLKPM58N +7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXIl7WdmplNsDz4SgCbZN2fOUvRJ9e4 +-----END CERTIFICATE----- + +Actalis Authentication Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVQxDjAM +BgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UE +AwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDky +MjExMjIwMlowazELMAkGA1UEBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlz +IFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNvUTufClrJ +wkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX4ay8IMKx4INRimlNAJZa +by/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9KK3giq0itFZljoZUj5NDKd45RnijMCO6 +zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1f +YVEiVRvjRuPjPdA1YprbrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2 +oxgkg4YQ51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2Fbe8l +EfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxeKF+w6D9Fz8+vm2/7 +hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4Fv6MGn8i1zeQf1xcGDXqVdFUNaBr8 +EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbnfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5 +jF66CyCU3nuDuP/jVo23Eek7jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLY +iDrIn3hm7YnzezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQALe3KHwGCmSUyI +WOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70jsNjLiNmsGe+b7bAEzlgqqI0 +JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDzWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKx +K3JCaKygvU5a2hi/a5iB0P2avl4VSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+ +Xlff1ANATIGk0k9jpwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC +4yyXX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+OkfcvHlXHo +2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7RK4X9p2jIugErsWx0Hbhz +lefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btUZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXem +OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9 +vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +Trustis FPS Root CA +=================== +-----BEGIN CERTIFICATE----- +MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQG +EwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQLExNUcnVzdGlzIEZQUyBSb290 +IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTExMzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNV +BAoTD1RydXN0aXMgTGltaXRlZDEcMBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQ +RUN+AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihHiTHcDnlk +H5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjjvSkCqPoc4Vu5g6hBSLwa +cY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zt +o3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlBOrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEA +AaNTMFEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAd +BgNVHQ4EFgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01GX2c +GE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmWzaD+vkAMXBJV+JOC +yinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP41BIy+Q7DsdwyhEQsb8tGD+pmQQ9P +8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZEf1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHV +l/9D7S3B2l0pKoU/rGXuhg8FjZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYl +iB6XzCGcKQENZetX2fNXlrtIzYE= +-----END CERTIFICATE----- + +Buypass Class 2 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMiBSb290IENBMB4X +DTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1owTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1 +g1Lr6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPVL4O2fuPn +9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC911K2GScuVr1QGbNgGE41b +/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHxMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqU +CqTqc/sLUegTBxj6DvEr0VQVfTzh97QZQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeff +awrbD02TTqigzXsu8lkBarcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgI +zRFo1clrUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLiFRhn +Bkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRSP/TizPJhk9H9Z2vX +Uq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN9SG9dKpN6nIDSdvHXx1iY8f93ZHs +M+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxPAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFMmAd+BikoL1RpzzuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAU18h9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3tOluwlN5E40EI +osHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo+fsicdl9sz1Gv7SEr5AcD48S +aq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYd +DnkM/crqJIByw5c/8nerQyIKx+u2DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWD +LfJ6v9r9jv6ly0UsH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0 +oyLQI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK75t98biGC +wWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h3PFaTWwyI0PurKju7koS +CTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPzY11aWOIv4x3kqdbQCtCev9eBCfHJxyYN +rJgWVqA= +-----END CERTIFICATE----- + +Buypass Class 3 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMyBSb290IENBMB4X +DTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFowTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRH +sJ8YZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3EN3coTRiR +5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9tznDDgFHmV0ST9tD+leh +7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX0DJq1l1sDPGzbjniazEuOQAnFN44wOwZ +ZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH +2xc519woe2v1n/MuwU8XKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV +/afmiSTYzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvSO1UQ +RwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D34xFMFbG02SrZvPA +Xpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgPK9Dx2hzLabjKSWJtyNBjYt1gD1iq +j6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFEe4zf/lb+74suwvTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAACAjQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXSIGrs/CIBKM+G +uIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2HJLw5QY33KbmkJs4j1xrG0aG +Q0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsaO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8 +ZORK15FTAaggiG6cX0S5y2CBNOxv033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2 +KSb12tjE8nVhz36udmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz +6MkEkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg413OEMXbug +UZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvDu79leNKGef9JOxqDDPDe +eOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq4/g7u9xN12TyUb7mqqta6THuBrxzvxNi +Cp/HuZc= +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 3 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgx +MDAxMTAyOTU2WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN8ELg63iIVl6bmlQdTQyK +9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/RLyTPWGrTs0NvvAgJ1gORH8EGoel15YU +NpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZF +iP0Zf3WHHx+xGwpzJFu5ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W +0eDrXltMEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1A/d2O2GCahKqGFPr +AyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOyWL6ukK2YJ5f+AbGwUgC4TeQbIXQb +fsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzT +ucpH9sry9uetuUg/vBa3wW306gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7h +P0HHRwA11fXT91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4pTpPDpFQUWw== +-----END CERTIFICATE----- + +EE Certification Centre Root CA +=============================== +-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQG +EwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEoMCYGA1UEAwwfRUUgQ2Vy +dGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIw +MTAxMDMwMTAxMDMwWhgPMjAzMDEyMTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlB +UyBTZXJ0aWZpdHNlZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRy +ZSBSb290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUyeuuOF0+W2Ap7kaJjbMeM +TC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvObntl8jixwKIy72KyaOBhU8E2lf/slLo2 +rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIwWFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw +93X2PaRka9ZP585ArQ/dMtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtN +P2MbRMNE1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/zQas8fElyalL1BSZ +MEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEF +BQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEFBQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+Rj +xY6hUFaTlrg4wCQiZrxTFGGVv9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqM +lIpPnTX/dqQGE5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u +uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIWiAYLtqZLICjU +3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/vGVCJYMzpJJUPwssd8m92kMfM +dcGWxZ0= +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTAe +Fw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NThaME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxE +LVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOAD +ER03UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42tSHKXzlA +BF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9RySPocq60vFYJfxLLHLGv +KZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsMlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7z +p+hnUquVH+BGPtikw8paxTGA6Eian5Rp/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUC +AwEAAaOCARowggEWMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ +4PGEMA4GA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVjdG9y +eS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUyMENBJTIwMiUyMDIw +MDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRlcmV2b2NhdGlvbmxpc3QwQ6BBoD+G +PWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAw +OS5jcmwwDQYJKoZIhvcNAQELBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm +2H6NMLVwMeniacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4KzCUqNQT4YJEV +dT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8PIWmawomDeCTmGCufsYkl4ph +X5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3YJohw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 EV 2009 +================================= +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUwNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfS +egpnljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM03TP1YtHh +zRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6ZqQTMFexgaDbtCHu39b+T +7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lRp75mpoo6Kr3HGrHhFPC+Oh25z1uxav60 +sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure35 +11H3a6UCAwEAAaOCASQwggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyv +cop9NteaHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFwOi8v +ZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xhc3MlMjAzJTIwQ0El +MjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1ERT9jZXJ0aWZpY2F0ZXJldm9jYXRp +b25saXN0MEagRKBChkBodHRwOi8vd3d3LmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xh +c3NfM19jYV8yX2V2XzIwMDkuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+ +PPoeUSbrh/Yp3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNFCSuGdXzfX2lX +ANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7naxpeG0ILD5EJt/rDiZE4OJudA +NCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqXKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVv +w9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +CA Disig Root R2 +================ +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNVBAYTAlNLMRMw +EQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMuMRkwFwYDVQQDExBDQSBEaXNp +ZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQyMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sx +EzARBgNVBAcTCkJyYXRpc2xhdmExEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERp +c2lnIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbC +w3OeNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNHPWSb6Wia +xswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3Ix2ymrdMxp7zo5eFm1tL7 +A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbeQTg06ov80egEFGEtQX6sx3dOy1FU+16S +GBsEWmjGycT6txOgmLcRK7fWV8x8nhfRyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqV +g8NTEQxzHQuyRpDRQjrOQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa +5Beny912H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJQfYE +koopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUDi/ZnWejBBhG93c+A +Ak9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORsnLMOPReisjQS1n6yqEm70XooQL6i +Fh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5u +Qu0wDQYJKoZIhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqfGopTpti72TVV +sRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkblvdhuDvEK7Z4bLQjb/D907Je +dR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W8 +1k/BfDxujRNt+3vrMNDcTa/F1balTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjx +mHHEt38OFdAlab0inSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01 +utI3gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18DrG5gPcFw0 +sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3OszMOl6W8KjptlwlCFtaOg +UxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8xL4ysEr3vQCj8KWefshNPZiTEUxnpHikV +7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +ACCVRAIZ1 +========= +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UEAwwJQUNDVlJB +SVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQswCQYDVQQGEwJFUzAeFw0xMTA1 +MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwH +UEtJQUNDVjENMAsGA1UECgwEQUNDVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCbqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gM +jmoYHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWoG2ioPej0 +RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpAlHPrzg5XPAOBOp0KoVdD +aaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhrIA8wKFSVf+DuzgpmndFALW4ir50awQUZ +0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDG +WuzndN9wrqODJerWx5eHk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs7 +8yM2x/474KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMOm3WR +5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpacXpkatcnYGMN285J +9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPluUsXQA+xtrn13k/c4LOsOxFwYIRK +Q26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYIKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRw +Oi8vd3d3LmFjY3YuZXMvZmlsZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEu +Y3J0MB8GCCsGAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeTVfZW6oHlNsyM +Hj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIGCCsGAQUFBwICMIIBFB6CARAA +QQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUAcgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBh +AO0AegAgAGQAZQAgAGwAYQAgAEEAQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUA +YwBuAG8AbABvAGcA7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBj +AHQAcgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAAQwBQAFMA +IABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUAczAwBggrBgEFBQcCARYk +aHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2MuaHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0 +dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRtaW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2 +MV9kZXIuY3JsMA4GA1UdDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZI +hvcNAQEFBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdpD70E +R9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gUJyCpZET/LtZ1qmxN +YEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+mAM/EKXMRNt6GGT6d7hmKG9Ww7Y49 +nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepDvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJ +TS+xJlsndQAJxGJ3KQhfnlmstn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3 +sCPdK6jT2iWH7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szAh1xA2syVP1Xg +Nce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xFd3+YJ5oyXSrjhO7FmGYvliAd +3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2HpPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3p +EfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +TWCA Global Root CA +=================== +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcxEjAQBgNVBAoT +CVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMTVFdDQSBHbG9iYWwgUm9vdCBD +QTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQK +EwlUQUlXQU4tQ0ExEDAOBgNVBAsTB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2C +nJfF10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz0ALfUPZV +r2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfChMBwqoJimFb3u/Rk28OKR +Q4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbHzIh1HrtsBv+baz4X7GGqcXzGHaL3SekV +tTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1W +KKD+u4ZqyPpcC1jcxkt2yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99 +sy2sbZCilaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYPoA/p +yJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQABDzfuBSO6N+pjWxn +kjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcEqYSjMq+u7msXi7Kx/mzhkIyIqJdI +zshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6g +cFGn90xHNcgL1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WFH6vPNOw/KP4M +8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNoRI2T9GRwoD2dKAXDOXC4Ynsg +/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlg +lPx4mI88k1HtQJAH32RjJMtOcQWh15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryP +A9gK8kxkRr05YuWW6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3m +i4TWnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5jwa19hAM8 +EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWzaGHQRiapIVJpLesux+t3 +zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmyKwbQBM0= +-----END CERTIFICATE----- + +TeliaSonera Root CA v1 +====================== +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAwNzEUMBIGA1UE +CgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJvb3QgQ0EgdjEwHhcNMDcxMDE4 +MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYDVQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwW +VGVsaWFTb25lcmEgUm9vdCBDQSB2MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+ +6yfwIaPzaSZVfp3FVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA +3GV17CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+XZ75Ljo1k +B1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+/jXh7VB7qTCNGdMJjmhn +Xb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxH +oLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkmdtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3 +F0fUTPHSiXk+TT2YqGHeOh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJ +oWjiUIMusDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4pgd7 +gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fsslESl1MpWtTwEhDc +TwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQarMCpgKIv7NHfirZ1fpoeDVNAgMB +AAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qW +DNXr+nuqF+gTEjANBgkqhkiG9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNm +zqjMDfz1mgbldxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1TjTQpgcmLNkQfW +pb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBedY2gea+zDTYa4EzAvXUYNR0PV +G6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpc +c41teyWRyu5FrgZLAMzTsVlQ2jqIOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOT +JsjrDNYmiLbAJM+7vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2 +qReWt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcnHL/EVlP6 +Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVxSK236thZiNSQvxaz2ems +WWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +E-Tugra Certification Authority +=============================== +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNVBAYTAlRSMQ8w +DQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamls +ZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN +ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMw +NTEyMDk0OFoXDTIzMDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmEx +QDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxl +cmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQD +DB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEA4vU/kwVRHoViVF56C/UYB4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vd +hQd2h8y/L5VMzH2nPbxHD5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5K +CKpbknSFQ9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEoq1+g +ElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3Dk14opz8n8Y4e0ypQ +BaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcHfC425lAcP9tDJMW/hkd5s3kc91r0 +E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsutdEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gz +rt48Ue7LE3wBf4QOXVGUnhMMti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAq +jqFGOjGY5RH8zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn +rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUXU8u3Zg5mTPj5 +dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6Jyr+zE7S6E5UMA8GA1UdEwEB +/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEG +MA0GCSqGSIb3DQEBCwUAA4ICAQAFNzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAK +kEh47U6YA5n+KGCRHTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jO +XKqYGwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c77NCR807 +VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3+GbHeJAAFS6LrVE1Uweo +a2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WKvJUawSg5TB9D0pH0clmKuVb8P7Sd2nCc +dlqMQ1DujjByTd//SffGqWfZbawCEeI6FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEV +KV0jq9BgoRJP3vQXzTLlyb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gT +Dx4JnW2PAJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpDy4Q0 +8ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8dNL/+I5c30jn6PQ0G +C7TbO6Orb1wdtn7os4I07QZcJA== +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 2 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgx +MDAxMTA0MDE0WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUdAqSzm1nzHoqvNK38DcLZ +SBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiCFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/F +vudocP05l03Sx5iRUKrERLMjfTlH6VJi1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx970 +2cu+fjOlbpSD8DT6IavqjnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGV +WOHAD3bZwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/WSA2AHmgoCJrjNXy +YdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhyNsZt+U2e+iKo4YFWz827n+qrkRk4 +r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPACuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNf +vNoBYimipidx5joifsFvHZVwIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR +3p1m0IvVVGb6g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlPBSeOE6Fuwg== +-----END CERTIFICATE----- + +Atos TrustedRoot 2011 +===================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UEAwwVQXRvcyBU +cnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0xMTA3MDcxNDU4 +MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMMFUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsG +A1UECgwEQXRvczELMAkGA1UEBhMCREUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV +hTuXbyo7LjvPpvMpNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr +54rMVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+SZFhyBH+ +DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ4J7sVaE3IqKHBAUsR320 +HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0Lcp2AMBYHlT8oDv3FdU9T1nSatCQujgKR +z3bFmx5VdJx4IbHwLfELn8LVlhgf8FQieowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7R +l+lwrrw7GWzbITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZ +bNshMBgGA1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +CwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8jvZfza1zv7v1Apt+h +k6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kPDpFrdRbhIfzYJsdHt6bPWHJxfrrh +TZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pcmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a9 +61qn8FYiqTxlVMYVqL2Gns2Dlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G +3mB/ufNPRJLvKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +QuoVadis Root CA 1 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakE +PBtVwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWerNrwU8lm +PNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF34168Xfuw6cwI2H44g4hWf6 +Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh4Pw5qlPafX7PGglTvF0FBM+hSo+LdoIN +ofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXpUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/l +g6AnhF4EwfWQvTA9xO+oabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV +7qJZjqlc3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/GKubX +9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSthfbZxbGL0eUQMk1f +iyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KOTk0k+17kBL5yG6YnLUlamXrXXAkg +t3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOtzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZI +hvcNAQELBQADggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2cDMT/uFPpiN3 +GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUNqXsCHKnQO18LwIE6PWThv6ct +Tr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP ++V04ikkwj+3x6xn0dxoxGE1nVGwvb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh +3jRJjehZrJ3ydlo28hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fa +wx/kNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNjZgKAvQU6 +O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhpq1467HxpvMc7hU6eFbm0 +FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFtnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOV +hMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +QuoVadis Root CA 2 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFh +ZiFfqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMWn4rjyduY +NM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ymc5GQYaYDFCDy54ejiK2t +oIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+o +MiwMzAkd056OXbxMmO7FGmh77FOm6RQ1o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+l +V0POKa2Mq1W/xPtbAd0jIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZo +L1NesNKqIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz8eQQ +sSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43ehvNURG3YBZwjgQQvD +6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l7ZizlWNof/k19N+IxWA1ksB8aRxh +lRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALGcC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZI +hvcNAQELBQADggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RCroijQ1h5fq7K +pVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0GaW/ZZGYjeVYg3UQt4XAoeo0L9 +x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgz +dWqTHBLmYF5vHX/JHyPLhGGfHoJE+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6X +U/IyAgkwo1jwDQHVcsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+Nw +mNtddbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNgKCLjsZWD +zYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeMHVOyToV7BjjHLPj4sHKN +JeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4WSr2Rz0ZiC3oheGe7IUIarFsNMkd7Egr +O3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +QuoVadis Root CA 3 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286 +IxSR/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNuFoM7pmRL +Mon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXRU7Ox7sWTaYI+FrUoRqHe +6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+cra1AdHkrAj80//ogaX3T7mH1urPnMNA3 +I4ZyYUUpSFlob3emLoG+B01vr87ERRORFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3U +VDmrJqMz6nWB2i3ND0/kA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f7 +5li59wzweyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634RylsSqi +Md5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBpVzgeAVuNVejH38DM +dyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0QA4XN8f+MFrXBsj6IbGB/kE+V9/Yt +rQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZI +hvcNAQELBQADggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnIFUBhynLWcKzS +t/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5WvvoxXqA/4Ti2Tk08HS6IT7SdEQ +TXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFgu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9Du +DcpmvJRPpq3t/O5jrFc/ZSXPsoaP0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGib +Ih6BJpsQBJFxwAYf3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmD +hPbl8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+DhcI00iX +0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HNPlopNLk9hM6xZdRZkZFW +dSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ywaZWWDYWGWVjUTR939+J399roD1B0y2 +PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +DigiCert Assured ID Root G2 +=========================== +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgw +MTE1MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSAn61UQbVH +35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4HteccbiJVMWWXvdMX0h5i89vq +bFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9HpEgjAALAcKxHad3A2m67OeYfcgnDmCXRw +VWmvo2ifv922ebPynXApVfSr/5Vh88lAbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OP +YLfykqGxvYmJHzDNw6YuYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+Rn +lTGNAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTO +w0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPIQW5pJ6d1Ee88hjZv +0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I0jJmwYrA8y8678Dj1JGG0VDjA9tz +d29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4GnilmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAW +hsI6yLETcDbYz+70CjTVW0z9B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0M +jomZmWzwPDCvON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +DigiCert Assured ID Root G3 +=========================== +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYD +VQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJfZn4f5dwb +RXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17QRSAPWXYQ1qAk8C3eNvJs +KTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgF +UaFNN6KDec6NHSrkhDAKBggqhkjOPQQDAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5Fy +YZ5eEJJZVrmDxxDnOOlYJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy +1vUhZscv6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +DigiCert Global Root G2 +======================= +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUx +MjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJ +kTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO +3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauV +BJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyM +UNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu +5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9PMWLSn/pvtsr +F9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0U +WTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBH +QRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/ +iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +DigiCert Global Root G3 +======================= +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYD +VQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAw +MDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k +aWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FGfp4tn+6O +YwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPOZ9wj/wMco+I+o0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNp +Yim8S8YwCgYIKoZIzj0EAwMDaAAwZQIxAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y +3maTD/HMsQmP3Wyr+mt/oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34 +VOKa5Vt8sycX +-----END CERTIFICATE----- + +DigiCert Trusted Root G4 +======================== +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEw +HwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEp +pz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9o +k3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7Fsa +vOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGY +QJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6 +MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtm +mnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7 +f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFH +dL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8 +oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2SV1EY+CtnJYY +ZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd+SeuMIW59mdNOj6PWTkiU0Tr +yF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWcfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy +7zBZLq7gcfJW5GqXb5JQbZaNaHqasjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iah +ixTXTBmyUEFxPT9NcCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN +5r5N0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie4u1Ki7wb +/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mIr/OSmbaz5mEP0oUA51Aa +5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tK +G48BtieVU+i2iW1bvGjUI+iLUaJW+fCmgKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP +82Z+ +-----END CERTIFICATE----- + +COMODO RSA Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCBhTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMTE5MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR6FSS0gpWsawNJN3Fz0Rn +dJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8Xpz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZ +FGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+ +5eNu/Nio5JIk2kNrYrhV/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pG +x8cgoLEfZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z+pUX +2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7wqP/0uK3pN/u6uPQL +OvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZahSL0896+1DSJMwBGB7FY79tOi4lu3 +sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVICu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+C +GCe01a60y1Dma/RMhnEw6abfFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5 +WdYgGq/yapiqcrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvlwFTPoCWOAvn9sKIN9SCYPBMt +rFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+ +nq6PK7o9mfjYcwlYRm6mnPTXJ9OV2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSg +tZx8jb8uk2IntznaFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwW +sRqZCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiKboHGhfKp +pC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmckejkk9u+UJueBPSZI9FoJA +zMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yLS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHq +ZJx64SIDqZxubw5lT2yHh17zbqD5daWbQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk52 +7RH89elWsn2/x20Kk4yl0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7I +LaZRfyHBNVOFBkpdn627G190 +-----END CERTIFICATE----- + +USERTrust RSA Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCAEmUXNg7D2wiz +0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2j +Y0K2dvKpOyuR+OJv0OwWIJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFn +RghRy4YUVD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O ++T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT79uq +/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6c0Plfg6lZrEpfDKE +Y1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJM +lXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8 +yexDJtC/QV9AqURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+ +eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPFUp/L+M+ZBn8b2kMVn54CVVeW +FPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KOVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ +7l8wXEskEVX/JJpuXior7gtNn3/3ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQ +Eg9zKC7F4iRO/Fjs8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM +8WcRiQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYzeSf7dNXGi +FSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZXHlKYC6SQK5MNyosycdi +yA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9c +J2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRBVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGw +sAvgnEzDHNb842m1R0aBL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gx +Q+6IHdfGjjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +USERTrust ECC Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqfloI+d61SRvU8Za2EurxtW2 +0eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinngo4N+LZfQYcTxmdwlkWOrfzCjtHDix6Ez +nPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNV +HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBB +HU6+4WMBzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbWRNZu +9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R4 +=========================== +-----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprl +OQcJFspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAwDgYDVR0P +AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61FuOJAf/sKbvu+M8k8o4TV +MAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGXkPoUVy0D7O48027KqGx2vKLeuwIgJ6iF +JzWbVsaj8kfSt24bAgAXqmemFZHe+pTsewv4n4Q= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R5 +=========================== +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6 +SFkc8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8kehOvRnkmS +h5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYIKoZIzj0EAwMDaAAwZQIxAOVpEslu28Yx +uglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7 +yFz9SO8NdCKoCOJuxUnOxwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +Staat der Nederlanden Root CA - G3 +================================== +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJOTDEeMBwGA1UE +CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFhdCBkZXIgTmVkZXJsYW5kZW4g +Um9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloXDTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMC +TkwxHjAcBgNVBAoMFVN0YWF0IGRlciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5l +ZGVybGFuZGVuIFJvb3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4y +olQPcPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WWIkYFsO2t +x1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqXxz8ecAgwoNzFs21v0IJy +EavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFyKJLZWyNtZrVtB0LrpjPOktvA9mxjeM3K +Tj215VKb8b475lRgsGYeCasH/lSJEULR9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUur +mkVLoR9BvUhTFXFkC4az5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU5 +1nus6+N86U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7Ngzp +07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHPbMk7ccHViLVlvMDo +FxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXtBznaqB16nzaeErAMZRKQFWDZJkBE +41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTtXUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleu +yjWcLhL75LpdINyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD +U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwpLiniyMMB8jPq +KqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8Ipf3YF3qKS9Ysr1YvY2WTxB1 +v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixpgZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA +8KCWAg8zxXHzniN9lLf9OtMJgwYh/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b +8KKaa8MFSu1BYBQw0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0r +mj1AfsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq4BZ+Extq +1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR1VmiiXTTn74eS9fGbbeI +JG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/QFH1T/U67cjF68IeHRaVesd+QnGTbksV +tzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM94B7IWcnMFk= +-----END CERTIFICATE----- + +Staat der Nederlanden EV Root CA +================================ +-----BEGIN CERTIFICATE----- +MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJOTDEeMBwGA1UE +CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFhdCBkZXIgTmVkZXJsYW5kZW4g +RVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0yMjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5M +MR4wHAYDVQQKDBVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRl +cmxhbmRlbiBFViBSb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkk +SzrSM4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nCUiY4iKTW +O0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3dZ//BYY1jTw+bbRcwJu+r +0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46prfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8 +Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13lpJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gV +XJrm0w912fxBmJc+qiXbj5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr +08C+eKxCKFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS/ZbV +0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0XcgOPvZuM5l5Tnrmd +74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH1vI4gnPah1vlPNOePqc7nvQDs/nx +fRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrPpx9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwa +ivsnuL8wbqg7MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI +eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u2dfOWBfoqSmu +c0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHSv4ilf0X8rLiltTMMgsT7B/Zq +5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTCwPTxGfARKbalGAKb12NMcIxHowNDXLldRqAN +b/9Zjr7dn3LDWyvfjFvO5QxGbJKyCqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tN +f1zuacpzEPuKqf2evTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi +5Dp6Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIaGl6I6lD4 +WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeLeG9QgkRQP2YGiqtDhFZK +DyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGy +eUN51q1veieQA6TqJIc/2b3Z6fJfUEkc7uzXLg== +-----END CERTIFICATE----- + +IdenTrust Commercial Root CA 1 +============================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBS +b290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQwMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzES +MBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENB +IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ld +hNlT3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU+ehcCuz/ +mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gpS0l4PJNgiCL8mdo2yMKi +1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1bVoE/c40yiTcdCMbXTMTEl3EASX2MN0C +XZ/g1Ue9tOsbobtJSdifWwLziuQkkORiT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl +3ZBWzvurpWCdxJ35UrCLvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzy +NeVJSQjKVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZKdHzV +WYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHTc+XvvqDtMwt0viAg +xGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hvl7yTmvmcEpB4eoCHFddydJxVdHix +uuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5NiGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZI +hvcNAQELBQADggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwtLRvM7Kqas6pg +ghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93nAbowacYXVKV7cndJZ5t+qnt +ozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmV +YjzlVYA211QC//G5Xc7UI2/YRYRKW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUX +feu+h1sXIFRRk0pTAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/ro +kTLql1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG4iZZRHUe +2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZmUlO+KWA2yUPHGNiiskz +Z2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7R +cGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +IdenTrust Public Sector Root CA 1 +================================= +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3Rv +ciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcNMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJV +UzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBS +b290IENBIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTy +P4o7ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGyRBb06tD6 +Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlSbdsHyo+1W/CD80/HLaXI +rcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF/YTLNiCBWS2ab21ISGHKTN9T0a9SvESf +qy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoS +mJxZZoY+rfGwyj4GD3vwEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFn +ol57plzy9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9VGxyh +LrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ2fjXctscvG29ZV/v +iDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsVWaFHVCkugyhfHMKiq3IXAAaOReyL +4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gDW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8B +Af8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMw +DQYJKoZIhvcNAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHVDRDtfULAj+7A +mgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9TaDKQGXSc3z1i9kKlT/YPyNt +GtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8GlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFt +m6/n6J91eEyrRjuazr8FGF1NFTwWmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMx +NRF4eKLg6TCMf4DfWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4 +Mhn5+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJtshquDDI +ajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhAGaQdp/lLQzfcaFpPz+vC +ZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ +3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +Entrust Root Certification Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVy +bXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ug +b25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIw +HhcNMDkwNzA3MTcyNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoT +DUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMx +OTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP +/vaCeb9zYQYKpSfYs1/TRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXz +HHfV1IWNcCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hWwcKU +s/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1U1+cPvQXLOZprE4y +TGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0jaWvYkxN4FisZDQSA/i2jZRjJKRx +AgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ6 +0B7vfec7aVHUbI2fkBJmqzANBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5Z +iXMRrEPR9RP/jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v1fN2D807iDgi +nWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4RnAuknZoh8/CbCzB428Hch0P+ +vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmHVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xO +e4pIb4tF9g== +-----END CERTIFICATE----- + +Entrust Root Certification Authority - EC1 +========================================== +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkGA1UEBhMCVVMx +FjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVn +YWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEzMDEGA1UEAxMqRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRUMxMB4XDTEyMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYw +FAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2Fs +LXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQg +dXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt +IEVDMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHy +AsWfoPZb1YsGGYZPUxBtByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef +9eNi1KlHBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVCR98crlOZF7ZvHH3h +vxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nXhTcGtXsI/esni0qU+eH6p44mCOh8 +kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +CFCA EV ROOT +============ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4GA1UE +CgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNB +IEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkxMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEw +MC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQD +DAxDRkNBIEVWIFJPT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnV +BU03sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpLTIpTUnrD +7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5/ZOkVIBMUtRSqy5J35DN +uF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp7hZZLDRJGqgG16iI0gNyejLi6mhNbiyW +ZXvKWfry4t3uMCz7zEasxGPrb382KzRzEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7 +xzbh72fROdOXW3NiGUgthxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9f +py25IGvPa931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqotaK8K +gWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNgTnYGmE69g60dWIol +hdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfVPKPtl8MeNPo4+QgO48BdK4PRVmrJ +tqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hvcWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAf +BgNVHSMEGDAWgBTj/i39KNALtbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObTej/tUxPQ4i9q +ecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdLjOztUmCypAbqTuv0axn96/Ua +4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBSESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sG +E5uPhnEFtC+NiWYzKXZUmhH4J/qyP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfX +BDrDMlI1Dlb4pd19xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjn +aH9dCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN5mydLIhy +PDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe/v5WOaHIz16eGWRGENoX +kbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+ZAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3C +ekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GB CA +=============================== +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQG +EwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAw +MzJaFw0zOTEyMDExNTEwMzFaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEds +b2JhbCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3HEokKtaX +scriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGxWuR51jIjK+FTzJlFXHtP +rby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk +9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNku7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4o +Qnc/nSMbsrY9gBQHTC5P99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvg +GUpuuy9rM2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZI +hvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrghcViXfa43FK8+5/ea4n32cZiZBKpD +dHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0 +VQreUGdNZtGn//3ZwLWoo4rOZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEui +HZeeevJuQHHfaPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +SZAFIR ROOT CA2 +=============== +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQELBQAwUTELMAkG +A1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6ZW5pb3dhIFMuQS4xGDAWBgNV +BAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkwNzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJ +BgNVBAYTAlBMMSgwJgYDVQQKDB9LcmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYD +VQQDDA9TWkFGSVIgUk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5Q +qEvNQLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT3PSQ1hNK +DJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw3gAeqDRHu5rr/gsUvTaE +2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr63fE9biCloBK0TXC5ztdyO4mTp4CEHCdJ +ckm1/zuVnsHMyAHs6A6KCpbns6aH5db5BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwi +ieDhZNRnvDF5YTy7ykHNXGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsFAAOC +AQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw8PRBEew/R40/cof5 +O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOGnXkZ7/e7DDWQw4rtTw/1zBLZpD67 +oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCPoky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul +4+vJhaAlIDf7js4MNIThPIGyd05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6 ++/NNIxuZMzSgLvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +Certum Trusted Network CA 2 +=========================== +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCBgDELMAkGA1UE +BhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1 +bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29y +ayBDQSAyMCIYDzIwMTExMDA2MDgzOTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQ +TDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENB +IDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWADGSdhhuWZGc/IjoedQF9 +7/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+o +CgCXhVqqndwpyeI1B+twTUrWwbNWuKFBOJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40b +Rr5HMNUuctHFY9rnY3lEfktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2p +uTRZCr+ESv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1mo130 +GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02isx7QBlrd9pPPV3WZ +9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOWOZV7bIBaTxNyxtd9KXpEulKkKtVB +Rgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgezTv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pye +hizKV/Ma5ciSixqClnrDvFASadgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vM +BhBgu4M1t15n3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZI +hvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQF/xlhMcQSZDe28cmk4gmb3DW +Al45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTfCVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuA +L55MYIR4PSFk1vtBHxgP58l1cb29XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMo +clm2q8KMZiYcdywmdjWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tM +pkT/WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jbAoJnwTnb +w3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksqP/ujmv5zMnHCnsZy4Ypo +J/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Kob7a6bINDd82Kkhehnlt4Fj1F4jNy3eFm +ypnTycUm/Q1oBEauttmbjL4ZvrHG8hnjXALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLX +is7VmFxWlgPF7ncGNf/P5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7 +zAYspsbiDrW5viSP +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions RootCA 2015 +======================================================= +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcT +BkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0 +aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAx +MTIxWjCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMg +QWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNV +BAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIw +MTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDC+Kk/G4n8PDwEXT2QNrCROnk8Zlrv +bTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+eh +iGsxr/CL0BgzuNtFajT0AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+ +6PAQZe104S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06CojXd +FPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV9Cz82XBST3i4vTwr +i5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrDgfgXy5I2XdGj2HUb4Ysn6npIQf1F +GQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2 +fu/Z8VFRfS0myGlZYeCsargqNhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9mu +iNX6hME6wGkoLfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVdctA4GGqd83EkVAswDQYJKoZI +hvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0IXtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+ +D1hYc2Ryx+hFjtyp8iY/xnmMsVMIM4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrM +d/K4kPFox/la/vot9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+y +d+2VZ5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/eaj8GsGsVn +82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnhX9izjFk0WaSrT2y7Hxjb +davYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQl033DlZdwJVqwjbDG2jJ9SrcR5q+ss7F +Jej6A7na+RZukYT1HCjI/CbM1xyQVqdfbzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVt +J94Cj8rDtSvK6evIIVM4pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGa +JI7ZjnHKe7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0vm9q +p/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions ECC RootCA 2015 +=========================================================== +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0 +aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u +cyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgRUNDIFJvb3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEw +MzcxMlowgaoxCzAJBgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmlj +IEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUQwQgYD +VQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIEVDQyBSb290 +Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKgQehLgoRc4vgxEZmGZE4JJS+dQS8KrjVP +dJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJajq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoK +Vlp8aQuqgAkkbH7BRqNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFLQiC4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaeplSTA +GiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7SofTUwJCA3sS61kFyjn +dc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +ISRG Root X1 +============ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UE +BhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQD +EwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQG +EwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMT +DElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54r +Vygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj1 +3Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8K +b4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCN +Aymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ +4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf +1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFu +hjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQH +usEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/r +OPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4G +A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY +9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV +0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwt +hDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJw +TdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nx +e5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZA +JzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahD +YVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9n +JEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJ +m+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +AC RAIZ FNMT-RCM +================ +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsxCzAJBgNVBAYT +AkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTAeFw0wODEw +MjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJD +TTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBALpxgHpMhm5/yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcf +qQgfBBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAzWHFctPVr +btQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxFtBDXaEAUwED653cXeuYL +j2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z374jNUUeAlz+taibmSXaXvMiwzn15Cou +08YfxGyqxRxqAQVKL9LFwag0Jl1mpdICIfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mw +WsXmo8RZZUc1g16p6DULmbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnT +tOmlcYF7wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peSMKGJ +47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2ZSysV4999AeU14EC +ll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMetUqIJ5G+GR4of6ygnXYMgrwTJbFaa +i0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FPd9xf3E6Jobd2Sn9R2gzL+HYJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1o +dHRwOi8vd3d3LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1RXxlDPiyN8+s +D8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYMLVN0V2Ue1bLdI4E7pWYjJ2cJ +j+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrT +Qfv6MooqtyuGC2mDOL7Nii4LcK2NJpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW ++YJF1DngoABd15jmfZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7 +Ixjp6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp1txyM/1d +8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B9kiABdcPUXmsEKvU7ANm +5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wokRqEIr9baRRmW1FMdW4R58MD3R++Lj8UG +rp1MYp3/RgT408m2ECVAdf4WqslKYIYvuu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +Amazon Root CA 1 +================ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAxMB4XDTE1 +MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALJ4gHHKeNXjca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgH +FzZM9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qwIFAGbHrQ +gLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6VOujw5H5SNz/0egwLX0t +dHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L93FcXmn/6pUCyziKrlA4b9v7LWIbxcce +VOF34GfID5yHI9Y/QCB/IIDEgEw+OyQmjgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3 +DQEBCwUAA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDIU5PM +CCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUsN+gDS63pYaACbvXy +8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vvo/ufQJVtMVT8QtPHRh8jrdkPSHCa +2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2 +xJNDd2ZhwLnoQdeXeGADbkpyrqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +Amazon Root CA 2 +================ +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAyMB4XDTE1 +MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBAK2Wny2cSkxKgXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4 +kHbZW0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg1dKmSYXp +N+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K8nu+NQWpEjTj82R0Yiw9 +AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvd +fLC6HM783k81ds8P+HgfajZRRidhW+mez/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAEx +kv8LV/SasrlX6avvDXbR8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSS +btqDT6ZjmUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz7Mt0 +Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6+XUyo05f7O0oYtlN +c/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI0u1ufm8/0i2BWSlmy5A5lREedCf+ +3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSw +DPBMMPQFWAJI/TPlUq9LhONmUjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oA +A7CXDpO8Wqj2LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kSk5Nrp+gvU5LE +YFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl7uxMMne0nxrpS10gxdr9HIcW +xkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygmbtmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQ +gj9sAq+uEjonljYE1x2igGOpm/HlurR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbW +aQbLU8uz/mtBzUF+fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoV +Yh63n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE76KlXIx3 +KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H9jVlpNMKVv/1F2Rs76gi +JUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT4PsJYGw= +-----END CERTIFICATE----- + +Amazon Root CA 3 +================ +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAzMB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZB +f8ANm+gBG1bG8lKlui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjr +Zt6jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSrttvXBp43 +rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkrBqWTrBqYaGFy+uGh0Psc +eGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteMYyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +Amazon Root CA 4 +================ +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSA0MB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN +/sGKe0uoe0ZLY7Bi9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri +83BkM6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WBMAoGCCqGSM49BAMDA2gA +MGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlwCkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1 +AE47xDqUEpHJWEadIRNyp4iciuRMStuW1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 +============================================= +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIxGDAWBgNVBAcT +D0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxpbXNlbCB2ZSBUZWtub2xvamlr +IEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0wKwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24g +TWVya2V6aSAtIEthbXUgU00xNjA0BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRp +ZmlrYXNpIC0gU3VydW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYD +VQQGEwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXllIEJpbGlt +c2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklUQUsxLTArBgNVBAsTJEth +bXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBTTTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11 +IFNNIFNTTCBLb2sgU2VydGlmaWthc2kgLSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAr3UwM6q7a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y8 +6Ij5iySrLqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INrN3wc +wv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2XYacQuFWQfw4tJzh0 +3+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/iSIzL+aFCr2lqBs23tPcLG07xxO9 +WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4fAJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQU +ZT/HiobGPN08VFw1+DrtUgxHV8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJ +KoZIhvcNAQELBQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPfIPP54+M638yc +lNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4lzwDGrpDxpa5RXI4s6ehlj2R +e37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0j +q5Rm+K37DwhuJi1/FwcJsoz7UMCflo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +GDCA TrustAUTH R5 ROOT +====================== +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCQ04xMjAw +BgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8wHQYDVQQD +DBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVow +YjELMAkGA1UEBhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJjDp6L3TQs +AlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBjTnnEt1u9ol2x8kECK62p +OqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+uKU49tm7srsHwJ5uu4/Ts765/94Y9cnrr +pftZTqfrlYwiOXnhLQiPzLyRuEH3FMEjqcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ +9Cy5WmYqsBebnh52nUpmMUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQ +xXABZG12ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloPzgsM +R6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3GkL30SgLdTMEZeS1SZ +D2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeCjGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4 +oR24qoAATILnsn8JuLwwoC8N9VKejveSswoAHQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx +9hoh49pwBiFYFIeFd3mqgnkCAwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlR +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZmDRd9FBUb1Ov9 +H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5COmSdI31R9KrO9b7eGZONn35 +6ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ryL3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd ++PwyvzeG5LuOmCd+uh8W4XAR8gPfJWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQ +HtZa37dG/OaG+svgIHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBD +F8Io2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV09tL7ECQ +8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQXR4EzzffHqhmsYzmIGrv +/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrqT8p+ck0LcIymSLumoRT2+1hEmRSuqguT +aaApJUqlyyvdimYHFngVV3Eb7PVHhPOeMTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +TrustCor RootCert CA-1 +====================== +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYDVQQGEwJQQTEP +MA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3Ig +U3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkxHzAdBgNVBAMMFlRydXN0Q29yIFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkx +MjMxMTcyMzE2WjCBpDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFu +YW1hIENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUGA1UECwwe +VHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZUcnVzdENvciBSb290Q2Vy +dCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv463leLCJhJrMxnHQFgKq1mq +jQCj/IDHUHuO1CAmujIS2CNUSSUQIpidRtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4 +pQa81QBeCQryJ3pS/C3Vseq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0 +JEsq1pme9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CVEY4h +gLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorWhnAbJN7+KIor0Gqw +/Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/DeOxCbeKyKsZn3MzUOcwHwYDVR0j +BBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwDQYJKoZIhvcNAQELBQADggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5 +mDo4Nvu7Zp5I/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf +ke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZyonnMlo2HD6C +qFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djtsL1Ac59v2Z3kf9YKVmgenFK+P +3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdNzl/HHk484IkzlQsPpTLWPFp5LBk= +-----END CERTIFICATE----- + +TrustCor RootCert CA-2 +====================== +-----BEGIN CERTIFICATE----- +MIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNVBAYTAlBBMQ8w +DQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQwIgYDVQQKDBtUcnVzdENvciBT +eXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRydXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0 +eTEfMB0GA1UEAwwWVHJ1c3RDb3IgUm9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEy +MzExNzI2MzlaMIGkMQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5h +bWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U +cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29yIFJvb3RDZXJ0 +IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnIG7CKqJiJJWQdsg4foDSq8Gb +ZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+QVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9Nk +RvRUqdw6VC0xK5mC8tkq1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1 +oYxOdqHp2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nKDOOb +XUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hapeaz6LMvYHL1cEksr1 +/p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF3wP+TfSvPd9cW436cOGlfifHhi5q +jxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88oWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQP +eSghYA2FFn3XVDjxklb9tTNMg9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+Ctg +rKAmrhQhJ8Z3mjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh +8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAdBgNVHQ4EFgQU +2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6UnrybPZx9mCAZ5YwwYrIwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/h +Osh80QA9z+LqBrWyOrsGS2h60COXdKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnp +kpfbsEZC89NiqpX+MWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv +2wnL/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RXCI/hOWB3 +S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYaZH9bDTMJBzN7Bj8RpFxw +PIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dv +DDqPys/cA8GiCcjl/YBeyGBCARsaU1q7N6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYU +RpFHmygk71dSTlxCnKr3Sewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANE +xdqtvArBAs8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp5KeX +RKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu1uwJ +-----END CERTIFICATE----- + +TrustCor ECA-1 +============== +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYDVQQGEwJQQTEP +MA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3Ig +U3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkxFzAVBgNVBAMMDlRydXN0Q29yIEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3Mjgw +N1owgZwxCzAJBgNVBAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5 +MSQwIgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRydXN0Q29y +IENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3IgRUNBLTEwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb3w9U73NjKYKtR8aja+3+XzP4Q1HpGjOR +MRegdMTUpwHmspI+ap3tDvl0mEDTPwOABoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23 +xFUfJ3zSCNV2HykVh0A53ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmc +p0yJF4OuowReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/wZ0+ +fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZFZtS6mFjBAgMBAAGj +YzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAfBgNVHSMEGDAWgBREnkj1zG1I1KBL +f/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF +AAOCAQEABT41XBVwm8nHc2FvcivUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u +/ukZMjgDfxT2AHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F +hcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50soIipX1TH0Xs +J5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BIWJZpTdwHjFGTot+fDz2LYLSC +jaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1WitJ/X5g== +-----END CERTIFICATE----- + +SSL.com Root Certification Authority RSA +======================================== +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxDjAM +BgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24x +MTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYw +MjEyMTczOTM5WhcNNDEwMjEyMTczOTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx +EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NM +LmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2RxFdHaxh3a3by/ZPkPQ/C +Fp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aXqhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8 +P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcCC52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/ge +oeOy3ZExqysdBP+lSgQ36YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkp +k8zruFvh/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrFYD3Z +fBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93EJNyAKoFBbZQ+yODJ +gUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVcUS4cK38acijnALXRdMbX5J+tB5O2 +UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi8 +1xtZPCvM8hnIk2snYxnP/Okm+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4s +bE6x/c+cCbqiM+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGVcpNxJK1ok1iOMq8bs3AD/CUr +dIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBcHadm47GUBwwyOabqG7B52B2ccETjit3E+ZUf +ijhDPwGFpUenPUayvOUiaPd7nNgsPgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAsl +u1OJD7OAUN5F7kR/q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjq +erQ0cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jra6x+3uxj +MxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90IH37hVZkLId6Tngr75qNJ +vTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/YK9f1JmzJBjSWFupwWRoyeXkLtoh/D1JI +Pb9s2KJELtFOt3JY04kTlf5Eq/jXixtunLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406y +wKBjYZC6VWg3dGq2ktufoYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NI +WuuA8ShYIc2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +SSL.com Root Certification Authority ECC +======================================== +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xMTAv +BgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEy +MTgxNDAzWhcNNDEwMjEyMTgxNDAzWjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO +BgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuBBAAiA2IA +BEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI7Z4INcgn64mMU1jrYor+ +8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPgCemB+vNH06NjMGEwHQYDVR0OBBYEFILR +hXMw5zUE044CkvvlpNHEIejNMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTT +jgKS++Wk0cQh6M0wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCW +e+0F+S8Tkdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+gA0z +5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +SSL.com EV Root Certification Authority RSA R2 +============================================== +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAlVTMQ4w +DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9u +MTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MB4XDTE3MDUzMTE4MTQzN1oXDTQyMDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI +DAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYD +VQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvqM0fNTPl9fb69LT3w23jh +hqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssufOePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7w +cXHswxzpY6IXFJ3vG2fThVUCAtZJycxa4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTO +Zw+oz12WGQvE43LrrdF9HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+ +B6KjBSYRaZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcAb9Zh +CBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQGp8hLH94t2S42Oim +9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQVPWKchjgGAGYS5Fl2WlPAApiiECto +RHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMOpgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+Slm +JuwgUHfbSguPvuUCYHBBXtSuUDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48 ++qvWBkofZ6aYMBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa49QaAJadz20Zp +qJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBWs47LCp1Jjr+kxJG7ZhcFUZh1 +++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nx +Y/hoLVUE0fKNsKTPvDxeH3jnpaAgcLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2G +guDKBAdRUNf/ktUM79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDz +OFSz/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXtll9ldDz7 +CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEmKf7GUmG6sXP/wwyc5Wxq +lD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKKQbNmC1r7fSOl8hqw/96bg5Qu0T/fkreR +rwU7ZcegbLHNYhLDkBvjJc40vG93drEQw/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1 +hlMYegouCRw2n5H9gooiS9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX +9hwJ1C07mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +SSL.com EV Root Certification Authority ECC +=========================================== +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xNDAy +BgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYw +MjEyMTgxNTIzWhcNNDEwMjEyMTgxNTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx +EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NM +LmNvbSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMAVIbc/R/fALhBYlzccBYy +3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1KthkuWnBaBu2+8KGwytAJKaNjMGEwHQYDVR0O +BBYEFFvKXuXe0oGqzagtZFG22XKbl+ZPMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe +5d7SgarNqC1kUbbZcpuX5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJ +N+vp1RPZytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZgh5Mm +m7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +GlobalSign Root CA - R6 +======================= +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEgMB4GA1UECxMX +R2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds +b2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQxMjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9i +YWxTaWduIFJvb3QgQ0EgLSBSNjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFs +U2lnbjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQss +grRIxutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1kZguSgMpE +3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxDaNc9PIrFsmbVkJq3MQbF +vuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJwLnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqM +PKq0pPbzlUoSB239jLKJz9CgYXfIWHSw1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+ +azayOeSsJDa38O+2HBNXk7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05O +WgtH8wY2SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/hbguy +CLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4nWUx2OVvq+aWh2IMP +0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpYrZxCRXluDocZXFSxZba/jJvcE+kN +b7gu3GduyYsRtYQUigAZcIN5kZeR1BonvzceMgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQE +AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNV +HSMEGDAWgBSubAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGtIxg93eFyRJa0 +lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr6155wsTLxDKZmOMNOsIeDjHfrY +BzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLjvUYAGm0CuiVdjaExUd1URhxN25mW7xocBFym +Fe944Hn+Xds+qkxV/ZoVqW/hpvvfcDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr +3TsTjxKM4kEaSHpzoHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB1 +0jZpnOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfspA9MRf/T +uTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+vJJUEeKgDu+6B5dpffItK +oZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+t +JDfLRVpOoERIyNiwmcUVhAn21klJwGW45hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GC CA +=============================== +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQswCQYDVQQGEwJD +SDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEo +MCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRa +Fw00MjA1MDkwOTU4MzNaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQL +ExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4nieUqjFqdr +VCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4Wp2OQ0jnUsYd4XxiWD1Ab +NTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7TrYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0E +AwMDaAAwZQIwJsdpW9zV57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtk +AjEA2zQgMgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +GTS Root R1 +=========== +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQG +EwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJv +b3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAG +A1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx +9vaMf/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7wCl7r +aKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjwTcLCeoiKu7rPWRnW +r4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0PfyblqAj+lug8aJRT7oM6iCsVlgmy4HqM +LnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly +4cpk9+aCEI3oncKKiPo4Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr +06zqkUspzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70paDPvOmbsB4om +3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrNVjzRlwW5y0vtOUucxD/SVRNu +JLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEM +BQADggIBADiWCu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1 +d5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6ZXPYfcX3v73sv +fuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZRgyFmxhE+885H7pwoHyXa/6xm +ld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3d8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9b +gsiG1eGZbYwE8na6SfZu6W0eX6DvJ4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq +4BjFbkerQUIpm/ZgDdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWEr +tXvM+SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyyF62ARPBo +pY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9SQ98POyDGCBDTtWTurQ0 +sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdwsE3PYJ/HQcu51OyLemGhmW/HGY0dVHLql +CFF1pkgl +-----END CERTIFICATE----- + +GTS Root R2 +=========== +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQbkepxlqz5yDFMJo/aFLybzANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQG +EwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJv +b3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAG +A1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTuk +k3LvCvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY6Dlo +7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAuMC6C/Pq8tBcKSOWI +m8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7kRXuJVfeKH2JShBKzwkCX44ofR5Gm +dFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbu +ak7MkogwTZq9TwtImoS1mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscsz +cTJGr61K8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW +Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKaG73Vululycsl +aVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCqgc7dGtxRcw1PcOnlthYhGXmy +5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEM +BQADggIBALZp8KZ3/p7uC4Gt4cCpx/k1HUCCq+YEtN/L9x0Pg/B+E02NjO7jMyLDOfxA325BS0JT +vhaI8dI4XsRomRyYUpOM52jtG2pzegVATX9lO9ZY8c6DR2Dj/5epnGB3GFW1fgiTz9D2PGcDFWEJ ++YF59exTpJ/JjwGLc8R3dtyDovUMSRqodt6Sm2T4syzFJ9MHwAiApJiS4wGWAqoC7o87xdFtCjMw +c3i5T1QWvwsHoaRc5svJXISPD+AVdyx+Jn7axEvbpxZ3B7DNdehyQtaVhJ2Gg/LkkM0JR9SLA3Da +WsYDQvTtN6LwG1BUSw7YhN4ZKJmBR64JGz9I0cNv4rBgF/XuIwKl2gBbbZCr7qLpGzvpx0QnRY5r +n/WkhLx3+WuXrD5RRaIRpsyF7gpo8j5QOHokYh4XIDdtak23CZvJ/KRY9bb7nE4Yu5UC56Gtmwfu +Nmsk0jmGwZODUNKBRqhfYlcsu2xkiAhu7xNUX90txGdj08+JN7+dIPT7eoOboB6BAFDC5AwiWVIQ +7UNWhwD4FFKnHYuTjKJNRn8nxnGbJN7k2oaLDX5rIMHAnuFl2GqjpuiFizoHCBy69Y9Vmhh1fuXs +gWbRIXOhNUQLgD1bnF5vKheW0YMjiGZt5obicDIvUiLnyOd/xCxgXS/Dr55FBcOEArf9LAhST4Ld +o/DUhgkC +-----END CERTIFICATE----- + +GTS Root R3 +=========== +-----BEGIN CERTIFICATE----- +MIICDDCCAZGgAwIBAgIQbkepx2ypcyRAiQ8DVd2NHTAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJV +UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg +UjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE +ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUU +Rout736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL24Cej +QjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTB8Sa6oC2uhYHP +0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEAgFukfCPAlaUs3L6JbyO5o91lAFJekazInXJ0 +glMLfalAvWhgxeG4VDvBNhcl2MG9AjEAnjWSdIUlUfUk7GRSJFClH9voy8l27OyCbvWFGFPouOOa +KaqW04MjyaR7YbPMAuhd +-----END CERTIFICATE----- + +GTS Root R4 +=========== +-----BEGIN CERTIFICATE----- +MIICCjCCAZGgAwIBAgIQbkepyIuUtui7OyrYorLBmTAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJV +UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg +UjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE +ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa +6zzuhXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvRHYqj +QjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSATNbrdP9JNqPV +2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBkAjBqUFJ0CMRw3J5QdCHojXohw0+WbhXRIjVhLfoI +N+4Zba3bssx9BzT1YBkstTTZbyACMANxsbqjYAuG7ZoIapVon+Kz4ZNkfF6Tpt95LY2F45TPI11x +zPKwTdb+mciUqXWi4w== +-----END CERTIFICATE----- + +UCA Global G2 Root +================== +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQG +EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBHbG9iYWwgRzIgUm9vdDAeFw0x +NjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0xCzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlU +cnVzdDEbMBkGA1UEAwwSVUNBIEdsb2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxeYrb3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmT +oni9kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzmVHqUwCoV +8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/RVogvGjqNO7uCEeBHANBS +h6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDcC/Vkw85DvG1xudLeJ1uK6NjGruFZfc8o +LTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIjtm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/ +R+zvWr9LesGtOxdQXGLYD0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBe +KW4bHAyvj5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6DlNaBa +4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6iIis7nCs+dwp4wwc +OxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznPO6Q0ibd5Ei9Hxeepl2n8pndntd97 +8XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFIHEjMz15DD/pQwIX4wVZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo +5sOASD0Ee/ojL3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl1qnN3e92mI0A +Ds0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oUb3n09tDh05S60FdRvScFDcH9 +yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LVPtateJLbXDzz2K36uGt/xDYotgIVilQsnLAX +c47QN6MUPJiVAAwpBVueSUmxX8fjy88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHo +jhJi6IjMtX9Gl8CbEGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZk +bxqgDMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI+Vg7RE+x +ygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGyYiGqhkCyLmTTX8jjfhFn +RR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bXUB+K+wb1whnw0A== +-----END CERTIFICATE----- + +UCA Extended Validation Root +============================ +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQG +EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9u +IFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMxMDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8G +A1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrs +iWogD4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvSsPGP2KxF +Rv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aopO2z6+I9tTcg1367r3CTu +eUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dksHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR +59mzLC52LqGj3n5qiAno8geK+LLNEOfic0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH +0mK1lTnj8/FtDw5lhIpjVMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KR +el7sFsLzKuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/TuDv +B0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41Gsx2VYVdWf6/wFlth +WG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs1+lvK9JKBZP8nm9rZ/+I8U6laUpS +NwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQDfwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS +3H5aBZ8eNJr34RQwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQEL +BQADggIBADaNl8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQVBcZEhrxH9cM +aVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5c6sq1WnIeJEmMX3ixzDx/BR4 +dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb ++7lsq+KePRXBOy5nAliRn+/4Qh8st2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOW +F3sGPjLtx7dCvHaj2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwi +GpWOvpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2CxR9GUeOc +GMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmxcmtpzyKEC2IPrNkZAJSi +djzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbMfjKaiJUINlK73nZfdklJrX+9ZSCyycEr +dhh2n1ax +-----END CERTIFICATE----- + +Certigna Root CA +================ +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAwWjELMAkGA1UE +BhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAwMiA0ODE0NjMwODEwMDAzNjEZ +MBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0xMzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjda +MFoxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYz +MDgxMDAwMzYxGTAXBgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sOty3tRQgX +stmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9MCiBtnyN6tMbaLOQdLNyz +KNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPuI9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8 +JXrJhFwLrN1CTivngqIkicuQstDuI7pmTLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16 +XdG+RCYyKfHx9WzMfgIhC59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq +4NYKpkDfePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3YzIoej +wpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWTCo/1VTp2lc5ZmIoJ +lXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1kJWumIWmbat10TWuXekG9qxf5kBdI +jzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp/ +/TBt2dzhauH8XwIDAQABo4IBGjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczovL3d3d3cuY2Vy +dGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilodHRwOi8vY3JsLmNlcnRpZ25h +LmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYraHR0cDovL2NybC5kaGlteW90aXMuY29tL2Nl +cnRpZ25hcm9vdGNhLmNybDANBgkqhkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOIt +OoldaDgvUSILSo3L6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxP +TGRGHVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH60BGM+RFq +7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncBlA2c5uk5jR+mUYyZDDl3 +4bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdio2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd +8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS +6Cvu5zHbugRqh5jnxV/vfaci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaY +tlu3zM63Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayhjWZS +aX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw3kAP+HwV96LOPNde +E4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +emSign Root CA - G1 +=================== +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJJTjET +MBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl +ZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBHMTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgx +ODMwMDBaMGcxCzAJBgNVBAYTAklOMRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVk +aHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQzf2N4aLTN +LnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO8oG0x5ZOrRkVUkr+PHB1 +cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aqd7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHW +DV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhMtTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ +6DqS0hdW5TUaQBw+jSztOd9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrH +hQIDAQABo0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQDAgEG +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31xPaOfG1vR2vjTnGs2 +vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjMwiI/aTvFthUvozXGaCocV685743Q +NcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6dGNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q ++Mri/Tm3R7nrft8EI6/6nAYH6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeih +U80Bv2noWgbyRQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +emSign ECC Root CA - G3 +======================= +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQGEwJJTjETMBEG +A1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg +MB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4 +MTgzMDAwWjBrMQswCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11 +ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0WXTsuwYc +58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xySfvalY8L1X44uT6EYGQIr +MgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuBzhccLikenEhjQjAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+D +CBeQyh+KTOgNG3qxrdWBCUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7 +jHvrZQnD+JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +emSign Root CA - C1 +=================== +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkGA1UEBhMCVVMx +EzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNp +Z24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UE +BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQD +ExNlbVNpZ24gUm9vdCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+up +ufGZBczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZHdPIWoU/ +Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH3DspVpNqs8FqOp099cGX +OFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvHGPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4V +I5b2P/AgNBbeCsbEBEV5f6f9vtKppa+cxSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleooms +lMuoaJuvimUnzYnu3Yy1aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+ +XJGFehiqTbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD +ggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87/kOXSTKZEhVb3xEp +/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4kqNPEjE2NuLe/gDEo2APJ62gsIq1 +NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrGYQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9 +wC68AivTxEDkigcxHpvOJpkT+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQ +BmIMMMAVSKeoWXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +emSign ECC Root CA - C3 +======================= +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQGEwJVUzETMBEG +A1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMxIDAeBgNVBAMTF2VtU2lnbiBF +Q0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UE +BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQD +ExdlbVNpZ24gRUNDIFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd +6bciMK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4OjavtisIGJAnB9 +SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0OBBYEFPtaSNCAIEDyqOkA +B2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gA +MGUCMQC02C8Cif22TGK6Q04ThHK1rt0c3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwU +ZOR8loMRnLDRWmFLpg9J0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +Hongkong Post Root CA 3 +======================= +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQELBQAwbzELMAkG +A1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJSG9uZyBLb25nMRYwFAYDVQQK +Ew1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25na29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2 +MDMwMjI5NDZaFw00MjA2MDMwMjI5NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtv +bmcxEjAQBgNVBAcTCUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMX +SG9uZ2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz +iNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFOdem1p+/l6TWZ5Mwc50tf +jTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mIVoBc+L0sPOFMV4i707mV78vH9toxdCim +5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOe +sL4jpNrcyCse2m5FHomY2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj +0mRiikKYvLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+TtbNe/ +JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZbx39ri1UbSsUgYT2u +y1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+l2oBlKN8W4UdKjk60FSh0Tlxnf0h ++bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YKTE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsG +xVd7GYYKecsAyVKvQv83j+GjHno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwID +AQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEwDQYJKoZIhvcN +AQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG7BJ8dNVI0lkUmcDrudHr9Egw +W62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCkMpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWld +y8joRTnU+kLBEUx3XZL7av9YROXrgZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov ++BS5gLNdTaqX4fnkGMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDc +eqFS3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJmOzj/2ZQw +9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+l6mc1X5VTMbeRRAc6uk7 +nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6cJfTzPV4e0hz5sy229zdcxsshTrD3mUcY +hcErulWuBurQB7Lcq9CClnXO0lD+mefPL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB +60PZ2Pierc+xYw5F9KBaLJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fq +dBb9HxEGmpv0 +-----END CERTIFICATE----- + +Entrust Root Certification Authority - G4 +========================================= +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAwgb4xCzAJBgNV +BAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3Qu +bmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1 +dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eSAtIEc0MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYT +AlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eSAtIEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3D +umSXbcr3DbVZwbPLqGgZ2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV +3imz/f3ET+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j5pds +8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAMC1rlLAHGVK/XqsEQ +e9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73TDtTUXm6Hnmo9RR3RXRv06QqsYJn7 +ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNXwbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5X +xNMhIWNlUpEbsZmOeX7m640A2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV +7rtNOzK+mndmnqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8 +dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwlN4y6mACXi0mW +Hv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNjc0kCAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9n +MA0GCSqGSIb3DQEBCwUAA4ICAQAS5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4Q +jbRaZIxowLByQzTSGwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht +7LGrhFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/B7NTeLUK +YvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uIAeV8KEsD+UmDfLJ/fOPt +jqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbwH5Lk6rWS02FREAutp9lfx1/cH6NcjKF+ +m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKW +RGhXxNUzzxkvFMSUHHuk2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjA +JOgc47OlIQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk5F6G ++TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuYn/PIjhs4ViFqUZPT +kcpG2om3PVODLAgfi49T3f+sHw== +-----END CERTIFICATE----- + +Microsoft ECC Root Certificate Authority 2017 +============================================= +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQgRUND +IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4 +MjMxNjA0WjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZRogPZnZH6 +thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYbhGBKia/teQ87zvH2RPUB +eMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIy5lycFIM ++Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlf +Xu5gKcs68tvWMoQZP3zVL8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaR +eNtUjGUBiudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +Microsoft RSA Root Certificate Authority 2017 +============================================= +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQg +UlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIw +NzE4MjMwMDIzWjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u +MTYwNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZNt9GkMml +7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0ZdDMbRnMlfl7rEqUrQ7e +S0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw7 +1VdyvD/IybLeS2v4I2wDwAW9lcfNcztmgGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+ +dkC0zVJhUXAoP8XFWvLJjEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49F +yGcohJUcaDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaGYaRS +MLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6W6IYZVcSn2i51BVr +lMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4KUGsTuqwPN1q3ErWQgR5WrlcihtnJ +0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJ +ClTUFLkqqNfs+avNJVgyeY+QW5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZCLgLNFgVZJ8og +6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OCgMNPOsduET/m4xaRhPtthH80 +dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk ++ONVFT24bcMKpBLBaYVu32TxU5nhSnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex +/2kskZGT4d9Mozd2TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDy +AmH3pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGRxpl/j8nW +ZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiAppGWSZI1b7rCoucL5mxAyE +7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKT +c0QWbej09+CVgI+WXTik9KveCjCHk9hNAHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D +5KbvtwEwXlGjefVwaaZBRA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +e-Szigno Root CA 2017 +===================== +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNVBAYTAkhVMREw +DwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUt +MjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJvb3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZa +Fw00MjA4MjIxMjA3MDZaMHExCzAJBgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UE +CgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3pp +Z25vIFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtvxie+RJCx +s1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+HWyx7xf58etqjYzBhMA8G +A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSHERUI0arBeAyxr87GyZDv +vzAEwDAfBgNVHSMEGDAWgBSHERUI0arBeAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEA +tVfd14pVCzbhhkT61NlojbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxO +svxyqltZ+efcMQ== +-----END CERTIFICATE----- + +certSIGN Root CA G2 +=================== +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNVBAYTAlJPMRQw +EgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjAeFw0xNzAy +MDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJBgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lH +TiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAMDFdRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05 +N0IwvlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZuIt4Imfk +abBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhpn+Sc8CnTXPnGFiWeI8Mg +wT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKscpc/I1mbySKEwQdPzH/iV8oScLumZfNp +dWO9lfsbl83kqK/20U6o2YpxJM02PbyWxPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91Qqh +ngLjYl/rNUssuHLoPj1PrCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732 +jcZZroiFDsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fxDTvf +95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgyLcsUDFDYg2WD7rlc +z8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6CeWRgKRM+o/1Pcmqr4tTluCRVLERL +iohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud +DgQWBBSCIS1mxteg4BXrzkwJd8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOB +ywaK8SJJ6ejqkX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQlqiCA2ClV9+BB +/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0OJD7uNGzcgbJceaBxXntC6Z5 +8hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+cNywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5 +BiKDUyUM/FHE5r7iOZULJK2v0ZXkltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklW +atKcsWMy5WHgUyIOpwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tU +Sxfj03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZkPuXaTH4M +NMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE1LlSVHJ7liXMvGnjSG4N +0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MXQRBdJ3NghVdJIgc= +-----END CERTIFICATE----- diff --git a/Client2021/AppSettings.xml b/Client2021/AppSettings.xml new file mode 100644 index 0000000..cd30ce0 --- /dev/null +++ b/Client2021/AppSettings.xml @@ -0,0 +1,5 @@ + + + content + http://www.syntax.eco + diff --git a/Client2021/ExtraContent/LuaPackages/.luacheckrc b/Client2021/ExtraContent/LuaPackages/.luacheckrc new file mode 100644 index 0000000..5f5cf25 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/.luacheckrc @@ -0,0 +1,41 @@ +stds.roblox = { + globals = { + "game" + }, + read_globals = { + -- Roblox globals + "script", + + -- Extra functions + "tick", "warn", "spawn", + "wait", "settings", "typeof", "delay", + + -- Types + "Vector2", "Vector3", + "Color3", + "UDim", "UDim2", + "Rect", + "CFrame", + "Enum", + "Instance", + } +} + +stds.testez = { + read_globals = { + "describe", + "it", "itFOCUS", "itSKIP", + "FOCUS", "SKIP", "HACK_NO_XPCALL", + "expect", + } +} + +ignore = { + "212", -- unused arguments +} + +std = "lua51+roblox" + +files["**/*.spec.lua"] = { + std = "+testez", +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/.robloxrc b/Client2021/ExtraContent/LuaPackages/.robloxrc new file mode 100644 index 0000000..37c60d8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/.robloxrc @@ -0,0 +1,17 @@ +{ + "language": { + "mode": "noinfer" + }, + "lint": { + "MultiLineStatement": "disabled", + "DeprecatedGlobal": "disabled", + "LocalShadow": "disabled", + "LocalUnused": "disabled", + "FunctionUnused": "fatal", + "ImportUnused": "disabled", + "SameLineStatement": "fatal", + "ImplicitReturn": "disabled", + "UnknownGlobal": "fatal", + "GlobalUsedAsLocal": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Analytics/Analytics.lua b/Client2021/ExtraContent/LuaPackages/Analytics/Analytics.lua new file mode 100644 index 0000000..8bc5a5b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Analytics/Analytics.lua @@ -0,0 +1,56 @@ +--[[ + A centralized hub for basic metrics reporting. + This class is designed to provide a baseline exposure to the reporters. + + Context specific Analytics.lua objects should be created in sub-projects, + to report specific actions like chat interactions, or game page interactions. + + Analytics.lua and the reporters here in Common should serve to cover the + most common interactions. +]] + +local AnalyticsService = game:GetService("RbxAnalyticsService") +local Reporters = script.Parent.AnalyticsReporters + +local DiagReporter = require(Reporters.Diag) +local EventStreamReporter = require(Reporters.EventStream) +local GoogleAnalyticsReporter = require(Reporters.GoogleAnalytics) +local InfluxDbReporter = require(Reporters.Influx) + + +local Analytics = {} +Analytics.__index = Analytics + +-- reportingService : (Service, optional) an object that exposes the same functions as AnalyticsService +function Analytics.new(reportingService) + if not reportingService then + reportingService = AnalyticsService + end + + -- All public reporting functions are exposed by the objects defined in the properties + local self = {} + self.Diag = DiagReporter.new(reportingService) + self.EventStream = EventStreamReporter.new(reportingService) + self.GoogleAnalytics = GoogleAnalyticsReporter.new(reportingService) + self.InfluxDb = InfluxDbReporter.new(reportingService) + + setmetatable(self, Analytics) + + return self +end + +function Analytics.mock() + -- create a reporting service that does not fire any requests out to the world + local fakeReportingService = {} + function fakeReportingService.ReportCounter() end + function fakeReportingService.ReportInfluxSeries() end + function fakeReportingService.ReportStats() end + function fakeReportingService.SetRBXEvent() end + function fakeReportingService.SetRBXEventStream() end + function fakeReportingService.TrackEvent() end + function fakeReportingService.UpdateHeartbeatObject() end + + return Analytics.new(fakeReportingService) +end + +return Analytics diff --git a/Client2021/ExtraContent/LuaPackages/Analytics/Analytics.spec.lua b/Client2021/ExtraContent/LuaPackages/Analytics/Analytics.spec.lua new file mode 100644 index 0000000..794b160 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Analytics/Analytics.spec.lua @@ -0,0 +1,69 @@ +return function() + local Analytics = require(script.Parent.Analytics) + + describe("new()", function() + it("should properly construct a new object", function() + local na = Analytics.new() + expect(na).to.be.ok() + end) + + it("should accept a custom reporting service", function() + local fakeService = {} + local na = Analytics.new(fakeService) + expect(na).to.be.ok() + end) + + it("should have a reporter specifically for Diag", function() + local na = Analytics.new() + expect(na.Diag).to.be.ok() + end) + + it("should have a reporter specifically for RBXEventStream", function() + local na = Analytics.new() + expect(na.EventStream).to.be.ok() + end) + + it("should have a reporter specifically for Google Analytics", function() + local na = Analytics.new() + expect(na.GoogleAnalytics).to.be.ok() + end) + + it("should have a reporter specifically for Influx", function() + local na = Analytics.new() + expect(na.InfluxDb).to.be.ok() + end) + end) + + describe("mock()", function() + it("should properly construct a new object", function() + local ma = Analytics.mock() + expect(ma).to.be.ok() + expect(ma.Diag).to.be.ok() + expect(ma.EventStream).to.be.ok() + expect(ma.GoogleAnalytics).to.be.ok() + expect(ma.InfluxDb).to.be.ok() + end) + + it("should succeed for all function calls in Diag", function() + local ma = Analytics.mock() + ma.Diag:reportCounter("fakeCounter", 1) + ma.Diag:reportStats("fakeCategory", 1) + end) + + it("should succeed for all function call in EventStream", function() + local ma = Analytics.mock() + ma.EventStream:setRBXEvent("fakeContext", "fakeEventName") + ma.EventStream:setRBXEventStream("fakeContext", "fakeEventName") + end) + + it("should succeed for all function call in GoogleAnalytics", function() + local ma = Analytics.mock() + ma.GoogleAnalytics:trackEvent("fakeCategory", "fakeAction", "fakeLabel") + end) + + it("should succeed for all function call in Influx", function() + local ma = Analytics.mock() + ma.InfluxDb:reportSeries("fakeSeries", {}, 1) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Diag.lua b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Diag.lua new file mode 100644 index 0000000..b33b490 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Diag.lua @@ -0,0 +1,64 @@ +--[[ + Specialized analytics reporter for ephemeral counters. + Useful for tracking at-a-glance health of a feature. +]] + +local UserInputService = game:GetService("UserInputService") + +local Diag = {} +Diag.__index = Diag + +-- reportingService - (object) any object that defines the same functions for Diag as AnalyticsService +function Diag.new(reportingService) + local rsType = type(reportingService) + assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService") + + local self = { + _reporter = reportingService, + _isEnabled = true, + } + setmetatable(self, Diag) + + return self +end + +-- isEnabled : (boolean) +function Diag:setEnabled(isEnabled) + assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean") + self._isEnabled = isEnabled +end + +-- counterName : (string) the name of the ephemeral counter to increment +-- amount : (int) the value to increment the counter by +function Diag:reportCounter(counterName, amount) + assert(type(counterName) == "string", "Expected counterName to be a string") + assert(type(amount) == "number", "Expected amount to be a number") + assert(self._isEnabled, "This reporting service is disabled") + + -- use special naming convention for Xbox counters + -- the call to GetPlatform is wrapped in a pcall() because the Testing Service + -- executes the scripts in the wrong authorization level + local platformID = Enum.Platform.None + pcall(function() + platformID = UserInputService:GetPlatform() + end) + if platformID == Enum.Platform.XBoxOne then + counterName = "Xbox-" .. tostring(counterName) + end + + -- the AnalyticsService should automatically handle batch reporting + self._reporter:ReportCounter(counterName, amount) +end + + +-- category : (string) the name of the statistics buffer which to append the data +-- value : (number) any type of numeric value +function Diag:reportStats(category, value) + assert(type(category) == "string", "Expected category to be a string") + assert(type(value) == "number", "Expected value to be a number") + assert(self._isEnabled, "This reporting service is disabled") + + self._reporter:ReportStats(category, value) +end + +return Diag diff --git a/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Diag.spec.lua b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Diag.spec.lua new file mode 100644 index 0000000..6dadff2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Diag.spec.lua @@ -0,0 +1,176 @@ +return function() + local Diag = require(script.Parent.Diag) + + local testCounterName = "testCounter" + local testCounterAmount = 1 + local testCategoryName = "testCategory" + local testCategoryValue = 98 + + local badTestCounterName = 5 + local badTestCounterAmount = "hello" + local badTestCategoryName = {} + local badTestCategoryValue = {} + + local DebugReportingService = {} + function DebugReportingService:ReportCounter(counterName, amount) + assert(counterName == testCounterName, "Unexpected value for counterName: " .. counterName) + assert(amount == testCounterAmount, "Unexpected value for amount: " .. amount) + end + function DebugReportingService:ReportStats(categoryName, value) + assert(categoryName == testCategoryName, "Unexpected value for category: " .. categoryName) + if value then + assert(value == testCategoryValue, "Unexpected value for value: " .. value) + end + end + + + describe("new()", function() + it("should construct with a Reporting Service", function() + local diag = Diag.new(DebugReportingService) + expect(diag).to.be.ok() + end) + + it("should throw an error to be constructed without a Reporting Service", function() + expect(function() + Diag.new(nil) + end).to.throw() + end) + end) + + describe("setEnabled()", function() + it("should succeed with valid input", function() + local diag = Diag.new(DebugReportingService) + diag:setEnabled(false) + diag:setEnabled(true) + end) + it("should disable the reporter", function() + local diag = Diag.new(DebugReportingService) + diag:setEnabled(false) + expect(function() + diag:reportCounter(testCounterName, testCounterAmount) + end).to.throw() + end) + end) + + describe("reportCounter()", function() + it("should work when appropriately enabled / disabled", function() + local diag = Diag.new(DebugReportingService) + + expect(function() + diag:setEnabled(false) + diag:reportCounter(testCounterName, testCounterAmount) + end).to.throw() + + diag:setEnabled(true) + diag:reportCounter(testCounterName, testCounterAmount) + end) + + it("should succeed with valid input", function() + local diag = Diag.new(DebugReportingService) + diag:reportCounter(testCounterName, testCounterAmount) + end) + + it("should throw an error with invalid input for the counter name", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(badTestCounterName, testCounterAmount) + end).to.throw() + end) + + it("should throw an error with invalid input for the amount", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(testCounterName, badTestCounterAmount) + end).to.throw() + end) + + it("should throw an error with completely invalid input", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(badTestCounterName, badTestCounterAmount) + end).to.throw() + end) + + it("should throw an error if it is missing a counter name", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(nil, testCounterAmount) + end).to.throw() + end) + + it("should throw an error if it is missing an amount", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(testCounterName, nil) + end).to.throw() + end) + + it("should throw an error if it is missing any input", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportCounter(nil, nil) + end).to.throw() + end) + end) + + describe("reportStats()", function() + it("should work when appropriately enabled / disabled", function() + local diag = Diag.new(DebugReportingService) + + expect(function() + diag:setEnabled(false) + diag:reportStats(testCategoryName, testCategoryValue) + end).to.throw() + + diag:setEnabled(true) + diag:reportStats(testCategoryName, testCategoryValue) + end) + + it("should succeed with valid input", function() + local diag = Diag.new(DebugReportingService) + diag:reportStats(testCategoryName, testCategoryValue) + end) + + it("should throw an error with invalid input for the category name", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(badTestCategoryName, testCategoryValue) + end).to.throw() + end) + + it("should throw an error with invalid input for the value", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(testCategoryName, badTestCategoryValue) + end).to.throw() + end) + + it("should throw an error with completely invalid input", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(badTestCategoryName, badTestCategoryValue) + end).to.throw() + end) + + it("should throw an error if it is missing a category name", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(nil, testCategoryValue) + end).to.throw() + end) + + it("should throw an error if it is missing a value", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(testCategoryName, nil) + end).to.throw() + end) + + it("should throw an error if it is missing any input", function() + local diag = Diag.new(DebugReportingService) + expect(function() + diag:reportStats(nil, nil) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/EventStream.lua b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/EventStream.lua new file mode 100644 index 0000000..5a32390 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/EventStream.lua @@ -0,0 +1,154 @@ +--[[ + Specialized reporter for RBX Event Ingest data. + Useful for tracking explicit user interactions with screens and guis. +]] + +local UserInputService = game:GetService("UserInputService") + +local function getPlatformTarget() + local platformTarget = "unknownLua" + local platformEnum = Enum.Platform.None + + -- the call to GetPlatform is wrapped in a pcall() because the Testing Service + -- executes the scripts in the wrong authorization level + pcall(function() + platformEnum = UserInputService:GetPlatform() + end) + + -- bucket the platform based on consumer platform + local isDesktopClient = (platformEnum == Enum.Platform.Windows) or (platformEnum == Enum.Platform.OSX) + + local isMobileClient = (platformEnum == Enum.Platform.IOS) or (platformEnum == Enum.Platform.Android) + isMobileClient = isMobileClient or (platformEnum == Enum.Platform.UWP) + + local isConsole = (platformEnum == Enum.Platform.XBox360) or (platformEnum == Enum.Platform.XBoxOne) + isConsole = isConsole or (platformEnum == Enum.Platform.PS3) or (platformEnum == Enum.Platform.PS4) + isConsole = isConsole or (platformEnum == Enum.Platform.WiiU) + + -- assign a target based on the form factor + if isDesktopClient then + platformTarget = "client" + elseif isMobileClient then + platformTarget = "mobile" + elseif isConsole then + platformTarget = "console" + else + -- if we don't have a name for the form factor, report it here so that we can eventually track it down + platformTarget = platformTarget .. tostring(platformEnum) + end + + + return platformTarget +end + + +local EventStream = {} +EventStream.__index = EventStream + +-- reportingService - (object) any object that defines the same functions for Event Stream as AnalyticsService +function EventStream.new(reportingService) + local rsType = type(reportingService) + assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService") + + local self = { + _reporter = reportingService, + _isEnabled = true, + } + setmetatable(self, EventStream) + + return self +end + +-- isEnabled : (boolean) +function EventStream:setEnabled(isEnabled) + assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean") + self._isEnabled = isEnabled +end + +-- eventContext : (string) the location or context in which the event is occurring. +-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example. +-- additionalArgs : (optional, map) table for additional information to appear in the event stream. +function EventStream:setRBXEvent(eventContext, eventName, additionalArgs) + local target = getPlatformTarget() + additionalArgs = additionalArgs or {} + + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(eventName) == "string", "Expected eventName to be a string") + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(self._isEnabled, "This reporting service is disabled") + + -- This function fires reports to the server right away + self._reporter:SetRBXEvent(target, eventContext, eventName, additionalArgs) +end + + +-- eventContext : (string) the location or context in which the event is occurring. +-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example. +-- additionalArgs : (optional, map) map for extra keys to appear in the event stream. +function EventStream:setRBXEventStream(eventContext, eventName, additionalArgs) + local target = getPlatformTarget() + additionalArgs = additionalArgs or {} + + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(eventName) == "string", "Expected eventName to be a string") + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(self._isEnabled, "This reporting service is disabled") + + -- this function sends reports to the server in batches, not real-time + self._reporter:SetRBXEventStream(target, eventContext, eventName, additionalArgs) +end + + +function EventStream:releaseRBXEventStream() + assert(self._isEnabled, "This reporting service is disabled") + + self._reporter:ReleaseRBXEventStream(getPlatformTarget()) +end + + +-- eventContext : (string) the location or context in which the event is occurring. +-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example. +-- additionalArgs : (optional, map) map for extra keys to appear in the event stream. +function EventStream:sendEventDeferred(eventContext, eventName, additionalArgs) + local target = getPlatformTarget() + additionalArgs = additionalArgs or {} + + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(eventName) == "string", "Expected eventName to be a string") + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(self._isEnabled, "This reporting service is disabled") + + -- this function sends reports to the server in batches, not real-time + self._reporter:SendEventDeferred(target, eventContext, eventName, additionalArgs) +end + + +-- eventContext : (string) the location or context in which the event is occurring. +-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example. +-- additionalArgs : (optional, map) map for extra keys to appear in the event stream. +function EventStream:sendEventImmediately(eventContext, eventName, additionalArgs) + local target = getPlatformTarget() + additionalArgs = additionalArgs or {} + + assert(type(eventContext) == "string", "Expected eventContext to be a string") + assert(type(eventName) == "string", "Expected eventName to be a string") + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(self._isEnabled, "This reporting service is disabled") + + -- this function sends reports to the server in batches, not real-time + self._reporter:SendEventImmediately(target, eventContext, eventName, additionalArgs) +end + + +-- additionalArgs : (optional, map) table for extra keys to appear in the event stream. +function EventStream:updateHeartbeatObject(additionalArgs) + additionalArgs = additionalArgs or {} + + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(self._isEnabled, "This reporting service is disabled") + + self._reporter:UpdateHeartbeatObject(additionalArgs) +end + + +return EventStream diff --git a/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/EventStream.spec.lua b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/EventStream.spec.lua new file mode 100644 index 0000000..597b6fc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/EventStream.spec.lua @@ -0,0 +1,393 @@ +return function() + + local EventStream = require(script.Parent.EventStream) + + local testArgs = { + testKey = "testValue" + } + local testContext = "testContext" + local testEvent = "testEventName" + local badTestArgs = "hello" + local badTestContext = {} + local badTestEvent = {} + + + local function isTableEqual(table1, table2) + if table1 == table2 then + return true + end + + if type(table1) ~= "table" then + return false + end + + if type(table2) ~= "table" then + return false + end + + for key, _ in pairs(table1) do + if table1[key] ~= table2[key] then + return false + end + end + for key, _ in pairs(table2) do + if table2[key] ~= table1[key] then + return false + end + end + + return true + end + + local function createDebugReportingService() + local function validateInputs(eventTarget, eventContext, eventName, additionalArgs) + assert(eventTarget, "no value found for eventTarget") + assert(eventContext == testContext, "unexpected value for eventContext : " .. eventContext) + assert(eventName == testEvent, "unexpected value for eventName : " .. eventName) + if additionalArgs and not isTableEqual(additionalArgs, {}) then + assert(isTableEqual(additionalArgs, testArgs), "unexpected value for additionalArgs") + end + end + + local DebugReportingService = {} + function DebugReportingService:SetRBXEvent(eventTarget, eventContext, eventName, additionalArgs) + validateInputs(eventTarget, eventContext, eventName, additionalArgs) + end + function DebugReportingService:SetRBXEventStream(eventTarget, eventContext, eventName, additionalArgs) + validateInputs(eventTarget, eventContext, eventName, additionalArgs) + end + function DebugReportingService:SendEventImmediately(eventTarget, eventContext, eventName, additionalArgs) + validateInputs(eventTarget, eventContext, eventName, additionalArgs) + end + function DebugReportingService:SendEventDeferred(eventTarget, eventContext, eventName, additionalArgs) + validateInputs(eventTarget, eventContext, eventName, additionalArgs) + end + function DebugReportingService:ReleaseRBXEventStream(eventTarget) + assert(eventTarget, "no value found for eventTarget") + end + function DebugReportingService:UpdateHeartbeatObject(additionalArgs) + if additionalArgs and not isTableEqual(additionalArgs, {}) then + assert(isTableEqual(additionalArgs, testArgs), "unexpected value for additionalArgs") + end + end + + return DebugReportingService + end + + + describe("new()", function() + it("should construct with a Reporting Service", function() + local es = EventStream.new(createDebugReportingService()) + expect(es).to.be.ok() + end) + + it("should not allow construction without a Reporting Service", function() + expect(function() + EventStream.new(nil) + end).to.throw() + end) + end) + + describe("setEnabled()", function() + it("should succeed with valid input", function() + local reporter = EventStream.new(createDebugReportingService()) + reporter:setEnabled(false) + reporter:setEnabled(true) + end) + it("should disable the reporter", function() + local reporter = EventStream.new(createDebugReportingService()) + reporter:setEnabled(false) + expect(function() + reporter:updateHeartbeatObject() + end).to.throw() + end) + end) + + describe("setRBXEvent()", function() + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:setRBXEvent(testContext, testEvent, testArgs) + end) + + it("should work when appropriately enabled / disabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:setRBXEvent(testContext, testEvent, testArgs) + end).to.throw() + + es:setEnabled(true) + es:setRBXEvent(testContext, testEvent, testArgs) + end) + + it("should throw an error if it is missing a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEvent(nil, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is missing an event name", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEvent(testContext, nil, testArgs) + end).to.throw() + end) + + it("should succeed even if there aren't any additional args", function() + local es = EventStream.new(createDebugReportingService()) + es:setRBXEvent(testContext, testEvent, nil) + end) + + it("should throw an error if it is given bad input for a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEvent(badTestContext, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for a event", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEvent(testContext, badTestEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for additionalArgs", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEvent(testContext, testEvent, badTestArgs) + end).to.throw() + end) + end) + + describe("setRBXEventStream()", function() + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:setRBXEventStream(testContext, testEvent, testArgs) + end) + + it("should work when appropriately enabled / disabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:setRBXEventStream(testContext, testEvent, testArgs) + end).to.throw() + + es:setEnabled(true) + es:setRBXEventStream(testContext, testEvent, testArgs) + end) + + it("should throw an error if it is missing a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEventStream(nil, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is missing an event name", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEventStream(testContext, nil, testArgs) + end).to.throw() + end) + + it("should succeed even if there aren't any additional args", function() + local es = EventStream.new(createDebugReportingService()) + es:setRBXEventStream(testContext, testEvent, nil) + end) + + it("should throw an error if it is given bad input for a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEventStream(badTestContext, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for a event", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEventStream(testContext, badTestEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for additionalArgs", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:setRBXEventStream(testContext, testEvent, badTestArgs) + end).to.throw() + end) + end) + + describe("sendEventImmediately()", function() + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:sendEventImmediately(testContext, testEvent, testArgs) + end) + + it("should call without a fuss when enabled and throw when disabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:sendEventImmediately(testContext, testEvent, testArgs) + end).to.throw() + + es:setEnabled(true) + es:sendEventImmediately(testContext, testEvent, testArgs) + end) + + it("should throw an error if it is missing a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventImmediately(nil, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is missing an event name", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventImmediately(testContext, nil, testArgs) + end).to.throw() + end) + + it("should succeed even if there aren't any additional args", function() + local es = EventStream.new(createDebugReportingService()) + es:sendEventImmediately(testContext, testEvent, nil) + end) + + it("should throw an error if it is given bad input for a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventImmediately(badTestContext, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for a event", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventImmediately(testContext, badTestEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for additionalArgs", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventImmediately(testContext, testEvent, badTestArgs) + end).to.throw() + end) + end) + + describe("sendEventDeferred()", function() + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:sendEventDeferred(testContext, testEvent, testArgs) + end) + + it("should call without a fuss when enabled and throw when disabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:sendEventDeferred(testContext, testEvent, testArgs) + end).to.throw() + + es:setEnabled(true) + es:sendEventDeferred(testContext, testEvent, testArgs) + end) + + it("should throw an error if it is missing a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventDeferred(nil, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is missing an event name", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventDeferred(testContext, nil, testArgs) + end).to.throw() + end) + + it("should succeed even if there aren't any additional args", function() + local es = EventStream.new(createDebugReportingService()) + es:sendEventDeferred(testContext, testEvent, nil) + end) + + it("should throw an error if it is given bad input for a context", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventDeferred(badTestContext, testEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for a event", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventDeferred(testContext, badTestEvent, testArgs) + end).to.throw() + end) + + it("should throw an error if it is given bad input for additionalArgs", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:sendEventDeferred(testContext, testEvent, badTestArgs) + end).to.throw() + end) + end) + + describe("releaseRBXEventStream()", function() + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:releaseRBXEventStream() + end) + + it("should throw when disabled and succeed when enabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:releaseRBXEventStream() + end).to.throw() + + es:setEnabled(true) + es:releaseRBXEventStream() + end) + end) + + describe("updateHeartbeatObject()", function() + it("should work when appropriately enabled / disabled", function() + local es = EventStream.new(createDebugReportingService()) + + expect(function() + es:setEnabled(false) + es:updateHeartbeatObject(testArgs) + end).to.throw() + + expect(function() + es:setEnabled(true) + es:updateHeartbeatObject(testArgs) + end).never.to.throw() + end) + + it("should succeed with valid input", function() + local es = EventStream.new(createDebugReportingService()) + es:updateHeartbeatObject(testArgs) + end) + + it("should succeed even if there aren't any additional args", function() + local es = EventStream.new(createDebugReportingService()) + es:updateHeartbeatObject(nil) + end) + + it("should throw an error with invalid input", function() + local es = EventStream.new(createDebugReportingService()) + expect(function() + es:updateHeartbeatObject(badTestArgs) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/GoogleAnalytics.lua b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/GoogleAnalytics.lua new file mode 100644 index 0000000..91e979c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/GoogleAnalytics.lua @@ -0,0 +1,51 @@ +--[[ + Specialized reporter for sending data to GA. + Useful for creating a breadcrumb trail of user interactions. + + Events in GA are aggregated and organized in order by category, action, label. +]] + +local GoogleAnalytics = {} +GoogleAnalytics.__index = GoogleAnalytics + +-- reportingService : (table or userdata) any object that defines the same functions for GA as AnalyticsService +function GoogleAnalytics.new(reportingService) + local rsType = type(reportingService) + assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService") + + local self = { + _reporter = reportingService, + _isEnabled = true, + } + setmetatable(self, GoogleAnalytics) + + return self +end + +-- isEnabled : (boolean) +function GoogleAnalytics:setEnabled(isEnabled) + assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean") + self._isEnabled = isEnabled +end + +-- category : (string) the most generic category by which to organize data, ex) LuaApp, Errors, GameSettings, etc. +-- action : (string) a specific event to record, ex) ButtonPressed, GameExit +-- label : (string, optional) a detail to differentiate one action over another, ex) LoginButton, Exit Code 0 +-- value : (integer, optional) the number of times this event has occurred +function GoogleAnalytics:trackEvent(category, action, label, value) + assert(type(category) == "string", "Expected category to be a string") + assert(type(action) == "string", "Expected action to be a string") + if label then + assert(type(label) == "string", "Expected label to be a string") + end + if value then + assert(type(value) == "number", "Expected value to be a number") + assert(value >= 0, "Expected value must not be a negative value") + end + assert(self._isEnabled, "This reporting service is disabled") + + self._reporter:TrackEvent(category, action, label, value) +end + + +return GoogleAnalytics diff --git a/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/GoogleAnalytics.spec.lua b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/GoogleAnalytics.spec.lua new file mode 100644 index 0000000..ede7486 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/GoogleAnalytics.spec.lua @@ -0,0 +1,140 @@ +return function() + local GoogleAnalytics = require(script.Parent.GoogleAnalytics) + + local testCategory = "testCategory" + local testAction = "testAction" + local testLabel = "testLabel" + local testValue = 6 + local badTestCategory = 13141 + local badTestAction = {} + local badTestLabel = {} + local badTestValue = "heyo" + + + local DebugReportingService = {} + function DebugReportingService:TrackEvent(category, action, label, value) + if category ~= testCategory then + error("unexpected value for category: " .. category) + end + if action ~= testAction then + error("unexpected value for action: " .. action) + end + if label then + if label ~= testLabel then + error("unexpected value for label: " .. label) + end + end + if value then + if value ~= testValue then + error("unexpected value for value: " .. value) + end + end + end + + + describe("new()", function() + it("should construct with a Reporting Service and Logging Service", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(ga).to.be.ok() + end) + + it("should fail to be constructed without a Reporting Service", function() + expect(function() + GoogleAnalytics.new(nil) + end).to.throw() + end) + end) + + describe("setEnabled()", function() + it("should succeed with valid input", function() + local reporter = GoogleAnalytics.new(DebugReportingService) + reporter:setEnabled(false) + reporter:setEnabled(true) + end) + it("should disable the reporter", function() + local reporter = GoogleAnalytics.new(DebugReportingService) + reporter:setEnabled(false) + expect(function() + reporter:trackEvent(testCategory, testAction, testLabel, testValue) + end).to.throw() + end) + end) + + describe("trackEvent()", function() + it("should work when appropriately enabled / disabled", function() + local ga = GoogleAnalytics.new(DebugReportingService) + + expect(function() + ga:setEnabled(false) + ga:trackEvent(testCategory, testAction, testLabel) + end).to.throw() + + ga:setEnabled(true) + ga:trackEvent(testCategory, testAction, testLabel) + end) + + it("should succeed with valid input", function() + local ga = GoogleAnalytics.new(DebugReportingService) + ga:trackEvent(testCategory, testAction, testLabel, testValue) + end) + + it("should throw an error if it is missing a category", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(nil, testAction, testLabel, testValue) + end).to.throw() + end) + + it("should throw an error if it is missing a testAction", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(testCategory, nil, testLabel, testValue) + end).to.throw() + end) + + it("should not throw an error if it is missing a label", function() + local ga = GoogleAnalytics.new(DebugReportingService) + ga:trackEvent(testCategory, testAction, nil, testValue) + end) + + it("should not throw an error if it is missing a value", function() + local ga = GoogleAnalytics.new(DebugReportingService) + ga:trackEvent(testCategory, testAction, testLabel) + end) + + it("should throw an error if it is given invalid input for category", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(badTestCategory, testAction, testLabel, testValue) + end).to.throw() + end) + + it("should throw an error if it is given invalid input for action", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(testCategory, badTestAction, testLabel, testValue) + end).to.throw() + end) + + it("should throw an error if it is given invalid input for label", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(testCategory, testAction, badTestLabel, testValue) + end).to.throw() + end) + + it("should throw an error if it is given invalid input for value", function() + local ga = GoogleAnalytics.new(DebugReportingService) + expect(function() + ga:trackEvent(testCategory, testAction, testLabel, badTestValue) + end).to.throw() + + expect(function() + ga:trackEvent(testCategory, testAction, testLabel, -1) + end).to.throw() + end) + end) + + + +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Influx.lua b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Influx.lua new file mode 100644 index 0000000..89f287e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Influx.lua @@ -0,0 +1,47 @@ +--[[ + Specialized reporter for sending data to InfluxDb. + Useful for very detailed information about specific errors. + + Due to how Influx sends data, it is disallowed on XBox. + ~Kyler Mulherin (9/12/2017) +]] + +local Influx = {} +Influx.__index = Influx + +-- reportingService - (object) any object that defines the same functions for Influx as AnalyticsService +function Influx.new(reportingService) + local rsType = type(reportingService) + assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService") + + local self = { + _reporter = reportingService, + _isEnabled = true, + } + setmetatable(self, Influx) + + return self +end + +-- isEnabled : (boolean) +function Influx:setEnabled(isEnabled) + assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean") + self._isEnabled = isEnabled +end + +-- seriesName : (string) the name of the series as it will appear in InfluxDb +-- additionalArgs : (map) extra key/values to appear in each series +-- throttlingPercent : (int) the chance to actually report this series +function Influx:reportSeries(seriesName, additionalArgs, throttlingPercent) + additionalArgs = additionalArgs or {} + + assert(type(seriesName) == "string", "Expected seriesName to be a string") + assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table") + assert(type(throttlingPercent) == "number", "Expected throttlingPercent to be a number") + assert(throttlingPercent >= 0 and throttlingPercent <= 10000, "throttlingPercent must be between 0 - 10,000") + assert(self._isEnabled, "This reporting service is disabled") + + self._reporter:ReportInfluxSeries(seriesName, additionalArgs, throttlingPercent) +end + +return Influx diff --git a/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Influx.spec.lua b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Influx.spec.lua new file mode 100644 index 0000000..8d417ba --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Analytics/AnalyticsReporters/Influx.spec.lua @@ -0,0 +1,165 @@ +return function() + local Influx = require(script.Parent.Influx) + + local testSeriesName = "testSeries" + local testArgs = { + testKey = "testValue" + } + local testThrottlingPercentage = 1000 + + local badTestSeriesName = 114 + local badTestArgs = "someString" + local badThrottlingPercentage1 = -15 + local badThrottlingPercentage2 = 150000 + local badThrottlingPercentage3 = "a lot" + + + local function isTableEqual(table1, table2) + if table1 == table2 then + return true + end + + if type(table2) ~= "table" then + return false + end + + for key, _ in pairs(table1) do + if table1[key] ~= table2[key] then + return false + end + end + for key, _ in pairs(table2) do + if table2[key] ~= table1[key] then + return false + end + end + + return true + end + + + local DebugReportingService = {} + function DebugReportingService:ReportInfluxSeries(seriesName, additionalArgs, throttlingPercentage) + if seriesName ~= seriesName then + error("Unexpected value for seriesName: " .. seriesName) + end + if throttlingPercentage ~= testThrottlingPercentage then + error("Unexpected value for throttlingPercentage: " .. throttlingPercentage) + end + if isTableEqual(additionalArgs, {}) == false then + if isTableEqual(additionalArgs, testArgs) == false then + error("Unexpected value for additionalArgs") + end + end + end + + + describe("new()", function() + it("should construct with a Reporting Service", function() + local influx = Influx.new(DebugReportingService) + expect(influx).to.be.ok() + end) + + it("should fail to be constructed without a Reporting Service", function() + expect(function() + Influx.new(nil) + end).to.throw() + end) + end) + + describe("setEnabled()", function() + it("should succeed with valid input", function() + local influx = Influx.new(DebugReportingService) + influx:setEnabled(false) + influx:setEnabled(true) + end) + it("should disable the reporter", function() + local influx = Influx.new(DebugReportingService) + influx:setEnabled(false) + expect(function() + influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage) + end).to.throw() + end) + end) + + describe("reportSeries()", function() + it("should work when appropriately enabled / disabled", function() + local influx = Influx.new(DebugReportingService) + + expect(function() + influx:setEnabled(false) + influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage) + end).to.throw() + + + influx:setEnabled(true) + influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage) + end) + + it("should succeed with valid input", function() + local influx = Influx.new(DebugReportingService) + influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage) + end) + + it("should succeed even if it is missing any additionalArgs", function() + local influx = Influx.new(DebugReportingService) + influx:reportSeries(testSeriesName, nil, testThrottlingPercentage) + end) + + it("should throw an error with invalid input for the seriesName", function() + local influx = Influx.new(DebugReportingService) + expect(function() + influx:reportSeries(badTestSeriesName, testArgs, testThrottlingPercentage) + end).to.throw() + end) + + it("should throw an error with invalid input for the throttlingPercentage - out of range - below zero", function() + expect(function() + local influx = Influx.new(DebugReportingService) + influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage1) + end).to.throw() + end) + + it("should throw an error with invalid input for the throttlingPercentage - out of range - above cap", function() + expect(function() + local influx = Influx.new(DebugReportingService) + influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage2) + end).to.throw() + end) + + it("should throw an error with invalid input for the throttlingPercentage - bad type", function() + expect(function() + local influx = Influx.new(DebugReportingService) + influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage3) + end).to.throw() + end) + + it("should throw an error with completely invalid input", function() + local influx = Influx.new(DebugReportingService) + expect(function() + influx:reportSeries(badTestSeriesName, badTestArgs, badThrottlingPercentage1) + end).to.throw() + end) + + it("should throw an error if it is missing a seriesName", function() + local influx = Influx.new(DebugReportingService) + expect(function() + influx:reportSeries(nil, testArgs, testThrottlingPercentage) + end).to.throw() + end) + + it("should throw an error if it is missing a throttlingPercentage", function() + local influx = Influx.new(DebugReportingService) + expect(function() + influx:reportSeries(testSeriesName, testArgs, nil) + end).to.throw() + end) + + it("should throw an error if it is missing any input", function() + local influx = Influx.new(DebugReportingService) + expect(function() + influx:reportSeries(nil, nil, nil) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/.robloxrc new file mode 100644 index 0000000..e12a7d8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/.robloxrc @@ -0,0 +1,7 @@ +{ + "lint": { + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/AnalyticsReporters/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/AnalyticsReporters/.robloxrc new file mode 100644 index 0000000..e721482 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/AnalyticsReporters/.robloxrc @@ -0,0 +1,9 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalShadow": "fatal", + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/AnalyticsReporters/Diag.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/AnalyticsReporters/Diag.lua new file mode 100644 index 0000000..ea43e3b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/AnalyticsReporters/Diag.lua @@ -0,0 +1,8 @@ +----------------------------------------------------------------------------- +--- --- +--- Under Migration to CorePackages --- +--- --- +--- Please put your changes in Analytics. --- +----------------------------------------------------------------------------- +local CorePackages = game:GetService("CorePackages") +return require(CorePackages.Analytics.AnalyticsReporters.Diag) diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Action.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Action.lua new file mode 100644 index 0000000..d3d1ff4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Action.lua @@ -0,0 +1,68 @@ +--[[ + A helper function to define a Rodux action creator with an associated name. + + Normally when creating a Rodux action, you can just create a function: + + return function(value) + return { + type = "MyAction", + value = value, + } + end + + And then when you check for it in your reducer, you either use a constant, + or type out the string name: + + if action.type == "MyAction" then + -- change some state + end + + Typos here are a remarkably common bug. We also have the issue that there's + no link between reducers and the actions that they respond to! + + `Action` (this helper) provides a utility that makes this a bit cleaner. + + Instead, define your Rodux action like this: + + return Action("MyAction", function(value) + return { + value = value, + } + end) + + We no longer need to add the `type` field manually. + + Additionally, the returned action creator now has a 'name' property that can + be checked by your reducer: + + local MyAction = require(Reducers.MyAction) + + ... + + if action.type == MyAction.name then + -- change some state! + end + + Now we have a clear link between our reducers and the actions they use, and + if we ever typo a name, we'll get a warning in LuaCheck as well as an error + at runtime! +]] + +return function(name, fn) + assert(type(name) == "string", "A name must be provided to create an Action") + assert(type(fn) == "function", "A function must be provided to create an Action") + + return setmetatable({ + name = name, + }, { + __call = function(self, ...) + local result = fn(...) + + assert(type(result) == "table", "An action must return a table") + + result.type = name + + return result + end + }) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Action.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Action.spec.lua new file mode 100644 index 0000000..799c2b3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Action.spec.lua @@ -0,0 +1,85 @@ +return function() + local Action = require(script.Parent.Action) + + it("should return a table", function() + local action = Action("foo", function() + return {} + end) + + expect(action).to.be.a("table") + end) + + it("should set the name of the action", function() + local action = Action("foo", function() + return {} + end) + + expect(action.name).to.equal("foo") + end) + + it("should be able to be called as a function", function() + local action = Action("foo", function() + return {} + end) + + expect(action).never.to.throw() + end) + + it("should return a table when called as a function", function() + local action = Action("foo", function() + return {} + end) + + expect(action()).to.be.a("table") + end) + + it("should set the type of the action", function() + local action = Action("foo", function() + return {} + end) + + expect(action().type).to.equal("foo") + end) + + it("should set values", function() + local action = Action("foo", function(value) + return { + value = value + } + end) + + expect(action(100).value).to.equal(100) + end) + + it("should throw when passed a function", function() + local action = Action("foo", function() + return function() end + end) + + expect(action).to.throw() + end) + + it("should throw with a invalid name", function() + expect(function() + Action(nil, function() + return {} + end) + end).to.throw() + + expect(function() + Action(100, function() + return {} + end) + end).to.throw() + end) + + it("should throw when passed a invalid function", function() + expect(function() + Action("foo", nil) + end).to.throw() + + expect(function() + Action("foo", {}) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Color.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Color.lua new file mode 100644 index 0000000..5845ab9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Color.lua @@ -0,0 +1,19 @@ +local Color = {} + +function Color.RgbFromHex(hexColor) + assert(hexColor >= 0 and hexColor <= 0xffffff, "RgbFromHex: Out of range") + + local b = hexColor % 256 + hexColor = (hexColor - b) / 256 + local g = hexColor % 256 + hexColor = (hexColor - g) / 256 + local r = hexColor + + return r, g, b +end + +function Color.Color3FromHex(hexColor) + return Color3.fromRGB(Color.RgbFromHex(hexColor)) +end + +return Color \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Color.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Color.spec.lua new file mode 100644 index 0000000..af7c914 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Color.spec.lua @@ -0,0 +1,33 @@ +return function() + local Color = require(script.Parent.Color) + + describe("RgbFromHex", function() + it("should convert a hex color to rgb correctly", function() + local r, g, b = Color.RgbFromHex(0x232527) + expect(r).to.equal(35) + expect(g).to.equal(37) + expect(b).to.equal(39) + + r, g, b = Color.RgbFromHex(0x0) + expect(r).to.equal(0) + expect(g).to.equal(0) + expect(b).to.equal(0) + + r, g, b = Color.RgbFromHex(0xffffff) + expect(r).to.equal(255) + expect(g).to.equal(255) + expect(b).to.equal(255) + end) + + + it("should assert if given a hex color out of range", function() + expect(function() + Color.RgbFromHex(-1) + end).to.throw() + + expect(function() + Color.RgbFromHex(0x1000000) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Functional.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Functional.lua new file mode 100644 index 0000000..a40c8ad --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Functional.lua @@ -0,0 +1,131 @@ +--[[ + Provides an implementation of functional programming primitives. +]] + +local Functional = {} + +--[[ + Create a copy of a list with only values for which `callback` returns true +]] +function Functional.Filter(list, callback) + local new = {} + + for key = 1, #list do + local value = list[key] + if callback(value, key) then + table.insert(new, value) + end + end + + return new +end + +--[[ + Create a copy of a list where each value is transformed by `callback` +]] +function Functional.Map(list, callback) + local new = {} + + for key = 1, #list do + new[key] = callback(list[key], key) + end + + return new +end + +--[[ + Identical to Map, except that the result will be reversed. +]] +function Functional.MapReverse(list, callback) + local new = {} + + for key = #list, 1, -1 do + new[key] = callback(list[key], key) + end + + return new +end + +--[[ + Create a copy of a list doing a combination filter and map. + + If callback returns nil for any item, it is considered filtered from the + list. Any other value is considered the result of the 'map' operation. +]] +function Functional.FilterMap(list, callback) + local new = {} + + for key = 1, #list do + local value = list[key] + local result = callback(value, key) + + if result ~= nil then + table.insert(new, result) + end + end + + return new +end + +--[[ + Performs a left-fold of the list with the given initial value and callback. +]] +function Functional.Fold(list, initial, callback) + local accum = initial + + for key = 1, #list do + accum = callback(accum, list[key], key) + end + + return accum +end + +--[[ + Performs a fold over the entries in the given dictionary. +]] +function Functional.FoldDictionary(dictionary, initial, callback) + local accum = initial + + for key, value in pairs(dictionary) do + accum = callback(accum, key, value) + end + + return accum +end + +--[[ + Returns a list that contains at most `count` values from the given list. +]] +function Functional.Take(list, count, startingIndex) + startingIndex = startingIndex or 1 + + local maxIndex = count + (startingIndex - 1) + if maxIndex > #list then + maxIndex = #list + end + + local new = {} + + for i = startingIndex, maxIndex do + local value = list[i] + local newIndex = i - (startingIndex - 1) + new[newIndex] = value + end + + return new +end + +--[[ + If the list contains the sought-after element, return its index, or nil otherwise. +]] +function Functional.Find(list, value) + for index, element in ipairs(list) do + if element == value then + return index + end + end + + return nil +end + +return Functional \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Functional.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Functional.spec.lua new file mode 100644 index 0000000..4ea87a4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Functional.spec.lua @@ -0,0 +1,218 @@ +return function() + local Functional = require(script.Parent.Functional) + + local function identity(...) + return ... + end + + local function add(a, b) + return a + b + end + + describe("Filter", function() + it("should copy lists correctly", function() + local listA = {1, 2, 3} + local listB = Functional.Filter(listA, function() + return true + end) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the filter predicate", function() + local listA = {1, 2, 3, 4, 5} + local listB = Functional.Filter(listA, function(value, key) + expect(value).to.equal(key) + + return value % 2 == 0 + end) + + expect(listB[1]).to.equal(2) + expect(listB[2]).to.equal(4) + end) + end) + + describe("Map", function() + it("should copy lists correctly using the identity function", function() + local listA = {1, 2, 3} + local listB = Functional.Map(listA, identity) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the map predicate", function() + local listA = {1, 2, 3} + local listB = Functional.Map(listA, function(value, key) + expect(value).to.equal(key) + + return value * 2 + end) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i] * 2) + end + end) + end) + + describe("MapReverse", function() + it("should copy lists correctly using the identity function", function() + local listA = {1, 2, 3} + local listB = Functional.MapReverse(listA, identity) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the map predicate", function() + local listA = {1, 2, 3} + local listB = Functional.MapReverse(listA, function(value, key) + expect(value).to.equal(key) + + return value * 2 + end) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i] * 2) + end + end) + + it("should iterate backwards", function() + local list = {1, 2, 3} + local nextKey = 3 + + Functional.MapReverse(list, function(value, key) + expect(value).to.equal(nextKey) + expect(key).to.equal(nextKey) + + nextKey = nextKey - 1 + end) + + expect(nextKey).to.equal(0) + end) + end) + + describe("FilterMap", function() + it("should copy truthy lists using the identity function", function() + local listA = {1, 2, 3} + local listB = Functional.FilterMap(listA, identity) + + expect(listB).never.to.equal(listA) + + for i = 1, #listB do + expect(listB[i]).to.equal(listA[i]) + end + end) + + it("should correctly use the filter-map predicate", function() + local listA = {1, 2, 3, 4, 5} + + -- Create a list containing only the odd numbers, and double those numbers + local listB = Functional.FilterMap(listA, function(value, key) + expect(value).to.equal(key) + + if value % 2 == 0 then + return nil + end + + return value * 2 + end) + + expect(listB[1]).to.equal(2) + expect(listB[2]).to.equal(6) + expect(listB[3]).to.equal(10) + end) + end) + + describe("Fold", function() + it("should left-fold lists", function() + local list = {1, 2, 3, 4, 5} + + local sum = Functional.Fold(list, 0, add) + + expect(sum).to.equal(15) + end) + end) + + describe("Take", function() + it("should take values from a list", function() + local a = {1, 2, 3} + local b = Functional.Take(a, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + end) + + it("should not take past the end of a list", function() + local a = {1, 2, 3} + local b = Functional.Take(a, 4) + + expect(#b).to.equal(3) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + end) + + it("should copy all values when taking past the end of a list", function() + local a = {1, 2, 3} + local b = Functional.Take(a, 4) + + expect(#b).to.equal(#a) + expect(a[1]).to.equal(b[1]) + expect(a[2]).to.equal(b[2]) + expect(a[3]).to.equal(b[3]) + end) + + it("should take values from a starting index when provided", function() + local a = {1, 2, 3, 4} + local b = Functional.Take(a, 2, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(2) + expect(b[2]).to.equal(3) + end) + + it("should not take past the end of a list when the starting index is provided", function() + local a = {1, 2, 3, 4} + local b = Functional.Take(a, 3, 3) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(3) + expect(b[2]).to.equal(4) + end) + end) + + describe("Find", function() + it("should return index of matched item", function() + local a = {"foo", "bar", "garply"} + local b = Functional.Find(a, "bar") + + expect(b).to.equal(2) + end) + + it("should find the first example in the case of duplicates", function() + local a = {"foo", "bar", "garply", "bar"} + local b = Functional.Find(a, "bar") + + expect(b).to.equal(2) + end) + + it("should return nil if item is not found", function() + local a = {"foo", "bar", "garply"} + local b = Functional.Find(a, "fleebledegoop") + + expect(b).to.equal(nil) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Immutable.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Immutable.lua new file mode 100644 index 0000000..a73d203 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Immutable.lua @@ -0,0 +1,141 @@ +--[[ + Provides functions for manipulating immutable data structures. +]] + +local Immutable = {} + +--[[ + Merges dictionary-like tables together. +]] +function Immutable.JoinDictionaries(...) + local result = {} + + for i = 1, select("#", ...) do + local dictionary = select(i, ...) + for key, value in pairs(dictionary) do + result[key] = value + end + end + + return result +end + +--[[ + Joins any number of lists together into a new list +]] +function Immutable.JoinLists(...) + local new = {} + + for listKey = 1, select("#", ...) do + local list = select(listKey, ...) + local len = #new + + for itemKey = 1, #list do + new[len + itemKey] = list[itemKey] + end + end + + return new +end + +--[[ + Creates a new copy of the dictionary and sets a value inside it. +]] +function Immutable.Set(dictionary, key, value) + local new = {} + + for key, value in pairs(dictionary) do + new[key] = value + end + + new[key] = value + + return new +end + +--[[ + Creates a new copy of the list with the given elements appended to it. +]] +function Immutable.Append(list, ...) + local new = {} + local len = #list + + for key = 1, len do + new[key] = list[key] + end + + for i = 1, select("#", ...) do + new[len + i] = select(i, ...) + end + + return new +end + +--[[ + Remove elements from a dictionary +]] +function Immutable.RemoveFromDictionary(dictionary, ...) + local result = {} + + for key, value in pairs(dictionary) do + local found = false + for listKey = 1, select("#", ...) do + if key == select(listKey, ...) then + found = true + break + end + end + if not found then + result[key] = value + end + end + + return result +end + +--[[ + Remove the given key from the list. +]] +function Immutable.RemoveFromList(list, removeIndex) + local new = {} + + for i = 1, #list do + if i ~= removeIndex then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Remove the range from the list starting from the index. +]] +function Immutable.RemoveRangeFromList(list, index, count) + local new = {} + + for i = 1, #list do + if i < index or i >= index + count then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Creates a new list that has no occurrences of the given value. +]] +function Immutable.RemoveValueFromList(list, removeValue) + local new = {} + + for i = 1, #list do + if list[i] ~= removeValue then + table.insert(new, list[i]) + end + end + + return new +end + +return Immutable diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Immutable.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Immutable.spec.lua new file mode 100644 index 0000000..961e1cc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Immutable.spec.lua @@ -0,0 +1,284 @@ +return function() + local Immutable = require(script.Parent.Immutable) + + describe("JoinDictionaries", function() + it("should preserve immutability", function() + local a = {} + local b = {} + + local c = Immutable.JoinDictionaries(a, b) + + expect(c).never.to.equal(a) + expect(c).never.to.equal(b) + end) + + it("should treat list-like values like dictionary values", function() + local a = { + [1] = 1, + [2] = 2, + [3] = 3 + } + + local b = { + [1] = 11, + [2] = 22 + } + + local c = Immutable.JoinDictionaries(a, b) + + expect(c[1]).to.equal(b[1]) + expect(c[2]).to.equal(b[2]) + expect(c[3]).to.equal(a[3]) + end) + + it("should merge dictionary values correctly", function() + local a = { + hello = "world", + foo = "bar" + } + + local b = { + foo = "baz", + tux = "penguin" + } + + local c = Immutable.JoinDictionaries(a, b) + + expect(c.hello).to.equal(a.hello) + expect(c.foo).to.equal(b.foo) + expect(c.tux).to.equal(b.tux) + end) + + it("should merge multiple dictionaries", function() + local a = { + foo = "yes" + } + + local b = { + bar = "yup" + } + + local c = { + baz = "sure" + } + + local d = Immutable.JoinDictionaries(a, b, c) + + expect(d.foo).to.equal(a.foo) + expect(d.bar).to.equal(b.bar) + expect(d.baz).to.equal(c.baz) + end) + end) + + describe("JoinLists", function() + it("should preserve immutability", function() + local a = {} + local b = {} + + local c = Immutable.JoinLists(a, b) + + expect(c).never.to.equal(a) + expect(c).never.to.equal(b) + end) + + it("should treat list-like values correctly", function() + local a = {1, 2, 3} + local b = {4, 5, 6} + + local c = Immutable.JoinLists(a, b) + + expect(#c).to.equal(6) + + for i = 1, #c do + expect(c[i]).to.equal(i) + end + end) + + it("should merge multiple lists", function() + local a = {1, 2} + local b = {3, 4} + local c = {5, 6} + + local d = Immutable.JoinLists(a, b, c) + + expect(#d).to.equal(6) + + for i = 1, #d do + expect(d[i]).to.equal(i) + end + end) + end) + + describe("Set", function() + it("should preserve immutability", function() + local a = {} + + local b = Immutable.Set(a, "foo", "bar") + + expect(b).never.to.equal(a) + end) + + it("should treat numeric keys normally", function() + local a = {1, 2, 3} + + local b = Immutable.Set(a, 2, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(4) + expect(b[3]).to.equal(3) + end) + + it("should overwrite dictionary-like keys", function() + local a = { + foo = "bar", + baz = "qux" + } + + local b = Immutable.Set(a, "foo", "hello there") + + expect(b.foo).to.equal("hello there") + expect(b.baz).to.equal(a.baz) + end) + end) + + describe("Append", function() + it("should preserve immutability", function() + local a = {} + + local b = Immutable.Append(a, "another happy landing") + + expect(b).never.to.equal(a) + end) + + it("should append values", function() + local a = {1, 2, 3} + local b = Immutable.Append(a, 4, 5) + + expect(#b).to.equal(5) + + for i = 1, #b do + expect(b[i]).to.equal(i) + end + end) + end) + + describe("RemoveFromDictionary", function() + it("should preserve immutability", function() + local a = { foo = "bar" } + + local b = Immutable.RemoveFromDictionary(a, "foo") + + expect(b).to.never.equal(a) + end) + + it("should remove fields from the dictionary", function() + local a = { + foo = "bar", + baz = "qux", + boof = "garply", + } + + local b = Immutable.RemoveFromDictionary(a, "foo", "boof") + + expect(b.foo).to.never.be.ok() + expect(b.baz).to.equal("qux") + expect(b.boof).to.never.be.ok() + end) + end) + + describe("RemoveFromList", function() + it("should preserve immutability", function() + local a = {1, 2, 3} + local b = Immutable.RemoveFromList(a, 2) + + expect(b).never.to.equal(a) + end) + + it("should remove elements from the list", function() + local a = {1, 2, 3} + local b = Immutable.RemoveFromList(a, 2) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + end) + + describe("RemoveRangeFromList", function() + it("should preserve immutability", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 1) + + expect(b).never.to.equal(a) + end) + + it("should remove elements properly from the list 1", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 1) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + + it("should remove elements properly from the list 2", function() + local a = {1, 2, 3, 4, 5, 6} + local b = Immutable.RemoveRangeFromList(a, 1, 4) + + expect(b[1]).to.equal(5) + expect(b[2]).to.equal(6) + expect(b[3]).never.to.be.ok() + end) + + it("should remove elements properly from the list 3", function() + local a = {1, 2, 3, 4, 5, 6} + local b = Immutable.RemoveRangeFromList(a, 2, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(6) + expect(b[3]).never.to.be.ok() + end) + + it("should remove elements properly from the list 4", function() + local a = {1, 2, 3, 4, 5, 6, 7} + local b = Immutable.RemoveRangeFromList(a, 4, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + expect(b[4]).never.to.be.ok() + end) + + it("should not remove any elements when count is 0 or less", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 0) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + + local c = Immutable.RemoveRangeFromList(a, 2, -1) + expect(c[1]).to.equal(1) + expect(c[2]).to.equal(2) + expect(c[3]).to.equal(3) + end) + end) + + describe("RemoveValueFromList", function() + it("should preserve immutability", function() + local a = {1, 1, 1} + local b = Immutable.RemoveValueFromList(a, 1) + + expect(b).never.to.equal(a) + end) + + it("should remove all elements from the list", function() + local a = {1, 2, 2, 3} + local b = Immutable.RemoveValueFromList(a, 2) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Signal.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Signal.lua new file mode 100644 index 0000000..438738a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Signal.lua @@ -0,0 +1,75 @@ +--[[ + A limited, simple implementation of a Signal. + + Handlers are fired in order, and (dis)connections are properly handled when + executing an event. + + Signal uses Immutable to avoid invalidating the 'Fire' loop iteration. +]] + +local Immutable = require(script.Parent.Immutable) + +local Signal = {} + +Signal.__index = Signal + +function Signal.new() + local self = { + _listeners = {} + } + + setmetatable(self, Signal) + + return self +end + +function Signal:connect(callback) + local listener = { + callback = callback, + isConnected = true, + } + self._listeners = Immutable.Append(self._listeners, listener) + + local function disconnect() + listener.isConnected = false + self._listeners = Immutable.RemoveValueFromList(self._listeners, listener) + end + + return { + Disconnect = function() + warn(string.format( + "Connection:Disconnect() has been deprecated, use Connection:disconnect()\n%s]", + debug.traceback() + )) + disconnect() + end, + disconnect = disconnect, + } +end + +function Signal:fire(...) + for _, listener in ipairs(self._listeners) do + if listener.isConnected then + listener.callback(...) + end + end +end + +function Signal:Connect(...) + warn(string.format( + "Signal:Connect() has been deprecated, use Signal:connect()\n%s]", + debug.traceback() + )) + return self:connect(...) +end + +function Signal:Fire(...) + warn(string.format( + "Signal:Fire() has been deprecated, use Signal:fire()\n%s]", + debug.traceback() + )) + self:fire(...) +end + + +return Signal \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Signal.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Signal.spec.lua new file mode 100644 index 0000000..f00f947 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Signal.spec.lua @@ -0,0 +1,114 @@ +return function() + local Signal = require(script.Parent.Signal) + + it("should construct from nothing", function() + local signal = Signal.new() + + expect(signal).to.be.ok() + end) + + it("should fire connected callbacks", function() + local callCount = 0 + local value1 = "Hello World" + local value2 = 7 + + local callback = function(arg1, arg2) + expect(arg1).to.equal(value1) + expect(arg2).to.equal(value2) + callCount = callCount + 1 + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + + connection:disconnect() + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + end) + + it("should disconnect handlers", function() + local callback = function() + error("Callback was called after disconnect!") + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + connection:disconnect() + + signal:fire() + end) + + it("should fire handlers in order", function() + local signal = Signal.new() + local x = 0 + local y = 0 + + local callback1 = function() + expect(x).to.equal(0) + expect(y).to.equal(0) + x = x + 1 + end + + local callback2 = function() + expect(x).to.equal(1) + expect(y).to.equal(0) + y = y + 1 + end + + signal:connect(callback1) + signal:connect(callback2) + signal:fire() + + expect(x).to.equal(1) + expect(y).to.equal(1) + end) + + it("should continue firing despite mid-event disconnection", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionA + connectionA = signal:connect(function() + connectionA:disconnect() + countA = countA + 1 + end) + + signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(1) + end) + + it("should skip listeners that were disconnected during event evaluation", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionB + + signal:connect(function() + countA = countA + 1 + connectionB:disconnect() + end) + + connectionB = signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(0) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Text.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Text.lua new file mode 100644 index 0000000..3002173 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Text.lua @@ -0,0 +1,113 @@ +local EngineFeatureTextBoundsRoundUp = game:GetEngineFeature("TextBoundsRoundUp") + +local TextService = game:GetService("TextService") + +local Text = {} + +-- FYI: Any number greater than 2^30 will make TextService:GetTextSize give invalid results +local MAX_BOUND = 10000 + +-- Remove with EngineFeatureTextBoundsRoundUp +Text._TEMP_PATCHED_PADDING = Vector2.new(0, 0) + +if not EngineFeatureTextBoundsRoundUp then + Text._TEMP_PATCHED_PADDING = Vector2.new(2, 2) +end + +-- Wrapper function for GetTextSize +function Text.GetTextBounds(text, font, fontSize, bounds) + return TextService:GetTextSize(text, fontSize, font, bounds) + Text._TEMP_PATCHED_PADDING +end + +function Text.GetTextWidth(text, font, fontSize) + return Text.GetTextBounds(text, font, fontSize, Vector2.new(MAX_BOUND, MAX_BOUND)).X +end + +function Text.GetTextHeight(text, font, fontSize, widthCap) + return Text.GetTextBounds(text, font, fontSize, Vector2.new(widthCap, MAX_BOUND)).Y +end + +-- TODO(CLIPLAYEREX-391): Kill these truncate functions once we have official support for text truncation +function Text.Truncate(text, font, fontSize, widthInPixels, overflowMarker) + overflowMarker = overflowMarker or "" + + if Text.GetTextWidth(text, font, fontSize) > widthInPixels then + -- A binary search may be more efficient + local lastText = "" + for _, stopIndex in utf8.graphemes(text) do + local newText = string.sub(text, 1, stopIndex) .. overflowMarker + if Text.GetTextWidth(newText, font, fontSize) > widthInPixels then + return lastText + end + lastText = newText + end + else -- No truncation needed + return text + end + + return "" +end + +function Text.TruncateTextLabel(textLabel, overflowMarker) + textLabel.Text = Text.Truncate(textLabel.Text, textLabel.Font, + textLabel.TextSize, textLabel.AbsoluteSize.X, overflowMarker) +end + +-- Remove whitespace from the beginning and end of the string +function Text.Trim(str) + if type(str) ~= "string" then + error(string.format("Text.Trim called on non-string type %s.", type(str)), 2) + end + return (str:gsub("^%s*(.-)%s*$", "%1")) +end + +-- Remove whitespace from the end of the string +function Text.RightTrim(str) + if type(str) ~= "string" then + error(string.format("Text.RightTrim called on non-string type %s.", type(str)), 2) + end + return (str:gsub("%s+$", "")) +end + +-- Remove whitespace from the beginning of the string +function Text.LeftTrim(str) + if type(str) ~= "string" then + error(string.format("Text.LeftTrim called on non-string type %s.", type(str)), 2) + end + return (str:gsub("^%s+", "")) +end + +-- Replace multiple whitespace with one; remove leading and trailing whitespace +function Text.SpaceNormalize(str) + if type(str) ~= "string" then + error(string.format("Text.SpaceNormalize called on non-string type %s.", type(str)), 2) + end + return (str:gsub("%s+", " "):gsub("^%s+" , ""):gsub("%s+$" , "")) +end + +-- Splits a string by the provided pattern into a table. The pattern is interpreted as plain text. +function Text.Split(str, pattern) + if type(str) ~= "string" then + error(string.format("Text.Split called on non-string type %s.", type(str)), 2) + elseif type(pattern) ~= "string" then + error(string.format("Text.Split called with a pattern that is non-string type %s.", type(pattern)), 2) + elseif pattern == "" then + error("Text.Split called with an empty pattern.", 2) + end + + local result = {} + local currentPosition = 1 + + while true do + local patternStart, patternEnd = string.find(str, pattern, currentPosition, true) + if not patternStart or not patternEnd then break end + table.insert(result, string.sub(str, currentPosition, patternStart - 1)) + currentPosition = patternEnd + 1 + end + + table.insert(result, string.sub(str, currentPosition, string.len(str))) + + return result +end + +return Text \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Text.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Text.spec.lua new file mode 100644 index 0000000..42633fc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/Text.spec.lua @@ -0,0 +1,410 @@ +return function() + local Text = require(script.Parent.Text) + + describe("GetTextBounds", function() + it("should return a bounds of padding width and font-size height when the string is empty", function() + local bounds = Text.GetTextBounds("", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000)) + expect(bounds.X).to.equal(Text._TEMP_PATCHED_PADDING.x) + expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y) + end) + it("should return the height and width of a string as one line with large bounds", function() + local bounds = Text.GetTextBounds("One Two Three", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000)) + expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y) + end) + + it("should return the height of the string as multiple lines with short bounds", function() + local bounds = Text.GetTextBounds("One Two Three Four", Enum.Font.SourceSans, 18, Vector2.new(32, 1000)) + expect(bounds.Y > 18).to.equal(true) + end) + end) + + describe("GetTextHeight", function() + it("should return height equal to font size when string is empty", function() + local height = Text.GetTextHeight("", Enum.Font.SourceSans, 18, 0) + expect(height).to.equal(18 + Text._TEMP_PATCHED_PADDING.y) + end) + end) + + describe("GetTextWidth", function() + it("should return width equal to 1 when string is empty", function() + local width = Text.GetTextWidth("", Enum.Font.SourceSans, 18) + expect(width).to.equal(Text._TEMP_PATCHED_PADDING.x) + end) + end) + + describe("Truncate", function() + it("should return empty string", function() + local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 0, "...") + expect(emptyQuery).to.be.a("string") + expect(emptyQuery).to.equal("") + end) + + it("should return empty string for not empty box", function() + local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 50, "...") + expect(emptyQuery).to.be.a("string") + expect(emptyQuery).to.equal("") + end) + + it("should truncate with ...", function() + local reallyLongQuery = Text.Truncate( + "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100, "...") + expect(reallyLongQuery).to.equal("One Two Thre...") + end) + + it("should truncate without a ...", function() + local reallyLongQueryNoOverflowMarker = Text.Truncate( + "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100) + expect(reallyLongQueryNoOverflowMarker).to.equal("One Two Three ") + end) + + it("should not truncate", function() + local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, 100) + expect(shouldFitQuery).to.equal("One Two") + end) + + it("should not truncate, off by one check", function() + local oneCharQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 100) + expect(oneCharQuery).to.equal("O") + end) + + it("should truncate, off by one check", function() + local oneCharNoRoomQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 0) + expect(oneCharNoRoomQuery).to.equal("") + end) + + it("should perform a negative width check", function() + local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, -100, "...") + expect(shouldFitQuery).to.equal("") + end) + + itFIXME("should truncate long graphemes properly", function() + -- 11-byte rainbow flag grapheme + -- Flag, zero-space-joiner, rainbow + local rainbowFlag = utf8.char(127987) .. utf8.char(8205) .. utf8.char(127752) + local oneFlagWithinLimit = Text.Truncate( + rainbowFlag, Enum.Font.SourceSans, 18, 100, "...") + expect(oneFlagWithinLimit).to.equal(rainbowFlag) + + local twoRainbowFlags = rainbowFlag .. rainbowFlag + local twoFlagsAreFine = Text.Truncate( + twoRainbowFlags, Enum.Font.SourceSans, 18, 100, "...") + expect(twoFlagsAreFine).to.equal(twoRainbowFlags) + + local fourRainbowFlags = twoRainbowFlags .. twoRainbowFlags + local fourFlagsIsTooLong = Text.Truncate( + fourRainbowFlags, Enum.Font.SourceSans, 18, 100, "...") + expect(fourFlagsIsTooLong).to.equal(twoRainbowFlags .. "...") -- With --fflags==true fails because of truncation + end) + end) + + describe("TruncateTextLabel", function() + it("should use text label attributes to truncate text", function() + local screenGui = Instance.new("ScreenGui") + local textLabel = Instance.new("TextLabel") + textLabel.Size = UDim2.new(0, 100, 0, 32) + textLabel.Text = "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve" + textLabel.Font = Enum.Font.SourceSans + textLabel.TextSize = 18 + textLabel.Parent = screenGui + Text.TruncateTextLabel(textLabel) + + expect(textLabel.Text).to.equal("One Two Three ") + end) + end) + + + describe("TrimString", function() + it("Should trim the string properly 1", function() + local trimmedInput = Text.Trim("") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly 2", function() + local trimmedInput = Text.Trim(" ") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly 3", function() + local trimmedInput = Text.Trim("ab") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly 4", function() + local trimmedInput = Text.Trim(" ab ") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly 5", function() + local trimmedInput = Text.Trim(" a b ") + local expected = "a b" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly 6", function() + local trimmedInput = Text.Trim("\r\n\t\f a\r\n\t\f ") + local expected = "a" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string with unicode characters properly", function() + local trimmedInput = Text.Trim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly 7", function() + local trimmedInput = Text.Trim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should trim the string properly 8", function() + local trimmedInput = Text.Trim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n") + local expected = "😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + end) + + + describe("RightTrimString", function() + it("Should right trim the string properly 1", function() + local trimmedInput = Text.RightTrim("") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly 2", function() + local trimmedInput = Text.RightTrim(" ") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly 3", function() + local trimmedInput = Text.RightTrim("ab") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly 4", function() + local trimmedInput = Text.RightTrim(" ab ") + local expected = " ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly 5", function() + local trimmedInput = Text.RightTrim(" a b ") + local expected = " a b" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly 6", function() + local trimmedInput = Text.RightTrim("\r\n\t\f a\r\n\t\f ") + local expected = "\r\n\t\f a" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string with unicode characters properly", function() + local trimmedInput = Text.RightTrim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly 7", function() + local trimmedInput = Text.RightTrim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ") + local expected = " 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should right trim the string properly 8", function() + local trimmedInput = Text.RightTrim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n") + local expected = "\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + end) + + + describe("LeftTrimString", function() + it("Should left trim the string properly 1", function() + local trimmedInput = Text.LeftTrim("") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly 2", function() + local trimmedInput = Text.LeftTrim(" ") + local expected = "" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly 3", function() + local trimmedInput = Text.LeftTrim("ab") + local expected = "ab" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly 4", function() + local trimmedInput = Text.LeftTrim(" ab ") + local expected = "ab " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly 5", function() + local trimmedInput = Text.LeftTrim(" a b ") + local expected = "a b " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly 6", function() + local trimmedInput = Text.LeftTrim("\r\n\t\f a\r\n\t\f ") + local expected = "a\r\n\t\f " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string with unicode characters properly", function() + local trimmedInput = Text.LeftTrim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓" + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly 7", function() + local trimmedInput = Text.LeftTrim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ") + local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 " + expect(trimmedInput).to.equal(expected) + end) + it("Should left trim the string properly", function() + local trimmedInput = Text.LeftTrim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n") + local expected = "😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n" + expect(trimmedInput).to.equal(expected) + end) + end) + + + describe("SpaceNormalize", function() + it("should remove multiple spaces between words", function() + local a = "This is not a normal sentence." + + expect(Text.SpaceNormalize(a)).to.equal("This is not a normal sentence.") + end) + + it("should remove leading and trailing whitespace", function() + local a = " SpaceTabSpaceTab " + + expect(Text.SpaceNormalize(a)).to.equal("SpaceTabSpaceTab") + end) + + it("should not change a string with no whitespace", function() + local a = "There'sNo%Whit.e\\space--InThis." + + expect(Text.SpaceNormalize(a)).to.equal(a) + end) + + it("should remove all whitespace in a string that is nothing but whitespace", function() + local a = " " + + expect(Text.SpaceNormalize(a)).to.equal("") + end) + + it("should handle the case where the string is empty", function() + local a = "" + + expect(Text.SpaceNormalize(a)).to.equal(a) + end) + + it("should throw an error if called an a non-string type", function() + local a = { first = 1, second = 2 } + + expect(function() + Text.SpaceNormalize(a) + end).to.throw() + end) + end) + + + describe("Split", function() + local function tableEquals(tb1, tb2) + local tables = { tb1, tb2 } + + for _,tb in ipairs(tables) do + for key in pairs(tb) do + if tb1[key] ~= tb2[key] then + return false + end + end + end + + return true + end + + it("should return the correct table for your standard use case", function() + local a = "this,is,comma,separated" + local pattern = "," + local expectedResult = { + [1] = "this", + [2] = "is", + [3] = "comma", + [4] = "separated", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should not remove whitespace", function() + local a = " SpaceTab , , Space" + local pattern = "," + local expectedResult = { + [1] = " SpaceTab ", + [2] = " ", + [3] = " Space", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should treat regular expressions as plain text", function() + local a = "Notyour^%s+normalstring.Thisisasecondsentence." + local b = "." + local c = "^%s+" + local d = "%A" + + local expectedB = { + [1] = "Notyour^%s+normalstring", + [2] = "Thisisasecondsentence", + [3] = "", + } + local expectedC = { + [1] = "Notyour", + [2] = "normalstring.Thisisasecondsentence." + } + local expectedD = { + [1] = "Notyour^%s+normalstring.Thisisasecondsentence." + } + + expect(tableEquals(Text.Split(a, b), expectedB)).to.equal(true) + expect(tableEquals(Text.Split(a, c), expectedC)).to.equal(true) + expect(tableEquals(Text.Split(a, d), expectedD)).to.equal(true) + end) + + it("should work when pattern is not in string", function() + local a = "The pattern you are looking for does not exist." + local pattern = "," + local expectedResult = { + [1] = "The pattern you are looking for does not exist.", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should work when called on an empty string", function() + local a = "" + local pattern = "," + local expectedResult = { + [1] = "", + } + + expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true) + end) + + it("should throw an error if called on an empty pattern", function() + local a = "The pattern definitely doesn't exist here." + local pattern = "" + + expect(function() + Text.Split(a, pattern) + end).to.throw() + end) + + it("should throw an error if called an a non-string type", function() + local a = { first = 1, second = 2 } + local b = "an actual string" + + expect(function() + Text.Split(a, b) + end).to.throw() + + expect(function() + Text.Split(b, a) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/memoize.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/memoize.lua new file mode 100644 index 0000000..261dcd0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/memoize.lua @@ -0,0 +1,68 @@ +--[[ + memoize creates a function as a wrapper that caches the last outputs of a function. + This is useful if you know that the function should return the same output every + time it is run with the same inputs. The function should only return an output, and + not have any side effects. These side effects are not cached. + + Without memoize's caching, even though the function ouputs the same values, the + memory locations of the values are different; tables made in the function, even if + they have the same values, won't be the same tables. + + memoize only caches the last set of inputs and ouputs. This means that it is only + helpful when the function is likely to be called with the same inputs multiple + times in a row. This is the case with most Roact use cases. + + Note that memoize only does a ** shallow check on table inputs ** . This means + that if the same table is input but the elements of the table are different then + it will be assumed that the table has not changed. + + In addition to all the previous warnings, memoize strips trailing nils. This means + that if foo is a memoized function and we call foo(), then foo(nil) will return a + cached value. This is opposed to how print handles input. print() only outputs a + new line, but print(nil) outputs "nil". This is because varargs can detect the + number of arguments passed in. So, be careful when using memoize with varargs. + Trailing nils will be stripped. + + The wrapper can take any number of inputs and give any number of outputs. + Leading and interspersed nils are handled gracefully. Trailing nils on the input + are stripped. +]] +local function captureSize(...) + return {...}, select("#", ...) +end + +local function memoize(func) + assert(type(func) == "function", "memoize requires a function to memoize") + + local lastArgs + local lastNumArgs + local lastOutput + local lastNumOutput + + return function(...) + local numArgs = select("#", ...) + + while numArgs > 0 and select(numArgs, ...) == nil do + numArgs = numArgs - 1 + end + + if numArgs ~= lastNumArgs then + lastArgs = {...} + lastNumArgs = numArgs + lastOutput, lastNumOutput = captureSize(func(...)) + return unpack(lastOutput, 1, lastNumOutput) + end + + for i = 1, lastNumArgs do + if select(i, ...) ~= lastArgs[i] then + lastArgs = {...} + lastOutput, lastNumOutput = captureSize(func(...)) + break + end + end + + return unpack(lastOutput, 1, lastNumOutput) + end +end + +return memoize \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/memoize.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/memoize.spec.lua new file mode 100644 index 0000000..5ae2b6e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Common/memoize.spec.lua @@ -0,0 +1,213 @@ +return function() + local memoize = require(script.Parent.memoize) + + describe("memoize", function() + it("should handle arity 0", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + expect(identity()).to.equal(nil) + expect(identity(nil)).to.equal(nil) + expect(identity(nil, nil)).to.equal(nil) + expect(callCount).to.equal(1) + end) + + it("should handle arity 1", function() + local callCount = 0 + local identity = memoize(function(a) + callCount = callCount + 1 + return a + end) + + expect(identity(5)).to.equal(5) + expect(identity(5)).to.equal(5) + expect(callCount).to.equal(1) + + expect(identity(6)).to.equal(6) + expect(callCount).to.equal(2) + + expect(identity(5)).to.equal(5) + expect(callCount).to.equal(3) + end) + + it("should handle arity 2", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + expect(callCount).to.equal(1) + + a, b = identity(6, 5) + expect(a).to.equal(6) + expect(b).to.equal(5) + + expect(callCount).to.equal(2) + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + expect(callCount).to.equal(3) + end) + + it("should handle mixed arity", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + a, b = identity(5, 6) + expect(a).to.equal(5) + expect(b).to.equal(6) + + expect(callCount).to.equal(1) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b = identity() + expect(a).to.equal(nil) + expect(b).to.equal(nil) + + a, b = identity() + expect(a).to.equal(nil) + expect(b).to.equal(nil) + + expect(callCount).to.equal(3) + end) + + it("should handle trailing nils", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(5, nil) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + expect(callCount).to.equal(1) + + a, b = identity(7) + expect(a).to.equal(7) + expect(b).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b = identity(5) + expect(a).to.equal(5) + expect(b).to.equal(nil) + + expect(callCount).to.equal(3) + end) + + it("should handle leading nils", function() + local callCount = 0 + local identity = memoize(function(a, b) + callCount = callCount + 1 + return a, b + end) + + local a, b + + a, b = identity(nil, 7) + expect(a).to.equal(nil) + expect(b).to.equal(7) + + a, b = identity(nil, 7) + expect(a).to.equal(nil) + expect(b).to.equal(7) + + expect(callCount).to.equal(1) + + a, b = identity(7) + expect(a).to.equal(7) + expect(b).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b = identity(nil, 7) + expect(a).to.equal(nil) + expect(b).to.equal(7) + + expect(callCount).to.equal(3) + end) + + it("should handle interspersed nils", function() + local callCount = 0 + local identity = memoize(function(a, b, c, d) + callCount = callCount + 1 + return a, b, c, d + end) + + local a, b, c, d + + a, b, c, d = identity(7, nil, 7, nil) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(7) + expect(d).to.equal(nil) + + -- Trailing nils can affect how interspersed nils are handled + a, b, c, d = identity(7, nil, 7) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(7) + expect(d).to.equal(nil) + + expect(callCount).to.equal(1) + + a, b, c, d = identity(7, nil, nil, nil) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(nil) + expect(d).to.equal(nil) + + expect(callCount).to.equal(2) + + a, b, c, d = identity(7, nil, 7, nil) + expect(a).to.equal(7) + expect(b).to.equal(nil) + expect(c).to.equal(7) + expect(d).to.equal(nil) + + expect(callCount).to.equal(3) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/.robloxrc new file mode 100644 index 0000000..635c2ec --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/.robloxrc @@ -0,0 +1,5 @@ +{ + "lint": { + "LocalShadow": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/AddUser.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/AddUser.lua new file mode 100644 index 0000000..7ac8be2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/AddUser.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(user) + return { + user = user + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/AddUsers.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/AddUsers.lua new file mode 100644 index 0000000..fad0bcb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/AddUsers.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(users) + return { + users = users + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsCompleted.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsCompleted.lua new file mode 100644 index 0000000..b90860d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsCompleted.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId) + return { + userId = userId + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsFailed.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsFailed.lua new file mode 100644 index 0000000..a4beb24 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsFailed.lua @@ -0,0 +1,9 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId, response) + return { + userId = userId, + response = response + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsStarted.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsStarted.lua new file mode 100644 index 0000000..b90860d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/FetchUserFriendsStarted.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId) + return { + userId = userId + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/ReceivedDisplayName.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/ReceivedDisplayName.lua new file mode 100644 index 0000000..cbd63e1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/ReceivedDisplayName.lua @@ -0,0 +1,9 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId, displayName) + return { + userId = tostring(userId), + displayName = displayName, + } +end) diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/ReceivedPlacesInfos.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/ReceivedPlacesInfos.lua new file mode 100644 index 0000000..266028f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/ReceivedPlacesInfos.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(placesInfos) + return { + placesInfos = placesInfos, + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/ReceivedUserCountryCode.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/ReceivedUserCountryCode.lua new file mode 100644 index 0000000..d67743d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/ReceivedUserCountryCode.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(countryCode) + return { + countryCode = countryCode, + } +end) diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/RemoveUser.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/RemoveUser.lua new file mode 100644 index 0000000..b407b15 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/RemoveUser.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId) + return { + userId = userId, + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetDeviceOrientation.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetDeviceOrientation.lua new file mode 100644 index 0000000..6cfa228 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetDeviceOrientation.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(deviceOrientation) + return { + deviceOrientation = deviceOrientation, + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetFriendCount.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetFriendCount.lua new file mode 100644 index 0000000..39aa112 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetFriendCount.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") +local Common = CorePackages.AppTempCommon.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(count) + return { + count = count, + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameIcons.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameIcons.lua new file mode 100644 index 0000000..458f12c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameIcons.lua @@ -0,0 +1,14 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) +local ArgCheck = require(CorePackages.ArgCheck) + +--[[ + Each entry in the table is a type of GameIcon with the universe id as key +]] +return Action(script.Name, function(iconsTable) + ArgCheck.isType(iconsTable, "table", "iconsTable") + + return { + gameIcons = iconsTable + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameIcons.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameIcons.spec.lua new file mode 100644 index 0000000..bc1086f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameIcons.spec.lua @@ -0,0 +1,27 @@ +return function() + local SetGameIcons = require(script.Parent.SetGameIcons) + + it("should assert if given a non-table for thumbnailsTable", function() + SetGameIcons({}) + + expect(function() + SetGameIcons("string") + end).to.throw() + + expect(function() + SetGameIcons(0) + end).to.throw() + + expect(function() + SetGameIcons(nil) + end).to.throw() + + expect(function() + SetGameIcons(false) + end).to.throw() + + expect(function() + SetGameIcons(function() end) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameThumbnails.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameThumbnails.lua new file mode 100644 index 0000000..8677990 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameThumbnails.lua @@ -0,0 +1,26 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +--[[ + Passes a table that looks like this : { "universeId" : {json}, ... } + + { + "26034470" : { + universeId : "26034470", + placeId : "70542190", + url : https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814, + final : true + }, {...}, ... + } + +]] + +--TODO MOBLUAPP-778 Refactor improper Setter Actions. +return Action(script.Name, function(thumbnailsTable) + assert(type(thumbnailsTable) == "table", + string.format("SetGameThumbnails action expects thumbnailsTable to be a table, was %s", type(thumbnailsTable))) + + return { + thumbnails = thumbnailsTable + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameUserIsPlaying.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameUserIsPlaying.lua new file mode 100644 index 0000000..7743f8f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetGameUserIsPlaying.lua @@ -0,0 +1,11 @@ +local CorePackages = game:GetService("CorePackages") +local Common = CorePackages.AppTempCommon.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(userId, universeId) + return { + userId = userId, + universeId = universeId, + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserIsFriend.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserIsFriend.lua new file mode 100644 index 0000000..690a05e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserIsFriend.lua @@ -0,0 +1,9 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId, isFriend) + return { + userId = userId, + isFriend = isFriend, + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserMembershipType.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserMembershipType.lua new file mode 100644 index 0000000..1c08dbe --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserMembershipType.lua @@ -0,0 +1,11 @@ +local CorePackages = game:GetService("CorePackages") +local Common = CorePackages.AppTempCommon.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(userId, membershipType) + return { + userId = userId, + membershipType = membershipType, + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserPresence.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserPresence.lua new file mode 100644 index 0000000..b25a118 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserPresence.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(userId, presence, lastLocation) + return { + userId = tostring(userId), + presence = presence, + lastLocation = lastLocation, + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserThumbnail.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserThumbnail.lua new file mode 100644 index 0000000..508cf70 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/SetUserThumbnail.lua @@ -0,0 +1,13 @@ +local CorePackages = game:GetService("CorePackages") +local Common = CorePackages.AppTempCommon.Common + +local Action = require(Common.Action) + +return Action(script.Name, function(userId, image, thumbnailType, thumbnailSize) + return { + userId = userId, + image = image, + thumbnailType = thumbnailType, + thumbnailSize = thumbnailSize, + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/UpdateFetchingStatus.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/UpdateFetchingStatus.lua new file mode 100644 index 0000000..1fc68b2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/UpdateFetchingStatus.lua @@ -0,0 +1,9 @@ +local CorePackages = game:GetService("CorePackages") +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(key, status) + return { + key = key, + status = status + } +end) diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/UpdateFetchingStatus.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/UpdateFetchingStatus.spec.lua new file mode 100644 index 0000000..1cb23fe --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Actions/UpdateFetchingStatus.spec.lua @@ -0,0 +1,21 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus) + + describe("Action UpdateFetchingStatus", function() + it("should return correct action name", function() + expect(UpdateFetchingStatus.name).to.equal("UpdateFetchingStatus") + end) + + it("should return correct action type name", function() + local action = UpdateFetchingStatus() + expect(action.type).to.equal(UpdateFetchingStatus.name) + end) + + it("should return a table with the correct key and status", function() + local action = UpdateFetchingStatus("key", "status") + expect(action.key).to.equal("key") + expect(action.status).to.equal("status") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/LoadingBar.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/LoadingBar.lua new file mode 100644 index 0000000..dfe129a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/LoadingBar.lua @@ -0,0 +1,51 @@ +local Workspace = game:GetService("Workspace") +local RunService = game:GetService('RunService') +local CorePackages = game:GetService("CorePackages") + +local Roact = require(CorePackages.Roact) + +local BAR_SLICE_CENTER = Rect.new(1, 0, 2, 3) +local BAR_MAX_SIZE = 15 +local BAR_MAX_AMPLITUDE = 40 +local BAR_DIAMETER = 4 +local BAR_PERIOD = 1.25 + +local LoadingBar = Roact.Component:extend("LoadingBar") + +function LoadingBar:init() + self.barRef = Roact.createRef() +end + +function LoadingBar:render() + local zIndex = self.props.ZIndex + + return Roact.createElement("ImageLabel", { + Image = "rbxasset://textures/ui/LuaApp/9-slice/gr-loading-indicator.png", + ScaleType = "Slice", + SliceCenter = BAR_SLICE_CENTER, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ZIndex = zIndex, + [Roact.Ref] = self.barRef, + }) +end + +function LoadingBar:didMount() + self.connection = RunService.RenderStepped:Connect(function() + local t = Workspace.DistributedGameTime + local instance = self.barRef.current + local period = 2.0 * math.pi / BAR_PERIOD + + local width = (BAR_MAX_SIZE/2) * (1 - math.cos(2*t*period)) + instance.Size = UDim2.new(0, BAR_DIAMETER + width, 0, BAR_DIAMETER) + + local x = BAR_MAX_AMPLITUDE * math.cos(t*period) + instance.Position = UDim2.new(0.5, x - width/2 - BAR_DIAMETER/2, 0.5, 0) + end) +end + +function LoadingBar:willUnmount() + self.connection:Disconnect() +end + +return LoadingBar \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/LoadingBar.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/LoadingBar.spec.lua new file mode 100644 index 0000000..163e7d8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Components/LoadingBar.spec.lua @@ -0,0 +1,17 @@ +return function() + local LoadingBar = require(script.Parent.LoadingBar) + + local CorePackages = game:GetService("CorePackages") + + local Roact = require(CorePackages.Roact) + + it("should create and destroy without errors", function() + local element = Roact.createElement(LoadingBar, { + Position = UDim2.new(0.5, 0, 0.5, 5), + AnchorPoint = Vector2.new(0.5, 0.5), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/AvatarThumbnailTypes.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/AvatarThumbnailTypes.lua new file mode 100644 index 0000000..67a7373 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/AvatarThumbnailTypes.lua @@ -0,0 +1,4 @@ +return { + AvatarThumbnail = "AvatarThumbnail", + HeadShot = "HeadShot", +} diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/RetrievalStatus.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/RetrievalStatus.lua new file mode 100644 index 0000000..0ea027a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/RetrievalStatus.lua @@ -0,0 +1,21 @@ +local RetrievalStatus = {} + +local EnumValues = +{ + NotStarted = "NotStarted", + Fetching = "Fetching", + Done = "Done", + Failed = "Failed", +} + +setmetatable(RetrievalStatus, + { + __newindex = function(t, key, index) + end, + __index = function(t, index) + assert(EnumValues[index] ~= nil, ("RetrievalStatus Enum has no value: " .. tostring(index))) + return EnumValues[index] + end + }) + +return RetrievalStatus \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/WebPresenceMap.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/WebPresenceMap.lua new file mode 100644 index 0000000..5d312c8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Enum/WebPresenceMap.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") + +local User = require(CorePackages.AppTempCommon.LuaApp.Models.User) + +return { + [0] = User.PresenceType.OFFLINE, + [1] = User.PresenceType.ONLINE, + [2] = User.PresenceType.IN_GAME, + [3] = User.PresenceType.IN_STUDIO, +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/AvatarEditorNewCatalogEnabled.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/AvatarEditorNewCatalogEnabled.lua new file mode 100644 index 0000000..4fa53ff --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/AvatarEditorNewCatalogEnabled.lua @@ -0,0 +1,15 @@ +local CorePackages = game:GetService("CorePackages") + +local ThrottleUserId = require(CorePackages.AppTempCommon.LuaApp.Utils.ThrottleUserId) + +local FIntAvatarEditorNewCatalogButton = settings():GetFVariable("AvatarEditorNewCatalogButton2") + +return function(userId) + if tonumber(userId) then + local throttleNumber = tonumber(FIntAvatarEditorNewCatalogButton) + local id = tonumber(userId) + return ThrottleUserId(throttleNumber, id) + else + return false + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/ConvertUniverseIdToString.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/ConvertUniverseIdToString.lua new file mode 100644 index 0000000..8e3a7f8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/ConvertUniverseIdToString.lua @@ -0,0 +1,11 @@ +-- TODO: Delete this file when deleting the flag: LuaAppConvertUniverseIdToStringV364 +local FFlagLuaAppConvertUniverseIdToString = settings():GetFFlag("LuaAppConvertUniverseIdToStringV364") + +return function(universeId) + -- When the flag is on, we've converted the universe id to string at the place we received it + if FFlagLuaAppConvertUniverseIdToString then + return universeId + else + return tostring(universeId) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/GetEnableFriendFooterOnHomePage.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/GetEnableFriendFooterOnHomePage.lua new file mode 100644 index 0000000..ae6c0aa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/GetEnableFriendFooterOnHomePage.lua @@ -0,0 +1,15 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local ThrottleUserId = require(CorePackages.AppTempCommon.LuaApp.Utils.ThrottleUserId) + +local FIntEnableFriendFooterOnHomePage = settings():GetFVariable("EnableFriendFooterOnHomePageV369") + +-- Don't call this function globally because we cannot get the userId +-- Reason: The LocalPlayer wouldn't be ready if we called it globally. +return function() + local throttleNumber = tonumber(FIntEnableFriendFooterOnHomePage) + local userId = Players.LocalPlayer.UserId + + return ThrottleUserId(throttleNumber, userId) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/GetFFlagUseDateTimeType.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/GetFFlagUseDateTimeType.lua new file mode 100644 index 0000000..7a2bbbd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Flags/GetFFlagUseDateTimeType.lua @@ -0,0 +1,3 @@ +return function() + return settings():GetFFlag("UseDateTimeType3") +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatSendGameLinkMessage.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatSendGameLinkMessage.lua new file mode 100644 index 0000000..0306124 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatSendGameLinkMessage.lua @@ -0,0 +1,19 @@ +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +return function(requestImpl, conversationId, universeId, decorators) + assert(requestImpl, "requestImpl is required") + assert(conversationId, "conversationId is required") + assert(universeId, "universeId is required") + + local payload = HttpService:JSONEncode({ + conversationId = conversationId, + universeId = universeId, + decorators = decorators + }) + local url = string.format("%s/send-game-link-message", Url.CHAT_URL) + + return requestImpl(url, "POST", { postBody = payload }) +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatSendMessage.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatSendMessage.lua new file mode 100644 index 0000000..bdf394a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatSendMessage.lua @@ -0,0 +1,16 @@ +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +return function(requestImpl, conversationId, messageText, decorators) + local payload = HttpService:JSONEncode({ + conversationId = conversationId, + message = messageText, + decorators = decorators + }) + + local url = string.format("%s/send-message", Url.CHAT_URL) + + return requestImpl(url, "POST", { postBody = payload }) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatStartOneToOneConversation.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatStartOneToOneConversation.lua new file mode 100644 index 0000000..4f7a75f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ChatStartOneToOneConversation.lua @@ -0,0 +1,14 @@ +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +return function(requestImpl, userId, clientId) + local payload = HttpService:JSONEncode({ + participantuserId = userId + }) + + local url = string.format("%s/start-one-to-one-conversation", Url.CHAT_URL) + + return requestImpl(url, "POST", { postBody = payload }) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesGetIcons.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesGetIcons.lua new file mode 100644 index 0000000..bc5548d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesGetIcons.lua @@ -0,0 +1,25 @@ +local CorePackages = game:GetService("CorePackages") +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +--[[ + Docs: https://thumbnails.roblox.com/docs#!/Games/get_v1_games_icons + This resolves to + { + "data": [ + { + "targetId": 0, + "state": "Error", + "imageUrl": "string" + } + ] +} +]] +return function (requestImpl, universeIds, size) + local qs = Url:makeQueryString({ + universeIds = table.concat(universeIds, ","), + format = "png", + size = size, + }) + local url = string.format("%sv1/games/icons?%s", Url.THUMBNAILS_URL, qs) + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesGetThumbnails.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesGetThumbnails.lua new file mode 100644 index 0000000..fb930f8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesGetThumbnails.lua @@ -0,0 +1,54 @@ +--[[ + *** DEPRECATED *** + TODO: removed this file after new thumbnail API is being in use without any flags + RELATED: GAMEDISC-27 GAMEDISC-126 FIntLuaAppPercentRollOutNewThumbnailsApiV3 +]] + +local CorePackages = game:GetService("CorePackages") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +--[[ + This endpoint returns a promise that resolves to: + [ + { + "final": true, + "url": "string", + "retryToken": "string", + "universeId": 0, + "placeId": 0 + }, {...}, ... + ] +]] + +-- requestImpl - (function>(url, requestMethod, options)) +-- imageTokens - (array) the placeIds of the places you want to get thumbnails for +-- height - (int) the height of the asset to render +-- width - (int) the width of the asset to render +return function(requestImpl, imageTokens, height, width) + local args = {} + + if height then + table.insert(args, string.format("height=%d", height)) + end + + if width then + table.insert(args, string.format("width=%d", width)) + end + + -- append all of the thumbnail tokens + local totalTokens = 0 + for _, value in pairs(imageTokens) do + totalTokens = totalTokens + 1 + table.insert(args, string.format("imageTokens=%s", value)) + end + if totalTokens == 0 then + error("cannot fetch thumbnails without tokens") + end + + -- construct the url + local url = string.format("%sv1/games/game-thumbnails?%s", Url.GAME_URL, table.concat(args, "&")) + + -- return a promise of the result listed above + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesMultigetPlaceDetails.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesMultigetPlaceDetails.lua new file mode 100644 index 0000000..65728aa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GamesMultigetPlaceDetails.lua @@ -0,0 +1,33 @@ +local CorePackages = game:GetService("CorePackages") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +--[[ + This endpoint returns games' information with a batches of place ids + Doc: https://games.roblox.com/docs#!/Games/get_v1_games_multiget_place_details + { + "placeId": 0, + "name": "string", + "description": "string", + "url": "string", + "builder": "string", + "builderId": 0, + "isPlayable": true, + "reasonProhibited": "string", + "universeId": 0, + "universeRootPlaceId": 0, + "price": 0, + "imageToken": "string" + } +]]-- + +return function(requestImpl, placeIds) + local argTable = { + placeIds = placeIds, + } + + local args = Url:makeQueryString(argTable) + local url = string.format("%s/v1/games/multiget-place-details?%s", Url.GAME_URL, args) + + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GetPlaceInfos.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GetPlaceInfos.lua new file mode 100644 index 0000000..883bc34 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/GetPlaceInfos.lua @@ -0,0 +1,17 @@ +local CorePackages = game:GetService("CorePackages") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +return function(requestImpl, placeIds) + local argTable = { + placeIds = placeIds, + } + + -- construct the url + local args = Url:makeQueryString(argTable) + local url = string.format("%s/v1/games/multiget-place-details?%s", + Url.GAME_URL, args + ) + + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ThumbnailsGetAvatar.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ThumbnailsGetAvatar.lua new file mode 100644 index 0000000..8ff1525 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ThumbnailsGetAvatar.lua @@ -0,0 +1,39 @@ +local CorePackages = game:GetService("CorePackages") +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +--[[ + Documentation of endpoint: + https://thumbnails.roblox.com/docs#!/Avatar/get_v1_users_avatar + + input: + userIds + thumbnailSize + output: + [ + { + "targetId": number, + "state": string, + "imageUrl": string, + }, + ] +]] + +local MAX_USER_IDS = 100 + +return function (networkImpl, userIds, thumbnailSize) + assert(type(userIds) == "table", "ThumbnailsGetAvatar expects userIds to be a table") + + if #userIds == 0 or #userIds > MAX_USER_IDS then + error(string.format("ThumbnailsGetAvatar request expects userIds count between 1-%d", MAX_USER_IDS)) + end + + local queryString = Url:makeQueryString({ + userIds = table.concat(userIds, ","), + size = thumbnailSize, + format = "png", + }) + + local url = string.format("%sv1/users/avatar?%s", Url.THUMBNAILS_URL, queryString) + + return networkImpl(url, "GET") +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ThumbnailsGetAvatarHeadshot.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ThumbnailsGetAvatarHeadshot.lua new file mode 100644 index 0000000..58fc9cd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/ThumbnailsGetAvatarHeadshot.lua @@ -0,0 +1,39 @@ +local CorePackages = game:GetService("CorePackages") +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +--[[ + Documentation of endpoint: + https://thumbnails.roblox.com/docs#!/Avatar/get_v1_users_avatar_headshot + + input: + userIds + thumbnailSize + output: + [ + { + "targetId": number, + "state": string, + "imageUrl": string, + }, + ] +]] + +local MAX_USER_IDS = 100 + +return function (networkImpl, userIds, thumbnailSize) + assert(type(userIds) == "table", "ThumbnailsGetAvatarHeadshot expects userIds to be a table") + + if #userIds == 0 or #userIds > MAX_USER_IDS then + error(string.format("ThumbnailsGetAvatarHeadshot request expects userIds count between 1-%d", MAX_USER_IDS)) + end + + local queryString = Url:makeQueryString({ + userIds = table.concat(userIds, ","), + size = thumbnailSize, + format = "png", + }) + + local url = string.format("%sv1/users/avatar-headshot?%s", Url.THUMBNAILS_URL, queryString) + + return networkImpl(url, "GET") +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriendCount.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriendCount.lua new file mode 100644 index 0000000..91ede51 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriendCount.lua @@ -0,0 +1,31 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +local isNewFriendsEndpointsEnabled = require(CorePackages.AppTempCommon.LuaChat.Flags.isNewFriendsEndpointsEnabled) + +--[[ + This endpoint returns a promise that resolves to: + + [ + { + "success:" true, + "count": "0" + }, + ] +]]-- + +-- requestImpl - (function>(url, requestMethod, options)) +return function(requestImpl) + + local url = string.format("%s/user/get-friendship-count?%s", + Url.API_URL, tostring(Players.LocalPlayer.UserId) + ) + + if isNewFriendsEndpointsEnabled() then + url = string.format("%s/my/friends/count", Url.FRIEND_URL) + end + + return requestImpl(url, "GET") +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriends.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriends.lua new file mode 100644 index 0000000..79b8653 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetFriends.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +return function(requestImpl, userId) + local url = string.format("%s/users/%s/friends", + Url.FRIEND_URL, userId + ) + + return requestImpl(url, "GET") +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetPresence.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetPresence.lua new file mode 100644 index 0000000..b054e6e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetPresence.lua @@ -0,0 +1,25 @@ +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) + +-- Endpoint documented here: +-- https://presence.roblox.com/docs + +return function(requestImpl, userIds) + local userIdsToNumber = {} + for _, id in pairs(userIds) do + local idToNumber = tonumber(id) + if idToNumber then + table.insert(userIdsToNumber, idToNumber) + end + end + + local payload = HttpService:JSONEncode({ + userIds = userIdsToNumber, + }) + + local url = string.format("%s/presence/users", Url.PRESENCE_URL) + + return requestImpl(url, "POST", { postBody = payload }) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetThumbnail.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetThumbnail.lua new file mode 100644 index 0000000..78d181e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Requests/UsersGetThumbnail.lua @@ -0,0 +1,48 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) + +local THUMBNAIL_TYPE_BY_NAME = { + AvatarThumbnail = Enum.ThumbnailType.AvatarThumbnail, + HeadShot = Enum.ThumbnailType.HeadShot, +} + +local THUMBNAIL_SIZE_BY_NAME = { + Size48x48 = Enum.ThumbnailSize.Size48x48, + Size60x60 = Enum.ThumbnailSize.Size60x60, + Size100x100 = Enum.ThumbnailSize.Size100x100, + Size150x150 = Enum.ThumbnailSize.Size150x150, + Size352x352 = Enum.ThumbnailSize.Size352x352 +} + +return function(userId, thumbnailType, thumbnailSize) + return Promise.new(function(resolve, reject) + --Async methods will yield the thread + spawn(function() + local result = {success = false} + local success, message = pcall(function() + local image, isFinal = Players:GetUserThumbnailAsync( + tonumber(userId), THUMBNAIL_TYPE_BY_NAME[thumbnailType], THUMBNAIL_SIZE_BY_NAME[thumbnailSize] + ) + + result = { + success = true, + id = userId, + thumbnailType = thumbnailType, + thumbnailSize = thumbnailSize, + + image = isFinal and image or nil, + isFinal = isFinal, + } + end) + + if success then + resolve(result) + else + result.message = message + reject(result) + end + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Url.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Url.lua new file mode 100644 index 0000000..cdf2ef2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Url.lua @@ -0,0 +1,181 @@ +--[[ + Url Constructor + + Provides a single location for base urls. + +]]-- +local ContentProvider = game:GetService("ContentProvider") + +local FFlagLuaFixEconomyCreatorStatsUrl = game:DefineFastFlag("LuaFixEconomyCreatorStatsUrl", false) + +-- helper functions +local function parseBaseUrlInformation() + -- get the current base url from the current configuration + local baseUrl = ContentProvider.BaseUrl + + -- keep a copy of the base url (https://www.roblox.com/) + -- append a trailing slash if there isn't one + if baseUrl:sub(#baseUrl) ~= "/" then + baseUrl = baseUrl .. "/" + end + + -- parse out scheme (http, https) + local _, schemeEnd = baseUrl:find("://") + + -- parse out the prefix (www, kyle, ying, etc.) + local prefixIndex, prefixEnd = baseUrl:find("%.", schemeEnd + 1) + local basePrefix = baseUrl:sub(schemeEnd + 1, prefixIndex - 1) + + -- parse out the domain (roblox.com/, sitetest1.robloxlabs.com/, etc.) + local baseDomain = baseUrl:sub(prefixEnd + 1) + + return baseUrl, basePrefix, baseDomain +end +local function preventTableModification(aTable, key, value) + error("Attempt to modify read-only table") +end +local function createReadOnlyTable(aTable) + return setmetatable({}, { + __index = aTable, + __newindex = preventTableModification, + __metatable = false + }); +end + + +-- url construction building blocks +local _baseUrl, _basePrefix, _baseDomain = parseBaseUrlInformation() + +-- construct urls once +local _baseApiUrl = string.format("https://api.%s", _baseDomain) +local _baseApisUrl = string.format("https://apis.%s", _baseDomain) +local _baseAuthUrl = string.format("https://auth.%s", _baseDomain) +local _baseAccountSettingsUrl = string.format("https://accountsettings.%s", _baseDomain) +local _baseAvatarUrl = string.format("https://avatar.%s", _baseDomain) +local _baseCatalogUrl = string.format("https://catalog.%s", _baseDomain) +local _baseInventoryUrl = string.format("https://inventory.%s", _baseDomain) +local _baseChatUrl = string.format("https://chat.%sv2", _baseDomain) +local _baseFriendUrl = string.format("https://friends.%sv1", _baseDomain) +local _baseGameAssetUrl = string.format("https://assetgame.%s", _baseDomain) +local _baseGamesUrl = string.format("https://games.%s", _baseDomain) +local _baseGroupsUrl = string.format("https://groups.%s", _baseDomain) +local _baseNotificationUrl = string.format("https://notifications.%s", _baseDomain) +local _basePresenceUrl = string.format("https://presence.%sv1", _baseDomain) +local _baseRealtimeUrl = string.format("https://realtime.%s", _baseDomain) +local _baseWebUrl = string.format("https://web.%s", _baseDomain) +local _baseWwwUrl = string.format("https://www.%s", _baseDomain) +local _baseAdsUrl = string.format("https://ads.%s", _baseDomain) +local _baseFollowingsUrl = string.format("https://followings.%s", _baseDomain) +local _baseEconomyUrl = string.format("https://economy.%s", _baseDomain) +local _baseThumbnailsUrl = string.format("https://thumbnails.%s", _baseDomain) +local _baseAccountSettings = string.format("https://accountsettings.%s", _baseDomain) +local _basePremiumFeatures = string.format("https://premiumfeatures.%s", _baseDomain) +local _baseLocale = string.format("https://locale.%s", _baseDomain) +local _baseBadgesUrl = string.format("https://badges.%s", _baseDomain) +local _baseMetricsUrl = string.format("https://metrics.%sv1", _baseDomain) +local _baseApisRcsUrl = string.format("https://apis.rcs.%s", _baseDomain) +local _baseDiscussionsUrl = string.format("https://discussions.%s", _baseDomain) +local _baseContactsUrl = string.format("https://contacts.%s", _baseDomain) +local _baseSearchUrl = string.format("https://search.%s", _baseDomain) +local _baseStaticUrl = string.format("https://static.%s", _baseDomain) +local _baseGameSearchUITreatments = string.format("https://gamesearchuitreatments.api.%s", _baseDomain) +local _baseEconomyCreatorStats = FFlagLuaFixEconomyCreatorStatsUrl + and string.format("https://economycreatorstats.%s", _baseDomain) + or string.format("https://economycreatorstats.api.%s", _baseDomain) +local _baseUrlSecure = string.gsub(_baseUrl, "http://", "https://") + +-- public api +local Url = { + DOMAIN = _baseDomain, + PREFIX = _basePrefix, + BASE_URL = _baseUrl, + BASE_URL_SECURE = _baseUrlSecure, + API_URL = _baseApiUrl, + APIS_URL = _baseApisUrl, + AUTH_URL = _baseAuthUrl, + ACCOUNT_SETTINGS_URL = _baseAccountSettingsUrl, + AVATAR_URL = _baseAvatarUrl, + CATALOG_URL = _baseCatalogUrl, + INVENTORY_URL = _baseInventoryUrl, + GAME_URL = _baseGamesUrl, + GAME_ASSET_URL = _baseGameAssetUrl, + GROUPS_URL = _baseGroupsUrl, + CHAT_URL = _baseChatUrl, + FRIEND_URL = _baseFriendUrl, + PRESENCE_URL = _basePresenceUrl, + NOTIFICATION_URL = _baseNotificationUrl, + REALTIME_URL = _baseRealtimeUrl, + WEB_URL = _baseWebUrl, + WWW_URL = _baseWwwUrl, + ADS_URL = _baseAdsUrl, + SEARCH_URL = _baseSearchUrl, + GAME_SEARCH_UI_TREATMENTS = _baseGameSearchUITreatments, + FOLLOWINGS_URL = _baseFollowingsUrl, + ECONOMY_URL = _baseEconomyUrl, + THUMBNAILS_URL = _baseThumbnailsUrl, + BADGES_URL = _baseBadgesUrl, + ACCOUNT_SETTINGS = _baseAccountSettings, + PREMIUM_FEATURES = _basePremiumFeatures, + LOCALE = _baseLocale, + METRICS_URL = _baseMetricsUrl, + APIS_RCS_URL = _baseApisRcsUrl, + DISCUSSIONS_URL = _baseDiscussionsUrl, + CONTACTS_URL = _baseContactsUrl, + STATIC_URL = _baseStaticUrl, + BLOG_URL = "https://blog.roblox.com/", + CORP_URL = "https://corp.roblox.com/", + ECNOMY_CREATOR_STATS = _baseEconomyCreatorStats, +} + +function Url:getUserProfileUrl(userId) + return string.format("%susers/%s/profile", self.BASE_URL, userId) +end + +function Url:getUserFriendsUrl(userId) + return string.format("%susers/%s/friends", self.BASE_URL, userId) +end + +function Url:getUserInventoryUrl(userId) + return string.format("%susers/%s/inventory", self.BASE_URL, userId) +end + +function Url:getPlaceDefaultThumbnailUrl(placeId, width, height) + return string.format( + "%sThumbs/Asset.ashx?width=%d&height=%d&assetId=%s&ignorePlaceMediaItems=true", + self.BASE_URL, + width, + height, + tostring(placeId)) +end + +function Url:isVanitySite() + return self.PREFIX ~= "www" +end + +-- data - (table) a table of key/value pairs to format +function Url:makeQueryString(data) + --NOTE - This function can be used to create a query string of parameters + -- at the end of url query, or create a application/form-url-encoded post body string + local params = {} + + -- NOTE - Arrays are handled, but generally data is expected to be flat. + for key, value in pairs(data) do + if value ~= nil then --for optional params + if type(value) == "table" then + for i = 1, #value do + table.insert(params, key .. "=" .. value[i]) + end + else + table.insert(params, key .. "=" .. tostring(value)) + end + end + end + + return table.concat(params, "&") +end + + +-- prevent anyone from modifying this table: +Url = createReadOnlyTable(Url) + +return Url diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Url.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Url.spec.lua new file mode 100644 index 0000000..62b8fab --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Http/Url.spec.lua @@ -0,0 +1,12 @@ +return function() + + local ContentProvider = game:GetService("ContentProvider") + local Url = require(script.Parent.Url) + + it("The base url has not been changed for debugging", function() + local baseUrl = ContentProvider.BaseUrl + + expect(baseUrl).to.equal(Url.BASE_URL) + end) + +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/MockId.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/MockId.lua new file mode 100644 index 0000000..92671ed --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/MockId.lua @@ -0,0 +1,17 @@ +--[[ + A function to return a fake ID, used for testing. + + We turn all IDs into strings as we typically use them as keys in the state. + It's better to use a string than a number, because a number would indicate + an array index. + + Roblox APIs expect to be given integers for IDs however, so just tonumber() + the ID in this case. +]] + +local lastId = 0 + +return function() + lastId = lastId + 1 + return tostring(lastId) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/.robloxrc new file mode 100644 index 0000000..fc3d643 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/.robloxrc @@ -0,0 +1,5 @@ +{ + "language": { + "mode": "nonstrict" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/Thumbnail.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/Thumbnail.lua new file mode 100644 index 0000000..27d4970 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/Thumbnail.lua @@ -0,0 +1,39 @@ +--[[ + { + universeId : string, + state : string, + url : string, + } +]] + +local Thumbnail = {} + +function Thumbnail.new() + local self = {} + + return self +end + +function Thumbnail.fromThumbnailData(thumbnailData, size) + local self = Thumbnail.new() + + self.universeId = tostring(thumbnailData.targetId) + self.state = thumbnailData.state + self.url = thumbnailData.imageUrl + self.size = size + + return self +end + +function Thumbnail.isCompleteThumbnailData(thumbnailData) + return type(thumbnailData) == "table" + and type(thumbnailData.targetId) == "number" + and type(thumbnailData.state) == "string" + and (type(thumbnailData.imageUrl) == "string" or thumbnailData.imageUrl == nil) +end + +function Thumbnail.checkStateIsFinal(thumbnailState) + return thumbnailState ~= "Pending" +end + +return Thumbnail \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/Thumbnail.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/Thumbnail.spec.lua new file mode 100644 index 0000000..d4c144d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/Thumbnail.spec.lua @@ -0,0 +1,20 @@ +return function() + local Thumbnail = require(script.Parent.Thumbnail) + + it("should set fields without errors", function() + local testData = + { + targetId = 123456, + state = "Completed", + imageUrl = "a url", + } + + local thumbnail = Thumbnail.fromThumbnailData(testData) + + expect(thumbnail).to.be.a("table") + expect(thumbnail.universeId).to.equal("123456") + expect(thumbnail.state).to.equal("Completed") + expect(thumbnail.url).to.equal("a url") + end) + +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/ThumbnailRequest.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/ThumbnailRequest.lua new file mode 100644 index 0000000..6df561f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/ThumbnailRequest.lua @@ -0,0 +1,18 @@ +local ThumbnailRequest = {} + +function ThumbnailRequest.new() + local self = {} + + return self +end + +function ThumbnailRequest.fromData(thumbnailType, thumbnailSize) + local self = ThumbnailRequest.new() + + self.thumbnailType = thumbnailType + self.thumbnailSize = thumbnailSize + + return self +end + +return ThumbnailRequest \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/User.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/User.lua new file mode 100644 index 0000000..9eb9ce2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/User.lua @@ -0,0 +1,134 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId) + +local User = {} + +User.PresenceType = { + OFFLINE = "OFFLINE", + ONLINE = "ONLINE", + IN_GAME = "IN_GAME", + IN_STUDIO = "IN_STUDIO", +} + +function User.new() + local self = {} + + return self +end + +function User.mock() + local self = User.new() + + self.id = MockId() + + self.isFetching = false + self.isFriend = false + self.lastLocation = nil + self.name = "USER NAME" + self.universeId = nil + self.placeId = nil + self.rootPlaceId = nil + self.gameInstanceId = nil + self.presence = User.PresenceType.OFFLINE + self.membership = nil + self.thumbnails = nil + self.lastOnline = nil + self.displayName = "DN+" .. self.name + + return self +end + +-- Note: Going forward, leverage User.fromDataTable() instead. +-- It accepts a more flexible parameter than User.fromData() and constructs the same User model +function User.fromData(id, name, isFriend) + local self = User.new() + + self.id = tostring(id) + + self.isFetching = false + self.isFriend = isFriend + self.lastLocation = nil + self.name = name + self.universeId = nil + self.placeId = nil + self.rootPlaceId = nil + self.gameInstanceId = nil + + self.presence = (Players.LocalPlayer and self.id == tostring(Players.LocalPlayer.UserId)) + and User.PresenceType.ONLINE or nil + self.thumbnails = nil + self.lastOnline = nil + + return self +end + +function User.fromDataTable(data) + local self = User.new() + + self.id = tostring(data.id) + self.isFriend = data.isFriend + self.presence = (Players.LocalPlayer + and self.id == tostring(Players.LocalPlayer.UserId)) and User.PresenceType.ONLINE or nil + self.isFetching = false + self.lastLocation = nil + self.name = data.name + self.displayName = data.displayName or data.name + self.universeId = nil + self.placeId = nil + self.rootPlaceId = nil + self.gameInstanceId = nil + self.thumbnails = nil + self.lastOnline = nil + + return self +end + +function User.compare(user1, user2) + assert(not(user1 == nil and user2 == nil)) + assert(user1 == nil or typeof(user1) == "table") + assert(user2 == nil or typeof(user2) == "table") + + -- Return false if any of the provided input is nil(empty). + if not user1 or not user2 then + return false + end + + for field, valueInUser2 in pairs(user2) do + if user1[field] ~= valueInUser2 then + return false + end + end + + for field, valueInUser1 in pairs(user1) do + if user2[field] ~= valueInUser1 then + return false + end + end + + return true +end + +function User.userPresenceToText(localization, user) + local presence = user.presence + local lastLocation = user.lastLocation + + if not presence then + return '' + end + + if presence == User.PresenceType.OFFLINE then + return localization:Format("Common.Presence.Label.Offline") + elseif presence == User.PresenceType.ONLINE then + return localization:Format("Common.Presence.Label.Online") + elseif (presence == User.PresenceType.IN_GAME) or (presence == User.PresenceType.IN_STUDIO) then + if lastLocation ~= nil then + return lastLocation + else + return localization:Format("Common.Presence.Label.Online") + end + end +end + +return User diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/User.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/User.spec.lua new file mode 100644 index 0000000..7f095fc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Models/User.spec.lua @@ -0,0 +1,98 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Immutable = require(CorePackages.AppTempCommon.Common.Immutable) + local User = require(CorePackages.AppTempCommon.LuaApp.Models.User) + + it("should detect if provided users are identical", function() + local clone1 = User.fromData(1, "Andy", true) + local clone2 = Immutable.Set(clone1, "isFriend", true) + + local result = User.compare(clone1, clone2) + expect(result).to.equal(true) + + result = User.compare(clone2, clone1) + expect(result).to.equal(true) + end) + + it("should detect when there is one or more fields with different values", function() + local andy = User.fromData(1, "Andy", true) + local ollie = Immutable.Set(andy, "name", "Ollie") + + local result = User.compare(andy, ollie) + expect(result).to.equal(false) + + result = User.compare(ollie, andy) + expect(result).to.equal(false) + end) + + it("should detect descrepancy when one user model contains more fields than the other", function() + local andy = User.fromData(1, "Andy", true) + local secretlyNotAndy = Immutable.Set(andy, "someDifferentField", "I'm Ollie!") + + local result = User.compare(andy, secretlyNotAndy) + expect(result).to.equal(false) + + result = User.compare(secretlyNotAndy, andy) + expect(result).to.equal(false) + end) + + it("should throw if invalid input is provided", function() + local aString = "I'm not a table." + local teddy = User.fromData(1, "Teddy", true) + + expect(function() User.compare(nil, nil) end).to.throw() + expect(function() User.compare(aString, nil) end).to.throw() + expect(function() User.compare(nil, aString) end).to.throw() + expect(function() User.compare(aString, aString) end).to.throw() + expect(function() User.compare(teddy, aString) end).to.throw() + expect(function() User.compare(aString, teddy) end).to.throw() + end) + + it("should return false if any one of the input is empty or nil)", function() + local emptyTable = {} + local teddy = User.fromData(1, "Teddy", true) + + local result = User.compare(teddy, nil) + expect(result).to.equal(false) + + result = User.compare(nil, teddy) + expect(result).to.equal(false) + + result = User.compare(teddy, emptyTable) + expect(result).to.equal(false) + + result = User.compare(emptyTable, teddy) + expect(result).to.equal(false) + end) + + describe("fromDataTable", function() + it("should properly set user data", function() + local data = { + id = 1, + name = "FooBar", + displayName = "FooBar+DN", + isFriend = false, + } + local user = User.fromDataTable(data) + + expect(user.id).to.equal("1") + expect(user.name).to.equal("FooBar") + expect(user.displayName).to.equal("FooBar+DN") + expect(user.isFriend).to.equal(false) + end) + + it("should still set user data without a displayName property", function() + local data = { + id = 1, + name = "FooBar", + isFriend = false, + } + local user = User.fromDataTable(data) + + expect(user.id).to.equal("1") + expect(user.name).to.equal("FooBar") + expect(user.displayName).to.equal("FooBar") + expect(user.isFriend).to.equal(false) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/NetworkProfiler.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/NetworkProfiler.lua new file mode 100644 index 0000000..a0bb17b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/NetworkProfiler.lua @@ -0,0 +1,51 @@ +local percentReporting = tonumber(settings():GetFVariable("PercentReportingNetworkProfileAfterStartup")) + +local FEATURE_NAME = "NetworkProfileDuringStartup" +local QUEUED_MEASURE_NAME = "Queued" +local NAME_LOOKUP_MEASURE_NAME = "NameLookup" +local CONNECT_MEASURE_NAME = "Connect" +local SSL_HANDSHAKE_MEASURE_NAME = "SSLHandshake" +local MAKE_REQUEST_MEASURE_NAME = "MakeRequest" +local RECEIVE_RESPONSE_MEASURE_NAME = "ReceiveResponse" + +local NetworkProfiler = {} +NetworkProfiler.__index = NetworkProfiler + +NetworkProfiler.aggregate = { + queued = 0.0, + nameLookup = 0.0, + connect = 0.0, + sslHandshake = 0.0, + makeRequest = 0.0, + receiveResponse = 0.0, +} + +function NetworkProfiler:track(timeProfile) + self.aggregate.queued = self.aggregate.queued + timeProfile.queued + if timeProfile.nameLookup >= 0 then + self.aggregate.nameLookup = self.aggregate.nameLookup + timeProfile.nameLookup + end + if timeProfile.connect >= 0 then + self.aggregate.connect = self.aggregate.connect + timeProfile.connect + end + if timeProfile.sslHandshake >= 0 then + self.aggregate.sslHandshake = self.aggregate.sslHandshake + timeProfile.sslHandshake + end + if timeProfile.makeRequest >= 0 then + self.aggregate.makeRequest = self.aggregate.makeRequest + timeProfile.makeRequest + end + if timeProfile.receiveResponse >= 0 then + self.aggregate.receiveResponse = self.aggregate.receiveResponse + timeProfile.receiveResponse + end +end + +function NetworkProfiler:report(reportToDiag) + reportToDiag(FEATURE_NAME, QUEUED_MEASURE_NAME, self.aggregate.queued, percentReporting) + reportToDiag(FEATURE_NAME, NAME_LOOKUP_MEASURE_NAME, self.aggregate.nameLookup, percentReporting) + reportToDiag(FEATURE_NAME, CONNECT_MEASURE_NAME, self.aggregate.connect, percentReporting) + reportToDiag(FEATURE_NAME, SSL_HANDSHAKE_MEASURE_NAME, self.aggregate.sslHandshake, percentReporting) + reportToDiag(FEATURE_NAME, MAKE_REQUEST_MEASURE_NAME, self.aggregate.makeRequest, percentReporting) + reportToDiag(FEATURE_NAME, RECEIVE_RESPONSE_MEASURE_NAME, self.aggregate.receiveResponse, percentReporting) +end + +return NetworkProfiler \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Promise.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Promise.lua new file mode 100644 index 0000000..ac10217 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Promise.lua @@ -0,0 +1,8 @@ +----------------------------------------------------------------------------- +--- --- +--- Under Migration to CorePackages --- +--- --- +----------------------------------------------------------------------------- +local CorePackages = game:GetService("CorePackages") + +return require(CorePackages.Promise) diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/PromiseUtilities.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/PromiseUtilities.lua new file mode 100644 index 0000000..6aaa493 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/PromiseUtilities.lua @@ -0,0 +1,91 @@ +--[[ + Provides utility functions for Promises +]] + +local CorePackages = game:GetService("CorePackages") + +local Result = require(CorePackages.AppTempCommon.LuaApp.Result) +local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) + +local PromiseUtilities = {} + +--[[ + Accept a table of promises; + promises = { + [1] = Promise.resolve(), + ["Home"] = Promise.reject(), + ... + } + Returns a new promise that: + * is resolved when all input promises are finished. + returns the results of each individual promises in a list of Results + results = { + [1] = Result1, + ["Home"] = Result2, + ... + } + * is never rejected. +]] +function PromiseUtilities.Batch(promises) + assert(type(promises) == "table", "PromiseUtilities expects a list of Promises!") + + local numberOfPromises = 0 + + for _, promise in pairs(promises) do + assert(Promise.is(promise), "PromiseUtilities expects a list of Promises!") + numberOfPromises = numberOfPromises + 1 + end + + return Promise.new(function(resolve, reject) + local totalCompleted = 0 + local results = {} + + local function promiseCompleted(key, success, value) + results[key] = Result.new(success, value) + totalCompleted = totalCompleted + 1 + + if totalCompleted == numberOfPromises then + resolve(results) + end + end + + if next(promises) == nil then + resolve(results) + end + + for key, promise in pairs(promises) do + promise:andThen( + function(result, ...) + if select("#", ...) > 0 then + warn("Promises in PromiseUtilities.Batch should not return tuple") + end + promiseCompleted(key, true, result) + end, + function(reason) + promiseCompleted(key, false, reason) + end + ) + end + end) +end + +function PromiseUtilities.CountResults(batchPromiseResults) + local totalCount = 0 + local failureCount = 0 + + for _, result in pairs(batchPromiseResults) do + local success, _ = result:unwrap() + if not success then + failureCount = failureCount + 1 + end + totalCount = totalCount + 1 + end + + return { + successCount = totalCount - failureCount, + failureCount = failureCount, + totalCount = totalCount, + } +end + +return PromiseUtilities \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/PromiseUtilities.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/PromiseUtilities.spec.lua new file mode 100644 index 0000000..a3b4f7a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/PromiseUtilities.spec.lua @@ -0,0 +1,188 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local PromiseUtilities = require(CorePackages.AppTempCommon.LuaApp.PromiseUtilities) + local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) + local Result = require(CorePackages.AppTempCommon.LuaApp.Result) + local TableUtilities = require(CorePackages.AppTempCommon.LuaApp.TableUtilities) + + describe("PromiseUtilities.Batch", function() + it("should assert if input is not a list of Promises", function() + expect(function() + PromiseUtilities.Batch() + end).to.throw() + + expect(function() + PromiseUtilities.Batch(Promise.resolve(), Promise.resolve()) + end).to.throw() + + expect(function() + PromiseUtilities.Batch({ + Promise.resolve(), + "something else" + }) + end).to.throw() + end) + + it("should invoke the given resolve callback when all promises are finished", function() + local promises = { + [1] = Promise.resolve(), + ["Home"] = Promise.resolve() + } + local callCount = 0 + + local batchedPromise = PromiseUtilities.Batch(promises):andThen( + function() + callCount = callCount + 1 + end + ) + + expect(batchedPromise).to.be.ok() + expect(callCount).to.equal(1) + expect(batchedPromise._status).to.equal(Promise.Status.Resolved) + end) + + it("should not invoke any callbacks when one of the promises are not finished", function() + local promises = { + [1] = Promise.resolve(), + ["Home"] = Promise.new(function() end) + } + local callCount = 0 + + local batchedPromise = PromiseUtilities.Batch(promises):andThen( + function() + callCount = callCount + 1 + end, + + function() + callCount = callCount + 1 + end + ) + + expect(batchedPromise).to.be.ok() + expect(callCount).to.equal(0) + expect(batchedPromise._status).to.equal(Promise.Status.Started) + end) + + it("should return the correct results of each individual promise", function() + local promises = { + [1] = Promise.resolve(5), + ["Home"] = Promise.reject("failed") + } + local promiseResults = nil + + local batchedPromise = PromiseUtilities.Batch(promises):andThen( + function(results) + promiseResults = results + end + ) + + expect(batchedPromise).to.be.ok() + expect(batchedPromise._status).to.equal(Promise.Status.Resolved) + expect(TableUtilities.FieldCount(promiseResults)).to.equal(2) + + expect(Result.is(promiseResults[1])).to.equal(true) + local success1, value1 = promiseResults[1]:unwrap() + expect(success1).to.equal(true) + expect(value1).to.equal(5) + local isMatchCalled1 = false + promiseResults[1]:match(function(result) + expect(result).to.equal(5) + isMatchCalled1 = true + end, + function() + error("should not be called") + end) + expect(isMatchCalled1).to.equal(true) + + + expect(Result.is(promiseResults["Home"])).to.equal(true) + local success2, value2 = promiseResults["Home"]:unwrap() + expect(success2).to.equal(false) + expect(value2).to.equal("failed") + local isMatchCalled2 = false + promiseResults["Home"]:match(function() + error("should not be called") + end, + function(err) + expect(err).to.equal("failed") + isMatchCalled2 = true + end) + expect(isMatchCalled2).to.equal(true) + end) + + it("should return the correct results of each individual promise that resolved later", function() + local resolveLater + local rejectLater + + local promises = { + [1] = Promise.new(function(resolve) + resolveLater = resolve + end), + ["Home"] = Promise.new(function(_, reject) + rejectLater = reject + end) + } + local promiseResults = nil + + local batchedPromise = PromiseUtilities.Batch(promises):andThen( + function(results) + promiseResults = results + end + ) + + resolveLater(5) + rejectLater("failed") + + expect(batchedPromise).to.be.ok() + expect(batchedPromise._status).to.equal(Promise.Status.Resolved) + expect(TableUtilities.FieldCount(promiseResults)).to.equal(2) + + expect(Result.is(promiseResults[1])).to.equal(true) + local success1, value1 = promiseResults[1]:unwrap() + expect(success1).to.equal(true) + expect(value1).to.equal(5) + + expect(Result.is(promiseResults["Home"])).to.equal(true) + local success2, value2 = promiseResults["Home"]:unwrap() + expect(success2).to.equal(false) + expect(value2).to.equal("failed") + end) + + it("should resolve if given an empty list of promises", function() + local emptyPromises = {} + local callCount = 0 + + local batchedPromise = PromiseUtilities.Batch(emptyPromises):andThen( + function(results) + callCount = callCount + 1 + end + ) + + expect(batchedPromise).to.be.ok() + expect(callCount).to.equal(1) + expect(batchedPromise._status).to.equal(Promise.Status.Resolved) + end) + end) + + describe("PromiseUtilities.CountResults", function() + it("should count the results correctly", function() + local emptyResults = {} + + local countResult = PromiseUtilities.CountResults(emptyResults) + + expect(countResult).to.be.ok() + expect(countResult.successCount).to.equal(0) + expect(countResult.failureCount).to.equal(0) + expect(countResult.totalCount).to.equal(0) + + local promiseResults = { Result.success(0), Result.success(0), Result.error(1) } + + countResult = PromiseUtilities.CountResults(promiseResults) + + expect(countResult).to.be.ok() + expect(countResult.successCount).to.equal(2) + expect(countResult.failureCount).to.equal(1) + expect(countResult.totalCount).to.equal(3) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/CountryCode.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/CountryCode.lua new file mode 100644 index 0000000..ae6baf7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/CountryCode.lua @@ -0,0 +1,11 @@ +local CorePackages = game:GetService("CorePackages") +local Rodux = require(CorePackages.Rodux) + +local ReceivedUserCountryCode = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedUserCountryCode) + +local DEFAULT_STATE = "" +return Rodux.createReducer(DEFAULT_STATE, { + [ReceivedUserCountryCode.name] = function(state, action) + return action.countryCode + end, +}) diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/CountryCode.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/CountryCode.spec.lua new file mode 100644 index 0000000..30f319f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/CountryCode.spec.lua @@ -0,0 +1,30 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local ReceivedUserCountryCode = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedUserCountryCode) + local CountryCodeReducer = require(CorePackages.AppTempCommon.LuaApp.Reducers.CountryCode) + + describe("CountryCode", function() + it("should be and empty string by default", function() + local state = CountryCodeReducer(nil, {}) + + expect(state).to.equal("") + end) + + it("should not be modified by other actions", function() + local oldState = CountryCodeReducer(nil, {}) + local newState = CountryCodeReducer(oldState, { type = "not a real action" }) + + expect(newState).to.equal(oldState) + end) + + it("should be changed using ReceivedUserCountryCode", function() + local state = CountryCodeReducer(nil, {}) + + state = CountryCodeReducer(state, ReceivedUserCountryCode("US")) + expect(state).to.equal("US") + + state = CountryCodeReducer(state, ReceivedUserCountryCode("")) + expect(state).to.equal("") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/FetchingStatus.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/FetchingStatus.lua new file mode 100644 index 0000000..6481cff --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/FetchingStatus.lua @@ -0,0 +1,29 @@ +local CorePackages = game:GetService("CorePackages") + +local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus) +local Cryo = require(CorePackages.Cryo) + +return function(state, action) + state = state or {} + + if action.type == UpdateFetchingStatus.name then + local key = action.key + local status = action.status + local value + if status ~= nil then + value = status + else + value = Cryo.None + end + + state = Cryo.Dictionary.join( + state, + { + [key] = value, + } + ) + + end + + return state +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/FetchingStatus.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/FetchingStatus.spec.lua new file mode 100644 index 0000000..c3f9f36 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/FetchingStatus.spec.lua @@ -0,0 +1,52 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus) + local FetchingStatusReducer = require(CorePackages.AppTempCommon.LuaApp.Reducers.FetchingStatus) + local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus) + local TableUtilities = require(CorePackages.AppTempCommon.LuaApp.TableUtilities) + + local KEY_1 = "key_1" + local KEY_2 = "key_2" + + describe("FetchingStatus", function() + it("should be empty by default", function() + local state = FetchingStatusReducer(nil, {}) + + expect(TableUtilities.FieldCount(state)).to.equal(0) + end) + + it("should not be modified by other actions", function() + local oldState = FetchingStatusReducer(nil, {}) + local newState = FetchingStatusReducer(oldState, { type = "not a real action" }) + + expect(newState).to.equal(oldState) + end) + + it("should be changed using UpdateFetchingStatus", function() + local state = FetchingStatusReducer(nil, {}) + + state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, RetrievalStatus.Fetching)) + expect(state[KEY_1]).to.equal(RetrievalStatus.Fetching) + + state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, RetrievalStatus.Failed)) + expect(state[KEY_1]).to.equal(RetrievalStatus.Failed) + end) + + it("should store different values for different keys", function() + local state = FetchingStatusReducer(nil, {}) + + state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, RetrievalStatus.Failed)) + state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_2, RetrievalStatus.Done)) + + expect(state[KEY_1]).to.equal(RetrievalStatus.Failed) + expect(state[KEY_2]).to.equal(RetrievalStatus.Done) + end) + + it("should clear values for nil keys", function() + local state = { [KEY_1] = RetrievalStatus.Fetching } + + state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, nil)) + expect(state[KEY_1]).to.equal(nil) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Friends.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Friends.lua new file mode 100644 index 0000000..06305fe --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Friends.lua @@ -0,0 +1,43 @@ +local CorePackages = game:GetService("CorePackages") + +local Immutable = require(CorePackages.AppTempCommon.Common.Immutable) +local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus) + +local FetchUserFriendsStarted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsStarted) +local FetchUserFriendsFailed = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsFailed) +local FetchUserFriendsCompleted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsCompleted) + +local function setFieldPerUser(state, fieldName, userId, value) + local field = state[fieldName] or {} + return Immutable.JoinDictionaries(state, { + [fieldName] = Immutable.JoinDictionaries(field, { + [userId] = value + }) + }) +end + +local function setRetrievalStatus(state, userId, status) + return setFieldPerUser(state, "retrievalStatus", userId, status) +end + +local function setRetrievalFailureResponse(state, userId, response) + return setFieldPerUser(state, "retrievalFailureResponse", userId, response) +end + +return function(state, action) + state = state or { + retrievalStatus = {}, + retrievalFailureResponse = {}, + } + + if action.type == FetchUserFriendsStarted.name then + state = setRetrievalStatus(state, action.userId, RetrievalStatus.Fetching) + elseif action.type == FetchUserFriendsFailed.name then + state = setRetrievalStatus(state, action.userId, RetrievalStatus.Failed) + state = setRetrievalFailureResponse(state, action.userId, action.response) + elseif action.type == FetchUserFriendsCompleted.name then + state = setRetrievalStatus(state, action.userId, RetrievalStatus.Done) + end + + return state +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/UniversePlaceInfos.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/UniversePlaceInfos.lua new file mode 100644 index 0000000..2cfdb55 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/UniversePlaceInfos.lua @@ -0,0 +1,21 @@ +local CorePackages = game:GetService("CorePackages") + +local Immutable = require(CorePackages.AppTempCommon.Common.Immutable) +local ReceivedPlacesInfos = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedPlacesInfos) + +local LuaAppFlags = CorePackages.AppTempCommon.LuaApp.Flags +local convertUniverseIdToString = require(LuaAppFlags.ConvertUniverseIdToString) + +return function(state, action) + state = state or {} + + if action.type == ReceivedPlacesInfos.name then + for _, placeInfo in pairs(action.placesInfos) do + local universeId = convertUniverseIdToString(placeInfo.universeId) + + state = Immutable.Set(state, universeId, placeInfo) + end + end + + return state +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.lua new file mode 100644 index 0000000..374ca8a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.lua @@ -0,0 +1,107 @@ +local CorePackages = game:GetService("CorePackages") + +local Immutable = require(CorePackages.AppTempCommon.Common.Immutable) + +local AddUser = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUser) +local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers) +local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence) +local RemoveUser = require(CorePackages.AppTempCommon.LuaApp.Actions.RemoveUser) +local SetUserIsFriend = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserIsFriend) +local SetUserMembershipType = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserMembershipType) +local SetUserPresence = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserPresence) +local SetUserThumbnail = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserThumbnail) +local ReceivedDisplayName = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedDisplayName) + +return function(state, action) + state = state or {} + + if action.type == AddUser.name then + local user = action.user + state = Immutable.Set(state, user.id, user) + elseif action.type == AddUsers.name then + local addedUsers = action.users + local usersUpdate = {} + for userId, addedUser in pairs(addedUsers) do + local existingUser = state[userId] + if existingUser then + usersUpdate[userId] = Immutable.JoinDictionaries(existingUser, addedUser) + else + usersUpdate[userId] = addedUser + end + end + + state = Immutable.JoinDictionaries(state, usersUpdate) + + elseif action.type == SetUserIsFriend.name then + local user = state[action.userId] + if user then + local newUser = Immutable.Set(user, "isFriend", action.isFriend) + state = Immutable.Set(state, user.id, newUser) + else + warn("Setting isFriend on user", action.userId, "who doesn't exist yet") + end + elseif action.type == SetUserPresence.name then + local user = state[action.userId] + if user then + local newUser = Immutable.JoinDictionaries(user, { + presence = action.presence, + lastLocation = action.lastLocation, + }) + state = Immutable.Set(state, user.id, newUser) + else + warn("Setting presence on user", action.userId, "who doesn't exist yet") + end + elseif action.type == ReceivedUserPresence.name then + local user = state[action.userId] + if user then + state = Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(user, { + presence = action.presence, + lastLocation = action.lastLocation, + placeId = action.placeId, + rootPlaceId = action.rootPlaceId, + gameInstanceId = action.gameInstanceId, + lastOnline = action.lastOnline, + universeId = action.universeId, + }), + }) + end + elseif action.type == SetUserThumbnail.name then + local user = state[action.userId] + if user then + local thumbnails = user.thumbnails or {} + state = Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(user, { + thumbnails = Immutable.JoinDictionaries(thumbnails, { + [action.thumbnailType] = Immutable.JoinDictionaries(thumbnails[action.thumbnailType] or {}, { + [action.thumbnailSize] = action.image, + }), + }), + }), + }) + end + elseif action.type == SetUserMembershipType.name then + local user = state[action.userId] + if user then + state = Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(user, { + membership = action.membershipType, + }), + }) + end + elseif action.type == RemoveUser.name then + if state[action.userId] then + state = Immutable.RemoveFromDictionary(state, action.userId) + end + elseif action.type == ReceivedDisplayName.name then + local user = state[action.userId] + if user then + state = Immutable.JoinDictionaries(state, { + [action.userId] = Immutable.JoinDictionaries(user, { + displayName = action.displayName, + }), + }) + end + end + return state +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.spec.lua new file mode 100644 index 0000000..4cda7ab --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Reducers/Users.spec.lua @@ -0,0 +1,106 @@ +return function() + local CorePackages = game:GetService("CorePackages") + + local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId) + local User = require(CorePackages.AppTempCommon.LuaApp.Models.User) + local Users = require(CorePackages.AppTempCommon.LuaApp.Reducers.Users) + + local AddUser = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUser) + local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence) + local SetUserIsFriend = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserIsFriend) + local SetUserMembershipType = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserMembershipType) + local SetUserPresence = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserPresence) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = Users(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("AddUser", function() + it("should add a user to the store", function() + local user = User.mock() + local state = {} + + state = Users(state, AddUser(user)) + + expect(state[user.id]).to.equal(user) + end) + end) + + describe("SetUserIsFriend", function() + it("should set isFriend on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + expect(state[user.id].isFriend).to.equal(false) + + state = Users(state, SetUserIsFriend(user.id, true)) + expect(state[user.id].isFriend).to.equal(true) + + state = Users(state, SetUserIsFriend(user.id, false)) + expect(state[user.id].isFriend).to.equal(false) + end) + end) + + describe("SetUserPresence", function() + it("should set presence on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + expect(state[user.id].presence).to.equal(User.PresenceType.OFFLINE) + + state = Users(state, SetUserPresence(user.id, User.PresenceType.ONLINE)) + expect(state[user.id].presence).to.equal(User.PresenceType.ONLINE) + + state = Users(state, SetUserPresence(user.id, User.PresenceType.IN_GAME)) + expect(state[user.id].presence).to.equal(User.PresenceType.IN_GAME) + + state = Users(state, SetUserPresence(user.id, User.PresenceType.IN_STUDIO)) + expect(state[user.id].presence).to.equal(User.PresenceType.IN_STUDIO) + end) + end) + + describe("ReceivedUserPresence", function() + it("should set presence on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + local existingPresence = user.presence + local newPresence = 'ONLINE' + local lastLocation = MockId() + local newPlaceId = MockId() + + state = Users(state, ReceivedUserPresence(user.id, newPresence, lastLocation, newPlaceId)) + + expect(user.presence).to.equal(existingPresence) + expect(state[user.id].presence).to.equal(newPresence) + expect(state[user.id].lastLocation).to.equal(lastLocation) + expect(state[user.id].placeId).to.equal(newPlaceId) + end) + end) + + describe("SetUserMembershipType", function() + it("should set membership on an existing user", function() + local user = User.mock() + local state = { + [user.id] = user + } + + local existingMembership = user.membership + local newMembership = Enum.MembershipType.BuildersClub + + state = Users(state, SetUserMembershipType(user.id, newMembership)) + + expect(user.membership).to.equal(existingMembership) + expect(state[user.id].membership).to.equal(newMembership) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Result.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Result.lua new file mode 100644 index 0000000..c27382f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Result.lua @@ -0,0 +1,8 @@ +----------------------------------------------------------------------------- +--- --- +--- Under Migration to CorePackages --- +--- --- +----------------------------------------------------------------------------- +local CorePackages = game:GetService("CorePackages") + +return require(CorePackages.Result) diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/AppStyleProvider.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/AppStyleProvider.lua new file mode 100644 index 0000000..0062238 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/AppStyleProvider.lua @@ -0,0 +1,34 @@ +--[[ + The is a wrapper for the style provider for apps. + props: + style : table - Includes the name of the theme and font being used. + { + themeName : string - The name of the theme being used. + fontName : string - The name of the font being used. + } +]] +local CorePackages = game:GetService("CorePackages") +local ArgCheck = require(CorePackages.ArgCheck) +local Roact = require(CorePackages.Roact) +local UIBlox = require(CorePackages.UIBlox) +local StyleProvider = UIBlox.Style.Provider +local StylePalette = require(script.Parent.StylePalette) + +local AppStyleProvider = Roact.Component:extend("AppStyleProvider") + +function AppStyleProvider:render() + local style = self.props.style + ArgCheck.isNotNil(style, "style prop for AppStyleProvider") + local themeName = style.themeName + local fontName = style.fontName + local stylePalette = StylePalette.new() + stylePalette:updateTheme(themeName) + stylePalette:updateFont(fontName) + local appStyle = stylePalette:currentStyle() + + return Roact.createElement(StyleProvider,{ + style = appStyle, + }, self.props[Roact.Children]) +end + +return AppStyleProvider \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/AppStyleProvider.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/AppStyleProvider.spec.lua new file mode 100644 index 0000000..4bb0c50 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/AppStyleProvider.spec.lua @@ -0,0 +1,32 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local AppStyleProvider = require(script.Parent.AppStyleProvider) + local Constants = require(script.Parent.Constants) + local appStyle = { + themeName = Constants.ThemeName.Dark, + fontName = Constants.FontName.Gotham, + } + it("should create and destroy without errors", function() + local element = Roact.createElement("Frame") + local appStyleProvider = Roact.createElement(AppStyleProvider, { + style = appStyle, + },{ + Element = element, + }) + + local instance = Roact.mount(appStyleProvider) + Roact.unmount(instance) + end) + + it("should throw when style prop is nil", function() + local element = Roact.createElement("Frame") + local appStyleProvider = Roact.createElement(AppStyleProvider, {},{ + Element = element, + }) + expect(function() + local instance = Roact.mount(appStyleProvider) + Roact.unmount(instance) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Colors.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Colors.lua new file mode 100644 index 0000000..92e5ab1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Colors.lua @@ -0,0 +1,24 @@ +local Colors = { + --Common colors + Black = Color3.fromRGB(0, 0, 0), + White = Color3.fromRGB(255, 255, 255), + Green = Color3.fromRGB(0, 176, 111), + Red = Color3.fromRGB(247, 75, 82), + + --Dark theme colors + Carbon = Color3.fromRGB(31, 33, 35), + Flint = Color3.fromRGB(57, 59, 61), + Graphite = Color3.fromRGB(101, 102, 104), + Obsidian = Color3.fromRGB(24, 25, 27), + Pumice = Color3.fromRGB(189, 190, 190), + Slate = Color3.fromRGB(35, 37, 39), + + --Light theme colors + Alabaster = Color3.fromRGB(242, 244, 245), + Ash = Color3.fromRGB(234, 237, 239), + Chalk = Color3.fromRGB(216, 219, 222), + Smoke = Color3.fromRGB(96, 97, 98), + XboxBlue = Color3.fromRGB(17, 139, 211), +} + +return Colors \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Constants.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Constants.lua new file mode 100644 index 0000000..9a373a4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Constants.lua @@ -0,0 +1,12 @@ +local Constants = {} + +Constants.ThemeName = { + Dark = "dark", + Light = "light", +} + +Constants.FontName = { + Gotham = "gotham", +} + +return Constants \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/Gotham.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/Gotham.lua new file mode 100644 index 0000000..b239e96 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/Gotham.lua @@ -0,0 +1,54 @@ +local baseSize = 16 +-- Nominal size conversion +-- https://confluence.rbx.com/display/PX/Font+Metrics +local nominalSizeFactor = 1.2 +local font = { + BaseSize = baseSize * nominalSizeFactor, + Title = { + Font = Enum.Font.GothamBlack, + RelativeSize = 32 / baseSize, + RelativeMinSize = 24 / baseSize, + }, + Header1 = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 20 / baseSize, + RelativeMinSize = 16 / baseSize, + }, + Header2 = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 16 / baseSize, + RelativeMinSize = 12 / baseSize, + }, + SubHeader1 = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 16 / baseSize, + RelativeMinSize = 12 / baseSize, + }, + Body = { + Font = Enum.Font.Gotham, + RelativeSize = 16 / baseSize, + RelativeMinSize = 12 / baseSize, + }, + CaptionHeader = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 12 / baseSize, + RelativeMinSize = 9 / baseSize, + }, + CaptionSubHeader = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 12 / baseSize, + RelativeMinSize = 9 / baseSize, + }, + CaptionBody = { + Font = Enum.Font.Gotham, + RelativeSize = 12 / baseSize, + RelativeMinSize = 9 / baseSize, + }, + Footer = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 10 / baseSize, + RelativeMinSize = 8 / baseSize, + }, +} + +return font \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/Gotham.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/Gotham.spec.lua new file mode 100644 index 0000000..0c31bf2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/Gotham.spec.lua @@ -0,0 +1,9 @@ +return function() + it("should be valid font palette without errors", function() + local CorePackages = game:GetService("CorePackages") + local UIBlox = require(CorePackages.UIBlox) + local validateFont = UIBlox.Style.Validator.validateFont + local Gotham = require(script.Parent.Gotham) + assert(validateFont(Gotham)) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/getFontFromName.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/getFontFromName.lua new file mode 100644 index 0000000..9dff393 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/getFontFromName.lua @@ -0,0 +1,20 @@ +local CorePackages = game:GetService("CorePackages") +local ArgCheck = require(CorePackages.ArgCheck) +local Logging = require(CorePackages.Logging) +local UIBlox = require(CorePackages.UIBlox) +local validateFont = UIBlox.Style.Validator.validateFont + +return function (fontName, defaultFont, fontMap) + local mappedFont + if fontName ~= nil and #fontName > 0 then + mappedFont = fontMap[string.lower(fontName)] + end + + if mappedFont == nil then + mappedFont = fontMap[defaultFont] + Logging.warn(string.format("Unrecognized font name: `%s`", tostring(fontName))) + end + + ArgCheck.assert(validateFont(mappedFont)) + return mappedFont +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/getFontFromName.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/getFontFromName.spec.lua new file mode 100644 index 0000000..a8c2b7a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Fonts/getFontFromName.spec.lua @@ -0,0 +1,33 @@ +return function() + local getFontFromName = require(script.Parent.getFontFromName) + local Constants = require(script.Parent.Parent.Constants) + it("should be able to get a font palette without errors", function() + local fontMap = { + [Constants.FontName.Gotham] = require(script.Parent.Gotham), + } + local fontTable = getFontFromName(Constants.FontName.Gotham, Constants.FontName.Gotham, fontMap) + expect(fontTable).to.be.a("table") + end) + + it("should be able to get a font palette using default without errors", function() + local fontMap = { + [Constants.FontName.Gotham] = require(script.Parent.Gotham), + } + local fontTable = getFontFromName("sourceSans", Constants.FontName.Gotham, fontMap) + expect(fontTable).to.be.a("table") + end) + + it("should throw the font palette is invalid", function() + expect(function() + local fontMap = { + [Constants.FontName.Gotham] = { + Font = { + Font = Enum.Font.Gotham, + RelativeSize = 1, + }, + }, + } + getFontFromName(Constants.FontName.Gotham, Constants.FontName.Gotham, fontMap) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/StylePalette.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/StylePalette.lua new file mode 100644 index 0000000..39d70de --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/StylePalette.lua @@ -0,0 +1,49 @@ +local getThemeFromName = require(script.Parent.Themes.getThemeFromName) +local getFontFromName = require(script.Parent.Fonts.getFontFromName) +local Constants = require(script.Parent.Constants) + +local StylePalette = {} +StylePalette.__index = StylePalette + +local DEFAULT_FONT = Constants.FontName.Gotham +local FONT_MAP = { + [Constants.FontName.Gotham] = require(script.Parent.Fonts.Gotham), +} + +local DEFAULT_THEME = Constants.ThemeName.Light +local THEME_MAP = { + [Constants.ThemeName.Dark] = require(script.Parent.Themes.DarkTheme), + [Constants.ThemeName.Light] = require(script.Parent.Themes.LightTheme), +} + +function StylePalette.new(style) + --By default a new style will be empty. + -- This will allow the font and theme to be merged independently even when one is empty. + local self = {} + + if style ~= nil then + self.Font = style.Font + self.Theme = style.Theme + end + + setmetatable(self, StylePalette) + return self +end + +function StylePalette:updateFont(fontName) + self.Font = getFontFromName(fontName, DEFAULT_FONT, FONT_MAP) +end + +function StylePalette:updateTheme(themeName) + self.Theme = getThemeFromName(themeName, DEFAULT_THEME, THEME_MAP) +end + +function StylePalette:currentStyle() + local style = { + Font = self.Font, + Theme = self.Theme, + } + return style +end + +return StylePalette \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/StylePalette.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/StylePalette.spec.lua new file mode 100644 index 0000000..94e6891 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/StylePalette.spec.lua @@ -0,0 +1,58 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local UIBlox = require(CorePackages.UIBlox) + local StylePalette = require(script.Parent.StylePalette) + local validateStye = UIBlox.Style.Validator.validateStyle + + it("should be able to create a style palette", function() + local stylePalette = StylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + end) + + it("should be able to create a style palette and be able to update theme 1", function() + local stylePalette = StylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + + stylePalette:updateTheme("light") + local newAppStyle = stylePalette:currentStyle() + expect(validateStye(newAppStyle)).equal(true) + end) + + it("should be able to create a style palette and be able to update theme 2", function() + local stylePalette = StylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + + stylePalette:updateFont("gotham") + local newAppStyle = stylePalette:currentStyle() + expect(validateStye(newAppStyle)).equal(true) + end) + + it("should be able to create a style palette and be able to merge an old one in", function() + local stylePalette = StylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + + local newstylePalette = StylePalette.new(stylePalette) + local newAppStyle = newstylePalette:currentStyle() + expect(validateStye(newAppStyle)).equal(true) + end) + + it("should be able to create a empty style palette", function() + local stylePalette = StylePalette.new() + local appStyle = stylePalette:currentStyle() + expect(appStyle.Font).equal(nil) + expect(appStyle.Theme).equal(nil) + expect(validateStye(appStyle)).equal(false) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.lua new file mode 100644 index 0000000..459e4c2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.lua @@ -0,0 +1,167 @@ +local ThemesRoot = script.Parent +local StylesRoot = ThemesRoot.Parent +local Colors = require(StylesRoot.Colors) + +local theme = { + BackgroundDefault = { + Color = Colors.Slate, + Transparency = 0, + }, + BackgroundContrast = { + Color = Colors.Carbon, + Transparency = 0, + }, + BackgroundMuted = { + Color = Colors.Obsidian, + Transparency = 0, + }, + BackgroundUIDefault = { + Color = Colors.Flint, + Transparency = 0, + }, + BackgroundUIContrast = { + Color = Colors.Black, + Transparency = 0.3, -- Alpha 0.7 + }, + BackgroundOnHover = { + Color = Colors.White, + Transparency = 0.9, -- Alpha 0.1 + }, + BackgroundOnPress = { + Color = Colors.Black, + Transparency = 0.7, -- Alpha 0.3 + }, + + UIDefault = { + Color = Colors.Graphite, + Transparency = 0, + }, + UIMuted = { + Color = Colors.Obsidian, + Transparency = 0.2, -- Alpha 0.8 + }, + UIEmphasis = { + Color = Colors.White, + Transparency = 0.7, -- Alpha 0.3 + }, + + ContextualPrimaryDefault = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryOnHover = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryContent = { + Color = Colors.White, + Transparency = 0, + }, + + SystemPrimaryDefault = { + Color = Colors.White, + Transparency = 0, + }, + SystemPrimaryOnHover = { + Color = Colors.White, + Transparency = 0, + }, + SystemPrimaryContent = { + Color = Colors.Flint, + Transparency = 0, + }, + + SecondaryDefault = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + SecondaryOnHover = { + Color = Colors.White, + Transparency = 0, + }, + SecondaryContent = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + + IconDefault = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 alpha + }, + IconEmphasis = { + Color = Colors.White, + Transparency = 0, + }, + IconOnHover = { + Color = Colors.White, + Transparency = 0, + }, + + TextEmphasis = { + Color = Colors.White, + Transparency = 0, + }, + TextDefault = { + Color = Colors.Pumice, + Transparency = 0, + }, + TextMuted = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + + Divider = { + Color = Colors.White, + Transparency = 0.8, -- 0.2 Alpha + }, + Overlay = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + DropShadow = { + Color = Colors.Black, + Transparency = 0, + }, + NavigationBar = { + Color = Colors.Carbon, + Transparency = 0, + }, + PlaceHolder = { + Color = Colors.Flint, + Transparency = 0.5, -- 0.5 Alpha + }, + + OnlineStatus = { + Color = Colors.Green, + Transparency = 0, + }, + OfflineStatus = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + + Success = { + Color = Colors.Green, + Transparency = 0, + }, + Alert = { + Color = Colors.Red, + Transparency = 0, + }, + + Badge = { + Color = Colors.White, + Transparency = 0, + }, + BadgeContent = { + Color = Colors.Flint, + Transparency = 0, + }, + + SelectionCursor = { + Color = Colors.White, + Transparency = 0, + }, +} + +return theme \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.spec.lua new file mode 100644 index 0000000..8f23083 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.spec.lua @@ -0,0 +1,9 @@ +return function() + it("should be a valid theme palette.", function() + local CorePackages = game:GetService("CorePackages") + local UIBlox = require(CorePackages.UIBlox) + local validateTheme = UIBlox.Style.Validator.validateTheme + local DarkTheme = require(script.Parent.DarkTheme) + assert(validateTheme(DarkTheme)) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.lua new file mode 100644 index 0000000..1295406 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.lua @@ -0,0 +1,168 @@ +local ThemesRoot = script.Parent +local StylesRoot = ThemesRoot.Parent + +local Colors = require(StylesRoot.Colors) + +local theme = { + BackgroundDefault = { + Color = Colors.Alabaster, + Transparency = 0, + }, + BackgroundContrast = { + Color = Colors.Ash, + Transparency = 0, + }, + BackgroundMuted = { + Color = Colors.Chalk, + Transparency = 0, + }, + BackgroundUIDefault = { + Color = Colors.White, + Transparency = 0, + }, + BackgroundUIContrast = { + Color = Colors.White, + Transparency = 0.1, -- Alpha 0.9 + }, + BackgroundOnHover = { + Color = Colors.Black, + Transparency = 0.9, -- Alpha 0.1 + }, + BackgroundOnPress = { + Color = Colors.Black, + Transparency = 0.9, -- Alpha 0.1 + }, + + UIDefault = { + Color = Colors.Pumice, + Transparency = 0, + }, + UIMuted = { + Color = Colors.Black, + Transparency = 0.9, -- Alpha 0.1 + }, + UIEmphasis = { + Color = Colors.Black, + Transparency = 0.7, -- Alpha 0.3 + }, + + ContextualPrimaryDefault = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryOnHover = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryContent = { + Color = Colors.White, + Transparency = 0, + }, + + SystemPrimaryDefault = { + Color = Colors.Flint, + Transparency = 0, + }, + SystemPrimaryOnHover = { + Color = Colors.Flint, + Transparency = 0, + }, + SystemPrimaryContent = { + Color = Colors.White, + Transparency = 0, + }, + + SecondaryDefault = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + SecondaryOnHover = { + Color = Colors.Flint, + Transparency = 0, + }, + SecondaryContent = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + + IconDefault = { + Color = Colors.Black, + Transparency = 0.4, -- 0.6 alpha + }, + IconEmphasis = { + Color = Colors.Flint, + Transparency = 0, + }, + IconOnHover = { + Color = Colors.Flint, + Transparency = 0, + }, + + TextEmphasis = { + Color = Colors.Flint, + Transparency = 0, + }, + TextDefault = { + Color = Colors.Smoke, + Transparency = 0, + }, + TextMuted = { + Color = Colors.Black, + Transparency = 0.4, -- 0.6 Alpha + }, + + Divider = { + Color = Colors.Pumice, + Transparency = 0, + }, + Overlay = { + Color = Colors.Black, + Transparency = 0.7, -- 0.3 Alpha + }, + DropShadow = { + Color = Colors.Black, + Transparency = 0, + }, + NavigationBar = { + Color = Colors.White, + Transparency = 0, + }, + PlaceHolder = { + Color = Colors.Chalk, + Transparency = 0.3, -- 0.7 Alpha + }, + + OnlineStatus = { + Color = Colors.Green, + Transparency = 0, + }, + OfflineStatus = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + + Success = { + Color = Colors.Green, + Transparency = 0, + }, + Alert = { + Color = Colors.Red, + Transparency = 0, + }, + + Badge = { + Color = Colors.Flint, + Transparency = 0, + }, + BadgeContent = { + Color = Colors.White, + Transparency = 0, + }, + + SelectionCursor = { + Color = Colors.XboxBlue, + Transparency = 0, + }, +} + +return theme \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.spec.lua new file mode 100644 index 0000000..5426b52 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.spec.lua @@ -0,0 +1,9 @@ +return function() + it("should be a valid theme palette.", function() + local CorePackages = game:GetService("CorePackages") + local UIBlox = require(CorePackages.UIBlox) + local validateTheme = UIBlox.Style.Validator.validateTheme + local LightTheme = require(script.Parent.DarkTheme) + assert(validateTheme(LightTheme)) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/getThemeFromName.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/getThemeFromName.lua new file mode 100644 index 0000000..c785b13 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/getThemeFromName.lua @@ -0,0 +1,19 @@ +local CorePackages = game:GetService("CorePackages") +local ArgCheck = require(CorePackages.ArgCheck) +local Logging = require(CorePackages.Logging) +local UIBlox = require(CorePackages.UIBlox) +local validateTheme = UIBlox.Style.Validator.validateTheme + +return function (themeName, defaultTheme, themeMap) + local mappedTheme + if themeName ~= nil and #themeName > 0 then + mappedTheme = themeMap[string.lower(themeName)] + end + + if mappedTheme == nil then + mappedTheme = themeMap[defaultTheme] + Logging.warn(string.format("Unrecognized theme name: `%s`", tostring(themeName))) + end + ArgCheck.assert(validateTheme(mappedTheme)) + return mappedTheme +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/getThemeFromName.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/getThemeFromName.spec.lua new file mode 100644 index 0000000..94a7552 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Style/Themes/getThemeFromName.spec.lua @@ -0,0 +1,33 @@ +return function() + local getThemeFromName = require(script.Parent.getThemeFromName) + local Constants = require(script.Parent.Parent.Constants) + it("should be able to get a theme palette without errors", function() + local themeMap = { + [Constants.ThemeName.Dark] = require(script.Parent.DarkTheme), + } + local themeTable = getThemeFromName(Constants.ThemeName.Dark, Constants.ThemeName.Dark,themeMap) + expect(themeTable).to.be.a("table") + end) + + it("should be able to get a theme palette using default without errors", function() + local themeMap = { + [Constants.ThemeName.Dark] = require(script.Parent.DarkTheme), + } + local themeTable = getThemeFromName("classic", Constants.ThemeName.Dark, themeMap) + expect(themeTable).to.be.a("table") + end) + + it("should throw with invalid theme palette", function() + expect(function() + local themeMap = { + [Constants.ThemeName.Dark] = { + Background = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + } + } + getThemeFromName(Constants.ThemeName.Dark, Constants.ThemeName.Dark, themeMap) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/TableUtilities.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/TableUtilities.lua new file mode 100644 index 0000000..f533197 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/TableUtilities.lua @@ -0,0 +1,21 @@ +----------------------------------------------------------------------------- +--- --- +--- Under Migration to CorePackages --- +--- --- +--- This file is a compatibility bridge between old and new api --- +----------------------------------------------------------------------------- + +local CorePackages = game:GetService("CorePackages") +local tutils = require(CorePackages.tutils) + +return { + CheckListConsistency = tutils.checkListConsistency, + DeepEqual = tutils.deepEqual, + EqualKey = tutils.equalKey, + FieldCount = tutils.fieldCount, + ListDifference = tutils.listDifferences, + Print = tutils.print, + RecursiveToString = tutils.toString, + ShallowEqual = tutils.shallowEqual, + TableDifference = tutils.tableDifference, +} diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/TableUtilities.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/TableUtilities.spec.lua new file mode 100644 index 0000000..ff71610 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/TableUtilities.spec.lua @@ -0,0 +1,228 @@ +return function() + local TableUtilities = require(script.Parent.TableUtilities) + local NotForProductionUse = game:GetService("CoreGui").RobloxGui.Modules.NotForProductionUse + local expectedFields = require(NotForProductionUse.UnitTestHelpers.expectedFields) + + describe("alias wrapper", function() + it("SHOULD have all required fields", function() + expectedFields(TableUtilities, { + "CheckListConsistency", + "DeepEqual", + "EqualKey", + "FieldCount", + "ListDifference", + "Print", + "RecursiveToString", + "ShallowEqual", + "TableDifference", + }) + end) + end) + + describe("legacy tests", function() + it("should return whether tables are equal to each other", function() + local tableA = nil + local tableB = nil + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = nil + tableB = {} + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = {} + tableB = nil + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = {} + tableB = {} + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(true) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(true) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value2", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value1", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value2", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + key2 = "value2", + } + expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false) + end) + + it("should return whether tables are equal to each other at key", function() + local tableA = nil + local tableB = nil + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = nil + tableB = {} + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = {} + tableB = nil + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = {} + tableB = {} + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(true) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value2", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value1", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key2 = "value2", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false) + + tableA = { + key1 = "value1", + } + tableB = { + key1 = "value1", + key2 = "value2", + } + expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false) + expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(true) + expect(TableUtilities.EqualKey(tableA, tableB, "key2")).to.equal(false) + end) + + it("should return table's field count", function() + local t = {} + expect(TableUtilities.FieldCount(t)).to.equal(0) + + t = { + key1 = "value1", + } + expect(TableUtilities.FieldCount(t)).to.equal(1) + + t = { + key1 = "value1", + key2 = "value2", + } + expect(TableUtilities.FieldCount(t)).to.equal(2) + end) + + describe("TableUtilities.DeepEqual", function() + it("works for primitve data types", function() + expect(TableUtilities.DeepEqual(1, 1)).to.equal(true) + expect(TableUtilities.DeepEqual("str1", "str1")).to.equal(true) + expect(TableUtilities.DeepEqual(1, 2)).to.equal(false) + expect(TableUtilities.DeepEqual("str1", "str2")).to.equal(false) + end) + it("correctly identifies deeply-equal tables", function() + local table1 = { + num = 1, + innerTable = { + innerString = "str" + } + } + local table2 = { + num = 1, + innerTable = { + innerString = "str" + } + } + expect(TableUtilities.DeepEqual(table1, table2)).to.equal(true) + end) + it("correctly rejects non-deeply-equal tables", function() + local table1 = { + num = 1, + innerTable = { + innerString = "str" + } + } + local table2 = { + num = 1, + innerTable = { + innerString = "differentStr" + } + } + expect(TableUtilities.DeepEqual(table1, table2)).to.equal(false) + local table3 = { + num = 1, + innerTable = { + innerString = "str" + } + } + local table4 = { + num = 1, + innerTableWithDifferentKey = { + innerString = "str" + } + } + expect(TableUtilities.DeepEqual(table3, table4)).to.equal(false) + end) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/.robloxrc new file mode 100644 index 0000000..d5a9604 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/.robloxrc @@ -0,0 +1,5 @@ +{ + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGameIcons.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGameIcons.lua new file mode 100644 index 0000000..9a11f9e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGameIcons.lua @@ -0,0 +1,30 @@ +local CorePackages = game:GetService("CorePackages") +local AppTempCommon = CorePackages.AppTempCommon +local Promise = require(AppTempCommon.LuaApp.Promise) +local ApiFetchThumbnails = require(AppTempCommon.LuaApp.Utils.ApiFetchThumbnails) +local GamesGetIcons = require(AppTempCommon.LuaApp.Http.Requests.GamesGetIcons) +local SetGameIcons = require(AppTempCommon.LuaApp.Actions.SetGameIcons) + +local DEFAULT_ICON_SIZE = "150x150" + +return function (networkImpl, universeIds, imageSize) + return function(store) + local state = store:getState() + local stateToCheckForDuplicates = state.GameIcons + + -- Filter out the icons that are already in the store. + local idsToGet = {} + for _, targetId in pairs(universeIds) do + if stateToCheckForDuplicates[targetId] == nil then + table.insert(idsToGet, targetId) + end + end + + if #idsToGet == 0 then + return Promise.resolve() + else + return ApiFetchThumbnails.Fetch(networkImpl, + idsToGet, imageSize or DEFAULT_ICON_SIZE, "Game", GamesGetIcons, SetGameIcons, store) + end + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGameThumbnails.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGameThumbnails.lua new file mode 100644 index 0000000..333b127 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGameThumbnails.lua @@ -0,0 +1,91 @@ +local CorePackages = game:GetService("CorePackages") +local LuaApp = CorePackages.AppTempCommon.LuaApp + +local GamesGetThumbnails = require(LuaApp.Http.Requests.GamesGetThumbnails) +local SetGameThumbnails = require(LuaApp.Actions.SetGameThumbnails) + +local Functional = require(CorePackages.AppTempCommon.Common.Functional) +local Promise = require(LuaApp.Promise) +local Result = require(LuaApp.Result) + +local TableUtilities = require(LuaApp.TableUtilities) + +local THUMBNAIL_PAGE_COUNT = 20 +local THUMBNAIL_SIZE = 150 +local RETRY_MAX_COUNT = math.max(0, settings():GetFVariable("LuaAppNonFinalThumbnailMaxRetries")) +local RETRY_TIME_MULTIPLIER = 2 -- seconds + +local function convertToId(value) + if type(value) ~= "number" and type(value) ~= "string" then + return Result.error("convertToId expects value passed in to be a number or a string") + end + + return Result.success(tostring(value)) +end + +local function subdivideThumbnailTokenArray(thumbnailTokens, tokenLimit) + local someTokens = {} + for i = 1, #thumbnailTokens, tokenLimit do + local subArray = Functional.Take(thumbnailTokens, tokenLimit, i) + table.insert(someTokens, subArray) + end + + return someTokens +end + +local function fetchThumbnailBatch(networkImpl, store, thumbnailTokens) + return GamesGetThumbnails(networkImpl, thumbnailTokens, THUMBNAIL_SIZE, THUMBNAIL_SIZE):andThen(function(result) + local thumbnails = {} + local unfinalizedThumbnails = {} + + for _,image in pairs(result.responseBody) do + local convertToIdResult = convertToId(image.universeId) + + convertToIdResult:match(function(universeId) + if image.final == false then + unfinalizedThumbnails[universeId] = image.retryToken + else + -- index all of the thumbnails by universeId + thumbnails[universeId] = image + end + end, function(convertToIdError) + warn(convertToIdError) + end) + end + + store:dispatch(SetGameThumbnails(thumbnails)) + return Promise.resolve(unfinalizedThumbnails) + end) +end + +local function fetchSubdividedThumbnailsArray(networkImpl, store, thumbnailTokens) + return fetchThumbnailBatch(networkImpl, store, thumbnailTokens):andThen(function(unfinalizedThumbnails) + local remainingUnfinalizedThumbnails = unfinalizedThumbnails + + for retryCount = 1, RETRY_MAX_COUNT do + if TableUtilities.FieldCount(remainingUnfinalizedThumbnails) == 0 then + return -- Bail out, we're done! + end + + wait(RETRY_TIME_MULTIPLIER * math.pow(2, retryCount - 1)) + remainingUnfinalizedThumbnails = fetchThumbnailBatch(networkImpl, store, remainingUnfinalizedThumbnails):await() + end + end) +end + +local function fetchThumbnails(networkImpl, thumbnailTokens) + return function(store) + -- NOTE : because the size of each thumbnail token, me must limit the number we can fetch at a time. + -- So break apart the array of tokens we get into smaller, more manageable pieces. + local fetchPromises = {} + local someTokens = subdivideThumbnailTokenArray(thumbnailTokens, THUMBNAIL_PAGE_COUNT) + for _, thumbsArr in ipairs(someTokens) do + local promise = fetchSubdividedThumbnailsArray(networkImpl, store, thumbsArr) + table.insert(fetchPromises, promise) + end + + return Promise.all(fetchPromises) + end +end + +return fetchThumbnails diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGamesDataByPlaceIds.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGamesDataByPlaceIds.lua new file mode 100644 index 0000000..74d7e56 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchGamesDataByPlaceIds.lua @@ -0,0 +1,37 @@ +local CorePackages = game:GetService("CorePackages") +local ApiFetchGameIcons = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchGameIcons) +local Functional = require(CorePackages.AppTempCommon.Common.Functional) +local GamesMultigetPlaceDetails = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.GamesMultigetPlaceDetails) +local PlaceInfoModel = require(CorePackages.AppTempCommon.LuaChat.Models.PlaceInfoModel) +local ReceivedPlacesInfos = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedPlacesInfos) + +local LuaAppFlags = CorePackages.AppTempCommon.LuaApp.Flags +local convertUniverseIdToString = require(LuaAppFlags.ConvertUniverseIdToString) + +return function(networkImpl, placeIds) + return function(store) + if not placeIds or #placeIds == 0 then + return + end + + return GamesMultigetPlaceDetails(networkImpl, placeIds):andThen(function(result) + local data = result.responseBody + + local thumbnailUniverseIds = {} + local placeInfos = Functional.Map(data, function(placeInfoData) + local placeInfo = PlaceInfoModel.fromWeb(placeInfoData) + local universeId = convertUniverseIdToString(placeInfo.universeId) + table.insert(thumbnailUniverseIds, universeId) + return placeInfo + end) + + store:dispatch(ReceivedPlacesInfos(placeInfos)) + + if #thumbnailUniverseIds > 0 then + store:dispatch(ApiFetchGameIcons(networkImpl, thumbnailUniverseIds)) + end + + return placeInfos + end) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchPlaceInfos.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchPlaceInfos.lua new file mode 100644 index 0000000..d80ae99 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchPlaceInfos.lua @@ -0,0 +1,24 @@ +local CorePackages = game:GetService("CorePackages") + +local Functional = require(CorePackages.AppTempCommon.Common.Functional) +local GetPlaceInfos = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.GetPlaceInfos) + +-- LuaChat +local PlaceInfoModel = require(CorePackages.AppTempCommon.LuaChat.Models.PlaceInfoModel) +local ReceivedMultiplePlaceInfos = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedMultiplePlaceInfos) + +return function(networkImpl, placeIds) + return function(store) + return GetPlaceInfos(networkImpl, placeIds):andThen(function(result) + local data = result.responseBody + + local placeInfos = Functional.Map(data, function(placeInfoData) + return PlaceInfoModel.fromWeb(placeInfoData) + end) + + store:dispatch(ReceivedMultiplePlaceInfos(placeInfos)) + + return placeInfos + end) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriendCount.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriendCount.lua new file mode 100644 index 0000000..04e2a0e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriendCount.lua @@ -0,0 +1,29 @@ +local CorePackages = game:GetService("CorePackages") + +local Actions = CorePackages.AppTempCommon.LuaApp.Actions +local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests + +local UsersGetFriendCount = require(Requests.UsersGetFriendCount) +local SetFriendCount = require(Actions.SetFriendCount) + +local isNewFriendsEndpointsEnabled = require(CorePackages.AppTempCommon.LuaChat.Flags.isNewFriendsEndpointsEnabled) + +return function(networkImpl) + return function(store) + return UsersGetFriendCount(networkImpl):andThen(function(result) + local data = result.responseBody + + if isNewFriendsEndpointsEnabled() then + if data.count then + store:dispatch(SetFriendCount(data.count)) + end + else + if data.success and data.count then + store:dispatch(SetFriendCount(data.count)) + end + end + + return data.count + end) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriends.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriends.lua new file mode 100644 index 0000000..8578a38 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersFriends.lua @@ -0,0 +1,68 @@ +local CorePackages = game:GetService("CorePackages") +local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests + +local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) +local ApiFetchUsersPresences = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchUsersPresences) +local ApiFetchUsersThumbnail = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchUsersThumbnail) +local UsersGetFriends = require(Requests.UsersGetFriends) + +local FetchUserFriendsStarted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsStarted) +local FetchUserFriendsFailed = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsFailed) +local FetchUserFriendsCompleted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsCompleted) +local UserModel = require(CorePackages.AppTempCommon.LuaApp.Models.User) +local UpdateUsers = require(CorePackages.AppTempCommon.LuaApp.Thunks.UpdateUsers) + +return function(requestImpl, userId, thumbnailRequest, checkPoints) + return function(store) + store:dispatch(FetchUserFriendsStarted(userId)) + + if checkPoints ~= nil and checkPoints.startFetchUserFriends ~= nil then + checkPoints:startFetchUserFriends() + end + + local fetchedUserIds = {} + return UsersGetFriends(requestImpl, userId):andThen(function(response) + local responseBody = response.responseBody + + local newUsers = {} + for _, userData in pairs(responseBody.data) do + local id = tostring(userData.id) + + userData.isFriend = true + local newUser = UserModel.fromDataTable(userData) + + table.insert(fetchedUserIds, id) + newUsers[newUser.id] = newUser + end + store:dispatch(UpdateUsers(newUsers)) + + if checkPoints ~= nil and checkPoints.finishFetchUserFriends ~= nil then + checkPoints:finishFetchUserFriends() + end + + return fetchedUserIds + end):andThen(function(userIds) + if checkPoints ~= nil and checkPoints.startFetchUsersPresences ~= nil then + checkPoints:startFetchUsersPresences() + end + -- Asynchronously fetch friend thumbnails so we don't block display of UI + store:dispatch(ApiFetchUsersThumbnail.Fetch(requestImpl, userIds, thumbnailRequest)) + + return store:dispatch(ApiFetchUsersPresences(requestImpl, userIds)) + end):andThen( + function(result) + store:dispatch(FetchUserFriendsCompleted(userId)) + + if checkPoints ~= nil and checkPoints.finishFetchUsersPresences ~= nil then + checkPoints:finishFetchUsersPresences() + end + + return Promise.resolve(fetchedUserIds) + end, + function(response) + store:dispatch(FetchUserFriendsFailed(userId, response)) + return Promise.reject(response) + end + ) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersPresences.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersPresences.lua new file mode 100644 index 0000000..cbaed26 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersPresences.lua @@ -0,0 +1,22 @@ +local CorePackages = game:GetService("CorePackages") + +local LuaApp = CorePackages.AppTempCommon.LuaApp +local ChatUtils = CorePackages.AppTempCommon.LuaChat.Utils + +local getPlaceIds = require(ChatUtils.getFriendsActiveGamesPlaceIdsFromUsersPresence) +local receiveUsersPresence = require(ChatUtils.receiveUsersPresence) + +local ApiFetchGamesDataByPlaceIds = require(LuaApp.Thunks.ApiFetchGamesDataByPlaceIds) +local UsersGetPresence = require(LuaApp.Http.Requests.UsersGetPresence) + +return function(networkImpl, userIds) + return function(store) + return UsersGetPresence(networkImpl, userIds):andThen(function(result) + local userPresences = result.responseBody.userPresences + receiveUsersPresence(userPresences, store) + + local placeIds = getPlaceIds(userPresences, store) + store:dispatch(ApiFetchGamesDataByPlaceIds(networkImpl, placeIds)) + end) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersThumbnail.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersThumbnail.lua new file mode 100644 index 0000000..7daded7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiFetchUsersThumbnail.lua @@ -0,0 +1,192 @@ +local CorePackages = game:GetService("CorePackages") + +local Cryo = require(CorePackages.Cryo) + +local Actions = CorePackages.AppTempCommon.LuaApp.Actions +local TableUtilities = require(CorePackages.AppTempCommon.LuaApp.TableUtilities) +local PromiseUtilities = require(CorePackages.AppTempCommon.LuaApp.PromiseUtilities) + +local ThumbnailsGetAvatar = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.ThumbnailsGetAvatar) +local ThumbnailsGetAvatarHeadshot = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.ThumbnailsGetAvatarHeadshot) + +local AvatarThumbnailTypes = require(CorePackages.AppTempCommon.LuaApp.Enum.AvatarThumbnailTypes) + +local SetUserThumbnail = require(Actions.SetUserThumbnail) +local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) +local PerformFetch = require(CorePackages.AppTempCommon.LuaApp.Thunks.Networking.Util.PerformFetch) +local Result = require(CorePackages.AppTempCommon.LuaApp.Result) + +local RETRY_MAX_COUNT = math.max(0, settings():GetFVariable("LuaAppNonFinalThumbnailMaxRetries")) +local RETRY_TIME_MULTIPLIER = math.max(0, settings():GetFVariable("LuaAppThumbnailsApiRetryTimeMultiplier")) + +local MAX_REQUEST_COUNT = 100 + +local ThumbnailsTypeToApiMap = { + [AvatarThumbnailTypes.AvatarThumbnail] = ThumbnailsGetAvatar, + [AvatarThumbnailTypes.HeadShot] = ThumbnailsGetAvatarHeadshot, +} + +local function subdivideEntries(entries, limit) + local subArrays = {} + for i = 1, #entries, limit do + local subArray = Cryo.List.getRange(entries, i, i + limit - 1) + table.insert(subArrays, subArray) + end + return subArrays +end + +local function keyMapper(userId, thumbnailType, thumbnailSize) + return "luaapp.usersthumbnailsapi." .. userId .. "." .. thumbnailType .. "." .. thumbnailSize +end + +local function isCompleteThumbnailData(entry) + return type(entry) == "table" + and type(entry.targetId) == "number" + and type(entry.state) == "string" + and type(entry.imageUrl) == "string" +end + +local ApiFetchUsersThumbnail = {} + +function ApiFetchUsersThumbnail.getThumbnailsSizeArgForSize(thumbnailSize) + assert(typeof(thumbnailSize) == "string", + string.format("ApiFetchUsersThumbnail expects a string for thumbnailSize. Type: %s", typeof(thumbnailSize)) + ) + + assert(string.match(thumbnailSize, 'Size.+x'), + string.format( + "ApiFetchUsersThumbnail expects thumbnailSize to follow format \"Size..x..\" Current thumbnailSize: ", + thumbnailSize + ) + ) + return string.gsub(thumbnailSize, "Size", "") +end + +function ApiFetchUsersThumbnail._fetch(networkImpl, listOfUserIds, thumbnailRequest) + local thumbnailSize = thumbnailRequest.thumbnailSize + local thumbnailType = thumbnailRequest.thumbnailType + + local thumbnailSizeRequestArg = ApiFetchUsersThumbnail.getThumbnailsSizeArgForSize(thumbnailSize) + local thumbnailsApiForThumbnailType = ThumbnailsTypeToApiMap[thumbnailType] + + assert(typeof(thumbnailType) == "string", + "ApiFetchUsersThumbnail expects thumbnailType to be a string") + assert(typeof(thumbnailsApiForThumbnailType) == "function", + "ApiFetchUsersThumbnail failed to find api for given type: ", thumbnailType) + + local function keyMapperForCurrentTypeAndSize(userId) + return keyMapper(userId, thumbnailType, thumbnailSize) + end + + local function getTableOfFailedResults(userIds) + local results = {} + for _, userId in pairs(userIds) do + local key = keyMapperForCurrentTypeAndSize(userId) + results[key] = Result.new(false, { + targetId = userId, + }) + end + return results + end + + return PerformFetch.Batch(listOfUserIds, keyMapperForCurrentTypeAndSize, function(store, userIdsToFetch) + local function fetchThumbnails(userIdsProvided) + return thumbnailsApiForThumbnailType(networkImpl, userIdsProvided, thumbnailSizeRequestArg):andThen( + function(result) + local results = getTableOfFailedResults(userIdsProvided) + local data = result and result.responseBody and result.responseBody.data + if typeof(data) == "table" then + for _, entry in pairs(data) do + if isCompleteThumbnailData(entry) then + local userId = tostring(entry.targetId) + local key = keyMapperForCurrentTypeAndSize(userId) + local success = false + if entry.state == "Completed" then + store:dispatch(SetUserThumbnail(tostring(entry.targetId), entry.imageUrl, thumbnailType, thumbnailSize)) + success = true + end + results[key] = Result.new(success, entry) + end + end + end + + return Promise.resolve(results) + end, + function(err) + local results = getTableOfFailedResults(userIdsProvided) + return Promise.resolve(results) + end + ) + end + + return fetchThumbnails(userIdsToFetch):andThen(function(results) + local completedThumbnails = {} + local thumbnailResults = results + + if _G.__TESTEZ_RUNNING_TEST__ then + RETRY_MAX_COUNT = 1 + RETRY_TIME_MULTIPLIER = 0.001 + end + + local function retry(retryCount) + local remainingUserIdsToFetch = {} + + for key, result in pairs(thumbnailResults) do + local isSuccessful, thumbnailInfo = result:unwrap() + + if isSuccessful and thumbnailInfo.state == "Completed" then + completedThumbnails[key] = result + elseif isSuccessful and thumbnailInfo.state == "Pending" then + table.insert(remainingUserIdsToFetch, thumbnailInfo.targetId) + end + end + + if TableUtilities.FieldCount(remainingUserIdsToFetch) == 0 then + return Promise.resolve(completedThumbnails) + end + + local delayPromise = Promise.new(function(resolve, reject) + coroutine.wrap(function() + wait(RETRY_TIME_MULTIPLIER * math.pow(2, retryCount - 1)) + resolve() + end)() + end) + + return delayPromise:andThen(function() + return fetchThumbnails(remainingUserIdsToFetch) + end):andThen(function(newResults) + thumbnailResults = newResults + if retryCount > 1 then + return retry(retryCount - 1) + else + return Promise.resolve(completedThumbnails) + end + end) + end + + return retry(RETRY_MAX_COUNT) + end) + end) +end + +function ApiFetchUsersThumbnail.Fetch(networkImpl, userIds, thumbnailRequests) + return function(store) + local allPromises = {} + local subArraysOfUserIds = subdivideEntries(userIds, MAX_REQUEST_COUNT) + + for _, thumbnailRequest in pairs(thumbnailRequests) do + for _, limitedListOfUserIds in pairs(subArraysOfUserIds) do + local promise = store:dispatch(ApiFetchUsersThumbnail._fetch(networkImpl, limitedListOfUserIds, thumbnailRequest)) + table.insert(allPromises, promise) + end + end + + return PromiseUtilities.Batch(allPromises) + end +end + +function ApiFetchUsersThumbnail.GetFetchingStatus(state, userId, thumbnailType, thumbnailSize) + return PerformFetch.GetStatus(state, keyMapper(userId, thumbnailType, thumbnailSize)) +end + +return ApiFetchUsersThumbnail \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiSendGameInvite.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiSendGameInvite.lua new file mode 100644 index 0000000..9ae7c77 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/ApiSendGameInvite.lua @@ -0,0 +1,44 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests + +local ChatSendMessage = require(Requests.ChatSendMessage) +local ChatStartOneToOneConversation = require(Requests.ChatStartOneToOneConversation) + +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator) + + +local ChatSendGameLinkMessage = require(Requests.ChatSendGameLinkMessage) + +return function(networkImpl, userId, placeInfo) + local clientId = Players.LocalPlayer.UserId + + -- Construct the invite messages based on place info + local inviteTextMessage + inviteTextMessage = RobloxTranslator:FormatByKey( + "Feature.SettingsHub.Message.InviteToGameTitle", { PLACENAME = placeInfo.name } + ) + + return function(store) + return ChatStartOneToOneConversation(networkImpl, userId, clientId):andThen(function(conversationResult) + local conversation = conversationResult.responseBody.conversation + + return ChatSendMessage(networkImpl, conversation.id, inviteTextMessage):andThen(function() + local function handleResult(inviteResult) + local data = inviteResult.responseBody + + return { + resultType = data.resultType, + conversationId = conversation.id, + placeId = placeInfo.universeRootPlaceId, + } + end + + return ChatSendGameLinkMessage(networkImpl, conversation.id, placeInfo.universeId):andThen(handleResult) + end) + end) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/.robloxrc new file mode 100644 index 0000000..fc3d643 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/.robloxrc @@ -0,0 +1,5 @@ +{ + "language": { + "mode": "nonstrict" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/Util/PerformFetch.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/Util/PerformFetch.lua new file mode 100644 index 0000000..8143137 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/Util/PerformFetch.lua @@ -0,0 +1,245 @@ +local CorePackages = game:GetService("CorePackages") + +local Result = require(CorePackages.AppTempCommon.LuaApp.Result) +local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) + +local PromiseUtilities = require(CorePackages.AppTempCommon.LuaApp.PromiseUtilities) +local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus) +local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus) + +--[[ + PerformFetch wraps the notion of a network request together with its fetching status + so that it is easier to de-duplicate concurrent requests for the same resource. The + fetching status for individual fetching operations are available in the store as: + + storeState.FetchingStatus[key] + + When you use one of the methods in this helper, you provide a key (or keymap), and + supply a functor that will only be called when a fetch actually needs to be performed. + + Any follow-up andThen/catch clauses will be correctly daisy-chained onto the original + ongoing fetch request if one is already underway. +]] +local PerformFetch = {} + +local batchPromises = {} -- fetch key = outstanding promise from PerformFetch.Batch + +--[[ + Helper function for unit tests to be able to clean up batchPromises created from + previous test case. This is because unit tests don't wait until the mock requests + are resolved and moves onto the next test. If tests happen to generate duplicate + fetchStatusKey, unresolved batchPromise will throw thinking that the promise does + not have the correct status. +]] +function PerformFetch.ClearOutstandingPromiseStatus() + batchPromises = {} +end + +local function singleFetchKeymapper(item) + -- Single fetch keys are used directly + return item +end + +--[[ + Get the fetching status for a given status key. Defaults to + RetrievalStatus.NotStarted for missing keys. +]] +function PerformFetch.GetStatus(state, fetchStatusKey) + assert(typeof(state) == "table") + assert(typeof(fetchStatusKey) == "string") + assert(#fetchStatusKey > 0) + return state.FetchingStatus[fetchStatusKey] or RetrievalStatus.NotStarted +end + +--[[ + Perform a fetch operation for a single resource. + + Args: + fetchStatusKey - String key for the fetching status to index the Rodux store. + fetchFunctor - Functor to call when a fetch needs to be performed for fetchStatusKey. + + Returns: + A Promise that resolves or rejects in accordance with the result of fetchFunctor, or the + promise for the original fetch if one is already ongoing. + + Usage: + In your main thunk, wrap your inner store function with this thunk, like this: + + return function(arg1, arg2) + return PerformFetch.Single("mykey", function(store) + return doYourLogicHere() -- Must return a Promise!!! + end) + end + + Please note that in order for single fetches to integrate well with batch fetches, + your promise must NEVER resolve or reject with multiple arguments! Wrap your results + in a table instead. +]] +function PerformFetch.Single(fetchStatusKey, fetchFunctor) + assert(typeof(fetchStatusKey) == "string") + assert(typeof(fetchFunctor) == "function") + assert(#fetchStatusKey > 0) + + return function(store) + -- Call batch API to handle the individual fetch + return PerformFetch.Batch({ fetchStatusKey }, singleFetchKeymapper, function(batchStore, itemsToFetch) + assert(#itemsToFetch == 1) + + local functorPromise = fetchFunctor(batchStore) + assert(Promise.is(functorPromise)) + + return functorPromise:andThen(function(...) + assert(#{...} <= 1) + return Promise.resolve({ [fetchStatusKey] = Result.new(true, (...)) }) + end, function(...) + assert(#{...} <= 1) + return Promise.resolve({ [fetchStatusKey] = Result.new(false, (...)) }) + end) + end)(store):andThen(function(batchResults) + local success, value = batchResults[fetchStatusKey]:unwrap() + if success then + return Promise.resolve(value) + else + return Promise.reject(value) + end + end) + end +end + +--[[ + Perform a fetch operation for multiple resources at once (batching). + + Args: + items - The list of item ids that need to be fetched. + keyMapper - A function that maps items to string keys for the Rodux store. + fetchFunctor - A function that will be called when at least one item needs to be fetched. + + Returns: + A Promise that always resolves. Result data is returned in a single table to the + andThen() clause, where item fetch keys are the keys and the results for each key + are encoded using a Result object. + + Usage: + In your main thunk, wrap your inner store function with this thunk, like this: + + local MAPPER = function(item) + return doSomething(item) -- Each key must be unique + end + + return function(arg1, arg2) + local allItems = makeYourItemsList() + return PerformFetch.Batch(allItems, MAPPER, function(store, itemsToFetch) + return doYourLogicHere(itemsToFetch) -- Must return a Promise!!! + end) + end + + Your implementation of fetchFunctor should return a promise that resolves + according to the structure of PromiseUtilities.Batch, ex: + + return Promise.resolve({ + itemFetchKey1 = Result.new(true, payload1), + itemFetchKey2 = Result.new(false, payload2), -- failed + }) + + Any other resolving arguments will be dropped for consistency and safety of the API. + Since this is a batching API, your implementation should NOT reject(). + + Please keep in mind that batching calls have to fit into an environment where they may + be daisy chained onto other batching calls, and those results have to be amalgamated + at the end of the chain into unique tables for each of the callers! +]] +function PerformFetch.Batch(items, keyMapper, fetchFunctor) + assert(typeof(items) == "table") + assert(typeof(keyMapper) == "function") + assert(typeof(fetchFunctor) == "function") + + return function(store) + local itemsToFetch = {} + local itemsToFetchKeyMap = {} + local batchPromisesForItemsAlreadyBeingFetched = {} + + -- Filter out items that do not need to be fetched + for _, item in ipairs(items) do + local fetchStatusKey = keyMapper(item) + local fetchingStatus = PerformFetch.GetStatus(store:getState(), fetchStatusKey) + local batchPromise = batchPromises[fetchStatusKey] + + if batchPromise then + assert(fetchingStatus == RetrievalStatus.Fetching) + + batchPromisesForItemsAlreadyBeingFetched[fetchStatusKey] = batchPromise + else + assert(fetchingStatus ~= RetrievalStatus.Fetching) + + table.insert(itemsToFetch, item) + itemsToFetchKeyMap[item] = fetchStatusKey + end + end + + local doResolve + local batchFetchingPromise = Promise.new(function(resolve) + doResolve = resolve + end) + + -- Call functor if there are items to fetch, otherwise short-circuit it + -- We want to call it FIRST because we need to kick off async fetch before blocking + -- on other responses. + local functorPromise + if #itemsToFetch > 0 then + -- Place remaining items into fetching state and make entry in table before + -- we kick off functor just in case it returns already-completed promise + for _, fetchStatusKey in pairs(itemsToFetchKeyMap) do + store:dispatch(UpdateFetchingStatus(fetchStatusKey, RetrievalStatus.Fetching)) + batchPromises[fetchStatusKey] = batchFetchingPromise + end + + functorPromise = fetchFunctor(store, itemsToFetch) + assert(Promise.is(functorPromise)) + else + functorPromise = Promise.resolve({}) + end + + functorPromise:andThen(function(myResults) + myResults = myResults or {} -- No resolve args = empty table for ease of use + + return PromiseUtilities.Batch(batchPromisesForItemsAlreadyBeingFetched):andThen(function(batchResults) + local filteredResults = {} + for batchKey, batchResult in pairs(batchResults) do + -- Extract only the result for the key we care about from the batch results + local _, value = batchResult:unwrap() + filteredResults[batchKey] = value[batchKey] + end + + return myResults, filteredResults + end) + end, + function() + assert(false, "PerformFetch fetchFunctor should never reject") + end):andThen(function(myResults, batchResults) + -- Iterate on requested items rather than on actual result set + -- so that we are sure to check all our keys and ignore extra ones + for _, fetchKey in pairs(itemsToFetchKeyMap) do + local resultObj = myResults[fetchKey] + if Result.is(resultObj) then + batchResults[fetchKey] = resultObj + else + batchResults[fetchKey] = Result.error() + end + + -- Update fetching status in store from Result object status + -- (The extra parens unwrap a multi-return value!) + local itemStatus = (batchResults[fetchKey]:unwrap()) and RetrievalStatus.Done or RetrievalStatus.Failed + store:dispatch(UpdateFetchingStatus(fetchKey, itemStatus)) + batchPromises[fetchKey] = nil + end + + return batchResults + end):andThen(function(joinedResults) + doResolve(joinedResults) + end) + + return batchFetchingPromise + end +end + +return PerformFetch diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/Util/PerformFetch.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/Util/PerformFetch.spec.lua new file mode 100644 index 0000000..63bf9b8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/Networking/Util/PerformFetch.spec.lua @@ -0,0 +1,510 @@ +return function() + local PerformFetch = require(script.Parent.PerformFetch) + local CorePackages = game:GetService("CorePackages") + + local Rodux = require(CorePackages.Rodux) + local FetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Reducers.FetchingStatus) + local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus) + local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise) + local Result = require(CorePackages.AppTempCommon.LuaApp.Result) + + local function batchKeyMapper(item) + return tostring(item) .. "_key" + end + + local TEST_ITEM_1 = "item1" + local TEST_ITEM_2 = "item2" + + local TEST_KEY_1 = batchKeyMapper(TEST_ITEM_1) + local TEST_KEY_2 = batchKeyMapper(TEST_ITEM_2) + + local function MockReducer(state, action) + state = state or {} + return { + FetchingStatus = FetchingStatus(state.FetchingStatus, action), + } + end + + local function makeResolver() + local startResolve + local startReject + local testPromise = Promise.new(function(resolve, reject) + startResolve = resolve + startReject = reject + end) + + return { + promise = testPromise, + resolve = startResolve, + reject = startReject + } + end + + local function doDispatchSingle(store, key, functor) + -- Wrap functor like a thunk would normally be + local thunkFunc = function() + return PerformFetch.Single(key, functor) + end + + return store:dispatch(thunkFunc()) + end + + local function doDispatchBatch(store, items, functor) + -- Wrap functor like a thunk would normally be + local thunkFunc = function() + return PerformFetch.Batch(items, batchKeyMapper, functor) + end + + return store:dispatch(thunkFunc()) + end + + local function doBasicSingleTest(key) + local resolver = makeResolver() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local thunkPromise = doDispatchSingle(store, key, function() + return resolver.promise + end) + + return { + store = store, + resolver = resolver, + promise = thunkPromise + } + end + + local function doBasicBatchTest(keys) + local resolver = makeResolver() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local thunkPromise = doDispatchBatch(store, keys, function() + return resolver.promise + end) + + return { + store = store, + resolver = resolver, + promise = thunkPromise + } + end + + describe("PerformFetch.GetStatus", function() + it("should return NotStarted for missing key", function() + local state = { FetchingStatus = {} } + local status = PerformFetch.GetStatus(state, TEST_KEY_1) + + expect(status).to.equal(RetrievalStatus.NotStarted) + end) + + it("should return matching status for state in store", function() + local statusesToTest = { + RetrievalStatus.NotStarted, + RetrievalStatus.Fetching, + RetrievalStatus.Done, + RetrievalStatus.Failed + } + + for _, testStatus in ipairs(statusesToTest) do + local state = { + FetchingStatus = { + [TEST_KEY_1] = testStatus + } + } + + expect(PerformFetch.GetStatus(state, TEST_KEY_1)).to.equal(testStatus) + end + + expect(#statusesToTest).to.equal(4) + end) + end) + + describe("PerformFetch.Single", function() + it("should set fetching state in store when fetch begins", function() + local bundle = doBasicSingleTest(TEST_KEY_1) + + expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Fetching) + bundle.resolver.resolve() -- clear key from global fetchingPromiseMap or later tests get blocked + end) + + it("should pass store parameter to fetch functor", function() + local originalStore = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local newStore + doDispatchSingle(originalStore, TEST_KEY_1, function(store) + newStore = store + return Promise.resolve() + end) + + expect(newStore ~= nil).to.equal(true) + end) + + it("should set fetching state to done for sync resolve", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.resolve() + end) + + expect(store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Done) + end) + + it("should set fetching state to failed for sync reject", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.reject() + end) + + expect(store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Failed) + end) + + it("should set fetching state to Done after async fetch resolves", function() + local bundle = doBasicSingleTest(TEST_KEY_1) + + bundle.resolver.resolve() + expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Done) + end) + + + it("should set fetching state to Failed after async fetch rejects", function() + local bundle = doBasicSingleTest(TEST_KEY_1) + + bundle.resolver.reject() + expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Failed) + end) + + + it("should not mix fetching status of two separate keys", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.resolve() + end) + + doDispatchSingle(store, TEST_KEY_2, function() + return Promise.reject() + end) + + expect(store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Done) + expect(store:getState().FetchingStatus[TEST_KEY_2]).to.equal(RetrievalStatus.Failed) + end) + + it("should pass original promise args to daisy-chained promise upon resolve", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local testTable = { a = 1 } + + local passedArg + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.resolve(testTable) + end):andThen(function(results) + passedArg = results + end) + + expect(passedArg).to.equal(testTable) + end) + + it("should pass original promise args to daisy-chained promise upon reject", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local testTable = { a = 1 } + + local passedArg + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.reject(testTable) + end):catch(function(results) + passedArg = results + end) + + expect(passedArg).to.equal(testTable) + end) + + it("should not call second thunk instance for same key while request is ongoing", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local firstThunkExecuted = false + local secondThunkExecuted = false + + local firstThunkResolver = makeResolver() + doDispatchSingle(store, TEST_KEY_1, function() + firstThunkExecuted = true + return firstThunkResolver.promise + end) + + doDispatchSingle(store, TEST_KEY_1, function() + secondThunkExecuted = true + return Promise.resolve() + end) + + expect(store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Fetching) + expect(firstThunkExecuted).to.equal(true) + expect(secondThunkExecuted).to.equal(false) + + firstThunkResolver.resolve() + end) + + it("should call both thunks when the first one is completed soon enough", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local firstThunkExecuted = false + local secondThunkExecuted = false + + doDispatchSingle(store, TEST_KEY_1, function() + firstThunkExecuted = true + return Promise.resolve() + end) + + doDispatchSingle(store, TEST_KEY_1, function() + secondThunkExecuted = true + return Promise.resolve() + end) + + expect(firstThunkExecuted).to.equal(true) + expect(secondThunkExecuted).to.equal(true) + end) + + it("should resolve daisy-chained promises after thunk resolves", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local chainedPromiseExecuted = false + + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.resolve() + end):andThen(function() + chainedPromiseExecuted = true + end):catch(function() + assert(false) + end) + + expect(chainedPromiseExecuted).to.equal(true) + end) + + it("should reject daisy-chained promises after thunk rejects", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local chainedCatchExecuted = false + + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.reject() + end):andThen(function() + assert(false) + end):catch(function() + chainedCatchExecuted = true + end) + + expect(chainedCatchExecuted).to.equal(true) + end) + + it("should resolve daisy-chained promises on second thunk after first resolves", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local secondPromiseResolved = false + + local startResolve + local firstThunkPromise = Promise.new(function(resolve) + startResolve = resolve + end) + + doDispatchSingle(store, TEST_KEY_1, function() + return firstThunkPromise + end) + + doDispatchSingle(store, TEST_KEY_1, function() + assert(false) + return Promise.reject() + end):andThen(function() + secondPromiseResolved = true + end):catch(function() + assert(false) + end) + + expect(secondPromiseResolved).to.equal(false) + + startResolve() + + expect(secondPromiseResolved).to.equal(true) + end) + + it("should reject daisy-chained promises on second thunk after first thunk rejects", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local secondPromiseRejected = false + + local startReject + local firstThunkPromise = Promise.new(function(_, reject) + startReject = reject + end) + + doDispatchSingle(store, TEST_KEY_1, function() + return firstThunkPromise + end) + + doDispatchSingle(store, TEST_KEY_1, function() + return Promise.new(function() end) + end):andThen(function() + assert(false) + end):catch(function() + secondPromiseRejected = true + end) + + expect(secondPromiseRejected).to.equal(false) + + startReject() + + expect(secondPromiseRejected).to.equal(true) + end) + end) + + describe("PerformFetch.Batch", function() + it("should set fetching state in store for all batch items when fetching begins", function() + local originalItemList = { TEST_ITEM_1, TEST_ITEM_2 } + local bundle = doBasicBatchTest(originalItemList) + + expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Fetching) + expect(bundle.store:getState().FetchingStatus[TEST_KEY_2]).to.equal(RetrievalStatus.Fetching) + + local results = { + [TEST_KEY_1] = Result.new(true), + [TEST_KEY_2] = Result.new(true), + } + + bundle.resolver.resolve(results) -- Cleanup to avoid test blockage + end) + + it("should set fetching state to matching status for all batch items when fetching completes successfully", function() + local originalItemList = { TEST_ITEM_1, TEST_ITEM_2 } + local bundle = doBasicBatchTest(originalItemList) + + local results = { + [TEST_KEY_1] = Result.new(true), + [TEST_KEY_2] = Result.new(false), + } + + bundle.resolver.resolve(results) + + expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Done) + expect(bundle.store:getState().FetchingStatus[TEST_KEY_2]).to.equal(RetrievalStatus.Failed) + end) + + it("should fail items when they are not in the result list", function() + local originalItemList = { TEST_ITEM_1, TEST_ITEM_2 } + local bundle = doBasicBatchTest(originalItemList) + + bundle.resolver.resolve({}) + + expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Failed) + expect(bundle.store:getState().FetchingStatus[TEST_KEY_2]).to.equal(RetrievalStatus.Failed) + end) + + it("should return a daisy chainable batch style promise that resolves with results", function() + local originalItemList = { TEST_ITEM_1, TEST_ITEM_2 } + local bundle = doBasicBatchTest(originalItemList) + + local chainedResults = nil + bundle.promise:andThen(function(promisedResults) + chainedResults = promisedResults + end) + + local results = { + [TEST_KEY_1] = Result.new(true, 42), + [TEST_KEY_2] = Result.new(false, 29), + } + + bundle.resolver.resolve(results) + + local result1Status, result1Value = chainedResults[TEST_KEY_1]:unwrap() + local result2Status, result2Value = chainedResults[TEST_KEY_2]:unwrap() + + expect(result1Status).to.equal(true) + expect(result1Value).to.equal(42) + + expect(result2Status).to.equal(false) + expect(result2Value).to.equal(29) + end) + + it("should not call batch functor when there are no items to fetch", function() + local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware }) + local promise = doDispatchBatch(store, {}, function() + assert(false, "Functor should not be called when there are no items to fetch") + end) + + local promiseResolved = false + promise:andThen(function() + promiseResolved = true + end) + + expect(promiseResolved).to.equal(true) + end) + + it("should amalgamate results from multiple batch calls", function() + local testItemList = { TEST_ITEM_1, TEST_ITEM_2 } + + local bundle = doBasicBatchTest(testItemList) + + local promise2 = doDispatchBatch(bundle.store, testItemList, function(_, itemsToFetch) + assert(false, "second batch should not be called") + return Promise.resolve({ }) + end) + + local chainedResults = nil + promise2:andThen(function(promisedResults) + chainedResults = promisedResults + end) + + local results = { + [TEST_KEY_1] = Result.new(true, 41), + [TEST_KEY_2] = Result.new(false, 39), + } + + bundle.resolver.resolve(results) + + local result1Status, result1Value = chainedResults[TEST_KEY_1]:unwrap() + local result2Status, result2Value = chainedResults[TEST_KEY_2]:unwrap() + + expect(result1Status).to.equal(true) + expect(result1Value).to.equal(41) + + expect(result2Status).to.equal(false) + expect(result2Value).to.equal(39) + end) + end) + + describe("PerformFetch with mixed Single/Batch", function() + it("should include outstanding single results for matching batch keys", function() + local singleBundle = doBasicSingleTest(TEST_KEY_1) + + local batchItemCount = -1 + local batchPromise = doDispatchBatch(singleBundle.store, { TEST_ITEM_1, TEST_ITEM_2 }, + function(_, items) + batchItemCount = #items + return Promise.resolve({ [TEST_KEY_2] = Result.new(false, 35) }) + end) + + singleBundle.resolver.resolve(49) + + local chainedResults = nil + batchPromise:andThen(function(results) + chainedResults = results + end) + + local result1Status, result1Value = chainedResults[TEST_KEY_1]:unwrap() + local result2Status, result2Value = chainedResults[TEST_KEY_2]:unwrap() + + expect(result1Status).to.equal(true) + expect(result1Value).to.equal(49) + + expect(result2Status).to.equal(false) + expect(result2Value).to.equal(35) + + expect(batchItemCount).to.equal(1) + end) + + it("should use batch result for duplicate single request", function() + local batchBundle = doBasicBatchTest({ TEST_ITEM_1, TEST_ITEM_2 }) + + local singlePromise = doDispatchSingle(batchBundle.store, TEST_KEY_1, function() + assert(false, "Single functor should not be called") + return Promise.reject() + end) + + batchBundle.resolver.resolve({ + [TEST_KEY_1] = Result.new(true, 42), + [TEST_KEY_2] = Result.new(true, 39) + }) + + local chainedResult = nil + singlePromise:andThen(function(result) + chainedResult = result + end) + + expect(chainedResult).to.equal(42) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/UpdateUsers.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/UpdateUsers.lua new file mode 100644 index 0000000..c606bd6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/UpdateUsers.lua @@ -0,0 +1,50 @@ +local CorePackages = game:GetService("CorePackages") + +local User = require(CorePackages.AppTempCommon.LuaApp.Models.User) +local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers) +local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount) + +return function(users) + return function(store) + local friendCountOffset = 0 + local updatedUsers = {} + + for _, user in pairs(users) do + local needsUpdate = false + local userId = user.id + local isFriend = user.isFriend + local offset = 0 + + assert(typeof(isFriend) == "boolean") + + local userInStore = store:getState().Users[userId] + if userInStore then + -- Mark user with needsUpdate if any of the field is different + -- from the existing user information in Store. + if not User.compare(userInStore, user) then + needsUpdate = true + if userInStore.isFriend ~= isFriend then + offset = isFriend and 1 or -1 + end + end + else + needsUpdate = true + offset = isFriend and 1 or 0 + end + + if needsUpdate then + friendCountOffset = friendCountOffset + offset + updatedUsers[userId] = user + end + end + + if next(updatedUsers) then + store:dispatch(AddUsers(updatedUsers)) + end + + if friendCountOffset ~= 0 then + local currentFriendCount = store:getState().FriendCount + store:dispatch(SetFriendCount(currentFriendCount + friendCountOffset)) + end + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/UpdateUsers.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/UpdateUsers.spec.lua new file mode 100644 index 0000000..6b31edf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Thunks/UpdateUsers.spec.lua @@ -0,0 +1,123 @@ +return function() + local CorePackages = game:GetService("CorePackages") + + local Rodux = require(CorePackages.Rodux) + local Immutable = require(CorePackages.AppTempCommon.Common.Immutable) + + local UpdateUsers = require(CorePackages.AppTempCommon.LuaApp.Thunks.UpdateUsers) + + local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers) + local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount) + + local FriendCount = require(CorePackages.AppTempCommon.LuaChat.Reducers.FriendCount) + local Users = require(CorePackages.AppTempCommon.LuaApp.Reducers.Users) + + local User = require(CorePackages.AppTempCommon.LuaApp.Models.User) + + local function UsersReducerMonitor (state, action) + state = state or { + numberOfAddUsersCalled = 0, + numberOfUsersPassedIn = 0, + } + + if action.type == AddUsers.name then + state.numberOfAddUsersCalled = state.numberOfAddUsersCalled + 1 + state.numberOfUsersPassedIn = 0 + for _, _ in pairs(action.users) do + state.numberOfUsersPassedIn = state.numberOfUsersPassedIn + 1 + end + end + + return state + end + + local function FriendCountReducerMonitor (state, action) + state = state or { + numberOfSetFriendCountCalled = 0, + } + + if action.type == SetFriendCount.name then + state.numberOfSetFriendCountCalled = state.numberOfSetFriendCountCalled + 1 + end + + return state + end + + local function CustomReducer(state, action) + state = state or {} + + return { + Users = Users(state.Users, action), + UsersReducerMonitor = UsersReducerMonitor(state.UsersReducerMonitor, action), + + FriendCount = FriendCount(state.FriendCount, action), + FriendCountReducerMonitor = FriendCountReducerMonitor(state.FriendCountReducerMonitor, action), + } + end + + local listOfUsers = { + ["1"] = User.fromData(1, "Hedonism Bot", true), + ["2"] = User.fromData(2, "Hypno Toad", true), + ["3"] = User.fromData(3, "John Zoidberg", false), + ["4"] = User.fromData(4, "Pazuzu", true), + ["5"] = User.fromData(5, "Ogden Wernstrom", true), + ["6"] = User.fromData(6, "Lrrr", true), + } + + it("should do nothing if empty list of users is provided", function() + local store = Rodux.Store.new(CustomReducer, {}, { + Rodux.thunkMiddleware, + }) + store:dispatch(UpdateUsers({ })) + + local state = store:getState() + + expect(state.UsersReducerMonitor.numberOfAddUsersCalled).to.equal(0) + expect(state.FriendCountReducerMonitor.numberOfSetFriendCountCalled).to.equal(0) + end) + + it("should update only the number of users with modified data", function() + local store = Rodux.Store.new(CustomReducer, { + Users = listOfUsers, + }, { + Rodux.thunkMiddleware, + }) + + local currentUsers = store:getState().Users + local listOfUsersWithPotentialUpdates = { + Immutable.Set(currentUsers["2"], "presence", User.PresenceType.IN_GAME), -- changed + Immutable.Set(currentUsers["5"], "isFriend", false), -- changed + Immutable.Set(currentUsers["6"], "isFriend", true), -- did not change + } + + store:dispatch(UpdateUsers(listOfUsersWithPotentialUpdates)) + + local state = store:getState() + expect(state.UsersReducerMonitor.numberOfAddUsersCalled).to.equal(1) + expect(state.UsersReducerMonitor.numberOfUsersPassedIn).to.equal(2) + end) + + it("should correctly update the number of friends", function() + local store = Rodux.Store.new(CustomReducer, {}, { + Rodux.thunkMiddleware, + }) + store:dispatch(UpdateUsers(listOfUsers)) + + local state = store:getState() + expect(state.FriendCountReducerMonitor.numberOfSetFriendCountCalled).to.equal(1) + expect(state.FriendCount).to.equal(5) + + local currentUsers = store:getState().Users + local listOfUsersWithPotentialUpdates = { + Immutable.Set(currentUsers["2"], "presence", User.PresenceType.IN_GAME), -- friendship didn't change + Immutable.Set(currentUsers["5"], "isFriend", false), -- friendship changed + Immutable.Set(currentUsers["6"], "isFriend", false), -- friendship changed + User.fromData(7, "Nibbler", true), -- new friend + } + + store:dispatch(UpdateUsers(listOfUsersWithPotentialUpdates)) + + state = store:getState() + expect(state.FriendCount).to.equal(4) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/.robloxrc new file mode 100644 index 0000000..d5a9604 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/.robloxrc @@ -0,0 +1,5 @@ +{ + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ApiFetchThumbnails.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ApiFetchThumbnails.lua new file mode 100644 index 0000000..7167dad --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ApiFetchThumbnails.lua @@ -0,0 +1,73 @@ +local CorePackages = game:GetService("CorePackages") +local Cryo = require(CorePackages.Cryo) +local ArgCheck = require(CorePackages.ArgCheck) +local PromiseUtilities = require(CorePackages.AppTempCommon.LuaApp.PromiseUtilities) +local FetchSubdividedThumbnails = require(script.Parent.FetchSubdividedThumbnails) + +local PerformFetch = require(CorePackages.AppTempCommon.LuaApp.Thunks.Networking.Util.PerformFetch) + +local ICON_PAGE_COUNT = 100 +local ICON_SIZE = "150x150" + +local function convertToId(value) + return tostring(value) +end + +local ApiFetchThumbnails = {} + +local keyMapper = function (request) + local targetId = request.targetId + local size = request.iconSize and "."..request.iconSize or "" + local requestName = request.requestName and "."..request.requestName or "" + return "luaapp.thumbnails." .. convertToId(targetId)..size..requestName +end + +ApiFetchThumbnails.KeyMapper = keyMapper + +local function subdivideIdsArray(requests, limit) + local someTokens = {} + for i = 1, #requests, limit do + local subArray = Cryo.List.getRange(requests, i, i + limit - 1) + table.insert(someTokens, subArray) + end + return someTokens +end + +function ApiFetchThumbnails.Fetch(networkImpl, targetIds, imageSize, requestName, fetchFunction, storeDispatch, store) + local size = imageSize or ICON_SIZE + ArgCheck.isType(targetIds, "table", "targetIds") + ArgCheck.isType(requestName, "string", "requestName") + ArgCheck.isNonNegativeNumber(#targetIds, "targetIds count") + + local requests = {} + local promises = {} + -- Filter out the icons that are already in the store. + for _, targetId in pairs(targetIds) do + table.insert(requests, { + targetId = targetId, + iconSize = size, + }) + end + local subdividedRequestsArray = subdivideIdsArray(requests, ICON_PAGE_COUNT) + for _, subdividedRequests in ipairs(subdividedRequestsArray) do + table.insert( + promises, + store:dispatch(FetchSubdividedThumbnails.Fetch( + networkImpl, + subdividedRequests, + keyMapper, + requestName, + fetchFunction, + storeDispatch + )) + ) + end + + return PromiseUtilities.Batch(promises) +end + +function ApiFetchThumbnails.GetFetchingStatus(state, targetId, iconSize, requestName) + return PerformFetch.GetStatus(state, keyMapper({targetId = targetId, requestName = requestName, iconSize = iconSize})) +end + +return ApiFetchThumbnails \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/FetchSubdividedThumbnails.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/FetchSubdividedThumbnails.lua new file mode 100644 index 0000000..47d7a20 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/FetchSubdividedThumbnails.lua @@ -0,0 +1,139 @@ +local CorePackages = game:GetService("CorePackages") +local LuaApp = CorePackages.AppTempCommon.LuaApp +local ArgCheck = require(CorePackages.ArgCheck) + +local Thumbnail = require(LuaApp.Models.Thumbnail) + +local PerformFetch = require(LuaApp.Thunks.Networking.Util.PerformFetch) +local Promise = require(LuaApp.Promise) +local Result = require(LuaApp.Result) + +local TableUtilities = require(LuaApp.TableUtilities) + +local RETRY_MAX_COUNT = math.max(0, settings():GetFVariable("LuaAppNonFinalThumbnailMaxRetries")) +local RETRY_TIME_MULTIPLIER = math.max(0, settings():GetFVariable("LuaAppThumbnailsApiRetryTimeMultiplier")) -- seconds + +local FetchSubdividedThumbnails = {} + +function FetchSubdividedThumbnails._fetchIcons(store, networkImpl, targetIds, iconSize, keyMapper, requestName, fetchFunction, storeDispatch) + local function keyMapperForCurrentRequestNameAndSize(targetId) + return keyMapper({ + targetId = targetId, + requestName = requestName, + iconSize = iconSize + }) + end + + local function getTableOfFailedResults(failedTargetIds) + local results = {} + for _, targetId in pairs(failedTargetIds) do + local key = keyMapperForCurrentRequestNameAndSize(targetId) + results[key] = Result.new(false, { + targetId = targetId, + }) + end + return results + end + + return fetchFunction(networkImpl, targetIds, iconSize):andThen( + function(result) + local results = getTableOfFailedResults(targetIds) + local validIcons = {} + + local data = result and result.responseBody and result.responseBody.data + if typeof(data) == "table" then + for _, iconInfo in pairs(data) do + if Thumbnail.isCompleteThumbnailData(iconInfo) then + local targetId = tostring(iconInfo.targetId) + local success = false + if Thumbnail.checkStateIsFinal(iconInfo.state) then + validIcons[targetId] = Thumbnail.fromThumbnailData(iconInfo, iconSize) + success = true + end + results[keyMapperForCurrentRequestNameAndSize(targetId)] = Result.new(success, iconInfo) + end + end + end + store:dispatch(storeDispatch(validIcons)) + return Promise.resolve(results) + end, + function(err) + local results = getTableOfFailedResults(targetIds) + return Promise.resolve(results) + end + ) +end + +function FetchSubdividedThumbnails._fetch(store, networkImpl, targetIds, size, keyMapper, requestName, fetchFunction, storeDispatch) + return FetchSubdividedThumbnails._fetchIcons(store, networkImpl, targetIds, size, keyMapper, requestName, fetchFunction, storeDispatch) + :andThen(function(results) + local completedIcons = {} + local iconResults = results + + if _G.__TESTEZ_RUNNING_TEST__ then + RETRY_MAX_COUNT = 1 + RETRY_TIME_MULTIPLIER = 0.001 + end + + local function retry(retryCount) + local remainingUnfinalizedIcons = {} + + for k, result in pairs(iconResults) do + local isSuccessful, iconInfo = result:unwrap() + -- Retry icon request for targetId that failed. + if isSuccessful and Thumbnail.checkStateIsFinal(iconInfo.state) then + completedIcons[k] = result + else + table.insert(remainingUnfinalizedIcons, iconInfo) + end + end + + if TableUtilities.FieldCount(remainingUnfinalizedIcons) == 0 then + --All requests are successful + return Promise.resolve(completedIcons) + end + + local delayPromise = Promise.new(function(resolve, reject) + coroutine.wrap(function() + wait(RETRY_TIME_MULTIPLIER * math.pow(2, retryCount - 1)) + resolve() + end)() + end) + + return delayPromise:andThen(function() + return FetchSubdividedThumbnails._fetchIcons(store, networkImpl, + targetIds, size, keyMapper, requestName, fetchFunction, storeDispatch) + end):andThen(function(newResults) + iconResults = newResults + if retryCount > 1 then + return retry(retryCount - 1) + else + return Promise.resolve(completedIcons) + end + end) + end + + return retry(RETRY_MAX_COUNT) + end) +end + +function FetchSubdividedThumbnails.Fetch(networkImpl, requests, keyMapper, requestName, fetchFunction, storeDispatch) + ArgCheck.isType(requests, "table", "requests") + ArgCheck.isType(requestName, "string", "requestName") + ArgCheck.isNonNegativeNumber(#requests, "requests count") + + FetchSubdividedThumbnails.KeyMapper = keyMapper + return PerformFetch.Batch(requests, keyMapper, function(store, filteredrequests) + local targetIdsNeeded = {} + local size + -- Filter out the icons that are already in the store. + for _, request in ipairs(filteredrequests) do + local targetId = request.targetId + size = request.iconSize + table.insert(targetIdsNeeded, targetId) + end + return FetchSubdividedThumbnails._fetch(store, networkImpl, targetIdsNeeded, size, keyMapper, requestName, fetchFunction, storeDispatch) + end) +end + +return FetchSubdividedThumbnails \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ThrottleUserId.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ThrottleUserId.lua new file mode 100644 index 0000000..d1f96c2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ThrottleUserId.lua @@ -0,0 +1,10 @@ +-- Helper function to throttle based on player Id: +return function(throttle, userId) + assert(type(throttle) == "number") + assert(type(userId) == "number") + + -- Determine userRollout using last two digits of user ID: + -- (+1 to change range from 0-99 to 1-100 as 0 is off, 100 is full on): + local userRollout = (userId % 100) + 1 + return userRollout <= throttle +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ThrottleUserId.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ThrottleUserId.spec.lua new file mode 100644 index 0000000..ea0424d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaApp/Utils/ThrottleUserId.spec.lua @@ -0,0 +1,67 @@ +return function() + local ThrottleUserId = require(script.Parent.ThrottleUserId) + + describe("ThrottleUserId", function() + it("should always reject zero%", function() + local gating = ThrottleUserId(0, 10000) + expect(gating).to.equal(false) + + gating = ThrottleUserId(0, 10001) + expect(gating).to.equal(false) + + gating = ThrottleUserId(0, 10025) + expect(gating).to.equal(false) + + gating = ThrottleUserId(0, 10075) + expect(gating).to.equal(false) + + gating = ThrottleUserId(0, 10099) + expect(gating).to.equal(false) + + gating = ThrottleUserId(0, 10100) + expect(gating).to.equal(false) + end) + + it("should always accept 100%", function() + local gating = ThrottleUserId(100, 10000) + expect(gating).to.equal(true) + + gating = ThrottleUserId(100, 10001) + expect(gating).to.equal(true) + + gating = ThrottleUserId(100, 10025) + expect(gating).to.equal(true) + + gating = ThrottleUserId(100, 10075) + expect(gating).to.equal(true) + + gating = ThrottleUserId(100, 10099) + expect(gating).to.equal(true) + + gating = ThrottleUserId(100, 10100) + expect(gating).to.equal(true) + end) + + it("should reject IDs over throttle percent", function() + local gating = ThrottleUserId(25, 10050) + expect(gating).to.equal(false) + + gating = ThrottleUserId(50, 10075) + expect(gating).to.equal(false) + + gating = ThrottleUserId(75, 10099) + expect(gating).to.equal(false) + end) + + it("should accept IDs under throttle percent", function() + local gating = ThrottleUserId(1, 10100) + expect(gating).to.equal(true) + + gating = ThrottleUserId(10, 10109) + expect(gating).to.equal(true) + + gating = ThrottleUserId(25, 10023) + expect(gating).to.equal(true) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/.robloxrc new file mode 100644 index 0000000..635c2ec --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/.robloxrc @@ -0,0 +1,5 @@ +{ + "lint": { + "LocalShadow": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedConversation.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedConversation.lua new file mode 100644 index 0000000..4d63e47 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedConversation.lua @@ -0,0 +1,9 @@ +local Modules = game:GetService("CorePackages").AppTempCommon +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(convo) + return { + conversation = convo, + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedMultiplePlaceInfos.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedMultiplePlaceInfos.lua new file mode 100644 index 0000000..2bb1bb1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedMultiplePlaceInfos.lua @@ -0,0 +1,10 @@ +local Modules = game:GetService("CorePackages").AppTempCommon + +local Common = Modules.Common +local Action = require(Common.Action) + +return Action(script.Name, function(placeInfos) + return { + placeInfos = placeInfos, + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedUserPresence.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedUserPresence.lua new file mode 100644 index 0000000..6d90f86 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Actions/ReceivedUserPresence.lua @@ -0,0 +1,30 @@ +local Modules = game:GetService("CorePackages").AppTempCommon + +local Action = require(Modules.Common.Action) +local LuaDateTime = require(Modules.LuaChat.DateTime) + +return Action(script.Name, function(userId, + presence, lastLocation, + placeId, rootPlaceId, + gameInstanceId, lastOnlineISO, universeId, previousUniverseId) + + local lastOnline = 0 + if lastOnlineISO ~= nil then + local lastDateTime = LuaDateTime.fromIsoDate(lastOnlineISO) + if lastDateTime ~= nil then + lastOnline = lastDateTime:GetUnixTimestamp() + end + end + + return { + userId = userId, + presence = presence, + lastLocation = lastLocation, + placeId = placeId, + rootPlaceId = rootPlaceId, + gameInstanceId = gameInstanceId, + lastOnline = lastOnline, + universeId = universeId, + previousUniverseId = previousUniverseId, + } +end) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/DateTime.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/DateTime.lua new file mode 100644 index 0000000..73dbbdf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/DateTime.lua @@ -0,0 +1,587 @@ +local LocalizationService = game:GetService("LocalizationService") +local CorePackages = game:GetService("CorePackages") +local GetFFlagUseDateTimeType = require(CorePackages.AppTempCommon.LuaApp.Flags.GetFFlagUseDateTimeType) + +--[[ + This is a Lua implementation of the DateTime API proposal. It'll eventually + be implemented in C++ and merged into the rest of the codebase if this model + of working with dates ends up being useful. +]] + +local TimeZone = require(script.Parent.TimeZone) +local TimeUnit = require(script.Parent.TimeUnit) + +local LuaDateTime = {} + +local monthShortNames = { + "Jan", "Feb", "Mar", "Apr", + "May", "Jun", "Jul", "Aug", + "Sep", "Oct", "Nov", "Dec" +} + +local monthLongNames = { + "January", "February", "March", "April", + "May", "June", "July", "August", + "September", "October", "November", "December" +} + +local dayShortNames = { + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" +} + +local dayLongNames = { + "Sunday", "Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday" +} + +--[[ + We structure tokens like this to preserve order, since Lua associative + arrays have no inherent order. +]] +local tokens = { + {"YYYY", function(values) + return tostring(values.Year) + end}, + {"MMMM", function(values) + return monthLongNames[values.Month] + end}, + {"MMM", function(values) + return monthShortNames[values.Month] + end}, + {"MM", function(values) + return ("%02d"):format(values.Month) + end}, + {"M", function(values) + return tostring(values.Month) + end}, + {"DDDD", function(values) + return dayLongNames[values.WeekDay] + end}, + {"DDD", function(values) + return dayShortNames[values.WeekDay] + end}, + {"DD", function(values) + return ("%02d"):format(values.Day) + end}, + {"D", function(values) + return tostring(values.Day) + end}, + {"HH", function(values) + local hour = values.Hour + + return ("%02d"):format(hour) + end}, + {"H", function(values) + local hour = values.Hour + + return tostring(hour) + end}, + {"hh", function(values) + local hour = values.Hour % 12 + if hour == 0 then + hour = 12 + end + + return ("%02d"):format(hour) + end}, + {"h", function(values) + local hour = values.Hour % 12 + if hour == 0 then + hour = 12 + end + + return tostring(hour) + end}, + {"mm", function(values) + return ("%02d"):format(values.Minute) + end}, + {"m", function(values) + return tostring(values.Minute) + end}, + {"ss", function(values) + return ("%02d"):format(values.Seconds) + end}, + {"s", function(values) + return tostring(values.Seconds) + end}, + {"A", function(values) + return values.Hour >= 12 and "PM" or "AM" + end}, + {"a", function(values) + return values.Hour >= 12 and "pm" or "am" + end} +} + +local tokenKeys = {} +for _, pair in ipairs(tokens) do + table.insert(tokenKeys, pair[1]) +end + +local tokenMap = {} +for _, pair in ipairs(tokens) do + tokenMap[pair[1]] = pair[2] +end + +--[[ + What's the next token in this source? +]] +local function getToken(source, i) + local char = source:sub(i, i) + + for _, token in ipairs(tokenKeys) do + -- Only keep checking if the first character matches the token + if token:sub(1, 1) == char then + local match = source:sub(i, i + token:len() - 1) + + if match == token then + return token + end + end + end +end + +--[[ + An estimate of the current time zone's offset from UTC in seconds. + + This might fail for weird timezones (UTC +/- 14), but we can fix that by + picking a reference time that's further away from the Unix epoch. +]] +local function getTimeZoneOffset() + local actualEpoch = 86400 + 43200 + local epoch = os.time({year = 1970, month = 1, day = 2, isdst = -1}) + if epoch then + return actualEpoch - epoch + else + return 0 + end +end + +-- Remove all above functions and tables when clean up GetFFlagUseDateTimeType() + +--[[ + Create a DateTime with the given values in UTC. + + All values are optional! +]] +function LuaDateTime.new(year, month, day, hour, minute, seconds, milliseconds) + if GetFFlagUseDateTimeType() then + local self = {} + self.dateTime = DateTime.fromUniversalTime( + year or 1970, + month or 1, + day or 1, + hour or 0, + minute or 0, + seconds or 0, + milliseconds or 0) + setmetatable(self, LuaDateTime) + return self + end + + local tzOffset = getTimeZoneOffset() + local timestamp = os.time({ + year = year or 1970, + month = month or 1, + day = day or 1, + hour = hour or 0, + min = minute or 0, + sec = seconds or 0, + isdst = -1 + }) + if timestamp == nil then + timestamp = 0 + end + + if seconds then + local subseconds = seconds - math.floor(seconds) + timestamp = timestamp + subseconds + end + + return LuaDateTime.fromUnixTimestamp(timestamp + tzOffset) +end + +--[[ + Create a DateTime representing now. +]] +function LuaDateTime.now() + if GetFFlagUseDateTimeType() then + local self = {} + self.dateTime = DateTime.now() + setmetatable(self, LuaDateTime) + return self + end + + return LuaDateTime.fromUnixTimestamp(os.time()) +end + +--[[ + Create a Datetime from the given Unix timestamp. + + Limited to the range [0, 2^32) when GetFFlagUseDateTimeType() is off, which lets us represent + dates out to about 2038. + + When GetFFlagUseDateTimeType() is on, year range is 1400-9999, and timestamp range is between + first second of year 1400 to the last second of year 9999. +]] +function LuaDateTime.fromUnixTimestamp(timestamp) + assert(type(timestamp) == "number", "Invalid argument #1 to fromUnixTimestamp, expected number.") + + if GetFFlagUseDateTimeType() then + local self = {} + self.dateTime = DateTime.fromUnixTimestampMillis(timestamp*1000) + setmetatable(self, LuaDateTime) + return self + end + + local self = {} + + self.value = timestamp + + setmetatable(self, LuaDateTime) + + return self +end + +--[[ + Attempt to create a DateTime from an ISO 8601 date-time string. + + Will return nil on failure and output a warning to a console denoting what + went wrong. This can probably turned into a second return value if we need + to handle that data programmatically. +]] +function LuaDateTime.fromIsoDate(isoDate) + assert(type(isoDate) == "string", "Invalid argument #1 to DateTime.fromIsoDate, expected string.") + + if GetFFlagUseDateTimeType() then + local self = {} + self.dateTime = DateTime.fromIsoDate(isoDate) + setmetatable(self, LuaDateTime) + return self + end + + local datePattern = "^(%d+)%-(%d+)%-(%d+)" -- 0000-00-00 + local timePattern = "T(%d+):(%d+):(%d+%.?%d*)" -- T00:00:00 + local utcPattern = "Z$" + local timeZonePattern = "([+-]%d+):(%d+)$" -- either Z or +/- followed by "00:00" + + local timezone = 0 + local values = {1970, 1, 1, 0, 0, 0} + local year, month, day = isoDate:match(datePattern) + + if not year then + warn(("Invalid ISO 8601 date: %q"):format(isoDate)) + return nil + end + + values[1] = tonumber(year) + values[2] = tonumber(month) + values[3] = tonumber(day) + + local hour, minute, seconds = isoDate:match(timePattern) + + if hour then + values[4] = tonumber(hour) + values[5] = tonumber(minute) + values[6] = tonumber(seconds) + + local isUtc = isoDate:match(utcPattern) + + if not isUtc then + local offsetHours, offsetMinutes = isoDate:match(timeZonePattern) + + if not offsetHours then + local offsetTotal = getTimeZoneOffset() + offsetHours = offsetTotal / 3600 + offsetMinutes = 0 + + warn(("Invalid time zone in ISO 8601 date: %q -- falling back to local time"):format(isoDate)) + end + + timezone = 3600 * tonumber(offsetHours) + 60 * tonumber(offsetMinutes) + end + end + + local date = LuaDateTime.new(unpack(values)) + date.value = date.value - timezone + + return date +end + +--[[ + Format our current date using a formatting string. Look at the DateTime + proposal to see information about the different formatting tokens. + Generally, they try to resemble LDML and/or Moment.js-style formatting. + + The time zone parameter is optional and defaults to the current time zone, + TimeZone.Current. +]] +function LuaDateTime:Format(formatString, tz, localeId) + assert(type(formatString) == "string", "Invalid argument #1 to Format, expected string.") + + if GetFFlagUseDateTimeType() then + tz = tz or TimeZone.Current + localeId = localeId or LocalizationService.RobloxLocaleId + + if tz == TimeZone.UTC then + return self.dateTime:FormatUniversalTime(formatString, localeId) + elseif tz == TimeZone.Current then + return self.dateTime:FormatLocalTime(formatString, localeId) + else + error(("Invalid TimeZone \"%s\""):format(tostring(tz)), 2) + end + end + + tz = tz or TimeZone.Current + + local values = self:GetValues(tz) + + local buffer = {} + + local i = 1 + while i <= formatString:len() do + local char = formatString:sub(i, i) + local token = getToken(formatString, i) + + if token then + table.insert(buffer, tokenMap[token](values)) + i = i + token:len() + elseif char == "[" then + -- Crawl forward until the next ] and interpret that text literally + local j = i + while j <= formatString:len() do + j = j + 1 + + if formatString:sub(j, j) == "]" then + break + end + end + + table.insert(buffer, formatString:sub(i + 1, j - 1)) + i = j + 1 + else + table.insert(buffer, char) + i = i + 1 + end + end + + local result = table.concat(buffer) + + return result +end + +--[[ + Get a table of values representing the date-time in the given timezone. + + The time zone parameter is optional and defaults to the current zime zone, + TimeZone.Current. + + When GetFFlagUseDateTimeType() is true, table would include + {Year, Month, Day, Hour, Minute, Second, Millisecond} +]] +function LuaDateTime:GetValues(tz) + if GetFFlagUseDateTimeType() then + tz = tz or TimeZone.Current + + if tz == TimeZone.UTC then + return self.dateTime:ToUniversalTime() + elseif tz == TimeZone.Current then + return self.dateTime:ToLocalTime() + else + error(("Invalid TimeZone \"%s\""):format(tostring(tz)), 2) + end + end + + tz = tz or TimeZone.Current + + local reference + + if tz == TimeZone.Current then + reference = os.date("*t", self.value) + elseif tz == TimeZone.UTC then + reference = os.date("!*t", self.value) + end + + if not reference then + error(("Invalid TimeZone \"%s\""):format(tostring(tz)), 2) + end + + return { + Year = reference.year, + Month = reference.month, + Day = reference.day, + Hour = reference.hour, + Minute = reference.min, + Seconds = reference.sec, + WeekDay = reference.wday + } +end + +--[[ + Recover a Unix timestamp representing the DateTime's value. +]] +function LuaDateTime:GetUnixTimestamp() + if GetFFlagUseDateTimeType() then + if self.dateTime:ToUniversalTime().Millisecond > 0 then + return self.dateTime.UnixTimestamp + (self.dateTime.UnixTimestampMillis % 1000)/1000 + else + return self.dateTime.UnixTimestamp + end + end + + return self.value +end + +--[[ + Format the DateTime as an ISO 8601 date string with time attached. + + Always formats the time as UTC. There generally aren't many reasons to + generate an ISO 8601 date in another time zone. +]] +function LuaDateTime:GetIsoDate() + if GetFFlagUseDateTimeType() then + return self.dateTime:ToIsoDate() + end + + return self:Format("YYYY-MM-DD[T]HH:mm:ss[Z]", TimeZone.UTC) +end + +-- Used by IsSame +-- Remove when clean up GetFFlagUseDateTimeType() +local descendingGranularityUnits = { + { + unit = TimeUnit.Years, + key = "Year" + }, + { + unit = TimeUnit.Months, + key = "Month" + }, + { + unit = TimeUnit.Days, + key = "Day" + }, + { + unit = TimeUnit.Hours, + key = "Hour" + }, + { + unit = TimeUnit.Minutes, + key = "Minute" + }, + { + unit = TimeUnit.Seconds, + key = "Seconds" + } +} + +--[[ + Checks whether two DateTime values are the same, given a granularity and + timezone value. + + Granularity defaults to seconds and time zone defaults to the current local + time zone. + + Remove when clean up GetFFlagUseDateTimeType() +]] +function LuaDateTime:IsSame(other, granularity, timezone) + granularity = granularity or TimeUnit.Seconds + timezone = timezone or TimeZone.Current + + local selfUnix = self:GetUnixTimestamp() + local otherUnix = other:GetUnixTimestamp() + + if selfUnix == otherUnix then + return true + end + + local selfValues = self:GetValues(timezone) + local otherValues = other:GetValues(timezone) + + -- Week logic is special + if granularity == TimeUnit.Weeks then + local diff = math.abs(selfUnix - otherUnix) + local diffDays = diff / (60 * 60 * 24) + + -- Two dates separated by 7 or more whole days are never in the same week + if diffDays >= 7 then + return false + end + + -- Two dates separated by less than 7 days will be sorted monotonically + -- if they're in the same week + -- TODO: Use start-of-week value to shift WeekDay for locale + if selfUnix > otherUnix then + return selfValues.WeekDay >= otherValues.WeekDay + else + return selfValues.WeekDay <= otherValues.WeekDay + end + end + + for _, unit in ipairs(descendingGranularityUnits) do + local selfValue = selfValues[unit.key] + local otherValue = otherValues[unit.key] + + if selfValue ~= otherValue then + return false + end + + if unit.unit == granularity then + break + end + end + + return true +end + +--[[ + Get a human-readable timestamp relative to the given epoch, which defaults + to now. The format of the time is contextual to how far away the times are. +]] +function LuaDateTime:GetLongRelativeTime(epoch, timezone, localeId) + if GetFFlagUseDateTimeType() then + -- Not relative time format for now, will do that later in DateTime v2 + return self:Format("lll", timezone, localeId) + end + + timezone = timezone or TimeZone.Current + epoch = epoch or LuaDateTime.now() + + if self:IsSame(epoch, TimeUnit.Days, timezone) then + return self:Format("h:mm A", timezone) + elseif self:IsSame(epoch, TimeUnit.Weeks, timezone) then + return self:Format("DDD | h:mm A", timezone) + elseif self:IsSame(epoch, TimeUnit.Years, timezone) then + return self:Format("MMM D | h:mm A", timezone) + else + return self:Format("MMM D, YYYY | h:mm A", timezone) + end +end + +--[[ + Get a human-readable timestamp relative to the given epoch, which defaults + to now. The format of the time is contextual to how far away the times are. +]] +function LuaDateTime:GetShortRelativeTime(epoch, timezone, localeId) + timezone = timezone or TimeZone.Current + + if GetFFlagUseDateTimeType() then + -- Not relative time format for now, will do that later in DateTime v2 + return self:Format("ll", timezone, localeId) + end + + epoch = epoch or LuaDateTime.now() + + if self:IsSame(epoch, TimeUnit.Days, timezone) then + return self:Format("h:mm A", timezone) + elseif self:IsSame(epoch, TimeUnit.Weeks, timezone) then + return self:Format("DDD", timezone) + elseif self:IsSame(epoch, TimeUnit.Years, timezone) then + return self:Format("MMM D", timezone) + else + return self:Format("MMM D, YYYY", timezone) + end +end + +LuaDateTime.__index = LuaDateTime + +return LuaDateTime \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/DateTime.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/DateTime.spec.lua new file mode 100644 index 0000000..4d9c5ab --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/DateTime.spec.lua @@ -0,0 +1,660 @@ +local CorePackages = game:GetService("CorePackages") +local GetFFlagUseDateTimeType = require(CorePackages.AppTempCommon.LuaApp.Flags.GetFFlagUseDateTimeType) + +return function() + local LuaDateTime = require(script.Parent.DateTime) + local TimeZone = require(script.Parent.TimeZone) + local TimeUnit = require(script.Parent.TimeUnit) + local localeIds = { + "en-us", + "en-gb", + "en-au", + "en-ca", + "en-nz", + "de-de", + "es-es", + "es-mx", + "fr-fr", + "fr-ca", + "it-it", + "pt-pt", + "pt-br", + "ru-ru", + "ja-jp", + "ko-kr", + "zh-cn", + "zh-hk", + "zh-tw", + "zh-hans", + "zh-hant", + "zh-cjv", + } + + describe("Constructors", function() + it("should construct with 'new'", function() + expect(LuaDateTime.new()).to.be.ok() + expect(LuaDateTime.new(2017)).to.be.ok() + expect(LuaDateTime.new(2017, 5)).to.be.ok() + expect(LuaDateTime.new(2017, 5, 3)).to.be.ok() + expect(LuaDateTime.new(2017, 5, 3, 12)).to.be.ok() + expect(LuaDateTime.new(2017, 5, 3, 12, 34)).to.be.ok() + expect(LuaDateTime.new(2017, 5, 3, 12, 34, 51)).to.be.ok() + + if GetFFlagUseDateTimeType() then + expect(LuaDateTime.new(2017, 5, 3, 12, 34, 51, 999)).to.be.ok() + end + end) + + it("should construct with 'now'", function() + expect(LuaDateTime.now()).to.be.ok() + end) + + it("should construct from a Unix timestamp", function() + expect(LuaDateTime.fromUnixTimestamp(0)).to.be.ok() + expect(LuaDateTime.fromUnixTimestamp(os.time())).to.be.ok() + end) + + it("should construct from an ISO 8601 date", function() + if GetFFlagUseDateTimeType() then + -- Basic date + do + local date = LuaDateTime.fromIsoDate("1988-03-17") + expect(date).to.be.ok() + expect(date.dateTime:ToUniversalTime().Year).to.equal(1988) + expect(date.dateTime:ToUniversalTime().Month).to.equal(3) + expect(date.dateTime:ToUniversalTime().Day).to.equal(17) + expect(date.dateTime:ToUniversalTime().Hour).to.equal(0) + expect(date.dateTime:ToUniversalTime().Minute).to.equal(0) + expect(date.dateTime:ToUniversalTime().Second).to.equal(0) + expect(date.dateTime:ToUniversalTime().Millisecond).to.equal(0) + end + + -- Date and time + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16.999Z") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816.999) + expect(date.dateTime:ToUniversalTime().Year).to.equal(2017) + expect(date.dateTime:ToUniversalTime().Month).to.equal(4) + expect(date.dateTime:ToUniversalTime().Day).to.equal(10) + expect(date.dateTime:ToUniversalTime().Hour).to.equal(20) + expect(date.dateTime:ToUniversalTime().Minute).to.equal(40) + expect(date.dateTime:ToUniversalTime().Second).to.equal(16) + expect(date.dateTime:ToUniversalTime().Millisecond).to.equal(999) + end + + -- Date and time with no time zone + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16.1") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816.1) + expect(date.dateTime:ToUniversalTime().Year).to.equal(2017) + expect(date.dateTime:ToUniversalTime().Month).to.equal(4) + expect(date.dateTime:ToUniversalTime().Day).to.equal(10) + expect(date.dateTime:ToUniversalTime().Hour).to.equal(20) + expect(date.dateTime:ToUniversalTime().Minute).to.equal(40) + expect(date.dateTime:ToUniversalTime().Second).to.equal(16) + expect(date.dateTime:ToUniversalTime().Millisecond).to.equal(100) + end + + -- Date, time, and time zone offset + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16+01:00") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816 - 3600) + expect(date.dateTime:ToUniversalTime().Year).to.equal(2017) + expect(date.dateTime:ToUniversalTime().Month).to.equal(4) + expect(date.dateTime:ToUniversalTime().Day).to.equal(10) + expect(date.dateTime:ToUniversalTime().Hour).to.equal(19) + expect(date.dateTime:ToUniversalTime().Minute).to.equal(40) + expect(date.dateTime:ToUniversalTime().Second).to.equal(16) + expect(date.dateTime:ToUniversalTime().Millisecond).to.equal(0) + end + + -- Date, time, and negative time zone offset + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16-01:00") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816 + 3600) + expect(date.dateTime:ToUniversalTime().Year).to.equal(2017) + expect(date.dateTime:ToUniversalTime().Month).to.equal(4) + expect(date.dateTime:ToUniversalTime().Day).to.equal(10) + expect(date.dateTime:ToUniversalTime().Hour).to.equal(21) + expect(date.dateTime:ToUniversalTime().Minute).to.equal(40) + expect(date.dateTime:ToUniversalTime().Second).to.equal(16) + expect(date.dateTime:ToUniversalTime().Millisecond).to.equal(0) + end + else + -- Basic date + do + local date = LuaDateTime.fromIsoDate("1988-03-17") + expect(date).to.be.ok() + end + + -- Date and time + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16Z") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816) + end + + -- Date and time with no time zone + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16") + expect(date).to.be.ok() + end + + -- Date, time, and time zone offset + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16+01:00") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816 - 3600) + end + + -- Date, time, and negative time zone offset + do + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16-01:00") + expect(date).to.be.ok() + expect(date:GetUnixTimestamp()).to.equal(1491856816 + 3600) + end + end + end) + end) + + describe("Measurements", function() + it("should get values in UTC", function() + local date = LuaDateTime.new() + local values = date:GetValues(TimeZone.UTC) + + expect(values).to.be.ok() + expect(values.Year).to.be.a("number") + expect(values.Month).to.be.a("number") + expect(values.Day).to.be.a("number") + expect(values.Hour).to.be.a("number") + expect(values.Minute).to.be.a("number") + + if GetFFlagUseDateTimeType() then + expect(values.Second).to.be.a("number") + expect(values.Millisecond).to.be.a("number") + else + expect(values.Seconds).to.be.a("number") + + -- Locale specific! + expect(values.WeekDay).to.be.a("number") + end + end) + + it("should get values in local time", function() + local date = LuaDateTime.new() + local values = date:GetValues(TimeZone.Current) + + expect(values).to.be.ok() + expect(values.Year).to.be.a("number") + expect(values.Month).to.be.a("number") + expect(values.Day).to.be.a("number") + expect(values.Hour).to.be.a("number") + expect(values.Minute).to.be.a("number") + + if GetFFlagUseDateTimeType() then + expect(values.Second).to.be.a("number") + expect(values.Millisecond).to.be.a("number") + else + expect(values.Seconds).to.be.a("number") + + -- Locale specific! + expect(values.WeekDay).to.be.a("number") + end + end) + + it("should preserve values from 'new' constructor", function() + local date = LuaDateTime.new(2017, 11, 3, 12, 34, 51) + local values = date:GetValues(TimeZone.UTC) + + expect(values.Year).to.equal(2017) + expect(values.Month).to.equal(11) + expect(values.Day).to.equal(3) + expect(values.Hour).to.equal(12) + expect(values.Minute).to.equal(34) + + if GetFFlagUseDateTimeType() then + expect(values.Second).to.equal(51) + expect(values.Millisecond).to.equal(0) + else + expect(values.Seconds).to.equal(51) + end + end) + + it("should preserve Unix timestamp values", function() + do + local date = LuaDateTime.fromUnixTimestamp(0) + expect(date:GetUnixTimestamp()).to.equal(0) + end + + do + local date = LuaDateTime.fromUnixTimestamp(123456789) + expect(date:GetUnixTimestamp()).to.equal(123456789) + end + end) + end) + + describe("Formatting", function() + it("should preserve text within brackets", function() + local date = LuaDateTime.new(2017, 1, 2, 15, 8, 9) + + local function format(str) + return date:Format(str, TimeZone.UTC) + end + + expect(format("[Hello, world!]")).to.equal("Hello, world!") + expect(format("[YYYY-MM-DD]")).to.equal("YYYY-MM-DD") + end) + + it("should create identical ISO 8601 dates for UTC inputs", function() + local date = LuaDateTime.fromIsoDate("2017-04-10T20:40:16Z") + expect(date:GetIsoDate()).to.equal("2017-04-10T20:40:16Z") + end) + + it("should have correct formatting tokens", function() + local date = LuaDateTime.new(2016, 1, 2, 15, 8, 9) + + -- Shortcut time zone specification + local function format(str, localeId) + return date:Format(str, TimeZone.UTC, localeId) + end + + expect(format("YYYY")).to.equal("2016") + expect(format("M")).to.equal("1") + expect(format("MM")).to.equal("01") + expect(format("D")).to.equal("2") + expect(format("DD")).to.equal("02") + expect(format("H")).to.equal("15") + expect(format("HH")).to.equal("15") + expect(format("h")).to.equal("3") + expect(format("hh")).to.equal("03") + expect(format("m")).to.equal("8") + expect(format("mm")).to.equal("08") + expect(format("s")).to.equal("9") + expect(format("ss")).to.equal("09") + + -- Locale-specific tests! + if GetFFlagUseDateTimeType() then + expect(format("SSS")).to.equal("000") + expect(format("SS")).to.equal("00") + expect(format("S")).to.equal("0") + expect(format("MMM", "en-us")).to.equal("Jan") + expect(format("MMMM", "en-us")).to.equal("January") + expect(format("MMM", "zh-cn")).to.equal("1月") + expect(format("MMMM", "zh-cn")).to.equal("一月") + expect(format("A", "en-us")).to.equal("PM") + expect(format("a", "en-us")).to.equal("pm") + expect(format("A", "zh-cn")).to.equal("下午") + expect(format("a", "zh-cn")).to.equal("下午") + else + expect(format("MMM")).to.equal("Jan") + expect(format("MMMM")).to.equal("January") + expect(format("A")).to.equal("PM") + expect(format("a")).to.equal("pm") + end + end) + + it("should handle dates around midnight", function() + local date = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + + expect(date:Format("H", TimeZone.UTC)).to.equal("0") + expect(date:Format("HH", TimeZone.UTC)).to.equal("00") + expect(date:Format("h", TimeZone.UTC)).to.equal("12") + expect(date:Format("hh", TimeZone.UTC)).to.equal("12") + + if GetFFlagUseDateTimeType() then + expect(date:Format("A", TimeZone.UTC, "en-us")).to.equal("AM") + expect(date:Format("a", TimeZone.UTC, "en-us")).to.equal("am") + expect(date:Format("A", TimeZone.UTC, "zh-cn")).to.equal("凌晨") + expect(date:Format("a", TimeZone.UTC, "zh-cn")).to.equal("凌晨") + else + expect(date:Format("a", TimeZone.UTC)).to.equal("am") + end + + end) + + it("should handle dates around noon", function() + local date = LuaDateTime.new(2017, 5, 23, 12, 0, 0) + + expect(date:Format("H", TimeZone.UTC)).to.equal("12") + expect(date:Format("HH", TimeZone.UTC)).to.equal("12") + expect(date:Format("h", TimeZone.UTC)).to.equal("12") + expect(date:Format("hh", TimeZone.UTC)).to.equal("12") + + if GetFFlagUseDateTimeType() then + expect(date:Format("A", TimeZone.UTC, "en-us")).to.equal("PM") + expect(date:Format("a", TimeZone.UTC, "en-us")).to.equal("pm") + expect(date:Format("A", TimeZone.UTC, "zh-cn")).to.equal("中午") + expect(date:Format("a", TimeZone.UTC, "zh-cn")).to.equal("中午") + else + expect(date:Format("a", TimeZone.UTC)).to.equal("pm") + end + end) + + it("should return correct 24-hour clock sequences", function() + local expected = { + "2017-09-13 00:00:00", + "2017-09-13 01:00:00", + "2017-09-13 02:00:00", + "2017-09-13 03:00:00", + "2017-09-13 04:00:00", + "2017-09-13 05:00:00", + "2017-09-13 06:00:00", + "2017-09-13 07:00:00", + "2017-09-13 08:00:00", + "2017-09-13 09:00:00", + "2017-09-13 10:00:00", + "2017-09-13 11:00:00", + "2017-09-13 12:00:00", + "2017-09-13 13:00:00", + "2017-09-13 14:00:00", + "2017-09-13 15:00:00", + "2017-09-13 16:00:00", + "2017-09-13 17:00:00", + "2017-09-13 18:00:00", + "2017-09-13 19:00:00", + "2017-09-13 20:00:00", + "2017-09-13 21:00:00", + "2017-09-13 22:00:00", + "2017-09-13 23:00:00", + "2017-09-14 00:00:00", + "2017-09-14 01:00:00", + } + + local formatString = "YYYY-MM-DD HH:mm:ss" + local date + + for _, localeId in ipairs(localeIds) do + date = LuaDateTime.new(2017, 9, 13, 0, 0, 0) + for i = 1, #expected do + local result = date:Format(formatString, TimeZone.UTC, localeId) + expect(result).to.equal(expected[i]) + + -- Advance once hour + date = date.fromUnixTimestamp(date:GetUnixTimestamp() + 3600) + end + end + end) + + it("should return correct 12-hour clock sequences", function() + local date = LuaDateTime.new(2017, 9, 13, 0, 0, 0) + + local formatString = "YYYY-MM-DD hh:mm:ss a" + + local expected + if GetFFlagUseDateTimeType() then + expected = { + ["en-us"] = { + "2017-09-13 12:00:00 am", + "2017-09-13 01:00:00 am", + "2017-09-13 02:00:00 am", + "2017-09-13 03:00:00 am", + "2017-09-13 04:00:00 am", + "2017-09-13 05:00:00 am", + "2017-09-13 06:00:00 am", + "2017-09-13 07:00:00 am", + "2017-09-13 08:00:00 am", + "2017-09-13 09:00:00 am", + "2017-09-13 10:00:00 am", + "2017-09-13 11:00:00 am", + "2017-09-13 12:00:00 pm", + "2017-09-13 01:00:00 pm", + "2017-09-13 02:00:00 pm", + "2017-09-13 03:00:00 pm", + "2017-09-13 04:00:00 pm", + "2017-09-13 05:00:00 pm", + "2017-09-13 06:00:00 pm", + "2017-09-13 07:00:00 pm", + "2017-09-13 08:00:00 pm", + "2017-09-13 09:00:00 pm", + "2017-09-13 10:00:00 pm", + "2017-09-13 11:00:00 pm", + "2017-09-14 12:00:00 am", + "2017-09-14 01:00:00 am", + }, + ["zh-cn"] = { + "2017-09-13 12:00:00 凌晨", + "2017-09-13 01:00:00 凌晨", + "2017-09-13 02:00:00 凌晨", + "2017-09-13 03:00:00 凌晨", + "2017-09-13 04:00:00 凌晨", + "2017-09-13 05:00:00 凌晨", + "2017-09-13 06:00:00 早上", + "2017-09-13 07:00:00 早上", + "2017-09-13 08:00:00 早上", + "2017-09-13 09:00:00 上午", + "2017-09-13 10:00:00 上午", + "2017-09-13 11:00:00 上午", + "2017-09-13 12:00:00 中午", + "2017-09-13 01:00:00 下午", + "2017-09-13 02:00:00 下午", + "2017-09-13 03:00:00 下午", + "2017-09-13 04:00:00 下午", + "2017-09-13 05:00:00 下午", + "2017-09-13 06:00:00 晚上", + "2017-09-13 07:00:00 晚上", + "2017-09-13 08:00:00 晚上", + "2017-09-13 09:00:00 晚上", + "2017-09-13 10:00:00 晚上", + "2017-09-13 11:00:00 晚上", + "2017-09-14 12:00:00 凌晨", + "2017-09-14 01:00:00 凌晨", + } + } + + for i = 1, #expected do + for j = 1, #expected[i] do + local result = date:Format(formatString, TimeZone.UTC, expected[i]) + expect(result).to.equal(expected[i][j]) + + -- Advance once hour + date = date.fromUnixTimestamp(date:GetUnixTimestamp() + 3600) + end + end + else + expected = { + "2017-09-13 12:00:00 am", + "2017-09-13 01:00:00 am", + "2017-09-13 02:00:00 am", + "2017-09-13 03:00:00 am", + "2017-09-13 04:00:00 am", + "2017-09-13 05:00:00 am", + "2017-09-13 06:00:00 am", + "2017-09-13 07:00:00 am", + "2017-09-13 08:00:00 am", + "2017-09-13 09:00:00 am", + "2017-09-13 10:00:00 am", + "2017-09-13 11:00:00 am", + "2017-09-13 12:00:00 pm", + "2017-09-13 01:00:00 pm", + "2017-09-13 02:00:00 pm", + "2017-09-13 03:00:00 pm", + "2017-09-13 04:00:00 pm", + "2017-09-13 05:00:00 pm", + "2017-09-13 06:00:00 pm", + "2017-09-13 07:00:00 pm", + "2017-09-13 08:00:00 pm", + "2017-09-13 09:00:00 pm", + "2017-09-13 10:00:00 pm", + "2017-09-13 11:00:00 pm", + "2017-09-14 12:00:00 am", + "2017-09-14 01:00:00 am", + } + + for i = 1, #expected do + local result = date:Format(formatString, TimeZone.UTC) + expect(result).to.equal(expected[i]) + + -- Advance once hour + date = date.fromUnixTimestamp(date:GetUnixTimestamp() + 3600) + end + end + end) + + describe("LongRelativeTime", function() + if GetFFlagUseDateTimeType() then + it("SHOULD handle UTC time correctly with different locales", function() + local date = LuaDateTime.new(2015, 4, 20, 13, 0, 0) + for _, localeId in ipairs(localeIds) do + local longRelativeTime = date:GetLongRelativeTime(date, TimeZone.UTC, localeId) + expect(longRelativeTime).to.equal(date.dateTime:FormatUniversalTime("lll", localeId)) + end + end) + + it("SHOULD handle Local time correctly with different locales", function() + local date = LuaDateTime.fromUnixTimestamp(DateTime.fromLocalTime(2015, 4, 20, 13, 0, 0).UnixTimestamp) + for _, localeId in ipairs(localeIds) do + local longRelativeTime = date:GetLongRelativeTime(date, TimeZone.Current, localeId) + expect(longRelativeTime).to.equal(date.dateTime:FormatLocalTime("lll", localeId)) + end + end) + else + it("SHOULD handle same day case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2015, 4, 20, 13, 0, 0) + expect(date:GetLongRelativeTime(now, TimeZone.UTC)).to.equal("1:00 PM") + end) + + it("SHOULD handle same week case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2015, 4, 19, 13, 0, 0) + expect(date:GetLongRelativeTime(now, TimeZone.UTC)).to.equal("Sun | 1:00 PM") + end) + + it("SHOULD handle same year case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2015, 1, 20, 13, 0, 0) + expect(date:GetLongRelativeTime(now, TimeZone.UTC)).to.equal("Jan 20 | 1:00 PM") + end) + + it("SHOULD handle different year case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2010, 1, 20, 13, 0, 0) + expect(date:GetLongRelativeTime(now, TimeZone.UTC)).to.equal("Jan 20, 2010 | 1:00 PM") + end) + end + end) + + describe("ShortRelativeTime", function() + if GetFFlagUseDateTimeType() then + it("SHOULD handle UTC time correctly with different locales", function() + local date = LuaDateTime.new(2015, 4, 20, 13, 0, 0) + for _, localeId in ipairs(localeIds) do + local shortRelativeTime = date:GetShortRelativeTime(date, TimeZone.UTC, localeId) + expect(shortRelativeTime).to.equal(date.dateTime:FormatUniversalTime("ll", localeId)) + end + end) + + it("SHOULD handle Local time correctly with different locales", function() + local date = LuaDateTime.fromUnixTimestamp(DateTime.fromLocalTime(2015, 4, 20, 13, 0, 0).UnixTimestamp) + for _, localeId in ipairs(localeIds) do + local shortRelativeTime = date:GetShortRelativeTime(date, TimeZone.Current, localeId) + expect(shortRelativeTime).to.equal(date.dateTime:FormatLocalTime("ll", localeId)) + end + end) + else + it("SHOULD handle same day case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2015, 4, 20, 13, 0, 0) + expect(date:GetShortRelativeTime(now, TimeZone.UTC)).to.equal("1:00 PM") + end) + + it("SHOULD handle same week case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2015, 4, 19, 13, 0, 0) + expect(date:GetShortRelativeTime(now, TimeZone.UTC)).to.equal("Sun") + end) + + it("SHOULD handle same year case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2015, 1, 20, 13, 0, 0) + expect(date:GetShortRelativeTime(now, TimeZone.UTC)).to.equal("Jan 20") + end) + + it("SHOULD handle different year case", function() + local now = LuaDateTime.new(2015, 4, 20, 0, 0, 0) + local date = LuaDateTime.new(2010, 1, 20, 13, 0, 0) + expect(date:GetShortRelativeTime(now, TimeZone.UTC)).to.equal("Jan 20, 2010") + end) + end + end) + end) + + if not GetFFlagUseDateTimeType() then + describe("Comparisons", function() + describe("IsSame", function() + it("should equate dates with different granularity", function() + local value = LuaDateTime.new(2003, 6, 11, 15, 8, 9) + local same = LuaDateTime.new(2003, 6, 11, 15, 8, 9) + + expect(value:IsSame(value)).to.equal(true) + expect(value:IsSame(same)).to.equal(true) + + local units = {TimeUnit.Years, TimeUnit.Months, TimeUnit.Days, TimeUnit.Hours, TimeUnit.Minutes} + for _, unit in ipairs(units) do + expect(value:IsSame(same, unit)).to.equal(true) + end + + local sameMinute = LuaDateTime.new(2003, 6, 11, 15, 8, 10) + + expect(value:IsSame(sameMinute)).to.equal(false) + expect(value:IsSame(sameMinute, TimeUnit.Minutes)).to.equal(true) + expect(value:IsSame(sameMinute, TimeUnit.Years)).to.equal(true) + + local sameHour = LuaDateTime.new(2003, 6, 11, 15, 9, 0) + + expect(value:IsSame(sameHour)).to.equal(false) + expect(value:IsSame(sameHour, TimeUnit.Hours)).to.equal(true) + expect(value:IsSame(sameHour, TimeUnit.Years)).to.equal(true) + + local sameDay = LuaDateTime.new(2003, 6, 11, 14, 8, 9) + + expect(value:IsSame(sameDay)).to.equal(false) + expect(value:IsSame(sameDay, TimeUnit.Days)).to.equal(true) + expect(value:IsSame(sameDay, TimeUnit.Years)).to.equal(true) + + local sameMonth = LuaDateTime.new(2003, 6, 12, 15, 8, 9) + + expect(value:IsSame(sameMonth)).to.equal(false) + expect(value:IsSame(sameMonth, TimeUnit.Months)).to.equal(true) + expect(value:IsSame(sameMonth, TimeUnit.Years)).to.equal(true) + + local sameYear = LuaDateTime.new(2003, 7, 12, 15, 8, 9) + + expect(value:IsSame(sameYear)).to.equal(false) + expect(value:IsSame(sameYear, TimeUnit.Years)).to.equal(true) + + local diffYear = LuaDateTime.new(2004, 6, 11, 15, 8, 9) + + expect(value:IsSame(diffYear)).to.equal(false) + expect(value:IsSame(diffYear, TimeUnit.Years)).to.equal(false) + end) + + it("should equate values using week boundaries", function() + local sunday = LuaDateTime.new(2017, 5, 7) + local saturday = LuaDateTime.new(2017, 5, 13) + local monday = LuaDateTime.new(2017, 5, 8) + local tuesday = LuaDateTime.new(2017, 5, 9) + + -- TODO: Specify locale when that lands; default may break tests + local function sameWeek(a, b) + return a:IsSame(b, TimeUnit.Weeks, TimeZone.UTC) + end + + expect(sameWeek(monday, monday)).to.equal(true) + + expect(sameWeek(sunday, monday)).to.equal(true) + expect(sameWeek(tuesday, monday)).to.equal(true) + expect(sameWeek(saturday, monday)).to.equal(true) + + local nextSunday = LuaDateTime.new(2017, 5, 14) + + expect(sameWeek(nextSunday, monday)).to.equal(false) + end) + end) + end) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Flags/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Flags/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Flags/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Flags/isNewFriendsEndpointsEnabled.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Flags/isNewFriendsEndpointsEnabled.lua new file mode 100644 index 0000000..70fef7e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Flags/isNewFriendsEndpointsEnabled.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") +local ThrottleUserId = require(CorePackages.AppTempCommon.LuaApp.Utils.ThrottleUserId) + +return function() + return ThrottleUserId( + game:DefineFastInt("LuaChatUseNewFriendsEndpointsV2", 0), + Players.LocalPlayer.UserId + ) +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Models/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Models/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Models/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Models/PlaceInfoModel.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Models/PlaceInfoModel.lua new file mode 100644 index 0000000..216a257 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Models/PlaceInfoModel.lua @@ -0,0 +1,45 @@ +local CorePackages = game:GetService("CorePackages") + +local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId) + +local FFlagLuaAppConvertUniverseIdToString = settings():GetFFlag("LuaAppConvertUniverseIdToStringV364") + +local PlaceInfoModel = {} + +function PlaceInfoModel.new() + local self = {} + + return self +end + +function PlaceInfoModel.mock() + local self = PlaceInfoModel.new() + + self.builder = "builder" + self.builderId = MockId() + self.description = "description" + self.imageToken = MockId() + self.isPlayable = true + self.name = "name" + self.placeId = MockId() + self.price = 0 + self.reasonProhibited = nil + self.universeId = MockId() + self.universeRootPlaceId = MockId() + self.url = "url" + + return self +end + +function PlaceInfoModel.fromWeb(data) + local self = data or {} + self.placeId = tostring(self.placeId) + + if FFlagLuaAppConvertUniverseIdToString then + self.universeId = tostring(self.universeId) + end + + return self +end + +return PlaceInfoModel \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/FriendCount.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/FriendCount.lua new file mode 100644 index 0000000..bab1179 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/FriendCount.lua @@ -0,0 +1,12 @@ +local CorePackages = game:GetService("CorePackages") +local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount) + +return function(state, action) + state = state or 0 + + if action.type == SetFriendCount.name then + state = action.count + end + + return state +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/FriendCount.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/FriendCount.spec.lua new file mode 100644 index 0000000..d7cc7ec --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/FriendCount.spec.lua @@ -0,0 +1,18 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local FriendCount = require(CorePackages.AppTempCommon.LuaChat.Reducers.FriendCount) + local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount) + + it("should be zero by default", function() + local state = FriendCount(nil, {}) + + expect(state).to.equal(0) + end) + + it("should respond to SetFriendCount", function() + local state = FriendCount(nil, {}) + state = FriendCount(state, SetFriendCount(520)) + + expect(state).to.equal(520) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.lua new file mode 100644 index 0000000..4a9ab03 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.lua @@ -0,0 +1,22 @@ +local CorePackages = game:GetService("CorePackages") + +local Common = CorePackages.AppTempCommon.Common +local LuaChat = CorePackages.AppTempCommon.LuaChat + +local ReceivedMultiplePlaceInfos = require(LuaChat.Actions.ReceivedMultiplePlaceInfos) + +local Immutable = require(Common.Immutable) + +return function(state, action) + state = state or {} + if action.type == ReceivedMultiplePlaceInfos.name then + + local newInfos = {} + for _, placeInfo in ipairs(action.placeInfos) do + newInfos[placeInfo.placeId] = placeInfo + end + + state = Immutable.JoinDictionaries(state, newInfos) + end + return state +end diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.spec.lua new file mode 100644 index 0000000..f23ff9e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Reducers/PlaceInfos.spec.lua @@ -0,0 +1,36 @@ +return function() + local CorePackages = game:GetService("CorePackages") + + local LuaApp = CorePackages.AppTempCommon.LuaApp + local LuaChat = CorePackages.AppTempCommon.LuaChat + + local MockId = require(LuaApp.MockId) + local ReceivedMultiplePlaceInfos = require(LuaChat.Actions.ReceivedMultiplePlaceInfos) + + local PlaceInfosReducer = require(script.Parent.PlaceInfos) + + describe("initial state", function() + it("should return an initial table when passed nil", function() + local state = PlaceInfosReducer(nil, {}) + expect(state).to.be.a("table") + end) + end) + + describe("ReceivedMultiplePlaceInfos", function() + it("should add place info to the store", function() + local state = PlaceInfosReducer(nil, {}) + + local placeId = MockId() + local returnedPlaceInfo = ReceivedMultiplePlaceInfos({ + { + placeId = placeId, + imageToken = "image-token", + }, + }) + + state = PlaceInfosReducer(state, returnedPlaceInfo) + + expect(state[placeId]).to.equal(returnedPlaceInfo.placeInfos[1]) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/TimeUnit.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/TimeUnit.lua new file mode 100644 index 0000000..1ce9701 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/TimeUnit.lua @@ -0,0 +1,17 @@ +local TimeUnit = setmetatable({}, { + __index = function(self, key) + error(("Invalid TimeUnit \"%s\""):format(tostring(key)), 2) + end +}) + +TimeUnit.Seconds = "Seconds" +TimeUnit.Minutes = "Minutes" +TimeUnit.Hours = "Hours" +TimeUnit.Days = "Days" +TimeUnit.Months = "Months" +TimeUnit.Years = "Years" + +-- Locale-specific +TimeUnit.Weeks = "Weeks" + +return TimeUnit \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/TimeZone.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/TimeZone.lua new file mode 100644 index 0000000..8c1e0c5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/TimeZone.lua @@ -0,0 +1,10 @@ +local TimeZone = setmetatable({}, { + __index = function(self, key) + error(("Invalid TimeZone \"%s\""):format(tostring(key)), 2) + end +}) + +TimeZone.UTC = -2 +TimeZone.Current = -1 + +return TimeZone \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/.robloxrc new file mode 100644 index 0000000..8d03e19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/getFriendsActiveGamesPlaceIdsFromUsersPresence.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/getFriendsActiveGamesPlaceIdsFromUsersPresence.lua new file mode 100644 index 0000000..a42715f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/getFriendsActiveGamesPlaceIdsFromUsersPresence.lua @@ -0,0 +1,19 @@ +local CorePackages = game:GetService("CorePackages") + +local User = require(CorePackages.AppTempCommon.LuaApp.Models.User) +local WebPresenceMap = require(CorePackages.AppTempCommon.LuaApp.Enum.WebPresenceMap) +local convertUniverseIdToString = require(CorePackages.AppTempCommon.LuaApp.Flags.ConvertUniverseIdToString) + +return function(friendsPresence, store) + local placeIds = {} + + for _, presenceModel in pairs(friendsPresence) do + local universeId = convertUniverseIdToString(presenceModel.universeId) + if WebPresenceMap[presenceModel.userPresenceType] == User.PresenceType.IN_GAME + and (not store:getState().UniversePlaceInfos[universeId]) then + table.insert(placeIds, presenceModel.placeId) + end + end + + return placeIds +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/receiveUsersPresence.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/receiveUsersPresence.lua new file mode 100644 index 0000000..0f75e18 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/LuaChat/Utils/receiveUsersPresence.lua @@ -0,0 +1,32 @@ +local CorePackages = game:GetService("CorePackages") + +local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence) +local WebPresenceMap = require(CorePackages.AppTempCommon.LuaApp.Enum.WebPresenceMap) + +local FFlagLuaAppConvertUniverseIdToString = settings():GetFFlag("LuaAppConvertUniverseIdToStringV364") + +return function(friendsPresence, store) + for _, presenceModel in pairs(friendsPresence) do + local userInStore = store:getState().Users[tostring(presenceModel.userId)] + local previousUniverseId = userInStore and userInStore.universeId or nil + + local universeId + if FFlagLuaAppConvertUniverseIdToString then + universeId = presenceModel.universeId and tostring(presenceModel.universeId) or nil + else + universeId = presenceModel.universeId + end + + store:dispatch(ReceivedUserPresence( + tostring(presenceModel.userId), + WebPresenceMap[presenceModel.userPresenceType], + presenceModel.lastLocation, + presenceModel.placeId and tostring(presenceModel.placeId) or nil, + presenceModel.rootPlaceId and tostring(presenceModel.rootPlaceId) or nil, + presenceModel.gameId and tostring(presenceModel.gameId) or nil, + presenceModel.lastOnline and tostring(presenceModel.lastOnline) or nil, + universeId, + previousUniverseId + )) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/.robloxrc b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/.robloxrc new file mode 100644 index 0000000..e721482 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/.robloxrc @@ -0,0 +1,9 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalShadow": "fatal", + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/EventStream.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/EventStream.lua new file mode 100644 index 0000000..c630b9d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/EventStream.lua @@ -0,0 +1,78 @@ +local AnalyticsService = game:GetService("RbxAnalyticsService") +local RunService = game:GetService("RunService") +local UserInputService = game:GetService("UserInputService") + +local SETTINGS_HUB_INVITE_RELEASE_STREAM_TIME = 10 + +local function getPlatformTarget() + local platformTarget = "unknownLua" + local platformEnum = Enum.Platform.None + + -- the call to GetPlatform is wrapped in a pcall() because the Testing Service + -- executes the scripts in the wrong authorization level + pcall(function() + platformEnum = UserInputService:GetPlatform() + end) + + -- bucket the platform based on consumer platform + local isDesktopClient = (platformEnum == Enum.Platform.Windows) or (platformEnum == Enum.Platform.OSX) + + local isMobileClient = (platformEnum == Enum.Platform.IOS) or (platformEnum == Enum.Platform.Android) + isMobileClient = isMobileClient or (platformEnum == Enum.Platform.UWP) + + local isConsole = (platformEnum == Enum.Platform.XBox360) or (platformEnum == Enum.Platform.XBoxOne) + isConsole = isConsole or (platformEnum == Enum.Platform.PS3) or (platformEnum == Enum.Platform.PS4) + isConsole = isConsole or (platformEnum == Enum.Platform.WiiU) + + -- assign a target based on the form factor + if isDesktopClient then + platformTarget = "client" + elseif isMobileClient then + platformTarget = "mobile" + elseif isConsole then + platformTarget = "console" + else + -- if we don't have a name for the form factor, report it here so that we can eventually track it down + platformTarget = platformTarget .. tostring(platformEnum) + end + + return platformTarget +end + +local EventStream = {} +EventStream.__index = EventStream + +function EventStream.new(overridePlatformTarget, overrideAnalyticsImpl) + local self = {} + setmetatable(self, EventStream) + + self._analyticsImpl = overrideAnalyticsImpl or AnalyticsService + self._platformTarget = overridePlatformTarget or getPlatformTarget() + + return self +end + +function EventStream:setRBXEventStream(eventContext, eventName, additionalArgs) + additionalArgs = additionalArgs or {} + -- this function sends reports to the server in batches, not real-time + self._analyticsImpl:SetRBXEventStream(self._platformTarget, eventContext, eventName, additionalArgs) + + if not self.timerSteppedConnection then + local lastGameTime = time() + self.timerSteppedConnection = RunService.Stepped:Connect(function(gameTime) + if gameTime - lastGameTime > SETTINGS_HUB_INVITE_RELEASE_STREAM_TIME then + self:releaseRBXEventStream() + end + end) + end +end + +function EventStream:releaseRBXEventStream() + self._analyticsImpl:ReleaseRBXEventStream(self._platformTarget) + if self.timerSteppedConnection then + self.timerSteppedConnection:Disconnect() + self.timerSteppedConnection = nil + end +end + +return EventStream diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/httpRequest.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/httpRequest.lua new file mode 100644 index 0000000..7a0751a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/httpRequest.lua @@ -0,0 +1,97 @@ +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local LuaApp = CorePackages.AppTempCommon.LuaApp + +local Promise = require(LuaApp.Promise) + +local DEFAULT_THROTTLING_PRIORITY = Enum.ThrottlingPriority.Extreme +local DEFAULT_POST_ASYNC_CONTENT_TYPE = Enum.HttpContentType.ApplicationJson + +-- httpRequest : (table, optional) an object that implements the same http functions as the data model +return function(httpImpl) + + local function doHttpPost(url, options) + local jsonPayload + assert(options.postBody, "Expected a postBody to be specified with this request") + if type(options.postBody) == "table" then + jsonPayload = HttpService:JSONEncode(options.postBody) + elseif type(options.postBody) == "string" then + jsonPayload = options.postBody + else + error("Expected postBody to be a string or table") + end + + if not options.contentType then + options.contentType = DEFAULT_POST_ASYNC_CONTENT_TYPE + end + + if not options.throttlingPriority then + options.throttlingPriority = DEFAULT_THROTTLING_PRIORITY + end + + return function() + return httpImpl:PostAsyncFullUrl( + url, + jsonPayload, + options.throttlingPriority, + options.contentType + ) + end + end + + local function doHttpGet(url) + return function() + return httpImpl:GetAsyncFullUrl(url, DEFAULT_THROTTLING_PRIORITY) + end + end + + -- return the request function + -- url : (string) + -- requestMethod : (string) "GET", "POST" + -- args : (table, optional) + -- options.throttlingPriority : (Enum.ThrottlingPriority, optional) + -- options.contentType : (Enum.HttpContentType, optional) + -- options.postBody : (string, optional ("POST" only)) + -- RETURNS : (promise) + return function(url, requestMethod, options) + assert(type(url) == "string", "Expected url to be a string") + assert(type(requestMethod) == "string", "Expected requestMethod to be a string") + assert(not options or type(options) == "table", "Expected options to be a table") + requestMethod = string.upper(requestMethod) + + local httpFunction + if requestMethod == "POST" then + httpFunction = doHttpPost(url, options) + elseif requestMethod == "GET" then + httpFunction = doHttpGet(url) + else + error(string.format("Unsupported requestMethod : %s", requestMethod or "nil")) + end + + return Promise.new(function(resolve, reject) + if httpFunction then + spawn(function() + local success, response = pcall(httpFunction) + + if success then + local jsonSuccess, decodedJson = pcall(function() + return HttpService:JSONDecode(response) + end) + if jsonSuccess then + resolve({ + responseBody = decodedJson, + }) + else + reject(decodedJson) + end + else + reject(response) + end + end) + else + reject() + end + end) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/httpRequest.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/httpRequest.spec.lua new file mode 100644 index 0000000..fed3d32 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/httpRequest.spec.lua @@ -0,0 +1,61 @@ +return function() + local httpRequest = require(script.Parent.httpRequest) + + local function createTestRequestFunc(testResponse) + local requestService = {} + function requestService:GetAsyncFullUrl() + return testResponse + end + function requestService:PostAsyncFullUrl() + return testResponse + end + + return httpRequest(requestService) + end + + it("should return a function", function() + expect(httpRequest()).to.be.ok() + expect(type(httpRequest())).to.equal("function") + end) + + it("should validate its inputs", function() + local testRequest = createTestRequestFunc() + local function testParams(url, requestMethod, args) + return function() + testRequest(url, requestMethod, args) + end + end + + local validUrl = "friends.roblox.com" + local validMethod = "GET" + local validArgs = {} + + -- url checks + expect(testParams(nil, validMethod, validArgs)).to.throw() + expect(testParams(123, validMethod, validArgs)).to.throw() + expect(testParams({}, validMethod, validArgs)).to.throw() + expect(testParams(true, validMethod, validArgs)).to.throw() + expect(testParams(function() end, validMethod, validArgs)).to.throw() + + -- request method checks + expect(testParams(validUrl, nil, validArgs)).to.throw() + expect(testParams(validUrl, 123, validArgs)).to.throw() + expect(testParams(validUrl, {}, validArgs)).to.throw() + expect(testParams(validUrl, true, validArgs)).to.throw() + expect(testParams(validUrl, function() end, validArgs)).to.throw() + + -- args checks + expect(testParams(validUrl, validMethod, 123)).to.throw() + expect(testParams(validUrl, validMethod, "Test")).to.throw() + expect(testParams(validUrl, validMethod, true)).to.throw() + expect(testParams(validUrl, validMethod, function() end)).to.throw() + end) + + it("should throw an error if the requestMethod isn't supported", function() + local testRequest = createTestRequestFunc("foo") + + expect(function() + testRequest("testUrl", "GIVEANDTAKE") + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.lua new file mode 100644 index 0000000..3a0d69f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.lua @@ -0,0 +1,16 @@ +return function(targetString, blacklistedCharacter) + local charactersArray = {} + local indexArray = {} + for index, byte in utf8.codes(targetString) do + local graphemeCharacter = utf8.char(byte) + table.insert(charactersArray, 1, graphemeCharacter) + table.insert(indexArray, 1, index) + end + for index, graphemeCharacter in ipairs(charactersArray) do + if graphemeCharacter ~= blacklistedCharacter then + return targetString:sub(1, indexArray[index]) + end + end + + return "" +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.spec.lua b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.spec.lua new file mode 100644 index 0000000..a08d217 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AppTempCommon/Temp/trimCharacterFromEndString.spec.lua @@ -0,0 +1,71 @@ +return function() + local trimCharacterFromEndString = require(script.Parent.trimCharacterFromEndString) + + describe("single byte characters", function() + it("should not trim a string if it does not end with passed character", function() + local passedString = "testing" + local passedCharacter = "/" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(passedString) + end) + + it("should trim a string if it ends with a single instance of the passed character", function() + local passedString = "testing/" + local passedCharacter = "/" + local expectedString = "testing" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + + it("should trim a string if it ends with multiple instances of the passed character", function() + local passedString = "testing///" + local passedCharacter = "/" + local expectedString = "testing" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + + it("should do nothing if the passed character is empty", function() + local passedString = "hunter2" + local passedCharacter = "" + local expectedString = "hunter2" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + end) + + describe("multiple byte characters", function() + it("should not trim a string if it does not end with passed character", function() + local passedString = "testing" + local passedCharacter = "🐶" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(passedString) + end) + + it("should trim a string if it ends with a single instance of the passed character", function() + local passedString = "testing🐶" + local passedCharacter = "🐶" + local expectedString = "testing" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + + it("should trim a string if it ends with multiple instances of the passed character", function() + local passedString = "testing🐶🐶🐶" + local passedCharacter = "🐶" + local expectedString = "testing" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + end) + + describe("a string with all blacklisted characters", function() + it("should return a empty string", function() + local passedString = "pppppppppppp" + local passedCharacter = "p" + local expectedString = "" + + expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/ArgCheck.lua b/Client2021/ExtraContent/LuaPackages/ArgCheck.lua new file mode 100644 index 0000000..99367c4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/ArgCheck.lua @@ -0,0 +1,7 @@ +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.ArgCheckImpl) + +return require(CorePackages.ArgCheckImpl) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/ArgCheckImpl/.robloxrc b/Client2021/ExtraContent/LuaPackages/ArgCheckImpl/.robloxrc new file mode 100644 index 0000000..33c7a1c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/ArgCheckImpl/.robloxrc @@ -0,0 +1,9 @@ +{ + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "ImplicitReturn": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/ArgCheckImpl/ArgCheck.lua b/Client2021/ExtraContent/LuaPackages/ArgCheckImpl/ArgCheck.lua new file mode 100644 index 0000000..cd6d931 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/ArgCheckImpl/ArgCheck.lua @@ -0,0 +1,142 @@ +local function IsRunningInStudio() + return game:GetService("RunService"):IsStudio() +end + +local function assert_(condition, message) + if IsRunningInStudio() or _G.__TESTEZ_RUNNING_TEST__ then + assert(condition, message) + end +end + +local ArgCheck = {} + +function ArgCheck.isNonNegativeNumber(value, name) + -- Temporarily disabled outside of studio/tests. See MOBLUAPP-1161. + assert_(typeof(value) == "number" and value >= 0, string.format("expects %s to be a non-negative number!", name)) + + return value +end + +function ArgCheck.isType(value, expectedType, name) + assert_(typeof(value) == expectedType, + string.format("expects %s to be a %s! it was: %s", name, expectedType, typeof(value))) + + return value +end + +function ArgCheck.isInTypes(value, expectedTypes, name) + for _, expectedType in ipairs(expectedTypes) do + if typeof(value) == expectedType then + return value + end + end + + assert_(false, string.format("expects %s to be one of expectedTypes! it was: %s", name, typeof(value))) + + return value +end + +function ArgCheck.isTypeOrNil(value, expectedType, name) + assert_(value == nil or typeof(value) == expectedType, + string.format("expects %s to be a %s! it was: %s", name, expectedType, typeof(value))) + + return value +end + +function ArgCheck.isNotNil(value, name) + assert_(value ~= nil, string.format("expects %s to be not nil!", name)) + + return value +end + +function ArgCheck.isNonEmptyString(value, name) + assert_(typeof(value) == "string" and value ~= "" , + string.format("expects %s to be a non-empty string!", name)) + + return value +end + +function ArgCheck.isEqual(value, expectedValue, name) + assert_(value == expectedValue, string.format("expects %s to equal %s! it was: %s", name, tostring(expectedValue), tostring(value))) + + return value +end + +-- checks for a number or string representing an integer +function ArgCheck.representsInteger(value, name) + local numberValue = tonumber(value) + assert_(numberValue ~= nil , string.format("expects %s to represent a number!", name)) + assert_(numberValue % 1 == 0 , string.format("expects %s to represent an integer!", name)) + + return value +end + +--[[ + Checks if the value matches the given interface + iface is the interface description; it can be (in order of priority): + * a custom type name: checks against a type from dependencies (see below) + * an ArgCheck handler: + * "integer" => ArgCheck.representsInteger + * "nonEmptyString" => ArgCheck.isNonEmptyString + * a lua type (string): equivalent to ArgCheck.isType + * a list style table (only first item is considered): + checks for a list table with items matching the given interface + * a dict style table: checks for a table with keys matching the given interfaces + dependencies is a table of named interfaces that can be referenced in iface + Example: + local myTypes = { + Tree = { + value = "string", + leaves = {"string"}, + branches = {"Tree"}, + }, + } + ArgCheck.matchesInterface(someValue, "Tree", "myVal", myTypes) +]]-- +function ArgCheck.matchesInterface(value, iface, name, dependencies) + if IsRunningInStudio() or _G.__TESTEZ_RUNNING_TEST__ then + local checkFnList = { + integer = ArgCheck.representsInteger, + nonEmptyString = ArgCheck.isNonEmptyString, + } + if type(iface) == "string" then + if dependencies and dependencies[iface] then + ArgCheck.matchesInterface(value, dependencies[iface], name, dependencies) + else + local checkFn = checkFnList[iface] + if type(checkFn) == "function" then + checkFn(value, name) + else + ArgCheck.isType(value, iface, name) + end + end + else + -- assume iface describes a table (list or dict) + ArgCheck.isType(value, "table", name) + if iface[1] ~= nil then + for index, item in ipairs(value) do + ArgCheck.matchesInterface(item, iface[1], name .. "[" .. index .. "]", dependencies) + end + else + for key, desc in pairs(iface) do + if string.sub(key, 1, 1) ~= "_" then + local itemName = name .. "." .. key + local itemValue = value[key] + local isRequired = iface._required and iface._required[key] + if isRequired or itemValue ~= nil then + ArgCheck.matchesInterface(itemValue, desc, itemName, dependencies) + end + end + end + end + end + end + + return value +end + +function ArgCheck.assert(...) + assert_(...) +end + +return ArgCheck diff --git a/Client2021/ExtraContent/LuaPackages/ArgCheckImpl/ArgCheck.spec.lua b/Client2021/ExtraContent/LuaPackages/ArgCheckImpl/ArgCheck.spec.lua new file mode 100644 index 0000000..ead56a6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/ArgCheckImpl/ArgCheck.spec.lua @@ -0,0 +1,287 @@ +return function() + local ArgCheck = require(script.Parent.ArgCheck) + + describe("isNonNegativeNumber", function() + it("should assert if given non-number, or negative number", function() + expect(function() + ArgCheck.isNonNegativeNumber(nil, "") + end).to.throw() + expect(function() + ArgCheck.isNonNegativeNumber({}, "") + end).to.throw() + expect(function() + ArgCheck.isNonNegativeNumber("string", "") + end).to.throw() + expect(function() + ArgCheck.isNonNegativeNumber(-1, "") + end).to.throw() + end) + + it("should return the value if it is a non-negative number", function() + expect(ArgCheck.isNonNegativeNumber(0, "")).to.equal(0) + expect(ArgCheck.isNonNegativeNumber(1, "")).to.equal(1) + end) + end) + + describe("isType", function() + it("should assert if type is wrong", function() + expect(function() + ArgCheck.isType(nil, "number", "") + end).to.throw() + expect(function() + ArgCheck.isType("test", "number", "") + end).to.throw() + expect(function() + ArgCheck.isType(5, "string", "") + end).to.throw() + expect(function() + ArgCheck.isType(5, "table", "") + end).to.throw() + end) + + it("should return the value if the type is correct", function() + expect(ArgCheck.isType(0, "number", "")).to.equal(0) + expect(ArgCheck.isType("test", "string", "")).to.equal("test") + end) + end) + + describe("isInTypes", function() + it("should assert if type is not expected", function() + expect(function() + ArgCheck.isInTypes(nil, {"number", "string", "table"}, "") + end).to.throw() + expect(function() + ArgCheck.isInTypes("test", {"number", "table"}, "") + end).to.throw() + expect(function() + ArgCheck.isInTypes(5, {"string", "table"}, "") + end).to.throw() + expect(function() + ArgCheck.isInTypes({}, {"number", "string"}, "") + end).to.throw() + end) + + it("should return the value if the type is expected", function() + expect(ArgCheck.isInTypes(0, {"number", "string"}, "")).to.equal(0) + expect(ArgCheck.isInTypes("test", {"table", "string"}, "")).to.equal("test") + local testTable = {} + expect(ArgCheck.isInTypes(testTable, {"table", "string"}, "")).to.equal(testTable) + local testFunction = function() end + expect(ArgCheck.isInTypes(testFunction, {"function", "string"}, "")).to.equal(testFunction) + end) + end) + + describe("isTypeOrNil", function() + it("should assert if type is wrong", function() + expect(function() + ArgCheck.isTypeOrNil("test", "number", "") + end).to.throw() + expect(function() + ArgCheck.isTypeOrNil(5, "string", "") + end).to.throw() + expect(function() + ArgCheck.isTypeOrNil(5, "table", "") + end).to.throw() + end) + + it("should return the value if the type is correct", function() + expect(ArgCheck.isTypeOrNil(nil, "number", "")).to.equal(nil) + expect(ArgCheck.isTypeOrNil(0, "number", "")).to.equal(0) + expect(ArgCheck.isTypeOrNil("test", "string", "")).to.equal("test") + end) + end) + + describe("isNotNil", function() + it("should assert if type is nil", function() + expect(function() + ArgCheck.isNotNil(nil, "") + end).to.throw() + end) + + it("should return the value if it's not nil", function() + expect(ArgCheck.isNotNil(0, "")).to.equal(0) + expect(ArgCheck.isNotNil("test", "")).to.equal("test") + local testTable = {} + expect(ArgCheck.isNotNil(testTable, "")).to.equal(testTable) + local testFunction = function() end + expect(ArgCheck.isNotNil(testFunction, "")).to.equal(testFunction) + end) + end) + + describe("isEqual", function() + it("should assert if not equal", function() + expect(function() + ArgCheck.isEqual(0, nil, "") + end).to.throw() + expect(function() + ArgCheck.isEqual(2, 1, "") + end).to.throw() + expect(function() + ArgCheck.isEqual("", "test", "") + end).to.throw() + expect(function() + ArgCheck.isEqual({}, {}, "") + end).to.throw() + expect(function() + ArgCheck.isEqual(function() end, function() end, "") + end).to.throw() + end) + + it("should return the value if value is equal to expected value", function() + expect(ArgCheck.isEqual(nil, nil, "")).to.equal(nil) + expect(ArgCheck.isEqual(0, 0, "")).to.equal(0) + expect(ArgCheck.isEqual(true, true, "")).to.equal(true) + expect(ArgCheck.isEqual("test", "test", "")).to.equal("test") + local testTable = {} + expect(ArgCheck.isEqual(testTable, testTable, "")).to.equal(testTable) + local testFunction = function() end + expect(ArgCheck.isEqual(testFunction, testFunction, "")).to.equal(testFunction) + end) + end) + + describe("representsInteger", function() + it("should fail if not a number", function() + expect(function() + ArgCheck.representsInteger(nil, "") + end).to.throw() + expect(function() + ArgCheck.representsInteger({}, "") + end).to.throw() + expect(function() + ArgCheck.representsInteger(function()end, "") + end).to.throw() + expect(function() + ArgCheck.representsInteger(true, "") + end).to.throw() + expect(function() + ArgCheck.representsInteger("NaN", "") + end).to.throw() + expect(function() + ArgCheck.representsInteger("1test", "") + end).to.throw() + end) + + it("should fail if not an integer", function() + expect(function() + ArgCheck.representsInteger(1.5, "") + end).to.throw() + expect(function() + ArgCheck.representsInteger("1.5", "") + end).to.throw() + expect(function() + ArgCheck.representsInteger("1e-1", "") + end).to.throw() + end) + + it("should return the same value on success", function() + expect(ArgCheck.representsInteger(5, "")).to.equal(5) + expect(ArgCheck.representsInteger("-5", "")).to.equal("-5") + expect(ArgCheck.representsInteger("1e1", "")).to.equal("1e1") + expect(ArgCheck.representsInteger("0xa", "")).to.equal("0xa") + end) + end) + + describe("matchesInterface", function() + it("should match a simple interface", function() + local interface = { + num = "number", + str = "string", + bool = "boolean", + func = "function", + tab = "table", + list = {"number"}, + -- only num is required, rest is optional + _required = { + num = true, + } + } + local obj1 = { + num = 5, + str = "5", + bool = true, + func = function()end, + tab = {}, + list = {1, 2, 3} + } + local obj2 = { + num = "5", + } + local obj3 = { + num = 5, + list = {"NaN"}, + } + local obj4 = { + str = "5", + } + expect(function() + ArgCheck.matchesInterface(obj1, interface, "") + end).to.never.throw() + expect(function() + ArgCheck.matchesInterface(obj2, interface, "") + end).to.throw() + expect(function() + ArgCheck.matchesInterface(obj3, interface, "") + end).to.throw() + expect(function() + ArgCheck.matchesInterface(obj4, interface, "") + end).to.throw() + end) + + it("should match ArgCheck functions", function() + expect(function() + ArgCheck.matchesInterface("5", "nonEmptyString", "") + end).to.never.throw() + expect(function() + ArgCheck.matchesInterface(5, "nonEmptyString", "") + end).to.throw() + expect(function() + ArgCheck.matchesInterface("", "nonEmptyString", "") + end).to.throw() + expect(function() + ArgCheck.matchesInterface({str = "5"}, {str = "nonEmptyString"}, "") + end).to.never.throw() + end) + + it("should match dependent types", function() + local types = { + child = { + name = "string", + }, + parent = { + name = "string", + children = {"child"}, + } + } + local child1 = { + name = "child1", + } + local child2 = { + name = "child2", + } + local parent1 = { + name = "parent1", + children = {child1, child2}, + } + local parent2 = { + name = "parent2", + children = {"child1", "child2"}, + } + expect(function() + ArgCheck.matchesInterface(child1, types.child, "", types) + end).to.never.throw() + expect(function() + ArgCheck.matchesInterface(parent1, types.parent, "", types) + end).to.never.throw() + expect(function() + ArgCheck.matchesInterface(parent2, types.parent, "", types) + end).to.throw() + end) + + it("should return the same value on success", function() + expect(ArgCheck.matchesInterface(5, "number", "")).to.equal(5) + expect(ArgCheck.matchesInterface("5", "nonEmptyString", "")).to.equal("5") + local list = {1, 2, 3} + expect(ArgCheck.matchesInterface(list, {"number"}, "")).to.equal(list) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/ArgCheckImpl/init.lua b/Client2021/ExtraContent/LuaPackages/ArgCheckImpl/init.lua new file mode 100644 index 0000000..f949535 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/ArgCheckImpl/init.lua @@ -0,0 +1 @@ +return require(script.ArgCheck) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/AvatarExperienceDeps.lua b/Client2021/ExtraContent/LuaPackages/AvatarExperienceDeps.lua new file mode 100644 index 0000000..4a04356 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/AvatarExperienceDeps.lua @@ -0,0 +1,12 @@ +--[[ + Proxy package for dependencies for AvatarExperience. +]] + + +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.AvatarExperienceDeps) diff --git a/Client2021/ExtraContent/LuaPackages/CodeCoverage/.robloxrc b/Client2021/ExtraContent/LuaPackages/CodeCoverage/.robloxrc new file mode 100644 index 0000000..321fd28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/CodeCoverage/.robloxrc @@ -0,0 +1,12 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "ImplicitReturn": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/CodeCoverage/FileScanner.lua b/Client2021/ExtraContent/LuaPackages/CodeCoverage/FileScanner.lua new file mode 100644 index 0000000..0f473fd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/CodeCoverage/FileScanner.lua @@ -0,0 +1,17 @@ + +local CodeCoverage = script.Parent +local LineScanner = require(CodeCoverage.LineScanner) + +return function(fileLinesArray) + local scanner = LineScanner:new() + + local excludedLines = {} + local excludedIfNotHitLines = {} + for _, line in ipairs(fileLinesArray) do + local excluded, excludedIfNotHit = scanner:consume(line) + table.insert(excludedLines, excluded) + table.insert(excludedIfNotHitLines, excludedIfNotHit) + end + + return excludedLines, excludedIfNotHitLines +end diff --git a/Client2021/ExtraContent/LuaPackages/CodeCoverage/LcovReporter.lua b/Client2021/ExtraContent/LuaPackages/CodeCoverage/LcovReporter.lua new file mode 100644 index 0000000..d0a1380 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/CodeCoverage/LcovReporter.lua @@ -0,0 +1,32 @@ +local LcovReporter = {} +LcovReporter.__index = LcovReporter + + +function LcovReporter.generate(files, includeFilter) + local report = {} + + for _, file in ipairs(files) do + if includeFilter(file) then + table.insert(report, "TN:") + table.insert(report, "SF:" .. file.path) + local foundFirstHit = false + for lineNumber, line in ipairs(file.lines) do + if not line.ignored and not foundFirstHit then + foundFirstHit = true + end + + if foundFirstHit and not line.ignored then + table.insert(report, ("DA:%d,%d"):format(lineNumber, line.hits)) + end + end + table.insert(report, ("LH:%d"):format(file.hits)) + table.insert(report, ("LF:%d"):format(file.hits + file.misses)) + table.insert(report, "end_of_record") + end + end + + return table.concat(report, "\n") +end + + +return LcovReporter diff --git a/Client2021/ExtraContent/LuaPackages/CodeCoverage/LineScanner.lua b/Client2021/ExtraContent/LuaPackages/CodeCoverage/LineScanner.lua new file mode 100644 index 0000000..5255e61 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/CodeCoverage/LineScanner.lua @@ -0,0 +1,387 @@ +--[[ +The MIT License (MIT) + +Copyright (c) 2007 - 2018 Hisham Muhammad. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Original: https://github.com/keplerproject/luacov/6e6232d766f051d9668dab785582451bfd69ad17/master/src/luacov/linescanner.lua +]] + +local LineScanner = {} +LineScanner.__index = LineScanner + +function LineScanner:new() + return setmetatable( + { + first = true, + comment = false, + after_function = false, + enabled = true + }, + self + ) +end + +-- Raw version of string.gsub +local function replace(s, old, new) + old = old:gsub("%p", "%%%0") + new = new:gsub("%%", "%%%%") + return (s:gsub(old, new)) +end + +local fixups = { + {"=", " ?= ?"}, -- '=' may be surrounded by spaces + {"(", " ?%( ?"}, -- '(' may be surrounded by spaces + {")", " ?%) ?"}, -- ')' may be surrounded by spaces + {"", "x ?[%[%.]? ?[ntfx0']* ?%]?"}, -- identifier, possibly indexed once + {"", "x ?, ?x[x, ]*"}, -- at least two comma-separated identifiers + {"", "%[? ?[ntfx0']+ ?%]?"}, -- field, possibly like ["this"] + {"", "[ %(]*"} -- optional opening parentheses +} + +-- Utility function to make patterns more readable +local function fixup(pat) + for _, fixup_pair in ipairs(fixups) do + pat = replace(pat, fixup_pair[1], fixup_pair[2]) + end + + return pat +end + +--- Lines that are always excluded from accounting +local any_hits_exclusions = { + "", -- Empty line + "end[,; %)]*", -- Single "end" + "else", -- Single "else" + "repeat", -- Single "repeat" + "do", -- Single "do" + "if", -- Single "if" + "then", -- Single "then" + "while t do", -- "while true do" generates no code + "if t then", -- "if true then" generates no code + "local x", -- "local var" + fixup "local x=", -- "local var =" + fixup "local ", -- "local var1, ..., varN" + fixup "local =", -- "local var1, ..., varN =" + "local function x" -- "local function f (arg1, ..., argN)" +} + +--- Lines that are only excluded from accounting when they have 0 hits +local zero_hits_exclusions = { + "[ntfx0',= ]+,", -- "var1 var2," multi columns table stuff + "{ ?} ?,", -- Empty table before comma leaves no trace in tables and calls + fixup "=.+[,;]", -- "[123] = 23," "['foo'] = "asd"," + fixup "=function", -- "[123] = function(...)" + fixup "='", -- "[123] = [[", possibly with opening parens + "return function", -- "return function(arg1, ..., argN)" + "function", -- "function(arg1, ..., argN)" + "[ntfx0]", -- Single token expressions leave no trace in tables, function calls and sometimes assignments + "''", -- Same for strings + "{ ?}", -- Same for empty tables + fixup "", -- Same for local variables indexed once + fixup "local x=function", -- "local a = function(arg1, ..., argN)" + fixup "local x='", -- "local a = [[", possibly with opening parens + fixup "local x=(", -- "local a = (", possibly with several parens + fixup "local =(", -- "local a, b = (", possibly with several parens + fixup "local x=n", -- "local a = nil; local b = nil" produces no trace for the second statement + fixup "='", -- "a.b = [[", possibly with opening parens + fixup "=function", -- "a = function(arg1, ..., argN)" + "} ?,", -- "}," generates no trace if the table ends with a key-value pair + "} ?, ?function", -- same with "}, function(...)" + "break", -- "break" generates no trace in Lua 5.2+ + "{", -- "{" opening table + "}?[ %)]*", -- optional closing paren, possibly with several closing parens + "[ntf0']+ ?}[ %)]*" -- a constant at the end of a table, possibly with closing parens (for LuaJIT) +} + +local function excluded(exclusions, line) + for _, e in ipairs(exclusions) do + if line:match("^ *" .. e .. " *$") then + return true + end + end + + return false +end + +function LineScanner:find(pattern) + return self.line:find(pattern, self.i) +end + +-- Skips string literal with quote stored as self.quote. +-- @return boolean indicating success. +function LineScanner:skip_string() + -- Look for closing quote, possibly after even number of backslashes. + local _, quote_i = self:find("^(\\*)%1" .. self.quote) + if not quote_i then + _, quote_i = self:find("[^\\](\\*)%1" .. self.quote) + end + + if quote_i then + self.i = quote_i + 1 + self.quote = nil + table.insert(self.simple_line_buffer, "'") + return true + else + return false + end +end + +-- Skips long string literal with equal signs stored as self.equals. +-- @return boolean indicating success. +function LineScanner:skip_long_string() + local _, bracket_i = self:find("%]" .. self.equals .. "%]") + + if bracket_i then + self.i = bracket_i + 1 + self.equals = nil + + if self.comment then + self.comment = false + else + table.insert(self.simple_line_buffer, "'") + end + + return true + else + return false + end +end + +-- Skips function arguments. +-- @return boolean indicating success. +function LineScanner:skip_args() + local _, paren_i = self:find("%)") + + if paren_i then + self.i = paren_i + 1 + self.args = nil + return true + else + return false + end +end + +function LineScanner:skip_whitespace() + local next_i = self:find("%S") or #self.line + 1 + + if next_i ~= self.i then + self.i = next_i + table.insert(self.simple_line_buffer, " ") + end +end + +function LineScanner:skip_number() + if self:find("^0[xX]") then + self.i = self.i + 2 + end + + local _ + _, _, self.i = self:find("^[%x%.]*()") + + if self:find("^[eEpP][%+%-]") then + -- Skip exponent, too. + self.i = self.i + 2 + _, _, self.i = self:find("^[%x%.]*()") + end + + -- Skip LuaJIT number suffixes (i, ll, ull). + _, _, self.i = self:find("^[iull]*()") + table.insert(self.simple_line_buffer, "0") +end + +local keywords = {["nil"] = "n", ["true"] = "t", ["false"] = "f"} + +for _, keyword in ipairs( + { + "and", + "break", + "do", + "else", + "elseif", + "end", + "for", + "function", + "goto", + "if", + "in", + "local", + "not", + "or", + "repeat", + "return", + "then", + "until", + "while" + } +) do + keywords[keyword] = keyword +end + +function LineScanner:skip_name() + -- It is guaranteed that the first character matches "%a_". + local _, _, name = self:find("^([%w_]*)") + self.i = self.i + #name + + if keywords[name] then + name = keywords[name] + else + name = "x" + end + + table.insert(self.simple_line_buffer, name) + + if name == "function" then + -- This flag indicates that the next pair of parentheses (function args) must be skipped. + self.after_function = true + end +end + +-- Source lines can be explicitly ignored using `enable` and `disable` inline options. +-- An inline option is a simple comment: `-- luacov: enable` or `-- luacov: disable`. +-- Inline option parsing is not whitespace sensitive. +-- All lines starting from a line containing `disable` option and up to a line containing `enable` +-- option (or end of file) are excluded. + +function LineScanner:check_inline_options(comment_body) + if comment_body:find("^%s*luacov:%s*enable%s*$") then + self.enabled = true + elseif comment_body:find("^%s*luacov:%s*disable%s*$") then + self.enabled = false + end +end + +-- Consumes and analyzes a line. +-- @return boolean indicating whether line must be excluded. +-- @return boolean indicating whether line must be excluded if not hit. +function LineScanner:consume(line) + if self.first then + self.first = false + + if line:match("^#!") then + -- Ignore Unix hash-bang magic line. + return true, true + end + end + + self.line = line + -- As scanner goes through the line, it puts its simplified parts into buffer. + -- Punctuation is preserved. Whitespace is replaced with single space. + -- Literal strings are replaced with "''", so that a string literal + -- containing special characters does not confuse exclusion rules. + -- Numbers are replaced with "0". + -- Identifiers are replaced with "x". + -- Literal keywords (nil, true and false) are replaced with "n", "t" and "f", + -- other keywords are preserved. + -- Function declaration arguments are removed. + self.simple_line_buffer = {} + self.i = 1 + + while self.i <= #line do + -- One iteration of this loop handles one token, where + -- string literal start and end are considered distinct tokens. + if self.quote then + if not self:skip_string() then + -- String literal ends on another line. + break + end + elseif self.equals then + if not self:skip_long_string() then + -- Long string literal or comment ends on another line. + break + end + elseif self.args then + if not self:skip_args() then + -- Function arguments end on another line. + break + end + else + self:skip_whitespace() + + if self:find("^%.%d") then + self.i = self.i + 1 + end + + if self:find("^%d") then + self:skip_number() + elseif self:find("^[%a_]") then + self:skip_name() + else + if self:find("^%-%-") then + self.comment = true + self.i = self.i + 2 + end + + local _, bracket_i, equals = self:find("^%[(=*)%[") + if equals then + self.i = bracket_i + 1 + self.equals = equals + + if not self.comment then + table.insert(self.simple_line_buffer, "'") + end + elseif self.comment then + -- Simple comment, check if it contains inline options and skip line. + self.comment = false + local comment_body = self.line:sub(self.i) + self:check_inline_options(comment_body) + break + else + local char = line:sub(self.i, self.i) + + if char == "." then + -- Dot can't be saved as one character because of + -- ".." and "..." tokens and the fact that number literals + -- can start with one. + local _, _, dots = self:find("^(%.*)") + self.i = self.i + #dots + table.insert(self.simple_line_buffer, dots) + else + self.i = self.i + 1 + + if char == "'" or char == '"' then + table.insert(self.simple_line_buffer, "'") + self.quote = char + elseif self.after_function and char == "(" then + -- This is the opening parenthesis of function declaration args. + self.after_function = false + self.args = true + else + -- Save other punctuation literally. + -- This inserts an empty string when at the end of line, + -- which is fine. + table.insert(self.simple_line_buffer, char) + end + end + end + end + end + end + + if not self.enabled then + -- Disabled by inline options, always exclude the line. + return true, true + end + + local simple_line = table.concat(self.simple_line_buffer) + return excluded(any_hits_exclusions, simple_line), excluded(zero_hits_exclusions, simple_line) +end + +return LineScanner diff --git a/Client2021/ExtraContent/LuaPackages/CodeCoverage/Reporter.lua b/Client2021/ExtraContent/LuaPackages/CodeCoverage/Reporter.lua new file mode 100644 index 0000000..ecd1645 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/CodeCoverage/Reporter.lua @@ -0,0 +1,124 @@ +local CodeCoverage = script.Parent +local LineScanner = require(CodeCoverage.LineScanner) +local LcovReporter = require(CodeCoverage.LcovReporter) + +local ScriptContext = game:GetService("ScriptContext") +local CoreScriptSyncService = game:GetService("CoreScriptSyncService") + +local Reporter = {} +Reporter.__index = Reporter + +function Reporter.processCoverageStats() + local stats = ScriptContext:GetCoverageStats() + + local files = {} + for _, scriptStats in ipairs(stats) do + local aScript = scriptStats.Script; + + local source = aScript.Source + + local hits = scriptStats.Hits + local lineHit = 0 + local lineMissed = 0 + local lines = {} + + if scriptStats.HitsPrecise then + local sources = source:split('\n') + + for n,h in ipairs(hits) do + local ignored = h < 0 + + if h > 0 then + lineHit = lineHit + 1 + elseif h == 0 then + lineMissed = lineMissed + 1 + end + + lines[n] = { + source = sources[n], + ignored = ignored, + hits = math.max(h, 0) + } + end + else + local scanner = LineScanner:new() + local lineNumber = 1 + + for line in source:gmatch('([^\r\n]*)[\r\n]?') do + local excluded, excludedIfNotHit = scanner:consume(line) + + local ignored = excluded + + if not excluded then + if hits[lineNumber] and hits[lineNumber] > 0 then + lineHit = lineHit + 1 + else + if excludedIfNotHit then + ignored = true + else + lineMissed = lineMissed + 1 + end + end + end + + lines[lineNumber] = { + source = line, + ignored = ignored, + hits = hits[lineNumber] or 0 + } + + lineNumber = lineNumber + 1 + end + end + + table.insert(files, { + script = aScript, + path = CoreScriptSyncService:GetScriptFilePath(aScript), + lines = lines, + hits = lineHit, + misses = lineMissed, + }) + end + + return files +end + +local function matchesAny(str, excludes) + if not str or str:len() == 0 or not excludes then + return false + end + + for _,exclude in ipairs(excludes) do + if string.find(str, exclude) ~= nil then + return true + end + end + return false +end + +function Reporter.generateReport(path, excludes) + local report = LcovReporter.generate(Reporter.processCoverageStats(), function(file) + local isExcluded = file.script.Name:match(".spec$") + or file.script:FindFirstAncestor("TestEZ") + or file.script:IsDescendantOf(CodeCoverage) + or matchesAny(file.path, excludes) + local isIncluded = file.path and file.path:len() > 0 + + return isIncluded and not isExcluded + end) + if report:len() == 0 then + warn("Generating code coverage report failed. Produced report has zero size.") + return + end + + local success, message = pcall(function() -- New API + local fs = game:GetService("FileSystemService") + fs:WriteFile(path, report) + end) + + if not success then + warn("Failed to save code coverage report at path: " .. path .. "\nError: " .. message) + end +end + +return Reporter diff --git a/Client2021/ExtraContent/LuaPackages/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Cryo.lua new file mode 100644 index 0000000..43e131b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Cryo.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Cryo) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/DeveloperTools.lua b/Client2021/ExtraContent/LuaPackages/DeveloperTools.lua new file mode 100644 index 0000000..b7e5207 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/DeveloperTools.lua @@ -0,0 +1,6 @@ +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Dev.DeveloperTools) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/ErrorReporters/.robloxrc b/Client2021/ExtraContent/LuaPackages/ErrorReporters/.robloxrc new file mode 100644 index 0000000..d828202 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/ErrorReporters/.robloxrc @@ -0,0 +1,11 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "ImplicitReturn": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReport.lua b/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReport.lua new file mode 100644 index 0000000..3f25fa9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReport.lua @@ -0,0 +1,232 @@ +--[[ + This module creates a crash object that can be sent to Backtrace. + For information about what are the acceptable fields, see document: + https://api.backtrace.io/#tag/submit-crash +]] + +local CorePackages = game:GetService("CorePackages") +local HttpService = game:GetService("HttpService") + +local Cryo = require(CorePackages.Packages.Cryo) +local t = require(CorePackages.Packages.t) + +local ProcessErrorStack = require(script.Parent.ProcessErrorStack) + +local DEFAULT_THREAD_NAME = "default" + +local IBacktraceStack = t.strictInterface({ + guessed_frame = t.optional(t.boolean), + funcName = t.optional(t.string), + address = t.optional(t.string), + line = t.optional(t.string), + column = t.optional(t.string), + sourceCode = t.optional(t.string), + library = t.optional(t.string), + debug_identifier = t.optional(t.string), + faulted = t.optional(t.boolean), + registers = t.optional(t.map(t.string, t.some(t.string, t.number))), +}) + +local IBacktraceThread = t.strictInterface({ + name = t.optional(t.string), + fault = t.optional(t.boolean), + stack = t.optional(t.array(IBacktraceStack)), +}) + +local IArch = t.strictInterface({ + name = t.string, + registers = t.map(t.string, t.string), +}) + +local ISourceCode = t.strictInterface({ + text = t.optional(t.string), + startLine = t.optional(t.number), + startColumn = t.optional(t.number), + startPos = t.optional(t.number), + path = t.optional(t.string), + tabWidth = t.optional(t.number), +}) + +local IPerm = t.strictInterface({ + read = t.boolean, + write = t.boolean, + exec = t.boolean, +}) + +local IMemory = t.strictInterface({ + start = t.string, + size = t.optional(t.number), + data = t.optional(t.string), + perms = t.optional(IPerm), +}) + +local IModule = t.strictInterface({ + start = t.string, + size = t.number, + code_file = t.optional(t.string), + version = t.optional(t.string), + debug_file = t.optional(t.string), + debug_identifier = t.optional(t.string), + debug_file_exists = t.optional(t.boolean), +}) + +local IAttributes = t.optional(t.map(t.string, t.some(t.string, t.number, t.boolean))) + +local IAnnotation = function(annotation) + local function checkTypeRecursive(value) + if type(value) == "table" then + for key, subValue in pairs(value) do + local valid, error = checkTypeRecursive(subValue) + if not valid then + return false, string.format("error when checking key: %s - %s", key, error) + end + end + return true + else + local type = t.some(t.string, t.number, t.boolean) + return type(value) + end + end + + return checkTypeRecursive(annotation) +end + +local IAnnotations = t.optional(t.map(t.string, IAnnotation)) + +local IBacktraceReport = t.intersection( + t.strictInterface({ + -- Must haves + uuid = t.string, + timestamp = t.number, + lang = t.string, + langVersion = t.string, + agent = t.string, + agentVersion = t.string, + threads = t.map(t.string, IBacktraceThread), + mainThread = t.string, + + -- Optionals + attributes = IAttributes, + annotations = IAnnotations, + symbolication = t.optional(t.literal("minidump")), + entryThread = t.optional(t.string), + arch = t.optional(IArch), + fingerprint = t.optional(t.string), + classifiers = t.optional(t.array(t.string)), + sourceCode = t.optional(t.map(t.string, ISourceCode)), + memory = t.optional(t.array(IMemory)), + modules = t.optional(t.array(IModule)), + }), + function(report) + local hasRegisters = false + + local threads = report.threads + for _, thread in pairs(threads) do + local stacks = thread.stack + if stacks ~= nil then + for _, stack in ipairs(stacks) do + if stack.registers ~= nil then + hasRegisters = true + break + end + end + end + if hasRegisters then + break + end + end + + if hasRegisters and report.arch == nil then + return false, "arch must exist if you want to have registers in the stack" + else + return true + end + end +) + +local BacktraceReport = { + IAttributes = IAttributes, + IAnnotations = IAnnotations, +} +BacktraceReport.__index = BacktraceReport + +function BacktraceReport:validate() + return IBacktraceReport(self) +end + +-- Return a basic report that has all the required fields +function BacktraceReport.new() + local self = { + uuid = HttpService:GenerateGUID(false):lower(), + timestamp = os.time(), + lang = "lua", + langVersion = "Roblox", + agent = "backtrace-Lua", + agentVersion = "0.1.0", + threads = {}, + mainThread = DEFAULT_THREAD_NAME, + } + + setmetatable(self, BacktraceReport) + + return self +end + +function BacktraceReport:addAttributes(newAttributes) + if type(newAttributes) ~= "table" then + warn("Cannot add attributes of type: ", type(newAttributes)) + return + end + + local attributes = self.attributes or {} + + attributes = Cryo.Dictionary.join(attributes, newAttributes) + + self.attributes = attributes +end + +function BacktraceReport:addAnnotations(newAnnotations) + if type(newAnnotations) ~= "table" then + warn("Cannot add annotations of type: ", type(newAnnotations)) + return + end + + local annotations = self.annotations or {} + + annotations = Cryo.Dictionary.join(annotations, newAnnotations) + + self.annotations = annotations +end + +function BacktraceReport:addStackToThread(stack, threadName) + local threads = self.threads + + threads = Cryo.Dictionary.join(threads, { + [threadName] = { + name = threadName, + stack = stack, + } + }) + + self.threads = threads +end + +function BacktraceReport:addStackToMainThread(stack) + self:addStackToThread(stack, self.mainThread) +end + +function BacktraceReport.fromMessageAndStack(errorMessage, errorStack) + local report = BacktraceReport.new() + + report:addAttributes({ + ["error.message"] = errorMessage, + }) + + local stack, sourceCode = ProcessErrorStack(errorStack) + report:addStackToMainThread(stack) + report.sourceCode = sourceCode + + return report +end + +return BacktraceReport diff --git a/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReport.spec.lua b/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReport.spec.lua new file mode 100644 index 0000000..3b055c2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReport.spec.lua @@ -0,0 +1,197 @@ +return function() + local BacktraceReport = require(script.Parent.BacktraceReport) + local CorePackages = game:GetService("CorePackages") + + local tutils = require(CorePackages.Packages.tutils) + + describe(".new", function() + it("should return a valid report", function() + local report = BacktraceReport.new() + local isValid = report:validate() + + expect(isValid).to.equal(true) + end) + end) + + describe(":validate", function() + it("should return false if the report has registers but no arch information, true otherwise", function() + local report = BacktraceReport.new() + + report.threads = { + default = { + stack = { + [1] = { + registers = { + rax = "16045690984833335023", + }, + } + }, + }, + } + + expect(report:validate()).to.equal(false) + + -- invalid arch + report.arch = { + name = "x64", + regsiters = nil, + } + + expect(report:validate()).to.equal(false) + + -- correct arch + report.arch = { + name = "x64", + registers = { + rax = "u64", + }, + } + + expect(report:validate()).to.equal(true) + end) + end) + + describe("IAnnotations", function() + local IAnnotations = BacktraceReport.IAnnotations + + it("should return false if not a table", function() + local result = IAnnotations("string") + expect(result).to.equal(false) + end) + + it("should return false if a non-table value is not string, number, or boolean", function() + local result = IAnnotations({ + Value = function() end, + }) + expect(result).to.equal(false) + + result = IAnnotations({ + Value = "ha", + Value2 = 1, + Value3 = false, + Recursive = { + Value11 = "haha", + MoreRecursive = { + Value12 = "hahaha", + Value22 = function() end, + }, + }, + }) + expect(result).to.equal(false) + end) + + it("should return true for an empty table", function() + expect(IAnnotations({})).to.equal(true) + end) + + it("should return true if a non-table value is either string, number, or boolean", function() + local result = IAnnotations({ + Value = "ha", + Value2 = 1, + Value3 = false, + Recursive = { + Value11 = "haha", + Value21 = 2, + Array = { + [1] = "hahaha", + [2] = 3, + [3] = true, + }, + }, + }) + expect(result).to.equal(true) + end) + end) + + describe(":addAttributes", function() + it("should correctly add attributes", function() + local report = BacktraceReport.new() + report:addAttributes({ + att1 = 1, + }) + expect(tutils.fieldCount(report.attributes)).to.equal(1) + expect(report.attributes.att1).to.equal(1) + report:addAttributes({ + att1 = 2, + att2 = false, + att3 = "test", + }) + expect(tutils.fieldCount(report.attributes)).to.equal(3) + expect(report.attributes.att1).to.equal(2) + expect(report.attributes.att2).to.equal(false) + expect(report.attributes.att3).to.equal("test") + end) + end) + + describe(":addAnnotations", function() + it("should correctly add annotations", function() + local environmentVariables = { + ENV_VAR_EXAMPLE = "example", + } + local dependencies = { + Roact = { + version = "0.2.0", + }, + Otter = { + version = "0.1.0", + }, + } + + local report = BacktraceReport.new() + + report:addAnnotations({ + EnvironmentVariables = environmentVariables, + }) + expect(tutils.fieldCount(report.annotations)).to.equal(1) + expect(tutils.deepEqual(report.annotations.EnvironmentVariables, environmentVariables)).to.equal(true) + + report:addAnnotations({ + SomeProperty = true, + Dependencies = dependencies, + }) + expect(tutils.fieldCount(report.annotations)).to.equal(3) + expect(tutils.deepEqual(report.annotations.EnvironmentVariables, environmentVariables)).to.equal(true) + expect(report.annotations.SomeProperty).to.equal(true) + expect(tutils.deepEqual(report.annotations.Dependencies, dependencies)).to.equal(true) + end) + end) + + describe(":addStackToThread", function() + it("should correctly add stack to the thread with the name provided", function() + local stack1 = { + [1] = { + line = "100", + funcName = "field testError", + sourceCode = "1", + } + } + local stack2 = { + [1] = { + line = "110", + funcName = "field ?", + sourceCode = "2", + } + } + + local report = BacktraceReport.new() + + report:addStackToThread(stack1, "main") + expect(tutils.fieldCount(report.threads)).to.equal(1) + expect(tutils.deepEqual(report.threads.main.stack, stack1)).to.equal(true) + + report:addStackToThread(stack2, "1") + expect(tutils.fieldCount(report.threads)).to.equal(2) + expect(tutils.deepEqual(report.threads.main.stack, stack1)).to.equal(true) + expect(tutils.deepEqual(report.threads["1"].stack, stack2)).to.equal(true) + end) + end) + + describe(".fromMessageAndStack", function() + it("should return a valid report", function() + local report = BacktraceReport.fromMessageAndStack("index nil", "Script 'Workspace.Script', Line 3") + local isValid = report:validate() + + expect(isValid).to.equal(true) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReporter.lua b/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReporter.lua new file mode 100644 index 0000000..7920f75 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReporter.lua @@ -0,0 +1,287 @@ +--[[ + Specialized reporter for sending data to Backtrace. + Useful for reporting Lua errors. +]] + +local CorePackages = game:GetService("CorePackages") + +local Cryo = require(CorePackages.Cryo) +local t = require(CorePackages.Packages.t) + +local BacktraceReport = require(script.Parent.BacktraceReport) +local ErrorQueue = require(script.Parent.Parent.ErrorQueue) + +local DEVELOPMENT_IN_STUDIO = game:GetService("RunService"):IsStudio() + +local DEFAULT_LOG_INTERVAL = 60 -- seconds + +local BacktraceReporter = {} +BacktraceReporter.__index = BacktraceReporter + +local IBacktraceReporter = t.strictInterface({ + httpService = t.some(t.instanceOf("HttpService"), t.interface({ + JSONEncode = t.callback, + JSONDecode = t.callback, + RequestInternal = t.callback, + })), + token = t.string, + processErrorReportMethod = t.optional(t.callback), + queueOptions = t.optional(t.table), + generateLogMethod = t.optional(t.callback), + logIntervalInSeconds = t.optional(t.numberPositive), +}) + +function BacktraceReporter.new(arguments) + local valid, message = IBacktraceReporter(arguments) + local self + + if valid then + self = { + _isEnabled = true, + _httpService = arguments.httpService, + _errorQueue = nil, + _reportUrl = game:GetFastString("ErrorUploadToBacktraceBaseUrl") .. "token=" .. arguments.token, + _processErrorReportMethod = arguments.processErrorReportMethod, + + _sharedAttributes = {}, + _sharedAnnotations = {}, + _generateLogMethod = arguments.generateLogMethod, + _logIntervalInSeconds = arguments.logIntervalInSeconds or DEFAULT_LOG_INTERVAL, + _lastLogTime = 0, + } + elseif (DEVELOPMENT_IN_STUDIO or _G.__TESTEZ_RUNNING_TEST__) then + error("invalid arguments for BacktraceReporter: " .. message) + else + self = { + _isEnabled = false, + } + end + + setmetatable(self, BacktraceReporter) + + -- Create and start the ErrorQueue for deferred reports. + if self._isEnabled then + self._errorQueue = ErrorQueue.new(function(...) + self:_reportErrorFromErrorQueue(...) + end, arguments.queueOptions) + + self._errorQueue:startTimer() + end + + return self +end + +function BacktraceReporter:sendErrorReport(report, log) + if not self._isEnabled then + return + end + + -- Validating the report can be slow; + -- And an invalid report might still be able to be processed, sent, and accepted by Backtrace. + -- So we don't validate reports in production. + if DEVELOPMENT_IN_STUDIO or _G.__TESTEZ_RUNNING_TEST__ then + assert(report:validate()) + end + + local encodeSuccess, jsonData = pcall(function() + return self._httpService:JSONEncode(report) + end) + + if not encodeSuccess then + warn("Cannot convert report to Json") + return + end + + pcall(function() + local httpRequest = self._httpService:RequestInternal({ + Url = self._reportUrl .. "&format=json", + Method = "POST", + Headers = { + ["Content-Type"] = "application/json", + }, + Body = jsonData, + }) + + httpRequest:Start(function(success, response) + -- Be aware that even when a response is 200, the report + -- might still be rejected/deleted by Backtrace after it is received. + if response.StatusCode == 200 and + log ~= nil then + + local decodeSuccesss, decodedBody = pcall(function() + return self._httpService:JSONDecode(response.Body) + end) + + if decodeSuccesss and decodedBody._rxid ~= nil then + self:_sendLogToReport(decodedBody._rxid, log) + end + end + end) + end) +end + +function BacktraceReporter:_sendLogToReport(reportRxid, log) + if type(log) ~= "string" or #log == 0 then + return + end + + pcall(function() + local httpRequest = self._httpService:RequestInternal({ + Url = self._reportUrl .. "&object=" .. reportRxid .. "&attachment_name=log.txt", + Method = "POST", + Headers = { + ["Content-Type"] = "text/plain", + }, + Body = log, + }) + + httpRequest:Start(function(reqSuccess, response) + -- We have no use for the result of this request right now. + end) + end) +end + +function BacktraceReporter:_generateLog() + if self._generateLogMethod ~= nil and + tick() - self._lastLogTime > self._logIntervalInSeconds then + self._lastLogTime = tick() + + local success, log = pcall(function() + return self._generateLogMethod() + end) + + if success and type(log) == "string" and #log > 0 then + return log + end + end + + return nil +end + +function BacktraceReporter:_generateErrorReport(errorMessage, errorStack, details) + local report = BacktraceReport.fromMessageAndStack(errorMessage, errorStack) + + report:addAttributes(self._sharedAttributes) + report:addAnnotations(self._sharedAnnotations) + + if type(details) == "string" and details ~= "" then + report:addAnnotations({ + ["stackDetails"] = details, + }) + end + + return report +end + +-- Immediate reports +-- You most likely should not use this. Use reportErrorDeferred instead. +function BacktraceReporter:reportErrorImmediately(errorMessage, errorStack, details) + if not self._isEnabled then + return + end + + local newReport = self:_generateErrorReport(errorMessage, errorStack, details) + + if self._processErrorReportMethod ~= nil then + newReport = self._processErrorReportMethod(newReport) + end + + local log = self:_generateLog() + + self:sendErrorReport(newReport, log) +end + +-- Deferred reports using an error queue +function BacktraceReporter:reportErrorDeferred(errorMessage, errorStack, details) + if not self._isEnabled then + return + end + + local errorKey = string.format("%s | %s", errorMessage, errorStack) + local errorData = {} + + -- If this error is a new one, we want a full report on it. + -- Similar errors following this one will be squashed in the queue and share report with this one + -- before they're flushed out and reported. + if not self._errorQueue:hasError(errorKey) then + local newReport = self:_generateErrorReport(errorMessage, errorStack, details) + + if self._processErrorReportMethod ~= nil then + newReport = self._processErrorReportMethod(newReport) + end + + errorData = { + backtraceReport = newReport, + log = self:_generateLog(), + } + end + + self._errorQueue:addError(errorKey, errorData) +end + +function BacktraceReporter:_reportErrorFromErrorQueue(errorKey, errorData, errorCount) + local errorReport = errorData.backtraceReport + local log = errorData.log + + errorReport:addAttributes({ + ErrorCount = errorCount, + }) + + self:sendErrorReport(errorReport, log) +end + +-- API for updating shared attributes/annotations +local IAttributes = BacktraceReport.IAttributes + +function BacktraceReporter:updateSharedAttributes(newAttributes) + -- Merge with current one first. This allows usage of Cryo.None. + local mergedAttributes = Cryo.Dictionary.join(self._sharedAttributes, newAttributes) + + -- Validate the merged result, and only update if it's valid. + local valid, message = IAttributes(mergedAttributes) + if not valid then + if DEVELOPMENT_IN_STUDIO or _G.__TESTEZ_RUNNING_TEST__ then + assert(valid, message) + else + return + end + end + + self._sharedAttributes = mergedAttributes +end + +local IAnnotations = BacktraceReport.IAnnotations + +function BacktraceReporter:updateSharedAnnotations(newAnnotations) + -- Although annotations can be nested tables, this is not a recursive merge. + local mergedAnnotations = Cryo.Dictionary.join(self._sharedAnnotations, newAnnotations) + + local valid, message = IAnnotations(mergedAnnotations) + if not valid then + if DEVELOPMENT_IN_STUDIO or _G.__TESTEZ_RUNNING_TEST__ then + assert(valid, message) + else + return + end + end + + self._sharedAnnotations = mergedAnnotations +end + +-- Flush all reports in the queue. +function BacktraceReporter:reportAllErrors() + if self._errorQueue ~= nil then + self._errorQueue:reportAllErrors() + end +end + +function BacktraceReporter:stop() + self._isEnabled = false + + if self._errorQueue ~= nil then + self:reportAllErrors() + self._errorQueue:stopTimer() + end +end + +return BacktraceReporter diff --git a/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReporter.spec.lua b/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReporter.spec.lua new file mode 100644 index 0000000..c371eab --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/BacktraceReporter.spec.lua @@ -0,0 +1,513 @@ +return function() + local BacktraceReporter = require(script.Parent.BacktraceReporter) + local BacktraceReport = require(script.Parent.BacktraceReport) + + local CorePackages = game:GetService("CorePackages") + local Cryo = require(CorePackages.Cryo) + local tutils = require(CorePackages.tutils) + + local requestsSent = 0 + local requestBody = nil + + local mockHttpRequestObj = {} + function mockHttpRequestObj:Start(onComplete) + requestsSent = requestsSent + 1 + onComplete(true, { + StatusCode = 200, + Body = { + _rxid = 12345, + } + }) + end + + local mockHttpService = {} + function mockHttpService:RequestInternal(options) + requestBody = options.Body + return mockHttpRequestObj + end + function mockHttpService:JSONEncode(data) + return data + end + function mockHttpService:JSONDecode(data) + return data + end + + local mockErrorMessage = "index nil" + local mockErrorStack = "Script 'Workspace.Script', Line 3" + + describe(".new", function() + it("should error if no httpService or token is passed in", function() + expect(function() + BacktraceReporter.new({}) + end).to.throw() + + expect(function() + BacktraceReporter.new({ + httpService = mockHttpService, + token = nil, + }) + end).to.throw() + + expect(function() + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "", + }) + reporter:stop() + end).to.never.throw() + end) + end) + + describe(":sendErrorReport", function() + it("should send error report through provided httpService", function() + requestsSent = 0 + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + local report = BacktraceReport.new() + + reporter:sendErrorReport(report) + + expect(requestsSent).to.equal(1) + + reporter:sendErrorReport(report) + + expect(requestsSent).to.equal(2) + + reporter:stop() + end) + + it("should assert if the error report is not valid", function() + requestsSent = 0 + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + local errorReport = { + error = "random", + } + + expect(function() + reporter:sendErrorReport(errorReport) + end).to.throw() + + expect(requestsSent).to.equal(0) + + reporter:stop() + end) + end) + + describe(":reportErrorImmediately", function() + it("should send error report through provided httpService", function() + requestsSent = 0 + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(2) + + reporter:stop() + end) + + it("should set details in the report if it's not nil", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack, "SomeDetails") + + expect(requestsSent).to.equal(1) + + expect(requestBody.annotations.stackDetails).to.equal("SomeDetails") + + reporter:stop() + end) + end) + + describe(":reportErrorDeferred", function() + it("should put error to a queue and send later", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + queueOptions = { + -- The queue should flush when there are 2 or more than 2 errors. + queueErrorLimit = 2, + }, + }) + + reporter:reportErrorDeferred(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(0) + + reporter:reportErrorDeferred(mockErrorMessage, mockErrorStack) + + -- These 2 errors would be squashed together + expect(requestsSent).to.equal(1) + expect(requestBody.attributes.ErrorCount).to.equal(2) + + reporter:stop() + end) + + it("should set details in the report if it's not nil", function() + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + queueOptions = { + -- The queue should flush when there are 2 or more than 2 errors. + queueErrorLimit = 2, + }, + }) + + requestsSent = 0 + requestBody = nil + reporter:reportErrorDeferred(mockErrorMessage, mockErrorStack, "SomeDetails") + reporter:reportErrorDeferred(mockErrorMessage, mockErrorStack, "SomeDetails") + + expect(requestsSent).to.equal(1) + + expect(requestBody.annotations.stackDetails).to.equal("SomeDetails") + + reporter:stop() + end) + end) + + describe("arguments.processErrorReportMethod", function() + it("should modify the error reports if passed in - reportErrorImmediately", function() + requestsSent = 0 + requestBody = nil + + local processErrorReport = function(report) + report.uuid = "id" + report.timestamp = 1 + report:addAttributes({ + ["Message"] = "test", + }) + return report + end + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + processErrorReportMethod = processErrorReport, + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + expect(requestBody.uuid).to.equal("id") + expect(requestBody.timestamp).to.equal(1) + expect(requestBody.attributes["Message"]).to.equal("test") + + reporter:stop() + end) + + it("should modify the error reports if passed in - reportErrorDeferred", function() + requestsSent = 0 + requestBody = nil + + local processErrorReport = function(report) + report.uuid = "id" + report.timestamp = 1 + report:addAttributes({ + ["Message"] = "test", + }) + return report + end + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + processErrorReportMethod = processErrorReport, + queueOptions = { + -- The queue should flush when there are 1 or more than 1 errors. + queueErrorLimit = 1, + }, + }) + + reporter:reportErrorDeferred(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + expect(requestBody.uuid).to.equal("id") + expect(requestBody.timestamp).to.equal(1) + expect(requestBody.attributes["Message"]).to.equal("test") + + reporter:stop() + end) + end) + + describe(":updateSharedAttributes", function() + it("should put the same attributes to all error reports", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + reporter:updateSharedAttributes({ + ["Message"] = "test", + ["Locale"] = "en-us", + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + expect(requestBody.attributes["Message"]).to.equal("test") + expect(requestBody.attributes["Locale"]).to.equal("en-us") + + requestBody = nil + reporter:reportErrorImmediately("some other message", mockErrorStack) + + expect(requestsSent).to.equal(2) + expect(requestBody.attributes["Message"]).to.equal("test") + expect(requestBody.attributes["Locale"]).to.equal("en-us") + + reporter:stop() + end) + + it("should merge attributes if called more than once", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + reporter:updateSharedAttributes({ + ["Message"] = "test", + ["Locale"] = "en-us", + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + expect(requestBody.attributes["Message"]).to.equal("test") + expect(requestBody.attributes["Locale"]).to.equal("en-us") + + reporter:updateSharedAttributes({ + ["Message"] = Cryo.None, + ["Locale"] = "zh-cn", + ["Theme"] = "light", + }) + + requestBody = nil + reporter:reportErrorImmediately("some other message", mockErrorStack) + + expect(requestsSent).to.equal(2) + expect(requestBody.attributes["Message"]).to.equal(nil) + expect(requestBody.attributes["Locale"]).to.equal("zh-cn") + expect(requestBody.attributes["Theme"]).to.equal("light") + + reporter:stop() + end) + + it("should throw if new attributes are ill-formatted", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + expect(function() + reporter:updateSharedAttributes({ + ["Message"] = Cryo.None, + ["Locale"] = "zh-cn", + ["Theme"] = function() end, -- callbacks are not allowed + }) + end).to.throw() + + reporter:stop() + end) + end) + + describe(":updateSharedAnnotations", function() + it("should put the same annotations to all error reports", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + local annotations = { + ["Message"] = "test", + ["AppInfo"] = { + ["Locale"] = "en-us", + ["Theme"] = "light", + }, + } + reporter:updateSharedAnnotations(annotations) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + expect(tutils.deepEqual(annotations, requestBody.annotations, true)).to.equal(true) + + requestBody = nil + reporter:reportErrorImmediately("some other message", mockErrorStack) + + expect(requestsSent).to.equal(2) + expect(tutils.deepEqual(annotations, requestBody.annotations, true)).to.equal(true) + + reporter:stop() + end) + + it("should merge annotations if called more than once", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + local annotations = { + ["Message"] = "test", + ["AppInfo"] = { + ["Locale"] = "en-us", + ["Theme"] = "light", + }, + ["AppVersion"] = "1.0", + } + reporter:updateSharedAnnotations(annotations) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + expect(tutils.deepEqual(annotations, requestBody.annotations, true)).to.equal(true) + + reporter:updateSharedAnnotations({ + ["Message"] = Cryo.None, + ["AppInfo"] = { + ["Theme"] = "dark", + }, + }) + + requestBody = nil + reporter:reportErrorImmediately("some other message", mockErrorStack) + + local expectedAnnotations = { + ["AppInfo"] = { + ["Theme"] = "dark", + }, + ["AppVersion"] = "1.0", + } + expect(requestsSent).to.equal(2) + expect(tutils.deepEqual(expectedAnnotations, requestBody.annotations, true)).to.equal(true) + + reporter:stop() + end) + + it("should throw if new annotations are ill-formatted", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + }) + + expect(function() + reporter:updateSharedAnnotations({ + ["Message"] = Cryo.None, + ["AppInfo"] = { + ["Locale"] = "en-us", + ["Theme"] = function() end, -- callbacks are not allowed + }, + }) + end).to.throw() + + reporter:stop() + end) + end) + + describe("Logging", function() + it("should send logs if provided generateLogMethod and error report is successful", function() + requestsSent = 0 + requestBody = nil + + local logText = "test log text" + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + generateLogMethod = function() + return logText + end, + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(2) -- one for error, one for log + expect(requestBody).to.equal(logText) + + reporter:stop() + end) + + it("should not send log if generateLogMethod did not return a string", function() + requestsSent = 0 + requestBody = nil + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + generateLogMethod = function() + return 123 + end, + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(1) + + reporter:stop() + end) + + it("should not send more than 1 log in logIntervalInSeconds provided", function() + requestsSent = 0 + requestBody = nil + + local logText = "test log text" + + local reporter = BacktraceReporter.new({ + httpService = mockHttpService, + token = "12345", + generateLogMethod = function() + return logText + end, + logIntervalInSeconds = 2, + }) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + + expect(requestsSent).to.equal(2) -- one for error, one for log + expect(requestBody).to.equal(logText) + + reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) + expect(requestsSent).to.equal(3) -- only one more, the error report + + reporter:stop() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/ProcessErrorStack.lua b/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/ProcessErrorStack.lua new file mode 100644 index 0000000..a3e7e9a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/ProcessErrorStack.lua @@ -0,0 +1,101 @@ +--[[ + Module for splitting the errorStack string to a list. + + Currently works with: + Pattern of error from ScriptContext.Error: + see ScriptContext.extractCallStack + + Pattern of error from ScriptContext.Error: (luau) + see ScriptContext.extractCallStack + + Pattern of error from debug.traceback: + see ScriptContext.printCallStack + + Pattern of error from debug.traceback: (luau) + see luau_backtrace +]] + +local function splitStringWithMarks(string, matches) + if type(string) ~= "string" or + type(matches) ~= "table" then + return string, "" + end + + for _, match in ipairs(matches) do + local start, stop = string.find(string, match, nil, true) + + if start ~= nil then + local first = string.sub(string, 1, start - 1) + local rest = string.sub(string, stop + 1) + return first, rest + end + end + + return string, "" +end + +local function findFileNameFromPath(pathStr) + if type(pathStr) ~= "string" then + return "" + end + + return string.match(pathStr, "([^.]*)$") +end + +local function ProcessErrorStack(errorStack) + local stack = {} + local sourceCodeDict = {} + local numOfSourceCode = 0 + + if type(errorStack) ~= "string" then + return stack, sourceCodeDict + end + + for line in errorStack:gmatch("[^\r\n]+") do + local newLine + local source + local funcName + local lineNumber + + source, newLine = splitStringWithMarks(line, {", line ", ", Line ", ":"}) + lineNumber, funcName = splitStringWithMarks(newLine, {" - "}) + + if lineNumber ~= "" and source ~= "" then + -- Convert "Script 'filePath'" to filePath + local _, _, matchedSource = string.find(source, "Script '(.*)'") + if matchedSource ~= nil then + source = matchedSource + end + + local index = sourceCodeDict[source] + if index == nil then + numOfSourceCode = numOfSourceCode + 1 + index = numOfSourceCode + sourceCodeDict[source] = index + end + + -- If no funcName is provided, Backtrace has difficulty differentiating the error + -- stacks apart. So we want to have something. + if funcName == "" then + funcName = findFileNameFromPath(source) + end + + table.insert(stack, { + line = lineNumber, + funcName = funcName, + sourceCode = tostring(index), + }) + end + end + + local sourceCodeOutput = {} + for path, index in pairs(sourceCodeDict) do + sourceCodeOutput[tostring(index)] = { + path = path, + } + end + + return stack, sourceCodeOutput +end + +return ProcessErrorStack diff --git a/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/ProcessErrorStack.spec.lua b/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/ProcessErrorStack.spec.lua new file mode 100644 index 0000000..6543d87 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/ErrorReporters/Backtrace/ProcessErrorStack.spec.lua @@ -0,0 +1,202 @@ +return function() + local ProcessErrorStack = require(script.Parent.ProcessErrorStack) + local CorePackages = game:GetService("CorePackages") + + local tutils = require(CorePackages.Packages.tutils) + + local testCasesNormal = { + ScriptContextError = { + error = "CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame, line 98 - field testError\nCoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame, line 111 - field ?\nCorePackages.Packages._Index.roact.roact.SingleEventManager, line 83", + expectedOutput = { + stack = { + [1] = { + line = "98", + funcName = "field testError", + sourceCode = "1", + }, + [2] = { + line = "111", + funcName = "field ?", + sourceCode = "1", + }, + [3] = { + line = "83", + funcName = "SingleEventManager", + sourceCode = "2", + }, + }, + sourceCodeOutput = { + ["1"] = { + path = "CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame", + }, + ["2"] = { + path = "CorePackages.Packages._Index.roact.roact.SingleEventManager", + }, + }, + }, + }, + ScriptContextErrorLuau = { + error = "CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame, line 98\nCoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame, line 111\nCorePackages.Packages._Index.roact.roact.SingleEventManager, line 83", + expectedOutput = { + stack = { + [1] = { + line = "98", + funcName = "HomePageWithAvatarViewportFrame", + sourceCode = "1", + }, + [2] = { + line = "111", + funcName = "HomePageWithAvatarViewportFrame", + sourceCode = "1", + }, + [3] = { + line = "83", + funcName = "SingleEventManager", + sourceCode = "2", + }, + }, + sourceCodeOutput = { + ["1"] = { + path = "CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame", + }, + ["2"] = { + path = "CorePackages.Packages._Index.roact.roact.SingleEventManager", + }, + }, + }, + }, + DebugTraceback = { + error = "Stack Begin\nScript 'CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame', Line 100 - field testError\nScript 'CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame', Line 111 - field ?\nScript 'CorePackages.Packages._Index.roact.roact.SingleEventManager', Line 83\nStack End", + expectedOutput = { + stack = { + [1] = { + line = "100", + funcName = "field testError", + sourceCode = "1", + }, + [2] = { + line = "111", + funcName = "field ?", + sourceCode = "1", + }, + [3] = { + line = "83", + funcName = "SingleEventManager", + sourceCode = "2", + }, + }, + sourceCodeOutput = { + ["1"] = { + path = "CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame", + }, + ["2"] = { + path = "CorePackages.Packages._Index.roact.roact.SingleEventManager", + }, + }, + }, + }, + DebugTracebackLuau = { + error = "CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame:100\nCoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame:111\nCorePackages.Packages._Index.roact.roact.SingleEventManager:83", + expectedOutput = { + stack = { + [1] = { + line = "100", + funcName = "HomePageWithAvatarViewportFrame", + sourceCode = "1", + }, + [2] = { + line = "111", + funcName = "HomePageWithAvatarViewportFrame", + sourceCode = "1", + }, + [3] = { + line = "83", + funcName = "SingleEventManager", + sourceCode = "2", + }, + }, + sourceCodeOutput = { + ["1"] = { + path = "CoreGui.RobloxGui.Modules.LuaApp.Components.Home.HomePageWithAvatarViewportFrame", + }, + ["2"] = { + path = "CorePackages.Packages._Index.roact.roact.SingleEventManager", + }, + }, + }, + }, + OneLineError = { + error = "Script 'Workspace.Script', Line 3", + expectedOutput = { + stack = { + [1] = { + line = "3", + funcName = "Script", + sourceCode = "1", + }, + }, + sourceCodeOutput = { + ["1"] = { + path = "Workspace.Script", + }, + }, + }, + }, + PathWithNumbers = { + error = "Script 'Workspace1.Script2', Line 3", + expectedOutput = { + stack = { + [1] = { + line = "3", + funcName = "Script2", + sourceCode = "1", + }, + }, + sourceCodeOutput = { + ["1"] = { + path = "Workspace1.Script2", + }, + }, + }, + }, + } + + it("should convert the error strings to the correct format", function() + for key, testCase in pairs(testCasesNormal) do + local stack, sourceCodeOutput = ProcessErrorStack(testCase.error) + + expect(tutils.deepEqual(testCase.expectedOutput, { + stack = stack, + sourceCodeOutput = sourceCodeOutput, + }, true)).to.equal(true) + end + end) + + local testCasesOther = { + {}, + 123, + function() error("test") end, + "", + " - ", + ", line ", + ", line - ", + ":", + " - , line ", + } + + it("should return empty results with inputs that are not valid stack trace", function() + for _, testCase in ipairs(testCasesOther) do + local stack, sourceCodeOutput = ProcessErrorStack(testCase) + + expect(tutils.shallowEqual(stack, {})).to.equal(true) + expect(tutils.shallowEqual(sourceCodeOutput, {})).to.equal(true) + end + end) + + it("should return empty results with nil input", function() + local stack, sourceCodeOutput = ProcessErrorStack(nil) + + expect(tutils.shallowEqual(stack, {})).to.equal(true) + expect(tutils.shallowEqual(sourceCodeOutput, {})).to.equal(true) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/ErrorReporters/ErrorQueue.lua b/Client2021/ExtraContent/LuaPackages/ErrorReporters/ErrorQueue.lua new file mode 100644 index 0000000..0762b3c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/ErrorReporters/ErrorQueue.lua @@ -0,0 +1,129 @@ +local CorePackages = game:GetService("CorePackages") +local RunService = game:GetService("RunService") + +local Cryo = require(CorePackages.Packages.Cryo) +local t = require(CorePackages.Packages.t) + +-- Time limit is in seconds. +local DEFAULT_QUEUE_TIME_LIMIT = 30 +local DEFAULT_QUEUE_ERROR_LIMIT = 30 +local DEFAULT_QUEUE_KEY_LIMIT = 10 + +local ErrorQueue = {} +ErrorQueue.__index = ErrorQueue + +local IErrorQueue = t.tuple( + t.callback, + t.optional(t.strictInterface({ + queueTimeLimit = t.optional(t.numberPositive), + queueErrorLimit = t.optional(t.numberPositive), + queueKeyLimit = t.optional(t.numberPositive), + })) +) + +function ErrorQueue.new(reportMethod, options) + assert(IErrorQueue(reportMethod, options)) + + options = options or {} + + local self = { + _reportMethod = reportMethod, + + -- config + _queueTimeLimit = options.queueTimeLimit or DEFAULT_QUEUE_TIME_LIMIT, + _queueErrorLimit = options.queueErrorLimit or DEFAULT_QUEUE_ERROR_LIMIT, + _queueKeyLimit = options.queueKeyLimit or DEFAULT_QUEUE_KEY_LIMIT, + + _errors = {}, + _totalErrorCount = 0, + _totalKeyCount = 0, + + _runningTime = 0, + _renderSteppedConnection = nil, + } + + setmetatable(self, ErrorQueue) + + return self +end + +function ErrorQueue:hasError(errorKey) + if type(errorKey) ~= "string" or errorKey == "" then + return false + else + return self._errors[errorKey] ~= nil + end +end + +function ErrorQueue:addError(errorKey, errorData) + if type(errorKey) ~= "string" or errorKey == "" then + return + end + + if not self._errors[errorKey] then + -- Errors with the same key will be sent together as 1 error, with a count parameter. + -- We only keep the data from the oldest error with this key in the queue. + self._errors[errorKey] = { + data = errorData, + count = 1, + } + self._totalKeyCount = self._totalKeyCount + 1 + else + self._errors[errorKey].count = self._errors[errorKey].count + 1 + end + + self._totalErrorCount = self._totalErrorCount + 1 + + if self:isReadyToReport() then + self:reportAllErrors() + end +end + +function ErrorQueue:isReadyToReport() + return self._totalKeyCount >= self._queueKeyLimit or + self._totalErrorCount >= self._queueErrorLimit or + (self._totalErrorCount > 0 and self._runningTime >= self._queueTimeLimit) +end + +function ErrorQueue:reportAllErrors() + -- copy the error queue and instantly clear it out + local errors = Cryo.Dictionary.join(self._errors, {}) + + self._errors = {} + self._totalErrorCount = 0 + self._totalKeyCount = 0 + self._runningTime = 0 + + -- report the errors + for errorKey, errData in pairs(errors) do + self._reportMethod(errorKey, errData.data, errData.count) + end +end + +function ErrorQueue:_onRenderStep(dt) + self._runningTime = self._runningTime + dt + + if self:isReadyToReport() then + self:reportAllErrors() + end +end + +function ErrorQueue:startTimer() + if self._renderSteppedConnection == nil then + self._runningTime = 0 + + self._renderSteppedConnection = RunService.renderStepped:Connect(function(dt) + self:_onRenderStep(dt) + end) + end +end + +function ErrorQueue:stopTimer() + if self._renderSteppedConnection ~= nil then + self._renderSteppedConnection:Disconnect() + self._runningTime = 0 + self._renderSteppedConnection = nil + end +end + +return ErrorQueue diff --git a/Client2021/ExtraContent/LuaPackages/ErrorReporters/ErrorQueue.spec.lua b/Client2021/ExtraContent/LuaPackages/ErrorReporters/ErrorQueue.spec.lua new file mode 100644 index 0000000..7572d01 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/ErrorReporters/ErrorQueue.spec.lua @@ -0,0 +1,299 @@ +return function() + local ErrorQueue = require(script.Parent.ErrorQueue) + local CorePackages = game:GetService("CorePackages") + + local tutils = require(CorePackages.Packages.tutils) + + local errorsToAdd = { + [1] = { + key = "test", + value = 2.33, + }, + [2] = { + key = "test", + value = 2, + }, + [3] = { + key = "test", + value = 3.14, + }, + [4] = { + key = "test2", + value = 123, + }, + [5] = { + key = "test2", + value = 456, + }, + [6] = { + key = "test3", + value = "something", + }, + } + + describe("queueErrorLimit", function() + it("should report errors correctly when the total error limit is reached", function() + local expectedErrorsReported = { + ["test"] = { + data = 2.33, + count = 3, + }, + ["test2"] = { + data = 123, + count = 2, + }, + ["test3"] = { + data = "something", + count = 1, + }, + } + + local errorsReported = {} + local reportCount = 0 + local reportMethod = function(errorKey, errorData, errorCount) + reportCount = reportCount + 1 + errorsReported[errorKey] = { + data = errorData, + count = errorCount, + } + end + + local errorQueue = ErrorQueue.new(reportMethod, { + queueErrorLimit = 6, + }) + + for _, error in ipairs(errorsToAdd) do + errorQueue:addError(error.key, error.value) + end + + expect(reportCount).to.equal(3) + for errorKey, expectedError in pairs(expectedErrorsReported) do + expect(tutils.deepEqual(expectedError, errorsReported[errorKey])).to.equal(true) + end + end) + + it("should not report errors before the total error limit is reached", function() + local errorsReported = {} + local reportCount = 0 + local reportMethod = function(errorKey, errorData, errorCount) + reportCount = reportCount + 1 + errorsReported[errorKey] = { + data = errorData, + count = errorCount, + } + end + + local errorQueue = ErrorQueue.new(reportMethod, { + queueErrorLimit = 6, + }) + + for index = 1, 5 do + local error = errorsToAdd[index] + errorQueue:addError(error.key, error.value) + end + + expect(reportCount).to.equal(0) + expect(tutils.deepEqual(errorsReported, {})).to.equal(true) + end) + end) + + describe("queueKeyLimit", function() + it("should report errors correctly when the total key limit is reached", function() + local expectedErrorsReported = { + ["test"] = { + data = 2.33, + count = 3, + }, + ["test2"] = { + data = 123, + count = 2, + }, + ["test3"] = { + data = "something", + count = 1, + }, + } + + local errorsReported = {} + local reportCount = 0 + local reportMethod = function(errorKey, errorData, errorCount) + reportCount = reportCount + 1 + errorsReported[errorKey] = { + data = errorData, + count = errorCount, + } + end + + local errorQueue = ErrorQueue.new(reportMethod, { + queueKeyLimit = 3, + queueErrorLimit = 100, + }) + + for _, error in ipairs(errorsToAdd) do + errorQueue:addError(error.key, error.value) + end + + expect(reportCount).to.equal(3) + for errorKey, expectedError in pairs(expectedErrorsReported) do + expect(tutils.deepEqual(expectedError, errorsReported[errorKey])).to.equal(true) + end + end) + + it("should not report errors before the total key limit is reached", function() + local errorsReported = {} + local reportCount = 0 + local reportMethod = function(errorKey, errorData, errorCount) + reportCount = reportCount + 1 + errorsReported[errorKey] = { + data = errorData, + count = errorCount, + } + end + + local errorQueue = ErrorQueue.new(reportMethod, { + queueKeyLimit = 4, + queueErrorLimit = 100, + }) + + for _, error in ipairs(errorsToAdd) do + errorQueue:addError(error.key, error.value) + end + + expect(reportCount).to.equal(0) + expect(tutils.deepEqual(errorsReported, {})).to.equal(true) + end) + end) + + describe("queueTimeLimit", function() + it("should report errors correctly when the total time limit is reached", function() + local expectedErrorsReported = { + ["test"] = { + data = 2.33, + count = 3, + }, + ["test2"] = { + data = 123, + count = 2, + }, + ["test3"] = { + data = "something", + count = 1, + }, + } + + local errorsReported = {} + local reportCount = 0 + local reportMethod = function(errorKey, errorData, errorCount) + reportCount = reportCount + 1 + errorsReported[errorKey] = { + data = errorData, + count = errorCount, + } + end + + local errorQueue = ErrorQueue.new(reportMethod, { + queueTimeLimit = 0.5, + queueErrorLimit = 100, + queueKeyLimit = 100, + }) + + errorQueue:startTimer() + + for _, error in ipairs(errorsToAdd) do + errorQueue:addError(error.key, error.value) + end + + errorQueue:_onRenderStep(0.5) + + expect(reportCount).to.equal(3) + for errorKey, expectedError in pairs(expectedErrorsReported) do + expect(tutils.deepEqual(expectedError, errorsReported[errorKey])).to.equal(true) + end + + errorQueue:stopTimer() + end) + + it("should not report errors before the total time limit is reached", function() + local errorsReported = {} + local reportCount = 0 + local reportMethod = function(errorKey, errorData, errorCount) + reportCount = reportCount + 1 + errorsReported[errorKey] = { + data = errorData, + count = errorCount, + } + end + + local errorQueue = ErrorQueue.new(reportMethod, { + queueTimeLimit = 2, + queueErrorLimit = 100, + queueKeyLimit = 100, + }) + + errorQueue:startTimer() + + for _, error in ipairs(errorsToAdd) do + errorQueue:addError(error.key, error.value) + end + + errorQueue:_onRenderStep(1) + + expect(reportCount).to.equal(0) + expect(tutils.deepEqual(errorsReported, {})).to.equal(true) + + errorQueue:stopTimer() + end) + end) + + describe("reportAllErrors", function() + it("should report all errors correctly when called", function() + local expectedErrorsReported = { + ["test"] = { + data = 2.33, + count = 3, + }, + ["test2"] = { + data = 123, + count = 2, + }, + ["test3"] = { + data = "something", + count = 1, + }, + } + + local errorsReported = {} + local reportCount = 0 + local reportMethod = function(errorKey, errorData, errorCount) + reportCount = reportCount + 1 + errorsReported[errorKey] = { + data = errorData, + count = errorCount, + } + end + + local errorQueue = ErrorQueue.new(reportMethod, { + queueTimeLimit = 100, + queueErrorLimit = 100, + queueKeyLimit = 100, + }) + + errorQueue:startTimer() + + for _, error in ipairs(errorsToAdd) do + errorQueue:addError(error.key, error.value) + end + + expect(reportCount).to.equal(0) + + errorQueue:reportAllErrors() + + expect(reportCount).to.equal(3) + for errorKey, expectedError in pairs(expectedErrorsReported) do + expect(tutils.deepEqual(expectedError, errorsReported[errorKey])).to.equal(true) + end + + errorQueue:stopTimer() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/InGameMenuDependencies.lua b/Client2021/ExtraContent/LuaPackages/InGameMenuDependencies.lua new file mode 100644 index 0000000..a359dff --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/InGameMenuDependencies.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.InGameMenuDependencies) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/InGameServices/.robloxrc b/Client2021/ExtraContent/LuaPackages/InGameServices/.robloxrc new file mode 100644 index 0000000..33c7a1c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/InGameServices/.robloxrc @@ -0,0 +1,9 @@ +{ + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "ImplicitReturn": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/InGameServices/MouseIconOverrideService.lua b/Client2021/ExtraContent/LuaPackages/InGameServices/MouseIconOverrideService.lua new file mode 100644 index 0000000..f40b758 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/InGameServices/MouseIconOverrideService.lua @@ -0,0 +1,71 @@ +--[[ + Prevents conflicts when multiple CoreScript subsystems are trying to override + the mouse cursor at the same time. This uses a stack approach similar to how + ContextActionService works. The most recent override takes precedence, and any + previously set overrides will remain on the stack beneath it until removed. + + Example usage: + local MOUSE_OVERRIDE_KEY = Symbol.named("PurchasePrompt") + MouseIconOverrideService.push(MOUSE_OVERRIDE_KEY, Enum.OverrideMouseIconBehavior.ForceShow) + MouseIconOverrideService.pop(MOUSE_OVERRIDE_KEY) +]] + +local UserInputService = game:GetService("UserInputService") + +local FFlagUseCursorOverrideManager2 = game:DefineFastFlag("UseCursorOverrideManager2", false) + +local cursorOverrideStack = {} + +local function update() + local activeOverride = cursorOverrideStack[#cursorOverrideStack] + if activeOverride then + UserInputService.OverrideMouseIconBehavior = activeOverride[2] + else + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None + end +end + +return { + push = function(key, behavior) + if not FFlagUseCursorOverrideManager2 then + UserInputService.OverrideMouseIconBehavior = behavior + return + end + + assert(type(key) == "userdata" or type(key) == "string") + assert(typeof(behavior) == "EnumItem") + assert(behavior.EnumType == Enum.OverrideMouseIconBehavior) + + for idx, entry in ipairs(cursorOverrideStack) do + if entry[1] == key then + table.remove(cursorOverrideStack, idx) + break + end + end + + table.insert(cursorOverrideStack, {key, behavior}) + update() + end, + pop = function(key) + if not FFlagUseCursorOverrideManager2 then + UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None + return + end + + assert(type(key) == "userdata" or type(key) == "string") + + local idx + + for testIdx, entry in ipairs(cursorOverrideStack) do + if entry[1] == key then + idx = testIdx + break + end + end + + assert(idx, "No cursor override named " .. tostring(key)) + + table.remove(cursorOverrideStack, idx) + update() + end, +} diff --git a/Client2021/ExtraContent/LuaPackages/Localization/.robloxrc b/Client2021/ExtraContent/LuaPackages/Localization/.robloxrc new file mode 100644 index 0000000..321fd28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Localization/.robloxrc @@ -0,0 +1,12 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "ImplicitReturn": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Localization/LocalizationConsumer.lua b/Client2021/ExtraContent/LuaPackages/Localization/LocalizationConsumer.lua new file mode 100644 index 0000000..8e6c135 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Localization/LocalizationConsumer.lua @@ -0,0 +1,100 @@ + local CorePackages = game:GetService("CorePackages") +local LocalizationService = game:GetService("LocalizationService") + +local Roact = require(CorePackages.Roact) +local ExternalEventConnection = require(CorePackages.RoactUtilities.ExternalEventConnection) + +local ArgCheck = require(CorePackages.ArgCheck) +local LocalizationKey = require(CorePackages.Localization.LocalizationKey) + +local LocalizationConsumer = Roact.Component:extend("LocalizationConsumer") + +function LocalizationConsumer:init() + local localization = self._context[LocalizationKey].localization + + if localization == nil then + error("LocalizationConsumer must be below a LocalizationProvider.") + end + + self.state = { + locale = LocalizationService.RobloxLocaleId, + } + + self.updateLocalization = function(newLocale) + if settings():GetFFlag("AppBridgeStartupController") then + newLocale = localization:GetLocale() + end + if newLocale ~= self.state.locale then + self:setState({ + locale = newLocale + }) + end + end + + if settings():GetFFlag("AppBridgeStartupController") then + self.connections = { + localization.changed:connect(self.updateLocalization) + } + end +end + +function LocalizationConsumer:willUnmount() + if settings():GetFFlag("AppBridgeStartupController") then + for _, connection in pairs(self.connections) do + connection:disconnect() + end + end +end + +function LocalizationConsumer:render() + local localization = self._context[LocalizationKey].localization + local render = self.props.render + local stringsToBeLocalized = self.props.stringsToBeLocalized + + ArgCheck.isType(render, "function", "LocalizationConsumer.props.render") + ArgCheck.isType(stringsToBeLocalized, "table", "LocalizationConsumer.props.stringsToBeLocalized") + + local localizedStrings = {} + for stringName, stringInfo in pairs(stringsToBeLocalized) do + if typeof(stringInfo) == "table" then + if typeof(stringInfo[1]) == "string" then + local success, result = pcall(function() + return localization:Format(stringInfo[1], stringInfo) + end) + + ArgCheck.isEqual(success, true, string.format( + "LocalizationConsumer finding value for translation key[%s]: %s", stringName, stringInfo[1])) + + localizedStrings[stringName] = success and result or "" + else + error(string.format("%s[1] in stringsToBeLocalized must be a string, got %s instead", + stringName, typeof(stringInfo[1]))) + end + elseif typeof(stringInfo) == "string" then + local success, result = pcall(function() + return localization:Format(stringInfo) + end) + + ArgCheck.isEqual(success, true, string.format( + "LocalizationConsumer finding value for translation key[%s]: %s", stringName, stringInfo)) + + localizedStrings[stringName] = success and result or "" + else + error(string.format("%s in stringsToBeLocalized must be a string or table, got %s instead", + stringName, typeof(stringInfo))) + end + end + + if settings():GetFFlag("AppBridgeStartupController") then + return render(localizedStrings) + else + return Roact.createElement(ExternalEventConnection, { + event = LocalizationService:GetPropertyChangedSignal("RobloxLocaleId"), + callback = self.updateLocalization, + }, { + Component = render(localizedStrings), + }) + end +end + +return LocalizationConsumer diff --git a/Client2021/ExtraContent/LuaPackages/Localization/LocalizationConsumer.spec.lua b/Client2021/ExtraContent/LuaPackages/Localization/LocalizationConsumer.spec.lua new file mode 100644 index 0000000..9a3dca2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Localization/LocalizationConsumer.spec.lua @@ -0,0 +1,4 @@ +return function() + itSKIP("SOC-6353 - These unit tests need to be moved from LuaApp", function() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Localization/LocalizationContext.lua b/Client2021/ExtraContent/LuaPackages/Localization/LocalizationContext.lua new file mode 100644 index 0000000..e78ef17 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Localization/LocalizationContext.lua @@ -0,0 +1,142 @@ +--[[ + Contains all of the loaded translations and provides methods to translate + keys and parameters to strings. + + LocalizationContext doesn't handle loading of specific languages, but does + recommend what languages should be loaded (if available). + + To create a new LocalizationContext: + + local currentLanguage = LocalizationService.RobloxLocaleId + local languages = LocalizationContext.getRelevantLanguages(currentLanguage) + + local translations = {} + + -- Use the list of languages to load a set of translation tables here. + -- A translation table is just a map from key to the translated string. + -- `translations` is a map from language to translation tables. + + local context = LocalizationContext.new(translations) + + -- Get a string that doesn't require parameters + context:getString(currentLanguage, "SOME_KEY") + + -- Passing parameters: + context:getString(currentLanguage, "FANCY_KEY", { + apples = 5, + }) + + Additional languages can be added after the LocalizationContext is created + by calling `addTranslations`. Whenever the user's language changes, call + `getRelevantLanguages` to get a new list of languages to load, load them, + then call `addTranslations` to merge them in with the existing tables. +]] + +--[[ + Finds the base language code for the given language, if there is one. + + We assume: + * Language codes are of the form LANGUAGE or LANGUAGE_COUNTRY + * LANGUAGE_COUNTRY is more specific than LANGUAGE +]] + +local function getBaseLanguage(languageName) + return languageName:match("^(%w+)[-_]") +end + +local LocalizationContext = {} +LocalizationContext.__index = LocalizationContext + +function LocalizationContext.new(translations) + local self = { + _translations = translations, + } + + setmetatable(self, LocalizationContext) + + return self +end + +--[[ + Add translations to an existing LocalizationContext, such as when a user + switches languages while the app is running. +]] +function LocalizationContext:addTranslations(translations) + self._translations = translations +end + +--[[ + Yields a list of languages relevant to the current user. + + When the user's language changes, query this value, load those translations, + and add them to the LocalizationContext using addTranslations. +]] +function LocalizationContext.getRelevantLanguages(primaryLanguage) + local languages = {} + + -- Load the language itself if available. + table.insert(languages, primaryLanguage) + + -- If there's a fallback for our current language, load that as well. + local fallbackLanguage = getBaseLanguage(primaryLanguage) + if fallbackLanguage then + table.insert(languages, fallbackLanguage) + end + + -- We should always load English, as it should contain every valid key. + table.insert(languages, "en-us") + return languages +end + +function LocalizationContext:_getSourceString(language, key) + local translationTable = self._translations[language] + + if not translationTable then + return nil + end + + return translationTable[key] +end + +--[[ + Translate a key with a set of arguments into the given language. + + `language` must be explicitly provided +]] +function LocalizationContext:getString(language, key, parameters) + local exactValue = self:_getSourceString(language, key) + + local baseLanguage = getBaseLanguage(language) + + local baseLanguageValue + if baseLanguage then + baseLanguageValue = self:_getSourceString(baseLanguage, key) + end + + local englishValue = self:_getSourceString("en-us", key) + + -- We try to find source strings in descending priority here: + local sourceString = exactValue or baseLanguageValue or englishValue + + -- Missing translations are considered a developer error, so we throw here. + if not sourceString then + local message = ( + "Couldn't find value for translation key %q!\n" .. + "Tried these languages: %s, %s, %s" + ):format( + key, + language, baseLanguage, "en-us" + ) + error(message, 2) + end + + -- If we have parameters to insert into the string, put them in! + -- We don't check for missing parameters, should we in the future? + if parameters then + return (sourceString:gsub("{(.-)}", parameters)) + else + return sourceString + end +end + +return LocalizationContext \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Localization/LocalizationContext.spec.lua b/Client2021/ExtraContent/LuaPackages/Localization/LocalizationContext.spec.lua new file mode 100644 index 0000000..8c7b8c0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Localization/LocalizationContext.spec.lua @@ -0,0 +1,66 @@ +return function() + local LocalizationContext = require(script.Parent.LocalizationContext) + + it("should pull from the correct language if available", function() + local context = LocalizationContext.new({ + ["es-mx"] = { + ["SomeKey"] = "Foo", + }, + ["es"] = { + ["SomeKey"] = "Bar", + }, + ["en-us"] = { + ["SomeKey"] = "Baz", + }, + }) + + expect(context:getString("es-mx", "SomeKey")).to.equal("Foo") + expect(context:getString("es", "SomeKey")).to.equal("Bar") + expect(context:getString("en", "SomeKey")).to.equal("Baz") + end) + + it("should fall through to a language's base language", function() + local context = LocalizationContext.new({ + ["es-mx"] = {}, + ["es"] = { + ["SomeKey"] = "Bar", + }, + ["en"] = { + ["SomeKey"] = "Baz", + }, + }) + + expect(context:getString("es-mx", "SomeKey")).to.equal("Bar") + expect(context:getString("es", "SomeKey")).to.equal("Bar") + expect(context:getString("en", "SomeKey")).to.equal("Baz") + end) + + it("should fall through to English if keys are missing in each table", function() + local context = LocalizationContext.new({ + ["es-mx"] = {}, + ["es"] = {}, + ["en-us"] = { + ["SomeKey"] = "Baz", + }, + }) + + expect(context:getString("es-mx", "SomeKey")).to.equal("Baz") + expect(context:getString("es", "SomeKey")).to.equal("Baz") + expect(context:getString("en_us", "SomeKey")).to.equal("Baz") + end) + + it("should replace formatting identifiers of the form {name}", function() + local context = LocalizationContext.new({ + ["en-us"] = { + ["SomeKey"] = "{greeting}, {target}!", + }, + }) + + local value = context:getString("en-us", "SomeKey", { + greeting = "Hello", + target = "world", + }) + + expect(value).to.equal("Hello, world!") + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Localization/LocalizationKey.lua b/Client2021/ExtraContent/LuaPackages/Localization/LocalizationKey.lua new file mode 100644 index 0000000..9207eb4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Localization/LocalizationKey.lua @@ -0,0 +1,4 @@ +local CorePackages = game:GetService("CorePackages") +local Symbol = require(CorePackages.Symbol) + +return Symbol.named("Localization") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Localization/LocalizationProvider.lua b/Client2021/ExtraContent/LuaPackages/Localization/LocalizationProvider.lua new file mode 100644 index 0000000..dd92ee6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Localization/LocalizationProvider.lua @@ -0,0 +1,19 @@ +local CorePackages = game:GetService("CorePackages") + +local Roact = require(CorePackages.Roact) +local LocalizationKey = require(CorePackages.Localization.LocalizationKey) + +local LocalizationProvider = Roact.Component:extend("LocalizationProvider") + +function LocalizationProvider:init(props) + local localization = props.localization + self._context[LocalizationKey] = { + localization = localization + } +end + +function LocalizationProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +return LocalizationProvider diff --git a/Client2021/ExtraContent/LuaPackages/Localization/NumberLocalization.lua b/Client2021/ExtraContent/LuaPackages/Localization/NumberLocalization.lua new file mode 100644 index 0000000..4b040e6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Localization/NumberLocalization.lua @@ -0,0 +1,308 @@ +-- Example locale-sensitive number formatting: +-- https://docs.oracle.com/cd/E19455-01/806-0169/overview-9/index.html + +--[[ + Locale specification: + [DECIMAL_SEPARATOR] = string for decimal point, if needed + [GROUP_DELIMITER] = string for groupings of numbers left of the decimal + List section = abbreviations for language, in increasing order + + Missing features in this code: + - No support for differences in number of digits per GROUP_DELIMITER. + Some Chinese dialects group by 10000 instead of 1000. + - No support for variable differences in number of digits per GROUP_DELIMITER. + Indian natural language groups the first 3 to left of decimal, then every 2 after that. + + See https://en.wikipedia.org/wiki/Decimal_separator#Digit_grouping +]] +local CorePackages = game:GetService("CorePackages") +local Logging = require(CorePackages.Logging) + +local RoundingBehaviour = require(script.Parent.RoundingBehaviour) + +local localeInfos = {} + +local DEFAULT_LOCALE = "en-us" + +-- Separator aliases to help avoid spelling errors +local DECIMAL_SEPARATOR = "decimalSeparator" +local GROUP_DELIMITER = "groupDelimiter" + +localeInfos["en-us"] = { + [DECIMAL_SEPARATOR] = ".", + [GROUP_DELIMITER] = ",", + { 1, "", }, + { 1e3, "K", }, + { 1e6, "M", }, + { 1e9, "B", }, +} + +localeInfos["es-es"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = ".", + { 1, "", }, + { 1e3, " mil", }, + { 1e6, " M", }, +} + +localeInfos["fr-fr"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = " ", + { 1, "", }, + { 1e3, " k", }, + { 1e6, " M", }, + { 1e9, " Md", }, +} + +localeInfos["de-de"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = " ", + { 1, "", }, + { 1e3, " Tsd.", }, + { 1e6, " Mio.", }, + { 1e9, " Mrd.", }, +} + +localeInfos["pt-br"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = ".", + { 1, "", }, + { 1e3, " mil", }, + { 1e6, " mi", }, + { 1e9, " bi", }, +} + +localeInfos["zh-cn"] = { + [DECIMAL_SEPARATOR] = ".", + [GROUP_DELIMITER] = ",", -- Chinese commonly uses 3 digit groupings, despite 10000s rule + { 1, "", }, + { 1e3, "千", }, + { 1e4, "万", }, + { 1e8, "亿", }, +} + +localeInfos["zh-cjv"] = { + [DECIMAL_SEPARATOR] = ".", + [GROUP_DELIMITER] = ",", + { 1, "", }, + { 1e3, "千", }, + { 1e4, "万", }, + { 1e8, "亿", }, +} + +localeInfos["zh-tw"] = { + [DECIMAL_SEPARATOR] = ".", + [GROUP_DELIMITER] = ",", + { 1, "", }, + { 1e3, "千", }, + { 1e4, "萬", }, + { 1e8, "億", }, +} + +localeInfos["ko-kr"] = { + [DECIMAL_SEPARATOR] = ".", + [GROUP_DELIMITER] = ",", + { 1, "", }, + { 1e3, "천", }, + { 1e4, "만", }, + { 1e8, "억", }, +} + +localeInfos["ja-jp"] = { + [DECIMAL_SEPARATOR] = ".", + [GROUP_DELIMITER] = ",", + { 1, "", }, + { 1e3, "千", }, + { 1e4, "万", }, + { 1e8, "億", }, +} + +localeInfos["it-it"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = " ", + { 1, "", }, + { 1e3, " mila", }, + { 1e6, " Mln", }, + { 1e9, " Mld", }, +} + +localeInfos["ru-ru"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = ".", + { 1, "", }, + { 1e3, " тыс", }, + { 1e6, " млн", }, + { 1e9, " млрд", }, +} + +localeInfos["id-id"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = ".", + { 1, "", }, + { 1e3, " rb", }, + { 1e6, " jt", }, + { 1e9, " M", }, +} + +localeInfos["vi-vn"] = { + [DECIMAL_SEPARATOR] = ".", + [GROUP_DELIMITER] = " ", + { 1, "", }, + { 1e3, " N", }, + { 1e6, " Tr", }, + { 1e9, " T", }, +} + +localeInfos["th-th"] = { + [DECIMAL_SEPARATOR] = ".", + [GROUP_DELIMITER] = ",", + { 1, "", }, + { 1e3, " พ", }, + { 1e4, " ม", }, + { 1e5, " ส", }, + { 1e6, " ล", }, +} + +localeInfos["tr-tr"] = { + [DECIMAL_SEPARATOR] = ",", + [GROUP_DELIMITER] = ".", + { 1, "", }, + { 1e3, " B", }, + { 1e6, " Mn", }, + { 1e9, " Mr", }, +} + +-- Aliases for languages that use the same mappings. +localeInfos["en-gb"] = localeInfos["en-us"] +localeInfos["es-mx"] = localeInfos["es-es"] + +local function findDecimalPointIndex(numberStr) + return string.find(numberStr, "%.") or #numberStr + 1 +end + +-- Find the base 10 offset needed to make 0.1 <= abs(number) < 1 +local function findDecimalOffset(number) + if number == 0 then + return 0 + end + + local offsetToOnesRange = math.floor(math.log10(math.abs(number))) + return -(offsetToOnesRange + 1) -- Offset one more (or less) digit +end + +local function roundToSignificantDigits(number, significantDigits, roundingBehaviour) + local offset = findDecimalOffset(number) + local multiplier = 10^(significantDigits + offset) + local significand + if roundingBehaviour == RoundingBehaviour.Truncate then + significand = math.modf(number * multiplier) + else + significand = math.floor(number * multiplier + 0.5) + end + return significand / multiplier; +end + +local function addGroupDelimiters(numberStr, delimiter) + local formatted = numberStr + local delimiterSubStr = string.format("%%1%s%%2", delimiter) + while true do + local lFormatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", delimiterSubStr) + formatted = lFormatted + if k == 0 then + break + end + end + return formatted +end + +local function findDenominationEntry(localeInfo, number, roundingBehaviour) + local denominationEntry = localeInfo[1] -- Default to base denominations + local absOfNumber = math.abs(number) + for i = #localeInfo, 2, -1 do + local entry = localeInfo[i] + local baseValue + if roundingBehaviour == RoundingBehaviour.Truncate then + baseValue = entry[1] + else + baseValue = entry[1] - (localeInfo[i - 1][1]) / 2 + end + if baseValue <= absOfNumber then + denominationEntry = entry + break + end + end + return denominationEntry +end + +local NumberLocalization = { } + +function NumberLocalization.localize(number, locale) + if number == 0 then + return "0" + end + + local localeInfo = localeInfos[locale] + if not localeInfo then + localeInfo = localeInfos[DEFAULT_LOCALE] + Logging.warn(string.format("Warning: Locale not found: '%s', reverting to '%s' instead.", + tostring(locale), DEFAULT_LOCALE)) + end + + if localeInfo.groupDelimiter then + return addGroupDelimiters(number, localeInfo.groupDelimiter) + end + + return number +end + +function NumberLocalization.abbreviate(number, locale, roundingBehaviour) + if number == 0 then + return "0" + end + + if roundingBehaviour == nil then + roundingBehaviour = RoundingBehaviour.RoundToClosest + end + + local localeInfo = localeInfos[locale] + if not localeInfo then + localeInfo = localeInfos[DEFAULT_LOCALE] + Logging.warn(string.format("Warning: Locale not found: '%s', reverting to '%s' instead.", + tostring(locale), DEFAULT_LOCALE)) + end + + -- select which denomination we are going to use + local denominationEntry = findDenominationEntry(localeInfo, number, roundingBehaviour) + local baseValue = denominationEntry[1] + local symbol = denominationEntry[2] + + -- Round to required significant digits + local significantQuotient = roundToSignificantDigits(number / baseValue, 3, roundingBehaviour) + + -- trim to 1 decimal point + local trimmedQuotient + if roundingBehaviour == RoundingBehaviour.Truncate then + trimmedQuotient = math.modf(significantQuotient * 10) / 10 + else + trimmedQuotient = math.floor(significantQuotient * 10 + 0.5) / 10 + end + local trimmedQuotientString = tostring(trimmedQuotient) + + -- Split the string into integer and fraction parts + local decimalPointIndex = findDecimalPointIndex(trimmedQuotientString) + local integerPart = string.sub(trimmedQuotientString, 1, decimalPointIndex - 1) + local fractionPart = string.sub(trimmedQuotientString, decimalPointIndex + 1, #trimmedQuotientString) + + -- Add group delimiters to integer part + if localeInfo.groupDelimiter then + integerPart = addGroupDelimiters(integerPart, localeInfo.groupDelimiter) + end + + if #fractionPart > 0 then + return integerPart .. localeInfo.decimalSeparator .. fractionPart .. symbol + else + return integerPart .. symbol + end +end + +return NumberLocalization diff --git a/Client2021/ExtraContent/LuaPackages/Localization/NumberLocalization.spec.lua b/Client2021/ExtraContent/LuaPackages/Localization/NumberLocalization.spec.lua new file mode 100644 index 0000000..fbc40ac --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Localization/NumberLocalization.spec.lua @@ -0,0 +1,110 @@ +return function() + local CorePackages = game:GetService("CorePackages") + local Logging = require(CorePackages.Logging) + local NumberLocalization = require(CorePackages.Localization.NumberLocalization) + + local RoundingBehaviour = require(script.Parent.RoundingBehaviour) + + local function checkLocale(locale, responseMapping) + for input, output in pairs(responseMapping) do + expect(NumberLocalization.localize(input, locale)).to.equal(output) + end + end + + local function checkValid_en_zh(locale) + checkLocale(locale, { + [0] = "0", + [1] = "1", + [25] = "25", + [364] = "364", + [4120] = "4,120", + [57860] = "57,860", + [624390] = "624,390", + [7857000] = "7,857,000", + [-12345678] = "-12,345,678", + [23987.45678] = "23,987.45678", + [-12.3456] = "-12.3456", + [-23987.45678] = "-23,987.45678", + }) + end + + describe("NumberLocalization.localize", function() + it("should default to en-us when locale is not recognized", function() + local logs = Logging.capture(function() + checkValid_en_zh("bad_locale") + end) + expect(string.match(logs.warnings[1], "^Warning: Locale not found:") ~= nil).to.equal(true) + end) + + it("should default to en-us when locale is nil", function() + local logs = Logging.capture(function() + checkValid_en_zh(nil) + end) + expect(string.match(logs.warnings[1], "^Warning: Locale not found:") ~= nil).to.equal(true) + end) + + it("should default to en-us when locale is empty", function() + local logs = Logging.capture(function() + checkValid_en_zh("") + end) + expect(string.match(logs.warnings[1], "^Warning: Locale not found:") ~= nil).to.equal(true) + end) + + it("should localize correctly. (en-us)", function() + checkValid_en_zh("en-us") + end) + + it("should localize correctly. (en-gb)", function() + checkValid_en_zh("en-gb") + end) + + it("should localize correctly. (zh-cn)", function() + checkValid_en_zh("zh-cn") + end) + + it("should localize correctly. (zh-tw)", function() + checkValid_en_zh("zh-tw") + end) + end) + + describe("NumberLocalization.abbreviate", function() + it("should round towards zero when using RoundingBehaviour.Truncate", function() + local roundToZeroMap = { + [0] = "0", + [1] = "1", + [25] = "25", + [364] = "364", + [4120] = "4.1K", + [57860] = "57.8K", + [624390] = "624K", + [999999] = "999K", + [7857000] = "7.8M", + [8e7] = "80M", + [9e8] = "900M", + [1e9] = "1B", + [1e12] = "1,000B", + [-0] = "0", + [-1] = "-1", + [-25] = "-25", + [-364] = "-364", + [-4120] = "-4.1K", + [-57860] = "-57.8K", + [-624390] = "-624K", + [-999999] = "-999K", + [-7857000] = "-7.8M", + [-8e7] = "-80M", + [-9e8] = "-900M", + [-1e9] = "-1B", + [-1e12] = "-1,000B", + [1.1] = "1.1", + [1499.99] = "1.4K", + [-1.1] = "-1.1", + [-1499.99] = "-1.4K", + } + + for input, output in pairs(roundToZeroMap) do + expect(NumberLocalization.abbreviate(input, "en-us", RoundingBehaviour.Truncate)).to.equal(output) + end + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Localization/RoundingBehaviour.lua b/Client2021/ExtraContent/LuaPackages/Localization/RoundingBehaviour.lua new file mode 100644 index 0000000..edcab47 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Localization/RoundingBehaviour.lua @@ -0,0 +1,7 @@ +local CorePackages = game:GetService("CorePackages") +local enumerate = require(CorePackages.enumerate) + +return enumerate("RoundingBehaviour", { + "RoundToClosest", + "Truncate", +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Localization/withLocalization.lua b/Client2021/ExtraContent/LuaPackages/Localization/withLocalization.lua new file mode 100644 index 0000000..4d5129c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Localization/withLocalization.lua @@ -0,0 +1,19 @@ +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) +local ArgCheck = require(CorePackages.ArgCheck) +local LocalizationConsumer = require(CorePackages.Localization.LocalizationConsumer) + +local function withLocalization(stringsToBeLocalized) + ArgCheck.isType(stringsToBeLocalized, "table", "stringsToBeLocalized passed to withLocalization()") + + return function(render) + ArgCheck.isType(render, "function", "render passed to withLocalization()") + + return Roact.createElement(LocalizationConsumer, { + render = render, + stringsToBeLocalized = stringsToBeLocalized, + }) + end +end + +return withLocalization \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Localization/withLocalization.spec.lua b/Client2021/ExtraContent/LuaPackages/Localization/withLocalization.spec.lua new file mode 100644 index 0000000..9a3dca2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Localization/withLocalization.spec.lua @@ -0,0 +1,4 @@ +return function() + itSKIP("SOC-6353 - These unit tests need to be moved from LuaApp", function() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Logging.lua b/Client2021/ExtraContent/LuaPackages/Logging.lua new file mode 100644 index 0000000..8826196 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Logging.lua @@ -0,0 +1,54 @@ +-- This is taken from Roact internal implementation details as a temporary helper +-- so that we can control warnings spew and also validate warning logs in unit tests. +-- We should clean it up and turn it into a nice central logging facility later on. + +local outputEnabled = true +local collectors = {} + +local function createLogInfo() + local logInfo = { + warnings = {}, + } + + setmetatable(logInfo, { + __tostring = function(self) + return ("LogInfo\n\tWarnings (%d):\n\t\t%s"):format( + #self.warnings, + table.concat(self.warnings, "\n\t\t") + ) + end, + }) + + return logInfo +end + +local Logging = {} + +function Logging.capture(callback) + local collector = createLogInfo() + + local wasOutputEnabled = outputEnabled + outputEnabled = false + collectors[collector] = true + + local success, result = pcall(callback) + + collectors[collector] = nil + outputEnabled = wasOutputEnabled + + assert(success, result) + + return collector +end + +function Logging.warn(message) + for collector in pairs(collectors) do + table.insert(collector.warnings, message) + end + + if outputEnabled then + warn(message) + end +end + +return Logging diff --git a/Client2021/ExtraContent/LuaPackages/LuaChatDeps.lua b/Client2021/ExtraContent/LuaPackages/LuaChatDeps.lua new file mode 100644 index 0000000..1647813 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/LuaChatDeps.lua @@ -0,0 +1,12 @@ + +--[[ + Proxy package for dependencies for LuaChat. +]] + +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.LuaChatDeps) diff --git a/Client2021/ExtraContent/LuaPackages/LuaDiscussionsDeps.lua b/Client2021/ExtraContent/LuaPackages/LuaDiscussionsDeps.lua new file mode 100644 index 0000000..0f0e860 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/LuaDiscussionsDeps.lua @@ -0,0 +1,12 @@ + +--[[ + Proxy package for dependencies for LuaDiscussions. +]] + +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.LuaDiscussionsDeps) diff --git a/Client2021/ExtraContent/LuaPackages/LuaSocialLibrariesDeps.lua b/Client2021/ExtraContent/LuaPackages/LuaSocialLibrariesDeps.lua new file mode 100644 index 0000000..d55def6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/LuaSocialLibrariesDeps.lua @@ -0,0 +1,12 @@ + +--[[ + Proxy package for dependencies for SocialLibraries. +]] + +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.LuaSocialLibrariesDeps) diff --git a/Client2021/ExtraContent/LuaPackages/Lumberyak.lua b/Client2021/ExtraContent/LuaPackages/Lumberyak.lua new file mode 100644 index 0000000..4dbd1d0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Lumberyak.lua @@ -0,0 +1,6 @@ +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Lumberyak) diff --git a/Client2021/ExtraContent/LuaPackages/Otter.lua b/Client2021/ExtraContent/LuaPackages/Otter.lua new file mode 100644 index 0000000..a20200b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Otter.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Otter) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/OtterApp.lua b/Client2021/ExtraContent/LuaPackages/OtterApp.lua new file mode 100644 index 0000000..4c60583 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/OtterApp.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.OtterApp) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/.robloxrc b/Client2021/ExtraContent/LuaPackages/Packages/.robloxrc new file mode 100644 index 0000000..6b52e84 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "nocheck" + }, + "lint": { + "*": "disabled" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/AvatarExperienceDeps.lua b/Client2021/ExtraContent/LuaPackages/Packages/AvatarExperienceDeps.lua new file mode 100644 index 0000000..6d11e11 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/AvatarExperienceDeps.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["AvatarExperienceDeps"]["AvatarExperienceDeps"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/Cryo.lua new file mode 100644 index 0000000..1f2cc8e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/Dev/DeveloperTools.lua b/Client2021/ExtraContent/LuaPackages/Packages/Dev/DeveloperTools.lua new file mode 100644 index 0000000..fda19d2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/Dev/DeveloperTools.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent._Index + +local package = PackageIndex["roblox_developer-tools"]["developer-tools"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/Dev/Rhodium.lua b/Client2021/ExtraContent/LuaPackages/Packages/Dev/Rhodium.lua new file mode 100644 index 0000000..6a51e47 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/Dev/Rhodium.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent._Index + +local package = PackageIndex["roblox_rhodium"]["rhodium"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/Dev/TestEZ.lua b/Client2021/ExtraContent/LuaPackages/Packages/Dev/TestEZ.lua new file mode 100644 index 0000000..edcea88 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/Dev/TestEZ.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent._Index + +local package = PackageIndex["roblox_testez"]["testez"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/InGameMenuDependencies.lua b/Client2021/ExtraContent/LuaPackages/Packages/InGameMenuDependencies.lua new file mode 100644 index 0000000..f218b9f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/InGameMenuDependencies.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["InGameMenuDependencies"]["InGameMenuDependencies"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/LuaChatDeps.lua b/Client2021/ExtraContent/LuaPackages/Packages/LuaChatDeps.lua new file mode 100644 index 0000000..6267110 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/LuaChatDeps.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["LuaChatDeps"]["LuaChatDeps"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/LuaDiscussionsDeps.lua b/Client2021/ExtraContent/LuaPackages/Packages/LuaDiscussionsDeps.lua new file mode 100644 index 0000000..7a47509 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/LuaDiscussionsDeps.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["LuaDiscussionsDeps"]["LuaDiscussionsDeps"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/LuaSocialLibrariesDeps.lua b/Client2021/ExtraContent/LuaPackages/Packages/LuaSocialLibrariesDeps.lua new file mode 100644 index 0000000..e2deffb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/LuaSocialLibrariesDeps.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["LuaSocialLibrariesDeps"]["LuaSocialLibrariesDeps"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/Lumberyak.lua b/Client2021/ExtraContent/LuaPackages/Packages/Lumberyak.lua new file mode 100644 index 0000000..40adebd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/Lumberyak.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_lumberyak-c9fb3068-76fee23f"]["lumberyak"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/Otter.lua b/Client2021/ExtraContent/LuaPackages/Packages/Otter.lua new file mode 100644 index 0000000..e06526e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/PolicyProvider.lua b/Client2021/ExtraContent/LuaPackages/Packages/PolicyProvider.lua new file mode 100644 index 0000000..bd36e09 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/PolicyProvider.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_lua-roact-policy-provider"]["lua-roact-policy-provider"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/PremiumUpsellDeps.lua b/Client2021/ExtraContent/LuaPackages/Packages/PremiumUpsellDeps.lua new file mode 100644 index 0000000..a146a68 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/PremiumUpsellDeps.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["PremiumUpsellDeps"]["PremiumUpsellDeps"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/Promise.lua b/Client2021/ExtraContent/LuaPackages/Packages/Promise.lua new file mode 100644 index 0000000..b4ec544 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/Promise.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["lua-promise"]["lua-promise"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/PurchasePrompt.lua b/Client2021/ExtraContent/LuaPackages/Packages/PurchasePrompt.lua new file mode 100644 index 0000000..de7dba5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/PurchasePrompt.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_purchase-prompt"]["purchase-prompt"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/PurchasePromptDeps.lua b/Client2021/ExtraContent/LuaPackages/Packages/PurchasePromptDeps.lua new file mode 100644 index 0000000..b3531e7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/PurchasePromptDeps.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_purchase-prompt-deps"]["purchase-prompt-deps"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/Result.lua b/Client2021/ExtraContent/LuaPackages/Packages/Result.lua new file mode 100644 index 0000000..b97b708 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/Result.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_lua-result"]["lua-result"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/Roact.lua new file mode 100644 index 0000000..3edd80d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/RoactGamepad.lua b/Client2021/ExtraContent/LuaPackages/Packages/RoactGamepad.lua new file mode 100644 index 0000000..91d3905 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/RoactGamepad.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_roact-gamepad"]["roact-gamepad"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/RoactNavigation.lua b/Client2021/ExtraContent/LuaPackages/Packages/RoactNavigation.lua new file mode 100644 index 0000000..98c9312 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/RoactNavigation.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_roact-navigation"]["roact-navigation"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/RoactRodux.lua b/Client2021/ExtraContent/LuaPackages/Packages/RoactRodux.lua new file mode 100644 index 0000000..be5aab0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/RoactRodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_roact-rodux"]["roact-rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/Rodux.lua b/Client2021/ExtraContent/LuaPackages/Packages/Rodux.lua new file mode 100644 index 0000000..8917670 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/Rodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_rodux"]["rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/StringUtilities.lua b/Client2021/ExtraContent/LuaPackages/Packages/StringUtilities.lua new file mode 100644 index 0000000..4616eca --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/StringUtilities.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_string-utilities"]["string-utilities"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/UIBlox.lua b/Client2021/ExtraContent/LuaPackages/Packages/UIBlox.lua new file mode 100644 index 0000000..3243f95 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/UIBlox.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["UIBlox"]["UIBlox"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/UrlBuilder.lua b/Client2021/ExtraContent/LuaPackages/Packages/UrlBuilder.lua new file mode 100644 index 0000000..15958c3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/UrlBuilder.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_url-builder"]["url-builder"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/AvatarExperienceDeps/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/AvatarExperienceDeps/init.lua new file mode 100644 index 0000000..9055f30 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/AvatarExperienceDeps/init.lua @@ -0,0 +1,5 @@ +local AvatarExperineceDeps = script.Parent + +return { + RoactFitComponents = require(AvatarExperineceDeps.RoactFitComponents), +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/RoactFitComponents.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/RoactFitComponents.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/RoactFitComponents.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/lock.toml new file mode 100644 index 0000000..a71c2c3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/AvatarExperienceDeps/lock.toml @@ -0,0 +1,6 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "AvatarExperienceDeps" +version = "0.0.1" +commit = "5a31b41867d2c5ebf0b5670d9be27f3edd403545" +source = "git+https://github.com/roblox/avatar-experience-deps#master" +dependencies = ["RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components"] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/InGameMenuDependencies/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/InGameMenuDependencies/init.lua new file mode 100644 index 0000000..19038db --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/InGameMenuDependencies/init.lua @@ -0,0 +1,12 @@ +local InGameMenuDependencies = script.Parent + +return { + Roact = require(InGameMenuDependencies.Roact), + Rodux = require(InGameMenuDependencies.Rodux), + RoactRodux = require(InGameMenuDependencies.RoactRodux), + UIBlox = require(InGameMenuDependencies.UIBlox), + Otter = require(InGameMenuDependencies.Otter), + Cryo = require(InGameMenuDependencies.Cryo), + t = require(InGameMenuDependencies.t), + PolicyProvider = require(InGameMenuDependencies.PolicyProvider), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Otter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/PolicyProvider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/PolicyProvider.lua new file mode 100644 index 0000000..5924c43 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/PolicyProvider.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_lua-roact-policy-provider"]["lua-roact-policy-provider"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/RoactRodux.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/RoactRodux.lua new file mode 100644 index 0000000..1b8d1c2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/RoactRodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-rodux"]["roact-rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Rodux.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Rodux.lua new file mode 100644 index 0000000..96b67df --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/Rodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_rodux"]["rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/UIBlox.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/UIBlox.lua new file mode 100644 index 0000000..2e0dcc2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/UIBlox.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["UIBlox"]["UIBlox"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/lock.toml new file mode 100644 index 0000000..969e7ec --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/lock.toml @@ -0,0 +1,15 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "InGameMenuDependencies" +version = "0.1.0" +commit = "aecc56c1fa6348886a1073785dbb196b655a0a90" +source = "git+https://github.com/roblox/in-game-menu-dependencies#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "PolicyProvider roblox/lua-roact-policy-provider 5f782af8 git+https://github.com/roblox/lua-roact-policy-provider#master", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "RoactRodux roblox/roact-rodux 0.2.2 url+https://github.com/roblox/roact-rodux", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "UIBlox UIBlox 3a82d9ed git+https://github.com/roblox/uiblox#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/t.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/InGameMenuDependencies/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/AssetCard.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/AssetCard.lua new file mode 100644 index 0000000..39310a8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/AssetCard.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_asset-card"]["asset-card"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/InfiniteScroller.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/InfiniteScroller.lua new file mode 100644 index 0000000..b534d1d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/InfiniteScroller.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_infinite-scroller-98304e77-0.7.3"]["infinite-scroller"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/InfiniteScroller71.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/InfiniteScroller71.lua new file mode 100644 index 0000000..35fe930 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/InfiniteScroller71.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_infinite-scroller-3aafd7fe-7286a922"]["infinite-scroller"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/LuaChatDeps/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/LuaChatDeps/init.lua new file mode 100644 index 0000000..a8223f0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/LuaChatDeps/init.lua @@ -0,0 +1,9 @@ +local LuaChatDeps = script.Parent + +return { + InfiniteScroll = require(LuaChatDeps.InfiniteScroller), + InfiniteScroll71 = require(LuaChatDeps.InfiniteScroller71), + RoduxNetworking = require(LuaChatDeps.RoduxNetworking), + UIBlox = require(LuaChatDeps.UIBlox), + AssetCard = require(LuaChatDeps.AssetCard), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/RoduxNetworking.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/RoduxNetworking.lua new file mode 100644 index 0000000..4523682 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/RoduxNetworking.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["rodux-networking"]["rodux-networking"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/UIBlox.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/UIBlox.lua new file mode 100644 index 0000000..2e0dcc2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/UIBlox.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["UIBlox"]["UIBlox"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/lock.toml new file mode 100644 index 0000000..2a16b25 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaChatDeps/lock.toml @@ -0,0 +1,12 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "LuaChatDeps" +version = "0.1.3" +commit = "f16c004618db2eacb01bf85c73946d0711bfe922" +source = "git+https://github.com/roblox/lua-chat-deps#master" +dependencies = [ + "AssetCard roblox/asset-card 103b2a57 git+https://github.com/roblox/asset-card#v1.0.3", + "InfiniteScroller roblox/infinite-scroller 0.7.3 url+https://github.com/roblox/infinite-scroller", + "InfiniteScroller71 roblox/infinite-scroller 7286a922 git+https://github.com/roblox/infinite-scroller#v0.7.1", + "RoduxNetworking rodux-networking 8902a6aa git+https://github.com/roblox/rodux-networking#v1.0.2", + "UIBlox UIBlox 3a82d9ed git+https://github.com/roblox/uiblox#master", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/InfiniteScroll.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/InfiniteScroll.lua new file mode 100644 index 0000000..efa0054 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/InfiniteScroll.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_infinite-scroller-98304e77-0.3.4"]["infinite-scroller"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/LuaDiscussionsDeps/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/LuaDiscussionsDeps/init.lua new file mode 100644 index 0000000..6313dbe --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/LuaDiscussionsDeps/init.lua @@ -0,0 +1,8 @@ +local LuaDiscussionsDeps = script.Parent + +return { + InfiniteScroll = require(LuaDiscussionsDeps.InfiniteScroll), + RoactFitComponents = require(LuaDiscussionsDeps.RoactFitComponents), + RoactNavigation = require(LuaDiscussionsDeps.RoactNavigation), + RoduxNetworking = require(LuaDiscussionsDeps.RoduxNetworking), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoactFitComponents.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoactFitComponents.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoactFitComponents.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoactNavigation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoactNavigation.lua new file mode 100644 index 0000000..8698791 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoactNavigation.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roact-navigation"]["roact-navigation"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoduxNetworking.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoduxNetworking.lua new file mode 100644 index 0000000..4523682 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/RoduxNetworking.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["rodux-networking"]["rodux-networking"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/lock.toml new file mode 100644 index 0000000..f6fd127 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaDiscussionsDeps/lock.toml @@ -0,0 +1,11 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "LuaDiscussionsDeps" +version = "0.1.2" +commit = "149f4695b874c2e43760131eda24779669bb3bec" +source = "git+https://github.com/roblox/lua-discussions-deps#master" +dependencies = [ + "InfiniteScroll roblox/infinite-scroller 0.3.4 url+https://github.com/roblox/infinite-scroller", + "RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "RoactNavigation roact-navigation 0.1.1-linter-fix url+https://github.com/roblox/roact-navigation", + "RoduxNetworking rodux-networking 8902a6aa git+https://github.com/roblox/rodux-networking#v1.0.2", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/GenericPagination.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/GenericPagination.lua new file mode 100644 index 0000000..f367c51 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/GenericPagination.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_genericpagination"]["genericpagination"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/LuaSocialLibrariesDeps/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/LuaSocialLibrariesDeps/init.lua new file mode 100644 index 0000000..ddfdbd7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/LuaSocialLibrariesDeps/init.lua @@ -0,0 +1,7 @@ +local LuaSocialLibrariesDeps = script.Parent + +return { + GenericPagination = require(LuaSocialLibrariesDeps.GenericPagination), + RoactFitComponents = require(LuaSocialLibrariesDeps.RoactFitComponents), + Mock = require(LuaSocialLibrariesDeps.Mock), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/Mock.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/Mock.lua new file mode 100644 index 0000000..428f95d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/Mock.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["jtaylor_mock"]["mock"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/RoactFitComponents.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/RoactFitComponents.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/RoactFitComponents.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/lock.toml new file mode 100644 index 0000000..5e1b635 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/LuaSocialLibrariesDeps/lock.toml @@ -0,0 +1,10 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "LuaSocialLibrariesDeps" +version = "0.1.1" +commit = "b04c58051c6067aaceb41d062c8eb1ddffeaba0a" +source = "git+https://github.com/roblox/lua-social-libraries-deps#master" +dependencies = [ + "GenericPagination roblox/genericpagination 885168d0 git+https://github.com/roblox/genericpagination#master", + "Mock jtaylor/mock d2c4005c git+https://github.com/roblox/mock#master", + "RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/PremiumUpsellDeps/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/PremiumUpsellDeps/init.lua new file mode 100644 index 0000000..9311ba2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/PremiumUpsellDeps/init.lua @@ -0,0 +1,5 @@ +local PremiumUpsellDeps = script.Parent + +return { + RoactFitComponents = require(PremiumUpsellDeps.RoactFitComponents), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/RoactFitComponents.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/RoactFitComponents.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/RoactFitComponents.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/lock.toml new file mode 100644 index 0000000..9d244da --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/PremiumUpsellDeps/lock.toml @@ -0,0 +1,6 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "PremiumUpsellDeps" +version = "0.0.0" +commit = "c51030b70236f2e9d69bb34348b7e045c2c437f6" +source = "git+https://github.com/roblox/premium-upsell-deps#master" +dependencies = ["RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components"] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/FitFrame.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/FitFrame.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/FitFrame.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/InfiniteScroller.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/InfiniteScroller.lua new file mode 100644 index 0000000..6ea364a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/InfiniteScroller.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_infinite-scroller-98304e77-0.5.6"]["infinite-scroller"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Otter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/RoactGamepad.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/RoactGamepad.lua new file mode 100644 index 0000000..2696d48 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/RoactGamepad.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-gamepad"]["roact-gamepad"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Accordion/AccordionView.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Accordion/AccordionView.lua new file mode 100644 index 0000000..bf068fa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Accordion/AccordionView.lua @@ -0,0 +1,314 @@ +local AccordionRoot = script.Parent +local AppRoot = AccordionRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local SpringAnimatedItem = require(UIBloxRoot.Utility.SpringAnimatedItem) + +local ITEM_PADDING = 10 +local ITEM_WIDTH_SHRINK_STEP = 20 -- How much each item shrinks below card above it +local COMPACT_VIEW_PLACEHOLDER_HEIGHT = 10 +local PRESSED_SCALE = 0.9 + +local ANIMATION_SPRING_SETTINGS = { + dampingRatio = 1, + frequency = 3.5, +} + +local AccordionView = Roact.PureComponent:extend("AccordionView") + +AccordionView.defaultProps = { + maxItemsInCompactView = 3, +} + +local validateProps = t.strictInterface({ + items = t.table, + itemWidth = t.number, + itemHeight = t.number, + renderItem = t.callback, + + placeholderColor = t.Color3, + placeholderBaseTransparency = t.number, + + collapseButtonSize = t.number, + renderCollapseButton = t.callback, + + LayoutOrder = t.optional(t.integer), + maxItemsInCompactView = t.numberPositive, +}) + +function AccordionView:init() + self.state = { + expanded = false, + isExpandButtonPressed = false, + } + + self.onExpandButtonActivated = function() + self:setState({ + expanded = true, + isExpandButtonPressed = false, + }) + end + + self.onCollapseButtonActivated = function() + self:setState({ + expanded = false, + }) + end + + self.onExpandButtonInputBegan = function(_, inputObject) + if inputObject.UserInputState == Enum.UserInputState.Begin and + (inputObject.UserInputType == Enum.UserInputType.Touch or + inputObject.UserInputType == Enum.UserInputType.MouseButton1) then + self:setState({ + isExpandButtonPressed = true, + }) + end + end + + self.onExpandButtonInputEnded = function() + if self.state.isExpandButtonPressed then + self:setState({ + isExpandButtonPressed = false, + }) + end + end + + self.rootFrameRef = Roact.createRef() + + self.onListLayoutAbsoluteContentSizeChanged = function(rbx) + if self.rootFrameRef.current then + local itemWidth = self.props.itemWidth + local minimumHeight = self:getCompactTotalHeight() + + self.rootFrameRef.current.Size = UDim2.new(0, itemWidth, + 0, math.max(rbx.AbsoluteContentSize.Y, minimumHeight)) + end + end +end + +function AccordionView:getCompactTotalHeight() + local items = self.props.items + local itemHeight = self.props.itemHeight + local maxItemsInCompactView = self.props.maxItemsInCompactView + local totalNumberOfItems = #items + + if totalNumberOfItems == 0 then + return 0 + else + return itemHeight + (math.min(maxItemsInCompactView, totalNumberOfItems) - 1) * COMPACT_VIEW_PLACEHOLDER_HEIGHT + end +end + +function AccordionView:getLayoutInfo() + local items = self.props.items + local itemWidth = self.props.itemWidth + local itemHeight = self.props.itemHeight + local placeholderBaseTransparency = self.props.placeholderBaseTransparency + local maxItemsInCompactView = self.props.maxItemsInCompactView + local expanded = self.state.expanded + + local layoutData = {} + local totalNumberOfItems = #items + local itemsShownInCompactView = math.min(maxItemsInCompactView, totalNumberOfItems) + + local placeholderTransparencyStep = 0 + if itemsShownInCompactView > 1 then + placeholderTransparencyStep = (1 - placeholderBaseTransparency) / (itemsShownInCompactView - 1) + end + + for index = 1, totalNumberOfItems do + if expanded then + layoutData[index] = { + width = itemWidth, + height = itemHeight, + placeholderTransparency = 1, + itemTransparency = 0, + } + else + if index == 1 then + layoutData[index] = { + width = itemWidth, + height = itemHeight, + placeholderTransparency = 1, + itemTransparency = 0, + } + elseif index <= maxItemsInCompactView then + layoutData[index] = { + width = itemWidth - ITEM_WIDTH_SHRINK_STEP * (index - 1), + height = COMPACT_VIEW_PLACEHOLDER_HEIGHT, + placeholderTransparency = placeholderBaseTransparency + placeholderTransparencyStep * (index - 2), + itemTransparency = 1, + } + else + layoutData[index] = { + width = itemWidth - ITEM_WIDTH_SHRINK_STEP * (index - 1), + height = 0, + placeholderTransparency = 1, + itemTransparency = 1, + } + end + end + end + + return layoutData +end + +function AccordionView:render() + assert(validateProps(self.props)) + + local items = self.props.items + local totalNumberOfItems = #items + + if totalNumberOfItems == 0 then + return nil + end + + local layoutOrder = self.props.LayoutOrder + local itemWidth = self.props.itemWidth + local renderItem = self.props.renderItem + local placeholderColor = self.props.placeholderColor + local collapseButtonSize = self.props.collapseButtonSize + local renderCollapseButton = self.props.renderCollapseButton + + local expanded = self.state.expanded + local isExpandButtonPressed = self.state.isExpandButtonPressed + + local layoutData = self:getLayoutInfo() + + local accordionContent = { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, expanded and ITEM_PADDING or 0), + [Roact.Change.AbsoluteContentSize] = self.onListLayoutAbsoluteContentSizeChanged, + }), + Scaler = Roact.createElement(SpringAnimatedItem.AnimatedUIScale, { + springOptions = ANIMATION_SPRING_SETTINGS, + animatedValues = { + scale = isExpandButtonPressed and PRESSED_SCALE or 1, + }, + mapValuesToProps = function(values) + return { + Scale = values.scale, + } + end, + }), + CollapseButton = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + springOptions = ANIMATION_SPRING_SETTINGS, + animatedValues = { + -- Increase the size by 1 pixel so the animation looks better + -- when the spring is damping in the end. + sizeOffsetY = expanded and collapseButtonSize + 1 or 0, + }, + mapValuesToProps = function(values) + return { + Size = UDim2.new(0, collapseButtonSize, 0, values.sizeOffsetY), + } + end, + regularProps = { + BackgroundTransparency = 1, + ClipsDescendants = true, + [Roact.Children] = { + ButtonMoveContainer = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + springOptions = ANIMATION_SPRING_SETTINGS, + animatedValues = { + positionOffsetY = expanded and 0 or collapseButtonSize / 2, + }, + mapValuesToProps = function(values) + return { + Position = UDim2.new(0, 0, 0, values.positionOffsetY), + } + end, + regularProps = { + Size = UDim2.new(0, collapseButtonSize, 0, collapseButtonSize), + BackgroundTransparency = 1, + [Roact.Children] = { + Button = renderCollapseButton(self.onCollapseButtonActivated), + }, + }, + }), + }, + }, + }), + } + + for index, _ in ipairs(items) do + local layout = layoutData[index] + + accordionContent["Item" .. tostring(index)] = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + springOptions = ANIMATION_SPRING_SETTINGS, + animatedValues = { + width = layout.width, + height = layout.height, + }, + mapValuesToProps = function(values) + return { + Size = UDim2.new(0, values.width, 0, values.height), + } + end, + regularProps = { + Size = UDim2.new(1, 0, 0, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = index + 1, + ZIndex = totalNumberOfItems + 1 - index; + ClipsDescendants = true, + [Roact.Children] = { + Item = renderItem(items[index], layout.itemTransparency, ANIMATION_SPRING_SETTINGS), + Placeholder = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + springOptions = ANIMATION_SPRING_SETTINGS, + animatedValues = { + transparency = layout.placeholderTransparency, + }, + mapValuesToProps = function(values) + return { + BackgroundTransparency = values.transparency, + } + end, + regularProps = { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = placeholderColor, + BorderSizePixel = 0, + }, + }), + }, + }, + }) + end + + local canExpand = (totalNumberOfItems > 1) + local clickToExpand = canExpand and not expanded + + return Roact.createElement("Frame", { + Size = UDim2.new(0, itemWidth, 0, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + [Roact.Ref] = self.rootFrameRef, + }, { + ContentFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + }, + accordionContent + ), + ClickToExpandButton = clickToExpand and Roact.createElement("TextButton", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ZIndex = totalNumberOfItems + 1, + Text = "", + [Roact.Event.Activated] = self.onExpandButtonActivated, + [Roact.Event.InputBegan] = self.onExpandButtonInputBegan, + [Roact.Event.InputEnded] = self.onExpandButtonInputEnded, + }), + }) +end + +return AccordionView diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Accordion/AccordionView.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Accordion/AccordionView.spec.lua new file mode 100644 index 0000000..2a13ab2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Accordion/AccordionView.spec.lua @@ -0,0 +1,72 @@ +return function() + local AccordionRoot = script.Parent + local AppRoot = AccordionRoot.Parent + local UIBloxRoot = AppRoot.Parent + local Packages = UIBloxRoot.Parent + local AccordionView = require(AccordionRoot.AccordionView) + local Roact = require(Packages.Roact) + + describe("AccordionView", function() + it("should mount correctly", function() + local element = Roact.createElement(AccordionView, { + items = {"test", "test2"}, + itemWidth = 355, + itemHeight = 188, + renderItem = function(item, transparency) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Text = item, + BackgroundTransparency = transparency, + }) + end, + placeholderColor = Color3.fromRGB(255, 255, 255), + placeholderBaseTransparency = 0.5, + collapseButtonSize = 40, + renderCollapseButton = function(activatedCallback) + return Roact.createElement("TextButton", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Text = "close", + AutoButtonColor = false, + [Roact.Event.Activated] = activatedCallback, + }) + end, + }) + + local instance = Roact.mount(element) + + Roact.unmount(instance) + end) + + it("should mount correctly with empty items", function() + local element = Roact.createElement(AccordionView, { + items = {}, + itemWidth = 355, + itemHeight = 188, + renderItem = function(item, transparency) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Text = item, + BackgroundTransparency = transparency, + }) + end, + placeholderColor = Color3.fromRGB(255, 255, 255), + placeholderBaseTransparency = 0.5, + collapseButtonSize = 40, + renderCollapseButton = function(activatedCallback) + return Roact.createElement("TextButton", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Text = "close", + AutoButtonColor = false, + [Roact.Event.Activated] = activatedCallback, + }) + end, + }) + + local instance = Roact.mount(element) + + Roact.unmount(instance) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/FullscreenTitleBar.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/FullscreenTitleBar.lua new file mode 100644 index 0000000..58e4f04 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/FullscreenTitleBar.lua @@ -0,0 +1,221 @@ +local Bar = script.Parent +local App = Bar.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Otter = require(Packages.Otter) +local t = require(Packages.t) + +local IconButton = require(App.Button.IconButton) +local Images = require(App.ImageSet.Images) +local IconSize = require(App.ImageSet.Enum.IconSize) +local getIconSize = require(App.ImageSet.getIconSize) +local ControlState = require(Packages.UIBlox.Core.Control.Enum.ControlState) +local withStyle = require(UIBlox.Core.Style.withStyle) +local ThreeSectionBar = require(UIBlox.Core.Bar.ThreeSectionBar) + +local lerp = require(Packages.UIBlox.Utility.lerp) +local divideTransparency = require(Packages.UIBlox.Utility.divideTransparency) + +local TITLE_BAR_HEIGHT = 64 +local SHADOW_HEIGHT = 24 +local MARGIN = 20 +local PADDING_BETWEEN = 12 + +local TITLE_BAR_OFF_POS = UDim2.new(0, 0, 0, -(TITLE_BAR_HEIGHT + SHADOW_HEIGHT)) +local TITLE_BAR_ON_POS = UDim2.fromOffset(0, 0) + +local EXIT_BUTTON_IMAGE_ID = "icons/actions/previewShrink" +local CLOSE_BUTTON_IMAGE_ID = "icons/navigation/close" + +local GRADIENT_OPACITY = 0.25 + +local MOTOR_OPTIONS = { + frequency = 5, +} + +local FullscreenTitleBar = Roact.PureComponent:extend("FullscreenTitleBar") + +FullscreenTitleBar.validateProps = t.strictInterface({ + title = t.string, + isTriggered = t.optional(t.boolean), + onDisappear = t.optional(t.callback), + + exitFullscreen = t.optional(t.callback), + closeRoblox = t.optional(t.callback), +}) + +function FullscreenTitleBar:init() + local initProgress = self.props.isTriggered and 1 or 0 + local setProgress + self.progress, setProgress = Roact.createBinding(initProgress) + + self.exitControlState, self.setExitControlState = Roact.createBinding(self.state.exitControlState) + self.closeControlState, self.setCloseControlState = Roact.createBinding(self.state.closeControlState) + + self.titleBarPosition = self.progress:map(function(value) + return TITLE_BAR_OFF_POS:lerp(TITLE_BAR_ON_POS, value) + end) + + self.progressMotor = Otter.createSingleMotor(initProgress) + self.progressMotor:onStep(setProgress) +end + +function FullscreenTitleBar:render() + return withStyle(function(style) + local theme = style.Theme + local font = style.Font + + local backgroundStyle = theme.BackgroundUIDefault + local textColorStyle = theme.TextEmphasis + + local centerTextFont = font.Header2 + local centerTextSize = centerTextFont.RelativeSize * font.BaseSize + + local titleBarTransparency = self.progress:map(function(value) + local baseTransparency = backgroundStyle.Transparency + return lerp(1, baseTransparency, value) + end) + + local exitButtonTransparency = Roact.joinBindings({ + progress = self.progress, + controlState = self.exitControlState, + }):map(function(values) + local baseTransparency = theme.ContextualPrimaryDefault.Transparency + local transparencyDivisor = values.controlState == ControlState.Pressed and 2 or 1 + return lerp(1, divideTransparency(baseTransparency, transparencyDivisor), values.progress) + end) + + local closeButtonTransparency = Roact.joinBindings({ + progress = self.progress, + controlState = self.closeControlState, + }):map(function(values) + local baseTransparency = theme.ContextualPrimaryDefault.Transparency + local transparencyDivisor = values.controlState == ControlState.Pressed and 2 or 1 + return lerp(1, divideTransparency(baseTransparency, transparencyDivisor), values.progress) + end) + + local textTransparency = self.progress:map(function(value) + local baseTransparency = textColorStyle.Transparency + return lerp(1, baseTransparency, value) + end) + + local function renderCenterText() + return Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Font = centerTextFont.Font, + Size = UDim2.new(1, 0, 0, centerTextSize), + Text = self.props.title, + TextColor3 = textColorStyle.Color, + TextSize = centerTextSize, + TextTransparency = textTransparency, + TextTruncate = Enum.TextTruncate.AtEnd, + TextWrapped = false, + }) + end + + local function renderRightButtons() + return Roact.createFragment({ + ExitButton = Roact.createElement(IconButton, { + icon = Images[EXIT_BUTTON_IMAGE_ID], + iconSize = IconSize.Medium, + iconTransparency = exitButtonTransparency, + onActivated = self.props.exitFullscreen, + layoutOrder = 1, + onStateChanged = function(oldState, newState) + self.setExitControlState(newState) + end + }), + CloseButton = Roact.createElement(IconButton, { + icon = Images[CLOSE_BUTTON_IMAGE_ID], + iconSize = IconSize.Medium, + iconTransparency = closeButtonTransparency, + onActivated = self.props.closeRoblox, + layoutOrder = 2, + onStateChanged = function(oldState, newState) + self.setCloseControlState(newState) + end + }), + }) + end + + local function renderMirrorButtons() + local iconSize = getIconSize(IconSize.Medium) + return Roact.createFragment({ + Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(iconSize, iconSize), + }), + Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(iconSize, iconSize), + }), + }) + end + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Position = self.titleBarPosition, + Size = UDim2.new(1, 0, 0, TITLE_BAR_HEIGHT + SHADOW_HEIGHT), + }, { + BarFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Position = UDim2.fromOffset(0, 0), + Size = UDim2.new(1, 0, 0, TITLE_BAR_HEIGHT), + [Roact.Event.MouseLeave] = self.props.onDisappear, + }, { + ThreeSectionBar = Roact.createElement(ThreeSectionBar, { + BackgroundColor3 = backgroundStyle.Color, + BackgroundTransparency = titleBarTransparency, + barHeight = TITLE_BAR_HEIGHT, + marginLeft = MARGIN, + contentPaddingLeft = UDim.new(0, PADDING_BETWEEN), + renderLeft = renderMirrorButtons, + renderCenter = renderCenterText, + marginRight = MARGIN, + contentPaddingRight = UDim.new(0, PADDING_BETWEEN), + renderRight = renderRightButtons, + }), + }), + ShadowFrame = Roact.createElement("Frame", { + BackgroundTransparency = backgroundStyle.Transparency, + BackgroundColor3 = backgroundStyle.Color, + BorderSizePixel = 0, + Position = UDim2.fromOffset(0, TITLE_BAR_HEIGHT), + Size = UDim2.new(1, 0, 0, SHADOW_HEIGHT), + }, { + UIGradient = Roact.createElement("UIGradient", { + Rotation = 90, + Color = ColorSequence.new({ + ColorSequenceKeypoint.new(0, Color3.new(0, 0, 0)), + ColorSequenceKeypoint.new(1, Color3.new(0, 0, 0)), + }), + Transparency = NumberSequence.new({ + NumberSequenceKeypoint.new(0, 1 - GRADIENT_OPACITY), + NumberSequenceKeypoint.new(1, 1.0), + }), + }) + }) + }) + end) +end + +function FullscreenTitleBar:didMount() + self.progressMotor:start() +end + +function FullscreenTitleBar:didUpdate(prevProps, prevState) + if prevProps.isTriggered ~= self.props.isTriggered then + local newProgress = self.props.isTriggered and 1 or 0 + self.progressMotor:setGoal(Otter.spring(newProgress, MOTOR_OPTIONS)) + end +end + +function FullscreenTitleBar:willUnmount() + self.progressMotor:destroy() +end + +return FullscreenTitleBar diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/FullscreenTitleBar.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/FullscreenTitleBar.spec.lua new file mode 100644 index 0000000..c78526d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/FullscreenTitleBar.spec.lua @@ -0,0 +1,33 @@ +return function() + local Packages = script.Parent.Parent.Parent.Parent + local Roact = require(Packages.Roact) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + + local FullscreenTitleBar = require(script.Parent.FullscreenTitleBar) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + TestTitleBar = Roact.createElement(FullscreenTitleBar, { + title = "", + }) + }) + local instance = Roact.mount(element, nil, "FullscreenTitleBar") + Roact.unmount(instance) + end) + + it("should mount and unmount without issue with valid properties", function() + local element = mockStyleComponent({ + TestTitleBar = Roact.createElement(FullscreenTitleBar, { + title = "", + onDisappear = function() end, + isTriggered = false, + exitFullscreen = function() end, + closeRoblox = function() end, + }) + }) + local instance = Roact.mount(element, nil, "FullscreenTitleBar") + Roact.unmount(instance) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/HeaderBar.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/HeaderBar.lua new file mode 100644 index 0000000..03101e1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/HeaderBar.lua @@ -0,0 +1,160 @@ +local Bar = script.Parent +local App = Bar.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(App.ImageSet.Images) +local IconSize = require(App.ImageSet.Enum.IconSize) +local getPageMargin = require(App.Container.getPageMargin) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local IconButton = require(UIBlox.App.Button.IconButton) +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) + +local ThreeSectionBar = require(UIBlox.Core.Bar.ThreeSectionBar) + +local HeaderBar = Roact.PureComponent:extend("HeaderBar") + +HeaderBar.renderLeft = { + backButton = function(onActivated) + return function(_) + return Roact.createElement(IconButton, { + size = UDim2.fromOffset(0, 0), + iconSize = IconSize.Medium, + icon = Images["icons/navigation/pushBack"], + onActivated = onActivated, + }) + end + end, +} + +HeaderBar.validateProps = t.strictInterface({ + -- The title of the screen + title = t.string, + + -- Use the left side root title style + isRootTitle = t.boolean, + + -- How tall the bar is + barHeight = t.optional(t.number), + + -- How much spacing between elements to allow on the right side of the bar + contentPaddingRight = t.optional(t.UDim), + + -- A function that returns a Roact Component, used for customizing buttons on the right side of the bar + renderRight = t.optional(t.callback), + + -- A function that returns a Roact Component, used for customizing, e.g. back button, on the left side of the bar + renderLeft = t.optional(t.callback), + + -- A function that returns a Roact Component, used for containing, e.g. search bar, on the center of the bar + renderCenter = t.optional(t.callback), + + -- Background transparency + backgroundTransparency = t.optional(t.number), + + -- Left side margin + marginLeft = t.optional(t.number), + + -- Right side margin + marginRight = t.optional(t.number), +}) + +-- default values are taken from Abstract +HeaderBar.defaultProps = { + title = "", + isRootTitle = false, + barHeight = 48, + contentPaddingRight = UDim.new(0, 0), +} + +function HeaderBar:init() + self.state = { + margin = 0 + } + + self.onResize = function(rbx) + local margin = getPageMargin(rbx.AbsoluteSize.X) + self:setState({ + margin = margin + }) + end +end + +function HeaderBar:render() + return withStyle(function(style) + local theme = style.Theme + local font = style.Font + + local isRootTitle = self.props.isRootTitle + local renderLeft = self.props.renderLeft + local renderCenter = self.props.renderCenter + local renderRight = self.props.renderRight + local estimatedCenterWidth = math.huge + + if not renderLeft and isRootTitle then + renderLeft = function() + return Roact.createElement(GenericTextLabel, { + fluidSizing = true, + Text = self.props.title, + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Left, + fontStyle = font.Title, + colorStyle = theme.TextEmphasis, + }) + end + end + + -- Make center fixed-width components in the center, e.g search bar + if renderLeft and renderCenter and renderRight then + estimatedCenterWidth = 0 + end + + if not renderCenter and not isRootTitle then + local centerTextFontStyle = font.Header1 + local centerTextSize = centerTextFontStyle.RelativeSize * font.BaseSize + renderCenter = function() + return Roact.createElement(GenericTextLabel, { + ClipsDescendants = true, + Size = UDim2.new(1, 0, 0, centerTextSize), + Text = self.props.title, + TextTruncate = Enum.TextTruncate.AtEnd, + TextWrapped = false, + fontStyle = centerTextFontStyle, + colorStyle = theme.TextEmphasis, + }) + end + estimatedCenterWidth = GetTextSize( + self.props.title, + centerTextSize, + centerTextFontStyle.Font, + Vector2.new(1000, 1000) + ).X + end + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, self.props.barHeight), + [Roact.Change.AbsoluteSize] = + (self.props.marginLeft == nil and self.props.marginRight == nil) and self.onResize or nil, + }, { + ThreeSectionBar = Roact.createElement(ThreeSectionBar, { + BackgroundTransparency = self.props.backgroundTransparency or theme.BackgroundDefault.Transparency, + BackgroundColor3 = theme.BackgroundDefault.Color, + barHeight = self.props.barHeight, + marginLeft = self.props.marginLeft or self.state.margin, + renderLeft = renderLeft, + renderCenter = renderCenter, + estimatedCenterWidth = estimatedCenterWidth, + marginRight = self.props.marginRight or self.state.margin, + contentPaddingRight = self.props.contentPaddingRight, + renderRight = renderRight, + }) + }) + end) +end + +return HeaderBar diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/HeaderBar.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/HeaderBar.spec.lua new file mode 100644 index 0000000..8a9e2a2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/HeaderBar.spec.lua @@ -0,0 +1,134 @@ +return function() + local Bar = script.Parent + local App = Bar.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local HeaderBar = require(UIBlox.App.Bar.HeaderBar) + + local BARSIZE_SMALL = UDim2.new(0, 320, 0, 40) + local BARSIZE_MEDIUM = UDim2.new(0, 480, 0, 40) + local BARSIZE_LARGE = UDim2.new(0, 600, 0, 40) + local MARGIN_SMALL = 12 + local MARGIN_MEDIUM = 24 + local MARGIN_LARGE = 48 + + describe("lifecycle", function() + it("should mount and unmount without issues", function() + local element = mockStyleComponent({ + bar = Roact.createElement(HeaderBar, { + title = "Header Bar", + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) + + describe("renderCenter", function() + it("should mount things correctly", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + barFrame = Roact.createElement("Frame", { + Size = UDim2.new(0, 0, 0, 0), + }, { + bar = Roact.createElement(HeaderBar, { + title = "Header Bar", + renderCenter = function() + return Roact.createElement("TextBox", { + Size = UDim2.fromOffset(200, 36), + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + Text = "Search Box Text", + }) + end, + }), + }) + }) + + local instance = Roact.mount(element, frame, "Frame") + local barFrame = frame:FindFirstChild("barFrame", true) + local bar = barFrame:FindFirstChild("bar") + local centerFrame = bar:FindFirstChild("centerFrame", true) + local centerContent = centerFrame:FindFirstChild("centerContent") + expect(centerContent.Text).to.equal("Search Box Text") + + Roact.unmount(instance) + end) + end) + + describe("margin logic", function() + it("should have correct left margin on different sized screens with renderLeft", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + barFrame = Roact.createElement("Frame", { + Size = UDim2.new(0, 0, 0, 0), + }, { + bar = Roact.createElement(HeaderBar, { + title = "Header Bar", + isRootTitle = true, + }), + }) + }) + + local instance = Roact.mount(element, frame, "Frame") + local barFrame = frame:FindFirstChild("barFrame", true) + local bar = barFrame:FindFirstChild("bar") + local leftFrame = bar:FindFirstChild("leftFrame", true) + local margin = leftFrame:FindFirstChild("$margin", true) + expect(margin).to.be.ok() + + barFrame.Size = BARSIZE_SMALL + local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize] + expect(margin.PaddingLeft.Offset).to.equal(MARGIN_SMALL) + + barFrame.Size = BARSIZE_MEDIUM + local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize] + expect(margin.PaddingLeft.Offset).to.equal(MARGIN_MEDIUM) + + barFrame.Size = BARSIZE_LARGE + local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize] + expect(margin.PaddingLeft.Offset).to.equal(MARGIN_LARGE) + + Roact.unmount(instance) + end) + + it("should have correct left margin on different sized screens without renderLeft", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + barFrame = Roact.createElement("Frame", { + Size = UDim2.new(0, 0, 0, 0), + }, { + bar = Roact.createElement(HeaderBar, { + title = "Header Bar", + isRootTitle = false, + }), + }) + }) + + local instance = Roact.mount(element, frame, "Frame") + local barFrame = frame:FindFirstChild("barFrame", true) + local bar = barFrame:FindFirstChild("bar") + local centerFrame = bar:FindFirstChild("centerFrame", true) + local UIPadding = centerFrame:FindFirstChild("UIPadding", true) + expect(UIPadding).to.be.ok() + + barFrame.Size = BARSIZE_SMALL + local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize] + expect(UIPadding.PaddingLeft.Offset).to.equal(MARGIN_SMALL) + + barFrame.Size = BARSIZE_MEDIUM + local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize] + expect(UIPadding.PaddingLeft.Offset).to.equal(MARGIN_MEDIUM) + + barFrame.Size = BARSIZE_LARGE + local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize] + expect(UIPadding.PaddingLeft.Offset).to.equal(MARGIN_LARGE) + + Roact.unmount(instance) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/RootHeaderBar.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/RootHeaderBar.lua new file mode 100644 index 0000000..6171157 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/RootHeaderBar.lua @@ -0,0 +1,91 @@ +local Bar = script.Parent +local App = Bar.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local getPageMargin = require(App.Container.getPageMargin) + +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) + +local ThreeSectionBar = require(UIBlox.Core.Bar.ThreeSectionBar) +local RootHeaderBar = Roact.PureComponent:extend("HeaderBar") + +RootHeaderBar.validateProps = t.strictInterface({ + -- The title of the screen + title = t.string, + + -- How tall the bar is + barHeight = t.optional(t.number), + + -- A function that returns a Roact Component, used for containing, e.g. search bar, on the center of the bar + renderCenter = t.optional(t.callback), + + -- A function that returns a Roact Component, used for customizing buttons on the right side of the bar + renderRight = t.optional(t.callback), + + backgroundTransparency = t.optional(t.number), +}) + +RootHeaderBar.defaultProps = { + barHeight = 64, + renderRight = function() + return nil + end, +} + +function RootHeaderBar:init() + self:setState({ + margin = 0, + }) + + self.setPageMargin = function(rbx) + local margin = getPageMargin(rbx.AbsoluteSize.X) + self:setState({ + margin = margin + }) + end +end + +function RootHeaderBar:render() + return withStyle(function(style) + local theme = style.Theme + local font = style.Font + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, self.props.barHeight), + [Roact.Change.AbsoluteSize] = self.setPageMargin, + }, { + ThreeSectionBar = Roact.createElement(ThreeSectionBar, { + BackgroundTransparency = self.props.backgroundTransparency or theme.BackgroundDefault.Transparency, + BackgroundColor3 = theme.BackgroundDefault.Color, + + barHeight = self.props.barHeight, + contentPaddingRight = UDim.new(0, 0), + + marginLeft = self.state.margin, + marginRight = self.state.margin, + + renderCenter = self.props.renderCenter, + renderRight = self.props.renderRight, + renderLeft = function(props) + return Roact.createFragment({ + Text = Roact.createElement(GenericTextLabel, { + fluidSizing = true, + Text = self.props.title, + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Left, + fontStyle = font.Title, + colorStyle = theme.TextEmphasis, + }, props[Roact.Children]) + }) + end, + }) + }) + end) +end + +return RootHeaderBar diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/RootHeaderBar.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/RootHeaderBar.spec.lua new file mode 100644 index 0000000..e463811 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/RootHeaderBar.spec.lua @@ -0,0 +1,142 @@ +return function() + local Bar = script.Parent + local App = Bar.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local RootHeaderBar = require(UIBlox.App.Bar.RootHeaderBar) + + local BARSIZE_SMALL = UDim2.new(0, 320, 0, 40) + local BARSIZE_MEDIUM = UDim2.new(0, 480, 0, 40) + local BARSIZE_LARGE = UDim2.new(0, 600, 0, 40) + local MARGIN_SMALL = 12 + local MARGIN_MEDIUM = 24 + local MARGIN_LARGE = 48 + + describe("lifecycle", function() + it("should mount and unmount without issues", function() + local element = mockStyleComponent({ + bar = Roact.createElement(RootHeaderBar, { + title = "Root Header Bar", + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) + + describe("renderCenter", function() + it("should mount things correctly", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + barFrame = Roact.createElement("Frame", { + Size = UDim2.new(0, 0, 0, 0), + }, { + bar = Roact.createElement(RootHeaderBar, { + title = "Root Header Bar", + renderCenter = function() + return Roact.createElement("TextBox", { + Size = UDim2.fromOffset(200, 36), + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + Text = "Search Box Text", + }) + end, + }), + }) + }) + + local instance = Roact.mount(element, frame, "Frame") + local barFrame = frame:FindFirstChild("barFrame", true) + local bar = barFrame:FindFirstChild("bar") + local centerFrame = bar:FindFirstChild("centerFrame", true) + local centerContent = centerFrame:FindFirstChild("centerContent") + expect(centerContent.Text).to.equal("Search Box Text") + + Roact.unmount(instance) + end) + end) + + describe("margin logic", function() + it("should have margin of 12 on small screens", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + barFrame = Roact.createElement("Frame", { + Size = UDim2.new(0, 0, 0, 0), + }, { + bar = Roact.createElement(RootHeaderBar, { + title = "Root Header Bar", + }), + }) + }) + + local instance = Roact.mount(element, frame, "Frame") + local barFrame = frame:FindFirstChild("barFrame", true) + local bar = barFrame:FindFirstChild("bar") + local leftFrame = bar:FindFirstChild("leftFrame", true) + local margin = leftFrame:FindFirstChild("$margin", true) + expect(margin).to.be.ok() + + barFrame.Size = BARSIZE_SMALL + local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize] + expect(margin.PaddingLeft.Offset).to.equal(MARGIN_SMALL) + + Roact.unmount(instance) + end) + + it("should have margin of 24 on medium screens", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + barFrame = Roact.createElement("Frame", { + Size = UDim2.new(0, 0, 0, 0), + }, { + bar = Roact.createElement(RootHeaderBar, { + title = "Root Header Bar", + }), + }) + }) + + local instance = Roact.mount(element, frame, "Frame") + local barFrame = frame:FindFirstChild("barFrame", true) + local bar = barFrame:FindFirstChild("bar") + local leftFrame = bar:FindFirstChild("leftFrame", true) + local margin = leftFrame:FindFirstChild("$margin", true) + expect(margin).to.be.ok() + + barFrame.Size = BARSIZE_MEDIUM + local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize] + expect(margin.PaddingLeft.Offset).to.equal(MARGIN_MEDIUM) + + Roact.unmount(instance) + end) + + it("should have margin of 48 on large screens", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + barFrame = Roact.createElement("Frame", { + Size = UDim2.new(0, 0, 0, 0), + }, { + bar = Roact.createElement(RootHeaderBar, { + title = "Root Header Bar", + }), + }) + }) + + local instance = Roact.mount(element, frame, "Frame") + local barFrame = frame:FindFirstChild("barFrame", true) + local bar = barFrame:FindFirstChild("bar") + local leftFrame = bar:FindFirstChild("leftFrame", true) + local margin = leftFrame:FindFirstChild("$margin", true) + expect(margin).to.be.ok() + + barFrame.Size = BARSIZE_LARGE + local _ = bar.AbsoluteSize -- need to reference AbsoluteSize to trigger [Roact.Change.AbsoluteSize] + expect(margin.PaddingLeft.Offset).to.equal(MARGIN_LARGE) + + Roact.unmount(instance) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/FullscreenTitleBar.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/FullscreenTitleBar.story.lua new file mode 100644 index 0000000..fa96b75 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/FullscreenTitleBar.story.lua @@ -0,0 +1,110 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local Bar = script.Parent.Parent +local App = Bar.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local FullscreenTitleBar = require(script.Parent.Parent.FullscreenTitleBar) + +local DISAPPEAR_DELAY = 0.5 + +local TitleBarStory = Roact.PureComponent:extend("TitleBarStory") + +function TitleBarStory:init() + self:setState({ + isTriggered = false, + }) + + self.triggerTitleBar = function() + print("Mouse entering trigger area") + if not self.state.isTriggered then + self:setState({ + isTriggered = true, + }) + end + end + + self.hideTitleBar = function() + print("Mouse leaving Title Bar area") + if self.state.isTriggered then + delay(DISAPPEAR_DELAY, function() + self:setState({ + isTriggered = false, + }) + end) + end + end + + self.buttonControl = function() + self:setState(function(prevState) + return { + isTriggered = not prevState.isTriggered, + } + end) + end +end + +function TitleBarStory:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + }), + ControlsFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 50), + LayoutOrder = 1, + }, { + TestButton = Roact.createElement("TextButton", { + Text = self.state.isTriggered and "Dismiss" or "Activate", + Size = UDim2.fromOffset(200, 50), + [Roact.Event.Activated] = self.buttonControl, + }), + }), + StoryItem = Roact.createElement(StoryItem, { + size = UDim2.fromScale(1, 1), + title = "FullscreenTitleBar", + subTitle = "App.Bar.FullscreenTitleBar", + layoutOrder = 2, + showDivider = true, + }, { + Demo = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + TriggerArea = Roact.createElement("Frame", { + BackgroundColor3 = Color3.fromRGB(0, 255, 255), + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 1), + [Roact.Event.MouseEnter] = self.triggerTitleBar, + }), + TitleBar = Roact.createElement(FullscreenTitleBar, { + title = "Roblox", + isTriggered = self.state.isTriggered, + onDisappear = self.hideTitleBar, + exitFullscreen = function() + print "Exit Fullscreen" + end, + closeRoblox = function() + print "Close Roblox" + end, + }), + }) + }), + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(TitleBarStory), + }), target, "FullscreenTitleBar") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/HeaderBar.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/HeaderBar.story.lua new file mode 100644 index 0000000..6b2252d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/HeaderBar.story.lua @@ -0,0 +1,264 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local Bar = script.Parent.Parent +local App = Bar.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local Images = require(App.ImageSet.Images) +local IconSize = require(App.ImageSet.Enum.IconSize) +local HeaderBar = require(Bar.HeaderBar) +local IconButton = require(UIBlox.App.Button.IconButton) +local TextButton = require(UIBlox.App.Button.TextButton) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local renderRightIcons = function() + return Roact.createFragment({ + search = Roact.createElement(IconButton, { + iconSize = IconSize.Medium, + icon = Images["icons/common/search"], + onActivated = function() + print("Opening Search!") + end, + layoutOrder = 1, + }), + premium = Roact.createElement(IconButton, { + iconSize = IconSize.Medium, + icon = Images["icons/common/goldrobux"], + onActivated = function() + print("Oooh Shiny!") + end, + layoutOrder = 2, + }), + alert = Roact.createElement(IconButton, { + iconSize = IconSize.Medium, + icon = Images["icons/common/notificationOn"], + onActivated = function() + print("Alert!") + end, + layoutOrder = 3, + }), + }) +end + +local function BarDemo() + return Roact.createElement(HeaderBar, { + title = string.rep("Header Bar Story", 1), + renderLeft = HeaderBar.renderLeft.backButton(function() + print("navProps.navigation.pop()") + end), + renderRight = renderRightIcons, + }) +end + +local function BarWithTextButtonsDemo() + return Roact.createElement(HeaderBar, { + title = string.rep("Header Bar Story", 1), + renderLeft = function() + return Roact.createFragment({ + search = Roact.createElement(TextButton, { + text = "Action 1", + onActivated = function() + print("Opening Search!") + end, + layoutOrder = 1, + }), + }) + end, + renderRight = function() + return Roact.createFragment({ + search = Roact.createElement(TextButton, { + text = "Action 2", + onActivated = function() + print("Opening Search!") + end, + layoutOrder = 1, + }), + }) + end, + }) +end + +local function HeaderBarWithSearchBox() + return Roact.createElement(HeaderBar, { + title = "", + renderLeft = HeaderBar.renderLeft.backButton(function() + print("navProps.navigation.pop()") + end), + renderCenter = function() + return Roact.createFragment({ + searchBoxMock = Roact.createElement(ImageSetComponent.Label, { + Image = Images["component_assets/circle_17_stroke_1"], + SliceCenter = Rect.new(8, 8, 9, 9), + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(1, 0, 0, 36), + BackgroundTransparency = 1, + }), + }) + end, + }) +end + +local function HeaderBarWithOnlySearchBox() + return Roact.createElement(HeaderBar, { + title = "", + renderLeft = nil, + renderCenter = function() + return Roact.createFragment({ + searchBoxMock = Roact.createElement(ImageSetComponent.Label, { + Image = Images["component_assets/circle_17_stroke_1"], + SliceCenter = Rect.new(8, 8, 9, 9), + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(1, 0, 0, 36), + BackgroundTransparency = 1, + }), + }) + end, + }) +end + +local function HeaderBarWithRootTitle() + return Roact.createElement(HeaderBar, { + title = "Avatar", + isRootTitle = true, + renderRight = renderRightIcons, + }) +end + +local function HeaderBarWithRootTitleAndSearchBoxForTablet() + return Roact.createElement(HeaderBar, { + title = "Discover", + isRootTitle = true, + renderCenter = function() + return Roact.createFragment({ + searchBoxMock = Roact.createElement(ImageSetComponent.Label, { + Image = Images["component_assets/circle_17_stroke_1"], + SliceCenter = Rect.new(8, 8, 9, 9), + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(0, 250, 0, 36), + BackgroundTransparency = 1, + }), + }) + end, + renderRight = renderRightIcons, + }) +end + +local function HeaderBarWithBackButtonAndSearchBoxForTablet() + return Roact.createElement(HeaderBar, { + renderLeft = HeaderBar.renderLeft.backButton(function() + print("navProps.navigation.pop()") + end), + renderCenter = function() + return Roact.createFragment({ + searchBoxMock = Roact.createElement(ImageSetComponent.Label, { + Image = Images["component_assets/circle_17_stroke_1"], + SliceCenter = Rect.new(8, 8, 9, 9), + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(0, 250, 0, 36), + BackgroundTransparency = 1, + }), + }) + end, + renderRight = renderRightIcons, + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(StoryItem, { + size = UDim2.fromScale(1, 1), + title = "HeaderBar", + subTitle = "App.Bar.HeaderBar", + }, { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 15), + }), + frame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(700, 45), + LayoutOrder = 1, + }, { + headerBar = Roact.createElement(BarDemo) + }), + frame2 = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(361, 45), + LayoutOrder = 2, + }, { + headerBar = Roact.createElement(BarDemo) + }), + frame3 = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(300, 45), + LayoutOrder = 3, + }, { + headerBar = Roact.createElement(BarDemo) + }), + frame4 = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(700, 45), + LayoutOrder = 4, + }, { + headerBar = Roact.createElement(BarWithTextButtonsDemo) + }), + frame5 = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(361, 45), + LayoutOrder = 5, + }, { + headerBar = Roact.createElement(BarWithTextButtonsDemo) + }), + frame6 = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(300, 45), + LayoutOrder = 6, + }, { + headerBar = Roact.createElement(BarWithTextButtonsDemo) + }), + frameHeaderBarWithSearchBoxForPhone = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(450, 45), + LayoutOrder = 7, + }, { + headerBar = Roact.createElement(HeaderBarWithSearchBox) + }), + frameHeaderBarWithOnlySearchBoxForPhone = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(450, 45), + LayoutOrder = 8, + }, { + headerBar = Roact.createElement(HeaderBarWithOnlySearchBox) + }), + frameHeaderBarWithRootTitleForPhone = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(450, 45), + LayoutOrder = 9, + }, { + headerBar = Roact.createElement(HeaderBarWithRootTitle) + }), + frameHeaderBarWithRootTitleAndSearchBoxForTablet = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(950, 45), + LayoutOrder = 10, + }, { + headerBar = Roact.createElement(HeaderBarWithRootTitleAndSearchBoxForTablet) + }), + frameHeaderBarWithBackButtonAndSearchBoxForTablet = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(950, 45), + LayoutOrder = 11, + }, { + headerBar = Roact.createElement(HeaderBarWithBackButtonAndSearchBoxForTablet) + }), + }), + }), target, "HeaderBar") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/RootHeaderBar.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/RootHeaderBar.story.lua new file mode 100644 index 0000000..15b3b00 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Bar/__stories__/RootHeaderBar.story.lua @@ -0,0 +1,124 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local Bar = script.Parent.Parent +local App = Bar.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local IconSize = require(App.ImageSet.Enum.IconSize) +local Images = require(App.ImageSet.Images) +local RootHeaderBar = require(Bar.RootHeaderBar) +local IconButton = require(App.Button.IconButton) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local function RootHeaderBarWithSearchBox() + return Roact.createElement(RootHeaderBar, { + title = "Hello", + renderCenter = function() + return Roact.createFragment({ + searchBoxMock = Roact.createElement(ImageSetComponent.Label, { + Image = Images["component_assets/circle_17_stroke_1"], + SliceCenter = Rect.new(8, 8, 9, 9), + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.fromOffset(200, 36), + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + }), + }) + end, + renderRight = function() + return Roact.createFragment({ + search = Roact.createElement(IconButton, { + iconSize = IconSize.Medium, + icon = Images["icons/common/search"], + onActivated = function() + print("Opening Search!") + end, + layoutOrder = 1, + }), + premium = Roact.createElement(IconButton, { + iconSize = IconSize.Medium, + icon = Images["icons/common/goldrobux"], + onActivated = function() + print("Oooh Shiny!") + end, + layoutOrder = 2, + }), + alert = Roact.createElement(IconButton, { + iconSize = IconSize.Medium, + icon = Images["icons/common/notificationOn"], + onActivated = function() + print("Alert!") + end, + layoutOrder = 3, + }), + }) + end, + }) +end + +local function RootHeaderBarForPhone() + return Roact.createElement(RootHeaderBar, { + title = "Hello", + renderRight = function() + return Roact.createFragment({ + search = Roact.createElement(IconButton, { + iconSize = IconSize.Medium, + icon = Images["icons/common/search"], + onActivated = function() + print("Opening Search!") + end, + layoutOrder = 1, + }), + premium = Roact.createElement(IconButton, { + iconSize = IconSize.Medium, + icon = Images["icons/common/goldrobux"], + onActivated = function() + print("Oooh Shiny!") + end, + layoutOrder = 2, + }), + alert = Roact.createElement(IconButton, { + iconSize = IconSize.Medium, + icon = Images["icons/common/notificationOn"], + onActivated = function() + print("Alert!") + end, + layoutOrder = 3, + }), + }) + end, + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 1, 0), + title = "RootHeaderBar", + subTitle = "App.Bar.RootHeaderBar", + }, { + layout = Roact.createElement("UIListLayout"), + frame1 = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(800, 45), + }, { + headerBar = Roact.createElement(RootHeaderBarWithSearchBox) + }), + frame2 = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(350, 45), + }, { + headerBar = Roact.createElement(RootHeaderBarForPhone) + }), + }) + }), target, "HeaderBar") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ActionBar.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ActionBar.lua new file mode 100644 index 0000000..487e7cc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ActionBar.lua @@ -0,0 +1,181 @@ +local Button = script.Parent +local App = Button.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local RoactGamepad = require(Packages.RoactGamepad) +local t = require(Packages.t) + +local FitFrame = require(Packages.FitFrame) +local FitFrameOnAxis = FitFrame.FitFrameOnAxis + +local PrimaryContextualButton = require(Button.PrimaryContextualButton) +local PrimarySystemButton = require(Button.PrimarySystemButton) +local IconButton = require(Button.IconButton) +local withStyle = require(UIBlox.Core.Style.withStyle) +local IconSize = require(App.ImageSet.Enum.IconSize) +local getPageMargin = require(App.Container.getPageMargin) +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local validateButtonProps = require(Button.validateButtonProps) +local validateIconButtonProps = IconButton.validateProps + +local ActionBar = Roact.PureComponent:extend("ActionBar") + +local BUTTON_PADDING = 12 +local BUTTON_HEIGHT = 48 +local ICON_SIZE = 36 + +function ActionBar:init() + self.buttonRefs = RoactGamepad.createRefCache() + + self.state = { + frameWidth = 0 + } + + self.updateFrameSize = function(rbx) + local frameWidth = rbx.AbsoluteSize.X + if frameWidth ~= self.state.frameWidth then + self:setState({ + frameWidth = frameWidth, + }) + end + end +end + +ActionBar.validateProps = t.strictInterface({ + -- buttons: A table of button tables that contain props that PrimaryContextualButton allow. + button = t.optional(t.strictInterface({ + props = validateButtonProps, + })), + + -- icons: A table of button tables that contain props that IconButton allow. + icons = t.optional(t.array(t.strictInterface({ + props = validateIconButtonProps + }))), + + -- Children + [Roact.Children] = t.optional(t.table), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) + +function ActionBar:render() + + return withStyle(function(stylePalette) + local margin = getPageMargin(self.state.frameWidth) + local contentWidth = self.state.frameWidth - margin * 2 + local iconSize = IconSize.Medium + + local iconNumber = 0 + if self.props.icons and #self.props.icons then + iconNumber = #self.props.icons + end + + local buttonTable = {} + + if iconNumber ~= 0 then + for iconButtonIndex, iconButton in ipairs(self.props.icons) do + local newProps = { + layoutOrder = iconButtonIndex, + iconSize = iconSize, + } + local iconButtonProps = Cryo.Dictionary.join(newProps, iconButton.props) + + if UIBloxConfig.enableExperimentalGamepadSupport then + local gamepadFrameProps = { + Size = UDim2.fromOffset(ICON_SIZE,ICON_SIZE), + BackgroundTransparency = 1, + [Roact.Ref] = self.buttonRefs[iconButtonIndex], + NextSelectionUp = nil, + NextSelectionDown = nil, + NextSelectionLeft = iconButtonIndex > 1 and self.buttonRefs[iconButtonIndex - 1] or nil, + NextSelectionRight = iconButtonIndex < iconNumber and self.buttonRefs[iconButtonIndex + 1] or nil, + inputBindings = { + [Enum.KeyCode.ButtonA] = iconButtonProps.onActivated, + }, + } + + table.insert(buttonTable, Roact.createElement(RoactGamepad.Focusable.Frame, gamepadFrameProps, { + Roact.createElement(IconButton, iconButtonProps) + })) + else + table.insert(buttonTable, Roact.createElement(IconButton, iconButtonProps)) + end + end + end + + if self.props.button then + local button = self.props.button + + local buttonSize = UDim2.fromOffset(contentWidth - iconNumber * (ICON_SIZE + BUTTON_PADDING), BUTTON_HEIGHT) + + local newProps = { + layoutOrder = iconNumber + 1, + size = buttonSize, + } + local buttonProps = Cryo.Dictionary.join(newProps, button.props) + + if UIBloxConfig.enableExperimentalGamepadSupport then + local gamepadFrameProps = { + Size = buttonSize, + BackgroundTransparency = 1, + [Roact.Ref] = self.buttonRefs[iconNumber + 1], + NextSelectionUp = nil, + NextSelectionDown = nil, + NextSelectionLeft = iconNumber and self.buttonRefs[iconNumber] or nil, + NextSelectionRight = nil, + inputBindings = { + [Enum.KeyCode.ButtonA] = buttonProps.onActivated, + }, + } + + table.insert(buttonTable, Roact.createElement(RoactGamepad.Focusable.Frame, gamepadFrameProps, { + Roact.createElement(iconNumber == 0 and PrimarySystemButton or PrimaryContextualButton, buttonProps) + })) + else + table.insert(buttonTable, + Roact.createElement(iconNumber == 0 and PrimarySystemButton or PrimaryContextualButton, buttonProps)) + end + end + + if self.props[Roact.Children] then + buttonTable = self.props[Roact.Children] + end + + return Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable[FitFrameOnAxis] or FitFrameOnAxis, { + BackgroundTransparency = 1, + minimumSize = UDim2.new(1, 0, 0, BUTTON_HEIGHT), + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + Position = UDim2.new(0, 0, 1, -24), + AnchorPoint = Vector2.new(0, 1), + contentPadding = UDim.new(0, BUTTON_PADDING), + [Roact.Ref] = self.props[Roact.Ref], + [Roact.Change.AbsoluteSize] = self.updateFrameSize, + margin = { + left = margin, + right = margin, + top = 0, + bottom = 0 + }, + + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + }, + buttonTable + ) + end) +end + +return ActionBar diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ActionBar.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ActionBar.spec.lua new file mode 100644 index 0000000..e23bcdd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ActionBar.spec.lua @@ -0,0 +1,111 @@ +return function() + local Button = script.Parent + local App = Button.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local Images = require(App.ImageSet.Images) + + local icon = Images["icons/common/robux_small"] + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local ActionBar = require(Button.ActionBar) + + it("should create and destroy ActionBar with one button without errors", function() + local element = mockStyleComponent({ + ActionBar = Roact.createElement(ActionBar, { + button = { + props = { + onActivated = function() end, + text = "Button", + }, + } + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy ActionBar with one button and one icon button without errors", function() + local element = mockStyleComponent({ + ActionBar = Roact.createElement(ActionBar, { + button = { + props = { + onActivated = function() end, + text = "Button", + icon = icon, + }, + }, + icons = { + { + props = { + anchorPoint = Vector2.new(0.5, 0.5), + position = UDim2.fromScale(0.5, 0.5), + icon = icon, + userInteractionEnabled = true, + onActivated = function() + print("Text Button Clicked!") + end, + } + } + } + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy ActionBar with one button and two icon button without errors", function() + local element = mockStyleComponent({ + ActionBar = Roact.createElement(ActionBar, { + button = { + props = { + onActivated = function() end, + text = "Button", + icon = icon, + }, + }, + icons = { + { + props = { + anchorPoint = Vector2.new(0.5, 0.5), + position = UDim2.fromScale(0.5, 0.5), + icon = icon, + userInteractionEnabled = true, + onActivated = function() + print("Text Button Clicked!") + end, + } + }, + { + props = { + anchorPoint = Vector2.new(0.5, 0.5), + position = UDim2.fromScale(0.5, 0.5), + icon = icon, + userInteractionEnabled = true, + onActivated = function() + print("Text Button Clicked!") + end, + } + } + } + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a ActionBar with children without errors", function() + local element = mockStyleComponent({ + ActionBar = Roact.createElement(ActionBar, {}, { + ChildFrame = Roact.createElement("Frame", {}) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/AlertButton.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/AlertButton.lua new file mode 100644 index 0000000..2abf7f5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/AlertButton.lua @@ -0,0 +1,65 @@ +local Button = script.Parent +local App = Button.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local RoactGamepad = require(Packages.RoactGamepad) + +local Images = require(App.ImageSet.Images) +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) + +local validateButtonProps = require(Button.validateButtonProps) +local GenericButton = require(UIBlox.Core.Button.GenericButton) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local AlertButton = Roact.PureComponent:extend("AlertButton") + +local BUTTON_STATE_COLOR = { + [ControlState.Default] = "Alert", +} + +local CONTENT_STATE_COLOR = { + [ControlState.Default] = "Alert", +} + +AlertButton.defaultProps = { + isDisabled = false, + isLoading = false, +} + +function AlertButton:render() + assert(validateButtonProps(self.props)) + local image = Images["component_assets/circle_17_stroke_1"] + local genericButtonComponent = UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable[GenericButton] or GenericButton + return withSelectionCursorProvider(function(getSelectionCursor) + return Roact.createElement(genericButtonComponent, { + Size = self.props.size, + AnchorPoint = self.props.anchorPoint, + Position = self.props.position, + LayoutOrder = self.props.layoutOrder, + SelectionImageObject = getSelectionCursor(CursorKind.RoundedRectNoInset), + icon = self.props.icon, + text = self.props.text, + isDisabled = self.props.isDisabled, + isLoading = self.props.isLoading, + onActivated = self.props.onActivated, + onStateChanged = self.props.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + buttonImage = image, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + [Roact.Ref] = self.props[Roact.Ref], + }) + end) +end + +return AlertButton \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/AlertButton.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/AlertButton.spec.lua new file mode 100644 index 0000000..c9ba9d6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/AlertButton.spec.lua @@ -0,0 +1,62 @@ +return function() + local Button = script.Parent + local App = Button.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local Images = require(App.ImageSet.Images) + + local icon = Images["icons/common/robux_small"] + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local AlertButton = require(Button.AlertButton) + + it("should create and destroy Alert Button with text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(AlertButton, { + text = "Button", + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Alert Button with text icon only without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(AlertButton, { + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Alert Button with text and text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(AlertButton, { + text = "Button", + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a blank Alert Button without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(AlertButton, { + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ButtonStack.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ButtonStack.lua new file mode 100644 index 0000000..b28e5f0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ButtonStack.lua @@ -0,0 +1,142 @@ +local ButtonRoot = script.Parent +local AppRoot = ButtonRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local RoactGamepad = require(Packages.RoactGamepad) + +local AlertButton = require(ButtonRoot.AlertButton) +local PrimaryContextualButton = require(ButtonRoot.PrimaryContextualButton) +local PrimarySystemButton = require(ButtonRoot.PrimarySystemButton) +local SecondaryButton = require(ButtonRoot.SecondaryButton) +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local FitFrame = require(Packages.FitFrame) +local FitFrameOnAxis = FitFrame.FitFrameOnAxis + +local ButtonType = require(ButtonRoot.Enum.ButtonType) + +local validateButtonStack = require(AppRoot.Button.Validator.validateButtonStack) +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local BUTTON_HEIGHT = 36 + +local ButtonStack = Roact.PureComponent:extend("ButtonStack") + +ButtonStack.defaultProps = { + buttonHeight = BUTTON_HEIGHT, + marginBetween = 12, + minHorizontalButtonPadding = 8, +} + +function ButtonStack:init() + self.buttonRefs = RoactGamepad.createRefCache() + + self.state = { + frameWidth = 0 + } + + self.updateFrameSize = function(rbx) + local frameWidth = rbx.AbsoluteSize.X + if frameWidth ~= self.state.frameWidth then + self:setState({ + frameWidth = frameWidth, + }) + end + end +end + +function ButtonStack:render() + assert(validateButtonStack(self.props)) + + return withStyle(function(stylePalette) + local font = stylePalette.Font + local textSize = font.Body.RelativeSize * font.BaseSize + + local buttons = self.props.buttons + local paddingBetween = #buttons > 1 and self.props.marginBetween or 0 + local nonStackedButtonWidth = (self.state.frameWidth / #buttons) - (paddingBetween * (#buttons - 1) / #buttons) + + local isButtonStacked = false + local fillDirection + if self.props.forcedFillDirection then + isButtonStacked = self.props.forcedFillDirection == Enum.FillDirection.Vertical + fillDirection = self.props.forcedFillDirection + else + for _, button in ipairs(buttons) do + local buttonTextWidth = GetTextSize( + button.props.text or "", + textSize, + font.Body.Font, + Vector2.new(self.state.frameWidth, self.props.buttonHeight) + ) + if buttonTextWidth.X > (nonStackedButtonWidth - (2 * self.props.minHorizontalButtonPadding)) then + isButtonStacked = true + break + end + end + fillDirection = isButtonStacked and Enum.FillDirection.Vertical or Enum.FillDirection.Horizontal + end + + local buttonSize = isButtonStacked and UDim2.new(1, 0, 0, self.props.buttonHeight) + or UDim2.new(0, nonStackedButtonWidth, 0, self.props.buttonHeight) + + local buttonTable = {} + for colIndex, button in ipairs(buttons) do + local newProps = { + layoutOrder = isButtonStacked and (#buttons - colIndex) or colIndex, + size = buttonSize, + } + local buttonProps = Cryo.Dictionary.join(newProps, button.props) + + local buttonComponent + if button.buttonType == ButtonType.PrimaryContextual then + buttonComponent = PrimaryContextualButton + elseif button.buttonType == ButtonType.PrimarySystem then + buttonComponent = PrimarySystemButton + elseif button.buttonType == ButtonType.Alert then + buttonComponent = AlertButton + else + buttonComponent = SecondaryButton + end + + if UIBloxConfig.enableExperimentalGamepadSupport then + local gamepadProps = { + [Roact.Ref] = self.buttonRefs[colIndex], + NextSelectionUp = (isButtonStacked and colIndex > 1) and self.buttonRefs[colIndex - 1] or nil, + NextSelectionDown = (isButtonStacked and colIndex < #buttons) and self.buttonRefs[colIndex + 1] or nil, + NextSelectionLeft = (not isButtonStacked and colIndex > 1) and self.buttonRefs[colIndex - 1] or nil, + NextSelectionRight = (not isButtonStacked and colIndex < #buttons) and self.buttonRefs[colIndex + 1] or nil, + } + local buttonPropsWithGamepad = Cryo.Dictionary.join(buttonProps, gamepadProps) + table.insert(buttonTable, Roact.createElement(buttonComponent, buttonPropsWithGamepad)) + else + table.insert(buttonTable, Roact.createElement(buttonComponent, buttonProps)) + end + end + + return Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable[FitFrameOnAxis] or FitFrameOnAxis, { + BackgroundTransparency = 1, + contentPadding = UDim.new(0, paddingBetween), + FillDirection = fillDirection, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + LayoutOrder = 3, + minimumSize = UDim2.new(1, 0, 0, self.props.buttonHeight), + [Roact.Ref] = self.props[Roact.Ref], + [Roact.Change.AbsoluteSize] = self.updateFrameSize, + + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + }, + buttonTable + ) + end) +end + +return ButtonStack \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ButtonStack.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ButtonStack.spec.lua new file mode 100644 index 0000000..649f33a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/ButtonStack.spec.lua @@ -0,0 +1,33 @@ +local ButtonRoot = script.Parent +local AppRoot = ButtonRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + +local ButtonStack = require(script.Parent.ButtonStack) + +local DEFAULT_REQUIRED_PROPS = { + buttons = { + { + props = { + text = "test", + onActivated = function() end, + }, + } + }, +} + +return function() + describe("lifecycle", function() + it("should mount and unmount button stacks without issue", function() + local tree = mockStyleComponent( + Roact.createElement(ButtonStack, DEFAULT_REQUIRED_PROPS) + ) + local handle = Roact.mount(tree) + Roact.unmount(handle) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/Enum/ButtonType.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/Enum/ButtonType.lua new file mode 100644 index 0000000..b17818b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/Enum/ButtonType.lua @@ -0,0 +1,13 @@ +local ButtonRoot = script.Parent.Parent +local AppRoot = ButtonRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local enumerate = require(Packages.enumerate) + +return enumerate("ButtonType", { + "Alert", + "PrimaryContextual", + "PrimarySystem", + "Secondary", +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/IconButton.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/IconButton.lua new file mode 100644 index 0000000..b7ebb8d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/IconButton.lua @@ -0,0 +1,157 @@ +local App = script:FindFirstAncestor("App") +local UIBlox = App.Parent +local Core = UIBlox.Core +local Packages = UIBlox.Parent + +local t = require(Packages.t) +local Roact = require(Packages.Roact) +local enumerate = require(Packages.enumerate) + +local Interactable = require(Core.Control.Interactable) + +local ControlState = require(Core.Control.Enum.ControlState) +local getContentStyle = require(Core.Button.getContentStyle) +local getIconSize = require(App.ImageSet.getIconSize) +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local bindingValidator = require(Core.Utility.bindingValidator) +local validateImage = require(Core.ImageSet.Validator.validateImage) + +local withStyle = require(Core.Style.withStyle) +local HoverButtonBackground = require(Core.Button.HoverButtonBackground) +local ImageSetComponent = require(Core.ImageSet.ImageSetComponent) +local IconSize = require(App.ImageSet.Enum.IconSize) + +local IconButton = Roact.PureComponent:extend("IconButton") +IconButton.debugProps = enumerate("debugProps", { + "controlState", +}) + +IconButton.validateProps = t.strictInterface({ + -- The state change callback for the button + onStateChanged = t.optional(t.callback), + + -- Is the button visually disabled + isDisabled = t.optional(t.boolean), + + colorStyleDefault = t.optional(t.string), + colorStyleHover = t.optional(t.string), + + --A Boolean value that determines whether user events are ignored and sink input + userInteractionEnabled = t.optional(t.boolean), + + -- The activated callback for the button + onActivated = t.optional(t.callback), + + anchorPoint = t.optional(t.Vector2), + layoutOrder = t.optional(t.number), + position = t.optional(t.UDim2), + size = t.optional(t.UDim2), + icon = t.optional(validateImage), + iconSize = t.optional(enumerateValidator(IconSize)), + iconColor3 = t.optional(t.Color3), + iconTransparency = t.optional(t.union(t.number, bindingValidator(t.number))), + + [Roact.Children] = t.optional(t.table), + + -- Override the default controlState + [IconButton.debugProps.controlState] = t.optional(enumerateValidator(ControlState)), +}) + +IconButton.defaultProps = { + anchorPoint = Vector2.new(0, 0), + layoutOrder = 0, + position = UDim2.new(0, 0, 0, 0), + size = nil, + icon = "", + iconSize = IconSize.Medium, + + colorStyleDefault = "SystemPrimaryDefault", + colorStyleHover = "SystemPrimaryDefault", + iconColor3 = nil, + iconTransparency = nil, + + isDisabled = false, + userInteractionEnabled = true, + + [IconButton.debugProps.controlState] = nil, +} + +function IconButton:init() + self:setState({ + controlState = ControlState.Initialize + }) + + self.onStateChanged = function(oldState, newState) + self:setState({ + controlState = newState, + }) + if self.props.onStateChanged then + self.props.onStateChanged(oldState, newState) + end + end + + local iconSizeToSizeScale = { + [IconSize.Small] = 1, + [IconSize.Medium] = 2, + [IconSize.Large] = 3, + [IconSize.XLarge] = 4, + [IconSize.XXLarge] = 5, + } + self.getSize = function(iconSizeMeasurement) + if self.props.size then + return self.props.size + end + + local iconSize = self.props.iconSize + local extents = iconSizeMeasurement + 4 * iconSizeToSizeScale[iconSize] + return UDim2.fromOffset(extents, extents) + end +end + +function IconButton:render() + return withStyle(function(style) + local iconSizeMeasurement = getIconSize(self.props.iconSize) + local size = self.getSize(iconSizeMeasurement) + local currentState = self.props[IconButton.debugProps.controlState] or self.state.controlState + + local iconStateColorMap = { + [ControlState.Default] = self.props.colorStyleDefault, + [ControlState.Hover] = self.props.colorStyleHover, + } + + local iconStyle = getContentStyle(iconStateColorMap, currentState, style) + + return Roact.createElement(Interactable, { + AnchorPoint = self.props.anchorPoint, + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + Size = size, + + isDisabled = self.props.isDisabled, + onStateChanged = self.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + BackgroundTransparency = 1, + AutoButtonColor = false, + + [Roact.Event.Activated] = self.props.onActivated, + }, { + sizeConstraint = Roact.createElement("UISizeConstraint", { + MinSize = Vector2.new(iconSizeMeasurement, iconSizeMeasurement), + }), + imageLabel = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.fromScale(0.5, 0.5), + Size = UDim2.fromOffset(iconSizeMeasurement, iconSizeMeasurement), + BackgroundTransparency = 1, + Image = self.props.icon, + ImageColor3 = self.props.iconColor3 or iconStyle.Color, + ImageTransparency = self.props.iconTransparency or iconStyle.Transparency, + }, + self.props[Roact.Children] + ), + background = currentState == ControlState.Hover and Roact.createElement(HoverButtonBackground), + }) + end) +end + +return IconButton diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/IconButton.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/IconButton.spec.lua new file mode 100644 index 0000000..5a23c6b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/IconButton.spec.lua @@ -0,0 +1,293 @@ +return function() + local IconButton = require(script.Parent.IconButton) + + local App = script:FindFirstAncestor("App") + local UIBlox = App.Parent + local Core = UIBlox.Core + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local ControlState = require(Core.Control.Enum.ControlState) + local IconSize = require(App.ImageSet.Enum.IconSize) + + describe("props", function() + local BUTTON_NAME = "test:" .. tostring(math.random(0, 999)) + local runTest = function(props) + local folder = Instance.new("Folder") + local element = mockStyleComponent({ + [BUTTON_NAME] = Roact.createElement(IconButton, props), + }) + + local instance = Roact.mount(element, folder) + + return folder, function() + Roact.unmount(instance) + folder:Destroy() + end + end + + local function getImageLabel(folder) + return folder:FindFirstChild("imageLabel", true) + end + + local function getGuiObjectRoot(folder) + return folder:FindFirstChild(BUTTON_NAME, true) + end + + local function getIconColor3(folder) + return getImageLabel(folder).ImageColor3 + end + + local function getIconTransparency(folder) + return getImageLabel(folder).ImageTransparency + end + + local function getGuiObjectRootAbsoluteSize(folder) + return getGuiObjectRoot(folder).AbsoluteSize + end + + local function getGuiObjectRootSize(folder) + return getGuiObjectRoot(folder).Size + end + + describe("iconSize", function() + it("SHOULD resize gui object root AbsoluteSize", function() + local smallFolder, smallCleanup = runTest({ + iconSize = IconSize.Small, + }) + + local mediumFolder, mediumCleanup = runTest({ + iconSize = IconSize.Medium, + }) + + expect(getGuiObjectRootAbsoluteSize(smallFolder)).to.never.equal(getGuiObjectRootAbsoluteSize(mediumFolder)) + + smallCleanup() + mediumCleanup() + end) + end) + + describe("appearance override props", function() + it("SHOULD override ImageLabel.ImageColor3 with iconColor3", function() + for _ = 1, 50 do + local randomColor = BrickColor.random().Color + local folder, cleanup = runTest({ + iconColor3 = randomColor, + }) + + expect(getIconColor3(folder)).to.equal(randomColor) + + cleanup() + end + end) + + it("SHOULD override ImageLabel.ImageTransparency with iconTransparency", function() + for transparency = 0, 1, 0.1 do + local folder, cleanup = runTest({ + iconTransparency = transparency, + }) + + expect(getIconTransparency(folder)).to.be.near(transparency, 0.001) + + cleanup() + end + end) + + it("SHOULD override ImageLabel.ImageTransparency with iconTransparency from RoactBinding value", function() + for transparency = 0, 1, 0.1 do + local folder, cleanup = runTest({ + iconTransparency = Roact.createBinding(transparency), + }) + + expect(getIconTransparency(folder)).to.be.near(transparency, 0.001) + + cleanup() + end + end) + + it("SHOULD override root guiObject.AbsoluteSize with size", function() + local testSizes = { + UDim2.fromScale(0.5, 0.5), + UDim2.fromScale(1, 1), + UDim2.fromOffset(1000, 10), + UDim2.fromOffset(0, 100), + } + for _, size in ipairs(testSizes) do + local controlGroupFolder, cleanupControlGroup = runTest({ + size = nil, + }) + + local variableGroupFolder, cleanupVariableGroup = runTest({ + size = size, + }) + + local controlSize = getGuiObjectRootAbsoluteSize(controlGroupFolder) + local variableSize = getGuiObjectRootAbsoluteSize(variableGroupFolder) + expect(controlSize).to.never.equal(variableSize) + + expect(getGuiObjectRootSize(variableGroupFolder)).to.equal(size) + + cleanupControlGroup() + cleanupVariableGroup() + end + end) + end) + + describe("positional props", function() + it("SHOULD respect AnchorPoint", function() + local function testAnchorPoint(anchorPoint) + local folder, cleanup = runTest({ + anchorPoint = anchorPoint, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.AnchorPoint).to.equal(anchorPoint) + + cleanup() + end + + testAnchorPoint(Vector2.new(0, 0)) + testAnchorPoint(Vector2.new(0, 1)) + testAnchorPoint(Vector2.new(1, 0)) + testAnchorPoint(Vector2.new(1, 1)) + testAnchorPoint(Vector2.new(0.5, 0.5)) + end) + + it("SHOULD respect Position", function() + local function testPosition(position) + local folder, cleanup = runTest({ + position = position, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.Position).to.equal(position) + + cleanup() + end + + testPosition(UDim2.new(0, 0, 0, 0)) + testPosition(UDim2.new(0.5, 10, 1, 20)) + testPosition(UDim2.fromScale(1, 1)) + testPosition(UDim2.fromOffset(100, 000)) + end) + + it("SHOULD respect LayoutOrder", function() + local function testLayoutOrder(layoutOrder) + local folder, cleanup = runTest({ + layoutOrder = layoutOrder, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.LayoutOrder).to.equal(layoutOrder) + + cleanup() + end + + testLayoutOrder(0) + testLayoutOrder(1) + testLayoutOrder(2) + end) + + it("SHOULD respect Size", function() + local function testSize(size) + local folder, cleanup = runTest({ + size = size, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.Size).to.equal(size) + + cleanup() + end + + testSize(UDim2.new(0, 0, 0, 0)) + testSize(UDim2.new(0.5, 10, 1, 20)) + testSize(UDim2.fromScale(1, 1)) + testSize(UDim2.fromOffset(100, 000)) + end) + end) + + describe("debugProps.controlState", function() + local function isShowingBackground(folder) + return folder:FindFirstChild("background", true) ~= nil + end + + local function isImageTransparent(folder) + local imageLabel = folder:FindFirstChild("imageLabel", true) + assert(imageLabel, "imageLabel never mounted") + return imageLabel.ImageTransparency > 0 + end + + it("SHOULD render ControlState.Default with no issues", function() + local folder, cleanup = runTest({ + [IconButton.debugProps.controlState] = ControlState.Default, + }) + + expect(isShowingBackground(folder)).to.equal(false) + expect(isImageTransparent(folder)).to.equal(false) + + cleanup() + end) + + it("SHOULD render ControlState.Hover with no issues", function() + local folder, cleanup = runTest({ + [IconButton.debugProps.controlState] = ControlState.Hover, + }) + + expect(isShowingBackground(folder)).to.equal(true) + expect(isImageTransparent(folder)).to.equal(false) + + cleanup() + end) + + it("SHOULD render ControlState.Pressed with no issues", function() + local folder, cleanup = runTest({ + [IconButton.debugProps.controlState] = ControlState.Pressed, + }) + + expect(isShowingBackground(folder)).to.equal(false) + expect(isImageTransparent(folder)).to.equal(true) + + cleanup() + end) + + it("SHOULD render ControlState.Disabled with no issues", function() + local folder, cleanup = runTest({ + [IconButton.debugProps.controlState] = ControlState.Disabled, + }) + + expect(isShowingBackground(folder)).to.equal(false) + expect(isImageTransparent(folder)).to.equal(true) + + cleanup() + end) + end) + end) + + describe("children", function() + it("SHOULD render children passed to it", function() + local BUTTON_NAME = "test:" .. tostring(math.random(0, 999)) + local COLOR = Color3.new(1, 0.5, 0) + + local folder = Instance.new("Folder") + local element = mockStyleComponent({ + [BUTTON_NAME] = Roact.createElement(IconButton, {}, { + childFrameElement = Roact.createElement("Frame", { + Size = UDim2.new(0, 16, 0, 16), + BackgroundColor3 = COLOR, + }), + }), + }) + + local instance = Roact.mount(element, folder) + + local childElement = folder:FindFirstChild("childFrameElement", true) + expect(childElement).to.be.ok() + expect(childElement.BackgroundColor3).to.equal(COLOR) + + Roact.unmount(instance) + folder:Destroy() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimaryContextualButton.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimaryContextualButton.lua new file mode 100644 index 0000000..2d9f8c7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimaryContextualButton.lua @@ -0,0 +1,65 @@ +local Button = script.Parent +local App = Button.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local RoactGamepad = require(Packages.RoactGamepad) + +local Images = require(App.ImageSet.Images) +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) +local validateButtonProps = require(Button.validateButtonProps) +local GenericButton = require(UIBlox.Core.Button.GenericButton) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local PrimaryContextualButton = Roact.PureComponent:extend("PrimaryContextualButton") + +local BUTTON_STATE_COLOR = { + [ControlState.Default] = "ContextualPrimaryDefault", + [ControlState.Hover] = "ContextualPrimaryOnHover", +} + +local CONTENT_STATE_COLOR = { + [ControlState.Default] = "ContextualPrimaryContent", +} + +PrimaryContextualButton.defaultProps = { + isDisabled = false, + isLoading = false, +} + +function PrimaryContextualButton:render() + assert(validateButtonProps(self.props)) + local image = Images["component_assets/circle_17"] + local genericButtonComponent = UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable[GenericButton] or GenericButton + return withSelectionCursorProvider(function(getSelectionCursor) + return Roact.createElement(genericButtonComponent, { + Size = self.props.size, + AnchorPoint = self.props.anchorPoint, + Position = self.props.position, + LayoutOrder = self.props.layoutOrder, + SelectionImageObject = getSelectionCursor(CursorKind.RoundedRectNoInset), + icon = self.props.icon, + text = self.props.text, + isDisabled = self.props.isDisabled, + isLoading = self.props.isLoading, + onActivated = self.props.onActivated, + onStateChanged = self.props.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + buttonImage = image, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + [Roact.Ref] = self.props[Roact.Ref], + }) + end) +end + +return PrimaryContextualButton \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimaryContextualButton.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimaryContextualButton.spec.lua new file mode 100644 index 0000000..655a90e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimaryContextualButton.spec.lua @@ -0,0 +1,62 @@ +return function() + local Button = script.Parent + local App = Button.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local Images = require(App.ImageSet.Images) + + local icon = Images["icons/common/robux_small"] + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local PrimaryContextualButton = require(Button.PrimaryContextualButton) + + it("should create and destroy Primary Contextual Button with text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimaryContextualButton, { + text = "Button", + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Primary Contextual Button with text icon only without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimaryContextualButton, { + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Primary Contextual Button with text and text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimaryContextualButton, { + text = "Button", + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a blank Primary Contextual Button without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimaryContextualButton, { + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimarySystemButton.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimarySystemButton.lua new file mode 100644 index 0000000..2f841e2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimarySystemButton.lua @@ -0,0 +1,66 @@ +local Button = script.Parent +local App = Button.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local RoactGamepad = require(Packages.RoactGamepad) + +local Images = require(App.ImageSet.Images) +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) +local validateButtonProps = require(Button.validateButtonProps) +local GenericButton = require(UIBlox.Core.Button.GenericButton) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local PrimarySystemButton = Roact.PureComponent:extend("PrimarySystemButton") + +local BUTTON_STATE_COLOR = { + [ControlState.Default] = "SystemPrimaryDefault", + [ControlState.Hover] = "SystemPrimaryOnHover", +} + +local CONTENT_STATE_COLOR = { + [ControlState.Default] = "SystemPrimaryContent", +} + + +PrimarySystemButton.defaultProps = { + isDisabled = false, + isLoading = false, +} + +function PrimarySystemButton:render() + assert(validateButtonProps(self.props)) + local image = Images["component_assets/circle_17"] + local genericButtonComponent = UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable[GenericButton] or GenericButton + return withSelectionCursorProvider(function(getSelectionCursor) + return Roact.createElement(genericButtonComponent, { + Size = self.props.size, + AnchorPoint = self.props.anchorPoint, + Position = self.props.position, + LayoutOrder = self.props.layoutOrder, + SelectionImageObject = getSelectionCursor(CursorKind.RoundedRectNoInset), + icon = self.props.icon, + text = self.props.text, + isDisabled = self.props.isDisabled, + isLoading = self.props.isLoading, + onActivated = self.props.onActivated, + onStateChanged = self.props.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + buttonImage = image, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + [Roact.Ref] = self.props[Roact.Ref], + }) + end) +end + +return PrimarySystemButton \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimarySystemButton.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimarySystemButton.spec.lua new file mode 100644 index 0000000..cbcd415 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/PrimarySystemButton.spec.lua @@ -0,0 +1,62 @@ +return function() + local Button = script.Parent + local App = Button.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local Images = require(App.ImageSet.Images) + + local icon = Images["icons/common/robux_small"] + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local PrimarySystemButton = require(Button.PrimarySystemButton) + + it("should create and destroy Primary System Button with text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimarySystemButton, { + text = "Button", + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Primary System Button with text icon only without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimarySystemButton, { + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Primary System Button with text and text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimarySystemButton, { + text = "Button", + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a blank Primary System Button without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(PrimarySystemButton, { + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/SecondaryButton.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/SecondaryButton.lua new file mode 100644 index 0000000..7d009b2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/SecondaryButton.lua @@ -0,0 +1,66 @@ +local Button = script.Parent +local App = Button.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local RoactGamepad = require(Packages.RoactGamepad) + +local Images = require(App.ImageSet.Images) +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) +local validateButtonProps = require(Button.validateButtonProps) +local GenericButton = require(UIBlox.Core.Button.GenericButton) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local SecondaryButton = Roact.PureComponent:extend("SecondaryButton") + +local BUTTON_STATE_COLOR = { + [ControlState.Default] = "SecondaryDefault", + [ControlState.Hover] = "SecondaryOnHover", +} + +local CONTENT_STATE_COLOR = { + [ControlState.Default] = "SecondaryContent", + [ControlState.Hover] = "SecondaryOnHover", +} + +SecondaryButton.defaultProps = { + isDisabled = false, + isLoading = false, +} + +function SecondaryButton:render() + assert(validateButtonProps(self.props)) + local image = Images["component_assets/circle_17_stroke_1"] + local genericButtonComponent = UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable[GenericButton] or GenericButton + return withSelectionCursorProvider(function(getSelectionCursor) + return Roact.createElement(genericButtonComponent, { + Size = self.props.size, + AnchorPoint = self.props.anchorPoint, + Position = self.props.position, + LayoutOrder = self.props.layoutOrder, + SelectionImageObject = getSelectionCursor(CursorKind.RoundedRectNoInset), + icon = self.props.icon, + text = self.props.text, + isDisabled = self.props.isDisabled, + isLoading = self.props.isLoading, + onActivated = self.props.onActivated, + onStateChanged = self.props.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + buttonImage = image, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + [Roact.Ref] = self.props[Roact.Ref], + }) + end) +end + +return SecondaryButton \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/SecondaryButton.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/SecondaryButton.spec.lua new file mode 100644 index 0000000..16ddfec --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/SecondaryButton.spec.lua @@ -0,0 +1,62 @@ +return function() + local Button = script.Parent + local App = Button.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local Images = require(App.ImageSet.Images) + + local icon = Images["icons/common/robux_small"] + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local SecondaryButton = require(Button.SecondaryButton) + + it("should create and destroy Secondary Button with text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(SecondaryButton, { + text = "Button", + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Secondary Button with text icon only without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(SecondaryButton, { + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Secondary Button with text and text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(SecondaryButton, { + text = "Button", + icon = icon, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a blank Secondary Button without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(SecondaryButton, { + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/TextButton.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/TextButton.lua new file mode 100644 index 0000000..b4808d7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/TextButton.lua @@ -0,0 +1,145 @@ +local App = script:FindFirstAncestor("App") +local UIBlox = App.Parent +local Core = UIBlox.Core +local Packages = UIBlox.Parent + +local t = require(Packages.t) +local Roact = require(Packages.Roact) +local enumerate = require(Packages.enumerate) + +local Interactable = require(Core.Control.Interactable) + +local ControlState = require(Core.Control.Enum.ControlState) +local getContentStyle = require(Core.Button.getContentStyle) +local GetTextSize = require(Core.Text.GetTextSize) +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) + +local withStyle = require(Core.Style.withStyle) +local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel) +local HoverButtonBackground = require(Core.Button.HoverButtonBackground) + +local VERTICAL_PADDING = 8 +local HORIZONTAL_PADDING = 11 + +local TextButton = Roact.PureComponent:extend("TextButton") +TextButton.debugProps = enumerate("debugProps", { + "getTextSize", + "controlState", +}) + +TextButton.validateProps = t.strictInterface({ + -- The state change callback for the button + onStateChanged = t.optional(t.callback), + + -- Is the button visually disabled + isDisabled = t.optional(t.boolean), + + fontStyle = t.optional(t.string), + colorStyleDefault = t.optional(t.string), + colorStyleHover = t.optional(t.string), + hoverBackgroundEnabled = t.optional(t.boolean), + richText = t.optional(t.boolean), + + --A Boolean value that determines whether user events are ignored and sink input + userInteractionEnabled = t.optional(t.boolean), + + -- The activated callback for the button + onActivated = t.optional(t.callback), + + anchorPoint = t.optional(t.Vector2), + layoutOrder = t.optional(t.number), + position= t.optional(t.UDim2), + size = t.optional(t.UDim2), + text = t.optional(t.string), + + -- A callback that replaces getTextSize implementation + [TextButton.debugProps.getTextSize] = t.optional(t.callback), + + -- Override the default controlState + [TextButton.debugProps.controlState] = t.optional(enumerateValidator(ControlState)), +}) + +TextButton.defaultProps = { + anchorPoint = Vector2.new(0, 0), + layoutOrder = 0, + position = UDim2.new(0, 0, 0, 0), + size = UDim2.fromScale(0, 0), + text = "", + + fontStyle = "Header2", + colorStyleDefault = "SystemPrimaryDefault", + colorStyleHover = "SystemPrimaryDefault", + hoverBackgroundEnabled = true, + richText = false, + + isDisabled = false, + userInteractionEnabled = true, + + [TextButton.debugProps.getTextSize] = GetTextSize, + [TextButton.debugProps.controlState] = nil, +} + +function TextButton:init() + self:setState({ + controlState = ControlState.Initialize + }) + + self.onStateChanged = function(oldState, newState) + self:setState({ + controlState = newState, + }) + if self.props.onStateChanged then + self.props.onStateChanged(oldState, newState) + end + end +end + +function TextButton:render() + return withStyle(function(style) + local currentState = self.props[TextButton.debugProps.controlState] or self.state.controlState + + local textStateColorMap = { + [ControlState.Default] = self.props.colorStyleDefault, + [ControlState.Hover] = self.props.colorStyleHover, + } + + local textStyle = getContentStyle(textStateColorMap, currentState, style) + local fontStyle = style.Font[self.props.fontStyle] + + local fontSize = fontStyle.RelativeSize * style.Font.BaseSize + local getTextSize = self.props[TextButton.debugProps.getTextSize] + local textWidth = getTextSize(self.props.text, fontSize, fontStyle.Font, Vector2.new(10000, 0)).X + + return Roact.createElement(Interactable, { + AnchorPoint = self.props.anchorPoint, + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + Size = self.props.size, + + isDisabled = self.props.isDisabled, + onStateChanged = self.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + BackgroundTransparency = 1, + AutoButtonColor = false, + + [Roact.Event.Activated] = self.props.onActivated, + }, { + sizeConstraint = Roact.createElement("UISizeConstraint", { + MinSize = Vector2.new(textWidth + VERTICAL_PADDING*2, fontSize + HORIZONTAL_PADDING*2), + }), + textLabel = Roact.createElement(GenericTextLabel, { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.fromScale(0.5, 0.5), + BackgroundTransparency = 1, + Text = self.props.text, + fontStyle = fontStyle, + colorStyle = textStyle, + RichText = self.props.richText, + }), + background = self.props.hoverBackgroundEnabled and currentState == ControlState.Hover + and Roact.createElement(HoverButtonBackground) + }) + end) +end + +return TextButton diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/TextButton.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/TextButton.spec.lua new file mode 100644 index 0000000..f009da2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/TextButton.spec.lua @@ -0,0 +1,288 @@ +return function() + local TextButton = require(script.Parent.TextButton) + + local App = script:FindFirstAncestor("App") + local UIBlox = App.Parent + local Core = UIBlox.Core + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local ControlState = require(Core.Control.Enum.ControlState) + + local noOpt = function() + end + local text = "Button" + + it("should create and destroy a button without errors", function() + local folder = Instance.new("Folder") + local element = mockStyleComponent({ + button = Roact.createElement(TextButton, { + text = text, + onActivated = noOpt, + fontStyle = "Body", + colorStyleDefault = "UIDefault", + colorStyleHover = "UIDefault", + }), + }) + + local instance = Roact.mount(element, folder) + local label = folder:FindFirstChildWhichIsA("TextLabel", true) + expect(label.Text).to.equal(text) + + Roact.unmount(instance) + end) + + it("should create and destroy a button that is disabled without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(TextButton, { + text = text, + onActivated = noOpt, + isDisabled = true, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a button without text without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(TextButton, { + onActivated = noOpt, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should be created as a disabled button", function() + local buttonState = nil + local element = mockStyleComponent({ + button = Roact.createElement(TextButton, { + onActivated = noOpt, + onStateChanged = function(_, newState) + buttonState = newState + end, + isDisabled = true, + }), + }) + + local instance = Roact.mount(element) + expect(buttonState).to.equal(ControlState.Disabled) + Roact.unmount(instance) + end) + + describe("text prop", function() + local BUTTON_NAME = "test:" .. tostring(math.random(0, 999)) + local runTest = function(props) + local folder = Instance.new("Folder") + local element = mockStyleComponent({ + [BUTTON_NAME] = Roact.createElement(TextButton, props), + }) + + local instance = Roact.mount(element, folder) + + return folder, function() + Roact.unmount(instance) + folder:Destroy() + end + end + + it("SHOULD resize to text when not given size property", function() + local folder1, cleanup1 = runTest({ + text = string.rep("!", 1), + [TextButton.debugProps.getTextSize] = function() + return Vector2.new(1, 1) + end, + }) + local folder2, cleanup2 = runTest({ + text = string.rep("!", 10), + [TextButton.debugProps.getTextSize] = function() + return Vector2.new(10, 1) + end, + }) + + local firstSize = folder1:FindFirstChild(BUTTON_NAME, true).AbsoluteSize + local secondSize = folder2:FindFirstChild(BUTTON_NAME, true).AbsoluteSize + + expect(firstSize).to.never.equal(secondSize) + + cleanup1() + cleanup2() + end) + end) + + describe("positional props", function() + local BUTTON_NAME = "test:" .. tostring(math.random(0, 999)) + local runTest = function(props) + local folder = Instance.new("Folder") + local element = mockStyleComponent({ + [BUTTON_NAME] = Roact.createElement(TextButton, props), + }) + + local instance = Roact.mount(element, folder) + + return folder, function() + Roact.unmount(instance) + folder:Destroy() + end + end + + it("SHOULD respect AnchorPoint", function() + local function testAnchorPoint(anchorPoint) + local folder, cleanup = runTest({ + anchorPoint = anchorPoint, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.AnchorPoint).to.equal(anchorPoint) + + cleanup() + end + + testAnchorPoint(Vector2.new(0, 0)) + testAnchorPoint(Vector2.new(0, 1)) + testAnchorPoint(Vector2.new(1, 0)) + testAnchorPoint(Vector2.new(1, 1)) + testAnchorPoint(Vector2.new(0.5, 0.5)) + end) + + it("SHOULD respect Position", function() + local function testPosition(position) + local folder, cleanup = runTest({ + position = position, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.Position).to.equal(position) + + cleanup() + end + + testPosition(UDim2.new(0, 0, 0, 0)) + testPosition(UDim2.new(0.5, 10, 1, 20)) + testPosition(UDim2.fromScale(1, 1)) + testPosition(UDim2.fromOffset(100, 000)) + end) + + it("SHOULD respect LayoutOrder", function() + local function testLayoutOrder(layoutOrder) + local folder, cleanup = runTest({ + layoutOrder = layoutOrder, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.LayoutOrder).to.equal(layoutOrder) + + cleanup() + end + + testLayoutOrder(0) + testLayoutOrder(1) + testLayoutOrder(2) + end) + + it("SHOULD respect Size", function() + local function testSize(size) + local folder, cleanup = runTest({ + size = size, + }) + + local guiObject = folder:FindFirstChild(BUTTON_NAME, true) + expect(guiObject.Size).to.equal(size) + + cleanup() + end + + testSize(UDim2.new(0, 0, 0, 0)) + testSize(UDim2.new(0.5, 10, 1, 20)) + testSize(UDim2.fromScale(1, 1)) + testSize(UDim2.fromOffset(100, 000)) + end) + end) + + describe("debugProps.controlState", function() + local BUTTON_NAME = "test:" .. tostring(math.random(0, 999)) + local runTest = function(props) + local folder = Instance.new("Folder") + local element = mockStyleComponent({ + [BUTTON_NAME] = Roact.createElement(TextButton, props), + }) + + local instance = Roact.mount(element, folder) + + return folder, function() + Roact.unmount(instance) + folder:Destroy() + end + end + + local function isShowingBackground(folder) + return folder:FindFirstChild("background", true) ~= nil + end + + local function isTextTransparent(folder) + local textLabel = folder:FindFirstChild("textLabel", true) + assert(textLabel, "textLabel never mounted") + return textLabel.TextTransparency > 0 + end + + it("SHOULD render ControlState.Default with no issues", function() + local folder, cleanup = runTest({ + [TextButton.debugProps.controlState] = ControlState.Default, + }) + + expect(isShowingBackground(folder)).to.equal(false) + expect(isTextTransparent(folder)).to.equal(false) + + cleanup() + end) + + it("SHOULD render ControlState.Hover with no issues", function() + local folder, cleanup = runTest({ + [TextButton.debugProps.controlState] = ControlState.Hover, + }) + + expect(isShowingBackground(folder)).to.equal(true) + expect(isTextTransparent(folder)).to.equal(false) + + cleanup() + end) + + it("SHOULD render ControlState.Hover when background disabled with no issues", function() + local folder, cleanup = runTest({ + [TextButton.debugProps.controlState] = ControlState.Hover, + hoverBackgroundEnabled = false, + }) + + expect(isShowingBackground(folder)).to.equal(false) + expect(isTextTransparent(folder)).to.equal(false) + + cleanup() + end) + + it("SHOULD render ControlState.Pressed with no issues", function() + local folder, cleanup = runTest({ + [TextButton.debugProps.controlState] = ControlState.Pressed, + }) + + expect(isShowingBackground(folder)).to.equal(false) + expect(isTextTransparent(folder)).to.equal(true) + + cleanup() + end) + + it("SHOULD render ControlState.Disabled with no issues", function() + local folder, cleanup = runTest({ + [TextButton.debugProps.controlState] = ControlState.Disabled, + }) + + expect(isShowingBackground(folder)).to.equal(false) + expect(isTextTransparent(folder)).to.equal(true) + + cleanup() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/Validator/validateButtonStack.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/Validator/validateButtonStack.lua new file mode 100644 index 0000000..282847a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/Validator/validateButtonStack.lua @@ -0,0 +1,46 @@ +local validatorRoot = script.Parent +local ButtonRoot = validatorRoot.Parent +local AppRoot = ButtonRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) +local Roact = require(Packages.Roact) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local validateButtonProps = require(ButtonRoot.validateButtonProps) + +local ButtonType = require(ButtonRoot.Enum.ButtonType) + +return t.strictInterface({ + -- buttons: A table of button tables that contain props that PrimaryContextualButton, + -- AlertButton, PrimarySystemButton, or SecondaryButton allow. Also contains a prop "buttonType" + -- to determine which of these button types to use. + buttons = t.array(t.strictInterface({ + buttonType = t.optional(enumerateValidator(ButtonType)), + props = validateButtonProps, + })), + + buttonHeight = t.optional(t.numberMin(0)), + + -- forceFillDirection: What fill direction to force into. If nil, then the fillDirection + -- will be Vertical and automatically change to Horizontal if any button's text is + -- too long. + forcedFillDirection = t.optional(t.enum(Enum.FillDirection)), + + -- marginBetween: the margin between each button. + marginBetween = t.optional(t.numberMin(0)), + + -- minHorizontalButtonPadding: The minimum left and right padding used to calculate + -- the when the button text overflows and automatically changes fillDirection. + -- The overflow calculation will be if the length of the button text is over + -- the button size - (2 * minHorizontalButtonPadding). + minHorizontalButtonPadding = t.optional(t.numberMin(0)), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/validateButtonProps.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/validateButtonProps.lua new file mode 100644 index 0000000..0b78a3f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Button/validateButtonProps.lua @@ -0,0 +1,49 @@ +local Button = script.Parent +local App = Button.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Core = UIBlox.Core + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local validateImage = require(Core.ImageSet.Validator.validateImage) + +return t.strictInterface({ + --The size of the button + size = t.optional(t.UDim2), + + --The anchor point of the button + anchorPoint = t.optional(t.Vector2), + + --The position of the button + position = t.optional(t.UDim2), + + --The layout order of the button + layoutOrder = t.optional(t.number), + + --The icon of the button + icon = t.optional(validateImage), + + --The text of the button + text = t.optional(t.string), + + --Is the button disabled + isDisabled = t.optional(t.boolean), + + --Is the button loading + isLoading = t.optional(t.boolean), + + --The activated callback for the button + onActivated = t.callback, + + --A Boolean value that determines whether user events are ignored and sink input + userInteractionEnabled = t.optional(t.boolean), + + -- Gamepad Support props + NextSelectionDown = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonCell.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonCell.lua new file mode 100644 index 0000000..947e39a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonCell.lua @@ -0,0 +1,94 @@ +local SelectionGroup = script.Parent +local Small = SelectionGroup.Parent +local Cell = Small.Parent +local App = Cell.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local GenericSelectionCell = require(Packages.UIBlox.Core.Cell.GenericSelectionCell) + +local DEFAULT_IMAGE = Images["component_assets/circle_24_stroke_1"] +local SELECTED_IMAGE = Images["component_assets/circle_16"] +local DEFAULT_IMAGE_SIZE = 24 +local SELECTED_IMAGE_SIZE = 16 +local CELL_SIZE = 56 + +local SmallRadioButtonCell = Roact.PureComponent:extend("SmallRadioButtonCell") + +SmallRadioButtonCell.validateProps = t.strictInterface({ + -- Unique key to identify this selection. + key = t.string, + + -- Text to display + text = t.optional(t.string), + + -- Callback for when this selection is activated. + onActivated = t.optional(t.callback), + + -- Whether this selection is selected or not. + isSelected = t.optional(t.boolean), + + -- If this cell is disabled + isDisabled = t.optional(t.boolean), + + -- If this cell should use the default control state + useDefaultControlState = t.optional(t.boolean), + + -- The LayoutOrder. + layoutOrder = t.optional(t.number), + + -- optional parameters for RoactGamepad + [Roact.Ref] = t.optional(t.table), + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + SelectionImageObject = t.optional(t.table), +}) + +SmallRadioButtonCell.defaultProps = { + text = "", + isSelected = false, +} + +function SmallRadioButtonCell:init() + self.onSetValue = function() + self.props.onActivated(self.props.key) + end +end + +function SmallRadioButtonCell:render() + assert(self.validateProps(self.props)) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, CELL_SIZE), + BorderSizePixel = 0, + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + }, { + GenericSelectionCell = Roact.createElement(GenericSelectionCell, { + isSelected = self.props.isSelected, + isDisabled = self.props.isDisabled, + defaultImage = DEFAULT_IMAGE, + selectedImage = SELECTED_IMAGE, + defaultImageSize = DEFAULT_IMAGE_SIZE, + selectedImageSize = SELECTED_IMAGE_SIZE, + text = self.props.text, + onActivated = self.onSetValue, + useDefaultControlState = self.props.useDefaultControlState, + + [Roact.Ref] = self.props[Roact.Ref], + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + SelectionImageObject = self.props.SelectionImageObject, + }), + }) +end + +return SmallRadioButtonCell \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonCell.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonCell.spec.lua new file mode 100644 index 0000000..4f04824 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonCell.spec.lua @@ -0,0 +1,25 @@ +return function() + local SelectionGroup = script.Parent + local Small = SelectionGroup.Parent + local Cell = Small.Parent + local App = Cell.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local SmallRadioButtonCell = require(script.Parent.SmallRadioButtonCell) + + it("should create and destroy SmallRadioButtonCell without errors", function() + local element = mockStyleComponent({ + smallRadioButtonCell = Roact.createElement(SmallRadioButtonCell, { + key = "1", + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonGroup.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonGroup.lua new file mode 100644 index 0000000..08095fe --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonGroup.lua @@ -0,0 +1,112 @@ +local SelectionGroup = script.Parent +local Small = SelectionGroup.Parent +local Cell = Small.Parent +local App = Cell.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local RoactGamepad = require(Packages.RoactGamepad) +local t = require(Packages.t) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local withSelectionCursorProvider = require(UIBlox.App.SelectionImage.withSelectionCursorProvider) +local CursorKind = require(UIBlox.App.SelectionImage.CursorKind) + +local SmallRadioButtonCell = require(UIBlox.App.Cell.Small.SelectionGroup.SmallRadioButtonCell) + +local SmallRadioButtonGroup = Roact.PureComponent:extend("SmallRadioButtonGroup") + +local buttonInterface = t.strictInterface({ + text = t.string, + key = t.string, + isDisabled = t.optional(t.boolean), +}) + +SmallRadioButtonGroup.validateProps = t.strictInterface({ + -- List of text, key pairs that will be used for each radio button. + items = t.optional(t.array(t.tuple(buttonInterface))), + + -- Which key is currently selected. + selectedValue = t.optional(t.string), + + -- Callback for when a cell is activated. + onActivated = t.callback, + + -- Layout order for this component. + layoutOrder = t.optional(t.number), + + -- If this cell should use the default control state + useDefaultControlState = t.optional(t.boolean), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) + +SmallRadioButtonGroup.defaultProps = { + selectedValue = nil, +} + +function SmallRadioButtonGroup:init() + self.gamepadRefs = RoactGamepad.createRefCache() +end + +function SmallRadioButtonGroup:render() + assert(self.validateProps(self.props)) + + local smallRadioButtonCellGroup = {} + smallRadioButtonCellGroup.layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 1), + }) + for index, button in ipairs(self.props.items) do + if UIBloxConfig.enableExperimentalGamepadSupport then + smallRadioButtonCellGroup["smallRadioButtonCell"..button.key] = + withSelectionCursorProvider(function(getSelectionCursor) + return Roact.createElement(RoactGamepad.Focusable[SmallRadioButtonCell], { + key = button.key, + text = button.text, + onActivated = self.props.onActivated, + isSelected = self.props.selectedValue == button.key, + isDisabled = button.isDisabled, + layoutOrder = index, + useDefaultControlState = self.props.useDefaultControlState, + + [Roact.Ref] = self.gamepadRefs[index], + NextSelectionUp = index > 1 and self.gamepadRefs[index - 1] or nil, + NextSelectionDown = index < #self.props.items and self.gamepadRefs[index + 1] or nil, + SelectionImageObject = getSelectionCursor(CursorKind.SelectionCell), + }) + end) + else + smallRadioButtonCellGroup["smallRadioButtonCell"..button.key] = Roact.createElement(SmallRadioButtonCell, { + key = button.key, + text = button.text, + onActivated = self.props.onActivated, + isSelected = self.props.selectedValue == button.key, + isDisabled = button.isDisabled, + layoutOrder = index, + }) + end + end + + local gamepadEnabled = (UIBloxConfig.enableExperimentalGamepadSupport and self.props.items and #self.props.items > 0) + + return Roact.createElement(gamepadEnabled and RoactGamepad.Focusable.Frame or "Frame", { + defaultChild = gamepadEnabled and self.gamepadRefs[1] or nil, + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionDown = self.props.NextSelectionDown, + NextSelectionUp = self.props.NextSelectionUp, + [Roact.Ref] = self.props[Roact.Ref], + }, smallRadioButtonCellGroup) +end + +return SmallRadioButtonGroup \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonGroup.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonGroup.spec.lua new file mode 100644 index 0000000..a9cb1ce --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Cell/Small/SelectionGroup/SmallRadioButtonGroup.spec.lua @@ -0,0 +1,47 @@ +return function() + local SelectionGroup = script.Parent + local Small = SelectionGroup.Parent + local Cell = Small.Parent + local App = Cell.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local SmallRadioButtonGroup = require(script.Parent.SmallRadioButtonGroup) + + local ITEMS = { + { text = "Selection 1", key = "1" }, + { text = "Selection 3", key = "3" }, + { text = "Selection 2", key = "2" }, + { text = "Disabled Cell", key = "4", isDisabled = true } + } + + it("should create and destroy SmallRadioButtonGroup without errors", function() + local element = mockStyleComponent({ + smallRadioButtonGroup = Roact.createElement(SmallRadioButtonGroup, { + onActivated = function() end, + items = ITEMS, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy SmallRadioButtonGroup without errors with all optional props used", function() + local element = mockStyleComponent({ + smallRadioButtonGroup = Roact.createElement(SmallRadioButtonGroup, { + onActivated = function() end, + items = ITEMS, + selectedValue = "1", + layoutOrder = 1, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Constant/IconSize.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Constant/IconSize.lua new file mode 100644 index 0000000..5084343 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Constant/IconSize.lua @@ -0,0 +1,7 @@ +return { + Small = 16, + Regular = 36, + Large = 48, + XLarge = 96, + XXLarge = 192, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/CarouselHeader.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/CarouselHeader.lua new file mode 100644 index 0000000..c65a866 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/CarouselHeader.lua @@ -0,0 +1,106 @@ +local Carousel = script.Parent +local Container = Carousel.Parent +local App = Container.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Style.withStyle) + +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) + +local IconSize = require(App.ImageSet.Enum.IconSize) +local getIconSize = require(App.ImageSet.getIconSize) +local Images = require(App.ImageSet.Images) + +local Core = UIBlox.Core +local Interactable = require(Core.Control.Interactable) +local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel) +local ImageSetComponent = require(Core.ImageSet.ImageSetComponent) + +local MAX_BOUND = 10000 +local SEE_ALL_ARROW = Images["icons/navigation/pushRight_small"] +local TEXT_ICON_PADDING = 4 + +local CarouselHeader = Roact.PureComponent:extend("CarouselHeader") + +CarouselHeader.validateProps = t.strictInterface({ + -- The header text for the carousel + headerText = t.optional(t.string), + + -- The callback for the see all arrow. if nil, the arrow won't be shown + onSeeAll = t.optional(t.callback), + + -- The carousel left margin + carouselMargin = t.optional(t.number), + + -- The layout order + layoutOrder = t.optional(t.number), +}) + +CarouselHeader.defaultProps = { + headerText = "", + carouselMargin = 0, +} + +function CarouselHeader:render() + local headerText = self.props.headerText + local onSeeAll = self.props.onSeeAll + + local layoutOrder = self.props.layoutOrder + local carouselMargin = self.props.carouselMargin + + return withStyle(function(style) + local fontStyle = style.Font.Header1 + local baseSize = style.Font.BaseSize + local fontSize = fontStyle.RelativeSize * baseSize + local textFont = fontStyle.Font + + local textboxBounds = GetTextSize(headerText, fontSize, textFont, Vector2.new(MAX_BOUND, MAX_BOUND)) + local textboxSize = UDim2.fromOffset(textboxBounds.X + TEXT_ICON_PADDING + getIconSize(IconSize.Small), + textboxBounds.Y) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, textboxBounds.Y), + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + CarouselHeaderButton = Roact.createElement(Interactable, { + Position = UDim2.fromOffset(carouselMargin, 0), + Size = textboxSize, + AutoButtonColor = false, + BackgroundTransparency = 1, + [Roact.Event.Activated] = onSeeAll, + --Note State change is not being used right now. + onStateChanged = function()end, + }, { + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, TEXT_ICON_PADDING), + }), + HeaderText = Roact.createElement(GenericTextLabel, { + Text = headerText, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + LayoutOrder = 1, + fontStyle = fontStyle, + colorStyle = style.Theme.TextEmphasis, + }), + SeeAllArrow = onSeeAll and Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(getIconSize(IconSize.Small), getIconSize(IconSize.Small)), + BackgroundTransparency = 1, + Image = SEE_ALL_ARROW, + ImageColor3 = style.Theme.TextEmphasis.Color, + ImageTransparency = style.Theme.TextEmphasis.Transparency, + LayoutOrder = 2, + }) or nil, + }) + }) + end) +end + +return CarouselHeader diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/CarouselHeader.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/CarouselHeader.spec.lua new file mode 100644 index 0000000..e681d5e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/CarouselHeader.spec.lua @@ -0,0 +1,40 @@ +return function() + local Carousel = script.Parent + local Container = Carousel.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local CarouselHeader = require(script.Parent.CarouselHeader) + + describe("should create and destroy CarouselHeader with default props without errors", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + Item = Roact.createElement(CarouselHeader) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) + + describe("should create and destroy CarouselHeader without errors", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + Item = Roact.createElement(CarouselHeader, { + headerText = "test header", + onSeeAll = function() end, + carouselMargin = 12, + layoutOrder = 1, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/FreeFlowCarousel.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/FreeFlowCarousel.lua new file mode 100644 index 0000000..f907657 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/FreeFlowCarousel.lua @@ -0,0 +1,96 @@ +local Carousel = script.Parent +local Container = Carousel.Parent +local App = Container.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local FitFrame = require(Packages.FitFrame) +local FitFrameOnAxis = FitFrame.FitFrameOnAxis + +local CarouselHeader = require(Carousel.CarouselHeader) +local HorizontalCarousel = require(Carousel.HorizontalCarousel) + +local DEFAULT_INNER_PADDING = 12 +local DEFAULT_ITEM_PADDING = 12 +local DEFAULT_MARGIN = 24 + +local FreeFlowCarousel = Roact.PureComponent:extend("FreeFlowCarousel") + +FreeFlowCarousel.validateProps = t.strictInterface({ + -- A function to uniquely identify list items. Calling this on the same item twice + -- should give the same result according to ==. + -- See infinite scroller for more details. + identifier = t.optional(t.callback), + + -- The header text for the carousel + headerText = t.optional(t.string), + + -- The callback for the see all arrow. if nil, the arrow won't be shown + onSeeAll = t.optional(t.callback), + + -- The list for the items in the carousel + itemList = t.array(t.any), + + -- A callback function, called with each visible item in the itemList when the list is rendered. + renderItem = t.callback, + + -- The size of the item + itemSize = t.optional(t.Vector2), + + -- The padding between items + itemPadding = t.optional(t.number), + + -- The carousel margin + carouselMargin = t.optional(t.number), + + -- The inner padding between the header and the carousel + innerPadding = t.optional(t.number), + + -- The layoutOrder + layoutOrder = t.optional(t.integer), + + -- A callback function, called when the infinite scroll reaches the leading end of the itemList (index + -- #itemList). + loadNext = t.optional(t.callback), +}) + +FreeFlowCarousel.defaultProps = { + headerText = "", + innerPadding = DEFAULT_INNER_PADDING, + itemPadding = DEFAULT_ITEM_PADDING, + carouselMargin = DEFAULT_MARGIN, +} + +function FreeFlowCarousel:render() + local innerPadding = self.props.innerPadding + local carouselMargin = self.props.carouselMargin + + return Roact.createElement(FitFrameOnAxis, { + axis = FitFrameOnAxis.Axis.Vertical, + minimumSize = UDim2.fromScale(1, 0), + LayoutOrder = self.props.layoutOrder, + contentPadding = UDim.new(0, innerPadding), + BackgroundTransparency = 1, + }, { + CarouselHeader = Roact.createElement(CarouselHeader, { + headerText = self.props.headerText, + onSeeAll = self.props.onSeeAll, + carouselMargin = carouselMargin, + layoutOrder = 1, + }), + Carousel = Roact.createElement(HorizontalCarousel, { + identifier = self.props.identifier, + itemList = self.props.itemList, + renderItem = self.props.renderItem, + itemSize = self.props.itemSize, + itemPadding = innerPadding, + carouselMargin = carouselMargin, + layoutOrder = 2, + loadNext = self.props.loadNext, + }), + }) +end + +return FreeFlowCarousel \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/FreeFlowCarousel.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/FreeFlowCarousel.spec.lua new file mode 100644 index 0000000..3e33012 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/FreeFlowCarousel.spec.lua @@ -0,0 +1,84 @@ +return function() + local Carousel = script.Parent + local Container = Carousel.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local FreeFlowCarousel = require(script.Parent.FreeFlowCarousel) + + describe("should create and destroy with required props without errors", function() + it("should mount and unmount without issue", function() + local items = {} + for i=1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + + local renderItem = function(props) + return Roact.createElement("TextLabel", props) + end + + local element = mockStyleComponent({ + Item = Roact.createElement(FreeFlowCarousel, { + itemList = items, + renderItem = renderItem, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) + + describe("should create and destroy FreeFlowCarousel without errors", function() + it("should mount and unmount without issue", function() + local items = {} + for i=1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + + local renderItem = function(props) + return Roact.createElement("TextLabel", props) + end + + local loadNext = function() + for i=1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + end + + local element = mockStyleComponent({ + Item = Roact.createElement(FreeFlowCarousel, { + identifier = function(item) + return tostring(item) + end, + headerText = "test header", + onSeeAll = function()end, + itemList = items, + renderItem = renderItem, + itemSize = Vector2.new(100, 100), + itemPadding = 12, + carouselMargin = 36, + layoutOrder = 1, + loadNext = loadNext, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/HorizontalCarousel.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/HorizontalCarousel.lua new file mode 100644 index 0000000..c811b92 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/HorizontalCarousel.lua @@ -0,0 +1,269 @@ +local Carousel = script.Parent +local Container = Carousel.Parent +local App = Container.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(App.ImageSet.Images) + +local ScrollButton = require(Carousel.ScrollButton) + +local Core = UIBlox.Core +local Scroller = require(Core.InfiniteScroller).Scroller + +local DEFAULT_ITEM_PADDING = 12 + +local LEFT_ICON = Images["icons/actions/cycleLeft"] +local RIGHT_ICON = Images["icons/actions/cycleRight"] + +local MOTOR_OPTIONS = { + frequency = 2, + dampingRatio = 0.9, + restingPositionLimit = 0.5, + restingVelocityLimit = 0.1, +} + +local HorizontalCarousel = Roact.PureComponent:extend("HorizontalCarousel") + +HorizontalCarousel.validateProps = t.strictInterface({ + -- Required. The list of items to scroll through. + itemList = t.array(t.any), + + -- A callback function, called with each visible item in the itemList when the list is rendered. + renderItem = t.callback, + + -- A function to uniquely identify list items. Calling this on the same item twice + -- should give the same result according to ==. + -- See infinite scroller for more details. + identifier = t.optional(t.callback), + + -- The size of the item + itemSize = t.optional(t.Vector2), + + -- The padding between items + itemPadding = t.optional(t.number), + + -- The carousel margin + carouselMargin = t.optional(t.number), + + -- The layoutOrder + layoutOrder = t.optional(t.integer), + + -- A callback function, called when the carousel reaches the leading end of the itemList (index + -- #itemList). + loadNext = t.optional(t.callback), + + -- A callback function, called when the carousel reaches the trailing end of the itemList (index 1). + loadPrevious = t.optional(t.callback), + + -- Animate the scrolling + animateScrolling = t.optional(t.boolean), +}) + +HorizontalCarousel.defaultProps = { + itemSize = Vector2.new(1, 1), + itemPadding = DEFAULT_ITEM_PADDING, +} + +local function updateScrollState(newIndex, numberOfItemsShown, numOfItems, scrollerFocusLock) + if newIndex == nil then + return {} + end + + --Disable the buttons because there is nothing to show + if numOfItems == nil or numOfItems == 0 then + return { + showLeftButton = false, + showRightButton = false, + } + end + + local targetIndex = newIndex + local showLeftButton = true + local showRightButton = true + + if newIndex <= 1 then + -- If scrolling pass the 1st element then reset the target index to the first item + targetIndex = 1 + scrollerFocusLock = scrollerFocusLock + 1 + showLeftButton = false + elseif newIndex > numOfItems then + -- If scrolling pass the last element then reset the target index to the last item + targetIndex = numOfItems + scrollerFocusLock = scrollerFocusLock + 1 + showRightButton = false + elseif newIndex + numberOfItemsShown > numOfItems then + -- There is no more items outside of the carousel then hide the scroll button + -- There is also no need to update the scrollerFocusLock or target in this case + showRightButton = false + end + + return { + scrollerFocusLock = scrollerFocusLock, + index = targetIndex, + showLeftButton = showLeftButton, + showRightButton = showRightButton, + numOfItems = numOfItems, + } +end + +function HorizontalCarousel.getDerivedStateFromProps(nextProps, lastState) + local numOfItems = #nextProps.itemList + if lastState.numOfItems ~= numOfItems then + return updateScrollState(lastState.index, lastState.numberOfItemsShown, numOfItems, lastState.scrollerFocusLock) + end + return nil +end + +function HorizontalCarousel:init() + self.frameRef = Roact.createRef() + local carouselMetaData = {} + + self:setState({ + scrollerFocusLock = 0, + index = 1, + hovering = false, + showLeftButton = false, + showRightButton = false, + numberOfItemsShown = 0, + numOfItems = 0, + }) + + self.onMouseEnter = function(gui, input) + if input.UserInputType == Enum.UserInputType.MouseMovement then + local anchorIndex = carouselMetaData.anchorIndex + local newState = updateScrollState(anchorIndex, + self.state.numberOfItemsShown, + self.state.numOfItems, + self.state.scrollerFocusLock) + newState.hovering = true + self:setState(newState) + end + end + + self.onMouseLeave = function(gui, input) + if input.UserInputType == Enum.UserInputType.MouseMovement then + self:setState({ + hovering = false, + }) + end + end + + self.onResize = function(rbx) + local totalWidth = rbx.AbsoluteSize.X + local numberOfItemsShown = math.floor(totalWidth / (self.props.itemSize.X + self.props.itemPadding)) + self:setState({ + numberOfItemsShown = numberOfItemsShown, + }) + end + + self.onScrollUpdate = function(data) + carouselMetaData = data + end + + self.scrollLeft = function() + if carouselMetaData.animationActive then + return + end + local newIndex = carouselMetaData.anchorIndex - self.state.numberOfItemsShown + self:setState( + updateScrollState(newIndex, self.state.numberOfItemsShown, self.state.numOfItems, self.state.scrollerFocusLock + 1) + ) + end + + self.scrollRight = function() + if carouselMetaData.animationActive then + return + end + local newIndex = carouselMetaData.anchorIndex + self.state.numberOfItemsShown + self:setState( + updateScrollState(newIndex, self.state.numberOfItemsShown, self.state.numOfItems, self.state.scrollerFocusLock + 1) + ) + end +end + +function HorizontalCarousel:render() + local itemList = self.props.itemList + local itemSize = self.props.itemSize + local renderItem = self.props.renderItem + local itemPadding = self.props.itemPadding + + local carouselMargin = self.props.carouselMargin + local layoutOrder = self.props.layoutOrder + + local loadNext = self.props.loadNext + local loadPrevious = self.props.loadPrevious + + local scrollLeftButton + if self.state.hovering and self.state.showLeftButton then + scrollLeftButton = Roact.createElement(ScrollButton, { + icon = LEFT_ICON, + callback = self.scrollLeft, + }) + end + + local scrollRightButton + if self.state.hovering and self.state.showRightButton then + scrollRightButton = Roact.createElement(ScrollButton, { + icon = RIGHT_ICON, + callback = self.scrollRight, + }) + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, itemSize.Y), + LayoutOrder = layoutOrder, + BackgroundTransparency = 1, + [Roact.Ref] = self.frameRef, + [Roact.Event.InputBegan] = self.onMouseEnter, + [Roact.Event.InputEnded] = self.onMouseLeave, + [Roact.Change.AbsoluteSize] = self.onResize, + },{ + LeftMargin = Roact.createElement("Frame", { + Position = UDim2.fromScale(0, 0), + AnchorPoint = Vector2.new(0, 0), + Size = UDim2.new(0, carouselMargin, 1, 0), + BackgroundTransparency = 1, + ZIndex = 2, + },{ + ScrollLeftButton = scrollLeftButton, + }), + InfiniteScrollerCarousel = self.state.numberOfItemsShown > 0 and Roact.createElement(Scroller, { + identifier = self.props.identifier, + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromOffset(carouselMargin, 0), + ScrollBarThickness = 0, + ClipsDescendants = false, + padding = UDim.new(0, itemPadding), + orientation = Scroller.Orientation.Right, + itemList = itemList, + loadingBuffer = 1, + mountingBuffer = self.state.numberOfItemsShown * 3 * itemSize.X, + loadNext = loadNext, + loadPrevious = loadPrevious, + focusLock = self.state.scrollerFocusLock, + focusIndex = self.state.index, + anchorLocation = UDim.new(1, 0), + estimatedItemSize = itemSize.X, + renderItem = renderItem, + onScrollUpdate = self.onScrollUpdate, + animateScrolling = true, + animateOptions = MOTOR_OPTIONS, + }) or nil, + RightMargin = Roact.createElement("Frame", { + Position = UDim2.fromScale(1, 0), + AnchorPoint = Vector2.new(1, 0), + Size = UDim2.new(0, carouselMargin, 1, 0), + BackgroundTransparency = 1, + ZIndex = 2, + },{ + ScrollRightButton = scrollRightButton, + }), + }) +end + +return HorizontalCarousel diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/HorizontalCarousel.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/HorizontalCarousel.spec.lua new file mode 100644 index 0000000..1e5db2e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/HorizontalCarousel.spec.lua @@ -0,0 +1,93 @@ +return function() + local Carousel = script.Parent + local Container = Carousel.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local HorizontalCarousel = require(script.Parent.HorizontalCarousel) + + describe("should create and destroy with required props without errors", function() + it("should mount and unmount without issue", function() + local items = {} + for i=1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + + local renderItem = function(props) + return Roact.createElement("TextLabel", props) + end + + local element = mockStyleComponent({ + Item = Roact.createElement(HorizontalCarousel, { + itemList = items, + renderItem = renderItem, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) + + describe("should create and destroy HorizontalCarousel without errors", function() + it("should mount and unmount without issue", function() + local items = {} + for i=1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + + local renderItem = function(props) + return Roact.createElement("TextLabel", props) + end + + local loadNext = function() + for i=1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + end + + local loadPrevious = function() + items = {} + for i=1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + end + + local element = mockStyleComponent({ + Item = Roact.createElement(HorizontalCarousel, { + identifier = function(item) + return tostring(item) + end, + itemList = items, + renderItem = renderItem, + itemSize = Vector2.new(100, 100), + itemPadding = 12, + carouselMargin = 36, + layoutOrder = 1, + loadNext = loadNext, + loadPrevious = loadPrevious, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/ScrollButton.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/ScrollButton.lua new file mode 100644 index 0000000..d535bd5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/ScrollButton.lua @@ -0,0 +1,53 @@ +local Carousel = script.Parent +local Container = Carousel.Parent +local App = Container.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Style.withStyle) + +local Core = UIBlox.Core +local getIconSize = require(App.ImageSet.getIconSize) +local IconSize = require(App.ImageSet.Enum.IconSize) +local Interactable = require(Core.Control.Interactable) +local ImageSetComponent = require(Core.ImageSet.ImageSetComponent) + +local ScrollButton = Roact.PureComponent:extend("ScrollButton") + +ScrollButton.validateProps = t.strictInterface({ + -- The icon of the button + icon = t.table, + + -- Callback action + callback = t.callback, +}) + +function ScrollButton:render() + return withStyle(function(style) + return Roact.createElement(Interactable, { + AutoButtonColor = false, + Size = UDim2.fromScale(1, 1), + BackgroundColor3 = style.Theme.BackgroundUIContrast.Color, + BackgroundTransparency = style.Theme.BackgroundUIContrast.Transparency, + BorderSizePixel = 0, + [Roact.Event.Activated] = self.props.callback, + --Note State change is not being used right now. + onStateChanged = function()end, + }, { + Icon = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(getIconSize(IconSize.Medium), getIconSize(IconSize.Medium)), + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = self.props.icon, + ImageColor3 = style.Theme.IconEmphasis.Color, + ImageTransparency = style.Theme.IconEmphasis.Transparency, + }), + }) + + end) +end + +return ScrollButton \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/ScrollButton.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/ScrollButton.spec.lua new file mode 100644 index 0000000..67a75cd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/ScrollButton.spec.lua @@ -0,0 +1,29 @@ +return function() + local Carousel = script.Parent + local Container = Carousel.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local Images = require(App.ImageSet.Images) + local ScrollButton = require(script.Parent.ScrollButton) + local icon = Images["icons/actions/cycleLeft"] + + it("should create and destroy ScrollButton without errors", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + Item = Roact.createElement(ScrollButton, { + icon = icon, + callback = function()end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/__stories__/FreeFlowCarousel.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/__stories__/FreeFlowCarousel.story.lua new file mode 100644 index 0000000..8adaee4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/Carousel/__stories__/FreeFlowCarousel.story.lua @@ -0,0 +1,64 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local Carousel = script.Parent.Parent +local Container = Carousel.Parent +local App = Container.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local FreeFlowCarousel = require(Carousel.FreeFlowCarousel) + +return function(target) + + local items = {} + for i = 1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + + local renderItem = function(props) + return Roact.createElement("TextLabel", props) + end + + local loadNext = function() + for i = 1, 10 do + table.insert(items, { + Text = i, + Size = UDim2.fromOffset(100, 100), + }) + end + end + + local element = Roact.createElement(StoryItem, { + size = UDim2.fromScale(1, 1), + title = "Carousel", + subTitle = "App.Container.Carousel.FreeFlowCarousel", + }, { + Roact.createElement(FreeFlowCarousel, { + identifier = function(item) + return tostring(item) + end, + headerText = "test header", + onSeeAll = function()end, + itemList = items, + renderItem = renderItem, + itemSize = Vector2.new(100, 100), + itemPadding = 12, + carouselMargin = 36, + layoutOrder = 1, + loadNext = loadNext, + }) + }) + + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = element + }), target, "Carousel") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/FailedStatePage.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/FailedStatePage.lua new file mode 100644 index 0000000..1d1231f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/FailedStatePage.lua @@ -0,0 +1,61 @@ +local Indicator = script.Parent +local App = Indicator.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(UIBlox.App.ImageSet.Images) +local EmptyState = require(UIBlox.App.Indicator.EmptyState) +local SecondaryButton = require(UIBlox.App.Button.SecondaryButton) + +local RenderOnFailedStyle = require(UIBlox.App.Loading.Enum.RenderOnFailedStyle) + +local RETRY_BUTTON_HEIGHT = 44 +local RETRY_BUTTON_WIDTH = 44 +local RETRY_BACKGROUND_IMAGE = "icons/common/refresh" +local ICON = 'icons/status/noconnection_large' + +local FailedStatePage = Roact.PureComponent:extend("FailedStatePage") + +FailedStatePage.validateProps = t.strictInterface({ + -- The onRetry function is called when a button is pressed + onRetry = t.optional(t.callback), + -- The renderOnFailed renders a page from a RenderOnFailedStyle enum + renderOnFailed = t.optional(RenderOnFailedStyle.isEnumValue), + -- text for emptystate + text = t.string, +}) + +FailedStatePage.defaultProps = { + renderOnFailed = RenderOnFailedStyle.RetryButton, +} + +function FailedStatePage:render() + if self.props.renderOnFailed == RenderOnFailedStyle.EmptyStatePage then + return Roact.createElement(EmptyState, { + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + onActivated = self.props.onRetry, + icon = Images[ICON], + text = self.props.text, + }) + elseif self.props.renderOnFailed == RenderOnFailedStyle.RetryButton then + if self.props.onRetry then + return Roact.createElement(SecondaryButton, { + size = UDim2.fromOffset(RETRY_BUTTON_HEIGHT, RETRY_BUTTON_WIDTH), + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + onActivated = self.props.onRetry, + icon = Images[RETRY_BACKGROUND_IMAGE], + }) + else + error("OnRetry callback empty. OnRetry needs to be a function to render the RetryButton") + end + else + error("Failed to provide proper RenderOnFailedStyle") + end +end + +return FailedStatePage diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/FailedStatePage.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/FailedStatePage.spec.lua new file mode 100644 index 0000000..546e5e4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/FailedStatePage.spec.lua @@ -0,0 +1,36 @@ +return function() + local Container = script.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local FailedStatePage = require(script.Parent.FailedStatePage) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + local RenderOnFailedStyle = require(UIBlox.App.Loading.Enum.RenderOnFailedStyle) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + it("should mount and unmount with the optional props", function() + local element = mockStyleComponent({ + failedStatePage = Roact.createElement(FailedStatePage, { + onRetry = function() end, + renderOnFailed = RenderOnFailedStyle.RetryButton, + text = "Failed" + }) + }) + local instance = Roact.mount(element, frame, "FailedStatePage") + Roact.unmount(instance) + end) + it("should error when RetryButton passed without onRetry callback", function() + local element = mockStyleComponent({ + failedStatePage = Roact.createElement(FailedStatePage, { + renderOnFailed = RenderOnFailedStyle.RetryButton, + text = "Failed" + }) + }) + expect(function() + return Roact.mount(element, frame, "FailedStatePage") + end).to.throw() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateContainer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateContainer.lua new file mode 100644 index 0000000..d5793e5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateContainer.lua @@ -0,0 +1,110 @@ +local Container = script.Parent +local App = Container.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local RetrievalStatus = require(UIBlox.App.Loading.Enum.RetrievalStatus) +local LoadingStateEnum = require(UIBlox.App.Loading.Enum.LoadingState) +local ReloadingStyle = require(UIBlox.App.Loading.Enum.ReloadingStyle) +local RenderOnFailedStyle = require(UIBlox.App.Loading.Enum.RenderOnFailedStyle) +local LoadingStatePage = require(UIBlox.App.Container.LoadingStatePage) +local FailedStatePage = require(UIBlox.App.Container.FailedStatePage) +local LoadingStateTables = require(UIBlox.App.Container.LoadingStateTables) + +local INITIAL_RELOADING_STYLE = ReloadingStyle.AllowReload +local INITIAL_LOADING_STATE = LoadingStateEnum.Loading + +local LoadingStateContainer = Roact.PureComponent:extend("LoadingStateContainer") + +LoadingStateContainer.validateProps = t.strictInterface({ + -- The dataStatus is a retrieval status enum + dataStatus = RetrievalStatus.isEnumValue, + -- Text for FailedStatePage EmptyState + failedText = t.string, + -- The onRetry function is called when a button is pressed + onRetry = t.optional(t.callback), + -- The renderOnLoaded is rendered if loading state is LoadingStateEnum.Loaded + renderOnLoaded = t.callback, + -- The renderOnFailed is rendered if loading state is LoadingStateEnum.Failed or is a RenderOnFailedStyle Enum. + renderOnFailed = t.optional(t.union(RenderOnFailedStyle.isEnumValue, t.callback)), + -- The renderOnLoading is rendered if loading state is LoadingStateEnum.Loading + renderOnLoading = t.optional(t.callback), + -- The reloadingStyle is the style of state table + reloadingStyle = t.optional(ReloadingStyle.isEnumValue), +}) + +LoadingStateContainer.defaultProps = { + renderOnFailed = RenderOnFailedStyle.RetryButton, + reloadingStyle = INITIAL_RELOADING_STYLE, +} + +function LoadingStateContainer:init() + self.onStateChange = function(oldState, newState) + self:setState({ + loadingState = newState, + }) + end + + self.updateState = function() + self.state.currentReloadingStyle:onStateChange(self.onStateChange) + self.state.currentReloadingStyle.events[self.props.dataStatus]() + end + + self:setState({ + loadingState = INITIAL_LOADING_STATE, + currentReloadingStyle = LoadingStateTables[INITIAL_RELOADING_STYLE](), + }) + + self.statePages = { + [LoadingStateEnum.Loading] = function() + if self.props.renderOnLoading then + return self.props.renderOnLoading() + else + return Roact.createElement(LoadingStatePage) + end + end, + [LoadingStateEnum.Failed] = function() + if t.callback(self.props.renderOnFailed) then + return self.props.renderOnFailed() + else + return Roact.createElement(FailedStatePage, { + onRetry = self.props.onRetry, + renderOnFailed = self.props.renderOnFailed, + text = self.props.failedText, + }) + end + end, + [LoadingStateEnum.Loaded] = function() + return self.props.renderOnLoaded() + end, + } +end + +function LoadingStateContainer.getDerivedStateFromProps(nextProps, lastState) + if lastState.currentReloadingStyle ~= nil and lastState.currentReloadingStyle ~= nextProps.reloadingStyle then + return { + --reloadingStyle = nextProps.reloadingStyle, + currentReloadingStyle = LoadingStateTables[nextProps.reloadingStyle]() + } + end +end + +function LoadingStateContainer:render() + return self.statePages[self.state.loadingState]() +end + +function LoadingStateContainer:didMount() + self.updateState() +end + +function LoadingStateContainer:didUpdate(prevProps) + if prevProps.dataStatus ~= self.props.dataStatus or prevProps.reloadingStyle ~= self.props.reloadingStyle then + self.updateState() + end +end + + +return LoadingStateContainer diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateContainer.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateContainer.spec.lua new file mode 100644 index 0000000..8cdd08e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateContainer.spec.lua @@ -0,0 +1,99 @@ +return function() + local Container = script.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local RetrievalStatus = require(UIBlox.App.Loading.Enum.RetrievalStatus) + local ReloadingStyle = require(UIBlox.App.Loading.Enum.ReloadingStyle) + local RenderOnFailedStyle = require(UIBlox.App.Loading.Enum.RenderOnFailedStyle) + local LoadingStateContainer = require(script.Parent.LoadingStateContainer) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + + it("should mount and unmount with only required props", function() + local element = mockStyleComponent({ + loadingStateContainer = Roact.createElement(LoadingStateContainer, { + renderOnLoaded = function() end, + dataStatus = RetrievalStatus.NotStarted, + failedText = "failed", + }) + }) + local instance = Roact.mount(element, frame, "LoadingStateContainer") + Roact.unmount(instance) + end) + + it("should mount and unmount with all props", function() + local element = mockStyleComponent({ + loadingStateContainer = Roact.createElement(LoadingStateContainer, { + dataStatus = RetrievalStatus.NotStarted, + onRetry = function() end, + renderOnLoaded = function() end, + renderOnFailed = function() end, + renderOnLoading = function() end, + reloadingStyle = ReloadingStyle.AllowReload, + failedText = "failed", + }) + }) + local instance = Roact.mount(element, frame, "LoadingStateContainer") + Roact.unmount(instance) + end) + + it("should render loading state page", function() + local element = mockStyleComponent({ + loadingStateContainer = Roact.createElement(LoadingStateContainer, { + dataStatus = RetrievalStatus.Fetching, + onRetry = function() end, + renderOnLoaded = function() end, + renderOnFailed = RenderOnFailedStyle.EmptyStatePage, + reloadingStyle = ReloadingStyle.AllowReload, + failedText = "failed", + }) + }) + local instance = Roact.mount(element, frame, "LoadingStateContainer") + local loadingIcon = frame:FindFirstChild("inner", true) + expect(loadingIcon).to.be.ok() + Roact.unmount(instance) + end) + + it("should render failed state page emptyState", function() + local element = mockStyleComponent({ + loadingStateContainer = Roact.createElement(LoadingStateContainer, { + dataStatus = RetrievalStatus.Failed, + onRetry = function() end, + renderOnLoaded = function() end, + renderOnFailed = RenderOnFailedStyle.EmptyStatePage, + reloadingStyle = ReloadingStyle.AllowReload, + failedText = "failed", + }) + }) + local instance = Roact.mount(element, frame, "LoadingStateContainer") + local content = frame:FindFirstChild("Content", true) + local buttonFrame = content:FindFirstChild("buttonFrame", true) + + expect(buttonFrame).to.be.ok() + Roact.unmount(instance) + end) + + it("should render failed state page RetryButton", function() + local element = mockStyleComponent({ + loadingStateContainer = Roact.createElement(LoadingStateContainer, { + dataStatus = RetrievalStatus.Failed, + onRetry = function() end, + renderOnLoaded = function() end, + renderOnFailed = RenderOnFailedStyle.RetryButton, + reloadingStyle = ReloadingStyle.AllowReload, + failedText = "failed", + }) + }) + local instance = Roact.mount(element, frame, "LoadingStateContainer") + local ButtonContent = frame:FindFirstChild("ButtonContent", true) + + expect(ButtonContent).to.be.ok() + Roact.unmount(instance) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStatePage.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStatePage.lua new file mode 100644 index 0000000..c9bfc55 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStatePage.lua @@ -0,0 +1,20 @@ +local Indicator = script.Parent +local App = Indicator.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local LoadingSpinner = require(UIBlox.App.Loading.LoadingSpinner) + +local LoadingStatePage = Roact.PureComponent:extend("LoadingStatePage") + +function LoadingStatePage:render() + return Roact.createElement(LoadingSpinner, { + size = UDim2.fromOffset(42, 42), + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + }) +end + +return LoadingStatePage diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStatePage.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStatePage.spec.lua new file mode 100644 index 0000000..37c497b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStatePage.spec.lua @@ -0,0 +1,21 @@ +return function() + local Container = script.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local LoadingStatePage = require(script.Parent.LoadingStatePage) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + it("should mount and unmount", function() + local element = mockStyleComponent({ + loadingStatePage = Roact.createElement(LoadingStatePage) + }) + local instance = Roact.mount(element, frame, "LoadingStatePage") + Roact.unmount(instance) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateTables.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateTables.lua new file mode 100644 index 0000000..da1f7e1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/LoadingStateTables.lua @@ -0,0 +1,62 @@ +local Container = script.Parent +local App = Container.Parent +local UIBlox = App.Parent + +local StateTable = require(UIBlox.StateTable.StateTable) +local ReloadingStyle = require(UIBlox.App.Loading.Enum.ReloadingStyle) +local RetrievalStatus = require(UIBlox.App.Loading.Enum.RetrievalStatus) +local LoadingStateEnum = require(UIBlox.App.Loading.Enum.LoadingState) + +local INITIAL_LOADING_STATE = LoadingStateEnum.Loading + +local LoadingStateTables = {} + +-- This state table allows reloading after it has loaded +LoadingStateTables[ReloadingStyle.AllowReload] = function() + return StateTable.new('AllowReload', INITIAL_LOADING_STATE, {}, { + [LoadingStateEnum.Loading] = { + [RetrievalStatus.NotStarted] = {}, + [RetrievalStatus.Fetching] = {}, + [RetrievalStatus.Done] = { nextState = LoadingStateEnum.Loaded }, + [RetrievalStatus.Failed] = { nextState = LoadingStateEnum.Failed }, + }, + [LoadingStateEnum.Loaded] = { + [RetrievalStatus.NotStarted] = {}, + [RetrievalStatus.Fetching] = { nextState = LoadingStateEnum.Loading }, + [RetrievalStatus.Done] = {}, + [RetrievalStatus.Failed] = { nextState = LoadingStateEnum.Failed }, + }, + [LoadingStateEnum.Failed] = { + [RetrievalStatus.NotStarted] = {}, + [RetrievalStatus.Fetching] = { nextState = LoadingStateEnum.Loading }, + [RetrievalStatus.Done] = { nextState = LoadingStateEnum.Loaded }, + [RetrievalStatus.Failed] = {}, + }, + }) +end + +-- This state table locks reloading after it has loaded. +LoadingStateTables[ReloadingStyle.LockReload] = function() + return StateTable.new('LockReload', INITIAL_LOADING_STATE, {}, { + [LoadingStateEnum.Loading] = { + [RetrievalStatus.NotStarted] = {}, + [RetrievalStatus.Fetching] = {}, + [RetrievalStatus.Done] = { nextState = LoadingStateEnum.Loaded }, + [RetrievalStatus.Failed] = { nextState = LoadingStateEnum.Failed }, + }, + [LoadingStateEnum.Loaded] = { + [RetrievalStatus.NotStarted] = {}, + [RetrievalStatus.Fetching] = { nextState = LoadingStateEnum.Failed }, + [RetrievalStatus.Done] = {}, + [RetrievalStatus.Failed] = { nextState = LoadingStateEnum.Failed }, + }, + [LoadingStateEnum.Failed] = { + [RetrievalStatus.NotStarted] = {}, + [RetrievalStatus.Fetching] = { nextState = LoadingStateEnum.Loading }, + [RetrievalStatus.Done] = { nextState = LoadingStateEnum.Loaded }, + [RetrievalStatus.Failed] = {}, + }, + }) +end + +return LoadingStateTables diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/VerticalScrollView.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/VerticalScrollView.lua new file mode 100644 index 0000000..6117802 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/VerticalScrollView.lua @@ -0,0 +1,176 @@ +local RunService = game:GetService("RunService") +local UserInputService = game:GetService("UserInputService") + +local App = script.Parent.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Cryo = require(Packages.Cryo) +local Otter = require(Packages.Otter) +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local withStyle = require(Packages.UIBlox.Core.Style.withStyle) + +local PADDING_HORIZONTAL = 24 +local SCROLL_BAR_RIGHT_PADDING = 4 +local MOUSE_SCROLL_BAR_THICKNESS = 8 +local TOUCH_OR_CONTROLLER_SCROLL_BAR_THICKNESS = 2 +local CANVAS_SIZE_X = UDim.new(0, 0) +local HIDE_SIDEBAR_AFTER_IN_SECONDS = 0.70 +local SPRING_PARAMETERS = { + frequency = 3, + dampingRatio = 1.5, +} + +local VerticalScrollView = Roact.Component:extend() + +VerticalScrollView.defaultProps = { + -- Frame Props + size = UDim2.new(1, 0, 1, 0), + -- ScrollingFrame Props + canvasSizeY = UDim.new(2, 0), + paddingHorizontal = PADDING_HORIZONTAL, +} + +VerticalScrollView.validateProps = t.strictInterface({ + -- Frame Props + size = t.optional(t.UDim2), + position = t.optional(t.UDim2), + elasticBehavior = t.optional(t.EnumItem), + + -- ScrollingFrame Props + canvasSizeY = t.optional(t.UDim), + paddingHorizontal = t.optional(t.numberMin(PADDING_HORIZONTAL/2)), + + -- Optional passthrough props for the scrolling frame + [Roact.Change.CanvasPosition] = t.optional(t.callback), + [Roact.Change.CanvasSize] = t.optional(t.callback), + [Roact.Ref] = t.optional(t.table), + + -- Children + [Roact.Children] = t.optional(t.table) +}) + +function VerticalScrollView:init() + self:setState({ + scrollBarThickness = 0, + scrollingWithTouch = false, + }) + + self.scrollBarImageTransparency, self.updateScrollBarImageTransparency = Roact.createBinding(0) + self.scrollBarImageTransparencyMotor = Otter.createSingleMotor(0) + self.scrollBarImageTransparencyMotor:onStep(self.updateScrollBarImageTransparency) + + self.lastTimeCanvasPositionChanged = tick() + + self.waitToHideSidebarConnection = nil + self.waitToHideSidebar = function() + local currentTime = tick() + local delta = currentTime - self.lastTimeCanvasPositionChanged + if delta > HIDE_SIDEBAR_AFTER_IN_SECONDS then + self.scrollBarImageTransparencyMotor:setGoal(Otter.spring(1, SPRING_PARAMETERS)) + self.disconnectWaitToHideSidebar() + end + end + self.disconnectWaitToHideSidebar = function() + if self.waitToHideSidebarConnection then + self.waitToHideSidebarConnection:Disconnect() + self.waitToHideSidebarConnection = nil + end + end + self.inputBegan = function(instance, input) + if input.UserInputType == Enum.UserInputType.MouseMovement then + self.disconnectWaitToHideSidebar() + self:setState({ + scrollBarThickness = MOUSE_SCROLL_BAR_THICKNESS, + }) + self.scrollBarImageTransparencyMotor:setGoal(Otter.instant(0)) + end + end + self.inputEnded = function(instance, input) + if input.UserInputType == Enum.UserInputType.MouseMovement then + self.disconnectWaitToHideSidebar() + self.scrollBarImageTransparencyMotor:setGoal(Otter.instant(1)) + end + end + self.canvasPosition = function(rbx) + self.lastTimeCanvasPositionChanged = tick() + if not self.waitToHideSidebarConnection and + UserInputService:GetLastInputType() == Enum.UserInputType.Touch + then + self.scrollBarImageTransparencyMotor:setGoal(Otter.instant(0)) + self:setState({ + scrollBarThickness = TOUCH_OR_CONTROLLER_SCROLL_BAR_THICKNESS, + }) + self.waitToHideSidebarConnection = RunService.Heartbeat:Connect(self.waitToHideSidebar) + end + + if self.props[Roact.Change.CanvasPosition] then + self.props[Roact.Change.CanvasPosition](rbx) + end + end +end + +function VerticalScrollView:render() + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + local canvasSizeY = self.props.canvasSizeY + local children = self.props[Roact.Children] or {} + local position = self.props.position + local size = self.props.size + local paddingHorizontal = self.props.paddingHorizontal + + local scrollBarThickness = self.state.scrollBarThickness + + local scrollingFrameChildren = Cryo.Dictionary.join({ + scrollingFrameInnerMargin = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0,paddingHorizontal), + PaddingRight = UDim.new(0, paddingHorizontal - SCROLL_BAR_RIGHT_PADDING), }), + }, + children + ) + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Position = position, + Size = size, + }, { + scrollingFrameOuterMargins = Roact.createElement("UIPadding", { + PaddingRight = UDim.new(0, SCROLL_BAR_RIGHT_PADDING), + }), + scrollingFrame = Roact.createElement("ScrollingFrame", { + Active = true, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.fromScale(1, 1), + ElasticBehavior = self.props.elasticBehavior, + -- ScrollingFrame Specific + CanvasSize = UDim2.new(CANVAS_SIZE_X, canvasSizeY), + ScrollBarImageColor3 = theme.UIEmphasis.Color, + ScrollBarImageTransparency = self.scrollBarImageTransparency, + ScrollBarThickness = scrollBarThickness, + ScrollingDirection = Enum.ScrollingDirection.Y, + + -- https://jira.rbx.com/browse/MOBLUAPP-2451 + -- TODO: 1.) Currently code assumes that Mouse is on desktop and touch is on mobile + -- On a mac touch pad is reported as mouse not as touch + -- No sure how many users use mouse on a phone + -- TODO: 2.) how to handle controller actions - when we do this, + -- we should make this part of the code platform specific + [Roact.Event.InputBegan] = self.inputBegan, + [Roact.Event.InputEnded] = self.inputEnded, + [Roact.Change.CanvasPosition] = self.canvasPosition, + [Roact.Change.CanvasSize] = self.props[Roact.Change.CanvasSize], + [Roact.Ref] = self.props[Roact.Ref], + }, scrollingFrameChildren) + }) + end) +end + +function VerticalScrollView:willUnmount() + self.disconnectWaitToHideSidebar() +end + +return VerticalScrollView diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/VerticalScrollView.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/VerticalScrollView.spec.lua new file mode 100644 index 0000000..6b5366c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/VerticalScrollView.spec.lua @@ -0,0 +1,57 @@ +return function() + local Container = script.Parent + local App = Container.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local VerticalScrollView = require(Container.VerticalScrollView) + + describe("mount/unmount", function() + it("should mount and unmount with default properties", function() + local verticalScrollViewWithStyle = mockStyleComponent({ + verticalScrollView = Roact.createElement(VerticalScrollView) + }) + local handle = Roact.mount(verticalScrollViewWithStyle) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + it("should mount and unmount with valid properties", function() + local verticalScrollViewWithStyle = mockStyleComponent({ + verticalScrollView = Roact.createElement(VerticalScrollView, { + position = UDim2.new(0, 50, 0,100), + size = UDim2.new(1, 30, 1, 50), + canvasSizeY = UDim.new(2, 0), + paddingHorizontal = 12, + elasticBehavior = Enum.ElasticBehavior.Always, + + [Roact.Change.CanvasPosition] = function()end, + [Roact.Change.CanvasSize] = function()end, + [Roact.Ref] = Roact.createRef(), + }) + }) + local handle = Roact.mount(verticalScrollViewWithStyle) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + -- skipping this until https://jira.rbx.com/browse/MOBLUAPP-2424 is merged to CI + itSKIP("mount should throw when created with invalid properties", function() + local function expectToThrowForInvalidProps(props) + local verticalScrollViewWithStyle = mockStyleComponent({ + verticalScrollView = Roact.createElement(VerticalScrollView, props) + }) + expect(function() + Roact.mount(verticalScrollViewWithStyle) + end).to.throw() + end + + expectToThrowForInvalidProps({ position = 3 }) + expectToThrowForInvalidProps({ size = 3 }) + expectToThrowForInvalidProps({ canvasSizeY = 3 }) + expectToThrowForInvalidProps({ paddingHorizontal = 3 }) + expectToThrowForInvalidProps({ NotInTheInterface = "Really it is not there" }) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/getPageMargin.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/getPageMargin.lua new file mode 100644 index 0000000..af7d70c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/getPageMargin.lua @@ -0,0 +1,17 @@ +local Container = script.Parent +local App = Container.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +return function(containerWidth) + assert(t.number(containerWidth)) + if containerWidth <= 360 then + return 12 + elseif containerWidth > 360 and containerWidth <= 599 then + return 24 + else + return 48 + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/getPageMargin.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/getPageMargin.spec.lua new file mode 100644 index 0000000..023ad16 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Container/getPageMargin.spec.lua @@ -0,0 +1,22 @@ +return function() + local getPageMargin = require(script.Parent.getPageMargin) + describe("getPageMargin()", function() + it("should return the number 12", function() + local pageMargin = getPageMargin(312) + expect(pageMargin).to.equal(12) + end) + it("should return the number 24", function() + local pageMargin = getPageMargin(400) + expect(pageMargin).to.equal(24) + end) + it("should return the number 48", function() + local pageMargin = getPageMargin(700) + expect(pageMargin).to.equal(48) + end) + it("it should error", function() + expect(function() + return getPageMargin("String") + end).to.throw() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Context/ContentProvider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Context/ContentProvider.lua new file mode 100644 index 0000000..3a30d14 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Context/ContentProvider.lua @@ -0,0 +1,6 @@ +local ContentProvider = game:GetService("ContentProvider") + +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) + +return Roact.createContext(ContentProvider) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/Knob/BaseKnob.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/Knob/BaseKnob.lua new file mode 100644 index 0000000..8758bfa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/Knob/BaseKnob.lua @@ -0,0 +1,191 @@ +local Knob = script.Parent +local Control = Knob.Parent +local App = Control.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local RoactGamepad = require(Packages.RoactGamepad) +local Images = require(App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local Interactable = require(UIBlox.Core.Control.Interactable) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) +local withStyle = require(UIBlox.Core.Style.withStyle) +local validateColor = require(UIBlox.Core.Style.Validator.validateColorInfo) +local BaseKnob = Roact.Component:extend("BaseKnob") + +-- local validateColor = t.interface({ +-- Color = t.Color3, +-- Transparency = t.number, +-- }) + +local ShadowColorMap = { + [ControlState.Default] = { + Color = Color3.fromRGB(0,0,0), + Transparency = 0.7 + }, + [ControlState.Hover] = { + Color = Color3.fromRGB(0,0,0), + Transparency = 0.5 + } +} + +local ShadowSizeMap = { + [ControlState.Default] = UDim2.fromOffset(48, 48), + [ControlState.Hover] = UDim2.fromOffset(48, 48), + [ControlState.Pressed] = UDim2.fromOffset(0, 0), + [ControlState.Disabled] = UDim2.fromOffset(0, 0), + [ControlState.Selected] = UDim2.fromOffset(52, 52), + [ControlState.SelectedPressed] = UDim2.fromOffset(42, 42), +} + +local ShadowImageMap = { + [ControlState.Default] = "component_assets/dropshadow_28", + [ControlState.Hover] = "component_assets/dropshadow_28", + [ControlState.Selected] = "component_assets/circle_52_stroke_3", + [ControlState.SelectedPressed] = "component_assets/circle_42_stroke_3", +} + +BaseKnob.validateProps = t.interface({ + -- The state change callback for the button + onStateChanged = t.optional(t.callback), + + -- Is the button visually disabled + isDisabled = t.optional(t.boolean), + + --A Boolean value that determines whether user events are ignored and sink input + userInteractionEnabled = t.optional(t.boolean), + + -- The activated callback for the button + onActivated = t.optional(t.callback), + + anchorPoint = t.optional(t.Vector2), + layoutOrder = t.optional(t.number), + position = t.optional(t.UDim2), + + + colorMap = t.strictInterface({ + [ControlState.Default] = validateColor, + [ControlState.Hover] = validateColor, + [ControlState.Pressed] = validateColor, + [ControlState.Disabled] = validateColor, + [ControlState.Selected] = validateColor, + [ControlState.SelectedPressed] = validateColor, + }), + + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) +BaseKnob.defaultProps = { + anchorPoint = Vector2.new(0.5, 0.5), + userInteractionEnabled = true, + isDisabled = false +} +function BaseKnob:init() + self:setState({ + controlState = ControlState.Initialize + }) + + self.onStateChanged = function(oldState, newState) + self:setState({ + controlState = newState, + }) + if self.props.onStateChanged then + self.props.onStateChanged(oldState, newState) + end + end +end + +function BaseKnob:render() + return withStyle(function(style) + local color = self.props.colorMap[self.state.controlState] or self.props.colorMap[ControlState.Default] + local shadowSize = ShadowSizeMap[self.state.controlState] or ShadowSizeMap[ControlState.Default] + local shadowImage = ShadowImageMap[self.state.controlState] or ShadowImageMap[ControlState.Default] + local isGamepadSelected = self.state.controlState == ControlState.Selected or + self.state.controlState == ControlState.SelectedPressed + local shadowColor = ShadowColorMap[self.state.controlState] or ShadowColorMap[ControlState.Default] + if isGamepadSelected then + shadowColor = style.Theme.SelectionCursor + end + if UIBloxConfig.enableExperimentalGamepadSupport then + return Roact.createElement("Frame",{ + AnchorPoint = self.props.anchorPoint, + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + BackgroundTransparency = 1, + Size = UDim2.fromOffset(28, 28) + }, { + KnobShadow = Roact.createElement(ImageSetComponent.Label, { + Size = shadowSize, + Position = UDim2.new(0.5, 0, 0.5, not isGamepadSelected and 2 or 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Image = Images[shadowImage], + ImageColor3 = shadowColor.Color, + ImageTransparency = shadowColor.Transparency, + Active = true, + BackgroundTransparency = 1, + }), + KnobButton = Roact.createElement(RoactGamepad.Focusable[Interactable], { + Size = UDim2.fromScale(1,1), + + isDisabled = self.props.isDisabled, + onStateChanged = self.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + BackgroundTransparency = 1, + + Image = Images["component_assets/circle_29"], + ImageColor3 = color.Color, + ImageTransparency = color.Transparency, + + [Roact.Ref] = self.props[Roact.Ref], + [Roact.Event.Activated] = self.props.onActivated, + + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + }), + }) + else + return Roact.createElement("Frame",{ + AnchorPoint = self.props.anchorPoint, + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + BackgroundTransparency = 1, + Size = UDim2.fromOffset(28, 28) + }, { + KnobShadow = Roact.createElement(ImageSetComponent.Label, { + Size = shadowSize, + Position = UDim2.new(0.5, 0, 0.5, not isGamepadSelected and 2 or 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Image = Images[shadowImage], + ImageColor3 = shadowColor.Color, + ImageTransparency = shadowColor.Transparency, + BackgroundTransparency = 1, + }), + KnobButton = Roact.createElement(Interactable, { + Size = UDim2.fromScale(1,1), + + isDisabled = self.props.isDisabled, + onStateChanged = self.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + BackgroundTransparency = 1, + + Image = Images["component_assets/circle_29"], + ImageColor3 = color.Color, + ImageTransparency = color.Transparency, + + [Roact.Event.Activated] = self.props.onActivated, + }), + }) + end + end) +end + +return BaseKnob \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/Knob/Knob.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/Knob/Knob.lua new file mode 100644 index 0000000..773b711 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/Knob/Knob.lua @@ -0,0 +1,82 @@ +local Knob = script.Parent +local Control = Knob.Parent +local App = Control.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) + +local BaseKnob = require(Knob.BaseKnob) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) +local Colors = require(App.Style.Colors) + +local colorMap = { + contextual = { + [ControlState.Default] = { + Color = Colors.White, + Transparency = 0, + }, + [ControlState.Hover] = { + Color = Colors.White, + Transparency = 0, + }, + [ControlState.Pressed] = { + Color = Colors.Green, + Transparency = 0, + }, + [ControlState.Disabled] = { + Color = Colors.Pumice, + Transparency = 0, + }, + [ControlState.Selected] = { + Color = Colors.White, + Transparency = 0, + }, + [ControlState.SelectedPressed] = { + Color = Colors.White, + Transparency = 0, + }, + }, + system = { + [ControlState.Default] = { + Color = Colors.White, + Transparency = 0, + }, + [ControlState.Hover] = { + Color = Colors.White, + Transparency = 0, + }, + [ControlState.Pressed] = { + Color = Colors.Pumice, + Transparency = 0, + }, + [ControlState.Disabled] = { + Color = Colors.Pumice, + Transparency = 0, + }, + [ControlState.Selected] = { + Color = Colors.White, + Transparency = 0, + }, + [ControlState.SelectedPressed] = { + Color = Colors.White, + Transparency = 0, + }, + } +} + +local function buildKnob(styleName) + return function(props) + local currentColorMap = colorMap[styleName] + local newProps = Cryo.Dictionary.join({},props,{ + colorMap = currentColorMap + }) + return Roact.createElement(BaseKnob,newProps) + end +end + +return { + ContextualKnob = buildKnob("contextual"), + SystemKnob = buildKnob("system"), +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/Knob/Knob.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/Knob/Knob.spec.lua new file mode 100644 index 0000000..1e1e673 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/Knob/Knob.spec.lua @@ -0,0 +1,30 @@ +return function() + local KnobFolder = script.Parent + local Control = KnobFolder.Parent + local App = Control.Parent + local UIBlox = App.Parent + local Roact = require(UIBlox.Parent.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local Knob = require(script.Parent.Knob) + + describe("lifecycle", function() + it("should mount and unmount without issue with only required props", function() + local element = mockStyleComponent({ + Knob1 = Roact.createElement(Knob.ContextualKnob,{ + onActivated = print + }), + Knob2 = Roact.createElement(Knob.SystemKnob,{ + onActivated = print + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/RobuxBalance.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/RobuxBalance.lua new file mode 100644 index 0000000..4021eac --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/RobuxBalance.lua @@ -0,0 +1,269 @@ +local Control = script.Parent +local App = Control.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Core = UIBlox.Core + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Interactable = require(Core.Control.Interactable) + +local ControlState = require(Core.Control.Enum.ControlState) +local getContentStyle = require(Core.Button.getContentStyle) + +local withStyle = require(Core.Style.withStyle) +local ImageSetComponent = require(Core.ImageSet.ImageSetComponent) +local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel) +local Images = require(App.ImageSet.Images) +local IconSize = require(App.ImageSet.Enum.IconSize) +local getIconSize = require(App.ImageSet.getIconSize) +local TooltipContainer = require(App.Dialog.Tooltip.TooltipContainer) + +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) + +local CONTENT_PADDING = 8 +local DEFAULT_BORDER_CIRCLE = Images["component_assets/circle_17_stroke_1"] +local DEFAULT_BACKGROUND_CIRCLE = Images["component_assets/circle_17"] +local DEFAULT_SLICE_CENTER = Rect.new(8, 8, 9, 9) +local SELECTED_BORDER_CIRCLE = Images["component_assets/circle_22_stroke_3"] +local SELECTED_SLICE_CENTER = Rect.new(11, 11, 12, 12) +local DEFAULT_ICON = Images["icons/common/goldrobux_small"] +local DELAY_TIME = 1 +local DEFAULT_PADDING = 8 +local MIN_WIDTH = 56 +local MAX_WIDTH = 108 +local TRIGGER_AREA_HEIGHT = 44 +local BUTTON_AREA_HEIGHT = 28 + +local BUTTON_STATE_COLOR_MAP = { + [ControlState.Default] = "Divider", + [ControlState.Hover] = "SystemPrimaryOnHover", + [ControlState.Selected] = "SystemPrimaryOnHover", +} +local TEXT_STATE_COLOR_MAP = { + [ControlState.Default] = "TextEmphasis" +} +local ICON_STATE_COLOR_MAP = { + [ControlState.Default] = "TextEmphasis" +} +local BACKGROUND_STATE_COLOR_MAP = { + [ControlState.Default] = "BackgroundUIDefault" +} + +local RobuxBalance = Roact.PureComponent:extend("RobuxBalance") + +function RobuxBalance:init() + self.triggerRef = Roact.createRef() + self.toolTipRef = Roact.createRef() + + self.isInHover = false + self.tooltipWidth, self.setTooltipWidth = Roact.createBinding(0) + + self.state = { + controlState = ControlState.Initialize, + triggerPosition = Vector2.new(0, 0), + triggerSize = Vector2.new(0, 0), + showToolTip = false + } + + self.onStateChanged = function(oldState, newState) + self:setState({ + controlState = newState, + }) + + local isFocused = newState == ControlState.Hover or newState == ControlState.Pressed + if self.props.fullText and isFocused then + self.isInHover = true + delay(DELAY_TIME, self.showToolTip) + else + self.isInHover = false + self.hideToolTip() + end + + if self.props.onStateChanged then + self.props.onStateChanged(oldState, newState) + end + end + + self.showToolTip = function() + if self.isInHover then + self:setState({ + showToolTip = true, + }) + end + end + + self.hideToolTip = function() + self:setState({ + showToolTip = false, + }) + end + + self.setPosition = function(rbx) + self:setState({ + triggerPosition = rbx.AbsolutePosition, + }) + end + + self.setSize = function(rbx) + self:setState({ + triggerSize = rbx.AbsoluteSize, + }) + end + + self.onActivated = function(value) + if self.props.onActivated then + self.props.onActivated(value) + end + end +end + +RobuxBalance.validateProps = t.strictInterface({ + --The text of the button + displayText = t.optional(t.string), + + --The text of the tooltip when hovered over 2 seconds + fullText = t.optional(t.string), + + --The position of this component + position = t.optional(t.UDim2), + + --The activated callback for the button + onActivated = t.optional(t.callback), + + --The state change callback for the button + onStateChanged = t.optional(t.callback), + + --The state change callback for the button + tooltipPosition = t.optional(t.UDim2), + + --A Boolean value that determines whether user events are ignored and sink input + userInteractionEnabled = t.optional(t.boolean), +}) + +function RobuxBalance:render() + return withStyle(function(style) + + local currentState = self.state.controlState + local showToolTip = self.state.showToolTip + + local text = self.props.displayText or "-" + local hasFullText = self.props.fullText + local icon = DEFAULT_ICON + + local buttonStyle = getContentStyle(BUTTON_STATE_COLOR_MAP, currentState, style) + local textStyle = getContentStyle(TEXT_STATE_COLOR_MAP, currentState, style) + local iconStyle = getContentStyle(ICON_STATE_COLOR_MAP, currentState, style) + local backgroundStyle = getContentStyle(BACKGROUND_STATE_COLOR_MAP, currentState, style) + local buttonImage = self.state.controlState == ControlState.Selected and + SELECTED_BORDER_CIRCLE or DEFAULT_BORDER_CIRCLE + local sliceCenter = self.state.controlState == ControlState.Selected and + SELECTED_SLICE_CENTER or DEFAULT_SLICE_CENTER + local fontStyle = style.Font.CaptionHeader + + local fontSize = fontStyle.RelativeSize * style.Font.BaseSize + local estimatedTextWidth = GetTextSize( + text, + fontSize, + fontStyle.Font, + Vector2.new(1000, 1000) + ).X + local buttonWidth = estimatedTextWidth + getIconSize(IconSize.Small) + DEFAULT_PADDING * 3 + return Roact.createElement(Interactable, { + Position = self.props.position or UDim2.fromScale(0, 0), + isDisabled = false, + onStateChanged = self.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + BackgroundTransparency = 1, + [Roact.Event.Activated] = self.onActivated, + Size = UDim2.fromOffset(buttonWidth, TRIGGER_AREA_HEIGHT), + AnchorPoint = Vector2.new(1, 0), + [Roact.Ref] = self.triggerRef, + [Roact.Change.AbsolutePosition] = self.setPosition, + [Roact.Change.AbsoluteSize] = self.setSize, + }, { + sizeConstraint = Roact.createElement("UISizeConstraint", { + MinSize = Vector2.new(MIN_WIDTH, TRIGGER_AREA_HEIGHT), + MaxSize = Vector2.new(MAX_WIDTH, TRIGGER_AREA_HEIGHT), + }), + UIPadding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, (TRIGGER_AREA_HEIGHT - BUTTON_AREA_HEIGHT) / 2), + PaddingBottom = UDim.new(0, (TRIGGER_AREA_HEIGHT - BUTTON_AREA_HEIGHT) / 2), + }), + ButtonContent = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, BUTTON_AREA_HEIGHT), + BackgroundTransparency = 1, + }, { + ButtonBackground = Roact.createElement(ImageSetComponent.Label, { + Image = DEFAULT_BACKGROUND_CIRCLE, + ScaleType = Enum.ScaleType.Slice, + ImageColor3 = backgroundStyle.Color, + ImageTransparency = backgroundStyle.Transparency, + BackgroundTransparency = 1, + SliceCenter = DEFAULT_SLICE_CENTER, + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromOffset(0, 0), + }), + ButtonContent = Roact.createElement(ImageSetComponent.Label, { + Image = buttonImage, + ScaleType = Enum.ScaleType.Slice, + ImageColor3 = buttonStyle.Color, + ImageTransparency = buttonStyle.Transparency, + BackgroundTransparency = 1, + SliceCenter = sliceCenter, + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromOffset(0, 0), + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, CONTENT_PADDING), + }), + Icon = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(getIconSize(IconSize.Small), getIconSize(IconSize.Small)), + BackgroundTransparency = 1, + Image = icon, + ImageColor3 = iconStyle.Color, + ImageTransparency = iconStyle.Transparency, + LayoutOrder = 1, + }), + Text = Roact.createElement(GenericTextLabel, { + BackgroundTransparency = 1, + Text = text, + fontStyle = fontStyle, + colorStyle = textStyle, + LayoutOrder = 2, + maxSize = Vector2.new(estimatedTextWidth, BUTTON_AREA_HEIGHT), + TextTruncate = Enum.TextTruncate.AtEnd, + TextWrapped = false, + TextXAlignment = Enum.TextXAlignment.Left + }), + }), + }), + TooltipContainer = hasFullText and Roact.createElement("Frame", { + Visible = showToolTip, + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromOffset(0, 0), + BackgroundTransparency = 1, + }, { + TooltipContent = Roact.createElement(TooltipContainer, { + triggerPosition = self.state.triggerPosition or Vector2.new(0 ,0), + triggerSize = self.state.triggerSize or Vector2.new(0 ,0), + position = self.props.tooltipPosition or UDim2.new(1, -self.tooltipWidth:getValue(), 1, 0), + bodyText = self.props.fullText or "", + isDirectChild = true, + }), + }) + }) + end) +end + +function RobuxBalance:didUpdate() + local hasFullText = self.props.fullText + self.setTooltipWidth((self.triggerRef.current and hasFullText) and + self.triggerRef.current.TooltipContainer.TooltipContent.Size.X.Offset or 0) +end +return RobuxBalance diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/RobuxBalance.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/RobuxBalance.spec.lua new file mode 100644 index 0000000..7df809f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/RobuxBalance.spec.lua @@ -0,0 +1,39 @@ +return function() + local Control = script.Parent + local App = Control.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local RobuxBalance = require(Control.RobuxBalance) + + it("should create and destroy Robux Balance with no props without errors", function() + local element = mockStyleComponent({ + RobuxBalance = Roact.createElement(RobuxBalance), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy Robux Balance with all props without errors", function() + local element = mockStyleComponent({ + RobuxBalance = Roact.createElement(RobuxBalance, { + displayText = "9.1K Robux", + fullText = "9107 Robux", + position = UDim2.new(1, 0, 0, 0), + onActivated = function() end, + onStateChanged = function() end, + tooltipPosition = UDim2.new(1, 0, 0, 0), + userInteractionEnabled = true, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/SegmentedControl.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/SegmentedControl.lua new file mode 100644 index 0000000..6be470b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/SegmentedControl.lua @@ -0,0 +1,425 @@ +local Control = script.Parent +local App = Control.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Core = UIBlox.Core + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) +local withStyle = require(UIBlox.Core.Style.withStyle) +local Otter = require(Packages.Otter) + +local RoactGamepad = require(Packages.RoactGamepad) +local UIBloxConfig = require(UIBlox.UIBloxConfig) + + +local ControlState = require(Core.Control.Enum.ControlState) +local SegmentedControlTabName = require(script.Parent.SegmentedControlTabName) +local ImageSetComponent = require(Core.ImageSet.ImageSetComponent) +local Images = require(App.ImageSet.Images) +local getContentStyle = require(Core.Button.getContentStyle) + +local FRAME_PADDING = 4 +local MIN_TAB_WIDTH = 108 +local MAX_WIDTH = 640 +local INTERACTION_HEIGHT = 44 +local BACKGROUND_HEIGHT = 36 +local TAB_HEIGHT = 28 +local SPRING_PARAMS = { + frequency = 10, + dampingRatio = 1, +} +local BACKGROUND_IMAGE = "component_assets/circle_17" +local SHADOW_IMAGE = "component_assets/dropshadow_25" +local BACKGROUND_COLOR_STATE_MAP = { + [ControlState.Default] = "BackgroundUIDefault" +} +local DIVIDER_COLOR_STATE_MAP = { + [ControlState.Default] = "Divider" +} +local SELECTED_BACKGROUND_COLOR_STATE_MAP = { + [ControlState.Default] = "UIDefault" +} +local DROPSHADOW_COLOR_STATE_MAP = { + [ControlState.Default] = "DropShadow" +} + + +local limitedLengthTabArray = function(array) + local typeChecker, typeCheckerMessage = t.array(t.strictInterface({ + -- Callback when this tab is selected + onActivated = t.optional(t.callback), + -- The name and ID for this tab + tabName = t.string, + -- If this tab is disabled. + isDisabled = t.optional(t.boolean), + }))(array) + + if not typeChecker then + return typeChecker, typeCheckerMessage + end + + local minLengthChecker, minLengthCheckerMessage = t.numberMin(2)(#array) + if not minLengthChecker then + return minLengthChecker, minLengthCheckerMessage + end + + local maxLengthChecker, maxLengthCheckerMessage = t.numberMax(4)(#array) + if not maxLengthChecker then + return maxLengthChecker, maxLengthCheckerMessage + end + + return true +end + +local SegmentedControl = Roact.Component:extend("SegmentedControl") + +SegmentedControl.validateProps = t.strictInterface({ + tabs = limitedLengthTabArray, + -- Width for this component + -- Will be restricted by the component's size constraint. + width = t.UDim, + defaultSelctedTabIndex = t.optional(t.number), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), +}) + +function SegmentedControl:init() + self.rootRef = Roact.createRef() + self.tabRefs = RoactGamepad.createRefCache() + + self.selectedBackgroundMotor = Otter.createSingleMotor(FRAME_PADDING) + self.selectedBackgroundMotor:onStep(function(selectedBackgroundPositionX) + self:setState({ + selectedBackgroundPositionX = selectedBackgroundPositionX + }) + end) + + local defaultSelectedIndex = self.props.defaultSelctedTabIndex or 1 + local defaultSelectedTab = self.props.tabs[defaultSelectedIndex] + self:setState({ + selectedTab = defaultSelectedTab, + selectedIndex = defaultSelectedIndex, + selectedBackgroundPositionX = 0, + tabWidth = 0, + }) + + self.onTabActivated = function(index) + self.selectTab(self.props.tabs[index], index) + end + + self.setSize = function(rbx) + local frameWidth = rbx.AbsoluteSize.X + local totalTabWidth = frameWidth - FRAME_PADDING * 2 + local tabWidth = math.floor(totalTabWidth / #self.props.tabs) + self.selectedBackgroundMotor:setGoal(Otter.spring(FRAME_PADDING + tabWidth * (self.state.selectedIndex - 1), + SPRING_PARAMS)) + self:setState({ + tabWidth = tabWidth, + }) + end + + self.selectTab = function(activatedTab, selectedIndex) + self.selectedBackgroundMotor:setGoal(Otter.spring(FRAME_PADDING + self.state.tabWidth * (selectedIndex - 1), + SPRING_PARAMS)) + self:setState({ + selectedTab = activatedTab, + selectedIndex = selectedIndex + }) + activatedTab.onActivated(activatedTab) + end +end +function SegmentedControl:renderDefault() + return withStyle(function(style) + -- render params + local currentState = self.state.controlState + local isDisabled = self.state.selectedTab.isDisabled + local forceSelectedBGState = isDisabled and ControlState.Disabled or currentState + local backgroundStyle = getContentStyle(BACKGROUND_COLOR_STATE_MAP, currentState, style) + local dividerStyle = getContentStyle(DIVIDER_COLOR_STATE_MAP, currentState, style) + local selectedBackgroundStyle = getContentStyle(SELECTED_BACKGROUND_COLOR_STATE_MAP, forceSelectedBGState, style) + local dropshadowStyle = getContentStyle(DROPSHADOW_COLOR_STATE_MAP, currentState, style) + local tabWidth = self.state.tabWidth + -- dividers between tabs + local dividers = {} + for i = 1, #self.props.tabs - 1, 1 do + if i ~= self.state.selectedIndex and i ~= self.state.selectedIndex - 1 then + table.insert(dividers, i, Roact.createElement("Frame", { + Size = UDim2.fromOffset(1, TAB_HEIGHT), + BackgroundTransparency = dividerStyle.Transparency, + BackgroundColor3 = dividerStyle.Color, + Position = UDim2.fromOffset(FRAME_PADDING + tabWidth * i, (INTERACTION_HEIGHT - TAB_HEIGHT) / 2) + })) + end + end + -- create the tabs as SegmentedControlTabName and add activated callback + local tabs = Cryo.List.map(self.props.tabs, function(tab, index) + return Roact.createElement("Frame",{ + LayoutOrder = index, + Size = UDim2.fromOffset(tabWidth, INTERACTION_HEIGHT), + BackgroundTransparency = 1, + }, { + Tab = Roact.createElement(SegmentedControlTabName, { + text = tab.tabName, + onActivated = function() self.onTabActivated(index) end, + Size = UDim2.fromScale(1, 1), + isDisabled = tab.isDisabled, + isSelectedStyle = self.state.selectedIndex == index + }) + }) + end) + -- add UIListLayout for tabs + table.insert(tabs, Roact.createElement("UIListLayout", { + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder + })) + + return Roact.createElement("Frame", { + Size = UDim2.new(self.props.width.Scale, self.props.width.Offset, 0, INTERACTION_HEIGHT), + BackgroundTransparency = 1, + [Roact.Change.AbsoluteSize] = self.setSize + }, { + SizeConstraint = Roact.createElement("UISizeConstraint", { + MinSize = Vector2.new(MIN_TAB_WIDTH * #self.props.tabs + FRAME_PADDING * 2, INTERACTION_HEIGHT), + MaxSize = Vector2.new(MAX_WIDTH, INTERACTION_HEIGHT), + }), + -- tab group background + Background = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.new(1, 0, 0, BACKGROUND_HEIGHT), + Position = UDim2.fromOffset(0, (INTERACTION_HEIGHT - BACKGROUND_HEIGHT) / 2), + BackgroundTransparency = 1, + Image = Images[BACKGROUND_IMAGE], + ImageColor3 = backgroundStyle.Color, + ImageTransparency = backgroundStyle.Transparency, + LayoutOrder = 1, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + ZIndex = 1, + }), + -- put dividers in one frame + Dividers = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, INTERACTION_HEIGHT), + Position = UDim2.fromOffset(0, 0), + BackgroundTransparency = 1, + ZIndex = 2, + }, dividers), + -- the only selected tab background, used to make animation + SelectedBackground = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(tabWidth, TAB_HEIGHT), + Position = UDim2.fromOffset(self.state.selectedBackgroundPositionX, (INTERACTION_HEIGHT - TAB_HEIGHT) / 2), + BackgroundTransparency = 1, + Image = BACKGROUND_IMAGE, + ImageColor3 = selectedBackgroundStyle.Color, + ImageTransparency = selectedBackgroundStyle.Transparency, + LayoutOrder = 2, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + ZIndex = 4, + }), + -- the shadow for selected tab background + SelectedBackgroundShadow = not isDisabled and Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(tabWidth + 6 * 2, TAB_HEIGHT + 6 * 2), + Position = UDim2.fromOffset( + self.state.selectedBackgroundPositionX - 6, + (INTERACTION_HEIGHT - TAB_HEIGHT) / 2 - 6 + 2 + ), + BackgroundTransparency = 1, + Image = Images[SHADOW_IMAGE], + ImageColor3 = dropshadowStyle.Color, + ImageTransparency = 0.3, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(18, 18, 19, 19), + ZIndex = 3, + }), + -- container for tabs + TabContainer = Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + Position = UDim2.fromScale(0, 0), + ZIndex = 5, + },tabs) + }) + end) +end + +function SegmentedControl:renderGamepadSupport() + return withStyle(function(style) + -- render params + local currentState = self.state.controlState + local isDisabled = self.state.selectedTab.isDisabled + local forceSelectedBGState = isDisabled and ControlState.Disabled or currentState + local backgroundStyle = getContentStyle(BACKGROUND_COLOR_STATE_MAP, currentState, style) + local dividerStyle = getContentStyle(DIVIDER_COLOR_STATE_MAP, currentState, style) + local selectedBackgroundStyle = getContentStyle(SELECTED_BACKGROUND_COLOR_STATE_MAP, forceSelectedBGState, style) + local dropshadowStyle = getContentStyle(DROPSHADOW_COLOR_STATE_MAP, currentState, style) + local tabWidth = self.state.tabWidth + + -- dividers between tabs + local dividers = {} + for i = 1, #self.props.tabs - 1, 1 do + if i ~= self.state.selectedIndex and i ~= self.state.selectedIndex - 1 then + table.insert(dividers, i, Roact.createElement("Frame", { + Size = UDim2.fromOffset(1, TAB_HEIGHT), + BackgroundTransparency = dividerStyle.Transparency, + BackgroundColor3 = dividerStyle.Color, + Position = UDim2.fromOffset(FRAME_PADDING + tabWidth * i, (INTERACTION_HEIGHT - TAB_HEIGHT) / 2) + })) + end + end + + return RoactGamepad.withFocusController(function(focusController) + local moveToPrevious = function(index) + if index > 1 then + focusController.moveFocusTo(self.tabRefs[index - 1]) + end + end + local moveToNext = function(index) + if index < #self.props.tabs then + focusController.moveFocusTo(self.tabRefs[index + 1]) + end + end + local moveToParent = function() + focusController.moveFocusTo(self.rootRef) + end + -- create the tabs as SegmentedControlTabName and add activated callback, adding gamepad support + -- gamepad focus is added to the frame, not SegmentedControlTabName + local tabs = Cryo.List.map(self.props.tabs, function(tab, index) + return Roact.createElement(RoactGamepad.Focusable.Frame,{ + LayoutOrder = index, + Size = UDim2.fromOffset(tabWidth, INTERACTION_HEIGHT), + BackgroundTransparency = 1, + [Roact.Ref] = self.tabRefs[index], + NextSelectionLeft = index > 1 and self.tabRefs[index - 1] or nil, + NextSelectionRight = index < #self.props.tabs and self.tabRefs[index + 1] or nil, + onFocusGained = function() + if not tab.isDisabled then + self.selectTab(tab) + end + end, + inputBindings = { + LeaveA = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonA, moveToParent), + LeaveB = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonB, moveToParent), + SelectNext1 = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonR1, function() moveToNext(index) end), + SelectPre1 = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonL1, function() moveToPrevious(index) end), + SelectNext2 = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonR2, function() moveToNext(index) end), + SelectPre2 = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonL2, function() moveToPrevious(index) end), + SelectNext3 = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonR3, function() moveToNext(index) end), + SelectPre3 = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonL3, function() moveToPrevious(index) end) + } + }, { + Tab = Roact.createElement(SegmentedControlTabName, { + text = tab.tabName, + onActivated = function() self.onTabActivated(index) end, + Size = UDim2.fromScale(1, 1), + isDisabled = tab.isDisabled, + isSelectedStyle = self.state.selectedIndex == index + }) + }) + end) + -- add UIListLayout for tabs + table.insert(tabs, Roact.createElement("UIListLayout", { + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder + })) + + return Roact.createElement("Frame", { + Size = UDim2.new(self.props.width.Scale, self.props.width.Offset, 0, INTERACTION_HEIGHT), + BackgroundTransparency = 1, + [Roact.Change.AbsoluteSize] = self.setSize, + }, { + SizeConstraint = Roact.createElement("UISizeConstraint", { + MinSize = Vector2.new(MIN_TAB_WIDTH * #self.props.tabs + FRAME_PADDING * 2, INTERACTION_HEIGHT), + MaxSize = Vector2.new(MAX_WIDTH, INTERACTION_HEIGHT), + }), + -- tab group background + Background = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.new(1, 0, 0, BACKGROUND_HEIGHT), + Position = UDim2.fromOffset(0, (INTERACTION_HEIGHT - BACKGROUND_HEIGHT) / 2), + BackgroundTransparency = 1, + Image = BACKGROUND_IMAGE, + ImageColor3 = backgroundStyle.Color, + ImageTransparency = backgroundStyle.Transparency, + LayoutOrder = 1, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + ZIndex = 1, + }), + -- put dividers in one frame, use this frame as the "Main" gamepad selection. + Dividers = Roact.createElement(RoactGamepad.Focusable.Frame, { + Size = UDim2.new(1, 0, 0, INTERACTION_HEIGHT), + Position = UDim2.fromOffset(0, 0), + BackgroundTransparency = 1, + ZIndex = 2, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + [Roact.Ref] = self.rootRef, + inputBindings = { + Enter = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonA, function() + focusController.moveFocusTo(self.tabRefs[self.state.selectedIndex]) + end) + } + }, dividers), + -- the only selected tab background, used to make animation + SelectedBackground = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(tabWidth, TAB_HEIGHT), + Position = UDim2.fromOffset( + self.state.selectedBackgroundPositionX, + (INTERACTION_HEIGHT - TAB_HEIGHT) / 2 + ), + BackgroundTransparency = 1, + Image = BACKGROUND_IMAGE, + ImageColor3 = selectedBackgroundStyle.Color, + ImageTransparency = selectedBackgroundStyle.Transparency, + LayoutOrder = 2, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + ZIndex = 4, + }), + -- the shadow for selected tab background + SelectedBackgroundShadow = not isDisabled and Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(tabWidth + 6 * 2, TAB_HEIGHT + 6 * 2), + Position = UDim2.fromOffset( + self.state.selectedBackgroundPositionX - 6, + (INTERACTION_HEIGHT - TAB_HEIGHT) / 2 - 6 + 2 + ), + BackgroundTransparency = 1, + Image = SHADOW_IMAGE, + ImageColor3 = dropshadowStyle.Color, + ImageTransparency = 0.3, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(18, 18, 19, 19), + ZIndex = 3, + }), + -- container for tabs + TabContainer = Roact.createElement(RoactGamepad.Focusable.Frame, { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + Position = UDim2.fromScale(0, 0), + ZIndex = 5, + defaultChild = self.tabRefs[self.state.selectedIndex] + },tabs) + }) + end) + end) +end +function SegmentedControl:render() + if UIBloxConfig.enableExperimentalGamepadSupport then + return self:renderGamepadSupport() + else + return self:renderDefault() + end +end + + +return SegmentedControl diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/SegmentedControl.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/SegmentedControl.spec.lua new file mode 100644 index 0000000..011af4e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/SegmentedControl.spec.lua @@ -0,0 +1,42 @@ +local Control = script.Parent +local App = Control.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + +local SegmentedControl = require(script.Parent.SegmentedControl) + +local DEFAULT_REQUIRED_PROPS = { + tabs = { + { + onActivated = function(tab) print(tab.tabName) end, + tabName = "a", + },{ + onActivated = function(tab) print(tab.tabName) end, + tabName = "b", + isDisabled = true + },{ + onActivated = function(tab) print(tab.tabName) end, + tabName = "c", + },{ + onActivated = function(tab) print(tab.tabName) end, + tabName = "d", + } + }, + width = UDim.new(0,10000), +} + +return function() + describe("lifecycle", function() + it("should mount and unmount SegmentedControl without issue", function() + local tree = mockStyleComponent( + Roact.createElement(SegmentedControl, DEFAULT_REQUIRED_PROPS) + ) + local handle = Roact.mount(tree) + Roact.unmount(handle) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/SegmentedControlTabName.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/SegmentedControlTabName.lua new file mode 100644 index 0000000..879393d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Control/SegmentedControlTabName.lua @@ -0,0 +1,195 @@ +local Control = script.Parent +local App = Control.Parent +local UIBlox = App.Parent +local Core = UIBlox.Core +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) + +local Interactable = require(Core.Control.Interactable) + +local ControlState = require(Core.Control.Enum.ControlState) +local getContentStyle = require(Core.Button.getContentStyle) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local ImageSetComponent = require(Core.ImageSet.ImageSetComponent) +local ShimmerPanel = require(UIBlox.App.Loading.ShimmerPanel) +local IconSize = require(UIBlox.App.ImageSet.Enum.IconSize) +local getIconSize = require(UIBlox.App.ImageSet.getIconSize) +local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel) +local validateFontInfo = require(UIBlox.Core.Style.Validator.validateFontInfo) + +local validateImage = require(Core.ImageSet.Validator.validateImage) + +local CONTENT_PADDING = 5 + +local SegmentedControlTabName = Roact.PureComponent:extend("SegmentedControlTabName") + +function SegmentedControlTabName:init() + self.state = { + controlState = ControlState.Initialize + } + + self.onStateChanged = function(oldState, newState) + self:setState({ + controlState = newState, + }) + if self.props.onStateChanged then + self.props.onStateChanged(oldState, newState) + end + end +end + +local colorStateMap = t.interface({ + -- The default state theme color class + [ControlState.Default] = t.string, +}) + +local validateProps = t.interface({ + --The icon of the button + icon = t.optional(validateImage), + + --The text of the button + text = t.optional(t.string), + + fontStyle = t.optional(validateFontInfo), + + --The theme color class mapping for different text tates + textStateColorMap = t.optional(colorStateMap), + + --The theme color class mapping for different icon tates + iconStateColorMap = t.optional(colorStateMap), + + --Is the button disabled + isDisabled = t.optional(t.boolean), + + --Is the button loading + isLoading = t.optional(t.boolean), + + --The activated callback for the button + onActivated = t.callback, + + --The state change callback for the button + onStateChanged = t.optional(t.callback), + + --A Boolean value that determines whether user events are ignored and sink input + userInteractionEnabled = t.optional(t.boolean), + + isSelectedStyle = t.optional(t.boolean), + + -- Note that this component can accept all valid properties of the Roblox ImageButton instance +}) + +SegmentedControlTabName.defaultProps = { + isDisabled = false, + isLoading = false, + isSelectedStyle = false, + SliceCenter = Rect.new(8, 8, 9, 9), + textStateColorMap = { + [ControlState.Default] = "SecondaryContent", + [ControlState.Hover] = "SecondaryOnHover", + }, + buttonStateColorMap = { + [ControlState.Default] = "SecondaryContent", + } +} + +SegmentedControlTabName.validateProps = validateProps + +function SegmentedControlTabName:render() + return withStyle(function(style) + + local currentState = self.state.controlState + + local text = self.props.text + local icon = self.props.icon + local isLoading = self.props.isLoading + local isDisabled = self.props.isDisabled + + local buttonStateColorMap = self.props.buttonStateColorMap + local contentStateColorMap = self.props.contentStateColorMap + local textStateColorMap = self.props.textStateColorMap or contentStateColorMap + local iconStateColorMap = self.props.iconStateColorMap or contentStateColorMap + + if isLoading then + isDisabled = true + end + + local textState = currentState + if self.props.isDisabled then + textState = ControlState.Disabled + elseif self.props.isSelectedStyle then + textState = ControlState.Hover + end + local buttonStyle = getContentStyle(buttonStateColorMap, currentState, style) + local textStyle = text and getContentStyle(textStateColorMap, textState, style) + local iconStyle = icon and getContentStyle(iconStateColorMap, currentState, style) + local fontStyle = self.props.fontStyle or style.Font.Header2 + + local buttonContentLayer + if isLoading then + buttonContentLayer = { + isLoadingShimmer = Roact.createElement(ShimmerPanel, { + Size = UDim2.new(1, 0, 1, 0), + }) + } + else + buttonContentLayer = self.props[Roact.Children] or { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, CONTENT_PADDING), + }), + Icon = icon and Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.new(0, getIconSize(IconSize.Medium), 0, getIconSize(IconSize.Medium)), + BackgroundTransparency = 1, + Image = icon, + ImageColor3 = iconStyle.Color, + ImageTransparency = iconStyle.Transparency, + LayoutOrder = 1, + }) or nil, + Text = text and Roact.createElement(GenericTextLabel, { + BackgroundTransparency = 1, + Text = text, + fontStyle = fontStyle, + colorStyle = textStyle, + LayoutOrder = 2, + }) or nil, + } + end + + return Roact.createElement(Interactable, Cryo.Dictionary.join(self.props, { + icon = Cryo.None, + text = Cryo.None, + buttonStateColorMap = Cryo.None, + contentStateColorMap = Cryo.None, + textStateColorMap = Cryo.None, + iconStateColorMap = Cryo.None, + onActivated = Cryo.None, + isLoading = Cryo.None, + isSelectedStyle = Cryo.None, + [Roact.Children] = Cryo.None, + isDisabled = isDisabled, + onStateChanged = self.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + Image = Cryo.None, + ScaleType = Enum.ScaleType.Slice, + ImageColor3 = buttonStyle.Color, + ImageTransparency = buttonStyle.Transparency, + BackgroundTransparency = 1, + AutoButtonColor = false, + [Roact.Event.Activated] = self.props.onActivated, + }), { + ButtonContent = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, buttonContentLayer) + }) + end) +end + +return SegmentedControlTabName diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Alert.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Alert.lua new file mode 100644 index 0000000..4e3fb29 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Alert.lua @@ -0,0 +1,213 @@ +local AlertRoot = script.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local RoactGamepad = require(Packages.RoactGamepad) +local Cryo = require(Packages.Cryo) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local GetTextHeight = require(UIBlox.Core.Text.GetTextHeight) +local Images = require(AppRoot.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local ButtonStack = require(AppRoot.Button.ButtonStack) + +local FitFrame = require(Packages.FitFrame) +local FitFrameOnAxis = FitFrame.FitFrameOnAxis + +local AlertType = require(AlertRoot.Enum.AlertType) +local AlertTitle = require(AlertRoot.AlertTitle) + +local BACKGROUND_IMAGE = "component_assets/circle_17" +local MARGIN = 24 + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local enableAlertTitleIconConfig = UIBloxConfig.enableAlertTitleIconConfig + +local validateButtonStack = require(AppRoot.Button.Validator.validateButtonStack) + +local Alert = Roact.PureComponent:extend("Alert") + +local validateProps = t.strictInterface({ + alertType = enumerateValidator(AlertType), + anchorPoint = t.optional(t.Vector2), + bodyText = t.optional(t.string), + buttonStackInfo = t.optional(validateButtonStack), + margin = t.optional(t.table), + maxWidth = t.optional(t.number), + minWidth = t.optional(t.number), + middleContent = t.optional(t.callback), + middleContentPaddingBetweenBodyText = t.optional(t.number), + onMounted = t.optional(t.callback), + onAbsoluteSizeChanged = t.optional(t.callback), + paddingBetween = t.optional(t.number), + position = t.optional(t.UDim2), + screenSize = t.Vector2, + title = t.string, + titleIcon = enableAlertTitleIconConfig and t.optional(t.union(t.table, t.string)) or nil, + titleIconSize = enableAlertTitleIconConfig and t.optional(t.number) or nil, + titlePadding = t.optional(t.number), + titlePaddingWithIcon = t.optional(t.number), + + --Gamepad props + defaultChildRef = t.optional(t.table), + isMiddleContentFocusable = t.optional(t.boolean), +}) + +Alert.defaultProps = { + anchorPoint = Vector2.new(0.5, 0.5), + margin = { + top = 0, + bottom = MARGIN, + left = MARGIN, + right = MARGIN, + }, + maxWidth = 400, + middleContentPaddingBetweenBodyText = 12, + minWidth = 272, + paddingBetween = 24, + position = UDim2.new(0.5, 0, 0.5, 0), +} + +function Alert:init() + self.contentSize, self.changeContentSize = Roact.createBinding(Vector2.new(0, 0)) + + if UIBloxConfig.enableExperimentalGamepadSupport then + self.middleContentRef = Roact.createRef() + end + self.buttonStackRef = Roact.createRef() +end + +function Alert:didMount() + if self.props.onMounted then + self.props.onMounted() + end +end + +function Alert:render() + assert(validateProps(self.props)) + local isMiddleContentFocusable = UIBloxConfig.enableExperimentalGamepadSupport and self.props.isMiddleContentFocusable + + local totalWidth = math.clamp(self.props.screenSize.X - self.props.margin.left - self.props.margin.right, + self.props.minWidth, self.props.maxWidth) + local innerWidth = totalWidth - self.props.margin.left - self.props.margin.right + + return withStyle(function(stylePalette) + local font = stylePalette.Font + local theme = stylePalette.Theme + local textFont = font.Body.Font + + local fontSize = font.BaseSize * font.Body.RelativeSize + + local fullTextHeight = self.props.bodyText + and GetTextHeight(self.props.bodyText, textFont, fontSize, innerWidth) + or 0 + + local backgroundTransparency + local imageColor + local imageTransparency + if self.props.alertType == AlertType.Interactive then + imageColor = theme.BackgroundUIDefault.Color + imageTransparency = theme.BackgroundUIDefault.Transparency + backgroundTransparency = 1 + else + backgroundTransparency = theme.BackgroundUIContrast.Transparency + imageTransparency = 1 + end + + local buttonStackInfo = self.props.buttonStackInfo + if UIBloxConfig.enableExperimentalGamepadSupport and buttonStackInfo then + buttonStackInfo = Cryo.Dictionary.join(buttonStackInfo, { + [Roact.Ref] = self.buttonStackRef, + NextSelectionUp = isMiddleContentFocusable and self.middleContentRef or nil, + }) + end + + return Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable[ImageSetComponent.Button] or ImageSetComponent.Button, { + Position = self.props.position, + AnchorPoint = self.props.anchorPoint, + Size = self.contentSize:map(function(absoluteSize) + return UDim2.new(0, absoluteSize.X, 0, absoluteSize.Y) + end), + BackgroundColor3 = theme.BackgroundUIDefault.Color, + BackgroundTransparency = backgroundTransparency, + BorderSizePixel = 0, + Image = Images[BACKGROUND_IMAGE], + ImageColor3 = imageColor, + ImageTransparency = imageTransparency, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + AutoButtonColor = false, + ClipsDescendants = true, + Selectable = false, + + [Roact.Ref] = self.props.defaultChildRef, + [Roact.Change.AbsoluteSize] = self.props.onAbsoluteSizeChanged, + defaultChild = UIBloxConfig.enableExperimentalGamepadSupport and self.buttonStackRef or nil, + }, { + AlertContents = Roact.createElement(FitFrameOnAxis, { + contentPadding = UDim.new(0, self.props.paddingBetween), + margin = self.props.margin, + minimumSize = UDim2.new(0, totalWidth, 0, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + [Roact.Change.AbsoluteSize] = function(rbx) + self.changeContentSize(rbx.AbsoluteSize) + end, + }, { + TitleContainer = Roact.createElement(AlertTitle, { + layoutOrder = 1, + margin = self.props.margin, + maxWidth = self.props.maxWidth, + minWidth = self.props.minWidth, + screenSize = self.props.screenSize, + title = self.props.title, + titleIcon = self.props.titleIcon, + titleIconSize = self.props.titleIconSize, + titlePadding = self.props.titlePadding, + titlePaddingWithIcon = self.props.titlePaddingWithIcon, + }), + Content = Roact.createElement(FitFrameOnAxis, { + BackgroundTransparency = 1, + contentPadding = UDim.new(0, self.props.middleContentPaddingBetweenBodyText), + LayoutOrder = 2, + minimumSize = UDim2.new(1, 0, 0, 0), + }, { + BodyText = self.props.bodyText and Roact.createElement(GenericTextLabel, { + BackgroundTransparency = 1, + colorStyle = theme.TextDefault, + fontStyle = font.Body, + LayoutOrder = 1, + Text = self.props.bodyText, + TextSize = fontSize, + TextXAlignment = Enum.TextXAlignment.Center, + Size = UDim2.new(1, 0, 0, fullTextHeight), + }), + MiddleContent = self.props.middleContent and Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable[FitFrameOnAxis] or FitFrameOnAxis, { + BackgroundTransparency = 1, + LayoutOrder = 2, + minimumSize = UDim2.new(1, 0, 0, 0), + + [Roact.Ref] = self.middleContentRef, + NextSelectionDown = isMiddleContentFocusable and self.buttonStackRef or nil, + }, + { + Content = self.props.middleContent() + } + ), + }), + Buttons = buttonStackInfo and Roact.createElement(ButtonStack, buttonStackInfo), + }) + }) + end) +end + +return Alert \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Alert.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Alert.spec.lua new file mode 100644 index 0000000..fd54934 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Alert.spec.lua @@ -0,0 +1,61 @@ +local AlertRoot = script.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) + +local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + +local Alert = require(script.Parent.Alert) +local AlertType = require(AlertRoot.Enum.AlertType) + +local DEFAULT_REQUIRED_PROPS = { + alertType = AlertType.Informative, + title = "Hello World", + screenSize = Vector2.new(100, 100), +} + +local function mountAlert(props) + local combinedProps = DEFAULT_REQUIRED_PROPS + if props then + combinedProps = Cryo.Dictionary.join(DEFAULT_REQUIRED_PROPS, props) + end + local tree = mockStyleComponent( + Roact.createElement(Alert, combinedProps) + ) + local handle = Roact.mount(tree) + return tree, function() + Roact.unmount(handle) + end +end + +return function() + describe("lifecycle", function() + it("should mount and unmount informative alerts without issue", function() + local _, cleanup = mountAlert({ + alertType = AlertType.Informative, + }) + cleanup() + end) + + it("should mount and unmount interactive alerts without issue", function() + local _, cleanup = mountAlert({ + alertType = AlertType.Interactive, + buttonStackInfo = { + buttons = { + { + props = { + text = "test", + onActivated = function() end, + } + }, + }, + }, + }) + cleanup() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/AlertTitle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/AlertTitle.lua new file mode 100644 index 0000000..c83d3b6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/AlertTitle.lua @@ -0,0 +1,117 @@ +local AlertRoot = script.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local FitFrame = require(Packages.FitFrame) +local FitFrameOnAxis = FitFrame.FitFrameOnAxis + +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local MARGIN = 24 + +local AlertTitle = Roact.PureComponent:extend("AlertTitle") + +local validateProps = t.strictInterface({ + layoutOrder = t.optional(t.number), + margin = t.optional(t.table), + maxWidth = t.optional(t.number), + minWidth = t.optional(t.number), + screenSize = t.Vector2, + title = t.string, + titleIcon = t.optional(t.union(t.table, t.string)), + titleIconSize = t.optional(t.number), + titlePadding = t.optional(t.number), + titlePaddingWithIcon = t.optional(t.number), +}) + +AlertTitle.defaultProps = { + margin = { + top = 0, + bottom = MARGIN, + left = MARGIN, + right = MARGIN, + }, + maxWidth = 400, + minWidth = 272, + titleIconSize = 48, + titlePadding = 12, + titlePaddingWithIcon = 24, +} + +function AlertTitle:render() + assert(validateProps(self.props)) + + local totalWidth = math.clamp(self.props.screenSize.X - self.props.margin.left - self.props.margin.right, + self.props.minWidth, self.props.maxWidth) + local innerWidth = totalWidth - self.props.margin.left - self.props.margin.right + + local titleTopMargin + if self.props.titleIcon then + titleTopMargin = self.props.titlePaddingWithIcon + else + titleTopMargin = self.props.titlePadding + end + + return withStyle(function(stylePalette) + local font = stylePalette.Font + local theme = stylePalette.Theme + + local headerSize = font.BaseSize * font.Header1.RelativeSize + + return Roact.createElement(FitFrameOnAxis, { + BackgroundTransparency = 1, + contentPadding = UDim.new(0, 8), + HorizontalAlignment = Enum.HorizontalAlignment.Center, + LayoutOrder = self.props.layoutOrder, + margin = { + top = titleTopMargin, + bottom = 0, + left = 0, + right = 0, + }, + minimumSize = UDim2.new(1, 0, 0, 0), + }, { + TitleIcon = self.props.titleIcon and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = self.props.titleIcon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + LayoutOrder = 0, + Size = UDim2.new(0, self.props.titleIconSize, 0, self.props.titleIconSize), + }), + TitleArea = Roact.createElement(FitFrameOnAxis, { + BackgroundTransparency = 1, + contentPadding = UDim.new(0, self.props.titlePadding), + HorizontalAlignment = Enum.HorizontalAlignment.Center, + LayoutOrder = 1, + minimumSize = UDim2.new(1, 0, 0, 0), + }, { + Title = Roact.createElement(GenericTextLabel, { + colorStyle = theme.TextEmphasis, + fontStyle = font.Header1, + maxSize = Vector2.new(innerWidth, headerSize * 2), + LayoutOrder = 1, + Text = self.props.title, + TextSize = headerSize, + TextTruncate = Enum.TextTruncate.AtEnd, + }), + Underline = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = theme.Divider.Color, + BackgroundTransparency = theme.Divider.Transparency, + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, 1), + }), + }), + }) + end) +end + +return AlertTitle \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/AlertTitle.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/AlertTitle.spec.lua new file mode 100644 index 0000000..418fda1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/AlertTitle.spec.lua @@ -0,0 +1,28 @@ +local AlertRoot = script.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + +local AlertTitle = require(script.Parent.AlertTitle) + + +return function() + describe("lifecycle", function() + it("should mount and unmount informative alerts without issue", function() + local element = mockStyleComponent({ + Item = Roact.createElement(AlertTitle, { + title = "Hello World", + screenSize = Vector2.new(100, 100), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Enum/AlertType.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Enum/AlertType.lua new file mode 100644 index 0000000..d484cff --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Enum/AlertType.lua @@ -0,0 +1,13 @@ +local AlertRoot = script.Parent.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local enumerate = require(Packages.enumerate) + +return enumerate("AlertType", { + "Informative", + "Interactive", + "Loading", +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InformativeAlert.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InformativeAlert.lua new file mode 100644 index 0000000..bca321b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InformativeAlert.lua @@ -0,0 +1,62 @@ +local AlertRoot = script.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Alert = require(AlertRoot.Alert) +local AlertType = require(AlertRoot.Enum.AlertType) + +local MIN_WIDTH = 272 +local MAX_WIDTH = 400 +local MARGIN = 24 +local PADDING_BETWEEN = 24 +local TITLE_PADDING = 12 +local TITLE_MARGIN_WITH_ICON = 24 +local TITLE_ICON_SIZE = 48 + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local enableAlertTitleIconConfig = UIBloxConfig.enableAlertTitleIconConfig + +local InformativeAlert = Roact.PureComponent:extend("InformativeAlert") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + bodyText = t.optional(t.string), + onMounted = t.optional(t.callback), + position = t.optional(t.UDim2), + screenSize = t.Vector2, + title = t.string, + titleIcon = t.optional(t.union(t.table, t.string)), +}) + +function InformativeAlert:render() + assert(validateProps(self.props)) + return Roact.createElement(Alert, { + anchorPoint = self.props.anchorPoint, + alertType = AlertType.Informative, + bodyText = self.props.bodyText, + margin = { + top = 0, + bottom = MARGIN, + left = MARGIN, + right = MARGIN, + }, + maxWidth = MAX_WIDTH, + minWidth = MIN_WIDTH, + onMounted = self.props.onMounted, + paddingBetween = PADDING_BETWEEN, + position = self.props.position, + screenSize = self.props.screenSize, + title = self.props.title, + titleIcon = enableAlertTitleIconConfig and self.props.titleIcon or nil, + titleIconSize = enableAlertTitleIconConfig and TITLE_ICON_SIZE or nil, + titlePadding = TITLE_PADDING, + titlePaddingWithIcon = TITLE_MARGIN_WITH_ICON, + }) +end + +return InformativeAlert \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InformativeAlert.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InformativeAlert.spec.lua new file mode 100644 index 0000000..1a81edd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InformativeAlert.spec.lua @@ -0,0 +1,27 @@ +return function() + local AlertRoot = script.Parent + local DialogRoot = AlertRoot.Parent + local AppRoot = DialogRoot.Parent + local UIBlox = AppRoot.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local InformativeAlert = require(AlertRoot.InformativeAlert) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + Item = Roact.createElement(InformativeAlert, { + screenSize = Vector2.new(100, 100), + title = "Hello World", + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InteractiveAlert.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InteractiveAlert.lua new file mode 100644 index 0000000..9eea0cf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InteractiveAlert.lua @@ -0,0 +1,76 @@ +local AlertRoot = script.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Alert = require(AlertRoot.Alert) +local AlertType = require(AlertRoot.Enum.AlertType) + +local MIN_WIDTH = 272 +local MAX_WIDTH = 400 +local MARGIN = 24 +local PADDING_BETWEEN = 24 +local TITLE_PADDING = 12 +local TITLE_MARGIN_WITH_ICON = 24 +local TITLE_ICON_SIZE = 48 + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local enableAlertTitleIconConfig = UIBloxConfig.enableAlertTitleIconConfig +local validateButtonStack = require(AppRoot.Button.Validator.validateButtonStack) + +local InteractiveAlert = Roact.PureComponent:extend("InteractiveAlert") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + bodyText = t.optional(t.string), + buttonStackInfo = validateButtonStack, + middleContent = t.optional(t.callback), + onMounted = t.optional(t.callback), + onAbsoluteSizeChanged = t.optional(t.callback), + position = t.optional(t.UDim2), + screenSize = t.Vector2, + title = t.string, + titleIcon = t.optional(t.union(t.table, t.string)), + + --Gamepad props + defaultChildRef = t.optional(t.table), + isMiddleContentFocusable = t.optional(t.boolean), +}) + +function InteractiveAlert:render() + assert(validateProps(self.props)) + return Roact.createElement(Alert, { + anchorPoint = self.props.anchorPoint, + alertType = AlertType.Interactive, + bodyText = self.props.bodyText, + margin = { + top = 0, + bottom = MARGIN, + left = MARGIN, + right = MARGIN, + }, + maxWidth = MAX_WIDTH, + minWidth = MIN_WIDTH, + buttonStackInfo = self.props.buttonStackInfo, + middleContent = self.props.middleContent, + isMiddleContentFocusable = self.props.isMiddleContentFocusable, + onMounted = self.props.onMounted, + onAbsoluteSizeChanged = self.props.onAbsoluteSizeChanged, + paddingBetween = PADDING_BETWEEN, + position = self.props.position, + screenSize = self.props.screenSize, + title = self.props.title, + titleIcon = enableAlertTitleIconConfig and self.props.titleIcon or nil, + titleIconSize = enableAlertTitleIconConfig and TITLE_ICON_SIZE or nil, + titlePadding = TITLE_PADDING, + titlePaddingWithIcon = TITLE_MARGIN_WITH_ICON, + + defaultChildRef = self.props.defaultChildRef, + }) +end + +return InteractiveAlert \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InteractiveAlert.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InteractiveAlert.spec.lua new file mode 100644 index 0000000..2e1fa63 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/InteractiveAlert.spec.lua @@ -0,0 +1,36 @@ +return function() + local AlertRoot = script.Parent + local DialogRoot = AlertRoot.Parent + local AppRoot = DialogRoot.Parent + local UIBlox = AppRoot.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local InteractiveAlert = require(AlertRoot.InteractiveAlert) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + Item = Roact.createElement(InteractiveAlert, { + buttonStackInfo = { + buttons = { + { + props = { + onActivated = function() end, + }, + }, + }, + }, + screenSize = Vector2.new(100, 100), + title = "Hello World", + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/LoadingAlert.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/LoadingAlert.lua new file mode 100644 index 0000000..2130ee5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/LoadingAlert.lua @@ -0,0 +1,79 @@ +local AlertRoot = script.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Alert = require(AlertRoot.Alert) +local AlertType = require(AlertRoot.Enum.AlertType) + +local MIN_WIDTH = 272 +local MAX_WIDTH = 400 +local MARGIN = 24 +local PADDING_BETWEEN = 24 +local TITLE_PADDING = 12 +local TITLE_MARGIN_WITH_ICON = 24 +local TITLE_ICON_SIZE = 48 + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local enableAlertTitleIconConfig = UIBloxConfig.enableAlertTitleIconConfig + +local LoadingSpinner = require(UIBlox.App.Loading.LoadingSpinner) + +local LoadingAlert = Roact.PureComponent:extend("LoadingAlert") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + bodyText = t.optional(t.string), + onMounted = t.optional(t.callback), + position = t.optional(t.UDim2), + screenSize = t.Vector2, + title = t.string, + titleIcon = t.optional(t.union(t.table, t.string)), +}) + +function LoadingAlert:init() + self.renderMiddleContent = function() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 48), + BackgroundTransparency = 1, + }, { + Spinner = Roact.createElement(LoadingSpinner, { + size = UDim2.fromOffset(48, 48), + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + }) + }) + end +end + +function LoadingAlert:render() + assert(validateProps(self.props)) + return Roact.createElement(Alert, { + anchorPoint = self.props.anchorPoint, + alertType = AlertType.Loading, + margin = { + top = 0, + bottom = MARGIN, + left = MARGIN, + right = MARGIN, + }, + maxWidth = MAX_WIDTH, + minWidth = MIN_WIDTH, + middleContent = self.renderMiddleContent, + onMounted = self.props.onMounted, + paddingBetween = PADDING_BETWEEN, + position = self.props.position, + screenSize = self.props.screenSize, + title = self.props.title, + titleIcon = enableAlertTitleIconConfig and self.props.titleIcon or nil, + titleIconSize = enableAlertTitleIconConfig and TITLE_ICON_SIZE or nil, + titlePadding = TITLE_PADDING, + titlePaddingWithIcon = TITLE_MARGIN_WITH_ICON, + }) +end + +return LoadingAlert diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/LoadingAlert.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/LoadingAlert.spec.lua new file mode 100644 index 0000000..de04a15 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/LoadingAlert.spec.lua @@ -0,0 +1,27 @@ +return function() + local AlertRoot = script.Parent + local DialogRoot = AlertRoot.Parent + local AppRoot = DialogRoot.Parent + local UIBlox = AppRoot.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local LoadingAlert = require(AlertRoot.LoadingAlert) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + Item = Roact.createElement(LoadingAlert, { + screenSize = Vector2.new(100, 100), + title = "Hello World", + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Validator/validateAlert.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Validator/validateAlert.lua new file mode 100644 index 0000000..c69aafb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/Validator/validateAlert.lua @@ -0,0 +1,38 @@ +local validatorRoot = script.Parent +local ButtonRoot = validatorRoot.Parent +local AppRoot = ButtonRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local validateButtonProps = require(ButtonRoot.validateButtonProps) + +local ButtonType = require(ButtonRoot.Enum.ButtonType) + +return t.strictInterface({ + -- buttons: A table of button tables that contain props that PrimaryContextualButton, + -- AlertButton, PrimarySystemButton, or SecondaryButton allow. Also contains a prop "buttonType" + -- to determine which of these button types to use. + buttons = t.array(t.strictInterface({ + buttonType = enumerateValidator(ButtonType), + props = validateButtonProps, + })), + + buttonHeight = t.optional(t.numberMin(0)), + + -- forceFillDirection: What fill direction to force into. If nil, then the fillDirection + -- will be Vertical and automatically change to Horizontal if any button's text is + -- too long. + forcedFillDirection = t.optional(t.enum(Enum.FillDirection)), + + -- marginBetween: the margin between each button. + marginBetween = t.optional(t.numberMin(0)), + + -- minHorizontalButtonPadding: The minimum left and right padding used to calculate + -- the when the button text overflows and automatically changes fillDirection. + -- The overflow calculation will be if the length of the button text is over + -- the button size - (2 * minHorizontalButtonPadding). + minHorizontalButtonPadding = t.optional(t.numberMin(0)), +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/__stories__/Alert.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/__stories__/Alert.story.lua new file mode 100644 index 0000000..eca4b18 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Alert/__stories__/Alert.story.lua @@ -0,0 +1,126 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local AlertRoot = script.Parent.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local Images = require(UIBlox.App.ImageSet.Images) + +local BACKGROUND_IMAGE = "icons/status/premium_small" + +local Alert = require(AlertRoot.Alert) +local AlertType = require(AlertRoot.Enum.AlertType) +local ButtonType = require(AppRoot.Button.Enum.ButtonType) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local enableAlertTitleIconConfig = UIBloxConfig.enableAlertTitleIconConfig + +local function close() + print("close") +end + +local function confirm() + print("confirm") +end + +local AlertContainer = Roact.PureComponent:extend("AlertContainer") + +function AlertContainer:init() + self.screenSize = nil + self.screenRef = Roact.createRef() + self.state = { + screenSize = Vector2.new(0, 0), + } + + self.changeScreenSize = function(rbx) + if self.state.screenSize ~= rbx.AbsoluteSize then + self:setState({ + screenSize = rbx.AbsoluteSize + }) + end + end + + self.renderMiddle = function() + return Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + LayoutOrder = 3, + Size = UDim2.new(1, 0, 0, 60), + },{ + CustomInner = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + LayoutOrder = 3, + Text = "Put any component you want here.", + TextSize = 13, + TextWrapped = true, + Size = UDim2.new(1, 0, 1, 0), + }), + }) + end +end + +function AlertContainer:didMount() + self.screenSize = self.screenRef and self.screenRef.current.AbsoluteSize +end + +function AlertContainer:render() + return Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 1, 0), + title = "AlertContainer", + subTitle = "<>", + }, { + Screen = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + [Roact.Ref] = self.screenRef, + [Roact.Change.AbsoluteSize] = self.changeScreenSize, + }, { + Alert = Roact.createElement(Alert, { + anchorPoint = Vector2.new(0.5, 0), + alertType = AlertType.Interactive, + bodyText = "Body text goes here. Both InformativeAlert and ".. + "InteractiveAlert use this component.", + buttonStackInfo = { + buttons = { + { + props = { + isDisabled = false, + onActivated = close, + text = "Cancel", + }, + }, + { + buttonType = ButtonType.PrimarySystem, + props = { + isDisabled = true, + onActivated = confirm, + text = "Confirm", + }, + }, + }, + }, + middleContent = self.renderMiddle, + position = UDim2.new(0.5, 0, 0, 10), + screenSize = self.state.screenSize, + title = "Alert Component. Title goes up to 2 lines max.", + titleIcon = enableAlertTitleIconConfig and Images[BACKGROUND_IMAGE] or nil, + }) + }) + }) +end + +return function(target) + local story = Roact.createElement(StoryView, {}, { + Roact.createElement(AlertContainer) + }) + local handle = Roact.mount(story, target, "Alert") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/EducationalModal.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/EducationalModal.lua new file mode 100644 index 0000000..d50da52 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/EducationalModal.lua @@ -0,0 +1,170 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local FitFrame = require(Packages.FitFrame) +local FitFrameVertical = FitFrame.FitFrameVertical + +local ImageSetLabel = require(UIBlox.Core.ImageSet.ImageSetComponent).Label +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local withStyle = require(UIBlox.Core.Style.withStyle) +local ButtonType = require(UIBlox.App.Button.Enum.ButtonType) +local getIconSize = require(AppRoot.ImageSet.getIconSize) +local IconSize = require(AppRoot.ImageSet.Enum.IconSize) + +local PartialPageModal = require(ModalRoot.PartialPageModal) + +local BODY_CONTENTS_WIDTH = 253 +local BODY_CONTENTS_MARGIN = 20 + +local EducationalModal = Roact.PureComponent:extend("EducationalModal") + +EducationalModal.validateProps = t.strictInterface({ + bodyContents = t.array(t.strictInterface({ + icon = t.union(t.string, t.table), + text = t.string, + layoutOrder = t.integer, + isSystemMenuIcon = t.optional(t.boolean), + })), + cancelText = t.string, + confirmText = t.string, + titleText = t.string, + titleBackgroundImageProps = t.strictInterface({ + image = t.string, + imageHeight = t.number, + }), + screenSize = t.Vector2, + + onDismiss = t.callback, + onCancel = t.callback, + onConfirm = t.callback, +}) + +local function ContentItem(props) + local totalTextSize = Vector2.new(16, 16) + local paddingBetween = 8 + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local font = stylePalette.Font + local textSize = font.Body.RelativeSize * font.BaseSize + + return Roact.createElement("Frame", { + Size = UDim2.new(0, BODY_CONTENTS_WIDTH, 0, totalTextSize.Y), + BackgroundTransparency = 1, + LayoutOrder = props.layoutOrder, + }, { + HorizontalLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + Padding = props.isSystemMenuIcon and UDim.new(0, paddingBetween + 2) + or UDim.new(0, paddingBetween), + VerticalAlignment = Enum.VerticalAlignment.Center + }), + Padding = Roact.createElement("UIPadding", { + PaddingLeft = props.isSystemMenuIcon and UDim.new(0, 2) + or UDim.new(0, 0), + }), + Icon = props.isSystemMenuIcon and Roact.createElement(ImageSetLabel, { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(32, 32), + Image = "rbxasset://textures/ui/TopBar/iconBase.png", + LayoutOrder = 1, + }, { + Icon = Roact.createElement(ImageSetLabel, { + ZIndex = 1, + BackgroundTransparency = 1, + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.fromOffset(24, 24), + Image = "rbxasset://textures/ui/TopBar/coloredlogo.png", + }), + }) or Roact.createElement(ImageSetLabel, { + Image = props.icon, + Size = UDim2.fromOffset(getIconSize(IconSize.Medium), getIconSize(IconSize.Medium)), + ImageColor3 = theme.IconDefault.Color, + ImageTransparency = theme.IconDefault.Transparency, + BackgroundTransparency = 1, + LayoutOrder = 1, + }), + Roact.createElement(GenericTextLabel, { + Size = UDim2.new(1, 0, 0, totalTextSize.Y), + BackgroundTransparency = 1, + Text = props.text, + TextSize = textSize, + colorStyle = theme.TextDefault, + TextTransparency = theme.TextDefault.Transparency, + fontStyle = font.Body, + TextXAlignment = Enum.TextXAlignment.Left, + TextWrapped = true, + LayoutOrder = 2, + }), + }) + end) +end + +function EducationalModal:init() + self.contentSize, self.changeContentSize = Roact.createBinding(Vector2.new(0, 0)) +end + +function EducationalModal:render() + local props = self.props + + local elements = {} + for _, content in ipairs(props.bodyContents) do + local current = Roact.createElement(ContentItem, { + icon = content.icon, + text = content.text, + layoutOrder = content.layoutOrder, + isSystemMenuIcon = content.isSystemMenuIcon, + }) + table.insert(elements, current) + end + + return Roact.createElement(PartialPageModal, { + title = props.titleText, + titleBackgroundImageProps = props.titleBackgroundImageProps, + screenSize = props.screenSize, + bottomPadding = 50, + buttonStackProps = { + buttons = { + { + props = { + isDisabled = false, + onActivated = props.onCancel, + text = props.cancelText, + }, + }, + { + buttonType = ButtonType.PrimarySystem, + props = { + isDisabled = false, + onActivated = props.onConfirm, + text = props.confirmText, + }, + }, + }, + }, + onCloseClicked = props.onDismiss, + }, { + BodyContents = Roact.createElement(FitFrameVertical, { + BackgroundTransparency = 1, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + width = UDim.new(1, 0), + contentPadding = UDim.new(0, 28), + margin = { + top = BODY_CONTENTS_MARGIN, + bottom = BODY_CONTENTS_MARGIN, + }, + [Roact.Change.AbsoluteSize] = function(rbx) + self.changeContentSize(rbx.AbsoluteSize) + end, + }, elements), + }) +end + +return EducationalModal diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/EducationalModal.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/EducationalModal.spec.lua new file mode 100644 index 0000000..892462f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/EducationalModal.spec.lua @@ -0,0 +1,61 @@ +return function() + local ModalRoot = script.Parent + local DialogRoot = ModalRoot.Parent + local AppRoot = DialogRoot.Parent + local UIBlox = AppRoot.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local Images = require(Packages.UIBlox.App.ImageSet.Images) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local EducationalModal = require(script.Parent.EducationalModal) + + local requiredProps = { + bodyContents = { + { + icon = Images["icons/logo/block"], + text = "Body 1", + layoutOrder = 1 + }, + { + icon = Images["icons/menu/home_on"], + text = "Body 2", + layoutOrder = 2 + }, + { + icon = Images["icons/menu/games_on"], + text = "Body 3", + layoutOrder = 3 + }, + }, + cancelText = "Cancel", + confirmText = "Confirm", + titleText = "Title", + titleBackgroundImageProps = { + image = "rbxassetid://2610133241", + imageHeight = 200, + }, + screenSize = Vector2.new(1920, 1080), + + onDismiss = function() + print("Dismiss") + end, + onCancel = function() + print("Cancel") + end, + onConfirm = function() + print("Confirm") + end, + } + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + EducationalModalDialog = Roact.createElement(EducationalModal, requiredProps), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/FullPageModal.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/FullPageModal.lua new file mode 100644 index 0000000..8703732 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/FullPageModal.lua @@ -0,0 +1,92 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local FitFrame = require(Packages.FitFrame) +local FitFrameVertical = FitFrame.FitFrameVertical + +local ButtonStack = require(AppRoot.Button.ButtonStack) + +local ModalTitle = require(ModalRoot.ModalTitle) +local ModalWindow = require(ModalRoot.ModalWindow) + +local FullPageModal = Roact.PureComponent:extend("FullPageModal") + +local MARGIN = 24 + +local validateProps = t.strictInterface({ + screenSize = t.Vector2, + [Roact.Children] = t.table, + + position = t.optional(t.UDim2), + title = t.optional(t.string), + + buttonStackProps = t.optional(t.table), -- Button stack validates the contents + + onCloseClicked = t.optional(t.callback), +}) + +function FullPageModal:init() + self.state = { + buttonFrameSize = Vector2.new(0, 0), + } + + self.changeButtonFrameSize = function(rbx) + if self.state.buttonFrameSize ~= rbx.AbsoluteSize then + self:setState({ + buttonFrameSize = rbx.AbsoluteSize + }) + end + end +end + + +function FullPageModal:render() + assert(validateProps(self.props)) + + return Roact.createElement(ModalWindow, { + isFullHeight = true, + screenSize = self.props.screenSize, + position = self.props.position, + }, { + TitleContainer = Roact.createElement(ModalTitle, { + title = self.props.title, + onCloseClicked = self.props.onCloseClicked, + }), + Content = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, -ModalTitle:GetHeight()), + Position = UDim2.new(0, 0, 0, ModalTitle:GetHeight()), + BackgroundTransparency = 1, + }, { + Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, MARGIN), + PaddingRight = UDim.new(0, MARGIN), + PaddingBottom = UDim.new(0, MARGIN), + }), + MiddleContent = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, -self.state.buttonFrameSize.Y), + BackgroundTransparency = 1, + LayoutOrder = 1 + }, self.props[Roact.Children]), + Buttons = self.props.buttonStackProps and Roact.createElement(FitFrameVertical, { + BackgroundTransparency = 1, + width = UDim.new(1, 0), + LayoutOrder = 2, + [Roact.Change.AbsoluteSize] = self.changeButtonFrameSize, + }, { + Roact.createElement(ButtonStack, self.props.buttonStackProps), + }), + }) + }) +end + +return FullPageModal \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/FullPageModal.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/FullPageModal.spec.lua new file mode 100644 index 0000000..fde98b9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/FullPageModal.spec.lua @@ -0,0 +1,56 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + +local ButtonType = require(AppRoot.Button.Enum.ButtonType) + +local FullPageModal = require(script.Parent.FullPageModal) + +return function() + describe("lifecycle", function() + it("should mount and unmount FullPageModal without issue", function() + local element = mockStyleComponent({ + FullPageModalContainer = Roact.createElement(FullPageModal, { + position = UDim2.new(0, 0, 0, 0), + title = "Title", + screenSize = Vector2.new(1920, 1080), + buttonStackProps = { + buttons = { + { + props = { + isDisabled = false, + onActivated = function() print("Cancel button was clicked") end, + text = "Cancel", + }, + }, + { + buttonType = ButtonType.PrimarySystem, + props = { + isDisabled = false, + onActivated = function() print("Confirm button was clicked") end, + text = "Confirm", + }, + }, + }, + }, + onCloseClicked = function() print("Close button was clicked") end, + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + Size = UDim2.new(1, 0, 0, 60), + }), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalTitle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalTitle.lua new file mode 100644 index 0000000..7ac2a2d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalTitle.lua @@ -0,0 +1,126 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local CoreRoot = UIBlox.Core +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(AppRoot.ImageSet.Images) +local ImageSetComponent = require(CoreRoot.ImageSet.ImageSetComponent) +local Controllable = require(CoreRoot.Control.Controllable) +local GenericTextLabel = require(CoreRoot.Text.GenericTextLabel.GenericTextLabel) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local X_BUTTON_SIZE = 36 +local X_LEFT_PADDING = 6 +local X_IMAGE = "icons/navigation/close" +local TITLE_HEIGHT = 48 +local TITLE_MAX_HEIGHT_WITH_IMAGE = 261 + +local ModalTitle = Roact.PureComponent:extend("ModalTitle") + +local validateProps = t.strictInterface({ + title = t.string, + position = t.optional(t.UDim2), + anchor = t.optional(t.Vector2), + onCloseClicked = t.optional(t.callback), + titleBackgroundImageProps = t.optional(t.strictInterface({ + image = t.string, + imageHeight = t.number, + })), +}) + +ModalTitle.defaultProps = { + title = "", + position = UDim2.new(0.5, 0, 0, 0), + anchor = Vector2.new(0.5, 0), +} + +function ModalTitle:GetHeight() + return TITLE_HEIGHT +end + +function ModalTitle:render() + assert(validateProps(self.props)) + local titleBackground = self.props.titleBackgroundImageProps + + return withStyle(function(stylePalette) + local font = stylePalette.Font + local theme = stylePalette.Theme + + local headerSize = font.BaseSize * font.Header1.RelativeSize + + local titleText = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, TITLE_HEIGHT), + BackgroundTransparency = 1, + }, { + CloseButton = Roact.createElement(Controllable, { + controlComponent = { + component = ImageSetComponent.Button, + props = { + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0, TITLE_HEIGHT * 0.5 + X_LEFT_PADDING, 0.5, 0), + Size = UDim2.new(0, TITLE_HEIGHT, 0, TITLE_HEIGHT), + [Roact.Event.Activated] = self.props.onCloseClicked, + }, + children = { + InputFillImage = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Size = UDim2.new(0, X_BUTTON_SIZE, 0, X_BUTTON_SIZE), + Image = Images[X_IMAGE], + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + }) + } + }, + onStateChanged = function(...) end, + }), + Title = Roact.createElement(GenericTextLabel, { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + colorStyle = theme.TextEmphasis, + fontStyle = font.Header1, + LayoutOrder = 1, + Text = self.props.title, + TextSize = headerSize, + TextTruncate = Enum.TextTruncate.AtEnd, + }), + Underline = not titleBackground and Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 1, 0), + BorderSizePixel = 0, + BackgroundColor3 = theme.Divider.Color, + BackgroundTransparency = theme.Divider.Transparency, + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, 1), + }), + }) + + if titleBackground then + local bgHeight = titleBackground.imageHeight + local height = math.min(math.max(TITLE_HEIGHT, bgHeight), TITLE_MAX_HEIGHT_WITH_IMAGE) + return Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.new(1, 0, 0, height), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundTransparency = 1, + ScaleType = Enum.ScaleType.Crop, + BorderSizePixel = 0, + Image = titleBackground.image, + ImageColor3 = Color3.new(255, 255, 255), + }, { + TitleText = titleText, + }) + else + return titleText + end + end) +end + +return ModalTitle \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalTitle.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalTitle.spec.lua new file mode 100644 index 0000000..c1d4c53 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalTitle.spec.lua @@ -0,0 +1,51 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + +local ModalTitle = require(script.Parent.ModalTitle) + +return function() + describe("lifecycle", function() + it("should mount and unmount ModalTitle without issue", function() + local element = mockStyleComponent({ + ModalTitleContainer = Roact.createElement(ModalTitle, { + title = "Title", + position = UDim2.new(0, 0, 0, 0), + anchor = Vector2.new(0, 0), + onCloseClicked = function() end, + titleBackgroundImageProps = { + image = "rbxassetid://2610133241", + imageHeight = 200, + }, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should throw on invalid props", function() + local element = mockStyleComponent({ + ModalTitleContainer = Roact.createElement(ModalTitle, { + title = "Title", + position = UDim2.new(0, 0, 0, 0), + anchor = Vector2.new(0, 0), + onCloseClicked = function() end, + titleBackgroundImageProps = { + image = "rbxassetid://2610133241", + }, + }) + }) + + expect(function() + Roact.mount(element) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalWindow.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalWindow.lua new file mode 100644 index 0000000..cb398a5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalWindow.lua @@ -0,0 +1,125 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local FitFrame = require(Packages.FitFrame) +local FitFrameVertical = FitFrame.FitFrameVertical + +local Images = require(AppRoot.ImageSet.Images) +local withStyle = require(UIBlox.Core.Style.withStyle) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local SLICE_CENTER = Rect.new(8, 8, 9, 9) + +local ANCHORED_BACKGROUND_IMAGE = "component_assets/bullet_17" +local FLOATING_BACKGROUND_IMAGE = "component_assets/circle_17" + +local ModalWindow = Roact.PureComponent:extend("ModalWindow") + +local MAX_WIDTH = 540 + +local validateProps = t.strictInterface({ + isFullHeight = t.boolean, + screenSize = t.Vector2, + [Roact.Children] = t.table, + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), +}) + +function ModalWindow:init() + self.contentSize, self.changeContentSize = Roact.createBinding(Vector2.new(0, 0)) +end + +-- Used to determine width of middle content for dynamically sizing children, see PartialPageModal +function ModalWindow:getWidth(screenWidth) + return self:isFullWidth(screenWidth) and screenWidth or MAX_WIDTH +end + +-- Used to determine if the modal is anchored in the middle or bottom of the screen +function ModalWindow:isFullWidth(screenWidth) + return screenWidth < MAX_WIDTH +end + +function ModalWindow:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local screenSize = self.props.screenSize + + local anchorPoint, backgroundImage, position, width + width = UDim.new(0, self:getWidth(screenSize.X)) + if self:isFullWidth(screenSize.X) then + anchorPoint = Vector2.new(0.5, 1) + backgroundImage = ANCHORED_BACKGROUND_IMAGE + position = self.props.position or UDim2.new(0.5, 0, 1, 0) + else + anchorPoint = Vector2.new(0.5, 0.5) + backgroundImage = FLOATING_BACKGROUND_IMAGE + position = self.props.position or UDim2.new(0.5, 0, 0.5, 0) + end + + anchorPoint = self.props.anchorPoint or anchorPoint + + if self.props.isFullHeight then + return Roact.createElement(ImageSetComponent.Button, { + Position = position, + Size = UDim2.new(width, UDim.new(1, 0)), + AnchorPoint = anchorPoint, + BackgroundTransparency = 0, + BorderSizePixel = 0, + Image = Images[backgroundImage], + ImageColor3 = theme.BackgroundUIDefault.Color, + ImageTransparency = theme.BackgroundUIDefault.Transparency, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = SLICE_CENTER, + AutoButtonColor = false, + ClipsDescendants = true, + Selectable = false, + }, { + BackgroundImage = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.new(1, 0, 1, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, self.props[Roact.Children]) + }) + else + return Roact.createElement(ImageSetComponent.Button, { + Position = position, + Size = self.contentSize:map(function(absoluteSize) + return UDim2.new(0, absoluteSize.X, 0, absoluteSize.Y) + end), + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = Images[backgroundImage], + ImageColor3 = theme.BackgroundUIDefault.Color, + ImageTransparency = theme.BackgroundUIDefault.Transparency, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = SLICE_CENTER, + AutoButtonColor = false, + ClipsDescendants = true, + Selectable = false, + }, { + BackgroundImage = Roact.createElement(FitFrameVertical, { + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + BorderSizePixel = 0, + width = width, + [Roact.Change.AbsoluteSize] = function(rbx) + self.changeContentSize(rbx.AbsoluteSize) + end, + }, self.props[Roact.Children]) + }) + end + end) +end + +return ModalWindow \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalWindow.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalWindow.spec.lua new file mode 100644 index 0000000..6feefba --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/ModalWindow.spec.lua @@ -0,0 +1,122 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + +local ModalWindow = require(script.Parent.ModalWindow) + +return function() + describe("lifecycle", function() + it("should mount and unmount full height and large width ModalWindow without issue", function() + local element = mockStyleComponent({ + PartialPageModalContainer = Roact.createElement(ModalWindow, { + isFullHeight = true, + screenSize = Vector2.new(1920, 1080), + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + Size = UDim2.new(1, 0, 1, 0), + }), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should mount and unmount full height and small width ModalWindow without issue", function() + local element = mockStyleComponent({ + PartialPageModalContainer = Roact.createElement(ModalWindow, { + isFullHeight = true, + screenSize = Vector2.new(400, 1080), + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + Size = UDim2.new(1, 0, 1, 0), + }), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should mount and unmount partial height and large width ModalWindow without issue", function() + local element = mockStyleComponent({ + PartialPageModalContainer = Roact.createElement(ModalWindow, { + isFullHeight = false, + screenSize = Vector2.new(1920, 1080), + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + Size = UDim2.new(1, 0, 1, 0), + }), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should mount and unmount partial height and small width ModalWindow without issue", function() + local element = mockStyleComponent({ + PartialPageModalContainer = Roact.createElement(ModalWindow, { + isFullHeight = false, + screenSize = Vector2.new(400, 1080), + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + Size = UDim2.new(1, 0, 1, 0), + }), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should mount and unmount arbitrary anchorPoints without issue", function() + local element = mockStyleComponent({ + AnchoredPageModalContainer = Roact.createElement(ModalWindow, { + anchorPoint = Vector2.new(0.25, 0.75), + isFullHeight = true, + screenSize = Vector2.new(1920, 1080), + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + Size = UDim2.new(1, 0, 1, 0), + }), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should return correct isFullWidth when screen size is larger than modal max width", function() + assert(ModalWindow:isFullWidth(800) == false) + end) + + it("should return correct isFullWidth when screen size is smaller than modal max width", function() + assert(ModalWindow:isFullWidth(400) == true) + end) + + it("should return correct getWidth when screen size is larger than modal max width", function() + assert(ModalWindow:getWidth(800) == 540) + end) + + it("should return correct getWidth when screen size is smaller than modal max width", function() + assert(ModalWindow:getWidth(400) == 400) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/PartialPageModal.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/PartialPageModal.lua new file mode 100644 index 0000000..98986b3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/PartialPageModal.lua @@ -0,0 +1,93 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local ButtonStack = require(AppRoot.Button.ButtonStack) + +local FitFrame = require(Packages.FitFrame) +local FitFrameVertical = FitFrame.FitFrameVertical + +local ModalTitle = require(ModalRoot.ModalTitle) +local ModalWindow = require(ModalRoot.ModalWindow) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local PartialPageModal = Roact.PureComponent:extend("PartialPageModal") + +local MARGIN = 24 + +local validateProps = t.strictInterface({ + screenSize = t.Vector2, + [Roact.Children] = t.table, + + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + title = t.optional(t.string), + titleBackgroundImageProps = t.optional(t.strictInterface({ + image = t.string, + imageHeight = t.number, + })), + bottomPadding = t.optional(t.number), + + buttonStackProps = t.optional(t.table), -- Button stack validates the contents + + onCloseClicked = t.optional(t.callback), + + contentPadding = t.optional(t.UDim), +}) + +-- Used to determine width of middle content for dynamically sizing children in the content +-- Example: Multi-lined text that requires to know the width of its space that can also dynamically change its height. +function PartialPageModal:getMiddleContentWidth(screenWidth) + return ModalWindow:getWidth(screenWidth) - 2 * MARGIN +end + +function PartialPageModal:render() + assert(validateProps(self.props)) + local screenSize = self.props.screenSize + local bottomPadding = self.props.bottomPadding or MARGIN + + -- Only add bottom padding when window is anchored to the bottom + -- Used to align buttons with previous UI + if not ModalWindow:isFullWidth(screenSize.X) then + bottomPadding = MARGIN + end + + return Roact.createElement(ModalWindow, { + isFullHeight = false, + screenSize = screenSize, + position = self.props.position, + anchorPoint = self.props.anchorPoint, + }, { + TitleContainer = Roact.createElement(ModalTitle, { + title = self.props.title, + titleBackgroundImageProps = self.props.titleBackgroundImageProps, + onCloseClicked = self.props.onCloseClicked, + }), + Content = Roact.createElement(FitFrameVertical, { + Position = UDim2.new(0, 0, 0, ModalTitle.TITLE_HEIGHT), + width = UDim.new(1, 0), + margin = { + top = 0, + bottom = bottomPadding, + left = MARGIN, + right = MARGIN, + }, + BackgroundTransparency = 1, + contentPadding = UIBloxConfig.enableExperimentalGamepadSupport and self.props.contentPadding or nil, + }, { + MiddlContent = Roact.createElement(FitFrameVertical, { + width = UDim.new(1, 0), + BackgroundTransparency = 1, + }, self.props[Roact.Children]), + Buttons = self.props.buttonStackProps and Roact.createElement(ButtonStack, self.props.buttonStackProps), + }) + }) +end + +return PartialPageModal \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/PartialPageModal.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/PartialPageModal.spec.lua new file mode 100644 index 0000000..62fb3b4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/PartialPageModal.spec.lua @@ -0,0 +1,88 @@ +local ModalRoot = script.Parent +local DialogRoot = ModalRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + +local ButtonType = require(AppRoot.Button.Enum.ButtonType) + +local PartialPageModal = require(script.Parent.PartialPageModal) + +return function() + describe("lifecycle", function() + it("should mount and unmount PartialPageModal without issue", function() + local element = mockStyleComponent({ + PartialPageModalContainer = Roact.createElement(PartialPageModal, { + position = UDim2.new(0, 0, 0, 0), + anchorPoint = Vector2.new(0, 0.5), + title = "Title", + titleBackgroundImageProps = { + image = "rbxassetid://2610133241", + imageHeight = 200, + }, + screenSize = Vector2.new(1920, 1080), + bottomPadding = 100, + buttonStackProps = { + buttons = { + { + props = { + isDisabled = false, + onActivated = function() print("Cancel button was clicked") end, + text = "Cancel", + }, + }, + { + buttonType = ButtonType.PrimarySystem, + props = { + isDisabled = false, + onActivated = function() print("Confirm button was clicked") end, + text = "Confirm", + }, + }, + }, + }, + onCloseClicked = function() print("Close button was clicked") end, + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(164, 86, 78), + Size = UDim2.new(1, 0, 1, 0), + }), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should throw on invalid props", function() + local element = mockStyleComponent({ + ModalTitleContainer = Roact.createElement(PartialPageModal, { + title = "Title", + titleBackgroundImageProps = { + image = "rbxassetid://2610133241", + }, + screenSize = Vector2.new(1920, 1080), + }) + }) + + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should return correct getMiddleContentWidth when screen size is larger than modal max width", function() + -- modal width - left margin - right margin + assert(PartialPageModal:getMiddleContentWidth(800) == 540 - 2 * 24) + end) + + it("should return correct getMiddleContentWidth when screen size is smaller than modal max width", function() + -- modal width - left margin - right margin + assert(PartialPageModal:getMiddleContentWidth(400) == 400 - 2 * 24) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/__stories__/ModalWindow.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/__stories__/ModalWindow.story.lua new file mode 100644 index 0000000..9131b58 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Modal/__stories__/ModalWindow.story.lua @@ -0,0 +1,111 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local ModalsRoot = script.Parent.Parent +local DialogRoot = ModalsRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local ModalWindow = require(ModalsRoot.ModalWindow) + +local PortraitModal = Roact.PureComponent:extend("PortraitModal") + +function PortraitModal:init() + self.screenSize = nil + self.screenRef = Roact.createRef() + self.state = { + screenSize = Vector2.new(0, 0), + isFullHeight = false + } + + self.changeScreenSize = function(rbx) + if self.state.screenSize ~= rbx.AbsoluteSize then + self:setState({ + screenSize = rbx.AbsoluteSize + }) + end + end + + self.toggleisFullHeight = function() + self:setState({ + isFullHeight = not self.state.isFullHeight + }) + end +end + +function PortraitModal:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + }), + ButtonControlsFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 50), + LayoutOrder = 1, + }, { + Grid = Roact.createElement("UIGridLayout", { + CellSize = UDim2.new(0, 200, 0, 45), + SortOrder = Enum.SortOrder.LayoutOrder, + }), + DisableControl = Roact.createElement("TextButton", { + Text = self.state.isFullHeight and "Fit" or "Full Height", + [Roact.Event.Activated] = self.toggleisFullHeight, + LayoutOrder = 1, + }), + }), + Overview = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, -50), + BackgroundTransparency = 1, + LayoutOrder = 2, + }, { + Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 1, 0), + title = "ModalWindowContainer", + subTitle = "Expand and shrink the width of the window to see how the modal behaves on different widths", + }, { + ViewFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + [Roact.Ref] = self.screenRef, + [Roact.Change.AbsoluteSize] = self.changeScreenSize, + } , { + ModalWindowContainer = Roact.createElement(ModalWindow, { + isFullHeight = self.state.isFullHeight, + screenSize = self.state.screenSize, + }, { + Custom = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 60), + },{ + CustomInner = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + LayoutOrder = 3, + Text = "Put any component you want here.", + TextSize = 13, + TextWrapped = true, + Size = UDim2.new(1, 0, 1, 0), + }), + }), + }) + }) + }) + }) + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(PortraitModal), + }), target, "PortraitModal") + + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Enum/AnimationState.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Enum/AnimationState.lua new file mode 100644 index 0000000..1ecaa83 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Enum/AnimationState.lua @@ -0,0 +1,6 @@ +return { + Appearing = "Appearing", + Appeared = "Appeared", + Disappearing = "Disappearing", + Disappeared = "Disappeared", +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InformativeToast.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InformativeToast.lua new file mode 100644 index 0000000..324557e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InformativeToast.lua @@ -0,0 +1,66 @@ +local ToastRoot = script.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ToastFrame = require(ToastRoot.ToastFrame) +local validateToastIcon = require(ToastRoot.Validator.validateToastIcon) +local validateToastText = require(ToastRoot.Validator.validateToastText) + +local InformativeToast = Roact.PureComponent:extend("InformativeToast") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + iconProps = t.optional(validateToastIcon), + iconChildren = t.optional(t.table), + layoutOrder = t.optional(t.integer), + padding = t.optional(t.numberMin(0)), + position = t.optional(t.UDim2), + size = t.UDim2, + subtitleTextProps = t.optional(validateToastText), + textFrameSize = t.optional(t.UDim2), + titleTextProps = validateToastText, +}) + +InformativeToast.defaultProps = { + anchorPoint = Vector2.new(0.5, 0.5), + layoutOrder = 1, + position = UDim2.new(0.5, 0, 0.5, 0), + size = UDim2.new(1, 0, 1, 0), +} + +function InformativeToast:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + return Roact.createElement("Frame", { + AnchorPoint = self.props.anchorPoint, + BackgroundColor3 = theme.BackgroundUIContrast.Color, + BackgroundTransparency = theme.BackgroundUIContrast.Transparency, + BorderSizePixel = 0, + ClipsDescendants = true, + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + Size = self.props.size, + }, { + ToastFrame = Roact.createElement(ToastFrame, { + iconProps = self.props.iconProps, + iconChildren = self.props.iconChildren, + padding = self.props.padding, + subtitleTextProps = self.props.subtitleTextProps, + textFrameSize = self.props.textFrameSize, + titleTextProps = self.props.titleTextProps, + }), + }) + end) +end + +return InformativeToast diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InformativeToast.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InformativeToast.spec.lua new file mode 100644 index 0000000..61feb93 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InformativeToast.spec.lua @@ -0,0 +1,125 @@ +return function() + local Toast = script.Parent + local Dialog = Toast.Parent + local App = Dialog.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local Images = require(UIBlox.App.ImageSet.Images) + local TestStyle = require(UIBlox.App.Style.Validator.TestStyle) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local InformativeToast = require(Toast.InformativeToast) + + local ICON_SIZE = 36 + + local testText = "Item On Sale" + local testSubText = "test test test" + + local createInformativeToast = function(props) + return mockStyleComponent({ + InformativeToast = Roact.createElement(InformativeToast, props) + }) + end + + it("should throw on invalid titleTextProps", function() + local element = createInformativeToast({ + toastText = {}, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors with valid titleTextProps", function() + local element = createInformativeToast({ + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, 0, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with valid titleTextProps and subtitleTextProps", function() + local element = createInformativeToast({ + subtitleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.CaptionBody, + Size = UDim2.new(1, 0, 0.5, 0), + Text = testSubText, + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, 0, 0.5, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with icon", function() + local element = createInformativeToast({ + iconProps = { + Image = "rbxassetid://4126499279", + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, -ICON_SIZE, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with ImageSet compatible icon", function() + local element = createInformativeToast({ + iconProps = { + Image = Images["icons/status/warning"], + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, -ICON_SIZE, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with composed image", function() + local element = createInformativeToast({ + iconProps = { + Image = Images["icons/status/warning"], + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }, + iconChildren = { + Child = Roact.createElement("TextLabel"), + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, -ICON_SIZE, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InteractiveToast.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InteractiveToast.lua new file mode 100644 index 0000000..acbc596 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InteractiveToast.lua @@ -0,0 +1,106 @@ +local ToastRoot = script.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) +local SpringAnimatedItem = require(UIBloxRoot.Utility.SpringAnimatedItem) + +local ToastFrame = require(ToastRoot.ToastFrame) +local validateToastIcon = require(ToastRoot.Validator.validateToastIcon) +local validateToastText = require(ToastRoot.Validator.validateToastText) + +local ANIMATION_SPRING_SETTINGS = { + dampingRatio = 1, + frequency = 4, +} +local PRESSED_SCALE = 0.95 +local TOAST_BACKGROUND_IMAGE = Images["component_assets/circle_21"] +local TOAST_BORDER_IMAGE = Images["component_assets/circle_21_stroke_1"] +local TOAST_SLICE_CENTER = Rect.new(10, 10, 11, 11) + +local InteractiveToast = Roact.PureComponent:extend("InteractiveToast") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + iconProps = t.optional(validateToastIcon), + iconChildren = t.optional(t.table), + layoutOrder = t.optional(t.integer), + padding = t.optional(t.numberMin(0)), + position = t.optional(t.UDim2), + pressed = t.optional(t.boolean), + pressedScale = t.number, + size = t.UDim2, + subtitleTextProps = t.optional(validateToastText), + textFrameSize = t.optional(t.UDim2), + titleTextProps = validateToastText, +}) + +InteractiveToast.defaultProps = { + anchorPoint = Vector2.new(0.5, 0.5), + layoutOrder = 1, + position = UDim2.new(0.5, 0, 0.5, 0), + pressedScale = PRESSED_SCALE, + size = UDim2.new(1, 0, 1, 0), +} + +function InteractiveToast:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + return Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = self.props.anchorPoint, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = TOAST_BACKGROUND_IMAGE, + ImageColor3 = theme.SystemPrimaryContent.Color, + ImageTransparency = theme.SystemPrimaryContent.Transparency, + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + ScaleType = Enum.ScaleType.Slice, + Size = self.props.size, + SliceCenter = TOAST_SLICE_CENTER, + }, { + Scaler = Roact.createElement(SpringAnimatedItem.AnimatedUIScale, { + springOptions = ANIMATION_SPRING_SETTINGS, + animatedValues = { + scale = self.props.pressed and self.props.pressedScale or 1, + }, + mapValuesToProps = function(values) + return { + Scale = values.scale, + } + end, + }), + ToastBorder = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = TOAST_BORDER_IMAGE, + ImageColor3 = theme.TextDefault.Color, + ImageTransparency = theme.TextDefault.Transparency, + Position = UDim2.new(0.5, 0, 0.5, 0), + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(1, 0, 1, 0), + SliceCenter = TOAST_SLICE_CENTER, + }), + ToastFrame = Roact.createElement(ToastFrame, { + iconProps = self.props.iconProps, + iconChildren = self.props.iconChildren, + padding = self.props.padding, + subtitleTextProps = self.props.subtitleTextProps, + textFrameSize = self.props.textFrameSize, + titleTextProps = self.props.titleTextProps, + }), + }) + end) +end + +return InteractiveToast diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InteractiveToast.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InteractiveToast.spec.lua new file mode 100644 index 0000000..6e342af --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/InteractiveToast.spec.lua @@ -0,0 +1,147 @@ +return function() + local Toast = script.Parent + local Dialog = Toast.Parent + local App = Dialog.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local Images = require(UIBlox.App.ImageSet.Images) + local TestStyle = require(UIBlox.App.Style.Validator.TestStyle) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local InteractiveToast = require(Toast.InteractiveToast) + + local ICON_SIZE = 36 + + local testText = "System Outage" + local testSubText = "Tap to see more information" + + local createInteractiveToast = function(props) + return mockStyleComponent({ + InteractiveToast = Roact.createElement(InteractiveToast, props) + }) + end + + it("should throw on invalid titleTextProps", function() + local element = createInteractiveToast({ + textFrameSize = UDim2.new(1, 0, 1, 0), + titleTextProps = {}, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors with valid titleTextProps", function() + local element = createInteractiveToast({ + textFrameSize = UDim2.new(1, 0, 1, 0), + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, 0, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with valid titleTextProps and subtitleTextProps", function() + local element = createInteractiveToast({ + textFrameSize = UDim2.new(1, 0, 1, 0), + subtitleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.CaptionBody, + Size = UDim2.new(1, 0, 0.5, 0), + Text = testSubText, + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, 0, 0.5, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with icon", function() + local element = createInteractiveToast({ + textFrameSize = UDim2.new(1, 0, 1, 0), + iconProps = { + Image = "rbxassetid://4126499279", + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, -ICON_SIZE, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with ImageSet compatible icon", function() + local element = createInteractiveToast({ + textFrameSize = UDim2.new(1, 0, 1, 0), + iconProps = { + Image = Images["icons/status/warning"], + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, -ICON_SIZE, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with composed image", function() + local element = createInteractiveToast({ + textFrameSize = UDim2.new(1, 0, 1, 0), + iconProps = { + Image = Images["icons/status/warning"], + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }, + iconChildren = { + Child = Roact.createElement("TextLabel"), + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, -ICON_SIZE, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when pressed", function() + local element = createInteractiveToast({ + pressed = true, + textFrameSize = UDim2.new(1, 0, 1, 0), + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, -ICON_SIZE, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/SlideFromTopToast.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/SlideFromTopToast.lua new file mode 100644 index 0000000..7ee53b1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/SlideFromTopToast.lua @@ -0,0 +1,245 @@ +local ToastRoot = script.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local SlidingDirection = require(UIBloxRoot.Core.Animation.Enum.SlidingDirection) +local SlidingContainer = require(UIBloxRoot.Core.Animation.SlidingContainer) +local StateTable = require(UIBloxRoot.StateTable.StateTable) + +local AnimationState = require(ToastRoot.Enum.AnimationState) +local InformativeToast = require(ToastRoot.InformativeToast) +local InteractiveToast = require(ToastRoot.InteractiveToast) +local ToastContainer = require(ToastRoot.ToastContainer) +local validateToastContent = require(ToastRoot.Validator.validateToastContent) + +local SlideFromTopToast = Roact.PureComponent:extend("SlideFromTopToast") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + duration = t.optional(t.number), + layoutOrder = t.optional(t.integer), + position = t.optional(t.UDim2), + show = t.optional(t.boolean), + size = t.optional(t.UDim2), + springOptions = t.optional(t.table), + toastContent = validateToastContent, +}) + +SlideFromTopToast.defaultProps = { + anchorPoint = Vector2.new(0.5, 0), + position = UDim2.new(0.5, 0, 0, 20), + show = true, +} + +local function toastContentEqual(toastContent1, toastContent2) + if toastContent1.iconColorStyle ~= toastContent2.iconColorStyle + or toastContent1.iconImage ~= toastContent2.iconImage + or toastContent1.iconSize ~= toastContent2.iconSize + or toastContent1.iconChildren ~= toastContent2.iconChildren + or toastContent1.onActivated ~= toastContent2.onActivated + or toastContent1.onDismissed ~= toastContent2.onDismissed + or toastContent1.swipeUpDismiss ~= toastContent2.swipeUpDismiss + or toastContent1.toastSubtitle ~= toastContent2.toastSubtitle + or toastContent1.toastTitle ~= toastContent2.toastTitle then + return false + end + return true +end + +function SlideFromTopToast:init() + self.isMounted = false + + self.currentToastContent = self.props.toastContent + + self.onActivated = function() + self.stateTable.events.Activated({ + activated = true, + }) + end + + self.onAppeared = function() + if self.currentToastContent.onAppeared then + self.currentToastContent.onAppeared() + end + local duration = self.props.duration + if duration and duration > 0 then + local currentToastContent = self.currentToastContent + delay(duration, function() + if currentToastContent == self.currentToastContent then + self.stateTable.events.AutoDismiss() + end + end) + end + end + + self.onComplete = function() + local duration = self.props.duration + + if self.state.currentState == AnimationState.Appearing and duration and duration <= 0 then + self.stateTable.events.AutoDismiss() + else + self.stateTable.events.AnimationComplete() + end + end + + self.onDisappeared = function() + if self.state.context.activated then + if self.currentToastContent.onActivated then + self.currentToastContent.onActivated() + end + else + if self.currentToastContent.onDismissed then + self.currentToastContent.onDismissed() + end + end + end + + self.onTouchSwipe = function(_, swipeDir) + if swipeDir == Enum.SwipeDirection.Up then + self.stateTable.events.ForceDismiss() + end + end + + self.renderInteractiveToast = function(props) + return Roact.createElement(InteractiveToast, props) + end + + self.renderInformativeToast = function(props) + return Roact.createElement(InformativeToast, props) + end + + self.setContext = function(_, _, data) + return data + end + + self.updateToastContent = function() + if self.currentToastContent ~= self.props.toastContent then + self.currentToastContent = self.props.toastContent + if self.props.show then + -- Show next toast content + self.stateTable.events.ForceAppear({ + activated = false, + }) + end + end + end + + local initialState = AnimationState.Disappeared + self.state = { + currentState = initialState, + context = { + activated = false, + }, + } + + local stateTableName = string.format("Animated(%s)", tostring(self)) + self.stateTable = StateTable.new(stateTableName, initialState, {}, { + [AnimationState.Appearing] = { + AnimationComplete = { nextState = AnimationState.Appeared, action = self.onAppeared }, + AutoDismiss = { nextState = AnimationState.Disappearing, action = self.onAppeared }, + ContentChanged = { nextState = AnimationState.Disappearing }, + ForceDismiss = { nextState = AnimationState.Disappearing }, + }, + [AnimationState.Appeared] = { + Activated = { nextState = AnimationState.Disappearing, action = self.setContext }, + AutoDismiss = { nextState = AnimationState.Disappearing }, + ContentChanged = { nextState = AnimationState.Disappearing }, + ForceDismiss = { nextState = AnimationState.Disappearing }, + }, + [AnimationState.Disappearing] = { + AnimationComplete = { nextState = AnimationState.Disappeared, action = self.onDisappeared }, + }, + [AnimationState.Disappeared] = { + ContentChanged = { nextState = AnimationState.Appearing, action = self.updateToastContent }, + ForceAppear = { nextState = AnimationState.Appearing, action = self.setContext }, + }, + }) + + self.stateTable:onStateChange(function(oldState, newState, updatedContext) + if self.isMounted and oldState ~= newState then + self:setState({ + currentState = newState, + context = updatedContext, + }) + end + end) +end + +function SlideFromTopToast:isShowing() + return self.state.currentState == AnimationState.Appearing or self.state.currentState == AnimationState.Appeared +end + +function SlideFromTopToast:render() + assert(validateProps(self.props)) + + local onActivated = self.currentToastContent.onActivated + local swipeUpDismiss = self.currentToastContent.swipeUpDismiss + if swipeUpDismiss == nil then + swipeUpDismiss = true + end + return Roact.createElement(SlidingContainer, { + show = self:isShowing(), + layoutOrder = self.props.layoutOrder, + onComplete = self.onComplete, + slidingDirection = SlidingDirection.Down, + springOptions = self.props.springOptions, + }, { + ToastContainer = Roact.createElement(ToastContainer, { + anchorPoint = self.props.anchorPoint, + position = self.props.position, + size = self.props.size, + -- Toast content props + iconColorStyle = self.currentToastContent.iconColorStyle, + iconImage = self.currentToastContent.iconImage, + iconSize = self.currentToastContent.iconSize, + iconChildren = self.currentToastContent.iconChildren, + onActivated = onActivated and self.onActivated, + onTouchSwipe = swipeUpDismiss and self.onTouchSwipe, + renderToast = onActivated and self.renderInteractiveToast or self.renderInformativeToast, + toastSubtitle = self.currentToastContent.toastSubtitle, + toastTitle = self.currentToastContent.toastTitle, + }), + }) +end + +function SlideFromTopToast:didMount() + self.isMounted = true + if self.props.show then + self.stateTable.events.ForceAppear({ + activated = false, + }) + end +end + +function SlideFromTopToast:willUnmount() + self.isMounted = false +end + +function SlideFromTopToast:didUpdate(oldProps, oldState) + if oldProps.show ~= self.props.show then + if self.props.show then + self.stateTable.events.ForceAppear({ + activated = false, + }) + else + self.stateTable.events.ForceDismiss() + end + end + if not toastContentEqual(oldProps.toastContent, self.props.toastContent) then + -- Toast content updated, need to force dismiss current toast and show the new one + self.stateTable.events.ContentChanged({ + activated = false, + }) + end + if oldState.currentState ~= self.state.currentState and + self.state.currentState == AnimationState.Disappeared then + self.updateToastContent() + end +end + +return SlideFromTopToast \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/SlideFromTopToast.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/SlideFromTopToast.spec.lua new file mode 100644 index 0000000..7157668 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/SlideFromTopToast.spec.lua @@ -0,0 +1,108 @@ +return function() + local RunService = game:GetService("RunService") + + local ToastRoot = script.Parent + local DialogRoot = ToastRoot.Parent + local AppRoot = DialogRoot.Parent + local UIBloxRoot = AppRoot.Parent + local Packages = UIBloxRoot.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local SlideFromTopToast = require(ToastRoot.SlideFromTopToast) + + local createSlideFromTopToast = function(props) + return mockStyleComponent({ + SlideFromTopToast = Roact.createElement(SlideFromTopToast, props) + }) + end + + it("should throw on empty toastTitle", function() + local element = createSlideFromTopToast({ + toastContent = { + toastTitle = nil, + }, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors", function() + local element = createSlideFromTopToast({ + toastContent = { + toastTitle = "Test Title", + }, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when render InformativeToast", function() + local element = createSlideFromTopToast({ + toastContent = { + iconImage = "rbxassetid://4126499279", + toastSubtitle = "test test test", + toastTitle = "Item on sale", + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when render InteractiveToast with onActivated callback", function() + local element = createSlideFromTopToast({ + toastContent = { + iconImage = "rbxassetid://4126499279", + onActivated = function() end, + toastSubtitle = "Tap to see more information", + toastTitle = "System Outage", + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should consistently close immediately after opening when provided a duration of 0", function() + -- Run test 10 times to confirm it is not flaky + for _ = 1, 10 do + local appeared = false + local disappeared = false + + local element = createSlideFromTopToast({ + toastContent = { + toastTitle = "Test Title", + onAppeared = function() + appeared = true + end, + onDismissed = function() + disappeared = true + end, + }, + springOptions = { + frequency = 15000000, + damping = 1, + }, + duration = 0, + }) + + local instance = Roact.mount(element) + + -- Wait a heartbeat and a step to ensure the otter motor has a chance to complete its movement + RunService.Heartbeat:Wait() + RunService.Stepped:Wait() + expect(appeared).to.equal(true) + expect(disappeared).to.equal(false) + + RunService.Heartbeat:Wait() + RunService.Stepped:Wait() + expect(appeared).to.equal(true) + expect(disappeared).to.equal(true) + + Roact.unmount(instance) + end + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastContainer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastContainer.lua new file mode 100644 index 0000000..aae8e86 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastContainer.lua @@ -0,0 +1,221 @@ +local ToastRoot = script.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local GetTextSize = require(UIBloxRoot.Core.Text.GetTextSize) +local Images = require(UIBloxRoot.App.ImageSet.Images) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) +local validateColorInfo = require(UIBloxRoot.Core.Style.Validator.validateColorInfo) + +local DEFAULT_PADDING = 12 +local DEFAULT_ICON_SIZE = Vector2.new(36, 36) + +local MAX_WIDTH = 400 +local MIN_WIDTH = 24 +local MIN_HEIGHT = 60 + +local MAX_BOUND = 10000 + +local function getTextHeight(text, font, fontSize, widthCap) + local bounds = Vector2.new(widthCap, MAX_BOUND) + local textSize = GetTextSize(text, fontSize, font, bounds) + return textSize.Y +end + +local ToastContainer = Roact.PureComponent:extend("ToastContainer") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + fitHeight = t.optional(t.boolean), + iconColorStyle = t.optional(validateColorInfo), + -- Optional image to be displayed in the toast. + iconImage = t.optional(t.union(t.table, t.string)), + iconSize = t.optional(t.Vector2), + iconChildren = t.optional(t.table), + layoutOrder = t.optional(t.integer), + onActivated = t.optional(t.callback), + onTouchSwipe = t.optional(t.callback), + padding = t.numberMin(0), + position = t.UDim2, + pressedScale = t.optional(t.number), + renderToast = t.callback, + size = t.UDim2, + sizeConstraint = t.optional(t.table), + toastSubtitle = t.optional(t.string), + toastTitle = t.string, +}) + +ToastContainer.defaultProps = { + anchorPoint = Vector2.new(0, 0), + fitHeight = true, + padding = DEFAULT_PADDING, + position = UDim2.new(0, 0, 0, 0), + size = UDim2.new(1, -DEFAULT_PADDING * 2, 0, 0), + sizeConstraint = { + MaxSize = Vector2.new(MAX_WIDTH, math.huge), + MinSize = Vector2.new(MIN_WIDTH, MIN_HEIGHT), + }, +} + +function ToastContainer:init() + self.containerRef = Roact.createRef() + self.isMounted = false + + self.state = { + containerWidth = 0, + pressed = false, + subtitleHeight = 0, + titleHeight = 0, + } + + self.getIconSize = function() + local iconSize = self.props.iconSize + local iconImage = self.props.iconImage + local imagesResolutionScale = Images.ImagesResolutionScale + if iconImage then + if iconSize then + return iconSize + elseif iconImage.ImageRectSize and imagesResolutionScale and imagesResolutionScale > 0 then + return iconImage.ImageRectSize / imagesResolutionScale + else + return DEFAULT_ICON_SIZE + end + end + return Vector2.new(0, 0) + end + + self.onButtonInputBegan = function(_, inputObject) + if inputObject.UserInputState == Enum.UserInputState.Begin and + (inputObject.UserInputType == Enum.UserInputType.Touch or + inputObject.UserInputType == Enum.UserInputType.MouseButton1) then + if not self.state.pressed then + self:setState({ + pressed = true, + }) + end + end + end + + self.onButtonInputEnded = function() + if self.state.pressed then + self:setState({ + pressed = false, + }) + end + end + + self.getTextHeights = function(stylePalette) + local iconImage = self.props.iconImage + local iconSize = self.getIconSize() + local padding = self.props.padding + local toastSubtitle = self.props.toastSubtitle + local toastTitle = self.props.toastTitle + + local font = stylePalette.Font + local titleStyle = font.Header2 + local subtitleStyle = font.CaptionBody + + local textFrameWidth = self.state.containerWidth - padding*2 + if iconImage then + textFrameWidth = textFrameWidth - iconSize.X - padding + end + + local titleFont = titleStyle.Font + local titleSize = titleStyle.RelativeSize * font.BaseSize + local titleHeight = math.max(0, getTextHeight(toastTitle, titleFont, titleSize, textFrameWidth)) + + local subtitleHeight = 0 + if toastSubtitle then + local subtitleFont = subtitleStyle.Font + local subtitleSize = subtitleStyle.RelativeSize * font.BaseSize + subtitleHeight = math.max(0, getTextHeight(toastSubtitle, subtitleFont, subtitleSize, textFrameWidth)) + end + + return subtitleHeight, titleHeight + end +end + +function ToastContainer:render() + assert(validateProps(self.props)) + local iconImage = self.props.iconImage + local iconSize = self.getIconSize() + local padding = self.props.padding + local toastSubtitle = self.props.toastSubtitle + local toastTitle = self.props.toastTitle + + return withStyle(function(stylePalette) + local subtitleHeight, titleHeight = self.getTextHeights(stylePalette) + local textFrameHeight = titleHeight + subtitleHeight + + local size = self.props.size + if self.props.fitHeight then + local containerHeight = math.max(iconSize.Y, textFrameHeight) + padding*2 + size = UDim2.new(size.X.Scale, size.X.Offset, 0, containerHeight) + end + + local theme = stylePalette.Theme + local font = stylePalette.Font + local titleStyle = font.Header2 + local subtitleStyle = font.CaptionBody + + return Roact.createElement("TextButton", { + AnchorPoint = self.props.anchorPoint, + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + Size = size, + Text = "", + [Roact.Change.AbsoluteSize] = function(rbx) + if self.state.containerWidth ~= rbx.AbsoluteSize.X then + self:setState({ + containerWidth = rbx.AbsoluteSize.X + }) + end + end, + [Roact.Event.Activated] = self.props.onActivated, + [Roact.Event.InputBegan] = self.onButtonInputBegan, + [Roact.Event.InputEnded] = self.onButtonInputEnded, + [Roact.Event.TouchSwipe] = self.props.onTouchSwipe, + [Roact.Ref] = self.containerRef, + }, { + UISizeConstraint = Roact.createElement("UISizeConstraint", self.props.sizeConstraint), + Toast = self.props.renderToast({ + iconProps = iconImage and { + colorStyle = self.props.iconColorStyle, + Image = iconImage, + Size = UDim2.new(0, iconSize.X, 0, iconSize.Y), + } or nil, + iconChildren = self.props.iconChildren, + padding = padding, + pressed = self.props.onActivated and self.state.pressed or nil, + pressedScale = self.props.pressedScale, + subtitleTextProps = toastSubtitle and { + colorStyle = theme.TextEmphasis, + fontStyle = subtitleStyle, + Size = UDim2.new(1, 0, 0, subtitleHeight), + Text = toastSubtitle, + } or nil, + textFrameSize = UDim2.new(1, iconImage and -iconSize.X - padding or 0, 0, textFrameHeight), + titleTextProps = { + colorStyle = theme.TextEmphasis, + fontStyle = titleStyle, + Size = UDim2.new(1, 0, 0, titleHeight), + Text = toastTitle, + }, + }), + }) + end) +end + +function ToastContainer:didMount() + self:setState({ + containerWidth = self.containerRef.current and self.containerRef.current.AbsoluteSize.X or 0 + }) +end + +return ToastContainer diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastContainer.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastContainer.spec.lua new file mode 100644 index 0000000..3942ce8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastContainer.spec.lua @@ -0,0 +1,69 @@ +return function() + local ToastRoot = script.Parent + local DialogRoot = ToastRoot.Parent + local AppRoot = DialogRoot.Parent + local UIBloxRoot = AppRoot.Parent + local Packages = UIBloxRoot.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local InformativeToast = require(ToastRoot.InformativeToast) + local InteractiveToast = require(ToastRoot.InteractiveToast) + local ToastContainer = require(ToastRoot.ToastContainer) + + local createToastContainer = function(props) + return mockStyleComponent({ + ToastContainer = Roact.createElement(ToastContainer, props) + }) + end + + it("should throw on empty renderToast and toastTitle", function() + local element = createToastContainer({ + renderToast = nil, + toastTitle = nil, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors", function() + local element = createToastContainer({ + renderToast = function() end, + toastTitle = "Test Title", + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when render InformativeToast", function() + local element = createToastContainer({ + iconImage = "rbxassetid://4126499279", + renderToast = function(props) + return Roact.createElement(InformativeToast, props) + end, + toastSubtitle = "test test test", + toastTitle = "Item on sale", + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when render InteractiveToast", function() + local element = createToastContainer({ + iconImage = "rbxassetid://4126499279", + onActivated = function() end, + renderToast = function(props) + return Roact.createElement(InteractiveToast, props) + end, + toastSubtitle = "Tap to see more information", + toastTitle = "System Outage", + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastFrame.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastFrame.lua new file mode 100644 index 0000000..5d9463b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastFrame.lua @@ -0,0 +1,87 @@ +local ToastRoot = script.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local ToastIcon = require(ToastRoot.ToastIcon) +local ToastText = require(ToastRoot.ToastText) +local validateToastIcon = require(ToastRoot.Validator.validateToastIcon) +local validateToastText = require(ToastRoot.Validator.validateToastText) + +local ToastFrame = Roact.PureComponent:extend("ToastFrame") + +local validateProps = t.strictInterface({ + anchorPoint = t.optional(t.Vector2), + iconProps = t.optional(validateToastIcon), + iconChildren = t.optional(t.table), + layoutOrder = t.optional(t.integer), + padding = t.numberMin(0), + position = t.optional(t.UDim2), + size = t.UDim2, + subtitleTextProps = t.optional(validateToastText), + textFrameSize = t.UDim2, + titleTextProps = validateToastText, +}) + +ToastFrame.defaultProps = { + padding = 0, + size = UDim2.new(1, 0, 1, 0), + textFrameSize = UDim2.new(1, 0, 1, 0), +} + +function ToastFrame:render() + assert(validateProps(self.props)) + + local iconProps = self.props.iconProps + local padding = self.props.padding + local subtitleTextProps = self.props.subtitleTextProps + + return Roact.createElement("Frame", { + AnchorPoint = self.props.anchorPoint, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = true, + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + Size = self.props.size, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + Padding = UDim.new(0, padding), + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + UIPadding = (padding > 0) and Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, padding), + PaddingLeft = UDim.new(0, padding), + PaddingRight = UDim.new(0, padding), + PaddingTop = UDim.new(0, padding), + }), + ToastIcon = iconProps and Roact.createElement(ToastIcon, Cryo.Dictionary.join(iconProps, { + LayoutOrder = 1, + }), self.props.iconChildren), + ToastTextFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = 2, + Size = self.props.textFrameSize, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + ToastTitle = Roact.createElement(ToastText, Cryo.Dictionary.join(self.props.titleTextProps, { + LayoutOrder = 1, + })), + ToastSubtitle = subtitleTextProps and Roact.createElement(ToastText, Cryo.Dictionary.join(subtitleTextProps, { + LayoutOrder = 2, + })), + }), + }) +end + +return ToastFrame diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastFrame.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastFrame.spec.lua new file mode 100644 index 0000000..828da78 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastFrame.spec.lua @@ -0,0 +1,108 @@ +return function() + local Toast = script.Parent + local Dialog = Toast.Parent + local App = Dialog.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local TestStyle = require(UIBlox.App.Style.Validator.TestStyle) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local ToastFrame = require(Toast.ToastFrame) + + local ICON_SIZE = 36 + + local testText = "Test Title" + local testSubText = "test test test" + + local createToastFrame = function(props) + return mockStyleComponent({ + ToastFrame = Roact.createElement(ToastFrame, props) + }) + end + + it("should throw on invalid titleTextProps", function() + local element = createToastFrame({ + titleTextProps = {}, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors with valid titleTextProps", function() + local element = createToastFrame({ + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, 0, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with valid titleTextProps and subtitleTextProps", function() + local element = createToastFrame({ + subtitleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.CaptionBody, + Size = UDim2.new(1, 0, 0.5, 0), + Text = testSubText, + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, 0, 0.5, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with valid titleTextProps and iconProps", function() + local element = createToastFrame({ + iconProps = { + colorStyle = TestStyle.Theme.IconEmphasis, + Image = "rbxassetid://4126499279", + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, -ICON_SIZE, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with composed image", function() + local element = createToastFrame({ + iconProps = { + colorStyle = TestStyle.Theme.IconEmphasis, + Image = "rbxassetid://4126499279", + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }, + iconChildren = { + Child = Roact.createElement("TextLabel"), + }, + titleTextProps = { + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, -ICON_SIZE, 1, 0), + Text = testText, + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastIcon.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastIcon.lua new file mode 100644 index 0000000..d95878a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastIcon.lua @@ -0,0 +1,51 @@ +local ToastRoot = script.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local validateColorInfo = require(UIBloxRoot.Core.Style.Validator.validateColorInfo) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ToastIcon = Roact.PureComponent:extend("ToastIcon") + +local validateProps = t.interface({ + colorStyle = t.optional(validateColorInfo), + Image = t.union(t.table, t.string), + Size = t.UDim2, +}) + +ToastIcon.defaultProps = { + BackgroundTransparency = 1, +} + +function ToastIcon:render() + assert(validateProps(self.props)) + + return withStyle(function(style) + local theme = style.Theme + + local colorStyle = self.props.colorStyle + if colorStyle == nil then + colorStyle = theme.IconEmphasis + end + + local imageColor = colorStyle.Color + local imageTransparency = colorStyle.Transparency + + local newProps = Cryo.Dictionary.join(self.props, { + colorStyle = Cryo.None, + ImageColor3 = imageColor, + ImageTransparency = imageTransparency, + }) + + return Roact.createElement(ImageSetComponent.Label, newProps) + end) +end + +return ToastIcon \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastIcon.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastIcon.spec.lua new file mode 100644 index 0000000..edf8ed2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastIcon.spec.lua @@ -0,0 +1,46 @@ +return function() + local ToastRoot = script.Parent + local DialogRoot = ToastRoot.Parent + local AppRoot = DialogRoot.Parent + local UIBloxRoot = AppRoot.Parent + local Packages = UIBloxRoot.Parent + + local Roact = require(Packages.Roact) + + local Images = require(UIBloxRoot.App.ImageSet.Images) + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local ToastIcon = require(ToastRoot.ToastIcon) + + local createToastIcon = function(props) + return mockStyleComponent({ + ToastIcon = Roact.createElement(ToastIcon, props) + }) + end + + it("should throw on invalid props", function() + local element = createToastIcon({}) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors with valid props", function() + local element = createToastIcon({ + Image = "rbxassetid://4126499279", + Size = UDim2.new(0, 36, 0, 36), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with ImageSet compatible icon", function() + local element = createToastIcon({ + Image = Images["icons/status/warning"], + Size = UDim2.new(0, 36, 0, 36), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastText.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastText.lua new file mode 100644 index 0000000..b3dd13d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastText.lua @@ -0,0 +1,34 @@ +local ToastRoot = script.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local validateFontInfo = require(UIBloxRoot.Core.Style.Validator.validateFontInfo) +local validateColorInfo = require(UIBloxRoot.Core.Style.Validator.validateColorInfo) +local GenericTextLabel = require(UIBloxRoot.Core.Text.GenericTextLabel.GenericTextLabel) + +local ToastText = Roact.PureComponent:extend("ToastText") + +local validateProps = t.interface({ + colorStyle = validateColorInfo, + fontStyle = validateFontInfo, + Size = t.UDim2, + Text = t.string, +}) + +ToastText.defaultProps = { + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, +} + +function ToastText:render() + assert(validateProps(self.props)) + + return Roact.createElement(GenericTextLabel, self.props) +end + +return ToastText diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastText.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastText.spec.lua new file mode 100644 index 0000000..89d73ef --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/ToastText.spec.lua @@ -0,0 +1,38 @@ +return function() + local Toast = script.Parent + local Dialog = Toast.Parent + local App = Dialog.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local TestStyle = require(UIBlox.App.Style.Validator.TestStyle) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local ToastText = require(Toast.ToastText) + + local createToastText = function(props) + return mockStyleComponent({ + ToastText = Roact.createElement(ToastText, props) + }) + end + + it("should throw on invalid props", function() + local element = createToastText({}) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors with valid props", function() + local element = createToastText({ + colorStyle = TestStyle.Theme.TextEmphasis, + fontStyle = TestStyle.Font.Header2, + Size = UDim2.new(1, 0, 1, 0), + Text = "System error", + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastContent.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastContent.lua new file mode 100644 index 0000000..6062512 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastContent.lua @@ -0,0 +1,22 @@ +local Validator = script.Parent +local Toast = Validator.Parent +local Dialog = Toast.Parent +local App = Dialog.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local t = require(Packages.t) +local validateColorInfo = require(UIBlox.Core.Style.Validator.validateColorInfo) + +return t.strictInterface({ + iconColorStyle = t.optional(validateColorInfo), + -- Optional image to be displayed in the toast. + iconImage = t.optional(t.union(t.table, t.string)), + iconSize = t.optional(t.Vector2), + iconChildren = t.optional(t.table), + onActivated = t.optional(t.callback), + onAppeared = t.optional(t.callback), + onDismissed = t.optional(t.callback), + swipeUpDismiss = t.optional(t.boolean), + toastSubtitle = t.optional(t.string), + toastTitle = t.string, +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastIcon.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastIcon.lua new file mode 100644 index 0000000..c031e66 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastIcon.lua @@ -0,0 +1,22 @@ +local validatorRoot = script.Parent +local ToastRoot = validatorRoot.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local t = require(Packages.t) + +local validateColorInfo = require(UIBloxRoot.Core.Style.Validator.validateColorInfo) + +return t.strictInterface({ + -- ImageSet compatible image info or image directory + Image = t.union(t.table, t.string), + Size = t.UDim2, + + AnchorPoint = t.optional(t.Vector2), + -- The color table from the style palette + colorStyle = t.optional(validateColorInfo), + LayoutOrder = t.optional(t.integer), + Position = t.optional(t.UDim2), +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastText.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastText.lua new file mode 100644 index 0000000..8dde972 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/Validator/validateToastText.lua @@ -0,0 +1,31 @@ +local validatorRoot = script.Parent +local ToastRoot = validatorRoot.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local t = require(Packages.t) + +local validateFontInfo = require(UIBloxRoot.Core.Style.Validator.validateFontInfo) +local validateColorInfo = require(UIBloxRoot.Core.Style.Validator.validateColorInfo) + +return t.strictInterface({ + -- The color table from the style palette + colorStyle = validateColorInfo, + -- The Font table from the style palette + fontStyle = validateFontInfo, + Size = t.UDim2, + Text = t.string, + + AnchorPoint = t.optional(t.Vector2), + -- Whether the TextLabel is Fluid Sizing between the font's min and default sizes (optional) + fluidSizing = t.optional(t.boolean), + LayoutOrder = t.optional(t.integer), + -- The max size avaliable for the textbox + maxSize = t.optional(t.Vector2), + Position = t.optional(t.UDim2), + TextTruncate = t.optional(t.EnumItem), + TextXAlignment = t.optional(t.EnumItem), + TextYAlignment = t.optional(t.EnumItem), +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/__stories__/InformativeToastContainer.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/__stories__/InformativeToastContainer.story.lua new file mode 100644 index 0000000..f8a71cf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/__stories__/InformativeToastContainer.story.lua @@ -0,0 +1,46 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local storyRoot = script.Parent +local ToastRoot = storyRoot.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) + +local Images = require(UIBloxRoot.App.ImageSet.Images) +local InformativeToast = require(ToastRoot.InformativeToast) +local ToastContainer = require(ToastRoot.ToastContainer) + +local function InformativeToastContainer() + return Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 1, 0), + title = "InteractiveToastContainer", + subTitle = "<>", + }, { + ToastContainer = Roact.createElement(ToastContainer, { + anchorPoint = Vector2.new(0.5, 0), + iconImage = Images["icons/status/warning"], + position = UDim2.new(0.5, 0, 0, 20), + renderToast = function(props) + return Roact.createElement(InformativeToast, props) + end, + toastSubtitle = "Some details here", + toastTitle = "Title Case", + }), + }) +end + +return function(target) + local story = Roact.createElement(StoryView, {}, { + Roact.createElement(InformativeToastContainer) + }) + local handle = Roact.mount(story, target, "InformativeToast") + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/__stories__/InteractiveToastContainer.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/__stories__/InteractiveToastContainer.story.lua new file mode 100644 index 0000000..b9a7c85 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Toast/__stories__/InteractiveToastContainer.story.lua @@ -0,0 +1,47 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local storyRoot = script.Parent +local ToastRoot = storyRoot.Parent +local DialogRoot = ToastRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) + +local Images = require(UIBloxRoot.App.ImageSet.Images) +local InteractiveToast = require(ToastRoot.InteractiveToast) +local ToastContainer = require(ToastRoot.ToastContainer) + +local function InteractiveToastContainer() + return Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 1, 0), + title = "InteractiveToastContainer", + subTitle = "<>", + }, { + ToastContainer = Roact.createElement(ToastContainer, { + anchorPoint = Vector2.new(0.5, 0), + iconImage = Images["icons/status/warning"], + onActivated = function() end, + position = UDim2.new(0.5, 0, 0, 20), + renderToast = function(props) + return Roact.createElement(InteractiveToast, props) + end, + toastSubtitle = "Some details here", + toastTitle = "Title Case", + }), + }) +end + +return function(target) + local story = Roact.createElement(StoryView, {}, { + Roact.createElement(InteractiveToastContainer) + }) + local handle = Roact.mount(story, target, "InteractiveToast") + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Enum/TooltipOrientation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Enum/TooltipOrientation.lua new file mode 100644 index 0000000..8105751 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Enum/TooltipOrientation.lua @@ -0,0 +1,14 @@ +local AlertRoot = script.Parent.Parent +local DialogRoot = AlertRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local enumerate = require(Packages.enumerate) + +return enumerate(script.Name, { + "Bottom", + "Top", + "Right", + "Left", +}) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Tooltip.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Tooltip.lua new file mode 100644 index 0000000..3a79eed --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Tooltip.lua @@ -0,0 +1,104 @@ +local TooltipRoot = script.Parent +local DialogRoot = TooltipRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Style.withStyle) +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) + +local TooltipContainer = require(TooltipRoot.TooltipContainer) + +local TooltipOrientation = require(TooltipRoot.Enum.TooltipOrientation) + +local Tooltip = Roact.PureComponent:extend("Tooltip") + +Tooltip.validateProps = t.strictInterface({ + triggerPosition = t.Vector2, + triggerSize = t.Vector2, + bodyText = t.string, + headerText = t.optional(t.string), + onDismiss = t.optional(t.callback), + screenSize = t.optional(t.Vector2), --the app screen size + position = t.optional(t.UDim2), + orientation = t.optional(enumerateValidator(TooltipOrientation)), + triggerOnHover = t.optional(t.boolean), + forceClickTriggerPoint = t.optional(t.boolean), + isDirectChild = t.optional(t.boolean), +}) + +Tooltip.defaultProps = { + screenSize = Vector2.new(10000, 10000), + orientation = TooltipOrientation.Bottom, + triggerOnHover = false, + forceClickTriggerPoint = false, + isDirectChild = false, +} + +function Tooltip:init() + self.onDismissDefault = function() + if self.props.forceClickTriggerPoint or self.props.triggerOnHover then + return + end + if self.props.onDismiss then + self.props.onDismiss() + end + end +end + +function Tooltip:render() + local enableTriggerMask = self.props.forceClickTriggerPoint or self.props.triggerOnHover + local isDirectChild = self.props.isDirectChild + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + local tooltipComponents = { + -- Force Click Trigger Point should not prevent the rest of the UI from being interactable + Background = not self.props.forceClickTriggerPoint and Roact.createElement("TextButton", { + ZIndex = 0, + AutoButtonColor = false, + BackgroundColor3 = theme.Overlay.Color, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Position = isDirectChild and UDim2.fromOffset(-self.props.triggerPosition.X, -self.props.triggerPosition.Y) + or UDim2.fromOffset(0, 0), + Size = UDim2.fromOffset(self.props.screenSize.X, self.props.screenSize.Y), + Text = "", + [Roact.Event.Activated] = self.onDismissDefault, + [Roact.Event.TouchSwipe] = self.onDismissDefault, + [Roact.Event.MouseWheelForward] = self.onDismissDefault, + [Roact.Event.MouseWheelBackward] = self.onDismissDefault, + }), + TriggerPointMask = enableTriggerMask and Roact.createElement("TextButton", { + Text = "", + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.fromOffset(self.props.triggerSize.X, self.props.triggerSize.Y), + Position = isDirectChild and UDim2.fromOffset(0, 0) + or UDim2.fromOffset(self.props.triggerPosition.X, self.props.triggerPosition.Y), + [Roact.Event.Activated] = self.props.onDismiss, + }), + TooltipContainer = Roact.createElement(TooltipContainer, { + triggerPosition = self.props.triggerPosition, + triggerSize = self.props.triggerSize, + bodyText = self.props.bodyText, + headerText = self.props.headerText, + screenSize = self.props.screenSize, + position = self.props.position, + orientation = self.props.orientation, + isDirectChild = isDirectChild, + }), + } + + return isDirectChild and Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, tooltipComponents) + or Roact.createFragment(tooltipComponents) + end) +end + +return Tooltip diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Tooltip.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Tooltip.spec.lua new file mode 100644 index 0000000..ddc7753 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/Tooltip.spec.lua @@ -0,0 +1,66 @@ +return function() + local TooltipRoot = script.Parent + local DialogRoot = TooltipRoot.Parent + local App = DialogRoot.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local Tooltip = require(TooltipRoot.Tooltip) + local TooltipOrientation = require(TooltipRoot.Enum.TooltipOrientation) + + describe("mount/unmount", function() + it("should mount and unmount with required properties", function() + local element = mockStyleComponent({ + TooltipTest = Roact.createElement(Tooltip, { + -- required + triggerPosition = Vector2.new(0 ,0), + triggerSize = Vector2.new(0 ,0), + bodyText = "Tooltip body text", + }) + }) + local handle = Roact.mount(element) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + it("should mount and unmount with valid properties", function() + local element = mockStyleComponent({ + TooltipTest = Roact.createElement(Tooltip, { + -- required + triggerPosition = Vector2.new(0 ,0), + triggerSize = Vector2.new(0 ,0), + bodyText = "Tooltip body text", + -- optional + headerText = "Header", + onDismiss = function() end, + screenSize = Vector2.new(300 ,600), + position = UDim2.new(0, 0, 0, 0), + orientation = TooltipOrientation.Top, + triggerOnHover = true, + forceClickTriggerPoint = false, + isDirectChild = true, + }) + }) + local handle = Roact.mount(element) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + it("should mount and unmount with empty bodyText", function() + local element = mockStyleComponent({ + TooltipTest = Roact.createElement(Tooltip, { + -- required + triggerPosition = Vector2.new(0 ,0), + triggerSize = Vector2.new(0 ,0), + bodyText = "", + }) + }) + local handle = Roact.mount(element) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/TooltipContainer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/TooltipContainer.lua new file mode 100644 index 0000000..f1b22c3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/TooltipContainer.lua @@ -0,0 +1,223 @@ +local TooltipRoot = script.Parent +local DialogRoot = TooltipRoot.Parent +local AppRoot = DialogRoot.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Otter = require(Packages.Otter) + +local withStyle = require(UIBlox.Style.withStyle) +local Images = require(AppRoot.ImageSet.Images) +local ImageSetLabel = require(UIBlox.Core.ImageSet.ImageSetComponent).Label +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) +local GetTextHeight = require(UIBlox.Core.Text.GetTextHeight) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local divideTransparency = require(UIBlox.Utility.divideTransparency) +local lerp = require(UIBlox.Utility.lerp) + +local TooltipOrientation = require(TooltipRoot.Enum.TooltipOrientation) +local getPositionInfo = require(TooltipRoot.getPositionInfo) + +local FRAME_MAX_WIDTH = 240 +local MARGIN = 12 +local PADDING_BETWEEN = 4 +local TRIANGLE_HEIGHT = 8 + +local TriangleImages = { + [TooltipOrientation.Bottom] = Images["component_assets/triangleUp_16"], + [TooltipOrientation.Top] = Images["component_assets/triangleDown_16"], + [TooltipOrientation.Right] = Images["component_assets/triangleLeft_16"], + [TooltipOrientation.Left] = Images["component_assets/triangleRight_16"], +} + +local MOTOR_OPTIONS = { + frequency = 50, + dampingRatio = 1, +} + +local TooltipContainer = Roact.PureComponent:extend("TooltipContainer") + +TooltipContainer.validateProps = t.strictInterface({ + triggerPosition = t.Vector2, + triggerSize = t.Vector2, + bodyText = t.string, + headerText = t.optional(t.string), + screenSize = t.optional(t.Vector2), --the app screen size + position = t.optional(t.UDim2), + orientation = t.optional(enumerateValidator(TooltipOrientation)), + isDirectChild = t.optional(t.boolean), +}) + +TooltipContainer.defaultProps = { + screenSize = Vector2.new(10000, 10000), + orientation = TooltipOrientation.Bottom, + isDirectChild = false, +} + +function TooltipContainer:init() + self.visible = true + + local setProgress + self.progress, setProgress = Roact.createBinding(0) + + self.progressMotor = Otter.createSingleMotor(0) + self.progressMotor:onStep(setProgress) + self.progressMotor:setGoal(Otter.spring(1, MOTOR_OPTIONS)) +end + +function TooltipContainer:render() + return withStyle(function(stylePalette) + local font = stylePalette.Font + local theme = stylePalette.Theme + + local headerFont = font.CaptionHeader + local bodyFont = font.CaptionBody + + local fontSize = font.BaseSize * font.CaptionBody.RelativeSize + + local bodyTextWidth = GetTextSize(self.props.bodyText, fontSize, bodyFont.Font, Vector2.new()).X + local innerWidth = math.min(bodyTextWidth, FRAME_MAX_WIDTH - 2 * MARGIN) + local frameWidth = innerWidth + 2 * MARGIN + + local bodyTextHeight = GetTextHeight(self.props.bodyText, bodyFont.Font, fontSize, innerWidth) + local headerTextHeight = self.props.headerText + and GetTextHeight(self.props.headerText, headerFont.Font, fontSize, innerWidth) + or 0 + + local frameHeight = self.props.headerText + and headerTextHeight + bodyTextHeight + MARGIN * 2 + PADDING_BETWEEN + or bodyTextHeight + MARGIN * 2 + + local positionInfo = getPositionInfo( + frameWidth, + frameHeight, + self.props.orientation, + self.props.triggerPosition, + self.props.triggerSize, + self.props.screenSize, + self.props.position) + + local containerPosition = self.progress:map(function(value) + local startPosition + if self.props.position then + startPosition = self.props.position + else + startPosition = self.props.isDirectChild and positionInfo.position or positionInfo.absolutePosition + end + local endPosition = startPosition + positionInfo.animatedDistance + return startPosition:lerp(endPosition, value) + end) + + local backgroundTransparency = self.progress:map(function(value) + local baseTransparency = theme.UIMuted.Transparency + local transparencyDivisor = 1 + return lerp(1, divideTransparency(baseTransparency, transparencyDivisor), value) + end) + + local textTransparency = self.progress:map(function(value) + local baseTransparency = theme.TextEmphasis.Transparency + local transparencyDivisor = 2 + return lerp(1, divideTransparency(baseTransparency, transparencyDivisor), value) + end) + + return Roact.createElement("Frame", { + Visible = self.visible, + Position = containerPosition, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = positionInfo.fillDirection == Enum.FillDirection.Vertical + and UDim2.fromOffset(frameWidth, frameHeight + TRIANGLE_HEIGHT) + or UDim2.fromOffset(frameWidth + TRIANGLE_HEIGHT, frameHeight), + }, { + UIListLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = positionInfo.fillDirection, + }), + CaretFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = positionInfo.caretFrameSize, + LayoutOrder = positionInfo.caretLayoutOrder, + }, { + Caret = Roact.createElement(ImageSetLabel, { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Position = positionInfo.caretPosition, + AnchorPoint = positionInfo.caretAnchorPoint, + Size = positionInfo.caretImageSize, + Image = TriangleImages[positionInfo.updatedOrientation], + ImageColor3 = theme.UIMuted.Color, + ImageTransparency = backgroundTransparency, + [Roact.Ref] = self.caretRef, + }), + }), + Content = Roact.createElement("TextButton", { + AutoButtonColor = false, + Text = "", + Size = UDim2.fromOffset(frameWidth, frameHeight), + BackgroundColor3 = theme.UIMuted.Color, + BackgroundTransparency = backgroundTransparency, + BorderSizePixel = 0, + LayoutOrder = positionInfo.contentLayoutOrder, + }, { + VerticalLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = UDim.new(0, PADDING_BETWEEN), + VerticalAlignment = Enum.VerticalAlignment.Center + }), + Padding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, MARGIN), + PaddingBottom = UDim.new(0, MARGIN), + PaddingLeft = UDim.new(0, MARGIN), + PaddingRight = UDim.new(0, MARGIN), + }), + Header = self.props.headerText and Roact.createElement(GenericTextLabel, { + colorStyle = theme.TextEmphasis, + fontStyle = headerFont, + LayoutOrder = 1, + Text = self.props.headerText, + TextTransparency = textTransparency, + TextXAlignment = Enum.TextXAlignment.Left, + Size = UDim2.new(1, 0, 0, headerTextHeight), + }), + Body = Roact.createElement(GenericTextLabel, { + colorStyle = theme.TextDefault, + fontStyle = bodyFont, + LayoutOrder = 2, + Text = self.props.bodyText, + TextTransparency = textTransparency, + TextXAlignment = Enum.TextXAlignment.Left, + Size = UDim2.new(1, 0, 0, bodyTextHeight), + }), + }), + }) + end) +end + +function TooltipContainer:didMount() + self.progressMotor:start() +end + +function TooltipContainer:didUpdate(lastProps, lastState) + if lastProps.triggerPosition ~= self.props.triggerPosition then + if self.props.triggerPosition.Y < 0 + or self.props.triggerPosition.Y + self.props.triggerSize.Y > self.props.screenSize.Y + then + self.visible = false + else + self.visible = true + end + end +end + +function TooltipContainer:willUnmount() + self.progressMotor:destroy() + self.progressMotor = nil +end + +return TooltipContainer diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/TooltipContainer.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/TooltipContainer.spec.lua new file mode 100644 index 0000000..0f49335 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/TooltipContainer.spec.lua @@ -0,0 +1,63 @@ +return function() + local TooltipRoot = script.Parent + local DialogRoot = TooltipRoot.Parent + local App = DialogRoot.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local TooltipContainer = require(TooltipRoot.TooltipContainer) + local TooltipOrientation = require(TooltipRoot.Enum.TooltipOrientation) + + describe("mount/unmount", function() + it("should mount and unmount with required properties", function() + local element = mockStyleComponent({ + TooltipTest = Roact.createElement(TooltipContainer, { + -- required + triggerPosition = Vector2.new(0 ,0), + triggerSize = Vector2.new(0 ,0), + bodyText = "Tooltip body text", + }) + }) + local handle = Roact.mount(element) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + it("should mount and unmount with valid properties", function() + local element = mockStyleComponent({ + TooltipTest = Roact.createElement(TooltipContainer, { + -- required + triggerPosition = Vector2.new(0 ,0), + triggerSize = Vector2.new(0 ,0), + bodyText = "Tooltip body text", + -- optional + headerText = "Header", + screenSize = Vector2.new(300 ,600), + position = UDim2.new(0, 0, 0, 0), + orientation = TooltipOrientation.Top, + isDirectChild = true, + }) + }) + local handle = Roact.mount(element) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + it("should mount and unmount with empty bodyText", function() + local element = mockStyleComponent({ + TooltipTest = Roact.createElement(TooltipContainer, { + -- required + triggerPosition = Vector2.new(0 ,0), + triggerSize = Vector2.new(0 ,0), + bodyText = "", + }) + }) + local handle = Roact.mount(element) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/getPositionInfo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/getPositionInfo.lua new file mode 100644 index 0000000..3d10af5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/getPositionInfo.lua @@ -0,0 +1,140 @@ +--[[ + Calculate absolute position of tooltip, as well as caret position / point direction, etc + return: + - absolutePosition: UDim2, Tooltip start absolute position + - position: UDim2, Tooltip start position offset from trigger point + - animatedDistance: UDim2, diff of animated position and start position + - updatedOrientation: Enum, tooltip may flip from bottom to top + - fillDirection: Enum, fill direction of caret and content frame + - caretLayoutOrder: number, 1: on top or left + - contentLayoutOrder: number + - caretPosition: UDim2, caret position relates to tooltip frame and trigger point + - caretImageSize: UDim2 + - caretAnchorPoint: Vector2 +]] + +local TooltipOrientation = require(script.Parent.Enum.TooltipOrientation) +local MARGIN = 12 +local TRIANGLE_HEIGHT = 8 -- height / width will be swapped if pointing left / right +local TRIANGLE_WIDTH = 16 + +return function(frameWidth, frameHeight, orientation, triggerPosition, triggerSize, screenSize, userInputPosition) + local positionInfo = {} + + local triggerCenter = triggerPosition + triggerSize * 0.5 + local triggerEnd = triggerPosition + triggerSize + + local absolutePosX + local absolutePosY + local offsetX + local offsetY + local animatedOffsetX = 0 + local animatedOffsetY = 0 + + local minOffset = MARGIN + + if orientation == TooltipOrientation.Bottom or orientation == TooltipOrientation.Top then + -- Vertical + + -- if space not enough under target, flip to top + -- disabled if user set position manually + if not userInputPosition and frameHeight + TRIANGLE_HEIGHT + MARGIN > screenSize.Y - triggerEnd.Y then + orientation = TooltipOrientation.Top + end + + local maxOffsetX = screenSize.X - MARGIN - frameWidth + absolutePosX = math.clamp(triggerCenter.X - frameWidth * 0.5, minOffset, maxOffsetX) + -- Tooltip offset from trigger point + offsetX = absolutePosX - triggerPosition.X + + local caretOffsetX + local maxCaretOffsetX = frameWidth - MARGIN - TRIANGLE_WIDTH + + if frameWidth < TRIANGLE_WIDTH + 2 * MARGIN then + positionInfo.caretPosition = UDim2.fromScale(0.5, 0) + elseif userInputPosition then + local dist = triggerSize.X * 0.5 + math.abs(triggerSize.X * userInputPosition.X.Scale + userInputPosition.X.Offset) + caretOffsetX = math.clamp(dist, MARGIN, maxCaretOffsetX) + positionInfo.caretPosition = UDim2.fromOffset(caretOffsetX, 0) + -- enough space for both left and right side + -- or frameWidth is less than TRIANGLE_WIDTH + 2 * MARGIN + elseif triggerCenter.X - frameWidth * 0.5 >= MARGIN and triggerCenter.X + frameWidth * 0.5 <= screenSize.X - MARGIN + then + positionInfo.caretPosition = UDim2.fromScale(0.5, 0) + else + caretOffsetX = math.clamp(triggerCenter.X - absolutePosX, MARGIN, maxCaretOffsetX) + positionInfo.caretPosition = UDim2.fromOffset(caretOffsetX, 0) + end + + positionInfo.caretFrameSize = UDim2.fromOffset(frameWidth, TRIANGLE_HEIGHT) + positionInfo.caretImageSize = UDim2.fromOffset(TRIANGLE_WIDTH, TRIANGLE_HEIGHT) + positionInfo.caretAnchorPoint = Vector2.new(0.5, 0) + positionInfo.fillDirection = Enum.FillDirection.Vertical + + if orientation == TooltipOrientation.Bottom then + -- caret pointing top + absolutePosY = triggerPosition.Y + triggerSize.Y + offsetY = triggerSize.Y + positionInfo.caretLayoutOrder = 1 + positionInfo.contentLayoutOrder = 2 + animatedOffsetY = 4 + else + -- caret pointing bottom + absolutePosY = triggerPosition.Y - frameHeight - TRIANGLE_HEIGHT + offsetY = -(frameHeight + TRIANGLE_HEIGHT) + positionInfo.caretLayoutOrder = 2 + positionInfo.contentLayoutOrder = 1 + animatedOffsetY = -4 + end + else + -- Horizontal + local maxOffsetY = screenSize.Y - MARGIN - frameHeight + absolutePosY = math.clamp(triggerCenter.Y - frameHeight * 0.5, minOffset, maxOffsetY) + offsetY = absolutePosY - triggerPosition.Y + + local caretOffsetY + local maxCaretOffsetY = frameHeight - MARGIN - TRIANGLE_WIDTH + if userInputPosition then + local dist = triggerSize.Y * 0.5 + math.abs(triggerSize.Y * userInputPosition.Y.Scale + userInputPosition.Y.Offset) + caretOffsetY = math.clamp(dist, MARGIN, maxCaretOffsetY) + positionInfo.caretPosition = UDim2.fromOffset(0, caretOffsetY) + -- frameHeight always greater than or equal to TRIANGLE_WIDTH + 2 * MARGIN + -- caret should in center for one-line tooltip + elseif triggerCenter.Y - frameHeight * 0.5 >= MARGIN and triggerCenter.Y + frameHeight * 0.5 <= screenSize.Y - MARGIN + or frameHeight <= 40 + then + positionInfo.caretPosition = UDim2.fromScale(0, 0.5) + else + local maxCaretOffset = frameHeight - MARGIN - TRIANGLE_WIDTH + caretOffsetY = math.clamp(triggerCenter.Y - absolutePosY, MARGIN, maxCaretOffset) + positionInfo.caretPosition = UDim2.fromOffset(0, caretOffsetY) + end + + positionInfo.caretFrameSize = UDim2.fromOffset(TRIANGLE_HEIGHT, frameHeight) + positionInfo.caretImageSize = UDim2.fromOffset(TRIANGLE_HEIGHT, TRIANGLE_WIDTH) + positionInfo.caretAnchorPoint = Vector2.new(0, 0.5) + positionInfo.fillDirection = Enum.FillDirection.Horizontal + + if orientation == TooltipOrientation.Right then + -- caret pointing left + absolutePosX = triggerEnd.X + offsetX = triggerSize.X + positionInfo.caretLayoutOrder = 1 + positionInfo.contentLayoutOrder = 2 + animatedOffsetX = 4 + else + -- caret pointing right + absolutePosX = triggerPosition.X - frameWidth - TRIANGLE_HEIGHT + offsetX = -(frameWidth - TRIANGLE_HEIGHT) + positionInfo.caretLayoutOrder = 2 + positionInfo.contentLayoutOrder = 1 + animatedOffsetX = -4 + end + end + + positionInfo.absolutePosition = UDim2.fromOffset(absolutePosX, absolutePosY) + positionInfo.position = UDim2.fromOffset(offsetX, offsetY) + positionInfo.animatedDistance = UDim2.fromOffset(animatedOffsetX, animatedOffsetY) + positionInfo.updatedOrientation = orientation + return positionInfo +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/getPositionInfo.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/getPositionInfo.spec.lua new file mode 100644 index 0000000..b39b204 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Dialog/Tooltip/getPositionInfo.spec.lua @@ -0,0 +1,35 @@ +return function() + local MARGIN = 12 + local TRIANGLE_HEIGHT = 8 -- height / width will be swapped if pointing left / right + local TRIANGLE_WIDTH = 16 + local getPositionInfo = require(script.Parent.getPositionInfo) + local TooltipOrientation = require(script.Parent.Enum.TooltipOrientation) + + describe("getPositionInfo()", function() + local triggerSize = Vector2.new(10, 10) + local screenSize = Vector2.new(800, 600) + it("should return proper position info", function() + local positionInfo = getPositionInfo(50, 30, TooltipOrientation.Bottom, Vector2.new(0, 0), triggerSize, screenSize) + expect(positionInfo).to.be.ok() + expect(positionInfo.absolutePosition).to.equal(UDim2.fromOffset(MARGIN, 10)) + expect(positionInfo.position).to.equal(UDim2.fromOffset(MARGIN, 10)) + expect(positionInfo.animatedDistance).to.equal(UDim2.fromOffset(0, 4)) + expect(positionInfo.updatedOrientation).to.equal(TooltipOrientation.Bottom) + expect(positionInfo.fillDirection).to.equal(Enum.FillDirection.Vertical) + expect(positionInfo.caretLayoutOrder).to.equal(1) + expect(positionInfo.contentLayoutOrder).to.equal(2) + expect(positionInfo.caretPosition).to.equal(UDim2.fromOffset(MARGIN, 0)) + expect(positionInfo.caretImageSize).to.equal(UDim2.fromOffset(TRIANGLE_WIDTH, TRIANGLE_HEIGHT)) + expect(positionInfo.caretAnchorPoint).to.equal(Vector2.new(0.5, 0)) + end) + it("should flip if space not enough under trigger point", function() + local positionInfo = getPositionInfo(50, 30, TooltipOrientation.Bottom, Vector2.new(0, 580), triggerSize, screenSize) + expect(positionInfo.absolutePosition).to.equal(UDim2.fromOffset(MARGIN, 542)) + expect(positionInfo.position).to.equal(UDim2.fromOffset(MARGIN, -38)) + expect(positionInfo.animatedDistance).to.equal(UDim2.fromOffset(0, -4)) + expect(positionInfo.updatedOrientation).to.equal(TooltipOrientation.Top) + expect(positionInfo.caretLayoutOrder).to.equal(2) + expect(positionInfo.contentLayoutOrder).to.equal(1) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/DefaultMetricsGridView.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/DefaultMetricsGridView.lua new file mode 100644 index 0000000..fe07c20 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/DefaultMetricsGridView.lua @@ -0,0 +1,144 @@ +local GridRoot = script.Parent +local AppRoot = GridRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local GridView = require(GridRoot.GridView) + +-- It is an error to have a window height > the maximum height the grid view can +-- grow to, so we check it here. +local function validateWindowHeight(props) + if props.windowHeight ~= nil and props.windowHeight > props.maxHeight then + return false, ("windowHeight must be less than or equal to maxHeight\nmaxHeight: %f\nwindowHeight: %f"):format( + props.maxHeight, + props.windowHeight + ) + end + + return true +end + +local isGridViewProps = t.intersection( + t.strictInterface({ + -- A function that, given the width of grid cells, returns the height of + -- grid cells. + getItemHeight = t.callback, + -- A grid metrics getter function (see GridMetrics). + getItemMetrics = t.callback, + -- How much of the grid view is visible. This determines the size of cells + -- in the grid. + windowHeight = t.optional(t.numberMin(0)), + -- A function that, given an item, returns a Roact element representing that + -- item. The item should expect to fill its parent. Setting LayoutOrder is + -- not necessary. + renderItem = t.callback, + -- The spacing between grid cells, on each axis. + itemPadding = t.Vector2, + -- All the items that can be displayed in the grid. renderItem should be + -- able to use all values in this table. + items = t.table, + -- The maximum height the grid view is allowed to grow to. + maxHeight = t.numberMin(0), + -- The layout order of the grid. + LayoutOrder = t.optional(t.integer), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), + restorePreviousChildFocus = t.optional(t.boolean), + onFocusGained = t.optional(t.callback), + + -- which selection will initally be selected (if using roact-gamepad) + defaultChildIndex = t.optional(t.numberMin(1)), + }), + validateWindowHeight +) + +local DefaultMetricsGridView = Roact.PureComponent:extend("DefaultMetricsGridView") + +DefaultMetricsGridView.defaultProps = { + maxHeight = math.huge, +} + +function DefaultMetricsGridView:init() + self.isMounted = false + + self.state = { + containerWidth = 0, + } + + self.checkSetInitialContainerWidth = function(rbx) + if self.isMounted and rbx:IsDescendantOf(game) then + self:setState({ + containerWidth = rbx.AbsoluteSize.X, + }) + end + end +end + +function DefaultMetricsGridView:render() + assert(isGridViewProps(self.props)) + + local itemMetrics = self.props.getItemMetrics(self.state.containerWidth, self.props.itemPadding.X) + local itemHeight = self.props.getItemHeight(itemMetrics.itemWidth) + + local size = Vector2.new( + math.max(0, itemMetrics.itemWidth), + math.max(0, itemHeight) + ) + + if self.state.containerWidth == 0 then + return Roact.createElement("Frame", { + Transparency = 1, + Size = UDim2.new(1, 0, 0, 0), + + [Roact.Change.AbsoluteSize] = self.checkSetInitialContainerWidth, + [Roact.Event.AncestryChanged] = self.checkSetInitialContainerWidth, + }) + end + + return Roact.createElement(GridView, { + renderItem = self.props.renderItem, + windowHeight = self.props.windowHeight, + maxHeight = self.props.maxHeight, + itemSize = size, + itemPadding = self.props.itemPadding, + items = self.props.items, + LayoutOrder = self.props.LayoutOrder, + + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + [Roact.Ref] = self.props[Roact.Ref], + + -- Optional gamepad props + defaultChildIndex = self.props.defaultChildIndex, + restorePreviousChildFocus = self.props.restorePreviousChildFocus, + onFocusGained = self.props.onFocusGained, + + onWidthChanged = function(newWidth) + if self.isMounted then + self:setState({ + containerWidth = newWidth, + }) + end + end, + }) +end + +function DefaultMetricsGridView:didMount() + self.isMounted = true +end + +function DefaultMetricsGridView:willUnmount() + self.isMounted = false +end + +return DefaultMetricsGridView \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/DefaultMetricsGridView.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/DefaultMetricsGridView.spec.lua new file mode 100644 index 0000000..dcdca12 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/DefaultMetricsGridView.spec.lua @@ -0,0 +1,182 @@ +local GridRoot = script.Parent +local AppRoot = GridRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) + +local DefaultMetricsGridView = require(script.Parent.DefaultMetricsGridView) +local GridMetrics = require(script.Parent.GridMetrics) + +-- Default properties used in testMount. Can be overridden. +local defaultTestProperties = { + renderItem = function() end, + getItemHeight = function() return 0 end, + getItemMetrics = GridMetrics.getSmallMetrics, + windowHeight = 0, + itemPadding = Vector2.new(), + items = {}, +} + +local function testMount(props) + local mergedProps = Cryo.Dictionary.join(defaultTestProperties, props) + local element = Roact.createElement(DefaultMetricsGridView, mergedProps) + local handle = Roact.mount(element, nil) + Roact.unmount(handle) +end + +return function() + describe("renderItem", function() + it("must be a function", function() + testMount({ + renderItem = function() end, + }) + + expect(function() + testMount({ + renderItem = "Frame", + }) + end).to.throw() + end) + end) + + describe("getItemHeight", function() + it("must be a function", function() + testMount({ + getItemHeight = function() return 25 end, + }) + + expect(function() + testMount({ + getItemHeight = 50, + }) + end).to.throw() + end) + end) + + describe("getItemMetrics", function() + it("must be a function", function() + testMount({ + getItemMetrics = GridMetrics.getMediumMetrics, + }) + + expect(function() + testMount({ + getItemMetrics = "large", + }) + end).to.throw() + end) + end) + + describe("windowHeight", function() + it("must be a number or nil", function() + testMount({ + windowHeight = 25, + }) + + testMount({ + windowHeight = Cryo.None, + }) + + expect(function() + testMount({ + windowHeight = Vector2.new(1, 0), + }) + end).to.throw() + end) + + it("must be non-negative", function() + expect(function() + testMount({ + windowHeight = -10, + }) + end).to.throw() + end) + + it("must be less than or equal to maxHeight", function() + testMount({ + maxHeight = 200, + windowHeight = 100, + }) + + testMount({ + maxHeight = 200, + windowHeight = 200, + }) + + expect(function() + testMount({ + maxHeight = 200, + windowHeight = 300, + }) + end).to.throw() + end) + end) + + describe("itemPadding", function() + it("must be a Vector2", function() + testMount({ + itemPadding = Vector2.new(10, 40), + }) + + expect(function() + testMount({ + itemPadding = UDim2.new(0, 10, 0.05, 0), + }) + end).to.throw() + end) + end) + + describe("items", function() + it("must be an array", function() + testMount({ + items = { 1, 2, 3 }, + }) + + expect(function() + testMount({ + items = "a,b,c", + }) + end).to.throw() + end) + end) + + describe("maxHeight", function() + it("must be a number", function() + testMount({ + maxHeight = 25, + }) + + expect(function() + testMount({ + maxHeight = Vector2.new(1, 0), + }) + end).to.throw() + end) + + it("must be non-negative", function() + expect(function() + testMount({ + maxHeight = -10, + }) + end).to.throw() + end) + end) + + describe("LayoutOrder", function() + it("must be an integer", function() + expect(function() + testMount({ + LayoutOrder = "1", + }) + end).to.throw() + + expect(function() + testMount({ + LayoutOrder = 0.5, + }) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridMetrics.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridMetrics.lua new file mode 100644 index 0000000..fd38151 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridMetrics.lua @@ -0,0 +1,72 @@ +--[[ + Documentation: + https://confluence.roblox.com/display/DESIGN/Grid+Systems + https://docs.google.com/spreadsheets/d/1zLNqGop2ha2Y4Twcvh3w_LwuAr2CwkM-AvTlhPrz7WU/edit?usp=sharing +]] + +local GridRoot = script.Parent +local AppRoot = GridRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent +local t = require(Packages.t) + +local mediumSettings = { + minimumItemsPerRow = 2, + minimumItemWidth = 160, +} + +local largeSettings = { + minimumItemsPerRow = 1, + minimumItemWidth = 332, +} + +local function getItemMetrics(containerWidth, horizontalPadding, settingsTable) + local itemsPerRow = math.floor( + (containerWidth + horizontalPadding) / (settingsTable.minimumItemWidth + horizontalPadding)) + itemsPerRow = math.max(settingsTable.minimumItemsPerRow, itemsPerRow) + local itemWidth = math.floor((containerWidth - (itemsPerRow - 1) * horizontalPadding) / itemsPerRow) + + return { + itemsPerRow = itemsPerRow, + itemWidth = itemWidth, + } +end + +local GridMetrics = {} + +local isMetricsSettings = t.strictInterface({ + minimumItemsPerRow = t.intersection(t.integer, t.numberMin(1)), + minimumItemWidth = t.numberMin(0), +}) + +local isGetterFunctionArgs = t.tuple( + t.numberMin(0), -- containerWidth + t.number -- horizontalPadding +) + +function GridMetrics.makeCustomMetricsGetter(settings) + assert(isMetricsSettings(settings)) + + return function(containerWidth, horizontalPadding) + assert(isGetterFunctionArgs(containerWidth, horizontalPadding)) + return getItemMetrics(containerWidth, horizontalPadding, settings) + end +end + +GridMetrics.getLargeMetrics = GridMetrics.makeCustomMetricsGetter(largeSettings) +GridMetrics.getMediumMetrics = GridMetrics.makeCustomMetricsGetter(mediumSettings) + +function GridMetrics.getSmallMetrics(containerWidth, horizontalPadding) + -- The small metrics are specifically defined to be one card more than the + -- medium metrics, so we grab that and then compute item width manually. + local mediumItemsPerRow = GridMetrics.getMediumMetrics(containerWidth, horizontalPadding).itemsPerRow + local itemsPerRow = mediumItemsPerRow + 1 + local itemWidth = math.floor((containerWidth - (itemsPerRow - 1) * horizontalPadding) / itemsPerRow) + + return { + itemsPerRow = itemsPerRow, + itemWidth = itemWidth, + } +end + +return GridMetrics \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridMetrics.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridMetrics.spec.lua new file mode 100644 index 0000000..b033ea5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridMetrics.spec.lua @@ -0,0 +1,135 @@ +local GridRoot = script.Parent +local AppRoot = GridRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent + +local t = require(Packages.t) + +local GridMetrics = require(script.Parent.GridMetrics) + +local isMetricsData = t.strictInterface({ + itemsPerRow = t.intersection(t.integer, t.numberMin(1)), + itemWidth = t.numberMin(0), +}) + +return function() + describe("makeCustomMetricsGetter", function() + it("should be a function", function() + expect(typeof(GridMetrics.makeCustomMetricsGetter)).to.equal("function") + end) + + it("should make metrics getter functions", function() + local getter = GridMetrics.makeCustomMetricsGetter({ + minimumItemsPerRow = 1, + minimumItemWidth = 160, + }) + + expect(typeof(getter)).to.equal("function") + end) + + it("should make functions that validate their arguments", function() + local getter = GridMetrics.makeCustomMetricsGetter({ + minimumItemsPerRow = 1, + minimumItemWidth = 160, + }) + + expect(function() + getter(-20, 10) + end).to.throw() + + expect(function() + getter(20, Vector2.new(10, 10)) + end).to.throw() + + expect(function() + getter(Vector2.new(10, 10), 0) + end).to.throw() + end) + + it("should throw if given invalid settings", function() + expect(function() + GridMetrics.makeCustomMetricsGetter({ + minimumItemsPerRow = 0, + }) + end).to.throw() + end) + end) + + describe("getLargeMetrics", function() + it("should be a function", function() + expect(typeof(GridMetrics.getLargeMetrics)).to.equal("function") + end) + + it("should return a metrics structure", function() + assert(isMetricsData(GridMetrics.getLargeMetrics(100, 10))) + end) + + it("should throw if given invalid arguments", function() + expect(function() + GridMetrics.getLargeMetrics(-20, 10) + end).to.throw() + + expect(function() + GridMetrics.getLargeMetrics(20, Vector2.new(10, 10)) + end).to.throw() + + expect(function() + GridMetrics.getLargeMetrics(Vector2.new(10, 10), 0) + end).to.throw() + end) + end) + + describe("getMediumMetrics", function() + it("should be a function", function() + expect(typeof(GridMetrics.getMediumMetrics)).to.equal("function") + end) + + it("should return a metrics structure", function() + assert(isMetricsData(GridMetrics.getMediumMetrics(100, 10))) + end) + + it("should throw if given invalid arguments", function() + expect(function() + GridMetrics.getMediumMetrics(-20, 10) + end).to.throw() + + expect(function() + GridMetrics.getMediumMetrics(20, Vector2.new(10, 10)) + end).to.throw() + + expect(function() + GridMetrics.getMediumMetrics(Vector2.new(10, 10), 0) + end).to.throw() + end) + end) + + describe("getSmallMetrics", function() + it("should be a function", function() + expect(typeof(GridMetrics.getSmallMetrics)).to.equal("function") + end) + + it("should return a metrics structure", function() + assert(isMetricsData(GridMetrics.getSmallMetrics(100, 10))) + end) + + it("should throw if given invalid arguments", function() + expect(function() + GridMetrics.getSmallMetrics(-20, 10) + end).to.throw() + + expect(function() + GridMetrics.getSmallMetrics(20, Vector2.new(10, 10)) + end).to.throw() + + expect(function() + GridMetrics.getSmallMetrics(Vector2.new(10, 10), 0) + end).to.throw() + end) + + it("should always have a card count 1 more than the medium getter", function() + local mediumSettings = GridMetrics.getMediumMetrics(400, 10) + local smallSettings = GridMetrics.getSmallMetrics(400, 10) + expect(smallSettings.itemsPerRow).to.equal(mediumSettings.itemsPerRow + 1) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridView.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridView.lua new file mode 100644 index 0000000..fb27afc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridView.lua @@ -0,0 +1,240 @@ +local GridRoot = script.Parent +local AppRoot = GridRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent +local UIBloxConfig = require(UIBloxRoot.UIBloxConfig) +local RoactGamepad = require(Packages.RoactGamepad) + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local positiveVector2 = require(UIBloxRoot.Utility.isPositiveVector2) + +local validateProps = t.strictInterface({ + -- A function that, given an item, returns a Roact element representing that + -- item. The item should expect to fill its parent. Setting LayoutOrder is + -- not necessary. + renderItem = t.callback, + -- The size of a grid item, in pixels. + itemSize = positiveVector2, + -- The spacing between grid cells, on each axis. + itemPadding = t.Vector2, + -- All the items that can be displayed in the grid. renderItem should be + -- able to use all values in this table. This must be an array (we don't + -- check if it is for performance reasons). + items = t.table, + -- The maximum height the grid view is allowed to grow to. + maxHeight = t.numberMin(0), + -- The height of the visible window in the grid view. If nil, the grid view + -- will render all of its items. + windowHeight = t.optional(t.numberMin(0)), + -- The layout order of the grid. + LayoutOrder = t.optional(t.integer), + -- Called when the grid view measures a change in its width. Used in + -- DefaultMetricsGridView to resize the grid cells. + onWidthChanged = t.optional(t.callback), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), + restorePreviousChildFocus = t.optional(t.boolean), + onFocusGained = t.optional(t.callback), + + -- which selection will initally be selected (if using roact-gamepad + defaultChildIndex = t.optional(t.numberMin(1)), +}) + +local GridView = Roact.PureComponent:extend("GridView") + +GridView.defaultProps = { + maxHeight = math.huge, + restorePreviousChildFocus = true, +} + +function GridView:init() + self.frameRef = Roact.createRef() + self.isMounted = false + + self.state = { + containerWidth = 0, + containerYPosition = 0, + } + + self.focusableRefs = RoactGamepad.createRefCache() +end + +function GridView:render() + assert(validateProps(self.props)) + local items = self.props.items + local itemCount = #items + + local itemSize = self.props.itemSize + local itemPadding = self.props.itemPadding + local maxHeight = self.props.maxHeight + local containerWidth = self.state.containerWidth + local containerYOffset = self.state.containerYPosition + local defaultChildIndex = self.props.defaultChildIndex + local startIndex = 1 + local endIndex = itemCount + local gridChildren = {} + local x, y = 0, 0 + local maxPossibleVisibleItems = itemCount + + local itemsPerRow = math.floor((containerWidth + itemPadding.X) / (itemSize.X + itemPadding.X)) + local totalRows = math.ceil(itemCount / itemsPerRow) + local maximumRenderableRows = math.floor((maxHeight + itemPadding.Y) / (itemSize.Y + itemPadding.Y)) + local displayedRows = math.min(maximumRenderableRows, totalRows) + local containerHeight = displayedRows * itemSize.Y + math.max(displayedRows - 1, 0) * itemPadding.Y + + if self.props.windowHeight ~= nil then + if UIBloxConfig.enableExperimentalGamepadSupport then + --ensure that when you scroll you don't see items "pop" into existence at the bottom + local padRows = 2 + local visibleRows = math.floor((self.props.windowHeight + itemPadding.Y) / (itemSize.Y + itemPadding.Y)) + padRows + local startingRow = math.floor((containerYOffset + itemPadding.Y) / (itemSize.Y + itemPadding.Y)) + local finalPadRows = 1 + local endingRow = math.min(displayedRows, startingRow + visibleRows) + finalPadRows + + startIndex = math.max(1, startingRow * itemsPerRow + 1) + endIndex = math.min(itemCount, endingRow * itemsPerRow) + y = startingRow * itemSize.Y + startingRow * itemPadding.Y + + local maxPossibleRowsDisplayed = math.min(maximumRenderableRows, visibleRows) + finalPadRows + maxPossibleVisibleItems = math.abs(maxPossibleRowsDisplayed * itemsPerRow) + else + -- Add one to ensure that when you scroll you don't see items "pop" into existence with windowing. + local visibleRows = math.floor((self.props.windowHeight + itemPadding.Y) / (itemSize.Y + itemPadding.Y)) + 1 + local startingRow = math.floor((containerYOffset + itemPadding.Y) / (itemSize.Y + itemPadding.Y)) + local endingRow = math.min(displayedRows, startingRow + visibleRows) + + startIndex = math.max(1, startingRow * itemsPerRow + 1) + endIndex = math.min(itemCount, endingRow * itemsPerRow + itemsPerRow) + y = startingRow * itemSize.Y + startingRow * itemPadding.Y + end + end + + -- using maxPossibleVisibleItems means the amount of render keys will not change between renders (assuming + -- positioning/size props don't change) this is important to ensure gamepad selection stability + local maxRenderKey = UIBloxConfig.enableExperimentalGamepadSupport and + maxPossibleVisibleItems or math.abs(startIndex - endIndex) + 1 + + local function calculateRenderKey(index) + return index % maxRenderKey + end + + local function getItemIndexRef(inputRow, inputCol) + local isRowAndColInRange = inputRow > 0 and inputCol > 0 and inputCol <= itemsPerRow + local index = 1 + (((inputRow-1)*itemsPerRow) + (inputCol-1)) + local isIndexInRange = index >= startIndex and index <= endIndex + return isIndexInRange and isRowAndColInRange and self.focusableRefs[calculateRenderKey(index)] or nil + end + + -- If the item height is already greater than the maximum size we shouldn't + -- render _anything_ + if containerHeight < maxHeight then + for itemIndex = startIndex, endIndex do + local renderKey = calculateRenderKey(itemIndex) + + local currentRow = 1 + (math.floor((itemIndex - 1) / itemsPerRow)) + local currentCol = 1 + ((itemIndex - 1) % itemsPerRow) + gridChildren[renderKey] = Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable.Frame or "Frame", { + BackgroundTransparency = 1, + Position = UDim2.new(0, x, 0, y), + Size = UDim2.new(0, itemSize.X, 0, itemSize.Y), + + NextSelectionLeft = getItemIndexRef(currentRow, currentCol-1), + NextSelectionRight = getItemIndexRef(currentRow, currentCol+1), + NextSelectionUp = getItemIndexRef(currentRow-1, currentCol), + NextSelectionDown = getItemIndexRef(currentRow+1, currentCol), + [Roact.Ref] = getItemIndexRef(currentRow, currentCol), + -- Optional Gamepad prop callback which is called when a grid member is focused on + onFocusGained = UIBloxConfig.enableExperimentalGamepadSupport and self.props.onFocusGained or nil, + }, { + Content = self.props.renderItem(items[itemIndex], itemIndex) + }) + + x = math.floor(x + itemSize.X + itemPadding.X) + + -- If the x position overflows the maximum size, wrap further content + -- onto another row. We check for just itemSize because the final + -- grid item doesn't have padding tacked onto the end of it. + if x + itemSize.X > containerWidth and itemIndex < endIndex then + x = 0 + y = y + itemPadding.Y + itemSize.Y + end + end + end + + return Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable.Frame or "Frame", { + BackgroundTransparency = 1, + LayoutOrder = self.props.LayoutOrder, + Size = UDim2.new(1, 0, 0, containerHeight), + [Roact.Change.AbsolutePosition] = self.props.windowHeight ~= nil and function(rbx) + spawn(function() + if self.isMounted then + self:setState({ + containerYPosition = -math.min(0, rbx.AbsolutePosition.Y), + }) + end + end) + end or nil, + [Roact.Change.AbsoluteSize] = function(rbx) + spawn(function() + if self.isMounted then + self:setState({ + containerWidth = rbx.AbsoluteSize.X, + }) + end + + if self.props.onWidthChanged ~= nil then + self.props.onWidthChanged(rbx.AbsoluteSize.X) + end + end) + end, + + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + + [Roact.Ref] = UIBloxConfig.enableExperimentalGamepadSupport and self.props[Roact.Ref] or self.frameRef, + -- Optional Gamepad prop for which grid member to focus on by default + defaultChild = (UIBloxConfig.enableExperimentalGamepadSupport and defaultChildIndex) and + self.focusableRefs[defaultChildIndex] or nil, + -- Optional Gamepad prop for whether the previous focused on grid member should be refocused + -- when returning focus to the grid + restorePreviousChildFocus = UIBloxConfig.enableExperimentalGamepadSupport and + self.props.restorePreviousChildFocus or nil, + }, gridChildren) +end + +function GridView:didMount() + self.isMounted = true + + local ref = UIBloxConfig.enableExperimentalGamepadSupport and self.props[Roact.Ref] or self.frameRef + + if ref.current and ref.current.AbsoluteSize.X ~= 0 then + self:setState({ + containerWidth = ref.current.AbsoluteSize.X, + }) + + if self.props.onWidthChanged ~= nil then + delay(0, function() + if ref.current then + self.props.onWidthChanged(ref.current.AbsoluteSize.X) + end + end) + end + end +end + +function GridView:willUnmount() + self.isMounted = false +end + +return GridView \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridView.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridView.spec.lua new file mode 100644 index 0000000..01a5333 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/GridView.spec.lua @@ -0,0 +1,224 @@ +local GridRoot = script.Parent +local AppRoot = GridRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent +local UIBloxConfig = require(UIBloxRoot.UIBloxConfig) + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) + +local GridView = require(GridRoot.GridView) + +-- Used to snapshot grid items for updating unit tests. +-- luacheck: ignore unused function snapshotGridItems +local function snapshotGridItems(grid) + local records = {} + for _, child in ipairs(grid:GetChildren()) do + table.insert(records, { + item = child.Content.Text, + relativePosition = child.AbsolutePosition - grid.AbsolutePosition, + }) + end + + table.sort(records, function(a, b) + return tonumber(a.item) < tonumber(b.item) + end) + + local buffer = { "{\n" } + for _, record in ipairs(records) do + table.insert(buffer, "\t{ relativePosition = Vector2.new(") + table.insert(buffer, record.relativePosition.X) + table.insert(buffer, ", ") + table.insert(buffer, record.relativePosition.Y) + table.insert(buffer, "), content = \"") + table.insert(buffer, record.item) + table.insert(buffer, "\" },\n") + end + table.insert(buffer, "}") + print(table.concat(buffer, "")) +end + +local function validateSnapshot(grid, snapshot) + local textToItemMap = {} + -- Grid views don't guarantee that child names are stable in any way, + -- particularly with windowing - the name of a grid item does not + -- reflect the index of the list item it was created for. + for _, child in ipairs(grid:GetChildren()) do + textToItemMap[child.Content.Text] = child + end + + assert(#snapshot == #grid:GetChildren(), + ("wrong number of children: %d present in grid, but %d in snapshot"):format( + #grid:GetChildren(), + #snapshot + ) + ) + + for _, record in ipairs(snapshot) do + local item = textToItemMap[record.content] + local relativePosition = item.AbsolutePosition - grid.AbsolutePosition + assert(relativePosition.X == record.relativePosition.X and relativePosition.Y == record.relativePosition.Y, + ("item %s: relative positions did not match: {%g, %g} ~= {%g, %g}"):format( + record.content, + relativePosition.X, + relativePosition.Y, + record.relativePosition.X, + record.relativePosition.Y + ) + ) + end +end + +return function() + HACK_NO_XPCALL() + + it("should lay items out sequentially", function() + -- This is a fairly unidiomatic test to verify that the grid view lays + -- out items correctly. It tears apart the rendered Roact tree, using + -- knowledge of GridView's internals, and determines if the grid items' + -- positions match what they should be. The values used in this test are + -- designed to test several edge cases of the grid view layout system. + local expectedRelativePositions = { + Vector2.new(0, 0), + Vector2.new(112, 0), + Vector2.new(224, 0), + -- The fourth item is expected to wrap because it can't completely + -- fit in the empty space on the first row after the third item. + Vector2.new(0, 112), + } + + local tree = Roact.createElement("Frame", { + Size = UDim2.new(0, 350, 0, 200), + }, { + Grid = Roact.createElement(GridView, { + renderItem = function(i) + return Roact.createElement("TextLabel", { + Text = i, + }) + end, + items = { 1, 2, 3, 4 }, + itemPadding = Vector2.new(12, 12), + itemSize = Vector2.new(100, 100), + }), + }) + + local container = Instance.new("ScreenGui") + local handle = Roact.mount(tree, container, "GridTest") + local grid = container.GridTest.Grid + + -- Grids expand to fill their parent on the X axis, and expand to fit + -- the total number of items in the grid on the Y axis. + expect(grid.AbsoluteSize.X).to.equal(350) + expect(grid.AbsoluteSize.Y).to.equal(212) + + local textToItemMap = {} + -- Grid views don't guarantee that child names are stable in any way, + -- particularly with windowing - the name of a grid item does not + -- reflect the index of the list item it was created for. + for _, child in ipairs(grid:GetChildren()) do + textToItemMap[child.Content.Text] = child + end + + for itemValue, expectedRelativePosition in ipairs(expectedRelativePositions) do + local item = textToItemMap[tostring(itemValue)] + assert(item ~= nil, "couldn't find item for index " .. itemValue) + + local relativePosition = item.AbsolutePosition - grid.AbsolutePosition + expect(relativePosition.X).to.equal(expectedRelativePosition.X) + expect(relativePosition.Y).to.equal(expectedRelativePosition.Y) + end + + -- Grids don't use a layout object, so this check will work for ensuring + -- that all the items were rendered. + expect(#grid:GetChildren()).to.equal(#expectedRelativePositions) + Roact.unmount(handle) + end) + + it("should window items if windowHeight is specified", function() + local itemCount = 100 + local items = {} + for i = 1, itemCount do + table.insert(items, i) + end + + local tree = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, (itemCount / 4) * 25), + }, { + Grid = Roact.createElement(GridView, { + renderItem = function(i) + return Roact.createElement("TextLabel", { + Text = i, + }) + end, + items = items, + itemPadding = Vector2.new(7, 7), + itemSize = Vector2.new(46, 38), + windowHeight = 127, + }), + }) + + local container = Instance.new("ScreenGui") + local handle = Roact.mount(tree, container, "GridTest") + local grid = container.GridTest.Grid + + -- Grids expand to fill their parent on the X axis, and expand to fit + -- the total number of items in the grid on the Y axis. When windowing, + -- they will still size themselves to fit _all_ the grids, even if + -- they're not all rendered! + expect(grid.AbsoluteSize.X).to.equal(100) + + local initialSnapshot = { + { relativePosition = Vector2.new(0, 0), content = "1" }, + { relativePosition = Vector2.new(53, 0), content = "2" }, + { relativePosition = Vector2.new(0, 45), content = "3" }, + { relativePosition = Vector2.new(53, 45), content = "4" }, + { relativePosition = Vector2.new(0, 90), content = "5" }, + { relativePosition = Vector2.new(53, 90), content = "6" }, + { relativePosition = Vector2.new(0, 135), content = "7" }, + { relativePosition = Vector2.new(53, 135), content = "8" }, + } + + if UIBloxConfig.enableExperimentalGamepadSupport then + initialSnapshot = Cryo.List.join(initialSnapshot, { + { relativePosition = Vector2.new(0, 180), content = "9" }, + { relativePosition = Vector2.new(53, 180), content = "10" }, + }) + end + + -- snapshotGridItems(grid) + validateSnapshot(grid, initialSnapshot) + + -- If we move the grid, it will relayout itself and render a different + -- set of items. + grid.Position = UDim2.new(0, 0, 0, -250) + -- Dummy read necessary to force the windowing to update. In the test + -- scenario that we have, absolute position will not update until the + -- property is read somewhere in the tree. + local _ = grid.AbsolutePosition + + local afterMoveSnapshot = { + { relativePosition = Vector2.new(0, 225), content = "11" }, + { relativePosition = Vector2.new(53, 225), content = "12" }, + { relativePosition = Vector2.new(0, 270), content = "13" }, + { relativePosition = Vector2.new(53, 270), content = "14" }, + { relativePosition = Vector2.new(0, 315), content = "15" }, + { relativePosition = Vector2.new(53, 315), content = "16" }, + { relativePosition = Vector2.new(0, 360), content = "17" }, + { relativePosition = Vector2.new(53, 360), content = "18" }, + } + + if UIBloxConfig.enableExperimentalGamepadSupport then + afterMoveSnapshot = Cryo.List.join(afterMoveSnapshot, { + { relativePosition = Vector2.new(0, 405), content = "19" }, + { relativePosition = Vector2.new(53, 405), content = "20" }, + }) + end + + wait() + + -- snapshotGridItems(grid) + validateSnapshot(grid, afterMoveSnapshot) + + Roact.unmount(handle) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/ScrollingGridView.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/ScrollingGridView.lua new file mode 100644 index 0000000..9c55ca3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/ScrollingGridView.lua @@ -0,0 +1,79 @@ +local Grid = script.Parent +local App = Grid.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local VerticalScrollView = require(App.Container.VerticalScrollView) + +local ScrollingGridView = Roact.PureComponent:extend("ScrollingGridView") + +ScrollingGridView.validateProps = t.strictInterface({ + items = t.table, + renderItem = t.callback, + itemSize = t.Vector2, + size = t.optional(t.UDim2), + itemPadding = t.optional(t.Vector2), + innerUIPadding = t.optional(t.strictInterface({ + PaddingTop = t.optional(t.UDim), + PaddingBottom = t.optional(t.UDim), + PaddingLeft = t.optional(t.UDim), + PaddingRight = t.optional(t.UDim), + })), + horizontalAlignment = t.optional(t.EnumItem), +}) + +ScrollingGridView.defaultProps = { + itemPadding = Vector2.new(12, 24), + horizontalAlignment = Enum.HorizontalAlignment.Center, +} + +function ScrollingGridView:init() + self.gridRef = Roact.createRef() + self.state ={ + contentSize = Vector2.new(0, 0) + } + + self.onGridResize = function() + local contentSize = self.gridRef.current.AbsoluteContentSize + self:setState({ + contentSize = contentSize, + }) + end +end + +function ScrollingGridView:render() + local contentSize = self.state.contentSize + + local gridItems = {} + + for key, value in pairs(self.props.items) do + gridItems[key] = self.props.renderItem(value) + end + + gridItems.GridLayout = Roact.createElement("UIGridLayout", { + CellSize = UDim2.fromOffset(self.props.itemSize.X, self.props.itemSize.Y), + CellPadding = UDim2.fromOffset(self.props.itemPadding.X, self.props.itemPadding.Y), + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = self.props.horizontalAlignment, + [Roact.Change.AbsoluteContentSize] = self.onGridResize, + [Roact.Ref] = self.gridRef, + }) + + gridItems.UIPadding = Roact.createElement("UIPadding", self.props.innerUIPadding) + + return Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + [Roact.Change.AbsoluteSize] = self.onResize, + }, { + VerticalScrollView = Roact.createElement(VerticalScrollView, { + size = UDim2.fromScale(1, 1), + canvasSizeY = UDim.new(0, contentSize.Y), + }, gridItems) + }) +end + +return ScrollingGridView diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/ScrollingGridView.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/ScrollingGridView.spec.lua new file mode 100644 index 0000000..995c76d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/ScrollingGridView.spec.lua @@ -0,0 +1,54 @@ +return function() + local Grid = script.Parent + local App = Grid.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local ScrollingGridView = require(script.Parent.ScrollingGridView) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + ScrollingGridView = Roact.createElement(ScrollingGridView, { + items = {1, 2, 3}, + renderItem = function(i) + return Roact.createElement("TextLabel", { + Text = i, + Size = UDim2.new(1, 0, 1, 0), + }) + end, + itemSize = Vector2.new(100, 100), + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with all props", function() + local element = mockStyleComponent({ + ScrollingGridView = Roact.createElement(ScrollingGridView, { + items = {1, 2, 3}, + renderItem = function(i) + return Roact.createElement("TextLabel", { + Text = i, + Size = UDim2.new(1, 0, 1, 0), + }) + end, + itemSize = Vector2.new(100, 100), + size = UDim2.new(1, 0, 1, 0), + itemPadding = Vector2.new(12, 24), + innerUIPadding = { + PaddingTop = UDim.new(0, 10), + PaddingBottom = UDim.new(0, 10), + PaddingLeft = UDim.new(0, 10), + PaddingRight = UDim.new(0, 10), + }, + horizontalAlignment = Enum.HorizontalAlignment.Center, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/__stories__/Grid.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/__stories__/Grid.story.lua new file mode 100644 index 0000000..4894df8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Grid/__stories__/Grid.story.lua @@ -0,0 +1,135 @@ +local GridRoot = script.Parent.Parent +local AppRoot = GridRoot.Parent +local UIBloxRoot = AppRoot.Parent +local Packages = UIBloxRoot.Parent +local Roact = require(Packages.Roact) +local UIBloxConfig = require(UIBloxRoot.UIBloxConfig) +local RoactGamepad = require(Packages.RoactGamepad) + +local DefaultMetricsGridView = require(GridRoot.DefaultMetricsGridView) +local GridMetrics = require(GridRoot.GridMetrics) + +local InputManager = require(Packages.StoryComponents.InputManager) + +local DemoComponent = Roact.PureComponent:extend("DemoComponent") + +function DemoComponent:init() + self.scrollingRef = Roact.createRef() + + self.state = { + windowSize = Vector2.new(0, 0), + metrics = GridMetrics.getSmallMetrics, + } + + if UIBloxConfig.enableExperimentalGamepadSupport then + self.focusController = RoactGamepad.createFocusController() + end +end + +function DemoComponent:updateWindowSize() + self:setState({ + windowSize = self.scrollingRef.current.AbsoluteWindowSize, + }) +end + +function DemoComponent:render() + local items = {} + + for i = 1, 100 do + table.insert(items, i) + end + + local metrics = { + { + name = "Small", + getter = GridMetrics.getSmallMetrics, + }, + { + name = "Medium", + getter = GridMetrics.getMediumMetrics, + }, + { + name = "Large", + getter = GridMetrics.getLargeMetrics, + }, + } + + local selectorChildren = { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + }) + } + + for index, item in ipairs(metrics) do + selectorChildren[item.name] = Roact.createElement("TextButton", { + Size = UDim2.new(1 / #metrics, 0, 1, 0), + Text = item.name, + LayoutOrder = index, + [Roact.Event.Activated] = function() + self:setState({ + metrics = GridMetrics["get" .. item.name .. "Metrics"], + }) + end, + }) + end + + return Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport and + RoactGamepad.Focusable.Frame or "Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + + focusController = UIBloxConfig.enableExperimentalGamepadSupport and self.focusController or nil, + }, { + InputManager = UIBloxConfig.enableExperimentalGamepadSupport and Roact.createElement(InputManager, { + focusController = self.focusController, + }), + MetricsSelector = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 30), + BackgroundTransparency = 1, + }, selectorChildren), + GridScroller = Roact.createElement("ScrollingFrame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, -30), + Position = UDim2.new(0, 0, 0, 30), + ScrollBarImageColor3 = Color3.new(0, 0, 0), + VerticalScrollBarInset = Enum.ScrollBarInset.Always, + [Roact.Ref] = self.scrollingRef, + [Roact.Change.AbsoluteSize] = function() + wait(0) + self:updateWindowSize() + end, + }, { + GridView = Roact.createElement(DefaultMetricsGridView, { + renderItem = function(i) + return Roact.createElement("TextLabel", { + Text = i, + LayoutOrder = i, + Size = UDim2.new(1, 0, 1, 0), + }) + end, + getItemHeight = function(width) + return width + end, + getItemMetrics = self.state.metrics, + windowHeight = self.state.windowSize.Y, + itemPadding = Vector2.new(12, 12), + items = items, + + defaultChildIndex = UIBloxConfig.enableExperimentalGamepadSupport and 1 or nil, + }) + }) + }) +end + +function DemoComponent:didMount() + self:updateWindowSize() +end + +return function(target) + local handle = Roact.mount(Roact.createElement(DemoComponent), target, "2DGrid") + + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Enum/IconSize.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Enum/IconSize.lua new file mode 100644 index 0000000..68d7035 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Enum/IconSize.lua @@ -0,0 +1,13 @@ +local ImageSet = script.Parent.Parent +local AppRoot = ImageSet.Parent +local UIBlox = AppRoot.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate("IconSize", { + "Small", + "Medium", + "Large", + "XLarge", + "XXLarge", +}) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/GetImageSetData.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/GetImageSetData.lua new file mode 100644 index 0000000..929f6db --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/GetImageSetData.lua @@ -0,0 +1,863 @@ + +-- luacheck: ignore +------------------------------------------------------------------ +-- AUTOGENERATED +-- by bin/ImagePacker.py in UIBlox +-- +-- DO NOT EDIT +------------------------------------------------------------------ + +local assets_1x = nil +function make_assets_1x() assets_1x = { + ['chat_bubble/chat-bubble'] = { ImageRectOffset = Vector2.new(22, 490), ImageRectSize = Vector2.new(21, 21), ImageSet = 'img_set_1x_2' }, + ['chat_bubble/chat-bubble-self'] = { ImageRectOffset = Vector2.new(0, 490), ImageRectSize = Vector2.new(21, 21), ImageSet = 'img_set_1x_2' }, + ['chat_bubble/chat-bubble-self-tip'] = { ImageRectOffset = Vector2.new(505, 139), ImageRectSize = Vector2.new(6, 6), ImageSet = 'img_set_1x_1' }, + ['chat_bubble/chat-bubble-self2'] = { ImageRectOffset = Vector2.new(490, 0), ImageRectSize = Vector2.new(21, 21), ImageSet = 'img_set_1x_2' }, + ['chat_bubble/chat-bubble-tip'] = { ImageRectOffset = Vector2.new(505, 146), ImageRectSize = Vector2.new(6, 6), ImageSet = 'img_set_1x_1' }, + ['chat_bubble/chat-bubble2'] = { ImageRectOffset = Vector2.new(472, 486), ImageRectSize = Vector2.new(21, 21), ImageSet = 'img_set_1x_1' }, + ['component_assets/bulletDown_17_stroke_3'] = { ImageRectOffset = Vector2.new(116, 493), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_2' }, + ['component_assets/bulletLeft_17_stroke_3'] = { ImageRectOffset = Vector2.new(362, 494), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/bulletRight_17_stroke_3'] = { ImageRectOffset = Vector2.new(98, 493), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_2' }, + ['component_assets/bulletUp_17_stroke_3'] = { ImageRectOffset = Vector2.new(344, 494), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/bullet_17'] = { ImageRectOffset = Vector2.new(380, 494), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_16'] = { ImageRectOffset = Vector2.new(135, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['component_assets/circle_17'] = { ImageRectOffset = Vector2.new(490, 22), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_2' }, + ['component_assets/circle_17_mask'] = { ImageRectOffset = Vector2.new(49, 487), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_2' }, + ['component_assets/circle_17_stroke_1'] = { ImageRectOffset = Vector2.new(67, 487), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_2' }, + ['component_assets/circle_17_stroke_3'] = { ImageRectOffset = Vector2.new(494, 486), ImageRectSize = Vector2.new(17, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_21'] = { ImageRectOffset = Vector2.new(424, 486), ImageRectSize = Vector2.new(21, 21), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_21_stroke_1'] = { ImageRectOffset = Vector2.new(450, 486), ImageRectSize = Vector2.new(21, 21), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_22_stroke_3'] = { ImageRectOffset = Vector2.new(401, 486), ImageRectSize = Vector2.new(22, 22), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_24_stroke_1'] = { ImageRectOffset = Vector2.new(218, 487), ImageRectSize = Vector2.new(24, 24), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_25'] = { ImageRectOffset = Vector2.new(301, 484), ImageRectSize = Vector2.new(25, 25), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_26_stroke_3'] = { ImageRectOffset = Vector2.new(470, 254), ImageRectSize = Vector2.new(26, 26), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_28_padding_10'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['component_assets/circle_29'] = { ImageRectOffset = Vector2.new(122, 477), ImageRectSize = Vector2.new(29, 29), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_29_mask'] = { ImageRectOffset = Vector2.new(92, 477), ImageRectSize = Vector2.new(29, 29), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_29_stroke_1'] = { ImageRectOffset = Vector2.new(62, 477), ImageRectSize = Vector2.new(29, 29), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_30_stroke_3'] = { ImageRectOffset = Vector2.new(0, 477), ImageRectSize = Vector2.new(30, 30), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_36'] = { ImageRectOffset = Vector2.new(49, 376), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['component_assets/circle_36_stroke_1'] = { ImageRectOffset = Vector2.new(49, 339), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['component_assets/circle_42_stroke_3'] = { ImageRectOffset = Vector2.new(49, 147), ImageRectSize = Vector2.new(42, 42), ImageSet = 'img_set_1x_2' }, + ['component_assets/circle_49'] = { ImageRectOffset = Vector2.new(193, 337), ImageRectSize = Vector2.new(49, 49), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_49_mask'] = { ImageRectOffset = Vector2.new(193, 387), ImageRectSize = Vector2.new(49, 49), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_49_stroke_1'] = { ImageRectOffset = Vector2.new(193, 437), ImageRectSize = Vector2.new(49, 49), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_52_stroke_3'] = { ImageRectOffset = Vector2.new(193, 284), ImageRectSize = Vector2.new(52, 52), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_60_stroke_2'] = { ImageRectOffset = Vector2.new(405, 327), ImageRectSize = Vector2.new(60, 60), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_68_stroke_2'] = { ImageRectOffset = Vector2.new(440, 70), ImageRectSize = Vector2.new(68, 68), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_69_stroke_3'] = { ImageRectOffset = Vector2.new(440, 0), ImageRectSize = Vector2.new(69, 69), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_9'] = { ImageRectOffset = Vector2.new(502, 228), ImageRectSize = Vector2.new(9, 9), ImageSet = 'img_set_1x_1' }, + ['component_assets/circle_9_stroke_1'] = { ImageRectOffset = Vector2.new(502, 238), ImageRectSize = Vector2.new(9, 9), ImageSet = 'img_set_1x_1' }, + ['component_assets/dropshadow_16_20'] = { ImageRectOffset = Vector2.new(344, 388), ImageRectSize = Vector2.new(56, 56), ImageSet = 'img_set_1x_1' }, + ['component_assets/dropshadow_25'] = { ImageRectOffset = Vector2.new(49, 190), ImageRectSize = Vector2.new(37, 37), ImageSet = 'img_set_1x_2' }, + ['component_assets/dropshadow_28'] = { ImageRectOffset = Vector2.new(0, 49), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['component_assets/dropshadow_chatOff'] = { ImageRectOffset = Vector2.new(49, 302), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['component_assets/dropshadow_chatOn'] = { ImageRectOffset = Vector2.new(49, 228), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['component_assets/dropshadow_more'] = { ImageRectOffset = Vector2.new(49, 265), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['component_assets/halfcircleLeft_17'] = { ImageRectOffset = Vector2.new(181, 477), ImageRectSize = Vector2.new(8, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/halfcircleRight_17'] = { ImageRectOffset = Vector2.new(502, 193), ImageRectSize = Vector2.new(8, 17), ImageSet = 'img_set_1x_1' }, + ['component_assets/square_7_stroke_3'] = { ImageRectOffset = Vector2.new(487, 281), ImageRectSize = Vector2.new(7, 7), ImageSet = 'img_set_1x_1' }, + ['component_assets/triangleDown_16'] = { ImageRectOffset = Vector2.new(327, 501), ImageRectSize = Vector2.new(16, 8), ImageSet = 'img_set_1x_1' }, + ['component_assets/triangleLeft_16'] = { ImageRectOffset = Vector2.new(502, 211), ImageRectSize = Vector2.new(8, 16), ImageSet = 'img_set_1x_1' }, + ['component_assets/triangleRight_16'] = { ImageRectOffset = Vector2.new(181, 495), ImageRectSize = Vector2.new(8, 16), ImageSet = 'img_set_1x_1' }, + ['component_assets/triangleUp_16'] = { ImageRectOffset = Vector2.new(470, 281), ImageRectSize = Vector2.new(16, 8), ImageSet = 'img_set_1x_1' }, + ['component_assets/user_60_mask'] = { ImageRectOffset = Vector2.new(344, 327), ImageRectSize = Vector2.new(60, 60), ImageSet = 'img_set_1x_1' }, + ['component_assets/vignette_246'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(246, 246), ImageSet = 'img_set_1x_1' }, + ['gradient/gradient_0_100'] = { ImageRectOffset = Vector2.new(243, 337), ImageRectSize = Vector2.new(1, 40), ImageSet = 'img_set_1x_1' }, + ['icons/actions/accept'] = { ImageRectOffset = Vector2.new(394, 456), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/block'] = { ImageRectOffset = Vector2.new(431, 49), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/calendar'] = { ImageRectOffset = Vector2.new(474, 176), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/actions/compose'] = { ImageRectOffset = Vector2.new(320, 308), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/cycleLeft'] = { ImageRectOffset = Vector2.new(283, 419), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/cycleRight'] = { ImageRectOffset = Vector2.new(357, 456), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/edit/add'] = { ImageRectOffset = Vector2.new(320, 345), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/edit/clear'] = { ImageRectOffset = Vector2.new(320, 382), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/edit/clear_small'] = { ImageRectOffset = Vector2.new(491, 176), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/actions/edit/copy'] = { ImageRectOffset = Vector2.new(394, 345), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/edit/delete'] = { ImageRectOffset = Vector2.new(320, 456), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/edit/edit'] = { ImageRectOffset = Vector2.new(357, 345), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/edit/remove'] = { ImageRectOffset = Vector2.new(320, 419), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/favoriteOff'] = { ImageRectOffset = Vector2.new(394, 308), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/favoriteOn'] = { ImageRectOffset = Vector2.new(357, 419), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/feedback'] = { ImageRectOffset = Vector2.new(431, 419), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/filter'] = { ImageRectOffset = Vector2.new(468, 345), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/friends/friendAdd'] = { ImageRectOffset = Vector2.new(468, 382), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/friends/friendInvite'] = { ImageRectOffset = Vector2.new(394, 382), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/friends/friendRemove'] = { ImageRectOffset = Vector2.new(394, 419), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/friends/friendsplaying'] = { ImageRectOffset = Vector2.new(431, 382), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/info'] = { ImageRectOffset = Vector2.new(394, 271), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/info_small'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_3' }, + ['icons/actions/previewExpand'] = { ImageRectOffset = Vector2.new(283, 382), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/previewShrink'] = { ImageRectOffset = Vector2.new(431, 308), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/randomize'] = { ImageRectOffset = Vector2.new(283, 456), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/reject'] = { ImageRectOffset = Vector2.new(320, 382), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/respawn'] = { ImageRectOffset = Vector2.new(357, 308), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/selectOn'] = { ImageRectOffset = Vector2.new(431, 456), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/selectOn_small'] = { ImageRectOffset = Vector2.new(0, 17), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_3' }, + ['icons/actions/send'] = { ImageRectOffset = Vector2.new(468, 419), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/share'] = { ImageRectOffset = Vector2.new(357, 382), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/truncationCollapse'] = { ImageRectOffset = Vector2.new(468, 308), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/truncationExpand'] = { ImageRectOffset = Vector2.new(474, 290), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/actions/viewOff'] = { ImageRectOffset = Vector2.new(457, 176), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/actions/viewOn'] = { ImageRectOffset = Vector2.new(327, 484), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/actions/vote/voteDownOff'] = { ImageRectOffset = Vector2.new(468, 271), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/vote/voteDownOn'] = { ImageRectOffset = Vector2.new(283, 345), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/vote/voteUpOff'] = { ImageRectOffset = Vector2.new(431, 271), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/vote/voteUpOn'] = { ImageRectOffset = Vector2.new(283, 308), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/zoomIn'] = { ImageRectOffset = Vector2.new(468, 456), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/actions/zoomOut'] = { ImageRectOffset = Vector2.new(431, 345), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/goldrobux'] = { ImageRectOffset = Vector2.new(320, 197), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/goldrobux_small'] = { ImageRectOffset = Vector2.new(0, 34), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_3' }, + ['icons/common/more'] = { ImageRectOffset = Vector2.new(431, 197), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/notificationOff'] = { ImageRectOffset = Vector2.new(209, 271), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/notificationOn'] = { ImageRectOffset = Vector2.new(394, 197), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/play'] = { ImageRectOffset = Vector2.new(468, 197), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/refresh'] = { ImageRectOffset = Vector2.new(209, 234), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/refresh_small'] = { ImageRectOffset = Vector2.new(497, 254), ImageRectSize = Vector2.new(14, 15), ImageSet = 'img_set_1x_1' }, + ['icons/common/robux'] = { ImageRectOffset = Vector2.new(357, 197), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/robux_small'] = { ImageRectOffset = Vector2.new(0, 51), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_3' }, + ['icons/common/search'] = { ImageRectOffset = Vector2.new(283, 197), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/search_small'] = { ImageRectOffset = Vector2.new(0, 68), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_3' }, + ['icons/common/settings'] = { ImageRectOffset = Vector2.new(209, 197), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/user'] = { ImageRectOffset = Vector2.new(246, 197), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/common/user_60'] = { ImageRectOffset = Vector2.new(441, 193), ImageRectSize = Vector2.new(60, 60), ImageSet = 'img_set_1x_1' }, + ['icons/controls/close-ingame'] = { ImageRectOffset = Vector2.new(98, 86), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/controls'] = { ImageRectOffset = Vector2.new(172, 419), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/emoteOff'] = { ImageRectOffset = Vector2.new(152, 477), ImageRectSize = Vector2.new(28, 28), ImageSet = 'img_set_1x_1' }, + ['icons/controls/emoteOn'] = { ImageRectOffset = Vector2.new(441, 254), ImageRectSize = Vector2.new(28, 28), ImageSet = 'img_set_1x_1' }, + ['icons/controls/keys/alt'] = { ImageRectOffset = Vector2.new(357, 160), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/apostrophe'] = { ImageRectOffset = Vector2.new(172, 160), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/arrowDown'] = { ImageRectOffset = Vector2.new(0, 119), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_3' }, + ['icons/controls/keys/arrowLeft'] = { ImageRectOffset = Vector2.new(0, 102), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_3' }, + ['icons/controls/keys/arrowRight'] = { ImageRectOffset = Vector2.new(0, 136), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_3' }, + ['icons/controls/keys/arrowUp'] = { ImageRectOffset = Vector2.new(0, 85), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_3' }, + ['icons/controls/keys/asterisk'] = { ImageRectOffset = Vector2.new(283, 160), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/backspace'] = { ImageRectOffset = Vector2.new(209, 160), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/capslock'] = { ImageRectOffset = Vector2.new(172, 308), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/caret'] = { ImageRectOffset = Vector2.new(246, 160), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/comma'] = { ImageRectOffset = Vector2.new(98, 345), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/command'] = { ImageRectOffset = Vector2.new(394, 160), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/control'] = { ImageRectOffset = Vector2.new(468, 123), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/dpadDown'] = { ImageRectOffset = Vector2.new(468, 160), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/dpadLeft'] = { ImageRectOffset = Vector2.new(209, 123), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/dpadRight'] = { ImageRectOffset = Vector2.new(283, 123), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/dpadUp'] = { ImageRectOffset = Vector2.new(135, 234), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/graveaccent'] = { ImageRectOffset = Vector2.new(135, 160), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/key_single'] = { ImageRectOffset = Vector2.new(357, 123), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/key_wide'] = { ImageRectOffset = Vector2.new(440, 139), ImageRectSize = Vector2.new(64, 36), ImageSet = 'img_set_1x_1' }, + ['icons/controls/keys/key_xwide'] = { ImageRectOffset = Vector2.new(344, 290), ImageRectSize = Vector2.new(92, 36), ImageSet = 'img_set_1x_1' }, + ['icons/controls/keys/option'] = { ImageRectOffset = Vector2.new(135, 197), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/period'] = { ImageRectOffset = Vector2.new(172, 271), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/return'] = { ImageRectOffset = Vector2.new(394, 123), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/shift'] = { ImageRectOffset = Vector2.new(135, 382), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/spacebar'] = { ImageRectOffset = Vector2.new(172, 197), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/tab'] = { ImageRectOffset = Vector2.new(135, 345), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxA'] = { ImageRectOffset = Vector2.new(98, 419), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxB'] = { ImageRectOffset = Vector2.new(172, 123), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxLS'] = { ImageRectOffset = Vector2.new(135, 123), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxLSDirectional'] = { ImageRectOffset = Vector2.new(135, 456), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxLSHorizontal'] = { ImageRectOffset = Vector2.new(320, 160), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxLSVertical'] = { ImageRectOffset = Vector2.new(431, 123), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxLT'] = { ImageRectOffset = Vector2.new(320, 123), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxRB'] = { ImageRectOffset = Vector2.new(135, 308), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxRS'] = { ImageRectOffset = Vector2.new(135, 419), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxRSDirectional'] = { ImageRectOffset = Vector2.new(98, 382), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxRSHorizontal'] = { ImageRectOffset = Vector2.new(246, 123), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxRSVertical'] = { ImageRectOffset = Vector2.new(98, 456), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxRT'] = { ImageRectOffset = Vector2.new(135, 271), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxView'] = { ImageRectOffset = Vector2.new(431, 160), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxX'] = { ImageRectOffset = Vector2.new(172, 345), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxY'] = { ImageRectOffset = Vector2.new(172, 234), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/keys/xboxmenu'] = { ImageRectOffset = Vector2.new(172, 382), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/leaderboardOff'] = { ImageRectOffset = Vector2.new(98, 123), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/leaderboardOn'] = { ImageRectOffset = Vector2.new(98, 197), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/mouse/clickLeft'] = { ImageRectOffset = Vector2.new(98, 308), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/mouse/clickRight'] = { ImageRectOffset = Vector2.new(98, 234), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/mouse/scroll'] = { ImageRectOffset = Vector2.new(98, 271), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/players'] = { ImageRectOffset = Vector2.new(468, 86), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/screenrecord'] = { ImageRectOffset = Vector2.new(172, 456), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/screenshot'] = { ImageRectOffset = Vector2.new(98, 160), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/topmenu-shadow'] = { ImageRectOffset = Vector2.new(98, 0), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/controls/vehicle/backward'] = { ImageRectOffset = Vector2.new(431, 86), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/vehicle/driver'] = { ImageRectOffset = Vector2.new(246, 86), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/vehicle/exit'] = { ImageRectOffset = Vector2.new(357, 86), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/vehicle/flip'] = { ImageRectOffset = Vector2.new(320, 86), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/vehicle/forward'] = { ImageRectOffset = Vector2.new(283, 86), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/vehicle/passenger'] = { ImageRectOffset = Vector2.new(394, 86), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/weapon/fire'] = { ImageRectOffset = Vector2.new(135, 86), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/weapon/scopeOff'] = { ImageRectOffset = Vector2.new(172, 86), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/controls/weapon/scopeOn'] = { ImageRectOffset = Vector2.new(209, 86), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/graphic/devices_xxlarge'] = { ImageRectOffset = Vector2.new(247, 0), ImageRectSize = Vector2.new(192, 192), ImageSet = 'img_set_1x_1' }, + ['icons/graphic/error_xlarge'] = { ImageRectOffset = Vector2.new(344, 193), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_1x_1' }, + ['icons/graphic/loadingspinner'] = { ImageRectOffset = Vector2.new(0, 441), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/graphic/lock_xxlarge'] = { ImageRectOffset = Vector2.new(0, 284), ImageRectSize = Vector2.new(192, 192), ImageSet = 'img_set_1x_1' }, + ['icons/graphic/premium_large'] = { ImageRectOffset = Vector2.new(49, 0), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/graphic/premium_xlarge'] = { ImageRectOffset = Vector2.new(247, 290), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_1x_1' }, + ['icons/graphic/success_xlarge'] = { ImageRectOffset = Vector2.new(247, 193), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_1x_1' }, + ['icons/imageUnavailable'] = { ImageRectOffset = Vector2.new(466, 327), ImageRectSize = Vector2.new(44, 44), ImageSet = 'img_set_1x_1' }, + ['icons/logo/block'] = { ImageRectOffset = Vector2.new(209, 308), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/logo/letterform'] = { ImageRectOffset = Vector2.new(0, 247), ImageRectSize = Vector2.new(207, 36), ImageSet = 'img_set_1x_1' }, + ['icons/menu/about_large'] = { ImageRectOffset = Vector2.new(343, 0), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/menu/avatar_off'] = { ImageRectOffset = Vector2.new(394, 234), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/avatar_on'] = { ImageRectOffset = Vector2.new(246, 345), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/blog'] = { ImageRectOffset = Vector2.new(431, 234), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/blog_large'] = { ImageRectOffset = Vector2.new(49, 98), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/menu/chat_off'] = { ImageRectOffset = Vector2.new(246, 419), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/chat_on'] = { ImageRectOffset = Vector2.new(209, 456), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/create_large'] = { ImageRectOffset = Vector2.new(147, 0), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/menu/customize'] = { ImageRectOffset = Vector2.new(468, 234), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/customize_large'] = { ImageRectOffset = Vector2.new(344, 445), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/events_large'] = { ImageRectOffset = Vector2.new(450, 388), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/feed'] = { ImageRectOffset = Vector2.new(209, 345), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/feed_large'] = { ImageRectOffset = Vector2.new(401, 388), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/friends'] = { ImageRectOffset = Vector2.new(357, 234), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/friends_large'] = { ImageRectOffset = Vector2.new(401, 437), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/games_off'] = { ImageRectOffset = Vector2.new(246, 234), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/games_on'] = { ImageRectOffset = Vector2.new(320, 271), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/giftcard'] = { ImageRectOffset = Vector2.new(246, 308), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/groups'] = { ImageRectOffset = Vector2.new(283, 271), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/groups_large'] = { ImageRectOffset = Vector2.new(450, 437), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_1' }, + ['icons/menu/help_large'] = { ImageRectOffset = Vector2.new(49, 49), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/menu/home_off'] = { ImageRectOffset = Vector2.new(246, 382), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/home_on'] = { ImageRectOffset = Vector2.new(320, 234), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/inventory'] = { ImageRectOffset = Vector2.new(209, 419), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/inventoryOff'] = { ImageRectOffset = Vector2.new(209, 382), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/inventoryOn'] = { ImageRectOffset = Vector2.new(209, 419), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/inventory_large'] = { ImageRectOffset = Vector2.new(196, 0), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/menu/messages_large'] = { ImageRectOffset = Vector2.new(441, 0), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/menu/more_off'] = { ImageRectOffset = Vector2.new(431, 197), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/more_on'] = { ImageRectOffset = Vector2.new(246, 456), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/profile'] = { ImageRectOffset = Vector2.new(246, 271), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/scanqr_large'] = { ImageRectOffset = Vector2.new(392, 0), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/menu/settings_large'] = { ImageRectOffset = Vector2.new(245, 0), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/menu/shop'] = { ImageRectOffset = Vector2.new(283, 234), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/menu/shop_large'] = { ImageRectOffset = Vector2.new(294, 0), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/menu/trade'] = { ImageRectOffset = Vector2.new(357, 271), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/navigation/close'] = { ImageRectOffset = Vector2.new(209, 49), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/navigation/pushBack'] = { ImageRectOffset = Vector2.new(49, 413), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/navigation/pushRight'] = { ImageRectOffset = Vector2.new(172, 49), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/navigation/pushRight_small'] = { ImageRectOffset = Vector2.new(152, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/navigation/swipe'] = { ImageRectOffset = Vector2.new(49, 450), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/navigation/swipeDown'] = { ImageRectOffset = Vector2.new(98, 49), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/navigation/swipeUp'] = { ImageRectOffset = Vector2.new(135, 49), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/placeholder/placeholderOff'] = { ImageRectOffset = Vector2.new(437, 290), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/placeholder/placeholderOn'] = { ImageRectOffset = Vector2.new(208, 247), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, + ['icons/placeholder/placeholderOn_small'] = { ImageRectOffset = Vector2.new(440, 176), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_1' }, + ['icons/status/alert'] = { ImageRectOffset = Vector2.new(246, 49), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/alert_small'] = { ImageRectOffset = Vector2.new(172, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/error_large'] = { ImageRectOffset = Vector2.new(0, 196), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/status/games/people-playing_large'] = { ImageRectOffset = Vector2.new(0, 343), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/status/games/people-playing_small'] = { ImageRectOffset = Vector2.new(394, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/games/rating_large'] = { ImageRectOffset = Vector2.new(0, 294), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/status/games/rating_small'] = { ImageRectOffset = Vector2.new(374, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/games/sessions_large'] = { ImageRectOffset = Vector2.new(0, 245), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/status/games/sessions_small'] = { ImageRectOffset = Vector2.new(357, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/imageunavailable'] = { ImageRectOffset = Vector2.new(0, 98), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/status/imageunavailable_small'] = { ImageRectOffset = Vector2.new(193, 487), ImageRectSize = Vector2.new(24, 24), ImageSet = 'img_set_1x_1' }, + ['icons/status/item/bundle'] = { ImageRectOffset = Vector2.new(300, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/item/limited'] = { ImageRectOffset = Vector2.new(320, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/item/owned'] = { ImageRectOffset = Vector2.new(283, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/noconnection'] = { ImageRectOffset = Vector2.new(283, 49), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/noconnection_large'] = { ImageRectOffset = Vector2.new(0, 392), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/status/oof_xlarge'] = { ImageRectOffset = Vector2.new(247, 387), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_1x_1' }, + ['icons/status/pending_small'] = { ImageRectOffset = Vector2.new(209, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/player/admin'] = { ImageRectOffset = Vector2.new(0, 170), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_3' }, + ['icons/status/player/developer'] = { ImageRectOffset = Vector2.new(485, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/player/following'] = { ImageRectOffset = Vector2.new(411, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/player/friend'] = { ImageRectOffset = Vector2.new(468, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/player/intern'] = { ImageRectOffset = Vector2.new(431, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/player/pending'] = { ImageRectOffset = Vector2.new(0, 153), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_3' }, + ['icons/status/player/videostar'] = { ImageRectOffset = Vector2.new(448, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/premium'] = { ImageRectOffset = Vector2.new(468, 49), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/premium_large'] = { ImageRectOffset = Vector2.new(0, 147), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_1x_2' }, + ['icons/status/premium_small'] = { ImageRectOffset = Vector2.new(263, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/private'] = { ImageRectOffset = Vector2.new(357, 49), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/private_small'] = { ImageRectOffset = Vector2.new(189, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/public'] = { ImageRectOffset = Vector2.new(320, 49), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/public_small'] = { ImageRectOffset = Vector2.new(337, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/success'] = { ImageRectOffset = Vector2.new(394, 49), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/success_small'] = { ImageRectOffset = Vector2.new(246, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/unavailable'] = { ImageRectOffset = Vector2.new(431, 49), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/unavailable_small'] = { ImageRectOffset = Vector2.new(226, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['icons/status/warning'] = { ImageRectOffset = Vector2.new(246, 49), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['icons/status/warning_small'] = { ImageRectOffset = Vector2.new(172, 493), ImageRectSize = Vector2.new(16, 16), ImageSet = 'img_set_1x_2' }, + ['squircles/fill'] = { ImageRectOffset = Vector2.new(274, 484), ImageRectSize = Vector2.new(26, 26), ImageSet = 'img_set_1x_1' }, + ['squircles/hollow'] = { ImageRectOffset = Vector2.new(247, 484), ImageRectSize = Vector2.new(26, 26), ImageSet = 'img_set_1x_1' }, + ['squircles/hollowBold'] = { ImageRectOffset = Vector2.new(31, 477), ImageRectSize = Vector2.new(30, 30), ImageSet = 'img_set_1x_1' }, + ['truncate_arrows/actions_truncationCollapse'] = { ImageRectOffset = Vector2.new(468, 308), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_2' }, + ['truncate_arrows/actions_truncationExpand'] = { ImageRectOffset = Vector2.new(474, 290), ImageRectSize = Vector2.new(36, 36), ImageSet = 'img_set_1x_1' }, +} end + +local assets_3x = nil +function make_assets_3x() assets_3x = { + ['chat_bubble/chat-bubble'] = { ImageRectOffset = Vector2.new(405, 957), ImageRectSize = Vector2.new(63, 63), ImageSet = 'img_set_3x_1' }, + ['chat_bubble/chat-bubble-self'] = { ImageRectOffset = Vector2.new(277, 957), ImageRectSize = Vector2.new(63, 63), ImageSet = 'img_set_3x_1' }, + ['chat_bubble/chat-bubble-self-tip'] = { ImageRectOffset = Vector2.new(963, 391), ImageRectSize = Vector2.new(18, 18), ImageSet = 'img_set_3x_1' }, + ['chat_bubble/chat-bubble-self2'] = { ImageRectOffset = Vector2.new(341, 957), ImageRectSize = Vector2.new(63, 63), ImageSet = 'img_set_3x_1' }, + ['chat_bubble/chat-bubble-tip'] = { ImageRectOffset = Vector2.new(944, 391), ImageRectSize = Vector2.new(18, 18), ImageSet = 'img_set_3x_1' }, + ['chat_bubble/chat-bubble2'] = { ImageRectOffset = Vector2.new(195, 957), ImageRectSize = Vector2.new(63, 63), ImageSet = 'img_set_3x_1' }, + ['component_assets/bulletDown_17_stroke_3'] = { ImageRectOffset = Vector2.new(218, 758), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_5' }, + ['component_assets/bulletLeft_17_stroke_3'] = { ImageRectOffset = Vector2.new(270, 706), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_5' }, + ['component_assets/bulletRight_17_stroke_3'] = { ImageRectOffset = Vector2.new(791, 956), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_1' }, + ['component_assets/bulletUp_17_stroke_3'] = { ImageRectOffset = Vector2.new(270, 654), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_5' }, + ['component_assets/bullet_17'] = { ImageRectOffset = Vector2.new(218, 654), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_5' }, + ['component_assets/circle_16'] = { ImageRectOffset = Vector2.new(975, 145), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_2' }, + ['component_assets/circle_17'] = { ImageRectOffset = Vector2.new(947, 149), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_17_mask'] = { ImageRectOffset = Vector2.new(218, 706), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_5' }, + ['component_assets/circle_17_stroke_1'] = { ImageRectOffset = Vector2.new(739, 956), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_17_stroke_3'] = { ImageRectOffset = Vector2.new(843, 956), ImageRectSize = Vector2.new(51, 51), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_21'] = { ImageRectOffset = Vector2.new(67, 957), ImageRectSize = Vector2.new(63, 63), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_21_stroke_1'] = { ImageRectOffset = Vector2.new(131, 957), ImageRectSize = Vector2.new(63, 63), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_22_stroke_3'] = { ImageRectOffset = Vector2.new(0, 957), ImageRectSize = Vector2.new(66, 66), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_24_stroke_1'] = { ImageRectOffset = Vector2.new(920, 939), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_25'] = { ImageRectOffset = Vector2.new(947, 0), ImageRectSize = Vector2.new(75, 75), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_26_stroke_3'] = { ImageRectOffset = Vector2.new(944, 208), ImageRectSize = Vector2.new(78, 78), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_28_padding_10'] = { ImageRectOffset = Vector2.new(866, 0), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['component_assets/circle_29'] = { ImageRectOffset = Vector2.new(920, 504), ImageRectSize = Vector2.new(87, 87), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_29_mask'] = { ImageRectOffset = Vector2.new(920, 594), ImageRectSize = Vector2.new(87, 87), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_29_stroke_1'] = { ImageRectOffset = Vector2.new(920, 682), ImageRectSize = Vector2.new(87, 87), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_30_stroke_3'] = { ImageRectOffset = Vector2.new(643, 848), ImageRectSize = Vector2.new(90, 90), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_36'] = { ImageRectOffset = Vector2.new(437, 873), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_2' }, + ['component_assets/circle_36_stroke_1'] = { ImageRectOffset = Vector2.new(866, 145), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_2' }, + ['component_assets/circle_42_stroke_3'] = { ImageRectOffset = Vector2.new(157, 866), ImageRectSize = Vector2.new(126, 126), ImageSet = 'img_set_3x_2' }, + ['component_assets/circle_49'] = { ImageRectOffset = Vector2.new(289, 577), ImageRectSize = Vector2.new(147, 147), ImageSet = 'img_set_3x_2' }, + ['component_assets/circle_49_mask'] = { ImageRectOffset = Vector2.new(289, 725), ImageRectSize = Vector2.new(147, 147), ImageSet = 'img_set_3x_2' }, + ['component_assets/circle_49_stroke_1'] = { ImageRectOffset = Vector2.new(289, 873), ImageRectSize = Vector2.new(147, 147), ImageSet = 'img_set_3x_2' }, + ['component_assets/circle_52_stroke_3'] = { ImageRectOffset = Vector2.new(0, 866), ImageRectSize = Vector2.new(156, 156), ImageSet = 'img_set_3x_2' }, + ['component_assets/circle_60_stroke_2'] = { ImageRectOffset = Vector2.new(739, 775), ImageRectSize = Vector2.new(180, 180), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_68_stroke_2'] = { ImageRectOffset = Vector2.new(739, 208), ImageRectSize = Vector2.new(204, 204), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_69_stroke_3'] = { ImageRectOffset = Vector2.new(739, 0), ImageRectSize = Vector2.new(207, 207), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_9'] = { ImageRectOffset = Vector2.new(993, 988), ImageRectSize = Vector2.new(27, 27), ImageSet = 'img_set_3x_1' }, + ['component_assets/circle_9_stroke_1'] = { ImageRectOffset = Vector2.new(206, 993), ImageRectSize = Vector2.new(27, 27), ImageSet = 'img_set_3x_2' }, + ['component_assets/dropshadow_16_20'] = { ImageRectOffset = Vector2.new(470, 848), ImageRectSize = Vector2.new(168, 168), ImageSet = 'img_set_3x_1' }, + ['component_assets/dropshadow_25'] = { ImageRectOffset = Vector2.new(437, 725), ImageRectSize = Vector2.new(111, 111), ImageSet = 'img_set_3x_2' }, + ['component_assets/dropshadow_28'] = { ImageRectOffset = Vector2.new(866, 289), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['component_assets/dropshadow_chatOff'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['component_assets/dropshadow_chatOn'] = { ImageRectOffset = Vector2.new(866, 434), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_2' }, + ['component_assets/dropshadow_more'] = { ImageRectOffset = Vector2.new(722, 867), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_2' }, + ['component_assets/halfcircleLeft_17'] = { ImageRectOffset = Vector2.new(999, 149), ImageRectSize = Vector2.new(24, 51), ImageSet = 'img_set_3x_1' }, + ['component_assets/halfcircleRight_17'] = { ImageRectOffset = Vector2.new(895, 956), ImageRectSize = Vector2.new(24, 51), ImageSet = 'img_set_3x_1' }, + ['component_assets/square_7_stroke_3'] = { ImageRectOffset = Vector2.new(993, 366), ImageRectSize = Vector2.new(21, 21), ImageSet = 'img_set_3x_1' }, + ['component_assets/triangleDown_16'] = { ImageRectOffset = Vector2.new(944, 366), ImageRectSize = Vector2.new(48, 24), ImageSet = 'img_set_3x_1' }, + ['component_assets/triangleLeft_16'] = { ImageRectOffset = Vector2.new(999, 860), ImageRectSize = Vector2.new(24, 48), ImageSet = 'img_set_3x_1' }, + ['component_assets/triangleRight_16'] = { ImageRectOffset = Vector2.new(993, 939), ImageRectSize = Vector2.new(24, 48), ImageSet = 'img_set_3x_1' }, + ['component_assets/triangleUp_16'] = { ImageRectOffset = Vector2.new(157, 993), ImageRectSize = Vector2.new(48, 24), ImageSet = 'img_set_3x_2' }, + ['component_assets/user_60_mask'] = { ImageRectOffset = Vector2.new(739, 594), ImageRectSize = Vector2.new(180, 180), ImageSet = 'img_set_3x_1' }, + ['component_assets/vignette_246'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(738, 738), ImageSet = 'img_set_3x_1' }, + ['gradient/gradient_0_100'] = { ImageRectOffset = Vector2.new(639, 848), ImageRectSize = Vector2.new(3, 120), ImageSet = 'img_set_3x_1' }, + ['icons/actions/accept'] = { ImageRectOffset = Vector2.new(686, 580), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/block'] = { ImageRectOffset = Vector2.new(327, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/actions/calendar'] = { ImageRectOffset = Vector2.new(218, 908), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/actions/compose'] = { ImageRectOffset = Vector2.new(0, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/cycleLeft'] = { ImageRectOffset = Vector2.new(654, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/cycleRight'] = { ImageRectOffset = Vector2.new(0, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/edit/add'] = { ImageRectOffset = Vector2.new(904, 798), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/edit/clear'] = { ImageRectOffset = Vector2.new(0, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/edit/clear_small'] = { ImageRectOffset = Vector2.new(218, 859), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/actions/edit/copy'] = { ImageRectOffset = Vector2.new(795, 907), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/edit/delete'] = { ImageRectOffset = Vector2.new(904, 689), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/edit/edit'] = { ImageRectOffset = Vector2.new(904, 907), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/edit/remove'] = { ImageRectOffset = Vector2.new(795, 798), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/favoriteOff'] = { ImageRectOffset = Vector2.new(795, 689), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/favoriteOn'] = { ImageRectOffset = Vector2.new(686, 689), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/feedback'] = { ImageRectOffset = Vector2.new(867, 435), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/filter'] = { ImageRectOffset = Vector2.new(0, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/friends/friendAdd'] = { ImageRectOffset = Vector2.new(577, 798), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/friends/friendInvite'] = { ImageRectOffset = Vector2.new(577, 907), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/friends/friendRemove'] = { ImageRectOffset = Vector2.new(577, 580), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/friends/friendsplaying'] = { ImageRectOffset = Vector2.new(577, 689), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/info'] = { ImageRectOffset = Vector2.new(0, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/info_small'] = { ImageRectOffset = Vector2.new(267, 908), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/actions/previewExpand'] = { ImageRectOffset = Vector2.new(435, 867), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/previewShrink'] = { ImageRectOffset = Vector2.new(904, 580), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/randomize'] = { ImageRectOffset = Vector2.new(686, 798), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/reject'] = { ImageRectOffset = Vector2.new(0, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/respawn'] = { ImageRectOffset = Vector2.new(0, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/selectOn'] = { ImageRectOffset = Vector2.new(0, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/selectOn_small'] = { ImageRectOffset = Vector2.new(267, 810), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/actions/send'] = { ImageRectOffset = Vector2.new(0, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/share'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/truncationCollapse'] = { ImageRectOffset = Vector2.new(109, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/truncationExpand'] = { ImageRectOffset = Vector2.new(795, 580), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/viewOff'] = { ImageRectOffset = Vector2.new(218, 810), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/actions/viewOn'] = { ImageRectOffset = Vector2.new(267, 859), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/actions/vote/voteDownOff'] = { ImageRectOffset = Vector2.new(218, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/vote/voteDownOn'] = { ImageRectOffset = Vector2.new(327, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/vote/voteUpOff'] = { ImageRectOffset = Vector2.new(545, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/vote/voteUpOn'] = { ImageRectOffset = Vector2.new(436, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/actions/zoomIn'] = { ImageRectOffset = Vector2.new(686, 907), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/actions/zoomOut'] = { ImageRectOffset = Vector2.new(435, 722), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/common/goldrobux'] = { ImageRectOffset = Vector2.new(763, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/common/goldrobux_small'] = { ImageRectOffset = Vector2.new(218, 957), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/common/more'] = { ImageRectOffset = Vector2.new(763, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/common/notificationOff'] = { ImageRectOffset = Vector2.new(654, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/common/notificationOn'] = { ImageRectOffset = Vector2.new(872, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/common/play'] = { ImageRectOffset = Vector2.new(872, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/common/refresh'] = { ImageRectOffset = Vector2.new(654, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/common/refresh_small'] = { ImageRectOffset = Vector2.new(975, 243), ImageRectSize = Vector2.new(42, 45), ImageSet = 'img_set_3x_2' }, + ['icons/common/robux'] = { ImageRectOffset = Vector2.new(763, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/common/robux_small'] = { ImageRectOffset = Vector2.new(327, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/common/search'] = { ImageRectOffset = Vector2.new(654, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/common/search_small'] = { ImageRectOffset = Vector2.new(267, 957), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/common/settings'] = { ImageRectOffset = Vector2.new(872, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/common/user'] = { ImageRectOffset = Vector2.new(872, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/common/user_60'] = { ImageRectOffset = Vector2.new(739, 413), ImageRectSize = Vector2.new(180, 180), ImageSet = 'img_set_3x_1' }, + ['icons/controls/close-ingame'] = { ImageRectOffset = Vector2.new(545, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/controls'] = { ImageRectOffset = Vector2.new(763, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/emoteOff'] = { ImageRectOffset = Vector2.new(920, 775), ImageRectSize = Vector2.new(84, 84), ImageSet = 'img_set_3x_1' }, + ['icons/controls/emoteOn'] = { ImageRectOffset = Vector2.new(643, 939), ImageRectSize = Vector2.new(84, 84), ImageSet = 'img_set_3x_1' }, + ['icons/controls/keys/alt'] = { ImageRectOffset = Vector2.new(654, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/apostrophe'] = { ImageRectOffset = Vector2.new(763, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/arrowDown'] = { ImageRectOffset = Vector2.new(425, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/controls/keys/arrowLeft'] = { ImageRectOffset = Vector2.new(523, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/controls/keys/arrowRight'] = { ImageRectOffset = Vector2.new(474, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/controls/keys/arrowUp'] = { ImageRectOffset = Vector2.new(376, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/controls/keys/asterisk'] = { ImageRectOffset = Vector2.new(436, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/backspace'] = { ImageRectOffset = Vector2.new(763, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/capslock'] = { ImageRectOffset = Vector2.new(872, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/caret'] = { ImageRectOffset = Vector2.new(545, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/comma'] = { ImageRectOffset = Vector2.new(436, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/command'] = { ImageRectOffset = Vector2.new(654, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/control'] = { ImageRectOffset = Vector2.new(763, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/dpadDown'] = { ImageRectOffset = Vector2.new(545, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/dpadLeft'] = { ImageRectOffset = Vector2.new(872, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/dpadRight'] = { ImageRectOffset = Vector2.new(218, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/dpadUp'] = { ImageRectOffset = Vector2.new(327, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/graveaccent'] = { ImageRectOffset = Vector2.new(763, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/key_single'] = { ImageRectOffset = Vector2.new(327, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/key_wide'] = { ImageRectOffset = Vector2.new(277, 848), ImageRectSize = Vector2.new(192, 108), ImageSet = 'img_set_3x_1' }, + ['icons/controls/keys/key_xwide'] = { ImageRectOffset = Vector2.new(0, 848), ImageRectSize = Vector2.new(276, 108), ImageSet = 'img_set_3x_1' }, + ['icons/controls/keys/option'] = { ImageRectOffset = Vector2.new(654, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/period'] = { ImageRectOffset = Vector2.new(872, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/return'] = { ImageRectOffset = Vector2.new(545, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/shift'] = { ImageRectOffset = Vector2.new(218, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/spacebar'] = { ImageRectOffset = Vector2.new(327, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/tab'] = { ImageRectOffset = Vector2.new(872, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxA'] = { ImageRectOffset = Vector2.new(218, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxB'] = { ImageRectOffset = Vector2.new(327, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxLS'] = { ImageRectOffset = Vector2.new(436, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxLSDirectional'] = { ImageRectOffset = Vector2.new(436, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxLSHorizontal'] = { ImageRectOffset = Vector2.new(545, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxLSVertical'] = { ImageRectOffset = Vector2.new(327, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxLT'] = { ImageRectOffset = Vector2.new(436, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxRB'] = { ImageRectOffset = Vector2.new(545, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxRS'] = { ImageRectOffset = Vector2.new(218, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxRSDirectional'] = { ImageRectOffset = Vector2.new(218, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxRSHorizontal'] = { ImageRectOffset = Vector2.new(327, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxRSVertical'] = { ImageRectOffset = Vector2.new(218, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxRT'] = { ImageRectOffset = Vector2.new(545, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxView'] = { ImageRectOffset = Vector2.new(327, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxX'] = { ImageRectOffset = Vector2.new(436, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxY'] = { ImageRectOffset = Vector2.new(218, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/keys/xboxmenu'] = { ImageRectOffset = Vector2.new(436, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/leaderboardOff'] = { ImageRectOffset = Vector2.new(654, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/leaderboardOn'] = { ImageRectOffset = Vector2.new(327, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/mouse/clickLeft'] = { ImageRectOffset = Vector2.new(654, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/mouse/clickRight'] = { ImageRectOffset = Vector2.new(545, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/mouse/scroll'] = { ImageRectOffset = Vector2.new(436, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/players'] = { ImageRectOffset = Vector2.new(654, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/screenrecord'] = { ImageRectOffset = Vector2.new(763, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/screenshot'] = { ImageRectOffset = Vector2.new(218, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/topmenu-shadow'] = { ImageRectOffset = Vector2.new(867, 145), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/controls/vehicle/backward'] = { ImageRectOffset = Vector2.new(109, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/vehicle/driver'] = { ImageRectOffset = Vector2.new(109, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/vehicle/exit'] = { ImageRectOffset = Vector2.new(109, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/vehicle/flip'] = { ImageRectOffset = Vector2.new(109, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/vehicle/forward'] = { ImageRectOffset = Vector2.new(109, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/vehicle/passenger'] = { ImageRectOffset = Vector2.new(109, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/weapon/fire'] = { ImageRectOffset = Vector2.new(109, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/weapon/scopeOff'] = { ImageRectOffset = Vector2.new(109, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/controls/weapon/scopeOn'] = { ImageRectOffset = Vector2.new(872, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/graphic/devices_xxlarge'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(576, 576), ImageSet = 'img_set_3x_2' }, + ['icons/graphic/error_xlarge'] = { ImageRectOffset = Vector2.new(577, 289), ImageRectSize = Vector2.new(288, 288), ImageSet = 'img_set_3x_2' }, + ['icons/graphic/loadingspinner'] = { ImageRectOffset = Vector2.new(577, 290), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/graphic/lock_xxlarge'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(576, 576), ImageSet = 'img_set_3x_3' }, + ['icons/graphic/premium_large'] = { ImageRectOffset = Vector2.new(722, 290), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/graphic/premium_xlarge'] = { ImageRectOffset = Vector2.new(577, 578), ImageRectSize = Vector2.new(288, 288), ImageSet = 'img_set_3x_2' }, + ['icons/graphic/success_xlarge'] = { ImageRectOffset = Vector2.new(577, 0), ImageRectSize = Vector2.new(288, 288), ImageSet = 'img_set_3x_2' }, + ['icons/imageUnavailable'] = { ImageRectOffset = Vector2.new(437, 577), ImageRectSize = Vector2.new(132, 132), ImageSet = 'img_set_3x_2' }, + ['icons/logo/block'] = { ImageRectOffset = Vector2.new(218, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/logo/letterform'] = { ImageRectOffset = Vector2.new(0, 739), ImageRectSize = Vector2.new(621, 108), ImageSet = 'img_set_3x_1' }, + ['icons/menu/about_large'] = { ImageRectOffset = Vector2.new(290, 867), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/menu/avatar_off'] = { ImageRectOffset = Vector2.new(109, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/avatar_on'] = { ImageRectOffset = Vector2.new(763, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/blog'] = { ImageRectOffset = Vector2.new(109, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/blog_large'] = { ImageRectOffset = Vector2.new(577, 145), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/menu/chat_off'] = { ImageRectOffset = Vector2.new(109, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/chat_on'] = { ImageRectOffset = Vector2.new(109, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/create_large'] = { ImageRectOffset = Vector2.new(722, 0), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/menu/customize'] = { ImageRectOffset = Vector2.new(327, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/customize_large'] = { ImageRectOffset = Vector2.new(145, 722), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/menu/events_large'] = { ImageRectOffset = Vector2.new(0, 577), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/menu/feed'] = { ImageRectOffset = Vector2.new(545, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/feed_large'] = { ImageRectOffset = Vector2.new(722, 145), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/menu/friends'] = { ImageRectOffset = Vector2.new(109, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/friends_large'] = { ImageRectOffset = Vector2.new(577, 0), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/menu/games_off'] = { ImageRectOffset = Vector2.new(218, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/games_on'] = { ImageRectOffset = Vector2.new(872, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/giftcard'] = { ImageRectOffset = Vector2.new(109, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/groups'] = { ImageRectOffset = Vector2.new(872, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/groups_large'] = { ImageRectOffset = Vector2.new(0, 722), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/menu/help_large'] = { ImageRectOffset = Vector2.new(290, 577), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/menu/home_off'] = { ImageRectOffset = Vector2.new(763, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/home_on'] = { ImageRectOffset = Vector2.new(218, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/inventory'] = { ImageRectOffset = Vector2.new(218, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/inventoryOff'] = { ImageRectOffset = Vector2.new(109, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/inventoryOn'] = { ImageRectOffset = Vector2.new(218, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/inventory_large'] = { ImageRectOffset = Vector2.new(145, 867), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/menu/messages_large'] = { ImageRectOffset = Vector2.new(145, 577), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/menu/more_off'] = { ImageRectOffset = Vector2.new(763, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/menu/more_on'] = { ImageRectOffset = Vector2.new(654, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/profile'] = { ImageRectOffset = Vector2.new(218, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/scanqr_large'] = { ImageRectOffset = Vector2.new(867, 0), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/menu/settings_large'] = { ImageRectOffset = Vector2.new(0, 867), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/menu/shop'] = { ImageRectOffset = Vector2.new(436, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/menu/shop_large'] = { ImageRectOffset = Vector2.new(290, 722), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/menu/trade'] = { ImageRectOffset = Vector2.new(109, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['icons/navigation/close'] = { ImageRectOffset = Vector2.new(0, 545), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/navigation/pushBack'] = { ImageRectOffset = Vector2.new(0, 436), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/navigation/pushRight'] = { ImageRectOffset = Vector2.new(0, 109), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/navigation/pushRight_small'] = { ImageRectOffset = Vector2.new(975, 194), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_2' }, + ['icons/navigation/swipe'] = { ImageRectOffset = Vector2.new(0, 327), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/navigation/swipeDown'] = { ImageRectOffset = Vector2.new(0, 218), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/navigation/swipeUp'] = { ImageRectOffset = Vector2.new(0, 654), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/placeholder/placeholderOff'] = { ImageRectOffset = Vector2.new(622, 739), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_1' }, + ['icons/placeholder/placeholderOn'] = { ImageRectOffset = Vector2.new(435, 577), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, + ['icons/placeholder/placeholderOn_small'] = { ImageRectOffset = Vector2.new(270, 758), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/alert'] = { ImageRectOffset = Vector2.new(109, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/alert_small'] = { ImageRectOffset = Vector2.new(719, 267), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/error_large'] = { ImageRectOffset = Vector2.new(866, 723), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/status/games/people-playing_large'] = { ImageRectOffset = Vector2.new(722, 435), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/status/games/people-playing_small'] = { ImageRectOffset = Vector2.new(425, 267), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/games/rating_large'] = { ImageRectOffset = Vector2.new(577, 435), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/status/games/rating_small'] = { ImageRectOffset = Vector2.new(327, 267), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/games/sessions_large'] = { ImageRectOffset = Vector2.new(866, 868), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/status/games/sessions_small'] = { ImageRectOffset = Vector2.new(376, 267), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/imageunavailable'] = { ImageRectOffset = Vector2.new(867, 290), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_3' }, + ['icons/status/imageunavailable_small'] = { ImageRectOffset = Vector2.new(947, 76), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_3x_1' }, + ['icons/status/item/bundle'] = { ImageRectOffset = Vector2.new(670, 267), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/item/limited'] = { ImageRectOffset = Vector2.new(572, 267), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/item/owned'] = { ImageRectOffset = Vector2.new(621, 267), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/noconnection'] = { ImageRectOffset = Vector2.new(0, 763), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/noconnection_large'] = { ImageRectOffset = Vector2.new(866, 578), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/status/oof_xlarge'] = { ImageRectOffset = Vector2.new(0, 577), ImageRectSize = Vector2.new(288, 288), ImageSet = 'img_set_3x_2' }, + ['icons/status/pending_small'] = { ImageRectOffset = Vector2.new(474, 267), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/player/admin'] = { ImageRectOffset = Vector2.new(768, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/player/developer'] = { ImageRectOffset = Vector2.new(866, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/player/following'] = { ImageRectOffset = Vector2.new(621, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/player/friend'] = { ImageRectOffset = Vector2.new(817, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/player/intern'] = { ImageRectOffset = Vector2.new(719, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/player/pending'] = { ImageRectOffset = Vector2.new(670, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/player/videostar'] = { ImageRectOffset = Vector2.new(915, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/premium'] = { ImageRectOffset = Vector2.new(436, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/premium_large'] = { ImageRectOffset = Vector2.new(577, 867), ImageRectSize = Vector2.new(144, 144), ImageSet = 'img_set_3x_2' }, + ['icons/status/premium_small'] = { ImageRectOffset = Vector2.new(975, 483), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_2' }, + ['icons/status/private'] = { ImageRectOffset = Vector2.new(0, 872), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/private_small'] = { ImageRectOffset = Vector2.new(572, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/public'] = { ImageRectOffset = Vector2.new(545, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/public_small'] = { ImageRectOffset = Vector2.new(964, 218), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/success'] = { ImageRectOffset = Vector2.new(218, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/success_small'] = { ImageRectOffset = Vector2.new(975, 434), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_2' }, + ['icons/status/unavailable'] = { ImageRectOffset = Vector2.new(327, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/unavailable_small'] = { ImageRectOffset = Vector2.new(523, 267), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['icons/status/warning'] = { ImageRectOffset = Vector2.new(109, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_4' }, + ['icons/status/warning_small'] = { ImageRectOffset = Vector2.new(719, 267), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_3x_5' }, + ['squircles/fill'] = { ImageRectOffset = Vector2.new(920, 860), ImageRectSize = Vector2.new(78, 78), ImageSet = 'img_set_3x_1' }, + ['squircles/hollow'] = { ImageRectOffset = Vector2.new(944, 287), ImageRectSize = Vector2.new(78, 78), ImageSet = 'img_set_3x_1' }, + ['squircles/hollowBold'] = { ImageRectOffset = Vector2.new(920, 413), ImageRectSize = Vector2.new(90, 90), ImageSet = 'img_set_3x_1' }, + ['truncate_arrows/actions_truncationCollapse'] = { ImageRectOffset = Vector2.new(109, 0), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_5' }, + ['truncate_arrows/actions_truncationExpand'] = { ImageRectOffset = Vector2.new(795, 580), ImageRectSize = Vector2.new(108, 108), ImageSet = 'img_set_3x_3' }, +} end + +local assets_2x = nil +function make_assets_2x() assets_2x = { + ['chat_bubble/chat-bubble'] = { ImageRectOffset = Vector2.new(460, 436), ImageRectSize = Vector2.new(42, 42), ImageSet = 'img_set_2x_2' }, + ['chat_bubble/chat-bubble-self'] = { ImageRectOffset = Vector2.new(86, 458), ImageRectSize = Vector2.new(42, 42), ImageSet = 'img_set_2x_3' }, + ['chat_bubble/chat-bubble-self-tip'] = { ImageRectOffset = Vector2.new(132, 493), ImageRectSize = Vector2.new(12, 12), ImageSet = 'img_set_2x_1' }, + ['chat_bubble/chat-bubble-self2'] = { ImageRectOffset = Vector2.new(185, 458), ImageRectSize = Vector2.new(42, 42), ImageSet = 'img_set_2x_3' }, + ['chat_bubble/chat-bubble-tip'] = { ImageRectOffset = Vector2.new(119, 493), ImageRectSize = Vector2.new(12, 12), ImageSet = 'img_set_2x_1' }, + ['chat_bubble/chat-bubble2'] = { ImageRectOffset = Vector2.new(129, 458), ImageRectSize = Vector2.new(42, 42), ImageSet = 'img_set_2x_3' }, + ['component_assets/bulletDown_17_stroke_3'] = { ImageRectOffset = Vector2.new(134, 471), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_4' }, + ['component_assets/bulletLeft_17_stroke_3'] = { ImageRectOffset = Vector2.new(99, 471), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_4' }, + ['component_assets/bulletRight_17_stroke_3'] = { ImageRectOffset = Vector2.new(298, 458), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_3' }, + ['component_assets/bulletUp_17_stroke_3'] = { ImageRectOffset = Vector2.new(474, 451), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_3' }, + ['component_assets/bullet_17'] = { ImageRectOffset = Vector2.new(228, 458), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_16'] = { ImageRectOffset = Vector2.new(460, 479), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_2' }, + ['component_assets/circle_17'] = { ImageRectOffset = Vector2.new(333, 458), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_17_mask'] = { ImageRectOffset = Vector2.new(459, 97), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_4' }, + ['component_assets/circle_17_stroke_1'] = { ImageRectOffset = Vector2.new(474, 416), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_17_stroke_3'] = { ImageRectOffset = Vector2.new(263, 458), ImageRectSize = Vector2.new(34, 34), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_21'] = { ImageRectOffset = Vector2.new(0, 458), ImageRectSize = Vector2.new(42, 42), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_21_stroke_1'] = { ImageRectOffset = Vector2.new(43, 458), ImageRectSize = Vector2.new(42, 42), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_22_stroke_3'] = { ImageRectOffset = Vector2.new(308, 458), ImageRectSize = Vector2.new(44, 44), ImageSet = 'img_set_2x_2' }, + ['component_assets/circle_24_stroke_1'] = { ImageRectOffset = Vector2.new(210, 458), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_2x_2' }, + ['component_assets/circle_25'] = { ImageRectOffset = Vector2.new(159, 458), ImageRectSize = Vector2.new(50, 50), ImageSet = 'img_set_2x_2' }, + ['component_assets/circle_26_stroke_3'] = { ImageRectOffset = Vector2.new(106, 458), ImageRectSize = Vector2.new(52, 52), ImageSet = 'img_set_2x_2' }, + ['component_assets/circle_28_padding_10'] = { ImageRectOffset = Vector2.new(193, 386), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['component_assets/circle_29'] = { ImageRectOffset = Vector2.new(0, 61), ImageRectSize = Vector2.new(58, 58), ImageSet = 'img_set_2x_9' }, + ['component_assets/circle_29_mask'] = { ImageRectOffset = Vector2.new(0, 179), ImageRectSize = Vector2.new(58, 58), ImageSet = 'img_set_2x_9' }, + ['component_assets/circle_29_stroke_1'] = { ImageRectOffset = Vector2.new(0, 120), ImageRectSize = Vector2.new(58, 58), ImageSet = 'img_set_2x_9' }, + ['component_assets/circle_30_stroke_3'] = { ImageRectOffset = Vector2.new(314, 385), ImageRectSize = Vector2.new(60, 60), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_36'] = { ImageRectOffset = Vector2.new(386, 97), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['component_assets/circle_36_stroke_1'] = { ImageRectOffset = Vector2.new(415, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_2' }, + ['component_assets/circle_42_stroke_3'] = { ImageRectOffset = Vector2.new(99, 386), ImageRectSize = Vector2.new(84, 84), ImageSet = 'img_set_2x_4' }, + ['component_assets/circle_49'] = { ImageRectOffset = Vector2.new(0, 386), ImageRectSize = Vector2.new(98, 98), ImageSet = 'img_set_2x_4' }, + ['component_assets/circle_49_mask'] = { ImageRectOffset = Vector2.new(385, 317), ImageRectSize = Vector2.new(98, 98), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_49_stroke_1'] = { ImageRectOffset = Vector2.new(385, 218), ImageRectSize = Vector2.new(98, 98), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_52_stroke_3'] = { ImageRectOffset = Vector2.new(385, 113), ImageRectSize = Vector2.new(104, 104), ImageSet = 'img_set_2x_3' }, + ['component_assets/circle_60_stroke_2'] = { ImageRectOffset = Vector2.new(385, 73), ImageRectSize = Vector2.new(120, 120), ImageSet = 'img_set_2x_2' }, + ['component_assets/circle_68_stroke_2'] = { ImageRectOffset = Vector2.new(0, 139), ImageRectSize = Vector2.new(136, 136), ImageSet = 'img_set_2x_5' }, + ['component_assets/circle_69_stroke_3'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(138, 138), ImageSet = 'img_set_2x_5' }, + ['component_assets/circle_9'] = { ImageRectOffset = Vector2.new(66, 493), ImageRectSize = Vector2.new(18, 18), ImageSet = 'img_set_2x_1' }, + ['component_assets/circle_9_stroke_1'] = { ImageRectOffset = Vector2.new(85, 493), ImageRectSize = Vector2.new(18, 18), ImageSet = 'img_set_2x_1' }, + ['component_assets/dropshadow_16_20'] = { ImageRectOffset = Vector2.new(385, 0), ImageRectSize = Vector2.new(112, 112), ImageSet = 'img_set_2x_3' }, + ['component_assets/dropshadow_25'] = { ImageRectOffset = Vector2.new(385, 436), ImageRectSize = Vector2.new(74, 74), ImageSet = 'img_set_2x_2' }, + ['component_assets/dropshadow_28'] = { ImageRectOffset = Vector2.new(386, 0), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['component_assets/dropshadow_chatOff'] = { ImageRectOffset = Vector2.new(0, 388), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['component_assets/dropshadow_chatOn'] = { ImageRectOffset = Vector2.new(290, 386), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_4' }, + ['component_assets/dropshadow_more'] = { ImageRectOffset = Vector2.new(97, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['component_assets/halfcircleLeft_17'] = { ImageRectOffset = Vector2.new(488, 35), ImageRectSize = Vector2.new(16, 34), ImageSet = 'img_set_2x_2' }, + ['component_assets/halfcircleRight_17'] = { ImageRectOffset = Vector2.new(488, 0), ImageRectSize = Vector2.new(16, 34), ImageSet = 'img_set_2x_2' }, + ['component_assets/square_7_stroke_3'] = { ImageRectOffset = Vector2.new(104, 493), ImageRectSize = Vector2.new(14, 14), ImageSet = 'img_set_2x_1' }, + ['component_assets/triangleDown_16'] = { ImageRectOffset = Vector2.new(0, 493), ImageRectSize = Vector2.new(32, 16), ImageSet = 'img_set_2x_1' }, + ['component_assets/triangleLeft_16'] = { ImageRectOffset = Vector2.new(493, 479), ImageRectSize = Vector2.new(16, 32), ImageSet = 'img_set_2x_2' }, + ['component_assets/triangleRight_16'] = { ImageRectOffset = Vector2.new(353, 458), ImageRectSize = Vector2.new(16, 32), ImageSet = 'img_set_2x_2' }, + ['component_assets/triangleUp_16'] = { ImageRectOffset = Vector2.new(33, 493), ImageRectSize = Vector2.new(32, 16), ImageSet = 'img_set_2x_1' }, + ['component_assets/user_60_mask'] = { ImageRectOffset = Vector2.new(385, 194), ImageRectSize = Vector2.new(120, 120), ImageSet = 'img_set_2x_2' }, + ['component_assets/vignette_246'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(492, 492), ImageSet = 'img_set_2x_1' }, + ['gradient/gradient_0_100'] = { ImageRectOffset = Vector2.new(493, 0), ImageRectSize = Vector2.new(2, 80), ImageSet = 'img_set_2x_1' }, + ['icons/actions/accept'] = { ImageRectOffset = Vector2.new(73, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/block'] = { ImageRectOffset = Vector2.new(97, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/actions/calendar'] = { ImageRectOffset = Vector2.new(97, 439), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_5' }, + ['icons/actions/compose'] = { ImageRectOffset = Vector2.new(146, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/cycleLeft'] = { ImageRectOffset = Vector2.new(219, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/cycleRight'] = { ImageRectOffset = Vector2.new(73, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/edit/add'] = { ImageRectOffset = Vector2.new(438, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/edit/clear'] = { ImageRectOffset = Vector2.new(73, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/edit/clear_small'] = { ImageRectOffset = Vector2.new(33, 470), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_5' }, + ['icons/actions/edit/copy'] = { ImageRectOffset = Vector2.new(292, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/edit/delete'] = { ImageRectOffset = Vector2.new(219, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/edit/edit'] = { ImageRectOffset = Vector2.new(365, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/edit/remove'] = { ImageRectOffset = Vector2.new(146, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/favoriteOff'] = { ImageRectOffset = Vector2.new(0, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/favoriteOn'] = { ImageRectOffset = Vector2.new(73, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/feedback'] = { ImageRectOffset = Vector2.new(430, 194), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/filter'] = { ImageRectOffset = Vector2.new(438, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/friends/friendAdd'] = { ImageRectOffset = Vector2.new(0, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/friends/friendInvite'] = { ImageRectOffset = Vector2.new(0, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/friends/friendRemove'] = { ImageRectOffset = Vector2.new(430, 388), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/friends/friendsplaying'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/info'] = { ImageRectOffset = Vector2.new(73, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/info_small'] = { ImageRectOffset = Vector2.new(97, 406), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_5' }, + ['icons/actions/previewExpand'] = { ImageRectOffset = Vector2.new(430, 291), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/actions/previewShrink'] = { ImageRectOffset = Vector2.new(73, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/randomize'] = { ImageRectOffset = Vector2.new(0, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/reject'] = { ImageRectOffset = Vector2.new(73, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/respawn'] = { ImageRectOffset = Vector2.new(146, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/selectOn'] = { ImageRectOffset = Vector2.new(365, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/selectOn_small'] = { ImageRectOffset = Vector2.new(97, 309), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_5' }, + ['icons/actions/send'] = { ImageRectOffset = Vector2.new(146, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/share'] = { ImageRectOffset = Vector2.new(146, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/truncationCollapse'] = { ImageRectOffset = Vector2.new(292, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/truncationExpand'] = { ImageRectOffset = Vector2.new(73, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/viewOff'] = { ImageRectOffset = Vector2.new(0, 470), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_5' }, + ['icons/actions/viewOn'] = { ImageRectOffset = Vector2.new(97, 373), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_5' }, + ['icons/actions/vote/voteDownOff'] = { ImageRectOffset = Vector2.new(146, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/vote/voteDownOn'] = { ImageRectOffset = Vector2.new(146, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/vote/voteUpOff'] = { ImageRectOffset = Vector2.new(219, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/vote/voteUpOn'] = { ImageRectOffset = Vector2.new(292, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/zoomIn'] = { ImageRectOffset = Vector2.new(0, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/actions/zoomOut'] = { ImageRectOffset = Vector2.new(0, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/common/goldrobux'] = { ImageRectOffset = Vector2.new(365, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/common/goldrobux_small'] = { ImageRectOffset = Vector2.new(97, 472), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_5' }, + ['icons/common/more'] = { ImageRectOffset = Vector2.new(292, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/common/notificationOff'] = { ImageRectOffset = Vector2.new(365, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/common/notificationOn'] = { ImageRectOffset = Vector2.new(292, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/common/play'] = { ImageRectOffset = Vector2.new(438, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/common/refresh'] = { ImageRectOffset = Vector2.new(292, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/common/refresh_small'] = { ImageRectOffset = Vector2.new(483, 0), ImageRectSize = Vector2.new(28, 30), ImageSet = 'img_set_2x_4' }, + ['icons/common/robux'] = { ImageRectOffset = Vector2.new(292, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/common/robux_small'] = { ImageRectOffset = Vector2.new(463, 461), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_5' }, + ['icons/common/search'] = { ImageRectOffset = Vector2.new(219, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/common/search_small'] = { ImageRectOffset = Vector2.new(430, 461), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_5' }, + ['icons/common/settings'] = { ImageRectOffset = Vector2.new(365, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/common/user'] = { ImageRectOffset = Vector2.new(438, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/common/user_60'] = { ImageRectOffset = Vector2.new(385, 315), ImageRectSize = Vector2.new(120, 120), ImageSet = 'img_set_2x_2' }, + ['icons/controls/close-ingame'] = { ImageRectOffset = Vector2.new(219, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/controls'] = { ImageRectOffset = Vector2.new(219, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/emoteOff'] = { ImageRectOffset = Vector2.new(0, 295), ImageRectSize = Vector2.new(56, 56), ImageSet = 'img_set_2x_9' }, + ['icons/controls/emoteOn'] = { ImageRectOffset = Vector2.new(0, 238), ImageRectSize = Vector2.new(56, 56), ImageSet = 'img_set_2x_9' }, + ['icons/controls/keys/alt'] = { ImageRectOffset = Vector2.new(73, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/apostrophe'] = { ImageRectOffset = Vector2.new(73, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/arrowDown'] = { ImageRectOffset = Vector2.new(0, 418), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_9' }, + ['icons/controls/keys/arrowLeft'] = { ImageRectOffset = Vector2.new(0, 352), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_9' }, + ['icons/controls/keys/arrowRight'] = { ImageRectOffset = Vector2.new(0, 385), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_9' }, + ['icons/controls/keys/arrowUp'] = { ImageRectOffset = Vector2.new(0, 451), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_9' }, + ['icons/controls/keys/asterisk'] = { ImageRectOffset = Vector2.new(73, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/backspace'] = { ImageRectOffset = Vector2.new(292, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/capslock'] = { ImageRectOffset = Vector2.new(0, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/caret'] = { ImageRectOffset = Vector2.new(146, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/comma'] = { ImageRectOffset = Vector2.new(365, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/command'] = { ImageRectOffset = Vector2.new(73, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/control'] = { ImageRectOffset = Vector2.new(0, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/dpadDown'] = { ImageRectOffset = Vector2.new(438, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/dpadLeft'] = { ImageRectOffset = Vector2.new(219, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/dpadRight'] = { ImageRectOffset = Vector2.new(292, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/dpadUp'] = { ImageRectOffset = Vector2.new(438, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/graveaccent'] = { ImageRectOffset = Vector2.new(0, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/key_single'] = { ImageRectOffset = Vector2.new(146, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/key_wide'] = { ImageRectOffset = Vector2.new(185, 385), ImageRectSize = Vector2.new(128, 72), ImageSet = 'img_set_2x_3' }, + ['icons/controls/keys/key_xwide'] = { ImageRectOffset = Vector2.new(0, 385), ImageRectSize = Vector2.new(184, 72), ImageSet = 'img_set_2x_3' }, + ['icons/controls/keys/option'] = { ImageRectOffset = Vector2.new(365, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/period'] = { ImageRectOffset = Vector2.new(0, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/return'] = { ImageRectOffset = Vector2.new(0, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/shift'] = { ImageRectOffset = Vector2.new(389, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/spacebar'] = { ImageRectOffset = Vector2.new(0, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/tab'] = { ImageRectOffset = Vector2.new(73, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/xboxA'] = { ImageRectOffset = Vector2.new(316, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxB'] = { ImageRectOffset = Vector2.new(146, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/xboxLS'] = { ImageRectOffset = Vector2.new(146, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/xboxLSDirectional'] = { ImageRectOffset = Vector2.new(438, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/xboxLSHorizontal'] = { ImageRectOffset = Vector2.new(146, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/xboxLSVertical'] = { ImageRectOffset = Vector2.new(292, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/xboxLT'] = { ImageRectOffset = Vector2.new(219, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/xboxRB'] = { ImageRectOffset = Vector2.new(146, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/xboxRS'] = { ImageRectOffset = Vector2.new(316, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxRSDirectional'] = { ImageRectOffset = Vector2.new(389, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxRSHorizontal'] = { ImageRectOffset = Vector2.new(365, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/xboxRSVertical'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/xboxRT'] = { ImageRectOffset = Vector2.new(146, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/xboxView'] = { ImageRectOffset = Vector2.new(219, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/xboxX'] = { ImageRectOffset = Vector2.new(73, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/keys/xboxY'] = { ImageRectOffset = Vector2.new(389, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/keys/xboxmenu'] = { ImageRectOffset = Vector2.new(73, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/leaderboardOff'] = { ImageRectOffset = Vector2.new(219, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/controls/leaderboardOn'] = { ImageRectOffset = Vector2.new(316, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/mouse/clickLeft'] = { ImageRectOffset = Vector2.new(243, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/mouse/clickRight'] = { ImageRectOffset = Vector2.new(243, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/mouse/scroll'] = { ImageRectOffset = Vector2.new(243, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/players'] = { ImageRectOffset = Vector2.new(316, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/screenrecord'] = { ImageRectOffset = Vector2.new(243, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/screenshot'] = { ImageRectOffset = Vector2.new(389, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/topmenu-shadow'] = { ImageRectOffset = Vector2.new(236, 388), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/controls/vehicle/backward'] = { ImageRectOffset = Vector2.new(170, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/vehicle/driver'] = { ImageRectOffset = Vector2.new(170, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/vehicle/exit'] = { ImageRectOffset = Vector2.new(170, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/vehicle/flip'] = { ImageRectOffset = Vector2.new(316, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/vehicle/forward'] = { ImageRectOffset = Vector2.new(170, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/vehicle/passenger'] = { ImageRectOffset = Vector2.new(389, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/weapon/fire'] = { ImageRectOffset = Vector2.new(243, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/weapon/scopeOff'] = { ImageRectOffset = Vector2.new(97, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/controls/weapon/scopeOn'] = { ImageRectOffset = Vector2.new(170, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/graphic/devices_xxlarge'] = { ImageRectOffset = Vector2.new(0, 73), ImageRectSize = Vector2.new(384, 384), ImageSet = 'img_set_2x_2' }, + ['icons/graphic/error_xlarge'] = { ImageRectOffset = Vector2.new(193, 0), ImageRectSize = Vector2.new(192, 192), ImageSet = 'img_set_2x_4' }, + ['icons/graphic/loadingspinner'] = { ImageRectOffset = Vector2.new(333, 291), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/graphic/lock_xxlarge'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(384, 384), ImageSet = 'img_set_2x_3' }, + ['icons/graphic/premium_large'] = { ImageRectOffset = Vector2.new(333, 388), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/graphic/premium_xlarge'] = { ImageRectOffset = Vector2.new(193, 193), ImageRectSize = Vector2.new(192, 192), ImageSet = 'img_set_2x_4' }, + ['icons/graphic/success_xlarge'] = { ImageRectOffset = Vector2.new(0, 193), ImageRectSize = Vector2.new(192, 192), ImageSet = 'img_set_2x_4' }, + ['icons/imageUnavailable'] = { ImageRectOffset = Vector2.new(385, 416), ImageRectSize = Vector2.new(88, 88), ImageSet = 'img_set_2x_3' }, + ['icons/logo/block'] = { ImageRectOffset = Vector2.new(365, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/logo/letterform'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(414, 72), ImageSet = 'img_set_2x_2' }, + ['icons/menu/about_large'] = { ImageRectOffset = Vector2.new(236, 291), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/menu/avatar_off'] = { ImageRectOffset = Vector2.new(292, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/avatar_on'] = { ImageRectOffset = Vector2.new(219, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/blog'] = { ImageRectOffset = Vector2.new(292, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/blog_large'] = { ImageRectOffset = Vector2.new(139, 291), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/menu/chat_off'] = { ImageRectOffset = Vector2.new(438, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/chat_on'] = { ImageRectOffset = Vector2.new(219, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/create_large'] = { ImageRectOffset = Vector2.new(236, 194), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/menu/customize'] = { ImageRectOffset = Vector2.new(365, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/customize_large'] = { ImageRectOffset = Vector2.new(236, 97), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/menu/events_large'] = { ImageRectOffset = Vector2.new(0, 276), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/menu/feed'] = { ImageRectOffset = Vector2.new(438, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/feed_large'] = { ImageRectOffset = Vector2.new(139, 194), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/menu/friends'] = { ImageRectOffset = Vector2.new(219, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/friends_large'] = { ImageRectOffset = Vector2.new(333, 194), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/menu/games_off'] = { ImageRectOffset = Vector2.new(438, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/menu/games_on'] = { ImageRectOffset = Vector2.new(438, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/menu/giftcard'] = { ImageRectOffset = Vector2.new(438, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/groups'] = { ImageRectOffset = Vector2.new(219, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/groups_large'] = { ImageRectOffset = Vector2.new(236, 0), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/menu/help_large'] = { ImageRectOffset = Vector2.new(333, 97), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/menu/home_off'] = { ImageRectOffset = Vector2.new(292, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/home_on'] = { ImageRectOffset = Vector2.new(438, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/inventory'] = { ImageRectOffset = Vector2.new(292, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/inventoryOff'] = { ImageRectOffset = Vector2.new(365, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/inventoryOn'] = { ImageRectOffset = Vector2.new(292, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/inventory_large'] = { ImageRectOffset = Vector2.new(139, 97), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/menu/messages_large'] = { ImageRectOffset = Vector2.new(0, 373), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/menu/more_off'] = { ImageRectOffset = Vector2.new(292, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_8' }, + ['icons/menu/more_on'] = { ImageRectOffset = Vector2.new(365, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/profile'] = { ImageRectOffset = Vector2.new(438, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/scanqr_large'] = { ImageRectOffset = Vector2.new(139, 388), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/menu/settings_large'] = { ImageRectOffset = Vector2.new(139, 0), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/menu/shop'] = { ImageRectOffset = Vector2.new(365, 438), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/menu/shop_large'] = { ImageRectOffset = Vector2.new(333, 0), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_5' }, + ['icons/menu/trade'] = { ImageRectOffset = Vector2.new(365, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['icons/navigation/close'] = { ImageRectOffset = Vector2.new(389, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/navigation/pushBack'] = { ImageRectOffset = Vector2.new(316, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/navigation/pushRight'] = { ImageRectOffset = Vector2.new(170, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/navigation/pushRight_small'] = { ImageRectOffset = Vector2.new(459, 132), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_4' }, + ['icons/navigation/swipe'] = { ImageRectOffset = Vector2.new(97, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/navigation/swipeDown'] = { ImageRectOffset = Vector2.new(170, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/navigation/swipeUp'] = { ImageRectOffset = Vector2.new(243, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/placeholder/placeholderOff'] = { ImageRectOffset = Vector2.new(430, 0), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/placeholder/placeholderOn'] = { ImageRectOffset = Vector2.new(430, 97), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_5' }, + ['icons/placeholder/placeholderOn_small'] = { ImageRectOffset = Vector2.new(97, 276), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_5' }, + ['icons/status/alert'] = { ImageRectOffset = Vector2.new(316, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/status/alert_small'] = { ImageRectOffset = Vector2.new(290, 459), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_4' }, + ['icons/status/error_large'] = { ImageRectOffset = Vector2.new(386, 290), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['icons/status/games/people-playing_large'] = { ImageRectOffset = Vector2.new(0, 97), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_6' }, + ['icons/status/games/people-playing_small'] = { ImageRectOffset = Vector2.new(462, 252), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/games/rating_large'] = { ImageRectOffset = Vector2.new(0, 291), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_6' }, + ['icons/status/games/rating_small'] = { ImageRectOffset = Vector2.new(462, 292), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/games/sessions_large'] = { ImageRectOffset = Vector2.new(0, 194), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_6' }, + ['icons/status/games/sessions_small'] = { ImageRectOffset = Vector2.new(462, 219), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/imageunavailable'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_6' }, + ['icons/status/imageunavailable_small'] = { ImageRectOffset = Vector2.new(259, 458), ImageRectSize = Vector2.new(48, 48), ImageSet = 'img_set_2x_2' }, + ['icons/status/item/bundle'] = { ImageRectOffset = Vector2.new(462, 33), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/item/limited'] = { ImageRectOffset = Vector2.new(462, 106), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/item/owned'] = { ImageRectOffset = Vector2.new(462, 73), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/noconnection'] = { ImageRectOffset = Vector2.new(243, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/status/noconnection_large'] = { ImageRectOffset = Vector2.new(386, 387), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['icons/status/oof_xlarge'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(192, 192), ImageSet = 'img_set_2x_4' }, + ['icons/status/pending_small'] = { ImageRectOffset = Vector2.new(33, 461), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/player/admin'] = { ImageRectOffset = Vector2.new(462, 438), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/player/developer'] = { ImageRectOffset = Vector2.new(462, 398), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/player/following'] = { ImageRectOffset = Vector2.new(462, 471), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/player/friend'] = { ImageRectOffset = Vector2.new(462, 365), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/player/intern'] = { ImageRectOffset = Vector2.new(61, 0), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_9' }, + ['icons/status/player/pending'] = { ImageRectOffset = Vector2.new(94, 0), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_9' }, + ['icons/status/player/videostar'] = { ImageRectOffset = Vector2.new(462, 325), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/premium'] = { ImageRectOffset = Vector2.new(97, 292), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/status/premium_large'] = { ImageRectOffset = Vector2.new(386, 193), ImageRectSize = Vector2.new(96, 96), ImageSet = 'img_set_2x_4' }, + ['icons/status/premium_small'] = { ImageRectOffset = Vector2.new(323, 459), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_4' }, + ['icons/status/private'] = { ImageRectOffset = Vector2.new(97, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/status/private_small'] = { ImageRectOffset = Vector2.new(462, 146), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/public'] = { ImageRectOffset = Vector2.new(97, 219), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/status/public_small'] = { ImageRectOffset = Vector2.new(462, 179), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/success'] = { ImageRectOffset = Vector2.new(389, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/status/success_small'] = { ImageRectOffset = Vector2.new(0, 461), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/unavailable'] = { ImageRectOffset = Vector2.new(97, 365), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/status/unavailable_small'] = { ImageRectOffset = Vector2.new(462, 0), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_6' }, + ['icons/status/warning'] = { ImageRectOffset = Vector2.new(316, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_6' }, + ['icons/status/warning_small'] = { ImageRectOffset = Vector2.new(290, 459), ImageRectSize = Vector2.new(32, 32), ImageSet = 'img_set_2x_4' }, + ['squircles/fill'] = { ImageRectOffset = Vector2.new(0, 458), ImageRectSize = Vector2.new(52, 52), ImageSet = 'img_set_2x_2' }, + ['squircles/hollow'] = { ImageRectOffset = Vector2.new(53, 458), ImageRectSize = Vector2.new(52, 52), ImageSet = 'img_set_2x_2' }, + ['squircles/hollowBold'] = { ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(60, 60), ImageSet = 'img_set_2x_9' }, + ['truncate_arrows/actions_truncationCollapse'] = { ImageRectOffset = Vector2.new(292, 73), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, + ['truncate_arrows/actions_truncationExpand'] = { ImageRectOffset = Vector2.new(73, 146), ImageRectSize = Vector2.new(72, 72), ImageSet = 'img_set_2x_7' }, +} end + +return function(scaleType) + if scaleType > 2.5 then + if not assets_3x then make_assets_3x() end + return assets_3x, 3 + elseif scaleType > 1.5 then + if not assets_2x then make_assets_2x() end + return assets_2x, 2 + end + if not assets_1x then make_assets_1x() end + return assets_1x, 1 +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_1x_1.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_1x_1.png new file mode 100644 index 0000000..8823a11 Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_1x_1.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_1x_2.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_1x_2.png new file mode 100644 index 0000000..ae4034b Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_1x_2.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_1x_3.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_1x_3.png new file mode 100644 index 0000000..134e87b Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_1x_3.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_1.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_1.png new file mode 100644 index 0000000..1a3a468 Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_1.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_2.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_2.png new file mode 100644 index 0000000..12a9960 Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_2.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_3.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_3.png new file mode 100644 index 0000000..b8f2565 Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_3.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_4.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_4.png new file mode 100644 index 0000000..555f96a Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_4.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_5.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_5.png new file mode 100644 index 0000000..1a813e0 Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_5.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_6.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_6.png new file mode 100644 index 0000000..a502e3b Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_6.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_7.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_7.png new file mode 100644 index 0000000..decb135 Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_7.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_8.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_8.png new file mode 100644 index 0000000..ba6b4a4 Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_8.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_9.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_9.png new file mode 100644 index 0000000..d33f926 Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_2x_9.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_1.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_1.png new file mode 100644 index 0000000..f119855 Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_1.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_2.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_2.png new file mode 100644 index 0000000..e9ee2b6 Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_2.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_3.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_3.png new file mode 100644 index 0000000..589ec02 Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_3.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_4.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_4.png new file mode 100644 index 0000000..11bfc0e Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_4.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_5.png b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_5.png new file mode 100644 index 0000000..5879830 Binary files /dev/null and b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/ImageAtlas/img_set_3x_5.png differ diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Images.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Images.lua new file mode 100644 index 0000000..3d42151 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Images.lua @@ -0,0 +1,74 @@ +-- This file just provides a convenient interface to query for images +local GetImageSetData = require(script.Parent.GetImageSetData) + +local GuiService = game:GetService("GuiService") + +-- fallback spritesheet image to use if CorePackages is unavailable +local FALLBACK_IMAGES = { + ["img_set_1x_1"] = "http://www.roblox.com/asset/?id=6074324731", + ["img_set_1x_2"] = "http://www.roblox.com/asset/?id=6074325323", + ["img_set_1x_3"] = "http://www.roblox.com/asset/?id=6074325723", +} + +local CorePackages = script:FindFirstAncestor("CorePackages") +local success, scale = pcall(GuiService.GetResolutionScale, GuiService) + +if not success or not CorePackages then + scale = 1 +end + +local sourceData = GetImageSetData(scale) + +local function getPackagePath() + local packageRoot = script.Parent + + if CorePackages == nil then + -- We're not running in CI as a core script, no internal path + return nil + end + + local path = {} + local current = packageRoot + while current ~= nil and current ~= CorePackages do + table.insert(path, 1, current.Name) + current = current.Parent + end + + return "LuaPackages/" .. table.concat(path, "/") +end + +local function getImagePath(packagePath, imageName) + if packagePath == nil then + -- fallback to an uploaded image + return FALLBACK_IMAGES[imageName] + else + return string.format("rbxasset://%s/ImageAtlas/%s.png", packagePath, imageName) + end +end + +local packagePath = getPackagePath() +local Images = { + ImagesResolutionScale = scale, +} + +for key, value in pairs(sourceData) do + assert(typeof(value) == "table") + local imageProps = {} + for imageKey, imageValue in pairs(value) do + if imageKey == "ImageSet" then + imageProps.Image = getImagePath(packagePath, imageValue) + else + imageProps[imageKey] = imageValue + end + end + Images[key] = imageProps +end + +-- Attach a metamethod to guard against typos +setmetatable(Images, { + __index = function(_, key) + error(("%q is not a valid member of Images"):format(tostring(key)), 2) + end, +}) + +return Images diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Images.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Images.spec.lua new file mode 100644 index 0000000..148fb1d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/Images.spec.lua @@ -0,0 +1,9 @@ +return function() + local Images = require(script.Parent.Images) + + it("should throw on invalid key", function() + expect(function() + local _ = Images["never a real key"] + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSize.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSize.lua new file mode 100644 index 0000000..ada3fa3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSize.lua @@ -0,0 +1,16 @@ +local ImageSet = script.Parent + +local IconSize = require(ImageSet.Enum.IconSize) + +local IconSizeMap = { + [IconSize.Small] = 16, + [IconSize.Medium] = 36, + [IconSize.Large] = 48, + [IconSize.XLarge] = 96, + [IconSize.XXLarge] = 192, +} + +return function(iconSizeEnum) + assert(IconSize.isEnumValue(iconSizeEnum)) + return IconSizeMap[iconSizeEnum] +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSize.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSize.spec.lua new file mode 100644 index 0000000..70656cd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSize.spec.lua @@ -0,0 +1,33 @@ +local ImageSet = script.Parent +local IconSizeEnum = require(ImageSet.Enum.IconSize) + +return function() + local getIconSize = require(script.Parent.getIconSize) + describe("getIconSize()", function() + it("should return the number 16", function() + local iconSize = getIconSize(IconSizeEnum.Small) + expect(iconSize).to.equal(16) + end) + it("should return the number 36", function() + local iconSize = getIconSize(IconSizeEnum.Medium) + expect(iconSize).to.equal(36) + end) + it("should return the number 48", function() + local iconSize = getIconSize(IconSizeEnum.Large) + expect(iconSize).to.equal(48) + end) + it("should return the number 96", function() + local iconSize = getIconSize(IconSizeEnum.XLarge) + expect(iconSize).to.equal(96) + end) + it("should return the number 192", function() + local iconSize = getIconSize(IconSizeEnum.XXLarge) + expect(iconSize).to.equal(192) + end) + it("should error", function() + expect(function() + return getIconSize("ASD") + end).to.throw() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSizeUDim2.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSizeUDim2.lua new file mode 100644 index 0000000..3489a0b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSizeUDim2.lua @@ -0,0 +1,10 @@ +local ImageSet = script.Parent + +local IconSize = require(ImageSet.Enum.IconSize) +local getIconSize = require(ImageSet.getIconSize) + +return function(iconSizeEnum) + assert(IconSize.isEnumValue(iconSizeEnum)) + local size = getIconSize(iconSizeEnum) + return UDim2.fromOffset(size, size) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSizeUDim2.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSizeUDim2.spec.lua new file mode 100644 index 0000000..5abf687 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/getIconSizeUDim2.spec.lua @@ -0,0 +1,34 @@ +local ImageSet = script.Parent +local IconSizeEnum = require(ImageSet.Enum.IconSize) + +return function() + local getIconSizeUDim2 = require(ImageSet.getIconSizeUDim2) + describe("getIconSizeUDim2()", function() + it("should return a UDim2 of value (0, 16, 0, 16)", function() + local iconSize = getIconSizeUDim2(IconSizeEnum.Small) + expect(iconSize).to.equal(UDim2.fromOffset(16, 16)) + end) + it("should return a UDim2 of value (0, 36, 0, 36)", function() + local iconSize = getIconSizeUDim2(IconSizeEnum.Medium) + expect(iconSize).to.equal(UDim2.fromOffset(36, 36)) + end) + it("should return a UDim2 of value (0, 48, 0, 48)", function() + local iconSize = getIconSizeUDim2(IconSizeEnum.Large) + expect(iconSize).to.equal(UDim2.fromOffset(48, 48)) + end) + it("should return a UDim2 of value (0, 96, 0, 96)", function() + local iconSize = getIconSizeUDim2(IconSizeEnum.XLarge) + expect(iconSize).to.equal(UDim2.fromOffset(96, 96)) + end) + it("should return a UDim2 of value (0, 192, 0, 192)", function() + local iconSize = getIconSizeUDim2(IconSizeEnum.XXLarge) + expect(iconSize).to.equal(UDim2.fromOffset(192, 192)) + end) + + it("should error", function() + expect(function() + return getIconSizeUDim2("ASD") + end).to.throw() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/scaleSliceToResolution.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/scaleSliceToResolution.lua new file mode 100644 index 0000000..76f2cd3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/scaleSliceToResolution.lua @@ -0,0 +1,20 @@ +--[[ + Takes in a table of ImageLabel properties and returns those properties + with the SliceCenter and SliceScale scaled based on the provided resolution scale + + imageSetProps: a table of ImageLabel properties + scale: a number representing the resolution scale of the user's device +]] + +return function(imageSetProps, scale) + local scaledProps = imageSetProps + + if scaledProps.SliceCenter then + local min = scaledProps.SliceCenter.Min * scale + local max = scaledProps.SliceCenter.Max * scale + scaledProps.SliceCenter = Rect.new(min, max) + scaledProps.SliceScale = (scaledProps.SliceScale or 1) / scale + end + + return scaledProps +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/scaleSliceToResolution.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/scaleSliceToResolution.spec.lua new file mode 100644 index 0000000..a0c543f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/ImageSet/scaleSliceToResolution.spec.lua @@ -0,0 +1,59 @@ +return function() + local scaleSliceToResolution = require(script.Parent.scaleSliceToResolution) + + describe("Scale to Device Resolution", function() + describe("With regular props", function() + it("Should return the props unchanged", function() + local size = UDim2.new(1,0,1,0) + local image = "example-rbxasset-string" + local scale = 1 + + local imageProps = { + Size = size, + Image = image, + } + + local result = scaleSliceToResolution(imageProps, scale) + + expect(result.Size).to.equal(size) + expect(result.Image).to.equal(image) + end) + end) + + describe("With SliceCenter", function() + it("Should scale SliceCenter based on the provided scale", function() + local SliceCenter = Rect.new(10, 10, 11, 11) + local scale = 2 + + local imageProps = { + SliceCenter = SliceCenter, + } + + local result = scaleSliceToResolution(imageProps, scale) + + expect(result.SliceCenter.Min).to.equal(SliceCenter.Min * scale) + expect(result.SliceCenter.Max).to.equal(SliceCenter.Max * scale) + expect(result.SliceScale).to.be.near(1 / scale) + end) + end) + + describe("With SliceCenter and SliceScale", function() + it("Should scale SliceCenter and SliceScale based on the provided scale", function() + local SliceCenter = Rect.new(10, 10, 11, 11) + local SliceScale = 0.5 + local scale = 2 + + local imageProps = { + SliceCenter = SliceCenter, + SliceScale = SliceScale, + } + + local result = scaleSliceToResolution(imageProps, scale) + + expect(result.SliceCenter.Min).to.equal(SliceCenter.Min * scale) + expect(result.SliceCenter.Max).to.equal(SliceCenter.Max * scale) + expect(result.SliceScale).to.be.near(SliceScale / scale) + end) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/Badge.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/Badge.lua new file mode 100644 index 0000000..f47346d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/Badge.lua @@ -0,0 +1,135 @@ +local TextService = game:GetService("TextService") + +local Indicator = script.Parent +local App = Indicator.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local withStyle = require(UIBlox.Core.Style.withStyle) + +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) + +local Images = require(UIBlox.App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local divideTransparency = require(UIBlox.Utility.divideTransparency) + +local BADGE_MIN_WIDTH = 24 +local INNER_PADDING = 2 +local TEXT_PADDING = 5 +local SHADOW_SIZE_OFFSET = 6 + +local MAX_BADGE_VALUE = 99 +local MAX_BADGE_TEXT = "99+" +local MAX_TEXT_LENGTH = 4 + +local ELLIPSES = "..." + +local BACKGROUND_CIRCLE_IMAGE = Images["component_assets/circle_25"] +local INNER_CIRCLE_IMAGE = Images["component_assets/circle_21"] + +local Badge = Roact.PureComponent:extend("Badge") + +Badge.validateProps = t.strictInterface({ + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + + disabled = t.optional(t.boolean), + hasShadow = t.optional(t.boolean), + value = t.union(t.string, t.integer), +}) + +Badge.defaultProps = { + position = UDim2.new(0, 0, 0, 0), + anchorPoint = Vector2.new(0, 0), + + disabled = false, + hasShadow = false, +} + +function Badge:render() + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local font = stylePalette.Font + + local badgeText = tostring(self.props.value) + if t.number(self.props.value) and self.props.value > MAX_BADGE_VALUE then + badgeText = MAX_BADGE_TEXT + elseif t.string(self.props.value) and utf8.len(utf8.nfcnormalize(self.props.value)) > MAX_TEXT_LENGTH then + local byteOffset = utf8.offset(self.props.value, MAX_TEXT_LENGTH) - 1 + badgeText = string.sub(self.props.value, 1, byteOffset) .. ELLIPSES + end + + local baseSize = stylePalette.Font.BaseSize + local fontSize = font.CaptionBody.RelativeSize * baseSize + + local textBounds = TextService:GetTextSize(badgeText, fontSize, font.CaptionBody.Font, Vector2.new(10000, 10000)).X + local badgeWidth = textBounds + (TEXT_PADDING * 2) + (INNER_PADDING * 2) + if badgeWidth < BADGE_MIN_WIDTH then + badgeWidth = BADGE_MIN_WIDTH + end + + return Roact.createElement("Frame", { + Position = self.props.position, + AnchorPoint = self.props.anchorPoint, + BackgroundTransparency = 1, + Size = UDim2.fromOffset(badgeWidth, BADGE_MIN_WIDTH), + }, { + Shadow = self.props.hasShadow and Roact.createElement(ImageSetComponent.Label, { + ZIndex = 1, + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Size = UDim2.new(1, SHADOW_SIZE_OFFSET * 2, 1, SHADOW_SIZE_OFFSET * 2), + + Image = Images["component_assets/dropshadow_25"], + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(18, 18, 19, 19), + }), + + Background = Roact.createElement(ImageSetComponent.Label, { + ZIndex = 2, + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + + ImageColor3 = theme.BackgroundDefault.Color, + ImageTransparency = divideTransparency(theme.BackgroundDefault.Transparency, self.props.disabled and 2 or 1), + Image = BACKGROUND_CIRCLE_IMAGE, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(14, 14, 15, 15), + }), + + Inner = Roact.createElement(ImageSetComponent.Label, { + ZIndex = 3, + BackgroundTransparency = 1, + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.new(1, -(INNER_PADDING * 2), 1, -(INNER_PADDING * 2)), + + ImageColor3 = theme.Badge.Color, + ImageTransparency = divideTransparency(theme.Badge.Transparency, self.props.disabled and 2 or 1), + Image = INNER_CIRCLE_IMAGE, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(12, 12, 13, 13), + }, { + TextLabel = Roact.createElement(GenericTextLabel, { + fontStyle = font.CaptionBody, + colorStyle = theme.BadgeContent, + + BackgroundTransparency = 1, + Text = badgeText, + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.fromScale(1, 1), + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + }), + }), + }) + end) +end + +return Badge \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/Badge.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/Badge.spec.lua new file mode 100644 index 0000000..85450c9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/Badge.spec.lua @@ -0,0 +1,68 @@ +return function() + local Indicator = script.Parent + local App = Indicator.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local Badge = require(script.Parent.Badge) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + it("should mount and unmount with only the required props", function() + local element = mockStyleComponent({ + radioButton = Roact.createElement(Badge, { + value = 60, + }) + }) + local instance = Roact.mount(element, frame, "Badge") + Roact.unmount(instance) + end) + + it("should mount and unmount with all the props", function() + local element = mockStyleComponent({ + radioButton = Roact.createElement(Badge, { + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0, 1), + + disabled = true, + hasShadow = true, + + value = 60, + }) + }) + local instance = Roact.mount(element, frame, "Badge") + Roact.unmount(instance) + end) + + it("should accept string values", function() + local element = mockStyleComponent({ + radioButton = Roact.createElement(Badge, { + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0, 1), + + value = "New", + }) + }) + local instance = Roact.mount(element, frame, "Badge") + Roact.unmount(instance) + end) + + it("should display as 99+ for values above 99", function() + local element = mockStyleComponent({ + radioButton = Roact.createElement(Badge, { + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0, 1), + + value = 100, + }) + }) + local instance = Roact.mount(element, frame, "Badge") + + local textLabel = frame:FindFirstChild("TextLabel", true) + expect(textLabel.Text).to.equal("99+") + + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/EmptyState.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/EmptyState.lua new file mode 100644 index 0000000..4be73be --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/EmptyState.lua @@ -0,0 +1,131 @@ +local Indicator = script.Parent +local App = Indicator.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local validateImage = require(UIBlox.Core.ImageSet.Validator.validateImage) +local withStyle = require(UIBlox.Style.withStyle) +local getPageMargin = require(App.Container.getPageMargin) +local IconSize = require(App.ImageSet.Enum.IconSize) +local getIconSize = require(App.ImageSet.getIconSize) +local Images = require(App.ImageSet.Images) +local SecondaryButton = require(App.Button.SecondaryButton) + +local DEFAULT_ICON = 'icons/status/oof_xlarge' +local DEFAULT_BUTTON_ICON = 'icons/common/refresh' +local ICON_TEXT_PADDING = 12 +local TEXT_BUTTON_PADDING = 24 +local BUTTON_HEIGHT = 48 +local BUTTON_MAX_SIZE = 640 + +local EmptyState = Roact.PureComponent:extend("EmptyState") + +EmptyState.validateProps = t.strictInterface({ + text = t.string, + icon = t.optional(validateImage), + size = t.optional(t.UDim2), + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + buttonIcon = t.optional(validateImage), + onActivated = t.optional(t.callback), +}) + +EmptyState.defaultProps = { + icon = Images[DEFAULT_ICON], + size = UDim2.fromScale(1, 1), + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + buttonIcon = Images[DEFAULT_BUTTON_ICON], +} + +function EmptyState:init() + self:setState({ + absoluteSize = Vector2.new(0, 0), + }) + self.onAbsoluteSizeChange = function(frame) + self:setState({ + absoluteSize = frame.AbsoluteSize, + }) + end +end + +function EmptyState:render() + return withStyle(function(style) + return Roact.createElement("Frame", { + [Roact.Change.AbsoluteSize] = self.onAbsoluteSizeChange, + Size = self.props.size, + Position = self.props.position, + AnchorPoint = self.props.anchorPoint, + BackgroundTransparency = 1, + }, { + Content = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 187), + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Icon = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0), + Size = UDim2.fromOffset(getIconSize(IconSize.XLarge), getIconSize(IconSize.XLarge)), + LayoutOrder = 1, + Image = self.props.icon, + BackgroundTransparency = 1, + ImageColor3 = style.Theme.IconEmphasis.Color, + ImageTransparency = style.Theme.IconEmphasis.Transparency, + }), + iconTextPadding = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.fromOffset(0, ICON_TEXT_PADDING), + BackgroundTransparency = 1, + LayoutOrder = 2, + }), + Text = Roact.createElement(GenericTextLabel, { + Text = self.props.text, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + LayoutOrder = 3, + fontStyle = style.Font.Body, + colorStyle = style.Theme.TextDefault, + }), + textButtonPadding = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.fromOffset(0, TEXT_BUTTON_PADDING), + BackgroundTransparency = 1, + LayoutOrder = 4, + }), + buttonFrame = self.props.onActivated and Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, BUTTON_HEIGHT), + AnchorPoint = Vector2.new(0.5, 0), + BackgroundTransparency = 1, + LayoutOrder = 5, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, getPageMargin(self.state.absoluteSize.X)), + PaddingRight = UDim.new(0, getPageMargin(self.state.absoluteSize.X)), + }), + UISizeConstraint = Roact.createElement("UISizeConstraint", { + MaxSize = Vector2.new(BUTTON_MAX_SIZE, BUTTON_HEIGHT), + }), + Button = Roact.createElement(SecondaryButton, { + size = UDim2.fromScale(1, 1), + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + onActivated = self.props.onActivated, + icon = self.props.buttonIcon + }) + }) + }) + }) + end) +end + +return EmptyState diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/EmptyState.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/EmptyState.spec.lua new file mode 100644 index 0000000..2a73466 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/EmptyState.spec.lua @@ -0,0 +1,39 @@ +return function() + local Indicator = script.Parent + local App = Indicator.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local EmptyState = require(script.Parent.EmptyState) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + local Images = require(UIBlox.App.ImageSet.Images) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + it("should mount and unmount with only the required props", function() + local element = mockStyleComponent({ + emptyState = Roact.createElement(EmptyState, { + text = "No [Items]", + }) + }) + local instance = Roact.mount(element, frame, "EmptyState") + Roact.unmount(instance) + end) + + it("should mount and unmount with all the props", function() + local element = mockStyleComponent({ + emptyState = Roact.createElement(EmptyState, { + text = "No [Items]", + icon = Images['icons/status/oof_xlarge'], + buttonIcon = Images['icons/common/refresh'], + size = UDim2.fromScale(1, 1), + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + onActivated = (function() print("callback") end), + }) + }) + local instance = Roact.mount(element, frame, "EmptyState") + Roact.unmount(instance) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/__stories__/Badge.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/__stories__/Badge.story.lua new file mode 100644 index 0000000..988342c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Indicator/__stories__/Badge.story.lua @@ -0,0 +1,183 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local Indicator = script.Parent.Parent +local App = Indicator.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local Badge = require(Indicator.Badge) +local Images = require(App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local DarkTheme = require(Packages.UIBlox.App.Style.Themes.DarkTheme) + +local BadgeStory = Roact.PureComponent:extend("BadgeStory") + +function BadgeStory:init() + self.state = { + badgeValue = "", + } +end + +function BadgeStory:render() + local badgeValue = self.state.badgeValue + if tonumber(badgeValue) then + badgeValue = tonumber(badgeValue) + end + if badgeValue == "" then + badgeValue = 0 + end + + return Roact.createElement("TextButton", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Text = "", + }, { + ScrollingFrame = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, 0, 1, 0), + CanvasSize = UDim2.new(1, 0, 0, 1500), + BackgroundTransparency = 1, + ScrollingDirection = Enum.ScrollingDirection.Y, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }), + + ValueUpdated = Roact.createElement("Frame", { + Size = UDim2.fromOffset(200, 100), + BackgroundTransparency = 1, + }, { + TextBox = Roact.createElement("TextBox", { + Size = UDim2.fromOffset(200, 50), + Text = tostring(self.state.badgeValue), + TextColor3 = Color3.new(0, 0, 0), + PlaceholderColor3 = Color3.new(0, 0, 0), + PlaceholderText = "Enter Badge Value", + + [Roact.Change.Text] = function(rbx) + self:setState({ + badgeValue = rbx.Text, + }) + end, + }) + }), + + BadgeStory = Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 0, 200), + layoutOrder = 2, + title = "Badge", + subTitle = "Indicator.Badge", + }, { + Icon = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Size = UDim2.new(0, 36, 0, 36), + Image = Images["icons/common/notificationOn"], + }, { + Badge = badgeValue ~= 0 and Roact.createElement(Badge, { + position = UDim2.new(0.5, 0, 0.5, 0), + anchorPoint = Vector2.new(0, 1), + + value = badgeValue, + }), + }), + }), + + DisabledBadgeStory = Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 0, 200), + layoutOrder = 3, + title = "Disabled Badge", + subTitle = "Indicator.Badge", + }, { + Icon = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Size = UDim2.new(0, 36, 0, 36), + ImageTransparency = 0.5, + Image = Images["icons/common/notificationOn"], + }, { + Badge = badgeValue ~= 0 and Roact.createElement(Badge, { + position = UDim2.new(0.5, 0, 0.5, 0), + anchorPoint = Vector2.new(0, 1), + + disabled = true, + value = badgeValue, + }), + }), + }), + + BadgeTileStory = Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 0, 300), + layoutOrder = 4, + title = "Badge Tile", + subTitle = "Indicator.Badge", + }, { + Tile = Roact.createElement("Frame", { + Size = UDim2.fromOffset(160, 160), + BackgroundTransparency = DarkTheme.BackgroundUIDefault.Transparency, + BackgroundColor3 = DarkTheme.BackgroundUIDefault.Color, + }, { + Badge = badgeValue ~= 0 and Roact.createElement(Badge, { + position = UDim2.new(1, -10, 0, 10), + anchorPoint = Vector2.new(1, 0), + + value = badgeValue, + }), + }), + }), + + BadgeShadowStory = Roact.createElement(StoryItem, { + size = UDim2.new(1, 0, 0, 200), + layoutOrder = 5, + title = "Badge With Shadow", + subTitle = "Indicator.Badge", + }, { + Background = Roact.createElement("Frame", { + BackgroundColor3 = Color3.fromRGB(128, 187, 219), + Size = UDim2.new(0, 200, 1, 0), + }, { + DropShadow = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.new(0, 36, 0, 36), + Image = Images["component_assets/dropshadow_chatOff"], + ZIndex = 1, + }), + + Icon = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.new(0, 36, 0, 36), + Image = Images["icons/menu/chat_off"], + ZIndex = 2, + }, { + Badge = badgeValue ~= 0 and Roact.createElement(Badge, { + position = UDim2.new(0, 24, 0.5, 0), + anchorPoint = Vector2.new(0, 0.5), + + hasShadow = true, + value = badgeValue, + }) + }), + }), + }), + }) + }) +end + +return function(target) + local story = Roact.createElement(StoryView, {}, { + Roact.createElement(BadgeStory) + }) + local handle = Roact.mount(story, target, "BadgeStory") + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Checkbox.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Checkbox.lua new file mode 100644 index 0000000..57dff97 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Checkbox.lua @@ -0,0 +1,89 @@ +local Packages = script.Parent.Parent.Parent.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(Packages.UIBlox.Core.Style.withStyle) +local Images = require(Packages.UIBlox.App.ImageSet.Images) +local InputButton = require(Packages.UIBlox.Core.InputButton.InputButton) + +--TODO: This code is considered Control.Checkbox by design, consider moving this out of InputButton for consistency. + +local Checkbox = Roact.PureComponent:extend("Checkbox") + +local validateProps = t.strictInterface({ + text = t.string, + isSelected = t.optional(t.boolean), + isDisabled = t.optional(t.boolean), + onActivated = t.callback, + size = t.optional(t.UDim2), + layoutOrder = t.optional(t.number), + [Roact.Ref] = t.optional(t.table), +}) + +Checkbox.defaultProps = { + text = "Checkbox Text", + isSelected = false, + isDisabled = false, +} + +local CHECKMARK_SIZE = 14 + +function Checkbox:init() + self.state = { + value = self.props.isSelected + } + + self.onFlip = function() + if self.props.isDisabled then return end + self.props.onActivated(not self.props.isSelected) + end +end + +function Checkbox:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + local image + local imageColor + local fillImage + local fillImageSize + + local transparency = theme.TextDefault.Transparency + local textColor = theme.TextDefault.Color + local fillImageColor = theme.SystemPrimaryContent.Color + + if self.props.isDisabled then + transparency = 0.5 + end + + if self.props.isSelected then + image = Images["squircles/fill"] + imageColor = theme.SystemPrimaryDefault.Color + fillImage = Images["icons/status/success_small"] + fillImageSize = UDim2.new(0, CHECKMARK_SIZE, 0, CHECKMARK_SIZE) + else + image = Images["squircles/hollow"] + imageColor = theme.TextDefault.Color + end + + return Roact.createElement(InputButton, { + text = self.props.text, + onActivated = self.onFlip, + size = self.props.size, + image = image, + imageColor = imageColor, + fillImage = fillImage, + fillImageSize = fillImageSize, + fillImageColor = fillImageColor, + selectedColor = theme.SystemPrimaryDefault.Color, + textColor = textColor, + transparency = transparency, + layoutOrder = self.props.layoutOrder, + isDisabled = self.props.isDisabled, + }) + end) +end + +return Checkbox diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Checkbox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Checkbox.spec.lua new file mode 100644 index 0000000..4c29fb9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Checkbox.spec.lua @@ -0,0 +1,60 @@ +return function() + local Packages = script.Parent.Parent.Parent.Parent + local Roact = require(Packages.Roact) + local Checkbox = require(script.Parent.Checkbox) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + local Images = require(Packages.UIBlox.App.ImageSet.Images) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + checkbox = Roact.createElement(Checkbox, { + text = "something", + onActivated = function () end, + size = UDim2.new(1, 0, 1, 0), + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element, frame, "Checkbox") + Roact.unmount(instance) + end) + + it("should have a hollow squircle as its false image", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + checkbox = Roact.createElement(Checkbox, { + text = "something", + onActivated = function () end, + size = UDim2.new(1, 0, 1, 0), + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element, frame, "Checkbox") + local image = frame:FindFirstChildWhichIsA("ImageButton", true) + Roact.update(instance, element) + expect(image.ImageRectOffset).to.equal(Images["squircles/hollow"].ImageRectOffset) + + Roact.unmount(instance) + end) + + it("should have a filled squircle as its true image", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + checkbox = Roact.createElement(Checkbox, { + text = "something", + isSelected = true, + onActivated = function () end, + size = UDim2.new(1, 0, 1, 0), + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element, frame, "Checkbox") + local image = frame:FindFirstChildWhichIsA("ImageButton", true) + expect(image.ImageRectOffset).to.equal(Images["squircles/fill"].ImageRectOffset) + + Roact.unmount(instance) + end) + end) + +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/CheckboxList.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/CheckboxList.lua new file mode 100644 index 0000000..ddbcf45 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/CheckboxList.lua @@ -0,0 +1,90 @@ +local Packages = script.Parent.Parent.Parent.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) +local Checkbox = require(script.Parent.Checkbox) + +local CheckboxList = Roact.PureComponent:extend("CheckboxList") + +local validateButton = t.strictInterface({ + label = t.string, + isSelected = t.optional(t.boolean), + isDisabled = t.optional(t.boolean), +}) + +local validateProps = t.strictInterface({ + checkboxes = t.array(t.union(t.string, validateButton)), + onActivated = t.callback, + elementSize = t.UDim2, + atMost = t.optional(t.number), + layoutOrder = t.optional(t.number) +}) + +local function numTrue(truthTable) + local num = 0 + for _, value in pairs(truthTable) do + if value then + num = num + 1 + end + end + + return num +end + +function CheckboxList:init() + local atMost = self.props.atMost or #self.props.checkboxes + + local selectedIndices = {} + local disabledIndices = {} + + for i, v in ipairs(self.props.checkboxes) do + if type(v) == "table" then + selectedIndices[i] = v.isSelected or false + disabledIndices[i] = v.isDisabled or false + end + end + + assert(numTrue(selectedIndices) < atMost, "number of 'isSelected' must be less than atMost!") + + self.state = { + selectedIndices = selectedIndices, + disabledIndices = disabledIndices, + } + + self.doLogic = function(key) + self:setState({ + selectedIndices = Cryo.Dictionary.join(self.state.selectedIndices, + {[key] = not self.state.selectedIndices[key] and numTrue(self.state.selectedIndices) < atMost}) + }) + self.props.onActivated(self.state.selectedIndices) + end +end + +function CheckboxList:render() + assert(validateProps(self.props)) + + local checkboxes = {} + checkboxes.layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder + }) + + for i, value in ipairs(self.props.checkboxes) do + checkboxes["Checkbox"..i] = Roact.createElement(Checkbox, { + text = type(value) == "table" and value.label or value, + isSelected = self.state.selectedIndices[i], + isDisabled = self.state.disabledIndices[i], + onActivated = function() self.doLogic(i) end, + size = self.props.elementSize, + layoutOrder = i, + }) + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + }, checkboxes) +end + +return CheckboxList diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/CheckboxList.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/CheckboxList.spec.lua new file mode 100644 index 0000000..8e6a6e7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/CheckboxList.spec.lua @@ -0,0 +1,56 @@ +return function() + local Packages = script.Parent.Parent.Parent.Parent + local Roact = require(Packages.Roact) + local CheckboxList = require(script.Parent.CheckboxList) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + local Images = require(Packages.UIBlox.App.ImageSet.Images) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + CheckboxList = Roact.createElement(CheckboxList, { + checkboxes = {"a", "b", "c"}, + onActivated = function() end, + elementSize = UDim2.new(0, 50, 0, 50), + }) + }) + local instance = Roact.mount(element, frame, "CheckboxList") + Roact.unmount(instance) + end) + + it("should have the proper default values", function() + local frame = Instance.new("Frame") + local element = mockStyleComponent({ + CheckboxList = Roact.createElement(CheckboxList, { + checkboxes = { + { + label = "a", + isSelected = true + }, + "b", + { + label = "c", + isSelected = true + } + }, + atMost = 6, + onActivated = function() end, + elementSize = UDim2.new(0, 50, 0, 50), + layoutOrder = 4, + }) + }) + local instance = Roact.mount(element, frame, "CheckboxList") + local image1 = frame:FindFirstChild("Checkbox1", true) + local image3 = frame:FindFirstChild("Checkbox3", true) + expect(image1.InputButtonImage.InputFillImage.ImageRectOffset).to.equal( + Images["icons/status/success_small"].ImageRectOffset) + expect(image3.InputButtonImage.InputFillImage.ImageRectOffset).to.equal( + Images["icons/status/success_small"].ImageRectOffset) + + Roact.unmount(instance) + end) + + end) + +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButton.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButton.lua new file mode 100644 index 0000000..6805057 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButton.lua @@ -0,0 +1,86 @@ +local Packages = script.Parent.Parent.Parent.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(Packages.UIBlox.Core.Style.withStyle) +local Images = require(Packages.UIBlox.App.ImageSet.Images) +local InputButton = require(Packages.UIBlox.Core.InputButton.InputButton) + +local RadioButton = Roact.PureComponent:extend("RadioButton") + +local validateProps = t.strictInterface({ + text = t.string, + isSelected = t.optional(t.boolean), + isDisabled = t.optional(t.boolean), + onActivated = t.callback, + size = t.UDim2, + layoutOrder = t.optional(t.number), + key = t.number, +}) + +RadioButton.defaultProps = { + text = "RadioButton Text", + isSelected = false, + isDisabled = false, + layoutOrder = 0, +} + +local INNER_BUTTON_SIZE = 18 + +function RadioButton:init() + self.onSetValue = function() + if not self.props.isDisabled then + self.props.onActivated(self.props.key) + end + end +end + +function RadioButton:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + local image = Images["component_assets/circle_24_stroke_1"] + local imageColor = theme.TextDefault.Color + + local fillImage = Images["component_assets/circle_16"] + local fillImageColor = theme.TextDefault.Color + + local fillImageSize + local isSelected = self.props.isSelected + + local textColor = theme.TextDefault.Color + local transparency = theme.TextDefault.Transparency + + if self.props.isDisabled then + transparency = 0.5 + end + + if isSelected then + fillImageSize = UDim2.new(0, INNER_BUTTON_SIZE, 0, INNER_BUTTON_SIZE) + fillImageColor = theme.SystemPrimaryDefault.Color + imageColor = theme.SystemPrimaryDefault.Color + else + fillImageSize = UDim2.new(0, 0, 0, 0) + end + + return Roact.createElement(InputButton, { + text = self.props.text, + onActivated = self.onSetValue, + size = self.props.size, + image = image, + imageColor = imageColor, + fillImage = fillImage, + fillImageSize = fillImageSize, + fillImageColor = fillImageColor, + selectedColor = theme.SystemPrimaryDefault.Color, + textColor = textColor, + transparency = transparency, + layoutOrder = self.props.layoutOrder, + isDisabled = self.props.isDisabled, + }) + end) +end + +return RadioButton diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButton.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButton.spec.lua new file mode 100644 index 0000000..54fce73 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButton.spec.lua @@ -0,0 +1,59 @@ +return function() + local Packages = script.Parent.Parent.Parent.Parent + local Roact = require(Packages.Roact) + local RadioButton = require(script.Parent.RadioButton) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + radioButton = Roact.createElement(RadioButton, { + text = "something", + onActivated = function () end, + size = UDim2.new(1, 0, 1, 0), + layoutOrder = 1, + key = 1, + }) + }) + local instance = Roact.mount(element, frame, "Checkbox") + Roact.unmount(instance) + end) + + it("should have an empty circle as its false image", function() + local element = mockStyleComponent({ + radioButton = Roact.createElement(RadioButton, { + text = "something", + onActivated = function () end, + size = UDim2.new(1, 0, 1, 0), + layoutOrder = 1, + key = 1, + }) + }) + local instance = Roact.mount(element, frame, "Checkbox") + local image = frame:FindFirstChild("InputFillImage", true) + expect(image.Size).to.equal(UDim2.new(0, 0)) + + Roact.unmount(instance) + end) + + it("should have a filled circle as its true image", function() + local element = mockStyleComponent({ + radioButton = Roact.createElement(RadioButton, { + text = "something", + isSelected = true, + onActivated = function () end, + size = UDim2.new(1, 0, 1, 0), + layoutOrder = 1, + key = 1, + }) + }) + local instance = Roact.mount(element, frame, "Checkbox") + local image = frame:FindFirstChild("InputFillImage", true) + expect(image.Size).never.to.equal(UDim2.new(0, 0)) + + Roact.unmount(instance) + end) + end) + +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButtonList.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButtonList.lua new file mode 100644 index 0000000..181d919 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButtonList.lua @@ -0,0 +1,69 @@ +local Packages = script.Parent.Parent.Parent.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local RadioButton = require(script.Parent.RadioButton) + +local RadioButtonList = Roact.PureComponent:extend("RadioButtonList") + +local validateButton = t.strictInterface({ + label = t.string, + isDisabled = t.optional(t.boolean), +}) + +local validateProps = t.strictInterface({ + radioButtons = t.array(t.union(t.string, validateButton)), + onActivated = t.callback, + elementSize = t.UDim2, + selectedValue = t.optional(t.number), + layoutOrder = t.optional(t.number) +}) + +function RadioButtonList:init() + self.state = { + currentValue = self.props.selectedValue or 0, + } + + local disabledIndices = {} + for i, v in ipairs(self.props.radioButtons) do + disabledIndices[i] = type(v) == "table" and v.isDisabled or false + end + self.state.disabledIndices = disabledIndices + + self.doLogic = function(key) + if self.state.disabledIndices[key] then return end + self:setState({ + currentValue = key, + }) + self.props.onActivated(key) + end +end + +function RadioButtonList:render() + assert(validateProps(self.props)) + + local radioButtons = {} + radioButtons.layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder + }) + + for i, value in ipairs(self.props.radioButtons) do + radioButtons["RadioButton"..i] = Roact.createElement(RadioButton, { + text = type(value) == "table" and value.label or value, + isSelected = i == self.state.currentValue, + isDisabled = self.state.disabledIndices[i], + onActivated = self.doLogic, + size = self.props.elementSize, + layoutOrder = i, + key = i, + }) + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + }, radioButtons) +end + +return RadioButtonList \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButtonList.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButtonList.spec.lua new file mode 100644 index 0000000..ca70688 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/RadioButtonList.spec.lua @@ -0,0 +1,43 @@ +return function() + local Packages = script.Parent.Parent.Parent.Parent + local Roact = require(Packages.Roact) + local RadioButtonList = require(script.Parent.RadioButtonList) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + local Images = require(Packages.UIBlox.App.ImageSet.Images) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + RadioButtonList = Roact.createElement(RadioButtonList, { + radioButtons = {"a", "b", "c"}, + onActivated = function() end, + elementSize = UDim2.new(0, 50, 0, 50), + }) + }) + local instance = Roact.mount(element, frame, "RadioButtonList") + Roact.unmount(instance) + end) + + it("should have the proper default value", function() + local element = mockStyleComponent({ + RadioButtonList = Roact.createElement(RadioButtonList, { + radioButtons = {"a", "b", "c"}, + selectedValue = 2, + onActivated = function() end, + elementSize = UDim2.new(0, 50, 0, 50), + layoutOrder = 4, + }) + }) + + local instance = Roact.mount(element, frame, "RadioButtonList") + local image2 = frame:FindFirstChild("RadioButton2", true) + expect(image2.InputButtonImage.InputFillImage.ImageRectOffset).to.equal( + Images["component_assets/circle_16"].ImageRectOffset) + + Roact.unmount(instance) + end) + + end) + +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Toggle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Toggle.lua new file mode 100644 index 0000000..0c5885d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Toggle.lua @@ -0,0 +1,210 @@ +local InputButton = script.Parent +local App = InputButton.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Otter = require(Packages.Otter) +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local withStyle = require(Packages.UIBlox.Core.Style.withStyle) +local Images = require(Packages.UIBlox.App.ImageSet.Images) +local Controllable = require(Packages.UIBlox.Core.Control.Controllable) +local ControlState = require(Packages.UIBlox.Core.Control.Enum.ControlState) +local ImageSetComponent = require(Packages.UIBlox.Core.ImageSet.ImageSetComponent) + +local divideTransparency = require(Packages.UIBlox.Utility.divideTransparency) +local lerp = require(Packages.UIBlox.Utility.lerp) + +local SPRING_PARAMETERS = { + frequency = 4, +} + +local KNOB_OFF_POSITION = UDim2.new(0, -4, 0.5, 0) +local KNOB_ON_POSITION = UDim2.new(0, 20, 0.5, 0) + +local TRACK_IMAGE_ID = "component_assets/circle_36_stroke_1" +local TRACK_SLICE_CENTER = Rect.new(18, 18, 18, 18) + +local TRACK_FILL_IMAGE_ID = "component_assets/circle_36" +local TRACK_FILL_SLICE_CENTER = Rect.new(18, 18, 18, 18) + +local KNOB_IMAGE_ID = "component_assets/circle_28_padding_10" +local KNOB_SHADOW_IMAGE_ID = "component_assets/dropshadow_28" + +local validateProps = t.strictInterface({ + isSelected = t.optional(t.boolean), + isDisabled = t.optional(t.boolean), + onActivated = t.callback, + + layoutOrder = t.optional(t.integer), + anchorPoint = t.optional(t.Vector2), + position = t.optional(t.UDim2), +}) + +local InnerToggle = Roact.PureComponent:extend("Toggle") + +function InnerToggle:init() + local initialProgress = self.props.isSelected and 1 or 0 + local setProgress + self.progress, setProgress = Roact.createBinding(initialProgress) + + self.style, self.setStyle = Roact.createBinding(self.props.style) + self.controlState, self.setControlState = Roact.createBinding(self.state.controlState) + + local joinedBinding = Roact.joinBindings({ + progress = self.progress, + style = self.style, + controlState = self.controlState, + }) + + self.fillTransparency = joinedBinding:map(function(values) + local baseTransparency = values.style.Theme.ContextualPrimaryDefault.Transparency + local transparencyDivisor = values.controlState == ControlState.Disabled and 2 or 1 + return lerp(1, divideTransparency(baseTransparency, transparencyDivisor), values.progress) + end) + + self.knobPosition = self.progress:map(function(value) + return KNOB_OFF_POSITION:lerp(KNOB_ON_POSITION, value) + end) + + self.knobTransparency = Roact.joinBindings({ + style = self.style, + controlState = self.controlState, + }):map(function(values) + local baseTransparency = values.style.Theme.ContextualPrimaryDefault.Transparency + local transparencyDivisor = values.controlState == ControlState.Disabled and 2 or 1 + return divideTransparency(baseTransparency, transparencyDivisor) + end) + + -- We need to fade the track outline out when the toggle is selected, because + -- otherwise it creates a visually jarring border around the filled track. + self.trackTransparency = joinedBinding:map(function(values) + local targetTransparency = values.controlState == ControlState.Hover + and values.style.Theme.SecondaryOnHover.Transparency + or values.style.Theme.SecondaryDefault.Transparency + + if values.controlState == ControlState.Disabled then + targetTransparency = 1 - (1 - targetTransparency) / 2 + end + + return lerp(targetTransparency, 1, values.progress) + end) + + self.trackColor = Roact.joinBindings({ + style = self.style, + controlState = self.controlState, + }):map(function(values) + if values.controlState == ControlState.Hover then + return values.style.Theme.SecondaryOnHover.Color + else + return values.style.Theme.SecondaryDefault.Color + end + end) + + self.fillColor = self.style:map(function(style) + return style.Theme.ContextualPrimaryDefault.Color + end) + + self.progressMotor = Otter.createSingleMotor(initialProgress) + self.progressMotor:onStep(setProgress) +end + +function InnerToggle:render() + return withStyle(function(style) + return Roact.createElement(Controllable, { + controlComponent = { + component = "ImageButton", + props = { + BackgroundTransparency = 1, + Image = "", + Size = UDim2.fromOffset(60, 44), + Position = self.props.position, + LayoutOrder = self.props.layoutOrder, + AnchorPoint = self.props.anchorPoint, + [Roact.Event.Activated] = not self.props.isDisabled and self.props.onActivated, + }, + children = { + Track = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(60, 36), + BackgroundTransparency = 1, + Image = Images[TRACK_IMAGE_ID], + ScaleType = Enum.ScaleType.Slice, + SliceCenter = TRACK_SLICE_CENTER, + ImageTransparency = self.trackTransparency, + ImageColor3 = self.trackColor, + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + }), + Fill = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(60, 36), + BackgroundTransparency = 1, + Image = Images[TRACK_FILL_IMAGE_ID], + ScaleType = Enum.ScaleType.Slice, + SliceCenter = TRACK_FILL_SLICE_CENTER, + ImageColor3 = self.fillColor, + ImageTransparency = self.fillTransparency, + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + ZIndex = 2, + }), + Knob = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(44, 44), + BackgroundTransparency = 1, + Image = Images[KNOB_IMAGE_ID], + ImageTransparency = self.knobTransparency, + Position = self.knobPosition, + AnchorPoint = Vector2.new(0, 0.5), + ZIndex = 4, + }), + KnobShadow = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(44, 44), + BackgroundTransparency = 1, + Image = Images[KNOB_SHADOW_IMAGE_ID], + ImageTransparency = self.knobTransparency, + Position = self.knobPosition, + AnchorPoint = Vector2.new(0, 0.5), + ZIndex = 3, + }), + } + }, + isDisabled = self.props.isDisabled, + onStateChanged = function(_, newState) + self.setControlState(newState) + end, + }) + end) +end + +function InnerToggle:didMount() + self.progressMotor:start() +end + +function InnerToggle:didUpdate(lastProps, lastState) + if lastProps.isSelected ~= self.props.isSelected then + local newProgress = self.props.isSelected and 1 or 0 + self.progressMotor:setGoal(Otter.spring(newProgress, SPRING_PARAMETERS)) + end + + if lastProps.style ~= self.props.style then + self.setStyle(self.props.style) + end +end + +function InnerToggle:willUnmount() + self.progressMotor:destroy() +end + +local function injectUIBloxStyle(props) + -- Validate props here, since the inner toggle receives these props plus + -- the style prop! + assert(validateProps(props)) + return withStyle(function(style) + return Roact.createElement(InnerToggle, Cryo.Dictionary.join(props, { + style = style, + })) + end) +end + +return injectUIBloxStyle diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Toggle.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Toggle.spec.lua new file mode 100644 index 0000000..e3d4b12 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/Toggle.spec.lua @@ -0,0 +1,19 @@ +return function() + local Packages = script.Parent.Parent.Parent.Parent + local Roact = require(Packages.Roact) + local Toggle = require(script.Parent.Toggle) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + TestToggle = Roact.createElement(Toggle, { + onActivated = function() + end, + }) + }) + local instance = Roact.mount(element, nil, "Toggle") + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultiLineCheckbox.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultiLineCheckbox.story.lua new file mode 100644 index 0000000..6d05138 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultiLineCheckbox.story.lua @@ -0,0 +1,82 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Packages = ReplicatedStorage.Packages +local Roact = require(Packages.Roact) +local UIBlox = require(Packages.UIBlox) +local GenericTextLabel = require(Packages.UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local withStyle = UIBlox.Core.Style.withStyle + +local StoryComponents = Packages.StoryComponents +local StoryView = require(StoryComponents.StoryView) +local StoryItem = require(StoryComponents.StoryItem) + +local CheckboxList = UIBlox.App.InputButton.CheckboxList + +return function(target) + local MulitlineCheckboxDemo = Roact.PureComponent:extend("MulitlineCheckboxDemo") + + function MulitlineCheckboxDemo:render() + return withStyle(function(style) + return Roact.createElement(StoryView, {}, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }), + Label = Roact.createElement(GenericTextLabel, { + BackgroundTransparency = 1, + Text = "This CheckboxList can have up to 3 boxes selected at a time", + Size = UDim2.new(1, 0, 0, 50), + LayoutOrder = 1, + fontStyle = style.Font.Header2, + colorStyle = style.Theme.TextDefault, + }), + CheckboxListFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 480, 0, 300), + LayoutOrder = 2, + }, { + CheckboxList = Roact.createElement(CheckboxList, { + atMost = 3, + checkboxes = { + { + label = "Selected and Disabled", + isSelected = true, + isDisabled = true, + }, + { + label = "Unselected and Disabled", + isSelected = false, + isDisabled = true, + }, + { + label = "Selected and Enabled", + isSelected = true, + isDisabled = false, + }, + "Unselected and Enabled", + "This is a checkbox that has an absurd amount of text in its label to demonstrate wrapping", + }, + onActivated = function() end, + elementSize = UDim2.new(0, 480, 0, 54), + layoutOrder = 1, + }), + }), + }) + end) + end + + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + StoryItem = Roact.createElement(StoryItem, { + title = "A demonstration of checkboxes with long labels", + subTitle = "Shows how text can wrap when it is long", + showDivider = true, + }, { + Roact.createElement(MulitlineCheckboxDemo) + }) + }), target, "CheckboxList") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultipleCheckboxes.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultipleCheckboxes.story.lua new file mode 100644 index 0000000..616ce33 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultipleCheckboxes.story.lua @@ -0,0 +1,75 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Packages = script.Parent.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local CheckboxList = require(script.Parent.Parent.CheckboxList) + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local yourComponent = Roact.Component:extend("yourComponent") +function yourComponent:init() + self.ref = Roact.createRef() +end + +function yourComponent:render() + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, -50), + BackgroundColor3 = Color3.fromRGB(55, 55, 55), + }, { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + + }), + checkboxes = Roact.createElement(CheckboxList, { + layoutOrder = 1, + atMost = 3, + checkboxes = { + { + label = "Vaporeon", + isSelected = true, + isDisabled = true, + }, + "Jolteon", + "Flareon", + "Espeon", + "Umbreon", + "Leafeon", + "Glaceon", + "Sylveon", + }, + onActivated = function(value) + local values = "" + for k, v in pairs(value) do + if v then + values = values .. k .. ", " + end + end + if self.ref.current then + self.ref.current.Text = "Pick at most 3. The current value is: " .. values + print(self.ref.current.Text) + end + end, + elementSize = UDim2.new(0, 480, 0, 54), + }), + text = Roact.createElement("TextLabel", { + LayoutOrder = 2, + TextSize = 14, + Text = "Pick at most 3", + TextXAlignment = Enum.TextXAlignment.Left, + Size = UDim2.new(1, 0, 0, 50), + [Roact.Ref] = self.ref, + }), + }) + +end + + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(yourComponent), + }), target, "MultipleCheckboxes") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultipleRadioButtons.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultipleRadioButtons.story.lua new file mode 100644 index 0000000..e78310e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/InputButton/__stories__/MultipleRadioButtons.story.lua @@ -0,0 +1,39 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Packages = script.Parent.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local RadioButtonList = require(script.Parent.Parent.RadioButtonList) + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +return function(target) + local styleProvider = Roact.createElement(StoryView, {}, { + Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = Color3.fromRGB(55, 55, 55), + }, { + Roact.createElement(RadioButtonList, { + radioButtons = { + { + label = "Bulbasaur" + }, + "Squirtle", + "Charmander", + { + label = "Mewtwo", + isDisabled = true, + }, + }, + onActivated = function(value) + print("The current value is: ", value) + end, + selectedValue = 2, + elementSize = UDim2.new(0, 480, 0, 54), + }) + }) + }) + local handle = Roact.mount(styleProvider, target, "MultipleRadioButtons") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/LoadingState.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/LoadingState.lua new file mode 100644 index 0000000..6d6b0dd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/LoadingState.lua @@ -0,0 +1,11 @@ +local Loading = script.Parent.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate("LoadingState", { + "Loading", + "Failed", + "Loaded" +}) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/ReloadingStyle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/ReloadingStyle.lua new file mode 100644 index 0000000..0545378 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/ReloadingStyle.lua @@ -0,0 +1,10 @@ +local Loading = script.Parent.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate("ReloadingStyle", { + "AllowReload", + "LockReload", +}) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/RenderOnFailedStyle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/RenderOnFailedStyle.lua new file mode 100644 index 0000000..31b9571 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/RenderOnFailedStyle.lua @@ -0,0 +1,10 @@ +local Loading = script.Parent.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate("RenderOnFailedStyle", { + "EmptyStatePage", + "RetryButton", +}) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/RetrievalStatus.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/RetrievalStatus.lua new file mode 100644 index 0000000..04db7bd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/Enum/RetrievalStatus.lua @@ -0,0 +1,12 @@ +local Loading = script.Parent.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate("RetrievalStatus", { + "NotStarted", + "Fetching", + "Done", + "Failed", +}) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadableImage.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadableImage.lua new file mode 100644 index 0000000..9cf3964 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadableImage.lua @@ -0,0 +1,319 @@ +local Loading = script.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) + +local ShimmerPanel = require(Loading.ShimmerPanel) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local Images = require(UIBlox.App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local ContentProviderContext = require(UIBlox.App.Context.ContentProvider) + +local LOAD_FAILED_RETRY_COUNT = 3 +local RETRY_TIME_MULTIPLIER = 1.5 + +local decal = Instance.new("Decal") +local inf = math.huge +local loadedImagesByUri = {} + +local LoadingState = { + InProgress = "InProgress", + Failed = "Failed", + Loaded = "Loaded", +} + +local function shouldLoadImage(image) + return image ~= nil and loadedImagesByUri[image] == nil +end + +local validateProps = t.strictInterface({ + -- The anchor point of the final and loading image + AnchorPoint = t.optional(t.Vector2), + -- The background color of the final image. Defaults to placeholder background color. + BackgroundColor3 = t.optional(t.Color3), + -- The background transparency of the final image. Defaults to placeholder transparency. + BackgroundTransparency = t.optional(t.number), + -- The corner radius of the image, shimmer, and failed image's rounded corners. + cornerRadius = t.optional(t.UDim), + -- The final image + Image = t.optional(t.string), + -- The transparency of the final and loading image + ImageTransparency = t.optional(t.number), + -- The image rect offset of the final and loading image + ImageRectOffset = t.optional(t.union(t.Vector2, t.table)), + -- The image rect size of the final and loading image + ImageRectSize = t.optional(t.union(t.Vector2, t.table)), + -- The layout order of the final and loading image + LayoutOrder = t.optional(t.integer), + -- The loading image which shows if useShimmerAnimationWhileLoading is false + loadingImage = t.optional(t.string), + -- The max size of all images shown + MaxSize = t.optional(t.Vector2), + -- The min size of all images shown + MinSize = t.optional(t.Vector2), + -- The function to call when loading is complete + onLoaded = t.optional(t.callback), + -- The position point of the loading image + Position = t.optional(t.UDim2), + -- The scale type of the final and loading image + ScaleType = t.optional(t.enum(Enum.ScaleType)), + -- The size point of the loading image + Size = t.UDim2, + -- Whether or not to show a static image or the shimmer animation while loading + useShimmerAnimationWhileLoading = t.optional(t.boolean), + -- Whether to show failed state when failed + showFailedStateWhenLoadingFailed = t.optional(t.boolean), + -- The ZIndex of the loading and final images + ZIndex = t.optional(t.integer), + + contentProvider = t.union(t.instanceOf("ContentProvider"), t.table), +}) + +local LoadableImage = Roact.PureComponent:extend("LoadableImage") + +LoadableImage.defaultProps = { + BackgroundTransparency = 0, + cornerRadius = UDim.new(0, 0), + MaxSize = Vector2.new(inf, inf), + MinSize = Vector2.new(0, 0), + useShimmerAnimationWhileLoading = false, + showFailedStateWhenLoadingFailed = false, +} + +function LoadableImage:init() + self.state = { + loadingState = loadedImagesByUri[self.props.Image] and LoadingState.Loaded or LoadingState.InProgress, + } + self._isMounted = false + + + self.isLoadingComplete = function(image) + if image == Roact.None or image == nil then + return false + else + return self.state.loadingState ~= LoadingState.InProgress + end + end +end + +function LoadableImage:render() + assert(validateProps(self.props)) + + local anchorPoint = self.props.AnchorPoint + local layoutOrder = self.props.LayoutOrder + local size = self.props.Size + local position = self.props.Position + local backgroundColor3 = self.props.BackgroundColor3 + local backgroundTransparency = self.props.BackgroundTransparency + local cornerRadius = self.props.cornerRadius + local scaleType = self.props.ScaleType + local zIndex = self.props.ZIndex + local image = self.props.Image + local imageTransparency = self.props.ImageTransparency + local imageRectOffset = self.props.ImageRectOffset + local imageRectSize = self.props.ImageRectSize + local maxSize = self.props.MaxSize + local minSize = self.props.MinSize + local loadingImage = self.props.loadingImage + local useShimmerAnimationWhileLoading = self.props.useShimmerAnimationWhileLoading + local showFailedStateWhenLoadingFailed = self.props.showFailedStateWhenLoadingFailed + local loadingComplete = self.isLoadingComplete(image) + local loadingFailed = self.state.loadingState == LoadingState.Failed + local hasUISizeConstraint = false + + if maxSize.X ~= inf or maxSize.Y ~= inf or minSize.X ~= 0 or minSize.Y ~= 0 then + hasUISizeConstraint = true + end + + local sizeConstraint = hasUISizeConstraint and Roact.createElement("UISizeConstraint", { + MaxSize = maxSize, + MinSize = minSize, + }) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + if loadingFailed and showFailedStateWhenLoadingFailed then + local failedImage = Images["icons/status/imageunavailable"] + local failedImageSize = failedImage.ImageRectSize / Images.ImagesResolutionScale + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BorderSizePixel = 0, + BackgroundColor3 = theme.PlaceHolder.Color, + BackgroundTransparency = theme.PlaceHolder.Transparency, + LayoutOrder = layoutOrder, + Position = position, + Size = size, + ZIndex = zIndex, + }, { + EmptyIcon = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + AnchorPoint = Vector2.new(0.5, 0.5), + Image = failedImage, + ImageColor3 = theme.UIDefault.Color, + ImageTransparency = theme.UIDefault.Transparency, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, failedImageSize.X, 0, failedImageSize.Y), + }, { + UICorner = cornerRadius ~= UDim.new(0, 0) and Roact.createElement("UICorner", { + CornerRadius = cornerRadius, + }) or nil, + }), + UISizeConstraint = sizeConstraint, + UICorner = cornerRadius ~= UDim.new(0, 0) and Roact.createElement("UICorner", { + CornerRadius = cornerRadius, + }) or nil, + }) + elseif not loadingComplete and useShimmerAnimationWhileLoading then + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BorderSizePixel = 0, + BackgroundColor3 = theme.PlaceHolder.Color, + BackgroundTransparency = theme.PlaceHolder.Transparency, + LayoutOrder = layoutOrder, + Position = position, + Size = size, + ZIndex = zIndex, + }, { + Shimmer = Roact.createElement(ShimmerPanel, { + Size = UDim2.new(1, 0, 1, 0), + cornerRadius = cornerRadius, + }), + UISizeConstraint = sizeConstraint, + UICorner = Roact.createElement("UICorner", { + CornerRadius = cornerRadius, + }) or nil, + }) + else + return Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = anchorPoint, + BackgroundColor3 = backgroundColor3 or theme.PlaceHolder.Color, + BackgroundTransparency = backgroundTransparency or theme.PlaceHolder.Transparency, + BorderSizePixel = 0, + Image = loadingComplete and image or loadingImage, + ImageTransparency = imageTransparency, + ImageRectOffset = imageRectOffset, + ImageRectSize = imageRectSize, + LayoutOrder = layoutOrder, + Position = position, + ScaleType = scaleType, + Size = size, + ZIndex = zIndex, + }, { + UISizeConstraint = sizeConstraint, + UICorner = cornerRadius ~= UDim.new(0, 0) and Roact.createElement("UICorner", { + CornerRadius = cornerRadius, + }) or nil, + }) + end + end) +end + +function LoadableImage:didUpdate(oldProps) + if oldProps.Image ~= self.props.Image then + self:_loadImage() + end +end + +function LoadableImage:didMount() + self._isMounted = true + + self:_loadImage() +end + +function LoadableImage:willUnmount() + self._isMounted = false +end + +function LoadableImage:_loadImage() + local image = self.props.Image + + if shouldLoadImage(image) then + self:setState({ + loadingState = LoadingState.InProgress + }) + else + if loadedImagesByUri[image] then + self:setState({ + loadingState = LoadingState.Loaded + }) + elseif loadedImagesByUri[image] == false then + self:setState({ + loadingState = LoadingState.Failed + }) + end + return + end + + -- Synchronization/Batching work should be done in engine for performance improvements + -- related ticket: CLIPLAYEREX-1764 + coroutine.wrap(function() + local retryCount = 0 + local loadingFailed + + while loadedImagesByUri[image] == nil and retryCount <= LOAD_FAILED_RETRY_COUNT do + if retryCount > 0 then + wait(RETRY_TIME_MULTIPLIER * math.pow(2, retryCount - 1)) + end + + loadingFailed = false + decal.Texture = image + + self.props.contentProvider:PreloadAsync({decal}, function(contentId, assetFetchStatus) + if contentId == image and assetFetchStatus == Enum.AssetFetchStatus.Failure then + loadingFailed = true + end + end) + + -- Image load succeeded, no retry required + if not loadingFailed then + break + end + + retryCount = retryCount + 1 + end + + if loadingFailed == nil then + loadingFailed = not loadedImagesByUri[image] + else + loadedImagesByUri[image] = not loadingFailed + end + + if self._isMounted and self.props.Image == image then + self:setState({ + loadingState = loadingFailed and LoadingState.Failed or LoadingState.Loaded, + }) + + if self.props.onLoaded then + self.props.onLoaded() + end + end + end)() +end + +function LoadableImage.isLoaded(image) + if image == Roact.None or image == nil then + return false + else + return loadedImagesByUri[image] == true + end +end + +return function(props) + return Roact.createElement(ContentProviderContext.Consumer, { + render = function(contentProvider) + local propsWithContentProvider = Cryo.Dictionary.join(props, { + contentProvider = contentProvider, + }) + + return Roact.createElement(LoadableImage, propsWithContentProvider) + end, + }) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadableImage.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadableImage.spec.lua new file mode 100644 index 0000000..33da6d7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadableImage.spec.lua @@ -0,0 +1,114 @@ +return function() + local Loading = script.Parent + local App = Loading.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local LoadableImage = require(Loading.LoadableImage) + + local ContentProviderContext = require(UIBlox.App.Context.ContentProvider) + + local function getMockedContentProvider() + local MockContentProvider = {} + MockContentProvider.__index = MockContentProvider + function MockContentProvider.new() + local self = {} + + setmetatable(self, { + __index = MockContentProvider, + }) + return self + end + + function MockContentProvider:PreloadAsync(assets, callback) + for _, value in ipairs(assets) do + callback(value, Enum.AssetFetchStatus.Success) + end + end + + return MockContentProvider.new() + end + + local testImage = "https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814" + local defaultLoadImage = "rbxasset://textures/ui/LuaApp/icons/ic-game.png" + + describe("LoadableImage", function() + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + Image = Roact.createElement(LoadableImage, { + Image = testImage, + Size = UDim2.fromOffset(80, 80), + Position = UDim2.fromOffset(50, 50), + BackgroundColor3 = Color3.new(0, 0, 0), + BackgroundTransparency = 0, + loadingImage = defaultLoadImage, + }) + }) + local instance = Roact.mount(element, nil, "LoadableImageSample") + Roact.unmount(instance) + end) + it("should not set loading image if image is already in cache", function() + local element = mockStyleComponent({ + ContentProviderProvider = Roact.createElement(ContentProviderContext.Provider, { + value = getMockedContentProvider(), + }, { + Image = Roact.createElement(LoadableImage, { + Image = testImage, + Size = UDim2.fromOffset(80, 80), + Position = UDim2.fromOffset(50, 50), + BackgroundColor3 = Color3.new(0, 0, 0), + BackgroundTransparency = 0, + loadingImage = defaultLoadImage, + }) + }) + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "LoadableImageSample") + expect(container.LoadableImageSample.Image).never.to.equal(defaultLoadImage) + Roact.unmount(instance) + end) + it("should create and destroy on all non-default optional parameters without errors", function() + local element = mockStyleComponent({ + ContentProviderProvider = Roact.createElement(ContentProviderContext.Provider, { + value = getMockedContentProvider(), + }, { + Image = Roact.createElement(LoadableImage, { + AnchorPoint = Vector2.new(0.5, 0), + BackgroundColor3 = Color3.new(255, 0, 0), + BackgroundTransparency = 0.5, + Image = testImage, + ImageRectOffset = Vector2.new(0, 0), + ImageRectSize = Vector2.new(50, 50), + LayoutOrder = 1, + loadingImage = defaultLoadImage, + MaxSize = Vector2.new(10, 10), + MinSize = Vector2.new(1, 1), + Position = UDim2.fromOffset(50, 50), + Size = UDim2.fromOffset(80, 80), + useShimmerAnimationWhileLoading = false, + ZIndex = 1, + }) + }) + }) + local instance = Roact.mount(element, nil, "LoadableImageSample") + Roact.unmount(instance) + end) + it("should set failed image without errors", function() + local element = mockStyleComponent({ + Image = Roact.createElement(LoadableImage, { + Image = "invalid-image-url", + Size = UDim2.fromOffset(80, 80), + Position = UDim2.fromOffset(50, 50), + BackgroundColor3 = Color3.new(0, 0, 0), + BackgroundTransparency = 0, + loadingImage = defaultLoadImage, + showFailedStateWhenLoadingFailed = true, + }) + }) + local instance = Roact.mount(element, nil, "LoadableImageSample") + Roact.unmount(instance) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadingSpinner.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadingSpinner.lua new file mode 100644 index 0000000..3af3e16 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadingSpinner.lua @@ -0,0 +1,32 @@ +local Loading = script.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(UIBlox.App.ImageSet.Images) + +local SpinningImage = require(UIBlox.Core.Animation.SpinningImage) + +local LoadingSpinner = Roact.PureComponent:extend("LoadingSpinner") + +LoadingSpinner.validateProps = t.strictInterface({ + size = t.optional(t.UDim2), + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + rotationRate = t.optional(t.number), +}) + +function LoadingSpinner:render() + return Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + size = self.props.size, + position = self.props.position, + anchorPoint = self.props.anchorPoint, + rotationRate = self.props.rotationRate, + }) +end + +return LoadingSpinner diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadingSpinner.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadingSpinner.spec.lua new file mode 100644 index 0000000..e0b603b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/LoadingSpinner.spec.lua @@ -0,0 +1,25 @@ +return function() + local Loading = script.Parent + local App = Loading.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local LoadingSpinner = require(Loading.LoadingSpinner) + + it("should create and destroy without errors", function() + local spinner = Roact.createElement(LoadingSpinner) + local instance = Roact.mount(spinner, nil, "LoadingSpinnerTest") + Roact.unmount(instance) + end) + + it("should accept valid props", function() + local spinner = Roact.createElement(LoadingSpinner, { + rotationRate = 1, + anchorPoint = Vector2.new(1, 2), + position = UDim2.new(1, 2, 3, 4), + }) + local instance = Roact.mount(spinner, nil, "LoadingSpinnerTest") + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/ShimmerPanel.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/ShimmerPanel.lua new file mode 100644 index 0000000..24f61f7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/ShimmerPanel.lua @@ -0,0 +1,86 @@ +local Loading = script.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local TextureScroller = require(script.Parent.TextureScroller) + +local validateProps = t.strictInterface({ + AnchorPoint = t.optional(t.Vector2), + LayoutOrder = t.optional(t.integer), + Position = t.optional(t.UDim2), + Size = t.UDim2, + + -- The corner radius of the image's rounded corners. Defaults to UDim(0, 0) for corners with no rounding. + cornerRadius = t.optional(t.UDim), + + -- The loading image that will move across the panel + Image = t.optional(t.string), + + -- The pixel dimensions of the moving image + imageDimensions = t.optional(t.Vector2), + + -- The scale of the moving image + imageScale = t.optional(t.number), + + -- The speed of the moving image + shimmerSpeed = t.optional(t.number), +}) + +local ShimmerPanel = Roact.PureComponent:extend("ShimmerPanel") + +ShimmerPanel.defaultProps = { + cornerRadius = UDim.new(0, 0), + Image = "rbxasset://textures/ui/LuaApp/graphic/shimmer.png", + imageDimensions = Vector2.new(219, 250), + imageScale = 2.5, + shimmerSpeed = 4, +} + +function ShimmerPanel:render() + assert(validateProps(self.props)) + + local anchorPoint = self.props.AnchorPoint + local layoutOrder = self.props.LayoutOrder + local position = self.props.Position + local cornerRadius = self.props.cornerRadius + local shimmerImage = self.props.Image + local shimmerImageDimensions = self.props.imageDimensions + local imageScale = self.props.imageScale + local shimmerSpeed = self.props.shimmerSpeed + local size = self.props.Size + + local imageRectSize = shimmerImageDimensions / imageScale + + local repeatTime = 0 + if shimmerSpeed ~= 0 then + repeatTime = (imageScale + 1) / shimmerSpeed + end + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + return Roact.createElement(TextureScroller, { + anchorPoint = anchorPoint, + layoutOrder = layoutOrder, + backgroundColor3 = theme.PlaceHolder.Color, + backgroundTransparency = theme.PlaceHolder.Transparency, + cornerRadius = cornerRadius, + image = shimmerImage, + imageRectSize = imageRectSize, + imageAnchorPoint = Vector2.new(0, 0.5), + imageTransparency = 0, + imageRectOffsetStart = Vector2.new(shimmerImageDimensions.X, shimmerImageDimensions.Y / 2), + imageRectOffsetEnd = Vector2.new(-imageRectSize.X, shimmerImageDimensions.Y / 2), + imageScrollCycleTime = repeatTime, + position = position, + size = size, + }) + end) +end + +return ShimmerPanel \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/ShimmerPanel.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/ShimmerPanel.spec.lua new file mode 100644 index 0000000..921bcb7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/ShimmerPanel.spec.lua @@ -0,0 +1,22 @@ +return function() + local Loading = script.Parent + local App = Loading.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local ShimmerPanel = require(Loading.ShimmerPanel) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + ShimmerPanel = Roact.createElement(ShimmerPanel, { + Size = UDim2.new(0, 100, 0, 100), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/TextureScroller.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/TextureScroller.lua new file mode 100644 index 0000000..ab1d345 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/TextureScroller.lua @@ -0,0 +1,134 @@ +local RunService = game:GetService("RunService") +local Loading = script.Parent +local App = Loading.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local ExternalEventConnection = require(UIBlox.Utility.ExternalEventConnection) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local function floorVector2(vector2) + return Vector2.new(math.floor(vector2.X), math.floor(vector2.Y)) +end + +local validateProps = t.strictInterface({ + -- The anchor point of the panel + anchorPoint = t.optional(t.Vector2), + -- The layout order of the panel + layoutOrder = t.optional(t.integer), + -- The background color of the panel + backgroundColor3 = t.optional(t.Color3), + -- The background transparency of the panel + backgroundTransparency = t.optional(t.number), + -- The position of the panel + position = t.optional(t.UDim2), + -- The corner radius of the image's rounded corners. Defaults to UDim(0, 0) for corners with no rounding. + cornerRadius = t.optional(t.UDim), + -- The image that will move across the panel + image = t.string, + -- The tranparency of the moving image + imageTransparency = t.optional(t.number), + -- The anchor point of the moving image rect + imageAnchorPoint = t.optional(t.Vector2), + -- The start position of the moving image rect + imageRectOffsetStart = t.Vector2, + -- The end position of the moving image rect + imageRectOffsetEnd = t.Vector2, + -- The take it takes for the image to move from the start positon + -- to the end positions + imageScrollCycleTime = t.optional(t.number), + -- The size of the image rect that is projected onto the panel + imageRectSize = t.Vector2, + -- The size point of the panel + size = t.optional(t.UDim2), +}) + +local TextureScroller = Roact.PureComponent:extend("TextureScroller") + +TextureScroller.defaultProps = { + backgroundTransparency = 1, + cornerRadius = UDim.new(0, 0), + imageAnchorPoint = Vector2.new(0, 0), + imageScrollCycleTime = 1, +} + +function TextureScroller:init() + self.lerpValue = 0 + self.imageRef = Roact.createRef() + + self.renderSteppedCallback = function(dt) + local imageScrollCycleTime = self.props.imageScrollCycleTime + local imageRectOffsetStart = self.props.imageRectOffsetStart + local imageRectOffsetEnd = self.props.imageRectOffsetEnd + local imageRectSize = self.props.imageRectSize + local imageAnchorPoint = self.props.imageAnchorPoint + + local anchoredImageOffsetStart = Vector2.new( + imageRectOffsetStart.X - imageAnchorPoint.X * imageRectSize.X, + imageRectOffsetStart.Y - imageAnchorPoint.Y * imageRectSize.Y) + local anchoredImageOffsetEnd = Vector2.new( + imageRectOffsetEnd.X - imageAnchorPoint.X * imageRectSize.X, + imageRectOffsetEnd.Y - imageAnchorPoint.Y * imageRectSize.Y) + + local lerpPerFrame = 0 + if imageScrollCycleTime ~= 0 then + lerpPerFrame = (dt / imageScrollCycleTime) + end + self.lerpValue = (self.lerpValue + lerpPerFrame) % 1 + if self.imageRef.current then + self.imageRef.current.ImageRectOffset = floorVector2( + anchoredImageOffsetStart:lerp(anchoredImageOffsetEnd, self.lerpValue)) + end + end +end + +function TextureScroller:render() + assert(validateProps(self.props)) + local anchorPoint = self.props.anchorPoint + local backgroundColor = self.props.backgroundColor3 + local backgroundTransparency = self.props.backgroundTransparency + local cornerRadius = self.props.cornerRadius + local image = self.props.image + local imageRectSize = self.props.imageRectSize + local imageTransparency = self.props.imageTransparency + local layoutOrder = self.props.layoutOrder + local position = self.props.position + local size = self.props.size + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BackgroundColor3 = backgroundColor, + BackgroundTransparency = backgroundTransparency, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + Position = position, + Size = size, + }, { + TextureScrollerImage = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = image, + ImageTransparency = imageTransparency, + Size = UDim2.new(1, 0, 1, 0), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(0, 0, imageRectSize.X, imageRectSize.Y), + [Roact.Ref] = self.imageRef, + ImageRectSize = imageRectSize, + }, { + UICorner = cornerRadius ~= UDim.new(0, 0) and Roact.createElement("UICorner", { + CornerRadius = cornerRadius, + }) or nil, + }), + renderStepped = Roact.createElement(ExternalEventConnection, { + callback = self.renderSteppedCallback, + event = RunService.renderStepped, + }), + UICorner = cornerRadius ~= UDim.new(0, 0) and Roact.createElement("UICorner", { + CornerRadius = cornerRadius, + }) or nil, + }) +end + +return TextureScroller \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/TextureScroller.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/TextureScroller.spec.lua new file mode 100644 index 0000000..c6edad0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Loading/TextureScroller.spec.lua @@ -0,0 +1,22 @@ +return function() + local Loading = script.Parent + local App = Loading.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local TextureScroller = require(Loading.TextureScroller) + + it("should create and destroy without errors", function() + local element = Roact.createElement(TextureScroller, { + image = "rbxasset://textures/ui/LuaApp/graphic/shimmer_darkTheme.png", + imageScrollCycleTime = 2, + imageRectOffsetStart = Vector2.new(219, 0), + imageRectOffsetEnd = Vector2.new(-219, 0), + imageRectSize = Vector2.new(219, 250), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/BaseMenu.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/BaseMenu.lua new file mode 100644 index 0000000..6e0d064 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/BaseMenu.lua @@ -0,0 +1,5 @@ +local makeBaseMenu = require(script.Parent.makeBaseMenu) + +local Cell = require(script.Parent.Cell) + +return makeBaseMenu(Cell, "BackgroundUIDefault") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/BaseMenu.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/BaseMenu.spec.lua new file mode 100644 index 0000000..6448a2b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/BaseMenu.spec.lua @@ -0,0 +1,79 @@ +return function() + local Menu = script.Parent + local App = Menu.Parent + local UIBloxRoot = App.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local Images = require(UIBloxRoot.App.ImageSet.Images) + + local BaseMenu = require(script.Parent.BaseMenu) + + describe("lifecycle", function() + it("should mount and unmount without issue with only required props", function() + local element = mockStyleComponent({ + BaseMenu = Roact.createElement(BaseMenu, { + buttonProps = { + { + text = "Test Text", + onActivated = function() + print("test") + end, + } + }, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should mount and unmount without issue with all props", function() + local element = mockStyleComponent({ + BaseMenu = Roact.createElement(BaseMenu, { + buttonProps = { + { + icon = Images["component_assets/circle_17"], + text = "Option 1", + onActivated = function() + print("Option 1 pressed") + end, + }, + { + icon = Images["component_assets/circle_17"], + text = "Option 2", + keyCodeLabel = Enum.KeyCode.Tab, + selected = false, + onActivated = function() + print("Option 2 pressed") + end, + + iconColorOverride = Color3.new(1, 0, 0), + textColorOverride = Color3.new(1, 0, 0), + }, + { + icon = Images["component_assets/circle_17"], + text = "Option 3", + selected = true, + onActivated = function() + print("Option 3 pressed") + end, + }, + }, + + width = UDim.new(0, 300), + position = UDim2.fromScale(0.5, 0.5), + anchorPoint = Vector2.new(0.5, 0.5), + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/Cell.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/Cell.lua new file mode 100644 index 0000000..3a37096 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/Cell.lua @@ -0,0 +1,3 @@ +local makeCell = require(script.Parent.makeCell) + +return makeCell("BackgroundUIDefault") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/Cell.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/Cell.spec.lua new file mode 100644 index 0000000..a87bd36 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/Cell.spec.lua @@ -0,0 +1,103 @@ +return function() + local Menu = script.Parent + local App = Menu.Parent + local UIBloxRoot = App.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local Images = require(UIBloxRoot.App.ImageSet.Images) + + local Cell = require(script.Parent.Cell) + + describe("lifecycle", function() + it("should mount and unmount without issue with only required props", function() + local element = mockStyleComponent({ + Cell = Roact.createElement(Cell, { + text = "Test String", + onActivated = function() + print("Test") + end, + + elementHeight = 50, + hasRoundTop = true, + hasRoundBottom = true, + hasDivider = false, + + layoutOrder = 1, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should mount and unmount without issue with all props", function() + local element = mockStyleComponent({ + Cell = Roact.createElement(Cell, { + icon = Images["component_assets/circle_17"], + text = "Test String", + keyCodeLabel = Enum.KeyCode.Tab, + selected = false, + onActivated = function() + print("Test") + end, + iconColorOverride = Color3.new(1, 0, 0), + textColorOverride = Color3.new(1, 0, 0), + + elementHeight = 60, + hasRoundTop = false, + hasRoundBottom = false, + hasDivider = true, + + layoutOrder = 4, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should correctly hold what it's given", function() + local instanceRef = Roact.createRef() + + local folder = Instance.new("Folder") + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + [Roact.Ref] = instanceRef, + }, { + Cell = Roact.createElement(Cell, { + text = "someSampleText", + icon = Images["component_assets/circle_17"], + onActivated = function() + print("Test") + end, + + elementHeight = 50, + hasRoundTop = true, + hasRoundBottom = true, + hasDivider = false, + + layoutOrder = 13, + }), + }), + }) + + local instance = Roact.mount(element, folder) + + local cell = instanceRef.current.Cell + local icon = cell.LeftAlignedContent.Icon + local text = cell.LeftAlignedContent.Text + + expect(cell.LayoutOrder).to.equal(13) + expect(icon.ImageRectOffset).to.equal(Images["component_assets/circle_17"].ImageRectOffset) + expect(text.Text).to.equal("someSampleText") + + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/ContextualMenu.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/ContextualMenu.lua new file mode 100644 index 0000000..bcedf50 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/ContextualMenu.lua @@ -0,0 +1,5 @@ +local makeContextualMenu = require(script.Parent.makeContextualMenu) + +local BaseMenu = require(script.Parent.BaseMenu) + +return makeContextualMenu(BaseMenu, "BackgroundUIDefault") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/ContextualMenu.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/ContextualMenu.spec.lua new file mode 100644 index 0000000..7c0629d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/ContextualMenu.spec.lua @@ -0,0 +1,92 @@ +return function() + local Menu = script.Parent + local App = Menu.Parent + local UIBloxRoot = App.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local Images = require(UIBloxRoot.App.ImageSet.Images) + + local ContextualMenu = require(script.Parent.ContextualMenu) + local MenuDirection = require(script.Parent.MenuDirection) + + describe("lifecycle", function() + it("should mount and unmount without issue with only required props", function() + local element = mockStyleComponent({ + ContextualMenu = Roact.createElement(ContextualMenu, { + buttonProps = { + { + text = "Test Text", + onActivated = function() + print("test") + end, + } + }, + + open = true, + menuDirection = MenuDirection.Down, + openPositionY = UDim.new(0, 20), + + screenSize = Vector2.new(1024, 1024), + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should mount and unmount without issue with all props", function() + local element = mockStyleComponent({ + ContextualMenu = Roact.createElement(ContextualMenu, { + buttonProps = { + { + icon = Images["component_assets/circle_17"], + text = "Option 1", + onActivated = function() + print("Option 1 pressed") + end, + }, + { + icon = Images["component_assets/circle_17"], + text = "Option 2", + keyCodeLabel = Enum.KeyCode.Tab, + selected = false, + onActivated = function() + print("Option 2 pressed") + end, + + iconColorOverride = Color3.new(1, 0, 0), + textColorOverride = Color3.new(1, 0, 0), + }, + { + icon = Images["component_assets/circle_17"], + text = "Option 3", + selected = true, + onActivated = function() + print("Option 3 pressed") + end, + }, + }, + + open = true, + menuDirection = MenuDirection.Up, + openPositionY = UDim.new(0, 20), + + screenSize = Vector2.new(1024, 1024), + + onDismiss = function() + print("Dismiss") + end, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenu.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenu.lua new file mode 100644 index 0000000..12535ba --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenu.lua @@ -0,0 +1,160 @@ +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) + +local Images = require(UIBlox.App.ImageSet.Images) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) + +local DropdownMenuList = require(UIBlox.App.Menu.DropdownMenuList) +local DropdownMenuCell = require(UIBlox.App.Menu.DropdownMenuCell) + +local BUTTON_IMAGE = "component_assets/circle_17_stroke_1" +local COLLAPSE_IMAGE = "truncate_arrows/actions_truncationCollapse" +local EXPAND_IMAGE = "truncate_arrows/actions_truncationExpand" + +local DropdownMenuComponent = Roact.Component:extend("DropdownMenuComponent") + +DropdownMenuComponent.validateProps = t.strictInterface({ + -- Texts shown by the DropdownCell when no value is selected, i.e. the initial state. + placeholder = t.string, + + -- The callback function when a value is selected, passing the selected value as the parameter. + onChange = t.callback, + + -- Size of the DropdownCell. + size = t.UDim2, + + -- Size of the display area, used to determine the position for dropdown menu and the size of dismiss layer. + screenSize = t.Vector2, + + -- If the component is in error state (shows the error style). + errorState = t.optional(t.boolean), + + -- If the component is disabled. + isDisabled = t.optional(t.boolean), + + -- Array of datas for menu cells + cellDatas = t.array(t.strictInterface({ + -- Icon can either be an Image in a ImageSet or a regular image asset + icon = t.optional(t.union(t.table, t.string)), + + -- value of the cell, also the text displayed in this cell + text = t.string, + + -- is the cell is disabled + disabled = t.optional(t.boolean), + + -- A KeyCode to display a keycode hint for, the display string based on the users keyboard is displayed. + keyCodeLabel = t.optional(t.enum(Enum.KeyCode)), + + -- the color to override the default icon color + iconColorOverride = t.optional(t.Color3), + + -- the color to override the default text color + textColorOverride = t.optional(t.Color3), + })) +}) + +function DropdownMenuComponent:init() + self.rootRef = Roact.createRef() + + self:setState({ + menuOpen = false, + selectedValue = self.props.placeholder, + }) + + self.openMenu = function() + self:setState({ + menuOpen = true + }) + end + + self.closeMenu = function() + self:setState({ + menuOpen = false + }) + end + + self.onSelect = function(cell) + local value = cell.LeftAlignedContent.Text.text + self:setState({ + menuOpen = false, + selectedValue = value, + }) + self.props.onChange(value) + end + + self.mapCellData = function(cellData, cellIndex) + local functionalCell = {} + for i,v in pairs(cellData) do + functionalCell[i] = v + end + functionalCell.onActivated = self.onSelect + functionalCell.selected = self.state.selectedValue == functionalCell.text + return functionalCell + end +end + +function DropdownMenuComponent:render() + local cellDatas = self.props.cellDatas + local functionalCells = Cryo.List.map(cellDatas, self.mapCellData) + + local defaultState = "SecondaryDefault" + local hoverState = "SecondaryOnHover" + local textState = "TextEmphasis" + + if self.state.menuOpen then + hoverState = defaultState + end + + if self.props.errorState then + defaultState = "Alert" + hoverState = "Alert" + end + + return Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + }, { + SpawnButton = Roact.createElement(DropdownMenuCell, { + Size = self.props.size, + buttonImage = Images[BUTTON_IMAGE], + buttonStateColorMap = { + [ControlState.Default] = defaultState, + [ControlState.Hover] = hoverState, + }, + contentStateColorMap = { + [ControlState.Default] = textState, + }, + icon = self.state.menuOpen and Images[COLLAPSE_IMAGE] or Images[EXPAND_IMAGE], + text = self.state.selectedValue, + isDisabled = self.props.isDisabled, + isLoading = false, + isActivated = self.state.menuOpen, + hasContent = self.state.selectedValue ~= self.props.placeholder, + userInteractionEnabled = true, + onActivated = self.openMenu, + }), + + DropdownMenuList = Roact.createElement(DropdownMenuList, { + buttonProps = functionalCells, + + zIndex = 2, + open = self.state.menuOpen, + openPositionY = UDim.new(0, 12), + buttonSize = self.props.size, + + closeBackgroundVisible = false, + screenSize = self.props.screenSize, + + onDismiss = self.closeMenu, + }), + }) +end + +return DropdownMenuComponent diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenu.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenu.spec.lua new file mode 100644 index 0000000..af8a59a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenu.spec.lua @@ -0,0 +1,79 @@ +return function() + local Menu = script.Parent + local App = Menu.Parent + local UIBlox = App.Parent + local Roact = require(UIBlox.Parent.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local Images = require(UIBlox.App.ImageSet.Images) + + local DropdownMenu = require(script.Parent.DropdownMenu) + + describe("lifecycle", function() + it("should mount and unmount without issue with only required props", function() + local element = mockStyleComponent({ + DropdownMenu = Roact.createElement(DropdownMenu, { + placeholder = "Placeholder Text", + onChange = print, + size = UDim2.new(0,250,0,48), + screenSize = Vector2.new(500,500), + cellDatas = { + { + text = "Item 1", + }, + { + text = "Item 2", + } + } + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should mount and unmount without issue with all props", function() + local element = mockStyleComponent({ + DropdownMenu = Roact.createElement(DropdownMenu, { + placeholder = "Placeholder Text", + errorState = false, + isDisabled = true, + onChange = print, + size = UDim2.new(0,250,0,48), + screenSize = Vector2.new(500,500), + cellDatas = { + { + text = "Item 1", + icon = Images["component_assets/circle_17_stroke_1"], + disabled = false, + -- A KeyCode to display a keycode hint for, the display string based on the users keyboard is displayed. + keyCodeLabel = Enum.KeyCode.Up, + + iconColorOverride = Color3.new(0,0,0), + textColorOverride = Color3.new(0,0,0), + }, + { + text = "Item 2", + icon = Images["component_assets/circle_17_stroke_1"], + disabled = false, + -- A KeyCode to display a keycode hint for, the display string based on the users keyboard is displayed. + keyCodeLabel = Enum.KeyCode.Up, + + iconColorOverride = Color3.new(0,0,0), + textColorOverride = Color3.new(0,0,0), + } + } + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenuCell.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenuCell.lua new file mode 100644 index 0000000..a5c4878 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenuCell.lua @@ -0,0 +1,252 @@ +--[[ + Create a generic button that can be themed for different state the background and content. +]] +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Core = UIBlox.Core + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) + +local Interactable = require(Core.Control.Interactable) + +local ControlState = require(Core.Control.Enum.ControlState) + +local withStyle = require(Core.Style.withStyle) +local ImageSetComponent = require(Core.ImageSet.ImageSetComponent) +local ShimmerPanel = require(App.Loading.ShimmerPanel) +local IconSize = require(App.Constant.IconSize) +local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel) + +local validateImage = require(Core.ImageSet.Validator.validateImage) +local ButtonGetContentStyle = require(Core.Button.getContentStyle) + +local CONTENT_PADDING = 5 +local DropdownMenuCell = Roact.PureComponent:extend("DropdownMenuCell") + + + +local function getButtonStyle(contentMap, controlState, style, isActive) + local buttonStyle = ButtonGetContentStyle(contentMap, controlState, style) + if (controlState ~= ControlState.Disabled and + controlState ~= ControlState.Pressed) and + isActive then + buttonStyle.Transparency = 0.5 * buttonStyle.Transparency + 0.5 + end + return buttonStyle +end + +local function getContentStyle(contentMap, controlState, style, isActive, hasContent) + local contentStyle = ButtonGetContentStyle(contentMap, controlState, style) + + if (controlState ~= ControlState.Disabled and + controlState ~= ControlState.Pressed) and + (isActive or not hasContent) then + contentStyle.Transparency = 0.5 * contentStyle.Transparency + 0.5 + end + return contentStyle +end + +function DropdownMenuCell:init() + self:setState({ + controlState = ControlState.Initialize, + }) + + self.onStateChanged = function(oldState, newState) + self:setState({ + controlState = newState, + }) + if self.props.onStateChanged then + self.props.onStateChanged(oldState, newState) + end + end +end + +local colorStateMap = t.interface({ + -- The default state theme color class + [ControlState.Default] = t.string, +}) + +DropdownMenuCell.validateProps = t.interface({ + --The icon of the button + icon = t.optional(validateImage), + + --The text of the button + text = t.optional(t.string), + + --The image being used as the background of the button + buttonImage = validateImage, + + --The theme color class mapping for different button states + buttonStateColorMap = colorStateMap, + + --The theme color class mapping for different content tates + contentStateColorMap = colorStateMap, + + --The theme color class mapping for different text tates + textStateColorMap = t.optional(colorStateMap), + + --The theme color class mapping for different icon tates + iconStateColorMap = t.optional(colorStateMap), + + --Is the button disabled + isDisabled = t.optional(t.boolean), + + --Is the button activated + isActivated = t.optional(t.boolean), + + --Does the button hold a selected value + hasContent = t.optional(t.boolean), + + --Is the button loading + isLoading = t.optional(t.boolean), + + --The activated callback for the button + onActivated = t.callback, + + --THe state change callback for the button + onStateChanged = t.optional(t.callback), + + --A Boolean value that determines whether user events are ignored and sink input + userInteractionEnabled = t.optional(t.boolean), + + -- Note that this component can accept all valid properties of the Roblox ImageButton instance +}) + +DropdownMenuCell.defaultProps = { + isDisabled = false, + isLoading = false, + SliceCenter = Rect.new(8, 8, 9, 9), +} + +function DropdownMenuCell:render() + return withStyle(function(style) + + assert(t.table(style), "Style provider is missing.") + + local currentState = self.state.controlState + + local text = self.props.text + local icon = self.props.icon + local isLoading = self.props.isLoading + local isDisabled = self.props.isDisabled + + local buttonStateColorMap = self.props.buttonStateColorMap + local contentStateColorMap = self.props.contentStateColorMap + local textStateColorMap = self.props.textStateColorMap or contentStateColorMap + local iconStateColorMap = self.props.iconStateColorMap or contentStateColorMap + + if isLoading then + isDisabled = true + end + + local buttonStyle = getButtonStyle(buttonStateColorMap, currentState, style, self.props.isActivated) + local textStyle = text and getContentStyle( + textStateColorMap, + currentState, + style, + self.props.isActivated, + self.props.hasContent) + local iconStyle = icon and getContentStyle( + iconStateColorMap, + currentState, + style, + self.props.isActivated, + true) + local buttonImage = self.props.buttonImage + local fontStyle = style.Font.Header2 + + local buttonContentLayer + if isLoading then + buttonContentLayer = { + isLoadingShimmer = Roact.createElement(ShimmerPanel, { + Size = UDim2.fromScale(1,1), + }) + } + else + buttonContentLayer = self.props[Roact.Children] or { + TextContainer = Roact.createElement("Frame", { + Size = UDim2.fromScale(1,1), + BackgroundTransparency = 1, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, CONTENT_PADDING), + }), + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, 12), + }), + Text = text and Roact.createElement(GenericTextLabel, { + BackgroundTransparency = 1, + Text = text, + fontStyle = fontStyle, + colorStyle = textStyle, + LayoutOrder = 1, + }) or nil, + }), + IconContainer = Roact.createElement("Frame",{ + Size = UDim2.fromScale(1,1), + BackgroundTransparency = 1, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, CONTENT_PADDING), + }), + Padding = Roact.createElement("UIPadding", { + PaddingRight = UDim.new(0, 20), + }), + Icon = icon and Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.fromOffset(IconSize.Regular, IconSize.Regular), + BackgroundTransparency = 1, + Image = icon, + ImageColor3 = iconStyle.Color, + ImageTransparency = iconStyle.Transparency, + LayoutOrder = 2, + }) or nil, + }), + } + end + + local PROPS_FILTER = { + isActivated = Cryo.None, + hasContent = Cryo.None, + icon = Cryo.None, + text = Cryo.None, + buttonImage = Cryo.None, + buttonStateColorMap = Cryo.None, + contentStateColorMap = Cryo.None, + textStateColorMap = Cryo.None, + iconStateColorMap = Cryo.None, + onActivated = Cryo.None, + isLoading = Cryo.None, + [Roact.Children] = Cryo.None, + isDisabled = isDisabled, + onStateChanged = self.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + Image = buttonImage, + ScaleType = Enum.ScaleType.Slice, + ImageColor3 = buttonStyle.Color, + ImageTransparency = buttonStyle.Transparency, + BackgroundTransparency = 1, + AutoButtonColor = false, + [Roact.Event.Activated] = self.props.onActivated, + } + + return Roact.createElement(Interactable, Cryo.Dictionary.join(self.props, PROPS_FILTER), { + ButtonContent = Roact.createElement("Frame", { + Size = UDim2.fromScale(1,1), + BackgroundTransparency = 1, + }, buttonContentLayer) + }) + end) +end +return DropdownMenuCell diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenuList.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenuList.lua new file mode 100644 index 0000000..838c294 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/DropdownMenuList.lua @@ -0,0 +1,172 @@ +-- https://share.goabstract.com/b4b09f34-438a-4d5e-ba7f-8f1f0657f4dd + +local GuiService = game:GetService("GuiService") + +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local BaseMenu = require(script.Parent.BaseMenu) +local validateButtonProps = require(script.Parent.validateButtonProps) + +local dropdownMenuListComponent = Roact.PureComponent:extend("DropdownMenuList") + +dropdownMenuListComponent.validateProps = t.strictInterface({ + buttonProps = validateButtonProps, + + zIndex = t.optional(t.integer), + open = t.boolean, + openPositionY = t.UDim, + + closeBackgroundVisible = t.optional(t.boolean), + screenSize = t.Vector2, + + onDismiss = t.optional(t.callback), + buttonSize = t.UDim2 +}) + +dropdownMenuListComponent.defaultProps = { + zIndex = 2, + closeBackgroundVisible = false, +} + +function dropdownMenuListComponent:init() + self:setState({ + absoluteSize = Vector2.new(0, 0), + absolutePosition = Vector2.new(0, 0), + visible = false + }) + + self.setAbsoluteSize = function(rbx) + self:setState({ + absoluteSize = rbx.AbsoluteSize, + }) + end + + self.setAbsolutePosition = function(rbx) + self:setState({ + absolutePosition = rbx.AbsolutePosition, + }) + end + + self.dismissMenu = function() + if self.state.visible then + self:setState({ + visible = false + }) + self.props.onDismiss() + end + end +end + +function dropdownMenuListComponent:render() + return withStyle(function(stylePalette) + local topCornerInset, _ = GuiService:GetGuiInset() + local absolutePosition = self.state.absolutePosition + topCornerInset + + local anchorPointY = 0 + local menuYScale = 1 + local menuYOffset = self.props.buttonSize.Y + local menuWidth = self.props.buttonSize.X + + if self.state.absolutePosition.Y > self.props.screenSize.Y / 2 then + anchorPointY = 1 + menuYScale = -1 + menuYOffset = UDim.new(0,0) + end + + local menuPositionY + if menuYScale == 1 then + menuPositionY = menuYOffset + self.props.openPositionY + else + menuPositionY = menuYOffset - self.props.openPositionY + end + local menuPosition = UDim2.new(0,0,menuPositionY.Scale,menuPositionY.Offset) + + if self.props.screenSize.X < 640 then + anchorPointY = 1 + menuPosition = UDim2.new( + -self.props.buttonSize.X.Scale / 2, + -absolutePosition.X + self.props.screenSize.X / 2 - self.props.buttonSize.X.Offset / 2, + 0, + self.props.screenSize.Y - absolutePosition.Y -24 + ) + end + + local backgroundTransparency = stylePalette.Theme.Overlay.Transparency + if not self.props.closeBackgroundVisible then + backgroundTransparency = 1 + end + + + + return Roact.createElement("Frame", { + Size = UDim2.fromOffset(self.props.screenSize.X, self.props.screenSize.Y), + BackgroundTransparency = 1, + Visible = self.state.visible, + ZIndex = self.props.zIndex, + + [Roact.Change.AbsoluteSize] = self.setAbsoluteSize, + + [Roact.Change.AbsolutePosition] = self.setAbsolutePosition, + }, { + Background = Roact.createElement("TextButton", { + ZIndex = 1, + Text = "", + BorderSizePixel = 0, + BackgroundColor3 = stylePalette.Theme.Overlay.Color, + BackgroundTransparency = backgroundTransparency, + AutoButtonColor = false, + + Position = UDim2.fromOffset(-absolutePosition.X, -absolutePosition.Y), + Size = UDim2.fromOffset(self.props.screenSize.X, self.props.screenSize.Y), + + [Roact.Event.Activated] = self.dismissMenu, + }), + + PositionFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + Position = menuPosition, + ZIndex = 2, + }, { + BaseMenu = Roact.createElement(BaseMenu, { + buttonProps = self.props.buttonProps, + + width = menuWidth, + position = UDim2.fromScale(0, 0), + anchorPoint = Vector2.new(0, anchorPointY), + }), + }), + }) + end) +end + +function dropdownMenuListComponent:didMount() + if self.props.open then + self:setState({ + visible = true + }) + end +end + +function dropdownMenuListComponent:didUpdate(previousProps, previousState) + if self.props.open ~= previousProps.open then + if self.props.open then + self:setState({ + visible = true + }) + else + self:setState({ + visible = false + }) + end + end +end + +return dropdownMenuListComponent diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/KeyLabel.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/KeyLabel.lua new file mode 100644 index 0000000..6787cff --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/KeyLabel.lua @@ -0,0 +1,195 @@ +local TextService = game:GetService("TextService") +local UserInputService = game:GetService("UserInputService") + +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local Images = require(UIBlox.App.ImageSet.Images) + +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) + +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +-- Additional width for keys with centered text like [Backspace], [Enter], etc +local CENTERED_EXTRA_WIDTH = 14 * 2 + +-- Side padding for off-centered keys [Shift ] +local OFF_CENTER_PADDING = 9 + +local TEXT_CENTER_OFFSET = -1 + +local KEY_LABEL_HEIGHT = 36 + +-- Big and small font sizes for key text. +local BIG = "CaptionHeader" +local SMALL = "Footer" + +local CONTENT_OVERRIDE_MAP = { + [Enum.KeyCode.Escape] = {text = "ESC", fontKey = SMALL, width = 36}, + [Enum.KeyCode.Space] = {text = "Space", width = 92}, + [Enum.KeyCode.LeftShift] = {text = "Shift", width = 66, alignment = Enum.TextXAlignment.Left}, + [Enum.KeyCode.LeftControl] = {text = "Ctrl", width = 66, alignment = Enum.TextXAlignment.Left}, + [Enum.KeyCode.Tab] = {text = "Tab", width = 56}, + + [Enum.KeyCode.LeftSuper] = {text = "Command"}, + [Enum.KeyCode.LeftMeta] = {text = "fn"}, + [Enum.KeyCode.LeftAlt] = {text = "Opt"}, + + [Enum.KeyCode.Tilde] = {text = "~", fontKey = BIG}, + + [Enum.KeyCode.F10] = {fontKey = BIG, width = 36}, + [Enum.KeyCode.F11] = {fontKey = BIG, width = 36}, + + [Enum.KeyCode.Up] = {image = Images["icons/controls/keys/arrowDown"]}, + [Enum.KeyCode.Down] = {image = Images["icons/controls/keys/arrowLeft"]}, + [Enum.KeyCode.Left] = {image = Images["icons/controls/keys/arrowRight"]}, + [Enum.KeyCode.Right] = {image = Images["icons/controls/keys/arrowUp"]}, +} + +local KeyLabel = Roact.PureComponent:extend("KeyLabel") + +KeyLabel.validateProps = t.strictInterface({ + keyCode = t.enum(Enum.KeyCode), + + anchorPoint = t.optional(t.Vector2), + position = t.optional(t.UDim2), + layoutOrder = t.optional(t.integer), + + [Roact.Change.AbsoluteSize] = t.optional(t.callback), +}) + +local cachedKeyCodeStrings = {} +local function getStringForKeyCode(keyCode) + if cachedKeyCodeStrings[keyCode] == nil then + cachedKeyCodeStrings[keyCode] = UserInputService:GetStringForKeyCode(keyCode) + end + return cachedKeyCodeStrings[keyCode] +end + +function KeyLabel:getLabelWidthAndContent(style) + local override = CONTENT_OVERRIDE_MAP[self.props.keyCode] + local font = style.Font + + if override and override.image then + local width = 36 + local content = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = override.image, + ImageColor3 = style.Theme.IconEmphasis.Color, + ImageTransparency = style.Theme.IconEmphasis.Transparency, + Position = UDim2.new(0.5, 0, 0.5, -1), + Size = UDim2.fromOffset(16, 16), + }) + + return width, content + else + local text, fontKey, width, alignment do + if override and override.text then + text = override.text + else + local keyString = getStringForKeyCode(self.props.keyCode) + if keyString and keyString ~= "" then + text = keyString + else + text = self.props.keyCode.Name + end + end + + local textIsShort = text:len() < 3 + + if override and override.fontKey then + fontKey = override.fontKey + else + fontKey = textIsShort and BIG or SMALL + end + + if override and override.width then + width = override.width + elseif textIsShort then + width = 36 + else + local fontStyle = style.Font[fontKey] + + local textSize = fontStyle.RelativeSize * style.Font.BaseSize + local fontType = fontStyle.Font + + local textWidth = TextService:GetTextSize( + text, + textSize, + fontType, + Vector2.new(math.huge, 36) + ).X + + width = textWidth + CENTERED_EXTRA_WIDTH + end + + if override and override.alignment then + alignment = override.alignment + end + end + + local contentFont = font[fontKey] + local contentTextSize = font.BaseSize * contentFont.RelativeSize + + local content = Roact.createElement(GenericTextLabel, { + colorStyle = style.Theme.TextDefault, + fontStyle = contentFont, + + Text = text, + TextSize = contentTextSize, + + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromOffset(0, TEXT_CENTER_OFFSET), + TextXAlignment = alignment, + }) + + return width, content, alignment + end +end + +function KeyLabel:render() + return withStyle(function(style) + local borderTheme = style.Theme.UIEmphasis + + local width, content, alignment = self:getLabelWidthAndContent(style) + + local padding + if alignment then + padding = OFF_CENTER_PADDING + end + + return Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + + ImageTransparency = borderTheme.Transparency, + ImageColor3 = borderTheme.Color, + + Image = Images["icons/controls/keys/key_single"], + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(Vector2.new(9, 7), Vector2.new(26, 26)), + + Size = UDim2.fromOffset(width, KEY_LABEL_HEIGHT), + Position = self.props.Position, + AnchorPoint = self.props.AnchorPoint, + + LayoutOrder = self.props.LayoutOrder, + + [Roact.Change.AbsoluteSize] = self.props[Roact.Change.AbsoluteSize] + }, { + Padding = padding and Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, padding), + PaddingRight = UDim.new(0, padding) + }), + LabelContent = content + }) + end) +end + +return KeyLabel \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/KeyLabel.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/KeyLabel.spec.lua new file mode 100644 index 0000000..b775759 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/KeyLabel.spec.lua @@ -0,0 +1,81 @@ +return function() + local Menu = script.Parent + local App = Menu.Parent + local UIBloxRoot = App.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + + local KeyLabel = require(script.Parent.KeyLabel) + + describe("lifecycle", function() + it("should mount and unmount without issue with only required props", function() + local element = mockStyleComponent({ + KeyLabel = Roact.createElement(KeyLabel, { + keyCode = Enum.KeyCode.X, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should mount and unmount without issue with all props", function() + local element = mockStyleComponent({ + KeyLabel = Roact.createElement(KeyLabel, { + keyCode = Enum.KeyCode.X, + + anchorPoint = Vector2.new(0.5, 0.5), + position = UDim2.fromScale(0.5, 0.5), + layoutOrder = 1, + + [Roact.Change.AbsoluteSize] = function(rbx) + print("Size changed!") + end, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should mount and unmount without issue when using special override keycodes", function() + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + }, { + EscapeKeyLabel = Roact.createElement(KeyLabel, { + keyCode = Enum.KeyCode.Escape, + }), + + SpaceKeyLabel = Roact.createElement(KeyLabel, { + keyCode = Enum.KeyCode.Space, + }), + + LeftControlKeyLabel = Roact.createElement(KeyLabel, { + keyCode = Enum.KeyCode.LeftControl, + }), + + F10KeyLabel = Roact.createElement(KeyLabel, { + keyCode = Enum.KeyCode.F10, + }), + + DownKeyLabel = Roact.createElement(KeyLabel, { + keyCode = Enum.KeyCode.Down, + }), + }) + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/MenuDirection.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/MenuDirection.lua new file mode 100644 index 0000000..791e121 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/MenuDirection.lua @@ -0,0 +1,10 @@ +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate(script.Name, { + "Up", + "Down", +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayBaseMenu.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayBaseMenu.lua new file mode 100644 index 0000000..5de3beb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayBaseMenu.lua @@ -0,0 +1,5 @@ +local makeBaseMenu = require(script.Parent.makeBaseMenu) + +local OverlayCell = require(script.Parent.OverlayCell) + +return makeBaseMenu(OverlayCell, "BackgroundUIContrast") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayCell.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayCell.lua new file mode 100644 index 0000000..4c7166d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayCell.lua @@ -0,0 +1,3 @@ +local makeCell = require(script.Parent.makeCell) + +return makeCell("BackgroundUIContrast") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayContextualMenu.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayContextualMenu.lua new file mode 100644 index 0000000..c05d046 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/OverlayContextualMenu.lua @@ -0,0 +1,5 @@ +local makeContextualMenu = require(script.Parent.makeContextualMenu) + +local OverlayBaseMenu = require(script.Parent.OverlayBaseMenu) + +return makeContextualMenu(OverlayBaseMenu, "BackgroundUIContrast") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/__stories__/CellTypes.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/__stories__/CellTypes.story.lua new file mode 100644 index 0000000..f782680 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/__stories__/CellTypes.story.lua @@ -0,0 +1,162 @@ +local Menu = script.Parent.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local StoryView = require(Packages.StoryComponents.StoryView) +local StoryItem = require(Packages.StoryComponents.StoryItem) + +local Cell = require(Menu.Cell) + +local CellTypesOverviewComponent = Roact.Component:extend("CellTypesOverviewComponent") + +function CellTypesOverviewComponent:render() + return Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + }, { + Overview = Roact.createElement("ScrollingFrame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + }, { + Grid = Roact.createElement("UIGridLayout", { + CellSize = UDim2.fromOffset(300, 200), + FillDirectionMaxCells = 2, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Padding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, 10), + PaddingLeft = UDim.new(0, 20), + }), + + TextOnly = Roact.createElement(StoryItem, { + size = UDim2.fromOffset(300, 128), + layoutOrder = 1, + title = "Text Only", + subTitle = "", + showDivider = true, + }, { + Button = Roact.createElement(Cell, { + text = "Title Case", + onActivated = function() + + end, + + elementHeight = 56, + hasRoundTop = false, + hasRoundBottom = false, + hasDivider = true, + + layoutOrder = 2, + }), + }), + + TextSelected = Roact.createElement(StoryItem, { + size = UDim2.fromOffset(300, 128), + layoutOrder = 2, + title = "Text Selected", + subTitle = "", + showDivider = true, + }, { + Button = Roact.createElement(Cell, { + text = "Title Case", + selected = true, + onActivated = function() + + end, + + elementHeight = 56, + hasRoundTop = false, + hasRoundBottom = false, + hasDivider = true, + + layoutOrder = 2, + }), + }), + + TextAndIcon = Roact.createElement(StoryItem, { + size = UDim2.fromOffset(300, 128), + layoutOrder = 3, + title = "Text and Icon", + subTitle = "", + showDivider = true, + }, { + Button = Roact.createElement(Cell, { + icon = Images["icons/menu/friends"], + text = "Title Case", + onActivated = function() + + end, + + elementHeight = 56, + hasRoundTop = false, + hasRoundBottom = false, + hasDivider = true, + + layoutOrder = 2, + }), + }), + + CellWithKeyLabel = Roact.createElement(StoryItem, { + size = UDim2.fromOffset(300, 128), + layoutOrder = 4, + title = "Cell with KeyLabel", + subTitle = "", + showDivider = true, + }, { + Button = Roact.createElement(Cell, { + icon = Images["icons/menu/friends"], + text = "Title Case", + keyCodeLabel = Enum.KeyCode.E, + onActivated = function() + + end, + + elementHeight = 56, + hasRoundTop = false, + hasRoundBottom = false, + hasDivider = true, + + layoutOrder = 2, + }), + }), + + Disabled = Roact.createElement(StoryItem, { + size = UDim2.fromOffset(300, 128), + layoutOrder = 5, + title = "Disabled", + subTitle = "", + showDivider = true, + }, { + Button = Roact.createElement(Cell, { + icon = Images["icons/menu/friends"], + text = "Title Case", + onActivated = function() + + end, + + elementHeight = 56, + hasRoundTop = false, + hasRoundBottom = false, + hasDivider = true, + + disabled = true, + layoutOrder = 2, + }), + }), + }) + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(CellTypesOverviewComponent), + }), target, "CellTypes") + + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeBaseMenu.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeBaseMenu.lua new file mode 100644 index 0000000..c621797 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeBaseMenu.lua @@ -0,0 +1,197 @@ +-- https://share.goabstract.com/b4b09f34-438a-4d5e-ba7f-8f1f0657f4dd + +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local RoactGamepad = require(Packages.RoactGamepad) +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) +local validateButtonProps = require(script.Parent.validateButtonProps) + +local withSelectionCursorProvider = require(UIBlox.App.SelectionImage.withSelectionCursorProvider) +local CursorKind = require(UIBlox.App.SelectionImage.CursorKind) +local Images = require(Packages.UIBlox.App.ImageSet.Images) +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local MENU_BACKGROUND_ASSET = Images["component_assets/circle_17"] + +local ELEMENT_HEIGHT = 56 +local MAXIMUM_ELEMENTS = 7 +local MAXIMUM_HEIGHT = ELEMENT_HEIGHT * (MAXIMUM_ELEMENTS + 0.5) + +local ROUNDED_CORNER_SIZE = 4 + +local SCROLLBAR_OFFSET = 4 + +local function makeBaseMenu(cellComponent, backgroundThemeKey) + local baseMenuComponent = Roact.PureComponent:extend("BaseMenuFor" ..backgroundThemeKey) + + baseMenuComponent.validateProps = t.strictInterface({ + buttonProps = validateButtonProps, + + width = t.optional(t.UDim), + -- The position can either be passed as a UDim2 or a Roact binding. + position = t.optional(t.union(t.UDim2, t.table)), + anchorPoint = t.optional(t.Vector2), + layoutOrder = t.optional(t.number), + topElementRounded = t.optional(t.boolean), + bottomElementRounded = t.optional(t.boolean), + }) + + baseMenuComponent.defaultProps = { + width = UDim.new(1, 0), + position = UDim2.new(0, 0, 0, 0), + topElementRounded = true, + bottomElementRounded = true, + } + + function baseMenuComponent:init() + if UIBloxConfig.enableExperimentalGamepadSupport then + self.gamepadRefs = RoactGamepad.createRefCache() + end + end + + function baseMenuComponent:render() + local menuHeight = #self.props.buttonProps * ELEMENT_HEIGHT + local needsScrollbar = false + if menuHeight >= MAXIMUM_HEIGHT then + menuHeight = MAXIMUM_HEIGHT + needsScrollbar = true + end + + local children = {} + for index, cellProps in ipairs(self.props.buttonProps) do + local mergedProps = Cryo.Dictionary.join(cellProps, { + elementHeight = ELEMENT_HEIGHT, + hasRoundTop = self.props.topElementRounded and index == 1 and not needsScrollbar, + hasRoundBottom = self.props.bottomElementRounded and index == #self.props.buttonProps and not needsScrollbar, + hasDivider = index < #self.props.buttonProps, + layoutOrder = index, + }) + + if UIBloxConfig.enableExperimentalGamepadSupport then + local cursorKind + if mergedProps.hasRoundBottom and mergedProps.hasRoundTop then + cursorKind = CursorKind.RoundedRectNoInset + elseif mergedProps.hasRoundBottom then + cursorKind = CursorKind.BulletDown + elseif mergedProps.hasRoundTop then + cursorKind = CursorKind.BulletUp + else + cursorKind = CursorKind.Square + end + children["cell " .. index] = withSelectionCursorProvider(function(getSelectionCursor) + return Roact.createElement(RoactGamepad.Focusable.Frame, { + Size = UDim2.new(self.props.width, UDim.new(0, ELEMENT_HEIGHT)), + BackgroundTransparency = 1, + LayoutOrder = index, + + [Roact.Ref] = self.gamepadRefs[index], + NextSelectionUp = index > 1 and self.gamepadRefs[index - 1] or nil, + NextSelectionDown = index < #self.props.buttonProps and self.gamepadRefs[index + 1] or nil, + inputBindings = { + Activated = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonA, cellProps.onActivated), + }, + SelectionImageObject = getSelectionCursor(cursorKind) + }, { + Cell = Roact.createElement(cellComponent, mergedProps) + }) + end) + else + children["cell " .. index] = Roact.createElement(cellComponent, mergedProps) + end + end + + children.layout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + local imageSize = MENU_BACKGROUND_ASSET.ImageRectSize + local imageOffset = MENU_BACKGROUND_ASSET.ImageRectOffset + + local imageRectOffset = imageOffset + local imageWidth = imageSize.X + local halfImageWidth = imageWidth / 2 + + return Roact.createElement("Frame", { + AnchorPoint = self.props.anchorPoint, + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + Size = UDim2.new(self.props.width.Scale, self.props.width.Offset, 0, menuHeight), + Position = self.props.position, + }, { + TopRoundedCorner = needsScrollbar and Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, ROUNDED_CORNER_SIZE), + Position = UDim2.fromScale(0, 0), + + Image = MENU_BACKGROUND_ASSET.Image, + ScaleType = Enum.ScaleType.Slice, + SliceScale = 0.5 / Images.ImagesResolutionScale, + SliceCenter = Rect.new(halfImageWidth - 1, halfImageWidth - 1, halfImageWidth +1, halfImageWidth), + ImageRectSize = Vector2.new(imageWidth, halfImageWidth), + ImageRectOffset = imageRectOffset, + + ImageTransparency = theme[backgroundThemeKey].Transparency, + ImageColor3 = theme[backgroundThemeKey].Color, + }), + + -- We turn off ClipsDescendants on the ScrollingFrame to allow the scroll bar to be offset over the contents. + ClippingFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, needsScrollbar and -(ROUNDED_CORNER_SIZE * 2) or 0), + Position = UDim2.fromScale(0, 0.5), + AnchorPoint = Vector2.new(0, 0.5), + ClipsDescendants = true, + }, { + ScrollingFrame = Roact.createElement("ScrollingFrame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, needsScrollbar and -SCROLLBAR_OFFSET or 0, 1, 0), + BorderSizePixel = 0, + ScrollBarThickness = 4, + ScrollBarImageColor3 = theme.UIEmphasis.Color, + ScrollBarImageTransparency = theme.UIEmphasis.Transparency, + ScrollingDirection = Enum.ScrollingDirection.Y, + CanvasSize = UDim2.new( + 1, + 0, + 0, + #self.props.buttonProps * ELEMENT_HEIGHT + ), + ClipsDescendants = false, + }, children), + }), + + BottomRoundedCorner = needsScrollbar and Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, ROUNDED_CORNER_SIZE), + Position = UDim2.fromScale(0, 1), + AnchorPoint = Vector2.new(0, 1), + + Image = MENU_BACKGROUND_ASSET.Image, + ScaleType = Enum.ScaleType.Slice, + SliceScale = 0.5 / Images.ImagesResolutionScale, + SliceCenter = Rect.new(halfImageWidth - 1, 0, halfImageWidth + 1, 1), + ImageRectSize = Vector2.new(imageWidth, halfImageWidth), + ImageRectOffset = imageOffset + Vector2.new(0, halfImageWidth), + + ImageTransparency = theme[backgroundThemeKey].Transparency, + ImageColor3 = theme[backgroundThemeKey].Color, + }), + }) + end) + end + + return baseMenuComponent +end + +return makeBaseMenu \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeCell.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeCell.lua new file mode 100644 index 0000000..b4d376e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeCell.lua @@ -0,0 +1,324 @@ +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local ImageSetComponent = require(Packages.UIBlox.Core.ImageSet.ImageSetComponent) +local Images = require(Packages.UIBlox.App.ImageSet.Images) +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local divideTransparency = require(UIBlox.Utility.divideTransparency) + +local Controllable = require(Packages.UIBlox.Core.Control.Controllable) +local ControlState = require(Packages.UIBlox.Core.Control.Enum.ControlState) + +local KeyLabel = require(script.Parent.KeyLabel) + +local TEXT_ONLY_PADDING = 24 --Text only padding at the start and end of cells +local ELEMENT_PADDING = 12 --Padding between elements +local SELECTED_ICON_PADDING = 24 --Padding for selected icons at the end of cells +local KEYLABEL_PADDING = 16 --Padding for key labels at the end of cells + +local ICON_SIZE = 36 +local SELECTED_ICON_SIZE = 16 + +local CELL_BACKGROUND_ASSET = Images["component_assets/circle_17"] + +local function makeCell(backgroundThemeKey) + local cellComponent = Roact.PureComponent:extend("CellFor" ..backgroundThemeKey) + + cellComponent.validateProps = t.strictInterface({ + -- Icon can either be an Image in a ImageSet or a regular image asset + icon = t.optional(t.union(t.table, t.string)), + text = t.string, + onActivated = t.callback, + + -- A KeyCode to display a keycode hint for, the display string based on the users keyboard is displayed. + keyCodeLabel = t.optional(t.enum(Enum.KeyCode)), + selected = t.optional(t.boolean), + + iconColorOverride = t.optional(t.Color3), + textColorOverride = t.optional(t.Color3), + + elementHeight = t.integer, + hasRoundTop = t.boolean, + hasRoundBottom = t.boolean, + + hasDivider = t.boolean, + disabled = t.optional(t.boolean), + + layoutOrder = t.integer, + }) + + cellComponent.defaultProps = { + selected = false, + disabled = false, + } + + function cellComponent:init() + self.state = { + controlState = ControlState.Default, + + keyLabelWidth = 0, + } + + self.keyLabelSizeChanged = function(rbx) + self:setState({ + keyLabelWidth = rbx.AbsoluteSize.X, + }) + end + + self.setControlState = function(controlState) + self:setState({ + controlState = controlState, + }) + end + end + + function cellComponent:getImageProperties() + local imageSize = CELL_BACKGROUND_ASSET.ImageRectSize + local imageOffset = CELL_BACKGROUND_ASSET.ImageRectOffset + + local xOffset = 8 * Images.ImagesResolutionScale + local yOffset = 8 * Images.ImagesResolutionScale + local imageCenter = Rect.new(xOffset, yOffset, imageSize.x - xOffset, imageSize.y - yOffset) + + local imageWidth = imageSize.X + local halfImageWidth = math.floor(imageWidth / 2) + + local imageRectSize, imageRectOffset, sliceCenter + + if self.props.hasRoundTop and self.props.hasRoundBottom then + imageRectSize = imageSize + imageRectOffset = imageOffset + sliceCenter = imageCenter + elseif self.props.hasRoundTop then + imageRectSize = Vector2.new(imageWidth, halfImageWidth) + imageRectOffset = imageOffset + sliceCenter = Rect.new(halfImageWidth - 1, halfImageWidth - 1, halfImageWidth +1, halfImageWidth) + elseif self.props.hasRoundBottom then + imageRectSize = Vector2.new(imageWidth, halfImageWidth) + imageRectOffset = imageOffset + Vector2.new(0, halfImageWidth) + sliceCenter = Rect.new(halfImageWidth - 1, 0, halfImageWidth + 1, 1) + else + imageRectSize = Vector2.new(1, 1) + imageRectOffset = imageOffset + Vector2.new(halfImageWidth, halfImageWidth) + sliceCenter = Rect.new(0, 0, 0, 0) + end + + return imageRectSize, imageRectOffset, sliceCenter + end + + function cellComponent:render() + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local font = stylePalette.Font + + local leftPadding = TEXT_ONLY_PADDING + if self.props.icon then + leftPadding = ELEMENT_PADDING + end + local rightPadding = 0 + if self.props.keyCodeLabel then + rightPadding = KEYLABEL_PADDING + elseif self.props.selected then + rightPadding = SELECTED_ICON_PADDING + end + + local overlayTheme = { + Color = Color3.new(1, 1, 1), + Transparency = 1, + } + + if self.state.controlState == ControlState.Pressed then + overlayTheme = theme.BackgroundOnPress + elseif self.state.controlState == ControlState.Hover then + overlayTheme = theme.BackgroundOnHover + end + + local imageRectSize, imageRectOffset, sliceCenter = self:getImageProperties() + + local textLengthOffset = 0 + local textOnly = true + if self.props.icon then + textOnly = false + leftPadding = ELEMENT_PADDING + textLengthOffset = ELEMENT_PADDING + ICON_SIZE + end + + if self.props.selected then + textOnly = false + textLengthOffset = textLengthOffset + SELECTED_ICON_SIZE + SELECTED_ICON_PADDING + end + + if self.props.keyCodeLabel then + textOnly = false + textLengthOffset = textLengthOffset + KEYLABEL_PADDING + self.state.keyLabelWidth + end + + -- Add start and end padding for text. + if textOnly then + textLengthOffset = textLengthOffset + TEXT_ONLY_PADDING * 2 + else + textLengthOffset = textLengthOffset + ELEMENT_PADDING * 2 + end + + local textTheme = theme.TextEmphasis + if self.props.textColorOverride then + textTheme = { + Color = self.props.textColorOverride, + Transparency = theme.TextEmphasis.Transparency + } + end + if self.state.controlState == ControlState.Pressed or self.props.disabled then + textTheme = { + Color = textTheme.Color, + Transparency = divideTransparency(theme.TextEmphasis.Transparency, 2) + } + end + + local cellStyle = theme[backgroundThemeKey] + + return Roact.createElement(Controllable, { + controlComponent = { + component = "ImageButton", + props = { + Size = UDim2.new(1, 0, 0, self.props.elementHeight), + BackgroundTransparency = 1, + + Image = CELL_BACKGROUND_ASSET.Image, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = sliceCenter, + ImageRectSize = imageRectSize, + ImageRectOffset = imageRectOffset, + SliceScale = 1 / Images.ImagesResolutionScale, + + ImageTransparency = cellStyle.Transparency, + ImageColor3 = cellStyle.Color, + AutoButtonColor = false, + LayoutOrder = self.props.layoutOrder, + + BorderSizePixel = 0, + + [Roact.Event.Activated] = self.props.onActivated, + }, + children = { + Divider = Roact.createElement("Frame", { + BackgroundColor3 = theme.Divider.Color, + BackgroundTransparency = theme.Divider.Transparency, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 1), + Position = UDim2.fromScale(0, 1), + AnchorPoint = Vector2.new(0, 1), + Visible = self.props.hasDivider, + }), + + StateOverlay = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + + Image = CELL_BACKGROUND_ASSET.Image, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = sliceCenter, + ImageRectSize = imageRectSize, + ImageRectOffset = imageRectOffset, + SliceScale = 1 / Images.ImagesResolutionScale, + + ImageColor3 = overlayTheme.Color, + ImageTransparency = overlayTheme.Transparency, + BorderSizePixel = 0, + Size = UDim2.fromScale(1, 1), + ZIndex = 2, + }), + + LeftAlignedContent = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + }, { + Layout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, ELEMENT_PADDING), + }), + + LeftPadding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, leftPadding) + }), + + Icon = self.props.icon and Roact.createElement(ImageSetComponent.Label, { + Image = self.props.icon, + Size = UDim2.fromOffset(ICON_SIZE, ICON_SIZE), + BackgroundTransparency = 1, + ImageColor3 = self.props.iconColorOverride or theme.IconEmphasis.Color, + ImageTransparency = divideTransparency( + theme.IconEmphasis.Transparency, + self.props.disabled and 2 or 1 + ), + LayoutOrder = 1, + }), + + Text = Roact.createElement(GenericTextLabel, { + fontStyle = font.Header2, + colorStyle = textTheme, + + BackgroundTransparency = 1, + Size = UDim2.new(1, -textLengthOffset, 1, 0), + Text = self.props.text, + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Left, + LayoutOrder = 2, + }), + }), + + RightAlignedContent = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + }, { + Layout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Right, + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, ELEMENT_PADDING), + }), + + RightPadding = Roact.createElement("UIPadding", { + PaddingRight = UDim.new(0, rightPadding) + }), + + KeyLabel = self.props.keyCodeLabel and Roact.createElement(KeyLabel, { + keyCode = self.props.keyCodeLabel, + + layoutOrder = 2, + + [Roact.Change.AbsoluteSize] = self.keyLabelSizeChanged + }), + + SelectedIcon = Roact.createElement(ImageSetComponent.Label, { + Image = Images["icons/status/success_small"], + Size = UDim2.fromOffset(SELECTED_ICON_SIZE, SELECTED_ICON_SIZE), + LayoutOrder = 1, + BackgroundTransparency = 1, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Visible = self.props.selected + }), + }), + }, + }, + onStateChanged = function(_, newState) + self.setControlState(newState) + end, + isDisabled = self.props.disabled, + }) + end) + end + + return cellComponent +end + +return makeCell \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeContextualMenu.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeContextualMenu.lua new file mode 100644 index 0000000..b38a618 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/makeContextualMenu.lua @@ -0,0 +1,190 @@ +-- https://share.goabstract.com/b4b09f34-438a-4d5e-ba7f-8f1f0657f4dd + +local GuiService = game:GetService("GuiService") + +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Otter = require(Packages.Otter) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) + +local MenuDirection = require(script.Parent.MenuDirection) +local validateButtonProps = require(script.Parent.validateButtonProps) + +local MOTOR_OPTIONS_OPEN = { + frequency = 4, + dampingRatio = 1, +} + +local MOTOR_OPTIONS_CLOSE = { + frequency = 2.6, + dampingRatio = 1, +} + +local CONTEXT_MENU_PADDING = 12 +local LARGE_WIDTH_SIZE = 300 +local LARGE_WIDTH_THRESHOLD = 640 + +local function makeContextualMenu(baseMenuComponent, backgroundThemeKey) + local contextualMenuComponent = Roact.PureComponent:extend("ContextualMenuFor" .. backgroundThemeKey) + + contextualMenuComponent.validateProps = t.strictInterface({ + buttonProps = validateButtonProps, + + zIndex = t.optional(t.integer), + open = t.boolean, + menuDirection = enumerateValidator(MenuDirection), + openPositionY = t.UDim, + + closeBackgroundVisible = t.optional(t.boolean), + screenSize = t.Vector2, + + onDismiss = t.optional(t.callback), + }) + + contextualMenuComponent.defaultProps = { + zIndex = 2, + closeBackgroundVisible = true, + } + + function contextualMenuComponent:init() + self.wasDismissed = false + + self.positionPercentBinding, self.positionPercentBindingUpdate = Roact.createBinding(0) + + self.motor = Otter.createSingleMotor(0) + self.motor:onStep(self.positionPercentBindingUpdate) + self.motor:onComplete(function() + if self.wasDismissed then + self.wasDismissed = false + if self.props.onDismiss then + self.props.onDismiss() + end + end + end) + + self.state = { + absoluteSize = Vector2.new(0, 0), + absolutePosition = Vector2.new(0, 0), + } + + self.positionBinding = self.positionPercentBinding:map(function(positionPercent) + if self.props.menuDirection == MenuDirection.Down then + return UDim2.fromScale(0.5, positionPercent - 1) + else + return UDim2.fromScale(0.5, 1 - positionPercent) + end + end) + + self.visibleBinding = self.positionPercentBinding:map(function(positionPercent) + return positionPercent ~= 0 + end) + end + + function contextualMenuComponent:render() + return withStyle(function(stylePalette) + local contextMenuWidth = UDim.new(1, -CONTEXT_MENU_PADDING * 2) + if self.state.absoluteSize.X > LARGE_WIDTH_THRESHOLD then + contextMenuWidth = UDim.new(0, LARGE_WIDTH_SIZE) + end + + local anchorPointY = 0 + if self.props.menuDirection == MenuDirection.Up then + anchorPointY = 1 + end + + local backgroundTransparency = stylePalette.Theme.Overlay.Transparency + if not self.props.closeBackgroundVisible then + backgroundTransparency = 1 + end + + local topCornerInset, _ = GuiService:GetGuiInset() + local absolutePosition = self.state.absolutePosition + topCornerInset + + return Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + Visible = self.visibleBinding, + ZIndex = self.props.zIndex, + + [Roact.Change.AbsoluteSize] = function(rbx) + self:setState({ + absoluteSize = rbx.AbsoluteSize, + }) + end, + + [Roact.Change.AbsolutePosition] = function(rbx) + self:setState({ + absolutePosition = rbx.AbsolutePosition, + }) + end, + }, { + Background = Roact.createElement("TextButton", { + ZIndex = 1, + Text = "", + BorderSizePixel = 0, + BackgroundColor3 = stylePalette.Theme.Overlay.Color, + BackgroundTransparency = backgroundTransparency, + AutoButtonColor = false, + + Position = UDim2.fromOffset(-absolutePosition.X, -absolutePosition.Y), + Size = UDim2.fromOffset(self.props.screenSize.X, self.props.screenSize.Y), + + [Roact.Event.Activated] = function() + if not self.wasDismissed then + self.wasDismissed = true + self.motor:setGoal(Otter.spring(0, MOTOR_OPTIONS_CLOSE)) + end + end, + }), + + PositionFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + Position = UDim2.new(0, 0, self.props.openPositionY.Scale, self.props.openPositionY.Offset), + ZIndex = 2, + }, { + BaseMenu = Roact.createElement(baseMenuComponent, { + buttonProps = self.props.buttonProps, + + width = contextMenuWidth, + position = self.positionBinding, + anchorPoint = Vector2.new(0.5, anchorPointY), + }), + }), + }) + end) + end + + function contextualMenuComponent:didMount() + if self.props.open then + self.wasDismissed = false + self.motor:setGoal(Otter.spring(1, MOTOR_OPTIONS_OPEN)) + end + end + + function contextualMenuComponent:didUpdate(previousProps, previousState) + if self.props.open ~= previousProps.open then + if self.props.open then + self.wasDismissed = false + self.motor:setGoal(Otter.spring(1, MOTOR_OPTIONS_OPEN)) + else + self.motor:setGoal(Otter.spring(0, MOTOR_OPTIONS_CLOSE)) + end + end + end + + function contextualMenuComponent:wilUnmount() + self.motor:destroy() + end + + return contextualMenuComponent +end + +return makeContextualMenu diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/validateButtonProps.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/validateButtonProps.lua new file mode 100644 index 0000000..0339123 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Menu/validateButtonProps.lua @@ -0,0 +1,21 @@ +local Menu = script.Parent +local App = Menu.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +return t.array(t.strictInterface({ + -- Icon can either be an Image in a ImageSet or a regular image asset + icon = t.optional(t.union(t.table, t.string)), + text = t.string, + onActivated = t.callback, + disabled = t.optional(t.boolean), + + -- A KeyCode to display a keycode hint for, the display string based on the users keyboard is displayed. + keyCodeLabel = t.optional(t.enum(Enum.KeyCode)), + selected = t.optional(t.boolean), + + iconColorOverride = t.optional(t.Color3), + textColorOverride = t.optional(t.Color3), +})) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Navigation/Enum/Placement.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Navigation/Enum/Placement.lua new file mode 100644 index 0000000..0cf89dc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Navigation/Enum/Placement.lua @@ -0,0 +1,10 @@ +local Core = script.Parent.Parent.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate(script.Name, { + "Left", + "Bottom", + "Auto", +}) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Navigation/SystemBar.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Navigation/SystemBar.lua new file mode 100644 index 0000000..b292e66 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Navigation/SystemBar.lua @@ -0,0 +1,308 @@ +local Navigation = script.Parent +local App = Navigation.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local validateImageSetData = require(UIBlox.Core.ImageSet.Validator.validateImageSetData) +local Badge = require(UIBlox.App.Indicator.Badge) +local IconSize = require(UIBlox.App.ImageSet.Enum.IconSize) +local getIconSize = require(UIBlox.App.ImageSet.getIconSize) + +local InteractableList = require(UIBlox.Core.Control.InteractableList) +local withAnimation = require(UIBlox.Core.Animation.withAnimation) + +local Placement = require(script.Parent.Enum.Placement) + +local SPRING_OPTIONS = { + frequency = 3, +} + +local ICON_SIZE = getIconSize(IconSize.Medium) -- 36 +local ICON_PADDING = 4 + +local ITEM_SIZE_X = ICON_SIZE + 2 * ICON_PADDING +local ITEM_SIZE_Y = ITEM_SIZE_X + +local ICON_SIZE_X = ICON_SIZE +local ICON_SIZE_Y = ICON_SIZE_X +local ICON_HOVER_OFFSET_X = ICON_PADDING +local ICON_HOVER_OFFSET_Y = ICON_HOVER_OFFSET_X +local ICON_TRANSPARENCY = 0 +local ICON_TRANSPARENCY_HOVERED = 0.5 +local BADGE_POSITION_X = 18 +local BADGE_POSITION_Y = -2 + +local MAX_SIZE_PORTRAIT_X = 600 +local TAB_SIZE_PORTRAIT_Y = 48 + +local TAB_SIZE_LANDSCAPE_X = 64 +local TAB_SIZE_LANDSCAPE_Y = TAB_SIZE_LANDSCAPE_X +local TAB_PADDING_LANDSCAPE_Y = 4 +local FIRST_TAB_PADDING_LANDSCAPE_Y = 12 + +local ICON_POSITION_X = (ITEM_SIZE_X - ICON_SIZE_X) / 2 +local ICON_POSITION_Y = (ITEM_SIZE_Y - ICON_SIZE_Y) / 2 +local ICON_POSITION_HOVERED_X = ICON_POSITION_X + ICON_HOVER_OFFSET_X +local ICON_POSITION_HOVERED_Y = ICON_POSITION_Y - ICON_HOVER_OFFSET_Y + +local FIRST_ITEM_PADDING_LANDSCAPE_Y = FIRST_TAB_PADDING_LANDSCAPE_Y + (TAB_SIZE_LANDSCAPE_Y - ITEM_SIZE_Y) / 2 +local ITEM_PADDING_LANDSCAPE_Y = TAB_PADDING_LANDSCAPE_Y + (TAB_SIZE_LANDSCAPE_Y - ITEM_SIZE_Y) + +--[[ + A navigation bar that adapts to orientation, screen resize and can be hidden. + it also notifies on safe area (area outside the navbar) change + safe area can change by resizing the window or hiding the systembar +]] + +local SystemBar = Roact.PureComponent:extend("SystemBar") + +SystemBar.validateProps = t.strictInterface({ + -- list of system bar items + itemList = t.array(t.strictInterface({ + -- icon if the item is currently selected + iconOn = validateImageSetData, + -- icon if the item is not selected + iconOff = validateImageSetData, + -- action when clicking on this item + onActivated = t.callback, + -- number to display as badge next to the icon + badgeValue = t.optional(t.integer), + })), + -- index of the currently selected item + selection = t.optional(t.integer), + -- display style: Left, Bottom, Auto (based on screen size) + placement = t.optional(Placement.isEnumValue), + -- hides the system bar (with animation) when true + hidden = t.optional(t.boolean), + -- function({Position, AbsolutePosition, Size, AbsoluteSize}) called when the safe area is resized + onSafeAreaChanged = t.optional(t.callback), + --- options for the main Frame (contains both systembar and safe area) + size = t.optional(t.UDim2), + position = t.optional(t.UDim2), + layoutOrder = t.optional(t.integer), + -- children are placed in a Frame occupying the safe area + [Roact.Children] = t.optional(t.any), +}) + +SystemBar.defaultProps = { + placement = Placement.Auto, +} + +function SystemBar:isPortrait() + if self.props.placement == Placement.Left then + return false + elseif self.props.placement == Placement.Bottom then + return true + else + return self.state.portrait + end +end + +function SystemBar:renderItem(item, state, selected) + local pressed = state == ControlState.Pressed + local hovered = pressed or state == ControlState.Hover + local hasBadge = item.badgeValue and item.badgeValue > 0 + + local positionX = ICON_POSITION_X + local positionY = ICON_POSITION_Y + if hovered then + if self:isPortrait() then + positionY = ICON_POSITION_HOVERED_Y + else + positionX = ICON_POSITION_HOVERED_X + end + end + return withAnimation({ + positionX = positionX, + positionY = positionY, + }, function(values) + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + return Roact.createElement(ImageSetComponent.Label, { + Position = UDim2.fromOffset( + math.floor(values.positionX + 0.5), + math.floor(values.positionY + 0.5) + ), + Size = UDim2.fromOffset(ICON_SIZE_X, ICON_SIZE_Y), + BackgroundTransparency = 1, + Image = selected and item.iconOn or item.iconOff, + ImageColor3 = theme.IconDefault.Color, + ImageTransparency = pressed and ICON_TRANSPARENCY_HOVERED or ICON_TRANSPARENCY, + }, { + Badge = hasBadge and Roact.createElement(Badge, { + position = UDim2.fromOffset(BADGE_POSITION_X, BADGE_POSITION_Y), + value = item.badgeValue, + }) or nil, + }) + end) + end, SPRING_OPTIONS) +end + +function SystemBar:renderPortrait(frameProps, contents) + return withAnimation({ + offset = self.props.hidden and 0 or -TAB_SIZE_PORTRAIT_Y + }, function(values) + return Roact.createElement("Frame", Cryo.Dictionary.join(frameProps, { + Position = UDim2.new(0, 0, 1, math.floor(values.offset + 0.5)), + Size = UDim2.new(1, 0, 0, TAB_SIZE_PORTRAIT_Y), + ZIndex = 99, + }), { + Layout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }), + InnerFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, Cryo.Dictionary.join({ + Constraint = Roact.createElement("UISizeConstraint", { + MaxSize = Vector2.new(MAX_SIZE_PORTRAIT_X, TAB_SIZE_PORTRAIT_Y), + }), + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + Padding = UDim.new(1 / #self.props.itemList, -ITEM_SIZE_X), + }, {}) + }, contents)) + }) + end, SPRING_OPTIONS) +end + +function SystemBar:renderLandscape(frameProps, contents) + return withAnimation({ + offset = self.props.hidden and -TAB_SIZE_LANDSCAPE_X or 0 + }, function(values) + return Roact.createElement("Frame", Cryo.Dictionary.join(frameProps, { + Position = UDim2.new(0, math.floor(values.offset + 0.5), 0, 0), + Size = UDim2.new(0, TAB_SIZE_LANDSCAPE_X, 1, 0), + ZIndex = 99, + }), Cryo.Dictionary.join({ + Padding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, FIRST_ITEM_PADDING_LANDSCAPE_Y), + }), + Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + Padding = UDim.new(0, ITEM_PADDING_LANDSCAPE_Y), + }), + }, contents)) + end, SPRING_OPTIONS) +end + +function SystemBar:renderBackground(...) + if self:isPortrait() then + return self:renderPortrait(...) + else + return self:renderLandscape(...) + end +end + +function SystemBar:renderSafeArea() + local position, size + if self.props.hidden then + position = UDim2.new(0, 0, 0, 0) + size = UDim2.new(1, 0, 1, 0) + elseif self:isPortrait() then + position = UDim2.new(0, 0, 0, 0) + size = UDim2.new(1, 0, 1, -TAB_SIZE_PORTRAIT_Y) + else + position = UDim2.new(0, 64, 0, 0) + size = UDim2.new(1, -TAB_SIZE_LANDSCAPE_X, 1, 0) + end + return Roact.createElement("Frame", { + Position = position, + Size = size, + BackgroundTransparency = 1, + [Roact.Change.AbsoluteSize] = self.onSafeAreaEvent, + [Roact.Change.AbsolutePosition] = self.onSafeAreaEvent, + }, self.props[Roact.Children]) +end + +function SystemBar:renderList(items, renderItem) + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local renderedItems = Cryo.List.map(items, function(item, key) + return renderItem(key) + end) + return Roact.createElement("Frame", { + Position = self.props.position or UDim2.new(0, 0, 0, 0), + Size = self.props.size or UDim2.new(1, 0, 1, 0), + ClipsDescendants = true, + LayoutOrder = self.props.layoutOrder, + [Roact.Change.AbsoluteSize] = function(rbx) + if self.state.portrait and rbx.AbsoluteSize.X > rbx.AbsoluteSize.Y then + self:setState({ + portrait = false, + }) + elseif not self.state.portrait and rbx.AbsoluteSize.X < rbx.AbsoluteSize.Y then + self:setState({ + portrait = true, + }) + end + end + }, { + NavBar = self:renderBackground({ + BackgroundColor3 = theme.BackgroundUIDefault.Color, + BackgroundTransparency = theme.BackgroundUIDefault.Transparency, + BorderSizePixel = 0, + }, renderedItems), + SafeArea = self:renderSafeArea(), + }) + end) +end + +function SystemBar:init() + self:setState({ + portrait = true, + }) + self.onSelectionChanged = function(selection) + if #selection > 0 then + self.props.itemList[selection[1]].onActivated() + end + end + self.onSafeAreaEvent = function(rbx) + if self.props.onSafeAreaChanged then + self.props.onSafeAreaChanged({ + Position = rbx.Position, + AbsolutePosition = rbx.AbsolutePosition, + Size = rbx.Size, + AbsoluteSize = rbx.AbsoluteSize, + }) + end + end +end + +function SystemBar:render() + local itemList = self.props.itemList + local selection = self.props.selection + if selection then + if itemList[selection] == nil then + selection = nil + else + selection = {selection} + end + end + return Roact.createElement(InteractableList, { + itemList = itemList, + selection = selection, + itemSize = UDim2.fromOffset(ITEM_SIZE_X, ITEM_SIZE_Y), + -- since renderList and renderItem depend on state and props, + -- we should always rerender the InteractableList + renderList = function(...) + return self:renderList(...) + end, + renderItem = function(...) + return self:renderItem(...) + end, + onSelectionChanged = self.onSelectionChanged, + }) +end + +return SystemBar diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Navigation/SystemBar.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Navigation/SystemBar.spec.lua new file mode 100644 index 0000000..f9593ce --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Navigation/SystemBar.spec.lua @@ -0,0 +1,45 @@ +return function() + local Navigation = script.Parent + local App = Navigation.Parent + local UIBloxRoot = App.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local Images = require(App.ImageSet.Images) + + local SystemBar = require(script.Parent.SystemBar) + local Placement = require(script.Parent.Enum.Placement) + + it("should create and destroy with minimum props without errors", function() + local element = mockStyleComponent({ + Roact.createElement(SystemBar, { + itemList = {}, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy with all props without errors", function() + local element = mockStyleComponent({ + Roact.createElement(SystemBar, { + itemList = {{ + iconOn = Images["icons/actions/favoriteOn"], + iconOff = Images["icons/actions/favoriteOff"], + onActivated = function() end, + badgeValue = 1, + }}, + selection = 1, + placement = Placement.Left, + hidden = false, + onSafeAreaChanged = function() end, + size = UDim2.new(), + position = UDim2.new(), + layoutOrder = 1, + }, { + Contents = Roact.createElement("Frame", {}, {}) + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/LargePill.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/LargePill.lua new file mode 100644 index 0000000..7c605df --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/LargePill.lua @@ -0,0 +1,159 @@ +local Pill = script.Parent +local App = Pill.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(App.ImageSet.Images) +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) +local GenericButton = require(UIBlox.Core.Button.GenericButton) +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local getContentStyle = require(Pill.getContentStyle) + +local HEIGHT = 48 +local PADDING = 24 + +local BUTTON_STATE_COLOR = { + [ControlState.Default] = "SecondaryDefault", + [ControlState.Hover] = "SecondaryOnHover", +} + +local SELECTED_BUTTON_STATE_COLOR = { + [ControlState.Default] = "UIDefault", +} + +local CONTENT_STATE_COLOR = { + [ControlState.Default] = "SecondaryContent", + [ControlState.Hover] = "SecondaryOnHover", +} + +local SELECTED_CONTENT_STATE_COLOR = { + [ControlState.Default] = "SecondaryOnHover", +} + +local LargePill = Roact.PureComponent:extend("LargePill") + +LargePill.validateProps = t.strictInterface({ + -- Position in an ordered layout + layoutOrder = t.optional(t.number), + -- Width of the pill + width = t.optional(t.UDim), + -- Text shown in the Pill + text = t.optional(t.string), + -- Flag that indicates that the Pill is selected (background is filled) + isSelected = t.optional(t.boolean), + -- Flag that indicates that the Pill is still loading + isLoading = t.optional(t.boolean), + -- Flag that indicates that the Pill is disabled + isDisabled = t.optional(t.boolean), + -- BackgroundColor (used for the loading animation) + backgroundColor = t.optional(t.Color3), + -- Callback function when the Pill is clicked + onActivated = t.callback, + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) + +LargePill.defaultProps = { + layoutOrder = 1, + width = UDim.new(.5, 0), + text = "", + isSelected = false, + isLoading = false, + isDisabled = false, +} + +function LargePill:init() + self.state = { + controlState = ControlState.Initialize + } + + self.onStateChanged = function(oldState, newState) + self:setState({ + controlState = newState, + }) + end +end + +function LargePill:render() + local isSelected = self.props.isSelected + local image = isSelected and Images["component_assets/circle_49"] or Images["component_assets/circle_49_stroke_1"] + + local buttonColors = isSelected and SELECTED_BUTTON_STATE_COLOR or BUTTON_STATE_COLOR + local contentColors = isSelected and SELECTED_CONTENT_STATE_COLOR or CONTENT_STATE_COLOR + + return withStyle(function(style) + return withSelectionCursorProvider(function(getSelectionCursor) + local theme = style.Theme + local currentState = self.state.controlState + local textStyle = getContentStyle(contentColors, currentState, style) + local size = UDim2.new(self.props.width, UDim.new(0, HEIGHT)) + local sliceCenter = Rect.new(24, 24, 25, 25) + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + }, { + Button = Roact.createElement(GenericButton, { + Size = size, + SliceCenter = sliceCenter, + SelectionImageObject = getSelectionCursor(CursorKind.LargePill), + isLoading = self.props.isLoading, + isDisabled = self.props.isDisabled, + text = self.props.text, + onActivated = self.props.onActivated, + buttonImage = image, + buttonStateColorMap = buttonColors, + contentStateColorMap = contentColors, + onStateChanged = self.onStateChanged, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + [Roact.Ref] = self.props[Roact.Ref], + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, PADDING), + }), + Text = Roact.createElement(GenericTextLabel, { + Size = UDim2.new(1, -2 * PADDING, 1, 0), + BackgroundTransparency = 1, + Text = self.props.text, + fontStyle = style.Font.Header2, + colorStyle = textStyle, + LayoutOrder = 2, + TextTruncate = Enum.TextTruncate.AtEnd, + }), + }), + Mask = self.props.isLoading and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = Images["component_assets/circle_49_mask"], + ImageColor3 = self.props.backgroundColor or theme.BackgroundDefault.Color, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = sliceCenter, + Size = size, + ZIndex = 3, + }), + }) + end) + end) +end + +return LargePill \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/LargePill.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/LargePill.spec.lua new file mode 100644 index 0000000..99dc7f2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/LargePill.spec.lua @@ -0,0 +1,39 @@ +return function() + local Pill = script.Parent + local App = Pill.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local LargePill = require(Pill.LargePill) + + it("should create and destroy Large Pill with text without errors", function() + local element = mockStyleComponent({ + pill = Roact.createElement(LargePill, { + text = "Large Pill", + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + it("should create and destroy Large Pill with all properties without errors", function() + local element = mockStyleComponent({ + pill = Roact.createElement(LargePill, { + text = "Large Pill", + width = UDim.new(0.75, 0), + isSelected = true, + isLoading = true, + isDisabled = true, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/SmallPill.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/SmallPill.lua new file mode 100644 index 0000000..855f415 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/SmallPill.lua @@ -0,0 +1,160 @@ +local Pill = script.Parent +local App = Pill.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(App.ImageSet.Images) +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) +local GenericButton = require(UIBlox.Core.Button.GenericButton) +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local getContentStyle = require(Pill.getContentStyle) + +local HEIGHT = 28 +local PADDING = 12 + +local BUTTON_STATE_COLOR = { + [ControlState.Default] = "SecondaryDefault", + [ControlState.Hover] = "SecondaryOnHover", +} + +local SELECTED_BUTTON_STATE_COLOR = { + [ControlState.Default] = "BackgroundOnHover", +} + +local CONTENT_STATE_COLOR = { + [ControlState.Default] = "TextMuted", + [ControlState.Hover] = "TextEmphasis", +} + +local SELECTED_CONTENT_STATE_COLOR = { + [ControlState.Default] = "TextEmphasis", +} + +local SmallPill = Roact.PureComponent:extend("SmallPill") + +SmallPill.validateProps = t.strictInterface({ + -- Position in an ordered layout + layoutOrder = t.optional(t.number), + -- Text shown in the Pill + text = t.optional(t.string), + -- Flag that indicates that the Pill is selected (background is filled) + isSelected = t.optional(t.boolean), + -- Flag that indicates that the Pill is still loading + isLoading = t.optional(t.boolean), + -- Flag that indicates that the Pill is disabled + isDisabled = t.optional(t.boolean), + -- BackgroundColor (used for the loading animation) + backgroundColor = t.optional(t.Color3), + -- Callback function when the Pill is clicked + onActivated = t.callback, + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) + +SmallPill.defaultProps = { + layoutOrder = 1, + text = "", + isSelected = false, + isLoading = false, + isDisabled = false, +} + +function SmallPill:init() + self.state = { + controlState = ControlState.Initialize + } + + self.onStateChanged = function(oldState, newState) + self:setState({ + controlState = newState, + }) + end +end + +function SmallPill:render() + local isSelected = self.props.isSelected + local image = isSelected and Images["component_assets/circle_29"] or Images["component_assets/circle_29_stroke_1"] + local text = self.props.text + + local buttonColors = isSelected and SELECTED_BUTTON_STATE_COLOR or BUTTON_STATE_COLOR + local contentColors = isSelected and SELECTED_CONTENT_STATE_COLOR or CONTENT_STATE_COLOR + local sliceCenter = Rect.new(14, 14, 15, 15) + + return withStyle(function(style) + return withSelectionCursorProvider(function(getSelectionCursor) + local theme = style.Theme + local fontStyle = style.Font.CaptionHeader + local textSize = fontStyle.RelativeSize * style.Font.BaseSize + local textWidth = GetTextSize(text, textSize, fontStyle.Font, Vector2.new()).X + local size = UDim2.new(0, textWidth + PADDING * 2, 0, HEIGHT) + + local currentState = self.state.controlState + local textStyle = getContentStyle(contentColors, currentState, style) + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + }, { + Button = Roact.createElement(GenericButton, { + Size = size, + SliceCenter = sliceCenter, + SelectionImageObject = getSelectionCursor(CursorKind.SmallPill), + isLoading = self.props.isLoading, + isDisabled = self.props.isDisabled, + text = self.props.text, + onActivated = self.props.onActivated, + buttonImage = image, + buttonStateColorMap = buttonColors, + contentStateColorMap = contentColors, + onStateChanged = self.onStateChanged, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + [Roact.Ref] = self.props[Roact.Ref], + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, PADDING), + }), + Text = Roact.createElement(GenericTextLabel, { + BackgroundTransparency = 1, + Text = self.props.text, + fontStyle = fontStyle, + colorStyle = textStyle, + LayoutOrder = 2, + }) + }), + Mask = self.props.isLoading and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = Images["component_assets/circle_29_mask"], + ImageColor3 = self.props.backgroundColor or theme.BackgroundDefault.Color, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = sliceCenter, + Size = size, + ZIndex = 3, + }), + }) + end) + end) +end + +return SmallPill \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/SmallPill.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/SmallPill.spec.lua new file mode 100644 index 0000000..f57a385 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/SmallPill.spec.lua @@ -0,0 +1,38 @@ +return function() + local Pill = script.Parent + local App = Pill.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local SmallPill = require(Pill.SmallPill) + + it("should create and destroy Small Pill with text without errors", function() + local element = mockStyleComponent({ + pill = Roact.createElement(SmallPill, { + text = "Small Pill", + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + it("should create and destroy Small Pill with all properties without errors", function() + local element = mockStyleComponent({ + pill = Roact.createElement(SmallPill, { + text = "Large Pill", + isSelected = true, + isLoading = true, + isDisabled = true, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/getContentStyle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/getContentStyle.lua new file mode 100644 index 0000000..808c02d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Pill/getContentStyle.lua @@ -0,0 +1,23 @@ +local Pill = script.Parent +local Apps = Pill.Parent +local UIBlox = Apps.Parent + +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) + +-- Copied from GenericButton. TODO: factor it out +return function(contentMap, controlState, style) + local contentThemeClass = contentMap[controlState] + or contentMap[ControlState.Default] + + local contentStyle = { + Color = style.Theme[contentThemeClass].Color, + Transparency = style.Theme[contentThemeClass].Transparency, + } + + --Based on the design specs, the disabled and pressed state is 0.5 * alpha value + if controlState == ControlState.Disabled or + controlState == ControlState.Pressed then + contentStyle.Transparency = 0.5 * contentStyle.Transparency + 0.5 + end + return contentStyle +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/BulletDown.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/BulletDown.lua new file mode 100644 index 0000000..01d9d3e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/BulletDown.lua @@ -0,0 +1,27 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local INSET_ADJUSTMENT = 2 +local ASSET_NAME = "component_assets/bulletDown_17_stroke_3" + +return function(props) + return withStyle(function(style) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + ImageColor3 = style.Theme.SelectionCursor.Color, + BackgroundTransparency = 1, + Size = UDim2.new(1, INSET_ADJUSTMENT * 2, 1, INSET_ADJUSTMENT * 2), + Position = UDim2.fromOffset(-INSET_ADJUSTMENT, -INSET_ADJUSTMENT), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + + [Roact.Ref] = props[Roact.Ref], + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/BulletUp.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/BulletUp.lua new file mode 100644 index 0000000..66e8fe9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/BulletUp.lua @@ -0,0 +1,27 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local INSET_ADJUSTMENT = 2 +local ASSET_NAME = "component_assets/bulletUp_17_stroke_3" + +return function(props) + return withStyle(function(style) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + ImageColor3 = style.Theme.SelectionCursor.Color, + BackgroundTransparency = 1, + Size = UDim2.new(1, INSET_ADJUSTMENT * 2, 1, INSET_ADJUSTMENT * 2), + Position = UDim2.fromOffset(-INSET_ADJUSTMENT, -INSET_ADJUSTMENT), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + + [Roact.Ref] = props[Roact.Ref], + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/InputFields.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/InputFields.lua new file mode 100644 index 0000000..83ca545 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/InputFields.lua @@ -0,0 +1,27 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local ASSET_NAME = "component_assets/circle_22_stroke_3" +local PADDING = 14 + +return function(props) + return withStyle(function(style) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + ImageColor3 = style.Theme.SelectionCursor.Color, + BackgroundTransparency = 1, + Size = UDim2.new(1, PADDING, 1, 0), + Position = UDim2.new(0, -PADDING / 2, 0, 0), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(11, 11, 12, 12), + + [Roact.Ref] = props[Roact.Ref], + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/LargePill.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/LargePill.lua new file mode 100644 index 0000000..0dc62b7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/LargePill.lua @@ -0,0 +1,25 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local ASSET_NAME = "component_assets/circle_52_stroke_3" + +return function(props) + return withStyle(function(style) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + ImageColor3 = style.Theme.SelectionCursor.Color, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(26, 26, 27, 27), + + [Roact.Ref] = props[Roact.Ref], + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/NavHighlight.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/NavHighlight.lua new file mode 100644 index 0000000..55bfd58 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/NavHighlight.lua @@ -0,0 +1,23 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local NAV_HIGHLIGHT_HEIGHT = 3 + +return function(props) + return withStyle(function(style) + return Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0, 1), + Position = UDim2.new(0, 0, 1, -NAV_HIGHLIGHT_HEIGHT), + Size = UDim2.new(1, 0, 0, NAV_HIGHLIGHT_HEIGHT), + BorderSizePixel = 1, + BackgroundColor3 = style.Theme.SelectionCursor.Color, + BorderColor3 = style.Theme.SelectionCursor.Color, + BackgroundTransparency = style.Theme.SelectionCursor.Transparency, + + [Roact.Ref] = props[Roact.Ref] + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/RoundedRect.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/RoundedRect.lua new file mode 100644 index 0000000..7e4885d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/RoundedRect.lua @@ -0,0 +1,27 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local INSET_ADJUSTMENT = 6 +local ASSET_NAME = "component_assets/circle_17_stroke_3" + +return function(props) + return withStyle(function(style) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + ImageColor3 = style.Theme.SelectionCursor.Color, + BackgroundTransparency = 1, + Size = UDim2.new(1, INSET_ADJUSTMENT * 2, 1, INSET_ADJUSTMENT * 2), + Position = UDim2.new(0, -INSET_ADJUSTMENT, 0, -INSET_ADJUSTMENT), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + + [Roact.Ref] = props[Roact.Ref], + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/RoundedRectNoInset.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/RoundedRectNoInset.lua new file mode 100644 index 0000000..1e7fb75 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/RoundedRectNoInset.lua @@ -0,0 +1,25 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local ASSET_NAME = "component_assets/circle_17_stroke_3" + +return function(props) + return withStyle(function(style) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + ImageColor3 = style.Theme.SelectionCursor.Color, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + + [Roact.Ref] = props[Roact.Ref], + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/SelectedKnob.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/SelectedKnob.lua new file mode 100644 index 0000000..2d18e68 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/SelectedKnob.lua @@ -0,0 +1,25 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local ASSET_NAME = "component_assets/circle_42_stroke_3" +local ASSET_SIZE = 42 + +return function(props) + return withStyle(function(style) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + ImageColor3 = style.Theme.SelectionCursor.Color, + BackgroundTransparency = 1, + Size = UDim2.fromOffset(ASSET_SIZE, ASSET_SIZE), + Position = UDim2.new(0.5, -ASSET_SIZE / 2, 0.5, -ASSET_SIZE / 2), + + [Roact.Ref] = props[Roact.Ref], + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/SelectionCell.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/SelectionCell.lua new file mode 100644 index 0000000..c253b08 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/SelectionCell.lua @@ -0,0 +1,28 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local INSET_ADJUSTMENT = 2 +local PADDING = 50 +local ASSET_NAME = "component_assets/square_7_stroke_3" + +return function(props) + return withStyle(function(style) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + ImageColor3 = style.Theme.SelectionCursor.Color, + BackgroundTransparency = 1, + Size = UDim2.new(1, INSET_ADJUSTMENT * 2 + PADDING, 1, INSET_ADJUSTMENT * 2), + Position = UDim2.fromOffset(-INSET_ADJUSTMENT - PADDING / 2, -INSET_ADJUSTMENT), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(3.5, 3.5, 3.5, 3.5), + + [Roact.Ref] = props[Roact.Ref], + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/SkinToneCircle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/SkinToneCircle.lua new file mode 100644 index 0000000..816cfd0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/SkinToneCircle.lua @@ -0,0 +1,23 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local ASSET_NAME = "component_assets/circle_69_stroke_3" + +return function(props) + return withStyle(function(style) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + ImageColor3 = style.Theme.SelectionCursor.Color, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + + [Roact.Ref] = props[Roact.Ref], + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/SmallPill.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/SmallPill.lua new file mode 100644 index 0000000..2f5983b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/SmallPill.lua @@ -0,0 +1,25 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local ASSET_NAME = "component_assets/circle_30_stroke_3" + +return function(props) + return withStyle(function(style) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + ImageColor3 = style.Theme.SelectionCursor.Color, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(15, 15, 16, 16), + + [Roact.Ref] = props[Roact.Ref], + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/Square.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/Square.lua new file mode 100644 index 0000000..b793f4f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/Square.lua @@ -0,0 +1,27 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local INSET_ADJUSTMENT = 2 +local ASSET_NAME = "component_assets/square_7_stroke_3" + +return function(props) + return withStyle(function(style) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + ImageColor3 = style.Theme.SelectionCursor.Color, + BackgroundTransparency = 1, + Size = UDim2.new(1, INSET_ADJUSTMENT * 2, 1, INSET_ADJUSTMENT * 2), + Position = UDim2.fromOffset(-INSET_ADJUSTMENT, -INSET_ADJUSTMENT), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(3.5, 3.5, 3.5, 3.5), + + [Roact.Ref] = props[Roact.Ref], + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/Toggle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/Toggle.lua new file mode 100644 index 0000000..6f430e8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/Toggle.lua @@ -0,0 +1,25 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local ASSET_NAME = "component_assets/circle_26_stroke_3" + +return function(props) + return withStyle(function(style) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + ImageColor3 = style.Theme.SelectionCursor.Color, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(13, 13, 14, 14), + + [Roact.Ref] = props[Roact.Ref], + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/UnselectedKnob.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/UnselectedKnob.lua new file mode 100644 index 0000000..7b1656d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/Components/UnselectedKnob.lua @@ -0,0 +1,25 @@ +local UIBloxRoot = script.Parent.Parent.Parent.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBloxRoot.Core.Style.withStyle) + +local ImageSetComponent = require(UIBloxRoot.Core.ImageSet.ImageSetComponent) +local Images = require(UIBloxRoot.App.ImageSet.Images) + +local ASSET_NAME = "component_assets/circle_52_stroke_3" +local ASSET_SIZE = 52 + +return function(props) + return withStyle(function(style) + return Roact.createElement(ImageSetComponent.Label, { + Image = Images[ASSET_NAME], + ImageColor3 = style.Theme.SelectionCursor.Color, + BackgroundTransparency = 1, + Size = UDim2.fromOffset(ASSET_SIZE, ASSET_SIZE), + Position = UDim2.new(0.5, -ASSET_SIZE / 2, 0.5, -ASSET_SIZE / 2), + + [Roact.Ref] = props[Roact.Ref], + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/CursorKind.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/CursorKind.lua new file mode 100644 index 0000000..5b867ad --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/CursorKind.lua @@ -0,0 +1,35 @@ +local Packages = script.Parent.Parent.Parent.Parent + +local enumerate = require(Packages.enumerate) + +local RoundedRectCursor = require(script.Parent.Components.RoundedRect) +local RoundedRectNoInsetCursor = require(script.Parent.Components.RoundedRectNoInset) +local SmallPillCursor = require(script.Parent.Components.SmallPill) +local LargePillCursor = require(script.Parent.Components.LargePill) +local SelectedKnobCursor = require(script.Parent.Components.SelectedKnob) +local UnselectedKnobCursor = require(script.Parent.Components.UnselectedKnob) +local NavHighlightCursor = require(script.Parent.Components.NavHighlight) +local SkinToneCircleCursor = require(script.Parent.Components.SkinToneCircle) +local SquareCursor = require(script.Parent.Components.Square) +local ToggleCursor = require(script.Parent.Components.Toggle) +local InputFieldsCursor = require(script.Parent.Components.InputFields) +local BulletUpCursor = require(script.Parent.Components.BulletUp) +local BulletDownCursor = require(script.Parent.Components.BulletDown) +local SelectionCellCursor = require(script.Parent.Components.SelectionCell) + +return enumerate(script.Name, { + RoundedRect = RoundedRectCursor, + RoundedRectNoInset = RoundedRectNoInsetCursor, + SmallPill = SmallPillCursor, + LargePill = LargePillCursor, + SelectedKnob = SelectedKnobCursor, + UnselectedKnob = UnselectedKnobCursor, + NavHighlight = NavHighlightCursor, + SkinToneCircle = SkinToneCircleCursor, + Square = SquareCursor, + Toggle = ToggleCursor, + InputFields = InputFieldsCursor, + BulletUp = BulletUpCursor, + BulletDown = BulletDownCursor, + SelectionCell = SelectionCellCursor, +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionCursorProvider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionCursorProvider.lua new file mode 100644 index 0000000..31abe06 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionCursorProvider.lua @@ -0,0 +1,61 @@ +local Packages = script.Parent.Parent.Parent.Parent + +local Roact = require(Packages.Roact) +local RoactGamepad = require(Packages.RoactGamepad) +local Cryo = require(Packages.Cryo) + +local CursorKind = require(script.Parent.CursorKind) +local SelectionImageContext = require(script.Parent.SelectionImageContext) + +local SelectionCursorProvider = Roact.PureComponent:extend("SelectionCursorProvider") + +function SelectionCursorProvider:init() + self.refs = RoactGamepad.createRefCache() + self:setState({ + mountedCursors = {} + }) + + self.getSelectionCursor = function(cursorKind) + assert(CursorKind.isEnumValue(cursorKind), + ("Invalid arg #1: expected a CursorKind enum variant, got %s"):format(tostring(cursorKind))) + + if self.state.mountedCursors[cursorKind] == nil then + self:setState(function(state) + return { + mountedCursors = Cryo.Dictionary.join(state.mountedCursors, { + [cursorKind] = true, + }) + } + end) + end + + -- Note that we return the ref here even if it shouldn't exist yet; + -- thanks to the refCache, we know that the ref created here is the same + -- one that will be ultimately assigned to the cursor component once the + -- setState completes and the component does re-render + return self.refs[cursorKind] + end +end + +function SelectionCursorProvider:render() + local cursors = {} + for cursorKind, _ in pairs(self.state.mountedCursors) do + local CursorComponent = cursorKind.rawValue() + local key = tostring(CursorComponent) + cursors[key] = Roact.createElement(CursorComponent, { + [Roact.Ref] = self.refs[cursorKind], + }) + end + + return Roact.createElement(SelectionImageContext.Provider, { + value = self.getSelectionCursor, + }, { + CursorContainer = Roact.createElement("Frame", { + Size = UDim2.new(0, 0, 0, 0), + Visible = false, + }, cursors), + Children = Roact.createFragment(self.props[Roact.Children]), + }) +end + +return SelectionCursorProvider diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionCursorProvider.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionCursorProvider.spec.lua new file mode 100644 index 0000000..d210e76 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionCursorProvider.spec.lua @@ -0,0 +1,92 @@ +return function() + local Packages = script.Parent.Parent.Parent.Parent + + local Roact = require(Packages.Roact) + local UIBlox = require(Packages.UIBlox) + + local SelectionCursorProvider = require(script.Parent.SelectionCursorProvider) + local SelectionImageContext = require(script.Parent.SelectionImageContext) + local CursorKind = require(script.Parent.CursorKind) + local testStyle = require(Packages.UIBlox.App.Style.Validator.TestStyle) + local StyleProvider = UIBlox.Core.Style.Provider + + describe("Managed singleton cache of UI elements for use as selection cursors", function() + it("should provide a ref that refers to an ImageLabel", function() + local ref + local function CaptureRef() + return Roact.createElement(SelectionImageContext.Consumer, { + render = function(getSelectionCursor) + ref = getSelectionCursor(CursorKind.RoundedRect) + return nil + end, + }) + end + + local tree = Roact.mount(Roact.createElement(StyleProvider, { + style = testStyle, + }, { + SelectionCursorProvider = Roact.createElement(SelectionCursorProvider, {}, { + RefCapturer = Roact.createElement(CaptureRef) + }) + })) + + expect(typeof(ref.getValue)).to.equal("function") + expect(typeof(ref:getValue())).to.equal("Instance") + expect(ref:getValue():IsA("ImageLabel")).to.equal(true) + + Roact.unmount(tree) + end) + + it("should return the same object on multiple calls", function() + local capturedRefs = {} + local function CaptureRef(props) + return Roact.createElement(SelectionImageContext.Consumer, { + render = function(getSelectionCursor) + capturedRefs[props.key] = getSelectionCursor(CursorKind.RoundedRect) + return nil + end, + }) + end + + local tree = Roact.mount(Roact.createElement(StyleProvider, { + style = testStyle, + }, { + SelectionCursorProvider = Roact.createElement(SelectionCursorProvider, {}, { + RefCapturer1 = Roact.createElement(CaptureRef, { + key = "ref1", + }), + RefCapturer2 = Roact.createElement(CaptureRef, { + key = "ref2", + }), + }) + })) + + expect(capturedRefs.ref1).to.be.ok() + expect(capturedRefs.ref2).to.be.ok() + expect(capturedRefs.ref1).to.equal(capturedRefs.ref2) + + Roact.unmount(tree) + end) + + it("Should throw an error when invoked with an invalid argument", function() + local badAssetName = "doesn't exist" + local function BadCursor(props) + return Roact.createElement(SelectionImageContext.Consumer, { + render = function(getSelectionCursor) + getSelectionCursor(badAssetName) + return nil + end, + }) + end + + local ok, err = pcall(function() + Roact.mount(Roact.createElement(SelectionCursorProvider, {}, { + BadCursor = Roact.createElement(BadCursor), + })) + end) + + expect(ok).to.equal(false) + expect(err:find(badAssetName)).to.be.ok() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionImageContext.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionImageContext.lua new file mode 100644 index 0000000..53b1fdf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/SelectionImageContext.lua @@ -0,0 +1,9 @@ +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) + +local SelectionImageContext = Roact.createContext(function() + -- By default, provides no cursors + return nil +end) + +return SelectionImageContext \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/withSelectionCursorProvider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/withSelectionCursorProvider.lua new file mode 100644 index 0000000..d0b470b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/SelectionImage/withSelectionCursorProvider.lua @@ -0,0 +1,13 @@ +local Packages = script.Parent.Parent.Parent.Parent + +local Roact = require(Packages.Roact) + +local SelectionImageContext = require(script.Parent.SelectionImageContext) + +local function SelectionCursorConsumer(renderWithCursor) + return Roact.createElement(SelectionImageContext.Consumer, { + render = renderWithCursor, + }) +end + +return SelectionCursorConsumer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/ContextualSlider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/ContextualSlider.lua new file mode 100644 index 0000000..462dfe9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/ContextualSlider.lua @@ -0,0 +1,3 @@ +local makeAppOneKnobSlider = require(script.Parent.makeAppOneKnobSlider) + +return makeAppOneKnobSlider("ContextualPrimaryDefault") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/ContextualSlider.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/ContextualSlider.spec.lua new file mode 100644 index 0000000..66d0568 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/ContextualSlider.spec.lua @@ -0,0 +1,47 @@ +return function() + local Slider = script.Parent + local App = Slider.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local ContextualSlider = require(script.Parent.ContextualSlider) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + contextualSlider = Roact.createElement(ContextualSlider, { + value = 10, + min = 0, + max = 100, + onValueChanged = function() end, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with all props", function() + local element = mockStyleComponent({ + contextualSlider = Roact.createElement(ContextualSlider, { + value = 10, + min = 0, + max = 100, + stepInterval = 1, + onValueChanged = function() end, + onDragStart = function() end, + onDragEnd = function() end, + isDisabled = false, + textInputEnabled = true, + + width = UDim.new(1, 1), + position = UDim2.new(1, 1, 1, 1), + anchorPoint = Vector2.new(0.5, 0.5), + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SliderTextInput.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SliderTextInput.lua new file mode 100644 index 0000000..d964468 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SliderTextInput.lua @@ -0,0 +1,165 @@ +-- Specialized TextBox for handling the text boxes that sliders can have. + +local UserInputService = game:GetService("UserInputService") + +local Packages = script.Parent.Parent.Parent.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local ImageSetComponent = require(Packages.UIBlox.Core.ImageSet.ImageSetComponent) +local Images = require(Packages.UIBlox.App.ImageSet.Images) +local withStyle = require(Packages.UIBlox.Style.withStyle) + +local divideTransparency = require(Packages.UIBlox.Utility.divideTransparency) + +local ExternalEventConnection = require(Packages.UIBlox.Utility.ExternalEventConnection) + +local SliderTextInput = Roact.PureComponent:extend("SliderTextInput") +SliderTextInput.validateProps = t.strictInterface({ + position = t.optional(t.union(t.UDim2, t.table)), + anchorPoint = t.optional(t.Vector2), + value = t.number, + min = t.number, + max = t.number, + disabled = t.optional(t.boolean), + stepInterval = t.numberPositive, + onValueChanged = t.callback, + layoutOrder = t.optional(t.integer), +}) + +SliderTextInput.defaultProps = { + disabled = false, +} + +function SliderTextInput:init() + self.textBoxRef = Roact.createRef() +end + +function SliderTextInput:render() + return withStyle(function(style) + local transparencyDivisor = self.props.disabled and 2 or 1 + local textTransparency = divideTransparency( + style.Theme.TextDefault.Transparency, + transparencyDivisor) + + local borderTransparency = divideTransparency( + style.Theme.Divider.Transparency, + transparencyDivisor) + + local backgroundTransparency = divideTransparency( + style.Theme.BackgroundUIContrast.Transparency, + transparencyDivisor) + + return Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = Images["component_assets/circle_16"], + ImageColor3 = style.Theme.BackgroundUIContrast.Color, + ImageTransparency = backgroundTransparency, + Position = self.props.position, + AnchorPoint = self.props.anchorPoint, + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.fromOffset(56, 36), + SliceCenter = Rect.new(8, 8, 8, 8), + LayoutOrder = self.props.layoutOrder, + }, { + Border = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = Images["component_assets/circle_17_stroke_1"], + ImageColor3 = style.Theme.Divider.Color, + ImageTransparency = borderTransparency, + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.fromScale(1, 1), + SliceCenter = Rect.new(8, 8, 8, 8), + }), + TextBox = Roact.createElement("TextBox", { + [Roact.Ref] = self.textBoxRef, + BackgroundTransparency = 1, + ClearTextOnFocus = false, + Font = style.Font.Body.Font, + TextSize = style.Font.Body.RelativeSize * style.Font.BaseSize, + TextColor3 = style.Theme.TextDefault.Color, + TextTransparency = textTransparency, + Size = UDim2.fromScale(1, 1), + Text = tostring(self.props.value), + TextScaled = true, + TextEditable = not self.props.disabled, + ZIndex = 2, + + [Roact.Event.Focused] = self.props.disabled and function(rbx) + rbx:ReleaseFocus() + end or nil, + [Roact.Event.FocusLost] = function(rbx, enterPressed) + if not enterPressed then + return + end + + local newValue = tonumber(rbx.Text) + + if newValue == nil then + rbx.Text = tostring(self.props.value) + return + end + + newValue = math.clamp( + math.floor(newValue / self.props.stepInterval + 0.5) * self.props.stepInterval, + self.props.min, + self.props.max) + + self.props.onValueChanged(newValue) + end, + }, { + TextSizeConstraint = Roact.createElement("UITextSizeConstraint", { + MinTextSize = style.Font.Body.RelativeMinSize * style.Font.BaseSize, + MaxTextSize = style.Font.Body.RelativeSize * style.Font.BaseSize, + }) + }), + UserInputConnection = not self.props.disabled and Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputBegan, + callback = function(input, gameProcessed) + if input.UserInputType ~= Enum.UserInputType.Keyboard then + return + end + + if UserInputService:GetFocusedTextBox() ~= self.textBoxRef.current then + return + end + + local direction = 0 + if input.KeyCode == Enum.KeyCode.Up then + direction = 1 + elseif input.KeyCode == Enum.KeyCode.Down then + direction = -1 + end + + if UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) + or UserInputService:IsKeyDown(Enum.KeyCode.RightShift) then + direction = direction * 10 + end + + if direction ~= 0 then + local rawNewValue = self.props.value + self.props.stepInterval * direction + local newValue = math.clamp( + math.floor(rawNewValue / self.props.stepInterval + 0.5) * self.props.stepInterval, + self.props.min, + self.props.max) + + if newValue ~= self.props.value then + self.props.onValueChanged(newValue) + end + end + end, + }) + }) + end) +end + +function SliderTextInput:didMount() + -- Set the textbox's TextInputType here, because the property is RobloxScript + -- only and not accessible in Horsecat or some tests. + pcall(function() + self.textBoxRef.current.TextInputType = Enum.TextInputType.Number + end) +end + +return SliderTextInput diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SliderTextInput.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SliderTextInput.spec.lua new file mode 100644 index 0000000..bd36e5f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SliderTextInput.spec.lua @@ -0,0 +1,42 @@ +return function() + local Slider = script.Parent + local App = Slider.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local SliderTextInput = require(script.Parent.SliderTextInput) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + sliderTextInput = Roact.createElement(SliderTextInput, { + value = 10, + min = 0, + max = 100, + stepInterval = 1, + onValueChanged = function() end, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with all props", function() + local element = mockStyleComponent({ + sliderTextInput = Roact.createElement(SliderTextInput, { + position = UDim2.new(1, 1, 1, 1), + anchorPoint = Vector2.new(0.5, 0.5), + value = 10, + min = 0, + max = 100, + disabled = false, + stepInterval = 1, + onValueChanged = function() end, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SystemSlider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SystemSlider.lua new file mode 100644 index 0000000..c471edf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SystemSlider.lua @@ -0,0 +1,3 @@ +local makeAppOneKnobSlider = require(script.Parent.makeAppOneKnobSlider) + +return makeAppOneKnobSlider("SystemPrimaryDefault") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SystemSlider.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SystemSlider.spec.lua new file mode 100644 index 0000000..85a6860 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/SystemSlider.spec.lua @@ -0,0 +1,47 @@ +return function() + local Slider = script.Parent + local App = Slider.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local SystemSlider = require(script.Parent.SystemSlider) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + systemSlider = Roact.createElement(SystemSlider, { + value = 10, + min = 0, + max = 100, + onValueChanged = function() end, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with all props", function() + local element = mockStyleComponent({ + systemSlider = Roact.createElement(SystemSlider, { + value = 10, + min = 0, + max = 100, + stepInterval = 1, + onValueChanged = function() end, + onDragStart = function() end, + onDragEnd = function() end, + isDisabled = false, + textInputEnabled = true, + + width = UDim.new(1, 1), + position = UDim2.new(1, 1, 1, 1), + anchorPoint = Vector2.new(0.5, 0.5), + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobContextualSlider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobContextualSlider.lua new file mode 100644 index 0000000..31d15cc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobContextualSlider.lua @@ -0,0 +1,3 @@ +local makeAppTwoKnobSlider = require(script.Parent.makeAppTwoKnobSlider) + +return makeAppTwoKnobSlider("ContextualPrimaryDefault") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobContextualSlider.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobContextualSlider.spec.lua new file mode 100644 index 0000000..b0cbc19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobContextualSlider.spec.lua @@ -0,0 +1,49 @@ +return function() + local Slider = script.Parent + local App = Slider.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local TwoKnobContextualSlider = require(script.Parent.TwoKnobContextualSlider) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + systemSlider = Roact.createElement(TwoKnobContextualSlider, { + lowerValue = 10, + upperValue = 90, + min = 0, + max = 100, + onValueChanged = function() end, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with all props", function() + local element = mockStyleComponent({ + systemSlider = Roact.createElement(TwoKnobContextualSlider, { + lowerValue = 10, + upperValue = 90, + min = 0, + max = 100, + stepInterval = 1, + onValueChanged = function() end, + onDragStartLower = function() end, + onDragStartUpper = function() end, + onDragEnd = function() end, + isDisabled = false, + + width = UDim.new(1, 1), + position = UDim2.new(1, 1, 1, 1), + anchorPoint = Vector2.new(0.5, 0.5), + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobSystemSlider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobSystemSlider.lua new file mode 100644 index 0000000..0b937cd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobSystemSlider.lua @@ -0,0 +1,3 @@ +local makeAppTwoKnobSlider = require(script.Parent.makeAppTwoKnobSlider) + +return makeAppTwoKnobSlider("SystemPrimaryDefault") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobSystemSlider.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobSystemSlider.spec.lua new file mode 100644 index 0000000..2e4d999 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/TwoKnobSystemSlider.spec.lua @@ -0,0 +1,49 @@ +return function() + local Slider = script.Parent + local App = Slider.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local TwoKnobSystemSlider = require(script.Parent.TwoKnobSystemSlider) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + systemSlider = Roact.createElement(TwoKnobSystemSlider, { + lowerValue = 10, + upperValue = 90, + min = 0, + max = 100, + onValueChanged = function() end, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with all props", function() + local element = mockStyleComponent({ + systemSlider = Roact.createElement(TwoKnobSystemSlider, { + lowerValue = 10, + upperValue = 90, + min = 0, + max = 100, + stepInterval = 1, + onValueChanged = function() end, + onDragStartLower = function() end, + onDragStartUpper = function() end, + onDragEnd = function() end, + isDisabled = false, + + width = UDim.new(1, 1), + position = UDim2.new(1, 1, 1, 1), + anchorPoint = Vector2.new(0.5, 0.5), + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/__stories__/SliderTextInput.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/__stories__/SliderTextInput.story.lua new file mode 100644 index 0000000..57970cb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/__stories__/SliderTextInput.story.lua @@ -0,0 +1,63 @@ +local SliderRoot = script.Parent.Parent +local App = SliderRoot.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local StoryView = require(Packages.StoryComponents.StoryView) + +local SliderTextInput = require(SliderRoot.SliderTextInput) + +local Story = Roact.PureComponent:extend("Story") + +function Story:init() + self:setState({ + value = 10 + }) +end + +function Story:render() + return Roact.createFragment({ + List = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + Padding = UDim.new(0, 10), + SortOrder = Enum.SortOrder.LayoutOrder, + }), + Normal = Roact.createElement(SliderTextInput, { + layoutOrder = 1, + value = self.state.value, + min = 0, + max = 100, + stepInterval = 10, + onValueChanged = function(newValue) + print(newValue) + self:setState({ + value = newValue, + }) + end, + }), + Disabled = Roact.createElement(SliderTextInput, { + layoutOrder = 2, + value = self.state.value, + min = 0, + max = 100, + stepInterval = 10, + disabled = true, + onValueChanged = function(newValue) + end, + }), + }) +end + +return function(target) + local styleProvider = Roact.createElement(StoryView, {}, { + Roact.createElement(Story) + }) + + local handle = Roact.mount(styleProvider, target, "SliderTextInput") + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppOneKnobSlider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppOneKnobSlider.lua new file mode 100644 index 0000000..80a48d8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppOneKnobSlider.lua @@ -0,0 +1,115 @@ +local Slider = script.Parent +local App = Slider.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local makeAppSlider = require(Slider.makeAppSlider) +local SliderTextInput = require(Slider.SliderTextInput) +local withStyle = require(UIBlox.Core.Style.withStyle) +local validateStyle = require(App.Style.Validator.validateStyle) + +local function wrapStyle(component) + return function(props) + return withStyle(function(style) + local joinedProps = Cryo.Dictionary.join(props, { + style = style, + }) + + return Roact.createElement(component, joinedProps) + end) + end +end + +local function makeAppOneKnobSlider(trackFillThemeKey) + local oneKnobAppSliderComponent = Roact.PureComponent:extend("OneKnobAppSliderFor" .. trackFillThemeKey) + local appSliderComponent = makeAppSlider(trackFillThemeKey, false) + oneKnobAppSliderComponent.validateProps = t.strictInterface({ + value = t.number, + min = t.number, + max = t.number, + onValueChanged = t.callback, + onDragStart = t.optional(t.callback), + onDragEnd = t.optional(t.callback), + stepInterval = t.optional(t.numberPositive), + textInputEnabled = t.optional(t.boolean), + isDisabled = t.optional(t.boolean), + + width = t.optional(t.UDim), + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + layoutOrder = t.optional(t.integer), + + [Roact.Ref] = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + --Internal Only - Don't Pass In + style = validateStyle + }) + + oneKnobAppSliderComponent.defaultProps = { + stepInterval = 1, + width = UDim.new(1, 0), + textInputEnabled = false + } + + function oneKnobAppSliderComponent:render() + local props = self.props + + local sliderProps = { + value = props.value, + min = props.min, + max = props.max, + stepInterval = props.stepInterval, + isDisabled = props.isDisabled, + onValueChanged = props.onValueChanged, + onDragStartLower = props.onDragStart, + onDragEnd = props.onDragEnd, + style = props.style, + [Roact.Ref] = props[Roact.Ref], + NextSelectionUp = props.NextSelectionUp, + NextSelectionDown = props.NextSelectionDown, + } + + if not props.textInputEnabled then + sliderProps.width = props.width + sliderProps.position = props.position + sliderProps.anchorPoint = props.anchorPoint + sliderProps.layoutOrder = props.layoutOrder + return Roact.createElement(appSliderComponent, sliderProps) + else + sliderProps.width = UDim.new(1, -(56 + 12)) + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new( + props.width.Scale, + props.width.Offset, + 0, + 44 + ), + AnchorPoint = props.anchorPoint, + LayoutOrder = props.layoutOrder, + Position = props.position, + }, { + Slider = Roact.createElement(appSliderComponent, sliderProps), + TextInput = Roact.createElement(SliderTextInput, { + position = UDim2.new(1, 0, 0.5, 0), + anchorPoint = Vector2.new(1, 0.5), + value = props.value, + min = props.min, + max = props.max, + disabled = props.isDisabled, + stepInterval = props.stepInterval, + onValueChanged = props.onValueChanged, + }) + }) + end + end + + return wrapStyle(oneKnobAppSliderComponent) +end + +return makeAppOneKnobSlider \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppSlider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppSlider.lua new file mode 100644 index 0000000..cc7c049 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppSlider.lua @@ -0,0 +1,227 @@ +local Slider = script.Parent +local App = Slider.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Otter = require(Packages.Otter) + +local Colors = require(App.Style.Colors) +local GenericSlider = require(UIBlox.Core.Slider.GenericSlider) +local Images = require(App.ImageSet.Images) + +local SPRING_PARAMETERS = { + frequency = 5, +} + +local divideTransparency = require(UIBlox.Utility.divideTransparency) +local lerp = require(UIBlox.Utility.lerp) + +local function makeAppSlider(trackFillThemeKey, isTwoKnobs) + -- Creates a slider using the specified theme key for the track fill color. + local appSliderComponent = Roact.PureComponent:extend("AppSliderFor" .. trackFillThemeKey) + + appSliderComponent.defaultProps = { + textInputEnabled = false, + stepInterval = 1, + width = UDim.new(1, 0), + } + + function appSliderComponent:init() + local setPressedProgressLower, setPressedProgressUpper + self.pressedProgressLower, setPressedProgressLower = Roact.createBinding(0) + self.pressedProgressUpper, setPressedProgressUpper = Roact.createBinding(0) + self.disabled, self.setDisabled = Roact.createBinding(self.props.isDisabled) + self.style, self.setStyle = Roact.createBinding(self.props.style) + + local joinedBindings = Roact.joinBindings({ + disabled = self.disabled, + pressedProgressLower = self.pressedProgressLower, + pressedProgressUpper = self.pressedProgressUpper, + style = self.style, + }) + + self.trackColor = joinedBindings:map(function(values) + return values.style.Theme.UIMuted.Color + end) + + self.trackTransparency = joinedBindings:map(function(values) + return divideTransparency( + values.style.Theme.UIMuted.Transparency, + values.disabled and 2 or 1) + end) + + self.trackFillColor = joinedBindings:map(function(values) + return values.style.Theme[trackFillThemeKey].Color + end) + + self.trackFillTransparency = joinedBindings:map(function(values) + return divideTransparency( + values.style.Theme[trackFillThemeKey].Transparency, + values.disabled and 2 or 1) + end) + + self.knobColorLower = joinedBindings:map(function(values) + if values.disabled then + return Colors.Pumice + end + -- The knob, when unpressed, is white on all themes. + local unpressedColor = Color3.new(1, 1, 1) + -- If the slider is a SystemSlider, it should be pumice on pressed + local pressedColor = Colors.Pumice + if trackFillThemeKey ~= "SystemPrimaryDefault" then + pressedColor = values.style.Theme[trackFillThemeKey].Color + end + + return unpressedColor:lerp(pressedColor, values.pressedProgressLower) + end) + + self.knobColorUpper = joinedBindings:map(function(values) + if values.disabled then + return Colors.Pumice + end + -- The knob, when unpressed, is white on all themes. + local unpressedColor = Color3.new(1, 1, 1) + -- If the slider is a SystemSlider, it should be pumice on pressed + local pressedColor = Colors.Pumice + if trackFillThemeKey ~= "SystemPrimaryDefault" then + pressedColor = values.style.Theme[trackFillThemeKey].Color + end + + return unpressedColor:lerp(pressedColor, values.pressedProgressUpper) + end) + + self.knobTransparency = joinedBindings:map(function(values) + return divideTransparency( + values.style.Theme[trackFillThemeKey].Transparency, + values.disabled and 2 or 1) + end) + + self.knobShadowTransparencyLower = joinedBindings:map(function(values) + if values.disabled then + return 1 + else + return lerp(values.style.Theme.DropShadow.Transparency, 1, values.pressedProgressLower) + end + end) + + self.knobShadowTransparencyUpper = joinedBindings:map(function(values) + if values.disabled then + return 1 + else + return lerp(values.style.Theme.DropShadow.Transparency, 1, values.pressedProgressUpper) + end + end) + + self.pressedMotorLower = Otter.createSingleMotor(0) + self.pressedMotorLower:onStep(setPressedProgressLower) + + self.pressedMotorUpper = Otter.createSingleMotor(0) + self.pressedMotorUpper:onStep(setPressedProgressUpper) + + self.onDragStartLower = function() + if self.props.onDragStartLower then + self.props.onDragStartLower() + end + self.pressedMotorLower:setGoal(Otter.spring(1, SPRING_PARAMETERS)) + end + + self.onDragStartUpper = function() + if self.props.onDragStartUpper then + self.props.onDragStartUpper() + end + self.pressedMotorUpper:setGoal(Otter.spring(1, SPRING_PARAMETERS)) + end + + self.onDragEnd = function() + if self.props.onDragEnd then + self.props.onDragEnd() + end + self.pressedMotorLower:setGoal(Otter.spring(0, SPRING_PARAMETERS)) + self.pressedMotorUpper:setGoal(Otter.spring(0, SPRING_PARAMETERS)) + end + end + + function appSliderComponent:render() + local props = self.props + local sliderProps = { + min = props.min, + max = props.max, + stepInterval = props.stepInterval, + isDisabled = props.isDisabled, + width = props.width, + position = props.position, + anchorPoint = props.anchorPoint, + layoutOrder = props.layoutOrder, + + onValueChanged = props.onValueChanged, + onDragStartLower = self.onDragStartLower, + onDragStartUpper = self.onDragStartUpper, + onDragEnd = self.onDragEnd, + + trackImage = Images["component_assets/circle_16"], + trackColor = self.trackColor, + trackTransparency = self.trackTransparency, + trackSliceCenter = Rect.new(8, 8, 8, 8), + + trackFillImage = Images["component_assets/circle_16"], + trackFillColor = self.trackFillColor, + trackFillTransparency = self.trackFillTransparency, + trackFillSliceCenter = Rect.new(8, 8, 8, 8), + + knobImage = Images["component_assets/circle_28_padding_10"], + knobColorLower = self.knobColorLower, + knobColorUpper = self.knobColorUpper, + knobTransparency = self.knobTransparency, + + knobImagePadding = 10, + + knobShadowImage = Images["component_assets/dropshadow_28"], + knobShadowTransparencyLower = self.knobShadowTransparencyLower, + knobShadowTransparencyUpper = self.knobShadowTransparencyUpper, + + [Roact.Ref] = props[Roact.Ref], + NextSelectionUp = props.NextSelectionUp, + NextSelectionDown = props.NextSelectionDown, + focusController = props.focusController, + } + + if isTwoKnobs then + sliderProps.upperValue = props.upperValue + sliderProps.lowerValue = props.lowerValue + else + sliderProps.lowerValue = props.value + end + + return Roact.createElement(GenericSlider, sliderProps) + end + + function appSliderComponent:didMount() + self.pressedMotorLower:start() + self.pressedMotorUpper:start() + end + + function appSliderComponent:didUpdate(prevProps) + if prevProps.style ~= self.props.style then + self.setStyle(self.props.style) + end + + if prevProps.isDisabled ~= self.props.isDisabled then + self.setDisabled(self.props.isDisabled) + + if self.props.isDisabled then + self.pressedMotorLower:setGoal(Otter.spring(0, SPRING_PARAMETERS)) + self.pressedMotorUpper:setGoal(Otter.spring(0, SPRING_PARAMETERS)) + end + end + end + + function appSliderComponent:willUnmount() + self.pressedMotorLower:destroy() + self.pressedMotorUpper:destroy() + end + + return appSliderComponent +end + +return makeAppSlider \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppTwoKnobSlider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppTwoKnobSlider.lua new file mode 100644 index 0000000..f6d71f0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Slider/makeAppTwoKnobSlider.lua @@ -0,0 +1,79 @@ +local Slider = script.Parent +local App = Slider.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local makeAppSlider = require(Slider.makeAppSlider) +local withStyle = require(UIBlox.Core.Style.withStyle) +local validateStyle = require(App.Style.Validator.validateStyle) + +local function wrapStyle(component) + return function(props) + return withStyle(function(style) + local joinedProps = Cryo.Dictionary.join(props, { + style = style, + }) + + return Roact.createElement(component, joinedProps) + end) + end +end + +local function makeAppTwoKnobSlider(trackFillThemeKey) + local twoKnobAppSliderComponent = Roact.PureComponent:extend("TwoKnobAppSliderFor" .. trackFillThemeKey) + local appSliderComponent = makeAppSlider(trackFillThemeKey, true) + local twoKnobSliderInterface = t.strictInterface({ + --value of the first knob (must be less than or equal to upperValue) + lowerValue = t.number, + --value of the second knob (must be greater than or equal to lowerValue) + upperValue = t.number, + min = t.number, + max = t.number, + stepInterval = t.optional(t.numberPositive), + onValueChanged = t.callback, + onDragStartLower = t.optional(t.callback), + onDragStartUpper = t.optional(t.callback), + onDragEnd = t.optional(t.callback), + isDisabled = t.optional(t.boolean), + + width = t.optional(t.UDim), + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + layoutOrder = t.optional(t.integer), + + [Roact.Ref] = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + focusController = t.optional(t.table), + + --Internal Only - Don't Pass In + style = validateStyle + }) + + local function valueValidator(props) + if props.lowerValue > props.upperValue then + return false, "The upper value must be greater than or equal to the lower" + end + + return true + end + + twoKnobAppSliderComponent.validateProps = t.intersection(twoKnobSliderInterface, valueValidator) + + twoKnobAppSliderComponent.defaultProps = { + stepInterval = 1, + width = UDim.new(1, 0), + } + + function twoKnobAppSliderComponent:render() + return Roact.createElement(appSliderComponent, self.props) + end + + return wrapStyle(twoKnobAppSliderComponent) +end + +return makeAppTwoKnobSlider \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStylePalette.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStylePalette.lua new file mode 100644 index 0000000..9d6e9a8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStylePalette.lua @@ -0,0 +1,57 @@ +local Style = script.Parent +local getThemeFromName = require(Style.Themes.getThemeFromName) +local getFontFromName = require(Style.Fonts.getFontFromName) +local Constants = require(Style.Constants) + +local validateStyle = require(Style.Validator.validateStyle) + +local AppStylePalette = {} +AppStylePalette.__index = AppStylePalette + +local DEFAULT_FONT = Constants.FontName.Gotham +local FONT_MAP = { + [Constants.FontName.Gotham] = require(script.Parent.Fonts.Gotham), +} + +local DEFAULT_THEME = Constants.ThemeName.Light +local THEME_MAP = { + [Constants.ThemeName.Dark] = require(script.Parent.Themes.DarkTheme), + [Constants.ThemeName.Light] = require(script.Parent.Themes.LightTheme), +} + +function AppStylePalette.new(style) + --By default a new style will be empty. + -- This will allow the font and theme to be merged independently even when one is empty. + local self = {} + + if style ~= nil then + self.Font = style.Font + self.Theme = style.Theme + else + self.Font = getFontFromName("", DEFAULT_FONT, FONT_MAP) + self.Theme = getThemeFromName("", DEFAULT_THEME, THEME_MAP) + end + + setmetatable(self, AppStylePalette) + return self +end + +function AppStylePalette:updateFont(fontName) + self.Font = getFontFromName(fontName, DEFAULT_FONT, FONT_MAP) +end + +function AppStylePalette:updateTheme(themeName) + self.Theme = getThemeFromName(themeName, DEFAULT_THEME, THEME_MAP) +end + +function AppStylePalette:currentStyle() + local style = { + Font = self.Font, + Theme = self.Theme, + } + + assert(validateStyle(style)) + return style +end + +return AppStylePalette \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStylePalette.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStylePalette.spec.lua new file mode 100644 index 0000000..38b73b7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStylePalette.spec.lua @@ -0,0 +1,49 @@ +return function() + local Style = script.Parent + local AppStylePalette = require(script.Parent.AppStylePalette) + local validateStye = require(Style.Validator.validateStyle) + + it("should be able to create a style palette", function() + local stylePalette = AppStylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + end) + + it("should be able to create a style palette and be able to update theme", function() + local stylePalette = AppStylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + + stylePalette:updateTheme("light") + local newAppStyle = stylePalette:currentStyle() + expect(validateStye(newAppStyle)).equal(true) + end) + + it("should be able to create a style palette and be able to update font", function() + local stylePalette = AppStylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + + stylePalette:updateFont("gotham") + local newAppStyle = stylePalette:currentStyle() + expect(validateStye(newAppStyle)).equal(true) + end) + + it("should be able to create a style palette and be able to merge an old one in", function() + local stylePalette = AppStylePalette.new() + stylePalette:updateTheme("dark") + stylePalette:updateFont("gotham") + local appStyle = stylePalette:currentStyle() + expect(validateStye(appStyle)).equal(true) + + local newstylePalette = AppStylePalette.new(stylePalette) + local newAppStyle = newstylePalette:currentStyle() + expect(validateStye(newAppStyle)).equal(true) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStyleProvider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStyleProvider.lua new file mode 100644 index 0000000..7f1d21a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStyleProvider.lua @@ -0,0 +1,41 @@ +--[[ + The is a wrapper for the style provider for apps. +]] +local Style = script.Parent +local Core = Style.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local StyleProvider = require(UIBlox.Core.Style.StyleProvider) + +local AppStylePalette = require(script.Parent.AppStylePalette) + +local AppStyleProvider = Roact.Component:extend("AppStyleProvider") + +local validateProps = t.strictInterface({ + -- The current style of the app. + style = t.strictInterface({ + themeName = t.string, + fontName = t.string + }), + [Roact.Children] = t.table +}) + +function AppStyleProvider:render() + assert(validateProps(self.props)) + local style = self.props.style + local themeName = style.themeName + local fontName = style.fontName + local stylePalette = AppStylePalette.new() + stylePalette:updateTheme(themeName) + stylePalette:updateFont(fontName) + local appStyle = stylePalette:currentStyle() + + return Roact.createElement(StyleProvider,{ + style = appStyle, + }, self.props[Roact.Children]) +end + +return AppStyleProvider \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStyleProvider.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStyleProvider.spec.lua new file mode 100644 index 0000000..4820f48 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/AppStyleProvider.spec.lua @@ -0,0 +1,36 @@ +return function() + local Style = script.Parent + local Core = Style.Parent + local UIBlox = Core.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + + local AppStyleProvider = require(script.Parent.AppStyleProvider) + local Constants = require(script.Parent.Constants) + local appStyle = { + themeName = Constants.ThemeName.Dark, + fontName = Constants.FontName.Gotham, + } + it("should create and destroy without errors", function() + local element = Roact.createElement("Frame") + local appStyleProvider = Roact.createElement(AppStyleProvider, { + style = appStyle, + },{ + Element = element, + }) + + local instance = Roact.mount(appStyleProvider) + Roact.unmount(instance) + end) + + it("should throw when style prop is nil", function() + local element = Roact.createElement("Frame") + local appStyleProvider = Roact.createElement(AppStyleProvider, {},{ + Element = element, + }) + expect(function() + local instance = Roact.mount(appStyleProvider) + Roact.unmount(instance) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Colors.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Colors.lua new file mode 100644 index 0000000..ebf45b5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Colors.lua @@ -0,0 +1,24 @@ +local Colors = { + --Common colors + Black = Color3.fromRGB(0, 0, 0), + White = Color3.fromRGB(255, 255, 255), + Green = Color3.fromRGB(0, 176, 111), + Red = Color3.fromRGB(247, 75, 82), + + --Dark theme colors + Carbon = Color3.fromRGB(25, 27, 29), + Flint = Color3.fromRGB(57, 59, 61), + Graphite = Color3.fromRGB(101, 102, 104), + Obsidian = Color3.fromRGB(17, 18, 20), + Pumice = Color3.fromRGB(189, 190, 190), + Slate = Color3.fromRGB(35, 37, 39), + + --Light theme colors + Alabaster = Color3.fromRGB(242, 244, 245), + Ash = Color3.fromRGB(222, 225, 227), + Chalk = Color3.fromRGB(199, 203, 206), + Smoke = Color3.fromRGB(96, 97, 98), + XboxBlue = Color3.fromRGB(17, 139, 211), +} + +return Colors \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Colors.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Colors.spec.lua new file mode 100644 index 0000000..c3082b4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Colors.spec.lua @@ -0,0 +1,6 @@ +return function() + it("should be able to require Colors without errors", function() + local Colors = require(script.Parent.Colors) + expect(Colors).to.be.a("table") + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Constants.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Constants.lua new file mode 100644 index 0000000..9a373a4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Constants.lua @@ -0,0 +1,12 @@ +local Constants = {} + +Constants.ThemeName = { + Dark = "dark", + Light = "light", +} + +Constants.FontName = { + Gotham = "gotham", +} + +return Constants \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Constants.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Constants.spec.lua new file mode 100644 index 0000000..05a49d6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Constants.spec.lua @@ -0,0 +1,6 @@ +return function() + it("should be able to require Constants without errors", function() + local Constants = require(script.Parent.Constants) + expect(Constants).to.be.a("table") + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/Gotham.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/Gotham.lua new file mode 100644 index 0000000..b239e96 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/Gotham.lua @@ -0,0 +1,54 @@ +local baseSize = 16 +-- Nominal size conversion +-- https://confluence.rbx.com/display/PX/Font+Metrics +local nominalSizeFactor = 1.2 +local font = { + BaseSize = baseSize * nominalSizeFactor, + Title = { + Font = Enum.Font.GothamBlack, + RelativeSize = 32 / baseSize, + RelativeMinSize = 24 / baseSize, + }, + Header1 = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 20 / baseSize, + RelativeMinSize = 16 / baseSize, + }, + Header2 = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 16 / baseSize, + RelativeMinSize = 12 / baseSize, + }, + SubHeader1 = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 16 / baseSize, + RelativeMinSize = 12 / baseSize, + }, + Body = { + Font = Enum.Font.Gotham, + RelativeSize = 16 / baseSize, + RelativeMinSize = 12 / baseSize, + }, + CaptionHeader = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 12 / baseSize, + RelativeMinSize = 9 / baseSize, + }, + CaptionSubHeader = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 12 / baseSize, + RelativeMinSize = 9 / baseSize, + }, + CaptionBody = { + Font = Enum.Font.Gotham, + RelativeSize = 12 / baseSize, + RelativeMinSize = 9 / baseSize, + }, + Footer = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 10 / baseSize, + RelativeMinSize = 8 / baseSize, + }, +} + +return font \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/Gotham.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/Gotham.spec.lua new file mode 100644 index 0000000..c0c7940 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/Gotham.spec.lua @@ -0,0 +1,9 @@ +return function() + it("should be valid font palette without errors", function() + local Fonts = script.Parent + local Style = Fonts.Parent + local validateFont = require(Style.Validator.validateFont) + local Gotham = require(Fonts.Gotham) + assert(validateFont(Gotham)) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/getFontFromName.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/getFontFromName.lua new file mode 100644 index 0000000..f28d81e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/getFontFromName.lua @@ -0,0 +1,18 @@ +local Themes = script.Parent +local Style = Themes.Parent + +local validateFont = require(Style.Validator.validateFont) + +return function (fontName, defaultFont, fontMap) + local mappedFont + if fontName ~= nil and #fontName > 0 then + mappedFont = fontMap[string.lower(fontName)] + end + + if mappedFont == nil then + mappedFont = fontMap[defaultFont] + end + + assert(validateFont(mappedFont)) + return mappedFont +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/getFontFromName.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/getFontFromName.spec.lua new file mode 100644 index 0000000..be29422 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Fonts/getFontFromName.spec.lua @@ -0,0 +1,36 @@ +return function() + local Themes = script.Parent + local Style = Themes.Parent + local Constants = require(Style.Constants) + local getFontFromName = require(script.Parent.getFontFromName) + + it("should be able to get a font palette without errors", function() + local fontMap = { + [Constants.FontName.Gotham] = require(script.Parent.Gotham), + } + local fontTable = getFontFromName(Constants.FontName.Gotham, Constants.FontName.Gotham, fontMap) + expect(fontTable).to.be.a("table") + end) + + it("should be able to get a font palette using default without errors", function() + local fontMap = { + [Constants.FontName.Gotham] = require(script.Parent.Gotham), + } + local fontTable = getFontFromName("sourceSans", Constants.FontName.Gotham, fontMap) + expect(fontTable).to.be.a("table") + end) + + it("should throw the font palette is invalid", function() + expect(function() + local fontMap = { + [Constants.FontName.Gotham] = { + Font = { + Font = Enum.Font.Gotham, + RelativeSize = 1, + }, + }, + } + getFontFromName(Constants.FontName.Gotham, Constants.FontName.Gotham, fontMap) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/DarkTheme.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/DarkTheme.lua new file mode 100644 index 0000000..459e4c2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/DarkTheme.lua @@ -0,0 +1,167 @@ +local ThemesRoot = script.Parent +local StylesRoot = ThemesRoot.Parent +local Colors = require(StylesRoot.Colors) + +local theme = { + BackgroundDefault = { + Color = Colors.Slate, + Transparency = 0, + }, + BackgroundContrast = { + Color = Colors.Carbon, + Transparency = 0, + }, + BackgroundMuted = { + Color = Colors.Obsidian, + Transparency = 0, + }, + BackgroundUIDefault = { + Color = Colors.Flint, + Transparency = 0, + }, + BackgroundUIContrast = { + Color = Colors.Black, + Transparency = 0.3, -- Alpha 0.7 + }, + BackgroundOnHover = { + Color = Colors.White, + Transparency = 0.9, -- Alpha 0.1 + }, + BackgroundOnPress = { + Color = Colors.Black, + Transparency = 0.7, -- Alpha 0.3 + }, + + UIDefault = { + Color = Colors.Graphite, + Transparency = 0, + }, + UIMuted = { + Color = Colors.Obsidian, + Transparency = 0.2, -- Alpha 0.8 + }, + UIEmphasis = { + Color = Colors.White, + Transparency = 0.7, -- Alpha 0.3 + }, + + ContextualPrimaryDefault = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryOnHover = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryContent = { + Color = Colors.White, + Transparency = 0, + }, + + SystemPrimaryDefault = { + Color = Colors.White, + Transparency = 0, + }, + SystemPrimaryOnHover = { + Color = Colors.White, + Transparency = 0, + }, + SystemPrimaryContent = { + Color = Colors.Flint, + Transparency = 0, + }, + + SecondaryDefault = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + SecondaryOnHover = { + Color = Colors.White, + Transparency = 0, + }, + SecondaryContent = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + + IconDefault = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 alpha + }, + IconEmphasis = { + Color = Colors.White, + Transparency = 0, + }, + IconOnHover = { + Color = Colors.White, + Transparency = 0, + }, + + TextEmphasis = { + Color = Colors.White, + Transparency = 0, + }, + TextDefault = { + Color = Colors.Pumice, + Transparency = 0, + }, + TextMuted = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + + Divider = { + Color = Colors.White, + Transparency = 0.8, -- 0.2 Alpha + }, + Overlay = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + DropShadow = { + Color = Colors.Black, + Transparency = 0, + }, + NavigationBar = { + Color = Colors.Carbon, + Transparency = 0, + }, + PlaceHolder = { + Color = Colors.Flint, + Transparency = 0.5, -- 0.5 Alpha + }, + + OnlineStatus = { + Color = Colors.Green, + Transparency = 0, + }, + OfflineStatus = { + Color = Colors.White, + Transparency = 0.3, -- 0.7 Alpha + }, + + Success = { + Color = Colors.Green, + Transparency = 0, + }, + Alert = { + Color = Colors.Red, + Transparency = 0, + }, + + Badge = { + Color = Colors.White, + Transparency = 0, + }, + BadgeContent = { + Color = Colors.Flint, + Transparency = 0, + }, + + SelectionCursor = { + Color = Colors.White, + Transparency = 0, + }, +} + +return theme \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/DarkTheme.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/DarkTheme.spec.lua new file mode 100644 index 0000000..2fa298a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/DarkTheme.spec.lua @@ -0,0 +1,9 @@ +return function() + it("should be a valid theme palette.", function() + local Themes = script.Parent + local Style = Themes.Parent + local validateTheme = require(Style.Validator.validateTheme) + local DarkTheme = require(Themes.DarkTheme) + assert(validateTheme(DarkTheme)) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/LightTheme.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/LightTheme.lua new file mode 100644 index 0000000..c3abe88 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/LightTheme.lua @@ -0,0 +1,167 @@ +local ThemesRoot = script.Parent +local StylesRoot = ThemesRoot.Parent +local Colors = require(StylesRoot.Colors) + +local theme = { + BackgroundDefault = { + Color = Colors.Alabaster, + Transparency = 0, + }, + BackgroundContrast = { + Color = Colors.Ash, + Transparency = 0, + }, + BackgroundMuted = { + Color = Colors.Chalk, + Transparency = 0, + }, + BackgroundUIDefault = { + Color = Colors.White, + Transparency = 0, + }, + BackgroundUIContrast = { + Color = Colors.White, + Transparency = 0.1, -- Alpha 0.9 + }, + BackgroundOnHover = { + Color = Colors.Black, + Transparency = 0.9, -- Alpha 0.1 + }, + BackgroundOnPress = { + Color = Colors.Black, + Transparency = 0.9, -- Alpha 0.1 + }, + + UIDefault = { + Color = Colors.Pumice, + Transparency = 0, + }, + UIMuted = { + Color = Colors.Black, + Transparency = 0.9, -- Alpha 0.1 + }, + UIEmphasis = { + Color = Colors.Black, + Transparency = 0.7, -- Alpha 0.3 + }, + + ContextualPrimaryDefault = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryOnHover = { + Color = Colors.Green, + Transparency = 0, + }, + ContextualPrimaryContent = { + Color = Colors.White, + Transparency = 0, + }, + + SystemPrimaryDefault = { + Color = Colors.Flint, + Transparency = 0, + }, + SystemPrimaryOnHover = { + Color = Colors.Flint, + Transparency = 0, + }, + SystemPrimaryContent = { + Color = Colors.White, + Transparency = 0, + }, + + SecondaryDefault = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + SecondaryOnHover = { + Color = Colors.Flint, + Transparency = 0, + }, + SecondaryContent = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + + IconDefault = { + Color = Colors.Black, + Transparency = 0.4, -- 0.6 alpha + }, + IconEmphasis = { + Color = Colors.Flint, + Transparency = 0, + }, + IconOnHover = { + Color = Colors.Flint, + Transparency = 0, + }, + + TextEmphasis = { + Color = Colors.Flint, + Transparency = 0, + }, + TextDefault = { + Color = Colors.Smoke, + Transparency = 0, + }, + TextMuted = { + Color = Colors.Black, + Transparency = 0.4, -- 0.6 Alpha + }, + + Divider = { + Color = Colors.Pumice, + Transparency = 0, + }, + Overlay = { + Color = Colors.Black, + Transparency = 0.7, -- 0.3 Alpha + }, + DropShadow = { + Color = Colors.Black, + Transparency = 0, + }, + NavigationBar = { + Color = Colors.White, + Transparency = 0, + }, + PlaceHolder = { + Color = Colors.Black, + Transparency = 0.9, -- 0.1 Alpha + }, + + OnlineStatus = { + Color = Colors.Green, + Transparency = 0, + }, + OfflineStatus = { + Color = Colors.Black, + Transparency = 0.5, -- 0.5 Alpha + }, + + Success = { + Color = Colors.Green, + Transparency = 0, + }, + Alert = { + Color = Colors.Red, + Transparency = 0, + }, + + Badge = { + Color = Colors.Flint, + Transparency = 0, + }, + BadgeContent = { + Color = Colors.White, + Transparency = 0, + }, + + SelectionCursor = { + Color = Colors.XboxBlue, + Transparency = 0, + }, +} + +return theme \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/LightTheme.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/LightTheme.spec.lua new file mode 100644 index 0000000..ca136b6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/LightTheme.spec.lua @@ -0,0 +1,9 @@ +return function() + it("should be a valid theme palette.", function() + local Themes = script.Parent + local Style = Themes.Parent + local validateTheme = require(Style.Validator.validateTheme) + local LightTheme = require(Themes.LightTheme) + assert(validateTheme(LightTheme)) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/getThemeFromName.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/getThemeFromName.lua new file mode 100644 index 0000000..dca471c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/getThemeFromName.lua @@ -0,0 +1,17 @@ +local Themes = script.Parent +local Style = Themes.Parent + +local validateTheme = require(Style.Validator.validateTheme) + +return function (themeName, defaultTheme, themeMap) + local mappedTheme + if themeName ~= nil and #themeName > 0 then + mappedTheme = themeMap[string.lower(themeName)] + end + + if mappedTheme == nil then + mappedTheme = themeMap[defaultTheme] + end + assert(validateTheme(mappedTheme)) + return mappedTheme +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/getThemeFromName.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/getThemeFromName.spec.lua new file mode 100644 index 0000000..870af42 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Themes/getThemeFromName.spec.lua @@ -0,0 +1,36 @@ +return function() + local Themes = script.Parent + local Style = Themes.Parent + local Constants = require(Style.Constants) + + local getThemeFromName = require(script.Parent.getThemeFromName) + it("should be able to get a theme palette without errors", function() + local themeMap = { + [Constants.ThemeName.Dark] = require(script.Parent.DarkTheme), + } + local themeTable = getThemeFromName(Constants.ThemeName.Dark, Constants.ThemeName.Dark,themeMap) + expect(themeTable).to.be.a("table") + end) + + it("should be able to get a theme palette using default without errors", function() + local themeMap = { + [Constants.ThemeName.Dark] = require(script.Parent.DarkTheme), + } + local themeTable = getThemeFromName("classic", Constants.ThemeName.Dark, themeMap) + expect(themeTable).to.be.a("table") + end) + + it("should throw with invalid theme palette", function() + expect(function() + local themeMap = { + [Constants.ThemeName.Dark] = { + Background = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + } + } + getThemeFromName(Constants.ThemeName.Dark, Constants.ThemeName.Dark, themeMap) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/TestStyle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/TestStyle.lua new file mode 100644 index 0000000..3d4f7bd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/TestStyle.lua @@ -0,0 +1,68 @@ +local color = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, +} +local testTheme = { + BackgroundDefault = color, + BackgroundContrast = color, + BackgroundMuted = color, + BackgroundUIDefault = color, + BackgroundUIContrast = color, + BackgroundOnHover = color, + BackgroundOnPress = color, + UIDefault = color, + UIMuted = color, + UIEmphasis = color, + ContextualPrimaryDefault = color, + ContextualPrimaryOnHover = color, + ContextualPrimaryContent = color, + SystemPrimaryDefault = color, + SystemPrimaryOnHover = color, + SystemPrimaryContent = color, + SecondaryDefault = color, + SecondaryOnHover = color, + SecondaryContent = color, + IconDefault = color, + IconEmphasis = color, + IconOnHover = color, + TextEmphasis = color, + TextDefault = color, + TextMuted = color, + Divider = color, + Overlay = color, + DropShadow = color, + NavigationBar = color, + PlaceHolder = color, + OnlineStatus = color, + OfflineStatus = color, + Success = color, + Alert = color, + Badge = color, + BadgeContent = color, + SelectionCursor = color, +} + +local font = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 1, + RelativeMinSize = 1, +} +local testFont = { + BaseSize = 10, + Title = font, + Header1 = font, + Header2 = font, + SubHeader1 = font, + Body = font, + CaptionHeader = font, + CaptionSubHeader = font, + CaptionBody = font, + Footer = font, +} + +local testStyle = { + Theme = testTheme, + Font = testFont, +} + +return testStyle \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/TestStyle.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/TestStyle.spec.lua new file mode 100644 index 0000000..92ab26c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/TestStyle.spec.lua @@ -0,0 +1,7 @@ +return function() + local validateStyle = require(script.Parent.validateStyle) + local testStyle = require(script.Parent.TestStyle) + it("Should be valid", function() + assert(validateStyle(testStyle)) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateFont.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateFont.lua new file mode 100644 index 0000000..0286a3b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateFont.lua @@ -0,0 +1,24 @@ +local Validator = script.Parent +local Style = Validator.Parent +local App = Style.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +local Font = require(UIBlox.Core.Style.Validator.validateFontInfo) + +local FontPalette = t.strictInterface({ + BaseSize = t.numberMinExclusive(0), + Title = Font, + Header1 = Font, + Header2 = Font, + SubHeader1 = Font, + Body = Font, + CaptionHeader = Font, + CaptionSubHeader = Font, + CaptionBody = Font, + Footer = Font, +}) + +return FontPalette diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateStyle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateStyle.lua new file mode 100644 index 0000000..62b7e96 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateStyle.lua @@ -0,0 +1,17 @@ +local Validator = script.Parent +local Style = Validator.Parent +local App = Style.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +local validateTheme = require(Validator.validateTheme) +local validateFont = require(Validator.validateFont) + +local StylePalette = t.strictInterface({ + Theme = validateTheme, + Font = validateFont, +}) + +return StylePalette diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateTheme.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateTheme.lua new file mode 100644 index 0000000..97ed75e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Style/Validator/validateTheme.lua @@ -0,0 +1,61 @@ +local Validator = script.Parent +local Style = Validator.Parent +local App = Style.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +local Color = require(UIBlox.Core.Style.Validator.validateColorInfo) + +local ThemePalette = t.strictInterface({ + BackgroundDefault = Color, + BackgroundContrast = Color, + BackgroundMuted = Color, + BackgroundUIDefault = Color, + BackgroundUIContrast = Color, + BackgroundOnHover = Color, + BackgroundOnPress = Color, + + UIDefault = Color, + UIMuted = Color, + UIEmphasis = Color, + + ContextualPrimaryDefault = Color, + ContextualPrimaryOnHover = Color, + ContextualPrimaryContent = Color, + + SystemPrimaryDefault = Color, + SystemPrimaryOnHover = Color, + SystemPrimaryContent = Color, + + SecondaryDefault = Color, + SecondaryOnHover = Color, + SecondaryContent = Color, + + IconDefault = Color, + IconEmphasis = Color, + IconOnHover = Color, + + TextEmphasis = Color, + TextDefault = Color, + TextMuted = Color, + + Divider = Color, + Overlay = Color, + DropShadow = Color, + NavigationBar = Color, + PlaceHolder = Color, + + OnlineStatus = Color, + OfflineStatus = Color, + Success = Color, + Alert = Color, + + Badge = Color, + BadgeContent = Color, + + SelectionCursor = Color, +}) + +return ThemePalette diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Text/ExpandableTextArea/ExpandableTextArea.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Text/ExpandableTextArea/ExpandableTextArea.lua new file mode 100644 index 0000000..ceac188 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Text/ExpandableTextArea/ExpandableTextArea.lua @@ -0,0 +1,286 @@ +local ExpandableTextAreaRoot = script.Parent +local Text = ExpandableTextAreaRoot.Parent +local App = Text.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local RoactGamepad = require(Packages.RoactGamepad) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local SpringAnimatedItem = require(UIBlox.Utility.SpringAnimatedItem) +local GetTextHeight = require(UIBlox.Core.Text.GetTextHeight) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local Images = require(UIBlox.App.ImageSet.Images) +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local ExpandableTextUtils = require(UIBlox.Core.Text.ExpandableText.ExpandableTextUtils) + +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local DEFAULT_PADDING_TOP = 30 +local PADDING_TOP = DEFAULT_PADDING_TOP +local SPACING_Y = 10 +local DEFAULT_PADDING_BOTTOM = 5 +local PADDING_BOTTOM = DEFAULT_PADDING_BOTTOM +local DOWN_ARROW_SIZE = UDim2.new(0, 36, 0, 36) +local PRESSABLE_AREA_SIZE = UDim2.new(1, 0, 0, 36) +local GRADIENT_HEIGHT = 30 +local GRADIENT_IMAGE = Images["gradient/gradient_0_100"] +local DOWN_ARROW_IMAGE_EXPAND = Images["truncate_arrows/actions_truncationExpand"] +local DOWN_ARROW_IMAGE_COLLAPSE = Images["truncate_arrows/actions_truncationCollapse"] + +-- TODO remove this when CLIPLAYEREX-1633 is fixed +local PATCHED_PADDING = 2 + +local ANIMATION_SPRING_SETTINGS = { + dampingRatio = 1, + frequency = 3.5, +} +local GRADIENT_ANIMATION_SPRING_SETTINGS = { + dampingRatio = 1, + frequency = 3.5, +} + +local SpringImageComponent = SpringAnimatedItem.wrap(ImageSetComponent.Label) +local ExpandableTextArea = Roact.PureComponent:extend("ExpandableTextArea") + +ExpandableTextArea.defaultProps = { + compactNumberOfLines = 2, + Text = "", +} + +local validateProps = t.strictInterface({ + Text = t.optional(t.string), + Position = t.optional(t.UDim2), + compactNumberOfLines = t.optional(t.number), + LayoutOrder = t.optional(t.number), + width = t.optional(t.UDim), + padding = t.optional(t.Vector2), + onClick = t.optional(t.callback), + + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) + +function ExpandableTextArea:init() + self.state = { + isExpanded = false, + frameWidth = 0, + } + + self.onClick = function() + self:setState(function(state) + return { + isExpanded = not state.isExpanded + } + end) + if self.props.onClick then + self.props.onClick(self.state.isExpanded) + end + end + + self.ref = Roact.createRef() + self.layoutRef = Roact.createRef() +end + +function ExpandableTextArea:getRef() + if UIBloxConfig.enableExperimentalGamepadSupport then + return self.props[Roact.Ref] or self.ref + end + return self.ref +end + +function ExpandableTextArea:applyFit(y) + local ref = self:getRef() + if not ref.current then + return + end + local frame = ref.current + local offset = (y + PADDING_TOP + PADDING_BOTTOM) + local width = self.props.width + if width then + frame.Size = UDim2.new(width.Scale, width.Offset, 0, offset) + else + frame.Size = UDim2.new(1, 0, 0, offset) + end +end + +function ExpandableTextArea:didMount() + self.isMounted = true + local layout = self.layoutRef.current + if layout then + local size = layout.AbsoluteContentSize + self:applyFit(size.y) + end +end + +function ExpandableTextArea:willUnmount() + self.isMounted = false +end + +function ExpandableTextArea:render() + assert(validateProps(self.props)) + local descriptionText = self.props.Text + local position = self.props.Position + local compactNumberOfLines = self.props.compactNumberOfLines + local layoutOrder = self.props.LayoutOrder + local width = self.props.width + local padding = self.props.padding + local ref = self:getRef() + + PADDING_TOP = padding and padding.Y or DEFAULT_PADDING_TOP + PADDING_BOTTOM = padding and padding.X or DEFAULT_PADDING_BOTTOM + + return withStyle(function(stylePalette) + return withSelectionCursorProvider(function(getSelectionCursor) + local theme = stylePalette.Theme + local font = stylePalette.Font + local textSize = font.BaseSize * font.Body.RelativeSize + local fullTextHeight, compactHeight + if UIBloxConfig.enableExperimentalGamepadSupport then + fullTextHeight, compactHeight = ExpandableTextUtils.getExpandableTextHeights( + font, self.state.frameWidth, descriptionText, compactNumberOfLines) + else + local textFont = font.Body.Font + fullTextHeight = GetTextHeight(descriptionText, textFont, textSize, self.state.frameWidth) + compactHeight = compactNumberOfLines * textSize + PATCHED_PADDING + end + + local compactSize = UDim2.new(1, 0, 0, compactHeight + PADDING_BOTTOM) + local fullSize = UDim2.new(1, 0, 0, fullTextHeight + PADDING_BOTTOM) + local canExpand = fullTextHeight > compactHeight + local isExpanded = not canExpand or self.state.isExpanded + + local size = isExpanded and fullSize or compactSize + local gradientHeight = isExpanded and 0 or GRADIENT_HEIGHT + + local isFocusable = UIBloxConfig.enableExperimentalGamepadSupport and canExpand + local frameComponent = isFocusable and RoactGamepad.Focusable.Frame or "Frame" + + return Roact.createElement(frameComponent, { + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + Position = position, + Size = width and UDim2.new(width.Scale, width.Offset, 0, 0) + or UDim2.new(1, 0, 0, 0), + SelectionImageObject = getSelectionCursor(CursorKind.RoundedRect), + [Roact.Ref] = ref, + [Roact.Change.AbsoluteSize] = function(rbx) + if self.state.frameWidth ~= rbx.AbsoluteSize.X then + -- Wrapped in spawn in order to avoid issues if Roact connects changed signal before the Size + -- prop is set in older versions of Roact (older than 1.0) In 1.0, this is fixed by deferring event + -- handlers and setState calls until after the current update]] + self:setState({ + frameWidth = rbx.AbsoluteSize.X, + }) + end + end, + + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + inputBindings = isFocusable and { + Activated = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonA, self.onClick), + } or nil, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = UDim.new(0, SPACING_Y), + [Roact.Change.AbsoluteContentSize] = function(rbx) + self:applyFit(rbx.AbsoluteContentSize.y) + end, + + [Roact.Ref] = self.layoutRef, + }), + UIPadding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, PADDING_TOP), + }), + ExpandableContainer = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + animatedValues = { + height = size.Y.Offset, + }, + mapValuesToProps = function(values) + return { + Size = UDim2.new(1, 0, size.Y.Scale, values.height), + } + end, + regularProps = { + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = true, + Size = size, + LayoutOrder = 0, + }, + springOptions = ANIMATION_SPRING_SETTINGS, + }, { + DescriptionText = Roact.createElement(GenericTextLabel, { + colorStyle = theme.TextDefault, + fontStyle = font.Body, + Size = fullSize, + Text = descriptionText, + TextSize = textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextWrapped = true, + BackgroundTransparency = 1, + }), + Gradient = canExpand and Roact.createElement(SpringImageComponent, { + animatedValues = { + height = gradientHeight, + }, + mapValuesToProps = function(values) + return { + Size = UDim2.new(1, 0, 0, values.height), + } + end, + regularProps = { + Size = UDim2.new(1, 0, 0, GRADIENT_HEIGHT), + Position = UDim2.new(0, 0, 1, 0), + AnchorPoint = Vector2.new(0, 1), + BackgroundTransparency = 1, + Image = GRADIENT_IMAGE, + ImageColor3 = theme.BackgroundDefault.Color, + }, + springOptions = GRADIENT_ANIMATION_SPRING_SETTINGS, + }) + }), + ButtonContainer = canExpand and Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 10), + LayoutOrder = 1, + }, { + PressableButton = Roact.createElement("TextButton", { + Position = UDim2.new(0, 0, 0, -24), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = PRESSABLE_AREA_SIZE, + Text = "", + [Roact.Event.Activated] = self.onClick, + }, { + DownArrow = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Position = UDim2.new(0.5, 0, 0, 0), + Image = (size == fullSize) and DOWN_ARROW_IMAGE_COLLAPSE or DOWN_ARROW_IMAGE_EXPAND, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Size = DOWN_ARROW_SIZE, + }), + }), + }) + }) + end) + end) +end + +return ExpandableTextArea \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Text/ExpandableTextArea/ExpandableTextArea.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Text/ExpandableTextArea/ExpandableTextArea.spec.lua new file mode 100644 index 0000000..35aadbf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Text/ExpandableTextArea/ExpandableTextArea.spec.lua @@ -0,0 +1,30 @@ +return function() + local ExpandableTextAreaFolder = script.Parent + local Text = ExpandableTextAreaFolder.Parent + local App = Text.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local ExpandableTextArea = require(ExpandableTextAreaFolder.ExpandableTextArea) + + local descriptionText = [[ + This golden crown was awarded as a prize in the June 2007 Domino Rally Building Contest. + Perhaps its most unique characteristic is its ability to inspire viewers with awe + while at the same time making the wearer look goofy. + ]] + + describe("ExpandableTextArea", function() + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + Image = Roact.createElement(ExpandableTextArea, { + Text = descriptionText, + }) + }) + + local instance = Roact.mount(element, nil, "ExpandableTextArea") + Roact.unmount(instance) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/Tile.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/Tile.lua new file mode 100644 index 0000000..7b56072 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/Tile.lua @@ -0,0 +1,191 @@ +local BaseTile = script.Parent +local TileRoot = BaseTile.Parent +local App = TileRoot.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local RoactGamepad = require(Packages.RoactGamepad) + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local CursorKind = require(App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(App.SelectionImage.withSelectionCursorProvider) + +local TileName = require(BaseTile.TileName) +local TileThumbnail = require(BaseTile.TileThumbnail) +local TileBanner = require(BaseTile.TileBanner) + +local Tile = Roact.PureComponent:extend("Tile") + +local tileInterface = t.strictInterface({ + -- The footer Roact element. + footer = t.optional(t.table), + + -- The item's name that will show a loading state if nil + name = t.optional(t.string), + + -- The number of lines of text for the item name + titleTextLineCount = t.optional(t.integer), + + -- The vertical padding between elements in the ItemTile + innerPadding = t.optional(t.integer), + + -- The function that gets called on itemTile click + onActivated = t.optional(t.callback), + + -- The item's thumbnail that will show a loading state if nil + thumbnail = t.optional(t.union(t.string, t.table)), + + -- The item thumbnail's size if not UDm2.new(1, 0, 1, 0) + thumbnailSize = t.optional(t.UDim2), + + -- Optional text to display in the Item Tile banner in place of the footer + bannerText = t.optional(t.string), + + -- Whether the tile is selected or not + isSelected = t.optional(t.boolean), + + -- Optional boolean indicating whether to create an overlay to round the corners of the image + hasRoundedCorners = t.optional(t.boolean), + + -- Optional image to be displayed in the title component + -- Image information should be ImageSet compatible + titleIcon = t.optional(t.table), + + -- Optional Roact elements that are overlayed over the thumbnail component + thumbnailOverlayComponents = t.optional(t.table), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) + +local function tileBannerUseValidator(props) + if props.bannerText and props.footer then + return false, "A custom footer and bannerText can't be used together" + end + + return true +end + +local validateProps = t.intersection(tileInterface, tileBannerUseValidator) + +Tile.defaultProps = { + titleTextLineCount = 2, + innerPadding = 8, + isSelected = false, + hasRoundedCorners = true, +} + +function Tile:init() + self.state = { + tileWidth = 0, + tileHeight = 0, + } + + self.onAbsoluteSizeChange = function(rbx) + local tileWidth = rbx.AbsoluteSize.X + local tileHeight = rbx.AbsoluteSize.Y + self:setState({ + tileWidth = tileWidth, + tileHeight = tileHeight, + }) + end +end + +function Tile:render() + assert(validateProps(self.props)) + local footer = self.props.footer + local name = self.props.name + local titleTextLineCount = self.props.titleTextLineCount + local innerPadding = self.props.innerPadding + local onActivated = self.props.onActivated + local thumbnail = self.props.thumbnail + local thumbnailSize = self.props.thumbnailSize + local bannerText = self.props.bannerText + local hasRoundedCorners = self.props.hasRoundedCorners + local isSelected = self.props.isSelected + local titleIcon = self.props.titleIcon + local thumbnailOverlayComponents = self.props.thumbnailOverlayComponents + + return withStyle(function(stylePalette) + return withSelectionCursorProvider(function(getSelectionCursor) + local font = stylePalette.Font + + local tileHeight = self.state.tileHeight + local tileWidth = self.state.tileWidth + + local maxTitleTextHeight = math.ceil(font.BaseSize * font.Header2.RelativeSize * titleTextLineCount) + local footerHeight = tileHeight - tileWidth - innerPadding - maxTitleTextHeight - innerPadding + footerHeight = math.max(0, footerHeight) + + local hasFooter = footer ~= nil or bannerText ~= nil + + -- TODO: use generic/state button from UIBlox + return Roact.createElement("TextButton", { + Text = "", + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Selectable = false, + [Roact.Event.Activated] = onActivated, + [Roact.Change.AbsoluteSize] = self.onAbsoluteSizeChange, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, innerPadding), + }), + Thumbnail = Roact.createElement(UIBloxConfig.enableExperimentalGamepadSupport + and RoactGamepad.Focusable.Frame or "Frame", { + Size = UDim2.new(1, 0, 1, 0), + SizeConstraint = Enum.SizeConstraint.RelativeXX, + BackgroundTransparency = 1, + LayoutOrder = 1, + + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + [Roact.Ref] = self.props[Roact.Ref], + SelectionImageObject = getSelectionCursor(CursorKind.RoundedRectNoInset), + inputBindings = UIBloxConfig.enableExperimentalGamepadSupport and { + Activate = RoactGamepad.Input.onBegin(Enum.KeyCode.ButtonA, onActivated) + } or nil, + }, { + Image = Roact.createElement(TileThumbnail, { + Image = thumbnail, + hasRoundedCorners = hasRoundedCorners, + isSelected = isSelected, + overlayComponents = thumbnailOverlayComponents, + imageSize = thumbnailSize, + }), + }), + Name = (titleTextLineCount > 0 and tileWidth > 0) and Roact.createElement(TileName, { + titleIcon = titleIcon, + name = name, + maxHeight = maxTitleTextHeight, + maxWidth = tileWidth, + LayoutOrder = 2, + }), + FooterContainer = hasFooter and Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, footerHeight), + BackgroundTransparency = 1, + LayoutOrder = 3, + }, { + Banner = bannerText and Roact.createElement(TileBanner, { + bannerText = bannerText, + }), + Footer = not bannerText and footer, + }), + }) + end) + end) +end + +return Tile diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/Tile.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/Tile.spec.lua new file mode 100644 index 0000000..ac926bb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/Tile.spec.lua @@ -0,0 +1,61 @@ +return function() + local BaseTile = script.Parent + local TileRoot = BaseTile.Parent + local App = TileRoot.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local Tile = require(BaseTile.Tile) + + it("should create and destroy without errors", function() + local testImage = "https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814" + local testName = "some test name" + local createFooter = function() + return Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }) + end + local onActivated = function() end + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }, { + Tile = Roact.createElement(Tile, { + footer = createFooter(), + name = testName, + onActivated = onActivated, + thumbnail = testImage, + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should not render name when no lines are allocated to name", function() + local testName = "test text" + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }, { + Tile = Roact.createElement(Tile, { + name = testName, + onActivated = function() end, + titleTextLineCount = 0, + }) + }) + }) + + local container = Instance.new("ScreenGui") + local instance = Roact.mount(element, container, "TitleTest") + + expect(container.TitleTest).to.be.ok() + expect(container.TitleTest.Frame).to.be.ok() + expect(container.TitleTest.Frame:FindFirstChild("Name")).to.never.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileBanner.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileBanner.lua new file mode 100644 index 0000000..3aaf4dd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileBanner.lua @@ -0,0 +1,52 @@ +local BaseTile = script.Parent +local Tile = BaseTile.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local TileBanner = Roact.PureComponent:extend("TileBanner") + +local TEXT_PADDING = 6 + +local validateProps = t.strictInterface({ + -- The text to display in the banner + bannerText = t.string, +}) + +function TileBanner:render() + assert(validateProps(self.props)) + + local bannerText = self.props.bannerText + + return withStyle(function(stylePalette) + local font = stylePalette.Font + local theme = stylePalette.Theme + + local bannerHeight = TEXT_PADDING + font.CaptionBody.RelativeSize * font.BaseSize + + return Roact.createElement("Frame", { + BackgroundColor3 = theme.SystemPrimaryDefault.Color, + BackgroundTransparency = theme.SystemPrimaryDefault.Transparency, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, bannerHeight), + }, { + TextLabel = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Font = font.CaptionBody.Font, + TextSize = font.CaptionBody.RelativeSize * font.BaseSize, + Text = bannerText, + TextColor3 = theme.SystemPrimaryContent.Color, + TextTransparency = theme.SystemPrimaryContent.Transparency, + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Center, + Size = UDim2.new(1, 0, 1, 0), + }), + }) + end) +end + +return TileBanner \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileBanner.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileBanner.spec.lua new file mode 100644 index 0000000..17a6f45 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileBanner.spec.lua @@ -0,0 +1,26 @@ +return function() + local BaseTile = script.Parent + local Tile = BaseTile.Parent + local App = Tile.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local TileBanner = require(script.Parent.TileBanner) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 50), + }, { + TileBanner = Roact.createElement(TileBanner, { + bannerText = "ONLY 12.3K LEFT!", + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileName.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileName.lua new file mode 100644 index 0000000..70ac28a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileName.lua @@ -0,0 +1,137 @@ +local BaseTile = script.Parent +local Tile = BaseTile.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) +local Images = require(UIBlox.App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local ImageTextLabel = require(UIBlox.Core.Text.ImageTextLabel.ImageTextLabel) +local ShimmerPanel = require(UIBlox.App.Loading.ShimmerPanel) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local fixItemTilePremiumIcon = UIBloxConfig.fixItemTilePremiumIcon + +local ICON_PADDING = 4 +local LINE_PADDING = 4 + +local ItemTileName = Roact.PureComponent:extend("ItemTileName") + +local validateProps = t.strictInterface({ + LayoutOrder = t.optional(t.integer), + + maxHeight = t.intersection(t.integer, t.numberMin(0)), + maxWidth = t.intersection(t.integer, t.numberMin(0)), + + -- Loading skeleton will be rendered if name is not included + name = t.optional(t.string), + + -- Optional image to be displayed in the title component + -- Image information should be ImageSet compatible + titleIcon = t.optional(t.table), +}) + +function ItemTileName:render() + assert(validateProps(self.props)) + + local layoutOrder = self.props.LayoutOrder + local maxHeight = self.props.maxHeight + local maxWidth = self.props.maxWidth + local name = self.props.name + local titleIcon = self.props.titleIcon + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local font = stylePalette.Font + local textSize = font.BaseSize * font.Header2.RelativeSize + + if name ~= nil then + local titleIconSize = titleIcon and titleIcon.ImageRectSize / Images.ImagesResolutionScale or Vector2.new(0, 0) + + if fixItemTilePremiumIcon then + return Roact.createElement(ImageTextLabel, { + imageProps = titleIcon and { + BackgroundTransparency = 1, + Image = titleIcon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Size = UDim2.new(0, titleIconSize.X, 0, titleIconSize.Y), + AnchorPoint = Vector2.new(0, 0), + Position = UDim2.new(0, 0, 0, 0), + } or nil, + + genericTextLabelProps = { + fontStyle = font.Header2, + colorStyle = theme.TextEmphasis, + Text = name, + TextTruncate = Enum.TextTruncate.AtEnd, + }, + + frameProps = { + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, + + maxSize = Vector2.new(maxWidth, maxHeight), + padding = ICON_PADDING, + }) + else + local labelWidth = titleIcon and (maxWidth - titleIconSize.X - ICON_PADDING) or maxWidth + + local labelTextSize = GetTextSize(name, textSize, font.Header2.Font, Vector2.new(labelWidth, maxHeight)) + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + Size = UDim2.new(0, maxWidth, 0, labelTextSize.Y), + }, { + Icon = titleIcon and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = titleIcon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Size = UDim2.new(0, titleIconSize.X, 0, titleIconSize.Y), + }), + + Name = Roact.createElement("TextLabel", { + AnchorPoint = Vector2.new(1, 0), + Position = UDim2.new(1, 0, 0, 0), + Size = UDim2.new(0, labelWidth, 1, 0), + BackgroundTransparency = 1, + TextSize = textSize, + TextColor3 = theme.TextEmphasis.Color, + TextTransparency = theme.TextEmphasis.Transparency, + Font = font.Header2.Font, + Text = name, + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextWrapped = true, + }), + }) + end + else + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + Size = UDim2.new(0, maxWidth, 0, maxHeight), + }, { + FirstLine = Roact.createElement(ShimmerPanel, { + Size = UDim2.new(1, 0, 0, textSize), + }), + + SecondLine = Roact.createElement(ShimmerPanel, { + Position = UDim2.new(0, 0, 0, textSize + LINE_PADDING), + Size = UDim2.new(0.4, 0, 0, textSize), + }), + }) + end + end) +end + +return ItemTileName \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileName.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileName.spec.lua new file mode 100644 index 0000000..2fcb0da --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileName.spec.lua @@ -0,0 +1,46 @@ +return function() + local BaseTile = script.Parent + local Tile = BaseTile.Parent + local App = Tile.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local TileName = require(script.Parent.TileName) + + it("should create and destroy without errors", function() + local testName = "some test name" + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }, { + TileName = Roact.createElement(TileName, { + name = testName, + maxHeight = 100, + maxWidth = 100, + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy with loading state without errors", function() + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }, { + TileName = Roact.createElement(TileName, { + name = nil, + maxHeight = 100, + maxWidth = 100, + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileSelectionOverlay.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileSelectionOverlay.lua new file mode 100644 index 0000000..4ba9054 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileSelectionOverlay.lua @@ -0,0 +1,62 @@ +local BaseTile = script.Parent +local Tile = BaseTile.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local Images = require(UIBlox.App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local TileSelectionOverlay = Roact.PureComponent:extend("TileSelectionOverlay") + +local PADDING_RIGHT = 6 +local PADDING_TOP = 6 + +TileSelectionOverlay.validateProps = t.strictInterface({ + ZIndex = t.optional(t.integer), + cornerRadius = t.optional(t.UDim), +}) + +TileSelectionOverlay.defaultProps = { + cornerRadius = UDim.new(0, 0), +} + +function TileSelectionOverlay:render() + local zIndex = self.props.ZIndex + local cornerRadius = self.props.cornerRadius + + local selectionIcon = Images["icons/actions/selectOn"] + local imageSize = selectionIcon.ImageRectSize / Images.ImagesResolutionScale + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + return Roact.createElement("Frame", { + BackgroundColor3 = theme.Overlay.Color, + BackgroundTransparency = theme.Overlay.Transparency, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + ZIndex = zIndex, + }, { + SelectionImage = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(1, 0), + BackgroundTransparency = 1, + Image = selectionIcon, + Position = UDim2.new(1, -PADDING_RIGHT, 0, PADDING_TOP), + Size = UDim2.new(0, imageSize.X, 0, imageSize.Y), + }), + UICorner = UIBloxConfig.useNewUICornerRoundedCorners and cornerRadius ~= UDim.new(0, 0) + and Roact.createElement("UICorner", { + CornerRadius = cornerRadius, + }) or nil, + }) + end) +end + +return TileSelectionOverlay \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileSelectionOverlay.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileSelectionOverlay.spec.lua new file mode 100644 index 0000000..de66139 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileSelectionOverlay.spec.lua @@ -0,0 +1,26 @@ +return function() + local BaseTile = script.Parent + local Tile = BaseTile.Parent + local App = Tile.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local TileSelectionOverlay = require(script.Parent.TileSelectionOverlay) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }, { + TileSelectionOverlay = Roact.createElement(TileSelectionOverlay, { + cornerRadius = UDim.new(0, 10), + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileThumbnail.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileThumbnail.lua new file mode 100644 index 0000000..31bcaed --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileThumbnail.lua @@ -0,0 +1,106 @@ +local BaseTile = script.Parent +local Tile = BaseTile.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local Images = require(UIBlox.App.ImageSet.Images) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local LoadableImage = require(UIBlox.App.Loading.LoadableImage) +local TileSelectionOverlay = require(BaseTile.TileSelectionOverlay) + +local TileThumbnail = Roact.PureComponent:extend("TileThumbnail") + +TileThumbnail.defaultProps = { + imageSize = UDim2.new(1, 0, 1, 0), +} + +local CORNER_RADIUS = UDim.new(0, 10) + +function TileThumbnail:render() + local hasRoundedCorners = self.props.hasRoundedCorners + local image = self.props.Image + local imageSize = self.props.imageSize + local isSelected = self.props.isSelected + local overlayComponents = self.props.overlayComponents + + local isImageSetImage = typeof(image) == "table" + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + }, { + ImageContainer = Roact.createElement("Frame", { + BackgroundColor3 = theme.PlaceHolder.Color, + BackgroundTransparency = theme.PlaceHolder.Transparency, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 0, + }, { + Image = not isImageSetImage and Roact.createElement(LoadableImage, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundColor3 = theme.PlaceHolder.Color, + BackgroundTransparency = theme.PlaceHolder.Transparency, + Image = image, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = imageSize, + ZIndex = 0, + cornerRadius = UIBloxConfig.useNewUICornerRoundedCorners and hasRoundedCorners and CORNER_RADIUS or nil, + showFailedStateWhenLoadingFailed = true, + useShimmerAnimationWhileLoading = true, + }), + + ImageSetImage = isImageSetImage and Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = image, + ImageColor3 = theme.UIEmphasis.Color, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = imageSize, + }, { + UICorner = UIBloxConfig.useNewUICornerRoundedCorners and hasRoundedCorners + and Roact.createElement("UICorner", { + CornerRadius = CORNER_RADIUS, + }) or nil, + }), + + UICorner = UIBloxConfig.useNewUICornerRoundedCorners and hasRoundedCorners + and Roact.createElement("UICorner", { + CornerRadius = CORNER_RADIUS, + }) or nil, + }), + + ComponentsFrame = overlayComponents and Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 1, + }, overlayComponents), + + SelectionOverlay = isSelected and Roact.createElement(TileSelectionOverlay, { + ZIndex = 2, + cornerRadius = hasRoundedCorners and CORNER_RADIUS or nil, + }), + + RoundedCornersOverlay = not UIBloxConfig.useNewUICornerRoundedCorners and hasRoundedCorners + and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = Images["component_assets/circle_17_mask"], + ImageColor3 = theme.BackgroundDefault.Color, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 3, + }), + }) + end) +end + +return TileThumbnail diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileThumbnail.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileThumbnail.spec.lua new file mode 100644 index 0000000..e95dff3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/BaseTile/TileThumbnail.spec.lua @@ -0,0 +1,50 @@ +return function() + local BaseTile = script.Parent + local Tile = BaseTile.Parent + local App = Tile.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local TileThumbnail = require(script.Parent.TileThumbnail) + + it("should create and destroy without errors", function() + local testImage = "https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814" + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }, { + ItemTileIcon = Roact.createElement(TileThumbnail, { + Image = testImage, + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy with additional components without errors", function() + local testImage = "https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814" + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }, { + ItemTileIcon = Roact.createElement(TileThumbnail, { + Image = testImage, + isSelected = true, + overlayComponents = { + Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + }), + } + }) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/Enum/ItemTileEnums.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/Enum/ItemTileEnums.lua new file mode 100644 index 0000000..1e565fa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/Enum/ItemTileEnums.lua @@ -0,0 +1,23 @@ +local Tile = script.Parent.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local enumerate = require(Packages.enumerate) + +local strict = require(UIBlox.Utility.strict) + +return strict({ + ItemIconType = enumerate("ItemIconType", { + "AnimationBundle", + "Bundle" + }), + StatusStyle = enumerate("StatusStyle", { + "Alert", + "Info" + }), + Restriction = enumerate("Restriction", { + "Limited", + "LimitedUnique" + }) +}, script.Name) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemIcon.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemIcon.lua new file mode 100644 index 0000000..020ad1e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemIcon.lua @@ -0,0 +1,62 @@ +local ItemTile = script.Parent +local Tile = ItemTile.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local Images = require(UIBlox.App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local ItemTileEnums = require(Tile.Enum.ItemTileEnums) + +local ItemIcon = Roact.PureComponent:extend("ItemIcon") + +local ItemIconTypesMap = { + [ItemTileEnums.ItemIconType.AnimationBundle] = Images["icons/status/item/bundle"], + [ItemTileEnums.ItemIconType.Bundle] = Images["icons/status/item/bundle"], +} + +local PADDING_BOTTOM = 12 +local PADDING_RIGHT = 12 + +local function isValidItemIconType(value) + if ItemIconTypesMap[value] then + return true + end + + return false, "Unknown ItemType " .. value +end + +local validateProps = t.strictInterface({ + -- Enum specifying the item type + itemIconType = t.intersection(enumerateValidator(ItemTileEnums.ItemIconType), isValidItemIconType), +}) + +function ItemIcon:render() + assert(validateProps(self.props)) + + local itemIconType = self.props.itemIconType + + local icon = ItemIconTypesMap[itemIconType] + local imageSize = icon.ImageRectSize / Images.ImagesResolutionScale + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + return Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(1, 1), + BackgroundTransparency = 1, + Image = icon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Position = UDim2.new(1, -PADDING_RIGHT, 1, -PADDING_BOTTOM), + Size = UDim2.new(0, imageSize.X, 0, imageSize.Y), + }) + end) +end + +return ItemIcon diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemRestrictionStatus.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemRestrictionStatus.lua new file mode 100644 index 0000000..f1ad44f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemRestrictionStatus.lua @@ -0,0 +1,117 @@ +local ItemTile = script.Parent +local Tile = ItemTile.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) +local Images = require(UIBlox.App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local ItemTileEnums = require(Tile.Enum.ItemTileEnums) + +local ItemRestrictionStatus = Roact.PureComponent:extend("ItemRestrictionStatus") + +local MAX_TEXT_SIZE = Vector2.new(50, 20) +local CONTENT_PADDING = Vector2.new(8, 8) + +local PADDING_LEFT = 12 +local PADDING_BOTTOM = 12 +local TEXT_PADDING = 10 + +local validateProps = t.strictInterface({ + -- Enum specifying the restriction type + restrictionTypes = t.map(enumerateValidator(ItemTileEnums.Restriction), t.boolean), + + -- Optional information about the restriction + restrictionInfo = t.optional(t.table), +}) + +local function getAdditionalText(restrictionTypes, restrictionInfo) + local additionalText = "" + + if restrictionTypes[ItemTileEnums.Restriction.LimitedUnique] then + additionalText = "#" + end + + if restrictionInfo and restrictionInfo.limitedSerialNumber then + additionalText = additionalText .. " " .. restrictionInfo.limitedSerialNumber + end + + return additionalText +end + +local function getRestrictionIcon(restrictionTypes) + if restrictionTypes[ItemTileEnums.Restriction.Limited] or + restrictionTypes[ItemTileEnums.Restriction.LimitedUnique] then + return Images["icons/status/item/limited"] + end + + return nil +end + +function ItemRestrictionStatus:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local fontInfo = stylePalette.Font + + local restrictionInfo = self.props.restrictionInfo + local restrictionTypes = self.props.restrictionTypes + local additionalText = getAdditionalText(restrictionTypes, restrictionInfo) + + local font = fontInfo.CaptionHeader.Font + local fontSize = fontInfo.BaseSize * fontInfo.CaptionHeader.RelativeSize + local textSize = GetTextSize(additionalText, fontSize, font, MAX_TEXT_SIZE) + + local icon = getRestrictionIcon(restrictionTypes) + local imageSize = icon and icon.ImageRectSize / Images.ImagesResolutionScale or Vector2.new(0, 0) + + local xSize = imageSize.X + textSize.X + CONTENT_PADDING.X + local ySize = math.max(imageSize.Y, textSize.Y) + CONTENT_PADDING.Y + + return Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0, 1), + BackgroundTransparency = 1, + Image = Images["component_assets/circle_17"], + ImageColor3 = theme.UIDefault.Color, + ImageTransparency = theme.UIDefault.Transparency, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + Position = UDim2.new(0, PADDING_LEFT, 1, -PADDING_BOTTOM), + Size = UDim2.new(0, xSize, 0, ySize), + }, { + Icon = icon and Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + Image = icon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Position = UDim2.new(0, CONTENT_PADDING.X / 2, 0.5, 0), + Size = UDim2.new(0, imageSize.X, 0, imageSize.Y), + }), + + Text = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Font = font, + TextSize = fontSize, + Text = additionalText, + TextColor3 = theme.TextMuted.Color, + TextTransparency = theme.TextMuted.TextTransparency, + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + Position = UDim2.new(0, imageSize.X, 0, 0), + Size = UDim2.new(0, textSize.X + TEXT_PADDING, 1, 0), + }), + }) + end) +end + +return ItemRestrictionStatus diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTile.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTile.lua new file mode 100644 index 0000000..d267bd8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTile.lua @@ -0,0 +1,161 @@ +local ItemTileRoot = script.Parent +local TileRoot = ItemTileRoot.Parent +local App = TileRoot.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local Images = require(UIBlox.App.ImageSet.Images) +local ItemRestrictionStatus = require(ItemTileRoot.ItemRestrictionStatus) +local ItemTileStatus = require(ItemTileRoot.ItemTileStatus) +local ItemTileEnums = require(TileRoot.Enum.ItemTileEnums) +local ItemIcon = require(ItemTileRoot.ItemIcon) +local Tile = require(TileRoot.BaseTile.Tile) + +local ItemTile = Roact.PureComponent:extend("ItemTile") + +local itemTileInterface = t.strictInterface({ + -- The footer Roact element. + footer = t.optional(t.table), + + -- The item's name that will show a loading state if nil + name = t.optional(t.string), + + -- The number of lines of text for the item name + titleTextLineCount = t.optional(t.integer), + + -- The vertical padding between elements in the ItemTile + innerPadding = t.optional(t.integer), + + -- The function that gets called on itemTile click + onActivated = t.optional(t.callback), + + -- The item's thumbnail that will show a loading state if nil + thumbnail = t.optional(t.string), + + -- Optional text to display in the Item Tile banner in place of the footer + bannerText = t.optional(t.string), + + -- Optional enum specifying the item icon type, will create an icon showing the item type on the card + itemIconType = t.optional(enumerateValidator(ItemTileEnums.ItemIconType)), + + -- Whether the tile is selected or not + isSelected = t.optional(t.boolean), + + -- Whether the tile is for a premium item or not + isPremium = t.optional(t.boolean), + + -- Enums specifying the restriction types if there are restrictions for the item + restrictionTypes = t.optional(t.map(enumerateValidator(ItemTileEnums.Restriction), t.boolean)), + + -- Optional information about the restriction + restrictionInfo = t.optional(t.table), + + -- Optional boolean indicating whether to create an overlay to round the corners of the image + hasRoundedCorners = t.optional(t.boolean), + + -- Optional tile status text + statusText = t.optional(t.string), + + -- Enum specifying the style for the status component + statusStyle = t.optional(enumerateValidator(ItemTileEnums.StatusStyle)), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) + +local function tileBannerUseValidator(props) + if props.bannerText and props.footer then + return false, "A custom footer and bannerText can't be used together" + end + + return true +end + +local validateProps = t.intersection(itemTileInterface, tileBannerUseValidator) + +ItemTile.defaultProps = { + titleTextLineCount = 2, + innerPadding = 8, + isSelected = false, + isPremium = false, + hasRoundedCorners = true, +} + +function ItemTile:render() + assert(validateProps(self.props)) + + local footer = self.props.footer + local bannerText = self.props.bannerText + local hasRoundedCorners = self.props.hasRoundedCorners + local innerPadding = self.props.innerPadding + local isPremium = self.props.isPremium + local isSelected = self.props.isSelected + local itemIconType = self.props.itemIconType + local name = self.props.name + local onActivated = self.props.onActivated + local restrictionInfo = self.props.restrictionInfo + local restrictionTypes = self.props.restrictionTypes + local statusStyle = self.props.statusStyle + local statusText = self.props.statusText + local titleTextLineCount = self.props.titleTextLineCount + local thumbnail = self.props.thumbnail + + local hasOverlayComponents = false + local overlayComponents = {} + + if itemIconType then + hasOverlayComponents = true + + overlayComponents.ItemIconType = Roact.createElement(ItemIcon, { + itemIconType = itemIconType, + }) + end + + if restrictionTypes then + hasOverlayComponents = true + + overlayComponents.RestrictionStatus = Roact.createElement(ItemRestrictionStatus, { + restrictionInfo = restrictionInfo, + restrictionTypes = restrictionTypes, + }) + end + + if statusText then + hasOverlayComponents = true + + overlayComponents.Status = Roact.createElement(ItemTileStatus, { + statusStyle = statusStyle, + statusText = statusText, + }) + end + + return Roact.createElement(Tile, { + bannerText = bannerText, + footer = footer, + hasRoundedCorners = hasRoundedCorners, + innerPadding = innerPadding, + isSelected = isSelected, + name = name, + onActivated = onActivated, + thumbnail = thumbnail, + thumbnailOverlayComponents = hasOverlayComponents and overlayComponents or nil, + titleIcon = isPremium and Images["icons/status/premium_small"] or nil, + titleTextLineCount = titleTextLineCount, + + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + [Roact.Ref] = self.props[Roact.Ref], + }) +end + +return ItemTile diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTileFooter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTileFooter.lua new file mode 100644 index 0000000..4e93d3a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTileFooter.lua @@ -0,0 +1,87 @@ +local ItemTileRoot = script.Parent +local TileRoot = ItemTileRoot.Parent +local App = TileRoot.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local Images = require(UIBlox.App.ImageSet.Images) + +local ShimmerPanel = require(UIBlox.App.Loading.ShimmerPanel) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local ICON_PADDING = 4 + +local ItemTileFooter = Roact.PureComponent:extend("ItemTileFooter") + +local validateProps = t.strictInterface({ + -- The price text of footer + priceText = t.optional(t.string), + + -- Is the item owned + isOwned = t.optional(t.boolean) +}) + +function ItemTileFooter:render() + assert(validateProps(self.props)) + + local priceText = self.props.priceText + local isOwned = self.props.isOwned + + local icon = Images["icons/common/robux_small"] + if isOwned then + icon = Images["icons/status/item/owned"] + end + return withStyle(function(stylePalette) + local font = stylePalette.Font.SubHeader1.Font + local fontSize = stylePalette.Font.BaseSize * stylePalette.Font.SubHeader1.RelativeSize + local theme = stylePalette.Theme + + local iconSize = icon.ImageRectSize / Images.ImagesResolutionScale + + local priceIsNumber = priceText and tonumber(priceText:sub(1, 1)) + local showIcon = priceText and (priceIsNumber or isOwned) + + local iconPadding = 0 + if showIcon then + iconPadding = iconSize.X + ICON_PADDING + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + Shimmer = not priceText and Roact.createElement(ShimmerPanel, { + Size = UDim2.new(0.8, 0, 0, fontSize), + }), + + icon = showIcon and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = icon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Size = UDim2.new(0, iconSize.X, 0, iconSize.Y), + }), + + TextLabel = priceText and Roact.createElement("TextLabel", { + AnchorPoint = Vector2.new(1, 0), + BackgroundTransparency = 1, + Position = UDim2.new(1, 0, 0, 0), + Size = UDim2.new(1, -iconPadding, 1, 0), + Font = font, + TextColor3 = theme.SecondaryContent.Color, + TextTransparency = theme.SecondaryContent.Transparency, + TextSize = fontSize, + Text = priceText, + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + }) + }) + end) +end + +return ItemTileFooter \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTileStatus.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTileStatus.lua new file mode 100644 index 0000000..e6e7e3f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/ItemTile/ItemTileStatus.lua @@ -0,0 +1,94 @@ +local ItemTileRoot = script.Parent +local TileRoot = ItemTileRoot.Parent +local App = TileRoot.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) +local Images = require(UIBlox.App.ImageSet.Images) +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local ItemTileEnums = require(TileRoot.Enum.ItemTileEnums) + +local ItemTileStatus = Roact.PureComponent:extend("ItemTileStatus") + +local MAX_TEXT_SIZE = Vector2.new(50, 20) +local TEXT_PADDING = Vector2.new(10, 6) + +local PADDING_LEFT = 12 +local PADDING_TOP = 12 + +local validateProps = t.strictInterface({ + -- The text to display in the status component + statusText = t.string, + + -- Enum specifying the style for the status component + statusStyle = enumerateValidator(ItemTileEnums.StatusStyle), +}) + +local function getStyle(theme, statusStyle) + if statusStyle == ItemTileEnums.StatusStyle.Info then + return { + Background = theme.SystemPrimaryDefault, + Text = theme.SystemPrimaryContent, + } + elseif statusStyle == ItemTileEnums.StatusStyle.Alert then + return { + Background = theme.Alert, + Text = theme.TextEmphasis, + } + else + return { + Background = theme.SystemPrimaryDefault, + Text = theme.SystemPrimaryContent, + } + end +end + +function ItemTileStatus:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local fontInfo = stylePalette.Font + + local statusText = self.props.statusText + local statusStyle = self.props.statusStyle + + local font = fontInfo.CaptionHeader.Font + local fontSize = fontInfo.BaseSize * fontInfo.CaptionHeader.RelativeSize + local textSize = GetTextSize(statusText, fontSize, font, MAX_TEXT_SIZE) + + local styleInfo = getStyle(theme, statusStyle) + + return Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = Images["component_assets/circle_17"], + ImageColor3 = styleInfo.Background.Color, + ImageTransparency = styleInfo.Background.Transparency, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + Position = UDim2.new(0, PADDING_LEFT, 0, PADDING_TOP), + Size = UDim2.new(0, textSize.X + TEXT_PADDING.X, 0, textSize.Y + TEXT_PADDING.Y), + }, { + Text = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Font = font, + TextSize = fontSize, + Text = statusText, + TextColor3 = styleInfo.Text.Color, + TextTransparency = styleInfo.Text.TextTransparency, + TextTruncate = Enum.TextTruncate.AtEnd, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + Size = UDim2.new(1, 0, 1, 0), + }), + }) + end) +end + +return ItemTileStatus \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/MenuTile/MenuTile.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/MenuTile/MenuTile.lua new file mode 100644 index 0000000..4301cce --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/MenuTile/MenuTile.lua @@ -0,0 +1,264 @@ +local TextService = game:GetService("TextService") + +local MenuTileRoot = script.Parent +local Tile = MenuTileRoot.Parent +local App = Tile.Parent +local UIBlox = App.Parent +local Core = UIBlox.Core +local Packages = UIBlox.Parent + +local Otter = require(Packages.Otter) +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local UIBloxConfig = require(UIBlox.UIBloxConfig) + +local Badge = require(App.Indicator.Badge) +local IconSize = require(App.ImageSet.Enum.IconSize) +local getIconSize = require(App.ImageSet.getIconSize) +local Images = require(App.ImageSet.Images) + +local ControlState = require(Core.Control.Enum.ControlState) +local Interactable = require(Core.Control.Interactable) +local ImageSetComponent = require(Core.ImageSet.ImageSetComponent) +local validateImage = require(Core.ImageSet.Validator.validateImage) + +local withStyle = require(UIBlox.Style.withStyle) +local divideTransparency = require(UIBlox.Utility.divideTransparency) + +local FULLY_TRANSPARENT = 1 +local LIST_PADDING = UDim.new(0, 12) +local PADDING_PADDING = UDim.new(0, 8) +local TITLE_MAX_NUMBER_OF_LINES = 2 + +-- ~0.33 duration +local SPRING_PARAMETERS = { + frequency = 6, + dampingRatio = 1, +} + +local Z_INDEX = { + BACKGROUND = 1, + HOVER_MASK = 2, + ICON_AND_TITLE_CONTAINER = 3, + BADGE_CONTAINER = 4, + ROUNDED_CORNERS_MASK = 5, +} + +local LAYOUT_ORDER = { + ICON = 1, + TITLE = 2, +} + +local MenuTile = Roact.Component:extend("MenuTile") + +MenuTile.defaultProps = { + size = UDim2.fromScale(1, 1), +} + +MenuTile.validateProps = t.strictInterface({ + -- Frame Props + size = t.optional(t.UDim2), + position = t.optional(t.UDim2), + layoutOrder = t.optional(t.number), + + -- Menu Tile specific props + badgeValue = t.optional(t.union(t.string, t.number)), + icon = validateImage, + title = t.string, + onActivated = t.callback, +}) + +function MenuTile:init() + self.hoverTransparency, self.updateHoverTransparency = Roact.createBinding(FULLY_TRANSPARENT) + self.hoverTransparencyMotor = Otter.createSingleMotor(FULLY_TRANSPARENT) + self.hoverTransparencyMotor:onStep(self.updateHoverTransparency) + self.hoverTransparencyMotor:onComplete(function(value) + if value == FULLY_TRANSPARENT then + self:setState({ + showHoverMask = false, + }) + end + end) + + self:setState({ + backgroundTransparency = 0, + iconTransparency = 0, + titleTransparency = 0, + showHoverMask = false, + }) +end + +function MenuTile:render() + local backgroundTransparency = self.state.backgroundTransparency + local iconTransparency = self.state.iconTransparency + local titleTransparency = self.state.titleTransparency + + local badgeValue = self.props.badgeValue + local icon = self.props.icon + local layoutOrder = self.props.layoutOrder + local onActivated = self.props.onActivated + local position = self.props.position + local size = self.props.size + local title = self.props.title + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + + local backgroundStyle = theme.BackgroundUIDefault + local iconStyle = theme.IconDefault + local roundedCornersStyle = theme.BackgroundDefault + local hoverStyle = theme.BackgroundOnHover + + local titleStyle = theme.TextDefault + local titleFont = stylePalette.Font.SubHeader1 + local titleFontSize = titleFont.RelativeSize * stylePalette.Font.BaseSize + local titleTextOneLineSizeY = TextService:GetTextSize(title, titleFontSize, titleFont.Font, + Vector2.new(100, titleFontSize)).Y + + local function onStateChanged(oldState, newState) + if newState == ControlState.Hover then + self:setState({ + backgroundTransparency = backgroundStyle.Transparency, + iconTransparency = iconStyle.Transparency, + titleTransparency = titleStyle.Transparency, + showHoverMask = true, + }) + self.hoverTransparencyMotor:setGoal(Otter.spring(hoverStyle.Transparency, SPRING_PARAMETERS)) + elseif newState == ControlState.Default then + self:setState({ + backgroundTransparency = backgroundStyle.Transparency, + iconTransparency = iconStyle.Transparency, + titleTransparency = titleStyle.Transparency, + }) + self.hoverTransparencyMotor:setGoal(Otter.spring(FULLY_TRANSPARENT, SPRING_PARAMETERS)) + elseif newState == ControlState.Pressed then + self:setState({ + backgroundTransparency = divideTransparency(backgroundStyle.Transparency, 2), + iconTransparency = divideTransparency(iconStyle.Transparency,2), + titleTransparency = divideTransparency(titleStyle.Transparency, 2), + showHoverMask = false, + }) + self.hoverTransparencyMotor:setGoal(Otter.instant(FULLY_TRANSPARENT)) + end + end + + return Roact.createElement(Interactable, { + Size = size, + Position = position, + BackgroundTransparency = 1, -- Default is 0 + LayoutOrder = layoutOrder, + onStateChanged = onStateChanged, + [Roact.Event.Activated] = onActivated, + }, { + MenuTileFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = size, + }, { + Background = Roact.createElement("Frame", { + BackgroundColor3 = backgroundStyle.Color, + BackgroundTransparency = backgroundTransparency, + BorderSizePixel = 0, + Size = UDim2.fromScale(1,1), + ZIndex = Z_INDEX.BACKGROUND, + }, { + RoundedCornerUI = UIBloxConfig.useNewUICornerRoundedCorners and Roact.createElement("UICorner", { + CornerRadius = UDim.new(0, 8), + }), + }), + HoverMask = self.state.showHoverMask and Roact.createElement("Frame", { + BackgroundColor3 = hoverStyle.Color, + BackgroundTransparency = self.hoverTransparency, + BorderSizePixel = 0, + Size = UDim2.fromScale(1,1), + ZIndex = Z_INDEX.HOVER_MASK, + }, { + RoundedCornerUI = UIBloxConfig.useNewUICornerRoundedCorners and Roact.createElement("UICorner", { + CornerRadius = UDim.new(0, 8), + }), + }), + IconAndTitleContainer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1,1), + ZIndex = Z_INDEX.ICON_AND_TITLE_CONTAINER + }, { + IconAndTitleUIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + Padding = LIST_PADDING, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + IconAndTitleUIPadding = Roact.createElement("UIPadding", { + PaddingBottom = PADDING_PADDING, + PaddingLeft = PADDING_PADDING, + PaddingRight = PADDING_PADDING, + -- pad by the height of the title line, to position Icon in the middle, + -- title height is always 2 lines high + PaddingTop = PADDING_PADDING + UDim.new(0, titleTextOneLineSizeY), + }), + Icon = icon and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = icon, + ImageColor3 = iconStyle.Color, + ImageTransparency = iconTransparency, + LayoutOrder = LAYOUT_ORDER.ICON, + Size = UDim2.fromOffset(getIconSize(IconSize.Large), getIconSize(IconSize.Large)), + }), + -- GenericText, does not limit to 2 lines + Title = title and Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Font = titleFont.Font, + LayoutOrder = LAYOUT_ORDER.TITLE, + Size = UDim2.new(1, 0, 0, titleTextOneLineSizeY * TITLE_MAX_NUMBER_OF_LINES), + Text = title, + TextColor3 = titleStyle.Color, + TextSize = titleFontSize, + TextTransparency = titleTransparency, + TextTruncate = Enum.TextTruncate.AtEnd, + TextWrapped = true, + TextYAlignment = Enum.TextYAlignment.Top, + }), + }), + BadgeContainer = badgeValue and Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1,1), + ZIndex = Z_INDEX.BADGE_CONTAINER, + }, { + BadgeUIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + Padding = LIST_PADDING, + VerticalAlignment = Enum.VerticalAlignment.Top, + }), + BadgeUIPadding = Roact.createElement("UIPadding", { + PaddingBottom = PADDING_PADDING, + PaddingLeft = PADDING_PADDING, + PaddingRight = PADDING_PADDING, + PaddingTop = PADDING_PADDING, + }), + Badge = Roact.createElement(Badge, { + value = badgeValue, + }), + }), + RoundedCornersMask = not UIBloxConfig.useNewUICornerRoundedCorners and + Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = Images["component_assets/circle_17_mask"], + ImageColor3 = roundedCornersStyle.Color, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 8, 9, 9), + Size = UDim2.fromScale(1, 1), + ZIndex = Z_INDEX.ROUNDED_CORNERS_MASK, + }), + } + ) + }) + end) +end + +function MenuTile:willUnmount() + if self.hoverTransparencyMotor then + self.hoverTransparencyMotor:destroy() + end +end +return MenuTile diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/MenuTile/MenuTile.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/MenuTile/MenuTile.spec.lua new file mode 100644 index 0000000..f98284b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/MenuTile/MenuTile.spec.lua @@ -0,0 +1,83 @@ +return function() + local MenuTileRoot = script.Parent + local Tile = MenuTileRoot.Parent + local App = Tile.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local Images = require(App.ImageSet.Images) + local MenuTile = require(MenuTileRoot.MenuTile) + + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local function onActivatedDummy() end + + describe("mount/unmount", function() + it("should mount and unmount with default properties", function() + local menuTileWithStyle = mockStyleComponent({ + MenuTileTest = Roact.createElement(MenuTile, { + -- required + icon = Images["icons/menu/shop_large"], + onActivated = onActivatedDummy, + title = "Shop", + }) + }) + local handle = Roact.mount(menuTileWithStyle) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + it("should mount and unmount with valid properties", function() + local menuTileWithStyle = mockStyleComponent({ + MenuTileTest = Roact.createElement(MenuTile, { + -- required + icon = Images["icons/menu/shop_large"], + onActivated = onActivatedDummy, + title = "Shop", + -- optional + badgeValue = "0", + layoutOrder = 2, + position = UDim2.new(0, 50, 0,100), + size = UDim2.new(1, 30, 1, 50), + }) + }) + local handle = Roact.mount(menuTileWithStyle) + expect(handle).to.be.ok() + Roact.unmount(handle) + end) + + -- skipping this until https://jira.rbx.com/browse/MOBLUAPP-2424 is merged to CI + itSKIP("mount should throw when created with invalid properties", function() + local function expectToThrowForInvalidProps(props) + -- make sure we start with valid props + local testProps = { + icon = Images["icons/menu/shop_large"], + onActivated = onActivatedDummy, + title = "Shop" + } + -- add/replace props passed in + for name, _ in pairs(props) do + testProps[name] = props[name] + end + + local menuTileWithStyle = mockStyleComponent({ + MenuTileTest = Roact.createElement(MenuTile, testProps) + }) + + expect(function() + Roact.mount(menuTileWithStyle) + end).to.throw() + end + + expectToThrowForInvalidProps({ icon = 1 }) + expectToThrowForInvalidProps({ onActivated= 2 }) + expectToThrowForInvalidProps({ title = 3 }) + expectToThrowForInvalidProps({ badgeValue = onActivatedDummy }) + expectToThrowForInvalidProps({ layoutOrder = "3" }) + expectToThrowForInvalidProps({ position = 3 }) + expectToThrowForInvalidProps({ size = 3 }) + expectToThrowForInvalidProps({ NotInTheInterface = "Really it is not there" }) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/SaveTile/SaveTile.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/SaveTile/SaveTile.lua new file mode 100644 index 0000000..a6b2676 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/SaveTile/SaveTile.lua @@ -0,0 +1,66 @@ +local SaveTileRoot = script.Parent +local TileRoot = SaveTileRoot.Parent +local App = TileRoot.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Images = require(UIBlox.App.ImageSet.Images) +local Tile = require(TileRoot.BaseTile.Tile) + +local SaveTile = Roact.PureComponent:extend("SaveTile") + + +local validateProps = t.strictInterface({ + -- Optional boolean indicating whether to create an overlay to round the corners of the image + hasRoundedCorners = t.optional(t.boolean), + + -- The function that gets called on SaveTile click + onActivated = t.optional(t.callback), + + -- The item's thumbnail that will show a loading state if nil + thumbnail = t.optional(t.union(t.string, t.table)), + + -- The item thumbnail's size + thumbnailSize = t.optional(t.UDim2), + + -- optional parameters for RoactGamepad + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + [Roact.Ref] = t.optional(t.table), +}) + +SaveTile.defaultProps = { + hasRoundedCorners = true, + thumbnail = Images["icons/actions/edit/add"], + thumbnailSize = UDim2.new(0, 36, 0, 36), +} + +function SaveTile:render() + assert(validateProps(self.props)) + + local hasRoundedCorners = self.props.hasRoundedCorners + local onActivated = self.props.onActivated + local thumbnail = self.props.thumbnail + local thumbnailSize = self.props.thumbnailSize + + return Roact.createElement(Tile, { + hasRoundedCorners = hasRoundedCorners, + name = "", + onActivated = onActivated, + thumbnail = thumbnail, + thumbnailSize = thumbnailSize, + + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + [Roact.Ref] = self.props[Roact.Ref], + }) +end + +return SaveTile diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/__stories__/Tile.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/__stories__/Tile.story.lua new file mode 100644 index 0000000..6e4974d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/App/Tile/__stories__/Tile.story.lua @@ -0,0 +1,150 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local TileRoot = script.Parent.Parent +local App = TileRoot.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Tile = require(TileRoot.BaseTile.Tile) +local Images = require(UIBlox.App.ImageSet.Images) + +local TileStoryContainer = Roact.PureComponent:extend("TileStoryContainer") + +local PADDING = 20 +local FOOTER_HEIGHT = 50 +local NAME_HEIGHT = 20 + +local function createFooter() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, FOOTER_HEIGHT), + }, { + TextLabel = Roact.createElement("TextLabel", { + Text = "Your custom footer goes here.", + Size = UDim2.new(1, 0, 1, 0), + }) + }) +end + +function TileStoryContainer:init() + self.state = { + image = nil, + longerLoadImage = nil, + name = nil, + } +end + +function TileStoryContainer:didMount() + -- Simulate component load + spawn(function() + wait(2.0) + self:setState({ + image = "rbxassetid://924320031", + name = "Item Name", + }) + wait(2.0) + self:setState({ + longerLoadImage = "rbxassetid://924320031", + }) + end) +end + +function TileStoryContainer:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, 20), + PaddingTop = UDim.new(0, 20), + }), + + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 20), + }), + + FullItemTileContainer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = 0, + Size = UDim2.new(0, 200, 0, 200 + NAME_HEIGHT + PADDING + FOOTER_HEIGHT), + }, { + FullItemTile = Roact.createElement(Tile, { + footer = createFooter(), + name = self.state.name, + onActivated = function() end, + hasRoundedCorners = false, + thumbnail = self.state.image, + }), + }), + + LongerLoadItemTileContainer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = 1, + Size = UDim2.new(0, 200, 0, 200 + NAME_HEIGHT + PADDING+ FOOTER_HEIGHT), + }, { + ThumbnailLongerLoadItemTile = Roact.createElement(Tile, { + footer = createFooter(), + name = self.state.name, + onActivated = function() end, + hasRoundedCorners = false, + thumbnail = self.state.longerLoadImage, + }), + }), + + FooterlessItemTileContainer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 200, 0, 200 + NAME_HEIGHT), + LayoutOrder = 2, + }, { + FooterlessItemTile = Roact.createElement(Tile, { + name = self.state.name, + onActivated = function() end, + hasRoundedCorners = false, + thumbnail = self.state.image, + }), + }), + + OverriddenSizedImageTileContainer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 200, 0, 200 + NAME_HEIGHT), + LayoutOrder = 3, + }, { + OverriddenSizedImagesetTile = Roact.createElement(Tile, { + name = self.state.name, + onActivated = function() end, + hasRoundedCorners = false, + thumbnail = self.state.image, + thumbnailSize = UDim2.new(0, 100, 0, 100), + }), + }), + + ImagesetTileContainer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 200, 0, 200 + NAME_HEIGHT), + LayoutOrder = 4, + }, { + ImagesetTile = Roact.createElement(Tile, { + name = "", + onActivated = function() end, + thumbnail = Images["icons/status/item/owned"], + thumbnailSize = UDim2.new(0, 25, 0, 25), + }), + }), + }) +end + +return function(target) + local styleProvider = Roact.createElement(StoryView, {}, { + Roact.createElement(TileStoryContainer) + }) + + local handle = Roact.mount(styleProvider, target, "TileStoryContainer") + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/Enum/SlidingDirection.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/Enum/SlidingDirection.lua new file mode 100644 index 0000000..e064287 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/Enum/SlidingDirection.lua @@ -0,0 +1,11 @@ +local Core = script.Parent.Parent.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate(script.Name, { + "Up", + "Down", + "Left", + "Right", +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.lua new file mode 100644 index 0000000..5d8bd9e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.lua @@ -0,0 +1,74 @@ +local AnimationRoot = script.Parent +local Core = AnimationRoot.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local t = require(UIBlox.Parent.t) +local enumerateValidator = require(UIBlox.Utility.enumerateValidator) +local SlidingDirection = require(AnimationRoot.Enum.SlidingDirection) +local SpringAnimatedItem = require(UIBlox.Utility.SpringAnimatedItem) + +local ANIMATION_SPRING_SETTINGS = { + dampingRatio = 1, + frequency = 4, +} + +-- A transparent frame covers the whole page, represent current navigated page +local SlidingContainer = Roact.PureComponent:extend("SlidingContainer") + +local InitialPosition = { + -- Slide up from bottom of page + [SlidingDirection.Up] = UDim2.new(0, 0, 1, 0), + + -- Slide down from top of page + [SlidingDirection.Down] = UDim2.new(0, 0, -1, 0), + + -- Slide left from right of page + [SlidingDirection.Left] = UDim2.new(1, 0, 0, 0), + + -- Slide right from left of page + [SlidingDirection.Right] = UDim2.new(-1, 0, 0, 0), +} + +local validateProps = t.strictInterface({ + layoutOrder = t.optional(t.integer), + onComplete = t.optional(t.callback), + show = t.optional(t.boolean), + slidingDirection = enumerateValidator(SlidingDirection), + springOptions = t.optional(t.table), + [Roact.Children] = t.optional(t.table), +}) + +SlidingContainer.defaultProps = { + springOptions = ANIMATION_SPRING_SETTINGS, +} + +function SlidingContainer:render() + assert(validateProps(self.props)) + + local show = self.props.show + local slidingDirection = self.props.slidingDirection + + return Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + springOptions = self.props.springOptions, + animatedValues = { + step = show and 0 or 1, + }, + mapValuesToProps = function(values) + local position = InitialPosition[slidingDirection] + return { + Position = UDim2.new(position.X.Scale * values.step, 0, position.Y.Scale * values.step, 0), + } + end, + regularProps = { + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + Position = show and InitialPosition[slidingDirection] or UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + }, + onComplete = self.props.onComplete, + [Roact.Children] = self.props[Roact.Children], + }) +end + +return SlidingContainer diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.spec.lua new file mode 100644 index 0000000..38fc8d4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.spec.lua @@ -0,0 +1,44 @@ +return function() + local UIBloxRoot = script.Parent.Parent.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local SlidingContainer = require(script.Parent.SlidingContainer) + local SlidingDirection = require(script.Parent.Enum.SlidingDirection) + + local createSlidingContainer = function(props) + return mockStyleComponent({ + SlidingContainer = Roact.createElement(SlidingContainer, props) + }) + end + + it("should throw on empty slidingDirection", function() + local element = createSlidingContainer({}) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors with valid slidingDirection", function() + local element = createSlidingContainer({ + slidingDirection = SlidingDirection.Down, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when show is true", function() + local element = createSlidingContainer({ + show = true, + slidingDirection = SlidingDirection.Right, + [Roact.Children] = { + Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }), + }, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.story.lua new file mode 100644 index 0000000..893a676 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SlidingContainer.story.lua @@ -0,0 +1,54 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Roact = require(ReplicatedStorage.Packages.Roact) + +local SlidingContainer = require(script.Parent.SlidingContainer) +local SlidingDirection = require(script.Parent.Enum.SlidingDirection) + +local SlidingContainerComponent = Roact.PureComponent:extend("SlidingContainerComponent") + +function SlidingContainerComponent:init() + self.state = { + show = false, + } + + self.buttonRef = Roact.createRef() +end + +function SlidingContainerComponent:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + SlidingContainer = Roact.createElement(SlidingContainer, { + show = self.state.show, + slidingDirection = SlidingDirection.Down, + }, { + Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }) + }), + ControlButton = Roact.createElement("TextButton", { + BackgroundColor3 = Color3.fromRGB(2, 183, 87), + Size = UDim2.new(0, 200, 0, 50), + Text = "Page slide down", + ZIndex = 2, + [Roact.Event.Activated] = function() + self:setState({ + show = not self.state.show, + }) + self.buttonRef.current.Text = self.state.show + and "Page slide up" or "Page slide down" + end, + [Roact.Ref] = self.buttonRef, + }) + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(SlidingContainerComponent), target, "SlidingContainer") + + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.lua new file mode 100644 index 0000000..bbc9005 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.lua @@ -0,0 +1,74 @@ +local RunService = game:GetService("RunService") +local AnimationRoot = script.Parent +local Core = AnimationRoot.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local t = require(UIBlox.Parent.t) + +local ImageSetLabel = require(UIBlox.Core.ImageSet.ImageSetComponent).Label + +local SpinningImage = Roact.PureComponent:extend("SpinningImage") + +SpinningImage.validateProps = t.strictInterface({ + image = t.table, + size = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + position = t.optional(t.UDim2), + rotationRate = t.optional(t.number), +}) + +SpinningImage.defaultProps = { + rotationRate = 360, +} + +function SpinningImage:init() + self.state = { + angle = 0, + } +end + +function SpinningImage:didMount() + self.heartbeatConnection = RunService.Heartbeat:Connect(function(dt) + local newAngle = self.state.angle + self.props.rotationRate*dt + if newAngle > 360 then + newAngle = newAngle - 360 + elseif newAngle < 0 then + newAngle = newAngle + 360 + end + self:setState({ + angle = newAngle + }) + end) +end + +function SpinningImage:willUnmount() + self.heartbeatConnection:Disconnect() +end + +function SpinningImage.getDerivedStateFromProps(nextProps, lastState) + local imageSize = nextProps.image.ImageRectSize + return { + size = nextProps.size or UDim2.fromOffset(imageSize.X, imageSize.Y) + } +end + +function SpinningImage:render() + return Roact.createElement("Frame", { + Size = self.state.size, + AnchorPoint = self.props.anchorPoint, + Position = self.props.position, + BackgroundTransparency = 1, + }, { + inner = Roact.createElement(ImageSetLabel, { + Size = self.state.size, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.fromScale(0.5, 0.5), + Image = self.props.image, + Rotation = self.state.angle, + BackgroundTransparency = 1, + }) + }) +end + +return SpinningImage diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.spec.lua new file mode 100644 index 0000000..4432e4f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.spec.lua @@ -0,0 +1,34 @@ +return function() + local UIBloxRoot = script.Parent.Parent.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local SpinningImage = require(script.Parent.SpinningImage) + local Images = require(UIBloxRoot.App.ImageSet.Images) + + it("should throw on empty image", function() + local element = Roact.createElement(SpinningImage, {}) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create and destroy without errors with valid image", function() + local element = Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should accept all valid props", function() + local element = Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + position = UDim2.new(1, 2, 3, 4), + anchorPoint = Vector2.new(1, 2), + rotationRate = 1, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.story.lua new file mode 100644 index 0000000..b0b4d09 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/SpinningImage.story.lua @@ -0,0 +1,47 @@ +local CoreRoot = script.Parent.Parent +local UIBlox = CoreRoot.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) + +local Images = require(UIBlox.App.ImageSet.Images) + +local SpinningImage = require(script.Parent.SpinningImage) + +local SpinningImageStory = Roact.PureComponent:extend("SpinningImageStory") + +function SpinningImageStory:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + Layout = Roact.createElement("UIListLayout"), + Spinner1 = Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + rotationRate = -720, + }), + Spinner2 = Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + rotationRate = -360, + }), + Spinner3 = Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + rotationRate = 0, + }), + Spinner4 = Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + }), + Spinner5 = Roact.createElement(SpinningImage, { + image = Images["icons/graphic/loadingspinner"], + rotationRate = 720, + }), + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(SpinningImageStory), target, "SpinningImageContainer") + + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/withAnimation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/withAnimation.lua new file mode 100644 index 0000000..6877f11 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/withAnimation.lua @@ -0,0 +1,74 @@ +local Animation = script.Parent +local Core = Animation.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Otter = require(Packages.Otter) +local t = require(Packages.t) + +--[[ + A convenient animation wrapper + provides the same features as SpringAnimatedItem, with a simpler API + + withAnimation({ + option1 = targetValue, + option2 = target2, + }, function(values) + local currentOption = values.option1 + return Roact.createComponent(.....) + end) +]] + +local AnimatedComponent = Roact.PureComponent:extend("AnimatedComponent") + +AnimatedComponent.validateProps = t.strictInterface({ + -- target values to animate to + values = t.table, + -- function(actualValues: table) called to render components at the current values + render = t.callback, + -- otter spring options + options = t.optional(t.table), +}) + +function AnimatedComponent:init() + self:setState({ + values = self.props.values, + }) + self.motor = Otter.createGroupMotor(self.props.values) + self.motor:onStep(function(values) + self:setState({ + values = values, + }) + end) +end + +function AnimatedComponent:willUpdate(nextProps) + local values = self.props.values + local options = self.props.options + local goals = {} + for key, targetValue in pairs(values) do + goals[key] = Otter.spring(targetValue, options) + end + self.motor:setGoal(goals) +end + +function AnimatedComponent:render() + local values = self.state.values + return self.props.render(values) +end + +function AnimatedComponent:willUnmount() + if self.motor then + self.motor:destroy() + self.motor = nil + end +end + +return function(goals, callback, options) + return Roact.createElement(AnimatedComponent, { + values = goals, + options = options, + render = callback, + }) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/withAnimation.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/withAnimation.spec.lua new file mode 100644 index 0000000..0946b35 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Animation/withAnimation.spec.lua @@ -0,0 +1,32 @@ +return function() + local Navigation = script.Parent + local App = Navigation.Parent + local UIBloxRoot = App.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + + local withAnimation = require(script.Parent.withAnimation) + + local lastValues = nil + local component = function(props) + return withAnimation(props, function(values) + lastValues = values + return Roact.createElement("Frame") + end) + end + + it("should create and destroy without errors", function() + local element = Roact.createElement(component) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should mount contents using initial values", function() + lastValues = nil + local element = Roact.createElement(component, { + animatedValue = 5, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(lastValues.animatedValue).to.equal(5) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/ThreeSectionBar.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/ThreeSectionBar.lua new file mode 100644 index 0000000..e53f8aa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/ThreeSectionBar.lua @@ -0,0 +1,217 @@ +local Bar = script.Parent +local Core = Bar.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) +local FitFrame = require(Packages.FitFrame) +local bindingValidator = require(Core.Utility.bindingValidator) + +local PADDING_BETWEEN_SIDE_AND_CENTER = 8 + +local ThreeSectionBar = Roact.PureComponent:extend("ThreeSectionBar") +ThreeSectionBar.validateProps = t.strictInterface({ + BackgroundColor3 = t.optional(t.Color3), + BackgroundTransparency = t.optional(t.union(t.number, bindingValidator(t.number))), + barHeight = t.optional(t.number), + contentPaddingLeft = t.optional(t.UDim), + contentPaddingRight = t.optional(t.UDim), + estimatedCenterWidth = t.optional(t.number), + marginLeft = t.optional(t.number), + marginRight = t.optional(t.number), + onWidthChange = t.optional(t.callback), + renderCenter = t.optional(t.callback), + renderLeft = t.optional(t.callback), + renderRight = t.optional(t.callback), +}) + +ThreeSectionBar.defaultProps = { + barHeight = 32, + BackgroundTransparency = 0, + marginLeft = 0, + marginRight = 0, + + contentPaddingLeft = UDim.new(0, 0), + contentPaddingRight = UDim.new(0, 0), + + renderLeft = nil, + renderRight = nil, + renderCenter = nil, + + onWidthChange = function() + return nil + end, + estimatedCenterWidth = math.huge, +} + +function ThreeSectionBar:init() + self.leftWidth, self.updateLeftWidth = Roact.createBinding(0) + self.rightWidth, self.updateRightWidth = Roact.createBinding(0) + self.fullWidth, self.updateFullWidth = Roact.createBinding(0) + + self.computeCenteredSize = function(widths) + local leftWidth, rightWidth, fullWidth = widths[1], widths[2], widths[3] + + local largestWidth = math.max(leftWidth, rightWidth) + + local centerPoint = Vector2.new(fullWidth/2, 0) + local largestEdge = Vector2.new(largestWidth, 0) + + local distance = (centerPoint - largestEdge).magnitude + + if not self.props.renderLeft or not self.props.renderRight then + return UDim2.new(1, -largestWidth, 1, 0) + end + + -- multiply by 2, since we are splitting distance by both sides + return UDim2.new(0, distance * 2, 1, 0) + end + + self.computeBumpedPosition = function(widths) + local leftWidth, rightWidth, fullWidth = widths[1], widths[2], widths[3] + + local x = (fullWidth - leftWidth - rightWidth) /2 + + return UDim2.new(0, leftWidth + x, 0.5, 0) + end + + self.computeBumpedSize = function(widths) + local leftWidth, rightWidth = widths[1], widths[2] + + return UDim2.new(1, -leftWidth - rightWidth, 1, 0) + end +end + +function ThreeSectionBar:didUpdate() + -- When we update the props to set renderLeft or renderRight to nil, + -- corresponding bindings should be reset + if not self.props.renderLeft then + self.updateLeftWidth(0) + end + if not self.props.renderRight then + self.updateRightWidth(0) + end +end + +function ThreeSectionBar:render() + local centerAnchor = Vector2.new(0.5, 0.5) + local centerPosition + + if not self.props.renderLeft and self.props.renderRight then + centerPosition = UDim2.fromScale(0, 0.5) + elseif self.props.renderLeft and not self.props.renderRight then + centerPosition = UDim2.fromScale(1, 0.5) + else + centerPosition = UDim2.fromScale(0.5, 0.5) + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, self.props.barHeight), + BackgroundColor3 = self.props.BackgroundColor3, + BackgroundTransparency = self.props.BackgroundTransparency, + BorderSizePixel = 0, + + [Roact.Change.AbsoluteSize] = function(rbx) + self.props.onWidthChange(rbx.AbsoluteSize.X) + self.updateFullWidth(rbx.AbsoluteSize.X) + end, + }, { + leftFrame = self.props.renderLeft and Roact.createElement(FitFrame.FitFrameHorizontal, { + AnchorPoint = Vector2.new(0, 0), + Position = UDim2.fromScale(0, 0), + BackgroundTransparency = 1, + + minimumSize = UDim.new(0, 200), + height = UDim.new(1, 0), + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + contentPadding = self.props.contentPaddingLeft, + margin = { + top = 0, + left = self.props.marginLeft, + right = PADDING_BETWEEN_SIDE_AND_CENTER, + bottom = 0, + }, + [Roact.Change.AbsoluteSize] = function(rbx) + self.updateLeftWidth(rbx.AbsoluteSize.X) + end, + }, { + leftContent = self.props.renderLeft(Cryo.Dictionary.join(self.props, { + [Roact.Children] = { + -- introduce a size constraint in order to give the renderRight priority + sizeConstraint = Roact.createElement("UISizeConstraint", { + MaxSize = Roact.joinBindings({self.leftWidth, self.rightWidth, self.fullWidth}):map(function(widths) + local _, rightWidth, fullWidth = widths[1], widths[2], widths[3] + + local maxLeftWidth = math.max(0, fullWidth - rightWidth - self.props.marginLeft) + + return Vector2.new(maxLeftWidth, math.huge) + end), + }), + } + })), + }), + + centerFrame = self.props.renderCenter and Roact.createElement("Frame", { + AnchorPoint = centerAnchor, + BackgroundTransparency = 1, + + Position = Roact.joinBindings({self.leftWidth, self.rightWidth, self.fullWidth}):map(function(widths) + local centeredSize = self.computeCenteredSize(widths) + + if math.abs(centeredSize.X.Offset) <= self.props.estimatedCenterWidth then + return self.computeBumpedPosition(widths) + else + return centerPosition + end + end), + Size = Roact.joinBindings({self.leftWidth, self.rightWidth, self.fullWidth}):map(function(widths) + local centeredSize = self.computeCenteredSize(widths) + + if math.abs(centeredSize.X.Offset) <= self.props.estimatedCenterWidth then + return self.computeBumpedSize(widths) + else + return centeredSize + end + end), + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingLeft = self.props.renderLeft and UDim.new(0, 0) or UDim.new(0, self.props.marginLeft), + PaddingRight = self.props.renderRight and UDim.new(0, 0) or UDim.new(0, self.props.marginRight), + }), + ["$layout"] = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + centerContent = self.props.renderCenter(self.props), + }), + + rightFrame = self.props.renderRight and Roact.createElement(FitFrame.FitFrameHorizontal, { + AnchorPoint = Vector2.new(1, 0), + Position = UDim2.fromScale(1, 0), + BackgroundTransparency = 1, + + minimumSize = UDim.new(0, 200), + height = UDim.new(1, 0), + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + VerticalAlignment = Enum.VerticalAlignment.Center, + contentPadding = self.props.contentPaddingRight, + margin = { + top = 0, + left = PADDING_BETWEEN_SIDE_AND_CENTER, + right = self.props.marginRight, + bottom = 0, + }, + [Roact.Change.AbsoluteSize] = function(rbx) + self.updateRightWidth(rbx.AbsoluteSize.X) + end, + }, { + rightContent = self.props.renderRight(self.props), + }) + }) +end + +return ThreeSectionBar diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoCenterRender.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoCenterRender.story.lua new file mode 100644 index 0000000..cbed773 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoCenterRender.story.lua @@ -0,0 +1,35 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Bar = script.Parent.Parent +local Core = Bar.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local ThreeSectionBar = require(Bar.ThreeSectionBar) + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(ThreeSectionBar, { + renderLeft = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 255, 255), + Size = UDim2.new(0, 50, 1, 0), + Text = "Left", + }) + end, + renderRight = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 255, 222), + Size = UDim2.new(0, 50, 1, 0), + Text = "Right", + }) + end + }), + }), target, "ThreeSectionBar") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoLeftRender.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoLeftRender.story.lua new file mode 100644 index 0000000..6632063 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoLeftRender.story.lua @@ -0,0 +1,35 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Bar = script.Parent.Parent +local Core = Bar.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local ThreeSectionBar = require(Bar.ThreeSectionBar) + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(ThreeSectionBar, { + renderCenter = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 222, 255), + Size = UDim2.new(1, 0, 1, 0), + Text = "This Element fills up the remaining space", + }) + end, + renderRight = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 255, 222), + Size = UDim2.new(0, 50, 1, 0), + Text = "Right", + }) + end + }), + }), target, "ThreeSectionBar") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoRightRender.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoRightRender.story.lua new file mode 100644 index 0000000..33ea4a2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/NoRightRender.story.lua @@ -0,0 +1,35 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Bar = script.Parent.Parent +local Core = Bar.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local ThreeSectionBar = require(Bar.ThreeSectionBar) + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(ThreeSectionBar, { + renderLeft = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 255, 255), + Size = UDim2.new(0, 50, 1, 0), + Text = "Left", + }) + end, + renderCenter = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 222, 255), + Size = UDim2.new(1, 0, 1, 0), + Text = "This Element fills up the remaining space", + }) + end, + }), + }), target, "ThreeSectionBar") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/ThreeSectionBar.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/ThreeSectionBar.story.lua new file mode 100644 index 0000000..7e6f1cb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Bar/__stories__/ThreeSectionBar.story.lua @@ -0,0 +1,42 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Bar = script.Parent.Parent +local Core = Bar.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local ThreeSectionBar = require(Bar.ThreeSectionBar) + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(ThreeSectionBar, { + renderLeft = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 255, 255), + Size = UDim2.new(0, 50, 1, 0), + Text = "Left", + }) + end, + renderCenter = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 222, 255), + Size = UDim2.new(0, 50, 1, 0), + Text = "Center", + }) + end, + renderRight = function() + return Roact.createElement("TextLabel", { + BackgroundColor3 = Color3.fromRGB(222, 255, 222), + Size = UDim2.new(0, 50, 1, 0), + Text = "Right", + }) + end + }), + }), target, "ThreeSectionBar") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/GenericButton.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/GenericButton.lua new file mode 100644 index 0000000..94cd632 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/GenericButton.lua @@ -0,0 +1,196 @@ +--[[ + Create a generic button that can be themed for different state the background and content. +]] +local Button = script.Parent +local Core = Button.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) + +local Interactable = require(Core.Control.Interactable) + +local ControlState = require(Core.Control.Enum.ControlState) +local getContentStyle = require(script.Parent.getContentStyle) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local ImageSetComponent = require(Core.ImageSet.ImageSetComponent) +local ShimmerPanel = require(UIBlox.App.Loading.ShimmerPanel) +local IconSize = require(UIBlox.App.ImageSet.Enum.IconSize) +local getIconSize = require(UIBlox.App.ImageSet.getIconSize) +local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel) + +local validateImage = require(Core.ImageSet.Validator.validateImage) + +local CONTENT_PADDING = 5 + +local GenericButton = Roact.PureComponent:extend("GenericButton") + +function GenericButton:init() + self.state = { + controlState = ControlState.Initialize + } + + self.onStateChanged = function(oldState, newState) + self:setState({ + controlState = newState, + }) + if self.props.onStateChanged then + self.props.onStateChanged(oldState, newState) + end + end +end + +local colorStateMap = t.interface({ + -- The default state theme color class + [ControlState.Default] = t.string, +}) + +local validateProps = t.interface({ + --The icon of the button + icon = t.optional(validateImage), + + --The text of the button + text = t.optional(t.string), + + --The image being used as the background of the button + buttonImage = validateImage, + + --The theme color class mapping for different button states + buttonStateColorMap = colorStateMap, + + --The theme color class mapping for different content tates + contentStateColorMap = t.optional(colorStateMap), + + --The theme color class mapping for different text tates + textStateColorMap = t.optional(colorStateMap), + + --The theme color class mapping for different icon tates + iconStateColorMap = t.optional(colorStateMap), + + --Is the button disabled + isDisabled = t.optional(t.boolean), + + --Is the button loading + isLoading = t.optional(t.boolean), + + --The activated callback for the button + onActivated = t.callback, + + --The state change callback for the button + onStateChanged = t.optional(t.callback), + + --A Boolean value that determines whether user events are ignored and sink input + userInteractionEnabled = t.optional(t.boolean), + + -- Note that this component can accept all valid properties of the Roblox ImageButton instance +}) + +GenericButton.defaultProps = { + isDisabled = false, + isLoading = false, + SliceCenter = Rect.new(8, 8, 9, 9), +} + +function GenericButton:render() + return withStyle(function(style) + + assert(validateProps(self.props)) + assert(t.table(style), "Style provider is missing.") + + local currentState = self.state.controlState + + local text = self.props.text + local icon = self.props.icon + local isLoading = self.props.isLoading + local isDisabled = self.props.isDisabled + + local buttonStateColorMap = self.props.buttonStateColorMap + local contentStateColorMap = self.props.contentStateColorMap + local textStateColorMap = self.props.textStateColorMap or contentStateColorMap + local iconStateColorMap = self.props.iconStateColorMap or contentStateColorMap + + if text then + assert(colorStateMap(textStateColorMap), "textStateColorMap is missing or invalid.") + end + if icon then + assert(colorStateMap(iconStateColorMap), "iconStateColorMap is missing or invalid.") + end + + if isLoading then + isDisabled = true + end + + local buttonStyle = getContentStyle(buttonStateColorMap, currentState, style) + local textStyle = text and getContentStyle(textStateColorMap, currentState, style) + local iconStyle = icon and getContentStyle(iconStateColorMap, currentState, style) + local buttonImage = self.props.buttonImage + local fontStyle = style.Font.Header2 + + local buttonContentLayer + if isLoading then + buttonContentLayer = { + isLoadingShimmer = Roact.createElement(ShimmerPanel, { + Size = UDim2.new(1, 0, 1, 0), + }) + } + else + buttonContentLayer = self.props[Roact.Children] or { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, CONTENT_PADDING), + }), + Icon = icon and Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.new(0, getIconSize(IconSize.Medium), 0, getIconSize(IconSize.Medium)), + BackgroundTransparency = 1, + Image = icon, + ImageColor3 = iconStyle.Color, + ImageTransparency = iconStyle.Transparency, + LayoutOrder = 1, + }) or nil, + Text = text and Roact.createElement(GenericTextLabel, { + BackgroundTransparency = 1, + Text = text, + fontStyle = fontStyle, + colorStyle = textStyle, + LayoutOrder = 2, + }) or nil, + } + end + + return Roact.createElement(Interactable, Cryo.Dictionary.join(self.props, { + icon = Cryo.None, + text = Cryo.None, + buttonImage = Cryo.None, + buttonStateColorMap = Cryo.None, + contentStateColorMap = Cryo.None, + textStateColorMap = Cryo.None, + iconStateColorMap = Cryo.None, + onActivated = Cryo.None, + isLoading = Cryo.None, + [Roact.Children] = Cryo.None, + isDisabled = isDisabled, + onStateChanged = self.onStateChanged, + userInteractionEnabled = self.props.userInteractionEnabled, + Image = buttonImage, + ScaleType = Enum.ScaleType.Slice, + ImageColor3 = buttonStyle.Color, + ImageTransparency = buttonStyle.Transparency, + BackgroundTransparency = 1, + AutoButtonColor = false, + [Roact.Event.Activated] = self.props.onActivated, + }), { + ButtonContent = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, buttonContentLayer) + }) + end) +end + +return GenericButton diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/GenericButton.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/GenericButton.spec.lua new file mode 100644 index 0000000..8b90a21 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/GenericButton.spec.lua @@ -0,0 +1,161 @@ +return function() + local Button = script.Parent + local Core = Button.Parent + local UIBlox = Core.Parent + local Packages = UIBlox.Parent + + local App = UIBlox.App + local Images = require(App.ImageSet.Images) + + local Roact = require(Packages.Roact) + local GenericButton = require(Button.GenericButton) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local ControlState = require(Core.Control.Enum.ControlState) + + local IMAGE = Images["component_assets/circle_17"] + + local text = "Button" + local icon = Images["icons/common/robux_small"] + + local BUTTON_STATE_COLOR = { + [ControlState.Default] = "SystemPrimaryDefault", + } + + local CONTENT_STATE_COLOR = { + [ControlState.Default] = "SystemPrimaryContent", + } + + it("should create and destroy a button without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + text = text, + icon = icon, + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a button that is disabled without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + text = text, + icon = icon, + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + onActivated = function()end, + isDisabled = true, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a button that is loading without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + text = text, + icon = icon, + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + onActivated = function()end, + isLoading = true, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a button overriding text color without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + text = text, + icon = icon, + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + textStateColorMap = CONTENT_STATE_COLOR, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a button overriding icon color without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + text = text, + icon = icon, + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + contentStateColorMap = CONTENT_STATE_COLOR, + iconStateColorMap = CONTENT_STATE_COLOR, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a button overriding text and icon color without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + text = text, + icon = icon, + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + textStateColorMap = CONTENT_STATE_COLOR, + iconStateColorMap = CONTENT_STATE_COLOR, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy a button without text and icon color without errors", function() + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + onActivated = function()end, + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should be created as a disabled button", function() + -- luacheck: ignore unused argument oldState + local buttonState = nil + local element = mockStyleComponent({ + button = Roact.createElement(GenericButton, { + buttonImage = IMAGE, + buttonStateColorMap = BUTTON_STATE_COLOR, + onActivated = function()end, + onStateChanged = function(oldState, newState) + buttonState = newState + end, + isDisabled = true, + }), + }) + + local instance = Roact.mount(element) + expect(buttonState).to.equal(ControlState.Disabled) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/HoverButtonBackground.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/HoverButtonBackground.lua new file mode 100644 index 0000000..69a482e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/HoverButtonBackground.lua @@ -0,0 +1,32 @@ +--[[ + Creates a background square that shows up behind buttons when hovered. +]] +local Button = script.Parent +local Core = Button.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local withStyle = require(Core.Style.withStyle) + +local CORNER_RADIUS = 8 + +local HoverButtonBackground = Roact.PureComponent:extend("HoverButtonBackground") + +function HoverButtonBackground:render() + return withStyle(function(style) + local backgroundHover = style.Theme.BackgroundOnHover + + return Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundColor3 = backgroundHover.Color, + BackgroundTransparency = backgroundHover.Transparency, + }, { + corner = Roact.createElement("UICorner", { + CornerRadius = UDim.new(0, CORNER_RADIUS), + }), + }) + end) +end + +return HoverButtonBackground diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/__stories__/GenericButton.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/__stories__/GenericButton.story.lua new file mode 100644 index 0000000..2061eee --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/__stories__/GenericButton.story.lua @@ -0,0 +1,121 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryWithControls = require(ReplicatedStorage.Packages.StoryComponents.StoryWithControls) + +local Button = script.Parent.Parent +local Core = Button.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + +local App = UIBlox.App +local Images = require(App.ImageSet.Images) + +local withStyle = require(UIBlox.Core.Style.withStyle) +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) + +local GenericButton = require(Button.GenericButton) + +local GenericButtonOverviewComponent = Roact.PureComponent:extend("GenericButtonOverviewComponent") + +function GenericButtonOverviewComponent:init() + self.isMounted = false + self.state = { + isDisabled = false, + isLoading = false, + userInteractionEnabled = true, + } + + self.toggleDisabled = function() + if self.isMounted then + self:setState({ + isDisabled = not self.state.isDisabled + }) + end + end + + self.toggleLoading = function() + if self.isMounted then + self:setState({ + isLoading = not self.state.isLoading + }) + end + end + + self.toggleUserInteraction = function() + if self.isMounted then + self:setState({ + userInteractionEnabled = not self.state.userInteractionEnabled + }) + end + end +end + +function GenericButtonOverviewComponent:render() + local isDisabled = self.state.isDisabled + local isLoading = self.state.isLoading + local userInteractionEnabled = self.state.userInteractionEnabled + local buttonImage = Images["component_assets/circle_17"] + return withStyle(function(style) + return Roact.createElement(StoryWithControls, { + title = "Generic Button", + subTitle = "<>", + controls = { + { + text = self.state.disabled and "Enable Buttons" or "Disable Buttons", + onActivated = self.toggleDisabled, + }, + { + text = isLoading and "Load Buttons" or "Unload Buttons", + onActivated = self.toggleLoading, + }, + { + text = "userInteractionEnabled = "..tostring(userInteractionEnabled), + onActivated = self.toggleUserInteraction, + }, + }, + }, { + Button = Roact.createElement(GenericButton, { + Size = UDim2.new(0, 144, 0, 48), + buttonImage = buttonImage, + buttonStateColorMap = { + [ControlState.Default] = "UIDefault", + [ControlState.Hover] = "UIEmphasis", + }, + contentStateColorMap = { + [ControlState.Default] = "UIDefault", + }, + isDisabled = isDisabled, + isLoading = isLoading, + userInteractionEnabled = userInteractionEnabled, + onActivated = function() + print("Generic Button Clicked!") + end, + onStateChanged = function(oldState, newState) + if oldState ~= ControlState.Initialize then + print("state changed \n oldState:", oldState, " newState:", newState) + end + end + }) + }) + end) +end + +function GenericButtonOverviewComponent:didMount() + self.isMounted = true +end + +function GenericButtonOverviewComponent:willUnmount() + self.isMounted = false +end + +return function(target) + local handle = Roact.mount(Roact.createElement(StoryView, {}, { + Story = Roact.createElement(GenericButtonOverviewComponent), + }), target, "Button") + + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/getContentStyle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/getContentStyle.lua new file mode 100644 index 0000000..6e0b291 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Button/getContentStyle.lua @@ -0,0 +1,22 @@ +local Button = script.Parent +local Core = Button.Parent + +local ControlState = require(Core.Control.Enum.ControlState) + +return function(contentMap, controlState, style) + local contentThemeClass = contentMap[controlState] + or contentMap[ControlState.Default] + + local contentStyle = { + Color = style.Theme[contentThemeClass].Color, + Transparency = style.Theme[contentThemeClass].Transparency, + } + + --Based on the design specs, the disabled and pressed state is 0.5 * alpha value + if controlState == ControlState.Disabled or + controlState == ControlState.Pressed then + contentStyle.Transparency = 0.5 * contentStyle.Transparency + 0.5 + end + + return contentStyle +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericCell.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericCell.lua new file mode 100644 index 0000000..3411ea1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericCell.lua @@ -0,0 +1,164 @@ +local Cell = script.Parent +local Core = Cell.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local GenericTextLabel = require(Core.Text.GenericTextLabel.GenericTextLabel) +local Interactable = require(Core.Control.Interactable) +local validateColorInfo = require(UIBlox.Core.Style.Validator.validateColorInfo) +local validateFontInfo = require(UIBlox.Core.Style.Validator.validateFontInfo) + +local GenericCell = Roact.PureComponent:extend("GenericCell") + +local TOP_BOTTOM_PADDING = 12 +local LEFT_RIGHT_PADDING = 24 + +GenericCell.validateProps = t.strictInterface({ + -- Callback for when this selection is activated. + onActivated = t.optional(t.callback), + + -- Whether this selection is selected or not. + isSelected = t.optional(t.boolean), + + -- If this cell is disabled. + isDisabled = t.optional(t.boolean), + + -- Callback for when the Control State has changed. + onStateChanged = t.callback, + + -- Center title text. + titleText = t.optional(t.string), + + -- Center subtitle text. + subtitleText = t.optional(t.string), + + -- Generic right content to render. + renderRightContent = t.optional(t.callback), + + -- Width of generic right content to render, + rightContentWidth = t.optional(t.number), + + -- Generic left content to render. + renderLeftContent = t.optional(t.callback), + + -- Width of the generic left content to render. + leftContentWidth = t.optional(t.number), + + -- Color style. + colorStyle = validateColorInfo, + + -- Text Style for the title text. + textStyle = t.optional(validateColorInfo), + + -- Font style for the title text. + fontStyle = t.optional(validateFontInfo), + + -- Text style for the subtitle text. + subtitleTextStyle = t.optional(validateColorInfo), + + -- Font style for the subtitle text. + subtitleFontStyle = t.optional(validateFontInfo), + + -- Divider style. + dividerStyle = t.table, + + -- optional parameters for RoactGamepad + [Roact.Ref] = t.optional(t.table), + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + SelectionImageObject = t.optional(t.table), +}) + +GenericCell.defaultProps = { + rightContentWidth = 0, + leftContentWidth = 0, + isDisabled = false, +} + +function GenericCell:render() + assert(self.validateProps(self.props)) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BorderSizePixel = 0, + BackgroundTransparency = 1, + }, { + Interactable = Roact.createElement(Interactable, { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = self.props.colorStyle.Color, + BackgroundTransparency = self.props.colorStyle.Transparency, + BorderSizePixel = 0, + AutoButtonColor = false, + [Roact.Event.Activated] = (not self.props.isDisabled) and self.props.onActivated, + [Roact.Ref] = self.props[Roact.Ref], + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + SelectionImageObject = self.props.SelectionImageObject, + isDisabled = self.props.isDisabled, + onStateChanged = self.props.onStateChanged, + }, { + Contents = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + LayoutOrder = 1, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, LEFT_RIGHT_PADDING), + PaddingRight = UDim.new(0, LEFT_RIGHT_PADDING), + PaddingTop = UDim.new(0, TOP_BOTTOM_PADDING), + PaddingBottom = UDim.new(0, TOP_BOTTOM_PADDING), + }), + UIListLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + Frame = Roact.createElement("Frame", { + Size = UDim2.new(1, -self.props.rightContentWidth - self.props.leftContentWidth, 1, 0), + BackgroundTransparency = 1, + LayoutOrder = 2, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + TitleText = self.props.titleText and Roact.createElement(GenericTextLabel, { + Size = UDim2.new(1, 0, 1, 0), + colorStyle = self.props.textStyle, + fontStyle = self.props.fontStyle, + Text = self.props.titleText, + LayoutOrder = 1, + TextXAlignment = Enum.TextXAlignment.Left, + }), + SubTitleText = self.props.subtitleText and Roact.createElement(GenericTextLabel, { + Size = UDim2.new(1, 0, 1, 0), + colorStyle = self.props.subtitleTextStyle, + fontStyle = self.props.subtitleFontStyle, + Text = self.props.subtitleText, + LayoutOrder = 2, + TextXAlignment = Enum.TextXAlignment.Left, + }), + }), + RightContent = self.props.renderRightContent and self.props.renderRightContent(), + LeftContent = self.props.renderLeftContent and self.props.renderLeftContent(), + }), + Divider = Roact.createElement("Frame", { + Size = UDim2.new(1, -LEFT_RIGHT_PADDING, 0, 1), + Position = UDim2.new(0, LEFT_RIGHT_PADDING, 1, -1), + BorderSizePixel = 0, + BackgroundColor3 = self.props.dividerStyle.Color, + BackgroundTransparency = self.props.dividerStyle.Transparency, + }), + }) + }) +end + +return GenericCell \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericCell.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericCell.spec.lua new file mode 100644 index 0000000..8517a87 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericCell.spec.lua @@ -0,0 +1,99 @@ +return function() + local Cell = script.Parent + local Core = Cell.Parent + local UIBlox = Core.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + + local GenericCell = require(Cell.GenericCell) + + local MOCK_THEME = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + } + + local MOCK_FONT = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 12, + RelativeMinSize = 12, + } + + it("should create and destroy GenericCell without errors", function() + local element = mockStyleComponent({ + genericCell = Roact.createElement(GenericCell, { + dividerStyle = MOCK_THEME, + colorStyle = MOCK_THEME, + onStateChanged = function() end, + renderLeftContent = function() + return Roact.createElement("Frame") + end, + renderRightContent = function() + return Roact.createElement("Frame") + end, + titleText = "title", + subtitleText = "subtitle", + fontStyle = MOCK_FONT, + textStyle = MOCK_THEME, + subtitleFontStyle = MOCK_FONT, + subtitleTextStyle = MOCK_THEME, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy GenericSelectionCell with left component without errors", function() + local element = mockStyleComponent({ + genericCell = Roact.createElement(GenericCell, { + dividerStyle = MOCK_THEME, + colorStyle = MOCK_THEME, + onStateChanged = function() end, + renderLeftContent = function() + return Roact.createElement("Frame") + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy GenericSelectionCell with right component without errors", function() + local element = mockStyleComponent({ + genericCell = Roact.createElement(GenericCell, { + dividerStyle = MOCK_THEME, + colorStyle = MOCK_THEME, + onStateChanged = function() end, + renderRightContent = function() + return Roact.createElement("Frame") + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + + it("should create and destroy GenericSelectionCell with Center Text without errors", function() + local element = mockStyleComponent({ + genericCell = Roact.createElement(GenericCell, { + dividerStyle = MOCK_THEME, + colorStyle = MOCK_THEME, + onStateChanged = function() end, + titleText = "title", + subtitleText = "subtitle", + fontStyle = MOCK_FONT, + subtitleFontStyle = MOCK_FONT, + textStyle = MOCK_THEME, + subtitleTextStyle = MOCK_THEME, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericSelectionCell.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericSelectionCell.lua new file mode 100644 index 0000000..5b62bdd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericSelectionCell.lua @@ -0,0 +1,200 @@ +local Cell = script.Parent +local Core = Cell.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local ControlState = require(Packages.UIBlox.Core.Control.Enum.ControlState) +local GenericCell = require(Packages.UIBlox.Core.Cell.GenericCell) +local ImageSetComponent = require(Packages.UIBlox.Core.ImageSet.ImageSetComponent) + +local GenericSelectionCell = Roact.PureComponent:extend("GenericSelectionCell") + +local CELL_STATE_COLOR = { + [ControlState.Default] = "BackgroundDefault", + [ControlState.Hover] = "BackgroundOnHover", + [ControlState.Pressed] = "BackgroundOnPress", +} + +local ICON_STATE_COLOR = { + [ControlState.Default] = "IconDefault", + [ControlState.Hover] = "IconEmphasis", + [ControlState.Pressed] = "IconDefault", +} + +local function getCellStyle(contentMap, controlState, style) + local buttonThemeClass = contentMap[controlState] + or contentMap[ControlState.Default] + + local buttonStyle = { + Color = style.Theme[buttonThemeClass].Color, + Transparency = style.Theme[buttonThemeClass].Transparency, + } + + -- Default/Disabled background color should be theme agnostic. Other control states deal with just + -- White/Black alpha transparency which work for any background color. + if controlState == ControlState.Default or controlState == ControlState.Disabled then + buttonStyle.Transparency = 1 + end + + return buttonStyle +end + +local function getIconStyle(contentMap, controlState, style) + local iconThemeClass = contentMap[controlState] + or contentMap[ControlState.Default] + + local iconStyle = { + Color = style.Theme[iconThemeClass].Color, + Transparency = style.Theme[iconThemeClass].Transparency, + } + + --Based on the design specs, the disabled and pressed state is 0.5 * alpha value + if controlState == ControlState.Disabled or + controlState == ControlState.Pressed then + iconStyle.Transparency = 0.5 * iconStyle.Transparency + 0.5 + end + + return iconStyle +end + +local function getTextStyle(theme, controlState) + local textStyle = { + Color = theme.Color, + Transparency = theme.Transparency, + } + + --Based on the design specs, the disabled and pressed state is 0.5 * alpha value + if controlState == ControlState.Disabled or + controlState == ControlState.Pressed then + textStyle.Transparency = 0.5 * textStyle.Transparency + 0.5 + end + + return textStyle +end + +GenericSelectionCell.validateProps = t.strictInterface({ + -- The title text to display + text = t.string, + + -- Subtitle text to display + subtitleText = t.optional(t.string), + + -- Default Image to render for the right component. + defaultImage = t.union(t.string, t.table), + + -- Image to render inside the defaultImage when this component is selected. + selectedImage = t.union(t.string, t.table), + + -- Size of the default image + defaultImageSize = t.number, + + -- Size of the selected image + selectedImageSize = t.number, + + -- Callback for when this selection is activated. + onActivated = t.optional(t.callback), + + -- Whether this cell is selected or not. + isSelected = t.optional(t.boolean), + + -- If this cell is disabled + isDisabled = t.optional(t.boolean), + + -- If this cell should use the default control state + useDefaultControlState = t.optional(t.boolean), + + -- optional parameters for RoactGamepad + [Roact.Ref] = t.optional(t.table), + NextSelectionLeft = t.optional(t.table), + NextSelectionRight = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + SelectionImageObject = t.optional(t.table), +}) + +GenericSelectionCell.defaultProps = { + isSelected = false, +} + +function GenericSelectionCell:init() + self.state = { + controlState = ControlState.Default + } + + self.onStateChanged = function(_, newState) + self:setState({ + controlState = newState, + }) + end +end + +function GenericSelectionCell:render() + assert(self.validateProps(self.props)) + + return withStyle(function(stylePalette) + local font = stylePalette.Font + local theme = stylePalette.Theme + + local controlState = self.props.useDefaultControlState and ControlState.Default or self.state.controlState + + local iconStyle = getIconStyle(ICON_STATE_COLOR, controlState, stylePalette) + local colorStyle = getCellStyle(CELL_STATE_COLOR, controlState, stylePalette) + local textStyle = getTextStyle(theme.TextEmphasis, controlState) + local subtitleTextStyle = getTextStyle(theme.TextDefault, controlState) + local dividerStyle = theme.Divider + + return Roact.createElement(GenericCell, { + titleText = self.props.text, + colorStyle = colorStyle, + textStyle = textStyle, + fontStyle = font.Header2, + subtitleText = self.props.subtitleText, + subtitleTextStyle = subtitleTextStyle, + subtitleFontStyle = font.Body, + rightContentWidth = self.props.defaultImageSize, + onActivated = self.props.onActivated, + dividerStyle = dividerStyle, + isDisabled = self.props.isDisabled, + [Roact.Ref] = self.props[Roact.Ref], + NextSelectionUp = self.props.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown, + NextSelectionLeft = self.props.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight, + SelectionImageObject = self.props.SelectionImageObject, + renderRightContent = function() + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, self.props.defaultImageSize, 1, 0), + LayoutOrder = 3, + }, { + SelectionImage = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = self.props.defaultImage, + Size = UDim2.new(0, self.props.defaultImageSize, 0, self.props.defaultImageSize), + ImageColor3 = iconStyle.Color, + ImageTransparency = iconStyle.Transparency, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + }, { + SelectedImage = self.props.isSelected and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Image = self.props.selectedImage, + Size = UDim2.new(0, self.props.selectedImageSize, 0, self.props.selectedImageSize), + ImageColor3 = iconStyle.Color, + ImageTransparency = iconStyle.Transparency, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + }) + }) + }) + end, + onStateChanged = self.onStateChanged, + }) + end) +end + +return GenericSelectionCell \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericSelectionCell.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericSelectionCell.spec.lua new file mode 100644 index 0000000..773a64c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Cell/GenericSelectionCell.spec.lua @@ -0,0 +1,29 @@ +return function() + local Cell = script.Parent + local Core = Cell.Parent + local UIBlox = Core.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local Images = require(Packages.UIBlox.App.ImageSet.Images) + + local GenericSelectionCell = require(Cell.GenericSelectionCell) + + local DEFAULT_IMAGE = Images["component_assets/circle_24_stroke_1"] + + it("should create and destroy GenericSelectionCell without errors", function() + local element = mockStyleComponent({ + genericCell = Roact.createElement(GenericSelectionCell, { + text = "text", + defaultImage = DEFAULT_IMAGE, + selectedImage = DEFAULT_IMAGE, + defaultImageSize = 16, + selectedImageSize = 16, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/Config.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/Config.lua new file mode 100644 index 0000000..c84d64e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/Config.lua @@ -0,0 +1,97 @@ +--[[ + Exposes an interface to set global configuration values for a package. + + Configuration can only occur once, and should only be done by an application + using this package, not a library. + + Any keys that aren't recognized will cause errors. Configuration is only + intended for configuring the package itself, not extensions or libraries. + + Configuration is expected to be set immediately after loading the package. Setting + configuration values after an application starts may produce unpredictable + behavior. +]] + +local Config = {} + +-- Every valid configuration value should be non-nil in the config table. +function Config.new(defaultConfig) + local self = {} + self.defaultConfig = defaultConfig + self.defaultConfigKeys = {} + + for key in pairs(defaultConfig) do + table.insert(self.defaultConfigKeys, key) + end + + self._currentConfig = setmetatable({}, { + __index = function(_, key) + local message = ( + "Invalid global configuration key %q. Valid configuration keys are: %s" + ):format( + tostring(key), + table.concat(self.defaultConfigKeys, ", ") + ) + + error(message, 3) + end + }) + + -- We manually bind these methods here so that the Config's methods can be + -- used without passing in self, since they could get exposed on the + -- root object. + self.set = function(...) + return Config.set(self, ...) + end + + self.get = function(...) + return Config.get(self, ...) + end + + self.scoped = function(...) + return Config.scoped(self, ...) + end + + self.set(defaultConfig) + + return self +end + +function Config:set(configValues) + -- Validate values without changing any configuration. + -- We only want to apply this configuration if it's valid! + for key, value in pairs(configValues) do + if self.defaultConfig[key] == nil then + local message = ( + "Invalid global configuration key %q (type %s). Valid configuration keys are: %s" + ):format( + tostring(key), + typeof(key), + table.concat(self.defaultConfigKeys, ", ") + ) + + error(message, 3) + end + + -- Right now, all configuration values must be boolean. + if typeof(value) ~= "boolean" then + local message = ( + "Invalid value %q (type %s) for global configuration key %q. Valid values are: true, false" + ):format( + tostring(value), + typeof(value), + tostring(key) + ) + + error(message, 3) + end + + self._currentConfig[key] = value + end +end + +function Config:get() + return self._currentConfig +end + +return Config \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/Config.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/Config.spec.lua new file mode 100644 index 0000000..e37d596 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/Config.spec.lua @@ -0,0 +1,57 @@ +return function() + local Config = require(script.Parent.Config) + + local defaultConfig = { + -- This is an example flag + exampleFlag = false, + } + + it("should accept valid configuration", function() + local config = Config.new(defaultConfig) + local values = config.get() + + expect(values.exampleFlag).to.equal(false) + + config.set({ + exampleFlag = true, + }) + + expect(values.exampleFlag).to.equal(true) + end) + + it("should reject invalid configuration keys", function() + local config = Config.new(defaultConfig) + + local badKey = "garblegoop" + + local ok, err = pcall(function() + config.set({ + [badKey] = true, + }) + end) + + expect(ok).to.equal(false) + + -- The error should mention our bad key somewhere. + expect(err:find(badKey)).to.be.ok() + end) + + it("should reject invalid configuration values", function() + local config = Config.new(defaultConfig) + + local goodKey = "exampleFlag" + local badValue = "Hello there!" + + local ok, err = pcall(function() + config.set({ + [goodKey] = badValue, + }) + end) + + expect(ok).to.equal(false) + + -- The error should mention both our key and value + expect(err:find(goodKey)).to.be.ok() + expect(err:find(badValue)).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/makeConfigurable.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/makeConfigurable.lua new file mode 100644 index 0000000..40a0af9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/makeConfigurable.lua @@ -0,0 +1,75 @@ +local Config = require(script.Parent.Config) + +local function trimTrailingNewline(str) + if str:sub(-1, -1) == "\n" then + return str:sub(1, -2) + end + + return str +end + +return function(initializeLibrary, name, defaultConfig) + if typeof(name) ~= "string" then + error("Bad argument #2 - expected a string for the name of the library") + end + if typeof(defaultConfig) ~= "table" then + error("Bad argument #3 - expected a default config table for the library") + end + + local Library = {} + local LibraryConfig = Config.new(defaultConfig) + + function Library.init(config) + setmetatable(Library, nil) + + if config then + LibraryConfig.set(config) + end + + Library.Config = LibraryConfig.get() + + local contents = initializeLibrary() + for key, value in pairs(contents) do + Library[key] = value + end + + local firstInitTraceback = trimTrailingNewline(debug.traceback()) + Library.init = function() + local currentInitTraceback = trimTrailingNewline(debug.traceback()) + warn(string.format("%s has already been configured\nFirst init traceback:\n%s\nCurrent init traceback:\n%s", + name, firstInitTraceback, currentInitTraceback)) + end + + return setmetatable(Library, { + __index = function(self, key) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + + __newindex = function(self, key, value) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + }) + end + + return setmetatable(Library, { + __index = function(self, key) + error(("You must call %s.init(config) before using it!"):format(name), 2) + end, + + __newindex = function(self, key, value) + error(("You must call %s.init(config) before using it!"):format(name), 2) + end, + }) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/makeConfigurable.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/makeConfigurable.spec.lua new file mode 100644 index 0000000..601e37a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Config/makeConfigurable.spec.lua @@ -0,0 +1,85 @@ +return function() + local makeConfigurable = require(script.Parent.makeConfigurable) + + local name = "Library" + + -- Function provide by a library that returns the API + local function initializeLibrary() + return { + someAPI = function() + return true + end + } + end + + local defaultConfig = { + -- This is an example flag + exampleFlag = false, + -- This is another example flag + anotherExampleFlag = false, + } + + local config = { + -- This is an example flag + ["exampleFlag"] = true, + } + + it("should require a name", function() + local ok = pcall(function() + makeConfigurable(initializeLibrary, nil, defaultConfig) + end) + + expect(ok).to.equal(false) + end) + + it("should require a default config", function() + local ok = pcall(function() + makeConfigurable(initializeLibrary, name, nil) + end) + + expect(ok).to.equal(false) + end) + + it("should have an init function", function() + local Library = makeConfigurable(initializeLibrary, name, defaultConfig) + + expect(typeof(Library.init)).to.equal("function") + end) + + it("should not have loaded its API before calling init", function() + local Library = makeConfigurable(initializeLibrary, name, defaultConfig) + + local ok = pcall(function() + Library.someAPI() + end) + + expect(ok).to.equal(false) + end) + + it("should not require a config when calling init", function() + local Library = makeConfigurable(initializeLibrary, name, defaultConfig) + + local ok = pcall(function() + Library.init() + end) + + expect(ok).to.equal(true) + end) + + it("should have a loaded config after calling init", function() + local Library = makeConfigurable(initializeLibrary, name, defaultConfig) + Library.init(config) + + expect(Library.Config).to.be.ok() + expect(Library.Config.exampleFlag).to.equal(true) + expect(Library.Config.anotherExampleFlag).to.equal(false) + end) + + it("should have a loaded API after calling init", function() + local Library = makeConfigurable(initializeLibrary, name, defaultConfig) + Library.init(config) + + expect(typeof(Library.someAPI)).to.equal("function") + expect(Library.someAPI()).to.equal(true) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Controllable.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Controllable.lua new file mode 100644 index 0000000..e153b24 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Controllable.lua @@ -0,0 +1,230 @@ +--[[ + Creates a Roact wrapper component that tracks state based on Roact input events to a UI control component. +]] +local ControlRoot = script.Parent +local CoreRoot = ControlRoot.Parent +local UIBloxRoot = CoreRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local ControlState = require(ControlRoot.Enum.ControlState) +local StateTable = require(UIBloxRoot.StateTable.StateTable) + +local Controllable = Roact.PureComponent:extend("Controllable") + +function Controllable:init() + self.isMounted = false + local initialState = ControlState.Initialize + self.state = { + currentState = initialState + } + local stateTableName = string.format("Controllable(%s)", tostring(self)) + + self.setDisabled = function(isDisabled) + if isDisabled then + self.stateTable.events.Disable() + else + self.stateTable.events.Enable() + end + end + + self.stateTable = StateTable.new(stateTableName, initialState, {}, + { + [ControlState.Initialize] = { + Enable = { nextState = ControlState.Default }, + Disable = { nextState = ControlState.Disabled }, + }, + [ControlState.Default] = { + OnPressed = { nextState = ControlState.Pressed }, + StartHover = { nextState = ControlState.Hover }, + OnSelectionGained = { nextState = ControlState.Selected }, + Disable = { nextState = ControlState.Disabled }, + }, + [ControlState.Hover] = { + OnSelectionGained = { nextState = ControlState.Selected }, + OnPressed = { nextState = ControlState.Pressed }, + EndHover = { nextState = ControlState.Default }, + Disable = { nextState = ControlState.Disabled }, + }, + [ControlState.Pressed] = { + OnSelectionGained = { nextState = ControlState.SelectedPressed }, + OnReleased = { nextState = ControlState.Default }, + OnReleasedHover = { nextState = ControlState.Hover }, + Disable = { nextState = ControlState.Disabled }, + }, + [ControlState.Selected] = { + OnSelectionLost = { nextState = ControlState.Default }, + OnPressed = { nextState = ControlState.SelectedPressed }, + Disable = { nextState = ControlState.Disabled }, + }, + [ControlState.SelectedPressed] = { + OnSelectionLost = { nextState = ControlState.Default }, + OnReleased = { nextState = ControlState.Selected }, + Disable = { nextState = ControlState.Disabled }, + }, + [ControlState.Disabled] = { + Enable = { nextState = ControlState.Default }, + }, + }) + + self.stateTable:onStateChange(function(oldState, newState) + self:setState({ + currentState = newState, + }) + if self.props.onStateChanged then + self.props.onStateChanged(oldState, newState) + end + end) +end + +local validateProps = t.strictInterface({ + -- The component that is controlled + controlComponent = t.strictInterface({ + -- the actual UI control component + component = t.union(t.callback, t.string, t.table), + + -- the props to pass to the UI control component + props = t.optional(t.table), + + -- child components + children = t.optional(t.table), + }), + + --callback function that's called when state changes + onStateChanged = t.callback, + + --disables state changes, and disables activating the UI control component + isDisabled = t.optional(t.boolean), + + --A Boolean value that determines whether user events are ignored and sink input + userInteractionEnabled = t.optional(t.boolean), +}) + +Controllable.defaultProps = { + userInteractionEnabled = true, + isDisabled = false, +} + +function Controllable:render() + assert(validateProps(self.props)) + + local controlComponent = self.props.controlComponent + local userInteractionEnabled = self.props.userInteractionEnabled + + if self.state.currentState == ControlState.Initialize then + return nil + end + + local newChildProps = Cryo.Dictionary.join( + self.props, + controlComponent.props or {}, + { + Selectable = true, + Active = not self.props.isDisabled, + [Roact.Event.MouseEnter] = function(...) + if not userInteractionEnabled then + return nil + end + self.stateTable.events.StartHover() + if controlComponent.props[Roact.Event.MouseEnter] ~= nil then + return controlComponent.props[Roact.Event.MouseEnter](...) + end + end, + [Roact.Event.MouseLeave] = function(...) + if not userInteractionEnabled then + return nil + end + self.stateTable.events.EndHover() + if controlComponent.props[Roact.Event.MouseLeave] ~= nil then + return controlComponent.props[Roact.Event.MouseLeave](...) + end + end, + [Roact.Event.InputBegan] = function(...) + if not userInteractionEnabled then + return nil + end + local inputObject = select(2, ...) + if inputObject.UserInputType == Enum.UserInputType.MouseButton1 or + inputObject.UserInputType == Enum.UserInputType.Touch or + inputObject.KeyCode == Enum.KeyCode.ButtonA then + self.stateTable.events.OnPressed() + end + if controlComponent.props[Roact.Event.InputBegan] ~= nil then + return controlComponent.props[Roact.Event.InputBegan](...) + end + end, + [Roact.Event.InputEnded] = function(...) + if not userInteractionEnabled then + return nil + end + local inputObject = select(2, ...) + if inputObject.UserInputType == Enum.UserInputType.MouseButton1 then + self.stateTable.events.OnReleasedHover() + elseif inputObject.UserInputType == Enum.UserInputType.Touch or + inputObject.KeyCode == Enum.KeyCode.ButtonA or + inputObject.UserInputType == Enum.UserInputType.MouseMovement then + self.stateTable.events.OnReleased() + end + if controlComponent.props[Roact.Event.InputEnded] ~= nil then + return controlComponent.props[Roact.Event.InputEnded](...) + end + end, + [Roact.Event.SelectionGained] = function(...) + if not userInteractionEnabled then + return nil + end + self.stateTable.events.OnSelectionGained() + if controlComponent.props[Roact.Event.SelectionGained] ~= nil then + return controlComponent.props[Roact.Event.SelectionGained](...) + end + end, + [Roact.Event.SelectionLost] = function(...) + if not userInteractionEnabled then + return nil + end + self.stateTable.events.OnSelectionLost() + if controlComponent.props[Roact.Event.SelectionLost] ~= nil then + return controlComponent.props[Roact.Event.SelectionLost](...) + end + end, + [Roact.Event.Activated] = function(...) + if not userInteractionEnabled then + return nil + end + if controlComponent.props[Roact.Event.Activated] then + if self.state.currentState ~= ControlState.Disabled then + return controlComponent.props[Roact.Event.Activated](...) + end + end + end, + + userInteractionEnabled = Cryo.None, + isDisabled = Cryo.None, + onStateChanged = Cryo.None, + [Roact.Children] = Cryo.None, + controlComponent = Cryo.None, + } + ) + + return Roact.createElement(controlComponent.component, newChildProps, controlComponent.children) +end + +function Controllable:didMount() + self.isMounted = true + self.setDisabled(self.props.isDisabled) +end + +function Controllable:didUpdate(previousProps) + if self.props.isDisabled ~= previousProps.isDisabled then + self.setDisabled(self.props.isDisabled) + end +end + +function Controllable:willUnmount() + self.isMounted = false +end + +return Controllable diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Controllable.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Controllable.spec.lua new file mode 100644 index 0000000..61d9350 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Controllable.spec.lua @@ -0,0 +1,113 @@ +--Skipping these tests until Roact 1.x. and spawn setstate is removed. +--https://jira.rbx.com/browse/UIBLOX-56 +return function() + local ControlRoot = script.Parent + local CoreRoot = ControlRoot.Parent + local UIBloxRoot = CoreRoot.Parent + local Packages = UIBloxRoot.Parent + + local Roact = require(Packages.Roact) + + local Controllable = require(ControlRoot.Controllable) + local ControlState = require(ControlRoot.Enum.ControlState) + + it("should create and destroy without errors", function() + -- luacheck: ignore unused argument oldState + local buttonState = nil + local element = Roact.createElement(Controllable, { + controlComponent = { + component = "TextButton", + props = {}, + }, + onStateChanged = function(oldState, newState) + buttonState = newState + end + }) + + local instance = Roact.mount(element) + expect(buttonState).to.equal(ControlState.Default) + Roact.unmount(instance) + end) + + it("should start isDisabled", function() + -- luacheck: ignore unused argument oldState + local buttonState = nil + local element = Roact.createElement(Controllable, { + controlComponent = { + component = "TextButton", + props = {}, + }, + onStateChanged = function(oldState, newState) + buttonState = newState + end, + isDisabled = true, + }) + + local instance = Roact.mount(element) + delay(0, function() expect(buttonState).to.equal(ControlState.Disabled) end) + Roact.unmount(instance) + end) + + it("should change from default to disabled", function() + -- luacheck: ignore unused argument oldState + local buttonState = nil + local element = Roact.createElement(Controllable, { + controlComponent = { + component = "TextButton", + props = {}, + }, + onStateChanged = function(oldState, newState) + buttonState = newState + end, + }) + + local instance = Roact.mount(element) + expect(buttonState).to.equal(ControlState.Default) + + Roact.update(instance, Roact.createElement(Controllable, { + controlComponent = { + component = "TextButton", + props = {}, + }, + onStateChanged = function(oldState, newState) + buttonState = newState + end, + isDisabled = true, + })) + expect(buttonState).to.equal(ControlState.Disabled) + + Roact.unmount(instance) + end) + + it("should change from isDisabled to default", function() + -- luacheck: ignore unused argument oldState + local buttonState = nil + local element = Roact.createElement(Controllable, { + controlComponent = { + component = "TextButton", + props = {}, + }, + onStateChanged = function(oldState, newState) + buttonState = newState + end, + + isDisabled = true, + }) + + local instance = Roact.mount(element) + expect(buttonState).to.equal(ControlState.Disabled) + + Roact.update(instance, Roact.createElement(Controllable, { + controlComponent = { + component = "TextButton", + props = {}, + }, + onStateChanged = function(oldState, newState) + buttonState = newState + end, + })) + expect(buttonState).to.equal(ControlState.Default) + + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Enum/ControlState.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Enum/ControlState.lua new file mode 100644 index 0000000..200bddc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Enum/ControlState.lua @@ -0,0 +1,14 @@ +local Core = script.Parent.Parent.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate(script.Name, { + "Initialize", + "Default", + "Pressed", + "Hover", + "Selected", + "SelectedPressed", + "Disabled", +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Enum/SelectionMode.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Enum/SelectionMode.lua new file mode 100644 index 0000000..e228a8f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Enum/SelectionMode.lua @@ -0,0 +1,10 @@ +local Core = script.Parent.Parent.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local enumerate = require(Packages.enumerate) + +return enumerate(script.Name, { + "Single", + "Multiple", + "None", +}) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Interactable.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Interactable.lua new file mode 100644 index 0000000..5fce064 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Interactable.lua @@ -0,0 +1,48 @@ +--[[ + Interactable is a component that can be used as a container that responds to control state changes. + It accepts all props that can be passed into a ImageButton in additional to + isDisabled: bool = false + onStateChanged: function(oldState: ControlState, newState: ControlState) +]] +local Control = script.Parent +local Core = Control.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + + +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) + +local Controllable = require(UIBlox.Core.Control.Controllable) + +local Interactable = Roact.PureComponent:extend("Interactable") + +Interactable.validateProps = t.interface({ + -- Is the interactable disabled + isDisabled = t.optional(t.boolean), + + -- function(oldState: ControlState, newState: ControlState) + -- A callback function for when the interactable state has changed + onStateChanged = t.callback, + + -- Note that this component can accept all valid properties of the Roblox ImageButton instance +}) + +function Interactable:render() + local controllerProps = { + onStateChanged = self.props.onStateChanged, + isDisabled = self.props.isDisabled, + userInteractionEnabled = self.props.userInteractionEnabled, + controlComponent = { + component = ImageSetComponent.Button, + props = self.props, + children = self.props[Roact.Children] or {}, + }, + } + + return Roact.createElement(Controllable, controllerProps) +end + +return Interactable \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Interactable.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Interactable.spec.lua new file mode 100644 index 0000000..dbb5fc3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/Interactable.spec.lua @@ -0,0 +1,27 @@ +return function() + local Control = script.Parent + local Core = Control.Parent + local UIBlox = Core.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local Interactable = require(Control.Interactable) + + it("should create and destroy without errors", function() + local controllableButton = Roact.createElement(Interactable, { + onStateChanged = function() end, + Size = UDim2.new(1, 1, 1, -1), + Position = UDim2.new(1, -1, 1, 1), + AnchorPoint = Vector2.new(0.1, 0.1), + LayoutOrder = 1, + BackgroundTransparency = 0.5, + ImageColor3 = Color3.fromRGB(25, 25, 25), + ImageTransparency = 0.5, + BorderSizePixel = 2, + AutoButtonColor = true, + }) + local instance = Roact.mount(controllableButton) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/InteractableList.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/InteractableList.lua new file mode 100644 index 0000000..04d7549 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/InteractableList.lua @@ -0,0 +1,161 @@ +local Control = script.Parent +local Core = Control.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) + +local ControlState = require(UIBlox.Core.Control.Enum.ControlState) + +local InteractableListItem = require(script.Parent.InteractableListItem) +local SelectionMode = require(script.Parent.Enum.SelectionMode) + +local LAYOUT_KEY = "$layout" + +--[[ + Manages selection and control state for a list of items, + useful as a base for components like button lists, tab bars, etc + + expects a list of arbitrary items, and a callback to render an item + each rendered item will be wrapped by a controllable, its state will be passed to the callback + each item has also a selection state, which can be managed by the component or forced through a prop + for managed selection, there are 3 selection modes: single, multiple, or none + a callback can be provided to handle user input selection change events (regardless of selection state) +]] + +local InteractableList = Roact.PureComponent:extend("InteractableList") + +InteractableList.validateProps = t.strictInterface({ + -- list of items to display as interactables, should be a table + itemList = t.table, + -- function(item: any, state: ControlState, selected: boolean) called to render each item + renderItem = t.callback, + -- function(itemList: table, renderItem: function(key)) called to render the list container + renderList = t.optional(t.callback), + -- list of currently selected keys (from itemList), if not provided selection will be managed by component + selection = t.optional(t.table), + -- current selection mode: single, multiple, none + selectionMode = t.optional(SelectionMode.isEnumValue), + -- function(newSelection) + onSelectionChanged = t.optional(t.callback), + --- options for default renderList, passed to a Frame component + size = t.optional(t.UDim2), + position = t.optional(t.UDim2), + layoutOrder = t.optional(t.integer), + padding = t.optional(t.UDim), + fillDirection = t.optional(t.enum(Enum.FillDirection)), + horizontalAlignment = t.optional(t.enum(Enum.HorizontalAlignment)), + verticalAlignment = t.optional(t.enum(Enum.VerticalAlignment)), + sortOrder = t.optional(t.enum(Enum.SortOrder)), + --- options for default controllable + -- container size for each item + itemSize = t.optional(t.UDim2), +}) + +InteractableList.defaultProps = { + renderList = function(items, renderItem, extraProps) + local children = {} + for key in pairs(items) do + children[key] = renderItem(key) + end + return Roact.createElement("Frame", { + Size = extraProps.size, + Position = extraProps.position, + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, Cryo.Dictionary.join({ + [LAYOUT_KEY] = Roact.createElement("UIListLayout", { + Padding = extraProps.padding, + FillDirection = extraProps.fillDirection, + HorizontalAlignment = extraProps.horizontalAlignment, + VerticalAlignment = extraProps.verticalAlignment, + SortOrder = extraProps.sortOrder, + }) + }, children)) + end, + size = UDim2.fromScale(1,1), + itemSize = UDim2.fromScale(1, 1), + selectionMode = SelectionMode.Single, +} + +function InteractableList:init() + local state = { + interactable = {}, + selection = {}, + } + if self.props.selectionMode == SelectionMode.Single then + local firstKey = next(self.props.itemList) + if firstKey ~= nil then + state.selection = { firstKey } + end + end + self:setState(state) + + self.setInteractableState = function(key, newState) + self:setState({ + interactable = Cryo.Dictionary.join(self.state.interactable, { + [key] = newState, + }) + }) + end + self.setSelection = function(newSelection) + self:setState({ + selection = newSelection, + }) + end +end + +function InteractableList:getSelection() + if self.props.selection then + return self.props.selection + end + if self.props.selectionMode == SelectionMode.None then + return {} + end + local selection = Cryo.List.filter(self.state.selection, function(selectedKey) + return self.props.itemList[selectedKey] ~= nil + end) + if self.props.selectionMode == SelectionMode.Single then + local firstKey = selection[#selection] + if firstKey == nil then + firstKey = next(self.props.itemList) + end + if firstKey ~= nil then + return { firstKey } + else + return {} + end + else + return selection + end +end + +function InteractableList:didMount() + if self.props.selection == nil and self.props.onSelectionChanged then + self.props.onSelectionChanged(self:getSelection()) + end +end + +function InteractableList:render() + return self.props.renderList(self.props.itemList, function(key) + local interactableState = self.state.interactable[key] or ControlState.Default + local selection = self:getSelection() + + return Roact.createElement(InteractableListItem, { + key = key, + item = self.props.itemList[key], + interactableState = interactableState, + selection = selection, + renderItem = self.props.renderItem, + itemSize = self.props.itemSize, + selectionMode = self.props.selectionMode, + onSelectionChanged = self.props.onSelectionChanged, + setInteractableState = self.setInteractableState, + setSelection = self.setSelection, + }) + end, self.props) +end + +return InteractableList diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/InteractableList.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/InteractableList.spec.lua new file mode 100644 index 0000000..7fde30d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/InteractableList.spec.lua @@ -0,0 +1,48 @@ +return function() + local Navigation = script.Parent + local App = Navigation.Parent + local UIBloxRoot = App.Parent + local Packages = UIBloxRoot.Parent + local Roact = require(Packages.Roact) + local Cryo = require(Packages.Cryo) + + local InteractableList = require(script.Parent.InteractableList) + local SelectionMode = require(script.Parent.Enum.SelectionMode) + + it("should create and destroy with minimum props without errors", function() + local element = Roact.createElement(InteractableList, { + itemList = {}, + renderItem = function() end, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy with all props without errors", function() + local element = Roact.createElement(InteractableList, { + itemList = {"one", "two"}, + renderItem = function(item) + return Roact.createElement("TextLabel", { + Text = item, + }) + end, + renderList = function(items, renderItem) + return Roact.createElement("Frame", {}, Cryo.List.map(Cryo.Dictionary.keys(items), renderItem)) + end, + selection = {}, + selectionMode = SelectionMode.None, + onSelectionChanged = function() end, + size = UDim2.new(), + position = UDim2.new(), + layoutOrder = 1, + padding = UDim.new(), + fillDirection = Enum.FillDirection.Horizontal, + horizontalAlignment = Enum.HorizontalAlignment.Center, + verticalAlignment = Enum.VerticalAlignment.Bottom, + sortOrder = Enum.SortOrder.Custom, + itemSize = UDim2.new(), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/InteractableListItem.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/InteractableListItem.lua new file mode 100644 index 0000000..6bbc4e5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Control/InteractableListItem.lua @@ -0,0 +1,61 @@ + +local Control = script.Parent +local Core = Control.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) + +local Interactable = require(UIBlox.Core.Control.Interactable) + +local SelectionMode = require(script.Parent.Enum.SelectionMode) + +--[[ + subcomponent of InteractableList, not intended for use separately + extracted from the main component's code to cache the onStateChanged and onActivated callbacks +]] + +local InteractableListItem = Roact.PureComponent:extend("InteractableListItem") + +function InteractableListItem:init() + self.onStateChanged = function(oldState, newState) + self.props.setInteractableState(self.props.key, newState) + end + self.onActivated = function() + local oldSelection = self.props.selection + local newSelection = { self.props.key } + if self.props.selectionMode == SelectionMode.Multiple then + newSelection = Cryo.List.filter(oldSelection, function(selectedKey) + return selectedKey ~= self.props.key + end) + if #newSelection == #oldSelection then + table.insert(newSelection, self.props.key) + end + end + if self.props.onSelectionChanged then + self.props.onSelectionChanged(newSelection, oldSelection) + end + if self.props.selectionMode ~= SelectionMode.None then + self.props.setSelection(newSelection) + end + end +end + +function InteractableListItem:render() + local selected = Cryo.List.find(self.props.selection, self.props.key) ~= nil + local renderedItem, extraProps = self.props.renderItem(self.props.item, self.props.interactableState, selected) + + return Roact.createElement(Interactable, Cryo.Dictionary.join({ + Size = self.props.itemSize, + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, extraProps or {}, { + onStateChanged = self.onStateChanged, + [Roact.Event.Activated] = self.onActivated, + }), { + [self.props.key] = renderedItem, + }) +end + +return InteractableListItem diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/ImageSetComponent.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/ImageSetComponent.lua new file mode 100644 index 0000000..80bf972 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/ImageSetComponent.lua @@ -0,0 +1,15 @@ +local createImageSetComponent = require(script.Parent.createImageSetComponent) + +local GuiService = game:GetService("GuiService") + +local CorePackages = script:FindFirstAncestor("CorePackages") + +local success, scale = pcall(GuiService.GetResolutionScale, GuiService) +if not success or not CorePackages then + scale = 1 +end + +return { + Button = createImageSetComponent("ImageButton", scale), + Label = createImageSetComponent("ImageLabel", scale), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/ImageSetComponent.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/ImageSetComponent.spec.lua new file mode 100644 index 0000000..f2077d8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/ImageSetComponent.spec.lua @@ -0,0 +1,31 @@ +return function() + local ImageSetComponent = require(script.Parent.ImageSetComponent) + local ImageSet = script.Parent + local Core = ImageSet.Parent + local UIBlox = Core.Parent + local Images = require(UIBlox.App.ImageSet.Images) + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + + it("should create and destroy button without errors", function() + local element = Roact.createElement(ImageSetComponent.Button, { + Size = UDim2.new(1, 0, 1, 0), + Image = Images["component_assets/circle_17_stroke_1"], + BackgroundTransparency = 1, + BorderSizePixel = 0, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy label without errors", function() + local element = Roact.createElement(ImageSetComponent.Label, { + Size = UDim2.new(1, 0, 1, 0), + Image = Images["component_assets/circle_17_stroke_1"], + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/Validator/validateImage.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/Validator/validateImage.lua new file mode 100644 index 0000000..1a58c85 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/Validator/validateImage.lua @@ -0,0 +1,11 @@ +local Validator = script.Parent +local ImageSet = Validator.Parent +local Core = ImageSet.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +local validateImageSetData = require(Validator.validateImageSetData) + +return t.union(t.string, validateImageSetData) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/Validator/validateImageSetData.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/Validator/validateImageSetData.lua new file mode 100644 index 0000000..ffb110e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/Validator/validateImageSetData.lua @@ -0,0 +1,14 @@ +local Validator = script.Parent +local ImageSet = Validator.Parent +local Core = ImageSet.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local t = require(Packages.t) + +local ImageSetData = t.strictInterface({ + ImageRectOffset = t.Vector2, + ImageRectSize = t.Vector2, + Image = t.string, +}) + +return ImageSetData \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/createImageSetComponent.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/createImageSetComponent.lua new file mode 100644 index 0000000..c1622a4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/createImageSetComponent.lua @@ -0,0 +1,43 @@ +local ImageSet = script.Parent +local Core = ImageSet.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local scaleSliceToResolution = require(UIBlox.App.ImageSet.scaleSliceToResolution) + +return function(innerComponent, resolutionScale) + assert(resolutionScale > 0) + + return function(props) + local fullProps = {} + local imageSetProps + local usesImageSet = false + + for key, value in pairs(props) do + if key == "Image" and typeof(value) == "table" then + usesImageSet = true + imageSetProps = value + else + fullProps[key] = value + end + end + + if usesImageSet then + for imageKey, imageValue in pairs(imageSetProps) do + if not fullProps[imageKey] then + fullProps[imageKey] = imageValue + elseif imageKey == "ImageRectOffset" then + fullProps[imageKey] = imageValue + fullProps[imageKey] * resolutionScale + elseif imageKey == "ImageRectSize" then + fullProps[imageKey] = fullProps[imageKey] * resolutionScale + end + end + end + + if usesImageSet then + fullProps = scaleSliceToResolution(fullProps, resolutionScale) + end + + return Roact.createElement(innerComponent, fullProps) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/createImageSetComponent.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/createImageSetComponent.spec.lua new file mode 100644 index 0000000..72cdefe --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/ImageSet/createImageSetComponent.spec.lua @@ -0,0 +1,80 @@ +return function() + local createImageSetComponent = require(script.Parent.createImageSetComponent) + local ImageSet = script.Parent + local Core = ImageSet.Parent + local UIBlox = Core.Parent + local Images = require(UIBlox.App.ImageSet.Images) + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + + it("should create and destroy button without errors", function() + local element = Roact.createElement(createImageSetComponent("ImageButton", 1), { + Size = UDim2.new(0, 8, 0, 8), + Image = Images["component_assets/circle_17_stroke_1"], + BackgroundTransparency = 1, + BorderSizePixel = 0, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy button with slice center", function() + local element = Roact.createElement(createImageSetComponent("ImageButton", 1), { + Size = UDim2.new(0, 8, 0, 8), + Image = Images["component_assets/circle_17_stroke_1"], + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(8, 7, 10, 9), + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "ImageSetComponentTest") + expect(container.ImageSetComponentTest.SliceCenter.Min.X).to.equal(8) + expect(container.ImageSetComponentTest.SliceCenter.Min.Y).to.equal(7) + expect(container.ImageSetComponentTest.SliceCenter.Max.X).to.equal(10) + expect(container.ImageSetComponentTest.SliceCenter.Max.Y).to.equal(9) + Roact.unmount(instance) + end) + + it("should create and destroy button with scaled slice center", function() + local element = Roact.createElement(createImageSetComponent("ImageButton", 2), { + Size = UDim2.new(0, 8, 0, 8), + Image = Images["component_assets/circle_17_stroke_1"], + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = Rect.new(11, 12, 14, 17), + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "ImageSetComponentTest") + expect(container.ImageSetComponentTest.SliceCenter.Min.X).to.equal(22) + expect(container.ImageSetComponentTest.SliceCenter.Min.Y).to.equal(24) + expect(container.ImageSetComponentTest.SliceCenter.Max.X).to.equal(28) + expect(container.ImageSetComponentTest.SliceCenter.Max.Y).to.equal(34) + Roact.unmount(instance) + end) + + it("should create and destroy button with scaled image rect offset and size", function() + local image = Images["component_assets/circle_17_stroke_1"] + local scale = 2 + local element = Roact.createElement(createImageSetComponent("ImageButton", scale), { + Size = UDim2.new(0, 8, 0, 8), + Image = image, + ImageRectOffset = Vector2.new(2, 2), + ImageRectSize = Vector2.new(15, 15), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "ImageSetComponentTest") + expect(container.ImageSetComponentTest.ImageRectOffset.X).to.equal(image.ImageRectOffset.X + 2 * scale) + expect(container.ImageSetComponentTest.ImageRectOffset.Y).to.equal(image.ImageRectOffset.Y + 2 * scale) + expect(container.ImageSetComponentTest.ImageRectSize.X).to.equal(15 * scale) + expect(container.ImageSetComponentTest.ImageRectSize.Y).to.equal(15 * scale) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InfiniteScroller/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InfiniteScroller/init.lua new file mode 100644 index 0000000..ea86168 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InfiniteScroller/init.lua @@ -0,0 +1,5 @@ +local Core = script.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +return require(Packages.InfiniteScroller) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/InputButton.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/InputButton.lua new file mode 100644 index 0000000..766f2b2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/InputButton.lua @@ -0,0 +1,199 @@ +local Packages = script.Parent.Parent.Parent.Parent +local TextService = game:GetService("TextService") + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Cryo = require(Packages.Cryo) + +local withStyle = require(Packages.UIBlox.Core.Style.withStyle) +local ImageSetComponent = require(Packages.UIBlox.Core.ImageSet.ImageSetComponent) +local Controllable = require(Packages.UIBlox.Core.Control.Controllable) +local ControlState = require(Packages.UIBlox.Core.Control.Enum.ControlState) + +local FitTextLabel = require(Packages.FitFrame).FitTextLabel + +-- TODO AVBURST-3748: Remove this soon after TextBoundsRoundUp is turned on to make the UIBlox places display +-- the same way as the App +local EngineFeatureTextBoundsRoundUp do + local success, value = pcall(function() + return game:GetEngineFeature("TextBoundsRoundUp") + end) + EngineFeatureTextBoundsRoundUp = success and value +end + +local InputButton = Roact.PureComponent:extend("InputButton") + +local validateProps = t.strictInterface({ + text = t.string, + size = t.optional(t.UDim2), + image = t.table, + imageColor = t.Color3, + fillImage = t.optional(t.table), + fillImageSize = t.optional(t.UDim2), + fillImageColor = t.optional(t.Color3), + selectedColor = t.Color3, + textColor = t.Color3, + transparency = t.number, + onActivated = t.callback, + isDisabled = t.optional(t.boolean), + layoutOrder = t.optional(t.number), +}) + +InputButton.defaultProps = { + layoutOrder = 0, + isDisabled = false, +} + +local SELECTION_BUTTON_SIZE = 26 +local HORIZONTAL_PADDING = 8 + +function InputButton:init() + self.state = { + outerImage = self.props.image, + outerTransparency = 1, + outerImageColor = self.props.imageColor, + innerImage = self.props.image, + innerImageColor = self.props.fillImageColor, + innerTransparency = 1, + } + + self.changeSprite = function(buttonState) + if buttonState == ControlState.Hover then + if not self.props.isDisabled then + self:setState({ + outerImageColor = self.props.selectedColor + }) + end + elseif buttonState == ControlState.Default then + self:setState({ + outerImageColor = self.props.imageColor + }) + end + end + + if not self.props.size then + --Initalize height to SELECTION_BUTTON_SIZE as the height can't be smaller than the button height + self.sizeBinding, self.updateSizeBinding = Roact.createBinding(UDim2.new(1, 0, 0, SELECTION_BUTTON_SIZE)) + + self.textAbsoluteSizeChanged = function(rbx) + local sizeY = SELECTION_BUTTON_SIZE + if rbx.AbsoluteSize.Y > sizeY then + sizeY = rbx.AbsoluteSize.Y + end + self.updateSizeBinding(UDim2.new(1, 0, 0, sizeY)) + end + end +end + +function InputButton:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local font = stylePalette.Font + local fontSize = font.Body.RelativeSize * font.BaseSize + + local textComponent + local textComponentProps = { + LayoutOrder = 2, + BackgroundTransparency = 1, + + Text = self.props.text, + TextXAlignment = Enum.TextXAlignment.Left, + + TextSize = fontSize, + Font = font.Body.Font, + TextWrapped = true, + TextColor3 = self.props.textColor, + TextTransparency = self.props.transparency, + } + + if self.props.size then + local size = self.props.size + local frameSize = Vector2.new(size.X.Offset - SELECTION_BUTTON_SIZE - HORIZONTAL_PADDING, size.Y.Offset) + local touchZoneWidth = TextService:GetTextSize(self.props.text, fontSize, font.Body.Font, frameSize).X + if (not EngineFeatureTextBoundsRoundUp) and touchZoneWidth > 0 then + -- GetTextSize documentation recommends to add a pixel of padding to the result to ensure no text is cut off + -- Only add that extra padding if there is text to display + touchZoneWidth = touchZoneWidth + 1 + end + + textComponent = "TextButton" + textComponentProps = Cryo.Dictionary.join(textComponentProps, { + Size = UDim2.new(0, touchZoneWidth, 1, 0), + [Roact.Event.Activated] = not self.props.isDisabled and self.props.onActivated or nil, + }) + else + textComponent = FitTextLabel + textComponentProps = Cryo.Dictionary.join(textComponentProps, { + width = UDim.new(1, -SELECTION_BUTTON_SIZE - HORIZONTAL_PADDING), + onActivated = self.props.onActivated, + [Roact.Change.AbsoluteSize] = self.textAbsoluteSizeChanged, + }) + end + + local fillImage = self.props.fillImage + + return Roact.createElement("Frame", { + Size = self.props.size or self.sizeBinding, + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + }, { + HorizontalLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + Padding = UDim.new(0, HORIZONTAL_PADDING), + VerticalAlignment = Enum.VerticalAlignment.Center + }), + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, HORIZONTAL_PADDING), + }), + InputButtonImage = Roact.createElement(Controllable, { + controlComponent = { + component = ImageSetComponent.Button, + props = { + BackgroundTransparency = 1, + Size = UDim2.new(0, SELECTION_BUTTON_SIZE, 0, SELECTION_BUTTON_SIZE), + Image = self.props.image, + ImageTransparency = self.props.transparency, + ScaleType = self.props.buttonSliceType, + SliceCenter = self.props.buttonSliceCenter, + ImageColor3 = self.state.outerImageColor, + [Roact.Event.Activated] = self.props.onActivated, + LayoutOrder = 1, + }, + children = { + InputFillImage = fillImage and Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + Size = self.props.fillImageSize, + Image = fillImage, + ImageTransparency = self.props.transparency, + ImageColor3 = self.props.fillImageColor, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + }) + } + }, + isDisabled = self.props.isDisabled, + + onStateChanged = function(_, newState) + self.changeSprite(newState) + end, + }), + -- Only create this element if there is text to display + InputButtonText = (self.props.text ~= "") and Roact.createElement(Controllable, { + controlComponent = + { + component = textComponent, + props = textComponentProps, + }, + isDisabled = self.props.isDisabled, + onStateChanged = function(_, newState) + self.changeSprite(newState) + end, + }) + } + ) + end) +end + +return InputButton diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/InputButton.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/InputButton.spec.lua new file mode 100644 index 0000000..32fb42e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/InputButton.spec.lua @@ -0,0 +1,37 @@ +return function() + local InputButtonRoot = script.Parent + local Core = InputButtonRoot.Parent + local UIBlox = Core.Parent + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local Images = require(Packages.UIBlox.App.ImageSet.Images) + local mockStyleComponent = require(Packages.UIBlox.Utility.mockStyleComponent) + + local InputButton = require(script.Parent.InputButton) + + describe("lifecycle", function() + local frame = Instance.new("Frame") + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + InputButton = Roact.createElement(InputButton, { + text = "some text", + size = UDim2.new(1, 0, 1, 0), + image = Images["component_assets/circle_24_stroke_1"], + imageColor = Color3.fromRGB(55, 111, 55), + fillImage = Images["component_assets/circle_16"], + fillImageSize = UDim2.new(10, 10), + fillImageColor = Color3.fromRGB(111, 222, 111), + selectedColor = Color3.fromRGB(8, 9, 8), + textColor = Color3.fromRGB(1, 2, 3), + transparency = 0.5, + onActivated = function(value) print(value) end, + layoutOrder = 1, + }) + }) + local instance = Roact.mount(element, frame, "Box") + + Roact.unmount(instance) + end) + end) + +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/__stories__/InputButton.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/__stories__/InputButton.story.lua new file mode 100644 index 0000000..af96b4a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/InputButton/__stories__/InputButton.story.lua @@ -0,0 +1,96 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local InputButtonRoot = script.Parent.Parent +local Core = InputButtonRoot.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local withStyle = require(UIBlox.Core.Style.withStyle) +local Images = require(Packages.UIBlox.App.ImageSet.Images) +local Roact = require(Packages.Roact) +local InputButton = require(InputButtonRoot.InputButton) + +local InputButtonStory = Roact.PureComponent:extend("InputButtonStory") + +local function createButton(props) + local theme = props.style.Theme.SystemPrimaryDefault + + return Roact.createElement(InputButton, { + text = props.text, + size = props.size, + image = Images["component_assets/circle_24_stroke_1"], + imageColor = Color3.fromRGB(0, 255, 0), + fillImage = Images["component_assets/circle_16"], + fillImageSize = UDim2.new(10, 10), + fillImageColor = Color3.fromRGB(111, 222, 111), + selectedColor = Color3.fromRGB(255, 0, 0), + textColor = theme.Color, + transparency = theme.Transparency, + onActivated = function(value) print(value) end, + layoutOrder = props.layoutOrder, + }) +end + +function InputButtonStory:render() + return withStyle(function(style) + return Roact.createElement(StoryItem, { + layoutOrder = 1, + title = "Input Button", + subTitle = "InputButton.InputButton", + showDivider = true, + }, { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + VerticalAlignment = Enum.VerticalAlignment.Top, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + Padding = UDim.new(0, 20), + }), + + noTextButton = createButton({ + text = "", + size = UDim2.new(0, 130, 0, 20), + layoutOrder = 1, + style = style, + }), + oneLineButton = createButton({ + text = "Text", + size = UDim2.new(0, 130, 0, 20), + layoutOrder = 2, + style = style, + }), + twoLineButton = createButton({ + text = "Two lines of text", + size = UDim2.new(0, 130, 0, 40), + layoutOrder = 3, + style = style, + }), + threeLineButton = createButton({ + text = "This has three lines of text", + size = UDim2.new(0, 130, 0, 60), + layoutOrder = 4, + style = style, + }), + tenLineButton = createButton({ + text = "An example of the unreasonable amount of text wrapping we could do. This is 10 lines", + size = UDim2.new(0, 130, 0, 200), + layoutOrder = 5, + style = style, + }), + }) + end) +end + +return function(target) + local styleProvider = Roact.createElement(StoryView, {}, { + Story = Roact.createElement(InputButtonStory), + }) + + local handle = Roact.mount(styleProvider, target, "InputButton") + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.lua new file mode 100644 index 0000000..0c43e60 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.lua @@ -0,0 +1,690 @@ +local PLUGINGUI_INPUT_CAPTURER_ZINDEX = 100000 +local SLIDER_HEIGHT = 36 +local KNOB_HEIGHT = 44 + +local UserInputService = game:GetService("UserInputService") + +local SliderRoot = script.Parent +local CoreRoot = SliderRoot.Parent +local UIBloxRoot = CoreRoot.Parent +local Packages = UIBloxRoot.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local Gamepad = require(Packages.RoactGamepad) +local ImageSetComponent = require(CoreRoot.ImageSet.ImageSetComponent) + +local lerp = require(UIBloxRoot.Utility.lerp) +local UIBloxConfig = require(UIBloxRoot.UIBloxConfig) +local CursorKind = require(UIBloxRoot.App.SelectionImage.CursorKind) +local withSelectionCursorProvider = require(UIBloxRoot.App.SelectionImage.withSelectionCursorProvider) + +local DPAD_INITIAL_MOVE_INTERVAL = 0.5 +local STICK_INITIAL_MOVE_INTERVAL = 0.2 +local STICK_MOVE_DEADZONE = 0.2 +local DPAD_SPEED = 8 -- In increments per second +local STICK_SPEED = 12 -- In increments per second + +local GenericSlider = Roact.PureComponent:extend("GenericSlider") + +GenericSlider.validateProps = t.strictInterface({ + --value of the first knob if the slider has two knobs, otherwise value of the only knob + lowerValue = t.number, + --value of the second knob if the slider has two knobs + upperValue = t.optional(t.number), + min = t.number, + max = t.number, + stepInterval = t.numberPositive, + isDisabled = t.optional(t.boolean), + + onValueChanged = t.callback, + --drag start function for first knob if the slider has two knobs, otherwise function for only knob + onDragStartLower = t.optional(t.callback), + --drag start function of the second knob if the slider has two knobs + onDragStartUpper = t.optional(t.callback), + onDragEnd = t.optional(t.callback), + + trackImage = t.union(t.string, t.table), + -- Allow bindings for style props + trackColor = t.union(t.Color3, t.table), + trackTransparency = t.union(t.number, t.table), + trackSliceCenter = t.optional(t.Rect), + + trackFillImage = t.union(t.string, t.table), + trackFillColor = t.union(t.Color3, t.table), + trackFillTransparency = t.union(t.number, t.table), + trackFillSliceCenter = t.optional(t.Rect), + + knobImage = t.union(t.string, t.table), + --knob color value of the first knob if the slider has two knobs, otherwise value of the only knob + knobColorLower = t.union(t.Color3, t.table), + --knob color value of the second knob if the slider has two knobs + knobColorUpper = t.optional(t.union(t.Color3, t.table)), + knobTransparency = t.union(t.number, t.table), + + knobImagePadding = t.optional(t.numberMin(0)), + + knobShadowImage = t.union(t.string, t.table), + --knob shadow transparency value of the first knob if the slider has two knobs, otherwise value of the only knob + knobShadowTransparencyLower = t.union(t.number, t.table), + --knob shadow transparency value of the second knob if the slider has two knobs + knobShadowTransparencyUpper = t.optional(t.union(t.number, t.table)), + + width = t.optional(t.UDim), + position = t.optional(t.UDim2), + anchorPoint = t.optional(t.Vector2), + layoutOrder = t.optional(t.integer), + + [Roact.Ref] = t.optional(t.table), + NextSelectionUp = t.optional(t.table), + NextSelectionDown = t.optional(t.table), + focusController = t.optional(t.table), +}) + +GenericSlider.defaultProps = { + width = UDim.new(1, 0), + knobImagePadding = 0, +} + +function GenericSlider:init() + self.rootRef = self.props[Roact.Ref] or Roact.createRef() + self.lowerKnobRef = Roact.createRef() + self.upperKnobRef = Roact.createRef() + self.moveDirection = 0 + + self.lowerKnobDrag = false + self.upperKnobDrag = false + self.totalMoveTime = 0 + self.isFirstMove = true + self.unhandledTime = 0 + + self.state = { + lowerKnobIsSelected = false, + upperKnobIsSelected = false, + } +end + +function GenericSlider:onMoveStep(inputObjects, delta) + if not (self.state.lowerKnobIsSelected or self.state.upperKnobIsSelected) then + return + end + + local stickInput = inputObjects[Enum.KeyCode.Thumbstick1].Position + local usingStick = stickInput.Magnitude > STICK_MOVE_DEADZONE + local increments = 0 + local initialMoveInterval, moveDirection, speed + self.totalMoveTime = self.totalMoveTime + delta + + if usingStick then + moveDirection = stickInput.x > 0 and 1 or -1 + initialMoveInterval = STICK_INITIAL_MOVE_INTERVAL + speed = STICK_SPEED + else + local leftMovement = inputObjects[Enum.KeyCode.DPadLeft].UserInputState == Enum.UserInputState.Begin and -1 or 0 + local rightMovement = inputObjects[Enum.KeyCode.DPadRight].UserInputState == Enum.UserInputState.Begin and 1 or 0 + moveDirection = leftMovement + rightMovement + initialMoveInterval = DPAD_INITIAL_MOVE_INTERVAL + speed = DPAD_SPEED + end + + if moveDirection ~= 0 then + -- Process input for the first button press + if self.isFirstMove then + self.isFirstMove = false + self.totalMoveTime = 0 + self.unhandledTime = 0 + increments = 1 + -- Process input if enough time has passed. + elseif self.totalMoveTime > initialMoveInterval then + -- How much of delta time that was in the first interval + local initialIntervalOverlap = math.max(initialMoveInterval - self.totalMoveTime - delta, 0) + local timeToHandle = delta - initialIntervalOverlap + self.unhandledTime + increments = math.floor(speed * timeToHandle) + + self.unhandledTime = timeToHandle - increments / speed + else + -- Period between first move and subsequent moves + increments = 0 + self.unhandledTime = 0 + end + else + self.totalMoveTime = 0 + self.isFirstMove = true + end + + if increments > 0 then + self:processGamepadInput(moveDirection, increments) + end +end + +function GenericSlider:processGamepadInput(polarity, increments) + if self:hasTwoKnobs() then + self:processTwoKnobGamepadInput(polarity, increments) + else + self:processOneKnobGamepadInput(polarity, increments) + end +end + +function GenericSlider:processTwoKnobGamepadInput(polarity, increments) + local stepInterval = self.props.stepInterval * polarity + local lowerValue = self.props.lowerValue + local upperValue = self.props.upperValue + + --[[ + If the knobs are overlapping, the gamepad may select either knob. + Set the one that should be selected by the direction of the input. + ]] + if lowerValue == upperValue and not self.state.processingGamepad then + if self.state.lowerKnobIsSelected and polarity == 1 and lowerValue ~= self.props.max then + self:setState({ + lowerKnobIsSelected = false, + upperKnobIsSelected = true, + processingGamepad = true, + }) + self.props.focusController.moveFocusTo(self.upperKnobRef) + elseif self.state.upperKnobIsSelected and polarity == -1 and upperValue ~= self.props.min then + self:setState({ + lowerKnobIsSelected = true, + upperKnobIsSelected = false, + processingGamepad = true, + }) + self.props.focusController.moveFocusTo(self.lowerKnobRef) + end + elseif not self.state.processingGamepad then + self:setState({ + processingGamepad = true, + }) + end + + if self.state.lowerKnobIsSelected then + local steppedValue = math.max(math.min(lowerValue + (stepInterval * increments), self.props.max), self.props.min) + if steppedValue <= upperValue then + lowerValue = steppedValue + else + lowerValue = upperValue + end + elseif self.state.upperKnobIsSelected then + local steppedValue = math.max(math.min(upperValue + (stepInterval * increments), self.props.max), self.props.min) + if steppedValue >= lowerValue then + upperValue = steppedValue + else + upperValue = lowerValue + end + end + + if upperValue ~= self.props.upperValue or lowerValue ~= self.props.lowerValue then + self.props.onValueChanged(lowerValue, upperValue) + end +end + +function GenericSlider:processOneKnobGamepadInput(polarity, increments) + local stepInterval = self.props.stepInterval * polarity + local lowerValue = self.props.lowerValue + + if self.state.lowerKnobIsSelected then + lowerValue = math.max(math.min(lowerValue + (stepInterval * increments), self.props.max), self.props.min) + end + + if lowerValue ~= self.props.lowerValue then + self.props.onValueChanged(lowerValue) + end +end + +function GenericSlider:render() + local knobIsSelected = self.state.lowerKnobIsSelected or self.state.upperKnobIsSelected + local isTwoKnobs = self:hasTwoKnobs() + local fillPercentLower = (self.props.lowerValue - self.props.min) / (self.props.max - self.props.min) + local fillPercentUpper = isTwoKnobs and (self.props.upperValue - self.props.min) / (self.props.max - self.props.min) + or nil + local visibleSize = KNOB_HEIGHT - (self.props.knobImagePadding * 2) + local positionOffsetLower = lerp(visibleSize / 2, -visibleSize / 2, fillPercentLower) + local positionOffsetUpper = isTwoKnobs and lerp(visibleSize / 2, -visibleSize / 2, fillPercentUpper) or nil + local knobPositionLower = UDim2.new(fillPercentLower, positionOffsetLower, 0.5, 0) + local knobPositionUpper = isTwoKnobs and UDim2.new(fillPercentUpper, positionOffsetUpper, 0.5, 0) or nil + local fillSize = isTwoKnobs and UDim2.fromScale(fillPercentUpper - fillPercentLower, 1) + or UDim2.fromScale(fillPercentLower, 1) + local selectedKnob = self.state.lowerKnobIsSelected or self.state.upperKnobIsSelected + + local imageSetComponent = UIBloxConfig.enableExperimentalGamepadSupport and + Gamepad.Focusable[ImageSetComponent.Button] or ImageSetComponent.Button + + return withSelectionCursorProvider(function(getSelectionCursor) + return Roact.createElement(imageSetComponent, { + BackgroundTransparency = 1, + AnchorPoint = self.props.anchorPoint, + Size = UDim2.new(self.props.width.Scale, self.props.width.Offset, 0, SLIDER_HEIGHT), + LayoutOrder = self.props.layoutOrder, + Position = self.props.position, + [Roact.Event.InputBegan] = function(rbx, inputObject) + if self.props.isDisabled then + return + end + + self:onInputBegan(inputObject, false) + end, + [Roact.Ref] = self.rootRef, + + NextSelectionUp = (not selectedKnob) and self.props.NextSelectionUp or self.rootRef, + NextSelectionDown = (not selectedKnob) and self.props.NextSelectionDown or self.rootRef, + defaultChild = UIBloxConfig.enableExperimentalGamepadSupport and + (self.props.upperValue ~= self.props.min and self.lowerKnobRef or self.upperKnobRef) or nil, + onFocusLost = UIBloxConfig.enableExperimentalGamepadSupport and function() + if self.state.lowerKnobIsSelected or self.state.upperKnobIsSelected then + self:setState({ + lowerKnobIsSelected = false, + upperKnobIsSelected = false, + }) + end + end or nil, + }, { + Track = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + ImageColor3 = self.props.trackColor, + ImageTransparency = self.props.trackTransparency, + Image = self.props.trackImage, + Size = UDim2.new(1, 0, 0, 4), + Position = UDim2.fromScale(0.5, 0.5), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = self.props.trackSliceCenter, + }, { + TrackFill = Roact.createElement(ImageSetComponent.Label, { + BackgroundTransparency = 1, + ImageColor3 = self.props.trackFillColor, + ImageTransparency = self.props.trackFillTransparency, + Image = self.props.trackFillImage, + Size = fillSize, + Position = isTwoKnobs and UDim2.new(fillPercentLower, 0, 0, 0) or UDim2.new(0, 0, 0, 0), + ScaleType = Enum.ScaleType.Slice, + SliceCenter = self.props.trackFillSliceCenter, + }) + }), + LowerKnob = Roact.createElement(imageSetComponent, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + ImageColor3 = self.props.knobColorLower, + ImageTransparency = self.props.knobTransparency, + Image = self.props.knobImage, + Size = UDim2.fromOffset(KNOB_HEIGHT, KNOB_HEIGHT), + Position = knobPositionLower, + ZIndex = 3, + inputBindings = UIBloxConfig.enableExperimentalGamepadSupport and { + OnMoveStep = Gamepad.Input.onMoveStep(function(inputObjects, delta) + self:onMoveStep(inputObjects, delta) + end), + SelectLowerKnob = Gamepad.Input.onBegin(Enum.KeyCode.ButtonA, function() + self:setState(function(state) + return { + lowerKnobIsSelected = not state.lowerKnobIsSelected, + processingGamepad = false, + } + end) + end), + UnselectLowerKnob = knobIsSelected and Gamepad.Input.onBegin(Enum.KeyCode.ButtonB, function() + self:setState({ + lowerKnobIsSelected = false, + processingGamepad = false, + }) + end) or nil, + } or nil, + NextSelectionLeft = knobIsSelected and self.lowerKnobRef or nil, + NextSelectionRight = (isTwoKnobs and not knobIsSelected and self.props.upperValue ~= self.props.lowerValue) + and self.upperKnobRef or nil, + NextSelectionUp = knobIsSelected and self.lowerKnobRef or nil, + NextSelectionDown = knobIsSelected and self.lowerKnobRef or nil, + SelectionImageObject = knobIsSelected and getSelectionCursor(CursorKind.SelectedKnob) + or getSelectionCursor(CursorKind.UnselectedKnob), + [Roact.Ref] = self.lowerKnobRef, + [Roact.Event.InputBegan] = function(rbx, inputObject) + if self.props.isDisabled then + return + end + + self:onInputBegan(inputObject, true) + end, + }), + LowerKnobShadow = Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + ImageTransparency = self.props.knobShadowTransparencyLower, + Image = self.props.knobShadowImage, + Size = UDim2.fromOffset(44, 44), + Position = knobPositionLower, + ZIndex = 2, + }), + UpperKnob = isTwoKnobs and Roact.createElement(imageSetComponent, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + ImageColor3 = self.props.knobColorUpper, + ImageTransparency = self.props.knobTransparency, + Image = self.props.knobImage, + Size = UDim2.fromOffset(KNOB_HEIGHT, KNOB_HEIGHT), + Position = knobPositionUpper, + ZIndex = 3, + + NextSelectionLeft = (isTwoKnobs and not knobIsSelected and self.props.upperValue ~= self.props.lowerValue) + and self.lowerKnobRef or nil, + NextSelectionRight = knobIsSelected and self.upperKnobRef or nil, + NextSelectionUp = knobIsSelected and self.upperKnobRef or nil, + NextSelectionDown = knobIsSelected and self.upperKnobRef or nil, + SelectionImageObject = knobIsSelected and getSelectionCursor(CursorKind.SelectedKnob) + or getSelectionCursor(CursorKind.UnselectedKnob), + [Roact.Ref] = self.upperKnobRef, + [Roact.Event.InputBegan] = function(rbx, inputObject) + if self.props.isDisabled then + return + end + + self:onInputBegan(inputObject, true) + end, + inputBindings = UIBloxConfig.enableExperimentalGamepadSupport and { + OnMoveStep = Gamepad.Input.onMoveStep(function(inputObjects, delta) + self:onMoveStep(inputObjects, delta) + end), + SelectUpperKnob = Gamepad.Input.onBegin(Enum.KeyCode.ButtonA, function() + self:setState(function(state) + return { + upperKnobIsSelected = not state.upperKnobIsSelected, + processingGamepad = false, + } + end) + end), + UnselectUpperKnob = knobIsSelected and Gamepad.Input.onBegin(Enum.KeyCode.ButtonB, function() + self:setState({ + upperKnobIsSelected = false, + processingGamepad = false, + }) + end) or nil, + } or nil, + }), + UpperKnobShadow = isTwoKnobs and Roact.createElement(ImageSetComponent.Label, { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + ImageTransparency = self.props.knobShadowTransparencyUpper, + Image = self.props.knobShadowImage, + Size = UDim2.fromOffset(44, 44), + Position = knobPositionUpper, + ZIndex = 2, + }), + }) + end) +end + +function GenericSlider:didMount() + local root = self.rootRef.current + + -- When didMount is first called we're still orphaned; we need to wait until + -- we're in the DataModel before checking whether we can use UserInputService. + -- Using a connection on AncestryChanged means we won't yield a frame to + -- figure this out. + local ancestryChangedConnection + ancestryChangedConnection = root.AncestryChanged:Connect(function() + if not root:IsDescendantOf(game) then + return + end + + ancestryChangedConnection:Disconnect() + + -- If we're mounted in a PluginGui, we cannot use UserInputService and we + -- need to resort to less clean methods to capture mouse movements. + self.canUseUserInputService = root:FindFirstAncestorWhichIsA("PluginGui") == nil + end) +end + +function GenericSlider:didUpdate() + if self.props.disabled then + self:stopListeningForDrag() + end +end + +function GenericSlider:willUnmount() + self:stopListeningForDrag() +end + +function GenericSlider:onInputBegan(inputObject, isKnob) + if self.props.disabled then + return + end + + -- Old touch inputs can trigger onInputBegan when they first touch a GuiElement + -- These are filtered by checking UserInputState + if UIBloxConfig.genericSliderFilterOldTouchInputs and inputObject.UserInputState ~= Enum.UserInputState.Begin then + return + end + + local inputType = inputObject.UserInputType + + if inputType ~= Enum.UserInputType.MouseButton1 and inputType ~= Enum.UserInputType.Touch then + return + end + + if inputType == Enum.UserInputType.Touch and not isKnob then + return + end + + local position = inputObject.Position.X + self:processDrag(position) + self:startListeningForDrag() +end + +function GenericSlider:startListeningForDrag() + local root = self.rootRef.current + + if root == nil then + return + end + + if self.dragging then + return + end + + if self.canUseUserInputService then + -- This is the nice clean path, where we can just use UserInputService to + -- capture the mouse movements. We will use this path in all production + -- cases (desktop, mobile, console, etc.) + self.moveConnection = UserInputService.InputChanged:Connect(function(inputObject) + -- We don't check whether the input was processed by something else + -- because we don't care about it: when we move the mouse, we want to + -- move the slider to match the movement, regardless of whether the + -- mouse movement was processed by something else. + + if not self.dragging then + return + end + + local inputType = inputObject.UserInputType + + if inputType ~= Enum.UserInputType.MouseMovement and inputType ~= Enum.UserInputType.Touch then + return + end + + if inputObject.UserInputState ~= Enum.UserInputState.Change then + return + end + + self:processDrag(inputObject.Position.X) + end) + + self.releaseConnection = UserInputService.InputEnded:Connect(function(inputObject) + local inputType = inputObject.UserInputType + if inputType ~= Enum.UserInputType.MouseButton1 and inputType ~= Enum.UserInputType.Touch then + return + end + + -- Stop listening for drag events before processing the input, since + -- that involves a callback to the user of the slider. + -- Only process one knob slider input because the two knob slider + -- should not move if it is not being dragged (since the track is not clickable) + -- and, therefore, should not process if the drag is ending + self:stopListeningForDrag() + self:processOneKnobDrag(inputObject.Position.X) + end) + + -- If the window loses focus the user can release the mouse and we won't + -- know about it, so the slider could get "stuck" to the mouse, even + -- though the user has let go of the mouse button. To resolve this, we + -- stop listening to events when we lose focus. + self.focusLostConnection = UserInputService.WindowFocusReleased:Connect(function() + self:stopListeningForDrag() + end) + else + -- This is the ugly, scary path, where UserInputService isn't available to + -- us and we have to cheat. In a PluginGui, UserInputService doesn't work; + -- its events only fire in the main viewport. The only way, currently, to + -- capture input like this is by creating a fake button at the top level + -- so that it overlays everything, then listening to input events on that. + -- This process is of less importance than the UserInputService connection + -- above, because it will only be run when the slider is used in a + -- Horsecat story or a similar environment. + local pluginGui = root:FindFirstAncestorWhichIsA("PluginGui") + + local inputCapturer = Instance.new("ImageButton") + inputCapturer.BackgroundTransparency = 1 + inputCapturer.Image = "" + inputCapturer.Name = "SliderPluginGuiInputCapturer" + inputCapturer.Size = UDim2.new(1, 0, 1, 0) + inputCapturer.ZIndex = PLUGINGUI_INPUT_CAPTURER_ZINDEX + self.moveConnection = inputCapturer.MouseMoved:Connect(function(x) + self:processDrag(x) + end) + + self.releaseConnection = inputCapturer.MouseButton1Up:Connect(function(x) + self:stopListeningForDrag() + self:processOneKnobDrag(x) + end) + + self.focusLostConnection = inputCapturer.MouseLeave:Connect(function(x) + self:stopListeningForDrag() + self:processOneKnobDrag(x) + end) + + inputCapturer.Parent = pluginGui + self.inputCapturerButton = inputCapturer + end + + self.dragging = true + + if self.lowerKnobDrag and self.props.onDragStartLower ~= nil then + self.props.onDragStartLower() + end + + if self.upperKnobDrag and self.props.onDragStartUpper ~= nil then + self.props.onDragStartUpper() + end +end + +function GenericSlider:getSteppedValue(x) + local root = self.rootRef.current + if root == nil then + return 0 + end + + local min = self.props.min + local max = self.props.max + local stepInterval = self.props.stepInterval + + local absoluteWidth = root.AbsoluteSize.X + local relativeX = x - root.AbsolutePosition.X + local clampedX = math.clamp(relativeX, 0, absoluteWidth) + local fractional = clampedX / absoluteWidth + local unsteppedValue = (fractional * (max - min)) + min + return math.floor(unsteppedValue / stepInterval + 0.5) * stepInterval +end + +function GenericSlider:processDrag(x) + if self:hasTwoKnobs() then + self:processTwoKnobDrag(x) + else + self:processOneKnobDrag(x) + end +end + +function GenericSlider:processOneKnobDrag(x) + if self:hasTwoKnobs() then + return + end + + local steppedValue = self:getSteppedValue(x) + self.lowerKnobDrag = true + + if steppedValue ~= self.props.lowerValue then + self.props.onValueChanged(steppedValue) + end +end + +function GenericSlider:processTwoKnobDrag(x) + local steppedValue = self:getSteppedValue(x) + local lowerValue = self.props.lowerValue + local upperValue = self.props.upperValue + + if not self.lowerKnobDrag and not self.upperKnobDrag then + --Set which knob is being dragged (both if they are at the same position) + if steppedValue == lowerValue then + self.lowerKnobDrag = true + end + if steppedValue == upperValue then + self.upperKnobDrag = true + end + elseif self.lowerKnobDrag and self.upperKnobDrag then + --decides which knob to actually drag and change the value of when both are atop one another + if steppedValue - self.props.stepInterval >= upperValue then + self.upperKnobDrag = true + self.lowerKnobDrag = false + upperValue = steppedValue + elseif steppedValue + self.props.stepInterval <= lowerValue then + self.upperKnobDrag = false + self.lowerKnobDrag = true + lowerValue = steppedValue + end + elseif self.lowerKnobDrag then + --drag the left knob (but not sofar as to surpass the right knob) + if steppedValue <= upperValue then + lowerValue = steppedValue + end + elseif self.upperKnobDrag then + --drag the right knob (but not sofar as to surpass the left knob) + if steppedValue >= lowerValue then + upperValue = steppedValue + end + end + + if upperValue ~= self.props.upperValue or lowerValue ~= self.props.lowerValue then + self.props.onValueChanged(lowerValue, upperValue) + end +end + +function GenericSlider:stopListeningForDrag() + if self.moveConnection ~= nil then + self.moveConnection:Disconnect() + self.moveConnection = nil + end + + if self.releaseConnection ~= nil then + self.releaseConnection:Disconnect() + self.releaseConnection = nil + end + + if self.focusLostConnection ~= nil then + self.focusLostConnection:Disconnect() + self.focusLostConnection = nil + end + + if self.inputCapturerButton ~= nil then + self.inputCapturerButton:Destroy() + self.inputCapturerButton = nil + end + + self.dragging = false + self.lowerKnobDrag = false + self.upperKnobDrag = false + + if self.props.onDragEnd ~= nil then + self.props.onDragEnd() + end +end + +function GenericSlider:hasTwoKnobs() + return self.props.upperValue ~= nil +end + +return GenericSlider \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.spec.lua new file mode 100644 index 0000000..837aa91 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.spec.lua @@ -0,0 +1,81 @@ +return function() + local Slider = script.Parent + local Core = Slider.Parent + local UIBlox = Core.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local GenericSlider = require(Slider.GenericSlider) + + it("should create and destroy without errors", function() + local genericSlider = Roact.createElement(GenericSlider, { + lowerValue = 0, + min = -10, + max = 10, + stepInterval = 1, + + onValueChanged = function() end, + + trackImage = "rbxassetid://3792530835", + -- Allow bindings for style props + trackColor = Color3.fromRGB(0, 0, 0), + trackTransparency = 0, + + trackFillImage = "rbxassetid://3792530835", + trackFillColor = Color3.fromRGB(0, 0, 0), + trackFillTransparency = 0, + + knobImage = "rbxassetid://3792530835", + knobColorLower = Color3.fromRGB(0, 0, 0), + knobTransparency = 0, + + knobShadowImage = "rbxassetid://3792530835", + knobShadowTransparencyLower = 0, + }) + local instance = Roact.mount(genericSlider) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with all props", function() + local genericSlider = Roact.createElement(GenericSlider, { + lowerValue = 0, + upperValue = 8, + min = -10, + max = 10, + stepInterval = 1, + isDisabled = false, + + onValueChanged = function() end, + onDragStartLower = function() end, + onDragEnd = function() end, + + trackImage = "rbxassetid://3792530835", + -- Allow bindings for style props + trackColor = Color3.fromRGB(0, 0, 0), + trackTransparency = 0, + trackSliceCenter = Rect.new(8, 8, 9, 9), + + trackFillImage = "rbxassetid://3792530835", + trackFillColor = Color3.fromRGB(0, 0, 0), + trackFillTransparency = 0, + trackFillSliceCenter = Rect.new(8, 8, 9, 9), + + knobImage = "rbxassetid://3792530835", + knobColorLower = Color3.fromRGB(0, 0, 0), + knobTransparency = 0, + + knobImagePadding = 0, + + knobShadowImage = "rbxassetid://3792530835", + knobShadowTransparencyLower = 0, + + width = UDim.new(1, 1, 1, 1), + position = UDim2.new(1, 1, 1, 1), + anchorPoint = Vector2.new(0.5, 0.5), + layoutOrder = 0, + }) + local instance = Roact.mount(genericSlider) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.story.lua new file mode 100644 index 0000000..1c929b6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Slider/GenericSlider.story.lua @@ -0,0 +1,74 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Roact = require(ReplicatedStorage.Packages.Roact) + +local GenericSlider = require(script.Parent.GenericSlider) + +local Images = require(script.Parent.Parent.Parent.App.ImageSet.Images) + +local Story = Roact.PureComponent:extend("Story") + +function Story:init() + self:setState({ + value = 50, + drag = false, + }) +end + +function Story:render() + return Roact.createElement(GenericSlider, { + lowerValue = self.state.value, + min = 0, + max = 100, + stepInterval = 10, + isDisabled = false, + + onValueChanged = function(value) + print("New slider value:", value) + self:setState({ + value = value, + }) + end, + onDragStartLower = function() + self:setState({ + drag = true, + }) + end, + onDragEnd = function() + self:setState({ + drag = false, + }) + end, + + trackImage = Images["component_assets/circle_16"], + trackTransparency = 0, + trackSliceCenter = Rect.new(8, 8, 8, 8), + trackColor = Color3.new(0, 0, 0), + + trackFillImage = Images["component_assets/circle_16"], + trackFillColor = Color3.new(1, 0, 0), + trackFillSliceCenter = Rect.new(8, 8, 8, 8), + trackFillTransparency = 0, + + knobImage = Images["component_assets/circle_28_padding_10"], + knobColorLower = Color3.new(0.8, 0.8, 0.8), + knobTransparency = 0, + + knobImagePadding = 0, + + knobShadowImage = Images["component_assets/dropshadow_28"], + knobShadowTransparencyLower = self.state.drag and 1 or 0, + + position = UDim2.fromScale(0.5, 0.5), + width = UDim.new(0.8, 0), + anchorPoint = Vector2.new(0.5, 0.5), + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(Story), target, "GenericSlider") + + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleConsumer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleConsumer.lua new file mode 100644 index 0000000..24b31dd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleConsumer.lua @@ -0,0 +1,52 @@ +local Style = script.Parent +local Core = Style.Parent +local UIBlox = Core.Parent + +--Note: remove along with styleRefactorConfig +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local styleRefactorConfig = UIBloxConfig.styleRefactorConfig + +if not styleRefactorConfig then + return require(UIBlox.Style.withStyle) +end +--- + +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local Palette = require(Style.Symbol.Palette) + +local StyleConsumer = Roact.Component:extend("StyleConsumer") + +local validateProps = t.strictInterface({ + render = t.callback, +}) + +function StyleConsumer:init() + self.palette = self._context[Palette] + local currentStyle = self.palette.style + + self.state = { + style = currentStyle, + } +end + +function StyleConsumer:render() + assert(validateProps(self.props)) + return self.props.render(self.state.style) +end + +function StyleConsumer:didMount() + self.disconnectStyleListener = self.palette.signal:subscribe(function(newStyle) + self:setState({ + style = newStyle, + }) + end) +end + +function StyleConsumer:willUnmount() + self.disconnectStyleListener() +end + +return StyleConsumer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleConsumer.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleConsumer.spec.lua new file mode 100644 index 0000000..1ff82a1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleConsumer.spec.lua @@ -0,0 +1,39 @@ +return function() + local Style = script.Parent + local Core = Style.Parent + local UIBlox = Core.Parent + --Note: remove along with styleRefactorConfig + local UIBloxConfig = require(UIBlox.UIBloxConfig) + local styleRefactorConfig = UIBloxConfig.styleRefactorConfig + + if not styleRefactorConfig then + return + end + --- + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + local StyleProvider = require(Style.StyleProvider) + + local StyleConsumer = require(script.Parent.StyleConsumer) + + + it("should create and destroy without errors", function() + local renderFunction = function(style) + expect(style).to.be.a("table") + return Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }) + end + local element = Roact.createElement(StyleProvider, { + style = {}, + }, { + StyleConsumer = Roact.createElement(StyleConsumer, { + render = renderFunction + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StylePalette.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StylePalette.lua new file mode 100644 index 0000000..f5f7573 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StylePalette.lua @@ -0,0 +1,24 @@ +local Style = script.Parent +local Core = Style.Parent +local UIBlox = Core.Parent + +local createSignal = require(UIBlox.Utility.createSignal) + +local StylePalette = {} + +function StylePalette.new(style) + local self = {} + self.style = style + self.signal = createSignal() + setmetatable(self, { + __index = StylePalette, + }) + return self +end + +function StylePalette:update(newStyle) + self.style = newStyle + self.signal:fire(newStyle) +end + +return StylePalette \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StylePalette.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StylePalette.spec.lua new file mode 100644 index 0000000..9c64c1f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StylePalette.spec.lua @@ -0,0 +1,53 @@ +return function() + local StylePalette = require(script.Parent.StylePalette) + + local testTheme = { + Background1 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background2 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background3 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background4 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0.3, -- Alpha 0.7 + }, + } + + local testFont = { + Normal = Enum.Font.Gotham, + Title = Enum.Font.GothamBold, + } + + it("should connect and fire signal for style change without errors", function() + local testStyle = { + Theme = testTheme, + Font = testFont, + } + local appStyle = StylePalette.new(testStyle) + + expect(appStyle.style).to.be.a("table") + + local testValue = "some test theme" + local testTable = { + Theme = testValue, + } + local disconnect = appStyle.signal:subscribe(function(newValues) + expect(newValues).to.be.a("table") + expect(newValues.Theme).to.equal(testValue) + end) + + appStyle:update(testTable) + + expect(appStyle.style).to.be.a("table") + expect(appStyle.style.Theme).to.equal(testValue) + disconnect() + end) + +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleProvider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleProvider.lua new file mode 100644 index 0000000..8d661d8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleProvider.lua @@ -0,0 +1,46 @@ +local Style = script.Parent +local Core = Style.Parent +local UIBlox = Core.Parent + +--Note: remove along with styleRefactorConfig +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local styleRefactorConfig = UIBloxConfig.styleRefactorConfig + +if not styleRefactorConfig then + return require(UIBlox.Style.StyleProvider) +end +--- + +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local StylePalette = require(Style.StylePalette) +local Palette = require(Style.Symbol.Palette) + +local StyleProvider = Roact.Component:extend("StyleProvider") + +local validateProps = t.strictInterface({ + -- The current style of the app. + style = t.table, + [Roact.Children] = t.table, +}) + +function StyleProvider:init() + local style = self.props.style + self.stylePalette = StylePalette.new(style) + self._context[Palette] = self.stylePalette +end + +function StyleProvider:render() + assert(validateProps(self.props)) + return Roact.oneChild(self.props[Roact.Children]) +end + +function StyleProvider:didUpdate(previousProps) + if self.props.style ~= previousProps.style then + self.style:update(self.props.style) + end +end + +return StyleProvider \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleProvider.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleProvider.spec.lua new file mode 100644 index 0000000..31411d9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/StyleProvider.spec.lua @@ -0,0 +1,68 @@ +return function() + local Style = script.Parent + local Core = Style.Parent + local UIBlox = Core.Parent + --Note: remove along with styleRefactorConfig + local UIBloxConfig = require(UIBlox.UIBloxConfig) + local styleRefactorConfig = UIBloxConfig.styleRefactorConfig + + if not styleRefactorConfig then + return + end + --- + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + + local StyleProvider = require(script.Parent.StyleProvider) + + local testTheme = { + Background1 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background2 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background3 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background4 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0.3, -- Alpha 0.7 + }, + } + + local testFont = { + Normal = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 1, + RelativeMinSize = 1, + }, + Title = { + Font = Enum.Font.GothamBold, + RelativeSize = 1, + RelativeMinSize = 1, + }, + } + + local testStyle = { + Theme = testTheme, + Font = testFont, + } + + it("should create and destroy without errors", function() + local someComponent = Roact.createElement("TextLabel", { + Text = "test", + }) + local styleProvider = Roact.createElement(StyleProvider, { + style = testStyle, + }, { + SomeComponent = someComponent, + }) + + local roactInstance = Roact.mount(styleProvider) + Roact.unmount(roactInstance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Symbol/Palette.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Symbol/Palette.lua new file mode 100644 index 0000000..6708c02 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Symbol/Palette.lua @@ -0,0 +1,7 @@ +local Style = script.Parent.Parent +local Core = Style.Parent +local Symbol = require(Core.Utility.Symbol) + +local Palette = Symbol.named("Palette") + +return Palette \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Validator/validateColorInfo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Validator/validateColorInfo.lua new file mode 100644 index 0000000..b13357c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Validator/validateColorInfo.lua @@ -0,0 +1,12 @@ +local Validator = script.Parent +local Style = Validator.Parent +local App = Style.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +return t.strictInterface({ + Color = t.Color3, + Transparency = t.numberConstrained(0, 1), +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Validator/validateFontInfo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Validator/validateFontInfo.lua new file mode 100644 index 0000000..a77d959 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/Validator/validateFontInfo.lua @@ -0,0 +1,13 @@ +local Validator = script.Parent +local Style = Validator.Parent +local App = Style.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local t = require(Packages.t) + +return t.strictInterface({ + RelativeSize = t.numberMinExclusive(0), + RelativeMinSize = t.numberMinExclusive(0), + Font = t.EnumItem, +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/withStyle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/withStyle.lua new file mode 100644 index 0000000..96bd76b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/withStyle.lua @@ -0,0 +1,27 @@ +local Style = script.Parent +local Core = Style.Parent +local UIBlox = Core.Parent + +--Note: remove along with styleRefactorConfig +local UIBloxConfig = require(UIBlox.UIBloxConfig) +local styleRefactorConfig = UIBloxConfig.styleRefactorConfig + +if not styleRefactorConfig then + return require(UIBlox.Style.withStyle) +end +--- + +local Packages = UIBlox.Parent +local Roact = require(Packages.Roact) + + +local StyleConsumer = require(Style.StyleConsumer) + +--[[ + This is a utility function that will wrap StyleConsumer. + `renderCallback` will be invoked with the current style. It should return a Roact element. +]] +return function(renderCallback) + assert(type(renderCallback) == "function", "Expect renderCallback to be a function.") + return Roact.createElement(StyleConsumer, { render = renderCallback }) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/withStyle.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/withStyle.spec.lua new file mode 100644 index 0000000..b5b7abc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Style/withStyle.spec.lua @@ -0,0 +1,80 @@ +return function() + local Style = script.Parent + local Core = Style.Parent + local UIBlox = Core.Parent + + --Note: remove along with styleRefactorConfig + local UIBloxConfig = require(UIBlox.UIBloxConfig) + local styleRefactorConfig = UIBloxConfig.styleRefactorConfig + + if not styleRefactorConfig then + return + end + --- + + local Packages = UIBlox.Parent + local Roact = require(Packages.Roact) + + local StyleProvider = require(Style.StyleProvider) + + local withStyle = require(script.Parent.withStyle) + + local testTheme = { + Background1 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background2 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background3 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background4 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0.3, -- Alpha 0.7 + }, + } + + local testFont = { + Normal = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 1, + RelativeMinSize = 1, + }, + Title = { + Font = Enum.Font.GothamBold, + RelativeSize = 1, + RelativeMinSize = 1, + }, + } + + local testStyle = { + Theme = testTheme, + Font = testFont, + } + + it("should create and destroy without errors", function() + local someTestElement = Roact.Component:extend("someTestElement") + -- luacheck: ignore unused argument self + function someTestElement:render() + return withStyle(function(style) + expect(style).to.be.a("table") + return Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }) + end) + end + + local element = Roact.createElement(StyleProvider, { + style = testStyle, + }, { + someTestElement = Roact.createElement(someTestElement), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ExpandableText/ExpandableTextUtils.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ExpandableText/ExpandableTextUtils.lua new file mode 100644 index 0000000..17d84eb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ExpandableText/ExpandableTextUtils.lua @@ -0,0 +1,21 @@ +local GetTextHeight = require(script.Parent.Parent.GetTextHeight) + +local function getExpandableTextHeights(font, frameWidth, textContent, compactNumberOfLines) + local textSize = font.BaseSize * font.Body.RelativeSize + local fullTextHeight = GetTextHeight(textContent, font.Body.Font, textSize, frameWidth) + local compactHeight = compactNumberOfLines * textSize + + return fullTextHeight, compactHeight +end + +--Function for whether or not an ExpandableTextArea can expand given parameters +--regarding font, text, width of the frame container, and number of lines in the compact view +local function getCanExpand(font, frameWidth, textContent, compactNumberOfLines) + local fullTextHeight, compactHeight = getExpandableTextHeights(font, frameWidth, textContent, compactNumberOfLines) + return fullTextHeight > compactHeight +end + +return { + getExpandableTextHeights = getExpandableTextHeights, + getCanExpand = getCanExpand, +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/GenericTextLabel.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/GenericTextLabel.lua new file mode 100644 index 0000000..1844a9e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/GenericTextLabel.lua @@ -0,0 +1,95 @@ +local GenericTextLabelRoot = script.Parent +local Text = GenericTextLabelRoot.Parent +local App = Text.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) +local validateFontInfo = require(UIBlox.Core.Style.Validator.validateFontInfo) +local validateColorInfo = require(UIBlox.Core.Style.Validator.validateColorInfo) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local GenericTextLabel = Roact.PureComponent:extend("GenericTextLabel") + +local MAX_BOUND = 10000 + +local validateProps = t.interface({ + -- The max size avaliable for the textbox + maxSize = t.optional(t.Vector2), + + -- The Font table from the style palette + fontStyle = validateFontInfo, + + -- The color table from the style palette + colorStyle = validateColorInfo, + + -- Whether the TextLabel is Fluid Sizing between the font's min and default sizes (optional) + fluidSizing = t.optional(t.boolean), + + -- Note that this component can accept all valid properties of the Roblox TextLabel instance +}) + +GenericTextLabel.defaultProps = { + maxSize = Vector2.new(MAX_BOUND, MAX_BOUND), + fluidSizing = false, +} + +function GenericTextLabel:render() + assert(validateProps(self.props)) + + local text = self.props.Text + local isFluidSizing = self.props.fluidSizing + + return withStyle(function(stylePalette) + local font = self.props.fontStyle + local color = self.props.colorStyle + local textColor = color.Color + local textTransparency = color.Transparency + + local baseSize = stylePalette.Font.BaseSize + local fontSizeMin = font.RelativeMinSize * baseSize + local fontSizeMax = font.RelativeSize * baseSize + local textFont = font.Font + + local textboxSize = self.props.Size + if textboxSize == nil then + local sampleText = text + if self.props.TextTruncate == Enum.TextTruncate.AtEnd then + sampleText = sampleText.."..." + end + local textBounds = self.props.maxSize + local textboxBounds = GetTextSize(sampleText, fontSizeMax, textFont, textBounds) + textboxSize = UDim2.new(0, textboxBounds.X, 0, textboxBounds.Y) + end + + local newProps = Cryo.Dictionary.join(self.props, { + [Roact.Children] = Cryo.None, + fluidSizing = Cryo.None, + fontStyle = Cryo.None, + colorStyle = Cryo.None, + maxSize = Cryo.None, + Size = textboxSize, + Text = text, + Font = textFont, + TextSize = fontSizeMax, + TextColor3 = textColor, + TextTransparency = textTransparency, + TextWrapped = self.props.TextWrapped == nil and true or self.props.TextWrapped, + TextScaled = isFluidSizing, + BackgroundTransparency = 1, + }) + + return Roact.createElement("TextLabel", newProps, Cryo.Dictionary.join({ + UITextSizeConstraint = isFluidSizing and Roact.createElement("UITextSizeConstraint", { + MaxTextSize = fontSizeMax, + MinTextSize = fontSizeMin, + } or nil) + }, self.props[Roact.Children] or {})) + end) +end + +return GenericTextLabel diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/GenericTextLabel.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/GenericTextLabel.spec.lua new file mode 100644 index 0000000..0d7165c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/GenericTextLabel.spec.lua @@ -0,0 +1,73 @@ +return function() + local GenericTextLabelRoot = script.Parent + local Text = GenericTextLabelRoot.Parent + local App = Text.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local GenericTextLabel = require(GenericTextLabelRoot.GenericTextLabel) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local TestStyle = require(UIBlox.App.Style.Validator.TestStyle) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + provider = Roact.createElement(GenericTextLabel, { + Text = "Test text", + Size = UDim2.new(0, 100, 0, 50), + colorStyle = TestStyle.Theme.SystemPrimaryDefault, + fontStyle = TestStyle.Font.Title, + fluidSizing = true, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and be able to auto size itself if not size is given", function() + local element = mockStyleComponent({ + provider = Roact.createElement(GenericTextLabel, { + Text = "Test text", + colorStyle = TestStyle.Theme.SystemPrimaryDefault, + fontStyle = TestStyle.Font.Title, + fluidSizing = true, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and be able to respect the max size", function() + local element = mockStyleComponent({ + provider = Roact.createElement(GenericTextLabel, { + Text = "Test text", + maxSize = Vector2.new(200, 40), + colorStyle = TestStyle.Theme.SystemPrimaryDefault, + fontStyle = TestStyle.Font.Title, + fluidSizing = true, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should accept all properties of the TextLabel instance", function() + local element = mockStyleComponent({ + provider = Roact.createElement(GenericTextLabel, { + Text = "Test text", + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(.5, 0, .5, 0), + colorStyle = TestStyle.Theme.SystemPrimaryDefault, + fontStyle = TestStyle.Font.Header1, + TextYAlignment = Enum.TextYAlignment.Bottom, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/__stories__/GenericTextLabel.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/__stories__/GenericTextLabel.story.lua new file mode 100644 index 0000000..2d9341d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GenericTextLabel/__stories__/GenericTextLabel.story.lua @@ -0,0 +1,51 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local GenericTextLabelRoot = script.Parent.Parent +local Text = GenericTextLabelRoot.Parent +local Core = Text.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local withStyle = require(UIBlox.Core.Style.withStyle) + +local Roact = require(Packages.Roact) + +local GenericTextLabel = require(GenericTextLabelRoot.GenericTextLabel) + +local GenericTextLabelStory = Roact.PureComponent:extend("GenericTextLabelStory") + +function GenericTextLabelStory:render() + return withStyle(function(style) + local theme = style.Theme + local font = style.Font + return Roact.createElement(StoryItem, { + size = UDim2.new(0, 300, 0, 128), + layoutOrder = 1, + title = "Generic Text Label", + subTitle = "Text.GenericTextLabel", + showDivider = true, + }, { + GenericTextLabel = Roact.createElement(GenericTextLabel, { + Text = "Phantom Forces [Sniper Update!]", + Size = UDim2.new(0, 150, 0, 45), + colorStyle = theme.SystemPrimaryDefault, + fontStyle = font.Header1, + fluidSizing = true, + }), + }) + end) +end + +return function(target) + local styleProvider = Roact.createElement(StoryView, {}, { + Story = Roact.createElement(GenericTextLabelStory), + }) + + local handle = Roact.mount(styleProvider, target, "GenericTextLabel") + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GetTextHeight.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GetTextHeight.lua new file mode 100644 index 0000000..b499a14 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GetTextHeight.lua @@ -0,0 +1,12 @@ +local GetTextSize = require(script.Parent.GetTextSize) + +-- NOTE: Any number greater than 2^30 will make TextService:GetTextSize give invalid results +local MAX_BOUND = 10000 + +local function getTextHeight(text, font, fontSize, widthCap) + local bounds = Vector2.new(widthCap, MAX_BOUND) + local textSize = GetTextSize(text, fontSize, font, bounds) + return textSize.Y +end + +return getTextHeight \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GetTextSize.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GetTextSize.lua new file mode 100644 index 0000000..30bc720 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/GetTextSize.lua @@ -0,0 +1,14 @@ +-- This file implements a wrapper function for TextService:GetTextSize() +-- Extra padding is added to the returned size because of a rounding issue with GetTextSize +-- TODO: Remove this temporary additional padding when CLIPLAYEREX-1633 is fixed + +local TextService = game:GetService("TextService") + +local TEMPORARY_TEXT_SIZE_PADDING = Vector2.new(2, 2) + +local function getTextSize(...) + local textSize = TextService:GetTextSize(...) + return textSize + TEMPORARY_TEXT_SIZE_PADDING +end + +return getTextSize \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/ImageTextLabel.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/ImageTextLabel.lua new file mode 100644 index 0000000..d29342d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/ImageTextLabel.lua @@ -0,0 +1,102 @@ +local ImageTextLabelRoot = script.Parent +local Text = ImageTextLabelRoot.Parent +local App = Text.Parent +local UIBlox = App.Parent +local Packages = UIBlox.Parent + +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local ImageSetComponent = require(UIBlox.Core.ImageSet.ImageSetComponent) +local GenericTextLabel = require(UIBlox.Core.Text.GenericTextLabel.GenericTextLabel) +local GetTextSize = require(UIBlox.Core.Text.GetTextSize) +local validateFontInfo = require(UIBlox.Core.Style.Validator.validateFontInfo) +local withStyle = require(UIBlox.Core.Style.withStyle) + +-- This component is used to inline an icon into your text. +-- The current version of the component will only work with the icon being before the text +-- Text must be aligned at the top left and cannot be dynamic. +-- Icon positioning is left up to the user to allow as much functionality as possible +local ImageTextLabel = Roact.PureComponent:extend("ImageTextLabel") + +local MAX_BOUND = 10000 + +-- If this component ever becomes public, please restrict the props that can be used on imageProps, textProps +-- Removing a used prop in the future will be difficult. +ImageTextLabel.validateProps = t.interface({ + imageProps = t.optional(t.interface({ + Size = t.UDim2, + })), + + genericTextLabelProps = t.interface({ + Text = t.string, + fontStyle = validateFontInfo, + + AnchorPoint = t.None, + Position = t.None, + Size = t.None, + TextXAlignment = t.None, + TextYAlignment = t.None, + TextScaled = t.None, + maxSize = t.None, + }), + + frameProps = t.optional(t.interface({ + Size = t.None, + })), + + maxSize = t.optional(t.Vector2), + padding = t.optional(t.number), +}) + +ImageTextLabel.defaultProps = { + maxSize = Vector2.new(MAX_BOUND, MAX_BOUND), + frameProps = {}, + padding = 0, +} + +function ImageTextLabel:render() + local genericTextLabelProps = self.props.genericTextLabelProps + local imageProps = self.props.imageProps + local frameProps = self.props.frameProps + local padding = self.props.padding + local text = self.props.genericTextLabelProps.Text + local maxSize = self.props.maxSize + + return withStyle(function(stylePalette) + local fontStyle = genericTextLabelProps.fontStyle + + local baseSize = stylePalette.Font.BaseSize + local fontSize = fontStyle.RelativeSize * baseSize + local font = fontStyle.Font + + if imageProps then + -- This method has flaws given our non-monospaced font but is the easiest closest approach to getting inlined icons. + local spaceTextSize = GetTextSize(" ", fontSize, font, Vector2.new(0, 0)) + - GetTextSize(" ", fontSize, font, Vector2.new(0, 0)) + local numSpaces = math.ceil((imageProps.Size.X.Offset + padding) / spaceTextSize.X) + text = string.rep(" ", numSpaces)..text + end + + local labelTextSize = GetTextSize(text, fontSize, font, Vector2.new(maxSize.X, maxSize.Y)) + + return Roact.createElement("Frame", Cryo.Dictionary.join(frameProps, { + Size = UDim2.new(0, labelTextSize.X, 0, labelTextSize.Y) + }), { + Icon = self.props.imageProps and Roact.createElement(ImageSetComponent.Label, imageProps) or nil, + Name = Roact.createElement(GenericTextLabel, Cryo.Dictionary.join(genericTextLabelProps, { + Text = text, + AnchorPoint = Vector2.new(0, 0), + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + fluidSizing = false, + maxSize = maxSize, + })), + }) + end) +end + +return ImageTextLabel \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/ImageTextLabel.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/ImageTextLabel.spec.lua new file mode 100644 index 0000000..7e32e40 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/ImageTextLabel.spec.lua @@ -0,0 +1,74 @@ +return function() + local ImageTextLabelRoot = script.Parent + local Text = ImageTextLabelRoot.Parent + local App = Text.Parent + local UIBlox = App.Parent + local Packages = UIBlox.Parent + + local Roact = require(Packages.Roact) + + local ImageTextLabel = require(ImageTextLabelRoot.ImageTextLabel) + local mockStyleComponent = require(UIBlox.Utility.mockStyleComponent) + local TestStyle = require(UIBlox.App.Style.Validator.TestStyle) + + it("should create and destroy without errors", function() + local element = mockStyleComponent({ + Roact.createElement(ImageTextLabel, { + imageProps = { + Size = UDim2.new(0, 50, 0, 50), + }, + genericTextLabelProps = { + Text = "Text", + TextSize = 15, + colorStyle = TestStyle.Theme.SystemPrimaryDefault, + fontStyle = TestStyle.Font.Title, + }, + padding = 4, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should handle not having an image", function() + local element = mockStyleComponent({ + Roact.createElement(ImageTextLabel, { + genericTextLabelProps = { + Text = "Text", + TextSize = 15, + colorStyle = TestStyle.Theme.SystemPrimaryDefault, + fontStyle = TestStyle.Font.Title, + }, + padding = 4, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should accept all properties", function() + local element = mockStyleComponent({ + Roact.createElement(ImageTextLabel, { + imageProps = { + Size = UDim2.new(0, 50, 0, 50), + }, + genericTextLabelProps = { + Text = "Text", + TextSize = 15, + colorStyle = TestStyle.Theme.SystemPrimaryDefault, + fontStyle = TestStyle.Font.Title, + }, + frameProps = { + BackgroundTransparency = 1, + }, + padding = 4, + maxSize = Vector2.new(100, 100), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/__stories__/ImageTextLabel.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/__stories__/ImageTextLabel.story.lua new file mode 100644 index 0000000..ea0af53 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Text/ImageTextLabel/__stories__/ImageTextLabel.story.lua @@ -0,0 +1,130 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) +local StoryItem = require(ReplicatedStorage.Packages.StoryComponents.StoryItem) + +local ImageTextLabelRoot = script.Parent.Parent +local Text = ImageTextLabelRoot.Parent +local Core = Text.Parent +local UIBlox = Core.Parent +local Packages = UIBlox.Parent + +local Images = require(UIBlox.App.ImageSet.Images) +local withStyle = require(UIBlox.Core.Style.withStyle) + +local Roact = require(Packages.Roact) + +local ImageTextLabel = require(ImageTextLabelRoot.ImageTextLabel) + +local ImageTextLabelStory = Roact.PureComponent:extend("ImageTextLabelStory") + +function ImageTextLabelStory:render() + return withStyle(function(style) + local theme = style.Theme + local font = style.Font + + local titleIcon = Images["icons/status/premium_small"] + local titleIconSize = titleIcon.ImageRectSize / Images.ImagesResolutionScale + + return Roact.createElement(StoryItem, { + size = UDim2.new(0, 300, 0, 128), + layoutOrder = 1, + title = "Image Text Label", + subTitle = "Text.ImageTextLabel", + showDivider = true, + }, { + verticalLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = UDim.new(0, 10), + }), + OneLineNoIcon = Roact.createElement(ImageTextLabel, { + genericTextLabelProps = { + TextSize = 15, + colorStyle = theme.TextEmphasis, + fontStyle = font.Header2, + Text = "One line with no Icon", + TextTruncate = Enum.TextTruncate.AtEnd, + }, + frameProps = { + BackgroundTransparency = 1, + LayoutOrder = 1, + }, + padding = 4, + }), + OneLine = Roact.createElement(ImageTextLabel, { + imageProps = { + BackgroundTransparency = 1, + Image = titleIcon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Size = UDim2.new(0, titleIconSize.X, 0, titleIconSize.Y), + AnchorPoint = Vector2.new(0, 0), + Position = UDim2.new(0, 0, 0, 0), + }, + genericTextLabelProps = { + TextSize = 15, + colorStyle = theme.TextEmphasis, + fontStyle = font.Header2, + Text = "One line with icon", + TextTruncate = Enum.TextTruncate.AtEnd, + }, + frameProps = { + BackgroundTransparency = 1, + LayoutOrder = 2, + }, + padding = 4, + }), + TwoLinesNoIcon = Roact.createElement(ImageTextLabel, { + genericTextLabelProps = { + TextSize = 15, + colorStyle = theme.TextEmphasis, + fontStyle = font.Header2, + Text = "Multiple lined text that should truncate at the end", + TextTruncate = Enum.TextTruncate.AtEnd, + }, + frameProps = { + BackgroundTransparency = 1, + LayoutOrder = 3, + }, + padding = 4, + maxSize = Vector2.new(170, 40), + }), + TwoLines = Roact.createElement(ImageTextLabel, { + imageProps = { + BackgroundTransparency = 1, + Image = titleIcon, + ImageColor3 = theme.IconEmphasis.Color, + ImageTransparency = theme.IconEmphasis.Transparency, + Size = UDim2.new(0, titleIconSize.X, 0, titleIconSize.Y), + AnchorPoint = Vector2.new(0, 0), + Position = UDim2.new(0, 0, 0, 0), + }, + genericTextLabelProps = { + TextSize = 15, + colorStyle = theme.TextEmphasis, + fontStyle = font.Header2, + Text = "Multiple lined text that should truncate at the end", + TextTruncate = Enum.TextTruncate.AtEnd, + }, + frameProps = { + BackgroundTransparency = 1, + LayoutOrder = 4, + }, + padding = 4, + maxSize = Vector2.new(170, 40), + }) + }) + end) +end + +return function(target) + local styleProvider = Roact.createElement(StoryView, {}, { + Story = Roact.createElement(ImageTextLabelStory), + }) + + local handle = Roact.mount(styleProvider, target, "ImageTextLabel") + return function() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/Symbol.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/Symbol.lua new file mode 100644 index 0000000..8b6adaf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/Symbol.lua @@ -0,0 +1,43 @@ +--[[ + A 'Symbol' is an opaque marker type that can be used to signify unique + statuses. Symbols have the type 'userdata', but when printed to the console, + the name of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +--[[ + Create an unnamed Symbol. Usually, you should create a named Symbol using + Symbol.named(name) +]] +function Symbol.unnamed() + local self = newproxy(true) + + getmetatable(self).__tostring = function() + return "Unnamed Symbol" + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/Symbol.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/Symbol.spec.lua new file mode 100644 index 0000000..e05061d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/Symbol.spec.lua @@ -0,0 +1,24 @@ +return function() + local Symbol = require(script.Parent.Symbol) + + describe("named", function() + it("should give an opaque object", function() + local symbol = Symbol.named("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = Symbol.named("foo") + + expect(tostring(symbol):find("foo")).to.be.ok() + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.named("abc") + local symbolB = Symbol.named("abc") + + expect(symbolA).never.to.equal(symbolB) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/bindingValidator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/bindingValidator.lua new file mode 100644 index 0000000..cb4e963 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/bindingValidator.lua @@ -0,0 +1,32 @@ +-- Validator for RoactBinding props +-- expectedType: meta type functions of `t` (e.g. `t.number`) + +local function bindingValidator(expectedType) + return function(binding) + local bindingType = string.match(tostring(binding), "RoactBinding") + + if typeof(binding) ~= "table" or not bindingType then + warn(string.format("RoactBinding expected, got %s", typeof(binding))) + return false + end + + local success, value = pcall(function() + return binding:getValue() + end) + + if not success then + warn("getValue() not defined") + return false + end + + local valueSuccess, valueErrMsg = expectedType(value) + if not valueSuccess then + warn(string.format("RoactBinding value: %s", valueErrMsg)) + return false + end + + return true + end +end + +return bindingValidator diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/bindingValidator.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/bindingValidator.spec.lua new file mode 100644 index 0000000..e8dfa02 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Core/Utility/bindingValidator.spec.lua @@ -0,0 +1,25 @@ +return function() + local UtilityRoot = script.Parent + local UIBloxRoot = UtilityRoot.Parent.Parent + local t = require(UIBloxRoot.Parent.t) + local Roact = require(UIBloxRoot.Parent.Roact) + + local bindingValidator = require(script.Parent.bindingValidator) + local numberBindingValidator = bindingValidator(t.number) + + it("should return false for non-bindings", function() + expect(numberBindingValidator("")).to.equal(false) + expect(numberBindingValidator({})).to.equal(false) + expect(numberBindingValidator(function() end)).to.equal(false) + end) + + it("should return false if binding value type not match", function() + local binding = Roact.createBinding("0") + expect(numberBindingValidator(binding)).to.equal(false) + end) + + it("should return true if binding value matches expected type", function() + local binding = Roact.createBinding(0) + expect(numberBindingValidator(binding)).to.equal(true) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheet.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheet.lua new file mode 100644 index 0000000..69e25ea --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheet.lua @@ -0,0 +1,147 @@ +local Packages = script.Parent.Parent.Parent + +local Otter = require(Packages.Otter) +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) +local withStyle = require(Packages.UIBlox.Core.Style.withStyle) +local ModalBottomSheetButton = require(script.Parent.ModalBottomSheetButton) + +-- https://share.goabstract.com/cfe90baa-ab79-4f34-ad1b-3ef389d39da4 +local ModalBottomSheet = Roact.PureComponent:extend("ModalBottomSheet") + +local WIDTH_THRESHOLD = 600 +local ELEMENT_HEIGHT = 56 +local MAXIMUM_SHEET_ELEMENTS = 7 +local MAXIMUM_SHEET_HEIGHT = ELEMENT_HEIGHT * (MAXIMUM_SHEET_ELEMENTS + 0.5) + +local MOTOR_OPTIONS = { + frequency = 4, + dampingRatio = 1, +} + +local validateProps = t.strictInterface({ + buttonModels = t.array(t.table), + -- this is screenWidth of the app, and is only used to calculate whether the MBS width is fixed or not + screenWidth = t.number, + -- a callback that when fired should result in this component no longer being rendered + -- this should probably relate to closeCentralOverlay in CI + onDismiss = t.callback, + + showImages = t.optional(t.boolean), + bottomGap = t.optional(t.number), + sheetContentXSize = t.optional(t.UDim), + sheetContentXPosition = t.optional(t.UDim), +}) + +ModalBottomSheet.defaultProps = { + bottomGap = 0, + showImages = true, + sheetContentXSize = UDim.new(1, 0), + sheetContentXPosition = UDim.new(0, 0), +} + +function ModalBottomSheet:init() + self.motor = Otter.createSingleMotor(0) + self.ref = Roact.createRef() + self.active = true +end + +function ModalBottomSheet:render() + assert(validateProps(self.props)) + local sheetContentXPosition = self.props.sheetContentXPosition + local sheetContentXSize = self.props.sheetContentXSize + + self.sheetHeight = #self.props.buttonModels * ELEMENT_HEIGHT + if #self.props.buttonModels >= MAXIMUM_SHEET_ELEMENTS then + self.sheetHeight = MAXIMUM_SHEET_HEIGHT + end + local children = {} + for index, buttonProps in ipairs(self.props.buttonModels) do + local mergedProps = Cryo.Dictionary.join(buttonProps, { + hasRoundTop = index == 1, + hasRoundBottom = index == #self.props.buttonModels, + isFixed = self.props.screenWidth > WIDTH_THRESHOLD, + elementHeight = ELEMENT_HEIGHT, + showImage = self.props.showImages, + LayoutOrder = index, + onActivatedAndDismissed = function(a) + if buttonProps.onActivated then + buttonProps.onActivated(a) + end + if not buttonProps.stayOnActivated then + self.props.onDismiss() + end + end, + }) + + children["button " .. index] = Roact.createElement(ModalBottomSheetButton, mergedProps) + end + + children.layout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + return withStyle(function(stylePalette) + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + Background = Roact.createElement("TextButton", { + ZIndex = 0, + AutoButtonColor = false, + BackgroundColor3 = stylePalette.Theme.Overlay.Color, + BackgroundTransparency = stylePalette.Theme.Overlay.Transparency, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 1, 0), + Text = "", + [Roact.Event.Activated] = function() + self.active = false + self.motor:setGoal(Otter.spring(0, MOTOR_OPTIONS)) + end + }), + SheetContent = Roact.createElement("ScrollingFrame", { + BackgroundTransparency = 1, + Size = UDim2.new(sheetContentXSize.Scale, sheetContentXSize.Offset, 0, self.sheetHeight), + Position = UDim2.new(sheetContentXPosition.Scale, sheetContentXPosition.Offset, 1, 0), + ScrollBarThickness = 0, + CanvasSize = UDim2.new( + sheetContentXSize.Scale, + sheetContentXSize.Offset, + 0, + #self.props.buttonModels * ELEMENT_HEIGHT), + ClipsDescendants = true, + [Roact.Ref] = self.ref, + }, children), + }) + end) +end + +function ModalBottomSheet:didMount() + local sheetContentXPosition = self.props.sheetContentXPosition + self.motor:onStep(function(value) + if self.ref.current then + self.ref.current.Position = UDim2.new( + sheetContentXPosition.Scale, + sheetContentXPosition.Offset, + 1, + -(self.sheetHeight + self.props.bottomGap) * value) + end + end) + self.motor:setGoal(Otter.spring(1, MOTOR_OPTIONS)) + self.motor:onComplete(function() + if not self.active then + self.props.onDismiss() + end + end) + self.motor:start() +end + +function ModalBottomSheet:wilUnmount() + self.motor:destroy() +end + +return ModalBottomSheet \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheet.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheet.spec.lua new file mode 100644 index 0000000..dc67bff --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheet.spec.lua @@ -0,0 +1,87 @@ +return function() + local ComponentRoot = script.Parent + local UIBloxRoot = ComponentRoot.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local mockStyleComponent = require(UIBloxRoot.Utility.mockStyleComponent) + local Images = require(UIBloxRoot.App.ImageSet.Images) + + local ModalBottomSheet = require(script.Parent.ModalBottomSheet) + + describe("lifecycle", function() + it("should mount and unmount without issue", function() + local element = mockStyleComponent({ + ModalBottomSheet = Roact.createElement(ModalBottomSheet, { + buttonModels = {}, + screenWidth = 100, + onDismiss = function() end, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + + it("should correctly hold what it's given", function() + local instanceRef = Roact.createRef() + + local folder = Instance.new("Folder") + local element = mockStyleComponent({ + Frame = Roact.createElement("Frame", { + [Roact.Ref] = instanceRef, + }, { + ModalBottomSheet = Roact.createElement(ModalBottomSheet, { + onDismiss = function() end, + screenWidth = 1000, + buttonModels = { + { + icon = Images["component_assets/circle_17"], + text = "someSampleText", + }, + }, + }), + }), + }) + + local instance = Roact.mount(element, folder) + + local modalBottomSheet = instanceRef.current.ModalBottomSheet + local button = modalBottomSheet.SheetContent["button 1"] + + local icon = button.buttonContents:FindFirstChildWhichIsA("ImageLabel") + expect(icon.ImageRectOffset).to.equal(Images["component_assets/circle_17"].ImageRectOffset) + local text = button.buttonContents:FindFirstChildWhichIsA("TextLabel") + expect(text.Text).to.equal("someSampleText") + + Roact.unmount(instance) + end) + + it("should work correctly when renderRightElement is present", function() + local element = mockStyleComponent({ + ModalBottomSheet = Roact.createElement(ModalBottomSheet, { + onDismiss = function() end, + screenWidth = 1000, + buttonModels = { + { + icon = Images["component_assets/circle_17"], + text = "someSampleText", + renderRightElement = function() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }) + end, + }, + }, + }), + }) + + local folder = Instance.new("Folder") + local instance = Roact.mount(element, folder) + + Roact.unmount(instance) + folder:Destroy() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheetButton.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheetButton.lua new file mode 100644 index 0000000..2a1bc53 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/ModalBottomSheetButton.lua @@ -0,0 +1,202 @@ +local Packages = script.Parent.Parent.Parent + +local Roact = require(Packages.Roact) +local t = require(Packages.t) +local withStyle = require(Packages.UIBlox.Core.Style.withStyle) +local ImageSetComponent = require(Packages.UIBlox.Core.ImageSet.ImageSetComponent) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local ModalBottomSheetButton = Roact.PureComponent:extend("ModalBottomSheetButton") +local imageSize = Images["component_assets/circle_17"].ImageRectSize +local imageOffset = Images["component_assets/circle_17"].ImageRectOffset + +local xOffset = 8 * Images.ImagesResolutionScale +local yOffset = 8 * Images.ImagesResolutionScale +local imageCenter = Rect.new(xOffset, yOffset, imageSize.x - xOffset, imageSize.y - yOffset) + +-- https://share.goabstract.com/cfe90baa-ab79-4f34-ad1b-3ef389d39da4 +local WIDTH_FIXED = 300 +local WIDTH_MARGIN = 16 +local WIDTH_INNER_MARGIN = 24 +local WIDTH_INNER_MARGIN_ICON = 12 + +local validateProps = t.strictInterface({ + icon = t.optional(t.union(t.table, t.string)), + text = t.optional(t.string), + onActivated = t.optional(t.callback), + renderRightElement = t.optional(t.callback), + + showImage = t.boolean, + isFixed = t.boolean, + onActivatedAndDismissed = t.callback, + elementHeight = t.integer, + hasRoundBottom = t.boolean, + hasRoundTop = t.boolean, + LayoutOrder = t.integer, + stayOnActivated = t.optional(t.boolean), +}) + +ModalBottomSheetButton.defaultProps = { + icon = {}, + text = "", + onActivated = function() end, +} + +function ModalBottomSheetButton:init() + self.ref = Roact.createRef() + self.onColorChange = function(styledColor) + if not self.ref.current then return end + self.ref.current.ImageColor3 = styledColor + end + -- TODO(UIBLOX-30): Update with Controllable.lua + self.onInputBegan = function(inputObject) + return inputObject.UserInputType == Enum.UserInputType.MouseButton1 or + inputObject.UserInputType == Enum.UserInputType.Touch + end + self.onInputEnd = function(inputObject) + return inputObject.UserInputType == Enum.UserInputType.MouseButton1 or + inputObject.UserInputType == Enum.UserInputType.Touch + end +end + +function ModalBottomSheetButton:render() + assert(validateProps(self.props)) + local hasRoundTop = self.props.hasRoundTop + local hasRoundBottom = self.props.hasRoundBottom + + local ImageRectSize + local ImageRectOffset + local SliceCenter + + local s100 = imageSize.X + local s50 = s100 / 2 + + -- we are slicing around a 1x1 pixel in the center + if hasRoundTop and hasRoundBottom then + ImageRectSize = imageSize + ImageRectOffset = imageOffset + SliceCenter = imageCenter + elseif hasRoundTop then + ImageRectSize = Vector2.new(s100, s50) + ImageRectOffset = imageOffset + SliceCenter = Rect.new(s50-1, s50-1, s50+1, s50) + elseif hasRoundBottom then + ImageRectSize = Vector2.new(s100, s50) + ImageRectOffset = imageOffset + Vector2.new(0, s50) + SliceCenter = Rect.new(s50-1, 0, s50+1, 1) + else + ImageRectSize = Vector2.new(1, 1) + ImageRectOffset = imageOffset + Vector2.new(s50, s50) + end + + local elementHeight = self.props.elementHeight + -- Width is dependant on parent width + local buttonSize + if self.props.isFixed then + buttonSize = UDim2.new(0, WIDTH_FIXED, 0, elementHeight) + else + buttonSize = UDim2.new(1, -WIDTH_MARGIN*2, 0, elementHeight) + end + + local padding = WIDTH_INNER_MARGIN + if self.props.showImage or self.props.renderRightElement then + padding = WIDTH_INNER_MARGIN_ICON + end + + local textWidthOffset = padding + local iconSize = elementHeight * 0.8 + if self.props.showImage then + textWidthOffset = textWidthOffset + iconSize + padding + end + if self.props.renderRightElement then + textWidthOffset = textWidthOffset + iconSize + padding + end + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local font = stylePalette.Font + local transparency = theme.BackgroundUIDefault.Transparency + local textColor = theme.TextEmphasis.Color + + return Roact.createElement("ImageButton", { + AutoButtonColor = false, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = SliceCenter, + Image = Images["component_assets/circle_17"].Image, + ImageColor3 = theme.BackgroundUIDefault.Color, + ImageRectSize = ImageRectSize, + ImageRectOffset = ImageRectOffset, + ImageTransparency = transparency, + Size = buttonSize, + LayoutOrder = self.props.LayoutOrder, + [Roact.Ref] = self.ref, + [Roact.Event.Activated] = self.props.onActivatedAndDismissed, + [Roact.Event.InputBegan] = function(_, inputObject) + if self.onInputBegan(inputObject) then + self.onColorChange(theme.BackgroundOnPress.Color) + end + end, + [Roact.Event.InputEnded] = function (_, inputObject) + if self.onInputEnd(inputObject) then + self.onColorChange(theme.BackgroundUIDefault.Color) + end + end, + }, { + buttonContents = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + horizontalLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, padding), + }), + padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, padding), + PaddingTop = UDim.new(0.1, 0), + PaddingBottom = UDim.new(0.1, 0), + }), + icon = self.props.showImage and Roact.createElement(ImageSetComponent.Label, { + Image = self.props.icon, + ImageColor3 = textColor, + ImageTransparency = transparency, + BackgroundTransparency = 1, + Size = UDim2.new(0, iconSize, 0, iconSize), + LayoutOrder = 1, + }) or nil, + textLabel = Roact.createElement("TextLabel", { + TextXAlignment = Enum.TextXAlignment.Left, + BackgroundTransparency = 1, + Size = UDim2.new(1, -textWidthOffset, 1, 0), + Text = self.props.text, + TextTransparency = transparency, + Font = font.Header2.Font, + TextColor3 = textColor, + TextSize = font.Header2.RelativeSize * font.BaseSize, + TextTruncate = Enum.TextTruncate.AtEnd, + LayoutOrder = 2, + }), + rightContainer = self.props.renderRightElement and Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, iconSize, 0, iconSize), + LayoutOrder = 3, + }, { + hint = self.props.renderRightElement(), + }) + }), + bottomBorder = not hasRoundBottom and Roact.createElement("Frame", { + LayoutOrder = 0, + BackgroundTransparency = 1, + BackgroundColor3 = theme.Divider.Color, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, 1), + AnchorPoint = Vector2.new(0, 1), + Position = UDim2.new(0, 0, 1, 0), + }) + }) + end) +end + +return ModalBottomSheetButton \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option1.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option1.story.lua new file mode 100644 index 0000000..7241c8f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option1.story.lua @@ -0,0 +1,100 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local ModalBottomSheet = require(script.Parent.Parent.ModalBottomSheet) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local doSomething = function(a) + print(a, "was pressed!") +end + +local dummyModalButtons1 = { + { + icon = Images["component_assets/circle_17"], + text = "option 1", + onActivated = doSomething, + }, +} + +local function withStyle(tree) + return Roact.createElement(StoryView, {}, { + oneChild = tree, + }) +end + +local function mountWithStyle(tree, target, name) + local styledTree = withStyle(tree) + + return Roact.mount(styledTree, target, name) +end + +local overlayComponent = Roact.Component:extend("overlayComponent") + +function overlayComponent:render() + local ModalContainer = self.props.ModalContainer + local showModal = self.state.showModal + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + Layout = Roact.createElement("UIListLayout", { + + }), + TestButton1 = Roact.createElement("TextButton", { + Text = "Spawn 1 Choice", + Size = UDim2.new(1, 0, 0.2, 0), + BackgroundColor3 = Color3.fromRGB(222, 0, 0), + AutoButtonColor = false, + [Roact.Event.Activated] = function() + self:setState({ + showModal = true, + }) + end + }), + modal = showModal and Roact.createElement(Roact.Portal, { + target = ModalContainer, + }, { + sheet = Roact.createElement(ModalBottomSheet, { + bottomGap = 10, + screenWidth = self.props.width, + onDismiss = function() + self:setState({ + showModal = false, + }) + end, + buttonModels = dummyModalButtons1, + }) + }) + }) +end + +return function(target) + local ModalContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ZIndex = 10, + }) + + Roact.mount(ModalContainer, target, "ModalContainer") + + local handle = mountWithStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = 0, + }), target, "preview") + + local connection = target:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + local tree = withStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = target.AbsoluteSize.X, + })) + + Roact.update(handle, tree) + end) + + return function() + connection:Disconnect() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option2.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option2.story.lua new file mode 100644 index 0000000..01dda98 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option2.story.lua @@ -0,0 +1,106 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local ModalBottomSheet = require(script.Parent.Parent.ModalBottomSheet) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local doSomething = function(a) + print(a, "was pressed!") +end + +local dummyModalButtons2 = { + { + icon = Images["component_assets/circle_17"], + text = "option 1, with no images", + onActivated = doSomething, + }, + { + text = "option 2, with no images", + onActivated = doSomething, + }, +} + +local function withStyle(tree) + return Roact.createElement(StoryView, {}, { + oneChild = tree, + }) +end + +local function mountWithStyle(tree, target, name) + local styledTree = withStyle(tree) + + return Roact.mount(styledTree, target, name) +end + +local overlayComponent = Roact.Component:extend("overlayComponent") + +function overlayComponent:render() + local ModalContainer = self.props.ModalContainer + local showModal = self.state.showModal + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + Layout = Roact.createElement("UIListLayout", { + + }), + TestButton1 = Roact.createElement("TextButton", { + Text = "Spawn 2 Choice", + Size = UDim2.new(1, 0, 0.2, 0), + BackgroundColor3 = Color3.fromRGB(22, 22, 222), + AutoButtonColor = false, + [Roact.Event.Activated] = function() + self:setState({ + showModal = true, + }) + end + }), + modal = showModal and Roact.createElement(Roact.Portal, { + target = ModalContainer, + }, { + sheet = Roact.createElement(ModalBottomSheet, { + bottomGap = 10, + screenWidth = self.props.width, + onDismiss = function() + self:setState({ + showModal = false, + }) + end, + showImages = false, + buttonModels = dummyModalButtons2, + }) + }) + }) +end + +return function(target) + local ModalContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ZIndex = 10, + }) + + Roact.mount(ModalContainer, target, "ModalContainer") + + local handle = mountWithStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = 0, + }), target, "preview") + + local connection = target:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + local tree = withStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = target.AbsoluteSize.X, + })) + + Roact.update(handle, tree) + end) + + return function() + connection:Disconnect() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option9.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option9.story.lua new file mode 100644 index 0000000..c09d6f5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Option9.story.lua @@ -0,0 +1,141 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local ModalBottomSheet = require(script.Parent.Parent.ModalBottomSheet) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local doSomething = function(a) + print(a, "was pressed!") +end + +local dummyModalButtons9 = { + { + icon = Images["component_assets/circle_17"], + text = "option 1", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 2", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 3", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 4", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 5", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 6", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 7", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 8", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 9", + onActivated = doSomething, + }, +} + +local function withStyle(tree) + return Roact.createElement(StoryView, {}, { + oneChild = tree, + }) +end + +local function mountWithStyle(tree, target, name) + local styledTree = withStyle(tree) + + return Roact.mount(styledTree, target, name) +end + +local overlayComponent = Roact.Component:extend("overlayComponent") + +function overlayComponent:render() + local ModalContainer = self.props.ModalContainer + local showModal = self.state.showModal + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + Layout = Roact.createElement("UIListLayout", { + + }), + TestButton1 = Roact.createElement("TextButton", { + Text = "Spawn 9 Choice", + Size = UDim2.new(1, 0, 0.2, 0), + BackgroundColor3 = Color3.fromRGB(0, 111, 0), + AutoButtonColor = false, + [Roact.Event.Activated] = function() + self:setState({ + showModal = true, + }) + end + }), + modal = showModal and Roact.createElement(Roact.Portal, { + target = ModalContainer, + }, { + sheet = Roact.createElement(ModalBottomSheet, { + bottomGap = 10, + screenWidth = self.props.width, + onDismiss = function() + self:setState({ + showModal = false, + }) + end, + buttonModels = dummyModalButtons9, + }) + }) + }) +end + +return function(target) + local ModalContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ZIndex = 10, + }) + + Roact.mount(ModalContainer, target, "ModalContainer") + + local handle = mountWithStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = 0, + }), target, "preview") + + local connection = target:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + local tree = withStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = target.AbsoluteSize.X, + })) + + Roact.update(handle, tree) + end) + + return function() + connection:Disconnect() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/RenderHint.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/RenderHint.story.lua new file mode 100644 index 0000000..6f5cea1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/RenderHint.story.lua @@ -0,0 +1,118 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local ModalBottomSheet = require(script.Parent.Parent.ModalBottomSheet) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local doSomething = function(a) + print(a, "was pressed!") +end + +local renderDummyHint = function() + return Roact.createElement("TextLabel", { + BackgroundTransparency = 0, + BorderSizePixel = 0, + BackgroundColor3 = Color3.new(255, 255, 255), + Text = "R", + TextColor3 = Color3.fromRGB(0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + }) +end + +local dummyModalButtons1 = { + { + icon = Images["component_assets/circle_17"], + text = "option 1", + onActivated = doSomething, + renderRightElement = renderDummyHint, + }, + { + icon = Images["component_assets/circle_17"], + text = "longer option that will be truncated", + onActivated = doSomething, + renderRightElement = renderDummyHint, + }, +} + +local function withStyle(tree) + return Roact.createElement(StoryView, {}, { + oneChild = tree, + }) +end + +local function mountWithStyle(tree, target, name) + local styledTree = withStyle(tree) + + return Roact.mount(styledTree, target, name) +end + +local overlayComponent = Roact.Component:extend("overlayComponent") + +function overlayComponent:render() + local ModalContainer = self.props.ModalContainer + local showModal = self.state.showModal + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + Layout = Roact.createElement("UIListLayout", { + + }), + TestButton1 = Roact.createElement("TextButton", { + Text = "Spawn 2 Choice", + Size = UDim2.new(1,0,0.2,0), + BackgroundColor3 = Color3.fromRGB(222,0,0), + AutoButtonColor = false, + [Roact.Event.Activated] = function() + self:setState({ + showModal = true, + }) + end + }), + modal = showModal and Roact.createElement(Roact.Portal, { + target = ModalContainer, + }, { + sheet = Roact.createElement(ModalBottomSheet, { + bottomGap = 10, + screenWidth = self.props.width, + onDismiss = function() + self:setState({ + showModal = false, + }) + end, + buttonModels = dummyModalButtons1, + }) + }) + }) +end + +return function(target) + local ModalContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ZIndex = 10, + }) + + Roact.mount(ModalContainer, target, "ModalContainer") + + local handle = mountWithStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = 0, + }), target, "preview") + + local connection = target:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + local tree = withStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = target.AbsoluteSize.X, + })) + + Roact.update(handle, tree) + end) + + return function() + connection:Disconnect() + Roact.unmount(handle) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Repositioned.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Repositioned.story.lua new file mode 100644 index 0000000..02641e0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/Repositioned.story.lua @@ -0,0 +1,123 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local ModalBottomSheet = require(script.Parent.Parent.ModalBottomSheet) +local Images = require(Packages.UIBlox.App.ImageSet.Images) + +local doSomething = function(a) + print(a, "was pressed!") +end + +local dummyModalButtons5 = { + { + icon = Images["component_assets/circle_17"], + text = "option 1", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 2", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 3", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 4", + onActivated = doSomething, + }, + { + icon = Images["component_assets/circle_17"], + text = "option 5", + onActivated = doSomething, + }, +} + +local function withStyle(tree) + return Roact.createElement(StoryView, {}, { + oneChild = tree, + }) +end + +local function mountWithStyle(tree, target, name) + local styledTree = withStyle(tree) + + return Roact.mount(styledTree, target, name) +end + +local overlayComponent = Roact.Component:extend("overlayComponent") + +function overlayComponent:render() + local ModalContainer = self.props.ModalContainer + local showModal = self.state.showModal + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + Layout = Roact.createElement("UIListLayout", { + + }), + TestButton1 = Roact.createElement("TextButton", { + Text = "Spawn repositioned Choice", + Size = UDim2.new(1, 0, 0.2, 0), + BackgroundColor3 = Color3.fromRGB(0, 111, 0), + AutoButtonColor = false, + [Roact.Event.Activated] = function() + self:setState({ + showModal = true, + }) + end + }), + modal = showModal and Roact.createElement(Roact.Portal, { + target = ModalContainer, + }, { + sheet = Roact.createElement(ModalBottomSheet, { + bottomGap = 10, + screenWidth = self.props.width, + onDismiss = function() + self:setState({ + showModal = false, + }) + end, + buttonModels = dummyModalButtons5, + sheetContentXSize = UDim.new(0.5, 0), + sheetContentXPosition = UDim.new(0.5, 0), + }) + }) + }) +end + +return function(target) + local ModalContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ZIndex = 10, + }) + + Roact.mount(ModalContainer, target, "ModalContainer") + + local handle = mountWithStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = 0, + }), target, "preview") + + local connection = target:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + local tree = withStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = target.AbsoluteSize.X, + })) + + Roact.reconcile(handle, tree) + end) + + return function() + connection:Disconnect() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/StayOnActivated.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/StayOnActivated.story.lua new file mode 100644 index 0000000..85a6ab3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/ModalBottomSheet/__stories__/StayOnActivated.story.lua @@ -0,0 +1,106 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local StoryView = require(ReplicatedStorage.Packages.StoryComponents.StoryView) + +local Packages = script.Parent.Parent.Parent.Parent +local Roact = require(Packages.Roact) +local ModalBottomSheet = require(script.Parent.Parent.ModalBottomSheet) + +local doSomething = function(a) + print(a, "was pressed!") +end + +local dummyModalButtons2 = { + { + text = "Stay on click", + onActivated = doSomething, + stayOnActivated = true, + }, + { + text = "Dismiss on click", + onActivated = doSomething, + stayOnActivated = false, + }, +} + +local function withStyle(tree) + return Roact.createElement(StoryView, {}, { + oneChild = tree, + }) +end + +local function mountWithStyle(tree, target, name) + local styledTree = withStyle(tree) + + return Roact.mount(styledTree, target, name) +end + +local overlayComponent = Roact.Component:extend("overlayComponent") + +function overlayComponent:render() + local ModalContainer = self.props.ModalContainer + local showModal = self.state.showModal + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + Layout = Roact.createElement("UIListLayout", { + + }), + TestButton1 = Roact.createElement("TextButton", { + Text = "Spawn Choices", + Size = UDim2.new(1, 0, 0.2, 0), + BackgroundColor3 = Color3.fromRGB(22, 22, 222), + AutoButtonColor = false, + [Roact.Event.Activated] = function() + self:setState({ + showModal = true, + }) + end + }), + modal = showModal and Roact.createElement(Roact.Portal, { + target = ModalContainer, + }, { + sheet = Roact.createElement(ModalBottomSheet, { + bottomGap = 10, + screenWidth = self.props.width, + onDismiss = function() + self:setState({ + showModal = false, + }) + end, + showImages = false, + buttonModels = dummyModalButtons2, + }) + }) + }) +end + +return function(target) + local ModalContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ZIndex = 10, + }) + + Roact.mount(ModalContainer, target, "ModalContainer") + + local handle = mountWithStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = 0, + }), target, "preview") + + local connection = target:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + local tree = withStyle(Roact.createElement(overlayComponent, { + ModalContainer = target:FindFirstChild("ModalContainer"), + width = target.AbsoluteSize.X, + })) + + Roact.update(handle, tree) + end) + + return function() + connection:Disconnect() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/StateTable/StateTable.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/StateTable/StateTable.lua new file mode 100644 index 0000000..8efc88c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/StateTable/StateTable.lua @@ -0,0 +1,195 @@ +local UIBloxRoot = script.Parent.Parent +local Cryo = require(UIBloxRoot.Parent.Cryo) + +local StateTable = {} + +StateTable.__index = StateTable + +local function validateStateTableItem(item, qualifier) + local type = typeof(item) + local isValid = type == "string" or type == "userdata" + assert(isValid, string.format("%s '%s' must be a string or userdata but is a %s", qualifier, tostring(item), type)) +end + +--[[ + This class method creates a new StateTable instance that you can use to control complicated + logic that is based upon your state machine design. Ex: + + self.stateTable = StateTable.new(name, initialState, initialContext, { + InitialState = { + EventName1 = { nextState = "StateOne", action = self.actionDoSomething }, + EventName2 = { nextState = "FinalState" } -- actions are optional + }, + StateOne = { + EventName1 = { nextState = "FinalState", action = self.actionDoSomethingAtLast }, + EventName3 = { action = self.actionDoSomethingElse } -- will maintain current state + }, + FinalState = {} -- transitions are optional + }) + + Arguments: + name - A debug name for this StateTable. (String) + initialState - Name of the beginning state for this StateTable. (String) + initialContext - A reference to an existing table where you hold all sidecar contextual data + that needs to be manipulated by this StateTable's actions. (Table) + transitionTable - Description of the state machine structure. (Table) + + The outermost keys in "transitionTable" represent individual states in your design, each of which + contains a description of the events that can be called while in that state. Calling an event + triggers a transition to a new state while also (optionally) running an action functor. + + (All states and events must be simple strings or userdata.) + (If using userdata for states and events, implement a tostring metamethod for ease of debugging.) + + Named events in your state table will be converted into functions that you can call directly. + Calling these event functions will transition the StateTable to the appropriate nextState and + call the registered action handler, if any. You may pass arguments to your actions through the + event function by passing them as a table. Ex: + + self.stateTable.events.EventName1(args) + + The combination of named states and events in StateTable make up the control flow portion of your + state machine. To run business logic, you need to implement Actions. + + Each action functor accepts four arguments: the current state, the next state, event arguments, + and the contextual data table that you passed in when you called the event function. Your action + functor should return a table containing the keys that need to be updated from currentContext. + The returned table will be merged into currentContext and passed back to your onStateChange + callback. Ex: + + function actionDoSomething(currentState, nextState, args, currentContext) + local contextDiff = doSomething(args, currentContext) + return contextDiff + end + + Do NOT update your own copy of the StateTable's internal state variable or your context in actions. + If this is an action-less transition, you'll fail to update it! + + To update your context and own tracking of the current state at the same time, listen to changes + via StateTable:onStateChange. See the documentation of that method for more details. +]] +function StateTable.new(name, initialState, initialContext, transitionTable) + assert(typeof(name) == "string", "name must be a string") + assert(#name > 0, "name must not be an empty string") + + validateStateTableItem(initialState, "initialState") + assert(initialContext == nil or typeof(initialContext) == "table", "initialContext must be a table or nil") + + assert(typeof(transitionTable) == "table", "transitionTable must be a table") + assert(typeof(transitionTable[initialState]) == "table", "initialState must be present in transitionTable") + + local self = {} + setmetatable(self, StateTable) + + self.name = name + self.currentState = initialState + self.currentContext = initialContext or {} + self.transitionTable = {} + self.events = {} + + for state, eventTable in pairs(transitionTable) do + validateStateTableItem(state, "state") + assert(typeof(eventTable) == "table", string.format("state '%s' must map to a table", tostring(state))) + + local parsedEventTable = {} + for event, eventData in pairs(eventTable) do + validateStateTableItem(event, "event") + assert(typeof(eventData) == "table", string.format("event '%s' must map to a table", tostring(event))) + + local nextState = eventData.nextState + local action = eventData.action + + if nextState ~= nil then + validateStateTableItem(nextState, "nextState") + + -- Check that the transition lands on a known state + assert(transitionTable[nextState] ~= nil, + string.format("nextState '%s' does not exist in transitionTable", tostring(nextState))) + end + + assert(action == nil or typeof(action) == "function", "action must be a function") + + parsedEventTable[event] = eventData + + -- Create a function to make it easy to call this event + if self.events[event] == nil then + self.events[event] = function(args) + return self:handleEvent(event, args) + end + end + end + + self.transitionTable[state] = parsedEventTable + end + + -- catch calls to invalid events earlier + setmetatable(self.events, { + __index = function(_, event) + error(string.format("'%s' is not a valid event in StateTable '%s'", + tostring(event), self.name), 2) + end + }) + + return self +end + +--[[ + It is recommended that you use the auto-generated event functions instead + of calling this method; see StateTable.new. + + Process an event through this StateTable instance. Pass in the name + of the event, and optional arguments. The arguments will be passed to the + registered action handler for the state/event transition, if any. + + This function does not return anything. Listen to changes using + StateTable:onStateChange if you need to store the current state or use the + results of an action. +]] +function StateTable:handleEvent(event, args) + validateStateTableItem(event, "event") + assert(args == nil or typeof(args) == "table", "args must be nil or valid table") + + local currentState = self.currentState + local eventMap = self.transitionTable[currentState] + + assert(eventMap ~= nil, "no transition events for current state") + + if eventMap[event] ~= nil then + local eventData = eventMap[event] + local nextState = eventData.nextState or currentState + local action = eventData.action + + local updatedContext = self.currentContext + if action ~= nil then + local contextDiff = action(currentState, nextState, args, self.currentContext) or {} + updatedContext = Cryo.Dictionary.join(self.currentContext, contextDiff) + self.currentContext = updatedContext + end + + self.currentState = nextState + + if self.stateChangeHandler ~= nil then + self.stateChangeHandler(currentState, nextState, updatedContext) + end + end +end + +--[[ + Register a function to process changes in state. Your function should have + the following signature and return nothing: + + function handleStateChange(oldState, newState, updatedContext) + self.currentState = newState + self.currentContext = updatedContext + end + + The updatedContext parameter contains the table that was returned by the action + handler associated with the event transition. +]] +function StateTable:onStateChange(stateChangeHandler) + assert(stateChangeHandler == nil or typeof(stateChangeHandler) == "function", + "stateChangeHandler must be nil or a function") + self.stateChangeHandler = stateChangeHandler +end + +return StateTable diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/StateTable/StateTable.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/StateTable/StateTable.spec.lua new file mode 100644 index 0000000..df751d9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/StateTable/StateTable.spec.lua @@ -0,0 +1,525 @@ +return function() + local StateTable = require(script.Parent.StateTable) + + local TEST_NAME = "test_state_table_name" + local TEST_INITIAL_STATE = "Initial" + local TEST_DATA = { foo = 1 } + + local DUMMY_ACTION = function(_, _, data) + return data + end + + local function FieldCount(t) + local fieldCount = 0 + for _ in pairs(t) do + fieldCount = fieldCount + 1 + end + return fieldCount + end + + local function ShallowEqual(A, B) + if not A or not B then + return false + elseif A == B then + return true + end + + for key, value in pairs(A) do + if B[key] ~= value then + return false + end + end + for key, value in pairs(B) do + if A[key] ~= value then + return false + end + end + + return true + end + + describe("StateTable.new validation throw tests", function() + it("should throw if name is nil", function() + expect(function() + StateTable.new(nil, TEST_INITIAL_STATE, {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if name is not a string", function() + expect(function() + StateTable.new(5, TEST_INITIAL_STATE, {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if name is empty", function() + expect(function() + StateTable.new("", TEST_INITIAL_STATE, {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if initialState is nil", function() + expect(function() + StateTable.new(TEST_NAME, nil, {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if initialState is not a string", function() + expect(function() + StateTable.new(TEST_NAME, 5, {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if initialState is empty", function() + expect(function() + StateTable.new(TEST_NAME, "", {}, { Initial = {} }) + end).to.throw() + end) + + it("should throw if initialContext is wrong type", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, 5, { Initial = {} }) + end).to.throw() + end) + + it("should throw if transitionTable is nil", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, nil) + end).to.throw() + end) + + it("should throw if empty transitionTable is provided", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, {}) + end).to.throw() + end) + + it("should throw if non-table transitionTable is provided", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, 1) + end).to.throw() + end) + + it("should throw if no arguments are provided", function() + expect(function() + StateTable.new() + end).to.throw() + end) + + it("should throw if non-string/userdata used for state name", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { [1] = {} }) + end).to.throw() + end) + + it("should throw if non-table is used for state table", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = 1 }) + end).to.throw() + end) + + it("should throw if non-string/userdata is used for event name", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { [1] = {} } }) + end).to.throw() + end) + + it("should throw if non-table is used for event data table", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = 1 } }) + end).to.throw() + end) + + it("should throw if non-string/userdata used for nextState", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { nextState = 1 } } }) + end).to.throw() + end) + + it("should throw if non-function used for action", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { action = 1 } } }) + end).to.throw() + end) + + it("should throw if initial state not in transitionTable", function() + expect(function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { NotInitial = {} }) + end).to.throw() + end) + end) + + describe("StateTable.new validation success tests", function() + it("should not throw if empty event table is provided", function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} }) + end) + + it("should not throw if empty event data table is provided", function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = {} } }) + end) + + it("should not throw if action is missing", function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Next" } + }, + Next = {} + }) + end) + + it("should not throw if nextState is missing", function() + StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { action = function() end } } }) + end) + end) + + describe("StateTable.new event function creation tests", function() + it("should create an event function for a single event", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { } } }) + expect(FieldCount(st.events)).to.equal(1) + expect(typeof(st.events.Event1)).to.equal("function") + end) + + it("should create different event functions for multiple events", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { }, Event2 = { } } }) + expect(FieldCount(st.events)).to.equal(2) + expect(typeof(st.events.Event1)).to.equal("function") + expect(typeof(st.events.Event2)).to.equal("function") + end) + + it("should not create event functions without at least one event", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} }) + expect(FieldCount(st.events)).to.equal(0) + end) + end) + + describe("StateTable onStateChange registration tests", function() + it("should assert when non-function provided", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} }) + expect(function() + st:onStateChange(5) + end).to.throw() + end) + + it("should not assert when function provided", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} }) + st:onStateChange(function() end) + end) + + it("should not assert when nil provided", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} }) + st:onStateChange(nil) + end) + + it("should call onStateChange handler when transition occurs", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = {} + } + }) + + local called = false + st:onStateChange(function() + called = true + end) + + st.events.Event1(nil) + expect(called).to.equal(true) + end) + + it("should not call onStateChange handler after it has been de-registered", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = {} + } + }) + + local called = false + st:onStateChange(function() + called = true + end) + + st:onStateChange(nil) + + st.events.Event1(nil) + expect(called).to.equal(false) + end) + end) + + describe("StateTable event call tests", function() + it("should change state when handleEvent is called", function() + local action1Called = false + local testAction1 = function() action1Called = true end + + local action2Called = false + local testAction2 = function() action2Called = true end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two", action = testAction1 } + }, + Two = { + Event2 = { action = testAction2 } + } + }) + + st:handleEvent("Event1", nil) + expect(action1Called).to.equal(true) + expect(action2Called).to.equal(false) + + action1Called = false + action2Called = false + + st:handleEvent("Event2", nil) + expect(action1Called).to.equal(false) + expect(action2Called).to.equal(true) + end) + + it("should call mapped action when handleEvent is called", function() + local actionOldState, actionNewState, actionData + local testAction = function(oldState, newState, data) + actionOldState = oldState + actionNewState = newState + actionData = data + end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two", action = testAction } + }, + Two = {} + }) + + st:handleEvent("Event1", TEST_DATA) + expect(actionOldState).to.equal("Initial") + expect(actionNewState).to.equal("Two") + expect(ShallowEqual(actionData, TEST_DATA)).to.equal(true) + end) + + it("should return expected nextState and data in onStateChange callback when handleEvent is called", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two", action = DUMMY_ACTION } + }, + Two = {} + }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st:handleEvent("Event1", TEST_DATA) + + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Two") + expect(ShallowEqual(updatedContext, TEST_DATA)).to.equal(true) + end) + + it("should call mapped action when event functor is called", function() + local actionOldState, actionNewState, actionData + local testAction = function(oldState, newState, data) + actionOldState = oldState + actionNewState = newState + actionData = data + end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two", action = testAction } + }, + Two = {} + }) + + st.events.Event1(TEST_DATA) + expect(actionOldState).to.equal("Initial") + expect(actionNewState).to.equal("Two") + expect(ShallowEqual(actionData, TEST_DATA)).to.equal(true) + end) + + it("should return expected nextState and data in onStateChange callback when event functor is called", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two", action = DUMMY_ACTION } + }, + Two = {} + }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st.events.Event1(TEST_DATA) + + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Two") + expect(ShallowEqual(updatedContext, TEST_DATA)).to.equal(true) + end) + + it("should still return nextState in onStateChange callback when no action handler is provided", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { nextState = "Two" } + }, + Two = {} + }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st.events.Event1(TEST_DATA) + + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Two") + expect(typeof(updatedContext)).to.equal("table") + expect(FieldCount(updatedContext)).to.equal(0) + end) + + it("should return current state and empty data if new state and action handler are not specified", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { } } }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st.events.Event1(TEST_DATA) + + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Initial") + expect(FieldCount(updatedContext)).to.equal(0) + end) + + it("should return current state and matching data if only action handler is provided", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { action = DUMMY_ACTION } } }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st.events.Event1(TEST_DATA) + + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Initial") + expect(ShallowEqual(updatedContext, TEST_DATA)).to.equal(true) + end) + + it("should return empty data in onStateChange callback when nil data is provided to no-action event", function() + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = {} } }) + + local updatedContext + st:onStateChange(function(_, _, uc) + updatedContext = uc + end) + + st.events.Event1(nil) + + expect(FieldCount(updatedContext)).to.equal(0) + end) + + it("should merge context updates with old context", function() + local initialContext = { foo = 1 } + + local function action1() + return { bar = 2 } + end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, initialContext, { + Initial = { + Event1 = { action = action1 } + } + }) + + local updatedContext + st:onStateChange(function(_, _, uc) + updatedContext = uc + end) + + st.events.Event1() + + expect(ShallowEqual(updatedContext, { foo = 1, bar = 2 })).to.equal(true) + end) + + it("should pass args to actions", function() + local passedArgs + local function action1(_, _, args) + passedArgs = args + end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { Event1 = { action = action1 } } + }) + + local theArgs = { argsAreHere = true } + st.events.Event1(theArgs) + + expect(ShallowEqual(theArgs, passedArgs)).to.equal(true) + end) + + it("should call actions independently for different events", function() + local testData1 = TEST_DATA + local testData2 = { foo = 2 } + + local action1OldState, action1NewState, action1Data + local testAction1 = function(oldState, newState, data) + action1OldState = oldState + action1NewState = newState + action1Data = data + return data + end + + local action2OldState, action2NewState, action2Data + local testAction2 = function(oldState, newState, data) + action2OldState = oldState + action2NewState = newState + action2Data = data + return data + end + + local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { + Initial = { + Event1 = { action = testAction1 }, + Event2 = { nextState = "Two", action = testAction2 }, + }, + Two = {} + }) + + local oldState, newState, updatedContext + st:onStateChange(function(os, ns, uc) + oldState = os + newState = ns + updatedContext = uc + end) + + st.events.Event1(testData1) + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Initial") + expect(ShallowEqual(updatedContext, testData1)).to.equal(true) + + st.events.Event2(testData2) + expect(oldState).to.equal("Initial") + expect(newState).to.equal("Two") + expect(ShallowEqual(updatedContext, testData2)).to.equal(true) + + expect(action1OldState).to.equal("Initial") + expect(action2OldState).to.equal("Initial") + expect(action1NewState).to.equal("Initial") + expect(action2NewState).to.equal("Two") + + expect(ShallowEqual(action1Data, testData1)).to.equal(true) + expect(ShallowEqual(action2Data, testData2)).to.equal(true) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/AppStyle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/AppStyle.lua new file mode 100644 index 0000000..0685b54 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/AppStyle.lua @@ -0,0 +1,22 @@ +local StyleRoot = script.Parent +local UIBloxRoot = StyleRoot.Parent +local createSignal = require(UIBloxRoot.Utility.createSignal) + +local AppStyle = {} + +function AppStyle.new(style) + local self = {} + self.style = style + self.signal = createSignal() + setmetatable(self, { + __index = AppStyle, + }) + return self +end + +function AppStyle:update(newStyle) + self.style = newStyle + self.signal:fire(newStyle) +end + +return AppStyle \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/AppStyle.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/AppStyle.spec.lua new file mode 100644 index 0000000..d6bae7e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/AppStyle.spec.lua @@ -0,0 +1,60 @@ +return function() + local AppStyle = require(script.Parent.AppStyle) + + local testTheme = { + Background1 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background2 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background3 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, + }, + Background4 = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0.3, -- Alpha 0.7 + }, + } + + local testFont = { + Normal = Enum.Font.Gotham, + Title = Enum.Font.GothamBold, + } + + local testAssets = { + ButtonFill9Slice = "buttonFill", + ButtonBorder9Slice = "buttonStroke", + Button9Slice = Rect.new(8, 8, 9, 9), + } + + it("should connect and fire signal for style change without errors", function() + local testStyle = { + Theme = testTheme, + Font = testFont, + Assets = testAssets, + } + local appStyle = AppStyle.new(testStyle) + + expect(appStyle.style).to.be.a("table") + + local testValue = "some test theme" + local testTable = { + Theme = testValue, + } + local disconnect = appStyle.signal:subscribe(function(newValues) + expect(newValues).to.be.a("table") + expect(newValues.Theme).to.equal(testValue) + end) + + appStyle:update(testTable) + + expect(appStyle.style).to.be.a("table") + expect(appStyle.style.Theme).to.equal(testValue) + disconnect() + end) + +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleConsumer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleConsumer.lua new file mode 100644 index 0000000..4f6b681 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleConsumer.lua @@ -0,0 +1,47 @@ +local StyleRoot = script.Parent +local UIBloxRoot = StyleRoot.Parent +local Roact = require(UIBloxRoot.Parent.Roact) +local t = require(UIBloxRoot.Parent.t) + +local StyleConsumer = Roact.Component:extend("StyleConsumer") + +--Note: remove along with styleRefactorConfig +local UIBloxConfig = require(UIBloxRoot.UIBloxConfig) +local styleRefactorConfig = UIBloxConfig.styleRefactorConfig +--- + +local validateProps = t.strictInterface({ + render = t.callback, +}) + +function StyleConsumer:init() + if styleRefactorConfig then + warn("Using deprecated `UIBlox.Style.withStyle`. Please use `UIBlox.Core.Style.withStyle`") + end + + self.appStyle = self._context.AppStyle + local currentStyle = self.appStyle.style + + self.state = { + style = currentStyle, + } +end + +function StyleConsumer:render() + assert(validateProps(self.props)) + return self.props.render(self.state.style) +end + +function StyleConsumer:didMount() + self.disconnectStyleListener = self.appStyle.signal:subscribe(function(newStyle) + self:setState({ + style = newStyle, + }) + end) +end + +function StyleConsumer:willUnmount() + self.disconnectStyleListener() +end + +return StyleConsumer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleConsumer.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleConsumer.spec.lua new file mode 100644 index 0000000..4ea2b0e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleConsumer.spec.lua @@ -0,0 +1,29 @@ +return function() + local StyleRoot = script.Parent + local UIBloxRoot = StyleRoot.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local testStyle = require(StyleRoot.Validator.TestStyle) + local StyleProvider = require(StyleRoot.StyleProvider) + local StyleConsumer = require(StyleRoot.StyleConsumer) + + + it("should create and destroy without errors", function() + local renderFunction = function(style) + expect(style).to.be.a("table") + return Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }) + end + local element = Roact.createElement(StyleProvider, { + style = testStyle, + }, { + StyleConsumer = Roact.createElement(StyleConsumer, { + render = renderFunction + }), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleProvider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleProvider.lua new file mode 100644 index 0000000..c313ba5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleProvider.lua @@ -0,0 +1,43 @@ +local StyleRoot = script.Parent +local UIBloxRoot = StyleRoot.Parent +local Roact = require(UIBloxRoot.Parent.Roact) +local t = require(UIBloxRoot.Parent.t) +local AppStyle = require(StyleRoot.AppStyle) +local validateStyle = require(StyleRoot.Validator.validateStyle) + +local StyleProvider = Roact.Component:extend("StyleProvider") + +--Note: remove along with styleRefactorConfig +local UIBloxConfig = require(UIBloxRoot.UIBloxConfig) +local styleRefactorConfig = UIBloxConfig.styleRefactorConfig +--- + +local validateStyleProviderProps = t.strictInterface({ + -- The current style of the app. + style = validateStyle, + [Roact.Children] = t.table, +}) + +function StyleProvider:init() + if styleRefactorConfig then + warn("Using deprecated `UIBlox.Style.Provider`. Please use `UIBlox.Core.Style.Provider`") + end + + local style = self.props.style + self.appStyle = AppStyle.new(style) + self._context.AppStyle = self.appStyle +end + +function StyleProvider:render() + assert(validateStyleProviderProps(self.props)) + assert(self.props.style ~= nil, "StyleProvider style should not be nil.") + return Roact.oneChild(self.props[Roact.Children]) +end + +function StyleProvider:didUpdate(previousProps) + if self.props.style ~= previousProps.style then + self.appStyle:update(self.props.style) + end +end + +return StyleProvider \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleProvider.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleProvider.spec.lua new file mode 100644 index 0000000..8ea95b7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/StyleProvider.spec.lua @@ -0,0 +1,37 @@ +return function() + local StyleRoot = script.Parent + local UIBloxRoot = StyleRoot.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local testStyle = require(StyleRoot.Validator.TestStyle) + local StyleProvider = require(StyleRoot.StyleProvider) + + it("should create and destroy without errors", function() + local someComponent = Roact.createElement("TextLabel", { + Text = "test", + }) + local styleProvider = Roact.createElement(StyleProvider, { + style = testStyle, + }, { + SomeComponent = someComponent, + }) + + local roactInstance = Roact.mount(styleProvider) + Roact.unmount(roactInstance) + end) + + it("should throw given an invalid style palette", function() + local invalidStyle = {} + local someComponent = Roact.createElement("TextLabel", { + Text = "test", + }) + local styleProvider = Roact.createElement(StyleProvider, { + style = invalidStyle, + }, { + SomeComponent = someComponent, + }) + expect(function() + local roactInstance = Roact.mount(styleProvider) + Roact.unmount(roactInstance) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/TestStyle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/TestStyle.lua new file mode 100644 index 0000000..3d4f7bd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/TestStyle.lua @@ -0,0 +1,68 @@ +local color = { + Color = Color3.fromRGB(0, 0, 0), + Transparency = 0, +} +local testTheme = { + BackgroundDefault = color, + BackgroundContrast = color, + BackgroundMuted = color, + BackgroundUIDefault = color, + BackgroundUIContrast = color, + BackgroundOnHover = color, + BackgroundOnPress = color, + UIDefault = color, + UIMuted = color, + UIEmphasis = color, + ContextualPrimaryDefault = color, + ContextualPrimaryOnHover = color, + ContextualPrimaryContent = color, + SystemPrimaryDefault = color, + SystemPrimaryOnHover = color, + SystemPrimaryContent = color, + SecondaryDefault = color, + SecondaryOnHover = color, + SecondaryContent = color, + IconDefault = color, + IconEmphasis = color, + IconOnHover = color, + TextEmphasis = color, + TextDefault = color, + TextMuted = color, + Divider = color, + Overlay = color, + DropShadow = color, + NavigationBar = color, + PlaceHolder = color, + OnlineStatus = color, + OfflineStatus = color, + Success = color, + Alert = color, + Badge = color, + BadgeContent = color, + SelectionCursor = color, +} + +local font = { + Font = Enum.Font.GothamSemibold, + RelativeSize = 1, + RelativeMinSize = 1, +} +local testFont = { + BaseSize = 10, + Title = font, + Header1 = font, + Header2 = font, + SubHeader1 = font, + Body = font, + CaptionHeader = font, + CaptionSubHeader = font, + CaptionBody = font, + Footer = font, +} + +local testStyle = { + Theme = testTheme, + Font = testFont, +} + +return testStyle \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/TestStyle.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/TestStyle.spec.lua new file mode 100644 index 0000000..92ab26c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/TestStyle.spec.lua @@ -0,0 +1,7 @@ +return function() + local validateStyle = require(script.Parent.validateStyle) + local testStyle = require(script.Parent.TestStyle) + it("Should be valid", function() + assert(validateStyle(testStyle)) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateColorInfo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateColorInfo.lua new file mode 100644 index 0000000..ec3f168 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateColorInfo.lua @@ -0,0 +1,9 @@ +local ValidatorRoot = script.Parent +local StyleRoot = ValidatorRoot.Parent +local UIBloxRoot = StyleRoot.Parent +local t = require(UIBloxRoot.Parent.t) + +return t.strictInterface({ + Color = t.Color3, + Transparency = t.numberConstrained(0, 1), +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateFont.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateFont.lua new file mode 100644 index 0000000..cf91832 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateFont.lua @@ -0,0 +1,21 @@ +local ValidatorRoot = script.Parent +local StyleRoot = ValidatorRoot.Parent +local UIBloxRoot = StyleRoot.Parent +local t = require(UIBloxRoot.Parent.t) + +local Font = require(ValidatorRoot.validateFontInfo) + +local FontPalette = t.strictInterface({ + BaseSize = t.numberMinExclusive(0), + Title = Font, + Header1 = Font, + Header2 = Font, + SubHeader1 = Font, + Body = Font, + CaptionHeader = Font, + CaptionSubHeader = Font, + CaptionBody = Font, + Footer = Font, +}) + +return FontPalette diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateFontInfo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateFontInfo.lua new file mode 100644 index 0000000..7060de4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateFontInfo.lua @@ -0,0 +1,10 @@ +local ValidatorRoot = script.Parent +local StyleRoot = ValidatorRoot.Parent +local UIBloxRoot = StyleRoot.Parent +local t = require(UIBloxRoot.Parent.t) + +return t.strictInterface({ + RelativeSize = t.numberMinExclusive(0), + RelativeMinSize = t.numberMinExclusive(0), + Font = t.EnumItem, +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateStyle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateStyle.lua new file mode 100644 index 0000000..17c9157 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateStyle.lua @@ -0,0 +1,13 @@ +local ValidatorRoot = script.Parent +local StyleRoot = ValidatorRoot.Parent +local UIBloxRoot = StyleRoot.Parent +local t = require(UIBloxRoot.Parent.t) +local validateTheme = require(ValidatorRoot.validateTheme) +local validateFont = require(ValidatorRoot.validateFont) + +local StylePalette = t.strictInterface({ + Theme = validateTheme, + Font = validateFont, +}) + +return StylePalette diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateTheme.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateTheme.lua new file mode 100644 index 0000000..27a9751 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/Validator/validateTheme.lua @@ -0,0 +1,58 @@ +local ValidatorRoot = script.Parent +local StyleRoot = ValidatorRoot.Parent +local UIBloxRoot = StyleRoot.Parent +local t = require(UIBloxRoot.Parent.t) + +local Color = require(ValidatorRoot.validateColorInfo) + +local ThemePalette = t.strictInterface({ + BackgroundDefault = Color, + BackgroundContrast = Color, + BackgroundMuted = Color, + BackgroundUIDefault = Color, + BackgroundUIContrast = Color, + BackgroundOnHover = Color, + BackgroundOnPress = Color, + + UIDefault = Color, + UIMuted = Color, + UIEmphasis = Color, + + ContextualPrimaryDefault = Color, + ContextualPrimaryOnHover = Color, + ContextualPrimaryContent = Color, + + SystemPrimaryDefault = Color, + SystemPrimaryOnHover = Color, + SystemPrimaryContent = Color, + + SecondaryDefault = Color, + SecondaryOnHover = Color, + SecondaryContent = Color, + + IconDefault = Color, + IconEmphasis = Color, + IconOnHover = Color, + + TextEmphasis = Color, + TextDefault = Color, + TextMuted = Color, + + Divider = Color, + Overlay = Color, + DropShadow = Color, + NavigationBar = Color, + PlaceHolder = Color, + + OnlineStatus = Color, + OfflineStatus = Color, + Success = Color, + Alert = Color, + + Badge = Color, + BadgeContent = Color, + + SelectionCursor = Color, +}) + +return ThemePalette diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/withStyle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/withStyle.lua new file mode 100644 index 0000000..f5c3fc7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/withStyle.lua @@ -0,0 +1,13 @@ +local StyleRoot = script.Parent +local UIBloxRoot = StyleRoot.Parent +local Roact = require(UIBloxRoot.Parent.Roact) +local StyleConsumer = require(StyleRoot.StyleConsumer) + +--[[ + This is a utility function that will wrap StyleConsumer. + `renderCallback` will be invoked with the current style. It should return a Roact element. +]] +return function(renderCallback) + assert(type(renderCallback) == "function", "Expect renderCallback to be a function.") + return Roact.createElement(StyleConsumer, { render = renderCallback }) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/withStyle.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/withStyle.spec.lua new file mode 100644 index 0000000..54dcb5b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Style/withStyle.spec.lua @@ -0,0 +1,30 @@ +return function() + local StyleRoot = script.Parent + local UIBloxRoot = StyleRoot.Parent + local Roact = require(UIBloxRoot.Parent.Roact) + local StyleProvider = require(StyleRoot.StyleProvider) + local withStyle = require(StyleRoot.withStyle) + local testStyle = require(StyleRoot.Validator.TestStyle) + + it("should create and destroy without errors", function() + local someTestElement = Roact.Component:extend("someTestElement") + -- luacheck: ignore unused argument self + function someTestElement:render() + return withStyle(function(style) + expect(style).to.be.a("table") + return Roact.createElement("Frame", { + Size = UDim2.new(0, 100, 0, 100), + }) + end) + end + + local element = Roact.createElement(StyleProvider, { + style = testStyle, + }, { + someTestElement = Roact.createElement(someTestElement), + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/UIBloxConfig.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/UIBloxConfig.lua new file mode 100644 index 0000000..c50b312 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/UIBloxConfig.lua @@ -0,0 +1 @@ +return require(script.Parent).Config \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/UIBloxDefaultConfig.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/UIBloxDefaultConfig.lua new file mode 100644 index 0000000..afe20e8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/UIBloxDefaultConfig.lua @@ -0,0 +1,24 @@ +return { + -- enableAlertTitleIconConfig: turning this on allows the Alert component to take + -- in an optional titleIcon prop, which displays an icon above the Alert's title. + enableAlertTitleIconConfig = false, + + --styleRefactorConfig: switch to use the refactored style system from Core and App. + styleRefactorConfig = false, + + -- fixes the premium icon and text placement on item tiles + -- when the flag is false, the second line of title text will align with the start of the text. + -- when the flag is true, the second line will align with the start of the premium icon. + fixItemTilePremiumIcon = false, + + --enableExperimentalGamepadSupport: Enables support of gamepad navigation via the roact-gamepad + -- library. This is currently experimental and not yet ready for release. + enableExperimentalGamepadSupport = false, + + --useNewUICornerRoundedCorners: Uses the new roblox CornerUI Instance instead of mask-based UI corners + useNewUICornerRoundedCorners = false, + + -- genericSliderFilterOldTouchInputs: Filters inputObjects that trigger inputBegan with a + -- non Enum.UserInputState.Begin UserInputState in the GenericSlider component + genericSliderFilterOldTouchInputs = false, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/ExternalEventConnection.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/ExternalEventConnection.lua new file mode 100644 index 0000000..e0b3a30 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/ExternalEventConnection.lua @@ -0,0 +1,51 @@ +--[[ + A component that establishes a connection to a Roblox event when it is rendered. +]] +local GridRoot = script.Parent +local UIBloxRoot = GridRoot.Parent +local Roact = require(UIBloxRoot.Parent.Roact) +local ExternalEventConnection = Roact.Component:extend("ExternalEventConnection") + +function ExternalEventConnection:init() + self.connection = nil +end + +--[[ + Render the child component so that ExternalEventConnections can be nested like so: + + Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputBegan, + callback = inputBeganCallback, + }, { + Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputEnded, + callback = inputChangedCallback, + }) + }) +]] +function ExternalEventConnection:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +function ExternalEventConnection:didMount() + local event = self.props.event + local callback = self.props.callback + + self.connection = event:Connect(callback) +end + +function ExternalEventConnection:didUpdate(oldProps) + if self.props.event ~= oldProps.event or self.props.callback ~= oldProps.callback then + self.connection:Disconnect() + + self.connection = self.props.event:Connect(self.props.callback) + end +end + +function ExternalEventConnection:willUnmount() + self.connection:Disconnect() + + self.connection = nil +end + +return ExternalEventConnection \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.lua new file mode 100644 index 0000000..5aee9ca --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.lua @@ -0,0 +1,164 @@ +--[[ + Creates a Roact component which will automatically animate. + The animations are created with Otter springs. + + Example: + To create a frame whose position animates up / down: + AnimatedFrame = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + regularProps = { + Size = ..., + BackgroundColor3 = ..., + }, + animatedValues = { + positionY = goal, + }, + mapValuesToProps = function(values) + return { + Position = UDim2.new(0, 0, 0, values.positionY), + } + end, + }) + Whenever goal changes, the frame will animate toward the new position. +]] + +local Packages = script.Parent.Parent.Parent + +local Otter = require(Packages.Otter) +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local PropTypes = t.intersection( + t.strictInterface({ + -- Values in this table will be animated with Otter. + animatedValues = t.table, + + -- This function describes how the animated values should be converted to + -- Roblox properties. + mapValuesToProps = t.callback, + + -- Options to pass to Otter's spring configuration + springOptions = t.optional(t.table), + + -- Called when the animation is complete + onComplete = t.optional(t.callback), + + -- Props to pass to the inner component. + regularProps = t.table, + + -- Children passed in by Roact + [Roact.Children] = t.optional(t.table), + }), + function(props) + if props[Roact.Children] ~= nil and props.regularProps[Roact.Children] ~= nil then + return false, "Children must be specified in one place, but the [Roact.Children] key was found" .. + " in both props and props.regularProps on SpringAnimatedItem." + end + + return true + end +) + +local SpringAnimatedItem = {} + +function SpringAnimatedItem.wrap(component) + local AnimatedComponent = Roact.PureComponent:extend(string.format("SpringAnimatedItem(%s)", + tostring(component))) + + AnimatedComponent.defaultProps = { + regularProps = {}, + } + + function AnimatedComponent:init() + self.ref = self.props.regularProps[Roact.Ref] or Roact.createRef() + + self.applyAnimatedValues = function(values) + local rbx = self.ref.current + if rbx == nil then + return + end + + local mapValuesToProps = self.props.mapValuesToProps + + local rbxProps = mapValuesToProps(values) + for key, value in pairs(rbxProps) do + rbx[key] = value + end + end + + self.onComplete = function() + if self.props.onComplete then + self.props.onComplete() + end + end + + self.motor = nil + end + + function AnimatedComponent:didMount() + local animatedValues = self.props.animatedValues + + -- Apply initial values + self.applyAnimatedValues(animatedValues) + + -- Set up motor + self.motor = Otter.createGroupMotor(animatedValues) + self.motor:onStep(function(newValues) + self.applyAnimatedValues(newValues) + end) + self.motor:onComplete(self.onComplete) + self.motor:start() + end + + function AnimatedComponent:willUpdate(newProps) + if self.props.regularProps[Roact.Ref] ~= newProps.regularProps[Roact.Ref] and + newProps.regularProps[Roact.Ref] ~= nil then + self.ref = newProps.regularProps[Roact.Ref] + end + end + + function AnimatedComponent:render() + assert(PropTypes(self.props)) + + local regularProps = self.props.regularProps + local props = Cryo.Dictionary.join(regularProps, { + [Roact.Ref] = self.ref, + [Roact.Children] = self.props[Roact.Children], + }) + + return Roact.createElement(component, props) + end + + function AnimatedComponent:didUpdate(oldProps) + -- If the animatedValues changed, set new goals for the motor so they animate. + if self.props.animatedValues ~= oldProps.animatedValues then + local goals = {} + local needsMotorUpdate = false + for key, newValue in pairs(self.props.animatedValues) do + local springOptions = self.props.springOptions -- nil means default + goals[key] = Otter.spring(newValue, springOptions) + if newValue ~= oldProps.animatedValues[key] then + needsMotorUpdate = true + end + end + if needsMotorUpdate then + self.motor:setGoal(goals) + end + end + end + + function AnimatedComponent:willUnmount() + self.motor:destroy() + self.motor = nil + end + + return AnimatedComponent +end + +SpringAnimatedItem.AnimatedFrame = SpringAnimatedItem.wrap("Frame") +SpringAnimatedItem.AnimatedScrollingFrame = SpringAnimatedItem.wrap("ScrollingFrame") +SpringAnimatedItem.AnimatedImageLabel = SpringAnimatedItem.wrap("ImageLabel") +SpringAnimatedItem.AnimatedTextButton = SpringAnimatedItem.wrap("TextButton") +SpringAnimatedItem.AnimatedUIScale = SpringAnimatedItem.wrap("UIScale") + +return SpringAnimatedItem diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.spec.lua new file mode 100644 index 0000000..87bdd72 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.spec.lua @@ -0,0 +1,181 @@ +return function() + local SpringAnimatedItem = require(script.Parent.SpringAnimatedItem) + local Packages = script.Parent.Parent.Parent + local Roact = require(Packages.Roact) + + describe("SpringAnimatedItem", function() + local function testAnimatedComponent(component) + local element = Roact.createElement(component, { + animatedValues = { + positionY = 100, + }, + mapValuesToProps = function(values) + return { + Position = UDim2.new(0, 0, 0, values.positionY), + } + end, + }) + + local instance = Roact.mount(element) + + Roact.update(instance, Roact.createElement(component, { + animatedValues = { + positionY = 200, + }, + mapValuesToProps = function(values) + return { + Position = UDim2.new(0, 0, 0, values.positionY), + } + end, + })) + + Roact.unmount(instance) + end + + local function testAnimatedUIScale() + local element = Roact.createElement(SpringAnimatedItem.AnimatedUIScale, { + animatedValues = { + Scale = 0.8, + }, + mapValuesToProps = function(values) + return { + Scale = UDim2.new(0, 0, 0, values.Scale), + } + end, + }) + + local instance = Roact.mount(element) + + Roact.update(instance, Roact.createElement(SpringAnimatedItem.AnimatedUIScale, { + animatedValues = { + Scale = 1, + }, + mapValuesToProps = function(values) + return { + Scale = UDim2.new(0, 0, 0, values.Scale), + } + end, + })) + + Roact.unmount(instance) + end + + it("should throw if prop types are not correct", function() + local function testShouldThrow(props) + expect(function() + local element = Roact.createElement(SpringAnimatedItem.AnimatedFrame, props) + + Roact.mount(element) + end).to.throw() + end + + testShouldThrow({ + animatedValues = 1, + }) + + testShouldThrow({ + animatedValues = {}, + }) + + testShouldThrow({ + animatedValues = {}, + mapValuesToProps = "string", + }) + + testShouldThrow({ + animatedValues = {}, + mapValuesToProps = function() end, + springOptions = "string", + }) + + testShouldThrow({ + animatedValues = {}, + mapValuesToProps = function() end, + onComplete = "string", + }) + + testShouldThrow({ + animatedValues = {}, + mapValuesToProps = function() end, + regularProps = "string", + }) + end) + + it("should create/destroy/update pre-made components without errors", function() + testAnimatedComponent(SpringAnimatedItem.AnimatedFrame) + testAnimatedComponent(SpringAnimatedItem.AnimatedImageLabel) + testAnimatedComponent(SpringAnimatedItem.AnimatedTextButton) + testAnimatedUIScale() + end) + + it(".wrap() should generate a component successfully", function() + local AnimatedImageButton = SpringAnimatedItem.wrap("ImageButton") + + testAnimatedComponent(AnimatedImageButton) + end) + + it("should support children in the regularProps table", function() + local container = Instance.new("Folder") + local element = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + animatedValues = {}, + mapValuesToProps = function() + return {} + end, + regularProps = { + [Roact.Children] = { + Foo = Roact.createElement("StringValue"), + }, + }, + }) + + local tree = Roact.mount(element, container) + local instance = container:GetChildren()[1] + expect(instance.ClassName).to.equal("Frame") + + local child = instance:FindFirstChild("Foo") + expect(child).to.be.ok() + expect(child.ClassName).to.equal("StringValue") + Roact.unmount(tree) + end) + + it("should support children in the top-level table", function() + local container = Instance.new("Folder") + local element = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + animatedValues = {}, + mapValuesToProps = function() + return {} + end, + }, { + Foo = Roact.createElement("StringValue"), + }) + + local tree = Roact.mount(element, container) + local instance = container:GetChildren()[1] + expect(instance.ClassName).to.equal("Frame") + + local child = instance:FindFirstChild("Foo") + expect(child).to.be.ok() + expect(child.ClassName).to.equal("StringValue") + Roact.unmount(tree) + end) + + it("should throw if children are specified in multiple ways", function() + local element = Roact.createElement(SpringAnimatedItem.AnimatedFrame, { + animatedValues = {}, + mapValuesToProps = function() + return {} + end, + regularProps = { + [Roact.Children] = { + Bar = Roact.createElement("IntValue"), + } + }, + }, { + Foo = Roact.createElement("StringValue"), + }) + + local success = pcall(Roact.mount, element) + assert(not success, "Roact.mount should have thrown an error, but it did not") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.story.lua new file mode 100644 index 0000000..1901d73 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/SpringAnimatedItem.story.lua @@ -0,0 +1,58 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Roact = require(ReplicatedStorage.Packages.Roact) + +local SpringAnimatedItem = require(script.Parent.SpringAnimatedItem) + +local TestButton = Roact.PureComponent:extend("TestButton") + +function TestButton:init() + self.state = { + activated = false, + } + + self.ref = Roact.createRef() +end + +function TestButton:render() + local activated = self.state.activated + + return Roact.createElement(SpringAnimatedItem.AnimatedTextButton, { + animatedValues = { + backgroundTransparency = activated and 0 or 0.5, + height = activated and 200 or 100, + positionOffsetY = activated and 200 or 0, + }, + mapValuesToProps = function(values) + return { + BackgroundTransparency = values.backgroundTransparency, + Size = UDim2.new(0, 200, 0, values.height), + Position = UDim2.new(0, 0, 0, values.positionOffsetY), + } + end, + regularProps = { + Text = "SpringAnimatedItem", + Size = UDim2.new(0, 200, 0, 0), + BackgroundColor3 = Color3.fromRGB(2, 183, 87), + AutoButtonColor = false, + [Roact.Event.Activated] = function() + self:setState({ + activated = not self.state.activated, + }) + + -- Change the text here, to verify that ref forwarding works properly. + self.ref.current.Text = self.state.activated + and "SpringAnimatedItem - Activated" or "SpringAnimatedItem" + end, + [Roact.Ref] = self.ref, + }, + }) +end + +return function(target) + local handle = Roact.mount(Roact.createElement(TestButton), target, "SpringAnimatedItem") + + return function() + Roact.unmount(handle) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/createSignal.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/createSignal.lua new file mode 100644 index 0000000..029adc1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/createSignal.lua @@ -0,0 +1,75 @@ +--[[ + This is a simple signal implementation that has a dead-simple API. + + local signal = createSignal() + + local disconnect = signal:subscribe(function(foo) + print("Cool foo:", foo) + end) + + signal:fire("something") + + disconnect() +]] + +local function addToMap(map, addKey, addValue) + local new = {} + + for key, value in pairs(map) do + new[key] = value + end + + new[addKey] = addValue + + return new +end + +local function removeFromMap(map, removeKey) + local new = {} + + for key, value in pairs(map) do + if key ~= removeKey then + new[key] = value + end + end + + return new +end + +local function createSignal() + local connections = {} + + local function subscribe(_, callback) + assert(typeof(callback) == "function", "Can only subscribe to signals with a function.") + + local connection = { + callback = callback, + } + + connections = addToMap(connections, callback, connection) + + local function disconnect() + assert(not connection.disconnected, "Listeners can only be disconnected once.") + + connection.disconnected = true + connections = removeFromMap(connections, callback) + end + + return disconnect + end + + local function fire(_, ...) + for callback, connection in pairs(connections) do + if not connection.disconnected then + callback(...) + end + end + end + + return { + subscribe = subscribe, + fire = fire, + } +end + +return createSignal \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/createSignal.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/createSignal.spec.lua new file mode 100644 index 0000000..4fd50e0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/createSignal.spec.lua @@ -0,0 +1,16 @@ +return function() + local createSignal = require(script.Parent.createSignal) + + it("should connect and fire signals without errors", function() + local signal = createSignal() + + local testValue = "Some test value." + local disconnect = signal:subscribe(function(newValues) + expect(newValues).to.equal(testValue) + end) + + signal:fire(testValue) + disconnect() + end) + +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/divideTransparency.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/divideTransparency.lua new file mode 100644 index 0000000..066f6bf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/divideTransparency.lua @@ -0,0 +1,9 @@ +--[[ + Divides a transparency value by a value, as if it were opacity. + divideTransparency(0, 2) -> 0.5 + divideTransparency(0.3, 2) -> 0.65 +]] + +return function(transparency, divisor) + return 1 - (1 - transparency) / divisor +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/divideTransparency.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/divideTransparency.spec.lua new file mode 100644 index 0000000..c684f08 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/divideTransparency.spec.lua @@ -0,0 +1,9 @@ +return function() + local divideTransparency = require(script.Parent.divideTransparency) + + it("should divide transparency", function() + expect(divideTransparency(0, 2)).to.equal(0.5) + expect(divideTransparency(0.5, 2)).to.equal(0.75) + expect(divideTransparency(0, 4)).to.equal(0.75) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/enumValidator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/enumValidator.lua new file mode 100644 index 0000000..cca3d45 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/enumValidator.lua @@ -0,0 +1,18 @@ +-- Validator for Roblox enums + +local UtilityRoot = script.Parent +local UIBloxRoot = UtilityRoot.Parent + +local t = require(UIBloxRoot.Parent.t) + +local function enumValidator(enum) + local validators = {} + + for _, enumItem in pairs(enum) do + validators[#validators + 1] = t.literal(enumItem) + end + + return t.union(unpack(validators)) +end + +return enumValidator \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/enumerateValidator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/enumerateValidator.lua new file mode 100644 index 0000000..c7fcc91 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/enumerateValidator.lua @@ -0,0 +1,7 @@ +-- Validator for custom enums created by enumerate + +return function(enum) + return function(value) + return enum.isEnumValue(value) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/isPositiveVector2.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/isPositiveVector2.lua new file mode 100644 index 0000000..cb26583 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/isPositiveVector2.lua @@ -0,0 +1,18 @@ +local UtilityRoot = script.Parent +local UIBloxRoot = UtilityRoot.Parent + +local t = require(UIBloxRoot.Parent.t) + +local positiveVector2 = t.intersection( + t.Vector2, + function(value) + if value.X < 0 or value.Y < 0 then + return false, + ("each component of the Vector2 must be >= 0; component values are: %d, %d"):format(value.X, value.Y) + end + + return true + end +) + +return positiveVector2 \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/isPositiveVector2.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/isPositiveVector2.spec.lua new file mode 100644 index 0000000..d660ba7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/isPositiveVector2.spec.lua @@ -0,0 +1,17 @@ +local isPositiveVector2 = require(script.Parent.isPositiveVector2) + +return function() + it("should return false for non-Vector2s", function() + expect(isPositiveVector2(0)).to.equal(false) + end) + + it("should return false for Vector2s with a negative component", function() + expect(isPositiveVector2(Vector2.new(-100, 100))).to.equal(false) + expect(isPositiveVector2(Vector2.new(100, -100))).to.equal(false) + expect(isPositiveVector2(Vector2.new(-100, -100))).to.equal(false) + end) + + it("should return true for Vector2s with positive components", function() + assert(isPositiveVector2(Vector2.new(100, 100))) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/lerp.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/lerp.lua new file mode 100644 index 0000000..47b3ff0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/lerp.lua @@ -0,0 +1,3 @@ +return function(a, b, alpha) + return (1 - alpha) * a + b * alpha +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/lerp.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/lerp.spec.lua new file mode 100644 index 0000000..b2e9b0d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/lerp.spec.lua @@ -0,0 +1,10 @@ +return function() + local lerp = require(script.Parent.lerp) + + it("should linearly interpolate numbers", function() + expect(lerp(0, 4, 0)).to.equal(0) + expect(lerp(0, 4, 1)).to.equal(4) + expect(lerp(0, 4, 0.5)).to.equal(2) + expect(lerp(0, 4, 0.75)).to.equal(3) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/mockStyleComponent.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/mockStyleComponent.lua new file mode 100644 index 0000000..3c08c6c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/mockStyleComponent.lua @@ -0,0 +1,18 @@ +local Component = script.Parent +local UIBlox = Component.Parent +local Roact = require(UIBlox.Parent.Roact) + +local AppStyleProvider = require(UIBlox.App.Style.AppStyleProvider) + +return function(elements) + return Roact.createElement(AppStyleProvider, { + style = { + themeName = "dark", + fontName = "gotham", + }, + }, { + Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, elements) + }) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/strict.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/strict.lua new file mode 100644 index 0000000..c1d21a5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/strict.lua @@ -0,0 +1,27 @@ +local function strict(t, name) + name = name or tostring(t) + + return setmetatable(t, { + __index = function(self, key) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + + __newindex = function(self, key, value) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + }) +end + +return strict \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/strict.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/strict.spec.lua new file mode 100644 index 0000000..fc44bff --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/Utility/strict.spec.lua @@ -0,0 +1,25 @@ +return function() + local strict = require(script.Parent.strict) + + it("should error when getting a nonexistent key", function() + local t = strict({ + a = 1, + b = 2, + }) + + expect(function() + return t.c + end).to.throw() + end) + + it("should error when setting a nonexistent key", function() + local t = strict({ + a = 1, + b = 2, + }) + + expect(function() + t.c = 3 + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/init.lua new file mode 100644 index 0000000..efa7ce6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/UIBlox/init.lua @@ -0,0 +1,286 @@ +local makeConfigurable = require(script.Core.Config.makeConfigurable) +local UIBloxDefaultConfig = require(script.UIBloxDefaultConfig) +local Packages = script.Parent + +local function initializeLibrary() + local strict = require(script.Utility.strict) + + local UIBlox = {} + + UIBlox.Core = strict({ + Animation = strict({ + SpringAnimatedItem = require(script.Utility.SpringAnimatedItem), + withAnimation = require(script.Core.Animation.withAnimation), + }), + + Bar = strict({ + ThreeSection = require(script.Core.Bar.ThreeSectionBar), + }), + + ImageSet = strict({ + Button = require(script.Core.ImageSet.ImageSetComponent).Button, + Label = require(script.Core.ImageSet.ImageSetComponent).Label, + Validator = strict({ + validateImage = require(script.Core.ImageSet.Validator.validateImage), + }), + }), + + Control = strict({ + Enum = strict({ + ControlState = require(script.Core.Control.Enum.ControlState), + SelectionMode = require(script.Core.Control.Enum.SelectionMode), + }), + Interactable = require(script.Core.Control.Interactable), + InteractableList = require(script.Core.Control.InteractableList), + }), + + Style = strict({ + Validator = strict({ + validateFontInfo = require(script.Core.Style.Validator.validateFontInfo), + validateColorInfo = require(script.Core.Style.Validator.validateColorInfo), + }), + Palette = require(script.Core.Style.Symbol.Palette), + Provider = require(script.Core.Style.StyleProvider), + withStyle = require(script.Core.Style.withStyle), + }), + + Text = strict({ + ExpandableText = strict({ + GetCanExpand = require(script.Core.Text.ExpandableText.ExpandableTextUtils).getCanExpand + }), + }), + + InfiniteScroller = strict(require(Packages.InfiniteScroller)), + }) + + UIBlox.App = strict({ + Context = strict({ + ContentProvider = require(script.App.Context.ContentProvider) + }), + + ImageSet = strict({ + Images = require(script.App.ImageSet.Images), + getIconSize = require(script.App.ImageSet.getIconSize), + getIconSizeUDim2 = require(script.App.ImageSet.getIconSizeUDim2), + scaleSliceToResolution = require(script.App.ImageSet.scaleSliceToResolution), + Enum = strict({ + IconSize = require(script.App.ImageSet.Enum.IconSize) + }), + }), + + Accordion = strict({ + AccordionView = require(script.App.Accordion.AccordionView), + }), + + Bar = strict({ + HeaderBar = require(script.App.Bar.HeaderBar), + RootHeaderBar = require(script.App.Bar.RootHeaderBar), + FullscreenTitleBar = require(script.App.Bar.FullscreenTitleBar), + }), + + Button = strict({ + Enum = strict({ + ButtonType = require(script.App.Button.Enum.ButtonType), + }), + PrimaryContextualButton = require(script.App.Button.PrimaryContextualButton), + PrimarySystemButton = require(script.App.Button.PrimarySystemButton), + SecondaryButton = require(script.App.Button.SecondaryButton), + AlertButton = require(script.App.Button.AlertButton), + ButtonStack = require(script.App.Button.ButtonStack), + TextButton = require(script.App.Button.TextButton), + IconButton = require(script.App.Button.IconButton), + ActionBar = require(script.App.Button.ActionBar) + }), + + Cell = strict({ + Small = strict({ + SelectionGroup = strict({ + SmallRadioButtonGroup = require(script.App.Cell.Small.SelectionGroup.SmallRadioButtonGroup), + }) + }) + }), + + Text = strict({ + ExpandableTextArea = require(script.App.Text.ExpandableTextArea.ExpandableTextArea), + }), + + Loading = strict({ + Enum = strict({ + RetrievalStatus = require(script.App.Loading.Enum.RetrievalStatus), + LoadingState = require(script.App.Loading.Enum.LoadingState), + RenderOnFailedStyle = require(script.App.Loading.Enum.RenderOnFailedStyle), + ReloadingStyle = require(script.App.Loading.Enum.ReloadingStyle), + }), + LoadableImage = require(script.App.Loading.LoadableImage), + ShimmerPanel = require(script.App.Loading.ShimmerPanel), + LoadingSpinner = require(script.App.Loading.LoadingSpinner), + }), + + InputButton = strict({ + RadioButtonList = require(script.App.InputButton.RadioButtonList), + CheckboxList = require(script.App.InputButton.CheckboxList), + Checkbox = require(script.App.InputButton.Checkbox), + Toggle = require(script.App.InputButton.Toggle), + }), + + Container = strict({ + Carousel = strict({ + FreeFlowCarousel = require(script.App.Container.Carousel.FreeFlowCarousel) + }), + VerticalScrollView = require(script.App.Container.VerticalScrollView), + getPageMargin = require(script.App.Container.getPageMargin), + LoadingStateContainer = require(script.App.Container.LoadingStateContainer), + }), + + Slider = strict({ + ContextualSlider = require(script.App.Slider.ContextualSlider), + SystemSlider = require(script.App.Slider.SystemSlider), + TwoKnobSystemSlider = require(script.App.Slider.TwoKnobSystemSlider), + TwoKnobContextualSlider = require(script.App.Slider.TwoKnobContextualSlider), + }), + + Grid = strict({ + GridView = require(script.App.Grid.GridView), + GridMetrics = require(script.App.Grid.GridMetrics), + DefaultMetricsGridView = require(script.App.Grid.DefaultMetricsGridView), + ScrollingGridView = require(script.App.Grid.ScrollingGridView), + }), + + Pill = strict({ + SmallPill = require(script.App.Pill.SmallPill), + LargePill = require(script.App.Pill.LargePill), + }), + + Tile = strict({ + Enum = strict({ + ItemTileEnums = require(script.App.Tile.Enum.ItemTileEnums), + }), + SaveTile = require(script.App.Tile.SaveTile.SaveTile), + ItemTile = require(script.App.Tile.ItemTile.ItemTile), + ItemTileFooter = require(script.App.Tile.ItemTile.ItemTileFooter), + MenuTile = require(script.App.Tile.MenuTile.MenuTile), + }), + + Dialog = strict({ + Modal = strict({ + -- TEMPORARY WORK! This should not be available yet. Please contact Eric Sauer for more info + --FullPageModal = require(script.App.Dialog.Modal.FullPageModal), + PartialPageModal = require(script.App.Dialog.Modal.PartialPageModal), + EducationalModal = require(script.App.Dialog.Modal.EducationalModal), + }), + Alert = strict({ + InformativeAlert = require(script.App.Dialog.Alert.InformativeAlert), + InteractiveAlert = require(script.App.Dialog.Alert.InteractiveAlert), + LoadingAlert = require(script.App.Dialog.Alert.LoadingAlert), + }), + Enum = strict({ + AlertType = require(script.App.Dialog.Alert.Enum.AlertType), + TooltipOrientation = require(script.App.Dialog.Tooltip.Enum.TooltipOrientation), + }), + Toast = require(script.App.Dialog.Toast.SlideFromTopToast), + Tooltip = require(script.App.Dialog.Tooltip.Tooltip), + }), + + Constant = strict({ + -- DEPRECATED: use App.ImageSet.getIconSize to get the size + IconSize = require(script.App.Constant.IconSize), + }), + + Style = strict({ + Validator = strict({ + validateFont = require(script.App.Style.Validator.validateFont), + validateTheme = require(script.App.Style.Validator.validateTheme), + validateStyle = require(script.App.Style.Validator.validateStyle), + }), + AppStyleProvider = require(script.App.Style.AppStyleProvider), + Colors = require(script.App.Style.Colors), + Constants = require(script.App.Style.Constants), + }), + + Indicator = strict({ + Badge = require(script.App.Indicator.Badge), + EmptyState = require(script.App.Indicator.EmptyState), + }), + + Menu = strict({ + BaseMenu = require(script.App.Menu.BaseMenu), + OverlayBaseMenu = require(script.App.Menu.OverlayBaseMenu), + + ContextualMenu = require(script.App.Menu.ContextualMenu), + OverlayContextualMenu = require(script.App.Menu.OverlayContextualMenu), + + MenuDirection = require(script.App.Menu.MenuDirection), + + DropdownMenu = require(script.App.Menu.DropdownMenu), + + }), + + Control = strict({ + Knob = require(script.App.Control.Knob.Knob), + SegmentedControl = require(script.App.Control.SegmentedControl), + RobuxBalance = require(script.App.Control.RobuxBalance), + }), + + Navigation = strict({ + Enum = strict({ + Placement = require(script.App.Navigation.Enum.Placement), + }), + SystemBar = require(script.App.Navigation.SystemBar), + }), + + SelectionImage = strict({ + SelectionCursorProvider = require(script.App.SelectionImage.SelectionCursorProvider), + CursorKind = require(script.App.SelectionImage.CursorKind), + withSelectionCursorProvider = require(script.App.SelectionImage.withSelectionCursorProvider), + }), + + }) + + -- DEPRECATED SECTION + + -- DEPRECATED: Use Core.Style instead + UIBlox.Style = { + Provider = require(script.Style.StyleProvider), + withStyle = require(script.Style.withStyle), + Validator = { + validateStyle = require(script.Style.Validator.validateStyle), + validateFont = require(script.Style.Validator.validateFont), + validateFontInfo = require(script.Style.Validator.validateFontInfo), + validateTheme = require(script.Style.Validator.validateTheme), + validateColorInfo = require(script.Style.Validator.validateColorInfo), + }, + } + + -- DEPRECATED: This is kept for compatibility. Use App.Accordion.AccordionView instead. + UIBlox.AccordionView = require(script.App.Accordion.AccordionView) + + -- DEPRECATED: This is kept for compatibility. This should not be used because it is an old design. + -- Use ContextualMenu instead + UIBlox.ModalBottomSheet = require(script.ModalBottomSheet.ModalBottomSheet) + + -- DEPRECATED: This is kept for compatibility. + UIBlox.Utility = { + ExternalEventConnection = require(script.Utility.ExternalEventConnection), + --Use Core.Animation.SpringAnimatedItem instead + SpringAnimatedItem = require(script.Utility.SpringAnimatedItem), + } + + -- DEPRECATED: use Core.Loading instead. + UIBlox.Loading = { + LoadableImage = require(script.App.Loading.LoadableImage), + ShimmerPanel = require(script.App.Loading.ShimmerPanel), + } + + -- DEPRECATED: use App.Tile instead. + UIBlox.Tile = { + SaveTile = require(script.App.Tile.SaveTile.SaveTile), + ItemTile = require(script.App.Tile.ItemTile.ItemTile), + ItemTileEnums = require(script.App.Tile.Enum.ItemTileEnums), + } + + -- END DEPRECATED SECTION + + return UIBlox +end + +return makeConfigurable(initializeLibrary, "UIBlox", UIBloxDefaultConfig) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/enumerate.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/enumerate.lua new file mode 100644 index 0000000..780f356 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/enumerate.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_enumerate"]["enumerate"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/lock.toml new file mode 100644 index 0000000..7ced171 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/lock.toml @@ -0,0 +1,15 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "UIBlox" +version = "0.1.1" +commit = "3a82d9edf6c4d9edf5bf34325ed4802c80d759dc" +source = "git+https://github.com/roblox/uiblox#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "InfiniteScroller roblox/infinite-scroller 0.5.6 url+https://github.com/roblox/infinite-scroller", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "RoactGamepad roblox/roact-gamepad 0.4.6 url+https://github.com/roblox/roact-gamepad", + "enumerate roblox/enumerate 1.0.0 url+https://github.com/roblox/enumerate", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/t.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/UIBlox/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/List/List.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/List/List.lua new file mode 100644 index 0000000..177c311 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/List/List.lua @@ -0,0 +1,356 @@ +local Root = script.Parent.Parent +local Cryo = require(Root.Parent.Cryo) +local binarySearch = require(Root.binarySearch.binarySearch) + +local List = {} + +List.__index = List + +--[[ + Create a new List from a list of values +]] +function List.new(...) + local self = { + values = {}, + _immutableDataStructureType = List, + } + + setmetatable(self, List) + + List._insertInPlace(self, 1, ...) + + return self +end + +--[[ + Internal method that absorbs an existing list-style table as the underlying data. +]] +function List._newCannibalizeTable(tab) + local self = { + values = tab, + _immutableDataStructureType = List, + } + + setmetatable(self, List) + + return self +end + +--[[ + Check if a given value is a list. +]] +function List.is(object) + if type(object) ~= "table" then + return false + end + return object._immutableDataStructureType == List +end + +--[[ + Creates a new List from a list-style table. +]] +function List.newFromListTable(table) + return List.new(unpack(table)) +end + +--[[ + Returns the size of a List. +]] +function List:size() + return #self.values +end + +--[[ + Creates a (shallow) copy of a list. +]] +function List:copy() + return List.new(unpack(self.values)) +end + +--[[ + Returns the (read only) value at the index. +]] +function List:get(index) + assert(index >= 1 and index <= #self.values, "Index out of bounds!") + return self.values[index] +end + +--[[ + Creates a new List at which the value at index is changed to value. +]] +function List:set(index, value) + assert(value ~= nil, "Cannot set a value to nil. Use remove() to remove values") + assert(1 <= index and index <= #self.values, "Index out of bounds!") + local new = self:copy() + new.values[index] = value + return new +end + +--[[ + Expects (any number of) dictionaries of the form { [index] = value }. + Preferred for when multiple sets are needed repeatedly, + as this version does no intermediate copying. +]] +function List:batchSet(...) + local new = self:copy() + local length = select("#", ...) + local values = new.values + for i = 1, length do + local pair = select(i, ...) + for key, value in pairs(pair) do + assert(1 <= key and key <= #values) + values[key] = value + end + end + return new +end + +--[[ + Creates a new List where the element at index is deleted. +]] +function List:remove(index) + assert(index >= 1 and index <= #self.values, "Index out of bounds!") + local new = self:copy() + table.remove(new.values, index) + return new +end + +--[[ + Creates a new List where the elements are inserted at index. +]] +function List:insert(index, ...) + assert(index >= 1 and index <= #self.values + 1, "Index out of bounds!") + local new = self:copy() + new:_insertInPlace(index, ...) + return new +end + +--[[ + Push a value onto the end of the list. +]] +function List:pushBack(value) + return self:insert(#self.values + 1, value) +end + +--[[ + Push a value onto the front of the list. +]] +function List:pushFront(value) + return self:insert(1, value) +end + +--[[ + Pop a value from the back of the list. +]] +function List:popBack() + return self:remove(#self.values) +end + +--[[ + Pop a value from the front of the list. +]] +function List:popFront() + return self:remove(1) +end + +--[[ + Returns a (shallow) copy of the underlying table. +]] +function List:toTable() + local new = self:copy() + return new.values +end + +--[[ + Internal method that inserts a number of values in place. +]] +function List:_insertInPlace(index, ...) + local length = select("#", ...) + if length == 0 then + return + end + self:_shift(index, length) + local values = self.values + for i = 0, length - 1 do + values[i + index] = select(i + 1, ...) + end +end + +--[[ + Internal method that shifts over a number of values to make space for insertion. +]] +function List:_shift(index, numPlacesToShift) + local values = self.values + for i = #self.values, index, -1 do + values[i + numPlacesToShift] = values[i] + values[i] = nil + end +end + +--[[ + Create a copy of the List with only values for which `callback` returns true. + Calls the callback with (value, index). +]] +function List:filter(callback) + local new = Cryo.List.filter(self.values, callback) + return List._newCannibalizeTable(new) +end + +--[[ + Create a copy of the List doing a combination filter and map. + + If callback returns nil for any item, it is considered filtered from the + list. Any other value is considered the result of the 'map' operation. +]] +function List:filterMap(callback) + local new = Cryo.List.filterMap(self.values, callback) + return List._newCannibalizeTable(new) +end + +--[[ + Returns the index of the first value found or nil if not found. +]] +function List:find(value) + return Cryo.List.find(self.values, value) +end + +--[[ + Returns the index of the first value for which predicate(value, index) is truthy, or nil if not found. +]] +function List:findWhere(predicate) + return Cryo.List.findWhere(self.values, predicate) +end + +--[[ + Performs a left-fold of the List with the given initial value and callback. +]] +function List:foldLeft(callback, initialValue) + return Cryo.List.foldLeft(self.values, callback, initialValue) +end + +--[[ + Performs a right-fold of the List with the given initial value and callback. +]] +function List:foldRight(callback, initialValue) + return Cryo.List.foldRight(self.values, callback, initialValue) +end + +--[[ + Returns a new List containing only the elements within the given range. +]] +function List:getRange(startIndex, endIndex) + local new = Cryo.List.getRange(self.values, startIndex, endIndex) + return List._newCannibalizeTable(new) +end + +--[[ + Create a copy of the List where each value is transformed by `callback` +]] +function List:map(callback) + local new = Cryo.List.map(self.values, callback) + return List._newCannibalizeTable(new) +end + +--[[ + Joins any number of Lists together into a new List +]] +function List:join(...) + local otherLists = {} + local len = select("#", ...) + for i = 1, len do + otherLists[i] = select(i, ...).values + end + local new = Cryo.List.join(self.values, unpack(otherLists)) + return List._newCannibalizeTable(new) +end + +--[[ + Create a copy of the List with removing the range from the List starting from the index. +]] +function List:removeRange(startIndex, endIndex) + local new = Cryo.List.removeRange(self.values, startIndex, endIndex) + return List._newCannibalizeTable(new) +end + +--[[ + Creates a new List that has no occurrences of the given value. +]] +function List:removeValue(value) + local new = Cryo.List.removeValue(self.values, value) + return List._newCannibalizeTable(new) +end + +--[[ + Returns a new List with the reversed order of the given list +]] +function List:reverse() + local new = Cryo.List.reverse(self.values) + return List._newCannibalizeTable(new) +end + +--[[ + Returns a new List, ordered with the given sort callback. + If no callback is given, the default table.sort will be used. +]] +function List:sort(callback) + local new = Cryo.List.sort(self.values, callback) + return List._newCannibalizeTable(new) +end + +--[[ + Returns an iterator that traverses the List in a forward direction. + e.g. + for index, value in list:iterator() do + ... + end +]] +function List:iterator() + local i = 0 + local length = #self.values + + return function() + i = i + 1 + if i <= length then + return i, self.values[i] + end + end +end + +--[[ + Returns an iterator that traverses the List in a backward direction. + e.g. + for index, value in list:reverseIterator() do + ... + end +]] +function List:reverseIterator() + local i = #self.values + 1 + + return function() + i = i - 1 + if i > 0 then + return i, self.values[i] + end + end +end + +--[[ + Does a binarySearch in the List, assuming that it is sorted. + Returns the leftmost index of the occurence of value according to the given comaprator, if provided, + otherwise <. If not found, returns nil. +]] +function List:binarySearch(value, comparator) + return binarySearch(self.values, value, comparator) +end + +--[[ + Potential TODO: + Add O(n) methods for sorted Lists (e.g. setIntersection, setUnion, etc.) +]] + +--[[ + Specify the behavior of deepJoin for OrderedMap. +]] +List.deepJoin = List.join + +return List \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/List/List.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/List/List.spec.lua new file mode 100644 index 0000000..24153d2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/List/List.spec.lua @@ -0,0 +1,559 @@ +return function() + local List = require(script.Parent.List) + describe("Basic list operations", function() + + it("Creates a new empty list", function() + local list = List.new() + expect(list).to.be.ok() + expect(list:size()).to.equal(0) + local tab = list:toTable() + expect(next(tab)).to.never.be.ok() + end) + + it("Can tell apart Lists from non-Lists", function() + local nonlist = {} + expect(List.is(nonlist)).to.equal(false) + local number = 5 + expect(List.is(number)).to.equal(false) + local list = List.new() + expect(List.is(list)).to.equal(true) + end) + + it("Creates a table with some values", function() + local list = List.new(3, 4, 5) + expect(list).to.be.ok() + expect(list:size()).to.equal(3) + expect(list:get(1)).to.equal(3) + expect(list:get(2)).to.equal(4) + expect(list:get(3)).to.equal(5) + end) + + it("Creates a table with some values from table", function() + local list = List.newFromListTable({ 3, 4, 5 }) + expect(list).to.be.ok() + expect(list:size()).to.equal(3) + expect(list:get(1)).to.equal(3) + expect(list:get(2)).to.equal(4) + expect(list:get(3)).to.equal(5) + end) + + it("Can create a copy", function() + local list = List.new(3, 4, 5) + local listCopy = list:copy() + + expect(listCopy).never.to.equal(list) + expect(list:get(1)).to.equal(3) + expect(list:get(2)).to.equal(4) + expect(list:get(3)).to.equal(5) + expect(listCopy:size()).to.equal(3) + end) + + it("Can be converted immutably to a table", function() + local list = List.new(3, 4, 5) + local tab = list:toTable() + tab[2] = 50 + expect(list:get(1)).to.equal(3) + expect(list:get(2)).to.equal(4) + expect(list:get(3)).to.equal(5) + end) + + it("Supports immutable set", function() + local list = List.new(3, 4, 5) + local newList = list:set(2, 1000) + + expect(newList).never.to.equal(list) + expect(list:get(2)).to.equal(4) + expect(newList:get(2)).to.equal(1000) + end) + + it("Supports deletion", function() + local list = List.new(3, 4, 5, 6, 7) + local newList = list:remove(1) + + expect(newList).never.to.equal(list) + expect(list:get(1)).to.equal(3) + expect(newList:get(1)).to.equal(4) + expect(newList:size()).to.equal(4) + + local newNewList = newList:remove(2) + expect(newNewList).never.to.equal(newList) + expect(newList:get(2)).to.equal(5) + expect(newNewList:get(2)).to.equal(6) + expect(newNewList:size()).to.equal(3) + end) + + it("Supports insertion", function() + local list = List.new() + local newList = list:insert(1, 4) + + expect(newList).never.to.equal(list) + expect(newList:get(1)).to.equal(4) + expect(newList:size()).to.equal(1) + + local anotherList = List.new(3, 4, 5, 6, 7) + local newAnotherList = anotherList:insert(2, 100, 101) + + expect(newAnotherList:get(2)).to.equal(100) + expect(newAnotherList:get(3)).to.equal(101) + expect(newAnotherList:get(4)).to.equal(4) + expect(newAnotherList:size()).to.equal(7) + end) + + it("Supports push/pop at the back", function() + local list = List.new(3, 4, 5, 6, 7) + local newList = list:pushBack(100) + expect(newList:size()).to.equal(6) + expect(newList:get(newList:size())).to.equal(100) + + local newNewList = list:popBack() + expect(newNewList:size()).to.equal(4) + expect(newNewList:get(newNewList:size())).to.equal(6) + + local empty = List.new() + local nonempty = empty:pushBack(10) + expect(nonempty:size()).to.equal(1) + expect(nonempty:get(nonempty:size())).to.equal(10) + end) + + it("Supports push/pop at the front", function() + local list = List.new(3, 4, 5, 6, 7) + local newList = list:pushFront(100) + expect(newList:size()).to.equal(6) + expect(newList:get(1)).to.equal(100) + + local newNewList = list:popFront() + expect(newNewList:size()).to.equal(4) + expect(newNewList:get(1)).to.equal(4) + + local empty = List.new() + local nonempty = empty:pushFront(10) + expect(nonempty:size()).to.equal(1) + expect(nonempty:get(1)).to.equal(10) + end) + + it("Supports batch setting", function() + local list = List.new(2, 3, 4, 5, 6, 7) + local newList = list:batchSet({ + [2] = 100, + [4] = 1000, + }) + + expect(newList).never.to.equal(list) + expect(newList:get(1)).to.equal(2) + expect(newList:get(2)).to.equal(100) + expect(newList:get(4)).to.equal(1000) + expect(newList:size()).to.equal(6) + end) + end) + + describe("More advanced functionality", function() + describe("Filtering", function() + it("should use the callback", function() + local list = List.new(3, 4, 5, 6, 7) + local newList = list:filter(function(value, index) + return value % 2 == 0 + end) + + expect(newList).never.to.equal(list) + expect(newList:size()).to.equal(2) + expect(list:get(1)).to.equal(3) + expect(newList:get(1)).to.equal(4) + expect(list:get(2)).to.equal(4) + expect(newList:get(2)).to.equal(6) + end) + + it("should work with an empty List", function() + local called = false + local function callback() + called = true + return true + end + local list = List.new() + local newList = list:filter(callback) + expect(newList:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("FilterMapping", function() + it("should return a new table", function() + local list = List.new(1, 2, 3) + local function callback() + return 1 + end + local newList = list:filterMap(callback) + + expect(list).never.to.equal(newList) + end) + + it("should correctly use the filter callback", function() + local list = List.new(1, 2, 3, 4, 5) + local function doubleOddOnly(value) + if value % 2 == 0 then + return nil + else + return value * 2 + end + end + local newList = list:filterMap(doubleOddOnly) + + expect(newList:size()).to.equal(3) + expect(newList:get(1)).to.equal(2) + expect(newList:get(2)).to.equal(6) + expect(newList:get(3)).to.equal(10) + end) + + it("should work with an empty table", function() + local called = false + local function callback() + called = true + return true + end + local list = List.new() + + local newList = list:filterMap(callback) + expect(newList:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Joining", function() + it("Should work with two tables", function() + local first = List.new(1, 2, 3, 4, 5) + local second = List.new(100, 101, 102) + local newList = first:join(second) + expect(newList:get(1)).to.equal(1) + expect(newList:get(5)).to.equal(5) + expect(newList:get(6)).to.equal(100) + expect(newList:get(8)).to.equal(102) + expect(newList:size()).to.equal(8) + end) + + it("Should work with multiple tables", function() + local first = List.new(1, 2, 3, 4, 5) + local second = List.new(100, 101, 102) + local third = List.new(1000, 1001, 1002) + local newList = first:join(second, third) + expect(newList:get(1)).to.equal(1) + expect(newList:get(5)).to.equal(5) + expect(newList:get(6)).to.equal(100) + expect(newList:get(8)).to.equal(102) + expect(newList:get(9)).to.equal(1000) + expect(newList:get(11)).to.equal(1002) + expect(newList:size()).to.equal(11) + end) + end) + + describe("Iterators", function() + it("Should work in a for loop", function() + local list = List.new(3, 4, 5, 6, 7) + local count = 1 + for index, value in list:iterator() do + if count == 1 then + expect(value).to.equal(3) + elseif count == 5 then + expect(value).to.equal(7) + end + expect(value).to.equal(index + 2) + count = count + 1 + end + end) + + it("Should work for empty lists", function() + local list = List.new() + local wasAnythingDone = false + for _, _ in list:iterator() do + wasAnythingDone = true + end + expect(wasAnythingDone).to.equal(false) + end) + + it("Should work in reverse", function() + local list = List.new(3, 4, 5, 6, 7) + local count = 1 + for index, value in list:reverseIterator() do + if count == 5 then + expect(value).to.equal(3) + elseif count == 1 then + expect(value).to.equal(7) + end + expect(value).to.equal(index + 2) + count = count + 1 + end + end) + + describe("binarySearch", function() + it("Works for a sorted List", function() + local list = List.new(1, 3, 5, 6, 7) + expect(list:binarySearch(1)).to.equal(1) + expect(list:binarySearch(3)).to.equal(2) + expect(list:binarySearch(5)).to.equal(3) + expect(list:binarySearch(6)).to.equal(4) + expect(list:binarySearch(7)).to.equal(5) + expect(list:binarySearch(100)).to.never.be.ok() + end) + + it("Works for a sorted List with duplicates", function() + local list = List.new(1, 1, 3, 3, 3, 5, 5, 10, 11) + expect(list:binarySearch(1)).to.equal(1) + expect(list:binarySearch(3)).to.equal(3) + expect(list:binarySearch(5)).to.equal(6) + expect(list:binarySearch(10)).to.equal(8) + expect(list:binarySearch(11)).to.equal(9) + end) + + it("Works for an empty List", function() + local list = List.new() + expect(list:binarySearch(1)).to.never.be.ok() + end) + + it("Works for a special comparator", function() + local list = List.new(5, 6, 1, 5, 7, 3) + local reverse = function(lhs, rhs) + return rhs < lhs + end + local newList = list:sort(reverse) + expect(newList:binarySearch(7, reverse)).to.equal(1) + expect(newList:binarySearch(6, reverse)).to.equal(2) + expect(newList:binarySearch(5, reverse)).to.equal(3) + expect(newList:binarySearch(3, reverse)).to.equal(5) + expect(newList:binarySearch(1, reverse)).to.equal(6) + end) + end) + end) + + --[[ + TODO: port over more of these Cryo tests, if necessary. + ]] + describe("Cryo functions", function() + describe("find", function() + local list = List.new(5, 4, 3, 2, 1) + it("should return the correct index", function() + expect(list:find(1)).to.equal(5) + expect(list:find(2)).to.equal(4) + expect(list:find(3)).to.equal(3) + expect(list:find(4)).to.equal(2) + expect(list:find(5)).to.equal(1) + end) + + it("should work with an empty table", function() + local empty = List.new() + expect(empty:find(1)).to.never.be.ok() + end) + + it("should return nil when the given value is not found", function() + expect(list:find(1000)).to.never.be.ok() + end) + + it("should return the index of the first value found", function() + local repeated = List.new(1, 2, 2) + + expect(repeated:find(2)).to.equal(2) + end) + end) + + describe("findWhere", function() + it("should return the correct index", function() + local list = List.new(1, 5, 10, 7) + local isEven = function(value) + return value % 2 == 0 + end + + local isOdd = function(value) + return value % 2 == 1 + end + + expect(list:findWhere(isEven)).to.equal(3) + expect(list:findWhere(isOdd)).to.equal(1) + end) + + it("should work with an empty table", function() + local empty = List.new() + local anything = function() + return true + end + expect(empty:findWhere(anything)).to.never.be.ok() + end) + + it("should return nil when the when no value satisfies the predicate", function() + local numbers = List.new(1, 2, 3) + local isFour = function(value) + return value == 4 + end + + expect(numbers:findWhere(isFour)).to.never.be.ok() + end) + + it("should return the index of the first value for which the predicate is true", function() + local numbers = List.new(1, 1, 1, 2, 2) + + local isTwo = function(value) + return value == 2 + end + + expect(numbers:findWhere(isTwo)).to.equal(4) + end) + + it("should allow access to both value and index in the predicate function", function() + local numbers = List.new(1, 1, 2, 2, 1) + + local sumValueAndIndexToFive = function(value, index) + return value + index == 5 + end + + expect(numbers:findWhere(sumValueAndIndexToFive)).to.equal(3) + end) + end) + + describe("foldLeft", function() + it("should call the callback for each element", function() + local a = List.new(4, 5, 6) + local copy = {} + + a:foldLeft(function(accum, value, index) + copy[index] = value + return accum + end, 0) + + expect(#copy).to.equal(a:size()) + + for key, value in a:iterator() do + expect(value).to.equal(copy[key]) + end + end) + end) + + describe("foldRight", function() + it("should call the callback for each element", function() + local a = List.new(4, 5, 6) + local copy = {} + + a:foldRight(function(accum, value, index) + copy[index] = value + return accum + end, 0) + + expect(#copy).to.equal(a:size()) + + for key, value in a:iterator() do + expect(value).to.equal(copy[key]) + end + end) + end) + + describe("getRange", function() + it("should return the correct range", function() + local a = List.new(1, 2, 3, 4) + local b = a:getRange(2, 3) + + expect(b:get(1)).to.equal(2) + expect(b:get(2)).to.equal(3) + expect(b:size()).to.equal(2) + + local c = a:getRange(4, 4) + expect(c:size()).to.equal(1) + expect(c:get(1)).to.equal(4) + end) + end) + + describe("map", function() + it("should call the callback for each element", function() + local a = List.new(5, 6, 7) + local copy = {} + a:map(function(value, index) + copy[index] = value + return value + end) + + for key, value in a:iterator() do + expect(copy[key]).to.equal(value) + end + + for key, value in pairs(copy) do + expect(value).to.equal(a:get(key)) + end + end) + end) + + describe("removeRange", function() + it("should remove elements properly", function() + local a = List.new(1, 2, 3) + local b = a:removeRange(2, 2) + + expect(b:size()).to.equal(2) + expect(b:get(1)).to.equal(1) + expect(b:get(2)).to.equal(3) + + local c = List.new(1, 2, 3, 4, 5, 6) + local d = c:removeRange(1, 4) + + expect(d:size()).to.equal(2) + expect(d:get(1)).to.equal(5) + expect(d:get(2)).to.equal(6) + + local e = c:removeRange(2, 5) + + expect(e:size()).to.equal(2) + expect(e:get(1)).to.equal(1) + expect(e:get(2)).to.equal(6) + end) + end) + + describe("removeValue", function() + it("should remove all occurences of the same given value", function() + local a = List.new(1, 2, 2, 3) + local b = a:removeValue(2) + + expect(b:size()).to.equal(2) + expect(b:get(1)).to.equal(1) + expect(b:get(2)).to.equal(3) + end) + end) + + describe("reverse", function() + it("should reverse the list", function() + local a = List.new(1, 2, 3, 4) + local b = a:reverse() + + expect(b:get(1)).to.equal(4) + expect(b:get(2)).to.equal(3) + expect(b:get(3)).to.equal(2) + expect(b:get(4)).to.equal(1) + end) + end) + + describe("sort", function() + it("should sort with the default table.sort when no callback is given", function() + local a = List.new(4, 2, 5, 3, 1) + local b = a:sort() + + local aTable = a:toTable() + table.sort(aTable) + + expect(b:size()).to.equal(a:size()) + for i = 1, #aTable do + expect(b:get(i)).to.equal(aTable[i]) + end + end) + + it("should sort with the given callback", function() + local a = List.new(1, 2, 5, 3, 4) + local function order(first, second) + return first > second + end + local b = a:sort(order) + + local aTable = a:toTable() + + table.sort(aTable, order) + + expect(b:size()).to.equal(#aTable) + for i = 1, #a do + expect(b:get(i)).to.equal(aTable[i]) + end + end) + end) + end) + + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/None.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/None.lua new file mode 100644 index 0000000..2b461fa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/None.lua @@ -0,0 +1,3 @@ +-- Alias for Cryo.None +local Cryo = require(script.Parent.Parent.Cryo) +return Cryo.None \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedMap/OrderedMap.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedMap/OrderedMap.lua new file mode 100644 index 0000000..8bf478a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedMap/OrderedMap.lua @@ -0,0 +1,479 @@ +local Root = script.Parent.Parent +local None = require(Root.None) +local List = require(Root.List.List) +local UnorderedMap = require(Root.UnorderedMap.UnorderedMap) +local binarySearch = require(Root.binarySearch.binarySearch) +local deepJoin = require(Root.deepJoin.deepJoin) +local sort = require(Root.sort.sort) + +local OrderedMap = {} + +--[[ + Currently, exactly batchSet and join will remove None values. +]] + +OrderedMap.__index = OrderedMap + +--[[ + A BST implementation is not necessary for an immutable copy-on-write OrderedMap, since + we cannot take much advantage of O(log n) insert/deletes anyways. +]] + +--[[ + Create a new OrderedMap from a sortPredicate and any number of dictionaries of values of the form + { [key] = value } + sortPredicate should be a function with the following signature: + sortPredicate(key1, key2) -> bool + returning true if key1 < key2 in the sorting invariant. +]] +function OrderedMap.new(sortPredicate, ...) + local self = { + keys = {}, + values = {}, + sortPredicate = sortPredicate or sort.default, + _immutableDataStructureType = OrderedMap, + } + + setmetatable(self, OrderedMap) + + OrderedMap._insertInPlace(self, ...) + + return self +end + +--[[ + Returns a new UnorderedMap with the same key-value pairs. +]] +function OrderedMap:toUnorderedMap() + return UnorderedMap._newCannibalizeTable(self.values) +end + +--[[ + Returns if a value is an OrderedMap. +]] +function OrderedMap.is(value) + if type(value) ~= "table" then + return false + end + return value._immutableDataStructureType == OrderedMap +end + +--[[ + Returns the value at key. +]] +function OrderedMap:get(key) + return self.values[key] +end + +--[[ + Returns a new OrderedMap, setting the value at key to be value. +]] +function OrderedMap:set(key, value) + if self:get(key) == nil then + local new = self:copy() + new:_insertPairInPlace(key, value) + return new + end + local new = self:copy() + new.values[key] = value + return new +end + +--[[ + Gets the index-th key, value in the OrderedMap according to the sorting invariant. +]] +function OrderedMap:getByIndex(index) + local id = self.keys[index] + + if id == nil then + return nil + end + + return id, self.values[id] +end + +--[[ + Returns a List of all of the values in the map. +]] +function OrderedMap:getValues() + local new = {} + for index, key in ipairs(self.keys) do + new[index] = self.values[key] + end + return List._newCannibalizeTable(new) +end + +--[[ + Returns a List of all of the keys in the map. +]] +function OrderedMap:getKeys() + return List._newCannibalizeTable(self.keys) +end + +--[[ + Returns the size (number of key-value pairs) of the map. +]] +function OrderedMap:size() + return #self.keys +end + +--[[ + Returns a new OrderedMap with the pairs at all given keys removed. +]] +function OrderedMap:remove(...) + local new = OrderedMap.new(self.sortPredicate) + + local len = select("#", ...) + + local newKeys = new.keys + local newValues = new.values + + for key, value in pairs(self.values) do + newValues[key] = value + end + + for i = 1, len do + local key = select(i, ...) + + newValues[key] = nil + end + + for _, value in ipairs(self.keys) do + if new.values[value] ~= nil then + newKeys[#(newKeys)+1] = value + end + end + + return new +end + +--[[ + Internal method for removing a value from the map, in place. +]] +function OrderedMap:_removeInPlace(key) + self.values[key] = nil + local indexToRemove = binarySearch(self.keys, key, self.sortPredicate) + if not indexToRemove then + return + end + table.remove(self.keys, indexToRemove) +end + +--[[ + Joins any number of (basic table) dictionaries of values of the form + { [key] = value } + into the OrderedMap, creating a new OrderedMap. +]] +function OrderedMap:batchSet(...) + local new = self:copyRemoveNone() + new:_insertInPlaceRemoveNone(...) + return new +end + +--[[ + Creates a (shallow) copy of the OrderedMap. +]] +function OrderedMap:copy() + local new = OrderedMap.new(self.sortPredicate) + + local newKeys = new.keys + local newValues = new.values + + for key, value in ipairs(self.keys) do + newKeys[key] = value + end + + for key, value in pairs(self.values) do + newValues[key] = value + end + + return new +end + +--[[ + Creates a (shallow) copy of the OrderedMap, with all pairs with value None removed. +]] +function OrderedMap:copyRemoveNone() + local new = OrderedMap.new(self.sortPredicate) + + local newKeys = new.keys + local newValues = new.values + + for key, value in ipairs(self.keys) do + if self.values[value] ~= None then + newKeys[key] = value + end + end + + for key, value in pairs(self.values) do + if value ~= None then + newValues[key] = value + end + end + + return new +end + + +--[[ + Returns the first key, value in the OrderedMap. +]] +function OrderedMap:first() + if self.keys[1] then + return self.keys[1], self:get(self.keys[1]) + end +end + +--[[ + Returns the last key, value in the OrderedMap. +]] +function OrderedMap:last() + local i = #self.keys + if self.keys[i] then + return self.keys[i], self:get(self.keys[i]) + end +end + +--[[ + Create a new OrderedMap, applying the given predicate to each element in + this OrderedMap. + + Predicate should have the signature + predicate(value, key) -> newValue, newKey + + Analogous to 'map' on a list. +]] +function OrderedMap:map(predicate) + local new = OrderedMap.new(self.sortPredicate) + local keyChanged = false + for key, value in self:iterator() do + local newValue, newKey = predicate(value, key) + newKey = newKey or key + newValue = newValue or value + if key ~= newKey then + keyChanged = true + end + new:_insertPairInPlaceUnsorted(newKey, newValue) + end + + if keyChanged then + new:_sortInPlace() + end + + return new +end + +--[[ + Create a new OrderedMap, where each key-value pair is included iff callback(value, key) is truthy. + + callback should be a function of signature + callback(value, key) -> bool +]] +function OrderedMap:filter(callback) + local new = OrderedMap.new(self.sortPredicate) + for key, value in self:iterator() do + if callback(value, key) then + new:_insertPairInPlaceUnsorted(key, value) + end + end + + new:_sortInPlace() + return new +end + +--[[ + Join any number of OrderedMaps. The sorting comparator of the leftmost argument/self is + used for the returned OrderedMap. +]] +function OrderedMap:join(...) + local new = self:copyRemoveNone() + + for i = 1, select("#", ...) do + local other = select(i, ...) + + if other:size() > 0 then + for key, value in other:iterator() do + new:_insertPairInPlaceUnsortedRemoveNone(key, value) + end + end + end + + new:_sortInPlace() + + return new +end + +--[[ + Internal method for inserting values without sorting the map. + + This means that the invariants that the map exposes will be broken until + the _sortInPlace() method is called. +]] +function OrderedMap:_insertInPlaceUnsorted(...) + local len = select("#", ...) + for i = 1, len do + local pair = select(i, ...) + for key, value in pairs(pair) do + self:_insertPairInPlaceUnsorted(key, value) + end + end +end + +--[[ + Internal method for inserting a single pair without sorting the map. + + This means that the invariants that the map exposes will be broken until + the _sortInPlace() method is called. +]] +function OrderedMap:_insertPairInPlaceUnsorted(key, value) + if self.values[key] == nil then + table.insert(self.keys, key) + end + self.values[key] = value +end + +--[[ + Internal method for inserting values without sorting the map, removing None instances. + + This means that the invariants that the map exposes will be broken until + the _sortInPlace() method is called. +]] +function OrderedMap:_insertInPlaceUnsortedRemoveNone(...) + local len = select("#", ...) + for i = 1, len do + local pair = select(i, ...) + for key, value in pairs(pair) do + self:_insertPairInPlaceUnsortedRemoveNone(key, value) + end + end +end + +--[[ + Internal method for inserting a single pair without sorting the map, removing None instances. + + This means that the invariants that the map exposes will be broken until + the _sortInPlace() method is called. +]] +function OrderedMap:_insertPairInPlaceUnsortedRemoveNone(key, value) + if value == None then + self:_removeInPlace(key) + else + if self.values[key] == nil then + table.insert(self.keys, key) + end + self.values[key] = value + end +end + +--[[ + Sorts the map; used in cases where the map would become out-of-order when + using internal recommendations. +]] +function OrderedMap:_sortInPlace() + table.sort(self.keys, self.sortPredicate) +end + +--[[ + Returns an iterator for the OrderedMap. + Example: + for key, value in orderedMap:iterator() do +]] +function OrderedMap:iterator() + local i = 0 + local length = #self.keys + + -- Iterator function + return function() + i = i + 1 + if i <= length then + local key = self.keys[i] + return key, self.values[key], i + end + end +end + +--[[ + Returns an iterator for the OrderedMap that traverses in the reverse direction. + Example: + for key, value in orderedMap:reverseIterator() do +]] +function OrderedMap:reverseIterator() + local i = #self.keys + 1 + + -- Iterator function + return function() + i = i - 1 + if i > 0 then + local key = self.keys[i] + return key, self.values[key], i + end + end +end + +--[[ + Internal function that inserts the given values into the map in-place. + Expects { [key] = value } dictionaries. + + This operation mutates the map; generally you should use set or batchSet instead. +]] +function OrderedMap:_insertInPlace(...) + self:_insertInPlaceUnsorted(...) + self:_sortInPlace() +end + +--[[ + Internal function that inserts the given pair into the map in-place. + + This operation mutates the map; generally you should use set or batchSet instead. +]] +function OrderedMap:_insertPairInPlace(key, value) + self:_insertPairInPlaceUnsorted(key, value) + self:_sortInPlace() +end + +--[[ + Internal function that inserts the given values into the map in-place, removing None instances. + Expects { [key] = value } dictionaries. + + This operation mutates the map; generally you should use set or batchSet instead. +]] +function OrderedMap:_insertInPlaceRemoveNone(...) + self:_insertInPlaceUnsortedRemoveNone(...) + self:_sortInPlace() +end + +--[[ + Specify the behavior of deepJoin for OrderedMap. +]] +function OrderedMap:deepJoin(rhs) + local newMap = OrderedMap.new(self.sortPredicate) + for lhsKey, lhsValue in self:iterator() do + local rhsValue = rhs:get(lhsKey) + if rhsValue then + if type(rhsValue) == "table" and type(lhsValue) == "table" then + newMap:_insertPairInPlaceUnsorted(lhsKey, deepJoin(lhsValue, rhsValue)) + else + if rhsValue ~= None then + newMap:_insertPairInPlaceUnsorted(lhsKey, rhsValue) + end + end + else + if lhsValue ~= None then + newMap:_insertPairInPlaceUnsorted(lhsKey, lhsValue) + end + end + end + + -- Copy over rhs keys that aren't in lhs + for rhsKey, rhsValue in rhs:iterator() do + local lhsValue = self:get(rhsKey) + if not lhsValue and rhsValue ~= None then + newMap:_insertPairInPlaceUnsorted(rhsKey, rhsValue) + end + end + + newMap:_sortInPlace() + return newMap +end + +return OrderedMap \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedMap/OrderedMap.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedMap/OrderedMap.spec.lua new file mode 100644 index 0000000..7bdb02e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedMap/OrderedMap.spec.lua @@ -0,0 +1,621 @@ +return function() + local Root = script.Parent.Parent + local OrderedMap = require(script.Parent.OrderedMap) + local UnorderedMap = require(Root.UnorderedMap.UnorderedMap) + local None = require(Root.None) + local sort = require(Root.sort.sort) + + describe("Basic ordered map operations", function() + + it("Creates a new empty table", function() + local map = OrderedMap.new() + expect(map).to.be.ok() + expect(map:size()).to.equal(0) + end) + + it("Can tell apart OrderedMaps from non-OrderedMaps", function() + local nonmap = {} + expect(OrderedMap.is(nonmap)).to.equal(false) + local number = 5 + expect(OrderedMap.is(number)).to.equal(false) + local map = OrderedMap.new() + expect(OrderedMap.is(map)).to.equal(true) + end) + + it("Correctly returns nil when accessing an OOB index", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + expect(map:getByIndex(1000)).to.never.be.ok() + expect(map:size()).to.equal(3) + expect(map:get("apple")).to.equal(1) + expect(map:get("grapes")).to.equal(2) + expect(map:get("banana")).to.equal(10) + end) + + it("Creates an ordered with some values", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + expect(map).to.be.ok() + expect(map:size()).to.equal(3) + expect(map:get("apple")).to.equal(1) + expect(map:get("grapes")).to.equal(2) + expect(map:get("banana")).to.equal(10) + + -- Good sorting by key + local firstKey, firstValue = map:first() + local secondKey, secondValue = map:getByIndex(2) + local lastKey, lastValue = map:last() + expect(firstKey).to.equal("apple") + expect(firstValue).to.equal(1) + expect(secondKey).to.equal("banana") + expect(secondValue).to.equal(10) + expect(lastKey).to.equal("grapes") + expect(lastValue).to.equal(2) + end) + + it("Returns nil on a miss", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + expect(map:get("broccoli")).never.to.be.ok() + end) + + it("Creates a table with some values from multiple tables", function() + local map = OrderedMap.new(sort.reverse, { + apple = 1, + grapes = 10, + banana = 2, + }, { + orange = 5, + banana = 5, + }) + expect(map).to.be.ok() + expect(map:size()).to.equal(4) + expect(map:get("orange")).to.equal(5) + expect(map:get("grapes")).to.equal(10) + expect(map:get("banana")).to.equal(5) + end) + + it("Can create a copy", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + local mapCopy = map:copy() + + expect(mapCopy).never.to.equal(map) + expect(mapCopy).to.be.ok() + expect(mapCopy:size()).to.equal(3) + expect(mapCopy:get("apple")).to.equal(1) + expect(mapCopy:get("grapes")).to.equal(2) + expect(mapCopy:get("banana")).to.equal(10) + + -- Good sorting by key + local firstKey, firstValue = mapCopy:first() + local secondKey, secondValue = mapCopy:getByIndex(2) + local lastKey, lastValue = map:last() + expect(firstKey).to.equal("apple") + expect(firstValue).to.equal(1) + expect(secondKey).to.equal("banana") + expect(secondValue).to.equal(10) + expect(lastKey).to.equal("grapes") + expect(lastValue).to.equal(2) + end) + + it("Can be converted immutably to Lists of keys and values", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + local keys = map:getKeys() + local values = map:getValues() + + expect(keys:get(1)).to.equal("apple") + expect(keys:get(2)).to.equal("banana") + expect(keys:get(3)).to.equal("grapes") + expect(values:get(1)).to.equal(1) + expect(values:get(2)).to.equal(10) + expect(values:get(3)).to.equal(2) + end) + + it("Supports immutable set", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + + local newMap = map:set("apple", 1000) + + expect(newMap).never.to.equal(map) + expect(map:get("apple")).to.equal(1) + expect(newMap:get("apple")).to.equal(1000) + + local newNewMap = newMap:set("lettuce", -100) + expect(newNewMap:get("lettuce")).to.equal(-100) + end) + + it("Supports batch set", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + + local newMap = map:batchSet({ + apple = 100, + watermelon = 4, + }, { + watermelon = 100, + kiwi = 3, + }) + + expect(newMap).never.to.equal(map) + expect(map:get("apple")).to.equal(1) + expect(newMap:get("apple")).to.equal(100) + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect((newMap:first())).to.equal("apple") + expect((newMap:last())).to.equal("watermelon") + expect(map:get("kiwi")).to.never.be.ok() + expect(newMap:size()).to.equal(5) + end) + + it("Supports join", function() + local map1 = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + + local map2 = OrderedMap.new(sort.reverse, { + apple = 100, + watermelon = 4, + }) + + local map3 = OrderedMap.new(sort.reverse, { + watermelon = 100, + kiwi = 3, + }) + + local newMap = map1:join(map2, map3) + + expect(newMap).never.to.equal(map1) + expect(map1:get("apple")).to.equal(1) + expect(newMap:get("apple")).to.equal(100) + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect((newMap:first())).to.equal("apple") + expect((newMap:last())).to.equal("watermelon") + expect(map1:get("kiwi")).to.never.be.ok() + expect(newMap:size()).to.equal(5) + end) + + it("Supports None", function() + local map1 = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + useless = None, + }) + expect(map1:size()).to.equal(4) + + local map2 = OrderedMap.new(sort.reverse, { + apple = None, + watermelon = 4, + }) + + local map3 = OrderedMap.new(sort.reverse, { + watermelon = 100, + kiwi = 3, + }) + + local newMap = map1:join(map2, map3) + + expect(newMap).never.to.equal(map1) + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect(newMap:get("useless")).to.never.be.ok() + expect((newMap:first())).to.equal("banana") + expect((newMap:last())).to.equal("watermelon") + expect(newMap:size()).to.equal(4) + + newMap = map1:batchSet({ + apple = None, + watermelon = 4, + }, { + watermelon = 100, + kiwi = 3, + }) + + expect(newMap).never.to.equal(map1) + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect(newMap:get("useless")).to.never.be.ok() + expect((newMap:first())).to.equal("banana") + expect((newMap:last())).to.equal("watermelon") + expect(newMap:size()).to.equal(4) + end) + + it("Supports deletion", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + local newMap = map:remove("apple", "grapes") + + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("grapes")).to.never.be.ok() + expect(newMap:get("banana")).to.equal(10) + expect(map:get("apple")).to.be.ok() + expect(map:get("grapes")).to.be.ok() + expect(map:get("banana")).to.be.ok() + expect(newMap:size()).to.equal(1) + end) + end) + + describe("More advanced functionality", function() + describe("Mapping", function() + it("should use the callback", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + + local newMap = map:map(function(value, key) + return value * 2, key .. " fruit" + end) + + expect(newMap).never.to.equal(map) + expect(newMap:size()).to.equal(3) + expect(map:get("apple fruit")).to.never.be.ok() + expect(newMap:get("apple fruit")).to.equal(2) + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("banana fruit")).to.equal(20) + end) + + it("should maintain the sorting invariant", function() + local map = OrderedMap.new(sort.default, { + apple = 1, + cheese = 5, + banana = 10, + }) + + local newMap = map:map(function(value, key) + return value, key:sub(2) + end) + + expect(newMap).never.to.equal(map) + expect(newMap:size()).to.equal(3) + expect(newMap:get("pple")).to.equal(1) + local firstKey, firstValue = newMap:first() + expect(firstKey).to.equal("anana") + expect(firstValue).to.equal(10) + local secondKey, secondValue = newMap:getByIndex(2) + expect(secondKey).to.equal("heese") + expect(secondValue).to.equal(5) + local lastKey, lastValue = newMap:last() + expect(lastKey).to.equal("pple") + expect(lastValue).to.equal(1) + end) + + it("should work with an empty OrderedMap", function() + local called = false + local function callback() + called = true + return "placeholderkey", "placeholdervalue" + end + local map = OrderedMap.new() + local newMap = map:map(callback) + expect(newMap:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Filtering", function() + it("should use the callback", function() + local map = OrderedMap.new(sort.default, { + orange = 100, + apple = 1, + grapes = 2, + banana = 10, + lemons = 4, + }) + + local newMap = map:filter(function(value, key) + return value < 9 or key == "orange" + end) + + expect(newMap).never.to.equal(map) + expect(newMap:size()).to.equal(4) + expect(newMap:get("apple")).to.equal(1) + expect(newMap:get("banana")).to.never.be.ok() + expect(map:get("banana")).to.equal(10) + expect(newMap:get("orange")).to.equal(100) + end) + + it("should maintain the sorting invariant", function() + local map = OrderedMap.new(sort.default, { + orange = 100, + apple = 1, + grapes = 2, + banana = 10, + lemons = 4, + }) + + local newMap = map:filter(function(value, key) + return value < 9 or key == "orange" + end) + + local firstKey, firstValue = newMap:first() + expect(firstKey).to.equal("apple") + expect(firstValue).to.equal(1) + local secondKey, secondValue = newMap:getByIndex(2) + expect(secondKey).to.equal("grapes") + expect(secondValue).to.equal(2) + local lastKey, lastValue = newMap:last() + expect(lastKey).to.equal("orange") + expect(lastValue).to.equal(100) + end) + + it("should work with an empty OrderedMap", function() + local called = false + local function callback() + called = true + return true + end + local map = OrderedMap.new() + local newMap = map:filter(callback) + expect(newMap:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Iterators", function() + it("Should work in a for loop", function() + local map = OrderedMap.new(sort.default, { + orange = 100, + apple = 1, + grapes = 2, + banana = 10, + lemons = 4, + }) + local count = 1 + for key, value in map:iterator() do + if count == 1 then + expect(key).to.equal("apple") + expect(value).to.equal(1) + elseif count == 2 then + expect(key).to.equal("banana") + expect(value).to.equal(10) + elseif count == 3 then + expect(key).to.equal("grapes") + expect(value).to.equal(2) + elseif count == 4 then + expect(key).to.equal("lemons") + expect(value).to.equal(4) + elseif count == 5 then + expect(key).to.equal("orange") + expect(value).to.equal(100) + end + count = count + 1 + end + expect(count - 1).to.equal(5) + end) + + it("Should work for empty maps", function() + local map = OrderedMap.new() + local doAnything = false + for _, _ in map:iterator() do + doAnything = true + end + expect(doAnything).to.equal(false) + end) + + it("Should work in reverse", function() + local map = OrderedMap.new(sort.default, { + orange = 100, + apple = 1, + grapes = 2, + banana = 10, + lemons = 4, + }) + local count = 1 + for key, value in map:reverseIterator() do + if count == 5 then + expect(key).to.equal("apple") + expect(value).to.equal(1) + elseif count == 4 then + expect(key).to.equal("banana") + expect(value).to.equal(10) + elseif count == 3 then + expect(key).to.equal("grapes") + expect(value).to.equal(2) + elseif count == 2 then + expect(key).to.equal("lemons") + expect(value).to.equal(4) + elseif count == 1 then + expect(key).to.equal("orange") + expect(value).to.equal(100) + end + count = count + 1 + end + expect(count - 1).to.equal(5) + end) + end) + describe("Downcasting", function() + it("Should successfully downcast to an UnorderedMap", function() + local ordered = OrderedMap.new(sort.default, { + orange = 100, + apple = 1, + grapes = 2, + banana = 10, + lemons = 4, + }) + local unordered = ordered:toUnorderedMap() + expect(UnorderedMap.is(unordered)).to.equal(true) + expect(unordered:get("orange")).to.equal(100) + expect(unordered:get("apple")).to.equal(1) + expect(unordered:get("grapes")).to.equal(2) + expect(unordered:get("banana")).to.equal(10) + expect(unordered:get("lemons")).to.equal(4) + expect(unordered:size()).to.equal(5) + end) + end) + end) + + describe("deepJoin", function() + it("Should work with basic maps", function() + local map1 = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + + local map2 = OrderedMap.new(sort.default, { + apple = 100, + watermelon = 4, + }) + + local newMap = map1:deepJoin(map2) + + expect(newMap:get("apple")).to.equal(100) + expect(newMap:get("watermelon")).to.equal(4) + expect(newMap:get("grapes")).to.equal(2) + expect(newMap:get("banana")).to.equal(10) + expect((newMap:first())).to.equal("apple") + expect((newMap:last())).to.equal("watermelon") + expect(newMap:size()).to.equal(4) + end) + + it("Should work with nested maps", function() + local innerMap1 = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + + local innerMap2 = OrderedMap.new(sort.default, { + peach = -10, + grapes = 15 + }) + + local innerMap3 = OrderedMap.new(sort.default, { + apple = 100, + banana = 1000, + beans = 400, + }) + + local innerMap4 = OrderedMap.new(sort.default, { + peach = 30, + }) + + local outerMap1 = OrderedMap.new(sort.default, { + inner1 = innerMap1, + inner2 = innerMap2, + irrelevantInteger = 5, + }) + + local outerMap2 = OrderedMap.new(sort.default, { + inner1 = innerMap3, + inner2 = innerMap4, + irrelevantString = "hello!", + }) + + local newMap = outerMap1:deepJoin(outerMap2) + expect(newMap:size()).to.equal(4) + expect(newMap:get("irrelevantInteger")).to.equal(5) + expect(newMap:get("irrelevantString")).to.equal("hello!") + + local mergedInner1 = newMap:get("inner1") + local mergedInner2 = newMap:get("inner2") + expect(mergedInner1:size()).to.equal(4) + expect(mergedInner1:get("apple")).to.equal(100) + expect(mergedInner1:get("grapes")).to.equal(2) + expect(mergedInner1:get("banana")).to.equal(1000) + expect(mergedInner1:get("beans")).to.equal(400) + + expect(mergedInner2:size()).to.equal(2) + expect(mergedInner2:get("peach")).to.equal(30) + expect(mergedInner2:get("grapes")).to.equal(15) + end) + + it("Should work with an empty map", function() + local map1 = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + }) + + local map2 = OrderedMap.new(sort.default) + local newMap = map1:deepJoin(map2) + + expect(newMap:get("apple")).to.equal(1) + expect(newMap:get("grapes")).to.equal(2) + expect(newMap:get("banana")).to.equal(10) + end) + + it("Should work with None", function() + local innerMap1 = OrderedMap.new(sort.default, { + apple = 1, + grapes = 2, + banana = 10, + useless = None, + }) + + local innerMap2 = OrderedMap.new(sort.default, { + peach = -10, + grapes = 15, + }) + + local innerMap3 = OrderedMap.new(sort.default, { + apple = None, + banana = 1000, + beans = 400, + }) + + local innerMap4 = OrderedMap.new(sort.default, { + grapes = None, + peach = 30, + }) + + local outerMap1 = OrderedMap.new(sort.default, { + inner1 = innerMap1, + inner2 = innerMap2, + irrelevantInteger = 5, + }) + + local outerMap2 = OrderedMap.new(sort.default, { + inner1 = innerMap3, + inner2 = innerMap4, + irrelevantInteger = None, + }) + + local newMap = outerMap1:deepJoin(outerMap2) + expect(newMap:size()).to.equal(2) + expect(newMap:get("irrelevantInteger")).to.never.be.ok() + + local mergedInner1 = newMap:get("inner1") + local mergedInner2 = newMap:get("inner2") + expect(mergedInner1:size()).to.equal(3) + expect(mergedInner1:get("apple")).to.never.be.ok() + + expect(mergedInner2:size()).to.equal(1) + expect(mergedInner2:get("grapes")).to.never.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedSet/OrderedSet.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedSet/OrderedSet.lua new file mode 100644 index 0000000..5f0074a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedSet/OrderedSet.lua @@ -0,0 +1,278 @@ +local Root = script.Parent.Parent +local OrderedMap = require(Root.OrderedMap.OrderedMap) +local UnorderedSet = require(Root.UnorderedSet.UnorderedSet) +local sort = require(Root.sort.sort) + +local OrderedSet = {} + +OrderedSet.__index = OrderedSet + +--[[ + Create a new OrderedSet from a sortPredicate and any number of keys + sortPredicate should be a function with the following signature: + sortPredicate(key1, key2) -> bool + returning true if key1 < key2 in the sorting invariant. +]] +function OrderedSet.new(sortPredicate, ...) + sortPredicate = sortPredicate or sort.default + local keyMap = OrderedSet._unpackedToTrueMap(...) + + local self = { + internalMap = OrderedMap.new(sortPredicate, keyMap), + _immutableDataStructureType = OrderedSet, + } + + setmetatable(self, OrderedSet) + + return self +end + +--[[ + Returns a new UnorderedSet with the same entries. +]] +function OrderedSet:toUnorderedSet() + local newInternalMap = self.internalMap:toUnorderedMap() + return UnorderedSet._newCannibalizeMap(newInternalMap) +end + +--[[ + Internal method that absorbs an existing OrderedMap as the underlying data structure for a + new OrderedSet. +]] +function OrderedSet._newCannibalizeMap(orderedMap) + local self = { + internalMap = orderedMap, + _immutableDataStructureType = OrderedSet, + } + + setmetatable(self, OrderedSet) + + return self +end + +--[[ + Internal method that takes an unpacked list of values (...) and transforms it into a table + in which each value is a key with value true. +]] +function OrderedSet._unpackedToTrueMap(...) + local keyMap = {} + local len = select("#", ...) + for i = 1, len do + keyMap[select(i, ...)] = true + end + return keyMap +end + +--[[ + Returns if a value is an OrderedSet. +]] +function OrderedSet.is(value) + if type(value) ~= "table" then + return false + end + return value._immutableDataStructureType == OrderedSet +end + +--[[ + Returns true if a key is in the set, false otherwise. +]] +function OrderedSet:find(key) + return self.internalMap:get(key) ~= nil +end + +--[[ + Returns a new OrderedSet, inserting a number of values into the set. +]] +function OrderedSet:insert(...) + local keyMap = OrderedSet._unpackedToTrueMap(...) + local newMap = self.internalMap:batchSet(keyMap) + return OrderedSet._newCannibalizeMap(newMap) +end + +--[[ + Gets the index-th key in the OrderedSet according to the sorting invariant. +]] +function OrderedSet:getByIndex(index) + return (self.internalMap:getByIndex(index)) +end + +--[[ + Returns a copy of the table of all of the keys in the map. +]] +function OrderedSet:getKeys() + return self.internalMap:getKeys() +end + +--[[ + Return the number of keys in the set. +]] +function OrderedSet:size() + return self.internalMap:size() +end + +--[[ + Returns a new OrderedSet, removing a number of values into the set. +]] +function OrderedSet:remove(...) + local newMap = self.internalMap:remove(...) + return OrderedSet._newCannibalizeMap(newMap) +end + +--[[ + Returns a (shallow) copy of the OrderedSet. +]] +function OrderedSet:copy() + local newMap = self.internalMap:copy() + return OrderedSet._newCannibalizeMap(newMap) +end + +--[[ + Returns the first key in the OrderedSet. +]] +function OrderedSet:first() + return (self.internalMap:first()) +end + +--[[ + Returns the last key in the OrderedSet. +]] +function OrderedSet:last() + return (self.internalMap:last()) +end + +--[[ + Create a new OrderedSet, applying the given predicate to each element in + this OrderedSet. + + Predicate should have the signature + predicate(key) -> newKey + + Analogous to 'map' on a list. +]] +function OrderedSet:map(predicate) + local alteredPredicate = function(_, key) + return nil, predicate(key) + end + local newMap = self.internalMap:map(alteredPredicate) + return OrderedSet._newCannibalizeMap(newMap) +end + +--[[ + Create a new OrderedSet, where each key is included iff callback(key) is truthy. + + callback should be a function of signature + callback(key) -> bool +]] +function OrderedSet:filter(callback) + local alteredCallback = function(_, key) + return callback(key) + end + local newMap = self.internalMap:filter(alteredCallback) + return OrderedSet._newCannibalizeMap(newMap) +end + +--[[ + Union any number of OrderedSets. The sorting comparator of the leftmost argument/self is + used for the returned OrderedSet. +]] +function OrderedSet:union(...) + local internalMaps = {} + local len = select("#", ...) + for i = 1, len do + internalMaps[i] = select(i, ...).internalMap + end + local newMap = self.internalMap:join(unpack(internalMaps)) + return OrderedSet._newCannibalizeMap(newMap) +end + +--[[ + Intersect any number of OrderedSets. The sorting comparator of the leftmost argument/self is + used for the returned OrderedSet. +]] +function OrderedSet:intersection(...) + local len = select("#", ...) + local args = { ... } + + local filterer = function(key) + for i = 1, len do + local set = args[i] + if not set:find(key) then + return false + end + end + return true + end + + return self:filter(filterer) +end + +--[[ + Find the difference between any number OrderedSets. The sorting comparator of the leftmost argument/self is + used for the returned OrderedSet. + The behavior of A:difference(B, C, D) is equivalent to the behavior of + A:difference(B:union(C, D)). +]] +function OrderedSet:difference(...) + local newSet = OrderedSet.new(self.sortPredicate) + local len = select("#", ...) + for _, key in self:iterator() do + local shouldRemain = true + for i = 1, len do + if select(i, ...):find(key) then + shouldRemain = false + break + end + end + if shouldRemain then + newSet.internalMap:_insertPairInPlaceUnsorted(key, true) + end + end + newSet.internalMap:_sortInPlace() + return newSet +end + +--[[ + Returns an iterator for the OrderedSet. + Example: + for index, key in orderedSet:iterator() do +]] +function OrderedSet:iterator() + local i = 0 + local length = self.internalMap:size() + + -- Iterator function + return function() + i = i + 1 + if i <= length then + local key = self:getByIndex(i) + return i, key + end + end +end + +--[[ + Returns an iterator for the OrderedSet that traverses in the reverse direction. + Example: + for index, key in orderedSet:reverseIterator() do +]] +function OrderedSet:reverseIterator() + local i = self.internalMap:size() + 1 + + -- Iterator function + return function() + i = i - 1 + if i > 0 then + local key = self:getByIndex(i) + return i, key + end + end +end + +OrderedSet.join = OrderedSet.union + +--[[ + Specify the behavior of deepJoin for OrderedSet. +]] +OrderedSet.deepJoin = OrderedSet.union + +return OrderedSet \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedSet/OrderedSet.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedSet/OrderedSet.spec.lua new file mode 100644 index 0000000..505203c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/OrderedSet/OrderedSet.spec.lua @@ -0,0 +1,359 @@ +return function() + local Root = script.Parent.Parent + local OrderedSet = require(script.Parent.OrderedSet) + local UnorderedSet = require(Root.UnorderedSet.UnorderedSet) + local sort = require(Root.sort.sort) + describe("Basic ordered map operations", function() + + it("Creates a new empty set", function() + local set = OrderedSet.new() + expect(set).to.be.ok() + expect(set:size()).to.equal(0) + end) + + it("Can tell apart OrderedSets from non-OrderedSets", function() + local nonset = {} + expect(OrderedSet.is(nonset)).to.equal(false) + local number = 5 + expect(OrderedSet.is(number)).to.equal(false) + local set = OrderedSet.new() + expect(OrderedSet.is(set)).to.equal(true) + end) + + it("Creates an ordered set with some values", function() + local set = OrderedSet.new(sort.default, 3, 2, 1, 3, 4) + expect(set).to.be.ok() + expect(set:size()).to.equal(4) + expect(set:find(1)).to.equal(true) + expect(set:find(2)).to.equal(true) + expect(set:find(3)).to.equal(true) + expect(set:find(4)).to.equal(true) + + -- Good sorting by key + expect(set:first()).to.equal(1) + expect(set:getByIndex(1)).to.equal(1) + expect(set:getByIndex(2)).to.equal(2) + expect(set:getByIndex(3)).to.equal(3) + expect(set:getByIndex(4)).to.equal(4) + expect(set:last()).to.equal(4) + end) + + it("Returns nil on a miss", function() + local set = OrderedSet.new(sort.default, 3, 2, 1, 3, 4) + expect(set:find(10000)).to.equal(false) + end) + + it("Creates a table with a reverse sort invariant", function() + local set = OrderedSet.new(sort.reverse, 3, 2, 1, 3, 4) + + -- Good sorting by key + expect(set:first()).to.equal(4) + expect(set:getByIndex(4)).to.equal(1) + expect(set:getByIndex(3)).to.equal(2) + expect(set:getByIndex(2)).to.equal(3) + expect(set:getByIndex(1)).to.equal(4) + expect(set:last()).to.equal(1) + end) + + it("Can create a copy", function() + local set = OrderedSet.new(sort.default, 3, 2, 1, 3, 4) + local setCopy = set:copy() + + expect(setCopy).never.to.equal(set) + expect(setCopy).to.be.ok() + expect(setCopy:size()).to.equal(4) + expect(setCopy:find(1)).to.equal(true) + expect(setCopy:find(2)).to.equal(true) + expect(setCopy:find(3)).to.equal(true) + expect(setCopy:find(4)).to.equal(true) + + -- Good sorting by key + expect(setCopy:first()).to.equal(1) + expect(setCopy:getByIndex(1)).to.equal(1) + expect(setCopy:getByIndex(2)).to.equal(2) + expect(setCopy:getByIndex(3)).to.equal(3) + expect(setCopy:getByIndex(4)).to.equal(4) + expect(setCopy:last()).to.equal(4) + end) + + it("Can be converted immutably to a list of keys", function() + local set = OrderedSet.new(sort.default, 10, 11, 11, 10, 15) + local keys = set:getKeys() + + expect(keys:get(1)).to.equal(10) + expect(keys:get(2)).to.equal(11) + expect(keys:get(3)).to.equal(15) + end) + + it("Supports immutable insert", function() + local set = OrderedSet.new(sort.default, 10, 11, 11, 10, 15) + + local newSet = set:insert(12) + expect(newSet).never.to.equal(set) + expect(newSet:find(12)).to.equal(true) + expect(newSet:size()).to.equal(4) + + expect(set:size()).to.equal(3) + expect(set:find(12)).to.equal(false) + + local newNewSet = newSet:insert(100, -100, 25, 10, 100) + expect(newNewSet:size()).to.equal(7) + end) + + it("Supports union", function() + local set1 = OrderedSet.new(sort.default, 4, 3, 2, 1) + + local set2 = OrderedSet.new(sort.default, 3, 4, 5, 6) + + local set3 = OrderedSet.new(sort.default, 3, 6, 100) + + local newSet = set1:union(set2, set3) + + expect(newSet).never.to.equal(set1) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(5)).to.equal(true) + expect(newSet:find(6)).to.equal(true) + expect(newSet:find(100)).to.equal(true) + expect(set1:size()).to.equal(4) + expect(newSet:size()).to.equal(7) + + local emptySet = OrderedSet.new() + local newNewSet = newSet:union(emptySet) + expect(newNewSet:size()).to.equal(7) + end) + + it("Supports deletion", function() + local set = OrderedSet.new(sort.default, 3, 2, 1, 4, 5, 6) + local newSet = set:remove(2, 4) + + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(false) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(false) + expect(newSet:find(5)).to.equal(true) + expect(newSet:find(6)).to.equal(true) + expect(newSet:size()).to.equal(4) + end) + + it("Supports intersection", function() + local set1 = OrderedSet.new(sort.default, 4, 3, 2, 1) + + local set2 = OrderedSet.new(sort.default, 3, 4, 5, 6, 1) + + local set3 = OrderedSet.new(sort.default, 3, 6, 100, 1) + + local newSet = set1:intersection(set2, set3) + + expect(newSet).never.to.equal(set1) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(false) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(false) + expect(newSet:find(5)).to.equal(false) + expect(newSet:find(6)).to.equal(false) + expect(newSet:find(100)).to.equal(false) + expect(set1:size()).to.equal(4) + expect(newSet:size()).to.equal(2) + + local emptySet = OrderedSet.new() + local newNewSet = newSet:intersection(emptySet) + expect(newNewSet:size()).to.equal(0) + end) + + it("Supports set difference", function() + local set1 = OrderedSet.new(sort.default, 10, 4, 3, 2, 1) + + local set2 = OrderedSet.new(sort.default, 3, 4, 5) + + local set3 = OrderedSet.new(sort.default, 100, 101) + + local newSet = set1:difference(set2, set3) + + expect(newSet).never.to.equal(set1) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(10)).to.equal(true) + expect(newSet:find(3)).to.equal(false) + expect(newSet:find(4)).to.equal(false) + expect(newSet:find(5)).to.equal(false) + expect(newSet:find(100)).to.equal(false) + expect(newSet:find(100)).to.equal(false) + expect(newSet:find(101)).to.equal(false) + expect(set1:size()).to.equal(5) + expect(newSet:size()).to.equal(3) + + local emptySet = OrderedSet.new() + local newNewSet = set1:difference(emptySet) + expect(newNewSet:size()).to.equal(5) + end) + end) + + describe("More advanced functionality", function() + describe("Mapping", function() + it("should use the callback", function() + local set = OrderedSet.new(sort.default, 4, 3, 2, 1) + + local newSet = set:map(function(key) + return key * 2 + end) + + expect(newSet:find(1)).to.equal(false) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(6)).to.equal(true) + expect(newSet:find(8)).to.equal(true) + expect(newSet:size()).to.equal(4) + end) + + it("should maintain the sorting invariant", function() + local set = OrderedSet.new(sort.default, 3, 2, 1, 4) + + local newSet = set:map(function(key) + return key % 3 + end) + + expect(newSet:find(0)).to.equal(true) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + + expect(newSet:getByIndex(1)).to.equal(0) + expect(newSet:getByIndex(2)).to.equal(1) + expect(newSet:getByIndex(3)).to.equal(2) + + expect(newSet:size()).to.equal(3) + end) + + it("should work with an empty OrderedSet", function() + local called = false + local function callback() + called = true + return "placeholderkey" + end + local set = OrderedSet.new() + local newSet = set:map(callback) + expect(newSet:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Filtering", function() + it("should use the callback", function() + local set = OrderedSet.new(sort.default, 4, 3, 2, 1) + local newSet = set:filter(function(key) + return key >= 3 + end) + + expect(newSet).never.to.equal(set) + + expect(newSet:size()).to.equal(2) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(1)).to.equal(false) + expect(newSet:find(2)).to.equal(false) + end) + + it("should maintain the sorting invariant", function() + local set = OrderedSet.new(sort.default, 4, 3, 2, 1, 6, 5, 100) + local newSet = set:filter(function(key) + return key >= 3 + end) + + expect(newSet).never.to.equal(set) + + expect(newSet:size()).to.equal(5) + expect(newSet:getByIndex(1)).to.equal(3) + expect(newSet:getByIndex(2)).to.equal(4) + expect(newSet:last()).to.equal(100) + end) + + it("should work with an empty OrderedSet", function() + local called = false + local function callback() + called = true + return true + end + local set = OrderedSet.new() + local newSet = set:filter(callback) + expect(newSet:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Iterators", function() + it("Should work in a for loop", function() + local set = OrderedSet.new(sort.default, 8, 3, 2, 1, 100) + local count = 1 + for index, key in set:iterator() do + if count == 1 then + expect(index).to.equal(1) + expect(key).to.equal(1) + elseif count == 2 then + expect(index).to.equal(2) + expect(key).to.equal(2) + elseif count == 3 then + expect(index).to.equal(3) + expect(key).to.equal(3) + elseif count == 4 then + expect(index).to.equal(4) + expect(key).to.equal(8) + elseif count == 5 then + expect(index).to.equal(5) + expect(key).to.equal(100) + end + count = count + 1 + end + expect(count - 1).to.equal(5) + end) + + it("Should work for empty sets", function() + local set = OrderedSet.new() + local doAnything = false + for _, _ in set:iterator() do + doAnything = true + end + expect(doAnything).to.equal(false) + end) + + it("Should work in reverse", function() + local set = OrderedSet.new(sort.default, 8, 3, 2, 1, 100) + local count = 1 + for index, key in set:reverseIterator() do + if count == 5 then + expect(index).to.equal(1) + expect(key).to.equal(1) + elseif count == 4 then + expect(index).to.equal(2) + expect(key).to.equal(2) + elseif count == 3 then + expect(index).to.equal(3) + expect(key).to.equal(3) + elseif count == 2 then + expect(index).to.equal(4) + expect(key).to.equal(8) + elseif count == 1 then + expect(index).to.equal(5) + expect(key).to.equal(100) + end + count = count + 1 + end + expect(count - 1).to.equal(5) + end) + end) + + describe("Downcasting", function() + it("Should successfully downcast to an UnorderedSet", function() + local ordered = OrderedSet.new(sort.default, 5, 4, 3, 2, 1) + local unordered = ordered:toUnorderedSet() + expect(UnorderedSet.is(unordered)).to.equal(true) + for i = 5, 1, -1 do + expect(unordered:find(i)).to.equal(true) + end + expect(unordered:size()).to.equal(5) + end) + end) + + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedMap/UnorderedMap.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedMap/UnorderedMap.lua new file mode 100644 index 0000000..bc2b46b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedMap/UnorderedMap.lua @@ -0,0 +1,303 @@ +local Root = script.Parent.Parent +local Cryo = require(Root.Parent.Cryo) +local List = require(Root.List.List) +local None = require(Root.None) +local deepJoin = require(Root.deepJoin.deepJoin) + +local UnorderedMap = {} + +UnorderedMap.__index = UnorderedMap + +--[[ + Create a new UnorderedMap from any number of dictionaries of values of the form + { [key] = value } +]] +function UnorderedMap.new(...) + local self = { + pairs = {}, + length = 0, + _immutableDataStructureType = UnorderedMap + } + + setmetatable(self, UnorderedMap) + + UnorderedMap._insertInPlace(self, ...) + + return self +end + +--[[ + Internal method that absorbs an existing table and constructs a new UnorderdMap + from it as the underlying data. +]] +function UnorderedMap._newCannibalizeTable(tab) + local count = 0 + for _ in pairs(tab) do + count = count + 1 + end + local self = { + pairs = tab, + length = count, + _immutableDataStructureType = UnorderedMap + } + + setmetatable(self, UnorderedMap) + + return self +end + +--[[ + Returns if a value is an UnorderedMap. +]] +function UnorderedMap.is(value) + if type(value) ~= "table" then + return false + end + return value._immutableDataStructureType == UnorderedMap +end + +--[[ + Returns the value at key. +]] +function UnorderedMap:get(key) + return self.pairs[key] +end + +--[[ + Returns a new UnorderedMap, setting the value at key to be value. +]] +function UnorderedMap:set(key, value) + if self:get(key) == nil then + self.length = self.length + 1 + end + local new = self:copy() + new.pairs[key] = value + return new +end + +--[[ + Returns a List of all of the values in the map. +]] +function UnorderedMap:getValues() + return List._newCannibalizeTable(Cryo.Dictionary.values(self.pairs)) +end + +--[[ + Returns a List of all of the keys in the map. +]] +function UnorderedMap:getKeys() + return List._newCannibalizeTable(Cryo.Dictionary.keys(self.pairs)) +end + +--[[ + Returns the size (number of key-value pairs) of the map. +]] +function UnorderedMap:size() + return self.length +end + +--[[ + Returns a new UnorderedMap with the pairs at all given keys removed. +]] +function UnorderedMap:remove(...) + local new = self:copy() + + local newPairs = new.pairs + + local len = select("#", ...) + for i = 1, len do + local key = select(i, ...) + if new:get(key) ~= nil then + new.length = new.length - 1 + newPairs[key] = nil + end + end + return new +end + +--[[ + Joins any number of (basic table) dictionaries of values of the form + { [key] = value } + into the OrderedMap, creating a new OrderedMap. +]] +function UnorderedMap:batchSet(...) + local new = self:copyRemoveNone() + new:_insertInPlaceRemoveNone(...) + return new +end + +--[[ + Returns a (shallow) copy of the UnorderedMap. +]] +function UnorderedMap:copy() + return UnorderedMap.new(self.pairs) +end + +--[[ + Returns a (shallow) copy of the UnorderedMap, removing all None instances. +]] +function UnorderedMap:copyRemoveNone() + local new = {} + for key, value in self:iterator() do + if value ~= None then + new[key] = value + end + end + return UnorderedMap._newCannibalizeTable(new) +end + +--[[ + Create a new UnorderedMap, applying the given predicate to each element in + this UnorderedMap. + + Predicate should have the signature + predicate(value, key) -> newValue, newKey + + Analogous to 'map' on a list. + +]] +function UnorderedMap:map(predicate) + local new = UnorderedMap.new() + for key, value in self:iterator() do + local newValue, newKey = predicate(value, key) + newKey = newKey or key + newValue = newValue or value + new:_insertPairInPlace(newKey, newValue) + end + + return new +end + +--[[ + Create a new UnorderedMap, where each key-value pair is included iff callback(value, key) is truthy. + + callback should be a function of signature + callback(value, key) -> bool +]] +function UnorderedMap:filter(callback) + local new = UnorderedMap.new() + for key, value in self:iterator() do + if callback(value, key) then + new:_insertPairInPlace(key, value) + end + end + + return new +end + +--[[ + Join any number of UnorderedMaps. +]] +function UnorderedMap:join(...) + local new = self:copyRemoveNone() + + for i = 1, select("#", ...) do + local other = select(i, ...) + + if other:size() > 0 then + for key, value in other:iterator() do + new:_insertPairInPlaceRemoveNone(key, value) + end + end + end + + return new +end + +--[[ + Internal method for inserting values in place. +]] +function UnorderedMap:_insertInPlace(...) + local len = select("#", ...) + for i = 1, len do + local pair = select(i, ...) + for key, value in pairs(pair) do + self:_insertPairInPlace(key, value) + end + end +end + +--[[ + Internal method for inserting a single pair in place. +]] +function UnorderedMap:_insertPairInPlace(key, value) + if self:get(key) == nil then + self.length = self.length + 1 + end + self.pairs[key] = value +end + +--[[ + Internal method for inserting values in place, removing all None instances. +]] +function UnorderedMap:_insertInPlaceRemoveNone(...) + local len = select("#", ...) + for i = 1, len do + local pair = select(i, ...) + for key, value in pairs(pair) do + self:_insertPairInPlaceRemoveNone(key, value) + end + end +end + +--[[ + Internal method for inserting values in place, removing all None instances. +]] +function UnorderedMap:_insertPairInPlaceRemoveNone(key, value) + if value == None then + if self:get(key) ~= nil then + self.pairs[key] = nil + self.length = self.length - 1 + end + else + if self:get(key) == nil then + self.length = self.length + 1 + end + self.pairs[key] = value + end +end + +--[[ + Specify the behavior of deepJoin for UnorderedMap. +]] +function UnorderedMap:deepJoin(rhs) + local newMap = UnorderedMap.new() + for lhsKey, lhsValue in self:iterator() do + local rhsValue = rhs:get(lhsKey) + if rhsValue then + if type(rhsValue) == "table" and type(lhsValue) == "table" then + newMap:_insertPairInPlace(lhsKey, deepJoin(lhsValue, rhsValue)) + else + if rhsValue ~= None then + newMap:_insertPairInPlace(lhsKey, rhsValue) + end + end + else + if lhsValue ~= None then + newMap:_insertPairInPlace(lhsKey, lhsValue) + end + end + end + + -- Copy over rhs keys that aren't in lhs + for rhsKey, rhsValue in rhs:iterator() do + local lhsValue = self:get(rhsKey) + if not lhsValue and rhsValue ~= None then + newMap:_insertPairInPlace(rhsKey, rhsValue) + end + end + + return newMap +end + +--[[ + Returns an iterator for the UnorderedMap. + Key-value pairs are returned in an undefined order. + Example: + for key, value in unorderedMap:iterator() do +]] +function UnorderedMap:iterator() + return next, self.pairs, nil +end + +return UnorderedMap \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedMap/UnorderedMap.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedMap/UnorderedMap.spec.lua new file mode 100644 index 0000000..6b4d8b1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedMap/UnorderedMap.spec.lua @@ -0,0 +1,476 @@ +return function() + local Root = script.Parent.Parent + local UnorderedMap = require(script.Parent.UnorderedMap) + local None = require(Root.None) + + describe("Basic unordered map operations", function() + + it("Creates a new empty table", function() + local map = UnorderedMap.new() + expect(map).to.be.ok() + expect(map:size()).to.equal(0) + end) + + it("Can tell apart UnorderedMaps from non-UnorderedMaps", function() + local nonmap = {} + expect(UnorderedMap.is(nonmap)).to.equal(false) + local number = 5 + expect(UnorderedMap.is(number)).to.equal(false) + local map = UnorderedMap.new() + expect(UnorderedMap.is(map)).to.equal(true) + end) + + it("Creates an ordered with some values", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + expect(map).to.be.ok() + expect(map:size()).to.equal(3) + expect(map:get("apple")).to.equal(1) + expect(map:get("grapes")).to.equal(2) + expect(map:get("banana")).to.equal(10) + end) + + it("Returns nil on a miss", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + expect(map:get("broccoli")).never.to.be.ok() + end) + + it("Creates a table with some values from multiple tables", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 10, + banana = 2, + }, { + orange = 5, + banana = 5, + }) + expect(map).to.be.ok() + expect(map:size()).to.equal(4) + expect(map:get("orange")).to.equal(5) + expect(map:get("grapes")).to.equal(10) + expect(map:get("banana")).to.equal(5) + end) + + it("Can create a copy", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + local mapCopy = map:copy() + + expect(mapCopy).never.to.equal(map) + expect(mapCopy).to.be.ok() + expect(mapCopy:size()).to.equal(3) + expect(mapCopy:get("apple")).to.equal(1) + expect(mapCopy:get("grapes")).to.equal(2) + expect(mapCopy:get("banana")).to.equal(10) + end) + + it("Can be converted immutably to lists of keys and values", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + local keys = map:getKeys() + local values = map:getValues() + + expect(keys:find("apple")).to.be.ok() + expect(keys:find("banana")).to.be.ok() + expect(keys:find("grapes")).to.be.ok() + expect(keys:size()).to.equal(3) + expect(values:find(1)).to.be.ok() + expect(values:find(2)).to.be.ok() + expect(values:find(10)).to.be.ok() + expect(values:size()).to.equal(3) + end) + + it("Supports immutable set", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + + local newMap = map:set("apple", 1000) + + expect(newMap).never.to.equal(map) + expect(map:get("apple")).to.equal(1) + expect(newMap:get("apple")).to.equal(1000) + + local newNewMap = newMap:set("lettuce", -100) + expect(newNewMap:get("lettuce")).to.equal(-100) + end) + + it("Supports batch set", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + + local newMap = map:batchSet({ + apple = 100, + watermelon = 4, + }, { + watermelon = 100, + kiwi = 3, + }) + + expect(newMap).never.to.equal(map) + expect(map:get("apple")).to.equal(1) + expect(newMap:get("apple")).to.equal(100) + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect(map:get("kiwi")).to.never.be.ok() + expect(newMap:size()).to.equal(5) + end) + + it("Supports join", function() + local map1 = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + + local map2 = UnorderedMap.new({ + apple = 100, + watermelon = 4, + }) + + local map3 = UnorderedMap.new({ + watermelon = 100, + kiwi = 3, + }) + + local newMap = map1:join(map2, map3) + + expect(newMap).never.to.equal(map1) + expect(map1:get("apple")).to.equal(1) + expect(newMap:get("apple")).to.equal(100) + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect(map1:get("kiwi")).to.never.be.ok() + expect(newMap:size()).to.equal(5) + end) + + it("Supports None", function() + local map1 = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + useless = None, + }) + expect(map1:size()).to.equal(4) + + local map2 = UnorderedMap.new({ + apple = None, + watermelon = 4, + }) + + local map3 = UnorderedMap.new({ + watermelon = 100, + kiwi = 3, + }) + + local newMap = map1:join(map2, map3) + + expect(newMap).never.to.equal(map1) + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect(newMap:get("useless")).to.never.be.ok() + expect(newMap:size()).to.equal(4) + + newMap = map1:batchSet({ + apple = None, + watermelon = 4, + }, { + watermelon = 100, + kiwi = 3, + }) + + expect(newMap).never.to.equal(map1) + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("watermelon")).to.equal(100) + expect(newMap:get("kiwi")).to.equal(3) + expect(newMap:get("useless")).to.never.be.ok() + expect(newMap:size()).to.equal(4) + end) + + + it("Supports deletion", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + local newMap = map:remove("apple", "grapes") + + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("grapes")).to.never.be.ok() + expect(newMap:get("banana")).to.equal(10) + expect(map:get("apple")).to.be.ok() + expect(map:get("grapes")).to.be.ok() + expect(map:get("banana")).to.be.ok() + expect(newMap:size()).to.equal(1) + end) + end) + + describe("More advanced functionality", function() + describe("Mapping", function() + it("should use the callback", function() + local map = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + + local newMap = map:map(function(value, key) + return value * 2, key .. " fruit" + end) + + expect(newMap).never.to.equal(map) + expect(newMap:size()).to.equal(3) + expect(map:get("apple fruit")).to.never.be.ok() + expect(newMap:get("apple fruit")).to.equal(2) + expect(newMap:get("apple")).to.never.be.ok() + expect(newMap:get("banana fruit")).to.equal(20) + end) + + it("should work with an empty UnorderedMap", function() + local called = false + local function callback() + called = true + return "placeholderkey", "placeholdervalue" + end + local map = UnorderedMap.new() + local newMap = map:map(callback) + expect(newMap:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Filtering", function() + it("should use the callback", function() + local map = UnorderedMap.new({ + orange = 100, + apple = 1, + grapes = 2, + banana = 10, + lemons = 4, + }) + + local newMap = map:filter(function(value, key) + return value < 9 or key == "orange" + end) + + expect(newMap).never.to.equal(map) + expect(newMap:size()).to.equal(4) + expect(newMap:get("apple")).to.equal(1) + expect(newMap:get("banana")).to.never.be.ok() + expect(map:get("banana")).to.equal(10) + expect(newMap:get("orange")).to.equal(100) + end) + + it("should work with an empty OrderedMap", function() + local called = false + local function callback() + called = true + return true + end + local map = UnorderedMap.new() + local newMap = map:filter(callback) + expect(newMap:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Iterators", function() + it("Should work in a for loop", function() + local map = UnorderedMap.new({ + orange = 100, + apple = 1, + grapes = 2, + banana = 10, + lemons = 4, + }) + local count = 0 + for key, value in map:iterator() do + if key == "orange" then + expect(value).to.equal(100) + elseif key == "apple" then + expect(value).to.equal(1) + elseif key == "grapes" then + expect(value).to.equal(2) + elseif key == "banana" then + expect(value).to.equal(10) + elseif key == "lemons" then + expect(value).to.equal(4) + else + error("There should not be such a key") + end + count = count + 1 + end + expect(count).to.equal(5) + end) + + it("Should work for empty maps", function() + local map = UnorderedMap.new() + local doAnything = false + for _, _ in map:iterator() do + doAnything = true + end + expect(doAnything).to.equal(false) + end) + end) + end) + + describe("deepJoin", function() + it("Should work with basic maps", function() + local map1 = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + + local map2 = UnorderedMap.new({ + apple = 100, + watermelon = 4, + }) + + local newMap = map1:deepJoin(map2) + + expect(newMap:get("apple")).to.equal(100) + expect(newMap:get("grapes")).to.equal(2) + expect(newMap:get("banana")).to.equal(10) + expect(newMap:get("watermelon")).to.equal(4) + + expect(newMap:size()).to.equal(4) + end) + + it("Should work with nested maps", function() + local innerMap1 = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + + local innerMap2 = UnorderedMap.new({ + peach = -10, + grapes = 15 + }) + + local innerMap3 = UnorderedMap.new({ + apple = 100, + banana = 1000, + beans = 400, + }) + + local innerMap4 = UnorderedMap.new({ + peach = 30, + }) + + local outerMap1 = UnorderedMap.new({ + inner1 = innerMap1, + inner2 = innerMap2, + irrelevantInteger = 5, + }) + + local outerMap2 = UnorderedMap.new({ + inner1 = innerMap3, + inner2 = innerMap4, + irrelevantString = "hello!", + }) + + local newMap = outerMap1:deepJoin(outerMap2) + expect(newMap:size()).to.equal(4) + expect(newMap:get("irrelevantInteger")).to.equal(5) + expect(newMap:get("irrelevantString")).to.equal("hello!") + + local mergedInner1 = newMap:get("inner1") + local mergedInner2 = newMap:get("inner2") + expect(mergedInner1:size()).to.equal(4) + expect(mergedInner1:get("apple")).to.equal(100) + expect(mergedInner1:get("grapes")).to.equal(2) + expect(mergedInner1:get("banana")).to.equal(1000) + expect(mergedInner1:get("beans")).to.equal(400) + + expect(mergedInner2:size()).to.equal(2) + expect(mergedInner2:get("peach")).to.equal(30) + expect(mergedInner2:get("grapes")).to.equal(15) + end) + + it("Should work with an empty map", function() + local map1 = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + }) + + local map2 = UnorderedMap.new() + local newMap = map1:deepJoin(map2) + + expect(newMap:get("apple")).to.equal(1) + expect(newMap:get("grapes")).to.equal(2) + expect(newMap:get("banana")).to.equal(10) + end) + + it("Should work with None", function() + local innerMap1 = UnorderedMap.new({ + apple = 1, + grapes = 2, + banana = 10, + useless = None, + }) + + local innerMap2 = UnorderedMap.new({ + peach = -10, + grapes = 15, + }) + + local innerMap3 = UnorderedMap.new({ + apple = None, + banana = 1000, + beans = 400, + }) + + local innerMap4 = UnorderedMap.new({ + grapes = None, + peach = 30, + }) + + local outerMap1 = UnorderedMap.new({ + inner1 = innerMap1, + inner2 = innerMap2, + irrelevantInteger = 5, + }) + + local outerMap2 = UnorderedMap.new({ + inner1 = innerMap3, + inner2 = innerMap4, + irrelevantInteger = None, + }) + + local newMap = outerMap1:deepJoin(outerMap2) + expect(newMap:size()).to.equal(2) + expect(newMap:get("irrelevantInteger")).to.never.be.ok() + + local mergedInner1 = newMap:get("inner1") + local mergedInner2 = newMap:get("inner2") + expect(mergedInner1:size()).to.equal(3) + expect(mergedInner1:get("apple")).to.never.be.ok() + + expect(mergedInner2:size()).to.equal(1) + expect(mergedInner2:get("grapes")).to.never.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedSet/UnorderedSet.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedSet/UnorderedSet.lua new file mode 100644 index 0000000..9456fa2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedSet/UnorderedSet.lua @@ -0,0 +1,216 @@ +local Root = script.Parent.Parent +local UnorderedMap = require(Root.UnorderedMap.UnorderedMap) + +local UnorderedSet = {} + +UnorderedSet.__index = UnorderedSet + +--[[ + Create a new UnorderedSet from any number of keys +]] +function UnorderedSet.new(...) + local keyMap = UnorderedSet._unpackedToTrueMap(...) + + local self = { + internalMap = UnorderedMap.new(keyMap), + _immutableDataStructureType = UnorderedSet, + } + + setmetatable(self, UnorderedSet) + + return self +end + +--[[ + Internal method that absorbs an existing Unorderedmap as the underlying data structure for a + new UnorderedSet. +]] +function UnorderedSet._newCannibalizeMap(unorderedMap) + local self = { + internalMap = unorderedMap, + _immutableDataStructureType = UnorderedSet, + } + + setmetatable(self, UnorderedSet) + + return self +end + +--[[ + Internal method that takes an unpacked list of values (...) and transforms it into a table + in which each value is a key with value true. +]] +function UnorderedSet._unpackedToTrueMap(...) + local keyMap = {} + local len = select("#", ...) + for i = 1, len do + keyMap[select(i, ...)] = true + end + return keyMap +end + +--[[ + Returns if a value is an UnorderedSet. +]] +function UnorderedSet.is(value) + if type(value) ~= "table" then + return false + end + return value._immutableDataStructureType == UnorderedSet +end + +--[[ + Returns true if a key is in the set, false otherwise. +]] +function UnorderedSet:find(id) + return self.internalMap:get(id) ~= nil +end + +--[[ + Returns a new UnorderedSet, inserting a number of values into the set. +]] +function UnorderedSet:insert(...) + local keyMap = UnorderedSet._unpackedToTrueMap(...) + local newMap = self.internalMap:batchSet(keyMap) + return UnorderedSet._newCannibalizeMap(newMap) +end + +--[[ + Returns a copy of the table of all of the keys in the map. +]] +function UnorderedSet:getKeys() + return self.internalMap:getKeys() +end + +--[[ + Return the number of keys in the set. +]] +function UnorderedSet:size() + return self.internalMap:size() +end + +--[[ + Returns a new UnorderedSet, removing a number of values into the set. +]] +function UnorderedSet:remove(...) + local newMap = self.internalMap:remove(...) + return UnorderedSet._newCannibalizeMap(newMap) +end + +--[[ + Returns a (shallow) copy of the UnorderedSet. +]] +function UnorderedSet:copy() + local newMap = self.internalMap:copy() + return UnorderedSet._newCannibalizeMap(newMap) +end + +--[[ + Create a new UnorderedSet, applying the given predicate to each element in + this OrderedSet. + + Predicate should have the signature + predicate(key) -> newKey + + Analogous to 'map' on a list. +]] +function UnorderedSet:map(predicate) + local alteredPredicate = function(_, key) + return nil, predicate(key) + end + local newMap = self.internalMap:map(alteredPredicate) + return UnorderedSet._newCannibalizeMap(newMap) +end + +--[[ + Create a new OrderedSet, where each key is included iff callback(key) is truthy. + + callback should be a function of signature + callback(key) -> bool +]] +function UnorderedSet:filter(callback) + local alteredCallback = function(_, key) + return callback(key) + end + local newMap = self.internalMap:filter(alteredCallback) + return UnorderedSet._newCannibalizeMap(newMap) +end + +--[[ + Union any number of UnorderedSets. +]] +function UnorderedSet:union(...) + local internalMaps = {} + local len = select("#", ...) + for i = 1, len do + internalMaps[i] = select(i, ...).internalMap + end + local newMap = self.internalMap:join(unpack(internalMaps)) + return UnorderedSet._newCannibalizeMap(newMap) +end + +--[[ + Find the difference between any number UnorderedSets. + The behavior of A:difference(B, C, D) is equivalent to the behavior of + A:difference(B:union(C, D)). +]] +function UnorderedSet:difference(...) + local newSet = UnorderedSet.new() + local len = select("#", ...) + for key in self:iterator() do + local shouldRemain = true + for i = 1, len do + if select(i, ...):find(key) then + shouldRemain = false + break + end + end + if shouldRemain then + newSet.internalMap:_insertPairInPlace(key, true) + end + end + return newSet +end + + +--[[ + Intersect any number of UnorderedSets. +]] +function UnorderedSet:intersection(...) + local len = select("#", ...) + local args = { ... } + + local filterer = function(key) + for i = 1, len do + local set = args[i] + if not set:find(key) then + return false + end + end + return true + end + + return self:filter(filterer) +end + +--[[ + Returns an iterator for the UnorderedSet. + Keys are returned in an undefined order. + Example: + for key in orderedSet:iterator() do +]] +function UnorderedSet:iterator() + local alteredNext = function(table, key) + return (next(table, key)) + end + return alteredNext, self.internalMap.pairs +end + +--[[ + Specify the behavior of deepJoin for UnorderedSet. +]] +UnorderedSet.deepJoin = UnorderedSet.union + +UnorderedSet.join = UnorderedSet.union + +return UnorderedSet \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedSet/UnorderedSet.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedSet/UnorderedSet.spec.lua new file mode 100644 index 0000000..ece985d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/UnorderedSet/UnorderedSet.spec.lua @@ -0,0 +1,255 @@ +return function() + local UnorderedSet = require(script.Parent.UnorderedSet) + describe("Basic ordered map operations", function() + + it("Creates a new empty set", function() + local set = UnorderedSet.new() + expect(set).to.be.ok() + expect(set:size()).to.equal(0) + end) + + it("Can tell apart UnorderedSets from non-UnorderedSets", function() + local nonset = {} + expect(UnorderedSet.is(nonset)).to.equal(false) + local number = 5 + expect(UnorderedSet.is(number)).to.equal(false) + local set = UnorderedSet.new() + expect(UnorderedSet.is(set)).to.equal(true) + end) + + it("Creates an unordered set with some values", function() + local set = UnorderedSet.new(3, 2, 1, 3, 4) + expect(set).to.be.ok() + expect(set:size()).to.equal(4) + expect(set:find(1)).to.equal(true) + expect(set:find(2)).to.equal(true) + expect(set:find(3)).to.equal(true) + expect(set:find(4)).to.equal(true) + end) + + it("Returns nil on a miss", function() + local set = UnorderedSet.new(3, 2, 1, 3, 4) + expect(set:find(10000)).to.equal(false) + end) + + it("Can create a copy", function() + local set = UnorderedSet.new(3, 2, 1, 3, 4) + local setCopy = set:copy() + + expect(setCopy).never.to.equal(set) + expect(setCopy).to.be.ok() + expect(setCopy:size()).to.equal(4) + expect(setCopy:find(1)).to.equal(true) + expect(setCopy:find(2)).to.equal(true) + expect(setCopy:find(3)).to.equal(true) + expect(setCopy:find(4)).to.equal(true) + end) + + it("Can be converted immutably to a List of keys", function() + local set = UnorderedSet.new(10, 11, 11, 10, 15) + local keys = set:getKeys() + expect(keys:size()).to.equal(3) + end) + + it("Supports immutable insert", function() + local set = UnorderedSet.new(10, 11, 11, 10, 15) + + local newSet = set:insert(12) + expect(newSet).never.to.equal(set) + expect(newSet:find(12)).to.equal(true) + expect(newSet:size()).to.equal(4) + + expect(set:size()).to.equal(3) + expect(set:find(12)).to.equal(false) + + local newNewSet = newSet:insert(100, -100, 25, 10, 100) + expect(newNewSet:size()).to.equal(7) + end) + + it("Supports union", function() + local set1 = UnorderedSet.new(4, 3, 2, 1) + + local set2 = UnorderedSet.new(3, 4, 5, 6) + + local set3 = UnorderedSet.new(3, 6, 100) + + local newSet = set1:union(set2, set3) + + expect(newSet).never.to.equal(set1) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(5)).to.equal(true) + expect(newSet:find(6)).to.equal(true) + expect(newSet:find(100)).to.equal(true) + expect(set1:size()).to.equal(4) + expect(newSet:size()).to.equal(7) + + local emptySet = UnorderedSet.new() + local newNewSet = newSet:union(emptySet) + expect(newNewSet:size()).to.equal(7) + end) + + it("Supports deletion", function() + local set = UnorderedSet.new(3, 2, 1, 4, 5, 6) + local newSet = set:remove(2, 4) + + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(false) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(false) + expect(newSet:find(5)).to.equal(true) + expect(newSet:find(6)).to.equal(true) + expect(newSet:size()).to.equal(4) + end) + + it("Supports intersection", function() + local set1 = UnorderedSet.new(4, 3, 2, 1) + + local set2 = UnorderedSet.new(3, 4, 5, 6, 1) + + local set3 = UnorderedSet.new(3, 6, 100, 1) + + local newSet = set1:intersection(set2, set3) + + expect(newSet).never.to.equal(set1) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(false) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(false) + expect(newSet:find(5)).to.equal(false) + expect(newSet:find(6)).to.equal(false) + expect(newSet:find(100)).to.equal(false) + expect(set1:size()).to.equal(4) + expect(newSet:size()).to.equal(2) + + local emptySet = UnorderedSet.new() + local newNewSet = newSet:intersection(emptySet) + expect(newNewSet:size()).to.equal(0) + end) + + it("Supports set difference", function() + local set1 = UnorderedSet.new(10, 4, 3, 2, 1) + + local set2 = UnorderedSet.new(3, 4, 5) + + local set3 = UnorderedSet.new(100, 101) + + local newSet = set1:difference(set2, set3) + + expect(newSet).never.to.equal(set1) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(10)).to.equal(true) + expect(newSet:find(3)).to.equal(false) + expect(newSet:find(4)).to.equal(false) + expect(newSet:find(5)).to.equal(false) + expect(newSet:find(100)).to.equal(false) + expect(newSet:find(100)).to.equal(false) + expect(newSet:find(101)).to.equal(false) + expect(set1:size()).to.equal(5) + expect(newSet:size()).to.equal(3) + + local emptySet = UnorderedSet.new() + local newNewSet = set1:difference(emptySet) + expect(newNewSet:size()).to.equal(5) + end) + end) + + describe("More advanced functionality", function() + describe("Mapping", function() + it("should use the callback", function() + local set = UnorderedSet.new(4, 3, 2, 1) + + local newSet = set:map(function(key) + return key * 2 + end) + + expect(newSet:find(1)).to.equal(false) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(6)).to.equal(true) + expect(newSet:find(8)).to.equal(true) + expect(newSet:size()).to.equal(4) + end) + + it("should work with an empty UnorderedSet", function() + local called = false + local function callback() + called = true + return "placeholderkey" + end + local set = UnorderedSet.new() + local newSet = set:map(callback) + expect(newSet:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Filtering", function() + it("should use the callback", function() + local set = UnorderedSet.new(4, 3, 2, 1) + local newSet = set:filter(function(key) + return key >= 3 + end) + + expect(newSet).never.to.equal(set) + + expect(newSet:size()).to.equal(2) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(1)).to.equal(false) + expect(newSet:find(2)).to.equal(false) + end) + + it("should work with an empty UnorderedSet", function() + local called = false + local function callback() + called = true + return true + end + local set = UnorderedSet.new() + local newSet = set:filter(callback) + expect(newSet:size()).to.equal(0) + expect(called).to.equal(false) + end) + end) + + describe("Iterators", function() + it("Should work in a for loop", function() + local set = UnorderedSet.new(8, 3, 2, 1, 100) + local seen = {} + local count = 0 + for key in set:iterator() do + seen[key] = true + count = count + 1 + end + expect(count).to.equal(5) + expect(seen[8]).to.equal(true) + expect(seen[3]).to.equal(true) + expect(seen[2]).to.equal(true) + expect(seen[1]).to.equal(true) + expect(seen[100]).to.equal(true) + end) + + it("Should work for empty sets", function() + local set = UnorderedSet.new() + local doAnything = false + for _ in set:iterator() do + doAnything = true + end + expect(doAnything).to.equal(false) + end) + + it("Should only provide keys", function() + local set = UnorderedSet.new(1, 2) + for key, value in set:iterator() do + expect(value).to.never.be.ok() + expect(key).to.be.ok() + end + end) + end) + + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/binarySearch/binarySearch.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/binarySearch/binarySearch.lua new file mode 100644 index 0000000..faeafef --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/binarySearch/binarySearch.lua @@ -0,0 +1,29 @@ +--[[ + Utility that returns the leftmost index of the occurence of value in a sorted list according to sortPredicate, + if provided, otherwise <. If not found, returns nil. +]] + +local function binarySearch(list, value, sortPredicate) + sortPredicate = sortPredicate or function(lhs, rhs) + return lhs < rhs + end + local low = 1 + local high = #list + while low <= high do + local mid = low + math.floor((high - low) / 2) + if sortPredicate(value, list[mid]) then + high = mid - 1 + elseif sortPredicate(list[mid], value) then + low = mid + 1 + else + -- Go as left as we can + while mid >= 1 and not (sortPredicate(list[mid], value) or sortPredicate(value, list[mid])) do + mid = mid - 1 + end + return mid + 1 + end + end + return nil +end + +return binarySearch \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/binarySearch/binarySearch.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/binarySearch/binarySearch.spec.lua new file mode 100644 index 0000000..4f00380 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/binarySearch/binarySearch.spec.lua @@ -0,0 +1,38 @@ +return function() + local binarySearch = require(script.Parent.binarySearch) + it("Works for a sorted list", function() + local list = { 1, 3, 5, 6, 7 } + expect(binarySearch(list, 1)).to.equal(1) + expect(binarySearch(list, 3)).to.equal(2) + expect(binarySearch(list, 5)).to.equal(3) + expect(binarySearch(list, 6)).to.equal(4) + expect(binarySearch(list, 7)).to.equal(5) + expect(binarySearch(list, 100)).to.never.be.ok() + end) + + it("Works for a sorted list with duplicates", function() + local list = { 1, 1, 3, 3, 3, 5, 5, 10, 11 } + expect(binarySearch(list, 1)).to.equal(1) + expect(binarySearch(list, 3)).to.equal(3) + expect(binarySearch(list, 5)).to.equal(6) + expect(binarySearch(list, 10)).to.equal(8) + expect(binarySearch(list, 11)).to.equal(9) + end) + + it("Works for an empty list", function() + local list = {} + expect(binarySearch(list, 1)).to.never.be.ok() + end) + + it("Works for a special comparator", function() + local list = { 7, 6, 5, 5, 3, 1 } + local reverse = function(lhs, rhs) + return rhs < lhs + end + expect(binarySearch(list, 7, reverse)).to.equal(1) + expect(binarySearch(list, 6, reverse)).to.equal(2) + expect(binarySearch(list, 5, reverse)).to.equal(3) + expect(binarySearch(list, 3, reverse)).to.equal(5) + expect(binarySearch(list, 1, reverse)).to.equal(6) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/deepJoin/deepJoin.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/deepJoin/deepJoin.lua new file mode 100644 index 0000000..ea98905 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/deepJoin/deepJoin.lua @@ -0,0 +1,50 @@ +local Root = script.Parent.Parent +local Cryo = require(Root.Parent.Cryo) +local None = require(Root.None) +local _deepJoinHelper + +--[[ + deepJoin deeply merges any number of immutable data structures. + It conforms to the following behavior: + 1. UnorderedMaps and OrderedMaps are merged with pairs in later maps overwriting pairs in earlier maps. Values + are recursively deepJoined. None is respected. UnorderedMaps will not be joined with OrderedMaps, and vice versa. + 2. UnorderedSets and OrderedSets are unioned. UnorderedSets will not be unioned with OrderedSets, and vice versa. + 3. Lists are joined (i.e. appended) to one another according to List:join(). + 4. Joining two differently typed (i.e. OrderedSet and UnorderedSet) immutable data structures will return the + RHS immutable data structure. + 5. All other values are overwritten, taking the furthest right value. +]] +local function deepJoin(...) + local values = { ... } + if #values == 0 then + return nil + end + return Cryo.List.foldLeft(values, function(accumulator, value, index) + if index == 1 then + return accumulator + end + return _deepJoinHelper(accumulator, value) + end, values[1]) +end + +function _deepJoinHelper(table1, table2) + if table2 == None then + return nil + end + if type(table1) ~= type(table2) or type(table1) ~= "table" then + return table2 + end + + if getmetatable(table1) == getmetatable(table2) and type(table1.deepJoin) == "function" then + if type(table1.deepJoin) == "function" then + return table1:deepJoin(table2) + else + warn([[deepJoining two tables with the same metatable, but not implementing deepJoin. + Overriding with rightmost table.]]) + end + end + + return table2 +end + +return deepJoin \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/deepJoin/deepJoin.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/deepJoin/deepJoin.spec.lua new file mode 100644 index 0000000..29126a9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/deepJoin/deepJoin.spec.lua @@ -0,0 +1,267 @@ +return function() + local Root = script.Parent.Parent + local OrderedMap = require(Root.OrderedMap.OrderedMap) + local UnorderedMap = require(Root.UnorderedMap.UnorderedMap) + local OrderedSet = require(Root.OrderedSet.OrderedSet) + local UnorderedSet = require(Root.UnorderedSet.UnorderedSet) + local List = require(Root.List.List) + local None = require(Root.None) + local sort = require(Root.sort.sort) + + local deepJoin = require(script.Parent.deepJoin) + + describe("deepJoin", function() + it("Should return nil when called on no values", function() + expect(deepJoin()).to.never.be.ok() + end) + + it("Should work with basic types", function() + expect(deepJoin(1, 2)).to.equal(2) + local table1 = { + key1 = "value1", + key2 = "value2" + } + local table2 = { + key1 = "adifferentvalue1", + key3 = "value3" + } + local newtable = deepJoin(table1, table2) + expect(newtable).to.equal(table2) + end) + + it("Should work with OrderedSet", function() + local set1 = OrderedSet.new(sort.default, 4, 3, 2, 1) + local set2 = OrderedSet.new(sort.default, 3, 5) + local newSet = deepJoin(set1, set2) + expect(OrderedSet.is(newSet)).to.equal(true) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(5)).to.equal(true) + expect(newSet:size()).to.equal(5) + expect(newSet:first()).to.equal(1) + expect(newSet:last()).to.equal(5) + end) + + it("Should work with UnorderedSet", function() + local set1 = UnorderedSet.new(4, 3, 2, 1) + local set2 = UnorderedSet.new(3, 5) + local newSet = deepJoin(set1, set2) + expect(UnorderedSet.is(newSet)).to.equal(true) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(5)).to.equal(true) + expect(newSet:size()).to.equal(5) + end) + + it("Should work with Lists", function() + local list1 = List.new(5, 4, 3, 2, 1) + local list2 = List.new(100, 101) + local newList = deepJoin(list1, list2) + expect(List.is(newList)).to.equal(true) + expect(newList:size()).to.equal(7) + expect(newList:get(1)).to.equal(5) + expect(newList:get(newList:size())).to.equal(101) + end) + + it("Should work for some crazy nested maps", function() + local outermostMap1 = UnorderedMap.new({ + innerMap1 = UnorderedMap.new({ + innerList = List.new(3, 4, 4), + innerObject = { + key1 = "value1", + key2 = "value2" + } + }), + innerMap2 = OrderedMap.new(sort.reverse, { + apple = 1, + orange = 100, + }), + differentlyTypedStructure = UnorderedSet.new(3, 4, 5), + irrelevantValue = -1000, + }) + + local outermostMap2 = UnorderedMap.new({ + innerMap1 = UnorderedMap.new({ + innerList = List.new(1000), + innerObject = { + key1 = "differentvalue1", + key3 = "value3" + } + }), + innerMap2 = OrderedMap.new(sort.default, { + kiwi = 3, + orange = -5, + }), + differentlyTypedStructure = List.new(3, 4, 5), + irrelevantValue = 1000, + anotherIrrelvantValue = 50, + }) + + local mergedMap = deepJoin(outermostMap1, outermostMap2) + expect(UnorderedMap.is(mergedMap)).to.equal(true) + expect(mergedMap:size()).to.equal(5) + + expect(mergedMap:get("irrelevantValue")).to.equal(1000) + expect(mergedMap:get("anotherIrrelvantValue")).to.equal(50) + + local differentlyTypedStructure = mergedMap:get("differentlyTypedStructure") + expect(List.is(differentlyTypedStructure)).to.equal(true) + expect(differentlyTypedStructure:get(2)).to.equal(4) + + local innerMap1 = mergedMap:get("innerMap1") + local innerMap2 = mergedMap:get("innerMap2") + expect(UnorderedMap.is(innerMap1)).to.equal(true) + expect(OrderedMap.is(innerMap2)).to.equal(true) + local innerList = innerMap1:get("innerList") + local innerObject = innerMap1:get("innerObject") + + expect(List.is(innerList)).to.equal(true) + expect(innerList:size()).to.equal(4) + expect(innerList:get(1)).to.equal(3) + expect(innerList:get(2)).to.equal(4) + expect(innerList:get(3)).to.equal(4) + expect(innerList:get(4)).to.equal(1000) + + expect(innerObject["key1"]).to.equal("differentvalue1") + expect(innerObject["key2"]).to.never.be.ok() + expect(innerObject["key3"]).to.equal("value3") + + expect(OrderedMap.is(innerMap2)).to.equal(true) + expect(innerMap2:get("apple")).to.equal(1) + expect(innerMap2:get("orange")).to.equal(-5) + expect(innerMap2:get("kiwi")).to.equal(3) + expect((innerMap2:first())).to.equal("orange") + expect((innerMap2:last())).to.equal("apple") + end) + + it("Should work for a reasonable case", function() + local state = UnorderedMap.new({ + [100] = UnorderedMap.new({ + username = "Cool username 1", + information = { + settings = "example 1", + }, + }), + [105] = UnorderedMap.new({ + username = "Cool username 2", + information = { + settings = "example 2", + }, + }), + [110] = UnorderedMap.new({ + username = "Cool username 3", + information = { + settings = "example 3", + }, + }), + [111] = UnorderedMap.new({ + username = "Cool username 4", + information = { + settings = "example 4", + }, + }), + }) + + local actionData = UnorderedMap.new({ + [100] = UnorderedMap.new({ + username = "Updated cool username 1", + information = { + settings = "updated example 1", + }, + }), + [110] = UnorderedMap.new({ + username = "Updated cool username 3", + information = { + settings = "updated example 3", + }, + }), + }) + + local newState = deepJoin(state, actionData) + expect(newState:size()).to.equal(4) + expect(newState:get(105)).to.equal(state:get(105)) + expect(newState:get(111)).to.equal(state:get(111)) + expect(newState:get(100)).never.to.equal(state:get(100)) + expect(newState:get(110)).never.to.equal(state:get(110)) + local user100 = newState:get(100) + local user110 = newState:get(110) + expect(user100:get("username")).to.equal("Updated cool username 1") + expect(user110:get("username")).to.equal("Updated cool username 3") + expect(user100:get("information")["settings"]).to.equal("updated example 1") + expect(user110:get("information")["settings"]).to.equal("updated example 3") + end) + + describe("Multiple joins and None", function() + it("Works in basic cases", function() + expect(deepJoin(1, 2, 3)).to.equal(3) + local data1 = { + name = "Jim", + age = 22 + } + local data2 = { + name = "James" + } + local data3 = { + birthday = 11 + } + local newData = deepJoin(data1, data2, data3) + expect(newData).to.equal(data3) + end) + + it("Should work with OrderedSet", function() + local set1 = OrderedSet.new(sort.default, 4, 3, 2, 1) + local set2 = OrderedSet.new(sort.default, 3, 5) + local set3 = OrderedSet.new(sort.default, 100, 4) + local newSet = deepJoin(set1, set2, set3) + expect(OrderedSet.is(newSet)).to.equal(true) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(5)).to.equal(true) + expect(newSet:find(100)).to.equal(true) + expect(newSet:size()).to.equal(6) + expect(newSet:first()).to.equal(1) + expect(newSet:last()).to.equal(100) + end) + + it("Should work with UnorderedSet", function() + local set1 = UnorderedSet.new(4, 3, 2, 1) + local set2 = UnorderedSet.new(3, 5) + local set3 = UnorderedSet.new(100, 4) + local newSet = deepJoin(set1, set2, set3) + expect(UnorderedSet.is(newSet)).to.equal(true) + expect(newSet:find(1)).to.equal(true) + expect(newSet:find(2)).to.equal(true) + expect(newSet:find(3)).to.equal(true) + expect(newSet:find(4)).to.equal(true) + expect(newSet:find(5)).to.equal(true) + expect(newSet:find(100)).to.equal(true) + expect(newSet:size()).to.equal(6) + end) + + it("Should work with Lists", function() + local list1 = List.new(5, 4, 3, 2, 1) + local list2 = List.new(100, 101) + local list3 = List.new(1000, 1001) + local newList = deepJoin(list1, list2, list3) + expect(List.is(newList)).to.equal(true) + expect(newList:size()).to.equal(9) + expect(newList:get(1)).to.equal(5) + expect(newList:get(6)).to.equal(100) + expect(newList:get(newList:size())).to.equal(1001) + end) + + describe("Working with None", function() + it("Should work in basic cases", function() + local result = deepJoin(5, None) + expect(result).to.never.be.ok() + end) + end) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/init.lua new file mode 100644 index 0000000..955714f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/init.lua @@ -0,0 +1,10 @@ +return { + List = require(script.List.List), + sort = require(script.sort.sort), + OrderedMap = require(script.OrderedMap.OrderedMap), + OrderedSet = require(script.OrderedSet.OrderedSet), + UnorderedMap = require(script.UnorderedMap.UnorderedMap), + UnorderedSet = require(script.UnorderedSet.UnorderedSet), + deepJoin = require(script.deepJoin.deepJoin), + None = require(script.None), +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/init.spec.lua new file mode 100644 index 0000000..ff7e8e3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/init.spec.lua @@ -0,0 +1,5 @@ +return function() + it("should load", function() + require(script.Parent) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/sort/sort.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/sort/sort.lua new file mode 100644 index 0000000..b1b74b5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/sort/sort.lua @@ -0,0 +1,10 @@ +local sort = { + default = function(key1, key2) + return key1 < key2 + end, + reverse = function(key1, key2) + return key2 < key1 + end, +} + +return sort \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/sort/sort.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/sort/sort.spec.lua new file mode 100644 index 0000000..a17f3ef --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/freeze/sort/sort.spec.lua @@ -0,0 +1,25 @@ +return function() + local sort = require(script.Parent.sort) + it("Should be an table with two functions", function() + expect(sort.default).to.be.a("function") + expect(sort.reverse).to.be.a("function") + local less = 1 + local more = 2 + expect(sort.default(less, more)).to.equal(true) + expect(sort.default(more, less)).to.equal(false) + expect(sort.reverse(less, more)).to.equal(false) + expect(sort.reverse(more, less)).to.equal(true) + end) + + it("Should should work well with table.sort", function() + local values = {4, 3, 5, 2, 1} + table.sort(values, sort.default) + for i = 1, #values do + expect(values[i]).to.equal(i) + end + table.sort(values, sort.reverse) + for i = 1, #values do + expect(values[i]).to.equal(5 - i + 1) + end + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/lock.toml new file mode 100644 index 0000000..e87d961 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/freeze/lock.toml @@ -0,0 +1,6 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "freeze" +version = "0.1.0" +commit = "ef89f9d0444a2a7f63d5fd913a38824f3edba69f" +source = "git+https://github.com/roblox/freeze#master" +dependencies = ["Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo"] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/lock.toml new file mode 100644 index 0000000..f5f374d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "jtaylor/mock" +version = "0.1.0" +commit = "d2c4005c863fd2f9fa74b7391ada64906d242847" +source = "git+https://github.com/roblox/mock#master" diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/AnyCallMatches.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/AnyCallMatches.lua new file mode 100644 index 0000000..996dead --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/AnyCallMatches.lua @@ -0,0 +1,50 @@ +-- Functions to check if a mock was ever called with given arguments. + +local getCalls = require(script.Parent.getCalls) +local cmpLiteralArgs = require(script.Parent.cmpLiteralArgs) +local cmpPredicateArgs = require(script.Parent.cmpPredicateArgs) +local fmtArgs = require(script.Parent.fmtArgs) + +local AnyCallMatches = {} + +function AnyCallMatches.args(mock, test) + local callList = getCalls(mock) + local size = #callList + if size == 0 then + return false, "mock was not called" + end + + local msg + for i = 1, size do + local result + result, msg = test(callList[i].args) + if result then + return true + end + end + + if not msg then + msg = fmtArgs(callList[size].args) .. " did not match" + end + if size > 1 then + msg = msg .. " (+" .. (size - 1) .. " other calls)" + end + + return false, msg +end + +function AnyCallMatches.literals(mock, ...) + local expected = table.pack(...) + return AnyCallMatches.args(mock, function(actual) + return cmpLiteralArgs(expected, actual) + end) +end + +function AnyCallMatches.predicates(mock, ...) + local predicates = table.pack(...) + return AnyCallMatches.args(mock, function(actual) + return cmpPredicateArgs(predicates, actual) + end) +end + +return AnyCallMatches diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/MagicMock.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/MagicMock.lua new file mode 100644 index 0000000..4111575 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/MagicMock.lua @@ -0,0 +1,76 @@ +local symbols = require(script.Parent.symbols) +local Spy = require(script.Parent.Spy) + +local MagicMock = {} +local MetaMock = {} + +function MetaMock:__index(key) + -- Any access to an undefined member will return a new MagicMock. + local meta = getmetatable(self) + local child = meta[symbols.Children][key] + if child == nil then + child = MagicMock.new() + meta[symbols.Children][key] = child + return child + elseif child == symbols.None then + return nil + else + return child + end +end + +function MetaMock:__newindex(key, value) + -- Store any assigned values for later recall. + local meta = getmetatable(self) + if type(value) == "function" then + local _, wrapper = Spy.new(value) + value = wrapper + elseif value == nil then + value = symbols.None + end + + if key == symbols.ReturnValue then + meta[symbols.ReturnValue] = { value, n=1 } + else + meta[symbols.Children][key] = value + end +end + +function MetaMock:__call(...) + -- Any call to a MagicMock will store the args and then return the + -- ReturnValue (or call that if it's a function). If no return + -- value was set, this will create a new MagicMock. + local meta = getmetatable(self) + local call = { + args = table.pack(...), + result = meta[symbols.ReturnValue], + } + if call.result == nil then + local child = MagicMock.new() + call.result = { child, n=1 } + meta[symbols.ReturnValue] = call.result + elseif call.result == symbols.None then + call.result = { n=1 } + end + + table.insert(meta[symbols.Calls], call) + return table.unpack(call.result) +end + +function MagicMock.new(lock) + local mock = { + [symbols.Calls] = {}, + [symbols.Children] = {}, + [symbols.Lock] = lock, + [symbols.ReturnValue] = nil, + } + + -- Copy the Meta functions from MetaMock + for k, v in pairs(MetaMock) do + mock[k] = v + end + + return setmetatable({}, mock) +end + +return MagicMock diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/Spy.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/Spy.lua new file mode 100644 index 0000000..df221b2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/Spy.lua @@ -0,0 +1,37 @@ +-- Create a wrapper around a function to capture what arguments it was called +-- with. + +local symbols = require(script.Parent.symbols) + +local Spy = {} +Spy.__index = Spy + +local spyLookup = {} +setmetatable(spyLookup, {__mode = "kv"}) + +function Spy.new(inner) + local spy = { + [symbols.Calls] = {} + } + setmetatable(spy, Spy) + + local wrapper = function(...) + local call = { + args = table.pack(...), + result = table.pack(inner(...)), + } + table.insert(spy[symbols.Calls], call) + return table.unpack(call.result) + end + + spy.inner = wrapper + spyLookup[wrapper] = spy + + return spy, wrapper +end + +function Spy.lookup(wrapper) + return spyLookup[wrapper] +end + +return Spy \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/cmpLiteralArgs.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/cmpLiteralArgs.lua new file mode 100644 index 0000000..e218148 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/cmpLiteralArgs.lua @@ -0,0 +1,17 @@ +-- Compare two packed tables for shallow equality. +local fmtArgs = require(script.Parent.fmtArgs) + +return function(e, a) + if e.n ~= a.n then + local msg = "number of literals in " .. fmtArgs(e) .. " does not match number of args in " .. fmtArgs(a) + return false, msg + end + + for i = 1, e.n do + if e[i] ~= a[i] then + local msg = "expected " .. fmtArgs(e) .. ", got " .. fmtArgs(a) + return false, msg + end + end + return true +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/cmpPredicateArgs.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/cmpPredicateArgs.lua new file mode 100644 index 0000000..f1fdc04 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/cmpPredicateArgs.lua @@ -0,0 +1,17 @@ +-- Compare a packed table with a packed table of predicates. +local fmtArgs = require(script.Parent.fmtArgs) + +return function(p, x) + if p.n ~= x.n then + local msg = "number of args in " .. fmtArgs(x) .. " does not match number of predicates" + return false, msg + end + + for i = 1, x.n do + if not p[i](x[i]) then + local msg = fmtArgs(x) .. " does not match predicates at position " .. i + return false, msg + end + end + return true +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/fmtArgs.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/fmtArgs.lua new file mode 100644 index 0000000..bbec14f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/fmtArgs.lua @@ -0,0 +1,11 @@ +return function(args) + local msg = {} + for i = 1, args.n do + if type(args[i]) == "string" then + table.insert(msg, '"' .. args[i] .. '"') + else + table.insert(msg, tostring(args[i])) + end + end + return "{" .. table.concat(msg, ", ") .. "}" +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/getCalls.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/getCalls.lua new file mode 100644 index 0000000..59cf63c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/getCalls.lua @@ -0,0 +1,29 @@ +local symbols = require(script.Parent.symbols) +local Spy = require(script.Parent.Spy) + +return function(mock) + -- To support the usecase of invoking getCalls on a function + -- wrapped with a spy + if type(mock) == "function" then + local spy = Spy.lookup(mock) + if spy then + return spy[symbols.Calls] + end + error("Calling getCalls on a non-spy function") + end + + -- To support the usecase of invoking getCalls on spy itself + local argsList = rawget(mock, symbols.Calls) + + -- To support the usecase of invoking getCalls on MagicMock + if argsList == nil then + local meta = getmetatable(mock) + argsList = meta[symbols.Calls] + end + + if argsList == nil then + error("Calling getCalls on a non-mock") + end + + return argsList +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/init.lua new file mode 100644 index 0000000..4015b79 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/init.lua @@ -0,0 +1,8 @@ +local symbols = require(script.symbols) + +return { + MagicMock = require(script.MagicMock), + AnyCallMatches = require(script.AnyCallMatches), + getCalls = require(script.getCalls), + Spy = require(script.Spy), +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/symbols.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/symbols.lua new file mode 100644 index 0000000..1e4b0fa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/jtaylor_mock/mock/symbols.lua @@ -0,0 +1,16 @@ +local function newSymbol(name) + local symbol = newproxy(true) + name = ("Mocks.%s"):format(name) + getmetatable(symbol).__tostring = function() + return name + end + return symbol +end + +return { + Calls = newSymbol("Calls"), + Children = newSymbol("Children"), + Lock = newSymbol("Lock"), + None = newSymbol("None"), + ReturnValue = newSymbol("ReturnValue"), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/lua-promise/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/lua-promise/lock.toml new file mode 100644 index 0000000..65a4594 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/lua-promise/lock.toml @@ -0,0 +1,6 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "lua-promise" +version = "0.1.0" +commit = "1bb842c39b74105e3cd4c775e300d5139b19d039" +source = "git+https://github.com/roblox/lua-promise#master" +dependencies = ["tutils tutils 937da4f7 git+https://github.com/roblox/tutils#master"] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/lua-promise/lua-promise/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/lua-promise/lua-promise/init.lua new file mode 100644 index 0000000..4e90842 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/lua-promise/lua-promise/init.lua @@ -0,0 +1,366 @@ +--[[ + An implementation of Promises similar to Promise/A+. +]] +local tutils = require(script.Parent.tutils) + +local PROMISE_DEBUG = true + +-- If promise debugging is on, use a version of pcall that warns on failure. +-- This is useful for finding errors that happen within Promise itself. +local wpcall +if PROMISE_DEBUG then + wpcall = function(f, ...) + local result = { pcall(f, ...) } + + if not result[1] then + warn(result[2]) + end + + return unpack(result) + end +else + wpcall = pcall +end + +--[[ + Creates a function that invokes a callback with correct error handling and + resolution mechanisms. +]] +local function createAdvancer(callback, resolve, reject) + return function(...) + local result = { wpcall(callback, ...) } + local ok = table.remove(result, 1) + + if ok then + resolve(unpack(result)) + else + reject(unpack(result)) + end + end +end + +local function isEmpty(t) + return next(t) == nil +end + +local Promise = {} +Promise.__index = Promise + +Promise.Status = { + Started = "Started", + Resolved = "Resolved", + Rejected = "Rejected", +} + +--[[ + Constructs a new Promise with the given initializing callback. + + This is generally only called when directly wrapping a non-promise API into + a promise-based version. + + The callback will receive 'resolve' and 'reject' methods, used to start + invoking the promise chain. + + For example: + + local function get(url) + return Promise.new(function(resolve, reject) + spawn(function() + resolve(HttpService:GetAsync(url)) + end) + end) + end + + get("https://google.com") + :andThen(function(stuff) + print("Got some stuff!", stuff) + end) +]] +function Promise.new(callback) + local promise = { + -- Used to locate where a promise was created + _source = debug.traceback(), + + -- A tag to identify us as a promise + _type = "Promise", + + _status = Promise.Status.Started, + + -- A table containing a list of all results, whether success or failure. + -- Only valid if _status is set to something besides Started + _value = nil, + + -- If an error occurs with no observers, this will be set. + _unhandledRejection = false, + + -- Queues representing functions we should invoke when we update! + _queuedResolve = {}, + _queuedReject = {}, + } + + setmetatable(promise, Promise) + + local function resolve(...) + promise:_resolve(...) + end + + local function reject(...) + promise:_reject(...) + end + + local ok, err = wpcall(callback, resolve, reject) + + if not ok and promise._status == Promise.Status.Started then + reject(err) + end + + return promise +end + +--[[ + Create a promise that represents the immediately resolved value. +]] +function Promise.resolve(value) + return Promise.new(function(resolve) + resolve(value) + end) +end + +--[[ + Create a promise that represents the immediately rejected value. +]] +function Promise.reject(value) + return Promise.new(function(_, reject) + reject(value) + end) +end + +--[[ + Returns a new promise that: + * is resolved when all input promises resolve + * is rejected if ANY input promises reject +]] +function Promise.all(...) + local promises = {...} + + -- check if we've been given a list of promises, not just a variable number of promises + if type(promises[1]) == "table" and promises[1]._type ~= "Promise" then + -- we've been given a table of promises already + promises = promises[1] + end + + return Promise.new(function(resolve, reject) + local isResolved = false + local results = {} + local totalCompleted = 0 + local function promiseCompleted(index, result) + if isResolved then + return + end + + results[index] = result + totalCompleted = totalCompleted + 1 + + if totalCompleted == #promises then + resolve(results) + isResolved = true + end + end + + if #promises == 0 then + resolve(results) + isResolved = true + return + end + + for index, promise in ipairs(promises) do + -- if a promise isn't resolved yet, add listeners for when it does + if promise._status == Promise.Status.Started then + promise:andThen(function(result) + promiseCompleted(index, result) + end):catch(function(reason) + isResolved = true + reject(reason) + end) + + -- if a promise is already resolved, move on + elseif promise._status == Promise.Status.Resolved then + promiseCompleted(index, unpack(promise._value)) + + -- if a promise is rejected, reject the whole chain + else --if promise._status == Promise.Status.Rejected then + isResolved = true + reject(unpack(promise._value)) + end + end + end) +end + +--[[ + Is the given object a Promise instance? +]] +function Promise.is(object) + if type(object) ~= "table" then + return false + end + + return object._type == "Promise" +end + +--[[ + Creates a new promise that receives the result of this promise. + + The given callbacks are invoked depending on that result. +]] +function Promise:andThen(successHandler, failureHandler) + -- Even if we haven't specified a failure handler, the rejection will be automatically + -- passed on by the advancer; other promises in the chain may have unhandled rejections, + -- but this one is taken care of as soon as any andThen is connected + self._unhandledRejection = false + + -- Create a new promise to follow this part of the chain + return Promise.new(function(resolve, reject) + -- Our default callbacks just pass values onto the next promise. + -- This lets success and failure cascade correctly! + + local successCallback = resolve + if successHandler then + successCallback = createAdvancer(successHandler, resolve, reject) + end + + local failureCallback = reject + if failureHandler then + failureCallback = createAdvancer(failureHandler, resolve, reject) + end + + if self._status == Promise.Status.Started then + -- If we haven't resolved yet, put ourselves into the queue + table.insert(self._queuedResolve, successCallback) + table.insert(self._queuedReject, failureCallback) + elseif self._status == Promise.Status.Resolved then + -- This promise has already resolved! Trigger success immediately. + successCallback(unpack(self._value)) + elseif self._status == Promise.Status.Rejected then + -- This promise died a terrible death! Trigger failure immediately. + failureCallback(unpack(self._value)) + end + end) +end + +--[[ + Used to catch any errors that may have occurred in the promise. +]] +function Promise:catch(failureCallback) + return self:andThen(nil, failureCallback) +end + +--[[ + Yield until the promise is completed. + + This matches the execution model of normal Roblox functions. +]] +function Promise:await() + self._unhandledRejection = false + + if self._status == Promise.Status.Started then + local result + local bindable = Instance.new("BindableEvent") + + self:andThen(function(...) + result = {...} + bindable:Fire(true) + end, function(...) + result = {...} + bindable:Fire(false) + end) + + local ok = bindable.Event:Wait() + bindable:Destroy() + + if not ok then + error(tostring(result[1]), 2) + end + + return unpack(result) + elseif self._status == Promise.Status.Resolved then + return unpack(self._value) + elseif self._status == Promise.Status.Rejected then + error(tostring(self._value[1]), 2) + end +end + +function Promise:_resolve(...) + if self._status ~= Promise.Status.Started then + return + end + + -- If the resolved value was a Promise, we chain onto it! + if Promise.is((...)) then + -- Without this warning, arguments sometimes mysteriously disappear + if select("#", ...) > 1 then + local message = ("When returning a Promise from andThen, extra arguments are discarded! See:\n\n%s"):format( + self._source + ) + warn(message) + end + + (...):andThen(function(...) + self:_resolve(...) + end, function(...) + self:_reject(...) + end) + + return + end + + self._status = Promise.Status.Resolved + self._value = {...} + + -- We assume that these callbacks will not throw errors. + for _, callback in ipairs(self._queuedResolve) do + callback(...) + end +end + +function Promise:_reject(...) + if self._status ~= Promise.Status.Started then + return + end + + self._status = Promise.Status.Rejected + self._value = {...} + + -- If there are any rejection handlers, call those! + if not isEmpty(self._queuedReject) then + -- We assume that these callbacks will not throw errors. + for _, callback in ipairs(self._queuedReject) do + callback(...) + end + else + -- At this point, no one was able to observe the error. + -- An error handler might still be attached if the error occurred + -- synchronously. We'll wait one tick, and if there are still no + -- observers, then we should put a message in the console. + + self._unhandledRejection = true + -- Rather than trying to figure out how to pack/represent the rejection values, + -- we just stringify the first value and leave the rest alone + local err = tutils.toString((...)) + + spawn(function() + -- Someone observed the error, hooray! + if not self._unhandledRejection then + return + end + + -- Build a reasonable message + local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format( + err, + self._source + ) + warn(message) + end) + end +end + +return Promise diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/lua-promise/tutils.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/lua-promise/tutils.lua new file mode 100644 index 0000000..cb6c720 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/lua-promise/tutils.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["tutils"]["tutils"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Otter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/lock.toml new file mode 100644 index 0000000..0360948 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/lock.toml @@ -0,0 +1,10 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roact-navigation" +version = "0.1.1-linter-fix" +commit = "943c1c661c396a39ef3ca8f45927d4242732581d" +source = "url+https://github.com/roblox/roact-navigation" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/BackBehavior.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/BackBehavior.lua new file mode 100644 index 0000000..5fe5b20 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/BackBehavior.lua @@ -0,0 +1,21 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +local NONE_TOKEN = NavigationSymbol("NONE") +local INITIAL_ROUTE_TOKEN = NavigationSymbol("INITIAL_ROUTE") +local ORDER_TOKEN = NavigationSymbol("ORDER") +local HISTORY_TOKEN = NavigationSymbol("HISTORY") + +--[[ + BackBehavior provides shared constants that are used to configure back + action styles for different navigators. Note that not all routers support + all BackBehaviors and they will fall back to appropriate defaults for + those cases. +]] +local BackBehavior = { + None = NONE_TOKEN, + InitialRoute = INITIAL_ROUTE_TOKEN, + Order = ORDER_TOKEN, + History = HISTORY_TOKEN, +} + +return BackBehavior diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/EdgeInsets.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/EdgeInsets.lua new file mode 100644 index 0000000..4540250 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/EdgeInsets.lua @@ -0,0 +1,38 @@ +--[[ + EdgeInsets provides standard tooling to conveniently create a table + to represent insets from the edge of a component's viewable area. + This is most useful for identifying a "safe area" for visible content, + e.g. reacting to variable status bar heights and other view adornments. + + Positive values represent insets into the viewable area, e.g. smaller + than the container. + + Several constructors are provided: + + EdgeInsets.new(top: UDim, right: UDim, bottom: UDim, left: UDim) + Creates a new edge insets from four UDims that you provide. + + EdgeInsets.fromOffsets(top: number, right: number, bottom: number, left: number) + Creates an offsets-only EdgeInsets using four numbers that you provide. +]] +local EdgeInsets = {} + +function EdgeInsets.new(topUDim, rightUDim, bottomUDim, leftUDim) + return { + top = topUDim or UDim.new(0, 0), + right = rightUDim or UDim.new(0, 0), + bottom = bottomUDim or UDim.new(0, 0), + left = leftUDim or UDim.new(0, 0), + } +end + +function EdgeInsets.fromOffsets(top, right, bottom, left) + return { + top = UDim.new(0, top or 0), + right = UDim.new(0, right or 0), + bottom = UDim.new(0, bottom or 0), + left = UDim.new(0, left or 0), + } +end + +return EdgeInsets diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationActions.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationActions.lua new file mode 100644 index 0000000..5719d57 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationActions.lua @@ -0,0 +1,76 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +local BACK_TOKEN = NavigationSymbol("BACK") +local INIT_TOKEN = NavigationSymbol("INIT") +local NAVIGATE_TOKEN = NavigationSymbol("NAVIGATE") +local SET_PARAMS_TOKEN = NavigationSymbol("SET_PARAMS") +local COMPLETE_TRANSITION_TOKEN = NavigationSymbol("COMPLETE_TRANSITION") + +--[[ + NavigationActions provides shared constants and methods to construct + actions that are dispatched to routers to cause a change in the route. +]] +local NavigationActions = { + Back = BACK_TOKEN, + Init = INIT_TOKEN, + Navigate = NAVIGATE_TOKEN, + SetParams = SET_PARAMS_TOKEN, + CompleteTransition = COMPLETE_TRANSITION_TOKEN, +} + +NavigationActions.__index = NavigationActions + +-- Navigate back in the history (temporally). +function NavigationActions.back(payload) + local data = payload or {} + return { + type = BACK_TOKEN, + key = data.key, + immediate = data.immediate, + } +end + +-- Initialize the navigation history if not already defined. +function NavigationActions.init(payload) + local data = payload or {} + return { + type = INIT_TOKEN, + params = data.params, + } +end + +-- Navigate to an existing or new route. +function NavigationActions.navigate(payload) + local data = payload or {} + return { + type = NAVIGATE_TOKEN, + routeName = data.routeName, + params = data.params, + action = data.action, + key = data.key, + } +end + +-- Swap out the params for an existing route, matched by the given key. +function NavigationActions.setParams(payload) + local data = payload or {} + return { + type = SET_PARAMS_TOKEN, + key = data.key, + params = data.params, + } +end + +-- For internal use. Triggers completion of a transition animation, if needed by the router. +-- This would be sent on e.g. didMount of the new page, so the router knows that the new screen +-- is ready to be displayed before it animates it in place. +function NavigationActions.completeTransition(payload) + local data = payload or {} + return { + type = COMPLETE_TRANSITION_TOKEN, + key = data.key, + toChildKey = data.toChildKey, + } +end + +return NavigationActions diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationEvents.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationEvents.lua new file mode 100644 index 0000000..a60610b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationEvents.lua @@ -0,0 +1,21 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +local WILL_FOCUS_TOKEN = NavigationSymbol("WILL_FOCUS") +local DID_FOCUS_TOKEN = NavigationSymbol("DID_FOCUS") +local WILL_BLUR_TOKEN = NavigationSymbol("WILL_BLUR") +local DID_BLUR_TOKEN = NavigationSymbol("DID_BLUR") +local ACTION_TOKEN = NavigationSymbol("ACTION") +local REFOCUS_TOKEN = NavigationSymbol("REFOCUS") + +--[[ + NavigationEvents provides shared constants that are used to register + listeners for different RoactNavigation UI state changes. +]] +return { + WillFocus = WILL_FOCUS_TOKEN, + DidFocus = DID_FOCUS_TOKEN, + WillBlur = WILL_BLUR_TOKEN, + DidBlur = DID_BLUR_TOKEN, + Action = ACTION_TOKEN, + Refocus = REFOCUS_TOKEN, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationSymbol.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationSymbol.lua new file mode 100644 index 0000000..90762dc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NavigationSymbol.lua @@ -0,0 +1,15 @@ +-- Taken from Roact.Symbol and modified to produce exact string names +-- to allow for serialization/pathing. + +return function (name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + -- Unlike Symbols in Roact, we need the exact names. + getmetatable(self).__tostring = function() + return name + end + + return self +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NoneSymbol.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NoneSymbol.lua new file mode 100644 index 0000000..10acee1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/NoneSymbol.lua @@ -0,0 +1,11 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +--[[ + RoactNavigation.None allows us to declare that certain values should be explicitly + removed from navigation props, e.g. for stack navigation options where we want to + remove the default header from a screen when drawing in a stack navigator with + headerMode == StackHeaderMode.Screen. +]] +local NONE_SYMBOL = NavigationSymbol("NONE") + +return NONE_SYMBOL diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/StackActions.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/StackActions.lua new file mode 100644 index 0000000..387ed44 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/StackActions.lua @@ -0,0 +1,80 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +local POP_TOKEN = NavigationSymbol("POP") +local POP_TO_TOP_TOKEN = NavigationSymbol("POP_TO_TOP") +local PUSH_TOKEN = NavigationSymbol("PUSH") +local RESET_TOKEN = NavigationSymbol("RESET") +local REPLACE_TOKEN = NavigationSymbol("REPLACE") + +--[[ + StackActions provides shared constants and methods to construct + actions that are dispatched to routers to cause a change in the route. + These actions are specific to Stack navigation. See NavigationActions + if you need to use more general APIs. +]] +local StackActions = { + Pop = POP_TOKEN, + PopToTop = POP_TO_TOP_TOKEN, + Push = PUSH_TOKEN, + Reset = RESET_TOKEN, + Replace = REPLACE_TOKEN, +} + +StackActions.__index = StackActions + +-- Pop the top-most item off the route stack, if any. +function StackActions.pop(payload) + local data = payload or {} + return { + type = POP_TOKEN, + n = data.n, + } +end + +-- Pop all the items except the last one off the route stack. +function StackActions.popToTop(payload) + local data = payload or {} + return { + type = POP_TO_TOP_TOKEN, + key = data.key, + } +end + +-- Push a new item onto the route stack. +function StackActions.push(payload) + local data = payload or {} + return { + type = PUSH_TOKEN, + routeName = data.routeName, + params = data.params, + action = data.action, + } +end + +-- Reset the route stack and replace it with a new stack, +-- specified by a list of actions to be applied. +function StackActions.reset(payload) + local data = payload or {} + return { + type = RESET_TOKEN, + index = data.index, + actions = data.actions, + key = data.key, + } +end + +-- Replace the route for the given key with a new route. +function StackActions.replace(payload) + local data = payload or {} + return { + type = REPLACE_TOKEN, + key = data.key, + newKey = data.newKey, + routeName = data.routeName, + params = data.params, + action = data.action, + immediate = data.immediate, + } +end + +return StackActions diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/StateUtils.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/StateUtils.lua new file mode 100644 index 0000000..415a6a6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/StateUtils.lua @@ -0,0 +1,262 @@ +local Cryo = require(script.Parent.Parent.Cryo) + +--[[ + StateUtils provides utilities to read and write standard route data. + Routes have the following general structure: + { + index = , + routes = [ + { + routeName = , + key = , + params = , + action = , + }, + ... + ] + } + + This structure is independent of the notion of stack, tab, drawer, or any + other kind of navigation. It simply represents a list of pages and their + parameters. Different kinds of routers can treat the data in their own way. +]] +local StateUtils = {} +StateUtils.__index = StateUtils + +-- Get the route matching the given key. Returns nil if no match is found. +function StateUtils.get(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + for _, route in ipairs(state.routes) do + if route.key == key then + return route + end + end + + return nil +end + +-- Get the route at the given index. Returns nil if no match is found. +function StateUtils.getAtIndex(state, index) + assert(type(state) == "table", "state must be a table") + assert(type(index) == "number", "index must be a number") + assert(index >= 0, "index must be non-negative") + + return state.routes[index] +end + +-- Get the active route from state. Returns nil if no routes. +function StateUtils.getActiveRoute(state) + assert(type(state) == "table", "state must be a table") + + local index = state.index + if index <= 0 then + return nil + end + + return state.routes[index] +end + +-- Get the index of the route matching the given key. Returns nil if no match is found. +function StateUtils.indexOf(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + for index, route in ipairs(state.routes) do + if route.key == key then + return index + end + end + + return nil +end + +-- Returns true if a route exists matching the given key, false otherwise. +function StateUtils.has(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + for _, route in ipairs(state.routes) do + if route.key == key then + return true + end + end + + return false +end + +-- Push a new route into the navigation state. Makes the pushed route active. +function StateUtils.push(state, route) + assert(type(state) == "table", "state must be a table") + assert(type(route) == "table", "route must be a table") + + assert(StateUtils.indexOf(state, route.key) == nil, + string.format("route with key '%s' already exists", route.key)) + + local routes = Cryo.List.join(state.routes, { route }) + return Cryo.Dictionary.join(state, { + index = #routes, + routes = routes, + }) +end + +-- Pop the top-most route from the navigation state (NOT the active route). +-- Makes the new top-most route active. +function StateUtils.pop(state) + assert(type(state) == "table", "state must be a table") + + if #state.routes == 0 then + -- NOTE: Popping empty state is a no-op + return state + end + + local routes = Cryo.List.removeIndex(state.routes, #state.routes) + return Cryo.Dictionary.join(state, { + index = #routes, + routes = routes, + }) +end + +-- Sets the active route to match the given index. +function StateUtils.jumpToIndex(state, index) + assert(type(state) == "table", "state must be a table") + assert(type(index) == "number", "index must be a number") + + if index == state.index then + return state + end + + assert(state.routes[index] ~= nil, + string.format("cannot jump to out-of-range index '%d'", index)) + + return Cryo.Dictionary.join(state, { + index = index, + }) +end + +-- Sets the active route to match the given key. +function StateUtils.jumpTo(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + local index = StateUtils.indexOf(state, key) + return StateUtils.jumpToIndex(state, index) +end + +-- Sets the active route to the previous route in the list. +function StateUtils.back(state) + assert(type(state) == "table", "state must be a table") + + local index = state.index - 1 + if not state.routes[index] then + return state + end + + return StateUtils.jumpToIndex(state, index) +end + +-- Sets the active route to the next route in the list. +function StateUtils.forward(state) + assert(type(state) == "table", "state must be a table") + + local index = state.index + 1 + if not state.routes[index] then + return state + end + + return StateUtils.jumpToIndex(state, index) +end + +-- Replace the route matching the given key. Sets the active route to the +-- newly replaced entry. Prunes the old entries that follow the replaced one. +function StateUtils.replaceAndPrune(state, key, route) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + assert(type(route) == "table", "route must be a table") + + local index = StateUtils.indexOf(state, key) + local replaced = StateUtils.replaceAtIndex(state, index, route) + + return Cryo.Dictionary.join(replaced, { + routes = { unpack(replaced.routes, 1, index) } + }) +end + +-- Replace the route matching the given key without pruning the following routes. +-- The active route will be updated to match the newly replaced one unless +-- preserveIndex is true. +function StateUtils.replaceAt(state, key, route, preserveIndex) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + assert(type(route) == "table", "route must be a table") + assert(preserveIndex == nil or type(preserveIndex) == "boolean", + "preserveIndex must be nil or a boolean") + + local index = StateUtils.indexOf(state, key) + local nextIndex = preserveIndex and state.index or index + local nextState = StateUtils.replaceAtIndex(state, index, route) + nextState.index = nextIndex + return nextState +end + +-- Replace the route at the given index. Updates the active route to point to +-- the replaced entry. +function StateUtils.replaceAtIndex(state, index, route) + assert(type(state) == "table", "state must be a table") + assert(type(index) == "number", "index must be a number") + assert(type(route) == "table", "route must be a table") + + assert(state.routes[index] ~= nil, + string.format("index '%d' does not exist in route '%s'", index, route.key)) + + if state.routes[index] == route and index == state.index then + return state + end + + local routes = Cryo.List.join(state.routes) + routes[index] = route + + return Cryo.Dictionary.join(state, { + index = index, + routes = routes, + }) +end + +-- Wipe away the existing routes and replace them with new routes. +-- Sets the active route to the provided index (if provided), otherwise +-- sets the active route to the last one in the list. +function StateUtils.reset(state, routes, index) + assert(type(state) == "table", "state must be a table") + assert(type(routes) == "table" and #routes > 0, + "routes must be a list with at least one element") + assert(index == nil or type(index) == "number", + "index must be a number or nil") + + local nextIndex = not index and #routes or index + + -- Bail out without replacing IFF index and routes all match + if #state.routes == #routes and state.index == nextIndex then + local routesAreEqual = true + for i = 1, #routes, 1 do + if state.routes[i] ~= routes[i] then + routesAreEqual = false + break + end + end + + if routesAreEqual then + return state + end + end + + assert(routes[nextIndex] ~= nil, + string.format("cannot reset index '%d' that does not exist", nextIndex)) + + return Cryo.Dictionary.join(state, { + index = nextIndex, + routes = routes, + }) +end + +return StateUtils diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/BackBehavior.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/BackBehavior.spec.lua new file mode 100644 index 0000000..cec6ddd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/BackBehavior.spec.lua @@ -0,0 +1,19 @@ +return function() + local BackBehavior = require(script.Parent.Parent.BackBehavior) + + describe("BackBehavior token tests", function() + it("should return same object for each token for multiple calls", function() + expect(BackBehavior.None).to.equal(BackBehavior.None) + expect(BackBehavior.InitialRoute).to.equal(BackBehavior.InitialRoute) + expect(BackBehavior.Order).to.equal(BackBehavior.Order) + expect(BackBehavior.History).to.equal(BackBehavior.History) + end) + + it("should return matching string names for symbols", function() + expect(tostring(BackBehavior.None)).to.equal("NONE") + expect(tostring(BackBehavior.InitialRoute)).to.equal("INITIAL_ROUTE") + expect(tostring(BackBehavior.Order)).to.equal("ORDER") + expect(tostring(BackBehavior.History)).to.equal("HISTORY") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/EdgeInsets.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/EdgeInsets.spec.lua new file mode 100644 index 0000000..03b67ab --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/EdgeInsets.spec.lua @@ -0,0 +1,75 @@ +return function() + local EdgeInsets = require(script.Parent.Parent.EdgeInsets) + + it("should create EdgeInsets with provided UDims", function() + local top = UDim.new(0, 0) + local right = UDim.new(1, 0) + local bottom = UDim.new(0, 1) + local left = UDim.new(1, 1) + + local result = EdgeInsets.new(top, right, bottom, left) + expect(result.top).to.equal(top) + expect(result.right).to.equal(right) + expect(result.bottom).to.equal(bottom) + expect(result.left).to.equal(left) + end) + + it("should create EdgeInsets with default UDims", function() + local result = EdgeInsets.new() + expect(typeof(result.top)).to.equal("UDim") + expect(result.top.Scale).to.equal(0) + expect(result.top.Offset).to.equal(0) + + expect(typeof(result.right)).to.equal("UDim") + expect(result.right.Scale).to.equal(0) + expect(result.right.Offset).to.equal(0) + + expect(typeof(result.bottom)).to.equal("UDim") + expect(result.bottom.Scale).to.equal(0) + expect(result.bottom.Offset).to.equal(0) + + expect(typeof(result.left)).to.equal("UDim") + expect(result.left.Scale).to.equal(0) + expect(result.left.Offset).to.equal(0) + end) + + it("should create EdgeInsets with provided offset", function() + local result = EdgeInsets.fromOffsets(0, 1, 2, 3) + + expect(typeof(result.top)).to.equal("UDim") + expect(result.top.Scale).to.equal(0) + expect(result.top.Offset).to.equal(0) + + expect(typeof(result.right)).to.equal("UDim") + expect(result.right.Scale).to.equal(0) + expect(result.right.Offset).to.equal(1) + + expect(typeof(result.bottom)).to.equal("UDim") + expect(result.bottom.Scale).to.equal(0) + expect(result.bottom.Offset).to.equal(2) + + expect(typeof(result.left)).to.equal("UDim") + expect(result.left.Scale).to.equal(0) + expect(result.left.Offset).to.equal(3) + end) + + it("should create EdgeInsets with defaults for missing values", function() + local result = EdgeInsets.fromOffsets() + + expect(typeof(result.top)).to.equal("UDim") + expect(result.top.Scale).to.equal(0) + expect(result.top.Offset).to.equal(0) + + expect(typeof(result.right)).to.equal("UDim") + expect(result.right.Scale).to.equal(0) + expect(result.right.Offset).to.equal(0) + + expect(typeof(result.bottom)).to.equal("UDim") + expect(result.bottom.Scale).to.equal(0) + expect(result.bottom.Offset).to.equal(0) + + expect(typeof(result.left)).to.equal("UDim") + expect(result.left.Scale).to.equal(0) + expect(result.left.Offset).to.equal(0) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationActions.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationActions.spec.lua new file mode 100644 index 0000000..03d08db --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationActions.spec.lua @@ -0,0 +1,80 @@ +return function() + local NavigationActions = require(script.Parent.Parent.NavigationActions) + + describe("NavigationActions token tests", function() + it("should return same object for each token for multiple calls", function() + expect(NavigationActions.Back).to.equal(NavigationActions.Back) + expect(NavigationActions.Init).to.equal(NavigationActions.Init) + expect(NavigationActions.Navigate).to.equal(NavigationActions.Navigate) + expect(NavigationActions.SetParams).to.equal(NavigationActions.SetParams) + expect(NavigationActions.CompleteTransition).to.equal(NavigationActions.CompleteTransition) + end) + + it("should return matching string names for symbols", function() + expect(tostring(NavigationActions.Back)).to.equal("BACK") + expect(tostring(NavigationActions.Init)).to.equal("INIT") + expect(tostring(NavigationActions.Navigate)).to.equal("NAVIGATE") + expect(tostring(NavigationActions.SetParams)).to.equal("SET_PARAMS") + expect(tostring(NavigationActions.CompleteTransition)).to.equal("COMPLETE_TRANSITION") + end) + end) + + describe("NavigationActions function tests", function() + it("should return a back action with matching data for a call to back()", function() + local backTable = NavigationActions.back({ + key = "the_key", + immediate = true, + }) + + expect(backTable.type).to.equal(NavigationActions.Back) + expect(backTable.key).to.equal("the_key") + expect(backTable.immediate).to.equal(true) + end) + + it("should return an init action with matching data for call to init()", function() + local initTable = NavigationActions.init({ + params = "foo", + }) + + expect(initTable.type).to.equal(NavigationActions.Init) + expect(initTable.params).to.equal("foo") + end) + + it("should return a navigate action with matching data for call to navigate()", function() + local navigateTable = NavigationActions.navigate({ + routeName = "routeName", + params = "foo", + action = "action", + key = "key", + }) + + expect(navigateTable.type).to.equal(NavigationActions.Navigate) + expect(navigateTable.routeName).to.equal("routeName") + expect(navigateTable.params).to.equal("foo") + expect(navigateTable.action).to.equal("action") + expect(navigateTable.key).to.equal("key") + end) + + it("should return a set params action with matching data for call to setParams()", function() + local setParamsTable = NavigationActions.setParams({ + key = "key", + params = "foo", + }) + + expect(setParamsTable.type).to.equal(NavigationActions.SetParams) + expect(setParamsTable.key).to.equal("key") + expect(setParamsTable.params).to.equal("foo") + end) + + it("should return a complete transition action with matching data for call to completeTransition()", function() + local completeTransitionTable = NavigationActions.completeTransition({ + key = "key", + toChildKey = "toChildKey", + }) + + expect(completeTransitionTable.type).to.equal(NavigationActions.CompleteTransition) + expect(completeTransitionTable.key).to.equal("key") + expect(completeTransitionTable.toChildKey).to.equal("toChildKey") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationEvents.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationEvents.spec.lua new file mode 100644 index 0000000..ed776d7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationEvents.spec.lua @@ -0,0 +1,23 @@ +return function() + local NavigationEvents = require(script.Parent.Parent.NavigationEvents) + + describe("NavigationEvents token tests", function() + it("should return same object for each token for multiple calls", function() + expect(NavigationEvents.WillFocus).to.equal(NavigationEvents.WillFocus) + expect(NavigationEvents.DidFocus).to.equal(NavigationEvents.DidFocus) + expect(NavigationEvents.WillBlur).to.equal(NavigationEvents.WillBlur) + expect(NavigationEvents.DidBlur).to.equal(NavigationEvents.DidBlur) + expect(NavigationEvents.Action).to.equal(NavigationEvents.Action) + expect(NavigationEvents.Refocus).to.equal(NavigationEvents.Refocus) + end) + + it("should return matching string names for symbols", function() + expect(tostring(NavigationEvents.WillFocus)).to.equal("WILL_FOCUS") + expect(tostring(NavigationEvents.DidFocus)).to.equal("DID_FOCUS") + expect(tostring(NavigationEvents.WillBlur)).to.equal("WILL_BLUR") + expect(tostring(NavigationEvents.DidBlur)).to.equal("DID_BLUR") + expect(tostring(NavigationEvents.Action)).to.equal("ACTION") + expect(tostring(NavigationEvents.Refocus)).to.equal("REFOCUS") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationSymbol.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationSymbol.spec.lua new file mode 100644 index 0000000..a43690b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NavigationSymbol.spec.lua @@ -0,0 +1,22 @@ +return function() + local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol) + + it("should give an opaque object", function() + local symbol = NavigationSymbol("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = NavigationSymbol("foo") + + expect(tostring(symbol)).to.equal("foo") + end) + + it("should be unique when constructed", function() + local symbolA = NavigationSymbol("abc") + local symbolB = NavigationSymbol("abc") + + expect(symbolA).never.to.equal(symbolB) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NoneSymbol.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NoneSymbol.spec.lua new file mode 100644 index 0000000..afcbced --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/NoneSymbol.spec.lua @@ -0,0 +1,11 @@ +return function() + local NoneSymbol = require(script.Parent.Parent.NoneSymbol) + + it("should return same object for each token for multiple calls", function() + expect(NoneSymbol).to.equal(NoneSymbol) + end) + + it("should return matching string names for symbols", function() + expect(tostring(NoneSymbol)).to.equal("NONE") + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/StackActions.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/StackActions.spec.lua new file mode 100644 index 0000000..7baf9cc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/StackActions.spec.lua @@ -0,0 +1,82 @@ +return function() + local StackActions = require(script.Parent.Parent.StackActions) + + describe("StackActions token tests", function() + it("should return same object for each token for multiple calls", function() + expect(StackActions.Pop).to.equal(StackActions.Pop) + expect(StackActions.PopToTop).to.equal(StackActions.PopToTop) + expect(StackActions.Push).to.equal(StackActions.Push) + expect(StackActions.Reset).to.equal(StackActions.Reset) + expect(StackActions.Replace).to.equal(StackActions.Replace) + end) + + it("should return matching string names for symbols", function() + expect(tostring(StackActions.Pop)).to.equal("POP") + expect(tostring(StackActions.PopToTop)).to.equal("POP_TO_TOP") + expect(tostring(StackActions.Push)).to.equal("PUSH") + expect(tostring(StackActions.Reset)).to.equal("RESET") + expect(tostring(StackActions.Replace)).to.equal("REPLACE") + end) + end) + + describe("StackActions function tests", function() + it("should return a pop action for pop()", function() + local popTable = StackActions.pop({ + n = "n", + }) + + expect(popTable.type).to.equal(StackActions.Pop) + expect(popTable.n).to.equal("n") + end) + + it("should return a pop to top action for popToTop()", function() + local popToTopTable = StackActions.popToTop() + + expect(popToTopTable.type).to.equal(StackActions.PopToTop) + end) + + it("should return a push action for push()", function() + local pushTable = StackActions.push({ + routeName = "routeName", + params = "params", + action = "action", + }) + + expect(pushTable.type).to.equal(StackActions.Push) + expect(pushTable.routeName).to.equal("routeName") + expect(pushTable.params).to.equal("params") + expect(pushTable.action).to.equal("action") + end) + + it("should return a reset action for reset()", function() + local resetTable = StackActions.reset({ + index = "index", + actions = "actions", + key = "key", + }) + + expect(resetTable.type).to.equal(StackActions.Reset) + expect(resetTable.index).to.equal("index") + expect(resetTable.key).to.equal("key") + end) + + it("should return a replace action for replace()", function() + local replaceTable = StackActions.replace({ + key = "key", + newKey = "newKey", + routeName = "routeName", + params = "params", + action = "action", + immediate = "immediate", + }) + + expect(replaceTable.type).to.equal(StackActions.Replace) + expect(replaceTable.key).to.equal("key") + expect(replaceTable.newKey).to.equal("newKey") + expect(replaceTable.routeName).to.equal("routeName") + expect(replaceTable.params).to.equal("params") + expect(replaceTable.action).to.equal("action") + expect(replaceTable.immediate).to.equal("immediate") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/StateUtils.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/StateUtils.spec.lua new file mode 100644 index 0000000..279873f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/StateUtils.spec.lua @@ -0,0 +1,725 @@ +return function() + local StateUtils = require(script.Parent.Parent.StateUtils) + + describe("StateUtils.get tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.get(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.get({}, 5) + end).to.throw() + end) + + it("should return nil if key is not found in routes", function() + local result = StateUtils.get({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + }, + }, + }, "key") + + expect(result).to.equal(nil) + end) + + it("should return route if key is found in routes", function() + local result = StateUtils.get({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + }, + }, "foo-1") + + expect(result.routeName).to.equal("foo") + expect(result.key).to.equal("foo-1") + end) + end) + + describe("StateUtils.getAtIndex tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.getAtIndex(nil, 0) + end).to.throw() + end) + + it("should assert if index is negative", function() + expect(function() + StateUtils.getAtIndex({}, -1) + end).to.throw() + end) + + it("should return nil if index is not found", function() + local result = StateUtils.getAtIndex({ + index = 1, + routes = { + { + routeName = "foo1", + key = "foo-1", + }, + { + routeName = "foo2", + key = "foo-2", + } + } + }, 5) + + expect(result).to.equal(nil) + end) + + it("should return a matching route", function() + local result = StateUtils.getAtIndex({ + index = 1, + routes = { + { + routeName = "foo1", + key = "foo-1", + }, + { + routeName = "foo2", + key = "foo-2", + } + } + }, 2) + + expect(result.routeName).to.equal("foo2") + expect(result.key).to.equal("foo-2") + end) + end) + + describe("StateUtils.getActiveRoute tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.getActiveRoute(nil) + end).to.throw() + end) + + it("should return nil if no routes", function() + local result = StateUtils.getActiveRoute({ + index = 0, + routes = {}, + }) + + expect(result).to.equal(nil) + end) + + it("should return active route", function() + local result = StateUtils.getActiveRoute({ + index = 1, + routes = { + { + routeName = "active", + key = "active-1", + } + }, + }) + + expect(result.routeName).to.equal("active") + expect(result.key).to.equal("active-1") + end) + end) + + describe("StateUtils.indexOf tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.indexOf(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.indexOf({}, 5) + end).to.throw() + end) + + it("should return nil if key is not found in routes", function() + local result = StateUtils.indexOf({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + }, + }, "key") + + expect(result).to.equal(nil) + end) + + it("should return index if key is found in routes", function() + local result = StateUtils.indexOf({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + }, + { + routeName = "foo2", + key = "foo-2", + } + }, + }, "foo-2") + + expect(result).to.equal(2) + end) + end) + + describe("StateUtils.has tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.has(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.has({}, 5) + end).to.throw() + end) + + it("should return false if key is not in routes", function() + local result = StateUtils.has({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + } + }, "key") + + expect(result).to.equal(false) + end) + + it("should return true if key is found in routes", function() + local result = StateUtils.has({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + } + }, "foo-1") + + expect(result).to.equal(true) + end) + end) + + describe("StateUtils.push tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.push(nil, {}) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.push({}, 5) + end).to.throw() + end) + + it("should assert if route.key is already present", function() + expect(function() + StateUtils.push({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + } + }, { + routeName = "foo", + key = "foo-1", + }) + end).to.throw() + end) + + it("should insert new route if it doesn't exist", function() + local newState = StateUtils.push({ + index = 1, + routes = { + { + routeName = "first", + key = "foo-1", + }, + }, + }, { + routeName = "second", + key = "foo-2", + }) + + expect(newState.index).to.equal(2) + expect(#newState.routes).to.equal(2) + expect(newState.routes[newState.index].key).to.equal("foo-2") + expect(newState.routes[newState.index].routeName).to.equal("second") + end) + end) + + describe("StateUtils.pop tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.pop(nil) + end).to.throw() + end) + + it("should return existing state if routes is empty", function() + local initialState = { + index = 0, + routes = {}, + } + + local newState = StateUtils.pop(initialState) + expect(newState).to.equal(initialState) + end) + + it("should return empty state if popping one route", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "route-1", }, + }, + } + + local newState = StateUtils.pop(initialState) + expect(newState.index).to.equal(0) + expect(#newState.routes).to.equal(0) + end) + + it("should remove top route if popping with more than one route", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "route-1", }, + { routeName = "route", key = "route-2", }, + }, + } + + local newState = StateUtils.pop(initialState) + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(1) + expect(newState.routes[1].key).to.equal("route-1") + end) + end) + + describe("StateUtils.jumpToIndex tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.jumpToIndex(nil, 0) + end).to.throw() + end) + + it("should assert if index is not a number", function() + expect(function() + StateUtils.jumpToIndex({}, "foo") + end).to.throw() + end) + + it("should assert if index does not match a route", function() + expect(function() + StateUtils.jumpToIndex({ + index = 1, + routes = { { routeName = "first", key = "first-1" } } + }, 5) + end).to.throw() + end) + + it("should return original state if index matches current", function() + local initialState = { + index = 1, + routes = { { routeName = "one", key = "1" } } + } + + local newState = StateUtils.jumpToIndex(initialState, 1) + expect(newState).to.equal(initialState) + end) + + it("should return updated state if index differs", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "route-1" }, + { routeName = "route", key = "route-2" }, + }, + } + + local newState = StateUtils.jumpToIndex(initialState, 2) + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.jumpTo tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.jumpTo(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.jumpTo({}, 0) + end).to.throw() + end) + + it("should return original state if key is already active route", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.jumpTo(initialState, "key-1") + expect(newState).to.equal(initialState) + end) + + it("should return state with new active route if key is not active", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.jumpTo(initialState, "key-2") + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.back tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.back(nil) + end).to.throw() + end) + + it("should return original state if route for new index does not exist", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + } + } + + local newState = StateUtils.back(initialState) + expect(newState).to.equal(initialState) + end) + + it("should remove top state if there is somewhere to go", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.back(initialState) + expect(newState.index).to.equal(1) + end) + end) + + describe("StateUtils.forward tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.forward(nil) + end).to.throw() + end) + + it("should not walk off the end of the route list", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + } + } + + local newState = StateUtils.forward(initialState) + expect(newState).to.equal(initialState) + end) + + it("should move to next route if available", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.forward(initialState) + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.replaceAndPrune tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.replaceAndPrune(nil, "key", {}) + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.replaceAndPrune({}, 0, {}) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.replaceAndPrune({}, "key", 0) + end).to.throw() + end) + + it("should replace matching route and prune following routes", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.replaceAndPrune(initialState, "key-1", { + routeName = "newRoute", key = "key-3" + }) + + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(1) + expect(newState.routes[1].routeName).to.equal("newRoute") + expect(newState.routes[1].key).to.equal("key-3") + end) + end) + + describe("StateUtils.replaceAt tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.replaceAt(nil, "key", {}, false) + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.replaceAt({}, 0, {}, false) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.replaceAt({}, "key", 0, false) + end).to.throw() + end) + + it("should assert if preserveIndex is not a boolean", function() + expect(function() + StateUtils.replaceAt({}, "key", {}, 0) + end).to.throw() + end) + + it("should replace matching route, not prune, and update index", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.replaceAt(initialState, "key-1", { + routeName = "newRoute", key = "key-3" + }, false) + + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(2) + expect(newState.routes[1].routeName).to.equal("newRoute") + expect(newState.routes[1].key).to.equal("key-3") + end) + + it("should replace matching route, not prune, and preserve existing index", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.replaceAt(initialState, "key-1", { + routeName = "newRoute", key = "key-3" + }, true) + + expect(newState.index).to.equal(2) + expect(#newState.routes).to.equal(2) + expect(newState.routes[1].routeName).to.equal("newRoute") + expect(newState.routes[1].key).to.equal("key-3") + end) + end) + + describe("StateUtils.replaceAtIndex tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.replaceAtIndex(nil, 0, {}) + end).to.throw() + end) + + it("should assert if index is not a number", function() + expect(function() + StateUtils.replaceAtIndex({}, nil, {}) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.replaceAtIndex({}, 5, nil) + end).to.throw() + end) + + it("should assert if index does not exist", function() + expect(function() + StateUtils.replaceAtIndex({ + index = 0, + routes = {} + }, 5, { routeName = "name", key = "key" }) + end).to.throw() + end) + + it("should return original state if inputs are same", function() + local testRoute = { routeName = "name", key = "key" } + local initialState = { + index = 1, + routes = { testRoute }, + } + + local newState = StateUtils.replaceAtIndex(initialState, 1, testRoute) + expect(newState).to.equal(initialState) + end) + + it("should replace route at index if route is not equal", function() + local initialState = { + index = 1, + routes = { + { routeName = "name", key = "key" } + }, + } + + local newState = StateUtils.replaceAtIndex(initialState, 1, { + routeName = "newName", + key = "key", + }) + + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(1) + expect(newState.routes[1].routeName).to.equal("newName") + expect(newState.routes[1].key).to.equal("key") + end) + + it("should update index, if new index differs but route does not", function() + local testRoute = { routeName = "name", key = "key-2" } + local initialState = { + index = 1, + routes = { + { routeName = "name", key = "key-1" }, + testRoute, + } + } + + local newState = StateUtils.replaceAtIndex(initialState, 2, testRoute) + expect(newState).never.to.equal(initialState) + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.reset tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.reset(nil, {}, 0) + end).to.throw() + end) + + it("should assert if routes is not a table", function() + expect(function() + StateUtils.reset({}, nil, 0) + end).to.throw() + end) + + it("should assert if index is not a number", function() + expect(function() + StateUtils.reset({}, {}, "foo") + end).to.throw() + end) + + it("should NOT assert if index is nil", function() + expect(function() + StateUtils.reset({}, {}) + end).to.throw() + end) + + it("should return original state if index matches and all routes are same objects", function() + local route1 = { routeName = "route1", key = "route-1" } + local route2 = { routeName = "route2", key = "route-2" } + + local initialState = { + index = 2, + routes = { route1, route2 }, + } + + local newState = StateUtils.reset(initialState, { + route1, + route2, + }, 2) + + expect(newState).to.equal(initialState) + end) + + it("should update state if index is not specified and old index is not last route", function() + local route1 = { routeName = "route1", key = "route-1" } + local route2 = { routeName = "route2", key = "route-2" } + + local initialState = { + index = 1, + routes = { route1, route2 }, + } + + local newState = StateUtils.reset(initialState, { + route1, + route2, + }) + + expect(newState).never.to.equal(initialState) + expect(newState.index).to.equal(2) + end) + + it("should update state if index matches but routes differ", function() + local route1 = { routeName = "route1", key = "route-1" } + local route2 = { routeName = "route2", key = "route-2" } + + local initialState = { + index = 1, + routes = { route1, route2 }, + } + + local newState = StateUtils.reset(initialState, { + route1, + { routeName = "route3", key = "route-3" }, + }, 1) + + expect(newState).never.to.equal(initialState) + expect(#newState.routes).to.equal(2) + expect(newState.index).to.equal(1) + expect(newState.routes[2].routeName).to.equal("route3") + expect(newState.routes[2].key).to.equal("route-3") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/createAppContainer.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/createAppContainer.spec.lua new file mode 100644 index 0000000..c691891 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/createAppContainer.spec.lua @@ -0,0 +1,97 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Roact) + local createAppContainer = require(script.Parent.Parent.createAppContainer) + local createSwitchNavigator = require(script.Parent.Parent.navigators.createSwitchNavigator) + + it("should be a function", function() + expect(type(createAppContainer)).to.equal("function") + end) + + it("should return a valid component when mounting a switch navigator", function() + local TestNavigator = createSwitchNavigator({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local TestApp = createAppContainer(TestNavigator) + local element = Roact.createElement(TestApp) + local instance = Roact.mount(element) + + Roact.unmount(instance) + end) + + it("should throw when navigator has both navigation and container props", function() + local TestAppComponent = Roact.Component:extend("TestAppComponent") + TestAppComponent.router = {} + function TestAppComponent:render() end + + local element = Roact.createElement(createAppContainer(TestAppComponent), { + navigation = {}, + somePropThatShouldNotBeHere = true, + }) + + local status, err = pcall(function() + Roact.mount(element) + end) + + expect(status).to.equal(false) + expect(string.find(err, "This navigator has both 'navigation' and container props.")).to.never.equal(nil) + end) + + it("should throw when not passed a table for AppComponent", function() + local TestAppComponent = 5 + + local status, err = pcall(function() + createAppContainer(TestAppComponent) + end) + + expect(status).to.equal(false) + expect(string.find(err, "AppComponent must be a navigator or a stateful Roact " .. + "component with a 'router' field")).to.never.equal(nil) + end) + + it("should throw when passed a stateful component without router field", function() + local TestAppComponent = Roact.Component:extend("TestAppComponent") + + local status, err = pcall(function() + createAppContainer(TestAppComponent) + end) + + expect(status).to.equal(false) + expect(string.find(err, "AppComponent must be a navigator or a stateful Roact " .. + "component with a 'router' field")).to.never.equal(nil) + end) + + it("should connect and disconnect from backActionSignal", function() + local TestNavigator = createSwitchNavigator({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local backHandler = nil + local backSignal = { + connect = function(handler) + backHandler = handler + return { + disconnect = function() + backHandler = nil + end + } + end + } + + local element = Roact.createElement(createAppContainer(TestNavigator), { + backActionSignal = backSignal, + }) + + local instance = Roact.mount(element) + expect(backHandler).to.never.equal(nil) + Roact.unmount(instance) + expect(backHandler).to.equal(nil) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildEventSubscriber.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildEventSubscriber.spec.lua new file mode 100644 index 0000000..09c033b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildEventSubscriber.spec.lua @@ -0,0 +1,464 @@ +return function() + local NavigationEvents = require(script.Parent.Parent.NavigationEvents) + local getChildEventSubscriber = require(script.Parent.Parent.getChildEventSubscriber) + + local function dummyAddListener() end + + local function makeListenerBundle() + local testUpstreamListenerMap = {} + local function testAddUpstreamListener(eventType, callback) + testUpstreamListenerMap[eventType] = callback + + return { + disconnect = function() + testUpstreamListenerMap[eventType] = nil + end + } + end + + return { + listenerMap = testUpstreamListenerMap, + addListener = testAddUpstreamListener, + } + end + + local SIMPLE_TEST_KEY = "Foo" + + local SIMPLE_TEST_STATE = { + state = { + routes = { + { key = SIMPLE_TEST_KEY } + }, + index = 1, + }, + lastState = { + routes = { + { key = SIMPLE_TEST_KEY } + }, + index = 1, + }, + action = { + type = "SomeAction" + }, + } + + it("should return a table with correct members", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + expect(type(childSubscriber.addListener)).to.equal("function") + expect(type(childSubscriber.emit)).to.equal("function") + end) + + describe("addListener tests", function() + it("should throw on invalid eventType", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + expect(function() + childSubscriber.addListener("BadSymbol", function() end) + end).to.throw() + end) + + it("should throw on invalid eventHandler", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + expect(function() + childSubscriber.addListener(NavigationEvents.Action, 5) + end).to.throw() + end) + + it("should allow disconnect of listener", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + local connection = childSubscriber.addListener(NavigationEvents.Refocus, function() end) + connection.disconnect() + end) + end) + + describe("emit tests", function() + it("should throw when trying to emit any event besides Refocus", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + expect(function() + childSubscriber.emit(NavigationEvents.WillFocus) + end).to.throw() + + expect(function() + childSubscriber.emit(NavigationEvents.DidFocus) + end).to.throw() + + expect(function() + childSubscriber.emit(NavigationEvents.WillBlur) + end).to.throw() + + expect(function() + childSubscriber.emit(NavigationEvents.DidBlur) + end).to.throw() + + expect(function() + childSubscriber.emit(NavigationEvents.Action) + end).to.throw() + end) + + it("should throw when payload is not a table", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + expect(function() + childSubscriber.emit(NavigationEvents.Refocus, 5) + end).to.throw() + end) + + it("should allow external caller to emit a refocus event with valid payload", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + local testPayload = { a = 1 } + local outputPayload = nil + + childSubscriber.addListener(NavigationEvents.Refocus, function(payload) + outputPayload = payload + end) + + childSubscriber.emit(NavigationEvents.Refocus, testPayload) + expect(outputPayload.a).to.equal(1) + expect(outputPayload.type).to.equal(NavigationEvents.Refocus) + end) + + it("should allow external caller to emit a refocus event with nil payload", function() + local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY) + + local outputPayload = nil + + childSubscriber.addListener(NavigationEvents.Refocus, function(payload) + outputPayload = payload + end) + + childSubscriber.emit(NavigationEvents.Refocus) + expect(outputPayload.type).to.equal(NavigationEvents.Refocus) + end) + end) + + describe("upstream event handling tests", function() + it("should register subscriptions for supported event types", function() + local testUpstreamListenerMap = {} + local function testAddUpstreamListener(eventType, callback) + expect(testUpstreamListenerMap[eventType]).to.equal(nil) + testUpstreamListenerMap[eventType] = true + + return { + disconnect = function() end + } + end + + getChildEventSubscriber(testAddUpstreamListener, SIMPLE_TEST_KEY) + + expect(testUpstreamListenerMap[NavigationEvents.Action]).to.equal(true) + expect(testUpstreamListenerMap[NavigationEvents.WillFocus]).to.equal(true) + expect(testUpstreamListenerMap[NavigationEvents.DidFocus]).to.equal(true) + expect(testUpstreamListenerMap[NavigationEvents.WillBlur]).to.equal(true) + expect(testUpstreamListenerMap[NavigationEvents.DidBlur]).to.equal(true) + expect(testUpstreamListenerMap[NavigationEvents.Refocus]).to.equal(true) + end) + + it("should disconnect subscriptions on DidBlur when there is no new route", function() + local testUpstreamListenerMap = {} + local function testAddUpstreamListener(eventType, callback) + testUpstreamListenerMap[eventType] = callback + + return { + disconnect = function() + testUpstreamListenerMap[eventType] = false + end + } + end + + local childSubscriber = getChildEventSubscriber( + testAddUpstreamListener, SIMPLE_TEST_KEY, NavigationEvents.DidBlur) + + childSubscriber.addListener(NavigationEvents.Action, function() end) + + testUpstreamListenerMap[NavigationEvents.DidBlur]({ + state = {}, + action = { + type = "SomeAction" + } + }) + + expect(testUpstreamListenerMap[NavigationEvents.Action]).to.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.WillFocus]).to.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.DidFocus]).to.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.WillBlur]).to.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.DidBlur]).to.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.Refocus]).to.equal(false) + end) + + it("should NOT disconnect subscriptions on DidBlur when there is a new route", function() + local testUpstreamListenerMap = {} + local function testAddUpstreamListener(eventType, callback) + testUpstreamListenerMap[eventType] = callback + + return { + disconnect = function() + testUpstreamListenerMap[eventType] = false + end + } + end + + local childSubscriber = getChildEventSubscriber( + testAddUpstreamListener, SIMPLE_TEST_KEY, NavigationEvents.DidBlur) + + childSubscriber.addListener(NavigationEvents.Action, function() end) + + testUpstreamListenerMap[NavigationEvents.DidBlur](SIMPLE_TEST_STATE) + + expect(testUpstreamListenerMap[NavigationEvents.Action]).to.never.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.WillFocus]).to.never.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.DidFocus]).to.never.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.WillBlur]).to.never.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.DidBlur]).to.never.equal(false) + expect(testUpstreamListenerMap[NavigationEvents.Refocus]).to.never.equal(false) + end) + + it("should propagate refocus event from upstream", function() + local bundle = makeListenerBundle() + + local outputPayload = nil + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY) + childSubscriber.addListener(NavigationEvents.Refocus, function(payload) + outputPayload = payload + end) + + bundle.listenerMap[NavigationEvents.Refocus]({ a = 1 }) + + expect(outputPayload.a).to.equal(1) + expect(outputPayload.type).to.equal(NavigationEvents.Refocus) + end) + + it("should emit WillFocus on WillFocus event when previously blurred and child is current index", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY) + + local willFocusPayload = nil + childSubscriber.addListener(NavigationEvents.WillFocus, function(payload) + willFocusPayload = payload + end) + + bundle.listenerMap[NavigationEvents.WillFocus](SIMPLE_TEST_STATE) + + -- Detailed analysis of generated payload. Further tests will just check that functor was called. + expect(willFocusPayload).to.never.equal(nil) + expect(willFocusPayload.state).to.never.equal(nil) + expect(willFocusPayload.lastState).to.never.equal(nil) + expect(willFocusPayload.action.type).to.equal("SomeAction") + expect(willFocusPayload.type).to.equal(NavigationEvents.WillFocus) + end) + + it("should emit WillFocus AND DidFocus on Action event when previously blurred and child is current index", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY) + + local willFocusCalled = false + childSubscriber.addListener(NavigationEvents.WillFocus, function() + willFocusCalled = true + end) + + local didFocusCalled = false + childSubscriber.addListener(NavigationEvents.DidFocus, function() + didFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action](SIMPLE_TEST_STATE) + expect(willFocusCalled).to.equal(true) + expect(didFocusCalled).to.equal(true) + end) + + it("should NOT emit WillFocus or DidFocus on Action event when previously blurred and child is NOT current index", + function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY) + + local willFocusCalled = false + childSubscriber.addListener(NavigationEvents.WillFocus, function() + willFocusCalled = true + end) + + local didFocusCalled = false + childSubscriber.addListener(NavigationEvents.DidFocus, function() + didFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action]({ + state = { + routes = { + { key = SIMPLE_TEST_KEY }, + { key = "NOT_SIMPLE_TEST_KEY" }, + }, + index = 2, + }, + action = { + "SomeAction" + }, + }) + + expect(willFocusCalled).to.equal(false) + expect(didFocusCalled).to.equal(false) + end) + + it("should emit DidFocus on DidFocus event when previous event was WillFocus and child is current index", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, NavigationEvents.WillFocus) + + local didFocusCalled = false + childSubscriber.addListener(NavigationEvents.DidFocus, function() + didFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.DidFocus](SIMPLE_TEST_STATE) + + expect(didFocusCalled).to.equal(true) + end) + + it("should emit DidFocus on Action event when previous event was WillFocus and child is current index", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, NavigationEvents.WillFocus) + + local didFocusCalled = false + childSubscriber.addListener(NavigationEvents.DidFocus, function() + didFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action](SIMPLE_TEST_STATE) + + expect(didFocusCalled).to.equal(true) + end) + + it("should NOT emit DidFocus on DidFocus event when previous event was WillFocus while transitioning", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, NavigationEvents.WillFocus) + + local didFocusCalled = false + childSubscriber.addListener(NavigationEvents.DidFocus, function() + didFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.DidFocus]({ + state = { + routes = { + { key = SIMPLE_TEST_KEY } + }, + index = 1, + isTransitioning = true, + }, + action = { + "SomeAction" + }, + }) + + expect(didFocusCalled).to.equal(false) + end) + + it("should emit WillBlur on WillBlur event when previous event was DidFocus", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, NavigationEvents.DidFocus) + + local willBlurCalled = false + childSubscriber.addListener(NavigationEvents.WillBlur, function() + willBlurCalled = true + end) + + bundle.listenerMap[NavigationEvents.WillBlur](SIMPLE_TEST_STATE) + + expect(willBlurCalled).to.equal(true) + end) + + it("should emit Action on Action event when previous event was DidFocus", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, NavigationEvents.DidFocus) + + local actionCalled = false + childSubscriber.addListener(NavigationEvents.Action, function() + actionCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action](SIMPLE_TEST_STATE) + + expect(actionCalled).to.equal(true) + end) + + it("should emit DidBlur on DidBlur event when previous event was WillBlur", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, NavigationEvents.WillBlur) + + local didBlurCalled = false + childSubscriber.addListener(NavigationEvents.DidBlur, function() + didBlurCalled = true + end) + + bundle.listenerMap[NavigationEvents.DidBlur](SIMPLE_TEST_STATE) + + expect(didBlurCalled).to.equal(true) + end) + + it("should emit DidBlur on Action event when previous event was WillBlur and we've finished transitioning", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, "Foo", NavigationEvents.WillBlur) + + local didBlurCalled = false + childSubscriber.addListener(NavigationEvents.DidBlur, function() + didBlurCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action]({ + state = { + routes = { + { key = "Foo" }, -- Transitioned away from this route! + { key = "Bar" }, + }, + index = 2, + }, + lastState = { + routes = { + { key = "Foo" }, + { key = "Bar" }, + }, + index = 1, + }, + action = { + type = "SomeAction" + }, + }) + + expect(didBlurCalled).to.equal(true) + end) + + it("should emit WillFocus on Action event when previois event was WillBlur, while transitioning to child", function() + local bundle = makeListenerBundle() + local childSubscriber = getChildEventSubscriber(bundle.addListener, "Bar", NavigationEvents.WillBlur) + + local willFocusCalled = false + childSubscriber.addListener(NavigationEvents.WillFocus, function() + willFocusCalled = true + end) + + bundle.listenerMap[NavigationEvents.Action]({ + state = { + routes = { + { key = "Foo" }, -- Transitioned away from this route! + { key = "Bar" }, + }, + index = 2, + isTransitioning = true, + }, + lastState = { + routes = { + { key = "Foo" }, + { key = "Bar" }, + }, + index = 1, + }, + action = { + type = "SomeAction" + }, + }) + + expect(willFocusCalled).to.equal(true) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildNavigation.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildNavigation.spec.lua new file mode 100644 index 0000000..1ebdf8a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildNavigation.spec.lua @@ -0,0 +1,119 @@ +return function() + local getChildNavigation = require(script.Parent.Parent.getChildNavigation) + + it("should return nil if there is no route matching requested key", function() + local testNavigation = { + state = { + routes = { + { key = "a" } + } + } + } + + local childNav = getChildNavigation(testNavigation, "invalid_child", function() + return testNavigation + end) + + expect(childNav).to.equal(nil) + end) + + it("should return cached child if its state is a top-level route", function() + local testNavigation = { + state = { + routes = { + { key = "a" } + }, + + }, + } + + testNavigation._childrenNavigation = { + a = { + state = testNavigation.state.routes[1] + } + } + + local childNav = getChildNavigation(testNavigation, "a", function() + return testNavigation + end) + + expect(childNav).to.equal(testNavigation._childrenNavigation.a) + end) + + it("should update cache and return new data when child's state has changed", function() + local testNavigation = { + state = { + routes = { + { key = "a", routeName = "a" }, + { key = "b", routeName = "b" }, + }, + index = 1, + }, + router = { + getComponentForRouteName = function(routeName) + return function() end + end, + getActionCreators = function() end + } + } + + local oldStateA = { + state = { + routes = { + { key = "a", routeName = "a" }, + { key = "b", routeName = "b" }, + }, + index = 2, + }, + } + + testNavigation._childrenNavigation = { + a = oldStateA + } + + local childNav = getChildNavigation(testNavigation, "a", function() + return testNavigation + end) + + expect(childNav).to.equal(testNavigation._childrenNavigation["a"]) + expect(childNav.state).to.equal(testNavigation.state.routes[1]) + expect(type(childNav.getParam)).to.equal("function") + end) + + it("should create a new entry if cached child does not exist yet", function() + local testNavigation = { + state = { + routes = { + { key = "a", routeName = "a", params = { a = 1 } }, + { key = "b", routeName = "b" }, + }, + index = 1, + }, + router = { + getComponentForRouteName = function(routeName) + return function() end + end, + getActionCreators = function() end, + }, + addListener = function() + return { + disconnect = function() end + } + end, + isFocused = function() + return true + end, + } + + local childNav = getChildNavigation(testNavigation, "a", function() + return testNavigation + end) + + expect(testNavigation._childrenNavigation["a"]).to.never.equal(nil) + expect(childNav).to.equal(testNavigation._childrenNavigation["a"]) + expect(childNav.isFocused()).to.equal(true) + + expect(childNav.getParam("a", 0)).to.equal(1) + expect(childNav.getParam("b", 0)).to.equal(0) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildrenNavigationCache.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildrenNavigationCache.spec.lua new file mode 100644 index 0000000..b96a218 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getChildrenNavigationCache.spec.lua @@ -0,0 +1,63 @@ +return function() + local getChildrenNavigationCache = require(script.Parent.Parent.getChildrenNavigationCache) + + it("should return empty table if navigation arg not provided", function() + expect(getChildrenNavigationCache()._childrenNavigation).to.equal(nil) + end) + + it("should populate navigation._childrenNavigation as a side-effect", function() + local navigation = { state = {} } + local result = getChildrenNavigationCache(navigation) + expect(result).to.never.equal(nil) + expect(navigation._childrenNavigation).to.equal(result) + end) + + it("should delete children cache keys that are no longer valid", function() + local navigation = { + state = { + routes = { + { key = "one" }, + { key = "two" }, + { key = "three" }, + } + }, + _childrenNavigation = { + one = {}, + two = {}, + three = {}, + four = {}, + } + } + + local result = getChildrenNavigationCache(navigation) + expect(result.one).to.never.equal(nil) + expect(result.two).to.never.equal(nil) + expect(result.three).to.never.equal(nil) + expect(result.four).to.equal(nil) + end) + + it("should not delete children cache keys if in transitioning state", function() + local navigation = { + state = { + routes = { + { key = "one" }, + { key = "two" }, + { key = "three" }, + }, + isTransitioning = true, + }, + _childrenNavigation = { + one = {}, + two = {}, + three = {}, + four = {}, + } + } + + local result = getChildrenNavigationCache(navigation) + expect(result.one).to.never.equal(nil) + expect(result.two).to.never.equal(nil) + expect(result.three).to.never.equal(nil) + expect(result.four).to.never.equal(nil) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getNavigation.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getNavigation.spec.lua new file mode 100644 index 0000000..bfd1a59 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/getNavigation.spec.lua @@ -0,0 +1,94 @@ +return function() + local NavigationEvents = require(script.Parent.Parent.NavigationEvents) + local getNavigation = require(script.Parent.Parent.getNavigation) + + local function makeTestBundle(testState) + testState = testState or { + routes = { + { key = "a" } + }, + index = 1, + } + + local testActions = {} + local bundle = { + testActions = testActions, + testState = testState, + testRouter = { + getActionCreators = function() + return testActions + end + }, + testDispatch = function() end, + testActionSubscribers = {}, + testGetScreenProps = function() end, + } + + function bundle.testGetCurrentNavigation() + return bundle.navigation + end + + bundle.navigation = getNavigation( + bundle.testRouter, + bundle.testState, + bundle.testDispatch, + bundle.testActionSubscribers, + bundle.testGetScreenProps, + bundle.testGetCurrentNavigation + ) + + return bundle + end + + it("should build out correct public props", function() + local bundle = makeTestBundle() + + expect(bundle.navigation.actions).to.equal(bundle.testActions) + expect(bundle.navigation.router).to.equal(bundle.testRouter) + expect(bundle.navigation.state).to.equal(bundle.testState) + expect(bundle.navigation.dispatch).to.equal(bundle.testDispatch) + expect(bundle.navigation.getScreenProps).to.equal(bundle.testGetScreenProps) + expect(#bundle.navigation._childrenNavigation).to.equal(0) + end) + + describe("isFocused tests", function() + it("should return focused=true for child key matching index", function() + local bundle = makeTestBundle() + expect(bundle.navigation.isFocused("a")).to.equal(true) + end) + + it("should return focused=false for child key not matching index", function() + local bundle = makeTestBundle({ + routes = { + { key = "a" }, + { key = "b" }, + }, + index = 2, + }) + expect(bundle.navigation.isFocused("a")).to.equal(false) + end) + + it("should return focused=true if no child key provided (parent always focused)", function() + local bundle = makeTestBundle() + expect(bundle.navigation.isFocused()).to.equal(true) + end) + end) + + describe("addListener tests", function() + it("should short-circuit subscriptions for non-Action events", function() + local bundle = makeTestBundle() + + local testHandler = function() end + bundle.navigation.addListener(NavigationEvents.WillFocus, testHandler) + expect(bundle.testActionSubscribers[testHandler]).to.equal(nil) + end) + + it("should add Action event handlers to actionSubscribers set", function() + local bundle = makeTestBundle() + + local testHandler = function() end + bundle.navigation.addListener(NavigationEvents.Action, testHandler) + expect(bundle.testActionSubscribers[testHandler]).to.equal(true) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/init.spec.lua new file mode 100644 index 0000000..623fee6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/_tests_/init.spec.lua @@ -0,0 +1,178 @@ +return function() + local RoactNavigation = require(script.Parent.Parent) + local Roact = require(script.Parent.Parent.Parent.Roact) + local EdgeInsets = require(script.Parent.Parent.EdgeInsets) + local StackHeaderMode = require(script.Parent.Parent.views.StackView.StackHeaderMode) + local StackPresentationStyle = require(script.Parent.Parent.views.StackView.StackPresentationStyle) + local NoneSymbol = require(script.Parent.Parent.NoneSymbol) + + it("should load", function() + require(script.Parent.Parent) + end) + + it("should return a function for createAppContainer", function() + expect(type(RoactNavigation.createAppContainer)).to.equal("function") + end) + + it("should return a function for getNavigation", function() + expect(type(RoactNavigation.getNavigation)).to.equal("function") + end) + + it("should return an appropriate table for Context", function() + expect(type(RoactNavigation.Context)).to.equal("table") + expect(type(RoactNavigation.Context.Provider)).to.equal("table") + expect(type(RoactNavigation.Context.Consumer)).to.equal("table") + expect(type(RoactNavigation.Context.connect)).to.equal("function") + end) + + it("should return a table for Provider", function() + expect(type(RoactNavigation.Provider)).to.equal("table") + end) + + it("should return a table for Consumer", function() + expect(type(RoactNavigation.Consumer)).to.equal("table") + end) + + it("should return a function for connect", function() + expect(type(RoactNavigation.connect)).to.equal("function") + end) + + it("should return a function for withNavigation", function() + expect(type(RoactNavigation.withNavigation)).to.equal("function") + end) + + it("should return a function for withNavigationFocus", function() + expect(type(RoactNavigation.withNavigationFocus)).to.equal("function") + end) + + it("should return a function for createSwitchNavigator", function() + expect(type(RoactNavigation.createSwitchNavigator)).to.equal("function") + end) + + it("should return a function for createTopBarStackNavigator", function() + expect(type(RoactNavigation.createTopBarStackNavigator)).to.equal("function") + end) + + it("should return a function for createNavigator", function() + expect(type(RoactNavigation.createNavigator)).to.equal("function") + end) + + it("should return a function for StackRouter", function() + expect(type(RoactNavigation.StackRouter)).to.equal("function") + end) + + it("should return a function for SwitchRouter", function() + expect(type(RoactNavigation.SwitchRouter)).to.equal("function") + end) + + it("should return a function for TabRouter", function() + expect(type(RoactNavigation.TabRouter)).to.equal("function") + end) + + it("should return a table for Actions", function() + expect(type(RoactNavigation.Actions)).to.equal("table") + end) + + it("should return a table for StackActions", function() + expect(type(RoactNavigation.StackActions)).to.equal("table") + end) + + it("should return a table for BackBehavior", function() + expect(type(RoactNavigation.BackBehavior)).to.equal("table") + end) + + it("should return a table for Events", function() + expect(type(RoactNavigation.Events)).to.equal("table") + end) + + it("should return a valid component for EventsAdapter", function() + expect(RoactNavigation.EventsAdapter.render).never.to.equal(nil) + local instance = Roact.mount(Roact.createElement(RoactNavigation.EventsAdapter, { + navigation = { + addListener = function() + return { disconnect = function() end } + end + } + })) + Roact.unmount(instance) + end) + + it("should return EdgeInsets", function() + expect(RoactNavigation.EdgeInsets).to.equal(EdgeInsets) + end) + + it("should return StackPresentationStyle", function() + expect(RoactNavigation.StackPresentationStyle).to.equal(StackPresentationStyle) + end) + + it("should return StackHeaderMode", function() + expect(RoactNavigation.StackHeaderMode).to.equal(StackHeaderMode) + end) + + it("should return NoneSymbol", function() + expect(RoactNavigation.None).to.equal(NoneSymbol) + end) + + it("should return a valid component for SceneView", function() + expect(RoactNavigation.SceneView.render).never.to.equal(nil) + local instance = Roact.mount(Roact.createElement(RoactNavigation.SceneView, { + navigation = {}, + component = function() end, + })) + Roact.unmount(instance) + end) + + it("should return a valid component for SwitchView", function() + expect(RoactNavigation.SwitchView.render).never.to.equal(nil) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo", } + }, + index = 1, + } + } + + local instance = Roact.mount(Roact.createElement(RoactNavigation.SwitchView, { + descriptors = { + Foo = { + getComponent = function() + return function() end + end, + navigation = testNavigation, + } + }, + navigation = testNavigation, + })) + Roact.unmount(instance) + end) + + it("should return a table for TopBar", function() + expect(type(RoactNavigation.TopBar)).to.equal("table") + end) + + it("should return a table for TopBarBackButton", function() + expect(type(RoactNavigation.TopBarBackButton)).to.equal("table") + end) + + it("should return a table for TopBarTitleContainer", function() + expect(type(RoactNavigation.TopBarTitleContainer)).to.equal("table") + end) + + it("should return a function for createConfigGetter", function() + expect(type(RoactNavigation.createConfigGetter)).to.equal("function") + end) + + it("should return a function for getScreenForRouteName", function() + expect(type(RoactNavigation.getScreenForRouteName)).to.equal("function") + end) + + it("should return a function for validateRouteConfigMap", function() + expect(type(RoactNavigation.validateRouteConfigMap)).to.equal("function") + end) + + it("should return a function for getActiveChildNavigationOptions", function() + expect(type(RoactNavigation.getActiveChildNavigationOptions)).to.equal("function") + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/createAppContainer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/createAppContainer.lua new file mode 100644 index 0000000..fec578a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/createAppContainer.lua @@ -0,0 +1,264 @@ +local Roact = require(script.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Cryo) +local NavigationActions = require(script.Parent.NavigationActions) +local NavigationEvents = require(script.Parent.NavigationEvents) +local AppNavigationContext = require(script.Parent.views.AppNavigationContext) +local getNavigation = require(script.Parent.getNavigation) +local validate = require(script.Parent.utils.validate) + +local function validateProps(props) + if not props.navigation then + return + end + + local errStr = + "This navigator has both 'navigation' and container props. " .. + "It is unclear if it should own its own state. Remove the " .. + "container props or don't pass a 'navigation' prop." + + for key in pairs(props) do + validate(key == "screenProps" or key == "navigation", errStr) + end +end + +--[[ + Construct a container Roact component that will host the navigation hierarchy + specified by your main AppComponent. AppComponent must be a navigator created by + a Roact-Navigation helper function, or a stateful Roact component + + If you are using a custom stateful Roact component, make sure to set the 'router' + field so that it can be hooked into the navigation system. You must also pass your + 'navigation' prop to any child navigators. + + Additional props: + renderLoading - Roact component to render while the app is loading. + backActionSignal - Signal that allows the container to listen to external + back action events (e.g. Android back button). +]] +return function(AppComponent) + validate(type(AppComponent) == "table" and AppComponent.router ~= nil, + "AppComponent must be a navigator or a stateful Roact component with a 'router' field") + + local containerName = string.format("NavigationContainer(%s)", tostring(AppComponent)) + local NavigationContainer = Roact.Component:extend(containerName) + + function NavigationContainer.getDerivedStateFromProps(nextProps) + validateProps(nextProps) + return nil + end + + function NavigationContainer:init() + validateProps(self.props) + + local backActionSignal = self.props.backActionSignal + + self._actionEventSubscribers = {} + self._initialAction = NavigationActions.init() + + local containerIsStateful = self:_isStateful() + + if containerIsStateful and backActionSignal ~= nil then + self.subs = backActionSignal.connect(function() + if not self._isMounted then + if self.subs then + self.subs.disconnect() + self.subs = nil + end + else + self:dispatch(NavigationActions.back()) + end + end) + end + + local initialNav = nil + if containerIsStateful and not self.props.persistenceKey then + initialNav = AppComponent.router.getStateForAction(self._initialAction) + end + + self.state = { + nav = initialNav, + } + end + + function NavigationContainer:_renderLoading() + local renderLoading = self.props.renderLoading + if renderLoading then + return renderLoading() + else + return nil + end + end + + function NavigationContainer:render() + local navigation = self.props.navigation + + if self:_isStateful() then + local navState = self.state.nav + if not navState then + return self:_renderLoading() + end + + if not self._navigation or self._navigation.state ~= navState then + self._navigation = getNavigation( + AppComponent.router, + navState, + function(...) + return self:dispatch(...) + end, + self._actionEventSubscribers, + function(...) + return self:_getScreenProps(...) + end, + function() + return self._navigation + end + ) + end + + navigation = self._navigation + end + + validate(navigation ~= nil, "failed to get navigation") + + return Roact.createElement(AppNavigationContext.Provider, { + navigation = navigation, + }, { + -- Provide navigation prop for top-level component so it doesn't have to connect. + AppComponent = Roact.createElement(AppComponent, Cryo.Dictionary.join(self.props, { + navigation = navigation, + })) + }) + end + + function NavigationContainer:didMount() + self._isMounted = true + + if not self:_isStateful() then + return + end + + local action = self._initialAction + local startupState = self.state.nav + + if not startupState then + startupState = AppComponent.router.getStateForAction(action) + end + + local function dispatchActionEvents() + -- _actionEventSubscribers is a table(handler, true), e.g. a Set container + for subscriber in pairs(self._actionEventSubscribers) do + subscriber({ + type = NavigationEvents.Action, + action = action, + state = self.state.nav, + -- there is no lastState for initial mounting + }) + end + end + + if startupState ~= self.state.nav then + self:setState({ + nav = startupState + }) + end + + dispatchActionEvents() + end + + function NavigationContainer:willUnmount() + self._isMounted = false + + -- TODO: Disconnect from from URL listener once implemented + + if self.subs then + self.subs.disconnect() + self.subs = nil + end + end + + function NavigationContainer:didUpdate() + -- Clear cached _navState every time we update. + if self._navState == self.state.nav then + self._navState = nil + end + end + + function NavigationContainer:_isStateful() + return not self.props.navigation + end + + -- NOTE: Not implementing _validateProps; it is duplicate + + -- NOTE: Not implementing _handleOpenURL; app should have a component + -- that transforms URLs into paths for AppContainer instead. + + function NavigationContainer:_onNavigationStateChange(prevNav, nextNav, action) + local onNavigationStateChange = self.props.onNavigationStateChange + + if type(onNavigationStateChange) == "function" then + onNavigationStateChange(prevNav, nextNav, action) + end + end + + function NavigationContainer:_getScreenProps() + return self.props.screenProps + end + + function NavigationContainer:dispatch(action) + if self.props.navigation then + return self.props.navigation.dispatch(action) + end + + self._navState = self._navState or self.state.nav + + local lastNavState = self._navState + validate(lastNavState ~= nil, "navState should be set in constructor if stateful") + + local reducedState = AppComponent.router.getStateForAction(action, lastNavState) + local navState = reducedState + if not navState then + navState = lastNavState + end + + local function dispatchActionEvents() + -- _actionEventSubscribers is a table(handler, true), e.g. a Set container + for subscriber in pairs(self._actionEventSubscribers) do + subscriber({ + type = NavigationEvents.Action, + action = action, + state = navState, + lastState = lastNavState, + }) + end + end + + if reducedState == nil then + -- Router returns nil when action has been handled and there is no state change. + -- dispatch() must return true whenever something has been handled. + dispatchActionEvents() + return true + end + + if navState ~= lastNavState then + -- Update cache to ensure that subsequent calls do not discard this change + self._navState = navState + + -- TODO: We have to dispatch events before or after setState (which mounts/unmounts components) + -- based upon the specific event type, to ensure that pages get them in the correct order... + + self:setState({ + nav = navState + }) + + self:_onNavigationStateChange(lastNavState, navState, action) + dispatchActionEvents() + -- TODO: Add call to persist navigation state here, if we ever implement it. + return true + end + + dispatchActionEvents() + return false + end + + return NavigationContainer +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildEventSubscriber.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildEventSubscriber.lua new file mode 100644 index 0000000..0773952 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildEventSubscriber.lua @@ -0,0 +1,181 @@ +local Cryo = require(script.Parent.Parent.Cryo) +local NavigationEvents = require(script.Parent.NavigationEvents) +local validate = require(script.Parent.utils.validate) + +--[[ + This utility will fire focus and blur events for the child based upon action events + and the current navigation state. +]] +return function(addListener, key, initialLastFocusEvent) + initialLastFocusEvent = initialLastFocusEvent or NavigationEvents.DidBlur + + local upstreamSubscribers = {} + + local subscriberMap = { + [NavigationEvents.Action] = {}, + [NavigationEvents.WillFocus] = {}, + [NavigationEvents.DidFocus] = {}, + [NavigationEvents.WillBlur] = {}, + [NavigationEvents.DidBlur] = {}, + [NavigationEvents.Refocus] = {}, + } + + local function disconnectAll() + for _, subscriberList in pairs(subscriberMap) do + for x in pairs(subscriberList) do + subscriberList[x] = nil + end + end + + for _, subs in pairs(upstreamSubscribers) do + if subs then + subs.disconnect() + end + end + end + + local function emit(subscriberType, payload) + local payloadWithType = Cryo.Dictionary.join(payload or {}, { type = subscriberType }) + local subscribers = subscriberMap[subscriberType] + if subscribers then + for _, subs in ipairs(subscribers) do + subs(payloadWithType) + end + end + end + + -- lastFocusEvent keeps track of focus state for one route. We assume that we are initially + -- in blurred state. If we are focused on initialization, then the first NavigationEvents.Action + -- will cause onFocus+willFocus to fire because we started off 'blurred'. + local lastFocusEvent = initialLastFocusEvent + + for eventType in pairs(subscriberMap) do + upstreamSubscribers[eventType] = addListener(eventType, function(payload) + if eventType == NavigationEvents.Refocus then + emit(eventType, payload) + return + end + + local state = payload.state + local lastState = payload.lastState + local action = payload.action + + local lastRoutes = lastState and lastState.routes + local routes = state and state.routes + + local focusKey = routes and routes[state.index].key or nil + local isChildFocused = focusKey == key + + local lastRoute = nil + if lastRoutes then + for _, route in ipairs(lastRoutes) do + if route.key == key then + lastRoute = route + break + end + end + end + + local newRoute = nil + if routes then + for _, route in ipairs(routes) do + if route.key == key then + newRoute = route + break + end + end + end + + local childPayload = { + context = string.format("%s:%s_%s", key, tostring(action.type), payload.context or 'Root'), + state = newRoute, + lastState = lastRoute, + action = action, + type = eventType, + } + + local isTransitioning = state and state.isTransitioning or false + + local previouslyLastFocusEvent = lastFocusEvent + + if lastFocusEvent == NavigationEvents.DidBlur then + -- Child is currently blurred; look for willFocus conditions + if isChildFocused and (eventType == NavigationEvents.WillFocus + or eventType == NavigationEvents.Action) then + lastFocusEvent = NavigationEvents.WillFocus + emit(lastFocusEvent, childPayload) + end + end + + if lastFocusEvent == NavigationEvents.WillFocus then + -- We are mid-focus. Look for didFocus conditions. If state.isTransitioning is false + -- then we know this child event happens immediately after willFocus + if (eventType == NavigationEvents.DidFocus or eventType == NavigationEvents.Action) + and isChildFocused and not isTransitioning then + lastFocusEvent = NavigationEvents.DidFocus + emit(lastFocusEvent, childPayload) + end + end + + if lastFocusEvent == NavigationEvents.DidFocus then + -- The child is currently focused. Look for blurring events. + if not isChildFocused or eventType == NavigationEvents.WillBlur then + lastFocusEvent = NavigationEvents.WillBlur + emit(lastFocusEvent, childPayload) + elseif eventType == NavigationEvents.Action + and previouslyLastFocusEvent == NavigationEvents.DidFocus then + -- While focused, pass action events to children to be handled by focused grandchildren + emit(NavigationEvents.Action, childPayload) + end + end + + if lastFocusEvent == NavigationEvents.WillBlur then + -- The child is mid-blur. Wait for transition to end. + if eventType == NavigationEvents.Action and not isChildFocused and not isTransitioning then + -- Child is done blurring because transitioning ended or there is no transition to do + lastFocusEvent = NavigationEvents.DidBlur + emit(lastFocusEvent, childPayload) + elseif eventType == NavigationEvents.DidBlur then + -- Pass through parent's DidBlur event + lastFocusEvent = NavigationEvents.DidBlur + emit(lastFocusEvent, childPayload) + elseif eventType == NavigationEvents.Action and isChildFocused and isTransitioning then + lastFocusEvent = NavigationEvents.WillFocus + emit(lastFocusEvent, childPayload) + end + end + + if lastFocusEvent == NavigationEvents.DidBlur and not newRoute then + -- Page is dead, disconnect subscribers + disconnectAll() + end + end) + end + + return { + addListener = function(eventType, eventHandler) + local subscribers = subscriberMap[eventType] + validate(subscribers ~= nil, "Invalid event type '%s'", tostring(eventType)) + validate(type(eventHandler) == "function", + "eventHandler for '%s' must be a function", tostring(eventType)) + table.insert(subscribers, eventHandler) + return { + disconnect = function() + for idx, subs in ipairs(subscribers) do + if subs == eventHandler then + table.remove(subscribers, idx) + break + end + end + end + } + end, + emit = function(eventType, payload) + validate(eventType == NavigationEvents.Refocus, + "navigation.emit only supports NavigationEvents.Refocus currently.") + validate(payload == nil or type(payload) == "table", + "navigation.emit payloads must be a table or nil") + emit(eventType, payload) + end, + } +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildNavigation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildNavigation.lua new file mode 100644 index 0000000..b4afb1b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildNavigation.lua @@ -0,0 +1,115 @@ +local Cryo = require(script.Parent.Parent.Cryo) +local getChildEventSubscriber = require(script.Parent.getChildEventSubscriber) +local getChildRouter = require(script.Parent.routers.getChildRouter) +local getNavigationActionCreators = require(script.Parent.routers.getNavigationActionCreators) +local getChildrenNavigationCache = require(script.Parent.getChildrenNavigationCache) + +local function createParamGetter(route) + return function(paramName, defaultValue) + local params = route.params + return params and params[paramName] or defaultValue + end +end + +local function getChildNavigation(navigation, childKey, getCurrentParentNavigation) + local children = getChildrenNavigationCache(navigation) + + local childRoute = nil + for _, route in ipairs(navigation.state.routes) do + if route.key == childKey then + childRoute = route + break + end + end + + if not childRoute then + return nil + end + + local requestedChild = children[childKey] + + if requestedChild and requestedChild.state == childRoute then + return requestedChild + end + + local childRouter = getChildRouter(navigation.router, childRoute.routeName) + + -- If the route has children that match our routes schema then get a reference + -- to the focused grandchild so we can pass the correct action creators to the + -- child router so that any action that depends on the child route will behave + -- as expected. + local focusedGrandChildRoute = nil + if childRoute.routes and type(childRoute.index) == "number" then + focusedGrandChildRoute = childRoute.routes[childRoute.index] + end + + local childRouterActionCreators = childRouter and + childRouter.getActionCreators(focusedGrandChildRoute, childRoute.key) or {} + + local actionCreators = Cryo.Dictionary.join( + navigation.actions or {}, + navigation.router.getActionCreators(childRoute, navigation.state.key) or {}, + childRouterActionCreators or {}, + getNavigationActionCreators(childRoute) or {}) + + local actionHelpers = {} + for key, creator in pairs(actionCreators) do + actionHelpers[key] = function(...) + local action = creator(...) + return navigation.dispatch(action) + end + end + + if requestedChild then + -- Update cache value for requestedChild because child's state has changed + children[childKey] = Cryo.Dictionary.join(requestedChild, actionHelpers, { + state = childRoute, + router = childRouter, + actions = actionCreators, + getParam = createParamGetter(childRoute), + }) + + return children[childKey] + else + -- No cached value for requestedChild. Create a new entry. + local childSubscriber = getChildEventSubscriber(navigation.addListener, childKey) + + children[childKey] = Cryo.Dictionary.join(actionHelpers, { + state = childRoute, + router = childRouter, + actions = actionCreators, + getParam = createParamGetter(childRoute), + getChildNavigation = function(grandChildKey) + return getChildNavigation(children[childKey], grandChildKey, function() + local nav = getCurrentParentNavigation() + return nav and nav.getChildNavigation(childKey) or nil + end) + end, + isFocused = function() + local currentNavigation = getCurrentParentNavigation() + if not currentNavigation then + return false + end + + local state = currentNavigation.state + local routes = state.routes + local index = state.index + + if not currentNavigation.isFocused() then + return false + end + + -- If we're transitioning to this state then we are NOT focused until the transition is over. + return (routes[index].key == childKey and state.isTransitioning ~= true) or false + end, + dispatch = navigation.dispatch, + getScreenProps = navigation.getScreenProps, + addListener = childSubscriber.addListener, + emit = childSubscriber.emit, + }) + + return children[childKey] + end +end + +return getChildNavigation diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildrenNavigationCache.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildrenNavigationCache.lua new file mode 100644 index 0000000..b9271da --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getChildrenNavigationCache.lua @@ -0,0 +1,26 @@ +return function(navigation) + if not navigation then + return {} + end + + if not navigation._childrenNavigation then + navigation._childrenNavigation = {} + end + + local childrenNavigationCache = navigation._childrenNavigation + + local childKeys = {} + for _, route in ipairs(navigation.state.routes or {}) do + childKeys[route.key] = true + end + + if not navigation.state.isTransitioning then + for cacheKey, _ in pairs(childrenNavigationCache) do + if not childKeys[cacheKey] then + childrenNavigationCache[cacheKey] = nil + end + end + end + + return navigation._childrenNavigation +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getNavigation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getNavigation.lua new file mode 100644 index 0000000..2b8568f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/getNavigation.lua @@ -0,0 +1,52 @@ +local Cryo = require(script.Parent.Parent.Cryo) +local NavigationEvents = require(script.Parent.NavigationEvents) +local getNavigationActionCreators = require(script.Parent.routers.getNavigationActionCreators) +local getChildNavigation = require(script.Parent.getChildNavigation) +local getChildrenNavigationCache = require(script.Parent.getChildrenNavigationCache) + +return function(router, state, dispatch, actionSubscribers, getScreenProps, getCurrentNavigation) + local actions = router.getActionCreators(state, nil) + + local navigation = { + actions = actions, + router = router, + state = state, + dispatch = dispatch, + getScreenProps = getScreenProps, + _childrenNavigation = getChildrenNavigationCache(getCurrentNavigation()), + } + + function navigation.getChildNavigation(childKey) + return getChildNavigation(navigation, childKey, getCurrentNavigation) + end + + function navigation.isFocused(childKey) + local routes = getCurrentNavigation().state.routes + local index = getCurrentNavigation().state.index + + return not childKey or routes[index].key == childKey + end + + function navigation.addListener(event, handler) + if event ~= NavigationEvents.Action then + return { disconnect = function() end } + else + actionSubscribers[handler] = true + return { + disconnect = function() + actionSubscribers[handler] = nil + end + } + end + end + + local actionCreators = Cryo.Dictionary.join(getNavigationActionCreators(navigation.state), actions) + + for actionName, _ in pairs(actionCreators) do + navigation[actionName] = function(...) + navigation.dispatch(actionCreators[actionName](...)) + end + end + + return navigation +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/init.lua new file mode 100644 index 0000000..c159c82 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/init.lua @@ -0,0 +1,60 @@ +-- Generator information: +-- Human name: Roact Navigation +-- Variable name: RoactNavigation +-- Repo name: roact-navigation + +return { + -- Navigation container construction + createAppContainer = require(script.createAppContainer), + getNavigation = require(script.getNavigation), + + -- Context Access + Context = require(script.views.AppNavigationContext), + Provider = require(script.views.AppNavigationContext).Provider, + Consumer = require(script.views.AppNavigationContext).Consumer, + connect = require(script.views.AppNavigationContext).connect, + + withNavigation = require(script.views.withNavigation), + withNavigationFocus = require(script.views.withNavigationFocus), + + -- Navigators + createTopBarStackNavigator = require(script.navigators.createTopBarStackNavigator), + createSwitchNavigator = require(script.navigators.createSwitchNavigator), + createNavigator = require(script.navigators.createNavigator), + + -- Routers + StackRouter = require(script.routers.StackRouter), + SwitchRouter = require(script.routers.SwitchRouter), + TabRouter = require(script.routers.TabRouter), + + -- Navigation Actions + Actions = require(script.NavigationActions), + StackActions = require(script.StackActions), + BackBehavior = require(script.BackBehavior), + + -- Navigation Events + Events = require(script.NavigationEvents), + EventsAdapter = require(script.views.NavigationEventsAdapter), + + -- Additional Types + EdgeInsets = require(script.EdgeInsets), + StackPresentationStyle = require(script.views.StackView.StackPresentationStyle), + StackHeaderMode = require(script.views.StackView.StackHeaderMode), + None = require(script.NoneSymbol), + + -- Screen Views + SceneView = require(script.views.SceneView), + SwitchView = require(script.views.SwitchView), + StackView = require(script.views.StackView.StackView), + + -- Top Bar Components + TopBar = require(script.views.TopBar.TopBar), + TopBarBackButton = require(script.views.TopBar.TopBarBackButton), + TopBarTitleContainer = require(script.views.TopBar.TopBarTitleContainer), + + -- Utilities + createConfigGetter = require(script.routers.createConfigGetter), + getScreenForRouteName = require(script.routers.getScreenForRouteName), + validateRouteConfigMap = require(script.routers.validateRouteConfigMap), + getActiveChildNavigationOptions = require(script.utils.getActiveChildNavigationOptions), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createNavigator.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createNavigator.spec.lua new file mode 100644 index 0000000..e890099 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createNavigator.spec.lua @@ -0,0 +1,78 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local createNavigator = require(script.Parent.Parent.createNavigator) + + local testRouter = { + getScreenOptions = function() return nil end, + } + + it("should return a Roact component that exposes navigator fields", function() + local testComponentMounted = nil + local TestViewComponent = Roact.Component:extend("TestViewComponent") + function TestViewComponent:render() end + function TestViewComponent:didMount() testComponentMounted = true end + function TestViewComponent:willUnmount() testComponentMounted = false end + + local testNavOptions = {} + + local navigator = createNavigator(TestViewComponent, testRouter, { + navigationOptions = testNavOptions, + }) + + expect(type(navigator.render)).to.equal("function") + expect(navigator.router).to.equal(testRouter) + expect(navigator.navigationOptions).to.equal(testNavOptions) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + }, + index = 1 + }, + getChildNavigation = function() return nil end, -- stub + } + + -- Try to mount it + local instance = Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + + expect(testComponentMounted).to.equal(true) + Roact.unmount(instance) + expect(testComponentMounted).to.equal(false) + end) + + it("should throw when trying to mount without navigation prop", function() + local TestViewComponent = function() end + + local navigator = createNavigator(TestViewComponent, testRouter, { + navigationOptions = {} + }) + + expect(function() + Roact.mount(Roact.createElement(navigator)) + end).to.throw() + end) + + it("should throw when trying to mount without routes", function() + local TestViewComponent = function() end + + local navigator = createNavigator(TestViewComponent, testRouter, { + navigationOptions = {} + }) + + local testNavigation = { + state = { + index = 1 + }, + getChildNavigation = function() return nil end, -- stub + } + + expect(function() + Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + end).to.throw() + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createSwitchNavigator.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createSwitchNavigator.spec.lua new file mode 100644 index 0000000..c331c26 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createSwitchNavigator.spec.lua @@ -0,0 +1,43 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local createSwitchNavigator = require(script.Parent.Parent.createSwitchNavigator) + local getChildNavigation = require(script.Parent.Parent.Parent.getChildNavigation) + + it("should return a mountable Roact component", function() + local navigator = createSwitchNavigator({ + routes = { + Foo = function() end + }, + initialRouteName = "Foo", + }) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + }, + index = 1 + }, + router = navigator.router + } + + function testNavigation.getChildNavigation(childKey) + return getChildNavigation(testNavigation, childKey, function() + return testNavigation + end) + end + + function testNavigation.addListener(symbol, callback) + return { + disconnect = function() end + } + end + + local instance = Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + + Roact.unmount(instance) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createTopBarStackNavigator.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createTopBarStackNavigator.spec.lua new file mode 100644 index 0000000..9decdf8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/_tests_/createTopBarStackNavigator.spec.lua @@ -0,0 +1,43 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local createTopBarStackNavigator = require(script.Parent.Parent.createTopBarStackNavigator) + local getChildNavigation = require(script.Parent.Parent.Parent.getChildNavigation) + + it("should return a mountable Roact component", function() + local navigator = createTopBarStackNavigator({ + routes = { + Foo = function() end + }, + initialRouteName = "Foo", + }) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + }, + index = 1 + }, + router = navigator.router + } + + function testNavigation.getChildNavigation(childKey) + return getChildNavigation(testNavigation, childKey, function() + return testNavigation + end) + end + + function testNavigation.addListener(symbol, callback) + return { + disconnect = function() end + } + end + + local instance = Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + + Roact.unmount(instance) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createNavigator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createNavigator.lua new file mode 100644 index 0000000..5d60055 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createNavigator.lua @@ -0,0 +1,78 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local validate = require(script.Parent.Parent.utils.validate) + +return function(navigatorViewComponent, router, navigationConfig) + local Navigator = Roact.Component:extend("Navigator") + + -- These statics need to be accessible to routers + Navigator.router = router + Navigator.navigationOptions = navigationConfig.navigationOptions + + function Navigator:init() + local screenProps = self.props.screenProps + + self.state = { + descriptors = {}, + screenProps = screenProps + } + end + + function Navigator.getDerivedStateFromProps(nextProps, prevState) + local prevDescriptors = prevState.descriptors + local navigation = nextProps.navigation + local screenProps = nextProps.screenProps + + validate(navigation ~= nil, "The navigation prop is missing for this navigator") + + local routes = navigation.state.routes + + validate(type(routes) == "table", "No 'routes' found in navigation state. " .. + "Don't try to pass the navigation prop from a Roact component to a Navigator child.") + + local descriptors = {} + + for _, route in ipairs(routes) do + if prevDescriptors and prevDescriptors[route.key] and + route == prevDescriptors[route.key].state and + screenProps == prevState.screenProps then + descriptors[route.key] = prevDescriptors[route.key] + else + local getComponent = function() + return router.getComponentForRouteName(route.routeName) + end + + local childNavigation = navigation.getChildNavigation(route.key) + local options = router.getScreenOptions(childNavigation, screenProps) + + descriptors[route.key] = { + key = route.key, + getComponent = getComponent, + options = options, + state = route, + navigation = childNavigation, + } + end + end + + return { + descriptors = descriptors, + screenProps = screenProps, + } + end + + function Navigator:render() + local navigation = self.props.navigation + local screenProps = self.state.screenProps + local descriptors = self.state.descriptors + + return Roact.createElement(navigatorViewComponent, Cryo.Dictionary.join(self.props, { + screenProps = screenProps, + navigation = navigation, + navigationConfig = navigationConfig, + descriptors = descriptors, + })) + end + + return Navigator +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createSwitchNavigator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createSwitchNavigator.lua new file mode 100644 index 0000000..a99c98d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createSwitchNavigator.lua @@ -0,0 +1,12 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local createNavigator = require(script.Parent.createNavigator) +local SwitchRouter = require(script.Parent.Parent.routers.SwitchRouter) +local SwitchView = require(script.Parent.Parent.views.SwitchView) + +return function(config) + local router = SwitchRouter(config) + return createNavigator(SwitchView, router, Cryo.Dictionary.join(config, { + routes = Cryo.None, -- navigator config doesn't need routes, remove from props + })) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createTopBarStackNavigator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createTopBarStackNavigator.lua new file mode 100644 index 0000000..35697c2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/navigators/createTopBarStackNavigator.lua @@ -0,0 +1,12 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local createNavigator = require(script.Parent.createNavigator) +local StackRouter = require(script.Parent.Parent.routers.StackRouter) +local StackView = require(script.Parent.Parent.views.StackView.StackView) + +return function(config) + local router = StackRouter(config) + return createNavigator(StackView, router, Cryo.Dictionary.join(config, { + routes = Cryo.None, -- navigator config doesn't need routes + })) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/StackRouter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/StackRouter.lua new file mode 100644 index 0000000..809d18a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/StackRouter.lua @@ -0,0 +1,564 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local NavigationActions = require(script.Parent.Parent.NavigationActions) +local StackActions = require(script.Parent.Parent.StackActions) +local KeyGenerator = require(script.Parent.Parent.utils.KeyGenerator) +local StateUtils = require(script.Parent.Parent.StateUtils) +local getScreenForRouteName = require(script.Parent.getScreenForRouteName) +local createConfigGetter = require(script.Parent.createConfigGetter) +local validateRouteConfigMap = require(script.Parent.validateRouteConfigMap) +local validate = require(script.Parent.Parent.utils.validate) +local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol) +local NoneSymbol = require(script.Parent.Parent.NoneSymbol) + +local STACK_ROUTER_ROOT_KEY = "StackRouterRoot" +local CHILD_IS_SCREEN = NavigationSymbol("CHILD_IS_SCREEN") + +local defaultActionCreators = function() return {} end + +local function behavesLikePushAction(action) + return action.type == NavigationActions.Navigate or + action.type == StackActions.Push +end + +local function isResetToRootStack(action) + return action.type == StackActions.Reset and action.key == NoneSymbol +end + +return function(config) + validate(type(config) == "table", "config must be a table") + + local routeConfigs = validateRouteConfigMap(config.routes) + local routeNames = Cryo.Dictionary.keys(routeConfigs) + + -- find child child routers + local childRouters = {} + for _, routeName in ipairs(routeNames) do + local screen = getScreenForRouteName(routeConfigs, routeName) + if type(screen) == "table" and screen.router then + -- if it has a router then it's a navigator + childRouters[routeName] = screen.router + else + -- TODO: This is a hack to make this code behave like React-Navigation's usage of + -- null and undefined values for childRouters. We should come up with a better approach. + childRouters[routeName] = CHILD_IS_SCREEN + end + end + + local getCustomActionCreators = config.getCustomActionCreators or defaultActionCreators + + local initialRouteParams = config.initialRouteParams or {} + local initialRouteName = validate(config.initialRouteName, "initialRouteName must be provided") + + local initialRouteIndex = Cryo.List.find(routeNames, initialRouteName) + + -- dump an error if initialRouteName is not in routes. + if initialRouteIndex == nil then + local availableRouteStr = "" + for _, name in ipairs(routeNames) do + availableRouteStr = availableRouteStr .. name .. "," + end + + error(string.format("Invalid initialRouteName '%s'. Must be one of [%s]", initialRouteName, availableRouteStr), 2) + end + + local initialChildRouter = childRouters[initialRouteName] + + local function getInitialState() + local route = {} + + if initialChildRouter ~= nil and initialChildRouter ~= CHILD_IS_SCREEN then + route = initialChildRouter.getStateForAction(NavigationActions.init({ + params = initialRouteParams, + })) + end + + local initialRouteConfig = routeConfigs[initialRouteName] + local initialRouteConfigParams = type(initialRouteConfig) == "table" and initialRouteConfig.params or {} + + local params = Cryo.Dictionary.join( + initialRouteConfigParams, -- params set in routes table! + route.params or {}, + initialRouteParams or {} -- params provided at top level + ) + + local initialRouteKey = config.initialRouteKey + route = Cryo.Dictionary.join(route, params, { + routeName = initialRouteName, + key = initialRouteKey or KeyGenerator.generateKey() + }) + + return { + key = STACK_ROUTER_ROOT_KEY, + isTransitioning = false, + index = 1, + routes = { route } + } + end + + local function getParamsForRouteAndAction(routeName, action) + local routeConfig = routeConfigs[routeName] + if type(routeConfig) == "table" and routeConfig.params then + return Cryo.Dictionary.join(routeConfig.params, action.params) + else + return action.params + end + end + + -- Strip out the CHILD_IS_SCREEN hacked elements before exposing publicly. + local strippedChildRouters = {} + for routerName, router in pairs(childRouters) do + if router ~= CHILD_IS_SCREEN then + strippedChildRouters[routerName] = router + end + end + + local StackRouter = { + childRouters = strippedChildRouters, + getScreenOptions = createConfigGetter(routeConfigs, config.defaultNavigationOptions), + _CHILD_IS_SCREEN = CHILD_IS_SCREEN, -- expose symbol for testing purposes + } + + function StackRouter.getComponentForState(state) + local activeChildRoute = state.routes[state.index] or {} + local routeName = activeChildRoute.routeName + validate(routeName, "There is no route defined for index '%d'. " .. + "Make sure that you passed in a navigation state with a " .. + "valid stack index.", state.index) + + local childRouter = childRouters[routeName] + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + return childRouters[routeName].getComponentForState(activeChildRoute) + end + + return getScreenForRouteName(routeConfigs, routeName) + end + + function StackRouter.getComponentForRouteName(routeName) + return getScreenForRouteName(routeConfigs, routeName) + end + + function StackRouter.getActionCreators(route, navStateKey) + return Cryo.Dictionary.join(getCustomActionCreators(route, navStateKey), { + pop = function(n, params) + return StackActions.pop(Cryo.Dictionary.join({ + n = n, + }, params or {})) + end, + popToTop = function(params) + return StackActions.popToTop(params) + end, + push = function(routeName, params, action) + return StackActions.push({ + routeName = routeName, + params = params, + action = action, + }) + end, + replace = function(replaceWith, params, action, newKey) + if type(replaceWith) == "string" then + return StackActions.replace({ + routeName = replaceWith, + params = params, + action = action, + key = route.key, + newKey = newKey, + }) + end + + validate(type(replaceWith) == "table", "replaceWith must be a table or string") + validate(params == nil, "params cannot be provided to .replace() when specifying a table") + validate(action == nil, "Child action cannot be provided to .replace() when specifying a table") + validate(newKey == nil, "newKey cannot be provided to .replace() when specifying a table") + + return StackActions.replace(replaceWith) + end, + reset = function(actions, index) + local resetIndex = index + if index == nil then + resetIndex = #actions - 1 + end + + return StackActions.reset({ + actions = actions, + index = resetIndex, + key = navStateKey, + }) + end, + dismiss = function() + return NavigationActions.back({ + key = navStateKey, + }) + end, + }) + end + + function StackRouter.getStateForAction(action, state) + -- Set up initial state if needed + state = state or getInitialState() + + local activeChildRoute = state.routes[state.index] + + if not isResetToRootStack(action) and action.type ~= NavigationActions.Navigate then + local activeChildRouter = childRouters[activeChildRoute.routeName] + if activeChildRouter ~= nil and activeChildRouter ~= CHILD_IS_SCREEN then + local route = activeChildRouter.getStateForAction(action, activeChildRoute) + if route ~= nil and route ~= activeChildRoute then + return StateUtils.replaceAt( + state, + activeChildRoute.key, + route, + action.type == NavigationActions.SetParams -- don't change index for setParam action + ) + end + end + elseif action.type == NavigationActions.Navigate then + -- Traverse routes from top of the stack to the bottom; active route has first opportunity + for i = #state.routes, 1, -1 do + local childRoute = state.routes[i] + local childRouter = childRouters[childRoute.routeName] + local childAction = action + if action.routeName == childRoute.routeName and action.action then + childAction = action.action + end + + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + local nextRouteState = childRouter.getStateForAction(childAction, childRoute) + if nextRouteState == nil or nextRouteState ~= childRoute then + local newState = StateUtils.replaceAndPrune( + state, + nextRouteState and nextRouteState.key or childRoute.key, + nextRouteState and nextRouteState or childRoute + ) + + local newTransitioning = state.isTransitioning + if state.index ~= newState.index then + newTransitioning = action.immediate ~= true + end + + return Cryo.Dictionary.join(newState, { + isTransitioning = newTransitioning, + }) + end + end + end + end + + -- Handle push and navigation actions. This must happen after focused child router + -- has had its chance to handle the action. + if behavesLikePushAction(action) and childRouters[action.routeName] ~= nil then + local childRouter = childRouters[action.routeName] + validate(action.type ~= StackActions.Push or action.key == nil, + "StackRouter does not support key on the push action") + + -- Before pushing new route, try to find existing one in the stack. + local lastRouteIndex = nil + for idx, route in ipairs(state.routes) do + if (action.key and route.key == action.key) or (route.routeName == action.routeName) then + lastRouteIndex = idx + break + end + end + + -- An instance of this route exists already and we're dealing with a Navigate action. + if action.type ~= StackActions.Push and lastRouteIndex ~= nil then + -- If index or params have not changed, leave state alone + if state.index == lastRouteIndex and not action.params then + return nil + end + + -- Remove unused routes at tail + local routes = Cryo.List.removeRange(state.routes, lastRouteIndex + 1, #state.routes) + + -- Apply params if provided + if action.params then + local route = state.routes[lastRouteIndex] + routes[lastRouteIndex] = Cryo.Dictionary.join(route, { + params = Cryo.Dictionary.join(route.params or {}, action.params) + }) + end + + -- Return state with new index, changing isTransitioning only if index has changed + local newIsTransitioning = state.isTransitioning + if state.index ~= lastRouteIndex then + newIsTransitioning = action.immediate ~= true + end + + return Cryo.Dictionary.join(state, { + isTransitioning = newIsTransitioning, + index = lastRouteIndex, + routes = routes, + }) + end + + local route + if childRouter ~= CHILD_IS_SCREEN then + -- Delegate to child router + local childAction = action.action or NavigationActions.init({ + params = getParamsForRouteAndAction(action.routeName, action) + }) + + route = Cryo.Dictionary.join({ + -- TODO: Does it make sense to wipe out the params here, or to incorporate params at all? + params = getParamsForRouteAndAction(action.routeName, action), + }, childRouter.getStateForAction(childAction), { + routeName = action.routeName, + key = action.key or KeyGenerator.generateKey(), + }) + else + -- Create new route from scratch + route = { + params = getParamsForRouteAndAction(action.routeName, action), + routeName = action.routeName, + key = action.key or KeyGenerator.generateKey(), + } + + end + + return Cryo.Dictionary.join(StateUtils.push(state, route), { + isTransitioning = action.immediate ~= true, + }) + elseif action.type == StackActions.Push and childRouters[action.routeName] == nil then + -- Return original state to bubble the action up + return state + end + + -- Handle navigation to other child routers that are not pushed yet. + if behavesLikePushAction(action) then + local childRouterNames = Cryo.Dictionary.keys(childRouters) + for _, childRouterName in ipairs(childRouterNames) do + local childRouter = childRouters[childRouterName] + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + -- Start with blank state for each child router + local initChildRoute = childRouter.getStateForAction(NavigationActions.init()) + + -- Check to see if it handles our action + local navigatedChildRoute = childRouter.getStateForAction(action, initChildRoute) + + local routeToPush = nil + if navigatedChildRoute == nil then + -- Push initial route if the router returned nil when handling action + routeToPush = initChildRoute + elseif navigatedChildRoute ~= initChildRoute then + -- Push new route if state changed in response to this action + routeToPush = navigatedChildRoute + end + + if routeToPush then + local route = Cryo.Dictionary.join(routeToPush, { + routeName = childRouterName, + key = action.key or KeyGenerator.generateKey(), + }) + + return Cryo.Dictionary.join(StateUtils.push(state, route), { + isTransitioning = action.immediate ~= true, + }) + end + end + end + end + + -- Handle pop-to-top behavior. This must happen after children have had a chance to handle + -- the action, so that the inner stack always pops first. + if action.type == StackActions.PopToTop then + -- Refuse to handle pop to top if a key is given that does not correspond to this router + if action.key and state.key ~= action.key then + return state + end + + -- If we're already at the top then return current state to allow action to bubble up. + if state.index <= 1 then + return state + end + + return Cryo.Dictionary.join(state, { + isTransitioning = action.immediate ~= true, + index = 1, + routes = { state.routes[1] } + }) + end + + if action.type == StackActions.Replace then + local routeIndex = nil + + -- If there is no key, set index to last route in stack + if not action.key and #state.routes > 0 then + routeIndex = #state.routes + else + for idx, route in ipairs(state.routes) do + if route.key == action.key then + routeIndex = idx + break + end + end + end + + if routeIndex then + local childRouter = childRouters[action.routeName] + local childState = {} + + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + local childAction = action.action or NavigationActions.init({ + params = getParamsForRouteAndAction(action.routeName, action) + }) + + childState = childRouter.getStateForAction(childAction) + end + + -- shallow copy and update routes + local routes = Cryo.List.join(state.routes) + routes[routeIndex] = Cryo.Dictionary.join({ + params = getParamsForRouteAndAction(action.routeName, action), + }, childState, { + routeName = action.routeName, + key = action.newKey or KeyGenerator.generateKey(), + }) + + return Cryo.Dictionary.join(state, { + routes = routes, + }) + end + end + + if action.type == NavigationActions.CompleteTransition and + (action.key == nil or action.key == state.key) and + action.toChildKey == state.routes[state.index].key and + state.isTransitioning then + return Cryo.Dictionary.join(state, { + isTransitioning = false, + }) + end + + if action.type == NavigationActions.SetParams then + local key = action.key + + local lastRouteIndex = nil + local lastRoute = nil + for idx, route in ipairs(state.routes) do + if route.key == key then + lastRouteIndex = idx + lastRoute = route + break + end + end + + if lastRoute then + local params = Cryo.Dictionary.join(lastRoute.params or {}, action.params or {}) + -- shallow copy and update routes + local routes = Cryo.List.join(state.routes) + routes[lastRouteIndex] = Cryo.Dictionary.join(lastRoute, { + params = params, + }) + + return Cryo.Dictionary.join(state, { + routes = routes, + }) + end + end + + if action.type == StackActions.Reset then + -- Only handle reset actions with matching key (or none) + if action.key ~= nil and action.key ~= state.key then + return state + end + + local specifiedActions = action.actions or {} + + local newRoutes = {} + for _, newStackAction in ipairs(specifiedActions) do + local router = childRouters[newStackAction.routeName] + + local childState = {} + if router ~= nil and router ~= CHILD_IS_SCREEN then + local childAction = newStackAction.action or NavigationActions.init({ + params = getParamsForRouteAndAction(newStackAction.routeName, newStackAction), + }) + + childState = router.getStateForAction(childAction) + end + + table.insert(newRoutes, Cryo.Dictionary.join({ + params = getParamsForRouteAndAction(newStackAction.routeName, newStackAction) + }, childState, { + routeName = newStackAction.routeName, + key = newStackAction.key or KeyGenerator.generateKey(), + })) + end + + return Cryo.Dictionary.join(state, { + routes = newRoutes, + index = action.index or #specifiedActions, + }) + end + + if action.type == NavigationActions.Back or + action.type == StackActions.Pop then + local key = action.key + local n = action.n + local immediate = action.immediate + + local backRouteIndex = state.index -- index to go back *FROM* + if action.type == StackActions.Pop and n ~= nil then + backRouteIndex = math.max(1, state.index - n + 1) + elseif key and key ~= NoneSymbol then + -- If key is specified and is not ours, we should NOT try to navigate back + -- because it might be intended for our parent! (So clear the backRouteIndex.) + backRouteIndex = 0 + for idx, route in ipairs(state.routes) do + if route.key == key then + backRouteIndex = idx + break + end + end + end + + if backRouteIndex > 1 then + return Cryo.Dictionary.join(state, { + routes = Cryo.List.removeRange(state.routes, backRouteIndex, #state.routes), + index = backRouteIndex - 1, + isTransitioning = immediate ~= true, + }) + end + end + + -- At this point, we've handled the behavior of the active route and any + -- stack actions. Now we allow non-active child routers to try to process the action, + -- and switch to them if they can handle it. + local keyIndex = action.key and StateUtils.indexOf(state, action.key) or nil + + -- Traverse from top of stack to bottom. + for i = #state.routes, 1, -1 do + local childRoute = state.routes[i] + -- Skip over the active route since we already let it try. + -- Also, skip calling getStateForAction on other child routers + -- if the provided key is in the route's state + if (childRoute.key ~= activeChildRoute.key) and + (not keyIndex or childRoute.key == action.key) then + local childRouter = childRouters[childRoute.routeName] + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + local route = childRouter.getStateForAction(action, childRoute) + if not route then + return state + end + + if route ~= childRoute then + return StateUtils.replaceAt( + state, + childRoute.key, + route, + -- don't change index for these action types + action.type == NavigationActions.SetParams or + action.type == StackActions.CompleteTransition + ) + end + end + end + end + + return state + end + + -- TODO: Implement StackRouter.getPathAndParamsForState after we add path expression support + -- TODO: Implement StackRouter.getActionForPathAndParams after we add path expression support + + return StackRouter +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/SwitchRouter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/SwitchRouter.lua new file mode 100644 index 0000000..c7675fd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/SwitchRouter.lua @@ -0,0 +1,331 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local NavigationActions = require(script.Parent.Parent.NavigationActions) +local BackBehavior = require(script.Parent.Parent.BackBehavior) +local getScreenForRouteName = require(script.Parent.getScreenForRouteName) +local createConfigGetter = require(script.Parent.createConfigGetter) +local validateRouteConfigMap = require(script.Parent.validateRouteConfigMap) +local validate = require(script.Parent.Parent.utils.validate) + +local defaultActionCreators = function() return {} end + +-- Until Cryo has a List function to do this, provide shallow copy+replace index +local function immutableReplaceListIndex(list, index, value) + local result = {} + for i, ival in ipairs(list) do + result[i] = ival + end + + result[index] = value + return result +end + +local function childrenUpdateWithoutSwitchingIndex(actionType) + return actionType == NavigationActions.SetParams or + actionType == NavigationActions.CompleteTransition +end + +local function collectChildRouters(routeConfigs) + local childRouters = {} + for routeName, _ in pairs(routeConfigs) do + local screen = getScreenForRouteName(routeConfigs, routeName) + if type(screen) == "table" and screen.router then + childRouters[routeName] = screen.router + end + end + + return childRouters +end + +local function getParamsForRoute(routeConfigs, routeName, initialParams) + local routeConfig = routeConfigs[routeName] + if type(routeConfig) == "table" and routeConfig.params then + return Cryo.Dictionary.join(routeConfig.params, initialParams) + else + return initialParams + end +end + + +return function(config) + validate(type(config) == "table", "config must be a table") + + local routeConfigs = validateRouteConfigMap(config.routes) + + -- Order is how we map the active index into the list of possible routes. + -- Lua does not guarantee any sense of order of table keys in dictionaries, so + -- we have to require the initialRouteName parameter instead defaulting to the + -- first route in the map. + local order = config.order or Cryo.Dictionary.keys(routeConfigs) + + local getCustomActionCreators = config.getCustomActionCreators or defaultActionCreators + local initialRouteParams = config.initialRouteParams or {} + + local initialRouteName = validate(config.initialRouteName, + "initialRouteName must be provided") + + local backBehavior = config.backBehavior or BackBehavior.None + local backShouldNavigateToInitialRoute = backBehavior == BackBehavior.InitialRoute + + local resetOnBlur = true + if type(config.resetOnBlur) == "boolean" then + resetOnBlur = config.resetOnBlur + end + + local initialRouteIndex = Cryo.List.find(order, initialRouteName) + if initialRouteIndex == nil then + local availableRouteStr = "" + for _, name in ipairs(order) do + availableRouteStr = availableRouteStr .. name .. "," + end + + error(string.format("Invalid initialRouteName '%s'. Must be one of [%s]", initialRouteName, availableRouteStr), 2) + end + + local childRouters = collectChildRouters(routeConfigs) + + local function resetChildRoute(routeName) + -- TODO: Do we want to merge initialRouteParams on TOP of route-specific params? + -- There is a comment in RoactNavigation that this is incorrect behavior, but they + -- do it to be consistent with their StackRouter. Do we even need this feature? + local initialParams = routeName == initialRouteName and initialRouteParams or {} + local params = getParamsForRoute(routeConfigs, routeName, initialParams) + local childRouter = childRouters[routeName] + if childRouter then + local childAction = NavigationActions.init() + return Cryo.Dictionary.join(childRouter.getStateForAction(childAction), { + key = routeName, + routeName = routeName, + params = params, + }) + else + return { + key = routeName, + routeName = routeName, + params = params, + } + end + end + + local function getNextState(prevState, possibleNextState) + if not prevState then + return possibleNextState + end + + if prevState.index ~= possibleNextState.index and resetOnBlur then + local prevRouteName = prevState.routes[prevState.index].routeName + local nextRoutes = immutableReplaceListIndex( + possibleNextState.routes, + prevState.index, + resetChildRoute(prevRouteName)) + + return Cryo.Dictionary.join(possibleNextState, { + routes = nextRoutes, + }) + else + return possibleNextState + end + end + + local function getInitialState() + return { + routes = Cryo.List.map(order, resetChildRoute), + index = initialRouteIndex, + isTransitioning = false, + } + end + + local SwitchRouter = { + childRouters = childRouters, + getScreenOptions = createConfigGetter(routeConfigs, config.defaultNavigationOptions) + } + + function SwitchRouter.getActionCreators(route, stateKey) + return getCustomActionCreators(route, stateKey) + end + + function SwitchRouter.getStateForAction(action, inputState) + local prevState = inputState and Cryo.Dictionary.join(inputState) or nil + local state = inputState or getInitialState() + local activeChildIndex = state.index + + if action.type == NavigationActions.Init then + -- TODO: React-Navigation has a comment that wonders why we merge params into child routes. + -- Need to understand if we really want to do this. + local params = action.params + if params then + state.routes = Cryo.List.map(state.routes, function(route) + local initialParams = route.routeName == initialRouteName and initialRouteParams or {} + return Cryo.Dictionary.join(route, { + params = Cryo.Dictionary.join(route.params, params, initialParams) + }) + end) + end + end + + -- Let the active child try to handle the action first. + local activeChildLastState = state.routes[state.index] + local activeChildRouter = childRouters[order[state.index]] + if activeChildRouter then + local activeChildState = activeChildRouter.getStateForAction(action, activeChildLastState) + if not activeChildState and inputState then + -- Child ran into error with known inputState. Propagate to caller. + return nil + end + + if activeChildState and activeChildState ~= activeChildLastState then + local routes = immutableReplaceListIndex(state.routes, state.index, activeChildState) + return getNextState(prevState, Cryo.Dictionary.join(state, { + routes = routes + })) + end + end + + -- Child did not handle it, so try to process the action ourselves. + local isBackEligible = not action.key or action.key == activeChildLastState.key + if action.type == NavigationActions.Back then + if isBackEligible and backShouldNavigateToInitialRoute then + activeChildIndex = initialRouteIndex + else + return state + end + end + + local didNavigate = false + if action.type == NavigationActions.Navigate then + for index, childId in ipairs(order) do + if childId == action.routeName then + activeChildIndex = index + didNavigate = true + break + end + end + + if didNavigate then + local childState = state.routes[activeChildIndex] + local childRouter = childRouters[action.routeName] + local newChildState = childState + + if action.action and childRouter then + local childStateUpdate = childRouter.getStateForAction(action.action, childState) + if childStateUpdate then + newChildState = childStateUpdate + end + end + + if action.params then + newChildState = Cryo.Dictionary.join(newChildState, { + params = Cryo.Dictionary.join(newChildState.params or {}, action.params) + }) + end + + if newChildState ~= childState then + local routes = immutableReplaceListIndex(state.routes, activeChildIndex, newChildState) + local nextState = Cryo.Dictionary.join(state, { + routes = routes, + index = activeChildIndex, + }) + + return getNextState(prevState, nextState) + elseif newChildState == childState and state.index == activeChildIndex and prevState then + return nil + end + end + end + + if action.type == NavigationActions.SetParams then + local key = action.key + local lastIndex, lastRoute + for index, route in ipairs(state.routes) do + if route.key == key then + lastIndex = index + lastRoute = route + break + end + end + + if lastRoute then + local params = Cryo.Dictionary.join(lastRoute.params or {}, action.params) + local mergedRoute = Cryo.Dictionary.join(lastRoute, { + params = params + }) + + local routes = immutableReplaceListIndex(state.routes, lastIndex, mergedRoute) + return getNextState(prevState, Cryo.Dictionary.join(state, { + routes = routes + })) + end + end + + if activeChildIndex ~= state.index then + return getNextState(prevState, Cryo.Dictionary.join(state, { index = activeChildIndex })) + elseif didNavigate and not inputState then + return state + elseif didNavigate then + return Cryo.Dictionary.join(state) + end + + -- Let other children handle it and switch to first child that returns a new state + local index = state.index + local routes = state.routes + + for i, childId in ipairs(order) do + if i ~= index then + local childRouter = childRouters[childId] + local childState = routes[i] + if childRouter then + childState = childRouter.getStateForAction(action, childState) + end + + if not childState then + index = i + break + end + + if childState ~= routes[i] then + routes = immutableReplaceListIndex(routes, i, childState) + index = i + break + end + end + end + + -- Nested routers can be updated after switching children with actions such as SetParams + -- and CompleteTransition + if childrenUpdateWithoutSwitchingIndex(action.type) then + index = state.index + end + + if index ~= state.index or routes ~= state.routes then + return getNextState(prevState, Cryo.Dictionary.join(state, { + index = index, + routes = routes, + })) + end + + return state + end + + function SwitchRouter.getComponentForState(state) + local activeRoute = state.routes[state.index] or {} + local routeName = activeRoute.routeName + validate(routeName, "There is no route defined for index '%d'. " .. + "Make sure that you passed in a navigation state with a " .. + "valid tab/screen index.", state.index) + + local childRouter = childRouters[routeName] + if childRouter then + return childRouter.getComponentForState(state.routes[state.index]) + else + return getScreenForRouteName(routeConfigs, routeName) + end + end + + function SwitchRouter.getComponentForRouteName(routeName) + return getScreenForRouteName(routeConfigs, routeName) + end + + -- TODO: Implement SwitchRouter.getPathAndParamsForState after we add path expression support + -- TODO: Implement SwitchRouter.getActionForPathAndParams after we add path expression support + + return SwitchRouter +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/TabRouter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/TabRouter.lua new file mode 100644 index 0000000..052d391 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/TabRouter.lua @@ -0,0 +1,13 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local SwitchRouter = require(script.Parent.SwitchRouter) +local BackBehavior = require(script.Parent.Parent.BackBehavior) + +return function(config) + -- Provide defaults suitable for tab routing. + local modifiedConfig = Cryo.Dictionary.join({ + resetOnBlur = false, + backBehavior = BackBehavior.InitialRoute, + }, config) + + return SwitchRouter(modifiedConfig) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/StackRouter.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/StackRouter.spec.lua new file mode 100644 index 0000000..2f6640d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/StackRouter.spec.lua @@ -0,0 +1,1191 @@ +return function() + local StackRouter = require(script.Parent.Parent.StackRouter) + local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) + local StackActions = require(script.Parent.Parent.Parent.StackActions) + + -- local TableUtilities = require(script.Parent.Parent.Parent.utils.TableUtilities) + + local function expectError(functor, msg) + local status, err = pcall(functor) + + if status ~= false then + error("expectError: Test function should have thrown error, but it passed", 2) + end + if string.find(err, msg) == nil then + error(string.format("expectError: Expected error message '%s' not found in actual message: '%s'", msg, err), 2) + end + end + + it("should be a function", function() + expect(type(StackRouter)).to.equal("function") + end) + + it("should throw when passed a non-table", function() + expectError(function() + StackRouter(5) + end, "config must be a table") + end) + + it("should throw for invalid routes config", function() + expect(function() + StackRouter({ routes = 5 }) + end).to.throw() + end) + + it("should throw if initialRouteName is not provided", function() + expectError(function() + StackRouter({ routes = { + Foo = function() end, + }}) + end, "initialRouteName must be provided") + end) + + it("should throw if initialRouteName is not found in routes table", function() + expectError(function() + StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "MyRoute", + }) + end, "Invalid initialRouteName 'MyRoute'. Must be one of %[Bar,Foo,%]") + end) + + it("should expose childRouters as a member", function() + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + router = "A", + }, + }, + Bar = { + screen = { + render = function() end, + router = "B", + }, + }, + }, + initialRouteName = "Foo", + }) + + expect(router.childRouters.Foo).to.equal("A") + expect(router.childRouters.Bar).to.equal("B") + end) + + it("should not expose childRouters list members if they are CHILD_IS_SCREEN", function() + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + router = "A", + }, + }, + Bar = { + screen = { + render = function() end, + }, + }, + }, + initialRouteName = "Foo", + }) + + expect(router.childRouters.Foo).to.equal("A") + + expect(router._CHILD_IS_SCREEN).to.never.equal(nil) + for _, childRouter in pairs(router.childRouters) do + expect(childRouter).to.never.equal(router._CHILD_IS_SCREEN) + end + end) + + describe("getScreenOptions tests", function() + it("should correctly configure default screen options", function() + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + } + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + } + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("FooTitle") + end) + + it("should correctly configure route-specified screen options", function() + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + }, + navigationOptions = { title = "RouteFooTitle" }, + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("RouteFooTitle") + end) + + it("should correctly configure component-specified screen options", function() + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentFooTitle" }, + }, + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("ComponentFooTitle") + end) + end) + + describe("getActionCreators tests", function() + it("should return basic action creators table if none are provided", function() + local router = StackRouter({ + routes = { + Foo = { render = function() end }, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + + local fieldCount = 0 + for _ in pairs(actionCreators) do + fieldCount = fieldCount + 1 + end + + expect(fieldCount).to.equal(6) + expect(type(actionCreators.pop)).to.equal("function") + expect(type(actionCreators.popToTop)).to.equal("function") + expect(type(actionCreators.push)).to.equal("function") + expect(type(actionCreators.replace)).to.equal("function") + expect(type(actionCreators.reset)).to.equal("function") + expect(type(actionCreators.dismiss)).to.equal("function") + end) + + it("should call custom action creators function if provided", function() + local router = StackRouter({ + routes = { + Foo = { render = function() end }, + }, + initialRouteName = "Foo", + getCustomActionCreators = function() + return { a = 1, popToTop = 2 } + end, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.a).to.equal(1) + + -- make sure that we merged the default ones on top! + expect(type(actionCreators.pop)).to.equal("function") + expect(type(actionCreators.popToTop)).to.equal("function") + end) + + it("should build a pop action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.pop(1).type).to.equal(StackActions.Pop) + end) + + it("should build a pop to top action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.popToTop().type).to.equal(StackActions.PopToTop) + end) + + it("should build a push action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.push("Foo").type).to.equal(StackActions.Push) + end) + + it("should build a replace action with a string replaceWith arg", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo", key = "Foo" }, "key") + expect(actionCreators.replace("Foo").type).to.equal(StackActions.Replace) + end) + + it("should build a replace action with a table replaceWith arg", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.replace({ routeName = "Foo" }).type).to.equal(StackActions.Replace) + end) + + it("should build a reset action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.reset({ + actions = { NavigationActions.navigate({ routeName = "Foo" }) }, + }).type).to.equal(StackActions.Reset) + end) + + it("should build a dismiss action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.dismiss().type).to.equal(NavigationActions.Back) + end) + end) + + describe("getComponentForState tests", function() + it("should return component matching requested state", function() + local testComponent = function() end + local router = StackRouter({ + routes = { + Foo = { screen = testComponent }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForState({ + routes = { + { routeName = "Foo" }, + }, + index = 1, + }) + expect(component).to.equal(testComponent) + end) + + it("should throw if there is no route matching active index", function() + local router = StackRouter({ + routes = { + Foo = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + expectError(function() + router.getComponentForState({ + routes = { + Foo = { screen = function() end }, + }, + index = 2, + }) + end, "There is no route defined for index '2'. " .. + "Make sure that you passed in a navigation state with a " .. + "valid stack index.") + + end) + + it("should descend child router for requested route", function() + local testComponent = function() end + local childRouter = StackRouter({ + routes = { + Bar = { screen = testComponent } + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + screen = { + render = function() end, + router = childRouter, + } + }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForState({ + routes = { + { + routeName = "Foo", + routes = { -- Child router's routes + { routeName = "Bar" }, + }, + index = 1 + }, + }, + index = 1, + }) + expect(component).to.equal(testComponent) + end) + end) + + describe("getComponentForRouteName tests", function() + it("should return a component that matches the given route name", function() + local testComponent = function() end + local router = StackRouter({ + routes = { + Foo = testComponent, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForRouteName("Foo") + expect(component).to.equal(testComponent) + end) + + it("should return a component that matches the given route name from accessed childRouter", function() + local testComponent = function() end + local childRouter = StackRouter({ + routes = { + Bar = testComponent, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + }, + initialRouteName = "Foo", + }) + + local component = router.childRouters.Foo.getComponentForRouteName("Bar") + expect(component).to.equal(testComponent) + end) + end) + + describe("getStateForAction tests", function() + it("should return initial state for init action", function() + local router = StackRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(#state.routes).to.equal(1) + expect(state.routes[state.index].routeName).to.equal("Foo") + expect(state.isTransitioning).to.equal(false) + end) + + it("should adjust initial state index to match initialRouteName's index", function() + local router = StackRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(state.routes[state.index].routeName).to.equal("Foo") + + local router2 = StackRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local state2 = router2.getStateForAction(NavigationActions.init(), nil) + expect(state2.routes[state2.index].routeName).to.equal("Bar") + end) + + it("should incorporate child router state", function() + local childRouter = StackRouter({ + routes = { + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + City = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + local activeState = state.routes[state.index] + expect(activeState.routeName).to.equal("Foo") -- parent's tracking uses parent's route name + expect(activeState.routes[activeState.index].routeName).to.equal("Bar") + end) + + it("should let active child handle non-init action first", function() + local childRouter = StackRouter({ + routes = { + Bar = { screen = function() end }, + City = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "City" })) + + expect(state.routes[1].routes[2].routeName).to.equal("City") + expect(state.index).to.equal(1) + expect(state.routes[1].index).to.equal(2) + end) + + it("should make historical inactive child router active if it handles action", function() + local childRouter = StackRouter({ + routes = { + City = function() end, + State = function() end, + }, + initialRouteName = "City", + }) + + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = { + render = function() end, + router = childRouter, + } + }, + initialRouteName = "Foo", + }) + + local initialState = { + routes = { + [1] = { routeName = "Foo", key = "Foo1" }, + [2] = { + routeName = "Bar", + key = "Bar", + routes = { + [1] = { routeName = "City", key = "City", }, + }, + index = 1 + }, + [3] = { routeName = "Foo", key = "Foo2" }, + }, + index = 3, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "State" }), initialState) + expect(resultState.routes[2].index).to.equal(2) + expect(resultState.routes[2].routes[2].routeName).to.equal("State") + expect(#resultState.routes[2].routes).to.equal(2) + expect(resultState.index).to.equal(2) + end) + + it("should go back to previous stack entry on back action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar", + } + }, + index = 2, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Foo") + expect(#resultState.routes).to.equal(1) -- it should delete top entry! + end) + + it("should not go back if at root of stack", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(resultState).to.equal(initialState) + end) + + it("should go back out of child stack if on root of child", function() + local childRouter = StackRouter({ + routes = { + Bar = { screen = function() end }, + City = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + Cat = function() end, + }, + initialRouteName = "Cat", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Cat", + key = "Cat", + }, + [2] = { + routeName = "Foo", + key = "Foo", + routes = { + [1] = { + routeName = "Bar", + key = "Bar", + } + }, + index = 1, + } + }, + index = 2, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(resultState.index).to.equal(1) + expect(#resultState.routes).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Cat") + end) + + it("should go back within active child if not on root of child", function() + local childRouter = StackRouter({ + routes = { + Bar = { screen = function() end }, + City = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + Cat = function() end, + }, + initialRouteName = "Cat", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Cat", + key = "Cat", + }, + [2] = { + routeName = "Foo", + key = "Foo", + routes = { + [1] = { + routeName = "Bar", + key = "Bar", + }, + [2] = { + routeName = "City", + key = "City", + }, + }, + index = 2, + } + }, + index = 2, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[1].routeName).to.equal("Cat") + expect(resultState.routes[2].routeName).to.equal("Foo") + + expect(#resultState.routes[2].routes).to.equal(1) + expect(resultState.routes[2].index).to.equal(1) + expect(resultState.routes[2].routes[1].routeName).to.equal("Bar") + end) + + it("should pop to top", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar1", + }, + [3] = { + routeName = "Bar", + key = "Bar2", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(StackActions.popToTop(), initialState) + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Foo") + end) + + it("should pop to top through child router", function() + local childRouter = StackRouter({ + routes = { + Bar = function() end, + City = function() end, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + screen = function() end, + router = childRouter, + }, + Crazy = function() end, + }, + initialRouteName = "Crazy", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Crazy", + key = "Crazy", + }, + [2] = { + routeName = "Foo", + key = "Foo", + routes = { + [1] = { + routeName = "Bar", + key = "Bar", + }, + [2] = { + routeName = "City", + key = "City", + }, + }, + index = 2, + } + }, + index = 2, + } + + local resultState = router.getStateForAction(StackActions.popToTop(), initialState) + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Crazy") + end) + + it("should push a new entry on navigate without instance of that screen", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + } + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + end) + + it("should jump to existing entry in stack if one exists already, on navigate", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + City = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar", + }, + [3] = { + routeName = "City", + key = "City", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + params = { a = 1 }, + }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + expect(resultState.routes[2].params.a).to.equal(1) + end) + + it("should always push new entry on push action even with pre-existing instance of that screen", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + City = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar", + }, + [3] = { + routeName = "City", + key = "City", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(StackActions.push({ routeName = "Foo" }), initialState) + expect(#resultState.routes).to.equal(4) + expect(resultState.index).to.equal(4) + expect(resultState.routes[4].routeName).to.equal("Foo") + end) + + it("should navigate to inactive child if route not present elsewhere", function() + local childRouter = StackRouter({ + routes = { + Bar = { screen = function() end }, + City = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = StackRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + Cat = function() end, + }, + initialRouteName = "Cat", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Cat", + key = "Cat", + }, + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "City" }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Foo") + + expect(#resultState.routes[2].routes).to.equal(2) + expect(resultState.routes[2].index).to.equal(2) + expect(resultState.routes[2].routes[2].routeName).to.equal("City") + end) + + it("should set params on route for setParams action", function() + local router = StackRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + initialRouteName = "Foo", + initialRouteKey = "FooKey", + }) + + local newState = router.getStateForAction(NavigationActions.setParams({ + key = "FooKey", + params = { a = 1 }, + })) + + expect(newState.routes[newState.index].params.a).to.equal(1) + end) + + it("should combine params from action and route config", function() + local router = StackRouter({ + routes = { + Foo = { render = function() end }, + Bar = { + screen = function() end, + params = { a = 1 }, + }, + }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + params = { b = 2 }, + })) + + expect(newState.routes[2].params.a).to.equal(1) + expect(newState.routes[2].params.b).to.equal(2) + end) + + it("should init and then replace initial route if prior state is not provided", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(StackActions.replace({ + routeName = "Bar", + })) + + expect(#newState.routes).to.equal(1) + expect(newState.index).to.equal(1) + expect(newState.routes[1].routeName).to.equal("Bar") + end) + + it("should replace top route if no key is provided", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + } + }, + index = 1, + } + + local newState = router.getStateForAction(StackActions.replace({ + routeName = "Bar", + }), initialState) + + expect(#newState.routes).to.equal(1) + expect(newState.index).to.equal(1) + expect(newState.routes[1].routeName).to.equal("Bar") + end) + + it("should replace keyed route if provided", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + }, + [2] = { + routeName = "Bar", + key = "Bar", + } + }, + index = 2, + } + + local newState = router.getStateForAction(StackActions.replace({ + routeName = "Foo", + key = "Bar", + newKey = "NewFoo", + }), initialState) + + expect(#newState.routes).to.equal(2) + expect(newState.index).to.equal(2) + expect(newState.routes[2].routeName).to.equal("Foo") + expect(newState.routes[2].key).to.equal("NewFoo") + end) + + it("should reset top-level routes if not given a key", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo1", + }, + [2] = { + routeName = "Foo", + key = "Foo2", + }, + }, + index = 2, + } + + local resultState = router.getStateForAction(StackActions.reset({ + actions = { + NavigationActions.navigate({ routeName = "Bar" }) + } + }), initialState) + + -- "actions" array replaces entire state, bypassing initial route config! + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Bar") + end) + + it("should reset keyed route if provided", function() + local childRouter = StackRouter({ + routes = { + City = function() end, + State = function() end, + }, + initialRouteName = "City", + }) + + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = { + screen = function() end, + router = childRouter, + }, + }, + initialRouteName = "Bar", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo1", + }, + [2] = { + routeName = "Bar", + key = "Bar", + routes = { + [1] = { + routeName = "City", + key = "City", + } + }, + index = 1, + }, + }, + index = 2, + } + + local resultState = router.getStateForAction(StackActions.reset({ + actions = { + NavigationActions.navigate({ routeName = "State" }) + }, + key = "Bar", + }), initialState) + + -- "actions" array replaces entire state, bypassing initial route config! + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + expect(resultState.routes[2].routes[1].routeName).to.equal("City") + end) + + it("should mark state as transitioning, then clear it on CompleteTransition action", function() + local router = StackRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "Foo", + }) + + local initialState = { + key = "root", + routes = { + [1] = { + routeName = "Foo", + key = "Foo", + } + }, + index = 1, + } + + local transitioningState = router.getStateForAction(StackActions.push({ routeName = "Bar" }), initialState) + expect(transitioningState.isTransitioning).to.equal(true) + + local completedState = router.getStateForAction(NavigationActions.completeTransition({ + toChildKey = transitioningState.routes[2].key, -- Need actual key to identify target + }), transitioningState) + + expect(completedState.isTransitioning).to.equal(false) + end) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/SwitchRouter.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/SwitchRouter.spec.lua new file mode 100644 index 0000000..2548212 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/SwitchRouter.spec.lua @@ -0,0 +1,691 @@ +return function() + local SwitchRouter = require(script.Parent.Parent.SwitchRouter) + local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) + local BackBehavior = require(script.Parent.Parent.Parent.BackBehavior) + + local function expectError(functor, msg) + local status, err = pcall(functor) + + if status ~= false then + error("expectError: Test function should have thrown error, but it passed", 2) + end + if string.find(err, msg) == nil then + error(string.format("expectError: Expected error message '%s' not found in actual message: '%s'", msg, err), 2) + end + end + + it("should be a function", function() + expect(type(SwitchRouter)).to.equal("function") + end) + + it("should throw when passed a non-table", function() + expectError(function() + SwitchRouter(5) + end, "config must be a table") + end) + + it("should throw for invalid routes config", function() + expect(function() + SwitchRouter({ routes = 5 }) + end).to.throw() -- throw is from validateRouteConfigs, so do not depend on message + end) + + it("should throw if initialRouteName is not provided", function() + expectError(function() + SwitchRouter({ routes = { + Foo = function() end, + } }) + end, "initialRouteName must be provided") + end) + + it("should throw if initialRouteName is not found in routes table", function() + expectError(function() + SwitchRouter({ + routes = { + Foo = function() end, + Bar = function() end, + }, + initialRouteName = "MyRoute", + }) + end, "Invalid initialRouteName 'MyRoute'. Must be one of %[Bar,Foo,%]") + end) + + it("should expose childRouters as a member", function() + local router = SwitchRouter({ + routes = { + Foo = { + screen = { + render = function() end, + router = "A", + }, + }, + Bar = { + screen = { + render = function() end, + router = "B", + }, + }, + }, + initialRouteName = "Foo", + }) + + expect(router.childRouters.Foo).to.equal("A") + expect(router.childRouters.Bar).to.equal("B") + end) + + describe("getScreenOptions tests", function() + it("should correctly configure default screen options", function() + local router = SwitchRouter({ + routes = { + Foo = { + screen = { + render = function() end, + } + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("FooTitle") + end) + + it("should correctly configure route-specified screen options", function() + local router = SwitchRouter({ + routes = { + Foo = { + screen = { + render = function() end, + }, + navigationOptions = { title = "RouteFooTitle" }, + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("RouteFooTitle") + end) + + it("should correctly configure component-specified screen options", function() + local router = SwitchRouter({ + routes = { + Foo = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentFooTitle" }, + }, + } + }, + initialRouteName = "Foo", + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("ComponentFooTitle") + end) + end) + + describe("getActionCreators tests", function() + it("should return empty action creators table if none are provided", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + }, + initialRouteName = "Foo", + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + + local fieldCount = 0 + for _ in pairs(actionCreators) do + fieldCount = fieldCount + 1 + end + + expect(fieldCount).to.equal(0) + end) + + it("should call custom action creators function if provided", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + }, + initialRouteName = "Foo", + getCustomActionCreators = function() + return { a = 1 } + end, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.a).to.equal(1) + end) + end) + + describe("getComponentForState tests", function() + it("should return component matching requested state", function() + local testComponent = function() end + local router = SwitchRouter({ + routes = { + Foo = { screen = testComponent }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForState({ + routes = { + { routeName = "Foo" }, + }, + index = 1, + }) + expect(component).to.equal(testComponent) + end) + + it("should throw if there is no route matching active index", function() + local router = SwitchRouter({ + routes = { + Foo = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + expectError(function() + router.getComponentForState({ + routes = { + Foo = { screen = function() end }, + }, + index = 2, + }) + end, "There is no route defined for index '2'. " .. + "Make sure that you passed in a navigation state with a " .. + "valid tab/screen index.") + + end) + + it("should descend child router for requested route", function() + local testComponent = function() end + local childRouter = SwitchRouter({ + routes = { + Bar = { screen = testComponent } + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + screen = { + render = function() end, + router = childRouter, + } + }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForState({ + routes = { + { + routeName = "Foo", + routes = { -- Child router's routes + { routeName = "Bar" }, + }, + index = 1 + }, + }, + index = 1, + }) + expect(component).to.equal(testComponent) + end) + end) + + describe("getComponentForRouteName tests", function() + it("should return a component that matches the given route name", function() + local testComponent = function() end + local router = SwitchRouter({ + routes = { + Foo = { screen = testComponent }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForRouteName("Foo") + expect(component).to.equal(testComponent) + end) + end) + + describe("getStateForAction tests", function() + it("should return initial state for init action", function() + local router = SwitchRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(#state.routes).to.equal(2) + expect(state.routes[state.index].routeName).to.equal("Foo") + expect(state.isTransitioning).to.equal(false) + end) + + it("should adjust initial state index to match initialRouteName's index", function() + local router = SwitchRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(state.routes[state.index].routeName).to.equal("Foo") + + local router2 = SwitchRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local state2 = router2.getStateForAction(NavigationActions.init(), nil) + expect(state2.routes[state2.index].routeName).to.equal("Bar") + end) + + it("should respect optional order property", function() + local router = SwitchRouter({ + routes = { + Foo = { screen = function() end }, + Bar = { screen = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(state.routes[1].routeName).to.equal("Foo") + expect(state.routes[2].routeName).to.equal("Bar") + end) + + it("should incorporate child router state", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + City = { screen = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + local activeState = state.routes[state.index] + expect(activeState.routeName).to.equal("Foo") -- parent's tracking uses parent's route name + expect(activeState.routes[activeState.index].routeName).to.equal("Bar") + end) + + it("should let active child handle non-init action first", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { screen = function() end }, + City = { screen = function() end }, + }, + order = { "Bar", "City" }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + State = { render = function() end }, + }, + order = { "Foo", "State" }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "City" })) + expect(state.routes[1].index).to.equal(2) + expect(state.index).to.equal(1) + end) + + it("should go back to initial route index if BackBehavior.InitialRoute", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + backBehavior = BackBehavior.InitialRoute, + initialRouteName = "Foo", + }) + + local prevState = { + routes = { + { routeName = "Foo" }, + { routeName = "Bar" }, + }, + index = 2, + } + + local newState = router.getStateForAction(NavigationActions.back(), prevState) + expect(newState.index).to.equal(1) + end) + + it("should not change state on back action if BackBehavior.None", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local prevState = { + routes = { + { routeName = "Foo" }, + { routeName = "Bar" }, + }, + index = 2, + } + + local newState = router.getStateForAction(NavigationActions.back(), prevState) + expect(newState).to.equal(prevState) + end) + + it("should change active route on navigate", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" })) + expect(newState.index).to.equal(2) + expect(newState.routes[newState.index].routeName).to.equal("Bar") + end) + + it("should pass sub-action to child router on navigate", function() + local childRouter = SwitchRouter({ + routes = { + City = { screen = function() end }, + State = { screen = function() end }, + }, + initialRouteName = "City", + }) + + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { + render = function() end, + router = childRouter, + }, + }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + action = NavigationActions.navigate({ routeName = "State" }), + })) + + local activeRoute = newState.routes[newState.index] + expect(activeRoute.routeName).to.equal("Bar") + expect(activeRoute.routes[activeRoute.index].routeName).to.equal("State") + end) + + it("should return initial state if navigating to active child without previous state", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + City = { render = function() end }, + }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Foo", + })) + + expect(newState.routes[newState.index].routeName).to.equal("Foo") + expect(newState.isTransitioning).to.equal(false) + end) + + it("should reset state for deactivated route by default", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local initialState = { + routes = { + { routeName = "Foo", params = { a = 1 } }, + { routeName = "Bar" }, + }, + index = 1, + } + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(state.routes[1].params.a).to.equal(nil) -- should be empty + end) + + it("should not reset state for deactivated route if resetOnBlur is false", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + resetOnBlur = false, + }) + + local testParams = { a = 1 } + + local initialState = { + routes = { + { routeName = "Foo", params = testParams }, + { routeName = "Bar" }, + }, + index = 1, + } + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(state.routes[1].params).to.equal(testParams) + end) + + it("should set params on route for setParams action", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + initialRouteName = "Foo", + }) + + local newState = router.getStateForAction(NavigationActions.setParams({ + key = "Foo", -- By default, key == routeName + params = { a = 1 }, + })) + + expect(newState.routes[newState.index].params.a).to.equal(1) + end) + + it("should preserve route configured params for child router", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { + screen = function() end, + params = { a = 2 }, + }, + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + params = { a = 1 }, + router = childRouter, + }, + City = { render = function() end }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.init()) + expect(state.routes[state.index].params.a).to.equal(1) + end) + + it("should merge initialRouteParams with initial route's own params", function() + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + params = { a = 1 }, + }, + Bar = { + render = function() end, + params = { a = 1 }, + }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + initialRouteParams = { a = 2, b = 3 }, + }) + + local state = router.getStateForAction(NavigationActions.init()) + expect(state.routes[1].params.a).to.equal(2) + expect(state.routes[1].params.b).to.equal(3) + expect(state.routes[2].params.a).to.equal(1) + expect(state.routes[2].params.b).to.equal(nil) + end) + + it("should merge init action params with initial route's own params and initialRouteParams", function() + local router = SwitchRouter({ + routes = { + Foo = { render = function() end, params = { a = 1 } } + }, + initialRouteName = "Foo", + initialRouteParams = { c = 3 }, + }) + + local state = router.getStateForAction(NavigationActions.init({ params = { b = 2 } })) + expect(state.routes[1].params.a).to.equal(1) + expect(state.routes[1].params.b).to.equal(2) + expect(state.routes[1].params.c).to.equal(3) + end) + + it("should merge navigate action params for child router", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { + screen = function() end, + params = { a = 2 }, + }, + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + }, + initialRouteName = "Foo", + }) + + local state = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + params = { b = 3 }, + })) + + expect(state.routes[1].routes[1].params.a).to.equal(2) + expect(state.routes[1].routes[1].params.b).to.equal(3) + end) + + it("should propagate a child router getStateForAction failure to caller", function() + local childRouter = SwitchRouter({ + routes = { + Bar = { screen = function() end }, + }, + initialRouteName = "Bar", + }) + + local router = SwitchRouter({ + routes = { + Foo = { + render = function() end, + router = childRouter, + }, + }, + initialRouteName = "Foo", + }) + + -- need to properly initialize state because we're being abusive of getStateForAction + local initialState = router.getStateForAction(NavigationActions.init()) + + childRouter.getStateForAction = function() return nil end + + local state = router.getStateForAction(NavigationActions.navigate("Bar"), initialState) + expect(state).to.equal(nil) + end) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/TabRouter.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/TabRouter.spec.lua new file mode 100644 index 0000000..f18a109 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/TabRouter.spec.lua @@ -0,0 +1,67 @@ +return function() + local TabRouter = require(script.Parent.Parent.TabRouter) + local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) + + -- NOTE: Most functional tests are covered by SwitchRouter.spec.lua + -- We just check that we can mount a basic case, and check that our custom + -- defaults for resetOnBlur and backBehavior work as expected. + + it("should return a component that matches the given route name", function() + local testComponent = function() end + local router = TabRouter({ + routes = { + Foo = { screen = testComponent }, + }, + initialRouteName = "Foo", + }) + + local component = router.getComponentForRouteName("Foo") + expect(component).to.equal(testComponent) + end) + + it("should not reset state for deactivated route", function() + local router = TabRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local testParams = { a = 1 } + + local initialState = { + routes = { + { routeName = "Foo", params = testParams }, + { routeName = "Bar" }, + }, + index = 1, + } + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(state.routes[1].params).to.equal(testParams) + end) + + it("should go back to initial route index", function() + local router = TabRouter({ + routes = { + Foo = { render = function() end }, + Bar = { render = function() end }, + }, + order = { "Foo", "Bar" }, + initialRouteName = "Foo", + }) + + local prevState = { + routes = { + { routeName = "Foo" }, + { routeName = "Bar" }, + }, + index = 2, + } + + local newState = router.getStateForAction(NavigationActions.back(), prevState) + expect(newState.index).to.equal(1) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/createConfigGetter.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/createConfigGetter.spec.lua new file mode 100644 index 0000000..6cb2e53 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/createConfigGetter.spec.lua @@ -0,0 +1,115 @@ +return function() + local createConfigGetter = require(script.Parent.Parent.createConfigGetter) + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + + it("should return a function", function() + local result = createConfigGetter({}, {}) + expect(type(result)).to.equal("function") + end) + + it("should return a screen config when called", function() + local HomeScreen = Roact.Component:extend("HomeScreen") + HomeScreen.navigationOptions = function(props) + local username = props.navigation.state.params and + props.navigation.state.params.user or "anonymous" + + return { + title = string.format("Welcome %s", username), + gesturesEnabled = true, + } + end + function HomeScreen:render() return nil end + + local SettingsScreen = Roact.Component:extend("SettingsScreen") + SettingsScreen.navigationOptions = { + title = "Settings!!!", + gesturesEnabled = false, + } + function SettingsScreen:render() return nil end + + local NotificationScreen = Roact.Component:extend("NotificationScreen") + NotificationScreen.navigationOptions = function(props) + local gesturesEnabled = true + if props.navigation.state.params then + gesturesEnabled = not props.navigation.state.params.fullscreen + end + + return { + title = "42", + gesturesEnabled = gesturesEnabled + } + end + + local getScreenOptions = createConfigGetter({ + Home = { screen = HomeScreen }, + Settings = { screen = SettingsScreen }, + Notifications = { + screen = NotificationScreen, + navigationOptions = { + title = "10 new notifications", + } + } + }) + + local routes = { + { key = "A", routeName = "Home", }, + { key = "B", routeName = "Home", params = { user = "jane"} }, + { key = "C", routeName = "Settings", }, + { key = "D", routeName = "Notifications", }, + { key = "E", routeName = "Notifications", params = { fullscreen = true } }, + } + + expect(getScreenOptions({ state = routes[1] }, {}).title + ).to.equal("Welcome anonymous") + + expect(getScreenOptions({ state = routes[2] }, {}).title + ).to.equal("Welcome jane") + + expect(getScreenOptions({ state = routes[1] }, {}).gesturesEnabled + ).to.equal(true) + + expect(getScreenOptions({ state = routes[3] }, {}).title + ).to.equal("Settings!!!") + + expect(getScreenOptions({ state = routes[3] }, {}).gesturesEnabled + ).to.equal(false) + + expect(getScreenOptions({ state = routes[4] }, {}).title + ).to.equal("10 new notifications") + + expect(getScreenOptions({ state = routes[4] }, {}).gesturesEnabled + ).to.equal(true) + + expect(getScreenOptions({ state = routes[5] }, {}).gesturesEnabled + ).to.equal(false) + end) + + it("should override default config with component-specific config", function() + local getScreenOptions = createConfigGetter({ + Home = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentHome" }, + }, + }, + defaultNavigationOptions = { title = "DefaultTitle" }, + }) + + expect(getScreenOptions({ state = { routeName = "Home" } }).title).to.equal("ComponentHome") + end) + + it("should override component-specific config with route-specific config", function() + local getScreenOptions = createConfigGetter({ + Home = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentHome" }, + }, + navigationOptions = { title = "RouteHome" }, + }, + defaultNavigationOptions = { title = "DefaultTitle" }, + }) + + expect(getScreenOptions({ state = { routeName = "Home" } }).title).to.equal("RouteHome") + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getChildRouter.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getChildRouter.spec.lua new file mode 100644 index 0000000..dec6a68 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getChildRouter.spec.lua @@ -0,0 +1,60 @@ +return function() + local getChildRouter = require(script.Parent.Parent.getChildRouter) + + it("should throw if router is not a table", function() + local status, err = pcall(function() + getChildRouter(5, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "router must be a table")).to.never.equal(nil) + end) + + it("should throw if routeName is not a string", function() + local status, err = pcall(function() + getChildRouter({}, 5) + end) + + expect(status).to.equal(false) + expect(string.find(err, "routeName must be a string")).to.never.equal(nil) + end) + + it("should return child router if found", function() + local childRouter = {} + local result = getChildRouter({ + childRouters = { + myRoute = childRouter, + } + }, "myRoute") + + expect(result).to.equal(childRouter) + end) + + it("should look up component router if no child router is found", function() + local component = { router = {} } + + local result = getChildRouter({ + getComponentForRouteName = function(routeName) + if routeName == "myRoute" then + return component + else + return nil + end + end + }, "myRoute") + + expect(result).to.equal(component.router) + end) + + it("should throw if no child routers are specified and getComponentForRouteName is not a function", function() + local status, err = pcall(function() + getChildRouter({ + getComponentForRouteName = 5 + }, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "router.getComponentForRouteName must be a function if no child routers are specified") + ).to.never.equal(nil) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getNavigationActionCreators.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getNavigationActionCreators.spec.lua new file mode 100644 index 0000000..bafe6b7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getNavigationActionCreators.spec.lua @@ -0,0 +1,101 @@ +return function() + local getNavigationActionCreators = require(script.Parent.Parent.getNavigationActionCreators) + local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) + + local function expectError(functor, msg) + local status, err = pcall(functor) + expect(status).to.equal(false) + expect(string.find(err, msg)).to.never.equal(nil) + end + + it("should return a table with correct functions when called", function() + local result = getNavigationActionCreators() + expect(type(result.goBack)).to.equal("function") + expect(type(result.navigate)).to.equal("function") + expect(type(result.setParams)).to.equal("function") + end) + + describe("goBack tests", function() + it("should return a Back action when called", function() + local result = getNavigationActionCreators().goBack("theKey") + expect(result.type).to.equal(NavigationActions.Back) + expect(result.key).to.equal("theKey") + end) + + it("should throw when route.key is not a string", function() + expectError(function() + getNavigationActionCreators({ key = 5 }).goBack() + end, "%.goBack%(%): key should be a string") + end) + + it("should fall back to route.key if key is not provided", function() + local result = getNavigationActionCreators({ key = "routeKey" }).goBack() + expect(result.key).to.equal("routeKey") + end) + + it("should override route.key if key is provided", function() + local result = getNavigationActionCreators({ key = "routeKey" }).goBack("theKey") + expect(result.key).to.equal("theKey") + end) + end) + + describe("navigate tests", function() + it("should return a Navigate action when called", function() + local theParams = {} + local childAction = {} + local result = getNavigationActionCreators().navigate("theRoute", theParams, childAction) + expect(result.type).to.equal(NavigationActions.Navigate) + expect(result.routeName).to.equal("theRoute") + expect(result.params).to.equal(theParams) + expect(result.action).to.equal(childAction) + end) + + it("should return a navigate action with matching properties when called with a table", function() + local testNavigateTo = { + routeName = "theRoute", + params = {}, + action = {}, + } + + local result = getNavigationActionCreators().navigate(testNavigateTo) + expect(result.type).to.equal(NavigationActions.Navigate) + expect(result.routeName).to.equal("theRoute") + expect(result.params).to.equal(testNavigateTo.params) + expect(result.action).to.equal(testNavigateTo.action) + end) + + it("should throw when navigateTo is not a valid type", function() + expectError(function() + getNavigationActionCreators().navigate(5) + end, "%.navigate%(%): navigateTo must be a string or table") + end) + + it("should throw when params is provided with a table navigateTo", function() + expectError(function() + getNavigationActionCreators().navigate({}, {}) + end, "%.navigate%(%): params can only be provided with a string navigateTo value") + end) + + it("should throw when action is provided with a table navigateTo", function() + expectError(function() + getNavigationActionCreators().navigate({}, nil, {}) + end, "%.navigate%(%): child action can only be provided with a string navigateTo value") + end) + end) + + describe("setParams tests", function() + it("should return a SetParams action when called", function() + local theParams = {} + local result = getNavigationActionCreators({ key = "theKey" }).setParams(theParams) + expect(result.type).to.equal(NavigationActions.SetParams) + expect(result.key).to.equal("theKey") + expect(result.params).to.equal(theParams) + end) + + it("should throw when called by a root navigator", function() + expectError(function() + getNavigationActionCreators({}).setParams({}) + end, "%.setParams%(%): cannot be called by the root navigator") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getScreenForRouteName.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getScreenForRouteName.spec.lua new file mode 100644 index 0000000..3ad8af1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/getScreenForRouteName.spec.lua @@ -0,0 +1,89 @@ +return function() + local getScreenForRouteName = require(script.Parent.Parent.getScreenForRouteName) + + it("should throw for invalid arg types", function() + local status, err = pcall(function() + getScreenForRouteName("", "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "routeConfigs must be a table")).to.never.equal(nil) + + status, err = pcall(function() + getScreenForRouteName({}, 5) + end) + + expect(status).to.equal(false) + expect(string.find(err, "routeName must be a string")).to.never.equal(nil) + end) + + it("should throw if requested route is not present within table", function() + local status, err = pcall(function() + getScreenForRouteName({ + notMyRoute = function() return "foo" end + }, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "There is no route defined for key 'myRoute'.")).to.never.equal(nil) + end) + + it("should return raw table if screen and getScreen are not props", function() + local screenComponent = { render = function() return nil end } + local result = getScreenForRouteName({ + myRoute = screenComponent + }, "myRoute") + + expect(result).to.equal(screenComponent) + end) + + it("should return screen prop if it is set in route data table", function() + local screenComponent = { render = function() return nil end } + local result = getScreenForRouteName({ + myRoute = { + screen = screenComponent + } + }, "myRoute") + + expect(result).to.equal(screenComponent) + end) + + it("should return object returned by getScreen function if object is valid Roact element", function() + local screenComponent = { render = function() return nil end } + local result = getScreenForRouteName({ + myRoute = { + getScreen = function() return screenComponent end + } + }, "myRoute") + + expect(result).to.equal(screenComponent) + end) + + it("should throw if getScreen does not return a valid Roact element", function() + local status, err = pcall(function() + getScreenForRouteName({ + myRoute = { + getScreen = function() return nil end + } + }, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "The getScreen function defined for route 'myRoute'" .. + " did not return a valid screen or navigator")).to.never.equal(nil) + end) + + it("should throw if screen is not a valid Roact element", function() + local status, err = pcall(function() + getScreenForRouteName({ + myRoute = { + screen = 5, + } + }, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "screen param for key 'myRoute' must be a valid Roact component.")).to.never.equal(nil) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/validateRouteConfigMap.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/validateRouteConfigMap.spec.lua new file mode 100644 index 0000000..59a3500 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/validateRouteConfigMap.spec.lua @@ -0,0 +1,101 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local validateRouteConfigMap = require(script.Parent.Parent.validateRouteConfigMap) + + local function expectError(functor, msg) + local status, err = pcall(functor) + expect(status).to.equal(false) + expect(string.find(err, msg)).to.never.equal(nil) + end + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + return nil + end + + + it("should throw if routeConfigs is not a table", function() + expectError(function() + validateRouteConfigMap(5) + end, "routeConfigs must be a table") + end) + + it("should throw if routeConfigs is empty", function() + expectError(function() + validateRouteConfigMap({}) + end, "Please specify at least one route when configuring a navigator%.") + end) + + it("should throw if routeConfigs contains an invalid Roact element", function() + expectError(function() + validateRouteConfigMap({ + myRoute = 5, + }) + end, "The component for route 'myRoute' must be a Roact component or table with 'getScreen'%.") + end) + + it("should throw when both screen and getScreen are provided for same component", function() + expectError(function() + validateRouteConfigMap({ + myRoute = { + screen = "TheScreen", + getScreen = function() end, + } + }) + end, "Route 'myRoute' should provide 'screen' or 'getScreen', but not both%.") + end) + + it("should throw for a simple table where screen is not a Roact component", function() + expectError(function() + validateRouteConfigMap({ + myRoute = { + screen = {}, + } + }) + end, "The component for route 'myRoute' must be a Roact component or table with 'getScreen'%.") + end) + + it("should throw for a non-function getScreen", function() + expectError(function() + validateRouteConfigMap({ + myRoute = { + getScreen = 5 + } + }) + end, "The component for route 'myRoute' must be a Roact component or table with 'getScreen'%.") + end) + + it("should pass for valid basic routeConfigs", function() + validateRouteConfigMap({ + basicComponentRoute = TestComponent, + functionalComponentRoute = function() end, + stringNameComponentRoute = "Frame", + portalComponentRoute = Roact.Portal, + }) + end) + + it("should pass for valid screen prop type routeConfigs", function() + validateRouteConfigMap({ + basicComponentRoute = { + screen = TestComponent, + }, + functionalComponentRoute = { + screen = function() end, + }, + stringNameComponentRoute = { + screen = "Frame", + }, + portalComponentRoute = { + screen = Roact.Portal, + }, + }) + end) + + it("should pass for valid getScreen route configs", function() + validateRouteConfigMap({ + getScreenRoute = { + getScreen = function() end, + } + }) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/validateScreenOptions.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/validateScreenOptions.spec.lua new file mode 100644 index 0000000..c1bb8ee --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/_tests_/validateScreenOptions.spec.lua @@ -0,0 +1,25 @@ +return function() + local validateScreenOptions = require(script.Parent.Parent.validateScreenOptions) + + it("should not throw when there are no problems", function() + validateScreenOptions({ title = "foo" }, { routeName = "foo" }) + end) + + it("should throw error if no routeName is provided", function() + local status, err = pcall(function() + validateScreenOptions({ title = "bar" }, {}) + end) + + expect(status).to.equal(false) + expect(string.find(err, "route.routeName must be a string")).to.never.equal(nil) + end) + + it("should throw error for options with function for title", function() + expect(function() + validateScreenOptions({ + title = function() end, + }, { routeName = "foo" }) + end).to.throw() + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/createConfigGetter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/createConfigGetter.lua new file mode 100644 index 0000000..627f478 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/createConfigGetter.lua @@ -0,0 +1,53 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local getScreenForRouteName = require(script.Parent.getScreenForRouteName) +local validateScreenOptions = require(script.Parent.validateScreenOptions) +local validate = require(script.Parent.Parent.utils.validate) + +local function applyConfig(configurer, navigationOptions, configProps) + navigationOptions = navigationOptions or {} + + local configurerType = type(configurer) + if configurerType == "function" then + return Cryo.Dictionary.join(navigationOptions, + configurer(Cryo.Dictionary.join(configProps or {}, { + navigationOptions = navigationOptions + }))) + elseif configurerType == "table" then + return Cryo.Dictionary.join(navigationOptions, configurer) + else + return navigationOptions + end +end + +return function(routeConfigs, navigatorScreenConfig) + return function(navigation, screenProps) + screenProps = screenProps or {} + local route = navigation.state + + validate(type(route) == "table", "navigation.state must be a table") + validate(type(route.routeName == "string"), "routeName must be a string") + + local component = getScreenForRouteName(routeConfigs, route.routeName) + local routeConfig = routeConfigs[route.routeName] + + local routeScreenConfig = nil + if routeConfig ~= component then + routeScreenConfig = routeConfig.navigationOptions + end + + local componentScreenConfig = type(component) == "table" + and component.navigationOptions or {} + + local configOptions = { + navigation = navigation, + screenProps = screenProps, + } + + local outputConfig = applyConfig(navigatorScreenConfig, {}, configOptions) + outputConfig = applyConfig(componentScreenConfig, outputConfig, configOptions) + outputConfig = applyConfig(routeScreenConfig, outputConfig, configOptions) + + validateScreenOptions(outputConfig, route) + return outputConfig + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getChildRouter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getChildRouter.lua new file mode 100644 index 0000000..fc50799 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getChildRouter.lua @@ -0,0 +1,20 @@ +local validate = require(script.Parent.Parent.utils.validate) + +return function(router, routeName) + validate(type(router) == "table", "router must be a table") + validate(type(routeName) == "string", "routeName must be a string") + + if router.childRouters and router.childRouters[routeName] then + return router.childRouters[routeName] + end + + validate(type(router.getComponentForRouteName) == "function", + "router.getComponentForRouteName must be a function if no child routers are specified") + local component = router.getComponentForRouteName(routeName) + if type(component) == "table" then + return component.router + else + return nil + end +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getNavigationActionCreators.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getNavigationActionCreators.lua new file mode 100644 index 0000000..753ce08 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getNavigationActionCreators.lua @@ -0,0 +1,42 @@ +local NavigationActions = require(script.Parent.Parent.NavigationActions) +local validate = require(script.Parent.Parent.utils.validate) + +return function(route) + local result = {} + + -- Go back to screen identified by 'key', or the default for current route. + function result.goBack(key) + if key == nil and route.key then + validate(type(route.key) == "string", ".goBack(): key should be a string") + key = route.key + end + + return NavigationActions.back({ key = key }) + end + + -- Navigate to a different screen, either by route name+params+action, or + -- by passing a raw navigation table. + function result.navigate(navigateTo, params, action) + if type(navigateTo) == "string" then + return NavigationActions.navigate({ + routeName = navigateTo, + params = params, + action = action, + }) + else + validate(type(navigateTo) == "table", ".navigate(): navigateTo must be a string or table") + validate(params == nil, ".navigate(): params can only be provided with a string navigateTo value") + validate(action == nil, ".navigate(): child action can only be provided with a string navigateTo value") + + return NavigationActions.navigate(navigateTo) + end + end + + -- Change navigation params for current route + function result.setParams(params) + validate(type(route.key) == "string", ".setParams(): cannot be called by the root navigator") + return NavigationActions.setParams({ params = params, key = route.key }) + end + + return result +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getScreenForRouteName.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getScreenForRouteName.lua new file mode 100644 index 0000000..16519c7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/getScreenForRouteName.lua @@ -0,0 +1,32 @@ +local validate = require(script.Parent.Parent.utils.validate) +local isValidRoactElementType = require(script.Parent.Parent.utils.isValidRoactElementType) + +-- Extract a single screen Roact component/navigator from +-- a navigator's config. +return function(routeConfigs, routeName) + validate(type(routeConfigs) == "table", "routeConfigs must be a table") + validate(type(routeName) == "string", "routeName must be a string") + + local routeConfig = routeConfigs[routeName] + validate(routeConfig ~= nil, "There is no route defined for key '%s'.", routeName) + + local routeConfigType = type(routeConfig) + + if routeConfigType == "table" then + if routeConfig.screen ~= nil then + validate(isValidRoactElementType(routeConfig.screen), + "screen param for key '%s' must be a valid Roact component.", routeName) + return routeConfig.screen + elseif type(routeConfig.getScreen) == "function" then + local screen = routeConfig.getScreen() + validate(isValidRoactElementType(screen), + "The getScreen function defined for route '%s' did not return a valid screen or navigator", routeName) + return screen + end + end + + validate(isValidRoactElementType(routeConfig), + "Value for key '%s' must be a route config table or a valid Roact component.", routeName) + + return routeConfig +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/validateRouteConfigMap.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/validateRouteConfigMap.lua new file mode 100644 index 0000000..720eaf8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/validateRouteConfigMap.lua @@ -0,0 +1,41 @@ +local validate = require(script.Parent.Parent.utils.validate) +local isValidRoactElementType = require(script.Parent.Parent.utils.isValidRoactElementType) + +--[[ + This utility checks to make sure that configs passed to a + router are in the correct format. + + Example: + routeConfigs = { + routeNameEx1 = Roact.Component, + routeNameEx2 = { + screen = Roact.Component, + }, + routeNameEx3 = { + getScreen = function() + return Roact.Component + end + } + } +]] +return function(routeConfigs) + validate(type(routeConfigs) == "table", "routeConfigs must be a table") + + local atLeastOne = false + for routeName, routeConfig in pairs(routeConfigs) do + local configIsTable = type(routeConfig) == "table" or false + local screenConfig = configIsTable and routeConfig or {} -- easy index .screen/.getScreen + local screenComponent = configIsTable and routeConfig.screen or routeConfig + + validate(isValidRoactElementType(screenComponent) or type(screenConfig.getScreen) == "function", + "The component for route '%s' must be a Roact component or table with 'getScreen'.", + routeName) + validate(screenConfig.screen == nil or screenConfig.getScreen == nil, + "Route '%s' should provide 'screen' or 'getScreen', but not both.", routeName) + atLeastOne = true + end + + validate(atLeastOne, "Please specify at least one route when configuring a navigator.") + + return routeConfigs +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/validateScreenOptions.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/validateScreenOptions.lua new file mode 100644 index 0000000..87b731d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/routers/validateScreenOptions.lua @@ -0,0 +1,11 @@ +local validate = require(script.Parent.Parent.utils.validate) + +return function(screenOptions, route) + validate(type(screenOptions) == "table", "screenOptions must be a table") + validate(type(route) == "table", "route must be a table") + validate(type(route.routeName) == "string", "route.routeName must be a string") + validate(type(screenOptions.title) ~= "function", + "title cannot be defined as a function in navigation options for screen '%s'", + route.routeName) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/KeyGenerator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/KeyGenerator.lua new file mode 100644 index 0000000..b21abea --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/KeyGenerator.lua @@ -0,0 +1,19 @@ +local uniqueBaseId = "id-" .. tostring(math.random(100000, 1000000)) +local uuidCount = 0 + +local KeyGenerator = {} + +-- NOTE: FOR TESTING ONLY. +-- Normalize keys so that tests can be consistent. +function KeyGenerator.normalizeKeys() + uniqueBaseId = "id-test-" + uuidCount = 0 +end + +-- Get a string key that is unique for this session. +function KeyGenerator.generateKey() + uuidCount = uuidCount + 1 + return uniqueBaseId .. tostring(uuidCount) +end + +return KeyGenerator diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/TableUtilities.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/TableUtilities.lua new file mode 100644 index 0000000..5922e53 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/TableUtilities.lua @@ -0,0 +1,255 @@ +--[[ + Provides functions for comparing and printing lua tables. +]] + +local TableUtilities = {} +local defaultIgnore = {} + +local function makeKeyString(key) + if type(key) == "string" then + return string.format("%s", key) + else + return string.format("[%s]", tostring(key)) + end +end + +local function makeValueString(value) + local valueType = type(value) + if valueType == "string" then + return string.format("%q", value) + elseif valueType == "function" or valueType == "table" then + return string.format("<%s>", tostring(value)) + else + return string.format("%s", tostring(value)) + end +end + +local function printKeypair(key, value, indentStr, comment) + local keyString = makeKeyString(key) + local valueString = makeValueString(value) + + local commentStr = comment and string.format(" -- %s", comment) or "" + print(string.format("%s%s = %s,%s", indentStr, keyString, valueString, commentStr)) +end + +--[[ + Takes two tables A and B, returns if they have the same key-value pairs + Except ignored keys +]] +function TableUtilities.ShallowEqual(A, B, ignore) + if not A or not B then + return false + elseif A == B then + return true + end + + if not ignore then + ignore = defaultIgnore + end + + for key, value in pairs(A) do + if B[key] ~= value and not ignore[key] then + return false + end + end + for key, value in pairs(B) do + if A[key] ~= value and not ignore[key] then + return false + end + end + + return true +end + +--[[ + Takes two tables A, B and a key, returns if two tables have the same value at key +]] +function TableUtilities.EqualKey(A, B, key) + if A and B and key and key ~= "" and A[key] and B[key] and A[key] == B[key] then + return true + end + return false +end + +--[[ + Takes two tables A and B, returns a new table with elements of A + which are either not keys in B or have a different value in B +]] +function TableUtilities.TableDifference(A, B) + local new = {} + + for key, value in pairs(A) do + if B[key] ~= A[key] then + new[key] = value + end + end + + return new +end + + +--[[ + Takes a list and returns a table whose + keys are elements of the list and whose + values are all true +]] +local function membershipTable(list) + local result = {} + for i = 1, #list do + result[list[i]] = true + end + return result +end + + +--[[ + Takes a table and returns a list of keys in that table +]] +local function listOfKeys(t) + local result = {} + for key,_ in pairs(t) do + table.insert(result, key) + end + return result +end + + +--[[ + Takes two lists A and B, returns a new list of elements of A + which are not in B +]] +function TableUtilities.ListDifference(A, B) + return listOfKeys(TableUtilities.TableDifference(membershipTable(A), membershipTable(B))) +end + + +--[[ + For debugging. Returns false if the given table has any of the following: + - a key that is neither a number or a string + - a mix of number and string keys + - number keys which are not exactly 1..#t +]] +function TableUtilities.CheckListConsistency(t) + local containsNumberKey = false + local containsStringKey = false + local numberConsistency = true + + local index = 1 + for x, _ in pairs(t) do + if type(x) == 'string' then + containsStringKey = true + elseif type(x) == 'number' then + if index ~= x then + numberConsistency = false + end + containsNumberKey = true + else + return false + end + + if containsStringKey and containsNumberKey then + return false + end + + index = index + 1 + end + + if containsNumberKey then + return numberConsistency + end + + return true +end + + +--[[ + For debugging, serializes the given table to a reasonable string that might even interpret as lua. +]] +function TableUtilities.RecursiveToString(t, indent) + indent = indent or '' + + if type(t) == 'table' then + local result = "" + if not TableUtilities.CheckListConsistency(t) then + result = result .. "-- WARNING: this table fails the list consistency test\n" + end + result = result .. "{\n" + for k,v in pairs(t) do + if type(k) == 'string' then + result = result + .. " " + .. indent + .. tostring(k) + .. " = " + .. TableUtilities.RecursiveToString(v, " "..indent) + ..";\n" + end + if type(k) == 'number' then + result = result .. " " .. indent .. TableUtilities.RecursiveToString(v, " "..indent)..",\n" + end + end + result = result .. indent .. "}" + return result + else + return tostring(t) + end +end + +--[[ + For debugging. Prints the table on multiple lines to overcome log-line length + limitations which are otherwise necessary for performance. Use sparingly. +]] +function TableUtilities.Print(t, indent) + indent = indent or ' ' + + if type(t) ~= "table" then + error("TableUtilities.Print must be passed a table", 2) + end + + -- For cycle detection + local printedTables = {} + + local function recurse(subTable, tableKey, level) + -- Prevent cycles by keeping track of what tables we have printed + printedTables[subTable] = true + + local indentStr = string.rep(indent, level) + local valueIndentStr = string.rep(indent, level + 1) + + if tableKey then + print(string.format("%s%s = %s {", indentStr, makeKeyString(tableKey), makeValueString(subTable))) + else + print(string.format("%s%s {", indentStr, makeValueString(subTable))) + end + + for key, value in pairs(subTable) do + if type(value) == "table" then + if printedTables[value] then + printKeypair(key, value, valueIndentStr, "Possible cycle") + else + recurse(value, key, level + 1) + end + else + printKeypair(key, value, valueIndentStr) + end + end + + print(string.format("%s}%s", indentStr, (level > 0 and "," or ""))) + end + + recurse(t, nil, 0) +end + +--[[ + Takes a table and returns the field count +]] +function TableUtilities.FieldCount(t) + local fieldCount = 0 + for _ in pairs(t) do + fieldCount = fieldCount + 1 + end + return fieldCount +end + +return TableUtilities + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/KeyGenerator.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/KeyGenerator.spec.lua new file mode 100644 index 0000000..2e414ac --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/KeyGenerator.spec.lua @@ -0,0 +1,14 @@ +return function() + local KeyGenerator = require(script.Parent.Parent.KeyGenerator) + + it("should generate a new string key when called", function() + KeyGenerator.normalizeKeys() + + expect(KeyGenerator.generateKey()).to.equal("id-test-1") + expect(KeyGenerator.generateKey()).to.equal("id-test-2") + end) + + it("should generate unique string keys without being normalized", function() + expect(KeyGenerator.generateKey()).to.never.equal(KeyGenerator.generateKey()) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/getActiveChildNavigationOptions.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/getActiveChildNavigationOptions.spec.lua new file mode 100644 index 0000000..99c57a5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/getActiveChildNavigationOptions.spec.lua @@ -0,0 +1,47 @@ +return function() + local getActiveChildNavigationOptions = require(script.Parent.Parent.getActiveChildNavigationOptions) + + it("should return a function", function() + expect(type(getActiveChildNavigationOptions)).to.equal("function") + end) + + it("should ask router for current screen options and return them", function() + local testInputScreenOpts = {} + local testScreenOpts = {} + + local navigation = { + state = { + routes = { + { key = "123" } + }, + index = 1, + + }, + router = {}, -- stub + } + + function navigation.getChildNavigation(key) + if key == "123" then + return navigation + else + return nil + end + end + + local testOutputScreenOpts = nil + function navigation.router.getScreenOptions(activeNav, screenProps) + testOutputScreenOpts = screenProps + + if activeNav == navigation then + return testScreenOpts + else + return nil + end + end + + expect(getActiveChildNavigationOptions(navigation, testInputScreenOpts)) + .to.equal(testScreenOpts) + expect(testOutputScreenOpts).to.equal(testInputScreenOpts) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/isValidRoactElementType.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/isValidRoactElementType.spec.lua new file mode 100644 index 0000000..d4d66d9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/_tests_/isValidRoactElementType.spec.lua @@ -0,0 +1,19 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local isValidRoactElementType = require(script.Parent.Parent.isValidRoactElementType) + + it("should return true for valid element types", function() + expect(isValidRoactElementType("foo")).to.equal(true) + expect(isValidRoactElementType(function() return "foo" end)).to.equal(true) + expect(isValidRoactElementType(Roact.Portal)).to.equal(true) + expect(isValidRoactElementType( + { render = function() return "foo" end })).to.equal(true) + end) + + it("should return false for invalid element types", function() + expect(isValidRoactElementType(5)).to.equal(false) + expect(isValidRoactElementType({ render = "bad" })).to.equal(false) + expect(isValidRoactElementType( + { notRender = function() return "foo" end })).to.equal(false) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/getActiveChildNavigationOptions.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/getActiveChildNavigationOptions.lua new file mode 100644 index 0000000..a036af6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/getActiveChildNavigationOptions.lua @@ -0,0 +1,11 @@ +return function(navigation, screenProps) + local state = navigation.state + local router = navigation.router + local getChildNavigation = navigation.getChildNavigation + + local activeRoute = state.routes[state.index] + local activeNavigation = getChildNavigation(activeRoute.key) + + return router.getScreenOptions(activeNavigation, screenProps) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/getSceneIndicesForInterpolationInputRange.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/getSceneIndicesForInterpolationInputRange.lua new file mode 100644 index 0000000..a2e5c7c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/getSceneIndicesForInterpolationInputRange.lua @@ -0,0 +1,44 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) + +return function(props) + local scene = props.scene + local scenes = props.scenes + + local index = scene.index + local lastSceneIndexInScenes = #scenes + local isBack = not scenes[lastSceneIndexInScenes].isActive + + if isBack then + local currentSceneIndexInScenes = Cryo.List.find(scenes, scene) + + local targetSceneIndexInScenes = nil + for i, iScene in ipairs(scenes) do + if iScene.isActive then + targetSceneIndexInScenes = i + break + end + end + + local targetSceneIndex = scenes[targetSceneIndexInScenes].index + local lastSceneIndex = scenes[lastSceneIndexInScenes].index + + if index ~= targetSceneIndex and currentSceneIndexInScenes == lastSceneIndexInScenes then + return { + first = math.min(targetSceneIndex, index - 1), + last = index + 1, + } + elseif index == targetSceneIndex and currentSceneIndexInScenes == targetSceneIndexInScenes then + return { + first = index - 1, + last = math.max(lastSceneIndex, index + 1) + } + elseif index == targetSceneIndex or currentSceneIndexInScenes > targetSceneIndexInScenes then + return nil + end + end + + return { + first = index - 1, + last = index + 1 + } +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/isValidRoactElementType.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/isValidRoactElementType.lua new file mode 100644 index 0000000..05d5214 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/isValidRoactElementType.lua @@ -0,0 +1,11 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) + +-- Returns true if the provided object can be used by Roact.createElement(). +-- We have this method because Roact does not expose a type-checking API yet. +return function(elementType) + local theType = type(elementType) + + return theType == "string" or theType == "function" or elementType == Roact.Portal or + (theType == "table" and type(elementType.render) == "function") +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/validate.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/validate.lua new file mode 100644 index 0000000..a31a4ce --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/utils/validate.lua @@ -0,0 +1,19 @@ +--[[ + Validate() provides a mechanism to validate input arguments and + internal state for your components. You can call it like this: + + function myFunc(arg1, arg2) + validate(arg1 ~= arg2, "arg1 (%s) and arg2 (%s) must be different!", + tostring(arg1), tostring(arg2)) + return doSomething(arg1, arg2) + end + + The error will be surfaced at the *call site* of your function. +]] +return function(result, ...) + if not result then + error(string.format(...), 3) + end + + return result +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/AppNavigationContext.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/AppNavigationContext.lua new file mode 100644 index 0000000..ac91d5c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/AppNavigationContext.lua @@ -0,0 +1,60 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol) +local validate = require(script.Parent.Parent.utils.validate) + +local APP_NAVIGATION_CONTEXT = NavigationSymbol("APP_NAVIGATION_CONTEXT") + +-- Provider +local NavigationProvider = Roact.Component:extend("NavigationProvider") + +function NavigationProvider:init() + local navigation = self.props.navigation + validate(navigation ~= nil, "AppNavigationContext.Provider requires a 'navigation' prop.") + self._context[APP_NAVIGATION_CONTEXT] = { navigation = navigation } +end + +function NavigationProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +-- Consumer +local NavigationConsumer = Roact.Component:extend("NavigationConsumer") + +function NavigationConsumer:render() + local renderProp = self.props.render + local context = self._context[APP_NAVIGATION_CONTEXT] or {} + local navigation = self.props.navigation or context.navigation + + validate(renderProp ~= nil, "AppNavigationContext.Consumer requires 'render' prop.") + validate(navigation ~= nil, "AppNavigationContext.Consumer requires a navigation prop or context entry.") + + return renderProp(navigation) +end + +-- Static connector +local function connect(innerComponent) + local componentName = string.format("NavigationConnection(%s)", tostring(innerComponent)) + local Connection = Roact.Component:extend(componentName) + + function Connection:render() + local props = self.props + + return Roact.createElement(NavigationConsumer, { + navigation = props.navigation, -- can be passed directly to wrapper + render = function(navigation) + return Roact.createElement(innerComponent, Cryo.Dictionary.join({ + navigation = navigation + }, props)) -- join other props last so someone can manually pass in 'navigation' + end + }) + end + + return Connection +end + +return { + Provider = NavigationProvider, + Consumer = NavigationConsumer, + connect = connect, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ContentHeightFitFrame.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ContentHeightFitFrame.lua new file mode 100644 index 0000000..2d89978 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ContentHeightFitFrame.lua @@ -0,0 +1,90 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Roact) +local validate = require(script.Parent.Parent.utils.validate) + +--[[ + ContentHeightFitFrame creates a UIListLayout-based FitFrame around a single child. + The FitFrame will grow/shrink in height to exactly fit its child, but takes up the + entire useable horizontal space in its parent container. + + Props: + contentHorizontalAlignment - How a smaller-than-width child should be aligned + within the ContentHeightFitFrame. (Optional) + initialHeight - Starting height for the ContentHeightFirFrame. (Optional) + onHeightChanged - Callback to monitor height changes. (Optional) + + In addition to the above props, the normal Frame props can be passed through + (BackgroundColor3, BackgroundTransparency, etc). +]] +local ContentHeightFitFrame = Roact.Component:extend("ContentHeightFitFrame") + +ContentHeightFitFrame.defaultProps = { + initialHeight = 0, + contentHorizontalAlignment = Enum.HorizontalAlignment.Center, +} + +function ContentHeightFitFrame:init() + self._layoutRef = Roact.createRef() + + local containerRef = Roact.createRef() + self._getRef = function() + return self.props[Roact.Ref] or containerRef + end + + self._onResize = function() + local onHeightChanged = self.props.onHeightChanged + local layoutInstance = self._layoutRef.current + local containerInstance = self:_getRef().current + + if not layoutInstance or not containerInstance then + return + end + + local layoutSize = layoutInstance.AbsoluteContentSize + if layoutSize.Y ~= containerInstance.Size.Y then + containerInstance.Size = UDim2.new(1, 0, 0, layoutSize.Y) + + if onHeightChanged then + onHeightChanged(layoutSize.Y) + end + end + end +end + +function ContentHeightFitFrame:render() + validate(self.props.Size == nil, "Size cannot be specified with ContentHeightFitFrame!") + local contentHorizontalAlignment = self.props.contentHorizontalAlignment + local initialHeight = self.props.initialHeight + local children = self.props[Roact.Children] + + local containerProps = Cryo.Dictionary.join(self.props, { + contentHorizontalAlignment = Cryo.None, + initialHeight = Cryo.None, + onHeightChanged = Cryo.None, + [Roact.Children] = Cryo.None, + [Roact.Ref] = self:_getRef(), + Size = UDim2.new(1, 0, 0, initialHeight), -- Will adjust by size change callback. + }) + + return Roact.createElement("Frame", containerProps, { + ["$FitLayout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = contentHorizontalAlignment, + VerticalAlignment = Enum.VerticalAlignment.Top, + Padding = UDim.new(0, 0), + [Roact.Change.AbsoluteContentSize] = self._onResize, + [Roact.Ref] = self._layoutRef, + }), + ["$Content"] = Roact.oneChild(children), + }) +end + +function ContentHeightFitFrame:didMount() + self._onResize() +end + +function ContentHeightFitFrame:didUpdate() + self._onResize() +end + +return ContentHeightFitFrame diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/NavigationEventsAdapter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/NavigationEventsAdapter.lua new file mode 100644 index 0000000..76c8c1b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/NavigationEventsAdapter.lua @@ -0,0 +1,91 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local AppNavigationContext = require(script.Parent.AppNavigationContext) +local NavigationEvents = require(script.Parent.Parent.NavigationEvents) +local validate = require(script.Parent.Parent.utils.validate) + +--[[ + NavigationEventsAdapter providers a wrapper component that allows you to subscribe + to the navigation lifecycle events without having to explicitly manage your own + listener subscriptions. + + Usage: + + function MyComponent:init() + self.willFocus = function() + -- Do tasks that need to happen right before the component will appear on screen. + end + + self.didFocus = function() + -- Do tasks that need to happen right after the component appears on screen. + end + end + + function MyComponent:render() + -- Note that you must capture the self reference lexically, if you need it. + return Roact.createElement(RoactNavigation.EventsAdapter, { + [RoactNavigation.Events.WillFocus] = self.willFocus, + [RoactNavigation.Events.DidFocus] = self.didFocus, + [RoactNavigation.Events.WillBlur] = self.willBlur, + [RoactNavigation.Events.DidBlur] = self.didBlur, + }, ) + end + + Remember that focus and blur events may be called more than once in the lifetime of a + component. If you navigate away from a component and then come back later, it will receive + willBlur/didBlur and then willFocus/didFocus events. + + Also remember that your event handlers must capture any self reference lexically, if necessary. +]] +local NavigationEventsAdapter = Roact.Component:extend("NavigationEventsAdapter") + +function NavigationEventsAdapter:init() + self.subscriptions = {} +end + +function NavigationEventsAdapter:_subscribeAll() + local navigation = self.props.navigation + assert(navigation ~= nil, "NavigationEventsAdapter can only be used within the view hierarchy of a navigator.") + + for _, symbol in pairs(NavigationEvents) do + self.subscriptions[symbol] = navigation.addListener(symbol, function(...) + -- Retrieve callback from props each time, in case props change. + local callback = self.props[symbol] or nil + if callback then + validate(type(callback) == "function", "Value for event '%s' must be a function callback", tostring(symbol)) + callback(...) + end + end) + end +end + +function NavigationEventsAdapter:_disconnectAll() + for _, symbol in pairs(NavigationEvents) do + local sub = self.subscriptions[symbol] + if sub then + sub.disconnect() + self.subscriptions[symbol] = nil + end + end +end + +function NavigationEventsAdapter:didMount() + self:_subscribeAll() +end + +function NavigationEventsAdapter:willUnmount() + self:_disconnectAll() +end + +function NavigationEventsAdapter:didUpdate(prevProps) + if self.props.navigation ~= prevProps.navigation then + -- This component might get reused for different state, so we need to hook back up to events + self:_disconnectAll() + self:_subscribeAll() + end +end + +function NavigationEventsAdapter:render() + return Roact.createElement("Folder", nil, self.props[Roact.Children]) +end + +return AppNavigationContext.connect(NavigationEventsAdapter) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/SceneView.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/SceneView.lua new file mode 100644 index 0000000..4e8fc00 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/SceneView.lua @@ -0,0 +1,21 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local AppNavigationContext = require(script.Parent.AppNavigationContext) + +local SceneView = Roact.PureComponent:extend("SceneView") + +function SceneView:render() + local screenProps = self.props.screenProps + local component = self.props.component + local navigation = self.props.navigation + + return Roact.createElement(AppNavigationContext.Provider, { + navigation = navigation, + }, { + Scene = Roact.createElement(component, { + screenProps = screenProps, + navigation = navigation, + }) + }) +end + +return SceneView diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ScenesReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ScenesReducer.lua new file mode 100644 index 0000000..9ab7ca4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ScenesReducer.lua @@ -0,0 +1,201 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local TableUtilities = require(script.Parent.Parent.utils.TableUtilities) +local validate = require(script.Parent.Parent.utils.validate) + +local SCENE_KEY_PREFIX = "scene_" + +-- Compare two scenes based upon index and view key. +local function compareScenes(a, b) + if a.index == b.index then + -- compare the route keys + local delta = #a.key - #b.key + if delta == 0 then + return a.key < b.key + else + return delta < 0 + end + else + -- rank by index first + return a.index < b.index + end +end + +local function routesAreShallowEqual(a, b) + if not a or not b then + return a == b + end + + if a.key ~= b.key then + return false + end + + return TableUtilities.ShallowEqual(a, b) +end + +local function scenesAreShallowEqual(a, b) + return + a.key == b.key and + a.index == b.index and + a.isStale == b.isStale and + a.isActive == b.isActive and + routesAreShallowEqual(a, b) +end + +return function(scenes, nextState, prevState, descriptors) + -- Always update descriptors. See react-navigation's bug, here: + -- https://github.com/react-navigation/react-navigation/issues/4271 + -- TODO: Do we need this? Can we do a real fix? + for _, scene in ipairs(scenes) do + local route = scene.route + if descriptors and descriptors[route.key] then + scene.desciptor = descriptors[route.key] + end + end + + -- Bail out early if state is not updated + if prevState == nextState then + return scenes + end + + local prevScenes = {} + local freshScenes = {} + local staleScenes = {} + + -- previously stale scenes should be marked stale + for _, scene in ipairs(scenes) do + local key = scene.key + if scene.isStale then + staleScenes[key] = scene + end + + prevScenes[key] = scene + end + + local nextKeys = {} -- fake set! + local nextRoutes = nextState.routes + local nextRoutesLength = #nextRoutes + + -- Clip nextRoutes to stop at index because index is top of stack! + if nextRoutesLength > nextState.index then + print("Warning: StackRouter provided invalid state. Index should always be the top route") + nextRoutes = Cryo.List.removeRange(nextRoutes, nextState.index, nextRoutesLength) + end + + for index, route in ipairs(nextRoutes) do + local key = SCENE_KEY_PREFIX .. route.key + local descriptor = descriptors and descriptors[route.key] or nil + + local scene = { + index = index, + isActive = false, + isStale = false, + key = key, + route = route, + descriptor = descriptor, + } + + validate(not nextKeys[key], "navigation.state.routes[%d].key '%s' conflicts with another route!", index, key) + nextKeys[key] = true + + if staleScenes[key] then + -- Previously stale scene was added to nextState, so we remove it from + -- the map of stale scenes. + staleScenes[key] = nil + end + + freshScenes[key] = scene + end + + if prevState then + local prevRoutes = prevState.routes + local prevRoutesLength = #prevRoutes + if prevRoutesLength > prevState.index then + print("StackRouter provided invalid state. Index should always be the top route.") + prevRoutes = Cryo.List.removeRange(prevRoutes, prevState.index, prevRoutesLength) + end + + -- Search previous routes and mark any removed scenes as stale + for index, route in ipairs(prevRoutes) do + local key = SCENE_KEY_PREFIX .. route.key + -- Skip any refreshed scenes + if not freshScenes[key] then + local lastScene = nil + for _, scene in ipairs(scenes) do + if scene.route.key == route.key then + lastScene = scene + break + end + end + + local descriptor = descriptors[route.key] + if lastScene then + descriptor = lastScene.descriptor + end + + if descriptor then + staleScenes[key] = { + index = index, + isActive = false, + isStale = true, + key = key, + route = route, + descriptor = descriptor, + } + end + end + end + end + + local nextScenes = {} + + local function mergeScene(nextScene) + local key = nextScene.key + local prevScene = prevScenes[key] or nil + if prevScene and scenesAreShallowEqual(prevScene, nextScene) then + -- reuse prevScene to avoid re-render + table.insert(nextScenes, prevScene) + else + table.insert(nextScenes, nextScene) + end + end + + for _, scene in pairs(staleScenes) do + mergeScene(scene) + end + + for _, scene in pairs(freshScenes) do + mergeScene(scene) + end + + table.sort(nextScenes, compareScenes) + + local activeScenesCount = 0 + for index, scene in ipairs(nextScenes) do + local isActive = not scene.isStale and scene.index == nextState.index + if isActive ~= scene.isActive then + nextScenes[index] = Cryo.Dictionary.join(scene, { + isActive = isActive, + }) + end + + if isActive then + activeScenesCount = activeScenesCount + 1 + end + end + + validate(activeScenesCount == 1, "There should only be one active scene, not %d", activeScenesCount) + + -- Conditionally return nextScenes based upon shallow comparison, for performance + if #nextScenes ~= #scenes then + return nextScenes + end + + for index, scene in ipairs(nextScenes) do + if not scenesAreShallowEqual(scenes[index], scene) then + return nextScenes + end + end + + -- Scenes have not changed + return scenes +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ScreenGuiWrapper.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ScreenGuiWrapper.lua new file mode 100644 index 0000000..1bb98fa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/ScreenGuiWrapper.lua @@ -0,0 +1,36 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Roact) + +local ScreenGuiWrapper = Roact.PureComponent:extend("ScreenGuiWrapper") + +ScreenGuiWrapper.defaultProps = { + DisplayOrder = 0, + OnTopOfCoreBlur = false, + visible = true, +} + +function ScreenGuiWrapper:render() + local props = self.props + local component = props.component + local visible = props.visible + local displayOrder = props.DisplayOrder + local onTopOfCoreBlur = props.OnTopOfCoreBlur + + local filteredProps = Cryo.Dictionary.join(props, { + component = Cryo.None, + DisplayOrder = Cryo.None, + OnTopOfCoreBlur = Cryo.None, + -- visible prop is passed down for convenience of inner component. + }) + + return Roact.createElement("ScreenGui", { + Enabled = visible, + ZIndexBehavior = Enum.ZIndexBehavior.Sibling, + DisplayOrder = displayOrder, + OnTopOfCoreBlur = onTopOfCoreBlur, + }, { + InnerComponent = Roact.createElement(component, filteredProps) + }) +end + +return ScreenGuiWrapper diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackHeaderMode.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackHeaderMode.lua new file mode 100644 index 0000000..a16b48c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackHeaderMode.lua @@ -0,0 +1,15 @@ +local NavigationSymbol = require(script.Parent.Parent.Parent.NavigationSymbol) + +local FLOAT_SYMBOL = NavigationSymbol("FLOAT") +local SCREEN_SYMBOL = NavigationSymbol("SCREEN") +local NONE_SYMBOL = NavigationSymbol("NONE") + +--[[ + StackHeaderMode determines the behavior of the header when screens + are pushed/popped from the stack. +]] +return { + None = NONE_SYMBOL, -- No header. + Float = FLOAT_SYMBOL, -- Header that stays in place during transitions. + Screen = SCREEN_SYMBOL, -- Header that sticks to each screen and transitions with it. +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackPresentationStyle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackPresentationStyle.lua new file mode 100644 index 0000000..d11af5e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackPresentationStyle.lua @@ -0,0 +1,43 @@ +local NavigationSymbol = require(script.Parent.Parent.Parent.NavigationSymbol) + +local DEFAULT_SYMBOL = NavigationSymbol("DEFAULT") +local MODAL_SYMBOL = NavigationSymbol("MODAL") +local OVERLAY_SYMBOL = NavigationSymbol("OVERLAY") + +--[[ + StackPresentationStyle is used with stack navigators/views to determine + the behavior of a given screen when it is pushed/popped from the stack, as + well as the visual effects/transitions applied while the view is on screen. +]] +return { + --[[ + The Default presentation style draws stack cards so that they will + slide in and out from the right side of the screen one at a time. No + special visual effects are applied, and cards always fill the entire space + available. Your screen content is rendered over an opaque background color + by default, but you have the option to draw the cards with a semi- or fully + transparent background via navigationOptions. Cards always prevent + tap-through of the content underneath them (in case your navigation + container is transparent). + ]] + Default = DEFAULT_SYMBOL, + + --[[ + The Modal presentation style causes screens to animate up/down from the + bottom of the navigation container and visually stack on top of each + other. Cards are opaque by default, but you may set navigationOptions + to make them semi- or fully transparent so that you can see the underlying + cards. Modal cards always prevent tap-through of any underlying UI, including + other cards in the same stack. + ]] + Modal = MODAL_SYMBOL, + + --[[ + The Overlay presentation style causes screens to pop in (later they will fade in) + on top of the underlying screens. Like modals, they visually stack on top of each + other. Cards are opaque by default, but you may set navigationOptions to make + them semi- or fully transparent. Overlay cards always prevent tap-through of any + underlying UI, including other cards in the same stack. + ]] + Overlay = OVERLAY_SYMBOL +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackView.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackView.lua new file mode 100644 index 0000000..9071f8e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackView.lua @@ -0,0 +1,100 @@ +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) +local StackViewLayout = require(script.Parent.StackViewLayout) +local Transitioner = require(script.Parent.Parent.Transitioner) +local StackViewTransitionConfigs = require(script.Parent.StackViewTransitionConfigs) +local StackPresentationStyle = require(script.Parent.StackPresentationStyle) + +local defaultNavigationConfig = { + mode = StackPresentationStyle.Default, +} + + +local StackView = Roact.Component:extend("StackView") + +function StackView:init() + self._doRender = function(...) + return self:_render(...) + end + + self._doConfigureTransition = function(...) + return self:_configureTransition(...) + end + + self._doOnTransitionEnd = function(...) + return self:_onTransitionEnd(...) + end +end + +function StackView:render() + local screenProps = self.props.screenProps + local navigation = self.props.navigation + local descriptors = self.props.descriptors + local onTransitionStart = self.props.onTransitionStart + or self.props.navigationConfig.onTransitionStart + + -- Transitioner handles setting up the animation motors and making that data + -- available to the lower layer. + return Roact.createElement(Transitioner, { + render = self._doRender, + configureTransition = self._doConfigureTransition, + screenProps = screenProps, + navigation = navigation, + descriptors = descriptors, + onTransitionStart = onTransitionStart, + onTransitionEnd = self._doOnTransitionEnd, + }) +end + +function StackView:didMount() + local navigation = self.props.navigation + if navigation.state.isTransitioning then + navigation.dispatch(NavigationActions.completeTransition({ + key = navigation.state.key, + })) + end +end + +function StackView:_render(transitionProps, lastTransitionProps) + local screenProps = self.props.screenProps + local navigationConfig = Cryo.Dictionary.join(defaultNavigationConfig, self.props.navigationConfig) + local descriptors = self.props.descriptors + + return Roact.createElement(StackViewLayout, Cryo.Dictionary.join(navigationConfig, { + screenProps = screenProps, + descriptors = descriptors, + transitionProps = transitionProps, + lastTransitionProps = lastTransitionProps, + })) +end + +function StackView:_configureTransition(transitionProps, prevTransitionProps) + return StackViewTransitionConfigs.getTransitionConfig( + self.props.navigationConfig.transitionConfig, + transitionProps, + prevTransitionProps, + self.props.navigationConfig.mode + ).transitionSpec +end + +function StackView:_onTransitionEnd(transition, lastTransition) + local navigationConfig = self.props.navigationConfig + local navigation = self.props.navigation + local onTransitionEnd = navigationConfig.onTransitionEnd + local transitionDestKey = transition.scene.route.key + local isCurrentKey = navigation.state.routes[navigation.state.index].key == transitionDestKey + + if transition.navigation.state.isTransitioning and isCurrentKey then + navigation.dispatch(NavigationActions.completeTransition({ + key = navigation.state.key, + toChildKey = transitionDestKey, + })) + end + + if onTransitionEnd then + onTransitionEnd(transition, lastTransition) + end +end + +return StackView diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewCard.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewCard.lua new file mode 100644 index 0000000..a848dd8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewCard.lua @@ -0,0 +1,114 @@ +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local validate = require(script.Parent.Parent.Parent.utils.validate) + +--[[ + Render a scene as a card for use in a StackView. This component is + responsible for correctly positioning the scene content in relation + to the other scenes. The content will be rendered inside a completely + transparent Frame whose position and size are controlled by the + transition logic. Any additional visual effects must be supplied by + the container or the child element created by renderScene(). + + Props: + renderScene(scene) -- Render prop to draw the scene inside the card. + initialPosition -- Starting position for the card. (Animated by Otter from there). + positionStep -- Stepper function from StackViewInterpolator. + position -- Otter motor for the position of the card. + scene -- Scene that the card is to render. + forceHidden -- Forcibly disable card rendering (e.g. animated off-screen). + transparent -- Card allows underlying content to show through (default: false). + cardColor3 -- Color of the card background if it's not transparent (default: engine setting). +]] +local StackViewCard = Roact.Component:extend("StackViewCard") + +StackViewCard.defaultProps = { + transparent = false, +} + +function StackViewCard:init() + local currentNavIndex = self.props.navigation.state.index + + self._isMounted = false + self._positionLastValue = currentNavIndex + + local selfRef = Roact.createRef() + self._getRef = function() + return self.props[Roact.Ref] or selfRef + end +end + +function StackViewCard:render() + local forceHidden = self.props.forceHidden + local cardColor3 = self.props.cardColor3 + local transparent = self.props.transparent + local initialPosition = self.props.initialPosition + local renderScene = self.props.renderScene + local scene = self.props.scene + + validate(type(renderScene) == "function", "renderScene must be a function") + + return Roact.createElement("Frame", { + Position = initialPosition, + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = cardColor3 or nil, + BackgroundTransparency = transparent and 1 or nil, + BorderSizePixel = 0, + ClipsDescendants = true, + Visible = not forceHidden, + [Roact.Ref] = self:_getRef(), + }, { + ["$content"] = renderScene(scene), + }) +end + +function StackViewCard:didMount() + self._isMounted = true + + local position = self.props.position + self._positionDisconnector = position:onStep(function(...) + self:_onPositionStep(...) + end) +end + +function StackViewCard:willUnmount() + self._isMounted = false + + if self._positionDisconnector then + self._positionDisconnector() + self._positionDisconnector = nil + end +end + +function StackViewCard:didUpdate(oldProps) + local position = self.props.position + local positionStep = self.props.positionStep + + if position ~= oldProps.position then + self._positionDisconnector() + self._positionDisconnector = position:onStep(function(...) + self:_onPositionStep(...) + end) + end + + if positionStep ~= oldProps.positionStep then + -- The motor won't fire just because stepper function has changed. We have to + -- update the position to match new requirements based upon last motor value. + self:_onPositionStep(self._positionLastValue) + end +end + +function StackViewCard:_onPositionStep(value) + if not self._isMounted then + return + end + + local positionStep = self.props.positionStep + + if positionStep then + positionStep(self:_getRef(), value) + end + + self._positionLastValue = value +end + +return StackViewCard diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewInterpolator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewInterpolator.lua new file mode 100644 index 0000000..84be517 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewInterpolator.lua @@ -0,0 +1,220 @@ +--[[ + Provides builders to create functions that interpolate the current Otter motor + position into the correct translation for stack cards based upon their associated + scene. + + Interpolator builders expect the following props as input: + { + initialPositionValue = , + scene = , + layout = { + initWidth = , + initHeight = , + isMeasured = , + } + } + + Each builder returns a props table to be merged onto your other StackViewCard props, ex: + { + positionStep = , + initialPosition = , + forceHidden = true, -- May disable card visibility if it's outside interpolating range. + } + + The props table may contain other changes, depending on the requirements of the animation. +]] +local getSceneIndicesForInterpolationInputRange = require( + script.Parent.Parent.Parent.utils.getSceneIndicesForInterpolationInputRange) + +-- Helper interpolates t with range [0,1] into the range [a,b]. +local function lerp(a, b, t) + return a * (1 - t) + b * t +end + +-- Render initial style when layout hasn't been measured yet. +local function forInitial(props) + local initialPositionValue = props.initialPositionValue + local scene = props.scene + + local forceHidden = initialPositionValue ~= scene.index + local translate = forceHidden and 1000000 or 0 + + return { + forceHidden = forceHidden, + initialPosition = UDim2.new(0, translate, 0, translate), + positionStep = nil, + } +end + +-- Slide-in from right style (e.g. navigation stack view). +local function forHorizontal(props) + local initialPositionValue = props.initialPositionValue + local layout = props.layout + local scene = props.scene + + if not layout.isMeasured then + return forInitial(props) + end + + local interpolate = getSceneIndicesForInterpolationInputRange(props) + + -- getSceneIndices* returns nil if card is not visible and need not be + -- considered for the animation until state changes. + if not interpolate then + return { + forceHidden = true, + initialPosition = UDim2.new(0, 100000, 0, 100000), + positionStep = nil, + } + end + + local first = interpolate.first + local last = interpolate.last + local index = scene.index + + local width = layout.initWidth + + local function calculate(positionValue) + -- 3 range LERP + if positionValue < first then + return width + elseif positionValue < index then + return lerp(width, 0, (positionValue - first) / (index - first)) + elseif positionValue == index then + return 0 + elseif positionValue < last then + return lerp(0, -width, (positionValue - index) / (last - index)) + else + return -width + end + end + + local function stepper(cardRef, positionValue) + local cardInstance = cardRef.current + if not cardInstance then + return + end + + local oldPosition = cardInstance.Position + cardInstance.Position = UDim2.new( + oldPosition.X.Scale, + calculate(positionValue), + oldPosition.Y.Scale, + oldPosition.Y.Offset + ) + end + + local initialPosition = UDim2.new(0, calculate(initialPositionValue), 0, 0) + + return { + initialPosition = initialPosition, + positionStep = stepper, + } +end + +-- Slide-in from bottom style (e.g. modals). +local function forVertical(props) + local initialPositionValue = props.initialPositionValue + local layout = props.layout + local scene = props.scene + + if not layout.isMeasured then + return forInitial(props) + end + + local interpolate = getSceneIndicesForInterpolationInputRange(props) + + if not interpolate then + return { + forceHidden = true, + initialPosition = UDim2.new(0, 100000, 0, 100000), + positionStep = nil, + } + end + + local first = interpolate.first + local index = scene.index + local height = layout.initHeight + + local function calculate(positionValue) + -- 2 range LERP + if positionValue < first then + return height + elseif positionValue < index then + return lerp(height, 0, (positionValue - first) / (index - first)) + else + return 0 + end + end + + local function stepper(cardRef, positionValue) + local cardInstance = cardRef.current + if not cardInstance then + return + end + + local oldPosition = cardInstance.Position + cardInstance.Position = UDim2.new( + oldPosition.X.Scale, + oldPosition.X.Offset, + oldPosition.Y.Scale, + calculate(positionValue) + ) + end + + local initialPosition = UDim2.new(0, 0, 0, calculate(initialPositionValue)) + + return { + initialPosition = initialPosition, + positionStep = stepper, + } +end + +-- Fade in place animation (e.g. popovers and toasts). Note that since we don't currently have +-- group transparency, this 'animation' just pops the views in for now. +local function forFade(props) + local initialPositionValue = props.initialPositionValue + local layout = props.layout + local scene = props.scene + + if not layout.isMeasured then + return forInitial(props) + end + + local interpolate = getSceneIndicesForInterpolationInputRange(props) + + if not interpolate then + return { + forceHidden = true, + initialPosition = UDim2.new(0, 100000, 0, 100000), + positionStep = nil, + } + end + + local index = scene.index + + local function calculate(positionValue) + return positionValue >= index - 0.5 + end + + local function stepper(cardRef, positionValue) + local cardInstance = cardRef.current + if not cardInstance then + return + end + + cardInstance.Visible = calculate(positionValue) + end + + return { + forceHidden = not calculate(initialPositionValue), + initialPosition = UDim2.new(0, 0, 0, 0), + positionStep = stepper, + } +end + +return { + forHorizontal = forHorizontal, + forVertical = forVertical, + forFade = forFade, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewLayout.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewLayout.lua new file mode 100644 index 0000000..5f7ccd0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewLayout.lua @@ -0,0 +1,420 @@ +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local Otter = require(script.Parent.Parent.Parent.Parent.Otter) +local AppNavigationContext = require(script.Parent.Parent.AppNavigationContext) +local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) +local StackHeaderMode = require(script.Parent.StackHeaderMode) +local StackPresentationStyle = require(script.Parent.StackPresentationStyle) +local NoneSymbol = require(script.Parent.Parent.Parent.NoneSymbol) +local StackViewTransitionConfigs = require(script.Parent.StackViewTransitionConfigs) +local StackViewCard = require(script.Parent.StackViewCard) +local SceneView = require(script.Parent.Parent.SceneView) +local TopBar = require(script.Parent.Parent.TopBar.TopBar) +local ContentHeightFitFrame = require(script.Parent.Parent.ContentHeightFitFrame) +local validate = require(script.Parent.Parent.Parent.utils.validate) + +local defaultScreenOptions = { + overlayEnabled = false, + overlayColor3 = Color3.new(0, 0, 0), + overlayTransparency = 0.7, + -- cardColor3 default not needed; we use the engine's default frame color +} + +-- Helper interpolates t with range [0,1] into the range [a,b]. +local function lerp(a, b, t) + return a * (1 - t) + b * t +end + +local function calculateFadeTransparency(scene, index, positionValue) + local navigationOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {}) + local overlayEnabled = navigationOptions.overlayEnabled + local overlayTransparency = navigationOptions.overlayTransparency + + if overlayEnabled then + local pRange = math.max(math.min(1 + positionValue - index, 1), 0) + return lerp(1, overlayTransparency, pRange) + else + return 1 + end +end + + +local StackViewLayout = Roact.Component:extend("StackViewLayout") + +function StackViewLayout:init() + local startingIndex = self.props.transitionProps.navigation.state.index + + self._isMounted = false + self._scenesContainerRef = Roact.createRef() + + self._overlayFrameRefs = {} -- map of scene indexes to refs + + self._positionLastValue = startingIndex + + self._renderScene = function(scene) + return self:_renderInnerScene(scene) + end +end + +function StackViewLayout:_getHeaderMode() + if self.props.headerMode then + return self.props.headerMode + elseif self.props.mode == StackPresentationStyle.Modal then + return StackHeaderMode.Screen + else + -- TODO: Change back to Float when TopBar implements it + -- return StackHeaderMode.Float + return StackHeaderMode.Screen + end +end + +function StackViewLayout:_renderHeader(scene, headerMode) + local options = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {}) + local header = options.header + + validate(type(header) ~= "string", + "header must be a valid Roact component, RoactNavigation.None, or nil, not a string") + + -- If HeaderMode is Screen and no header was explicitly removed from + -- navigationOptions, then we do NOT want to render a header for this screen! + if header == NoneSymbol and headerMode == StackHeaderMode.Screen then + return nil + end + + -- We will use header component if supplied, otherwise use default TopBar. + local headerComponent = header ~= NoneSymbol and header or function(headerProps) + return Roact.createElement(TopBar, headerProps) + end + + local transitionProps = self.props.transitionProps or {} + local passProps = Cryo.Dictionary.join(self.props, { + transitionProps = Cryo.None, + }) + + return Roact.createElement(AppNavigationContext.Provider, { + navigation = scene.descriptor.navigation, + }, { + ["$header"] = Roact.createElement(headerComponent, Cryo.Dictionary.join( + passProps, + transitionProps, -- transitionProps override directly passed props + { + scene = scene, + mode = headerMode, + }) + ) + }) +end + +function StackViewLayout:_reset(resetToIndex, frequency) + local position = self.props.transitionProps.position + + position:setGoal(Otter.spring(resetToIndex, { + frequency, + })) +end + +function StackViewLayout:_goBack(backFromIndex, frequency) + local navigation = self.props.transitionProps.navigation + local position = self.props.transitionProps.position + local scenes = self.props.transitionProps.scenes + + local toValue = math.max(backFromIndex - 1, 1) + + -- Set up temporary completion handler + local onCompleteDisconnector + onCompleteDisconnector = position:onComplete(function() + if onCompleteDisconnector then + onCompleteDisconnector() + onCompleteDisconnector = nil + end + + local backFromScene + for _, scene in ipairs(scenes) do + if scene.index == toValue + 1 then + backFromScene = scene + break + end + end + + if backFromScene then + navigation.dispatch(NavigationActions.back({ + key = backFromScene.route.key, + immediate = true, + })) + + navigation.dispatch(NavigationActions.completeTransition()) + end + end) + + position:setGoal(Otter.spring(toValue, { + frequency = frequency, + })) +end + +function StackViewLayout:_onFloatingHeaderHeightChanged(height) + local scenesContainer = self._scenesContainerRef.current + if self._isMounted and scenesContainer then + scenesContainer.Position = UDim2.new(0, 0, 0, height) + scenesContainer.Size = UDim2.new(1, 0, 1, -height) + end +end + +function StackViewLayout:_renderCard(scene, index) + local transitionProps = self.props.transitionProps -- Core animation info from Transitioner. + local lastTransitionProps = self.props.lastTransitionProps -- Previous transition info. + local transitionConfig = self.state.transitionConfig -- State based info from scene config. + + local navigationOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {}) + + local cardColor3 = navigationOptions.cardColor3 + local overlayEnabled = navigationOptions.overlayEnabled + + local initialPositionValue = transitionProps.scene.index + if lastTransitionProps then + initialPositionValue = lastTransitionProps.scene.index + end + + local cardInterpolationProps = {} + local screenInterpolator = transitionConfig.screenInterpolator + if screenInterpolator then + cardInterpolationProps = screenInterpolator( + Cryo.Dictionary.join(transitionProps, { + initialPositionValue = initialPositionValue, + scene = scene, + }) + ) + end + + -- Merge down the various prop packages to be applied to StackViewCard. + return Roact.createElement(StackViewCard, Cryo.Dictionary.join( + transitionProps, cardInterpolationProps, { + key = "card_" .. tostring(scene.key), + scene = scene, + renderScene = self._renderScene, + transparent = overlayEnabled, + cardColor3 = cardColor3, + }) + ) +end + +function StackViewLayout:_renderInnerScene(scene) + local navigation = scene.descriptor.navigation + + local sceneComponent = scene.descriptor.getComponent() + local screenProps = self.props.screenProps + + local sceneElement = Roact.createElement(SceneView, { + screenProps = screenProps, + navigation = navigation, + component = sceneComponent, + }) + + local headerMode = self:_getHeaderMode() + if headerMode == StackHeaderMode.Screen then + --[[ + This ref is used to change the scene container size whenever the header changes height. + It's not too expensive to create one every time this thing is rendered, and since it's + not being set on the actual scene element then it won't trigger a bunch of reconciling + beyond this immediate layer. Its lifetime is the same as the onSizeChanged callback. + ]] + local sceneWrapperRef = Roact.createRef() + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + }, { + screenHeader = Roact.createElement(ContentHeightFitFrame, { + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + onHeightChanged = function(height) + local sceneWrapper = sceneWrapperRef.current + if self._isMounted and sceneWrapper then + sceneWrapper.Position = UDim2.new(0, 0, 0, height) + sceneWrapper.Size = UDim2.new(1, 0, 1, -height) + end + end + }, { + headerContent = self:_renderHeader(scene, headerMode) + }), + sceneWrapper = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + [Roact.Ref] = sceneWrapperRef, + }, { + scene = sceneElement, + }) + }) + else + return sceneElement + end +end + +function StackViewLayout:render() + local headerMode = self:_getHeaderMode() + local transitionProps = self.props.transitionProps + local topMostOpaqueSceneIndex = self.state.topMostOpaqueSceneIndex + local scenes = transitionProps.scenes + local floatingHeader = nil + + if headerMode == StackHeaderMode.Float then + local scene = transitionProps.scene + floatingHeader = Roact.createElement(ContentHeightFitFrame, { + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + onHeightChanged = function(...) + self:_onFloatingHeaderHeightChanged(...) + end + }, { + headerContent = self:_renderHeader(scene, headerMode) + }) + end + + local renderedScenes = Cryo.List.map(scenes, function(scene, idx) + -- The card is obscured if: + -- It's not the active card (e.g. we're transitioning TO it). + -- It's hidden underneath an opaque card that is NOT currently transitioning. + -- It's completely off-screen. + local cardObscured = idx < topMostOpaqueSceneIndex and not scene.isActive + + local navigationOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {}) + local overlayColor3 = navigationOptions.overlayColor3 + + -- Each scene gets its own overlay frame whose transparency must be managed. + local overlayFrameRef = self._overlayFrameRefs[idx] + if not overlayFrameRef then + overlayFrameRef = Roact.createRef() + self._overlayFrameRefs[idx] = overlayFrameRef + end + + -- Wrap all cards in a TextButton so we can control hidden state and ZIndex without bleeding props. + -- This button also provides the card's overlay effect when required and prevents pass-through of + -- stray touches. + return Roact.createElement("TextButton", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = overlayColor3, + BackgroundTransparency = calculateFadeTransparency(scene, idx, self._positionLastValue), + AutoButtonColor = false, + BorderSizePixel = 0, + Text = " ", + ZIndex = idx, + Visible = not cardObscured, + [Roact.Ref] = overlayFrameRef, + }, { + -- Cards need to have unique keys so that instances of the same components are not + -- reused for different scenes. (Could lead to unanticipated lifecycle problems). + ["card_" .. scene.key] = self:_renderCard(scene, idx), + }) + end) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + }, { + floatingHeader = floatingHeader or nil, + scenesContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), -- Overridden by _onFloatingHeaderHeightChanged + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + [Roact.Ref] = self._scenesContainerRef, + }, renderedScenes), + }) +end + +function StackViewLayout.getDerivedStateFromProps(nextProps, lastState) + local transitionProps = nextProps.transitionProps + local scenes = transitionProps.scenes + local state = transitionProps.navigation.state + local isTransitioning = state.isTransitioning + local topMostIndex = #scenes + + local isOverlayMode = nextProps.mode == StackPresentationStyle.Modal or + nextProps.mode == StackPresentationStyle.Overlay + + -- Find the last opaque scene in a modal stack so that we can optimize rendering. + local topMostOpaqueSceneIndex = 0 + if isOverlayMode then + for idx = topMostIndex, 1, -1 do + local scene = scenes[idx] + local navigationOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {}) + + -- Card covers other pages if it's not an overlay and it's not the top-most index while transitioning. + if not navigationOptions.overlayEnabled and not (isTransitioning and idx == topMostIndex) then + topMostOpaqueSceneIndex = idx + break + end + end + else + for idx = topMostIndex, 1, -1 do + if not (isTransitioning and idx == topMostIndex) then + topMostOpaqueSceneIndex = idx + break + end + end + end + + return { + topMostOpaqueSceneIndex = topMostOpaqueSceneIndex, + transitionConfig = StackViewTransitionConfigs.getTransitionConfig( + nextProps.transitionConfig, + nextProps.transitionProps, + nextProps.lastTransitionProps, + nextProps.mode), + } +end + +function StackViewLayout:didMount() + self._isMounted = true + + self._positionDisconnector = self.props.transitionProps.position:onStep(function(...) + self:_onPositionStep(...) + end) +end + +function StackViewLayout:willUnmount() + self._isMounted = false + + if self._positionDisconnector then + self._positionDisconnector() + self._positionDisconnector = nil + end +end + +function StackViewLayout:didUpdate(oldProps) + local position = self.props.transitionProps.position + + if position ~= oldProps.transitionProps.position then + self._positionDisconnector() + self._positionDisconnector = position:onStep(function(...) + self:_onPositionStep(...) + end) + end +end + +function StackViewLayout:_onPositionStep(value) + if self._isMounted then + local transitionProps = self.props.transitionProps + local scenes = transitionProps.scenes + + for idx, scene in ipairs(scenes) do + local frameRef = self._overlayFrameRefs[idx] + local frameInstance = frameRef and frameRef.current + + if frameInstance then + frameInstance.BackgroundTransparency = calculateFadeTransparency(scene, idx, value) + end + end + + self._positionLastValue = value + end +end + +return StackViewLayout diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewTransitionConfigs.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewTransitionConfigs.lua new file mode 100644 index 0000000..83e7a33 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/StackViewTransitionConfigs.lua @@ -0,0 +1,53 @@ +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) +local StackViewInterpolator = require(script.Parent.StackViewInterpolator) +local StackPresentationStyle = require(script.Parent.StackPresentationStyle) + +local DefaultTransitionSpec = { + frequency = 3, -- Hz + dampingRatio = 1, +} + +local SlideFromRight = { + transitionSpec = DefaultTransitionSpec, + screenInterpolator = StackViewInterpolator.forHorizontal, +} + +local ModalSlideFromBottom = { + transitionSpec = DefaultTransitionSpec, + screenInterpolator = StackViewInterpolator.forVertical, +} + +local FadeInPlace = { + transitionSpec = DefaultTransitionSpec, + screenInterpolator = StackViewInterpolator.forFade, +} + +local function getDefaultTransitionConfig(transitionProps, prevTransitionProps, presentationStyle) + if presentationStyle == StackPresentationStyle.Modal then + return ModalSlideFromBottom + elseif presentationStyle == StackPresentationStyle.Overlay then + return FadeInPlace + else + return SlideFromRight + end +end + +local function getTransitionConfig(transitionConfigurer, transitionProps, prevTransitionProps, presentationStyle) + local defaultConfig = getDefaultTransitionConfig(transitionProps, prevTransitionProps, presentationStyle) + if transitionConfigurer then + return Cryo.Dictionary.join( + defaultConfig, + transitionConfigurer(transitionProps, prevTransitionProps, presentationStyle) + ) + end + + return defaultConfig +end + +return { + getDefaultTransitionConfig = getDefaultTransitionConfig, + getTransitionConfig = getTransitionConfig, + SlideFromRight = SlideFromRight, + ModalSlideFromBottom = ModalSlideFromBottom, + FadeInPlace = FadeInPlace, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackHeaderMode.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackHeaderMode.spec.lua new file mode 100644 index 0000000..49ec372 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackHeaderMode.spec.lua @@ -0,0 +1,17 @@ +return function() + local StackHeaderMode = require(script.Parent.Parent.StackHeaderMode) + + describe("StackMode token tests", function() + it("should return same object for each token for multiple calls", function() + expect(StackHeaderMode.None).to.equal(StackHeaderMode.None) + expect(StackHeaderMode.Float).to.equal(StackHeaderMode.Float) + expect(StackHeaderMode.Screen).to.equal(StackHeaderMode.Screen) + end) + + it("should return matching string names for symbols", function() + expect(tostring(StackHeaderMode.None)).to.equal("NONE") + expect(tostring(StackHeaderMode.Float)).to.equal("FLOAT") + expect(tostring(StackHeaderMode.Screen)).to.equal("SCREEN") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackPresentationStyle.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackPresentationStyle.spec.lua new file mode 100644 index 0000000..0a64c09 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackPresentationStyle.spec.lua @@ -0,0 +1,17 @@ +return function() + local StackPresentationStyle = require(script.Parent.Parent.StackPresentationStyle) + + describe("StackPresentationStyle token tests", function() + it("should return same object for each token for multiple calls", function() + expect(StackPresentationStyle.Default).to.equal(StackPresentationStyle.Default) + expect(StackPresentationStyle.Modal).to.equal(StackPresentationStyle.Modal) + expect(StackPresentationStyle.Overlay).to.equal(StackPresentationStyle.Overlay) + end) + + it("should return matching string names for symbols", function() + expect(tostring(StackPresentationStyle.Default)).to.equal("DEFAULT") + expect(tostring(StackPresentationStyle.Modal)).to.equal("MODAL") + expect(tostring(StackPresentationStyle.Overlay)).to.equal("OVERLAY") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackView.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackView.spec.lua new file mode 100644 index 0000000..cd1a24e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackView.spec.lua @@ -0,0 +1,8 @@ +return function() + -- local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + -- local StackView = require(script.Parent.Parent.StackView) + + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewCard.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewCard.spec.lua new file mode 100644 index 0000000..1b3e68f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewCard.spec.lua @@ -0,0 +1,36 @@ +return function() + local Otter = require(script.Parent.Parent.Parent.Parent.Parent.Otter) + local Roact = require(script.Parent.Parent.Parent.Parent.Parent.Roact) + local StackViewCard = require(script.Parent.Parent.StackViewCard) + + it("should mount its renderProp and pass it scene", function() + local didRender = false + local testScene = { + isActive = true, + index = 1, + } + + local renderedScene = nil + local element = Roact.createElement(StackViewCard, { + renderScene = function(theScene) + renderedScene = theScene + return Roact.createElement(function() + didRender = true -- verifies component is attached to tree + end) + end, + scene = testScene, + position = Otter.createSingleMotor(1), + navigation = { + state = { + index = 1, + } + } + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(renderedScene).to.equal(testScene) + expect(didRender).to.equal(true) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewInterpolator.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewInterpolator.spec.lua new file mode 100644 index 0000000..b3092a6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewInterpolator.spec.lua @@ -0,0 +1,7 @@ +return function() + local StackViewInterpolator = require(script.Parent.Parent.StackViewInterpolator) + + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewLayout.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewLayout.spec.lua new file mode 100644 index 0000000..9882e6d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewLayout.spec.lua @@ -0,0 +1,8 @@ +return function() + -- local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + -- local StackViewLayout = require(script.Parent.Parent.StackViewLayout) + + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewTransitionConfigs.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewTransitionConfigs.spec.lua new file mode 100644 index 0000000..46e4e1e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/StackView/_tests_/StackViewTransitionConfigs.spec.lua @@ -0,0 +1,5 @@ +return function() + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/SwitchView.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/SwitchView.lua new file mode 100644 index 0000000..6602cc7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/SwitchView.lua @@ -0,0 +1,21 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local SceneView = require(script.Parent.SceneView) + +local SwitchView = Roact.Component:extend("SwitchView") + +function SwitchView:render() + local navState = self.props.navigation.state + local screenProps = self.props.screenProps + + local activeKey = navState.routes[navState.index].key + local descriptor = self.props.descriptors[activeKey] + local childComponent = descriptor.getComponent() + + return Roact.createElement(SceneView, { + component = childComponent, + navigation = descriptor.navigation, + screenProps = screenProps, + }) +end + +return SwitchView diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBar.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBar.lua new file mode 100644 index 0000000..195a236 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBar.lua @@ -0,0 +1,222 @@ +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) +local TopBarBackButton = require(script.Parent.TopBarBackButton) +local TopBarTitleContainer = require(script.Parent.TopBarTitleContainer) +local isValidRoactElementType = require(script.Parent.Parent.Parent.utils.isValidRoactElementType) +local StackHeaderMode = require(script.Parent.Parent.StackView.StackHeaderMode) +local validate = require(script.Parent.Parent.Parent.utils.validate) + +local TopBar = Roact.Component:extend("TopBar") + +local DEFAULT_HEIGHT = 56 +local DEFAULT_LEFT_WIDTH = UDim.new(0.3, 0) +local DEFAULT_CENTER_WIDTH = UDim.new(0.4, 0) +local DEFAULT_RIGHT_WIDTH = UDim.new(0.3, 0) + +function TopBar:_getTopBarTitleString(scene) + local options = scene.descriptor.options + if type(options.headerTitle) == "string" then + return options.headerTitle + end + if options.title and type(options.title) ~= "string" then + error("Invalid title for route" .. scene.route.routeName .. + " - title must be string or nil, instead it was of type " .. type(options.title)) + end + return options.title +end + +--[[ + Render either the provided headerTitleContainer component or our TopBarTitleContainer component +]] +function TopBar:_renderTitleComponent(props) + + local options = props.scene.descriptor.options + local headerSubtitle = options.headerSubtitle + local headerTitleContainerStyle = options.headerTitleContainerStyle or {} + + local renderHeaderTitle = options.renderHeaderTitle + local renderHeaderSubtitle = options.renderHeaderSubtitle + + local titleString = self:_getTopBarTitleString(props.scene) + local headerTitleStyle = options.headerTitleStyle + local headerSubtitleStyle = options.headerSubtitleStyle + + local renderHeaderTitleContainer = options.renderHeaderTitleContainer + + headerTitleContainerStyle = Cryo.Dictionary.join({ + Size = UDim2.new(DEFAULT_CENTER_WIDTH, UDim.new(1, 0)) + }, headerTitleContainerStyle) + + if renderHeaderTitleContainer then + return Roact.createElement(renderHeaderTitleContainer, { + headerTitleContainerStyle = headerTitleContainerStyle, + headerTitleStyle = headerTitleStyle, + headerSubtitleStyle = headerSubtitleStyle, + headerTitle = titleString, + headerSubtitle = headerSubtitle, + renderHeaderTitle = renderHeaderTitle, + renderHeaderSubtitle = renderHeaderSubtitle, + }) + end + + return Roact.createElement(TopBarTitleContainer, { + headerTitleContainerStyle = headerTitleContainerStyle, + headerTitleStyle = headerTitleStyle, + headerSubtitleStyle = headerSubtitleStyle, + headerTitle = titleString, + headerSubtitle = headerSubtitle, + renderHeaderTitle = renderHeaderTitle, + renderHeaderSubtitle = renderHeaderSubtitle, + }) +end + +function TopBar:_renderLeftComponent(props) + local options = props.scene.descriptor.options + + local renderHeaderLeftContainer = options.renderHeaderLeftContainer + local headerLeftContainerStyle = options.headerLeftContainerStyle or {} + local renderHeaderBackButton = options.renderHeaderBackButton + local headerBackButtonStyle = options.headerBackButtonStyle + + headerLeftContainerStyle = Cryo.Dictionary.join({ + Size = UDim2.new(DEFAULT_LEFT_WIDTH, UDim.new(1, 0)), + }, headerLeftContainerStyle) + + local function goBack() + props.scene.descriptor.navigation.goBack(props.scene.descriptor.key) + end + + if renderHeaderLeftContainer then + return Roact.createElement(renderHeaderLeftContainer, { + goBack = goBack, + headerLeftContainerStyle = headerLeftContainerStyle, + renderHeaderBackButton = renderHeaderBackButton, + headerBackButtonStyle = headerBackButtonStyle, + + }) + end + + -- Don't display anything if there's no page to go back to, by default + if props.scene.index == 1 then + return Roact.createElement("Frame", { + Size = UDim2.new(DEFAULT_LEFT_WIDTH, UDim.new(1, 0)), + BackgroundTransparency = 1, + }) + end + + return Roact.createElement(TopBarBackButton, { + goBack = goBack, + headerLeftContainerStyle = headerLeftContainerStyle, + renderHeaderBackButton = renderHeaderBackButton, + headerBackButtonStyle = headerBackButtonStyle, + }) +end + +function TopBar:_renderRightComponent(props) + local renderHeaderRight = props.scene.descriptor.options.renderHeaderRight + if renderHeaderRight then + return Roact.createElement(props.scene.descriptor.options.renderHeaderRight) + end + return Roact.createElement("Frame", { + Size = UDim2.new(DEFAULT_RIGHT_WIDTH, UDim.new(1, 0)), + BackgroundTransparency = 1, + LayoutOrder = 3, + }) +end + +function TopBar:_renderHeader(props) + local options = props.scene.descriptor.options + local left = self:_renderLeftComponent(props) + local center = self:_renderTitleComponent(props) + local right = self:_renderRightComponent(props) + local headerStyle = options.headerStyle or {} + local DEFAULT_HEADER_STYLE = { + Size = UDim2.new(1, 0, 0, DEFAULT_HEIGHT) + } + + headerStyle = Cryo.Dictionary.join(DEFAULT_HEADER_STYLE, headerStyle) + return Roact.createElement("Frame", headerStyle, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + left = left, + center = center, + right = right, + }) +end + +function TopBar:render() + local appBar + local mode = self.props.mode + local scene = self.props.scene + + if mode == StackHeaderMode.Float then + local scenesByIndex = {} + for _, s in ipairs(self.props.scenes) do + scenesByIndex[scene.index] = s + end + -- For each scene, create its props + local scenesProps = Cryo.List.map(Cryo.Dictionary.values(scenesByIndex), function(s, index) + return { + position = self.props.position, + scene = s + } + end) + appBar = Cryo.List.map(scenesProps, self._renderHeader) + error("TODO: implement support for Float") + else + local headerProps = { + scene = self.props.scene, + } + appBar = self:_renderHeader(headerProps) + end + return appBar +end + +local function validateProps(props) + local options = props.scene.descriptor.options + local headerTitle = options.headerTitle + local renderHeaderTitle = options.renderHeaderTitle + + local headerSubtitle = options.headerSubtitle + local renderHeaderSubtitle = options.renderHeaderSubtitle + + local renderHeaderTitleContainer = options.renderHeaderTitleContainer + local renderHeaderRight = options.renderHeaderRight + local renderLeftContainer = options.renderLeftContainer + local renderHeaderBackButton = options.renderHeaderBackButton + + validate(not (headerTitle and renderHeaderTitle), "You must not specify both headerTitle and renderHeaderTitle.") + validate(not (headerSubtitle and renderHeaderSubtitle), + "You must not specify both headerSubtitle and renderHeaderSubtitle.") + + if renderHeaderTitle then + validate(isValidRoactElementType(renderHeaderTitle), "renderHeaderTitle must be a valid Roact element type.") + end + if renderHeaderSubtitle then + validate(isValidRoactElementType(renderHeaderSubtitle), "renderHeaderSubtitle must be a valid Roact element type.") + end + if renderHeaderTitleContainer then + validate(isValidRoactElementType(renderHeaderTitleContainer), + "renderHeaderTitleContainer must be a valid Roact element type.") + end + if renderHeaderRight then + validate(isValidRoactElementType(renderHeaderRight), "renderHeaderRight must be a valid Roact element type.") + end + if renderLeftContainer then + validate(isValidRoactElementType(renderLeftContainer), "renderLeftContainer must be a valid Roact element type.") + end + if renderHeaderBackButton then + validate(isValidRoactElementType(renderHeaderBackButton), + "renderHeaderBackButton must be a valid Roact element type.") + end +end + +function TopBar.getDerivedStateFromProps(nextProps) + validateProps(nextProps) + return {} +end + +return TopBar \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBarBackButton.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBarBackButton.lua new file mode 100644 index 0000000..334e2e4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBarBackButton.lua @@ -0,0 +1,74 @@ +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) + +local TopBarBackButton = Roact.PureComponent:extend("TopBarBackButton") + +--[[ + Here rather than in defaultProps to allow one to overwrite specific parts of each style +]] +local DEFAULT_STYLES = { + headerLeftContainerStyle = { + Size = UDim2.new(0.3, 0, 1, 0), + BackgroundTransparency = 1, + }, + headerBackButtonStyle = { + Text = "<-", + LayoutOrder = 2, + Size = UDim2.new(0, 40, 0, 40), + }, +} + +TopBarBackButton.defaultProps = { + headerBackButtonStyle = {}, + headerLeftContainerStyle = {}, + goBack = function() end, +} + +function TopBarBackButton:_renderBackButton() + local renderHeaderBackButton = self.props.renderHeaderBackButton + local headerBackButtonStyle = self.props.headerBackButtonStyle + local goBack = self.props.goBack + + if renderHeaderBackButton then + headerBackButtonStyle = Cryo.Dictionary.join(headerBackButtonStyle, { + LayoutOrder = 2, + }) + return Roact.createElement(renderHeaderBackButton, { + goBack = goBack, + headerBackButtonStyle = headerBackButtonStyle, + }) + end + + headerBackButtonStyle = Cryo.Dictionary.join( + DEFAULT_STYLES.headerBackButtonStyle, + headerBackButtonStyle, + { + [Roact.Event.Activated] = goBack, + } + ) + + return Roact.createElement("TextButton", headerBackButtonStyle) +end + +function TopBarBackButton:render() + local headerLeftContainerStyle = self.props.headerLeftContainerStyle + headerLeftContainerStyle = Cryo.Dictionary.join(DEFAULT_STYLES.headerLeftContainerStyle, headerLeftContainerStyle, { + LayoutOrder = 1, + }) + return Roact.createElement("Frame", headerLeftContainerStyle, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + }), + Spacer = Roact.createElement("Frame", { + Size = UDim2.new(0, 12, 1, 0), + BackgroundTransparency = 1, + LayoutOrder = 1, + }), + Button = self:_renderBackButton(), + }) +end + +return TopBarBackButton \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBarTitleContainer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBarTitleContainer.lua new file mode 100644 index 0000000..d03a0a4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/TopBarTitleContainer.lua @@ -0,0 +1,137 @@ +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) + +local TopBarTitleContainer = Roact.PureComponent:extend("TopBarTitleContainer") + +local DEFAULT_STYLES = { + headerTitleContainerStyle = { + Size = UDim2.new(1, 0, 1, 0), + LayoutOrder = 2, + BackgroundTransparency = 1, + }, + headerTitleStyle = { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0.6, 0), + LayoutOrder = 1, + TextSize = 28, + Text = "Title", + TextColor3 = Color3.fromRGB(0, 0, 0), + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + }, + headerSubtitleStyle = { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0.2, 0), + LayoutOrder = 2, + TextSize = 14, + Text = "Subtitle", + TextColor3 = Color3.fromRGB(0, 0, 0), + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + } +} + +TopBarTitleContainer.defaultProps = { + headerTitleContainerStyle = {}, + headerTitleStyle = {}, + headerSubtitleStyle = {}, +} + +--[[ + Returns a Roact element that holds the title and subtitle elements +]] +function TopBarTitleContainer:_renderContainer(children) + local renderHeaderTitleContainer = self.props.renderHeaderTitleContainer + local headerTitle = self.props.headerTitle + local headerTitleStyle = self.props.headerTitleStyle + local headerSubtitle = self.props.headerSubtitle + local headerSubtitleStyle = self.props.headerSubtitleStyle + local headerTitleContainerStyle = self.props.headerTitleContainerStyle + + headerTitleContainerStyle = Cryo.Dictionary.join( + DEFAULT_STYLES.headerTitleContainerStyle, + headerTitleContainerStyle + ) + if renderHeaderTitleContainer then + return Roact.createElement(renderHeaderTitleContainer, { + headerTitleContainerStyle = headerTitleContainerStyle, + headerTitle = headerTitle, + headerTitleStyle = headerTitleStyle, + headerSubtitle = headerSubtitle, + headerSubtitleStyle = headerSubtitleStyle, + renderHeaderTitle = children["Title"], + renderHeaderSubtitle = children["Subtitle"], + }) + end + children["Layout"] = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + }) + return Roact.createElement("Frame", headerTitleContainerStyle, children) + +end + +--[[ + Returns a Roact element representing the title +]] +function TopBarTitleContainer:_renderTitle() + local headerTitle = self.props.headerTitle + local renderHeaderTitle = self.props.renderHeaderTitle + local headerTitleStyle = self.props.headerTitleStyle + + if renderHeaderTitle then + return Roact.createElement(renderHeaderTitle, { + headerTitle = headerTitle, + headerTitleStyle = headerTitleStyle, + }) + end + + headerTitleStyle = Cryo.Dictionary.join( + DEFAULT_STYLES.headerTitleStyle, + { + Text = headerTitle, + }, + headerTitleStyle) + return Roact.createElement("TextLabel", headerTitleStyle) +end + +--[[ + Returns a Roact element representing the subtitle +]] +function TopBarTitleContainer:_renderSubtitle() + local headerSubtitle = self.props.headerSubtitle + local renderHeaderSubtitle = self.props.renderHeaderSubtitle + local headerSubtitleStyle = self.props.headerSubtitleStyle + + if not (headerSubtitle or renderHeaderSubtitle) then + return nil + end + + if renderHeaderSubtitle then + return Roact.createElement(renderHeaderSubtitle, { + headerSubtitle = headerSubtitle, + headerSubtitleStyle = headerSubtitleStyle, + }) + end + + headerSubtitleStyle = Cryo.Dictionary.join( + DEFAULT_STYLES.headerSubtitleStyle, + headerSubtitleStyle, + { + Text = headerSubtitle, + } + ) + return Roact.createElement("TextLabel", headerSubtitleStyle) +end + +function TopBarTitleContainer:render() + local children = { + Title = self:_renderTitle(), + Subtitle = self:_renderSubtitle(), + } + return self:_renderContainer(children) +end + +return TopBarTitleContainer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBar.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBar.spec.lua new file mode 100644 index 0000000..d0d08ae --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBar.spec.lua @@ -0,0 +1,364 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Parent.Roact) + local TopBar = require(script.Parent.Parent.TopBar) + + it("should mount and unmount without issue", function() + local scene = { + descriptor = { + options = { + headerTitle = "HOME", + }, + navigation = { + goBack = function(key) + print(key) + end, + }, + key = "aScene" + }, + index = 1, + } + local props = { + scenes = { + scene, + }, + scene = scene, + } + + local instance = Roact.mount(Roact.createElement(TopBar, props)) + Roact.unmount(instance) + end) + + itSKIP("should detect when to display the back button", function() + local scene = { + descriptor = { + options = { + headerTitle = "HOME", + }, + navigation = { + goBack = function(key) + print(key) + end, + }, + key = "aScene" + }, + index = 2, + } + local props = { + scenes = { + scene, + }, + scene = scene, + } + + local topBar = Roact.createElement(TopBar, props) + local frame = Instance.new("Frame") + local handle = Roact.mount(topBar, frame) + + local Button = frame:FindFirstChild("Button", true) + + expect(Button).to.be.ok() + + Roact.unmount(handle) + frame:Destroy() + end) + + itSKIP("should detect when NOT to display the back button", function() + local scene = { + descriptor = { + options = { + headerTitle = "HOME", + }, + navigation = { + goBack = function(key) + print(key) + end, + }, + key = "aScene" + }, + index = 1, + } + local props = { + scenes = { + scene, + }, + scene = scene, + } + + local topBar = Roact.createElement(TopBar, props) + local frame = Instance.new("Frame") + local handle = Roact.mount(topBar, frame) + + local Button = frame:FindFirstChild("Button", true) + + expect(Button).to.never.be.ok() + + Roact.unmount(handle) + frame:Destroy() + end) + + it("should throw if we provide the wrong props", function() + local function renderAndMountAndCleanup(component, props) + local handle = Roact.mount(Roact.createElement(component, props)) + Roact.unomunt(handle) + end + + local scene1 = { + descriptor = { + options = { + headerTitle = "Throw Title!", + renderHeaderTitle = function() + return Roact.createElement("TextLabel") + end, + }, + navigation = { + goBack = function(key) + print(key) + end, + }, + key = "aScene" + }, + index = 2, + } + local props1 = { + scenes = { + scene1, + }, + scene = scene1, + } + expect(function() + renderAndMountAndCleanup(TopBar, props1) + end).to.throw() + local scene2 = { + descriptor = { + options = { + headerSubtitle = "Throw Subtitle!", + renderHeaderSubtitle = function() + return Roact.createElement("TextLabel") + end, + }, + navigation = { + goBack = function(key) + print(key) + end, + }, + key = "aScene" + }, + index = 2, + } + local props2 = { + scenes = { + scene2, + }, + scene = scene2, + } + expect(function() + renderAndMountAndCleanup(TopBar, props2) + end).to.throw() + local scene3 = { + descriptor = { + options = { + renderHeaderSubtitle = 38139, + }, + navigation = { + goBack = function(key) + print(key) + end, + }, + key = "aScene" + }, + index = 2, + } + local props3 = { + scenes = { + scene3, + }, + scene = scene3, + } + expect(function() + renderAndMountAndCleanup(TopBar, props3) + end).to.throw() + end) + + itSKIP("should properly pass on the options to the default components", function() + local testHeaderBackgroundColor = Color3.fromRGB(200, 200, 200) + local testTitle = "Title!" + local testSubtitle = "Subtitle!" + local testTitleSize = 55 + local testSubtitleSize = 11 + local testTitleContainerColor = Color3.fromRGB(144, 124, 123) + local testLeftContainerColor = Color3.fromRGB(23, 235, 244) + + local scene = { + descriptor = { + options = { + headerTitle = testTitle, + headerSubtitle = testSubtitle, + headerStyle = { + BackgroundColor3 = testHeaderBackgroundColor + }, + headerTitleStyle = { + TextSize = testTitleSize, + }, + headerSubtitleStyle = { + TextSize = testSubtitleSize, + }, + headerTitleContainerStyle = { + BackgroundColor3 = testTitleContainerColor, + }, + headerLeftContainerStyle = { + BackgroundColor3 = testLeftContainerColor, + } + }, + navigation = { + goBack = function(key) + print(key) + end, + }, + key = "aScene" + }, + index = 2, + } + local props = { + scenes = { + scene, + }, + scene = scene, + } + + local topBar = Roact.createElement(TopBar, props) + local frame = Instance.new("Frame") + local handle = Roact.mount(topBar, frame) + + local TopBarOutermost = frame:FindFirstChildWhichIsA("Frame", true) + local Title = frame:FindFirstChild("Title", true) + local Subtitle = frame:FindFirstChild("Subtitle", true) + local LeftContainer = frame:FindFirstChild("left", true) + local CenterContainer = frame:FindFirstChild("center", true) + + expect(TopBarOutermost.BackgroundColor3).to.equal(testHeaderBackgroundColor) + expect(Title.Text).to.equal(testTitle) + expect(Subtitle.Text).to.equal(testSubtitle) + expect(Title.TextSize).to.equal(testTitleSize) + expect(Subtitle.TextSize).to.equal(testSubtitleSize) + expect(LeftContainer.BackgroundColor3).to.equal(testLeftContainerColor) + expect(CenterContainer.BackgroundColor3).to.equal(testTitleContainerColor) + + Roact.unmount(handle) + frame:Destroy() + end) + + itSKIP("should accept render props and inject props", function() + local testHeaderBackgroundColor = Color3.fromRGB(200, 200, 200) + local testTitle = "Title!" + local testSubtitle = "Subtitle!" + local testTitleSize = 55 + local testSubtitleSize = 11 + local testTitleContainerColor = Color3.fromRGB(144, 124, 123) + local testLeftContainerColor = Color3.fromRGB(23, 235, 244) + local testRightContainerColor = Color3.fromRGB(233, 0, 2) + local testKey = "testKey" + + local scene = { + descriptor = { + options = { + renderHeaderTitle = function(props) + return Roact.createElement("TextLabel", { + Text = props.headerTitle, + TextSize = props.headerTitleStyle.TextSize, + }) + end, + renderHeaderSubtitle = function(props) + return Roact.createElement("TextLabel", { + Text = props.headerSubtitle, + TextSize = props.headerSubtitleStyle.TextSize, + }) + end, + renderHeaderTitleContainer = function(props) + return Roact.createElement("Frame", { + BackgroundColor3 = props.headerTitleContainerStyle.BackgroundColor3, + }, { + Title = Roact.createElement(props.renderHeaderTitle, { + headerTitleStyle = props.headerTitleStyle, + headerTitle = testTitle + }), + Subtitle = Roact.createElement(props.renderHeaderSubtitle, { + headerSubtitleStyle = props.headerSubtitleStyle, + headerSubtitle = testSubtitle + }), + }) + end, + renderHeaderRight = function() + return Roact.createElement("Frame", { + BackgroundColor3 = testRightContainerColor + }) + end, + renderHeaderLeftContainer = function(props) + return Roact.createElement("Frame", { + BackgroundColor3 = props.headerLeftContainerStyle.BackgroundColor3, + }, { + Button = Roact.createElement(props.renderHeaderBackButton, { + goBack = props.goBack, + }), + }) + end, + renderHeaderBackButton = function(props) + return Roact.createElement("ImageButton", { + [Roact.Event.Activated] = props.goBack, + }) + end, + headerStyle = { + BackgroundColor3 = testHeaderBackgroundColor + }, + headerTitleStyle = { + TextSize = testTitleSize, + }, + headerSubtitleStyle = { + TextSize = testSubtitleSize, + }, + headerTitleContainerStyle = { + BackgroundColor3 = testTitleContainerColor, + }, + headerLeftContainerStyle = { + BackgroundColor3 = testLeftContainerColor, + } + }, + navigation = { + goBack = function(key) + return key + end, + }, + key = testKey + }, + index = 2, + } + local props = { + scenes = { + scene, + }, + scene = scene, + } + + local topBar = Roact.createElement(TopBar, props) + local frame = Instance.new("Frame") + local handle = Roact.mount(topBar, frame) + + local TopBarOutermost = frame:FindFirstChildWhichIsA("Frame", true) + local Title = frame:FindFirstChild("Title", true) + local Subtitle = frame:FindFirstChild("Subtitle", true) + local LeftContainer = frame:FindFirstChild("left", true) + local CenterContainer = frame:FindFirstChild("center", true) + local RightContainer = frame:FindFirstChild("right", true) + + expect(TopBarOutermost.BackgroundColor3).to.equal(testHeaderBackgroundColor) + expect(Title.Text).to.equal(testTitle) + expect(Subtitle.Text).to.equal(testSubtitle) + expect(Title.TextSize).to.equal(testTitleSize) + expect(Subtitle.TextSize).to.equal(testSubtitleSize) + expect(LeftContainer.BackgroundColor3).to.equal(testLeftContainerColor) + expect(CenterContainer.BackgroundColor3).to.equal(testTitleContainerColor) + expect(RightContainer.BackgroundColor3).to.equal(testRightContainerColor) + + Roact.unmount(handle) + frame:Destroy() + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBarBackButton.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBarBackButton.spec.lua new file mode 100644 index 0000000..4436010 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBarBackButton.spec.lua @@ -0,0 +1,36 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Parent.Roact) + local TopBarBackButton = require(script.Parent.Parent.TopBarBackButton) + + it("should mount and unmount without issue", function() + local instance = Roact.mount(Roact.createElement(TopBarBackButton)) + Roact.unmount(instance) + end) + + itSKIP("should accept render props and inject them with props", function() + local testText = "This is some test text!" + local props = { + renderHeaderBackButton = function(props) + local headerBackButtonStyle = props.headerBackButtonStyle + return Roact.createElement("TextButton", { + Text = headerBackButtonStyle.Text + }) + end, + headerBackButtonStyle = { + Text = testText, + } + } + + local backButton = Roact.createElement(TopBarBackButton, props) + local frame = Instance.new("Frame") + local handle = Roact.mount(backButton, frame) + + local textButton = frame:FindFirstChildWhichIsA("TextButton", true) + local text = textButton.Text + expect(text).to.equal(testText) + + Roact.unmount(handle) + frame:Destroy() + end) + +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBarTitleContainer.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBarTitleContainer.spec.lua new file mode 100644 index 0000000..524d70b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/TopBar/_tests_/TopBarTitleContainer.spec.lua @@ -0,0 +1,98 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Parent.Roact) + local TopBarTitleContainer = require(script.Parent.Parent.TopBarTitleContainer) + + it("should mount and unmount without issue", function() + local instance = Roact.mount(Roact.createElement(TopBarTitleContainer)) + Roact.unmount(instance) + end) + + itSKIP("should inject the default with the provided style", function() + local testTitle = "Test title!" + local testSubtitle = "Test subtitle!" + local testTitleSize = 55 + local testSubtitleSize = 11 + local props = { + headerTitleStyle = { + TextSize = testTitleSize + }, + headerSubtitleStyle = { + TextSize = testSubtitleSize, + }, + headerTitle = testTitle, + headerSubtitle = testSubtitle, + } + + local container = Roact.createElement(TopBarTitleContainer, props) + local frame = Instance.new("Frame") + local handle = Roact.mount(container, frame) + + local Title = frame:FindFirstChild("Title", true) + local Subtitle = frame:FindFirstChild("Subtitle", true) + local titleText = Title.Text + local subtitleText = Subtitle.Text + local titleSize = Title.TextSize + local subtitleSize = Subtitle.TextSize + + expect(titleText).to.equal(testTitle) + expect(subtitleText).to.equal(testSubtitle) + expect(titleSize).to.equal(testTitleSize) + expect(subtitleSize).to.equal(testSubtitleSize) + + Roact.unmount(handle) + frame:Destroy() + end) + + itSKIP("should accept render props and inject them with props", function() + local testTitle = "Test title!" + local testSubtitle = "Test subtitle!" + local testTitleSize = 55 + local testSubtitleSize = 11 + local props = { + renderHeaderTitle = function(props) + local headerTitle = props.headerTitle + local headerTitleStyle = props.headerTitleStyle + return Roact.createElement("TextLabel", { + Text = headerTitle, + TextSize = headerTitleStyle.TextSize, + }) + end, + renderHeaderSubtitle = function(props) + local headerSubtitle = props.headerSubtitle + local headerSubtitleStyle = props.headerSubtitleStyle + return Roact.createElement("TextLabel", { + Text = headerSubtitle, + TextSize = headerSubtitleStyle.TextSize, + }) + end, + headerTitleStyle = { + TextSize = testTitleSize + }, + headerSubtitleStyle = { + TextSize = testSubtitleSize, + }, + headerTitle = testTitle, + headerSubtitle = testSubtitle, + } + + local container = Roact.createElement(TopBarTitleContainer, props) + local frame = Instance.new("Frame") + local handle = Roact.mount(container, frame) + + local Title = frame:FindFirstChild("Title", true) + local Subtitle = frame:FindFirstChild("Subtitle", true) + local titleText = Title.Text + local subtitleText = Subtitle.Text + local titleSize = Title.TextSize + local subtitleSize = Subtitle.TextSize + + expect(titleText).to.equal(testTitle) + expect(subtitleText).to.equal(testSubtitle) + expect(titleSize).to.equal(testTitleSize) + expect(subtitleSize).to.equal(testSubtitleSize) + + Roact.unmount(handle) + frame:Destroy() + end) + +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/Transitioner.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/Transitioner.lua new file mode 100644 index 0000000..a8a5bd7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/Transitioner.lua @@ -0,0 +1,298 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Roact) +local Otter = require(script.Parent.Parent.Parent.Otter) +local ScenesReducer = require(script.Parent.ScenesReducer) +local validate = require(script.Parent.Parent.utils.validate) + +local DEFAULT_TRANSITION_SPEC = { + frequency = 4 -- Hz +} + +local function buildTransitionProps(props, state) + local navigation = props.navigation + local options = props.options + + local layout = state.layout + local position = state.position + local scenes = state.scenes + + local activeScene + for _, x in ipairs(scenes) do + if x.isActive then + activeScene = x + break + end + end + + validate(activeScene, "Could not find active scene") + + return { + layout = layout, + navigation = navigation, + position = position, + scenes = scenes, + scene = activeScene, + options = options, + index = activeScene.index, + } +end + +local function filterStale(scenes) + local filtered = Cryo.List.filter(scenes, function(scene) + return not scene.isStale + end) + + if #filtered == #scenes then + return scenes + else + return filtered + end +end + +local Transitioner = Roact.Component:extend("Transitioner") + +function Transitioner:init() + local navigationState = self.props.navigation.state + local descriptors = self.props.descriptors + + self.state = { + -- Layout is passed to StackViewLayout in order to allow it to + -- sync animations. + layout = { + height = Otter.createSingleMotor(0), + width = Otter.createSingleMotor(0), + initWidth = 0, + initHeight = 0, + isMeasured = false, + }, + position = Otter.createSingleMotor(navigationState.index), + scenes = ScenesReducer({}, navigationState, nil, descriptors), + } + + self._doOnAbsoluteSizeChanged = function(...) + return self:_onAbsoluteSizeChanged(...) + end + + self._positionLastValue = navigationState.index + + self._prevTransitionProps = nil + self._transitionProps = buildTransitionProps(self.props, self.state) + + self._isMounted = false + self._isTransitionRunning = false + self._transitionQueue = {} + + self._completeSignalDisconnector = self.state.position:onComplete(function() + spawn(function() + if self._isMounted then + self:_onTransitionEnd() + end + end) + end) + + self._stepSignalDisconnector = self.state.position:onStep(function(value) + self._positionLastValue = value + end) +end + +function Transitioner:didMount() + self._isMounted = true +end + +function Transitioner:willUnmount() + self._isMounted = false + + if self._completeSignalDisconnector then + self._completeSignalDisconnector() + self._completeSignalDisconnector = nil + end + + if self._stepSignalDisconnector then + self._stepSignalDisconnector() + self._stepSignalDisconnector = nil + end +end + +function Transitioner:didUpdate(prevProps) + -- React-navigation uses componentWillReceiveProps that is only called when Parent + -- re-renders or when this component is actually being given new props, so we need to + -- filter here. If not, this would trigger on setState and enter an infinite loop. + if self.props ~= prevProps then + if self._isTransitionRunning then + local mostRecentTransition = self._transitionQueue[#self._transitionQueue] or {} + -- don't enqueue spurious extra copies of same transition props + if mostRecentTransition.prevProps ~= prevProps then + table.insert(self._transitionQueue, { prevProps = prevProps }) + end + + return + end + + self:_startTransition(prevProps, self.props) + end +end + +function Transitioner:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = true, + [Roact.Change.AbsoluteSize] = self._doOnAbsoluteSizeChanged, + }, { + ["$InnerComponent"] = self.props.render( + self._transitionProps, self._prevTransitionProps), + }) +end + +-- equivalent to React-Nav's Transitioner._onLayout +function Transitioner:_onAbsoluteSizeChanged(rbx) + local width = rbx.AbsoluteSize.X + local height = rbx.AbsoluteSize.Y + + if width == self.state.layout.initWidth and + height == self.state.layout.initHeight then + return + end + + local layout = Cryo.Dictionary.join(self.state.layout, { + initWidth = width, + initHeight = height, + isMeasured = true, + }) + + layout.width:setGoal(Otter.instant(width)) + layout.height:setGoal(Otter.instant(height)) + + local nextState = Cryo.Dictionary.join(self.state, { + layout = layout, + }) + + self._transitionProps = buildTransitionProps(self.props, nextState) + + spawn(function() + if self._isMounted then + self:setState(nextState) + end + end) +end + +function Transitioner:_computeScenes(props, nextProps) + local nextScenes = ScenesReducer( + self.state.scenes, + nextProps.navigation.state, + props.navigation.state, + nextProps.descriptors) + + if not nextProps.navigation.state.isTransitioning then + nextScenes = filterStale(nextScenes) + end + + if nextScenes == self.state.scenes then + return nil + end + + return nextScenes +end + +function Transitioner:_startTransition(props, nextProps) + local indexHasChanged = props.navigation.state.index ~= nextProps.navigation.state.index + local nextScenes = self:_computeScenes(props, nextProps) + + if not nextScenes then + -- If nextScenes is nil, nothing has changed, so report transition end, then bail + self._prevTransitionProps = self._transitionProps + + -- Ensure that position is set to final position before firing transitionEnd + -- See https://github.com/react-navigation/react-navigation/issues/5247 + self.state.position:setGoal(Otter.instant(props.navigation.state.index)) + -- Transition end will be called by position motor. + return + end + + local nextState = Cryo.Dictionary.join(self.state, { + scenes = nextScenes, + }) + + local position = nextState.position + local toValue = nextProps.navigation.state.index + + -- compute transitionProps + self._prevTransitionProps = self._transitionProps + self._transitionProps = buildTransitionProps(nextProps, nextState) + local isTransitioning = self._transitionProps.navigation.state.isTransitioning + + if not isTransitioning or not indexHasChanged then + -- If state is not transitioning, then we go immediately to new index. + -- Likewise, if the index has not changed then we still need to set up initial + -- positions via setState. + self:setState(nextState) + + if nextProps.onTransitionStart then + nextProps.onTransitionStart(self._transitionProps, self._prevTransitionProps) + end + + if indexHasChanged then + position:setGoal(Otter.instant(toValue)) + -- motor will call end for us + else + -- motor not running, need to end manually + self:_onTransitionEnd() + end + elseif isTransitioning then + self._isTransitionRunning = true + self:setState(nextState) + + if nextProps.onTransitionStart then + nextProps.onTransitionStart(self._transitionProps, self._prevTransitionProps) + end + + -- get transition spec + local transitionUserSpec = {} + if nextProps.configureTransition then + transitionUserSpec = nextProps.configureTransition( + self._transitionProps, self._prevTransitionProps) or {} + end + + local transitionSpec = Cryo.Dictionary.join(DEFAULT_TRANSITION_SPEC, transitionUserSpec) + + local positionHasChanged = self._positionLastValue ~= toValue + if indexHasChanged and positionHasChanged then + position:setGoal(Otter.spring(nextProps.navigation.state.index, transitionSpec)) + -- motor will call end transition + else + -- motor not running, end transition manually + self:_onTransitionEnd() + end + end +end + +function Transitioner:_onTransitionEnd() + local prevTransitionProps = self._prevTransitionProps + self._prevTransitionProps = nil + + local scenes = filterStale(self.state.scenes) + + local nextState = Cryo.Dictionary.join(self.state, { + scenes = scenes, + }) + + self._transitionProps = buildTransitionProps(self.props, nextState) + + self:setState(nextState) + + if self.props.onTransitionEnd then + self.props.onTransitionEnd(self._transitionProps, prevTransitionProps) + end + + local firstQueuedTransition = self._transitionQueue[1] + if firstQueuedTransition then + local prevProps = firstQueuedTransition.prevProps + self._transitionQueue = Cryo.List.removeIndex(self._transitionQueue, 1) + self:_startTransition(prevProps, self.props) + else + self._isTransitionRunning = false + end +end + +return Transitioner diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/AppNavigationContext.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/AppNavigationContext.spec.lua new file mode 100644 index 0000000..814b205 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/AppNavigationContext.spec.lua @@ -0,0 +1,91 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local AppNavigationContext = require(script.Parent.Parent.AppNavigationContext) + + it("should propagate navigation prop from provider to consumer", function() + local testNavigationContextProp = {} + local testComponentNavContext = nil + + local provider = Roact.createElement(AppNavigationContext.Provider, { + navigation = testNavigationContextProp + }, { + Child = Roact.createElement(AppNavigationContext.Consumer, { + render = function(navigation) + testComponentNavContext = navigation + end + }) + }) + + local instance = Roact.mount(provider) + expect(testComponentNavContext).to.equal(testNavigationContextProp) + Roact.unmount(instance) + end) + + it("should override context navigation prop if custom prop is set on consumer", function() + local testCustomNavigationProp = {} + local testComponentNavContext = nil + + local provider = Roact.createElement(AppNavigationContext.Provider, { + navigation = {} + }, { + Child = Roact.createElement(AppNavigationContext.Consumer, { + navigation = testCustomNavigationProp, + render = function(navigation) + testComponentNavContext = navigation + end + }) + }) + + local instance = Roact.mount(provider) + expect(testComponentNavContext).to.equal(testCustomNavigationProp) + Roact.unmount(instance) + end) + + it("should pass navigation prop to statically wrapped components", function() + local testNavigationContextProp = {} + local passedNavigationProp = nil + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + passedNavigationProp = self.props.navigation + end + + local WrappedComponent = AppNavigationContext.connect(TestComponent) + + local element = Roact.createElement(AppNavigationContext.Provider, { + navigation = testNavigationContextProp + }, { + Child = Roact.createElement(WrappedComponent) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(passedNavigationProp).to.equal(testNavigationContextProp) + end) + + it("should override static wrapper navigation prop when navigation is directly set", function() + local testNavigationProp = {} + local passedNavigationProp = nil + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + passedNavigationProp = self.props.navigation + end + + local WrappedComponent = AppNavigationContext.connect(TestComponent) + + local element = Roact.createElement(AppNavigationContext.Provider, { + navigation = {} + }, { + Child = Roact.createElement(WrappedComponent, { + navigation = testNavigationProp + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(passedNavigationProp).to.equal(testNavigationProp) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ContentHeightFitFrame.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ContentHeightFitFrame.spec.lua new file mode 100644 index 0000000..3e41511 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ContentHeightFitFrame.spec.lua @@ -0,0 +1,21 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local ContentHeightFitFrame = require(script.Parent.Parent.ContentHeightFitFrame) + + -- This must be skipped because Lemur does not behave like engine. + itSKIP("should size to contents", function() + local element = Roact.createElement(ContentHeightFitFrame, nil, { + ChildOne = Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 100), + }), + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "FitTest") + + expect(container.FitTest.Size.X.Offset).to.equal(50) + expect(container.FitTest.Size.Y.Offset).to.equal(100) + + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/NavigationEventsAdapter.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/NavigationEventsAdapter.spec.lua new file mode 100644 index 0000000..75a3dbd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/NavigationEventsAdapter.spec.lua @@ -0,0 +1,61 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local NavigationEvents = require(script.Parent.Parent.Parent.NavigationEvents) + local NavigationEventsAdapter = require(script.Parent.Parent.NavigationEventsAdapter) + + it("should subscribe to events for each registered handler", function() + local mockNavContext = { + subscribedHandlers = {}, + calledHandlers = {}, + } + + function mockNavContext:makeHandler(symbol) + return function() + self.calledHandlers[symbol] = true + end + end + + function mockNavContext.addListener(symbol, callback) + mockNavContext.subscribedHandlers[symbol] = callback + return { + disconnect = function() + mockNavContext.subscribedHandlers[symbol] = nil + end + } + end + + local adapter = Roact.createElement(NavigationEventsAdapter, { + navigation = mockNavContext, + [NavigationEvents.WillFocus] = mockNavContext:makeHandler(NavigationEvents.WillFocus), + [NavigationEvents.DidFocus] = mockNavContext:makeHandler(NavigationEvents.DidFocus), + [NavigationEvents.WillBlur] = mockNavContext:makeHandler(NavigationEvents.WillBlur), + [NavigationEvents.DidBlur] = mockNavContext:makeHandler(NavigationEvents.DidBlur), + }) + + local instance = Roact.mount(adapter) + + expect(type(mockNavContext.subscribedHandlers[NavigationEvents.WillFocus])).to.equal("function") + expect(type(mockNavContext.subscribedHandlers[NavigationEvents.DidFocus])).to.equal("function") + expect(type(mockNavContext.subscribedHandlers[NavigationEvents.WillBlur])).to.equal("function") + expect(type(mockNavContext.subscribedHandlers[NavigationEvents.DidBlur])).to.equal("function") + + mockNavContext.subscribedHandlers[NavigationEvents.WillFocus]() + expect(mockNavContext.calledHandlers[NavigationEvents.WillFocus]).to.equal(true) + + mockNavContext.subscribedHandlers[NavigationEvents.DidFocus]() + expect(mockNavContext.calledHandlers[NavigationEvents.DidFocus]).to.equal(true) + + mockNavContext.subscribedHandlers[NavigationEvents.WillBlur]() + expect(mockNavContext.calledHandlers[NavigationEvents.WillBlur]).to.equal(true) + + mockNavContext.subscribedHandlers[NavigationEvents.DidBlur]() + expect(mockNavContext.calledHandlers[NavigationEvents.DidBlur]).to.equal(true) + + Roact.unmount(instance) + + expect(mockNavContext.subscribedHandlers[NavigationEvents.WillFocus]).to.equal(nil) + expect(mockNavContext.subscribedHandlers[NavigationEvents.DidFocus]).to.equal(nil) + expect(mockNavContext.subscribedHandlers[NavigationEvents.WillBlur]).to.equal(nil) + expect(mockNavContext.subscribedHandlers[NavigationEvents.DidBlur]).to.equal(nil) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/SceneView.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/SceneView.spec.lua new file mode 100644 index 0000000..8cd3941 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/SceneView.spec.lua @@ -0,0 +1,36 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local SceneView = require(script.Parent.Parent.SceneView) + local withNavigation = require(script.Parent.Parent.withNavigation) + + it("should mount inner component and pass down required props+context.navigation", function() + local testComponentNavigationFromProp = nil + local testComponentScreenProps = nil + local testComponentNavigationFromContext = nil + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + testComponentNavigationFromProp = self.props.navigation + testComponentScreenProps = self.props.screenProps + + return withNavigation(function(navigation) + testComponentNavigationFromContext = navigation + end) + end + + local testScreenProps = {} + local testNav = {} + local element = Roact.createElement(SceneView, { + screenProps = testScreenProps, + navigation = testNav, + component = TestComponent, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(testComponentScreenProps).to.equal(testScreenProps) + expect(testComponentNavigationFromProp).to.equal(testNav) + expect(testComponentNavigationFromContext).to.equal(testNav) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ScenesReducer.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ScenesReducer.spec.lua new file mode 100644 index 0000000..46e4e1e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ScenesReducer.spec.lua @@ -0,0 +1,5 @@ +return function() + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ScreenGuiWrapper.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ScreenGuiWrapper.spec.lua new file mode 100644 index 0000000..1d81665 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/ScreenGuiWrapper.spec.lua @@ -0,0 +1,50 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local ScreenGuiWrapper = require(script.Parent.Parent.ScreenGuiWrapper) + + it("should mount the inner component if visible", function() + local innerComponentProps = nil + + local innerComponent = function(props) + innerComponentProps = props + return Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50) + }) + end + + local instance = Roact.mount(Roact.createElement(ScreenGuiWrapper, { + component = innerComponent, + visible = true, + myPassedInValue = 5, + })) + + expect(innerComponentProps).to.never.equal(nil) + expect(innerComponentProps.visible).to.equal(true) + expect(innerComponentProps.DisplayOrder).to.equal(nil) + expect(innerComponentProps.component).to.equal(nil) + expect(innerComponentProps.myPassedInValue).to.equal(5) + + Roact.unmount(instance) + end) + + it("should still mount the inner component if ScreenGui is not visible", function() + local innerComponentProps = nil + + local innerComponent = function(props) + innerComponentProps = props + return Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50) + }) + end + + local instance = Roact.mount(Roact.createElement(ScreenGuiWrapper, { + component = innerComponent, + visible = false, + })) + + expect(innerComponentProps).to.never.equal(nil) + expect(innerComponentProps.visible).to.equal(false) + + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/SwitchView.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/SwitchView.spec.lua new file mode 100644 index 0000000..05a9592 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/SwitchView.spec.lua @@ -0,0 +1,54 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local SwitchView = require(script.Parent.Parent.SwitchView) + local withNavigation = require(script.Parent.Parent.withNavigation) + + it("should mount and pass required props and context", function() + local testScreenProps = {} + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" } + }, + index = 1, + }, + } + + local testComponentNavigationFromProp = nil + local testComponentScreenProps = nil + local testComponentNavigationFromContext = nil + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + testComponentNavigationFromProp = self.props.navigation + testComponentScreenProps = self.props.screenProps + + return withNavigation(function(navigation) + testComponentNavigationFromContext = navigation + end) + end + + local testDescriptors = { + Foo = { + getComponent = function() + return TestComponent + end, + navigation = testNavigation, + } + } + + local element = Roact.createElement(SwitchView, { + screenProps = testScreenProps, + navigation = testNavigation, + descriptors = testDescriptors, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(testComponentNavigationFromProp).to.equal(testNavigation) + expect(testComponentScreenProps).to.equal(testScreenProps) + expect(testComponentNavigationFromContext).to.equal(testNavigation) + end) + +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/Transitioner.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/Transitioner.spec.lua new file mode 100644 index 0000000..ddbe63a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/Transitioner.spec.lua @@ -0,0 +1,8 @@ +return function() + -- local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + -- local Transitioner = require(script.Parent.Parent.Transitioner) + + itSKIP("should have its tests implemented", function() + + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/withNavigation.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/withNavigation.spec.lua new file mode 100644 index 0000000..7cf9988 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/withNavigation.spec.lua @@ -0,0 +1,34 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local withNavigation = require(script.Parent.Parent.withNavigation) + local AppNavigationContext = require(script.Parent.Parent.AppNavigationContext) + + it("should throw if no renderProp is provided", function() + local status, err = pcall(function() + withNavigation(nil) + end) + + expect(status).to.equal(false) + expect(string.find(err, "withNavigation must be passed a render prop")).to.never.equal(nil) + end) + + it("should extract navigation object from provider and pass it through", function() + local testNavigation = {} + local extractedNavigation = nil + + local rootElement = Roact.createElement(AppNavigationContext.Provider, { + navigation = testNavigation, + }, { + Child = Roact.createElement(function() + return withNavigation(function(nav) + extractedNavigation = nav + end) + end) + }) + + local rootInstance = Roact.mount(rootElement) + Roact.unmount(rootInstance) + + expect(extractedNavigation).to.equal(testNavigation) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/withNavigationFocus.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/withNavigationFocus.spec.lua new file mode 100644 index 0000000..88b5696 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/_tests_/withNavigationFocus.spec.lua @@ -0,0 +1,147 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local AppNavigationContext = require(script.Parent.Parent.AppNavigationContext) + local NavigationEvents = require(script.Parent.Parent.Parent.NavigationEvents) + local withNavigationFocus = require(script.Parent.Parent.withNavigationFocus) + + it("should pass focused=true when initially focused", function() + local testNavigation, testFocused + local component = function() + return withNavigationFocus(function(navigation, focused) + testNavigation = navigation + testFocused = focused + return nil + end) + end + + local navigationProp = { + isFocused = function() + return true + end, + addListener = function() + return { + disconnect = function() end + } + end + } + + local rootElement = Roact.createElement(AppNavigationContext.Provider, { + navigation = navigationProp, + }, { + child = Roact.createElement(component) + }) + + local instance = Roact.mount(rootElement) + expect(testNavigation).to.equal(navigationProp) + expect(testFocused).to.equal(true) + + Roact.unmount(instance) + end) + + it("should pass focused=false when initially unfocused", function() + local testNavigation, testFocused + local component = function() + return withNavigationFocus(function(navigation, focused) + testNavigation = navigation + testFocused = focused + return nil + end) + end + + local navigationProp = { + isFocused = function() + return false + end, + addListener = function() + return { + disconnect = function() end + } + end + } + + local rootElement = Roact.createElement(AppNavigationContext.Provider, { + navigation = navigationProp, + }, { + child = Roact.createElement(component) + }) + + local instance = Roact.mount(rootElement) + expect(testNavigation).to.equal(navigationProp) + expect(testFocused).to.equal(false) + + Roact.unmount(instance) + end) + + it("should re-render and set focused status for events", function() + local testListeners = {} + local testFocused = false + local component = function() + return withNavigationFocus(function(navigation, focused) + testFocused = focused + return nil + end) + end + + local navigationProp = { + isFocused = function() + return false + end, + addListener = function(event, listener) + testListeners[event] = listener + return { + disconnect = function() + testListeners[event] = nil + end + } + end + } + + local rootElement = Roact.createElement(AppNavigationContext.Provider, { + navigation = navigationProp, + }, { + child = Roact.createElement(component) + }) + + local instance = Roact.mount(rootElement) + expect(testFocused).to.equal(false) + expect(type(testListeners[NavigationEvents.DidFocus])).to.equal("function") + expect(type(testListeners[NavigationEvents.WillBlur])).to.equal("function") + + testListeners[NavigationEvents.DidFocus]() + expect(testFocused).to.equal(true) + + testListeners[NavigationEvents.WillBlur]() + expect(testFocused).to.equal(false) + + Roact.unmount(instance) + expect(testListeners[NavigationEvents.DidFocus]).to.equal(nil) + expect(testListeners[NavigationEvents.WillBlur]).to.equal(nil) + end) + + it("should throw when renderProp is not provided", function() + local success, err = pcall(function() + withNavigationFocus(nil) + end) + + expect(success).to.equal(false) + expect(string.find(err, + "withNavigationFocus must be passed a render prop")).to.never.equal(nil) + end) + + it("should throw when used outside of a navigation provider", function() + local component = function() + return withNavigationFocus(function(navigation, focused) + + end) + end + + local element = Roact.createElement(component) + + local success, _ = pcall(function() + Roact.unmount(Roact.mount(element)) + end) + + expect(success).to.equal(false) + -- We do not test the message because NavigationConsumer gets in the way here. + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/withNavigation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/withNavigation.lua new file mode 100644 index 0000000..4590801 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/withNavigation.lua @@ -0,0 +1,24 @@ +local Roact = require(script.Parent.Parent.Parent.Roact) +local AppNavigationContext = require(script.Parent.AppNavigationContext) +local validate = require(script.Parent.Parent.utils.validate) + +--[[ + withNavigation() is a convenience function that you can use in your component's + render function to access the navigation context object. For example: + + function MyComponent:render() + return withNavigation(function(navigation) + return Roact.createElement("TextButton", { + [Roact.Activated] = function() + navigation.navigate("DetailPage") + end + }) + end) + end +]] +return function(renderProp) + validate(renderProp ~= nil, "withNavigation must be passed a render prop") + return Roact.createElement(AppNavigationContext.Consumer, { + render = renderProp + }) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/withNavigationFocus.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/withNavigationFocus.lua new file mode 100644 index 0000000..cf9d96b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roact-navigation/roact-navigation/views/withNavigationFocus.lua @@ -0,0 +1,89 @@ +--[[ + withNavigationFocus() is a convenience function that extends withNavigation(), + allowing your render function (and therefor your subgraph) to access the + navigation context object AND an additional boolean that indicates whether or + not the containing screen component is in focus. For example: + + function MyButtonComponent:render() + return withNavigationFocus(function(navigation, focused) + return Roact.createElement("TextButton", { + Enabled = focused, + [Roact.Event.Activated] = function() + navigation.navigate("DetailPage") + end, + }) + end) + end + + This is very useful when writing generic components that need to work with + the navigation system (e.g. preventing buttons from navigating when a screen + is not in focus so you don't cause double-navigation). + + Note that if you ONLY need the 'navigation' context object, it is recommended + that you use withNavigation() for performance reasons. +]] +local Roact = require(script.Parent.Parent.Parent.Roact) +local NavigationEvents = require(script.Parent.Parent.NavigationEvents) +local AppNavigationContext = require(script.Parent.AppNavigationContext) +local validate = require(script.Parent.Parent.utils.validate) + + +local NavigationFocusComponent = Roact.Component:extend("NavigationFocusComponent") + +function NavigationFocusComponent:init() + local navigation = self.props.navigation + self.state = { + isFocused = navigation and navigation.isFocused() or false + } +end + +function NavigationFocusComponent:didMount() + local navigation = self.props.navigation + validate(navigation ~= nil, + "withNavigationFocus can only be used within the view hierarchy of a navigator. " .. + "The wrapped component cannot access 'navigation' from props or context.") + + self._didFocusListener = navigation.addListener(NavigationEvents.DidFocus, function() + -- no spawn because we expect this to be called directly from safe paths + self:setState({ + isFocused = true, + }) + end) + + self._willBlurListener = navigation.addListener(NavigationEvents.WillBlur, function() + -- no spawn because we expect this to be called directly from safe paths + self:setState({ + isFocused = false, + }) + end) +end + +function NavigationFocusComponent:willUnmount() + if self._didFocusListener then + self._didFocusListener:disconnect() + self._didFocusListener = nil + end + + if self._willBlurListener then + self._willBlurListener:disconnect() + self._willBlurListener = nil + end +end + +function NavigationFocusComponent:render() + local isFocused = self.state.isFocused + local navigation = self.props.navigation + local render = self.props.render + + return render(navigation, isFocused) +end + +NavigationFocusComponent = AppNavigationContext.connect(NavigationFocusComponent) + +return function(renderProp) + validate(renderProp ~= nil, "withNavigationFocus must be passed a render prop") + + return Roact.createElement(NavigationFocusComponent, { + render = renderProp + }) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/Rodux.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/Rodux.lua new file mode 100644 index 0000000..96b67df --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/Rodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_rodux"]["rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/UIBlox.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/UIBlox.lua new file mode 100644 index 0000000..2e0dcc2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/UIBlox.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["UIBlox"]["UIBlox"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/asset-card/asset-card/Components/AssetCard.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/asset-card/asset-card/Components/AssetCard.lua new file mode 100644 index 0000000..9e0bc82 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/asset-card/asset-card/Components/AssetCard.lua @@ -0,0 +1,103 @@ +local TextService = game:GetService("TextService") + +local root = script.Parent.Parent +local Packages = root.Parent + +local UIBlox = require(Packages.UIBlox) +local Roact = require(Packages.Roact) +local t = require(Packages.t) + +local LoadableImage = UIBlox.App.Loading.LoadableImage + +local IMAGE_SIZE = 128 +local FOOTER_HEIGHT = 20 + +local MAX_TEXT_BOUND = 10000 +local TEXT_LINE_HEIGHT = 20 +local TEXT_PADDING = 12 +local TEXT_TOP_PADDING = 8 + +local AssetCard = Roact.PureComponent:extend("AssetCard") + +local validateProps = t.interface({ + image = t.string, + imageTransparency = t.number, + textColor3 = t.Color3, + textTransparency = t.number, + text = t.string, + font = t.enum(Enum.Font), + onActivated = t.callback, +}) + +AssetCard.defaultProps = { + image = "", + imageTransparency = 0, + textColor3 = Color3.fromRGB(242, 244, 245), + textTransparency = 0, + text = "tucker was here", + font = Enum.Font.Gotham, + onActivated = function() end, +} + +function AssetCard:render() + local props = self.props + assert(validateProps(props)) + + local text = props.text + local font = props.font + + local textLabelWidth = IMAGE_SIZE - (TEXT_PADDING*2) + local textWidth = TextService:GetTextSize(text, 16, font, Vector2.new(MAX_TEXT_BOUND, MAX_TEXT_BOUND)).X + + local lines = 1 + + if textWidth > textLabelWidth then + lines = 2 + end + + local textFooterSize = FOOTER_HEIGHT+(TEXT_LINE_HEIGHT*lines) + + return Roact.createElement("ImageButton", { + Size = UDim2.new(0, IMAGE_SIZE, 0, IMAGE_SIZE + textFooterSize), + BackgroundTransparency = 1, + [Roact.Event.Activated] = props.onActivated, + }, { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + }), + assetIcon = Roact.createElement(LoadableImage, { + Size = UDim2.new(0, IMAGE_SIZE, 0, IMAGE_SIZE), + BackgroundTransparency = 0.5, + LayoutOrder = 1, + Image = props.image, + ImageTransparency = props.imageTransparency, + useShimmerAnimationWhileLoading = true, + showFailedStateWhenLoadingFailed = true, + }), + nameContainer = Roact.createElement("Frame", { + Size = UDim2.new(0, IMAGE_SIZE, 0, textFooterSize), + BackgroundTransparency = 1, + LayoutOrder = 2, + }, { + padding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0,TEXT_TOP_PADDING), + PaddingBottom = UDim.new(0, TEXT_PADDING), + PaddingLeft = UDim.new(0, TEXT_PADDING), + PaddingRight = UDim.new(0,TEXT_PADDING), + }), + assetName = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Font = font, + TextTransparency = props.textTransparency, + TextColor3 = props.textColor3, + TextSize = 16, + TextTruncate = Enum.TextTruncate.AtEnd, + TextWrapped = true, + Text = text, + }) + }) + }) +end + +return AssetCard diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/asset-card/asset-card/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/asset-card/asset-card/init.lua new file mode 100644 index 0000000..ce59c8b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/asset-card/asset-card/init.lua @@ -0,0 +1,3 @@ +return { + AssetCard = require(script.Components.AssetCard), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/lock.toml new file mode 100644 index 0000000..29aebb2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/lock.toml @@ -0,0 +1,11 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/asset-card" +version = "1.0.3" +commit = "103b2a571ef09611ac91d38f15c3777682f69bfe" +source = "git+https://github.com/roblox/asset-card#v1.0.3" +dependencies = [ + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "UIBlox UIBlox 3a82d9ed git+https://github.com/roblox/uiblox#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/t.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_asset-card/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/init.lua new file mode 100644 index 0000000..46cf353 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/init.lua @@ -0,0 +1,12 @@ +--[[ + Defines utilities for working with 'dictionary-like' tables. + + Dictionaries can be indexed by any value, but don't have the ordering + expectations that lists have. +]] + +return { + join = require(script.join), + keys = require(script.keys), + values = require(script.values), +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/init.spec.lua new file mode 100644 index 0000000..ff7e8e3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/init.spec.lua @@ -0,0 +1,5 @@ +return function() + it("should load", function() + require(script.Parent) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/join.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/join.lua new file mode 100644 index 0000000..2af8270 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/join.lua @@ -0,0 +1,30 @@ +local None = require(script.Parent.Parent.None) + +--[[ + Combine a number of dictionary-like tables into a new table. + + Keys specified in later tables will overwrite keys in previous tables. + + Use `Cryo.None` as a value to remove a key. This is necessary because + Lua does not distinguish between a value not being present in a table and a + value being `nil`. +]] +local function join(...) + local new = {} + + for i = 1, select("#", ...) do + local source = select(i, ...) + + for key, value in pairs(source) do + if value == None then + new[key] = nil + else + new[key] = value + end + end + end + + return new +end + +return join \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/join.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/join.spec.lua new file mode 100644 index 0000000..73eec64 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/join.spec.lua @@ -0,0 +1,94 @@ +return function() + local join = require(script.Parent.join) + local None = require(script.Parent.Parent.None) + + it("should return a new table", function() + local a = {} + + expect(join(a)).never.to.equal(a) + end) + + it("should merge tables, overwriting previous values", function() + local a = { + foo = "foo-a", + bar = "bar-a", + } + + local b = { + foo = "foo-b", + baz = "baz-b", + } + + local c = join(a, b) + + expect(c.foo).to.equal(b.foo) + expect(c.bar).to.equal(a.bar) + expect(c.baz).to.equal(b.baz) + end) + + it("should remove values set to None", function() + local a = { + foo = "foo-a", + } + + local b = { + foo = None, + } + + local c = join(a, b) + + expect(c.foo).to.equal(nil) + end) + + it("should not mutate passed in tables", function() + local mutationsA = 0 + local mutationsB = 0 + + local a = {} + local b = { + foo = "foo-b", + } + + setmetatable(a, { + __newindex = function() + mutationsA = mutationsA + 1 + end, + }) + + setmetatable(b, { + __newindex = function() + mutationsB = mutationsB + 1 + end, + }) + + join(a, b) + + expect(mutationsA).to.equal(0) + expect(mutationsB).to.equal(0) + expect(b.foo).to.equal("foo-b") + end) + + it("should accept arbitrary numbers of tables", function() + local a = { + foo = "foo-a", + } + + local b = { + bar = "bar-b", + } + + local c = { + baz = "baz-c", + } + + local d = join(a, b, c) + + expect(d.foo).to.equal(a.foo) + expect(d.bar).to.equal(b.bar) + expect(d.baz).to.equal(c.baz) + end) + + it("should accept zero tables", function() + expect(join()).to.be.a("table") + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/keys.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/keys.lua new file mode 100644 index 0000000..e2e4d1a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/keys.lua @@ -0,0 +1,16 @@ +--[[ + Returns a list of the keys from the given dictionary. +]] +local function keys(dictionary) + local new = {} + local index = 1 + + for key in pairs(dictionary) do + new[index] = key + index = index + 1 + end + + return new +end + +return keys \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/keys.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/keys.spec.lua new file mode 100644 index 0000000..a342a82 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/keys.spec.lua @@ -0,0 +1,74 @@ +return function() + local keys = require(script.Parent.keys) + local None = require(script.Parent.Parent.None) + + it("should not mutate the given table", function() + local a = { + Foo = "FooValue", + Bar = "BarValue" + } + local aCopy = { + Foo = "FooValue", + Bar = "BarValue" + } + + keys(a) + + for key, value in pairs(a) do + expect(aCopy[key]).to.equal(value) + end + for key, value in pairs(aCopy) do + expect(a[key]).to.equal(value) + end + end) + + it("should return the correct keys", function() + local a = { + Foo = "FooValue", + Bar = "BarValue", + Test = "TestValue" + } + local keyCount = { + Foo = 1, + Bar = 1, + Test = 1 + } + local b = keys(a) + + expect(#b).to.equal(3) + for _, key in ipairs(b) do + expect(keyCount[key]).never.to.equal(nil) + keyCount[key] = keyCount[key] - 1 + end + for _, count in pairs(keyCount) do + expect(count).to.equal(0) + end + end) + + it("should work with an empty table", function() + local a = keys({}) + + expect(next(a)).to.equal(nil) + end) + + it("should contain a None element if there is a None key in the dictionary", function() + local a = { + [None] = "Foo", + Bar = "BarValue" + } + local keyCount = { + [None] = 1, + Bar = 1 + } + local b = keys(a) + + expect(#b).to.equal(2) + for _, key in ipairs(b) do + expect(keyCount[key]).never.to.equal(nil) + keyCount[key] = keyCount[key] - 1 + end + for _, count in pairs(keyCount) do + expect(count).to.equal(0) + end + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/values.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/values.lua new file mode 100644 index 0000000..d97e49f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/values.lua @@ -0,0 +1,17 @@ +--[[ + Returns a list of the values of the given dictionary. +]] + +local function values(dictionary) + local new = {} + local index = 1 + + for _, value in pairs(dictionary) do + new[index] = value + index = index + 1 + end + + return new +end + +return values \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/values.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/values.spec.lua new file mode 100644 index 0000000..e87271d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/Dictionary/values.spec.lua @@ -0,0 +1,96 @@ +return function() + local values = require(script.Parent.values) + local None = require(script.Parent.Parent.None) + + it("should not mutate the given table", function() + local a = { + Foo = "FooValue", + Bar = "BarValue" + } + local aCopy = { + Foo = "FooValue", + Bar = "BarValue" + } + + values(a) + + for key, value in pairs(a) do + expect(aCopy[key]).to.equal(value) + end + for key, value in pairs(aCopy) do + expect(a[key]).to.equal(value) + end + end) + + it("should return the correct values", function() + local a = { + Foo = "FooValue", + Bar = "BarValue", + Test = "TestValue" + } + local valueCount = { + FooValue = 1, + BarValue = 1, + TestValue = 1 + } + local b = values(a) + + expect(#b).to.equal(3) + for _, value in ipairs(b) do + expect(valueCount[value]).never.to.equal(nil) + valueCount[value] = valueCount[value] - 1 + end + for _, count in pairs(valueCount) do + expect(count).to.equal(0) + end + end) + + it("should return duplicates if two values are the same", function() + local a = { + Foo = "FooValue", + Bar = "BarValue", + Test = "FooValue" + } + local valueCount = { + FooValue = 2, + BarValue = 1, + } + local b = values(a) + + expect(#b).to.equal(3) + for _, value in ipairs(b) do + expect(valueCount[value]).never.to.equal(nil) + valueCount[value] = valueCount[value] - 1 + end + for _, count in pairs(valueCount) do + expect(count).to.equal(0) + end + end) + + it("should work with an empty table", function() + local a = values({}) + + expect(next(a)).to.equal(nil) + end) + + it("should contain a None element if there is a None value in the dictionary", function() + local a = { + Foo = None, + Bar = "BarValue" + } + local valueCount = { + [None] = 1, + BarValue = 1 + } + local b = values(a) + + expect(#b).to.equal(2) + for _, value in ipairs(b) do + expect(valueCount[value]).never.to.equal(nil) + valueCount[value] = valueCount[value] - 1 + end + for _, count in pairs(valueCount) do + expect(count).to.equal(0) + end + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filter.lua new file mode 100644 index 0000000..637978b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filter.lua @@ -0,0 +1,20 @@ +--[[ + Create a copy of a list with only values for which `callback` returns true. + Calls the callback with (value, index). +]] +local function filter(list, callback) + local new = {} + local index = 1 + + for i = 1, #list do + local value = list[i] + if callback(value, i) then + new[index] = value + index = index + 1 + end + end + + return new +end + +return filter \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filter.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filter.spec.lua new file mode 100644 index 0000000..1f6346d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filter.spec.lua @@ -0,0 +1,77 @@ +return function() + local filter = require(script.Parent.filter) + + it("should call the callback for each element", function() + local a = { + "foo1", + "foo2", + "foo3" + } + local copy = {} + local function copyCallback(value, index) + copy[index] = value + return true + end + filter(a, copyCallback) + + for key, value in pairs(a) do + expect(copy[key]).to.equal(value) + end + + for key, value in pairs(copy) do + expect(value).to.equal(a[key]) + end + end) + + it("should correctly use the filter callback", function() + local a = {1, 2, 3, 4, 5} + local function evenOnly(value) + return value % 2 == 0 + end + local b = filter(a, evenOnly) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(2) + expect(b[2]).to.equal(4) + end) + + it("should copy the list correctly", function() + local a = {1, 2, 3} + local function keepAll() + return true + end + local b = filter(a, keepAll) + + expect(b).never.to.equal(a) + + for key, value in pairs(a) do + expect(b[key]).to.equal(value) + end + + for key, value in pairs(b) do + expect(value).to.equal(a[key]) + end + end) + + it("should work with an empty table", function() + local called = false + local function callback() + called = true + return true + end + local a = filter({}, callback) + + expect(#a).to.equal(0) + expect(called).to.equal(false) + end) + + it("should remove all element from a list when callback return always false", function() + local a = {6, 2, 8, 6, 7} + local function removeAll() + return false + end + local b = filter(a, removeAll) + + expect(#b).to.equal(0) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filterMap.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filterMap.lua new file mode 100644 index 0000000..494fd66 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filterMap.lua @@ -0,0 +1,23 @@ +--[[ + Create a copy of a list doing a combination filter and map. + + If callback returns nil for any item, it is considered filtered from the + list. Any other value is considered the result of the 'map' operation. +]] +local function filterMap(list, callback) + local new = {} + local index = 1 + + for i = 1, #list do + local result = callback(list[i], i) + + if result ~= nil then + new[index] = result + index = index + 1 + end + end + + return new +end + +return filterMap \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filterMap.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filterMap.spec.lua new file mode 100644 index 0000000..a77f2c5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/filterMap.spec.lua @@ -0,0 +1,92 @@ +return function() + local filterMap = require(script.Parent.filterMap) + + it("should return a new table", function() + local a = {1, 2, 3} + local function callback() + return 1 + end + local b = filterMap(a, callback) + + expect(b).never.to.equal(a) + end) + + it("should call the callback for each element", function() + local a = { + "foo1", + "foo2", + "foo3" + } + local copy = {} + local function callback(value, index) + copy[index] = value + return value + end + filterMap(a, callback) + + for key, value in pairs(a) do + expect(copy[key]).to.equal(value) + end + + for key, value in pairs(copy) do + expect(value).to.equal(a[key]) + end + end) + + it("should correctly use the filter callback", function() + local a = {1, 2, 3, 4, 5} + local function doubleOddOnly(value) + if value % 2 == 0 then + return nil + else + return value * 2 + end + end + local b = filterMap(a, doubleOddOnly) + + expect(#b).to.equal(3) + expect(b[1]).to.equal(2) + expect(b[2]).to.equal(6) + expect(b[3]).to.equal(10) + end) + + it("should copy the list correctly", function() + local a = {1, 2, 3} + local function copyCallback(value) + return value + end + local b = filterMap(a, copyCallback) + + expect(b).never.to.equal(a) + + for key, value in pairs(a) do + expect(b[key]).to.equal(value) + end + + for key, value in pairs(b) do + expect(value).to.equal(a[key]) + end + end) + + it("should work with an empty table", function() + local called = false + local function callback() + called = true + return true + end + local a = filterMap({}, callback) + + expect(#a).to.equal(0) + expect(called).to.equal(false) + end) + + it("should remove all elements from a list when callback return always nil", function() + local a = {6, 2, 8, 6, 7} + local function removeAll() + return nil + end + local b = filterMap(a, removeAll) + + expect(#b).to.equal(0) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/find.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/find.lua new file mode 100644 index 0000000..d2fa161 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/find.lua @@ -0,0 +1,14 @@ +--[[ + Returns the index of the first value found or nil if not found. +]] + +local function find(list, value) + for i = 1, #list do + if list[i] == value then + return i + end + end + return nil +end + +return find \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/find.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/find.spec.lua new file mode 100644 index 0000000..c7ca169 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/find.spec.lua @@ -0,0 +1,30 @@ +return function() + local find = require(script.Parent.find) + + it("should return the correct index", function() + local a = {5, 4, 3, 2, 1} + + expect(find(a, 1)).to.equal(5) + expect(find(a, 2)).to.equal(4) + expect(find(a, 3)).to.equal(3) + expect(find(a, 4)).to.equal(2) + expect(find(a, 5)).to.equal(1) + end) + + it("should work with an empty table", function() + expect(find({}, 1)).to.equal(nil) + end) + + it("should return nil when the given value is not found", function() + local a = {1, 2, 3} + + expect(find(a, 4)).to.equal(nil) + expect(type(find(a, 4))).to.equal("nil") + end) + + it("should return the index of the first value found", function() + local list = {1, 2, 2} + + expect(find(list, 2)).to.equal(2) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/findWhere.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/findWhere.lua new file mode 100644 index 0000000..dfac37a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/findWhere.lua @@ -0,0 +1,14 @@ +--[[ + Returns the index of the first value for which predicate(value, index) is truthy, or nil if not found. +]] + +local function findWhere(list, predicate) + for i = 1, #list do + if predicate(list[i], i) then + return i + end + end + return nil +end + +return findWhere \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/findWhere.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/findWhere.spec.lua new file mode 100644 index 0000000..9a2c937 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/findWhere.spec.lua @@ -0,0 +1,63 @@ +return function() + local findWhere = require(script.Parent.findWhere) + + it("should return the correct index", function() + local numbers = { 1, 5, 10, 7 } + local isEven = function(value) + return value % 2 == 0 + end + + local isOdd = function(value) + return value % 2 == 1 + end + + expect(findWhere(numbers, isEven)).to.equal(3) + expect(findWhere(numbers, isOdd)).to.equal(1) + end) + + it("should work with an empty table", function() + local anything = function() + return true + end + expect(findWhere({}, anything)).to.equal(nil) + end) + + it("should return nil when the when no value satisfies the predicate", function() + local numbers = { 1, 2, 3 } + local isFour = function(value) + return value == 4 + end + + expect(findWhere(numbers, isFour)).to.equal(nil) + end) + + it("should return the index of the first value for which the predicate is true", function() + local list = { 1, 1, 1, 2, 2 } + + local isTwo = function(value) + return value == 2 + end + + expect(findWhere(list, isTwo)).to.equal(4) + end) + + it("should allow access to table index in the predicate function", function() + local list = { 5, 4, 3, 2, 1 } + + local isIndexFour = function(_, index) + return index == 4 + end + + expect(findWhere(list, isIndexFour)).to.equal(4) + end) + + it("should allow access to both value and index in the predicate function", function() + local list = { 1, 1, 2, 2, 1 } + + local sumValueAndIndexToFive = function(value, index) + return value + index == 5 + end + + expect(findWhere(list, sumValueAndIndexToFive)).to.equal(3) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldLeft.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldLeft.lua new file mode 100644 index 0000000..9b254e4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldLeft.lua @@ -0,0 +1,14 @@ +--[[ + Performs a left-fold of the list with the given initial value and callback. +]] +local function foldLeft(list, callback, initialValue) + local accum = initialValue + + for i = 1, #list do + accum = callback(accum, list[i], i) + end + + return accum +end + +return foldLeft \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldLeft.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldLeft.spec.lua new file mode 100644 index 0000000..43ee185 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldLeft.spec.lua @@ -0,0 +1,61 @@ +return function() + local foldLeft = require(script.Parent.foldLeft) + + it("should call the callback", function() + local a = {1, 2, 3} + local called = 0 + + foldLeft(a, function() + called = called + 1 + end, 0) + + expect(called).to.equal(3) + end) + + it("should not call the callback when the list is empty", function() + local called = false + + foldLeft({}, function() + called = true + end, 0) + + expect(called).to.equal(false) + end) + + it("should call the callback for each element", function() + local a = {4, 5, 6} + local copy = {} + + foldLeft(a, function(accum, value, index) + copy[index] = value + return accum + end, 0) + + expect(#copy).to.equal(#a) + + for key, value in pairs(a) do + expect(value).to.equal(copy[key]) + end + end) + + it("should pass the same modified initial value to the callback", function() + local a = {5, 4, 3} + local initialValue = {} + + foldLeft(a, function(accum) + expect(accum).to.equal(initialValue) + return accum + end, initialValue) + end) + + it("should call the callback in the correct order", function() + local a = {5, 4, 3} + local index = 1 + + foldLeft(a, function(accum, value) + expect(value).to.equal(a[index]) + index = index + 1 + return accum + end, 0) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldRight.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldRight.lua new file mode 100644 index 0000000..981504c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldRight.lua @@ -0,0 +1,14 @@ +--[[ + Performs a right-fold of the list with the given initial value and callback. +]] +local function foldRight(list, callback, initialValue) + local accum = initialValue + + for i = #list, 1, -1 do + accum = callback(accum, list[i], i) + end + + return accum +end + +return foldRight \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldRight.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldRight.spec.lua new file mode 100644 index 0000000..25ebc2b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/foldRight.spec.lua @@ -0,0 +1,61 @@ +return function() + local foldRight = require(script.Parent.foldRight) + + it("should call the callback", function() + local a = {1, 2, 3} + local called = 0 + + foldRight(a, function() + called = called + 1 + end, 0) + + expect(called).to.equal(3) + end) + + it("should not call the callback when the list is empty", function() + local called = false + + foldRight({}, function() + called = true + end, 0) + + expect(called).to.equal(false) + end) + + it("should call the callback for each element", function() + local a = {4, 5, 6} + local copy = {} + + foldRight(a, function(accum, value, index) + copy[index] = value + return accum + end, 0) + + expect(#copy).to.equal(#a) + + for key, value in pairs(a) do + expect(value).to.equal(copy[key]) + end + end) + + it("should pass the same modified initial value to the callback", function() + local a = {5, 4, 3} + local initialValue = {} + + foldRight(a, function(accum) + expect(accum).to.equal(initialValue) + return accum + end, initialValue) + end) + + it("should call the callback in the correct order", function() + local a = {5, 4, 3} + local index = 3 + + foldRight(a, function(accum, value) + expect(value).to.equal(a[index]) + index = index - 1 + return accum + end, 0) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/getRange.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/getRange.lua new file mode 100644 index 0000000..8df4949 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/getRange.lua @@ -0,0 +1,19 @@ +--[[ + Returns a new list containing only the elements within the given range. +]] + +local function getRange(list, startIndex, endIndex) + assert(startIndex <= endIndex, "startIndex must be less than or equal to endIndex") + + local new = {} + local index = 1 + + for i = math.max(1, startIndex), math.min(#list, endIndex) do + new[index] = list[i] + index = index + 1 + end + + return new +end + +return getRange \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/getRange.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/getRange.spec.lua new file mode 100644 index 0000000..50f5641 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/getRange.spec.lua @@ -0,0 +1,62 @@ +return function() + local getRange = require(script.Parent.getRange) + + it("should return the correct range", function() + local a = {1, 2, 3, 4} + local b = getRange(a, 2, 3) + + expect(b[1]).to.equal(2) + expect(b[2]).to.equal(3) + expect(#b).to.equal(2) + + local c = getRange(a, 4, 4) + expect(#c).to.equal(1) + expect(c[1]).to.equal(4) + end) + + it("should throw when the start index is higher than the end index", function() + local a = {5, 8, 7, 2, 3, 7} + + expect(function() + getRange(a, 4, 1) + end).to.throw() + end) + + it("should copy the table", function() + local a = {6, 8, 1, 3, 7, 2} + local b = getRange(a, 1, #a) + + for key, value in pairs(a) do + expect(b[key]).to.equal(value) + end + + for key, value in pairs(b) do + expect(value).to.equal(a[key]) + end + end) + + it("should work with an empty table", function() + local a = getRange({}, 1, 5) + + expect(a).to.be.a("table") + expect(#a).to.equal(0) + end) + + it("should work when the start index is smaller that 1", function() + local a = {1, 2, 3, 4} + local b = getRange(a, -2, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + end) + + it("should work when the end index is larger that the list length", function() + local a = {1, 2, 3, 4} + local b = getRange(a, 3, 18) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(3) + expect(b[2]).to.equal(4) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/init.lua new file mode 100644 index 0000000..c0022c4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/init.lua @@ -0,0 +1,22 @@ +--[[ + Defines utilities for working with 'list-like' tables. +]] + +return { + filter = require(script.filter), + filterMap = require(script.filterMap), + find = require(script.find), + findWhere = require(script.findWhere), + foldLeft = require(script.foldLeft), + foldRight = require(script.foldRight), + getRange = require(script.getRange), + join = require(script.join), + map = require(script.map), + removeIndex = require(script.removeIndex), + removeRange = require(script.removeRange), + removeValue = require(script.removeValue), + replaceIndex = require(script.replaceIndex), + reverse = require(script.reverse), + sort = require(script.sort), + toSet = require(script.toSet), +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/init.spec.lua new file mode 100644 index 0000000..ff7e8e3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/init.spec.lua @@ -0,0 +1,5 @@ +return function() + it("should load", function() + require(script.Parent) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/join.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/join.lua new file mode 100644 index 0000000..496a26f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/join.lua @@ -0,0 +1,25 @@ +local None = require(script.Parent.Parent.None) + +--[[ + Joins any number of lists together into a new list +]] +local function join(...) + local new = {} + + for listKey = 1, select("#", ...) do + local list = select(listKey, ...) + local len = #new + + for itemKey = 1, #list do + if list[itemKey] == None then + len = len - 1 + else + new[len + itemKey] = list[itemKey] + end + end + end + + return new +end + +return join \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/join.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/join.spec.lua new file mode 100644 index 0000000..49dfaaa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/join.spec.lua @@ -0,0 +1,44 @@ +return function() + local join = require(script.Parent.join) + local None = require(script.Parent.Parent.None) + + it("should return a new table", function() + local a = {} + + expect(join(a)).never.to.equal(a) + end) + + it("should remove elements equal to None", function() + local a = { + "foo-a" + } + + local b = { + None, + "foo-b" + } + + local c = join(a, b) + + expect(c[1]).to.equal("foo-a") + expect(c[2]).to.equal("foo-b") + expect(c[3]).to.equal(nil) + end) + + it("should accept arbitrary numbers of tables", function() + local a = {1} + local b = {2} + local c = {3} + + local d = join(a, b, c) + + expect(#d).to.equal(3) + expect(d[1]).to.equal(1) + expect(d[2]).to.equal(2) + expect(d[3]).to.equal(3) + end) + + it("should accept zero tables", function() + expect(join()).to.be.a("table") + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/map.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/map.lua new file mode 100644 index 0000000..ef74e96 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/map.lua @@ -0,0 +1,14 @@ +--[[ + Create a copy of a list where each value is transformed by `callback` +]] +local function map(list, callback) + local new = {} + + for i = 1, #list do + new[i] = callback(list[i], i) + end + + return new +end + +return map \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/map.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/map.spec.lua new file mode 100644 index 0000000..eec73c3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/map.spec.lua @@ -0,0 +1,61 @@ +return function() + local map = require(script.Parent.map) + + it("should return a new table", function() + local a = {1, 2, 3} + + expect(map(a, function() end)).never.to.equal(a) + end) + + it("should call the callback for each element", function() + local a = {5, 6, 7} + local copy = {} + map(a, function(value, index) + copy[index] = value + return value + end) + + for key, value in pairs(a) do + expect(copy[key]).to.equal(value) + end + + for key, value in pairs(copy) do + expect(value).to.equal(a[key]) + end + end) + + it("should copy list", function() + local a = {1, 2, 3} + local b = map(a, function(value) + return value + end) + + for key, value in pairs(a) do + expect(b[key]).to.equal(value) + end + + for key, value in pairs(b) do + expect(value).to.equal(a[key]) + end + end) + + it("should sets the new values to the result of the given callback", function() + local a = {5, 6, 7} + local b = map(a, function(value) + return value * 2 + end) + + expect(#b).to.equal(#a) + for i = 1, #a do + expect(b[i]).to.equal(a[i] * 2) + end + end) + + it("should work with an empty list", function() + local a = {} + local b = map(a, function() end) + + expect(b).to.be.a("table") + expect(b).never.to.equal(a) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeIndex.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeIndex.lua new file mode 100644 index 0000000..b7bce5b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeIndex.lua @@ -0,0 +1,19 @@ +--[[ + Remove the element at the given index. +]] +local function removeIndex(list, index) + local new = {} + local removed = 0 + + for i = 1, #list do + if i == index then + removed = 1 + else + new[i - removed] = list[i] + end + end + + return new +end + +return removeIndex \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeIndex.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeIndex.spec.lua new file mode 100644 index 0000000..2f4c975 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeIndex.spec.lua @@ -0,0 +1,53 @@ +return function() + local removeIndex = require(script.Parent.removeIndex) + local None = require(script.Parent.Parent.None) + + it("should remove the element at the given index", function() + local a = { + "first", + "second", + "third" + } + + local b = removeIndex(a, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal("first") + expect(b[2]).to.equal("third") + end) + + it("should not remove any element if index is out of bound", function() + local a = { + "first", + "second", + "third" + } + local b = removeIndex(a, 4) + + expect(#b).to.equal(#a) + for i = 1, #a do + expect(b[i]).to.equal(a[i]) + end + + local c = removeIndex(a, -2) + + expect(#c).to.equal(#a) + for i = 1, #a do + expect(c[i]).to.equal(a[i]) + end + end) + + it("should work with a None element", function() + local a = { + "first", + None, + "third" + } + + local b = removeIndex(a, 1) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(None) + expect(b[2]).to.equal("third") + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeRange.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeRange.lua new file mode 100644 index 0000000..a4e4d2f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeRange.lua @@ -0,0 +1,23 @@ +--[[ + Remove the range from the list starting from the index. +]] +local function removeRange(list, startIndex, endIndex) + assert(startIndex <= endIndex, "startIndex must be less than or equal to endIndex") + + local new = {} + local index = 1 + + for i = 1, math.min(#list, startIndex - 1) do + new[index] = list[i] + index = index + 1 + end + + for i = endIndex + 1, #list do + new[index] = list[i] + index = index + 1 + end + + return new +end + +return removeRange \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeRange.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeRange.spec.lua new file mode 100644 index 0000000..73ffbf7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeRange.spec.lua @@ -0,0 +1,75 @@ +return function() + local removeRange = require(script.Parent.removeRange) + local None = require(script.Parent.Parent.None) + + it("should remove elements properly", function() + local a = {1, 2, 3} + local b = removeRange(a, 2, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + + local c = {1, 2, 3, 4, 5, 6} + local d = removeRange(c, 1, 4) + + expect(#d).to.equal(2) + expect(d[1]).to.equal(5) + expect(d[2]).to.equal(6) + + local e = removeRange(c, 2, 5) + + expect(#e).to.equal(2) + expect(e[1]).to.equal(1) + expect(e[2]).to.equal(6) + end) + + it("should throw when the start index is higher than the end index", function() + local a = {1, 2, 3} + + expect(function() + removeRange(a, 2, 0) + end).to.throw() + + expect(function() + removeRange(a, 1, -1) + end).to.throw() + end) + + it("should copy the table when then indexes are higher than the list length", function() + local a = {1, 2, 3} + local b = removeRange(a, 4, 7) + + expect(#b).to.equal(3) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + end) + + it("should work when the start index is smaller than 1", function() + local a = {1, 2, 3, 4} + local b = removeRange(a, -5, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(3) + expect(b[2]).to.equal(4) + end) + + it("should work when the end index is greater than the list length", function() + local a = {1, 2, 3, 4} + local b = removeRange(a, 3, 8) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + end) + + it("should work with a None element", function() + local a = {1, None, 3} + local b = removeRange(a, 1, 1) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(None) + expect(b[2]).to.equal(3) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeValue.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeValue.lua new file mode 100644 index 0000000..4930db0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeValue.lua @@ -0,0 +1,18 @@ +--[[ + Creates a new list that has no occurrences of the given value. +]] +local function removeValue(list, value) + local new = {} + local index = 1 + + for i = 1, #list do + if list[i] ~= value then + new[index] = list[i] + index = index + 1 + end + end + + return new +end + +return removeValue \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeValue.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeValue.spec.lua new file mode 100644 index 0000000..705c2ba --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/removeValue.spec.lua @@ -0,0 +1,43 @@ +return function() + local removeValue = require(script.Parent.removeValue) + local None = require(script.Parent.Parent.None) + + it("should remove the given value", function() + local a = {1, 4, 3} + local b = removeValue(a, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + end) + + it("should remove all occurences of the same given value", function() + local a = {1, 2, 2, 3} + local b = removeValue(a, 2) + + expect(#b).to.equal(2) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + end) + + it("should work with an empty list", function() + local a = removeValue({}, 1) + + expect(a).to.be.a("table") + expect(#a).to.equal(0) + end) + + it("should work with a None element", function() + local a = {1, 2, None, 3} + local b = removeValue(a, 2) + + expect(#b).to.equal(3) + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(None) + expect(b[3]).to.equal(3) + + local c = removeValue(a, None) + + expect(c[3]).to.equal(3) + expect(#c).to.equal(3) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/replaceIndex.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/replaceIndex.lua new file mode 100644 index 0000000..6a010f0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/replaceIndex.lua @@ -0,0 +1,22 @@ +--[[ + Returns a new list with the new value replaced at the given index. +]] + +local function replaceIndex(list, index, value) + local new = {} + local len = #list + + assert(index <= len, "index must be less or equal than the list length") + + for i = 1, len do + if i == index then + new[i] = value + else + new[i] = list[i] + end + end + + return new +end + +return replaceIndex \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/replaceIndex.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/replaceIndex.spec.lua new file mode 100644 index 0000000..8e30e53 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/replaceIndex.spec.lua @@ -0,0 +1,52 @@ +return function() + local replaceIndex = require(script.Parent.replaceIndex) + + it("should return a new table", function() + local list = {1, 2, 3} + + expect(replaceIndex(list, 2, 0)).never.to.equal(list) + end) + + it("should not mutate the original list", function() + local list = {false, "foo", 3} + local value = {} + replaceIndex(list, 2, value) + + expect(#list).to.equal(3) + expect(list[1]).to.equal(false) + expect(list[2]).to.equal("foo") + expect(list[3]).to.equal(3) + end) + + it("should replace the value at the given index", function() + local list = {1, 2, 3} + local value = {} + local result = replaceIndex(list, 2, value) + + expect(result[1]).to.equal(1) + expect(result[2]).to.equal(value) + expect(result[3]).to.equal(3) + expect(next(result[2])).to.equal(nil) + end) + + it("should throw if the given index is higher than the list length", function() + local list = {1} + + expect(function() + replaceIndex(list, #list + 1, {}) + end).to.throw() + end) + + it("should be able to replace to a falsy value", function() + local tableElement = {} + local list = {tableElement, false, "value", true} + local newValue = false + + local result = replaceIndex(list, 3, newValue) + + expect(result[1]).to.equal(tableElement) + expect(result[2]).to.equal(false) + expect(result[3]).to.equal(newValue) + expect(result[4]).to.equal(true) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/reverse.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/reverse.lua new file mode 100644 index 0000000..8c24fad --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/reverse.lua @@ -0,0 +1,17 @@ +--[[ + Returns a new list with the reversed order of the given list +]] + +local function reverse(list) + local new = {} + local len = #list + local top = len + 1 + + for i = 1, len do + new[i] = list[top - i] + end + + return new +end + +return reverse \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/reverse.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/reverse.spec.lua new file mode 100644 index 0000000..147ce1d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/reverse.spec.lua @@ -0,0 +1,46 @@ +return function() + local reverse = require(script.Parent.reverse) + + it("should return a new table", function() + local a = {1, 2, 3} + + expect(reverse(a)).never.to.equal(a) + end) + + it("should not mutate the given table", function() + local a = {1, 2, 3} + reverse(a) + + expect(#a).to.equal(3) + expect(a[1]).to.equal(1) + expect(a[2]).to.equal(2) + expect(a[3]).to.equal(3) + end) + + it("should contain the same elements", function() + local a = { + "Foo", + "Bar" + } + local aSet = { + Foo = true, + Bar = true + } + local b = reverse(a) + + expect(#b).to.equal(2) + for _, value in ipairs(b) do + expect(aSet[value]).to.equal(true) + end + end) + + it("should reverse the list", function() + local a = {1, 2, 3, 4} + local b = reverse(a) + + expect(b[1]).to.equal(4) + expect(b[2]).to.equal(3) + expect(b[3]).to.equal(2) + expect(b[4]).to.equal(1) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/sort.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/sort.lua new file mode 100644 index 0000000..a8fc8fd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/sort.lua @@ -0,0 +1,18 @@ +--[[ + Returns a new list, ordered with the given sort callback. + If no callback is given, the default table.sort will be used. +]] + +local function sort(list, callback) + local new = {} + + for i = 1, #list do + new[i] = list[i] + end + + table.sort(new, callback) + + return new +end + +return sort \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/sort.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/sort.spec.lua new file mode 100644 index 0000000..b5c9d1e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/sort.spec.lua @@ -0,0 +1,74 @@ +return function() + local sort = require(script.Parent.sort) + + it("should return a new table", function() + local a = {} + + expect(sort(a)).never.to.equal(a) + end) + + it("should not mutate the given table", function() + local a = {77, "foo", 2} + local function order(first, second) + return tostring(first) < tostring(second) + end + sort(a, order) + + expect(#a).to.equal(3) + expect(a[1]).to.equal(77) + expect(a[2]).to.equal("foo") + expect(a[3]).to.equal(2) + end) + + it("should contain the same elements from the given table", function() + local a = { + "Foo", + "Bar", + "Test" + } + local elementSet = { + Foo = true, + Bar = true, + Test = true + } + local b = sort(a) + + expect(#b).to.equal(3) + for _, value in ipairs(b) do + expect(elementSet[value]).to.equal(true) + end + end) + + it("should sort with the default table.sort when no callback is given", function() + local a = {4, 2, 5, 3, 1} + local b = sort(a) + + table.sort(a) + + expect(#b).to.equal(#a) + for i = 1, #a do + expect(b[i]).to.equal(a[i]) + end + end) + + it("should sort with the given callback", function() + local a = {1, 2, 5, 3, 4} + local function order(first, second) + return first > second + end + local b = sort(a, order) + + table.sort(a, order) + + expect(#b).to.equal(#a) + for i = 1, #a do + expect(b[i]).to.equal(a[i]) + end + end) + + it("should work with an empty table", function() + local a = sort({}) + + expect(#a).to.equal(0) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/toSet.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/toSet.lua new file mode 100644 index 0000000..692dbc9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/toSet.lua @@ -0,0 +1,16 @@ +--[[ + Create a dictionary where each value in the given list corresponds to a key + in the dictionary with a value of true +]] + +local function toSet(list) + local new = {} + + for i = 1, #list do + new[list[i]] = true + end + + return new +end + +return toSet \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/toSet.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/toSet.spec.lua new file mode 100644 index 0000000..f9f06f8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/List/toSet.spec.lua @@ -0,0 +1,42 @@ +return function() + local toSet = require(script.Parent.toSet) + + it("should return a new table", function() + local a = {1, 2, 3} + + expect(toSet(a)).never.to.equal(a) + end) + + it("should not mutate the given table", function() + local a = {"a", "b", "c"} + toSet(a) + + for k, v in pairs(a) do + if k == 1 then + expect(v).to.equal("a") + elseif k == 2 then + expect(v).to.equal("b") + elseif k == 3 then + expect(v).to.equal("c") + else + error("Extra key was added to table a") + end + end + end) + + it("should have every value in a as a key mapped to true in b", function() + local a = {1, 2, 3, "a", "b", "c"} + local b = toSet(a) + + expect(#b).to.equal(3) + expect(b[1]).to.equal(true) + expect(b[2]).to.equal(true) + expect(b[3]).to.equal(true) + + expect(b[4]).to.equal(nil) + + expect(b["a"]).to.equal(true) + expect(b["b"]).to.equal(true) + expect(b["c"]).to.equal(true) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/None.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/None.lua new file mode 100644 index 0000000..3fc61bd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/None.lua @@ -0,0 +1,15 @@ +--[[ + Represents a value that is intentionally present, but should be interpreted + as `nil`. + + Cryo.None is used by included utilities to make removing values more + ergonomic. +]] + +local None = newproxy(true) + +getmetatable(None).__tostring = function() + return "Cryo.None" +end + +return None \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/None.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/None.spec.lua new file mode 100644 index 0000000..33907f5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/None.spec.lua @@ -0,0 +1,14 @@ +return function() + local None = require(script.Parent.None) + + it("should be a userdata", function() + expect(None).to.be.a("userdata") + end) + + it("should have a nice string name", function() + local coerced = tostring(None) + + expect(coerced:find("^userdata: ")).never.to.be.ok() + expect(coerced:find("None")).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/init.lua new file mode 100644 index 0000000..e952302 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/init.lua @@ -0,0 +1,6 @@ +return { + Dictionary = require(script.Dictionary), + List = require(script.List), + isEmpty = require(script.isEmpty), + None = require(script.None), +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/init.spec.lua new file mode 100644 index 0000000..ff7e8e3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/init.spec.lua @@ -0,0 +1,5 @@ +return function() + it("should load", function() + require(script.Parent) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/isEmpty.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/isEmpty.lua new file mode 100644 index 0000000..d41de09 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/cryo/isEmpty.lua @@ -0,0 +1,5 @@ +local function isEmpty(object) + return next(object) == nil +end + +return isEmpty \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/lock.toml new file mode 100644 index 0000000..97c383c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_cryo/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/cryo" +version = "1.0.0" +commit = "272caa8f3f3b3b29296b462f80b65cc9b1c92f1e" +source = "url+https://github.com/roblox/cryo" diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/Error.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/Error.lua new file mode 100644 index 0000000..1bbe6b1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/Error.lua @@ -0,0 +1,63 @@ +--[[ + Create an error object with a specified name and message. + + In native Lua, errors can only be string values. At Roblox, we can take advantage of throwing + error objects to provide structured information about problems that occur. + + The tags table stores serializable information about an error which can be provided when it is + thrown, and later passed to a logging endpoint. + + Throwing an error instance captures its stack trace, avoiding the need to explicitly use xpcall. + + @usage In general, errors should not be used during normal control flow. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local class = require(Dash.class) +local format = require(Dash.format) +local join = require(Dash.join) + +--[[ + Create a new Error instance. + @param name The name of the error + @param string A message for the error which will be formatted using Dash.format + @param tags Any fixed tags +]] +local Error = class("Error", function(name: string, message: string, tags: Types.Table?) + return { + name = name, + message = message or "An error occurred", + tags = tags or {} + } +end) + +function Error:toString(): string + return format("{}: {}\n{}", self.name, format(self.message, self.tags), self.stack) +end + +--[[ + Return a new error instance containing the tags provided joined to any existing tags of the + current error instance. +]] +function Error:joinTags(tags: Types.Table?): Error + return Error.new(self.name, self.message, join(self.tags, tags)) +end + +--[[ + Throw an error. + + The stack of the error is captured and stored. + + If additional tags are provided, a new error is created with the joined tags of + this instance. +]] +function Error:throw(tags: Types.Table?) + local instance = self:joinTags(tags) + instance.stack = debug.traceback() + error(instance) +end + +-- TODO Luau: Define class types automatically +export type Error = typeof(Error.new("", "")) + +return Error \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/None.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/None.lua new file mode 100644 index 0000000..9287ec9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/None.lua @@ -0,0 +1,13 @@ +--[[ + A symbol representing nothing, that can be used in place of nil as a key or value of a table, + where nil is illegal. + + Utility functions can check for the None symbol and treat it like a nil value. + + @usage Use cases include: + 1. Creating an ordered list with undefined values in it + 2. Creating a map with a key pointing to a nil value +]] +local Symbol = require(script.Parent.Symbol) +local None = Symbol.new("None") +return None \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/Symbol.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/Symbol.lua new file mode 100644 index 0000000..661fd88 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/Symbol.lua @@ -0,0 +1,32 @@ +--[[ + Create a symbol with a specified name. Upper snake-case is recommended as the symbol is a + constant, unless you are linking the symbol conceptually to a different string. + + Symbols are useful when you want a value that isn't equal to any other type, for example if you + want to store a unique property on an object that won't be accidentally accessed with a simple + string lookup. + + @example + local CHEESE = Symbol.new("CHEESE") + local FAKE_CHEESE = Symbol.new("CHEESE") + print(CHEESE == CHEESE) --> true + print(CHEESE == FAKE_CHEESE) --> false + print(tostring(CHEESE)) --> "Symbol.new(CHEESE)" +]] +local Dash = script.Parent +local class = require(Dash.class) + +local Symbol = class("Symbol", function(name: string) + return { + name = name + } +end) + +function Symbol:toString(): string + return ("Symbol(%s)"):format(self.name) +end + +-- TODO Luau: Define class types automatically +export type Symbol = typeof(Symbol.new("")) + +return Symbol \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/Types.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/Types.lua new file mode 100644 index 0000000..3f3e995 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/Types.lua @@ -0,0 +1,19 @@ +-- TODO Luau: Support these globally +-- A table with values of type _Value_ and numeric keys 1..n with no gaps +export type Array = {[number]: Value} +-- A table with values of type _Value_ and numeric keys, possibly with gaps +export type Args = {[number]: Value} +-- A table with keys of type _Key_ and values of type _Value_ +export type Map = {[Key]: Value} +-- A table with keys of a fixed type _Key_ and a boolean value representing membership of the set (default is false) +export type Set = {[Key]: boolean} +-- A table of any type +export type Table = {[any]: any} +-- A class has a constructor returning an instance of _Object_ type +export type Class = { + new: () -> Object +} +-- Represents a function which takes any arguments and returns any value +export type AnyFunction = () -> any + +return {} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/append.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/append.lua new file mode 100644 index 0000000..b887ad3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/append.lua @@ -0,0 +1,31 @@ +--[[ + Adds new elements to the _target_ Array from subsequent Array arguments in left-to-right order. + + Arguments which are `nil` or None are skipped. + + @mutable target +]] + +local Dash = script.Parent +local None = require(Dash.None) +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) +local forEachArgs = require(Dash.forEachArgs) +local forEach = require(Dash.forEach) +local insert = table.insert + +-- TODO Luau: Add varags typings +local function append(target: Types.Array, ...): Types.Array + assertEqual(typeof(target), "table", [[Attempted to call Dash.append with argument #1 of type {left:?} not {right:?}]]) + forEachArgs(function(list: Types.Table?) + if list == None or list == nil then + return + end + forEach(list, function(value: any) + insert(target, value) + end) + end, ...) + return target +end + +return append \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/assertEqual.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/assertEqual.lua new file mode 100644 index 0000000..7477ef6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/assertEqual.lua @@ -0,0 +1,23 @@ +--[[ + Performs a simple equality check and throws an error if _left_ is not equal to _right_. + + The formatted error message can be customized, which by default provides a serialization of + both inputs using Dash.pretty. + + The `left` and `right` values are available to be referenced in the formatted message. +]] + +local Dash = script.Parent + +local function assertEqual(left: any, right: any, formattedErrorMessage: string?) + if left ~= right then + local Error = require(Dash.Error) + local TypeError = Error.new("AssertError", formattedErrorMessage or [[Left {left:?} does not equal right {right:?}]]) + TypeError:throw({ + left = left, + right = right + }) + end +end + +return assertEqual \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/assign.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/assign.lua new file mode 100644 index 0000000..378c1b4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/assign.lua @@ -0,0 +1,73 @@ +--[[ + Adds new values to _target_ from subsequent Table arguments in left-to-right order. + + The None symbol can be used to remove existing elements in the target. + + @param ... any number of other tables + @example + local characters = { + Frodo = { + name = "Frodo Baggins", + team = "blue" + }, + Boromir = { + score = 5 + } + } + local otherCharacters = { + Frodo = { + team = "red", + score = 10 + }, + Bilbo = { + team = "yellow", + }, + Boromir = { + score = {1, 2, 3} + } + } + local result = assign(characters, otherCharacters) + print(result) --> { + Frodo = { + team = "red", + score = 10 + }, + Bilbo = { + team = "yellow" + }, + Boromir = { + score = {1, 2, 3} + } + } +]] +local Dash = script.Parent +local None = require(Dash.None) +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) +local forEach = require(Dash.forEach) +local forEachArgs = require(Dash.forEachArgs) + + +-- TODO Luau: Support typing varargs +-- TODO Luau: Support function generics +local function assign(target: Types.Table, ...): Types.Table + assertEqual(typeof(target), "table", [[Attempted to call Dash.assign with argument #1 of type {left:?} not {right:?}]]) + -- Iterate through the varags in order + forEachArgs(function(input: Types.Table?) + -- Ignore items which are not defined + if input == nil or input == None then + return + end + -- Iterate through each key of the input and assign to target at the same key + forEach(input, function(value, key) + if value == None then + target[key] = nil + else + target[key] = value + end + end) + end, ...) + return target +end + +return assign \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/class.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/class.lua new file mode 100644 index 0000000..d81b124 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/class.lua @@ -0,0 +1,288 @@ +--[[ + Create a class called _name_ with the specified _constructor_. The constructor should return a + plain table which will be turned into an instance of _Class_ from a call to `Class.new(...)`. + + @example + -- Create a simple Vehicle class + local Vehicle = class("Vehicle", function(wheelCount: number) return + { + speed = 0, + wheelCount = wheelCount + } + end) + function Vehicle:drive(speed) + self.speed = speed + end + -- Create a car instance + local car = Vehicle.new(4) + car.wheelCount --> 4 + car.speed --> 0 + -- Drive the car + car:drive(10) + car.speed --> 10 + + @usage When using Dash classes, private fields should be prefixed with `_` to avoid accidental access. + @usage A private field should only be accessed by a method of the class itself, though Rodash + does not restrict this in code. + @usage Public fields are recommended when there is no complex access logic e.g. `position.x` +]] +local Dash = script.Parent +local Types = require(Dash.Types) + +local function throwNotImplemented(tags: Types.Table) + local Error = require(Dash.Error) + local NotImplemented = Error.new("NotImplemented", [[The method "{methodName}" is not implemented on the class "{className}"]]) + NotImplemented:throw(tags) +end + +export type Constructor = () -> Types.Table + +local function class(name: string, constructor: Constructor?) + constructor = constructor or function() + return {} + end + local Class = { + name = name + } + --[[ + Return a new instance of the class, passing any arguments to the specified constructor. + @example + local Car = class("Car", function(speed) + return { + speed = speed + } + end) + local car = Car.new(5) + pretty(car) --> 'Car {speed = 5}' + ]] + function Class.new(...) + local instance = constructor(...) + setmetatable( + instance, + { + __index = Class, + __tostring = Class.toString, + __eq = Class.equals, + __lt = Class.__lt, + __le = Class.__le, + __add = Class.__add, + __sub = Class.__sub, + __mul = Class.__mul, + __div = Class.__div, + __mod = Class.__mod + } + ) + instance.Class = Class + instance:_init(...) + return instance + end + --[[ + Run after the instance has been properly initialized, allowing methods on the instance to + be used. + @example + local Vehicle = dash.class("Vehicle", function(wheelCount) return + { + speed = 0, + wheelCount = wheelCount + } + end) + -- Let's define a static private function to generate a unique id for each vehicle. + function Vehicle._getNextId() + Vehicle._nextId = Vehicle._nextId + 1 + return Vehicle._nextId + end + Vehicle._nextId = 0 + -- A general purpose init function may call other helper methods + function Vehicle:_init() + self._id = self:_generateId() + end + -- Assign an id to the new instance + function Vehicle:_generateId() + return format("#{}: {} wheels", Vehicle._getNextId(), self.wheelCount) + end + -- Return the id if the instance is represented as a string + function Vehicle:toString() + return self._id + end + local car = Vehicle.new(4) + tostring(car) --> "#1: 4 wheels" + ]] + function Class:_init() + end + + --[[ + Returns `true` if _value_ is an instance of _Class_ or any sub-class. + @example + local Vehicle = dash.class("Vehicle", function(wheelCount) return + { + speed = 0, + wheelCount = wheelCount + } + end) + local Car = Vehicle:extend("Vehicle", function() + return Vehicle.constructor(4) + end) + local car = Car.new() + car.isInstance(Car) --> true + car.isInstance(Vehicle) --> true + car.isInstance(Bike) --> false + ]] + function Class.isInstance(value) + local ok, isInstance = pcall(function() + local metatable = getmetatable(value) + while metatable do + if metatable.__index == Class then + return true + end + metatable = getmetatable(metatable.__index) + end + return false + end) + return ok and isInstance + end + + --[[ + Create a subclass of _Class_ with a new _name_ that inherits the metatable of _Class_, + optionally overriding the _constructor_ and providing additional _decorators_. + The super-constructor can be accessed with `Class.constructor`. + Super methods can be accessed using `Class.methodName` and should be called with self. + @example + local Vehicle = dash.class("Vehicle", function(wheelCount) return + { + speed = 0, + wheelCount = wheelCount + } + end) + -- Let's define a static private function to generate a unique id for each vehicle. + function Vehicle._getNextId() + Vehicle._nextId = Vehicle._nextId + 1 + return Vehicle._nextId + end + Vehicle._nextId = 0 + -- A general purpose init function may call other helper methods + function Vehicle:_init() + self.id = self:_generateId() + end + -- Assign an id to the new instance + function Vehicle:_generateId() + return dash.format("#{}: {} wheels", Vehicle._getNextId(), self.wheelCount) + end + -- Let's make a Car class which has a special way to generate ids + local Car = Vehicle:extend("Vehicle", function() + return Vehicle.constructor(4) + end) + -- Uses the super method to generate a car-specific id + function Car:_generateId() + self.id = dash.format("Car {}", Vehicle._generateId(self)) + end + local car = Car.new() + car.id --> "Car #1: 4 wheels" + ]] + function Class:extend(name: string, constructor) + local SubClass = class(name, constructor or Class.new) + setmetatable(SubClass, {__index = self}) + return SubClass + end + + --[[ + Return a string representation of the instance. By default this is the _name_ field (or the + Class name if this is not defined), but the method can be overridden. + @example + local Car = class("Car", function(name) + return { + name = name + } + end) + + local car = Car.new() + car:toString() --> 'Car' + tostring(car) --> 'Car' + print("Hello " .. car) -->> Hello Car + local bob = Car.new("Bob") + bob:toString() --> 'Bob' + tostring(bob) --> 'Bob' + print("Hello " .. bob) -->> Hello Bob + @example + local NamedCar = class("NamedCar", function(name) + return { + name = name + } + end) + function NamedCar:toString() + return "Car called " .. self.name + end + local bob = NamedCar.new("Bob") + bob:toString() --> 'Car called Bob' + tostring(bob) --> 'Car called Bob' + print("Hello " .. bob) -->> Hello Car called Bob + ]] + function Class:toString() + return self.name + end + + --[[ + Returns `true` if `self` is considered equal to _other_. This replaces the `==` operator + on instances of this class, and can be overridden to provide a custom implementation. + ]] + function Class:equals(other) + return rawequal(self, other) + end + + --[[ + Returns `true` if `self` is considered less than _other_. This replaces the `<` operator + on instances of this class, and can be overridden to provide a custom implementation. + ]] + function Class:__lt(other) + throwNotImplemented({ + methodName = "__lt", + className = name + }) + end + + --[[ + Returns `true` if `self` is considered less than or equal to _other_. This replaces the + `<=` operator on instances of this class, and can be overridden to provide a custom + implementation. + ]] + function Class:__le(other) + throwNotImplemented({ + methodName = "__le", + className = name + }) + end + + function Class:__add() + throwNotImplemented({ + methodName = "__add", + className = name + }) + end + function Class:__sub() + throwNotImplemented({ + methodName = "__sub", + className = name + }) + end + function Class:__mul() + throwNotImplemented({ + methodName = "__mul", + className = name + }) + end + function Class:__div() + throwNotImplemented({ + methodName = "__div", + className = name + }) + end + function Class:__mod() + throwNotImplemented({ + methodName = "__mod", + className = name + }) + end + + return Class +end + +return class \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/collect.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/collect.lua new file mode 100644 index 0000000..41e4dc1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/collect.lua @@ -0,0 +1,28 @@ +--[[ + Collect returns a new Table derived from _input_ by iterating through its pairs and calling + the handler on each `(key, child)` tuple. + + The handler should return a new `(newKey, value)` tuple to be inserted into the returned Table, + or `nil` if no value should be added. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) +local iterator = require(Dash.iterator) + +-- TODO Luau: support generic function definitions +export type CollectHandler = () -> (any, any) + +local function collect(input: Types.Table, handler: CollectHandler): Types.Map + assertEqual(typeof(input), "table", [[Attempted to call Dash.collect with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(handler), "function", [[Attempted to call Dash.collect with argument #2 of type {left:?} not {right:?}]]) + local result = {} + for key, child in iterator(input) do + local outputKey, outputValue = handler(key, child) + if outputKey ~= nil then + result[outputKey] = outputValue + end + end + return result +end +return collect \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/collectArray.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/collectArray.lua new file mode 100644 index 0000000..4b9a9f7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/collectArray.lua @@ -0,0 +1,28 @@ +--[[ + Collect returns a new Array derived from _input_ by iterating through its pairs and calling + the handler on each `(key, child)` tuple. + + The handler should return a new value to be pushed onto the end of the result array, or `nil` + if no value should be added. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) +local iterator = require(Dash.iterator) + +local insert = table.insert + +-- TODO Luau: Support generic functions +local function collectArray(input: Types.Table, handler: Types.AnyFunction) + assertEqual(typeof(input), "table", [[Attempted to call Dash.collectArray with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(handler), "function", [[Attempted to call Dash.collectArray with argument #2 of type {left:?} not {right:?}]]) + local result = {} + for key, child in iterator(input) do + local outputValue = handler(key, child) + if outputValue ~= nil then + insert(result, outputValue) + end + end + return result +end +return collectArray \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/collectSet.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/collectSet.lua new file mode 100644 index 0000000..3afff91 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/collectSet.lua @@ -0,0 +1,29 @@ +--[[ + Build a set from the entries of the _input_ Table, calling _handler_ on each entry and using + the returned value as an element to add to the set. + + If _handler_ is not provided, values of `input` are used as elements. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) +local iterator = require(Dash.iterator) + +-- TODO Luau: Support generic functions +local function collectSet(input: Types.Table, handler: Types.AnyFunction?) + assertEqual(typeof(input), "table", [[Attempted to call Dash.collectSet with argument #1 of type {left:?} not {right:?}]]) + local result = {} + for key, child in iterator(input) do + local outputValue + if handler == nil then + outputValue = child + else + outputValue = handler(key, child) + end + if outputValue ~= nil then + result[outputValue] = true + end + end + return result +end +return collectSet \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/compose.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/compose.lua new file mode 100644 index 0000000..33087a1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/compose.lua @@ -0,0 +1,36 @@ +--[[ + Returns a function that calls the argument functions in left-right order on an input, passing + the return of the previous function as argument(s) to the next. + + @example + local function fry(item) + return "fried " .. item + end + local function cheesify(item) + return "cheesy " .. item + end + local prepare = compose(fry, cheesify) + prepare("nachos") --> "cheesy fried nachos" +]] +-- TODO Luau: Support generic functions +-- TODO Luau: Support varargs +--: ((...A -> ...A)[]) -> ...A -> A +local Dash = script.Parent +local identity = require(Dash.identity) + +local function compose(...) + local fnCount = select("#", ...) + if fnCount == 0 then + return identity + end + local fns = {...} + return function(...) + local result = {fns[1](...)} + for i = 2, fnCount do + result = {fns[i](unpack(result))} + end + return unpack(result) + end +end + +return compose \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/copy.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/copy.lua new file mode 100644 index 0000000..4f8f826 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/copy.lua @@ -0,0 +1,13 @@ +--[[ + Returns a shallow copy of the _input_ Table. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assign = require(Dash.assign) +local assertEqual = require(Dash.assertEqual) + +local function copy(input: Types.Table): Types.Table + assertEqual(typeof(input), "table", [[Attempted to call Dash.copy with argument #1 of type {left:?} not {right:?}]]) + return assign({}, input) +end +return copy \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/cycles.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/cycles.lua new file mode 100644 index 0000000..66be5bd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/cycles.lua @@ -0,0 +1,90 @@ +--[[ + Get information about the number of times references to the same table values appear in a data structure. + + Operates on cyclic structures, and returns a Cycles object for a given _value_ by walking it recursively. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local includes = require(Dash.includes) +local join = require(Dash.join) +local keys = require(Dash.keys) + +local sort = table.sort + +export type Cycles = { + -- A set of tables which were visited recursively + visited: Types.Set, + -- A map from table to unique index in visit order + refs: Types.Map, + -- The number to use for the next unique table visited + nextRef: number, + -- An array of keys which should not be visited + omit: Types.Array, +} + +local function getDefaultCycles(): Cycles + return { + visited = {}, + refs = {}, + nextRef = 1, + omit = {}, + } +end + +-- TODO Luau: Improve type inference to a point that this definition does not produce so many type errors +-- TYPED: local function cycles(value: any, depth: number?, initialCycles: Cycles?): Cycles +local function cycles(input: any, depth: number?, initialCycles: any): Cycles? + if depth == -1 then + return initialCycles + end + + if typeof(input) == "table" then + local childCycles = initialCycles or getDefaultCycles() + + if childCycles.visited[input] then + -- We have already visited the table, so check if it has a reference + if not childCycles.refs[input] then + -- If not, create one as it is present at least twice + childCycles.refs[input] = childCycles.nextRef + childCycles.nextRef += 1 + end + return nil + else + -- We haven't yet visited the table, so recurse + childCycles.visited[input] = true + -- Visit in order to preserve reference consistency + local inputKeys = keys(input) + sort(inputKeys, function(left, right) + if typeof(left) == "number" and typeof(right) == "number" then + return left < right + else + return tostring(left) < tostring(right) + end + end) + for _, key in ipairs(inputKeys) do + local value = input[key] + if includes(childCycles.omit, key) then + -- Don't visit omitted keys + continue + end + -- TODO Luau: support type narrowring with "and" + -- TYPED: cycles(key, depth and depth - 1 or nil, childCycles) + -- TYPED: cycles(value, depth and depth - 1 or nil, childCycles) + -- Recurse through both the keys and values of the table + if depth then + cycles(key, depth - 1, childCycles) + cycles(value, depth - 1, childCycles) + else + cycles(key, nil, childCycles) + cycles(value, nil, childCycles) + end + end + end + return childCycles + else + -- Non-tables do not have cycles + return nil + end +end + +return cycles \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/endsWith.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/endsWith.lua new file mode 100644 index 0000000..ff62ddd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/endsWith.lua @@ -0,0 +1,15 @@ +--[[ + Checks if _input_ ends with the string _suffix_. + @example endsWith("Fun Roblox Games", "Games") --> true + @example endsWith("Bad Roblox Memes", "Games") --> false +]] +local Dash = script.Parent +local assertEqual = require(Dash.assertEqual) + +local function endsWith(input: string, suffix: string) + assertEqual(typeof(input), "string", [[Attempted to call Dash.endsWith with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(suffix), "string", [[Attempted to call Dash.endsWith with argument #2 of type {left:?} not {right:?}]]) + return input:sub(-suffix:len()) == suffix +end + +return endsWith \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/filter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/filter.lua new file mode 100644 index 0000000..73b7ecb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/filter.lua @@ -0,0 +1,27 @@ +--[[ + Filter the _input_ Table by calling the handler on each `(child, index)` tuple. + + For an Array input, the order of elements is prevered in the output. + + The handler should return truthy to preserve the value in the resulting Table. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) +local iterator = require(Dash.iterator) + +-- TODO Luau: support generic function definitions +export type FilterHandler = (any, any) -> boolean + +local function filter(input: Types.Table, handler: FilterHandler): Types.Array + assertEqual(typeof(input), "table", [[Attempted to call Dash.filter with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(handler), "function", [[Attempted to call Dash.filter with argument #2 of type {left:?} not {right:?}]]) + local result = {} + for index, child in iterator(input) do + if handler(child, index) then + table.insert(result, child) + end + end + return result +end +return filter \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/find.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/find.lua new file mode 100644 index 0000000..1d14cf4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/find.lua @@ -0,0 +1,28 @@ +--[[ + Returns an element in the _input_ Table that the handler returns `true` for, when passed the + `(child, key)` entry. + + Returns nil if no entires satisfy the condition. + + For an Array, this first matching element is returned. + + For a Map, an arbitrary matching element is returned if it exists. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) +local iterator = require(Dash.iterator) + +-- TODO Luau: support generic function definitions +export type FindHandler = (any, any) -> boolean + +local function find(input: Types.Table, handler: FindHandler) + assertEqual(typeof(input), "table", [[Attempted to call Dash.find with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(handler), "function", [[Attempted to call Dash.find with argument #2 of type {left:?} not {right:?}]]) + for key, child in iterator(input) do + if handler(child, key) then + return child + end + end +end +return find \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/findIndex.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/findIndex.lua new file mode 100644 index 0000000..a3dcd0d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/findIndex.lua @@ -0,0 +1,23 @@ +--[[ + Returns the index of the first element in the _input_ Array that the handler returns `true` for, + when passed the `(child, key)` entry. + + Returns nil if no entires satisfy the condition. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) + +-- TODO Luau: support generic function definitions +export type FindHandler = (any, any) -> boolean + +local function findIndex(input: Types.Array, handler: FindHandler) + assertEqual(typeof(input), "table", [[Attempted to call Dash.findIndex with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(handler), "function", [[Attempted to call Dash.findIndex with argument #2 of type {left:?} not {right:?}]]) + for key, child in ipairs(input) do + if handler(child, key) then + return key + end + end +end +return findIndex \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/flat.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/flat.lua new file mode 100644 index 0000000..a8cb2ad --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/flat.lua @@ -0,0 +1,22 @@ +--[[ + Flattens the input array by a single level. + + Outputs a new Array of elements merged from the _input_ Array arguments in left-to-right order. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local append = require(Dash.append) +local assertEqual = require(Dash.assertEqual) +local forEach = require(Dash.forEach) + +-- TODO Luau: Support function generics +local function flat(input: Types.Array>): Types.Array + assertEqual(typeof(input), "table", [[Attempted to call Dash.flat with argument #1 of type {left:?} not {right:?}]]) + local result = {} + forEach(input, function(childArray: Types.Array) + append(result, childArray) + end) + return result +end + +return flat \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/forEach.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/forEach.lua new file mode 100644 index 0000000..c4b06f1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/forEach.lua @@ -0,0 +1,25 @@ +--[[ + Iterates through the elements of the _input_ Table. + + If the table is an Array, it iterates in order 1..n. + + If the table is a Map, the keys are visited in an arbitrary order. + + Calls the _handler_ for each entry. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) +local iterator = require(Dash.iterator) + +export type ForEachHandler = (Value, number) -> () +-- TODO Luau: Support function generics +--local function forEach(input: Types.Array, handler: ForEachHandler) +local function forEach(input: Types.Table, handler: Types.AnyFunction) + assertEqual(typeof(input), "table", [[Attempted to call Dash.forEach with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(handler), "function", [[Attempted to call Dash.forEach with argument #2 of type {left:?} not {right:?}]]) + for key, value in iterator(input) do + handler(value, key) + end +end +return forEach \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/forEachArgs.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/forEachArgs.lua new file mode 100644 index 0000000..f7c34ae --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/forEachArgs.lua @@ -0,0 +1,20 @@ +--[[ + Iterates through the tail arguments in order, including nil values up to the argument list length. + Calls the _handler_ for each entry. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) + +export type ForEachArgsHandler = (Value, number) -> () +-- TODO Luau: Support function generics +-- TODO Luau: Support vararg types +--local function forEachArgs(handler: ForEachArgsHandler, ...: Types.Args) + +local function forEachArgs(handler: Types.AnyFunction, ...) + assertEqual(typeof(handler), "function", [[Attempted to call Dash.forEachArgs with argument #1 of type {left:?} not {right:?}]]) + for index = 1, select('#', ...) do + handler(select(index, ...), index) + end +end +return forEachArgs \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/format.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/format.lua new file mode 100644 index 0000000..8d98caa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/format.lua @@ -0,0 +1,88 @@ +--[[ + Returns the _format_ string with placeholders `{...}` substituted with readable representations + of the subsequent arguments. + This function is a simpler & more powerful version of `string.format`, inspired by `format!` + in Rust. + + * `{}` formats and prints the next argument using `:format()` if available, or a suitable + default representation depending on its type. + * `{blah}` formats and prints the key "blah" of the 1st argument + * `{2}` formats and prints the 2nd argument. + * `{#2}` prints the length of the 2nd argument. + Display parameters can be combined after a `:` in the curly braces. Any format parameters used + in `string.format` can be used here, along with these extras: + * `{:?}` formats any value using `pretty`. + * `{:#?}` formats any value using multiline `pretty`. + @example + local props = {"teeth", "claws", "whiskers", "tail"} + format("{:?} is in {:#?}", "whiskers", props) + -> '"whiskers" is in {"teeth", "claws", "whiskers", "tail"}' + @example + format("The time is {:02}:{:02}", 2, 4) -> "The time is 02:04" + @example + format("The color blue is #{:06X}", 255) -> "The color blue is #0000FF" + @usage Escape `{` with `{{` and `}` similarly with `}}`. + @usage See [https://developer.roblox.com/articles/Format-String](https://developer.roblox.com/articles/Format-String) + for complete list of formating options and further use cases. +]] + +local Dash = script.Parent +local assertEqual = require(Dash.assertEqual) +local formatValue = require(Dash.formatValue) +local splitOn = require(Dash.splitOn) +local startsWith = require(Dash.startsWith) + +local concat = table.concat +local insert = table.insert + +local function format(formatString: string, ...) + assertEqual(typeof(formatString), "string", [[Attempted to call Dash.format with argument #1 of type {left:?} not {right:?}]]) + local args = {...} + local argIndex = 1 + local texts, subs = splitOn(formatString, "{[^{}]*}") + local result = {} + -- Iterate through possible curly-brace matches, ignoring escaped and substituting valid ones + for i, text in pairs(texts) do + local unescaped = text:gsub("{{", "{"):gsub("}}", "}") + insert(result, unescaped) + local placeholder = subs[i] and subs[i]:sub(2, -2) + if placeholder then + -- Ensure that the curly braces have not been escaped + local escapeMatch = text:gmatch("{+$")() + local isEscaped = escapeMatch and #escapeMatch % 2 == 1 + if not isEscaped then + -- Split the placeholder into left & right parts pivoting on the central ":" + local placeholderSplit = splitOn(placeholder, ":") + local isLength = startsWith(placeholderSplit[1], "#") + local argString = isLength and placeholderSplit[1]:sub(2) or placeholderSplit[1] + local nextIndex = tonumber(argString) + local displayString = placeholderSplit[2] + local arg = "nil" + if nextIndex then + -- Return the next argument + arg = args[nextIndex] + elseif argString:len() > 0 then + -- Print a child key of the 1st argument + local argChild = args[1] and args[1][argString] + if argChild ~= nil then + arg = argChild + end + else + arg = args[argIndex] + argIndex = argIndex + 1 + end + if isLength then + arg = #arg + end + -- Format the selected value + insert(result, formatValue(arg, displayString or "")) + else + local unescapedSub = placeholder + insert(result, unescapedSub) + end + end + end + return concat(result, "") +end + +return format \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/formatValue.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/formatValue.lua new file mode 100644 index 0000000..5f9a9cc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/formatValue.lua @@ -0,0 +1,43 @@ +--[[ + Format a specific _value_ using the specified _displayString_. + @example + formatValue(255, "06X") --> "0000FF" + @example + formatValue(255.5) --> "255.5" + @see `format` - for a full description of valid display strings. +]] + +local Dash = script.Parent +local assertEqual = require(Dash.assertEqual) + +local function formatValue(value: any, displayString: string): string + displayString = displayString or "" + assertEqual(typeof(displayString), "string", [[Attempted to call Dash.formatValue with argument #2 of type {left:?} not {right:?}]]) + -- Inline require to prevent infinite require cycle + local displayTypeStart, displayTypeEnd = displayString:find("[A-Za-z#?]+") + if displayTypeStart then + local displayType = displayString:sub(displayTypeStart, displayTypeEnd) + local formatAsString = + "%" .. displayString:sub(1, displayTypeStart - 1) .. displayString:sub(displayTypeEnd + 1) .. "s" + -- Pretty print values + local pretty = require(Dash.pretty) + if displayType == "#?" then + -- Multiline print a value + return formatAsString:format(pretty(value, {multiline = true})) + elseif displayType == "?" then + -- Inspect a value + return formatAsString:format(pretty(value)) + end + return ("%" .. displayString):format(value) + else + local displayType = "s" + if type(value) == "number" then + -- Correctly display floats or integers + local _, fraction = math.modf(value) + displayType = fraction == 0 and "d" or "f" + end + return ("%" .. displayString .. displayType):format(tostring(value)) + end +end + +return formatValue \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/freeze.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/freeze.lua new file mode 100644 index 0000000..356b537 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/freeze.lua @@ -0,0 +1,77 @@ +--[[ + Returns a new read-only view of _object_ which prevents any values from being changed. + + @param name The name of the object for improved error message readability. + @param throwIfMissing If `true` then access to a missing key will also throw. + + @note + Unfortunately you cannot iterate using `pairs` or `ipairs` on frozen objects because Luau + doesn't support defining these custom iterators in metatables. + + @example + local drink = freeze("Ice Cream", { + flavor = "mint", + topping = "sprinkles" + }, true) + print(drink.flavor) --> "mint" + drink.flavor = "vanilla" + --!> ReadonlyKey: Attempt to write to readonly key "flavor" (a string) of frozen object "Ice Cream"` + print(drink.syrup) --> nil + --!> `MissingKey: Attempt to read missing key "syrup" (a string) of frozen object "Ice Cream"` +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local Error = require(Dash.Error) +local assertEqual = require(Dash.assertEqual) +local format = require(Dash.format) + +-- TODO Luau: Improve type inference to make these not need to be any +local ReadonlyKey: any = Error.new("ReadonlyKey", "Attempted to write to readonly key {key:?} of frozen object {objectName:?}") +local MissingKey: any = Error.new("MissingKey", "Attempted to read missing key {key:?} of frozen object {objectName:?}") + +-- TODO Luau: Support generic functions +-- TODO Luau: Support generic extends syntax +-- TYPED: local function freeze(objectName: string, object: T, throwIfMissing: boolean?): T +local function freeze(objectName: string, object: Types.Table, throwIfMissing: boolean?) + assertEqual(typeof(objectName), "string", [[Attempted to call Dash.freeze with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(object), "table", [[Attempted to call Dash.freeze with argument #2 of type {left:?} not {right:?}]]) + -- We create a proxy so that the underlying object is not affected + local proxy = {} + setmetatable( + proxy, + { + __index = function(_, key: any) + local value = object[key] + if value == nil and throwIfMissing then + -- Tried to read a key which isn't present in the underlying object + MissingKey:throw({ + key = key, + objectName = objectName + }) + end + return value + end, + __newindex = function(_, key: any) + -- Tried to write to any key + ReadonlyKey:throw({ + key = key, + objectName = objectName + }) + end, + __len = function() + return #object + end, + __tostring = function() + return format("Frozen({})", objectName) + end, + __call = function(_, ...) + -- TODO Luau: Gated check for if a function has a __call value + local callable: any = object + return callable(...) + end + } + ) + return proxy +end + +return freeze \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/getOrSet.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/getOrSet.lua new file mode 100644 index 0000000..37f0a94 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/getOrSet.lua @@ -0,0 +1,25 @@ +--[[ + Returns a value of the _input_ Table at the _key_ provided. + + If the key is missing, the value is acquired from the _getValue_ handler, + added to the _input_ Table and returned. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local Error = require(Dash.Error) +local assertEqual = require(Dash.assertEqual) +local format = require(Dash.format) + +-- TODO Luau: Support generic functions +export type GetValueHandler = (any) -> any + +local function getOrSet(input: Types.Table, key: any, getValue: GetValueHandler) + assertEqual(typeof(input), "table", [[Attempted to call Dash.getOrSet with argument #1 of type {left:?} not {right:?}]]) + assertEqual(key == nil, false, [[Attempted to call Dash.getOrSet with a nil key argument]]) + assertEqual(typeof(getValue), "function", [[Attempted to call Dash.getOrSet with argument #3 of type {left:?} not {right:?}]]) + if input[key] == nil then + input[key] = getValue(input, key) + end + return input[key] +end +return getOrSet \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/groupBy.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/groupBy.lua new file mode 100644 index 0000000..56b252b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/groupBy.lua @@ -0,0 +1,39 @@ +--[[ + Groups values in the _input_ Table by their _getKey_ value. + + Each value of the result Table is an Array of values from the _input_ Table which were assigned + the corresponding key. + + If _getKey_ is a function, it is called with each `(child, key)` entry and uses the return + value as the corresponding key to insert at in the result Table. Otherwise, the _getKey_ value + is used directly as the key itself. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) + +local insert = table.insert + +-- TODO Luau: Support generic functions +local function groupBy(input: Dash.Table, getKey: any) + assertEqual(typeof(input), "table", [[Attempted to call Dash.groupBy with argument #1 of type {left:?} not {right:?}]]) + assertEqual(getKey == nil, false, [[Attempted to call Dash.groupBy with a nil getKey argument]]) + local result = {} + for key, child in pairs(input) do + local groupKey + if typeof(getKey) == "function" then + groupKey = getKey(child, key) + else + groupKey = child[getKey] + end + if groupKey ~= nil then + if result[groupKey] ~= nil then + insert(result[groupKey], child) + else + result[groupKey] = {child} + end + end + end + return result +end +return groupBy \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/identity.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/identity.lua new file mode 100644 index 0000000..0c30e3c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/identity.lua @@ -0,0 +1,11 @@ +--[[ + The identity function, which simply returns its input parameters. + + Can be used to make it clear that a handler returns its inputs. +]] +-- TODO Luau: Support typing generic return tuple +local function identity(...) + return ... +end + +return identity \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/includes.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/includes.lua new file mode 100644 index 0000000..91ea7ca --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/includes.lua @@ -0,0 +1,23 @@ +--[[ + Returns `true` if the _item_ exists as a value in the _input_ table. + + A nil _item_ will always return `false`. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) + +-- TODO Luau: Support generic functions +local function includes(input: Types.Table, item: any): boolean + assertEqual(typeof(input), "table", [[Attempted to call Dash.includes with argument #1 of type {left:?} not {right:?}]]) + if item == nil then + return false + end + for _, child in pairs(input) do + if child == item then + return true + end + end + return false +end +return includes \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/init.lua new file mode 100644 index 0000000..5677fba --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/init.lua @@ -0,0 +1,8 @@ +local Dash = {} + +-- Require and add the Dash functions to the Dash table +for _, fn in pairs(script:GetChildren()) do + Dash[fn.Name] = require(fn) +end + +return Dash.freeze("Dash", Dash, true) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/isCallable.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/isCallable.lua new file mode 100644 index 0000000..f99f7ba --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/isCallable.lua @@ -0,0 +1,9 @@ +--[[ + Returns `true` if the value can be called i.e. you can write `value(...)`. +]] +local function isCallable(value: any): boolean + return type(value) == "function" or + (type(value) == "table" and getmetatable(value) and getmetatable(value).__call ~= nil) or false +end + +return isCallable \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/isLowercase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/isLowercase.lua new file mode 100644 index 0000000..c95b7d5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/isLowercase.lua @@ -0,0 +1,19 @@ +--[[ + Returns `true` if the first character of _input_ is a lower-case character. + + Throws if the _input_ is not a string or it is the empty string. + + Our current version of Lua unfortunately does not support upper or lower-case detection outside + the english alphabet. This function has been implemented to return the expected result once + this has been corrected. +]] +local Dash = script.Parent +local assertEqual = require(Dash.assertEqual) + +local function isLowercase(input: string) + assertEqual(typeof(input), "string", [[Attempted to call Dash.isLowercase with argument #1 of type {left:?} not {right:?}]]) + assertEqual(#input > 0, true, [[Attempted to call Dash.isLowercase with an empty string]]) + local firstLetter = input:sub(1, 1) + return firstLetter == firstLetter:lower() +end +return isLowercase \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/isUppercase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/isUppercase.lua new file mode 100644 index 0000000..96efa49 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/isUppercase.lua @@ -0,0 +1,19 @@ +--[[ + Returns `true` if the first character of _input_ is an upper-case character. + + Throws if the _input_ is not a string or it is the empty string. + + Our current version of Lua unfortunately does not support upper or lower-case detection outside + the english alphabet. This function has been implemented to return the expected result once + this has been corrected. +]] +local Dash = script.Parent +local assertEqual = require(Dash.assertEqual) + +local function isUppercase(input: string) + assertEqual(typeof(input), "string", [[Attempted to call Dash.isUppercase with argument #1 of type {left:?} not {right:?}]]) + assertEqual(#input > 0, true, [[Attempted to call Dash.isUppercase with an empty string]]) + local firstLetter = input:sub(1, 1) + return firstLetter == firstLetter:upper() +end +return isUppercase \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/iterable.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/iterable.lua new file mode 100644 index 0000000..b086968 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/iterable.lua @@ -0,0 +1,43 @@ +--[[ + Creates a stateful iterator for the _input_ Table, first visting ordered numeric keys 1..n + and then the remaining unordered keys in any order. + + @see Dash.iterator - for an iterator that can iterate over an iterable. +]] + +local Dash = script.Parent +local Types = require(Dash.Types) + +local function iterable(input: Types.Table): Types.AnyFunction + local currentIndex = 1 + local inOrderedKeys = true + local currentKey + local iterateFn + iterateFn = function() + if inOrderedKeys then + local value = input[currentIndex] + if value == nil then + inOrderedKeys = false + else + local index = currentIndex + currentIndex += 1 + return index, value + end + end + while true do + currentKey = next(input, currentKey) + -- Don't re-visit ordered keys 1..n + if typeof(currentKey) == "number" and currentKey > 0 and currentKey < currentIndex and currentKey % 1 == 0 then + continue + end + if currentKey == nil then + return nil + else + return currentKey, input[currentKey] + end + end + end + return iterateFn +end + +return iterable \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/iterator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/iterator.lua new file mode 100644 index 0000000..c6c4471 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/iterator.lua @@ -0,0 +1,29 @@ +--[[ + Iterates using a `pairs` iterator for an _input_ Table if zero length, otherwise an `ipairs` + iterator for an Array. + + If _input_ is a function it is used as a stateful iterator instead. + + This function can be used to build behaviour that iterates over both Arrays and Maps. + + @see Dash.iterable if you want to iterate over a Table with numeric but un-ordered keys. +]] + +local Dash = script.Parent +local Types = require(Dash.Types) + +local function iterator(input: Types.Table | Types.AnyFunction): Types.AnyFunction + if typeof(input) == "function" then + return input + elseif typeof(input) == "table" then + if #input > 0 then + return ipairs(input) + else + return pairs(input) + end + else + return nil + end +end + +return iterator \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/join.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/join.lua new file mode 100644 index 0000000..11cca52 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/join.lua @@ -0,0 +1,18 @@ +--[[ + Output a new Map from merging all the keys in the Map arguments in left-to-right order. + + The None symbol can be used to remove existing elements. + + @param ... any number of tables +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assign = require(Dash.assign) + +-- TODO Luau: Support typing varargs +-- TODO Luau: Support function generics +local function join(...): Types.Map + return assign({}, ...) +end + +return join \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/joinDeep.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/joinDeep.lua new file mode 100644 index 0000000..b942bed --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/joinDeep.lua @@ -0,0 +1,41 @@ +--[[ + Creates a shallow clone of the _source_ Map, and copies the values from the _delta_ Map + by key, like the join utility. + + However, if any of the values are tables themselves, the joinDeep function is called + recursively to produce a new table at the specified key. + + The purpose of this function is to merge nested immutable data using as few table + creation operations as possible, making it appropriate for updating state in a reducer. + + The None symbol can be used to remove an existing value. +]] +local Dash = script.Parent +local None = require(Dash.None) +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) +local forEach = require(Dash.forEach) +local copy = require(Dash.copy) + +-- TODO Luau: Support typing varargs +-- TODO Luau: Support function generics +local function joinDeep(source: Types.Table, delta: Types.Table): Types.Table + assertEqual(typeof(source), "table", [[Attempted to call Dash.joinDeep with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(delta), "table", [[Attempted to call Dash.joinDeep with argument #2 of type {left:?} not {right:?}]]) + local result = copy(source) + -- Iterate through each key of the input and assign to target at the same key + forEach(delta, function(value, key) + if typeof(source[key]) == "table" and typeof(value) == "table" then + -- Only merge tables + result[key] = joinDeep(source[key], value) + elseif value == None then + -- Remove none values + result[key] = nil + else + result[key] = value + end + end) + return result +end + +return joinDeep \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/keyBy.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/keyBy.lua new file mode 100644 index 0000000..464748d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/keyBy.lua @@ -0,0 +1,31 @@ +--[[ + Assigns values in the _input_ Table by their _getKey_ value. + + If _getKey_ is a function, it is called with each `(child, key)` entry and uses the return + value as the corresponding key to assign to in the result Table. Otherwise, the _getKey_ value + is used directly as the key itself. +]] + +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) +local collect = require(Dash.collect) + +local insert = table.insert + +-- TODO Luau: Support generic functions +local function keyBy(input: Types.Table, getKey: any): Types.Table + assertEqual(typeof(input), "table", [[Attempted to call Dash.keyBy with argument #1 of type {left:?} not {right:?}]]) + assertEqual(getKey == nil, false, [[Attempted to call Dash.keyBy with a nil getKey argument]]) + + return collect(input, function(key, child) + local newKey + if typeof(getKey) == "function" then + newKey = getKey(child, key) + else + newKey = child[getKey] + end + return newKey, child + end) +end +return keyBy \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/keys.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/keys.lua new file mode 100644 index 0000000..6746634 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/keys.lua @@ -0,0 +1,24 @@ +--[[ + Returns an Array of the keys in the _input_ Table. + + If the input is an Array, ordering is preserved. + + If the input is a Map, elements are returned in an arbitrary order. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) +local iterator = require(Dash.iterator) + +local insert = table.insert + +-- TODO Luau: Support generic functions +local function keys(input: Types.Table): Types.Array + assertEqual(typeof(input), "table", [[Attempted to call Dash.keys with argument #1 of type {left:?} not {right:?}]]) + local result = {} + for key, _ in iterator(input) do + insert(result, key) + end + return result +end +return keys \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/last.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/last.lua new file mode 100644 index 0000000..0b75811 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/last.lua @@ -0,0 +1,29 @@ +--[[ + Returns the last element in the _input_ Array that the handler returns `true` for, when + passed the `(child, index)` entry. + + Returns nil if no entires satisfy the condition. + + If handler is not defined, the function simply returns the last element of the Array. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) + +-- TODO Luau: support generic function definitions +export type FindHandler = (any, any) -> boolean + +local function last(input: Types.Array, handler: FindHandler?) + assertEqual(typeof(input), "table", [[Attempted to call Dash.last with argument #1 of type {left:?} not {right:?}]]) + for index = #input, 1, -1 do + local child = input[index] + if not handler then + return child + end + if handler(child, index) then + return child + end + end +end + +return last \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/leftPad.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/leftPad.lua new file mode 100644 index 0000000..0595f0e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/leftPad.lua @@ -0,0 +1,24 @@ +--[[ + Makes a string of `length` from `input` by repeating characters from `prefix` at the start of the string. + @example leftPad("toast", 6) --> " toast" + @example leftPad("2", 2, "0") --> "02" + @example leftPad("toast", 10, ":)") --> ":):):toast" + @param prefix (default = `" "`) +]] +local Dash = script.Parent +local assertEqual = require(Dash.assertEqual) + +local function leftPad(input: string, length: number, prefix: string?): string + assertEqual(typeof(input), "string", [[Attempted to call Dash.leftPad with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(length), "number", [[Attempted to call Dash.leftPad with argument #2 of type {left:?} not {right:?}]]) + + local definedPrefix = prefix or " " + assertEqual(typeof(definedPrefix), "string", [[Attempted to call Dash.leftPad with argument #3 of type {left:?} not {right:?}]]) + + local padLength = length - input:len() + local remainder = padLength % definedPrefix:len() + local repetitions = (padLength - remainder) / definedPrefix:len() + return string.rep(definedPrefix or " ", repetitions) .. definedPrefix:sub(1, remainder) .. input +end + +return leftPad \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/map.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/map.lua new file mode 100644 index 0000000..033c1b0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/map.lua @@ -0,0 +1,38 @@ +--[[ + Iterates through the elements of the _input_ Table. + + For an Array input, the elements are visted in order 1..n. + + For a Map input, the elements are visited in an arbitrary order. + + Calls the _handler_ for each entry and constructs a new Table using the same keys but replacing + the values with new ones returned from the handler. + + Values returned by _handler_ must be defined. + + @see Dash.collectArray if you want to return nil values. +]] + +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) +local iterator = require(Dash.iterator) + +-- TODO Luau: Support generic functions +export type MapHandler = (any, number) -> any + +-- TYPED: export type MapHandler = (Value, number) -> Output +-- TYPED: local function map(input: Types.Array, fn: MapFn) + +local function map(input: Types.Array, handler: MapHandler): Types.Array + assertEqual(typeof(input), "table", [[Attempted to call Dash.map with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(handler), "function", [[Attempted to call Dash.map with argument #2 of type {left:?} not {right:?}]]) + local result = {} + for key, child in iterator(input) do + local value = handler(child, key) + assertEqual(value == nil, false, [[Returned nil from a Dash.map handler]]) + result[key] = value + end + return result +end +return map \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/mapFirst.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/mapFirst.lua new file mode 100644 index 0000000..79f7097 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/mapFirst.lua @@ -0,0 +1,23 @@ +--[[ + Iterates through the elements of the _input_ Array in order 1..n. + + Calls the _handler_ for each entry and returns the first non-nil value returned by the handler. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) + +-- TODO Luau: Support generic functions +export type MapHandler = (any, number) -> any + +local function mapFirst(input: Types.Array, handler: MapHandler) + assertEqual(typeof(input), "table", [[Attempted to call Dash.mapFirst with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(handler), "function", [[Attempted to call Dash.mapFirst with argument #2 of type {left:?} not {right:?}]]) + for index, child in ipairs(input) do + local output = handler(child, index) + if output ~= nil then + return output + end + end +end +return mapFirst \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/mapLast.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/mapLast.lua new file mode 100644 index 0000000..73961c8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/mapLast.lua @@ -0,0 +1,24 @@ +--[[ + Iterates through the elements of the _input_ Array in reverse in order n..1. + + Calls the _handler_ for each entry and returns the first non-nil value returned by the handler. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) + +-- TODO Luau: Support generic functions +export type MapHandler = (any, number) -> any + +local function mapLast(input: Types.Array, handler: MapHandler) + assertEqual(typeof(input), "table", [[Attempted to call Dash.mapLast with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(handler), "function", [[Attempted to call Dash.mapLast with argument #2 of type {left:?} not {right:?}]]) + for key = #input, 1, -1 do + local child = input[key] + local output = handler(child, key) + if output ~= nil then + return output + end + end +end +return mapLast \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/mapOne.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/mapOne.lua new file mode 100644 index 0000000..ab8bb69 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/mapOne.lua @@ -0,0 +1,29 @@ +--[[ + Iterates through the elements of the _input_ Table in no particular order. + + Calls the _handler_ for each entry and returns the first non-nil value returned by the handler. + + If _handler_ is nil, the first value visited is returned. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) + +-- TODO Luau: Support generic functions +export type MapHandler = (any, number) -> any + +local function mapOne(input: Types.Table, handler: MapHandler?) + assertEqual(typeof(input), "table", [[Attempted to call Dash.mapOne with argument #1 of type {left:?} not {right:?}]]) + for key, child in pairs(input) do + local output + if handler then + output = handler(child, key) + else + output = child + end + if output ~= nil then + return output + end + end +end +return mapOne \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/noop.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/noop.lua new file mode 100644 index 0000000..5f1b351 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/noop.lua @@ -0,0 +1,10 @@ +--[[ + A function which does nothing. + + Can be used to make it clear that a handler has no function. +]] +local function noop() + +end + +return noop \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/omit.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/omit.lua new file mode 100644 index 0000000..2d454be --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/omit.lua @@ -0,0 +1,30 @@ +--[[ + Return a new Table made from entries in the _input_ Table whose key is not in the _keys_ Array. + + If the input is an Array, ordering is preserved. + + If the input is a Map, elements are returned in an arbitrary order. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) +local collectSet = require(Dash.collectSet) +local forEach = require(Dash.forEach) + +-- TODO Luau: Support generic functions, then substitute type signature +-- TYPED: local function omit(input: Types.Map, keys: Types.Array): Value +local function omit(input: Types.Table, keys: Types.Array): Types.Table + assertEqual(typeof(input), "table", [[Attempted to call Dash.omit with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(keys), "table", [[Attempted to call Dash.omit with argument #2 of type {left:?} not {right:?}]]) + local output = {} + local keySet = collectSet(keys) + -- TYPED: forEach(input, function(child: Value, key: Key) + forEach(input, function(child, key) + if not keySet[key] then + output[key] = input[key] + end + end) + return output +end + +return omit \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/pick.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/pick.lua new file mode 100644 index 0000000..dbf7c92 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/pick.lua @@ -0,0 +1,26 @@ +--[[ + Pick entries in the _input_ Table which should remain in the output by calling the handler on + each `(child, index)` tuple. + + The handler should return truthy to preserve the value in the resulting Table. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) +local iterator = require(Dash.iterator) + +-- TODO Luau: support generic function definitions +export type PickHandler = (any, any) -> boolean + +local function pick(input: Types.Map, handler: PickHandler): Types.Map + assertEqual(typeof(input), "table", [[Attempted to call Dash.pick with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(handler), "function", [[Attempted to call Dash.pick with argument #2 of type {left:?} not {right:?}]]) + local result = {} + for key, child in iterator(input) do + if handler(child, key) then + result[key] = child + end + end + return result +end +return pick \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/pretty.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/pretty.lua new file mode 100644 index 0000000..9fc8f2b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/pretty.lua @@ -0,0 +1,189 @@ +--[[ + Return a pretty string serialization of _object_. + + This implementation deals with cycles in tables and can neatly display metatables. + + Optionally use an indented multiline string, limit the depth of tables, omit or pick keys. +]] +local Dash = script.Parent +local Types = require(Dash.Types) + +local append = require(Dash.append) +local assign = require(Dash.assign) +local cycles = require(Dash.cycles) +local includes = require(Dash.includes) +local join = require(Dash.join) +local map = require(Dash.map) +local keys = require(Dash.keys) +local slice = require(Dash.slice) + +local concat = table.concat +local insert = table.insert +local sort = table.sort + +export type PrettyOptions = { + -- The maximum depth of ancestors of a table to display (default = 2) + depth: number?, + -- An array of keys which should not be visited + omit: Types.Array?, + -- Whether to use multiple lines (default = false) + multiline: boolean?, + -- Whether to show the length of any array in front of its content + arrayLength: boolean?, + -- The maximum length of a line (default = 80) + maxLineLength: number?, + -- Whether to drop the quotation marks around strings. By default, this is true for table keys + noQuotes: boolean?, + -- The indent string to use (default = "\t") + indent: string?, + -- A set of tables which have already been visited and should be referred to by reference + visited: Types.Set?, + -- A cycles object returned from `cycles` to aid reference display + cycles: cycles.Cycles?, +} + +local function indentLines(lines: Types.Array, indent: string) + return map(lines, function(line: string) + return indent .. line + end) +end + +local pretty + + +-- TODO Luau: Improve type inference to a point that this definition does not produce so many type errors +-- local function prettyLines(object: any, options: PrettyOptions?): Types.Array +local function prettyLines(object: any, options: any): Types.Array + options = options or {} + if type(object) == "table" then + -- A table needs to be serialized recusively + -- Construct the options for recursive calls for the table values + local valueOptions = assign({ + visited = {}, + indent = "\t", + depth = 2 + }, options, { + -- Depth is reduced until we shouldn't recurse any more + depth = options.depth and options.depth - 1 or nil, + cycles = options.cycles or cycles(object, options.depth, { + visited = {}, + refs = {}, + nextRef = 0, + depth = options.depth, + omit = options.omit or {} + }) + }) + if valueOptions.depth == -1 then + -- Indicate there is more information available beneath the maximum depth + return {"..."} + end + if valueOptions.visited[object] then + -- Indicate this table has been printed already, so print a ref number instead of + -- printing it multiple times + return {"&" .. valueOptions.cycles.refs[object]} + end + + valueOptions.visited[object] = true + + local multiline = valueOptions.multiline + local comma = multiline and "," or ", " + + -- If the table appears multiple times in the output, mark it with a ref prefix so it can + -- be identified if it crops up later on + local ref = valueOptions.cycles.refs[object] + local refTag = ref and ("<%s>"):format(ref) or "" + local lines = {refTag .. "{"} + + -- Build the options for the recursive call for the table keys + local keyOptions = join(valueOptions, { + noQuotes = true, + multiline = false + }) + + -- Compact numeric keys into a simpler array style + local maxConsecutiveIndex = 0 + local first = true + for index, value in ipairs(object) do + if valueOptions.omit and includes(valueOptions.omit, index) then + -- Don't include keys which are omitted + continue + end + if first then + first = false + else + lines[#lines] = lines[#lines] .. comma + end + if valueOptions.multiline then + local indendedValue = indentLines(prettyLines(value, valueOptions), valueOptions.indent) + append(lines, indendedValue) + else + lines[#lines] = lines[#lines] .. pretty(value, valueOptions) + end + maxConsecutiveIndex = index + end + if #object > 0 and valueOptions.arrayLength then + lines[1] = ("#%d %s"):format(#object, lines[1]) + end + -- Ensure keys are printed in order to guarantee consistency + local objectKeys = keys(object) + sort(objectKeys, function(left, right) + if typeof(left) == "number" and typeof(right) == "number" then + return left < right + else + return tostring(left) < tostring(right) + end + end) + for _, key in ipairs(objectKeys) do + local value = object[key] + -- We printed a key if it's an index e.g. an integer in the range 1..n. + if typeof(key) == "number" and key % 1 == 0 and key >= 1 and key <= maxConsecutiveIndex then + continue + end + if valueOptions.omit and includes(valueOptions.omit, key) then + -- Don't include keys which are omitted + continue + end + if first then + first = false + else + lines[#lines] = lines[#lines] .. comma + end + if valueOptions.multiline then + local keyLines = prettyLines(key, keyOptions) + local indentedKey = indentLines(keyLines, valueOptions.indent) + local valueLines = prettyLines(value, valueOptions) + local valueTail = slice(valueLines, 2) + local indendedValueTail = indentLines(valueTail, valueOptions.indent) + -- The last line of the key and first line of the value are concatenated together + indentedKey[#indentedKey] = ("%s = %s"):format(indentedKey[#indentedKey], valueLines[1]) + append(lines, indentedKey) + append(lines, indendedValueTail) + else + lines[#lines] = ("%s%s = %s"):format(lines[#lines], pretty(key, keyOptions), pretty(value, valueOptions)) + end + end + if valueOptions.multiline then + if first then + -- An empty table is just represented as {} + lines[#lines] = lines[#lines] .. "}" + else + insert(lines, "}") + end + else + lines[#lines] = ("%s}"):format(lines[#lines]) + end + return lines + elseif type(object) == "string" and not options.noQuotes then + return {('"%s"'):format(object)} + else + return {tostring(object)} + end +end + +-- TODO Luau: Improve type inference to a point that this definition does not produce so many type errors +-- pretty = function(object: any, options: PrettyOptions?): string +pretty = function(object: any, options: any): string + return concat(prettyLines(object, options), "\n") +end + +return pretty \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/reduce.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/reduce.lua new file mode 100644 index 0000000..0562504 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/reduce.lua @@ -0,0 +1,26 @@ +--[[ + Iterate through the elements of the _input_ Array in order 1..n. + + Call the _handler_ for each element, passing the return of the previous call as the first argument. + + The _initial_ value is passed into the first call, and the final value returned by the function. +]] + +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) + +-- TODO Luau: Support generic functions +export type ReduceHandler = (any, any, any) -> any + +local function reduce(input: Types.Array, handler: ReduceHandler, initial: any) + assertEqual(typeof(input), "table", [[Attempted to call Dash.reduce with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(handler), "function", [[Attempted to call Dash.reduce with argument #2 of type {left:?} not {right:?}]]) + local result = initial + for i = 1, #input do + result = handler(result, input[i], i) + end + return result +end + +return reduce \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/reverse.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/reverse.lua new file mode 100644 index 0000000..8e87c13 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/reverse.lua @@ -0,0 +1,19 @@ +--[[ + Reverse the order of the elements in the _input_ Array. +]] + +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) + +local insert = table.insert + +local function reverse(input: Types.Array): Types.Array + assertEqual(typeof(input), "table", [[Attempted to call Dash.reverse with argument #1 of type {left:?} not {right:?}]]) + local output = {} + for i = #input, 1, -1 do + insert(output, input[i]) + end + return output +end +return reverse \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/rightPad.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/rightPad.lua new file mode 100644 index 0000000..806f6cd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/rightPad.lua @@ -0,0 +1,26 @@ +--[[ + Makes a string of `length` from `input` by repeating characters from `suffix` at the end of the string. + + By default, suffix is " ". + + @example rightPad("toast", 6) --> "toast " + @example rightPad("2", 2, "!") --> "2!" + @example rightPad("toast", 10, ":)") --> "toast:):):" +]] +local Dash = script.Parent +local assertEqual = require(Dash.assertEqual) + +local function rightPad(input: string, length: number, suffix: string?): string + assertEqual(typeof(input), "string", [[Attempted to call Dash.rightPad with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(length), "number", [[Attempted to call Dash.rightPad with argument #2 of type {left:?} not {right:?}]]) + + local definedSuffix = suffix or " " + assertEqual(typeof(definedSuffix), "string", [[Attempted to call Dash.rightPad with argument #3 of type {left:?} not {right:?}]]) + + local padLength = length - input:len() + local remainder = padLength % definedSuffix:len() + local repetitions = (padLength - remainder) / definedSuffix:len() + return input .. string.rep(suffix or " ", repetitions) .. definedSuffix:sub(1, remainder) +end + +return rightPad \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/shallowEqual.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/shallowEqual.lua new file mode 100644 index 0000000..76bdd00 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/shallowEqual.lua @@ -0,0 +1,28 @@ +--[[ + Returns `true` if the _left_ and _right_ values are equal (by the equality operator) or the + inputs are tables, and all their keys are equal. +]] +local function shallowEqual(left: any, right: any) + if left == right then + return true + end + if typeof(left) ~= "table" or typeof(right) ~= "table" or #left ~= #right then + return false + end + if left == nil or right == nil then + return false + end + for key, value in pairs(left) do + if right[key] ~= value then + return false + end + end + for key, value in pairs(right) do + if left[key] ~= value then + return false + end + end + return true +end + +return shallowEqual diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/slice.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/slice.lua new file mode 100644 index 0000000..802bf0a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/slice.lua @@ -0,0 +1,41 @@ +--[[ + Return a portion of the _input_ Array starting with the element at the _left_ index and ending + with the element at the _right_ index (i.e. an inclusive range) + + If _left_ is not defined, it defaults to 1. + If _right_ is not defined, it defaults to the length of the array (i.e. the last element) + + If _left_ is `-n`, the slice starts with the element `n` places from the last one. + If _right_ is `-n`, the slice ends with the element `n` places from the last one. + + An empty array is returned if the slice has no or negative length. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) + +local insert = table.insert + +local function slice(input: Types.Array, left: number?, right: number?) + assertEqual(typeof(input), "table", [[Attempted to call Dash.slice with argument #1 of type {left:?} not {right:?}]]) + local output = {} + + -- Default values + left = left or 1 + right = right or #input + assertEqual(typeof(left), "number", [[Attempted to call Dash.slice with argument #2 of type {left:?} not {right:?}]]) + assertEqual(typeof(right), "number", [[Attempted to call Dash.slice with argument #3 of type {left:?} not {right:?}]]) + + if left < 0 then + left = #input + left + end + if right and right < 0 then + right = #input + right + end + for i = left, right do + insert(output, input[i]) + end + return output +end + +return slice \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/some.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/some.lua new file mode 100644 index 0000000..b23a1e8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/some.lua @@ -0,0 +1,24 @@ +--[[ + Iterates through the elements of the _input_ Table in no particular order. + + Calls the _handler_ for each entry and returns `true` if the handler returns truthy for any + element which it is called with. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) + +-- TODO Luau: Support generic functions +export type SomeHandler = (any, any) -> boolean + +local function some(input: Types.Map, handler: SomeHandler?): boolean + assertEqual(typeof(input), "table", [[Attempted to call Dash.some with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(handler), "function", [[Attempted to call Dash.some with argument #2 of type {left:?} not {right:?}]]) + for key, child in pairs(input) do + if handler(child, key) then + return true + end + end + return false +end +return some \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/splitOn.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/splitOn.lua new file mode 100644 index 0000000..e66601a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/splitOn.lua @@ -0,0 +1,33 @@ +--[[ + Splits _input_ into parts based on a _pattern_ delimiter and returns a Table of the parts, + followed by a Table of the matched delimiters. +]] +local Dash = script.Parent +local assertEqual = require(Dash.assertEqual) + +local insert = table.insert + +local function splitOn(input: string, pattern: string): Types.Array + assertEqual(typeof(input), "string", [[Attempted to call Dash.splitOn with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(pattern), "string", [[Attempted to call Dash.splitOn with argument #2 of type {left:?} not {right:?}]]) + local parts = {} + local delimiters = {} + local from = 1 + if not pattern then + for i = 1, #input do + insert(parts, input:sub(i, i)) + end + return parts + end + local delimiterStart, delimiterEnd = input:find(pattern, from) + while delimiterStart do + insert(delimiters, input:sub(delimiterStart, delimiterEnd)) + insert(parts, input:sub(from, delimiterStart - 1)) + from = delimiterEnd + 1 + delimiterStart, delimiterEnd = input:find(pattern, from) + end + insert(parts, input:sub(from)) + return parts, delimiters +end + +return splitOn \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/startsWith.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/startsWith.lua new file mode 100644 index 0000000..ff6465e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/startsWith.lua @@ -0,0 +1,15 @@ +--[[ + Checks if _input_ starts with the string _start_. + @example startsWith("Fun Roblox Games", "Fun") --> true + @example startsWith("Chess", "Fun") --> false +]] +local Dash = script.Parent +local assertEqual = require(Dash.assertEqual) + +local function startsWith(input: string, prefix: string): boolean + assertEqual(typeof(input), "string", [[Attempted to call Dash.startsWith with argument #1 of type {left:?} not {right:?}]]) + assertEqual(typeof(prefix), "string", [[Attempted to call Dash.startsWith with argument #2 of type {left:?} not {right:?}]]) + return input:sub(1, prefix:len()) == prefix +end + +return startsWith \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/trim.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/trim.lua new file mode 100644 index 0000000..3faeaa5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/trim.lua @@ -0,0 +1,12 @@ +--[[ + Remove any whitespace at the start and end of the _input_ string. +]] +local Dash = script.Parent +local assertEqual = require(Dash.assertEqual) + +local function trim(input: string): string + assertEqual(typeof(input), "string", [[Attempted to call Dash.trim with argument #1 of type {left:?} not {right:?}]]) + return input:match("^%s*(.-)%s*$") +end + +return trim \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/values.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/values.lua new file mode 100644 index 0000000..554c226 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/dash/values.lua @@ -0,0 +1,24 @@ +--[[ + Returns an Array of the values in the _input_ Map. + + If the input is an Array, ordering is preserved. + + If the input is a Map, elements are returned in an arbitrary order. +]] +local Dash = script.Parent +local Types = require(Dash.Types) +local assertEqual = require(Dash.assertEqual) +local iterator = require(Dash.iterator) + +local insert = table.insert + +-- TODO Luau: Support generic functions +local function values(input: Types.Map): Types.Array + assertEqual(typeof(input), "table", [[Attempted to call Dash.values with argument #1 of type {left:?} not {right:?}]]) + local result = {} + for _, value in iterator(input) do + insert(result, value) + end + return result +end +return values \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/lock.toml new file mode 100644 index 0000000..a9640bc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_dash/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/dash" +version = "0.1.7" +commit = "03aaa4ab36e34e5d4c683c3ec337af80cf70a546" +source = "url+https://github.com/roblox/dash" diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/Dash.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/Dash.lua new file mode 100644 index 0000000..983194a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/Dash.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_dash"]["dash"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/BindableEventBridge.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/BindableEventBridge.lua new file mode 100644 index 0000000..afcd23c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/BindableEventBridge.lua @@ -0,0 +1,73 @@ +--[[ + The Bridge interface allows code in different plugins / data-models / libraries to communicate + with each other. + + The BindableEventBridge implementation uses a BindableEvent in the data model to act as a + message passing interface. +]] + +local HttpService = game:GetService("HttpService") + +local Source = script.Parent.Parent +local Packages = Source.Parent + +local Dash = require(Packages.Dash) +local join = Dash.join +local class = Dash.class +local forEach = Dash.forEach +local insert = table.insert + +local BINDABLE_EVENT_NAME = "DeveloperTools" + +local BindableEventBridge = class("BindableEventBridge", function(parent: Instance, noCreate: boolean) + local id = HttpService:GenerateGUID() + return { + id = id, + connections = {}, + noCreate = noCreate + } +end) + +function BindableEventBridge:_createEvent(parent: Instance) + if self.noCreate then + return nil + end + local event = Instance.new("BindableEvent") + event.Name = BINDABLE_EVENT_NAME + event.Parent = parent + return event +end + +function BindableEventBridge:_init(parent: Instance) + self.event = parent:FindFirstChild(BINDABLE_EVENT_NAME) or self:_createEvent(parent) +end + +function BindableEventBridge:send(message) + local outMessage = join(message, { + fromBridgeId = self.id + }) + if self.event then + self.event:Fire(outMessage) + end +end + +function BindableEventBridge:connect(listener) + local function onEvent(message) + if message.fromBridgeId ~= self.id then + listener(message) + end + end + if self.event then + local connection = self.event.Event:Connect(onEvent) + insert(self.connections, connection) + return connection + end +end + +function BindableEventBridge:destroy() + forEach(self.connections, function(connection) + connection:Disconnect() + end) +end + +return BindableEventBridge \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/CoreGuiDebugInterface.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/CoreGuiDebugInterface.lua new file mode 100644 index 0000000..99e7e4f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/CoreGuiDebugInterface.lua @@ -0,0 +1,32 @@ +--[[ + The CoreGuiDebugInterface class is used to attach the DeveloperInspector to the CoreGui. +]] +local Source = script.Parent.Parent + +local BindableEventBridge = require(Source.Classes.BindableEventBridge) +local DebugInterface = require(Source.Classes.DebugInterface) +local CoreGui = game:GetService("CoreGui") + +local CoreGuiDebugInterface = DebugInterface:extend("CoreGuiDebugInterface", function(appName: string, guiOptions) + local bridge = BindableEventBridge.new(CoreGui) + local interface = DebugInterface.new("CoreGui", appName, {bridge}) + if guiOptions then + spawn(function() + -- The children may not be available synchronously + interface:setGuiOptions({ + rootInstance = CoreGui:WaitForChild(guiOptions.rootInstance, 10), + pickerParent = CoreGui:WaitForChild(guiOptions.pickerParent, 10), + rootPath = guiOptions.rootPath or {guiOptions.rootInstance} + }) + end) + else + interface:setGuiOptions({ + rootInstance = CoreGui, + pickerParent = CoreGui, + rootPath = {} + }) + end + return interface +end) + +return CoreGuiDebugInterface \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/DebugInterface.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/DebugInterface.lua new file mode 100644 index 0000000..b0f5cc9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/DebugInterface.lua @@ -0,0 +1,158 @@ +--[[ + The DebugInterface class controls interactions between consumers of the DeveloperTools library + and the DeveloperTools inspector plugin itself. +]] +local HttpService = game:GetService("HttpService") + +local Source = script.Parent.Parent +local Packages = Source.Parent + +local Dash = require(Packages.Dash) +local class = Dash.class +local forEach = Dash.forEach + +local insert = table.insert + +local EventName = require(Source.EventName) +local PluginEventRouter = require(Source.Classes.PluginEventRouter) +local RoactInspectorWorker = require(Source.RoactInspector.Classes.RoactInspectorWorker) + +local DebugInterface = class("DebugInterface", function(sourceKind: string, sourceName: string, bridges) + return { + sourceId = HttpService:GenerateGUID(), + sourceKind = sourceKind, + sourceName = sourceName, + bridges = bridges, + routers = {}, + targets = {}, + workers = {}, + connectionsForListener = {}, + outboundBridgeForBridgeId = {} + } +end) + +function DebugInterface:addRoactTree(targetName: string, roactTree) + assert(typeof(targetName) == "string", "targetName must be a string") + assert(roactTree, "roactTree must be defined") + + self:_addTarget(targetName, function(targetId: string, toBridgeId: string) + return RoactInspectorWorker.new(self, targetId, toBridgeId, roactTree) + end) +end + +function DebugInterface:addPluginRouter(plugin) + insert(self.routers, PluginEventRouter.new(self.sourceName, plugin, self.bridges)) +end + +function DebugInterface:_connectTargets() + -- Ensure that GetTargets is only connected to once. + -- If targets have already been added we don't need to add another listener + if #self.targets > 0 then + return + end + self:_connect({ + eventName = EventName.GetTargets, + onEvent = function(message) + self:_send({ + eventName = EventName.ShowTargets, + toBridgeId = message.fromBridgeId, + sourceId = self.sourceId, + sourceName = self.sourceName, + sourceKind = self.sourceKind, + targets = self.targets, + }) + end + }) +end + +function DebugInterface:_send(message) + if not (game:GetService("StudioService"):HasInternalPermission()) then + return + end + if not message.toBridgeId then + forEach(self.bridges, function(bridge) + bridge:send(message) + end) + else + local bridge = self.outboundBridgeForBridgeId[message.toBridgeId] + if not bridge then + error(("[DeveloperTools] No bridge to other bridge %s"):format(message.toBridgeId)) + end + bridge:send(message) + end +end + +function DebugInterface:_connect(listener) + if not (game:GetService("StudioService"):HasInternalPermission()) then + return + end + local connections = {} + self.connectionsForListener[listener] = connections + forEach(self.bridges, function(bridge) + local newListener = function(message) + self.outboundBridgeForBridgeId[message.fromBridgeId] = bridge + local matchesEvent = listener.eventName == nil or listener.eventName == message.eventName + local matchesBridge = listener.bridgeId == nil or listener.bridgeId == message.toBridgeId + local matchesTarget = listener.targetId == nil or listener.targetId == message.toTargetId + if matchesEvent and matchesBridge and matchesTarget then + listener.onEvent(message) + end + end + local connection = bridge:connect(newListener) + insert(connections, connection) + end) +end + +function DebugInterface:_disconnect(listener) + local connections = self.connectionsForListener[listener] + forEach(connections, function(connection) + connection:Disconnect() + end) +end + +function DebugInterface:_addTarget(targetName: string, getWorker) + self:_connectTargets() + local id = HttpService:GenerateGUID() + local target = { + id = id, + name = targetName, + } + self.targets[id] = target + self:_connect({ + targetId = id, + eventName = EventName.AttachTarget, + onEvent = function(message) + local worker = self.workers[id] or getWorker(id, message.fromBridgeId) + self.workers[id] = worker + end + }) +end + +function DebugInterface:_removeWorker(id) + local worker = self.workers[id] + if worker then + worker:destroy() + end + self.workers[id] = nil +end + +function DebugInterface:setGuiOptions(guiOptions) + self.rootInstance = guiOptions.rootInstance + self.pickerParent = guiOptions.pickerParent + self.rootPath = guiOptions.rootPath + self.rootPrefix = guiOptions.rootPrefix +end + +function DebugInterface:destroy() + forEach(self.bridges, function(bridge) + bridge:destroy() + end) + forEach(self.routers, function(router) + router:destroy() + end) + forEach(self.workers, function(worker) + worker:destroy() + end) +end + +return DebugInterface \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/InspectorDebugInterface.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/InspectorDebugInterface.lua new file mode 100644 index 0000000..2ebde48 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/InspectorDebugInterface.lua @@ -0,0 +1,57 @@ +--[[ + The InspectorDebugInterface class is used by the DeveloperInspector plugin to attach to other + sources of information. +]] +local Source = script.Parent.Parent + +local BindableEventBridge = require(Source.Classes.BindableEventBridge) +local DebugInterface = require(Source.Classes.DebugInterface) +local RoactInspectorApi = require(Source.RoactInspector.Classes.RoactInspectorApi) +local EventName = require(Source.EventName) + +local InspectorDebugInterface = DebugInterface:extend("InspectorDebugInterface", function(handlers) + local pluginBridge = BindableEventBridge.new(game:GetService("StudioService")) + local coreGuiBridge = BindableEventBridge.new(game:GetService("CoreGui")) + local libraryBridge = BindableEventBridge.new(game:GetService("ReplicatedStorage"), true) + local interface = DebugInterface.new("Inspector", "Inspector", {pluginBridge, coreGuiBridge, libraryBridge}) + interface.handlers = handlers + return interface +end) + +function InspectorDebugInterface:_init() + self:_connectInspector() +end + +function InspectorDebugInterface:getTargetApi() + return self.targetApi +end + +function InspectorDebugInterface:closeTargetApi() + if self.targetApi then + self.targetApi:close() + end +end + +function InspectorDebugInterface:_connectInspector() + self:_connect({ + eventName = EventName.ShowTargets, + onEvent = function(message) + self.handlers.onAddTargets(message) + end + }) +end + +function InspectorDebugInterface:getTargets() + self:_send({ + eventName = EventName.GetTargets + }) +end + +function InspectorDebugInterface:attachRoactTree(bridgeId, targetId) + local roactInspectorApi = RoactInspectorApi.new(self, bridgeId, targetId) + roactInspectorApi:attach(self.handlers.RoactInspector) + self.targetApi = roactInspectorApi + return self.targetApi +end + +return InspectorDebugInterface diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/LibraryDebugInterface.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/LibraryDebugInterface.lua new file mode 100644 index 0000000..f595331 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/LibraryDebugInterface.lua @@ -0,0 +1,19 @@ +--[[ + The LibraryDebugInterface class is used to attach the DeveloperInspector to a library being + developed inside Studio in the ReplicatedStorage service. +]] +local Source = script.Parent.Parent + +local BindableEventBridge = require(Source.Classes.BindableEventBridge) +local DebugInterface = require(Source.Classes.DebugInterface) + +local LibraryDebuggerInterface = DebugInterface:extend("LibraryDebuggerInterface", function(libraryName: string, guiOptions) + local bridge = BindableEventBridge.new(game:GetService("CoreGui")) + local interface = DebugInterface.new("Library", libraryName, {bridge}) + interface:setGuiOptions(guiOptions or { + rootInstance = game:GetService("ReplicatedStorage") + }) + return interface +end) + +return LibraryDebuggerInterface \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/PluginDebugInterface.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/PluginDebugInterface.lua new file mode 100644 index 0000000..b6d8877 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/PluginDebugInterface.lua @@ -0,0 +1,32 @@ +--[[ + The PluginDebugInterface class is used to attach the DeveloperInspector to another plugin. +]] +local Source = script.Parent.Parent + +local BindableEventBridge = require(Source.Classes.BindableEventBridge) +local DebugInterface = require(Source.Classes.DebugInterface) +local RobloxPluginGuiService = game:GetService("RobloxPluginGuiService") + +local PluginDebugInterface = DebugInterface:extend("PluginDebugInterface", function(pluginName: string, plugin, rootInstance: Instance?) + local bridge = BindableEventBridge.new(game:GetService("StudioService")) + local interface = DebugInterface.new("Plugin", pluginName, {bridge}) + if rootInstance then + interface:setGuiOptions({ + rootInstance = rootInstance + }) + else + -- Default to finding the root instance under the plugin gui service + spawn(function() + -- Wait for the UI to populate + local gui = RobloxPluginGuiService:WaitForChild(pluginName, 10) + if gui then + interface:setGuiOptions({ + rootInstance = gui + }) + end + end) + end + return interface +end) + +return PluginDebugInterface \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/PluginEventBridge.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/PluginEventBridge.lua new file mode 100644 index 0000000..2cea404 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/PluginEventBridge.lua @@ -0,0 +1,50 @@ +--[[ + The PluginEventBridge class provides a means for a plugin that has code in a different + data model to be inspected by the DeveloperTools plugin. +]] +local HttpService = game:GetService("HttpService") + +local Source = script.Parent.Parent +local Packages = Source.Parent + +local Dash = require(Packages.Dash) +local class = Dash.class +local join = Dash.join +local forEach = Dash.forEach +local insert = table.insert + +local PLUGIN_EVENT_NAME = "DeveloperTools" + +local PluginEventBridge = class("PluginEventBridge", function(plugin) + return { + id = HttpService:GenerateGUID(), + plugin = plugin, + connections = {} + } +end) + +function PluginEventBridge:send(message) + local outMessage = join(message, { + fromBridgeId = self.id + }) + -- print("[DeveloperTools] Send (Plugin):", pretty(outMessage)) + self.plugin:Invoke(PLUGIN_EVENT_NAME, outMessage) +end + +function PluginEventBridge:connect(listener) + local function onEvent(message) + if message.fromBridgeId ~= self.id then + listener(message) + end + end + local connection = self.plugin:OnInvoke(PLUGIN_EVENT_NAME, onEvent) + insert(self.connections, connection) +end + +function PluginEventBridge:destroy() + forEach(self.connections, function(connection) + connection:Disconnect() + end) +end + +return PluginEventBridge \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/PluginEventRouter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/PluginEventRouter.lua new file mode 100644 index 0000000..2063865 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/PluginEventRouter.lua @@ -0,0 +1,62 @@ +--[[ + The PluginEventRouter class provides a means for a plugin that has code in a different + data model to be inspected by the DeveloperTools plugin. +]] +local HttpService = game:GetService("HttpService") + +local Source = script.Parent.Parent +local Packages = Source.Parent + +local Dash = require(Packages.Dash) +local join = Dash.join +local class = Dash.class +local forEach = Dash.forEach + +local PLUGIN_EVENT_NAME = "DeveloperTools" + + +local PluginEventRouter = class("PluginEventRouter", function(sourceName, plugin, bridges) + return { + routerId = HttpService:GenerateGUID(), + sourceName = sourceName, + plugin = plugin, + bridges = bridges, + -- A list of ids of PluginEvent bridges that have invoked our event + outboundBridgeIds = {} + } +end) + +function PluginEventRouter:_init() + self.connection = self.plugin:OnInvoke(PLUGIN_EVENT_NAME, function(message) + -- Ignore messages from ourselves + if message.fromRouter then + return + end + -- Mark the bridge that this message originated from as accessible + self.outboundBridgeIds[message.fromBridgeId] = true + local outMessage = join(message, { + sourceName = self.sourceName + }) + forEach(self.bridges, function(bridge) + bridge:send(outMessage) + end) + end) + local function onEvent(message) + -- Only route messages that have no particular destination + -- or to bridges we know already exist behind our event + local outMessage = join(message, { + fromRouter = true + }) + -- print("[DeveloperTools] Route (Plugin)", pretty(outMessage)) + self.plugin:Invoke(PLUGIN_EVENT_NAME, outMessage) + end + forEach(self.bridges, function(bridge) + bridge:connect(onEvent) + end) +end + +function PluginEventRouter:destroy() + self.connection:Disconnect() +end + +return PluginEventRouter \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/StandalonePluginDebugInterface.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/StandalonePluginDebugInterface.lua new file mode 100644 index 0000000..0efc32b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/StandalonePluginDebugInterface.lua @@ -0,0 +1,17 @@ +--[[ + The StandalonePluginDebugInterface class is used to attach the DeveloperInspector to a standalone plugin. +]] +local Source = script.Parent.Parent +local Packages = Source.Parent + +local PluginEventBridge = require(Source.Classes.PluginEventBridge) +local DebugInterface = require(Source.Classes.DebugInterface) + +local StandalonePluginDebugInterface = DebugInterface:extend("StandalonePluginDebugInterface", function(pluginName: string, plugin, guiOptions) + local bridge = PluginEventBridge.new(plugin) + local interface = DebugInterface.new("StandalonePlugin", pluginName, {bridge}) + interface:setGuiOptions(guiOptions) + return interface +end) + +return StandalonePluginDebugInterface \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/TargetApi.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/TargetApi.lua new file mode 100644 index 0000000..2f026c5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/TargetApi.lua @@ -0,0 +1,49 @@ +--[[ + The TargetApi class is a base for methods that the inspector can use to spawn messages to + other targets. +]] +local Source = script.Parent.Parent +local Packages = Source.Parent + +local EventName = require(Source.EventName) + +local Dash = require(Packages.Dash) +local join = Dash.join +local class = Dash.class + +local TargetApi = class("TargetApi", function(debugInterface, bridgeId, targetId) + return { + debugInterface = debugInterface, + bridgeId = bridgeId, + targetId = targetId + } +end) + +function TargetApi:close() + self:_send({ + eventName = EventName.CloseTarget + }) +end + +function TargetApi:_send(message) + local newMessage = join(message, { + toBridgeId = self.bridgeId, + toTargetId = self.targetId, + }) + self.debugInterface:_send(newMessage) +end + +function TargetApi:_connect(listener) + local newListener = join(listener, { + fromTargetId = self.targetId, + }) + self.debugInterface:_connect(newListener) +end + +function TargetApi:attach() + self:_send({ + eventName = EventName.AttachTarget, + }) +end + +return TargetApi \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/TargetWorker.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/TargetWorker.lua new file mode 100644 index 0000000..ae02e61 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Classes/TargetWorker.lua @@ -0,0 +1,56 @@ +--[[ + The TargetWorker class is a base for functionality that tracks and notifies the inspector of + any changes to a local state value. +]] +local Source = script.Parent.Parent +local Packages = Source.Parent +local EventName= require(Source.EventName) + +local Dash = require(Packages.Dash) +local class = Dash.class +local forEach = Dash.forEach +local join = Dash.join + +local insert = table.insert + +local TargetWorker = class("TargetWorker", function(debugInterface, targetId, toBridgeId) + return { + targetId = targetId, + toBridgeId = toBridgeId, + debugInterface = debugInterface, + listeners = {} + } +end) + +function TargetWorker:connectEvents() + self:connect({ + eventName = EventName.CloseTarget, + onEvent = function(message) + self.debugInterface:_removeWorker(self.targetId) + end + }) +end + +function TargetWorker:connect(listener) + local newListener = join(listener, { + targetId = self.targetId, + }) + self.debugInterface:_connect(newListener) + insert(self.listeners, newListener) +end + +function TargetWorker:send(message) + local newMessage = join(message, { + fromTargetId = self.targetId, + toBridgeId = self.toBridgeId, + }) + self.debugInterface:_send(newMessage) +end + +function TargetWorker:destroy() + forEach(self.listeners, function(listener) + self.debugInterface:_disconnect(listener) + end) +end + +return TargetWorker \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/EventName.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/EventName.lua new file mode 100644 index 0000000..f0dbb5a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/EventName.lua @@ -0,0 +1,24 @@ +local Source = script.Parent +local Packages = Source.Parent + +local Dash = require(Packages.Dash) +local freeze = Dash.freeze + +return freeze("EventName", { + CloseTarget = "CloseTarget", + GetTargets = "GetTargets", + ShowTargets = "ShowTargets", + AttachTarget = "AttachTarget", + RoactInspector = freeze("EventName.RoactInspector", { + GetChildren = "RoactInspector.GetChildren", + ShowChildren = "RoactInspector.ShowChildren", + GetBranch = "RoactInspector.GetBranch", + ShowBranch = "RoactInspector.ShowBranch", + GetFields = "RoactInspector.GetFields", + ShowFields = "RoactInspector.ShowFields", + SetPicking = "RoactInspector.SetPicking", + PickInstance = "RoactInspector.PickInstance", + Highlight = "RoactInspector.GetHighlight", + Dehighlight = "RoactInspector.Dehighlight", + }, true) +}, true) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Classes/FieldWatcher.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Classes/FieldWatcher.lua new file mode 100644 index 0000000..2aa4355 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Classes/FieldWatcher.lua @@ -0,0 +1,131 @@ +--[[ + The FieldWatcher class is provided with a root table using setRoot and paths to watch for + nested changes at. It periodically polls to check if any of the values being watched have + changed, and calls the onFieldsChange handler with a list of changed paths. +]] +local Source = script.Parent.Parent.Parent +local Packages = Source.Parent +local getChildAtKey = require(Source.RoactInspector.Utils.getChildAtKey) + +local Dash = require(Packages.Dash) +local Types = Dash.Types +local assign = Dash.assign +local class = Dash.class +local collect = Dash.collect +local copy = Dash.copy +local keys = Dash.keys +local pretty = Dash.pretty +local reduce = Dash.reduce +local shallowEqual = Dash.shallowEqual + +local insert = table.insert +local sort = table.sort + +local POLL_DELAY = 0.5 + +type Path = Types.Array + +local FieldWatcher = class("FieldWatcher", function(onFieldsChanged) + return { + onFieldsChanged = onFieldsChanged, + } +end) + +function FieldWatcher:_init() + -- A periodic check for changes + self.onPoll = function() + if self.root and self.polling then + self:_checkFields() + delay(POLL_DELAY, self.onPoll) + end + end +end + +-- Start polling for changes +function FieldWatcher:monitor() + if not self.polling then + self.polling = true + spawn(self.onPoll) + end +end + +-- Determine if any fields have changed +function FieldWatcher:_checkFields() + -- Collect a map of paths which have had their values changed + local changedEntries = collect(self.activeFields, function(path: Path, value) + local newValue = self:walk(self.root, path) + -- Perform a one-deep comparison as any deeper changes will be monitored by a separate path + if not shallowEqual(value, newValue) then + return path, newValue + end + end) + -- Update the values being listened to + assign(self.activeFields, changedEntries) + -- Check for changes and fire the event handler + local changedPaths = keys(changedEntries) + if #changedPaths > 0 then + self.onFieldsChanged(changedPaths) + end +end + +function FieldWatcher:collect(table, depth: number, path) + if typeof(table) ~= "table" then + return {} + end + local fields = collect(table, function(key, value) + -- Permit numbers to preserve numeric ordering of numbers (rather than 1, 10, 11, 2, 3...) + local name = typeof(key) == "number" and key or tostring(key) + -- The path to a nested value uses number or string representations of the keys indexing + -- into each table. While it is possible to have name conflicts between the two + -- representations or sibling keys, they will hopefully be rare, as complex data types + -- typically stringify to a pointer ref in Lua. + local childPath = copy(path) + insert(childPath, name) + local children = typeof(value) == "table" and depth > 0 and self:collect(value, depth - 1, childPath) or {} + return name, { + Name = name, + Summary = pretty(value, {depth = 2, arrayLength = true}), + Path = childPath, + Children = children + } + end) + return fields +end + +-- Access a nested child of _value_ addressed by _path_. +function FieldWatcher:walk(value: Types.Table, path: Path) + -- Guard against invalid accesses resulting from stale paths or user-defined assertions. + local ok, value = pcall(function() + return reduce(path, function(current, key) + return getChildAtKey(current, key) + end, self.root) + end) + if not ok then + warn(("Cannot walk path %s: %s "):format(pretty(path), value)) + value = nil + end + return value +end + +-- Update the root table being watched, from which paths are addressed from. +function FieldWatcher:setRoot(root: Types.Table) + self.activeFields = {} + self.root = root + self:monitor() +end + +-- Add a new path to be listened to, made up of an array of keys addressing descendants. +-- If the child at the path or any of its children shallow change, an event will be fired. +function FieldWatcher:addPath(path: Path) + local value = self:walk(self.root, path) + -- Store a copy so mutations to the existing value don't mutate the keys we are + -- observing, allowing the shallowEqual utility to work its magic + self.activeFields[path] = value and copy(value) or {} +end + +function FieldWatcher:destroy() + self.root = nil + self.polling = false +end + +return FieldWatcher \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Classes/InstancePicker.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Classes/InstancePicker.lua new file mode 100644 index 0000000..13f4a4b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Classes/InstancePicker.lua @@ -0,0 +1,218 @@ +--[[ + The InstancePicker allows a user to hover over UI instances and choose one to be inspected in the + DeveloperInspector plugin. +]] +local Source = script.Parent.Parent.Parent +local Packages = Source.Parent + +local RunService = game:GetService("RunService") + +local Dash = require(Packages.Dash) +local class = Dash.class +local format = Dash.format +local reduce = Dash.reduce + +local ZERO_UDIM2 = UDim2.fromScale(0, 0) +local PICK_DELAY = 0.2 + +-- Arbitrary number larger than DevFramework FOCUSED_ZINDEX +local PICKER_ZINDEX = 1100000 + +local function tweenUDim2(prevValue, nextValue, progress) + local scaleX = (prevValue.X.Scale * (1 - progress) + nextValue.X.Scale * progress) + local offsetX = (prevValue.X.Offset * (1 - progress) + nextValue.X.Offset * progress) + local scaleY = (prevValue.Y.Scale * (1 - progress) + nextValue.Y.Scale * progress) + local offsetY = (prevValue.Y.Offset * (1 - progress) + nextValue.Y.Offset * progress) + return UDim2.new(scaleX, offsetX, scaleY, offsetY) +end + +local function createPickerArea() + local pickerArea = Instance.new("ImageButton") + pickerArea.Name = "InspectorHover" + pickerArea.Active = true + pickerArea.AutoButtonColor = false + pickerArea.BorderSizePixel = 1 + pickerArea.BorderColor3 = Color3.fromRGB(0, 0, 0) + pickerArea.BackgroundColor3 = Color3.fromRGB(220, 230, 255) + pickerArea.BackgroundTransparency = 0.2 + pickerArea.ZIndex = PICKER_ZINDEX + return pickerArea +end + +local function createHighlightArea() + local highlightArea = Instance.new("Frame") + highlightArea.Name = "InspectorHover" + highlightArea.BorderSizePixel = 1 + highlightArea.BorderColor3 = Color3.fromRGB(0, 0, 0) + highlightArea.BackgroundColor3 = Color3.fromRGB(220, 230, 255) + highlightArea.BackgroundTransparency = 0.2 + highlightArea.ZIndex = PICKER_ZINDEX + + local dimensions = Instance.new("TextLabel") + dimensions.Name = "Dimensions" + dimensions.Size = UDim2.new(0, 60, 0, 24) + dimensions.Parent = highlightArea + dimensions.BorderColor3 = Color3.fromRGB(0, 0, 0) + dimensions.BorderSizePixel = 1 + dimensions.BackgroundColor3 = Color3.fromRGB(255, 255, 255) + dimensions.BackgroundTransparency = 0.2 + dimensions.ZIndex = PICKER_ZINDEX + 1 + + return highlightArea +end + +local InstancePicker = class("InstancePicker", function(debugInterface, onSelect) + return { + debugInterface = debugInterface, + onSelect = onSelect, + active = false, + connection = nil, + selectedObject = nil, + pickerArea = createPickerArea(), + highlightArea = createHighlightArea(), + selectedTime = os.clock(), + nextPosition = ZERO_UDIM2, + nextSize = ZERO_UDIM2, + prevPosition = ZERO_UDIM2, + prevSize = ZERO_UDIM2, + } +end) + +function InstancePicker:getRoot() + return self.debugInterface.rootInstance +end + +function InstancePicker:getPickerParent() + return self.debugInterface.pickerParent or self:getRoot() +end + +function InstancePicker:getRelativeMousePosition() + local root = self:getRoot() + if not root then + return nil + end + if root:IsA("PluginGui") then + return root:GetRelativeMousePosition() + else + local mouse = game:GetService("Players").LocalPlayer:GetMouse() + return Vector2.new(mouse.X, mouse.Y) + end +end + +function InstancePicker:_init() + self.pickerArea.Activated:Connect(function() + self.onSelect(self.selectedObject) + end) +end + +function InstancePicker:setActive(active: boolean) + self.active = active + local root = self:getRoot() + if not root then + return + end + if active then + self.pickerArea.Parent = self:getPickerParent() + local onCalculate + onCalculate = function() + if not self.active then + return + end + self:calculateFramePosition() + delay(PICK_DELAY, onCalculate) + end + onCalculate() + self.connection = RunService.RenderStepped:Connect(function() + self:updateFrame() + end) + elseif self.connection then + self.connection:Disconnect() + self.connection = nil + self.pickerArea.Parent = nil + end +end + +function InstancePicker:updateFrame() + local progress = math.min(1, (os.clock() - self.selectedTime) / 0.1) + self.pickerArea.Position = tweenUDim2(self.prevPosition, self.nextPosition, progress) + self.pickerArea.Size = tweenUDim2(self.prevSize, self.nextSize, progress) +end + +function InstancePicker:intersectMouse(root: Instance, mousePosition: Vector2) + local descendants = root:GetDescendants() + local result = reduce(descendants, function(current, instance: Instance) + if instance == self.pickerArea then + return current + end + local intersects = false + if instance:IsA("GuiBase2d") then + local mousePosition = mousePosition - instance.AbsolutePosition + intersects = mousePosition.X >= 0 + and mousePosition.X <= instance.AbsoluteSize.X + and mousePosition.Y >= 0 + and mousePosition.Y <= instance.AbsoluteSize.Y + end + if instance:IsA("GuiObject") and not instance.Visible then + intersects = false + end + if intersects then + local area = instance.AbsoluteSize.X * instance.AbsoluteSize.Y + if current.minArea > area then + current.minArea = area + current.instance = instance + end + end + return current + end, { + minArea = math.huge + }) + return result.instance +end + +function InstancePicker:calculateFramePosition() + local root = self:getRoot() + local mousePosition = self:getRelativeMousePosition() + if not root or not mousePosition then + return + end + if mousePosition ~= self.mousePosition then + self.mousePosition = mousePosition + local foundObject = self:intersectMouse(root, mousePosition) + if foundObject then + if foundObject ~= self.selectedObject then + self.selectedObject = foundObject + self.prevPosition = UDim2.fromOffset(self.pickerArea.AbsolutePosition.X, self.pickerArea.AbsolutePosition.Y) + self.prevSize = UDim2.fromOffset(self.pickerArea.AbsoluteSize.X, self.pickerArea.AbsoluteSize.Y) + self.nextPosition = UDim2.fromOffset(foundObject.AbsolutePosition.X, foundObject.AbsolutePosition.Y) + self.nextSize = UDim2.fromOffset(foundObject.AbsoluteSize.X, foundObject.AbsoluteSize.Y) + self.selectedTime = os.clock() + end + else + self.selectedObject = nil + end + end +end + +function InstancePicker:highlight(instance: Instance) + -- Some instances may not have sizes so guard for error + pcall(function() + self.highlightArea.Parent = self:getPickerParent() + self.highlightArea.Position = UDim2.fromOffset(instance.AbsolutePosition.X, instance.AbsolutePosition.Y) + self.highlightArea.Size = UDim2.fromOffset(instance.AbsoluteSize.X, instance.AbsoluteSize.Y) + local dimensions = self.highlightArea:FindFirstChild("Dimensions") + dimensions.Text = format("{X}x{Y}", instance.AbsoluteSize) + end) +end + +function InstancePicker:dehighlight() + self.highlightArea.Parent = nil +end + +function InstancePicker:destroy() + if self.connection then + self.connection:Disconnect() + end + self.pickerArea:Destroy() +end + +return InstancePicker \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Classes/RoactInspectorApi.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Classes/RoactInspectorApi.lua new file mode 100644 index 0000000..52de268 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Classes/RoactInspectorApi.lua @@ -0,0 +1,96 @@ +--[[ + The RoactInspectorApi class manages the interface used by the inspector to discover and get + notified of changes to the Roact tree it is attached to. +]] +local Source = script.Parent.Parent.Parent +local Packages = Source.Parent + +local EventName = require(Source.EventName) +local TargetApi = require(Source.Classes.TargetApi) + +local Dash = require(Packages.Dash) +local append = Dash.append + +local insert = table.insert + +local RoactInspectorApi = TargetApi:extend("RoactInspectorApi") + +type Path = Types.Array + +function RoactInspectorApi:attach(handlers) + self.handlers = handlers + TargetApi.attach(self) + self:_connect({ + eventName = EventName.RoactInspector.ShowChildren, + onEvent = function(message) + self.handlers.onUpdateInstances(message.path, message.children, message.updatedIndexes) + end + }) + self:_connect({ + eventName = EventName.RoactInspector.ShowBranch, + onEvent = function(message) + self.handlers.onUpdateBranch(message.path, message.branch) + end + }) + self:_connect({ + eventName = EventName.RoactInspector.ShowFields, + onEvent = function(message) + self.handlers.onUpdateFields(message.path, message.nodeIndex, message.fieldPath, message.fields) + end + }) + self:_connect({ + eventName = EventName.RoactInspector.PickInstance, + onEvent = function(message) + self.handlers.onPickInstance(message.path) + end + }) +end + +function RoactInspectorApi:getChildren(path: Path) + self:_send({ + eventName = EventName.RoactInspector.GetChildren, + path = path + }) +end + +function RoactInspectorApi:getRoot() + self:getChildren({}) +end + +function RoactInspectorApi:getBranch(path: Path) + self:_send({ + eventName = EventName.RoactInspector.GetBranch, + path = path + }) +end + +function RoactInspectorApi:getFields(path: Path, nodeIndex: number, fieldPath: Path) + self:_send({ + eventName = EventName.RoactInspector.GetFields, + path = path, + nodeIndex = nodeIndex, + fieldPath = fieldPath + }) +end + +function RoactInspectorApi:setPicking(isPicking: boolean) + self:_send({ + eventName = EventName.RoactInspector.SetPicking, + isPicking = isPicking + }) +end + +function RoactInspectorApi:highlight(path: Path) + self:_send({ + eventName = EventName.RoactInspector.Highlight, + path = path + }) +end + +function RoactInspectorApi:dehighlight() + self:_send({ + eventName = EventName.RoactInspector.Dehighlight, + }) +end + +return RoactInspectorApi \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Classes/RoactInspectorWorker.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Classes/RoactInspectorWorker.lua new file mode 100644 index 0000000..437ee62 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Classes/RoactInspectorWorker.lua @@ -0,0 +1,228 @@ +--[[ + The RoactInspectorWorker class manages the serialization of a roact tree across the bridge, + notifies the inspector of any changes to the tree, and allows the inspector to make + modifications to the tree. +]] +local Source = script.Parent.Parent.Parent +local Packages = Source.Parent + +local getChildAtKey = require(Source.RoactInspector.Utils.getChildAtKey) + +local TargetWorker = require(Source.Classes.TargetWorker) +local EventName = require(Source.EventName) +local RoactTreeWatcher = require(Source.RoactInspector.Classes.RoactTreeWatcher) +local InstancePicker = require(Source.RoactInspector.Classes.InstancePicker) +local FieldWatcher = require(Source.RoactInspector.Classes.FieldWatcher) + +local Selection = game:GetService("Selection") + +local Dash = require(Packages.Dash) +local Types = Dash.Types +local map = Dash.map +local forEach = Dash.forEach +local last = Dash.last +local pretty = Dash.pretty +local reduce = Dash.reduce + +local insert = table.insert + +type Path = Types.Array + +local RoactInspectorWorker = TargetWorker:extend("RoactInspectorWorker", function(debugInterface, targetId, toBridgeId, tree) + local worker = TargetWorker.new(debugInterface, targetId, toBridgeId) + worker.tree = tree + return worker +end) + +function RoactInspectorWorker:_init() + self.treeWatcher = RoactTreeWatcher.new(self.debugInterface, self.tree, function(changedPath, changedIndexes) + self:showChildren(changedPath, changedIndexes) + end) + self.treeWatcher:monitor() + + self.fieldWatcher = FieldWatcher.new(function(changedPaths) + self:showFields({}) + end) + + self.picker = InstancePicker.new(self.debugInterface, function(instance) + return self:pickInstance(instance) + end) + + self:connectEvents() +end + +function RoactInspectorWorker:connectEvents() + TargetWorker.connectEvents(self) + self:connect({ + eventName = EventName.RoactInspector.GetChildren, + onEvent = function(message) + self:showChildren(message.path) + end + }) + self:connect({ + eventName = EventName.RoactInspector.GetBranch, + onEvent = function(message) + self:showBranch(message.path) + end + }) + self:connect({ + eventName = EventName.RoactInspector.GetFields, + onEvent = function(message) + self.currentPath = message.path + self.currentNodeIndex = message.nodeIndex + self:showFields(message.fieldPath or {}) + end + }) + self:connect({ + eventName = EventName.RoactInspector.Highlight, + onEvent = function(message) + local node = self.treeWatcher:getNode(message.path) + if not node then + return + end + local hostNode = self.treeWatcher:getHostNode(node) + if hostNode and hostNode.hostObject then + self.picker:highlight(hostNode.hostObject) + else + self.picker:dehighlight() + end + end + }) + self:connect({ + eventName = EventName.RoactInspector.Dehighlight, + onEvent = function(message) + self.picker:dehighlight() + end + }) + self:connect({ + eventName = EventName.RoactInspector.SetPicking, + onEvent = function(message) + self.picker:setActive(message.isPicking) + end + }) +end + +function RoactInspectorWorker:getNodeInfo(node) + local source = "" + local link = "" + if node.currentElement.source then + local lines = node.currentElement.source:split("\n") + if lines[1] then + source = lines[1] + link = lines[1]:match("[A-Za-z0-9_]+%.[A-Za-z0-9_]+:[0-9]+") or "" + end + end + return { + Name = self.treeWatcher:getNodeName(node), + Source = source, + Link = link, + Icon = self.treeWatcher:getNodeIcon(node) + } +end + +function RoactInspectorWorker:pickInstance(instance: Instance) + self.picker:setActive(false) + + local path = self.treeWatcher:getPath(instance) + local currentPath = {} + + -- Gather all the children for these instances and update + forEach(path, function(key) + insert(currentPath, key) + self:showChildren(currentPath) + end) + + -- Pick the instance based on the id path to reach it + self:send({ + eventName = EventName.RoactInspector.PickInstance, + path = path + }) + + self:showBranch(path) +end + +function RoactInspectorWorker:showChildren(path, updatedIndexes: Types.Array?) + local node = self.treeWatcher:getNode(path) + if not node then + warn("[DeveloperInspector - Roact] Missing path " .. pretty(path)) + return + end + self.treeWatcher:watchPath(path) + -- Generate at depth two to get grandchildren, so tree rows with children + -- display with a toggle arrow. + local children = self.treeWatcher:getChildren(path, node, 2) + self:send({ + eventName = EventName.RoactInspector.ShowChildren, + path = path, + children = children, + updatedIndexes = updatedIndexes + }) +end + +function RoactInspectorWorker:showBranch(path) + local nodes = self.treeWatcher:getNodes(path) + if not nodes then + return + end + local hostNode = last(nodes) + if hostNode and hostNode.hostObject then + Selection:Set({hostNode.hostObject}) + end + local branch = map(nodes, function(node) + return self:getNodeInfo(node) + end) + self:send({ + eventName = EventName.RoactInspector.ShowBranch, + path = path, + branch = branch + }) +end + +function RoactInspectorWorker:showFields(fieldPath: Path) + local nodes = self.treeWatcher:getNodes(self.currentPath) + if not nodes then + return + end + local node = nodes[self.currentNodeIndex] + if not node then + return + end + local container = node.instance or node.currentElement + + self.fieldWatcher:setRoot(container) + self.fieldWatcher:addPath(fieldPath) + + -- Safely walk through the container to the correct descendant + local fieldRoot = reduce(fieldPath, function(table, key) + local ok, child = pcall(function() + return getChildAtKey(table, key) + end) + if ok then + return child + else + return nil + end + end, container) + + if fieldRoot == nil then + return + end + + self:send({ + eventName = EventName.RoactInspector.ShowFields, + path = self.currentPath, + nodeIndex = self.currentNodeIndex, + fieldPath = fieldPath, + fields = self.fieldWatcher:collect(fieldRoot, 2, fieldPath) + }) +end + +function RoactInspectorWorker:destroy() + TargetWorker.destroy(self) + self.picker:destroy() + self.treeWatcher:destroy() + self.fieldWatcher:destroy() + self.treeWatcher = nil +end + +return RoactInspectorWorker \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Classes/RoactTreeWatcher.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Classes/RoactTreeWatcher.lua new file mode 100644 index 0000000..1c6e94e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Classes/RoactTreeWatcher.lua @@ -0,0 +1,359 @@ +--[[ + The RoactTreeWatcher class walks a roact tree and watches for any changes to the nodes. +]] +local Source = script.Parent.Parent.Parent +local Packages = Source.Parent +local getSymbol = require(Source.RoactInspector.Utils.getSymbol) +local getChildAtKey = require(Source.RoactInspector.Utils.getChildAtKey) + +local Dash = require(Packages.Dash) +local Types = Dash.Types +local append = Dash.append +local class = Dash.class +local collectArray = Dash.collectArray +local pick = Dash.pick +local keys = Dash.keys +local last = Dash.last +local map = Dash.map +local mapOne = Dash.mapOne +local reduce = Dash.reduce +local reverse = Dash.reverse +local slice = Dash.slice + +local insert = table.insert + +type Path = Types.Array + +local RoactTreeWatcher = class("RoactTreeWatcher", function(debugInterface, tree, onPathChanged) + return { + debugInterface = debugInterface, + tree = tree, + onPathChanged = onPathChanged, + cachedRoot = { + branchData = {}, + childNodes = {} + } + } +end) + +local POLL_DELAY = 0.25 + +function RoactTreeWatcher:_init() + -- A periodic check for changes + self.onPoll = function() + if self.tree then + self:_checkNodes() + delay(POLL_DELAY, self.onPoll) + end + end +end + +function RoactTreeWatcher:getRootNode() + return getSymbol(self.tree, "InternalData").rootNode +end + +-- Start polling for changes +function RoactTreeWatcher:monitor() + spawn(self.onPoll) +end + +function RoactTreeWatcher:_checkNodes() + local root = self:getRootNode() + self:_checkNode(root, self.cachedRoot, {}) +end + +function RoactTreeWatcher:_checkNode(node, cachedNode, path) + local branch = self:_getBranchNodes(node) + local updatedIndexes = collectArray(branch, function(index, branchNode) + local nodeData = cachedNode.branchData[index] + local container = branchNode.instance or branchNode.currentElement + if not nodeData or container.props ~= nodeData.props or container.state ~= nodeData.state then + cachedNode.branchData[index] = { + props = container.props, + state = container.state + } + return index + end + end) + if #updatedIndexes > 0 then + self.onPathChanged(path, updatedIndexes) + end + local hostNode = self:getHostNode(node) + cachedNode.childNodes = pick(cachedNode.childNodes, function(cachedChildNode, key) + local childPath = append({}, path, {key}) + local childNode = getChildAtKey(hostNode.children, key) + if childNode then + self:_checkNode(childNode, cachedChildNode, childPath) + return true + else + -- The node has been removed + return false + end + end) +end + +function RoactTreeWatcher:getNode(path: Path) + local root = self:getRootNode() + local hostNode = self:getHostNode(root) + -- Walk down the node tree by the path provided + return reduce(path, function(node, key: any) + if node and node.children then + local childNode = getChildAtKey(node.children, key) + return childNode and self:getHostNode(childNode) or nil + else + return nil + end + end, hostNode) +end + +function RoactTreeWatcher:watchPath(path: Path) + local node = self:getRootNode() + -- Store references to the current nodes in the tree in a nested structure keyed by the path. + -- If the data in these nodes change reference value in the source tree, we know that they + -- have been updated and re-rendered. + local cachedNode = self.cachedRoot + cachedNode.branchData = self:_getBranchData(node) + reduce(path, function(currentNode, key: any) + if not currentNode then + return nil + end + cachedNode.childNodes[key] = cachedNode.childNodes[key] or { + childNodes = {} + } + cachedNode = cachedNode.childNodes[key] + local childNode = getChildAtKey(currentNode.children, key) + if not childNode then + return nil + end + cachedNode.branchData = self:_getBranchData(childNode) + return self:getHostNode(childNode) + end, self:getHostNode(node)) +end + +function RoactTreeWatcher:_getBranchData(topNode) + local branch = self:_getBranchNodes(topNode) + return map(branch, function(node) + local container = node.instance or node.currentElement + return { + props = container.props, + state = container.state + } + end) +end + +function RoactTreeWatcher:getChildren(path: Path, node, depth: number) + -- Return nil to indicate the tree has been truncated, rather that there are 0 children. + if depth == 0 then + return nil + end + local hostNode = self:getHostNode(node) + if not hostNode then + return nil + end + local children = map(hostNode.children, function(child, key) + local childPath = append({}, path, {key}) + local hostObject = self:getHostNode(child).hostObject + local icon = hostObject and hostObject.ClassName or "Branch" + return { + Name = typeof(key) == "number" and key or tostring(key), + Icon = icon, + Children = self:getChildren(childPath, child, depth - 1), + Path = childPath + } + end) + return children +end + +-- Head to the bottom of the current branch, optionally moving through any fragments or portals. +function RoactTreeWatcher:getHostNode(node, jumpBranch: boolean?) + local child = getSymbol(node.children, "UseParentKey") + while child do + node = child + child = getSymbol(node.children, "UseParentKey") + -- If node isn't in branch, try jumping to the next one + if not child and jumpBranch then + if self:isFragment(node) then + child = node.children[1] + elseif self:isPortal(node) then + child = mapOne(node.children) + end + end + end + return node +end + +function RoactTreeWatcher:getRootPath() + local node = self:getRootNode() + local child = getSymbol(node.children, "UseParentKey") + local path = {} + while child do + node = child + child = getSymbol(node.children, "UseParentKey") + -- If node isn't in branch, try jumping to the next one + if not child then + if self:isFragment(node) then + child = node.children[1] + insert(path, 1) + elseif self:isPortal(node) then + local childKeys = keys(node.children) + insert(path, childKeys[1]) + child = node.children[childKeys[1]] + end + end + end + return slice(path, 0, -1) +end + +function RoactTreeWatcher:isFragment(node) + return node.currentElement and node.currentElement.elements +end + +function RoactTreeWatcher:isPortal(node) + return node.currentElement and tostring(node.currentElement.component) == "Symbol(Portal)" +end + +function RoactTreeWatcher:isFunction(node) + return node.currentElement and typeof(node.currentElement.component) == "function" +end + +function RoactTreeWatcher:isHost(node) + return node.currentElement and typeof(node.currentElement.component) == "string" +end + +function RoactTreeWatcher:getNodes(path: Path) + -- Split last key off from path + local frontPath = slice(path, 1, -1) + local lastKey = last(path) + -- Get the parent of the path provided + local node = self:getNode(frontPath) + if not node then + return nil + end + local currentChild = getChildAtKey(node.children, lastKey) + return self:_getBranchNodes(currentChild) +end + +function RoactTreeWatcher:_getBranchNodes(node) + -- Build up the node branch by walking through elements which use the parent key + local branch = {} + while node do + insert(branch, node) + node = getSymbol(node.children, "UseParentKey") + end + return branch +end + +function RoactTreeWatcher:getNodeName(node) + if self:isFragment(node) then + return "Fragment" + elseif self:isPortal(node) then + return "Portal" + elseif self:isFunction(node) then + return tostring(node.currentElement.component) + elseif self:isHost(node) then + return node.currentElement.component + else + return node.currentElement.component.__componentName + end +end + +function RoactTreeWatcher:getNodeIcon(node) + if self:isFragment(node) then + return "Fragment" + elseif self:isPortal(node) then + return "Portal" + elseif self:isFunction(node) then + return "Functional" + elseif self:isHost(node) then + return node.hostObject.ClassName + else + local componentName = node.currentElement.component.__componentName + if componentName:find("Provider") then + return "Provider" + elseif componentName:find("Consumer") or componentName:find("RoduxConnection") then + return "Consumer" + elseif node.currentElement.component.shouldUpdate then + return "Pure" + else + return "Stateful" + end + end +end + +function RoactTreeWatcher:getPath(instance: Instance) + local instancePath = self:_getInstancePath(instance) + local fullPath = self:_getFullPath(instancePath) + return fullPath +end + +function RoactTreeWatcher:_getInstancePath(instance: Instance) + local reversePath = {} + while instance and instance ~= self.debugInterface.rootInstance do + -- Always prefer a number if there is a representation, as it is easier to convert back + -- later on if need be. + local numberName = tonumber(instance.Name) + insert(reversePath, numberName or instance.Name) + instance = instance.Parent + end + local fullPath = append({}, self.debugInterface.rootPath or self:getRootPath(), reverse(reversePath)) + if self.debugInterface.rootPrefix then + -- Remove the prefix from the computed path + local strippedPath = slice(fullPath, #self.debugInterface.rootPrefix + 1) + return strippedPath + else + return fullPath + end +end + +-- Returns the full path including virutal nodes given a path +-- that includes only the nodes with Roblox Instances +function RoactTreeWatcher:_getFullPath(instancePath: Path) + local fullPath = {} + local root = self:getRootNode() + local hostNode = self:getHostNode(root) + + local found = reduce(instancePath, function(node, key: any) + return self:_dfsFindNextChildNode(node, key, fullPath) + end, hostNode) + + if found ~= nil then + return fullPath + else + return instancePath + end +end + +function RoactTreeWatcher:_dfsFindNextChildNode(node, key: any, fullPath: Path) + if node == nil or node.children == nil then + return nil + end + + local childNode = getChildAtKey(node.children, key) + if childNode then + local hostNode = self:getHostNode(childNode) + if hostNode ~= nil then + insert(fullPath, key) + return hostNode + end + end + + for childKey, childNode in pairs(node.children) do + local useParentKey = tostring(childKey) == "Symbol(UseParentKey)" + if not useParentKey then + insert(fullPath, childKey) + end + local found = self:_dfsFindNextChildNode(childNode, key, fullPath) + if found ~= nil then + return found + end + if not useParentKey then + table.remove(fullPath) + end + end +end + +function RoactTreeWatcher:destroy() + self.tree = nil +end + +return RoactTreeWatcher diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Types.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Types.lua new file mode 100644 index 0000000..0f0e1db --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Types.lua @@ -0,0 +1,7 @@ +export type SerializedInstance = { + id: number; + name: string; + className: string; +} + +return {} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Utils/getChildAtKey.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Utils/getChildAtKey.lua new file mode 100644 index 0000000..80a274c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Utils/getChildAtKey.lua @@ -0,0 +1,25 @@ +local Source = script.Parent.Parent.Parent +local Packages = Source.Parent +local getSymbol = require(Source.RoactInspector.Utils.getSymbol) + +local Dash = require(Packages.Dash) +local startsWith = Dash.startsWith + +local function getChildAtKey(object, key) + if object == nil then + return nil + end + local child = object[key] + if child == nil and typeof(key) == "number" then + -- Try a string representation of a numeric key if need be + child = object[tostring(key)] + end + if child == nil and typeof(key) == "string" and startsWith(key, "Symbol(") then + -- Strip Symbol() from the key + local symbolName = key:sub(8, -2) + child = getSymbol(object, symbolName) + end + return child +end + +return getChildAtKey \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Utils/getSymbol.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Utils/getSymbol.lua new file mode 100644 index 0000000..0c83ccc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/Utils/getSymbol.lua @@ -0,0 +1,15 @@ +local foundSymbols = {} + +local function getSymbol(object, name) + if foundSymbols[name] then + return object[foundSymbols[name]] + end + for key, value in pairs(object) do + if tostring(key) == "Symbol(" .. name .. ")" then + foundSymbols[name] = key + return value + end + end +end + +return getSymbol \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/init.lua new file mode 100644 index 0000000..519f2df --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/RoactInspector/init.lua @@ -0,0 +1,3 @@ +local RoactInspectorWorker = require(script.Classes.RoactInspectorWorker) + +return RoactInspectorWorker \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Types.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Types.lua new file mode 100644 index 0000000..835692d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/Types.lua @@ -0,0 +1,14 @@ +export type BridgeMessage = { + eventName: string; + fromBridgeId: string?; + fromTargetId: string?; + toBridgeId: string?; + toTargetId: string?; +} + +export type BridgeListener = { + eventName: string?; + bridgeId: string?; + targetId: string?; + onEvent: (BridgeMessage) -> (); +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/init.lua new file mode 100644 index 0000000..613d6a7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/init.lua @@ -0,0 +1,56 @@ +--[[ + The entry point for the DeveloperTools library. + + These static methods each create a new DebugInterface that provides a method of communication + to the Developer Inspector plugin from the information source you are trying to inspect. + + Each interface uses a slightly different way to communicate based on the current level of + security and ability to use built-in services. +]] +local PluginDebugInterface = require(script.Classes.PluginDebugInterface) +local CoreGuiDebugInterface = require(script.Classes.CoreGuiDebugInterface) +local StandalonePluginDebugInterface = require(script.Classes.StandalonePluginDebugInterface) +local LibraryDebugInterface = require(script.Classes.LibraryDebugInterface) +local InspectorDebugInterface = require(script.Classes.InspectorDebugInterface) +local RoactInspectorApi = require(script.RoactInspector.Classes.RoactInspectorApi) + +local helpfulErrorMessage = "%s must be a string, did you write DeveloperTools:%s() instead of DeveloperTools.%s() by mistake?" + +export type GuiOptions = { + rootInstance: Instance?; + pickerParent: Instance?; +} + +return { + -- Get an inspector instance for a local, installed or built-in plugin + forPlugin = function(pluginName: string, plugin) + assert(typeof(pluginName) == "string", helpfulErrorMessage:format(pluginName, "forPlugin", "forPlugin")) + assert(plugin, "DeveloperTools:forPlugin() expected plugin for argument #2") + return PluginDebugInterface.new(pluginName, plugin) + end, + -- Get an inspector instance for a standalone plugin + forStandalonePlugin = function(pluginName: string, plugin, rootInstance) + assert(typeof(pluginName) == "string", helpfulErrorMessage:format(pluginName, "forStandalonePlugin", "forStandalonePlugin")) + assert(plugin, "DeveloperTools:forStandalonePlugin() expected plugin for argument #2") + return StandalonePluginDebugInterface.new(pluginName, plugin, rootInstance) + end, + -- Get an inspector instance for an interface placed in the CoreGui + forCoreGui = function(appName: string, guiOptions: GuiOptions) + assert(typeof(appName) == "string", helpfulErrorMessage:format(appName, "appName", "appName")) + return CoreGuiDebugInterface.new(appName, guiOptions) + end, + -- Get an inspector instance for a library, such as UniversalApps or a set of ModuleScripts + -- which are stored in ReplicatedStorage + forLibrary = function(libraryName: string, guiOptions: GuiOptions) + assert(typeof(libraryName) == "string", helpfulErrorMessage:format(libraryName, "libraryName", "libraryName")) + return LibraryDebugInterface.new(libraryName, guiOptions) + end, + -- Used by the Developer Inspector plugin itself to communicate with all available channels + forInspector = function(handlers) + return InspectorDebugInterface.new(handlers) + end, + -- A reference to the RoactInspectorApi class. + -- You can use the static isInstance method to check if the current api is a RoactInspectorApi + -- i.e. RoactInspectorApi.isInstance(inspector:getCurrentApi()) + RoactInspectorApi = RoactInspectorApi +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/init.spec.lua new file mode 100644 index 0000000..eb013ac --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/developer-tools/init.spec.lua @@ -0,0 +1,22 @@ +return function() + local DeveloperTools = require(script.Parent) + + local Dash = require(script.Parent.Parent.Dash) + local keys = Dash.keys + + describe("DeveloperTools", function() + describe("Roact inspecting", function() + it("can be initialized for a plugin", function() + local inspector = DeveloperTools.forPlugin("TestPlugin", {}) + inspector:addRoactTree("Roact tree", {}) + expect(#keys(inspector.targets)).to.equal(1) + end) + it("can be initialized for a library", function() + local inspector = DeveloperTools.forLibrary("TestLibrary", {}) + inspector:addRoactTree("Roact tree", {}) + expect(#keys(inspector.targets)).to.equal(1) + end) + end) + end) + +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/lock.toml new file mode 100644 index 0000000..02dccc9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_developer-tools/lock.toml @@ -0,0 +1,6 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/developer-tools" +version = "0.1.4" +commit = "90266d909da3b9264eb893a3761d80d97d2d3f4f" +source = "url+https://github.com/roblox/developer-tools" +dependencies = ["Dash roblox/dash 0.1.7 url+https://github.com/roblox/dash"] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/enumerate.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/enumerate.lua new file mode 100644 index 0000000..ab1305e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/enumerate.lua @@ -0,0 +1,100 @@ +local function strict(t, name) + name = name or tostring(t) + + return setmetatable(t, { + __index = function(self, key) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + + __newindex = function(self, key, value) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + }) +end + +local function addEnumValue(enumInternal, enumInternalRawValues, enumName, valueName, rawValue) + local isValueNameString = typeof(valueName) == "string" + assert(isValueNameString, "Only string names are supported for enums!") + if isValueNameString then + assert(valueName ~= "fromRawValue", "fromRawValue is reserved") + assert(valueName ~= "isEnumValue", "isEnumValue is reserved") + end + assert(enumInternal[valueName] == nil, "Enum value names can only be used once!") + assert(enumInternalRawValues[valueName] == nil, "Enum values can only be used once!") + + local value = newproxy(true) + local valueMetatable = getmetatable(value) + + valueMetatable.__tostring = function() + return ("%s.%s"):format(enumName, valueName) + end + + valueMetatable.__index = strict({ + rawValue = function() + return rawValue + end + }) + + enumInternal[valueName] = value + enumInternalRawValues[rawValue] = value +end + +local function enumerate(enumName, values) + assert(typeof(enumName) == "string", "Bad argument #1 - enums must be created using a string name!") + assert(typeof(values) == "table", "Bad argument #2 - enums must be created using a table!") + + local enumInternal = {} + local enumInternalRawValues = {} + + -- Allow a list-like syntax for string enums for convenience + if values[1] ~= nil then + for _, valueName in ipairs(values) do + addEnumValue(enumInternal, enumInternalRawValues, enumName, valueName, valueName) + end + else + for valueName, rawValue in pairs(values) do + addEnumValue(enumInternal, enumInternalRawValues, enumName, valueName, rawValue) + end + end + + function enumInternal.fromRawValue(rawValue) + return enumInternalRawValues[rawValue] + end + + function enumInternal.isEnumValue(value) + if typeof(value) ~= "userdata" then + return false + end + + for _, enumValue in pairs(enumInternal) do + if enumValue == value then + return true + end + end + + return nil + end + + local enum = newproxy(true) + local meta = getmetatable(enum) + meta.__index = strict(enumInternal, enumName) + meta.__tostring = function() + return enumName + end + + return enum +end + +return enumerate \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/enumerate.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/enumerate.spec.lua new file mode 100644 index 0000000..9272b65 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/enumerate.spec.lua @@ -0,0 +1,69 @@ +return function() + local enumerate = require(script.Parent.enumerate) + + describe("Any Enum", function() + local AnyEnum = enumerate("AnyEnum", { "A", "B", "C" }) + + it("should be valid", function() + expect(AnyEnum).to.be.ok() + end) + + it("should have the correct name", function() + expect(tostring(AnyEnum)).to.be.equal("AnyEnum") + end) + + it("cannot be altered", function() + expect(function() + AnyEnum.A = "B" + end).to.throw() + end) + + it("should error when accessing an invalid value", function() + expect(function() + local _ = AnyEnum.D + end).to.throw() + end) + + it("should have values that can be compared for equality", function() + expect(AnyEnum.A).to.equal(AnyEnum.A) + expect(AnyEnum.A == AnyEnum.B).to.equal(false) + end) + + it("should have userdata values", function() + expect(typeof(AnyEnum.A)).to.equal("userdata") + end) + + it("should have values with correct rawValue types", function() + expect(typeof(AnyEnum.A.rawValue())).to.equal("string") + end) + + it("should have values with a useful name", function() + expect(tostring(AnyEnum.A)).to.equal("AnyEnum.A") + end) + + it("should have values with correct rawValues", function() + expect(AnyEnum.A.rawValue()).to.equal("A") + end) + + it("should return the correct value from a rawValue", function() + expect(AnyEnum.fromRawValue("A")).to.equal(AnyEnum.A) + end) + + it("should detect whether a value is an enum value", function() + expect(AnyEnum.isEnumValue(AnyEnum.A)).to.equal(true) + expect(AnyEnum.isEnumValue("A")).to.equal(false) + end) + end) + + it("should error when creating an enum with a non-string name", function() + expect(function() + enumerate(1, { "A", "B", "C" }) + end).to.throw() + end) + + it("should error when creating an enum with duplicate values", function() + expect(function() + enumerate(1, { "A", "B", "C", "C" }) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/init.lua new file mode 100644 index 0000000..7edae60 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/init.lua @@ -0,0 +1 @@ +return require(script.enumerate) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/init.spec.lua new file mode 100644 index 0000000..2a950f0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/enumerate/init.spec.lua @@ -0,0 +1,7 @@ +return function() + local enumerate = require(script.Parent) + + it("should load a function", function() + expect(typeof(enumerate)).to.equal("function") + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/lock.toml new file mode 100644 index 0000000..5bad4c9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_enumerate/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/enumerate" +version = "1.0.0" +commit = "48daaf0df47eaf36c154d691a8320239e4a1312e" +source = "url+https://github.com/roblox/enumerate" diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/Promise.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/Promise.lua new file mode 100644 index 0000000..5dea2b4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/Promise.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["lua-promise"]["lua-promise"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/LinkedList.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/LinkedList.lua new file mode 100644 index 0000000..a34ea50 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/LinkedList.lua @@ -0,0 +1,51 @@ +local LinkedList = {} +LinkedList.__index = LinkedList + +LinkedList.createNode = function(value) + local node = { + previous = nil, + next = nil, + value = value, + } + + setmetatable(node, LinkedList) + + return node +end + +-- Inserts a new node between the 'self' node and its 'next' node. Can also be used to append a node to the end +function LinkedList:CreateNext(value) + local nextNode = { + previous = self, + next = self.next, + value = value or {}, + } + + if self.next then + self.next.previous = nextNode + end + + setmetatable(nextNode, LinkedList) + self.next = nextNode + return nextNode +end + + +-- Inserts a new node between the 'self' node and its 'previous' node. Can also prepend a node to the beginning +function LinkedList:CreatePrevious(value) + local previousNode = { + previous = self.previous, + next = self, + value = value or {}, + } + + if self.previous then + self.previous.next = previousNode + end + + setmetatable(previousNode, LinkedList) + self.previous = previousNode + return previousNode +end + +return LinkedList diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/LinkedList.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/LinkedList.spec.lua new file mode 100644 index 0000000..3294bdb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/LinkedList.spec.lua @@ -0,0 +1,66 @@ +return function() + local LinkedList = require(script.Parent.LinkedList) + + describe("it should initialize properly", function() + local node = LinkedList.createNode(456) + local emptyNode = LinkedList.createNode() + + it("given a value", function() + expect(node.value).to.equal(456) + end) + + it("given nothing", function() + expect(emptyNode.value).to.equal(nil) + end) + end) + + describe("it should properly link nodes", function() + it("when calling createNext on the end of a list", function() + local nodeA = LinkedList.createNode("a") + local nodeB = nodeA:CreateNext("b") + + expect(nodeA.next).to.equal(nodeB) + expect(nodeA.next.value).to.equal(nodeB.value) + + expect(nodeB.previous).to.equal(nodeA) + expect(nodeB.previous.value).to.equal(nodeA.value) + end) + + it("when calling createNext inbetween Nodes", function() + local nodeA = LinkedList.createNode("a") + local nodeC = nodeA:CreateNext("c") + local nodeB = nodeA:CreateNext("b") + + expect(nodeA.next).to.equal(nodeB) + expect(nodeB.previous).to.equal(nodeA) + expect(nodeB.next).to.equal(nodeC) + expect(nodeC.previous).to.equal(nodeB) + end) + + it("when calling createPrevious on the beginning of a list", function() + local nodeA = LinkedList.createNode("a") + local nodeZ = nodeA:CreatePrevious("z") + + expect(nodeA.previous).to.equal(nodeZ) + expect(nodeA.previous.value).to.equal(nodeZ.value) + + expect(nodeZ.next).to.equal(nodeA) + expect(nodeZ.next.value).to.equal(nodeA.value) + end) + + it("when calling createPrevious inbetween Nodes", function() + local nodeA = LinkedList.createNode("a") + local nodeY = nodeA:CreatePrevious("y") + local nodeZ = nodeA:CreatePrevious("z") + + expect(nodeA.previous).to.equal(nodeZ) + expect(nodeZ.previous).to.equal(nodeY) + expect(nodeZ.next).to.equal(nodeA) + expect(nodeY.next).to.equal(nodeZ) + end) + + + + end) + +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/Paginator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/Paginator.lua new file mode 100644 index 0000000..c544be8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/Paginator.lua @@ -0,0 +1,106 @@ +--[[ + Make sure to read api-reference.md for how to use. +--]] + +local dependencies = require(script.Parent.dependencies) +local t = dependencies.t +local LinkedList = dependencies.LinkedList +local Promise = dependencies.Promise + +local Paginator = { + llRoot = nil, + llIndex = nil, + _isFetching = false, +} +Paginator.__index = Paginator + +local requiredProps = t.strictInterface({ + pageSize = t.number, + fetchWithCursor = t.callback, + fetchInit = t.callback, +}) + +Paginator.new = function(props) + assert(requiredProps(props)) + local self = {} + for k, v in pairs(props) do + self[k] = v + end + setmetatable(self, Paginator) + + self:_init() + return self +end + +function Paginator:_init() + -- we don't know our currentCursor... just our previous and next + self._isFetching = true + return self.fetchInit():andThen(function(previousCursor, nextCursor) + self.llRoot = LinkedList.createNode() + self.llIndex = self.llRoot + self.llIndex:CreatePrevious(previousCursor) + self.llIndex:CreateNext(nextCursor) + self._isFetching = false + end) + +end + +function Paginator:getCurrent() + return self.llIndex.value or {} +end + +function Paginator:getNext() + if self._isFetching then + return Promise.reject("Paginator is currently busy. Please wait.") + end + + local cursor = self.llIndex.next.value or "" + + if cursor == "" then + return Promise.reject("Next cursor is invalid") + end + + self.llIndex = self.llIndex.next + self._isFetching = true + + return self.fetchWithCursor(cursor):andThen(function(previousCursor, nextCursor) + self.llIndex.previous.value = previousCursor + if self.llIndex.next then + self.llIndex.next.value = nextCursor + else + self.llIndex:CreateNext(nextCursor) + end + self._isFetching = false + end) +end + +function Paginator:getPrevious() + if self._isFetching then + return Promise.reject("Paginator is currently busy. Please wait.") + end + + local cursor = self.llIndex.previous.value or "" + + if cursor == "" then + return Promise.reject("Previous cursor is invalid") + end + + self.llIndex = self.llIndex.previous + self._isFetching = true + + return self.fetchWithCursor(cursor, true):andThen(function(previousCursor, nextCursor) + self.llIndex.next.value = nextCursor + if self.llIndex.previous then + self.llIndex.previous.value = previousCursor + else + self.llIndex:CreatePrevious(previousCursor) + end + self._isFetching = false + end) +end + +function Paginator:isFetching() + return self._isFetching +end + +return Paginator diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/Paginator.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/Paginator.spec.lua new file mode 100644 index 0000000..46e400a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/Paginator.spec.lua @@ -0,0 +1,200 @@ +local dependencies = require(script.Parent.dependencies) +local Promise = dependencies.Promise + +return function() + local Paginator = require(script.Parent.Paginator) + + describe("it should initialize properly", function() + local mockFetchInit = function(params) + return function() + return Promise.resolve():andThen(function() + return params-1, params+1 + end) + end + end + + local mockFetchWithCursor = function(cursor) + return Promise.resolve():andThen(function() + local newPrevCursor = cursor - 1 + local newNextCursor = cursor + 1 + return newPrevCursor, newNextCursor + end) + end + + local startCursor = 4 + + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + it("given proper values", function() + expect(paginator).to.be.ok() + end) + end) + + describe("it should return the correct cursors", function() + local mockFetchInit = function(params) + return function() + return Promise.resolve():andThen(function() + return params-1, params+1 + end) + end + end + + local mockFetchWithCursor = function(cursor) + return Promise.resolve():andThen(function() + local newPrevCursor = cursor - 1 + local newNextCursor = cursor + 1 + return newPrevCursor, newNextCursor + end) + end + + local startCursor = 4 + + it("when fetching next", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getNext() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor + 1) + end) + + it("when fetching previous", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getPrevious() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor - 1) + end) + + it("when fetching next then previous", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getNext() + paginator:getPrevious() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor) + end) + + it("when fetching previous then next", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getPrevious() + paginator:getNext() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor) + end) + + it("when fetching next next previous", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getPrevious() + paginator:getNext() + paginator:getNext() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor + 1) + end) + + it("when fetching previous previous next", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getPrevious() + paginator:getPrevious() + paginator:getNext() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor - 1) + end) + + it("when fetching previous next next", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getPrevious() + paginator:getNext() + paginator:getNext() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor + 1) + end) + + it("when fetching next previous previous", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getNext() + paginator:getPrevious() + paginator:getPrevious() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor - 1) + end) + + it("when fetching next next next", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getNext() + paginator:getNext() + paginator:getNext() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor + 3) + end) + + it("when fetching previous previous previous", function() + local paginator = Paginator.new({ + pageSize = 10, + fetchInit = mockFetchInit(startCursor), + fetchWithCursor = mockFetchWithCursor, + }) + + paginator:getPrevious() + paginator:getPrevious() + paginator:getPrevious() + local newCursor = paginator:getCurrent() + + expect(newCursor).to.equal(startCursor - 3) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/dependencies.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/dependencies.lua new file mode 100644 index 0000000..03130a7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/dependencies.lua @@ -0,0 +1,8 @@ +local ROOT = script.Parent +local Packages = script:FindFirstAncestor("Packages") + +return { + t = require(Packages.t), + LinkedList = require(ROOT.LinkedList), + Promise = require(Packages.Promise), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/init.lua new file mode 100644 index 0000000..4b6e3f8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/init.lua @@ -0,0 +1,8 @@ +-- Generator information: +-- Human name: GenericPagination +-- Variable name: GenericPagination +-- Repo name: genericpagination + +local Paginator = require(script.Paginator) + +return Paginator \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/inspect.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/inspect.lua new file mode 100644 index 0000000..656d247 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/inspect.lua @@ -0,0 +1,333 @@ +local inspect ={ + _VERSION = 'inspect.lua 3.1.0', + _URL = 'http://github.com/kikito/inspect.lua', + _DESCRIPTION = 'human-readable representations of tables', + _LICENSE = [[ + MIT LICENSE + + Copyright (c) 2013 Enrique García Cota + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ]] + } + + local tostring = tostring + + inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) + inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) + + local function rawpairs(t) + return next, t, nil + end + + -- Apostrophizes the string if it has quotes, but not aphostrophes + -- Otherwise, it returns a regular quoted string + local function smartQuote(str) + if str:match('"') and not str:match("'") then + return "'" .. str .. "'" + end + return '"' .. str:gsub('"', '\\"') .. '"' + end + + -- \a => '\\a', \0 => '\\0', 31 => '\31' + local shortControlCharEscapes = { + ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", + ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" + } + local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031 + for i=0, 31 do + local ch = string.char(i) + if not shortControlCharEscapes[ch] then + shortControlCharEscapes[ch] = "\\"..i + longControlCharEscapes[ch] = string.format("\\%03d", i) + end + end + + local function escape(str) + return (str:gsub("\\", "\\\\") + :gsub("(%c)%f[0-9]", longControlCharEscapes) + :gsub("%c", shortControlCharEscapes)) + end + + local function isIdentifier(str) + return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) + end + + local function isSequenceKey(k, sequenceLength) + return type(k) == 'number' + and 1 <= k + and k <= sequenceLength + and math.floor(k) == k + end + + local defaultTypeOrders = { + ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, + ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 + } + + local function sortKeys(a, b) + local ta, tb = type(a), type(b) + + -- strings and numbers are sorted numerically/alphabetically + if ta == tb and (ta == 'string' or ta == 'number') then return a < b end + + local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] + -- Two default types are compared according to the defaultTypeOrders table + if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] + elseif dta then return true -- default types before custom ones + elseif dtb then return false -- custom types after default ones + end + + -- custom types are sorted out alphabetically + return ta < tb + end + + -- For implementation reasons, the behavior of rawlen & # is "undefined" when + -- tables aren't pure sequences. So we implement our own # operator. + local function getSequenceLength(t) + local len = 1 + local v = rawget(t,len) + while v ~= nil do + len = len + 1 + v = rawget(t,len) + end + return len - 1 + end + + local function getNonSequentialKeys(t) + local keys, keysLength = {}, 0 + local sequenceLength = getSequenceLength(t) + for k,_ in rawpairs(t) do + if not isSequenceKey(k, sequenceLength) then + keysLength = keysLength + 1 + keys[keysLength] = k + end + end + table.sort(keys, sortKeys) + return keys, keysLength, sequenceLength + end + + local function countTableAppearances(t, tableAppearances) + tableAppearances = tableAppearances or {} + + if type(t) == 'table' then + if not tableAppearances[t] then + tableAppearances[t] = 1 + for k,v in rawpairs(t) do + countTableAppearances(k, tableAppearances) + countTableAppearances(v, tableAppearances) + end + countTableAppearances(getmetatable(t), tableAppearances) + else + tableAppearances[t] = tableAppearances[t] + 1 + end + end + + return tableAppearances + end + + local copySequence = function(s) + local copy, len = {}, #s + for i=1, len do copy[i] = s[i] end + return copy, len + end + + local function makePath(path, ...) + local keys = {...} + local newPath, len = copySequence(path) + for i=1, #keys do + newPath[len + i] = keys[i] + end + return newPath + end + + local function processRecursive(process, item, path, visited) + if item == nil then return nil end + if visited[item] then return visited[item] end + + local processed = process(item, path) + if type(processed) == 'table' then + local processedCopy = {} + visited[item] = processedCopy + local processedKey + + for k,v in rawpairs(processed) do + processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) + if processedKey ~= nil then + processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) + end + end + + local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) + if type(mt) ~= 'table' then mt = nil end -- ignore not nil/table __metatable field + setmetatable(processedCopy, mt) + processed = processedCopy + end + return processed + end + + + + ------------------------------------------------------------------- + + local Inspector = {} + local Inspector_mt = {__index = Inspector} + + function Inspector:puts(...) + local args = {...} + local buffer = self.buffer + local len = #buffer + for i=1, #args do + len = len + 1 + buffer[len] = args[i] + end + end + + function Inspector:down(f) + self.level = self.level + 1 + f() + self.level = self.level - 1 + end + + function Inspector:tabify() + self:puts(self.newline, string.rep(self.indent, self.level)) + end + + function Inspector:alreadyVisited(v) + return self.ids[v] ~= nil + end + + function Inspector:getId(v) + local id = self.ids[v] + if not id then + local tv = type(v) + id = (self.maxIds[tv] or 0) + 1 + self.maxIds[tv] = id + self.ids[v] = id + end + return tostring(id) + end + + function Inspector:putKey(k) + if isIdentifier(k) then return self:puts(k) end + self:puts("[") + self:putValue(k) + self:puts("]") + end + + function Inspector:putTable(t) + if t == inspect.KEY or t == inspect.METATABLE then + self:puts(tostring(t)) + elseif self:alreadyVisited(t) then + self:puts('
') + elseif self.level >= self.depth then + self:puts('{...}') + else + if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end + + local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t) + local mt = getmetatable(t) + + self:puts('{') + self:down(function() + local count = 0 + for i=1, sequenceLength do + if count > 0 then self:puts(',') end + self:puts(' ') + self:putValue(t[i]) + count = count + 1 + end + + for i=1, nonSequentialKeysLength do + local k = nonSequentialKeys[i] + if count > 0 then self:puts(',') end + self:tabify() + self:putKey(k) + self:puts(' = ') + self:putValue(t[k]) + count = count + 1 + end + + if type(mt) == 'table' then + if count > 0 then self:puts(',') end + self:tabify() + self:puts(' = ') + self:putValue(mt) + end + end) + + if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing } + self:tabify() + elseif sequenceLength > 0 then -- array tables have one extra space before closing } + self:puts(' ') + end + + self:puts('}') + end + end + + function Inspector:putValue(v) + local tv = type(v) + + if tv == 'string' then + self:puts(smartQuote(escape(v))) + elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or + tv == 'cdata' or tv == 'ctype' then + self:puts(tostring(v)) + elseif tv == 'table' then + self:putTable(v) + else + self:puts('<', tv, ' ', self:getId(v), '>') + end + end + + ------------------------------------------------------------------- + + function inspect.inspect(root, options) + options = options or {} + + local depth = options.depth or math.huge + local newline = options.newline or '\n' + local indent = options.indent or ' ' + local process = options.process + + if process then + root = processRecursive(process, root, {}, {}) + end + + local inspector = setmetatable({ + depth = depth, + level = 0, + buffer = {}, + ids = {}, + maxIds = {}, + newline = newline, + indent = indent, + tableAppearances = countTableAppearances(root) + }, Inspector_mt) + + inspector:putValue(root) + + return table.concat(inspector.buffer) + end + + setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) + + return inspect diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/publicApi.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/publicApi.spec.lua new file mode 100644 index 0000000..5dd2c1d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/genericpagination/publicApi.spec.lua @@ -0,0 +1,7 @@ +return function() + it("SHOULD return a valid, constructable object", function() + local api = require(script.Parent) + expect(api).to.be.ok() + expect(api.new).to.be.ok() + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/lock.toml new file mode 100644 index 0000000..9ed5f10 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/lock.toml @@ -0,0 +1,9 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/genericpagination" +version = "0.1.0" +commit = "885168d01a0de5e0753653746ff8b392dc1d8acc" +source = "git+https://github.com/roblox/genericpagination#master" +dependencies = [ + "Promise lua-promise 1bb842c3 git+https://github.com/roblox/lua-promise#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/t.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_genericpagination/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/FitFrame.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/FitFrame.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/FitFrame.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/Lumberyak.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/Lumberyak.lua new file mode 100644 index 0000000..7803fb4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/Lumberyak.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_lumberyak-6ce30d59-0.1.1"]["lumberyak"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/Otter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/.robloxrc b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/.robloxrc new file mode 100644 index 0000000..b261580 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/.robloxrc @@ -0,0 +1,5 @@ +{ + "language": { + "mode": "strict" + } +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/Distance.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/Distance.lua new file mode 100644 index 0000000..65f28a2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/Distance.lua @@ -0,0 +1,16 @@ +return { + -- Returns the signed distance from a point to a range. Returns 0 if the + -- point is within the range, positive if it's below and negative if it's + -- above. + fromPointToRangeSigned = function(point: number, rangeTop: number, rangeSize: number) + local rangeBottom = rangeTop + rangeSize + + if point < rangeTop then + return point - rangeTop + elseif point > rangeBottom then + return point - rangeBottom + else + return 0 + end + end +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/KeyPool.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/KeyPool.lua new file mode 100644 index 0000000..8b80459 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/KeyPool.lua @@ -0,0 +1,83 @@ +--[[ + KeyPool provides a pool of objects suitable for use as map keys. + + Create a new KeyPool, then call pool:get() to get a new key. Once you're done with it, call key:release(). + + Example: + local pool = KeyPool.new("foo") + ... + local key1 = pool:get() + local key2 = pool:get() + map[key1] = thing1 + map[key2] = thing2 + ... + map[key1] = nil + key1:release() + key1 = nil +]] +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local t = require(Root.t) + +-- Forward declarations +local KeyPool = {} +KeyPool.__index = KeyPool + +local Key = {} +Key.__index = Key + +-- This is Key.new, but we don't want to expose that publicly. +local function newkey(pool: KeyPool, index) + local key = { + pool = pool, + index = index, + } + + setmetatable(key, Key) + return key +end + +-- KeyPool functions + +function KeyPool.new(class: string) + assert(t.string(class)) + + local pool = { + class = class, + available = {}, + limit = 0, + count = 0, + } + + setmetatable(pool, KeyPool) + return pool +end + +export type KeyPool = typeof(KeyPool.new("")) +export type Key = typeof(newkey(KeyPool.new(""), 1)) + +-- Get a currently unused key, or create a new one if everything is in use. +function KeyPool.get(self: KeyPool): Key + if self.count == 0 then + self.limit = self.limit + 1 + return newkey(self, self.limit) + end + + local key = self.available[self.count] + self.count = self.count - 1 + return key +end + +-- Key functions + +function Key.__tostring(self: Key) + return self.pool.class .. "_" .. string.format("%02d", self.index) +end + +-- Return this key to the pool it came from. Whatever previously held this key should not keep the reference after +-- calling this. +function Key.release(self: Key) + self.pool.count = self.pool.count + 1 + self.pool.available[self.pool.count] = self +end + +return KeyPool diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/Logger.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/Logger.lua new file mode 100644 index 0000000..725261e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/Logger.lua @@ -0,0 +1,4 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Lumberyak = require(Root.Lumberyak) + +return Lumberyak.Logger.new(nil, script:GetFullName()) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/NotifyReady.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/NotifyReady.lua new file mode 100644 index 0000000..a564707 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/NotifyReady.lua @@ -0,0 +1 @@ +return {} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/Orientation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/Orientation.lua new file mode 100644 index 0000000..a31399b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/Orientation.lua @@ -0,0 +1,31 @@ +-- Enum for specifying the leading edge of the scroller. +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local t = require(Root.t) + +local Orientation = { + Up = "Orientation.Up", + Down = "Orientation.Down", + Left = "Orientation.Left", + Right = "Orientation.Right", +} + +local metaindex = { + isOrientation = t.union( + t.literal(Orientation.Up), + t.literal(Orientation.Down), + t.literal(Orientation.Left), + t.literal(Orientation.Right) + ) +} + +setmetatable(Orientation, { + __index = function(self, key) + return metaindex[key] or + error(tostring(key) .. " is not a valid member of Scroller.Orientation", 2) + end, + __newindex = function() + error("Scroller.Orientation is read-only", 2) + end, +}) + +return Orientation diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/Round.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/Round.lua new file mode 100644 index 0000000..2755954 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/Round.lua @@ -0,0 +1,28 @@ +local epsilon = 1e-15 + +return { + nearest = function(num) + local q, r = math.modf(num) + if r <= -0.5 then + return q - 1 + elseif r >= 0.5 then + return q + 1 + else + return q + end + end, + towardsZero = function(num) + local result, _ = math.modf(num) + return result + end, + awayFromZero = function(num) + local q, r = math.modf(num) + if r < -epsilon then + return q - 1 + elseif r > epsilon then + return q + 1 + else + return q + end + end, +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/Scroller.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/Scroller.lua new file mode 100644 index 0000000..1bd1a00 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/Scroller.lua @@ -0,0 +1,1638 @@ +local RunService = game:GetService("RunService") +local HttpService = game:GetService("HttpService") + +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local t = require(Root.t) +local Otter = require(Root.Otter) + +local FitFrame = require(Root.FitFrame).FitFrameOnAxis +local findNewIndices = require(script.Parent.findNewIndices) +local relocateIndices = require(script.Parent.relocateIndices) +local Round = require(script.Parent.Round) +local Distance = require(script.Parent.Distance) +local KeyPool = require(script.Parent.KeyPool) + +local Logger = require(script.Parent.Logger) +local TimeLogger = require(script.Parent.TimeLogger) + +local NotifyReady = require(script.Parent.NotifyReady) + +local Scroller = Roact.PureComponent:extend("Scroller") + +Scroller.Orientation = require(script.Parent.Orientation) + +local MOTOR_OPTIONS = { + frequency = 4, + dampingRatio = 1, +} + +local REPORT_TRIMMING_ERROR_THRESHOLD = 3 + +local isVertical = { + [Scroller.Orientation.Up] = true, + [Scroller.Orientation.Down] = true, + [Scroller.Orientation.Left] = false, + [Scroller.Orientation.Right] = false, +} + +local isReverse = { + [Scroller.Orientation.Up] = true, + [Scroller.Orientation.Down] = false, + [Scroller.Orientation.Left] = true, + [Scroller.Orientation.Right] = false, +} + +local direction = { + [Scroller.Orientation.Up] = -1, + [Scroller.Orientation.Down] = 1, + [Scroller.Orientation.Left] = -1, + [Scroller.Orientation.Right] = 1, +} + +Scroller.validateProps = t.interface({ + -- Required. The list of items to scroll through. + itemList = t.array(t.any), + + -- Required. A callback function, called with each visible item in the itemList when the list is rendered. + renderItem = t.callback, + + -- A function to uniquely identify list items. Calling this on the same item twice should give the same result + -- accoring to ==. + identifier = t.optional(t.callback), + + -- One of the Scroller.Orientation enums. Determines the leading edge of the infinite scroll. + orientation = t.optional(Scroller.Orientation.isOrientation), + + -- A callback function, called when the infinite scroll reaches the leading end of the itemList (index + -- #itemList). + loadNext = t.optional(t.callback), + + -- A callback function, called when the infinite scroll reaches the trailing end of the itemList (index 1). + loadPrevious = t.optional(t.callback), + + -- Padding between elements in the scrolling frame. The Scale is relative to the size of the scrolling frame. + padding = t.optional(t.UDim), + + -- The minimum number of unmounted elements to keep at the top and bottom of the list. If there are fewer than + -- this call loadNext or loadPrevious. + loadingBuffer = t.optional(t.numberPositive), + + -- The amount of space above and below the view to render items in. + mountingBuffer = t.optional(t.numberPositive), + + -- The amount of empty space past the leading element of the list. The Scale is relative to the size of the scrolling frame. + leadBuffer = t.optional(t.UDim), + + -- The amount of empty space past the trailing element of the list. The Scale is relative to the size of the scrolling frame. + trailBuffer = t.optional(t.UDim), + + -- An initial guess at the average size of an item. + estimatedItemSize = t.optional(t.numberPositive), + + -- The maximum distance to search for moved elements. + maximumSearchDistance = t.optional(t.numberPositive), + + -- The element to put in focus initially. + focusIndex = t.optional(t.integer), + + -- An arbitrary value to prevent the list from refocusing every render. Change this to cause the list to reset + -- and refocus on the new focusIndex. + focusLock = t.optional(t.any), + + -- The position within the view to keep still as other things move. The Scale is relative to the size of the + -- scrolling frame. + anchorLocation = t.optional(t.UDim), + + -- Animate the scrolling + animateScrolling = t.optional(t.boolean), + + --Animation options + animateOptions = t.optional(t.table), + + -- Properties that should trigger rerenders of the children elements even though the scroller itself does not + -- use them. + extraProps = t.optional(t.table), + + -- A callback function that will update the index change + onScrollUpdate = t.optional(t.callback), + + -- Which components to disable instance recycling for. + recyclingDisabledFor = t.optional(t.array(t.string)), + + ---- INTERNAL ONLY ---- + [NotifyReady] = t.any, +}) + +-- Default values for all the infinite-scroller-specific props. Any prop not in this list will be passed on to the +-- underlying ScrollingFrame. +Scroller.defaultProps = { + itemList = {}, + renderItem = {}, + identifier = function(item) + return item + end, + orientation = Scroller.Orientation.Down, + loadNext = function() end, + loadPrevious = function() end, + padding = UDim.new(), + loadingBuffer = 10, + mountingBuffer = 200, + leadBuffer = UDim.new(), + trailBuffer = UDim.new(), + estimatedItemSize = 50, + maximumSearchDistance = 100, + focusIndex = 1, + focusLock = {}, + anchorLocation = UDim.new(0, 0), + animateScrolling = false, + animateOptions = MOTOR_OPTIONS, + extraProps = {}, + onScrollUpdate = function() end, + recyclingDisabledFor = {}, + [NotifyReady] = false, +} + +function Scroller:render() + self.log:debug("render") + + -- Gather vertical/horizontal specific variables. + local axis = isVertical[self.props.orientation] and { + fillDirection = Enum.FillDirection.Vertical, + scrollDirection = Enum.ScrollingDirection.Y, + fitDirection = FitFrame.Axis.Vertical, + minimumSize = UDim2.new(1, 0, 0, 0), + canvasSize = UDim2.new(0, 0, 0, self.state.size), + paddingSize = UDim2.new(0, 0, 0, self.state.padding), + } or { + fillDirection = Enum.FillDirection.Horizontal, + scrollDirection = Enum.ScrollingDirection.X, + fitDirection = FitFrame.Axis.Horizontal, + minimumSize = UDim2.new(0, 0, 1, 0), + canvasSize = UDim2.new(0, self.state.size, 0, 0), + paddingSize = UDim2.new(0, self.state.padding, 0, 0), + } + + -- Remove non-standard props from list to pass on to ScrollingFrame. These are the same props given in + -- defaultProps. + local props = Cryo.Dictionary.join( + self.props, + self.propsToClear, + { + CanvasSize = axis.canvasSize, + ScrollingDirection = axis.scrollDirection, + [Roact.Change.CanvasPosition] = self.onScroll, + [Roact.Change.AbsoluteSize] = self.onResize, + [Roact.Ref] = self:getRef(), + } + ) + + local children = { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = axis.fillDirection, + Padding = UDim.new(0, self.itemPadding), + [Roact.Change.AbsoluteContentSize] = self.onContentResize, + }), + padding = Roact.createElement("Frame", { + Size = axis.paddingSize, + LayoutOrder = -1 - (self.state.listSize or 0), + BackgroundTransparency = 1, + }), + } + + -- Trailing and leading indicies won't be set if this isn't true. + if self.state.ready and not Cryo.isEmpty(self.props.itemList) then + self.log:trace(" Rendering elements between {} and {}", self.state.trail.index, self.state.lead.index) + for n = self.state.trail.index, self.state.lead.index do + local metadata = self:getMetadata(n) + children[metadata.name] = Roact.createElement(FitFrame, { + minimumSize = axis.minimumSize, + axis = axis.fitDirection, + FillDirection = axis.fillDirection, + BackgroundTransparency = 1, + LayoutOrder = isReverse[self.props.orientation] and -n or n, + [Roact.Ref] = metadata.ref, + }, { + item = self.props.renderItem(self.props.itemList[n], false), + }) + end + end + + return Roact.createElement("ScrollingFrame", props, children) +end + +function Scroller:shouldUpdate(nextProps, nextState) + if not self.alive then + return false + end + + self.log:trace("shouldUpdate") + + if self.props["Size"] ~= nextProps["Size"] then + self.log:trace(" Size Prop Changed") + self._sizePropChanged = true + end + + -- Check for state and props changes in the same way PureComponent would, + -- but go one more level down for extraProps. + if nextState ~= self.state then + self.log:trace(" State changed") + return true + end + + for key, value in pairs(nextProps) do + if self.props[key] ~= value then + if key ~= "extraProps" then + self.log:trace(" Prop changed: {}", key) + return true + end + + for extraKey, extraValue in pairs(value) do + if self.props.extraProps[extraKey] ~= extraValue then + self.log:trace(" Extra prop changed: {}", extraKey) + return true + end + end + end + end + + for key, value in pairs(self.props) do + if nextProps[key] ~= value then + if key ~= "extraProps" then + self.log:trace(" Prop changed: {}", key) + return true + end + + for extraKey, extraValue in pairs(value) do + if nextProps.extraProps[extraKey] ~= extraValue then + self.log:trace(" Extra prop changed: {}", extraKey) + return true + end + end + end + end + + return false +end + +function Scroller:willUpdate(nextProps, nextState) + if not self.alive then + return + end + + self.log:debug("willUpdate") + + if not nextState.ready then + return + end + + if self.updating then + self.timeLog:info("Middle of update") + else + self.timeLog:info("Start of update") + + -- Reset error counters + self.numberOfTopTrims = 0 + self.numberOfBottomTrims = 0 + self.lastTrimTop = 0 + self.lastTrimBottom = 0 + end + self.updating = true + + self.sizeDebounce = true + + local deletions = {} + local additions = {} + + if not Cryo.isEmpty(self.props.itemList) and self.state.lead then + for n = self.state.trail.index, self.state.lead.index do + local id = self.props.identifier(self.props.itemList[n]) + deletions[id] = true + end + end + + if not Cryo.isEmpty(nextProps.itemList) and nextState.lead then + for n = nextState.trail.index, nextState.lead.index do + local item = nextProps.itemList[n] + local id = nextProps.identifier(item) + if deletions[id] then + -- Element is in both ranges. + deletions[id] = nil + else + additions[id] = item + end + end + end + + -- Clear names first, so new items can use them. + for id, _ in pairs(deletions) do + self:clearMetadata(id) + end + for id, item in pairs(additions) do + self:updateMetadata(id, item, nextProps) + end + + -- The focus lock changed, clear the non-state anchor variables. + if self.state.lastFocusLock ~= nextState.lastFocusLock then + self.scrollDebounce = true + self.motorActive = false + self.anchorFramePosition = 0 + if self.state.listSize and self.state.listSize > nextState.listSize then + -- Perform a full reset when the list decreases in size to ensure the canvas + -- is sized properly + self.anchorCanvasPosition = self.relativeAnchorLocation + else + -- The canvas hasn't necessarily been reset in size here, so we need to convert the frame + -- coordinates of relativeAnchorLocation to canvas coordinates. + self.anchorCanvasPosition = self:frameToCanvasPosition(self.relativeAnchorLocation) + end + end +end + +function Scroller:didUpdate(previousProps, previousState) + if not self.alive then + return + end + + self.log:debug("didUpdate") + + if Cryo.isEmpty(self.props.itemList) then + return + end + + if not self.state.ready then + self.onResize(self:getRef().current) + return + end + + if self.props.focusIndex ~= previousProps.focusIndex and + self.props.focusLock ~= previousProps.focusLock then + + self.indexChanged = { + oldIndex = previousProps.focusIndex, + newIndex = self.props.focusIndex, + lastFocusLock = self.props.focusLock, + } + self.motorActive = false + + self.log:trace("self.props.focusIndex {}", self.props.focusIndex) + self.log:trace("self.state.anchor.index {}", self.state.anchor.index) + self.log:trace("previousState.anchor.index {}", previousState.anchor.index) + end + + local adjustedCanvas = self:adjustCanvas(self.scrollingForward, self.scrollingBackward) + if not adjustedCanvas then + if self.indexChanged and self.props.animateScrolling then + self:scrollToAnchor() + else + self:moveToAnchor() + end + + if self.anchorOffset ~= 0 then + self:setState({}) + return + end + + -- The canvas has finished adjusting itself after a resize + self.resized = false + + self:loadMore() + self.sizeDebounce = false + + if self.updating then + self.timeLog:info("End of update") + end + self.updating = false + + --Return the updated index + if self.props.onScrollUpdate then + self.props.onScrollUpdate({ + leadIndex = self.state.lead.index, + anchorIndex = self.state.anchor.index, + trailIndex = self.state.trail.index, + animationActive = self.motorActive, + }) + end + end + + self._sizePropChanged = false + + self.prevCycle = { + frameSize = self:measure(self:getCurrent().AbsoluteSize), + canvasSize = self.state.size, + canvasPosition = Round.nearest(self:measure(self:getCurrent().CanvasPosition)), + relativeAnchorLocation = self.relativeAnchorLocation, + } +end + +function Scroller.getDerivedStateFromProps(nextProps, lastState) + -- No self in static functions + Logger:trace("getDerivedStateFromProps") + + if not lastState.ready or Cryo.isEmpty(nextProps.itemList) then + return nil + end + + local listSize = #nextProps.itemList + local lastFocusLock = nil + + -- Reset the state if the focus lock changes. This is guaranteed to be true the first time. + if lastState.lastFocusLock ~= nextProps.focusLock then + Logger:trace(" Resetting focus lock {} to {}", lastState.lastFocusLock, nextProps.focusLock) + if nextProps.animateScrolling and lastState.lastFocusLock ~= nil then + lastFocusLock = nextProps.focusLock + else + local focusID = nextProps.identifier(nextProps.itemList[nextProps.focusIndex]) + return { + listSize = listSize, + trail = {index=nextProps.focusIndex, id=focusID}, + anchor = {index=nextProps.focusIndex, id=focusID}, + lead = {index=nextProps.focusIndex, id=focusID}, + padding = 0, + size = 0, + lastFocusLock = nextProps.focusLock, + } + end + end + + local trailIndex, anchorIndex, leadIndex = findNewIndices(nextProps, lastState) + Logger:trace(" Trailing index moved from {} to {}", lastState.trail.index, trailIndex) + Logger:trace(" Anchor index moved from {} to {}", lastState.anchor.index, anchorIndex) + Logger:trace(" Leading index moved from {} to {}", lastState.lead.index, leadIndex) + + -- Nothing changed. Return early to avoid triggering an update. + if anchorIndex and lastState.anchor.index == anchorIndex + and trailIndex and lastState.trail.index == trailIndex + and leadIndex and lastState.lead.index == leadIndex then + Logger:trace(" No change, returning early") + if listSize == lastState.listSize then + if lastFocusLock then + return { + lastFocusLock = lastFocusLock, + } + else + return nil + end + else + return { + listSize = listSize, + lastFocusLock = lastFocusLock, + } + end + end + + -- TODO #51 findNewIndices, state and this should all agree on a format + local newIndices = relocateIndices( + {trailIndex=trailIndex, anchorIndex=anchorIndex, leadIndex=leadIndex}, + {trailIndex=lastState.trail.index, anchorIndex=lastState.anchor.index, leadIndex=lastState.lead.index}, + listSize + ) + + Logger:trace(" Anchor index moved to {}", newIndices.anchorIndex) + Logger:trace(" Trailing index moved to {}", newIndices.trailIndex) + Logger:trace(" Leading index moved to {}", newIndices.leadIndex) + + local trailID = nextProps.identifier(nextProps.itemList[newIndices.trailIndex]) + local anchorID = nextProps.identifier(nextProps.itemList[newIndices.anchorIndex]) + local leadID = nextProps.identifier(nextProps.itemList[newIndices.leadIndex]) + + return { + listSize = listSize, + trail = {index=newIndices.trailIndex, id=trailID}, + anchor = {index=newIndices.anchorIndex, id=anchorID}, + lead = {index=newIndices.leadIndex, id=leadID}, + lastFocusLock = lastFocusLock, + } +end + +function Scroller:init() + self.guid = HttpService:GenerateGUID() + self.log = Logger:new(script:GetFullName() .. "." .. self.guid) + self.timeLog = TimeLogger:new(script:GetFullName() .. "." .. self.guid) + + self.log:debug("init") + + -- Only self:getRef() should access this. + self._ref = Roact.createRef() + + self.motorPrevValue = 0 + + self.motorOnStep = function(value) + self.log:trace("onStep {}", value) + if not self.motorActive or self.indexChanged == nil then + self.motor:stop() + return + end + local currentValue = self.indexChanged.currentPos + if currentValue == nil then + self.motor:stop() + return + end + local diff = value - self.motorPrevValue + if self:getCurrent() then + self:scrollRelative(diff) + self.motorPrevValue = value + end + end + + self.motorOnComplete = function() + self.log:trace("otter onComplete") + self.motorActive = false + --Return the updated index + if self.props.onScrollUpdate then + self.props.onScrollUpdate({ + leadIndex = self.props.focusIndex, + anchorIndex = self.props.focusIndex, + trailIndex = self.props.focusIndex, + animationActive = self.motorActive, + }) + end + self.motorPrevValue = 0 + self.indexChanged = nil + if self.motor then + self.motor:destroy() + end + end + + self.motorActive = false + self.springLock = 0 + self.scrollDebounce = false + self.sizeDebounce = true + + --Used to track index changes + self.indexChanged = nil + + self.onScroll = function(rbx) + self.log:trace("onScroll") + if not self.alive or self.resized then + return + end + + self.log:trace(" CanvasPosition is {}", rbx.CanvasPosition) + if self.scrollDebounce then + self.log:trace(" Debouncing scroll") + return + end + + local delta, newState = self:recalculateAnchor() + self.scrollingBackward = delta < 0 + self.scrollingForward = delta > 0 + self.log:trace(" Delta is {}", delta) + + newState = Cryo.Dictionary.join( + newState, + self:resetAnchorPosition(newState), + self:recalculateBounds(self.scrollingForward, self.scrollingBackward, newState) + ) + + if not Cryo.isEmpty(newState) then + self:setState(newState) + end + + -- Handle any passed in scroll callback. + if self.props[Roact.Change.CanvasPosition] then + self.props[Roact.Change.CanvasPosition](rbx) + end + + self.prevCycle.canvasPosition = Round.nearest(self:measure(rbx.CanvasPosition)) + end + + self.onResize = function(rbx) + self.log:trace("onResize") + if not self.alive then + return + end + + local size = self:measure(rbx.AbsoluteSize) + local pos = self:measure(rbx.AbsolutePosition) + + self.leadBufferPx = Round.nearest( + self.props.leadBuffer.Scale * size + self.props.leadBuffer.Offset) + + self.trailBufferPx = Round.nearest( + self.props.trailBuffer.Scale * size + self.props.trailBuffer.Offset) + + self.itemPadding = self.props.padding.Scale * size + self.props.padding.Offset + if isReverse[self.props.orientation] then + self.relativeAnchorLocation = Round.nearest( + self.props.anchorLocation.Scale * size + self.props.anchorLocation.Offset) + else + self.relativeAnchorLocation = Round.nearest( + (1 - self.props.anchorLocation.Scale) * size - self.props.anchorLocation.Offset) + end + self.absoluteAnchorLocation = self.relativeAnchorLocation + pos + self.mountAboveAnchor = self.relativeAnchorLocation + self.props.mountingBuffer + self.mountBelowAnchor = size - self.relativeAnchorLocation + self.props.mountingBuffer + self.resized = true + self.increasedSize = self.state.size < size + + -- Handle any passed in resize callback. + if self.props[Roact.Change.AbsoluteSize] then + self.props[Roact.Change.AbsoluteSize](rbx) + end + + if not self.state.ready then + self.log:trace(" Setting initial anchor position to {}", self.relativeAnchorLocation) + -- When setting this for the first time, set the frame position of the current anchor to 0, + -- and its canvas position to equal where it should be in the frame. When the scroller goes + -- to correct this, the anchor will end up in the right place with the right padding around it. + self.anchorFramePosition = 0 + self.anchorCanvasPosition = self.relativeAnchorLocation + + coroutine.wrap(function() + RunService.Heartbeat:Wait() + if not self.state.ready and self.alive then + self:setState({ + ready = true + }) + + -- This should only be set by tests. + if self.props[NotifyReady] then + self.props[NotifyReady]:Fire() + end + end + end)() + else + self:handleResize(size) + end + end + + self.onContentResize = function() + self.log:trace("onContentResize") + + if not self.alive or self.sizeDebounce or not self.state.ready or self:isScrollingWithElasticBehavior() then + self.log:trace(" Skipping onContentResize") + return + end + self:setState({}) -- Force a rerender. + end + + self.anchorCanvasPosition = 0 + self.anchorFramePosition = 0 + self.anchorOffset = 0 + + self.metadata = {} + self.pools = {} + self.refpool = {} + + self.scrollingBackward = false + self.scrollingForward = false + + self.lastLoadPrevItems = nil + self.lastLoadNextItems = nil + + self.alive = true + self.updating = false + + -- Store the list of props to not pass on to the underlying scrolling frame. + self.propsToClear = {} + for k, _ in pairs(Scroller.defaultProps) do + self.propsToClear[k] = Cryo.None + end + + -- This will get updated shortly, but one render will happen before state.ready is set + self:setState({ + ready = false, + lastFocusLock = nil, + padding = 0, + size = 0, + }) +end + +function Scroller:willUnmount() + if self.motor then + self.motor:destroy() + end + + self.alive = false +end + +-- Find which element is currently closest to the anchor position. +function Scroller:recalculateAnchor() + self.log:trace("recalculateAnchor") + + -- Find the index of the element at the appropriate position + local index = self:findIndexAt( + self:absoluteToCanvasPosition(self.absoluteAnchorLocation), self.state.anchor.index, false) + + self.anchorCanvasPosition = self:getAnchorCanvasFromIndex(index) + self.anchorFramePosition = self:getAnchorFrameFromIndex(index) + + local delta + if index == self.state.anchor.index then + self.log:trace(" Current anchor still works") + return 0, {} + elseif index < self.state.anchor.index then + delta = -1 + else + delta = 1 + end + + self.log:trace(" New anchor at index {}", index) + + -- Store the new anchor's details + self.log:trace(" New anchor at canvas position {}", self.anchorCanvasPosition) + self.log:trace(" New anchor at frame position {}", self.anchorFramePosition) + return delta, { + anchor = {index=index, id=self:getID(index)}, + } +end + +-- Move all the rendered elements up or down to put the anchor back where it was. +function Scroller:resetAnchorPosition(newState) + local anchor = newState and newState.anchor or self.state.anchor + local padding = newState and newState.padding or self.state.padding + + self.log:trace("resetAnchorPosition") + self.log:trace(" Anchor index is {}", anchor.index) + local offset = self:getAnchorCanvasFromIndex(anchor.index) + self.log:trace(" Anchor is at {}", offset) + self.log:trace(" Anchor offset is {}", self.anchorOffset) + local newPos = self.anchorCanvasPosition - self.anchorOffset + self.log:trace(" Anchor should be at {}", newPos) + local diff = Round.nearest(newPos - offset) + if diff ~= 0 then + self.log:trace(" Changing padding from {} to {}", padding, padding + diff) + self.log:trace(" Changing anchorCanvasPosition from {} to {}", self.anchorCanvasPosition, Round.nearest(self.anchorCanvasPosition - self.anchorOffset)) + self.anchorCanvasPosition = Round.nearest(self.anchorCanvasPosition - self.anchorOffset) + self.anchorOffset = 0 + return {padding = padding + diff} + else + return {} + end +end + +-- Get the current padding from the UIPadding child. +function Scroller:getCurrentPadding() + local pad = self:getCurrent().padding + -- Only one of these will be non-zero + return pad.Size.X.Offset + pad.Size.Y.Offset +end + +-- Move the top and bottom of the range to be rendered up and down to make sure +-- enough things are being rendered. +function Scroller:recalculateBounds(trimTrailing, trimLeading, newState) + local lead = newState and newState.lead or self.state.lead + local trail = newState and newState.trail or self.state.trail + local anchor = newState and newState.anchor or self.state.anchor + + self.log:trace("recalculateBounds") + self.log:trace(" Leading index was {}", lead.index) + self.log:trace(" Trailing index was {}", trail.index) + + local anchorPos = self:getAnchorCanvasFromIndex(anchor.index) + local mountTop = anchorPos - self.mountAboveAnchor + local mountBottom = anchorPos + self.mountBelowAnchor + self.log:trace(" Target for top at {}", mountTop) + self.log:trace(" Target for bottom at {}", mountBottom) + + local topIndex = self:findIndexAt(mountTop, anchor.index, true) + self.log:trace(" Found new top index at {}", topIndex) + local bottomIndex = self:findIndexAt(mountBottom, anchor.index, true) + self.log:trace(" Found new bottom index at {}", bottomIndex) + + local leadIndex = math.max(topIndex, bottomIndex) + if leadIndex < lead.index and not trimLeading then + leadIndex = lead.index + end + + local trailIndex = math.min(topIndex, bottomIndex) + if trailIndex > trail.index and not trimTrailing then + trailIndex = trail.index + end + + if trailIndex < trail.index or leadIndex > lead.index then + self.log:trace(" Changing leading index to {}", leadIndex) + self.log:trace(" Changing trailing index to {}", trailIndex) + return { + trail = {index=trailIndex, id=self:getID(trailIndex)}, + lead = {index=leadIndex, id=self:getID(leadIndex)}, + } + else + return {} + end +end + +-- Find the index of the element that overlaps the given canvas-relative position. +function Scroller:findIndexAt(targetPos, hintIndex, extrapolate) + self.log:trace(" findIndexAt") + -- Get the distance from the hinted index or the anchor. + local currentIndex = hintIndex or self.state.anchor.index + local currentDist = self:distanceToPosition(currentIndex, targetPos) + self.log:trace(" Searching from index {}", currentIndex) + self.log:trace(" Position is {} from {}", currentDist, targetPos) + if currentDist == 0 then + -- resolving border disputes + local nextIndex = currentIndex + Round.awayFromZero(self.props.anchorLocation.Scale - 0.5) + if nextIndex > self.state.listSize or nextIndex < 1 then + return currentIndex + end + + local nextDist = self:distanceToPosition(nextIndex, targetPos) + if nextDist == 0 then + return nextIndex + end + return currentIndex + end + + -- Get the distance from one end of the list. + local nextIndex = (currentDist < 0) and self.state.trail.index or self.state.lead.index + self.log:trace(" Nearest end at {}", nextIndex) + if currentIndex == nextIndex then + self.log:trace(" Hint index already at end") + -- If the target position lies outside of the loaded elements. + if currentIndex + currentDist < self.state.trail.index + or currentIndex + currentDist > self.state.lead.index then + self.log:trace(" Target out of bounds") + if not extrapolate then + -- Do not extrapolate. Return the closest loaded element. + return currentIndex + end + + -- Extrapolate using the estimated item size. + local delta = Round.awayFromZero(currentDist / self.props.estimatedItemSize) + self.log:trace(" Estimating target at {} from end", delta) + return math.min(math.max(currentIndex + delta, 1), self.state.listSize) + end + else + local nextDist = self:distanceToPosition(nextIndex, targetPos) + self.log:trace(" End is {} from target", nextDist) + if nextDist == 0 then + return nextIndex + end + + -- If the target position lies outside of the loaded elements. + if currentDist * nextDist > 0 then + self.log:trace(" Target out of bounds") + if not extrapolate then + -- Do not extrapolate. Return the closest loaded element. + return nextIndex + end + + -- Extrapolate using the estimated item size. + local delta = Round.awayFromZero(nextDist / self.props.estimatedItemSize) + self.log:trace(" Estimating target at {} from end", delta) + return math.min(math.max(nextIndex + delta, 1), self.state.listSize) + end + + -- Jump to the approximate location of the target based on the distance from current and next. + local totalDist = math.abs(currentDist) + math.abs(nextDist) + local indexCount = math.abs(currentIndex - nextIndex) + currentIndex = currentIndex + Round.nearest(indexCount * currentDist / totalDist) + currentDist = self:distanceToPosition(currentIndex, targetPos) + self.log:trace(" Interpolated index is {}", currentIndex) + self.log:trace(" Distance from interpolated index is {}", currentDist) + end + + -- Linear search from best guess index. + while currentDist ~= 0 do + if currentDist < 0 then + currentIndex = currentIndex - 1 + else + currentIndex = currentIndex + 1 + end + currentDist = self:distanceToPosition(currentIndex, targetPos) + self.log:trace(" Distance after step is {}", currentDist) + end + + return currentIndex +end + +function Scroller:findIndexWithRemainder(targetPos, hintIndex, extrapolate) + local reverse = isReverse[self.props.orientation] + local topBufferPx = reverse and self.leadBufferPx or self.trailBufferPx + + if targetPos < topBufferPx then + local topIndex = reverse and self.state.listSize or 1 + return topIndex, 0 + elseif targetPos > Round.nearest(self:measure(self:getCurrent().CanvasSize).Offset) then + local bottomIndex = reverse and 1 or self.state.listSize + return bottomIndex, 0 + end + + local currentIndex = self:findIndexAt(targetPos, hintIndex, extrapolate) + local actualIndexPosition = self:getAnchorCanvasFromIndex(currentIndex) + local nextIndex = currentIndex + Round.awayFromZero(self.props.anchorLocation.Scale - 0.5) + + if nextIndex > self.state.listSize or nextIndex < 1 then + return currentIndex, 0 + end + + local nextDist = self:distanceToPosition(nextIndex, targetPos) + if nextDist == 0 then + return nextIndex, 0 + end + + return currentIndex, actualIndexPosition - targetPos +end + +-- Check if trimTop is trimming the same amount multiple times. +-- If it is, there is an issue with one of the buffers which will be reported to the user. +function Scroller:checkTrimTopError(topDiff) + if self.lastTrimTop == topDiff then + if self.numberOfTopTrims >= REPORT_TRIMMING_ERROR_THRESHOLD then + error("There was an error laying out the items. Check to see if you provided enough leadBuffer and/or trailBuffer") + else + self.numberOfTopTrims = self.numberOfTopTrims + 1 + end + else + self.lastTrimTop = topDiff + self.numberOfTopTrims = 0 + end +end + +function Scroller:trimTopHelper(reverse, size, padding, anchor, topIndex, topDiff) + local topBufferPx = reverse and self.leadBufferPx or self.trailBufferPx + -- Ensure this trim does not set the padding to less than the topBufferPx + if padding + topDiff < topBufferPx then + topDiff = padding - topBufferPx + end + + -- Make sure the topDiff will not expand canvas and that + -- it will not trim to a negative value + if topDiff >= 0 or size + topDiff < 0 then + return {} + end + + self:checkTrimTopError(topDiff) + + local trimmedCanvasSize = Round.nearest(size + topDiff) + local trimmedPadding = Round.nearest(padding + topDiff) + local newAnchorCanvasPosition = Round.nearest(self.anchorCanvasPosition + topDiff) + self.log:trace(" Changing anchor canvas position from {} to {}", self.anchorCanvasPosition, newAnchorCanvasPosition) + self.anchorCanvasPosition = newAnchorCanvasPosition + + -- Change the anchor when there is empty space at the top of the list and the anchor position has + -- been shifted past the anchor element + local topFramePos = self:getChildFramePosition(topIndex) + local nextAnchorIndex = anchor.index - 1 + if nextAnchorIndex > 0 and topFramePos > 0 then + local nextAnchorCanvasPosition = self:getChildCanvasPosition(nextAnchorIndex) + if nextAnchorCanvasPosition > self.anchorCanvasPosition then + local nextAnchor = {index = nextAnchorIndex, id=self:getID(nextAnchorIndex)} + + return { + size = trimmedCanvasSize, + padding = trimmedPadding, + anchor = nextAnchor, + } + end + end + + return { + size = trimmedCanvasSize, + padding = trimmedPadding, + } +end + +-- Shrink the canvas and padding so that the top of the list plus the buffer +-- is at the top of the canvas +function Scroller:trimTop(reverse, size, padding, anchor, topIndex, topCanvasPos) + local atTop = (reverse and topIndex == self.state.listSize) or (not reverse and topIndex == 1) + local bufferSize = reverse and self.leadBufferPx or self.trailBufferPx + local topDiff = atTop and bufferSize - topCanvasPos or 0 + + return self:trimTopHelper(reverse, size, padding, anchor, topIndex, topDiff) +end + +-- Shrink the canvas and padding so that the top of the canvas is at the top of the frame +function Scroller:shortTrimTop(reverse, size, frameSize, padding, anchor, topIndex) + local topFramePos = self:getChildFramePosition(topIndex) + local bottomBufferPx = reverse and self.trailBufferPx or self.leadBufferPx + + -- Only trim to the top of the frame + local topDiff + if topFramePos >= 0 and topFramePos <= frameSize then + topDiff = -Round.towardsZero(size - bottomBufferPx - frameSize) + else + topDiff = topFramePos + end + return self:trimTopHelper(reverse, size, padding, anchor, topIndex, topDiff) +end + +-- Check if trimBottom is trimming the same amount multiple times. +-- If it is, there is an issue with one of the buffers which will be reported to the user. +function Scroller:checkTrimBottomError(bottomDiff) + if self.lastTrimBottom == bottomDiff then + if self.numberOfBottomTrims >= REPORT_TRIMMING_ERROR_THRESHOLD then + error("There was an error laying out the items. Check to see if you provided enough leadBuffer and/or trailBuffer") + else + self.numberOfBottomTrims = self.numberOfBottomTrims + 1 + end + else + self.lastTrimBottom = bottomDiff + self.numberOfBottomTrims = 0 + end +end + +function Scroller:trimBottomHelper(size, bottomDiff) + -- Make sure the bottomDiff will not expand canvas + if bottomDiff <= 0 then + return {} + end + + self:checkTrimBottomError(bottomDiff) + + local trimmedCanvasSize = Round.nearest(size - bottomDiff) + + return { + size = trimmedCanvasSize, + } +end + +-- Shrink the canvas so that the bottom of the list plus the buffer +-- is at the bottom of the canvas +function Scroller:trimBottom(reverse, bottomIndex, bottomCanvasPos, size) + local atBottom = (reverse and bottomIndex == 1) or (not reverse and bottomIndex == self.state.listSize) + local bufferSize = reverse and self.trailBufferPx or self.leadBufferPx + local bottomDiff = atBottom and (size - bufferSize) - bottomCanvasPos or 0 + + -- When resizing to a larger frame size extra padding needs to be added before trimming can happen + if self.resized and self.increasedSize then + return {} + end + return self:trimBottomHelper(size, bottomDiff) +end + +-- Shrink the canvas so that the bottom of the canvas is at the bottom of the frame +function Scroller:shortTrimBottom(reverse, bottomIndex, bottomCanvasPos, size, frameSize) + local bottomFramePos = self:getChildFramePosition(bottomIndex) + local bottomSize = self:getChildSize(bottomIndex) + bottomFramePos = bottomFramePos + bottomSize + + if bottomFramePos >= 0 and bottomFramePos <= frameSize then + return {} + end + + local bufferSize = reverse and self.trailBufferPx or self.leadBufferPx + -- Only trim to the bottom of the frame + the buffer at the end of the list + local bottomDiff = (size - bufferSize) - bottomCanvasPos + + return self:trimBottomHelper(size, bottomDiff) +end + +-- When the ends of the list are rendered, adjust the padding and canvas size so that the ends of +-- the canvas do not extend past the items in the list + the top and bottom buffers. +function Scroller:adjustEdges(newState) + self.log:trace("adjustEdges") + + local size = newState.size or self.state.size + size = Round.nearest(size) + local padding = newState.padding or self.state.padding + local anchor = newState.anchor or self.state.anchor + local reverse = isReverse[self.props.orientation] + + local frameSize = self:measure(self:getCurrent().AbsoluteSize) + local topIndex = reverse and self.state.lead.index or self.state.trail.index + local topCanvasPos = Round.nearest(self:getChildCanvasPosition(topIndex)) - self.itemPadding + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + local childSize = self:getChildSize(bottomIndex) + local bottomCanvasPos = Round.nearest(self:getChildCanvasPosition(bottomIndex) + childSize) + + local totalSize = self.leadBufferPx + self.trailBufferPx + (bottomCanvasPos - topCanvasPos) + + local combinedState + if bottomCanvasPos - topCanvasPos <= frameSize or totalSize <= frameSize then + -- For lists that are less than the size of the scrolling frame, trim the trailing edge followed by the leading edge. + if reverse then + local trimBottomReturnState = self:trimBottom(reverse, bottomIndex, bottomCanvasPos, size) + local newCanvasSize = trimBottomReturnState.size or size + local trimTopReturnState = self:shortTrimTop(reverse, newCanvasSize, frameSize, padding, anchor, topIndex) + combinedState = Cryo.Dictionary.join(trimBottomReturnState, trimTopReturnState) + else + local trimTopReturnState = self:trimTop(reverse, size, padding, anchor, topIndex, topCanvasPos) + local newCanvasSize = trimTopReturnState.size or size + local trimBottomReturnState = self:shortTrimBottom(reverse, bottomIndex, bottomCanvasPos, newCanvasSize, frameSize) + combinedState = Cryo.Dictionary.join(trimTopReturnState, trimBottomReturnState) + end + else + -- For lists larger than the scrolling frame the order of trimming does not matter. + local trimTopReturnState = self:trimTop(reverse, size, padding, anchor, topIndex, topCanvasPos) + local newCanvasSize = trimTopReturnState.size or size + local trimBottomReturnState = self:trimBottom(reverse, bottomIndex, bottomCanvasPos, newCanvasSize) + combinedState = Cryo.Dictionary.join(trimTopReturnState, trimBottomReturnState) + end + + if combinedState.size then + self.log:trace(" Changing canvas size from {} to {}", size, combinedState.size) + end + if combinedState.padding then + self.log:trace(" Changing canvas padding from {} to {}", padding, combinedState.padding) + end + if combinedState.anchor then + self.log:trace(" Changing anchor index from {} to {}", anchor.index, combinedState.anchor.index) + end + + return combinedState +end + +-- Expand the size of the scrolling frame's canvas to make sure everything still fits. +function Scroller:expandCanvas(newState) + self.log:trace("expandCanvas") + local reverse = isReverse[self.props.orientation] + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + local topBufferPx = reverse and self.leadBufferPx or self.trailBufferPx + local bottomBufferPx = reverse and self.trailBufferPx or self.leadBufferPx + + local size = newState.size or self.state.size + size = Round.nearest(size) + local originalSize = size + local newPadding = newState.padding or self.state.padding + local originalPadding = newPadding + local oldPadding = self:getCurrentPadding() + + local bottomPos = self:getChildCanvasPosition(bottomIndex) + + self:getChildSize(bottomIndex) - (oldPadding - newPadding) + + local bottomTarget = Round.nearest(bottomPos + bottomBufferPx) + + self.log:trace(" Padding is {}", newPadding) + self.log:trace(" Padding should be at least {}", topBufferPx) + if newPadding < topBufferPx then + -- Minus header + local diff = newPadding - topBufferPx + size = Round.nearest(size - diff) + self.anchorCanvasPosition = Round.nearest(self.anchorCanvasPosition - diff) + newPadding = topBufferPx + self.log:trace(" Expanding canvas top to size {}", size) + self.log:trace(" Shifting anchor to {}", self.anchorCanvasPosition) + self.log:trace(" Padding is now {}", newPadding) + end + + self.log:trace(" Bottom of bottom child is {}", bottomPos) + self.log:trace(" Canvas size is {}", originalSize) + self.log:trace(" Canvas bottom should be {}", bottomTarget) + + local minSize = self:measure(self:getCurrent().AbsoluteSize) - math.max(0, newPadding) + if originalSize < minSize then + size = minSize + self.log:trace(" Expanding canvas to minimum size {}", size) + end + + if originalSize < bottomTarget then + -- Plus footer + size = math.max(bottomTarget, size) + self.log:trace(" Expanding canvas bottom to size {}", size) + end + + if size ~= originalSize or newPadding ~= originalPadding then + self.log:trace(" Changing size from {} to {}", originalSize, size) + self.log:trace(" Changing padding from {} to {}", originalPadding, newPadding) + return { + size = size, + padding = newPadding, + } + else + self.log:trace(" No changes to size or padding") + return {} + end +end + +-- Try and get the canvas as close to correct as possible this rendering pass. +function Scroller:adjustCanvas(trimTrailing, trimLeading) + self.log:trace("adjustCanvas") + + -- if our Size prop changes, do NOT adjust, onResize() will handle this + if self._sizePropChanged then + self.log:trace(" Skipping because Size prop changed") + return true + end + + local newState = Cryo.Dictionary.join( + self:resetAnchorPosition(), + self:recalculateBounds(trimTrailing, trimLeading) + ) + + if not newState.trail and not newState.lead then + newState = Cryo.Dictionary.join(newState, self:expandCanvas(newState)) + end + + if not newState.size and not newState.padding then + newState = Cryo.Dictionary.join(newState, self:adjustEdges(newState)) + end + + if Cryo.isEmpty(newState) then + self.log:trace(" No state changes after adjustment") + return false + end + + self:setState(newState) + return true +end + +function Scroller:handleResize(size) + local reverse = isReverse[self.props.orientation] + local canvasSize = Round.nearest(self:measure(self:getCurrent().CanvasSize).Offset) + local frameSize = self:measure(self:getCurrent().AbsoluteSize) + + local topBufferPx = reverse and self.leadBufferPx or self.trailBufferPx + local bottomBufferPx = reverse and self.trailBufferPx or self.leadBufferPx + + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + local topIndex = reverse and self.state.lead.index or self.state.trail.index + + local childSize = self:getChildSize(bottomIndex) + local topPos = Round.nearest(self:getChildCanvasPosition(topIndex)) - self.itemPadding + local bottomPos = Round.nearest(self:getChildCanvasPosition(bottomIndex)) + childSize + local itemSize = bottomPos + bottomBufferPx - topPos + + local frameDiff = 0 + if self.prevCycle.frameSize and size then + frameDiff = size - self.prevCycle.frameSize + end + + local bottomFrameCanvasPosition = self.prevCycle.canvasPosition + self.prevCycle.frameSize + local openCanvasAtBottom = math.max(self.prevCycle.canvasSize - bottomBufferPx - bottomFrameCanvasPosition, 0) + local openCanvasAtTop = self.prevCycle.canvasPosition + + local trimmablePadding = 0 + if frameSize < canvasSize then + trimmablePadding = topPos - topBufferPx + end + + local botAdjustment + local topAdjustment + + if frameDiff < 0 then -- contracting + if self.relativeAnchorLocation > frameSize * 0.5 then -- if we're at the bottom + topAdjustment = math.min(trimmablePadding, frameDiff) + botAdjustment = frameDiff - topAdjustment + else -- at the top + botAdjustment = frameDiff + topAdjustment = 0 + end + else -- expanding + -- first fill opposite end, then spillover to same end. + -- Any remaining space goes back to opposite end + if self.relativeAnchorLocation > frameSize * 0.5 then + topAdjustment = math.min(openCanvasAtTop, frameDiff) + local frameDiffRemainder = frameDiff - topAdjustment + botAdjustment = math.min(openCanvasAtBottom, frameDiffRemainder) + frameDiffRemainder = frameDiffRemainder - botAdjustment + topAdjustment = topAdjustment + frameDiffRemainder + else + botAdjustment = math.min(openCanvasAtBottom, frameDiff) + local frameDiffRemainder = frameDiff - botAdjustment + topAdjustment = math.min(openCanvasAtTop, frameDiffRemainder) + frameDiffRemainder = frameDiffRemainder - topAdjustment + botAdjustment = botAdjustment + frameDiffRemainder + end + + end + + -- if bottomUp scroller + if self.relativeAnchorLocation >= frameSize * 0.5 then + + local currentAnchorCanvasPosition = + self:getAnchorCanvasFromIndex(self.state.anchor.index) - self.anchorFramePosition + + local newAnchor, remainder = + self:findIndexWithRemainder(currentAnchorCanvasPosition + frameDiff - topAdjustment, + self.state.anchor.index, + false) + + + self.anchorFramePosition = math.max(remainder, 0) + self.anchorCanvasPosition = math.min(currentAnchorCanvasPosition + frameDiff, canvasSize - bottomBufferPx) + + if frameDiff > 0 then + -- self.anchorOffset affects padding + self.anchorOffset = openCanvasAtTop + self.anchorFramePosition - topAdjustment + end + + self:setState({ + anchor = {index = newAnchor, id = self:getID(newAnchor)}, + }) + else--topDown scroller + local newCanvasSize = canvasSize + if canvasSize > itemSize then + newCanvasSize = math.min(itemSize, canvasSize + botAdjustment) + end + + local newAnchor, remainder = + self:findIndexWithRemainder(self.anchorCanvasPosition - topAdjustment, + self.state.anchor.index, + false) + + -- if we expand past the amount of canvas available, move the anchor to the bottom of the first element + if math.abs(botAdjustment) < math.abs(frameDiff) then + self.anchorFramePosition = 0 + end + + if newAnchor == self.state.anchor.index then + self.anchorFramePosition = self.anchorFramePosition + topAdjustment + else + self.anchorFramePosition = self.anchorFramePosition + remainder + end + + self.anchorCanvasPosition = self.anchorCanvasPosition - topAdjustment + self:setState({ + size = newCanvasSize, + anchor = { index = newAnchor, id = self:getID(newAnchor) }, + }) + end +end + +-- Move the canvas position so that the anchor element is in the same place on the screen. +function Scroller:scrollToAnchor() + if self.motorActive then + return + end + self.log:trace("scrollToAnchor") + if self.indexChanged == nil then + self:moveToAnchor() + end + + local newIndex = self.indexChanged.newIndex + local previousIndex = self.indexChanged.oldIndex + self.log:trace(" newIndex {}", newIndex) + self.log:trace(" previousIndex {}", previousIndex) + + local oldPos = self:measure(self:getCurrent().CanvasPosition) + self.relativeAnchorLocation + local newPos = self:getAnchorCanvasFromIndex(newIndex) + + self.log:trace(" old anchor pos {}", oldPos) + self.log:trace(" new anchor pos {}", newPos) + + self.indexChanged.currentPos = oldPos + self.indexChanged.newPos = newPos + self.motorActive = true + self.springLock = self.springLock + 1 + + local delta = newPos - oldPos + self.log:trace(" delta {}", delta) + self.motor = Otter.createSingleMotor(0) + self.motor:onStep(self.motorOnStep) + self.motor:onComplete(self.motorOnComplete) + self.motor:setGoal(Otter.spring(delta, self.props.animateOptions)) +end + +-- Move the canvas position so that the anchor element is in the same place on the screen. +function Scroller:moveToAnchor() + self.log:trace("moveToAnchor") + if self.motorActive then + return + end + if self:isScrollingWithElasticBehavior() then + return + end + + local currentPos = self:getAnchorFramePosition() + self.log:trace(" Anchor was at frame position {}", self.anchorFramePosition) + self.log:trace(" Anchor is currently at frame position {}", currentPos) + local newPos = self:measure(self:getCurrent().CanvasPosition) + currentPos - self.anchorFramePosition + self.log:trace(" Canvas should scroll to {}", newPos) + local current = self:getCurrent() + local maxPos = math.max(0, self:measure(current.CanvasSize).Offset - self:measure(current.AbsoluteSize)) + + self:setScroll(newPos) + if newPos < 0 then + self.log:trace(" Canvas scroll limited to 0, was {}", newPos) + self.anchorOffset = Round.towardsZero(newPos) + elseif newPos >= maxPos then + self.log:trace(" Canvas scroll limited to {}, was {}", maxPos, newPos) + self.anchorOffset = Round.towardsZero(newPos - maxPos) + else + self.log:trace(" Clearing anchorOffset") + self.anchorOffset = 0 + end +end + +-- Prevent scrolling when experiencing elastic behavior on touch devices +function Scroller:isScrollingWithElasticBehavior() + -- When the canvas is resized the bottom of the canvas is bigger than the canvas for + -- the first update. Since a resize is not an elastic scroll skip these checks after a resize + if self.resized then + return false + end + + -- Check if the top of the list has scrolled past the frame because of ElasticBehavior + local reverse = isReverse[self.props.orientation] + local topIndex = reverse and self.state.lead.index or self.state.trail.index + local startOfListIndex = reverse and self.state.listSize or 1 + if self:measure(self:getCurrent().CanvasPosition) < 0 and topIndex == startOfListIndex then + return true + end + + -- Check if the bottom of the list has scrolled past the frame because of ElasticBehavior + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + local frameSize = self:measure(self:getCurrent().AbsoluteSize) + local canvasPosition = self:measure(self:getCurrent().CanvasPosition) + local endOfListIndex = reverse and 1 or self.state.listSize + local bottomOfCanvas = Round.nearest(canvasPosition + frameSize) + local canvasSize = Round.nearest(self:measure(self:getCurrent().CanvasSize).Offset) + + if bottomOfCanvas > canvasSize and bottomIndex == endOfListIndex and canvasPosition > 0 then + return true + end + + return false +end + +-- Call loadNext and loadPrevious if needed. +function Scroller:loadMore() + self.log:trace("loadMore") + if self.props.loadPrevious and + self.state.trail.index <= self.props.loadingBuffer and + self.props.itemList ~= self.lastLoadPrevItems then + self.log:trace(" Calling loadPrevious") + self.lastLoadPrevItems = self.props.itemList + self.props.loadPrevious() + end + if self.props.loadNext and + self.state.lead.index > self.state.listSize - self.props.loadingBuffer and + self.props.itemList ~= self.lastLoadNextItems then + self.log:trace(" Calling loadNext") + self.lastLoadNextItems = self.props.itemList + self.props.loadNext() + end +end + +-- Set the current canvas position according to Orientation without calling +-- onScroll. +function Scroller:setScroll(pos) + self.log:trace(" Scrolling to {}", pos) + self.scrollDebounce = true + self:getCurrent().CanvasPosition = isVertical[self.props.orientation] + and Vector2.new(self:getCurrent().CanvasPosition.X, pos) + or Vector2.new(pos, self:getCurrent().CanvasPosition.Y) + self.scrollDebounce = false +end + +-- Scroll by a relative amount +function Scroller:scrollRelative(amount) + self.log:trace(" Current CanvasPosition {}", self:getCurrent().CanvasPosition) + self.log:trace("self.motorActive {}", self.motorActive) + self:setScroll(self:measure(self:getCurrent().CanvasPosition) + amount, true) + self.onScroll(self:getCurrent()) +end + + +-- Returns the signed distance from the element to the given canvas-relative +-- position, or 0 if the element overlaps it. The sign of the distance is +-- relative to the list indices. For this distance calculation, the padding +-- between elements is considered part of the current element. Returns nil if +-- the element is not currently rendered. +function Scroller:distanceToPosition(index, pos) + local child = self:getRbx(index) + if not child then + return nil + end + + local childTop = self:absoluteToCanvasPosition(self:measure(child.AbsolutePosition)) - self.itemPadding + local childSize = self:measure(child.AbsoluteSize) + 2 * self.itemPadding + + return Round.nearest(Distance.fromPointToRangeSigned(pos, childTop, childSize) * direction[self.props.orientation]) +end + +-- Get the canvas-relative position of the current anchor element. +function Scroller:getAnchorCanvasPosition() + return self:getAnchorCanvasFromIndex(self.state.anchor.index) +end + +function Scroller:getAnchorCanvasFromIndex(index) + local scale = self.props.anchorLocation.Scale + if not isReverse[self.props.orientation] then + scale = 1 - scale + end + + return Round.nearest(self:getChildCanvasPosition(index) + scale * self:getChildSize(index)) +end + +-- Get the frame-relative position of the current anchor element. +function Scroller:getAnchorFramePosition() + return self:getAnchorFrameFromIndex(self.state.anchor.index) +end + +function Scroller:getAnchorFrameFromIndex(index) + local scale = self.props.anchorLocation.Scale + if not isReverse[self.props.orientation] then + scale = 1 - scale + end + + return Round.nearest(self:getChildFramePosition(index) + scale * self:getChildSize(index)) + - self.relativeAnchorLocation +end + +-- Convert an AbsolutePosition to a position relative to the top-left corner of the canvas. +function Scroller:absoluteToCanvasPosition(position) + local current = self:getCurrent() + local canvas = current.CanvasPosition + local absolute = current.AbsolutePosition + return position + self:measure(canvas) - self:measure(absolute) +end + +-- Convert an AbsolutePosition to a position relative to the top-left corner of the scrolling frame. +function Scroller:absoluteToFramePosition(position) + local current = self:getCurrent() + local absolute = current.AbsolutePosition + return position - self:measure(absolute) +end + +-- Convert a position relative to the frame to a position relative to the canvas. +function Scroller:frameToCanvasPosition(position) + local current = self:getCurrent() + local canvas = current.CanvasPosition + return position + self:measure(canvas) +end + +-- Get the canvas-relative position of the element at the specified index. +function Scroller:getChildCanvasPosition(index) + local current = self:getRbx(index) + return current and self:absoluteToCanvasPosition(self:measure(current.AbsolutePosition)) or 0 +end + +-- Get the frame-relative position of the element at the specified index. +function Scroller:getChildFramePosition(index) + local current = self:getRbx(index) + return current and self:absoluteToFramePosition(self:measure(current.AbsolutePosition)) or 0 +end + +-- Get the absolute size of the element at the specified index. +function Scroller:getChildSize(index) + local current = self:getRbx(index) + return current and self:measure(current.AbsoluteSize) or 0 +end + +-- Get the ID of an element at a specific index. +function Scroller:getID(index) + return self.props.identifier(self.props.itemList[index]) +end + +-- Create or update a metadata entry for the given element. This can't use +-- self.props in willUpdate as any props it uses could be out of date. +function Scroller:updateMetadata(id, item, props) + local meta = self.metadata[id] + if not meta then + meta = {} + self.metadata[id] = meta + end + + if not meta.name then + local elem = props.renderItem(item, false) + meta.class = tostring(elem.component) + local pool = self:getKeyPool(meta.class) + meta.name = pool:get() + end + + if not self.refpool[meta.name] then + self.refpool[meta.name] = Roact.createRef() + end + meta.ref = self.refpool[meta.name] +end + +-- Clear the metadata for an element that is being unloaded. +function Scroller:clearMetadata(id) + local meta = self.metadata[id] + if not meta then + return + end + + -- Not releasing the names seems like it would be a memory leak, but this + -- relies on the fact that the key pool does not track in use keys. Rather, + -- the key tracks which pool it came from. So if nothing is using an + -- unreleased key, it will be garbage collected and never reused. + if not Cryo.List.find(self.props.recyclingDisabledFor, meta.class) then + meta.name:release() + end + meta.name = nil + meta.ref = nil +end + +-- Get the key pool for the given class of elements, or create a new one if that doesn't exist yet. +function Scroller:getKeyPool(class) + if not self.pools[class] then + self.pools[class] = KeyPool.new(class) + end + return self.pools[class] +end + +-- Get the metadata info for the element at the specified index. +function Scroller:getMetadata(index) + return self.metadata[self:getID(index)] +end + +-- Get the current Roblox instance from the ref stored in the metadata. +function Scroller:getRbx(index) + local meta = self:getMetadata(index) + return meta and meta.ref and meta.ref.current +end + +-- Return X or Y depending on the orientation. +function Scroller:measure(vecOrUDim2) + return isVertical[self.props.orientation] and vecOrUDim2.Y or vecOrUDim2.X +end + +-- Get the current ScrollingFrame instance. +function Scroller:getCurrent() + return self:getRef().current +end + +function Scroller:getRef() + -- Make sure to get the ref from props if that exists. + return self.props[Roact.Ref] or self._ref +end + +return Scroller diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/TimeLogger.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/TimeLogger.lua new file mode 100644 index 0000000..ee7237e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/TimeLogger.lua @@ -0,0 +1,10 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Lumberyak = require(Root.Lumberyak) + +local TimeLogger = Lumberyak.Logger.new(nil, script:GetFullName()) +TimeLogger:setContext({ + tick = tick, + prefix = "{tick}: ", +}) + +return TimeLogger diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/findNewIndices.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/findNewIndices.lua new file mode 100644 index 0000000..c0a8856 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/findNewIndices.lua @@ -0,0 +1,67 @@ +-- Find the new indicies of the trailing, anchor and leading elements. +-- props is expected to contain itemList, the identifier function and the maximum search distance. +-- state is expected to contain the old top, anchor and bottom indices and ids. +return function(props, state): (number?, number?, number?) + local topIndex = state.trail.index + local topID = state.trail.id + local anchorIndex = state.anchor.index + local anchorID = state.anchor.id + local bottomIndex = state.lead.index + local bottomID = state.lead.id + + local listSize = #props.itemList + + -- If too much got deleted and the previous anchor index is off the bottom of the list, start the search from + -- the bottom. + if topIndex > listSize then + topIndex = listSize + end + if anchorIndex > listSize then + anchorIndex = listSize + end + if bottomIndex > listSize then + bottomIndex = listSize + end + + -- No access to self:getID + local getID = function(index) + return props.identifier(props.itemList[index]) + end + local topStill = getID(topIndex) == topID + local anchorStill = getID(anchorIndex) == anchorID + local bottomStill = getID(bottomIndex) == bottomID + if topStill and anchorStill and bottomStill then + -- Nothing important moved + return topIndex, anchorIndex, bottomIndex + end + + local step = 0 + local foundTop = topStill and topIndex or nil + local foundAnchor = nil + local foundBottom = bottomStill and bottomIndex or nil + + -- Scan outward from the old anchor index until we find the top and bottom or hit the max distance + local deltas = {top=-1, bottom=1} + repeat + for _, delta: number in pairs(deltas) do + local pos = anchorIndex + delta * step + + if pos >= 1 and pos <= listSize then + local id = getID(pos) + if id == topID then + foundTop = pos + end + if id == anchorID then + foundAnchor = pos + end + if id == bottomID then + foundBottom = pos + end + end + end + + step = step + 1 + until (foundTop and foundAnchor and foundBottom) or step > props.maximumSearchDistance + + return foundTop, foundAnchor, foundBottom +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/relocateIndices.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/relocateIndices.lua new file mode 100644 index 0000000..7e330c3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/Components/relocateIndices.lua @@ -0,0 +1,71 @@ +local Round = require(script.Parent.Round) + +export type Indices = { + anchorIndex: number?, + leadIndex: number?, + trailIndex: number?, +} + +-- Returns the three new indices with any nils filled in and any misorderings corrected. +-- new and old should be tables containing an anchorIndex, a leadIndex and a trailIndex. +return function(new: Indices, old: Indices, listSize: number): Indices + local isNewAnchorPresent: boolean = new.anchorIndex ~= nil + local isNewLeadPresent: boolean = new.leadIndex ~= nil + local isNewTrailPresent: boolean = new.trailIndex ~= nil + + local finalAnchorIndex: number = tonumber(new.anchorIndex or 0) + local finalLeadIndex: number = tonumber(new.leadIndex or 0) + local finalTrailIndex: number = tonumber(new.trailIndex or 0) + + local oldAnchorIndex: number = tonumber(old.anchorIndex or 0) + local oldLeadIndex: number = tonumber(old.leadIndex or 0) + local oldTrailIndex: number = tonumber(old.trailIndex or 0) + + -- There are 8 possibilities here as any combination of these could be deleted. Also, we can't use findIndexAt + -- here since that requires access to the children's measurements. + if not isNewAnchorPresent then + if isNewLeadPresent and isNewTrailPresent then + -- Estimate that the new anchor is proportionally the same distance from the lead and trail indices. + if finalLeadIndex == finalTrailIndex then + -- Guard against divide by zero. + finalAnchorIndex = finalLeadIndex + else + local oldRatio = (oldAnchorIndex - oldLeadIndex) / (oldTrailIndex - oldLeadIndex) + finalAnchorIndex = Round.nearest((finalTrailIndex - finalLeadIndex) * oldRatio + finalLeadIndex) + finalAnchorIndex = math.min(math.max(finalAnchorIndex, 1), listSize) + end + elseif isNewLeadPresent then + -- Given only the new leading index, estimate that the new anchor is the same distance away as it was. + finalAnchorIndex = finalLeadIndex + oldAnchorIndex - oldLeadIndex + finalAnchorIndex = math.min(math.max(finalAnchorIndex, 1), listSize) + elseif isNewTrailPresent then + -- Given only the new trailing index, estimate that the new anchor is the same distance away as it was. + finalAnchorIndex = finalTrailIndex + oldAnchorIndex - oldTrailIndex + finalAnchorIndex = math.min(math.max(finalAnchorIndex, 1), listSize) + else + -- Everything is gone. Just reuse the same index if that's still within the bounds of the list. + finalAnchorIndex = math.min(math.max(oldAnchorIndex, 1), listSize) + end + end + + -- If the leading and trailing indices haven't been worked out yet, estimate that the new ones should be the + -- same distance from the anchor as the old ones were. + if not isNewTrailPresent then + finalTrailIndex = finalAnchorIndex + oldTrailIndex - oldAnchorIndex + finalTrailIndex = math.min(math.max(finalTrailIndex, 1), listSize) + end + if not isNewLeadPresent then + finalLeadIndex = finalAnchorIndex + oldLeadIndex - oldAnchorIndex + finalLeadIndex = math.min(math.max(finalLeadIndex, 1), listSize) + end + + -- Make sure the resulting indices are in the right order. + local minIndex = math.min(finalAnchorIndex, finalLeadIndex, finalTrailIndex) + local maxIndex = math.max(finalAnchorIndex, finalLeadIndex, finalTrailIndex) + + return { + trailIndex = minIndex, + anchorIndex = finalAnchorIndex, + leadIndex = maxIndex, + } +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/init.lua new file mode 100644 index 0000000..25fe17c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/infinite-scroller/init.lua @@ -0,0 +1,7 @@ +local Scroller = require(script.Components.Scroller) +local Logger = require(script.Components.Logger) + +return { + Scroller = Scroller, + Logger = Logger, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/lock.toml new file mode 100644 index 0000000..09d4e7e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/lock.toml @@ -0,0 +1,13 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/infinite-scroller" +version = "0.7.1" +commit = "7286a92274106024c813d7a9364e84e1597f9034" +source = "git+https://github.com/roblox/infinite-scroller#v0.7.1" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Lumberyak roblox/lumberyak 0.1.1 url+https://github.com/roblox/lumberyak", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/t.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-3aafd7fe-7286a922/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/FitFrame.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/FitFrame.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/FitFrame.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Otter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/KeyPool.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/KeyPool.lua new file mode 100644 index 0000000..55d6006 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/KeyPool.lua @@ -0,0 +1,80 @@ +--[[ + KeyPool provides a pool of objects suitable for use as map keys. + + Create a new KeyPool, then call pool:get() to get a new key. Once you're done with it, call key:release(). + + Example: + local pool = KeyPool.new("foo") + ... + local key1 = pool:get() + local key2 = pool:get() + map[key1] = thing1 + map[key2] = thing2 + ... + map[key1] = nil + key1:release() + key1 = nil +]] +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local t = require(Root.t) + +-- Forward declarations +local KeyPool = {} +KeyPool.__index = KeyPool + +local Key = {} +Key.__index = Key + +-- This is Key.new, but we don't want to expose that publicly. +local function newkey(pool, index) + local key = { + pool = pool, + index = index, + } + + setmetatable(key, Key) + return key +end + +-- KeyPool functions + +function KeyPool.new(class) + assert(t.string(class)) + + local pool = { + class = class, + available = {}, + limit = 0, + count = 0, + } + + setmetatable(pool, KeyPool) + return pool +end + +-- Get a currently unused key, or create a new one if everything is in use. +function KeyPool:get() + if self.count == 0 then + self.limit = self.limit + 1 + return newkey(self, self.limit) + end + + local key = self.available[self.count] + self.count = self.count - 1 + return key +end + +-- Key functions + +function Key:__tostring() + return self.pool.class .. "_" .. tostring(self.index) +end + +-- Return this key to the pool it came from. Whatever previously held this key should not keep the reference after +-- calling this. +function Key:release() + self.pool.count = self.pool.count + 1 + self.pool.available[self.pool.count] = self +end + +return KeyPool diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/NotifyReady.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/NotifyReady.lua new file mode 100644 index 0000000..a564707 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/NotifyReady.lua @@ -0,0 +1 @@ +return {} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Orientation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Orientation.lua new file mode 100644 index 0000000..a31399b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Orientation.lua @@ -0,0 +1,31 @@ +-- Enum for specifying the leading edge of the scroller. +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local t = require(Root.t) + +local Orientation = { + Up = "Orientation.Up", + Down = "Orientation.Down", + Left = "Orientation.Left", + Right = "Orientation.Right", +} + +local metaindex = { + isOrientation = t.union( + t.literal(Orientation.Up), + t.literal(Orientation.Down), + t.literal(Orientation.Left), + t.literal(Orientation.Right) + ) +} + +setmetatable(Orientation, { + __index = function(self, key) + return metaindex[key] or + error(tostring(key) .. " is not a valid member of Scroller.Orientation", 2) + end, + __newindex = function() + error("Scroller.Orientation is read-only", 2) + end, +}) + +return Orientation diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Round.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Round.lua new file mode 100644 index 0000000..2755954 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Round.lua @@ -0,0 +1,28 @@ +local epsilon = 1e-15 + +return { + nearest = function(num) + local q, r = math.modf(num) + if r <= -0.5 then + return q - 1 + elseif r >= 0.5 then + return q + 1 + else + return q + end + end, + towardsZero = function(num) + local result, _ = math.modf(num) + return result + end, + awayFromZero = function(num) + local q, r = math.modf(num) + if r < -epsilon then + return q - 1 + elseif r > epsilon then + return q + 1 + else + return q + end + end, +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Scroller.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Scroller.lua new file mode 100644 index 0000000..2536967 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/Scroller.lua @@ -0,0 +1,866 @@ +local RunService = game:GetService("RunService") + +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local t = require(Root.t) + +local FitFrame = require(Root.FitFrame).FitFrameOnAxis +local findNewIndices = require(script.Parent.findNewIndices) +local Round = require(script.Parent.Round) +local KeyPool = require(script.Parent.KeyPool) + +local NotifyReady = require(script.Parent.NotifyReady) + +local debugPrint = function() end + +local Scroller = Roact.PureComponent:extend("Scroller") + +Scroller.Orientation = require(script.Parent.Orientation) + +local isVertical = { + [Scroller.Orientation.Up] = true, + [Scroller.Orientation.Down] = true, + [Scroller.Orientation.Left] = false, + [Scroller.Orientation.Right] = false, +} + +local isReverse = { + [Scroller.Orientation.Up] = true, + [Scroller.Orientation.Down] = false, + [Scroller.Orientation.Left] = true, + [Scroller.Orientation.Right] = false, +} + +local direction = { + [Scroller.Orientation.Up] = -1, + [Scroller.Orientation.Down] = 1, + [Scroller.Orientation.Left] = -1, + [Scroller.Orientation.Right] = 1, +} + +Scroller.validateProps = t.strictInterface({ + -- Required. The list of items to scroll through. + itemList = t.array(t.any), + + -- Required. A callback function, called with each visible item in the itemList when the list is rendered. + renderItem = t.callback, + + -- A function to uniquely identify list items. Calling this on the same item twice should give the same result + -- accoring to ==. + identifier = t.optional(t.callback), + + -- One of the Scroller.Orientation enums. Determines the leading edge of the infinite scroll. + orientation = t.optional(Scroller.Orientation.isOrientation), + + -- A callback function, called when the infinite scroll reaches the leading end of the itemList (index + -- #itemList). + loadNext = t.optional(t.callback), + + -- A callback function, called when the infinite scroll reaches the trailing end of the itemList (index 1). + loadPrevious = t.optional(t.callback), + + -- Padding between elements in the scrolling frame. The Scale is relative to the size of the scrolling frame. + padding = t.optional(t.UDim), + + -- The minimum number of unmounted elements to keep at the top and bottom of the list. If there are fewer than + -- this call loadNext or loadPrevious. + loadingBuffer = t.optional(t.numberPositive), + + -- The amount of space above and below the view to render items in. + mountingBuffer = t.optional(t.numberPositive), + + -- The amount of empty space to keep at the top and bottom on the scroll. + dragBuffer = t.optional(t.numberPositive), + + -- An initial guess at the average size of an item. + estimatedItemSize = t.optional(t.numberPositive), + + -- The maximum distance to search for moved elements. + maximumSearchDistance = t.optional(t.numberPositive), + + -- The element to put in focus initially. + focusIndex = t.optional(t.integer), + + -- An arbitrary value to prevent the list from refocusing every render. Change this to cause the list to reset + -- and refocus on the new focusIndex. + focusLock = t.optional(t.any), + + -- The position within the view to keep still as other things move. The Scale is relative to the size of the + -- scrolling frame. + anchorLocation = t.optional(t.UDim), + + ---- INTERNAL ONLY ---- + [NotifyReady] = t.any, +}) + +-- Default values for all the infinite-scroller-specific props. Any prop not in this list will be passed on to the +-- underlying ScrollingFrame. +Scroller.defaultProps = { + itemList = {}, + renderItem = {}, + identifier = function(item) + return item + end, + orientation = Scroller.Orientation.Down, + loadNext = false, + loadPrevious = false, + padding = UDim.new(0, 0), + loadingBuffer = 10, + mountingBuffer = 200, + dragBuffer = 100, + estimatedItemSize = 50, + maximumSearchDistance = 100, + focusIndex = 1, + focusLock = {}, + anchorLocation = UDim.new(0, 0), + [NotifyReady] = false, +} + +function Scroller:render() + debugPrint("render") + + -- Gather vertical/horizontal specific variables. + local axis = isVertical[self.props.orientation] and { + fillDirection = Enum.FillDirection.Vertical, + fitDirection = FitFrame.Axis.Vertical, + minimumSize = UDim2.new(1, 0, 0, 0), + canvasSize = UDim2.new(0, 0, 0, self.state.size), + paddingSide = "PaddingTop", + } or { + fillDirection = Enum.FillDirection.Horizontal, + fitDirection = FitFrame.Axis.Horizontal, + minimumSize = UDim2.new(0, 0, 1, 0), + canvasSize = UDim2.new(0, self.state.size, 0, 0), + paddingSide = "PaddingLeft", + } + + -- Remove non-standard props from list to pass on to ScrollingFrame. These are the same props given in + -- defaultProps. + local props = Cryo.Dictionary.join( + self.props, + self.propsToClear, + { + CanvasSize = axis.canvasSize, + [Roact.Change.CanvasPosition] = self.onScroll, + [Roact.Change.AbsoluteSize] = self.onResize, + [Roact.Ref] = self:getRef(), + } + ) + + local children = { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = axis.fillDirection, + Padding = UDim.new(0, self.itemPadding), + [Roact.Change.AbsoluteContentSize] = self.onContentResize, + }), + padding = Roact.createElement("UIPadding", { + [axis.paddingSide] = UDim.new(0, self.state.padding), + }), + } + + -- Trailing and leading indicies won't be set if this isn't true. + if self.state.ready and not Cryo.isEmpty(self.props.itemList) then + debugPrint(" Rendering elements between", self.state.trail.index, "and", self.state.lead.index) + for n = self.state.trail.index, self.state.lead.index do + local metadata = self:getMetadata(n) + children[metadata.name] = Roact.createElement(FitFrame, { + minimumSize = axis.minimumSize, + axis = axis.fitDirection, + FillDirection = axis.fillDirection, + BackgroundTransparency = 1, + LayoutOrder = isReverse[self.props.orientation] and -n or n, + [Roact.Ref] = metadata.ref, + }, { + item = self.props.renderItem(self.props.itemList[n], false), + }) + end + end + + return Roact.createElement("ScrollingFrame", props, children) +end + +function Scroller:willUpdate(nextProps, nextState) + debugPrint("willUpdate") + + if not nextState.ready then + return + end + + self.sizeDebounce = true + + local deletions = {} + local additions = {} + + if not Cryo.isEmpty(self.props.itemList) and self.state.lead then + for n = self.state.trail.index, self.state.lead.index do + local id = self.props.identifier(self.props.itemList[n]) + deletions[id] = true + end + end + + if not Cryo.isEmpty(nextProps.itemList) and nextState.lead then + for n = nextState.trail.index, nextState.lead.index do + local item = nextProps.itemList[n] + local id = nextProps.identifier(item) + if deletions[id] then + -- Element is in both ranges. + deletions[id] = nil + else + additions[id] = item + end + end + end + + -- Clear names first, so new items can use them. + for id, _ in pairs(deletions) do + self:clearMetadata(id) + end + for id, item in pairs(additions) do + self:updateMetadata(id, item, nextProps) + end + + -- The focus lock changed, clear the non-state anchor variables. + if self.state.lastFocusLock ~= nextState.lastFocusLock then + self.anchorFramePosition = 0 + self.anchorCanvasPosition = self.relativeAnchorLocation + end +end + +function Scroller:didUpdate(previousProps, previousState) + debugPrint("didUpdate") + + if Cryo.isEmpty(self.props.itemList) then + return + end + + if not self.state.ready then + self.onResize(self:getRef().current) + return + end + + if not self:adjustCanvas(self.scrollingForward, self.scrollingBackward) then + self:moveToAnchor() + self:loadMore() + self.sizeDebounce = false + end +end + +function Scroller.getDerivedStateFromProps(nextProps, lastState) + debugPrint("getDerivedStateFromProps") + if not lastState.ready or Cryo.isEmpty(nextProps.itemList) then + return + end + + local listSize = #nextProps.itemList + + -- Reset the state if the focus lock changes. This is guaranteed to be true the first time. + if lastState.lastFocusLock ~= nextProps.focusLock then + debugPrint(" Resetting focus") + local focusID = nextProps.identifier(nextProps.itemList[nextProps.focusIndex]) + return { + listSize = listSize, + trail = {index=nextProps.focusIndex, id=focusID}, + anchor = {index=nextProps.focusIndex, id=focusID}, + lead = {index=nextProps.focusIndex, id=focusID}, + padding = 0, + size = 0, + lastFocusLock = nextProps.focusLock, + } + end + + local trailIndex, anchorIndex, leadIndex = findNewIndices(nextProps, lastState) + debugPrint(" Trailing index moved from", lastState.trail.index, "to", trailIndex) + debugPrint(" Anchor index moved from", lastState.anchor.index, "to", anchorIndex) + debugPrint(" Leading index moved from", lastState.lead.index, "to", leadIndex) + + -- There are 8 possibilities here as any combination of these could be deleted. Also, we can't use findIndexAt + -- here since that requires access to the children's measurements. + if not anchorIndex then + if leadIndex and trailIndex then + -- Estimate that the new anchor is proportionally the same distance from the lead and trail indices. + if leadIndex == trailIndex then + -- Guard against divide by zero. + anchorIndex = leadIndex + else + local oldRatio = (lastState.anchor.index - lastState.lead.index) + / (lastState.trail.index - lastState.lead.index) + anchorIndex = Round.nearest((trailIndex - leadIndex) * oldRatio + leadIndex) + anchorIndex = math.min(math.max(anchorIndex, 1), listSize) + end + elseif leadIndex then + -- Given only the new leading index, estimate that the new anchor is the same distance away as it was. + anchorIndex = leadIndex + lastState.anchor.index - lastState.lead.index + anchorIndex = math.min(math.max(anchorIndex, 1), listSize) + elseif trailIndex then + -- Given only the new trailing index, estimate that the new anchor is the same distance away as it was. + anchorIndex = trailIndex + lastState.anchor.index - lastState.trail.index + anchorIndex = math.min(math.max(anchorIndex, 1), listSize) + else + -- Everything is gone. Just reuse the same index if that's still within the bounds of the list. + anchorIndex = math.min(math.max(lastState.anchor.index, 1), listSize) + end + debugPrint(" Anchor index moved to", anchorIndex) + end + + -- If the leading and trailing indices haven't been worked out yet, estimate that the new ones should be the + -- same distance from the anchor as the old ones were. + if not trailIndex then + trailIndex = anchorIndex + lastState.trail.index - lastState.anchor.index + trailIndex = math.min(math.max(trailIndex, 1), listSize) + debugPrint(" Trailing index moved to", trailIndex) + end + if not leadIndex then + leadIndex = anchorIndex + lastState.lead.index - lastState.anchor.index + leadIndex = math.min(math.max(leadIndex, 1), listSize) + debugPrint(" Leading index moved to", leadIndex) + end + + local trailID = nextProps.identifier(nextProps.itemList[trailIndex]) + local anchorID = nextProps.identifier(nextProps.itemList[anchorIndex]) + local leadID = nextProps.identifier(nextProps.itemList[leadIndex]) + + return { + listSize = listSize, + trail = {index=trailIndex, id=trailID}, + anchor = {index=anchorIndex, id=anchorID}, + lead = {index=leadIndex, id=leadID}, + } +end + +function Scroller:init() + debugPrint("init") + + -- Only self:getRef() should access this. + self._ref = Roact.createRef() + + self.scrollDebounce = false + self.sizeDebounce = true + + self.onScroll = function(rbx) + debugPrint("onScroll") + debugPrint(" CanvasPosition is", rbx.CanvasPosition) + if self.scrollDebounce then + debugPrint(" Debouncing scroll") + return + end + + local delta, newState = self:recalculateAnchor() + self.scrollingBackward = delta < 0 + self.scrollingForward = delta > 0 + debugPrint(" Delta is", delta) + + if not Cryo.isEmpty(newState) then + self:setState(newState) + end + + -- Handle any passed in scroll callback. + if self.props[Roact.Change.CanvasPosition] then + self.props[Roact.Change.CanvasPosition](rbx) + end + end + + self.onResize = function(rbx) + debugPrint("onResize") + local size = self:measure(rbx.AbsoluteSize) + local pos = self:measure(rbx.AbsolutePosition) + + self.itemPadding = self.props.padding.Scale * size + self.props.padding.Offset + if isReverse[self.props.orientation] then + self.relativeAnchorLocation = self.props.anchorLocation.Scale * size + self.props.anchorLocation.Offset + else + self.relativeAnchorLocation = (1 - self.props.anchorLocation.Scale) * size + - self.props.anchorLocation.Offset + end + self.absoluteAnchorLocation = self.relativeAnchorLocation + pos + self.mountAboveAnchor = self.relativeAnchorLocation + self.props.mountingBuffer + self.mountBelowAnchor = size - self.relativeAnchorLocation + self.props.mountingBuffer + + -- Handle any passed in resize callback. + if self.props[Roact.Change.AbsoluteSize] then + self.props[Roact.Change.AbsoluteSize](rbx) + end + + if not self.state.ready then + debugPrint(" Setting initial anchor position to", self.relativeAnchorLocation) + self.anchorFramePosition = 0 + self.anchorCanvasPosition = self.relativeAnchorLocation + + coroutine.wrap(function() + RunService.Heartbeat:Wait() + self:setState({ + ready = true + }) + + -- This should only be set by tests. + if self.props[NotifyReady] then + self.props[NotifyReady]:Fire() + end + end)() + else + self:moveToAnchor() + self:setState({}) -- Force a rerender. + end + end + + self.onContentResize = function() + debugPrint("onContentResize") + + if self.sizeDebounce or not self.state.ready then + debugPrint(" Skipping onContentResize") + return + end + + self:moveToAnchor() + self:setState({}) -- Force a rerender. + end + + self.anchorCanvasPosition = 0 + self.anchorFramePosition = 0 + + self.metadata = {} + self.pools = {} + self.refpool = {} + + self.scrollingBackward = false + self.scrollingForward = false + + -- Store the list of props to not pass on to the underlying scrolling frame. + self.propsToClear = {} + for k, _ in pairs(Scroller.defaultProps) do + self.propsToClear[k] = Cryo.None + end + + -- This will get updated shortly, but one render will happen before state.ready is set + self.state = { + ready = false, + lastFocusLock = nil, + padding = 0, + size = 0, + } +end + +-- Find which element is currently closest to the anchor position. +function Scroller:recalculateAnchor() + debugPrint("recalculateAnchor") + + -- Find the index of the element at the appropriate position + local index = self:findIndexAt( + self:absoluteToCanvasPosition(self.absoluteAnchorLocation), self.state.anchor.index, false) + + local delta + if index == self.state.anchor.index then + debugPrint(" Current anchor still works") + return 0, {} + elseif index < self.state.anchor.index then + delta = -1 + else + delta = 1 + end + + debugPrint(" New anchor at index", index) + + -- Store the new anchor's details + self.anchorCanvasPosition = self:getAnchorCanvasFromIndex(index) + self.anchorFramePosition = self:getAnchorFrameFromIndex(index) + debugPrint(" New anchor at canvas position", self.anchorCanvasPosition) + debugPrint(" New anchor at frame position", self.anchorFramePosition) + return delta, { + anchor = {index=index, id=self:getID(index)}, + } +end + +-- Move all the rendered elements up or down to put the anchor back where it was. +function Scroller:resetAnchorPosition() + debugPrint("resetAnchorPosition") + debugPrint(" Anchor index is", self.state.anchor.index) + local offset = self:getAnchorCanvasPosition() + debugPrint(" Anchor is at", offset) + debugPrint(" Anchor should be at", self.anchorCanvasPosition) + local diff = math.floor(self.anchorCanvasPosition - offset + 0.5) + if diff ~= 0 then + debugPrint(" Changing padding from", self.state.padding, "to", (self.state.padding + diff)) + return {padding = self.state.padding + diff} + else + return {} + end +end + +-- Get the current padding from the UIPadding child. +function Scroller:getCurrentPadding() + local pad = self:getCurrent().padding + -- Only one of these will be non-zero + return pad.PaddingTop.Offset + pad.PaddingLeft.Offset +end + +-- Move the top and bottom of the range to be rendered up and down to make sure enough things are being rendered. +function Scroller:recalculateBounds(trimTrailing, trimLeading) + debugPrint("recalculateBounds") + debugPrint(" Leading index was", self.state.lead.index) + debugPrint(" Trailing index was", self.state.trail.index) + + local anchorPos = self:getAnchorCanvasPosition() + local mountTop = anchorPos - self.mountAboveAnchor + local mountBottom = anchorPos + self.mountBelowAnchor + debugPrint(" Target for top at", mountTop) + debugPrint(" Target for bottom at", mountBottom) + + local topIndex = self:findIndexAt(mountTop, nil, true) + debugPrint(" Found new top index at", topIndex) + local bottomIndex = self:findIndexAt(mountBottom, nil, true) + debugPrint(" Found new bottom index at", bottomIndex) + + local leadIndex = math.max(topIndex, bottomIndex) + if leadIndex < self.state.lead.index and not trimLeading then + leadIndex = self.state.lead.index + end + + local trailIndex = math.min(topIndex, bottomIndex) + if trailIndex > self.state.trail.index and not trimTrailing then + trailIndex = self.state.trail.index + end + + if trailIndex < self.state.trail.index or leadIndex > self.state.lead.index then + debugPrint(" Changing leading index to", leadIndex) + debugPrint(" Changing trailing index to", trailIndex) + return { + trail = {index=trailIndex, id=self:getID(trailIndex)}, + lead = {index=leadIndex, id=self:getID(leadIndex)}, + } + else + return {} + end +end + +-- Find the index of the element that overlaps the given canvas-relative position. +function Scroller:findIndexAt(targetPos, hintIndex, extrapolate) + debugPrint(" findIndexAt") + -- Get the distance from the hinted index or the anchor. + local currentIndex = hintIndex or self.state.anchor.index + local currentDist = self:distanceToPosition(currentIndex, targetPos) + debugPrint(" Searching from index", currentIndex) + debugPrint(" Position is", currentDist, "from", targetPos) + if currentDist == 0 then + return currentIndex + end + + -- Get the distance from one end of the list. + local nextIndex = (currentDist < 0) and self.state.trail.index or self.state.lead.index + debugPrint(" Nearest end at", nextIndex) + if currentIndex == nextIndex then + debugPrint(" Hint index already at end") + -- If the target position lies outside of the loaded elements. + if currentIndex + currentDist < self.state.trail.index + or currentIndex + currentDist > self.state.lead.index then + debugPrint(" Target out of bounds") + if not extrapolate then + -- Do not extrapolate. Return the closest loaded element. + return currentIndex + end + + -- Extrapolate using the estimated item size. + local delta = Round.awayFromZero(currentDist / self.props.estimatedItemSize) + debugPrint(" Estimating target at", delta, "from end") + return math.min(math.max(currentIndex + delta, 1), self.state.listSize) + end + else + local nextDist = self:distanceToPosition(nextIndex, targetPos) + debugPrint(" End is", nextDist, "from target") + if nextDist == 0 then + return nextIndex + end + + -- If the target position lies outside of the loaded elements. + if currentDist * nextDist > 0 then + debugPrint(" Target out of bounds") + if not extrapolate then + -- Do not extrapolate. Return the closest loaded element. + return nextIndex + end + + -- Extrapolate using the estimated item size. + local delta = Round.awayFromZero(nextDist / self.props.estimatedItemSize) + debugPrint(" Estimating target at", delta, "from end") + return math.min(math.max(nextIndex + delta, 1), self.state.listSize) + end + + -- Jump to the approximate location of the target based on the distance from current and next. + local totalDist = math.abs(currentDist) + math.abs(nextDist) + local indexCount = math.abs(currentIndex - nextIndex) + currentIndex = currentIndex + Round.nearest(indexCount * currentDist / totalDist) + currentDist = self:distanceToPosition(currentIndex, targetPos) + debugPrint(" Interpolated index is", currentIndex) + debugPrint(" Distance from interpolated index is", currentDist) + end + + -- Linear search from best guess index. + while currentDist ~= 0 do + if currentDist < 0 then + currentIndex = currentIndex - 1 + else + currentIndex = currentIndex + 1 + end + currentDist = self:distanceToPosition(currentIndex, targetPos) + debugPrint(" Distance after step is", currentDist) + end + + return currentIndex +end + +-- Expand the size of the scrolling frame's canvas to make sure everything still fits. +function Scroller:expandCanvas(newState) + debugPrint("expandCanvas") + local reverse = isReverse[self.props.orientation] + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + + local size = self.state.size + local newPadding = newState.padding or self.state.padding + local oldPadding = self:getCurrentPadding() + + local bottomPos = self:getChildCanvasPosition(bottomIndex) + + self:getChildSize(bottomIndex) - (oldPadding - newPadding) + debugPrint(" Bottom of bottom child is", bottomPos) + debugPrint(" Canvas size is", self.state.size) + debugPrint(" Canvas bottom should be", bottomPos + self.props.dragBuffer) + if bottomPos > self.state.size - self.props.dragBuffer then + -- Plus footer + size = bottomPos + self.props.dragBuffer + debugPrint(" Expanding canvas bottom to size", size) + end + + debugPrint(" Padding is", newPadding) + debugPrint(" Padding should be", self.props.dragBuffer) + if newPadding < self.props.dragBuffer then + -- Minus header + local diff = newPadding - self.props.dragBuffer + size = size - diff + self.anchorCanvasPosition = self.anchorCanvasPosition - diff + newPadding = self.props.dragBuffer + debugPrint(" Expanding canvas top to size", size) + debugPrint(" Shifting anchor to", self.anchorCanvasPosition) + debugPrint(" Padding is now", newPadding) + end + + if size ~= self.state.size or newPadding ~= self.state.padding then + debugPrint(" Changing size from", self.state.size, "to", size) + debugPrint(" Changing padding from", self.state.padding, "to", newPadding) + return { + size = size, + padding = newPadding, + } + else + return {} + end +end + +-- Try and get the canvas as close to correct as possible this rendering pass. +function Scroller:adjustCanvas(trimTrailing, trimLeading) + debugPrint("adjustCanvas") + + local newState = Cryo.Dictionary.join( + self:resetAnchorPosition(), + self:recalculateBounds(trimTrailing, trimLeading) + ) + + if not newState.trail and not newState.lead then + newState = Cryo.Dictionary.join(newState, self:expandCanvas(newState)) + end + + if Cryo.isEmpty(newState) then + return false + end + + self:setState(newState) + return true +end + +-- Move the cavnas position so that the anchor element is in the same place on the screen. +function Scroller:moveToAnchor() + debugPrint("moveToAnchor") + local currentPos = self:getAnchorFramePosition() + debugPrint(" Anchor was at frame position", self.anchorFramePosition) + debugPrint(" Anchor is currently at frame position", currentPos) + self:setScroll(self:measure(self:getCurrent().CanvasPosition) + currentPos - self.anchorFramePosition) +end + +-- Call loadNext and loadPrevious if needed. +function Scroller:loadMore() + debugPrint("loadMore") + if self.props.loadPrevious and self.state.trail.index <= self.props.loadingBuffer then + debugPrint(" Calling loadPrevious") + self.props.loadPrevious() + end + if self.props.loadNext and self.state.lead.index > self.state.listSize - self.props.loadingBuffer then + debugPrint(" Calling loadNext") + self.props.loadNext() + end +end + +-- Set the current canvas position according to Orientation without calling onScroll. +function Scroller:setScroll(pos) + self.scrollDebounce = true + debugPrint(" Scrolling to", pos) + self:getCurrent().CanvasPosition = isVertical[self.props.orientation] + and Vector2.new(self:getCurrent().CanvasPosition.X, pos) + or Vector2.new(pos, self:getCurrent().CanvasPosition.Y) + self.scrollDebounce = false +end + +-- Returns the signed distance from the element to the given canvas-relative position, or 0 if the element overlaps +-- it. The sign of the distance is relative to the list indices. For this distance calculation, the padding between +-- elements is considered part of the current element. Returns nil if the element is not currently rendered. +function Scroller:distanceToPosition(index, pos) + local child = self:getRbx(index) + if not child then + return nil + end + + local childTop = self:absoluteToCanvasPosition(self:measure(child.AbsolutePosition)) - self.itemPadding + local childBottom = childTop + self:measure(child.AbsoluteSize) + 2 * self.itemPadding + + if pos < childTop then + return (pos - childTop) * direction[self.props.orientation] + elseif pos > childBottom then + return (pos - childBottom) * direction[self.props.orientation] + else + return 0 + end +end + +-- Get the canvas-relative position of the current anchor element. +function Scroller:getAnchorCanvasPosition() + return self:getAnchorCanvasFromIndex(self.state.anchor.index) +end + +function Scroller:getAnchorCanvasFromIndex(index) + local scale = self.props.anchorLocation.Scale + if not isReverse[self.props.orientation] then + scale = 1 - scale + end + + return Round.nearest(self:getChildCanvasPosition(index) + scale * self:getChildSize(index)) +end + +-- Get the frame-relative position of the current anchor element. +function Scroller:getAnchorFramePosition() + return self:getAnchorFrameFromIndex(self.state.anchor.index) +end + +function Scroller:getAnchorFrameFromIndex(index) + local scale = self.props.anchorLocation.Scale + if not isReverse[self.props.orientation] then + scale = 1 - scale + end + + return Round.nearest(self:getChildFramePosition(index) + scale * self:getChildSize(index)) + - self.relativeAnchorLocation +end + +-- Convert an AbsolutePosition to a position relative to the top-left corner of the canvas. +function Scroller:absoluteToCanvasPosition(position) + local current = self:getCurrent() + local canvas = current.CanvasPosition + local absolute = current.AbsolutePosition + return position + self:measure(canvas) - self:measure(absolute) +end + +-- Convert an AbsolutePosition to a position relative to the top-left corner of the scrolling frame. +function Scroller:absoluteToFramePosition(position) + local current = self:getCurrent() + local absolute = current.AbsolutePosition + return position - self:measure(absolute) +end + +-- Get the canvas-relative position of the element at the specified index. +function Scroller:getChildCanvasPosition(index) + local current = self:getRbx(index) + return current and self:absoluteToCanvasPosition(self:measure(current.AbsolutePosition)) or 0 +end + +-- Get the frame-relative position of the element at the specified index. +function Scroller:getChildFramePosition(index) + local current = self:getRbx(index) + return current and self:absoluteToFramePosition(self:measure(current.AbsolutePosition)) or 0 +end + +-- Get the absolute size of the element at the specified index. +function Scroller:getChildSize(index) + local current = self:getRbx(index) + return current and self:measure(current.AbsoluteSize) or 0 +end + +-- Get the ID of an element at a specific index. +function Scroller:getID(index) + return self.props.identifier(self.props.itemList[index]) +end + +-- Create or update a metadata entry for the given element. This can't use +-- self.props in willUpdate as any props it uses could be out of date. +function Scroller:updateMetadata(id, item, props) + local meta = self.metadata[id] + if not meta then + meta = {} + self.metadata[id] = meta + end + + if not meta.name then + local elem = props.renderItem(item, false) + local class = tostring(elem.component) + local pool = self:getKeyPool(class) + meta.name = pool:get() + end + + if not self.refpool[meta.name] then + self.refpool[meta.name] = Roact.createRef() + end + meta.ref = self.refpool[meta.name] +end + +-- Clear the metadata for an element that is being unloaded. +function Scroller:clearMetadata(id) + local meta = self.metadata[id] + if not meta then + return + end + + meta.name:release() + meta.name = nil + meta.ref = nil +end + +-- Get the key pool for the given class of elements, or create a new one if that doesn't exist yet. +function Scroller:getKeyPool(class) + if not self.pools[class] then + self.pools[class] = KeyPool.new(class) + end + return self.pools[class] +end + +-- Get the metadata info for the element at the specified index. +function Scroller:getMetadata(index) + return self.metadata[self:getID(index)] +end + +-- Get the current Roblox instance from the ref stored in the metadata. +function Scroller:getRbx(index) + local meta = self:getMetadata(index) + return meta and meta.ref and meta.ref.current +end + +-- Return X or Y depending on the orientation. +function Scroller:measure(vecOrUDim2) + return isVertical[self.props.orientation] and vecOrUDim2.Y or vecOrUDim2.X +end + +-- Get the current ScrollingFrame instance. +function Scroller:getCurrent() + return self:getRef().current +end + +function Scroller:getRef() + -- Make sure to get the ref from props if that exists. + return self.props[Roact.Ref] or self._ref +end + +return Scroller diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/findNewIndices.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/findNewIndices.lua new file mode 100644 index 0000000..d983a90 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Components/findNewIndices.lua @@ -0,0 +1,61 @@ +-- Find the new indicies of the trailing, anchor and leading elements. +-- props is expected to contain itemList, the identifier function and the maximum search distance. +-- state is expected to contain the old top, anchor and bottom indices and ids. +return function(props, state) + local topIndex = state.trail.index + local topID = state.trail.id + local anchorIndex = state.anchor.index + local anchorID = state.anchor.id + local bottomIndex = state.lead.index + local bottomID = state.lead.id + + local listSize = #props.itemList + + -- If too much got deleted and the previous anchor index is off the bottom of the list, start the search from + -- the bottom. + if anchorIndex > listSize then + anchorIndex = listSize + end + + -- No access to self:getID + local getID = function(index) + return props.identifier(props.itemList[index]) + end + local topStill = getID(topIndex) == topID + local anchorStill = getID(anchorIndex) == anchorID + local bottomStill = getID(bottomIndex) == bottomID + if topStill and anchorStill and bottomStill then + -- Nothing important moved + return topIndex, anchorIndex, bottomIndex + end + + local step = 0 + local foundTop = topStill and topIndex or nil + local foundAnchor = nil + local foundBottom = bottomStill and bottomIndex or nil + + -- Scan outward from the old anchor index until we find the top and bottom or hit the max distance + local deltas = {top=-1, bottom=1} + repeat + for _, delta in pairs(deltas) do + local pos = anchorIndex + delta * step + + if pos >= 1 and pos <= listSize then + local id = getID(pos) + if id == topID then + foundTop = pos + end + if id == anchorID then + foundAnchor = pos + end + if id == bottomID then + foundBottom = pos + end + end + end + + step = step + 1 + until (foundTop and foundAnchor and foundBottom) or step > props.maximumSearchDistance + + return foundTop, foundAnchor, foundBottom +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/FragmentThing.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/FragmentThing.lua new file mode 100644 index 0000000..98d53b1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/FragmentThing.lua @@ -0,0 +1,17 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +return function(props) + return Roact.createFragment({ + foo = Roact.createElement("Frame", { + BackgroundColor3 = props.color, + Size = UDim2.new(0, props.width, 0, props.width), + LayoutOrder = props.LayoutOrder, + }), + bar = Roact.createElement("Frame", { + BackgroundColor3 = props.color, + Size = UDim2.new(0, props.width, 0, props.width), + LayoutOrder = props.LayoutOrder + 1, + }) + }) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/FunctionThing.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/FunctionThing.lua new file mode 100644 index 0000000..a70d3fa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/FunctionThing.lua @@ -0,0 +1,10 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +return function(props) + return Roact.createElement("Frame", { + BackgroundColor3 = props.color, + Size = UDim2.new(0, props.width, 0, props.width), + LayoutOrder = props.LayoutOrder, + }) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/NestedThing.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/NestedThing.lua new file mode 100644 index 0000000..64cdd37 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/NestedThing.lua @@ -0,0 +1,24 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +local Thing = Roact.PureComponent:extend("Thing") + +function Thing:render() + return Roact.createElement("Frame", { + BackgroundColor3 = self.props.color, + Size = UDim2.new(0, self.props.width, 0, self.props.width), + LayoutOrder = self.props.LayoutOrder, + }) +end + +local ThingRoot = Roact.PureComponent:extend("ThingRoot") + +function ThingRoot:render() + return Roact.createElement(Thing, { + color = self.props.color, + width = self.props.width, + LayoutOrder = self.props.LayoutOrder, + }) +end + +return ThingRoot diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/ResizingThing.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/ResizingThing.lua new file mode 100644 index 0000000..6552ff7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/ResizingThing.lua @@ -0,0 +1,37 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +local Thing = Roact.PureComponent:extend("Thing") + +function Thing:init() + self.state = { + clicked = false, + token = nil, + } +end + +function Thing.getDerivedStateFromProps(nextProps, lastState) + -- Use the token we're already being passed as a surrogate lock, just for this story. + -- Normally, this would be a separate prop. + if nextProps.token ~= lastState.token then + return { + clicked = false, + token = nextProps.token, + } + end +end + +function Thing:render() + return Roact.createElement("Frame", { + BackgroundColor3 = self.props.color, + Size = UDim2.new(1, -self.props.width, 0, self.state.clicked and 10 or self.props.width), + LayoutOrder = self.props.LayoutOrder, + [Roact.Event.InputBegan] = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self:setState({clicked = not self.state.clicked}) + end + end, + }) +end + +return Thing diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/init.lua new file mode 100644 index 0000000..fd1d114 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/init.lua @@ -0,0 +1,23 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +local SCREEN_SIZE = Vector2.new(800, 480) + +return { + name = "Scroller", + storyRoot = script, + middleware = function(story, target) + local tree = Roact.createElement("Frame", { + BackgroundColor3 = Color3.fromRGB(30, 31, 28), + Size = UDim2.new(0, SCREEN_SIZE.X, 0, SCREEN_SIZE.Y), + ClipsDescendants = true, + }, { + Story = Roact.createElement(story) + }) + + local handle = Roact.mount(tree, target, "Root") + return function() + Roact.unmount(handle) + end + end, +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.deletion.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.deletion.story.lua new file mode 100644 index 0000000..2f59f28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.deletion.story.lua @@ -0,0 +1,75 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - item deletion test") + +function Story:init() + self.state.items = {} + for i = -20,20 do + table.insert(self.state.items, i) + end +end + +function Story:render() + return Roact.createElement("Frame", { + Size=UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 100, 0, 100), + padding = UDim.new(), + itemList = self.state.items, + focusIndex = 21, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = item == 0 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, 128 - 8*item, 128 + 8*item), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, + self.props, + {toDelete = Cryo.None} + )), + deletion = Roact.createElement("TextButton", { + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "Delete", + [Roact.Event.Activated] = function() + local nextItems + if self.props.toDelete then + nextItems = Cryo.List.filter(self.state.items, function(item) + for _, v in pairs(self.props.toDelete) do + if item == v then + return false + end + end + return true + end) + else + local n = math.random(1, #self.state.items) + print("Deleting index " .. tostring(n)) + nextItems = Cryo.List.removeIndex(self.state.items, n) + end + self:setState({ + items = nextItems + }) + end, + }), + }) +end + +return Story \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.few.large.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.few.large.story.lua new file mode 100644 index 0000000..a976be7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.few.large.story.lua @@ -0,0 +1,30 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - a few large items") + +function Story:render() + return Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 50, 0, 50), + padding = UDim.new(), + itemList = {1, 2, 3, 4, 5, 6, 7}, + focusIndex = 4, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 49, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + BackgroundColor3 = Color3.fromRGB(item*30, item*30, item*30), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, self.props)) +end + +return Story \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.infinite.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.infinite.story.lua new file mode 100644 index 0000000..ea63c4e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.infinite.story.lua @@ -0,0 +1,125 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - infinite scroll in both directions") + +function Story:init() + self.state.items = { + { + token = 0, + color = Color3.fromRGB(255, 255, 255), + } + } + self.state.size = Vector2.new(50, 50) + + self.loadPrevious = function() + local newItems = {} + local n = self.state.items[1].token + for i = n-10, n-1 do + table.insert(newItems, { + token = i, + color = Color3.fromRGB(0, 128 - i, 128 + i), + }) + end + self:setState({ + items = Cryo.List.join(newItems, self.state.items) + }) + end + + self.loadNext = function() + local newItems = {} + local n = self.state.items[#self.state.items].token + for i = n+1, n+10 do + table.insert(newItems, { + token = i, + color = Color3.fromRGB(0, 128 - i, 128 + i), + }) + end + self:setState({ + items = Cryo.List.join(self.state.items, newItems) + }) + end +end + +function Story:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, self.state.size.X, 0, self.state.size.Y), + padding = UDim.new(0, 3), + itemList = self.state.items, + loadNext = self.loadNext, + loadPrevious = self.loadPrevious, + focusIndex = 1, + anchorLocation = UDim.new(0.5, 0), + orientation = Scroller.Orientation.Up, + estimatedItemSize = 10, + mountingBuffer = 50, + identifier = function(item) + return item.token + end, + renderItem = function(item, _) + assert(item.token, "Item's token is unset") + assert(item.color, "Item's color is unset") + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = item.color, + }, { + ["INDEX" .. tostring(item.token)] = Roact.createElement("Frame"), + }) + end, + }, self.props)), + moveUp = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "^", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, -20) + }) + end, + }), + moveDown = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 100), + AnchorPoint = Vector2.new(1, 0), + Text = "v", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, 20) + }) + end, + }), + moveLeft = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -100, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = "<", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(-20, 0) + }) + end, + }), + moveRight = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, 0, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = ">", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(20, 0) + }) + end, + }), + }) +end + +return Story \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.resize.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.resize.story.lua new file mode 100644 index 0000000..66194ff --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.resize.story.lua @@ -0,0 +1,113 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Bar = Roact.PureComponent:extend("Bar") + +function Bar:init() + self.state = { + clicked = false, + } +end + +function Bar:render() + return Roact.createElement("TextButton", Cryo.Dictionary.join( + { + Size = UDim2.new(1, 0, 0, self.state.clicked and 30 or 10), + [Roact.Event.Activated] = function() + self:setState({ + clicked = not self.state.clicked, + }) + end, + }, + self.props + )) +end + +local Story = Roact.PureComponent:extend("Rhodium Story - frame resize test") + +function Story:init() + self.state = { + size = Vector2.new(50, 50), + } +end + +function Story:render() + return Roact.createElement("Frame", { + Size=UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, self.state.size.X, 0, self.state.size.Y), + padding = UDim.new(), + itemList = {1, 2, 3}, + focusIndex = 2, + anchorLocation = UDim.new(0, 0), + orientation = Scroller.Orientation.Down, + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + renderItem = function(item, _) + return Roact.createElement(Bar, { + BackgroundColor3 = item == 2 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, -128 + 128*item, 376 - 128*item), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, + self.props + )), + moveUp = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "^", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, -20) + }) + end, + }), + moveDown = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 100), + AnchorPoint = Vector2.new(1, 0), + Text = "v", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, 20) + }) + end, + }), + moveLeft = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -100, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = "<", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(-20, 0) + }) + end, + }), + moveRight = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, 0, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = ">", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(20, 0) + }) + end, + }), + }) +end + +return Story \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.several.small.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.several.small.story.lua new file mode 100644 index 0000000..d67e076 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.several.small.story.lua @@ -0,0 +1,33 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - enough small items to fill the view") + +function Story:render() + return Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 50, 0, 50), + padding = UDim.new(), + itemList = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, + focusIndex = 6, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = item == 6 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, item*23, item*23), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, self.props)) +end + +return Story \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.single.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.single.story.lua new file mode 100644 index 0000000..d8ba0ae --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/rho.single.story.lua @@ -0,0 +1,22 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - a single small item") + +function Story:render() + return Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 50, 0, 50), + itemList = {1}, + anchorLocation = UDim.new(0.5, 0), + dragBuffer = 0, + renderItem = function() + return Roact.createElement("Frame", {Size=UDim2.new(0, 10, 0, 10)}) + end, + }, self.props)) +end + +return Story \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/scroller.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/scroller.story.lua new file mode 100644 index 0000000..3157319 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/Storybook/scroller.story.lua @@ -0,0 +1,139 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Thing = require(script.Parent.ResizingThing) + +local Story = Roact.PureComponent:extend("Story") + +function Story:render() + return Roact.createFragment({ + scroller = Roact.createElement(Scroller, { + BackgroundColor3 = Color3.fromRGB(56, 19, 18), + Size = UDim2.new(0, self.state.size.X, 1, self.state.size.Y), + Position = UDim2.new(0, 50, 0, 50), + ScrollBarThickness = 8, + padding = UDim.new(0, 5), + orientation = Scroller.Orientation.Down, + itemList = self.state.items, + loadNext = self.loadNext, + loadPrevious = self.loadPrevious, + focusLock = self.state.lock, + focusIndex = 101, + anchorLocation = UDim.new(0, 0), + estimatedItemSize = 40, + identifier = function(item) return item.token end, + renderItem = function(item, _) + return Roact.createElement(Thing, item) + end, + }), + refresh = Roact.createElement("TextButton", { + Size = UDim2.new(0, 110, 0, 30), + Position = UDim2.new(1, -50, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = "Refresh", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickRefresh, + }), + up = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -90, 0, 90), + AnchorPoint = Vector2.new(1, 0), + Text = "^", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickUp, + }), + down = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -90, 0, 170), + AnchorPoint = Vector2.new(1, 0), + Text = "v", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickDown, + }), + left = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -130, 0, 130), + AnchorPoint = Vector2.new(1, 0), + Text = "<", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickLeft, + }), + right = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -50, 0, 130), + AnchorPoint = Vector2.new(1, 0), + Text = ">", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickRight, + }), + }) +end + +local function generate(token) + if token == 0 then + return {color=Color3.fromRGB(255, 255, 255), width=50, token=0} + end + return {color=Color3.fromRGB(128-token, 255, 128+token), width=50+40*math.sin(token/5), token=token} +end + +function Story:init() + local items = {} + for i = -100,100 do table.insert(items, generate(i)) end + self.state = { + lock = 1, + size = Vector2.new(200, -100), + items = items, + } + + self.loadNext = function() + self:setState({ + items = Cryo.List.join(self.state.items, {generate(self.state.items[#self.state.items].token + 1)}), + }) + end + + self.loadPrevious = function() + self:setState({ + items = Cryo.List.join({generate(self.state.items[1].token - 1)}, self.state.items), + }) + end + + self.clickRefresh = function() + print("Recentering") + self:setState({ + lock = self.state.lock + 1 + }) + end + + self.clickUp = function() + print("Moving up") + self:setState({ + size = self.state.size + Vector2.new(0, -20), + }) + end + + self.clickDown = function() + print("Moving down") + self:setState({ + size = self.state.size + Vector2.new(0, 20), + }) + end + + self.clickLeft = function() + print("Moving left") + self:setState({ + size = self.state.size + Vector2.new(-20, 0), + }) + end + + self.clickRight = function() + print("Moving right") + self:setState({ + size = self.state.size + Vector2.new(20, 0), + }) + end +end + +return Story \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/init.lua new file mode 100644 index 0000000..61e7222 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/infinite-scroller/init.lua @@ -0,0 +1,5 @@ +local Scroller = require(script.Components.Scroller) + +return { + Scroller = Scroller, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/lock.toml new file mode 100644 index 0000000..85a25ef --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/lock.toml @@ -0,0 +1,12 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/infinite-scroller" +version = "0.3.4" +commit = "43d4eedf0d7a0a97d2dd55e7f066ac617e7fde9f" +source = "url+https://github.com/roblox/infinite-scroller" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/t.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.3.4/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/FitFrame.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/FitFrame.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/FitFrame.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Otter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Distance.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Distance.lua new file mode 100644 index 0000000..0a959bd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Distance.lua @@ -0,0 +1,16 @@ +return { + -- Returns the signed distance from a point to a range. Returns 0 if the + -- point is within the range, positive if it's below and negative if it's + -- above. + fromPointToRangeSigned = function(point, rangeTop, rangeSize) + local rangeBottom = rangeTop + rangeSize + + if point < rangeTop then + return point - rangeTop + elseif point > rangeBottom then + return point - rangeBottom + else + return 0 + end + end +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/KeyPool.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/KeyPool.lua new file mode 100644 index 0000000..81f7669 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/KeyPool.lua @@ -0,0 +1,80 @@ +--[[ + KeyPool provides a pool of objects suitable for use as map keys. + + Create a new KeyPool, then call pool:get() to get a new key. Once you're done with it, call key:release(). + + Example: + local pool = KeyPool.new("foo") + ... + local key1 = pool:get() + local key2 = pool:get() + map[key1] = thing1 + map[key2] = thing2 + ... + map[key1] = nil + key1:release() + key1 = nil +]] +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local t = require(Root.t) + +-- Forward declarations +local KeyPool = {} +KeyPool.__index = KeyPool + +local Key = {} +Key.__index = Key + +-- This is Key.new, but we don't want to expose that publicly. +local function newkey(pool, index) + local key = { + pool = pool, + index = index, + } + + setmetatable(key, Key) + return key +end + +-- KeyPool functions + +function KeyPool.new(class) + assert(t.string(class)) + + local pool = { + class = class, + available = {}, + limit = 0, + count = 0, + } + + setmetatable(pool, KeyPool) + return pool +end + +-- Get a currently unused key, or create a new one if everything is in use. +function KeyPool:get() + if self.count == 0 then + self.limit = self.limit + 1 + return newkey(self, self.limit) + end + + local key = self.available[self.count] + self.count = self.count - 1 + return key +end + +-- Key functions + +function Key:__tostring() + return self.pool.class .. "_" .. string.format("%02d", tostring(self.index)) +end + +-- Return this key to the pool it came from. Whatever previously held this key should not keep the reference after +-- calling this. +function Key:release() + self.pool.count = self.pool.count + 1 + self.pool.available[self.pool.count] = self +end + +return KeyPool diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/NotifyReady.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/NotifyReady.lua new file mode 100644 index 0000000..a564707 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/NotifyReady.lua @@ -0,0 +1 @@ +return {} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Orientation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Orientation.lua new file mode 100644 index 0000000..a31399b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Orientation.lua @@ -0,0 +1,31 @@ +-- Enum for specifying the leading edge of the scroller. +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local t = require(Root.t) + +local Orientation = { + Up = "Orientation.Up", + Down = "Orientation.Down", + Left = "Orientation.Left", + Right = "Orientation.Right", +} + +local metaindex = { + isOrientation = t.union( + t.literal(Orientation.Up), + t.literal(Orientation.Down), + t.literal(Orientation.Left), + t.literal(Orientation.Right) + ) +} + +setmetatable(Orientation, { + __index = function(self, key) + return metaindex[key] or + error(tostring(key) .. " is not a valid member of Scroller.Orientation", 2) + end, + __newindex = function() + error("Scroller.Orientation is read-only", 2) + end, +}) + +return Orientation diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Round.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Round.lua new file mode 100644 index 0000000..2755954 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Round.lua @@ -0,0 +1,28 @@ +local epsilon = 1e-15 + +return { + nearest = function(num) + local q, r = math.modf(num) + if r <= -0.5 then + return q - 1 + elseif r >= 0.5 then + return q + 1 + else + return q + end + end, + towardsZero = function(num) + local result, _ = math.modf(num) + return result + end, + awayFromZero = function(num) + local q, r = math.modf(num) + if r < -epsilon then + return q - 1 + elseif r > epsilon then + return q + 1 + else + return q + end + end, +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Scroller.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Scroller.lua new file mode 100644 index 0000000..649e349 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/Scroller.lua @@ -0,0 +1,1365 @@ +local RunService = game:GetService("RunService") + +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local t = require(Root.t) +local Otter = require(Root.Otter) + +local FitFrame = require(Root.FitFrame).FitFrameOnAxis +local findNewIndices = require(script.Parent.findNewIndices) +local relocateIndices = require(script.Parent.relocateIndices) +local Round = require(script.Parent.Round) +local Distance = require(script.Parent.Distance) +local KeyPool = require(script.Parent.KeyPool) + +local NotifyReady = require(script.Parent.NotifyReady) + +local debugPrint = function() end + +local Scroller = Roact.PureComponent:extend("Scroller") + +Scroller.Orientation = require(script.Parent.Orientation) + +local MOTOR_OPTIONS = { + frequency = 4, + dampingRatio = 1, +} + +local isVertical = { + [Scroller.Orientation.Up] = true, + [Scroller.Orientation.Down] = true, + [Scroller.Orientation.Left] = false, + [Scroller.Orientation.Right] = false, +} + +local isReverse = { + [Scroller.Orientation.Up] = true, + [Scroller.Orientation.Down] = false, + [Scroller.Orientation.Left] = true, + [Scroller.Orientation.Right] = false, +} + +local direction = { + [Scroller.Orientation.Up] = -1, + [Scroller.Orientation.Down] = 1, + [Scroller.Orientation.Left] = -1, + [Scroller.Orientation.Right] = 1, +} + +Scroller.validateProps = t.interface({ + -- Required. The list of items to scroll through. + itemList = t.array(t.any), + + -- Required. A callback function, called with each visible item in the itemList when the list is rendered. + renderItem = t.callback, + + -- A function to uniquely identify list items. Calling this on the same item twice should give the same result + -- accoring to ==. + identifier = t.optional(t.callback), + + -- One of the Scroller.Orientation enums. Determines the leading edge of the infinite scroll. + orientation = t.optional(Scroller.Orientation.isOrientation), + + -- A callback function, called when the infinite scroll reaches the leading end of the itemList (index + -- #itemList). + loadNext = t.optional(t.callback), + + -- A callback function, called when the infinite scroll reaches the trailing end of the itemList (index 1). + loadPrevious = t.optional(t.callback), + + -- Padding between elements in the scrolling frame. The Scale is relative to the size of the scrolling frame. + padding = t.optional(t.UDim), + + -- The minimum number of unmounted elements to keep at the top and bottom of the list. If there are fewer than + -- this call loadNext or loadPrevious. + loadingBuffer = t.optional(t.numberPositive), + + -- The amount of space above and below the view to render items in. + mountingBuffer = t.optional(t.numberPositive), + + -- The amount of empty space to keep at the top and bottom on the scroll. + dragBuffer = t.optional(t.numberMin(0)), + + -- An initial guess at the average size of an item. + estimatedItemSize = t.optional(t.numberPositive), + + -- The maximum distance to search for moved elements. + maximumSearchDistance = t.optional(t.numberPositive), + + -- The element to put in focus initially. + focusIndex = t.optional(t.integer), + + -- An arbitrary value to prevent the list from refocusing every render. Change this to cause the list to reset + -- and refocus on the new focusIndex. + focusLock = t.optional(t.any), + + -- The position within the view to keep still as other things move. The Scale is relative to the size of the + -- scrolling frame. + anchorLocation = t.optional(t.UDim), + + -- Animate the scrolling + animateScrolling = t.optional(t.boolean), + + --Animation options + animateOptions = t.optional(t.table), + + -- Properties that should trigger rerenders of the children elements even though the scroller itself does not + -- use them. + extraProps = t.optional(t.table), + + -- A callback function that will update the index change + onScrollUpdate = t.optional(t.callback), + + -- Which components to disable instance recycling for. + recyclingDisabledFor = t.optional(t.array(t.string)), + + ---- INTERNAL ONLY ---- + [NotifyReady] = t.any, +}) + +-- Default values for all the infinite-scroller-specific props. Any prop not in this list will be passed on to the +-- underlying ScrollingFrame. +Scroller.defaultProps = { + itemList = {}, + renderItem = {}, + identifier = function(item) + return item + end, + orientation = Scroller.Orientation.Down, + loadNext = function() end, + loadPrevious = function() end, + padding = UDim.new(0, 0), + loadingBuffer = 10, + mountingBuffer = 200, + dragBuffer = 0, + estimatedItemSize = 50, + maximumSearchDistance = 100, + focusIndex = 1, + focusLock = {}, + anchorLocation = UDim.new(0, 0), + animateScrolling = false, + animateOptions = MOTOR_OPTIONS, + extraProps = {}, + onScrollUpdate = function() end, + recyclingDisabledFor = {}, + [NotifyReady] = false, +} + +function Scroller:render() + debugPrint("render") + + -- Gather vertical/horizontal specific variables. + local axis = isVertical[self.props.orientation] and { + fillDirection = Enum.FillDirection.Vertical, + scrollDirection = Enum.ScrollingDirection.Y, + fitDirection = FitFrame.Axis.Vertical, + minimumSize = UDim2.new(1, 0, 0, 0), + canvasSize = UDim2.new(0, 0, 0, self.state.size), + paddingSize = UDim2.new(0, 0, 0, self.state.padding), + } or { + fillDirection = Enum.FillDirection.Horizontal, + scrollDirection = Enum.ScrollingDirection.X, + fitDirection = FitFrame.Axis.Horizontal, + minimumSize = UDim2.new(0, 0, 1, 0), + canvasSize = UDim2.new(0, self.state.size, 0, 0), + paddingSize = UDim2.new(0, self.state.padding, 0, 0), + } + + -- Remove non-standard props from list to pass on to ScrollingFrame. These are the same props given in + -- defaultProps. + local props = Cryo.Dictionary.join( + self.props, + self.propsToClear, + { + CanvasSize = axis.canvasSize, + ScrollingDirection = axis.scrollDirection, + [Roact.Change.CanvasPosition] = self.onScroll, + [Roact.Change.AbsoluteSize] = self.onResize, + [Roact.Ref] = self:getRef(), + } + ) + + local children = { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = axis.fillDirection, + Padding = UDim.new(0, self.itemPadding), + [Roact.Change.AbsoluteContentSize] = self.onContentResize, + }), + padding = Roact.createElement("Frame", { + Size = axis.paddingSize, + LayoutOrder = -1 - (self.state.listSize or 0), + BackgroundTransparency = 1, + }), + } + + -- Trailing and leading indicies won't be set if this isn't true. + if self.state.ready and not Cryo.isEmpty(self.props.itemList) then + debugPrint(" Rendering elements between", self.state.trail.index, "and", self.state.lead.index) + for n = self.state.trail.index, self.state.lead.index do + local metadata = self:getMetadata(n) + children[metadata.name] = Roact.createElement(FitFrame, { + minimumSize = axis.minimumSize, + axis = axis.fitDirection, + FillDirection = axis.fillDirection, + BackgroundTransparency = 1, + LayoutOrder = isReverse[self.props.orientation] and -n or n, + [Roact.Ref] = metadata.ref, + }, { + item = self.props.renderItem(self.props.itemList[n], false), + }) + end + end + + return Roact.createElement("ScrollingFrame", props, children) +end + +function Scroller:shouldUpdate(nextProps, nextState) + if not self.alive then + return false + end + + debugPrint("shouldUpdate") + + -- Check for state and props changes in the same way PureComponent would, + -- but go one more level down for extraProps. + if nextState ~= self.state then + debugPrint(" State changed") + return true + end + + for key, value in pairs(nextProps) do + if self.props[key] ~= value then + if key ~= "extraProps" then + debugPrint(" Prop changed:", key) + return true + end + + for extraKey, extraValue in pairs(value) do + if self.props.extraProps[extraKey] ~= extraValue then + debugPrint(" Extra prop changed:", extraKey) + return true + end + end + end + end + + for key, value in pairs(self.props) do + if nextProps[key] ~= value then + if key ~= "extraProps" then + debugPrint(" Prop changed:", key) + return true + end + + for extraKey, extraValue in pairs(value) do + if nextProps.extraProps[extraKey] ~= extraValue then + debugPrint(" Extra prop changed:", extraKey) + return true + end + end + end + end + + return false +end + +function Scroller:willUpdate(nextProps, nextState) + if not self.alive then + return + end + + debugPrint("willUpdate") + + if not nextState.ready then + return + end + + self.sizeDebounce = true + + local deletions = {} + local additions = {} + + if not Cryo.isEmpty(self.props.itemList) and self.state.lead then + for n = self.state.trail.index, self.state.lead.index do + local id = self.props.identifier(self.props.itemList[n]) + deletions[id] = true + end + end + + if not Cryo.isEmpty(nextProps.itemList) and nextState.lead then + for n = nextState.trail.index, nextState.lead.index do + local item = nextProps.itemList[n] + local id = nextProps.identifier(item) + if deletions[id] then + -- Element is in both ranges. + deletions[id] = nil + else + additions[id] = item + end + end + end + + -- Clear names first, so new items can use them. + for id, _ in pairs(deletions) do + self:clearMetadata(id) + end + for id, item in pairs(additions) do + self:updateMetadata(id, item, nextProps) + end + + -- The focus lock changed, clear the non-state anchor variables. + if self.state.lastFocusLock ~= nextState.lastFocusLock then + self.scrollDebounce = true + self.motorActive = false + self.anchorFramePosition = 0 + if self.state.listSize and self.state.listSize > nextState.listSize then + -- Perform a full reset when the list decreases in size to ensure the canvas + -- is sized properly + self.anchorCanvasPosition = self.relativeAnchorLocation + else + -- The canvas hasn't necessarily been reset in size here, so we need to convert the frame + -- coordinates of relativeAnchorLocation to canvas coordinates. + self.anchorCanvasPosition = self:frameToCanvasPosition(self.relativeAnchorLocation) + end + end +end + +function Scroller:didUpdate(previousProps, previousState) + if not self.alive then + return + end + + debugPrint("didUpdate") + + if Cryo.isEmpty(self.props.itemList) then + return + end + + if not self.state.ready then + self.onResize(self:getRef().current) + return + end + + if self.props.focusIndex ~= previousProps.focusIndex and + self.props.focusLock ~= previousProps.focusLock then + + self.indexChanged = { + oldIndex = previousProps.focusIndex, + newIndex = self.props.focusIndex, + lastFocusLock = self.props.focusLock, + } + self.motorActive = false + + debugPrint("self.props.focusIndex", self.props.focusIndex) + debugPrint("self.state.anchor.index", self.state.anchor.index) + debugPrint("previousState.anchor.index", previousState.anchor.index) + end + + local adjustedCanvas = self:adjustCanvas(self.scrollingForward, self.scrollingBackward) + if not adjustedCanvas then + if self.indexChanged and self.props.animateScrolling then + self:scrollToAnchor() + else + self:moveToAnchor() + end + + if self.anchorOffset ~= 0 then + self:setState({}) + return + end + + -- The canvas has finished adjusting itself after a resize + self.resized = false + + self:loadMore() + self.sizeDebounce = false + + --Return the updated index + if self.props.onScrollUpdate then + self.props.onScrollUpdate({ + leadIndex = self.state.lead.index, + anchorIndex = self.state.anchor.index, + trailIndex = self.state.trail.index, + animationActive = self.motorActive, + }) + end + end +end + +function Scroller.getDerivedStateFromProps(nextProps, lastState) + debugPrint("getDerivedStateFromProps") + if not lastState.ready or Cryo.isEmpty(nextProps.itemList) then + return nil + end + + local listSize = #nextProps.itemList + local lastFocusLock = nil + + -- Reset the state if the focus lock changes. This is guaranteed to be true the first time. + if lastState.lastFocusLock ~= nextProps.focusLock then + debugPrint(" Resetting focus lock", lastState.lastFocusLock," to ", nextProps.focusLock) + if nextProps.animateScrolling and lastState.lastFocusLock ~= nil then + lastFocusLock = nextProps.focusLock + else + local focusID = nextProps.identifier(nextProps.itemList[nextProps.focusIndex]) + return { + listSize = listSize, + trail = {index=nextProps.focusIndex, id=focusID}, + anchor = {index=nextProps.focusIndex, id=focusID}, + lead = {index=nextProps.focusIndex, id=focusID}, + padding = 0, + size = 0, + lastFocusLock = nextProps.focusLock, + } + end + end + + local trailIndex, anchorIndex, leadIndex = findNewIndices(nextProps, lastState) + debugPrint(" Trailing index moved from", lastState.trail.index, "to", trailIndex) + debugPrint(" Anchor index moved from", lastState.anchor.index, "to", anchorIndex) + debugPrint(" Leading index moved from", lastState.lead.index, "to", leadIndex) + + -- Nothing changed. Return early to avoid triggering an update. + if anchorIndex and lastState.anchor.index == anchorIndex + and trailIndex and lastState.trail.index == trailIndex + and leadIndex and lastState.lead.index == leadIndex then + debugPrint(" No change, returning early") + if listSize == lastState.listSize then + if lastFocusLock then + return { + lastFocusLock = lastFocusLock, + } + else + return nil + end + else + return { + listSize = listSize, + lastFocusLock = lastFocusLock, + } + end + end + + -- TODO #51 findNewIndices, state and this should all agree on a format + local newIndices = relocateIndices( + {trailIndex=trailIndex, anchorIndex=anchorIndex, leadIndex=leadIndex}, + {trailIndex=lastState.trail.index, anchorIndex=lastState.anchor.index, leadIndex=lastState.lead.index}, + listSize + ) + + debugPrint(" Anchor index moved to", newIndices.anchorIndex) + debugPrint(" Trailing index moved to", newIndices.trailIndex) + debugPrint(" Leading index moved to", newIndices.leadIndex) + + local trailID = nextProps.identifier(nextProps.itemList[newIndices.trailIndex]) + local anchorID = nextProps.identifier(nextProps.itemList[newIndices.anchorIndex]) + local leadID = nextProps.identifier(nextProps.itemList[newIndices.leadIndex]) + + return { + listSize = listSize, + trail = {index=newIndices.trailIndex, id=trailID}, + anchor = {index=newIndices.anchorIndex, id=anchorID}, + lead = {index=newIndices.leadIndex, id=leadID}, + lastFocusLock = lastFocusLock, + } +end + +function Scroller:init() + debugPrint("init") + + -- Only self:getRef() should access this. + self._ref = Roact.createRef() + + self.motorPrevValue = 0 + + self.motorOnStep = function(value) + debugPrint("onStep", value) + if not self.motorActive or self.indexChanged == nil then + self.motor:stop() + return + end + local currentValue = self.indexChanged.currentPos + if currentValue == nil then + self.motor:stop() + return + end + local diff = value - self.motorPrevValue + if self:getCurrent() then + self:scrollRelative(diff) + self.motorPrevValue = value + end + end + + self.motorOnComplete = function() + debugPrint("otter onComplete") + self.motorActive = false + --Return the updated index + if self.props.onScrollUpdate then + self.props.onScrollUpdate({ + leadIndex = self.props.focusIndex, + anchorIndex = self.props.focusIndex, + trailIndex = self.props.focusIndex, + animationActive = self.motorActive, + }) + end + self.motorPrevValue = 0 + self.indexChanged = nil + if self.motor then + self.motor:destroy() + end + end + + self.motorActive = false + self.springLock = 0 + self.scrollDebounce = false + self.sizeDebounce = true + + --Used to track index changes + self.indexChanged = nil + + self.onScroll = function(rbx) + debugPrint("onScroll") + if not self.alive then + return + end + + debugPrint(" CanvasPosition is", rbx.CanvasPosition) + if self.scrollDebounce then + debugPrint(" Debouncing scroll") + return + end + + local delta, newState = self:recalculateAnchor() + self.scrollingBackward = delta < 0 + self.scrollingForward = delta > 0 + debugPrint(" Delta is", delta) + + if not Cryo.isEmpty(newState) then + self:setState(newState) + end + + -- Handle any passed in scroll callback. + if self.props[Roact.Change.CanvasPosition] then + self.props[Roact.Change.CanvasPosition](rbx) + end + end + + self.onResize = function(rbx) + debugPrint("onResize") + if not self.alive then + return + end + + local size = self:measure(rbx.AbsoluteSize) + local pos = self:measure(rbx.AbsolutePosition) + + self.itemPadding = self.props.padding.Scale * size + self.props.padding.Offset + if isReverse[self.props.orientation] then + self.relativeAnchorLocation = Round.nearest( + self.props.anchorLocation.Scale * size + self.props.anchorLocation.Offset) + else + self.relativeAnchorLocation = Round.nearest( + (1 - self.props.anchorLocation.Scale) * size - self.props.anchorLocation.Offset) + end + self.absoluteAnchorLocation = self.relativeAnchorLocation + pos + self.mountAboveAnchor = self.relativeAnchorLocation + self.props.mountingBuffer + self.mountBelowAnchor = size - self.relativeAnchorLocation + self.props.mountingBuffer + self.resized = true + + -- Handle any passed in resize callback. + if self.props[Roact.Change.AbsoluteSize] then + self.props[Roact.Change.AbsoluteSize](rbx) + end + + if not self.state.ready then + debugPrint(" Setting initial anchor position to", self.relativeAnchorLocation) + -- When setting this for the first time, set the frame position of the current anchor to 0, + -- and its canvas position to equal where it should be in the frame. When the scroller goes + -- to correct this, the anchor will end up in the right place with the right padding around it. + self.anchorFramePosition = 0 + self.anchorCanvasPosition = self.relativeAnchorLocation + + coroutine.wrap(function() + RunService.Heartbeat:Wait() + if not self.state.ready and self.alive then + self:setState({ + ready = true + }) + + -- This should only be set by tests. + if self.props[NotifyReady] then + self.props[NotifyReady]:Fire() + end + end + end)() + else + self:setState({}) -- Force a rerender. + end + end + + self.onContentResize = function() + debugPrint("onContentResize") + + if not self.alive or self.sizeDebounce or not self.state.ready then + debugPrint(" Skipping onContentResize") + return + end + self:setState({}) -- Force a rerender. + end + + self.anchorCanvasPosition = 0 + self.anchorFramePosition = 0 + self.anchorOffset = 0 + + self.metadata = {} + self.pools = {} + self.refpool = {} + + self.scrollingBackward = false + self.scrollingForward = false + + self.lastLoadPrevItems = nil + self.lastLoadNextItems = nil + + self.alive = true + + -- Store the list of props to not pass on to the underlying scrolling frame. + self.propsToClear = {} + for k, _ in pairs(Scroller.defaultProps) do + self.propsToClear[k] = Cryo.None + end + + -- This will get updated shortly, but one render will happen before state.ready is set + self:setState({ + ready = false, + lastFocusLock = nil, + padding = 0, + size = 0, + }) +end + +function Scroller:willUnmount() + if self.motor then + self.motor:destroy() + end + + self.alive = false +end + +-- Find which element is currently closest to the anchor position. +function Scroller:recalculateAnchor() + debugPrint("recalculateAnchor") + + -- Find the index of the element at the appropriate position + local index = self:findIndexAt( + self:absoluteToCanvasPosition(self.absoluteAnchorLocation), self.state.anchor.index, false) + + self.anchorCanvasPosition = self:getAnchorCanvasFromIndex(index) + self.anchorFramePosition = self:getAnchorFrameFromIndex(index) + + local delta + if index == self.state.anchor.index then + debugPrint(" Current anchor still works") + return 0, {} + elseif index < self.state.anchor.index then + delta = -1 + else + delta = 1 + end + + debugPrint(" New anchor at index", index) + + -- Store the new anchor's details + debugPrint(" New anchor at canvas position", self.anchorCanvasPosition) + debugPrint(" New anchor at frame position", self.anchorFramePosition) + return delta, { + anchor = {index=index, id=self:getID(index)}, + } +end + +-- Move all the rendered elements up or down to put the anchor back where it was. +function Scroller:resetAnchorPosition() + debugPrint("resetAnchorPosition") + debugPrint(" Anchor index is", self.state.anchor.index) + local offset = self:getAnchorCanvasPosition() + debugPrint(" Anchor is at", offset) + debugPrint(" Anchor offset is", self.anchorOffset) + local newPos = self.anchorCanvasPosition - self.anchorOffset + debugPrint(" Anchor should be at", newPos) + local diff = Round.nearest(newPos - offset) + if diff ~= 0 then + debugPrint(" Changing padding from", self.state.padding, "to", (self.state.padding + diff)) + self.anchorCanvasPosition = self.anchorCanvasPosition - self.anchorOffset + self.anchorOffset = 0 + return {padding = self.state.padding + diff} + else + return {} + end +end + +-- Get the current padding from the UIPadding child. +function Scroller:getCurrentPadding() + local pad = self:getCurrent().padding + -- Only one of these will be non-zero + return pad.Size.X.Offset + pad.Size.Y.Offset +end + +-- Move the top and bottom of the range to be rendered up and down to make sure +-- enough things are being rendered. +function Scroller:recalculateBounds(trimTrailing, trimLeading) + debugPrint("recalculateBounds") + debugPrint(" Leading index was", self.state.lead.index) + debugPrint(" Trailing index was", self.state.trail.index) + + local anchorPos = self:getAnchorCanvasPosition() + local mountTop = anchorPos - self.mountAboveAnchor + local mountBottom = anchorPos + self.mountBelowAnchor + debugPrint(" Target for top at", mountTop) + debugPrint(" Target for bottom at", mountBottom) + + local topIndex = self:findIndexAt(mountTop, nil, true) + debugPrint(" Found new top index at", topIndex) + local bottomIndex = self:findIndexAt(mountBottom, nil, true) + debugPrint(" Found new bottom index at", bottomIndex) + + local leadIndex = math.max(topIndex, bottomIndex) + if leadIndex < self.state.lead.index and not trimLeading then + leadIndex = self.state.lead.index + end + + local trailIndex = math.min(topIndex, bottomIndex) + if trailIndex > self.state.trail.index and not trimTrailing then + trailIndex = self.state.trail.index + end + + if trailIndex < self.state.trail.index or leadIndex > self.state.lead.index then + debugPrint(" Changing leading index to", leadIndex) + debugPrint(" Changing trailing index to", trailIndex) + return { + trail = {index=trailIndex, id=self:getID(trailIndex)}, + lead = {index=leadIndex, id=self:getID(leadIndex)}, + } + else + return {} + end +end + +-- Find the index of the element that overlaps the given canvas-relative position. +function Scroller:findIndexAt(targetPos, hintIndex, extrapolate) + debugPrint(" findIndexAt") + -- Get the distance from the hinted index or the anchor. + local currentIndex = hintIndex or self.state.anchor.index + local currentDist = self:distanceToPosition(currentIndex, targetPos) + debugPrint(" Searching from index", currentIndex) + debugPrint(" Position is", currentDist, "from", targetPos) + if currentDist == 0 then + return currentIndex + end + + -- Get the distance from one end of the list. + local nextIndex = (currentDist < 0) and self.state.trail.index or self.state.lead.index + debugPrint(" Nearest end at", nextIndex) + if currentIndex == nextIndex then + debugPrint(" Hint index already at end") + -- If the target position lies outside of the loaded elements. + if currentIndex + currentDist < self.state.trail.index + or currentIndex + currentDist > self.state.lead.index then + debugPrint(" Target out of bounds") + if not extrapolate then + -- Do not extrapolate. Return the closest loaded element. + return currentIndex + end + + -- Extrapolate using the estimated item size. + local delta = Round.awayFromZero(currentDist / self.props.estimatedItemSize) + debugPrint(" Estimating target at", delta, "from end") + return math.min(math.max(currentIndex + delta, 1), self.state.listSize) + end + else + local nextDist = self:distanceToPosition(nextIndex, targetPos) + debugPrint(" End is", nextDist, "from target") + if nextDist == 0 then + return nextIndex + end + + -- If the target position lies outside of the loaded elements. + if currentDist * nextDist > 0 then + debugPrint(" Target out of bounds") + if not extrapolate then + -- Do not extrapolate. Return the closest loaded element. + return nextIndex + end + + -- Extrapolate using the estimated item size. + local delta = Round.awayFromZero(nextDist / self.props.estimatedItemSize) + debugPrint(" Estimating target at", delta, "from end") + return math.min(math.max(nextIndex + delta, 1), self.state.listSize) + end + + -- Jump to the approximate location of the target based on the distance from current and next. + local totalDist = math.abs(currentDist) + math.abs(nextDist) + local indexCount = math.abs(currentIndex - nextIndex) + currentIndex = currentIndex + Round.nearest(indexCount * currentDist / totalDist) + currentDist = self:distanceToPosition(currentIndex, targetPos) + debugPrint(" Interpolated index is", currentIndex) + debugPrint(" Distance from interpolated index is", currentDist) + end + + -- Linear search from best guess index. + while currentDist ~= 0 do + if currentDist < 0 then + currentIndex = currentIndex - 1 + else + currentIndex = currentIndex + 1 + end + currentDist = self:distanceToPosition(currentIndex, targetPos) + debugPrint(" Distance after step is", currentDist) + end + + return currentIndex +end + +function Scroller:trimTop(newState) + local reverse = isReverse[self.props.orientation] + local padding = newState.padding or self.state.padding + local topIndex = reverse and self.state.lead.index or self.state.trail.index + + if padding == self.props.dragBuffer then + return {} + end + + local keepGoing = false + + -- trimTop only happens under very specific circumstances. + -- in a top down list + if not reverse then + -- if the top most element is at the top + if topIndex == 1 then + -- and if the padding is greater than the distance between the top of the frame and the top element's starting position + if padding > self.relativeAnchorLocation + self.props.dragBuffer then + keepGoing = true + end + end + else -- OR in a bottom up list + -- if the top most element is at the top + if topIndex == self.state.listSize then + -- and if the nonzero padding is equivalent to position of the top element + the size of the dragBuffer REEXAMINE THIS CONDITIONAL + if padding > self.props.dragBuffer then + keepGoing = true + end + end + end + + if not keepGoing then + return {} + end + + local size = newState.size or self.state.size + + local newPadding = self.props.dragBuffer + if not reverse then + newPadding = self.relativeAnchorLocation + newPadding + end + local paddingDiff = padding - newPadding + local newSize = Round.nearest(size - paddingDiff) + local newAnchorPos = self.anchorCanvasPosition - paddingDiff + debugPrint(" Anchor moved from", self.anchorCanvasPosition, "to", newAnchorPos) + self.anchorCanvasPosition = newAnchorPos + + if reverse then + local newAnchorFramePosition = self.anchorFramePosition - paddingDiff + debugPrint(" Anchor frame position moved from", self.anchorFramePosition, "to", newAnchorFramePosition) + self.anchorFramePosition = newAnchorFramePosition + end + + debugPrint(" Trimming padding from", padding, "to", newPadding) + debugPrint(" Reducing canvas size from", size, "to", newSize) + return { + size = newSize, + padding = newPadding, + } +end + +function Scroller:trimBottom(newState) + local reverse = isReverse[self.props.orientation] + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + local padding = newState.padding or self.state.padding + local size = newState.size or self.state.size + + local absSize = self:measure(self:getCurrent().AbsoluteSize) + local minSize = absSize + local childSize = self:getChildSize(bottomIndex) + local bottomPos = self:getChildCanvasPosition(bottomIndex) + childSize + local newSize = bottomPos + self.props.dragBuffer + + if not reverse then + minSize = minSize - math.max(0, padding) + newSize = Round.nearest(newSize + self.relativeAnchorLocation) + else -- reverse + local oldPadding = self:getCurrentPadding() + bottomPos = self:getChildCanvasPosition(bottomIndex) + childSize - (oldPadding - padding) + newSize = Round.nearest(bottomPos + self.props.dragBuffer + (absSize - self.relativeAnchorLocation)) + end + + local returnState = {} + if newSize > minSize and newSize < Round.nearest(size) then + returnState.size = newSize + debugPrint(" Changing canvas size from", size, "to", returnState.newSize) + + if reverse then + -- Changing the canvas size without changing the padding requires the anchorFramePosition to be + -- reset to prevent moveToAnchor from adjusting the position of the canvas + self.anchorFramePosition = 0 + if padding > 0 then + + -- When the scroller is reversed, the start of the list can have excess padding from when the canvas has been + -- expanded while loading more items. When that is the case, trim the padding along with the canvas size. + local sizeDiff = Round.nearest(size - newSize) + local newAnchorPos = self.anchorCanvasPosition - sizeDiff + local newPadding = math.max(0, padding - sizeDiff) + + debugPrint(" Moving anchor from", self.anchorCanvasPosition, "to", newAnchorPos) + self.anchorCanvasPosition = newAnchorPos + + returnState.padding = newPadding + debugPrint(" Changing padding from", padding, "to", returnState.padding) + end + end + elseif bottomPos + self.props.dragBuffer < size then + if not reverse then + -- Shift the list so that the bottom element is touching the bottom of the frame + -- while keeping the canvas size the same. This is to ensure that when scrolling to the top of the + -- list there is still the anchor space between the top element and the top of the frame + local diff = Round.nearest(size - bottomPos + self.props.dragBuffer) + if diff ~= 0 then + local newPadding = padding + diff + local newAnchorPos = self.anchorCanvasPosition + diff + + debugPrint(" Moving anchor from", self.anchorCanvasPosition, "to", newAnchorPos) + self.anchorCanvasPosition = newAnchorPos + + -- Since the anchor position does not update before moveToAnchor is called, update the + -- anchorFramePosition here + self.anchorFramePosition = self.anchorFramePosition + diff + + returnState.padding = newPadding + debugPrint(" Changing padding from", padding, "to", returnState.padding) + end + end + end + + return returnState +end + + +-- When the ends of the list are loaded, adjust the padding and canvas size so that the ends of +-- the canvas do not extend past the items in the list +-- This adjustment preserves the space between the start of the list and the anchor +-- Eg when the anchorLocation is (0.5, 0) on a 100px tall frame, there is 50px of empty space +-- between the leading edge of the frame and the first element. +function Scroller:adjustEdges(newState) + debugPrint("adjustEdges") + local reverse = isReverse[self.props.orientation] + local topIndex = reverse and self.state.lead.index or self.state.trail.index + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + + if not reverse then + if topIndex == 1 then + return self:trimTop(newState) + elseif bottomIndex == self.state.listSize then + return self:trimBottom(newState) + end + else -- reverse + if bottomIndex == 1 then + return self:trimBottom(newState) + elseif topIndex == self.state.listSize then + return self:trimTop(newState) + end + end + + return {} +end + +-- Expand the size of the scrolling frame's canvas to make sure everything still fits. +function Scroller:expandCanvas(newState) + debugPrint("expandCanvas") + local reverse = isReverse[self.props.orientation] + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + + local size = newState.size or self.state.size + local originalSize = size + local newPadding = newState.padding or self.state.padding + local originalPadding = newPadding + local oldPadding = self:getCurrentPadding() + + local bottomPos = self:getChildCanvasPosition(bottomIndex) + + self:getChildSize(bottomIndex) - (oldPadding - newPadding) + + local bottomTarget = bottomPos + self.props.dragBuffer + + debugPrint(" Bottom of bottom child is", bottomPos) + debugPrint(" Canvas size is", originalSize) + debugPrint(" Canvas bottom should be", bottomTarget) + + local minSize = self:measure(self:getCurrent().AbsoluteSize) - math.max(0, newPadding) + if originalSize < minSize then + size = minSize + debugPrint(" Expanding canvas to minimum size", size) + end + + if originalSize < bottomTarget then + -- Plus footer + size = math.max(bottomTarget, size) + debugPrint(" Expanding canvas bottom to size", size) + end + + debugPrint(" Padding is", newPadding) + debugPrint(" Padding should be at least", self.props.dragBuffer) + if newPadding < self.props.dragBuffer then + -- Minus header + local diff = newPadding - self.props.dragBuffer + size = size - diff + self.anchorCanvasPosition = self.anchorCanvasPosition - diff + newPadding = self.props.dragBuffer + debugPrint(" Expanding canvas top to size", size) + debugPrint(" Shifting anchor to", self.anchorCanvasPosition) + debugPrint(" Padding is now", newPadding) + end + + if size ~= originalSize or newPadding ~= originalPadding then + debugPrint(" Changing size from", originalSize, "to", size) + debugPrint(" Changing padding from", originalPadding, "to", newPadding) + return { + size = size, + padding = newPadding, + } + else + debugPrint(" No changes to size or padding") + return {} + end +end + +-- Try and get the canvas as close to correct as possible this rendering pass. +function Scroller:adjustCanvas(trimTrailing, trimLeading) + debugPrint("adjustCanvas") + + local newState = Cryo.Dictionary.join( + self:resetAnchorPosition(), + self:recalculateBounds(trimTrailing, trimLeading) + ) + + if not newState.trail and not newState.lead then + newState = Cryo.Dictionary.join(newState, self:expandCanvas(newState)) + newState = Cryo.Dictionary.join(newState, self:adjustEdges(newState)) + end + + if Cryo.isEmpty(newState) then + debugPrint(" No state changes after adjustment") + return false + end + + self:setState(newState) + return true +end + +-- Move the canvas position so that the anchor element is in the same place on the screen. +function Scroller:scrollToAnchor() + if self.motorActive then + return + end + debugPrint("scrollToAnchor") + if self.indexChanged == nil then + self:moveToAnchor() + end + + local newIndex = self.indexChanged.newIndex + local previousIndex = self.indexChanged.oldIndex + debugPrint(" newIndex", newIndex) + debugPrint(" previousIndex", previousIndex) + + local oldPos = self:measure(self:getCurrent().CanvasPosition) + self.relativeAnchorLocation + local newPos = self:getAnchorCanvasFromIndex(newIndex) + + debugPrint(" old anchor pos", oldPos) + debugPrint(" new anchor pos", newPos) + + self.indexChanged.currentPos = oldPos + self.indexChanged.newPos = newPos + self.motorActive = true + self.springLock = self.springLock + 1 + + local delta = newPos - oldPos + debugPrint(" delta", delta) + self.motor = Otter.createSingleMotor(0) + self.motor:onStep(self.motorOnStep) + self.motor:onComplete(self.motorOnComplete) + self.motor:setGoal(Otter.spring(delta, self.props.animateOptions)) +end + +-- Move the canvas position so that the anchor element is in the same place on the screen. +function Scroller:moveToAnchor() + debugPrint("moveToAnchor") + if self.motorActive then + return + end + if self:isScrollingWithElasticBehavior() then + return + end + + local currentPos = self:getAnchorFramePosition() + debugPrint(" Anchor was at frame position", self.anchorFramePosition) + debugPrint(" Anchor is currently at frame position", currentPos) + local newPos = self:measure(self:getCurrent().CanvasPosition) + currentPos - self.anchorFramePosition + debugPrint(" Canvas should scroll to", newPos) + local current = self:getCurrent() + local maxPos = math.max(0, self:measure(current.CanvasSize).Offset - self:measure(current.AbsoluteSize)) + + self:setScroll(newPos) + if newPos < 0 then + debugPrint(" Canvas scroll limited to 0, was", newPos) + self.anchorOffset = Round.towardsZero(newPos) + elseif newPos >= maxPos then + debugPrint(" Canvas scroll limited to", maxPos, ", was", newPos) + self.anchorOffset = Round.towardsZero(newPos - maxPos) + else + debugPrint(" Clearing anchorOffset") + self.anchorOffset = 0 + end +end + +-- Prevent scrolling when experiencing elastic behavior on touch devices +function Scroller:isScrollingWithElasticBehavior() + -- When the canvas is resized the bottom of the canvas is bigger than the canvas for + -- the first update. Since a resize is not an elastic scroll skip these checks after a resize + if self.resized then + return false + end + + -- Check if the top of the list has scrolled past the frame because of ElasticBehavior + local reverse = isReverse[self.props.orientation] + local topIndex = reverse and self.state.lead.index or self.state.trail.index + local startOfListIndex = reverse and self.state.listSize or 1 + if self:measure(self:getCurrent().CanvasPosition) < 0 and topIndex == startOfListIndex then + return true + end + + -- Check if the bottom of the list has scrolled past the frame because of ElasticBehavior + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + local absSize = self:measure(self:getCurrent().AbsoluteSize) + local endOfListIndex = reverse and 1 or self.state.listSize + local bottomOfCanvas = Round.nearest(self:measure(self:getCurrent().CanvasPosition) + absSize) + local canvasSize = Round.nearest(self:measure(self:getCurrent().CanvasSize).Offset) + + if bottomOfCanvas > canvasSize and bottomIndex == endOfListIndex then + return true + end + + return false +end + +-- Call loadNext and loadPrevious if needed. +function Scroller:loadMore() + debugPrint("loadMore") + if self.props.loadPrevious and + self.state.trail.index <= self.props.loadingBuffer and + self.props.itemList ~= self.lastLoadPrevItems then + debugPrint(" Calling loadPrevious") + self.lastLoadPrevItems = self.props.itemList + self.props.loadPrevious() + end + if self.props.loadNext and + self.state.lead.index > self.state.listSize - self.props.loadingBuffer and + self.props.itemList ~= self.lastLoadNextItems then + debugPrint(" Calling loadNext") + self.lastLoadNextItems = self.props.itemList + self.props.loadNext() + end +end + +-- Set the current canvas position according to Orientation without calling +-- onScroll. +function Scroller:setScroll(pos) + debugPrint(" Scrolling to", pos) + self.scrollDebounce = true + self:getCurrent().CanvasPosition = isVertical[self.props.orientation] + and Vector2.new(self:getCurrent().CanvasPosition.X, pos) + or Vector2.new(pos, self:getCurrent().CanvasPosition.Y) + self.scrollDebounce = false +end + +-- Scroll by a relative amount +function Scroller:scrollRelative(amount) + debugPrint(" Current CanvasPosition", self:getCurrent().CanvasPosition) + debugPrint("self.motorActive", self.motorActive) + self:setScroll(self:measure(self:getCurrent().CanvasPosition) + amount, true) + self.onScroll(self:getCurrent()) +end + + +-- Returns the signed distance from the element to the given canvas-relative +-- position, or 0 if the element overlaps it. The sign of the distance is +-- relative to the list indices. For this distance calculation, the padding +-- between elements is considered part of the current element. Returns nil if +-- the element is not currently rendered. +function Scroller:distanceToPosition(index, pos) + local child = self:getRbx(index) + if not child then + return nil + end + + local childTop = self:absoluteToCanvasPosition(self:measure(child.AbsolutePosition)) - self.itemPadding + local childSize = self:measure(child.AbsoluteSize) + 2 * self.itemPadding + + return Distance.fromPointToRangeSigned(pos, childTop, childSize) * direction[self.props.orientation] +end + +-- Get the canvas-relative position of the current anchor element. +function Scroller:getAnchorCanvasPosition() + return self:getAnchorCanvasFromIndex(self.state.anchor.index) +end + +function Scroller:getAnchorCanvasFromIndex(index) + local scale = self.props.anchorLocation.Scale + if not isReverse[self.props.orientation] then + scale = 1 - scale + end + + return Round.nearest(self:getChildCanvasPosition(index) + scale * self:getChildSize(index)) +end + +-- Get the frame-relative position of the current anchor element. +function Scroller:getAnchorFramePosition() + return self:getAnchorFrameFromIndex(self.state.anchor.index) +end + +function Scroller:getAnchorFrameFromIndex(index) + local scale = self.props.anchorLocation.Scale + if not isReverse[self.props.orientation] then + scale = 1 - scale + end + + return Round.nearest(self:getChildFramePosition(index) + scale * self:getChildSize(index)) + - self.relativeAnchorLocation +end + +-- Convert an AbsolutePosition to a position relative to the top-left corner of the canvas. +function Scroller:absoluteToCanvasPosition(position) + local current = self:getCurrent() + local canvas = current.CanvasPosition + local absolute = current.AbsolutePosition + return position + self:measure(canvas) - self:measure(absolute) +end + +-- Convert an AbsolutePosition to a position relative to the top-left corner of the scrolling frame. +function Scroller:absoluteToFramePosition(position) + local current = self:getCurrent() + local absolute = current.AbsolutePosition + return position - self:measure(absolute) +end + +-- Convert a position relative to the frame to a position relative to the canvas. +function Scroller:frameToCanvasPosition(position) + local current = self:getCurrent() + local canvas = current.CanvasPosition + return position + self:measure(canvas) +end + +-- Get the canvas-relative position of the element at the specified index. +function Scroller:getChildCanvasPosition(index) + local current = self:getRbx(index) + return current and self:absoluteToCanvasPosition(self:measure(current.AbsolutePosition)) or 0 +end + +-- Get the frame-relative position of the element at the specified index. +function Scroller:getChildFramePosition(index) + local current = self:getRbx(index) + return current and self:absoluteToFramePosition(self:measure(current.AbsolutePosition)) or 0 +end + +-- Get the absolute size of the element at the specified index. +function Scroller:getChildSize(index) + local current = self:getRbx(index) + return current and self:measure(current.AbsoluteSize) or 0 +end + +-- Get the ID of an element at a specific index. +function Scroller:getID(index) + return self.props.identifier(self.props.itemList[index]) +end + +-- Create or update a metadata entry for the given element. This can't use +-- self.props in willUpdate as any props it uses could be out of date. +function Scroller:updateMetadata(id, item, props) + local meta = self.metadata[id] + if not meta then + meta = {} + self.metadata[id] = meta + end + + if not meta.name then + local elem = props.renderItem(item, false) + meta.class = tostring(elem.component) + local pool = self:getKeyPool(meta.class) + meta.name = pool:get() + end + + if not self.refpool[meta.name] then + self.refpool[meta.name] = Roact.createRef() + end + meta.ref = self.refpool[meta.name] +end + +-- Clear the metadata for an element that is being unloaded. +function Scroller:clearMetadata(id) + local meta = self.metadata[id] + if not meta then + return + end + + -- Not releasing the names seems like it would be a memory leak, but this + -- relies on the fact that the key pool does not track in use keys. Rather, + -- the key tracks which pool it came from. So if nothing is using an + -- unreleased key, it will be garbage collected and never reused. + if not Cryo.List.find(self.props.recyclingDisabledFor, meta.class) then + meta.name:release() + end + meta.name = nil + meta.ref = nil +end + +-- Get the key pool for the given class of elements, or create a new one if that doesn't exist yet. +function Scroller:getKeyPool(class) + if not self.pools[class] then + self.pools[class] = KeyPool.new(class) + end + return self.pools[class] +end + +-- Get the metadata info for the element at the specified index. +function Scroller:getMetadata(index) + return self.metadata[self:getID(index)] +end + +-- Get the current Roblox instance from the ref stored in the metadata. +function Scroller:getRbx(index) + local meta = self:getMetadata(index) + return meta and meta.ref and meta.ref.current +end + +-- Return X or Y depending on the orientation. +function Scroller:measure(vecOrUDim2) + return isVertical[self.props.orientation] and vecOrUDim2.Y or vecOrUDim2.X +end + +-- Get the current ScrollingFrame instance. +function Scroller:getCurrent() + return self:getRef().current +end + +function Scroller:getRef() + -- Make sure to get the ref from props if that exists. + return self.props[Roact.Ref] or self._ref +end + +return Scroller diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/findNewIndices.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/findNewIndices.lua new file mode 100644 index 0000000..b00ee3b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/findNewIndices.lua @@ -0,0 +1,67 @@ +-- Find the new indicies of the trailing, anchor and leading elements. +-- props is expected to contain itemList, the identifier function and the maximum search distance. +-- state is expected to contain the old top, anchor and bottom indices and ids. +return function(props, state) + local topIndex = state.trail.index + local topID = state.trail.id + local anchorIndex = state.anchor.index + local anchorID = state.anchor.id + local bottomIndex = state.lead.index + local bottomID = state.lead.id + + local listSize = #props.itemList + + -- If too much got deleted and the previous anchor index is off the bottom of the list, start the search from + -- the bottom. + if topIndex > listSize then + topIndex = listSize + end + if anchorIndex > listSize then + anchorIndex = listSize + end + if bottomIndex > listSize then + bottomIndex = listSize + end + + -- No access to self:getID + local getID = function(index) + return props.identifier(props.itemList[index]) + end + local topStill = getID(topIndex) == topID + local anchorStill = getID(anchorIndex) == anchorID + local bottomStill = getID(bottomIndex) == bottomID + if topStill and anchorStill and bottomStill then + -- Nothing important moved + return topIndex, anchorIndex, bottomIndex + end + + local step = 0 + local foundTop = topStill and topIndex or nil + local foundAnchor = nil + local foundBottom = bottomStill and bottomIndex or nil + + -- Scan outward from the old anchor index until we find the top and bottom or hit the max distance + local deltas = {top=-1, bottom=1} + repeat + for _, delta in pairs(deltas) do + local pos = anchorIndex + delta * step + + if pos >= 1 and pos <= listSize then + local id = getID(pos) + if id == topID then + foundTop = pos + end + if id == anchorID then + foundAnchor = pos + end + if id == bottomID then + foundBottom = pos + end + end + end + + step = step + 1 + until (foundTop and foundAnchor and foundBottom) or step > props.maximumSearchDistance + + return foundTop, foundAnchor, foundBottom +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/relocateIndices.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/relocateIndices.lua new file mode 100644 index 0000000..79b926d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Components/relocateIndices.lua @@ -0,0 +1,57 @@ +local Round = require(script.Parent.Round) + +-- Returns the three new indices with any nils filled in and any misorderings corrected. +-- new and old should be tables containing an anchorIndex, a leadIndex and a trailIndex. +return function(new, old, listSize) + local newAnchor = new.anchorIndex + local newLead = new.leadIndex + local newTrail = new.trailIndex + + -- There are 8 possibilities here as any combination of these could be deleted. Also, we can't use findIndexAt + -- here since that requires access to the children's measurements. + if not newAnchor then + if newLead and newTrail then + -- Estimate that the new anchor is proportionally the same distance from the lead and trail indices. + if newLead == newTrail then + -- Guard against divide by zero. + newAnchor = newLead + else + local oldRatio = (old.anchorIndex - old.leadIndex) / (old.trailIndex - old.leadIndex) + newAnchor = Round.nearest((newTrail - newLead) * oldRatio + newLead) + newAnchor = math.min(math.max(newAnchor, 1), listSize) + end + elseif newLead then + -- Given only the new leading index, estimate that the new anchor is the same distance away as it was. + newAnchor = newLead + old.anchorIndex - old.leadIndex + newAnchor = math.min(math.max(newAnchor, 1), listSize) + elseif newTrail then + -- Given only the new trailing index, estimate that the new anchor is the same distance away as it was. + newAnchor = newTrail + old.anchorIndex - old.trailIndex + newAnchor = math.min(math.max(newAnchor, 1), listSize) + else + -- Everything is gone. Just reuse the same index if that's still within the bounds of the list. + newAnchor = math.min(math.max(old.anchorIndex, 1), listSize) + end + end + + -- If the leading and trailing indices haven't been worked out yet, estimate that the new ones should be the + -- same distance from the anchor as the old ones were. + if not newTrail then + newTrail = newAnchor + old.trailIndex - old.anchorIndex + newTrail = math.min(math.max(newTrail, 1), listSize) + end + if not newLead then + newLead = newAnchor + old.leadIndex - old.anchorIndex + newLead = math.min(math.max(newLead, 1), listSize) + end + + -- Make sure the resulting indices are in the right order. + local minIndex = math.min(newAnchor, newLead, newTrail) + local maxIndex = math.max(newAnchor, newLead, newTrail) + + return { + trailIndex = minIndex, + anchorIndex = newAnchor, + leadIndex = maxIndex, + } +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/ComplexThing.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/ComplexThing.lua new file mode 100644 index 0000000..bf7e225 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/ComplexThing.lua @@ -0,0 +1,56 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +ComplexThing = Roact.PureComponent:extend("ComplexThing") + +ComplexThing.defaultProps = { + -- things start to lag at 5? + nestedLayer = 3, + Size = UDim2.fromScale(0.5, 0.5), +} + +function ComplexThing:render() + --temporary hard limit + if self.props.nestedLayer > 6 then + return Roact.createElement("Frame", { + Size = UDim2.fromOffset(100, 100) + }) + end + + local children = {} + + if self.props.nestedLayer > 1 then + children = { + ["TL"..self.props.nestedLayer] = Roact.createElement(ComplexThing, { + Position = UDim2.fromScale(0, 0), + nestedLayer = self.props.nestedLayer - 1, + }), + ["TR"..self.props.nestedLayer] = Roact.createElement(ComplexThing, { + Position = UDim2.fromScale(0.5, 0), + nestedLayer = self.props.nestedLayer - 1, + }), + ["BL"..self.props.nestedLayer] = Roact.createElement(ComplexThing, { + Position = UDim2.fromScale(0, 0.5), + nestedLayer = self.props.nestedLayer - 1, + }), + ["BR"..self.props.nestedLayer] = Roact.createElement(ComplexThing, { + Position = UDim2.fromScale(0.5, 0.5), + nestedLayer = self.props.nestedLayer - 1, + }), + } + end + + local r = math.random(255) + local g = math.random(255) + local b = math.random(255) + + return Roact.createElement("Frame", { + Size = self.props.Size, + Position = self.props.Position, + BackgroundColor3 = Color3.fromRGB(r, g, b), + BorderSizePixel = self.props.Size.X.Scale == 0.5 and 0 or 4, + BorderColor3 = Color3.fromRGB(255, 255, 255), + }, children) +end + +return ComplexThing diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/FragmentThing.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/FragmentThing.lua new file mode 100644 index 0000000..98d53b1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/FragmentThing.lua @@ -0,0 +1,17 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +return function(props) + return Roact.createFragment({ + foo = Roact.createElement("Frame", { + BackgroundColor3 = props.color, + Size = UDim2.new(0, props.width, 0, props.width), + LayoutOrder = props.LayoutOrder, + }), + bar = Roact.createElement("Frame", { + BackgroundColor3 = props.color, + Size = UDim2.new(0, props.width, 0, props.width), + LayoutOrder = props.LayoutOrder + 1, + }) + }) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/FunctionThing.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/FunctionThing.lua new file mode 100644 index 0000000..a70d3fa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/FunctionThing.lua @@ -0,0 +1,10 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +return function(props) + return Roact.createElement("Frame", { + BackgroundColor3 = props.color, + Size = UDim2.new(0, props.width, 0, props.width), + LayoutOrder = props.LayoutOrder, + }) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/NestedThing.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/NestedThing.lua new file mode 100644 index 0000000..64cdd37 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/NestedThing.lua @@ -0,0 +1,24 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +local Thing = Roact.PureComponent:extend("Thing") + +function Thing:render() + return Roact.createElement("Frame", { + BackgroundColor3 = self.props.color, + Size = UDim2.new(0, self.props.width, 0, self.props.width), + LayoutOrder = self.props.LayoutOrder, + }) +end + +local ThingRoot = Roact.PureComponent:extend("ThingRoot") + +function ThingRoot:render() + return Roact.createElement(Thing, { + color = self.props.color, + width = self.props.width, + LayoutOrder = self.props.LayoutOrder, + }) +end + +return ThingRoot diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/ResizingThing.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/ResizingThing.lua new file mode 100644 index 0000000..9385d87 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/ResizingThing.lua @@ -0,0 +1,38 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +local Thing = Roact.PureComponent:extend("Thing") + +function Thing:init() + self.state = { + clicked = false, + token = nil, + } +end + +function Thing.getDerivedStateFromProps(nextProps, lastState) + -- Use the token we're already being passed as a surrogate lock, just for this story. + -- Normally, this would be a separate prop. + if nextProps.token ~= lastState.token then + return { + clicked = false, + token = nextProps.token, + } + end + return nil +end + +function Thing:render() + return Roact.createElement("Frame", { + BackgroundColor3 = self.props.color, + Size = UDim2.new(1, -self.props.width, 0, self.state.clicked and 10 or self.props.width), + LayoutOrder = self.props.LayoutOrder, + [Roact.Event.InputBegan] = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self:setState({clicked = not self.state.clicked}) + end + end, + }) +end + +return Thing diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/animatedScroller.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/animatedScroller.story.lua new file mode 100644 index 0000000..d7658a8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/animatedScroller.story.lua @@ -0,0 +1,154 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Story") + +function Story:init() + local startingItems = {} + for i = 1, self.props.startingItems or 50 do + table.insert(startingItems, i) + end + self:setState({ + items = startingItems, + lock = 1, + width = 260, + itemSize = 30, + }) + + self.focus = self.props.startingFocus or 1 +end + +function Story:render() + return Roact.createFragment({ + output = Roact.createElement("Frame", { + Size = UDim2.new(0, self.state.width, 0, self.state.itemSize + 30), + BackgroundTransparency = 1, + },{ + scroller = Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + orientation = Scroller.Orientation.Right, + padding = UDim.new(0, 5), + itemList = self.state.items, + focusIndex = self.focus, + focusLock = self.state.lock, + anchorLocation = UDim.new(1, -15), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + dragBuffer = 120, + animateScrolling = true, + identifier = function(item) + assert(item) + return item + end, + renderItem = function(item, _) + return Roact.createElement("TextLabel", { + Size = UDim2.new(0, self.state.itemSize, 0, self.state.itemSize), + Text = tostring(item), + BackgroundColor3 = Color3.fromRGB(255, item*5, (51 - item)*5), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + onScrollUpdate = function(data) + self.focus = data.anchorIndex + self.animationActive = data.animationActive + end, + }, self.props, { + startingItems = Cryo.None, + startingFocus = Cryo.None, + })), + }), + scrollLeft = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(0, 0, 0, 100), + Text = "<", + [Roact.Event.Activated] = function() + if self.animationActive then + return + end + self.focus = math.max(self.focus - 4, 1) + self:setState({lock = self.state.lock + 1}) + end, + }), + scrollRight = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(0, 270, 0, 100), + Text = ">", + [Roact.Event.Activated] = function() + if self.animationActive then + return + end + self.focus = math.min(self.focus + 4, #self.state.items) + self:setState({lock = self.state.lock + 1}) + end, + }), + decreaseWidth = Roact.createElement("TextButton", { + Size = UDim2.new(0, 60, 0, 30), + Position = UDim2.new(0, 90, 0, 100), + Text = "Width -", + [Roact.Event.Activated] = function() + if self.animationActive then + return + end + self.focus = math.max(self.focus - 4, 1) + self:setState({ + width = self.state.width - 10, + }) + end, + }), + increaseWidth = Roact.createElement("TextButton", { + Size = UDim2.new(0, 60, 0, 30), + Position = UDim2.new(0, 150, 0, 100), + Text = "Width +", + [Roact.Event.Activated] = function() + if self.animationActive then + return + end + self:setState({ + width = self.state.width + 10, + }) + end, + }), + removeItem = Roact.createElement("TextButton", { + Size = UDim2.new(0, 60, 0, 30), + Position = UDim2.new(0, 30, 0, 100), + Text = "Item -", + [Roact.Event.Activated] = function() + if self.animationActive then + return + end + local items = self.state.items + local lock = nil + if self.focus >= #items then + self.focus = #items - 1 + lock = self.state.lock + 1 + end + self:setState({ + items = Cryo.List.removeIndex(items, #items), + lock = lock, + }) + end, + }), + addItem = Roact.createElement("TextButton", { + Size = UDim2.new(0, 60, 0, 30), + Position = UDim2.new(0, 210, 0, 100), + Text = "Item +", + [Roact.Event.Activated] = function() + if self.animationActive then + return + end + local items = self.state.items + self:setState({ + items = Cryo.List.join(items, {#items + 1}), + }) + end, + }), + }) +end + +return Story diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/chat.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/chat.story.lua new file mode 100644 index 0000000..23c0c88 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/chat.story.lua @@ -0,0 +1,87 @@ +local HttpService = game:GetService("HttpService") + +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Box = function(props) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, 40), + Text = props.text, + }) +end + +local Story = Roact.PureComponent:extend("Story") + +function Story:init() + self.state = { + items = {}, + lock = 0, + index = 1, + size = Vector2.new(400, -100), + } +end + +function Story:render() + return Roact.createFragment({ + scroller = Roact.createElement(Scroller, { + BackgroundColor3 = Color3.fromRGB(56, 19, 18), + Size = UDim2.new(0, self.state.size.X, 1, self.state.size.Y), + Position = UDim2.new(0, 50, 0, 50), + ScrollBarThickness = 8, + padding = UDim.new(0, 5), + orientation = Scroller.Orientation.Down, + itemList = self.state.items, + focusLock = self.state.lock, + focusIndex = self.state.index, + anchorLocation = UDim.new(0, 0), + estimatedItemSize = 40, + dragBuffer = 0, + extraProps = self.state.items, + identifier = function(item) + return item.guid + end, + renderItem = function(item, _) + return Roact.createElement(Box, item) + end, + onScrollUpdate = function(data) + self.indexData = data + end + }), + textbox = Roact.createElement("TextBox", { + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(1, -100, 0, 0), + [Roact.Event.FocusLost] = function(rbx, entered) + if entered and rbx.Text ~= "" then + local newItem = { + text = rbx.Text, + guid = HttpService:GenerateGUID(false), + } + + local newState + if self.state.items then + newState = { items = Cryo.List.join(self.state.items, { newItem })} + + if self.indexData and self.indexData.anchorIndex == #self.state.items then + newState.lock = self.state.lock + 1 + newState.index = #newState.items + end + else + newState = { + items = { newItem }, + index = 1, + lock = 1, + } + end + + rbx.Text = "" + self:setState(newState) + end + end + }), + }) +end + +return Story \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/complexThing.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/complexThing.story.lua new file mode 100644 index 0000000..dbd32b4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/complexThing.story.lua @@ -0,0 +1,41 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) + +local ComplexThing = require(script.Parent.ComplexThing) +local Story = Roact.PureComponent:extend("Story") + +Story.defaultProps = { + numThings = 5, +} + +function Story:init() + self.ref = Roact.createRef() + + self.manyComplexThings = { + layout = Roact.createElement("UIListLayout", { + + }) + } + local function makeComplexThing() + return Roact.createElement(ComplexThing, { + Size = UDim2.fromOffset(256, 256), + nestedLayer = 5, + }) + end + for _ = 1, self.props.numThings do + table.insert(self.manyComplexThings, makeComplexThing()) + end +end + +function Story:render() + return Roact.createElement("ScrollingFrame", { + Position = UDim2.new(0, 0, 0, 300), + ClipsDescendants = false, + Size = UDim2.fromOffset(256, 256), + CanvasSize = UDim2.new(1, 0, 0, self.props.numThings * 256), + ScrollingDirection = Enum.ScrollingDirection.Y, + }, self.manyComplexThings) +end + +return Story diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/elementShuffle.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/elementShuffle.story.lua new file mode 100644 index 0000000..67019b7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/elementShuffle.story.lua @@ -0,0 +1,80 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - item shuffle test") + +local NUM_ITEMS = 10 +function Story:init() + self.state.items = {} + for i = 1,NUM_ITEMS do + table.insert(self.state.items, {id = i}) + end + + self.state.focusLock = 1 +end + +function Story:render() + return Roact.createElement("Frame", { + Size=UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 100, 0, 100), + padding = UDim.new(), + itemList = self.state.items, + focusIndex = 1, + focusLock = self.state.focusLock, + anchorLocation = UDim.new(1, 0), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + identifier = function(item) + return item.id + end, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 20, 0, 20), + BackgroundColor3 = Color3.fromRGB(0, 128 - 8*item.id, 128 + 8*item.id), + }, { + ["INDEX" .. tostring(item.id)] = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Text = item.id, + TextColor3 = Color3.new(1, 1, 1), + BackgroundTransparency = 1, + }), + }) + end, + }, + self.props, + {toDelete = Cryo.None} + )), + shuffle = Roact.createElement("TextButton", { + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "Shuffle", + [Roact.Event.Activated] = function() + local nextItems = self.state.items + + for _, v in pairs(nextItems) do + v.id = v.id+1 + if v.id > NUM_ITEMS then + v.id = 1 + end + end + + self:setState({ + items = nextItems, + focusLock = self.state.focusLock + 1, + }) + end, + }), + }) +end + +return Story diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/init.lua new file mode 100644 index 0000000..c0a4fec --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/init.lua @@ -0,0 +1,27 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) + +Roact.setGlobalConfig({ + propValidation = true, +}) + +local SCREEN_SIZE = Vector2.new(800, 800) + +return { + name = "Scroller", + storyRoot = script, + middleware = function(story, target) + local tree = Roact.createElement("Frame", { + BackgroundColor3 = Color3.fromRGB(30, 31, 28), + Size = UDim2.new(0, SCREEN_SIZE.X, 0, SCREEN_SIZE.Y), + ClipsDescendants = true, + }, { + Story = Roact.createElement(story) + }) + + local handle = Roact.mount(tree, target, "Root") + return function() + Roact.unmount(handle) + end + end, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.addition.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.addition.story.lua new file mode 100644 index 0000000..c814f99 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.addition.story.lua @@ -0,0 +1,88 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - item addition test") + +function Story:init() + local items = {} + local numberOfItems = self.props.numberOfItems or 30 + for i = 1, numberOfItems do + table.insert(items, {id = i}) + end + + self.state = { + itemList = items, + } +end + +function Story:render() + local focusIndex = self.props.addToFront and 1 or #self.state.itemList + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + Size = UDim2.new(0, 100, 0, 200), + focusIndex = focusIndex, + focusLock = self.state.itemList[focusIndex].id, + anchorLocation = UDim.new(1, 0), + dragBuffer = 200, + itemList = self.state.itemList, + orientation = Scroller.Orientation.Up, + identifier = function(item) + return item.id + end, + renderItem = function(item, _) + local r = 88+88*math.sin(math.rad(8*item.id+90)) + local g = 88+44*math.sin(math.rad(8*item.id+0)) + local b = 88+88*math.sin(math.rad(8*item.id+180)) + + return Roact.createElement("Frame", { + Size = UDim2.new(0, 20, 0, 20), + BackgroundColor3 = Color3.fromRGB(r, g, b), + }, { + ["INDEX" .. tostring(item.id)] = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Text = item.id, + TextColor3 = Color3.new(1, 1, 1), + BackgroundTransparency = 1, + }), + }) + end, + }, + self.props, + { + addToFront = Cryo.None, + numberOfItems = Cryo.None, + } + )), + addition = Roact.createElement("TextButton", { + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "Add Item", + + [Roact.Event.Activated] = function() + local nextId = #self.state.itemList + 1 + + local nextItems + if self.props.addToFront then + nextItems = Cryo.List.join({{id = nextId}}, self.state.itemList) + else + nextItems = Cryo.List.join(self.state.itemList, {{id = nextId}}) + end + + self:setState({ + itemList = nextItems, + }) + end, + }), + }) +end + +return Story diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.deletion.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.deletion.story.lua new file mode 100644 index 0000000..63aff65 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.deletion.story.lua @@ -0,0 +1,78 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - item deletion test") + +function Story:init() + self.state.items = {} + for i = -20,20 do + table.insert(self.state.items, {id = i}) + end +end + +function Story:render() + return Roact.createElement("Frame", { + Size=UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 100, 0, 100), + padding = UDim.new(), + itemList = self.state.items, + focusIndex = 21, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + identifier = function(item) + return item.id + end, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = item == 0 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, 128 - 8*item.id, 128 + 8*item.id), + }, { + ["INDEX" .. tostring(item.id)] = Roact.createElement("Frame"), + }) + end, + }, + self.props, + {toDelete = Cryo.None} + )), + deletion = Roact.createElement("TextButton", { + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "Delete", + [Roact.Event.Activated] = function() + local nextItems + if self.props.toDelete then + nextItems = Cryo.List.filter(self.state.items, function(item) + for _, v in pairs(self.props.toDelete) do + if item.id == v then + return false + end + end + return true + end) + else + local n = math.random(1, #self.state.items) + print("Deleting index " .. tostring(n)) + nextItems = Cryo.List.removeIndex(self.state.items, n) + end + self:setState({ + items = nextItems + }) + end, + }), + }) +end + +return Story \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.extras.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.extras.story.lua new file mode 100644 index 0000000..e26838b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.extras.story.lua @@ -0,0 +1,78 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - extra props test") + +local smallScroll = Roact.PureComponent:extend("smallScroll") +function smallScroll:render() + return Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 50, 0, 50), + padding = UDim.new(), + focusIndex = 6, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + }, self.props)) +end + +function Story:init() + self.state = { + items = {}, + store = {}, + } + for i = 1, 11 do + self.state.items[i] = i + self.state.store[i] = (i == 6 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, i*23, i*23)) + end + + self.renderItem = function(item) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = self.state.store[item], + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end +end + +function Story:render() + return Roact.createElement("Frame", {}, { + scroller = Roact.createElement(smallScroll, Cryo.Dictionary.join({ + itemList = self.state.items, + renderItem = self.renderItem, + extraProps = {self.state.store}, + }, self.props, {newColor = Cryo.None})), + change = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(0, 100, 0, 10), + BackgroundColor3 = Color3.new(1, 1, 1), + Text = "X", + [Roact.Event.Activated] = function() + local newColor = self.props.newColor + if not newColor then + newColor = Color3.fromRGB( + math.random(0, 255), + math.random(0, 255), + math.random(0, 255) + ) + print("Changing color to ", newColor) + end + + local store = {} + for i = 1, 11 do + store[i] = newColor + end + self:setState({store = store}) + end, + }), + }) +end + +return Story diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.few.large.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.few.large.story.lua new file mode 100644 index 0000000..a976be7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.few.large.story.lua @@ -0,0 +1,30 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - a few large items") + +function Story:render() + return Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 50, 0, 50), + padding = UDim.new(), + itemList = {1, 2, 3, 4, 5, 6, 7}, + focusIndex = 4, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 49, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 50, 0, 50), + BackgroundColor3 = Color3.fromRGB(item*30, item*30, item*30), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, self.props)) +end + +return Story \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.infinite.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.infinite.story.lua new file mode 100644 index 0000000..ea63c4e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.infinite.story.lua @@ -0,0 +1,125 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - infinite scroll in both directions") + +function Story:init() + self.state.items = { + { + token = 0, + color = Color3.fromRGB(255, 255, 255), + } + } + self.state.size = Vector2.new(50, 50) + + self.loadPrevious = function() + local newItems = {} + local n = self.state.items[1].token + for i = n-10, n-1 do + table.insert(newItems, { + token = i, + color = Color3.fromRGB(0, 128 - i, 128 + i), + }) + end + self:setState({ + items = Cryo.List.join(newItems, self.state.items) + }) + end + + self.loadNext = function() + local newItems = {} + local n = self.state.items[#self.state.items].token + for i = n+1, n+10 do + table.insert(newItems, { + token = i, + color = Color3.fromRGB(0, 128 - i, 128 + i), + }) + end + self:setState({ + items = Cryo.List.join(self.state.items, newItems) + }) + end +end + +function Story:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, self.state.size.X, 0, self.state.size.Y), + padding = UDim.new(0, 3), + itemList = self.state.items, + loadNext = self.loadNext, + loadPrevious = self.loadPrevious, + focusIndex = 1, + anchorLocation = UDim.new(0.5, 0), + orientation = Scroller.Orientation.Up, + estimatedItemSize = 10, + mountingBuffer = 50, + identifier = function(item) + return item.token + end, + renderItem = function(item, _) + assert(item.token, "Item's token is unset") + assert(item.color, "Item's color is unset") + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = item.color, + }, { + ["INDEX" .. tostring(item.token)] = Roact.createElement("Frame"), + }) + end, + }, self.props)), + moveUp = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "^", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, -20) + }) + end, + }), + moveDown = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 100), + AnchorPoint = Vector2.new(1, 0), + Text = "v", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, 20) + }) + end, + }), + moveLeft = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -100, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = "<", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(-20, 0) + }) + end, + }), + moveRight = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, 0, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = ">", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(20, 0) + }) + end, + }), + }) +end + +return Story \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.rearrange.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.rearrange.story.lua new file mode 100644 index 0000000..4604bee --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.rearrange.story.lua @@ -0,0 +1,69 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - enough small items to fill the view") + +Story.defaultProps = { + startItems = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, + newItems = {11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}, +} + +function Story:init() + self.state.items = self.props.startItems +end + +function Story:render() + return Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + }, { + Scroller = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.fromOffset(50, 50), + padding = UDim.new(), + itemList = self.state.items, + focusIndex = 6, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = item == 6 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, item*23, item*23), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, + self.props, + { + startItems = Cryo.None, + newItems = Cryo.None, + clicked = Cryo.None, + } + )), + Button = Roact.createElement("TextButton", { + Size = UDim2.fromOffset(100, 50), + Position = UDim2.fromScale(1, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "Rearrange", + [Roact.Event.Activated] = function() + self:setState({ + items = self.props.newItems, + }) + if self.props.clicked then + self.props.clicked() + end + end, + }) + }) +end + +return Story diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.removeTopEntry.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.removeTopEntry.story.lua new file mode 100644 index 0000000..ce8853c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.removeTopEntry.story.lua @@ -0,0 +1,96 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - delete top item test") + +local END_OF_LIST_INDEX = 100 + +function Story:init() + self.state.items = {} + + if self.props.loadAll then + for i = 1, END_OF_LIST_INDEX do + table.insert(self.state.items, {id = i}) + end + else + for i = 1, 20 do + table.insert(self.state.items, {id = i}) + end + end +end + +function Story:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(38, 161, 38), + Size = UDim2.new(0, 100, 0, 100), + padding = UDim.new(), + itemList = self.state.items, + focusIndex = 1, + anchorLocation = UDim.new(1, 0), + estimatedItemSize = 20, + orientation = Scroller.Orientation.Up, + BackgroundTransparency = 0, + dragBuffer = 0, + ElasticBehavior = Enum.ElasticBehavior.Always, + ScrollBarThickness = 10, + VerticalScrollBarInset = Enum.ScrollBarInset.Always, + identifier = function(item) + return item.id + end, + renderItem = function(item, _) + return Roact.createElement("TextButton", { + Size = UDim2.new(0, 20, 0, 20), + BackgroundColor3 = Color3.fromRGB(200,200, 200), + Text = tostring(item.id) + }, { + ["INDEX" .. tostring(item.id)] = Roact.createElement("Frame"), + }) + end, + loadNext = function() + if not self.props.loadAll then + local newItems = {} + local n = self.state.items[#self.state.items].id + local endIndex = math.min(n + 10, END_OF_LIST_INDEX) + for i = n+1, endIndex do + table.insert(newItems, { + id = i, + }) + end + + if not Cryo.isEmpty(newItems) then + self:setState({ + items = Cryo.List.join(self.state.items, newItems) + }) + end + end + end, + }, + self.props, + {deleteLastItem = Cryo.None}, + {loadAll = Cryo.None} + )), + deletion = Roact.createElement("TextButton", { + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "Delete", + [Roact.Event.Activated] = function() + local indexToDelete = self.props.deleteLastItem and #self.state.items or 1 + local nextItems = Cryo.List.removeIndex(self.state.items, indexToDelete) + self:setState({ + items = nextItems + }) + end, + }), + }) +end + +return Story diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.resize.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.resize.story.lua new file mode 100644 index 0000000..2f602b8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.resize.story.lua @@ -0,0 +1,121 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Bar = Roact.PureComponent:extend("Bar") + +function Bar:init() + self.state = { + clicked = false, + } +end + +function Bar:render() + return Roact.createElement("TextButton", Cryo.Dictionary.join( + { + Size = UDim2.new(1, 0, 0, self.state.clicked and 30 or 10), + [Roact.Event.Activated] = function() + self:setState({ + clicked = not self.state.clicked, + }) + end, + }, + self.props + )) +end + +local Story = Roact.PureComponent:extend("Rhodium Story - frame resize test") +Story.defaultProps = { + resizeAmount = 20, + initialHeight = 50, +} + +function Story:init() + self.state = { + size = Vector2.new(50, self.props.initialHeight), + } +end + +function Story:render() + return Roact.createElement("Frame", { + Size=UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, self.state.size.X, 0, self.state.size.Y), + padding = UDim.new(), + itemList = {1, 2, 3}, + focusIndex = 2, + anchorLocation = UDim.new(0, 0), + orientation = Scroller.Orientation.Down, + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + renderItem = function(item, _) + return Roact.createElement(Bar, { + BackgroundColor3 = item == 2 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, -128 + 128*item, 376 - 128*item), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, + self.props, + { + resizeAmount = Cryo.None, + initialHeight = Cryo.None, + } + )), + moveUp = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 0), + AnchorPoint = Vector2.new(1, 0), + Text = "^", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, -self.props.resizeAmount) + }) + end, + }), + moveDown = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -50, 0, 100), + AnchorPoint = Vector2.new(1, 0), + Text = "v", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(0, self.props.resizeAmount) + }) + end, + }), + moveLeft = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, -100, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = "<", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(-self.props.resizeAmount, 0) + }) + end, + }), + moveRight = Roact.createElement("TextButton", { + Size = UDim2.new(0, 50, 0, 50), + Position = UDim2.new(0.5, 0, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = ">", + [Roact.Event.Activated] = function() + self:setState({ + size = self.state.size + Vector2.new(self.props.resizeAmount, 0) + }) + end, + }), + }) +end + +return Story diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.several.small.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.several.small.story.lua new file mode 100644 index 0000000..4313821 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.several.small.story.lua @@ -0,0 +1,46 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - enough small items to fill the view") + +function Story:render() + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = self.props.frameSize or UDim2.new(0, 50, 0, 50), + }, { + layout = Roact.createElement("UIListLayout", { + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }), + + scroll = Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 50, 0, 50), + padding = UDim.new(), + itemList = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, + focusIndex = 6, + anchorLocation = UDim.new(0.5, 0), + loadingBuffer = 2, + mountingBuffer = 99, + estimatedItemSize = 10, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 10, 0, 10), + BackgroundColor3 = item == 6 + and Color3.fromRGB(255, 255, 255) + or Color3.fromRGB(0, item*23, item*23), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("Frame"), + }) + end, + }, + self.props, + { frameSize = Cryo.None } + )) + }) +end + +return Story diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.single.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.single.story.lua new file mode 100644 index 0000000..d8ba0ae --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.single.story.lua @@ -0,0 +1,22 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - a single small item") + +function Story:render() + return Roact.createElement(Scroller, Cryo.Dictionary.join({ + BackgroundColor3 = Color3.fromRGB(255, 0, 0), + Size = UDim2.new(0, 50, 0, 50), + itemList = {1}, + anchorLocation = UDim.new(0.5, 0), + dragBuffer = 0, + renderItem = function() + return Roact.createElement("Frame", {Size=UDim2.new(0, 10, 0, 10)}) + end, + }, self.props)) +end + +return Story \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.swapLists.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.swapLists.story.lua new file mode 100644 index 0000000..e65a943 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/rho.swapLists.story.lua @@ -0,0 +1,87 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +local Story = Roact.PureComponent:extend("Rhodium Story - Swap a large list for a smaller one") +Story.defaultProps = { + anchorLocation = UDim.new(1, -10), + focusIndex = 1, + orientation = Scroller.Orientation.Up, + size = UDim2.new(0, 400, 0, 300), + + shortToLong = false, -- Swap from a short list to a long list when true, swap from a long list to a short list when false + longItemList = {"a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"}, + shortItemList = {"Z", "X", "Y"}, +} + +function Story:init() + self.state = { + items = self.props.shortToLong and self.props.shortItemList or self.props.longItemList, + } +end + +function Story:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + scroller = Roact.createElement(Scroller, Cryo.Dictionary.join( + { + BackgroundColor3 = Color3.fromRGB(56, 19, 18), + Size = self.props.size, + orientation = self.props.orientation, + itemList = self.state.items, + focusLock = self.state.items[1], + focusIndex = self.props.focusIndex, + anchorLocation = self.props.anchorLocation, + estimatedItemSize = 40, + dragBuffer = 0, + identifier = function(item) + return item + end, + renderItem = function(item, _) + return Roact.createElement("Frame", { + Size = UDim2.new(0, 300, 0, 40), + BackgroundColor3 = Color3.fromRGB(0, 255, 21), + }, { + ["INDEX" .. tostring(item)] = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Text = item, + TextColor3 = Color3.new(0, 0, 0), + BackgroundTransparency = 1, + }), + }) + end, + }, + self.props, + { + shortToLong = Cryo.None, + longItemList = Cryo.None, + shortItemList = Cryo.None, + size = Cryo.None, + } + )), + swapButton = Roact.createElement("TextButton", { + Size = UDim2.new(0, 100, 0, 50), + Position = UDim2.new(1, -100, 0, 0), + Text = "Swap Lists", + + [Roact.Event.Activated] = function() + -- Swap lists + if #self.state.items == #self.props.shortItemList then + self:setState({ + items = self.props.longItemList, + }) + else + self:setState({ + items = self.props.shortItemList, + }) + end + end, + }), + }) +end + +return Story diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/scroller.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/scroller.story.lua new file mode 100644 index 0000000..521afa6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/scroller.story.lua @@ -0,0 +1,204 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller + +-- Note: if you set this to ComplexThing, you may crash +local Thing = require(script.Parent.ResizingThing) + +local Story = Roact.PureComponent:extend("Story") + +function Story:render() + return Roact.createFragment({ + scroller = Roact.createElement(Scroller, { + BackgroundColor3 = Color3.fromRGB(56, 19, 18), + Size = UDim2.new(0, self.state.size.X, 1, self.state.size.Y), + Position = UDim2.new(0, 50, 0, 50), + ScrollBarThickness = 8, + padding = UDim.new(0, 5), + orientation = Scroller.Orientation.Down, + itemList = self.state.items, + loadNext = self.loadNext, + loadPrevious = self.loadPrevious, + focusLock = self.state.lock, + focusIndex = self.state.index, + anchorLocation = UDim.new(0, 0), + estimatedItemSize = 40, + identifier = function(item) return item.token end, + renderItem = function(item, _) + return Roact.createElement(Thing, item) + end, + onScrollUpdate = function(data) + self.indexData = data + end + }), + refresh = Roact.createElement("TextButton", { + Size = UDim2.new(0, 110, 0, 30), + Position = UDim2.new(1, -50, 0, 50), + AnchorPoint = Vector2.new(1, 0), + Text = "Refresh", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickRefresh, + }), + up = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -90, 0, 90), + AnchorPoint = Vector2.new(1, 0), + Text = "^", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickUp, + }), + down = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -90, 0, 170), + AnchorPoint = Vector2.new(1, 0), + Text = "v", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickDown, + }), + left = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -130, 0, 130), + AnchorPoint = Vector2.new(1, 0), + Text = "<", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickLeft, + }), + right = Roact.createElement("TextButton", { + Size = UDim2.new(0, 30, 0, 30), + Position = UDim2.new(1, -50, 0, 130), + AnchorPoint = Vector2.new(1, 0), + Text = ">", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.clickRight, + }), + skipUp = Roact.createElement("TextButton", { + Size = UDim2.new(0, 60, 0, 30), + Position = UDim2.new(1, -90, 0, 220), + AnchorPoint = Vector2.new(1, 0), + Text = "SkipUp", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.skipUp, + }), + skipDown = Roact.createElement("TextButton", { + Size = UDim2.new(0, 60, 0, 30), + Position = UDim2.new(1, -90, 0, 270), + AnchorPoint = Vector2.new(1, 0), + Text = "SkipDown", + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + [Roact.Event.Activated] = self.skipDown, + }), + SkipAmount = Roact.createElement("TextBox", { + Size = UDim2.new(0, 60, 0, 30), + Position = UDim2.new(1, 0, 0, 270), + AnchorPoint = Vector2.new(1, 0), + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + Text = "SkipAmt", + PlaceholderText = "Skip", + ClearTextOnFocus = true, + [Roact.Change.Text] = self.onChangeText, + }), + }) +end + +local function generate(token) + if token == 0 then + return {color=Color3.fromRGB(255, 255, 255), width=50, token=0} + end + return {color=Color3.fromRGB(128-token, 255, 128+token), width=50+40*math.sin(token/5), token=token} +end + +function Story:init() + local items = {} + for i = -100,100 do table.insert(items, generate(i)) end + self.state = { + lock = 1, + index = 101, + skipAmount = 1, + size = Vector2.new(200, -100), + items = items, + } + self.indexData = { + anchorIndex = 101, + } + + self.loadNext = function() + self:setState({ + items = Cryo.List.join(self.state.items, {generate(self.state.items[#self.state.items].token + 1)}), + }) + end + + self.loadPrevious = function() + self:setState({ + items = Cryo.List.join({generate(self.state.items[1].token - 1)}, self.state.items), + }) + end + + self.clickRefresh = function() + print("Recentering") + self:setState({ + lock = self.state.lock + 1, + }) + end + + self.clickUp = function() + print("Moving up") + self:setState({ + size = self.state.size + Vector2.new(0, -20), + }) + end + + self.clickDown = function() + print("Moving down") + self:setState({ + size = self.state.size + Vector2.new(0, 20), + }) + end + + self.clickLeft = function() + print("Moving left") + self:setState({ + size = self.state.size + Vector2.new(-20, 0), + }) + end + + self.clickRight = function() + print("Moving right") + self:setState({ + size = self.state.size + Vector2.new(20, 0), + }) + end + + self.skipUp = function() + local newIndex = self.indexData.anchorIndex + self.state.skipAmount + print("Skipping up to index:", newIndex) + self:setState({ + lock = self.state.lock + 1, + index = newIndex, + }) + end + + self.skipDown = function() + local newIndex = self.indexData.anchorIndex - self.state.skipAmount + print("Skipping down to index:", newIndex) + self:setState({ + lock = self.state.lock + 1, + index = newIndex, + }) + end + + self.onChangeText = function(rbx) + local skipAmount = tonumber(rbx.Text) + print("skipAmount:", skipAmount) + if skipAmount then + self:setState({ + skipAmount = skipAmount, + }) + else + rbx.Text = self.state.skipAmount + end + end +end + +return Story diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/scrollerDebugger.story.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/scrollerDebugger.story.lua new file mode 100644 index 0000000..feae994 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/Storybook/scrollerDebugger.story.lua @@ -0,0 +1,513 @@ +local InfiniteScroller = script:FindFirstAncestor("infinite-scroller") +local Root = InfiniteScroller.Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local Scroller = require(InfiniteScroller).Scroller +local ComplexThing = require(script.Parent.ComplexThing) + +local TextService = game:GetService("TextService") + +local debugScroller = Roact.PureComponent:extend("debugScroller") + +local COLORS = { + DEFAULT = Color3.fromRGB(188, 188, 188), + WHITE = Color3.fromRGB(244, 244, 244), + BLACK = Color3.fromRGB(0, 0, 0), + TRUE = Color3.fromRGB(188, 222, 188), + FALSE = Color3.fromRGB(222, 188, 188), + + FOCUS_INDEX = Color3.fromRGB(0, 255, 0), + ANCHOR_LOCATION = Color3.fromRGB(0, 127, 255), + DRAG_BUFFER = Color3.fromRGB(222, 0, 222), + CANVAS = Color3.fromRGB(244, 188, 66), + LEAD_INDEX = Color3.fromRGB(0, 255, 0), + TRAIL_INDEX = Color3.fromRGB(255, 0, 0), + ANCHOR_INDEX = Color3.fromRGB(255, 255, 0), + PADDING = Color3.fromRGB(145, 88, 222), +} + +local COMPLEX_THING_SIZE = 128 + +local LAYOUT_ORDER_INDEX = 0 +function getNextLayout() + LAYOUT_ORDER_INDEX = LAYOUT_ORDER_INDEX + 1 + return LAYOUT_ORDER_INDEX +end + +function makeLabel(text, value, color) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, 20), + Text = string.format("%s: %s", text, tostring(value)), + TextColor3 = color or COLORS.WHITE, + TextXAlignment = Enum.TextXAlignment.Left, + BorderSizePixel = 0, + BackgroundTransparency = 1, + LayoutOrder = getNextLayout(), + }) +end + +function makeButton(text, func, color) + color = color or COLORS.DEFAULT + return Roact.createElement("TextButton", { + Size = UDim2.fromOffset(110, 30), + BackgroundColor3 = color, + TextWrapped = true, + Text = text, + [Roact.Event.Activated] = func, + LayoutOrder = getNextLayout(), + }) +end + +function debugScroller:makeToggleButton(value) + local function doToggle() + self:setState({ + [value] = not self.state[value] + }) + end + return makeButton(value, doToggle, self.state[value] and COLORS.TRUE or COLORS.FALSE) +end + +function makeLabelLine(text, position, color, pointsRight) + color = color or COLORS.BLACK + local LINE_WIDTH = 20 + + local textSize = TextService:GetTextSize(text, 8, Enum.Font.Legacy, Vector2.new()) + return Roact.createElement("Frame", { + Size = UDim2.fromOffset(textSize.X + LINE_WIDTH, textSize.Y), + Position = position, + AnchorPoint = pointsRight and Vector2.new(1, 0.5) or Vector2.new(0, 0.5), + BackgroundTransparency = 1, + }, { + Roact.createFragment({ + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + labelLine = Roact.createElement("Frame", { + Position = position, + Size = UDim2.fromOffset(LINE_WIDTH, 1), + BorderSizePixel = 0, + BackgroundColor3 = color, + LayoutOrder = 2, + }), + label = Roact.createElement("TextLabel", { + AnchorPoint = Vector2.new(0, 0.5), + Size = UDim2.new(1, -LINE_WIDTH, 0, 8), + Text = text, + TextColor3 = color, + BackgroundTransparency = 1, + LayoutOrder = pointsRight and 1 or 3 + }) + }) + }) +end + +debugScroller.defaultProps = { + anchorLocation = UDim.new(1, 0), + mountingBuffer = 150, + dragBuffer = 0, + focusIndex = 1, + + Size = UDim2.new(1, -20, 0, 100), + numItems = 20, +} + +function debugScroller:init() + self.initialState = { + items = {}, + -- Scroller props. These can be changed by debugger buttons + focusLock = 1, + orientation = Scroller.Orientation.Up, + clipsDescendants = false, + nestedLayer = 1, + + -- Scroller internals. Don't change these manually, meant for DISPLAY ONLY + canvasPosition = 0, + canvasSize = 0, + paddingSize = -1, + paddingPosition = 0, + anchorLinePositionY = 0, + leadIndex = -1, + trailIndex = -1, + anchorIndex = -1, + } + + self.mutableState = { + loadPreviousEnabled = false, + loadNextEnabled = false, + } + + for i = 1,self.props.numItems do + table.insert(self.initialState.items, {id = i}) + end + + self.state = Cryo.Dictionary.join(self.initialState, self.mutableState) + + -- horsecat storybooks have a Y-offset that we need to address before using absolutePosition + self.initialY, self.updateInitialY = Roact.createBinding(0) + + self.ref = Roact.createRef() +end + +function debugScroller:render() + return Roact.createElement("Frame", { + Size=UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + [Roact.Change.AbsolutePosition] = function(rbx) + self.updateInitialY(rbx.AbsolutePosition.Y) + end + }, { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + }), + visibilityFrame = Roact.createElement("Frame", { + LayoutOrder = 1, + Size = UDim2.new(0, 200, 1, 0), + BackgroundTransparency = 1, + }, { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + }), + anchorLocationLabel = makeLabel("anchorLocation", self.props.anchorLocation, COLORS.ANCHOR_LOCATION), + anchorIndexLabel = makeLabel("anchorIndex", self.state.anchorIndex, COLORS.ANCHOR_INDEX), + leadIndexLabel = makeLabel("leadIndex", self.state.leadIndex, COLORS.LEAD_INDEX), + trailIndexLabel = makeLabel("trailIndex", self.state.trailIndex, COLORS.TRAIL_INDEX), + canvasPositionLabel = makeLabel("canvasPosition", self.state.canvasPosition, COLORS.CANVAS), + canvasSizeLabel = makeLabel("canvasSize", self.state.canvasSize, COLORS.CANVAS), + paddingSizeLabel = makeLabel("size of padding Frame", self.state.paddingSize, COLORS.PADDING), + + whiteSpace = Roact.createElement("Frame", { Size = UDim2.fromOffset(0, 20), BackgroundTransparency = 1, LayoutOrder = getNextLayout()}), + + disableLoadPrevious = self:makeToggleButton("loadPreviousEnabled"), + disableLoadNext = self:makeToggleButton("loadNextEnabled"), + clipsDescendants = self:makeToggleButton("clipsDescendants"), + }), + + scrollerFrame = Roact.createElement("Frame", { + LayoutOrder = 2, + Size = UDim2.new(0, 160, 0, 300), + BackgroundTransparency = 1, + }, { + scroller = Roact.createElement(Scroller, { + ElasticBehavior = Enum.ElasticBehavior.Always, + ClipsDescendants = self.state.clipsDescendants, + Position = UDim2.fromOffset(0, 200), + BackgroundColor3 = Color3.fromRGB(111, 111, 111), + Size = self.props.Size, + + orientation = self.state.orientation, + padding = UDim.new(), + itemList = self.state.items, + focusIndex = self.props.focusIndex, + focusLock = self.state.focusLock, + anchorLocation = self.props.anchorLocation, + dragBuffer = self.props.dragBuffer, + mountingBuffer = self.props.mountingBuffer, + estimatedItemSize = self.state.nestedLayer ~= 1 and COMPLEX_THING_SIZE or 20, + [Roact.Ref] = self.ref, + identifier = function(item) + return item.id + end, + --recyclingDisabledFor={"ComplexThing"}, + renderItem = function(item, _) + if self.state.nestedLayer ~= 1 then + return Roact.createElement(ComplexThing, { + Size = UDim2.fromOffset(COMPLEX_THING_SIZE, COMPLEX_THING_SIZE), + nestedLayer = self.state.nestedLayer, + }) + else + local r = 88+88*math.sin(math.rad(8*item.id+90)) + local g = 88+44*math.sin(math.rad(8*item.id+0)) + local b = 88+88*math.sin(math.rad(8*item.id+180)) + + local leadIndexId = self.state.items[self.state.leadIndex] and self.state.items[self.state.leadIndex].id + local trailIndexId = self.state.items[self.state.trailIndex] and self.state.items[self.state.trailIndex].id + local anchorIndexId = self.state.items[self.state.anchorIndex] and self.state.items[self.state.anchorIndex].id + + return Roact.createElement("Frame", { + Size = UDim2.new(0, 20, 0, 20), + BackgroundColor3 = Color3.fromRGB(r, g, b), + }, { + ["INDEX" .. tostring(item.id)] = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Text = item.id, + TextColor3 = Color3.new(1, 1, 1), + BackgroundTransparency = 1, + }, { + item.id == leadIndexId and makeLabelLine("leadIndex ", + UDim2.fromScale(1, 0), + COLORS.LEAD_INDEX, + false), + item.id == trailIndexId and makeLabelLine("trailIndex ", + UDim2.fromScale(1, 0), + COLORS.TRAIL_INDEX, + false), + item.id == anchorIndexId and makeLabelLine("anchorIndex ", + UDim2.new(), + COLORS.ANCHOR_INDEX, + true) + }), + }) + end + end, + + loadPrevious = function() + if not self.state.loadPreviousEnabled then + return + end + local newItems = {} + local n = self.state.items[1].id + for i = n-10, n-1 do + table.insert(newItems, { + id = i, + }) + end + self:setState({ + items = Cryo.List.join(newItems, self.state.items) + }) + end, + + loadNext = function() + if not self.state.loadNextEnabled then + return + end + local newItems = {} + local n = self.state.items[#self.state.items].id + for i = n+1, n+10 do + table.insert(newItems, { + id = i, + }) + end + self:setState({ + items = Cryo.List.join(self.state.items, newItems) + }) + end, + + [Roact.Change.CanvasPosition] = function() + if not self.ref.current then + return + end + + local rbx = self.ref.current + + -- this feels dirty, but allows us to visualize the padding frame + local padding_rbx = rbx:FindFirstChild("padding") + + local anchorLinePositionY = rbx.AbsolutePosition.Y - self.props.anchorLocation.Offset + if self.state.orientation == Scroller.Orientation.Down then + anchorLinePositionY = anchorLinePositionY + (1 - self.props.anchorLocation.Scale) * rbx.AbsoluteSize.Y + elseif self.state.orientation == Scroller.Orientation.Up then + anchorLinePositionY = anchorLinePositionY + self.props.anchorLocation.Scale * rbx.AbsoluteSize.Y + else + anchorLinePositionY = 0 + end + + self:setState({ + canvasPosition = rbx.CanvasPosition.Y, + canvasSize = rbx.CanvasSize.Y.Offset, + paddingSize = padding_rbx.Size.Y.Offset, + + anchorLinePositionY = anchorLinePositionY, + paddingPosition = padding_rbx.AbsolutePosition.Y, + }) + end, + + onScrollUpdate = function(indices) + self.state.leadIndex = indices.leadIndex + self.state.anchorIndex = indices.anchorIndex + self.state.trailIndex = indices.trailIndex + end + }), + + anchorLine = self.ref.current and makeLabelLine("anchorLocation ", + UDim2.fromOffset(20, self.state.anchorLinePositionY - self.initialY:getValue()), + COLORS.ANCHOR_LOCATION, + true), + + dragBuffer1 = self.ref.current and self.props.dragBuffer ~= 0 and makeLabelLine("dragBuffer ", + UDim2.fromOffset(20, self.props.dragBuffer + self.ref.current.AbsolutePosition.Y - self.initialY:getValue()), + COLORS.DRAG_BUFFER, + true), + + dragBuffer2 = self.ref.current and self.props.dragBuffer ~= 0 and makeLabelLine("dragBuffer ", + UDim2.fromOffset(20, self.ref.current.AbsoluteSize.Y - self.props.dragBuffer + self.ref.current.AbsolutePosition.Y - self.initialY:getValue()), + COLORS.DRAG_BUFFER, + true), + + mountingBuffer1 = self.ref.current and makeLabelLine("mountingBuffer ", + UDim2.fromOffset(20, -self.props.mountingBuffer + self.ref.current.AbsolutePosition.Y - self.initialY:getValue()), + COLORS.WHITE, + true), + + mountingBuffer2 = self.ref.current and makeLabelLine("mountingBuffer ", + UDim2.fromOffset(20, self.ref.current.AbsoluteSize.Y + self.props.mountingBuffer + self.ref.current.AbsolutePosition.Y - self.initialY:getValue()), + COLORS.WHITE, + true), + + canvasEstimate = self.ref.current and Roact.createElement("Frame", { + Size = UDim2.fromOffset(40, self.state.canvasSize), + Position = UDim2.fromOffset(20, self.state.paddingPosition - self.initialY:getValue()), + BackgroundColor3 = COLORS.CANVAS, + BorderSizePixel = 0, + ZIndex = -10, + }), + + paddingEstimate = self.ref.current and Roact.createElement("Frame", { + Size = UDim2.fromOffset(30, self.state.paddingSize), + Position = UDim2.fromOffset(0, self.state.paddingPosition - self.initialY:getValue()), + BackgroundColor3 = COLORS.PADDING, + BorderSizePixel = 0, + ZIndex = -9, + }), + }), + operationsFrame = Roact.createElement("Frame", { + LayoutOrder = 3, + Size = UDim2.new(0, 100, 1, 0), + BackgroundTransparency = 1, + }, { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + }), + reset = makeButton("Reset ItemList", function() + local newState = Cryo.Dictionary.join(self.initialState, { + focusLock = self.state.focusLock + 1, + loadNext = self.state.loadNext, + loadPrev = self.state.loadPrev, + clipsDescendants = self.state.clipsDescendants, + }) + self:setState(newState) + end), + + -- Reversing the Orientation currently does not call resize, does not update the anchorLocation + -- reverse = makeButton("Reverse Orientation", function() + -- local newOrientation + -- if self.state.orientation == Scroller.Orientation.Down then + -- newOrientation = Scroller.Orientation.Up + -- else + -- newOrientation = Scroller.Orientation.Down + -- end + -- self:setState({ + -- orientation = newOrientation, + -- focusLock = self.state.focusLock + 1, + -- }) + -- end), + + rotateForward = makeButton("Rotate Forward", function() + local nextItems = {} + local numItems = #self.state.items + local a = self.state.items[numItems] + table.insert(nextItems, a) + for i = 1, numItems-1 do + table.insert(nextItems, self.state.items[i]) + end + + self:setState({ + focusLock = self.state.focusLock + 1, + items = nextItems, + }) + end), + + rotateBack = makeButton("Rotate Backward", function() + local nextItems = {} + local a = self.state.items[1] + for i = 2, #self.state.items do + table.insert(nextItems, self.state.items[i]) + end + table.insert(nextItems, a) + + self:setState({ + focusLock = self.state.focusLock + 1, + items = nextItems, + }) + end), + + insertFront = makeButton("Insert Front", function() + local nextItems = {} + table.insert(nextItems, {id = self.state.items[1].id-1 }) + for k, v in pairs(self.state.items) do + table.insert(nextItems, v) + end + + self:setState({ + focusLock = self.state.focusLock + 1, + items = nextItems, + }) + end), + + insertBack = makeButton("Insert Back", function() + local nextItems = {} + for k, v in pairs(self.state.items) do + table.insert(nextItems, v) + end + table.insert(nextItems, {id = self.state.items[#self.state.items].id+1 }) + + self:setState({ + focusLock = self.state.focusLock + 1, + items = nextItems, + }) + end), + + removeFront = makeButton("Remove Front", function() + local nextItems = {} + local numItems = #self.state.items + for i = 2, numItems do + table.insert(nextItems, self.state.items[i]) + end + + self:setState({ + focusLock = self.state.focusLock + 1, + items = nextItems, + }) + end), + + removeBack = makeButton("Remove Back", function() + local nextItems = {} + local numItems = #self.state.items + for i = 1, numItems-1 do + table.insert(nextItems, self.state.items[i]) + end + + self:setState({ + focusLock = self.state.focusLock + 1, + items = nextItems, + }) + end), + + + reverseList = makeButton("Reverse List", function() + local nextItems = {} + local numItems = #self.state.items + for i = 1, numItems do + nextItems[i] = self.state.items[numItems - i + 1] + end + + self:setState({ + focusLock = self.state.focusLock + 1, + items = nextItems, + }) + end), + + scrollUpOnce = makeButton("Scroll Up 1px", function() + if self.ref.current then + self.ref.current.CanvasPosition = self.ref.current.CanvasPosition - Vector2.new(0, 1) + end + end), + + scrollDownOnce = makeButton("Scroll Down 1px", function() + if self.ref.current then + self.ref.current.CanvasPosition = self.ref.current.CanvasPosition + Vector2.new(0, 1) + end + end), + + toggleComplexity = makeButton("Toggle Complexity", function() + self:setState({ + nestedLayer = self.state.nestedLayer%5 + 1 + }) + end), + }) + }) +end + +return debugScroller diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/init.lua new file mode 100644 index 0000000..61e7222 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/infinite-scroller/init.lua @@ -0,0 +1,5 @@ +local Scroller = require(script.Components.Scroller) + +return { + Scroller = Scroller, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/lock.toml new file mode 100644 index 0000000..2003503 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/lock.toml @@ -0,0 +1,12 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/infinite-scroller" +version = "0.5.6" +commit = "d622d74bec4a599c5f8ef642194fa9eaf973b5c0" +source = "url+https://github.com/roblox/infinite-scroller" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/t.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.5.6/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/FitFrame.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/FitFrame.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/FitFrame.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/Lumberyak.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/Lumberyak.lua new file mode 100644 index 0000000..7803fb4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/Lumberyak.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_lumberyak-6ce30d59-0.1.1"]["lumberyak"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/Otter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/.robloxrc b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/.robloxrc new file mode 100644 index 0000000..b261580 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/.robloxrc @@ -0,0 +1,5 @@ +{ + "language": { + "mode": "strict" + } +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/Distance.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/Distance.lua new file mode 100644 index 0000000..65f28a2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/Distance.lua @@ -0,0 +1,16 @@ +return { + -- Returns the signed distance from a point to a range. Returns 0 if the + -- point is within the range, positive if it's below and negative if it's + -- above. + fromPointToRangeSigned = function(point: number, rangeTop: number, rangeSize: number) + local rangeBottom = rangeTop + rangeSize + + if point < rangeTop then + return point - rangeTop + elseif point > rangeBottom then + return point - rangeBottom + else + return 0 + end + end +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/KeyPool.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/KeyPool.lua new file mode 100644 index 0000000..8b80459 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/KeyPool.lua @@ -0,0 +1,83 @@ +--[[ + KeyPool provides a pool of objects suitable for use as map keys. + + Create a new KeyPool, then call pool:get() to get a new key. Once you're done with it, call key:release(). + + Example: + local pool = KeyPool.new("foo") + ... + local key1 = pool:get() + local key2 = pool:get() + map[key1] = thing1 + map[key2] = thing2 + ... + map[key1] = nil + key1:release() + key1 = nil +]] +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local t = require(Root.t) + +-- Forward declarations +local KeyPool = {} +KeyPool.__index = KeyPool + +local Key = {} +Key.__index = Key + +-- This is Key.new, but we don't want to expose that publicly. +local function newkey(pool: KeyPool, index) + local key = { + pool = pool, + index = index, + } + + setmetatable(key, Key) + return key +end + +-- KeyPool functions + +function KeyPool.new(class: string) + assert(t.string(class)) + + local pool = { + class = class, + available = {}, + limit = 0, + count = 0, + } + + setmetatable(pool, KeyPool) + return pool +end + +export type KeyPool = typeof(KeyPool.new("")) +export type Key = typeof(newkey(KeyPool.new(""), 1)) + +-- Get a currently unused key, or create a new one if everything is in use. +function KeyPool.get(self: KeyPool): Key + if self.count == 0 then + self.limit = self.limit + 1 + return newkey(self, self.limit) + end + + local key = self.available[self.count] + self.count = self.count - 1 + return key +end + +-- Key functions + +function Key.__tostring(self: Key) + return self.pool.class .. "_" .. string.format("%02d", self.index) +end + +-- Return this key to the pool it came from. Whatever previously held this key should not keep the reference after +-- calling this. +function Key.release(self: Key) + self.pool.count = self.pool.count + 1 + self.pool.available[self.pool.count] = self +end + +return KeyPool diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/Logger.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/Logger.lua new file mode 100644 index 0000000..725261e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/Logger.lua @@ -0,0 +1,4 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Lumberyak = require(Root.Lumberyak) + +return Lumberyak.Logger.new(nil, script:GetFullName()) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/NotifyReady.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/NotifyReady.lua new file mode 100644 index 0000000..a564707 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/NotifyReady.lua @@ -0,0 +1 @@ +return {} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/Orientation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/Orientation.lua new file mode 100644 index 0000000..a31399b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/Orientation.lua @@ -0,0 +1,31 @@ +-- Enum for specifying the leading edge of the scroller. +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local t = require(Root.t) + +local Orientation = { + Up = "Orientation.Up", + Down = "Orientation.Down", + Left = "Orientation.Left", + Right = "Orientation.Right", +} + +local metaindex = { + isOrientation = t.union( + t.literal(Orientation.Up), + t.literal(Orientation.Down), + t.literal(Orientation.Left), + t.literal(Orientation.Right) + ) +} + +setmetatable(Orientation, { + __index = function(self, key) + return metaindex[key] or + error(tostring(key) .. " is not a valid member of Scroller.Orientation", 2) + end, + __newindex = function() + error("Scroller.Orientation is read-only", 2) + end, +}) + +return Orientation diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/Round.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/Round.lua new file mode 100644 index 0000000..2755954 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/Round.lua @@ -0,0 +1,28 @@ +local epsilon = 1e-15 + +return { + nearest = function(num) + local q, r = math.modf(num) + if r <= -0.5 then + return q - 1 + elseif r >= 0.5 then + return q + 1 + else + return q + end + end, + towardsZero = function(num) + local result, _ = math.modf(num) + return result + end, + awayFromZero = function(num) + local q, r = math.modf(num) + if r < -epsilon then + return q - 1 + elseif r > epsilon then + return q + 1 + else + return q + end + end, +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/Scroller.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/Scroller.lua new file mode 100644 index 0000000..34a9e97 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/Scroller.lua @@ -0,0 +1,1634 @@ +local RunService = game:GetService("RunService") +local HttpService = game:GetService("HttpService") + +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Roact = require(Root.Roact) +local Cryo = require(Root.Cryo) +local t = require(Root.t) +local Otter = require(Root.Otter) + +local FitFrame = require(Root.FitFrame).FitFrameOnAxis +local findNewIndices = require(script.Parent.findNewIndices) +local relocateIndices = require(script.Parent.relocateIndices) +local Round = require(script.Parent.Round) +local Distance = require(script.Parent.Distance) +local KeyPool = require(script.Parent.KeyPool) + +local Logger = require(script.Parent.Logger) +local TimeLogger = require(script.Parent.TimeLogger) + +local NotifyReady = require(script.Parent.NotifyReady) + +local Scroller = Roact.PureComponent:extend("Scroller") + +Scroller.Orientation = require(script.Parent.Orientation) + +local MOTOR_OPTIONS = { + frequency = 4, + dampingRatio = 1, +} + +local REPORT_TRIMMING_ERROR_THRESHOLD = 3 + +local isVertical = { + [Scroller.Orientation.Up] = true, + [Scroller.Orientation.Down] = true, + [Scroller.Orientation.Left] = false, + [Scroller.Orientation.Right] = false, +} + +local isReverse = { + [Scroller.Orientation.Up] = true, + [Scroller.Orientation.Down] = false, + [Scroller.Orientation.Left] = true, + [Scroller.Orientation.Right] = false, +} + +local direction = { + [Scroller.Orientation.Up] = -1, + [Scroller.Orientation.Down] = 1, + [Scroller.Orientation.Left] = -1, + [Scroller.Orientation.Right] = 1, +} + +Scroller.validateProps = t.interface({ + -- Required. The list of items to scroll through. + itemList = t.array(t.any), + + -- Required. A callback function, called with each visible item in the itemList when the list is rendered. + renderItem = t.callback, + + -- A function to uniquely identify list items. Calling this on the same item twice should give the same result + -- accoring to ==. + identifier = t.optional(t.callback), + + -- One of the Scroller.Orientation enums. Determines the leading edge of the infinite scroll. + orientation = t.optional(Scroller.Orientation.isOrientation), + + -- A callback function, called when the infinite scroll reaches the leading end of the itemList (index + -- #itemList). + loadNext = t.optional(t.callback), + + -- A callback function, called when the infinite scroll reaches the trailing end of the itemList (index 1). + loadPrevious = t.optional(t.callback), + + -- Padding between elements in the scrolling frame. The Scale is relative to the size of the scrolling frame. + padding = t.optional(t.UDim), + + -- The minimum number of unmounted elements to keep at the top and bottom of the list. If there are fewer than + -- this call loadNext or loadPrevious. + loadingBuffer = t.optional(t.numberPositive), + + -- The amount of space above and below the view to render items in. + mountingBuffer = t.optional(t.numberPositive), + + -- The amount of empty space past the leading element of the list. The Scale is relative to the size of the scrolling frame. + leadBuffer = t.optional(t.UDim), + + -- The amount of empty space past the trailing element of the list. The Scale is relative to the size of the scrolling frame. + trailBuffer = t.optional(t.UDim), + + -- An initial guess at the average size of an item. + estimatedItemSize = t.optional(t.numberPositive), + + -- The maximum distance to search for moved elements. + maximumSearchDistance = t.optional(t.numberPositive), + + -- The element to put in focus initially. + focusIndex = t.optional(t.integer), + + -- An arbitrary value to prevent the list from refocusing every render. Change this to cause the list to reset + -- and refocus on the new focusIndex. + focusLock = t.optional(t.any), + + -- The position within the view to keep still as other things move. The Scale is relative to the size of the + -- scrolling frame. + anchorLocation = t.optional(t.UDim), + + -- Animate the scrolling + animateScrolling = t.optional(t.boolean), + + --Animation options + animateOptions = t.optional(t.table), + + -- Properties that should trigger rerenders of the children elements even though the scroller itself does not + -- use them. + extraProps = t.optional(t.table), + + -- A callback function that will update the index change + onScrollUpdate = t.optional(t.callback), + + -- Which components to disable instance recycling for. + recyclingDisabledFor = t.optional(t.array(t.string)), + + ---- INTERNAL ONLY ---- + [NotifyReady] = t.any, +}) + +-- Default values for all the infinite-scroller-specific props. Any prop not in this list will be passed on to the +-- underlying ScrollingFrame. +Scroller.defaultProps = { + itemList = {}, + renderItem = {}, + identifier = function(item) + return item + end, + orientation = Scroller.Orientation.Down, + loadNext = function() end, + loadPrevious = function() end, + padding = UDim.new(), + loadingBuffer = 10, + mountingBuffer = 200, + leadBuffer = UDim.new(), + trailBuffer = UDim.new(), + estimatedItemSize = 50, + maximumSearchDistance = 100, + focusIndex = 1, + focusLock = {}, + anchorLocation = UDim.new(0, 0), + animateScrolling = false, + animateOptions = MOTOR_OPTIONS, + extraProps = {}, + onScrollUpdate = function() end, + recyclingDisabledFor = {}, + [NotifyReady] = false, +} + +function Scroller:render() + self.log:debug("render") + + -- Gather vertical/horizontal specific variables. + local axis = isVertical[self.props.orientation] and { + fillDirection = Enum.FillDirection.Vertical, + scrollDirection = Enum.ScrollingDirection.Y, + fitDirection = FitFrame.Axis.Vertical, + minimumSize = UDim2.new(1, 0, 0, 0), + canvasSize = UDim2.new(0, 0, 0, self.state.size), + paddingSize = UDim2.new(0, 0, 0, self.state.padding), + } or { + fillDirection = Enum.FillDirection.Horizontal, + scrollDirection = Enum.ScrollingDirection.X, + fitDirection = FitFrame.Axis.Horizontal, + minimumSize = UDim2.new(0, 0, 1, 0), + canvasSize = UDim2.new(0, self.state.size, 0, 0), + paddingSize = UDim2.new(0, self.state.padding, 0, 0), + } + + -- Remove non-standard props from list to pass on to ScrollingFrame. These are the same props given in + -- defaultProps. + local props = Cryo.Dictionary.join( + self.props, + self.propsToClear, + { + CanvasSize = axis.canvasSize, + ScrollingDirection = axis.scrollDirection, + [Roact.Change.CanvasPosition] = self.onScroll, + [Roact.Change.AbsoluteSize] = self.onResize, + [Roact.Ref] = self:getRef(), + } + ) + + local children = { + layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = axis.fillDirection, + Padding = UDim.new(0, self.itemPadding), + [Roact.Change.AbsoluteContentSize] = self.onContentResize, + }), + padding = Roact.createElement("Frame", { + Size = axis.paddingSize, + LayoutOrder = -1 - (self.state.listSize or 0), + BackgroundTransparency = 1, + }), + } + + -- Trailing and leading indicies won't be set if this isn't true. + if self.state.ready and not Cryo.isEmpty(self.props.itemList) then + self.log:trace(" Rendering elements between {} and {}", self.state.trail.index, self.state.lead.index) + for n = self.state.trail.index, self.state.lead.index do + local metadata = self:getMetadata(n) + children[metadata.name] = Roact.createElement(FitFrame, { + minimumSize = axis.minimumSize, + axis = axis.fitDirection, + FillDirection = axis.fillDirection, + BackgroundTransparency = 1, + LayoutOrder = isReverse[self.props.orientation] and -n or n, + [Roact.Ref] = metadata.ref, + }, { + item = self.props.renderItem(self.props.itemList[n], false), + }) + end + end + + return Roact.createElement("ScrollingFrame", props, children) +end + +function Scroller:shouldUpdate(nextProps, nextState) + if not self.alive then + return false + end + + self.log:trace("shouldUpdate") + + if self.props["Size"] ~= nextProps["Size"] then + self.log:trace(" Size Prop Changed") + self._sizePropChanged = true + end + + -- Check for state and props changes in the same way PureComponent would, + -- but go one more level down for extraProps. + if nextState ~= self.state then + self.log:trace(" State changed") + return true + end + + for key, value in pairs(nextProps) do + if self.props[key] ~= value then + if key ~= "extraProps" then + self.log:trace(" Prop changed: {}", key) + return true + end + + for extraKey, extraValue in pairs(value) do + if self.props.extraProps[extraKey] ~= extraValue then + self.log:trace(" Extra prop changed: {}", extraKey) + return true + end + end + end + end + + for key, value in pairs(self.props) do + if nextProps[key] ~= value then + if key ~= "extraProps" then + self.log:trace(" Prop changed: {}", key) + return true + end + + for extraKey, extraValue in pairs(value) do + if nextProps.extraProps[extraKey] ~= extraValue then + self.log:trace(" Extra prop changed: {}", extraKey) + return true + end + end + end + end + + return false +end + +function Scroller:willUpdate(nextProps, nextState) + if not self.alive then + return + end + + self.log:debug("willUpdate") + + if not nextState.ready then + return + end + + if self.updating then + self.timeLog:info("Middle of update") + else + self.timeLog:info("Start of update") + + -- Reset error counters + self.numberOfTopTrims = 0 + self.numberOfBottomTrims = 0 + self.lastTrimTop = 0 + self.lastTrimBottom = 0 + end + self.updating = true + + self.sizeDebounce = true + + local deletions = {} + local additions = {} + + if not Cryo.isEmpty(self.props.itemList) and self.state.lead then + for n = self.state.trail.index, self.state.lead.index do + local id = self.props.identifier(self.props.itemList[n]) + deletions[id] = true + end + end + + if not Cryo.isEmpty(nextProps.itemList) and nextState.lead then + for n = nextState.trail.index, nextState.lead.index do + local item = nextProps.itemList[n] + local id = nextProps.identifier(item) + if deletions[id] then + -- Element is in both ranges. + deletions[id] = nil + else + additions[id] = item + end + end + end + + -- Clear names first, so new items can use them. + for id, _ in pairs(deletions) do + self:clearMetadata(id) + end + for id, item in pairs(additions) do + self:updateMetadata(id, item, nextProps) + end + + -- The focus lock changed, clear the non-state anchor variables. + if self.state.lastFocusLock ~= nextState.lastFocusLock then + self.scrollDebounce = true + self.motorActive = false + self.anchorFramePosition = 0 + if self.state.listSize and self.state.listSize > nextState.listSize then + -- Perform a full reset when the list decreases in size to ensure the canvas + -- is sized properly + self.anchorCanvasPosition = self.relativeAnchorLocation + else + -- The canvas hasn't necessarily been reset in size here, so we need to convert the frame + -- coordinates of relativeAnchorLocation to canvas coordinates. + self.anchorCanvasPosition = self:frameToCanvasPosition(self.relativeAnchorLocation) + end + end +end + +function Scroller:didUpdate(previousProps, previousState) + if not self.alive then + return + end + + self.log:debug("didUpdate") + + if Cryo.isEmpty(self.props.itemList) then + return + end + + if not self.state.ready then + self.onResize(self:getRef().current) + return + end + + if self.props.focusIndex ~= previousProps.focusIndex and + self.props.focusLock ~= previousProps.focusLock then + + self.indexChanged = { + oldIndex = previousProps.focusIndex, + newIndex = self.props.focusIndex, + lastFocusLock = self.props.focusLock, + } + self.motorActive = false + + self.log:trace("self.props.focusIndex {}", self.props.focusIndex) + self.log:trace("self.state.anchor.index {}", self.state.anchor.index) + self.log:trace("previousState.anchor.index {}", previousState.anchor.index) + end + + local adjustedCanvas = self:adjustCanvas(self.scrollingForward, self.scrollingBackward) + if not adjustedCanvas then + if self.indexChanged and self.props.animateScrolling then + self:scrollToAnchor() + else + self:moveToAnchor() + end + + if self.anchorOffset ~= 0 then + self:setState({}) + return + end + + -- The canvas has finished adjusting itself after a resize + self.resized = false + + self:loadMore() + self.sizeDebounce = false + + if self.updating then + self.timeLog:info("End of update") + end + self.updating = false + + --Return the updated index + if self.props.onScrollUpdate then + self.props.onScrollUpdate({ + leadIndex = self.state.lead.index, + anchorIndex = self.state.anchor.index, + trailIndex = self.state.trail.index, + animationActive = self.motorActive, + }) + end + end + + self._sizePropChanged = false + + self.prevCycle = { + frameSize = self:measure(self:getCurrent().AbsoluteSize), + canvasSize = self.state.size, + canvasPosition = Round.nearest(self:measure(self:getCurrent().CanvasPosition)), + relativeAnchorLocation = self.relativeAnchorLocation, + } +end + +function Scroller.getDerivedStateFromProps(nextProps, lastState) + -- No self in static functions + Logger:trace("getDerivedStateFromProps") + + if not lastState.ready or Cryo.isEmpty(nextProps.itemList) then + return nil + end + + local listSize = #nextProps.itemList + local lastFocusLock = nil + + -- Reset the state if the focus lock changes. This is guaranteed to be true the first time. + if lastState.lastFocusLock ~= nextProps.focusLock then + Logger:trace(" Resetting focus lock {} to {}", lastState.lastFocusLock, nextProps.focusLock) + if nextProps.animateScrolling and lastState.lastFocusLock ~= nil then + lastFocusLock = nextProps.focusLock + else + local focusID = nextProps.identifier(nextProps.itemList[nextProps.focusIndex]) + return { + listSize = listSize, + trail = {index=nextProps.focusIndex, id=focusID}, + anchor = {index=nextProps.focusIndex, id=focusID}, + lead = {index=nextProps.focusIndex, id=focusID}, + padding = 0, + size = 0, + lastFocusLock = nextProps.focusLock, + } + end + end + + local trailIndex, anchorIndex, leadIndex = findNewIndices(nextProps, lastState) + Logger:trace(" Trailing index moved from {} to {}", lastState.trail.index, trailIndex) + Logger:trace(" Anchor index moved from {} to {}", lastState.anchor.index, anchorIndex) + Logger:trace(" Leading index moved from {} to {}", lastState.lead.index, leadIndex) + + -- Nothing changed. Return early to avoid triggering an update. + if anchorIndex and lastState.anchor.index == anchorIndex + and trailIndex and lastState.trail.index == trailIndex + and leadIndex and lastState.lead.index == leadIndex then + Logger:trace(" No change, returning early") + if listSize == lastState.listSize then + if lastFocusLock then + return { + lastFocusLock = lastFocusLock, + } + else + return nil + end + else + return { + listSize = listSize, + lastFocusLock = lastFocusLock, + } + end + end + + -- TODO #51 findNewIndices, state and this should all agree on a format + local newIndices = relocateIndices( + {trailIndex=trailIndex, anchorIndex=anchorIndex, leadIndex=leadIndex}, + {trailIndex=lastState.trail.index, anchorIndex=lastState.anchor.index, leadIndex=lastState.lead.index}, + listSize + ) + + Logger:trace(" Anchor index moved to {}", newIndices.anchorIndex) + Logger:trace(" Trailing index moved to {}", newIndices.trailIndex) + Logger:trace(" Leading index moved to {}", newIndices.leadIndex) + + local trailID = nextProps.identifier(nextProps.itemList[newIndices.trailIndex]) + local anchorID = nextProps.identifier(nextProps.itemList[newIndices.anchorIndex]) + local leadID = nextProps.identifier(nextProps.itemList[newIndices.leadIndex]) + + return { + listSize = listSize, + trail = {index=newIndices.trailIndex, id=trailID}, + anchor = {index=newIndices.anchorIndex, id=anchorID}, + lead = {index=newIndices.leadIndex, id=leadID}, + lastFocusLock = lastFocusLock, + } +end + +function Scroller:init() + self.guid = HttpService:GenerateGUID() + self.log = Logger:new(script:GetFullName() .. "." .. self.guid) + self.timeLog = TimeLogger:new(script:GetFullName() .. "." .. self.guid) + + self.log:debug("init") + + -- Only self:getRef() should access this. + self._ref = Roact.createRef() + + self.motorPrevValue = 0 + + self.motorOnStep = function(value) + self.log:trace("onStep {}", value) + if not self.motorActive or self.indexChanged == nil then + self.motor:stop() + return + end + local currentValue = self.indexChanged.currentPos + if currentValue == nil then + self.motor:stop() + return + end + local diff = value - self.motorPrevValue + if self:getCurrent() then + self:scrollRelative(diff) + self.motorPrevValue = value + end + end + + self.motorOnComplete = function() + self.log:trace("otter onComplete") + self.motorActive = false + --Return the updated index + if self.props.onScrollUpdate then + self.props.onScrollUpdate({ + leadIndex = self.props.focusIndex, + anchorIndex = self.props.focusIndex, + trailIndex = self.props.focusIndex, + animationActive = self.motorActive, + }) + end + self.motorPrevValue = 0 + self.indexChanged = nil + if self.motor then + self.motor:destroy() + end + end + + self.motorActive = false + self.springLock = 0 + self.scrollDebounce = false + self.sizeDebounce = true + + --Used to track index changes + self.indexChanged = nil + + self.onScroll = function(rbx) + self.log:trace("onScroll") + if not self.alive or self.resized then + return + end + + self.log:trace(" CanvasPosition is {}", rbx.CanvasPosition) + if self.scrollDebounce then + self.log:trace(" Debouncing scroll") + return + end + + local delta, newState = self:recalculateAnchor() + self.scrollingBackward = delta < 0 + self.scrollingForward = delta > 0 + self.log:trace(" Delta is {}", delta) + + newState = Cryo.Dictionary.join( + newState, + self:resetAnchorPosition(newState), + self:recalculateBounds(self.scrollingForward, self.scrollingBackward, newState) + ) + + if not Cryo.isEmpty(newState) then + self:setState(newState) + end + + -- Handle any passed in scroll callback. + if self.props[Roact.Change.CanvasPosition] then + self.props[Roact.Change.CanvasPosition](rbx) + end + + self.prevCycle.canvasPosition = Round.nearest(self:measure(rbx.CanvasPosition)) + end + + self.onResize = function(rbx) + self.log:trace("onResize") + if not self.alive then + return + end + + local size = self:measure(rbx.AbsoluteSize) + local pos = self:measure(rbx.AbsolutePosition) + + self.leadBufferPx = Round.nearest( + self.props.leadBuffer.Scale * size + self.props.leadBuffer.Offset) + + self.trailBufferPx = Round.nearest( + self.props.trailBuffer.Scale * size + self.props.trailBuffer.Offset) + + self.itemPadding = self.props.padding.Scale * size + self.props.padding.Offset + if isReverse[self.props.orientation] then + self.relativeAnchorLocation = Round.nearest( + self.props.anchorLocation.Scale * size + self.props.anchorLocation.Offset) + else + self.relativeAnchorLocation = Round.nearest( + (1 - self.props.anchorLocation.Scale) * size - self.props.anchorLocation.Offset) + end + self.absoluteAnchorLocation = self.relativeAnchorLocation + pos + self.mountAboveAnchor = self.relativeAnchorLocation + self.props.mountingBuffer + self.mountBelowAnchor = size - self.relativeAnchorLocation + self.props.mountingBuffer + self.resized = true + self.increasedSize = self.state.size < size + + -- Handle any passed in resize callback. + if self.props[Roact.Change.AbsoluteSize] then + self.props[Roact.Change.AbsoluteSize](rbx) + end + + if not self.state.ready then + self.log:trace(" Setting initial anchor position to {}", self.relativeAnchorLocation) + -- When setting this for the first time, set the frame position of the current anchor to 0, + -- and its canvas position to equal where it should be in the frame. When the scroller goes + -- to correct this, the anchor will end up in the right place with the right padding around it. + self.anchorFramePosition = 0 + self.anchorCanvasPosition = self.relativeAnchorLocation + + coroutine.wrap(function() + RunService.Heartbeat:Wait() + if not self.state.ready and self.alive then + self:setState({ + ready = true + }) + + -- This should only be set by tests. + if self.props[NotifyReady] then + self.props[NotifyReady]:Fire() + end + end + end)() + else + self:handleResize(size) + end + end + + self.onContentResize = function() + self.log:trace("onContentResize") + + if not self.alive or self.sizeDebounce or not self.state.ready or self:isScrollingWithElasticBehavior() then + self.log:trace(" Skipping onContentResize") + return + end + self:setState({}) -- Force a rerender. + end + + self.anchorCanvasPosition = 0 + self.anchorFramePosition = 0 + self.anchorOffset = 0 + + self.metadata = {} + self.pools = {} + self.refpool = {} + + self.scrollingBackward = false + self.scrollingForward = false + + self.lastLoadPrevItems = nil + self.lastLoadNextItems = nil + + self.alive = true + self.updating = false + + -- Store the list of props to not pass on to the underlying scrolling frame. + self.propsToClear = {} + for k, _ in pairs(Scroller.defaultProps) do + self.propsToClear[k] = Cryo.None + end + + -- This will get updated shortly, but one render will happen before state.ready is set + self:setState({ + ready = false, + lastFocusLock = nil, + padding = 0, + size = 0, + }) +end + +function Scroller:willUnmount() + if self.motor then + self.motor:destroy() + end + + self.alive = false +end + +-- Find which element is currently closest to the anchor position. +function Scroller:recalculateAnchor() + self.log:trace("recalculateAnchor") + + -- Find the index of the element at the appropriate position + local index = self:findIndexAt( + self:absoluteToCanvasPosition(self.absoluteAnchorLocation), self.state.anchor.index, false) + + self.anchorCanvasPosition = self:getAnchorCanvasFromIndex(index) + self.anchorFramePosition = self:getAnchorFrameFromIndex(index) + + local delta + if index == self.state.anchor.index then + self.log:trace(" Current anchor still works") + return 0, {} + elseif index < self.state.anchor.index then + delta = -1 + else + delta = 1 + end + + self.log:trace(" New anchor at index {}", index) + + -- Store the new anchor's details + self.log:trace(" New anchor at canvas position {}", self.anchorCanvasPosition) + self.log:trace(" New anchor at frame position {}", self.anchorFramePosition) + return delta, { + anchor = {index=index, id=self:getID(index)}, + } +end + +-- Move all the rendered elements up or down to put the anchor back where it was. +function Scroller:resetAnchorPosition(newState) + local anchor = newState and newState.anchor or self.state.anchor + local padding = newState and newState.padding or self.state.padding + + self.log:trace("resetAnchorPosition") + self.log:trace(" Anchor index is {}", anchor.index) + local offset = self:getAnchorCanvasFromIndex(anchor.index) + self.log:trace(" Anchor is at {}", offset) + self.log:trace(" Anchor offset is {}", self.anchorOffset) + local newPos = self.anchorCanvasPosition - self.anchorOffset + self.log:trace(" Anchor should be at {}", newPos) + local diff = Round.nearest(newPos - offset) + if diff ~= 0 then + self.log:trace(" Changing padding from {} to {}", padding, padding + diff) + self.log:trace(" Changing anchorCanvasPosition from {} to {}", self.anchorCanvasPosition, Round.nearest(self.anchorCanvasPosition - self.anchorOffset)) + self.anchorCanvasPosition = Round.nearest(self.anchorCanvasPosition - self.anchorOffset) + self.anchorOffset = 0 + return {padding = padding + diff} + else + return {} + end +end + +-- Get the current padding from the UIPadding child. +function Scroller:getCurrentPadding() + local pad = self:getCurrent().padding + -- Only one of these will be non-zero + return pad.Size.X.Offset + pad.Size.Y.Offset +end + +-- Move the top and bottom of the range to be rendered up and down to make sure +-- enough things are being rendered. +function Scroller:recalculateBounds(trimTrailing, trimLeading, newState) + local lead = newState and newState.lead or self.state.lead + local trail = newState and newState.trail or self.state.trail + local anchor = newState and newState.anchor or self.state.anchor + + self.log:trace("recalculateBounds") + self.log:trace(" Leading index was {}", lead.index) + self.log:trace(" Trailing index was {}", trail.index) + + local anchorPos = self:getAnchorCanvasFromIndex(anchor.index) + local mountTop = anchorPos - self.mountAboveAnchor + local mountBottom = anchorPos + self.mountBelowAnchor + self.log:trace(" Target for top at {}", mountTop) + self.log:trace(" Target for bottom at {}", mountBottom) + + local topIndex = self:findIndexAt(mountTop, anchor.index, true) + self.log:trace(" Found new top index at {}", topIndex) + local bottomIndex = self:findIndexAt(mountBottom, anchor.index, true) + self.log:trace(" Found new bottom index at {}", bottomIndex) + + local leadIndex = math.max(topIndex, bottomIndex) + if leadIndex < lead.index and not trimLeading then + leadIndex = lead.index + end + + local trailIndex = math.min(topIndex, bottomIndex) + if trailIndex > trail.index and not trimTrailing then + trailIndex = trail.index + end + + if trailIndex < trail.index or leadIndex > lead.index then + self.log:trace(" Changing leading index to {}", leadIndex) + self.log:trace(" Changing trailing index to {}", trailIndex) + return { + trail = {index=trailIndex, id=self:getID(trailIndex)}, + lead = {index=leadIndex, id=self:getID(leadIndex)}, + } + else + return {} + end +end + +-- Find the index of the element that overlaps the given canvas-relative position. +function Scroller:findIndexAt(targetPos, hintIndex, extrapolate) + self.log:trace(" findIndexAt") + -- Get the distance from the hinted index or the anchor. + local currentIndex = hintIndex or self.state.anchor.index + local currentDist = self:distanceToPosition(currentIndex, targetPos) + self.log:trace(" Searching from index {}", currentIndex) + self.log:trace(" Position is {} from {}", currentDist, targetPos) + if currentDist == 0 then + -- resolving border disputes + local nextIndex = currentIndex + Round.awayFromZero(self.props.anchorLocation.Scale - 0.5) + if nextIndex > self.state.listSize or nextIndex < 1 then + return currentIndex + end + + local nextDist = self:distanceToPosition(nextIndex, targetPos) + if nextDist == 0 then + return nextIndex + end + return currentIndex + end + + -- Get the distance from one end of the list. + local nextIndex = (currentDist < 0) and self.state.trail.index or self.state.lead.index + self.log:trace(" Nearest end at {}", nextIndex) + if currentIndex == nextIndex then + self.log:trace(" Hint index already at end") + -- If the target position lies outside of the loaded elements. + if currentIndex + currentDist < self.state.trail.index + or currentIndex + currentDist > self.state.lead.index then + self.log:trace(" Target out of bounds") + if not extrapolate then + -- Do not extrapolate. Return the closest loaded element. + return currentIndex + end + + -- Extrapolate using the estimated item size. + local delta = Round.awayFromZero(currentDist / self.props.estimatedItemSize) + self.log:trace(" Estimating target at {} from end", delta) + return math.min(math.max(currentIndex + delta, 1), self.state.listSize) + end + else + local nextDist = self:distanceToPosition(nextIndex, targetPos) + self.log:trace(" End is {} from target", nextDist) + if nextDist == 0 then + return nextIndex + end + + -- If the target position lies outside of the loaded elements. + if currentDist * nextDist > 0 then + self.log:trace(" Target out of bounds") + if not extrapolate then + -- Do not extrapolate. Return the closest loaded element. + return nextIndex + end + + -- Extrapolate using the estimated item size. + local delta = Round.awayFromZero(nextDist / self.props.estimatedItemSize) + self.log:trace(" Estimating target at {} from end", delta) + return math.min(math.max(nextIndex + delta, 1), self.state.listSize) + end + + -- Jump to the approximate location of the target based on the distance from current and next. + local totalDist = math.abs(currentDist) + math.abs(nextDist) + local indexCount = math.abs(currentIndex - nextIndex) + currentIndex = currentIndex + Round.nearest(indexCount * currentDist / totalDist) + currentDist = self:distanceToPosition(currentIndex, targetPos) + self.log:trace(" Interpolated index is {}", currentIndex) + self.log:trace(" Distance from interpolated index is {}", currentDist) + end + + -- Linear search from best guess index. + while currentDist ~= 0 do + if currentDist < 0 then + currentIndex = currentIndex - 1 + else + currentIndex = currentIndex + 1 + end + currentDist = self:distanceToPosition(currentIndex, targetPos) + self.log:trace(" Distance after step is {}", currentDist) + end + + return currentIndex +end + +function Scroller:findIndexWithRemainder(targetPos, hintIndex, extrapolate) + local reverse = isReverse[self.props.orientation] + local topBufferPx = reverse and self.leadBufferPx or self.trailBufferPx + + if targetPos < topBufferPx then + local topIndex = reverse and self.state.listSize or 1 + return topIndex, 0 + elseif targetPos > Round.nearest(self:measure(self:getCurrent().CanvasSize).Offset) then + local bottomIndex = reverse and 1 or self.state.listSize + return bottomIndex, 0 + end + + local currentIndex = self:findIndexAt(targetPos, hintIndex, extrapolate) + local actualIndexPosition = self:getAnchorCanvasFromIndex(currentIndex) + local nextIndex = currentIndex + Round.awayFromZero(self.props.anchorLocation.Scale - 0.5) + + if nextIndex > self.state.listSize or nextIndex < 1 then + return currentIndex, 0 + end + + local nextDist = self:distanceToPosition(nextIndex, targetPos) + if nextDist == 0 then + return nextIndex, 0 + end + + return currentIndex, actualIndexPosition - targetPos +end + +-- Check if trimTop is trimming the same amount multiple times. +-- If it is, there is an issue with one of the buffers which will be reported to the user. +function Scroller:checkTrimTopError(topDiff) + if self.lastTrimTop == topDiff then + if self.numberOfTopTrims >= REPORT_TRIMMING_ERROR_THRESHOLD then + error("There was an error laying out the items. Check to see if you provided enough leadBuffer and/or trailBuffer") + else + self.numberOfTopTrims = self.numberOfTopTrims + 1 + end + else + self.lastTrimTop = topDiff + self.numberOfTopTrims = 0 + end +end + +function Scroller:trimTopHelper(reverse, size, padding, anchor, topIndex, topDiff) + local topBufferPx = reverse and self.leadBufferPx or self.trailBufferPx + -- Ensure this trim does not set the padding to less than the topBufferPx + if padding + topDiff < topBufferPx then + topDiff = padding - topBufferPx + end + + -- Make sure the topDiff will not expand canvas and that + -- it will not trim to a negative value + if topDiff >= 0 or size + topDiff < 0 then + return {} + end + + self:checkTrimTopError(topDiff) + + local trimmedCanvasSize = Round.nearest(size + topDiff) + local trimmedPadding = Round.nearest(padding + topDiff) + local newAnchorCanvasPosition = Round.nearest(self.anchorCanvasPosition + topDiff) + self.log:trace(" Changing anchor canvas position from {} to {}", self.anchorCanvasPosition, newAnchorCanvasPosition) + self.anchorCanvasPosition = newAnchorCanvasPosition + + -- Change the anchor when there is empty space at the top of the list and the anchor position has + -- been shifted past the anchor element + local topFramePos = self:getChildFramePosition(topIndex) + local nextAnchorIndex = anchor.index - 1 + if nextAnchorIndex > 0 and topFramePos > 0 then + local nextAnchorCanvasPosition = self:getChildCanvasPosition(nextAnchorIndex) + if nextAnchorCanvasPosition > self.anchorCanvasPosition then + local nextAnchor = {index = nextAnchorIndex, id=self:getID(nextAnchorIndex)} + + return { + size = trimmedCanvasSize, + padding = trimmedPadding, + anchor = nextAnchor, + } + end + end + + return { + size = trimmedCanvasSize, + padding = trimmedPadding, + } +end + +-- Shrink the canvas and padding so that the top of the list plus the buffer +-- is at the top of the canvas +function Scroller:trimTop(reverse, size, padding, anchor, topIndex, topCanvasPos) + local atTop = (reverse and topIndex == self.state.listSize) or (not reverse and topIndex == 1) + local bufferSize = reverse and self.leadBufferPx or self.trailBufferPx + local topDiff = atTop and bufferSize - topCanvasPos or 0 + + return self:trimTopHelper(reverse, size, padding, anchor, topIndex, topDiff) +end + +-- Shrink the canvas and padding so that the top of the canvas is at the top of the frame +function Scroller:shortTrimTop(reverse, size, frameSize, padding, anchor, topIndex) + local topFramePos = self:getChildFramePosition(topIndex) + local bottomBufferPx = reverse and self.trailBufferPx or self.leadBufferPx + + -- Only trim to the top of the frame + local topDiff + if topFramePos >= 0 and topFramePos <= frameSize then + topDiff = -Round.towardsZero(size - bottomBufferPx - frameSize) + else + topDiff = topFramePos + end + return self:trimTopHelper(reverse, size, padding, anchor, topIndex, topDiff) +end + +-- Check if trimBottom is trimming the same amount multiple times. +-- If it is, there is an issue with one of the buffers which will be reported to the user. +function Scroller:checkTrimBottomError(bottomDiff) + if self.lastTrimBottom == bottomDiff then + if self.numberOfBottomTrims >= REPORT_TRIMMING_ERROR_THRESHOLD then + error("There was an error laying out the items. Check to see if you provided enough leadBuffer and/or trailBuffer") + else + self.numberOfBottomTrims = self.numberOfBottomTrims + 1 + end + else + self.lastTrimBottom = bottomDiff + self.numberOfBottomTrims = 0 + end +end + +function Scroller:trimBottomHelper(size, bottomDiff) + -- Make sure the bottomDiff will not expand canvas + if bottomDiff <= 0 then + return {} + end + + self:checkTrimBottomError(bottomDiff) + + local trimmedCanvasSize = Round.nearest(size - bottomDiff) + + return { + size = trimmedCanvasSize, + } +end + +-- Shrink the canvas so that the bottom of the list plus the buffer +-- is at the bottom of the canvas +function Scroller:trimBottom(reverse, bottomIndex, bottomCanvasPos, size) + local atBottom = (reverse and bottomIndex == 1) or (not reverse and bottomIndex == self.state.listSize) + local bufferSize = reverse and self.trailBufferPx or self.leadBufferPx + local bottomDiff = atBottom and (size - bufferSize) - bottomCanvasPos or 0 + + -- When resizing to a larger frame size extra padding needs to be added before trimming can happen + if self.resized and self.increasedSize then + return {} + end + return self:trimBottomHelper(size, bottomDiff) +end + +-- Shrink the canvas so that the bottom of the canvas is at the bottom of the frame +function Scroller:shortTrimBottom(reverse, bottomIndex, bottomCanvasPos, size, frameSize) + local bottomFramePos = self:getChildFramePosition(bottomIndex) + local bottomSize = self:getChildSize(bottomIndex) + bottomFramePos = bottomFramePos + bottomSize + + if bottomFramePos >= 0 and bottomFramePos <= frameSize then + return {} + end + + local bufferSize = reverse and self.trailBufferPx or self.leadBufferPx + -- Only trim to the bottom of the frame + the buffer at the end of the list + local bottomDiff = (size - bufferSize) - bottomCanvasPos + + return self:trimBottomHelper(size, bottomDiff) +end + +-- When the ends of the list are rendered, adjust the padding and canvas size so that the ends of +-- the canvas do not extend past the items in the list + the top and bottom buffers. +function Scroller:adjustEdges(newState) + self.log:trace("adjustEdges") + + local size = newState.size or self.state.size + size = Round.nearest(size) + local padding = newState.padding or self.state.padding + local anchor = newState.anchor or self.state.anchor + local reverse = isReverse[self.props.orientation] + + local frameSize = self:measure(self:getCurrent().AbsoluteSize) + local topIndex = reverse and self.state.lead.index or self.state.trail.index + local topCanvasPos = Round.nearest(self:getChildCanvasPosition(topIndex)) - self.itemPadding + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + local childSize = self:getChildSize(bottomIndex) + local bottomCanvasPos = Round.nearest(self:getChildCanvasPosition(bottomIndex) + childSize) + + local totalSize = self.leadBufferPx + self.trailBufferPx + (bottomCanvasPos - topCanvasPos) + + local combinedState + if bottomCanvasPos - topCanvasPos <= frameSize or totalSize <= frameSize then + -- For lists that are less than the size of the scrolling frame, trim the trailing edge followed by the leading edge. + if reverse then + local trimBottomReturnState = self:trimBottom(reverse, bottomIndex, bottomCanvasPos, size) + local newCanvasSize = trimBottomReturnState.size or size + local trimTopReturnState = self:shortTrimTop(reverse, newCanvasSize, frameSize, padding, anchor, topIndex) + combinedState = Cryo.Dictionary.join(trimBottomReturnState, trimTopReturnState) + else + local trimTopReturnState = self:trimTop(reverse, size, padding, anchor, topIndex, topCanvasPos) + local newCanvasSize = trimTopReturnState.size or size + local trimBottomReturnState = self:shortTrimBottom(reverse, bottomIndex, bottomCanvasPos, newCanvasSize, frameSize) + combinedState = Cryo.Dictionary.join(trimTopReturnState, trimBottomReturnState) + end + else + -- For lists larger than the scrolling frame the order of trimming does not matter. + local trimTopReturnState = self:trimTop(reverse, size, padding, anchor, topIndex, topCanvasPos) + local newCanvasSize = trimTopReturnState.size or size + local trimBottomReturnState = self:trimBottom(reverse, bottomIndex, bottomCanvasPos, newCanvasSize) + combinedState = Cryo.Dictionary.join(trimTopReturnState, trimBottomReturnState) + end + + if combinedState.size then + self.log:trace(" Changing canvas size from {} to {}", size, combinedState.size) + end + if combinedState.padding then + self.log:trace(" Changing canvas padding from {} to {}", padding, combinedState.padding) + end + if combinedState.anchor then + self.log:trace(" Changing anchor index from {} to {}", anchor.index, combinedState.anchor.index) + end + + return combinedState +end + +-- Expand the size of the scrolling frame's canvas to make sure everything still fits. +function Scroller:expandCanvas(newState) + self.log:trace("expandCanvas") + local reverse = isReverse[self.props.orientation] + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + local topBufferPx = reverse and self.leadBufferPx or self.trailBufferPx + local bottomBufferPx = reverse and self.trailBufferPx or self.leadBufferPx + + local size = newState.size or self.state.size + size = Round.nearest(size) + local originalSize = size + local newPadding = newState.padding or self.state.padding + local originalPadding = newPadding + local oldPadding = self:getCurrentPadding() + + local bottomPos = self:getChildCanvasPosition(bottomIndex) + + self:getChildSize(bottomIndex) - (oldPadding - newPadding) + + local bottomTarget = Round.nearest(bottomPos + bottomBufferPx) + + self.log:trace(" Padding is {}", newPadding) + self.log:trace(" Padding should be at least {}", topBufferPx) + if newPadding < topBufferPx then + -- Minus header + local diff = newPadding - topBufferPx + size = Round.nearest(size - diff) + self.anchorCanvasPosition = Round.nearest(self.anchorCanvasPosition - diff) + newPadding = topBufferPx + self.log:trace(" Expanding canvas top to size {}", size) + self.log:trace(" Shifting anchor to {}", self.anchorCanvasPosition) + self.log:trace(" Padding is now {}", newPadding) + end + + self.log:trace(" Bottom of bottom child is {}", bottomPos) + self.log:trace(" Canvas size is {}", originalSize) + self.log:trace(" Canvas bottom should be {}", bottomTarget) + + local minSize = self:measure(self:getCurrent().AbsoluteSize) - math.max(0, newPadding) + if originalSize < minSize then + size = minSize + self.log:trace(" Expanding canvas to minimum size {}", size) + end + + if originalSize < bottomTarget then + -- Plus footer + size = math.max(bottomTarget, size) + self.log:trace(" Expanding canvas bottom to size {}", size) + end + + if size ~= originalSize or newPadding ~= originalPadding then + self.log:trace(" Changing size from {} to {}", originalSize, size) + self.log:trace(" Changing padding from {} to {}", originalPadding, newPadding) + return { + size = size, + padding = newPadding, + } + else + self.log:trace(" No changes to size or padding") + return {} + end +end + +-- Try and get the canvas as close to correct as possible this rendering pass. +function Scroller:adjustCanvas(trimTrailing, trimLeading) + self.log:trace("adjustCanvas") + + -- if our Size prop changes, do NOT adjust, onResize() will handle this + if self._sizePropChanged then + self.log:trace(" Skipping because Size prop changed") + return true + end + + local newState = Cryo.Dictionary.join( + self:resetAnchorPosition(), + self:recalculateBounds(trimTrailing, trimLeading) + ) + + if not newState.trail and not newState.lead then + newState = Cryo.Dictionary.join(newState, self:expandCanvas(newState)) + end + + if not newState.size and not newState.padding then + newState = Cryo.Dictionary.join(newState, self:adjustEdges(newState)) + end + + if Cryo.isEmpty(newState) then + self.log:trace(" No state changes after adjustment") + return false + end + + self:setState(newState) + return true +end + +function Scroller:handleResize(size) + local reverse = isReverse[self.props.orientation] + local canvasSize = Round.nearest(self:measure(self:getCurrent().CanvasSize).Offset) + local frameSize = self:measure(self:getCurrent().AbsoluteSize) + + local topBufferPx = reverse and self.leadBufferPx or self.trailBufferPx + local bottomBufferPx = reverse and self.trailBufferPx or self.leadBufferPx + + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + local topIndex = reverse and self.state.lead.index or self.state.trail.index + + local childSize = self:getChildSize(bottomIndex) + local topPos = Round.nearest(self:getChildCanvasPosition(topIndex)) - self.itemPadding + local bottomPos = Round.nearest(self:getChildCanvasPosition(bottomIndex)) + childSize + local itemSize = bottomPos + bottomBufferPx - topPos + + local frameDiff = 0 + if self.prevCycle.frameSize and size then + frameDiff = size - self.prevCycle.frameSize + end + + local bottomFrameCanvasPosition = self.prevCycle.canvasPosition + self.prevCycle.frameSize + local openCanvasAtBottom = math.max(self.prevCycle.canvasSize - bottomBufferPx - bottomFrameCanvasPosition, 0) + local openCanvasAtTop = self.prevCycle.canvasPosition + + local trimmablePadding = 0 + if frameSize < canvasSize then + trimmablePadding = topPos - topBufferPx + end + + local botAdjustment + local topAdjustment + + if frameDiff < 0 then -- contracting + if self.relativeAnchorLocation > frameSize * 0.5 then -- if we're at the bottom + topAdjustment = math.min(trimmablePadding, frameDiff) + botAdjustment = frameDiff - topAdjustment + else -- at the top + botAdjustment = frameDiff + topAdjustment = 0 + end + else -- expanding + -- first fill opposite end, then spillover to same end. + -- Any remaining space goes back to opposite end + if self.relativeAnchorLocation > frameSize * 0.5 then + topAdjustment = math.min(openCanvasAtTop, frameDiff) + local frameDiffRemainder = frameDiff - topAdjustment + botAdjustment = math.min(openCanvasAtBottom, frameDiffRemainder) + frameDiffRemainder = frameDiffRemainder - botAdjustment + topAdjustment = topAdjustment + frameDiffRemainder + else + botAdjustment = math.min(openCanvasAtBottom, frameDiff) + local frameDiffRemainder = frameDiff - botAdjustment + topAdjustment = math.min(openCanvasAtTop, frameDiffRemainder) + frameDiffRemainder = frameDiffRemainder - topAdjustment + botAdjustment = botAdjustment + frameDiffRemainder + end + end + + -- if bottomUp scroller + if self.relativeAnchorLocation >= frameSize * 0.5 then + + local currentAnchorCanvasPosition = + self:getAnchorCanvasFromIndex(self.state.anchor.index) - self.anchorFramePosition + + local newAnchor, remainder = + self:findIndexWithRemainder(currentAnchorCanvasPosition + frameDiff - topAdjustment, + self.state.anchor.index, + false) + + self.anchorFramePosition = math.max(remainder, 0) + self.anchorCanvasPosition = math.min(currentAnchorCanvasPosition + frameDiff, canvasSize - bottomBufferPx) + + if frameDiff > 0 then + -- self.anchorOffset affects padding + self.anchorOffset = openCanvasAtTop + self.anchorFramePosition - topAdjustment + end + + self:setState({ + anchor = {index = newAnchor, id = self:getID(newAnchor)}, + }) + else--topDown scroller + local newCanvasSize = canvasSize + if canvasSize > itemSize then + newCanvasSize = math.min(itemSize, canvasSize + botAdjustment) + end + + local newAnchor, remainder = + self:findIndexWithRemainder(self.anchorCanvasPosition - topAdjustment, + self.state.anchor.index, + false) + + -- if we expand past the amount of canvas available, move the anchor to the bottom of the first element + if math.abs(botAdjustment) < math.abs(frameDiff) then + self.anchorFramePosition = 0 + elseif newAnchor == self.state.anchor.index then + self.anchorFramePosition = self.anchorFramePosition + topAdjustment + else + self.anchorFramePosition = self.anchorFramePosition + remainder + end + + self.anchorCanvasPosition = self.anchorCanvasPosition - topAdjustment + self:setState({ + size = newCanvasSize, + anchor = { index = newAnchor, id = self:getID(newAnchor) }, + }) + end +end + +-- Move the canvas position so that the anchor element is in the same place on the screen. +function Scroller:scrollToAnchor() + if self.motorActive then + return + end + self.log:trace("scrollToAnchor") + if self.indexChanged == nil then + self:moveToAnchor() + end + + local newIndex = self.indexChanged.newIndex + local previousIndex = self.indexChanged.oldIndex + self.log:trace(" newIndex {}", newIndex) + self.log:trace(" previousIndex {}", previousIndex) + + local oldPos = self:measure(self:getCurrent().CanvasPosition) + self.relativeAnchorLocation + local newPos = self:getAnchorCanvasFromIndex(newIndex) + + self.log:trace(" old anchor pos {}", oldPos) + self.log:trace(" new anchor pos {}", newPos) + + self.indexChanged.currentPos = oldPos + self.indexChanged.newPos = newPos + self.motorActive = true + self.springLock = self.springLock + 1 + + local delta = newPos - oldPos + self.log:trace(" delta {}", delta) + self.motor = Otter.createSingleMotor(0) + self.motor:onStep(self.motorOnStep) + self.motor:onComplete(self.motorOnComplete) + self.motor:setGoal(Otter.spring(delta, self.props.animateOptions)) +end + +-- Move the canvas position so that the anchor element is in the same place on the screen. +function Scroller:moveToAnchor() + self.log:trace("moveToAnchor") + if self.motorActive then + return + end + if self:isScrollingWithElasticBehavior() then + return + end + + local currentPos = self:getAnchorFramePosition() + self.log:trace(" Anchor was at frame position {}", self.anchorFramePosition) + self.log:trace(" Anchor is currently at frame position {}", currentPos) + local newPos = self:measure(self:getCurrent().CanvasPosition) + currentPos - self.anchorFramePosition + self.log:trace(" Canvas should scroll to {}", newPos) + local current = self:getCurrent() + local maxPos = math.max(0, self:measure(current.CanvasSize).Offset - self:measure(current.AbsoluteSize)) + + self:setScroll(newPos) + if newPos < 0 then + self.log:trace(" Canvas scroll limited to 0, was {}", newPos) + self.anchorOffset = Round.towardsZero(newPos) + elseif newPos >= maxPos then + self.log:trace(" Canvas scroll limited to {}, was {}", maxPos, newPos) + self.anchorOffset = Round.towardsZero(newPos - maxPos) + else + self.log:trace(" Clearing anchorOffset") + self.anchorOffset = 0 + end +end + +-- Prevent scrolling when experiencing elastic behavior on touch devices +function Scroller:isScrollingWithElasticBehavior() + -- When the canvas is resized the bottom of the canvas is bigger than the canvas for + -- the first update. Since a resize is not an elastic scroll skip these checks after a resize + if self.resized then + return false + end + + -- Check if the top of the list has scrolled past the frame because of ElasticBehavior + local reverse = isReverse[self.props.orientation] + local topIndex = reverse and self.state.lead.index or self.state.trail.index + local startOfListIndex = reverse and self.state.listSize or 1 + if self:measure(self:getCurrent().CanvasPosition) < 0 and topIndex == startOfListIndex then + return true + end + + -- Check if the bottom of the list has scrolled past the frame because of ElasticBehavior + local bottomIndex = reverse and self.state.trail.index or self.state.lead.index + local frameSize = self:measure(self:getCurrent().AbsoluteSize) + local canvasPosition = self:measure(self:getCurrent().CanvasPosition) + local endOfListIndex = reverse and 1 or self.state.listSize + local bottomOfCanvas = Round.nearest(canvasPosition + frameSize) + local canvasSize = Round.nearest(self:measure(self:getCurrent().CanvasSize).Offset) + + if bottomOfCanvas > canvasSize and bottomIndex == endOfListIndex and canvasPosition > 0 then + return true + end + + return false +end + +-- Call loadNext and loadPrevious if needed. +function Scroller:loadMore() + self.log:trace("loadMore") + if self.props.loadPrevious and + self.state.trail.index <= self.props.loadingBuffer and + self.props.itemList ~= self.lastLoadPrevItems then + self.log:trace(" Calling loadPrevious") + self.lastLoadPrevItems = self.props.itemList + self.props.loadPrevious() + end + if self.props.loadNext and + self.state.lead.index > self.state.listSize - self.props.loadingBuffer and + self.props.itemList ~= self.lastLoadNextItems then + self.log:trace(" Calling loadNext") + self.lastLoadNextItems = self.props.itemList + self.props.loadNext() + end +end + +-- Set the current canvas position according to Orientation without calling +-- onScroll. +function Scroller:setScroll(pos) + self.log:trace(" Scrolling to {}", pos) + self.scrollDebounce = true + self:getCurrent().CanvasPosition = isVertical[self.props.orientation] + and Vector2.new(self:getCurrent().CanvasPosition.X, pos) + or Vector2.new(pos, self:getCurrent().CanvasPosition.Y) + self.scrollDebounce = false +end + +-- Scroll by a relative amount +function Scroller:scrollRelative(amount) + self.log:trace(" Current CanvasPosition {}", self:getCurrent().CanvasPosition) + self.log:trace("self.motorActive {}", self.motorActive) + self:setScroll(self:measure(self:getCurrent().CanvasPosition) + amount, true) + self.onScroll(self:getCurrent()) +end + + +-- Returns the signed distance from the element to the given canvas-relative +-- position, or 0 if the element overlaps it. The sign of the distance is +-- relative to the list indices. For this distance calculation, the padding +-- between elements is considered part of the current element. Returns nil if +-- the element is not currently rendered. +function Scroller:distanceToPosition(index, pos) + local child = self:getRbx(index) + if not child then + return nil + end + + local childTop = self:absoluteToCanvasPosition(self:measure(child.AbsolutePosition)) - self.itemPadding + local childSize = self:measure(child.AbsoluteSize) + 2 * self.itemPadding + + return Round.nearest(Distance.fromPointToRangeSigned(pos, childTop, childSize) * direction[self.props.orientation]) +end + +-- Get the canvas-relative position of the current anchor element. +function Scroller:getAnchorCanvasPosition() + return self:getAnchorCanvasFromIndex(self.state.anchor.index) +end + +function Scroller:getAnchorCanvasFromIndex(index) + local scale = self.props.anchorLocation.Scale + if not isReverse[self.props.orientation] then + scale = 1 - scale + end + + return Round.nearest(self:getChildCanvasPosition(index) + scale * self:getChildSize(index)) +end + +-- Get the frame-relative position of the current anchor element. +function Scroller:getAnchorFramePosition() + return self:getAnchorFrameFromIndex(self.state.anchor.index) +end + +function Scroller:getAnchorFrameFromIndex(index) + local scale = self.props.anchorLocation.Scale + if not isReverse[self.props.orientation] then + scale = 1 - scale + end + + return Round.nearest(self:getChildFramePosition(index) + scale * self:getChildSize(index)) + - self.relativeAnchorLocation +end + +-- Convert an AbsolutePosition to a position relative to the top-left corner of the canvas. +function Scroller:absoluteToCanvasPosition(position) + local current = self:getCurrent() + local canvas = current.CanvasPosition + local absolute = current.AbsolutePosition + return position + self:measure(canvas) - self:measure(absolute) +end + +-- Convert an AbsolutePosition to a position relative to the top-left corner of the scrolling frame. +function Scroller:absoluteToFramePosition(position) + local current = self:getCurrent() + local absolute = current.AbsolutePosition + return position - self:measure(absolute) +end + +-- Convert a position relative to the frame to a position relative to the canvas. +function Scroller:frameToCanvasPosition(position) + local current = self:getCurrent() + local canvas = current.CanvasPosition + return position + self:measure(canvas) +end + +-- Get the canvas-relative position of the element at the specified index. +function Scroller:getChildCanvasPosition(index) + local current = self:getRbx(index) + return current and self:absoluteToCanvasPosition(self:measure(current.AbsolutePosition)) or 0 +end + +-- Get the frame-relative position of the element at the specified index. +function Scroller:getChildFramePosition(index) + local current = self:getRbx(index) + return current and self:absoluteToFramePosition(self:measure(current.AbsolutePosition)) or 0 +end + +-- Get the absolute size of the element at the specified index. +function Scroller:getChildSize(index) + local current = self:getRbx(index) + return current and self:measure(current.AbsoluteSize) or 0 +end + +-- Get the ID of an element at a specific index. +function Scroller:getID(index) + return self.props.identifier(self.props.itemList[index]) +end + +-- Create or update a metadata entry for the given element. This can't use +-- self.props in willUpdate as any props it uses could be out of date. +function Scroller:updateMetadata(id, item, props) + local meta = self.metadata[id] + if not meta then + meta = {} + self.metadata[id] = meta + end + + if not meta.name then + local elem = props.renderItem(item, false) + meta.class = tostring(elem.component) + local pool = self:getKeyPool(meta.class) + meta.name = pool:get() + end + + if not self.refpool[meta.name] then + self.refpool[meta.name] = Roact.createRef() + end + meta.ref = self.refpool[meta.name] +end + +-- Clear the metadata for an element that is being unloaded. +function Scroller:clearMetadata(id) + local meta = self.metadata[id] + if not meta then + return + end + + -- Not releasing the names seems like it would be a memory leak, but this + -- relies on the fact that the key pool does not track in use keys. Rather, + -- the key tracks which pool it came from. So if nothing is using an + -- unreleased key, it will be garbage collected and never reused. + if not Cryo.List.find(self.props.recyclingDisabledFor, meta.class) then + meta.name:release() + end + meta.name = nil + meta.ref = nil +end + +-- Get the key pool for the given class of elements, or create a new one if that doesn't exist yet. +function Scroller:getKeyPool(class) + if not self.pools[class] then + self.pools[class] = KeyPool.new(class) + end + return self.pools[class] +end + +-- Get the metadata info for the element at the specified index. +function Scroller:getMetadata(index) + return self.metadata[self:getID(index)] +end + +-- Get the current Roblox instance from the ref stored in the metadata. +function Scroller:getRbx(index) + local meta = self:getMetadata(index) + return meta and meta.ref and meta.ref.current +end + +-- Return X or Y depending on the orientation. +function Scroller:measure(vecOrUDim2) + return isVertical[self.props.orientation] and vecOrUDim2.Y or vecOrUDim2.X +end + +-- Get the current ScrollingFrame instance. +function Scroller:getCurrent() + return self:getRef().current +end + +function Scroller:getRef() + -- Make sure to get the ref from props if that exists. + return self.props[Roact.Ref] or self._ref +end + +return Scroller diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/TimeLogger.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/TimeLogger.lua new file mode 100644 index 0000000..ee7237e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/TimeLogger.lua @@ -0,0 +1,10 @@ +local Root = script:FindFirstAncestor("infinite-scroller").Parent +local Lumberyak = require(Root.Lumberyak) + +local TimeLogger = Lumberyak.Logger.new(nil, script:GetFullName()) +TimeLogger:setContext({ + tick = tick, + prefix = "{tick}: ", +}) + +return TimeLogger diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/findNewIndices.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/findNewIndices.lua new file mode 100644 index 0000000..465e793 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/findNewIndices.lua @@ -0,0 +1,67 @@ +-- Find the new indicies of the trailing, anchor and leading elements. +-- props is expected to contain itemList, the identifier function and the maximum search distance. +-- state is expected to contain the old top, anchor and bottom indices and ids. +return function(props, state): (number?, number?, number?) + local topIndex = state.trail.index + local topID = state.trail.id + local anchorIndex = state.anchor.index + local anchorID = state.anchor.id + local bottomIndex = state.lead.index + local bottomID = state.lead.id + + local listSize = #props.itemList + + -- If too much got deleted and the previous anchor index is off the bottom of the list, start the search from + -- the bottom. + if topIndex > listSize then + topIndex = listSize + end + if anchorIndex > listSize then + anchorIndex = listSize + end + if bottomIndex > listSize then + bottomIndex = listSize + end + + -- No access to self:getID + local getID = function(index): string + return props.identifier(props.itemList[index]) + end + local topStill = getID(topIndex) == topID + local anchorStill = getID(anchorIndex) == anchorID + local bottomStill = getID(bottomIndex) == bottomID + if topStill and anchorStill and bottomStill then + -- Nothing important moved + return topIndex, anchorIndex, bottomIndex + end + + local step = 0 + local foundTop = topStill and topIndex or nil + local foundAnchor = nil + local foundBottom = bottomStill and bottomIndex or nil + + -- Scan outward from the old anchor index until we find the top and bottom or hit the max distance + local deltas = {top=-1, bottom=1} + repeat + for _, delta: number in pairs(deltas) do + local pos = anchorIndex + delta * step + + if pos >= 1 and pos <= listSize then + local id = getID(pos) + if id == topID then + foundTop = pos + end + if id == anchorID then + foundAnchor = pos + end + if id == bottomID then + foundBottom = pos + end + end + end + + step = step + 1 + until (foundTop and foundAnchor and foundBottom) or step > props.maximumSearchDistance + + return foundTop, foundAnchor, foundBottom +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/relocateIndices.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/relocateIndices.lua new file mode 100644 index 0000000..7e330c3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/Components/relocateIndices.lua @@ -0,0 +1,71 @@ +local Round = require(script.Parent.Round) + +export type Indices = { + anchorIndex: number?, + leadIndex: number?, + trailIndex: number?, +} + +-- Returns the three new indices with any nils filled in and any misorderings corrected. +-- new and old should be tables containing an anchorIndex, a leadIndex and a trailIndex. +return function(new: Indices, old: Indices, listSize: number): Indices + local isNewAnchorPresent: boolean = new.anchorIndex ~= nil + local isNewLeadPresent: boolean = new.leadIndex ~= nil + local isNewTrailPresent: boolean = new.trailIndex ~= nil + + local finalAnchorIndex: number = tonumber(new.anchorIndex or 0) + local finalLeadIndex: number = tonumber(new.leadIndex or 0) + local finalTrailIndex: number = tonumber(new.trailIndex or 0) + + local oldAnchorIndex: number = tonumber(old.anchorIndex or 0) + local oldLeadIndex: number = tonumber(old.leadIndex or 0) + local oldTrailIndex: number = tonumber(old.trailIndex or 0) + + -- There are 8 possibilities here as any combination of these could be deleted. Also, we can't use findIndexAt + -- here since that requires access to the children's measurements. + if not isNewAnchorPresent then + if isNewLeadPresent and isNewTrailPresent then + -- Estimate that the new anchor is proportionally the same distance from the lead and trail indices. + if finalLeadIndex == finalTrailIndex then + -- Guard against divide by zero. + finalAnchorIndex = finalLeadIndex + else + local oldRatio = (oldAnchorIndex - oldLeadIndex) / (oldTrailIndex - oldLeadIndex) + finalAnchorIndex = Round.nearest((finalTrailIndex - finalLeadIndex) * oldRatio + finalLeadIndex) + finalAnchorIndex = math.min(math.max(finalAnchorIndex, 1), listSize) + end + elseif isNewLeadPresent then + -- Given only the new leading index, estimate that the new anchor is the same distance away as it was. + finalAnchorIndex = finalLeadIndex + oldAnchorIndex - oldLeadIndex + finalAnchorIndex = math.min(math.max(finalAnchorIndex, 1), listSize) + elseif isNewTrailPresent then + -- Given only the new trailing index, estimate that the new anchor is the same distance away as it was. + finalAnchorIndex = finalTrailIndex + oldAnchorIndex - oldTrailIndex + finalAnchorIndex = math.min(math.max(finalAnchorIndex, 1), listSize) + else + -- Everything is gone. Just reuse the same index if that's still within the bounds of the list. + finalAnchorIndex = math.min(math.max(oldAnchorIndex, 1), listSize) + end + end + + -- If the leading and trailing indices haven't been worked out yet, estimate that the new ones should be the + -- same distance from the anchor as the old ones were. + if not isNewTrailPresent then + finalTrailIndex = finalAnchorIndex + oldTrailIndex - oldAnchorIndex + finalTrailIndex = math.min(math.max(finalTrailIndex, 1), listSize) + end + if not isNewLeadPresent then + finalLeadIndex = finalAnchorIndex + oldLeadIndex - oldAnchorIndex + finalLeadIndex = math.min(math.max(finalLeadIndex, 1), listSize) + end + + -- Make sure the resulting indices are in the right order. + local minIndex = math.min(finalAnchorIndex, finalLeadIndex, finalTrailIndex) + local maxIndex = math.max(finalAnchorIndex, finalLeadIndex, finalTrailIndex) + + return { + trailIndex = minIndex, + anchorIndex = finalAnchorIndex, + leadIndex = maxIndex, + } +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/init.lua new file mode 100644 index 0000000..25fe17c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/infinite-scroller/init.lua @@ -0,0 +1,7 @@ +local Scroller = require(script.Components.Scroller) +local Logger = require(script.Components.Logger) + +return { + Scroller = Scroller, + Logger = Logger, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/lock.toml new file mode 100644 index 0000000..2f64780 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/lock.toml @@ -0,0 +1,13 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/infinite-scroller" +version = "0.7.3" +commit = "8dbd1980cdd54930abdd9d251263f364c9aa6f0b" +source = "url+https://github.com/roblox/infinite-scroller" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Lumberyak roblox/lumberyak 0.1.1 url+https://github.com/roblox/lumberyak", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/t.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_infinite-scroller-98304e77-0.7.3/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-result/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-result/lock.toml new file mode 100644 index 0000000..37fa438 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-result/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/lua-result" +version = "0.1.0" +commit = "c6817c8455aa1f5922d83dd44c8bedbc7726e51d" +source = "git+https://github.com/roblox/lua-result#master" diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-result/lua-result/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-result/lua-result/init.lua new file mode 100644 index 0000000..7148129 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-result/lua-result/init.lua @@ -0,0 +1,100 @@ +local Result = {} +Result.__index = Result + +local ResultTypeSymbol = newproxy(true) + +function Result.new(status, value) + assert(typeof(status) == "boolean") + + local result = { + -- Used to locate where a result was created + _source = debug.traceback(), + + -- A tag to identify us as a result + [ResultTypeSymbol] = true, + + _status = status, + + -- The value success value or error message. + _value = value, + } + + setmetatable(result, Result) + + return result +end + +function Result.success(value) + return Result.new(true, value) +end + +function Result.error(value) + return Result.new(false, value) +end + +--[[ + Is the given object a Result instance? +]] +function Result.is(object) + if typeof(object) ~= "table" then + return false + end + + return object[ResultTypeSymbol] +end + +--[[ + The given callbacks are invoked depending on that result. + + Creates a new result that receives the output of the callback. +]] +function Result:match(successHandler, errorHandler) + assert(typeof(successHandler) == "function" or typeof(successHandler) == "nil", + string.format("Result:match expects successHandler to be a function or nil, got %s", typeof(successHandler))) + assert(typeof(errorHandler) == "function" or typeof(errorHandler) == "nil", + string.format("Result:match expects errorHandler to be a function or nil, got %s", typeof(errorHandler))) + + local newResult + if self._status then + if successHandler ~= nil then + newResult = successHandler(self._value) + else + return self + end + else + if errorHandler ~= nil then + newResult = errorHandler(self._value) + else + return self + end + end + if Result.is(newResult) then + return newResult + else + return Result.success(newResult) + end +end + +--[[ + The given callback is invoked if the result is success. + + Creates a new result that receives the output of the callback. +]] +function Result:matchSuccess(successHandler) + return self:match(successHandler, nil) +end + +--[[ + The given callback is invoked if the result is error. + + Creates a new result that receives the output of the callback. +]] +function Result:matchError(errorHandler) + return self:match(nil, errorHandler) +end + +function Result:unwrap() + return self._status, self._value +end + +return Result \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Lumberyak.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Lumberyak.lua new file mode 100644 index 0000000..21a2a23 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Lumberyak.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_lumberyak-c9fb3068-76fee23f"]["lumberyak"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Mock.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Mock.lua new file mode 100644 index 0000000..428f95d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Mock.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["jtaylor_mock"]["mock"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Promise.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Promise.lua new file mode 100644 index 0000000..5dea2b4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Promise.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["lua-promise"]["lua-promise"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Symbol.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Symbol.lua new file mode 100644 index 0000000..f9086fa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/Symbol.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_lua-symbol"]["lua-symbol"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lock.toml new file mode 100644 index 0000000..7039d34 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lock.toml @@ -0,0 +1,14 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/lua-roact-policy-provider" +version = "0.1.0" +commit = "5f782af8bb981d6e9d2b3fa92f69b02bd4f4cf37" +source = "git+https://github.com/roblox/lua-roact-policy-provider#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Lumberyak roblox/lumberyak 76fee23f git+https://github.com/roblox/lumberyak#master", + "Mock jtaylor/mock d2c4005c git+https://github.com/roblox/mock#master", + "Promise lua-promise 1bb842c3 git+https://github.com/roblox/lua-promise#master", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "Symbol roblox/lua-symbol 139fdfe6 git+https://github.com/roblox/lua-symbol#master", + "tutils tutils 937da4f7 git+https://github.com/roblox/tutils#master", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/Logger.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/Logger.lua new file mode 100644 index 0000000..939f724 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/Logger.lua @@ -0,0 +1,6 @@ +local Packages = script.Parent.Parent +local Lumberyak = require(Packages.Lumberyak) + +local logger = Lumberyak.Logger.new() + +return logger diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/Provider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/Provider.lua new file mode 100644 index 0000000..232276d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/Provider.lua @@ -0,0 +1,23 @@ +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) + +local appPolicyKey = require(script.Parent.appPolicyKey) + +return function() + local PolicyProvider = Roact.Component:extend("PolicyProvider") + + function PolicyProvider:init(props) + assert(type(props.policy) == "table", "Provider expects props.policy to be a table") + + self._context[appPolicyKey] = { + presentationPolicy = props.policy, + staticExternalPolicy = props.policyData, + } + end + + function PolicyProvider:render() + return Roact.oneChild(self.props[Roact.Children]) + end + + return PolicyProvider +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/appPolicyKey.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/appPolicyKey.lua new file mode 100644 index 0000000..9ef702e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/appPolicyKey.lua @@ -0,0 +1,4 @@ +local Packages = script.Parent.Parent +local Symbol = require(Packages.Symbol) + +return Symbol.named("AppPolicy") diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/appPolicyKey.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/appPolicyKey.spec.lua new file mode 100644 index 0000000..2213e8f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/appPolicyKey.spec.lua @@ -0,0 +1,9 @@ +return function() + local appPolicyKey = require(script.Parent.appPolicyKey) + + describe("require return value", function() + it("SHOULD return a valid Symbol", function() + expect(appPolicyKey).to.be.ok() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/connect.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/connect.lua new file mode 100644 index 0000000..3a8792a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/connect.lua @@ -0,0 +1,99 @@ +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local Promise = require(Packages.Promise) + +local Logger = require(script.Parent.Logger) +local appPolicyKey = require(script.Parent.appPolicyKey) + +local function mergePolicies(base, params) + local policyWrapper = {} + + for _, wrapper in ipairs(params) do + policyWrapper = Cryo.Dictionary.join(policyWrapper, wrapper(base)) + end + + return policyWrapper +end + +return function(getPolicyImpl) + assert(getPolicyImpl, "expected getPolicyImpl") + + return function(mapper) + assert(type(mapper) == "function", "connect expects mapper to be a function") + + return function(component) + local name = ("AppPolicy(%s)"):format(tostring(component)) + local componentLogger = Logger:new(name) + componentLogger:setContext({ + prefix = string.format("%s: ", name), + }) + + local providerNotFound = string.format("%s: Not a descendent of PolicyProvider", name) + local Connection = Roact.PureComponent:extend(name) + + componentLogger:trace("Connected to component: {}", tostring(component)) + + function Connection:init(props) + self.policyContext = self._context[appPolicyKey] + assert(self.policyContext, providerNotFound) + + self.setWithEmptyPolicy = function() + self.state = { + policy = mergePolicies({}, self.policyContext.presentationPolicy), + } + end + + if self.policyContext.staticExternalPolicy then + -- if we already have a staticExternalPolicy, there is + -- no need to read it from our implementation + self.state = { + policy = mergePolicies(self.policyContext.staticExternalPolicy, self.policyContext.presentationPolicy), + } + else + local retrievedExternalPolicy = getPolicyImpl.read() + if retrievedExternalPolicy then + self.state = { + policy = mergePolicies(retrievedExternalPolicy, self.policyContext.presentationPolicy), + } + else + self.setWithEmptyPolicy() + componentLogger:trace("No app policy data available") + end + + self.onPolicyChanged = function(newExternalPolicy) + self:setState({ + policy = mergePolicies(newExternalPolicy, self.policyContext.presentationPolicy), + }) + end + end + end + + function Connection:didMount() + if self._context[appPolicyKey].staticExternalPolicy then + return + end + self.connection = getPolicyImpl.onPolicyChanged(function(incomingExternalPolicy) + componentLogger:trace("Received policy update from MemStorageService") + self.onPolicyChanged(incomingExternalPolicy) + end) + end + + function Connection:render() + local policyProps = mapper(self.state.policy, self.props) + local newProps = Cryo.Dictionary.join(self.props, policyProps) + return Roact.createElement(component, newProps) + end + + function Connection:willUnmount() + if self.connection then + self.connection:Disconnect() + end + -- sometimes the callback will fire even after :Disconnect was called + self.onPolicyChange = nil + end + + return Connection + end + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/connect.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/connect.spec.lua new file mode 100644 index 0000000..a13dc32 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/connect.spec.lua @@ -0,0 +1,542 @@ +return function() + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + local tutils = require(Packages.tutils) + local Mock = require(Packages.Mock) + local MagicMock = Mock.MagicMock + + local fromMemStorageService = require(script.Parent.getPolicyImplementations.fromMemStorageService) + + local Provider = require(script.Parent.Provider) + local providerInstance = Provider() + + local function checkForPropsAfterMounting(params) + assert(params.expectedProps) + assert(params.mapper) + assert(params.policyProp) + assert(params.connect) + assert(params.provider) + + params.shouldCheckPropsIf = params.shouldCheckPropsIf or function() + return true + end + + local hasBaseComponentEverRendered = false + local hasPropsEverBeenChecked = false + + local baseComponent = function(props) + hasBaseComponentEverRendered = true + + if params.shouldCheckPropsIf() then + hasPropsEverBeenChecked = true + for propName, propValue in pairs(params.expectedProps) do + if props[propName] ~= propValue then + fail(string.format( + "Expected baseComponent to have prop `%s` = `%s`." .. " " .. + "Got: `%s` instead." .. " " .. + "(Check the `mapper` function is correctly formatted)", + propName, + tostring(propValue), + tostring(props[propName]) + )) + end + end + end + + return Roact.createElement("Folder") + end + + local wrappedComponent = params.connect(params.mapper)(baseComponent) + + local tree = Roact.createElement(params.provider, { + policy = params.policyProp, + policyData = params.policyDataProp, + }, { + wrappedComponent = Roact.createElement(wrappedComponent) + }) + + local instance = Roact.mount(tree) + if params.funcAfterMounting then + params.funcAfterMounting() + end + Roact.unmount(instance) + + expect(hasBaseComponentEverRendered).to.equal(true) + expect(hasPropsEverBeenChecked).to.equal(true) + end + + describe("WHEN required", function() + local connect = require(script.Parent.connect) + it("SHOULD return a function", function() + expect(connect).to.be.a("function") + end) + + describe("GIVEN a fromMemStorageServiceWithBehavior", function() + local behavior = "mockBehavior" + + describe("GIVEN empty dependencies", function() + local getPolicyImpl = fromMemStorageService({ + HttpService = MagicMock.new(), + MemStorageService = MagicMock.new(), + })(behavior) + local connectInstance = connect(getPolicyImpl) + + it("SHOULD return a function", function() + expect(connectInstance).to.be.a("function") + end) + + describe("GIVEN a static mapper function", function() + local mapper = function() + return { + foo = "bar", + } + end + local mappedConnection = connectInstance(mapper) + + it("SHOULD return a function", function() + expect(mappedConnection).to.be.a("function") + end) + + describe("GIVEN a component", function() + local baseComponent = function() + return Roact.createElement("Folder") + end + local wrappedComponent = mappedConnection(baseComponent) + + it("SHOULD return a new component", function() + expect(baseComponent).to.never.equal(wrappedComponent) + end) + + describe("GIVEN a Roact tree without a Provider", function() + local tree = Roact.createElement(wrappedComponent) + + it("SHOULD throw", function() + expect(function() + Roact.mount(tree) + end).to.throw() + end) + end) + + describe("GIVEN a Roact tree with a Provider", function() + describe("GIVEN a nil policy prop", function() + local tree = Roact.createElement(providerInstance, { + policy = nil, + }, { + wrappedComponent = Roact.createElement(wrappedComponent) + }) + + it("SHOULD throw", function() + expect(function() + Roact.mount(tree) + end).to.throw() + end) + end) + + describe("GIVEN an empty policy prop", function() + local tree = Roact.createElement(providerInstance, { + policy = {}, + }, { + wrappedComponent = Roact.createElement(wrappedComponent) + }) + + it("SHOULD mount and unmount successfully", function() + local instance = Roact.mount(tree) + Roact.unmount(instance) + end) + end) + + describe("GIVEN policy prop with a single (static) definition", function() + local mockPolicy1 = function(_) + return { + isFeatureEnabled = "mockPolicy1Enabled", + } + end + + it("SHOULD allow mapper to create props for base component", function() + checkForPropsAfterMounting({ + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + } + end, + expectedProps = { + isPolicy1FeatureEnabled = "mockPolicy1Enabled" + }, + }) + end) + end) + + describe("GIVEN policy prop with a multiple (static) definitions", function() + local mockPolicy1 = function(_) + return { + isFeature1Enabled = 100, + } + end + + local mockPolicy2 = function(_) + return { + isFeature2Enabled = 200, + } + end + + it("SHOULD allow mapper to create props for base component", function() + checkForPropsAfterMounting({ + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1, mockPolicy2 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeature1Enabled, + isPolicy2FeatureEnabled = policy.isFeature2Enabled, + } + end, + expectedProps = { + isPolicy1FeatureEnabled = 100, + isPolicy2FeatureEnabled = 200, + }, + }) + end) + end) + end) + end) + end) + end) + + describe("GIVEN MemStorageService can retrieve a string and HttpService can decode it", function() + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function(str) + return { + foo = "bar", + } + end + + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return "mockStorageData" + end + + local getPolicyImpl = fromMemStorageService({ + HttpService = mockHttpService, + MemStorageService = mockMemStorageService, + })(behavior) + + local connectInstance = connect(getPolicyImpl) + + describe("GIVEN policy prop with a single dynamic definition (read from MemStorageService)", function() + local mockPolicy1 = function(policy) + return { + isFeatureEnabled = policy.foo, + } + end + + it("SHOULD allow mapper to create props for base component", function() + checkForPropsAfterMounting({ + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + } + end, + expectedProps = { + -- bar is the result from HttpService's JSONDecode + -- (mocking the pull from MemStorageService) + isPolicy1FeatureEnabled = "bar" + }, + }) + end) + end) + end) + + describe("GIVEN MemStorageService cannot retrieve a string", function() + local mockHttpService = MagicMock.new() + + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return nil + end + + local getPolicyImpl = fromMemStorageService({ + HttpService = mockHttpService, + MemStorageService = mockMemStorageService, + })(behavior) + + local connectInstance = connect(getPolicyImpl) + + describe("GIVEN policy prop with a single dynamic definition (read from MemStorageService)", function() + it("SHOULD allow mapper to create props for base component", function() + local wasMockPolicy1EverCalled = false + local mockPolicy1 = function(policy) + wasMockPolicy1EverCalled = true + + -- policy should be an empty table because + -- MemStorageService is providing invalid values + expect(tutils.shallowEqual(policy, {})).to.equal(true) + return { + isFeatureEnabled = policy and policy.foo, + } + end + + checkForPropsAfterMounting({ + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + } + end, + expectedProps = {}, + }) + + expect(wasMockPolicy1EverCalled).to.equal(true) + end) + end) + end) + + describe("GIVEN MemStorageService updates with a new string", function() + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function(_, jsonString) + if jsonString == "mockJsonString" then + return { + isFeatureEnabled = "hello.world", + } + end + + return nil + end + + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return nil + end + + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + + local getPolicyImpl = fromMemStorageService({ + HttpService = mockHttpService, + MemStorageService = mockMemStorageService, + })(behavior) + + local connectInstance = connect(getPolicyImpl) + + describe("GIVEN policy prop with a single dynamic definition (read from MemStorageService)", function() + it("SHOULD allow mapper to create props for base component", function() + local numberOfTimesMockPolicy1EverCalled = 0 + local mockPolicy1 = function(policy) + numberOfTimesMockPolicy1EverCalled = numberOfTimesMockPolicy1EverCalled + 1 + + return { + isFeatureEnabled = policy and policy.isFeatureEnabled, + } + end + + checkForPropsAfterMounting({ + funcAfterMounting = function() + updateMemStorageService:Fire("mockJsonString") + end, + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + } + end, + shouldCheckPropsIf = function() + return numberOfTimesMockPolicy1EverCalled > 1 + end, + expectedProps = { + isPolicy1FeatureEnabled = "hello.world", + }, + }) + + expect(numberOfTimesMockPolicy1EverCalled).to.equal(2) + end) + end) + end) + + describe("GIVEN MemStorageService updates with a nil value", function() + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function(_, jsonString) + if jsonString == "mockJsonString" then + return { + isFeatureEnabled = "hello.world", + } + end + + return nil + end + + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return nil + end + + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + + local getPolicyImpl = fromMemStorageService({ + HttpService = mockHttpService, + MemStorageService = mockMemStorageService, + })(behavior) + + local connectInstance = connect(getPolicyImpl) + + describe("GIVEN policy prop with a single dynamic definition (read from MemStorageService)", function() + it("SHOULD allow mapper to create props for base component", function() + local numberOfTimesMockPolicy1EverCalled = 0 + local mockPolicy1 = function(policy) + numberOfTimesMockPolicy1EverCalled = numberOfTimesMockPolicy1EverCalled + 1 + + return { + isFeatureEnabled = policy and policy.isFeatureEnabled, + } + end + + checkForPropsAfterMounting({ + funcAfterMounting = function() + updateMemStorageService:Fire("mockJsonString") + end, + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + } + end, + shouldCheckPropsIf = function() + return numberOfTimesMockPolicy1EverCalled > 1 + end, + expectedProps = { + isPolicy1FeatureEnabled = "hello.world", + }, + }) + + expect(numberOfTimesMockPolicy1EverCalled).to.equal(2) + end) + end) + end) + + describe("GIVEN a policyData prop", function() + local mockHttpService = MagicMock.new() + local mockMemStorageService = MagicMock.new() + + local getPolicyImpl = fromMemStorageService({ + HttpService = mockHttpService, + MemStorageService = mockMemStorageService, + })(behavior) + + local connectInstance = connect(getPolicyImpl) + local mockPolicy1 = function(policy) + return { + isFeatureEnabled = policy.mockIsFeatureEnabled, + } + end + + it("SHOULD return policyData and not call MemStorageService", function() + checkForPropsAfterMounting({ + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + policyDataProp = { + mockIsFeatureEnabled = "hello.world", + }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + } + end, + expectedProps = { + isPolicy1FeatureEnabled = "hello.world", + }, + }) + + local callsGetItem = Mock.getCalls(mockMemStorageService.GetItem) + expect(#callsGetItem).to.equal(0) + + local callsBindAndFire = Mock.getCalls(mockMemStorageService.BindAndFire) + expect(#callsBindAndFire).to.equal(0) + end) + end) + end) + + describe("GIVEN a policy prop that reads `foo`", function() + local mockPolicy1 = function(policy) + return { + isFeatureEnabled = policy.foo, + } + end + + describe("GIVEN getPolicyImpl with a static response", function() + local getPolicyImpl = MagicMock.new() + getPolicyImpl.read = function() + return { + foo = "bar", + } + end + + local connectInstance = connect(getPolicyImpl) + + it("SHOULD pass props to the lower component from the Promise resolution", function() + checkForPropsAfterMounting({ + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + hasProblemsFetching = (policy.isFeatureEnabled == nil), + } + end, + expectedProps = { + isPolicy1FeatureEnabled = "bar", + hasProblemsFetching = false, + }, + }) + end) + end) + + describe("GIVEN getPolicyImpl with a nil response", function() + local getPolicyImpl = MagicMock.new() + getPolicyImpl.read = function() + return nil + end + + local connectInstance = connect(getPolicyImpl) + + it("SHOULD not be able to define `isPolicy1FeatureEnabled`", function() + checkForPropsAfterMounting({ + connect = connectInstance, + provider = providerInstance, + policyProp = { mockPolicy1 }, + mapper = function(policy) + return { + isPolicy1FeatureEnabled = policy.isFeatureEnabled, + hasProblemsFetching = (policy.isFeatureEnabled == nil) + } + end, + expectedProps = { + isPolicy1FeatureEnabled = nil, + hasProblemsFetching = true, + }, + }) + end) + end) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/endToEnd.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/endToEnd.spec.lua new file mode 100644 index 0000000..d4a48d9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/endToEnd.spec.lua @@ -0,0 +1,64 @@ +return function() + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + + local PolicyProvider = require(script.Parent) + + it("SHOULD work", function() + local presentationPolicy = function(externalPolicy) + return { + isFeatureEnabled = externalPolicy.isFeatureEnabled or false, + } + end + + local externalPolicy = { + isFeatureEnabled = true, + } + local getPolicyImpl = PolicyProvider.GetPolicyImplementations.Static(externalPolicy) + local UniversalAppPolicyProvider = PolicyProvider.withGetPolicyImplementation(getPolicyImpl) + + local function MyComponent(props) + -- Values from PolicyProvider can be accessed just like regular props + local isFeatureEnabled = props.isFeatureEnabled + + return Roact.createElement("ScreenGui", nil, { + Label = Roact.createElement("TextLabel", { + -- ...and used in your components! + Text = "isFeatureEnabled: " .. tostring(isFeatureEnabled), + Size = UDim2.new(1, 0, 1, 0), + }) + }) + end + + -- `mapPolicyToProps` should return a table containing props that will be passed to + -- your component! + + -- `connect` returns a function, so we call that function, passing in our + -- component, getting back a new component! + MyComponent = UniversalAppPolicyProvider.connect( + function(incomingPresentationPolicy, props) + -- mapPolicyToProps is run every time the policy's state updates. + -- It's also run whenever the component receives new props. + return { + isFeatureEnabled = incomingPresentationPolicy.isFeatureEnabled, + } + end + )(MyComponent) + + local app = Roact.createElement(UniversalAppPolicyProvider.Provider, { + -- policy can be a list of PresentationalPolicies + policy = { presentationPolicy }, + }, { + Main = Roact.createElement(MyComponent), + }) + + local folder = Instance.new("Folder") + Roact.mount(app, folder) + + -- if the label has the following string, we know + -- everything is working! + local Label = folder:FindFirstChild("Label", true) + expect(Label).to.be.ok() + expect(Label.Text).to.equal("isFeatureEnabled: true") + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromMemStorageService.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromMemStorageService.lua new file mode 100644 index 0000000..68bee21 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromMemStorageService.lua @@ -0,0 +1,98 @@ +local DefaultHttpService = game:GetService("HttpService") +local DefaultMemStorageService = game:GetService("MemStorageService") +local DefaultPlayersService = game:GetService("Players") + +return function(dependencies) + dependencies = dependencies or {} + dependencies.HttpService = dependencies.HttpService or DefaultHttpService + dependencies.MemStorageService = dependencies.MemStorageService or DefaultMemStorageService + dependencies.PlayersService = dependencies.PlayersService or DefaultPlayersService + + assert(dependencies.HttpService, "expected dependencies.HttpService") + assert(dependencies.MemStorageService, "expected dependencies.MemStorageService") + assert(dependencies.PlayersService, "expected dependencies.PlayersService") + + local HttpService = dependencies.HttpService + local MemStorageService = dependencies.MemStorageService + local PlayersService = dependencies.PlayersService + + return function(behavior) + assert(behavior, "expected behavior") + + local function getStoreKey() + local userId = -1 + if PlayersService.LocalPlayer then + userId = PlayersService.LocalPlayer.UserId + end + return "GUAC:" .. userId .. ":" .. behavior + end + + local connectionStoreKey + local memStorageConnection + local previouslyReadValue + + local onPolicyChangedEvent = Instance.new("BindableEvent") + + local function onPolicyUpdated(newPolicyData) + -- MemStorageService will not de-duplicate the same item from storage + if newPolicyData ~= previouslyReadValue then + if newPolicyData and #newPolicyData > 0 then + local success, decodedExternalPolicy = pcall(function() + return HttpService:JSONDecode(newPolicyData) + end) + if success then + -- never store garbage + previouslyReadValue = newPolicyData + onPolicyChangedEvent:Fire(decodedExternalPolicy) + end + end + end + end + + return { + read = function() + local storeKey = getStoreKey() + local policyData = MemStorageService:GetItem(storeKey) + if policyData and #policyData > 0 then + local success, policy = pcall(function() + return HttpService:JSONDecode(policyData) + end) + if success then + -- Be sure to store the json string + previouslyReadValue = policyData + return policy + end + end + + return nil + end, + + onPolicyChanged = function(func) + local storeKey = getStoreKey() + + local connection = onPolicyChangedEvent.Event:Connect(func) + + if memStorageConnection and connectionStoreKey == storeKey then + -- Fire listener with existing value + if previouslyReadValue then + local success, policy = pcall(function() + return HttpService:JSONDecode(previouslyReadValue) + end) + + if success then + func(policy) + end + end + else + if memStorageConnection then + memStorageConnection:Disconnect() + end + connectionStoreKey = storeKey + memStorageConnection = MemStorageService:BindAndFire(storeKey, onPolicyUpdated) + end + + return connection + end, + } + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromMemStorageService.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromMemStorageService.spec.lua new file mode 100644 index 0000000..36af960 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromMemStorageService.spec.lua @@ -0,0 +1,304 @@ +return function() + local Packages = script.Parent.Parent.Parent + local Mock = require(Packages.Mock) + local MagicMock = Mock.MagicMock + + local fromMemStorageService = require(script.Parent.fromMemStorageService) + + describe("GIVEN a behavior", function() + local behavior = "mockBehavior" + describe("GIVEN a MemStorageService and HttpService with functional GetItem, BindAndFire and JSONDecode", function() + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return "jsonExternalPolicy" + end + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + local initialValue = mockMemStorageService.GetItem() + local connection = updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + + func(initialValue) + return connection + end + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function() + return "decodedExternalPolicy" + end + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + HttpService = mockHttpService, + })(behavior) + it("SHOULD return the policy when `read` is invoked", function() + local result = fromMemStorageServiceInstance.read() + expect(result).to.equal("decodedExternalPolicy") + end) + + it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function() + local result = fromMemStorageServiceInstance.onPolicyChanged(function() end) + expect(result).to.be.ok() + expect(result.Disconnect).to.be.ok() + result:Disconnect() + end) + + it("SHOULD invoke passed in function with JSONDecode results when updateMemStorageService is fired", function() + local wasEverCalled = false + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + wasEverCalled = true + expect(value).to.equal("decodedExternalPolicy") + end) + + updateMemStorageService:Fire("jsonExternalPolicyUpdated") + + result:Disconnect() + + expect(wasEverCalled).to.equal(true) + end) + + it("SHOULD NOT invoke passed in function with JSONDecode results ".. + "when updateMemStorageService is fired with the same value", function() + local timesEverCalled = 0 + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + timesEverCalled = timesEverCalled + 1 + expect(value).to.equal("decodedExternalPolicy") + end) + + updateMemStorageService:Fire("foo") + updateMemStorageService:Fire("foo") + updateMemStorageService:Fire("foo") + updateMemStorageService:Fire("foo") + + result:Disconnect() + + -- Once for initial value and then once for foo + expect(timesEverCalled).to.equal(2) + end) + + it("SHOULD NOT invoke passed in function with JSONDecode results ".. + "when updateMemStorageService is fired with the same value, while ignore nils", function() + local timesEverCalled = 0 + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + timesEverCalled = timesEverCalled + 1 + expect(value).to.equal("decodedExternalPolicy") + end) + + updateMemStorageService:Fire("bar") + updateMemStorageService:Fire(nil) + updateMemStorageService:Fire("bar") + + result:Disconnect() + + -- Once for initial value and then once for bar + expect(timesEverCalled).to.equal(2) + end) + + it("SHOULD invoke multiple listeners when updateMemStorageService is fired", function() + local listenerACalled = 0 + local connA = fromMemStorageServiceInstance.onPolicyChanged(function(value) + listenerACalled = listenerACalled + 1 + expect(value).to.equal("decodedExternalPolicy") + end) + + local listenerBCalled = 0 + local connB = fromMemStorageServiceInstance.onPolicyChanged(function(value) + listenerBCalled = listenerBCalled + 1 + expect(value).to.equal("decodedExternalPolicy") + end) + + updateMemStorageService:Fire("baz") + + connA:Disconnect() + connB:Disconnect() + + -- Once for initial value and then once for baz + expect(listenerACalled).to.equal(2) + expect(listenerBCalled).to.equal(2) + end) + end) + + describe("GIVEN a functional MemStorageService and broken HttpService JSONDecode", function() + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return "jsonExternalPolicy" + end + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function() + return nil + end + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + HttpService = mockHttpService, + })(behavior) + it("SHOULD return nil when `read` is invoked", function() + local result = fromMemStorageServiceInstance.read() + expect(result).to.equal(nil) + end) + + it("SHOULD invoke passed in function with JSONDecode results when updateMemStorageService is fired", function() + local wasEverCalled = false + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + wasEverCalled = true + expect(value).to.equal(nil) + end) + + updateMemStorageService:Fire("jsonExternalPolicyUpdated") + + result:Disconnect() + + expect(wasEverCalled).to.equal(true) + end) + end) + + describe("GIVEN a MemStorageService that always returns invalid results", function() + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return nil + end + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + local mockHttpService = MagicMock.new() + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + HttpService = mockHttpService, + })(behavior) + it("SHOULD return nil when `read` is invoked", function() + local result = fromMemStorageServiceInstance.read() + expect(result).to.equal(nil) + end) + + it("SHOULD never invoke passed function with JSONDecode results when updateMemStorageService is fired", function() + local wasEverCalled = false + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + wasEverCalled = true + end) + + updateMemStorageService:Fire(nil) + + result:Disconnect() + + expect(wasEverCalled).to.equal(false) + end) + end) + + describe("GIVEN a PlayersService with a missing LocalPlayer", function() + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return "mockStorageItemJSON" + end + + local mockHttpService = MagicMock.new() + local mockPlayersService = MagicMock.new() + mockPlayersService.LocalPlayer = nil + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + PlayersService = mockPlayersService, + HttpService = mockHttpService, + })(behavior) + + it("SHOULD still return a value when `read` is invoked", function() + local result = fromMemStorageServiceInstance.read() + expect(result).to.be.ok() + end) + end) + + describe("GIVEN a HttpService that can throw based on MemStorageService's GetItem", function() + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return "garbage" + end + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function(_, value) + if value == "validJson" then + return { foo = true } + end + + error("invalid json") + end + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + HttpService = mockHttpService, + })(behavior) + + it("SHOULD return nil since GetItem returns garbage", function() + local result = fromMemStorageServiceInstance.read() + expect(result).to.never.be.ok() + end) + + it("SHOULD never invoke passed function with JSONDecode results when updateMemStorageService is fired", function() + local numberOfTimesCalled = 0 + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + numberOfTimesCalled = numberOfTimesCalled + 1 + end) + + updateMemStorageService:Fire("validJson") + updateMemStorageService:Fire("garbage") + updateMemStorageService:Fire("validJson") + + result:Disconnect() + + expect(numberOfTimesCalled).to.equal(1) + end) + end) + + describe("GIVEN a new instance of fromMemStorageService", function() + local validJson = "validJson" + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return validJson + end + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function(_, value) + return "tableFromJson" + end + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + HttpService = mockHttpService, + })(behavior) + + describe("WHEN read returns a value and onPolicyChanged returns the same value", function() + it("SHOULD not fire onPolicyChanged function", function() + local numberOfTimesCalled = 0 + fromMemStorageServiceInstance.onPolicyChanged(function() + numberOfTimesCalled = numberOfTimesCalled + 1 + end) + local _ = fromMemStorageServiceInstance.read() + -- intentionally fire the same value that we read + updateMemStorageService:Fire(validJson) + + expect(numberOfTimesCalled).to.equal(0) + end) + end) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromPolicyService.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromPolicyService.lua new file mode 100644 index 0000000..9b1a350 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromPolicyService.lua @@ -0,0 +1,61 @@ +local DefaultPolicyService = game:GetService("PolicyService") +local DefaultPlayersService = game:GetService("Players") + +local root = script.Parent.Parent +local Packages = root.Parent +local Promise = require(Packages.Promise) + +local Logger = require(root.Logger) + +return function(dependencies) + dependencies = dependencies or {} + dependencies.PolicyService = dependencies.PolicyService or DefaultPolicyService + dependencies.PlayersService = dependencies.PlayersService or DefaultPlayersService + + assert(dependencies.PolicyService, "expected dependencies.PolicyService") + assert(dependencies.PlayersService, "expected dependencies.PlayersService") + + local PolicyService = dependencies.PolicyService + local PlayersService = dependencies.PlayersService + + return function() + return { + read = function() + return nil + end, + + onPolicyChanged = function(func) + local onPolicyChangedEvent = Instance.new("BindableEvent") + + -- be sure to connect before our Promise can resolve + local connection = onPolicyChangedEvent.Event:Connect(func) + + Promise.new(function(resolve, reject) + local player = PlayersService.LocalPlayer + if player then + local success, result = pcall(function() + return PolicyService:GetPolicyInfoForPlayerAsync(player) + end) + if success then + if result then + resolve(result) + else + reject("GetPolicyInfoForPlayerAsync return nil value") + end + else + reject("GetPolicyInfoForPlayerAsync had an error when calling") + end + else + reject("LocalPlayer not found") + end + end):andThen(function(newPolicy) + onPolicyChangedEvent:Fire(newPolicy) + end):catch(function(errorString) + Logger:warning("Could not fetch from PolicyService due to error: {}", errorString) + end) + + return connection + end, + } + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromPolicyService.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromPolicyService.spec.lua new file mode 100644 index 0000000..3208230 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromPolicyService.spec.lua @@ -0,0 +1,160 @@ +return function() + local Packages = script.Parent.Parent.Parent + local Mock = require(Packages.Mock) + local MagicMock = Mock.MagicMock + + local fromPolicyService = require(script.Parent.fromPolicyService) + + describe("GIVEN a fully mocked dependencies", function() + local mockPolicyService = MagicMock.new() + mockPolicyService.GetPolicyInfoForPlayerAsync = function() + return "mockGetPolicyInfoForPlayerAsyncResponse" + end + + local dependencies = { + PolicyService = mockPolicyService, + PlayersService = MagicMock.new(), + } + + describe("WHEN invoked", function() + local fromPolicyServiceInstance = fromPolicyService(dependencies)() + + it("SHOULD return a table", function() + expect(fromPolicyServiceInstance).to.be.a("table") + end) + + it("SHOULD return a table with read and `onPolicyChanged` fields", function() + expect(fromPolicyServiceInstance.read).to.be.a("function") + expect(fromPolicyServiceInstance.onPolicyChanged).to.be.a("function") + end) + + describe("WHEN `onPolicyChanged` is invoked", function() + local timesEverCalled = 0 + local lastValue + local connection = fromPolicyServiceInstance.onPolicyChanged(function(value) + timesEverCalled = timesEverCalled + 1 + lastValue = value + end) + it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function() + expect(connection).to.be.ok() + expect(connection.Disconnect).to.be.ok() + end) + + it("SHOULD fire when it resolves with data", function() + expect(timesEverCalled).to.equal(1) + expect(lastValue).to.equal("mockGetPolicyInfoForPlayerAsyncResponse") + end) + end) + + describe("WHEN `read` is invoked", function() + local result = fromPolicyServiceInstance.read() + it("SHOULD return nil", function() + expect(result).to.never.be.ok() + end) + end) + end) + end) + + describe("GIVEN a PlayersService with no LocalPlayer", function() + local mockPlayersService = MagicMock.new() + mockPlayersService.LocalPlayer = nil + + local dependencies = { + PolicyService = MagicMock.new(), + PlayersService = mockPlayersService, + } + + describe("WHEN invoked", function() + local fromPolicyServiceInstance = fromPolicyService(dependencies)() + + it("SHOULD return a table", function() + expect(fromPolicyServiceInstance).to.be.a("table") + end) + + describe("WHEN `onPolicyChanged` is invoked", function() + local timesEverCalled = 0 + local connection = fromPolicyServiceInstance.onPolicyChanged(function(value) + timesEverCalled = timesEverCalled + 1 + end) + it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function() + expect(connection).to.be.ok() + expect(connection.Disconnect).to.be.ok() + end) + + it("SHOULD never fire when it rejects", function() + expect(timesEverCalled).to.equal(0) + end) + end) + end) + end) + + describe("GIVEN a PolicyService that throws", function() + local mockPolicyService = MagicMock.new() + mockPolicyService.GetPolicyInfoForPlayerAsync = function() + error("Throw") + end + + local dependencies = { + PolicyService = mockPolicyService, + PlayersService = MagicMock.new(), + } + + describe("WHEN invoked", function() + local fromPolicyServiceInstance = fromPolicyService(dependencies)() + + it("SHOULD return a table", function() + expect(fromPolicyServiceInstance).to.be.a("table") + end) + + describe("WHEN `onPolicyChanged` is invoked", function() + local timesEverCalled = 0 + local connection = fromPolicyServiceInstance.onPolicyChanged(function(value) + timesEverCalled = timesEverCalled + 1 + end) + it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function() + expect(connection).to.be.ok() + expect(connection.Disconnect).to.be.ok() + end) + + it("SHOULD never fire when it rejects", function() + expect(timesEverCalled).to.equal(0) + end) + end) + end) + end) + + describe("GIVEN a PolicyService that returns nil", function() + local mockPolicyService = MagicMock.new() + mockPolicyService.GetPolicyInfoForPlayerAsync = function() + return nil + end + + local dependencies = { + PolicyService = mockPolicyService, + PlayersService = MagicMock.new(), + } + + describe("WHEN invoked", function() + local fromPolicyServiceInstance = fromPolicyService(dependencies)() + + it("SHOULD return a table", function() + expect(fromPolicyServiceInstance).to.be.a("table") + end) + + describe("WHEN `onPolicyChanged` is invoked", function() + local timesEverCalled = 0 + local connection = fromPolicyServiceInstance.onPolicyChanged(function(value) + timesEverCalled = timesEverCalled + 1 + end) + it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function() + expect(connection).to.be.ok() + expect(connection.Disconnect).to.be.ok() + end) + + it("SHOULD never fire when it rejects", function() + expect(timesEverCalled).to.equal(0) + end) + end) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromStaticSource.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromStaticSource.lua new file mode 100644 index 0000000..73400ab --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromStaticSource.lua @@ -0,0 +1,19 @@ +local function noOpt() +end + +return function() + return function(externalPolicy) + assert(externalPolicy, "expected externalPolicy") + + return { + read = function() + return externalPolicy + end, + + onPolicyChanged = function(func) + func = func or noOpt + return Instance.new("BindableEvent").Event:Connect(func) + end, + } + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromStaticSource.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromStaticSource.spec.lua new file mode 100644 index 0000000..174e1e6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/fromStaticSource.spec.lua @@ -0,0 +1,45 @@ +return function() + describe("WHEN required", function() + local fromStaticSource = require(script.Parent.fromStaticSource) + + it("SHOULD have the following interface", function() + expect(fromStaticSource).to.be.a("function") + end) + + describe("WHEN invoked", function() + local fromStaticSourceInstance = fromStaticSource() + it("SHOULD have the following interface", function() + expect(fromStaticSourceInstance).to.be.a("function") + end) + + describe("GIVEN a static value", function() + local mockExternalPolicy = "mockExternalPolicy" + + describe("WHEN invoked", function() + local fromStaticSourceInstanceWithExternalPolicy = fromStaticSourceInstance(mockExternalPolicy) + + it("SHOULD have the following interface", function() + expect(fromStaticSourceInstanceWithExternalPolicy).to.be.a("table") + expect(fromStaticSourceInstanceWithExternalPolicy.read).to.be.a("function") + expect(fromStaticSourceInstanceWithExternalPolicy.onPolicyChanged).to.be.a("function") + end) + + describe("WHEN read is invoked", function() + local result = fromStaticSourceInstanceWithExternalPolicy.read() + it("SHOULD return the static value", function() + expect(result).to.equal(mockExternalPolicy) + end) + end) + + describe("WHEN onPolicyChanged is invoked", function() + local result = fromStaticSourceInstanceWithExternalPolicy.onPolicyChanged() + it("SHOULD return a Disconnect-able object", function() + expect(result).to.be.ok() + expect(result.Disconnect).to.be.ok() + end) + end) + end) + end) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/oldFromMemStorageService.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/oldFromMemStorageService.lua new file mode 100644 index 0000000..6337f94 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/oldFromMemStorageService.lua @@ -0,0 +1,74 @@ +local DefaultHttpService = game:GetService("HttpService") +local DefaultMemStorageService = game:GetService("MemStorageService") +local DefaultPlayersService = game:GetService("Players") + +return function(dependencies) + dependencies = dependencies or {} + dependencies.HttpService = dependencies.HttpService or DefaultHttpService + dependencies.MemStorageService = dependencies.MemStorageService or DefaultMemStorageService + dependencies.PlayersService = dependencies.PlayersService or DefaultPlayersService + + assert(dependencies.HttpService, "expected dependencies.HttpService") + assert(dependencies.MemStorageService, "expected dependencies.MemStorageService") + assert(dependencies.PlayersService, "expected dependencies.PlayersService") + + local HttpService = dependencies.HttpService + local MemStorageService = dependencies.MemStorageService + local PlayersService = dependencies.PlayersService + + return function(behavior) + assert(behavior, "expected behavior") + + local function getStoreKey() + local userId = -1 + if PlayersService.LocalPlayer then + userId = PlayersService.LocalPlayer.UserId + end + return "GUAC:" .. userId .. ":" .. behavior + end + + local previouslyReadValue + + return { + read = function() + local storeKey = getStoreKey() + local policyData = MemStorageService:GetItem(storeKey) + if policyData and #policyData > 0 then + local success, policy = pcall(function() + return HttpService:JSONDecode(policyData) + end) + if success then + -- Be sure to store the json string + previouslyReadValue = policyData + return policy + end + end + + return nil + end, + + onPolicyChanged = function(func) + local storeKey = getStoreKey() + local memStorageConnection = MemStorageService:BindAndFire(storeKey, function(newPolicyData) + -- MemStorageService will not du-duplicate the same item from storage + if newPolicyData ~= previouslyReadValue then + if newPolicyData and #newPolicyData > 0 then + local success, decodedExternalPolicy = pcall(function() + return HttpService:JSONDecode(newPolicyData) + end) + if success then + -- never store garbage + previouslyReadValue = newPolicyData + if func then + func(decodedExternalPolicy) + end + end + end + end + end) + + return memStorageConnection + end, + } + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/oldFromMemStorageService.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/oldFromMemStorageService.spec.lua new file mode 100644 index 0000000..04a96bc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/getPolicyImplementations/oldFromMemStorageService.spec.lua @@ -0,0 +1,273 @@ +return function() + local Packages = script.Parent.Parent.Parent + local Mock = require(Packages.Mock) + local MagicMock = Mock.MagicMock + + local fromMemStorageService = require(script.Parent.oldFromMemStorageService) + + describe("GIVEN a behavior", function() + local behavior = "mockBehavior" + describe("GIVEN a MemStorageService and HttpService with functional GetItem, BindAndFire and JSONDecode", function() + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return "jsonExternalPolicy" + end + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function() + return "decodedExternalPolicy" + end + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + HttpService = mockHttpService, + })(behavior) + it("SHOULD return the policy when `read` is invoked", function() + local result = fromMemStorageServiceInstance.read() + expect(result).to.equal("decodedExternalPolicy") + end) + + it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function() + local result = fromMemStorageServiceInstance.onPolicyChanged() + expect(result).to.be.ok() + expect(result.Disconnect).to.be.ok() + result:Disconnect() + end) + + it("SHOULD invoke passed in function with JSONDecode results when updateMemStorageService is fired", function() + local wasEverCalled = false + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + wasEverCalled = true + expect(value).to.equal("decodedExternalPolicy") + end) + + updateMemStorageService:Fire("jsonExternalPolicyUpdated") + + result:Disconnect() + + expect(wasEverCalled).to.equal(true) + end) + + it("SHOULD NOT invoke passed in function with JSONDecode results ".. + "when updateMemStorageService is fired with the same value", function() + local timesEverCalled = 0 + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + timesEverCalled = timesEverCalled + 1 + expect(value).to.equal("decodedExternalPolicy") + end) + + updateMemStorageService:Fire("foo") + updateMemStorageService:Fire("foo") + + result:Disconnect() + + expect(timesEverCalled).to.equal(1) + end) + + it("SHOULD NOT invoke passed in function with JSONDecode results ".. + "when updateMemStorageService is fired with the same value, while ignore nils", function() + local timesEverCalled = 0 + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + timesEverCalled = timesEverCalled + 1 + expect(value).to.equal("decodedExternalPolicy") + end) + + updateMemStorageService:Fire("bar") + updateMemStorageService:Fire(nil) + updateMemStorageService:Fire("bar") + + result:Disconnect() + + expect(timesEverCalled).to.equal(1) + end) + end) + + describe("GIVEN a functional MemStorageService and broken HttpService JSONDecode", function() + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return "jsonExternalPolicy" + end + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function() + return nil + end + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + HttpService = mockHttpService, + })(behavior) + it("SHOULD return nil when `read` is invoked", function() + local result = fromMemStorageServiceInstance.read() + expect(result).to.equal(nil) + end) + + it("SHOULD invoke passed in function with JSONDecode results when updateMemStorageService is fired", function() + local wasEverCalled = false + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + wasEverCalled = true + expect(value).to.equal(nil) + end) + + updateMemStorageService:Fire("jsonExternalPolicyUpdated") + + result:Disconnect() + + expect(wasEverCalled).to.equal(true) + end) + end) + + describe("GIVEN a MemStorageService that always returns invalid results", function() + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return nil + end + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + local mockHttpService = MagicMock.new() + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + HttpService = mockHttpService, + })(behavior) + it("SHOULD return nil when `read` is invoked", function() + local result = fromMemStorageServiceInstance.read() + expect(result).to.equal(nil) + end) + + it("SHOULD never invoke passed function with JSONDecode results when updateMemStorageService is fired", function() + local wasEverCalled = false + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + wasEverCalled = true + end) + + updateMemStorageService:Fire(nil) + + result:Disconnect() + + expect(wasEverCalled).to.equal(false) + end) + end) + + describe("GIVEN a PlayersService with a missing LocalPlayer", function() + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return "mockStorageItemJSON" + end + + local mockHttpService = MagicMock.new() + local mockPlayersService = MagicMock.new() + mockPlayersService.LocalPlayer = nil + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + PlayersService = mockPlayersService, + HttpService = mockHttpService, + })(behavior) + + it("SHOULD still return a value when `read` is invoked", function() + local result = fromMemStorageServiceInstance.read() + expect(result).to.be.ok() + end) + end) + + describe("GIVEN a HttpService that can throw based on MemStorageService's GetItem", function() + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return "garbage" + end + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function(_, value) + if value == "validJson" then + return { foo = true } + end + + error("invalid json") + end + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + HttpService = mockHttpService, + })(behavior) + + it("SHOULD return nil since GetItem returns garbage", function() + local result = fromMemStorageServiceInstance.read() + expect(result).to.never.be.ok() + end) + + it("SHOULD never invoke passed function with JSONDecode results when updateMemStorageService is fired", function() + local numberOfTimesCalled = 0 + local result = fromMemStorageServiceInstance.onPolicyChanged(function(value) + numberOfTimesCalled = numberOfTimesCalled + 1 + end) + + updateMemStorageService:Fire("validJson") + updateMemStorageService:Fire("garbage") + updateMemStorageService:Fire("validJson") + + result:Disconnect() + + expect(numberOfTimesCalled).to.equal(1) + end) + end) + + describe("GIVEN a new instance of fromMemStorageService", function() + local validJson = "validJson" + local mockMemStorageService = MagicMock.new() + mockMemStorageService.GetItem = function() + return validJson + end + local updateMemStorageService = Instance.new("BindableEvent") + mockMemStorageService.BindAndFire = function(_, _, func) + return updateMemStorageService.Event:Connect(function(value, a) + func(value) + end) + end + + local mockHttpService = MagicMock.new() + mockHttpService.JSONDecode = function(_, value) + return "tableFromJson" + end + + local fromMemStorageServiceInstance = fromMemStorageService({ + MemStorageService = mockMemStorageService, + HttpService = mockHttpService, + })(behavior) + + describe("WHEN read returns a value and onPolicyChanged returns the same value", function() + it("SHOULD not fire onPolicyChanged function", function() + local numberOfTimesCalled = 0 + fromMemStorageServiceInstance.onPolicyChanged(function() + numberOfTimesCalled = numberOfTimesCalled + 1 + end) + local result = fromMemStorageServiceInstance.read() + -- intentionally fire the same value that we read + updateMemStorageService:Fire(validJson) + + expect(numberOfTimesCalled).to.equal(0) + end) + end) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/init.lua new file mode 100644 index 0000000..f6b5e04 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/init.lua @@ -0,0 +1,36 @@ +local connect = require(script.connect) +local Provider = require(script.Provider) +local Logger = require(script.Logger) + +local GetPolicyImplementations = script.getPolicyImplementations +local fromMemStorageService +local fromPolicyService = require(GetPolicyImplementations.fromPolicyService) +local fromStaticSource = require(GetPolicyImplementations.fromStaticSource) + +local FFlagPolicyProviderFromMemStorageServiceFix = game:DefineFastFlag("PolicyProviderFromMemStorageServiceFix", false) + +if FFlagPolicyProviderFromMemStorageServiceFix then + fromMemStorageService = require(GetPolicyImplementations.fromMemStorageService) +else + fromMemStorageService = require(GetPolicyImplementations.oldFromMemStorageService) +end + +return { + withGetPolicyImplementation = function(getPolicyImpl) + -- assign default + assert(getPolicyImpl.read, "expected getPolicyImpl to have `read` function") + assert(getPolicyImpl.onPolicyChanged, "expected getPolicyImpl to have `onPolicyChanged` function") + return { + connect = connect(getPolicyImpl), + Provider = Provider(), + } + end, + + GetPolicyImplementations = { + MemStorageService = fromMemStorageService(), + PolicyService = fromPolicyService(), + Static = fromStaticSource(), + }, + + Logger = Logger, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/publicInterface.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/publicInterface.spec.lua new file mode 100644 index 0000000..4ab35c6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/lua-roact-policy-provider/publicInterface.spec.lua @@ -0,0 +1,82 @@ +return function() + local Packages = script.Parent.Parent + local tutils = require(Packages.tutils) + local Cryo = require(Packages.Cryo) + + -- Given an interface and a list of expected interface members, + -- this expectation will fail the test + local function expectInterface(actualInterface, expectedInterface) + -- build a list of keys that are in the actualInterface + local foundKeys = Cryo.List.map(expectedInterface, function(value) + if actualInterface[value] then + return value + end + end) + + -- compare the list of keys found in actualInterface and expectedInterface + local result = tutils.shallowEqual(foundKeys, expectedInterface) + if not result then + local differences = tutils.listDifferences(expectedInterface, foundKeys) + fail(string.format("Expected interface missing the following: %s", tutils.toString(differences))) + end + end + + describe("WHEN require is invoked", function() + local PolicyProvider = require(script.Parent) + + it("SHOULD have the following public interface", function() + expectInterface(PolicyProvider, { "withGetPolicyImplementation", "GetPolicyImplementations", "Logger" }) + end) + + it("SHOULD have all GetPolicyImplementations", function() + expectInterface(PolicyProvider.GetPolicyImplementations, { "MemStorageService", "PolicyService", "Static" }) + end) + + describe("WHEN withGetPolicyImplementation is invoked", function() + describe("GIVEN a stubbed GetPolicyImpl", function() + local stubbedGetPolicyImpl = { + read = function() + end, + onPolicyChanged = function() + end, + } + it("SHOULD have the following public interface", function() + local initializedProvider = PolicyProvider.withGetPolicyImplementation(stubbedGetPolicyImpl) + expectInterface(initializedProvider, { "connect", "Provider" }) + end) + end) + + describe("GIVEN a nothing", function() + it("SHOULD throw", function() + expect(function() + PolicyProvider.withGetPolicyImplementation() + end).to.throw() + end) + end) + + describe("GIVEN a GetPolicyImpl without a read", function() + local withoutRead = { + onPolicyChanged = function() + end + } + it("SHOULD throw", function() + expect(function() + PolicyProvider.withGetPolicyImplementation(withoutRead) + end).to.throw() + end) + end) + + describe("GIVEN a GetPolicyImpl without a onPolicyChanged", function() + local withoutOnPolicyChanged = { + read = function() + end + } + it("SHOULD throw", function() + expect(function() + PolicyProvider.withGetPolicyImplementation(withoutOnPolicyChanged) + end).to.throw() + end) + end) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/tutils.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/tutils.lua new file mode 100644 index 0000000..cb6c720 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-roact-policy-provider/tutils.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["tutils"]["tutils"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lock.toml new file mode 100644 index 0000000..e7dbb7b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/lua-symbol" +version = "0.1.0" +commit = "139fdfe6e4d4eca690887d3beb80e1514af501bf" +source = "git+https://github.com/roblox/lua-symbol#master" diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/Symbol.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/Symbol.lua new file mode 100644 index 0000000..8b6adaf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/Symbol.lua @@ -0,0 +1,43 @@ +--[[ + A 'Symbol' is an opaque marker type that can be used to signify unique + statuses. Symbols have the type 'userdata', but when printed to the console, + the name of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +--[[ + Create an unnamed Symbol. Usually, you should create a named Symbol using + Symbol.named(name) +]] +function Symbol.unnamed() + local self = newproxy(true) + + getmetatable(self).__tostring = function() + return "Unnamed Symbol" + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/Symbol.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/Symbol.spec.lua new file mode 100644 index 0000000..cde9be0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/Symbol.spec.lua @@ -0,0 +1,45 @@ +return function() + local Symbol = require(script.Parent.Symbol) + + describe("named", function() + it("should give an opaque object", function() + local symbol = Symbol.named("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = Symbol.named("foo") + + expect(tostring(symbol):find("foo")).to.be.ok() + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.named("abc") + local symbolB = Symbol.named("abc") + + expect(symbolA).never.to.equal(symbolB) + end) + end) + + describe("unnamed", function() + it("should give an opaque object", function() + local symbol = Symbol.unnamed() + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to some string", function() + local symbol = Symbol.unnamed() + + expect(tostring(symbol)).to.be.a("string") + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.unnamed() + local symbolB = Symbol.unnamed() + + expect(symbolA).never.to.equal(symbolB) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/init.lua new file mode 100644 index 0000000..6aaa8ef --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lua-symbol/lua-symbol/init.lua @@ -0,0 +1 @@ +return require(script.Symbol) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lock.toml new file mode 100644 index 0000000..6591fe5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lock.toml @@ -0,0 +1,6 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/lumberyak" +version = "0.1.1" +commit = "80926beddd32fe7686e036da2c2e6e08d42c74cf" +source = "url+https://github.com/roblox/lumberyak" +dependencies = ["Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo"] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/Logger.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/Logger.lua new file mode 100644 index 0000000..7ed85f5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/Logger.lua @@ -0,0 +1,248 @@ +local Root = script.Parent.Parent +local Cryo = require(Root.Cryo) + +local Logger = {} +Logger.__index = Logger + +Logger.Levels = { + Error = "Error", + Warning = "Warning", + Info = "Info", + Debug = "Debug", + Trace = "Trace", +} + +local levelOrder = { + Logger.Levels.Error, + Logger.Levels.Warning, + Logger.Levels.Info, + Logger.Levels.Debug, + Logger.Levels.Trace, +} + +local levelRank = {} +for k, v in pairs(levelOrder) do + levelRank[v] = k +end + +function Logger.Levels.fromString(str) + if type(str) ~= "string" then + return nil + end + for _, k in pairs(levelOrder) do + if string.lower(k) == string.lower(str) then + return k + end + end + return nil +end + +function Logger.new(parent, name) + local logger = { + name = name, + sinks = {}, + children = {}, + parent = parent, + context = {}, + dirty = true, + active = {}, + cache = { + sinks = {}, + context = {}, + } + } + + for k, _ in pairs(Logger.Levels) do + if parent then + logger.active[k] = parent.active[k] + else + logger.active[k] = false + end + end + + if parent then + parent.children[logger] = true + end + + setmetatable(logger, Logger) + return logger +end + +-- Activate `level` and above logging levels. +local function setActive(level, node) + local maxLevel = levelRank[level] + if maxLevel then + for n = 1,maxLevel do + node.active[levelOrder[n]] = true + end + for k, _ in pairs(node.children) do + setActive(level, k) + end + end +end + +-- Set the dirty flag for `node` and all its children. +local function setDirty(node) + node.dirty = true + for k, _ in pairs(node.children) do + setDirty(k) + end +end + +-- Update the context and sinks cache for `node` and its ancestors. +local function updateCache(node) + if not node.dirty then + return + end + + if not node.parent then + node.cache.context = node.context + node.cache.sinks = node.sinks + node.dirty = false + return + end + + updateCache(node.parent) + + -- Dictionary join the context. List join the sinks. Concatenate the prefixes. + node.cache.context = Cryo.Dictionary.join(node.parent.cache.context, node.context) + if node.parent.cache.context.prefix and node.context.prefix then + node.cache.context.prefix = node.parent.cache.context.prefix .. node.context.prefix + end + node.cache.sinks = Cryo.List.join(node.parent.cache.sinks, node.sinks) + node.dirty = false +end + +-- Set the parent of this Logger and update its cache, active bits and dirty bit. +function Logger:setParent(parent) + if self.parent then + self.parent.children[self] = nil + end + + updateCache(parent) + self.parent = parent + self.parent.children[self] = true + + local maxLevel = -1 + for _, sink in pairs(parent.cache.sinks) do + local sinkLevel = levelRank[sink.maxLevel] + if sinkLevel then + maxLevel = math.max(maxLevel, levelRank[sink.maxLevel]) + end + end + + if maxLevel > -1 then + setActive(levelOrder[maxLevel], self) + end + + setDirty(self) +end + +function Logger:addSink(sink) + setActive(sink.maxLevel, self) + table.insert(self.sinks, sink) + setDirty(self) +end + +function Logger:setContext(context) + self.context = context + setDirty(self) +end + +local function log(level, node, args) + if node.dirty then + updateCache(node) + end + + -- Collect per-log context. + local fullContext = { + level = level, + rawMessage = args, + loggerName = node.name, + } + + -- Call any functions in the context. + for k, v in pairs(node.cache.context) do + if type(v) == "function" then + fullContext[k] = v() + else + fullContext[k] = v + end + end + + -- Interpolate the log message. + local interpMsg + if args.n == 0 then + interpMsg = "LUMBERYAK INTERNAL: No log message given" + else + interpMsg = args[1] + end + if fullContext.prefix then + interpMsg = fullContext.prefix .. interpMsg + end + + if interpMsg:find("{") then + local i = 1 + interpMsg = (interpMsg:gsub("{(.-)}", function(w) + -- Treat {} as a positional arg. + if w == "" then + i = i + 1 + return tostring(args[i]) + end + local c = fullContext[w] + return c and tostring(c) + end)) + if i < args.n then + interpMsg = interpMsg .. "\nLUMBERYAK INTERNAL: Too many arguments given for format string" + elseif i > args.n then + interpMsg = interpMsg .. "\nLUMBERYAK INTERNAL: Too few arguments given for format string" + end + elseif args.n > 1 then + interpMsg = interpMsg .. "\nLUMBERYAK INTERNAL: Too many arguments given for format string" + end + + -- Send the message to any sinks that are listening to the right level. + local rank = levelRank[level] + for _, k in pairs(node.cache.sinks) do + if levelRank[k.maxLevel] and levelRank[k.maxLevel] >= rank then + k:log(interpMsg, fullContext) + end + end +end + +function Logger:error(...) + if not self.active[Logger.Levels.Error] then + return + end + log(Logger.Levels.Error, self, table.pack(...)) +end + +function Logger:warning(...) + if not self.active[Logger.Levels.Warning] then + return + end + log(Logger.Levels.Warning, self, table.pack(...)) +end + +function Logger:info(...) + if not self.active[Logger.Levels.Info] then + return + end + log(Logger.Levels.Info, self, table.pack(...)) +end + +function Logger:debug(...) + if not self.active[Logger.Levels.Debug] then + return + end + log(Logger.Levels.Debug, self, table.pack(...)) +end + +function Logger:trace(...) + if not self.active[Logger.Levels.Trace] then + return + end + log(Logger.Levels.Trace, self, table.pack(...)) +end + +return Logger diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/Logger.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/Logger.spec.lua new file mode 100644 index 0000000..c9177ab --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/Logger.spec.lua @@ -0,0 +1,703 @@ +return function() + local Logger = require(script.Parent.Logger) + + local function newSink(level) + return { + maxLevel = level, + seen = {}, + log = function(self, message, context) + table.insert(self.seen, {message=message, context=context}) + end, + } + end + + describe("A new Logger", function() + it("should be creatable without a parent", function() + expect(function() + local _ = Logger.new() + end).to.never.throw() + end) + + it("should be creatable with a parent", function() + expect(function() + local log1 = Logger.new() + local _ = Logger.new(log1) + end).to.never.throw() + end) + + it("should be creatable with a parent, alternate syntax", function() + expect(function() + local log1 = Logger.new() + local _ = log1:new() + end).to.never.throw() + end) + + it("should add sinks", function() + expect(function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + end).to.never.throw() + end) + + it("should add context", function() + expect(function() + local log = Logger.new() + log:setContext({foo = "bar"}) + end).to.never.throw() + end) + end) + + describe("Basic logging", function() + it("to the root logger", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo") + end) + + it("to a child logger", function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + + log2:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo") + end) + + it("to a sibling logger", function() + local log1 = Logger.new() + local log2 = log1:new() + local log3 = log1:new() + local sink = newSink(Logger.Levels.Info) + log2:addSink(sink) + + log3:info("foo") + + expect(#sink.seen).to.equal(0) + end) + + it("to a parent logger", function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(Logger.Levels.Info) + log2:addSink(sink) + + log1:info("foo") + + expect(#sink.seen).to.equal(0) + end) + + it("to a child logger, sink added first", function() + local log1 = Logger.new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + local log2 = log1:new() + + log2:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo") + end) + + it("to a sibling logger, sink added first", function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(Logger.Levels.Info) + log2:addSink(sink) + local log3 = log1:new() + + log3:info("foo") + + expect(#sink.seen).to.equal(0) + end) + end) + + describe("When logging different levels", function() + local cases = { + [Logger.Levels.Error] = 1, + [Logger.Levels.Warning] = 2, + [Logger.Levels.Info] = 3, + [Logger.Levels.Debug] = 4, + [Logger.Levels.Trace] = 5, + } + + for level, count in pairs(cases) do + it("logging to the root should respect " .. level, function() + local log = Logger.new() + local sink = newSink(level) + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(count) + end) + + it("logging to the child should respect " .. level, function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(level) + log1:addSink(sink) + + log2:error("error") + log2:warning("warning") + log2:info("info") + log2:debug("debug") + log2:trace("trace") + + expect(#sink.seen).to.equal(count) + end) + end + + describe("should treat invalid log levels as disabled", function() + it("should log to no levels when given a bad maxLevel", function() + local log = Logger.new() + local sink = newSink("not-a-level") + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(0) + end) + + it("should log to no levels when given nil", function() + local log = Logger.new() + local sink = newSink(nil) + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(0) + end) + + it("should handle multiple sinks when some are disabled", function() + local log = Logger.new() + local sink1 = newSink(nil) + local sink2 = newSink(Logger.Levels.Trace) + log:addSink(sink1) + log:addSink(sink2) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink1.seen).to.equal(0) + expect(#sink2.seen).to.equal(5) + end) + end) + end) + + describe("When logging different levels using fromString", function() + local cases = { + ["error"] = 1, + ["Warning"] = 2, + ["INFO"] = 3, + ["dEBUG"] = 4, + ["TrAcE"] = 5, + ["invalid"] = 0, + } + + for level, count in pairs(cases) do + it("fromString should handle " .. level, function() + local log = Logger.new() + local sink = newSink(Logger.Levels.fromString(level)) + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(count) + end) + end + end) + + describe("sinks", function() + it("should be disabled without error when maxLevel isn't set", function() + local log = Logger.new() + local seen = 0 + log:addSink({ + log = function() + seen = seen + 1 + end, + }) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(seen).to.equal(0) + end) + + it("should be disabled without error when maxLevel is set incorrectly", function() + local log = Logger.new() + local seen = 0 + log:addSink({ + maxLevel = "foo", + log = function() + seen = seen + 1 + end, + }) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(seen).to.equal(0) + end) + + it("should not cause problems with parenting when not set", function() + local log1 = Logger.new() + local seen = 0 + log1:addSink({ + log = function() + seen = seen + 1 + end, + }) + + local log2 = log1:new() + + log2:error("error") + log2:warning("warning") + log2:info("info") + log2:debug("debug") + log2:trace("trace") + + expect(seen).to.equal(0) + end) + end) + + describe("should treat invalid log levels as disabled", function() + it("should log to no levels when given a bad maxLevel", function() + local log = Logger.new() + local sink = newSink("not-a-level") + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(0) + end) + + it("should log to no levels when given nil", function() + local log = Logger.new() + local sink = newSink(nil) + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(0) + end) + + it("should handle multiple sinks when some are disabled", function() + local log = Logger.new() + local sink1 = newSink(nil) + local sink2 = newSink(Logger.Levels.Trace) + log:addSink(sink1) + log:addSink(sink2) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink1.seen).to.equal(0) + expect(#sink2.seen).to.equal(5) + end) + end) + + describe("with positional arguments", function() + it("should work with one arg", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {}", "bar") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo bar") + end) + + it("should work with two args", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {} {}", "bar", "baz") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo bar baz") + end) + + it("should output a warning with too many arguments", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {}", "bar", "baz") + + assert(string.find(sink.seen[1].message, "LUMBERYAK INTERNAL"), + "Expected an internal warning, got [[\n" .. sink.seen[1].message .. "\n]]") + end) + + it("should output a warning with too few arguments", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {} {}", "bar") + + assert(string.find(sink.seen[1].message, "LUMBERYAK INTERNAL"), + "Expected an internal warning, got [[\n" .. sink.seen[1].message .. "\n]]") + end) + + it("should handle a single nil positional argument", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {} bar", nil) + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo nil bar") + end) + + it("should handle a nil positional argument in the middle", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {} {}", nil, "bar") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo nil bar") + end) + + it("should handle multiple nil arguments", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {} {}", nil, nil) + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo nil nil") + end) + + it("should ignore a nil named arugment", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {notFound} bar") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo {notFound} bar") + end) + end) + + describe("When passing in context", function() + describe("with context from root", function() + it("should pass along static context", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({bar = 1}) + + log:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].context.bar).to.equal(1) + end) + + it("should call dynamic context", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({bar = function() + return 1 + end}) + + log:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].context.bar).to.equal(1) + end) + end) + + describe("when combining context", function() + it("should merge non-overlapping context", function() + local log1 = Logger.new() + local log2 = log1:new() + local log3 = log2:new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + log1:setContext({bar = 1}) + log2:setContext({baz = 2}) + log3:setContext({quz = 3}) + + log3:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].context.bar).to.equal(1) + expect(sink.seen[1].context.baz).to.equal(2) + expect(sink.seen[1].context.quz).to.equal(3) + end) + + it("should overwrite overlapping context", function() + local log1 = Logger.new() + local log2 = log1:new() + local log3 = log2:new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + log1:setContext({bar = 1, baz = 1, quz = 1}) + log2:setContext({bar = 2, baz = 2}) + log3:setContext({baz = 3}) + + log3:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].context.quz).to.equal(1) + expect(sink.seen[1].context.bar).to.equal(2) + expect(sink.seen[1].context.baz).to.equal(3) + end) + + it("should call dynamic context", function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + log1:setContext({bar = 1}) + log2:setContext({bar = function() + return 2 + end}) + + log2:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].context.bar).to.equal(2) + end) + end) + end) + + describe("Message interpolation", function() + it("should leave plain messages alone", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({bar = "baz"}) + + log:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo") + end) + + it("should substitute info from static context", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({bar = "baz"}) + + log:info("foo {bar}") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo baz") + end) + + it("should substitute info from dynamic context", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({bar = function() + return "baz" + end}) + + log:info("foo {bar}") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo baz") + end) + end) + + describe("Message prefix", function() + it("should prepend the prefix", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({prefix = "foo: "}) + + log:info("bar") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo: bar") + end) + + it("should interpolate the prefix", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({prefix = "{foo}: ", foo = "baz"}) + + log:info("bar") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("baz: bar") + end) + + it("should stack prefixes", function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + log1:setContext({prefix = "foo: "}) + log2:setContext({prefix = "bar: "}) + + log2:info("baz") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo: bar: baz") + end) + end) + + it("Should get the name of logger used", function() + local log1 = Logger.new(nil, "log1") + local log2 = log1:new("log2") + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + + log1:info("{loggerName}") + log2:info("{loggerName}") + + expect(#sink.seen).to.equal(2) + expect(sink.seen[1].message).to.equal("log1") + expect(sink.seen[2].message).to.equal("log2") + end) + + describe("setParent should work", function() + it("when calling A->B then B->C", function() + local a = Logger.new() + local b = Logger.new() + local c = Logger.new() + + local sink = newSink(Logger.Levels.Info) + a:addSink(sink) + + a:setContext({a = "A"}) + b:setContext({b = "B"}) + c:setContext({c = "C"}) + + b:setParent(a) + c:setParent(b) + + c:info("{a} {b} {c}") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("A B C") + end) + + it("when calling B->C then A->B", function() + local a = Logger.new() + local b = Logger.new() + local c = Logger.new() + + local sink = newSink(Logger.Levels.Info) + a:addSink(sink) + + a:setContext({a = "A"}) + b:setContext({b = "B"}) + c:setContext({c = "C"}) + + c:setParent(b) + b:setParent(a) + + c:info("{a} {b} {c}") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("A B C") + end) + + it("when moving D from B to C", function() + local a = Logger.new() + local b = Logger.new() + local c = Logger.new() + local d = Logger.new() + + local sink = newSink(Logger.Levels.Info) + a:addSink(sink) + + a:setContext({x = "A"}) + b:setContext({y = "B"}) + c:setContext({y = "C"}) + d:setContext({z = "D"}) + + c:setParent(a) + b:setParent(a) + + d:setParent(b) + d:info("{x} {y} {z}") + + d:setParent(c) + d:info("{x} {y} {z}") + + expect(#sink.seen).to.equal(2) + expect(sink.seen[1].message).to.equal("A B D") + expect(sink.seen[2].message).to.equal("A C D") + end) + + it("when mixing setParent and static parents", function() + local a = Logger.new() + local b = Logger.new() + local c = b:new() + + local sink = newSink(Logger.Levels.Info) + a:addSink(sink) + + a:setContext({a = "A"}) + b:setContext({b = "B"}) + c:setContext({c = "C"}) + + b:setParent(a) + + c:info("{a} {b} {c}") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("A B C") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/MockLogger.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/MockLogger.lua new file mode 100644 index 0000000..1e63570 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/MockLogger.lua @@ -0,0 +1,47 @@ +-- This mock has the same API as the Logger, but none of the calls do anything. +-- Note that MockLogger and Logger can't be parented to each other. + +local MockLogger = {} + +MockLogger.Levels = { + Error = "MockError", + Warning = "MockWarning", + Info = "MockInfo", + Debug = "MockDebug", + Trace = "MockTrace", + fromString = function() + return "MockInfo" + end, +} + +MockLogger.__index = MockLogger + +function MockLogger.new() + return setmetatable({}, MockLogger) +end + +function MockLogger.setParent() +end + +function MockLogger.setContext() +end + +function MockLogger.addSink() +end + +function MockLogger.error() +end + +function MockLogger.warning() +end + +function MockLogger.info() +end + +function MockLogger.debug() +end + +function MockLogger.trace() +end + +return MockLogger diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/MockLogger.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/MockLogger.spec.lua new file mode 100644 index 0000000..c7fcbe7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/MockLogger.spec.lua @@ -0,0 +1,22 @@ +return function() + local MockLogger = require(script.Parent.MockLogger) + local Logger = require(script.Parent.Logger) + + it("MockLogger should have the same API as Logger", function() + for k, v in pairs(Logger) do + local mock = MockLogger[k] + assert(mock, "Expected a mock of " .. k) + assert(type(mock) == type(v), + "Expected the type of " .. k .. " to be " .. type(v) .. ", got " .. type(mock)) + end + end) + + it("MockLogger.Levels should have the same API as Logger.Levels", function() + for k, v in pairs(Logger.Levels) do + local mock = MockLogger.Levels[k] + assert(mock, "Expected a mock of " .. k) + assert(type(mock) == type(v), + "Expected the type of " .. k .. " to be " .. type(v) .. ", got " .. type(mock)) + end + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/benchmark/benchmark.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/benchmark/benchmark.lua new file mode 100644 index 0000000..4fe0c15 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/benchmark/benchmark.lua @@ -0,0 +1,159 @@ +local Logger = require(game.LoadedCode.Packages.Lumberyak.Logger) + +local runs = 2000000 + +local function timeit(makeLogger, callLogger) + local log = makeLogger() + + local t = tick() + for _ = 1, runs do + callLogger(log) + end + return (tick() - t) / runs +end + +local function callSimple(log) + log:info("foo") +end + +local function callInterp(log) + log:info("foo {}", 2) +end + +local function simple() + local count = 0 + local countSink = { + maxLevel = Logger.Levels.Info, + log = function(_, _, context) + count = count + context.number + end, + } + + local log = Logger.new() + log:setContext({number = 1}) + log:addSink(countSink) + return log +end + +local function empty() + return { + info = function() end + } +end + +local function off() + local count = 0 + local countSink = { + maxLevel = Logger.Levels.Error, + log = function(_, _, context) + count = count + context.number + end, + } + + local log = Logger.new() + log:setContext({number = 1}) + log:addSink(countSink) + return log +end + +local function short() + local count = 0 + local countSink = { + maxLevel = Logger.Levels.Info, + log = function(_, _, context) + count = count + context.number + end, + } + + local log = Logger.new() + local child = log:new() + log:setContext({number = 1}) + log:addSink(countSink) + return child +end + +local function long() + local count = 0 + local countSink = { + maxLevel = Logger.Levels.Info, + log = function(_, _, context) + count = count + context.number + end, + } + + local log = Logger.new() + log:setContext({number = 1}) + log:addSink(countSink) + local a = log:new() + local b = a:new() + local c = b:new() + local d = c:new() + return d +end + +local function many() + local count = 0 + local sink1 = { + maxLevel = Logger.Levels.Error, + log = function(_, _, context) + count = count + context.number + end, + } + local sink2 = { + maxLevel = Logger.Levels.Warning, + log = function(_, _, context) + count = count + context.number + end, + } + local sink3 = { + maxLevel = Logger.Levels.Info, + log = function(_, _, context) + count = count + context.number + end, + } + local sink4 = { + maxLevel = Logger.Levels.Debug, + log = function(_, _, context) + count = count + context.number + end, + } + local sink5 = { + maxLevel = Logger.Levels.Trace, + log = function(_, _, context) + count = count + context.number + end, + } + + local log = Logger.new() + log:setContext({number = 1}) + log:addSink(sink1) + log:addSink(sink2) + log:addSink(sink3) + log:addSink(sink4) + log:addSink(sink5) + return log +end + +local base = timeit(empty, callSimple) +local interp = timeit(empty, callInterp) + +local tests = { + ["Log level off"] = off, + ["Simple logger"] = simple, + ["Short chain"] = short, + ["Long chain"] = long, + ["Many sinks"] = many, +} + +local function fmt(name, case, t) + print(string.format("%-40s %0.3e [%0.4fx]", name .. " - " .. case, t, t / base)) +end + +fmt("Empty function", "Simple", base) +fmt("Empty function", "Interpolation", interp) +for k, v in pairs(tests) do + local t1 = timeit(v, callSimple) + fmt(k, "Simple", t1) + local t2 = timeit(v, callInterp) + fmt(k, "Interpolation", t2) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/app/PrintSink.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/app/PrintSink.lua new file mode 100644 index 0000000..90b6ff3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/app/PrintSink.lua @@ -0,0 +1,24 @@ +local Logger = require(script.Parent.Parent.Parent.Logger) + +local PrintSink = {} + +function PrintSink.new(level) + local printer = { + maxLevel = level + } + + setmetatable(printer, PrintSink) + return printer +end + +function PrintSink:log(message, context) + if context.level == Logger.Levels.Error then + error(message, 5) + elseif context.level == Logger.Levels.Warning then + warn(message) + else + print(message) + end +end + +return PrintSink diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/app/app.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/app/app.lua new file mode 100644 index 0000000..2d1f265 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/app/app.lua @@ -0,0 +1,13 @@ +local log = require(script.Parent.appLogger) +-- The app has some way to import the page, but not vice versa. +local page = require(script.Parent.Parent.page.page) +local printer = require(script.Parent.PrintSink) + +log:addSink(printer.new(log.Levels.Error)) + +return function() + log:info("calling {root}") + page.init(log) + + return page.doSomething() +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/app/appLogger.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/app/appLogger.lua new file mode 100644 index 0000000..a02f33d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/app/appLogger.lua @@ -0,0 +1,4 @@ +local Logger = require(script.Parent.Parent.Parent.Logger) +local log = Logger.new() +log:setContext({root = "root"}) +return log diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/example.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/example.spec.lua new file mode 100644 index 0000000..c74c9ea --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/example.spec.lua @@ -0,0 +1,26 @@ +return function() + local app = require(script.Parent.app.app) + local log = require(script.Parent.app.appLogger) + + local function newSink(level) + return { + maxLevel = level, + seen = {}, + log = function(self, message, context) + table.insert(self.seen, {message=message, context=context}) + end, + } + end + + it("should generate expected messages", function() + local sink = newSink(log.Levels.Info) + log:addSink(sink) + + local result = app() + expect(result).to.equal("done") + expect(#sink.seen).to.equal(3) + expect(sink.seen[1].message).to.equal("calling root") + expect(sink.seen[2].message).to.equal("calling root foo") + expect(sink.seen[3].message).to.equal("calling root foo bar") + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/page/component.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/page/component.lua new file mode 100644 index 0000000..21e165c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/page/component.lua @@ -0,0 +1,7 @@ +local log = require(script.Parent.pageLogger):new() +log:setContext({bar = "bar"}) + +return function() + log:info("calling {root} {foo} {bar}") + return "done" +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/page/page.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/page/page.lua new file mode 100644 index 0000000..324b9ec --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/page/page.lua @@ -0,0 +1,13 @@ +-- foo does not need to require root +local log = require(script.Parent.pageLogger) +local component = require(script.Parent.component) + +return { + init = function(logParent) + log:setParent(logParent) + end, + doSomething = function() + log:info("calling {root} {foo}") + return component() + end +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/page/pageLogger.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/page/pageLogger.lua new file mode 100644 index 0000000..d8d0b89 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/example/page/pageLogger.lua @@ -0,0 +1,4 @@ +local Logger = require(script.Parent.Parent.Parent.Logger) +local log = Logger.new() +log:setContext({foo = "foo"}) +return log diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/init.lua new file mode 100644 index 0000000..ddbb40f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-6ce30d59-0.1.1/lumberyak/init.lua @@ -0,0 +1,3 @@ +return { + Logger = require(script.Logger), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lock.toml new file mode 100644 index 0000000..ebecec2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lock.toml @@ -0,0 +1,6 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/lumberyak" +version = "0.1.1" +commit = "76fee23fcefb988bfe97b4825dad792eb57c0e04" +source = "git+https://github.com/roblox/lumberyak#master" +dependencies = ["Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo"] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/Logger.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/Logger.lua new file mode 100644 index 0000000..7ed85f5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/Logger.lua @@ -0,0 +1,248 @@ +local Root = script.Parent.Parent +local Cryo = require(Root.Cryo) + +local Logger = {} +Logger.__index = Logger + +Logger.Levels = { + Error = "Error", + Warning = "Warning", + Info = "Info", + Debug = "Debug", + Trace = "Trace", +} + +local levelOrder = { + Logger.Levels.Error, + Logger.Levels.Warning, + Logger.Levels.Info, + Logger.Levels.Debug, + Logger.Levels.Trace, +} + +local levelRank = {} +for k, v in pairs(levelOrder) do + levelRank[v] = k +end + +function Logger.Levels.fromString(str) + if type(str) ~= "string" then + return nil + end + for _, k in pairs(levelOrder) do + if string.lower(k) == string.lower(str) then + return k + end + end + return nil +end + +function Logger.new(parent, name) + local logger = { + name = name, + sinks = {}, + children = {}, + parent = parent, + context = {}, + dirty = true, + active = {}, + cache = { + sinks = {}, + context = {}, + } + } + + for k, _ in pairs(Logger.Levels) do + if parent then + logger.active[k] = parent.active[k] + else + logger.active[k] = false + end + end + + if parent then + parent.children[logger] = true + end + + setmetatable(logger, Logger) + return logger +end + +-- Activate `level` and above logging levels. +local function setActive(level, node) + local maxLevel = levelRank[level] + if maxLevel then + for n = 1,maxLevel do + node.active[levelOrder[n]] = true + end + for k, _ in pairs(node.children) do + setActive(level, k) + end + end +end + +-- Set the dirty flag for `node` and all its children. +local function setDirty(node) + node.dirty = true + for k, _ in pairs(node.children) do + setDirty(k) + end +end + +-- Update the context and sinks cache for `node` and its ancestors. +local function updateCache(node) + if not node.dirty then + return + end + + if not node.parent then + node.cache.context = node.context + node.cache.sinks = node.sinks + node.dirty = false + return + end + + updateCache(node.parent) + + -- Dictionary join the context. List join the sinks. Concatenate the prefixes. + node.cache.context = Cryo.Dictionary.join(node.parent.cache.context, node.context) + if node.parent.cache.context.prefix and node.context.prefix then + node.cache.context.prefix = node.parent.cache.context.prefix .. node.context.prefix + end + node.cache.sinks = Cryo.List.join(node.parent.cache.sinks, node.sinks) + node.dirty = false +end + +-- Set the parent of this Logger and update its cache, active bits and dirty bit. +function Logger:setParent(parent) + if self.parent then + self.parent.children[self] = nil + end + + updateCache(parent) + self.parent = parent + self.parent.children[self] = true + + local maxLevel = -1 + for _, sink in pairs(parent.cache.sinks) do + local sinkLevel = levelRank[sink.maxLevel] + if sinkLevel then + maxLevel = math.max(maxLevel, levelRank[sink.maxLevel]) + end + end + + if maxLevel > -1 then + setActive(levelOrder[maxLevel], self) + end + + setDirty(self) +end + +function Logger:addSink(sink) + setActive(sink.maxLevel, self) + table.insert(self.sinks, sink) + setDirty(self) +end + +function Logger:setContext(context) + self.context = context + setDirty(self) +end + +local function log(level, node, args) + if node.dirty then + updateCache(node) + end + + -- Collect per-log context. + local fullContext = { + level = level, + rawMessage = args, + loggerName = node.name, + } + + -- Call any functions in the context. + for k, v in pairs(node.cache.context) do + if type(v) == "function" then + fullContext[k] = v() + else + fullContext[k] = v + end + end + + -- Interpolate the log message. + local interpMsg + if args.n == 0 then + interpMsg = "LUMBERYAK INTERNAL: No log message given" + else + interpMsg = args[1] + end + if fullContext.prefix then + interpMsg = fullContext.prefix .. interpMsg + end + + if interpMsg:find("{") then + local i = 1 + interpMsg = (interpMsg:gsub("{(.-)}", function(w) + -- Treat {} as a positional arg. + if w == "" then + i = i + 1 + return tostring(args[i]) + end + local c = fullContext[w] + return c and tostring(c) + end)) + if i < args.n then + interpMsg = interpMsg .. "\nLUMBERYAK INTERNAL: Too many arguments given for format string" + elseif i > args.n then + interpMsg = interpMsg .. "\nLUMBERYAK INTERNAL: Too few arguments given for format string" + end + elseif args.n > 1 then + interpMsg = interpMsg .. "\nLUMBERYAK INTERNAL: Too many arguments given for format string" + end + + -- Send the message to any sinks that are listening to the right level. + local rank = levelRank[level] + for _, k in pairs(node.cache.sinks) do + if levelRank[k.maxLevel] and levelRank[k.maxLevel] >= rank then + k:log(interpMsg, fullContext) + end + end +end + +function Logger:error(...) + if not self.active[Logger.Levels.Error] then + return + end + log(Logger.Levels.Error, self, table.pack(...)) +end + +function Logger:warning(...) + if not self.active[Logger.Levels.Warning] then + return + end + log(Logger.Levels.Warning, self, table.pack(...)) +end + +function Logger:info(...) + if not self.active[Logger.Levels.Info] then + return + end + log(Logger.Levels.Info, self, table.pack(...)) +end + +function Logger:debug(...) + if not self.active[Logger.Levels.Debug] then + return + end + log(Logger.Levels.Debug, self, table.pack(...)) +end + +function Logger:trace(...) + if not self.active[Logger.Levels.Trace] then + return + end + log(Logger.Levels.Trace, self, table.pack(...)) +end + +return Logger diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/Logger.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/Logger.spec.lua new file mode 100644 index 0000000..c9177ab --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/Logger.spec.lua @@ -0,0 +1,703 @@ +return function() + local Logger = require(script.Parent.Logger) + + local function newSink(level) + return { + maxLevel = level, + seen = {}, + log = function(self, message, context) + table.insert(self.seen, {message=message, context=context}) + end, + } + end + + describe("A new Logger", function() + it("should be creatable without a parent", function() + expect(function() + local _ = Logger.new() + end).to.never.throw() + end) + + it("should be creatable with a parent", function() + expect(function() + local log1 = Logger.new() + local _ = Logger.new(log1) + end).to.never.throw() + end) + + it("should be creatable with a parent, alternate syntax", function() + expect(function() + local log1 = Logger.new() + local _ = log1:new() + end).to.never.throw() + end) + + it("should add sinks", function() + expect(function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + end).to.never.throw() + end) + + it("should add context", function() + expect(function() + local log = Logger.new() + log:setContext({foo = "bar"}) + end).to.never.throw() + end) + end) + + describe("Basic logging", function() + it("to the root logger", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo") + end) + + it("to a child logger", function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + + log2:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo") + end) + + it("to a sibling logger", function() + local log1 = Logger.new() + local log2 = log1:new() + local log3 = log1:new() + local sink = newSink(Logger.Levels.Info) + log2:addSink(sink) + + log3:info("foo") + + expect(#sink.seen).to.equal(0) + end) + + it("to a parent logger", function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(Logger.Levels.Info) + log2:addSink(sink) + + log1:info("foo") + + expect(#sink.seen).to.equal(0) + end) + + it("to a child logger, sink added first", function() + local log1 = Logger.new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + local log2 = log1:new() + + log2:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo") + end) + + it("to a sibling logger, sink added first", function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(Logger.Levels.Info) + log2:addSink(sink) + local log3 = log1:new() + + log3:info("foo") + + expect(#sink.seen).to.equal(0) + end) + end) + + describe("When logging different levels", function() + local cases = { + [Logger.Levels.Error] = 1, + [Logger.Levels.Warning] = 2, + [Logger.Levels.Info] = 3, + [Logger.Levels.Debug] = 4, + [Logger.Levels.Trace] = 5, + } + + for level, count in pairs(cases) do + it("logging to the root should respect " .. level, function() + local log = Logger.new() + local sink = newSink(level) + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(count) + end) + + it("logging to the child should respect " .. level, function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(level) + log1:addSink(sink) + + log2:error("error") + log2:warning("warning") + log2:info("info") + log2:debug("debug") + log2:trace("trace") + + expect(#sink.seen).to.equal(count) + end) + end + + describe("should treat invalid log levels as disabled", function() + it("should log to no levels when given a bad maxLevel", function() + local log = Logger.new() + local sink = newSink("not-a-level") + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(0) + end) + + it("should log to no levels when given nil", function() + local log = Logger.new() + local sink = newSink(nil) + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(0) + end) + + it("should handle multiple sinks when some are disabled", function() + local log = Logger.new() + local sink1 = newSink(nil) + local sink2 = newSink(Logger.Levels.Trace) + log:addSink(sink1) + log:addSink(sink2) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink1.seen).to.equal(0) + expect(#sink2.seen).to.equal(5) + end) + end) + end) + + describe("When logging different levels using fromString", function() + local cases = { + ["error"] = 1, + ["Warning"] = 2, + ["INFO"] = 3, + ["dEBUG"] = 4, + ["TrAcE"] = 5, + ["invalid"] = 0, + } + + for level, count in pairs(cases) do + it("fromString should handle " .. level, function() + local log = Logger.new() + local sink = newSink(Logger.Levels.fromString(level)) + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(count) + end) + end + end) + + describe("sinks", function() + it("should be disabled without error when maxLevel isn't set", function() + local log = Logger.new() + local seen = 0 + log:addSink({ + log = function() + seen = seen + 1 + end, + }) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(seen).to.equal(0) + end) + + it("should be disabled without error when maxLevel is set incorrectly", function() + local log = Logger.new() + local seen = 0 + log:addSink({ + maxLevel = "foo", + log = function() + seen = seen + 1 + end, + }) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(seen).to.equal(0) + end) + + it("should not cause problems with parenting when not set", function() + local log1 = Logger.new() + local seen = 0 + log1:addSink({ + log = function() + seen = seen + 1 + end, + }) + + local log2 = log1:new() + + log2:error("error") + log2:warning("warning") + log2:info("info") + log2:debug("debug") + log2:trace("trace") + + expect(seen).to.equal(0) + end) + end) + + describe("should treat invalid log levels as disabled", function() + it("should log to no levels when given a bad maxLevel", function() + local log = Logger.new() + local sink = newSink("not-a-level") + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(0) + end) + + it("should log to no levels when given nil", function() + local log = Logger.new() + local sink = newSink(nil) + log:addSink(sink) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink.seen).to.equal(0) + end) + + it("should handle multiple sinks when some are disabled", function() + local log = Logger.new() + local sink1 = newSink(nil) + local sink2 = newSink(Logger.Levels.Trace) + log:addSink(sink1) + log:addSink(sink2) + + log:error("error") + log:warning("warning") + log:info("info") + log:debug("debug") + log:trace("trace") + + expect(#sink1.seen).to.equal(0) + expect(#sink2.seen).to.equal(5) + end) + end) + + describe("with positional arguments", function() + it("should work with one arg", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {}", "bar") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo bar") + end) + + it("should work with two args", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {} {}", "bar", "baz") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo bar baz") + end) + + it("should output a warning with too many arguments", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {}", "bar", "baz") + + assert(string.find(sink.seen[1].message, "LUMBERYAK INTERNAL"), + "Expected an internal warning, got [[\n" .. sink.seen[1].message .. "\n]]") + end) + + it("should output a warning with too few arguments", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {} {}", "bar") + + assert(string.find(sink.seen[1].message, "LUMBERYAK INTERNAL"), + "Expected an internal warning, got [[\n" .. sink.seen[1].message .. "\n]]") + end) + + it("should handle a single nil positional argument", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {} bar", nil) + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo nil bar") + end) + + it("should handle a nil positional argument in the middle", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {} {}", nil, "bar") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo nil bar") + end) + + it("should handle multiple nil arguments", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {} {}", nil, nil) + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo nil nil") + end) + + it("should ignore a nil named arugment", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + + log:info("foo {notFound} bar") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo {notFound} bar") + end) + end) + + describe("When passing in context", function() + describe("with context from root", function() + it("should pass along static context", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({bar = 1}) + + log:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].context.bar).to.equal(1) + end) + + it("should call dynamic context", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({bar = function() + return 1 + end}) + + log:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].context.bar).to.equal(1) + end) + end) + + describe("when combining context", function() + it("should merge non-overlapping context", function() + local log1 = Logger.new() + local log2 = log1:new() + local log3 = log2:new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + log1:setContext({bar = 1}) + log2:setContext({baz = 2}) + log3:setContext({quz = 3}) + + log3:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].context.bar).to.equal(1) + expect(sink.seen[1].context.baz).to.equal(2) + expect(sink.seen[1].context.quz).to.equal(3) + end) + + it("should overwrite overlapping context", function() + local log1 = Logger.new() + local log2 = log1:new() + local log3 = log2:new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + log1:setContext({bar = 1, baz = 1, quz = 1}) + log2:setContext({bar = 2, baz = 2}) + log3:setContext({baz = 3}) + + log3:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].context.quz).to.equal(1) + expect(sink.seen[1].context.bar).to.equal(2) + expect(sink.seen[1].context.baz).to.equal(3) + end) + + it("should call dynamic context", function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + log1:setContext({bar = 1}) + log2:setContext({bar = function() + return 2 + end}) + + log2:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].context.bar).to.equal(2) + end) + end) + end) + + describe("Message interpolation", function() + it("should leave plain messages alone", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({bar = "baz"}) + + log:info("foo") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo") + end) + + it("should substitute info from static context", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({bar = "baz"}) + + log:info("foo {bar}") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo baz") + end) + + it("should substitute info from dynamic context", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({bar = function() + return "baz" + end}) + + log:info("foo {bar}") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo baz") + end) + end) + + describe("Message prefix", function() + it("should prepend the prefix", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({prefix = "foo: "}) + + log:info("bar") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo: bar") + end) + + it("should interpolate the prefix", function() + local log = Logger.new() + local sink = newSink(Logger.Levels.Info) + log:addSink(sink) + log:setContext({prefix = "{foo}: ", foo = "baz"}) + + log:info("bar") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("baz: bar") + end) + + it("should stack prefixes", function() + local log1 = Logger.new() + local log2 = log1:new() + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + log1:setContext({prefix = "foo: "}) + log2:setContext({prefix = "bar: "}) + + log2:info("baz") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("foo: bar: baz") + end) + end) + + it("Should get the name of logger used", function() + local log1 = Logger.new(nil, "log1") + local log2 = log1:new("log2") + local sink = newSink(Logger.Levels.Info) + log1:addSink(sink) + + log1:info("{loggerName}") + log2:info("{loggerName}") + + expect(#sink.seen).to.equal(2) + expect(sink.seen[1].message).to.equal("log1") + expect(sink.seen[2].message).to.equal("log2") + end) + + describe("setParent should work", function() + it("when calling A->B then B->C", function() + local a = Logger.new() + local b = Logger.new() + local c = Logger.new() + + local sink = newSink(Logger.Levels.Info) + a:addSink(sink) + + a:setContext({a = "A"}) + b:setContext({b = "B"}) + c:setContext({c = "C"}) + + b:setParent(a) + c:setParent(b) + + c:info("{a} {b} {c}") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("A B C") + end) + + it("when calling B->C then A->B", function() + local a = Logger.new() + local b = Logger.new() + local c = Logger.new() + + local sink = newSink(Logger.Levels.Info) + a:addSink(sink) + + a:setContext({a = "A"}) + b:setContext({b = "B"}) + c:setContext({c = "C"}) + + c:setParent(b) + b:setParent(a) + + c:info("{a} {b} {c}") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("A B C") + end) + + it("when moving D from B to C", function() + local a = Logger.new() + local b = Logger.new() + local c = Logger.new() + local d = Logger.new() + + local sink = newSink(Logger.Levels.Info) + a:addSink(sink) + + a:setContext({x = "A"}) + b:setContext({y = "B"}) + c:setContext({y = "C"}) + d:setContext({z = "D"}) + + c:setParent(a) + b:setParent(a) + + d:setParent(b) + d:info("{x} {y} {z}") + + d:setParent(c) + d:info("{x} {y} {z}") + + expect(#sink.seen).to.equal(2) + expect(sink.seen[1].message).to.equal("A B D") + expect(sink.seen[2].message).to.equal("A C D") + end) + + it("when mixing setParent and static parents", function() + local a = Logger.new() + local b = Logger.new() + local c = b:new() + + local sink = newSink(Logger.Levels.Info) + a:addSink(sink) + + a:setContext({a = "A"}) + b:setContext({b = "B"}) + c:setContext({c = "C"}) + + b:setParent(a) + + c:info("{a} {b} {c}") + + expect(#sink.seen).to.equal(1) + expect(sink.seen[1].message).to.equal("A B C") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/MockLogger.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/MockLogger.lua new file mode 100644 index 0000000..1e63570 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/MockLogger.lua @@ -0,0 +1,47 @@ +-- This mock has the same API as the Logger, but none of the calls do anything. +-- Note that MockLogger and Logger can't be parented to each other. + +local MockLogger = {} + +MockLogger.Levels = { + Error = "MockError", + Warning = "MockWarning", + Info = "MockInfo", + Debug = "MockDebug", + Trace = "MockTrace", + fromString = function() + return "MockInfo" + end, +} + +MockLogger.__index = MockLogger + +function MockLogger.new() + return setmetatable({}, MockLogger) +end + +function MockLogger.setParent() +end + +function MockLogger.setContext() +end + +function MockLogger.addSink() +end + +function MockLogger.error() +end + +function MockLogger.warning() +end + +function MockLogger.info() +end + +function MockLogger.debug() +end + +function MockLogger.trace() +end + +return MockLogger diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/MockLogger.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/MockLogger.spec.lua new file mode 100644 index 0000000..c7fcbe7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/MockLogger.spec.lua @@ -0,0 +1,22 @@ +return function() + local MockLogger = require(script.Parent.MockLogger) + local Logger = require(script.Parent.Logger) + + it("MockLogger should have the same API as Logger", function() + for k, v in pairs(Logger) do + local mock = MockLogger[k] + assert(mock, "Expected a mock of " .. k) + assert(type(mock) == type(v), + "Expected the type of " .. k .. " to be " .. type(v) .. ", got " .. type(mock)) + end + end) + + it("MockLogger.Levels should have the same API as Logger.Levels", function() + for k, v in pairs(Logger.Levels) do + local mock = MockLogger.Levels[k] + assert(mock, "Expected a mock of " .. k) + assert(type(mock) == type(v), + "Expected the type of " .. k .. " to be " .. type(v) .. ", got " .. type(mock)) + end + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/benchmark/benchmark.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/benchmark/benchmark.lua new file mode 100644 index 0000000..4fe0c15 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/benchmark/benchmark.lua @@ -0,0 +1,159 @@ +local Logger = require(game.LoadedCode.Packages.Lumberyak.Logger) + +local runs = 2000000 + +local function timeit(makeLogger, callLogger) + local log = makeLogger() + + local t = tick() + for _ = 1, runs do + callLogger(log) + end + return (tick() - t) / runs +end + +local function callSimple(log) + log:info("foo") +end + +local function callInterp(log) + log:info("foo {}", 2) +end + +local function simple() + local count = 0 + local countSink = { + maxLevel = Logger.Levels.Info, + log = function(_, _, context) + count = count + context.number + end, + } + + local log = Logger.new() + log:setContext({number = 1}) + log:addSink(countSink) + return log +end + +local function empty() + return { + info = function() end + } +end + +local function off() + local count = 0 + local countSink = { + maxLevel = Logger.Levels.Error, + log = function(_, _, context) + count = count + context.number + end, + } + + local log = Logger.new() + log:setContext({number = 1}) + log:addSink(countSink) + return log +end + +local function short() + local count = 0 + local countSink = { + maxLevel = Logger.Levels.Info, + log = function(_, _, context) + count = count + context.number + end, + } + + local log = Logger.new() + local child = log:new() + log:setContext({number = 1}) + log:addSink(countSink) + return child +end + +local function long() + local count = 0 + local countSink = { + maxLevel = Logger.Levels.Info, + log = function(_, _, context) + count = count + context.number + end, + } + + local log = Logger.new() + log:setContext({number = 1}) + log:addSink(countSink) + local a = log:new() + local b = a:new() + local c = b:new() + local d = c:new() + return d +end + +local function many() + local count = 0 + local sink1 = { + maxLevel = Logger.Levels.Error, + log = function(_, _, context) + count = count + context.number + end, + } + local sink2 = { + maxLevel = Logger.Levels.Warning, + log = function(_, _, context) + count = count + context.number + end, + } + local sink3 = { + maxLevel = Logger.Levels.Info, + log = function(_, _, context) + count = count + context.number + end, + } + local sink4 = { + maxLevel = Logger.Levels.Debug, + log = function(_, _, context) + count = count + context.number + end, + } + local sink5 = { + maxLevel = Logger.Levels.Trace, + log = function(_, _, context) + count = count + context.number + end, + } + + local log = Logger.new() + log:setContext({number = 1}) + log:addSink(sink1) + log:addSink(sink2) + log:addSink(sink3) + log:addSink(sink4) + log:addSink(sink5) + return log +end + +local base = timeit(empty, callSimple) +local interp = timeit(empty, callInterp) + +local tests = { + ["Log level off"] = off, + ["Simple logger"] = simple, + ["Short chain"] = short, + ["Long chain"] = long, + ["Many sinks"] = many, +} + +local function fmt(name, case, t) + print(string.format("%-40s %0.3e [%0.4fx]", name .. " - " .. case, t, t / base)) +end + +fmt("Empty function", "Simple", base) +fmt("Empty function", "Interpolation", interp) +for k, v in pairs(tests) do + local t1 = timeit(v, callSimple) + fmt(k, "Simple", t1) + local t2 = timeit(v, callInterp) + fmt(k, "Interpolation", t2) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/app/PrintSink.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/app/PrintSink.lua new file mode 100644 index 0000000..90b6ff3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/app/PrintSink.lua @@ -0,0 +1,24 @@ +local Logger = require(script.Parent.Parent.Parent.Logger) + +local PrintSink = {} + +function PrintSink.new(level) + local printer = { + maxLevel = level + } + + setmetatable(printer, PrintSink) + return printer +end + +function PrintSink:log(message, context) + if context.level == Logger.Levels.Error then + error(message, 5) + elseif context.level == Logger.Levels.Warning then + warn(message) + else + print(message) + end +end + +return PrintSink diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/app/app.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/app/app.lua new file mode 100644 index 0000000..2d1f265 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/app/app.lua @@ -0,0 +1,13 @@ +local log = require(script.Parent.appLogger) +-- The app has some way to import the page, but not vice versa. +local page = require(script.Parent.Parent.page.page) +local printer = require(script.Parent.PrintSink) + +log:addSink(printer.new(log.Levels.Error)) + +return function() + log:info("calling {root}") + page.init(log) + + return page.doSomething() +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/app/appLogger.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/app/appLogger.lua new file mode 100644 index 0000000..a02f33d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/app/appLogger.lua @@ -0,0 +1,4 @@ +local Logger = require(script.Parent.Parent.Parent.Logger) +local log = Logger.new() +log:setContext({root = "root"}) +return log diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/example.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/example.spec.lua new file mode 100644 index 0000000..c74c9ea --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/example.spec.lua @@ -0,0 +1,26 @@ +return function() + local app = require(script.Parent.app.app) + local log = require(script.Parent.app.appLogger) + + local function newSink(level) + return { + maxLevel = level, + seen = {}, + log = function(self, message, context) + table.insert(self.seen, {message=message, context=context}) + end, + } + end + + it("should generate expected messages", function() + local sink = newSink(log.Levels.Info) + log:addSink(sink) + + local result = app() + expect(result).to.equal("done") + expect(#sink.seen).to.equal(3) + expect(sink.seen[1].message).to.equal("calling root") + expect(sink.seen[2].message).to.equal("calling root foo") + expect(sink.seen[3].message).to.equal("calling root foo bar") + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/page/component.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/page/component.lua new file mode 100644 index 0000000..21e165c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/page/component.lua @@ -0,0 +1,7 @@ +local log = require(script.Parent.pageLogger):new() +log:setContext({bar = "bar"}) + +return function() + log:info("calling {root} {foo} {bar}") + return "done" +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/page/page.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/page/page.lua new file mode 100644 index 0000000..324b9ec --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/page/page.lua @@ -0,0 +1,13 @@ +-- foo does not need to require root +local log = require(script.Parent.pageLogger) +local component = require(script.Parent.component) + +return { + init = function(logParent) + log:setParent(logParent) + end, + doSomething = function() + log:info("calling {root} {foo}") + return component() + end +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/page/pageLogger.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/page/pageLogger.lua new file mode 100644 index 0000000..d8d0b89 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/example/page/pageLogger.lua @@ -0,0 +1,4 @@ +local Logger = require(script.Parent.Parent.Parent.Logger) +local log = Logger.new() +log:setContext({foo = "foo"}) +return log diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/init.lua new file mode 100644 index 0000000..ddbb40f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_lumberyak-c9fb3068-76fee23f/lumberyak/init.lua @@ -0,0 +1,3 @@ +return { + Logger = require(script.Logger), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/lock.toml new file mode 100644 index 0000000..10672cb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/otter" +version = "0.1.3" +commit = "968bf9466a01286b74c14ef93a28d6ea8b23b561" +source = "url+https://github.com/roblox/otter" diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/assign.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/assign.lua new file mode 100644 index 0000000..7b763d5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/assign.lua @@ -0,0 +1,13 @@ +local function assign(target, ...) + for i = 1, select("#", ...) do + local source = select(i, ...) + + for key, value in pairs(source) do + target[key] = value + end + end + + return target +end + +return assign \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createGroupMotor.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createGroupMotor.lua new file mode 100644 index 0000000..280e26a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createGroupMotor.lua @@ -0,0 +1,137 @@ +local RunService = game:GetService("RunService") + +local assign = require(script.Parent.assign) +local createSignal = require(script.Parent.createSignal) + +local GroupMotor = {} +GroupMotor.prototype = {} +GroupMotor.__index = GroupMotor.prototype + +local function createGroupMotor(initialValues) + assert(typeof(initialValues) == "table") + + local states = {} + + for key, value in pairs(initialValues) do + states[key] = { + value = value, + complete = true, + } + end + + local self = { + __goals = {}, + __states = states, + __allComplete = true, + __onComplete = createSignal(), + __onStep = createSignal(), + __running = false, + } + + setmetatable(self, GroupMotor) + + return self +end + +function GroupMotor.prototype:start() + if self.__running then + return + end + + self.__connection = RunService.Heartbeat:Connect(function(dt) + self:step(dt) + end) + + self.__running = true +end + +function GroupMotor.prototype:stop() + if self.__connection ~= nil then + self.__connection:Disconnect() + self.__running = false + end +end + +function GroupMotor.prototype:step(dt) + assert(typeof(dt) == "number") + + if self.__allComplete then + return + end + + local allComplete = true + local values = {} + + for key, state in pairs(self.__states) do + if not state.complete then + local goal = self.__goals[key] + + if goal ~= nil then + local maybeNewState = goal:step(state, dt) + + if maybeNewState ~= nil then + state = maybeNewState + self.__states[key] = maybeNewState + end + else + state.complete = true + end + + if not state.complete then + allComplete = false + end + end + + values[key] = state.value + end + + local wasAllComplete = self.__allComplete + self.__allComplete = allComplete + + self.__onStep:fire(values) + + -- Check self.__allComplete as the motor may have been restarted in the onStep callback + -- even if allComplete is true. + -- Check self.__running in case the motor was stopped by onStep + if self.__allComplete and not wasAllComplete and self.__running then + self:stop() + self.__onComplete:fire(values) + end +end + +function GroupMotor.prototype:setGoal(goals) + assert(typeof(goals) == "table") + + self.__goals = assign({}, self.__goals, goals) + + for key in pairs(goals) do + local state = self.__states[key] + + if state == nil then + error(("Cannot set goal for the value %s because it doesn't exist"):format(tostring(key)), 2) + end + + state.complete = false + end + + self.__allComplete = false + self:start() +end + +function GroupMotor.prototype:onStep(callback) + assert(typeof(callback) == "function") + + return self.__onStep:subscribe(callback) +end + +function GroupMotor.prototype:onComplete(callback) + assert(typeof(callback) == "function") + + return self.__onComplete:subscribe(callback) +end + +function GroupMotor.prototype:destroy() + self:stop() +end + +return createGroupMotor diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createGroupMotor.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createGroupMotor.spec.lua new file mode 100644 index 0000000..88e30cb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createGroupMotor.spec.lua @@ -0,0 +1,275 @@ +return function() + local validateMotor = require(script.Parent.validateMotor) + local createSpy = require(script.Parent.createSpy) + + local createGroupMotor = require(script.Parent.createGroupMotor) + + -- test motion object that completes after step has been called numSteps times + local function createStepper(numSteps) + local self = { + stepCount = 0, + } + + self.step = function(_, state, dt) + self.stepCount = self.stepCount + 1 + + if self.stepCount >= numSteps then + return { + value = state.value, + velocity = state.velocity, + complete = true, + } + end + + return state + end + + setmetatable(self, { + __index = function(_, key) + error(("%q is not a valid member of stepper"):format(key)) + end, + }) + + return self + end + + it("should be a valid motor", function() + local motor = createGroupMotor({}) + validateMotor(motor) + motor:destroy() + end) + + describe("onStep", function() + it("should not be called initially", function() + local motor = createGroupMotor({ + x = 0, + }) + + local spy = createSpy() + motor:onStep(spy.value) + + motor:setGoal({ + x = createStepper(5), + }) + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + end) + + describe("setGoal", function() + it("should work as intended in onComplete callbacks", function() + local motor = createGroupMotor({ + x = 0, + }) + + local spy = createSpy(function() + motor:setGoal({ x = createStepper(3), }) + end) + + motor:onComplete(spy.value) + + motor:setGoal({ x = createStepper(3), }) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(1) + --make sure the motor continues to run after calling setGoal in onComplete + expect(motor.__running).to.equal(true) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(2) + expect(motor.__running).to.equal(true) + + motor:destroy() + end) + end) + + describe("onComplete should be called when", function() + it("has completed its motion", function() + local motor = createGroupMotor({ + x = 0, + }) + + motor:setGoal({ + x = createStepper(5), + }) + + local spy = createSpy() + + motor:onComplete(spy.value) + + for _ = 1, 5 do + motor:step(1) + end + + expect(spy.callCount).to.equal(1) + + motor:destroy() + end) + + it("has multiple atributes in motion", function() + local motor = createGroupMotor({ + x = 0, + y = 10, + }) + + motor:setGoal({ + x = createStepper(2), + y = createStepper(5), + }) + + local spy = createSpy() + + motor:onComplete(spy.value) + + for _ = 1, 2 do + motor:step(1) + end + + expect(spy.callCount).to.equal(0) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(1) + + motor:destroy() + end) + + it("has restarted its motion", function() + local motor = createGroupMotor({ + x = 0, + }) + + motor:setGoal({ + x = createStepper(3), + }) + + local spy = createSpy() + + motor:onComplete(spy.value) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(1) + + motor:setGoal({ + x = createStepper(3), + }) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(2) + + motor:destroy() + end) + end) + + describe("onComplete should not be called when", function() + it("has no goals set", function() + local motor = createGroupMotor({ + x = 2, + }) + + local spy = createSpy() + motor:onComplete(spy.value) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + + it("has not completed motion", function() + local motor = createGroupMotor({ + x = 0, + }) + + motor:setGoal({ + x = createStepper(2), + }) + + local spy = createSpy() + motor:onComplete(spy.value) + + motor:step(1) + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + + it("has one non-completed motion", function() + local motor = createGroupMotor({ + x = 0, + y = 0, + }) + + motor:setGoal({ + x = createStepper(0), + y = createStepper(2), + }) + + local spy = createSpy() + motor:onComplete(spy.value) + + motor:step(1) + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + + it("does not call step", function() + local motor = createGroupMotor({ + x = 0, + }) + + motor:setGoal({ + x = createStepper(0), + }) + + local spy = createSpy() + motor:onComplete(spy.value) + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + + it("is stopped in onStep", function() + local motor = createGroupMotor({ + x = 0, + }) + + motor:setGoal({ + x = createStepper(1), + }) + + local spy = createSpy() + motor:onComplete(spy.value) + motor:onStep(function() motor:stop() end) + + motor:step(1) + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSignal.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSignal.lua new file mode 100644 index 0000000..5632b80 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSignal.lua @@ -0,0 +1,61 @@ +local function addToMap(map, addKey, addValue) + local new = {} + + for key, value in pairs(map) do + new[key] = value + end + + new[addKey] = addValue + + return new +end + +local function removeFromMap(map, removeKey) + local new = {} + + for key, value in pairs(map) do + if key ~= removeKey then + new[key] = value + end + end + + return new +end + +local function createSignal() + local connections = {} + + local function subscribe(self, callback) + assert(typeof(callback) == "function", "Can only subscribe to signals with a function.") + + local connection = { + callback = callback, + } + + connections = addToMap(connections, callback, connection) + + local function disconnect() + assert(not connection.disconnected, "Listeners can only be disconnected once.") + + connection.disconnected = true + connections = removeFromMap(connections, callback) + end + + return disconnect + end + + local function fire(self, ...) + for callback, connection in pairs(connections) do + if not connection.disconnected then + callback(...) + end + end + end + + return { + subscribe = subscribe, + fire = fire, + } +end + +return createSignal \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSignal.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSignal.spec.lua new file mode 100644 index 0000000..ed9cf97 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSignal.spec.lua @@ -0,0 +1,89 @@ +return function() + local createSignal = require(script.Parent.createSignal) + + local createSpy = require(script.Parent.createSpy) + + it("should fire subscribers and disconnect them", function() + local signal = createSignal() + + local spy = createSpy() + local disconnect = signal:subscribe(spy.value) + + expect(spy.callCount).to.equal(0) + + local a = 1 + local b = {} + local c = "hello" + signal:fire(a, b, c) + + expect(spy.callCount).to.equal(1) + spy:assertCalledWith(a, b, c) + + disconnect() + + signal:fire() + + expect(spy.callCount).to.equal(1) + end) + + it("should handle multiple subscribers", function() + local signal = createSignal() + + local spyA = createSpy() + local spyB = createSpy() + + local disconnectA = signal:subscribe(spyA.value) + local disconnectB = signal:subscribe(spyB.value) + + expect(spyA.callCount).to.equal(0) + expect(spyB.callCount).to.equal(0) + + local a = {} + local b = 67 + signal:fire(a, b) + + expect(spyA.callCount).to.equal(1) + spyA:assertCalledWith(a, b) + + expect(spyB.callCount).to.equal(1) + spyB:assertCalledWith(a, b) + + disconnectA() + + signal:fire(b, a) + + expect(spyA.callCount).to.equal(1) + + expect(spyB.callCount).to.equal(2) + spyB:assertCalledWith(b, a) + + disconnectB() + end) + + it("should stop firing a connection if disconnected mid-fire", function() + local signal = createSignal() + + -- In this test, we'll connect two listeners that each try to disconnect + -- the other. Because the order of listeners firing isn't defined, we + -- have to be careful to handle either case. + + local disconnectA + local disconnectB + + local spyA = createSpy(function() + disconnectB() + end) + + local spyB = createSpy(function() + disconnectA() + end) + + disconnectA = signal:subscribe(spyA.value) + disconnectB = signal:subscribe(spyB.value) + + signal:fire() + + -- Exactly once listener should have been called. + expect(spyA.callCount + spyB.callCount).to.equal(1) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSingleMotor.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSingleMotor.lua new file mode 100644 index 0000000..9708c3e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSingleMotor.lua @@ -0,0 +1,95 @@ +local RunService = game:GetService("RunService") + +local createSignal = require(script.Parent.createSignal) + +local SingleMotor = {} +SingleMotor.prototype = {} +SingleMotor.__index = SingleMotor.prototype + +local function createSingleMotor(initialValue) + assert(typeof(initialValue) == "number") + + local self = { + __goal = nil, + __state = { + value = initialValue, + complete = true, + }, + __onComplete = createSignal(), + __onStep = createSignal(), + __running = false, + } + + setmetatable(self, SingleMotor) + + return self +end + +function SingleMotor.prototype:start() + if self.__running then + return + end + + self.__connection = RunService.Heartbeat:Connect(function(dt) + self:step(dt) + end) + + self.__running = true +end + +function SingleMotor.prototype:stop() + if self.__connection ~= nil then + self.__connection:Disconnect() + end + + self.__running = false +end + +function SingleMotor.prototype:step(dt) + assert(typeof(dt) == "number") + + if self.__state.complete then + return + end + + if self.__goal == nil then + return + end + + local newState = self.__goal:step(self.__state, dt) + + if newState ~= nil then + self.__state = newState + end + + self.__onStep:fire(self.__state.value) + + if self.__state.complete and self.__running then + self:stop() + self.__onComplete:fire(self.__state.value) + end +end + +function SingleMotor.prototype:setGoal(goal) + self.__goal = goal + self.__state.complete = false + self:start() +end + +function SingleMotor.prototype:onStep(callback) + assert(typeof(callback) == "function") + + return self.__onStep:subscribe(callback) +end + +function SingleMotor.prototype:onComplete(callback) + assert(typeof(callback) == "function") + + return self.__onComplete:subscribe(callback) +end + +function SingleMotor.prototype:destroy() + self:stop() +end + +return createSingleMotor \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSingleMotor.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSingleMotor.spec.lua new file mode 100644 index 0000000..27ab4d4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSingleMotor.spec.lua @@ -0,0 +1,210 @@ +return function() + local validateMotor = require(script.Parent.validateMotor) + local createSpy = require(script.Parent.createSpy) + + local createSingleMotor = require(script.Parent.createSingleMotor) + + + local identityGoal = { + step = function(self, state, dt) + return state + end, + } + + -- test motion object that completes after step has been called numSteps times + local function createStepper(numSteps) + local self = { + stepCount = 0, + } + + self.step = function(_, state, dt) + self.stepCount = self.stepCount + 1 + + if self.stepCount >= numSteps then + return { + value = state.value, + velocity = state.velocity, + complete = true, + } + end + + return state + end + + setmetatable(self, { + __index = function(_, key) + error(("%q is not a valid member of stepper"):format(key)) + end, + }) + + return self + end + + it("should be a valid motor", function() + local motor = createSingleMotor(0) + validateMotor(motor) + motor:destroy() + end) + + describe("onStep", function() + it("should not be called initially", function() + local motor = createSingleMotor(0) + + local spy = createSpy() + motor:onStep(spy.value) + + motor:setGoal(createStepper(5)) + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + end) + + it("should invoke subscribers with new values", function() + local motor = createSingleMotor(8) + motor:setGoal(identityGoal) + + local spy = createSpy() + + local disconnect = motor:onStep(spy.value) + + expect(spy.callCount).to.equal(0) + + motor:step(1) + + expect(spy.callCount).to.equal(1) + spy:assertCalledWith(8) + + disconnect() + + motor:step(1) + + expect(spy.callCount).to.equal(1) + + motor:destroy() + end) + + describe("setGoal", function() + it("should work as intended in onComplete callbacks", function() + local motor = createSingleMotor(0) + + local spy = createSpy(function() + motor:setGoal(createStepper(3)) + end) + + motor:onComplete(spy.value) + + motor:setGoal(createStepper(3)) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(1) + --make sure the motor continues to run after calling setGoal in onComplete + expect(motor.__running).to.equal(true) + + for _ = 1, 3 do + motor:step(1) + end + + expect(spy.callCount).to.equal(2) + expect(motor.__running).to.equal(true) + + motor:destroy() + end) + end) + + describe("onComplete should be called when", function() + it("has completed its motion", function() + local motor = createSingleMotor(0) + motor:setGoal(createStepper(5)) + + local spy = createSpy() + + motor:onComplete(spy.value) + + for _ = 1, 5 do + motor:step(1) + end + + expect(spy.callCount).to.equal(1) + + motor:destroy() + end) + + it("has restarted its motion", function() + local motor = createSingleMotor(0) + motor:setGoal(createStepper(5)) + + local spy = createSpy() + + motor:onComplete(spy.value) + + expect(spy.callCount).to.equal(0) + + for _ = 1, 5 do + motor:step(1) + end + + expect(spy.callCount).to.equal(1) + + motor:setGoal(createStepper(5)) + + for _ = 1, 5 do + motor:step(1) + end + + expect(spy.callCount).to.equal(2) + + motor:destroy() + end) + end) + + describe("onComplete should not be called when", function() + it("has not completed motion", function() + local motor = createSingleMotor(0) + motor:setGoal(createStepper(10)) + + local spy = createSpy() + + motor:onComplete(spy.value) + + motor:step(1) + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + + it("does not call step", function() + local motor = createSingleMotor(0) + motor:setGoal(createStepper(0)) + + local spy = createSpy() + + motor:onComplete(spy.value) + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + + it("is stopped in onStep", function() + local motor = createSingleMotor(0) + + motor:setGoal(createStepper(1)) + + local spy = createSpy() + motor:onComplete(spy.value) + motor:onStep(function() motor:stop() end) + + motor:step(1) + + expect(spy.callCount).to.equal(0) + + motor:destroy() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSpy.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSpy.lua new file mode 100644 index 0000000..8f23651 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/createSpy.lua @@ -0,0 +1,38 @@ +local function createSpy(inner) + local self = { + callCount = 0, + values = {}, + valuesLength = 0, + } + + self.value = function(...) + self.callCount = self.callCount + 1 + self.values = {...} + self.valuesLength = select("#", ...) + + if inner ~= nil then + return inner(...) + end + end + + self.assertCalledWith = function(_, ...) + local len = select("#", ...) + + assert(self.valuesLength, len, "length of expected values differs from stored values") + + for i = 1, len do + local expected = select(i, ...) + assert(self.values[i], expected, "value differs") + end + end + + setmetatable(self, { + __index = function(_, key) + error(("%q is not a valid member of spy"):format(key)) + end, + }) + + return self +end + +return createSpy \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/init.lua new file mode 100644 index 0000000..88a73be --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/init.lua @@ -0,0 +1,6 @@ +return { + createGroupMotor = require(script.createGroupMotor), + createSingleMotor = require(script.createSingleMotor), + spring = require(script.spring), + instant = require(script.instant), +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/init.spec.lua new file mode 100644 index 0000000..688ee33 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/init.spec.lua @@ -0,0 +1,5 @@ +return function() + it("should load successfully", function() + require(script.Parent) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/instant.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/instant.lua new file mode 100644 index 0000000..0ab2a7d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/instant.lua @@ -0,0 +1,15 @@ +local function step(self, state, dt) + return { + value = self.__targetValue, + complete = true, + } +end + +local function instant(targetValue) + return { + __targetValue = targetValue, + step = step, + } +end + +return instant \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/instant.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/instant.spec.lua new file mode 100644 index 0000000..84ffb0c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/instant.spec.lua @@ -0,0 +1,39 @@ +return function() + local instant = require(script.Parent.instant) + + it("should have the expected APIs", function() + local goal = instant(5) + + expect(goal).to.be.a("table") + expect(goal.step).to.be.a("function") + end) + + it("should immediately complete", function() + local state = { + value = 5, + complete = false, + } + + local goal = instant(10) + state = goal:step(state, 1e-3) + + expect(state.value).to.equal(10) + expect(state.complete).to.equal(true) + end) + + it("should remove extra values from state", function() + local state = { + value = 5, + complete = false, + + velocity = 7, + somethingElse = {}, + } + + local goal = instant(10) + state = goal:step(state, 1e-3) + + expect(state.velocity).to.never.be.ok() + expect(state.somethingElse).to.never.be.ok() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/spring.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/spring.lua new file mode 100644 index 0000000..ff3bfe1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/spring.lua @@ -0,0 +1,159 @@ +--[[ + An analytical spring solution as a function of damping ratio and frequency. + + Adapted from + https://gist.github.com/Fraktality/1033625223e13c01aa7144abe4aaf54d +]] + +local assign = require(script.Parent.assign) + +local pi = math.pi +local abs = math.abs +local exp = math.exp +local sin = math.sin +local cos = math.cos +local sqrt = math.sqrt + +local DEFAULT_RESTING_VELOCITY_LIMIT = 1e-3 +local DEFAULT_RESTING_POSITION_LIMIT = 1e-2 + +local function step(self, state, dt) + -- Advance the spring simulation by dt seconds. + -- Take the damped harmonic oscillator ODE: + -- f^2*(X[t] - g) + 2*d*f*X'[t] + X''[t] = 0 + -- Where X[t] is position at time t, g is desired position, f is angular frequency, and d is damping ratio. + -- Apply constant initial conditions: + -- X[0] = p0 + -- X'[0] = v0 + -- Solve the IVP to get analytic expressions for X[t] and X'[t]. + -- The solution takes on one of three forms for d=1, d<1, and d>1 + + local d = self.__dampingRatio + local f = self.__frequency * 2 * pi -- Rad/s + local g = self.__goalPosition + local velLimit = self.__restingVelocityLimit + local posLimit = self.__restingPositionLimit + + local p0 = state.value + local v0 = state.velocity or 0 + + local offset = p0 - g + local decay = exp(-dt*d*f) + + local p1, v1 + + if d == 1 then -- Critically damped + p1 = (v0*dt + offset*(f*dt + 1))*decay + g + v1 = (v0 - f*dt*(offset*f + v0))*decay + + elseif d < 1 then -- Underdamped + local c = sqrt(1 - d*d) + + local i = cos(f*c*dt) + local j = sin(f*c*dt) + + -- Problem: Damping ratios close to 1 can cause numerical instability. + -- Solution: Rearrange to group terms involving j/c, then find an approximation z for j/c. + -- z = sin(dt*f*c)/c + -- Substitute a for dt*f + -- z = sin(a*c)/c + -- Take the 5th-order series expansion of z at c = 0 + -- z = a - (a^3*c^2)/6 + (a^5*c^4)/120 + O(c^6) + -- z ≈ a - (a^3*c^2)/6 + (a^5*c^4)/120 + -- Rewrite in Horner form to mitigate precision issues + -- z ≈ a + ((a*a)*(c*c)*(c*c)/20 - c*c)*(a*a*a)/6 + + local z + if c > 1e-4 then + z = j/c + else + local a = dt*f + z = a + ((a*a)*(c*c)*(c*c)/20 - c*c)*(a*a*a)/6 + end + + -- Repeat the process with a->dt and c->b=f*c for the f->0 case + local y + if f*c > 1e-4 then + y = j/(f*c) + else + local b = f*c + y = dt + ((dt*dt)*(b*b)*(b*b)/20 - b*b)*(dt*dt*dt)/6 + end + + p1 = (offset*(i + d*z) + v0*y)*decay + g + v1 = (v0*(i - z*d) - offset*(z*f))*decay + + else -- Overdamped + local c = sqrt(d*d - 1) + + local r1 = -f*(d - c) + local r2 = -f*(d + c) + + local co2 = (v0 - r1*offset)/(2*f*c) + local co1 = offset - co2 + + local e1 = co1*exp(r1*dt) + local e2 = co2*exp(r2*dt) + + p1 = e1 + e2 + g + v1 = r1*e1 + r2*e2 + end + + local positionOffset = abs(p1 - self.__goalPosition) + local velocityOffset = abs(v1) + + local complete = velocityOffset < velLimit and positionOffset < posLimit + + if complete then + p1 = self.__goalPosition + v1 = 0 + end + + return { + value = p1, + velocity = v1, + complete = complete, + } +end + +local function spring(goalPosition, inputOptions) + assert(typeof(goalPosition) == "number") + + local options = { + dampingRatio = 1, + frequency = 1, + restingVelocityLimit = DEFAULT_RESTING_VELOCITY_LIMIT, + restingPositionLimit = DEFAULT_RESTING_POSITION_LIMIT, + } + + if inputOptions ~= nil then + assert(typeof(inputOptions) == "table") + assign(options, inputOptions) + end + + local dampingRatio = options.dampingRatio + local frequency = options.frequency + local restingVelocityLimit = options.restingVelocityLimit + local restingPositionLimit = options.restingPositionLimit + + assert(typeof(dampingRatio) == "number") + assert(typeof(frequency) == "number") + assert(typeof(restingVelocityLimit) == "number") + assert(typeof(restingPositionLimit) == "number") + + assert(restingVelocityLimit >= 0, "Expected restingVelocityLimit >= 0") + assert(restingPositionLimit >= 0, "Expected restingPositionLimit >= 0") + + local self = { + __dampingRatio = dampingRatio, + __frequency = frequency, -- Hz + __restingVelocityLimit = restingVelocityLimit, + __restingPositionLimit = restingPositionLimit, + __goalPosition = goalPosition, + step = step, + } + + return self +end + +return spring diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/spring.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/spring.spec.lua new file mode 100644 index 0000000..343f20e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/spring.spec.lua @@ -0,0 +1,206 @@ +return function() + local spring = require(script.Parent.spring) + + it("should have all expected APIs", function() + expect(spring).to.be.a("function") + + local s = spring(1, { + dampingRatio = 0.1, + frequency = 10, + restingVelocityLimit = 0.1, + restingPositionLimit = 0.01, + }) + + expect(s).to.be.a("table") + expect(s.step).to.be.a("function") + + -- handle when spring lacks option table + s = spring(1) + + expect(s).to.be.a("table") + expect(s.step).to.be.a("function") + end) + + it("should handle being still correctly", function() + local s = spring(1, { + dampingRatio = 0.1, + frequency = 10, + restingVelocityLimit = 0.1, + restingPositionLimit = 0.01, + }) + + local state = s:step({ + value = 1, + velocity = 0, + complete = false, + }, 1) + + expect(state.value).to.equal(1) + expect(state.velocity).to.equal(0) + expect(state.complete).to.equal(true) + end) + + it("should return not complete when in motion", function() + local goal = spring(100, { + dampingRatio = 0.1, + frequency = 10, + }) + + local state = { + value = 1, + velocity = 0, + complete = false, + } + + state = goal:step(state, 1e-3) + + expect(state.value < 100).to.equal(true) + expect(state.velocity > 0).to.equal(true) + expect(state.complete).to.equal(false) + end) + + describe("should eventaully complete when", function() + it("is critically damped", function() + local s = spring(3, { + dampingRatio = 1, + frequency = 0.5, + }) + + local state = { + value = 1, + velocity = 0, + complete = false, + } + + while not state.complete do + state = s:step(state, 0.5) + end + + expect(state.complete).to.equal(true) + expect(state.value).to.equal(3) + expect(state.velocity).to.equal(0) + end) + + it("is over damped", function() + local s = spring(3, { + dampingRatio = 10, + frequency = 0.5, + }) + + local state = { + value = 1, + velocity = 0, + complete = false, + } + + while not state.complete do + state = s:step(state, 0.5) + end + + expect(state.complete).to.equal(true) + expect(state.value).to.equal(3) + expect(state.velocity).to.equal(0) + end) + + it("is under damped", function() + local s = spring(3, { + dampingRatio = 0.1, + frequency = 0.5, + }) + + local state = { + value = 1, + velocity = 0, + complete = false, + } + + while not state.complete do + state = s:step(state, 0.5) + end + + expect(state.complete).to.equal(true) + expect(state.value).to.equal(3) + expect(state.velocity).to.equal(0) + end) + end) + + describe("should handle infinite time deltas when", function() + -- TODO: This test is broken. + itSKIP("is critically damped", function() + local s = spring(20, { + dampingRatio = 1, + frequency = 1, + }) + + local state = { + value = -10, + velocity = 0, + complete = false, + } + state = s:step(state, math.huge) + + expect(state.complete).to.equal(true) + expect(state.value).to.equal(20) + expect(state.velocity).to.equal(0) + end) + + -- TODO: This test is broken. + itSKIP("is underdamped", function() + local s = spring(20, { + dampingRatio = 0.5, + frequency = 1, + }) + + local state = { + value = -10, + velocity = 0, + complete = false, + } + state = s:step(state, math.huge) + + expect(state.complete).to.equal(true) + expect(state.value).to.equal(20) + expect(state.velocity).to.equal(0) + end) + + it("is overdamped", function() + local s = spring(20, { + dampingRatio = 2, + frequency = 1, + }) + + local state = { + value = -10, + velocity = 0, + complete = false, + } + state = s:step(state, math.huge) + + expect(state.complete).to.equal(true) + expect(state.value).to.equal(20) + expect(state.velocity).to.equal(0) + end) + end) + + it("should remain complete when completed", function() + local s = spring(3, { + dampingRatio = 1, + frequency = 0.5, + }) + + local state = { + value = 1, + velocity = 0, + complete = false, + } + + while not state.complete do + state = s:step(state, 0.5) + end + state = s:step(state, 0.5) + + expect(state.complete).to.equal(true) + expect(state.value).to.equal(3) + expect(state.velocity).to.equal(0) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/validateMotor.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/validateMotor.lua new file mode 100644 index 0000000..3741759 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_otter/otter/validateMotor.lua @@ -0,0 +1,12 @@ +local function validateMotor(motor) + assert(typeof(motor) == "table") + assert(typeof(motor.start) == "function") + assert(typeof(motor.stop) == "function") + assert(typeof(motor.step) == "function") + assert(typeof(motor.setGoal) == "function") + assert(typeof(motor.onStep) == "function") + assert(typeof(motor.onComplete) == "function") + assert(typeof(motor.destroy) == "function") +end + +return validateMotor \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/FitFrame.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/FitFrame.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/FitFrame.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/Otter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/RoactRodux.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/RoactRodux.lua new file mode 100644 index 0000000..1b8d1c2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/RoactRodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-rodux"]["roact-rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/Rodux.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/Rodux.lua new file mode 100644 index 0000000..96b67df --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/Rodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_rodux"]["rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/UIBlox.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/UIBlox.lua new file mode 100644 index 0000000..2e0dcc2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/UIBlox.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["UIBlox"]["UIBlox"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/lock.toml new file mode 100644 index 0000000..c0e7957 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/lock.toml @@ -0,0 +1,15 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/purchase-prompt-deps" +version = "0.0.1" +commit = "c337b7da6ec46375e18cd3f14d6b5cb255fe0c0a" +source = "git+https://github.com/roblox/purchase-prompt-deps#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "RoactRodux roblox/roact-rodux 0.2.2 url+https://github.com/roblox/roact-rodux", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "UIBlox UIBlox 3a82d9ed git+https://github.com/roblox/uiblox#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/purchase-prompt-deps/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/purchase-prompt-deps/init.lua new file mode 100644 index 0000000..038ac1e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/purchase-prompt-deps/init.lua @@ -0,0 +1,12 @@ +local PurchasePromptDependencies = script.Parent + +return { + Roact = require(PurchasePromptDependencies.Roact), + Rodux = require(PurchasePromptDependencies.Rodux), + RoactRodux = require(PurchasePromptDependencies.RoactRodux), + Otter = require(PurchasePromptDependencies.Otter), + Cryo = require(PurchasePromptDependencies.Cryo), + t = require(PurchasePromptDependencies.t), + UIBlox = require(PurchasePromptDependencies.UIBlox), + FitFrame = require(PurchasePromptDependencies.FitFrame), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/t.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt-deps/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/FitFrame.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/FitFrame.lua new file mode 100644 index 0000000..a5be988 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/FitFrame.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Otter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/RoactRodux.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/RoactRodux.lua new file mode 100644 index 0000000..1b8d1c2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/RoactRodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact-rodux"]["roact-rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Rodux.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Rodux.lua new file mode 100644 index 0000000..96b67df --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/Rodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_rodux"]["rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/UIBlox.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/UIBlox.lua new file mode 100644 index 0000000..2e0dcc2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/UIBlox.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["UIBlox"]["UIBlox"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/lock.toml new file mode 100644 index 0000000..8b65109 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/lock.toml @@ -0,0 +1,15 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/purchase-prompt" +version = "0.1.6" +commit = "81ce762fbffc86c3402e4a8139c2eb6c6894eb5d" +source = "git+https://github.com/roblox/purchasepromptscript-roact#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "RoactRodux roblox/roact-rodux 0.2.2 url+https://github.com/roblox/roact-rodux", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "UIBlox UIBlox 3a82d9ed git+https://github.com/roblox/uiblox#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/AccountInfoReceived.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/AccountInfoReceived.lua new file mode 100644 index 0000000..31ba693 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/AccountInfoReceived.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "accountInfo") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/BundleProductInfoReceived.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/BundleProductInfoReceived.lua new file mode 100644 index 0000000..ea54e9e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/BundleProductInfoReceived.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "bundleProductInfo") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/CompleteRequest.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/CompleteRequest.lua new file mode 100644 index 0000000..8540ef9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/CompleteRequest.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/ErrorOccurred.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/ErrorOccurred.lua new file mode 100644 index 0000000..40d3358 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/ErrorOccurred.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "purchaseError") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PremiumInfoRecieved.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PremiumInfoRecieved.lua new file mode 100644 index 0000000..a0f81b3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PremiumInfoRecieved.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "premiumInfo") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/ProductInfoReceived.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/ProductInfoReceived.lua new file mode 100644 index 0000000..40b4778 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/ProductInfoReceived.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "productInfo") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PromptNativeUpsell.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PromptNativeUpsell.lua new file mode 100644 index 0000000..836f9a5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PromptNativeUpsell.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "robuxProductId", "robuxPurchaseAmount") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PurchaseCompleteRecieved.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PurchaseCompleteRecieved.lua new file mode 100644 index 0000000..8540ef9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/PurchaseCompleteRecieved.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestAssetPurchase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestAssetPurchase.lua new file mode 100644 index 0000000..f66e2dd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestAssetPurchase.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "id", "equipIfPurchased", "isRobloxPurchase") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestBundlePurchase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestBundlePurchase.lua new file mode 100644 index 0000000..1c92d3b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestBundlePurchase.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "id") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestGamepassPurchase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestGamepassPurchase.lua new file mode 100644 index 0000000..1c92d3b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestGamepassPurchase.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "id") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestPremiumPurchase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestPremiumPurchase.lua new file mode 100644 index 0000000..8540ef9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestPremiumPurchase.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestProductPurchase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestProductPurchase.lua new file mode 100644 index 0000000..8d8e28c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestProductPurchase.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "id", "equipIfPurchased") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestSubscriptionPurchase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestSubscriptionPurchase.lua new file mode 100644 index 0000000..1c92d3b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/RequestSubscriptionPurchase.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "id") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetABVariation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetABVariation.lua new file mode 100644 index 0000000..47a39f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetABVariation.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "key", "variation") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetGamepadEnabled.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetGamepadEnabled.lua new file mode 100644 index 0000000..49f1164 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetGamepadEnabled.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "enabled") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetProduct.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetProduct.lua new file mode 100644 index 0000000..ba760fb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetProduct.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "id", "infoType", "equipIfPurchased") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetPromptState.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetPromptState.lua new file mode 100644 index 0000000..a106faf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetPromptState.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "promptState") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetWindowState.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetWindowState.lua new file mode 100644 index 0000000..cbd05ab --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/SetWindowState.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "state") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/StartHidingPrompt.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/StartHidingPrompt.lua new file mode 100644 index 0000000..8540ef9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/StartHidingPrompt.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/StartPurchase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/StartPurchase.lua new file mode 100644 index 0000000..44cb373 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/StartPurchase.lua @@ -0,0 +1,3 @@ +local makeActionCreator = require(script.Parent.makeActionCreator) + +return makeActionCreator(script.Name, "purchasingStartTime") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/makeActionCreator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/makeActionCreator.lua new file mode 100644 index 0000000..d67c577 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/makeActionCreator.lua @@ -0,0 +1,42 @@ +--[[ + A helper function to define a named Rodux action creator. + + Takes a name followed by a list of fields that should be + provided to the resulting action creator. + + Returns an object with a name field that can also be called + to create an action. When called, it will validate its given + arguments against the expected set of arguments. +]] +local function makeActionCreator(name, ...) + local fields = {...} + + assert(type(name) == "string", + "Bad argument #1 to makeActionCreator, expected string") + + for i = 1, select("#", ...) do + assert(typeof(select(i, ...)) == "string", + "Bad argument to makeActionCreator, all arguments must be of type string") + end + + return setmetatable({ + name = name + }, { + __call = function(self, ...) + local result = { + type = name, + } + + assert(select("#", ...) == #fields, + "Incorrect number of arguments provided to action creator " .. name) + + for index, argName in ipairs(fields) do + result[argName] = select(index, ...) + end + + return result + end + }) +end + +return makeActionCreator \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/makeActionCreator.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/makeActionCreator.spec.lua new file mode 100644 index 0000000..e739fd0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Actions/makeActionCreator.spec.lua @@ -0,0 +1,47 @@ +return function() + local makeActionCreator = require(script.Parent.makeActionCreator) + + describe("the generated action creator", function() + it("should throw if given an invalid arguments", function() + expect(function() + makeActionCreator(100) + end).to.throw() + expect(function() + makeActionCreator("Test", 12) + end).to.throw() + end) + + it("should generate a callable table with a 'name' field", function() + local actionCreator = makeActionCreator("Action") + + expect(type(actionCreator)).to.equal("table") + expect(actionCreator.name).to.equal("Action") + expect(actionCreator).never.to.throw() + end) + end) + + describe("the action object generated by the action creator", function() + it("should expect a matching number of inputs", function() + local actionWithFields = makeActionCreator("AddItem", "id", "title") + + expect(function() + actionWithFields(10, "Apple", "extra arg") + end).to.throw() + expect(function() + actionWithFields(10) + end).to.throw() + + expect(function() + actionWithFields(10, "Orange") + end).never.to.throw() + end) + + it("should correctly count trailing nils", function() + local actionWithFields = makeActionCreator("SetProperty", "value", "default") + + expect(function() + actionWithFields("1", nil) + end).never.to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/BrowserPurchaseFinishedConnector.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/BrowserPurchaseFinishedConnector.lua new file mode 100644 index 0000000..89e6eed --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/BrowserPurchaseFinishedConnector.lua @@ -0,0 +1,42 @@ +--[[ + Connects to GuiService's browser close callback to retry purchase after upsell +]] +local Root = script.Parent.Parent.Parent +local GuiService = game:GetService("GuiService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local retryAfterUpsell = require(Root.Thunks.retryAfterUpsell) +local connectToStore = require(Root.connectToStore) + +local ExternalEventConnection = require(script.Parent.ExternalEventConnection) + +local function BrowserPurchaseFinishedConnector(props) + local onBrowserWindowClosed = props.onBrowserWindowClosed + + --[[ + CLILUACORE-309: The browser window closing is the ONLY + indication we have about when the user finished interacting + with the upsell flow on desktop. + ]] + return Roact.createElement(ExternalEventConnection, { + event = GuiService.BrowserWindowClosed, + callback = onBrowserWindowClosed, + }) +end + +local function mapDispatchToProps(dispatch) + return { + onBrowserWindowClosed = function() + dispatch(retryAfterUpsell()) + end, + } +end + +BrowserPurchaseFinishedConnector = connectToStore( + nil, + mapDispatchToProps +)(BrowserPurchaseFinishedConnector) + +return BrowserPurchaseFinishedConnector \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/EventConnections.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/EventConnections.lua new file mode 100644 index 0000000..6746ea8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/EventConnections.lua @@ -0,0 +1,38 @@ +--[[ + Connects relevant Roblox engine events to the rodux store +]] +local Root = script.Parent.Parent.Parent +local UserInputService = game:GetService("UserInputService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local UpsellFlow = require(Root.Enums.UpsellFlow) +local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow) + +local MarketplaceServiceEventConnector = require(script.Parent.MarketplaceServiceEventConnector) +local InputTypeManager = require(script.Parent.InputTypeManager) +local BrowserPurchaseFinishedConnector = require(script.Parent.BrowserPurchaseFinishedConnector) +local NativePurchaseFinishedConnector = require(script.Parent.NativePurchaseFinishedConnector) +local PlayerConnector = require(script.Parent.PlayerConnector) + +local function EventConnections() + local upsellConnector + local upsellFlow = getUpsellFlow(UserInputService:GetPlatform()) + if upsellFlow == UpsellFlow.Web then + upsellConnector = Roact.createElement(BrowserPurchaseFinishedConnector) + elseif upsellFlow == UpsellFlow.Mobile then + upsellConnector = Roact.createElement(NativePurchaseFinishedConnector) + end + + local enableInputManager = UserInputService:GetPlatform() ~= Enum.Platform.XBoxOne + + return Roact.createElement("Folder", {}, { + MarketPlaceServiceEventConnector = Roact.createElement(MarketplaceServiceEventConnector), + InputTypeManager = enableInputManager and Roact.createElement(InputTypeManager) or nil, + UpsellFinishedConnector = upsellConnector, + PlayerConnector = Roact.createElement(PlayerConnector), + }) +end + +return EventConnections \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/ExternalEventConnection.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/ExternalEventConnection.lua new file mode 100644 index 0000000..1888c57 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/ExternalEventConnection.lua @@ -0,0 +1,53 @@ +--[[ + A component that establishes a connection to a Roblox event when it is rendered. +]] +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local ExternalEventConnection = Roact.Component:extend("ExternalEventConnection") + +function ExternalEventConnection:init() + self.connection = nil +end + +--[[ + Render the child component so that ExternalEventConnections can be nested like so: + + Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputBegan, + callback = inputBeganCallback, + }, { + Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputEnded, + callback = inputChangedCallback, + }) + }) +]] +function ExternalEventConnection:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +function ExternalEventConnection:didMount() + local event = self.props.event + local callback = self.props.callback + + self.connection = event:Connect(callback) +end + +function ExternalEventConnection:didUpdate(oldProps) + if self.props.event ~= oldProps.event or self.props.callback ~= oldProps.callback then + self.connection:Disconnect() + + self.connection = self.props.event:Connect(self.props.callback) + end +end + +function ExternalEventConnection:willUnmount() + self.connection:Disconnect() + + self.connection = nil +end + +return ExternalEventConnection \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/InputTypeManager.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/InputTypeManager.lua new file mode 100644 index 0000000..ca410f1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/InputTypeManager.lua @@ -0,0 +1,105 @@ +--[[ + Sets whether or not gamepad buttons should be shown, based on recently + received inputs +]] +local Root = script.Parent.Parent.Parent +local CorePackages = game:GetService("CorePackages") +local UserInputService = game:GetService("UserInputService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local MouseIconOverrideService = require(CorePackages.InGameServices.MouseIconOverrideService) + +local SetGamepadEnabled = require(Root.Actions.SetGamepadEnabled) +local PromptState = require(Root.Enums.PromptState) +local connectToStore = require(Root.connectToStore) + +local ExternalEventConnection = require(script.Parent.ExternalEventConnection) + +local CURSOR_OVERRIDE_KEY = "PurchasePromptOverrideKey" + +local gamepadInputs = { + [Enum.UserInputType.Gamepad1] = true, + [Enum.UserInputType.Gamepad2] = true, + [Enum.UserInputType.Gamepad3] = true, + [Enum.UserInputType.Gamepad4] = true, +} + +local InputTypeManager = Roact.Component:extend("InputTypeManager") + +function InputTypeManager:init() + local setGamepadEnabled = self.props.setGamepadEnabled + + self.dispatchOnChange = function(lastInputType) + local newEnabledStatus + if gamepadInputs[lastInputType] then + newEnabledStatus = true + else + newEnabledStatus = false + end + + setGamepadEnabled(newEnabledStatus) + end + + self.cursorOverridden = false +end + +function InputTypeManager:render() + return Roact.createElement(ExternalEventConnection, { + event = UserInputService.LastInputTypeChanged, + callback = self.dispatchOnChange, + }) +end + +function InputTypeManager:didUpdate(prevProps, prevState) + local didShow = prevProps.promptState == PromptState.None + and self.props.promptState ~= PromptState.None + local didHide = prevProps.promptState ~= PromptState.None + and self.props.promptState == PromptState.None + + local isShown = self.props.promptState ~= PromptState.None + + local overrideStatus = self.props.gamepadEnabled + and Enum.OverrideMouseIconBehavior.ForceHide + or Enum.OverrideMouseIconBehavior.ForceShow + + -- If we're already showing the prompt and the gamepad status changed + if isShown and prevProps.gamepadEnabled ~= self.props.gamepadEnabled then + if self.cursorOverridden then + MouseIconOverrideService.pop(CURSOR_OVERRIDE_KEY) + end + MouseIconOverrideService.push(CURSOR_OVERRIDE_KEY, overrideStatus) + self.cursorOverridden = true + -- If the purchase prompt goes from None to shown + elseif didShow then + MouseIconOverrideService.push(CURSOR_OVERRIDE_KEY, overrideStatus) + self.cursorOverridden = true + -- If the purchase prompt goes from shown to None + elseif didHide then + MouseIconOverrideService.pop(CURSOR_OVERRIDE_KEY) + self.cursorOverridden = false + end +end + +local function mapStateToProps(state) + return { + promptState = state.promptState, + gamepadEnabled = state.gamepadEnabled, + } +end + +local function mapDispatchToProps(dispatch) + return { + setGamepadEnabled = function(enabled) + dispatch(SetGamepadEnabled(enabled)) + end, + } +end + +InputTypeManager = connectToStore( + mapStateToProps, + mapDispatchToProps +)(InputTypeManager) + +return InputTypeManager \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LayoutValuesConsumer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LayoutValuesConsumer.lua new file mode 100644 index 0000000..dc65091 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LayoutValuesConsumer.lua @@ -0,0 +1,43 @@ +--[[ + LayoutValuesConsumer will extract the LayoutValues object + from context and pass it into the given render callback +]] +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local LayoutValuesKey = require(Root.Symbols.LayoutValuesKey) + +local LayoutValuesConsumer = Roact.Component:extend("LayoutValuesConsumer") + +-- TODO(esauer): Add validation when adding t +-- local validateProps = t.strictInterface({ +-- render = t.callback, +-- }) + +function LayoutValuesConsumer:init() + self.layoutValues = self._context[LayoutValuesKey] + self.state = { + layout = self.layoutValues.layout, + } +end + +function LayoutValuesConsumer:render() + -- assert(validateProps(self.props)) + return self.props.render(self.state.layout) +end + +function LayoutValuesConsumer:didMount() + self.disconnectLayoutListener = self.layoutValues.signal:subscribe(function(newLayout) + self:setState({ + layout = newLayout, + }) + end) +end + +function LayoutValuesConsumer:willUnmount() + self.disconnectLayoutListener() +end + +return LayoutValuesConsumer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LayoutValuesProvider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LayoutValuesProvider.lua new file mode 100644 index 0000000..c13064d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LayoutValuesProvider.lua @@ -0,0 +1,65 @@ +--[[ + LayoutValuesProvider is a simple wrapper component that injects the + specified services into context +]] +local Root = script.Parent.Parent.Parent + +local ContentProvider = game:GetService("ContentProvider") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local LayoutValues = require(Root.Services.LayoutValues) +local LayoutValuesKey = require(Root.Symbols.LayoutValuesKey) +local connectToStore = require(Root.connectToStore) + +local LayoutValuesProvider = Roact.Component:extend("LayoutValuesProvider") + +function LayoutValuesProvider:init(props) + assert(type(props.isTenFootInterface) == "boolean", "Expected required prop 'isTenFootInterface' to be a boolean") + assert(type(props.render) == "function", "Expected prop 'render' to be a function") + + self.layoutValues = LayoutValues.new(self.props.isTenFootInterface, false) + self._context[LayoutValuesKey] = self.layoutValues +end + +function LayoutValuesProvider:didMount() + -- preload images + spawn(function() + local assets = {} + + for _, image in pairs(self.layoutValues.layout.Image) do + local decal = Instance.new("Decal") + decal.Texture = image.Path + table.insert(assets, decal) + end + + ContentProvider:PreloadAsync(assets) + + for _,asset in pairs(assets) do + asset:Destroy() + end + end) +end + +function LayoutValuesProvider:render() + return self.props.render() +end + +function LayoutValuesProvider:didUpdate(previousProps) + if self.props.isTenFootInterface ~= previousProps.isTenFootInterface then + self.layoutValues:update(self.props.isTenFootInterface) + end +end + +local function mapStateToProps(state) + return { + abVariations = state.abVariations, + } +end + +LayoutValuesProvider = connectToStore( + mapStateToProps +)(LayoutValuesProvider) + +return LayoutValuesProvider \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LocalizationContextConsumer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LocalizationContextConsumer.lua new file mode 100644 index 0000000..c0f2b6a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LocalizationContextConsumer.lua @@ -0,0 +1,23 @@ +--[[ + LocalizationContextConsumer will extract the localization context + object from Roact context and provide it to the given render callback + + Used for components that need to perform localization using this + project's LocalizationService +]] +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local LocalizationContextKey = require(Root.Symbols.LocalizationContextKey) + +local LocalizationContextConsumer = Roact.Component:extend("LocalizationContextConsumer") + +function LocalizationContextConsumer:render() + local localizationContext = self._context[LocalizationContextKey] + + return self.props.render(localizationContext) +end + +return LocalizationContextConsumer diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LocalizationContextProvider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LocalizationContextProvider.lua new file mode 100644 index 0000000..5019ef0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/LocalizationContextProvider.lua @@ -0,0 +1,25 @@ +--[[ + LocalizationContextProvider is a simple wrapper component that injects the + specified services into context +]] +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local LocalizationContextKey = require(Root.Symbols.LocalizationContextKey) + +local LocalizationContextProvider = Roact.Component:extend("LocalizationContextProvider") + +function LocalizationContextProvider:init(props) + assert(props.localizationContext, "Missing required prop 'localizationContext'") + assert(props.render, "Missing required prop 'render'") + + self._context[LocalizationContextKey] = props.localizationContext +end + +function LocalizationContextProvider:render() + return self.props.render(LocalizationContextKey) +end + +return LocalizationContextProvider \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/MarketplaceServiceEventConnector.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/MarketplaceServiceEventConnector.lua new file mode 100644 index 0000000..94e3147 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/MarketplaceServiceEventConnector.lua @@ -0,0 +1,140 @@ +--[[ + Connects Rodux store to external MarketplaceService events +]] +local Root = script.Parent.Parent.Parent +local MarketplaceService = game:GetService("MarketplaceService") +local Players = game:GetService("Players") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local PurchaseError = require(Root.Enums.PurchaseError) +local completePurchase = require(Root.Thunks.completePurchase) +local initiatePurchase = require(Root.Thunks.initiatePurchase) +local initiateBundlePurchase = require(Root.Thunks.initiateBundlePurchase) +local initiatePremiumPurchase = require(Root.Thunks.initiatePremiumPurchase) +local initiateSubscriptionPurchase = require(Root.Thunks.initiateSubscriptionPurchase) +local connectToStore = require(Root.connectToStore) + +local GetFFlagPromptRobloxPurchaseEnabled = require(Root.Flags.GetFFlagPromptRobloxPurchaseEnabled) +local GetFFlagDeveloperSubscriptionsEnabled = require(Root.Flags.GetFFlagDeveloperSubscriptionsEnabled) + +local ExternalEventConnection = require(script.Parent.ExternalEventConnection) + +local function MarketplaceServiceEventConnector(props) + local onPurchaseRequest = props.onPurchaseRequest + local onProductPurchaseRequest = props.onProductPurchaseRequest + local onPurchaseGamePassRequest = props.onPurchaseGamePassRequest + local onServerPurchaseVerification = props.onServerPurchaseVerification + local onBundlePurchaseRequest = props.onBundlePurchaseRequest + local onPremiumPurchaseRequest = props.onPremiumPurchaseRequest + local onRobloxPurchaseRequest = props.onRobloxPurchaseRequest + local onSubscriptionPurchaseRequest = props.onSubscriptionPurchaseRequest + + return Roact.createFragment({ + Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.PromptPurchaseRequested, + callback = onPurchaseRequest, + }), + RobloxPurchase = GetFFlagPromptRobloxPurchaseEnabled() and Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.PromptRobloxPurchaseRequested, + callback = onRobloxPurchaseRequest, + }), + Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.PromptProductPurchaseRequested, + callback = onProductPurchaseRequest, + }), + Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.PromptGamePassPurchaseRequested, + callback = onPurchaseGamePassRequest, + }), + Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.ServerPurchaseVerification, + callback = onServerPurchaseVerification, + }), + Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.PromptBundlePurchaseRequested, + callback = onBundlePurchaseRequest, + }), + Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.PromptPremiumPurchaseRequested, + callback = onPremiumPurchaseRequest, + }), + SubscriptionsPurchase = GetFFlagDeveloperSubscriptionsEnabled() and Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.PromptSubscriptionPurchaseRequested, + callback = onSubscriptionPurchaseRequest, + }) + }) +end + +MarketplaceServiceEventConnector = connectToStore(nil, +function(dispatch) + local function onPurchaseRequest(player, assetId, equipIfPurchased, currencyType) + if player == Players.LocalPlayer then + dispatch(initiatePurchase(assetId, Enum.InfoType.Asset, equipIfPurchased, false)) + end + end + + local function onRobloxPurchaseRequest(assetId, equipIfPurchased) + dispatch(initiatePurchase(assetId, Enum.InfoType.Asset, equipIfPurchased, true)) + end + + local function onProductPurchaseRequest(player, productId, equipIfPurchased, currencyType) + if player == Players.LocalPlayer then + dispatch(initiatePurchase(productId, Enum.InfoType.Product, equipIfPurchased)) + end + end + + local function onPurchaseGamePassRequest(player, gamePassId) + if player == Players.LocalPlayer then + dispatch(initiatePurchase(gamePassId, Enum.InfoType.GamePass, false)) + end + end + + -- Specific to purchasing dev products + local function onServerPurchaseVerification(serverResponseTable) + if not serverResponseTable then + dispatch(ErrorOccurred(PurchaseError.UnknownFailure)) + else + local playerId = serverResponseTable["playerId"] + if playerId ~= nil then + playerId = tonumber(serverResponseTable["playerId"]) + end + if playerId == Players.LocalPlayer.UserId then + dispatch(completePurchase()) + end + end + end + + local function onBundlePurchaseRequest(player, bundleId) + if player == Players.LocalPlayer then + dispatch(initiateBundlePurchase(bundleId)) + end + end + + local function onPremiumPurchaseRequest(player) + if player == Players.LocalPlayer then + dispatch(initiatePremiumPurchase()) + end + end + + local function onSubscriptionPurchaseRequest(player, subscriptionId) + if player == Players.LocalPlayer then + dispatch(initiateSubscriptionPurchase(subscriptionId)) + end + end + + return { + onPurchaseRequest = onPurchaseRequest, + onRobloxPurchaseRequest = onRobloxPurchaseRequest, + onProductPurchaseRequest = onProductPurchaseRequest, + onPurchaseGamePassRequest = onPurchaseGamePassRequest, + onServerPurchaseVerification = onServerPurchaseVerification, + onBundlePurchaseRequest = onBundlePurchaseRequest, + onPremiumPurchaseRequest = onPremiumPurchaseRequest, + onSubscriptionPurchaseRequest = onSubscriptionPurchaseRequest, + } +end)(MarketplaceServiceEventConnector) + +return MarketplaceServiceEventConnector diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/MultiTextLocalizer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/MultiTextLocalizer.lua new file mode 100644 index 0000000..2c629ac --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/MultiTextLocalizer.lua @@ -0,0 +1,40 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) +local t = require(LuaPackages.t) + +local LocalizationService = require(Root.Localization.LocalizationService) + +local LocalizationContextConsumer = require(script.Parent.LocalizationContextConsumer) + +local validateProps = t.strictInterface({ + keys = t.table, + render = t.callback, +}) + +local validateItem = t.strictInterface({ + key = t.string, + params = t.optional(t.table), +}) + +local function MultiTextLocalizer(props) + assert(validateProps(props)) + for _, item in pairs(props.keys) do + assert(validateItem(item)) + end + + local render = props.render + + return Roact.createElement(LocalizationContextConsumer, { + render = function(localizationContext) + local textMap = {} + for key, item in pairs(props.keys) do + textMap[key] = LocalizationService.getString(localizationContext, item.key, item.params) + end + return render(textMap) + end, + }) +end + +return MultiTextLocalizer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/NativePurchaseFinishedConnector.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/NativePurchaseFinishedConnector.lua new file mode 100644 index 0000000..57eb31e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/NativePurchaseFinishedConnector.lua @@ -0,0 +1,45 @@ +--[[ + Connects to MarketplaceService's callback for completing a native purchase, so that we can + retry after an upsell purchase was processed +]] +local Root = script.Parent.Parent.Parent + +local MarketplaceService = game:GetService("MarketplaceService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local PurchaseError = require(Root.Enums.PurchaseError) +local retryAfterUpsell = require(Root.Thunks.retryAfterUpsell) +local connectToStore = require(Root.connectToStore) + +local ExternalEventConnection = require(script.Parent.ExternalEventConnection) + +local function NativePurchaseFinishedConnector(props) + local nativePurchaseFinished = props.nativePurchaseFinished + + return Roact.createElement(ExternalEventConnection, { + event = MarketplaceService.NativePurchaseFinished, + callback = nativePurchaseFinished, + }) +end + +local function mapDispatchToProps(dispatch) + return { + nativePurchaseFinished = function(player, productId, wasPurchased) + if wasPurchased then + dispatch(retryAfterUpsell()) + else + dispatch(ErrorOccurred(PurchaseError.InvalidFunds)) + end + end, + } +end + +NativePurchaseFinishedConnector = connectToStore( + nil, + mapDispatchToProps +)(NativePurchaseFinishedConnector) + +return NativePurchaseFinishedConnector \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/NumberLocalizer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/NumberLocalizer.lua new file mode 100644 index 0000000..2c93ac9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/NumberLocalizer.lua @@ -0,0 +1,24 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local LocalizationService = require(Root.Localization.LocalizationService) + +local LocalizationContextConsumer = require(script.Parent.LocalizationContextConsumer) + +local function NumberLocalizer(props) + local number = props.number + local render = props.render + + assert(typeof(number) == "number", "prop 'number' must be provided") + assert(typeof(render) == "function", "Render prop must be a function") + + return Roact.createElement(LocalizationContextConsumer, { + render = function(localizationContext) + return render(LocalizationService.formatNumber(localizationContext, number)) + end, + }) +end + +return NumberLocalizer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/PlayerConnector.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/PlayerConnector.lua new file mode 100644 index 0000000..47fe83c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/PlayerConnector.lua @@ -0,0 +1,41 @@ +--[[ + Connects to MarketplaceService's callback for completing a native purchase, so that we can + retry after an upsell purchase was processed +]] +local Root = script.Parent.Parent.Parent + +local Players = game:GetService("Players") +local ABTestService = game:GetService("ABTestService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local connectToStore = require(Root.connectToStore) + +local ExternalEventConnection = require(script.Parent.ExternalEventConnection) + +local function PlayerConnector(props) + local playerConnects = props.playerConnects + + return Roact.createElement(ExternalEventConnection, { + event = Players.PlayerAdded, + callback = playerConnects, + }) +end + +local function mapDispatchToProps(dispatch) + return { + playerConnects = function(player) + if player == Players.LocalPlayer then + ABTestService:InitializeForUserId(Players.LocalPlayer.UserId) + end + end, + } +end + +PlayerConnector = connectToStore( + nil, + mapDispatchToProps +)(PlayerConnector) + +return PlayerConnector \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/TextLocalizer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/TextLocalizer.lua new file mode 100644 index 0000000..9d223f9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/TextLocalizer.lua @@ -0,0 +1,25 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local LocalizationService = require(Root.Localization.LocalizationService) + +local LocalizationContextConsumer = require(script.Parent.LocalizationContextConsumer) + +local function TextLocalizer(props) + local key = props.key + local params = props.params + local render = props.render + + assert(typeof(key) == "string", "String key must be provided") + assert(typeof(render) == "function", "Render prop must be a function") + + return Roact.createElement(LocalizationContextConsumer, { + render = function(localizationContext) + return render(LocalizationService.getString(localizationContext, key, params)) + end, + }) +end + +return TextLocalizer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/provideRobloxLocale.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/provideRobloxLocale.lua new file mode 100644 index 0000000..4ef7847 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/provideRobloxLocale.lua @@ -0,0 +1,23 @@ +--[[ + Helper for supplying the current Roblox Locale to the Roact + tree via context using the LocalizationContextProvider +]] +local Root = script.Parent.Parent.Parent + +local LocalizationService = game:GetService("LocalizationService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local getLocalizationContext = require(Root.Localization.getLocalizationContext) + +local LocalizationContextProvider = require(script.Parent.LocalizationContextProvider) + +local function provideRobloxLocale(renderFunc) + return Roact.createElement(LocalizationContextProvider, { + localizationContext = getLocalizationContext(LocalizationService.RobloxLocaleId), + render = renderFunc + }) +end + +return provideRobloxLocale \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/withLayoutValues.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/withLayoutValues.lua new file mode 100644 index 0000000..e1576fe --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/Connection/withLayoutValues.lua @@ -0,0 +1,18 @@ +--[[ + Helpful wrapper around LayoutValuesConsumer to make it a + little less verbose to use +]] +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local LayoutValuesConsumer = require(script.Parent.LayoutValuesConsumer) + +local function withLayoutValues(renderFunc) + return Roact.createElement(LayoutValuesConsumer, { + render = renderFunc, + }) +end + +return withLayoutValues \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/AutoSizedText.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/AutoSizedText.lua new file mode 100644 index 0000000..90a9537 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/AutoSizedText.lua @@ -0,0 +1,52 @@ +local Root = script.Parent.Parent.Parent +local TextService = game:GetService("TextService") + +local LuaPackages = Root.Parent +local UIBlox = require(LuaPackages.UIBlox) +local Roact = require(LuaPackages.Roact) +local t = require(LuaPackages.t) + +local withStyle = UIBlox.Style.withStyle + +local AutoSizedText = Roact.PureComponent:extend("AutoSizedText") + +local validateProps = t.strictInterface({ + text = t.string, + width = t.number, + layoutOrder = t.number, +}) + +function AutoSizedText:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local fonts = stylePalette.Font + + local text = self.props.text + local textSize = fonts.Body.RelativeSize * fonts.BaseSize + local font = fonts.Body.Font + + local totalTextSize + if text ~= nil then + totalTextSize = TextService:GetTextSize(text, textSize, font, Vector2.new(self.props.width, 10000)) + else + totalTextSize = Vector2.new(0, 0) + end + + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, totalTextSize.Y), + BackgroundTransparency = 1, + Text = text, + TextSize = textSize, + TextColor3 = theme.TextDefault.Color, + TextTransparency = theme.TextDefault.Transparency, + Font = font, + TextXAlignment = Enum.TextXAlignment.Left, + TextWrapped = true, + LayoutOrder = self.props.layoutOrder, + }) + end) +end + +return AutoSizedText \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/AutoSizedText.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/AutoSizedText.spec.lua new file mode 100644 index 0000000..cb85320 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/AutoSizedText.spec.lua @@ -0,0 +1,23 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local AutoSizedText = require(script.Parent.AutoSizedText) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(AutoSizedText, { + text = "Test", + width = 0, + layoutOrder = 0, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/BulletPoint.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/BulletPoint.lua new file mode 100644 index 0000000..0d945b0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/BulletPoint.lua @@ -0,0 +1,73 @@ +local Root = script.Parent.Parent.Parent +local TextService = game:GetService("TextService") + +local LuaPackages = Root.Parent +local UIBlox = require(LuaPackages.UIBlox) +local Roact = require(LuaPackages.Roact) +local t = require(LuaPackages.t) +local Cryo = require(LuaPackages.Cryo) + +local Images = UIBlox.App.ImageSet.Images +local withStyle = UIBlox.Style.withStyle +local IconSize = UIBlox.App.Constant.IconSize + +local CHECK_ICON = "icons/status/success_small" +local TEXT_LEFT_PADDING = IconSize.Small + 16 + +local BulletPoint = Roact.PureComponent:extend("BulletPoint") + +local validateProps = t.strictInterface({ + text = t.string, + width = t.number, + layoutOrder = t.number, +}) + +function BulletPoint:render() + assert(validateProps(self.props)) + + return withStyle(function(stylePalette) + local theme = stylePalette.Theme + local fonts = stylePalette.Font + + local text = self.props.text + local textSize = fonts.Body.RelativeSize * fonts.BaseSize + local font = fonts.Body.Font + + local totalTextSize + if text ~= nil then + totalTextSize = TextService:GetTextSize(text, textSize, font, + Vector2.new(self.props.width - TEXT_LEFT_PADDING, 10000)) + else + totalTextSize = Vector2.new(0, 0) + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, totalTextSize.Y), + BackgroundTransparency = 1, + LayoutOrder = self.props.layoutOrder, + } , { + Roact.createElement("ImageLabel", Cryo.Dictionary.join(Images[CHECK_ICON], { + Position = UDim2.new(0, 2, 0, 2), + Size = UDim2.new(0, IconSize.Small, 0, IconSize.Small), + ImageColor3 = theme.IconDefault.Color, + ImageTransparency = theme.IconDefault.Transparency, + BackgroundTransparency = 1, + })), + Roact.createElement("TextLabel", { + Position = UDim2.new(0, TEXT_LEFT_PADDING, 0, 0), + Size = UDim2.new(1, -TEXT_LEFT_PADDING, 0, totalTextSize.Y), + BackgroundTransparency = 1, + Text = text, + TextSize = textSize, + TextColor3 = theme.TextDefault.Color, + TextTransparency = theme.TextDefault.Transparency, + Font = font, + TextXAlignment = Enum.TextXAlignment.Left, + TextWrapped = true, + LayoutOrder = self.props.layoutOrder, + }) + }) + end) +end + +return BulletPoint \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/BulletPoint.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/BulletPoint.spec.lua new file mode 100644 index 0000000..16e012a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/BulletPoint.spec.lua @@ -0,0 +1,23 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local BulletPoint = require(script.Parent.BulletPoint) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(BulletPoint, { + text = "Test", + width = 0, + layoutOrder = 0, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumModal.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumModal.lua new file mode 100644 index 0000000..0e68a0b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumModal.lua @@ -0,0 +1,209 @@ +local Root = script.Parent.Parent.Parent +local LuaPackages = Root.Parent + +local Roact = require(LuaPackages.Roact) +local Cryo = require(LuaPackages.Cryo) +local FitFrame = require(LuaPackages.FitFrame) +local FitFrameVertical = FitFrame.FitFrameVertical +local UIBlox = require(LuaPackages.UIBlox) +local PartialPageModal = UIBlox.App.Dialog.Modal.PartialPageModal +local ButtonType = UIBlox.App.Button.Enum.ButtonType +local Images = UIBlox.App.ImageSet.Images + +local PromptState = require(Root.Enums.PromptState) +local launchPremiumUpsell = require(Root.Thunks.launchPremiumUpsell) +local hideWindow = require(Root.Thunks.hideWindow) +local connectToStore = require(Root.connectToStore) + +local MultiTextLocalizer = require(script.Parent.Parent.Connection.MultiTextLocalizer) + +local AutoSizedText = require(script.Parent.AutoSizedText) +local BulletPoint = require(script.Parent.BulletPoint) + +local PREMIUM_ICON = "icons/graphic/premium_large" + +local PremiumModal = Roact.Component:extend(script.Name) + +local PREMIUM_MODAL_LOC_KEY = "CoreScripts.PremiumModal.%s" + +local CONTENT_PADDING = 24 +local CONDENSED_CONTENT_PADDING = 12 +local ICON_SIZE = 80 +local CONDENSED_ICON_SIZE = 48 + +-- TODO(esauer): +-- Make the 120 calculation come directly from UIBlox Modal +-- self.updateContentSizes should automatically know how to make the calculation + +function PremiumModal:init() + self.isCondensed = false + -- Hack :( The modal should tell you what the width is, this just prevents having to render + -- 540 (Max modal width) - 24 * 2 (Side paddings) + self.contentSize = Vector2.new(self.props.screenSize.X > 492 and 492 or self.props.screenSize.X, 0) + self.contentSizes, self.changeContentSizes = Roact.createBinding({ + padding = UDim.new(0, CONTENT_PADDING), + iconSize = UDim2.new(1, 0, 0, ICON_SIZE) + }) + + self.purchasePremium = function() + self.props.purchasePremium() + end + + self.updateContentSizes = function() + -- 120 is the height of the components of the modal not including the customized content + if self.isCondensed then + self.isCondensed = self.props.screenSize.Y < self.contentSize.Y + 120 + + ICON_SIZE - CONDENSED_ICON_SIZE + + (CONTENT_PADDING - CONDENSED_CONTENT_PADDING) * 2 + else + self.isCondensed = self.props.screenSize.Y <= self.contentSize.Y + 120 + end + + self.changeContentSizes({ + padding = UDim.new(0, self.isCondensed and CONDENSED_CONTENT_PADDING or CONTENT_PADDING), + iconSize = UDim2.new(1, 0, 0, self.isCondensed and CONDENSED_ICON_SIZE or ICON_SIZE) + }) + end +end + +function PremiumModal:didUpdate(prevProps) + if self.props.screenSize ~= prevProps.screenSize then + self.updateContentSizes() + end +end + +function PremiumModal:render() + local promptState = self.props.promptState + local premiumProductInfo = self.props.premiumProductInfo + local screenSize = self.props.screenSize + + return Roact.createElement(MultiTextLocalizer, { + keys = { + titleLocalizedText = { + key = PREMIUM_MODAL_LOC_KEY:format("Title.PremiumRequired"), + }, + monthlyLocalizedText = { + key = PREMIUM_MODAL_LOC_KEY:format("Action.PricePerMonth"), + params = { + price = premiumProductInfo.currencySymbol..tostring(premiumProductInfo.price) + }, + }, + descLocalizedText = { + key = PREMIUM_MODAL_LOC_KEY:format("Body.Description"), + }, + bulletPoint1Text = { + key = PREMIUM_MODAL_LOC_KEY:format("Body.RobuxMonthlyV2"), + params = { + robux = premiumProductInfo.robuxAmount + }, + }, + bulletPoint2Text = { + key = PREMIUM_MODAL_LOC_KEY:format("Body.PremiumOnlyAreas"), + }, + bulletPoint3Text = { + key = PREMIUM_MODAL_LOC_KEY:format("Body.RobuxDiscount"), + }, + }, + render = function(locTextMap) + return Roact.createElement(PartialPageModal, { + title = locTextMap.titleLocalizedText, + screenSize = screenSize, + buttonStackProps = { + buttons = { + { + buttonType = ButtonType.PrimarySystem, + props = { + isDisabled = promptState ~= PromptState.PremiumUpsell, + onActivated = self.purchasePremium, + text = locTextMap.monthlyLocalizedText, + }, + }, + }, + buttonHeight = 48, + }, + onCloseClicked = self.props.setHideWindow + }, { + Roact.createElement(FitFrameVertical, { + BackgroundTransparency = 1, + width = UDim.new(1, 0), + contentPadding = UDim.new(0, 24), + margin = { + top = 24, + bottom = 24, + }, + [Roact.Change.AbsoluteSize] = function(rbx) + self.contentSize = rbx.AbsoluteSize + self.updateContentSizes() + end + }, { + Icon = Roact.createElement("ImageLabel", Cryo.Dictionary.join(Images[PREMIUM_ICON], { + AnchorPoint = Vector2.new(0.5, 0.5), + Size = self.contentSizes:map(function(values) + return values.iconSize + end), + ScaleType = Enum.ScaleType.Fit, + BackgroundTransparency = 1, + LayoutOrder = 1, + })), + Roact.createElement(AutoSizedText, { + text = locTextMap.descLocalizedText, + width = self.contentSize.X, + layoutOrder = 2, + }), + Roact.createElement(FitFrameVertical, { + BackgroundTransparency = 1, + LayoutOrder = 3, + width = UDim.new(1, 0), + contentPadding = self.contentSizes:map(function(values) + return values.padding + end), + }, { + Bullet1 = Roact.createElement(BulletPoint, { + text = locTextMap.bulletPoint1Text, + width = self.contentSize.X, + layoutOrder = 1, + }), + Bullet2 = Roact.createElement(BulletPoint, { + text = locTextMap.bulletPoint2Text, + width = self.contentSize.X, + layoutOrder = 2, + }), + Bullet3 = Roact.createElement(BulletPoint, { + text = locTextMap.bulletPoint3Text, + width = self.contentSize.X, + layoutOrder = 3, + }) + }), + }) + }) + end + }) +end + +local function mapStateToProps(state) + return { + premiumProductInfo = state.premiumProductInfo, + promptState = state.promptState, + requestType = state.promptRequest.requestType, + purchaseError = state.purchaseError, + windowState = state.windowState, + } +end + +local function mapDispatchToProps(dispatch) + return { + purchasePremium = function() + dispatch(launchPremiumUpsell()) + end, + setHideWindow = function() + dispatch(hideWindow()) + end, + } +end + +PremiumModal = connectToStore( + mapStateToProps, + mapDispatchToProps +)(PremiumModal) + +return PremiumModal \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumModal.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumModal.spec.lua new file mode 100644 index 0000000..424baf6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumModal.spec.lua @@ -0,0 +1,43 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local RequestType = require(Root.Enums.RequestType) + local WindowState = require(Root.Enums.WindowState) + local Reducer = require(Root.Reducers.Reducer) + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local PremiumModal = require(script.Parent.PremiumModal) + PremiumModal = PremiumModal.getUnconnected() + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, { + overrideStore = Rodux.Store.new(Reducer) + }, { + Roact.createElement(PremiumModal, { + premiumProductInfo = { + premiumFeatureTypeName = "Subscription", + mobileProductId = "com.roblox.robloxmobile.RobloxPremium450", + description = "Roblox Premium 450", + price = 4.99, + robuxAmount = 450, + isSubscriptionOnly = false, + currencySymbol = "$", + }, + promptState = PromptState.PremiumUpsell, + promptRequest = { + requestType = RequestType.Premium + }, + windowState = WindowState.Hidden, + screenSize = Vector2.new(100, 100) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumPrompt.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumPrompt.lua new file mode 100644 index 0000000..5c03bf3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumPrompt.lua @@ -0,0 +1,206 @@ +local Root = script.Parent.Parent.Parent +local GuiService = game:GetService("GuiService") +local ContextActionService = game:GetService("ContextActionService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) +local Otter = require(LuaPackages.Otter) +local UIBlox = require(LuaPackages.UIBlox) +local InteractiveAlert = UIBlox.App.Dialog.Alert.InteractiveAlert +local ButtonType = UIBlox.App.Button.Enum.ButtonType + +local PurchaseError = require(Root.Enums.PurchaseError) +local PromptState = require(Root.Enums.PromptState) +local RequestType = require(Root.Enums.RequestType) +local WindowState = require(Root.Enums.WindowState) +local completeRequest = require(Root.Thunks.completeRequest) +local launchPremiumUpsell = require(Root.Thunks.launchPremiumUpsell) +local hideWindow = require(Root.Thunks.hideWindow) +local connectToStore = require(Root.connectToStore) + +local ExternalEventConnection = require(script.Parent.Parent.Connection.ExternalEventConnection) +local MultiTextLocalizer = require(script.Parent.Parent.Connection.MultiTextLocalizer) + +local PremiumModal = require(script.Parent.PremiumModal) + +local PREMIUM_MODAL_LOC_KEY = "CoreScripts.PremiumModal.%s" +local PREMIUM_BUTTON_BIND = "PremiumBackButton" + +local PremiumPrompt = Roact.Component:extend(script.Name) + +local SPRING_CONFIG = { + dampingRatio = 1, + frequency = 1.6, +} + +local function isRelevantRequestType(requestType) + return requestType == RequestType.Premium +end + +function PremiumPrompt:init() + self.state = { + screenSize = Vector2.new(0, 0), + } + + self.changeScreenSize = function(rbx) + if self.state.screenSize ~= rbx.AbsoluteSize then + self:setState({ + screenSize = rbx.AbsoluteSize, + }) + end + end + + local animationProgress, setProgress = Roact.createBinding(0) + + self.motor = Otter.createSingleMotor(0) + self.motor:start() + + self.motor:onStep(setProgress) + self.animationProgress = animationProgress + + self.motor:onComplete(function() + -- Do not complete the request if we are waiting for the browser or native purchase to complete + if self.props.windowState == WindowState.Hidden + and isRelevantRequestType(self.props.requestType) + and self.props.promptState ~= PromptState.UpsellInProgress then + self.props.setCompleteRequest() + end + end) + + self.onClose = function() + self.props.hideWindow() + end +end + +function PremiumPrompt:didUpdate(prevProps, prevState) + if prevProps.windowState ~= self.props.windowState then + local goal = (self.props.windowState == WindowState.Hidden or not isRelevantRequestType(self.props.requestType)) and 0 or 1 + self.motor:setGoal(Otter.spring(goal, SPRING_CONFIG)) + + if self.props.windowState == WindowState.Shown then + ContextActionService:BindCoreAction( + PREMIUM_BUTTON_BIND, + function(actionName, inputState, inputObj) + if inputState == Enum.UserInputState.Begin then + self.onClose() + end + end, false, Enum.KeyCode.ButtonB) + else + ContextActionService:UnbindCoreAction(PREMIUM_BUTTON_BIND) + end + end +end + +function PremiumPrompt:RenderError() + local purchaseError = self.props.purchaseError + + local errorKey + if purchaseError == PurchaseError.AlreadyPremium then + errorKey = "Error.AlreadyPremium" + elseif purchaseError == PurchaseError.PremiumUnavailablePlatform then + errorKey = "Error.PlatformUnavailable" + else + errorKey = "Error.Unavailable" + end + + return Roact.createElement(MultiTextLocalizer, { + keys = { + titleLocalizedText = { + key = PREMIUM_MODAL_LOC_KEY:format("Title.Error"), + }, + errorLocalizedText = { + key = PREMIUM_MODAL_LOC_KEY:format(errorKey), + }, + okLocalizedText = { + key = "CoreScripts.PurchasePrompt.Button.OK", + }, + }, + render = function(textMap) + return Roact.createElement(InteractiveAlert, { + bodyText = textMap.errorLocalizedText, + buttonStackInfo = { + buttons = { + { + buttonType = ButtonType.PrimarySystem, + props = { + onActivated = self.onClose, + text = textMap.okLocalizedText, + }, + }, + }, + }, + screenSize = self.state.screenSize, + title = textMap.titleLocalizedText, + }) + end + }) +end + +function PremiumPrompt:render() + local promptState = self.props.promptState + local requestType = self.props.requestType + + local contents + if promptState == PromptState.None or not isRelevantRequestType(requestType) then + --[[ + When the prompt is hidden, we'd rather not keep unused Roblox + instances for it around, so we don't render them + ]] + contents = nil + else + if promptState == PromptState.Error then + contents = self:RenderError() + else + contents = Roact.createElement(PremiumModal,{ + screenSize = self.state.screenSize, + }) + end + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + AnchorPoint = Vector2.new(0.5, 0), + Position = self.animationProgress:map(function(value) + return UDim2.new(0.5, 0, 1 - value, 0) + end), + [Roact.Change.AbsoluteSize] = self.changeScreenSize, + BackgroundTransparency = 1, + }, { + PremiumPrompt = contents, + OnCoreGuiMenuOpened = Roact.createElement(ExternalEventConnection, { + event = GuiService.MenuOpened, + callback = self.onClose, + }) + }) +end + +local function mapStateToProps(state) + return { + premiumProductInfo = state.premiumProductInfo, + promptState = state.promptState, + requestType = state.promptRequest.requestType, + purchaseError = state.purchaseError, + windowState = state.windowState, + } +end + +local function mapDispatchToProps(dispatch) + return { + purchasePremium = function() + dispatch(launchPremiumUpsell()) + end, + hideWindow = function() + dispatch(hideWindow()) + end, + setCompleteRequest = function() + dispatch(completeRequest()) + end + } +end + +PremiumPrompt = connectToStore( + mapStateToProps, + mapDispatchToProps +)(PremiumPrompt) + +return PremiumPrompt \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumPrompt.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumPrompt.spec.lua new file mode 100644 index 0000000..f792e04 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PremiumPrompt/PremiumPrompt.spec.lua @@ -0,0 +1,42 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local RequestType = require(Root.Enums.RequestType) + local WindowState = require(Root.Enums.WindowState) + local Reducer = require(Root.Reducers.Reducer) + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local PremiumPrompt = require(script.Parent.PremiumPrompt) + PremiumPrompt = PremiumPrompt.getUnconnected() + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, { + overrideStore = Rodux.Store.new(Reducer) + }, { + Roact.createElement(PremiumPrompt, { + premiumProductInfo = { + premiumFeatureTypeName = "Subscription", + mobileProductId = "com.roblox.robloxmobile.RobloxPremium450", + description = "Roblox Premium 450", + price = 4.99, + robuxAmount = 450, + isSubscriptionOnly = false, + currencySymbol = "$", + }, + promptState = PromptState.PremiumUpsell, + promptRequest = { + requestType = RequestType.Premium + }, + windowState = WindowState.Hidden, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AdditionalDetailLabel.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AdditionalDetailLabel.lua new file mode 100644 index 0000000..97cfc2a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AdditionalDetailLabel.lua @@ -0,0 +1,119 @@ +local Root = script.Parent.Parent.Parent + +local UserInputService = game:GetService("UserInputService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + + +local PromptState = require(Root.Enums.PromptState) +local UpsellFlow = require(Root.Enums.UpsellFlow) +local LocalizationService = require(Root.Localization.LocalizationService) +local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow) +local isMockingPurchases = require(Root.Utils.isMockingPurchases) +local getPlayerPrice = require(Root.Utils.getPlayerPrice) +local connectToStore = require(Root.connectToStore) + +local TextLocalizer = require(script.Parent.Parent.Connection.TextLocalizer) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local PURCHASE_DETAILS_KEY = "CoreScripts.PurchasePrompt.PurchaseDetails.%s" + +local function AdditionalDetailLabel(props) + return withLayoutValues(function(values) + local layoutOrder = props.layoutOrder + local messageKey = props.messageKey + local messageParams = props.messageParams + + if messageKey == nil then + -- We return an empty frame to preserve UIListLayout spacing + return Roact.createElement("Frame", { + LayoutOrder = layoutOrder, + Size = values.Size.AdditionalDetailsLabel, + BackgroundTransparency = 1, + BorderSizePixel = 0, + }) + end + + return Roact.createElement(TextLocalizer, { + key = messageKey, + params = messageParams, + render = function(localizedText) + return Roact.createElement("TextLabel", { + Text = localizedText, + LayoutOrder = layoutOrder, + Size = values.Size.AdditionalDetailsLabel, + BackgroundTransparency = 1, + BorderSizePixel = 0, + TextColor3 = Color3.new(1, 1, 1), + Font = Enum.Font.SourceSans, + TextSize = values.TextSize.AdditionalDetails, + TextYAlignment = Enum.TextYAlignment.Top, + TextScaled = true, + TextWrapped = true, + }, { + TextSizeConstraint = Roact.createElement("UITextSizeConstraint", { + MaxTextSize = values.TextSize.AdditionalDetails, + }) + }) + end, + }) + end) +end + +local function mapStateToProps(state) + local promptState = state.promptState + + local messageKey = nil + local messageParams = nil + + local isPlayerPremium = state.accountInfo.membershipType == 4 + local price = getPlayerPrice(state.productInfo, isPlayerPremium) + local balance = state.accountInfo.balance + + if promptState == PromptState.PromptPurchase then + if price == 0 then + messageKey = PURCHASE_DETAILS_KEY:format("BalanceUnaffected") + elseif isMockingPurchases() then + messageKey = PURCHASE_DETAILS_KEY:format("MockPurchase") + else + messageKey = PURCHASE_DETAILS_KEY:format("BalanceFutureV2") + messageParams = { + BALANCE_FUTURE = LocalizationService.numberParam(balance - price), + } + end + elseif promptState == PromptState.RobuxUpsell then + local upsellFlow = getUpsellFlow(UserInputService:GetPlatform()) + + if upsellFlow ~= UpsellFlow.Web then + local upsellRobux = state.nativeUpsell.robuxPurchaseAmount + local amountNeeded = price - balance + + local amountRemaining = upsellRobux - amountNeeded + messageKey = PURCHASE_DETAILS_KEY:format("RemainingAfterUpsell") + messageParams = { + REMAINING_ROBUX = LocalizationService.numberParam(amountRemaining), + } + end + elseif promptState == PromptState.PurchaseComplete then + if isMockingPurchases() then + messageKey = PURCHASE_DETAILS_KEY:format("MockPurchaseComplete") + else + messageKey = PURCHASE_DETAILS_KEY:format("BalanceNow") + messageParams = { + BALANCE_NOW = LocalizationService.numberParam(balance - price), + } + end + end + + return { + messageKey = messageKey, + messageParams = messageParams, + } +end + +AdditionalDetailLabel = connectToStore( + mapStateToProps +)(AdditionalDetailLabel) + +return AdditionalDetailLabel diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AdditionalDetailLabel.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AdditionalDetailLabel.spec.lua new file mode 100644 index 0000000..134eccf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AdditionalDetailLabel.spec.lua @@ -0,0 +1,35 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local AdditionalDetailLabel = require(script.Parent.AdditionalDetailLabel) + + AdditionalDetailLabel = AdditionalDetailLabel.getUnconnected() + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(AdditionalDetailLabel, { + layoutOrder = 1, + messageKey = "test", + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors when showing no text", function() + local emptyMessageElement = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(AdditionalDetailLabel, { + layoutOrder = 1, + }) + }) + + local instance = Roact.mount(emptyMessageElement) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AnimatedDot.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AnimatedDot.lua new file mode 100644 index 0000000..044da75 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AnimatedDot.lua @@ -0,0 +1,38 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local GRAY = Color3.fromRGB(184, 184, 184) +local BLUE = Color3.fromRGB(0, 162, 255) + +local function Dot(props) + local layoutOrder = props.layoutOrder + local time = props.time + local size = 0.8 + local color = GRAY + + if time >= layoutOrder -1 and time <= layoutOrder then + local animationProgress = math.sin(math.pi * (time % 1)) + size = size + (1 - size) * animationProgress + color = GRAY:lerp(BLUE, animationProgress) + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1/3, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + }, { + Dot = Roact.createElement("Frame", { + Size = UDim2.new(0.7, 0, size, 0), + SizeConstraint = Enum.SizeConstraint.RelativeYY, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundColor3 = color, + BorderSizePixel = 0, + }) + }) +end + +return Dot \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AnimatedDot.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AnimatedDot.spec.lua new file mode 100644 index 0000000..0ad803d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AnimatedDot.spec.lua @@ -0,0 +1,18 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local AnimatedDot = require(script.Parent.AnimatedDot) + + it("should create and destroy without errors", function() + local element = Roact.createElement(AnimatedDot, { + time = 1, + layoutOrder = 1, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoResizeList.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoResizeList.lua new file mode 100644 index 0000000..24f92ba --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoResizeList.lua @@ -0,0 +1,82 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) +local Cryo = require(LuaPackages.Cryo) + +local AutoResizeList = Roact.Component:extend("AutoResizeList") + +function AutoResizeList:init() + self.containerRef = Roact.createRef() + self.listRef = Roact.createRef() + + self.contentSizeCallback = function() + if self.listRef.current and self.containerRef.current then + self:resizeContainer() + end + end +end + +function AutoResizeList:didMount() + self:resizeContainer() +end + +function AutoResizeList:resizeContainer() + self.containerRef.current.Size = UDim2.new( + 0, self.listRef.current.AbsoluteContentSize.X, + 0, self.listRef.current.AbsoluteContentSize.Y) +end + +function AutoResizeList:render() + local layoutOrder = self.props.layoutOrder + local position = self.props.position + local anchorPoint = self.props.anchorPoint + local backgroundImage = self.props.backgroundImage + local sliceCenter = self.props.sliceCenter + + local horizontalAlignment = self.props.horizontalAlignment + local verticalAlignment = self.props.verticalAlignment + local fillDirection = self.props.fillDirection + local listPadding = self.props.listPadding + + local children = Cryo.Dictionary.join(self.props[Roact.Children] or {}, { + ["$ListLayout"] = Roact.createElement("UIListLayout", { + HorizontalAlignment = horizontalAlignment, + VerticalAlignment = verticalAlignment, + FillDirection = fillDirection, + Padding = listPadding, + SortOrder = Enum.SortOrder.LayoutOrder, + + [Roact.Ref] = self.listRef, + [Roact.Change.AbsoluteContentSize] = self.contentSizeCallback, + }) + }) + + if backgroundImage == nil then + return Roact.createElement("Frame", { + LayoutOrder = layoutOrder, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Position = position, + AnchorPoint = anchorPoint, + + [Roact.Ref] = self.containerRef, + }, children) + else + local scaleType = (sliceCenter ~= nil) and Enum.ScaleType.Slice or Enum.ScaleType.Stretch + return Roact.createElement("ImageLabel", { + Image = backgroundImage, + LayoutOrder = layoutOrder, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScaleType = scaleType, + SliceCenter = sliceCenter, + Position = position, + AnchorPoint = anchorPoint, + + [Roact.Ref] = self.containerRef, + }, children) + end +end + +return AutoResizeList \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoResizeList.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoResizeList.spec.lua new file mode 100644 index 0000000..7e5bb97 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoResizeList.spec.lua @@ -0,0 +1,17 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local AutoResizeList = require(script.Parent.AutoResizeList) + + it("should create and destroy without errors", function() + local element = Roact.createElement(AutoResizeList, { + layoutOrder = 1, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoSizedTextLabel.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoSizedTextLabel.lua new file mode 100644 index 0000000..c4a5687 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoSizedTextLabel.lua @@ -0,0 +1,38 @@ +local Root = script.Parent.Parent.Parent +local TextService = game:GetService("TextService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) +local Cryo = require(LuaPackages.Cryo) + +local AutoSizedTextLabel = Roact.PureComponent:extend("AutoSizedTextLabel") + +function AutoSizedTextLabel:render() + local text = self.props.Text + local textSize = self.props.TextSize + local font = self.props.Font + local width = self.props.width + local maxHeight = self.props.maxHeight + + local totalTextSize + if text ~= nil then + totalTextSize = TextService:GetTextSize(text, textSize, font, Vector2.new(width, 10000)) + else + totalTextSize = Vector2.new(0, 0) + end + + local height = totalTextSize.Y + if maxHeight and height > maxHeight then + height = maxHeight + end + + local textLabelProps = Cryo.Dictionary.join(self.props, { + width = Cryo.None, + maxHeight = Cryo.None, + Size = UDim2.new(0, width, 0, height), + }) + + return Roact.createElement("TextLabel", textLabelProps) +end + +return AutoSizedTextLabel diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoSizedTextLabel.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoSizedTextLabel.spec.lua new file mode 100644 index 0000000..75b467e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/AutoSizedTextLabel.spec.lua @@ -0,0 +1,46 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local AutoSizedTextLabel = require(script.Parent.AutoSizedTextLabel) + + it("should create and destroy without errors", function() + local element = Roact.createElement(AutoSizedTextLabel, { + Text = "Hello", + TextSize = 10, + Font = Enum.Font.SourceSans, + width = 100, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should not throw even if no text is provided", function() + local element = Roact.createElement(AutoSizedTextLabel, { + TextSize = 10, + Font = Enum.Font.SourceSans, + width = 100, + }) + + expect(Roact.mount(element)).to.be.ok() + end) + + it("should clamp its height if maxHeight was provided", function() + local element = Roact.createElement(AutoSizedTextLabel, { + Text = "Really long text that should get to a height larger than 1 line!", + TextSize = 10, + Font = Enum.Font.SourceSans, + width = 100, + maxHeight = 2, + }) + + local folder = Instance.new("Frame") + Roact.mount(element, folder) + local textLabel = folder:FindFirstChildWhichIsA("GuiObject") + + expect(textLabel.Size.Y.Offset).to.equal(2) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Button.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Button.lua new file mode 100644 index 0000000..2fa4f4d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Button.lua @@ -0,0 +1,161 @@ +local Root = script.Parent.Parent.Parent +local ContextActionService = game:GetService("ContextActionService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local connectToStore = require(Root.connectToStore) + +local TextLocalizer = require(script.Parent.Parent.Connection.TextLocalizer) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local Button = Roact.PureComponent:extend("Button") + +function Button:init() + local onClick = self.props.onClick + local imageDown = self.props.imageDown + local imageUp = self.props.imageUp + + self.state = { + currentImage = imageUp + } + + self.inputBegan = function() + self:setState({ + currentImage = imageDown + }) + end + + self.inputEnded = function() + self:setState({ + currentImage = imageUp + }) + end + + self.activated = function() + onClick() + self:setState({ + currentImage = imageUp + }) + end +end + +function Button:didMount() + -- Some buttons need to support additional button bindings + local bindings = self.props.additionalBindings or {} + table.insert(bindings, self.props.gamepadButton) + + ContextActionService:BindCoreAction( + self.props.stringKey, + function(actionName, inputState, inputObj) + --[[ + CLILUACORE-521: + InputState MUST be 'Begin' in this case; otherwise, opening + the settings menu it will create new ContextActionService + bindings. When it does this, they trigger the 'Cancel' input + state, which invoke our binding (in order to tell it that + it's being canceled) + ]] + if inputState == Enum.UserInputState.Begin then + self.props.onClick() + end + end, + false, + unpack(bindings) + ) +end + +function Button:willUnmount() + ContextActionService:UnbindCoreAction(self.props.stringKey) +end + +function Button:render() + assert(typeof(self.props.gamepadButton) == "EnumItem" + and self.props.gamepadButton.EnumType == Enum.KeyCode, + "Prop 'gamepadButton' is required and must be of type Enum.KeyCode") + + return withLayoutValues(function(values) + local stringKey = self.props.stringKey + local font = self.props.font + local size = self.props.size + local position = self.props.position + + local gamepadEnabled = self.props.gamepadEnabled + local gamepadButton = self.props.gamepadButton + + local imageData = values.Image[self.state.currentImage] + + local buttonContents = { + ButtonLabel = Roact.createElement(TextLocalizer, { + key = stringKey, + render = function(localizedText) + return Roact.createElement("TextLabel", { + Text = localizedText, + Font = font, + Size = UDim2.new(0.6, 0, 0.8, 0), + Position = UDim2.new(0.2, 0, 0.1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + TextColor3 = Color3.new(1, 1, 1), + TextSize = values.TextSize.Button, + TextScaled = true, + TextWrapped = false, + LayoutOrder = 2, + }, { + TextSizeConstraint = Roact.createElement("UITextSizeConstraint", { + MaxTextSize = values.TextSize.Button, + }), + }) + end, + }) + } + + if gamepadEnabled then + -- Using a frame allows the icon to be left-aligned and still + -- be in a size-constrained section of the overall button + buttonContents.ButtonIcon = Roact.createElement("Frame", { + Size = UDim2.new(0.2, 0, 0.8, 0), + Position = UDim2.new(0, 0, 0.1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + Icon = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, values.Size.ButtonIconPadding, 0, values.Size.ButtonIconYOffset), + SizeConstraint = Enum.SizeConstraint.RelativeYY, + ScaleType = Enum.ScaleType.Fit, + Image = values.Image[gamepadButton.Name].Path, + BackgroundTransparency = 1, + BorderSizePixel = 0, + }) + }) + end + + return Roact.createElement("ImageButton", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + AutoButtonColor = false, + Modal = true, + + Size = size, + Position = position, + ScaleType = Enum.ScaleType.Slice, + Image = imageData.Path, + SliceCenter = imageData.SliceCenter, + + [Roact.Event.InputBegan] = self.inputBegan, + [Roact.Event.InputEnded] = self.inputEnded, + [Roact.Event.Activated] = self.activated, + }, buttonContents) + end) +end + +local function mapStateToProps(state) + return { + gamepadEnabled = state.gamepadEnabled + } +end + +Button = connectToStore(mapStateToProps)(Button) + +return Button \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Button.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Button.spec.lua new file mode 100644 index 0000000..1b2f929 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Button.spec.lua @@ -0,0 +1,50 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local Button = require(script.Parent.Button) + + Button = Button.getUnconnected() + + it("should create and destroy without errors with gamepad disabled", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(Button, { + gamepadEnabled = false, + + stringKey = "testing123", + gamepadButton = Enum.KeyCode.ButtonA, + font = Enum.Font.SourceSans, + imageUp = "ButtonUp", + imageDown = "ButtonDown", + onClick = function() + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with gamepad enabled", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(Button, { + gamepadEnabled = true, + + stringKey = "testing123", + gamepadButton = Enum.KeyCode.ButtonA, + font = Enum.Font.SourceSans, + imageUp = "ButtonUp", + imageDown = "ButtonDown", + onClick = function() + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/CancelButton.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/CancelButton.lua new file mode 100644 index 0000000..8d44052 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/CancelButton.lua @@ -0,0 +1,28 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local Button = require(script.Parent.Button) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local function CancelButton(props) + return withLayoutValues(function(values) + local onClick = props.onClick + + return Roact.createElement(Button, { + font = Enum.Font.SourceSans, + imageUp = "ButtonUpRight", + imageDown = "ButtonDownRight", + gamepadButton = Enum.KeyCode.ButtonB, + + stringKey = "CoreScripts.PurchasePrompt.CancelPurchase.Cancel", + size = UDim2.new(0.5, 0, 1, 0), + position = UDim2.new(0.5, 0, 0, 0), + + onClick = onClick, + }) + end) +end + +return CancelButton \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/CancelButton.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/CancelButton.spec.lua new file mode 100644 index 0000000..7a6233b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/CancelButton.spec.lua @@ -0,0 +1,22 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local CancelButton = require(script.Parent.CancelButton) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(CancelButton, { + onClick = function() + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ConfirmButton.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ConfirmButton.lua new file mode 100644 index 0000000..18c2e3d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ConfirmButton.lua @@ -0,0 +1,52 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local ClickScamDetector = require(Root.Utils.ClickScamDetector) + +local Button = require(script.Parent.Button) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local CONFIRM_BUTTON = Enum.KeyCode.ButtonA + +local ConfirmButton = Roact.Component:extend("ConfirmButton") + +function ConfirmButton:init() + local onClick = self.props.onClick + + self.activated = function() + if self.clickScamDetector:isClickValid() then + onClick() + end + end + + self.clickScamDetector = ClickScamDetector.new({ + buttonInput = CONFIRM_BUTTON, + initialDelay = 0.5, + }) +end + +function ConfirmButton:willUnmount() + self.clickScamDetector:destroy() +end + +function ConfirmButton:render() + return withLayoutValues(function(values) + local stringKey = self.props.stringKey + + return Roact.createElement(Button, { + font = Enum.Font.SourceSansBold, + imageUp = "ButtonUpLeft", + imageDown = "ButtonDownLeft", + gamepadButton = CONFIRM_BUTTON, + + stringKey = stringKey, + size = UDim2.new(0.5, 0, 1, 0), + + onClick = self.activated, + }) + end) +end + +return ConfirmButton \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ConfirmButton.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ConfirmButton.spec.lua new file mode 100644 index 0000000..164cbe9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ConfirmButton.spec.lua @@ -0,0 +1,23 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local ConfirmButton = require(script.Parent.ConfirmButton) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(ConfirmButton, { + stringKey = "testing123", + onClick = function() + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/InProgressContents.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/InProgressContents.lua new file mode 100644 index 0000000..48366f6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/InProgressContents.lua @@ -0,0 +1,62 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local TextLocalizer = require(script.Parent.Parent.Connection.TextLocalizer) + +local AutoSizedTextLabel = require(script.Parent.AutoSizedTextLabel) +local PurchasingAnimation = require(script.Parent.PurchasingAnimation) + +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local function InProgressContents(props) + return withLayoutValues(function(values) + return Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + + ScaleType = Enum.ScaleType.Slice, + Image = values.Image.InProgressBackground.Path, + SliceCenter = values.Image.InProgressBackground.SliceCenter, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + ListLayout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 20) + }), + PurchasingText = Roact.createElement(TextLocalizer, { + key = "CoreScripts.PurchasePrompt.Purchasing", + render = function(localizedText) + return Roact.createElement(AutoSizedTextLabel, { + width = values.Size.Dialog.X.Offset, + Text = localizedText, + BackgroundTransparency = 1, + BorderSizePixel = 0, + TextColor3 = Color3.new(1, 1, 1), + Font = Enum.Font.SourceSans, + TextSize = values.TextSize.Purchasing, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + TextScaled = true, + TextWrapped = true, + LayoutOrder = 1, + }, { + TextSizeConstraint = Roact.createElement("UITextSizeConstraint", { + MaxTextSize = values.TextSize.Purchasing, + }), + }) + end, + }), + PurchasingAnimation = Roact.createElement(PurchasingAnimation, { + layoutOrder = 2, + }), + }) + end) +end + +return InProgressContents \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/InProgressContents.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/InProgressContents.spec.lua new file mode 100644 index 0000000..c8550d8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/InProgressContents.spec.lua @@ -0,0 +1,22 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local InProgressContents = require(script.Parent.InProgressContents) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(InProgressContents, { + anchorPoint = Vector2.new(0, 0), + position = UDim2.new(0, 0, 0, 0), + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ItemPreviewImage.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ItemPreviewImage.lua new file mode 100644 index 0000000..fe51de3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ItemPreviewImage.lua @@ -0,0 +1,94 @@ +local Root = script.Parent.Parent.Parent +local LuaPackages = Root.Parent +local Cryo = require(LuaPackages.Cryo) +local Roact = require(LuaPackages.Roact) +local UIBlox = require(LuaPackages.UIBlox) +local UIBloxImages = UIBlox.App.ImageSet.Images + +local PurchaseError = require(Root.Enums.PurchaseError) +local PromptState = require(Root.Enums.PromptState) +local connectToStore = require(Root.connectToStore) + +local PREMIUM_ICON = UIBloxImages["icons/graphic/premium_large"] +local ADULT_ERROR_ICON = UIBloxImages["icons/status/error_large"] + +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local function ItemPreviewImage(props) + return withLayoutValues(function(values) + local layoutOrder = props.layoutOrder + local promptState = props.promptState + local purchaseError = props.purchaseError + local productImageUrl = props.productImageUrl + + local showPremiumIcon = false + local showAdultErrorIcon = false + local backgroundTransparency = 0 + if promptState == PromptState.AdultConfirmation then + showAdultErrorIcon = true + backgroundTransparency = 1 + elseif promptState == PromptState.Error then + if purchaseError == PurchaseError.PremiumOnly then + showPremiumIcon = true + else + productImageUrl = values.Image.ErrorIcon.Path + end + end + + return Roact.createElement("Frame", { + Size = values.Size.ItemPreviewContainerFrame, + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + }, { + PremiumIcon = showPremiumIcon and Roact.createElement("ImageLabel", Cryo.Dictionary.join(PREMIUM_ICON, { + Size = values.Size.ItemPreview, + BackgroundTransparency = 1, + BorderSizePixel = 0, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + ImageTransparency = 0, + })) or nil, + ItemPreviewImageContainer = not showPremiumIcon and Roact.createElement("Frame", { + Size = values.Size.ItemPreviewWhiteFrame, + BackgroundTransparency = backgroundTransparency, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + BorderSizePixel = 0, + BackgroundColor3 = Color3.new(1, 1, 1), + }, { + ItemImage = showAdultErrorIcon and Roact.createElement("ImageLabel", Cryo.Dictionary.join(ADULT_ERROR_ICON, { + Size = values.Size.ItemPreview, + BackgroundTransparency = 1, + BorderSizePixel = 0, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + ImageTransparency = 0, + })) + or Roact.createElement("ImageLabel", { + Size = values.Size.ItemPreview, + BackgroundTransparency = 1, + BorderSizePixel = 0, + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Image = productImageUrl, + ImageTransparency = 0, + }), + }) or nil, + }) + end) +end + +local function mapStateToProps(state) + return { + promptState = state.promptState, + purchaseError = state.purchaseError, + productImageUrl = state.productInfo.imageUrl, + } +end + +ItemPreviewImage = connectToStore( + mapStateToProps +)(ItemPreviewImage) + +return ItemPreviewImage diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ItemPreviewImage.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ItemPreviewImage.spec.lua new file mode 100644 index 0000000..4c53b17 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ItemPreviewImage.spec.lua @@ -0,0 +1,24 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local ItemPreviewImage = require(script.Parent.ItemPreviewImage) + + ItemPreviewImage = ItemPreviewImage.getUnconnected() + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(ItemPreviewImage, { + layoutOrder = 1, + imageUrl = "", + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/OkButton.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/OkButton.lua new file mode 100644 index 0000000..2d6612d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/OkButton.lua @@ -0,0 +1,30 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local Button = require(script.Parent.Button) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local function OkButton(props) + return withLayoutValues(function(values) + local onClick = props.onClick + + return Roact.createElement(Button, { + font = Enum.Font.SourceSans, + imageUp = "ButtonUp", + imageDown = "ButtonDown", + gamepadButton = Enum.KeyCode.ButtonA, + additionalBindings = { + Enum.KeyCode.ButtonB, + }, + + stringKey = "CoreScripts.PurchasePrompt.Button.OK", + size = UDim2.new(1, 0, 0, values.Size.ButtonHeight-4), + + onClick = onClick + }) + end) +end + +return OkButton \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/OkButton.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/OkButton.spec.lua new file mode 100644 index 0000000..0a97258 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/OkButton.spec.lua @@ -0,0 +1,22 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local OkButton = require(script.Parent.OkButton) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(OkButton, { + onClick = function() + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Price.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Price.lua new file mode 100644 index 0000000..417e54f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Price.lua @@ -0,0 +1,60 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local NumberLocalizer = require(script.Parent.Parent.Connection.NumberLocalizer) +local AutoResizeList = require(script.Parent.AutoResizeList) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +return function(props) + return withLayoutValues(function(values) + local layoutOrder = props.layoutOrder + local price = props.price + + return Roact.createElement(AutoResizeList, { + layoutOrder = layoutOrder, + horizontalAlignment = Enum.HorizontalAlignment.Left, + verticalAlignment = Enum.VerticalAlignment.Center, + fillDirection = Enum.FillDirection.Horizontal, + }, { + RobuxIconContainer = Roact.createElement("Frame", { + BorderSizePixel = 0, + BackgroundTransparency = 1, + Size = values.Size.RobuxIconContainerFrame, + LayoutOrder = 1, + }, { + RobuxIcon = Roact.createElement("ImageLabel", { + Size = values.Size.RobuxIcon, + BackgroundTransparency = 1, + BorderSizePixel = 0, + AnchorPoint = Vector2.new(0, 0.5), + Position = UDim2.new(0, 0, 0.5, 0), + Image = values.Image.RobuxIcon.Path, + }), + }), + PriceTextLabel = Roact.createElement(NumberLocalizer, { + number = price, + render = function(localizedNumber) + return Roact.createElement("TextLabel", { + Text = localizedNumber, + LayoutOrder = 2, + Size = values.Size.PriceTextLabel, + BackgroundTransparency = 1, + BorderSizePixel = 0, + TextColor3 = values.TextColor.PriceLabel, + Font = Enum.Font.SourceSansBold, + TextSize = values.TextSize.Default, + TextXAlignment = Enum.TextXAlignment.Left, + TextScaled = true, + TextWrapped = true, + }, { + TextSizeConstraint = Roact.createElement("UITextSizeConstraint", { + MaxTextSize = values.TextSize.Default, + }) + }) + end, + }) + }) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Price.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Price.spec.lua new file mode 100644 index 0000000..30a5ac6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/Price.spec.lua @@ -0,0 +1,23 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local Price = require(script.Parent.Price) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(Price, { + layoutOrder = 1, + imageUrl = "", + price = 50, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ProductDescription.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ProductDescription.lua new file mode 100644 index 0000000..bed229a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ProductDescription.lua @@ -0,0 +1,148 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local PromptState = require(Root.Enums.PromptState) +local PurchaseError = require(Root.Enums.PurchaseError) + +local LocalizationService = require(Root.Localization.LocalizationService) + +local TextLocalizer = require(script.Parent.Parent.Connection.TextLocalizer) +local AutoSizedTextLabel = require(script.Parent.AutoSizedTextLabel) +local Price = require(script.Parent.Price) + +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local getPlayerPrice = require(Root.Utils.getPlayerPrice) +local connectToStore = require(Root.connectToStore) + +local PURCHASE_MESSAGE_KEY = "CoreScripts.PurchasePrompt.PurchaseMessage.%s" + +local function ProductDescription(props) + return withLayoutValues(function(values) + local layoutOrder = props.layoutOrder + local descriptionKey = props.descriptionKey + local descriptionParams = props.descriptionParams + local showPrice = props.showPrice + local price = props.price + + return Roact.createElement("Frame", { + BorderSizePixel = 0, + Size = values.Size.ProductDescription, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + ProductDescriptionPadding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, values.Size.ProductDescriptionPaddingTop), + }), + ProductDescriptionListLayout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + ProductDescriptionText = Roact.createElement(TextLocalizer, { + key = descriptionKey, + params = descriptionParams, + render = function(localizedText) + return Roact.createElement(AutoSizedTextLabel, { + width = values.Size.ProductDescription.X.Offset - values.Size.HorizontalPadding, + maxHeight = showPrice + and values.Size.ProductDescription.Y.Offset - values.Size.RobuxIconContainerFrame.Y.Offset + or values.Size.ProductDescription.Y.Offset, + Text = localizedText, + BackgroundTransparency = 1, + BorderSizePixel = 0, + TextColor3 = Color3.new(1, 1, 1), + Font = Enum.Font.SourceSans, + TextSize = values.TextSize.ProductDescription, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextScaled = true, + TextWrapped = true, + LayoutOrder = 1, + }, { + TextSizeConstraint = Roact.createElement("UITextSizeConstraint", { + MaxTextSize = values.TextSize.ProductDescription, + }), + }) + end, + }), + Price = showPrice and Roact.createElement(Price, { + layoutOrder = 2, + price = price, + }) or nil, + }) + end) +end + +local function mapStateToProps(state) + local promptState = state.promptState + local isPlayerPremium = state.accountInfo.membershipType == 4 + local price = getPlayerPrice(state.productInfo, isPlayerPremium) + local isFree = price == 0 + local canPurchase = promptState ~= PromptState.Error + + local descriptionKey + local descriptionParams + + + if promptState == PromptState.PurchaseComplete then + descriptionKey = PURCHASE_MESSAGE_KEY:format("Succeeded") + descriptionParams = { + ITEM_NAME = state.productInfo.name, + NEEDED_AMOUNT = LocalizationService.numberParam( + price - state.accountInfo.balance + ), + ASSET_TYPE = LocalizationService.nestedKeyParam( + LocalizationService.getKeyFromItemType(state.productInfo.itemType)) + } + elseif promptState == PromptState.RobuxUpsell then + descriptionKey = PURCHASE_MESSAGE_KEY:format("NeedMoreRobux") + descriptionParams = { + ITEM_NAME = state.productInfo.name, + NEEDED_AMOUNT = LocalizationService.numberParam( + price - state.accountInfo.balance + ), + ASSET_TYPE = LocalizationService.nestedKeyParam( + LocalizationService.getKeyFromItemType(state.productInfo.itemType)) + } + elseif promptState == PromptState.AdultConfirmation then + descriptionKey = "CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText" + elseif promptState == PromptState.Error then + descriptionKey = LocalizationService.getErrorKey(state.purchaseError) + if state.purchaseError == PurchaseError.UnknownFailure then + descriptionParams = { + ITEM_NAME = state.productInfo.name + } + end + elseif promptState ~= PromptState.None then + if isFree then + descriptionKey = PURCHASE_MESSAGE_KEY:format("Free") + descriptionParams = { + ITEM_NAME = state.productInfo.name, + } + else + descriptionKey = PURCHASE_MESSAGE_KEY:format("Purchase") + descriptionParams = { + ASSET_TYPE = LocalizationService.nestedKeyParam( + LocalizationService.getKeyFromItemType(state.productInfo.itemType)), + ITEM_NAME = state.productInfo.name, + } + end + end + + return { + descriptionKey = descriptionKey, + descriptionParams = descriptionParams, + showPrice = not isFree and canPurchase and promptState ~= PromptState.AdultConfirmation, + price = price, + } +end + +ProductDescription = connectToStore( + mapStateToProps +)(ProductDescription) + +return ProductDescription diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ProductDescription.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ProductDescription.spec.lua new file mode 100644 index 0000000..f6f191c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/ProductDescription.spec.lua @@ -0,0 +1,31 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + local Rodux = require(LuaPackages.Rodux) + + local Reducer = require(Root.Reducers.Reducer) + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local ProductDescription = require(script.Parent.ProductDescription) + + ProductDescription = ProductDescription.getUnconnected() + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, { + overrideStore = Rodux.Store.new(Reducer, { + productInfo = { + price = 10, + }, + }) + }, { + Roact.createElement(ProductDescription, { + descriptionKey = "CoreScripts.PurchasePrompt.PurchaseMessage.Purchase", + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptButtons.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptButtons.lua new file mode 100644 index 0000000..0af92dd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptButtons.lua @@ -0,0 +1,100 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local PromptState = require(Root.Enums.PromptState) +local purchaseItem = require(Root.Thunks.purchaseItem) +local launchRobuxUpsell = require(Root.Thunks.launchRobuxUpsell) +local getPlayerPrice = require(Root.Utils.getPlayerPrice) +local connectToStore = require(Root.connectToStore) + +local ConfirmButton = require(script.Parent.ConfirmButton) +local CancelButton = require(script.Parent.CancelButton) +local OkButton = require(script.Parent.OkButton) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local CONFIRM_PURCHASE_KEY = "CoreScripts.PurchasePrompt.ConfirmPurchase.%s" + +local PromptButtons = Roact.PureComponent:extend("PromptButtons") + +function PromptButtons:render() + return withLayoutValues(function(values) + local layoutOrder = self.props.layoutOrder + local onClose = self.props.onClose + local promptState = self.props.promptState + local price = self.props.price + + local onBuy = self.props.onBuy + local onRobuxUpsell = self.props.onRobuxUpsell + + local children + if promptState == PromptState.PurchaseComplete + or promptState == PromptState.Error + then + children = { + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, 4), + }), + OkButton = Roact.createElement(OkButton, { + onClick = onClose, + }), + } + else + local confirmButtonStringKey = CONFIRM_PURCHASE_KEY:format("BuyNow") + local leftButtonCallback = onBuy + if price == 0 then + confirmButtonStringKey = CONFIRM_PURCHASE_KEY:format("TakeFree") + elseif promptState == PromptState.RobuxUpsell then + confirmButtonStringKey = CONFIRM_PURCHASE_KEY:format("BuyRobuxV2") + leftButtonCallback = onRobuxUpsell + elseif promptState == PromptState.AdultConfirmation then + confirmButtonStringKey = "CoreScripts.PurchasePrompt.Button.OK" + leftButtonCallback = onRobuxUpsell + end + children = { + ConfirmButton = Roact.createElement(ConfirmButton, { + stringKey = confirmButtonStringKey, + onClick = leftButtonCallback, + }), + CancelButton = Roact.createElement(CancelButton, { + onClick = onClose, + }), + } + end + + return Roact.createElement("Frame", { + LayoutOrder = layoutOrder, + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(1, 0, 0, values.Size.ButtonHeight) + }, children) + end) +end + +local function mapStateToProps(state) + local isPlayerPremium = state.accountInfo.membershipType == 4 + local price = getPlayerPrice(state.productInfo, isPlayerPremium) + return { + promptState = state.promptState, + price = price, + } +end + +local function mapDispatchToProps(dispatch) + return { + onBuy = function() + dispatch(purchaseItem()) + end, + onRobuxUpsell = function() + dispatch(launchRobuxUpsell()) + end, + } +end + +PromptButtons = connectToStore( + mapStateToProps, + mapDispatchToProps +)(PromptButtons) + +return PromptButtons diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptButtons.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptButtons.spec.lua new file mode 100644 index 0000000..2cf3e66 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptButtons.spec.lua @@ -0,0 +1,44 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local PromptState = require(Root.Enums.PromptState) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local PromptButtons = require(script.Parent.PromptButtons) + PromptButtons = PromptButtons.getUnconnected() + + local function noop() + end + + it("should create and destroy without errors with one button", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(PromptButtons, { + layoutOrder = 1, + onClose = noop, + promptState = PromptState.PurchaseComplete, + price = 1, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should create and destroy without errors with two buttons", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(PromptButtons, { + layoutOrder = 1, + onClose = noop, + promptState = PromptState.PromptPurchase, + price = 1, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptContents.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptContents.lua new file mode 100644 index 0000000..904e6cc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptContents.lua @@ -0,0 +1,56 @@ +local Root = script.Parent.Parent.Parent + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local AutoResizeList = require(script.Parent.AutoResizeList) +local ItemPreviewImage = require(script.Parent.ItemPreviewImage) +local ProductDescription = require(script.Parent.ProductDescription) +local PromptButtons = require(script.Parent.PromptButtons) +local AdditionalDetailLabel = require(script.Parent.AdditionalDetailLabel) + +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local function PromptContents(props) + return withLayoutValues(function(values) + local onClose = props.onClose + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BorderSizePixel = 0, + BackgroundTransparency = 1, + }, { + ListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + PromptBody = Roact.createElement(AutoResizeList, { + layoutOrder = 1, + backgroundImage = values.Image.PromptBackground.Path, + sliceCenter = values.Image.PromptBackground.SliceCenter, + fillDirection = Enum.FillDirection.Vertical, + }, { + ProductInfo = Roact.createElement(AutoResizeList, { + layoutOrder = 1, + fillDirection = Enum.FillDirection.Horizontal, + }, { + ItemPreviewImage = Roact.createElement(ItemPreviewImage, { + layoutOrder = 1, + }), + ProductDescription = Roact.createElement(ProductDescription, { + layoutOrder = 2, + }) + }), + AdditionalDetails = Roact.createElement(AdditionalDetailLabel, { + layoutOrder = 2, + }), + }), + PromptButtons = Roact.createElement(PromptButtons, { + layoutOrder = 2, + onClose = onClose, + }), + }) + end) +end + +return PromptContents diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptContents.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptContents.spec.lua new file mode 100644 index 0000000..1804892 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PromptContents.spec.lua @@ -0,0 +1,39 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local PromptContents = require(script.Parent.PromptContents) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, { + promptState = PromptState.PromptPurchase, + overrideStore = Rodux.Store.new(Reducer, { + promptState = PromptState.PromptPurchase, + accountInfo = { + balance = 100, + }, + productInfo = { + assetTypeId = 2, -- T-shirt + price = 10, + itemType = 2, + }, + }) + }, { + Roact.createElement(PromptContents, { + layoutOrder = 1, + onClose = function() + end, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasePrompt.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasePrompt.lua new file mode 100644 index 0000000..831691a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasePrompt.lua @@ -0,0 +1,126 @@ +local Root = script.Parent.Parent.Parent +local GuiService = game:GetService("GuiService") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) +local Otter = require(LuaPackages.Otter) + +local RequestType = require(Root.Enums.RequestType) +local PromptState = require(Root.Enums.PromptState) +local WindowState = require(Root.Enums.WindowState) +local hideWindow = require(Root.Thunks.hideWindow) +local completeRequest = require(Root.Thunks.completeRequest) +local connectToStore = require(Root.connectToStore) + +local ExternalEventConnection = require(script.Parent.Parent.Connection.ExternalEventConnection) +local PromptContents = require(script.Parent.PromptContents) +local InProgressContents = require(script.Parent.InProgressContents) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local PurchasePrompt = Roact.Component:extend(script.Name) + +local SPRING_CONFIG = { + dampingRatio = 1, + frequency = 1.6, +} + +local function isRelevantRequestType(requestType) + return requestType == RequestType.Asset + or requestType == RequestType.Bundle + or requestType == RequestType.GamePass + or requestType == RequestType.Product + or requestType == RequestType.Subscription +end + +function PurchasePrompt:init() + local animationProgress, setProgress = Roact.createBinding(0) + + self.motor = Otter.createSingleMotor(0) + self.motor:start() + + self.motor:onStep(setProgress) + self.animationProgress = animationProgress + + self.motor:onComplete(function() + if self.props.windowState == WindowState.Hidden and isRelevantRequestType(self.props.requestType) then + self.props.completeRequest() + end + end) + + self.onClose = function() + self.props.hideWindow() + end +end + +function PurchasePrompt:didUpdate(prevProps, prevState) + if prevProps.windowState ~= self.props.windowState then + local goal = (self.props.windowState == WindowState.Hidden or not isRelevantRequestType(self.props.requestType)) and 0 or 1 + self.motor:setGoal(Otter.spring(goal, SPRING_CONFIG)) + end +end + +function PurchasePrompt:render() + return withLayoutValues(function(values) + local promptState = self.props.promptState + local requestType = self.props.requestType + + local contents + if promptState == PromptState.None or not isRelevantRequestType(requestType) then + --[[ + When the prompt is hidden, we'd rather not keep unused Roblox + instances for it around, so we don't render them + ]] + contents = nil + elseif promptState == PromptState.PurchaseInProgress or promptState == PromptState.UpsellInProgress then + contents = Roact.createElement(InProgressContents) + else + contents = Roact.createElement(PromptContents, { + onClose = self.onClose, + }) + end + + return Roact.createElement("Frame", { + Size = values.Size.Dialog, + BorderSizePixel = 0, + BackgroundTransparency = 1, + AnchorPoint = self.animationProgress:map(function(value) + return Vector2.new(0.5, 1 - 0.5 * value) + end), + Position = self.animationProgress:map(function(value) + return UDim2.new(0.5, 0, 0.5 * value, 0) + end), + }, { + PromptContents = contents, + OnCoreGuiMenuOpened = Roact.createElement(ExternalEventConnection, { + event = GuiService.MenuOpened, + callback = self.onClose, + }) + }) + end) +end + +local function mapStateToProps(state) + return { + promptState = state.promptState, + requestType = state.promptRequest.requestType, + windowState = state.windowState, + } +end + +local function mapDispatchToProps(dispatch) + return { + hideWindow = function() + dispatch(hideWindow()) + end, + completeRequest = function() + dispatch(completeRequest()) + end + } +end + +PurchasePrompt = connectToStore( + mapStateToProps, + mapDispatchToProps +)(PurchasePrompt) + +return PurchasePrompt diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasePrompt.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasePrompt.spec.lua new file mode 100644 index 0000000..19e94d3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasePrompt.spec.lua @@ -0,0 +1,35 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local PurchasePrompt = require(script.Parent.PurchasePrompt) + PurchasePrompt = PurchasePrompt.getUnconnected() + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, { + overrideStore = Rodux.Store.new(Reducer, { + promptState = PromptState.PromptPurchase, + accountInfo = { + balance = 100, + }, + productInfo = { + assetTypeId = 2, -- T-shirt + price = 10, + itemType = 2, + }, + }) + }, { + Roact.createElement(PurchasePrompt) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasingAnimation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasingAnimation.lua new file mode 100644 index 0000000..7839c4a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasingAnimation.lua @@ -0,0 +1,68 @@ +local Root = script.Parent.Parent.Parent +local RunService = game:GetService("RunService") +local Workspace = game:GetService("Workspace") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) + +local AnimatedDot = require(script.Parent.AnimatedDot) +local ExternalEventConnection = require(script.Parent.Parent.Connection.ExternalEventConnection) +local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues) + +local ANIMATION_SPEED_MULTIPLIER = 2.00 + +local PurchasingAnimation = Roact.Component:extend("PurchasingAnimation") + +function PurchasingAnimation:init() + self.state = { + gameTime = 0, + } + + self.onRenderStepped = function() + self:setState({ + gameTime = Workspace.DistributedGameTime + }) + end +end + +function PurchasingAnimation:render() + return withLayoutValues(function(values) + local layoutOrder = self.props.layoutOrder + + local gameTime = self.state.gameTime + + local animationTime = (gameTime * ANIMATION_SPEED_MULTIPLIER) % 3 + + return Roact.createElement("Frame", { + Size = values.Size.PurchasingAnimation, + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + }, { + RenderSteppedConnection = Roact.createElement(ExternalEventConnection, { + event = RunService.RenderStepped, + callback = self.onRenderStepped, + }), + ListLayout = Roact.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + AnimatedDot1 = Roact.createElement(AnimatedDot, { + layoutOrder = 1, + time = animationTime, + }), + AnimatedDot2 = Roact.createElement(AnimatedDot, { + layoutOrder = 2, + time = animationTime, + }), + AnimatedDot3 = Roact.createElement(AnimatedDot, { + layoutOrder = 3, + time = animationTime, + }), + }) + end) +end + +return PurchasingAnimation \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasingAnimation.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasingAnimation.spec.lua new file mode 100644 index 0000000..e9d3403 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePrompt/PurchasingAnimation.spec.lua @@ -0,0 +1,21 @@ +return function() + local Root = script.Parent.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local UnitTestContainer = require(Root.Test.UnitTestContainer) + + local PurchasingAnimation = require(script.Parent.PurchasingAnimation) + + it("should create and destroy without errors", function() + local element = Roact.createElement(UnitTestContainer, nil, { + Roact.createElement(PurchasingAnimation, { + layoutOrder = 1, + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePromptApp.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePromptApp.lua new file mode 100644 index 0000000..ac00a5c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePromptApp.lua @@ -0,0 +1,79 @@ +local Root = script.Parent.Parent +local CorePackages = game:GetService("CorePackages") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) +local Rodux = require(LuaPackages.Rodux) +local RoactRodux = require(LuaPackages.RoactRodux) +local UIBlox = require(LuaPackages.UIBlox) +local StyleProvider = UIBlox.Style.Provider + +local Reducer = require(Root.Reducers.Reducer) +local Network = require(Root.Services.Network) +local Analytics = require(Root.Services.Analytics) +local PlatformInterface = require(Root.Services.PlatformInterface) +local ExternalSettings = require(Root.Services.ExternalSettings) +local Thunk = require(Root.Thunk) + +local PremiumPrompt = require(script.Parent.PremiumPrompt.PremiumPrompt) +local PurchasePrompt = require(script.Parent.PurchasePrompt.PurchasePrompt) +local EventConnections = require(script.Parent.Connection.EventConnections) +local LayoutValuesProvider = require(script.Parent.Connection.LayoutValuesProvider) +local provideRobloxLocale = require(script.Parent.Connection.provideRobloxLocale) + +local DarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme) +local Gotham = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham) + +local PurchasePromptApp = Roact.Component:extend("PurchasePromptApp") + +function PurchasePromptApp:init() + local initialState = {} + + local network = Network.new() + local analytics = Analytics.new() + local platformInterface = PlatformInterface.new() + local externalSettings = ExternalSettings.new() + + self.state = { + store = Rodux.Store.new(Reducer, initialState, { + Thunk.middleware({ + [Network] = network, + [Analytics] = analytics, + [PlatformInterface] = platformInterface, + [ExternalSettings] = externalSettings, + }), + }), + isTenFootInterface = externalSettings.isTenFootInterface(), + } +end + +function PurchasePromptApp:render() + return provideRobloxLocale(function() + return Roact.createElement(RoactRodux.StoreProvider, { + store = self.state.store, + }, { + StyleProvider = Roact.createElement(StyleProvider, { + style = { + Theme = DarkTheme, + Font = Gotham, + }, + }, { + PurchasePrompt = Roact.createElement(LayoutValuesProvider, { + isTenFootInterface = self.state.isTenFootInterface, + render = function() + return Roact.createElement("ScreenGui", { + AutoLocalize = false, + IgnoreGuiInset = true, + }, { + PremiumPromptUI = Roact.createElement(PremiumPrompt), + PurchasePromptUI = Roact.createElement(PurchasePrompt), + EventConnections = Roact.createElement(EventConnections), + }) + end + }) + }) + }) + end) +end + +return PurchasePromptApp \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePromptApp.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePromptApp.spec.lua new file mode 100644 index 0000000..4de2c47 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Components/PurchasePromptApp.spec.lua @@ -0,0 +1,15 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Roact = require(LuaPackages.Roact) + + local PurchasePromptApp = require(script.Parent.PurchasePromptApp) + + it("should create and destroy without errors", function() + local element = Roact.createElement(PurchasePromptApp) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/ItemType.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/ItemType.lua new file mode 100644 index 0000000..3dea2fa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/ItemType.lua @@ -0,0 +1,13 @@ +--[[ + Enumeration of types of items that can be purchased. +]] +local createEnum = require(script.Parent.createEnum) + +local ItemType = createEnum("ItemType", { + "Asset", + "GamePass", + "Product", + "Bundle", +}) + +return ItemType \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/PromptState.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/PromptState.lua new file mode 100644 index 0000000..944746a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/PromptState.lua @@ -0,0 +1,18 @@ +--[[ + Enumerated state of the purchase prompt +]] +local createEnum = require(script.Parent.createEnum) + +local PromptState = createEnum("PromptState", { + "None", + "PremiumUpsell", + "RobuxUpsell", + "PromptPurchase", + "PurchaseInProgress", + "UpsellInProgress", + "AdultConfirmation", + "PurchaseComplete", + "Error", +}) + +return PromptState diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/PurchaseError.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/PurchaseError.lua new file mode 100644 index 0000000..856b027 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/PurchaseError.lua @@ -0,0 +1,35 @@ +--[[ + Enumeration of all possible error states +]] +local createEnum = require(script.Parent.createEnum) + +local PurchaseError = createEnum("PurchaseError", { + -- Pre-purchase network failures + "CannotGetBalance", + "CannotGetItemPrice", + + -- Premium + "AlreadyPremium", + "PremiumUnavailable", + "PremiumUnavailablePlatform", + + -- Item unvailable + "NotForSale", + "AlreadyOwn", + "PremiumOnly", + "Under13", + "Limited", + "Guest", + "ThirdPartyDisabled", + "NotEnoughRobux", + "NotEnoughRobuxXbox", + "NotEnoughRobuxNoUpsell", + + -- Network-reported failures + "UnknownFailure", + "UnknownFailureNoItemName", + "PurchaseDisabled", + "InvalidFunds", +}) + +return PurchaseError \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/RequestType.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/RequestType.lua new file mode 100644 index 0000000..0cc81ff --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/RequestType.lua @@ -0,0 +1,13 @@ +local createEnum = require(script.Parent.createEnum) + +local RequestType = createEnum("RequestType", { + "None", + "Asset", + "Bundle", + "GamePass", + "Product", + "Premium", + "Subscription", +}) + +return RequestType \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/UpsellFlow.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/UpsellFlow.lua new file mode 100644 index 0000000..67ff7be --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/UpsellFlow.lua @@ -0,0 +1,11 @@ +local createEnum = require(script.Parent.createEnum) + +local UpsellFlow = createEnum("UpsellFlow", { + "Web", + "Mobile", + "Xbox", + "Unavailable", + "None", +}) + +return UpsellFlow \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/WindowState.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/WindowState.lua new file mode 100644 index 0000000..aafbb23 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/WindowState.lua @@ -0,0 +1,8 @@ +local createEnum = require(script.Parent.createEnum) + +local WindowState = createEnum("WindowState", { + "Hidden", + "Shown", +}) + +return WindowState \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/createEnum.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/createEnum.lua new file mode 100644 index 0000000..2b28cab --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/createEnum.lua @@ -0,0 +1,58 @@ +--[[ + An implementation of an enumerated type in Lua. Creates enumerated + types with uniquely identifiable values (using symbol) + + Note that resulting enum object does not associate ordinals with its + values, and cannot be iterated through. It can, however, test if a provided + value is a member of its set of values with the `isMember` function + + This is valuable for the purchase prompt because it relies heavily on + enumerated values to determine things like state and which errors occurred. +]] +local Root = script.Parent.Parent +local Symbol = require(Root.Symbols.Symbol) +local strict = require(Root.strict) + +--[[ + Returns a new enum type with the given name. +]] +local function createEnum(enumName, values) + assert(typeof(enumName) == "string", "Bad argument #1, expected string") + assert(typeof(values) == "table", "Bad argument #2, expected list of values") + + local enumInternal = {} + + for _, valueName in ipairs(values) do + assert(valueName ~= "isMember", "Shadowing 'isMember' function is not allowed") + assert(typeof(valueName) == "string", "Only string names are supported for enum types") + + local enumValue = Symbol.named(valueName) + local asString = ("%s.%s"):format(enumName, valueName) + getmetatable(enumValue).__tostring = function() + return asString + end + + enumInternal[valueName] = enumValue + end + + function enumInternal.isMember(value) + if typeof(value) ~= "userdata" then + return false + end + + for _, enumeratedValue in pairs(enumInternal) do + if value == enumeratedValue then + return true + end + end + + return false + end + + local enum = newproxy(true) + getmetatable(enum).__index = enumInternal + + return strict(enumInternal, enumName) +end + +return createEnum \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/createEnum.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/createEnum.spec.lua new file mode 100644 index 0000000..93f71ea --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Enums/createEnum.spec.lua @@ -0,0 +1,66 @@ +return function() + local createEnum = require(script.Parent.createEnum) + + describe("validation rules", function() + it("should throw errors if given invalid values", function() + expect(function() + createEnum(1, {}) + end).to.throw() + + expect(function() + createEnum("MyEnum", "not a table") + end).to.throw() + end) + + it("should throw errors if provided table contains invalid values", function() + expect(function() + createEnum("IllegalShadowing", { + "isMember", + }) + end).to.throw() + + expect(function() + createEnum("MyEnum", { + "Test", + 12, + }) + end) + end) + end) + + describe("enum properties", function() + it("should have a reasonable string format for debugging", function() + local MyEnum = createEnum("MyEnum", { + "Value", + }) + + expect(tostring(MyEnum.Value)).to.equal("MyEnum.Value") + end) + + it("should provide an isMember function to check membership", function() + local MyEnum = createEnum("MyEnum", { + "Value", + }) + + expect(MyEnum.isMember(MyEnum.Value)).to.equal(true) + expect(MyEnum.isMember(newproxy(true))).to.equal(false) + expect(MyEnum.isMember("Value")).to.equal(false) + end) + + it("should generate objects that are unique even when named the same", function() + + local Enum1 = createEnum("MyEnum", { + "Value", + }) + local Enum2 = createEnum("MyEnum", { + "Value", + }) + + expect(Enum1.isMember(Enum1.Value)).to.equal(true) + expect(Enum2.isMember(Enum2.Value)).to.equal(true) + + expect(Enum1.isMember(Enum2.Value)).to.equal(false) + expect(Enum2.isMember(Enum1.Value)).to.equal(false) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagAdultConfirmationEnabled.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagAdultConfirmationEnabled.lua new file mode 100644 index 0000000..a1a1d27 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagAdultConfirmationEnabled.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("AdultConfirmationEnabledV4", false) + +return function() + return game:GetFastFlag("AdultConfirmationEnabledV4") +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagAdultConfirmationEnabledNew.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagAdultConfirmationEnabledNew.lua new file mode 100644 index 0000000..fc83fe4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagAdultConfirmationEnabledNew.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("AdultConfirmationEnabledNew", false) + +return function() + return game:GetFastFlag("AdultConfirmationEnabledNew") +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagDeveloperSubscriptionsEnabled.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagDeveloperSubscriptionsEnabled.lua new file mode 100644 index 0000000..4468b66 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagDeveloperSubscriptionsEnabled.lua @@ -0,0 +1,3 @@ +return function() + return settings():GetFFlag("DeveloperSubscriptionsEnabled") +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagDisableRobuxUpsell.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagDisableRobuxUpsell.lua new file mode 100644 index 0000000..0cb4755 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagDisableRobuxUpsell.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("DisableRobuxUpsell", false) + +return function() + return game:GetFastFlag("DisableRobuxUpsell") +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagHideThirdPartyPurchaseFailure.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagHideThirdPartyPurchaseFailure.lua new file mode 100644 index 0000000..74583b8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagHideThirdPartyPurchaseFailure.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("HideThirdPartyPurchaseFailure", false) + +return function() + return game:GetFastFlag("HideThirdPartyPurchaseFailure") +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagIGPPPremiumPrice.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagIGPPPremiumPrice.lua new file mode 100644 index 0000000..2f6b0ea --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagIGPPPremiumPrice.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("IGPPPremiumPriceV2", false) + +return function() + return game:GetFastFlag("IGPPPremiumPriceV2") +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagLuaPremiumCatalogIGPP.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagLuaPremiumCatalogIGPP.lua new file mode 100644 index 0000000..5c631fb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagLuaPremiumCatalogIGPP.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("LuaPremiumCatalogIGPP", false) + +return function() + return game:GetFastFlag("LuaPremiumCatalogIGPP") +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagLuaUseThirdPartyPermissions.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagLuaUseThirdPartyPermissions.lua new file mode 100644 index 0000000..31f85a6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagLuaUseThirdPartyPermissions.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("LuaUseThirdPartyPermissions", false) + +return function() + return game:GetFastFlag("LuaUseThirdPartyPermissions") +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagNewEconomyDeveloperProductUrl.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagNewEconomyDeveloperProductUrl.lua new file mode 100644 index 0000000..d6ebb28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagNewEconomyDeveloperProductUrl.lua @@ -0,0 +1,6 @@ +game:DefineFastFlag("NewEconomyDeveloperProductUrlLua", false) + + +return function() + return game:GetFastFlag("NewEconomyDeveloperProductUrlLua") +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagProductPercentLocFix.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagProductPercentLocFix.lua new file mode 100644 index 0000000..54385a1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagProductPercentLocFix.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("ProductPercentLocFix", false) + +return function() + return game:GetFastFlag("ProductPercentLocFix") +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagPromptRobloxPurchaseEnabled.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagPromptRobloxPurchaseEnabled.lua new file mode 100644 index 0000000..2010872 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagPromptRobloxPurchaseEnabled.lua @@ -0,0 +1,3 @@ +return function() + return settings():GetFFlag("PromptRobloxPurchaseEnabled") +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagUpsellDirectToPackage.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagUpsellDirectToPackage.lua new file mode 100644 index 0000000..d3feac6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Flags/GetFFlagUpsellDirectToPackage.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("UpsellDirectToPackage", false) + +return function() + return game:GetFastFlag("UpsellDirectToPackage") +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/KeyMappings.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/KeyMappings.lua new file mode 100644 index 0000000..83c43a6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/KeyMappings.lua @@ -0,0 +1,84 @@ +local Root = script.Parent.Parent + +local PurchaseError = require(Root.Enums.PurchaseError) + +local KeyMappings = {} + +local PURCHASE_FAILED_KEY = "CoreScripts.PurchasePrompt.PurchaseFailed.%s" +local ASSET_TYPE_KEY = "Common.AssetTypes.Label.%s" + +KeyMappings.AssetTypeById = { + --[[ + This key is a special case; developer products only exist + within the context of a game, so they're localized with the + rest of the purchase prompt strings. + ]] + ["0"] = "CoreScripts.PurchasePrompt.ProductType.Product", + + --[[ + The rest of these are asset types associated with Roblox + assets that exist outside of games, mostly related to + avatar customization + ]] + ["2"] = ASSET_TYPE_KEY:format("TShirt"), + ["3"] = ASSET_TYPE_KEY:format("Audio"), + ["4"] = ASSET_TYPE_KEY:format("Mesh"), + ["8"] = ASSET_TYPE_KEY:format("Hat"), + ["9"] = ASSET_TYPE_KEY:format("Place"), + ["10"] = ASSET_TYPE_KEY:format("Model"), + ["11"] = ASSET_TYPE_KEY:format("Shirt"), + ["12"] = ASSET_TYPE_KEY:format("Pants"), + ["13"] = ASSET_TYPE_KEY:format("Decal"), + ["17"] = ASSET_TYPE_KEY:format("Head"), + ["18"] = ASSET_TYPE_KEY:format("Face"), + ["19"] = ASSET_TYPE_KEY:format("Gear"), + ["21"] = ASSET_TYPE_KEY:format("Badge"), + ["24"] = ASSET_TYPE_KEY:format("Animation"), + ["27"] = ASSET_TYPE_KEY:format("Torso"), + ["28"] = ASSET_TYPE_KEY:format("RightArm"), + ["29"] = ASSET_TYPE_KEY:format("LeftArm"), + ["30"] = ASSET_TYPE_KEY:format("LeftLeg"), + ["31"] = ASSET_TYPE_KEY:format("RightLeg"), + ["32"] = ASSET_TYPE_KEY:format("Package"), + ["34"] = ASSET_TYPE_KEY:format("GamePass"), + ["38"] = ASSET_TYPE_KEY:format("Plugin"), + ["40"] = ASSET_TYPE_KEY:format("MeshPart"), + ["41"] = ASSET_TYPE_KEY:format("Hair"), + ["42"] = ASSET_TYPE_KEY:format("Face"), + ["43"] = ASSET_TYPE_KEY:format("Neck"), + ["44"] = ASSET_TYPE_KEY:format("Shoulder"), + ["45"] = ASSET_TYPE_KEY:format("Front"), + ["46"] = ASSET_TYPE_KEY:format("Back"), + ["47"] = ASSET_TYPE_KEY:format("Waist"), + ["48"] = ASSET_TYPE_KEY:format("Climb"), + ["49"] = ASSET_TYPE_KEY:format("Death"), + ["50"] = ASSET_TYPE_KEY:format("Fall"), + ["51"] = ASSET_TYPE_KEY:format("Idle"), + ["52"] = ASSET_TYPE_KEY:format("Jump"), + ["53"] = ASSET_TYPE_KEY:format("Run"), + ["54"] = ASSET_TYPE_KEY:format("Swim"), + ["55"] = ASSET_TYPE_KEY:format("Walk"), + ["56"] = ASSET_TYPE_KEY:format("Pose"), + ["61"] = ASSET_TYPE_KEY:format("Emote"), +} + +KeyMappings.PurchaseErrorKey = { + [PurchaseError.CannotGetBalance] = PURCHASE_FAILED_KEY:format("CannotGetBalance"), + [PurchaseError.CannotGetItemPrice] = PURCHASE_FAILED_KEY:format("CannotGetItemPrice"), + [PurchaseError.NotForSale] = PURCHASE_FAILED_KEY:format("NotForSale"), + [PurchaseError.AlreadyOwn] = PURCHASE_FAILED_KEY:format("AlreadyOwn"), + [PurchaseError.Under13] = PURCHASE_FAILED_KEY:format("Under13"), + [PurchaseError.Limited] = PURCHASE_FAILED_KEY:format("Limited"), + [PurchaseError.Guest] = PURCHASE_FAILED_KEY:format("PromptPurchaseOnGuest"), + [PurchaseError.ThirdPartyDisabled] = PURCHASE_FAILED_KEY:format("ThirdPartyDisabled"), + [PurchaseError.NotEnoughRobux] = PURCHASE_FAILED_KEY:format("NotEnoughRobux"), + [PurchaseError.NotEnoughRobuxXbox] = PURCHASE_FAILED_KEY:format("NotEnoughRobuxXbox"), + [PurchaseError.NotEnoughRobuxNoUpsell] = PURCHASE_FAILED_KEY:format("NotEnoughRobuxNoUpsell"), + [PurchaseError.UnknownFailure] = PURCHASE_FAILED_KEY:format("UnknownFailure"), + [PurchaseError.UnknownFailureNoItemName] = PURCHASE_FAILED_KEY:format("UnknownFailureNoItemName"), + [PurchaseError.PurchaseDisabled] = PURCHASE_FAILED_KEY:format("PurchaseDisabled"), + [PurchaseError.InvalidFunds] = PURCHASE_FAILED_KEY:format("InvalidFunds"), + [PurchaseError.PremiumOnly] = PURCHASE_FAILED_KEY:format("PremiumOnly"), +} + +return KeyMappings \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bg-bg.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bg-bg.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bg-bg.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bn-bd.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bn-bd.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bn-bd.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bs-ba.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bs-ba.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/bs-ba.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/cs-cz.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/cs-cz.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/cs-cz.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/da-dk.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/da-dk.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/da-dk.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/de-de.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/de-de.lua new file mode 100644 index 0000000..55ee19a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/de-de.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[Accessoires]], + ["Common.AssetTypes.Label.Hat"] = [[Hut]], + ["Common.AssetTypes.Label.Hair"] = [[Haare]], + ["Common.AssetTypes.Label.Face"] = [[Gesicht]], + ["Common.AssetTypes.Label.Neck"] = [[Hals]], + ["Common.AssetTypes.Label.Shoulder"] = [[Schulter]], + ["Common.AssetTypes.Label.Front"] = [[Vorderseite]], + ["Common.AssetTypes.Label.Back"] = [[Rückseite]], + ["Common.AssetTypes.Label.Waist"] = [[Taille]], + ["Common.AssetTypes.Label.Animations"] = [[Animationen]], + ["Common.AssetTypes.Label.Audio"] = [[Audiodateien]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[Avataranimationen]], + ["Common.AssetTypes.Label.Badges"] = [[Abzeichen]], + ["Common.AssetTypes.Label.Decals"] = [[Decals]], + ["Common.AssetTypes.Label.Faces"] = [[Gesichter]], + ["Common.AssetTypes.Label.GamePasses"] = [[Spielpässe]], + ["Common.AssetTypes.Label.Gear"] = [[Ausrüstung]], + ["Common.AssetTypes.Label.Heads"] = [[Köpfe]], + ["Common.AssetTypes.Label.Meshes"] = [[Meshes]], + ["Common.AssetTypes.Label.Models"] = [[Modelle]], + ["Common.AssetTypes.Label.Packages"] = [[Pakete]], + ["Common.AssetTypes.Label.Pants"] = [[Hosen]], + ["Common.AssetTypes.Label.Places"] = [[Orte]], + ["Common.AssetTypes.Label.Plugins"] = [[Plug-ins]], + ["Common.AssetTypes.Label.Shirts"] = [[Hemden]], + ["Common.AssetTypes.Label.TShirts"] = [[T-Shirts]], + ["Common.AssetTypes.Label.VipServers"] = [[VIP-Server]], + ["Common.AssetTypes.Label.Run"] = [[Laufen]], + ["Common.AssetTypes.Label.Walk"] = [[Gehen]], + ["Common.AssetTypes.Label.Fall"] = [[Fallen]], + ["Common.AssetTypes.Label.Jump"] = [[Springen]], + ["Common.AssetTypes.Label.Idle"] = [[Untätig]], + ["Common.AssetTypes.Label.Swim"] = [[Schwimmen]], + ["Common.AssetTypes.Label.Climb"] = [[Klettern]], + ["Common.AssetTypes.Label.Hats"] = [[Hüte]], + ["Common.AssetTypes.Label.Shoulders"] = [[Schultern]], + ["Common.AssetTypes.Label.Death"] = [[Tod]], + ["Common.AssetTypes.Label.Pose"] = [[Pose]], + ["Common.AssetTypes.Label.Head"] = [[Kopf]], + ["Common.AssetTypes.Label.TShirt"] = [[T-Shirt]], + ["Common.AssetTypes.Label.Shirt"] = [[Hemd]], + ["Common.AssetTypes.Label.Decal"] = [[Decal]], + ["Common.AssetTypes.Label.Model"] = [[Modell]], + ["Common.AssetTypes.Label.Plugin"] = [[Plug-in]], + ["Common.AssetTypes.Label.MeshPart"] = [[Mesh-Teil]], + ["Common.AssetTypes.Label.GamePass"] = [[Spielpass]], + ["Common.AssetTypes.Label.Badge"] = [[Abzeichen]], + ["Common.AssetTypes.Label.Package"] = [[Paket]], + ["Common.AssetTypes.Label.Place"] = [[Ort]], + ["Common.AssetTypes.Label.LeftArm"] = [[Linker Arm]], + ["Common.AssetTypes.Label.LeftLeg"] = [[Linkes Bein]], + ["Common.AssetTypes.Label.RightArm"] = [[Rechter Arm]], + ["Common.AssetTypes.Label.RightLeg"] = [[Rechtes Bein]], + ["Common.AssetTypes.Label.Torso"] = [[Torso]], + ["Common.AssetTypes.Label.Animation"] = [[Animation]], + ["Common.AssetTypes.Label.Emote"] = [[Emote]], + ["Common.BuildersClub.Label.PlanFree"] = [[Gratis]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Klassisch]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[„Builders Club“-Mitgliedschaft]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[„Turbo Builders Club“-Mitgliedschaft]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[„Outrageous Builders Club“-Mitgliedschaft]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[Ja]], + ["Common.BuildersClub.Label.No"] = [[Nein]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[NIEMALS]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[Auf Lebenszeit]], + ["Common.BuildersClub.Label.Membership"] = [[Mitgliedschaft]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Premium erforderlich]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Durch Roblox Premium erhältst du:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[450 Robux pro Monat]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Zugriff auf exklusive Premium-Vorteile]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[10% Bonus beim Kauf von Robux]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price}/Monat]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[Der Kauf von Roblox Premium wird auf deiner Plattform nicht unterstützt. Bitte verwende deinen Desktop-Computer, um Premium zu kaufen.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[Der Entwickler versucht, dich dazu aufzufordern, Roblox Premium zu kaufen, aber du bist bereits ein Premium-Mitglied!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[Premium ist derzeit nicht verfügbar. Bitte versuche es später wieder!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[{robux} Robux im Monat]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[Kauf wurde nicht abgeschlossen, bitte versuche es erneut.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[Fehler]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[Gratis nehmen]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[Aufwerten]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[Jetzt kaufen]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[R$ kaufen]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[Abbrechen]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[Okay]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[Wird gekauft ...]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[Das Guthaben deines Kontos wird durch diese Transaktion nicht beeinflusst.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[Dies ist ein Testkauf. Dein Konto wird dadurch nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[Dies war ein Testkauf.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[Dieser Artikel erfordert {BC_LEVEL}. Klicke auf „Aufwerten“, um deinen Builders-Club-Status zu verbessern!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[Nach dieser Transaktion wird dein Guthaben {BALANCE_FUTURE} R$ betragen.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[Dein Guthaben beträgt nun {BALANCE_NOW}.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[Die verbleibenden {REMAINING_ROBUX} Robux werden deinem Guthaben gutgeschrieben.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[Dein Kauf ist fehlgeschlagen, da dein Konto nicht über genügend Robux verfügt. Dein Konto wurde nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[Dein Kauf ist fehlgeschlagen, da du ein Abonnement benötigst, um diesen Artikel zu kaufen. Dein Konto wurde nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[Dieser Artikel kostet mehr Robux, als dir zur Verfügung stehen. Bitte verlasse dieses Spiel, gehe zum Robux-Menü und kaufe dort mehr.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[Dieser Artikel kostet mehr Robux, als du kaufen kannst. Bitte besuche www.roblox.com, um mehr Robux zu kaufen.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[Dein Kauf ist fehlgeschlagen, da Käufe im Spiel derzeit deaktiviert sind. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[Dein Kauf ist fehlgeschlagen, da ein Problem aufgetreten ist. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[Dein Kauf von „{ITEM_NAME}“ ist fehlgeschlagen, da ein Problem aufgetreten ist. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[Dein Guthaben kann derzeit nicht abgerufen werden. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[Der Preis des Artikels kann derzeit nicht abgerufen werden. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[Von diesem limitierten Artikel gibt es keine weiteren Exemplare. Such auf www.roblox.com nach einem anderen Verkäufer. Dein Konto wurde nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[Dieser Artikel steht derzeit nicht zum Verkauf. Dein Konto wurde nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[Du musst ein Roblox-Konto erstellen, um Artikel zu kaufen. Auf www.roblox.com findest du weitere Infos.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[Artikel von Drittanbietern können an diesem Ort nicht verkauft werden. Dein Konto wurde nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[Dein Konto ist für Spieler unter 13 Jahren. Der Kauf dieses Artikels ist nicht gestattet. Dein Konto wurde nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[Diesen Artikel besitzt du bereits. Dein Konto wurde nicht belastet.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[Möchtest du „{ITEM_NAME}“ gerne GRATIS nehmen?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[Möchtest du „{ITEM_NAME}“ ({ASSET_TYPE}) kaufen für]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[Du hast „{ITEM_NAME}“ gekauft!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[Du brauchst noch {NEEDED_AMOUNT} Robux, um „{ITEM_NAME}“ ({ASSET_TYPE}) zu kaufen. Möchtest du mehr Robux kaufen?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[Produkt]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[Paket]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[Ja]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[Abbrechen]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[Du verlängerst dieses Abonnement derzeit nicht. Deine Abonnements haben sich nicht geändert.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[Du hast dein {ITEM_NAME} -Abonnement gekündigt. Du behältst alle Leistungen bis zum Ende der Zahlungsperiode.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[Möchtest du wirklich dein {ITEM_NAME}-Abonnement kündigen? Du behältst alle Leistungen bis zum Ende der Zahlungsperiode.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[Möchtest du das {ITEM_NAME}-Abonnement {ITEM_NAME} abonnieren? Kosten:]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[Abonnement]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[{PRICE} pro Monat]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[Abonnieren]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[Du musst Premium-Mitglied sein, um dich anzumelden!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[Aufwerten]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[Dein Kauf ist fehlgeschlagen, da du Premium-Mitglied sein musst, um dich anzumelden. Dein Konto wurde nicht belastet]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Robux kaufen]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[Dein Kontostand nach dieser Transaktion beträgt {BALANCE_FUTURE} Robux]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[Dieser Gegenstand kostet mehr Robux, als dir zur Verfügung stehen. ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Nur Premium]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[Du musst Roblox Premium abonniert haben, um diesen Gegenstand zu kaufen.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[Dieser Kauf involviert den Austausch von echtem Geld. Ich bestätige, dass ich mindestens 18 Jahre alt bin und dass ich ein Elternteil oder Erziehungsberechtigter des Kontoinhabers bin. Ich genehmige diesen Kauf und stimme den Nutzungsbedingungen zu.]], +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/el-gr.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/el-gr.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/el-gr.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/en-us.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/en-us.lua new file mode 100644 index 0000000..7583958 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/en-us.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[Accessories]], + ["Common.AssetTypes.Label.Hat"] = [[Hat]], + ["Common.AssetTypes.Label.Hair"] = [[Hair]], + ["Common.AssetTypes.Label.Face"] = [[Face]], + ["Common.AssetTypes.Label.Neck"] = [[Neck]], + ["Common.AssetTypes.Label.Shoulder"] = [[Shoulder]], + ["Common.AssetTypes.Label.Front"] = [[Front]], + ["Common.AssetTypes.Label.Back"] = [[Back]], + ["Common.AssetTypes.Label.Waist"] = [[Waist]], + ["Common.AssetTypes.Label.Animations"] = [[Animations]], + ["Common.AssetTypes.Label.Audio"] = [[Audio]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[Avatar Animations]], + ["Common.AssetTypes.Label.Badges"] = [[Badges]], + ["Common.AssetTypes.Label.Decals"] = [[Decals]], + ["Common.AssetTypes.Label.Faces"] = [[Faces]], + ["Common.AssetTypes.Label.GamePasses"] = [[Game Passes]], + ["Common.AssetTypes.Label.Gear"] = [[Gear]], + ["Common.AssetTypes.Label.Heads"] = [[Heads]], + ["Common.AssetTypes.Label.Meshes"] = [[Meshes]], + ["Common.AssetTypes.Label.Models"] = [[Models]], + ["Common.AssetTypes.Label.Packages"] = [[Packages]], + ["Common.AssetTypes.Label.Pants"] = [[Pants]], + ["Common.AssetTypes.Label.Places"] = [[Places]], + ["Common.AssetTypes.Label.Plugins"] = [[Plugins]], + ["Common.AssetTypes.Label.Shirts"] = [[Shirts]], + ["Common.AssetTypes.Label.TShirts"] = [[T-Shirts]], + ["Common.AssetTypes.Label.VipServers"] = [[VIP Servers]], + ["Common.AssetTypes.Label.Run"] = [[Run]], + ["Common.AssetTypes.Label.Walk"] = [[Walk]], + ["Common.AssetTypes.Label.Fall"] = [[Fall]], + ["Common.AssetTypes.Label.Jump"] = [[Jump]], + ["Common.AssetTypes.Label.Idle"] = [[Idle]], + ["Common.AssetTypes.Label.Swim"] = [[Swim]], + ["Common.AssetTypes.Label.Climb"] = [[Climb]], + ["Common.AssetTypes.Label.Hats"] = [[Hats]], + ["Common.AssetTypes.Label.Shoulders"] = [[Shoulders]], + ["Common.AssetTypes.Label.Death"] = [[Death]], + ["Common.AssetTypes.Label.Pose"] = [[Pose]], + ["Common.AssetTypes.Label.Head"] = [[Head]], + ["Common.AssetTypes.Label.TShirt"] = [[T-Shirt]], + ["Common.AssetTypes.Label.Shirt"] = [[Shirt]], + ["Common.AssetTypes.Label.Decal"] = [[Decal]], + ["Common.AssetTypes.Label.Model"] = [[Model]], + ["Common.AssetTypes.Label.Plugin"] = [[Plugin]], + ["Common.AssetTypes.Label.MeshPart"] = [[Mesh Part]], + ["Common.AssetTypes.Label.GamePass"] = [[Game Pass]], + ["Common.AssetTypes.Label.Badge"] = [[Badge]], + ["Common.AssetTypes.Label.Package"] = [[Package]], + ["Common.AssetTypes.Label.Place"] = [[Place]], + ["Common.AssetTypes.Label.LeftArm"] = [[Left Arm]], + ["Common.AssetTypes.Label.LeftLeg"] = [[Left Leg]], + ["Common.AssetTypes.Label.RightArm"] = [[Right Arm]], + ["Common.AssetTypes.Label.RightLeg"] = [[Right Leg]], + ["Common.AssetTypes.Label.Torso"] = [[Torso]], + ["Common.AssetTypes.Label.Animation"] = [[Animation]], + ["Common.AssetTypes.Label.Emote"] = [[Emote]], + ["Common.BuildersClub.Label.PlanFree"] = [[Free]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Classic]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Builders Club Membership]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Turbo Builders Club Membership]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Outrageous Builders Club Membership]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[Yes]], + ["Common.BuildersClub.Label.No"] = [[No]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[NEVER]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[Lifetime]], + ["Common.BuildersClub.Label.Membership"] = [[Membership]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Premium Required]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Roblox Premium will get you:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[450 Robux per month]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Access to Premium only benefits]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[10% bonus when buying Robux]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price}/month]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[Purchasing Roblox Premium is not supported on your platform. Please use your desktop to purchase Premium.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[Looks like the developer is trying to prompt you to purchase Roblox Premium, but you are already a Premium member!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[Premium is unavailable at the moment. Please again try later!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[{robux} Robux per month]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[Purchase was not complete, please try again.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[Error]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[Take Free]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[Upgrade]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[Buy Now]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[Buy R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[Cancel]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[OK]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[Purchasing]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[Your account balance will not be affected by this transaction.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[This is a test purchase; your account will not be charged.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[This was a test purchase.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[This item requires {BC_LEVEL}. Click 'Upgrade' to upgrade your Builders Club!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[Your balance after this transaction will be R${BALANCE_FUTURE}]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[Your balance is now {BALANCE_NOW}.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[The remaining {REMAINING_ROBUX} Robux will be credited to your balance.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[Your purchase failed because your account does not have enough Robux. Your account has not been charged.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[Your purchase failed because you need a subscription to purchase this item. Your account has not been charged.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[This item cost more Robux than you have available. Please leave this game and go to the Robux screen to purchase more.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[This item cost more Robux than you can purchase. Please visit www.roblox.com to purchase more Robux.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[Your purchase failed because In-game purchases are temporarily disabled. Your account has not been charged. Please try again later.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[Your purchase failed because something went wrong. Your account has not been charged. Please try again later.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[Your purchase of {ITEM_NAME} failed because something went wrong. Your account has not been charged. Please try again later.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[Cannot retrieve your balance at this time. Your account has not been charged. Please try again later.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[We couldn't retrieve the price of the item at this time. Your account has not been charged. Please try again later.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[This limited item has no more copies. Try buying from another user on www.roblox.com. Your account has not been charged.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[This item is not currently for sale. Your account has not been charged.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[You need to create a ROBLOX account to buy items, visit www.roblox.com for more info.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[Third-party item sales have been disabled for this place. Your account has not been charged.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[Your account is under 13. Purchase of this item is not allowed. Your account has not been charged.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[You already own this item. Your account has not been charged.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[Would you like to take {ITEM_NAME} for FREE?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[Want to buy the {ASSET_TYPE} {ITEM_NAME} for]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[Your purchase of {ITEM_NAME} succeeded!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[You need {NEEDED_AMOUNT} more Robux to buy the {ASSET_TYPE} {ITEM_NAME}. Would you like to buy more Robux?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[Product]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[Bundle]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[Yes]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[Canceling]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[You aren't currently renewing this subscription. Your subscriptions have not changed.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[You've canceled your {ITEM_NAME} subscription. You will retain your benefits until the end of the pay period.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[Are you sure you would like to cancel your {ITEM_NAME} subscription? You will retain your benefits until the end of the pay period.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[Want to subscribe to the subscription {ITEM_NAME} for]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[Subscription]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[{PRICE} per month]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[Subscribe]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[You must be a Premium member in order to subscribe!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[Upgrade]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[Your purchase failed because you need to be a Premium member to subscribe. Your account has not been charged]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Buy Robux]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[Your balance after this transaction will be {BALANCE_FUTURE} Robux]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[This item cost more Robux than you have available. ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Premium Only]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[You need to have Roblox Premium in order to purchase this item.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[This purchase involves the exchange of real money. I agree that I am at least 18 years of age, and am the parent or legal guardian of the account owner. I authorize this purchase and agree to the Terms of Service.]], +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/es-es.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/es-es.lua new file mode 100644 index 0000000..7c77b7a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/es-es.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[Accesorios]], + ["Common.AssetTypes.Label.Hat"] = [[Sombrero]], + ["Common.AssetTypes.Label.Hair"] = [[Pelo]], + ["Common.AssetTypes.Label.Face"] = [[Cara]], + ["Common.AssetTypes.Label.Neck"] = [[Cuello]], + ["Common.AssetTypes.Label.Shoulder"] = [[Hombro]], + ["Common.AssetTypes.Label.Front"] = [[Frontal]], + ["Common.AssetTypes.Label.Back"] = [[Trasero]], + ["Common.AssetTypes.Label.Waist"] = [[Cintura]], + ["Common.AssetTypes.Label.Animations"] = [[Animaciones]], + ["Common.AssetTypes.Label.Audio"] = [[Sonidos]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[Animaciones de avatar]], + ["Common.AssetTypes.Label.Badges"] = [[Emblemas]], + ["Common.AssetTypes.Label.Decals"] = [[Adhesivos]], + ["Common.AssetTypes.Label.Faces"] = [[Caras]], + ["Common.AssetTypes.Label.GamePasses"] = [[Pases del juego]], + ["Common.AssetTypes.Label.Gear"] = [[Equipamiento]], + ["Common.AssetTypes.Label.Heads"] = [[Cabezas]], + ["Common.AssetTypes.Label.Meshes"] = [[Mallas]], + ["Common.AssetTypes.Label.Models"] = [[Modelos]], + ["Common.AssetTypes.Label.Packages"] = [[Paquetes]], + ["Common.AssetTypes.Label.Pants"] = [[Pantalones]], + ["Common.AssetTypes.Label.Places"] = [[Lugares]], + ["Common.AssetTypes.Label.Plugins"] = [[Complementos]], + ["Common.AssetTypes.Label.Shirts"] = [[Camisas]], + ["Common.AssetTypes.Label.TShirts"] = [[Camisetas]], + ["Common.AssetTypes.Label.VipServers"] = [[Servidores VIP]], + ["Common.AssetTypes.Label.Run"] = [[Carrera]], + ["Common.AssetTypes.Label.Walk"] = [[Marcha]], + ["Common.AssetTypes.Label.Fall"] = [[Caída]], + ["Common.AssetTypes.Label.Jump"] = [[Salto]], + ["Common.AssetTypes.Label.Idle"] = [[Inactividad]], + ["Common.AssetTypes.Label.Swim"] = [[Nado]], + ["Common.AssetTypes.Label.Climb"] = [[Escalada]], + ["Common.AssetTypes.Label.Hats"] = [[Sombreros]], + ["Common.AssetTypes.Label.Shoulders"] = [[Hombros]], + ["Common.AssetTypes.Label.Death"] = [[La muerte]], + ["Common.AssetTypes.Label.Pose"] = [[Pose]], + ["Common.AssetTypes.Label.Head"] = [[Cabeza]], + ["Common.AssetTypes.Label.TShirt"] = [[Camiseta]], + ["Common.AssetTypes.Label.Shirt"] = [[Camisa]], + ["Common.AssetTypes.Label.Decal"] = [[Adhesivo]], + ["Common.AssetTypes.Label.Model"] = [[Modelo]], + ["Common.AssetTypes.Label.Plugin"] = [[Complemento]], + ["Common.AssetTypes.Label.MeshPart"] = [[Parte de la malla]], + ["Common.AssetTypes.Label.GamePass"] = [[Pase del juego]], + ["Common.AssetTypes.Label.Badge"] = [[Emblema]], + ["Common.AssetTypes.Label.Package"] = [[Paquete]], + ["Common.AssetTypes.Label.Place"] = [[Lugar]], + ["Common.AssetTypes.Label.LeftArm"] = [[Brazo izquierdo]], + ["Common.AssetTypes.Label.LeftLeg"] = [[Pierna izquierda]], + ["Common.AssetTypes.Label.RightArm"] = [[Brazo derecho]], + ["Common.AssetTypes.Label.RightLeg"] = [[Pierna derecha]], + ["Common.AssetTypes.Label.Torso"] = [[Torso]], + ["Common.AssetTypes.Label.Animation"] = [[Animación]], + ["Common.AssetTypes.Label.Emote"] = [[Emote]], + ["Common.BuildersClub.Label.PlanFree"] = [[Gratis]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Clásico]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Suscripción al Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Suscripción al Turbo Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Suscripción al Outrageous Builders Club]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[Sí]], + ["Common.BuildersClub.Label.No"] = [[No]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[NUNCA]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Builders Club Clásico]], + ["Common.BuildersClub.Label.Lifetime"] = [[Vitalicia]], + ["Common.BuildersClub.Label.Membership"] = [[Suscripción]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Se requiere Premium]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Con Roblox Premium también obtienes:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[450 Robux al mes]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Acceso a beneficios exclusivos para suscriptores de Premium]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[10% de Robux extra en la compra de nuestra moneda virtual]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price} al mes]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[La compra de Roblox Premium no es compatible con tu plataforma. Usa un equipo de escritorio para suscribirte.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[Parece que el desarrollador te quiere motivar a que te suscribas a Premium, pero tú ya tienes una suscripción a Premium.]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[Premium no está disponible en este momento. Inténtalo más tarde.]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[{robux} Robux al mes]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[No se ha finalizado la compra. Inténtalo de nuevo.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[Error]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[Llévatelo gratis]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[Mejorar]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[Comprar ahora]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[Comprar R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[Cancelar]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[Aceptar]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[Comprando]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[Esta operación no afectará tu saldo.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[Esta es una compra de prueba; no se te cobrará por esta operación.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[Esta fue una compra de prueba.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[Para comprar este objeto se requiere {BC_LEVEL}. Haz clic en Mejorar para pasar a un nivel de suscripción superior del Builders Club.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[Tu saldo después de esta transacción será de R${BALANCE_FUTURE}]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[Tu saldo es de {BALANCE_NOW}.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[Los {REMAINING_ROBUX} Robux restantes se abonarán a tu saldo.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[La compra no se ha realizado porque la cuenta no tiene suficientes Robux. No se te ha cobrado.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[La compra no se ha realizado porque se necesita una suscripción para adquirir este objeto. No se ha cobrado.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[No tienes suficientes Robux para comprar este objeto. Sal del juego y dirígete a la pantalla de Robux para obtener más.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[No tienes suficientes Robux para comprar este objeto. Visita la página www.roblox.com para obtener más Robux.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[La compra no se ha realizado porque las compras dentro del juego están desactivadas temporalmente. No se te ha cobrado. Inténtalo de nuevo más tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[La compra no se ha realizado porque algo ha ido mal. No se te ha cobrado. Inténtalo de nuevo más tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[La compra de {ITEM_NAME} no se ha realizado porque algo ha ido mal. No se te ha cobrado. Inténtalo de nuevo más tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[No se ha podido recuperar tu saldo en este momento. No se te ha cobrado. Inténtalo de nuevo más tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[No se ha podido recuperar el precio de este objeto en este momento. No se te ha cobrado. Inténtalo de nuevo más tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[No hay más copias disponibles de este objeto de edición limitada. Intenta comprarlo de otro usuario en www.roblox.com. No se te ha cobrado.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[Este objeto no está en venta en este momento. No se te ha cobrado.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[Tienes que crear una cuenta de Roblox para comprar objetos. Visita www.roblox.com para más información.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[Se ha desactivado la venta de objetos de terceros para este lugar. No se te ha cobrado.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[Tu cuenta es para menores de 13 años. No se permite la compra de este objeto. No se te ha cobrado.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[Ya tienes este objeto. No se te ha cobrado.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[El objeto {ITEM_NAME} es gratuito. ¿Te lo quieres llevar?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[¿Quieres comprar {ASSET_TYPE} {ITEM_NAME} por]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[¡La compra de {ITEM_NAME} se ha realizado correctamente!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[Se necesitan {NEEDED_AMOUNT} Robux más para comprar {ASSET_TYPE} {ITEM_NAME}. ¿Quieres obtener más Robux?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[Artículos]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[Paquete]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[Sí]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[Cancelando]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[No estás renovando la suscripción. Esta no ha cambiado.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[Has cancelado tu suscripción de {ITEM_NAME}. Mantendrás todos los beneficios hasta el final del periodo pagado.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[¿Seguro que quieres cancelar la suscripción de {ITEM_NAME}? Mantendrás todos tus beneficios hasta el final del periodo pagado.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[¿Quieres suscribirte a {ITEM_NAME} para]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[Suscripción]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[{PRICE} por mes]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[Subscribir]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[Debes ser miembro de Premium para suscribirte.]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[Mejorar]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[La compra no se ha realizado porque necesitas ser miembro de Premium para suscribirte. No se te ha cobrado]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Comprar Robux]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[Tu saldo después de esta transacción será de {BALANCE_FUTURE} Robux]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[No tienes suficientes Robux para comprar este objeto. ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Solo Premium]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[Necesitas Roblox Premium para comprar este objeto.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[Esta compra implica el intercambio de moneda real. Certifico que soy mayor de 18 años y que soy el progenitor o tutor del propietario de la cuenta. Autorizo esta compra y acepto los Términos de servicio.]], +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/et-ee.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/et-ee.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/et-ee.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fi-fi.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fi-fi.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fi-fi.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fil-ph.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fil-ph.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fil-ph.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fr-fr.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fr-fr.lua new file mode 100644 index 0000000..90a45ae --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/fr-fr.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[Accessoires]], + ["Common.AssetTypes.Label.Hat"] = [[Chapeau]], + ["Common.AssetTypes.Label.Hair"] = [[Cheveux]], + ["Common.AssetTypes.Label.Face"] = [[Visage]], + ["Common.AssetTypes.Label.Neck"] = [[Cou]], + ["Common.AssetTypes.Label.Shoulder"] = [[Épaules]], + ["Common.AssetTypes.Label.Front"] = [[Avant]], + ["Common.AssetTypes.Label.Back"] = [[Retour]], + ["Common.AssetTypes.Label.Waist"] = [[Taille]], + ["Common.AssetTypes.Label.Animations"] = [[Animations]], + ["Common.AssetTypes.Label.Audio"] = [[Audio]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[Animations d'avatar]], + ["Common.AssetTypes.Label.Badges"] = [[Badges]], + ["Common.AssetTypes.Label.Decals"] = [[Insignes]], + ["Common.AssetTypes.Label.Faces"] = [[Visages]], + ["Common.AssetTypes.Label.GamePasses"] = [[Passes de jeu]], + ["Common.AssetTypes.Label.Gear"] = [[Équipement]], + ["Common.AssetTypes.Label.Heads"] = [[Têtes]], + ["Common.AssetTypes.Label.Meshes"] = [[Maillages]], + ["Common.AssetTypes.Label.Models"] = [[Modèles]], + ["Common.AssetTypes.Label.Packages"] = [[Packs]], + ["Common.AssetTypes.Label.Pants"] = [[Pantalons]], + ["Common.AssetTypes.Label.Places"] = [[Emplacements]], + ["Common.AssetTypes.Label.Plugins"] = [[Plugins]], + ["Common.AssetTypes.Label.Shirts"] = [[Chemises]], + ["Common.AssetTypes.Label.TShirts"] = [[Tee-shirts]], + ["Common.AssetTypes.Label.VipServers"] = [[Serveurs VIP]], + ["Common.AssetTypes.Label.Run"] = [[Course]], + ["Common.AssetTypes.Label.Walk"] = [[Marche]], + ["Common.AssetTypes.Label.Fall"] = [[Chute]], + ["Common.AssetTypes.Label.Jump"] = [[Saut]], + ["Common.AssetTypes.Label.Idle"] = [[Inaction]], + ["Common.AssetTypes.Label.Swim"] = [[Nage]], + ["Common.AssetTypes.Label.Climb"] = [[Escalade]], + ["Common.AssetTypes.Label.Hats"] = [[Chapeaux]], + ["Common.AssetTypes.Label.Shoulders"] = [[Épaules]], + ["Common.AssetTypes.Label.Death"] = [[Mort]], + ["Common.AssetTypes.Label.Pose"] = [[Pose]], + ["Common.AssetTypes.Label.Head"] = [[Tête]], + ["Common.AssetTypes.Label.TShirt"] = [[Tee-shirt]], + ["Common.AssetTypes.Label.Shirt"] = [[Chemise]], + ["Common.AssetTypes.Label.Decal"] = [[Insigne]], + ["Common.AssetTypes.Label.Model"] = [[Modèle]], + ["Common.AssetTypes.Label.Plugin"] = [[Plugin]], + ["Common.AssetTypes.Label.MeshPart"] = [[Partie de maillage]], + ["Common.AssetTypes.Label.GamePass"] = [[Passe de jeu]], + ["Common.AssetTypes.Label.Badge"] = [[Badge]], + ["Common.AssetTypes.Label.Package"] = [[Pack]], + ["Common.AssetTypes.Label.Place"] = [[Emplacement]], + ["Common.AssetTypes.Label.LeftArm"] = [[Bras gauche]], + ["Common.AssetTypes.Label.LeftLeg"] = [[Jambe gauche]], + ["Common.AssetTypes.Label.RightArm"] = [[Bras droit]], + ["Common.AssetTypes.Label.RightLeg"] = [[Jambe droite]], + ["Common.AssetTypes.Label.Torso"] = [[Torse]], + ["Common.AssetTypes.Label.Animation"] = [[Animation]], + ["Common.AssetTypes.Label.Emote"] = [[Emote]], + ["Common.BuildersClub.Label.PlanFree"] = [[Gratuit]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Classique]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Abonnement au Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Abonnement au Turbo Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Abonnement à l'Outrageous Builders Club]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[Oui]], + ["Common.BuildersClub.Label.No"] = [[Non]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[JAMAIS]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[À vie]], + ["Common.BuildersClub.Label.Membership"] = [[Abonnement]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Niveau Premium requis]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Avec Roblox Premium, vous avez droit à :]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[450 Robux par mois]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Accédez à des avantages réservés aux membres Premium]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[10 % de bonus lors de l'achat de Robux]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price} par mois]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[L'achat de Roblox Premium n'est pas pris en charge sur votre plateforme. Veuillez utiliser un ordinateur pour l'acheter.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[Il semble que le dévelopeur essaie de vous inviter à acheter Roblox Premium mais vous êtes déjà une membre Premium !]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[Le niveau Premium n'est pas disponible pour le moment. Réessayez plus tard !]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[{robux} Robux par mois]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[L'achat n'est pas finalisé, réessaie s'il te plaît.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[Erreur]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[Prendre gratuitement]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[Améliorer]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[Acheter maintenant]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[Acheter des R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[Annuler]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[OK]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[Achat]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[Le solde de votre compte ne sera pas affecté par cette transaction.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[Ceci est un achat test ; votre compte ne sera pas débité.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[C'était un achat test.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[Cet objet nécessite {BC_LEVEL}. Cliquez sur « Améliorer » pour améliorer votre Builders Club !]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[Votre solde après cette transaction sera de {BALANCE_FUTURE} R$]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[Votre solde est désormais de {BALANCE_NOW}.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[Les {REMAINING_ROBUX} Robux restants seront portés à votre solde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[Échec de la transaction. Motif : votre compte ne possède pas assez de Robux. Votre compte n'a pas été débité.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[Échec de la transaction. Motif : il vous faut un abonnement pour acheter cet objet. Votre compte n'a pas été débité.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[Cette objet coûte plus de Robux que vous n'en avez. Veuillez quitter le jeu et aller à l'écran Robux pour en acheter plus.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[Cet objet coûte trop de Robux pour que vous l'achetiez. Veuillez vous rendre sur www.roblox.com pour acheter plus de Robux.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[Échec de la transaction. Motif : les achats en jeu sont temporairement désactivés. Votre compte n'a pas été débité. Veuillez réessayer plus tard.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[Échec de la transaction. Motif : un problème est survenu. Votre compte n'a pas été débité. Veuillez réessayer plus tard.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[Votre achat de {ITEM_NAME} a échoué. Motif : un problème est survenu. Votre compte n'a pas été débité. Veuillez réessayer plus tard.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[Impossible d'obtenir votre solde pour l'instant. Votre compte n'a pas été débité. Veuillez réessayer plus tard.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[Nous ne pouvons pas récupérer le prix de cet objet pour l'instant. Votre compte n'a pas été débité. Veuillez réessayer plus tard.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[Il n'y a plus d'exemplaires de cet objet en série limitée. Essayez de l'acheter à un autre utilisateur sur www.roblox.com. Votre compte n'a pas été débité.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[Cet objet n'est pas en vente pour l'instant. Votre compte n'a pas été débité.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[Vous devez créer un compte ROBLOX pour acheter des objets, rendez-vous sur www.roblox.com pour plus d'informations.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[Les ventes d'objets par des tiers ont été désactivées pour cet emplacement. Votre compte n'a pas été débité.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[Le propriétaire de ce compte a moins de 13 ans. L'achat de cet objet n'est pas permis. Votre compte n'a pas été débité.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[Vous possédez déjà cet objet. Votre compte n'a pas été débité.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[Souhaitez-vous prendre l'objet {ITEM_NAME} GRATUITEMENT ?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[Vous voulez acheter le {ASSET_TYPE} {ITEM_NAME} pour]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[Votre achat de {ITEM_NAME} a réussi !]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[Vous avez besoin de {NEEDED_AMOUNT} Robux de plus pour acheter le {ASSET_TYPE} {ITEM_NAME}. Souhaitez-vous acheter plus de Robux ?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[Produit]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[Paquet]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[Oui]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[Annulation en cours]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[Tu n'as pas prévu de renouveller cet abonnement. Tes abonnements n'ont pas changé.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[Tu as annulé ton abonnement à {ITEM_NAME}. Tu garderas tes avantages jusqu'à la fin de la période payée.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[Es-tu sûr-e de vouloir annuler ton abonnement à {ITEM_NAME} ? Tu garderas tes avantages jusqu'à la fin de la période payée.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[Tu veux t'abonner à {ITEM_NAME} pour]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[Abonnement]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[{PRICE} par mois]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[S'inscrire]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[Tu dois être un membre Premium pour t'inscrire !]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[Actualiser]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[Échec de la transaction. Motif : il te faut un abonnement premium pour acheter cet objet. Ton compte n'a pas été débité]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Acheter des Robux]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[Ton solde après cette transaction sera de {BALANCE_FUTURE} Robux.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[Cette objet coûte plus de Robux que vous n'en avez. ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Premium uniquement]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[Tu dois avoir Roblox Premium pour acheter cet article.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[Cet achat implique l'échange d'argent réel. Je certifie avoir au moins 18 ans et être le parent ou le tuteur légal du propriétaire du compte. J'autorise cet achat et j'accepte les conditions de service.]], +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hi-in.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hi-in.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hi-in.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hr-hr.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hr-hr.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hr-hr.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hu-hu.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hu-hu.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/hu-hu.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/id-id.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/id-id.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/id-id.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/it-it.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/it-it.lua new file mode 100644 index 0000000..d066f0b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/it-it.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[Accessori]], + ["Common.AssetTypes.Label.Hat"] = [[Cappello]], + ["Common.AssetTypes.Label.Hair"] = [[Capelli]], + ["Common.AssetTypes.Label.Face"] = [[Faccia]], + ["Common.AssetTypes.Label.Neck"] = [[Collo]], + ["Common.AssetTypes.Label.Shoulder"] = [[Spalle]], + ["Common.AssetTypes.Label.Front"] = [[Fronte]], + ["Common.AssetTypes.Label.Back"] = [[Indietro]], + ["Common.AssetTypes.Label.Waist"] = [[Vita]], + ["Common.AssetTypes.Label.Animations"] = [[Animazioni]], + ["Common.AssetTypes.Label.Audio"] = [[Audio]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[Animazioni avatar]], + ["Common.AssetTypes.Label.Badges"] = [[Contrassegni]], + ["Common.AssetTypes.Label.Decals"] = [[Decalcomanie]], + ["Common.AssetTypes.Label.Faces"] = [[Facce]], + ["Common.AssetTypes.Label.GamePasses"] = [[Pass di gioco]], + ["Common.AssetTypes.Label.Gear"] = [[Attrezzatura]], + ["Common.AssetTypes.Label.Heads"] = [[Teste]], + ["Common.AssetTypes.Label.Meshes"] = [[Mesh]], + ["Common.AssetTypes.Label.Models"] = [[Modelli]], + ["Common.AssetTypes.Label.Packages"] = [[Pacchetti]], + ["Common.AssetTypes.Label.Pants"] = [[Pantaloni]], + ["Common.AssetTypes.Label.Places"] = [[Località]], + ["Common.AssetTypes.Label.Plugins"] = [[Plug-in]], + ["Common.AssetTypes.Label.Shirts"] = [[Camicie]], + ["Common.AssetTypes.Label.TShirts"] = [[Magliette]], + ["Common.AssetTypes.Label.VipServers"] = [[Server VIP]], + ["Common.AssetTypes.Label.Run"] = [[Corri]], + ["Common.AssetTypes.Label.Walk"] = [[Cammina]], + ["Common.AssetTypes.Label.Fall"] = [[Cadi]], + ["Common.AssetTypes.Label.Jump"] = [[Salta]], + ["Common.AssetTypes.Label.Idle"] = [[Inattivo]], + ["Common.AssetTypes.Label.Swim"] = [[Nuota]], + ["Common.AssetTypes.Label.Climb"] = [[Scala]], + ["Common.AssetTypes.Label.Hats"] = [[Cappelli]], + ["Common.AssetTypes.Label.Shoulders"] = [[Spalle]], + ["Common.AssetTypes.Label.Death"] = [[Morte]], + ["Common.AssetTypes.Label.Pose"] = [[Posa]], + ["Common.AssetTypes.Label.Head"] = [[Testa]], + ["Common.AssetTypes.Label.TShirt"] = [[Maglietta]], + ["Common.AssetTypes.Label.Shirt"] = [[Camicia]], + ["Common.AssetTypes.Label.Decal"] = [[Decalcomania]], + ["Common.AssetTypes.Label.Model"] = [[Modello]], + ["Common.AssetTypes.Label.Plugin"] = [[Plug-in]], + ["Common.AssetTypes.Label.MeshPart"] = [[Parte mesh]], + ["Common.AssetTypes.Label.GamePass"] = [[Pass di gioco]], + ["Common.AssetTypes.Label.Badge"] = [[Contrassegno]], + ["Common.AssetTypes.Label.Package"] = [[Pacchetto]], + ["Common.AssetTypes.Label.Place"] = [[Località]], + ["Common.AssetTypes.Label.LeftArm"] = [[Braccio sinistro]], + ["Common.AssetTypes.Label.LeftLeg"] = [[Gamba sinistra]], + ["Common.AssetTypes.Label.RightArm"] = [[Braccio destro]], + ["Common.AssetTypes.Label.RightLeg"] = [[Gamba destra]], + ["Common.AssetTypes.Label.Torso"] = [[Busto]], + ["Common.AssetTypes.Label.Animation"] = [[Animazione]], + ["Common.AssetTypes.Label.Emote"] = [[Emoticon]], + ["Common.BuildersClub.Label.PlanFree"] = [[Gratis]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Classica]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Turbo]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Abbonamento Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Abbonamento Turbo Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Abbonamento Outrageous Builders Club]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[Sì]], + ["Common.BuildersClub.Label.No"] = [[No]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[MAI]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Builders Club Classico]], + ["Common.BuildersClub.Label.Lifetime"] = [[A vita]], + ["Common.BuildersClub.Label.Membership"] = [[Abbonamento]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Premium necessario]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Con Roblox Premium otterrai:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[450 Robux al mese]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Accesso a vantaggi esclusivi Premium]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[Bonus del 10% quando acquisti Robux]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price}/mese]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[Non è possibile acquistare Roblox Premium sulla tua piattaforma. Acquista la versione Premium da un computer desktop.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[Sembra che lo sviluppatore stia cercando di farti acquistare Roblox Premium, ma sei già un membro Premium!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[Premium non è al momento disponibile. Riprova più tardi!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[{robux} Robux al mese]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[L'acquisto non è stato completato, riprova.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[Errore]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[Prendi gratis]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[Aggiorna]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[Compra ora]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[Compra R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[Annulla]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[OK]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[Acquisto]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[I fondi del tuo account non verranno intaccati da questa transazione.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[Questo è un acquisto di prova; il tuo account non subirà addebiti.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[Questo era un acquisto di prova.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[Questo oggetto richiede: {BC_LEVEL}. Clicca su "Migliora" per potenziare il tuo Builders Club!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[Dopo la transazione, avrai R${BALANCE_FUTURE}]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[Ora i tuoi fondi sono {BALANCE_NOW}.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[I rimanenti {REMAINING_ROBUX} Robux verranno accreditati ai tuoi fondi.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[Il tuo acquisto non è andato a buon fine poiché il tuo account non ha abbastanza Robux. Il tuo account non ha subito addebiti.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[Il tuo acquisto non è riuscito poiché hai bisogno di un abbonamento per acquistare questo articolo. Il tuo account non ha subito alcun addebito.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[Questo oggetto costa più Robux di quanti tu ne abbia. Esci dal gioco e vai nella schermata dei ROBUX per acquistarne altri.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[Questo oggetto costa più Robux di quanti tu possa acquistarne. Visita il sito www.roblox.com per acquistare altri ROBUX.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[Il tuo acquisto non è andato a buon fine poiché gli acquisti in gioco sono temporaneamente disabilitati. Il tuo account non ha subito alcun addebito. Riprova più tardi.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[L'acquisto non è andato a buon fine a causa di un errore. Il tuo account non ha subito addebiti. Riprova più tardi.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[L'acquisto di {ITEM_NAME} non è andato a buon fine a causa di un errore. Il tuo account non ha subito addebiti. Riprova più tardi.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[Impossibile recuperare i tuoi fondi in questo momento. Il tuo account non ha subito addebiti. Riprova più tardi.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[Impossibile recuperare il prezzo dell'oggetto in questo momento. Il tuo account non ha subito addebiti. Riprova più tardi.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[Questo oggetto limitato non è più disponibile. Prova ad acquistarlo da un altro utente sul sito www.roblox.com. Il tuo account non ha subito addebiti.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[Questo oggetto non è attualmente in vendita. Il tuo account non ha subito addebiti.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[Per poter comprare oggetti, devi creare un account ROBLOX. Visita il sito www.roblox.com per maggiori informazioni.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[Le vendite di oggetti di terzi sono state disattivate per questa località. Il tuo account non ha subito addebiti.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[Il tuo account è per giocatori con meno di 13 anni. L'acquisto di questo oggetto non è permesso. Il tuo account non ha subito addebiti.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[Possiedi già questo oggetto. Il tuo account non ha subito addebiti.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[Vuoi prendere l'oggetto {ITEM_NAME} GRATIS?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[Vuoi comprare {ASSET_TYPE} {ITEM_NAME} per]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[Il tuo acquisto di {ITEM_NAME} è andato a buon fine!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[Hai bisogno di {NEEDED_AMOUNT} Robux in più per comprare: {ASSET_TYPE} {ITEM_NAME}. Vuoi acquistare altri ROBUX?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[Prodotto]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[Bundle]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[Sì]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[Annullamento]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[Al momento non stai rinnovando l'abbonamento. I tuoi abbonamenti non sono cambiati.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[Hai disdetto l'abbonamento a {ITEM_NAME}. Manterrai i tuoi benefici fino alla fine del periodo di pagamento.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[Sei sicuro di voler disdire l'abbonamento a {ITEM_NAME}. Manterrai i tuoi benefici fino alla fine del periodo di pagamento.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[Vuoi abbonarti a {ITEM_NAME} per]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[Abbonamento]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[{PRICE} al mese]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[Abbonati]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[Devi essere un membro Premium per poterti abbonare!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[Aggiorna]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[Il tuo acquisto non è riuscito poiché devi essere un membro Premium per abbonarti. Il tuo account non ha subito alcun addebito.]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Acquista Robux]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[Dopo la transazione, i tuoi fondi ammonteranno a {BALANCE_FUTURE} Robux.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[Questo oggetto costa più Robux di quanti tu ne abbia. ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Solo Premium]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[Hai bisogno di Roblox Premium per poter comprare questo oggetto.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[Solo gli adulti possono fare acquisti su Roblox. Riconosco di avere almeno 18 anni. Sono il proprietario di questo account o il genitore o il tutore legale del proprietario. Autorizzo questo acquisto e accetto i Termini di servizio.]], +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ja-jp.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ja-jp.lua new file mode 100644 index 0000000..1bb5da1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ja-jp.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[アクセサリ]], + ["Common.AssetTypes.Label.Hat"] = [[帽子]], + ["Common.AssetTypes.Label.Hair"] = [[髪]], + ["Common.AssetTypes.Label.Face"] = [[顔]], + ["Common.AssetTypes.Label.Neck"] = [[首]], + ["Common.AssetTypes.Label.Shoulder"] = [[肩]], + ["Common.AssetTypes.Label.Front"] = [[正面]], + ["Common.AssetTypes.Label.Back"] = [[背面]], + ["Common.AssetTypes.Label.Waist"] = [[腰]], + ["Common.AssetTypes.Label.Animations"] = [[アニメーション]], + ["Common.AssetTypes.Label.Audio"] = [[オーディオ]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[アバターアニメ]], + ["Common.AssetTypes.Label.Badges"] = [[バッジ]], + ["Common.AssetTypes.Label.Decals"] = [[デカール]], + ["Common.AssetTypes.Label.Faces"] = [[顔]], + ["Common.AssetTypes.Label.GamePasses"] = [[ゲームパス]], + ["Common.AssetTypes.Label.Gear"] = [[ギア]], + ["Common.AssetTypes.Label.Heads"] = [[頭]], + ["Common.AssetTypes.Label.Meshes"] = [[メッシュ]], + ["Common.AssetTypes.Label.Models"] = [[モデル]], + ["Common.AssetTypes.Label.Packages"] = [[パッケージ]], + ["Common.AssetTypes.Label.Pants"] = [[ズボン]], + ["Common.AssetTypes.Label.Places"] = [[プレース]], + ["Common.AssetTypes.Label.Plugins"] = [[プラグイン]], + ["Common.AssetTypes.Label.Shirts"] = [[シャツ]], + ["Common.AssetTypes.Label.TShirts"] = [[Tシャツ]], + ["Common.AssetTypes.Label.VipServers"] = [[VIPサーバー]], + ["Common.AssetTypes.Label.Run"] = [[走る]], + ["Common.AssetTypes.Label.Walk"] = [[歩く]], + ["Common.AssetTypes.Label.Fall"] = [[落下]], + ["Common.AssetTypes.Label.Jump"] = [[ジャンプ]], + ["Common.AssetTypes.Label.Idle"] = [[待機]], + ["Common.AssetTypes.Label.Swim"] = [[泳ぐ]], + ["Common.AssetTypes.Label.Climb"] = [[登る]], + ["Common.AssetTypes.Label.Hats"] = [[帽子]], + ["Common.AssetTypes.Label.Shoulders"] = [[肩]], + ["Common.AssetTypes.Label.Death"] = [[死]], + ["Common.AssetTypes.Label.Pose"] = [[ポーズ]], + ["Common.AssetTypes.Label.Head"] = [[頭]], + ["Common.AssetTypes.Label.TShirt"] = [[Tシャツ]], + ["Common.AssetTypes.Label.Shirt"] = [[シャツ]], + ["Common.AssetTypes.Label.Decal"] = [[デカール]], + ["Common.AssetTypes.Label.Model"] = [[モデル]], + ["Common.AssetTypes.Label.Plugin"] = [[プラグイン]], + ["Common.AssetTypes.Label.MeshPart"] = [[メッシュパーツ]], + ["Common.AssetTypes.Label.GamePass"] = [[ゲームパス]], + ["Common.AssetTypes.Label.Badge"] = [[バッジ]], + ["Common.AssetTypes.Label.Package"] = [[パッケージ]], + ["Common.AssetTypes.Label.Place"] = [[プレース]], + ["Common.AssetTypes.Label.LeftArm"] = [[左腕]], + ["Common.AssetTypes.Label.LeftLeg"] = [[左脚]], + ["Common.AssetTypes.Label.RightArm"] = [[右腕]], + ["Common.AssetTypes.Label.RightLeg"] = [[右脚]], + ["Common.AssetTypes.Label.Torso"] = [[胴体]], + ["Common.AssetTypes.Label.Animation"] = [[アニメーション]], + ["Common.AssetTypes.Label.Emote"] = [[エモート]], + ["Common.BuildersClub.Label.PlanFree"] = [[無料]], + ["Common.BuildersClub.Label.PlanClassic"] = [[クラシック]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Builders Club メンバーシップ]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Turbo Builders Club メンバーシップ]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Outrageous Builders Club メンバーシップ]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[はい]], + ["Common.BuildersClub.Label.No"] = [[いいえ]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[しない]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[クラシック Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[永久]], + ["Common.BuildersClub.Label.Membership"] = [[メンバーシップ]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Premiumが必要です]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Roblox Premiumには以下が含まれています:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[1ヶ月に450Robux]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Premium限定特典にアクセス]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[Robux購入時に10%のボーナス]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price}/月額]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[お使いのプラットフォームはRoblox Premiumの購入に対応していません。Premiumを購入するにはデスクトップをお使いください。]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[開発者がRoblox Premiumを購入するように促しているようですが、すでにPremiumメンバーです!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[現在、Premiumが利用できません。あとでお試しください!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[1ヶ月につき{robux} Robux]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[購入は完了していません。もう一度お試しください。]], + ["CoreScripts.PremiumModal.Title.Error"] = [[エラー]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[無料配布]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[アップグレード]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[今すぐ買う]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[R$ を買う]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[キャンセル]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[OK]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[購入しています]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[この取引であなたのアカウント残高は変わりません。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[これはテスト購入です。アカウントには課金されません。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[これはテスト購入です。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[このアイテムを買うには{BC_LEVEL}である必要があります。「アップグレード」をクリックしてBuilders Clubをアップグレードしてください!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[この取引の後の残高は R${BALANCE_FUTURE}です]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[現在の残高は{BALANCE_NOW}です。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[残りの{REMAINING_ROBUX} Robuxがあなたのアカウントに払い戻しされます。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[アカウントのRobuxが不足しているため購入を完了できませんでした。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[このアイテムを買うにはサブスクリプションが必要なため、購入を完了できませんでした。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[このアイテムを買うにはお手持ちのRobuxでは足りません。ゲームを終了して、Robux画面で追加購入してください。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[このアイテムは、お手持ちのRobuxでは購入できません。www.roblox.comでRobuxを追加購入してください。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[ゲーム内購入が一時的に無効になっているため、購入できませんでした。アカウントは課金されていません。後でもう一度お試し下さい。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[問題が発生したため、購入を完了できませんでした。アカウントは課金されていません。後でもう一度お試し下さい。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[問題が発生したため、{ITEM_NAME} の購入を完了できませんでした。アカウントは課金されていません。後でもう一度お試し下さい。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[現在、残高を取得できません。アカウントは課金されていません。後でもう一度お試し下さい。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[現在、アイテム価格を取得できません。アカウントは課金されていません。後でもう一度お試し下さい。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[この限定アイテムはもう残っていません。www.roblox.com で他のユーザーから購入してみてください。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[このアイテムは現在売られていません。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[アイテムを買うにはRobloxアカウントを作る必要があります。詳しくは www.roblox.com でご確認ください。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[サードパーティ製のアイテムを売ることは、ここでは禁止されています。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[アカウントが13歳未満用です。このアイテムの購入は許可されていません。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[このアイテムはすでに持っています。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[{ITEM_NAME} を無料で入手しますか?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[{ASSET_TYPE} {ITEM_NAME} を以下の値段で買いますか:]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[{ITEM_NAME}を購入しました!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[{ASSET_TYPE} {ITEM_NAME}を買うにはあと{NEEDED_AMOUNT}のRobuxが必要です。もっとRobuxを買い足しますか?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[製品]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[バンドル]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[はい]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[キャンセル中]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[現在、サブスクリプションを更新していません。サブスクリプション料金は課金されていません。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[{ITEM_NAME}のサブスクリプションをキャンセルしました。お支払い期間の終了日までサービスをご利用できます。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[{ITEM_NAME}のサブスクリプションをキャンセルしますか。お支払い期間の終了日までサービスをご利用できます。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[{ITEM_NAME}のサブスクリプションを申し込みます]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[サブスクリプション]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[月額 {PRICE} ]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[サブスクリプション契約する]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[サブスクリプション契約するには、Premiumメンバーである必要があります!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[アップグレード]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[このアイテムを買うにはサブスクリプションが必要なため、購入を完了できませんでした。アカウントは課金されていません。]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Robuxを買う]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[取引後の残高は{BALANCE_FUTURE} Robuxになります]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[このアイテムの費用は利用できるRobuxの額を超えています。 ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Premium限定]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[このアイテムを購入するには、Roblox Premiumが必要です。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[この購入には、実際のお金の交換が関わります。私の年齢が少なくとも18歳であり、アカウント所有者の親か法的保護者であることを認めます。この購入を許可し、利用規約に同意します。]], +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ka-ge.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ka-ge.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ka-ge.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/kk-kz.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/kk-kz.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/kk-kz.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/km-kh.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/km-kh.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/km-kh.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ko-kr.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ko-kr.lua new file mode 100644 index 0000000..25f8211 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ko-kr.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[장신구]], + ["Common.AssetTypes.Label.Hat"] = [[모자]], + ["Common.AssetTypes.Label.Hair"] = [[헤어]], + ["Common.AssetTypes.Label.Face"] = [[얼굴]], + ["Common.AssetTypes.Label.Neck"] = [[목]], + ["Common.AssetTypes.Label.Shoulder"] = [[어깨]], + ["Common.AssetTypes.Label.Front"] = [[가슴]], + ["Common.AssetTypes.Label.Back"] = [[등]], + ["Common.AssetTypes.Label.Waist"] = [[허리]], + ["Common.AssetTypes.Label.Animations"] = [[애니메이션]], + ["Common.AssetTypes.Label.Audio"] = [[오디오]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[아바타 애니메이션]], + ["Common.AssetTypes.Label.Badges"] = [[배지]], + ["Common.AssetTypes.Label.Decals"] = [[데칼]], + ["Common.AssetTypes.Label.Faces"] = [[얼굴]], + ["Common.AssetTypes.Label.GamePasses"] = [[게임패스]], + ["Common.AssetTypes.Label.Gear"] = [[장비]], + ["Common.AssetTypes.Label.Heads"] = [[머리]], + ["Common.AssetTypes.Label.Meshes"] = [[메시]], + ["Common.AssetTypes.Label.Models"] = [[모델]], + ["Common.AssetTypes.Label.Packages"] = [[패키지]], + ["Common.AssetTypes.Label.Pants"] = [[바지]], + ["Common.AssetTypes.Label.Places"] = [[플레이스]], + ["Common.AssetTypes.Label.Plugins"] = [[플러그인]], + ["Common.AssetTypes.Label.Shirts"] = [[셔츠]], + ["Common.AssetTypes.Label.TShirts"] = [[티셔츠]], + ["Common.AssetTypes.Label.VipServers"] = [[VIP 서버]], + ["Common.AssetTypes.Label.Run"] = [[달리기]], + ["Common.AssetTypes.Label.Walk"] = [[걷기]], + ["Common.AssetTypes.Label.Fall"] = [[낙하]], + ["Common.AssetTypes.Label.Jump"] = [[점프]], + ["Common.AssetTypes.Label.Idle"] = [[대기]], + ["Common.AssetTypes.Label.Swim"] = [[수영]], + ["Common.AssetTypes.Label.Climb"] = [[오르기]], + ["Common.AssetTypes.Label.Hats"] = [[모자]], + ["Common.AssetTypes.Label.Shoulders"] = [[어깨]], + ["Common.AssetTypes.Label.Death"] = [[사망]], + ["Common.AssetTypes.Label.Pose"] = [[포즈]], + ["Common.AssetTypes.Label.Head"] = [[머리]], + ["Common.AssetTypes.Label.TShirt"] = [[티셔츠]], + ["Common.AssetTypes.Label.Shirt"] = [[셔츠]], + ["Common.AssetTypes.Label.Decal"] = [[데칼]], + ["Common.AssetTypes.Label.Model"] = [[모델]], + ["Common.AssetTypes.Label.Plugin"] = [[플러그인]], + ["Common.AssetTypes.Label.MeshPart"] = [[메시 파트]], + ["Common.AssetTypes.Label.GamePass"] = [[게임패스]], + ["Common.AssetTypes.Label.Badge"] = [[배지]], + ["Common.AssetTypes.Label.Package"] = [[패키지]], + ["Common.AssetTypes.Label.Place"] = [[플레이스]], + ["Common.AssetTypes.Label.LeftArm"] = [[왼팔]], + ["Common.AssetTypes.Label.LeftLeg"] = [[왼 다리]], + ["Common.AssetTypes.Label.RightArm"] = [[오른팔]], + ["Common.AssetTypes.Label.RightLeg"] = [[오른 다리]], + ["Common.AssetTypes.Label.Torso"] = [[몸통]], + ["Common.AssetTypes.Label.Animation"] = [[애니메이션]], + ["Common.AssetTypes.Label.Emote"] = [[감정 표현]], + ["Common.BuildersClub.Label.PlanFree"] = [[무료]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Classic]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Builders Club 멤버십]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Turbo Builders Club 멤버십]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Outrageous Builders Club 멤버십]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[예]], + ["Common.BuildersClub.Label.No"] = [[아니요]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[절대 안 함]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[평생]], + ["Common.BuildersClub.Label.Membership"] = [[멤버십]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Premium 필요]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Roblox Premium 회원을 위한 다양한 혜택:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[매달 450 Robux 획득]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Premium 전용 혜택 이용]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[Robux 구입 시 10% 추가 보너스 증정]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price}/월]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[Roblox Premium 구매가 지원되지 않는 플랫폼입니다. Premium을 구매하려면 데스크톱을 사용하세요.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[개발자가 Roblox Premium 구매를 도와드리려 한 것 같네요. 그런데 이미 Premium 회원이세요!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[Premium이 일시적으로 이용 불가능합니다. 나중에 다시 시도하세요!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[매월 {robux} Robux 지급]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[구매를 완료하지 못했습니다. 다시 시도하세요.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[오류]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[무료 획득]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[업그레이드]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[지금 구매하기]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[R$ 구매]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[취소]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[확인]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[구매 중]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[본 거래는 계정 잔액에 영향을 주지 않습니다.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[테스트 구매입니다. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[테스트 구매입니다.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[본 아이템은 {BC_LEVEL}이(가) 필요합니다. '업그레이드' 버튼을 클릭해 Builders Club을 업그레이드하세요!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[본 거래 후 예상 잔액은 R${BALANCE_FUTURE}입니다.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[현재 잔액은 {BALANCE_NOW}입니다.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[남은 {REMAINING_ROBUX} Robux는 회원님의 계정에 적립됩니다.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[Robux가 부족해 구매하지 못했습니다. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[본 아이템을 구매에 필요한 플랜에 가입하지 않아 아이템을 구매하지 못했습니다. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[Robux가 부족해 본 아이템을 구매할 수 없습니다. 게임을 종료한 후 Robux 화면으로 이동하여 로벅스를 추가 구매하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[Robux가 부족해 본 아이템을 구매할 수 없습니다. Robux를 구매하려면 www.roblox.com을 방문하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[게임 내 구매의 일시적으로 비활성화로 인해 구매하지 못했습니다. 계정에 비용이 청구되지 않아요. 나중에 다시 시도하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[오류가 발생해 구매하지 못했습니다. 계정에 비용이 청구되지 않아요. 나중에 다시 시도하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[오류가 발생해 {ITEM_NAME} 구매하지 못했습니다. 계정에 비용이 청구되지 않아요. 나중에 다시 시도하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[지금은 잔액을 불러올 수 없습니다. 계정에 비용이 청구되지 않아요. 나중에 다시 시도하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[지금은 아이템 가격을 불러올 수 없어요. 계정에 비용이 청구되지 않아요. 나중에 다시 시도하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[본 아이템은 한정판 아이템으로 더 이상 재고가 없습니다. www.roblox.com에서 다른 사용자에게 구매해보세요. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[현재 판매 중인 아이템이 아닙니다. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[ROBLOX 계정을 만들어야 아이템을 구매할 수 있습니다. 자세한 정보는 www.roblox.com에서 확인하세요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[본 장소에 대한 제삼자 아이템 판매가 비활성화 상태입니다. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[본 아이템은 만 13세 미만 계정으로 구매할 수 없어요. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[이미 보유하고 있는 아이템입니다. 계정에 비용이 청구되지 않아요.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[무료로 {ITEM_NAME}을 획득하시겠습니까?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[다음 가격으로 {ASSET_TYPE} {ITEM_NAME}을(를) 구매하시겠습니까?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[{ITEM_NAME} 구매 성공!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[{NEEDED_AMOUNT} Robux가 더 있어야 {ASSET_TYPE} {ITEM_NAME}을(를) 구매할 수 있습니다. Robux를 구매하시겠습니까?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[상품]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[번들]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[예]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[취소 중]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[가입을 갱신하지 않습니다. 변경 사항이 없습니다.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[{ITEM_NAME} 가입을 취소했어요. 지불 기간이 끝날 때까지 혜택은 계속됩니다.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[{ITEM_NAME} 가입을 정말로 취소할까요? 취소하더라도 지불 기간이 끝날 때까지 혜택은 계속됩니다.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[{ITEM_NAME}에 가입할까요? 비용은]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[가입]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[매월 {PRICE}]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[가입]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[가입하려면 Premium 멤버여야 해요!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[업그레이드]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[Premium 멤버가 아니어서 아이템을 구매하지 못했어요. 비용도 청구되지 않았습니다.]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Robux 구매]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[본 거래 후 예상 잔액은 {BALANCE_FUTURE} Robux입니다.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[아이템 구매에 충분한 Robux를 보유하고 있지 않습니다.]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Premium 전용]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[이 아이템을 구매하려면 Roblox Premium 회원이어야 해요.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[이 구매는 실물 화폐 교환으로 이루어집니다. 본인은 18세 이상이며 계정 소유자의 부모 또는 법적 보호자임에 동의합니다. 또한 이 구매를 승인하고 서비스 약관에 동의합니다.]], +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/lt-lt.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/lt-lt.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/lt-lt.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/lv-lv.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/lv-lv.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/lv-lv.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ms-my.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ms-my.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ms-my.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/my-mm.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/my-mm.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/my-mm.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/nb-no.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/nb-no.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/nb-no.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/nl-nl.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/nl-nl.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/nl-nl.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/pl-pl.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/pl-pl.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/pl-pl.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/pt-br.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/pt-br.lua new file mode 100644 index 0000000..e550e27 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/pt-br.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[Acessórios]], + ["Common.AssetTypes.Label.Hat"] = [[Chapéu]], + ["Common.AssetTypes.Label.Hair"] = [[Cabelo]], + ["Common.AssetTypes.Label.Face"] = [[Rosto]], + ["Common.AssetTypes.Label.Neck"] = [[Pescoço]], + ["Common.AssetTypes.Label.Shoulder"] = [[Ombro]], + ["Common.AssetTypes.Label.Front"] = [[Frente]], + ["Common.AssetTypes.Label.Back"] = [[Costas]], + ["Common.AssetTypes.Label.Waist"] = [[Cintura]], + ["Common.AssetTypes.Label.Animations"] = [[Animações]], + ["Common.AssetTypes.Label.Audio"] = [[Áudio]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[Animações de avatar]], + ["Common.AssetTypes.Label.Badges"] = [[Emblemas]], + ["Common.AssetTypes.Label.Decals"] = [[Adesivos]], + ["Common.AssetTypes.Label.Faces"] = [[Rostos]], + ["Common.AssetTypes.Label.GamePasses"] = [[Passes de jogo]], + ["Common.AssetTypes.Label.Gear"] = [[Equipamentos]], + ["Common.AssetTypes.Label.Heads"] = [[Cabeças]], + ["Common.AssetTypes.Label.Meshes"] = [[Malhas]], + ["Common.AssetTypes.Label.Models"] = [[Modelos]], + ["Common.AssetTypes.Label.Packages"] = [[Pacotes]], + ["Common.AssetTypes.Label.Pants"] = [[Calças]], + ["Common.AssetTypes.Label.Places"] = [[Locais]], + ["Common.AssetTypes.Label.Plugins"] = [[Plugins]], + ["Common.AssetTypes.Label.Shirts"] = [[Camisas]], + ["Common.AssetTypes.Label.TShirts"] = [[Camisetas]], + ["Common.AssetTypes.Label.VipServers"] = [[Servidores VIP]], + ["Common.AssetTypes.Label.Run"] = [[Correr]], + ["Common.AssetTypes.Label.Walk"] = [[Andar]], + ["Common.AssetTypes.Label.Fall"] = [[Cair]], + ["Common.AssetTypes.Label.Jump"] = [[Pular]], + ["Common.AssetTypes.Label.Idle"] = [[Inatividade]], + ["Common.AssetTypes.Label.Swim"] = [[Nadar]], + ["Common.AssetTypes.Label.Climb"] = [[Escalar]], + ["Common.AssetTypes.Label.Hats"] = [[Chapéus]], + ["Common.AssetTypes.Label.Shoulders"] = [[Ombros]], + ["Common.AssetTypes.Label.Death"] = [[Morte]], + ["Common.AssetTypes.Label.Pose"] = [[Pose]], + ["Common.AssetTypes.Label.Head"] = [[Cabeça]], + ["Common.AssetTypes.Label.TShirt"] = [[Camisetas]], + ["Common.AssetTypes.Label.Shirt"] = [[Camisas]], + ["Common.AssetTypes.Label.Decal"] = [[Adesivos]], + ["Common.AssetTypes.Label.Model"] = [[Modelos]], + ["Common.AssetTypes.Label.Plugin"] = [[Plugins]], + ["Common.AssetTypes.Label.MeshPart"] = [[Parte da malha]], + ["Common.AssetTypes.Label.GamePass"] = [[Passe de jogo]], + ["Common.AssetTypes.Label.Badge"] = [[Emblemas]], + ["Common.AssetTypes.Label.Package"] = [[Pacotes]], + ["Common.AssetTypes.Label.Place"] = [[Locais]], + ["Common.AssetTypes.Label.LeftArm"] = [[Braço esquerdo]], + ["Common.AssetTypes.Label.LeftLeg"] = [[Perna esquerda]], + ["Common.AssetTypes.Label.RightArm"] = [[Braço direito]], + ["Common.AssetTypes.Label.RightLeg"] = [[Perna direita]], + ["Common.AssetTypes.Label.Torso"] = [[Tronco]], + ["Common.AssetTypes.Label.Animation"] = [[Animações]], + ["Common.AssetTypes.Label.Emote"] = [[Emote]], + ["Common.BuildersClub.Label.PlanFree"] = [[Grátis]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Clássico]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Assinatura do Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Assinatura do Turbo Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Assinatura do Outrageous Builders Club]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[Sim]], + ["Common.BuildersClub.Label.No"] = [[Não]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[NUNCA]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[Vitalícia]], + ["Common.BuildersClub.Label.Membership"] = [[Assinatura]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Requer Premium]], + ["CoreScripts.PremiumModal.Body.Description"] = [[O Roblox Premium dá direito a:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[450 Robux por mês]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Acesso a benefícios exclusivos]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[Bônus de 10% ao comprar Robux]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price}/mês]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[A compra do Roblox Premium não está disponível na sua plataforma. Use o computador para comprar a assinatura Premium.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[Parece que o desenvolvedor quer te convidar para comprar uma inscrição do Roblox Premium, mas você já se inscreveu!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[O Roblox Premium não está disponível no momento. Tente mais tarde!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[{robux} Robux por mês]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[A compra não foi concluída; tente novamente.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[Erro]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[Pegue de graça]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[Melhorar]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[Comprar agora]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[Comprar R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[Cancelar]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[OK]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[Comprando]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[O saldo da sua conta não será afetado por esta transação.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[Esta é uma compra de teste. Nada será cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[Esta foi uma compra de teste.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[Este item requer {BC_LEVEL}. Clique em 'Melhorar' para melhorar seu Builders Club!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[Seu saldo depois desta transação será de R$ {BALANCE_FUTURE}]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[Seu saldo agora é {BALANCE_NOW}.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[Os {REMAINING_ROBUX} Robux restantes serão somados ao seu saldo.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[Sua compra falhou porque sua conta não tem ROBUX suficientes. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[Sua compra falhou porque você precisa de uma assinatura para comprar este item. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[Este item custa mais Robux do que você possui disponível. Saia do jogo e vá para a tela de Robux para comprar mais.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[Este item custa mais Robux do que você pode comprar. Visite www.roblox.com para comprar mais Robux.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[Sua compra falhou porque as compras no jogo estão temporariamente desabilitadas. Nada foi cobrado da sua conta. Tente de novo mais tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[Sua compra falhou porque algo deu errado. Nada foi cobrado da sua conta. Tente de novo mais tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[Sua compra do item {ITEM_NAME} falhou porque algo deu errado. Nada foi cobrado da sua conta. Tente de novo mais tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[Impossível obter seu saldo no momento. Nada foi cobrado da sua conta. Tente de novo mais tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[Não conseguimos obter o preço do item no momento. Nada foi cobrado da sua conta. Tente de novo mais tarde.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[Esgotaram-se as cópias deste item limitado. Tente comprar de outro usuário em www.roblox.com. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[Este item não está à venda no momento. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[Você precisa criar uma conta ROBLOX para comprar itens. Visite www.roblox.com para mais informações.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[Vendas de itens de terceiros foram desabilitadas para este local. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[Sua conta é para menor de 13 anos. A compra deste item não é permitida. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[Você já possui este item. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[Gostaria de obter {ITEM_NAME} GRÁTIS?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[Deseja comprar o(a) {ASSET_TYPE} {ITEM_NAME} por]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[Sua compra de {ITEM_NAME} foi bem-sucedida!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[Você precisa de mais {NEEDED_AMOUNT} Robux para comprar o(a) {ASSET_TYPE} {ITEM_NAME}. Gostaria de comprar mais Robux?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[Produto]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[Pacote]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[Sim]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[Cancelando]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[Você não está renovando esta assinatura no momento. Suas assinaturas não foram alteradas.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[Você cancelou sua assinatura {ITEM_NAME}. Você continuará com os benefícios até o fim do período de pagamento.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[Você quer mesmo cancelar sua assinatura {ITEM_NAME}? Você continuará com os benefícios até o fim do período de pagamento.]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[Deseja adquirir a assinatura {ITEM_NAME} por]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[Assinatura]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[{PRICE} por mês]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[Assinar]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[Você deve ser um membro Premium para assinar!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[Melhorar]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[Sua compra falhou porque você precisa ser um membro Premium para assinar. Nada foi cobrado da sua conta.]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[Comprar Robux]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[Seu saldo depois desta transação será de {BALANCE_FUTURE} Robux]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[Você não tem Robux suficientes para comprar este item. ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Apenas Premium]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[Você precisa ter Roblox Premium para comprar este item.]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[Só adultos podem efetuar compras na Roblox. Eu afirmo ter mais de 18 anos e sou o dono desta conta ou pai ou responsável legal do dono. Autorizo esta compra e concordo com os Termos de Uso.]], +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ro-ro.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ro-ro.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ro-ro.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ru-ru.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ru-ru.lua new file mode 100644 index 0000000..13ff657 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/ru-ru.lua @@ -0,0 +1,26 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[Требуется премиум-подписка]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Преимущества премиум-подписки Roblox:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[450 Robux в месяц;]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Доступ к возможностям только для премиум-подписчиков]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[бонус в 10% при покупке Robux.]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price}/месяц]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[Покупка премиум-подписки Roblox не поддерживается на вашей платформе. Используйте ПК для покупки.]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[Похоже, разработчик пытается убедить вас купить премиум-подписку Roblox, а она у вас уже есть!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[В настоящий момент премиум-подписка недоступна. Повторите попытку позже!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[{robux} Robux в месяц;]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[Покупка не удалась, попробуйте еще раз.]], + ["CoreScripts.PremiumModal.Title.Error"] = [[Ошибка]], +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/si-lk.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/si-lk.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/si-lk.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sk-sk.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sk-sk.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sk-sk.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sl-sl.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sl-sl.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sl-sl.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sq-al.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sq-al.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sq-al.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sr-rs.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sr-rs.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sr-rs.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sv-se.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sv-se.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/sv-se.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/th-th.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/th-th.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/th-th.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/tr-tr.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/tr-tr.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/tr-tr.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/uk-ua.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/uk-ua.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/uk-ua.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/vi-vn.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/vi-vn.lua new file mode 100644 index 0000000..dba36f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/vi-vn.lua @@ -0,0 +1,14 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-cjv.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-cjv.lua new file mode 100644 index 0000000..9afc684 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-cjv.lua @@ -0,0 +1,154 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[配饰]], + ["Common.AssetTypes.Label.Hat"] = [[帽子]], + ["Common.AssetTypes.Label.Hair"] = [[发型]], + ["Common.AssetTypes.Label.Face"] = [[表情]], + ["Common.AssetTypes.Label.Neck"] = [[颈部]], + ["Common.AssetTypes.Label.Shoulder"] = [[肩部]], + ["Common.AssetTypes.Label.Front"] = [[正面]], + ["Common.AssetTypes.Label.Back"] = [[背面]], + ["Common.AssetTypes.Label.Waist"] = [[腰部]], + ["Common.AssetTypes.Label.Animations"] = [[动画]], + ["Common.AssetTypes.Label.Audio"] = [[音频]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[虚拟形象动画]], + ["Common.AssetTypes.Label.Badges"] = [[徽章]], + ["Common.AssetTypes.Label.Decals"] = [[贴花]], + ["Common.AssetTypes.Label.Faces"] = [[表情]], + ["Common.AssetTypes.Label.GamePasses"] = [[游戏通行证]], + ["Common.AssetTypes.Label.Gear"] = [[装备]], + ["Common.AssetTypes.Label.Heads"] = [[头部]], + ["Common.AssetTypes.Label.Meshes"] = [[网格]], + ["Common.AssetTypes.Label.Models"] = [[模型]], + ["Common.AssetTypes.Label.Packages"] = [[套装]], + ["Common.AssetTypes.Label.Pants"] = [[裤子]], + ["Common.AssetTypes.Label.Places"] = [[场景]], + ["Common.AssetTypes.Label.Plugins"] = [[插件]], + ["Common.AssetTypes.Label.Shirts"] = [[衬衫]], + ["Common.AssetTypes.Label.TShirts"] = [[T 恤]], + ["Common.AssetTypes.Label.VipServers"] = [[VIP 服务器]], + ["Common.AssetTypes.Label.Run"] = [[奔跑]], + ["Common.AssetTypes.Label.Walk"] = [[步行]], + ["Common.AssetTypes.Label.Fall"] = [[下落]], + ["Common.AssetTypes.Label.Jump"] = [[跳跃]], + ["Common.AssetTypes.Label.Idle"] = [[闲置]], + ["Common.AssetTypes.Label.Swim"] = [[游泳]], + ["Common.AssetTypes.Label.Climb"] = [[攀爬]], + ["Common.AssetTypes.Label.Hats"] = [[帽子]], + ["Common.AssetTypes.Label.Shoulders"] = [[肩部]], + ["Common.AssetTypes.Label.Death"] = [[死亡]], + ["Common.AssetTypes.Label.Pose"] = [[姿势]], + ["Common.AssetTypes.Label.Head"] = [[头部]], + ["Common.AssetTypes.Label.TShirt"] = [[T 恤]], + ["Common.AssetTypes.Label.Shirt"] = [[衬衫]], + ["Common.AssetTypes.Label.Decal"] = [[贴花]], + ["Common.AssetTypes.Label.Model"] = [[模型]], + ["Common.AssetTypes.Label.Plugin"] = [[插件]], + ["Common.AssetTypes.Label.MeshPart"] = [[网格组件]], + ["Common.AssetTypes.Label.GamePass"] = [[游戏通行证]], + ["Common.AssetTypes.Label.Badge"] = [[徽章]], + ["Common.AssetTypes.Label.Package"] = [[套装]], + ["Common.AssetTypes.Label.Place"] = [[场景]], + ["Common.AssetTypes.Label.LeftArm"] = [[左臂]], + ["Common.AssetTypes.Label.LeftLeg"] = [[左腿]], + ["Common.AssetTypes.Label.RightArm"] = [[右臂]], + ["Common.AssetTypes.Label.RightLeg"] = [[右腿]], + ["Common.AssetTypes.Label.Torso"] = [[躯干]], + ["Common.AssetTypes.Label.Animation"] = [[动画]], + ["Common.AssetTypes.Label.Emote"] = [[动作]], + ["Common.BuildersClub.Label.PlanFree"] = [[免费]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Classic]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Builders Club 会员资格]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Turbo Builders Club 会员资格]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Outrageous Builders Club 会员资格]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[是]], + ["Common.BuildersClub.Label.No"] = [[否]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[从不]], + ["Common.BuildersClub.Label.Robux"] = [[罗宝]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[终身]], + ["Common.BuildersClub.Label.Membership"] = [[会员资格]], + ["CoreScripts.InGameMenu.EducationalPopup.MenuIconTooltip"] = [[]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[需要 Premium 会员]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Roblox Premium 将给你:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[每月 450 Robux]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Premium 会员限定福利]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[购买 Robux 时额外增送 10%]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price} / 月]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[你的平台不支持购买 Roblox Premium。请使用电脑购买 Premium 会员。]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[开发者似乎想给你发送购买 Roblox Premium 的提示,但你已经是 Premium 会员了!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[当前无法购买 Premium,请稍后重试。]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[每月 {robux} Robux]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[购买未完成,请重试。]], + ["CoreScripts.PremiumModal.Title.Error"] = [[错误]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[免费领取]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[升级]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[立即购买]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[购买 R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[取消]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[好]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[正在购买]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[此次交易不会影响你的帐户余额。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[此为测试性购买,你的帐户不会被收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[此为测试性购买。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[此物品需要 {BC_LEVEL}。请点按“升级”来升级你的 Builders Club!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[你在此次交易后的余额将为 R${BALANCE_FUTURE}]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[你的当前余额为 {BALANCE_NOW}。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[剩余的 {REMAINING_ROBUX} 罗宝将会加进你的余额中。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[由于你帐户的罗宝余额不足,购买失败。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[你的订阅等级不足,无法购买此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[你的罗宝不足,无法购买此物品。请离开此游戏,然后前往罗宝屏幕购买更多罗宝。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[你的罗宝不足,无法购买此物品。请购买更多罗宝。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[挑战内购买暂时停用,购买失败。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[购买过程中发生错误,购买失败。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[购买过程中发生错误,购买“{ITEM_NAME}”失败。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[当前无法取回你的余额信息。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[当前无法读取此物品价格。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[此限量物品已售完。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[此物品当前为非卖品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[若要购买此物品,请先创建 Roblox 帐户,如需更多信息,请访问 www.roblox.com。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[此场景已停用第三方物品贩售。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[因为你的账号为 13 岁以下,因此不允许购买此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[你已拥有此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[是否要免费领取“{ITEM_NAME}”?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[确定购买{ASSET_TYPE}“{ITEM_NAME}”?价格为]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[“{ITEM_NAME}”购买成功!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[你还需要 {NEEDED_AMOUNT} 罗宝才能购买“{ASSET_TYPE}{ITEM_NAME}”。是否要购买更多罗宝?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[产品]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[套装]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[是]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[正在取消]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[你没有订阅此项目,你的订阅状态并未改变。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[您已取消“{ITEM_NAME}”的订阅。订阅期结束前,你还将继续享有订阅福利。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[确定要取消你的“{ITEM_NAME}”订阅?订阅期结束前,你仍将继续享有订阅福利。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[确定订阅“{ITEM_NAME}”?价格为]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[订阅]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[每月 {PRICE}]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[订阅]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[你必须成为高级会员才能订阅!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[升级]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[你需要成为Premium 会员才能订阅,因此你无法购买此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[购买罗宝]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[此次交易后,你的余额将为 {BALANCE_FUTURE} 罗宝]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[你的罗宝不足,无法购买此物品。 ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[仅限高级会员]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[若要购买此物品,请先成为罗布乐思高级会员。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[只有成人可以在罗布乐思上进行购买。我同意我已满 18 岁,且为当前账户的所有者、所有者父母或所有者的法定监护人。我将对此项购买进行授权,并同意使用条款。]], +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-cn.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-cn.lua new file mode 100644 index 0000000..d949cd2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-cn.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[配饰]], + ["Common.AssetTypes.Label.Hat"] = [[帽子]], + ["Common.AssetTypes.Label.Hair"] = [[发型]], + ["Common.AssetTypes.Label.Face"] = [[表情]], + ["Common.AssetTypes.Label.Neck"] = [[颈部]], + ["Common.AssetTypes.Label.Shoulder"] = [[肩部]], + ["Common.AssetTypes.Label.Front"] = [[正面]], + ["Common.AssetTypes.Label.Back"] = [[背面]], + ["Common.AssetTypes.Label.Waist"] = [[腰部]], + ["Common.AssetTypes.Label.Animations"] = [[动画]], + ["Common.AssetTypes.Label.Audio"] = [[音频]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[虚拟形象动画]], + ["Common.AssetTypes.Label.Badges"] = [[徽章]], + ["Common.AssetTypes.Label.Decals"] = [[贴花]], + ["Common.AssetTypes.Label.Faces"] = [[表情]], + ["Common.AssetTypes.Label.GamePasses"] = [[游戏通行证]], + ["Common.AssetTypes.Label.Gear"] = [[装备]], + ["Common.AssetTypes.Label.Heads"] = [[头部]], + ["Common.AssetTypes.Label.Meshes"] = [[网格]], + ["Common.AssetTypes.Label.Models"] = [[模型]], + ["Common.AssetTypes.Label.Packages"] = [[套装]], + ["Common.AssetTypes.Label.Pants"] = [[裤子]], + ["Common.AssetTypes.Label.Places"] = [[场景]], + ["Common.AssetTypes.Label.Plugins"] = [[插件]], + ["Common.AssetTypes.Label.Shirts"] = [[衬衫]], + ["Common.AssetTypes.Label.TShirts"] = [[T 恤]], + ["Common.AssetTypes.Label.VipServers"] = [[VIP 服务器]], + ["Common.AssetTypes.Label.Run"] = [[奔跑]], + ["Common.AssetTypes.Label.Walk"] = [[步行]], + ["Common.AssetTypes.Label.Fall"] = [[下落]], + ["Common.AssetTypes.Label.Jump"] = [[跳跃]], + ["Common.AssetTypes.Label.Idle"] = [[闲置]], + ["Common.AssetTypes.Label.Swim"] = [[游泳]], + ["Common.AssetTypes.Label.Climb"] = [[攀爬]], + ["Common.AssetTypes.Label.Hats"] = [[帽子]], + ["Common.AssetTypes.Label.Shoulders"] = [[肩部]], + ["Common.AssetTypes.Label.Death"] = [[死亡]], + ["Common.AssetTypes.Label.Pose"] = [[姿势]], + ["Common.AssetTypes.Label.Head"] = [[头部]], + ["Common.AssetTypes.Label.TShirt"] = [[T 恤]], + ["Common.AssetTypes.Label.Shirt"] = [[衬衫]], + ["Common.AssetTypes.Label.Decal"] = [[贴花]], + ["Common.AssetTypes.Label.Model"] = [[模型]], + ["Common.AssetTypes.Label.Plugin"] = [[插件]], + ["Common.AssetTypes.Label.MeshPart"] = [[网格组件]], + ["Common.AssetTypes.Label.GamePass"] = [[游戏通行证]], + ["Common.AssetTypes.Label.Badge"] = [[徽章]], + ["Common.AssetTypes.Label.Package"] = [[套装]], + ["Common.AssetTypes.Label.Place"] = [[场景]], + ["Common.AssetTypes.Label.LeftArm"] = [[左臂]], + ["Common.AssetTypes.Label.LeftLeg"] = [[左腿]], + ["Common.AssetTypes.Label.RightArm"] = [[右臂]], + ["Common.AssetTypes.Label.RightLeg"] = [[右腿]], + ["Common.AssetTypes.Label.Torso"] = [[躯干]], + ["Common.AssetTypes.Label.Animation"] = [[动画]], + ["Common.AssetTypes.Label.Emote"] = [[动作]], + ["Common.BuildersClub.Label.PlanFree"] = [[免费]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Classic]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Builders Club 会员资格]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Turbo Builders Club 会员资格]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Outrageous Builders Club 会员资格]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[是]], + ["Common.BuildersClub.Label.No"] = [[否]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[从不]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[终身]], + ["Common.BuildersClub.Label.Membership"] = [[会员资格]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[需要 Premium 会员]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Roblox Premium 将给你:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[每月 450 Robux]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[Premium 会员限定福利]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[购买 Robux 时额外增送 10%]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price} / 月]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[你的平台不支持购买 Roblox Premium。请使用电脑购买 Premium 会员。]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[开发者似乎想给你发送购买 Roblox Premium 的提示,但你已经是 Premium 会员了!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[当前无法购买 Premium,请稍后重试。]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[每月 {robux} Robux]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[购买未完成,请重试。]], + ["CoreScripts.PremiumModal.Title.Error"] = [[错误]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[免费领取]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[升级]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[立即购买]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[购买 R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[取消]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[好]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[正在购买]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[此次交易不会影响你的帐户余额。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[此为测试性购买,你的帐户不会被收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[此为测试性购买。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[此物品需要 {BC_LEVEL}。请点按“升级”来升级你的 Builders Club!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[你在此次交易后的余额将为 R${BALANCE_FUTURE}]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[你的当前余额为 {BALANCE_NOW}。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[剩余的 {REMAINING_ROBUX} Robux 将会加进你的余额中。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[由于你帐户的 Robux 余额不足,购买失败。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[你的订阅等级不足,无法购买此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[你的 Robux 不足,无法购买此物品。请离开此游戏,然后前往 Robux 屏幕购买更多 Robux。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[你的 Robux 不足,无法购买此物品。请购买更多 Robux。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[游戏内购买暂时停用,购买失败。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[购买过程中发生错误,购买失败。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[购买过程中发生错误,购买“{ITEM_NAME}”失败。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[当前无法取回你的余额信息。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[当前无法读取此物品价格。系统并未向你的帐户收取费用,请稍后重试。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[此限量物品已售完。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[此物品当前为非卖品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[若要购买此物品,请先创建 Roblox 帐户,如需更多信息,请访问 www.roblox.com。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[此场景已停用第三方物品贩售。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[因为你的账号为 13 岁以下,因此不允许购买此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[你已拥有此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[是否要免费领取“{ITEM_NAME}”?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[确定购买{ASSET_TYPE}“{ITEM_NAME}”?价格为]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[“{ITEM_NAME}”购买成功!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[你还需要 {NEEDED_AMOUNT} Robux 才能购买“{ASSET_TYPE}{ITEM_NAME}”。是否要购买更多 Robux?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[产品]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[套装]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[是]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[正在取消]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[你没有订阅此项目,你的订阅状态并未改变。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[您已取消“{ITEM_NAME}”的订阅。订阅期结束前,你还将继续享有订阅福利。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[确定要取消你的“{ITEM_NAME}”订阅?订阅期结束前,你仍将继续享有订阅福利。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[确定订阅“{ITEM_NAME}”?价格为]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[订阅]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[每月 {PRICE}]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[订阅]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[你必须成为 Premium 会员才能订阅!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[升级]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[你需要成为Premium 会员才能订阅,因此你无法购买此物品。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[购买 Robux]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[此次交易后,你的余额将为 {BALANCE_FUTURE} Robux]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[你的 Robux 不足,无法购买此物品。 ]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Premium 会员限定]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[该物品仅限 Roblox Premium 会员才能购买。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[只有成年用户才能在 Roblox 上进行购买。购买者声明本人已满 18 岁,且为当前账户的所有者、所有者父母或所有者的法定监护人。购买者将对此购买进行授权,并同意 Roblox 使用条款。]], +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-tw.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-tw.lua new file mode 100644 index 0000000..544181e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/Locales/zh-tw.lua @@ -0,0 +1,153 @@ +--[[---------------------------------------------------------------------------------------------------- + + This file was generated by: ClientIntegration/Tools/LuaStringsGenerator/GenerateAllLocales.py + + Changes to this file should always follow: + Building an Internationalized Feature - Engineer's Guide: + https://confluence.roblox.com/display/IN/Building+an+Internationalized+Feature+-+Engineer%27s+Guide + Sync up with newly-updated translations: + https://confluence.roblox.com/display/MOBAPP/Sync+up+with+newly-updated+translations + +--------------------------------------------------------------------------------------------------------]] + +return{ + ["Common.AssetTypes.Label.Accessories"] = [[飾品]], + ["Common.AssetTypes.Label.Hat"] = [[帽子]], + ["Common.AssetTypes.Label.Hair"] = [[髮型]], + ["Common.AssetTypes.Label.Face"] = [[臉部]], + ["Common.AssetTypes.Label.Neck"] = [[頸部]], + ["Common.AssetTypes.Label.Shoulder"] = [[肩膀]], + ["Common.AssetTypes.Label.Front"] = [[正面]], + ["Common.AssetTypes.Label.Back"] = [[背面]], + ["Common.AssetTypes.Label.Waist"] = [[腰部]], + ["Common.AssetTypes.Label.Animations"] = [[動畫]], + ["Common.AssetTypes.Label.Audio"] = [[音訊]], + ["Common.AssetTypes.Label.AvatarAnimations"] = [[虛擬人偶動畫]], + ["Common.AssetTypes.Label.Badges"] = [[徽章]], + ["Common.AssetTypes.Label.Decals"] = [[貼花]], + ["Common.AssetTypes.Label.Faces"] = [[臉部]], + ["Common.AssetTypes.Label.GamePasses"] = [[遊戲證]], + ["Common.AssetTypes.Label.Gear"] = [[裝備]], + ["Common.AssetTypes.Label.Heads"] = [[頭部]], + ["Common.AssetTypes.Label.Meshes"] = [[網格]], + ["Common.AssetTypes.Label.Models"] = [[模型]], + ["Common.AssetTypes.Label.Packages"] = [[套裝]], + ["Common.AssetTypes.Label.Pants"] = [[褲子]], + ["Common.AssetTypes.Label.Places"] = [[地點]], + ["Common.AssetTypes.Label.Plugins"] = [[外掛程式]], + ["Common.AssetTypes.Label.Shirts"] = [[襯衫]], + ["Common.AssetTypes.Label.TShirts"] = [[T 恤]], + ["Common.AssetTypes.Label.VipServers"] = [[VIP 伺服器]], + ["Common.AssetTypes.Label.Run"] = [[奔跑]], + ["Common.AssetTypes.Label.Walk"] = [[步行]], + ["Common.AssetTypes.Label.Fall"] = [[跌落]], + ["Common.AssetTypes.Label.Jump"] = [[跳躍]], + ["Common.AssetTypes.Label.Idle"] = [[閒置]], + ["Common.AssetTypes.Label.Swim"] = [[游泳]], + ["Common.AssetTypes.Label.Climb"] = [[攀爬]], + ["Common.AssetTypes.Label.Hats"] = [[帽子]], + ["Common.AssetTypes.Label.Shoulders"] = [[肩膀]], + ["Common.AssetTypes.Label.Death"] = [[死亡]], + ["Common.AssetTypes.Label.Pose"] = [[姿勢]], + ["Common.AssetTypes.Label.Head"] = [[頭部]], + ["Common.AssetTypes.Label.TShirt"] = [[T 恤]], + ["Common.AssetTypes.Label.Shirt"] = [[襯衫]], + ["Common.AssetTypes.Label.Decal"] = [[貼花]], + ["Common.AssetTypes.Label.Model"] = [[模型]], + ["Common.AssetTypes.Label.Plugin"] = [[外掛程式]], + ["Common.AssetTypes.Label.MeshPart"] = [[網格零件]], + ["Common.AssetTypes.Label.GamePass"] = [[遊戲證]], + ["Common.AssetTypes.Label.Badge"] = [[徽章]], + ["Common.AssetTypes.Label.Package"] = [[套裝]], + ["Common.AssetTypes.Label.Place"] = [[地點]], + ["Common.AssetTypes.Label.LeftArm"] = [[左臂]], + ["Common.AssetTypes.Label.LeftLeg"] = [[左腿]], + ["Common.AssetTypes.Label.RightArm"] = [[右臂]], + ["Common.AssetTypes.Label.RightLeg"] = [[右腿]], + ["Common.AssetTypes.Label.Torso"] = [[軀幹]], + ["Common.AssetTypes.Label.Animation"] = [[動畫]], + ["Common.AssetTypes.Label.Emote"] = [[動作]], + ["Common.BuildersClub.Label.PlanFree"] = [[免費]], + ["Common.BuildersClub.Label.PlanClassic"] = [[Classic]], + ["Common.BuildersClub.Label.PlanTurbo"] = [[Turbo]], + ["Common.BuildersClub.Label.PlanOutrageous"] = [[Outrageous]], + ["Common.BuildersClub.Label.BuildersClub"] = [[Builders Club]], + ["Common.BuildersClub.Label.BuildersClubMembership"] = [[Builders Club 會員資格]], + ["Common.BuildersClub.Label.BuildersClubMembershipTurbo"] = [[Turbo Builders Club 會員資格]], + ["Common.BuildersClub.Label.BuildersClubMembershipOutrageous"] = [[Outrageous Builders Club 會員資格]], + ["Common.BuildersClub.Label.TurboBuildersClub"] = [[Turbo Builders Club]], + ["Common.BuildersClub.Label.OutrageousBuildersClub"] = [[Outrageous Builders Club]], + ["Common.BuildersClub.Label.Yes"] = [[是]], + ["Common.BuildersClub.Label.No"] = [[否]], + ["Common.BuildersClub.Label.NeverUppercase"] = [[永不]], + ["Common.BuildersClub.Label.Robux"] = [[Robux]], + ["Common.BuildersClub.Label.ClassicBuildersClub"] = [[Classic Builders Club]], + ["Common.BuildersClub.Label.Lifetime"] = [[Lifetime]], + ["Common.BuildersClub.Label.Membership"] = [[會員資格]], + ["CoreScripts.PremiumModal.Title.PremiumRequired"] = [[需要 Premium]], + ["CoreScripts.PremiumModal.Body.Description"] = [[Roblox Premium 將會給您:]], + ["CoreScripts.PremiumModal.Body.RobuxMonthly"] = [[每個月 450 Robux]], + ["CoreScripts.PremiumModal.Body.PremiumOnlyAreas"] = [[獲得 Premium 限定福利]], + ["CoreScripts.PremiumModal.Body.RobuxDiscount"] = [[購買 Robux 時獲得 10% 額外 Robux]], + ["CoreScripts.PremiumModal.Action.PricePerMonth"] = [[{price} / 月]], + ["CoreScripts.PremiumModal.Error.PlatformUnavailable"] = [[您的平台不支援購買 Roblox Premium,請使用電腦購買 Premium。]], + ["CoreScripts.PremiumModal.Error.AlreadyPremium"] = [[開發人員似乎想給您購買 Roblox Premium 的提示,但您已經是 Premium 會員了!]], + ["CoreScripts.PremiumModal.Error.Unavailable"] = [[目前無法購買 Premium,請稍後再試!]], + ["CoreScripts.PremiumModal.Body.RobuxMonthlyV2"] = [[每月 {robux} Robux]], + ["CoreScripts.PremiumModal.Error.FailedNativePurchase"] = [[購買未完成,請重新嘗試。]], + ["CoreScripts.PremiumModal.Title.Error"] = [[錯誤]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.TakeFree"] = [[免費領取]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.UpgradeBuildersClub"] = [[升級]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyNow"] = [[現在購買]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[購買 R$]], + ["CoreScripts.PurchasePrompt.CancelPurchase.Cancel"] = [[取消]], + ["CoreScripts.PurchasePrompt.Button.OK"] = [[確定]], + ["CoreScripts.PurchasePrompt.Purchasing"] = [[正在購買]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceUnaffected"] = [[您的帳號餘額不受此交易影響。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchase"] = [[此為測試性購買,您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.MockPurchaseComplete"] = [[此為測試性購買。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidBuildersClub"] = [[此道具需要 {BC_LEVEL}。按下「升級」來升級您的 Builders Club!]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[您在此交易後的餘額將為 R${BALANCE_FUTURE}]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceNow"] = [[您目前的餘額為 {BALANCE_NOW}。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.RemainingAfterUpsell"] = [[剩餘的 {REMAINING_ROBUX} Robux 將會加入您的餘額。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[您的 Robux 不足,無法購買。您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.BuildersClubUpsellFailure"] = [[您的訂閱等級不足,無法購買此道具。您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxXbox"] = [[此道具的 Robux 價格超過您可以購買的 Robux 金額,請離開遊戲並前往 www.roblox.com 購買更多 Robux。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[此道具的 Robux 價格超過您可以購買的 Robux 金額,請前往 www.roblox.com 購買更多 Robux。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PurchaseDisabled"] = [[遊戲中購買暫時停用,無法購買。您的帳號餘額維持不變,請稍後再試。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailureNoItemName"] = [[購買過程發生錯誤,無法購買。您的帳號餘額維持不變,請稍後再試。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.UnknownFailure"] = [[購買過程發生錯誤,無法購買 {ITEM_NAME}。您的帳號餘額維持不變,請稍後再試。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetBalance"] = [[目前無法取得您的餘額。您的帳號餘額維持不變,請稍後再試。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.CannotGetItemPrice"] = [[目前無法取得道具價格。您的帳號餘額維持不變,請稍後再試。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Limited"] = [[此限量道具已售完,請前往 www.roblox.com 向其他使用者購買。您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotForSale"] = [[此道具目前為非賣品。您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PromptPurchaseOnGuest"] = [[若要購買道具,請前往 www.roblox.com 建立 Roblox 帳號。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.ThirdPartyDisabled"] = [[此地點的第三方買賣目前停用,您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.Under13"] = [[您的帳號為 13 歲以下,不允許購買此道具。您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.AlreadyOwn"] = [[您已擁有此道具,您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Free"] = [[您要免費領取 {ITEM_NAME} 嗎?]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Purchase"] = [[購買{ASSET_TYPE} {ITEM_NAME}?價格為]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Succeeded"] = [[成功購買 {ITEM_NAME}!]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.NeedMoreRobux"] = [[您還需要 {NEEDED_AMOUNT} Robux 才能購買 {ASSET_TYPE} {ITEM_NAME}。您要加購 Robux 嗎?]], + ["CoreScripts.PurchasePrompt.ProductType.Product"] = [[商品]], + ["CoreScripts.PurchasePrompt.ItemType.Bundle"] = [[組合包]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.CancelSubscription"] = [[是]], + ["CoreScripts.PurchasePrompt.Canceling"] = [[正在取消]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotCurrentlySubscribed"] = [[您沒有訂閱此項目,您的訂閱狀態沒有改變。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.CancellationSucceeded"] = [[您已取消 {ITEM_NAME} 訂閱。訂閱期間結束前,您還會繼續享有訂閱福利。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Cancellation"] = [[確定取消 {ITEM_NAME} 訂閱?訂閱期間結束前,您還會繼續享有訂閱福利。]], + ["CoreScripts.PurchasePrompt.PurchaseMessage.Subscribe"] = [[確定訂閱 {ITEM_NAME}?價格為]], + ["CoreScripts.PurchasePrompt.ProductType.Subscription"] = [[訂閱]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Monthly"] = [[每月 {PRICE}]], + ["CoreScripts.PurchasePrompt.PurchaseInterval.Once"] = [[{PRICE}]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.Subscribe"] = [[訂閱]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.InvalidPremium"] = [[您必須是 Premium 會員才可以訂閱!]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.SubscribePremium"] = [[升級]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumUpsellFailure"] = [[您不是 Premium 會員,無法購買此道具。您的帳號餘額維持不變。]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobuxV2"] = [[購買 Robux]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFutureV2"] = [[您在此交易後的餘額將為 {BALANCE_FUTURE} Robux]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobuxNoUpsell"] = [[您的 Robux 不足,無法購買此道具。]], + ["CoreScripts.PurchasePrompt.Button.PremiumOnly"] = [[Premium 限定]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.PremiumOnly"] = [[若要購買此道具,請訂閱 Roblox Premium。]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.AgeLegalText"] = [[此購買將會涉及到真實金錢交易。我同意我滿 18 歲,並是帳號主人的家長或法定監護人。我批准此筆購買並同意服務條款。]], +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/LocalizationService.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/LocalizationService.lua new file mode 100644 index 0000000..d960c10 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/LocalizationService.lua @@ -0,0 +1,207 @@ +local Root = script.Parent.Parent + +local ItemType = require(Root.Enums.ItemType) +local PurchaseError = require(Root.Enums.PurchaseError) +local Symbol = require(Root.Symbols.Symbol) + +local KeyMappings = require(script.Parent.KeyMappings) + +local FFlagChinaLicensingApp = settings():GetFFlag("ChinaLicensingApp") +local HARDCODED_CLB_TRANSLATIONS = { + ["CoreScripts.PurchasePrompt.PurchaseFailed.InvalidFunds"] = [[由于你帐户的乐币余额不足,购买失败。系统并未向你的帐户收取费用。]], + ["CoreScripts.PurchasePrompt.PurchaseFailed.NotEnoughRobux"] = [[你的乐币余额不足,无法购买此物品。]], + ["CoreScripts.PurchasePrompt.ConfirmPurchase.BuyRobux"] = [[购买乐币]], + ["CoreScripts.PurchasePrompt.PurchaseDetails.BalanceFuture"] = [[你在此次交易后的余额将为 {BALANCE_FUTURE} 乐币]], +} + +local GetFFlagProductPercentLocFix = require(Root.Flags.GetFFlagProductPercentLocFix) + +local DEBUG_LOCALIZATION = false + +--[[ + Locale-specific group delimiters for displaying numbers. Used to + format values like 100000 to strings like "100,000". This table + does not provide any info regarding decimal separators +]] +local groupDelimiterByLocale = { + ["en-us"] = ",", + ["en-gb"] = ",", + ["es-mx"] = ",", + ["es-es"] = ".", + ["fr-fr"] = " ", + ["de-de"] = " ", + ["pt-br"] = ".", + ["zh-cn"] = ",", + ["zh-cjv"] = ",", + ["zh-tw"] = ",", + ["ko-kr"] = ",", + ["ja-jp"] = ",", + ["it-it"] = " ", + ["ru-ru"] = ".", + ["id-id"] = ".", + ["vi-vn"] = ".", + ["th-th"] = ",", + ["tr-tr"] = ".", +} + +--[[ + This is a marker used to indicate that a provided param needs a locale-aware + formatting pass. We need this for nested localization and number formatting +]] +local FormattedParamTag = Symbol.named("FormattedParam") + +local function isFormattedParam(paramValue) + return typeof(paramValue) == "table" and paramValue[FormattedParamTag] == true +end + +local function createFormattedParam(formatFunc) + return { + [FormattedParamTag] = true, + format = formatFunc, + } +end + +--[[ + Looks up the given key in the localization context's translation table +]] +local function getLocalizedString(localizationContext, key) + local translations = localizationContext.translations + local fallbackTranslations = localizationContext.fallbackTranslations + + if FFlagChinaLicensingApp then + --[[ + We've been instructed by Tencent to replace all references to 'Robux' with a new + word for it; we don't want to do this as translations, since we don't want to affect + other Chinese language users. + + So our approach here is to short-circuit translation with hard-coded strings for the + case where we + a. Are in the China Licensing Build + b. Encounter the specific string keys that were problematic + ]] + if HARDCODED_CLB_TRANSLATIONS[key] ~= nil then + return HARDCODED_CLB_TRANSLATIONS[key] + end + end + + if DEBUG_LOCALIZATION and translations[key] == nil then + warn(("Missing translation for %s in locale %s"):format(key, localizationContext.locale)) + end + + if fallbackTranslations ~= nil and translations[key] == nil then + return fallbackTranslations[key] + end + + return translations[key] +end + +--[[ + Separates digits in a number into groups of three using the given + delimiter and ignoring anything after a decimal point + + This function is not locale-aware, and will not be useful for + formatting numbers in languages that use inconsistent group sizes like + Indian numbering systems and myriad-based Chinese numbering systems +]] +local function addGroupDelimiters(numberStr, delimiter) + local delimiterReplace = string.format("%%1%s%%2", delimiter) + + -- Repeat substitution until there are no more unbroken four-digit sequences + local substitutions + repeat + numberStr, substitutions = string.gsub(numberStr, "^(-?%d+)(%d%d%d)", delimiterReplace) + until substitutions == 0 + + return numberStr +end + +local LocalizationService = {} + +function LocalizationService.formatNumber(localizationContext, number) + local delimiter = groupDelimiterByLocale[localizationContext.locale] or "," + return addGroupDelimiters(number, delimiter) +end + +--[[ + Generates a placeholder for a number param that needs locale-aware formatting +]] +function LocalizationService.numberParam(number) + return createFormattedParam(function(localizationContext) + return LocalizationService.formatNumber(localizationContext, number) + end) +end + +--[[ + Generates a placeholder for a param substitution that needs + its own localization pass +]] +function LocalizationService.nestedKeyParam(key) + return createFormattedParam(function(localizationContext) + return getLocalizedString(localizationContext, key) + end) +end + +--[[ + Utility function returns the localization key for a given item type +]] +function LocalizationService.getKeyFromItemType(itemType) + assert(ItemType.isMember(itemType) or typeof(itemType) == "number" or typeof(itemType) == "string", + "provided item type " ..tostring(itemType) .." must be a number, string, or ItemType enum") + + local localizationKey + if itemType == ItemType.Bundle then + localizationKey = "CoreScripts.PurchasePrompt.ItemType.Bundle" + else + localizationKey = KeyMappings.AssetTypeById[tostring(itemType)] + end + + if DEBUG_LOCALIZATION and localizationKey == nil then + warn("Invalid Asset Type id " .. tostring(itemType)) + end + + return localizationKey +end + +--[[ + Utility function to retrieve relevant localization key for various + types of errors that may be encountered +]] +function LocalizationService.getErrorKey(errorType) + assert(PurchaseError.isMember(errorType), + "provided value " .. tostring(errorType) .. " is not a member of PurchaseError enum") + + return KeyMappings.PurchaseErrorKey[errorType] +end + +--[[ + The primary function of this object + + Retrieves a localized string from the provided context with the + given key and performs parameter substitutions +]] +function LocalizationService.getString(localizationContext, key, params) + assert(localizationContext ~= nil, "Must provide valid localization context") + + local localizedString = getLocalizedString(localizationContext, key) + + if params ~= nil then + for param, value in pairs(params) do + local replacement = value + local paramPlaceholder = ("{%s}"):format(param) + + if isFormattedParam(value) then + replacement = value.format(localizationContext) + end + + if GetFFlagProductPercentLocFix() then + localizedString = string.gsub(localizedString, paramPlaceholder, function() return replacement end) + else + localizedString = string.gsub(localizedString, paramPlaceholder, replacement) + end + end + end + + return localizedString +end + +return LocalizationService diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/getLocalizationContext.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/getLocalizationContext.lua new file mode 100644 index 0000000..25945c6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Localization/getLocalizationContext.lua @@ -0,0 +1,27 @@ +local Locales = script.Parent.Locales + +local FALLBACK_LOCALE = "en-us" + +local function getLocalizationContext(locale) + local primary = Locales:FindFirstChild(locale) + + if primary ~= nil then + return { + locale = locale, + translations = require(primary), + fallbackTranslations = require(Locales:FindFirstChild(FALLBACK_LOCALE)) + } + else + --[[ + If the requested language is not available, fallback to + the default; for now, this will be American English. + ]] + local fallback = Locales:FindFirstChild(FALLBACK_LOCALE) + return { + locale = FALLBACK_LOCALE, + translations = require(fallback), + } + end +end + +return getLocalizationContext \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Misc/Constants.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Misc/Constants.lua new file mode 100644 index 0000000..ac2b6b8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Misc/Constants.lua @@ -0,0 +1,9 @@ +local Root = script.Parent.Parent + +local strict = require(Root.strict) + +return strict({ + ABTests = strict({ + ADULT_CONFIRMATION = "AllUsers.Payments.U13PurchaseCombinedABTest", + }, "Constants.ABTests") +}, "Constants") diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Misc/createSignal.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Misc/createSignal.lua new file mode 100644 index 0000000..029adc1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Misc/createSignal.lua @@ -0,0 +1,75 @@ +--[[ + This is a simple signal implementation that has a dead-simple API. + + local signal = createSignal() + + local disconnect = signal:subscribe(function(foo) + print("Cool foo:", foo) + end) + + signal:fire("something") + + disconnect() +]] + +local function addToMap(map, addKey, addValue) + local new = {} + + for key, value in pairs(map) do + new[key] = value + end + + new[addKey] = addValue + + return new +end + +local function removeFromMap(map, removeKey) + local new = {} + + for key, value in pairs(map) do + if key ~= removeKey then + new[key] = value + end + end + + return new +end + +local function createSignal() + local connections = {} + + local function subscribe(_, callback) + assert(typeof(callback) == "function", "Can only subscribe to signals with a function.") + + local connection = { + callback = callback, + } + + connections = addToMap(connections, callback, connection) + + local function disconnect() + assert(not connection.disconnected, "Listeners can only be disconnected once.") + + connection.disconnected = true + connections = removeFromMap(connections, callback) + end + + return disconnect + end + + local function fire(_, ...) + for callback, connection in pairs(connections) do + if not connection.disconnected then + callback(...) + end + end + end + + return { + subscribe = subscribe, + fire = fire, + } +end + +return createSignal \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Models/PremiumProduct.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Models/PremiumProduct.lua new file mode 100644 index 0000000..202baf8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Models/PremiumProduct.lua @@ -0,0 +1,44 @@ +--[[ + Docs: https://premiumfeatures.roblox.com/docs#!/PremiumFeaturesProducts/get_v1_products + + Provides a model for response from product purchase request. +]] +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local t = require(LuaPackages.t) + +local strict = require(Root.strict) + +local checkJson = t.interface({ + productId = t.number, + mobileProductId = t.string, + robuxAmount = t.number, + isSubscriptionOnly = t.boolean, + premiumFeatureTypeName = t.string, + description = t.string, + price = t.interface({ + amount = t.number, + currency = t.interface({ + currencySymbol = t.string + }) + }) +}) + +return function(jsonData) + local success, error = checkJson(jsonData) + if not success then + return nil + end + + return { + productId = jsonData.productId, + mobileProductId = jsonData.mobileProductId, + robuxAmount = jsonData.robuxAmount, + isSubscriptionOnly = jsonData.isSubscriptionOnly, + premiumFeatureTypeName = jsonData.premiumFeatureTypeName, + description = jsonData.description, + price = jsonData.price.amount, + currencySymbol = jsonData.price.currency.currencySymbol, + } +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/NativeProducts.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/NativeProducts.lua new file mode 100644 index 0000000..7f35ee2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/NativeProducts.lua @@ -0,0 +1,181 @@ +--[[ + New products (Premium): + + Product naming conventions: + All prefixed with "com.roblox.robloxmobile" + iOS: PascalCase product name + Android, Amazon, UWP: all lowercase product name + + Product naming conventions: + iOS: prefixed with "com.roblox.robloxmobile", PascalCase product name + Android, Amazon, UWP: prefixed with "com.roblox.client", all lowercase product name + + CLILUACORE-310: Ideally we would retrieve these values via native platform code, + like we do with Xbox, or from some reasonable endpoint. As it's implemented now, + we need to make client changes in order to introduce new products +]] + +local NativeProducts = { + IOS = { + PremiumSubscribed = { + { + robuxValue = 88, + productId = "com.roblox.robloxmobile.Premium88Subscribed", + }, { + robuxValue = 175, + productId = "com.roblox.robloxmobile.Premium175Subscribed", + }, { + robuxValue = 265, + productId = "com.roblox.robloxmobile.Premium265Subscribed", + }, { + robuxValue = 350, + productId = "com.roblox.robloxmobile.Premium350Subscribed", + }, { + robuxValue = 440, + productId = "com.roblox.robloxmobile.Premium440Subscribed2", + }, { + robuxValue = 880, + productId = "com.roblox.robloxmobile.Premium880Subscribed", + }, { + robuxValue = 1870, + productId = "com.roblox.robloxmobile.Premium1870Subscribed", + }, + }, + PremiumNotSubscribed = { + { + robuxValue = 80, + productId = "com.roblox.robloxmobile.Premium80Robux", + }, { + robuxValue = 160, + productId = "com.roblox.robloxmobile.Premium160Robux", + }, { + robuxValue = 240, + productId = "com.roblox.robloxmobile.Premium240Robux", + }, { + robuxValue = 320, + productId = "com.roblox.robloxmobile.Premium320Robux", + }, { + robuxValue = 400, + productId = "com.roblox.robloxmobile.Premium400Robux", + }, { + robuxValue = 800, + productId = "com.roblox.robloxmobile.Premium800Robux", + }, { + robuxValue = 1700, + productId = "com.roblox.robloxmobile.Premium1700Robux", + }, + }, + }, + Standard = { + PremiumSubscribed = { + { + robuxValue = 88, + productId = "com.roblox.robloxmobile.premium88subscribed", + }, { + robuxValue = 175, + productId = "com.roblox.robloxmobile.premium175subscribed", + }, { + robuxValue = 265, + productId = "com.roblox.robloxmobile.premium265subscribed", + }, { + robuxValue = 350, + productId = "com.roblox.robloxmobile.premium350subscribed", + }, { + robuxValue = 440, + productId = "com.roblox.robloxmobile.premium440subscribed2", + }, { + robuxValue = 880, + productId = "com.roblox.robloxmobile.premium880subscribed", + }, { + robuxValue = 1870, + productId = "com.roblox.robloxmobile.premium1870subscribed", + } + }, + PremiumSubscribedLarger = { + { + robuxValue = 88, + productId = "com.roblox.robloxmobile.premium88subscribed", + }, { + robuxValue = 175, + productId = "com.roblox.robloxmobile.premium175subscribed", + }, { + robuxValue = 265, + productId = "com.roblox.robloxmobile.premium265subscribed", + }, { + robuxValue = 350, + productId = "com.roblox.robloxmobile.premium350subscribed", + }, { + robuxValue = 440, + productId = "com.roblox.robloxmobile.premium440subscribed2", + }, { + robuxValue = 880, + productId = "com.roblox.robloxmobile.premium880subscribed", + }, { + robuxValue = 1870, + productId = "com.roblox.robloxmobile.premium1870subscribed", + }, { + robuxValue = 4950, + productId = "com.roblox.robloxmobile.premium4950subscribed", + }, { + robuxValue = 11000, + productId = "com.roblox.robloxmobile.premium11000subscribed", + }, + }, + PremiumNotSubscribed = { + { + robuxValue = 80, + productId = "com.roblox.robloxmobile.premium80robux", + }, { + robuxValue = 160, + productId = "com.roblox.robloxmobile.premium160robux", + }, { + robuxValue = 240, + productId = "com.roblox.robloxmobile.premium240robux", + }, { + robuxValue = 320, + productId = "com.roblox.robloxmobile.premium320robux", + }, { + robuxValue = 400, + productId = "com.roblox.robloxmobile.premium400robux", + }, { + robuxValue = 800, + productId = "com.roblox.robloxmobile.premium800robux", + }, { + robuxValue = 1700, + productId = "com.roblox.robloxmobile.premium1700robux", + }, + }, + PremiumNotSubscribedLarger = { + { + robuxValue = 80, + productId = "com.roblox.robloxmobile.premium80robux", + }, { + robuxValue = 160, + productId = "com.roblox.robloxmobile.premium160robux", + }, { + robuxValue = 240, + productId = "com.roblox.robloxmobile.premium240robux", + }, { + robuxValue = 320, + productId = "com.roblox.robloxmobile.premium320robux", + }, { + robuxValue = 400, + productId = "com.roblox.robloxmobile.premium400robux", + }, { + robuxValue = 800, + productId = "com.roblox.robloxmobile.premium800robux", + }, { + robuxValue = 1700, + productId = "com.roblox.robloxmobile.premium1700robux", + }, { + robuxValue = 4500, + productId = "com.roblox.robloxmobile.premium4500robux", + }, { + robuxValue = 10000, + productId = "com.roblox.robloxmobile.premium10000robux", + }, + }, + } +} + +return NativeProducts \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/XboxCatalogData.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/XboxCatalogData.lua new file mode 100644 index 0000000..ccb9c1d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/XboxCatalogData.lua @@ -0,0 +1,46 @@ +--[[ + CLILUACORE-311: We need to find a proper way to encapsulate this; + conditionally depending on PlatformService is bad! +]] +local PlatformService = nil +pcall(function() + PlatformService = game:GetService("PlatformService") +end) + +local Promise = require(script.Parent.Parent.Promise) + +local function parseRobuxValue(productInfo) + local rawText = productInfo and productInfo.Name + local noJunk = string.gsub(rawText, ",", "") + noJunk = noJunk and string.match(noJunk, "[0-9]+") or nil + return noJunk and tonumber(noJunk) or 1000 +end + +local XboxCatalogData = {} + +function XboxCatalogData.GetCatalogInfoAsync() + if PlatformService == nil then + error("PlatformService unavailable; are you on XboxOne?") + end + + local promisified = Promise.promisify(function() + return PlatformService:BeginGetCatalogInfo() + end) + + return promisified() + :andThen(function(catalogInfo) + local availableProducts = {} + + for _, productInfo in pairs(catalogInfo) do + local product = { + robuxValue = parseRobuxValue(productInfo), + productId = productInfo.ProductId + } + table.insert(availableProducts, product) + end + + return availableProducts + end) +end + +return XboxCatalogData diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/getUpsellFlow.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/getUpsellFlow.lua new file mode 100644 index 0000000..73a2b97 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/getUpsellFlow.lua @@ -0,0 +1,24 @@ +local Root = script.Parent.Parent + +local UpsellFlow = require(Root.Enums.UpsellFlow) + +local FFlagChinaLicensingApp = settings():GetFFlag("ChinaLicensingApp") +local GetFFlagDisableRobuxUpsell = require(Root.Flags.GetFFlagDisableRobuxUpsell) + +local function getUpsellFlow(platform) + if FFlagChinaLicensingApp or GetFFlagDisableRobuxUpsell() then + return UpsellFlow.Unavailable + end + + if platform == Enum.Platform.Windows or platform == Enum.Platform.OSX or platform == Enum.Platform.Linux then + return UpsellFlow.Web + elseif platform == Enum.Platform.IOS or platform == Enum.Platform.Android or platform == Enum.Platform.UWP then + return UpsellFlow.Mobile + elseif platform == Enum.Platform.XBoxOne then + return UpsellFlow.Xbox + end + + return UpsellFlow.None +end + +return getUpsellFlow diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/selectRobuxProduct.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/selectRobuxProduct.lua new file mode 100644 index 0000000..7c60d03 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/selectRobuxProduct.lua @@ -0,0 +1,52 @@ +local XboxCatalogData = require(script.Parent.XboxCatalogData) +local NativeProducts = require(script.Parent.NativeProducts) + +local Promise = require(script.Parent.Parent.Promise) + +local function sortAscending(a, b) + return a.robuxValue < b.robuxValue +end + +local function selectProduct(neededRobux, availableProducts) + table.sort(availableProducts, sortAscending) + + for _, product in ipairs(availableProducts) do + if product.robuxValue >= neededRobux then + return Promise.resolve(product) + end + end + + return Promise.reject() +end + +local function selectRobuxProduct(platform, neededRobux, userIsSubscribed) + -- Premium is not yet enabled for XBox, so we always use the existing approach + if platform == Enum.Platform.XBoxOne then + return XboxCatalogData.GetCatalogInfoAsync() + :andThen(function(availableProducts) + return selectProduct(neededRobux, availableProducts) + end) + end + + local productOptions + if platform == Enum.Platform.IOS then + productOptions = userIsSubscribed + and NativeProducts.IOS.PremiumSubscribed + or NativeProducts.IOS.PremiumNotSubscribed + else -- This product format is standard for other supported platforms (Android, Amazon, and UWP) + if platform == Enum.Platform.Android then + -- Contains upsell for 4500 and 10000 packages only available on android + productOptions = userIsSubscribed + and NativeProducts.Standard.PremiumSubscribedLarger + or NativeProducts.Standard.PremiumNotSubscribedLarger + else + productOptions = userIsSubscribed + and NativeProducts.Standard.PremiumSubscribed + or NativeProducts.Standard.PremiumNotSubscribed + end + end + + return selectProduct(neededRobux, productOptions) +end + +return selectRobuxProduct \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/selectRobuxProduct.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/selectRobuxProduct.spec.lua new file mode 100644 index 0000000..7e0837a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/NativeUpsell/selectRobuxProduct.spec.lua @@ -0,0 +1,57 @@ +return function() + local selectRobuxProduct = require(script.Parent.selectRobuxProduct) + + describe("premium products", function() + it("should select the appropriate premium product when user IS NOT premium", function() + selectRobuxProduct(Enum.Platform.IOS, 80, false) + :andThen(function(iosProduct) + expect(iosProduct).to.be.ok() + expect(iosProduct.productId).to.equal("com.roblox.robloxmobile.Premium80Robux") + end) + + selectRobuxProduct(Enum.Platform.Android, 80, false) + :andThen(function(androidProduct) + expect(androidProduct).to.be.ok() + expect(androidProduct.productId).to.equal("com.roblox.robloxmobile.premium80robux") + end) + + selectRobuxProduct(Enum.Platform.Android, 9000, false) + :andThen(function(androidProduct) + expect(androidProduct).to.be.ok() + expect(androidProduct.productId).to.equal("com.roblox.robloxmobile.premium10000robux") + end) + + selectRobuxProduct(Enum.Platform.UWP, 80, false) + :andThen(function(uwpProduct) + expect(uwpProduct).to.be.ok() + expect(uwpProduct.productId).to.equal("com.roblox.robloxmobile.premium80robux") + end) + end) + + it("should select the appropriate premium product when user IS premium", function() + selectRobuxProduct(Enum.Platform.IOS, 88, true) + :andThen(function(iosProduct) + expect(iosProduct).to.be.ok() + expect(iosProduct.productId).to.equal("com.roblox.robloxmobile.Premium88Subscribed") + end) + + selectRobuxProduct(Enum.Platform.Android, 88, true) + :andThen(function(androidProduct) + expect(androidProduct).to.be.ok() + expect(androidProduct.productId).to.equal("com.roblox.robloxmobile.premium88subscribed") + end) + + selectRobuxProduct(Enum.Platform.Android, 9000, true) + :andThen(function(androidProduct) + expect(androidProduct).to.be.ok() + expect(androidProduct.productId).to.equal("com.roblox.robloxmobile.premium11000subscribed") + end) + + selectRobuxProduct(Enum.Platform.UWP, 88, true) + :andThen(function(uwpProduct) + expect(uwpProduct).to.be.ok() + expect(uwpProduct.productId).to.equal("com.roblox.robloxmobile.premium88subscribed") + end) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getAccountInfo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getAccountInfo.lua new file mode 100644 index 0000000..823bf8f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getAccountInfo.lua @@ -0,0 +1,40 @@ +local Root = script.Parent.Parent +local UserInputService = game:GetService("UserInputService") + +local PurchaseError = require(Root.Enums.PurchaseError) +local Promise = require(Root.Promise) + +local MAX_ROBUX = 2147483647 + +local function getAccountInfo(network, externalSettings) + if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then + return Promise.all({ + accountInfo = network.getAccountInfo(), + xboxBalance = network.getXboxRobuxBalance(), + }):andThen(function(results) + local accountInfo = results.accountInfo + -- Override balance with platform-specific balance + accountInfo.RobuxBalance = results.xboxBalance.Robux + + return Promise.resolve(accountInfo) + end) + end + + return network.getAccountInfo() + :andThen(function(result) + --[[ + In studio, we falsely report that users have the maximum amount + of robux, so that they can always test the normal purchase flow + ]] + if externalSettings.isStudio() then + result.RobuxBalance = MAX_ROBUX + end + + return Promise.resolve(result) + end) + :catch(function(failure) + return Promise.reject(PurchaseError.UnknownFailure) + end) +end + +return getAccountInfo \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getBundleDetails.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getBundleDetails.lua new file mode 100644 index 0000000..703f3e9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getBundleDetails.lua @@ -0,0 +1,16 @@ +local Root = script.Parent.Parent + +local PurchaseError = require(Root.Enums.PurchaseError) +local Promise = require(Root.Promise) + +local function getBundleDetails(network, bundleId) + return network.getBundleDetails(bundleId) + :andThen(function(result) + return Promise.resolve(result) + end) + :catch(function(failure) + return Promise.reject(PurchaseError.UnknownFailure) + end) +end + +return getBundleDetails \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getIsAlreadyOwned.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getIsAlreadyOwned.lua new file mode 100644 index 0000000..d490666 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getIsAlreadyOwned.lua @@ -0,0 +1,14 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") + +local PurchaseError = require(Root.Enums.PurchaseError) +local Promise = require(Root.Promise) + +local function getIsAlreadyOwned(network, id, infoType) + return network.getPlayerOwns(Players.LocalPlayer, id, infoType) + :catch(function(failure) + return Promise.reject(PurchaseError.UnknownFailure) + end) +end + +return getIsAlreadyOwned \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getPremiumProductInfo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getPremiumProductInfo.lua new file mode 100644 index 0000000..cef4b5a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getPremiumProductInfo.lua @@ -0,0 +1,12 @@ +local Root = script.Parent.Parent +local PurchaseError = require(Root.Enums.PurchaseError) +local Promise = require(Root.Promise) + +local function getPremiumProductInfo(network) + return network.getPremiumProductInfo() + :catch(function(failure) + return Promise.reject(PurchaseError.UnknownFailureNoItemName) + end) +end + +return getPremiumProductInfo \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getPremiumUpsellPrecheck.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getPremiumUpsellPrecheck.lua new file mode 100644 index 0000000..8219305 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getPremiumUpsellPrecheck.lua @@ -0,0 +1,14 @@ +local Root = script.Parent.Parent +local Promise = require(Root.Promise) + +local function getPremiumUpsellPrecheck(network) + return network.getPremiumUpsellPrecheck() + :andThen(function(results) + return Promise.resolve(true) + end) + :catch(function(failure) + return Promise.resolve(false) + end) +end + +return getPremiumUpsellPrecheck \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getProductInfo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getProductInfo.lua new file mode 100644 index 0000000..3eb76c4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getProductInfo.lua @@ -0,0 +1,13 @@ +local Root = script.Parent.Parent + +local PurchaseError = require(Root.Enums.PurchaseError) +local Promise = require(Root.Promise) + +local function getProductInfo(network, id, infoType) + return network.getProductInfo(id, infoType) + :catch(function(failure) + return Promise.reject(PurchaseError.UnknownFailureNoItemName) + end) +end + +return getProductInfo \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getProductPurchasableDetails.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getProductPurchasableDetails.lua new file mode 100644 index 0000000..cb9e713 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getProductPurchasableDetails.lua @@ -0,0 +1,16 @@ +local Root = script.Parent.Parent + +local PurchaseError = require(Root.Enums.PurchaseError) +local Promise = require(Root.Promise) + +local function getProductPurchasableDetails(network, productId) + return network.getProductPurchasableDetails(productId) + :andThen(function(result) + return Promise.resolve(result) + end) + :catch(function(failure) + return Promise.reject(PurchaseError.UnknownFailure) + end) +end + +return getProductPurchasableDetails \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getToolAsset.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getToolAsset.lua new file mode 100644 index 0000000..4c95897 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/getToolAsset.lua @@ -0,0 +1,22 @@ +local function getToolAsset(network, assetId) + return network.loadAssetForEquip(assetId) + :andThen(function(tool) + if tool:IsA("Tool") then + return tool + else + local children = tool:GetChildren() + for _, child in ipairs(children) do + if child:IsA("Tool") then + return child + end + end + end + end) + :catch(function(failure) + -- There isn't really much we can do here with error reporting, + -- since the failure is unrelated to purchasing itself + return nil + end) +end + +return getToolAsset \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/performPurchase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/performPurchase.lua new file mode 100644 index 0000000..8cea07e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/performPurchase.lua @@ -0,0 +1,56 @@ +local Root = script.Parent.Parent + +local PurchaseError = require(Root.Enums.PurchaseError) +local Promise = require(Root.Promise) +local GetFFlagNewEconomyDeveloperProductUrl = require(Root.Flags.GetFFlagNewEconomyDeveloperProductUrl) + +local function performPurchase(network, infoType, productId, expectedPrice, requestId, isRobloxPurchase) + return network.performPurchase(infoType, productId, expectedPrice, requestId, isRobloxPurchase) + :andThen(function(result) + --[[ + User might purchase the product through the web after having + opened the purchase prompt, so an AlreadyOwned status is + acceptable. + ]] + + --[[ + Assets and Gamepasses use the new economy purchasing endpoint. Developer Products still use + the old marketplace/submitpurchase endpoint. + ]] + if infoType == Enum.InfoType.Asset or infoType == Enum.InfoType.GamePass or infoType == Enum.InfoType.Bundle + or (GetFFlagNewEconomyDeveloperProductUrl() and infoType == Enum.InfoType.Product) then + if result.purchased or result.reason == "AlreadyOwned" then + return Promise.resolve(result) + elseif result.reason == "EconomyDisabled" then + return Promise.reject(PurchaseError.PurchaseDisabled) + else + return Promise.reject(PurchaseError.UnknownFailure) + end + elseif infoType == Enum.InfoType.Product then + if result.success or result.status == "AlreadyOwned" then + return Promise.resolve(result) + elseif not result.receipt then + return Promise.reject(PurchaseError.UnknownFailure) + else + if result.status == "EconomyDisabled" then + return Promise.reject(PurchaseError.PurchaseDisabled) + else + return Promise.reject(PurchaseError.UnknownFailure) + end + end + elseif infoType == Enum.InfoType.Subscription then + if result.success or result.reason == "AlreadyOwned" then + return Promise.resolve(result) + elseif result.reason == "EconomyDisabled" then + return Promise.reject(PurchaseError.PurchaseDisabled) + else + return Promise.reject(PurchaseError.UnknownFailure) + end + end + + end, function(failure) + return Promise.reject(PurchaseError.UnknownFailure) + end) +end + +return performPurchase diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/postPremiumImpression.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/postPremiumImpression.lua new file mode 100644 index 0000000..65ddc62 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Network/postPremiumImpression.lua @@ -0,0 +1,5 @@ +local function postPremiumImpression(network) + return network.postPremiumImpression() +end + +return postPremiumImpression \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Promise.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Promise.lua new file mode 100644 index 0000000..9ce6c7e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Promise.lua @@ -0,0 +1,422 @@ +--[[ + An implementation of Promises similar to Promise/A+. +]] + +local PROMISE_DEBUG = false + +--[[ + Packs a number of arguments into a table and returns its length. + Used to cajole varargs without dropping sparse values. +]] +local function pack(...) + local len = select("#", ...) + + return len, { ... } +end + +--[[ + wpcallPacked is a version of xpcall that: + * Returns the length of the result first + * Returns the result packed into a table + * Passes extra arguments through to the passed function, which xpcall does not + * Issues a warning if PROMISE_DEBUG is enabled +]] +local function wpcallPacked(f, ...) + local argsLength, args = pack(...) + + local body = function() + return f(unpack(args, 1, argsLength)) + end + + local resultLength, result = pack(xpcall(body, debug.traceback)) + + -- If promise debugging is on, warn whenever a pcall fails. + -- This is useful for debugging issues within the Promise implementation + -- itself. + if PROMISE_DEBUG and not result[1] then + warn(result[2]) + end + + return resultLength, result +end + +--[[ + Creates a function that invokes a callback with correct error handling and + resolution mechanisms. +]] +local function createAdvancer(callback, resolve, reject) + return function(...) + local resultLength, result = wpcallPacked(callback, ...) + local ok = result[1] + + if ok then + resolve(unpack(result, 2, resultLength)) + else + reject(unpack(result, 2, resultLength)) + end + end +end + +local function isEmpty(t) + return next(t) == nil +end + +local Promise = {} +Promise.__index = Promise + +Promise.Status = { + Started = "Started", + Resolved = "Resolved", + Rejected = "Rejected", +} + +--[[ + Constructs a new Promise with the given initializing callback. + This is generally only called when directly wrapping a non-promise API into + a promise-based version. + The callback will receive 'resolve' and 'reject' methods, used to start + invoking the promise chain. + For example: + local function get(url) + return Promise.new(function(resolve, reject) + spawn(function() + resolve(HttpService:GetAsync(url)) + end) + end) + end + get("https://google.com") + :andThen(function(stuff) + print("Got some stuff!", stuff) + end) +]] +function Promise.new(callback) + local promise = { + -- Used to locate where a promise was created + _source = debug.traceback(), + + -- A tag to identify us as a promise + _type = "Promise", + + _status = Promise.Status.Started, + + -- A table containing a list of all results, whether success or failure. + -- Only valid if _status is set to something besides Started + _values = nil, + + -- Lua doesn't like sparse arrays very much, so we explicitly store the + -- length of _values to handle middle nils. + _valuesLength = -1, + + -- If an error occurs with no observers, this will be set. + _unhandledRejection = false, + + -- Queues representing functions we should invoke when we update! + _queuedResolve = {}, + _queuedReject = {}, + } + + setmetatable(promise, Promise) + + local function resolve(...) + promise:_resolve(...) + end + + local function reject(...) + promise:_reject(...) + end + + local _, result = wpcallPacked(callback, resolve, reject) + local ok = result[1] + local err = result[2] + + if not ok and promise._status == Promise.Status.Started then + reject(err) + end + + return promise +end + +--[[ + Create a promise that represents the immediately resolved value. +]] +function Promise.resolve(value) + return Promise.new(function(resolve) + resolve(value) + end) +end + +--[[ + Create a promise that represents the immediately rejected value. +]] +function Promise.reject(value) + return Promise.new(function(_, reject) + reject(value) + end) +end + +--[[ + Returns a new promise that: + * is resolved when all input promises resolve + * is rejected if ANY input promises reject +]] +function Promise.all(...) + local promises = {...} + + -- check if we've been given a list of promises, not just a variable number of promises + if type(promises[1]) == "table" and promises[1]._type ~= "Promise" then + -- we've been given a table of promises already + promises = promises[1] + end + + return Promise.new(function(resolve, reject) + local isResolved = false + local results = {} + local totalCompleted = 0 + local promiseCount = 0 + + -- If we're agnostic about whether the promises are a table + -- or a list, users can provide tables with useful keys if they like + for _ in pairs(promises) do + promiseCount = promiseCount + 1 + end + + local function promiseCompleted(key, result) + if isResolved then + return + end + + results[key] = result + totalCompleted = totalCompleted + 1 + + if totalCompleted == promiseCount then + resolve(results) + isResolved = true + end + end + + if promiseCount == 0 then + resolve(results) + isResolved = true + return + end + + for key, promise in pairs(promises) do + -- if a promise isn't resolved yet, add listeners for when it does + if promise._status == Promise.Status.Started then + promise:andThen(function(result) + promiseCompleted(key, result) + end):catch(function(reason) + isResolved = true + reject(reason) + end) + + -- if a promise is already resolved, move on + elseif promise._status == Promise.Status.Resolved then + promiseCompleted(key, unpack(promise._values)) + + -- if a promise is rejected, reject the whole chain + else --if promise._status == Promise.Status.Rejected then + -- We catch here to indicate that the intermediate rejection + -- has been handled and seen + promise:catch(function(reason) + isResolved = true + reject(unpack(promise._values)) + end) + end + end + end) +end + +--[[ + Is the given object a Promise instance? +]] +function Promise.is(object) + if type(object) ~= "table" then + return false + end + + return object._type == "Promise" +end + +--[[ + Construct a promise from a yielding function +]] +function Promise.promisify(callback) + return function(...) + local args = {...} + local argLength = select("#", ...) + + return Promise.new(function(resolve, reject) + spawn(function() + local success, result = pcall(callback, unpack(args, 1, argLength)) + + if success then + resolve(result) + else + reject(result) + end + end) + end) + end +end + +function Promise:getStatus() + return self._status +end + +--[[ + Creates a new promise that receives the result of this promise. + The given callbacks are invoked depending on that result. +]] +function Promise:andThen(successHandler, failureHandler) + self._unhandledRejection = false + + -- Create a new promise to follow this part of the chain + return Promise.new(function(resolve, reject) + -- Our default callbacks just pass values onto the next promise. + -- This lets success and failure cascade correctly! + + local successCallback = resolve + if successHandler then + successCallback = createAdvancer(successHandler, resolve, reject) + end + + local failureCallback = reject + if failureHandler then + failureCallback = createAdvancer(failureHandler, resolve, reject) + end + + if self._status == Promise.Status.Started then + -- If we haven't resolved yet, put ourselves into the queue + table.insert(self._queuedResolve, successCallback) + table.insert(self._queuedReject, failureCallback) + elseif self._status == Promise.Status.Resolved then + -- This promise has already resolved! Trigger success immediately. + successCallback(unpack(self._values, 1, self._valuesLength)) + elseif self._status == Promise.Status.Rejected then + -- This promise died a terrible death! Trigger failure immediately. + failureCallback(unpack(self._values, 1, self._valuesLength)) + end + end) +end + +--[[ + Used to catch any errors that may have occurred in the promise. +]] +function Promise:catch(failureCallback) + return self:andThen(nil, failureCallback) +end + +--[[ + Yield until the promise is completed. + This matches the execution model of normal Roblox functions. +]] +function Promise:await() + self._unhandledRejection = false + + if self._status == Promise.Status.Started then + local result + local resultLength + local bindable = Instance.new("BindableEvent") + + self:andThen(function(...) + result = {...} + resultLength = select("#", ...) + bindable:Fire(true) + end, function(...) + result = {...} + resultLength = select("#", ...) + bindable:Fire(false) + end) + + local ok = bindable.Event:Wait() + bindable:Destroy() + + return ok, unpack(result, 1, resultLength) + elseif self._status == Promise.Status.Resolved then + return true, unpack(self._values, 1, self._valuesLength) + elseif self._status == Promise.Status.Rejected then + return false, unpack(self._values, 1, self._valuesLength) + end +end + +function Promise:_resolve(...) + if self._status ~= Promise.Status.Started then + return + end + + local argLength = select("#", ...) + + -- If the resolved value was a Promise, we chain onto it! + if Promise.is((...)) then + -- Without this warning, arguments sometimes mysteriously disappear + if argLength > 1 then + local message = ( + "When returning a Promise from andThen, extra arguments are " .. + "discarded! See:\n\n%s" + ):format( + self._source + ) + warn(message) + end + + (...):andThen(function(...) + self:_resolve(...) + end, function(...) + self:_reject(...) + end) + + return + end + + self._status = Promise.Status.Resolved + self._values = {...} + self._valuesLength = argLength + + -- We assume that these callbacks will not throw errors. + for _, callback in ipairs(self._queuedResolve) do + callback(...) + end +end + +function Promise:_reject(...) + if self._status ~= Promise.Status.Started then + return + end + + self._status = Promise.Status.Rejected + self._values = {...} + self._valuesLength = select("#", ...) + + -- If there are any rejection handlers, call those! + if not isEmpty(self._queuedReject) then + -- We assume that these callbacks will not throw errors. + for _, callback in ipairs(self._queuedReject) do + callback(...) + end + else + -- At this point, no one was able to observe the error. + -- An error handler might still be attached if the error occurred + -- synchronously. We'll wait one tick, and if there are still no + -- observers, then we should put a message in the console. + + self._unhandledRejection = true + local err = tostring((...)) + + spawn(function() + -- Someone observed the error, hooray! + if not self._unhandledRejection then + return + end + + -- Build a reasonable message + local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format( + err, + self._source + ) + warn(message) + end) + end +end + +return Promise \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ABVariationReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ABVariationReducer.lua new file mode 100644 index 0000000..ca09fe3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ABVariationReducer.lua @@ -0,0 +1,19 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) +local Cryo = require(LuaPackages.Cryo) + +local SetABVariation = require(Root.Actions.SetABVariation) + +local ABVariationReducer = Rodux.createReducer({}, { + [SetABVariation.name] = function(state, action) + assert(type(action.key) == "string", "Expected 'key' to be a string") + assert(type(action.variation) == "string", "Expected 'variation' to be a string") + return Cryo.Dictionary.join(state, { + [action.key] = action.variation + }) + end, +}) + +return ABVariationReducer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/AccountInfoReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/AccountInfoReducer.lua new file mode 100644 index 0000000..80a5f44 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/AccountInfoReducer.lua @@ -0,0 +1,19 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local AccountInfoReceived = require(Root.Actions.AccountInfoReceived) + +local ProductInfoReducer = Rodux.createReducer({}, { + [AccountInfoReceived.name] = function(state, action) + local accountInfo = action.accountInfo + + return { + balance = accountInfo.RobuxBalance, + membershipType = accountInfo.MembershipType, + } + end, +}) + +return ProductInfoReducer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/GamepadEnabledReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/GamepadEnabledReducer.lua new file mode 100644 index 0000000..115dc07 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/GamepadEnabledReducer.lua @@ -0,0 +1,17 @@ +local Root = script.Parent.Parent +local UserInputService = game:GetService("UserInputService") + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local SetGamepadEnabled = require(Root.Actions.SetGamepadEnabled) + +local gamepadDefault = UserInputService:GetPlatform() == Enum.Platform.XBoxOne + +local GamepadEnabledReducer = Rodux.createReducer(gamepadDefault, { + [SetGamepadEnabled.name] = function(state, action) + return action.enabled + end, +}) + +return GamepadEnabledReducer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/HasCompletedPurchaseReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/HasCompletedPurchaseReducer.lua new file mode 100644 index 0000000..6f82065 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/HasCompletedPurchaseReducer.lua @@ -0,0 +1,16 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local PurchaseCompleteRecieved = require(Root.Actions.PurchaseCompleteRecieved) +local CompleteRequest = require(Root.Actions.CompleteRequest) + +return Rodux.createReducer(false, { + [PurchaseCompleteRecieved.name] = function(state, action) + return true + end, + [CompleteRequest.name] = function(state, action) + return false + end, +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/NativeUpsellReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/NativeUpsellReducer.lua new file mode 100644 index 0000000..4fcb3b7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/NativeUpsellReducer.lua @@ -0,0 +1,18 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local PromptNativeUpsell = require(Root.Actions.PromptNativeUpsell) + +local NativeUpsellReducer = Rodux.createReducer({}, { + [PromptNativeUpsell.name] = function(state, action) + + return { + robuxProductId = action.robuxProductId, + robuxPurchaseAmount = action.robuxPurchaseAmount, + } + end, +}) + +return NativeUpsellReducer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PremiumProductsReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PremiumProductsReducer.lua new file mode 100644 index 0000000..063283a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PremiumProductsReducer.lua @@ -0,0 +1,18 @@ +local Root = script.Parent.Parent +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local CompleteRequest = require(Root.Actions.CompleteRequest) +local PremiumInfoRecieved = require(Root.Actions.PremiumInfoRecieved) + +local PremiumProductsReducer = Rodux.createReducer({}, { + [PremiumInfoRecieved.name] = function(state, action) + return action.premiumInfo + end, + [CompleteRequest.name] = function(state, action) + -- Clear product info when we hide the prompt + return {} + end, +}) + +return PremiumProductsReducer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ProductInfoReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ProductInfoReducer.lua new file mode 100644 index 0000000..63f1f43 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ProductInfoReducer.lua @@ -0,0 +1,60 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local CompleteRequest = require(Root.Actions.CompleteRequest) +local ProductInfoReceived = require(Root.Actions.ProductInfoReceived) +local BundleProductInfoReceived = require(Root.Actions.BundleProductInfoReceived) +local ItemType = require(Root.Enums.ItemType) +local getPreviewImageUrl = require(Root.getPreviewImageUrl) + +local USER_OUTFIT = "UserOutfit" + +local ProductInfoReducer = Rodux.createReducer({}, { + [ProductInfoReceived.name] = function(state, action) + local productInfo = action.productInfo + + return { + name = productInfo.Name, + price = productInfo.PriceInRobux or 0, + premiumPrice = productInfo.PremiumPriceInRobux, + imageUrl = getPreviewImageUrl(productInfo), + assetTypeId = productInfo.AssetTypeId, + productId = productInfo.ProductId, + membershipTypeRequired = productInfo.MinimumMembershipLevel, + itemType = productInfo.AssetTypeId + } + end, + + [BundleProductInfoReceived.name] = function(state, action) + local bundleProductInfo = action.bundleProductInfo + + -- For now we need the user outfit id to show the image of the bundle. + local costumeId + for _, item in ipairs(bundleProductInfo.items) do + if item.type == USER_OUTFIT then + costumeId = item.id + end + end + bundleProductInfo.costumeId = costumeId + bundleProductInfo.itemType = ItemType.Bundle + + return { + name = bundleProductInfo.name, + price = bundleProductInfo.product.priceInRobux or 0, + imageUrl = getPreviewImageUrl(bundleProductInfo), + assetTypeId = nil, + productId = bundleProductInfo.product.id, + membershipTypeRequired = nil, + itemType = bundleProductInfo.itemType + } + end, + + [CompleteRequest.name] = function(state, action) + -- Clear product info when we hide the prompt + return {} + end, +}) + +return ProductInfoReducer diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ProductReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ProductReducer.lua new file mode 100644 index 0000000..05b9eb9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/ProductReducer.lua @@ -0,0 +1,22 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local SetProduct = require(Root.Actions.SetProduct) +local CompleteRequest = require(Root.Actions.CompleteRequest) + +local ProductReducer = Rodux.createReducer({}, { + [SetProduct.name] = function(state, action) + return { + id = action.id, + infoType = action.infoType, + equipIfPurchased = action.equipIfPurchased, + } + end, + [CompleteRequest.name] = function(state, action) + return {} + end, +}) + +return ProductReducer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PromptRequestReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PromptRequestReducer.lua new file mode 100644 index 0000000..33f2ea7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PromptRequestReducer.lua @@ -0,0 +1,69 @@ +local Root = script.Parent.Parent +local LuaPackages = Root.Parent + +local Rodux = require(LuaPackages.Rodux) + +local RequestAssetPurchase = require(Root.Actions.RequestAssetPurchase) +local RequestBundlePurchase = require(Root.Actions.RequestBundlePurchase) +local RequestGamepassPurchase = require(Root.Actions.RequestGamepassPurchase) +local RequestProductPurchase = require(Root.Actions.RequestProductPurchase) +local RequestPremiumPurchase = require(Root.Actions.RequestPremiumPurchase) +local RequestSubscriptionPurchase = require(Root.Actions.RequestSubscriptionPurchase) +local CompleteRequest = require(Root.Actions.CompleteRequest) +local RequestType = require(Root.Enums.RequestType) + +local EMPTY_STATE = { requestType = RequestType.None } + +local RequestReducer = Rodux.createReducer(EMPTY_STATE, { + [RequestAssetPurchase.name] = function(state, action) + return { + id = action.id, + infoType = Enum.InfoType.Asset, + requestType = RequestType.Asset, + equipIfPurchased = action.equipIfPurchased, + isRobloxPurchase = action.isRobloxPurchase, + } + end, + [RequestGamepassPurchase.name] = function(state, action) + return { + id = action.id, + infoType = Enum.InfoType.GamePass, + requestType = RequestType.GamePass, + isRobloxPurchase = false, + } + end, + [RequestProductPurchase.name] = function(state, action) + return { + id = action.id, + infoType = Enum.InfoType.Product, + requestType = RequestType.Product, + isRobloxPurchase = false, + } + end, + [RequestBundlePurchase.name] = function(state, action) + return { + id = action.id, + infoType = Enum.InfoType.Bundle, + requestType = RequestType.Bundle, + isRobloxPurchase = true, + } + end, + [RequestPremiumPurchase.name] = function(state, action) + return { + requestType = RequestType.Premium, + } + end, + [RequestSubscriptionPurchase.name] = function(state, action) + return { + id = action.id, + infoType = Enum.InfoType.Subscription, + requestType = RequestType.Subscription, + } + end, + [CompleteRequest.name] = function(state, action) + -- Clear product info when we hide the prompt + return EMPTY_STATE + end, +}) + +return RequestReducer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PromptStateReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PromptStateReducer.lua new file mode 100644 index 0000000..08d0ae5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PromptStateReducer.lua @@ -0,0 +1,31 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local SetPromptState = require(Root.Actions.SetPromptState) +local CompleteRequest = require(Root.Actions.CompleteRequest) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local StartPurchase = require(Root.Actions.StartPurchase) +local PromptNativeUpsell = require(Root.Actions.PromptNativeUpsell) +local PromptState = require(Root.Enums.PromptState) + +local PromptStateReducer = Rodux.createReducer(PromptState.None, { + [SetPromptState.name] = function(state, action) + return action.promptState + end, + [CompleteRequest.name] = function(state, action) + return PromptState.None + end, + [ErrorOccurred.name] = function(state, action) + return PromptState.Error + end, + [StartPurchase.name] = function(state, action) + return PromptState.PurchaseInProgress + end, + [PromptNativeUpsell.name] = function(state, action) + return PromptState.RobuxUpsell + end, +}) + +return PromptStateReducer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PurchaseErrorReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PurchaseErrorReducer.lua new file mode 100644 index 0000000..4bd1899 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PurchaseErrorReducer.lua @@ -0,0 +1,16 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local CompleteRequest = require(Root.Actions.CompleteRequest) + +return Rodux.createReducer({}, { + [ErrorOccurred.name] = function(state, action) + return action.purchaseError + end, + [CompleteRequest.name] = function(state, action) + return {} + end, +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PurchasingStartTimeReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PurchasingStartTimeReducer.lua new file mode 100644 index 0000000..6e9cfc3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/PurchasingStartTimeReducer.lua @@ -0,0 +1,15 @@ +local Root = script.Parent.Parent + +local StartPurchase = require(Root.Actions.StartPurchase) + +local function PurchasingStartTimeReducer(state, action) + state = state or -1 + + if action.type == StartPurchase.name then + return action.purchasingStartTime + end + + return state +end + +return PurchasingStartTimeReducer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/Reducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/Reducer.lua new file mode 100644 index 0000000..dfe8487 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/Reducer.lua @@ -0,0 +1,37 @@ +--[[ + The main reducer for the app's store +]] +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local PromptRequestReducer = require(script.Parent.PromptRequestReducer) +local ProductInfoReducer = require(script.Parent.ProductInfoReducer) +local PremiumProductsReducer = require(script.Parent.PremiumProductsReducer) +local NativeUpsellReducer = require(script.Parent.NativeUpsellReducer) +local PromptStateReducer = require(script.Parent.PromptStateReducer) +local PurchaseErrorReducer = require(script.Parent.PurchaseErrorReducer) +local AccountInfoReducer = require(script.Parent.AccountInfoReducer) +local PurchasingStartTimeReducer = require(script.Parent.PurchasingStartTimeReducer) +local HasCompletedPurchaseReducer = require(script.Parent.HasCompletedPurchaseReducer) +local GamepadEnabledReducer = require(script.Parent.GamepadEnabledReducer) +local ABVariationReducer = require(script.Parent.ABVariationReducer) +local WindowStateReducer = require(script.Parent.WindowStateReducer) + +local Reducer = Rodux.combineReducers({ + promptRequest = PromptRequestReducer, + productInfo = ProductInfoReducer, + premiumProductInfo = PremiumProductsReducer, + nativeUpsell = NativeUpsellReducer, + promptState = PromptStateReducer, + purchaseError = PurchaseErrorReducer, + accountInfo = AccountInfoReducer, + purchasingStartTime = PurchasingStartTimeReducer, + hasCompletedPurchase = HasCompletedPurchaseReducer, + gamepadEnabled = GamepadEnabledReducer, + abVariations = ABVariationReducer, + windowState = WindowStateReducer, +}) + +return Reducer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/WindowStateReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/WindowStateReducer.lua new file mode 100644 index 0000000..197cf8b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Reducers/WindowStateReducer.lua @@ -0,0 +1,38 @@ +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Rodux = require(LuaPackages.Rodux) + +local SetPromptState = require(Root.Actions.SetPromptState) +local CompleteRequest = require(Root.Actions.CompleteRequest) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local StartPurchase = require(Root.Actions.StartPurchase) +local PromptNativeUpsell = require(Root.Actions.PromptNativeUpsell) +local SetWindowState = require(Root.Actions.SetWindowState) +local WindowState = require(Root.Enums.WindowState) +local PromptState = require(Root.Enums.PromptState) + +return Rodux.createReducer(WindowState.Hidden, { + [SetPromptState.name] = function(state, action) + if action.promptState == PromptState.None then + return WindowState.Hidden + else + return WindowState.Shown + end + end, + [SetWindowState.name] = function(state, action) + return action.state + end, + [ErrorOccurred.name] = function(state, action) + return WindowState.Shown + end, + [StartPurchase.name] = function(state, action) + return WindowState.Shown + end, + [PromptNativeUpsell.name] = function(state, action) + return WindowState.Shown + end, + [CompleteRequest.name] = function(state, action) + return WindowState.Hidden + end, +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/Analytics.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/Analytics.lua new file mode 100644 index 0000000..100e6b3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/Analytics.lua @@ -0,0 +1,49 @@ +local Players = game:GetService("Players") +local AnalyticsService = game:GetService("RbxAnalyticsService") +local MarketplaceService = game:GetService("MarketplaceService") + +local Analytics = {} + +function Analytics.new() + local service = {} + + setmetatable(service, { + __tostring = function() + return "Service(Analytics)" + end + }) + + function service.reportRobuxUpsellStarted() + return MarketplaceService:ReportRobuxUpsellStarted() + end + + function service.reportNativeUpsellStarted(productId) + AnalyticsService:SendEventImmediately("mobile", "robuxSelected", "mobileUpsell", { + productId = productId, + }) + end + + function service.signalPurchaseSuccess(id, infoType, salePrice, result) + if infoType == Enum.InfoType.Product then + MarketplaceService:SignalClientPurchaseSuccess(result.receipt, Players.LocalPlayer.UserId, id) + else + MarketplaceService:ReportAssetSale(id, salePrice) + end + end + + function service.signalPremiumUpsellShownPremium() + AnalyticsService:SetRBXEvent("client", "InGamePrompt", "PremiumUpsellShownPremium", { gameID = game.GameId }) + end + + function service.signalPremiumUpsellShownNonPremium() + AnalyticsService:SetRBXEvent("client", "InGamePrompt", "PremiumUpsellShownNonPremium", { gameID = game.GameId }) + end + + function service.signalAdultLegalTextShown() + AnalyticsService:SetRBXEvent("client", "InGamePrompt", "AdultLegalTextShown", { gameID = game.GameId }) + end + + return service +end + +return Analytics diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/ExternalSettings.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/ExternalSettings.lua new file mode 100644 index 0000000..6e883c2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/ExternalSettings.lua @@ -0,0 +1,60 @@ +local Root = script.Parent.Parent +local RunService = game:GetService("RunService") +local GuiService = game:GetService("GuiService") +local UserInputService = game:GetService("UserInputService") +local GetFFlagLuaUseThirdPartyPermissions = require(Root.Flags.GetFFlagLuaUseThirdPartyPermissions) +local GetFFlagHideThirdPartyPurchaseFailure = require(Root.Flags.GetFFlagHideThirdPartyPurchaseFailure) + +local ExternalSettings = {} + +function ExternalSettings.new() + local service = {} + + setmetatable(service, { + __tostring = function() + return "Service(ExternalSettings)" + end, + }) + + function service.getPlatform() + return UserInputService:GetPlatform() + end + + function service.isStudio() + return RunService:IsStudio() + end + + function service.isThirdPartyPurchaseAllowed() + -- If PermissionsService is not created (flag is not enabled), don't fail. + local result = true + pcall(function() + result = game:GetService("PermissionsService"):GetIsThirdPartyPurchaseAllowed() + end) + return result + end + + function service.getLuaUseThirdPartyPermissions() + return GetFFlagLuaUseThirdPartyPermissions() + end + + function service.getFlagHideThirdPartyPurchaseFailure() + return GetFFlagHideThirdPartyPurchaseFailure() + end + + -- TODO(DEVTOOLS-4227): Remove this flag + function service.getFlagRestrictSales2() + return settings():GetFFlag("RestrictSales2") + end + + function service.getFlagOrder66() + return settings():GetFFlag("Order66") + end + + function service.isTenFootInterface() + return GuiService:IsTenFootInterface() + end + + return service +end + +return ExternalSettings diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/LayoutValues.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/LayoutValues.lua new file mode 100644 index 0000000..6d571cf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/LayoutValues.lua @@ -0,0 +1,184 @@ +local Root = script.Parent.Parent + +local createSignal = require(Root.Misc.createSignal) +local strict = require(Root.strict) + +local FFlagChinaLicensingApp = settings():GetFFlag("ChinaLicensingApp") + +local function makeImageData(path, sliceCenter) + return { + Path = "rbxasset://textures/" .. path, + SliceCenter = sliceCenter, + } +end + +local LayoutValues = {} +LayoutValues.__tostring = function() + return "Service(LayoutValues)" +end + +function LayoutValues.new(isTenFoot) + local self = {} + setmetatable(self, { + __index = LayoutValues, + }) + + self.signal = createSignal() + self.layout = self:generate(isTenFoot) + + return self +end + +function LayoutValues:update(isTenFoot) + self.layout = self:generate(isTenFoot) + self.signal:fire(self.layout) +end + +function LayoutValues:generate(isTenFoot) + local scaleFactor = isTenFoot and 3 or 1 + + local ButtonHeight = 44 * scaleFactor + local PostTextHeight = 30 * scaleFactor + + local RobuxIconPadding = 6 * scaleFactor + + local RobuxIconWidth = 16 * scaleFactor + local RobuxIconHeight = RobuxIconWidth + + local ProductDescriptionPaddingTop = 18 * scaleFactor + local ProductDescriptionWidth = 210 * scaleFactor + local ProductDescriptionHeight = 106 * scaleFactor + + local PurchasingAnimationWidth = 96 * scaleFactor + local PurchasingAnimationHeight = 20 * scaleFactor + + local HorizontalPadding = 25 * scaleFactor + local ItemPreviewBorder = 2 * scaleFactor + local ItemPreviewWidth = 64 * scaleFactor + local ItemPreviewHeight = 64 * scaleFactor + + local ButtonIconPadding = 3 * scaleFactor + -- Button icons have drop shadow and need a slight offset in order to look centered + local ButtonIconYOffset = 2 * scaleFactor + + local ItemPreviewBackgroundWidth = ItemPreviewWidth + 2 * ItemPreviewBorder + local ItemPreviewBackgroundHeight = ItemPreviewHeight + 2 * ItemPreviewBorder + + local ItemPreviewContainerWidth = ItemPreviewBackgroundWidth + 2 * HorizontalPadding + local ItemPreviewContainerHeight = ItemPreviewBackgroundHeight + 2 * HorizontalPadding + + --[[ + Sizes for UI elements + ]] + local Size = { + AdditionalDetailsLabel = UDim2.new(1, 0, 0, PostTextHeight), + + ItemPreview = UDim2.new(0, ItemPreviewWidth, 0, ItemPreviewHeight), + ItemPreviewWhiteFrame = UDim2.new(0, ItemPreviewBackgroundWidth, 0, ItemPreviewBackgroundHeight), + ItemPreviewContainerFrame = UDim2.new(0, ItemPreviewContainerWidth, 0, ItemPreviewContainerHeight), + + HorizontalPadding = HorizontalPadding, + ProductDescription = UDim2.new(0, ProductDescriptionWidth, 0, ProductDescriptionHeight), + ProductDescriptionPaddingTop = ProductDescriptionPaddingTop, + + RobuxIconContainerFrame = UDim2.new(0, RobuxIconWidth + RobuxIconPadding, 0, RobuxIconHeight + 2 * RobuxIconPadding), + RobuxIcon = UDim2.new(0, RobuxIconWidth, 0, RobuxIconHeight), + PriceTextLabel = UDim2.new( + 0, ProductDescriptionWidth - (RobuxIconWidth + RobuxIconPadding), + 0, RobuxIconHeight + ), + + PurchasingAnimation = UDim2.new(0, PurchasingAnimationWidth, 0, PurchasingAnimationHeight), + + ButtonIconPadding = ButtonIconPadding, + ButtonIconYOffset = ButtonIconYOffset, + ButtonHeight = ButtonHeight, + Dialog = UDim2.new( + 0, ItemPreviewContainerWidth + ProductDescriptionWidth, + 0, math.max(ItemPreviewContainerHeight, ProductDescriptionHeight) + PostTextHeight + ButtonHeight + ), + } + + --[[ + Font sizes for UI elements + ]] + local TextSize = { + Default = 18 * scaleFactor, + ProductDescription = 18 * scaleFactor, + Button = 24 * scaleFactor, + AdditionalDetails = 14 * scaleFactor, + Purchasing = 36 * scaleFactor, + } + + --[[ + Special text colors, as needed + ]] + local TextColor = { + PriceLabel = Color3.new(1, 1, 1) + } + + --[[ + Background images, including slice center + ]] + local Image = {} + Image.PromptBackground = isTenFoot + and makeImageData("ui/PurchasePrompt/PurchasePromptBG@2x.png", Rect.new(17, 17, 19, 19)) + or makeImageData("ui/PurchasePrompt/PurchasePromptBG.png", Rect.new(8, 9, 10, 10)) + Image.InProgressBackground = isTenFoot + and makeImageData("ui/PurchasePrompt/LoadingBG@2x.png", Rect.new(17, 17, 19, 19)) + or makeImageData("ui/PurchasePrompt/LoadingBG.png", Rect.new(9, 9, 11, 11)) + + Image.ButtonUpLeft = isTenFoot + and makeImageData("ui/PurchasePrompt/LeftButton@2x.png", Rect.new(18, 5, 20, 7)) + or makeImageData("ui/PurchasePrompt/LeftButton.png", Rect.new(8, 3, 10, 4)) + Image.ButtonDownLeft = isTenFoot + and makeImageData("ui/PurchasePrompt/LeftButtonDown@2x.png", Rect.new(18, 5, 20, 7)) + or makeImageData("ui/PurchasePrompt/LeftButtonDown.png", Rect.new(8, 3, 10, 4)) + Image.ButtonUpRight = isTenFoot + and makeImageData("ui/PurchasePrompt/RightButton@2x.png", Rect.new(3, 5, 5, 7)) + or makeImageData("ui/PurchasePrompt/RightButton.png", Rect.new(2, 3, 3, 4)) + Image.ButtonDownRight = isTenFoot + and makeImageData("ui/PurchasePrompt/RightButtonDown@2x.png", Rect.new(3, 5, 5, 7)) + or makeImageData("ui/PurchasePrompt/RightButtonDown.png", Rect.new(2, 3, 3, 4)) + Image.ButtonUp = isTenFoot + and makeImageData("ui/PurchasePrompt/SingleButton@2x.png", Rect.new(18, 5, 20, 7)) + or makeImageData("ui/PurchasePrompt/SingleButton.png", Rect.new(8, 3, 10, 4)) + Image.ButtonDown = isTenFoot + and makeImageData("ui/PurchasePrompt/SingleButtonDown@2x.png", Rect.new(18, 5, 20, 7)) + or makeImageData("ui/PurchasePrompt/SingleButtonDown.png", Rect.new(8, 3, 10, 4)) + + Image.PremiumIcon = makeImageData("ui/PurchasePrompt/Premium.png") + + if FFlagChinaLicensingApp then + Image.RobuxIcon = isTenFoot + and makeImageData("ui/clb_robux_20@3x.png") + or makeImageData("ui/clb_robux_20.png") + else + -- Set a reference to both so they can be preloaded and displayed depending on AB test + Image.RobuxIcon = isTenFoot + and makeImageData("ui/common/robux_small@2x.png") + or makeImageData("ui/common/robux_small.png") + end + + Image.ErrorIcon = isTenFoot + and makeImageData("ui/ErrorIcon.png") + or makeImageData("ui/ErrorIcon.png") + + Image.ButtonA = isTenFoot + and makeImageData("ui/Settings/Help/AButtonDark@2x.png") + or makeImageData("ui/Settings/Help/AButtonDark.png") + Image.ButtonB = isTenFoot + and makeImageData("ui/Settings/Help/BButtonDark@2x.png") + or makeImageData("ui/Settings/Help/BButtonDark.png") + + local LayoutValues = strict({ + Size = strict(Size, "LayoutValues.Size"), + TextSize = strict(TextSize, "LayoutValues.TextSize"), + TextColor = strict(TextColor, "LayoutValues.TextColor"), + Image = strict(Image, "LayoutValues.Image"), + }, "LayoutValues") + + return LayoutValues +end + +return LayoutValues \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/Network.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/Network.lua new file mode 100644 index 0000000..b70dff5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/Network.lua @@ -0,0 +1,240 @@ +local Root = script.Parent.Parent +local HttpService = game:GetService("HttpService") +local ContentProvider = game:GetService("ContentProvider") +local MarketplaceService = game:GetService("MarketplaceService") +local InsertService = game:GetService("InsertService") +local UserInputService = game:GetService("UserInputService") +local Players = game:GetService("Players") + +local Promise = require(Root.Promise) +local PremiumProduct = require(Root.Models.PremiumProduct) + +-- This is the approximate strategy for URL building that we use elsewhere +local BASE_URL = string.gsub(ContentProvider.BaseUrl:lower(), "/m.", "/www.") +BASE_URL = string.gsub(BASE_URL, "http:", "https:") + +local API_URL = string.gsub(BASE_URL, "https://www", "https://api") +local AB_TEST_URL = string.gsub(BASE_URL, "https://www", "https://abtesting") +local BASE_CATALOG_URL = string.gsub(BASE_URL, "https://www.", "https://catalog.") +local BASE_ECONOMY_URL = string.gsub(BASE_URL, "https://www.", "https://economy.") +local PREMIUM_FEATURES_URL = string.gsub(BASE_URL, "https://www.", "https://premiumfeatures.") +local ECONOMY_CREATOR_STATS_URL = string.gsub(BASE_URL, "https://www.", "https://economycreatorstats.") + +local function request(options, resolve, reject) + return HttpService:RequestInternal(options):Start(function(success, response) + if success then + local result + success, result = pcall(HttpService.JSONDecode, HttpService, response.Body) + + if success then + resolve(result) + else + reject("Could not parse JSON.") + end + else + reject(tostring(response.StatusMessage)) + end + end) +end + +local AB_SUBJECT_TYPE_USER_ID = 1 + +local function getABTestGroup(userId, testName) + local abTestRequest = { + { + ExperimentName = testName, + SubjectType = AB_SUBJECT_TYPE_USER_ID, + SubjectTargetId = userId, + } + } + + return Promise.new(function(resolve, reject) + return request({ + Url = AB_TEST_URL .. "v1/get-enrollments", + Method = "POST", + Body = HttpService:JSONEncode(abTestRequest), + Headers = { + ["Content-Type"] = "application/json", + ["Accept"] = "application/json", + } + }, resolve, reject) + end) +end + +local function getProductInfo(id, infoType) + return MarketplaceService:GetProductInfo(id, infoType) +end + +local function getPremiumProductInfo() + return Promise.new(function(resolve, reject) + request({ + Url = PREMIUM_FEATURES_URL .. "v1/products?typeName=Subscription", + Method = "GET", + }, function(response) + -- Gets cheapest premium package + local subscription + for _, product in ipairs(response.products) do + local iapProduct = PremiumProduct(product) + if iapProduct ~= nil then + if subscription == nil or subscription.robuxAmount > iapProduct.robuxAmount then + subscription = iapProduct + end + end + end + + -- Remove after backend fixes their end... + local platform = UserInputService:GetPlatform() + if platform == Enum.Platform.Android and subscription ~= nil and subscription.mobileProductId ~= nil then + subscription.mobileProductId = subscription.mobileProductId:lower() + end + + resolve(subscription) + end, reject) + end) +end + +local function getPlayerOwns(player, id, infoType) + if infoType == Enum.InfoType.Asset then + return MarketplaceService:PlayerOwnsAsset(player, id) + elseif infoType == Enum.InfoType.GamePass then + return MarketplaceService:UserOwnsGamePassAsync(player.UserId, id) + elseif infoType == Enum.InfoType.Subscription then + return MarketplaceService:IsPlayerSubscribed(player, id) + end + + return false +end + +local function performPurchase(infoType, productId, expectedPrice, requestId, isRobloxPurchase) + local success, result = pcall(function() + return MarketplaceService:PerformPurchase(infoType, productId, expectedPrice, requestId, isRobloxPurchase) + end) + + if success then + return result + end + + error(tostring(result)) +end + +local function loadAssetForEquip(assetId) + return InsertService:LoadAsset(assetId) +end + +local function getAccountInfo() + return Promise.new(function(resolve, reject) + request({ + Url = API_URL .. "users/account-info", + Method = "GET", + }, resolve, reject) + end) +end + +local function getXboxRobuxBalance() + return Promise.new(function(resolve, reject) + request({ + Url = API_URL .. "my/platform-currency-budget", + Method = "GET", + }, resolve, reject) + end) +end + +local function getBundleDetails(bundleId) + local url = BASE_CATALOG_URL .."v1/bundles/" ..tostring(bundleId) .."/details" + local options = { + Url = url, + Method = "GET", + } + + return Promise.new(function(resolve, reject) + spawn(function() + request(options, resolve, reject) + end) + end) +end + +local function getProductPurchasableDetails(productId) + local url = BASE_ECONOMY_URL .."v1/products/" ..tostring(productId) .."?showPurchasable=true" + local options = { + Url = url, + Method = "GET" + } + + return Promise.new(function(resolve, reject) + spawn(function() + request(options, resolve, reject) + end) + end) +end + +local function postPremiumImpression() + local url = ECONOMY_CREATOR_STATS_URL.."v1/universes/" ..tostring(game.GameId) .."/premium-impressions/increment" + local options = { + Url = url, + Method = "POST", + Body = HttpService:JSONEncode("{}"), + Headers = { + ["Content-Type"] = "application/json", + ["Accept"] = "application/json", + } + } + + return Promise.new(function(resolve, reject) + spawn(function() + return HttpService:RequestInternal(options):Start(function(success, response) + -- Ignore all responses, don't need to do anything + end) + end) + end) +end + +local function getPremiumUpsellPrecheck() + local options = { + Url = string.format("%sv1/users/%d/premium-upsell-precheck?universeId=%d&placeId=%d", + PREMIUM_FEATURES_URL, Players.LocalPlayer.UserId, game.GameId, game.PlaceId), + Method = "GET", + } + + return Promise.new(function(resolve, reject) + spawn(function() + return HttpService:RequestInternal(options):Start(function(success, response) + if success and response.StatusCode == 200 then + resolve() + else + reject() + end + end) + end) + end) +end + +local Network = {} + +-- TODO: "Promisify" is not strictly necessary with the new `request` structure, +-- refactor this to clean up the overzealous promise-wrapping +function Network.new() + local networkService = { + getABTestGroup = Promise.promisify(getABTestGroup), + getProductInfo = Promise.promisify(getProductInfo), + getPlayerOwns = Promise.promisify(getPlayerOwns), + performPurchase = Promise.promisify(performPurchase), + loadAssetForEquip = Promise.promisify(loadAssetForEquip), + getAccountInfo = Promise.promisify(getAccountInfo), + getXboxRobuxBalance = Promise.promisify(getXboxRobuxBalance), + getBundleDetails = getBundleDetails, + getProductPurchasableDetails = getProductPurchasableDetails, + getPremiumProductInfo = Promise.promisify(getPremiumProductInfo), + postPremiumImpression = Promise.promisify(postPremiumImpression), + getPremiumUpsellPrecheck = Promise.promisify(getPremiumUpsellPrecheck), + } + + setmetatable(networkService, { + __tostring = function() + return "Service(Network)" + end + }) + + return networkService +end + +return Network \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/PlatformInterface.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/PlatformInterface.lua new file mode 100644 index 0000000..04e9b10 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Services/PlatformInterface.lua @@ -0,0 +1,57 @@ +local Root = script.Parent.Parent +local ContentProvider = game:GetService("ContentProvider") +local GuiService = game:GetService("GuiService") +local MarketplaceService = game:GetService("MarketplaceService") +local PlatformService = nil +pcall(function() + PlatformService = game:GetService("PlatformService") +end) + +local GetFFlagUpsellDirectToPackage = require(Root.Flags.GetFFlagUpsellDirectToPackage) + +local BASE_URL = string.gsub(ContentProvider.BaseUrl:lower(), "/m.", "/www.") + +local PlatformInterface = {} + +function PlatformInterface.new() + local service = {} + + setmetatable(service, { + __tostring = function() + return "Service(PlatformInterface)" + end, + }) + + function service.signalMockPurchasePremium() + MarketplaceService:SignalMockPurchasePremium() + end + + function service.startPremiumUpsell(productId) + local url = nil + if GetFFlagUpsellDirectToPackage() then + url = ("%supgrades/paymentmethods?ap=%d"):format(BASE_URL, productId) + else + url = ("%spremium/membership"):format(BASE_URL) + end + + GuiService:OpenBrowserWindow(url) + end + + function service.startRobuxUpsellWeb() + local url = ("%sUpgrades/Robux.aspx"):format(BASE_URL) + + GuiService:OpenBrowserWindow(url) + end + + function service.promptNativePurchase(player, mobileProductId) + return MarketplaceService:PromptNativePurchase(player, mobileProductId) + end + + function service.beginPlatformStorePurchase(xboxProductId) + return PlatformService:BeginPlatformStorePurchase(xboxProductId) + end + + return service +end + +return PlatformInterface \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/LayoutValuesKey.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/LayoutValuesKey.lua new file mode 100644 index 0000000..c965e19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/LayoutValuesKey.lua @@ -0,0 +1,5 @@ +local Symbol = require(script.Parent.Symbol) + +local LayoutValuesKey = Symbol.named("LayoutValuesKey") + +return LayoutValuesKey \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/LocalizationContextKey.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/LocalizationContextKey.lua new file mode 100644 index 0000000..700afc2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/LocalizationContextKey.lua @@ -0,0 +1,5 @@ +local Symbol = require(script.Parent.Symbol) + +local LocalizationContextKey = Symbol.named("LocalizationContextKey") + +return LocalizationContextKey \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/Symbol.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/Symbol.lua new file mode 100644 index 0000000..305d66a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Symbols/Symbol.lua @@ -0,0 +1,30 @@ +--[[ + A 'Symbol' is an opaque marker type. + + Symbols have the type 'userdata', but when printed to the console, the name + of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockAnalytics.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockAnalytics.lua new file mode 100644 index 0000000..e62a81d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockAnalytics.lua @@ -0,0 +1,44 @@ +--[[ + Mocks our analytics interface so we can make sure certain thunks + trigger analytics calls without actually calling the real ones. +]] +local createSpy = require(script.Parent.createSpy) + +local MockAnalytics = {} + +function MockAnalytics.new() + local reportRobuxUpsellStarted = createSpy() + local signalPurchaseSuccess = createSpy() + local signalPremiumUpsellShownPremium = createSpy() + local signalPremiumUpsellShownNonPremium = createSpy() + local signalAdultLegalTextShown = createSpy() + + local mockService = { + reportRobuxUpsellStarted = reportRobuxUpsellStarted.value, + signalPurchaseSuccess = signalPurchaseSuccess.value, + signalPremiumUpsellShownPremium = signalPremiumUpsellShownPremium.value, + signalPremiumUpsellShownNonPremium = signalPremiumUpsellShownNonPremium.value, + signalAdultLegalTextShown = signalAdultLegalTextShown.value, + } + + local spies = { + reportRobuxUpsellStarted = reportRobuxUpsellStarted, + signalPurchaseSuccess = signalPurchaseSuccess, + signalPremiumUpsellShownPremium = signalPremiumUpsellShownPremium, + signalPremiumUpsellShownNonPremium = signalPremiumUpsellShownNonPremium, + signalAdultLegalTextShown = signalAdultLegalTextShown, + } + + setmetatable(mockService, { + __tostring = function() + return "Service(MockAnalytics)" + end, + }) + + return { + spies = spies, + mockService = mockService, + } +end + +return MockAnalytics diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockExternalSettings.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockExternalSettings.lua new file mode 100644 index 0000000..9eac893 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockExternalSettings.lua @@ -0,0 +1,75 @@ +--[[ + Mocks some external settings so we can test the purchase prompt's + behavior under various external circumstances. +]] +local Root = script.Parent.Parent + +local LuaPackages = Root.Parent +local Cryo = require(LuaPackages.Cryo) +local GetFFlagLuaUseThirdPartyPermissions = require(Root.Flags.GetFFlagLuaUseThirdPartyPermissions) + +local DEFAULT_FLAG_STATES = { + -- Allow restriction of third-party sales. Was never properly turned on in + -- the old prompt. We should change this if it defaults to on. + RestrictSales2 = false, + -- Disables all in-game purchasing. A kill-switch for emergency purposes + Order66 = false, +} + +local MockExternalSettings = {} + +function MockExternalSettings.new(isStudio, isTenFoot, flags, platform) + local service = {} + + flags = Cryo.Dictionary.join(DEFAULT_FLAG_STATES, flags) + + --[[ + getMockFlag allows you to test both flag states for tests unrelated to your flag. Usage: + function service.getFFlagTestFlag() + return getMockFlag(flags.TestFlag, GetFFlagTestFlag()) + end + ]] + local function getMockFlag(mockFlag, systemFlag) + if mockFlag ~= nil then + return mockFlag + end + return systemFlag + end + + function service.getPlatform() + return platform + end + + function service.isStudio() + return isStudio + end + + function service.isThirdPartyPurchaseAllowed() + return flags.PermissionsServiceIsThirdPartyPurchaseAllowed + end + + function service.getLuaUseThirdPartyPermissions() + return getMockFlag(flags.LuaUseThirdPartyPermissions, GetFFlagLuaUseThirdPartyPermissions()) + end + + function service.getFlagHideThirdPartyPurchaseFailure() + return flags.HideThirdPartyPurchaseFailure + end + + -- TODO(DEVTOOLS-4227): Remove this flag + function service.getFlagRestrictSales2() + return flags.RestrictSales2 + end + + function service.getFlagOrder66() + return flags.Order66 + end + + function service.isTenFootInterface() + return isTenFoot + end + + return service +end + +return MockExternalSettings diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockNetwork.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockNetwork.lua new file mode 100644 index 0000000..4dca864 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockNetwork.lua @@ -0,0 +1,147 @@ +--[[ + Mock network implementation that returns values in the expected + formats, or returns promise rejections if specified +]] +local Promise = require(script.Parent.Parent.Promise) + +local function getABTestGroup() + return Promise.resolve(false) +end + +local function getProductInfo(id, infoType) + return Promise.resolve({ + AssetId = 1, + AssetTypeId = 2, + ContentRatingTypeId = 0, + Creator = { + CreatorType = "Group", + CreatorTargetId = 1, + Name = "ROBLOX", + Id = 1, + }, + Description = "This item isn't real!", + IconImageAssetId = 1, + IsForSale = true, + IsLimited = false, + IsLimitedUnique = false, + IsNew = false, + IsPublicDomain = false, + MinimumMembershipLevel = 0, + Name = "Test Item", + PriceInRobux = 100, + ProductId = 1, + }) +end + +local function getPlayerOwns(player, id, infoType) + return Promise.resolve(false) +end + +local function performPurchase(infoType, productId, expectedPrice, requestId) + return Promise.resolve({ + success = true, + purchased = true, + receipt = "fake-receipt-hash", + }) +end + +local function loadAssetForEquip(assetId) + return Promise.resolve(Instance.new("Tool")) +end + +local function getAccountInfo() + return Promise.resolve({ + RobuxBalance = 2147483647, + MembershipType = 0, + }) +end + +local function getBundleDetails(bundleId) + return Promise.resolve({ + id = 1, + name = "mock-name", + description = "mock-description", + items = { + [1] = { + id = 1, + name = "outfit-name", + type = "UserOutfit", + }, + }, + creator = { + id = 1, + name = "ROBLOX", + type = "User", + }, + product = { + id = 1, + isForSale = true, + priceInRobux = 100, + } + }) +end + +local function getProductPurchasableDetails(productId) + return Promise.resolve({ + purchasable = false, + reason = "mock-reason", + price = 100, + }) +end + +local function postPremiumImpression() + return Promise.resolve() +end + +local function getPremiumUpsellPrecheck() + return Promise.resolve(true) +end + +local function networkFailure(id, infoType) + return Promise.reject("Failed to access network service") +end + +local MockNetwork = {} +MockNetwork.__index = MockNetwork + +function MockNetwork.new(shouldFail) + local mockNetworkService + + if shouldFail then + mockNetworkService = { + getABTestGroup = networkFailure, + getProductInfo = networkFailure, + getPlayerOwns = networkFailure, + performPurchase = networkFailure, + loadAssetForEquip = networkFailure, + getAccountInfo = networkFailure, + getBundleDetails = networkFailure, + getProductPurchasableDetails = networkFailure, + postPremiumImpression = networkFailure, + getPremiumUpsellPrecheck = networkFailure, + } + else + mockNetworkService = { + getABTestGroup = getABTestGroup, + getProductInfo = getProductInfo, + getPlayerOwns = getPlayerOwns, + performPurchase = performPurchase, + loadAssetForEquip = loadAssetForEquip, + getAccountInfo = getAccountInfo, + getBundleDetails = getBundleDetails, + getProductPurchasableDetails = getProductPurchasableDetails, + postPremiumImpression = postPremiumImpression, + getPremiumUpsellPrecheck = getPremiumUpsellPrecheck, + } + end + + setmetatable(mockNetworkService, { + __tostring = function() + return "MockService(Network)" + end + }) + + return mockNetworkService +end + +return MockNetwork \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockPlatformInterface.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockPlatformInterface.lua new file mode 100644 index 0000000..191357a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/MockPlatformInterface.lua @@ -0,0 +1,41 @@ +--[[ + Mocks calls to certain platform-specific functions so that we can + ensure they're being called properly by our thunks. +]] +local createSpy = require(script.Parent.createSpy) + +local MockPlatformInterface = {} + +function MockPlatformInterface.new() + local startRobuxUpsellWeb = createSpy() + local promptNativePurchase = createSpy() + local startPremiumUpsell = createSpy() + local signalMockPurchasePremium = createSpy() + + local mockService = { + startPremiumUpsell = startPremiumUpsell.value, + signalMockPurchasePremium = signalMockPurchasePremium.value, + startRobuxUpsellWeb = startRobuxUpsellWeb.value, + promptNativePurchase = promptNativePurchase.value, + } + + local spies = { + startPremiumUpsell = startPremiumUpsell, + signalMockPurchasePremium = signalMockPurchasePremium, + startRobuxUpsellWeb = startRobuxUpsellWeb, + promptNativePurchase = promptNativePurchase, + } + + setmetatable(mockService, { + __tostring = function() + return "Service(MockPlatformInterface)" + end, + }) + + return { + spies = spies, + mockService = mockService, + } +end + +return MockPlatformInterface \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/UnitTestContainer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/UnitTestContainer.lua new file mode 100644 index 0000000..89c67a3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/UnitTestContainer.lua @@ -0,0 +1,66 @@ +--[[ + Component that wraps its provided children with a store provider, + a LayoutValues object, and a ScreenGui. Convenient for testing! +]] +local Root = script.Parent.Parent +local LocalizationService = game:GetService("LocalizationService") +local CorePackages = game:GetService("CorePackages") + +local LuaPackages = Root.Parent +local Roact = require(LuaPackages.Roact) +local Rodux = require(LuaPackages.Rodux) +local RoactRodux = require(LuaPackages.RoactRodux) +local UIBlox = require(LuaPackages.UIBlox) +local StyleProvider = UIBlox.Style.Provider + +local LayoutValuesProvider = require(Root.Components.Connection.LayoutValuesProvider) +local LocalizationContextProvider = require(Root.Components.Connection.LocalizationContextProvider) +local getLocalizationContext = require(Root.Localization.getLocalizationContext) +local Reducer = require(Root.Reducers.Reducer) +local LayoutValues = require(Root.Services.LayoutValues) + +local DarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme) +local Gotham = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham) + +local UnitTestContainer = Roact.Component:extend("UnitTestContainer") + +function UnitTestContainer:init() + self.layoutValues = LayoutValues.new(false, false).layout + self.store = self.props.overrideStore or Rodux.Store.new(Reducer, {}) + + local locale = self.props.overrideLocale or LocalizationService.RobloxLocaleId + self.localizationContext = getLocalizationContext(locale) +end + +function UnitTestContainer:render() + assert(self.props[Roact.Children] ~= nil and #self.props[Roact.Children] > 0, + "UnitTestContainer: no children provided, nothing will be tested") + + return Roact.createElement(RoactRodux.StoreProvider, { + store = self.store, + }, { + PurchasePrompt = Roact.createElement(StyleProvider, { + style = { + Theme = DarkTheme, + Font = Gotham, + }, + }, { + Roact.createElement(LocalizationContextProvider, { + localizationContext = self.localizationContext, + render = function() + return Roact.createElement(LayoutValuesProvider, { + isTenFootInterface = false, + render = function() + return Roact.createElement("ScreenGui", { + AutoLocalize = false, + IgnoreGuiInset = true, + }, self.props[Roact.Children]) + end, + }) + end, + }) + }) + }) +end + +return UnitTestContainer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/createSpy.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/createSpy.lua new file mode 100644 index 0000000..e1ab9fc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Test/createSpy.lua @@ -0,0 +1,66 @@ +--[[ + A utility used to create a function spy that can be used to robustly test + that functions are invoked the correct number of times and with the correct + number of arguments. + + This should only be used in tests. +]] + +local function createSpy(inner) + local self = { + callCount = 0, + values = {}, + valuesLength = 0, + } + + self.value = function(...) + self.callCount = self.callCount + 1 + self.values = {...} + self.valuesLength = select("#", ...) + + if inner ~= nil then + return inner(...) + end + end + + self.assertCalledWith = function(_, ...) + local len = select("#", ...) + + if self.valuesLength ~= len then + error(("Expected %d arguments, but was called with %d arguments"):format( + self.valuesLength, + len + ), 2) + end + + for i = 1, len do + local expected = select(i, ...) + + assert(self.values[i] == expected, "value differs") + end + end + + self.captureValues = function(_, ...) + local len = select("#", ...) + local result = {} + + assert(self.valuesLength == len, "length of expected values differs from stored values") + + for i = 1, len do + local key = select(i, ...) + result[key] = self.values[i] + end + + return result + end + + setmetatable(self, { + __index = function(_, key) + error(("%q is not a valid member of spy"):format(key)) + end, + }) + + return self +end + +return createSpy \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunk.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunk.lua new file mode 100644 index 0000000..79ca8da --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunk.lua @@ -0,0 +1,86 @@ +--[[ + An upgraded version of Rodux's thunk middleware that describes a table + format for thunks. This allows them to be named (via the `type` field) + and gives us cleaner methods for dependency injection via providing + services to our middleware that can be threaded into any thunks that + might request them +]] +local Root = script.Parent +local Symbol = require(Root.Symbols.Symbol) + +local Thunk = {} + +local ThunkTag = Symbol.named("ThunkTag") + +function Thunk.middleware(services) + services = services or {} + + return function(nextDispatch, store) + --[[ + This middleware doesn't need to do anything during initialization + so we go straight to returning the wrapped dispatch function + ]] + return function(action) + if action[ThunkTag] == true then + local injectedServices = {} + + for _, service in pairs(action.requiredServices) do + local providedService = services[service] + + if providedService == nil then + error(( + "Service with key %s is a dependency but was not provided" + ):format(service)) + end + + injectedServices[service] = providedService + end + + --[[ + By convention, we return the result of our thunk operation. + This value is not guaranteed to have any particular form or + meaning, but it prevents our middleware from conditionally returning, + which is a dangerous pattern in Lua. + ]] + return action(store, injectedServices) + else + return nextDispatch(action) + end + end + end +end + +function Thunk.new(name, requiredServices, onInvoke) + assert(typeof(name) == "string", "Bad arg #1: name must be a string") + assert(requiredServices == nil or typeof(requiredServices) == "table", + "Bad arg #2: requiredServices must be a table or nil") + assert(typeof(onInvoke) == "function", "Bad arg #3: onInvoke must be a function") + + requiredServices = requiredServices or {} + + return setmetatable({ + [ThunkTag] = true, + type = name, + requiredServices = requiredServices, + }, { + __call = function(self, ...) + onInvoke(...) + end, + }) +end + +function Thunk.test(thunk, store, providedServices) + assert(typeof(thunk) == "table" and thunk[ThunkTag] == true, + "Test Error - Bad arg #1: Must provide a valid thunk") + + if #thunk.requiredServices > 0 then + for _, service in ipairs(thunk.requiredServices) do + assert(providedServices[service] ~= nil, + "Test Error - Bad arg #3: Missing required service "..tostring(service)) + end + end + + return thunk(store, providedServices) +end + +return Thunk \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunk.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunk.spec.lua new file mode 100644 index 0000000..14f2263 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunk.spec.lua @@ -0,0 +1,106 @@ +return function() + local Root = script.Parent + local LuaPackages = Root.Parent + + local Rodux = require(LuaPackages.Rodux) + + local Thunk = require(script.Parent.Thunk) + + describe("Thunk middleware", function() + local function lastActionReducer(state, action) + return { + count = (state.count or 0) + 1, + lastAction = action, + } + end + + it("should only intercept thunk objects", function() + local store = Rodux.Store.new(lastActionReducer, {}, { Thunk.middleware() }) + + expect(store:getState().count).to.equal(1) + expect(store:getState().lastAction.type).to.equal("@@INIT") + + local thunk = Thunk.new("Foo", {}, function() + -- do nothing in particular + end) + store:dispatch(thunk) + + expect(store:getState().count).to.equal(1) + expect(store:getState().lastAction.type).to.equal("@@INIT") + + store:dispatch({ type = "NewAction" }) + + expect(store:getState().count).to.equal(2) + expect(store:getState().lastAction.type).to.equal("NewAction") + end) + + it("should invoke the provided functions of intercepted thunks", function() + local store = Rodux.Store.new(lastActionReducer, {}, { Thunk.middleware() }) + local thunkInvocations = 0 + + local thunk = Thunk.new("Foo", {}, function() + thunkInvocations = thunkInvocations + 1 + end) + + expect(thunkInvocations).to.equal(0) + + store:dispatch(thunk) + expect(thunkInvocations).to.equal(1) + + store:dispatch(thunk) + expect(thunkInvocations).to.equal(2) + end) + + it("should provide only the requested services to the thunk on invocation", function() + local fooServiceKey = newproxy(false) + local barServiceKey = newproxy(false) + local FooService = {} + local BarService = {} + + local store = Rodux.Store.new(lastActionReducer, {}, { + Thunk.middleware({ + [fooServiceKey] = FooService, + [barServiceKey] = BarService, + }) + }) + + local servicesFound = nil + local thunk = Thunk.new("Foo", { fooServiceKey }, function(store, services) + servicesFound = services + end) + + store:dispatch(thunk) + expect(servicesFound[fooServiceKey]).to.equal(FooService) + expect(servicesFound[barServiceKey]).never.to.be.ok() + end) + + it("should throw if thunks requests services that are not provided", function() + local store = Rodux.Store.new(lastActionReducer, {}, { Thunk.middleware() }) + local thunk = Thunk.new("Foo", { "fakeService" }, function() + -- do nothing in particular + end) + + expect(function() store:dispatch(thunk) end).to.throw() + end) + end) + + describe("Thunk constructor", function() + it("should validate arguments", function() + local noop = function() end + + expect(Thunk.new).to.throw() + expect(function() Thunk.new(10, nil, noop) end).to.throw() + expect(function() Thunk.new("Foo", 10, noop) end).to.throw() + expect(function() Thunk.new("Foo", nil, 10) end).to.throw() + end) + + it("should produce a callable table", function() + local thunk = Thunk.new("Foo", {}, function() + -- do nothing in particular + end) + + expect(type(thunk)).to.equal("table") + expect(function() thunk() end).never.to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completePurchase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completePurchase.lua new file mode 100644 index 0000000..2437f53 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completePurchase.lua @@ -0,0 +1,33 @@ +local Root = script.Parent.Parent +local Workspace = game:GetService("Workspace") + +local SetPromptState = require(Root.Actions.SetPromptState) +local PurchaseCompleteRecieved = require(Root.Actions.PurchaseCompleteRecieved) +local PromptState = require(Root.Enums.PromptState) +local Thunk = require(Root.Thunk) + +--[[ + This delay is used to make sure the animation plays long enough + for the player to see that the purchase is happening; it's only + for visual effect +]] +local DELAY = 1 + +local function completePurchase() + return Thunk.new(script.Name, {}, function(store, services) + local startTime = store:getState().purchasingStartTime + local timeElapsed = Workspace.DistributedGameTime - startTime + + store:dispatch(PurchaseCompleteRecieved()) + + if timeElapsed >= DELAY then + return store:dispatch(SetPromptState(PromptState.PurchaseComplete)) + else + delay(DELAY - timeElapsed, function() + return store:dispatch(SetPromptState(PromptState.PurchaseComplete)) + end) + end + end) +end + +return completePurchase \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completeRequest.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completeRequest.lua new file mode 100644 index 0000000..c1150ef --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completeRequest.lua @@ -0,0 +1,42 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") +local MarketplaceService = game:GetService("MarketplaceService") + +local CompleteRequest = require(Root.Actions.CompleteRequest) +local RequestType = require(Root.Enums.RequestType) +local PurchaseError = require(Root.Enums.PurchaseError) +local Thunk = require(Root.Thunk) + +local function completeRequest() + return Thunk.new(script.Name, {}, function(store, services) + local state = store:getState() + local requestType = state.promptRequest.requestType + local purchaseError = state.purchaseError + local id = state.promptRequest.id + local didPurchase = state.hasCompletedPurchase + + if requestType == RequestType.Product then + local playerId = Players.LocalPlayer.UserId + + MarketplaceService:SignalPromptProductPurchaseFinished(playerId, id, didPurchase) + elseif requestType == RequestType.GamePass then + MarketplaceService:SignalPromptGamePassPurchaseFinished(Players.LocalPlayer, id, didPurchase) + elseif requestType == RequestType.Bundle then + MarketplaceService:SignalPromptBundlePurchaseFinished(Players.LocalPlayer, id, didPurchase) + elseif requestType == RequestType.Asset then + MarketplaceService:SignalPromptPurchaseFinished(Players.LocalPlayer, id, didPurchase) + + local assetTypeId = state.productInfo.assetTypeId + if didPurchase and assetTypeId then + -- AssetTypeId returned by the platform endpoint might not exist in the AssetType Enum + pcall(function() MarketplaceService:SignalAssetTypePurchased(Players.LocalPlayer, assetTypeId) end) + end + elseif requestType == RequestType.Premium then + MarketplaceService:SignalPromptPremiumPurchaseFinished(didPurchase or purchaseError == PurchaseError.AlreadyPremium) + end + + return store:dispatch(CompleteRequest()) + end) +end + +return completeRequest diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completeRequest.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completeRequest.spec.lua new file mode 100644 index 0000000..ccad2be --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/completeRequest.spec.lua @@ -0,0 +1,202 @@ +return function() + local Root = script.Parent.Parent + local MarketplaceService = game:GetService("MarketplaceService") + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local RequestType = require(Root.Enums.RequestType) + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local createSpy = require(Root.Test.createSpy) + local Thunk = require(Root.Thunk) + + local completeRequest = require(script.Parent.completeRequest) + + describe("should signal prompt finished when purchase was not made", function() + it("should signal product purchase finished", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.PromptPurchase, + promptRequest = { + id = 123, + requestType = RequestType.Product, + infoType = Enum.InfoType.Product + }, + }) + + local thunk = completeRequest() + + local finishedSignalSpy = createSpy() + local connection = MarketplaceService.PromptProductPurchaseFinished:Connect(finishedSignalSpy.value) + + Thunk.test(thunk, store) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.None) + + expect(finishedSignalSpy.callCount).to.equal(1) + + local values = finishedSignalSpy:captureValues("userId", "productId", "didPurchase") + + expect(values.productId).to.equal(123) + expect(values.didPurchase).to.equal(false) + + connection:Disconnect() + end) + + it("should signal game pass purchase finished", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.Error, + promptRequest = { + id = 456, + requestType = RequestType.GamePass, + infoType = Enum.InfoType.GamePass + }, + }) + + local thunk = completeRequest() + + local finishedSignalSpy = createSpy() + local connection = MarketplaceService.PromptGamePassPurchaseFinished:Connect(finishedSignalSpy.value) + + Thunk.test(thunk, store) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.None) + + expect(finishedSignalSpy.callCount).to.equal(1) + + local values = finishedSignalSpy:captureValues("player", "gamePassId", "didPurchase") + + expect(values.gamePassId).to.equal(456) + expect(values.didPurchase).to.equal(false) + + connection:Disconnect() + end) + + it("should signal asset purchase finished", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.Error, + promptRequest = { + id = 789, + requestType = RequestType.Asset, + infoType = Enum.InfoType.Asset + }, + }) + + local thunk = completeRequest() + + local finishedSignalSpy = createSpy() + local connection = MarketplaceService.PromptPurchaseFinished:Connect(finishedSignalSpy.value) + + Thunk.test(thunk, store) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.None) + + expect(finishedSignalSpy.callCount).to.equal(1) + + local values = finishedSignalSpy:captureValues("player", "assetId", "didPurchase") + + expect(values.assetId).to.equal(789) + expect(values.didPurchase).to.equal(false) + + connection:Disconnect() + end) + end) + + describe("should signal prompt finished when purchase was completed", function() + it("should signal product purchase finished", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.PurchaseComplete, + promptRequest = { + id = 123, + requestType = RequestType.Product, + infoType = Enum.InfoType.Product + }, + hasCompletedPurchase = true, + }) + + local thunk = completeRequest() + + local finishedSignalSpy = createSpy() + local connection = MarketplaceService.PromptProductPurchaseFinished:Connect(finishedSignalSpy.value) + + Thunk.test(thunk, store) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.None) + + expect(finishedSignalSpy.callCount).to.equal(1) + + local values = finishedSignalSpy:captureValues("userId", "productId", "didPurchase") + + expect(values.productId).to.equal(123) + expect(values.didPurchase).to.equal(true) + + connection:Disconnect() + end) + + it("should signal game pass purchase finished", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.PurchaseComplete, + promptRequest = { + id = 456, + requestType = RequestType.GamePass, + infoType = Enum.InfoType.GamePass + }, + hasCompletedPurchase = true, + }) + + local thunk = completeRequest() + + local finishedSignalSpy = createSpy() + local connection = MarketplaceService.PromptGamePassPurchaseFinished:Connect(finishedSignalSpy.value) + + Thunk.test(thunk, store) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.None) + + expect(finishedSignalSpy.callCount).to.equal(1) + + local values = finishedSignalSpy:captureValues("player", "gamePassId", "didPurchase") + + expect(values.gamePassId).to.equal(456) + expect(values.didPurchase).to.equal(true) + + connection:Disconnect() + end) + + it("should signal asset purchase finished", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.PurchaseComplete, + promptRequest = { + id = 789, + requestType = RequestType.Asset, + infoType = Enum.InfoType.Asset + }, + hasCompletedPurchase = true, + }) + + local thunk = completeRequest() + + local finishedSignalSpy = createSpy() + local connection = MarketplaceService.PromptPurchaseFinished:Connect(finishedSignalSpy.value) + + Thunk.test(thunk, store) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.None) + + expect(finishedSignalSpy.callCount).to.equal(1) + + local values = finishedSignalSpy:captureValues("player", "assetId", "didPurchase") + + expect(values.assetId).to.equal(789) + expect(values.didPurchase).to.equal(true) + + connection:Disconnect() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/hideWindow.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/hideWindow.lua new file mode 100644 index 0000000..62b9bf2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/hideWindow.lua @@ -0,0 +1,13 @@ +local Root = script.Parent.Parent + +local SetWindowState = require(Root.Actions.SetWindowState) +local WindowState = require(Root.Enums.WindowState) +local Thunk = require(Root.Thunk) + +local function hideWindow(productInfo, accountInfo, alreadyOwned) + return Thunk.new(script.Name, {}, function(store, services) + return store:dispatch(SetWindowState(WindowState.Hidden)) + end) +end + +return hideWindow \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateBundlePurchase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateBundlePurchase.lua new file mode 100644 index 0000000..521a42d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateBundlePurchase.lua @@ -0,0 +1,67 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") + +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local RequestBundlePurchase = require(Root.Actions.RequestBundlePurchase) +local PurchaseError = require(Root.Enums.PurchaseError) +local getBundleDetails = require(Root.Network.getBundleDetails) +local getProductPurchasableDetails = require(Root.Network.getProductPurchasableDetails) +local getAccountInfo = require(Root.Network.getAccountInfo) +local Network = require(Root.Services.Network) +local ExternalSettings = require(Root.Services.ExternalSettings) +local hasPendingRequest = require(Root.Utils.hasPendingRequest) +local Promise = require(Root.Promise) +local Thunk = require(Root.Thunk) + +local resolveBundlePromptState = require(script.Parent.resolveBundlePromptState) + +local requiredServices = { + Network, + ExternalSettings, +} + +local function initiateBundlePurchase(bundleId) + return Thunk.new(script.Name, requiredServices, function(store, services) + local network = services[Network] + local externalSettings = services[ExternalSettings] + + if hasPendingRequest(store:getState()) then + return nil + end + + store:dispatch(RequestBundlePurchase(bundleId)) + + local isStudio = externalSettings.isStudio() + + if not isStudio and Players.LocalPlayer.UserId <= 0 then + store:dispatch(ErrorOccurred(PurchaseError.Guest)) + return nil + end + + if externalSettings.getFlagOrder66() then + store:dispatch(ErrorOccurred(PurchaseError.PurchaseDisabled)) + return nil + end + + return Promise.all({ + bundleDetails = getBundleDetails(network, bundleId), + accountInfo = getAccountInfo(network, externalSettings) + }) + :andThen(function(results) + local bundleProductId = results.bundleDetails.product.id + getProductPurchasableDetails(network, bundleProductId) + :andThen(function(productPurchasableDetails) + store:dispatch(resolveBundlePromptState( + productPurchasableDetails, + results.bundleDetails, + results.accountInfo + )) + end) + end) + :catch(function(errorReason) + store:dispatch(ErrorOccurred(errorReason)) + end) + end) +end + +return initiateBundlePurchase \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateBundlePurchase.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateBundlePurchase.spec.lua new file mode 100644 index 0000000..2d77b09 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateBundlePurchase.spec.lua @@ -0,0 +1,70 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local RequestType = require(Root.Enums.RequestType) + local Reducer = require(Root.Reducers.Reducer) + local Network = require(Root.Services.Network) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockNetwork = require(Root.Test.MockNetwork) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + + local initiateBundlePurchase = require(script.Parent.initiateBundlePurchase) + + it("should run without errors", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiateBundlePurchase(15) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(), + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.promptRequest.id).to.equal(15) + expect(state.promptRequest.requestType).to.equal(RequestType.Bundle) + end) + + it("should abort when a purchase is already in progress", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.PromptPurchase, + promptRequest = { + id = 12, + infoType = Enum.InfoType.Product, + requestType = RequestType.Product + } + }) + + -- Initiate a purchase for a different product id + local thunk = initiateBundlePurchase(999) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(), + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + expect(state.promptRequest.id).to.equal(12) + expect(state.promptState).to.equal(PromptState.PromptPurchase) + end) + + it("should resolve to an error state if a network failure occurs", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiateBundlePurchase(15) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(true), + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.Error) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePremiumPurchase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePremiumPurchase.lua new file mode 100644 index 0000000..0a6a333 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePremiumPurchase.lua @@ -0,0 +1,52 @@ +local Root = script.Parent.Parent + +local Promise = require(Root.Promise) +local Thunk = require(Root.Thunk) +local PurchaseError = require(Root.Enums.PurchaseError) + +local RequestPremiumPurchase = require(Root.Actions.RequestPremiumPurchase) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local getPremiumUpsellPrecheck = require(Root.Network.getPremiumUpsellPrecheck) +local getPremiumProductInfo = require(Root.Network.getPremiumProductInfo) +local getAccountInfo = require(Root.Network.getAccountInfo) +local Network = require(Root.Services.Network) +local ExternalSettings = require(Root.Services.ExternalSettings) +local resolvePremiumPromptState = require(Root.Thunks.resolvePremiumPromptState) +local hasPendingRequest = require(Root.Utils.hasPendingRequest) + +local requiredServices = { + Network, + ExternalSettings, +} + +local function initiatePremiumPurchase(id, infoType, equipIfPurchased) + return Thunk.new(script.Name, requiredServices, function(store, services) + local network = services[Network] + local externalSettings = services[ExternalSettings] + + if hasPendingRequest(store:getState()) then + return nil + end + store:dispatch(RequestPremiumPurchase()) + + if externalSettings.getFlagOrder66() then + store:dispatch(ErrorOccurred(PurchaseError.PurchaseDisabled)) + return nil + end + + local shouldPrecheck = not externalSettings.isStudio() + return Promise.all({ + canShowUpsell = shouldPrecheck and getPremiumUpsellPrecheck(network) or Promise.resolve(true), + premiumProductInfo = getPremiumProductInfo(network), + accountInfo = getAccountInfo(network, externalSettings), + }) + :andThen(function(results) + store:dispatch(resolvePremiumPromptState(results.accountInfo, results.premiumProductInfo, results.canShowUpsell)) + end) + :catch(function(errorReason) + store:dispatch(ErrorOccurred(errorReason)) + end) + end) +end + +return initiatePremiumPurchase diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePurchase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePurchase.lua new file mode 100644 index 0000000..5920580 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePurchase.lua @@ -0,0 +1,89 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") +local ABTestService = game:GetService("ABTestService") + +local SetABVariation = require(Root.Actions.SetABVariation) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local RequestAssetPurchase = require(Root.Actions.RequestAssetPurchase) +local RequestGamepassPurchase = require(Root.Actions.RequestGamepassPurchase) +local RequestProductPurchase = require(Root.Actions.RequestProductPurchase) +local PurchaseError = require(Root.Enums.PurchaseError) +local Constants = require(Root.Misc.Constants) +local Network = require(Root.Services.Network) +local getProductInfo = require(Root.Network.getProductInfo) +local getIsAlreadyOwned = require(Root.Network.getIsAlreadyOwned) +local getAccountInfo = require(Root.Network.getAccountInfo) +local ExternalSettings = require(Root.Services.ExternalSettings) +local hasPendingRequest = require(Root.Utils.hasPendingRequest) +local Promise = require(Root.Promise) +local Thunk = require(Root.Thunk) + +local GetFFlagAdultConfirmationEnabled = require(Root.Flags.GetFFlagAdultConfirmationEnabled) + +local resolvePromptState = require(script.Parent.resolvePromptState) + +local requiredServices = { + Network, + ExternalSettings, +} + +local function initiatePurchase(id, infoType, equipIfPurchased, isRobloxPurchase) + return Thunk.new(script.Name, requiredServices, function(store, services) + local network = services[Network] + local externalSettings = services[ExternalSettings] + + if hasPendingRequest(store:getState()) then + return nil + end + + if GetFFlagAdultConfirmationEnabled() then + pcall(function() + store:dispatch(SetABVariation(Constants.ABTests.ADULT_CONFIRMATION, + ABTestService:GetVariant(Constants.ABTests.ADULT_CONFIRMATION))) + end) + end + + if infoType == Enum.InfoType.Asset then + store:dispatch(RequestAssetPurchase(id, equipIfPurchased, isRobloxPurchase)) + elseif infoType == Enum.InfoType.GamePass then + store:dispatch(RequestGamepassPurchase(id)) + elseif infoType == Enum.InfoType.Product then + store:dispatch(RequestProductPurchase(id, equipIfPurchased)) + else + assert(false, "Invalid product type") + return nil + end + + local isStudio = externalSettings.isStudio() + + if not isStudio and Players.LocalPlayer.UserId <= 0 then + store:dispatch(ErrorOccurred(PurchaseError.Guest)) + return nil + end + + if externalSettings.getFlagOrder66() then + store:dispatch(ErrorOccurred(PurchaseError.PurchaseDisabled)) + return nil + end + + return Promise.all({ + productInfo = getProductInfo(network, id, infoType), + accountInfo = getAccountInfo(network, externalSettings), + alreadyOwned = getIsAlreadyOwned(network, id, infoType), + }) + :andThen(function(results) + -- Once we've finished all of our async data fetching, we'll + -- resolve the state of the prompt + store:dispatch(resolvePromptState( + results.productInfo, + results.accountInfo, + results.alreadyOwned + )) + end) + :catch(function(errorReason) + store:dispatch(ErrorOccurred(errorReason)) + end) + end) +end + +return initiatePurchase diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePurchase.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePurchase.spec.lua new file mode 100644 index 0000000..1b253b9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiatePurchase.spec.lua @@ -0,0 +1,87 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local RequestType = require(Root.Enums.RequestType) + local PromptState = require(Root.Enums.PromptState) + local PurchaseError = require(Root.Enums.PurchaseError) + local Reducer = require(Root.Reducers.Reducer) + local Network = require(Root.Services.Network) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockNetwork = require(Root.Test.MockNetwork) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + + local initiatePurchase = require(script.Parent.initiatePurchase) + + it("should run without errors", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiatePurchase(15, Enum.InfoType.Product, false) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(), + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + }) + + local state = store:getState() + + expect(state.promptRequest.id).to.equal(15) + end) + + it("should abort when a purchase is already in progress", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.PromptPurchase, + promptRequest = { + id = 12, + requestType = RequestType.Product, + infoType = Enum.InfoType.Product, + } + }) + + -- Initiate a purchase for a different product id + local thunk = initiatePurchase(999, Enum.InfoType.Product, false) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(), + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + }) + + local state = store:getState() + expect(state.promptRequest.id).to.equal(12) + expect(state.promptState).to.equal(PromptState.PromptPurchase) + end) + + it("should resolve to an error state if a network failure occurs", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiatePurchase(15, Enum.InfoType.Product, false) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(true), + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + }) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.Error) + end) + + it("should resolve to an error state if purchasing is disabled", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiatePurchase(15, Enum.InfoType.Product, false) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(true), + [ExternalSettings] = MockExternalSettings.new(false, false, { + Order66 = true, + }), + }) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.Error) + expect(state.purchaseError).to.equal(PurchaseError.PurchaseDisabled) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateSubscriptionPurchase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateSubscriptionPurchase.lua new file mode 100644 index 0000000..9786386 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateSubscriptionPurchase.lua @@ -0,0 +1,64 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") + +local Promise = require(Root.Promise) +local Thunk = require(Root.Thunk) +local PurchaseError = require(Root.Enums.PurchaseError) + +local RequestSubscriptionPurchase = require(Root.Actions.RequestSubscriptionPurchase) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local getProductInfo = require(Root.Network.getProductInfo) +local getIsAlreadyOwned = require(Root.Network.getIsAlreadyOwned) +local getAccountInfo = require(Root.Network.getAccountInfo) +local Network = require(Root.Services.Network) +local ExternalSettings = require(Root.Services.ExternalSettings) +local hasPendingRequest = require(Root.Utils.hasPendingRequest) +local resolveSubscriptionPromptState = require(Root.Thunks.resolveSubscriptionPromptState) +local GetFFlagDeveloperSubscriptionsEnabled = require(Root.Flags.GetFFlagDeveloperSubscriptionsEnabled) + +local requiredServices = { + Network, + ExternalSettings, +} + +local function initiateSubscriptionPurchase(id) + return Thunk.new(script.Name, requiredServices, function(store, services) + local network = services[Network] + local externalSettings = services[ExternalSettings] + + if not GetFFlagDeveloperSubscriptionsEnabled() or hasPendingRequest(store:getState()) then + return nil + end + store:dispatch(RequestSubscriptionPurchase(id)) + + local isStudio = externalSettings.isStudio() + + if not isStudio and Players.LocalPlayer.UserId <= 0 then + store:dispatch(ErrorOccurred(PurchaseError.Guest)) + return nil + end + + if externalSettings.getFlagOrder66() then + store:dispatch(ErrorOccurred(PurchaseError.PurchaseDisabled)) + return nil + end + + return Promise.all({ + productInfo = getProductInfo(network, id, Enum.InfoType.Subscription), + accountInfo = getAccountInfo(network, externalSettings), + alreadyOwned = getIsAlreadyOwned(network, id, Enum.InfoType.Subscription), + }) + :andThen(function(results) + store:dispatch(resolveSubscriptionPromptState( + results.productInfo, + results.accountInfo, + results.alreadyOwned + )) + end) + :catch(function(errorReason) + store:dispatch(ErrorOccurred(errorReason)) + end) + end) +end + +return initiateSubscriptionPurchase \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateSubscriptionPurchase.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateSubscriptionPurchase.spec.lua new file mode 100644 index 0000000..8a3e8d2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/initiateSubscriptionPurchase.spec.lua @@ -0,0 +1,92 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local RequestType = require(Root.Enums.RequestType) + local PromptState = require(Root.Enums.PromptState) + local PurchaseError = require(Root.Enums.PurchaseError) + local Reducer = require(Root.Reducers.Reducer) + local Network = require(Root.Services.Network) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockNetwork = require(Root.Test.MockNetwork) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + local GetFFlagDeveloperSubscriptionsEnabled = require(Root.Flags.GetFFlagDeveloperSubscriptionsEnabled) + + local initiateSubscriptionPurchase = require(script.Parent.initiateSubscriptionPurchase) + + if not GetFFlagDeveloperSubscriptionsEnabled() then + return + end + + it("should run without errors", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiateSubscriptionPurchase(15) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(), + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + }) + + local state = store:getState() + + expect(state.promptRequest.id).to.equal(15) + end) + + it("should abort when a purchase is already in progress", function() + local store = Rodux.Store.new(Reducer, { + promptState = PromptState.PromptPurchase, + promptRequest = { + id = 12, + requestType = RequestType.Product, + infoType = Enum.InfoType.Product, + } + }) + + -- Initiate a purchase for a different product id + local thunk = initiateSubscriptionPurchase(999) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(), + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + }) + + local state = store:getState() + expect(state.promptRequest.id).to.equal(12) + expect(state.promptState).to.equal(PromptState.PromptPurchase) + end) + + it("should resolve to an error state if a network failure occurs", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiateSubscriptionPurchase(15) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(true), + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + }) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.Error) + end) + + it("should resolve to an error state if purchasing is disabled", function() + local store = Rodux.Store.new(Reducer) + + local thunk = initiateSubscriptionPurchase(15) + + Thunk.test(thunk, store, { + [Network] = MockNetwork.new(true), + [ExternalSettings] = MockExternalSettings.new(false, false, { + Order66 = true, + }), + }) + + local state = store:getState() + expect(state.promptState).to.equal(PromptState.Error) + expect(state.purchaseError).to.equal(PurchaseError.PurchaseDisabled) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchPremiumUpsell.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchPremiumUpsell.lua new file mode 100644 index 0000000..65656b7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchPremiumUpsell.lua @@ -0,0 +1,60 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") + +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local PurchaseCompleteRecieved = require(Root.Actions.PurchaseCompleteRecieved) +local SetPromptState = require(Root.Actions.SetPromptState) +local SetWindowState = require(Root.Actions.SetWindowState) +local UpsellFlow = require(Root.Enums.UpsellFlow) +local PromptState = require(Root.Enums.PromptState) +local PurchaseError = require(Root.Enums.PurchaseError) +local WindowState = require(Root.Enums.WindowState) +local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow) +local PlatformInterface = require(Root.Services.PlatformInterface) +local ExternalSettings = require(Root.Services.ExternalSettings) +local hideWindow = require(Root.Thunks.hideWindow) +local Thunk = require(Root.Thunk) + +local requiredServices = { + PlatformInterface, + ExternalSettings, +} + +local function launchPremiumUpsell() + return Thunk.new(script.Name, requiredServices, function(store, services) + local platformInterface = services[PlatformInterface] + local externalSettings = services[ExternalSettings] + local state = store:getState() + local premiumProductInfo = state.premiumProductInfo + + if externalSettings.isStudio() then + -- Signal back end that they clicked yes + -- waits for SignalPromptPremiumPurchaseFinished to report membership changed + platformInterface.signalMockPurchasePremium() + store:dispatch(PurchaseCompleteRecieved()) + return store:dispatch(SetWindowState(WindowState.Hidden)) + end + + local upsellFlow = getUpsellFlow(externalSettings.getPlatform()) + + if upsellFlow == UpsellFlow.Web then + local productId = premiumProductInfo.productId + + platformInterface.startPremiumUpsell(productId) + store:dispatch(SetPromptState(PromptState.UpsellInProgress)) + store:dispatch(hideWindow()) + + elseif upsellFlow == UpsellFlow.Mobile then + local nativeProductId = premiumProductInfo.mobileProductId + + platformInterface.promptNativePurchase(Players.LocalPlayer, nativeProductId) + store:dispatch(SetPromptState(PromptState.UpsellInProgress)) + store:dispatch(hideWindow()) + + else + store:dispatch(ErrorOccurred(PurchaseError.PremiumUnavailablePlatform)) + end + end) +end + +return launchPremiumUpsell \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchPremiumUpsell.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchPremiumUpsell.spec.lua new file mode 100644 index 0000000..7493ec9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchPremiumUpsell.spec.lua @@ -0,0 +1,110 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local WindowState = require(Root.Enums.WindowState) + local PurchaseError = require(Root.Enums.PurchaseError) + local Reducer = require(Root.Reducers.Reducer) + local PlatformInterface = require(Root.Services.PlatformInterface) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockPlatformInterface = require(Root.Test.MockPlatformInterface) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + + local launchPremiumUpsell = require(script.Parent.launchPremiumUpsell) + + it("should run without errors on Studio", function() + local store = Rodux.Store.new(Reducer, { + premiumProductInfo = { + id = 350, + } + }) + + local thunk = launchPremiumUpsell() + local platformInterface = MockPlatformInterface.new() + local externalSettings = MockExternalSettings.new(true, false, {}, Enum.Platform.Windows) + + Thunk.test(thunk, store, { + [PlatformInterface] = platformInterface.mockService, + [ExternalSettings] = externalSettings + }) + + local state = store:getState() + + expect(platformInterface.spies.signalMockPurchasePremium.callCount).to.equal(1) + expect(state.promptState).to.equal(PromptState.None) + expect(state.windowState).to.equal(WindowState.Hidden) + end) + + it("should run without errors on Desktop", function() + local store = Rodux.Store.new(Reducer, { + premiumProductInfo = { + id = 350, + } + }) + + local thunk = launchPremiumUpsell() + local platformInterface = MockPlatformInterface.new() + local externalSettings = MockExternalSettings.new(false, false, {}, Enum.Platform.Windows) + + Thunk.test(thunk, store, { + [PlatformInterface] = platformInterface.mockService, + [ExternalSettings] = externalSettings + }) + + local state = store:getState() + + -- https://jira.rbx.com/browse/EC-46 + -- expect(platformInterface.spies.startPremiumUpsell.callCount).to.equal(1) + -- expect(state.promptState).to.equal(PromptState.UpsellInProgress) + end) + + it("should run without errors on Mobile", function() + local store = Rodux.Store.new(Reducer, { + premiumProductInfo = { + id = 350, + } + }) + + local thunk = launchPremiumUpsell() + local platformInterface = MockPlatformInterface.new() + local externalSettings = MockExternalSettings.new(false, false, {}, Enum.Platform.IOS) + + Thunk.test(thunk, store, { + [PlatformInterface] = platformInterface.mockService, + [ExternalSettings] = externalSettings + }) + + local state = store:getState() + + -- https://jira.rbx.com/browse/EC-46 + -- expect(platformInterface.spies.promptNativePurchase.callCount).to.equal(1) + -- expect(state.promptState).to.equal(PromptState.UpsellInProgress) + end) + + it("should run into error on unsupported platforms", function() + local store = Rodux.Store.new(Reducer, { + premiumProductInfo = { + id = 350, + } + }) + + local thunk = launchPremiumUpsell() + local platformInterface = MockPlatformInterface.new() + local externalSettings = MockExternalSettings.new(false, false, {}, Enum.Platform.XBoxOne) + + Thunk.test(thunk, store, { + [PlatformInterface] = platformInterface.mockService, + [ExternalSettings] = externalSettings + }) + + local state = store:getState() + + -- https://jira.rbx.com/browse/EC-46 + -- expect(platformInterface.spies.startPremiumUpsell.callCount).to.equal(0) + -- expect(state.purchaseError).to.equal(PurchaseError.PremiumUnavailablePlatform) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchRobuxUpsell.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchRobuxUpsell.lua new file mode 100644 index 0000000..15df2a4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchRobuxUpsell.lua @@ -0,0 +1,78 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") + +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local SetPromptState = require(Root.Actions.SetPromptState) +local UpsellFlow = require(Root.Enums.UpsellFlow) +local PromptState = require(Root.Enums.PromptState) +local PurchaseError = require(Root.Enums.PurchaseError) +local Constants = require(Root.Misc.Constants) +local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow) +local Analytics = require(Root.Services.Analytics) +local PlatformInterface = require(Root.Services.PlatformInterface) +local Thunk = require(Root.Thunk) +local Promise = require(Root.Promise) + +local retryAfterUpsell = require(script.Parent.retryAfterUpsell) + +local GetFFlagAdultConfirmationEnabled = require(Root.Flags.GetFFlagAdultConfirmationEnabled) +local GetFFlagAdultConfirmationEnabledNew = require(Root.Flags.GetFFlagAdultConfirmationEnabledNew) + +local requiredServices = { + Analytics, + PlatformInterface, +} + +local function launchRobuxUpsell() + return Thunk.new(script.Name, requiredServices, function(store, services) + local analytics = services[Analytics] + local platformInterface = services[PlatformInterface] + local state = store:getState() + local abVars = state.abVariations + + if (GetFFlagAdultConfirmationEnabledNew() + or (GetFFlagAdultConfirmationEnabled() and (abVars[Constants.ABTests.ADULT_CONFIRMATION] == "Variation1"))) + and state.accountInfo.AgeBracket ~= 0 + and state.promptState ~= PromptState.AdultConfirmation then + analytics.signalAdultLegalTextShown() + store:dispatch(SetPromptState(PromptState.AdultConfirmation)) + return + end + + local upsellFlow = getUpsellFlow(UserInputService:GetPlatform()) + + if upsellFlow == UpsellFlow.Web then + platformInterface.startRobuxUpsellWeb() + analytics.reportRobuxUpsellStarted() + store:dispatch(SetPromptState(PromptState.UpsellInProgress)) + + elseif upsellFlow == UpsellFlow.Mobile then + local nativeProductId = store:getState().nativeUpsell.robuxProductId + + analytics.reportNativeUpsellStarted(nativeProductId) + platformInterface.promptNativePurchase(Players.LocalPlayer, nativeProductId) + store:dispatch(SetPromptState(PromptState.UpsellInProgress)) + + elseif upsellFlow == UpsellFlow.Xbox then + local nativeProductId = store:getState().nativeUpsell.robuxProductId + store:dispatch(SetPromptState(PromptState.UpsellInProgress)) + return Promise.new(function(resolve, reject) + local platformPurchaseResult = platformInterface.beginPlatformStorePurchase(nativeProductId) + + Promise.resolve(platformPurchaseResult) + end) + :andThen(function(result) + if result ~= 0 then + store:dispatch(retryAfterUpsell) + end + end) + elseif upsellFlow == UpsellFlow.Unavailable then + store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobuxNoUpsell)) + else + warn("Need more Robux: platform not supported for Robux purchase") + end + end) +end + +return launchRobuxUpsell diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchRobuxUpsell.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchRobuxUpsell.spec.lua new file mode 100644 index 0000000..c06aa7b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/launchRobuxUpsell.spec.lua @@ -0,0 +1,128 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local Analytics = require(Root.Services.Analytics) + local PlatformInterface = require(Root.Services.PlatformInterface) + local MockAnalytics = require(Root.Test.MockAnalytics) + local MockPlatformInterface = require(Root.Test.MockPlatformInterface) + local Constants = require(Root.Misc.Constants) + local Thunk = require(Root.Thunk) + + local GetFFlagAdultConfirmationEnabled = require(Root.Flags.GetFFlagAdultConfirmationEnabled) + local GetFFlagAdultConfirmationEnabledNew = require(Root.Flags.GetFFlagAdultConfirmationEnabledNew) + + local launchRobuxUpsell = require(script.Parent.launchRobuxUpsell) + + it("should run without errors", function() + local store = Rodux.Store.new(Reducer, { + accountInfo = { + AgeBracket = 0, + }, + promptState = PromptState.PromptPurchase, + }) + + local thunk = launchRobuxUpsell() + local analytics = MockAnalytics.new() + local platformInterface = MockPlatformInterface.new() + + Thunk.test(thunk, store, { + [Analytics] = analytics.mockService, + [PlatformInterface] = platformInterface.mockService, + }) + + local state = store:getState() + + if not settings():GetFFlag("ChinaLicensingApp") then + expect(analytics.spies.reportRobuxUpsellStarted.callCount).to.equal(1) + expect(platformInterface.spies.startRobuxUpsellWeb.callCount).to.equal(1) + expect(state.promptState).to.equal(PromptState.UpsellInProgress) + end + end) + + if GetFFlagAdultConfirmationEnabledNew() then + it("should show adult legal text if under 13", function() + local store = Rodux.Store.new(Reducer, { + accountInfo = { + AgeBracket = 1, + }, + promptState = PromptState.PromptPurchase, + }) + + local thunk = launchRobuxUpsell() + local analytics = MockAnalytics.new() + local platformInterface = MockPlatformInterface.new() + + Thunk.test(thunk, store, { + [Analytics] = analytics.mockService, + [PlatformInterface] = platformInterface.mockService, + }) + + local state = store:getState() + + expect(analytics.spies.signalAdultLegalTextShown.callCount).to.equal(1) + expect(state.promptState).to.equal(PromptState.AdultConfirmation) + end) + end + + if GetFFlagAdultConfirmationEnabled() then + it("should show adult legal text if under 13 and part of ab test", function() + local store = Rodux.Store.new(Reducer, { + accountInfo = { + AgeBracket = 1, + }, + promptState = PromptState.PromptPurchase, + abVariations = { + [Constants.ABTests.ADULT_CONFIRMATION] = "Variation1", + } + }) + + local thunk = launchRobuxUpsell() + local analytics = MockAnalytics.new() + local platformInterface = MockPlatformInterface.new() + + Thunk.test(thunk, store, { + [Analytics] = analytics.mockService, + [PlatformInterface] = platformInterface.mockService, + }) + + local state = store:getState() + + expect(analytics.spies.signalAdultLegalTextShown.callCount).to.equal(1) + expect(state.promptState).to.equal(PromptState.AdultConfirmation) + end) + + it("should continue as normal if under 13 and not apart of ab test", function() + local store = Rodux.Store.new(Reducer, { + accountInfo = { + AgeBracket = 1, + }, + promptState = PromptState.PromptPurchase, + abVariations = { + [Constants.ABTests.ADULT_CONFIRMATION] = "Control", + } + }) + + local thunk = launchRobuxUpsell() + local analytics = MockAnalytics.new() + local platformInterface = MockPlatformInterface.new() + + Thunk.test(thunk, store, { + [Analytics] = analytics.mockService, + [PlatformInterface] = platformInterface.mockService, + }) + + local state = store:getState() + + if not settings():GetFFlag("ChinaLicensingApp") then + expect(analytics.spies.reportRobuxUpsellStarted.callCount).to.equal(1) + expect(platformInterface.spies.startRobuxUpsellWeb.callCount).to.equal(1) + expect(state.promptState).to.equal(PromptState.UpsellInProgress) + end + end) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/purchaseItem.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/purchaseItem.lua new file mode 100644 index 0000000..622af77 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/purchaseItem.lua @@ -0,0 +1,82 @@ +local Root = script.Parent.Parent +local HttpService = game:GetService("HttpService") +local Workspace = game:GetService("Workspace") +local Players = game:GetService("Players") + +local StartPurchase = require(Root.Actions.StartPurchase) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local ItemType = require(Root.Enums.ItemType) +local getToolAsset = require(Root.Network.getToolAsset) +local performPurchase = require(Root.Network.performPurchase) +local Network = require(Root.Services.Network) +local Analytics = require(Root.Services.Analytics) +local getPlayerPrice = require(Root.Utils.getPlayerPrice) +local Thunk = require(Root.Thunk) +local Promise = require(Root.Promise) + +local GetFFlagPromptRobloxPurchaseEnabled = require(Root.Flags.GetFFlagPromptRobloxPurchaseEnabled) + +local completePurchase = require(script.Parent.completePurchase) + +-- Only tools can be equipped on purchase +local ASSET_TYPE_TOOL = 19 + +local requiredServices = { + Network, + Analytics, +} + +local function purchaseItem() + return Thunk.new(script.Name, requiredServices, function(store, services) + local network = services[Network] + local analytics = services[Analytics] + + store:dispatch(StartPurchase(Workspace.DistributedGameTime)) + + local state = store:getState() + + local requestId = HttpService:GenerateGUID(false) + + local id = state.promptRequest.id + local infoType = state.promptRequest.infoType + local equipIfPurchased = state.promptRequest.equipIfPurchased + local isRobloxPurchase = GetFFlagPromptRobloxPurchaseEnabled() and state.promptRequest.isRobloxPurchase or false + + local isPlayerPremium = state.accountInfo.membershipType == 4 + local salePrice = getPlayerPrice(state.productInfo, isPlayerPremium) + local assetTypeId = state.productInfo.assetTypeId + local productId = state.productInfo.productId + + local itemType = state.productInfo.itemType + + return performPurchase(network, infoType, productId, salePrice, requestId, isRobloxPurchase) + :andThen(function(result) + --[[ + If the purchase was successful, we signal success, + record analytics, and equip the item if needed + ]] + store:dispatch(completePurchase()) + + -- Marketplace Analytics for bundles is not available yet. + if itemType ~= ItemType.Bundle then + analytics.signalPurchaseSuccess(id, infoType, salePrice, result) + end + + if equipIfPurchased and assetTypeId == ASSET_TYPE_TOOL then + return getToolAsset(network, id) + :andThen(function(tool) + if tool then + tool.Parent = Players.LocalPlayer.Backpack + end + end) + end + + return Promise.resolve() + end) + :catch(function(errorReason) + store:dispatch(ErrorOccurred(errorReason)) + end) + end) +end + +return purchaseItem diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/purchaseItem.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/purchaseItem.spec.lua new file mode 100644 index 0000000..8d6e56f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/purchaseItem.spec.lua @@ -0,0 +1,52 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local Network = require(Root.Services.Network) + local Analytics = require(Root.Services.Analytics) + local MockNetwork = require(Root.Test.MockNetwork) + local MockAnalytics = require(Root.Test.MockAnalytics) + local Thunk = require(Root.Thunk) + + local purchaseItem = require(script.Parent.purchaseItem) + + it("should run without errors", function() + local store = Rodux.Store.new(Reducer) + + local thunk = purchaseItem() + local network = MockNetwork.new() + local analytics = MockAnalytics.new() + + Thunk.test(thunk, store, { + [Network] = network, + [Analytics] = analytics.mockService, + }) + + local state = store:getState() + + expect(analytics.spies.signalPurchaseSuccess.callCount).to.equal(1) + expect(state.promptState).to.equal(PromptState.PurchaseInProgress) + end) + + it("should resolve to an error state if a network error occurs", function() + local store = Rodux.Store.new(Reducer) + + local thunk = purchaseItem() + local network = MockNetwork.new(true) + local analytics = MockAnalytics.new() + + Thunk.test(thunk, store, { + [Network] = network, + [Analytics] = analytics.mockService, + }) + + local state = store:getState() + + expect(analytics.spies.signalPurchaseSuccess.callCount).to.equal(0) + expect(state.promptState).to.equal(PromptState.Error) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveBundlePromptState.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveBundlePromptState.lua new file mode 100644 index 0000000..2e4b4d9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveBundlePromptState.lua @@ -0,0 +1,73 @@ +local Root = script.Parent.Parent +local UserInputService = game:GetService("UserInputService") + +local SetPromptState = require(Root.Actions.SetPromptState) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local BundleProductInfoReceived = require(Root.Actions.BundleProductInfoReceived) +local AccountInfoReceived = require(Root.Actions.AccountInfoReceived) +local PromptNativeUpsell = require(Root.Actions.PromptNativeUpsell) +local PromptState = require(Root.Enums.PromptState) +local PurchaseError = require(Root.Enums.PurchaseError) +local UpsellFlow = require(Root.Enums.UpsellFlow) +local selectRobuxProduct = require(Root.NativeUpsell.selectRobuxProduct) +local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow) +local Thunk = require(Root.Thunk) + +local function getPurchasableStatus(productPurchasableDetails) + local reason = productPurchasableDetails.reason + + if reason == "InsufficientFunds" then + return PurchaseError.NotEnoughRobux + elseif reason == "AlreadyOwned" then + return PurchaseError.AlreadyOwn + elseif reason == "NotForSale" then + return PurchaseError.NotForSale + elseif reason == "ContentRatingRestricted" then + return PurchaseError.Under13 + else + return PurchaseError.UnknownFailure + end +end + +local function resolveBundlePromptState(productPurchasableDetails, bundleDetails, accountInfo) + return Thunk.new(script.Name, {}, function(store, services) + store:dispatch(BundleProductInfoReceived(bundleDetails)) + store:dispatch(AccountInfoReceived(accountInfo)) + + local canPurchase = productPurchasableDetails.purchasable + local failureReason = getPurchasableStatus(productPurchasableDetails) + local price = productPurchasableDetails.price + local platform = UserInputService:GetPlatform() + local upsellFlow = getUpsellFlow(platform) + + if not canPurchase then + if failureReason == PurchaseError.NotEnoughRobux then + if upsellFlow == UpsellFlow.Web then + return store:dispatch(SetPromptState(PromptState.RobuxUpsell)) + else + local neededRobux = price - accountInfo.RobuxBalance + local hasMembership = accountInfo.MembershipType > 0 + + return selectRobuxProduct(platform, neededRobux, hasMembership) + :andThen(function(product) + -- We found a valid upsell product for the current platform + store:dispatch(PromptNativeUpsell(product.productId, product.robuxValue)) + end, function() + -- No upsell item will provide sufficient funds to make this purchase + if platform == Enum.Platform.XBoxOne then + store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobuxXbox)) + else + store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobux)) + end + end) + end + else + return store:dispatch(ErrorOccurred(failureReason)) + end + end + + return store:dispatch(SetPromptState(PromptState.PromptPurchase)) + end) +end + +return resolveBundlePromptState \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveBundlePromptState.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveBundlePromptState.spec.lua new file mode 100644 index 0000000..00321b7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveBundlePromptState.spec.lua @@ -0,0 +1,133 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + + local resolveBundlePromptState = require(script.Parent.resolveBundlePromptState) + + local function getTestPurchasableDetails() + return { + purchasable = true, + reason = "mock-reason", + price = 100, + } + end + + local function getTestBundleDetails() + return { + id = 1, + name = "mock-name", + description = "mock-description", + items = { + [1] = { + id = 1, + name = "outfit-name", + type = "UserOutfit", + }, + }, + creator = { + id = 1, + name = "ROBLOX", + type = "User", + }, + product = { + id = 1, + isForSale = true, + priceInRobux = 100, + } + } + end + + it("should populate store with provided info", function() + local store = Rodux.Store.new(Reducer, {}) + + local purchasableDetails = getTestPurchasableDetails() + local bundleDetails = getTestBundleDetails() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolveBundlePromptState(purchasableDetails, bundleDetails, accountInfo) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.productInfo.name).to.be.ok() + expect(state.accountInfo.balance).to.be.ok() + end) + + it("should resolve state to Error if prerequisites are failed", function() + local store = Rodux.Store.new(Reducer, {}) + + local purchasableDetails = getTestPurchasableDetails() + local bundleDetails = getTestBundleDetails() -- Set product to not for sale + purchasableDetails.purchasable = false + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolveBundlePromptState(purchasableDetails, bundleDetails, accountInfo) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.Error) + end) + + it("should resolve state to PromptPurchase if account meets requirements", function() + local store = Rodux.Store.new(Reducer, {}) + + local purchasableDetails = getTestPurchasableDetails() + local bundleDetails = getTestBundleDetails() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolveBundlePromptState(purchasableDetails, bundleDetails, accountInfo) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.PromptPurchase) + end) + + it("should resolve state to RobuxUpsell if account is short on Robux", function() + local store = Rodux.Store.new(Reducer, {}) + + local purchasableDetails = getTestPurchasableDetails() + local bundleDetails = getTestBundleDetails() + + purchasableDetails.purchasable = false + purchasableDetails.reason = "InsufficientFunds" + -- Player will not have enough robux + local accountInfo = { + RobuxBalance = 0, + MembershipType = 0, + } + local thunk = resolveBundlePromptState(purchasableDetails, bundleDetails, accountInfo) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.RobuxUpsell) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePremiumPromptState.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePremiumPromptState.lua new file mode 100644 index 0000000..a1e99a6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePremiumPromptState.lua @@ -0,0 +1,65 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") + +local SetPromptState = require(Root.Actions.SetPromptState) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local PremiumInfoRecieved = require(Root.Actions.PremiumInfoRecieved) +local AccountInfoReceived = require(Root.Actions.AccountInfoReceived) +local PromptState = require(Root.Enums.PromptState) +local PurchaseError = require(Root.Enums.PurchaseError) +local Analytics = require(Root.Services.Analytics) +local ExternalSettings = require(Root.Services.ExternalSettings) +local Network = require(Root.Services.Network) +local postPremiumImpression = require(Root.Network.postPremiumImpression) +local completeRequest = require(Root.Thunks.completeRequest) +local Thunk = require(Root.Thunk) + + +local requiredServices = { + Network, + ExternalSettings, + Analytics, +} + +local function resolvePremiumPromptState(accountInfo, premiumProduct, canShowUpsell) + return Thunk.new(script.Name, requiredServices, function(store, services) + local network = services[Network] + local externalSettings = services[ExternalSettings] + local analytics = services[Analytics] + local platform = externalSettings.getPlatform() + + store:dispatch(PremiumInfoRecieved(premiumProduct)) + store:dispatch(AccountInfoReceived(accountInfo)) + + if canShowUpsell == false then + return store:dispatch(completeRequest()) + end + + if externalSettings.isStudio() then + if Players.LocalPlayer.MembershipType == Enum.MembershipType.Premium then + return store:dispatch(ErrorOccurred(PurchaseError.AlreadyPremium)) + end + else + if accountInfo.MembershipType == 4 then + analytics.signalPremiumUpsellShownPremium() + return store:dispatch(ErrorOccurred(PurchaseError.AlreadyPremium)) + end + end + + if platform == Enum.Platform.XBoxOne then + return store:dispatch(ErrorOccurred(PurchaseError.PremiumUnavailablePlatform)) + end + + if premiumProduct == nil then + return store:dispatch(ErrorOccurred(PurchaseError.PremiumUnavailable)) + end + + if not externalSettings.isStudio() then + analytics.signalPremiumUpsellShownNonPremium() + postPremiumImpression(network) + end + return store:dispatch(SetPromptState(PromptState.PremiumUpsell)) + end) +end + +return resolvePremiumPromptState diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePremiumPromptState.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePremiumPromptState.spec.lua new file mode 100644 index 0000000..a36655a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePremiumPromptState.spec.lua @@ -0,0 +1,116 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local Analytics = require(Root.Services.Analytics) + local ExternalSettings = require(Root.Services.ExternalSettings) + local Network = require(Root.Services.Network) + local MockAnalytics = require(Root.Test.MockAnalytics) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local MockNetwork = require(Root.Test.MockNetwork) + local Thunk = require(Root.Thunk) + + local resolvePremiumPromptState = require(script.Parent.resolvePremiumPromptState) + + local function getTestProductInfo() + return { + premiumFeatureTypeName = "Subscription", + mobileProductId = "com.roblox.robloxmobile.RobloxPremium450", + description = "Roblox Premium 450", + price = 4.99, + currencySymbol = "$", + isSubscriptionOnly = false, + robuxAmount = 450 + } + end + + it("should populate store with provided info", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePremiumPromptState(accountInfo, productInfo) + + Thunk.test(thunk, store, { + [Analytics] = MockAnalytics.new().mockService, + [ExternalSettings] = MockExternalSettings.new(false, false, { + }, true), + [Network] = MockNetwork.new(), + }) + + local state = store:getState() + + expect(state.premiumProductInfo.mobileProductId).to.be.ok() + expect(state.accountInfo.membershipType).to.be.ok() + end) + + it("should resolve state to Error if failed to get premium products", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = nil + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePremiumPromptState(accountInfo, productInfo) + + Thunk.test(thunk, store, { + [Analytics] = MockAnalytics.new().mockService, + [ExternalSettings] = MockExternalSettings.new(false, false, {}, true), + [Network] = MockNetwork.new(), + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.Error) + end) + + it("should show the upsell given correct data", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePremiumPromptState(accountInfo, productInfo, true) + + Thunk.test(thunk, store, { + [Analytics] = MockAnalytics.new().mockService, + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + [Network] = MockNetwork.new(), + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.PremiumUpsell) + end) + + it("should complete the request and show nothing when failing precheck", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePremiumPromptState(accountInfo, productInfo, false) + + Thunk.test(thunk, store, { + [Analytics] = MockAnalytics.new().mockService, + [ExternalSettings] = MockExternalSettings.new(false, false, {}), + [Network] = MockNetwork.new(), + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.None) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePromptState.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePromptState.lua new file mode 100644 index 0000000..ccec9aa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePromptState.lua @@ -0,0 +1,82 @@ +local Root = script.Parent.Parent +local UserInputService = game:GetService("UserInputService") + +local SetPromptState = require(Root.Actions.SetPromptState) +local ProductInfoReceived = require(Root.Actions.ProductInfoReceived) +local AccountInfoReceived = require(Root.Actions.AccountInfoReceived) +local PromptNativeUpsell = require(Root.Actions.PromptNativeUpsell) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local CompleteRequest = require(Root.Actions.CompleteRequest) +local PromptState = require(Root.Enums.PromptState) +local PurchaseError = require(Root.Enums.PurchaseError) +local UpsellFlow = require(Root.Enums.UpsellFlow) +local selectRobuxProduct = require(Root.NativeUpsell.selectRobuxProduct) +local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow) +local ExternalSettings = require(Root.Services.ExternalSettings) +local meetsPrerequisites = require(Root.Utils.meetsPrerequisites) +local getPlayerProductInfoPrice = require(Root.Utils.getPlayerProductInfoPrice) +local Thunk = require(Root.Thunk) + +local requiredServices = { + ExternalSettings, +} + +local function resolvePromptState(productInfo, accountInfo, alreadyOwned) + return Thunk.new(script.Name, requiredServices, function(store, services) + local externalSettings = services[ExternalSettings] + + store:dispatch(ProductInfoReceived(productInfo)) + store:dispatch(AccountInfoReceived(accountInfo)) + + local restrictThirdParty = externalSettings.getLuaUseThirdPartyPermissions() or externalSettings.getFlagRestrictSales2() + + local canPurchase, failureReason = meetsPrerequisites(productInfo, alreadyOwned, restrictThirdParty, externalSettings) + if not canPurchase then + if externalSettings.getFlagHideThirdPartyPurchaseFailure() then + if not externalSettings.isStudio() and failureReason == PurchaseError.ThirdPartyDisabled then + -- Do not annoy player with 3rd party failure notifications. + return store:dispatch(CompleteRequest()) + end + return store:dispatch(ErrorOccurred(failureReason)) + else + return store:dispatch(ErrorOccurred(failureReason)) + end + end + + local isPlayerPremium = accountInfo.MembershipType == 4 + local price = getPlayerProductInfoPrice(productInfo, isPlayerPremium) + local platform = UserInputService:GetPlatform() + local upsellFlow = getUpsellFlow(platform) + + if price > accountInfo.RobuxBalance then + + if upsellFlow == UpsellFlow.Unavailable then + return store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobuxNoUpsell)) + end + + if upsellFlow == UpsellFlow.Web then + return store:dispatch(SetPromptState(PromptState.RobuxUpsell)) + else + local neededRobux = price - accountInfo.RobuxBalance + local hasMembership = accountInfo.MembershipType > 0 + + return selectRobuxProduct(platform, neededRobux, hasMembership) + :andThen(function(product) + -- We found a valid upsell product for the current platform + store:dispatch(PromptNativeUpsell(product.productId, product.robuxValue)) + end, function() + -- No upsell item will provide sufficient funds to make this purchase + if platform == Enum.Platform.XBoxOne then + store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobuxXbox)) + else + store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobux)) + end + end) + end + end + + return store:dispatch(SetPromptState(PromptState.PromptPurchase)) + end) +end + +return resolvePromptState diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePromptState.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePromptState.spec.lua new file mode 100644 index 0000000..0b927b2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolvePromptState.spec.lua @@ -0,0 +1,139 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + + local resolvePromptState = require(script.Parent.resolvePromptState) + local RequestType = require(Root.Enums.RequestType) + + local function getTestProductInfo() + return { + IsForSale = true, + Name = "Test Product", + PriceInRobux = 10, + MinimumMembershipLevel = 0, + Creator = { + CreatorType = "User", + CreatorTargetId = 1, + }, + } + end + + it("should populate store with provided info", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.productInfo.name).to.be.ok() + expect(state.accountInfo.balance).to.be.ok() + end) + + it("should resolve state to None if hiding 3rd party purchase failure", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + -- Make creator a 3rd party + productInfo.AssetId = 0 + productInfo.Creator.CreatorTargetId = game.CreatorId + 2 + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, { + LuaUseThirdPartyPermissions = true, + PermissionsServiceIsThirdPartyPurchaseAllowed = false, + HideThirdPartyPurchaseFailure = true, + }) + }) + + local state = store:getState() + + expect(state.promptRequest.requestType).to.equal(RequestType.None) + expect(state.promptState).to.equal(PromptState.None) + end) + + it("should resolve state to Error if prerequisites are failed", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + -- Set product to not for sale + productInfo.IsForSale = false + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.Error) + end) + + it("should resolve state to PromptPurchase if account meets requirements", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolvePromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + if not settings():GetFFlag("ChinaLicensingApp") then + expect(state.promptState).to.equal(PromptState.PromptPurchase) + end + end) + + it("should resolve state to RobuxUpsell if account is short on Robux", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + -- Player will not have enough robux + local accountInfo = { + RobuxBalance = 0, + MembershipType = 0, + } + local thunk = resolvePromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + if not settings():GetFFlag("ChinaLicensingApp") then + expect(state.promptState).to.equal(PromptState.RobuxUpsell) + end + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveSubscriptionPromptState.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveSubscriptionPromptState.lua new file mode 100644 index 0000000..fd38826 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveSubscriptionPromptState.lua @@ -0,0 +1,64 @@ +local Root = script.Parent.Parent +local UserInputService = game:GetService("UserInputService") + +local SetPromptState = require(Root.Actions.SetPromptState) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local ProductInfoReceived = require(Root.Actions.ProductInfoReceived) +local AccountInfoReceived = require(Root.Actions.AccountInfoReceived) +local PromptState = require(Root.Enums.PromptState) +local PurchaseError = require(Root.Enums.PurchaseError) +local UpsellFlow = require(Root.Enums.UpsellFlow) +local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow) +local PromptNativeUpsell = require(Root.Actions.PromptNativeUpsell) +local selectRobuxProduct = require(Root.NativeUpsell.selectRobuxProduct) +local Thunk = require(Root.Thunk) + + +local function resolveSubscriptionPromptState(productInfo, accountInfo, alreadyOwned) + return Thunk.new(script.Name, {}, function(store, services) + store:dispatch(ProductInfoReceived(productInfo)) + store:dispatch(AccountInfoReceived(accountInfo)) + + if alreadyOwned then + return store:dispatch(ErrorOccurred(PurchaseError.AlreadyOwn)) + end + + if not productInfo.IsForSale then + return store:dispatch(ErrorOccurred(PurchaseError.NotForSale)) + end + + local price = productInfo.PriceInRobux or 0 + local platform = UserInputService:GetPlatform() + local upsellFlow = getUpsellFlow(platform) + + if price > accountInfo.RobuxBalance then + if upsellFlow == UpsellFlow.Unavailable then + return store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobuxNoUpsell)) + end + + if upsellFlow == UpsellFlow.Web then + return store:dispatch(SetPromptState(PromptState.RobuxUpsell)) + else + local neededRobux = price - accountInfo.RobuxBalance + local hasMembership = accountInfo.MembershipType > 0 + + return selectRobuxProduct(platform, neededRobux, hasMembership) + :andThen(function(product) + -- We found a valid upsell product for the current platform + store:dispatch(PromptNativeUpsell(product.productId, product.robuxValue)) + end, function() + -- No upsell item will provide sufficient funds to make this purchase + if platform == Enum.Platform.XBoxOne then + store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobuxXbox)) + else + store:dispatch(ErrorOccurred(PurchaseError.NotEnoughRobux)) + end + end) + end + end + + return store:dispatch(SetPromptState(PromptState.PromptPurchase)) + end) +end + +return resolveSubscriptionPromptState diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveSubscriptionPromptState.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveSubscriptionPromptState.spec.lua new file mode 100644 index 0000000..3276020 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/resolveSubscriptionPromptState.spec.lua @@ -0,0 +1,112 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local PromptState = require(Root.Enums.PromptState) + local Reducer = require(Root.Reducers.Reducer) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + local GetFFlagDeveloperSubscriptionsEnabled = require(Root.Flags.GetFFlagDeveloperSubscriptionsEnabled) + + local resolveSubscriptionPromptState = require(script.Parent.resolveSubscriptionPromptState) + + if not GetFFlagDeveloperSubscriptionsEnabled() then + return + end + + local function getTestProductInfo() + return { + IsForSale = true, + Name = "Test Product", + PriceInRobux = 10, + MinimumMembershipLevel = 0, + } + end + + it("should populate store with provided info", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolveSubscriptionPromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.productInfo.name).to.be.ok() + expect(state.accountInfo.balance).to.be.ok() + end) + + it("should resolve state to Error if prerequisites are failed", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + -- Set product to not for sale + productInfo.IsForSale = false + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolveSubscriptionPromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + expect(state.promptState).to.equal(PromptState.Error) + end) + + it("should resolve state to PromptPurchase if account meets requirements", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + local accountInfo = { + RobuxBalance = 10, + MembershipType = 0, + } + local thunk = resolveSubscriptionPromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + if not settings():GetFFlag("ChinaLicensingApp") then + expect(state.promptState).to.equal(PromptState.PromptPurchase) + end + end) + + it("should resolve state to RobuxUpsell if account is short on Robux", function() + local store = Rodux.Store.new(Reducer, {}) + + local productInfo = getTestProductInfo() + -- Player will not have enough robux + local accountInfo = { + RobuxBalance = 0, + MembershipType = 0, + } + local thunk = resolveSubscriptionPromptState(productInfo, accountInfo, false) + + Thunk.test(thunk, store, { + [ExternalSettings] = MockExternalSettings.new(false, false, {}) + }) + + local state = store:getState() + + if not settings():GetFFlag("ChinaLicensingApp") then + expect(state.promptState).to.equal(PromptState.RobuxUpsell) + end + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/retryAfterUpsell.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/retryAfterUpsell.lua new file mode 100644 index 0000000..8758b18 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/retryAfterUpsell.lua @@ -0,0 +1,75 @@ +local Root = script.Parent.Parent + +local AccountInfoReceived = require(Root.Actions.AccountInfoReceived) +local PurchaseCompleteRecieved = require(Root.Actions.PurchaseCompleteRecieved) +local ErrorOccurred = require(Root.Actions.ErrorOccurred) +local PurchaseError = require(Root.Enums.PurchaseError) +local PromptState = require(Root.Enums.PromptState) +local RequestType = require(Root.Enums.RequestType) +local getAccountInfo = require(Root.Network.getAccountInfo) +local Network = require(Root.Services.Network) +local ExternalSettings = require(Root.Services.ExternalSettings) +local completeRequest = require(Root.Thunks.completeRequest) +local getPlayerPrice = require(Root.Utils.getPlayerPrice) +local Thunk = require(Root.Thunk) + +local purchaseItem = require(script.Parent.purchaseItem) + +local MAX_RETRIES = 3 +local RETRY_RATE = 1 + +local requiredServices = { + Network, + ExternalSettings, +} + +local function retryAfterUpsell(retriesRemaining) + retriesRemaining = retriesRemaining or MAX_RETRIES + + return Thunk.new(script.Name, requiredServices, function(store, services) + local network = services[Network] + local externalSettings = services[ExternalSettings] + local state = store:getState() + local requestType = state.promptRequest.requestType + local promptState = state.promptState + + if requestType == RequestType.Premium then + if promptState == PromptState.UpsellInProgress then + store:dispatch(PurchaseCompleteRecieved()) + store:dispatch(completeRequest()) + end + else + return getAccountInfo(network, externalSettings) + :andThen(function(accountInfo) + local state = store:getState() + + local isPlayerPremium = state.accountInfo.membershipType == 4 + local price = getPlayerPrice(state.productInfo, isPlayerPremium) + + local balance = accountInfo.RobuxBalance + + store:dispatch(AccountInfoReceived(accountInfo)) + + if price ~= nil and price > balance then + if retriesRemaining > 0 then + -- Upsell result may not yet have propagated, so we need to + -- wait a while and try again + delay(RETRY_RATE, function() + store:dispatch(retryAfterUpsell(retriesRemaining - 1)) + end) + else + store:dispatch(ErrorOccurred(PurchaseError.InvalidFunds)) + end + else + -- Upsell was successful and purchase can now be completed + store:dispatch(purchaseItem()) + end + end) + :catch(function(error) + store:dispatch(ErrorOccurred(error)) + end) + end + end) +end + +return retryAfterUpsell diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/retryAfterUpsell.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/retryAfterUpsell.spec.lua new file mode 100644 index 0000000..3baaf3e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Thunks/retryAfterUpsell.spec.lua @@ -0,0 +1,43 @@ +return function() + local Root = script.Parent.Parent + + local LuaPackages = Root.Parent + local Rodux = require(LuaPackages.Rodux) + + local Reducer = require(Root.Reducers.Reducer) + local Network = require(Root.Services.Network) + local ExternalSettings = require(Root.Services.ExternalSettings) + local MockNetwork = require(Root.Test.MockNetwork) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + local Thunk = require(Root.Thunk) + + local retryAfterUpsell = require(script.Parent.retryAfterUpsell) + + it("should run without errors", function() + local store = Rodux.Store.new(Reducer, { + productInfo = { + price = 0, + membershipTypeRequired = 0, + } + }) + + local thunk = retryAfterUpsell() + local network = MockNetwork.new() + local externalSettings = MockExternalSettings.new(true, false, {}) + + Thunk.test(thunk, store, { + [Network] = network, + [ExternalSettings] = externalSettings, + }) + + local state = store:getState() + local accountInfo + network.getAccountInfo():andThen(function(result) + accountInfo = result + end) + + -- Account info should be re-populated + expect(state.accountInfo.balance).to.be.equal(accountInfo.RobuxBalance) + expect(state.accountInfo.membershipType).to.be.equal(accountInfo.MembershipType) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/ClickScamDetector.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/ClickScamDetector.lua new file mode 100644 index 0000000..be79f05 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/ClickScamDetector.lua @@ -0,0 +1,108 @@ +local Root = script.Parent.Parent +local UserInputService = game:GetService("UserInputService") + +local LuaPackages = Root.Parent +local Cryo = require(LuaPackages.Cryo) + +--[[ + CLILUACORE-318: Revisit this approach and evaluate if it adequately + addresses existing or future scams +]] +local ClickScamDetector = {} +ClickScamDetector.__index = ClickScamDetector + +--[[ + Create a new scam detector with the specified options + + This object will track any clicks or confirm button presses that occur + during its lifetime and attempt to determine whether the player is + being asked to spam clicks or button presses. When processing an action, + users of this object can abort their behavior if calling isClickValid + returns false. +]] +function ClickScamDetector.new(options) + --[[ + Overwrite default values with provided options + ]] + options = Cryo.Dictionary.join({ + -- The number of clicks within the window that is interpreted as a scam + clickSpeedThreshold = 3, + -- The window in which to measure clicks + clickTimeWindow = 1, + -- A delay to allow clicks to stack up and prevent immediate clicks + initialDelay = 1, + -- Allow a button input to be associated with this click detection + buttonInput = nil, + }, options or {}) + + local self = { + _inputConnection = nil, + + _clickCount = 0, + _startTime = tick(), + _options = options, + } + + setmetatable(self, ClickScamDetector) + + self._inputConnection = UserInputService.InputBegan:Connect(function(input) + self:_onInput(input) + end) + + return self +end + +--[[ + Track mouse inputs, counting the number that occurred in the last + CLICK_TIME_WINDOW duration + + Includes touch inputs and pressing the A button on a gamepad +]] +function ClickScamDetector:_onInput(input) + local inputType = input.UserInputType + + local isGamepad = self._options.buttonInput ~= nil and input.KeyCode == self._options.buttonInput + + local isMouseOrTouch = inputType == Enum.UserInputType.MouseButton1 + or inputType == Enum.UserInputType.Touch + + if isGamepad or isMouseOrTouch then + self._clickCount = self._clickCount + 1 + + delay(self._options.clickTimeWindow, function() + self._clickCount = self._clickCount - 1 + end) + end +end + +--[[ + Determine whether or not there's a possibility of a scam occurring + and return whether or not we believe the click to be valid +]] +function ClickScamDetector:isClickValid() + --[[ + If the mouse behavior is locked by dev-facing APIs, clicks are not valid + ]] + if UserInputService.MouseBehavior == Enum.MouseBehavior.LockCurrentPosition then + return false + end + + --[[ + Don't allow any clicks until the initial delay has passed; + ]] + if tick() - self._startTime < self._options.initialDelay then + return false + end + + return self._clickCount / self._options.clickTimeWindow < self._options.clickSpeedThreshold +end + +--[[ + Cleanup connection to InputService; should be called when + the UI element using this object is destroyed +]] +function ClickScamDetector:destroy() + self._inputConnection:Disconnect() +end + +return ClickScamDetector \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/ClickScamDetector.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/ClickScamDetector.spec.lua new file mode 100644 index 0000000..b09626b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/ClickScamDetector.spec.lua @@ -0,0 +1,33 @@ +return function() + local UserInputService = game:GetService("UserInputService") + + local ClickScamDetector = require(script.Parent.ClickScamDetector) + + -- We need better ways to fake time passing, so that we can test further functionality; + -- May want to indirect the `tick` function in the ClickScamDetector and allow overriding + + it("should always report a scam if the mouse is locked", function() + local clickScamDetector = ClickScamDetector.new() + + -- No clicks have been fired + UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition + expect(clickScamDetector:isClickValid()).to.equal(false) + + UserInputService.MouseBehavior = Enum.MouseBehavior.Default + clickScamDetector:destroy() + end) + + it("should report a scam if there are many clicks in quick succession", function() + local clickScamDetector = ClickScamDetector.new() + + local fakeInput = { + UserInputType = Enum.UserInputType.MouseButton1, + } + clickScamDetector:_onInput(fakeInput) + clickScamDetector:_onInput(fakeInput) + clickScamDetector:_onInput(fakeInput) + expect(clickScamDetector:isClickValid()).to.equal(false) + + clickScamDetector:destroy() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerPrice.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerPrice.lua new file mode 100644 index 0000000..8edee6d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerPrice.lua @@ -0,0 +1,20 @@ +local Root = script.Parent.Parent + +local GetFFlagIGPPPremiumPrice = require(Root.Flags.GetFFlagIGPPPremiumPrice) + +-- Used on the data in the state +return function(productInfo, isPlayerPremium) + if GetFFlagIGPPPremiumPrice() then + if isPlayerPremium then + if productInfo.premiumPrice ~= nil then + return productInfo.premiumPrice + else + return productInfo.price + end + else + return productInfo.price + end + else + return productInfo.price + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerPrice.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerPrice.spec.lua new file mode 100644 index 0000000..f512cdd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerPrice.spec.lua @@ -0,0 +1,33 @@ +return function() + local Root = script.Parent.Parent + + local GetFFlagIGPPPremiumPrice = require(Root.Flags.GetFFlagIGPPPremiumPrice) + + local getPlayerPrice = require(script.Parent.getPlayerPrice) + + it("should return correct sale price when not premium", function() + local productInfo = { + price = 5, + premiumPrice = 10, + } + + local price = getPlayerPrice(productInfo, false) + + expect(price).to.equal(5) + end) + + it("should return correct sale price when premium", function() + local productInfo = { + price = 5, + premiumPrice = 10, + } + + local price = getPlayerPrice(productInfo, true) + + if GetFFlagIGPPPremiumPrice() then + expect(price).to.equal(10) + else + expect(price).to.equal(5) + end + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerProductInfoPrice.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerProductInfoPrice.lua new file mode 100644 index 0000000..df1a4e7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerProductInfoPrice.lua @@ -0,0 +1,21 @@ +local Root = script.Parent.Parent + +local GetFFlagIGPPPremiumPrice = require(Root.Flags.GetFFlagIGPPPremiumPrice) + +-- Used on the data directly from the endpoint +-- TODO: Consolidate this with the other function +return function(productInfo, isPlayerPremium) + if GetFFlagIGPPPremiumPrice() then + if isPlayerPremium then + if productInfo.PremiumPriceInRobux ~= nil then + return productInfo.PremiumPriceInRobux + else + return productInfo.PriceInRobux or 0 + end + else + return productInfo.PriceInRobux or 0 + end + else + return productInfo.PriceInRobux or 0 + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerProductInfoPrice.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerProductInfoPrice.spec.lua new file mode 100644 index 0000000..b8098ef --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/getPlayerProductInfoPrice.spec.lua @@ -0,0 +1,33 @@ +return function() + local Root = script.Parent.Parent + + local GetFFlagIGPPPremiumPrice = require(Root.Flags.GetFFlagIGPPPremiumPrice) + + local getPlayerProductInfoPrice = require(script.Parent.getPlayerProductInfoPrice) + + it("should return correct sale price when not premium", function() + local productInfo = { + PriceInRobux = 5, + PremiumPriceInRobux = 10, + } + + local price = getPlayerProductInfoPrice(productInfo, false) + + expect(price).to.equal(5) + end) + + it("should return correct sale price when premium", function() + local productInfo = { + PriceInRobux = 5, + PremiumPriceInRobux = 10, + } + + local price = getPlayerProductInfoPrice(productInfo, true) + + if GetFFlagIGPPPremiumPrice() then + expect(price).to.equal(10) + else + expect(price).to.equal(5) + end + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/hasPendingRequest.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/hasPendingRequest.lua new file mode 100644 index 0000000..b374441 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/hasPendingRequest.lua @@ -0,0 +1,7 @@ +local Root = script.Parent.Parent + +local RequestType = require(Root.Enums.RequestType) + +return function(state) + return state.promptRequest.requestType ~= RequestType.None +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/isMockingPurchases.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/isMockingPurchases.lua new file mode 100644 index 0000000..8e4cf69 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/isMockingPurchases.lua @@ -0,0 +1,12 @@ +local RunService = game:GetService("RunService") + +--[[ + CLILUACORE-314: This should be something we get from MarketplaceService, + so that we'll always be in sync w/ the engine about whether or + not we're mocking purchases +]] +local function isMockingPurchases() + return RunService:IsStudio() +end + +return isMockingPurchases \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/meetsPrerequisites.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/meetsPrerequisites.lua new file mode 100644 index 0000000..ab216b6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/meetsPrerequisites.lua @@ -0,0 +1,84 @@ +local Root = script.Parent.Parent +local Players = game:GetService("Players") +local Workspace = game:GetService("Workspace") + +local PurchaseError = require(Root.Enums.PurchaseError) + +local GetFFlagLuaPremiumCatalogIGPP = require(Root.Flags.GetFFlagLuaPremiumCatalogIGPP) + +local CONTENT_RATING_13_PLUS = 1 +local ROBLOX_CREATOR = 1 +local DEVELOPER_PRODUCT_TYPE = "Developer Product" + +local THIRD_PARTY_WARNING = "AllowThirdPartySales has blocked the purchase" + .. " prompt for %d created by %d. To sell this asset made by a" + .. " different %s, you will need to enable AllowThirdPartySales." + +local function meetsPrerequisites(productInfo, alreadyOwned, restrictThirdParty, externalSettings) + if alreadyOwned then + return false, PurchaseError.AlreadyOwn + end + + if not (productInfo.IsForSale or productInfo.IsPublicDomain) then + return false, PurchaseError.NotForSale + end + + if productInfo.IsLimited or productInfo.IsLimitedUnique then + if productInfo.Remaining == nil or productInfo.Remaining == 0 then + return false, PurchaseError.Limited + end + end + + if GetFFlagLuaPremiumCatalogIGPP() and productInfo.MinimumMembershipLevel == Enum.MembershipType.Premium.Value + and Players.LocalPlayer.MembershipType ~= Enum.MembershipType.Premium then + return false, PurchaseError.PremiumOnly + end + + if productInfo.ContentRatingTypeId == CONTENT_RATING_13_PLUS and Players.LocalPlayer:GetUnder13() then + return false, PurchaseError.Under13 + end + + local allowThirdPartyPurchase = true + if externalSettings.getLuaUseThirdPartyPermissions() then + -- Use Q2 2020 universe-wide permission to restrict access. + allowThirdPartyPurchase = externalSettings.isThirdPartyPurchaseAllowed() + else + -- TODO(DEVTOOLS-4227): Need to remove here before removing AllowThirdPartySales from DataModel/Workspace. + allowThirdPartyPurchase = Workspace.AllowThirdPartySales + end + + -- Restricting third party sales is only valid for Assets and Game Passes + if productInfo.ProductType ~= DEVELOPER_PRODUCT_TYPE + and restrictThirdParty + and not allowThirdPartyPurchase + then + local isGroupGame = game.CreatorType == Enum.CreatorType.Group + local isGroupAsset = productInfo.Creator.CreatorType == "Group" + local productCreator = tonumber(productInfo.Creator.CreatorTargetId) + + --[[ + Third party sales will be restricted if the creator of the asset + is not the creator of this game, whether the creators be users or groups + ]] + if productCreator ~= ROBLOX_CREATOR + and (isGroupGame ~= isGroupAsset or productCreator ~= game.CreatorId) + then + --[[ + Typically we avoid messaging the console for core scripts, but + in this case we want to inform developers why their game isn't + allowing sales while they're testing. + ]] + warn((THIRD_PARTY_WARNING):format( + productInfo.AssetId, + productCreator, + isGroupGame and "group" or "user" + )) + return false, PurchaseError.ThirdPartyDisabled + end + end + + -- No failed prerequisites + return true, nil +end + +return meetsPrerequisites diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/meetsPrerequisites.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/meetsPrerequisites.spec.lua new file mode 100644 index 0000000..d9a7cbe --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/Utils/meetsPrerequisites.spec.lua @@ -0,0 +1,140 @@ +return function() + local Root = script.Parent.Parent + local Workspace = game:GetService("Workspace") + + local PurchaseError = require(Root.Enums.PurchaseError) + + local GetFFlagLuaPremiumCatalogIGPP = require(Root.Flags.GetFFlagLuaPremiumCatalogIGPP) + local MockExternalSettings = require(Root.Test.MockExternalSettings) + + local meetsPrerequisites = require(script.Parent.meetsPrerequisites) + local defaultExternalSettings = MockExternalSettings.new(false, false, {}) + + local function getValidProductInfo() + return { + IsForSale = true, + IsPublicDomain = true, + IsLimited = false, + ContentRatingTypeId = 0, + AssetId = 0, + -- Assets have ProductType "User Product" + ProductType = "User Product", + Creator = { + CreatorType = "User", + CreatorTargetId = 1, + }, + } + end + + it("should return true if prerequisites are all met", function() + local productInfo = getValidProductInfo() + local met, _ = meetsPrerequisites(productInfo, false, false, defaultExternalSettings) + + expect(met).to.equal(true) + end) + + it("should return true if third party restrictions do not apply", function() + local productInfo = getValidProductInfo() + Workspace.AllowThirdPartySales = false + + -- Set creator id to game's creator id + productInfo.Creator.CreatorTargetId = game.CreatorId + + local met, _ = meetsPrerequisites(productInfo, false, true, defaultExternalSettings) + + expect(met).to.equal(true) + end) + + it("should return false if the player owns the item already", function() + local productInfo = getValidProductInfo() + local met, errorReason = meetsPrerequisites(productInfo, true, true, defaultExternalSettings) + + expect(met).to.equal(false) + expect(errorReason).to.equal(PurchaseError.AlreadyOwn) + end) + + it("should return false if the product is not for sale", function() + local productInfo = getValidProductInfo() + productInfo.IsForSale = false + productInfo.IsPublicDomain = false + + local met, errorReason = meetsPrerequisites(productInfo, false, false, defaultExternalSettings) + + expect(met).to.equal(false) + expect(errorReason).to.equal(PurchaseError.NotForSale) + end) + + it("should return false if no copies are available", function() + local productInfo = getValidProductInfo() + productInfo.IsLimited = true + productInfo.Remaining = 0 + + local met, errorReason = meetsPrerequisites(productInfo, false, false, defaultExternalSettings) + + expect(met).to.equal(false) + expect(errorReason).to.equal(PurchaseError.Limited) + end) + + it("should return false if third-party sales are restricted by permissions service", function() + local productInfo = getValidProductInfo() + + -- Set product creator to a number that is ~= to game.CreatorId and is > 1 + -- (which is considered ROBLOX, and is never restricted) + productInfo.Creator.CreatorTargetId = game.CreatorId + 2 + + local externalSettings = MockExternalSettings.new(false, false, { + LuaUseThirdPartyPermissions = true, + PermissionsServiceIsThirdPartyPurchaseAllowed = false, + }) + local met, errorReason = meetsPrerequisites(productInfo, false, true, externalSettings) + + expect(met).to.equal(false) + expect(errorReason).to.equal(PurchaseError.ThirdPartyDisabled) + end) + + it("should return true if third-party sales are allowed by permissions service", function() + local productInfo = getValidProductInfo() + + -- Set product creator to a number that is ~= to game.CreatorId and is > 1 + -- (which is considered ROBLOX, and is never restricted) + productInfo.Creator.CreatorTargetId = game.CreatorId + 2 + + local externalSettings = MockExternalSettings.new(false, false, { + LuaUseThirdPartyPermissions = true, + PermissionsServiceIsThirdPartyPurchaseAllowed = true, + }) + local met, errorReason = meetsPrerequisites(productInfo, false, true, externalSettings) + + expect(met).to.equal(true) + end) + + it("should return false if third-party sales are restricted", function() + local productInfo = getValidProductInfo() + Workspace.AllowThirdPartySales = false + + -- Set product creator to a number that is ~= to game.CreatorId and is > 1 + -- (which is considered ROBLOX, and is never restricted) + productInfo.Creator.CreatorTargetId = game.CreatorId + 2 + + local externalSettings = MockExternalSettings.new(false, false, { + LuaUseThirdPartyPermissions = false, + RestrictSales2 = true, + }) + local met, errorReason = meetsPrerequisites(productInfo, false, true, externalSettings) + + expect(met).to.equal(false) + expect(errorReason).to.equal(PurchaseError.ThirdPartyDisabled) + end) + + if GetFFlagLuaPremiumCatalogIGPP() then + it("should return false if premium purchase", function() + local productInfo = getValidProductInfo() + productInfo.MinimumMembershipLevel = 4 + + local met, errorReason = meetsPrerequisites(productInfo, false, true, defaultExternalSettings) + + expect(met).to.equal(false) + expect(errorReason).to.equal(PurchaseError.PremiumOnly) + end) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/connectToStore.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/connectToStore.lua new file mode 100644 index 0000000..60560b6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/connectToStore.lua @@ -0,0 +1,26 @@ +--[[ + Small wrapper for RoactRodux's connect function that + additionally exposes the original, unconnected component + for testing +]] +local Root = script.Parent + +local LuaPackages = Root.Parent +local RoactRodux = require(LuaPackages.RoactRodux) + +local function connectToStore(mapStateToProps, mapDispatchToProps) + return function(innerComponent) + local connectedComponent = RoactRodux.UNSTABLE_connect2( + mapStateToProps, + mapDispatchToProps + )(innerComponent) + + function connectedComponent.getUnconnected() + return innerComponent + end + + return connectedComponent + end +end + +return connectToStore \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/getPreviewImageUrl.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/getPreviewImageUrl.lua new file mode 100644 index 0000000..8d6b3e9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/getPreviewImageUrl.lua @@ -0,0 +1,32 @@ +local Root = script.Parent +local ContentProvider = game:GetService("ContentProvider") +local ItemType = require(Root.Enums.ItemType) + +local BASE_URL = string.gsub(ContentProvider.BaseUrl:lower(), "https?://m.", "https?://www.") +local THUMBNAIL_URL = BASE_URL.."thumbs/asset.ashx?assetid=" +local BUNDLE_THUMBNAIL_URL = BASE_URL.."outfit-thumbnail/image?userOutfitId=%s&width=100&height=100&format=png" + +local XBOX_DEFAULT_IMAGE = "rbxasset://textures/ui/Shell/Icons/ROBUXIcon@1080.png" + +--[[ + Depending on the type of item, get the proper preview image, sized correctly +]] +local function getPreviewImageUrl(productInfo, platform) + local imageId + + -- AssetId will only be populated if ProductInfo was from an asset + if productInfo.itemType == ItemType.Bundle then + return string.format(BUNDLE_THUMBNAIL_URL, productInfo.costumeId) + elseif productInfo.AssetId ~= nil and productInfo.AssetId ~= 0 then + imageId = productInfo.AssetId + elseif productInfo.IconImageAssetId ~= nil then + imageId = productInfo.IconImageAssetId + elseif platform == Enum.Platform.XBoxOne then + -- XBoxOne has its own default image if anything doesn't load + return XBOX_DEFAULT_IMAGE + end + + return THUMBNAIL_URL..tostring(imageId).."&x=100&y=100&format=png" +end + +return getPreviewImageUrl \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/init.lua new file mode 100644 index 0000000..86f18aa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/init.lua @@ -0,0 +1,22 @@ +local Root = script +local LuaPackages = Root.Parent +local CoreGui = game:GetService("CoreGui") +local RunService = game:GetService("RunService") + +local Roact = require(LuaPackages.Roact) + +local PurchasePromptApp = require(script.Components.PurchasePromptApp) + +local function mountPurchasePrompt() + if RunService:IsStudio() and RunService:IsEdit() then + return nil + end + + local handle = Roact.mount(Roact.createElement(PurchasePromptApp), CoreGui, "PurchasePromptApp") + + return handle +end + +return { + mountPurchasePrompt = mountPurchasePrompt, +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/strict.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/strict.lua new file mode 100644 index 0000000..f23357b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/strict.lua @@ -0,0 +1,27 @@ +--[[ + Locks a table from indexing or setting any keys that are not already defined + + Useful for constants or any unchanging data, where indexing non-existent values + is always a mistake +]] +local function invalidKey(self, key) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + tostring(self) + ) + + error(message, 2) +end + +local function strict(t, name) + return setmetatable(t, { + __index = invalidKey, + __newindex = invalidKey, + __tostring = function() + return name + end, + }) +end + +return strict \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/strict.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/strict.spec.lua new file mode 100644 index 0000000..5e0e7ad --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/purchase-prompt/strict.spec.lua @@ -0,0 +1,30 @@ +return function() + local strict = require(script.Parent.strict) + + it("should produce a table that throws errors when indexing invalid keys", function() + local object = strict({ + x = 1, + y = 3, + }, "object") + + expect(function() + print(object.z) + end).to.throw() + expect(function() + object.z = 1 + end).to.throw() + + expect(function() + object.x = 2 + end).never.to.throw() + end) + + it("should return the given name with the resulting table's tostring", function() + local object = strict({ + x = 1, + y = 3, + }, "object") + + expect(tostring(object)).to.equal("object") + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/t.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_purchase-prompt/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/lock.toml new file mode 100644 index 0000000..b317d20 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/rhodium" +version = "0.2.5" +commit = "7e623fc1d95af129f8fe9ca959160969e469e2ef" +source = "url+https://github.com/roblox/rhodium" diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/Element.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/Element.lua new file mode 100644 index 0000000..e9e083c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/Element.lua @@ -0,0 +1,272 @@ +local VirtualInput = require(script.Parent.VirtualInput) +local XPath = require(script.Parent.XPath) + +local Element = {} +Element.__index = Element + +function Element.new(argument) + local self = {} + if type(argument) == "string" then + self.path = XPath.new(argument) + elseif type(argument) == "table" and argument.__type == "XPath" then + self.path = argument + elseif type(argument) == "userdata" then + self.path = XPath.new(argument) + self.rbxInstance = argument + else + error("invalid parameter for element") + end + + setmetatable(self, Element) + local scrollNums = self:_scrollingFrames(self.rbxInstance) + + self.isInScrollingFrame = scrollNums ~= 0 + + return self +end + +function Element:getAttribute(name) + return self:getRbxInstance()[name] +end + +function Element:getLocation() + return self:getRbxInstance().AbsolutePosition +end + +function Element:getRect() + local topLeft = self:getLocation() + local bottomRight = self:getSize() + topLeft + return Rect.new(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y) +end + +function Element:getSize() + return self:getRbxInstance().AbsoluteSize +end + +function Element:getCenter() + return self:getLocation()+self:getSize()/2 +end + +function Element:getText() + return self:getRbxInstance().Text +end + +function Element:getAnchor() + return self:getLocation()+self.anchor +end + +-- Set anchor at offset from absolute position +function Element:setAnchor(offsetX, offsetY) + if(offsetX > self:getSize().x or offsetY > self:getSize().y or offsetX < 0 or offsetY < 0) then + error("Attempt to set anchor beyond element's bounds") + else + self.anchor = Vector2.new(offsetX, offsetY) + end +end + +function Element:isDisplayed() + return self:getRbxInstance().Visible +end + +function Element:isSelected() + return self:getRbxInstance().Selected +end + +function Element:getRbxInstance() + return self:waitForRbxInstance(self.path.waitDelay, self.path.waitTimeout) +end + +function Element:waitForRbxInstance(timeout, delay) + if self.rbxInstance == nil and self.path ~= nil then + self.path:setWait(timeout, delay) + self.rbxInstance = self.path:waitForFirstInstance() + end + + if self.rbxInstance and not self.anchor then + if pcall(function() local size = self.rbxInstance.AbsoluteSize end) then + self.anchor = self.rbxInstance.AbsoluteSize/2 + else + self.anchor = nil + end + end + + return self.rbxInstance +end + +function Element:_override(class) + for k, v in pairs(class) do + if not k:find("^_") then + self[k] = v + end + end +end + +function Element:centralizeInstance() + self:_centralizeInScrollingFrame(self:getRbxInstance()) +end + +function Element:centralize() + local instance = self:getRbxInstance() + if instance then + self:centralizeInstance() + else + self:centralizeWithInfiniteScrolling() + end +end + +function Element:_scrollingFrames(instance) + if instance == nil or instance == game then return 0 end + local num = self:_scrollingFrames(instance.Parent) + if instance.ClassName == "ScrollingFrame" then num = num + 1 end + return num +end + +function Element:_centralizeInScrollingFrame(child, parent) + if child == game then return end + parent = parent or child.Parent + if parent == game then return end + + if parent.ClassName == "ScrollingFrame" then + self:_centralizeInScrollingFrame(parent, parent.Parent) + -- this is computational error tolerate. + local threshold = 2 + + --first scroll down to make child appears neas screen, + --so that we can access child.AbsolutPosition property + local isChildInScreen = false + while not isChildInScreen do + + local prevChildPosition = child.AbsolutePosition + local prevCanvasPosition = parent.CanvasPosition + -- when scroll too much at one time, the element may move out side of screen immediately + -- its AbsoluteSize will not update. limit to 300 + local scrollDistance = Vector2.new(math.min(300, parent.AbsoluteSize.X), math.min(300, parent.AbsoluteSize.Y)) + parent.CanvasPosition = parent.CanvasPosition + scrollDistance + wait() + local deltaCanvas = (parent.CanvasPosition - prevCanvasPosition) + local isBottom = deltaCanvas.Magnitude <= threshold + local deltaChild = child.AbsolutePosition - prevChildPosition + isChildInScreen = isBottom or deltaChild.Magnitude > threshold + end + --second scroll to centerize the child, at most twice. + for _ = 1, 2 do + local frameCenter = parent.AbsolutePosition + parent.AbsoluteSize/2 + local childCenter = child.AbsolutePosition + child.AbsoluteSize/2 + local delta = childCenter - frameCenter + if delta.Magnitude <= threshold then break end + parent.CanvasPosition = parent.CanvasPosition + delta + wait() + end + else + self:_centralizeInScrollingFrame(child, parent.Parent) + end +end + +function Element:_scrollToFindInstance(scrollingFrame, absPath) +-- first reset scrollingFrame to zero position + scrollingFrame.CanvasPosition = Vector2.new(0, 0) + local width = scrollingFrame.AbsoluteSize.X + local height = scrollingFrame.AbsoluteSize.Y + + local isBottom = false + local instance + local threshold = 2 + while not isBottom do + wait(0.1) + --if find the element then return + instance = absPath:getFirstInstance() + if instance then return instance end + --scroll + local oldPosition = scrollingFrame.CanvasPosition + scrollingFrame.CanvasPosition = scrollingFrame.CanvasPosition + + Vector2.new(math.min(width, 300), math.min(height, 300)) + --wait for content to refresh + local delta = scrollingFrame.CanvasPosition - oldPosition + isBottom = delta.Magnitude < threshold + --if it is the bottom, then return not found + end + return nil +end + +function Element:centralizeWithInfiniteScrolling() + local instances, lastSeenIndex = self.path:getInstances() + if #instances > 0 then self:centralizeInstance() end + + local lastSeenPath = self.path:copy() + while #lastSeenPath.data > lastSeenIndex do + table.remove(lastSeenPath.data) + end + + local lastSeenInstance = lastSeenPath:getFirstInstance() + local lastScrollingFrame = nil + while true do + if lastSeenInstance.ClassName == "ScrollingFrame" then + lastScrollingFrame = lastSeenInstance + break + end + lastSeenInstance = lastSeenInstance.Parent + if lastSeenInstance == game then break end + end + if lastScrollingFrame == nil then return end + if self:_scrollToFindInstance(lastScrollingFrame, self.path) == nil then return end + self:_centralizeInScrollingFrame(self:getRbxInstance()) +end + +function Element:setPluginWindow() + local window = self.rbxInstance:FindFirstAncestorOfClass("DockWidgetPluginGui") + VirtualInput.setCurrentWindow(window) +end + +function Element:click(repeatCount) + self:centralize() + self:setPluginWindow() + + repeatCount = repeatCount or 1 + VirtualInput.Mouse.multiClick(self:getAnchor(), repeatCount) +end + +function Element:rightClick() + self:centralize() + self:setPluginWindow() + + VirtualInput.Mouse.rightClick(self:getAnchor()) +end + +function Element:mouseWheel(num) + self:centralize() + VirtualInput.Mouse.mouseWheel(self:getAnchor(), num) +end + +function Element:mouseDrag(xOffset, yOffset, duration) + self:centralize() + local posTo = self:getAnchor() + Vector2.new(xOffset, yOffset) + VirtualInput.Mouse.mouseDrag(self:getAnchor(), posTo, duration, true) +end + +function Element:mouseDragTo(posTo, duration) + self:centralize() + VirtualInput.Mouse.mouseDrag(self:getAnchor(), posTo, duration, true) +end + +function Element:sendKey(key) + self:setPluginWindow() + VirtualInput.Keyboard.hitKey(key) +end + +function Element:sendText(str) + self:click() + wait(0) + VirtualInput.Text.sendText(str) +end + +function Element:tap() + self:centralize() + VirtualInput.Touch.tap(self:getAnchor()) +end + +function Element:touchScroll(xOffset, yOffset, duration, multitouchId) + self:centralize() + VirtualInput.Touch.touchScroll(self:getAnchor(), xOffset, yOffset, duration, true, multitouchId) +end + +return Element diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/Element.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/Element.spec.lua new file mode 100644 index 0000000..9068103 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/Element.spec.lua @@ -0,0 +1,46 @@ +return function() + local XPath = require(script.Parent.XPath) + local Element = require(script.Parent.Element) + + local function makeInstance(className, props, children) + local instance = Instance.new(className) + if children then + for _, child in ipairs(children) do + child.Parent = instance + end + end + if props then + for k, v in pairs(props) do + instance[k] = v + end + end + return instance + end + + local root = makeInstance("Folder", + { + Name = "root", + Parent = workspace + }, { + makeInstance("Frame", {Name = "Frame"}, { + makeInstance("TextLabel", { + Text = "Label1" + }), + }), + }) + + describe("element creation", function() + it("valid element creation", function() + local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame]") + local validElement = Element.new(path:cat(XPath.new("TextLabel"))) + + expect(validElement:getRbxInstance()).to.be.ok() + end) + it("invalid element creation", function() + local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame]") + local invalidElement = Element.new(path:cat(XPath.new("TextLabel2"))) + + expect(invalidElement:getRbxInstance()).to.never.be.ok() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/GamePad.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/GamePad.lua new file mode 100644 index 0000000..412f16c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/GamePad.lua @@ -0,0 +1,105 @@ +local VirtualInputUtils = require(script.Parent.Parent.VirtualInputUtils) + +local VirtualInputManager = game:GetService("VirtualInputManager") + +local GamePad = {} +GamePad.__index = GamePad +local gamePadDeviceId = 123 + +GamePad.KeyCode = { + ButtonX = Enum.KeyCode.ButtonX, + ButtonY = Enum.KeyCode.ButtonY, + ButtonA = Enum.KeyCode.ButtonA, + ButtonB = Enum.KeyCode.ButtonB, + ButtonR1 = Enum.KeyCode.ButtonR1, + ButtonL1 = Enum.KeyCode.ButtonL1, + ButtonR2 = Enum.KeyCode.ButtonR2, + ButtonL2 = Enum.KeyCode.ButtonL2, + ButtonR3 = Enum.KeyCode.ButtonR3, + ButtonL3 = Enum.KeyCode.ButtonL3, + ButtonStart = Enum.KeyCode.ButtonStart, + ButtonSelect = Enum.KeyCode.ButtonSelect, + DPadLeft = Enum.KeyCode.DPadLeft, + DPadRight = Enum.KeyCode.DPadRight, + DPadUp = Enum.KeyCode.DPadUp, + DPadDown = Enum.KeyCode.DPadDown, + Thumbstick1 = Enum.KeyCode.Thumbstick1, + Thumbstick2 = Enum.KeyCode.Thumbstick2, +} + +function GamePad.new() + local self = {deviceId = gamePadDeviceId} + gamePadDeviceId = gamePadDeviceId + 1 + setmetatable(self, GamePad) + VirtualInputManager:HandleGamepadConnect(self.deviceId) + return self +end + +function GamePad:disconnect() + VirtualInputManager:HandleGamepadDisconnect(self.deviceId) +end + +function GamePad:pressButton(button) + VirtualInputManager:HandleGamepadButtonInput(self.deviceId, button, 1); +end + +function GamePad:releaseButton(button) + VirtualInputManager:HandleGamepadButtonInput(self.deviceId, button, 0); +end + +function GamePad:hitButton(button) + self:pressButton(button) + self:releaseButton(button) +end + +function GamePad:moveStickTo(stick, vec2) + VirtualInputManager:HandleGamepadAxisInput(self.deviceId, stick, vec2.x, vec2.y, 0) +end + +function GamePad:smoothMoveStickTo(stick, from, to, duration) + duration = duration or 0 + if duration == 0 then + self:moveStickTo(stick, to) + return + end + local passed = 0 + local function run(dt) + local ratio = passed / duration + passed = passed + dt + if ratio < 1 then + local pos = from + (to - from) * ratio + self:moveStickTo(stick, pos) + return false + else + self:moveStickTo(stick, to) + return true + end + end + VirtualInputUtils.__syncRun(run) +end + +function GamePad:swingStick(stick, pos, duration) + duration = duration or 0 + local origin = Vector2.new(0, 0) + self:moveStickTo(stick, origin) + self:smoothMoveStickTo(stick, origin, pos, duration / 2) + self:smoothMoveStickTo(stick, pos, origin, duration / 2) +end + +function GamePad:swingLeft(stick, duration) + self:swingStick(stick, Vector2.new(-1, 0), duration) +end + +function GamePad:swingRight(stick, duration) + self:swingStick(stick, Vector2.new(1, 0), duration) +end + +function GamePad:swingTop(stick, duration) + self:swingStick(stick, Vector2.new(0, 1), duration) +end + +function GamePad:swingDown(stick, duration) + self:swingStick(stick, Vector2.new(0, -1), duration) +end + +return GamePad \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Keyboard.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Keyboard.lua new file mode 100644 index 0000000..5f0553b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Keyboard.lua @@ -0,0 +1,24 @@ +local VirtualInputUtils = require(script.Parent.Parent.VirtualInputUtils) + +local VirtualInputManager = game:GetService("VirtualInputManager") + +local Keyboard = {} + +function Keyboard.SendKeyEvent(isPressed, keyCode, isRepeated) + VirtualInputManager:SendKeyEvent(isPressed, keyCode, isRepeated, VirtualInputUtils.getCurrentWindow()) +end + +function Keyboard.pressKey(keyCode) + Keyboard.SendKeyEvent(true, keyCode, false) +end + +function Keyboard.releaseKey(keyCode) + Keyboard.SendKeyEvent(false, keyCode, false) +end + +function Keyboard.hitKey(keyCode) + Keyboard.pressKey(keyCode) + Keyboard.releaseKey(keyCode) +end + +return Keyboard \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Mouse.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Mouse.lua new file mode 100644 index 0000000..ec821e7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Mouse.lua @@ -0,0 +1,118 @@ +local VirtualInputUtils = require(script.Parent.Parent.VirtualInputUtils) + +local VirtualInputManager = game:GetService("VirtualInputManager") +local InputVisualizer = require(script.Parent.Parent.InputVisualizer):new() + +local Mouse = {} + +function Mouse.sendMouseButtonEvent(x, y, button, isDown, repeatCount) + x, y = VirtualInputUtils.__handleGuiInset(x, y) + VirtualInputManager:SendMouseButtonEvent(x, y, button, isDown, VirtualInputUtils.getCurrentWindow(), repeatCount or 0) +end + +function Mouse.SendMouseMoveEvent(x, y) + x, y = VirtualInputUtils.__handleGuiInset(x, y) + VirtualInputManager:SendMouseMoveEvent(x, y, VirtualInputUtils.getCurrentWindow()) +end + +function Mouse.SendMouseWheelEvent(x, y, isForwardScroll) + x, y = VirtualInputUtils.__handleGuiInset(x, y) + VirtualInputManager:SendMouseWheelEvent(x, y, isForwardScroll, VirtualInputUtils.getCurrentWindow()) +end + +function Mouse.mouseWheel(vec2, num) + local forward = false + if num < 0 then + forward = true + num = -num + end + for _ = 1, num do + Mouse.SendMouseWheelEvent(vec2.x, vec2.y, forward) + end +end + +local function click(vec2, count, clickType) + InputVisualizer:click(vec2, VirtualInputUtils.getCurrentWindow()) + Mouse.sendMouseButtonEvent(vec2.x, vec2.y, clickType, true, count) + Mouse.sendMouseButtonEvent(vec2.x, vec2.y, clickType, false, count) +end + +local function multiClick(vec2, count, clickType) + local waiting = true + local repeatCount = 0 + return function() + if waiting then + waiting = false + return false + elseif count >= 1 then + click(vec2, repeatCount, clickType) + count = count - 1 + repeatCount = repeatCount + 1 + waiting = true + return false + elseif count == 0 then + return true + end + end +end + +function Mouse.click(vec2) + VirtualInputUtils.__syncRun(multiClick(vec2, 1, 0)) +end + +function Mouse.multiClick(vec2, count) + VirtualInputUtils.__syncRun(multiClick(vec2, count, 0)) +end + +function Mouse.rightClick(vec2) + VirtualInputUtils.__syncRun(multiClick(vec2, 1, 1)) +end + +function Mouse.mouseLeftDown(vec2) + Mouse.sendMouseButtonEvent(vec2.x, vec2.y, 0, true) +end + +function Mouse.mouseLeftUp(vec2) + Mouse.sendMouseButtonEvent(vec2.x, vec2.y, 0, false) +end + +function Mouse.mouseRightDown(vec2) + Mouse.sendMouseButtonEvent(vec2.x, vec2.y, 1, true) +end + +function Mouse.mouseRightUp(vec2) + Mouse.sendMouseButtonEvent(vec2.x, vec2.y, 1, false) +end + +function Mouse.mouseMove(vec2) + Mouse.SendMouseMoveEvent(vec2.X, vec2.Y) +end + +local function drag(posFrom, posTo, duration) + local passed = 0 + local started = false + return function(dt) + if not started then + Mouse.mouseLeftDown(posFrom) + started = true + else + passed = passed + dt + if duration and passed < duration then + local percent = passed / duration + local pos = (posTo - posFrom) * percent + posFrom + Mouse.mouseMove(pos) + else + Mouse.mouseMove(posTo) + Mouse.mouseLeftUp(posTo) + return true + end + end + return false + end +end + +function Mouse.mouseDrag(posFrom, posTo, duration) + VirtualInputUtils.__syncRun(drag(posFrom, posTo, duration)) +end + +return Mouse \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Text.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Text.lua new file mode 100644 index 0000000..4a6b5b6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Text.lua @@ -0,0 +1,11 @@ +local VirtualInputUtils = require(script.Parent.Parent.VirtualInputUtils) + +local VirtualInputManager = game:GetService("VirtualInputManager") + +local Text = {} + +function Text.sendText(str) + VirtualInputManager:sendTextInputCharacterEvent(str, VirtualInputUtils.getCurrentWindow()) +end + +return Text diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Touch.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Touch.lua new file mode 100644 index 0000000..585de78 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputTypes/Touch.lua @@ -0,0 +1,71 @@ +local VirtualInputUtils = require(script.Parent.Parent.VirtualInputUtils) + +local VirtualInputManager = game:GetService("VirtualInputManager") + +local Touch = {} +local defaultTouchId = 123456 + +function Touch.SendTouchEvent(touchId, state, x, y) + x, y = VirtualInputUtils.__handleGuiInset(x, y) + VirtualInputManager:SendTouchEvent(touchId, state, x, y) +end + +function Touch.touchStart(vec2, multitouchId) + local touchId = defaultTouchId + (multitouchId or 0) + Touch.SendTouchEvent(touchId, 0, vec2.x, vec2.y) +end + +function Touch.touchMove(vec2, multitouchId) + local touchId = defaultTouchId + (multitouchId or 0) + Touch.SendTouchEvent(touchId, 1, vec2.x, vec2.y) +end + +function Touch.touchStop(vec2, multitouchId) + local touchId = defaultTouchId + (multitouchId or 0) + Touch.SendTouchEvent(touchId, 2, vec2.x, vec2.y) +end + +local function smoothSwipe(posFrom, posTo, duration, multitouchId) + local passed = 0 + local started = false + local touchId = defaultTouchId + (multitouchId or 0) + return function(dt) + if not started then + Touch.touchStart(posFrom, touchId) + started = true + else + passed = passed + dt + if duration and passed < duration then + local percent = passed / duration + local pos = (posTo - posFrom) * percent + posFrom + Touch.touchMove(pos, touchId) + else + Touch.touchMove(posTo, touchId) + Touch.touchStop(posTo, touchId) + return true + end + end + return false + end +end + +function Touch.swipe(posFrom, posTo, duration, async, multitouchId) + local touchId = defaultTouchId + (multitouchId or 0) + if async == true then + VirtualInputUtils.__asyncRun(smoothSwipe(posFrom, posTo, duration, touchId)) + else + VirtualInputUtils.__syncRun(smoothSwipe(posFrom, posTo, duration, touchId)) + end +end + +function Touch.touchScroll(startPos, xOffset, yOffset, duration, async, multitouchId) + local posTo = startPos + Vector2.new(xOffset, yOffset) + Touch.swipe(startPos, posTo, duration, async, multitouchId) +end + +function Touch.tap(vec2) + Touch.touchStart(vec2) + Touch.touchStop(vec2) +end + +return Touch \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputVisualizer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputVisualizer.lua new file mode 100644 index 0000000..7bdfec0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/InputVisualizer.lua @@ -0,0 +1,79 @@ +local InputVisualizer = {} +InputVisualizer.__index = InputVisualizer + +local TweenService = game:GetService("TweenService") +local Debris = game:GetService("Debris") +local UserInputService = game:GetService("UserInputService") + +function InputVisualizer.new() + local self = {} + local state, guiRoot = pcall(function() return game.CoreGui.Parent.CoreGui end) + if state == false then + local LocalPlayer = game.Players.LocalPlayer + while LocalPlayer == nil do + LocalPlayer = game.Players.LocalPlayer + wait() + end + guiRoot = LocalPlayer.PlayerGui + end + + local GuiName = "InputVisualizer" + if guiRoot:FindFirstChild(GuiName) == nil then + local screenGui = Instance.new("ScreenGui") + screenGui.Name = GuiName + screenGui.DisplayOrder = 1000000 + screenGui.Parent = guiRoot + end + guiRoot = guiRoot[GuiName] + self.guiRoot = guiRoot + + setmetatable(self, InputVisualizer) + return self +end + +function InputVisualizer:onInputBegan(input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self:click(Vector2.new(input.Position.X, input.Position.Y)) + elseif input.UserInputType == Enum.UserInputType.Touch then + self:click(Vector2.new(input.Position.X, input.Position.Y)) + end +end + +function InputVisualizer:click(vec2, pluginGui) + local delay = 0.5 + local image = nil + if image == nil then + image = Instance.new("ImageLabel") + image.Image = "rbxassetid://1549893588" + image.BackgroundTransparency = 1 + image.Parent = pluginGui or self.guiRoot + image.Size = UDim2.new(0, 20, 0, 20) + image.Name = "MouseClick" + image.ZIndex = 10 + end + + image.Visible = true + image.Position = UDim2.new(0, vec2.X-image.Size.X.Offset/2, 0, vec2.Y-image.Size.Y.Offset/2) + image.ImageTransparency = 0 + local goal = {ImageTransparency = 1} + local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false) + local tween = TweenService:Create(image, tweenInfo, goal) + tween:Play() + Debris:AddItem(image, delay) +end + +function InputVisualizer:enable() + self.handler = UserInputService.InputBegan:connect( + function(input, gameProcessed) + self:onInputBegan(input, gameProcessed) + end) +end + +function InputVisualizer:disable() + if self.handler then + self.handler:Disconnect() + end + self.handler = nil +end + +return InputVisualizer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/RemoteRhodium.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/RemoteRhodium.lua new file mode 100644 index 0000000..3cc4488 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/RemoteRhodium.lua @@ -0,0 +1,76 @@ +local RemoteRhodium = {} +local HttpService = game:getService("HttpService") + +local rootPath = nil + +local function split(s, delimiter) + local result = {}; + while(s:len()>0) do + local pos, stop = string.find(s,delimiter,1,true) + if pos == nil then + table.insert(result,s) + s = "" + else + table.insert(result,string.sub(s,1,pos-1)) + s = string.sub(s,stop+1) + if s == "" then table.insert(result,"") end + end + end + return result +end + +function RemoteRhodium.setCommandPath(p) + rootPath = p +end + +local function onCommand(command) + assert(type(command) == "string", "command should be a string") + + local op = command:find("(", 1, true) + local cp = command:reverse():find(")", 1, true) + if cp ~= nil then + cp = #command - cp + 1 + end + + local args = {} + if op then + assert(cp, "invalid syntex, expecting \")\"") + local argStr = command:sub(op+1, cp-1) + if #argStr > 0 then + args = HttpService:JSONDecode("["..argStr.."]") + end + command = command:sub(1, op-1) + end + + local subPathTab = split(command, ".") + + local instance = rootPath + for i = 1, #subPathTab do + local p = subPathTab[i] + instance = instance[p] + if instance == nil then + error("can not find " .. p) + end + if type(instance) == "userdata" and instance.ClassName == "ModuleScript" then + instance = require(instance) + end + end + if type(instance) ~= "function" then + error("target is not a function") + end + return instance(unpack(args)) +end + +function RemoteRhodium.setCommandPath(p) + rootPath = p +end + +local success, RhodiumService = pcall(function() + return game:getService("RhodiumService") + end) + +if success then + RhodiumService.onCommand = onCommand +end + +return RemoteRhodium \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInput.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInput.lua new file mode 100644 index 0000000..37e6933 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInput.lua @@ -0,0 +1,20 @@ +local Keyboard = require(script.Parent.InputTypes.Keyboard) +local Mouse = require(script.Parent.InputTypes.Mouse) +local Touch = require(script.Parent.InputTypes.Touch) +local Text = require(script.Parent.InputTypes.Text) +local GamePad = require(script.Parent.InputTypes.GamePad) + +local VirtualInputUtils = require(script.Parent.VirtualInputUtils) + +local VirtualInput = { + Keyboard = Keyboard, + Mouse = Mouse, + Touch = Touch, + Text = Text, + GamePad = GamePad, + + setCurrentWindow = VirtualInputUtils.setCurrentWindow, + getCurrentWindow = VirtualInputUtils.getCurrentWindow, +} + +return VirtualInput \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInput.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInput.spec.lua new file mode 100644 index 0000000..06b5591 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInput.spec.lua @@ -0,0 +1,14 @@ +return function() + describe("VirtualInput", function() + it("should load", function() + local VirtualInput = require(script.Parent.VirtualInput) + + expect(VirtualInput).to.be.ok() + expect(VirtualInput.Keyboard).to.be.ok() + expect(VirtualInput.Mouse).to.be.ok() + expect(VirtualInput.Touch).to.be.ok() + expect(VirtualInput.GamePad).to.be.ok() + expect(VirtualInput.Text).to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInputUtils.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInputUtils.lua new file mode 100644 index 0000000..dddaa9d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/VirtualInputUtils.lua @@ -0,0 +1,64 @@ +local RunService = game:GetService("RunService") +local GuiService = game:GetService("GuiService") + +local runSet = {} +local signals = {} + +RunService.Heartbeat:Connect(function(dt) + local finishedList = {} + for runable, _ in pairs(runSet) do + local finished = runable(dt) + if finished then + table.insert(finishedList, runable) + end + end + + for _, toDelete in ipairs(finishedList) do + runSet[toDelete] = nil + end + + for signal, _ in pairs(signals) do + if signal == false then + signals[signal] = nil + else + signal:Fire(dt) + end + end +end) + +local VirtualInputUtils = {} + +local currentWindow = nil + +function VirtualInputUtils.setCurrentWindow(window) + local old = currentWindow + currentWindow = window + return old +end + +function VirtualInputUtils.getCurrentWindow() + return currentWindow +end + +function VirtualInputUtils.__asyncRun(runable) + runSet[runable] = true +end + +function VirtualInputUtils.__syncRun(runable) + local signal = Instance.new("BindableEvent") + signals[signal] = true + local dt = 0 + while true do + local finished = runable(dt) + if finished then break end + dt = signal.Event:Wait() + end + signals[signal] = false +end + +function VirtualInputUtils.__handleGuiInset(x, y) + local guiOffset, _ = GuiService:GetGuiInset() + return x + guiOffset.X, y + guiOffset.Y +end + +return VirtualInputUtils \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/XPath.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/XPath.lua new file mode 100644 index 0000000..4bb4e45 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/XPath.lua @@ -0,0 +1,622 @@ +local XPath = {} +XPath.__index = XPath +XPath.__type = "XPath" + +local specialChars = [[\.=[],]] +local specialCharMap = {} +for i = 1, #specialChars do + local ch = specialChars:sub(i, i) + specialCharMap[ch] = true +end + +function XPath.addSlash(str) + local tab = {} + for i = 1, str:len() do + local ch = str:sub(i, i) + if specialCharMap[ch] then + table.insert(tab, "\\") + end + table.insert(tab, ch) + end + return table.concat(tab) +end + +function XPath.removeSlash(str) + local tab = {} + local isBackSlash = false + for i = 1, str:len() do + local ch = str:sub(i, i) + if ch == "\\" and isBackSlash == false then + isBackSlash = true + else + if isBackSlash == true then + isBackSlash = false + end + table.insert(tab, ch) + end + end + return table.concat(tab) +end + +local function splitByCharWithSlash(s, token) + local result = {} + if s == nil or s == "" then return result end + local isBackSlash = false + local lastIndex = 1 + for i = 1, s:len() do + local ch = s:sub(i, i) + if ch == "\\" and isBackSlash == false then + isBackSlash = true + else + if isBackSlash == true then + isBackSlash = false + else + if ch == token then + table.insert(result, (s:sub(lastIndex, i-1))) + lastIndex = i+1 + end + end + end + end + table.insert(result, (s:sub(lastIndex, s:len()))) + return result +end + +local function deepCopy(t) + local t2 = {} + for k, v in pairs(t) do + if type(v) == "table" then + t2[k] = deepCopy(v) + else + t2[k] = v + end + end + return t2 +end + +function XPath.new(obj, root) + local self = {data = {}, root = root, waitDelay = 0.2, waitTimeOut = 2} + setmetatable(self, XPath) + + if type(obj) == "string" then + self:fromString(obj) + elseif type(obj) == "userdata" then + local current = obj + while current do + local name = current.Name + if current.ClassName == "DataModel" then + name = "game" + end + table.insert(self.data, 1, {name = name}) + current = current.Parent + end + elseif getmetatable(obj).__type == XPath.__type then + return obj:copy() + else + error("unknown parameter ", obj) + end + return self +end + +function XPath:size() + return #self.data +end + +function XPath:mergeFilter(index, additionalFilter) + if index > self:size() then error("bad index") end + local filter = self.data[index].filter or {} + local filterDict = {} + for _, item in ipairs(filter) do + filterDict[item.key] = item.value + end + if additionalFilter then + for _, item in ipairs(additionalFilter) do + filterDict[item.key] = tostring(item.value) + end + end + local newFilter = {} + for k, v in pairs(filterDict) do + table.insert(newFilter, {key = k, value = v}) + end + self.data[index].filter = newFilter + return self +end + +function XPath:fromString(str) + local inBracket = false + local isBackSlash = false + local data = {} + local lastIndex = 1 + str = str .. "." + for i = 1, str:len() do + local ch = str:sub(i, i) + if ch == "\\" and isBackSlash == false then + isBackSlash = true + else + if isBackSlash == true then + isBackSlash = false + else + if ch == "." then + if not inBracket then + table.insert(data, {name = str:sub(lastIndex, i-1)}) + lastIndex = i+1 + end + elseif ch == "[" then + if inBracket == true then + error("no nested bracket allowed: " .. str) + end + inBracket = true + elseif ch == "]" then + if not inBracket then + error("unbalanced brackets: " .. str) + end + inBracket = false + end + end + end + end + if inBracket == true then error("unbalanced brackets: " .. str) end + + for i = 1, #data do + local name, filters = data[i].name:match("%s*(.*[^\\])%[(.*[^\\])%]%s*") + if name == nil then + filters = "" + name = data[i].name + end + if name ~= nil then + data[i].name = XPath.removeSlash(name) + local filterArray = splitByCharWithSlash(filters, ",") + local filterObjs = {} + for _, filter in ipairs(filterArray) do + local key, value = filter:match("^%s*(.-[^\\])%s*=%s*(.-)%s*$") + if key then + table.insert(filterObjs, {key = key, value = value}) + end + end + data[i].filter = filterObjs + end + end + self.data = data +end + +function XPath:copy() + local result = deepCopy(self) + setmetatable(result, XPath) + return result +end + + +function XPath:parent() + local newOne = self:copy() + if #newOne.data <= 1 then + --error("this is the root") + return newOne + else + table.remove(newOne.data, #newOne.data) + end + return newOne +end + +function XPath:_itemToString(item) + local result = XPath.addSlash(item.name) + if item.filter and #item.filter > 0 then + local filter = {} + for _, v in ipairs(item.filter) do + table.insert(filter, v.key .. " = " ..v.value) + end + result = result .. "[" .. table.concat(filter, ", ") .. "]" + end + return result +end + +function XPath:toString(arg) + if arg == nil then + local tab = {} + for _, item in ipairs(self.data) do + table.insert(tab, self:_itemToString(item)) + end + return table.concat(tab, ".") + elseif type(arg) == "number" then + if arg < 0 then arg = self:size() + arg + 1 end + if arg > self:size() or arg < 1 then error("invalid index") end + return self:_itemToString(self.data[arg]) + elseif type(arg) == "table" then + return self:_itemToString(arg) + end +end + +function XPath:hasChild(child) + return child:relative(self) ~= nil +end + +function XPath:relative(root) + if self:size()<#root.data then return nil end + local newRoot = root:copy() + local newSelf = self:copy() + + while #newRoot.data > 0 do + if newRoot.data[1].name ~= newSelf.data[1].name then return nil end + table.remove(newRoot.data, 1) + table.remove(newSelf.data, 1) + end + return newSelf +end + + +function XPath:cat(path) + local newOne = self:copy() + for _, k in ipairs(path.data) do + table.insert(newOne.data, k) + end + return newOne +end + +function XPath:clearFilter() + for i = 1, self:size() do + self.data[i].filter = nil + end + return self +end + +local function getProperty(instance, property) + local state, result = pcall(function() return instance[property] end ) + return state == true and result or nil +end + +local function propertyEqual(lhs, rhs) + return tostring(lhs) == tostring(rhs) +end + +local function propertyMatch(prop, expr) + prop = tostring(prop) + expr = tostring(expr) + return expr == "*" or prop == expr +end + +local function findChildrenByName(instance, name) + local children = instance:GetChildren() + local result = {} + for _, child in ipairs(children) do + if propertyMatch(getProperty(child, "Name"), name) then + table.insert(result, child) + end + end + -- for game.Players, their is no child "LocalPlayer", but you can access it. + if #result == 0 then + local instance = getProperty(instance, name) + if instance then + table.insert(result, instance) + end + end + return result +end + +local function findChildren(instances, name) + local result = {} + for _, instance in ipairs(instances) do + local children = findChildrenByName(instance, name) + for _, child in ipairs(children) do + table.insert(result, child) + end + end + return result +end + +local function findCandidates(instances, path) + for _, name in ipairs(path) do + instances = findChildren(instances, name) + end + return instances +end + +local function passFilter(instance, filter) + local key = filter.key + local path, propertyName = key:match("^%.?(.*[^\\])%.(%w-)$") + if propertyName == nil then + path = "" + propertyName = key:match("^%.?(%w-)$") + end + local pathData = splitByCharWithSlash(path, ".") + for i = 1, #pathData do pathData[i] = XPath.removeSlash(pathData[i]) end + local canditates = findCandidates({instance}, pathData) + for _, canditate in ipairs(canditates) do + local property = getProperty(canditate, propertyName) + if propertyMatch(property, XPath.removeSlash(filter.value)) then + return true + end + end + return false +end + +local function passFilters(instance, filters) + for _, filter in ipairs(filters) do + if not passFilter(instance, filter) then + return false + end + end + return true +end + +local function applyFilters(canditates, filters) + if filters == nil then return canditates end + local result = {} + for _, canditate in ipairs(canditates) do + if passFilters(canditate, filters) then + table.insert(result, canditate) + end + end + return result +end + +function XPath:getFirstInstance() + local instances = self:getInstances() + if #instances == 0 then return nil else return instances[1] end +end + +function XPath:getInstances() + if self:size() <1 then error("instance " .. self:toString() .. " does not exist") end + + local rootInstance = nil + local rootName = self.data[1].name + + if rootName == "game" then + rootInstance = game + elseif rootName == "PluginGuiService" then + rootInstance = game:GetService("PluginGuiService") + end + + if self.root == nil and rootInstance == nil then + error("instance " .. self:toString() .. " does not exist") + end + + local instances = {self.root or rootInstance} + local i = self.root and 1 or 2 + while i <= self:size() do + local name = self.data[i].name + local filters = self.data[i].filter + local candidates = findChildren(instances, name) + instances = applyFilters(candidates, filters) + if #instances == 0 then return instances, i-1 end + i = i + 1 + end + return instances, i-1 +end + +function XPath:setWait(timeOut, delay) + self.waitDelay = delay or self.waitDelay + self.waitTimeOut = timeOut or self.waitTimeOut + return self +end + +function XPath:waitFor(execute, condition, delay, timeOut) + delay = delay or self.waitDelay + timeOut = timeOut or self.waitTimeOut + timeOut = tick()+timeOut + while true do + local result = execute() + if condition(result) then + return result, true + end + wait(delay) + if tick() > timeOut then return result, false end + end +end + +function XPath:waitForFirstInstance() +-- return self:waitForInstances(function(instances) return #instances >0 end)[1] + local instances = self:waitForNInstances(1) + if instances ~= nil and #instances > 0 then return instances[1] end + return nil +end + +function XPath:waitForInstances(condition) + if type(condition) ~= "function" then error("arg #1 should be a function") end + return self:waitFor(function() + return self:getInstances() + end, condition) +end + +function XPath:waitForDisappear() + local _, state = self:waitForInstances(function(instances) return #instances == 0 end) + return state == true +end + +function XPath:waitForNInstances(n) + return self:waitForInstances(function(instances) + return #instances >= n + end) +end + +local function makeInstance(className, props, children) + local instance = Instance.new(className) + if children then + for _, child in ipairs(children) do + child.Parent = instance + end + end + if props then + for k, v in pairs(props) do + instance[k] = v + end + end + return instance +end + +local function test() + local function closeTo(lhs, rhs, err) + return math.abs(lhs-rhs) <= err + end + + local specialChars = [[special chars !"#$%&'()*+,-./:;<=>?@[]\^_`{|}~]] + local convertedSpecialChars = [[special chars !"#$%&'()*+\,-\./:;<\=>?@\[\]\\^_`{|}~]] + + local root + if game.Workspace:FindFirstChild("root") == nil then + root = makeInstance("Folder", + { + Name = "root", + Parent = game.Workspace + }, { + makeInstance("Frame", {Name = "Frame"}, { + makeInstance("TextButton", { + Text = "Button1" + }), + makeInstance("TextLabel", { + Text = "Label1" + }), + makeInstance("ImageButton", { + + }) + }), + makeInstance("Frame", {Name = "Frame"}, { + makeInstance("TextButton", { + Text = "Button2" + }), + makeInstance("TextLabel", { + Text = "Label2" + }) + }), + makeInstance("Frame", {Name = "Frame"}, { + makeInstance("TextLabel", {Text = "Label3"}), + makeInstance("Frame", {Name = specialChars}, { + makeInstance("TextButton", {Name = "TextButton3"}) + }), + makeInstance("TextLabel", {Name = "SpecialCharLabel", Text = specialChars}) + }) + }) + end + + local containerPath = XPath.new("game.Workspace.root.Frame") + + local getContainerDetail = function(root) + return { + textButton = XPath.new("TextButton", root), + textLabel = XPath.new("TextLabel", root), + } + end + + local createSearch = function(container, relativePath, property, value) + local rootPath = container:copy() + local filter = {{key = "." .. relativePath:toString().."."..property, value = value}} + rootPath:mergeFilter(rootPath:size(), filter) + return rootPath + end + + local rootPath = createSearch(containerPath, getContainerDetail().textButton, "Text", "Button2") + print("createSearch:", rootPath:toString()) + local rootInstance = rootPath:waitForFirstInstance() + assert(rootInstance) + + local containerDetail = getContainerDetail(rootInstance) + + local label2 = containerDetail.textLabel:waitForFirstInstance() + assert(label2.Text == "Label2") + + print("relative path test") + + local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2]") + local instance = path:getFirstInstance() + assert(instance) + local relativePath = XPath.new("TextButton", instance) + local instance = relativePath:getFirstInstance() + assert(instance.Text == "Button2") + + local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame].TextLabel") + local instance = path:getFirstInstance() + assert(instance.Text=="Label2") + local newPathString = XPath.new(instance):toString() + assert(newPathString=="game.Workspace.root.Frame.TextLabel") + + local pathStr = "game.Workspace.root.Frame[."..convertedSpecialChars..".TextButton3.Name = TextButton3].TextLabel" + local path = XPath.new(pathStr) + assert(path:toString()==pathStr) + local instance = path:getFirstInstance() + assert(instance.Text=="Label3") + + local pathStr = "game.Workspace.root.Frame[.SpecialCharLabel.Text = "..convertedSpecialChars.."].TextLabel" + local path = XPath.new(pathStr) + assert(path:toString()==pathStr) + local instance = path:getFirstInstance() + assert(instance.Text=="Label3") + + local pathStr = "game.Workspace.root.Frame."..convertedSpecialChars..".TextButton3" + local path = XPath.new(pathStr) + local newPathStr = path:toString() + assert(newPathStr==pathStr) + local instance = path:getFirstInstance() + assert(instance.Name=="TextButton3") + + local rootPath = XPath.new("game.Workspace.root") + local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame].TextLabel") + local relativePath = path:relative(rootPath) + relativePath:clearFilter() + + print("testing getFirstInstance()") + print(path:toString()) + local instance = path:getFirstInstance() + assert(instance.Text == "Label2") + + print("testing wildcard *") + path = XPath.new("game.Workspace.root.*[.TextButton.Text = Button2].TextLabel") + instance = path:getFirstInstance() + assert(instance.Text == "Label2") + + path = XPath.new("game.Workspace.root.*[.ImageButton.Name = *].TextLabel") + instance = path:getFirstInstance() + assert(instance.Text == "Label1") + + local timeBefore = nil + print("testing timeout waitForNInstances() ") + path = XPath.new("game.Workspace.root.Frame.TextLabel") + timeBefore = tick() + local instances, state = path:setWait(2):waitForNInstances(5) + assert(closeTo(tick() - timeBefore, 2, 0.5)) + assert(state == false) + + print("testing normal waitForNInstances() ") + timeBefore = tick() + spawn(function() + wait(2) + makeInstance("Frame", { + Name = "Frame", + Parent = game.Workspace.root + }, { + makeInstance("TextButton", { + Text = "Button3" + }), + makeInstance("TextLabel", { + Text = "Label3" + }) + }) + end) + local instances = path:setWait(5):waitForNInstances(5) + assert(closeTo(tick() - timeBefore, 2, 0.5)) + assert(#instances >= 5) + + print("testing getFirstInstance() ") + path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button3]") + instance = path:getFirstInstance() + assert(instance.TextButton.Text == "Button3") + + print("testing timeout waitForDisappear() ") + timeBefore = tick() + local notExist = path:setWait(2):waitForDisappear() + assert(notExist == false) + assert(closeTo(tick() - timeBefore, 2, 0.5)) + + print("testing normal waitForDisappear() ") + timeBefore = tick() + spawn(function() + wait(2) + instance:Destroy() + end) + local notExist = path:setWait(5):waitForDisappear() + assert(closeTo(tick() - timeBefore, 2, 0.5)) + assert(notExist == true) + + print("test finised") +end + +--test() + +return XPath \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/XPath.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/XPath.spec.lua new file mode 100644 index 0000000..250d5d0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/XPath.spec.lua @@ -0,0 +1,96 @@ +return function() + local XPath = require(script.Parent.XPath) + + local function makeInstance(className, props, children) + local instance = Instance.new(className) + if children then + for _, child in ipairs(children) do + child.Parent = instance + end + end + if props then + for k, v in pairs(props) do + instance[k] = v + end + end + return instance + end + + local specialChars = [[special chars !"#$%&'()*+,-./:;<=>?@[]\^_`{|}~]] + local convertedSpecialChars = [[special chars !"#$%&'()*+\,-\./:;<\=>?@\[\]\\^_`{|}~]] + + local root = makeInstance("Folder", + { + Name = "root", + Parent = workspace + }, { + makeInstance("Frame", {Name = "Frame"}, { + makeInstance("TextButton", { + Text = "Button1" + }), + makeInstance("TextLabel", { + Text = "Label1" + }), + makeInstance("ImageButton", { + + }) + }), + makeInstance("Frame", {Name = "Frame"}, { + makeInstance("TextButton", { + Text = "Button2" + }), + makeInstance("TextLabel", { + Text = "Label2" + }) + }), + makeInstance("Frame", {Name = "Frame"}, { + makeInstance("TextLabel", {Text = "Label3"}), + makeInstance("Frame", {Name = specialChars}, { + makeInstance("TextButton", {Name = "TextButton3"}) + }), + makeInstance("TextLabel", {Name = "SpecialCharLabel", Text = specialChars}) + }) + }) + + describe("basic tests", function() + it("getFirstInstance should work", function() + local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame].TextLabel") + local instance = path:getFirstInstance() + expect(instance.Text).to.equal("Label2") + expect(XPath.new(instance):toString()).to.equal("game.Workspace.root.Frame.TextLabel") + end) + it("wildcard should work", function() + local path = XPath.new("game.Workspace.root.*[.TextButton.Text = Button2].TextLabel") + local instance = path:getFirstInstance() + expect(instance.Text).to.equal("Label2") + end) + it("wildcard on property should work", function() + local path = XPath.new("game.Workspace.root.*[.ImageButton.Name = *].TextLabel") + local instance = path:getFirstInstance() + expect(instance.Text).to.equal("Label1") + end) + end) + describe("should work with special chars", function() + it("should work with special chars in path", function() + local pathStr = "game.Workspace.root.Frame."..convertedSpecialChars..".TextButton3" + local path = XPath.new(pathStr) + expect(path:toString()).to.equal(pathStr) + local instance = path:getFirstInstance() + expect(instance.Name).to.equal("TextButton3") + end) + it("should work with special chars in filter keys", function() + local pathStr = "game.Workspace.root.Frame[."..convertedSpecialChars..".TextButton3.Name = TextButton3].TextLabel" + local path = XPath.new(pathStr) + expect(path:toString()).to.equal(pathStr) + local instance = path:getFirstInstance() + expect(instance.Text).to.equal("Label3") + end) + it("should work with special chars in filter values", function() + local pathStr = "game.Workspace.root.Frame[.SpecialCharLabel.Text = "..convertedSpecialChars.."].TextLabel" + local path = XPath.new(pathStr) + expect(path:toString()).to.equal(pathStr) + local instance = path:getFirstInstance() + expect(instance.Text).to.equal("Label3") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/init.lua new file mode 100644 index 0000000..04acd1e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/init.lua @@ -0,0 +1,11 @@ +local Element = require(script.Element) +local VirtualInput = require(script.VirtualInput) +local XPath = require(script.XPath) + +local Rhodium = { + Element = Element, + VirtualInput = VirtualInput, + XPath = XPath, +} + +return Rhodium \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/init.spec.lua new file mode 100644 index 0000000..4c2c632 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rhodium/rhodium/init.spec.lua @@ -0,0 +1,12 @@ +return function() + describe("Rhodium", function() + it("should load", function() + local Rhodium = require(script.Parent) + + expect(Rhodium).to.be.ok() + expect(Rhodium.Element).to.be.ok() + expect(Rhodium.XPath).to.be.ok() + expect(Rhodium.VirtualInput).to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/lock.toml new file mode 100644 index 0000000..e00a519 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/lock.toml @@ -0,0 +1,9 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/roact-fit-components" +version = "1.2.5" +commit = "b784928fdef64215d38e2febae28412a3b2a520b" +source = "url+https://github.com/roblox/roact-fit-components" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameHorizontal.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameHorizontal.lua new file mode 100644 index 0000000..280f847 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameHorizontal.lua @@ -0,0 +1,21 @@ +local root = script.Parent +local Packages = root.Parent + +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) + +local FitFrameOnAxis = require(script.Parent.FitFrameOnAxis) + +return function(props) + props = props or {} + local height = props.height + + local filteredProps = Cryo.Dictionary.join(props, { + axis = FitFrameOnAxis.Axis.Horizontal, + minimumSize = UDim2.new(UDim.new(0, 0), height), + + height = Cryo.None, + }) + + return Roact.createElement(FitFrameOnAxis, filteredProps) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameOnAxis.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameOnAxis.lua new file mode 100644 index 0000000..bb20c26 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameOnAxis.lua @@ -0,0 +1,182 @@ +local root = script.Parent +local Packages = root.Parent + +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) + +local Rect = require(root.Rect) + +local FitFrameOnAxis = Roact.PureComponent:extend("FitFrameOnAxis") +FitFrameOnAxis.Axis = { + Horizontal = {}, + Vertical = {}, + Both = {}, +} + +FitFrameOnAxis.defaultProps = { + axis = FitFrameOnAxis.Axis.Vertical, + minimumSize = UDim2.new(UDim.new(0, 0), UDim.new(0, 0)), + margin = Rect.square(0), + + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + ImageSet = {}, + VerticalAlignment = Enum.VerticalAlignment.Top, + contentPadding = UDim.new(0, 0), + textProps = nil, +} + +function FitFrameOnAxis:init() + self.layoutRef = Roact.createRef() + self.frameRef = self.props[Roact.Ref] or Roact.createRef() + + self.onResize = function() + local currentLayout = self.layoutRef.current + local currentFrame = self.frameRef.current + if not currentFrame or not currentLayout then + return + end + + currentFrame.Size = self:__getSize(currentLayout) + end +end + +function FitFrameOnAxis:render() + assert(self.props.Size == nil, "Size is not a valid property of FitFrameOnAxis. Did you mean `minimumSize`?") + local children = self.props[Roact.Children] or {} + local filteredProps = self:__getFilteredProps() + + local instanceType = self.props.onActivated and "ImageButton" or "ImageLabel" + + children = Cryo.Dictionary.join(children, { + ["$layout"] = Roact.createElement("UIListLayout", { + FillDirection = self.props.FillDirection, + HorizontalAlignment = self.props.HorizontalAlignment, + Padding = self.props.contentPadding, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = self.props.VerticalAlignment, + + [Roact.Change.AbsoluteContentSize] = self.onResize, + [Roact.Ref] = self.layoutRef, + }), + ["$margin"] = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, self.props.margin.left), + PaddingRight = UDim.new(0, self.props.margin.right), + PaddingTop = UDim.new(0, self.props.margin.top), + PaddingBottom = UDim.new(0, self.props.margin.bottom), + }), + }) + + if self.props.textProps then + return Roact.createElement(instanceType, filteredProps, { + TextLabel = Roact.createElement("TextLabel", Cryo.Dictionary.join(self.props.textProps, { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + })), + + ChildFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + }, children), + }) + else + return Roact.createElement(instanceType, filteredProps, children) + end +end + +function FitFrameOnAxis:didMount() + self.onResize() +end + +function FitFrameOnAxis:didUpdate() + self.onResize() +end + +function FitFrameOnAxis:__getFilteredProps() + -- Will return a new prop map after removing + -- Roact.Children and any defaultProps in an effort + -- to only return safe Roblox Instance "ImageLabel" + -- properties that may be present. + local filteredProps = Cryo.Dictionary.join(self.props.ImageSet, { + [Roact.Ref] = self.frameRef, + [Roact.Event.Activated] = self.props.onActivated, + }) + + for property, _ in pairs(FitFrameOnAxis.defaultProps) do + filteredProps[property] = Cryo.None + end + + filteredProps.textProps = Cryo.None + + return Cryo.Dictionary.join(self.props, filteredProps, { + onActivated = Cryo.None, + [Roact.Children] = Cryo.None, + }) +end + +function FitFrameOnAxis:__getSize(currentLayout) + if self.props.axis == FitFrameOnAxis.Axis.Both then + return self:__getBothAxisSize(currentLayout) + else + -- Arrangement of UDims are flip-flopped based + -- on which axis is our primary axis + local axisUDim = self:__getAxisUDim(currentLayout.AbsoluteContentSize) + local otherUDim = self:__getOtherUDim() + + if self.props.axis == FitFrameOnAxis.Axis.Vertical then + return UDim2.new(otherUDim, axisUDim) + elseif self.props.axis == FitFrameOnAxis.Axis.Horizontal then + return UDim2.new(axisUDim, otherUDim) + end + end +end + +function FitFrameOnAxis:__getBothAxisSize(currentLayout) + local minimumSize = self.props.minimumSize + local absoluteContentSize = currentLayout.AbsoluteContentSize + + local xAxis = UDim.new(minimumSize.X.Scale, absoluteContentSize.X + self:__getHorizontalMargin()) + local yAxis = UDim.new(minimumSize.Y.Scale, absoluteContentSize.Y + self:__getVerticalMargin()) + + return UDim2.new(xAxis, yAxis) +end + +function FitFrameOnAxis:__getAxisUDim(vector2) + -- Merges minimumSize with given Vector2 + -- to create UDim for primary axis + local minimumSize = self.props.minimumSize + + local targetUDim + local lengthOfChildren + if self.props.axis == FitFrameOnAxis.Axis.Vertical then + targetUDim = minimumSize.Y + lengthOfChildren = vector2.Y + self:__getVerticalMargin() + elseif self.props.axis == FitFrameOnAxis.Axis.Horizontal then + targetUDim = minimumSize.X + lengthOfChildren = vector2.X + self:__getHorizontalMargin() + end + + return UDim.new(targetUDim.Scale, math.max(lengthOfChildren, targetUDim.Offset)) +end + +function FitFrameOnAxis:__getVerticalMargin() + return self.props.margin.top + self.props.margin.bottom +end + +function FitFrameOnAxis:__getHorizontalMargin() + return self.props.margin.left + self.props.margin.right +end + +function FitFrameOnAxis:__getOtherUDim() + -- Since there is no primary axis to merge with, + -- this UDim is entirely represented by minimumSize + local minimumSize = self.props.minimumSize + + if self.props.axis == FitFrameOnAxis.Axis.Vertical then + return minimumSize.X + elseif self.props.axis == FitFrameOnAxis.Axis.Horizontal then + return minimumSize.Y + end +end + +return FitFrameOnAxis diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameVertical.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameVertical.lua new file mode 100644 index 0000000..0aade4c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitFrameVertical.lua @@ -0,0 +1,21 @@ +local root = script.Parent +local Packages = root.Parent + +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) + +local FitFrameOnAxis = require(script.Parent.FitFrameOnAxis) + +return function(props) + props = props or {} + local width = props.width + + local filteredProps = Cryo.Dictionary.join(props, { + axis = FitFrameOnAxis.Axis.Vertical, + minimumSize = UDim2.new(width, UDim.new(0, 0)), + + width = Cryo.None, + }) + + return Roact.createElement(FitFrameOnAxis, filteredProps) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitTextLabel.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitTextLabel.lua new file mode 100644 index 0000000..453dce8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/FitTextLabel.lua @@ -0,0 +1,120 @@ +local root = script.Parent +local Packages = root.Parent + +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) + +local EngineFeatureTextBoundsRoundUp do + local success, value = pcall(function() + return game:GetEngineFeature("TextBoundsRoundUp") + end) + EngineFeatureTextBoundsRoundUp = success and value +end + +-- We need to add 2 to these values as a workaround to a documented engine bug +local TextService = game:GetService("TextService") +local function getTextHeight(text, fontSize, font, widthCap) + if EngineFeatureTextBoundsRoundUp then + return TextService:GetTextSize(text, fontSize, font, Vector2.new(widthCap, 10000)).Y + else + return TextService:GetTextSize(text, fontSize, font, Vector2.new(widthCap, 10000)).Y + 2 + end +end + +local function getTextWidth(text, fontSize, font) + if EngineFeatureTextBoundsRoundUp then + return TextService:GetTextSize(text, fontSize, font, Vector2.new(10000, 10000)).X + else + return TextService:GetTextSize(text, fontSize, font, Vector2.new(10000, 10000)).X + 2 + end +end + +local FitTextLabel = Roact.PureComponent:extend("FitTextLabel") +FitTextLabel.Width = { + FitToText = {}, +} +FitTextLabel.defaultProps = { + Font = Enum.Font.SourceSans, + Text = "Label", + TextSize = 12, + TextWrapped = true, + + maximumWidth = math.huge, +} + +function FitTextLabel:init() + self.frameRef = Roact.createRef() + + self.onResize = function() + if not self.frameRef.current then + return + end + + self.frameRef.current.Size = self:__getSize(self.frameRef.current) + end +end + +function FitTextLabel:render() + local instanceType = self.props.onActivated and "TextButton" or "TextLabel" + return Roact.createElement(instanceType, self:__getFilteredProps()) +end + +function FitTextLabel:didMount() + self.onResize() +end + +function FitTextLabel:didUpdate() + self.onResize() +end + +function FitTextLabel:__getFilteredProps() + -- Will return a new prop map after removing + -- Roact.Children and any defaultProps in an effort + -- to only return safe Roblox Instance "TextLabel" + -- properties that may be present. + local filteredProps = { + width = Cryo.None, + maximumWidth = Cryo.None, + onActivated = Cryo.None, + Size = UDim2.new(self.props.width, UDim.new(0, 0)), + [Roact.Ref] = self.frameRef, + + [Roact.Children] = Cryo.Dictionary.join(self.props[Roact.Children] or {}, { + sizeConstraint = self.props.maximumWidth < math.huge and Roact.createElement("UISizeConstraint", { + MaxSize = Vector2.new(self.props.maximumWidth, math.huge), + }) + }), + + [Roact.Event.Activated] = self.props.onActivated, + + [Roact.Change.AbsoluteSize] = function(rbx) + if self.props[Roact.Change.AbsoluteSize] then + self.props[Roact.Change.AbsoluteSize](rbx) + end + self.onResize() + end, + } + + return Cryo.Dictionary.join(self.props, filteredProps) +end + +function FitTextLabel:__getSize(rbx) + local maximumWidth = self.props.maximumWidth + local width = self.props.width + if width == FitTextLabel.Width.FitToText then + local textWidth = getTextWidth(self.props.Text, self.props.TextSize, self.props.Font) + width = UDim.new(0, math.min(textWidth, maximumWidth)) + end + + local widthCap = math.max(maximumWidth < math.huge and maximumWidth or 0, rbx.AbsoluteSize.X) + local textHeight = getTextHeight( + self.props.Text, + self.props.TextSize, + self.props.Font, + widthCap + ) + + return UDim2.new(width, UDim.new(0, textHeight)) +end + +return FitTextLabel diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/Rect.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/Rect.lua new file mode 100644 index 0000000..4f2f8c7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/Rect.lua @@ -0,0 +1,26 @@ +return { + rectangle = function(horizontal, vertical) + return { + top = vertical, + bottom = vertical, + left = horizontal, + right = horizontal, + } + end, + square = function(x) + return { + top = x, + bottom = x, + left = x, + right = x, + } + end, + quad = function(top, right, bottom, left) + return { + top = top, + bottom = bottom, + left = left, + right = right, + } + end, +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/init.lua new file mode 100644 index 0000000..ac42b94 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-fit-components/roact-fit-components/init.lua @@ -0,0 +1,7 @@ +return { + FitFrameHorizontal = require(script.FitFrameHorizontal), + FitFrameOnAxis = require(script.FitFrameOnAxis), + FitFrameVertical = require(script.FitFrameVertical), + FitTextLabel = require(script.FitTextLabel), + Rect = require(script.Rect), +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/enumerate.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/enumerate.lua new file mode 100644 index 0000000..780f356 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/enumerate.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_enumerate"]["enumerate"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/lock.toml new file mode 100644 index 0000000..8df3e7e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/lock.toml @@ -0,0 +1,11 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/roact-gamepad" +version = "0.4.6" +commit = "fc0b421162ed09a155ceeaf01142e58d8e686e74" +source = "url+https://github.com/roblox/roact-gamepad" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "enumerate roblox/enumerate 1.0.0 url+https://github.com/roblox/enumerate", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusContext.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusContext.lua new file mode 100644 index 0000000..acc9dac --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusContext.lua @@ -0,0 +1,4 @@ +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) + +return Roact.createContext(nil) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusController.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusController.lua new file mode 100644 index 0000000..85fa799 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusController.lua @@ -0,0 +1,363 @@ +--!nonstrict +-- Manages groups of selectable elements, reacting to selection changes for +-- individual items and triggering events for group selection changes +local Packages = script.Parent.Parent +local Cryo = require(Packages.Cryo) + +local Input = require(script.Parent.Input) +local createSignal = require(script.Parent.createSignal) +local debugPrint = require(script.Parent.debugPrint) + +local InternalApi = require(script.Parent.FocusControllerInternalApi) + +local FocusControllerInternal = {} +FocusControllerInternal.__index = FocusControllerInternal + +function FocusControllerInternal.new() + local self = setmetatable({ + selectionChangedSignal = createSignal(), + boundInputsChangedSignal = createSignal(), + + focusNodeTree = {}, + allNodes = {}, + + rootRef = nil, + engineInterface = nil, + captureFocusOnInitialize = false, + inputDisconnectors = {}, + boundInputs = {}, + focusedLeaf = nil, + }, FocusControllerInternal) + + return self +end + +function FocusControllerInternal:moveFocusTo(ref) + if self.engineInterface == nil then + error("FocusController is not connected to a component hierarchy!", 2) + end + + debugPrint("[FOCUS] Move focus to", ref) + local node = self.allNodes[ref] + + if node ~= nil and not self:isNodeFocused(node) then + node:focus() + end +end + +function FocusControllerInternal:moveFocusToNeighbor(neighborProp) + if self.engineInterface == nil then + error("FocusController is not connected to a component hierarchy!", 2) + end + + if self.focusedLeaf ~= nil then + debugPrint("[FOCUS] Move focus to", neighborProp, "from", self.focusedLeaf.ref) + local refValue = self.focusedLeaf.ref:getValue() + if refValue ~= nil and refValue[neighborProp] ~= nil then + self:setSelection(refValue[neighborProp]) + end + end +end + +function FocusControllerInternal:getSelection() + return self.engineInterface.getSelection() +end + +function FocusControllerInternal:setSelection(ref) + self.engineInterface.setSelection(ref) +end + +function FocusControllerInternal:registerNode(parentNode, refKey, node) + if parentNode ~= nil then + debugPrint("[TREE ] Registering child node", refKey) + + local parentEntry = self.focusNodeTree[parentNode] or {} + parentEntry[refKey] = node + self.focusNodeTree[parentNode] = parentEntry + else + debugPrint("[TREE ] Registering root node", refKey) + self.rootRef = refKey + end + + self.allNodes[refKey] = node +end + +function FocusControllerInternal:deregisterNode(parentNode, refKey) + debugPrint("[TREE ] Deregistering child node", refKey) + if parentNode ~= nil then + self.focusNodeTree[parentNode][refKey] = nil + end + + self.allNodes[refKey] = nil +end + +function FocusControllerInternal:descendantRemovedRefocus() + -- If focusedLeaf is nil, then we've lost focus altogether, which likely + -- means that focus belongs to a different focusable tree. Since that's out + -- of our control, we can stop here. + if self.focusedLeaf == nil then + return + end + + -- If the currently focused leaf has a nil ref, then its associated host + -- component has unmounted and we need to refocus. + if self.focusedLeaf.ref:getValue() == nil then + debugPrint("[FOCUS] Focused node was removed; refocusing from nearest existing ancestor") + + -- Climb up the focusedLeaf's ancestry until we find a node that still + -- exists; if we do find one, focus it + local ancestorNode = self.focusedLeaf.parent + while ancestorNode ~= nil and self.allNodes[ancestorNode.ref] == nil do + ancestorNode = ancestorNode.Parent + end + + if ancestorNode ~= nil then + ancestorNode:focus() + end + end +end + +function FocusControllerInternal:descendantAddedRefocus() + -- If focusedLeaf is nil, then we've lost focus altogether, which likely + -- means that focus belongs to a different focusable tree. Since that's out + -- of our control, we can stop here. + if self.focusedLeaf == nil then + return + end + + -- If the current focusedLeaf has children, then descendants must have been + -- added to it, and we should re-run its focus logic. + if not Cryo.isEmpty(self:getChildren(self.focusedLeaf)) then + -- A new descendant was introduced, which means that we need to refocus + -- the current leaf + debugPrint("[FOCUS] Currently-focused node is no longer a leaf; refocusing", self.focusedLeaf.ref) + self.focusedLeaf:focus() + end +end + +function FocusControllerInternal:getChildren(parentNode) + return self.focusNodeTree[parentNode] or {} +end + +function FocusControllerInternal:isNodeFocused(node) + if self.focusedLeaf == nil then + return false + end + + if self.focusedLeaf == node then + return true + end + + -- Find out if one of the focused leaf's parents is equal to the provided + -- node, in which case, it remains in focus + local parentNode = self.focusedLeaf.parent + while parentNode ~= nil do + if parentNode == node then + return true + end + + parentNode = parentNode.parent + end + + return false +end + +-- Prints a human-readable version of the node tree. +function FocusControllerInternal:debugPrintTree() + local function recursePrintTree(node, indent) + -- Print the current node + debugPrint(indent, tostring(node.ref)) + + -- Recurse through children + local children = self:getChildren(node) + for _, childNode in pairs(children) do + recursePrintTree(childNode, indent .. " ") + end + end + + debugPrint("Printing Focus Node Tree:") + local rootNode = self.allNodes[self.rootRef] + recursePrintTree(rootNode, "") +end + +function FocusControllerInternal:updateInputBindings() + local newBindings = {} + + local focusChainNode = self.focusedLeaf + while focusChainNode ~= nil do + for _, binding in pairs(focusChainNode.inputBindings) do + local key = Input.getUniqueKey(binding) + local existing = newBindings[key] + if existing == nil then + debugPrint("[INPUT] Bind input", key) + newBindings[key] = binding + end + end + + focusChainNode = focusChainNode.parent + end + + -- It's pretty straightforward to simply disconnect and reconnect all event + -- connections whenever this function is called; we wouldn't typically be + -- able to rely on binding identity equality anyways + for _, disconnector in pairs(self.inputDisconnectors) do + disconnector() + end + + self.inputDisconnectors = {} + self.boundInputs = {} + for key, binding in pairs(newBindings) do + self.inputDisconnectors[key] = Input.connectToEvent(binding, self.engineInterface) + if binding.keyCode then + self.boundInputs[binding.keyCode] = binding.meta or {} + end + end + self.boundInputsChangedSignal:fire(self.boundInputs) +end + +function FocusControllerInternal:initialize(engineInterface) + -- If the engineInterface is already set, then this FocusController was + -- probably also assigned to another tree + if self.engineInterface ~= nil then + error("FocusController cannot be initialized more than once; make sure you are not passing it to multiple components") + end + + self.engineInterface = engineInterface + + -- Create a connection to the GuiService property relevant to the navigation + -- tree we want to connect + self.guiServiceConnection = engineInterface.subscribeToSelectionChanged(function() + -- This FocusController is not attached to an Instance hierarchy yet, so + -- we shouldn't try to manage selection + if self.rootRef == nil then + return + end + + -- Track whether or not the previous focus was inside this hierarchy + local wasPreviouslyFocused = self.focusedLeaf ~= nil + + -- Nil out our focusedLeaf (we'll recalculate it if necessary) and get + -- the current selection + self.focusedLeaf = nil + local selectedInstance = engineInterface.getSelection() + local rootRefValue = self.rootRef:getValue() + + -- If selection is occurring within this FocusControllerInternal's + -- hierarchy, we need to recompute the currently focused leaf + if selectedInstance ~= nil then + if rootRefValue == selectedInstance or selectedInstance:IsDescendantOf(rootRefValue) then + debugPrint( + "[EVENT] Selection changed to", + selectedInstance, + "in focus hierarchy beginning at", + rootRefValue + ) + + -- Find the currently-focused node within our hierarchy and set + -- self.focusedLeaf accordingly. + for ref, node in pairs(self.allNodes) do + if selectedInstance == ref:getValue() then + self.focusedLeaf = node + break + end + end + end + end + + -- We should fire our selectionChanged signal in the event that any of + -- the following occur: + -- 1. Selection moved within the hierarchy + -- 2. Selection moved from outside the hierarchy to an element inside it + -- 3. Selection moved from inside the hierarchy to an element outside it + if self.focusedLeaf ~= nil or wasPreviouslyFocused then + self.selectionChangedSignal:fire() + + -- Update input connections here + self:updateInputBindings() + end + end) + + if self.captureFocusOnInitialize then + self:captureFocus() + end +end + +function FocusControllerInternal:captureFocus() + if self.engineInterface == nil then + self.captureFocusOnInitialize = true + else + self.allNodes[self.rootRef]:focus() + end +end + +function FocusControllerInternal:releaseFocus() + if self.engineInterface ~= nil then + self.engineInterface.setSelection(nil) + end +end + +function FocusControllerInternal:teardown() + if self.guiServiceConnection ~= nil then + self.guiServiceConnection:Disconnect() + end + + -- Disconnect all bound inputs. These can be left dangling when a whole tree + -- is unmounted at once + for _, disconnect in pairs(self.inputDisconnectors) do + disconnect() + end + + -- Make sure this controller is restored to its uninitialized state + self.rootRef = nil + self.engineInterface = nil + self.captureFocusOnInitialize = false + self.focusedLeaf = nil +end + +function FocusControllerInternal:subscribeToSelectionChange(callback) + debugPrint("[TREE ] New subscription to selection change event") + return self.selectionChangedSignal:subscribe(callback) +end + +-- Creates an object with a public API for managing focus. This object can be +-- used in components to direct focus as necessary +function FocusControllerInternal.createPublicApiWrapper() + local focusControllerInternal = FocusControllerInternal.new() + + return { + [InternalApi] = focusControllerInternal, + moveFocusTo = function(...) + focusControllerInternal:moveFocusTo(...) + end, + moveFocusLeft = function() + focusControllerInternal:moveFocusToNeighbor("NextSelectionLeft") + end, + moveFocusRight = function() + focusControllerInternal:moveFocusToNeighbor("NextSelectionRight") + end, + moveFocusUp = function() + focusControllerInternal:moveFocusToNeighbor("NextSelectionUp") + end, + moveFocusDown = function() + focusControllerInternal:moveFocusToNeighbor("NextSelectionDown") + end, + captureFocus = function() + focusControllerInternal:captureFocus() + end, + releaseFocus = function() + focusControllerInternal:releaseFocus() + end, + getCurrentFocus = function() + local focusedLeaf = focusControllerInternal.focusedLeaf + return focusedLeaf and focusedLeaf.ref or nil + end, + getBoundInputs = function() + return focusControllerInternal.boundInputs + end, + subscribeToBoundInputsChanged = function(callback) + return focusControllerInternal.boundInputsChangedSignal:subscribe(callback) + end, + } +end + +return FocusControllerInternal \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusController.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusController.spec.lua new file mode 100644 index 0000000..55b26cb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusController.spec.lua @@ -0,0 +1,452 @@ +return function() + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + + local FocusNode = require(script.Parent.FocusNode) + local FocusController = require(script.Parent.FocusController) + local InternalApi = require(script.Parent.FocusControllerInternalApi) + local Input = require(script.Parent.Input) + + local MockEngine = require(script.Parent.Test.MockEngine) + local createSpy = require(script.Parent.Test.createSpy) + + local function createRootNode(ref) + local node = FocusNode.new({ + focusController = FocusController.createPublicApiWrapper(), + [Roact.Ref] = ref, + }) + + node:attachToTree(nil, function() end) + + return node + end + + local function addChildNode(parentNode) + local instance = Instance.new("Frame") + instance.Parent = parentNode.ref:getValue() + + local childRef, _ = Roact.createBinding(instance) + local childNode = FocusNode.new({ + parentFocusNode = parentNode, + [Roact.Ref] = childRef, + }) + childNode:attachToTree(parentNode, function() end) + + return childNode, childRef + end + + describe("event management", function() + it("should not fire subscribed signals before it's initialized", function() + local ref, _ = Roact.createBinding(Instance.new("Frame")) + local focusNode = createRootNode(ref) + + local focusController = focusNode.focusController[InternalApi] + local selectionChangeSpy = createSpy() + focusController:subscribeToSelectionChange(selectionChangeSpy.value) + + local mockEngine, _ = MockEngine.new() + + mockEngine:simulateSelectionChanged(Instance.new("Frame")) + expect(selectionChangeSpy.callCount).to.equal(0) + end) + + it("should fire subscribed signals when selection changes occur once initialized", function() + local ref, _ = Roact.createBinding(Instance.new("Frame")) + local focusNode = createRootNode(ref) + + local focusController = focusNode.focusController[InternalApi] + local selectionChangeSpy = createSpy() + focusController:subscribeToSelectionChange(selectionChangeSpy.value) + local mockEngine, engineInterface = MockEngine.new() + + expect(selectionChangeSpy.callCount).to.equal(0) + + focusController:initialize(engineInterface) + mockEngine:simulateSelectionChanged(ref:getValue()) + expect(selectionChangeSpy.callCount).to.equal(1) + end) + end) + + describe("focus logic", function() + it("should consider selected objects to be in focus", function() + local ref, _ = Roact.createBinding(Instance.new("Frame")) + local focusNode = createRootNode(ref) + + local _, engineInterface = MockEngine.new() + local focusController = focusNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + expect(focusController:isNodeFocused(focusNode)).to.equal(false) + focusNode:focus() + expect(focusController:isNodeFocused(focusNode)).to.equal(true) + end) + + it("should consider parent nodes of selected objects to be in focus", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + local childNode, _ = addChildNode(parentNode) + + local _, engineInterface = MockEngine.new() + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + expect(focusController:isNodeFocused(parentNode)).to.equal(false) + expect(focusController:isNodeFocused(childNode)).to.equal(false) + childNode:focus() + expect(focusController:isNodeFocused(parentNode)).to.equal(true) + expect(focusController:isNodeFocused(childNode)).to.equal(true) + end) + + it("should change its notion of focus when selection is changed", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local childNodeA, _ = addChildNode(parentNode) + local childNodeB, childRefB = addChildNode(parentNode) + + local _, engineInterface = MockEngine.new() + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + childNodeA:focus() + + expect(focusController:isNodeFocused(childNodeA)).to.equal(true) + expect(focusController:isNodeFocused(childNodeB)).to.equal(false) + + engineInterface.setSelection(childRefB:getValue()) + expect(focusController:isNodeFocused(childNodeA)).to.equal(false) + expect(focusController:isNodeFocused(childNodeB)).to.equal(true) + end) + + it("should change its notion of focus when selection changes via the engine", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local childNodeA, _ = addChildNode(parentNode) + local childNodeB, childRefB = addChildNode(parentNode) + + local mockEngine, engineInterface = MockEngine.new() + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + childNodeA:focus() + + expect(focusController:isNodeFocused(childNodeA)).to.equal(true) + expect(focusController:isNodeFocused(childNodeB)).to.equal(false) + + mockEngine:simulateSelectionChanged(childRefB:getValue()) + expect(focusController:isNodeFocused(childNodeA)).to.equal(false) + expect(focusController:isNodeFocused(childNodeB)).to.equal(true) + end) + + it("should only track selection changes inside its hierarchy", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local childNodeA, _ = addChildNode(parentNode) + local childNodeB, _ = addChildNode(parentNode) + + local mockEngine, engineInterface = MockEngine.new() + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + local selectionChangedSpy = createSpy() + focusController:subscribeToSelectionChange(selectionChangedSpy.value) + + childNodeA:focus() + expect(focusController:isNodeFocused(childNodeA)).to.equal(true) + expect(selectionChangedSpy.callCount).to.equal(1) + + -- Moving selection within the hierarchy should trigger fire events + childNodeB:focus() + expect(selectionChangedSpy.callCount).to.equal(2) + + -- When selection changes from _inside_ this hierarchy to nil, + -- notify accordingly + mockEngine:simulateSelectionChanged(nil) + expect(selectionChangedSpy.callCount).to.equal(3) + + -- Moving back into the hierarchy should once again trigger events + childNodeA:focus() + expect(selectionChangedSpy.callCount).to.equal(4) + + -- When selection changes from inside the hierarchy to outside the + -- hierarchy, events should fire + local unconnectedFrame = Instance.new("Frame") + mockEngine:simulateSelectionChanged(unconnectedFrame) + expect(selectionChangedSpy.callCount).to.equal(5) + + -- When selection changes between elements outside the hierarchy, + -- no events should be triggered + local unconnectedFrame2 = Instance.new("Frame") + mockEngine:simulateSelectionChanged(unconnectedFrame2) + expect(selectionChangedSpy.callCount).to.equal(5) + + -- When selection changes from an element outside the hierarchy to + -- nil, no events should be triggered + mockEngine:simulateSelectionChanged(nil) + expect(selectionChangedSpy.callCount).to.equal(5) + end) + end) + + describe("Tree-level focus management", function() + it("should not automatically capture focus", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local focusControllerInternal = parentNode.focusController[InternalApi] + expect(focusControllerInternal:isNodeFocused(parentNode)).to.equal(false) + end) + + it("should focus the top-level node when captureFocus is called", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local _, engineInterface = MockEngine.new() + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + parentNode.focusController.captureFocus() + local focusControllerInternal = parentNode.focusController[InternalApi] + expect(focusControllerInternal:isNodeFocused(parentNode)).to.equal(true) + end) + + it("should focus the top-level node when captureFocus is called, even if initialized afterwards", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local _, engineInterface = MockEngine.new() + + -- Capture focus first, then initialize the node afterwards. This + -- simulates scenarios in which a component wants to captureFocus on + -- didMount, but is not yet parented to the DataModel + parentNode.focusController.captureFocus() + local focusControllerInternal = parentNode.focusController[InternalApi] + focusControllerInternal:initialize(engineInterface) + + expect(focusControllerInternal:isNodeFocused(parentNode)).to.equal(true) + end) + + it("should set selection to nil when focus is released", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local _, engineInterface = MockEngine.new() + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + parentNode.focusController.captureFocus() + expect(engineInterface.getSelection()).to.equal(rootRef:getValue()) + + parentNode.focusController.releaseFocus() + expect(engineInterface.getSelection()).to.equal(nil) + end) + end) + + describe("Input binding", function() + it("should only call bound inputs when the element is in focus", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local childNodeA, _ = addChildNode(parentNode) + local callbackSpyA = createSpy() + childNodeA.inputBindings = { + action = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, callbackSpyA.value), + } + local childNodeB, _ = addChildNode(parentNode) + local callbackSpyB = createSpy() + childNodeB.inputBindings = { + action = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, callbackSpyB.value), + } + + local mockEngine, engineInterface = MockEngine.new() + local focusControllerInternal = parentNode.focusController[InternalApi] + focusControllerInternal:initialize(engineInterface) + expect(callbackSpyA.callCount).to.equal(0) + expect(callbackSpyB.callCount).to.equal(0) + + childNodeA:focus() + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + expect(callbackSpyA.callCount).to.equal(1) + expect(callbackSpyB.callCount).to.equal(0) + + childNodeB:focus() + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + expect(callbackSpyA.callCount).to.equal(1) + expect(callbackSpyB.callCount).to.equal(1) + end) + + it("should allow input bindings to override parent input bindings", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + local callbackSpyParent = createSpy() + parentNode.inputBindings = { + action = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, callbackSpyParent.value), + } + + -- ChildA does not override the parent's binding + local childNodeA, _ = addChildNode(parentNode) + + -- ChildB has a binding to the same button, which overrides the parent + local childNodeB, _ = addChildNode(parentNode) + local callbackSpyChild = createSpy() + childNodeB.inputBindings = { + action = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, callbackSpyChild.value), + } + + local mockEngine, engineInterface = MockEngine.new() + local focusControllerInternal = parentNode.focusController[InternalApi] + focusControllerInternal:initialize(engineInterface) + expect(callbackSpyParent.callCount).to.equal(0) + expect(callbackSpyChild.callCount).to.equal(0) + + -- When A is focused, we should use the parent's input binding + childNodeA:focus() + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + expect(callbackSpyParent.callCount).to.equal(1) + expect(callbackSpyChild.callCount).to.equal(0) + + -- When B is focused, we should use child B's input binding + childNodeB:focus() + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + expect(callbackSpyParent.callCount).to.equal(1) + expect(callbackSpyChild.callCount).to.equal(1) + end) + + it("should override onStep bindings the same as begin and end", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + local callbackSpyParent = createSpy() + parentNode.inputBindings = { + action = Input.PublicInterface.onStep(Enum.KeyCode.ButtonX, callbackSpyParent.value), + } + + -- ChildA does not override the parent's binding + local childNodeA, _ = addChildNode(parentNode) + + -- ChildB has a binding to the same button, which overrides the parent + local childNodeB, _ = addChildNode(parentNode) + local callbackSpyChild = createSpy() + childNodeB.inputBindings = { + action = Input.PublicInterface.onStep(Enum.KeyCode.ButtonX, callbackSpyChild.value), + } + + local mockEngine, engineInterface = MockEngine.new() + local focusControllerInternal = parentNode.focusController[InternalApi] + focusControllerInternal:initialize(engineInterface) + expect(callbackSpyParent.callCount).to.equal(0) + expect(callbackSpyChild.callCount).to.equal(0) + + -- When A is focused, we should use the parent's input binding + childNodeA:focus() + mockEngine:renderStep(0.03) + expect(callbackSpyParent.callCount).to.equal(1) + expect(callbackSpyChild.callCount).to.equal(0) + + -- When B is focused, both it and the parent's binding run + childNodeB:focus() + mockEngine:renderStep(0.03) + expect(callbackSpyParent.callCount).to.equal(1) + expect(callbackSpyChild.callCount).to.equal(1) + end) + + it("should override onMoveStep bindings wholesale, since they don't differ by keycode", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + local callbackSpyParent = createSpy() + parentNode.inputBindings = { + action = Input.PublicInterface.onMoveStep(callbackSpyParent.value), + } + + -- ChildA does not override the parent's binding + local childNodeA, _ = addChildNode(parentNode) + + -- ChildB has a binding to the same button, which overrides the parent + local childNodeB, _ = addChildNode(parentNode) + local callbackSpyChild = createSpy() + childNodeB.inputBindings = { + action = Input.PublicInterface.onMoveStep(callbackSpyChild.value), + } + + local mockEngine, engineInterface = MockEngine.new() + local focusControllerInternal = parentNode.focusController[InternalApi] + focusControllerInternal:initialize(engineInterface) + expect(callbackSpyParent.callCount).to.equal(0) + expect(callbackSpyChild.callCount).to.equal(0) + + -- When A is focused, we should use the parent's input binding + childNodeA:focus() + mockEngine:renderStep(0.03) + expect(callbackSpyParent.callCount).to.equal(1) + expect(callbackSpyChild.callCount).to.equal(0) + + -- When B is focused, its binding is run instead + childNodeB:focus() + mockEngine:renderStep(0.03) + expect(callbackSpyParent.callCount).to.equal(1) + expect(callbackSpyChild.callCount).to.equal(1) + end) + + it("should update input bindings when focus changes", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local parentNode = createRootNode(rootRef) + + local boundInputsChangedSpy = createSpy() + local disconnect = parentNode.focusController.subscribeToBoundInputsChanged(boundInputsChangedSpy.value) + + local childNodeA, _ = addChildNode(parentNode) + childNodeA.inputBindings = { + action = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, function() end, { + key = "actionX" + }), + } + local childNodeB, _ = addChildNode(parentNode) + childNodeB.inputBindings = { + action1 = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonY, function() end, { + key = "actionY" + }), + action2 = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonA, function() end), + } + + local _, engineInterface = MockEngine.new() + local focusControllerInternal = parentNode.focusController[InternalApi] + focusControllerInternal:initialize(engineInterface) + + expect(boundInputsChangedSpy.callCount).to.equal(0) + childNodeA:focus() + expect(boundInputsChangedSpy.callCount).to.equal(1) + + local boundInputs = parentNode.focusController.getBoundInputs() + expect(boundInputs[Enum.KeyCode.ButtonY]).to.equal(nil) + expect(boundInputs[Enum.KeyCode.ButtonA]).to.equal(nil) + expect(boundInputs[Enum.KeyCode.ButtonX].key).to.equal("actionX") + + childNodeB:focus() + expect(boundInputsChangedSpy.callCount).to.equal(2) + + boundInputs = parentNode.focusController.getBoundInputs() + expect(boundInputs[Enum.KeyCode.ButtonY].key).to.equal("actionY") + expect(boundInputs[Enum.KeyCode.ButtonX]).to.equal(nil) + -- expect a binding without a meta table field to return an empty table rather than nil + expect(#boundInputs[Enum.KeyCode.ButtonA]).to.equal(0) + + disconnect() + childNodeA:focus() + expect(boundInputsChangedSpy.callCount).to.equal(2) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusControllerInternalApi.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusControllerInternalApi.lua new file mode 100644 index 0000000..16b8ed0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusControllerInternalApi.lua @@ -0,0 +1,3 @@ +local Symbol = require(script.Parent.Symbol) + +return Symbol.named("FocusControllerInternalApi") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusNode.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusNode.lua new file mode 100644 index 0000000..141d4d4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusNode.lua @@ -0,0 +1,167 @@ +--!nonstrict +-- Manages groups of selectable elements, reacting to selection changes for +-- individual items and triggering events for group selection changes +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) + +local InternalApi = require(script.Parent.FocusControllerInternalApi) +local inputBindingsEqual = require(script.Parent.inputBindingsEqual) + +local FocusNode = {} +FocusNode.__index = FocusNode + +function FocusNode.new(navProps) + local focusController + if navProps.parentFocusNode ~= nil then + focusController = navProps.parentFocusNode.focusController + elseif navProps.focusController ~= nil then + focusController = navProps.focusController + else + -- FIXME: do we ever even hit this? + error("Cannot create node without focus manager") + end + + local self = setmetatable({ + focusController = focusController, + ref = navProps[Roact.Ref], + + lastFocused = nil, + }, FocusNode) + + self:updateNavProps(navProps) + + return self +end + +function FocusNode:__getFocusControllerInternal() + return self.focusController[InternalApi] +end + +function FocusNode:__findDefaultChildNode() + local lowestLayoutOrder = math.huge + local lowestLayoutOrderChild = nil + + local focusController = self:__getFocusControllerInternal() + local children = focusController:getChildren(self) + local groupHostObject = self.ref:getValue() + + -- Iterate through all children of this node, looking for a LayoutOrder + -- associated with each node. Picking the lowest LayoutOrder is a good + -- approximation of selecting the "first" child of a given container if the + -- case that we don't have a default or a previous value to restore + for ref, child in pairs(children) do + local hostObject = ref:getValue() + + -- For each child node, determine whether it or any of its ancestors (up + -- to the group) has the LayoutOrder property defined + while hostObject ~= groupHostObject and hostObject ~= nil do + if hostObject:isA("GuiObject") then + local layoutOrder = hostObject.LayoutOrder + + -- LayoutOrder == 0 is the default value; in most cases, this + -- implies that it wasn't explicitly set, so we should ignore it + if layoutOrder ~= 0 then + if layoutOrder < lowestLayoutOrder then + lowestLayoutOrder = layoutOrder + lowestLayoutOrderChild = child + end + + -- Once we've found a layout order to associate with the + -- Focusable, we break out and move on to the next child + break + end + + end + + hostObject = hostObject.Parent + end + end + + if lowestLayoutOrderChild ~= nil then + return lowestLayoutOrderChild + else + -- If no valid target was returned, we return any valid member + local _, arbitraryChild = next(children) + + return arbitraryChild + end +end + +function FocusNode:updateNavProps(navProps) + local restorePreviousChildFocus = false + if navProps.restorePreviousChildFocus ~= nil then + restorePreviousChildFocus = navProps.restorePreviousChildFocus + end + + local oldInputBindings = self.inputBindings + + self.defaultChildRef = navProps.defaultChild + self.restorePreviousChildFocus = restorePreviousChildFocus + self.inputBindings = navProps.inputBindings or {} + + local focusController = self:__getFocusControllerInternal() + if focusController:isNodeFocused(self) and not inputBindingsEqual(oldInputBindings, self.inputBindings) then + focusController:updateInputBindings() + end +end + +function FocusNode:focus() + local focusController = self:__getFocusControllerInternal() + local children = focusController:getChildren(self) + if Cryo.isEmpty(children) then + focusController:setSelection(self.ref:getValue()) + else + if self.restorePreviousChildFocus and self.lastFocused ~= nil then + focusController:moveFocusTo(self.lastFocused) + elseif self.defaultChildRef ~= nil then + focusController:moveFocusTo(self.defaultChildRef) + else + local defaultChild = self:__findDefaultChildNode() + if defaultChild ~= nil then + defaultChild:focus() + end + end + end +end + +function FocusNode:attachToTree(parent, onFocusChanged) + local focusController = self:__getFocusControllerInternal() + focusController:registerNode(parent, self.ref, self) + + self.parent = parent + self.disconnectSelectionListener = focusController:subscribeToSelectionChange(function() + -- Perform focus management operations set up by the FocusNode's owner + local focused = focusController:isNodeFocused(self) + onFocusChanged(focused) + + if self.parent ~= nil and focused then + self.parent.lastFocused = self.ref + end + + -- Keep track of the last focused ref so that we can provide it to + -- self.props.selectionRule whenever we regain focus + local children = focusController:getChildren(self) + if not Cryo.isEmpty(children) and focused then + -- For the special-case scenario in which the ref for our group + -- gained selection, we follow any established rules to find the + -- correct member of the group to bounce selection to, managed in + -- the `focus` callback on focusController + if focusController:getSelection() == self.ref:getValue() then + self:focus() + end + end + end) +end + +function FocusNode:detachFromTree() + local focusController = self:__getFocusControllerInternal() + focusController:deregisterNode(self.parent, self.ref) + + if self.disconnectSelectionListener ~= nil then + self.disconnectSelectionListener() + self.disconnectSelectionListener = nil + end +end + +return FocusNode \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusNode.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusNode.spec.lua new file mode 100644 index 0000000..a661d05 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/FocusNode.spec.lua @@ -0,0 +1,262 @@ +return function() + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + + local FocusNode = require(script.Parent.FocusNode) + local FocusController = require(script.Parent.FocusController) + local InternalApi = require(script.Parent.FocusControllerInternalApi) + + local MockEngine = require(script.Parent.Test.MockEngine) + + local function createRootNode(ref) + local node = FocusNode.new({ + focusController = FocusController.createPublicApiWrapper(), + [Roact.Ref] = ref, + }) + + node:attachToTree(nil, function() end) + + return node + end + + local function addChildNode(parentNode) + local instance = Instance.new("Frame") + instance.Parent = parentNode.ref:getValue() + + local childRef, _ = Roact.createBinding(instance) + local childNode = FocusNode.new({ + parentFocusNode = parentNode, + [Roact.Ref] = childRef, + }) + childNode:attachToTree(parentNode, function() end) + + return childNode, childRef + end + + describe("basic selection behavior", function() + it("should set selection to a ref when the ref's node is focused", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local focusNode = createRootNode(rootRef) + local focusController = focusNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + focusNode:focus() + expect(engineInterface.getSelection()).to.equal(rootRef:getValue()) + end) + + it("should redirect selection to a child when a non-leaf node is focused", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local _, childRef = addChildNode(parentNode) + + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + parentNode:focus() + expect(engineInterface.getSelection()).to.equal(childRef:getValue()) + end) + + it("should redirect selection to a child when a non-leaf node gains selection", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local mockEngine, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local _, childRef = addChildNode(parentNode) + + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + mockEngine:simulateSelectionChanged(rootRef:getValue()) + expect(engineInterface.getSelection()).to.equal(childRef:getValue()) + end) + end) + + describe("child auto-selection behavior", function() + it("should select the provided default when present", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local childNodeA, _ = addChildNode(parentNode) + local childNodeB, childRefB = addChildNode(parentNode) + parentNode.defaultChildRef = childRefB + + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + parentNode:focus() + expect(focusController:isNodeFocused(childNodeB)).to.equal(true) + + childNodeA:focus() + parentNode:focus() + expect(focusController:isNodeFocused(childNodeB)).to.equal(true) + end) + + it("should restore previous selection when the option is enabled", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local childNodeA, _ = addChildNode(parentNode) + local childNodeB, _ = addChildNode(parentNode) + parentNode.restorePreviousChildFocus = true + + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + childNodeA:focus() + parentNode:focus() + expect(focusController:isNodeFocused(childNodeA)).to.equal(true) + + childNodeB:focus() + parentNode:focus() + expect(focusController:isNodeFocused(childNodeB)).to.equal(true) + end) + + it("should defer to the default child ref when no child was previously focused", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local childNodeA, _ = addChildNode(parentNode) + local childNodeB, _ = addChildNode(parentNode) + local childNodeC, childRefC = addChildNode(parentNode) + parentNode.defaultChildRef = childRefC + parentNode.restorePreviousChildFocus = true + + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + parentNode:focus() + expect(focusController:isNodeFocused(childNodeC)).to.equal(true) + + childNodeA:focus() + childNodeB:focus() + parentNode:focus() + expect(focusController:isNodeFocused(childNodeB)).to.equal(true) + end) + end) + + describe("initial selection logic when nothing is specified", function() + local function addChildNested(parentNode, parentInstance) + local instance = Instance.new("Frame") + instance.Parent = parentInstance + + local childRef, _ = Roact.createBinding(instance) + local childNode = FocusNode.new({ + parentFocusNode = parentNode, + [Roact.Ref] = childRef, + }) + childNode:attachToTree(parentNode, function() end) + + return childNode, childRef + end + + it("should choose the child with the lowest LayoutOrder", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + local childNode1, childRef1 = addChildNode(parentNode) + childRef1:getValue().LayoutOrder = 1 + local _, childRef2 = addChildNode(parentNode) + childRef2:getValue().LayoutOrder = 2 + local _, childRef3 = addChildNode(parentNode) + childRef3:getValue().LayoutOrder = 3 + + parentNode:focus() + expect(focusController:isNodeFocused(childNode1)).to.equal(true) + end) + + it("should choose the lowest layout order even of nested elements", function() + local function insertAncestor(instance) + local newParent = Instance.new("Frame") + local grandparent = instance.Parent + instance.Parent = newParent + newParent.Parent = grandparent + end + + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + local childNode1, childRef1 = addChildNode(parentNode) + insertAncestor(childRef1:getValue()) + childRef1:getValue().Parent.LayoutOrder = 1 + local _, childRef2 = addChildNode(parentNode) + insertAncestor(childRef2:getValue()) + childRef2:getValue().Parent.LayoutOrder = 2 + local _, childRef3 = addChildNode(parentNode) + insertAncestor(childRef3:getValue()) + childRef3:getValue().Parent.LayoutOrder = 3 + + parentNode:focus() + expect(focusController:isNodeFocused(childNode1)).to.equal(true) + end) + + it("should only use the LayoutOrder closest to each child", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + -- Insert an intermediate frame with LayoutOrder = 1; we want to + -- make sure not to traverse back up to this and choose the wrong + -- child + local intermediateFrame = Instance.new("Frame") + intermediateFrame.Parent = rootRef:getValue() + intermediateFrame.LayoutOrder = 1 + + local childNode1, childRef1 = addChildNested(parentNode, intermediateFrame) + childRef1:getValue().LayoutOrder = 2 + local _, childRef2 = addChildNested(parentNode, intermediateFrame) + childRef2:getValue().LayoutOrder = 3 + local _, childRef3 = addChildNested(parentNode, intermediateFrame) + childRef3:getValue().LayoutOrder = 4 + + parentNode:focus() + expect(focusController:isNodeFocused(childNode1)).to.equal(true) + end) + + it("should safely skip past non-GuiObjects", function() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local _, engineInterface = MockEngine.new() + + local parentNode = createRootNode(rootRef) + local focusController = parentNode.focusController[InternalApi] + focusController:initialize(engineInterface) + + -- Insert an intermediate frame with LayoutOrder = 1; we want to + -- make sure not to traverse back up to this and choose the wrong + -- child + local intermediateFolder = Instance.new("Folder") + intermediateFolder.Parent = rootRef:getValue() + + local childNode1, childRef1 = addChildNested(parentNode, intermediateFolder) + childRef1:getValue().LayoutOrder = 1 + local _, childRef2 = addChildNested(parentNode, intermediateFolder) + childRef2:getValue().LayoutOrder = 2 + + -- None specified; this one will have to climb to the tree + local _, _ = addChildNested(parentNode, intermediateFolder) + + expect(function() + parentNode:focus() + end).never.to.throw() + + expect(focusController:isNodeFocused(childNode1)).to.equal(true) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Input.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Input.lua new file mode 100644 index 0000000..1855fee --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Input.lua @@ -0,0 +1,197 @@ +local debugPrint = require(script.Parent.debugPrint) +local InputBindingKind = require(script.Parent.InputBindingKind) + +local INPUT_TYPES = { + [Enum.UserInputType.Keyboard] = true, + [Enum.UserInputType.Gamepad1] = true, + [Enum.UserInputType.Gamepad2] = true, + [Enum.UserInputType.Gamepad3] = true, + [Enum.UserInputType.Gamepad4] = true, + [Enum.UserInputType.Gamepad5] = true, + [Enum.UserInputType.Gamepad6] = true, + [Enum.UserInputType.Gamepad7] = true, + [Enum.UserInputType.Gamepad8] = true, +} + +--[[ + In order to reduce the friction of tracking this globally, we track the + gamepad connection state of each "engine interface" individually. In + production contexts, this table will have one key-value pair. When running + tests, each test will provide it's own mock engine interface; this table + will keep them separate and prevent tests from interfering with one another +]] +local engineGamepadState = {} + +local function initializeEngineGamepadState() + return { + gamepadConnectedConnection = nil, + gamepadDisconnectedConnection = nil, + onStepConnections = 0, + primaryGamepadState = {}, + } +end + +local function getEngineState(engineInterface) + if engineGamepadState[engineInterface] == nil then + engineGamepadState[engineInterface] = initializeEngineGamepadState() + end + + return engineGamepadState[engineInterface] +end + +local function updatePrimaryGamepad(engineInterface) + local engineState = getEngineState(engineInterface) + + local primaryGamepad = Enum.UserInputType.Gamepad1 + for _, gamepadNum in ipairs(engineInterface.getNavigationGamepads()) do + if engineInterface.getGamepadConnected(gamepadNum) then + primaryGamepad = gamepadNum + break + end + end + + -- States returned by getGamepadState are mutable and updated by the engine, + -- so this table only needs to be setup once per gamepad update + local states = engineInterface.getGamepadState(primaryGamepad) + + engineState.primaryGamepadState = {} + for _, state in ipairs(states) do + engineState.primaryGamepadState[state.KeyCode] = state + end +end + +local function getInputEvent(action, matchInput) + return function(inputObject) + if matchInput(inputObject) then + debugPrint("[EVENT] Process input: ", + inputObject.KeyCode, + "-", + inputObject.UserInputState + ) + action(inputObject) + end + end +end + +local function wrapWithGamepadStateListener(engineInterface, connection) + local engineState = getEngineState(engineInterface) + + if engineState.onStepConnections == 0 then + updatePrimaryGamepad(engineInterface) + engineState.gamepadConnectedConnection = engineInterface.subscribeToGamepadConnected(function() + updatePrimaryGamepad(engineInterface) + end) + engineState.gamepadDisconnectedConnection = engineInterface.subscribeToGamepadDisconnected(function() + updatePrimaryGamepad(engineInterface) + end) + end + engineState.onStepConnections = engineState.onStepConnections + 1 + + return function() + connection:Disconnect() + + engineState.onStepConnections = engineState.onStepConnections - 1 + if engineState.onStepConnections == 0 then + engineState.gamepadConnectedConnection:Disconnect() + engineState.gamepadConnectedConnection = nil + engineState.gamepadDisconnectedConnection:Disconnect() + engineState.gamepadDisconnectedConnection = nil + end + end +end + +-- Returns a function that can be called to disconnect from the event +local function connectToEvent(binding, engineInterface) + if binding.kind == InputBindingKind.Begin then + local function matchInput(inputObject) + return INPUT_TYPES[inputObject.UserInputType] + and inputObject.UserInputState == Enum.UserInputState.Begin + and inputObject.KeyCode == binding.keyCode + end + + local connection = engineInterface.subscribeToInputBegan(getInputEvent(binding.action, matchInput)) + + return function() + connection:Disconnect() + end + elseif binding.kind == InputBindingKind.End then + local function matchInput(inputObject) + return INPUT_TYPES[inputObject.UserInputType] + and inputObject.UserInputState == Enum.UserInputState.End + and inputObject.KeyCode == binding.keyCode + end + + local connection = engineInterface.subscribeToInputEnded(getInputEvent(binding.action, matchInput)) + + return function() + connection:Disconnect() + end + elseif binding.kind == InputBindingKind.Step then + local engineState = getEngineState(engineInterface) + + local connection = engineInterface.subscribeToRenderStepped(function(step) + debugPrint("[EVENT] Render step triggered onStep callback") + binding.action(engineState.primaryGamepadState[binding.keyCode], step) + end) + + return wrapWithGamepadStateListener(engineInterface, connection) + elseif binding.kind == InputBindingKind.MoveStep then + local engineState = getEngineState(engineInterface) + + local connection = engineInterface.subscribeToRenderStepped(function(step) + debugPrint("[EVENT] Render step triggered onMoveStep callback") + local moveState = { + [Enum.KeyCode.Thumbstick1] = engineState.primaryGamepadState[Enum.KeyCode.Thumbstick1], + [Enum.KeyCode.DPadUp] = engineState.primaryGamepadState[Enum.KeyCode.DPadUp], + [Enum.KeyCode.DPadDown] = engineState.primaryGamepadState[Enum.KeyCode.DPadDown], + [Enum.KeyCode.DPadLeft] = engineState.primaryGamepadState[Enum.KeyCode.DPadLeft], + [Enum.KeyCode.DPadRight] = engineState.primaryGamepadState[Enum.KeyCode.DPadRight], + } + + binding.action(moveState, step) + end) + + return wrapWithGamepadStateListener(engineInterface, connection) + end +end + +local function makeInputBinding(kind) + return function(keyCode, action, meta) + assert(typeof(keyCode) == "EnumItem" and keyCode.EnumType == Enum.KeyCode, + "Invalid argument #1: expected a member of Enum.KeyCode") + assert(typeof(action) == "function", "Invalid argument #2: expected a function") + + return { + kind = kind, + keyCode = keyCode, + action = action, + meta = meta, + } + end +end + +local function onMoveStepInputBinding(action) + return { + kind = InputBindingKind.MoveStep, + action = action, + } +end + +local function getUniqueKey(binding) + if binding.keyCode then + return tostring(binding.kind) .. "-" .. tostring(binding.keyCode) + else + return tostring(binding.kind) + end +end + +return { + getUniqueKey = getUniqueKey, + connectToEvent = connectToEvent, + PublicInterface = { + onBegin = makeInputBinding(InputBindingKind.Begin), + onEnd = makeInputBinding(InputBindingKind.End), + onStep = makeInputBinding(InputBindingKind.Step), + onMoveStep = onMoveStepInputBinding, + } +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Input.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Input.spec.lua new file mode 100644 index 0000000..fdd1dff --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Input.spec.lua @@ -0,0 +1,312 @@ +return function() + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + + local Input = require(script.Parent.Input) + local createSpy = require(script.Parent.Test.createSpy) + local MockEngine = require(script.Parent.Test.MockEngine) + + describe("onBegin and onEnd", function() + it("should run respond to relevant events when onBegin is connected", function() + local mockEngine, engineInterface = MockEngine.new() + + local onBeginSpy = createSpy() + local onBegin = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonA, onBeginSpy.value) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.Begin, + }) + + expect(onBeginSpy.callCount).to.equal(0) + + local disconnector = Input.connectToEvent(onBegin, engineInterface) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.Begin, + }) + + expect(onBeginSpy.callCount).to.equal(1) + local captured = onBeginSpy:captureValues("inputObject") + + expect(captured.inputObject.KeyCode).to.equal(Enum.KeyCode.ButtonA) + expect(captured.inputObject.UserInputState).to.equal(Enum.UserInputState.Begin) + + -- When state is `End`, the callback shouldn't be called + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.End, + }) + expect(onBeginSpy.callCount).to.equal(1) + + -- After disconnecting, the callback shouldn't be called + disconnector() + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.Begin, + }) + expect(onBeginSpy.callCount).to.equal(1) + end) + + it("should run respond to relevant events when onEnd is connected", function() + local mockEngine, engineInterface = MockEngine.new() + + local onEndSpy = createSpy() + local onEnd = Input.PublicInterface.onEnd(Enum.KeyCode.ButtonA, onEndSpy.value) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.End, + }) + + expect(onEndSpy.callCount).to.equal(0) + + local disconnector = Input.connectToEvent(onEnd, engineInterface) + + -- When state is `Begin`, the callback shouldn't be called + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.Begin, + }) + expect(onEndSpy.callCount).to.equal(0) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.End, + }) + + expect(onEndSpy.callCount).to.equal(1) + local captured = onEndSpy:captureValues("inputObject") + + expect(captured.inputObject.KeyCode).to.equal(Enum.KeyCode.ButtonA) + expect(captured.inputObject.UserInputState).to.equal(Enum.UserInputState.End) + + -- After disconnecting, the callback shouldn't be called + disconnector() + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.End, + }) + expect(onEndSpy.callCount).to.equal(1) + end) + + it("should ignore events with unrelated KeyCodes", function() + local mockEngine, engineInterface = MockEngine.new() + + local onBeginSpy = createSpy() + local onBegin = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonA, onBeginSpy.value) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.End, + }) + + expect(onBeginSpy.callCount).to.equal(0) + + local disconnector = Input.connectToEvent(onBegin, engineInterface) + + -- When the KeyCode does not match, the callback won't be fired + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonB, + UserInputState = Enum.UserInputState.Begin, + }) + expect(onBeginSpy.callCount).to.equal(0) + + disconnector() + end) + + it("should keep a separate notion of gamepad state for each 'engine interface', for testing purposes", function() + local mockEngine1, engineInterface1 = MockEngine.new() + local mockEngine2, engineInterface2 = MockEngine.new() + + local onStepSpy1 = createSpy() + local onStep1 = Input.PublicInterface.onStep(Enum.KeyCode.Thumbstick1, onStepSpy1.value) + local disconnector1 = Input.connectToEvent(onStep1, engineInterface1) + + local onStepSpy2 = createSpy() + local onStep2 = Input.PublicInterface.onStep(Enum.KeyCode.Thumbstick1, onStepSpy2.value) + local disconnector2 = Input.connectToEvent(onStep2, engineInterface2) + + mockEngine1:simulateInput({ + KeyCode = Enum.KeyCode.Thumbstick1, + UserInputState = Enum.UserInputState.Begin, + Delta = Vector3.new(1, 1, 0), + }) + mockEngine1:renderStep(0.03) + + expect(onStepSpy1.callCount).to.equal(1) + expect(onStepSpy2.callCount).to.equal(0) + + local capturedValues1 = onStepSpy1:captureValues("inputObject", "_") + local thumbstickValue1 = capturedValues1.inputObject + expect(thumbstickValue1.Delta).to.equal(Vector3.new(1, 1, 0)) + + mockEngine2:simulateInput({ + KeyCode = Enum.KeyCode.Thumbstick1, + UserInputState = Enum.UserInputState.Begin, + Delta = Vector3.new(-1, -1, 0), + }) + mockEngine2:renderStep(0.03) + + expect(onStepSpy1.callCount).to.equal(1) + expect(onStepSpy2.callCount).to.equal(1) + + local capturedValues2 = onStepSpy2:captureValues("inputObject", "_") + local thumbstickValue2 = capturedValues2.inputObject + + -- Verify that the first one didn't change + expect(thumbstickValue1.Delta).to.equal(Vector3.new(1, 1, 0)) + expect(thumbstickValue2.Delta).to.equal(Vector3.new(-1, -1, 0)) + + disconnector1() + disconnector2() + end) + end) + + describe("onStep", function() + it("should fire once per render step when connected", function() + local mockEngine, engineInterface = MockEngine.new() + + local onStepSpy = createSpy() + local onStep = Input.PublicInterface.onStep(Enum.KeyCode.ButtonA, onStepSpy.value) + + expect(onStepSpy.callCount).to.equal(0) + local disconnector = Input.connectToEvent(onStep, engineInterface) + + mockEngine:renderStep(0.1) + + expect(onStepSpy.callCount).to.equal(1) + local captured = onStepSpy:captureValues("_inputObjects", "deltaTime") + expect(captured.deltaTime).to.equal(0.1) + + disconnector() + end) + + it("should accept a keyCode and provide a relevant inputObject to the callback", function() + local mockEngine, engineInterface = MockEngine.new() + + local onStepSpy = createSpy() + local onStep = Input.PublicInterface.onStep(Enum.KeyCode.Thumbstick1, onStepSpy.value) + + expect(onStepSpy.callCount).to.equal(0) + local disconnector = Input.connectToEvent(onStep, engineInterface) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.Thumbstick1, + UserInputState = Enum.UserInputState.Change, + Position = Vector3.new(0, 0, 0), + Delta = Vector3.new(0, 0, 0), + }) + mockEngine:renderStep(0.1) + + expect(onStepSpy.callCount).to.equal(1) + local captured = onStepSpy:captureValues("inputObject", "_deltaTime") + expect(captured.inputObject.KeyCode).to.equal(Enum.KeyCode.Thumbstick1) + expect(captured.inputObject.UserInputState).to.equal(Enum.UserInputState.Change) + expect(captured.inputObject.Position).to.equal(Vector3.new(0, 0, 0)) + expect(captured.inputObject.Delta).to.equal(Vector3.new(0, 0, 0)) + + disconnector() + end) + + it("should provide an up-to-date inputObject to the callback", function() + local mockEngine, engineInterface = MockEngine.new() + + local onStepSpy = createSpy() + local onStep = Input.PublicInterface.onStep(Enum.KeyCode.Thumbstick1, onStepSpy.value) + + expect(onStepSpy.callCount).to.equal(0) + local disconnector = Input.connectToEvent(onStep, engineInterface) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.Thumbstick1, + UserInputState = Enum.UserInputState.Change, + Position = Vector3.new(0, 0, 0), + Delta = Vector3.new(0, 0, 0), + }) + mockEngine:renderStep(0.1) + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.Thumbstick1, + UserInputState = Enum.UserInputState.End, + Position = Vector3.new(1, 0, 0), + Delta = Vector3.new(1, 0, 0), + }) + mockEngine:renderStep(0.1) + + expect(onStepSpy.callCount).to.equal(2) + local captured = onStepSpy:captureValues("inputObject", "_deltaTime") + expect(captured.inputObject.KeyCode).to.equal(Enum.KeyCode.Thumbstick1) + expect(captured.inputObject.UserInputState).to.equal(Enum.UserInputState.End) + expect(captured.inputObject.Position).to.equal(Vector3.new(1, 0, 0)) + expect(captured.inputObject.Delta).to.equal(Vector3.new(1, 0, 0)) + + disconnector() + end) + end) + + describe("onMoveStep", function() + local function simulateSeveralInputs(mockEngine, state, keyCodes) + for _, keyCode in ipairs(keyCodes) do + mockEngine:simulateInput({ + KeyCode = keyCode, + UserInputState = state, + }) + end + end + + it("should reflect the state of each input at each render step", function() + local mockEngine, engineInterface = MockEngine.new() + + local onMoveStepSpy = createSpy() + local onStep = Input.PublicInterface.onMoveStep(onMoveStepSpy.value) + + expect(onMoveStepSpy.callCount).to.equal(0) + local disconnector = Input.connectToEvent(onStep, engineInterface) + + simulateSeveralInputs(mockEngine, Enum.UserInputState.Begin, { + Enum.KeyCode.Thumbstick1, + Enum.KeyCode.DPadUp, + Enum.KeyCode.DPadDown, + Enum.KeyCode.DPadLeft, + Enum.KeyCode.DPadRight, + }) + mockEngine:renderStep(0.1) + + expect(onMoveStepSpy.callCount).to.equal(1) + + local captured = onMoveStepSpy:captureValues("inputObjects", "_deltaTime") + local captured = { + Thumbstick1 = captured.inputObjects[Enum.KeyCode.Thumbstick1], + DPadUp = captured.inputObjects[Enum.KeyCode.DPadUp], + DPadDown = captured.inputObjects[Enum.KeyCode.DPadDown], + DPadLeft = captured.inputObjects[Enum.KeyCode.DPadLeft], + DPadRight = captured.inputObjects[Enum.KeyCode.DPadRight], + } + expect(captured.Thumbstick1.UserInputState).to.equal(Enum.UserInputState.Begin) + expect(captured.DPadUp.UserInputState).to.equal(Enum.UserInputState.Begin) + expect(captured.DPadDown.UserInputState).to.equal(Enum.UserInputState.Begin) + expect(captured.DPadLeft.UserInputState).to.equal(Enum.UserInputState.Begin) + expect(captured.DPadRight.UserInputState).to.equal(Enum.UserInputState.Begin) + + simulateSeveralInputs(mockEngine, Enum.UserInputState.Change, { + Enum.KeyCode.Thumbstick1, + Enum.KeyCode.DPadUp, + Enum.KeyCode.DPadDown, + Enum.KeyCode.DPadLeft, + Enum.KeyCode.DPadRight, + }) + mockEngine:renderStep(0.1) + + -- Shouldn't need to recapture, since the inputObjects themselves + -- are being mutated directly by the engine + expect(captured.Thumbstick1.UserInputState).to.equal(Enum.UserInputState.Change) + expect(captured.DPadUp.UserInputState).to.equal(Enum.UserInputState.Change) + expect(captured.DPadDown.UserInputState).to.equal(Enum.UserInputState.Change) + expect(captured.DPadLeft.UserInputState).to.equal(Enum.UserInputState.Change) + expect(captured.DPadRight.UserInputState).to.equal(Enum.UserInputState.Change) + + disconnector() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/InputBindingKind.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/InputBindingKind.lua new file mode 100644 index 0000000..b95dc1f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/InputBindingKind.lua @@ -0,0 +1,9 @@ +local Packages = script.Parent.Parent +local enumerate = require(Packages.enumerate) + +return enumerate("InputBindingKind", { + "Begin", + "End", + "Step", + "MoveStep", +}) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Symbol.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Symbol.lua new file mode 100644 index 0000000..305d66a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Symbol.lua @@ -0,0 +1,30 @@ +--[[ + A 'Symbol' is an opaque marker type. + + Symbols have the type 'userdata', but when printed to the console, the name + of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/MockEngine.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/MockEngine.lua new file mode 100644 index 0000000..de61dbe --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/MockEngine.lua @@ -0,0 +1,246 @@ +local Packages = script.Parent.Parent.Parent +local Cryo = require(Packages.Cryo) + +local createSignal = require(script.Parent.Parent.createSignal) + +local MockEngine = {} +MockEngine.__index = MockEngine + +local VALID_INPUT_TYPES = { + [Enum.UserInputType.Gamepad1] = true, + [Enum.UserInputType.Gamepad2] = true, + [Enum.UserInputType.Gamepad3] = true, + [Enum.UserInputType.Gamepad4] = true, + [Enum.UserInputType.Gamepad5] = true, + [Enum.UserInputType.Gamepad6] = true, + [Enum.UserInputType.Gamepad7] = true, + [Enum.UserInputType.Gamepad8] = true, + [Enum.UserInputType.Keyboard] = true, +} + +local GAMEPAD_STATES = { + Enum.KeyCode.Thumbstick2, + Enum.KeyCode.DPadDown, + Enum.KeyCode.DPadUp, + Enum.KeyCode.ButtonL3, + Enum.KeyCode.ButtonL2, + Enum.KeyCode.DPadRight, + Enum.KeyCode.ButtonR1, + Enum.KeyCode.ButtonSelect, + Enum.KeyCode.ButtonStart, + Enum.KeyCode.ButtonY, + Enum.KeyCode.DPadLeft, + Enum.KeyCode.ButtonR2, + Enum.KeyCode.ButtonR3, + Enum.KeyCode.ButtonX, + Enum.KeyCode.Thumbstick1, + Enum.KeyCode.ButtonB, + Enum.KeyCode.ButtonA, + Enum.KeyCode.ButtonL1, +} + +local DIRECTIONAL_INPUTS = { + [Enum.KeyCode.DPadUp] = "NextSelectionUp", + [Enum.KeyCode.Up] = "NextSelectionUp", + [Enum.KeyCode.DPadDown] = "NextSelectionDown", + [Enum.KeyCode.Down] = "NextSelectionDown", + [Enum.KeyCode.DPadLeft] = "NextSelectionLeft", + [Enum.KeyCode.Left] = "NextSelectionLeft", + [Enum.KeyCode.DPadRight] = "NextSelectionRight", + [Enum.KeyCode.Right] = "NextSelectionRight", +} + +local function defaultInputObject(keyCode) + return { + KeyCode = keyCode, + + Delta = Vector3.new(), + Position = Vector3.new(), + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.End, + } +end + +local function wrapDisconnector(disconnect) + return { + Disconnect = disconnect + } +end + +function MockEngine.new() + local self = setmetatable({ + -- Mock engine state + __mockSelected = nil, + __connectedGamepads = {}, + __gamepadStates = {}, + + -- Signals that represent engine events firing + __selectionChangedSignal = createSignal(), + __inputSignal = createSignal(), + __renderStepped = createSignal(), + __gamepadConnected = createSignal(), + __gamepadDisconnected = createSignal(), + + __interface = nil, + }, MockEngine) + + local mockInterface = { + getSelection = function() + return self.__mockSelected + end, + setSelection = function(selectionTarget) + self.__mockSelected = selectionTarget + + -- Since this isn't driven by the engine, manually fire the signal + self.__selectionChangedSignal:fire() + end, + getGamepadConnected = function(id) + return self.__connectedGamepads[id] or false + end, + getGamepadState = function(id) + if self.__gamepadStates[id] == nil then + self:__initializeGamepadState(id) + end + + -- To mimic the real engine behavior, this returns a table that can + -- continue to be mutated by the mock engine + return self.__gamepadStates[id] + end, + getNavigationGamepads = function(id) + return Cryo.Dictionary.keys(self.__connectedGamepads) + end, + subscribeToSelectionChanged = function(callback) + local disconnector = self.__selectionChangedSignal:subscribe(callback) + + return wrapDisconnector(disconnector) + end, + subscribeToInputBegan = function(callback) + local disconnector = self.__inputSignal:subscribe(callback) + + return wrapDisconnector(disconnector) + end, + subscribeToInputChanged = function(callback) + local disconnector = self.__inputSignal:subscribe(callback) + + return wrapDisconnector(disconnector) + end, + subscribeToInputEnded = function(callback) + local disconnector = self.__inputSignal:subscribe(callback) + + return wrapDisconnector(disconnector) + end, + subscribeToRenderStepped = function(callback) + local disconnector = self.__renderStepped:subscribe(callback) + + return wrapDisconnector(disconnector) + end, + subscribeToGamepadConnected = function(callback) + local disconnector = self.__gamepadConnected:subscribe(callback) + + return wrapDisconnector(disconnector) + end, + subscribeToGamepadDisconnected = function(callback) + local disconnector = self.__gamepadDisconnected:subscribe(callback) + + return wrapDisconnector(disconnector) + end, + } + + self.__interface = mockInterface + + return self, mockInterface +end + +function MockEngine:__initializeGamepadState(id) + self.__gamepadStates[id] = Cryo.List.map(GAMEPAD_STATES, function(keyCode) + return defaultInputObject(keyCode) + end) +end + +function MockEngine:simulateSelectionChanged(selectionTarget) + self.__mockSelected = selectionTarget + self.__selectionChangedSignal:fire() +end + +function MockEngine:simulateInput(inputObject) + -- Simplify the input object so that we don't always need to provide all the + -- values + local inputObject = Cryo.Dictionary.join({ + Delta = Vector3.new(), + Position = Vector3.new(), + UserInputType = Enum.UserInputType.Gamepad1, + }, inputObject) + + assert(typeof(inputObject.KeyCode) == "EnumItem" and inputObject.KeyCode.EnumType == Enum.KeyCode, + "Invalid inputObject.KeyCode: expected a member of Enum.KeyCode") + assert(VALID_INPUT_TYPES[inputObject.UserInputType], "Invalid inputObject.UserInputType") + + -- Simulate navigational actions by jumping to relevant neighbors + local keyCode = inputObject.KeyCode + local neighborProperty = DIRECTIONAL_INPUTS[keyCode] + + -- To mimic engine behavior, this happens *before* the input signal is + -- processed + if neighborProperty ~= nil and self.__interface.getSelection() ~= nil then + self.__interface.setSelection(self.__interface.getSelection()[neighborProperty]) + end + + -- For gamepad inputs, update the gamepad state InputObject + if inputObject.UserInputType ~= Enum.UserInputType.Keyboard then + local gamepadId = inputObject.UserInputType + + -- As a shorthand, if the originating gamepad for this simulated input + -- isn't connected yet, we first simulate connecting that gamepad + if not self.__connectedGamepads[gamepadId] then + self:connectGamepad(gamepadId) + end + + local gamepadState = self.__gamepadStates[gamepadId] + local index = Cryo.List.findWhere(gamepadState, function(state) + return state.KeyCode == keyCode + end) + + if index == nil then + error(("Invalid InputObject: KeyCode %s is not possible on %s"):format( + tostring(keyCode), + tostring(gamepadId) + ), 2) + end + + for key, value in pairs(inputObject) do + gamepadState[index][key] = value + end + end + + -- Pass on input by firing the input signal with an approximation of a + -- InputObject + self.__inputSignal:fire(inputObject) +end + +function MockEngine:renderStep(deltaTime) + if deltaTime == nil then + deltaTime = 1 / 30 + end + + self.__renderStepped:fire(deltaTime) +end + +function MockEngine:connectGamepad(id) + assert(typeof(id) == "EnumItem" and id.EnumType == Enum.UserInputType, + "Invalid argument #1: expected a member of Enum.UserInputType") + + self.__connectedGamepads[id] = true + self:__initializeGamepadState(id) + + self.__gamepadConnected:fire(id) +end + +function MockEngine:disconnectGamepad(id) + assert(typeof(id) == "EnumItem" and id.EnumType == Enum.UserInputType, + "Invalid argument #1: expected a member of Enum.UserInputType") + + self.__connectedGamepads[id] = nil + self.__gamepadDisconnected:fire(id) +end + +return MockEngine \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/MockEngine.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/MockEngine.spec.lua new file mode 100644 index 0000000..fb4b849 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/MockEngine.spec.lua @@ -0,0 +1,210 @@ +return function() + local Packages = script.Parent.Parent.Parent + local Cryo = require(Packages.Cryo) + + local MockEngine = require(script.Parent.MockEngine) + local createSpy = require(script.Parent.createSpy) + + describe("selection logic simulation", function() + it("should fire a change signal when simulating selection change", function() + local mockEngine, engineInterface = MockEngine.new() + + local selectionChangedSpy = createSpy() + engineInterface.subscribeToSelectionChanged(selectionChangedSpy.value) + + local selectionTarget = Instance.new("Frame") + mockEngine:simulateSelectionChanged(selectionTarget) + + expect(selectionChangedSpy.callCount).to.equal(1) + expect(engineInterface.getSelection()).to.equal(selectionTarget) + end) + + it("should automatically simulate selection change when simulating gamepad directional inputs", function() + local mockEngine, engineInterface = MockEngine.new() + + -- Create two UI elements that are vertical neighbors + local upper = Instance.new("Frame") + local lower = Instance.new("Frame") + upper.NextSelectionDown = lower + lower.NextSelectionUp = upper + + -- Set starting selection to the upper element of the two + engineInterface.setSelection(upper) + expect(engineInterface.getSelection()).to.equal(upper) + + local selectionChangedSpy = createSpy() + local inputBeganSpy = createSpy() + engineInterface.subscribeToSelectionChanged(selectionChangedSpy.value) + engineInterface.subscribeToInputBegan(inputBeganSpy.value) + + -- Simulate a downward input, and confirm that the expected + -- selection change occurs and selection is now on the lower element + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.DPadDown + }) + expect(inputBeganSpy.callCount).to.equal(1) + expect(selectionChangedSpy.callCount).to.equal(1) + expect(engineInterface.getSelection()).to.equal(lower) + + -- Simulate a downward input, and confirm that the expected + -- selection change occurs and selection returns to the upper + -- element + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.DPadUp + }) + expect(inputBeganSpy.callCount).to.equal(2) + expect(selectionChangedSpy.callCount).to.equal(2) + expect(engineInterface.getSelection()).to.equal(upper) + end) + + it("should automatically simulate selection change when simulating keyboard directional inputs", function() + local mockEngine, engineInterface = MockEngine.new() + + -- Create two UI elements that are vertical neighbors + local upper = Instance.new("Frame") + local lower = Instance.new("Frame") + upper.NextSelectionDown = lower + lower.NextSelectionUp = upper + + -- Set starting selection to the upper element of the two + engineInterface.setSelection(upper) + expect(engineInterface.getSelection()).to.equal(upper) + + local selectionChangedSpy = createSpy() + local inputBeganSpy = createSpy() + engineInterface.subscribeToSelectionChanged(selectionChangedSpy.value) + engineInterface.subscribeToInputBegan(inputBeganSpy.value) + + -- Simulate a downward input, and confirm that the expected + -- selection change occurs and selection is now on the lower element + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.DPadDown, + UserInputState = Enum.UserInputState.Begin, + }) + expect(inputBeganSpy.callCount).to.equal(1) + expect(selectionChangedSpy.callCount).to.equal(1) + expect(engineInterface.getSelection()).to.equal(lower) + + -- Simulate a downward input, and confirm that the expected + -- selection change occurs and selection returns to the upper + -- element + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.DPadUp, + UserInputState = Enum.UserInputState.Begin, + }) + expect(inputBeganSpy.callCount).to.equal(2) + expect(selectionChangedSpy.callCount).to.equal(2) + expect(engineInterface.getSelection()).to.equal(upper) + end) + end) + + describe("gamepad state simulation", function() + it("should fire events when gamepads are connected and disconnected", function() + local mockEngine, engineInterface = MockEngine.new() + + local gamepadConnectedSpy = createSpy() + local gamepadDisconnectedSpy = createSpy() + + engineInterface.subscribeToGamepadConnected(gamepadConnectedSpy.value) + engineInterface.subscribeToGamepadDisconnected(gamepadDisconnectedSpy.value) + + expect(gamepadConnectedSpy.callCount).to.equal(0) + expect(gamepadDisconnectedSpy.callCount).to.equal(0) + + local gamepad1 = Enum.UserInputType.Gamepad1 + mockEngine:connectGamepad(gamepad1) + + expect(gamepadConnectedSpy.callCount).to.equal(1) + gamepadConnectedSpy:assertCalledWith(gamepad1) + expect(gamepadDisconnectedSpy.callCount).to.equal(0) + + mockEngine:disconnectGamepad(gamepad1) + + expect(gamepadConnectedSpy.callCount).to.equal(1) + expect(gamepadDisconnectedSpy.callCount).to.equal(1) + gamepadDisconnectedSpy:assertCalledWith(gamepad1) + end) + + it("should report gamepad connection status", function() + local mockEngine, engineInterface = MockEngine.new() + + expect(engineInterface.getGamepadConnected(gamepad1)).to.equal(false) + + local gamepad1 = Enum.UserInputType.Gamepad1 + mockEngine:connectGamepad(gamepad1) + + expect(engineInterface.getGamepadConnected(gamepad1)).to.equal(true) + + mockEngine:disconnectGamepad(gamepad1) + + expect(engineInterface.getGamepadConnected(gamepad1)).to.equal(false) + end) + + it("should automatically 'connect' a gamepad when a matching input is simulated", function() + local mockEngine, engineInterface = MockEngine.new() + + expect(engineInterface.getGamepadConnected(Enum.UserInputType.Gamepad1)).to.equal(false) + + mockEngine:simulateInput({ + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.Begin, + UserInputType = Enum.UserInputType.Gamepad1, + }) + + expect(engineInterface.getGamepadConnected(Enum.UserInputType.Gamepad1)).to.equal(true) + end) + + it("should give access to current gamepad input state", function() + local mockEngine, engineInterface = MockEngine.new() + + local gamepad1 = Enum.UserInputType.Gamepad1 + mockEngine:connectGamepad(gamepad1) + + -- Check that a valid state is returned even when no inputs for the + -- given keyCode have been issued. There should be an entry for each + -- possible KeyCode (Thumbstick 1 and 2, L1/L2/L3, R1/R2/R3, + -- A/B/X/Y, Select/Start, DPad up/down/left/right), 18 total + local gamepadState = engineInterface.getGamepadState(gamepad1) + expect(#gamepadState).to.equal(18) + + local buttonAIndex = Cryo.List.findWhere(gamepadState, function(inputObject) + return inputObject.KeyCode == Enum.KeyCode.ButtonA + end) + expect(buttonAIndex).to.be.ok() + + local buttonAInputObject = gamepadState[buttonAIndex] + + -- A quick consistency check with expected default values + expect(buttonAInputObject.UserInputType).to.equal(gamepad1) + expect(buttonAInputObject.UserInputState).to.equal(Enum.UserInputState.End) + + mockEngine:simulateInput({ + UserInputType = gamepad1, + KeyCode = Enum.KeyCode.ButtonA, + UserInputState = Enum.UserInputState.Begin, + }) + + -- The engine will directly mutate the InputObjects it returns from + -- `GetGamepadState`, so we simulate the same behavior here. This + -- means we can check that the input object we already saved to + -- buttonAInputObject to mutate in response to inputs + expect(buttonAInputObject.UserInputState).to.equal(Enum.UserInputState.Begin) + end) + end) + + describe("render step simulation", function() + it("should fire subscribed events when render steps are simulated", function() + local mockEngine, engineInterface = MockEngine.new() + + local renderSteppedSpy = createSpy() + engineInterface.subscribeToRenderStepped(renderSteppedSpy.value) + + expect(renderSteppedSpy.callCount).to.equal(0) + + mockEngine:renderStep(0.03) + + expect(renderSteppedSpy.callCount).to.equal(1) + renderSteppedSpy:assertCalledWith(0.03) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/createSpy.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/createSpy.lua new file mode 100644 index 0000000..3423ab5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/Test/createSpy.lua @@ -0,0 +1,67 @@ +--[[ + A utility used to create a function spy that can be used to robustly test + that functions are invoked the correct number of times and with the correct + number of arguments. + + This should only be used in tests. +]] +local function createSpy(inner) + local self = { + callCount = 0, + values = {}, + valuesLength = 0, + } + + self.value = function(...) + self.callCount = self.callCount + 1 + self.values = {...} + self.valuesLength = select("#", ...) + + if inner ~= nil then + return inner(...) + end + + return + end + + self.assertCalledWith = function(_, ...) + local len = select("#", ...) + + if self.valuesLength ~= len then + error(("Expected %d arguments, but was called with %d arguments"):format( + self.valuesLength, + len + ), 2) + end + + for i = 1, len do + local expected = select(i, ...) + + assert(self.values[i] == expected, "value differs; got " .. tostring(self.values[i]) .. ", expected " .. tostring(expected)) + end + end + + self.captureValues = function(_, ...) + local len = select("#", ...) + local result = {} + + assert(self.valuesLength == len, "length of expected values differs from stored values") + + for i = 1, len do + local key = select(i, ...) + result[key] = self.values[i] + end + + return result + end + + setmetatable(self, { + __index = function(_, key) + error(("%q is not a valid member of spy"):format(key)) + end, + }) + + return self +end + +return createSpy \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/asFocusable.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/asFocusable.lua new file mode 100644 index 0000000..1c48de6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/asFocusable.lua @@ -0,0 +1,298 @@ +--!nonstrict +-- Manages groups of selectable elements, reacting to selection changes for +-- individual items and triggering events for group selection changes +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local t = require(Packages.t) + +local FocusContext = require(script.Parent.FocusContext) +local forwardRef = require(script.Parent.forwardRef) +local FocusNode = require(script.Parent.FocusNode) +local getEngineInterface = require(script.Parent.getEngineInterface) + +local InternalApi = require(script.Parent.FocusControllerInternalApi) + +local nonHostProps = { + parentFocusNode = Cryo.None, + parentNeighbors = Cryo.None, + focusController = Cryo.None, + + onFocusGained = Cryo.None, + onFocusLost = Cryo.None, + onFocusChanged = Cryo.None, + inputBindings = Cryo.None, + + restorePreviousChildFocus = Cryo.None, + defaultChild = Cryo.None, +} + +local function checkFocusManager(props) + if props.focusController ~= nil and props.parentFocusNode ~= nil then + return false, "Cannot attach a new focusController beneath an existing one" + end + + return true +end + +local focusableValidateProps = t.intersection(t.interface({ + parentFocusNode = t.optional(t.table), + focusController = t.optional(t.table), + [Roact.Ref] = t.table, + + restorePreviousChildFocus = t.boolean, + inputBindings = t.table, + defaultChild = t.optional(t.table), + + onFocusGained = t.optional(t.callback), + onFocusLost = t.optional(t.callback), + onFocusChanged = t.optional(t.callback), +}), checkFocusManager) + +local focusableDefaultProps = { + restorePreviousChildFocus = false, + inputBindings = {}, +} + +--[[ + Identifies an instance as a focusable element or a group of focusable + elements. Injects a navigation context object that propagates its own + navigational props to any children that are also selectable. +]] +local function asFocusable(innerComponent) + local componentName = ("Focusable(%s)"):format(tostring(innerComponent)) + + -- Selection container component; groups together children and reacts to changes + -- in GuiService.SelectedObject + local Focusable = Roact.Component:extend(componentName) + + Focusable.validateProps = focusableValidateProps + Focusable.defaultProps = focusableDefaultProps + + function Focusable:init() + self.focused = false + + local parentNeighbors = self.props.parentNeighbors or {} + self.navContext = { + focusNode = FocusNode.new(self.props), + neighbors = { + NextSelectionLeft = self.props.NextSelectionLeft or parentNeighbors.NextSelectionLeft, + NextSelectionRight = self.props.NextSelectionRight or parentNeighbors.NextSelectionRight, + NextSelectionUp = self.props.NextSelectionUp or parentNeighbors.NextSelectionUp, + NextSelectionDown = self.props.NextSelectionDown or parentNeighbors.NextSelectionDown, + } + } + + self.updateFocusedState = function(newFocusedState) + if not self.focused and newFocusedState then + self:gainFocus() + elseif self.focused and not newFocusedState then + self:loseFocus() + end + end + + if self:isRoot() then + local isRooted = false + -- If this Focusable needs to behave as a root, it is responsible for + -- initializing the FocusManager. Once it becomes a descendant of + -- `game`, we initialize the FocusManager which determines which sort of + -- PlayerGui this focus tree is contained under + self.ancestryChanged = function(instance) + if not isRooted and instance:IsDescendantOf(game) then + isRooted = true + self:getFocusControllerInternal():initialize(getEngineInterface(instance)) + end + end + + -- This function is called separately, since we don't want to falsely + -- trigger an existing callback in props when we call + -- `ancestryChanged` in didMount + self.ancestryChangedListener = function(instance) + self.ancestryChanged(instance) + + local existingCallback = self.props[Roact.Event.AncestryChanged] + if existingCallback ~= nil then + existingCallback(instance) + end + end + + self.refreshFocusOnDescendantAdded = function(descendant) + self:getFocusControllerInternal():descendantAddedRefocus() + + local existingCallback = self.props[Roact.Event.DescendantAdded] + if existingCallback ~= nil then + existingCallback(descendant) + end + end + + self.refreshFocusOnDescendantRemoved = function(descendant) + self:getFocusControllerInternal():descendantRemovedRefocus() + + local existingCallback = self.props[Roact.Event.DescendantRemoving] + if existingCallback ~= nil then + existingCallback(descendant) + end + end + end + end + + function Focusable:willUpdate(nextProps) + -- Here, we need to carefully update the navigation context according to + -- the incoming props. There are three different categories of prop + -- changes we have to deal with. + + -- 1. Apply the changes from the navigation props themselves. These only + -- affect navigation behavior for this node's ref and do not need to + -- cascade to other parts of the tree + self.navContext.focusNode:updateNavProps(nextProps) + + -- 2. If neighbors changed, we need to cascade this change through + -- context, so we make sure the value that we pass to context has a + -- _new_ identity + if + nextProps.NextSelectionLeft ~= self.navContext.neighbors.NextSelectionLeft + or nextProps.NextSelectionRight ~= self.navContext.neighbors.NextSelectionRight + or nextProps.NextSelectionDown ~= self.navContext.neighbors.NextSelectionDown + or nextProps.NextSelectionUp ~= self.navContext.neighbors.NextSelectionUp + or nextProps.parentNeighbors ~= self.props.parentNeighbors + then + local parentNeighbors = nextProps.parentNeighbors or {} + self.navContext = { + focusNode = self.navContext.focusNode, + neighbors = { + NextSelectionLeft = nextProps.NextSelectionLeft or parentNeighbors.NextSelectionLeft, + NextSelectionRight = nextProps.NextSelectionRight or parentNeighbors.NextSelectionRight, + NextSelectionUp = nextProps.NextSelectionUp or parentNeighbors.NextSelectionUp, + NextSelectionDown = nextProps.NextSelectionDown or parentNeighbors.NextSelectionDown, + } + } + end + + -- 3. Finally, if the ref changed, then for now we simply get angry and + -- throw an error; we'll likely have to manage this another way + -- anyways! + if self.navContext.focusNode.ref ~= nextProps[Roact.Ref] then + error("Cannot change the ref passed to a Focusable component", 0) + end + end + + function Focusable:gainFocus() + self.focused = true + + if self.props.onFocusGained ~= nil then + self.props.onFocusGained() + end + + if self.props.onFocusChanged ~= nil then + self.props.onFocusChanged(true) + end + end + + function Focusable:loseFocus() + self.focused = false + + if self.props.onFocusLost ~= nil then + self.props.onFocusLost() + end + + if self.props.onFocusChanged ~= nil then + self.props.onFocusChanged(false) + end + end + + -- Determines whether or not this Focusable is supposed to be the root of a + -- focusable tree, determined by whether or not it has parent or focus props + -- provided + function Focusable:isRoot() + return self.props.focusController ~= nil and self.props.parentFocusNode == nil + end + + function Focusable:getFocusControllerInternal() + return self.navContext.focusNode.focusController[InternalApi] + end + + function Focusable:render() + local ref = self.props[Roact.Ref] + local childDefaultNavProps = { + NextSelectionLeft = ref, + NextSelectionRight = ref, + NextSelectionDown = ref, + NextSelectionUp = ref, + + [Roact.Ref] = ref, + } + + local innerProps + if self:isRoot() then + local rootNavProps = { + [Roact.Event.AncestryChanged] = self.ancestryChangedListener, + [Roact.Event.DescendantAdded] = self.refreshFocusOnDescendantAdded, + [Roact.Event.DescendantRemoving] = self.refreshFocusOnDescendantRemoved, + } + + innerProps = Cryo.Dictionary.join( + childDefaultNavProps, + self.props, + rootNavProps, + nonHostProps + ) + else + innerProps = Cryo.Dictionary.join( + childDefaultNavProps, + self.props.parentNeighbors or {}, + self.props, + nonHostProps + ) + end + + -- We pass the inner component as a single child (instead of part of a + -- table of children) because it causes Roact to reuse the key provided + -- to _this_ component when naming the resulting object. This means that + -- Focusable avoids disrupting the naming of the Instance hierarchy + return Roact.createElement(FocusContext.Provider, { + value = self.navContext, + }, Roact.createElement(innerComponent, innerProps)) + end + + function Focusable:didMount() + self.navContext.focusNode:attachToTree(self.props.parentFocusNode, self.updateFocusedState) + + if self:isRoot() then + -- Ancestry change may not trigger if the UI elements we're mounting + -- to were previously mounted to the DataModel already + self.ancestryChanged(self.props[Roact.Ref]:getValue()) + end + end + + function Focusable:willUnmount() + self.navContext.focusNode:detachFromTree() + + if self:isRoot() then + self:getFocusControllerInternal():teardown() + end + end + + return forwardRef(function(props, ref) + return Roact.createElement(FocusContext.Consumer, { + render = function(navContext) + if navContext == nil and props.focusController == nil then + -- If this component can't be the root, and there's no + -- parent, behave like the underlying component and ignore + -- all focus logic + local hostPropsOnly = Cryo.Dictionary.join(props, nonHostProps) + return Roact.createElement(innerComponent, hostPropsOnly) + end + + local propsWithNav = Cryo.Dictionary.join(props, { + parentFocusNode = navContext and navContext.focusNode or nil, + parentNeighbors = navContext and navContext.neighbors or nil, + [Roact.Ref] = ref, + }) + + return Roact.createElement(Focusable, propsWithNav) + end, + }) + end) +end + +return asFocusable \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/asFocusable.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/asFocusable.spec.lua new file mode 100644 index 0000000..acae647 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/asFocusable.spec.lua @@ -0,0 +1,801 @@ +return function() + local Players = game:GetService("Players") + + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + + local asFocusable = require(script.Parent.asFocusable) + local createRefCache = require(script.Parent.createRefCache) + local FocusContext = require(script.Parent.FocusContext) + local FocusNode = require(script.Parent.FocusNode) + local Input = require(script.Parent.Input) + local FocusController = require(script.Parent.FocusController) + local InternalApi = require(script.Parent.FocusControllerInternalApi) + local MockEngine = require(script.Parent.Test.MockEngine) + local createSpy = require(script.Parent.Test.createSpy) + + local function createRootNode(ref) + local node = FocusNode.new({ + focusController = FocusController.createPublicApiWrapper(), + [Roact.Ref] = ref, + }) + + node:attachToTree(nil, function() end) + + return node + end + + local function createTestContainer() + local rootRef, _ = Roact.createBinding(Instance.new("Frame")) + local focusNode = createRootNode(rootRef) + local mockEngine, engineInterface = MockEngine.new() + focusNode.focusController[InternalApi]:initialize(engineInterface) + + return { + rootRef = rootRef, + rootFocusNode = focusNode, + focusController = focusNode.focusController[InternalApi], + + mockEngine = mockEngine, + engineInterface = engineInterface, + getNode = function(ref) + return focusNode.focusController[InternalApi].allNodes[ref] + end, + FocusProvider = function(props) + return Roact.createElement(FocusContext.Provider, { + value = { + focusNode = focusNode, + } + }, props[Roact.Children]) + end + } + end + + describe("Focusable component basics", function() + it("adds a new node to the focus tree when it mounts", function() + local testContainer = createTestContainer() + + local FocusableFrame = asFocusable("Frame") + + local injectedRef = Roact.createRef() + local tree = Roact.mount(Roact.createElement(testContainer.FocusProvider, {}, { + FocusChild = Roact.createElement(FocusableFrame, { + [Roact.Ref] = injectedRef, + }), + })) + + local focusController = testContainer.focusController + + expect(injectedRef:getValue()).to.be.ok() + expect(focusController.allNodes[injectedRef]).to.be.ok() + + local children = focusController:getChildren(testContainer.rootFocusNode) + expect(children[injectedRef]).to.be.ok() + + Roact.unmount(tree) + end) + + it("removes nodes from the focus tree when the component unmounts", function() + local testContainer = createTestContainer() + + local FocusableFrame = asFocusable("Frame") + + local injectedRef = Roact.createRef() + local tree = Roact.mount(Roact.createElement(testContainer.FocusProvider, {}, { + FocusChild = Roact.createElement(FocusableFrame, { + [Roact.Ref] = injectedRef, + }), + })) + + -- Update the tree with the child focusable frame absent, which will + -- unmount it from the tree + Roact.update(tree, Roact.createElement(testContainer.FocusProvider)) + + local focusController = testContainer.focusController + + expect(injectedRef:getValue()).to.equal(nil) + expect(focusController.allNodes[injectedRef]).to.equal(nil) + + local children = focusController:getChildren(testContainer.rootFocusNode) + expect(children[injectedRef]).to.equal(nil) + + Roact.unmount(tree) + end) + + it("triggers callbacks when focus changes", function() + local testContainer = createTestContainer() + local FocusableFrame = asFocusable("Frame") + + local focusGainedSpy = createSpy() + local focusLostSpy = createSpy() + local focusChangedSpy = createSpy() + + local childRefA = Roact.createRef() + local childRefB = Roact.createRef() + local tree = Roact.mount(Roact.createElement(testContainer.FocusProvider, {}, { + FocusChildA = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefA, + + onFocusGained = focusGainedSpy.value, + onFocusLost = focusLostSpy.value, + onFocusChanged = focusChangedSpy.value, + }), + FocusChildB = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefB, + }) + }), testContainer.rootRef:getValue()) + + testContainer.focusController:moveFocusTo(childRefA) + + expect(focusGainedSpy.callCount).to.equal(1) + expect(focusChangedSpy.callCount).to.equal(1) + focusChangedSpy:assertCalledWith(true) + + testContainer.focusController:moveFocusTo(childRefB) + + expect(focusLostSpy.callCount).to.equal(1) + expect(focusChangedSpy.callCount).to.equal(2) + focusChangedSpy:assertCalledWith(false) + + Roact.unmount(tree) + end) + + it("triggers callbacks when focus is released", function() + local testContainer = createTestContainer() + local FocusableFrame = asFocusable("Frame") + + local focusLostSpy = createSpy() + local focusChangedSpy = createSpy() + + local childRefA = Roact.createRef() + local tree = Roact.mount(Roact.createElement(testContainer.FocusProvider, {}, { + FocusChildA = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefA, + + onFocusLost = focusLostSpy.value, + onFocusChanged = focusChangedSpy.value, + }), + }), testContainer.rootRef:getValue()) + + testContainer.focusController:moveFocusTo(childRefA) + expect(focusChangedSpy.callCount).to.equal(1) + focusChangedSpy:assertCalledWith(true) + + testContainer.focusController:releaseFocus() + + expect(focusLostSpy.callCount).to.equal(1) + expect(focusChangedSpy.callCount).to.equal(2) + focusChangedSpy:assertCalledWith(false) + + Roact.unmount(tree) + end) + end) + + describe("Root vs non-root Focusable", function() + it("should ignore focusable logic if no parent or controller is provided", function() + local FocusableFrame = asFocusable("Frame") + local focusGainedSpy = createSpy() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + onFocusGained = focusGainedSpy.value, + })) + + expect(focusGainedSpy.callCount).to.equal(0) + + Roact.unmount(tree) + end) + + it("should initialize and teardown focusController when it's the root", function() + local FocusableFrame = asFocusable("Frame") + + -- This test is testing the automatic initialization of the internal + -- focusController based on the instance tree it's attached to. To + -- do this, we depend on the using a PlayerGui instance to avoid + -- simulate the real-world use case instead of the mock engine + expect(Players.LocalPlayer.PlayerGui).to.be.ok() + + local focusController = FocusController.createPublicApiWrapper() + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + }), Players.LocalPlayer.PlayerGui) + + expect(focusController[InternalApi].engineInterface).never.to.equal(nil) + + Roact.unmount(tree) + + expect(focusController[InternalApi].engineInterface).to.equal(nil) + end) + + it("should inherit parent neighbors through multiple layers", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + local function getNode(ref) + return focusController[InternalApi].allNodes[ref] + end + + local refs = createRefCache() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + [Roact.Ref] = refs.root, + }, { + TopSelectionTarget = Roact.createElement(FocusableFrame, { + NextSelectionDown = refs.bottomFocusable, + [Roact.Ref] = refs.topFocusable, + }), + BottomSelectionTarget = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.bottomFocusable, + NextSelectionUp = refs.topFocusable, + }, { + IntermediateChild = Roact.createElement(FocusableFrame, {}, { + -- This focusable child should be able to inherit + -- neighbors from its grandparent + LeafChild = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.bottomLeaf, + }) + }), + }), + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local mockEngine, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + -- Initialize gamepad focus to the top element + local topNode = getNode(refs.topFocusable) + topNode:focus() + expect(focusControllerInternal:isNodeFocused(topNode)).to.equal(true) + + -- Move focus down; this should work without neighbor propagation + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.DPadDown, + }) + + local bottomLeafNode = getNode(refs.bottomLeaf) + expect(focusControllerInternal:isNodeFocused(bottomLeafNode)).to.equal(true) + + -- Move focus up; this only works if grandparents' neighbors get + -- passed down correctly + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.DPadUp, + }) + expect(focusControllerInternal:isNodeFocused(topNode)).to.equal(true) + + Roact.unmount(tree) + end) + end) + + -- These tests rely on the fact that a FocusController passed to a Focusable + -- component will _not_ be automatically initialized if it's not mounted + -- under a PlayerGui. We leverage this technicality to initialize it + -- ourselves with the mock engine interface. + describe("Refresh focus logic", function() + it("should redirect focus to the parent when a focused child is detached", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + local function getNode(ref) + return focusController[InternalApi].allNodes[ref] + end + + local refs = createRefCache() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + [Roact.Ref] = refs.root, + }, { + FocusChild = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.child, + }), + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local _, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + local childNode = getNode(refs.child) + childNode:focus() + expect(focusControllerInternal:isNodeFocused(childNode)).to.equal(true) + + tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + [Roact.Ref] = refs.root, + })) + + local rootNode = getNode(refs.root) + expect(focusControllerInternal:isNodeFocused(rootNode)).to.equal(true) + + Roact.unmount(tree) + end) + + it("should trigger parent focus logic when a focused child is detached", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + local function getNode(ref) + return focusController[InternalApi].allNodes[ref] + end + + local refs = createRefCache() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + FocusChildA = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.childA, + }), + FocusChildB = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.childB, + }), + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local _, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + local childNodeA = getNode(refs.childA) + childNodeA:focus() + expect(focusControllerInternal:isNodeFocused(childNodeA)).to.equal(true) + + tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + FocusChildB = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.childB, + }), + })) + + local childNodeB = getNode(refs.childB) + expect(focusControllerInternal:isNodeFocused(childNodeB)).to.equal(true) + + Roact.unmount(tree) + end) + + it("should trigger parent focus logic when a node has children added to it", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + local function getNode(ref) + return focusController[InternalApi].allNodes[ref] + end + + local refs = createRefCache() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + [Roact.Ref] = refs.root, + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local _, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + focusController.captureFocus() + local rootNode = getNode(refs.root) + expect(focusControllerInternal:isNodeFocused(rootNode)).to.equal(true) + + tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + [Roact.Ref] = refs.root, + }, { + FocusChild = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.child, + }), + })) + + local childNode = getNode(refs.child) + expect(focusControllerInternal:isNodeFocused(childNode)).to.equal(true) + + Roact.unmount(tree) + end) + + it("should not refocus when adding children to a parent that already has at least one child", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + local function getNode(ref) + return focusController[InternalApi].allNodes[ref] + end + + local childRefA = Roact.createRef() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + FocusChildA = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefA, + }), + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local _, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + local childNodeA = getNode(childRefA) + childNodeA:focus() + expect(focusControllerInternal:isNodeFocused(childNodeA)).to.equal(true) + + tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + FocusChildA = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefA, + }), + FocusChildB = Roact.createElement(FocusableFrame), + })) + + -- Focus should not have moved as a result of the above change + expect(focusControllerInternal:isNodeFocused(childNodeA)).to.equal(true) + + Roact.unmount(tree) + end) + + it("should clean up input event subscriptions when the Focusable they're bound to is detached", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + + local beginCallbackSpy, moveStepCallbackSpy = createSpy(), createSpy() + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + FocusChildA = Roact.createElement(FocusableFrame, { + inputBindings = { + Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, beginCallbackSpy.value), + Input.PublicInterface.onMoveStep(moveStepCallbackSpy.value), + } + }), + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local mockEngine, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + focusController.captureFocus() + + expect(beginCallbackSpy.callCount).to.equal(0) + expect(moveStepCallbackSpy.callCount).to.equal(0) + + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + mockEngine:renderStep() + expect(beginCallbackSpy.callCount).to.equal(1) + expect(moveStepCallbackSpy.callCount).to.equal(1) + + -- Remove the child from the tree, which will also nil its parents + -- and trigger any auto-refocusing logic + local tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + -- Child was removed + }), nil) + + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + mockEngine:renderStep() + expect(beginCallbackSpy.callCount).to.equal(1) + expect(moveStepCallbackSpy.callCount).to.equal(1) + + Roact.unmount(tree) + end) + + it("should clean up input event subscriptions when the whole tree is cleaned up", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + + local beginCallbackSpy, moveStepCallbackSpy = createSpy(), createSpy() + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + inputBindings = { + Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, beginCallbackSpy.value), + Input.PublicInterface.onMoveStep(moveStepCallbackSpy.value), + } + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local mockEngine, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + focusController.captureFocus() + expect(beginCallbackSpy.callCount).to.equal(0) + expect(moveStepCallbackSpy.callCount).to.equal(0) + + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + mockEngine:renderStep() + expect(beginCallbackSpy.callCount).to.equal(1) + expect(moveStepCallbackSpy.callCount).to.equal(1) + + -- Remove the child from the tree, which will also nil its parents + -- and trigger any auto-refocusing logic + Roact.unmount(tree) + + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + mockEngine:renderStep() + expect(beginCallbackSpy.callCount).to.equal(1) + expect(moveStepCallbackSpy.callCount).to.equal(1) + end) + end) + + describe("Component behavior", function() + it("should not replace any provided event handlers", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + + local ancestryChangedSpy = createSpy() + local descendantAddedSpy = createSpy() + local descendantRemovedSpy = createSpy() + local rootRef = Roact.createRef() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + [Roact.Ref] = rootRef, + + [Roact.Event.AncestryChanged] = ancestryChangedSpy.value, + [Roact.Event.DescendantAdded] = descendantAddedSpy.value, + [Roact.Event.DescendantRemoving] = descendantRemovedSpy.value, + }), nil) + + expect(ancestryChangedSpy.callCount).to.equal(0) + expect(descendantAddedSpy.callCount).to.equal(0) + expect(descendantRemovedSpy.callCount).to.equal(0) + + local newParentFrame = Instance.new("Frame") + rootRef:getValue().Parent = newParentFrame + expect(ancestryChangedSpy.callCount).to.equal(1) + + local newChildFrame = Instance.new("Frame") + newChildFrame.Parent = rootRef:getValue() + expect(descendantAddedSpy.callCount).to.equal(1) + + newChildFrame.Parent = nil + expect(descendantRemovedSpy.callCount).to.equal(1) + + Roact.unmount(tree) + end) + + it("should update navigation logic correctly when props update", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + + local xBindingSpy = createSpy() + local yBindingSpy = createSpy() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + inputBindings = { + onXButton = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonX, xBindingSpy.value), + } + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local mockEngine, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + focusController.captureFocus() + expect(xBindingSpy.callCount).to.equal(0) + expect(yBindingSpy.callCount).to.equal(0) + + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + expect(xBindingSpy.callCount).to.equal(1) + expect(yBindingSpy.callCount).to.equal(0) + + tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + inputBindings = { + onYButton = Input.PublicInterface.onBegin(Enum.KeyCode.ButtonY, yBindingSpy.value), + } + })) + + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonY, + }) + expect(xBindingSpy.callCount).to.equal(1) + expect(yBindingSpy.callCount).to.equal(1) + + -- Pressing X again does not trigger the old callback + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.ButtonX, + }) + expect(xBindingSpy.callCount).to.equal(1) + + Roact.unmount(tree) + end) + + it("should propagate updates to inherited parent neighbor relationships", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + local function getNode(ref) + return focusController[InternalApi].allNodes[ref] + end + + local parentRefA = Roact.createRef() + local parentRefB = Roact.createRef() + local childRefA = Roact.createRef() + local childRefB = Roact.createRef() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + ParentA = Roact.createElement(FocusableFrame, { + NextSelectionDown = parentRefB, + [Roact.Ref] = parentRefA, + }, { + ChildA = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefA, + }) + }), + ParentB = Roact.createElement(FocusableFrame, { + -- No neighbors set + [Roact.Ref] = parentRefB, + }, { + ChildB = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefB, + }) + }), + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local mockEngine, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + local childNodeA = getNode(childRefA) + local childNodeB = getNode(childRefB) + + childNodeA:focus() + expect(focusControllerInternal:isNodeFocused(childNodeA)).to.equal(true) + + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.DPadDown, + }) + expect(focusControllerInternal:isNodeFocused(childNodeB)).to.equal(true) + + -- Try moving back up; no neighbors are set, and none are inherited + -- from the parents, so this shouldn't cause the selection to move + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.DPadUp, + }) + expect(focusControllerInternal:isNodeFocused(childNodeB)).to.equal(true) + + -- Update the tree to introduce an upward neighbor + tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + ParentA = Roact.createElement(FocusableFrame, { + NextSelectionDown = parentRefB, + [Roact.Ref] = parentRefA, + }, { + ChildA = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefA, + }) + }), + ParentB = Roact.createElement(FocusableFrame, { + NextSelectionUp = parentRefA, + [Roact.Ref] = parentRefB, + }, { + ChildB = Roact.createElement(FocusableFrame, { + [Roact.Ref] = childRefB, + }) + }), + })) + + -- Make sure we're still on B like before + expect(focusControllerInternal:isNodeFocused(childNodeB)).to.equal(true) + + -- This time, moving back up should work as expected + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.DPadUp, + }) + expect(focusControllerInternal:isNodeFocused(childNodeA)).to.equal(true) + + Roact.unmount(tree) + end) + + it("should propagate updates to inherited grandparent neighbor relationships", function() + local FocusableFrame = asFocusable("Frame") + local focusController = FocusController.createPublicApiWrapper() + local function getNode(ref) + return focusController[InternalApi].allNodes[ref] + end + + local refs = createRefCache() + + local tree = Roact.mount(Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + TopSelectionTarget = Roact.createElement(FocusableFrame, { + NextSelectionDown = refs.bottomFocusable, + [Roact.Ref] = refs.topFocusable, + }), + BottomSelectionTarget = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.bottomFocusable, + }, { + IntermediateChild = Roact.createElement(FocusableFrame, {}, { + LeafChild = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.bottomLeaf, + }) + }), + }), + }), nil) + + local focusControllerInternal = focusController[InternalApi] + local mockEngine, engineInterface = MockEngine.new() + focusControllerInternal:initialize(engineInterface) + + local bottomLeafNode = getNode(refs.bottomLeaf) + bottomLeafNode:focus() + expect(focusControllerInternal:isNodeFocused(bottomLeafNode)).to.equal(true) + + -- This upward input will not work on this tree, since there's no + -- upward neighbor defined just yet + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.DPadUp, + }) + expect(focusControllerInternal:isNodeFocused(bottomLeafNode)).to.equal(true) + + -- Update the tree to introduce an upward neighbor + tree = Roact.update(tree, Roact.createElement(FocusableFrame, { + focusController = focusController, + }, { + TopSelectionTarget = Roact.createElement(FocusableFrame, { + NextSelectionDown = refs.bottomFocusable, + [Roact.Ref] = refs.topFocusable, + }), + BottomSelectionTarget = Roact.createElement(FocusableFrame, { + NextSelectionUp = refs.topFocusable, + [Roact.Ref] = refs.bottomFocusable, + }, { + IntermediateChild = Roact.createElement(FocusableFrame, {}, { + -- This focusable child should be able to inherit + -- neighbors from its grandparent + LeafChild = Roact.createElement(FocusableFrame, { + [Roact.Ref] = refs.bottomLeaf, + }) + }), + }), + })) + + -- Make sure we're still on B like before + expect(focusControllerInternal:isNodeFocused(bottomLeafNode)).to.equal(true) + + -- This time, moving up should work as expected + mockEngine:simulateInput({ + UserInputType = Enum.UserInputType.Gamepad1, + UserInputState = Enum.UserInputState.Begin, + KeyCode = Enum.KeyCode.DPadUp, + }) + local topNode = getNode(refs.topFocusable) + expect(focusControllerInternal:isNodeFocused(topNode)).to.equal(true) + + Roact.unmount(tree) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createFocusableCache.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createFocusableCache.lua new file mode 100644 index 0000000..a13d106 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createFocusableCache.lua @@ -0,0 +1,51 @@ +local asFocusable = require(script.Parent.asFocusable) + +local function checkHostProperties(component) + local instance = Instance.new(component) + + assert(instance:IsA("GuiObject")) +end + +local function isValidFocusable(component) + local componentType = typeof(component) + if componentType == "string" then + local hasHostProps, _ = pcall(checkHostProperties, component) + return hasHostProps + elseif componentType == "function" or componentType == "table" then + -- Not much else we can do here right now + return true + end + + -- All other types are invalid components anyways + return false +end + +-- Returns a table that dynamically instantiates Focusable components whenever a +-- new key is accessed. This means that any valid component can be Focusable +local function createFocusableCache() + local focusableComponentCache = {} + + setmetatable(focusableComponentCache, { + __index = function(_, key) + if not isValidFocusable(key) then + error("Component " .. tostring(key) .. " (" .. typeof(key) .. ") is not a valid focusable component", 2) + end + + local newComponent = asFocusable(key) + focusableComponentCache[key] = newComponent + + return newComponent + end, + __tostring = function(self) + local result = "{" + for key, componentClass in pairs(self) do + result = ("%s\n\t%s -> %s"):format(result, tostring(key), tostring(componentClass)) + end + return result .. "\n}" + end + }) + + return focusableComponentCache +end + +return createFocusableCache \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createFocusableCache.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createFocusableCache.spec.lua new file mode 100644 index 0000000..28b982b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createFocusableCache.spec.lua @@ -0,0 +1,42 @@ +return function() + local createFocusableCache = require(script.Parent.createFocusableCache) + + it("should always return Roact components", function() + local focusableCache = createFocusableCache() + + local keys = { "TextLabel", "Frame", "ImageLabel" } + for _, key in ipairs(keys) do + local focusableComponent = focusableCache[key] + expect(focusableComponent).never.to.equal(nil) + -- We don't really have a good way to verify types right now + expect(typeof(focusableComponent.render)).to.equal("function") + end + end) + + it("should return the same object for the same key", function() + local focusableCache = createFocusableCache() + local TextLabel = focusableCache.TextLabel + local Frame = focusableCache.Frame + + expect(TextLabel).to.equal(focusableCache.TextLabel) + expect(Frame).to.equal(focusableCache.Frame) + end) + + it("should verify (to the best of its ability) that the provided component is a viable focusable component", function() + local focusableCache = createFocusableCache() + + expect(function() + return focusableCache[1] + end).to.throw() + expect(function() + return focusableCache.Folder + end).to.throw() + + local function myFunctionComponent(props) + return nil + end + expect(function() + return focusableCache[myFunctionComponent] + end).never.to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createRefCache.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createRefCache.lua new file mode 100644 index 0000000..e1c1548 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createRefCache.lua @@ -0,0 +1,32 @@ +--[[ + This is a handy trick to allow us to reference refs before we've actually + rendered anything, and without duplicating rendering logic! +]] +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) + +-- Returns a table that dynamically instantiates refs whenever a new key is +-- accessed; helpful for building dynamic lists of elements +local function createRefCache() + local refCache = {} + + setmetatable(refCache, { + __index = function(_, key) + local newRef = Roact.createRef() + refCache[key] = newRef + + return newRef + end, + __tostring = function(self) + local result = "{" + for key, ref in pairs(self) do + result = ("%s\n\t%s -> %s"):format(result, tostring(key), tostring(ref)) + end + return result .. "\n}" + end + }) + + return refCache +end + +return createRefCache \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createRefCache.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createRefCache.spec.lua new file mode 100644 index 0000000..35adf69 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createRefCache.spec.lua @@ -0,0 +1,23 @@ +return function() + local createRefCache = require(script.Parent.createRefCache) + + it("should always return valid ref objects", function() + local refCache = createRefCache() + + local keys = { "test", "whatever", "some key" } + for _, key in ipairs(keys) do + local ref = refCache[key] + expect(ref).never.to.equal(nil) + expect(typeof(ref.getValue)).to.equal("function") + end + end) + + it("should return the same object for the same key", function() + local refCache = createRefCache() + local firstRef = refCache.firstRef + local secondRef = refCache.secondRef + + expect(firstRef).to.equal(refCache.firstRef) + expect(secondRef).to.equal(refCache.secondRef) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createSignal.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createSignal.lua new file mode 100644 index 0000000..3db6354 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/createSignal.lua @@ -0,0 +1,75 @@ +--[[ + This is a simple signal implementation that has a dead-simple API. + + local signal = createSignal() + + local disconnect = signal:subscribe(function(foo) + print("Cool foo:", foo) + end) + + signal:fire("something") + + disconnect() +]] + +local function addToMap(map, addKey, addValue) + local new = {} + + for key, value in pairs(map) do + new[key] = value + end + + new[addKey] = addValue + + return new +end + +local function removeFromMap(map, removeKey) + local new = {} + + for key, value in pairs(map) do + if key ~= removeKey then + new[key] = value + end + end + + return new +end + +local function createSignal() + local connections = {} + + local function subscribe(self, callback) + assert(typeof(callback) == "function", "Can only subscribe to signals with a function.") + + local connection = { + callback = callback, + } + + connections = addToMap(connections, callback, connection) + + local function disconnect() + assert(not connection.disconnected, "Listeners can only be disconnected once.") + + connection.disconnected = true + connections = removeFromMap(connections, callback) + end + + return disconnect + end + + local function fire(self, ...) + for callback, connection in pairs(connections) do + if not connection.disconnected then + callback(...) + end + end + end + + return { + subscribe = subscribe, + fire = fire, + } +end + +return createSignal \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/debugPrint.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/debugPrint.lua new file mode 100644 index 0000000..1da7b49 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/debugPrint.lua @@ -0,0 +1,9 @@ +local DEBUG = false + +local function debugPrint(...) + if DEBUG then + print(...) + end +end + +return debugPrint \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/forwardRef.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/forwardRef.lua new file mode 100644 index 0000000..b26f139 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/forwardRef.lua @@ -0,0 +1,24 @@ +-- TODO: Consider contributing this to Roact itself +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) + +--[[ + Passed a provided ref to given render callback. Can be used to treat class + components as host components and assign the passed-in ref to the underlying + host component +]] +local function forwardRef(render) + local ForwardRefComponent = Roact.Component:extend("ForwardRefContainer") + + function ForwardRefComponent:init() + self.defaultRef = Roact.createRef() + end + + function ForwardRefComponent:render() + return render(self.props, self.props[Roact.Ref] or self.defaultRef) + end + + return ForwardRefComponent +end + +return forwardRef \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/forwardRef.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/forwardRef.spec.lua new file mode 100644 index 0000000..39dd1b1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/forwardRef.spec.lua @@ -0,0 +1,54 @@ +return function() + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + + local forwardRef = require(script.Parent.forwardRef) + local createSpy = require(script.Parent.Test.createSpy) + + it("should provide a valid ref to the given function when none is passed in", function() + local internalComponentSpy = createSpy(function(props, ref) + return nil + end) + + local Component = forwardRef(internalComponentSpy.value) + + expect(internalComponentSpy.callCount).to.equal(0) + + local tree = Roact.mount(Roact.createElement(Component, { + text = "Hello", + }), nil) + + expect(internalComponentSpy.callCount).to.equal(1) + + local args = internalComponentSpy:captureValues("props", "ref") + expect(args.props.text).to.equal("Hello") + expect(args.ref).to.be.ok() + expect(typeof(args.ref.getValue)).to.equal("function") + + Roact.unmount(tree) + end) + + it("should forward a provided ref if present", function() + local internalComponentSpy = createSpy(function(props, ref) + return nil + end) + + local Component = forwardRef(internalComponentSpy.value) + + expect(internalComponentSpy.callCount).to.equal(0) + + local providedRef = Roact.createRef() + local tree = Roact.mount(Roact.createElement(Component, { + text = "Hello", + [Roact.Ref] = providedRef, + }), nil) + + expect(internalComponentSpy.callCount).to.equal(1) + + local args = internalComponentSpy:captureValues("props", "ref") + expect(args.props.text).to.equal("Hello") + expect(args.ref).to.equal(providedRef) + + Roact.unmount(tree) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/getEngineInterface.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/getEngineInterface.lua new file mode 100644 index 0000000..19f3e70 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/getEngineInterface.lua @@ -0,0 +1,98 @@ +--!strict +local GuiService = game:GetService("GuiService") +local RunService = game:GetService("RunService") +local UserInputService = game:GetService("UserInputService") +local CoreGui = game:GetService("CoreGui") + +--[[ + The CoreInterface will be used for any focus trees mounted under the + `CoreGui` service in the DataModel. +]] +local CoreInterface = {} + +function CoreInterface.getGamepadConnected(gamepadNum) + return UserInputService:GetGamepadConnected(gamepadNum) +end + +function CoreInterface.getGamepadState(gamepadNum) + return UserInputService:GetGamepadState(gamepadNum) +end + +function CoreInterface.getNavigationGamepads() + return UserInputService:GetNavigationGamepads() +end + +function CoreInterface.getSelection() + return GuiService.SelectedCoreObject +end + +function CoreInterface.setSelection(selectionTarget) + GuiService.SelectedCoreObject = selectionTarget +end + +function CoreInterface.subscribeToSelectionChanged(callback) + return GuiService:GetPropertyChangedSignal("SelectedCoreObject"):Connect(callback) +end + +function CoreInterface.subscribeToRenderStepped(callback) + return RunService.RenderStepped:Connect(callback) +end + +function CoreInterface.subscribeToGamepadConnected(callback) + return UserInputService.GamepadConnected:Connect(callback) +end + +function CoreInterface.subscribeToGamepadDisconnected(callback) + return UserInputService.GamepadDisconnected:Connect(callback) +end + +function CoreInterface.subscribeToInputBegan(callback) + return UserInputService.InputBegan:Connect(callback) +end + +function CoreInterface.subscribeToInputEnded(callback) + return UserInputService.InputEnded:Connect(callback) +end + +--[[ + The PlayerGuiInterface will be used for focus trees mounted anywhere under a + `PlayerGui` instance +]] +local PlayerGuiInterface = {} + +function PlayerGuiInterface.getSelection() + return GuiService.SelectedObject +end + +function PlayerGuiInterface.setSelection(selectionTarget) + GuiService.SelectedObject = selectionTarget +end + +function PlayerGuiInterface.subscribeToSelectionChanged(callback) + return GuiService:GetPropertyChangedSignal("SelectedObject"):Connect(callback) +end + +-- These functions aren't distinct from their core counterparts, but are still +-- very useful to mock. For the PlayerGuiInterface, we simply reuse the same +-- function as the CoreInterface +PlayerGuiInterface.getGamepadConnected = CoreInterface.getGamepadConnected +PlayerGuiInterface.getGamepadState = CoreInterface.getGamepadState +PlayerGuiInterface.getNavigationGamepads = CoreInterface.getNavigationGamepads +PlayerGuiInterface.subscribeToRenderStepped = CoreInterface.subscribeToRenderStepped +PlayerGuiInterface.subscribeToGamepadConnected = CoreInterface.subscribeToGamepadConnected +PlayerGuiInterface.subscribeToGamepadDisconnected = CoreInterface.subscribeToGamepadDisconnected +PlayerGuiInterface.subscribeToInputBegan = CoreInterface.subscribeToInputBegan +PlayerGuiInterface.subscribeToInputEnded = CoreInterface.subscribeToInputEnded + +return function(instance) + if instance:IsDescendantOf(CoreGui) then + return CoreInterface + else + local playerGui = instance:FindFirstAncestorWhichIsA("PlayerGui") + if playerGui == nil then + error("Gamepad navigation not supported. Must be a child of CoreGui or a PlayerGui") + end + + return PlayerGuiInterface + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/init.lua new file mode 100644 index 0000000..0e95691 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/init.lua @@ -0,0 +1,13 @@ +local createFocusableCache = require(script.createFocusableCache) + +local api = { + createRefCache = require(script.createRefCache), + + Focusable = createFocusableCache(), + Input = require(script.Input).PublicInterface, + + withFocusController = require(script.withFocusController), + createFocusController = require(script.FocusController).createPublicApiWrapper, +} + +return api \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/inputBindingsEqual.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/inputBindingsEqual.lua new file mode 100644 index 0000000..d1569c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/inputBindingsEqual.lua @@ -0,0 +1,54 @@ +local function inputBindingsEqual(bindings1, bindings2) + if bindings1 == bindings2 then + return true + end + + for bindingKey, binding1 in pairs(bindings1) do + local binding2 = bindings2[bindingKey] + + if binding2 == nil then + return false + end + + if binding1.kind ~= binding2.kind + or binding1.keyCode ~= binding2.keyCode + or binding1.action ~= binding2.action then + return false + end + + local meta1 = binding1.meta + local meta2 = binding2.meta + if meta1 ~= meta2 then + if typeof(meta1) == "table" and typeof(meta2) == "table" then + for metaKey, metaValue1 in pairs(meta1) do + local metaValue2 = meta2[metaKey] + if metaValue1 ~= metaValue2 then + return false + end + end + + -- It's possible that meta2 contains all pairs in meta1, but not the inverse, + -- so we must explicitly check that + for metaKey in pairs(meta2) do + if meta1[metaKey] == nil then + return false + end + end + else + return false + end + end + end + + -- It's possible that bindings2 contains all pairs in bindings1, but not the inverse, + -- so we must explicitly check that + for bindingKey in pairs(bindings2) do + if bindings1[bindingKey] == nil then + return false + end + end + + return true +end + +return inputBindingsEqual \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/inputBindingsEqual.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/inputBindingsEqual.spec.lua new file mode 100644 index 0000000..6978169 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/inputBindingsEqual.spec.lua @@ -0,0 +1,133 @@ +return function() + local Input = require(script.Parent.Input).PublicInterface + local inputBindingsEqual = require(script.Parent.inputBindingsEqual) + + it("should return true if the binding lists are identical", function() + local bindings = {} + + expect(inputBindingsEqual(bindings, bindings)).to.equal(true) + end) + + it("should return false if any a binding in the first list does not exist in the second", function() + local action = function() end + local bindings1 = { + A = Input.onBegin(Enum.KeyCode.ButtonA, action), + } + local bindings2 = { + B = Input.onBegin(Enum.KeyCode.ButtonA, action), + } + + expect(inputBindingsEqual(bindings1, bindings2)).to.equal(false) + end) + + it("should return false if any a binding in the second list does not exist in the first", function() + local action = function() end + local bindings1 = { + A = Input.onBegin(Enum.KeyCode.ButtonA, action), + } + local bindings2 = { + B = Input.onBegin(Enum.KeyCode.ButtonA, action), + A = Input.onBegin(Enum.KeyCode.ButtonA, action), + } + + expect(inputBindingsEqual(bindings1, bindings2)).to.equal(false) + end) + + it("should return false if any bindings of the same name have different kinds", function() + local action = function() end + local bindings1 = { + A = Input.onBegin(Enum.KeyCode.ButtonA, action), + } + local bindings2 = { + A = Input.onEnd(Enum.KeyCode.ButtonA, action), + } + + expect(inputBindingsEqual(bindings1, bindings2)).to.equal(false) + end) + + it("should return false if any bindings of the same name have different keycodes", function() + local action = function() end + local bindings1 = { + A = Input.onBegin(Enum.KeyCode.ButtonA, action), + } + local bindings2 = { + A = Input.onBegin(Enum.KeyCode.ButtonB, action), + } + + expect(inputBindingsEqual(bindings1, bindings2)).to.equal(false) + end) + + it("should return false if any bindings of the same name have different actions", function() + local bindings1 = { + A = Input.onBegin(Enum.KeyCode.ButtonA, function() end), + } + local bindings2 = { + A = Input.onBegin(Enum.KeyCode.ButtonA, function() end), + } + + expect(inputBindingsEqual(bindings1, bindings2)).to.equal(false) + end) + + describe("differing metas", function() + it("should return false if both metas are not equal and one or both are not tables", function() + local action = function() end + local bindings1 = { + A = Input.onBegin(Enum.KeyCode.ButtonA, action, "binding1"), + } + local bindings2 = { + A = Input.onBegin(Enum.KeyCode.ButtonA, action, "binding2"), + } + + expect(inputBindingsEqual(bindings1, bindings2)).to.equal(false) + end) + + it("should return false if both metas are tables and an entry in the first table does not exist in the second", function() + local action = function() end + local bindings1 = { + A = Input.onBegin(Enum.KeyCode.ButtonA, action, { + A = "A", + }), + } + local bindings2 = { + A = Input.onBegin(Enum.KeyCode.ButtonA, action, { + B = "B", + }), + } + + expect(inputBindingsEqual(bindings1, bindings2)).to.equal(false) + end) + + it("should return false if both metas are tables and an entry in the second table does not exist in the first", function() + local action = function() end + local bindings1 = { + A = Input.onBegin(Enum.KeyCode.ButtonA, action, { + A = "A", + }), + } + local bindings2 = { + A = Input.onBegin(Enum.KeyCode.ButtonA, action, { + B = "B", + A = "A", + }), + } + + expect(inputBindingsEqual(bindings1, bindings2)).to.equal(false) + end) + + it("should return false if both metas are tables and any entries of the same name are not equal", function() + local action = function() end + local bindings1 = { + A = Input.onBegin(Enum.KeyCode.ButtonA, action, { + A = "A", + }), + } + local bindings2 = { + A = Input.onBegin(Enum.KeyCode.ButtonA, action, { + A = "B", + }), + } + + expect(inputBindingsEqual(bindings1, bindings2)).to.equal(false) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/withFocusController.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/withFocusController.lua new file mode 100644 index 0000000..760688f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/withFocusController.lua @@ -0,0 +1,30 @@ +--!strict +-- Manages groups of selectable elements, reacting to selection changes for +-- individual items and triggering events for group selection changes +local Packages = script.Parent.Parent +local Roact = require(Packages.Roact) + +local FocusContext = require(script.Parent.FocusContext) + +local function FocusControllerConsumer(props) + return Roact.createElement(FocusContext.Consumer, { + render = function(navContext) + local focusController + if navContext then + focusController = navContext.focusNode.focusController + else + focusController = nil + end + + return props.render(focusController) + end, + }) +end + +local function withFocusController(render) + return Roact.createElement(FocusControllerConsumer, { + render = render + }) +end + +return withFocusController \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/withFocusController.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/withFocusController.spec.lua new file mode 100644 index 0000000..f04df87 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/roact-gamepad/withFocusController.spec.lua @@ -0,0 +1,19 @@ +return function() + local Packages = script.Parent.Parent + local Roact = require(Packages.Roact) + local asFocusable = require(script.Parent.asFocusable) + local withFocusController = require(script.Parent.withFocusController) + + describe("withFocusController", function() + it("should give a nil focusController if no focusRoot exists in the tree", function() + local FocusableFrame = asFocusable("Frame") + + local tree = Roact.mount(withFocusController(function(focusController) + expect(focusController).to.equal(nil) + return Roact.createElement(FocusableFrame) + end)) + + Roact.unmount(tree) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/t.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/t.lua new file mode 100644 index 0000000..c01744c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-gamepad/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Otter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Otter.lua new file mode 100644 index 0000000..e4e8f5b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Otter.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_otter"]["otter"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/lock.toml new file mode 100644 index 0000000..f092adc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/lock.toml @@ -0,0 +1,10 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/roact-navigation" +version = "0.4.1" +commit = "20f4bf21637414c86d4a61965f8a01ae51a81f54" +source = "url+https://github.com/roblox/roact-navigation" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/BackBehavior.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/BackBehavior.lua new file mode 100644 index 0000000..888393a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/BackBehavior.lua @@ -0,0 +1,29 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +local NONE_TOKEN = NavigationSymbol("NONE") +local INITIAL_ROUTE_TOKEN = NavigationSymbol("INITIAL_ROUTE") +local ORDER_TOKEN = NavigationSymbol("ORDER") +local HISTORY_TOKEN = NavigationSymbol("HISTORY") + +--[[ + BackBehavior provides shared constants that are used to configure back + action styles for different navigators. Note that not all routers support + all BackBehaviors and they will fall back to appropriate defaults for + those cases. +]] +local BackBehavior = { + None = NONE_TOKEN, + InitialRoute = INITIAL_ROUTE_TOKEN, + Order = ORDER_TOKEN, + History = HISTORY_TOKEN, +} + +-- we are using this metatable to error when BackBehavior is indexed +-- with an unexpected key. +setmetatable(BackBehavior, { + __index = function(self, key) + error(("%q is not a valid member of BackBehavior"):format(tostring(key)), 2) + end, +}) + +return BackBehavior diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/Events.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/Events.lua new file mode 100644 index 0000000..395cc5c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/Events.lua @@ -0,0 +1,21 @@ +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +local WILL_FOCUS_TOKEN = NavigationSymbol("WILL_FOCUS") +local DID_FOCUS_TOKEN = NavigationSymbol("DID_FOCUS") +local WILL_BLUR_TOKEN = NavigationSymbol("WILL_BLUR") +local DID_BLUR_TOKEN = NavigationSymbol("DID_BLUR") +local ACTION_TOKEN = NavigationSymbol("ACTION") +local REFOCUS_TOKEN = NavigationSymbol("REFOCUS") + +--[[ + Events provides shared constants that are used to register + listeners for different RoactNavigation UI state changes. +]] +return { + WillFocus = WILL_FOCUS_TOKEN, + DidFocus = DID_FOCUS_TOKEN, + WillBlur = WILL_BLUR_TOKEN, + DidBlur = DID_BLUR_TOKEN, + Action = ACTION_TOKEN, + Refocus = REFOCUS_TOKEN, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NavigationActions.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NavigationActions.lua new file mode 100644 index 0000000..7b34bc6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NavigationActions.lua @@ -0,0 +1,71 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/NavigationActions.ts + +local NavigationSymbol = require(script.Parent.NavigationSymbol) + +local BACK_TOKEN = NavigationSymbol("BACK") +local INIT_TOKEN = NavigationSymbol("INIT") +local NAVIGATE_TOKEN = NavigationSymbol("NAVIGATE") +local SET_PARAMS_TOKEN = NavigationSymbol("SET_PARAMS") + +--[[ + NavigationActions provides shared constants and methods to construct + actions that are dispatched to routers to cause a change in the route. +]] +local NavigationActions = { + Back = BACK_TOKEN, + Init = INIT_TOKEN, + Navigate = NAVIGATE_TOKEN, + SetParams = SET_PARAMS_TOKEN, +} + +-- deviation: we using this metatable to error when NavigationActions is indexed +-- with an unexpected key. +setmetatable(NavigationActions, { + __index = function(self, key) + error(("%q is not a valid member of NavigationActions"):format(tostring(key)), 2) + end, +}) + +-- Navigate back in the history (temporally). +function NavigationActions.back(payload) + local data = payload or {} + return { + type = BACK_TOKEN, + key = data.key, + immediate = data.immediate, + } +end + +-- Initialize the navigation history if not already defined. +function NavigationActions.init(payload) + local data = payload or {} + return { + type = INIT_TOKEN, + params = data.params, + } +end + +-- Navigate to an existing or new route. +function NavigationActions.navigate(payload) + local data = payload or {} + return { + type = NAVIGATE_TOKEN, + routeName = data.routeName, + params = data.params, + action = data.action, + key = data.key, + } +end + +-- Swap out the params for an existing route, matched by the given key. +function NavigationActions.setParams(payload) + local data = payload or {} + return { + type = SET_PARAMS_TOKEN, + preserveFocus = true, + key = data.key, + params = data.params, + } +end + +return NavigationActions diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NavigationSymbol.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NavigationSymbol.lua new file mode 100644 index 0000000..90762dc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/NavigationSymbol.lua @@ -0,0 +1,15 @@ +-- Taken from Roact.Symbol and modified to produce exact string names +-- to allow for serialization/pathing. + +return function (name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + -- Unlike Symbols in Roact, we need the exact names. + getmetatable(self).__tostring = function() + return name + end + + return self +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/StateUtils.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/StateUtils.lua new file mode 100644 index 0000000..ea96ca6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/StateUtils.lua @@ -0,0 +1,241 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/StateUtils.js + +local Cryo = require(script.Parent.Parent.Cryo) + +--[[ + StateUtils provides utilities to read and write standard route data. + Routes have the following general structure: + { + index = , + routes = [ + { + routeName = , + key = , + params = , + action = , + }, + ... + ] + } + + This structure is independent of the notion of stack, tab, drawer, or any + other kind of navigation. It simply represents a list of pages and their + parameters. Different kinds of routers can treat the data in their own way. +]] +local StateUtils = {} + +-- Get the route matching the given key. Returns nil if no match is found. +function StateUtils.get(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + for _, route in ipairs(state.routes) do + if route.key == key then + return route + end + end + + return nil +end + +-- Get the index of the route matching the given key. Returns nil if no match is found. +function StateUtils.indexOf(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + for index, route in ipairs(state.routes) do + if route.key == key then + return index + end + end + + -- deviation: returning nil instead of -1 + return nil +end + +-- Returns true if a route exists matching the given key, false otherwise. +function StateUtils.has(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + for _, route in ipairs(state.routes) do + if route.key == key then + return true + end + end + + return false +end + +-- Push a new route into the navigation state. Makes the pushed route active. +function StateUtils.push(state, route) + assert(type(state) == "table", "state must be a table") + assert(type(route) == "table", "route must be a table") + + assert(StateUtils.indexOf(state, route.key) == nil, + ("should not push route with duplicated key %s"):format(route.key)) + + local routes = Cryo.List.join(state.routes, { route }) + return Cryo.Dictionary.join(state, { + index = #routes, + routes = routes, + }) +end + +-- Pop the top-most route from the navigation state (NOT the active route). +-- Makes the new top-most route active. +function StateUtils.pop(state) + assert(type(state) == "table", "state must be a table") + + if state.index <= 1 then + -- [Note]: Over-popping does not throw error. Instead, it will be no-op. + return state + end + + local routes = Cryo.List.removeIndex(state.routes, #state.routes) + return Cryo.Dictionary.join(state, { + index = #routes, + routes = routes, + }) +end + +-- Sets the active route to match the given index. +function StateUtils.jumpToIndex(state, index) + assert(type(state) == "table", "state must be a table") + assert(type(index) == "number", "index must be a number") + + if index == state.index then + return state + end + + assert(state.routes[index] ~= nil, ("invalid index %d to jump to"):format(index)) + + return Cryo.Dictionary.join(state, { + index = index, + }) +end + +-- Sets the active route to match the given key. +function StateUtils.jumpTo(state, key) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + + local index = StateUtils.indexOf(state, key) + assert(index ~= nil, ('attempt to jump to unknown key "%s"'):format(key)) + + return StateUtils.jumpToIndex(state, index) +end + +-- Sets the active route to the previous route in the list. +function StateUtils.back(state) + assert(type(state) == "table", "state must be a table") + + local index = state.index - 1 + if not state.routes[index] then + return state + end + + return StateUtils.jumpToIndex(state, index) +end + +-- Sets the active route to the next route in the list. +function StateUtils.forward(state) + assert(type(state) == "table", "state must be a table") + + local index = state.index + 1 + if not state.routes[index] then + return state + end + + return StateUtils.jumpToIndex(state, index) +end + +-- Replace the route matching the given key. Sets the active route to the +-- newly replaced entry. Prunes the old entries that follow the replaced one. +function StateUtils.replaceAndPrune(state, key, route) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + assert(type(route) == "table", "route must be a table") + + local index = StateUtils.indexOf(state, key) + local replaced = StateUtils.replaceAtIndex(state, index, route) + + return Cryo.Dictionary.join(replaced, { + routes = { unpack(replaced.routes, 1, index) } + }) +end + +-- Replace the route matching the given key without pruning the following routes. +-- The active route will be updated to match the newly replaced one unless +-- preserveIndex is true. +function StateUtils.replaceAt(state, key, route, preserveIndex) + assert(type(state) == "table", "state must be a table") + assert(type(key) == "string", "key must be a string") + assert(type(route) == "table", "route must be a table") + assert(preserveIndex == nil or type(preserveIndex) == "boolean", + "preserveIndex must be nil or a boolean") + + local index = StateUtils.indexOf(state, key) + local nextIndex = preserveIndex and state.index or index + local nextState = StateUtils.replaceAtIndex(state, index, route) + nextState.index = nextIndex + return nextState +end + +-- Replace the route at the given index. Updates the active route to point to +-- the replaced entry. +function StateUtils.replaceAtIndex(state, index, route) + assert(type(state) == "table", "state must be a table") + assert(type(index) == "number", "index must be a number") + assert(type(route) == "table", "route must be a table") + + assert(state.routes[index] ~= nil, + ("invalid index %d for replacing route %s"):format(index, route.key)) + + if state.routes[index] == route and index == state.index then + return state + end + + local routes = Cryo.List.join(state.routes) + routes[index] = route + + return Cryo.Dictionary.join(state, { + index = index, + routes = routes, + }) +end + +-- Wipe away the existing routes and replace them with new routes. +-- Sets the active route to the provided index (if provided), otherwise +-- sets the active route to the last one in the list. +function StateUtils.reset(state, routes, index) + assert(type(state) == "table", "state must be a table") + assert(type(routes) == "table" and #routes > 0, "invalid routes to replace") + assert(index == nil or type(index) == "number", "index must be a number or nil") + + local nextIndex = not index and #routes or index + + -- Bail out without replacing IFF index and routes all match + if #state.routes == #routes and state.index == nextIndex then + local routesAreEqual = true + for i = 1, #routes, 1 do + if state.routes[i] ~= routes[i] then + routesAreEqual = false + break + end + end + + if routesAreEqual then + return state + end + end + + assert(routes[nextIndex] ~= nil, ("invalid index %d to reset"):format(nextIndex)) + + return Cryo.Dictionary.join(state, { + index = nextIndex, + routes = routes, + }) +end + +return StateUtils diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/BackBehavior.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/BackBehavior.spec.lua new file mode 100644 index 0000000..cec6ddd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/BackBehavior.spec.lua @@ -0,0 +1,19 @@ +return function() + local BackBehavior = require(script.Parent.Parent.BackBehavior) + + describe("BackBehavior token tests", function() + it("should return same object for each token for multiple calls", function() + expect(BackBehavior.None).to.equal(BackBehavior.None) + expect(BackBehavior.InitialRoute).to.equal(BackBehavior.InitialRoute) + expect(BackBehavior.Order).to.equal(BackBehavior.Order) + expect(BackBehavior.History).to.equal(BackBehavior.History) + end) + + it("should return matching string names for symbols", function() + expect(tostring(BackBehavior.None)).to.equal("NONE") + expect(tostring(BackBehavior.InitialRoute)).to.equal("INITIAL_ROUTE") + expect(tostring(BackBehavior.Order)).to.equal("ORDER") + expect(tostring(BackBehavior.History)).to.equal("HISTORY") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/Events.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/Events.roblox.spec.lua new file mode 100644 index 0000000..9405e59 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/Events.roblox.spec.lua @@ -0,0 +1,23 @@ +return function() + local Events = require(script.Parent.Parent.Events) + + describe("Events token tests", function() + it("should return same object for each token for multiple calls", function() + expect(Events.WillFocus).to.equal(Events.WillFocus) + expect(Events.DidFocus).to.equal(Events.DidFocus) + expect(Events.WillBlur).to.equal(Events.WillBlur) + expect(Events.DidBlur).to.equal(Events.DidBlur) + expect(Events.Action).to.equal(Events.Action) + expect(Events.Refocus).to.equal(Events.Refocus) + end) + + it("should return matching string names for symbols", function() + expect(tostring(Events.WillFocus)).to.equal("WILL_FOCUS") + expect(tostring(Events.DidFocus)).to.equal("DID_FOCUS") + expect(tostring(Events.WillBlur)).to.equal("WILL_BLUR") + expect(tostring(Events.DidBlur)).to.equal("DID_BLUR") + expect(tostring(Events.Action)).to.equal("ACTION") + expect(tostring(Events.Refocus)).to.equal("REFOCUS") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationActions.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationActions.roblox.spec.lua new file mode 100644 index 0000000..3b12364 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationActions.roblox.spec.lua @@ -0,0 +1,73 @@ +return function() + local NavigationActions = require(script.Parent.Parent.NavigationActions) + + it("throws when indexing an unknown field", function() + expect(function() + return NavigationActions.foo + end).to.throw("\"foo\" is not a valid member of NavigationActions") + end) + + describe("NavigationActions token tests", function() + it("should return same object for each token for multiple calls", function() + expect(NavigationActions.Back).to.equal(NavigationActions.Back) + expect(NavigationActions.Init).to.equal(NavigationActions.Init) + expect(NavigationActions.Navigate).to.equal(NavigationActions.Navigate) + expect(NavigationActions.SetParams).to.equal(NavigationActions.SetParams) + end) + + it("should return matching string names for symbols", function() + expect(tostring(NavigationActions.Back)).to.equal("BACK") + expect(tostring(NavigationActions.Init)).to.equal("INIT") + expect(tostring(NavigationActions.Navigate)).to.equal("NAVIGATE") + expect(tostring(NavigationActions.SetParams)).to.equal("SET_PARAMS") + end) + end) + + describe("NavigationActions function tests", function() + it("should return a back action with matching data for a call to back()", function() + local backTable = NavigationActions.back({ + key = "the_key", + immediate = true, + }) + + expect(backTable.type).to.equal(NavigationActions.Back) + expect(backTable.key).to.equal("the_key") + expect(backTable.immediate).to.equal(true) + end) + + it("should return an init action with matching data for call to init()", function() + local initTable = NavigationActions.init({ + params = "foo", + }) + + expect(initTable.type).to.equal(NavigationActions.Init) + expect(initTable.params).to.equal("foo") + end) + + it("should return a navigate action with matching data for call to navigate()", function() + local navigateTable = NavigationActions.navigate({ + routeName = "routeName", + params = "foo", + action = "action", + key = "key", + }) + + expect(navigateTable.type).to.equal(NavigationActions.Navigate) + expect(navigateTable.routeName).to.equal("routeName") + expect(navigateTable.params).to.equal("foo") + expect(navigateTable.action).to.equal("action") + expect(navigateTable.key).to.equal("key") + end) + + it("should return a set params action with matching data for call to setParams()", function() + local setParamsTable = NavigationActions.setParams({ + key = "key", + params = "foo", + }) + + expect(setParamsTable.type).to.equal(NavigationActions.SetParams) + expect(setParamsTable.key).to.equal("key") + expect(setParamsTable.params).to.equal("foo") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationActions.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationActions.spec.lua new file mode 100644 index 0000000..1d44e3e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationActions.spec.lua @@ -0,0 +1,75 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/72e8160537954af40f1b070aa91ef45fc02bba69/packages/core/src/__tests__/NavigationActions.test.js + +return function() + local NavigationActions = require(script.Parent.Parent.NavigationActions) + + local expectDeepEqual = require(script.Parent.Parent.utils.expectDeepEqual) + + describe("generic navigation actions", function() + local params = { foo = "bar" } + local navigateAction = NavigationActions.navigate({ routeName = "another" }) + + it("exports back action and type", function() + expectDeepEqual(NavigationActions.back(), { type = NavigationActions.Back }) + expectDeepEqual( + NavigationActions.back({ key = "test" }), + { + type = NavigationActions.Back, + key = "test", + } + ) + end) + + it("exports init action and type", function() + expectDeepEqual(NavigationActions.init(), { type = NavigationActions.Init }) + expectDeepEqual( + NavigationActions.init({ params = params }), + { + type = NavigationActions.Init, + params = params, + } + ) + end) + + it("exports navigate action and type", function() + expectDeepEqual( + NavigationActions.navigate({ routeName = "test" }), + { + type = NavigationActions.Navigate, + routeName = "test", + } + ) + expectDeepEqual( + NavigationActions.navigate({ + routeName = "test", + params = params, + action = navigateAction, + }), + { + type = NavigationActions.Navigate, + routeName = "test", + params = params, + action = { + type = NavigationActions.Navigate, + routeName = "another", + }, + } + ) + end) + + it("exports setParams action and type", function() + expectDeepEqual( + NavigationActions.setParams({ + key = "test", + params = params, + }), + { + type = NavigationActions.SetParams, + key = "test", + preserveFocus = true, + params = params, + } + ) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationFocusEvents.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationFocusEvents.spec.lua new file mode 100644 index 0000000..22884d9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationFocusEvents.spec.lua @@ -0,0 +1,271 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/9b55493e7662f4d54c21f75e53eb3911675f61bc/packages/core/src/__tests__/NavigationFocusEvents.test.js +local RunService = game:GetService("RunService") + +return function() + local root = script.Parent.Parent + local Packages = root.Parent + + local TabRouter = require(root.routers.TabRouter) + local createAppContainer = require(root.createAppContainer) + local createNavigator = require(root.navigators.createNavigator) + local Events = require(root.Events) + local NavigationActions = require(root.NavigationActions) + local createSpy = require(root.utils.createSpy) + + local Cryo = require(Packages.Cryo) + local Roact = require(Packages.Roact) + + -- deviation: utility function moved out of test scope because + -- it is shared across both tests + local function createTestNavigator(routeConfigMap, config) + config = config or {} + local router = TabRouter(routeConfigMap, config) + + return createNavigator( + function(props) + local navigation = props.navigation + local descriptors = props.descriptors + + local children = Cryo.List.foldLeft(navigation.state.routes, function(acc, route) + local Comp = descriptors[route.key].getComponent() + acc[route.key] = Roact.createElement(Comp, { + key = route.key, + navigation = descriptors[route.key].navigation + }) + return acc + end, {}) + + return Roact.createFragment(children) + end, + router, + config + ) + end + + local function waitUntil(predicate, timeout) + timeout = timeout or 1 + local waitedTime = 0 + while waitedTime < timeout and not predicate() do + waitedTime = waitedTime + RunService.Heartbeat:Wait() + end + end + + -- deviation: utility function moved out of test scope because + -- it is shared across both tests + local function createComponent(focusCallback, blurCallback) + local TestComponent = Roact.Component:extend("TestComponent") + + function TestComponent:didMount() + local navigation = self.props.navigation + + self.focusSub = navigation.addListener(Events.WillFocus, focusCallback) + self.blurSub = navigation.addListener(Events.WillBlur, blurCallback) + end + + function TestComponent:willUnmount() + self.focusSub.remove() + self.blurSub.remove() + end + + function TestComponent:render() + return nil + end + + return TestComponent + end + + it("fires focus and blur events in root navigator", function() + local firstFocusCallback = createSpy() + local firstBlurCallback = createSpy() + + local secondFocusCallback = createSpy() + local secondBlurCallback = createSpy() + + local thirdFocusCallback = createSpy() + local thirdBlurCallback = createSpy() + + local fourthFocusCallback = createSpy() + local fourthBlurCallback = createSpy() + + local Navigator = createAppContainer( + createTestNavigator({ + { first = createComponent(firstFocusCallback.value, firstBlurCallback.value) }, + { second = createComponent(secondFocusCallback.value, secondBlurCallback.value) }, + { third = createComponent(thirdFocusCallback.value, thirdBlurCallback.value) }, + { fourth = createComponent(fourthFocusCallback.value, fourthBlurCallback.value) }, + }) + ) + + local dispatch + local element = Roact.createElement(Navigator, { + externalDispatchConnector = function(currentDispatch) + dispatch = currentDispatch + return function () + dispatch = nil + end + end + }) + + Roact.mount(element) + + waitUntil(function() + return firstFocusCallback.callCount > 0 + end) + + expect(firstFocusCallback.callCount).to.equal(1) + expect(firstBlurCallback.callCount).to.equal(0) + expect(secondFocusCallback.callCount).to.equal(0) + expect(secondBlurCallback.callCount).to.equal(0) + expect(thirdFocusCallback.callCount).to.equal(0) + expect(thirdBlurCallback.callCount).to.equal(0) + expect(fourthFocusCallback.callCount).to.equal(0) + expect(fourthBlurCallback.callCount).to.equal(0) + + dispatch(NavigationActions.navigate({ routeName = 'second' })) + + waitUntil(function() + return firstBlurCallback.callCount > 0 + end) + + expect(firstBlurCallback.callCount).to.equal(1) + expect(secondFocusCallback.callCount).to.equal(1) + + dispatch(NavigationActions.navigate({ routeName = 'fourth' })) + + waitUntil(function() + return secondBlurCallback.callCount > 0 + end) + + expect(firstFocusCallback.callCount).to.equal(1) + expect(firstBlurCallback.callCount).to.equal(1) + expect(secondFocusCallback.callCount).to.equal(1) + expect(secondBlurCallback.callCount).to.equal(1) + expect(thirdFocusCallback.callCount).to.equal(0) + expect(thirdBlurCallback.callCount).to.equal(0) + expect(fourthFocusCallback.callCount).to.equal(1) + expect(fourthBlurCallback.callCount).to.equal(0) + end) + + it('fires focus and blur events in nested navigator', function() + local firstFocusCallback = createSpy() + local firstBlurCallback = createSpy() + + local secondFocusCallback = createSpy() + local secondBlurCallback = createSpy() + + local thirdFocusCallback = createSpy() + local thirdBlurCallback = createSpy() + + local fourthFocusCallback = createSpy() + local fourthBlurCallback = createSpy() + + local Navigator = createAppContainer( + createTestNavigator({ + { first = createComponent(firstFocusCallback.value, firstBlurCallback.value) }, + { second = createComponent(secondFocusCallback.value, secondBlurCallback.value) }, + { + nested = createTestNavigator({ + { third = createComponent(thirdFocusCallback.value, thirdBlurCallback.value) }, + { fourth = createComponent(fourthFocusCallback.value, fourthBlurCallback.value) }, + }) + }, + }) + ) + + local dispatch + local element = Roact.createElement(Navigator, { + externalDispatchConnector = function(currentDispatch) + dispatch = currentDispatch + return function () + dispatch = nil + end + end + }) + + Roact.mount(element) + + waitUntil(function() + return firstFocusCallback.callCount > 0 + end) + + expect(thirdFocusCallback.callCount).to.equal(0) + expect(firstFocusCallback.callCount).to.equal(1) + + dispatch(NavigationActions.navigate({ routeName = 'nested' })) + + waitUntil(function() + return thirdFocusCallback.callCount > 0 + end) + + expect(firstFocusCallback.callCount).to.equal(1) + expect(fourthFocusCallback.callCount).to.equal(0) + expect(thirdFocusCallback.callCount).to.equal(1) + + dispatch(NavigationActions.navigate({ routeName = 'second' })) + + waitUntil(function() + return secondFocusCallback.callCount > 0 + end) + + expect(thirdFocusCallback.callCount).to.equal(1) + expect(secondFocusCallback.callCount).to.equal(1) + expect(fourthBlurCallback.callCount).to.equal(0) + + dispatch(NavigationActions.navigate({ routeName = 'nested' })) + + waitUntil(function() + return thirdFocusCallback.callCount > 1 + end) + + expect(firstBlurCallback.callCount).to.equal(1) + expect(secondBlurCallback.callCount).to.equal(1) + expect(thirdFocusCallback.callCount).to.equal(2) + expect(fourthFocusCallback.callCount).to.equal(0) + + dispatch(NavigationActions.navigate({ routeName = 'third' })) + + expect(fourthBlurCallback.callCount).to.equal(0) + expect(thirdFocusCallback.callCount).to.equal(2) + + dispatch(NavigationActions.navigate({ routeName = 'first' })) + + waitUntil(function() + return firstFocusCallback.callCount > 1 + end) + + expect(firstFocusCallback.callCount).to.equal(2) + expect(thirdBlurCallback.callCount).to.equal(2) + + dispatch(NavigationActions.navigate({ routeName = 'fourth' })) + + waitUntil(function() + return fourthFocusCallback.callCount > 0 + end) + + expect(fourthFocusCallback.callCount).to.equal(1) + expect(thirdBlurCallback.callCount).to.equal(2) + expect(firstBlurCallback.callCount).to.equal(2) + + dispatch(NavigationActions.navigate({ routeName = 'third' })) + + waitUntil(function() + return thirdFocusCallback.callCount > 2 + end) + + expect(thirdFocusCallback.callCount).to.equal(3) + expect(fourthBlurCallback.callCount).to.equal(1) + + -- Make sure nothing else has changed + expect(firstFocusCallback.callCount).to.equal(2) + expect(firstBlurCallback.callCount).to.equal(2) + + expect(secondFocusCallback.callCount).to.equal(1) + expect(secondBlurCallback.callCount).to.equal(1) + + expect(thirdFocusCallback.callCount).to.equal(3) + expect(thirdBlurCallback.callCount).to.equal(2) + + expect(fourthFocusCallback.callCount).to.equal(1) + expect(fourthBlurCallback.callCount).to.equal(1) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationSymbol.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationSymbol.spec.lua new file mode 100644 index 0000000..a43690b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/NavigationSymbol.spec.lua @@ -0,0 +1,22 @@ +return function() + local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol) + + it("should give an opaque object", function() + local symbol = NavigationSymbol("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = NavigationSymbol("foo") + + expect(tostring(symbol)).to.equal("foo") + end) + + it("should be unique when constructed", function() + local symbolA = NavigationSymbol("abc") + local symbolB = NavigationSymbol("abc") + + expect(symbolA).never.to.equal(symbolB) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/StateUtils.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/StateUtils.roblox.spec.lua new file mode 100644 index 0000000..455f9b2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/StateUtils.roblox.spec.lua @@ -0,0 +1,630 @@ +return function() + local StateUtils = require(script.Parent.Parent.StateUtils) + + describe("StateUtils.get tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.get(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.get({}, 5) + end).to.throw() + end) + + it("should return nil if key is not found in routes", function() + local result = StateUtils.get({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + }, + }, + }, "key") + + expect(result).to.equal(nil) + end) + + it("should return route if key is found in routes", function() + local result = StateUtils.get({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + }, + }, "foo-1") + + expect(result.routeName).to.equal("foo") + expect(result.key).to.equal("foo-1") + end) + end) + + describe("StateUtils.indexOf tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.indexOf(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.indexOf({}, 5) + end).to.throw() + end) + + it("should return nil if key is not found in routes", function() + local result = StateUtils.indexOf({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + }, + }, "key") + + expect(result).to.equal(nil) + end) + + it("should return index if key is found in routes", function() + local result = StateUtils.indexOf({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + }, + { + routeName = "foo2", + key = "foo-2", + } + }, + }, "foo-2") + + expect(result).to.equal(2) + end) + end) + + describe("StateUtils.has tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.has(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.has({}, 5) + end).to.throw() + end) + + it("should return false if key is not in routes", function() + local result = StateUtils.has({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + } + }, "key") + + expect(result).to.equal(false) + end) + + it("should return true if key is found in routes", function() + local result = StateUtils.has({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + } + }, "foo-1") + + expect(result).to.equal(true) + end) + end) + + describe("StateUtils.push tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.push(nil, {}) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.push({}, 5) + end).to.throw() + end) + + it("should assert if route.key is already present", function() + expect(function() + StateUtils.push({ + index = 1, + routes = { + { + routeName = "foo", + key = "foo-1", + } + } + }, { + routeName = "foo", + key = "foo-1", + }) + end).to.throw() + end) + + it("should insert new route if it doesn't exist", function() + local newState = StateUtils.push({ + index = 1, + routes = { + { + routeName = "first", + key = "foo-1", + }, + }, + }, { + routeName = "second", + key = "foo-2", + }) + + expect(newState.index).to.equal(2) + expect(#newState.routes).to.equal(2) + expect(newState.routes[newState.index].key).to.equal("foo-2") + expect(newState.routes[newState.index].routeName).to.equal("second") + end) + end) + + describe("StateUtils.pop tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.pop(nil) + end).to.throw() + end) + + it("should return existing state if routes is empty", function() + local initialState = { + index = 0, + routes = {}, + } + + local newState = StateUtils.pop(initialState) + expect(newState).to.equal(initialState) + end) + + it("should remove top route if popping with more than one route", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "route-1", }, + { routeName = "route", key = "route-2", }, + }, + } + + local newState = StateUtils.pop(initialState) + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(1) + expect(newState.routes[1].key).to.equal("route-1") + end) + end) + + describe("StateUtils.jumpToIndex tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.jumpToIndex(nil, 0) + end).to.throw() + end) + + it("should assert if index is not a number", function() + expect(function() + StateUtils.jumpToIndex({}, "foo") + end).to.throw() + end) + + it("should assert if index does not match a route", function() + expect(function() + StateUtils.jumpToIndex({ + index = 1, + routes = { { routeName = "first", key = "first-1" } } + }, 5) + end).to.throw() + end) + + it("should return original state if index matches current", function() + local initialState = { + index = 1, + routes = { { routeName = "one", key = "1" } } + } + + local newState = StateUtils.jumpToIndex(initialState, 1) + expect(newState).to.equal(initialState) + end) + + it("should return updated state if index differs", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "route-1" }, + { routeName = "route", key = "route-2" }, + }, + } + + local newState = StateUtils.jumpToIndex(initialState, 2) + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.jumpTo tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.jumpTo(nil, "key") + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.jumpTo({}, 0) + end).to.throw() + end) + + it("should return original state if key is already active route", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.jumpTo(initialState, "key-1") + expect(newState).to.equal(initialState) + end) + + it("should return state with new active route if key is not active", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.jumpTo(initialState, "key-2") + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.back tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.back(nil) + end).to.throw() + end) + + it("should return original state if route for new index does not exist", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + } + } + + local newState = StateUtils.back(initialState) + expect(newState).to.equal(initialState) + end) + + it("should remove top state if there is somewhere to go", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.back(initialState) + expect(newState.index).to.equal(1) + end) + end) + + describe("StateUtils.forward tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.forward(nil) + end).to.throw() + end) + + it("should not walk off the end of the route list", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + } + } + + local newState = StateUtils.forward(initialState) + expect(newState).to.equal(initialState) + end) + + it("should move to next route if available", function() + local initialState = { + index = 1, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.forward(initialState) + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.replaceAndPrune tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.replaceAndPrune(nil, "key", {}) + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.replaceAndPrune({}, 0, {}) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.replaceAndPrune({}, "key", 0) + end).to.throw() + end) + + it("should replace matching route and prune following routes", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.replaceAndPrune(initialState, "key-1", { + routeName = "newRoute", key = "key-3" + }) + + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(1) + expect(newState.routes[1].routeName).to.equal("newRoute") + expect(newState.routes[1].key).to.equal("key-3") + end) + end) + + describe("StateUtils.replaceAt tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.replaceAt(nil, "key", {}, false) + end).to.throw() + end) + + it("should assert if key is not a string", function() + expect(function() + StateUtils.replaceAt({}, 0, {}, false) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.replaceAt({}, "key", 0, false) + end).to.throw() + end) + + it("should assert if preserveIndex is not a boolean", function() + expect(function() + StateUtils.replaceAt({}, "key", {}, 0) + end).to.throw() + end) + + it("should replace matching route, not prune, and update index", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.replaceAt(initialState, "key-1", { + routeName = "newRoute", key = "key-3" + }, false) + + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(2) + expect(newState.routes[1].routeName).to.equal("newRoute") + expect(newState.routes[1].key).to.equal("key-3") + end) + + it("should replace matching route, not prune, and preserve existing index", function() + local initialState = { + index = 2, + routes = { + { routeName = "route", key = "key-1" }, + { routeName = "route", key = "key-2" }, + } + } + + local newState = StateUtils.replaceAt(initialState, "key-1", { + routeName = "newRoute", key = "key-3" + }, true) + + expect(newState.index).to.equal(2) + expect(#newState.routes).to.equal(2) + expect(newState.routes[1].routeName).to.equal("newRoute") + expect(newState.routes[1].key).to.equal("key-3") + end) + end) + + describe("StateUtils.replaceAtIndex tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.replaceAtIndex(nil, 0, {}) + end).to.throw() + end) + + it("should assert if index is not a number", function() + expect(function() + StateUtils.replaceAtIndex({}, nil, {}) + end).to.throw() + end) + + it("should assert if route is not a table", function() + expect(function() + StateUtils.replaceAtIndex({}, 5, nil) + end).to.throw() + end) + + it("should assert if index does not exist", function() + expect(function() + StateUtils.replaceAtIndex({ + index = 0, + routes = {} + }, 5, { routeName = "name", key = "key" }) + end).to.throw() + end) + + it("should return original state if inputs are same", function() + local testRoute = { routeName = "name", key = "key" } + local initialState = { + index = 1, + routes = { testRoute }, + } + + local newState = StateUtils.replaceAtIndex(initialState, 1, testRoute) + expect(newState).to.equal(initialState) + end) + + it("should replace route at index if route is not equal", function() + local initialState = { + index = 1, + routes = { + { routeName = "name", key = "key" } + }, + } + + local newState = StateUtils.replaceAtIndex(initialState, 1, { + routeName = "newName", + key = "key", + }) + + expect(newState.index).to.equal(1) + expect(#newState.routes).to.equal(1) + expect(newState.routes[1].routeName).to.equal("newName") + expect(newState.routes[1].key).to.equal("key") + end) + + it("should update index, if new index differs but route does not", function() + local testRoute = { routeName = "name", key = "key-2" } + local initialState = { + index = 1, + routes = { + { routeName = "name", key = "key-1" }, + testRoute, + } + } + + local newState = StateUtils.replaceAtIndex(initialState, 2, testRoute) + expect(newState).never.to.equal(initialState) + expect(newState.index).to.equal(2) + end) + end) + + describe("StateUtils.reset tests", function() + it("should assert if state is not a table", function() + expect(function() + StateUtils.reset(nil, {}, 0) + end).to.throw() + end) + + it("should assert if routes is not a table", function() + expect(function() + StateUtils.reset({}, nil, 0) + end).to.throw() + end) + + it("should assert if index is not a number", function() + expect(function() + StateUtils.reset({}, {}, "foo") + end).to.throw() + end) + + -- the test does not seem to match with the name + it("should NOT assert if index is nil", function() + expect(function() + StateUtils.reset({}, {}) + end).to.throw() + end) + + it("should return original state if index matches and all routes are same objects", function() + local route1 = { routeName = "route1", key = "route-1" } + local route2 = { routeName = "route2", key = "route-2" } + + local initialState = { + index = 2, + routes = { route1, route2 }, + } + + local newState = StateUtils.reset(initialState, { + route1, + route2, + }, 2) + + expect(newState).to.equal(initialState) + end) + + it("should update state if index is not specified and old index is not last route", function() + local route1 = { routeName = "route1", key = "route-1" } + local route2 = { routeName = "route2", key = "route-2" } + + local initialState = { + index = 1, + routes = { route1, route2 }, + } + + local newState = StateUtils.reset(initialState, { + route1, + route2, + }) + + expect(newState).never.to.equal(initialState) + expect(newState.index).to.equal(2) + end) + + it("should update state if index matches but routes differ", function() + local route1 = { routeName = "route1", key = "route-1" } + local route2 = { routeName = "route2", key = "route-2" } + + local initialState = { + index = 1, + routes = { route1, route2 }, + } + + local newState = StateUtils.reset(initialState, { + route1, + { routeName = "route3", key = "route-3" }, + }, 1) + + expect(newState).never.to.equal(initialState) + expect(#newState.routes).to.equal(2) + expect(newState.index).to.equal(1) + expect(newState.routes[2].routeName).to.equal("route3") + expect(newState.routes[2].key).to.equal("route-3") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/StateUtils.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/StateUtils.spec.lua new file mode 100644 index 0000000..a0a573c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/StateUtils.spec.lua @@ -0,0 +1,500 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/__tests__/NavigationStateUtils.test.js + +return function() + local StateUtils = require(script.Parent.Parent.StateUtils) + + local utils = script.Parent.Parent.utils + local expectDeepEqual = require(utils.expectDeepEqual) + + local routeName = "Anything" + + describe("StateUtils", function() + describe("get", function() + it("gets route", function() + local state = { + index = 1, + routes = { + { + key = "a", + routeName = routeName, + }, + }, + } + + expectDeepEqual( + StateUtils.get(state, "a"), + { + key = "a", + routeName = routeName, + } + ) + end) + + it("returns null when getting an unknown route", function() + local state = { + index = 1, + routes = { + { + key = "a", + routeName = routeName, + }, + }, + } + + expect(StateUtils.get(state, "b")).to.equal(nil) + end) + end) + + describe("indexOf", function() + it("gets route index", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + + expect(StateUtils.indexOf(state, "a")).to.equal(1) + expect(StateUtils.indexOf(state, "b")).to.equal(2) + end) + + -- deviation(will not fix): it is preferable to return `nil` as it's + -- more common so there is less chance to surprise the consumer of + -- that function + itSKIP("returns -1 when getting an unknown route index", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + }, + isTransitioning = false, + } + expect(StateUtils.indexOf(state, "b")).to.equal(-1) + end) + end) + + it("has a route", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + + expect(StateUtils.has(state, "b")).to.equal(true) + expect(StateUtils.has(state, "c")).to.equal(false) + end) + + describe("push", function() + it("pushes a route", function() + local state = { + index = 1, + routes = {{ key = "a", routeName = routeName }}, + isTransitioning = false, + } + local newState = { + index = 2, + isTransitioning = false, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + } + + expectDeepEqual( + StateUtils.push(state, { key = "b", routeName = routeName }), + newState + ) + end) + + it("does not push duplicated route", function() + local state = { + index = 1, + routes = {{ key = "a", routeName = routeName }}, + isTransitioning = false, + } + + expect(function() + StateUtils.push(state, { key = "a", routeName = routeName }) + end).to.throw("should not push route with duplicated key a") + end) + end) + + describe("pop", function() + it("pops route", function() + local state = { + index = 2, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + local newState = { + index = 1, + routes = {{ key = "a", routeName = routeName }}, + isTransitioning = false, + } + + expectDeepEqual(StateUtils.pop(state), newState) + end) + + it("does not pop route if not applicable with single route config", function() + local state = { + index = 1, + routes = {{ key = "a", routeName = routeName }}, + isTransitioning = false, + } + + expectDeepEqual( + StateUtils.pop(state), + state + ) + end) + + it("does not pop route if not applicable with multiple route config", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + }; + expectDeepEqual(StateUtils.pop(state), state) + end) + end) + + describe("jumpToIndex", function() + it("jumps to new index", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + local newState = { + index = 2, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + + expectDeepEqual( + StateUtils.jumpToIndex(state, 1), + state + ) + expectDeepEqual( + StateUtils.jumpToIndex(state, 2), + newState + ) + end) + + it("throws if jumps to invalid index", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + + expect(function() + StateUtils.jumpToIndex(state, 3) + end).to.throw("invalid index 3 to jump to") + end) + end) + + describe("jumpTo", function() + it("jumps to the current key", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + expectDeepEqual(StateUtils.jumpTo(state, "a"), state) + end) + + it("jumps to new key", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + local newState = { + index = 2, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + + expectDeepEqual( + StateUtils.jumpTo(state, "b"), + newState + ) + end) + + it("throws if jumps to invalid key", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + + expect(function() + StateUtils.jumpTo(state, "c") + end).to.throw("attempt to jump to unknown key \"c\"") + end) + end) + + describe("back", function() + it("move backwards", function() + local state = { + index = 2, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + local newState = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + + expectDeepEqual(StateUtils.back(state), newState) + end) + + it("does not move backwards when the active route is the first", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + expect(StateUtils.back(state)).to.equal(state) + end) + end) + + describe("forward", function() + it("move forwards", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + local newState = { + index = 2, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + expectDeepEqual(StateUtils.forward(state), newState) + end) + + it("does not move forward when active route is already the top-most", function() + local state = { + index = 2, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + + expectDeepEqual(StateUtils.forward(state), state) + end) + end) + + describe("replace", function() + it("Replaces by key", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + local newState = { + index = 2, + routes = { + { key = "a", routeName = routeName }, + { key = "c", routeName = routeName }, + }, + isTransitioning = false, + } + + expectDeepEqual( + StateUtils.replaceAt(state, "b", { key = "c", routeName = routeName }), + newState + ) + end) + + it("Replaces by index", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + local newState = { + index = 2, + routes = { + { key = "a", routeName = routeName }, + { key = "c", routeName = routeName }, + }, + isTransitioning = false, + } + + expectDeepEqual( + StateUtils.replaceAtIndex(state, 2, { key = "c", routeName = routeName }), + newState + ) + end) + + it("Returns the state with updated index if route is unchanged but index changes", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + + expectDeepEqual( + StateUtils.replaceAtIndex(state, 2, state.routes[2]), + { + index = 2, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + ) + end) + end) + + describe("reset", function() + it("Resets routes", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + local newState = { + index = 2, + routes = { + { key = "x", routeName = routeName }, + { key = "y", routeName = routeName }, + }, + isTransitioning = false, + } + + expectDeepEqual( + StateUtils.reset(state, { + { key = "x", routeName = routeName }, + { key = "y", routeName = routeName }, + }), + newState + ) + end) + + it("throws when attempting to set empty state", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + expect(function() + StateUtils.reset(state, {}) + end).to.throw("invalid routes to replace") + end) + + it("Resets routes with index", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + local newState = { + index = 1, + routes = { + { key = "x", routeName = routeName }, + { key = "y", routeName = routeName }, + }, + isTransitioning = false, + } + + expectDeepEqual( + StateUtils.reset(state, { + { key = "x", routeName = routeName }, + { key = "y", routeName = routeName }, + }, 1), + newState + ) + end) + + it("throws when attempting to set an out of range route index", function() + local state = { + index = 1, + routes = { + { key = "a", routeName = routeName }, + { key = "b", routeName = routeName }, + }, + isTransitioning = false, + } + expect(function() + StateUtils.reset(state, { + { key = "x", routeName = routeName }, + { key = "y", routeName = routeName }, + }, 100) + end).to.throw("invalid index 100 to reset") + end) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/createAppContainer.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/createAppContainer.roblox.spec.lua new file mode 100644 index 0000000..8074720 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/createAppContainer.roblox.spec.lua @@ -0,0 +1,136 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Roact) + local NavigationActions = require(script.Parent.Parent.NavigationActions) + local createAppContainer = require(script.Parent.Parent.createAppContainer) + local createRobloxSwitchNavigator = require(script.Parent.Parent.navigators.createRobloxSwitchNavigator) + + it("should be a function", function() + expect(type(createAppContainer)).to.equal("function") + end) + + it("should return a valid component when mounting a switch navigator", function() + local TestNavigator = createRobloxSwitchNavigator({ + { Foo = function() end }, + }) + + local TestApp = createAppContainer(TestNavigator) + local element = Roact.createElement(TestApp) + local instance = Roact.mount(element) + + Roact.unmount(instance) + end) + + it("should throw when navigator has both navigation and container props", function() + local TestAppComponent = Roact.Component:extend("TestAppComponent") + TestAppComponent.router = {} + function TestAppComponent:render() end + + local element = Roact.createElement(createAppContainer(TestAppComponent), { + navigation = {}, + somePropThatShouldNotBeHere = true, + }) + + local status, err = pcall(function() + Roact.mount(element) + end) + + expect(status).to.equal(false) + expect(string.find(err, "This navigator has both 'navigation' and container props.")).to.never.equal(nil) + end) + + it("should throw when not passed a table for AppComponent", function() + local TestAppComponent = 5 + + local status, err = pcall(function() + createAppContainer(TestAppComponent) + end) + + expect(status).to.equal(false) + expect(string.find(err, "AppComponent must be a navigator or a stateful Roact " .. + "component with a 'router' field")).to.never.equal(nil) + end) + + it("should throw when passed a stateful component without router field", function() + local TestAppComponent = Roact.Component:extend("TestAppComponent") + + local status, err = pcall(function() + createAppContainer(TestAppComponent) + end) + + expect(status).to.equal(false) + expect(string.find(err, "AppComponent must be a navigator or a stateful Roact " .. + "component with a 'router' field")).to.never.equal(nil) + end) + + it("should accept actions from externalDispatchConnector", function() + local TestNavigator = createRobloxSwitchNavigator({ + { Foo = function() end }, + }) + + local registeredCallback = nil + local externalDispatchConnector = function(rnCallback) + registeredCallback = rnCallback + return function() + registeredCallback = nil + end + end + + local element = Roact.createElement(createAppContainer(TestNavigator), { + externalDispatchConnector = externalDispatchConnector, + }) + + local instance = Roact.mount(element) + expect(type(registeredCallback)).to.equal("function") + + -- Make sure it processes action + local result = registeredCallback(NavigationActions.navigate({ + routeName = "Foo", + })) + expect(result).to.equal(true) + + local failResult = registeredCallback(NavigationActions.navigate({ + routeName = "Bar", -- should fail because not a valid route + })) + expect(failResult).to.equal(false) + + Roact.unmount(instance) + expect(registeredCallback).to.equal(nil) + end) + + it("should correctly pass screenProps to pages", function() + local passedScreenProps = nil + local extractedValue1 = nil + local extractedMissingValue1 = nil + local extractedMissingValue2 = nil + + local testScreenProps = { + MyKey1 = "MyValue1", + } + + local TestNavigator = createRobloxSwitchNavigator({ + { + Foo = function(props) + -- doing this in render is an abuse, but it's just a test + passedScreenProps = props.navigation.getScreenProps() + extractedValue1 = props.navigation.getScreenProps("MyKey1") + extractedMissingValue1 = props.navigation.getScreenProps("MyMissingKey", 5) + extractedMissingValue2 = props.navigation.getScreenProps("MyMissingKey") + end, + }, + }) + + local TestApp = createAppContainer(TestNavigator) + local element = Roact.createElement(TestApp, { + screenProps = testScreenProps, + }) + local instance = Roact.mount(element) + + expect(passedScreenProps).to.equal(testScreenProps) + expect(extractedValue1).to.equal("MyValue1") + expect(extractedMissingValue1).to.equal(5) + expect(extractedMissingValue2).to.equal(nil) + + Roact.unmount(instance) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildNavigation.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildNavigation.roblox.spec.lua new file mode 100644 index 0000000..e062a9e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildNavigation.roblox.spec.lua @@ -0,0 +1,122 @@ +return function() + local getChildNavigation = require(script.Parent.Parent.getChildNavigation) + + it("should return nil if there is no route matching requested key", function() + local testNavigation = { + state = { + routes = { + { key = "a" } + } + } + } + + local childNav = getChildNavigation(testNavigation, "invalid_child", function() + return testNavigation + end) + + expect(childNav).to.equal(nil) + end) + + it("should return cached child if its state is a top-level route", function() + local testNavigation = { + state = { + routes = { + { key = "a" } + }, + + }, + } + + testNavigation._childrenNavigation = { + a = { + state = testNavigation.state.routes[1] + } + } + + local childNav = getChildNavigation(testNavigation, "a", function() + return testNavigation + end) + + expect(childNav).to.equal(testNavigation._childrenNavigation.a) + end) + + it("should update cache and return new data when child's state has changed", function() + local testNavigation = { + state = { + routes = { + { key = "a", routeName = "a" }, + { key = "b", routeName = "b" }, + }, + index = 1, + }, + router = { + getComponentForRouteName = function(routeName) + return function() end + end, + getActionCreators = function() end, + }, + } + + local oldStateA = { + isFirstRouteInParent = function() + return true + end, + state = { + routes = { + { key = "a", routeName = "a" }, + { key = "b", routeName = "b" }, + }, + index = 2, + }, + } + + testNavigation._childrenNavigation = { + a = oldStateA, + } + + local childNav = getChildNavigation(testNavigation, "a", function() + return testNavigation + end) + + expect(childNav).to.equal(testNavigation._childrenNavigation["a"]) + expect(childNav.state).to.equal(testNavigation.state.routes[1]) + expect(type(childNav.getParam)).to.equal("function") + end) + + it("should create a new entry if cached child does not exist yet", function() + local testNavigation = { + state = { + routes = { + { key = "a", routeName = "a", params = { a = 1 } }, + { key = "b", routeName = "b" }, + }, + index = 1, + }, + router = { + getComponentForRouteName = function(routeName) + return function() end + end, + getActionCreators = function() end, + }, + addListener = function() + return { + remove = function() end + } + end, + isFocused = function() + return true + end, + } + + local childNav = getChildNavigation(testNavigation, "a", function() + return testNavigation + end) + + expect(testNavigation._childrenNavigation["a"]).to.never.equal(nil) + expect(childNav).to.equal(testNavigation._childrenNavigation["a"]) + expect(childNav.isFocused()).to.equal(true) + + expect(childNav.getParam("a", 0)).to.equal(1) + expect(childNav.getParam("b", 0)).to.equal(0) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildRouter.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildRouter.roblox.spec.lua new file mode 100644 index 0000000..dec6a68 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildRouter.roblox.spec.lua @@ -0,0 +1,60 @@ +return function() + local getChildRouter = require(script.Parent.Parent.getChildRouter) + + it("should throw if router is not a table", function() + local status, err = pcall(function() + getChildRouter(5, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "router must be a table")).to.never.equal(nil) + end) + + it("should throw if routeName is not a string", function() + local status, err = pcall(function() + getChildRouter({}, 5) + end) + + expect(status).to.equal(false) + expect(string.find(err, "routeName must be a string")).to.never.equal(nil) + end) + + it("should return child router if found", function() + local childRouter = {} + local result = getChildRouter({ + childRouters = { + myRoute = childRouter, + } + }, "myRoute") + + expect(result).to.equal(childRouter) + end) + + it("should look up component router if no child router is found", function() + local component = { router = {} } + + local result = getChildRouter({ + getComponentForRouteName = function(routeName) + if routeName == "myRoute" then + return component + else + return nil + end + end + }, "myRoute") + + expect(result).to.equal(component.router) + end) + + it("should throw if no child routers are specified and getComponentForRouteName is not a function", function() + local status, err = pcall(function() + getChildRouter({ + getComponentForRouteName = 5 + }, "myRoute") + end) + + expect(status).to.equal(false) + expect(string.find(err, "router.getComponentForRouteName must be a function if no child routers are specified") + ).to.never.equal(nil) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildrenNavigationCache.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildrenNavigationCache.spec.lua new file mode 100644 index 0000000..ca036d9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getChildrenNavigationCache.spec.lua @@ -0,0 +1,64 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/f10543f9fcc0f347c9d23aeb57616fd0f21cd4e3/packages/core/src/__tests__/getChildrenNavigationCache.test.js +return function() + local getChildrenNavigationCache = require(script.Parent.Parent.getChildrenNavigationCache) + + it("should return empty table if navigation arg not provided", function() + expect(getChildrenNavigationCache()._childrenNavigation).to.equal(nil) + end) + + it("should populate navigation._childrenNavigation as a side-effect", function() + local navigation = { state = {} } + local result = getChildrenNavigationCache(navigation) + expect(result).to.never.equal(nil) + expect(navigation._childrenNavigation).to.equal(result) + end) + + it("should delete children cache keys that are no longer valid", function() + local navigation = { + state = { + routes = { + { key = "one" }, + { key = "two" }, + { key = "three" }, + } + }, + _childrenNavigation = { + one = {}, + two = {}, + three = {}, + four = {}, + } + } + + local result = getChildrenNavigationCache(navigation) + expect(result.one).to.never.equal(nil) + expect(result.two).to.never.equal(nil) + expect(result.three).to.never.equal(nil) + expect(result.four).to.equal(nil) + end) + + it("should not delete children cache keys if in transitioning state", function() + local navigation = { + state = { + routes = { + { key = "one" }, + { key = "two" }, + { key = "three" }, + }, + isTransitioning = true, + }, + _childrenNavigation = { + one = {}, + two = {}, + three = {}, + four = {}, + } + } + + local result = getChildrenNavigationCache(navigation) + expect(result.one).to.never.equal(nil) + expect(result.two).to.never.equal(nil) + expect(result.three).to.never.equal(nil) + expect(result.four).to.never.equal(nil) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getEventManager.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getEventManager.spec.lua new file mode 100644 index 0000000..f2a0378 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getEventManager.spec.lua @@ -0,0 +1,54 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/f10543f9fcc0f347c9d23aeb57616fd0f21cd4e3/packages/core/src/__tests__/getEventManager.test.js +return function() + local root = script.Parent.Parent + local getEventManager = require(root.getEventManager) + local Events = require(root.Events) + local createSpy = require(root.utils.createSpy) + + local TARGET = "target" + + it("calls listeners to emitted event", function() + local eventManager = getEventManager(TARGET) + local callback = createSpy() + eventManager.addListener(Events.DidFocus, callback.value) + + eventManager.emit(Events.DidFocus) + + expect(callback.callCount).to.equal(1) + end) + + it("does not call listeners connected to a different event", function() + local eventManager = getEventManager(TARGET) + local callback = createSpy() + eventManager.addListener(Events.DidFocus, callback.value) + + eventManager.emit("didBlur") + + expect(callback.callCount).to.equal(0) + end) + + it("does not call removed listeners", function() + local eventManager = getEventManager(TARGET) + local callback = createSpy() + local remove = eventManager.addListener(Events.DidFocus, callback.value).remove + + eventManager.emit(Events.DidFocus) + expect(callback.callCount).to.equal(1) + + remove() + + eventManager.emit(Events.DidFocus) + expect(callback.callCount).to.equal(1) + end) + + it("calls the listeners with the given payload", function() + local eventManager = getEventManager(TARGET) + local callback = createSpy() + eventManager.addListener(Events.DidFocus, callback.value) + + local payload = { foo = 0 } + eventManager.emit(Events.DidFocus, payload) + + callback:assertCalledWithDeepEqual(payload) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getNavigation.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getNavigation.roblox.spec.lua new file mode 100644 index 0000000..5d80f5e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getNavigation.roblox.spec.lua @@ -0,0 +1,94 @@ +return function() + local Events = require(script.Parent.Parent.Events) + local getNavigation = require(script.Parent.Parent.getNavigation) + + local function makeTestBundle(testState) + testState = testState or { + routes = { + { key = "a" } + }, + index = 1, + } + + local testActions = {} + local bundle = { + testActions = testActions, + testState = testState, + testRouter = { + getActionCreators = function() + return testActions + end + }, + testDispatch = function() end, + testActionSubscribers = {}, + testGetScreenProps = function() end, + } + + function bundle.testGetCurrentNavigation() + return bundle.navigation + end + + bundle.navigation = getNavigation( + bundle.testRouter, + bundle.testState, + bundle.testDispatch, + bundle.testActionSubscribers, + bundle.testGetScreenProps, + bundle.testGetCurrentNavigation + ) + + return bundle + end + + it("should build out correct public props", function() + local bundle = makeTestBundle() + + expect(bundle.navigation.actions).to.equal(bundle.testActions) + expect(bundle.navigation.router).to.equal(bundle.testRouter) + expect(bundle.navigation.state).to.equal(bundle.testState) + expect(bundle.navigation.dispatch).to.equal(bundle.testDispatch) + expect(bundle.navigation.getScreenProps).to.equal(bundle.testGetScreenProps) + expect(#bundle.navigation._childrenNavigation).to.equal(0) + end) + + describe("isFocused tests", function() + it("should return focused=true for child key matching index", function() + local bundle = makeTestBundle() + expect(bundle.navigation.isFocused("a")).to.equal(true) + end) + + it("should return focused=false for child key not matching index", function() + local bundle = makeTestBundle({ + routes = { + { key = "a" }, + { key = "b" }, + }, + index = 2, + }) + expect(bundle.navigation.isFocused("a")).to.equal(false) + end) + + it("should return focused=true if no child key provided (parent always focused)", function() + local bundle = makeTestBundle() + expect(bundle.navigation.isFocused()).to.equal(true) + end) + end) + + describe("addListener tests", function() + it("should short-circuit subscriptions for non-Action events", function() + local bundle = makeTestBundle() + + local testHandler = function() end + bundle.navigation.addListener(Events.WillFocus, testHandler) + expect(bundle.testActionSubscribers[testHandler]).to.equal(nil) + end) + + it("should add Action event handlers to actionSubscribers set", function() + local bundle = makeTestBundle() + + local testHandler = function() end + bundle.navigation.addListener(Events.Action, testHandler) + expect(bundle.testActionSubscribers[testHandler]).to.equal(true) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getNavigation.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getNavigation.spec.lua new file mode 100644 index 0000000..4a799b8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/getNavigation.spec.lua @@ -0,0 +1,131 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/20e2625f351f90fadadbf98890270e43e744225b/packages/core/src/__tests__/getNavigation.test.js + +return function() + local root = script.Parent.Parent + local getNavigation = require(root.getNavigation) + local NavigationActions = require(root.NavigationActions) + + local createSpy = require(root.utils.createSpy) + + it("getNavigation provides default action helpers", function() + local router = { + getActionCreators = function() + return {} + end, + getStateForAction = function(action, lastState) + return lastState or {} + end, + } + + local dispatchSpy = createSpy() + + local topNav = getNavigation( + router, + {}, + dispatchSpy.value, + {}, + function() + return {} + end, + function() end + ) + + topNav.navigate("GreatRoute") + + expect(dispatchSpy.callCount).to.equal(1) + expect(dispatchSpy.values[1].type).to.equal(NavigationActions.Navigate) + expect(dispatchSpy.values[1].routeName).to.equal("GreatRoute") + end) + + it("getNavigation provides router action helpers", function() + local router = { + getActionCreators = function() + return { + foo = function(bar) + return { type = "FooBarAction", bar = bar } + end, + } + end, + getStateForAction = function(action, lastState) + return lastState or {} + end, + } + + local dispatchSpy = createSpy() + + local topNav = nil + topNav = getNavigation( + router, + {}, + dispatchSpy.value, + {}, + function() + return {} + end, + function() + return topNav + end + ) + + topNav.foo("Great") + + expect(dispatchSpy.callCount).to.equal(1) + expect(dispatchSpy.values[1].type).to.equal("FooBarAction") + expect(dispatchSpy.values[1].bar).to.equal("Great") + end) + + it("getNavigation get child navigation with router", function() + local actionSubscribers = {} + local navigation = nil + + local routerA = { + getActionCreators = function() + return {} + end, + getStateForAction = function(action, lastState) + return lastState or {} + end, + } + local router = { + childRouters = { + RouteA = routerA, + }, + getActionCreators = function() + return {} + end, + getStateForAction = function(action, lastState) + return lastState or {} + end, + } + + local initState = { + index = 0, + routes = { + { + key = "a", + routeName = "RouteA", + routes = {{ key = "c", routeName = "RouteC" }}, + index = 0, + }, + { key = "b", routeName = "RouteB" }, + }, + } + + local topNav = getNavigation( + router, + initState, + function() end, + actionSubscribers, + function() + return {} + end, + function() + return navigation + end + ) + + local childNavA = topNav.getChildNavigation("a") + + expect(childNavA.router).to.equal(routerA) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/init.spec.lua new file mode 100644 index 0000000..fa8cca5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/_tests_/init.spec.lua @@ -0,0 +1,147 @@ +return function() + local root = script.Parent.Parent + local Packages = root.Parent + local RoactNavigation = require(root) + local Roact = require(Packages.Roact) + local StackPresentationStyle = require(root.views.RobloxStackView.StackPresentationStyle) + + it("should return a function for createAppContainer", function() + expect(type(RoactNavigation.createAppContainer)).to.equal("function") + end) + + it("should return a function for getNavigation", function() + expect(type(RoactNavigation.getNavigation)).to.equal("function") + end) + + it("should return an appropriate table for Context", function() + expect(type(RoactNavigation.Context)).to.equal("table") + expect(type(RoactNavigation.Context.Provider)).to.equal("table") + expect(type(RoactNavigation.Context.Consumer)).to.equal("table") + end) + + it("should return a Component for Provider", function() + expect(type(RoactNavigation.Provider)).to.equal("table") + end) + + it("should return a Component for Consumer", function() + expect(type(RoactNavigation.Consumer)).to.equal("table") + end) + + it("should return a function for withNavigation", function() + expect(type(RoactNavigation.withNavigation)).to.equal("function") + end) + + it("should return a function for withNavigationFocus", function() + expect(type(RoactNavigation.withNavigationFocus)).to.equal("function") + end) + + it("should return a function for createRobloxSwitchNavigator", function() + expect(type(RoactNavigation.createRobloxSwitchNavigator)).to.equal("function") + end) + + it("should return a function for createRobloxStackNavigator", function() + expect(type(RoactNavigation.createRobloxStackNavigator)).to.equal("function") + end) + + it("should return a function for createNavigator", function() + expect(type(RoactNavigation.createNavigator)).to.equal("function") + end) + + it("should return a function for StackRouter", function() + expect(type(RoactNavigation.StackRouter)).to.equal("function") + end) + + it("should return a function for SwitchRouter", function() + expect(type(RoactNavigation.SwitchRouter)).to.equal("function") + end) + + it("should return a function for TabRouter", function() + expect(type(RoactNavigation.TabRouter)).to.equal("function") + end) + + it("should return a table for Actions", function() + expect(type(RoactNavigation.Actions)).to.equal("table") + end) + + it("should return a table for StackActions", function() + expect(type(RoactNavigation.StackActions)).to.equal("table") + end) + + it("should return a table for SwitchActions", function() + expect(type(RoactNavigation.SwitchActions)).to.equal("table") + end) + + it("should return a table for BackBehavior", function() + expect(type(RoactNavigation.BackBehavior)).to.equal("table") + end) + + it("should return a table for Events", function() + expect(type(RoactNavigation.Events)).to.equal("table") + end) + + it("should return a valid component for NavigationEvents", function() + local instance = Roact.mount(Roact.createElement(RoactNavigation.Provider, { + value = { + addListener = function() + return { remove = function() end } + end + } + }, { + Events = Roact.createElement(RoactNavigation.NavigationEvents), + })) + Roact.unmount(instance) + end) + + it("should return StackPresentationStyle", function() + expect(RoactNavigation.StackPresentationStyle).to.equal(StackPresentationStyle) + end) + + it("should return a valid component for SceneView", function() + expect(RoactNavigation.SceneView.render).never.to.equal(nil) + local instance = Roact.mount(Roact.createElement(RoactNavigation.SceneView, { + navigation = {}, + component = function() end, + })) + Roact.unmount(instance) + end) + + it("should return a valid component for RobloxSwitchView", function() + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo", } + }, + index = 1, + } + } + + local instance = Roact.mount(Roact.createElement(RoactNavigation.RobloxSwitchView, { + descriptors = { + Foo = { + getComponent = function() + return function() end + end, + navigation = testNavigation, + } + }, + navigation = testNavigation, + })) + Roact.unmount(instance) + end) + + it("should return a function for createConfigGetter", function() + expect(type(RoactNavigation.createConfigGetter)).to.equal("function") + end) + + it("should return a function for getScreenForRouteName", function() + expect(type(RoactNavigation.getScreenForRouteName)).to.equal("function") + end) + + it("should return a function for validateRouteConfigMap", function() + expect(type(RoactNavigation.validateRouteConfigMap)).to.equal("function") + end) + + it("should return a function for getActiveChildNavigationOptions", function() + expect(type(RoactNavigation.getActiveChildNavigationOptions)).to.equal("function") + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/createAppContainer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/createAppContainer.lua new file mode 100644 index 0000000..fe2d0fc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/createAppContainer.lua @@ -0,0 +1,300 @@ +local Roact = require(script.Parent.Parent.Roact) +local Cryo = require(script.Parent.Parent.Cryo) +local NavigationActions = require(script.Parent.NavigationActions) +local Events = require(script.Parent.Events) +local NavigationContext = require(script.Parent.views.NavigationContext) +local getNavigation = require(script.Parent.getNavigation) +local validate = require(script.Parent.utils.validate) + +local function validateProps(props) + if not props.navigation then + return + end + + local errStr = + "This navigator has both 'navigation' and container props. " .. + "It is unclear if it should own its own state. Remove the " .. + "container props or don't pass a 'navigation' prop." + + for key in pairs(props) do + validate(key == "screenProps" or key == "navigation", errStr) + end +end + +--[[ + Construct a container Roact component that will host the navigation hierarchy + specified by your main AppComponent. AppComponent must be a navigator created by + a Roact-Navigation helper function, or a stateful Roact component + + If you are using a custom stateful Roact component, make sure to set the 'router' + field so that it can be hooked into the navigation system. You must also pass your + 'navigation' prop to any child navigators. + + Additional props: + renderLoading - Roact component to render while the app is loading. + externalDispatchConnector - Function that Roact Navigation can use to connect to + externally triggered navigation Actions. This is useful + for external UI or handling of the Android back button. + + Ex: + local connector = function(rnDispatch) + -- You store rnDispatch and call it when you want to inject + -- an event from outside RN. + return function() + -- You disconnect rnDispatch when RN calls this. + end + end + + ... + Roact.createElement(MyRNAppContainer, { + externalDispatchConnector = connector, + }) +]] +return function(AppComponent) + validate(type(AppComponent) == "table" and AppComponent.router ~= nil, + "AppComponent must be a navigator or a stateful Roact component with a 'router' field") + + local containerName = string.format("NavigationContainer(%s)", tostring(AppComponent)) + local NavigationContainer = Roact.Component:extend(containerName) + + function NavigationContainer.getDerivedStateFromProps(nextProps) + validateProps(nextProps) + return nil + end + + function NavigationContainer:init() + validateProps(self.props) + + self._actionEventSubscribers = {} + self._initialAction = NavigationActions.init() + + local initialNav = nil + local containerIsStateful = self:_isStateful() + if containerIsStateful and not self.props.persistenceKey then + initialNav = AppComponent.router.getStateForAction(self._initialAction) + end + + self.state = { + nav = initialNav, + } + end + + function NavigationContainer:_updateExternalDispatchConnector() + local externalDispatchConnector = self.props.externalDispatchConnector + if self._subs then + self._subs() + self._subs = nil + end + + if externalDispatchConnector ~= nil then + self._subs = externalDispatchConnector(function(...) + if self._isMounted then + return self:dispatch(...) + end + + -- External dispatch while we're not mounted gets dropped on floor. + return false + end) + end + end + + function NavigationContainer:_renderLoading() + local renderLoading = self.props.renderLoading + if renderLoading then + return renderLoading() + else + return nil + end + end + + function NavigationContainer:render() + local navigation = self.props.navigation + + if self:_isStateful() then + local navState = self.state.nav + if not navState then + return self:_renderLoading() + end + + if not self._navigation or self._navigation.state ~= navState then + self._navigation = getNavigation( + AppComponent.router, + navState, + function(...) + return self:dispatch(...) + end, + self._actionEventSubscribers, + function(...) + return self:_getScreenProps(...) + end, + function() + return self._navigation + end + ) + end + + navigation = self._navigation + end + + validate(navigation ~= nil, "failed to get navigation") + + return Roact.createElement(NavigationContext.Provider, { + value = navigation, + }, { + -- Provide navigation prop for top-level component so it doesn't have to connect. + AppComponent = Roact.createElement(AppComponent, Cryo.Dictionary.join(self.props, { + navigation = navigation, + })) + }) + end + + function NavigationContainer:didMount() + self._isMounted = true + + self:_updateExternalDispatchConnector() + + if not self:_isStateful() then + return + end + + local action = self._initialAction + local startupState = self.state.nav + + if not startupState then + startupState = AppComponent.router.getStateForAction(action) + end + + local function dispatchActionEvents() + -- _actionEventSubscribers is a table(handler, true), e.g. a Set container + for subscriber in pairs(self._actionEventSubscribers) do + subscriber({ + type = Events.Action, + action = action, + state = self.state.nav, + -- there is no lastState for initial mounting + }) + end + end + + if startupState ~= self.state.nav then + self:setState({ + nav = startupState + }) + end + + -- This must be spawned until we get async setState callback handler in Roact + spawn(dispatchActionEvents) + end + + function NavigationContainer:willUnmount() + self._isMounted = false + + -- TODO: Disconnect from from URL listener once implemented + + if self._subs then + self._subs() + self._subs = nil + end + end + + function NavigationContainer:didUpdate(oldProps) + -- Clear cached _navState every time we update. + if self._navState == self.state.nav then + self._navState = nil + end + + if self.props.externalDispatchConnector ~= oldProps.externalDispatchConnector then + self:_updateExternalDispatchConnector() + end + end + + function NavigationContainer:_isStateful() + return not self.props.navigation + end + + -- NOTE: Not implementing _validateProps; it is duplicate + + -- NOTE: Not implementing _handleOpenURL; app should have a component + -- that transforms URLs into paths for AppContainer instead. + + function NavigationContainer:_onNavigationStateChange(prevNav, nextNav, action) + local onNavigationStateChange = self.props.onNavigationStateChange + + if type(onNavigationStateChange) == "function" then + onNavigationStateChange(prevNav, nextNav, action) + end + end + + function NavigationContainer:_getScreenProps(propKey, defaultValue) + local screenProps = self.props.screenProps or {} + if propKey ~= nil then + return screenProps[propKey] or defaultValue + end + + -- Legacy: return original table if no args provided + return screenProps + end + + function NavigationContainer:dispatch(action) + if self.props.navigation then + return self.props.navigation.dispatch(action) + end + + self._navState = self._navState or self.state.nav + + local lastNavState = self._navState + validate(lastNavState ~= nil, "navState should be set in constructor if stateful") + + local reducedState = AppComponent.router.getStateForAction(action, lastNavState) + local navState = reducedState + if not navState then + navState = lastNavState + end + + local function dispatchActionEvents() + -- _actionEventSubscribers is a table(handler, true), e.g. a Set container + for subscriber in pairs(self._actionEventSubscribers) do + subscriber({ + type = Events.Action, + action = action, + state = navState, + lastState = lastNavState, + }) + end + end + + if reducedState == nil then + -- Router returns nil when action has been handled and there is no state change. + -- dispatch() must return true whenever something has been handled. + dispatchActionEvents() + return true + end + + if navState ~= lastNavState then + -- Update cache to ensure that subsequent calls do not discard this change + self._navState = navState + + -- TODO: We have to dispatch events before or after setState (which mounts/unmounts components) + -- based upon the specific event type, to ensure that pages get them in the correct order... + + self:setState({ + nav = navState + }) + + -- Must be spawned until we get async setState callback handler in Roact. + spawn(function() + self:_onNavigationStateChange(lastNavState, navState, action) + dispatchActionEvents() + -- TODO: Add call to persist navigation state here, if we ever implement it. + end) + + return true + end + + spawn(dispatchActionEvents) + + return false + end + + return NavigationContainer +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildNavigation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildNavigation.lua new file mode 100644 index 0000000..d6a5e8e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildNavigation.lua @@ -0,0 +1,137 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/72e8160537954af40f1b070aa91ef45fc02bba69/packages/core/src/getChildNavigation.js + +local Cryo = require(script.Parent.Parent.Cryo) +local getEventManager = require(script.Parent.getEventManager) +local getChildRouter = require(script.Parent.getChildRouter) +local getNavigationActionCreators = require(script.Parent.routers.getNavigationActionCreators) +local getChildrenNavigationCache = require(script.Parent.getChildrenNavigationCache) + +local function createParamGetter(route) + return function(paramName, defaultValue) + local params = route.params + + if params and params[paramName] ~= nil then + return params[paramName] + else + return defaultValue + end + end +end + +local function getChildNavigation(navigation, childKey, getCurrentParentNavigation) + local children = getChildrenNavigationCache(navigation) + + local childRouteIndex = Cryo.List.findWhere(navigation.state.routes, function(route) + return route.key == childKey + end) + + if not childRouteIndex then + return nil + end + local childRoute = navigation.state.routes[childRouteIndex] + + local requestedChild = children[childKey] + + if requestedChild and requestedChild.state == childRoute then + return requestedChild + end + + local childRouter = getChildRouter(navigation.router, childRoute.routeName) + + -- If the route has children that match our routes schema then get a reference + -- to the focused grandchild so we can pass the correct action creators to the + -- child router so that any action that depends on the child route will behave + -- as expected. + local focusedGrandChildRoute = nil + if childRoute.routes and type(childRoute.index) == "number" then + focusedGrandChildRoute = childRoute.routes[childRoute.index] + end + + local childRouterActionCreators = childRouter and + childRouter.getActionCreators(focusedGrandChildRoute, childRoute.key) or {} + + local actionCreators = Cryo.Dictionary.join( + navigation.actions or {}, + navigation.router.getActionCreators(childRoute, navigation.state.key) or {}, + childRouterActionCreators or {}, + getNavigationActionCreators(childRoute) or {} + ) + + local actionHelpers = {} + for actionName, actionCreator in pairs(actionCreators) do + actionHelpers[actionName] = function(...) + local action = actionCreator(...) + return navigation.dispatch(action) + end + end + + local isFirstRouteInParent = true; + + local parentNavigation = getCurrentParentNavigation(); + + if parentNavigation then + isFirstRouteInParent = Cryo.List.find(parentNavigation.state.routes, childRoute) == 1; + end + + if requestedChild and requestedChild.isFirstRouteInParent() == isFirstRouteInParent then + -- Update cache value for requestedChild because child's state has changed + children[childKey] = Cryo.Dictionary.join(requestedChild, actionHelpers, { + state = childRoute, + router = childRouter, + actions = actionCreators, + getParam = createParamGetter(childRoute), + }) + + return children[childKey] + else + -- No cached value for requestedChild. Create a new entry. + local childSubscriber = getEventManager(childKey) + + children[childKey] = Cryo.Dictionary.join(actionHelpers, { + state = childRoute, + router = childRouter, + actions = actionCreators, + getParam = createParamGetter(childRoute), + getChildNavigation = function(grandChildKey) + return getChildNavigation(children[childKey], grandChildKey, function() + local nav = getCurrentParentNavigation() + return nav and nav.getChildNavigation(childKey) or nil + end) + end, + isFocused = function() + local currentNavigation = getCurrentParentNavigation() + if not currentNavigation then + return false + end + + if not currentNavigation.isFocused() then + return false + end + + local state = currentNavigation.state + local routes = state.routes + local index = state.index + + if routes[index].key == childKey then + return true + end + + return false + end, + isFirstRouteInParent = function() + return isFirstRouteInParent + end, + dispatch = navigation.dispatch, + getScreenProps = navigation.getScreenProps, + -- deviation: `dangerouslyGetParent` is renamed as private because + -- it is deprecated in latest react navigation + _dangerouslyGetParent = getCurrentParentNavigation, + addListener = childSubscriber.addListener, + emit = childSubscriber.emit, + }) + + return children[childKey] + end +end + +return getChildNavigation diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildRouter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildRouter.lua new file mode 100644 index 0000000..ffcde51 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildRouter.lua @@ -0,0 +1,22 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/getChildRouter.ts +local validate = require(script.Parent.utils.validate) + +return function(router, routeName) + validate(type(router) == "table", "router must be a table") + validate(type(routeName) == "string", "routeName must be a string") + + if router.childRouters and router.childRouters[routeName] then + return router.childRouters[routeName] + end + + validate(type(router.getComponentForRouteName) == "function", + "router.getComponentForRouteName must be a function if no child routers are specified") + local component = router.getComponentForRouteName(routeName) + + -- deviation: functional components cannot be indexed in Lua + if type(component) == "table" then + return component.router + else + return nil + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildrenNavigationCache.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildrenNavigationCache.lua new file mode 100644 index 0000000..fec4306 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getChildrenNavigationCache.lua @@ -0,0 +1,28 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/views/withNavigation.js + +return function(navigation) + if not navigation then + return {} + end + + if not navigation._childrenNavigation then + navigation._childrenNavigation = {} + end + + local childrenNavigationCache = navigation._childrenNavigation + + local childKeys = {} + for _, route in ipairs(navigation.state.routes or {}) do + childKeys[route.key] = true + end + + if not navigation.state.isTransitioning then + for cacheKey, _ in pairs(childrenNavigationCache) do + if not childKeys[cacheKey] then + childrenNavigationCache[cacheKey] = nil + end + end + end + + return navigation._childrenNavigation +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getEventManager.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getEventManager.lua new file mode 100644 index 0000000..e46a4da --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getEventManager.lua @@ -0,0 +1,49 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/9b55493e7662f4d54c21f75e53eb3911675f61bc/packages/core/src/getEventManager.js + +local root = script.Parent +local Packages = root.Parent +local Cryo = require(Packages.Cryo) + +local function getEventManager(target) + local listeners = {} + + local function removeListener(type, callback) + local callbacks = listeners[type] and listeners[type][target] + + if not callbacks then + return + end + + local index = table.find(callbacks, callback) + table.remove(callbacks, index) + end + + local function addListener(type, callback) + listeners[type] = listeners[type] or {} + listeners[type][target] = listeners[type][target] or {} + + table.insert(listeners[type][target], callback) + + return { + remove = function() + removeListener(type, callback) + end, + } + end + + return { + addListener = addListener, + emit = function(type, data) + local items = listeners[type] or {} + local callbacks = items[target] and Cryo.List.join({}, items[target]) + + if callbacks then + for _, callback in ipairs(callbacks) do + callback(data) + end + end + end, + } +end + +return getEventManager diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getNavigation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getNavigation.lua new file mode 100644 index 0000000..4fd795c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/getNavigation.lua @@ -0,0 +1,63 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/72e8160537954af40f1b070aa91ef45fc02bba69/packages/core/src/getNavigation.js + +local Cryo = require(script.Parent.Parent.Cryo) +local Events = require(script.Parent.Events) +local getNavigationActionCreators = require(script.Parent.routers.getNavigationActionCreators) +local getChildNavigation = require(script.Parent.getChildNavigation) +local getChildrenNavigationCache = require(script.Parent.getChildrenNavigationCache) + +return function(router, state, dispatch, actionSubscribers, getScreenProps, getCurrentNavigation) + local actions = router.getActionCreators(state, nil) + + local navigation = { + actions = actions, + router = router, + state = state, + dispatch = dispatch, + getScreenProps = getScreenProps, + -- deviation: `dangerouslyGetParent` is renamed as private because + -- it is deprecated in latest react navigation + _dangerouslyGetParent = function() + return nil + end, + isFirstRouteInParent = function() + return true + end, + _childrenNavigation = getChildrenNavigationCache(getCurrentNavigation()), + } + + function navigation.getChildNavigation(childKey) + return getChildNavigation(navigation, childKey, getCurrentNavigation) + end + + function navigation.isFocused(childKey) + local currentState = getCurrentNavigation().state + local routes = currentState.routes + local index = currentState.index + + return childKey == nil or routes[index].key == childKey + end + + function navigation.addListener(event, handler) + if event ~= Events.Action then + return { remove = function() end } + else + actionSubscribers[handler] = true + return { + remove = function() + actionSubscribers[handler] = nil + end + } + end + end + + local actionCreators = Cryo.Dictionary.join(getNavigationActionCreators(navigation.state), actions) + + for actionName, _ in pairs(actionCreators) do + navigation[actionName] = function(...) + navigation.dispatch(actionCreators[actionName](...)) + end + end + + return navigation +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/init.lua new file mode 100644 index 0000000..b912896 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/init.lua @@ -0,0 +1,54 @@ +-- Generator information: +-- Human name: Roact Navigation +-- Variable name: RoactNavigation +-- Repo name: roact-navigation + +local NavigationContext = require(script.views.NavigationContext) + +return { + -- Navigation container construction + createAppContainer = require(script.createAppContainer), + getNavigation = require(script.getNavigation), + + -- Context Access + Context = NavigationContext, + Provider = NavigationContext.Provider, + Consumer = NavigationContext.Consumer, + + withNavigation = require(script.views.withNavigation), + withNavigationFocus = require(script.views.withNavigationFocus), + + -- Navigators + createRobloxStackNavigator = require(script.navigators.createRobloxStackNavigator), + createRobloxSwitchNavigator = require(script.navigators.createRobloxSwitchNavigator), + createNavigator = require(script.navigators.createNavigator), + + -- Routers + StackRouter = require(script.routers.StackRouter), + SwitchRouter = require(script.routers.SwitchRouter), + TabRouter = require(script.routers.TabRouter), + + -- Navigation Actions + Actions = require(script.NavigationActions), + StackActions = require(script.routers.StackActions), + SwitchActions = require(script.routers.SwitchActions), + BackBehavior = require(script.BackBehavior), + + -- Navigation Events + Events = require(script.Events), + NavigationEvents = require(script.views.NavigationEvents), + + -- Additional Types + StackPresentationStyle = require(script.views.RobloxStackView.StackPresentationStyle), + + -- Screen Views + SceneView = require(script.views.SceneView), + RobloxSwitchView = require(script.views.RobloxSwitchView), + RobloxStackView = require(script.views.RobloxStackView.StackView), + + -- Utilities + createConfigGetter = require(script.routers.createConfigGetter), + getScreenForRouteName = require(script.routers.getScreenForRouteName), + validateRouteConfigMap = require(script.routers.validateRouteConfigMap), + getActiveChildNavigationOptions = require(script.utils.getActiveChildNavigationOptions), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createNavigator.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createNavigator.spec.lua new file mode 100644 index 0000000..15a8a2c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createNavigator.spec.lua @@ -0,0 +1,79 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local createNavigator = require(script.Parent.Parent.createNavigator) + + local testRouter = { + getScreenOptions = function() return nil end, + } + + it("should return a Roact component that exposes navigator fields", function() + local testComponentMounted = nil + local TestViewComponent = Roact.Component:extend("TestViewComponent") + function TestViewComponent:render() end + function TestViewComponent:didMount() testComponentMounted = true end + function TestViewComponent:willUnmount() testComponentMounted = false end + + local testNavOptions = {} + + local navigator = createNavigator(TestViewComponent, testRouter, { + navigationOptions = testNavOptions, + }) + + expect(navigator.render).to.be.a("function") + expect(navigator.router).to.equal(testRouter) + expect(navigator.navigationOptions).to.equal(testNavOptions) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + }, + index = 1 + }, + getChildNavigation = function() return nil end, -- stub + addListener = function() end, + } + + -- Try to mount it + local instance = Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + + expect(testComponentMounted).to.equal(true) + Roact.unmount(instance) + expect(testComponentMounted).to.equal(false) + end) + + it("should throw when trying to mount without navigation prop", function() + local TestViewComponent = function() end + + local navigator = createNavigator(TestViewComponent, testRouter, { + navigationOptions = {} + }) + + expect(function() + Roact.mount(Roact.createElement(navigator)) + end).to.throw() + end) + + it("should throw when trying to mount without routes", function() + local TestViewComponent = function() end + + local navigator = createNavigator(TestViewComponent, testRouter, { + navigationOptions = {} + }) + + local testNavigation = { + state = { + index = 1 + }, + getChildNavigation = function() return nil end, -- stub + } + + expect(function() + Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + end).to.throw() + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createRobloxStackNavigator.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createRobloxStackNavigator.roblox.spec.lua new file mode 100644 index 0000000..512e19d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createRobloxStackNavigator.roblox.spec.lua @@ -0,0 +1,40 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local createRobloxStackNavigator = require(script.Parent.Parent.createRobloxStackNavigator) + local getChildNavigation = require(script.Parent.Parent.Parent.getChildNavigation) + + it("should return a mountable Roact component", function() + local navigator = createRobloxStackNavigator({ + { Foo = function() end }, + }) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + }, + index = 1 + }, + router = navigator.router + } + + function testNavigation.getChildNavigation(childKey) + return getChildNavigation(testNavigation, childKey, function() + return testNavigation + end) + end + + function testNavigation.addListener(symbol, callback) + return { + remove = function() end + } + end + + local instance = Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + + Roact.unmount(instance) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createRobloxSwitchNavigator.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createRobloxSwitchNavigator.roblox.spec.lua new file mode 100644 index 0000000..29aec47 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createRobloxSwitchNavigator.roblox.spec.lua @@ -0,0 +1,40 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local createRobloxSwitchNavigator = require(script.Parent.Parent.createRobloxSwitchNavigator) + local getChildNavigation = require(script.Parent.Parent.Parent.getChildNavigation) + + it("should return a mountable Roact component", function() + local navigator = createRobloxSwitchNavigator({ + { Foo = function() end }, + }) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + }, + index = 1 + }, + router = navigator.router + } + + function testNavigation.getChildNavigation(childKey) + return getChildNavigation(testNavigation, childKey, function() + return testNavigation + end) + end + + function testNavigation.addListener(symbol, callback) + return { + remove = function() end + } + end + + local instance = Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + + Roact.unmount(instance) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createSwitchNavigator.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createSwitchNavigator.roblox.spec.lua new file mode 100644 index 0000000..4afc60a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/_tests_/createSwitchNavigator.roblox.spec.lua @@ -0,0 +1,40 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local createSwitchNavigator = require(script.Parent.Parent.createSwitchNavigator) + local getChildNavigation = require(script.Parent.Parent.Parent.getChildNavigation) + + it("should return a mountable Roact component", function() + local navigator = createSwitchNavigator({ + { Foo = function() end }, + }) + + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + }, + index = 1 + }, + router = navigator.router + } + + function testNavigation.getChildNavigation(childKey) + return getChildNavigation(testNavigation, childKey, function() + return testNavigation + end) + end + + function testNavigation.addListener(symbol, callback) + return { + remove = function() end + } + end + + local instance = Roact.mount(Roact.createElement(navigator, { + navigation = testNavigation + })) + + Roact.unmount(instance) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createNavigator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createNavigator.lua new file mode 100644 index 0000000..a1b7c1e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createNavigator.lua @@ -0,0 +1,105 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/9b55493e7662f4d54c21f75e53eb3911675f61bc/packages/core/src/navigators/createNavigator.js + +local root = script.Parent.Parent +local Packages = root.Parent +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local validate = require(root.utils.validate) +local NavigationFocusEvents = require(root.views.NavigationFocusEvents) + +return function(navigatorViewComponent, router, navigationConfig) + local Navigator = Roact.Component:extend("Navigator") + + -- These statics need to be accessible to routers + Navigator.router = router + Navigator.navigationOptions = navigationConfig.navigationOptions + -- deviation: no theme support + + function Navigator:init() + local screenProps = self.props.screenProps + + self.state = { + descriptors = {}, + screenProps = screenProps, + -- deviation: no theme support + } + end + + function Navigator.getDerivedStateFromProps(nextProps, prevState) + local prevDescriptors = prevState.descriptors + local navigation = nextProps.navigation + local screenProps = nextProps.screenProps + + validate(navigation ~= nil, "The navigation prop is missing for this navigator. " .. + "In react-navigation v3 and v4 you must set up your app container directly. " .. + "More info: https://reactnavigation.org/docs/en/app-containers.html") + + local routes = navigation.state.routes + + validate(type(routes) == "table", 'No "routes" found in navigation state. ' .. + "Did you try to pass the navigation prop of a React component to a Navigator child? " .. + "See https://reactnavigation.org/docs/en/custom-navigators.html#navigator-navigation-prop") + + local descriptors = Cryo.List.foldLeft(routes, function(descriptors, route) + if + prevDescriptors and + prevDescriptors[route.key] and + route == prevDescriptors[route.key].state and + screenProps == prevState.screenProps + -- deviation: no theme support + then + descriptors[route.key] = prevDescriptors[route.key] + else + local getComponent = function() + return router.getComponentForRouteName(route.routeName) + end + + local childNavigation = navigation.getChildNavigation(route.key) + local options = router.getScreenOptions(childNavigation, screenProps) + + descriptors[route.key] = { + key = route.key, + getComponent = getComponent, + options = options, + state = route, + navigation = childNavigation, + } + end + + return descriptors + end, {}) + + return { + descriptors = descriptors, + screenProps = screenProps, + -- deviation: no theme support + } + end + + -- deviation: no `componentDidUpdate` because no theme support + + function Navigator:render() + local navigation = self.props.navigation + local screenProps = self.state.screenProps + local descriptors = self.state.descriptors + + return Roact.createFragment({ + Events = Roact.createElement(NavigationFocusEvents, { + navigation = navigation, + onEvent = function(target, type, data) + if descriptors[target] then + descriptors[target].navigation.emit(type, data); + end + end, + }), + View = Roact.createElement(navigatorViewComponent, Cryo.Dictionary.join(self.props, { + screenProps = screenProps, + navigation = navigation, + navigationConfig = navigationConfig, + descriptors = descriptors, + })) + }) + end + + return Navigator +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createRobloxStackNavigator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createRobloxStackNavigator.lua new file mode 100644 index 0000000..e832176 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createRobloxStackNavigator.lua @@ -0,0 +1,10 @@ +local root = script.Parent.Parent +local createNavigator = require(script.Parent.createNavigator) +local StackRouter = require(root.routers.StackRouter) +local StackView = require(root.views.RobloxStackView.StackView) + +return function(routeArray, stackConfig) + local router = StackRouter(routeArray, stackConfig) + + return createNavigator(StackView, router, stackConfig or {}) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createRobloxSwitchNavigator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createRobloxSwitchNavigator.lua new file mode 100644 index 0000000..c15fa0e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createRobloxSwitchNavigator.lua @@ -0,0 +1,25 @@ +local root = script.Parent.Parent +local createNavigator = require(script.Parent.createNavigator) +local SwitchRouter = require(root.routers.SwitchRouter) +local RobloxSwitchView = require(root.views.RobloxSwitchView) + +--[[ + Creates a navigator component that provides simple screen "switcher" behavior. + Each page is mutually exclusive and no transition animation is used. + + Additional config options: + order : List + Specifies the index order for page components, e.g. for use with tab bars. + keepVisitedScreensMounted : Boolean (false) + Set to true if you want to keep previously visited screens mounted for better performance. + resetOnBlur : Boolean (true) + Set to false if you want to preserve existing state for child navigators. + backBehavior : BackBehavior (None) + Set to BackBehavior.InitialRoute to allow a goBack() operation to return to the + initial route name. By default, the SwitchNavigator will not do anything on a back action. +]] +return function(routeArray, switchConfig) + local router = SwitchRouter(routeArray, switchConfig) + + return createNavigator(RobloxSwitchView, router, switchConfig or {}) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createSwitchNavigator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createSwitchNavigator.lua new file mode 100644 index 0000000..1b64f51 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/navigators/createSwitchNavigator.lua @@ -0,0 +1,14 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/1f5000e86bef5e4c8ee6fbeb25e3ca3eb8873ad0/packages/core/src/navigators/createSwitchNavigator.js +local root = script.Parent.Parent +local createNavigator = require(script.Parent.createNavigator) +local SwitchRouter = require(root.routers.SwitchRouter) +local SwitchView = require(root.views.SwitchView.SwitchView) + +return function(routeArray, switchConfig) + if switchConfig == nil then + switchConfig = {} + end + local router = SwitchRouter(routeArray, switchConfig) + + return createNavigator(SwitchView, router, switchConfig) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/StackActions.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/StackActions.lua new file mode 100644 index 0000000..d421ada --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/StackActions.lua @@ -0,0 +1,74 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/routers/StackActions.js + +local root = script.Parent.Parent +local Packages = root.Parent +local Cryo = require(Packages.Cryo) +local NavigationSymbol = require(root.NavigationSymbol) + +local POP_TOKEN = NavigationSymbol("POP") +local POP_TO_TOP_TOKEN = NavigationSymbol("POP_TO_TOP") +local PUSH_TOKEN = NavigationSymbol("PUSH") +local RESET_TOKEN = NavigationSymbol("RESET") +local REPLACE_TOKEN = NavigationSymbol("REPLACE") +local COMPLETE_TRANSITION_TOKEN = NavigationSymbol("COMPLETE_TRANSITION") + +--[[ + StackActions provides shared constants and methods to construct + actions that are dispatched to routers to cause a change in the route. + These actions are specific to Stack navigation. See NavigationActions + if you need to use more general APIs. +]] +local StackActions = { + Pop = POP_TOKEN, + PopToTop = POP_TO_TOP_TOKEN, + Push = PUSH_TOKEN, + Reset = RESET_TOKEN, + Replace = REPLACE_TOKEN, + CompleteTransition = COMPLETE_TRANSITION_TOKEN, +} + +-- deviation: we using this metatable to error when StackActions is indexed +-- with an unexpected key. +setmetatable(StackActions, { + __index = function(self, key) + error(("%q is not a valid member of StackActions"):format(tostring(key)), 2) + end, +}) + +-- Pop the top-most item off the route stack, if any. +function StackActions.pop(payload) + return Cryo.Dictionary.join({ type = POP_TOKEN }, payload or {}) +end + +-- Pop all the items except the last one off the route stack. +function StackActions.popToTop(payload) + return Cryo.Dictionary.join({ type = POP_TO_TOP_TOKEN }, payload or {}) +end + +-- Push a new item onto the route stack. +function StackActions.push(payload) + return Cryo.Dictionary.join({ type = PUSH_TOKEN }, payload or {}) +end + +-- Reset the route stack and replace it with a new stack, +-- specified by a list of actions to be applied. +function StackActions.reset(payload) + return Cryo.Dictionary.join({ type = RESET_TOKEN }, payload or {}) +end + +-- Replace the route for the given key with a new route. +function StackActions.replace(payload) + return Cryo.Dictionary.join( + { type = REPLACE_TOKEN, preserveFocus = true }, + payload or {} + ) +end + +function StackActions.completeTransition(payload) + return Cryo.Dictionary.join( + { type = COMPLETE_TRANSITION_TOKEN, preserveFocus = true }, + payload or {} + ) +end + +return StackActions diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/StackRouter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/StackRouter.lua new file mode 100644 index 0000000..2441e00 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/StackRouter.lua @@ -0,0 +1,666 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/6390aacd07fd647d925dfec842a766c8aad5272f/packages/core/src/routers/TabRouter.js + +local root = script.Parent.Parent +local Packages = root.Parent + +local Cryo = require(Packages.Cryo) +local NavigationActions = require(root.NavigationActions) +local StackActions = require(script.Parent.StackActions) +local KeyGenerator = require(root.utils.KeyGenerator) +local StateUtils = require(root.StateUtils) +local getScreenForRouteName = require(script.Parent.getScreenForRouteName) +local createConfigGetter = require(script.Parent.createConfigGetter) +local validateRouteConfigArray = require(script.Parent.validateRouteConfigArray) +local validateRouteConfigMap = require(script.Parent.validateRouteConfigMap) +local validate = require(root.utils.validate) +local NavigationSymbol = require(root.NavigationSymbol) + +local STACK_ROUTER_ROOT_KEY = "StackRouterRoot" +-- This symbol is used to differentiate if a router has a child router +-- or if is a regular Roact component. React-navigation does it by using +-- undefined vs. null (that's why we need) +local CHILD_IS_SCREEN = NavigationSymbol("CHILD_IS_SCREEN") + +local defaultActionCreators = function() return {} end + +local function behavesLikePushAction(action) + return action.type == NavigationActions.Navigate or + action.type == StackActions.Push +end + +local function isResetToRootStack(action) + return action.type == StackActions.Reset and action.key == nil +end + +local function mapToRouteName(element) + local routeName = next(element) + return routeName +end + +local function foldToRoutes(routes, element, index) + local routeName, value = next(element) + routes[routeName] = value + return routes +end + +return function(routeArray, config) + validateRouteConfigArray(routeArray) + config = config or {} + local routeConfigs = validateRouteConfigMap( + Cryo.List.foldLeft(routeArray, foldToRoutes, {}) + ) + + local routeNames = config.order or Cryo.List.map(routeArray, mapToRouteName) + + -- Loop through routes and find child routers + local childRouters = {} + for _, routeName in ipairs(routeNames) do + -- We're not using `getScreenForRouteName` here to preserve the lazy loading + -- behaviour of routes. This means that routes with child routers must be + -- defined using a component directly or with an object with a screen prop. + local routeConfig = routeConfigs[routeName] + local screen = (type(routeConfig) == "table" and routeConfig.screen) + and routeConfig.screen or routeConfig + if type(screen) == "table" and screen.router then + -- If it has a router it's a navigator. + childRouters[routeName] = screen.router + else + -- TODO: This is a hack to make this code behave like React-Navigation's usage of + -- null and undefined values for childRouters. We should come up with a better approach. + childRouters[routeName] = CHILD_IS_SCREEN + end + end + + local initialRouteParams = config.initialRouteParams + local getCustomActionCreators = config.getCustomActionCreators or defaultActionCreators + + local initialRouteName = config.initialRouteName or routeNames[1] + + local initialChildRouter = childRouters[initialRouteName] + + local initialRouteIndex = Cryo.List.find(routeNames, initialRouteName) + -- dump an error if initialRouteName is not in routes. + if initialRouteIndex == nil then + local availableRouteStr = "" + for _, name in ipairs(routeNames) do + availableRouteStr = availableRouteStr .. name .. "," + end + + error(string.format("Invalid initialRouteName '%s'. Must be one of [%s]", initialRouteName, availableRouteStr), 2) + end + + local function getInitialState(action) + local route = {} + local childRouter = childRouters[action.routeName] + + -- This is a push-like action, and childRouter will be a router or null + -- if we are responsible for this routeName + if behavesLikePushAction(action) and childRouter ~= nil then + local childState = {} + + -- The router is 'CHILD_IS_SCREEN' for normal leaf routes + if childRouter ~= CHILD_IS_SCREEN then + local childAction = action.action + or NavigationActions.init({ params = action.params }) + childState = childRouter.getStateForAction(childAction) + end + + return { + key = STACK_ROUTER_ROOT_KEY, + isTransitioning = false, + index = 1, + routes = { + Cryo.Dictionary.join({ params = action.params }, childState, { + key = action.key or KeyGenerator.generateKey(), + routeName = action.routeName, + }) + } + } + end + + -- we need to check if initialChildRouter is not CHILD_IS_SCREEN because + -- of the divergence with react-navigation. + if initialChildRouter ~= nil and initialChildRouter ~= CHILD_IS_SCREEN then + route = initialChildRouter.getStateForAction(NavigationActions.navigate({ + routeName = initialRouteName, + params = initialRouteParams, + })) + end + + local initialRouteConfig = routeConfigs[initialRouteName] + -- we need to check if the routeConfig is a table because functions can't be + -- indexed in Lua. + local initialRouteConfigParams = type(initialRouteConfig) == "table" and initialRouteConfig.params + + local params = (initialRouteConfigParams or route.params or action.params or initialRouteParams) + and Cryo.Dictionary.join( + initialRouteConfigParams or {}, -- params set in routes table! + route.params or {}, + action.params or {}, + initialRouteParams or {} -- params provided at top level + ) + + local initialRouteKey = config.initialRouteKey + route = Cryo.Dictionary.join(route, { + params = params, + routeName = initialRouteName, + key = action.key or initialRouteKey or KeyGenerator.generateKey() + }) + + return { + key = STACK_ROUTER_ROOT_KEY, + isTransitioning = false, + index = 1, + routes = { route } + } + end + + local function getParamsForRouteAndAction(routeName, action) + local routeConfig = routeConfigs[routeName] + -- we need to check if the routeConfig is a table because functions can't be + -- indexed in Lua. + if type(routeConfig) == "table" and routeConfig.params then + return Cryo.Dictionary.join(routeConfig.params, action.params) + else + return action.params + end + end + + -- Strip out the CHILD_IS_SCREEN hacked elements before exposing publicly. + local strippedChildRouters = {} + for routerName, router in pairs(childRouters) do + if router ~= CHILD_IS_SCREEN then + strippedChildRouters[routerName] = router + end + end + + local StackRouter = { + childRouters = strippedChildRouters, + getScreenOptions = createConfigGetter(routeConfigs, config.defaultNavigationOptions), + _CHILD_IS_SCREEN = CHILD_IS_SCREEN, -- expose symbol for testing purposes + } + + function StackRouter.getComponentForState(state) + local activeChildRoute = state.routes[state.index] or {} + local routeName = activeChildRoute.routeName + validate(routeName, "There is no route defined for index '%d'. " .. + "Make sure that you passed in a navigation state with a " .. + "valid stack index.", state.index) + + local childRouter = childRouters[routeName] + -- we need to check if initialChildRouter is not CHILD_IS_SCREEN because + -- of the divergence with react-navigation. + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + return childRouters[routeName].getComponentForState(activeChildRoute) + end + + return getScreenForRouteName(routeConfigs, routeName) + end + + function StackRouter.getComponentForRouteName(routeName) + return getScreenForRouteName(routeConfigs, routeName) + end + + function StackRouter.getActionCreators(route, navStateKey) + return Cryo.Dictionary.join(getCustomActionCreators(route, navStateKey), { + pop = function(n, params) + return StackActions.pop(Cryo.Dictionary.join({ + n = n, + }, params or {})) + end, + popToTop = function(params) + return StackActions.popToTop(params) + end, + push = function(routeName, params, action) + return StackActions.push({ + routeName = routeName, + params = params, + action = action, + }) + end, + replace = function(replaceWith, params, action, newKey) + if type(replaceWith) == "string" then + return StackActions.replace({ + routeName = replaceWith, + params = params, + action = action, + key = route.key, + newKey = newKey, + }) + end + + validate(type(replaceWith) == "table", "replaceWith must be a table or string") + validate(params == nil, "params cannot be provided to .replace() when specifying a table") + validate(action == nil, "Child action cannot be provided to .replace() when specifying a table") + validate(newKey == nil, "newKey cannot be provided to .replace() when specifying a table") + + return StackActions.replace(replaceWith) + end, + reset = function(actions, index) + local resetIndex = index + if index == nil then + resetIndex = #actions + end + + return StackActions.reset({ + actions = actions, + index = resetIndex, + key = navStateKey, + }) + end, + dismiss = function() + return NavigationActions.back({ + key = navStateKey, + }) + end, + }) + end + + function StackRouter.getStateForAction(action, state) + -- Set up initial state if needed + if not state then + return getInitialState(action) + end + + local activeChildRoute = state.routes[state.index] + + if not isResetToRootStack(action) and action.type ~= NavigationActions.Navigate then + -- Let the active child router handle the action + local activeChildRouter = childRouters[activeChildRoute.routeName] + if activeChildRouter ~= nil and activeChildRouter ~= CHILD_IS_SCREEN then + local route = activeChildRouter.getStateForAction(action, activeChildRoute) + if route ~= nil and route ~= activeChildRoute then + return StateUtils.replaceAt( + state, + activeChildRoute.key, + route, + -- the following tells replaceAt to NOT change the index to this + -- route for the setParam action, because people don't expect + -- param-setting actions to switch the active route + action.type == NavigationActions.SetParams + ) + end + end + elseif action.type == NavigationActions.Navigate then + -- Traverse routes from the top of the stack to the bottom, so the + -- active route has the first opportunity, then the one before it, etc. + for i = #state.routes, 1, -1 do + local childRoute = state.routes[i] + local childRouter = childRouters[childRoute.routeName] + local childAction = action + if action.routeName == childRoute.routeName and action.action then + childAction = action.action + end + + -- we need to check if initialChildRouter is not CHILD_IS_SCREEN because + -- of the divergence with react-navigation. + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + local nextRouteState = childRouter.getStateForAction(childAction, childRoute) + if nextRouteState == nil or nextRouteState ~= childRoute then + local newState = StateUtils.replaceAndPrune( + state, + nextRouteState and nextRouteState.key or childRoute.key, + nextRouteState and nextRouteState or childRoute + ) + + local newTransitioning = state.isTransitioning + if state.index ~= newState.index then + newTransitioning = action.immediate ~= true + end + + return Cryo.Dictionary.join(newState, { + isTransitioning = newTransitioning, + }) + end + end + end + end + + -- Handle push and navigation actions. This must happen after focused child router + -- has had its chance to handle the action. + -- If a router equals `nil` it means that it is not a childRouter or a screen. + if behavesLikePushAction(action) and childRouters[action.routeName] ~= nil then + local childRouter = childRouters[action.routeName] + validate(action.type ~= StackActions.Push or action.key == nil, + "StackRouter does not support key on the push action") + + -- Before pushing a new route we first try to find one in the existing route stack + -- More information on this: https://github.com/react-navigation/rfcs/blob/master/text/0004-less-pushy-navigate.md + local findRoute = function(route) + return route.routeName == action.routeName + end + if action.key then + findRoute = function(route) + return route.key == action.key + end + end + local lastRouteIndex = Cryo.List.findWhere(state.routes, findRoute) + + -- An instance of this route exists already and we're dealing with a navigate action. + if action.type ~= StackActions.Push and lastRouteIndex ~= nil then + -- If index is unchanged and params are not being set, leave state identity intact + if state.index == lastRouteIndex and not action.params then + return nil + end + + -- Remove the now unused routes at the tail of the routes array + local routes = Cryo.List.getRange(state.routes, 1, lastRouteIndex) + + -- Apply params if provided, otherwise leave route identity intact + if action.params then + local route = state.routes[lastRouteIndex] + routes[lastRouteIndex] = Cryo.Dictionary.join(route, { + params = Cryo.Dictionary.join(route.params or {}, action.params) + }) + end + + -- Return state with new index, changing isTransitioning only if index has changed + local newIsTransitioning = state.isTransitioning + if state.index ~= lastRouteIndex then + newIsTransitioning = action.immediate ~= true + end + + return Cryo.Dictionary.join(state, { + isTransitioning = newIsTransitioning, + index = lastRouteIndex, + routes = routes, + }) + end + + local route + if childRouter ~= CHILD_IS_SCREEN then + -- Delegate to the child router with the given action, or init it + local childAction = action.action or NavigationActions.init({ + params = getParamsForRouteAndAction(action.routeName, action) + }) + + route = Cryo.Dictionary.join({ + -- does it make sense to wipe out the params here? or even to + -- add params at all? need more info about what this solves + params = getParamsForRouteAndAction(action.routeName, action), + }, childRouter.getStateForAction(childAction), { + routeName = action.routeName, + key = action.key or KeyGenerator.generateKey(), + }) + else + -- Create the route from scratch + route = { + params = getParamsForRouteAndAction(action.routeName, action), + routeName = action.routeName, + key = action.key or KeyGenerator.generateKey(), + } + end + + return Cryo.Dictionary.join(StateUtils.push(state, route), { + isTransitioning = action.immediate ~= true, + }) + elseif action.type == StackActions.Push and childRouters[action.routeName] == nil then + -- Return the state identity to bubble the action up + return state + end + + -- Handle navigation to other child routers that are not yet pushed + if behavesLikePushAction(action) then + local childRouterNames = Cryo.Dictionary.keys(childRouters) + for _, childRouterName in ipairs(childRouterNames) do + local childRouter = childRouters[childRouterName] + + -- we need to check if initialChildRouter is not CHILD_IS_SCREEN because + -- of the divergence with react-navigation. + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + -- For each child router, start with a blank state + local initChildRoute = childRouter.getStateForAction(NavigationActions.init()) + + -- Then check to see if the router handles our navigate action + local navigatedChildRoute = childRouter.getStateForAction(action, initChildRoute) + + local routeToPush = nil + if navigatedChildRoute == nil then + -- Push the route if the router has 'handled' the action and returned null + routeToPush = initChildRoute + elseif navigatedChildRoute ~= initChildRoute then + -- Push the route if the state has changed in response to this navigation + routeToPush = navigatedChildRoute + end + + if routeToPush then + local route = Cryo.Dictionary.join(routeToPush, { + routeName = childRouterName, + key = action.key or KeyGenerator.generateKey(), + }) + + return Cryo.Dictionary.join(StateUtils.push(state, route), { + isTransitioning = action.immediate ~= true, + }) + end + end + end + end + + -- Handle pop-to-top behavior. Make sure this happens after children have had a + -- chance to handle the action, so that the inner stack pops to top first. + if action.type == StackActions.PopToTop then + -- Refuse to handle pop to top if a key is given that does not correspond + -- to this router + if action.key and state.key ~= action.key then + return state + end + + -- If we're already at the top, then we return the state with a new + -- identity so that the action is handled by this router. + if state.index > 1 then + return Cryo.Dictionary.join(state, { + isTransitioning = action.immediate ~= true, + index = 1, + routes = { state.routes[1] } + }) + end + + return state + end + + if action.type == StackActions.Replace then + local routeIndex = nil -- luacheck: ignore routeIndex + + -- If the key param is undefined, set the index to the last route in the stack + if action.key == nil and #state.routes > 0 then + routeIndex = #state.routes + else + routeIndex = Cryo.List.findWhere(state.routes, function(route) + return route.key == action.key + end) + end + + if routeIndex then + local childRouter = childRouters[action.routeName] + local childState = {} + + -- we need to check if initialChildRouter is not CHILD_IS_SCREEN because + -- of the divergence with react-navigation. + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + local childAction = action.action or NavigationActions.init({ + params = getParamsForRouteAndAction(action.routeName, action) + }) + + childState = childRouter.getStateForAction(childAction) + end + + local routes = Cryo.List.replaceIndex( + state.routes, + routeIndex, + Cryo.Dictionary.join({ + params = getParamsForRouteAndAction(action.routeName, action), + }, childState, { + routeName = action.routeName, + key = action.newKey or KeyGenerator.generateKey(), + }) + ) + + return Cryo.Dictionary.join(state, { routes = routes }) + end + end + + if action.type == StackActions.CompleteTransition and + (action.key == nil or action.key == state.key) and + action.toChildKey == state.routes[state.index].key and + state.isTransitioning + then + return Cryo.Dictionary.join(state, { + isTransitioning = false, + }) + end + + if action.type == NavigationActions.SetParams then + local key = action.key + local lastRouteIndex = Cryo.List.findWhere(state.routes, function(route) + return route.key == key + end) + + if lastRouteIndex then + local lastRoute = state.routes[lastRouteIndex] + local params = Cryo.Dictionary.join(lastRoute.params or {}, action.params or {}) + local routes = Cryo.List.replaceIndex( + state.routes, + lastRouteIndex, + Cryo.Dictionary.join(lastRoute, { params = params }) + ) + + return Cryo.Dictionary.join(state, { + routes = routes, + }) + end + end + + if action.type == StackActions.Reset then + -- Only handle reset actions that are unspecified or match this state key + if action.key ~= nil and action.key ~= state.key then + return state + end + + local newStackActions = action.actions or {} + local newRoutes = Cryo.List.map(newStackActions, function(newStackAction) + local router = childRouters[newStackAction.routeName] + + local childState = {} + -- we need to check if initialChildRouter is not CHILD_IS_SCREEN because + -- of the divergence with react-navigation. + if router ~= nil and router ~= CHILD_IS_SCREEN then + local childAction = newStackAction.action or NavigationActions.init({ + params = getParamsForRouteAndAction( + newStackAction.routeName, + newStackAction + ), + }) + + childState = router.getStateForAction(childAction) + end + + return Cryo.Dictionary.join({ + params = getParamsForRouteAndAction(newStackAction.routeName, newStackAction) + }, childState, { + routeName = newStackAction.routeName, + key = newStackAction.key or KeyGenerator.generateKey(), + }) + end) + + return Cryo.Dictionary.join(state, { + routes = newRoutes, + index = action.index, + }) + end + + if action.type == NavigationActions.Back or action.type == StackActions.Pop then + local key = action.key + local n = action.n + local immediate = action.immediate + local prune = action.prune + + if action.type == StackActions.Pop and prune == false and key then + local index = Cryo.List.findWhere(state.routes, function(route) + return route.key == key + end) + + if index ~= nil then + local count = math.max(index - (n or 1), 1) + local routes = Cryo.List.join( + Cryo.List.getRange(state.routes, 1, count), + Cryo.List.getRange(state.routes, index + 1, math.huge) + ) + + if #routes > 0 then + return Cryo.Dictionary.join(state, { + routes = routes, + index = #routes, + isTransitioning = immediate ~= true, + }) + end + end + else + local backRouteIndex = state.index + + if action.type == StackActions.Pop and n ~= nil then + -- determine the index to go back *from*. In this case, n=1 means to go + -- back from state.index, as if it were a normal "BACK" action + backRouteIndex = math.max(2, state.index - n + 1) + elseif key then + backRouteIndex = Cryo.List.findWhere(state.routes, function(route) + return route.key == key + end) + end + + if backRouteIndex and backRouteIndex > 1 then + return Cryo.Dictionary.join(state, { + routes = Cryo.List.getRange(state.routes, 1, backRouteIndex - 1), + index = backRouteIndex - 1, + isTransitioning = immediate ~= true, + }) + end + end + end + + -- By this point in the router's state handling logic, we have handled the behavior + -- of the active route, and handled any stack actions. If we haven't returned by + -- now, we should allow non-active child routers to handle this action, and switch + -- to that index if the child state (route) does change.. + + local keyIndex = action.key and StateUtils.indexOf(state, action.key) or nil + + -- Traverse routes from the top of the stack to the bottom, so the + -- active route has the first opportunity, then the one before it, etc. + for i = #state.routes, 1, -1 do + local childRoute = state.routes[i] + -- skip over the active child because we let it attempt to handle the action + -- earlier. + -- If a key is provided and in routes state then let's use that + -- knowledge to skip extra getStateForAction calls on other child + -- routers + if (childRoute.key ~= activeChildRoute.key) and + (keyIndex == 1 or childRoute.key == action.key) + then + local childRouter = childRouters[childRoute.routeName] + if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then + local route = childRouter.getStateForAction(action, childRoute) + + if route == nil then + return state + elseif route ~= childRoute then + return StateUtils.replaceAt( + state, + childRoute.key, + route, + -- People don't expect these actions to switch the active route + action.preserveFocus + ) + end + end + end + end + + return state + end + + -- TODO: Implement StackRouter.getPathAndParamsForState after we add path expression support + -- TODO: Implement StackRouter.getActionForPathAndParams after we add path expression support + + return StackRouter +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/SwitchActions.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/SwitchActions.lua new file mode 100644 index 0000000..ccb2a15 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/SwitchActions.lua @@ -0,0 +1,26 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol) + +local JUMP_TO_TOKEN = NavigationSymbol("JUMP_TO") + +local SwitchActions = { + JumpTo = JUMP_TO_TOKEN, +} + +-- deviation: we using this metatable to error when SwitchActions is indexed +-- with an unexpected key. +setmetatable(SwitchActions, { + __index = function(self, key) + error(("%q is not a valid member of SwitchActions"):format(tostring(key)), 2) + end, +}) + +-- Pop the top-most item off the route stack, if any. +function SwitchActions.jumpTo(payload) + return Cryo.Dictionary.join( + { type = JUMP_TO_TOKEN, preserveFocus = true }, + payload or {} + ) +end + +return SwitchActions diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/SwitchRouter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/SwitchRouter.lua new file mode 100644 index 0000000..0633a28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/SwitchRouter.lua @@ -0,0 +1,445 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/routers/SwitchRouter.js +local Root = script.Parent.Parent +local Packages = Root.Parent + +local Cryo = require(Packages.Cryo) +local NavigationActions = require(Root.NavigationActions) +local BackBehavior = require(Root.BackBehavior) +local getScreenForRouteName = require(script.Parent.getScreenForRouteName) +local createConfigGetter = require(script.Parent.createConfigGetter) +local validateRouteConfigMap = require(script.Parent.validateRouteConfigMap) +local validateRouteConfigArray = require(script.Parent.validateRouteConfigArray) +local validate = require(Root.utils.validate) +local StackActions = require(Root.routers.StackActions) +local SwitchActions = require(script.Parent.SwitchActions) + +local function defaultActionCreators() + return {} +end + +local function mapToRouteName(element) + local routeName = next(element) + return routeName +end + +local function foldToRoutes(routes, element) + local routeName, value = next(element) + routes[routeName] = value + return routes +end + +return function(routeArray, config) + validateRouteConfigArray(routeArray) + config = config or {} + local routeConfigs = validateRouteConfigMap( + Cryo.List.foldLeft(routeArray, foldToRoutes, {}) + ) + + -- Order is how we map the active index into the list of possible routes. + -- Lua does not guarantee any sense of order of table keys in dictionaries, so + -- we have to deviate from react-navigation SwitchRouter API. Instead of using a + -- map for the routeConfigs, we wrap each key-value pair into its own table so that + -- routeConfigs becomes an array (i.e. { { Foo = Screen }, { Bar = Screen } }). + local order = config.order or Cryo.List.map(routeArray, mapToRouteName) + + local getCustomActionCreators = config.getCustomActionCreators or defaultActionCreators + + local initialRouteParams = config.initialRouteParams + local initialRouteName = config.initialRouteName or order[1] + local backBehavior = config.backBehavior or BackBehavior.None + + local resetOnBlur = true + if config.resetOnBlur ~= nil then + resetOnBlur = config.resetOnBlur + end + + local initialRouteIndex = Cryo.List.find(order, initialRouteName) + if initialRouteIndex == nil then + local availableRouteNames = table.concat( + Cryo.List.map(order, function(routeName) + return ('"%s"'):format(routeName) + end), + ", " + ) + + error(("Invalid initialRouteName '%s'. Should be one of %s"):format( + initialRouteName, + availableRouteNames + ), 2) + end + + local childRouters = {} + for _, routeName in ipairs(order) do + childRouters[routeName] = false + local screen = getScreenForRouteName(routeConfigs, routeName) + if type(screen) == "table" and screen.router then + childRouters[routeName] = screen.router + end + end + + local function getParamsForRoute(routeName, params) + local routeConfig = routeConfigs[routeName] + if type(routeConfig) == "table" and routeConfig.params then + return Cryo.Dictionary.join(routeConfig.params, params or {}) + else + return params + end + end + + local function resetChildRoute(routeName) + local initialParams = routeName == initialRouteName and initialRouteParams or nil + -- note(brentvatne): merging initialRouteParams *on top* of default params + -- on the route seems incorrect but it's consistent with existing behavior + -- in stackrouter + local params = getParamsForRoute(routeName, initialParams) + local childRouter = childRouters[routeName] + if childRouter then + local childAction = NavigationActions.init() + return Cryo.Dictionary.join(childRouter.getStateForAction(childAction), { + key = routeName, + routeName = routeName, + params = params, + }) + end + + return { + key = routeName, + routeName = routeName, + params = params, + } + end + + local function getNextState(action, prevState, possibleNextState) + local nextState = possibleNextState + + if prevState and possibleNextState and + prevState.index ~= possibleNextState.index and + resetOnBlur + then + local prevRouteName = prevState.routes[prevState.index].routeName + local nextRoutes = Cryo.List.join(possibleNextState.routes) + nextRoutes[prevState.index] = resetChildRoute(prevRouteName) + nextState = Cryo.Dictionary.join( + possibleNextState, + { routes = nextRoutes } + ) + end + + if backBehavior ~= BackBehavior.History or + (prevState and nextState and nextState.index == prevState.index ) + then + return nextState + end + + local nextRouteKeyHistory = prevState and prevState.routeKeyHistory or {} + + if action.type == NavigationActions.Navigate then + local keyToAdd = nextState.routes[nextState.index].key + nextRouteKeyHistory = Cryo.List.filter(nextRouteKeyHistory, function(k) + return k ~= keyToAdd + end) + table.insert(nextRouteKeyHistory, keyToAdd) + + elseif action.type == NavigationActions.Back then + nextRouteKeyHistory = Cryo.List.removeIndex(nextRouteKeyHistory, #nextRouteKeyHistory) + end + + return Cryo.Dictionary.join( + nextState, + { routeKeyHistory = nextRouteKeyHistory } + ) + end + + local function getInitialState() + local routes = Cryo.List.map(order, resetChildRoute) + local initialState = { + routes = routes, + index = initialRouteIndex, + } + + if backBehavior == BackBehavior.History then + local initialKey = routes[initialRouteIndex].key + initialState.routeKeyHistory = { initialKey } + end + + return initialState + end + + local SwitchRouter = { + childRouters = childRouters, + getActionCreators = function(route, stateKey) + return getCustomActionCreators(route, stateKey) + end, + getScreenOptions = createConfigGetter(routeConfigs, config.defaultNavigationOptions) + } + + function SwitchRouter.getStateForAction(action, inputState) + local prevState = inputState and Cryo.Dictionary.join(inputState) or nil + local state = inputState or getInitialState() + local activeChildIndex = state.index + + if action.type == NavigationActions.Init then + -- TODO: React-Navigation has a comment that wonders why we merge params into child routes. + -- Need to understand if we really want to do this. + local params = action.params + if params then + state.routes = Cryo.List.map(state.routes, function(route) + local initialParams = route.routeName == initialRouteName and initialRouteParams or {} + return Cryo.Dictionary.join(route, { + params = Cryo.Dictionary.join(route.params or {}, params, initialParams) + }) + end) + end + end + + if action.type == SwitchActions.JumpTo and + (action.key == nil or action.key == state.key) + then + local params = action.params + local index = Cryo.List.findWhere(state.routes, function(route) + return route.routeName == action.routeName + end) + + if index == nil then + error( + ("There is no route named '%s' in the navigator with the key '%s'.\n"):format( + action.routeName, + action.key + ) .. + "Must be one of: " .. table.concat( + Cryo.List.map(state.routes, function(route) + return route.routeName + end), + "," + ) + ) + end + + local routes = state.routes + if params then + return state.routes.map(function(route, i) + if i == index then + return Cryo.Dictionary.join(route, { + params = Cryo.Dictionary.join(route.params, params), + }) + end + + return route + end) + end + + return getNextState(action, prevState, Cryo.Dictionary.join(state, { + routes = routes, + index = index, + })) + end + + -- Let the current child handle it + local activeChildLastState = state.routes[state.index] + local activeChildRouter = childRouters[order[state.index]] + + if activeChildRouter then + local activeChildState = activeChildRouter.getStateForAction( + action, + activeChildLastState + ) + if not activeChildState and inputState then + return nil + end + if activeChildState and activeChildState ~= activeChildLastState then + local routes = Cryo.List.join(state.routes) + routes[state.index] = activeChildState + return getNextState(action, prevState, Cryo.Dictionary.join( + state, + { routes = routes } + )) + end + end + + -- Handle tab changing. Do this after letting the current tab try to + -- handle the action, to allow inner children to change first + local isBackEligible = action.key == nil or action.key == activeChildLastState.key + + if action.type == NavigationActions.Back then + if isBackEligible and backBehavior == BackBehavior.InitialRoute then + activeChildIndex = initialRouteIndex + elseif isBackEligible and backBehavior == BackBehavior.Order then + activeChildIndex = math.max(1, activeChildIndex - 1) + -- The history contains current route, so we can only go back + -- if there is more than one item in the history + elseif isBackEligible and + backBehavior == BackBehavior.History and + #state.routeKeyHistory > 1 + then + local routeKey = state.routeKeyHistory[#state.routeKeyHistory - 1] + activeChildIndex = Cryo.List.find(order, routeKey) + end + end + + local didNavigate = false + + if action.type == NavigationActions.Navigate then + didNavigate = nil ~= Cryo.List.findWhere(order, function(childId, i) + if childId == action.routeName then + activeChildIndex = i + return true + end + return false + end) + + if didNavigate then + local childState = state.routes[activeChildIndex] + local childRouter = childRouters[action.routeName] + local newChildState = childState + + if action.action and childRouter then + local childStateUpdate = childRouter.getStateForAction( + action.action, + childState + ) + if childStateUpdate then + newChildState = childStateUpdate + end + end + + if action.params then + newChildState = Cryo.Dictionary.join(newChildState, { + params = Cryo.Dictionary.join( + newChildState.params or {}, + action.params + ), + }) + end + + if newChildState ~= childState then + local routes = Cryo.List.join(state.routes) + routes[activeChildIndex] = newChildState + local nextState = Cryo.Dictionary.join(state, { + routes = routes, + index = activeChildIndex, + }) + return getNextState(action, prevState, nextState) + + elseif newChildState == childState and + state.index == activeChildIndex and + prevState + then + return nil + end + end + end + + if action.type == NavigationActions.SetParams then + local key = action.key + local lastRouteIndex = Cryo.List.findWhere(state.routes, function(route) + return route.key == key + end) + + if lastRouteIndex ~= nil then + local lastRoute = state.routes[lastRouteIndex] + local params = Cryo.Dictionary.join(lastRoute.params or {}, action.params) + local routes = Cryo.List.join(state.routes) + + routes[lastRouteIndex] = Cryo.Dictionary.join( + lastRoute, + { params = params } + ) + + return getNextState(action, prevState, Cryo.Dictionary.join( + state, + { routes = routes }) + ) + end + end + + if activeChildIndex ~= state.index then + return getNextState(action, prevState, Cryo.Dictionary.join( + state, + { index = activeChildIndex }) + ) + elseif didNavigate and not inputState then + return state + elseif didNavigate then + return Cryo.List.join(state) + end + + + local isActionBackOrPop = action.type == NavigationActions.Back or + action.type == StackActions.Pop or action.type == StackActions.PopToTop + local sendActionToInactiveChildren = not isActionBackOrPop or + (action.type == NavigationActions.Back and action.key ~= nil) + + -- Let other children handle it and switch to the first child that returns a new state + -- Do not do this for StackActions.Pop or NavigationActions.Back actions without a key: + -- it would be unintuitive for these actions to switch to another tab just because that tab had a Stack that could accept a back action + if sendActionToInactiveChildren then + local index = state.index + local routes = state.routes + Cryo.List.findWhere(order, function(childId, i) + local childRouter = childRouters[childId] + if i == index then + return false + end + + local childState = routes[i] + if childRouter then + childState = childRouter.getStateForAction(action, childState) + end + + if not childState then + index = i + return true + end + + if childState ~= routes[i] then + routes = Cryo.List.join(routes) + routes[i] = childState + index = i + return true + end + + return false + end) + + -- Nested routers can be updated after switching children with actions such as SetParams + -- and CompleteTransition. + if action.preserveFocus then + index = state.index + end + + if index ~= state.index or routes ~= state.routes then + return getNextState(action, prevState, Cryo.Dictionary.join(state, { + index = index, + routes = routes, + })) + end + end + + return state + end + + function SwitchRouter.getComponentForState(state) + local activeRoute = state.routes[state.index] or {} + local routeName = activeRoute.routeName + validate(routeName, "There is no route defined for index '%d'. " .. + "Check that you passed in a navigation state with a " .. + "valid tab/screen index.", state.index) + + local childRouter = childRouters[routeName] + + if childRouter then + return childRouter.getComponentForState(state.routes[state.index]) + end + + return getScreenForRouteName(routeConfigs, routeName) + end + + function SwitchRouter.getComponentForRouteName(routeName) + return getScreenForRouteName(routeConfigs, routeName) + end + + -- TODO: Implement SwitchRouter.getPathAndParamsForState after we add path expression support + -- TODO: Implement SwitchRouter.getActionForPathAndParams after we add path expression support + + return SwitchRouter +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/TabRouter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/TabRouter.lua new file mode 100644 index 0000000..99fac03 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/TabRouter.lua @@ -0,0 +1,18 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/routers/TabRouter.js +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local SwitchRouter = require(script.Parent.SwitchRouter) +local BackBehavior = require(script.Parent.Parent.BackBehavior) + +return function(routeArray, config) + -- Provide defaults suitable for tab routing. + local switchConfig = { + resetOnBlur = false, + backBehavior = BackBehavior.InitialRoute, + } + + if config then + switchConfig = Cryo.Dictionary.join(switchConfig, config) + end + + return SwitchRouter(routeArray, switchConfig) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/Routers.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/Routers.spec.lua new file mode 100644 index 0000000..b38f547 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/Routers.spec.lua @@ -0,0 +1,580 @@ +-- upstream: https://github.com/react-navigation/react-navigation/blob/a57e47786c5654a3803582b2c4953547164f26a0/packages/core/src/routers/__tests__/Routers.test.js + +return function() + local Root = script.Parent.Parent.Parent + local Packages = Root.Parent + + local Cryo = require(Packages.Cryo) + local Roact = require(Packages.Roact) + local StackRouter = require(script.Parent.Parent.StackRouter) + local TabRouter = require(script.Parent.Parent.TabRouter) + local SwitchRouter = require(script.Parent.Parent.SwitchRouter) + local NavigationActions = require(Root.NavigationActions) + local StackActions = require(Root.routers.StackActions) + local KeyGenerator = require(Root.utils.KeyGenerator) + local expectDeepEqual = require(Root.utils.expectDeepEqual) + + beforeEach(function() + KeyGenerator._TESTING_ONLY_normalize_keys() + end) + + local ROUTERS = { + TabRouter = TabRouter, + StackRouter = StackRouter, + SwitchRouter = SwitchRouter, + } + + local function dummyEventSubscriber() + return { remove = function() end } + end + + for routerName in pairs(ROUTERS) do + local Router = ROUTERS[routerName] + + describe(("General router features - %s"):format(routerName), function() + it(("title is configurable using navigationOptions and getScreenOptions - %s"):format(routerName), function() + local FooView = Roact.Component:extend("FooView") + function FooView:render() + return Roact.createElement("Frame") + end + + local BarView = Roact.Component:extend("BarView") + function BarView:render() + return Roact.createElement("Frame") + end + BarView.navigationOptions = { + title = "BarTitle", + } + + local BazView = Roact.Component:extend("BazView") + function BazView:render() + return Roact.createElement("Frame") + end + BazView.navigationOptions = function(options) + local navigation = options.navigation + + return { title = ("Baz-%s"):format(navigation.state.params.id) } + end + + local router = Router({ + { Foo = { screen = FooView } }, + { Bar = { screen = BarView } }, + { Baz = { screen = BazView } }, + }) + local routes = { + { key = "A", routeName = "Foo" }, + { key = "B", routeName = "Bar" }, + { key = "A", routeName = "Baz", params = { id = "123" } }, + } + + expect( + router.getScreenOptions( + { + state = routes[1], + dispatch = function() return false end, + addListener = dummyEventSubscriber, + }, + {} + ).title + ).to.equal(nil) + expect( + router.getScreenOptions( + { + state = routes[2], + dispatch = function() return false end, + addListener = dummyEventSubscriber, + }, + {} + ).title + ).to.equal("BarTitle") + expect( + router.getScreenOptions( + { + state = routes[3], + dispatch = function() return false end, + addListener = dummyEventSubscriber, + }, + {} + ).title + ).to.equal("Baz-123") + end) + + it(("set params works in %s"):format(routerName), function() + local FooView = Roact.Component:extend("FooView") + + function FooView:render() + return Roact.createElement("Frame") + end + + local router = Router({ + { Foo = { screen = FooView } }, + { Bar = { screen = FooView } }, + }) + local initState = router.getStateForAction(NavigationActions.init()) + local initRoute = initState.routes[initState.index] + + expect(initRoute.params).to.equal(nil) + + local state0 = router.getStateForAction( + NavigationActions.setParams({ params = {foo = 42}, key = initRoute.key }), + initState + ) + + expect(state0.routes[state0.index].params.foo).to.equal(42) + end) + + it("merges existing params when set params on existing state", function() + local function Screen() + return Roact.createElement("Frame") + end + local router = Router({ + { Foo = { screen = Screen, params = { a = 1 } } }, + }) + local key = "Foo" + local state = router.getStateForAction({ + type = NavigationActions.Init, + key = key, + }) + + expect(state.index).to.equal(1) + expect(state.routes[1].key).to.equal(key) + expectDeepEqual(state.routes[1].params, { a = 1 }) + + local newState = router.getStateForAction( + NavigationActions.setParams({ key = key, params = { b = 2 } }), + state + ) + + expectDeepEqual(newState.routes[newState.index].params, { a = 1, b = 2 }) + end) + + it("merges params when setting params during init", function() + local function Screen() + return Roact.createElement("Frame") + end + local router = Router({ + { Foo = { screen = Screen, params = { a = 1 } } }, + }) + local newState = router.getStateForAction( + NavigationActions.setParams({ key = "Foo", params = { b = 2 } }) + ) + + expectDeepEqual( + newState.routes[newState.index].params, + { a = 1, b = 2 } + ) + end) + end) + end + + it("Nested navigate behavior test", function() + local function Leaf() + return Roact.createElement("Frame") + end + local First = Roact.Component:extend("First") + function First:render() + return Roact.createElement("Frame") + end + + First.router = StackRouter({ + { First1 = Leaf }, + { First2 = Leaf }, + }) + + local Second = Roact.Component:extend("Second") + function Second:render() + return Roact.createElement("Frame") + end + + Second.router = StackRouter({ + { Second1 = Leaf }, + { Second2 = Leaf }, + }) + + local Main = Roact.Component:extend("Main") + function Main:render() + return Roact.createElement("Frame") + end + + Main.router = StackRouter({ + { First = First }, + { Second = Second }, + }) + + local TestRouter = SwitchRouter({ + { Login = Leaf }, + { Main = Main }, + }) + local state1 = TestRouter.getStateForAction({ type = NavigationActions.Init }) + local state2 = TestRouter.getStateForAction( + { type = NavigationActions.Navigate, routeName = "First" }, + state1 + ) + + expect(state2.index).to.equal(2) + expect(state2.routes[2].index).to.equal(1) + expect(state2.routes[2].routes[1].index).to.equal(1) + + local state3 = TestRouter.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Second2" }, + state2 + ) + + expect(state3.index).to.equal(2) + expect(state3.routes[2].index).to.equal(2) -- second + expect(state3.routes[2].routes[2].index).to.equal(2) -- second.second2 + + local state4 = TestRouter.getStateForAction( + { + type = NavigationActions.Navigate, + routeName = "First", + action = { type = NavigationActions.Navigate, routeName = "First2" }, + }, + state3, + true + ) + + expect(state4.index).to.equal(2) -- main + expect(state4.routes[2].index).to.equal(1) -- first + expect(state4.routes[2].routes[1].index).to.equal(2) -- first2 + + local state5 = TestRouter.getStateForAction( + { + type = NavigationActions.Navigate, + routeName = "First", + action = { type = NavigationActions.Navigate, routeName = "First1" }, + }, + state3 -- second.second2 is active on state3 + ) + + expect(state5.index).to.equal(2) -- main + expect(state5.routes[2].index).to.equal(1) -- first + expect(state5.routes[2].routes[1].index).to.equal(1) -- first.first1 + end) + + it("Handles no-op actions with tabs within stack router", function() + local function BarView() + return Roact.createElement("Frame") + end + local FooTabNavigator = Roact.Component:extend("FooTabNavigator") + + function FooTabNavigator:render() + return Roact.createElement("Frame") + end + + FooTabNavigator.router = TabRouter({ + { Zap = { screen = BarView } }, + { Zoo = { screen = BarView } }, + }) + + local TestRouter = StackRouter({ + { Foo = {screen = FooTabNavigator } }, + { Bar = {screen = BarView } }, + }) + local state1 = TestRouter.getStateForAction({ type = NavigationActions.Init }) + local state2 = TestRouter.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Qux"} + ) + + expect(state1.routes[1].key).to.equal("id-0") + expect(state2.routes[1].key).to.equal("id-1") + + state1.routes[1].key = state2.routes[1].key + + expectDeepEqual(state1, state2) + + local state3 = TestRouter.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Zap" }, + state2 + ) + + expect(state2, state3) + end) + + it("Handles deep action", function() + local function BarView() + return Roact.createElement("Frame") + end + + local FooTabNavigator = Roact.Component:extend("FooTabNavigator") + function FooTabNavigator:render() + return Roact.createElement("Frame") + end + + FooTabNavigator.router = TabRouter({ + { Zap = {screen = BarView } }, + { Zoo = {screen = BarView } }, + }) + + local TestRouter = StackRouter({ + { Bar = {screen = BarView} }, + { Foo = {screen = FooTabNavigator} }, + }) + local state1 = TestRouter.getStateForAction({ type = NavigationActions.Init }) + local expectedState = { + index = 1, + isTransitioning = false, + key = "StackRouterRoot", + routes = { + { key = "id-0", routeName = "Bar" }, + }, + } + + expectDeepEqual(state1, expectedState) + + local state2 = TestRouter.getStateForAction( + { + type = NavigationActions.Navigate, + routeName = "Foo", + immediate = true, + action = { type = NavigationActions.Navigate, routeName = "Zoo" }, + }, + state1 + ) + + expect(state2 and state2.index).to.equal(2) + expect(state2 and state2.routes[2].index).to.equal(2) + end) + + it("Handles the navigate action with params", function() + local FooTabNavigator = Roact.Component:extend("FooTabNavigator") + function FooTabNavigator:render() + return Roact.createElement("Frame") + end + + FooTabNavigator.router = TabRouter({ + { Baz = { screen = function() return Roact.createElement("Frame") end } }, + { Boo = { screen = function() return Roact.createElement("Frame") end } }, + }) + + local TestRouter = StackRouter({ + { Foo = { screen = function() return Roact.createElement("Frame") end } }, + { Bar = { screen = FooTabNavigator } }, + }) + local state = TestRouter.getStateForAction({ type = NavigationActions.Init }) + local state2 = TestRouter.getStateForAction( + { + type = NavigationActions.Navigate, + immediate = true, + routeName = "Bar", + params = { foo = "42" }, + }, + state + ) + + expectDeepEqual(state2 and state2.routes[2].params, { foo = "42" }) + expectDeepEqual(state2 and state2.routes[2].routes, { + { + key = "Baz", + routeName = "Baz", + params = { foo = "42" }, + }, + { + key = "Boo", + routeName = "Boo", + params = { foo = "42" }, + }, + }) + end) + + it("Handles the setParams action", function() + local FooTabNavigator = Roact.Component:extend("FooTabNavigator") + function FooTabNavigator:render() + return Roact.createElement("Frame") + end + + FooTabNavigator.router = TabRouter({ + { Baz = { screen = function() return Roact.createElement("Frame") end } }, + }) + + local TestRouter = StackRouter({ + { Foo = { screen = FooTabNavigator } }, + { Bar = { screen = function() return Roact.createElement("Frame") end } }, + }) + local state = TestRouter.getStateForAction({ type = NavigationActions.Init }) + local state2 = TestRouter.getStateForAction( + { + type = NavigationActions.SetParams, + params = { name = "foobar" }, + key = "Baz", + }, + state + ) + + expect(state2 and state2.index).to.equal(1) + expectDeepEqual(state2 and state2.routes[1].routes, { + { + key = "Baz", + routeName = "Baz", + params = { name = "foobar" }, + }, + }) + end) + + it("Supports lazily-evaluated getScreen", function() + local function BarView() + return Roact.createElement("Frame") + end + local FooTabNavigator = Roact.Component:extend("FooTabNavigator") + function FooTabNavigator:render() + return Roact.createElement("Frame") + end + + FooTabNavigator.router = TabRouter({ + { Zap = { screen = BarView } }, + { Zoo = { screen = BarView } }, + }) + + local TestRouter = StackRouter({ + { Foo = { screen = FooTabNavigator } }, + { Bar = { getScreen = function() return BarView end } }, + }) + local state1 = TestRouter.getStateForAction({ type = NavigationActions.Init }) + local state2 = TestRouter.getStateForAction( + { type = NavigationActions.Navigate, immediate = true, routeName = "Qux" } + ) + + expect(state1.routes[1].key).to.equal("id-0") + expect(state2.routes[1].key).to.equal("id-1") + + state1.routes[1].key = state2.routes[1].key + + expectDeepEqual(state1, state2) + + local state3 = TestRouter.getStateForAction( + { type = NavigationActions.Navigate, immediate = true, routeName = "Zap" }, + state2 + ) + + expectDeepEqual(state2, state3) + end) + + it("Does not switch tab index when TabRouter child handles COMPLETE_NAVIGATION or SET_PARAMS", function() + local FooStackNavigator = Roact.Component:extend("FooStackNavigator") + function FooStackNavigator:render() + return Roact.createElement("Frame") + end + local function BarView() + return Roact.createElement("Frame") + end + + FooStackNavigator.router = StackRouter({ + { Foo = { screen = BarView } }, + { Bar = { screen = BarView } }, + }) + + local TestRouter = TabRouter({ + { Zap = { screen = FooStackNavigator } }, + { Zoo = { screen = FooStackNavigator } }, + }) + + local state1 = TestRouter.getStateForAction({ type = NavigationActions.Init }) + + -- Navigate to the second screen in the first tab + local state2 = TestRouter.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Bar" }, + state1 + ) + + -- Switch tabs + local state3 = TestRouter.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Zoo" }, + state2 + ) + + local stateAfterCompleteTransition = TestRouter.getStateForAction( + { + type = StackActions.CompleteTransition, + preserveFocus = true, + key = state2.routes[1].key, + }, + state3 + ) + local stateAfterSetParams = TestRouter.getStateForAction( + { + type = NavigationActions.SetParams, + preserveFocus = true, + key = state1.routes[1].routes[1].key, + params = { key = "value" }, + }, + state3 + ) + + expect(stateAfterCompleteTransition.index).to.equal(2) + expect(stateAfterSetParams.index).to.equal(2) + end) + + it("Inner actions are only unpacked if the current tab matches", function() + local function PlainScreen() + return Roact.createElement("Frame") + end + local ScreenA = Roact.Component:extend("ScreenA") + function ScreenA:render() + return Roact.createElement("Frame") + end + local ScreenB = Roact.Component:extend("ScreenB") + function ScreenB:render() + return Roact.createElement("Frame") + end + + ScreenB.router = StackRouter({ + { Baz = { screen = PlainScreen } }, + { Zoo = { screen = PlainScreen } }, + }) + ScreenA.router = StackRouter({ + { Bar = { screen = PlainScreen } }, + { Boo = { screen = ScreenB } }, + }) + + local TestRouter = TabRouter({ + { Foo = { screen = ScreenA } }, + }) + local screenApreState = { + index = 1, + key = "Init", + isTransitioning = false, + routeName = "Foo", + routes = { + { key = "Init", routeName = "Bar" }, + }, + } + local preState = { + index = 1, + isTransitioning = false, + routes = { screenApreState }, + } + + local function comparable(state) + local result = {} + + if typeof(state.routeName) == "string" then + result = Cryo.Dictionary.join(result, { + routeName = state.routeName + }) + end + + if typeof(state.routes) == "table" then + result = Cryo.Dictionary.join(result, { + routes = Cryo.List.map(state.routes, comparable) + }) + end + + return result + end + + local action = NavigationActions.navigate({ + routeName = "Boo", + action = NavigationActions.navigate({ routeName = "Zoo" }), + }) + + local expectedState = ScreenA.router.getStateForAction(action, screenApreState) + local state = TestRouter.getStateForAction(action, preState) + + local innerState = state and state.routes[1] or state + + expectDeepEqual( + expectedState and comparable(expectedState), + innerState and comparable(innerState) + ) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/StackActions.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/StackActions.roblox.spec.lua new file mode 100644 index 0000000..db7e89f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/StackActions.roblox.spec.lua @@ -0,0 +1,99 @@ +return function() + local StackActions = require(script.Parent.Parent.StackActions) + + it("throws when indexing an unknown field", function() + expect(function() + return StackActions.foo + end).to.throw("\"foo\" is not a valid member of StackActions") + end) + + describe("StackActions token tests", function() + it("should return same object for each token for multiple calls", function() + expect(StackActions.Pop).to.equal(StackActions.Pop) + expect(StackActions.PopToTop).to.equal(StackActions.PopToTop) + expect(StackActions.Push).to.equal(StackActions.Push) + expect(StackActions.Reset).to.equal(StackActions.Reset) + expect(StackActions.Replace).to.equal(StackActions.Replace) + end) + + it("should return matching string names for symbols", function() + expect(tostring(StackActions.Pop)).to.equal("POP") + expect(tostring(StackActions.PopToTop)).to.equal("POP_TO_TOP") + expect(tostring(StackActions.Push)).to.equal("PUSH") + expect(tostring(StackActions.Reset)).to.equal("RESET") + expect(tostring(StackActions.Replace)).to.equal("REPLACE") + end) + end) + + describe("StackActions function tests", function() + it("should return a pop action for pop()", function() + local popTable = StackActions.pop({ + n = "n", + }) + + expect(popTable.type).to.equal(StackActions.Pop) + expect(popTable.n).to.equal("n") + end) + + it("should return a pop to top action for popToTop()", function() + local popToTopTable = StackActions.popToTop() + + expect(popToTopTable.type).to.equal(StackActions.PopToTop) + end) + + it("should return a push action for push()", function() + local pushTable = StackActions.push({ + routeName = "routeName", + params = "params", + action = "action", + }) + + expect(pushTable.type).to.equal(StackActions.Push) + expect(pushTable.routeName).to.equal("routeName") + expect(pushTable.params).to.equal("params") + expect(pushTable.action).to.equal("action") + end) + + it("should return a reset action for reset()", function() + local resetTable = StackActions.reset({ + index = "index", + actions = "actions", + key = "key", + }) + + expect(resetTable.type).to.equal(StackActions.Reset) + expect(resetTable.index).to.equal("index") + expect(resetTable.key).to.equal("key") + end) + + it("should return a replace action for replace()", function() + local replaceTable = StackActions.replace({ + key = "key", + newKey = "newKey", + routeName = "routeName", + params = "params", + action = "action", + immediate = "immediate", + }) + + expect(replaceTable.type).to.equal(StackActions.Replace) + expect(replaceTable.key).to.equal("key") + expect(replaceTable.newKey).to.equal("newKey") + expect(replaceTable.routeName).to.equal("routeName") + expect(replaceTable.params).to.equal("params") + expect(replaceTable.action).to.equal("action") + expect(replaceTable.immediate).to.equal("immediate") + end) + + it("should return a complete transition action with matching data for call to completeTransition()", function() + local completeTransitionTable = StackActions.completeTransition({ + key = "key", + toChildKey = "toChildKey", + }) + + expect(completeTransitionTable.type).to.equal(StackActions.CompleteTransition) + expect(completeTransitionTable.key).to.equal("key") + expect(completeTransitionTable.toChildKey).to.equal("toChildKey") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/StackRouter.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/StackRouter.roblox.spec.lua new file mode 100644 index 0000000..6637f91 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/StackRouter.roblox.spec.lua @@ -0,0 +1,1211 @@ +return function() + local Root = script.Parent.Parent.Parent + local StackRouter = require(script.Parent.Parent.StackRouter) + local NavigationActions = require(Root.NavigationActions) + local StackActions = require(script.Parent.Parent.StackActions) + + local function expectError(functor, msg) + local status, err = pcall(functor) + + if status ~= false then + error("expectError: Test function should have thrown error, but it passed", 2) + end + if string.find(err, msg) == nil then + error(string.format("expectError: Expected error message '%s' not found in actual message: '%s'", msg, err), 2) + end + end + + it("should be a function", function() + expect(type(StackRouter)).to.equal("function") + end) + + it("should throw when passed a non-table", function() + expect(function() + StackRouter(5) + end).to.throw("routeConfigs must be an array table") + end) + + it("should throw if initialRouteName is not found in routes table", function() + expectError(function() + StackRouter({ + { Foo = function() end }, + { Bar = function() end }, + }, { + initialRouteName = "MyRoute", + }) + end, "Invalid initialRouteName 'MyRoute'. Must be one of %[Foo,Bar,%]") + end) + + it("should expose childRouters as a member", function() + local router = StackRouter({ + { + Foo = { + screen = { + render = function() end, + router = "A", + }, + }, + }, + { + Bar = { + screen = { + render = function() end, + router = "B", + }, + }, + }, + }) + + expect(router.childRouters.Foo).to.equal("A") + expect(router.childRouters.Bar).to.equal("B") + end) + + it("should not expose childRouters list members if they are CHILD_IS_SCREEN", function() + local router = StackRouter({ + { + Foo = { + screen = { + render = function() end, + router = "A", + }, + }, + }, + { + Bar = { + screen = { + render = function() end, + }, + }, + }, + }) + + expect(router.childRouters.Foo).to.equal("A") + + expect(router._CHILD_IS_SCREEN).to.never.equal(nil) + for _, childRouter in pairs(router.childRouters) do + expect(childRouter).to.never.equal(router._CHILD_IS_SCREEN) + end + end) + + describe("getScreenOptions tests", function() + it("should correctly configure default screen options", function() + local router = StackRouter({ + { + Foo = { + screen = { + render = function() end, + } + } + }, + }, { + defaultNavigationOptions = { + title = "FooTitle", + } + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("FooTitle") + end) + + it("should correctly configure route-specified screen options", function() + local router = StackRouter({ + { + Foo = { + screen = { + render = function() end, + }, + navigationOptions = { title = "RouteFooTitle" }, + } + }, + }, { + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("RouteFooTitle") + end) + + it("should correctly configure component-specified screen options", function() + local router = StackRouter({ + { + Foo = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentFooTitle" }, + }, + } + }, + }, { + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("ComponentFooTitle") + end) + end) + + describe("getActionCreators tests", function() + it("should return basic action creators table if none are provided", function() + local router = StackRouter({ + { + Foo = { render = function() end }, + }, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + + local fieldCount = 0 + for _ in pairs(actionCreators) do + fieldCount = fieldCount + 1 + end + + expect(fieldCount).to.equal(6) + expect(type(actionCreators.pop)).to.equal("function") + expect(type(actionCreators.popToTop)).to.equal("function") + expect(type(actionCreators.push)).to.equal("function") + expect(type(actionCreators.replace)).to.equal("function") + expect(type(actionCreators.reset)).to.equal("function") + expect(type(actionCreators.dismiss)).to.equal("function") + end) + + it("should call custom action creators function if provided", function() + local router = StackRouter({ + { + Foo = { render = function() end }, + }, + }, { + getCustomActionCreators = function() + return { a = 1, popToTop = 2 } + end, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.a).to.equal(1) + + -- make sure that we merged the default ones on top! + expect(type(actionCreators.pop)).to.equal("function") + expect(type(actionCreators.popToTop)).to.equal("function") + end) + + it("should build a pop action", function() + local router = StackRouter({ + { Foo = function() end }, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.pop(1).type).to.equal(StackActions.Pop) + end) + + it("should build a pop to top action", function() + local router = StackRouter({ + { Foo = function() end }, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.popToTop().type).to.equal(StackActions.PopToTop) + end) + + it("should build a push action", function() + local router = StackRouter({ + { Foo = function() end }, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.push("Foo").type).to.equal(StackActions.Push) + end) + + it("should build a replace action with a string replaceWith arg", function() + local router = StackRouter({ + { Foo = function() end }, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo", key = "Foo" }, "key") + expect(actionCreators.replace("Foo").type).to.equal(StackActions.Replace) + end) + + it("should build a replace action with a table replaceWith arg", function() + local router = StackRouter({ + { Foo = function() end }, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.replace({ routeName = "Foo" }).type).to.equal(StackActions.Replace) + end) + + it("should build a reset action", function() + local router = StackRouter({ + { Foo = function() end }, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.reset({ + actions = { NavigationActions.navigate({ routeName = "Foo" }) }, + }).type).to.equal(StackActions.Reset) + end) + + it("should build a dismiss action", function() + local router = StackRouter({ + { Foo = function() end }, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.dismiss().type).to.equal(NavigationActions.Back) + end) + end) + + describe("getComponentForState tests", function() + it("should throw if there is no route matching active index", function() + local router = StackRouter({ + { Foo = { screen = function() end } }, + }) + + local message = "There is no route defined for index '2'. " .. + "Make sure that you passed in a navigation state with a " .. + "valid stack index." + expect(function() + router.getComponentForState({ + routes = { + Foo = { screen = function() end }, + }, + index = 2, + }) + end).to.throw(message) + end) + + it("should descend child router for requested route", function() + local testComponent = function() end + local childRouter = StackRouter({ + { Bar = { screen = testComponent } }, + }) + + local router = StackRouter({ + { + Foo = { + screen = { + render = function() end, + router = childRouter, + } + }, + }, + }) + + local component = router.getComponentForState({ + routes = { + { + routeName = "Foo", + routes = { -- Child router's routes + { routeName = "Bar" }, + }, + index = 1 + }, + }, + index = 1, + }) + expect(component).to.equal(testComponent) + end) + end) + + describe("getComponentForRouteName tests", function() + it("should return a component that matches the given route name from accessed childRouter", function() + local testComponent = function() end + local childRouter = StackRouter({ + { Bar = testComponent }, + }) + + local router = StackRouter({ + { + Foo = { + render = function() end, + router = childRouter, + }, + }, + }) + + local component = router.childRouters.Foo.getComponentForRouteName("Bar") + expect(component).to.equal(testComponent) + end) + end) + + describe("getStateForAction tests", function() + it("should return initial state for init action", function() + local router = StackRouter({ + { Foo = { screen = function() end } }, + { Bar = { screen = function() end } }, + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(#state.routes).to.equal(1) + expect(state.routes[state.index].routeName).to.equal("Foo") + expect(state.isTransitioning).to.equal(false) + end) + + it("should adjust initial state index to match initialRouteName's index", function() + local router = StackRouter({ + { Foo = { screen = function() end } }, + { Bar = { screen = function() end } }, + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(state.routes[state.index].routeName).to.equal("Foo") + + local router2 = StackRouter({ + { Foo = { screen = function() end } }, + { Bar = { screen = function() end } }, + }, { + initialRouteName = "Bar", + }) + + local state2 = router2.getStateForAction(NavigationActions.init(), nil) + expect(state2.routes[state2.index].routeName).to.equal("Bar") + end) + + it("should incorporate child router state", function() + local childRouter = StackRouter({ + { Bar = { screen = function() end } }, + }) + + local router = StackRouter({ + { + Foo = { + render = function() end, + router = childRouter, + }, + }, + { City = { screen = function() end } }, + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + local activeState = state.routes[state.index] + expect(activeState.routeName).to.equal("Foo") -- parent's tracking uses parent's route name + expect(activeState.routes[activeState.index].routeName).to.equal("Bar") + end) + + it("should make historical inactive child router active if it handles action", function() + local childRouter = StackRouter({ + { City = function() end }, + { State = function() end }, + }) + + local router = StackRouter({ + { Foo = function() end }, + { + Bar = { + render = function() end, + router = childRouter, + } + }, + }) + + local initialState = { + routes = { + { routeName = "Foo", key = "Foo1" }, + { + routeName = "Bar", + key = "Bar", + routes = { + { routeName = "City", key = "City", }, + }, + index = 1 + }, + { routeName = "Foo", key = "Foo2" }, + }, + index = 3, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "State" }), initialState) + expect(resultState.routes[2].index).to.equal(2) + expect(resultState.routes[2].routes[2].routeName).to.equal("State") + expect(#resultState.routes[2].routes).to.equal(2) + expect(resultState.index).to.equal(2) + end) + + it("should go back to previous stack entry on back action", function() + local router = StackRouter({ + { Foo = function() end }, + { Bar = function() end }, + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo", + }, + { + routeName = "Bar", + key = "Bar", + } + }, + index = 2, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Foo") + expect(#resultState.routes).to.equal(1) -- it should delete top entry! + end) + + it("should not go back if at root of stack", function() + local router = StackRouter({ + { Foo = function() end }, + { Bar = function() end }, + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo", + }, + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(resultState).to.equal(initialState) + end) + + it("should go back out of child stack if on root of child", function() + local childRouter = StackRouter({ + { Bar = { screen = function() end } }, + { City = { screen = function() end } }, + }) + + local router = StackRouter({ + { + Foo = { + render = function() end, + router = childRouter, + }, + }, + { Cat = function() end }, + }, { + initialRouteName = "Cat", + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Cat", + key = "Cat", + }, + { + routeName = "Foo", + key = "Foo", + routes = { + { + routeName = "Bar", + key = "Bar", + } + }, + index = 1, + } + }, + index = 2, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(resultState.index).to.equal(1) + expect(#resultState.routes).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Cat") + end) + + it("should go back within active child if not on root of child", function() + local childRouter = StackRouter({ + { Bar = { screen = function() end } }, + { City = { screen = function() end } }, + }) + + local router = StackRouter({ + { + Foo = { + render = function() end, + router = childRouter, + }, + }, + { Cat = function() end }, + }, { + initialRouteName = "Cat", + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Cat", + key = "Cat", + }, + { + routeName = "Foo", + key = "Foo", + routes = { + { + routeName = "Bar", + key = "Bar", + }, + { + routeName = "City", + key = "City", + }, + }, + index = 2, + } + }, + index = 2, + } + + local resultState = router.getStateForAction(NavigationActions.back(), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[1].routeName).to.equal("Cat") + expect(resultState.routes[2].routeName).to.equal("Foo") + + expect(#resultState.routes[2].routes).to.equal(1) + expect(resultState.routes[2].index).to.equal(1) + expect(resultState.routes[2].routes[1].routeName).to.equal("Bar") + end) + + it("should pop to top", function() + local router = StackRouter({ + { Foo = function() end }, + { Bar = function() end }, + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo", + }, + { + routeName = "Bar", + key = "Bar1", + }, + { + routeName = "Bar", + key = "Bar2", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(StackActions.popToTop(), initialState) + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Foo") + end) + + it("should pop to top through child router", function() + local childRouter = StackRouter({ + { Bar = function() end }, + { City = function() end }, + }) + + local router = StackRouter({ + { + Foo = { + screen = function() end, + router = childRouter, + }, + }, + { Crazy = function() end }, + }, { + initialRouteName = "Crazy", + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Crazy", + key = "Crazy", + }, + { + routeName = "Foo", + key = "Foo", + routes = { + { + routeName = "Bar", + key = "Bar", + }, + { + routeName = "City", + key = "City", + }, + }, + index = 2, + } + }, + index = 2, + } + + local resultState = router.getStateForAction(StackActions.popToTop(), initialState) + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Crazy") + end) + + it("should push a new entry on navigate without instance of that screen", function() + local router = StackRouter({ + { Foo = function() end }, + { Bar = function() end }, + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo", + } + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + end) + + it("should jump to existing entry in stack if one exists already, on navigate", function() + local router = StackRouter({ + { Foo = function() end }, + { Bar = function() end }, + { City = function() end }, + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo", + }, + { + routeName = "Bar", + key = "Bar", + }, + { + routeName = "City", + key = "City", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + params = { a = 1 }, + }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + expect(resultState.routes[2].params.a).to.equal(1) + end) + + it("should jump to existing entry in stack if one exists already, on navigate, with empty params", function() + local router = StackRouter({ + { Foo = function() end }, + { Bar = function() end }, + { City = function() end }, + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo", + }, + { + routeName = "Bar", + key = "Bar", + }, + { + routeName = "City", + key = "City", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + expect(resultState.routes[2].params).to.equal(nil) + end) + + it("should jump to existing entry in stack with existing params if params is not provided, on navigate", function() + local router = StackRouter({ + { Foo = function() end }, + { Bar = function() end }, + { City = function() end }, + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo", + }, + { + routeName = "Bar", + key = "Bar", + params = { a = 1 }, + }, + { + routeName = "City", + key = "City", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + expect(resultState.routes[2].params.a).to.equal(1) + end) + + it("should jump to existing entry in stack with updated params if params is provided, on navigate", function() + local router = StackRouter({ + { Foo = function() end }, + { Bar = function() end }, + { City = function() end }, + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo", + }, + { + routeName = "Bar", + key = "Bar", + params = { a = 1 }, + }, + { + routeName = "City", + key = "City", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + params = { a = 2 }, + }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + expect(resultState.routes[2].params.a).to.equal(2) + end) + + it("should stay at current route in stack if navigate with different params", function() + local router = StackRouter({ + { Foo = function() end }, + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo", + }, + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Foo", + params = { a = 1 }, + }), initialState) + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Foo") + expect(resultState.routes[1].params.a).to.equal(1) + end) + + it("should stay at current route with existing params if navigate with empty params", function() + local router = StackRouter({ + { Foo = function() end }, + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo", + params = { a = 1 }, + }, + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Foo", + params = {}, + }), initialState) + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Foo") + expect(resultState.routes[1].params.a).to.equal(1) + end) + + it("should always push new entry on push action even with pre-existing instance of that screen", function() + local router = StackRouter({ + { Foo = function() end }, + { Bar = function() end }, + { City = function() end }, + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo", + }, + { + routeName = "Bar", + key = "Bar", + }, + { + routeName = "City", + key = "City", + }, + }, + index = 3, + } + + local resultState = router.getStateForAction(StackActions.push({ routeName = "Foo" }), initialState) + expect(#resultState.routes).to.equal(4) + expect(resultState.index).to.equal(4) + expect(resultState.routes[4].routeName).to.equal("Foo") + end) + + it("should navigate to inactive child if route not present elsewhere", function() + local childRouter = StackRouter({ + { Bar = { screen = function() end } }, + { City = { screen = function() end } }, + }) + + local router = StackRouter({ + { + Foo = { + render = function() end, + router = childRouter, + }, + }, + { Cat = function() end }, + }, { + initialRouteName = "Cat", + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Cat", + key = "Cat", + }, + }, + index = 1, + } + + local resultState = router.getStateForAction(NavigationActions.navigate({ routeName = "City" }), initialState) + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Foo") + + expect(#resultState.routes[2].routes).to.equal(2) + expect(resultState.routes[2].index).to.equal(2) + expect(resultState.routes[2].routes[2].routeName).to.equal("City") + end) + + it("should set params on route for setParams action", function() + local router = StackRouter({ + { Foo = { render = function() end } }, + { Bar = { render = function() end } }, + }, { + initialRouteKey = "FooKey", + }) + + local newState = router.getStateForAction(NavigationActions.setParams({ + key = "FooKey", + params = { a = 1 }, + })) + + expect(newState.routes[newState.index].params.a).to.equal(1) + end) + + it("should combine params from action and route config", function() + local router = StackRouter({ + { Foo = { render = function() end } }, + { + Bar = { + screen = function() end, + params = { a = 1 }, + }, + }, + }) + + local state = router.getStateForAction(NavigationActions.init()) + local newState = router.getStateForAction( + NavigationActions.navigate({ routeName = "Bar", params = { b = 2 } }), + state + ) + + expect(newState.routes[2].params.a).to.equal(1) + expect(newState.routes[2].params.b).to.equal(2) + end) + + it("should replace top route if no key is provided", function() + local router = StackRouter({ + { Foo = function() end }, + { Bar = function() end }, + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo", + } + }, + index = 1, + } + + local newState = router.getStateForAction(StackActions.replace({ + routeName = "Bar", + }), initialState) + + expect(#newState.routes).to.equal(1) + expect(newState.index).to.equal(1) + expect(newState.routes[1].routeName).to.equal("Bar") + end) + + it("should replace keyed route if provided", function() + local router = StackRouter({ + { Foo = function() end }, + { Bar = function() end }, + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo", + }, + { + routeName = "Bar", + key = "Bar", + } + }, + index = 2, + } + + local newState = router.getStateForAction(StackActions.replace({ + routeName = "Foo", + key = "Bar", + newKey = "NewFoo", + }), initialState) + + expect(#newState.routes).to.equal(2) + expect(newState.index).to.equal(2) + expect(newState.routes[2].routeName).to.equal("Foo") + expect(newState.routes[2].key).to.equal("NewFoo") + end) + + it("should reset top-level routes if not given a key", function() + local router = StackRouter({ + { Foo = function() end }, + { Bar = function() end }, + }) + + local initialState = { + key = "root", + routes = { + { routeName = "Foo", key = "Foo1" }, + { routeName = "Foo", key = "Foo2" }, + }, + index = 2, + } + + local resultState = router.getStateForAction(StackActions.reset({ + index = 1, + actions = { + NavigationActions.navigate({ routeName = "Bar" }) + } + }), initialState) + + -- "actions" array replaces entire state, bypassing initial route config! + expect(#resultState.routes).to.equal(1) + expect(resultState.index).to.equal(1) + expect(resultState.routes[1].routeName).to.equal("Bar") + end) + + it("should reset keyed route if provided", function() + local childRouter = StackRouter({ + { City = function() end }, + { State = function() end }, + }) + + local router = StackRouter({ + { Foo = function() end }, + { + Bar = { + screen = function() end, + router = childRouter, + }, + }, + }, { + initialRouteName = "Bar", + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo1", + }, + { + routeName = "Bar", + key = "Bar", + routes = { + { + routeName = "City", + key = "City", + } + }, + index = 1, + }, + }, + index = 2, + } + + local resultState = router.getStateForAction(StackActions.reset({ + actions = { + NavigationActions.navigate({ routeName = "State" }) + }, + key = "Bar", + }), initialState) + + -- "actions" array replaces entire state, bypassing initial route config! + expect(#resultState.routes).to.equal(2) + expect(resultState.index).to.equal(2) + expect(resultState.routes[2].routeName).to.equal("Bar") + expect(resultState.routes[2].routes[1].routeName).to.equal("City") + end) + + it("should mark state as transitioning, then clear it on CompleteTransition action", function() + local router = StackRouter({ + { Foo = function() end }, + { Bar = function() end }, + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo", + } + }, + index = 1, + } + + local transitioningState = router.getStateForAction(StackActions.push({ routeName = "Bar" }), initialState) + expect(transitioningState.isTransitioning).to.equal(true) + + local completedState = router.getStateForAction(StackActions.completeTransition({ + toChildKey = transitioningState.routes[2].key, -- Need actual key to identify target + }), transitioningState) + + expect(completedState.isTransitioning).to.equal(false) + end) + + it("should mark root and child states as transitioning, then separately clear them on CompleteTransition", function() + local childRouter = StackRouter({ + { BarA = function() end }, + { BarB = function() end }, + }) + local router = StackRouter({ + { Foo = function() end }, + { + Bar = { + screen = { + render = function() end, + router = childRouter, + }, + }, + }, + }) + + local initialState = { + key = "root", + routes = { + { + routeName = "Foo", + key = "Foo", + }, + }, + index = 1, + } + + local transitioningState = router.getStateForAction(NavigationActions.navigate({ routeName = "BarB" }), initialState) + expect(transitioningState).to.be.ok() + expect(transitioningState.isTransitioning).to.equal(true) + expect(transitioningState.routes[2].isTransitioning).to.equal(true) + expect(transitioningState.routes[2].routes[2].routeName).to.equal("BarB") + + local childOnlyCompletedState = router.getStateForAction(StackActions.completeTransition({ + toChildKey = transitioningState.routes[2].routes[2].key, + }), transitioningState) + expect(childOnlyCompletedState.isTransitioning).to.equal(true) -- *** parent needs its own completeTransition call *** + expect(childOnlyCompletedState.routes[2].isTransitioning).to.equal(false) + + local completedState = router.getStateForAction(StackActions.completeTransition({ + toChildKey = transitioningState.routes[2].key, + }), childOnlyCompletedState) + expect(completedState.isTransitioning).to.equal(false) + expect(completedState.routes[2].isTransitioning).to.equal(false) + end) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/StackRouter.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/StackRouter.spec.lua new file mode 100644 index 0000000..72ab5b4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/StackRouter.spec.lua @@ -0,0 +1,2032 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/72e8160537954af40f1b070aa91ef45fc02bba69/packages/core/src/routers/__tests__/StackRouter.test.js + +return function() + local Root = script.Parent.Parent.Parent + local Packages = Root.Parent + local Cryo = require(Packages.Cryo) + local Roact = require(Packages.Roact) + local StackRouter = require(script.Parent.Parent.StackRouter) + local StackActions = require(Root.routers.StackActions) + local NavigationActions = require(Root.NavigationActions) + local KeyGenerator = require(Root.utils.KeyGenerator) + local expectDeepEqual = require(Root.utils.expectDeepEqual) + + beforeEach(function() + KeyGenerator._TESTING_ONLY_normalize_keys() + end) + + local function ListScreen() + return Roact.createElement("Frame") + end + + local ProfileNavigator = Roact.Component:extend("ProfileNavigator") + ProfileNavigator.router = StackRouter({ + { + list = { + path = "list/:id", + screen = ListScreen, + }, + }, + }) + + function ProfileNavigator:render() + return Roact.createElement("Frame") + end + + local MainNavigator = Roact.Component:extend("MainNavigator") + MainNavigator.router = StackRouter({ + { + profile = { + path = "p/:id", + screen = ProfileNavigator, + }, + }, + }) + + function MainNavigator:render() + return Roact.createElement("Frame") + end + + local function LoginScreen() + return Roact.createElement("Frame") + end + + local AuthNavigator = Roact.Component:extend("AuthNavigator") + AuthNavigator.router = StackRouter({ + { login = { screen = LoginScreen } }, + }) + + function AuthNavigator:render() + return Roact.createElement("Frame") + end + + local FooNavigator = Roact.Component:extend("FooNavigator") + FooNavigator.router = StackRouter({ + { + bar = { + path = "b/:barThing", + screen = function() return Roact.createElement("Frame") end, + }, + }, + }) + + function FooNavigator:render() + return Roact.createElement("Frame") + end + + local PersonScreen = function() + return Roact.createElement("Frame") + end + + local TestStackRouter = StackRouter({ + { main = { screen = MainNavigator } }, + { baz = { path = nil, screen = FooNavigator } }, + { auth = { screen = AuthNavigator } }, + { person = { path = "people/:id", screen = PersonScreen } }, + { foo = { path = "fo/:fooThing", screen = FooNavigator } }, + }) + + describe("StackRouter", function() + it("Gets the active screen for a given state", function() + local function FooScreen() + return Roact.createElement("Frame") + end + local function BarScreen() + return Roact.createElement("Frame") + end + local router = StackRouter({ + { foo = { screen = FooScreen } }, + { bar = { screen = BarScreen } }, + }) + + expect(router.getComponentForState({ + index = 1, + isTransitioning = false, + routes = { + { key = "a", routeName = "foo" }, + { key = "b", routeName = "bar" }, + { key = "c", routeName = "foo" }, + }, + })).to.equal(FooScreen) + expect(router.getComponentForState({ + index = 2, + isTransitioning = false, + routes = { + { key = "a", routeName = "foo" }, + { key = "b", routeName = "bar" }, + }, + })).to.equal(BarScreen) + end) + + it("Handles getScreen in getComponentForState", function() + local function FooScreen() + return Roact.createElement("Frame") + end + local function BarScreen() + return Roact.createElement("Frame") + end + local router = StackRouter({ + { foo = { getScreen = function() return FooScreen end } }, + { bar = { getScreen = function() return BarScreen end } }, + }) + + expect(router.getComponentForState({ + index = 1, + isTransitioning = false, + routes = { + { key = "a", routeName = "foo" }, + { key = "b", routeName = "bar" }, + { key = "c", routeName = "foo" }, + }, + })).to.equal(FooScreen) + expect(router.getComponentForState({ + index = 2, + isTransitioning = false, + routes = { + { key = "a", routeName = "foo" }, + { key = "b", routeName = "bar" }, + }, + })).to.equal(BarScreen) + end) + + it("Gets the screen for given route", function() + local function FooScreen() + return Roact.createElement("Frame") + end + + local BarScreen = Roact.Component:extend("BarScreen") + function BarScreen:render() + return Roact.createElement("Frame") + end + + local BazScreen = Roact.Component:extend("BazScreen") + function BazScreen:render() + return Roact.createElement("Frame") + end + + local router = StackRouter({ + { foo = { screen = FooScreen } }, + { bar = { screen = BarScreen } }, + { baz = { screen = BazScreen } }, + }) + + expect(router.getComponentForRouteName("foo")).to.equal(FooScreen) + expect(router.getComponentForRouteName("bar")).to.equal(BarScreen) + expect(router.getComponentForRouteName("baz")).to.equal(BazScreen) + end) + + it("Handles getScreen in getComponent", function() + local function FooScreen() + return Roact.createElement("Frame") + end + + local BarScreen = Roact.Component:extend("BarScreen") + function BarScreen:render() + return Roact.createElement("Frame") + end + + local BazScreen = Roact.Component:extend("BazScreen") + function BazScreen:render() + return Roact.createElement("Frame") + end + + local router = StackRouter({ + { foo = { getScreen = function() return FooScreen end } }, + { bar = { getScreen = function() return BarScreen end } }, + { baz = { getScreen = function() return BazScreen end } }, + }) + + expect(router.getComponentForRouteName("foo")).to.equal(FooScreen) + expect(router.getComponentForRouteName("bar")).to.equal(BarScreen) + expect(router.getComponentForRouteName("baz")).to.equal(BazScreen) + end) + + -- deviation: StackRouter.getActionForPathAndParams not implemented + itSKIP("Parses simple paths", function() + expectDeepEqual( + AuthNavigator.router.getActionForPathAndParams("login"), + { + type = NavigationActions.Navigate, + routeName = "login", + params = {}, + } + ) + end) + + -- deviation: StackRouter.getActionForPathAndParams not implemented + itSKIP("Parses paths with a param", function() + expectDeepEqual( + TestStackRouter.getActionForPathAndParams("people/foo"), + { + type = NavigationActions.Navigate, + routeName = "person", + params = { id = "foo" }, + } + ) + end) + + -- deviation: StackRouter.getActionForPathAndParams not implemented + itSKIP("Parses paths with a query", function() + expectDeepEqual( + TestStackRouter.getActionForPathAndParams("people/foo", { + code = "test", + foo = "bar", + }), + { + type = NavigationActions.Navigate, + routeName = "person", + params = { + id = "foo", + code = "test", + foo = "bar", + }, + } + ) + end) + + -- deviation: StackRouter.getActionForPathAndParams not implemented + itSKIP("Parses paths with an empty query value", function() + expectDeepEqual( + TestStackRouter.getActionForPathAndParams("people/foo", { + code = "", + foo = "bar", + }), + { + type = NavigationActions.Navigate, + routeName = "person", + params = { + id = "foo", + code = "", + foo = "bar", + }, + } + ) + end) + + -- deviation: StackRouter.getActionForPathAndParams not implemented + itSKIP("Correctly parses a path without arguments into an action chain", function() + local uri = "auth/login" + local action = TestStackRouter.getActionForPathAndParams(uri) + + expectDeepEqual(action, { + type = NavigationActions.Navigate, + routeName = "auth", + params = {}, + action = { + type = NavigationActions.Navigate, + routeName = "login", + params = {}, + }, + }) + end) + + -- deviation: StackRouter.getActionForPathAndParams not implemented + itSKIP("Correctly parses a path with arguments into an action chain", function() + local uri = "main/p/4/list/10259959195" + local action = TestStackRouter.getActionForPathAndParams(uri) + + expectDeepEqual(action, { + type = NavigationActions.Navigate, + routeName = "main", + params = {}, + action = { + type = NavigationActions.Navigate, + routeName = "profile", + params = { id = "4" }, + action = { + type = NavigationActions.Navigate, + routeName = "list", + params = { id = "10259959195" }, + }, + }, + }) + end) + + -- deviation: StackRouter.getActionForPathAndParams not implemented + itSKIP("Correctly parses a path to the router connected to another router " + .. "through a pure wildcard route into an action chain", function() + local uri = "b/123" + local action = TestStackRouter.getActionForPathAndParams(uri) + + expectDeepEqual(action, { + type = NavigationActions.Navigate, + routeName = "baz", + params = {}, + action = { + type = NavigationActions.Navigate, + routeName = "bar", + params = { + barThing = "123", + }, + }, + }) + end) + + -- deviation: StackRouter.getActionForPathAndParams not implemented + itSKIP("Correctly returns null action for non-existent path", function() + local uri = "asdf/1234" + local action = TestStackRouter.getActionForPathAndParams(uri) + + expect(action).to.equal(nil) + end) + + -- deviation: StackRouter.getActionForPathAndParams not implemented + itSKIP("Correctly returns action chain for partially matched path", function() + local uri = "auth/login" + local action = TestStackRouter.getActionForPathAndParams(uri) + + expectDeepEqual(action, { + type = NavigationActions.Navigate, + routeName = "auth", + params = {}, + action = { + type = NavigationActions.Navigate, + routeName = "login", + params = {}, + }, + }) + end) + + -- deviation: StackRouter.getActionForPathAndParams not implemented + itSKIP("Correctly returns action for path with multiple parameters", function() + local path = "fo/22/b/hello" + local action = TestStackRouter.getActionForPathAndParams(path) + + expectDeepEqual(action, { + type = NavigationActions.Navigate, + routeName = "foo", + params = { fooThing = "22" }, + action = { + type = NavigationActions.Navigate, + routeName = "bar", + params = { barThing = "hello" }, + }, + }) + end) + + it("Pushes other navigators when navigating to an unopened route name", function() + local Bar = Roact.Component:extend("Bar") + function Bar:render() + return Roact.createElement("Frame") + end + + Bar.router = StackRouter({ + { baz = { screen = function() return Roact.createElement("Frame") end } }, + { qux = { screen = function() return Roact.createElement("Frame") end } }, + }) + + local TestRouter = StackRouter({ + { foo = { screen = function() return Roact.createElement("Frame") end } }, + { bar = { screen = Bar } }, + }) + local initState = TestRouter.getStateForAction(NavigationActions.init()) + + expectDeepEqual(initState, { + index = 1, + isTransitioning = false, + key = "StackRouterRoot", + routes = { { key = "id-0", routeName = "foo" } }, + }) + + local pushedState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "qux" }), + initState + ) + + expect(pushedState.index).to.equal(2) + expect(pushedState.routes[2].index).to.equal(2) + expect(pushedState.routes[2].routes[2].routeName).to.equal("qux") + end) + + it("push bubbles up", function() + local ChildNavigator = Roact.Component:extend("ChildNavigator") + function ChildNavigator:render() + return Roact.createElement("Frame") + end + + ChildNavigator.router = StackRouter({ + { Baz = { screen = function() return Roact.createElement("Frame") end } }, + { Qux = { screen = function() return Roact.createElement("Frame") end } }, + }) + + local router = StackRouter({ + { Foo = { screen = function() return Roact.createElement("Frame") end } }, + { Bar = { screen = ChildNavigator } }, + { Bad = { screen = function() return Roact.createElement("Frame") end } }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + local state2 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Bar" }, + state + ) + local state3 = router.getStateForAction({ + type = StackActions.Push, + routeName = "Bad", + }, state2) + + expect(state3 and state3.index).to.equal(3) + expect(state3 and #state3.routes).to.equal(3) + end) + + it("pop bubbles up", function() + local ChildNavigator = Roact.Component:extend("ChildNavigator") + + function ChildNavigator:render() + return Roact.createElement("Frame") + end + + ChildNavigator.router = StackRouter({ + { Baz = { screen = function() return Roact.createElement("Frame") end } }, + { Qux = { screen = function() return Roact.createElement("Frame") end } }, + }) + + local router = StackRouter({ + { Foo = { screen = function() return Roact.createElement("Frame") end } }, + { Bar = { screen = ChildNavigator } }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + local state2 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Bar", + key = "StackRouterRoot", + }, state) + local state3 = router.getStateForAction({ type = StackActions.Pop }, state2) + + expect(state3 and state3.index).to.equal(1) + end) + + -- deviation: StackRouter.getActionForPathAndParams not implemented + itSKIP("Handle navigation to nested navigator", function() + local action = TestStackRouter.getActionForPathAndParams("fo/22/b/hello") + local state2 = TestStackRouter.getStateForAction(action) + + expectDeepEqual(state2, { + index = 1, + isTransitioning = false, + key = "StackRouterRoot", + routes = { + { + index = 1, + key = "id-1", + isTransitioning = false, + routeName = "foo", + params = { fooThing = "22" }, + routes = { + { + routeName = "bar", + key = "id-0", + params = { + barThing = "hello", + }, + }, + }, + }, + }, + }) + end) + + it("popToTop bubbles up", function() + local ChildNavigator = Roact.Component:extend("ChildNavigator") + + function ChildNavigator:render() + return Roact.createElement("Frame") + end + + ChildNavigator.router = StackRouter({ + { Baz = { screen = function() return Roact.createElement("Frame") end } }, + { Qux = { screen = function() return Roact.createElement("Frame") end } }, + }) + + local router = StackRouter({ + { Foo = { screen = function() return Roact.createElement("Frame") end } }, + { Bar = { screen = ChildNavigator } }, + }) + + local state = router.getStateForAction({ type = NavigationActions.Init }) + local state2 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Bar" }, + state + ) + local state3 = router.getStateForAction({ type = StackActions.PopToTop }, state2) + + expect(state3 and state3.index).to.equal(1) + end) + + it("popToTop targets StackRouter by key if specified", function() + local ChildNavigator = Roact.Component:extend("ChildNavigator") + + function ChildNavigator:render() + return Roact.createElement("Frame") + end + + ChildNavigator.router = StackRouter({ + { Baz = { screen = function() return Roact.createElement("Frame") end } }, + { Qux = { screen = function() return Roact.createElement("Frame") end } }, + }) + + local router = StackRouter({ + { Foo = { screen = function() return Roact.createElement("Frame") end } }, + { Bar = { screen = ChildNavigator } }, + }) + + local state = router.getStateForAction({ type = NavigationActions.Init }) + local state2 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Bar" }, + state + ) + local state3 = router.getStateForAction({ + type = StackActions.PopToTop, + key = state2.key, + }, state2) + + expect(state3 and state3.index).to.equal(1) + end) + + it("pop action works as expected", function() + local TestRouter = StackRouter({ + { foo = { screen = function() return Roact.createElement("Frame") end } }, + { bar = { screen = function() return Roact.createElement("Frame") end } }, + }) + local state = { + index = 4, + isTransitioning = false, + routes = { + { key = "A", routeName = "foo" }, + { key = "B", routeName = "bar", params = { bazId = "321" } }, + { key = "C", routeName = "foo" }, + { key = "D", routeName = "bar" }, + }, + } + local poppedState = TestRouter.getStateForAction(StackActions.pop(), state) + + expect(#poppedState.routes).to.equal(3) + expect(poppedState.index).to.equal(3) + expect(poppedState.isTransitioning).to.equal(true) + + local poppedState2 = TestRouter.getStateForAction( + StackActions.pop({ n = 2, immediate = true }), + state + ) + + expect(#poppedState2.routes).to.equal(2) + expect(poppedState2.index).to.equal(2) + expect(poppedState2.isTransitioning).to.equal(false) + + local poppedState3 = TestRouter.getStateForAction( + StackActions.pop({ n = 5 }), + state + ) + expect(#poppedState3.routes).to.equal(1) + expect(poppedState3.index).to.equal(1) + expect(poppedState3.isTransitioning).to.equal(true) + local poppedState4 = TestRouter.getStateForAction( + StackActions.pop({ key = "C", prune = false, immediate = true }), + state + ) + + expect(#poppedState4.routes).to.equal(3) + expect(poppedState4.index).to.equal(3) + expect(poppedState4.isTransitioning).to.equal(false) + expectDeepEqual(poppedState4.routes, { + { key = "A", routeName = "foo" }, + { key = "B", routeName = "bar", params = { bazId = "321" } }, + { key = "D", routeName = "bar" }, + }) + + local poppedState5 = TestRouter.getStateForAction(StackActions.pop({ + n = 2, + key = "C", + prune = false, + }), state) + + expect(#poppedState5.routes).to.equal(2) + expect(poppedState5.index).to.equal(2) + expect(poppedState5.isTransitioning).to.equal(true) + expectDeepEqual(poppedState5.routes, { + { key = "A", routeName = "foo" }, + { key = "D", routeName = "bar" }, + }) + end) + + it("popToTop works as expected", function() + local TestRouter = StackRouter({ + { foo = { screen = function() return Roact.createElement("Frame") end } }, + { bar = { screen = function() return Roact.createElement("Frame") end } }, + }) + local state = { + index = 2, + isTransitioning = false, + routes = { + { key = "A", routeName = "foo" }, + { key = "B", routeName = "bar", params = { bazId = "321" } }, + { key = "C", routeName = "foo" }, + }, + } + local poppedState = TestRouter.getStateForAction(StackActions.popToTop(), state) + + expect(#poppedState.routes).to.equal(1) + expect(poppedState.index).to.equal(1) + expect(poppedState.isTransitioning).to.equal(true) + + local poppedState2 = TestRouter.getStateForAction(StackActions.popToTop(), poppedState) + + expectDeepEqual(poppedState, poppedState2) + + local poppedImmediatelyState = TestRouter.getStateForAction( + StackActions.popToTop({ immediate = true }), + state + ) + + expect(#poppedImmediatelyState.routes).to.equal(1) + expect(poppedImmediatelyState.index).to.equal(1) + expect(poppedImmediatelyState.isTransitioning).to.equal(false) + end) + + it("Navigate does not push duplicate routeName", function() + local TestRouter = StackRouter({ + { foo = { screen = function() return Roact.createElement("Frame") end } }, + { bar = { screen = function() return Roact.createElement("Frame") end } }, + }, { + initialRouteName = "foo", + }) + local initState = TestRouter.getStateForAction(NavigationActions.init()) + local barState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "bar" }), + initState + ) + + expect(barState.index).to.equal(2) + expect(barState.routes[2].routeName).to.equal("bar") + + local navigateOnBarState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "bar" }), + barState + ) + + expect(navigateOnBarState).to.equal(nil) + end) + + it("Navigate focuses given routeName if already active in stack", function() + local TestRouter = StackRouter({ + { foo = { screen = function() return Roact.createElement("Frame") end } }, + { bar = { screen = function() return Roact.createElement("Frame") end } }, + { + baz = { + screen = function() return Roact.createElement("Frame") end, + }, + }, + }, { + initialRouteName = "foo", + }) + local initialState = TestRouter.getStateForAction(NavigationActions.init()) + local fooBarState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "bar" }), + initialState + ) + local fooBarBazState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "baz" }), + fooBarState + ) + + expect(fooBarBazState.index).to.equal(3) + expect(fooBarBazState.routes[3].routeName).to.equal("baz") + + local fooState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "foo" }), + fooBarBazState + ) + + expect(fooState.index).to.equal(1) + expect(#fooState.routes).to.equal(1) + expect(fooState.routes[1].routeName).to.equal("foo") + end) + + it("Navigate pushes duplicate routeName if unique key is provided", function() + local TestRouter = StackRouter({ + { foo = { screen = function() return Roact.createElement("Frame") end } }, + { bar = { screen = function() return Roact.createElement("Frame") end } }, + }) + local initState = TestRouter.getStateForAction(NavigationActions.init()) + local pushedState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "bar" }), + initState + ) + + expect(pushedState.index).to.equal(2) + expect(pushedState.routes[2].routeName).to.equal("bar") + + local pushedTwiceState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "bar", key = "new-unique-key!" }), + pushedState + ) + + expect(pushedTwiceState.index).to.equal(3) + expect(pushedTwiceState.routes[3].routeName).to.equal("bar") + end) + + it("Navigate from top propagates to any arbitary depth of stacks", function() + local GrandChildNavigator = Roact.Component:extend("GrandChildNavigator") + function GrandChildNavigator:render() + return Roact.createElement("Frame") + end + + GrandChildNavigator.router = StackRouter({ + { Quux = { screen = function() return Roact.createElement("Frame") end } }, + { Corge = { screen = function() return Roact.createElement("Frame") end } }, + }) + + local ChildNavigator = Roact.Component:extend("ChildNavigator") + function ChildNavigator:render() + return Roact.createElement("Frame") + end + + ChildNavigator.router = StackRouter({ + { Baz = { screen = function() return Roact.createElement("Frame") end } }, + { Woo = { screen = function() return Roact.createElement("Frame") end } }, + { Qux = { screen = GrandChildNavigator } }, + }) + local Parent = StackRouter({ + { Foo = { screen = function() return Roact.createElement("Frame") end } }, + { Bar = { screen = ChildNavigator } }, + }) + + local state = Parent.getStateForAction({ type = NavigationActions.Init }) + local state2 = Parent.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Corge" }, + state + ) + + expect(state2.isTransitioning).to.equal(true) + expect(state2.index).to.equal(2) + expect(state2.routes[2].index).to.equal(2) + expect(state2.routes[2].routes[2].index).to.equal(2) + expect(state2.routes[2].routes[2].routes[2].routeName).to.equal("Corge") + end) + + it("Navigate to initial screen is possible", function() + local TestRouter = StackRouter({ + { foo = { screen = function() return Roact.createElement("Frame") end } }, + { bar = { screen = function() return Roact.createElement("Frame") end } }, + }, { + initialRouteKey = "foo", + }) + local initState = TestRouter.getStateForAction(NavigationActions.init()) + local pushedState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "foo", key = "foo" }), + initState + ) + + expect(pushedState).to.equal(nil) + end) + + it("Navigate with key and without it is idempotent", function() + local TestRouter = StackRouter({ + { foo = { screen = function() return Roact.createElement("Frame") end } }, + { bar = { screen = function() return Roact.createElement("Frame") end } }, + }) + local initState = TestRouter.getStateForAction(NavigationActions.init()) + local pushedState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "bar", key = "a" }), + initState + ) + + expect(pushedState.index).to.equal(2) + expect(pushedState.routes[2].routeName).to.equal("bar") + + local pushedTwiceState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "bar", key = "a" }), + pushedState + ) + + expect(pushedTwiceState).to.equal(nil) + end) + + -- https://github.com/react-navigation/react-navigation/issues/4063 + it("Navigate on inactive stackrouter is idempotent", function() + local FirstChildNavigator = Roact.Component:extend("FirstChildNavigator") + function FirstChildNavigator:render() + return Roact.createElement("Frame") + end + + FirstChildNavigator.router = StackRouter({ + { First1 = function() return Roact.createElement("Frame") end }, + { First2 = function() return Roact.createElement("Frame") end }, + }) + + local SecondChildNavigator = Roact.Component:extend("SecondChildNavigator") + function SecondChildNavigator:render() + return Roact.createElement("Frame") + end + + SecondChildNavigator.router = StackRouter({ + { Second1 = function() return Roact.createElement("Frame") end }, + { Second2 = function() return Roact.createElement("Frame") end }, + }) + + local router = StackRouter({ + { Leaf = function() return Roact.createElement("Frame") end }, + { First = FirstChildNavigator }, + { Second = SecondChildNavigator }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + local first = router.getStateForAction( + NavigationActions.navigate({ routeName = "First2" }), + state + ) + local second = router.getStateForAction( + NavigationActions.navigate({ routeName = "Second2" }), + first + ) + local firstAgain = router.getStateForAction( + NavigationActions.navigate({ routeName = "First2", params = { debug = true } }), + second + ) + + expect(#first.routes).to.equal(2) + expect(first.index).to.equal(2) + expect(#second.routes).to.equal(3) + expect(second.index).to.equal(3) + + expect(firstAgain.index).to.equal(2) + expect(#firstAgain.routes).to.equal(2) + end) + + it("Navigate to current routeName returns null to indicate handled action", function() + local TestRouter = StackRouter({ + { foo = { screen = function() return Roact.createElement("Frame") end } }, + { bar = { screen = function() return Roact.createElement("Frame") end } }, + }) + local initState = TestRouter.getStateForAction(NavigationActions.init()) + local navigatedState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "foo" }), + initState + ) + + expect(navigatedState).to.equal(nil) + end) + + it("Push behaves like navigate, except for key", function() + local TestRouter = StackRouter({ + { foo = { screen = function() return Roact.createElement("Frame") end } }, + { bar = { screen = function() return Roact.createElement("Frame") end } }, + }) + local initState = TestRouter.getStateForAction(NavigationActions.init()) + local pushedState = TestRouter.getStateForAction( + StackActions.push({ routeName = "bar" }), + initState + ) + + expect(pushedState.index).to.equal(2) + expect(pushedState.routes[2].routeName).to.equal("bar") + expect(function() + TestRouter.getStateForAction({ + type = StackActions.Push, + routeName = "bar", + key = "a", + }, pushedState) + end).to.throw("StackRouter does not support key on the push action") + end) + + it("Push adds new routes every time", function() + local TestRouter = StackRouter({ + { foo = { screen = function() return Roact.createElement("Frame") end } }, + { bar = { screen = function() return Roact.createElement("Frame") end } }, + }) + local initState = TestRouter.getStateForAction(NavigationActions.init()) + local pushedState = TestRouter.getStateForAction( + StackActions.push({ routeName = "bar" }), + initState + ) + + expect(pushedState.index).to.equal(2) + expect(pushedState.routes[2].routeName).to.equal("bar") + + local secondPushedState = TestRouter.getStateForAction(StackActions.push({ + routeName = "bar", + }), pushedState) + + expect(secondPushedState.index).to.equal(3) + expect(secondPushedState.routes[3].routeName).to.equal("bar") + end) + + it("Navigate backwards with key removes leading routes", function() + local TestRouter = StackRouter({ + { foo = { screen = function() return Roact.createElement("Frame") end } }, + { bar = { screen = function() return Roact.createElement("Frame") end } }, + }) + local initState = TestRouter.getStateForAction(NavigationActions.init()) + local pushedState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "bar", key = "a" }), + initState + ) + local pushedTwiceState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "bar", key = "b`" }), + pushedState + ) + local pushedThriceState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "foo", key = "c`" }), + pushedTwiceState + ) + + expect(#pushedThriceState.routes).to.equal(4) + + local navigatedBackToFirstRouteState = TestRouter.getStateForAction(NavigationActions.navigate({ + routeName = "foo", + key = pushedThriceState.routes[1].key, + }), pushedThriceState) + + expect(navigatedBackToFirstRouteState.index).to.equal(1) + expect(#navigatedBackToFirstRouteState.routes).to.equal(1) + end) + + it("Handle basic stack logic for plain components", function() + local function FooScreen() + return Roact.createElement("Frame") + end + local function BarScreen() + return Roact.createElement("Frame") + end + local router = StackRouter({ + { Foo = { screen = FooScreen } }, + { Bar = { screen = BarScreen } }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + + expectDeepEqual(state, { + index = 1, + isTransitioning = false, + key = "StackRouterRoot", + routes = { + { key = "id-0", routeName = "Foo" }, + }, + }) + + local state2 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Bar", + params = { name = "Zoom" }, + immediate = true, + }, state) + + expect(state2.index).to.equal(2) + expect(state2.routes[2].routeName).to.equal("Bar") + expectDeepEqual(state2.routes[2].params, { name = "Zoom" }) + expect(#state2.routes).to.equal(2) + + local state3 = router.getStateForAction( + { type = NavigationActions.Back, immediate = true }, + state2 + ) + + expectDeepEqual(state3, { + index = 1, + isTransitioning = false, + key = "StackRouterRoot", + routes = { + { key = "id-0", routeName = "Foo" }, + }, + }) + end) + + it("Replace action works", function() + local TestRouter = StackRouter({ + { foo = { screen = function() return Roact.createElement("Frame") end } }, + { bar = { screen = function() return Roact.createElement("Frame") end } }, + }) + local initState = TestRouter.getStateForAction( + NavigationActions.navigate({ routeName = "foo" }) + ) + local replacedState = TestRouter.getStateForAction(StackActions.replace({ + routeName = "bar", + params = { meaning = 42 }, + key = initState.routes[1].key, + }), initState) + + expect(replacedState.index).to.equal(1) + expect(#replacedState.routes).to.equal(1) + expect(replacedState.routes[1].key).never.to.equal(initState.routes[1].key) + expect(replacedState.routes[1].routeName).to.equal("bar") + expect(replacedState.routes[1].params.meaning).to.equal(42) + + local replacedState2 = TestRouter.getStateForAction(StackActions.replace({ + routeName = "bar", + key = initState.routes[1].key, + newKey = "wow", + }), initState) + + expect(replacedState2.index).to.equal(1) + expect(#replacedState2.routes).to.equal(1) + expect(replacedState2.routes[1].key).to.equal("wow") + expect(replacedState2.routes[1].routeName).to.equal("bar") + end) + + it("Replace action returns most recent route if no key is provided", function() + local GrandChildNavigator = Roact.Component:extend("GrandChildNavigator") + function GrandChildNavigator:render() + return Roact.createElement("Frame") + end + + GrandChildNavigator.router = StackRouter({ + { Quux = { screen = function() return Roact.createElement("Frame") end } }, + { Corge = { screen = function() return Roact.createElement("Frame") end } }, + { Grault = { screen = function() return Roact.createElement("Frame") end } }, + }) + + local ChildNavigator = Roact.Component:extend("ChildNavigator") + function ChildNavigator:render() + return Roact.createElement("Frame") + end + + ChildNavigator.router = StackRouter({ + { Baz = { screen = function() return Roact.createElement("Frame") end } }, + { Woo = { screen = function() return Roact.createElement("Frame") end } }, + { Qux = { screen = GrandChildNavigator } }, + }) + + local router = StackRouter({ + { Foo = { screen = function() return Roact.createElement("Frame") end } }, + { Bar = { screen = ChildNavigator } }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + local state2 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Bar" }, + state + ) + local state3 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Qux" }, + state2 + ) + local state4 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Corge" }, + state3 + ) + local state5 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Grault" }, + state4 + ) + local replacedState = router.getStateForAction( + StackActions.replace({ routeName = "Woo", params = { meaning = 42 } }), + state5 + ) + local originalCurrentScreen = state5.routes[2].routes[2].routes[3] + local replacedCurrentScreen = replacedState.routes[2].routes[2].routes[3] + + expect(replacedState.routes[2].routes[2].index).to.equal(3) + expect(#replacedState.routes[2].routes[2].routes).to.equal(3) + expect(replacedCurrentScreen.key).never.to.equal(originalCurrentScreen.key) + expect(replacedCurrentScreen.routeName).never.to.equal(originalCurrentScreen.routeName) + expect(replacedCurrentScreen.routeName).to.equal("Woo") + expect(replacedCurrentScreen.params.meaning).to.equal(42) + end) + + it("Handles push transition logic with completion action", function() + local function FooScreen() + return Roact.createElement("Frame") + end + local function BarScreen() + return Roact.createElement("Frame") + end + local router = StackRouter({ + { Foo = { screen = FooScreen } }, + { Bar = { screen = BarScreen } }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + local state2 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Bar", + params = { name = "Zoom" }, + }, state) + + expect(state2 and state2.index).to.equal(2) + expect(state2 and state2.isTransitioning).to.equal(true) + + local state3 = router.getStateForAction({ + type = StackActions.CompleteTransition, + toChildKey = state2.routes[2].key, + }, state2) + + expect(state3 and state3.index).to.equal(2) + expect(state3 and state3.isTransitioning).to.equal(false) + end) + + it("Completion action does not work with incorrect key", function() + local function FooScreen() + return Roact.createElement("Frame") + end + local router = StackRouter({ + { Foo = { screen = FooScreen } }, + { Bar = { screen = FooScreen } }, + }) + local state = { + key = "StackKey", + index = 2, + isTransitioning = true, + routes = { + { key = "a", routeName = "Foo" }, + { key = "b", routeName = "Foo" }, + }, + } + local outputState = router.getStateForAction({ + type = StackActions.CompleteTransition, + toChildKey = state.routes[state.index].key, + key = "not StackKey", + }, state) + + expect(outputState.isTransitioning).to.equal(true) + end) + + it("Completion action does not work with incorrect toChildKey", function() + local function FooScreen() + return Roact.createElement("Frame") + end + local router = StackRouter({ + { Foo = { screen = FooScreen } }, + { Bar = { screen = FooScreen } }, + }) + local state = { + key = "StackKey", + index = 2, + isTransitioning = true, + routes = { + { + key = "a", + routeName = "Foo", + }, + { + key = "b", + routeName = "Foo", + }, + }, + } + local outputState = router.getStateForAction({ + type = StackActions.CompleteTransition, + -- for this action to toggle isTransitioning, toChildKey should be state.routes[state.index].key, + toChildKey = "incorrect", + key = "StackKey", + }, state) + + expect(outputState.isTransitioning).to.equal(true) + end) + + it("Back action parent is prioritized over inactive child routers", function() + local Bar = Roact.Component:extend("Bar") + function Bar:render() + return Roact.createElement("Frame") + end + + Bar.router = StackRouter({ + { baz = { screen = function() return Roact.createElement("Frame") end } }, + { qux = { screen = function() return Roact.createElement("Frame") end } }, + }) + + local TestRouter = StackRouter({ + { foo = { screen = function() return Roact.createElement("Frame") end } }, + { bar = { screen = Bar } }, + { boo = { screen = function() return Roact.createElement("Frame") end } }, + }) + local state = { + key = "top", + index = 4, + routes = { + { routeName = "foo", key = "f" }, + { + routeName = "bar", + key = "b", + index = 2, + routes = { + { routeName = "baz", key = "bz" }, + { routeName = "qux", key = "bx" }, + }, + }, + { routeName = "foo", key = "f1" }, + { routeName = "boo", key = "z" }, + }, + } + local testState = TestRouter.getStateForAction({ type = NavigationActions.Back }, state) + + expect(testState.index).to.equal(3) + expect(testState.routes[2].index).to.equal(2) + end) + + it("Handle basic stack logic for components with router", function() + local function FooScreen() + return Roact.createElement("Frame") + end + local BarScreen = Roact.Component:extend("BarScreen") + function BarScreen:render() + return Roact.createElement("Frame") + end + + BarScreen.router = StackRouter({ + { Xyz = { screen = function() return nil end } }, + }) + + local router = StackRouter({ + { Foo = { screen = FooScreen } }, + { Bar = { screen = BarScreen } }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + + expectDeepEqual(state, { + index = 1, + isTransitioning = false, + key = "StackRouterRoot", + routes = { + { key = "id-0", routeName = "Foo" }, + }, + }) + + local state2 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Bar", + params = { name = "Zoom" }, + immediate = true, + }, state) + + expect(state2 and state2.index).to.equal(2) + expect(state2 and state2.routes[2].routeName).to.equal("Bar") + expectDeepEqual(state2 and state2.routes[2].params, { name = "Zoom" }) + expect(state2 and #state2.routes).to.equal(2) + + local state3 = router.getStateForAction( + { type = NavigationActions.Back, immediate = true }, + state2 + ) + + expectDeepEqual(state3, { + index = 1, + isTransitioning = false, + key = "StackRouterRoot", + routes = { + { key = "id-0", routeName = "Foo" }, + }, + }) + end) + + -- deviation: `getPathAndParamsForState` not implemented + itSKIP("Gets deep path (stack behavior)", function() + local ScreenA = Roact.Component:extend("ScreenA") + function ScreenA:render() + return Roact.createElement("Frame") + end + local function ScreenB() + return Roact.createElement("Frame") + end + + ScreenA.router = StackRouter({ + { Boo = { path = "boo", screen = ScreenB } }, + { Baz = { path = "baz/:bazId", screen = ScreenB } }, + }) + local router = StackRouter({ + { Foo = { path = "f/:id", screen = ScreenA } }, + { Bar = { screen = ScreenB } }, + }) + + local state = { + index = 1, + isTransitioning = false, + routes = { + { + index = 2, + key = "Foo", + routeName = "Foo", + params = { id = "123" }, + routes = { + { key = "Boo", routeName = "Boo" }, + { + key = "Baz", + routeName = "Baz", + params = { bazId = "321" }, + }, + }, + }, + { key = "Bar", routeName = "Bar" }, + }, + } + local pathAndParam = router.getPathAndParamsForState(state) + local path = pathAndParam.path + local params = pathAndParam.params + + expect(path).to.equal("f/123/baz/321") + expectDeepEqual(params, {}) + end) + + it("Handle goBack identified by key", function() + local function FooScreen() + return Roact.createElement("Frame") + end + local function BarScreen() + return Roact.createElement("Frame") + end + local router = StackRouter({ + { Foo = { screen = FooScreen } }, + { Bar = { screen = BarScreen } }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + local state2 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Bar", + immediate = true, + params = { name = "Zoom" }, + }, state) + local state3 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Bar", + immediate = true, + params = { name = "Foo" }, + }, state2) + local state4 = router.getStateForAction({ + type = NavigationActions.Back, + key = "wrongKey", + }, state3) + + expectDeepEqual(state3, state4) + + local state5 = router.getStateForAction({ + type = NavigationActions.Back, + key = state3 and state3.routes[2].key, + immediate = true, + }, state4) + + expectDeepEqual(state5, state) + end) + + it("Handle initial route navigation", function() + local function FooScreen() + return Roact.createElement("Frame") + end + local function BarScreen() + return Roact.createElement("Frame") + end + local router = StackRouter({ + { Foo = { screen = FooScreen } }, + { Bar = { screen = BarScreen } }, + }, { + initialRouteName = "Bar", + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + + expectDeepEqual(state, { + index = 1, + isTransitioning = false, + key = "StackRouterRoot", + routes = { + { key = "id-0", routeName = "Bar" }, + }, + }) + end) + + it("Initial route params appear in nav state", function() + local function FooScreen() + return Roact.createElement("Frame") + end + local router = StackRouter({ + { Foo = { screen = FooScreen } }, + }, { + initialRouteName = "Foo", + initialRouteParams = { foo = "bar" }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + + expectDeepEqual(state, { + index = 1, + isTransitioning = false, + key = "StackRouterRoot", + routes = { + { + key = state and state.routes[1].key, + routeName = "Foo", + params = { foo = "bar" }, + }, + }, + }) + end) + + it("params in route config are merged with initialRouteParams", function() + local function FooScreen() + return Roact.createElement("Frame") + end + local router = StackRouter({ + { + Foo = { + screen = FooScreen, + params = { foo = "not-bar", meaning = 42 }, + }, + }, + }, { + initialRouteName = "Foo", + initialRouteParams = { foo = "bar" }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + + expectDeepEqual(state, { + index = 1, + isTransitioning = false, + key = "StackRouterRoot", + routes = { + { + key = state and state.routes[1].key, + routeName = "Foo", + params = { foo = "bar", meaning = 42 }, + }, + }, + }) + end) + + it("Action params appear in nav state", function() + local function FooScreen() + return Roact.createElement("Frame") + end + local function BarScreen() + return Roact.createElement("Frame") + end + local router = StackRouter({ + { Foo = { screen = FooScreen } }, + { Bar = { screen = BarScreen } }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + local state2 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Bar", + params = { bar = "42" }, + immediate = true, + }, state) + + expect(state2).never.equal(nil) + expect(state2 and state2.index).to.equal(2) + expectDeepEqual(state2 and state2.routes[2].params, { bar = "42" }) + end) + + it("Handles the SetParams action", function() + local router = StackRouter({ + { Foo = { screen = function() return Roact.createElement("Frame") end } }, + { Bar = { screen = function() return Roact.createElement("Frame") end } }, + }, { + initialRouteName = "Bar", + initialRouteParams = { name = "Zoo" }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + local key = state and state.routes[1].key + local state2 = key and router.getStateForAction({ + type = NavigationActions.SetParams, + params = { name = "Qux" }, + key = key, + }, state) + + expect(state2 and state2.index).to.equal(1) + expectDeepEqual(state2 and state2.routes[1].params, { name = "Qux" }) + end) + + it("Handles the SetParams action for inactive routes", function() + local router = StackRouter({ + { Foo = { screen = function() return Roact.createElement("Frame") end } }, + { Bar = { screen = function() return Roact.createElement("Frame") end } }, + }, { + initialRouteName = "Bar", + initialRouteParams = { name = "Zoo" }, + }) + local initialState = { + index = 2, + routes = { + { + key = "RouteA", + routeName = "Foo", + params = { name = "InitialParam", other = "Unchanged" }, + }, + { key = "RouteB", routeName = "Bar", params = {} }, + }, + } + local state = router.getStateForAction({ + type = NavigationActions.SetParams, + params = { + name = "NewParam", + }, + key = "RouteA", + }, initialState) + + expect(state.index).to.equal(2) + expect(state.routes[1].params, { + name = "NewParam", + other = "Unchanged", + }) + end) + + it("Handles the setParams action with nested routers", function() + local ChildNavigator = Roact.Component:extend("ChildNavigator") + function ChildNavigator:render() + return Roact.createElement("Frame") + end + + ChildNavigator.router = StackRouter({ + { Baz = { screen = function() return Roact.createElement("Frame") end } }, + { Qux = { screen = function() return Roact.createElement("Frame") end } }, + }) + + local router = StackRouter({ + { Foo = { screen = ChildNavigator } }, + { Bar = { screen = function() return Roact.createElement("Frame") end } }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + local state2 = router.getStateForAction({ + type = NavigationActions.SetParams, + params = { name = "foobar" }, + key = "id-0", + }, state) + + expect(state2 and state2.index).to.equal(1) + expectDeepEqual(state2 and state2.routes[1].routes, { + { + key = "id-0", + routeName = "Baz", + params = { name = "foobar" }, + }, + }) + end) + + it("Handles the reset action", function() + local router = StackRouter({ + { Foo = { screen = function() return Roact.createElement("Frame") end } }, + { Bar = { screen = function() return Roact.createElement("Frame") end } }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + local state2 = router.getStateForAction({ + type = StackActions.Reset, + actions = { + { + type = NavigationActions.Navigate, + routeName = "Foo", + params = { bar = "42" }, + immediate = true, + }, + { + type = NavigationActions.Navigate, + routeName = "Bar", + immediate = true, + }, + }, + index = 2, + }, state) + + expect(state2 and state2.index).to.equal(2) + expectDeepEqual(state2 and state2.routes[1].params, { bar = "42" }) + expect(state2 and state2.routes[1].routeName).to.equal("Foo") + expect(state2 and state2.routes[2].routeName).to.equal("Bar") + end) + + it("Handles the reset action only with correct key set", function() + local router = StackRouter({ + { Foo = { screen = function() return Roact.createElement("Frame") end } }, + { Bar = { screen = function() return Roact.createElement("Frame") end } }, + }) + local state1 = router.getStateForAction({ type = NavigationActions.Init }) + local resetAction = { + type = StackActions.Reset, + key = "Bad Key", + actions = { + { + type = NavigationActions.Navigate, + routeName = "Foo", + params = { bar = "42" }, + immediate = true, + }, + { + type = NavigationActions.Navigate, + routeName = "Bar", + immediate = true, + }, + }, + index = 2, + } + local state2 = router.getStateForAction(resetAction, state1) + + expect(state2).to.equal(state1) + + local state3 = router.getStateForAction( + Cryo.Dictionary.join(resetAction, { key = state2.key }), + state2 + ) + + expect(state3 and state3.index).to.equal(2) + expectDeepEqual(state3 and state3.routes[1].params, { bar = "42" }) + expect(state3 and state3.routes[1].routeName).to.equal("Foo") + expect(state3 and state3.routes[2].routeName).to.equal("Bar") + end) + + it("Handles the reset action with nested Router", function() + local ChildRouter = StackRouter({ + { baz = { screen = function() return Roact.createElement("Frame") end } }, + }) + local ChildNavigator = Roact.Component:extend("ChildNavigator") + function ChildNavigator:render() + return Roact.createElement("Frame") + end + + ChildNavigator.router = ChildRouter + + local router = StackRouter({ + { Foo = { screen = ChildNavigator } }, + { Bar = { screen = function() return Roact.createElement("Frame") end } }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + local state2 = router.getStateForAction({ + type = StackActions.Reset, + key = nil, + actions = { + { + type = NavigationActions.Navigate, + routeName = "Foo", + immediate = true, + }, + }, + index = 1, + }, state) + + expect(state2 and state2.index).to.equal(1) + expect(state2 and state2.routes[1].routeName).to.equal("Foo") + expect(state2 and state2.routes[1].routes[1].routeName).to.equal("baz") + end) + + it("Handles the reset action with a key", function() + local ChildRouter = StackRouter({ + { baz = { screen = function() return Roact.createElement("Frame") end } }, + }) + local ChildNavigator = Roact.Component:extend("ChildNavigator") + function ChildNavigator:render() + return Roact.createElement("Frame") + end + + ChildNavigator.router = ChildRouter + + local router = StackRouter({ + { Foo = { screen = ChildNavigator } }, + { Bar = { screen = function() return Roact.createElement("Frame") end } }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + local state2 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Foo", + immediate = true, + action = { + type = NavigationActions.Navigate, + routeName = "baz", + immediate = true, + }, + }, state) + local state3 = router.getStateForAction({ + type = StackActions.Reset, + key = "Init", + actions = { + { + type = NavigationActions.Navigate, + routeName = "Foo", + immediate = true, + }, + }, + index = 1, + }, state2) + local state4 = router.getStateForAction({ + type = StackActions.Reset, + key = nil, + actions = { + { + type = NavigationActions.Navigate, + routeName = "Bar", + immediate = true, + }, + }, + index = 1, + }, state3) + + expect(state4 and state4.index).to.equal(1) + expect(state4 and state4.routes[1].routeName).to.equal("Bar") + end) + + it("Handles the navigate action with params and nested StackRouter", function() + local ChildNavigator = Roact.Component:extend("ChildNavigator") + function ChildNavigator:render() + return Roact.createElement("Frame") + end + + ChildNavigator.router = StackRouter({ + { Baz = { screen = function() return Roact.createElement("Frame") end } }, + }) + + local router = StackRouter({ + { Foo = { screen = function() return Roact.createElement("Frame") end } }, + { Bar = { screen = ChildNavigator } }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + local state2 = router.getStateForAction( + { + type = NavigationActions.Navigate, + immediate = true, + routeName = "Bar", + params = { foo = "42" }, + }, + state + ) + + expectDeepEqual(state2 and state2.routes[2].params, { foo = "42" }) + expect(state2 and state2.routes[2].routes[1]).to.be.ok() + expect(state2.routes[2].routes[1].routeName).to.equal("Baz") + expectDeepEqual(state2.routes[2].routes[1].params, { foo = "42" }) + end) + + it("Navigate action to previous nested StackRouter causes isTransitioning start", function() + local ChildNavigator = Roact.Component:extend("ChildNavigator") + function ChildNavigator:render() + return Roact.createElement("Frame") + end + + ChildNavigator.router = StackRouter({ + { Baz = { screen = function() return Roact.createElement("Frame") end } }, + }) + + local router = StackRouter({ + { Bar = { screen = ChildNavigator } }, + { Foo = { screen = function() return Roact.createElement("Frame") end } }, + }) + local state = router.getStateForAction( + { + type = NavigationActions.Navigate, + immediate = true, + routeName = "Foo", + }, + router.getStateForAction({ type = NavigationActions.Init }) + ) + local state2 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Baz" }, + state + ) + + expect(state2.index).to.equal(1) + expect(state2.isTransitioning).to.equal(true) + end) + + it("Handles the navigate action with params and nested StackRouter as a first action", function() + local state = TestStackRouter.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "main", + params = { code = "test", foo = "bar" }, + action = { + type = NavigationActions.Navigate, + routeName = "profile", + params = { id = "4", code = "test", foo = "bar" }, + action = { + type = NavigationActions.Navigate, + routeName = "list", + params = { id = "10259959195", code = "test", foo = "bar" }, + }, + }, + }) + + expectDeepEqual(state, { + index = 1, + isTransitioning = false, + key = "StackRouterRoot", + routes = { + { + index = 1, + isTransitioning = false, + key = "id-2", + params = { code = "test", foo = "bar" }, + routeName = "main", + routes = { + { + index = 1, + isTransitioning = false, + key = "id-1", + params = { code = "test", foo = "bar", id = "4" }, + routeName = "profile", + routes = { + { + key = "id-0", + params = { + code = "test", + foo = "bar", + id = "10259959195", + }, + routeName = "list", + type = nil, + }, + }, + }, + }, + }, + }, + }) + + local state2 = TestStackRouter.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "main", + params = { code = "", foo = "bar" }, + action = { + type = NavigationActions.Navigate, + routeName = "profile", + params = { id = "4", code = "", foo = "bar" }, + action = { + type = NavigationActions.Navigate, + routeName = "list", + params = { id = "10259959195", code = "", foo = "bar" }, + }, + }, + }) + + expectDeepEqual(state2, { + index = 1, + isTransitioning = false, + key = "StackRouterRoot", + routes = { + { + index = 1, + isTransitioning = false, + key = "id-5", + params = { code = "", foo = "bar" }, + routeName = "main", + routes = { + { + index = 1, + isTransitioning = false, + key = "id-4", + params = { code = "", foo = "bar", id = "4" }, + routeName = "profile", + routes = { + { + key = "id-3", + params = { + code = "", + foo = "bar", + id = "10259959195", + }, + routeName = "list", + type = nil, + }, + }, + }, + }, + }, + }, + }) + end) + + it("Handles deep navigate completion action", function() + local LeafScreen = function() + return Roact.createElement("Frame") + end + local FooScreen = Roact.Component:extend("FooScreen") + function FooScreen:render() + return Roact.createElement("Frame") + end + + FooScreen.router = StackRouter({ + { Boo = { path = "boo", screen = LeafScreen } }, + { Baz = { path = "baz/:bazId", screen = LeafScreen } }, + }) + + local router = StackRouter({ + { Foo = { screen = FooScreen } }, + { Bar = { screen = LeafScreen } }, + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + + expect(state and state.index).to.equal(1) + expect(state and state.routes[1].routeName).to.equal("Foo") + + local key = state and state.routes[1].key + local state2 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Baz" }, + state + ) + + expect(state2.index).to.equal(1) + expect(state2.isTransitioning).to.equal(false) + expect(state2.routes[1].index).to.equal(2) + expect(state2.routes[1].isTransitioning).to.equal(true) + expect(not not key).to.equal(true) + + local state3 = router.getStateForAction({ + type = StackActions.CompleteTransition, + toChildKey = state2.routes[1].routes[2].key, + }, state2) + + expect(state3 and state3.index).to.equal(1) + expect(state3 and state3.isTransitioning).to.equal(false) + expect(state3 and state3.routes[1].index).to.equal(2) + expect(state3 and state3.routes[1].isTransitioning).to.equal(false) + end) + + it("order of handling navigate action is correct for nested stackrouters", function() + local Screen = function() + return Roact.createElement("Frame") + end + local NestedStack = Roact.Component:extend("NestedStack") + function NestedStack:render() + return Roact.createElement("Frame") + end + local nestedRouter = StackRouter({ + { Foo = Screen }, + { Bar = Screen }, + }) + + NestedStack.router = nestedRouter + + local router = StackRouter({ + { NestedStack = NestedStack }, + { Bar = Screen }, + { Baz = Screen }, + }, { + initialRouteName = "Baz", + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + + expect(state.routes[state.index].routeName).to.equal("Baz") + + local state2 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Bar" }, + state + ) + + expect(state2.routes[state2.index].routeName).to.equal("Bar") + + local state3 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Baz", + }, state2) + + expect(state3.routes[state3.index].routeName).to.equal("Baz") + + local state4 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Foo", + }, state3) + local activeState4 = state4.routes[state4.index] + + expect(activeState4.routeName).to.equal("NestedStack") + expect(activeState4.routes[activeState4.index].routeName).to.equal("Foo") + + local state5 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Bar" }, + state4 + ) + local activeState5 = state5.routes[state5.index] + + expect(activeState5.routeName).to.equal("NestedStack") + expect(activeState5.routes[activeState5.index].routeName).to.equal("Bar") + end) + + it("order of handling navigate action is correct for nested stackrouters 2", function() + local function Screen() + return Roact.createElement("Frame") + end + local NestedStack = Roact.Component:extend("NestedStack") + function NestedStack:render() + return Roact.createElement("Frame") + end + local OtherNestedStack = Roact.Component:extend("OtherNestedStack") + function OtherNestedStack:render() + return Roact.createElement("Frame") + end + local nestedRouter = StackRouter({ + { Foo = Screen }, + { Bar = Screen }, + }) + local otherNestedRouter = StackRouter({ + { Foo = Screen }, + }) + + NestedStack.router = nestedRouter + OtherNestedStack.router = otherNestedRouter + + local router = StackRouter({ + { NestedStack = NestedStack }, + { OtherNestedStack = OtherNestedStack }, + { Bar = Screen }, + }, { + initialRouteName = "OtherNestedStack", + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + + expect(state.routes[state.index].routeName).to.equal("OtherNestedStack") + + local state2 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Bar" }, + state + ) + + expect(state2.routes[state2.index].routeName).to.equal("Bar") + + local state3 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "NestedStack" }, + state2 + ) + local state4 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Bar" }, + state3 + ) + local activeState4 = state4.routes[state4.index] + + expect(activeState4.routeName).to.equal("NestedStack") + expect(activeState4.routes[activeState4.index].routeName).to.equal("Bar") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/SwitchActions.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/SwitchActions.roblox.spec.lua new file mode 100644 index 0000000..4885d81 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/SwitchActions.roblox.spec.lua @@ -0,0 +1,31 @@ +return function() + local SwitchActions = require(script.Parent.Parent.SwitchActions) + + it("throws when indexing an unknown field", function() + expect(function() + return SwitchActions.foo + end).to.throw("\"foo\" is not a valid member of SwitchActions") + end) + + describe("token tests", function() + it("returns same object for each token for multiple calls", function() + expect(SwitchActions.JumpTo).to.equal(SwitchActions.JumpTo) + end) + + it("should return matching string names for symbols", function() + expect(tostring(SwitchActions.JumpTo)).to.equal("JUMP_TO") + end) + end) + + describe("creators", function() + it("returns a JumpTo action for jumpTo()", function() + local popTable = SwitchActions.jumpTo({ + routeName = "foo", + }) + + expect(popTable.type).to.equal(SwitchActions.JumpTo) + expect(popTable.routeName).to.equal("foo") + expect(popTable.preserveFocus).to.equal(true) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/SwitchRouter.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/SwitchRouter.roblox.spec.lua new file mode 100644 index 0000000..afc2faf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/SwitchRouter.roblox.spec.lua @@ -0,0 +1,589 @@ +return function() + local Root = script.Parent.Parent.Parent + local SwitchRouter = require(script.Parent.Parent.SwitchRouter) + local NavigationActions = require(Root.NavigationActions) + local BackBehavior = require(Root.BackBehavior) + + it("should be a function", function() + expect(type(SwitchRouter)).to.equal("function") + end) + + it("should throw when passed a non-table", function() + expect(function() + SwitchRouter(5) + end).to.throw("routeConfigs must be an array table") + end) + + it("should throw if initialRouteName is not found in routes table", function() + expect(function() + SwitchRouter({ + { Foo = function() end }, + { Bar = function() end }, + }, { + initialRouteName = "MyRoute", + }) + end).to.throw("Invalid initialRouteName 'MyRoute'. Should be one of \"Foo\", \"Bar\"") + end) + + it("should expose childRouters as a member", function() + local router = SwitchRouter({ + { + Foo = { + screen = { + render = function() end, + router = "A", + }, + }, + }, + { + Bar = { + screen = { + render = function() end, + router = "B", + }, + }, + }, + }) + + expect(router.childRouters.Foo).to.equal("A") + expect(router.childRouters.Bar).to.equal("B") + end) + + describe("getScreenOptions tests", function() + it("should correctly configure default screen options", function() + local router = SwitchRouter({ + { + Foo = { + screen = { + render = function() end, + } + } + }, + }, { + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("FooTitle") + end) + + it("should correctly configure route-specified screen options", function() + local router = SwitchRouter({ + { + Foo = { + screen = { + render = function() end, + }, + navigationOptions = { title = "RouteFooTitle" }, + } + }, + }, { + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("RouteFooTitle") + end) + + it("should correctly configure component-specified screen options", function() + local router = SwitchRouter({ + { + Foo = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentFooTitle" }, + }, + } + }, + }, { + defaultNavigationOptions = { + title = "FooTitle", + }, + }) + + local screenOptions = router.getScreenOptions({ + state = { + routeName = "Foo", + } + }) + + expect(screenOptions.title).to.equal("ComponentFooTitle") + end) + end) + + describe("getActionCreators tests", function() + it("should return empty action creators table if none are provided", function() + local router = SwitchRouter({ + { Foo = { render = function() end } }, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + + local fieldCount = 0 + for _ in pairs(actionCreators) do + fieldCount = fieldCount + 1 + end + + expect(fieldCount).to.equal(0) + end) + + it("should call custom action creators function if provided", function() + local router = SwitchRouter({ + { Foo = { render = function() end } }, + }, { + getCustomActionCreators = function() + return { a = 1 } + end, + }) + + local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key") + expect(actionCreators.a).to.equal(1) + end) + end) + + describe("getComponentForState tests", function() + it("should return component matching requested state", function() + local testComponent = function() end + local router = SwitchRouter({ + { Foo = { screen = testComponent } }, + }) + + local component = router.getComponentForState({ + routes = { + { routeName = "Foo" }, + }, + index = 1, + }) + expect(component).to.equal(testComponent) + end) + + it("should throw if there is no route matching active index", function() + local router = SwitchRouter({ + { Foo = { screen = function() end } }, + }) + + local message = "There is no route defined for index '2'. " .. + "Check that you passed in a navigation state with a " .. + "valid tab/screen index." + expect(function() + router.getComponentForState({ + routes = { + Foo = { screen = function() end }, + }, + index = 2, + }) + end).to.throw(message) + end) + + it("should descend child router for requested route", function() + local testComponent = function() end + local childRouter = SwitchRouter({ + { Bar = { screen = testComponent } }, + }) + + local router = SwitchRouter({ + { + Foo = { + screen = { + render = function() end, + router = childRouter, + } + }, + }, + }) + + local component = router.getComponentForState({ + routes = { + { + routeName = "Foo", + routes = { -- Child router's routes + { routeName = "Bar" }, + }, + index = 1 + }, + }, + index = 1, + }) + expect(component).to.equal(testComponent) + end) + end) + + describe("getComponentForRouteName tests", function() + it("should return a component that matches the given route name", function() + local testComponent = function() end + local router = SwitchRouter({ + { Foo = { screen = testComponent } }, + }) + + local component = router.getComponentForRouteName("Foo") + expect(component).to.equal(testComponent) + end) + end) + + describe("getStateForAction tests", function() + it("should return initial state for init action", function() + local router = SwitchRouter({ + { Foo = { screen = function() end } }, + { Bar = { screen = function() end } }, + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(#state.routes).to.equal(2) + expect(state.routes[state.index].routeName).to.equal("Foo") + end) + + it("should adjust initial state index to match initialRouteName's index", function() + local router = SwitchRouter({ + { Foo = { screen = function() end } }, + { Bar = { screen = function() end } }, + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(state.routes[state.index].routeName).to.equal("Foo") + + local router2 = SwitchRouter({ + { Foo = { screen = function() end } }, + { Bar = { screen = function() end } }, + }, { + initialRouteName = "Bar", + }) + + local state2 = router2.getStateForAction(NavigationActions.init(), nil) + expect(state2.routes[state2.index].routeName).to.equal("Bar") + end) + + it("should respect optional order property", function() + local router = SwitchRouter({ + { Foo = { screen = function() end } }, + { Bar = { screen = function() end } }, + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + expect(state.routes[1].routeName).to.equal("Foo") + expect(state.routes[2].routeName).to.equal("Bar") + end) + + it("should incorporate child router state", function() + local childRouter = SwitchRouter({ + { Bar = { screen = function() end } }, + }) + + local router = SwitchRouter({ + { + Foo = { + render = function() end, + router = childRouter, + }, + }, + { City = { screen = function() end } }, + }) + + local state = router.getStateForAction(NavigationActions.init(), nil) + local activeState = state.routes[state.index] + expect(activeState.routeName).to.equal("Foo") -- parent's tracking uses parent's route name + expect(activeState.routes[activeState.index].routeName).to.equal("Bar") + end) + + it("should let active child handle non-init action first", function() + local childRouter = SwitchRouter({ + { Bar = { screen = function() end } }, + { City = { screen = function() end } }, + }) + + local router = SwitchRouter({ + { + Foo = { + render = function() end, + router = childRouter, + }, + }, + { State = { render = function() end } }, + }) + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "City" })) + expect(state.routes[1].index).to.equal(2) + expect(state.index).to.equal(1) + end) + + it("should go back to initial route index if BackBehavior.InitialRoute", function() + local router = SwitchRouter({ + { Foo = { render = function() end } }, + { Bar = { render = function() end } }, + }, { + backBehavior = BackBehavior.InitialRoute, + }) + + local prevState = { + routes = { + { routeName = "Foo" }, + { routeName = "Bar" }, + }, + index = 2, + } + + local newState = router.getStateForAction(NavigationActions.back(), prevState) + expect(newState.index).to.equal(1) + end) + + it("should not change state on back action if BackBehavior.None", function() + local router = SwitchRouter({ + { Foo = { render = function() end } }, + { Bar = { render = function() end } }, + }) + + local prevState = { + routes = { + { routeName = "Foo" }, + { routeName = "Bar" }, + }, + index = 2, + } + + local newState = router.getStateForAction(NavigationActions.back(), prevState) + expect(newState).to.equal(prevState) + end) + + it("should change active route on navigate", function() + local router = SwitchRouter({ + { Foo = { render = function() end } }, + { Bar = { render = function() end } }, + }) + + local newState = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" })) + expect(newState.index).to.equal(2) + expect(newState.routes[newState.index].routeName).to.equal("Bar") + end) + + it("should pass sub-action to child router on navigate", function() + local childRouter = SwitchRouter({ + { City = { screen = function() end } }, + { State = { screen = function() end } }, + }) + + local router = SwitchRouter({ + { Foo = { render = function() end } }, + { Bar = { + render = function() end, + router = childRouter, + }, + }, + }) + + local newState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + action = NavigationActions.navigate({ routeName = "State" }), + })) + + local activeRoute = newState.routes[newState.index] + expect(activeRoute.routeName).to.equal("Bar") + expect(activeRoute.routes[activeRoute.index].routeName).to.equal("State") + end) + + it("should return initial state if navigating to active child without previous state", function() + local childRouter = SwitchRouter({ + { Bar = { screen = function() end } }, + }) + + local router = SwitchRouter({ + { + Foo = { + render = function() end, + router = childRouter, + }, + }, + { City = { render = function() end } }, + }) + + local newState = router.getStateForAction(NavigationActions.navigate({ + routeName = "Foo", + })) + + expect(newState.routes[newState.index].routeName).to.equal("Foo") + end) + + it("should reset state for deactivated route by default", function() + local router = SwitchRouter({ + { Foo = { render = function() end } }, + { Bar = { render = function() end } }, + }) + + local initialState = { + routes = { + { routeName = "Foo", params = { a = 1 } }, + { routeName = "Bar" }, + }, + index = 1, + } + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(state.routes[1].params).to.equal(nil) -- should be empty + end) + + it("should not reset state for deactivated route if resetOnBlur is false", function() + local router = SwitchRouter({ + { Foo = { render = function() end } }, + { Bar = { render = function() end } }, + }, { + resetOnBlur = false, + }) + + local testParams = { a = 1 } + + local initialState = { + routes = { + { routeName = "Foo", params = testParams }, + { routeName = "Bar" }, + }, + index = 1, + } + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(state.routes[1].params).to.equal(testParams) + end) + + it("should set params on route for setParams action", function() + local router = SwitchRouter({ + { Foo = { render = function() end } }, + { Bar = { render = function() end } }, + }) + + local newState = router.getStateForAction(NavigationActions.setParams({ + key = "Foo", -- By default, key == routeName + params = { a = 1 }, + })) + + expect(newState.routes[newState.index].params.a).to.equal(1) + end) + + it("should preserve route configured params for child router", function() + local childRouter = SwitchRouter({ + { + Bar = { + screen = function() end, + params = { a = 2 }, + }, + }, + }) + + local router = SwitchRouter({ + { + Foo = { + render = function() end, + params = { a = 1 }, + router = childRouter, + }, + }, + { City = { render = function() end } }, + }) + + local state = router.getStateForAction(NavigationActions.init()) + expect(state.routes[state.index].params.a).to.equal(1) + end) + + it("should merge initialRouteParams with initial route's own params", function() + local router = SwitchRouter({ + { + Foo = { + render = function() end, + params = { a = 1 }, + }, + }, + { + Bar = { + render = function() end, + params = { a = 1 }, + }, + }, + }, { + initialRouteParams = { a = 2, b = 3 }, + }) + + local state = router.getStateForAction(NavigationActions.init()) + expect(state.routes[1].params.a).to.equal(2) + expect(state.routes[1].params.b).to.equal(3) + expect(state.routes[2].params.a).to.equal(1) + expect(state.routes[2].params.b).to.equal(nil) + end) + + it("should merge init action params with initial route's own params and initialRouteParams", function() + local router = SwitchRouter({ + { + Foo = { render = function() end, params = { a = 1 } } + }, + }, { + initialRouteParams = { c = 3 }, + }) + + local state = router.getStateForAction(NavigationActions.init({ params = { b = 2 } })) + expect(state.routes[1].params.a).to.equal(1) + expect(state.routes[1].params.b).to.equal(2) + expect(state.routes[1].params.c).to.equal(3) + end) + + it("should merge navigate action params for child router", function() + local childRouter = SwitchRouter({ + { + Bar = { + screen = function() end, + params = { a = 2 }, + }, + }, + }) + + local router = SwitchRouter({ + { + Foo = { + render = function() end, + router = childRouter, + }, + }, + }) + + local state = router.getStateForAction(NavigationActions.navigate({ + routeName = "Bar", + params = { b = 3 }, + })) + + expect(state.routes[1].routes[1].params.a).to.equal(2) + expect(state.routes[1].routes[1].params.b).to.equal(3) + end) + + it("should propagate a child router getStateForAction failure to caller", function() + local childRouter = SwitchRouter({ + { Bar = { screen = function() end } }, + }) + + local router = SwitchRouter({ + { + Foo = { + render = function() end, + router = childRouter, + }, + }, + }) + + -- need to properly initialize state because we're being abusive of getStateForAction + local initialState = router.getStateForAction(NavigationActions.init()) + + childRouter.getStateForAction = function() return nil end + + local state = router.getStateForAction(NavigationActions.navigate("Bar"), initialState) + expect(state).to.equal(nil) + end) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/SwitchRouter.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/SwitchRouter.spec.lua new file mode 100644 index 0000000..c263da4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/SwitchRouter.spec.lua @@ -0,0 +1,463 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/fcd7d83c4c33ad1fa508c8cfe687d2fa259bfc2c/packages/core/src/routers/__tests__/SwitchRouter.test.js + +return function() + local Root = script.Parent.Parent.Parent + local Packages = Root.Parent + local Roact = require(Packages.Roact) + local Cryo = require(Packages.Cryo) + local StackRouter = require(script.Parent.Parent.StackRouter) + local SwitchRouter = require(script.Parent.Parent.SwitchRouter) + local NavigationActions = require(Root.NavigationActions) + local BackBehavior = require(Root.BackBehavior) + local expectDeepEqual = require(Root.utils.expectDeepEqual) + local getRouterTestHelper = require(script.Parent.routerTestHelper) + + local function getExampleRouter(config) + config = config or {} + + local function PlainScreen() + return Roact.createElement("Frame") + end + local StackA = Roact.Component:extend("StackA") + function StackA:render() + return Roact.createElement("Frame") + end + local StackB = Roact.Component:extend("StackB") + function StackB:render() + return Roact.createElement("Frame") + end + local StackC = Roact.Component:extend("StackC") + function StackC:render() + return Roact.createElement("Frame") + end + + StackA.router = StackRouter({ + {A1 = PlainScreen}, + {A2 = PlainScreen} + }) + StackB.router = StackRouter({ + {B1 = PlainScreen}, + {B2 = PlainScreen} + }) + StackC.router = StackRouter({ + {C1 = PlainScreen}, + {C2 = PlainScreen} + }) + + local router = SwitchRouter({ + {A = {screen = StackA, path = ""}}, + {B = {screen = StackB, path = "great/path"}}, + {C = {screen = StackC, path = "pathC"}} + }, Cryo.Dictionary.join( + {initialRouteName = "A"}, + config + )) + + return router + end + + describe("SwitchRouter", function() + it("resets the route when unfocusing a tab by default", function() + local helper = getRouterTestHelper(getExampleRouter()) + local navigateTo = helper.navigateTo + local getState = helper.getState + + navigateTo("A2") + expect(getState().routes[1].index).to.equal(2) + expect(#getState().routes[1].routes).to.equal(2) + + navigateTo("B") + expect(getState().routes[1].index).to.equal(1) + expect(#getState().routes[1].routes).to.equal(1) + end) + + it("does not reset the route on unfocus if resetOnBlur is false", function() + local helper = getRouterTestHelper(getExampleRouter({resetOnBlur = false})) + local navigateTo = helper.navigateTo + local getState = helper.getState + + navigateTo("A2") + expect(getState().routes[1].index).to.equal(2) + expect(#getState().routes[1].routes).to.equal(2) + navigateTo("B") + expect(getState().routes[1].index).to.equal(2) + expect(#getState().routes[1].routes).to.equal(2) + end) + + it("ignores back by default", function() + local helper = getRouterTestHelper(getExampleRouter()) + local jumpTo = helper.jumpTo + local back = helper.back + local getState = helper.getState + + jumpTo("B") + expect(getState().index).to.equal(2) + + back() + expect(getState().index).to.equal(2) + end) + + it("handles initialRoute backBehavior", function() + local helper = getRouterTestHelper(getExampleRouter({ + backBehavior = BackBehavior.InitialRoute, + initialRouteName = "B", + })) + local jumpTo = helper.jumpTo + local back = helper.back + local getState = helper.getState + + expect(getState().routeKeyHistory).to.equal(nil) + expect(getState().index).to.equal(2) + + jumpTo("C") + expect(getState().index).to.equal(3) + + jumpTo("A") + expect(getState().index).to.equal(1) + + back() + expect(getState().index).to.equal(2) + + back() + expect(getState().index).to.equal(2) + end) + + it("handles order backBehavior", function() + local helper = getRouterTestHelper(getExampleRouter({ + backBehavior = BackBehavior.Order, + })) + local navigateTo = helper.navigateTo + local back = helper.back + local getState = helper.getState + + expect(getState().routeKeyHistory).to.equal(nil) + + navigateTo("C") + expect(getState().index).to.equal(3) + + back() + expect(getState().index).to.equal(2) + + back() + expect(getState().index).to.equal(1) + + back() + expect(getState().index).to.equal(1) + end) + + it("handles history backBehavior", function() + local helper = getRouterTestHelper(getExampleRouter({ + backBehavior = BackBehavior.History, + })) + local navigateTo = helper.navigateTo + local back = helper.back + local getState = helper.getState + + expectDeepEqual(getState().routeKeyHistory, {"A"}) + + navigateTo("B") + expect(getState().index).to.equal(2) + expectDeepEqual(getState().routeKeyHistory, {"A", "B"}) + + navigateTo("A") + expect(getState().index).to.equal(1) + expectDeepEqual(getState().routeKeyHistory, {"B", "A"}) + + navigateTo("C") + expect(getState().index).to.equal(3) + expectDeepEqual(getState().routeKeyHistory, {"B", "A", "C"}) + + navigateTo("A") + expect(getState().index).to.equal(1) + expectDeepEqual(getState().routeKeyHistory, {"B", "C", "A"}) + + back() + expect(getState().index).to.equal(3) + expectDeepEqual(getState().routeKeyHistory, {"B", "C"}) + + back() + expect(getState().index).to.equal(2) + expectDeepEqual(getState().routeKeyHistory, {"B"}) + + back() + expect(getState().index).to.equal(2) + expectDeepEqual(getState().routeKeyHistory, {"B"}) + end) + + it("handles history backBehavior without popping routeKeyHistory when child handles action", function() + local helper = getRouterTestHelper(getExampleRouter({ + backBehavior = BackBehavior.History, + })) + local navigateTo = helper.navigateTo + local back = helper.back + local getState = helper.getState + local getSubState = helper.getSubState + + expectDeepEqual(getState().routeKeyHistory, { + "A", + }) + + navigateTo("B") + expect(getState().index).to.equal(2) + expectDeepEqual(getState().routeKeyHistory, { + "A", + "B", + }) + + navigateTo("B2") + expect(getState().index).to.equal(2) + expectDeepEqual(getState().routeKeyHistory, { + "A", + "B", + }) + expect(getSubState(2).routeName).to.equal("B2") + + back() + expect(getState().index).to.equal(2) + -- "B" should not be popped when the child handles the back action + expectDeepEqual(getState().routeKeyHistory, { + "A", + "B", + }) + expect(getSubState(2).routeName).to.equal("B1") + end) + + it("handles back and does not apply back action to inactive child", function() + local helper = getRouterTestHelper(getExampleRouter({ + backBehavior = "initialRoute", + resetOnBlur = false, -- Don't erase the state of substack B when we switch back to A + })) + local navigateTo = helper.navigateTo + local back = helper.back + local getSubState = helper.getSubState + + expect(getSubState(1).routeName).to.equal("A") + + navigateTo("B") + navigateTo("B2") + expect(getSubState(1).routeName).to.equal("B") + expect(getSubState(2).routeName).to.equal("B2") + + navigateTo("A") + expect(getSubState(1).routeName).to.equal("A") + + -- The back action should not switch to B. It should stay on A + back(nil) + expect(getSubState(1).routeName).to.equal("A") + end) + + it("handles pop and does not apply pop action to inactive child", function() + local helper = getRouterTestHelper(getExampleRouter({ + backBehavior = "initialRoute", + resetOnBlur = false, -- Don't erase the state of substack B when we switch back to A + })) + local navigateTo = helper.navigateTo + local pop = helper.pop + local getSubState = helper.getSubState + + expect(getSubState(1).routeName).to.equal("A") + + navigateTo("B") + navigateTo("B2") + expect(getSubState(1).routeName).to.equal("B") + expect(getSubState(2).routeName).to.equal("B2") + + navigateTo("A") + expect(getSubState(1).routeName).to.equal("A") + + -- The pop action should not switch to B. It should stay on A + pop() + expect(getSubState(1).routeName).to.equal("A") + end) + + it("handles popToTop and does not apply popToTop action to inactive child", function() + local helper = getRouterTestHelper(getExampleRouter({ + backBehavior = "initialRoute", + resetOnBlur = false, -- Don't erase the state of substack B when we switch back to A + })) + local navigateTo = helper.navigateTo + local popToTop = helper.popToTop + local getSubState = helper.getSubState + + expect(getSubState(1).routeName).to.equal("A") + + navigateTo("B") + navigateTo("B2") + expect(getSubState(1).routeName).to.equal("B") + expect(getSubState(2).routeName).to.equal("B2") + + navigateTo("A") + expect(getSubState(1).routeName).to.equal("A") + + -- The popToTop action should not switch to B. It should stay on A + popToTop() + expect(getSubState(1).routeName).to.equal("A") + end) + + it("handles back and does switch to inactive child with matching key", function() + local helper = getRouterTestHelper(getExampleRouter({ + backBehavior = "initialRoute", + resetOnBlur = false, -- Don't erase the state of substack B when we switch back to A + })) + local navigateTo = helper.navigateTo + local back = helper.back + local getSubState = helper.getSubState + + expect(getSubState(1).routeName).to.equal("A") + + navigateTo("B") + navigateTo("B2") + expect(getSubState(1).routeName).to.equal("B") + expect(getSubState(2).routeName).to.equal("B2") + + local b2Key = getSubState(2).key + + navigateTo("A") + expect(getSubState(1).routeName).to.equal("A") + + -- The back action should switch to B and go back from B2 to B1 + back(b2Key) + expect(getSubState(1).routeName).to.equal("B") + expect(getSubState(2).routeName).to.equal("B1") + end) + + it("handles nested actions", function() + local helper = getRouterTestHelper(getExampleRouter()) + local navigateTo = helper.navigateTo + local getSubState = helper.getSubState + + navigateTo("B", { + action = { + type = NavigationActions.Navigate, + routeName = "B2", + }, + }) + expect(getSubState(1).routeName).to.equal("B") + expect(getSubState(2).routeName).to.equal("B2") + end) + + it("handles nested actions and params simultaneously", function() + local helper = getRouterTestHelper(getExampleRouter()) + local navigateTo = helper.navigateTo + local getSubState = helper.getSubState + + local params1 = { foo = "bar" } + local params2 = { bar = "baz" } + + navigateTo("B", { + params = params1, + action = { + type = NavigationActions.Navigate, + routeName = "B2", + params = params2, + }, + }) + expect(getSubState(1).routeName).to.equal("B") + expectDeepEqual(getSubState(1).params, params1) + expect(getSubState(2).routeName).to.equal("B2") + expectDeepEqual(getSubState(2).params, params2) + end) + + it("order of handling navigate action is correct for nested switchrouters", function() + -- router = switch({ Nested: switch({ Foo, Bar }), Other: switch({ Foo }), Bar }) + -- if we are focused on Other and navigate to Bar, what should happen? + + local function Screen() + return Roact.createElement("Frame") + end + local NestedSwitch = Roact.Component:extend("NestedSwitch") + function NestedSwitch:render() + return Roact.createElement("Frame") + end + local OtherNestedSwitch = Roact.Component:extend("OtherNestedSwitch") + function OtherNestedSwitch:render() + return Roact.createElement("Frame") + end + + local nestedRouter = SwitchRouter({ + {Foo = Screen}, + {Bar = Screen}, + }) + local otherNestedRouter = SwitchRouter({ + {Foo = Screen}, + }) + + NestedSwitch.router = nestedRouter + OtherNestedSwitch.router = otherNestedRouter + + local router = SwitchRouter({ + {NestedSwitch = NestedSwitch}, + {OtherNestedSwitch = OtherNestedSwitch}, + {Bar = Screen} + }, { initialRouteName = "OtherNestedSwitch" }) + + local helper = getRouterTestHelper(router) + local navigateTo = helper.navigateTo + local getSubState = helper.getSubState + + expect(getSubState(1).routeName).to.equal("OtherNestedSwitch") + + navigateTo("Bar") + expect(getSubState(1).routeName).to.equal("Bar") + + navigateTo("NestedSwitch") + navigateTo("Bar") + + expect(getSubState(1).routeName).to.equal("NestedSwitch") + expect(getSubState(2).routeName).to.equal("Bar") + end) + + -- https://github.com/react-navigation/react-navigation.github.io/issues/117#issuecomment-385597628 + it("order of handling navigate action is correct for nested stackrouters", function() + local function Screen() + return Roact.createElement("Frame") + end + local MainStack = Roact.Component:extend("MainStack") + function MainStack:render() + return Roact.createElement("Frame") + end + local LoginStack = Roact.Component:extend("LoginStack") + function LoginStack:render() + return Roact.createElement("Frame") + end + + MainStack.router = StackRouter({ + {Home = Screen}, + {Profile = Screen} + }) + LoginStack.router = StackRouter({ + {Form = Screen}, + {ForgotPassword = Screen} + }) + + local router = SwitchRouter({ + {Home = Screen}, + {Login = LoginStack}, + {Main = MainStack} + },{ initialRouteName = "Login" }) + + local helper = getRouterTestHelper(router) + local navigateTo = helper.navigateTo + local getSubState = helper.getSubState + + expect(getSubState(1).routeName).to.equal("Login") + + navigateTo("Home") + expect(getSubState(1).routeName).to.equal("Home") + end) + + it("does not error for a nested navigate action in an uninitialized history router", function() + local helper = getRouterTestHelper(getExampleRouter({ + backBehavior = "history", + }), {skipInitializeState = true}) + local navigateTo = helper.navigateTo + local getSubState = helper.getSubState + + navigateTo("B", { + action = NavigationActions.navigate({ routeName = "B2" }), + }) + expect(getSubState(1).routeName).to.equal("B") + expect(getSubState(2).routeName).to.equal("B2") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/TabRouter.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/TabRouter.roblox.spec.lua new file mode 100644 index 0000000..fd70c3f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/TabRouter.roblox.spec.lua @@ -0,0 +1,56 @@ +return function() + local TabRouter = require(script.Parent.Parent.TabRouter) + local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) + + -- NOTE: Most functional tests are covered by SwitchRouter.spec.lua + -- We just check that we can mount a basic case, and check that our custom + -- defaults for resetOnBlur and backBehavior work as expected. + + it("should return a component that matches the given route name", function() + local testComponent = function() end + local router = TabRouter({ + { Foo = { screen = testComponent } }, + }) + + local component = router.getComponentForRouteName("Foo") + expect(component).to.equal(testComponent) + end) + + it("should not reset state for deactivated route", function() + local router = TabRouter({ + { Foo = { render = function() end } }, + { Bar = { render = function() end } }, + }) + + local testParams = { a = 1 } + + local initialState = { + routes = { + { routeName = "Foo", params = testParams }, + { routeName = "Bar" }, + }, + index = 1, + } + + local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState) + expect(state.routes[1].params).to.equal(testParams) + end) + + it("should go back to initial route index", function() + local router = TabRouter({ + { Foo = { render = function() end } }, + { Bar = { render = function() end } }, + }) + + local prevState = { + routes = { + { routeName = "Foo" }, + { routeName = "Bar" }, + }, + index = 2, + } + + local newState = router.getStateForAction(NavigationActions.back(), prevState) + expect(newState.index).to.equal(1) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/TabRouter.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/TabRouter.spec.lua new file mode 100644 index 0000000..1f51897 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/TabRouter.spec.lua @@ -0,0 +1,895 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/72e8160537954af40f1b070aa91ef45fc02bba69/packages/core/src/routers/__tests__/TabRouter.test.js + +return function() + local Root = script.Parent.Parent.Parent + local Packages = Root.Parent + + local Roact = require(Packages.Roact) + local BackBehavior = require(Root.BackBehavior) + local NavigationActions = require(Root.NavigationActions) + local expectDeepEqual = require(Root.utils.expectDeepEqual) + + local TabRouter = require(script.Parent.Parent.TabRouter) + + local BareLeafRouteConfig = { + screen = function() + return Roact.createElement("Frame") + end, + } + + local INIT_ACTION = { type = NavigationActions.Init } + + describe("TabRouter", function() + it("Handles basic tab logic", function() + local function ScreenA() + return Roact.createElement("Frame") + end + local function ScreenB() + return Roact.createElement("Frame") + end + local router = TabRouter({ + { Foo = { screen = ScreenA } }, + { Bar = { screen = ScreenB } }, + }) + local state = router.getStateForAction({ + type = NavigationActions.Init, + }) + local expectedState = { + index = 1, + routes = { + { key = "Foo", routeName = "Foo" }, + { key = "Bar", routeName = "Bar" }, + }, + } + + expectDeepEqual(state, expectedState) + + local state2 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Bar", + }, state) + local expectedState2 = { + index = 2, + routes = { + { key = "Foo", routeName = "Foo" }, + { key = "Bar", routeName = "Bar" }, + }, + } + + expectDeepEqual(state2, expectedState2) + expect(router.getComponentForState(expectedState)).to.equal(ScreenA) + expect(router.getComponentForState(expectedState2)).to.equal(ScreenB) + + local state3 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Bar", + }, state2) + + expect(state3).to.equal(nil) + end) + + it("Handles getScreen", function() + local function ScreenA() + return Roact.createElement("Frame") + end + local function ScreenB() + return Roact.createElement("Frame") + end + local router = TabRouter({ + { Foo = { getScreen = function() return ScreenA end } }, + { Bar = { getScreen = function() return ScreenB end } }, + }) + local state = router.getStateForAction({ + type = NavigationActions.Init, + }) + local expectedState = { + index = 1, + routes = { + { key = "Foo", routeName = "Foo" }, + { key = "Bar", routeName = "Bar" }, + }, + } + + expectDeepEqual(state, expectedState) + + local state2 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Bar", + }, state) + local expectedState2 = { + index = 2, + routes = { + { key = "Foo", routeName = "Foo" }, + { key = "Bar", routeName = "Bar" }, + }, + } + + expectDeepEqual(state2, expectedState2) + expect(router.getComponentForState(expectedState)).to.equal(ScreenA) + expect(router.getComponentForState(expectedState2)).to.equal(ScreenB) + + local state3 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Bar", + }, state2) + + expect(state3).to.equal(nil) + end) + + it("Can set the initial tab", function() + local router = TabRouter({ + { Foo = BareLeafRouteConfig }, + { Bar = BareLeafRouteConfig }, + }, { + initialRouteName = "Bar", + }) + local state = router.getStateForAction({ type = NavigationActions.Init }) + + expectDeepEqual(state, { + index = 2, + routes = { + { key = "Foo", routeName = "Foo" }, + { key = "Bar", routeName = "Bar" }, + }, + }) + end) + + it("Can set the initial params", function() + local router = TabRouter({ + { Foo = BareLeafRouteConfig }, + { Bar = BareLeafRouteConfig }, + }, { + initialRouteName = "Bar", + initialRouteParams = { name = "Qux" }, + }) + local state = router.getStateForAction({ + type = NavigationActions.Init, + }) + + expectDeepEqual(state, { + index = 2, + routes = { + { key = "Foo", routeName = "Foo" }, + { key = "Bar", routeName = "Bar", params = { name = "Qux" } }, + }, + }) + end) + + it("Handles the SetParams action", function() + local router = TabRouter({ + { + Foo = { + screen = function() return Roact.createElement("Frame") end, + }, + }, + { + Bar = { + screen = function() return Roact.createElement("Frame") end, + }, + }, + }) + local state2 = router.getStateForAction({ + type = NavigationActions.SetParams, + params = { name = "Qux" }, + key = "Foo", + }) + + expect(state2).to.be.a('table') + expectDeepEqual(state2.routes[1].params, { + name = "Qux", + }) + end) + + it("Handles the SetParams action for inactive routes", function() + local router = TabRouter({ + { + Foo = { + screen = function() return Roact.createElement("Frame") end, + }, + }, + { + Bar = { + screen = function() return Roact.createElement("Frame") end, + }, + }, + }, { + initialRouteName = "Bar", + }) + local initialState = { + index = 2, + routes = { + { + key = "RouteA", + routeName = "Foo", + params = { name = "InitialParam", other = "Unchanged" }, + }, + { key = "RouteB", routeName = "Bar", params = {} }, + }, + } + local state = router.getStateForAction({ + type = NavigationActions.SetParams, + params = { name = "NewParam" }, + key = "RouteA", + }, initialState) + + expect(state.index).to.equal(2) + expectDeepEqual(state.routes[1].params, { + name = "NewParam", + other = "Unchanged", + }) + end) + + it("getStateForAction returns null when navigating to same tab", function() + local router = TabRouter({ + { Foo = BareLeafRouteConfig }, + { Bar = BareLeafRouteConfig }, + }, { + initialRouteName = "Bar", + }) + local state = router.getStateForAction({ + type = NavigationActions.Init, + }) + local state2 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Bar", + }, state) + + expect(state2).to.equal(nil) + end) + + it("getStateForAction returns initial navigate", function() + local router = TabRouter({ + { Foo = BareLeafRouteConfig }, + { Bar = BareLeafRouteConfig }, + }) + local state = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Foo", + }) + + expect(state and state.index).to.equal(1) + end) + + -- deviation: Router.getActionForPathAndParams not implemented yet. + itSKIP("Handles nested tabs and nested actions", function() + local ChildTabNavigator = Roact.Component:extend("ChildTabNavigator") + + function ChildTabNavigator:render() + return Roact.createElement("Frame") + end + + ChildTabNavigator.router = TabRouter({ + { Foo = BareLeafRouteConfig }, + { Bar = BareLeafRouteConfig }, + }) + + local router = TabRouter({ + { Foo = BareLeafRouteConfig }, + { Baz = { screen = ChildTabNavigator } }, + { Boo = BareLeafRouteConfig }, + }) + local action = router.getActionForPathAndParams("Baz/Bar", { foo = "42" }) + local navAction = { + type = NavigationActions.Navigate, + routeName = "Baz", + params = { foo = "42" }, + action = { + type = NavigationActions.Navigate, + routeName = "Bar", + params = { foo = "42" }, + }, + } + + expectDeepEqual(action, navAction) + + local state = router.getStateForAction(navAction) + + expectDeepEqual(state, { + index = 2, + routes = { + { key = "Foo", routeName = "Foo" }, + { + index = 2, + key = "Baz", + routeName = "Baz", + params = { foo = "42" }, + routes = { + { key = "Foo", routeName = "Foo" }, + { + key = "Bar", + routeName = "Bar", + params = { foo = "42" }, + }, + }, + }, + { key = "Boo", routeName = "Boo" }, + }, + }) + end) + + it("Handles passing params to nested tabs", function() + local ChildTabNavigator = Roact.Component:extend("ChildTabNavigator") + + function ChildTabNavigator:render() + return Roact.createElement("Frame") + end + + ChildTabNavigator.router = TabRouter({ + { Boo = BareLeafRouteConfig }, + { Bar = BareLeafRouteConfig }, + }) + + local router = TabRouter({ + { Foo = BareLeafRouteConfig }, + { Baz = { screen = ChildTabNavigator } }, + }) + local navAction = { + type = NavigationActions.Navigate, + routeName = "Baz", + } + local state = router.getStateForAction(navAction) + + expectDeepEqual(state, { + index = 2, + routes = { + { key = "Foo", routeName = "Foo" }, + { + index = 1, + key = "Baz", + routeName = "Baz", + routes = { + { key = "Boo", routeName = "Boo" }, + { key = "Bar", routeName = "Bar" }, + }, + }, + }, + }) + + -- Ensure that navigating back and forth doesn't overwrite + state = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Bar" }, + state + ) + state = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Boo" }, + state + ) + + expectDeepEqual(state and state.routes[2], { + index = 1, + key = "Baz", + routeName = "Baz", + routes = { + { key = "Boo", routeName = "Boo" }, + { key = "Bar", routeName = "Bar" }, + }, + }) + end) + + it("Handles initial deep linking into nested tabs", function() + local ChildTabNavigator = Roact.Component:extend("ChildTabNavigator") + + function ChildTabNavigator:render() + return Roact.createElement("Frame") + end + + ChildTabNavigator.router = TabRouter({ + { Foo = BareLeafRouteConfig }, + { Bar = BareLeafRouteConfig }, + }) + + local router = TabRouter({ + { Foo = BareLeafRouteConfig }, + { Baz = { screen = ChildTabNavigator } }, + { Boo = BareLeafRouteConfig }, + }) + local state = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Bar", + }) + + expectDeepEqual(state, { + index = 2, + routes = { + { key = "Foo", routeName = "Foo" }, + { + index = 2, + key = "Baz", + routeName = "Baz", + routes = { + { key = "Foo", routeName = "Foo" }, + { key = "Bar", routeName = "Bar" }, + }, + }, + { key = "Boo", routeName = "Boo" }, + }, + }) + + local state2 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Foo" }, + state + ) + expectDeepEqual(state2, { + index = 2, + routes = { + { key = "Foo", routeName = "Foo" }, + { + index = 1, + key = "Baz", + routeName = "Baz", + routes = { + { key = "Foo", routeName = "Foo" }, + { key = "Bar", routeName = "Bar" }, + }, + }, + { key = "Boo", routeName = "Boo" }, + }, + }) + + local state3 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Foo" }, + state2 + ) + expect(state3).to.equal(nil) + end) + + it("Handles linking across of deeply nested tabs", function() + local ChildNavigator0 = Roact.Component:extend("ChildNavigator0") + function ChildNavigator0:render() + return Roact.createElement("Frame") + end + + ChildNavigator0.router = TabRouter({ + { Boo = BareLeafRouteConfig }, + { Baz = BareLeafRouteConfig }, + }) + + local ChildNavigator1 = Roact.Component:extend("ChildNavigator1") + function ChildNavigator1:render() + return Roact.createElement("Frame") + end + + ChildNavigator1.router = TabRouter({ + { Zoo = BareLeafRouteConfig }, + { Zap = BareLeafRouteConfig }, + }) + + local MidNavigator = Roact.Component:extend("MidNavigator") + function MidNavigator:render() + return Roact.createElement("Frame") + end + + MidNavigator.router = TabRouter({ + { Fee = { screen = ChildNavigator0 } }, + { Bar = { screen = ChildNavigator1 } }, + }) + + local router = TabRouter({ + { Foo = { screen = MidNavigator } }, + { Gah = BareLeafRouteConfig }, + }) + local state = router.getStateForAction(INIT_ACTION) + + expectDeepEqual(state, { + index = 1, + routes = { + { + index = 1, + key = "Foo", + routeName = "Foo", + routes = { + { + index = 1, + key = "Fee", + routeName = "Fee", + routes = { + { key = "Boo", routeName = "Boo" }, + { key = "Baz", routeName = "Baz" }, + }, + }, + { + index = 1, + key = "Bar", + routeName = "Bar", + routes = { + { key = "Zoo", routeName = "Zoo" }, + { key = "Zap", routeName = "Zap" }, + }, + }, + }, + }, + { key = "Gah", routeName = "Gah" }, + }, + }) + + local state2 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Zap" }, + state + ) + + expectDeepEqual(state2, { + index = 1, + routes = { + { + index = 2, + key = "Foo", + routeName = "Foo", + routes = { + { + index = 1, + key = "Fee", + routeName = "Fee", + routes = { + { key = "Boo", routeName = "Boo" }, + { key = "Baz", routeName = "Baz" }, + }, + }, + { + index = 2, + key = "Bar", + routeName = "Bar", + routes = { + { key = "Zoo", routeName = "Zoo" }, + { key = "Zap", routeName = "Zap" }, + }, + }, + }, + }, + { key = "Gah", routeName = "Gah" }, + }, + }) + + local state3 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "Zap" }, + state2 + ) + + expect(state3).to.equal(nil) + + local state4 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "Foo", + action = { + type = NavigationActions.Navigate, + routeName = "Bar", + action = { type = NavigationActions.Navigate, routeName = "Zap" }, + }, + }) + + expectDeepEqual(state4, { + index = 1, + routes = { + { + index = 2, + key = "Foo", + routeName = "Foo", + routes = { + { + index = 1, + key = "Fee", + routeName = "Fee", + routes = { + { key = "Boo", routeName = "Boo" }, + { key = "Baz", routeName = "Baz" }, + }, + }, + { + index = 2, + key = "Bar", + routeName = "Bar", + routes = { + { key = "Zoo", routeName = "Zoo" }, + { key = "Zap", routeName = "Zap" }, + }, + }, + }, + }, + { key = "Gah", routeName = "Gah" }, + }, + }) + end) + + -- deviation: Router.getActionForPathAndParams not implemented yet. + itSKIP("Handles path configuration", function() + local function ScreenA() + return Roact.createElement("Frame") + end + local function ScreenB() + return Roact.createElement("Frame") + end + local router = TabRouter({ + { Foo = { path = "f", screen = ScreenA } }, + { Bar = { path = "b/:great", screen = ScreenB } }, + }) + local params = { foo = "42" } + local action = router.getActionForPathAndParams("b/anything", params) + local expectedAction = { + params = { + foo = "42", + great = "anything", + }, + routeName = "Bar", + type = NavigationActions.Navigate, + } + + expectDeepEqual(action, expectedAction) + + local state = router.getStateForAction({ type = NavigationActions.Init }) + local expectedState = { + index = 1, + routes = { + { key = "Foo", routeName = "Foo" }, + { key = "Bar", routeName = "Bar" }, + }, + } + + expect(state).to.equal(expectedState) + + local state2 = router.getStateForAction(expectedAction, state) + local expectedState2 = { + index = 2, + routes = { + { key = "Foo", routeName = "Foo", params = nil }, + { + key = "Bar", + routeName = "Bar", + params = { foo = "42", great = "anything" }, + }, + }, + } + + expectDeepEqual(state2, expectedState2) + expect(router.getComponentForState(expectedState)).to.equal(ScreenA) + expect(router.getComponentForState(expectedState2)).to.equal(ScreenB) + expect(router.getPathAndParamsForState(expectedState).path).to.equal("f") + expect(router.getPathAndParamsForState(expectedState2).path).to.equal("b/anything") + end) + + -- deviation: Router.getActionForPathAndParams not implemented yet. + itSKIP("Handles default configuration", function() + local function ScreenA() + return Roact.createElement("Frame") + end + local function ScreenB() + return Roact.createElement("Frame") + end + local router = TabRouter({ + { Foo = { path = "", screen = ScreenA } }, + { Bar = { path = "b", screen = ScreenB } }, + }) + local action = router.getActionForPathAndParams("", { foo = "42" }) + + expectDeepEqual(action, { + params = { foo = "42" }, + routeName = "Foo", + type = NavigationActions.Navigate, + }) + end) + + -- deviation: Router.getActionForPathAndParams not implemented yet. + itSKIP("Gets deep path", function() + local ScreenA = Roact.Component:extend("ScreenA") + function ScreenA:render() + return Roact.createElement("Frame") + end + local function ScreenB() + return Roact.createElement("Frame") + end + + ScreenA.router = TabRouter({ + { Baz = { screen = ScreenB } }, + { Boo = { screen = ScreenB } }, + }) + + local router = TabRouter({ + { Foo = { path = "f", screen = ScreenA } }, + { Bar = { screen = ScreenB } }, + }) + local state = { + index = 1, + routes = { + { + index = 2, + key = "Foo", + routeName = "Foo", + routes = { + { key = "Boo", routeName = "Boo" }, + { key = "Baz", routeName = "Baz" }, + }, + }, + { key = "Bar", routeName = "Bar" }, + }, + } + local path = router.getPathAndParamsForState(state).path + + expect(path).to.equal("f/Baz") + end) + + it("Can navigate to other tab (no router) with params", function() + local function ScreenA() + return Roact.createElement("Frame") + end + local function ScreenB() + return Roact.createElement("Frame") + end + local router = TabRouter({ + { a = { screen = ScreenA } }, + { b = { screen = ScreenB } }, + }) + local state0 = router.getStateForAction(INIT_ACTION) + + expectDeepEqual(state0, { + index = 1, + routes = { + { key = "a", routeName = "a" }, + { key = "b", routeName = "b" }, + }, + }) + + local params = { key = "value" } + local state1 = router.getStateForAction({ + type = NavigationActions.Navigate, + routeName = "b", + params = params, + }, state0) + + expectDeepEqual(state1, { + index = 2, + routes = { + { key = "a", routeName = "a" }, + { key = "b", routeName = "b", params = params }, + }, + }) + end) + + it("Back actions are not propagated to inactive children", function() + local function ScreenA() + return Roact.createElement("Frame") + end + local function ScreenB() + return Roact.createElement("Frame") + end + local function ScreenC() + return Roact.createElement("Frame") + end + local InnerNavigator = Roact.Component:extend("InnerNavigator") + function InnerNavigator:render() + return Roact.createElement("Frame") + end + + InnerNavigator.router = TabRouter({ + { a = { screen = ScreenA } }, + { b = { screen = ScreenB } }, + }) + + local router = TabRouter({ + { inner = { screen = InnerNavigator } }, + { c = { screen = ScreenC } }, + }, { + backBehavior = BackBehavior.None, + }) + local state0 = router.getStateForAction(INIT_ACTION) + local state1 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "b" }, + state0 + ) + local state2 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "c" }, + state1 + ) + local state3 = router.getStateForAction( + { type = NavigationActions.Back }, + state2 + ) + + expectDeepEqual(state3, state2) + end) + + it("Back behavior initialRoute works", function() + local function ScreenA() + return Roact.createElement("Frame") + end + local function ScreenB() + return Roact.createElement("Frame") + end + local router = TabRouter({ + { a = { screen = ScreenA } }, + { b = { screen = ScreenB } }, + }) + local state0 = router.getStateForAction(INIT_ACTION) + local state1 = router.getStateForAction( + { type = NavigationActions.Navigate, routeName = "b" }, + state0 + ) + local state2 = router.getStateForAction( + { type = NavigationActions.Back }, + state1 + ) + + expectDeepEqual(state2, state0) + end) + + it("Inner actions are only unpacked if the current tab matches", function() + local PlainScreen = function() + return Roact.createElement("Frame") + end + local ScreenA = Roact.Component:extend("ScreenA") + function ScreenA:render() + return Roact.createElement("Frame") + end + local ScreenB = Roact.Component:extend("ScreenB") + function ScreenB:render() + return Roact.createElement("Frame") + end + + ScreenB.router = TabRouter({ + { Baz = { screen = PlainScreen } }, + { Zoo = { screen = PlainScreen } }, + }) + ScreenA.router = TabRouter({ + { Bar = { screen = PlainScreen } }, + { Boo = { screen = ScreenB } }, + }) + local router = TabRouter({ + { Foo = { screen = ScreenA } }, + }) + + local screenApreState = { + index = 1, + key = "Foo", + routeName = "Foo", + routes = { + { key = "Bar", routeName = "Bar" }, + }, + } + local preState = { + index = 1, + routes = { screenApreState }, + } + + local function comparable(state) + local result = {} + + if typeof(state.routeName) == "string" then + result.routeName = state.routeName + end + if typeof(state.routes) == 'table' then + result.routes = {} + for i=1, #state.routes do + result.routes[i] = comparable(state.routes[i]) + end + end + + return result + end + + local action = NavigationActions.navigate({ + routeName = "Boo", + action = NavigationActions.navigate({ routeName = "Zoo" }), + }) + local expectedState = ScreenA.router.getStateForAction(action, screenApreState) + local state = router.getStateForAction(action, preState) + local innerState = state and state.routes[1] or state + + expect(innerState.routes[2].index).to.equal(2) + expectDeepEqual( + expectedState and comparable(expectedState), + innerState and comparable(innerState) + ) + + local noMatchAction = NavigationActions.navigate({ + routeName = "Qux", + action = NavigationActions.navigate({ routeName = "Zoo" }), + }) + local expectedState2 = ScreenA.router.getStateForAction(noMatchAction, screenApreState) + local state2 = router.getStateForAction(noMatchAction, preState) + local innerState2 = state2 and state2.routes[1] or state2 + + expect(innerState2.routes[2].index).to.equal(1) + expectDeepEqual( + expectedState2 and comparable(expectedState2), + innerState2 and comparable(innerState2) + ) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/createConfigGetter.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/createConfigGetter.roblox.spec.lua new file mode 100644 index 0000000..540138d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/createConfigGetter.roblox.spec.lua @@ -0,0 +1,37 @@ +return function() + local createConfigGetter = require(script.Parent.Parent.createConfigGetter) + + it("should return a function", function() + local result = createConfigGetter({}, {}) + expect(result).to.be.a("function") + end) + + it("should override default config with component-specific config", function() + local getScreenOptions = createConfigGetter({ + Home = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentHome" }, + }, + }, + defaultNavigationOptions = { title = "DefaultTitle" }, + }) + + expect(getScreenOptions({ state = { routeName = "Home" } }).title).to.equal("ComponentHome") + end) + + it("should override component-specific config with route-specific config", function() + local getScreenOptions = createConfigGetter({ + Home = { + screen = { + render = function() end, + navigationOptions = { title = "ComponentHome" }, + }, + navigationOptions = { title = "RouteHome" }, + }, + defaultNavigationOptions = { title = "DefaultTitle" }, + }) + + expect(getScreenOptions({ state = { routeName = "Home" } }).title).to.equal("RouteHome") + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/createConfigGetter.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/createConfigGetter.spec.lua new file mode 100644 index 0000000..ace9b00 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/createConfigGetter.spec.lua @@ -0,0 +1,102 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/72e8160537954af40f1b070aa91ef45fc02bba69/packages/core/src/routers/__tests__/createConfigGetter.test.js + +return function() + local createConfigGetter = require(script.Parent.Parent.createConfigGetter) + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + + it("should get config for screen", function() + local HomeScreen = Roact.Component:extend("HomeScreen") + HomeScreen.navigationOptions = function(props) + local username = props.navigation.state.params and + props.navigation.state.params.user or "anonymous" + + return { + title = string.format("Welcome %s", username), + gesturesEnabled = true, + } + end + function HomeScreen:render() return nil end + + local SettingsScreen = Roact.Component:extend("SettingsScreen") + SettingsScreen.navigationOptions = { + title = "Settings!!!", + gesturesEnabled = false, + } + function SettingsScreen:render() return nil end + + local NotificationScreen = Roact.Component:extend("NotificationScreen") + NotificationScreen.navigationOptions = function(props) + local gesturesEnabled = true + if props.navigation.state.params then + gesturesEnabled = not props.navigation.state.params.fullscreen + end + + return { + title = "42", + gesturesEnabled = gesturesEnabled + } + end + + local getScreenOptions = createConfigGetter({ + Home = { screen = HomeScreen }, + Settings = { screen = SettingsScreen }, + Notifications = { + screen = NotificationScreen, + navigationOptions = { + title = "10 new notifications", + } + } + }) + + local routes = { + { key = "A", routeName = "Home", }, + { key = "B", routeName = "Home", params = { user = "jane"} }, + { key = "C", routeName = "Settings", }, + { key = "D", routeName = "Notifications", }, + { key = "E", routeName = "Notifications", params = { fullscreen = true } }, + } + + expect(getScreenOptions({ state = routes[1] }, {}).title) + .to.equal("Welcome anonymous") + + expect(getScreenOptions({ state = routes[2] }, {}).title) + .to.equal("Welcome jane") + + expect(getScreenOptions({ state = routes[1] }, {}).gesturesEnabled) + .to.equal(true) + + expect(getScreenOptions({ state = routes[3] }, {}).title) + .to.equal("Settings!!!") + + expect(getScreenOptions({ state = routes[3] }, {}).gesturesEnabled) + .to.equal(false) + + expect(getScreenOptions({ state = routes[4] }, {}).title) + .to.equal("10 new notifications") + + expect(getScreenOptions({ state = routes[4] }, {}).gesturesEnabled) + .to.equal(true) + + expect(getScreenOptions({ state = routes[5] }, {}).gesturesEnabled) + .to.equal(false) + end) + + it("should throw if the route does not exist", function() + local HomeScreen = Roact.Component:extend("HomeScreen") + + HomeScreen.navigationOptions = { + title = "Home screen", + gesturesEnabled = true, + } + + local getScreenOptions = createConfigGetter({ + Home = { screen = HomeScreen }, + }) + + local routes = {{ key = "B", routeName = "Settings" }} + + expect(function() + getScreenOptions({ state = routes[1] }, {}) + end).to.throw("There is no route defined for key Settings.\nMust be one of: 'Home'") + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/getNavigationActionCreators.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/getNavigationActionCreators.spec.lua new file mode 100644 index 0000000..bafe6b7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/getNavigationActionCreators.spec.lua @@ -0,0 +1,101 @@ +return function() + local getNavigationActionCreators = require(script.Parent.Parent.getNavigationActionCreators) + local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions) + + local function expectError(functor, msg) + local status, err = pcall(functor) + expect(status).to.equal(false) + expect(string.find(err, msg)).to.never.equal(nil) + end + + it("should return a table with correct functions when called", function() + local result = getNavigationActionCreators() + expect(type(result.goBack)).to.equal("function") + expect(type(result.navigate)).to.equal("function") + expect(type(result.setParams)).to.equal("function") + end) + + describe("goBack tests", function() + it("should return a Back action when called", function() + local result = getNavigationActionCreators().goBack("theKey") + expect(result.type).to.equal(NavigationActions.Back) + expect(result.key).to.equal("theKey") + end) + + it("should throw when route.key is not a string", function() + expectError(function() + getNavigationActionCreators({ key = 5 }).goBack() + end, "%.goBack%(%): key should be a string") + end) + + it("should fall back to route.key if key is not provided", function() + local result = getNavigationActionCreators({ key = "routeKey" }).goBack() + expect(result.key).to.equal("routeKey") + end) + + it("should override route.key if key is provided", function() + local result = getNavigationActionCreators({ key = "routeKey" }).goBack("theKey") + expect(result.key).to.equal("theKey") + end) + end) + + describe("navigate tests", function() + it("should return a Navigate action when called", function() + local theParams = {} + local childAction = {} + local result = getNavigationActionCreators().navigate("theRoute", theParams, childAction) + expect(result.type).to.equal(NavigationActions.Navigate) + expect(result.routeName).to.equal("theRoute") + expect(result.params).to.equal(theParams) + expect(result.action).to.equal(childAction) + end) + + it("should return a navigate action with matching properties when called with a table", function() + local testNavigateTo = { + routeName = "theRoute", + params = {}, + action = {}, + } + + local result = getNavigationActionCreators().navigate(testNavigateTo) + expect(result.type).to.equal(NavigationActions.Navigate) + expect(result.routeName).to.equal("theRoute") + expect(result.params).to.equal(testNavigateTo.params) + expect(result.action).to.equal(testNavigateTo.action) + end) + + it("should throw when navigateTo is not a valid type", function() + expectError(function() + getNavigationActionCreators().navigate(5) + end, "%.navigate%(%): navigateTo must be a string or table") + end) + + it("should throw when params is provided with a table navigateTo", function() + expectError(function() + getNavigationActionCreators().navigate({}, {}) + end, "%.navigate%(%): params can only be provided with a string navigateTo value") + end) + + it("should throw when action is provided with a table navigateTo", function() + expectError(function() + getNavigationActionCreators().navigate({}, nil, {}) + end, "%.navigate%(%): child action can only be provided with a string navigateTo value") + end) + end) + + describe("setParams tests", function() + it("should return a SetParams action when called", function() + local theParams = {} + local result = getNavigationActionCreators({ key = "theKey" }).setParams(theParams) + expect(result.type).to.equal(NavigationActions.SetParams) + expect(result.key).to.equal("theKey") + expect(result.params).to.equal(theParams) + end) + + it("should throw when called by a root navigator", function() + expectError(function() + getNavigationActionCreators({}).setParams({}) + end, "%.setParams%(%): cannot be called by the root navigator") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/getScreenForRouteName.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/getScreenForRouteName.spec.lua new file mode 100644 index 0000000..08e022d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/getScreenForRouteName.spec.lua @@ -0,0 +1,80 @@ +return function() + local getScreenForRouteName = require(script.Parent.Parent.getScreenForRouteName) + + it("should throw for invalid arg types", function() + expect(function() + getScreenForRouteName("", "myRoute") + end).to.throw("routeConfigs must be a table") + + expect(function() + getScreenForRouteName({}, 5) + end).to.throw("routeName must be a string") + end) + + it("should throw if requested route is not present within table", function() + local function shouldThrow() + getScreenForRouteName({ + notMyRoute = function() return "foo" end + }, "myRoute") + end + + expect(shouldThrow).to.throw( + "There is no route defined for key myRoute.\nMust be one of: 'notMyRoute'" + ) + end) + + it("should return raw table if screen and getScreen are not props", function() + local screenComponent = { render = function() return nil end } + local result = getScreenForRouteName({ + myRoute = screenComponent + }, "myRoute") + + expect(result).to.equal(screenComponent) + end) + + it("should return screen prop if it is set in route data table", function() + local screenComponent = { render = function() return nil end } + local result = getScreenForRouteName({ + myRoute = { + screen = screenComponent + } + }, "myRoute") + + expect(result).to.equal(screenComponent) + end) + + it("should return object returned by getScreen function if object is valid Roact element", function() + local screenComponent = { render = function() return nil end } + local result = getScreenForRouteName({ + myRoute = { + getScreen = function() return screenComponent end + } + }, "myRoute") + + expect(result).to.equal(screenComponent) + end) + + it("should throw if getScreen does not return a valid Roact element", function() + local errorExpected = "The getScreen defined for route 'myRoute' didn't return a valid " .. + "screen or navigator.\n\n" + + expect(function() + getScreenForRouteName({ + myRoute = { + getScreen = function() return nil end + } + }, "myRoute") + end).to.throw(errorExpected) + end) + + it("should throw if screen is not a valid Roact element", function() + expect(function() + getScreenForRouteName({ + myRoute = { + screen = 5, + } + }, "myRoute") + end).to.throw("screen for key 'myRoute' must be a valid Roact component.") + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/routerTestHelper.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/routerTestHelper.lua new file mode 100644 index 0000000..964ad7e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/routerTestHelper.lua @@ -0,0 +1,95 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/fcd7d83c4c33ad1fa508c8cfe687d2fa259bfc2c/packages/core/src/routers/__tests__/routerTestHelper.js + +local Root = script.Parent.Parent.Parent +local Packages = Root.Parent +local Cryo = require(Packages.Cryo) +local StackActions = require(Root.routers.StackActions) +local SwitchActions = require(Root.routers.SwitchActions) +local NavigationActions = require(Root.NavigationActions) + +local defaultOptions = { skipInitializeState = false } + +local function getSubStateRecursive(state, level) + level = level or 1 + if level == 0 then + return state + else + local directSubState = state.routes[state.index] + return getSubStateRecursive(directSubState, level -1 ) + end +end + +local function getRouterTestHelper(router, options) + options = options or defaultOptions + + local state = nil + if (not options.skipInitializeState) then + state = router.getStateForAction({ + type = NavigationActions.Init, + }) + end + + local function applyAction(action) + state = router.getStateForAction(action, state) + end + + local function navigateTo(routeName, otherActionAttributes) + otherActionAttributes = otherActionAttributes or {} + + return applyAction(Cryo.Dictionary.join({ + type = NavigationActions.Navigate, + routeName = routeName, + }, otherActionAttributes)) + end + + local function jumpTo(routeName, otherActionAttributes) + otherActionAttributes = otherActionAttributes or {} + + return applyAction(Cryo.Dictionary.join({ + type = SwitchActions.JumpTo, + routeName = routeName, + }, otherActionAttributes)) + end + + local function back(key) + return applyAction({ + type = NavigationActions.Back, + key = key, + }) + end + + local function pop() + return applyAction({ + type = StackActions.Pop, + }) + end + + local function popToTop() + return applyAction({ + type = StackActions.PopToTop, + }) + end + + local function getState() + return state + end + + local function getSubState(level) + level = level or 1 + + return getSubStateRecursive(state, level) + end + + return{ + applyAction = applyAction, + navigateTo = navigateTo, + jumpTo = jumpTo, + back = back, + pop = pop, + popToTop = popToTop, + getState = getState, + getSubState = getSubState, + } +end + +return getRouterTestHelper \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/validateRouteConfigArray.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/validateRouteConfigArray.spec.lua new file mode 100644 index 0000000..3e699db --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/validateRouteConfigArray.spec.lua @@ -0,0 +1,129 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local validateRouteConfigArray = require(script.Parent.Parent.validateRouteConfigArray) + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + return nil + end + + it("should throw if routeConfigs is not a table", function() + expect(function() + validateRouteConfigArray(5) + end).to.throw("routeConfigs must be an array table") + end) + + it("should throw if routeConfigs is empty", function() + expect(function() + validateRouteConfigArray({}) + end).to.throw("Please specify at least one route when configuring a navigator") + end) + + it("should throw if routeConfigs contains an invalid Roact element", function() + local error = "The component for route 'myRoute' must be a Roact Function/Stateful" + .. " component or table with 'getScreen'.getScreen function must return Roact" + .. " Function/Stateful component" + expect(function() + validateRouteConfigArray({ + { myRoute = 5 }, + }) + end).to.throw(error) + end) + + it("should throw if getScreen returns invalid Roact element", function() + local error = "The component for route 'myRoute' must be a Roact Function/Stateful" + .. " component or table with 'getScreen'.getScreen function must return Roact" + .. " Function/Stateful component" + expect(function() + validateRouteConfigArray({ + { myRoute = { getScreen = function() end } }, + }) + end).to.throw(error) + end) + + it("should throw when both screen and getScreen are provided for same component", function() + expect(function() + validateRouteConfigArray({ + {myRoute = { + screen = "TheScreen", + getScreen = function() return TestComponent end, + }} + }) + end).to.throw("Route 'myRoute' should provide 'screen' or 'getScreen', but not both") + end) + + it("should throw for a simple table where screen is not a Roact Function/Stateful component", function() + local error = "The component for route 'myRoute' must be a Roact Function/Stateful" + .. " component or table with 'getScreen'.getScreen function must return Roact" + .. " Function/Stateful component" + expect(function() + validateRouteConfigArray({ + { myRoute = { screen = {} } }, + }) + end).to.throw(error) + end) + + it("should throw for a non-function getScreen", function() + local error = "The component for route 'myRoute' must be a Roact Function/Stateful" + .. " component or table with 'getScreen'.getScreen function must return" + .. " Roact Function/Stateful component" + expect(function() + validateRouteConfigArray({ + { myRoute = { getScreen = 5 } }, + }) + end).to.throw(error) + end) + + it("should throw for a Host Component", function() + local error = "The component for route 'myRoute' must be a Roact Function/Stateful" + .. " component or table with 'getScreen'.getScreen function must return Roact" + .. " Function/Stateful component" + expect(function() + validateRouteConfigArray({ + { myRoute = { aFrame = "Frame" } }, + }) + end).to.throw(error) + end) + + it("should throw if routeConfig is a map", function() + local key = "basicComponentRoute" + local error = ("routeConfigs must be an array table (found non-number key %q of type %q"):format( + key, + type(key) + ) + expect(function() + validateRouteConfigArray({ + [key] = TestComponent, + }) + end).to.throw(error) + end) + + it("should throw if there is more than one route in each array entry", function() + local error = "only one route must be defined in each entry (found multiple at index 1)" + expect(function() + validateRouteConfigArray({ + { aRouteName = TestComponent, anotherRoute = TestComponent }, + }) + end).to.throw(error) + end) + + it("should pass for valid basic routeConfigs", function() + validateRouteConfigArray({ + { basicComponentRoute = TestComponent }, + { functionalComponentRoute = function() end }, + }) + end) + + it("should pass for valid screen prop type routeConfigs", function() + validateRouteConfigArray({ + { basicComponentRoute = { screen = TestComponent } }, + { functionalComponentRoute = { screen = function() end } }, + }) + end) + + it("should pass for valid getScreen route configs", function() + validateRouteConfigArray({ + { getScreenRoute = { getScreen = function() return TestComponent end } }, + }) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/validateRouteConfigMap.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/validateRouteConfigMap.spec.lua new file mode 100644 index 0000000..bd693be --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/validateRouteConfigMap.spec.lua @@ -0,0 +1,99 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local validateRouteConfigMap = require(script.Parent.Parent.validateRouteConfigMap) + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + return nil + end + + local INVALID_COMPONENT_MESSAGE = "The component for route 'myRoute' must be a Roact" .. + " component or table with 'getScreen'." + + it("should throw if routeConfigs is not a table", function() + expect(function() + validateRouteConfigMap(5) + end).to.throw("routeConfigs must be a table") + end) + + it("should throw if routeConfigs is empty", function() + expect(function() + validateRouteConfigMap({}) + end).to.throw("Please specify at least one route when configuring a navigator.") + end) + + it("should throw if routeConfigs contains an invalid Roact element", function() + expect(function() + validateRouteConfigMap({ + myRoute = 5, + }) + end).to.throw() + end) + + it("should throw when both screen and getScreen are provided for same component", function() + expect(function() + validateRouteConfigMap({ + myRoute = { + screen = "TheScreen", + getScreen = function() return TestComponent end, + } + }) + end).to.throw("Route 'myRoute' should declare a screen or a getScreen, not both.") + end) + + it("should throw for a simple table where screen is not a Roact component", function() + expect(function() + validateRouteConfigMap({ + myRoute = { + screen = {}, + } + }) + end).to.throw(INVALID_COMPONENT_MESSAGE) + end) + + it("should throw for a non-function getScreen", function() + expect(function() + validateRouteConfigMap({ + myRoute = { + getScreen = 5 + } + }) + end).to.throw(INVALID_COMPONENT_MESSAGE) + end) + + it("should throw for a Host Component", function() + expect(function() + validateRouteConfigMap({ + myRoute = { + aFrame = "Frame" + } + }) + end).to.throw(INVALID_COMPONENT_MESSAGE) + end) + + it("should pass for valid basic routeConfigs", function() + validateRouteConfigMap({ + basicComponentRoute = TestComponent, + functionalComponentRoute = function() end, + }) + end) + + it("should pass for valid screen prop type routeConfigs", function() + validateRouteConfigMap({ + basicComponentRoute = { + screen = TestComponent, + }, + functionalComponentRoute = { + screen = function() end, + }, + }) + end) + + it("should pass for valid getScreen route configs", function() + validateRouteConfigMap({ + getScreenRoute = { + getScreen = function() return TestComponent end, + } + }) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/validateScreenOptions.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/validateScreenOptions.spec.lua new file mode 100644 index 0000000..c1bb8ee --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/_tests_/validateScreenOptions.spec.lua @@ -0,0 +1,25 @@ +return function() + local validateScreenOptions = require(script.Parent.Parent.validateScreenOptions) + + it("should not throw when there are no problems", function() + validateScreenOptions({ title = "foo" }, { routeName = "foo" }) + end) + + it("should throw error if no routeName is provided", function() + local status, err = pcall(function() + validateScreenOptions({ title = "bar" }, {}) + end) + + expect(status).to.equal(false) + expect(string.find(err, "route.routeName must be a string")).to.never.equal(nil) + end) + + it("should throw error for options with function for title", function() + expect(function() + validateScreenOptions({ + title = function() end, + }, { routeName = "foo" }) + end).to.throw() + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/createConfigGetter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/createConfigGetter.lua new file mode 100644 index 0000000..c2b9c11 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/createConfigGetter.lua @@ -0,0 +1,62 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/6390aacd07fd647d925dfec842a766c8aad5272f/packages/core/src/routers/createConfigGetter.js +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local getScreenForRouteName = require(script.Parent.getScreenForRouteName) +local validateScreenOptions = require(script.Parent.validateScreenOptions) +local validate = require(script.Parent.Parent.utils.validate) + +local function applyConfig(configurer, navigationOptions, configProps) + navigationOptions = navigationOptions or {} + + local configurerType = type(configurer) + if configurerType == "function" then + return Cryo.Dictionary.join( + navigationOptions, + configurer(Cryo.Dictionary.join(configProps, { + navigationOptions = navigationOptions + })) + ) + elseif configurerType == "table" then + return Cryo.Dictionary.join(navigationOptions, configurer) + else + return navigationOptions + end +end + +return function(routeConfigs, navigatorScreenConfig) + return function(navigation, screenProps) + screenProps = screenProps or {} + local route = navigation.state + + validate(type(route) == "table", "navigation.state must be a table") + validate( + type(route.routeName == "string"), + "Cannot get config because the route does not have a routeName." + ) + + local component = getScreenForRouteName(routeConfigs, route.routeName) + local routeConfig = routeConfigs[route.routeName] + + local routeScreenConfig = nil + if routeConfig ~= component then + routeScreenConfig = routeConfig.navigationOptions + end + + -- deviation: check if the component is a table, because it could be a + -- function and it can't be indexed in Lua. + local componentScreenConfig = type(component) == "table" + and component.navigationOptions or {} + + local configOptions = { + navigation = navigation, + screenProps = screenProps, + -- deviation: no theme support + } + + local outputConfig = applyConfig(navigatorScreenConfig, {}, configOptions) + outputConfig = applyConfig(componentScreenConfig, outputConfig, configOptions) + outputConfig = applyConfig(routeScreenConfig, outputConfig, configOptions) + + validateScreenOptions(outputConfig, route) + return outputConfig + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/getNavigationActionCreators.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/getNavigationActionCreators.lua new file mode 100644 index 0000000..106c266 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/getNavigationActionCreators.lua @@ -0,0 +1,42 @@ +local NavigationActions = require(script.Parent.Parent.NavigationActions) +local validate = require(script.Parent.Parent.utils.validate) + +return function(route) + local result = {} + + -- Go back FROM screen identified by 'key', or the default for current route. + function result.goBack(key) + if key == nil and route.key then + validate(type(route.key) == "string", ".goBack(): key should be a string") + key = route.key + end + + return NavigationActions.back({ key = key }) + end + + -- Navigate to a different screen, either by route name+params+action, or + -- by passing a raw navigation table. + function result.navigate(navigateTo, params, action) + if type(navigateTo) == "string" then + return NavigationActions.navigate({ + routeName = navigateTo, + params = params, + action = action, + }) + else + validate(type(navigateTo) == "table", ".navigate(): navigateTo must be a string or table") + validate(params == nil, ".navigate(): params can only be provided with a string navigateTo value") + validate(action == nil, ".navigate(): child action can only be provided with a string navigateTo value") + + return NavigationActions.navigate(navigateTo) + end + end + + -- Change navigation params for current route + function result.setParams(params) + validate(type(route.key) == "string", ".setParams(): cannot be called by the root navigator") + return NavigationActions.setParams({ params = params, key = route.key }) + end + + return result +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/getScreenForRouteName.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/getScreenForRouteName.lua new file mode 100644 index 0000000..95692ab --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/getScreenForRouteName.lua @@ -0,0 +1,60 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/routers/SwitchRouter.js +local root = script.Parent.Parent +local Packages = root.Parent +local Cryo = require(Packages.Cryo) +local validate = require(root.utils.validate) +local isValidScreenComponent = require(root.utils.isValidScreenComponent) + +-- Extract a single screen Roact component/navigator from +-- a navigator's config. +return function(routeConfigs, routeName) + validate(type(routeConfigs) == "table", "routeConfigs must be a table") + validate(type(routeName) == "string", "routeName must be a string") + + local routeConfig = routeConfigs[routeName] + + if routeConfig == nil then + local possibleRoutes = Cryo.List.map(Cryo.Dictionary.keys(routeConfigs), function(name) + return ("'%s'"):format(name) + end) + + local message = ("There is no route defined for key %s.\nMust be one of: %s"):format( + routeName, + table.concat(possibleRoutes, ",") + ) + error(message, 2) + end + + local routeConfigType = type(routeConfig) + + if routeConfigType == "table" then + if routeConfig.screen ~= nil then + validate( + isValidScreenComponent(routeConfig.screen), + "screen for key '%s' must be a valid Roact component.", + routeName + ) + return routeConfig.screen + elseif type(routeConfig.getScreen) == "function" then + local screen = routeConfig.getScreen() + validate( + isValidScreenComponent(screen), + "The getScreen defined for route '%s' didn't return a valid " .. + "screen or navigator.\n\n" .. + "Please pass it like this:\n" .. + "%s = {\n getScreen: function() return MyScreen end\n}", + routeName, + routeName + ) + return screen + end + end + + validate( + isValidScreenComponent(routeConfig), + "Value for key '%s' must be a route config table or a valid Roact component.", + routeName + ) + + return routeConfig +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/validateRouteConfigArray.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/validateRouteConfigArray.lua new file mode 100644 index 0000000..b4f797f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/validateRouteConfigArray.lua @@ -0,0 +1,54 @@ +local validate = require(script.Parent.Parent.utils.validate) +local isValidScreenComponent = require(script.Parent.Parent.utils.isValidScreenComponent) + +--[[ + This utility checks to make sure that configs passed to a + router are in the correct format. + + Example: + routeConfigs = { + { routeNameEx1 = Roact.Function/Stateful_Component }, + { routeNameEx2 = { screen = Roact.Function/Stateful_Component } }, + { + routeNameEx3 = { + getScreen = function() return Roact.Function/Stateful_Component end + } + } + { routeNameEx4 = AnotherRoactNavigator } -- this is a Stateful Component + } +]] +return function(routeConfigs) + validate(type(routeConfigs) == "table", "routeConfigs must be an array table") + + for index, route in pairs(routeConfigs) do + validate( + type(index) == "number", + ("routeConfigs must be an array table (found non-number key %q of type %q)"):format( + index, + type(index) + ) + ) + local routeName, routeConfig = next(route) + validate( + next(route, routeName) == nil, + ("only one route must be defined in each entry (found multiple at index %d)"):format( + index + ) + ) + local configIsTable = type(routeConfig) == "table" or false + local screenConfig = configIsTable and routeConfig or {} -- easy index .screen/.getScreen + local screenComponent = configIsTable and routeConfig.screen or routeConfig + validate(isValidScreenComponent(screenComponent) or + (type(screenConfig.getScreen) == "function" and isValidScreenComponent(screenConfig.getScreen())), + "The component for route '%s' must be a Roact Function/Stateful component or table with 'getScreen'." .. + "getScreen function must return Roact Function/Stateful component.", + routeName) + + validate(screenConfig.screen == nil or screenConfig.getScreen == nil, + "Route '%s' should provide 'screen' or 'getScreen', but not both.", routeName) + end + + validate(#routeConfigs > 0, "Please specify at least one route when configuring a navigator.") + + return routeConfigs +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/validateRouteConfigMap.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/validateRouteConfigMap.lua new file mode 100644 index 0000000..e75fdd4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/validateRouteConfigMap.lua @@ -0,0 +1,76 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/6390aacd07fd647d925dfec842a766c8aad5272f/packages/core/src/routers/validateRouteConfigMap.js +local root = script.Parent.Parent +local validate = require(root.utils.validate) +local isValidScreenComponent = require(root.utils.isValidScreenComponent) + +local function getScreenComponent(routeConfig) + if not routeConfig then + return nil + end + + if type(routeConfig) == "table" and routeConfig.screen then + return routeConfig.screen + end + + return routeConfig +end + +--[[ + This utility checks to make sure that configs passed to a + router are in the correct format. + + Example: + routeConfigs = { + routeNameEx1 = Roact.Function/Stateful_Component, + routeNameEx2 = { + screen = Roact.Function/Stateful_Component, + }, + routeNameEx3 = { + getScreen = function() + return Roact.Function/Stateful_Component + end + } + routeNameEx4 = AnotherRoactNavigator -- this is a Stateful Component + } +]] +return function(routeConfigs) + validate(type(routeConfigs) == "table", "routeConfigs must be a table") + validate(next(routeConfigs) ~= nil, "Please specify at least one route when configuring a navigator.") + + for routeName, routeConfig in pairs(routeConfigs) do + local screenComponent = getScreenComponent(routeConfig) + + local tableRouteConfig = type(routeConfig) == "table" + validate( + isValidScreenComponent(screenComponent) or + (tableRouteConfig and type(routeConfig.getScreen) == "function"), + "The component for route '%s' must be a Roact component or table with 'getScreen'." .. + [[ For example: + +local MyScreen = require(script.Parent.MyScreen) +... +%s = MyScreen, +} + +You can also use a navigator: + +local MyNavigator = require(script.Parent.MyNavigator) +... +%s = MyNavigator, +}]], + routeName, + routeName, + routeName + ) + + if tableRouteConfig then + validate( + routeConfig.screen == nil or routeConfig.getScreen == nil, + "Route '%s' should declare a screen or a getScreen, not both.", + routeName + ) + end + end + + return routeConfigs +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/validateScreenOptions.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/validateScreenOptions.lua new file mode 100644 index 0000000..b4354b6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/routers/validateScreenOptions.lua @@ -0,0 +1,10 @@ +local validate = require(script.Parent.Parent.utils.validate) + +return function(screenOptions, route) + validate(type(screenOptions) == "table", "screenOptions must be a table") + validate(type(route) == "table", "route must be a table") + validate(type(route.routeName) == "string", "route.routeName must be a string") + validate(type(screenOptions.title) ~= "function", + "title cannot be defined as a function in navigation options for screen '%s'", + route.routeName) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/KeyGenerator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/KeyGenerator.lua new file mode 100644 index 0000000..b38db4f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/KeyGenerator.lua @@ -0,0 +1,21 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/72e8160537954af40f1b070aa91ef45fc02bba69/packages/core/src/routers/KeyGenerator.ts +local uniqueBaseId = "id-" .. tostring(math.random(100000, 1000000)) +local uuidCount = 0 + +local KeyGenerator = {} + +-- NOTE: FOR TESTING ONLY. +-- Normalize keys so that tests can be consistent. +function KeyGenerator._TESTING_ONLY_normalize_keys() + uniqueBaseId = "id-" + uuidCount = 0 +end + +-- Get a string key that is unique for this session. +function KeyGenerator.generateKey() + local key = uniqueBaseId .. tostring(uuidCount) + uuidCount = uuidCount + 1 + return key +end + +return KeyGenerator diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/PageNavigationEvent.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/PageNavigationEvent.lua new file mode 100644 index 0000000..e225ab7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/PageNavigationEvent.lua @@ -0,0 +1,36 @@ +local validate = require(script.Parent.validate) + + +local PageNavigationEvent = {} +PageNavigationEvent.__index = PageNavigationEvent + +function PageNavigationEvent.new(pageName, event) + validate(typeof(pageName) == "string", "pageName should be string") + validate(typeof(event) == "userdata", "event should be RoactNavigation.Event") + local self = { + event = event, + pageName = pageName, + } + + setmetatable(self, PageNavigationEvent) + + return self +end + +function PageNavigationEvent.isPageNavigationEvent(instance) + return getmetatable(instance).__index == PageNavigationEvent +end + + +PageNavigationEvent.__tostring = function (pageNavigationEvent) + return string.format( "%-15s - %s", tostring(pageNavigationEvent.event), pageNavigationEvent.pageName) +end + + +function PageNavigationEvent:equalTo(anotherPageNavigationEvent) + validate(PageNavigationEvent.isPageNavigationEvent(anotherPageNavigationEvent), "should be PageNavigationEvent") + return self.pageName == anotherPageNavigationEvent.pageName and self.event == anotherPageNavigationEvent.event +end + + +return PageNavigationEvent \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/TableUtilities.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/TableUtilities.lua new file mode 100644 index 0000000..a8e447c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/TableUtilities.lua @@ -0,0 +1,320 @@ +--[[ + Provides functions for comparing and printing lua tables. +]] + +local TableUtilities = {} +local defaultIgnore = {} + +local function makeKeyString(key) + if type(key) == "string" then + return string.format("%s", key) + else + return string.format("[%s]", tostring(key)) + end +end + +local function makeValueString(value) + local valueType = type(value) + if valueType == "string" then + return string.format("%q", value) + elseif valueType == "function" or valueType == "table" then + return string.format("<%s>", tostring(value)) + else + return string.format("%s", tostring(value)) + end +end + +local function printKeypair(key, value, indentStr, comment) + local keyString = makeKeyString(key) + local valueString = makeValueString(value) + + local commentStr = comment and string.format(" -- %s", comment) or "" + print(string.format("%s%s = %s,%s", indentStr, keyString, valueString, commentStr)) +end + +--[[ + Takes two tables A and B, returns if they have the same key-value pairs + Except ignored keys +]] +function TableUtilities.ShallowEqual(A, B, ignore) + if not A or not B then + return false + elseif A == B then + return true + end + + if not ignore then + ignore = defaultIgnore + end + + for key, value in pairs(A) do + if B[key] ~= value and not ignore[key] then + return false + end + end + for key, value in pairs(B) do + if A[key] ~= value and not ignore[key] then + return false + end + end + + return true +end + +local function formatDeepEqualMessage(message, level) + if level ~= 0 then + return message + end + + return message + :gsub("{1}", "first") + :gsub("{2}", "second") +end + +--[[ + Takes two tables A and B, returns if they have the same key-value pairs recursively +]] +function TableUtilities.DeepEqual(a, b, level) + level = level or 0 + if a == b then + return true + end + + if typeof(a) ~= typeof(b) then + local message = ("{1} is of type %s, but {2} is of type %s"):format( + typeof(a), + typeof(b) + ) + + return false, formatDeepEqualMessage(message, level) + end + + if typeof(a) == "table" then + local visitedKeys = {} + + for key, value in pairs(a) do + visitedKeys[key] = true + + local success, innerMessage = TableUtilities.DeepEqual(value, b[key], level + 1) + if not success then + local message = innerMessage + :gsub("{1}", ("{1}[%s]"):format(tostring(key))) + :gsub("{2}", ("{2}[%s]"):format(tostring(key))) + + return false, formatDeepEqualMessage(message, level) + end + end + + for key, value in pairs(b) do + if not visitedKeys[key] then + local success, innerMessage = TableUtilities.DeepEqual(a[key], value, level + 1) + + if not success then + local message = innerMessage + :gsub("{1}", ("{1}[%s]"):format(tostring(key))) + :gsub("{2}", ("{2}[%s]"):format(tostring(key))) + + return false, formatDeepEqualMessage(message, level) + end + end + end + + return true + end + + local message = "{1} ~= {2}" + return false, formatDeepEqualMessage(message, level) +end + +--[[ + Takes two tables A, B and a key, returns if two tables have the same value at key +]] +function TableUtilities.EqualKey(A, B, key) + if A and B and key and key ~= "" and A[key] and B[key] and A[key] == B[key] then + return true + end + return false +end + +--[[ + Takes two tables A and B, returns a new table with elements of A + which are either not keys in B or have a different value in B +]] +function TableUtilities.TableDifference(A, B) + local new = {} + + for key, value in pairs(A) do + if B[key] ~= A[key] then + new[key] = value + end + end + + return new +end + + +--[[ + Takes a list and returns a table whose + keys are elements of the list and whose + values are all true +]] +local function membershipTable(list) + local result = {} + for i = 1, #list do + result[list[i]] = true + end + return result +end + + +--[[ + Takes a table and returns a list of keys in that table +]] +local function listOfKeys(t) + local result = {} + for key,_ in pairs(t) do + table.insert(result, key) + end + return result +end + + +--[[ + Takes two lists A and B, returns a new list of elements of A + which are not in B +]] +function TableUtilities.ListDifference(A, B) + return listOfKeys(TableUtilities.TableDifference(membershipTable(A), membershipTable(B))) +end + + +--[[ + For debugging. Returns false if the given table has any of the following: + - a key that is neither a number or a string + - a mix of number and string keys + - number keys which are not exactly 1..#t +]] +function TableUtilities.CheckListConsistency(t) + local containsNumberKey = false + local containsStringKey = false + local numberConsistency = true + + local index = 1 + for x, _ in pairs(t) do + if type(x) == 'string' then + containsStringKey = true + elseif type(x) == 'number' then + if index ~= x then + numberConsistency = false + end + containsNumberKey = true + else + return false + end + + if containsStringKey and containsNumberKey then + return false + end + + index = index + 1 + end + + if containsNumberKey then + return numberConsistency + end + + return true +end + + +--[[ + For debugging, serializes the given table to a reasonable string that might even interpret as lua. +]] +function TableUtilities.RecursiveToString(t, indent) + indent = indent or '' + + if type(t) == 'table' then + local result = "" + if not TableUtilities.CheckListConsistency(t) then + result = result .. "-- WARNING: this table fails the list consistency test\n" + end + result = result .. "{\n" + for k,v in pairs(t) do + if type(k) == 'string' then + result = result + .. " " + .. indent + .. tostring(k) + .. " = " + .. TableUtilities.RecursiveToString(v, " "..indent) + ..";\n" + end + if type(k) == 'number' then + result = result .. " " .. indent .. TableUtilities.RecursiveToString(v, " "..indent)..",\n" + end + end + result = result .. indent .. "}" + return result + else + return tostring(t) + end +end + +--[[ + For debugging. Prints the table on multiple lines to overcome log-line length + limitations which are otherwise necessary for performance. Use sparingly. +]] +function TableUtilities.Print(t, indent) + indent = indent or ' ' + + if type(t) ~= "table" then + error("TableUtilities.Print must be passed a table", 2) + end + + -- For cycle detection + local printedTables = {} + + local function recurse(subTable, tableKey, level) + -- Prevent cycles by keeping track of what tables we have printed + printedTables[subTable] = true + + local indentStr = string.rep(indent, level) + local valueIndentStr = string.rep(indent, level + 1) + + if tableKey then + print(string.format("%s%s = %s {", indentStr, makeKeyString(tableKey), makeValueString(subTable))) + else + print(string.format("%s%s {", indentStr, makeValueString(subTable))) + end + + for key, value in pairs(subTable) do + if type(value) == "table" then + if printedTables[value] then + printKeypair(key, value, valueIndentStr, "Possible cycle") + else + recurse(value, key, level + 1) + end + else + printKeypair(key, value, valueIndentStr) + end + end + + print(string.format("%s}%s", indentStr, (level > 0 and "," or ""))) + end + + recurse(t, nil, 0) +end + +--[[ + Takes a table and returns the field count +]] +function TableUtilities.FieldCount(t) + local fieldCount = 0 + for _ in pairs(t) do + fieldCount = fieldCount + 1 + end + return fieldCount +end + +return TableUtilities + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/TrackNavigationEvents.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/TrackNavigationEvents.lua new file mode 100644 index 0000000..be55c59 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/TrackNavigationEvents.lua @@ -0,0 +1,118 @@ +local root = script.Parent.Parent +local Packages = root.Parent +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) +local NavigationEvents = require(root.views.NavigationEvents) +local Events = require(root.Events) +local validate = require(script.Parent.validate) +local PageNavigationEvent = require(script.Parent.PageNavigationEvent) + +local TrackNavigationEvents = {} +TrackNavigationEvents.__index = TrackNavigationEvents + +function TrackNavigationEvents.new() + local self = { + navigationEvents = {}, + } + + setmetatable(self, TrackNavigationEvents) + + return self +end + +function TrackNavigationEvents:getNavigationEvents() + return self.navigationEvents +end + +function TrackNavigationEvents:printNavigationEvents() + print("Total Events: ", #self.navigationEvents) + for _, navigationEvent in ipairs(self.navigationEvents) do + print(navigationEvent) + end +end + +function TrackNavigationEvents:waitForNumberEventsMaxWaitTime(numberOfEvents, maxWaitTimeInSeconds) + local secondsWaitedFor = 0 + local waitDurationPerIteration = 0.33 + while #self.navigationEvents < numberOfEvents + and secondsWaitedFor <= maxWaitTimeInSeconds + do + wait(waitDurationPerIteration) + -- print("waiting for number of events to reach:", numberOfEvents, "waited for:", secondsWaitedFor) + secondsWaitedFor = secondsWaitedFor + waitDurationPerIteration + end +end + +function TrackNavigationEvents:resetNavigationEvents() + self.navigationEvents = {} +end + +local propNameToEvent = { + onWillFocus = Events.WillFocus, + onDidFocus = Events.DidFocus, + onWillBlur = Events.WillBlur, + onDidBlur = Events.DidBlur, +} + +function TrackNavigationEvents:createNavigationAdapter(pageName) + local props = {} + for propName, event in pairs(propNameToEvent) do + props[propName] = function() + PageNavigationEvent.new(pageName, event) + table.insert(self.navigationEvents, PageNavigationEvent.new(pageName, event)) + end + end + + return Roact.createElement(NavigationEvents, props) +end + +function TrackNavigationEvents:equalTo(pageNavigationEventList) + validate(typeof(pageNavigationEventList) == "table", "should be a list") + local numberOfEvents = #self.navigationEvents + + if numberOfEvents ~= #pageNavigationEventList then + return false, "different amount of events" + end + + for i=1, numberOfEvents do + if not self.navigationEvents[i]:equalTo(pageNavigationEventList[i]) then + return false, ("events at position %d do not match"):format(i) + end + end + + return true +end + +function TrackNavigationEvents:expect(pageNavigationEventList) + local result, message = self:equalTo(pageNavigationEventList) + + if not result then + local selfEvents = "{}" + local expectedEvents = "{}" + + if #self.navigationEvents > 0 then + selfEvents = ("{\n %s,\n}"):format( + table.concat( + Cryo.List.map(self.navigationEvents, tostring), + ',\n ' + ) + ) + end + if #pageNavigationEventList > 0 then + expectedEvents = ("{\n %s,\n}"):format( + table.concat( + Cryo.List.map(pageNavigationEventList, tostring), + ',\n ' + ) + ) + end + + error(("%s\nGot events: %s\n\nExpected events: %s"):format( + message, + selfEvents, + expectedEvents + )) + end +end + +return TrackNavigationEvents diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/KeyGenerator.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/KeyGenerator.spec.lua new file mode 100644 index 0000000..f67a174 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/KeyGenerator.spec.lua @@ -0,0 +1,14 @@ +return function() + local KeyGenerator = require(script.Parent.Parent.KeyGenerator) + + it("should generate a new string key when called", function() + KeyGenerator._TESTING_ONLY_normalize_keys() + + expect(KeyGenerator.generateKey()).to.equal("id-0") + expect(KeyGenerator.generateKey()).to.equal("id-1") + end) + + it("should generate unique string keys without being normalized", function() + expect(KeyGenerator.generateKey()).to.never.equal(KeyGenerator.generateKey()) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/PageNavigationEvent.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/PageNavigationEvent.spec.lua new file mode 100644 index 0000000..e893e22 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/PageNavigationEvent.spec.lua @@ -0,0 +1,46 @@ +local RoactNavigation = require(script.Parent.Parent.Parent) +local PageNavigationEvent = require(script.Parent.Parent.PageNavigationEvent) + +return function() + local testPage = "TEST PAGE" + local willFocusEvent = RoactNavigation.Events.WillFocus + + it("should validate constructor inputs", function() + expect(function () PageNavigationEvent.new(testPage, willFocusEvent) end).never.to.throw() + + expect(function () PageNavigationEvent.new(testPage, 1) end).to.throw() + expect(function () PageNavigationEvent.new(testPage, "event") end).to.throw() + expect(function () PageNavigationEvent.new(testPage, nil) end).to.throw() + expect(function () PageNavigationEvent.new(testPage, {some = "junk"}) end).to.throw() + expect(function () PageNavigationEvent.new(1, willFocusEvent) end).to.throw() + expect(function () PageNavigationEvent.new(nil, willFocusEvent) end).to.throw() + expect(function () PageNavigationEvent.new({"bogus"}, willFocusEvent) end).to.throw() + end) + + it("should be constructed from page name and RoactNavigation.Events", function() + for _, event in pairs(RoactNavigation.Events) do + local pageName = testPage .. tostring(event) + local testPageNavigationEvent = PageNavigationEvent.new(pageName, event) + expect(testPageNavigationEvent.pageName).to.be.equal(pageName) + expect(testPageNavigationEvent.event).to.be.equal(event) + expect(PageNavigationEvent.isPageNavigationEvent(testPageNavigationEvent)).to.be.equal(true) + end + end) + + it("should implement tostring and eq", function() + for _, event in pairs(RoactNavigation.Events) do + local pageName = testPage .. tostring(event) + local testPageNavigationEvent = PageNavigationEvent.new(pageName, event) + expect(testPageNavigationEvent:equalTo(PageNavigationEvent.new(pageName, event))).to.be.equal(true) + expect(tostring(testPageNavigationEvent)).to.be.equal(string.format("%-15s - %s",tostring(event), pageName)) + end + + local testPageNavigationEvent = PageNavigationEvent.new(testPage, willFocusEvent) + local willFocus = PageNavigationEvent.new(testPage, willFocusEvent) + expect(testPageNavigationEvent:equalTo(willFocus)).to.be.equal(true) + local bogusWillFocus = PageNavigationEvent.new(testPage .. "bogus", willFocusEvent) + expect(testPageNavigationEvent:equalTo(bogusWillFocus)).to.be.equal(false) + local willBlur = PageNavigationEvent.new(testPage, RoactNavigation.Events.WillBlur) + expect(testPageNavigationEvent:equalTo(willBlur)).to.be.equal(false) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/TableUtilities.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/TableUtilities.spec.lua new file mode 100644 index 0000000..7bdadde --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/TableUtilities.spec.lua @@ -0,0 +1,115 @@ +return function() + local TableUtilities = require(script.Parent.Parent.TableUtilities) + + describe("DeepEqual", function() + it("should succeed", function() + expect(false).to.be.ok() + end) + + it("should fail with a message when args are not equal", function() + local success, message = TableUtilities.DeepEqual(1, 2) + + expect(success).to.equal(false) + expect(message:find("first ~= second")).to.be.ok() + + success, message = TableUtilities.DeepEqual({ + foo = 1, + }, { + foo = 2, + }) + + expect(success).to.equal(false) + expect(message:find("first%[foo%] ~= second%[foo%]")).to.be.ok() + end) + + it("should compare non-table values using standard '==' equality", function() + expect(TableUtilities.DeepEqual(1, 1)).to.equal(true) + expect(TableUtilities.DeepEqual("hello", "hello")).to.equal(true) + expect(TableUtilities.DeepEqual(nil, nil)).to.equal(true) + + local someFunction = function() end + local theSameFunction = someFunction + + expect(TableUtilities.DeepEqual(someFunction, theSameFunction)).to.equal(true) + + local A = { + foo = someFunction + } + local B = { + foo = theSameFunction + } + + expect(TableUtilities.DeepEqual(A, B)).to.equal(true) + end) + + it("should fail when types differ", function() + local success, message = TableUtilities.DeepEqual(1, "1") + + expect(success).to.equal(false) + expect(message:find("first is of type number, but second is of type string")).to.be.ok() + end) + + it("should compare (and report about) nested tables", function() + local A = { + foo = "bar", + nested = { + foo = 1, + bar = 2, + } + } + local B = { + foo = "bar", + nested = { + foo = 1, + bar = 2, + } + } + + expect(TableUtilities.DeepEqual(A, B)).to.equal(true) + + local C = { + foo = "bar", + nested = { + foo = 1, + bar = 3, + } + } + + local success, message = TableUtilities.DeepEqual(A, C) + + expect(success).to.equal(false) + expect(message:find("first%[nested%]%[bar%] ~= second%[nested%]%[bar%]")).to.be.ok() + end) + + it("should be commutative", function() + local equalArgsA = { + foo = "bar", + hello = "world", + } + local equalArgsB = { + foo = "bar", + hello = "world", + } + + expect(TableUtilities.DeepEqual(equalArgsA, equalArgsB)).to.equal(true) + expect(TableUtilities.DeepEqual(equalArgsB, equalArgsA)).to.equal(true) + + local nonEqualArgs = { + foo = "bar", + } + + local successA = TableUtilities.DeepEqual(equalArgsA, nonEqualArgs) + local successB = TableUtilities.DeepEqual(nonEqualArgs, equalArgsA) + + expect(successA).to.equal(false) + expect(successB).to.equal(false) + end) + + it("should give the appropriate message if the second table has extra fields", function() + local success, message = TableUtilities.DeepEqual({}, { foo = 1 }) + + expect(success).to.equal(false) + expect(message).to.equal("first[foo] is of type nil, but second[foo] is of type number") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/TrackNavigationEvents.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/TrackNavigationEvents.spec.lua new file mode 100644 index 0000000..1491d9b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/TrackNavigationEvents.spec.lua @@ -0,0 +1,36 @@ +local RoactNavigation = require(script.Parent.Parent.Parent) +local TrackNavigationEvents = require(script.Parent.Parent.TrackNavigationEvents) +local PageNavigationEvent = require(script.Parent.Parent.PageNavigationEvent) + + +return function() + local testPage = "TEST PAGE" + local testPageWillFocus = PageNavigationEvent.new(testPage, RoactNavigation.Events.WillFocus) + local testPageWillBlur = PageNavigationEvent.new(testPage, RoactNavigation.Events.WillBlur) + + local trackNavigationEvents = TrackNavigationEvents.new() + it("should implement equalTo function", function() + expect(trackNavigationEvents:equalTo({})).to.be.equal(true) + + local navigationEvents = trackNavigationEvents:getNavigationEvents() + table.insert(navigationEvents, testPageWillFocus) + expect(trackNavigationEvents:equalTo({testPageWillFocus})).to.be.equal(true) + expect(trackNavigationEvents:equalTo({})).to.be.equal(false) + + table.insert(navigationEvents, testPageWillBlur) + expect(trackNavigationEvents:equalTo({testPageWillFocus, testPageWillBlur})).to.be.equal(true) + + table.insert(navigationEvents, testPageWillFocus) + expect(trackNavigationEvents:equalTo({testPageWillFocus, testPageWillBlur})).to.be.equal(false) + expect(trackNavigationEvents:equalTo({testPageWillFocus, testPageWillBlur, testPageWillFocus})).to.be.equal(true) + end) + + it("should be empty after reset", function() + trackNavigationEvents:resetNavigationEvents() + local navigationEvents = trackNavigationEvents:getNavigationEvents() + expect(typeof(navigationEvents)).to.be.equal('table') + expect(#navigationEvents).to.be.equal(0) + expect(trackNavigationEvents:equalTo({})).to.be.equal(true) + end) + +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/createSpy.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/createSpy.spec.lua new file mode 100644 index 0000000..698fee3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/createSpy.spec.lua @@ -0,0 +1,145 @@ +return function() + local createSpy = require(script.Parent.Parent.createSpy) + + describe("createSpy", function() + it("should create spies", function() + local spy = createSpy(function() end) + + expect(spy).to.be.ok() + end) + + it("should throw if spies are indexed by an invalid key", function() + local spy = createSpy(function() end) + + expect(function() + return spy.test + end).to.throw() + end) + end) + + describe("value", function() + it("should increment callCount when called", function() + local spy = createSpy(function() end) + spy.value() + + expect(spy.callCount).to.equal(1) + end) + + it("should store all values passed", function() + local spy = createSpy(function() end) + spy.value(1, true, "3") + + expect(spy.valuesLength).to.equal(3) + expect(spy.values[1]).to.equal(1) + expect(spy.values[2]).to.equal(true) + expect(spy.values[3]).to.equal("3") + end) + + it("should return the value of the inner function", function() + local spy = createSpy(function() + return true + end) + + expect(spy.value()).to.equal(true) + end) + end) + + describe("assertCalledWith", function() + it("should throw if the number of values differs", function() + local spy = createSpy(function() end) + spy.value(1, 2) + + expect(function() + spy:assertCalledWith(1) + end).to.throw() + end) + + it("should throw if any value differs", function() + local spy = createSpy(function() end) + spy.value(1, 2) + + expect(function() + spy:assertCalledWith(1, 3) + end).to.throw() + + expect(function() + spy:assertCalledWith(2, 3) + end).to.throw() + end) + end) + + describe("captureValues", function() + it("should throw if the number of values differs", function() + local spy = createSpy(function() end) + spy.value(1, 2) + + expect(function() + spy:captureValues("a") + end).to.throw() + end) + + it("should capture all values in a table", function() + local spy = createSpy(function() end) + spy.value(1, 2) + + local captured = spy:captureValues("a", "b") + expect(captured.a).to.equal(1) + expect(captured.b).to.equal(2) + end) + end) + + describe("calls", function() + it("should keep all the arguments given to each call", function() + local spy = createSpy() + local firstCall = {} + spy.value(firstCall) + local secondCall = {} + spy.value(secondCall) + + expect(spy.calls[1][1]).to.equal(firstCall) + expect(spy.calls[2][1]).to.equal(secondCall) + end) + end) + + describe("mockClear", function() + it("clears the spy state", function() + local spy = createSpy() + spy.value(1) + expect(spy.callCount).to.equal(1) + + spy:mockClear() + + expect(spy.callCount).to.equal(0) + expect(#spy.calls).to.equal(0) + expect(#spy.values).to.equal(0) + expect(spy.valuesLength).to.equal(0) + end) + end) + + describe("call", function() + it("should increment callCount when called", function() + local spy = createSpy(function() end) + spy() + + expect(spy.callCount).to.equal(1) + end) + + it("should store all values passed", function() + local spy = createSpy(function() end) + spy(1, true, "3") + + expect(spy.valuesLength).to.equal(3) + expect(spy.values[1]).to.equal(1) + expect(spy.values[2]).to.equal(true) + expect(spy.values[3]).to.equal("3") + end) + + it("should return the value of the inner function", function() + local spy = createSpy(function() + return true + end) + + expect(spy()).to.equal(true) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/getActiveChildNavigationOptions.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/getActiveChildNavigationOptions.roblox.spec.lua new file mode 100644 index 0000000..99c57a5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/getActiveChildNavigationOptions.roblox.spec.lua @@ -0,0 +1,47 @@ +return function() + local getActiveChildNavigationOptions = require(script.Parent.Parent.getActiveChildNavigationOptions) + + it("should return a function", function() + expect(type(getActiveChildNavigationOptions)).to.equal("function") + end) + + it("should ask router for current screen options and return them", function() + local testInputScreenOpts = {} + local testScreenOpts = {} + + local navigation = { + state = { + routes = { + { key = "123" } + }, + index = 1, + + }, + router = {}, -- stub + } + + function navigation.getChildNavigation(key) + if key == "123" then + return navigation + else + return nil + end + end + + local testOutputScreenOpts = nil + function navigation.router.getScreenOptions(activeNav, screenProps) + testOutputScreenOpts = screenProps + + if activeNav == navigation then + return testScreenOpts + else + return nil + end + end + + expect(getActiveChildNavigationOptions(navigation, testInputScreenOpts)) + .to.equal(testScreenOpts) + expect(testOutputScreenOpts).to.equal(testInputScreenOpts) + end) +end + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/isValidScreenComponent.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/isValidScreenComponent.spec.lua new file mode 100644 index 0000000..e1bbffc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/isValidScreenComponent.spec.lua @@ -0,0 +1,25 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local isValidScreenComponent = require(script.Parent.Parent.isValidScreenComponent) + local TestComponent = Roact.Component:extend("TestFoo") + it("should return true for valid element types", function() + -- Function Component is valid + expect(isValidScreenComponent(function() end)).to.equal(true) + -- Stateful Component is valid + expect(isValidScreenComponent(TestComponent)).to.equal(true) + expect(isValidScreenComponent( + { render = function() return TestComponent end })).to.equal(true) + expect(isValidScreenComponent( -- we do not test if render function returns valid component + { render = function() end })).to.equal(true) + end) + + it("should return false for invalid element types", function() + expect(isValidScreenComponent("foo")).to.equal(false) + expect(isValidScreenComponent(Roact.createElement("Frame"))).to.equal(false) + expect(isValidScreenComponent(5)).to.equal(false) + expect(isValidScreenComponent(Roact.Portal)).to.equal(false) + expect(isValidScreenComponent({ render = "bad" })).to.equal(false) + expect(isValidScreenComponent( + { notRender = function() return "foo" end })).to.equal(false) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/lerp.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/lerp.spec.lua new file mode 100644 index 0000000..6996e7a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/_tests_/lerp.spec.lua @@ -0,0 +1,21 @@ +return function() + local lerp = require(script.Parent.Parent.lerp) + + it("should return bottom of range for bottom input", function() + expect(lerp(0, 1, 0)).to.equal(0) + expect(lerp(1, 0, 0)).to.equal(1) + expect(lerp(-1, 0, 0)).to.equal(-1) + end) + + it("should return middle of range for middle input", function() + expect(lerp(0, 1, 0.5)).to.equal(0.5) + expect(lerp(1, 0, 0.5)).to.equal(0.5) + expect(lerp(-1, 0, 0.5)).to.equal(-0.5) + end) + + it("should return top of range for top input", function() + expect(lerp(0, 1, 1)).to.equal(1) + expect(lerp(1, 0, 1)).to.equal(0) + expect(lerp(-1, 0, 1)).to.equal(0) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/createSpy.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/createSpy.lua new file mode 100644 index 0000000..96001a8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/createSpy.lua @@ -0,0 +1,93 @@ +-- Taken from Roact + +local expectDeepEqual = require(script.Parent.expectDeepEqual) + +local function createSpy(inner) + local self = { + callCount = 0, + calls = {}, + values = {}, + valuesLength = 0, + } + + self.value = function(...) + self.callCount = self.callCount + 1 + self.values = {...} + self.valuesLength = select("#", ...) + table.insert(self.calls, self.values) + + if inner ~= nil then + return inner(...) + end + + return + end + + self.assertCalledWith = function(_, ...) + local len = select("#", ...) + + if self.valuesLength ~= len then + error(("Expected %d arguments, but was called with %d arguments"):format( + self.valuesLength, + len + ), 2) + end + + for i = 1, len do + local expected = select(i, ...) + + assert(self.values[i] == expected, "value differs") + end + end + + self.assertCalledWithDeepEqual = function(_, ...) + local len = select("#", ...) + + if self.valuesLength ~= len then + error(("Expected %d arguments, but was called with %d arguments"):format( + self.valuesLength, + len + ), 2) + end + + for i = 1, len do + local expected = select(i, ...) + + expectDeepEqual(self.values[i], expected) + end + end + + self.captureValues = function(_, ...) + local len = select("#", ...) + local result = {} + + assert(self.valuesLength == len, "length of expected values differs from stored values") + + for i = 1, len do + local key = select(i, ...) + result[key] = self.values[i] + end + + return result + end + + self.mockClear = function() + self.callCount = 0 + self.calls = {} + self.values = {} + self.valuesLength = 0 + end + + setmetatable(self, { + __index = function(_, key) + error(("%q is not a valid member of spy"):format(key)) + end, + __call = function(_, ...) + return self.value(...) + end + }) + + return self +end + +return createSpy diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/expectDeepEqual.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/expectDeepEqual.lua new file mode 100644 index 0000000..70419a9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/expectDeepEqual.lua @@ -0,0 +1,10 @@ +local TableUtilities = require(script.Parent.TableUtilities) + +return function(a, b) + local success, innerMessage = TableUtilities.DeepEqual(a, b) + + if not success then + local message = ("Values were not deep-equal.\n%s"):format(innerMessage) + error(message, 2) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/expectDeepEqual.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/expectDeepEqual.spec.lua new file mode 100644 index 0000000..8d22c6e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/expectDeepEqual.spec.lua @@ -0,0 +1,47 @@ +return function() + local expectDeepEqual = require(script.Parent.expectDeepEqual) + + it("should fail with a message when args are not equal", function() + expect(function() + expectDeepEqual(1, 2) + end).to.throw("Values were not deep-equal.\nfirst ~= second") + + expect(function() + expectDeepEqual({ + foo = 1, + }, { + foo = 2, + }) + end).to.throw("Values were not deep-equal.\nfirst[foo] ~= second[foo]") + end) + + it("should succeed when comparing non-table equal values", function() + expect(function() + expectDeepEqual(1, 1) + end).never.to.throw() + expect(function() + expectDeepEqual("hello", "hello") + end).never.to.throw() + expect(function() + expectDeepEqual(nil, nil) + end).never.to.throw() + + local someFunction = function() end + local theSameFunction = someFunction + + expect(function() + expectDeepEqual(someFunction, theSameFunction) + end).never.to.throw() + + end) + + it("should succeed when comparing different table identities with same structure", function() + expect(function() + expectDeepEqual({ + foo = "bar", + }, { + foo = "bar", + }) + end).never.to.throw() + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/getActiveChildNavigationOptions.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/getActiveChildNavigationOptions.lua new file mode 100644 index 0000000..03a36ef --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/getActiveChildNavigationOptions.lua @@ -0,0 +1,14 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/6390aacd07fd647d925dfec842a766c8aad5272f/packages/core/src/utils/getActiveChildNavigationOptions.js + +-- deviation: no theme parameter because no support for theme +return function(navigation, screenProps) + local state = navigation.state + local router = navigation.router + local getChildNavigation = navigation.getChildNavigation + + local activeRoute = state.routes[state.index] + local activeNavigation = getChildNavigation(activeRoute.key) + + -- deviation: no support for theme + return router.getScreenOptions(activeNavigation, screenProps) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/getSceneIndicesForInterpolationInputRange.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/getSceneIndicesForInterpolationInputRange.lua new file mode 100644 index 0000000..a2e5c7c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/getSceneIndicesForInterpolationInputRange.lua @@ -0,0 +1,44 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) + +return function(props) + local scene = props.scene + local scenes = props.scenes + + local index = scene.index + local lastSceneIndexInScenes = #scenes + local isBack = not scenes[lastSceneIndexInScenes].isActive + + if isBack then + local currentSceneIndexInScenes = Cryo.List.find(scenes, scene) + + local targetSceneIndexInScenes = nil + for i, iScene in ipairs(scenes) do + if iScene.isActive then + targetSceneIndexInScenes = i + break + end + end + + local targetSceneIndex = scenes[targetSceneIndexInScenes].index + local lastSceneIndex = scenes[lastSceneIndexInScenes].index + + if index ~= targetSceneIndex and currentSceneIndexInScenes == lastSceneIndexInScenes then + return { + first = math.min(targetSceneIndex, index - 1), + last = index + 1, + } + elseif index == targetSceneIndex and currentSceneIndexInScenes == targetSceneIndexInScenes then + return { + first = index - 1, + last = math.max(lastSceneIndex, index + 1) + } + elseif index == targetSceneIndex or currentSceneIndexInScenes > targetSceneIndexInScenes then + return nil + end + end + + return { + first = index - 1, + last = index + 1 + } +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/isValidScreenComponent.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/isValidScreenComponent.lua new file mode 100644 index 0000000..66f9b26 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/isValidScreenComponent.lua @@ -0,0 +1,10 @@ +-- We have to do this using type because ElementKind is not exported by Roact +return function (screenComponent) + local componentType = type(screenComponent) + local valid = componentType == "function" or -- Function Component + (componentType == "table" and type(screenComponent.render) == "function") -- Stateful Component + return valid +end + + + diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/lerp.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/lerp.lua new file mode 100644 index 0000000..b06b11d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/lerp.lua @@ -0,0 +1,6 @@ +-- Helper interpolates t with range [0,1] into the range [a,b]. +local function lerp(a, b, t) + return a * (1 - t) + b * t +end + +return lerp diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/validate.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/validate.lua new file mode 100644 index 0000000..a31a4ce --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/utils/validate.lua @@ -0,0 +1,19 @@ +--[[ + Validate() provides a mechanism to validate input arguments and + internal state for your components. You can call it like this: + + function myFunc(arg1, arg2) + validate(arg1 ~= arg2, "arg1 (%s) and arg2 (%s) must be different!", + tostring(arg1), tostring(arg2)) + return doSomething(arg1, arg2) + end + + The error will be surfaced at the *call site* of your function. +]] +return function(result, ...) + if not result then + error(string.format(...), 3) + end + + return result +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/NavigationContext.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/NavigationContext.lua new file mode 100644 index 0000000..db2d276 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/NavigationContext.lua @@ -0,0 +1,7 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/20e2625f351f90fadadbf98890270e43e744225b/packages/core/src/views/NavigationContext.ts + +local Roact = require(script.Parent.Parent.Parent.Roact) + +local NavigationContext = Roact.createContext(nil) + +return NavigationContext diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/NavigationEvents.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/NavigationEvents.lua new file mode 100644 index 0000000..1791f19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/NavigationEvents.lua @@ -0,0 +1,104 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/6390aacd07fd647d925dfec842a766c8aad5272f/packages/core/src/views/NavigationEvents.js +local root = script.Parent.Parent +local Packages = root.Parent +local Roact = require(Packages.Roact) +local withNavigation = require(script.Parent.withNavigation) +local Events = require(root.Events) + +--[[ + NavigationEvents providers a wrapper component that allows you to subscribe + to the navigation lifecycle events without having to explicitly manage your own + listener subscriptions. + + Usage: + + function MyComponent:init() + self.willFocus = function() + -- Do tasks that need to happen right before the component will appear on screen. + end + + self.didFocus = function() + -- Do tasks that need to happen right after the component appears on screen. + end + end + + function MyComponent:render() + -- Note that you must capture the self reference lexically, if you need it. + return Roact.createElement(RoactNavigation.NavigationEvents, { + onWillFocus = self.willFocus, + onDidFocus = self.didFocus, + onWillBlur = self.willBlur, + onDidBlur = self.didBlur, + }) + end + + Remember that focus and blur events may be called more than once in the lifetime of a + component. If you navigate away from a component and then come back later, it will receive + willBlur/didBlur and then willFocus/didFocus events. + + Also remember that your event handlers must capture any self reference lexically, if necessary. +]] + +local EventNameToPropName = { + [Events.WillFocus] = "onWillFocus", + [Events.DidFocus] = "onDidFocus", + [Events.WillBlur] = "onWillBlur", + [Events.DidBlur] = "onDidBlur", +} + +local NavigationEvents = Roact.Component:extend("NavigationEvents") + +function NavigationEvents:didMount() + -- We register all navigation listeners on mount to ensure listener stability across re-render + -- A former implementation was replacing (removing/adding) listeners on all update (if prop provided) + -- but there were issues (see https://github.com/react-navigation/react-navigation/issues/5058) + self:subscribeAll() +end + +function NavigationEvents:didUpdate(prevProps) + if self.props.navigation ~= prevProps.navigation then + -- This component might get reused for different state, so we need to hook back up to events + self:removeAll() + self:subscribeAll() + end +end + +function NavigationEvents:willUnmount() + self:removeAll() +end + +function NavigationEvents:getPropListener(eventName) + return self.props[EventNameToPropName[eventName]] +end + +function NavigationEvents:subscribeAll() + local navigation = self.props.navigation + + self.subscriptions = {} + + for symbol in pairs(EventNameToPropName) do + self.subscriptions[symbol] = navigation.addListener(symbol, function(...) + -- Retrieve callback from props each time, in case props change. + local callback = self:getPropListener(symbol) + if callback then + callback(...) + end + end) + end +end + +function NavigationEvents:removeAll() + for symbol in pairs(EventNameToPropName) do + local sub = self.subscriptions[symbol] + if sub then + sub.remove() + self.subscriptions[symbol] = nil + end + end +end + +function NavigationEvents:render() + return nil +end + +return withNavigation(NavigationEvents) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/NavigationFocusEvents.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/NavigationFocusEvents.lua new file mode 100644 index 0000000..67fabd0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/NavigationFocusEvents.lua @@ -0,0 +1,347 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/9b55493e7662f4d54c21f75e53eb3911675f61bc/packages/core/src/views/NavigationFocusEvents.js + +local root = script.Parent.Parent +local Packages = root.Parent +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) +local Events = require(root.Events) + +local NavigationEventManager = Roact.Component:extend("NavigationEventManager") + +function NavigationEventManager:didMount() + local navigation = self.props.navigation + + self._actionSubscription = navigation.addListener( + Events.Action, + function(...) + return self:_handleAction(...) + end + ) + + self._willFocusSubscription = navigation.addListener( + Events.WillFocus, + function(...) + return self:_handleWillFocus(...) + end + ) + + self._willBlurSubscription = navigation.addListener( + Events.WillBlur, + function(...) + return self:_handleWillBlur(...) + end + ) + + self._didFocusSubscription = navigation.addListener( + Events.DidFocus, + function(...) + return self:_handleDidFocus(...) + end + ) + + self._didBlurSubscription = navigation.addListener( + Events.DidBlur, + function(...) + return self:_handleDidBlur(...) + end + ) + + self._refocusSubscription = navigation.addListener( + Events.Refocus, + function(...) + return self:_handleRefocus(...) + end + ) +end + +function NavigationEventManager:willUnmount() + if self._actionSubscription then + self._actionSubscription.remove() + end + if self._willFocusSubscription then + self._willFocusSubscription.remove() + end + if self._willBlurSubscription then + self._willBlurSubscription.remove() + end + if self._didFocusSubscription then + self._didFocusSubscription.remove() + end + if self._didBlurSubscription then + self._didBlurSubscription.remove() + end + if self._refocusSubscription then + self._refocusSubscription.remove() + end +end + +function NavigationEventManager:_handleAction(actionPayload) + local state = actionPayload.state + local lastState = actionPayload.lastState + local action = actionPayload.action + local type = actionPayload.type + local context = actionPayload.context + + local navigation = self.props.navigation + local onEvent = self.props.onEvent + + -- We should only emit events when the navigator is focused + -- When navigator is not focused, screens inside shouldn"t receive focused status either + if not navigation.isFocused() then + return + end + + local previous + if lastState and lastState.routes then + previous = lastState.routes[lastState.index] + end + local current = state.routes[state.index] + + local payload = { + context = ("%s:%s_%s"):format(current.key, tostring(action.type), context or "Root"), + state = current, + lastState = previous, + action = action, + type = type, + } + + if (previous and previous.key) ~= current.key then + self:_emitWillFocus(current.key, payload) + + if previous and previous.key then + self:_emitWillBlur(previous.key, payload) + end + end + + if lastState and lastState.isTransitioning ~= state.isTransitioning and + state.isTransitioning == false + then + if self._lastWillBlurKey then + self:_emitDidBlur(self._lastWillBlurKey, payload) + end + + if self._lastWillFocusKey then + self:_emitDidFocus(self._lastWillFocusKey, payload) + end + end + + onEvent(current.key, Events.Action, payload) +end + +function NavigationEventManager:_handleWillFocus(args) + local lastState = args.lastState + local action = args.action + local context = args.context + local type = args.type + + local navigation = self.props.navigation + local route = navigation.state.routes[navigation.state.index] + + local nextLastState = nil + if lastState and lastState.routes then + local nextLastStateIndex = Cryo.List.findWhere(lastState and lastState.routes or {}, function(r) + return r.key == route.key + end) + if nextLastStateIndex then + nextLastState = lastState.routes[nextLastStateIndex] + end + end + + self:_emitWillFocus(route.key, { + context = ("%s:%s_%s"):format(route.key, tostring(action.type), context or "Root"), + state = route, + lastState = nextLastState, + action = action, + type = type, + }) +end + +function NavigationEventManager:_handleWillBlur(args) + local lastState = args.lastState + local action = args.action + local context = args.context + local type = args.type + + local navigation = self.props.navigation + local route = navigation.state.routes[navigation.state.index] + + local nextLastState = nil + if lastState and lastState.routes then + local nextLastStateIndex = Cryo.List.findWhere(lastState and lastState.routes or {}, function(r) + return r.key == route.key + end) + if nextLastStateIndex then + nextLastState = lastState.routes[nextLastStateIndex] + end + end + + self:_emitWillBlur(route.key, { + context = ("%s:%s_%s"):format(route.key, tostring(action.type), context or "Root"), + state = route, + lastState = nextLastState, + action = action, + type = type, + }); +end + +function NavigationEventManager:_handleDidFocus(args) + local lastState = args.lastState + local action = args.action + local context = args.context + local type = args.type + + local navigation = self.props.navigation + + if self._lastWillFocusKey then + local routeIndex = Cryo.List.findWhere(navigation.state.routes, function(r) + return r.key == self._lastWillFocusKey + end) + + if routeIndex then + local route = navigation.state.routes[routeIndex] + + local nextLastState = nil + if lastState and lastState.routes then + local nextLastStateIndex = Cryo.List.findWhere(lastState and lastState.routes or {}, function(r) + return r.key == route.key + end) + if nextLastStateIndex then + nextLastState = lastState.routes[nextLastStateIndex] + end + end + + self:_emitDidFocus(route.key, { + context = ("%s:%s_%s"):format(route.key, tostring(action.type), context or "Root"), + state = route, + lastState = nextLastState, + action = action, + type = type, + }); + end + end +end + +function NavigationEventManager:_handleDidBlur(args) + local lastState = args.lastState + local action = args.action + local context = args.context + local type = args.type + + local navigation = self.props.navigation + + if self._lastWillBlurKey then + local routeIndex = Cryo.List.findWhere(navigation.state.routes, function(r) + return r.key == self._lastWillBlurKey + end) + + if routeIndex then + local route = navigation.state.routes[routeIndex] + + local nextLastState = nil + if lastState and lastState.routes then + local nextLastStateIndex = Cryo.List.findWhere(lastState and lastState.routes or {}, function(r) + return r.key == route.key + end) + if nextLastStateIndex then + nextLastState = lastState.routes[nextLastStateIndex] + end + end + + self:_emitDidBlur(route.key, { + context = ("%s:%s_%s"):format(route.key, tostring(action.type), context or "Root"), + state = route, + lastState = nextLastState, + action = action, + type = type, + }); + end + end +end + +function NavigationEventManager:_handleRefocus() + local onEvent = self.props.onEvent + local navigation = self.props.navigation + local route = navigation.state.routes[navigation.state.index] + + onEvent(route.key, Events.Refocus) +end + +function NavigationEventManager:_emitWillFocus(target, payload) + if self._lastWillBlurKey == target then + self._lastWillBlurKey = nil + end + + if self._lastWillFocusKey == target then + return + end + + self._lastDidFocusKey = nil + self._lastWillFocusKey = target + + local navigation = self.props.navigation + local onEvent = self.props.onEvent + + onEvent(target, Events.WillFocus, payload); + + if typeof(navigation.state.isTransitioning) ~= "boolean" or + (navigation.state.isTransitioning ~= true and + not navigation._dangerouslyGetParent()) -- TODO: what should we do with dangerouslyGetParent + then + self:_emitDidFocus(target, payload) + end +end + +function NavigationEventManager:_emitWillBlur(target, payload) + if self._lastWillFocusKey == target then + self._lastWillFocusKey = nil + end + + if self._lastWillBlurKey == target then + return + end + + self._lastDidBlurKey = nil + self._lastWillBlurKey = target + + local navigation = self.props.navigation + local onEvent = self.props.onEvent + + onEvent(target, Events.WillBlur, payload) + + if typeof(navigation.state.isTransitioning) ~= "boolean" or + (navigation.state.isTransitioning ~= true and + not navigation._dangerouslyGetParent()) + then + self:_emitDidBlur(target, payload) + end +end + +function NavigationEventManager:_emitDidFocus(target, payload) + if self._lastWillFocusKey ~= target or self._lastDidFocusKey == target then + return + end + + self._lastDidFocusKey = target + + local onEvent = self.props.onEvent + + onEvent(target, Events.DidFocus, payload) +end + +function NavigationEventManager:_emitDidBlur(target, payload) + if self._lastWillBlurKey ~= target or self._lastDidBlurKey == target then + return + end + + self._lastDidBlurKey = target + + local onEvent = self.props.onEvent + + onEvent(target, Events.DidBlur, payload) +end + +function NavigationEventManager:render() + return nil +end + +return NavigationEventManager diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/ScenesReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/ScenesReducer.lua new file mode 100644 index 0000000..09b7d86 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/ScenesReducer.lua @@ -0,0 +1,203 @@ +local root = script.Parent.Parent.Parent +local Packages = root.Parent +local Cryo = require(Packages.Cryo) +local TableUtilities = require(root.utils.TableUtilities) +local validate = require(root.utils.validate) + +local SCENE_KEY_PREFIX = "scene_" + +-- Compare two scenes based upon index and view key. +local function compareScenes(a, b) + if a.index == b.index then + -- compare the route keys + local delta = #a.key - #b.key + if delta == 0 then + return a.key < b.key + else + return delta < 0 + end + else + -- rank by index first + return a.index < b.index + end +end + +local function routesAreShallowEqual(a, b) + if not a or not b then + return a == b + end + + if a.key ~= b.key then + return false + end + + return TableUtilities.ShallowEqual(a, b) +end + +local function scenesAreShallowEqual(a, b) + return + a.key == b.key and + a.index == b.index and + a.isStale == b.isStale and + a.isActive == b.isActive and + routesAreShallowEqual(a, b) +end + +return function(scenes, nextState, prevState, descriptors) + -- Always update descriptors. See react-navigation's bug, here: + -- https://github.com/react-navigation/react-navigation/issues/4271 + -- TODO: Do we need this? Can we do a real fix? + for _, scene in ipairs(scenes) do + local route = scene.route + if descriptors and descriptors[route.key] then + scene.descriptor = descriptors[route.key] + end + end + + -- Bail out early if state is not updated + if prevState == nextState then + return scenes + end + + local prevScenes = {} + local freshScenes = {} + local staleScenes = {} + + -- previously stale scenes should be marked stale + for _, scene in ipairs(scenes) do + local key = scene.key + if scene.isStale then + staleScenes[key] = scene + end + + prevScenes[key] = scene + end + + local nextKeys = {} -- fake set! + local nextRoutes = nextState.routes + local nextRoutesLength = #nextRoutes + + -- Clip nextRoutes to stop at index because index is top of stack! + if nextRoutesLength > nextState.index then + print("Warning: StackRouter provided invalid state. Index should always be the top route") + nextRoutes = Cryo.List.removeRange(nextRoutes, nextState.index, nextRoutesLength) + end + + for index, route in ipairs(nextRoutes) do + local key = SCENE_KEY_PREFIX .. route.key + local descriptor = descriptors and descriptors[route.key] or nil + + local scene = { + index = index, + isActive = false, + isStale = false, + key = key, + route = route, + descriptor = descriptor, + } + + validate(not nextKeys[key], "navigation.state.routes[%d].key '%s' conflicts with another route!", index, key) + nextKeys[key] = true + + if staleScenes[key] then + -- Previously stale scene was added to nextState, so we remove it from + -- the map of stale scenes. + staleScenes[key] = nil + end + + freshScenes[key] = scene + end + + if prevState then + local prevRoutes = prevState.routes + local prevRoutesLength = #prevRoutes + if prevRoutesLength > prevState.index then + print("StackRouter provided invalid state. Index should always be the top route.") + prevRoutes = Cryo.List.removeRange(prevRoutes, prevState.index, prevRoutesLength) + end + + -- Search previous routes and mark any removed scenes as stale + for index, route in ipairs(prevRoutes) do + local key = SCENE_KEY_PREFIX .. route.key + -- Skip any refreshed scenes + if not freshScenes[key] then + local lastScene = nil + for _, scene in ipairs(scenes) do + if scene.route.key == route.key then + lastScene = scene + break + end + end + + local descriptor = descriptors[route.key] + if lastScene then + descriptor = lastScene.descriptor + end + + if descriptor then + staleScenes[key] = { + index = index, + isActive = false, + isStale = true, + key = key, + route = route, + descriptor = descriptor, + } + end + end + end + end + + local nextScenes = {} + + local function mergeScene(nextScene) + local key = nextScene.key + local prevScene = prevScenes[key] or nil + if prevScene and scenesAreShallowEqual(prevScene, nextScene) then + -- reuse prevScene to avoid re-render + table.insert(nextScenes, prevScene) + else + table.insert(nextScenes, nextScene) + end + end + + for _, scene in pairs(staleScenes) do + mergeScene(scene) + end + + for _, scene in pairs(freshScenes) do + mergeScene(scene) + end + + table.sort(nextScenes, compareScenes) + + local activeScenesCount = 0 + for index, scene in ipairs(nextScenes) do + local isActive = not scene.isStale and scene.index == nextState.index + if isActive ~= scene.isActive then + nextScenes[index] = Cryo.Dictionary.join(scene, { + isActive = isActive, + }) + end + + if isActive then + activeScenesCount = activeScenesCount + 1 + end + end + + validate(activeScenesCount == 1, "There should only be one active scene, not %d", activeScenesCount) + + -- Conditionally return nextScenes based upon shallow comparison, for performance + if #nextScenes ~= #scenes then + return nextScenes + end + + for index, scene in ipairs(nextScenes) do + if not scenesAreShallowEqual(scenes[index], scene) then + return nextScenes + end + end + + -- Scenes have not changed + return scenes +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackPresentationStyle.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackPresentationStyle.lua new file mode 100644 index 0000000..d11af5e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackPresentationStyle.lua @@ -0,0 +1,43 @@ +local NavigationSymbol = require(script.Parent.Parent.Parent.NavigationSymbol) + +local DEFAULT_SYMBOL = NavigationSymbol("DEFAULT") +local MODAL_SYMBOL = NavigationSymbol("MODAL") +local OVERLAY_SYMBOL = NavigationSymbol("OVERLAY") + +--[[ + StackPresentationStyle is used with stack navigators/views to determine + the behavior of a given screen when it is pushed/popped from the stack, as + well as the visual effects/transitions applied while the view is on screen. +]] +return { + --[[ + The Default presentation style draws stack cards so that they will + slide in and out from the right side of the screen one at a time. No + special visual effects are applied, and cards always fill the entire space + available. Your screen content is rendered over an opaque background color + by default, but you have the option to draw the cards with a semi- or fully + transparent background via navigationOptions. Cards always prevent + tap-through of the content underneath them (in case your navigation + container is transparent). + ]] + Default = DEFAULT_SYMBOL, + + --[[ + The Modal presentation style causes screens to animate up/down from the + bottom of the navigation container and visually stack on top of each + other. Cards are opaque by default, but you may set navigationOptions + to make them semi- or fully transparent so that you can see the underlying + cards. Modal cards always prevent tap-through of any underlying UI, including + other cards in the same stack. + ]] + Modal = MODAL_SYMBOL, + + --[[ + The Overlay presentation style causes screens to pop in (later they will fade in) + on top of the underlying screens. Like modals, they visually stack on top of each + other. Cards are opaque by default, but you may set navigationOptions to make + them semi- or fully transparent. Overlay cards always prevent tap-through of any + underlying UI, including other cards in the same stack. + ]] + Overlay = OVERLAY_SYMBOL +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackView.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackView.lua new file mode 100644 index 0000000..e803b4c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackView.lua @@ -0,0 +1,136 @@ +local root = script.Parent.Parent.Parent +local Packages = root.Parent +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) +local StackActions = require(root.routers.StackActions) +local StackViewLayout = require(script.Parent.StackViewLayout) +local Transitioner = require(script.Parent.Transitioner) +local StackViewTransitionConfigs = require(script.Parent.StackViewTransitionConfigs) +local StackPresentationStyle = require(script.Parent.StackPresentationStyle) + +local defaultNavigationConfig = { + mode = StackPresentationStyle.Default, +} + + +local StackView = Roact.Component:extend("StackView") + +function StackView:init() + self._doRender = function(...) + return self:_render(...) + end + + self._doConfigureTransition = function(...) + return self:_configureTransition(...) + end + + self._doOnTransitionStart = function(...) + return self:_onTransitionStart(...) + end + + self._doOnTransitionEnd = function(...) + return self:_onTransitionEnd(...) + end + + self._doOnTransitionStep = function(...) + return self:_onTransitionStep(...) + end +end + +function StackView:render() + local screenProps = self.props.screenProps + local navigation = self.props.navigation + local descriptors = self.props.descriptors + + -- Transitioner handles setting up the animation motors and making that data + -- available to the lower layer. + return Roact.createElement(Transitioner, { + render = self._doRender, + configureTransition = self._doConfigureTransition, + screenProps = screenProps, + navigation = navigation, + descriptors = descriptors, + onTransitionStart = self._doOnTransitionStart, + onTransitionEnd = self._doOnTransitionEnd, + onTransitionStep = self._doOnTransitionStep, + }) +end + +function StackView:didMount() + local navigation = self.props.navigation + if navigation.state.isTransitioning then + navigation.dispatch(StackActions.completeTransition({ + key = navigation.state.key, + })) + end +end + +function StackView:_render(transition, lastTransition) + local screenProps = self.props.screenProps + local navigationConfig = Cryo.Dictionary.join(defaultNavigationConfig, self.props.navigationConfig) + local descriptors = self.props.descriptors + + return Roact.createElement(StackViewLayout, Cryo.Dictionary.join(navigationConfig, { + screenProps = screenProps, + descriptors = descriptors, + transitionProps = transition, + lastTransitionProps = lastTransition, + })) +end + +function StackView:_configureTransition(transition, lastTransition) + return StackViewTransitionConfigs.getTransitionConfig( + self.props.navigationConfig.transitionConfig, + transition, + lastTransition, + self.props.navigationConfig.mode + ).transitionSpec +end + +function StackView:_onTransitionStart(transition, lastTransition) + local onTransitionStart = self.props.onTransitionStart + or self.props.navigationConfig.onTransitionStart + + -- Only propagate transition changes to caller for transitions where the actual + -- index has changed. Transitioner sends updates for _all_ transitions, including + -- those to the same screen that result from animation completion events. + if onTransitionStart and transition.index ~= lastTransition.index then + onTransitionStart(transition.navigation, lastTransition.navigation) + end +end + +function StackView:_onTransitionEnd(transition, lastTransition) + local navigationConfig = self.props.navigationConfig + local navigation = self.props.navigation + local onTransitionEnd = self.props.onTransitionEnd or navigationConfig.onTransitionEnd + local transitionDestKey = transition.scene.route.key + local isCurrentKey = navigation.state.routes[navigation.state.index].key == transitionDestKey + + if transition.navigation.state.isTransitioning and isCurrentKey then + navigation.dispatch(StackActions.completeTransition({ + key = navigation.state.key, + toChildKey = transitionDestKey, + })) + end + + -- Only propagate transition changes to caller for transitions where the actual + -- index has changed. Transitioner sends updates for _all_ transitions, including + -- those to the same screen that result from animation completion events. + if onTransitionEnd and transition.index ~= lastTransition.index then + onTransitionEnd(transition.navigation, lastTransition.navigation) + end +end + +function StackView:_onTransitionStep(transition, lastTransition, value) + local onTransitionStep = self.props.onTransitionStep + or self.props.navigationConfig.onTransitionStep + + -- Only propagate transition changes to caller for transitions where the actual + -- index has changed. Transitioner sends updates for _all_ transitions, including + -- those to the same screen that result from animation completion events. + if onTransitionStep and transition.index ~= lastTransition.index then + onTransitionStep(transition.navigation, lastTransition.navigation, value) + end +end + +return StackView diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackViewCard.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackViewCard.lua new file mode 100644 index 0000000..b7b2046 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackViewCard.lua @@ -0,0 +1,116 @@ +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local validate = require(script.Parent.Parent.Parent.utils.validate) + +--[[ + Render a scene as a card for use in a StackView. This component is + responsible for correctly positioning the scene content in relation + to the other scenes. The content will be rendered inside a Frame + whose position and size are controlled by the transition logic. The + frame may either be transparent or a solid color, depending upon props. + Any additional visual effects must be supplied by the container or the + child element created by renderScene(). + + Props: + renderScene(scene) -- Render prop to draw the scene inside the card. + initialPosition -- Starting position for the card. (Animated by Otter from there). + positionStep -- Stepper function from StackViewInterpolator. + position -- Otter motor for the position of the card. + scene -- Scene that the card is to render. + forceHidden -- Forcibly disable card rendering (e.g. animated off-screen). + transparent -- Card allows underlying content to show through (default: false). + cardColor3 -- Color of the card background if it's not transparent (default: white). +]] +local StackViewCard = Roact.Component:extend("StackViewCard") + +StackViewCard.defaultProps = { + transparent = false, + cardColor3 = Color3.new(1, 1, 1), +} + +function StackViewCard:init() + local currentNavIndex = self.props.navigation.state.index + + self._isMounted = false + self._positionLastValue = currentNavIndex + + local selfRef = Roact.createRef() + self._getRef = function() + return self.props[Roact.Ref] or selfRef + end +end + +function StackViewCard:render() + local forceHidden = self.props.forceHidden + local cardColor3 = self.props.cardColor3 + local transparent = self.props.transparent + local initialPosition = self.props.initialPosition + local renderScene = self.props.renderScene + local scene = self.props.scene + + validate(type(renderScene) == "function", "renderScene must be a function") + + return Roact.createElement("Frame", { + Position = initialPosition, + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = cardColor3, + BackgroundTransparency = transparent and 1 or nil, + BorderSizePixel = 0, + ClipsDescendants = true, + Visible = not forceHidden, + [Roact.Ref] = self:_getRef(), + }, { + Content = renderScene(scene), + }) +end + +function StackViewCard:didMount() + self._isMounted = true + + local position = self.props.position + self._positionDisconnector = position:onStep(function(...) + self:_onPositionStep(...) + end) +end + +function StackViewCard:willUnmount() + self._isMounted = false + + if self._positionDisconnector then + self._positionDisconnector() + self._positionDisconnector = nil + end +end + +function StackViewCard:didUpdate(oldProps) + local position = self.props.position + local positionStep = self.props.positionStep + + if position ~= oldProps.position then + self._positionDisconnector() + self._positionDisconnector = position:onStep(function(...) + self:_onPositionStep(...) + end) + end + + if positionStep ~= oldProps.positionStep then + -- The motor won't fire just because stepper function has changed. We have to + -- update the position to match new requirements based upon last motor value. + self:_onPositionStep(self._positionLastValue) + end +end + +function StackViewCard:_onPositionStep(value) + if not self._isMounted then + return + end + + local positionStep = self.props.positionStep + + if positionStep then + positionStep(self:_getRef(), value) + end + + self._positionLastValue = value +end + +return StackViewCard diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackViewInterpolator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackViewInterpolator.lua new file mode 100644 index 0000000..0ca0a09 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackViewInterpolator.lua @@ -0,0 +1,216 @@ +--[[ + Provides builders to create functions that interpolate the current Otter motor + position into the correct translation for stack cards based upon their associated + scene. + + Interpolator builders expect the following props as input: + { + initialPositionValue = , + scene = , + layout = { + initWidth = , + initHeight = , + isMeasured = , + } + } + + Each builder returns a props table to be merged onto your other StackViewCard props, ex: + { + positionStep = , + initialPosition = , + forceHidden = true, -- May disable card visibility if it's outside interpolating range. + } + + The props table may contain other changes, depending on the requirements of the animation. +]] +local getSceneIndicesForInterpolationInputRange = require( + script.Parent.Parent.Parent.utils.getSceneIndicesForInterpolationInputRange) +local lerp = require(script.Parent.Parent.Parent.utils.lerp) + +-- Render initial style when layout hasn't been measured yet. +local function forInitial(props) + local initialPositionValue = props.initialPositionValue + local scene = props.scene + + local forceHidden = initialPositionValue ~= scene.index + local translate = forceHidden and 1000000 or 0 + + return { + forceHidden = forceHidden, + initialPosition = UDim2.new(0, translate, 0, translate), + positionStep = nil, + } +end + +-- Slide-in from right style (e.g. navigation stack view). +local function forHorizontal(props) + local initialPositionValue = props.initialPositionValue + local layout = props.layout + local scene = props.scene + + if not layout.isMeasured then + return forInitial(props) + end + + local interpolate = getSceneIndicesForInterpolationInputRange(props) + + -- getSceneIndices* returns nil if card is not visible and need not be + -- considered for the animation until state changes. + if not interpolate then + return { + forceHidden = true, + initialPosition = UDim2.new(0, 100000, 0, 100000), + positionStep = nil, + } + end + + local first = interpolate.first + local last = interpolate.last + local index = scene.index + + local width = layout.initWidth + + local function calculate(positionValue) + -- 3 range LERP + if positionValue < first then + return width + elseif positionValue < index then + return lerp(width, 0, (positionValue - first) / (index - first)) + elseif positionValue == index then + return 0 + elseif positionValue < last then + return lerp(0, -width, (positionValue - index) / (last - index)) + else + return -width + end + end + + local function stepper(cardRef, positionValue) + local cardInstance = cardRef.current + if not cardInstance then + return + end + + local oldPosition = cardInstance.Position + cardInstance.Position = UDim2.new( + oldPosition.X.Scale, + calculate(positionValue), + oldPosition.Y.Scale, + oldPosition.Y.Offset + ) + end + + local initialPosition = UDim2.new(0, calculate(initialPositionValue), 0, 0) + + return { + initialPosition = initialPosition, + positionStep = stepper, + } +end + +-- Slide-in from bottom style (e.g. modals). +local function forVertical(props) + local initialPositionValue = props.initialPositionValue + local layout = props.layout + local scene = props.scene + + if not layout.isMeasured then + return forInitial(props) + end + + local interpolate = getSceneIndicesForInterpolationInputRange(props) + + if not interpolate then + return { + forceHidden = true, + initialPosition = UDim2.new(0, 100000, 0, 100000), + positionStep = nil, + } + end + + local first = interpolate.first + local index = scene.index + local height = layout.initHeight + + local function calculate(positionValue) + -- 2 range LERP + if positionValue < first then + return height + elseif positionValue < index then + return lerp(height, 0, (positionValue - first) / (index - first)) + else + return 0 + end + end + + local function stepper(cardRef, positionValue) + local cardInstance = cardRef.current + if not cardInstance then + return + end + + local oldPosition = cardInstance.Position + cardInstance.Position = UDim2.new( + oldPosition.X.Scale, + oldPosition.X.Offset, + oldPosition.Y.Scale, + calculate(positionValue) + ) + end + + local initialPosition = UDim2.new(0, 0, 0, calculate(initialPositionValue)) + + return { + initialPosition = initialPosition, + positionStep = stepper, + } +end + +-- Fade in place animation (e.g. popovers and toasts). Note that since we don't currently have +-- group transparency, this 'animation' just pops the views in for now. +local function forFade(props) + local initialPositionValue = props.initialPositionValue + local layout = props.layout + local scene = props.scene + + if not layout.isMeasured then + return forInitial(props) + end + + local interpolate = getSceneIndicesForInterpolationInputRange(props) + + if not interpolate then + return { + forceHidden = true, + initialPosition = UDim2.new(0, 100000, 0, 100000), + positionStep = nil, + } + end + + local index = scene.index + + local function calculate(positionValue) + return positionValue >= index - 0.5 + end + + local function stepper(cardRef, positionValue) + local cardInstance = cardRef.current + if not cardInstance then + return + end + + cardInstance.Visible = calculate(positionValue) + end + + return { + forceHidden = not calculate(initialPositionValue), + initialPosition = UDim2.new(0, 0, 0, 0), + positionStep = stepper, + } +end + +return { + forHorizontal = forHorizontal, + forVertical = forVertical, + forFade = forFade, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackViewLayout.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackViewLayout.lua new file mode 100644 index 0000000..342e38e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackViewLayout.lua @@ -0,0 +1,265 @@ +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local StackPresentationStyle = require(script.Parent.StackPresentationStyle) +local StackViewTransitionConfigs = require(script.Parent.StackViewTransitionConfigs) +local StackViewOverlayFrame = require(script.Parent.StackViewOverlayFrame) +local StackViewCard = require(script.Parent.StackViewCard) +local SceneView = require(script.Parent.Parent.SceneView) + +local defaultScreenOptions = { + absorbInput = true, + overlayEnabled = false, + overlayColor3 = Color3.new(0, 0, 0), + overlayTransparency = 0.7, + -- cardColor3 default is provided by StackViewCard + renderOverlay = function(navigationOptions, initialTransitionValue, transitionChangedSignal) + -- NOTE: renderOverlay will not be called if sceneOptions.overlayEnabled evaluates false + return Roact.createElement(StackViewOverlayFrame, { + navigationOptions = navigationOptions, + initialTransitionValue = initialTransitionValue, + transitionChangedSignal = transitionChangedSignal, + }) + end, +} + +local function calculateTransitionValue(index, position) + return math.max(math.min(1 + position - index, 1), 0) +end + + +local StackViewLayout = Roact.Component:extend("StackViewLayout") + +function StackViewLayout:init() + local startingIndex = self.props.transitionProps.navigation.state.index + + self._isMounted = false + self._positionLastValue = startingIndex + + self._renderScene = function(scene) + return self:_renderInnerScene(scene) + end + + self._subscribeToOverlayUpdates = function(callback) + local position = self.props.transitionProps.position + local index = self.props.transitionProps.scene.index + + return position:onStep(function(value) + callback(calculateTransitionValue(index, value)) + end) + end +end + +function StackViewLayout:_renderCard(scene, navigationOptions) + local transitionProps = self.props.transitionProps -- Core animation info from Transitioner. + local lastTransitionProps = self.props.lastTransitionProps -- Previous transition info. + local transitionConfig = self.state.transitionConfig -- State based info from scene config. + + local cardColor3 = navigationOptions.cardColor3 + local overlayEnabled = navigationOptions.overlayEnabled + + local initialPositionValue = transitionProps.scene.index + if lastTransitionProps then + initialPositionValue = lastTransitionProps.scene.index + end + + local cardInterpolationProps = {} + local screenInterpolator = transitionConfig.screenInterpolator + if screenInterpolator then + cardInterpolationProps = screenInterpolator( + Cryo.Dictionary.join(transitionProps, { + initialPositionValue = initialPositionValue, + scene = scene, + }) + ) + end + + -- Merge down the various prop packages to be applied to StackViewCard. + return Roact.createElement(StackViewCard, Cryo.Dictionary.join( + transitionProps, cardInterpolationProps, { + key = "card_" .. tostring(scene.key), + scene = scene, + renderScene = self._renderScene, + transparent = overlayEnabled, + cardColor3 = cardColor3, + }) + ) +end + +function StackViewLayout:_renderInnerScene(scene) + local navigation = scene.descriptor.navigation + + local sceneComponent = scene.descriptor.getComponent() + local screenProps = self.props.screenProps + + return Roact.createElement(SceneView, { + screenProps = screenProps, + navigation = navigation, + component = sceneComponent, + }) +end + +function StackViewLayout:render() + local transitionProps = self.props.transitionProps + local topMostOpaqueSceneIndex = self.state.topMostOpaqueSceneIndex + local scenes = transitionProps.scenes + + local renderedScenes = Cryo.List.map(scenes, function(scene) + -- The card is obscured if: + -- It's not the active card (e.g. we're transitioning TO it). + -- It's hidden underneath an opaque card that is NOT currently transitioning. + -- It's completely off-screen. + local cardObscured = scene.index < topMostOpaqueSceneIndex and not scene.isActive + + local screenOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {}) + local overlayEnabled = screenOptions.overlayEnabled + local absorbInput = screenOptions.absorbInput + local renderOverlay = screenOptions.renderOverlay + + local stationaryContent = nil + if overlayEnabled then + stationaryContent = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + ZIndex = 1, + }, { + Overlay = renderOverlay( + screenOptions, + calculateTransitionValue(scene.index, self._positionLastValue), + self._subscribeToOverlayUpdates) + }) + end + + -- Wrapper frame holds default/custom card background and the card content. + -- It MUST be a Frame when absorbInput=false because of legacy behavior on desktop + -- for GuiObject.Active=false blocking mouse clicks from falling through. + -- (Active=false DOES work on mobile, but not desktop). + -- When absorbInput=true, we add a TextButton behind the frame that will catch + -- mouse clicks + local absorbInputElement = nil + if not cardObscured and absorbInput then + absorbInputElement = Roact.createElement("TextButton", { + Active = true, + AutoButtonColor = false, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = true, + Size = UDim2.new(1, 0, 1, 0), + Text = " ", + ZIndex = 2 * scene.index - 1, + }) + end + + return Roact.createFragment({ + AbsorbInput = absorbInputElement, + -- use scene index for key, it makes testing with Rhodium easier + [tostring(scene.index)] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = true, + ZIndex = 2 * scene.index, + Visible = not cardObscured, + }, { + StationaryContent = stationaryContent, + DynamicContent = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + ZIndex = 2, + }, { + -- Cards need to have unique keys so that instances of the same components are not + -- reused for different scenes. (Could lead to unanticipated lifecycle problems). + ["card_" .. scene.key] = self:_renderCard(scene, screenOptions), + }) + }), + }) + end) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + }, renderedScenes) +end + +function StackViewLayout.getDerivedStateFromProps(nextProps, lastState) + local transitionProps = nextProps.transitionProps + local scenes = transitionProps.scenes + local state = transitionProps.navigation.state + local isTransitioning = state.isTransitioning + local topMostIndex = #scenes + + local isOverlayMode = nextProps.mode == StackPresentationStyle.Modal or + nextProps.mode == StackPresentationStyle.Overlay + + -- Find the last opaque scene in a modal stack so that we can optimize rendering. + local topMostOpaqueSceneIndex = 0 + if isOverlayMode then + for idx = topMostIndex, 1, -1 do + local scene = scenes[idx] + local navigationOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {}) + + -- Card covers other pages if it's not an overlay and it's not the top-most index while transitioning. + if not navigationOptions.overlayEnabled and not (isTransitioning and idx == topMostIndex) then + topMostOpaqueSceneIndex = idx + break + end + end + else + for idx = topMostIndex, 1, -1 do + if not (isTransitioning and idx == topMostIndex) then + topMostOpaqueSceneIndex = idx + break + end + end + end + + return { + topMostOpaqueSceneIndex = topMostOpaqueSceneIndex, + transitionConfig = StackViewTransitionConfigs.getTransitionConfig( + nextProps.transitionConfig, + nextProps.transitionProps, + nextProps.lastTransitionProps, + nextProps.mode), + } +end + +function StackViewLayout:didMount() + self._isMounted = true + + self._positionDisconnector = self.props.transitionProps.position:onStep(function(...) + self:_onPositionStep(...) + end) +end + +function StackViewLayout:willUnmount() + self._isMounted = false + + if self._positionDisconnector then + self._positionDisconnector() + self._positionDisconnector = nil + end +end + +function StackViewLayout:didUpdate(oldProps) + local position = self.props.transitionProps.position + + if position ~= oldProps.transitionProps.position then + self._positionDisconnector() + self._positionDisconnector = position:onStep(function(...) + self:_onPositionStep(...) + end) + end +end + +function StackViewLayout:_onPositionStep(value) + if self._isMounted then + self._positionLastValue = value + end +end + +return StackViewLayout diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackViewOverlayFrame.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackViewOverlayFrame.lua new file mode 100644 index 0000000..6d293e3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackViewOverlayFrame.lua @@ -0,0 +1,71 @@ +local Roact = require(script.Parent.Parent.Parent.Parent.Roact) +local lerp = require(script.Parent.Parent.Parent.utils.lerp) + +local StackViewOverlayFrame = Roact.Component:extend("StackViewOverlayFrame") + +function StackViewOverlayFrame:init() + self._signalDisconnect = nil + + local selfRef = Roact.createRef() + self._getRef = function() + return self.props[Roact.Ref] or selfRef + end +end + +function StackViewOverlayFrame:render() + local navigationOptions = self.props.navigationOptions + local initialTransitionValue = self.props.initialTransitionValue + + local overlayTransparency = lerp(1, navigationOptions.overlayTransparency, initialTransitionValue) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = navigationOptions.overlayColor3, + BackgroundTransparency = overlayTransparency, + BorderSizePixel = 0, + [Roact.Ref] = self:_getRef(), + }) +end + +function StackViewOverlayFrame:didUpdate(oldProps) + local transitionChangedSignal = self.props.transitionChangedSignal + + if transitionChangedSignal ~= oldProps.transitionChangedSignal then + if self._signalDisconnect then + self._signalDisconnect() + end + + self._signalDisconnect = transitionChangedSignal(function(...) + self:_transitionChanged(...) + end) + end +end + +function StackViewOverlayFrame:didMount() + self._isMounted = true + self._signalDisconnect = self.props.transitionChangedSignal(function(...) + self:_transitionChanged(...) + end) +end + +function StackViewOverlayFrame:willUnmount() + self._isMounted = false + if self._signalDisconnect then + self._signalDisconnect() + end +end + +function StackViewOverlayFrame:_transitionChanged(value) + if not self._isMounted then + return + end + + local myRef = self:_getRef() + if myRef.current then + local navigationOptions = self.props.navigationOptions + local overlayTransparency = lerp(1, navigationOptions.overlayTransparency, value) + myRef.current.BackgroundTransparency = overlayTransparency + end +end + +return StackViewOverlayFrame diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackViewTransitionConfigs.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackViewTransitionConfigs.lua new file mode 100644 index 0000000..83e7a33 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/StackViewTransitionConfigs.lua @@ -0,0 +1,53 @@ +local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo) +local StackViewInterpolator = require(script.Parent.StackViewInterpolator) +local StackPresentationStyle = require(script.Parent.StackPresentationStyle) + +local DefaultTransitionSpec = { + frequency = 3, -- Hz + dampingRatio = 1, +} + +local SlideFromRight = { + transitionSpec = DefaultTransitionSpec, + screenInterpolator = StackViewInterpolator.forHorizontal, +} + +local ModalSlideFromBottom = { + transitionSpec = DefaultTransitionSpec, + screenInterpolator = StackViewInterpolator.forVertical, +} + +local FadeInPlace = { + transitionSpec = DefaultTransitionSpec, + screenInterpolator = StackViewInterpolator.forFade, +} + +local function getDefaultTransitionConfig(transitionProps, prevTransitionProps, presentationStyle) + if presentationStyle == StackPresentationStyle.Modal then + return ModalSlideFromBottom + elseif presentationStyle == StackPresentationStyle.Overlay then + return FadeInPlace + else + return SlideFromRight + end +end + +local function getTransitionConfig(transitionConfigurer, transitionProps, prevTransitionProps, presentationStyle) + local defaultConfig = getDefaultTransitionConfig(transitionProps, prevTransitionProps, presentationStyle) + if transitionConfigurer then + return Cryo.Dictionary.join( + defaultConfig, + transitionConfigurer(transitionProps, prevTransitionProps, presentationStyle) + ) + end + + return defaultConfig +end + +return { + getDefaultTransitionConfig = getDefaultTransitionConfig, + getTransitionConfig = getTransitionConfig, + SlideFromRight = SlideFromRight, + ModalSlideFromBottom = ModalSlideFromBottom, + FadeInPlace = FadeInPlace, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/Transitioner.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/Transitioner.lua new file mode 100644 index 0000000..de5d625 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/Transitioner.lua @@ -0,0 +1,312 @@ +local root = script.Parent.Parent.Parent +local Packages = root.Parent +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) +local Otter = require(Packages.Otter) +local ScenesReducer = require(script.Parent.ScenesReducer) +local validate = require(root.utils.validate) + +local DEFAULT_TRANSITION_SPEC = { + frequency = 4 -- Hz +} + +local function buildTransitionProps(props, state) + local navigation = props.navigation + local options = props.options + + local layout = state.layout + local position = state.position + local scenes = state.scenes + + local activeScene + for _, x in ipairs(scenes) do + if x.isActive then + activeScene = x + break + end + end + + validate(activeScene, "Could not find active scene") + + return { + layout = layout, + navigation = navigation, + position = position, + scenes = scenes, + scene = activeScene, + options = options, + index = activeScene.index, + } +end + +local function filterStale(scenes) + local filtered = Cryo.List.filter(scenes, function(scene) + return not scene.isStale + end) + + if #filtered == #scenes then + return scenes + else + return filtered + end +end + +local Transitioner = Roact.Component:extend("Transitioner") + +function Transitioner:init() + local navigationState = self.props.navigation.state + local descriptors = self.props.descriptors + + self.state = { + -- Layout is passed to StackViewLayout in order to allow it to + -- sync animations. + layout = { + initWidth = 0, + initHeight = 0, + isMeasured = false, + }, + position = Otter.createSingleMotor(navigationState.index), + scenes = ScenesReducer({}, navigationState, nil, descriptors), + } + + self._doOnAbsoluteSizeChanged = function(...) + return self:_onAbsoluteSizeChanged(...) + end + + self._positionLastValue = navigationState.index + + self._prevTransitionProps = nil + self._transitionProps = buildTransitionProps(self.props, self.state) + + self._isMounted = false + self._isTransitionRunning = false + self._transitionQueue = {} + + self._completeSignalDisconnector = self.state.position:onComplete(function() + -- This spawn is required because of this Otter bug: https://github.com/Roblox/otter/issues/26 + -- Otter.SingleMotor's step function calls onComplete before it calls stop(). This leaves their + -- __running=true, and the setGoal() in our _onTransitionEnd() does nothing. So the whole queue + -- handling just stops cold, and the transition never actually happens! + spawn(function() + if self._isMounted then + self:_onTransitionEnd() + end + end) + end) + + self._stepSignalDisconnector = self.state.position:onStep(function(value) + if self._isMounted then + self:_onPositionStep(value) + end + end) +end + +function Transitioner:didMount() + self._isMounted = true +end + +function Transitioner:willUnmount() + self._isMounted = false + + if self._completeSignalDisconnector then + self._completeSignalDisconnector() + self._completeSignalDisconnector = nil + end + + if self._stepSignalDisconnector then + self._stepSignalDisconnector() + self._stepSignalDisconnector = nil + end +end + +function Transitioner:didUpdate(prevProps) + -- React-navigation uses componentWillReceiveProps that is only called when Parent + -- re-renders or when this component is actually being given new props, so we need to + -- filter here. If not, this would trigger on setState and enter an infinite loop. + if self.props ~= prevProps then + if self._isTransitionRunning then + local mostRecentTransition = self._transitionQueue[#self._transitionQueue] or {} + -- don't enqueue spurious extra copies of same transition props + if mostRecentTransition.prevProps ~= prevProps then + table.insert(self._transitionQueue, { prevProps = prevProps }) + end + + return + end + + self:_startTransition(prevProps, self.props) + end +end + +function Transitioner:render() + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + ClipsDescendants = true, + [Roact.Change.AbsoluteSize] = self._doOnAbsoluteSizeChanged, + }, { + ["TransitionerScenes"] = self.props.render( + self._transitionProps, self._prevTransitionProps), + }) +end + +-- equivalent to React-Nav's Transitioner._onLayout +function Transitioner:_onAbsoluteSizeChanged(rbx) + local width = rbx.AbsoluteSize.X + local height = rbx.AbsoluteSize.Y + + if width == self.state.layout.initWidth and + height == self.state.layout.initHeight then + return + end + + local layout = Cryo.Dictionary.join(self.state.layout, { + initWidth = width, + initHeight = height, + isMeasured = true, + }) + + local nextState = Cryo.Dictionary.join(self.state, { + layout = layout, + }) + + self._transitionProps = buildTransitionProps(self.props, nextState) + + self:setState({ + layout = layout, + }) +end + +function Transitioner:_computeScenes(props, nextProps) + local nextScenes = ScenesReducer( + self.state.scenes, + nextProps.navigation.state, + props.navigation.state, + nextProps.descriptors) + + if not nextProps.navigation.state.isTransitioning then + nextScenes = filterStale(nextScenes) + end + + if nextScenes == self.state.scenes then + return nil + end + + return nextScenes +end + +function Transitioner:_startTransition(props, nextProps) + local indexHasChanged = props.navigation.state.index ~= nextProps.navigation.state.index + local nextScenes = self:_computeScenes(props, nextProps) + + if not nextScenes then + -- If nextScenes is nil, nothing has changed, so report transition end, then bail + self._prevTransitionProps = self._transitionProps + + -- Trigger end transition logic if we daisy-chained from queued transitions via _onTransitionEnd. + if self._isTransitionRunning then + self:_onTransitionEnd() + end + + return + end + + local nextState = Cryo.Dictionary.join(self.state, { + scenes = nextScenes, + }) + + local position = nextState.position + local toValue = nextProps.navigation.state.index + + -- compute transitionProps + self._prevTransitionProps = self._transitionProps + self._transitionProps = buildTransitionProps(nextProps, nextState) + local isTransitioning = self._transitionProps.navigation.state.isTransitioning + + if not isTransitioning or not indexHasChanged then + -- If state is not transitioning, then we go immediately to new index. + -- Likewise, if the index has not changed then we still need to set up initial + -- positions via setState. + self:setState(nextState) + + if nextProps.onTransitionStart then + nextProps.onTransitionStart(self._transitionProps, self._prevTransitionProps) + end + + -- motor will call _endTransition for us + position:setGoal(Otter.instant(toValue)) + elseif isTransitioning then + self._isTransitionRunning = true + self:setState(nextState) + + if nextProps.onTransitionStart then + nextProps.onTransitionStart(self._transitionProps, self._prevTransitionProps) + end + + local positionHasChanged = self._positionLastValue ~= toValue + if indexHasChanged and positionHasChanged then + -- get transition spec + local transitionUserSpec = {} + if nextProps.configureTransition then + transitionUserSpec = nextProps.configureTransition( + self._transitionProps, self._prevTransitionProps) or {} + end + + local transitionSpec = Cryo.Dictionary.join(DEFAULT_TRANSITION_SPEC, transitionUserSpec) + + -- motor will call _endTransition for us + position:setGoal(Otter.spring(nextProps.navigation.state.index, transitionSpec)) + else + -- Set motor to current state to trigger _endTransition call with correct sequencing. + position:setGoal(Otter.instant(nextProps.navigation.state.index)) + end + end +end + +function Transitioner:_onTransitionEnd() + local prevTransitionProps = self._prevTransitionProps + self._prevTransitionProps = nil + + local scenes = filterStale(self.state.scenes) + + local nextState = Cryo.Dictionary.join(self.state, { + scenes = scenes, + }) + + self._transitionProps = buildTransitionProps(self.props, nextState) + + self:setState(nextState) + + if self.props.onTransitionEnd then + self.props.onTransitionEnd(self._transitionProps, prevTransitionProps) + end + + local firstQueuedTransition = self._transitionQueue[1] + if firstQueuedTransition then + local prevProps = firstQueuedTransition.prevProps + self._transitionQueue = Cryo.List.removeIndex(self._transitionQueue, 1) + self:_startTransition(prevProps, self.props) + else + self._isTransitionRunning = false + end +end + +function Transitioner:_onPositionStep(value) + self._positionLastValue = value + + local targetIndex = self._transitionProps.index + + -- _prevTransitionProps can be nil, so guard against it. + local startingIndex = targetIndex + if self._prevTransitionProps then + startingIndex = self._prevTransitionProps.index + end + + if self.props.onTransitionStep and startingIndex ~= targetIndex then + local transitionValue = (value - startingIndex) / (targetIndex - startingIndex) + self.props.onTransitionStep(self._transitionProps, self._prevTransitionProps, transitionValue) + end +end + +return Transitioner diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/_tests_/ScenesReducer.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/_tests_/ScenesReducer.spec.lua new file mode 100644 index 0000000..d9b8c23 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/_tests_/ScenesReducer.spec.lua @@ -0,0 +1,266 @@ +return function() + -- ScenesReducer(scenes, nextState, prevState, descriptors) + local ScenesReducer = require(script.Parent.Parent.ScenesReducer) + + local initialRouteKey = "id-1" + local initialRouteName = "First Route" + local initialRoute = { + routeName = initialRouteName, + key = initialRouteKey, + } + + local initialState = { + index = 1, + key = "StackRouterRoot", + isTransitioning = false, + routes = { + initialRoute, + }, + } + + local initialDescriptors = { + [initialRouteKey] = { + key = initialRouteKey, + navigation = { + state = initialRoute, + }, + state = initialRoute, + }, + } + + local initialScenes = nil + + it("should generate valid initial scene", function() + local scenes = ScenesReducer({}, initialState, nil, initialDescriptors) + local sceneOne = scenes[1] + expect(#scenes).to.be.equal(1) + expect(sceneOne.route).to.be.equal(initialRoute) + expect(sceneOne.index).to.be.equal(1) + expect(sceneOne.isActive).to.be.equal(true) + expect(sceneOne.isStale).to.be.equal(false) + initialScenes = scenes + end) + + it("should update descriptor", function() + local scenes = ScenesReducer({}, initialState, nil, initialDescriptors) + local dummyDescriptor = { key = "this is a dummy descriptor" } + scenes[1].descriptor = dummyDescriptor + local updatedScenes = ScenesReducer(scenes, initialState, initialState, initialDescriptors) + expect(#updatedScenes).to.be.equal(1) + local sceneOne = updatedScenes[1] + expect(sceneOne.descriptor).to.never.be.equal(dummyDescriptor) + expect(sceneOne.descriptor).to.be.equal(initialDescriptors[initialRouteKey]) + expect(sceneOne.route).to.be.equal(initialRoute) + expect(sceneOne.index).to.be.equal(1) + expect(sceneOne.isActive).to.be.equal(true) + expect(sceneOne.isStale).to.be.equal(false) + end) + + it("should bail out early", function() + expect(initialScenes).to.never.be.equal(nil) + local scenes = ScenesReducer(initialScenes, nil, nil, nil) + expect(scenes).to.be.equal(initialScenes) + scenes = ScenesReducer(initialScenes, initialState, initialState, initialDescriptors) + expect(scenes).to.be.equal(initialScenes) + end) + + local secondRouteKey = "id-2" + local secondRouteName = "Second Route" + local secondRoute = { + key = secondRouteKey, + routeName = secondRouteName, + } + + local secondState = { + index = 2, + key = "StackRouterRoot", + isTransitioning = true, + routes = { + initialRoute, + secondRoute, + }, + } + + local secondDescriptors = { + [initialRouteKey] = initialDescriptors[initialRouteKey], + [secondRouteKey] ={ + key = secondRouteKey, + navigation = { + state = secondRoute, + }, + state = secondRoute, + }, + } + + local secondScenes = nil + + it("should add second scene", function() + expect(initialScenes).to.never.be.equal(nil) + local scenes = ScenesReducer(initialScenes, secondState, initialState, secondDescriptors) + expect(scenes).to.never.be.equal(initialScenes) + expect(#scenes).to.be.equal(2) + + local sceneOne = scenes[1] + expect(sceneOne.route).to.be.equal(initialRoute) + expect(sceneOne.index).to.be.equal(1) + expect(sceneOne.isActive).to.be.equal(false) + expect(sceneOne.isStale).to.be.equal(false) + + local sceneTwo = scenes[2] + expect(sceneTwo.route).to.be.equal(secondRoute) + expect(sceneTwo.index).to.be.equal(2) + expect(sceneTwo.isActive).to.be.equal(true) + expect(sceneTwo.isStale).to.be.equal(false) + + secondScenes = scenes + end) + + local thirdRouteKey = "id-3" + local thirdRouteName = "Third Route" + local thirdRoute = { + key = thirdRouteKey, + routeName = thirdRouteName, + } + + local thirdState = { + index = 3, + key = "StackRouterRoot", + isTransitioning = true, + routes = { + initialRoute, + secondRoute, + thirdRoute, + }, + } + + local thirdDescriptors = { + [initialRouteKey] = initialDescriptors[initialRouteKey], + [secondRouteKey] = secondDescriptors[secondRouteKey], + [thirdRouteKey] = { + key = thirdRouteKey, + navigation = { + state = thirdRoute, + }, + state = thirdRoute, + }, + } + + local thirdScenes = nil + + it("should add third scene", function() + expect(initialScenes).to.never.be.equal(nil) + local scenes = ScenesReducer(secondScenes, thirdState, secondState, thirdDescriptors) + expect(scenes).to.never.be.equal(initialScenes) + expect(#scenes).to.be.equal(3) + + local sceneOne = scenes[1] + expect(sceneOne.route).to.be.equal(initialRoute) + expect(sceneOne.index).to.be.equal(1) + expect(sceneOne.isActive).to.be.equal(false) + expect(sceneOne.isStale).to.be.equal(false) + + local sceneTwo = scenes[2] + expect(sceneTwo.route).to.be.equal(secondRoute) + expect(sceneTwo.index).to.be.equal(2) + expect(sceneTwo.isActive).to.be.equal(false) + expect(sceneTwo.isStale).to.be.equal(false) + + local sceneThree = scenes[3] + expect(sceneThree.route).to.be.equal(thirdRoute) + expect(sceneThree.index).to.be.equal(3) + expect(sceneThree.isActive).to.be.equal(true) + expect(sceneThree.isStale).to.be.equal(false) + + thirdScenes = scenes + end) + + it("should mark removed scenes as stale", function() + expect(secondState).to.never.be.equal(nil) + local scenes = ScenesReducer(thirdScenes, initialState, thirdState, initialDescriptors) + expect(scenes).to.never.be.equal(initialScenes) + expect(#scenes).to.be.equal(3) + + local sceneOne = scenes[1] + expect(sceneOne.route).to.be.equal(initialRoute) + expect(sceneOne.index).to.be.equal(1) + expect(sceneOne.isActive).to.be.equal(true) + expect(sceneOne.isStale).to.be.equal(false) + + local sceneTwo = scenes[2] + expect(sceneTwo.route).to.be.equal(secondRoute) + expect(sceneTwo.index).to.be.equal(2) + expect(sceneTwo.isActive).to.be.equal(false) + expect(sceneTwo.isStale).to.be.equal(true) + + local sceneThree = scenes[3] + expect(sceneThree.route).to.be.equal(thirdRoute) + expect(sceneThree.index).to.be.equal(3) + expect(sceneThree.isActive).to.be.equal(false) + expect(sceneThree.isStale).to.be.equal(true) + end) + + local secondScreenReplacementKey = "id-22" + local secondScreenReplacementName = "Second Route Replacement" + local secondScreenReplacementRoute = { + key = secondScreenReplacementKey, + routeName = secondScreenReplacementName, + } + + local replacedSecondSceneState = { + index = 3, + key = "StackRouterRoot", + isTransitioning = true, + routes = { + initialRoute, + secondScreenReplacementRoute, + thirdRoute, + }, + } + + local replacedSceneDescriptors = { + [initialRouteKey] = initialDescriptors[initialRouteKey], + [secondRouteKey] = { + key = secondScreenReplacementKey, + navigation = { + state = secondScreenReplacementRoute + }, + state = secondScreenReplacementRoute, + }, + [thirdRouteKey] = thirdDescriptors[thirdRouteKey], + } + + it("should mark replaced scene as stale", function() + expect(secondState).to.never.be.equal(nil) + local scenes = ScenesReducer(thirdScenes, replacedSecondSceneState, thirdState, replacedSceneDescriptors) + expect(scenes).to.never.be.equal(initialScenes) + expect(#scenes).to.be.equal(4) -- replaced scene is marked stale, it is not removed + + local sceneOne = scenes[1] + expect(sceneOne.route).to.be.equal(initialRoute) + expect(sceneOne.index).to.be.equal(1) + expect(sceneOne.isActive).to.be.equal(false) + expect(sceneOne.isStale).to.be.equal(false) + + local sceneTwo = scenes[2] + expect(sceneTwo.route).to.be.equal(secondRoute) + expect(sceneTwo.index).to.be.equal(2) + expect(sceneTwo.isActive).to.be.equal(false) + expect(sceneTwo.isStale).to.be.equal(true) + + -- because of comparison algorithm in SceneReducer.lua compareScenes + -- the replacement scene is after the scene it replaced because id-2 < id-22 + local sceneTwoReplacement = scenes[3] + expect(sceneTwoReplacement.route).to.be.equal(secondScreenReplacementRoute) + -- index is still 2 because the scene index come from route index in the nextState + -- this is ok because filterStale in Transitioner.lua will remove the stale scene + expect(sceneTwoReplacement.index).to.be.equal(2) + expect(sceneTwoReplacement.isActive).to.be.equal(false) + expect(sceneTwoReplacement.isStale).to.be.equal(false) + + local sceneThree = scenes[4] + expect(sceneThree.route).to.be.equal(thirdRoute) + expect(sceneThree.index).to.be.equal(3) + expect(sceneThree.isActive).to.be.equal(true) + expect(sceneThree.isStale).to.be.equal(false) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/_tests_/StackPresentationStyle.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/_tests_/StackPresentationStyle.spec.lua new file mode 100644 index 0000000..0a64c09 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/_tests_/StackPresentationStyle.spec.lua @@ -0,0 +1,17 @@ +return function() + local StackPresentationStyle = require(script.Parent.Parent.StackPresentationStyle) + + describe("StackPresentationStyle token tests", function() + it("should return same object for each token for multiple calls", function() + expect(StackPresentationStyle.Default).to.equal(StackPresentationStyle.Default) + expect(StackPresentationStyle.Modal).to.equal(StackPresentationStyle.Modal) + expect(StackPresentationStyle.Overlay).to.equal(StackPresentationStyle.Overlay) + end) + + it("should return matching string names for symbols", function() + expect(tostring(StackPresentationStyle.Default)).to.equal("DEFAULT") + expect(tostring(StackPresentationStyle.Modal)).to.equal("MODAL") + expect(tostring(StackPresentationStyle.Overlay)).to.equal("OVERLAY") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/_tests_/StackViewCard.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/_tests_/StackViewCard.spec.lua new file mode 100644 index 0000000..1b3e68f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxStackView/_tests_/StackViewCard.spec.lua @@ -0,0 +1,36 @@ +return function() + local Otter = require(script.Parent.Parent.Parent.Parent.Parent.Otter) + local Roact = require(script.Parent.Parent.Parent.Parent.Parent.Roact) + local StackViewCard = require(script.Parent.Parent.StackViewCard) + + it("should mount its renderProp and pass it scene", function() + local didRender = false + local testScene = { + isActive = true, + index = 1, + } + + local renderedScene = nil + local element = Roact.createElement(StackViewCard, { + renderScene = function(theScene) + renderedScene = theScene + return Roact.createElement(function() + didRender = true -- verifies component is attached to tree + end) + end, + scene = testScene, + position = Otter.createSingleMotor(1), + navigation = { + state = { + index = 1, + } + } + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(renderedScene).to.equal(testScene) + expect(didRender).to.equal(true) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxSwitchView.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxSwitchView.lua new file mode 100644 index 0000000..fc5b0cc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/RobloxSwitchView.lua @@ -0,0 +1,68 @@ +local Cryo = require(script.Parent.Parent.Parent.Cryo) +local Roact = require(script.Parent.Parent.Parent.Roact) +local SceneView = require(script.Parent.SceneView) + +local defaultNavigationConfig = { + keepVisitedScreensMounted = false, +} + +local RobloxSwitchView = Roact.Component:extend("RobloxSwitchView") + +function RobloxSwitchView.getDerivedStateFromProps(nextProps, prevState) + local navState = nextProps.navigation.state + local activeKey = navState.routes[navState.index].key + local descriptors = nextProps.descriptors + + local navigationConfig = Cryo.Dictionary.join(defaultNavigationConfig, nextProps.navigationConfig or {}) + local keepVisitedScreensMounted = navigationConfig.keepVisitedScreensMounted + + local visitedScreenKeys = { + [activeKey] = true + } + + if keepVisitedScreensMounted then + -- prune visited screen keys if they are not included in incoming descriptors + for prevKey in pairs(prevState.visitedScreenKeys or {}) do + if descriptors[prevKey] ~= nil then + visitedScreenKeys[prevKey] = true + end + end + end + + return { + visitedScreenKeys = visitedScreenKeys, + } +end + +function RobloxSwitchView:render() + local navState = self.props.navigation.state + local screenProps = self.props.screenProps + local descriptors = self.props.descriptors + local visitedScreenKeys = self.state.visitedScreenKeys + local activeKey = navState.routes[navState.index].key + + local screenElements = {} + for key, descriptor in pairs(descriptors) do + local isActiveKey = (key == activeKey) + + if visitedScreenKeys[key] == true then + screenElements["card_" .. key] = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + BorderSizePixel = 0, + Visible = isActiveKey, + }, { + Content = Roact.createElement(SceneView, { + component = descriptor.getComponent(), + navigation = descriptor.navigation, + screenProps = screenProps, + }) + }) + end + end + + return Roact.createElement("Folder", nil, screenElements) +end + +return RobloxSwitchView diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/SceneView.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/SceneView.lua new file mode 100644 index 0000000..d4d1885 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/SceneView.lua @@ -0,0 +1,23 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/views/SceneView.js + +local Roact = require(script.Parent.Parent.Parent.Roact) +local NavigationContext = require(script.Parent.NavigationContext) + +local SceneView = Roact.PureComponent:extend("SceneView") + +function SceneView:render() + local screenProps = self.props.screenProps + local component = self.props.component + local navigation = self.props.navigation + + return Roact.createElement(NavigationContext.Provider, { + value = navigation, + }, { + Scene = Roact.createElement(component, { + screenProps = screenProps, + navigation = navigation, + }) + }) +end + +return SceneView diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/SwitchView/SwitchView.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/SwitchView/SwitchView.lua new file mode 100644 index 0000000..6e3dddb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/SwitchView/SwitchView.lua @@ -0,0 +1,21 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/1f5000e86bef5e4c8ee6fbeb25e3ca3eb8873ad0/packages/core/src/views/SwitchView/SwitchView.js + +local root = script.Parent.Parent.Parent +local Packages = root.Parent +local Roact = require(Packages.Roact) +local SceneView = require(script.Parent.Parent.SceneView) + +local function SwitchView(props) + local state = props.navigation.state + local activeKey = state.routes[state.index].key + local descriptor = props.descriptors[activeKey] + local ChildComponent = descriptor.getComponent() + + return Roact.createElement(SceneView, { + component = ChildComponent, + navigation = descriptor.navigation, + screenProps = props.screenProps, + }) +end + +return SwitchView diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/NavigationEvents.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/NavigationEvents.spec.lua new file mode 100644 index 0000000..60ebf01 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/NavigationEvents.spec.lua @@ -0,0 +1,240 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/20e2625f351f90fadadbf98890270e43e744225b/packages/core/src/views/__tests__/NavigationEvents.test.js + +return function() + local root = script.Parent.Parent.Parent + local Packages = root.Parent + local Cryo = require(Packages.Cryo) + local Roact = require(Packages.Roact) + local NavigationEvents = require(script.Parent.Parent.NavigationEvents) + local NavigationContext = require(root.views.NavigationContext) + local Events = require(root.Events) + local createSpy = require(root.utils.createSpy) + + local function createPropListener() + return createSpy() + end + + local EVENT_TO_PROP_NAME = { + [Events.WillFocus] = "onWillFocus", + [Events.DidFocus] = "onDidFocus", + [Events.WillBlur] = "onWillBlur", + [Events.DidBlur] = "onDidBlur", + }; + + local function createEventListenersProp() + return { + onWillFocus = createPropListener(), + onDidFocus = createPropListener(), + onWillBlur = createPropListener(), + onDidBlur = createPropListener(), + } + end + + local function createTestNavigationAndHelpers() + local NavigationListenersAPI = (function() + local listeners = { + [Events.WillFocus] = {}, + [Events.DidFocus] = {}, + [Events.WillBlur] = {}, + [Events.DidBlur] = {}, + } + + return { + add = function(eventName, handler) + table.insert(listeners[eventName], handler) + end, + remove = function(eventName, handler) + listeners[eventName] = Cryo.List.filter(listeners[eventName], function(current) + return current ~= handler + end) + end, + get = function(eventName) + return listeners[eventName] + end, + call = function(eventName) + for _, listener in ipairs(listeners[eventName]) do + listener() + end + end, + } + end)() + + local navigation = { + addListener = createSpy(function(eventName, handler) + NavigationListenersAPI.add(eventName, handler) + + return { + remove = function() + return NavigationListenersAPI.remove(eventName, handler) + end, + } + end), + } + + return { + navigation = navigation, + NavigationListenersAPI = NavigationListenersAPI, + } + end + + describe("NavigationEvents", function() + it( + "add all listeners on mount and remove them on unmount, even without any event prop provided (see #5058)", + function() + local helper = createTestNavigationAndHelpers() + local navigation = helper.navigation + local NavigationListenersAPI = helper.NavigationListenersAPI + + local tree = Roact.mount( + Roact.createElement(NavigationEvents, {navigation = navigation}) + ) + + expect(#NavigationListenersAPI.get(Events.WillFocus)).to.equal(1) + expect(#NavigationListenersAPI.get(Events.DidFocus)).to.equal(1) + expect(#NavigationListenersAPI.get(Events.WillBlur)).to.equal(1) + expect(#NavigationListenersAPI.get(Events.DidBlur)).to.equal(1) + + Roact.unmount(tree) + expect(#NavigationListenersAPI.get(Events.WillFocus)).to.equal(0) + expect(#NavigationListenersAPI.get(Events.DidFocus)).to.equal(0) + expect(#NavigationListenersAPI.get(Events.WillBlur)).to.equal(0) + expect(#NavigationListenersAPI.get(Events.DidBlur)).to.equal(0) + end + ) + + it("support context-provided navigation", function() + local helper = createTestNavigationAndHelpers() + local navigation = helper.navigation + local NavigationListenersAPI = helper.NavigationListenersAPI + + local tree = Roact.mount( + Roact.createElement(NavigationContext.Provider, { + value = navigation + }, Roact.createElement(NavigationEvents)) + ) + + expect(#NavigationListenersAPI.get(Events.WillFocus)).to.equal(1) + expect(#NavigationListenersAPI.get(Events.DidFocus)).to.equal(1) + expect(#NavigationListenersAPI.get(Events.WillBlur)).to.equal(1) + expect(#NavigationListenersAPI.get(Events.DidBlur)).to.equal(1) + + Roact.unmount(tree) + expect(#NavigationListenersAPI.get(Events.WillFocus)).to.equal(0) + expect(#NavigationListenersAPI.get(Events.DidFocus)).to.equal(0) + expect(#NavigationListenersAPI.get(Events.WillBlur)).to.equal(0) + expect(#NavigationListenersAPI.get(Events.DidBlur)).to.equal(0) + end) + + it("wire props listeners to navigation listeners", function() + local helper = createTestNavigationAndHelpers() + local navigation = helper.navigation + local NavigationListenersAPI = helper.NavigationListenersAPI + + local eventListenerProps = createEventListenersProp() + + Roact.mount( + Roact.createElement(NavigationEvents, Cryo.Dictionary.join( + {navigation = navigation}, + eventListenerProps + )) + ) + + local function checkPropListenerIsCalled(eventName, propName) + expect(eventListenerProps[propName].callCount).to.equal(0) + NavigationListenersAPI.call(eventName) + expect(eventListenerProps[propName].callCount).to.equal(1) + end + + checkPropListenerIsCalled(Events.WillFocus, "onWillFocus") + checkPropListenerIsCalled(Events.DidFocus, "onDidFocus") + checkPropListenerIsCalled(Events.WillBlur, "onWillBlur") + checkPropListenerIsCalled(Events.DidBlur, "onDidBlur") + end) + + it("wires props listeners to latest navigation updates", function() + local helper = createTestNavigationAndHelpers() + local navigation = helper.navigation + local NavigationListenersAPI = helper.NavigationListenersAPI + local nextHelper = createTestNavigationAndHelpers() + local nextNavigation = nextHelper.navigation + local nextNavigationListenersAPI = nextHelper.NavigationListenersAPI + + local eventListenerProps = createEventListenersProp() + local tree = Roact.mount( + Roact.createElement(NavigationEvents, Cryo.Dictionary.join( + {navigation = navigation}, + eventListenerProps + )) + ) + + for eventName, propName in pairs(EVENT_TO_PROP_NAME) do + expect(eventListenerProps[propName].callCount).to.equal(0) + NavigationListenersAPI.call(eventName) + expect(eventListenerProps[propName].callCount).to.equal(1) + end + + Roact.update( + tree, + Roact.createElement(NavigationEvents, Cryo.Dictionary.join( + {navigation = nextNavigation}, + eventListenerProps + )) + ) + + for eventName, propName in pairs(EVENT_TO_PROP_NAME) do + NavigationListenersAPI.call(eventName) + expect(eventListenerProps[propName].callCount).to.equal(1) + nextNavigationListenersAPI.call(eventName) + expect(eventListenerProps[propName].callCount).to.equal(2) + end + end) + + it( + "wire latest props listener to navigation listeners on updates (support closure/arrow functions update)", + function() + local helper = createTestNavigationAndHelpers() + local navigation = helper.navigation + local NavigationListenersAPI = helper.NavigationListenersAPI + + local tree = Roact.mount( + Roact.createElement(NavigationEvents, Cryo.Dictionary.join( + {navigation = navigation}, + createEventListenersProp() + )) + ) + + Roact.update(tree, Roact.createElement(NavigationEvents, { + navigation = navigation, + onWillBlur = function() + error("should not be called") + end, + onDidFocus = function() + error("should not be called") + end, + })) + Roact.update(tree, Roact.createElement(NavigationEvents, Cryo.Dictionary.join( + {navigation = navigation}, + createEventListenersProp() + ))) + + local latestEventListenerProps = createEventListenersProp() + + Roact.update(tree, Roact.createElement(NavigationEvents, Cryo.Dictionary.join( + {navigation = navigation}, + latestEventListenerProps + ))) + + local function checkLatestPropListenerCalled(eventName, propName) + expect(latestEventListenerProps[propName].callCount).to.equal(0) + NavigationListenersAPI.call(eventName) + expect(latestEventListenerProps[propName].callCount).to.equal(1) + end + + checkLatestPropListenerCalled(Events.WillFocus, "onWillFocus") + checkLatestPropListenerCalled(Events.DidFocus, "onDidFocus") + checkLatestPropListenerCalled(Events.WillBlur, "onWillBlur") + checkLatestPropListenerCalled(Events.DidBlur, "onDidBlur") + end + ) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/NavigationFocusEvents.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/NavigationFocusEvents.spec.lua new file mode 100644 index 0000000..9bd9195 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/NavigationFocusEvents.spec.lua @@ -0,0 +1,328 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/9b55493e7662f4d54c21f75e53eb3911675f61bc/packages/core/src/views/__tests__/NavigationFocusEvents.test.js + +return function() + local root = script.Parent.Parent.Parent + local Packages = root.Parent + local Cryo = require(Packages.Cryo) + local Roact = require(Packages.Roact) + local NavigationFocusEvents = require(script.Parent.Parent.NavigationFocusEvents) + local getEventManager = require(root.getEventManager) + local NavigationActions = require(root.NavigationActions) + local StackActions = require(root.routers.StackActions) + local Events = require(root.Events) + local createSpy = require(root.utils.createSpy) + local expectDeepEqual = require(root.utils.expectDeepEqual) + + local function getNavigationMock(mock) + local eventManager = getEventManager("target") + + local default = { + state = { + routes = { + { key = "a", routeName = "foo" }, + { key = "b", routeName = "bar" }, + }, + index = 1, + }, + isFocused = function() + return true + end, + addListener = createSpy(eventManager.addListener), + emit = eventManager.emit, + _dangerouslyGetParent = function() + return nil + end, + } + + if mock then + return Cryo.Dictionary.join(default, mock) + end + + return default + end + + it("emits refocus event with current route key on refocus", function() + local navigation = getNavigationMock() + local onEvent = createSpy() + + Roact.mount(Roact.createElement(NavigationFocusEvents, { + navigation = navigation, + onEvent = onEvent.value, + })) + + navigation.emit(Events.Refocus) + + expect(onEvent.callCount).to.equal(1) + local key = navigation.state.routes[navigation.state.index].key + onEvent:assertCalledWith(key, Events.Refocus) + end) + + describe("on navigation action emitted", function() + it("does not emit if navigation is not focused", function() + local navigation = getNavigationMock({ + isFocused = function() + return false + end, + }) + local onEvent = createSpy() + + Roact.mount(Roact.createElement(NavigationFocusEvents, { + navigation = navigation, + onEvent = onEvent.value, + })) + + navigation.emit(Events.Action, { + state = navigation.state, + action = NavigationActions.init(), + type = Events.Action, + }) + + expect(onEvent.callCount).to.equal(0) + end) + + it("emits only willFocus and willBlur if state is transitioning", function() + local state = { + routes = { + { key = "First", routeName = "First" }, + { key = "Second", routeName = "Second" }, + }, + index = 2, + routeKeyHistory = { "First", "Second" }, + isTransitioning = true, + } + local action = NavigationActions.init() + + local navigation = getNavigationMock({ + state = state, + }) + local onEvent = createSpy() + + Roact.mount(Roact.createElement(NavigationFocusEvents, { + navigation = navigation, + onEvent = onEvent.value, + })) + + local lastState = { + routes = { + { key = "First", routeName = "First" }, + { key = "Second", routeName = "Second" }, + }, + index = 1, + routeKeyHistory = { "First" }, + } + navigation.emit(Events.Action, { + state = state, + lastState = lastState, + action = action, + type = Events.Action, + }) + + local expectedPayload = { + action = action, + state = { key = "Second", routeName = "Second" }, + lastState = { key = "First", routeName = "First" }, + context = "Second:INIT_Root", + type = Events.Action, + } + + expectDeepEqual(onEvent.calls, { + {"Second", Events.WillFocus, expectedPayload}, + {"First", Events.WillBlur, expectedPayload}, + {"Second", Events.Action, expectedPayload}, + }) + end) + + it("emits didFocus after willFocus and didBlur after willBlur if no transitions", function() + local state = { + routes = { + { key = "First", routeName = "First" }, + { key = "Second", routeName = "Second" }, + }, + index = 2, + routeKeyHistory = { "First", "Second" }, + } + local action = NavigationActions.navigate({ routeName = "Second" }) + + local navigation = getNavigationMock({ state = state }) + local onEvent = createSpy() + + Roact.mount(Roact.createElement(NavigationFocusEvents, { + navigation = navigation, + onEvent = onEvent.value, + })) + + local lastState = { + routes = { + { key = "First", routeName = "First" }, + { key = "Second", routeName = "Second" }, + }, + index = 1, + routeKeyHistory = { "First" }, + } + navigation.emit(Events.Action, { + state = state, + lastState = lastState, + action = action, + type = Events.Action, + }) + + local expectedPayload = { + action = action, + state = { key = "Second", routeName = "Second" }, + lastState = { key = "First", routeName = "First" }, + context = "Second:NAVIGATE_Root", + type = Events.Action, + } + + expectDeepEqual(onEvent.calls, { + {"Second", Events.WillFocus, expectedPayload}, + {"Second", Events.DidFocus, expectedPayload}, + {"First", Events.WillBlur, expectedPayload}, + {"First", Events.DidBlur, expectedPayload}, + {"Second", Events.Action, expectedPayload}, + }) + end) + + it("emits didBlur and didFocus when transition ends", function() + local initialState = { + routes = { + { key = "First", routeName = "First" }, + { key = "Second", routeName = "Second" }, + }, + index = 1, + routeKeyHistory = { "First" }, + isTransitioning = true, + } + local intermediateState = { + routes = { + { key = "First", routeName = "First" }, + { key = "Second", routeName = "Second" }, + }, + index = 2, + routeKeyHistory = { "First", "Second" }, + isTransitioning = true, + } + local finalState = { + routes = { + { key = "First", routeName = "First" }, + { key = "Second", routeName = "Second" }, + }, + index = 2, + routeKeyHistory = { "First", "Second" }, + isTransitioning = false, + } + local actionNavigate = NavigationActions.navigate({ routeName = "Second" }) + local actionEndTransition = StackActions.completeTransition({ key = "Second" }) + + local navigation = getNavigationMock({ + state = intermediateState, + }) + local onEvent = createSpy() + + Roact.mount(Roact.createElement(NavigationFocusEvents, { + navigation = navigation, + onEvent = onEvent.value, + })) + + navigation.emit(Events.Action, { + state = intermediateState, + lastState = initialState, + action = actionNavigate, + type = Events.Action, + }) + + local expectedPayloadNavigate = { + action = actionNavigate, + state = { key = "Second", routeName = "Second" }, + lastState = { key = "First", routeName = "First" }, + context = "Second:NAVIGATE_Root", + type = Events.Action, + } + + expectDeepEqual(onEvent.calls, { + {"Second", Events.WillFocus, expectedPayloadNavigate}, + {"First", Events.WillBlur, expectedPayloadNavigate}, + {"Second", Events.Action, expectedPayloadNavigate}, + }) + onEvent:mockClear() + + navigation.emit(Events.Action, { + state = finalState, + lastState = intermediateState, + action = actionEndTransition, + type = Events.Action, + }) + + local expectedPayloadEndTransition = { + action = actionEndTransition, + state = { key = "Second", routeName = "Second" }, + lastState = { key = "Second", routeName = "Second" }, + context = "Second:COMPLETE_TRANSITION_Root", + type = Events.Action, + } + + expectDeepEqual(onEvent.calls, { + {"First", Events.DidBlur, expectedPayloadEndTransition}, + {"Second", Events.DidFocus, expectedPayloadEndTransition}, + {"Second", Events.Action, expectedPayloadEndTransition}, + }) + end) + end) + + describe("on willFocus emitted", function() + it("emits didFocus after willFocus if no transition", function() + local navigation = getNavigationMock({ + state = { + routes = { + { key = "FirstLanding", routeName = "FirstLanding" }, + { key = "Second", routeName = "Second" }, + }, + index = 1, + key = "First", + routeName = "First", + }, + }) + local onEvent = createSpy() + + Roact.mount(Roact.createElement(NavigationFocusEvents, { + navigation = navigation, + onEvent = onEvent.value, + })) + + local lastState = { key = "Third", routeName = "Third" } + local action = NavigationActions.navigate({ routeName = "First" }) + + navigation.emit(Events.WillFocus, { + lastState = lastState, + action = action, + context = "First:NAVIGATE_Root", + type = Events.Action, + }) + + local expectedPayload = { + action = action, + state = { key = "FirstLanding", routeName = "FirstLanding" }, + context = + "FirstLanding:NAVIGATE_First:NAVIGATE_Root", + type = Events.Action, + } + + expectDeepEqual(onEvent.calls, { + {"FirstLanding", Events.WillFocus, expectedPayload}, + {"FirstLanding", Events.DidFocus, expectedPayload}, + }) + + onEvent:mockClear() + + -- the nested navigator might emit a didFocus that should be ignored + navigation.emit(Events.DidFocus, { + lastState = lastState, + action = action, + context = "First:NAVIGATE_Root", + type = Events.Action, + }) + + expect(onEvent.callCount).to.equal(0) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/RobloxSwitchView.roblox.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/RobloxSwitchView.roblox.spec.lua new file mode 100644 index 0000000..d4fc316 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/RobloxSwitchView.roblox.spec.lua @@ -0,0 +1,268 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local RobloxSwitchView = require(script.Parent.Parent.RobloxSwitchView) + + it("should mount and pass required props and context", function() + local testScreenProps = {} + local testNavigation = { + state = { + routes = { + { routeName = "Foo", key = "Foo" } + }, + index = 1, + }, + } + + local testComponentNavigationFromProp = nil + local testComponentScreenProps = nil + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + testComponentNavigationFromProp = self.props.navigation + testComponentScreenProps = self.props.screenProps + return nil + end + + local testDescriptors = { + Foo = { + getComponent = function() + return TestComponent + end, + navigation = testNavigation, + } + } + + local element = Roact.createElement(RobloxSwitchView, { + screenProps = testScreenProps, + navigation = testNavigation, + descriptors = testDescriptors, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(testComponentNavigationFromProp).to.equal(testNavigation) + expect(testComponentScreenProps).to.equal(testScreenProps) + end) + + it("should unmount inactive pages when keepVisitedScreensMounted is false", function() + local fooUnmounted = false + local TestComponentFoo = Roact.Component:extend("TestComponentFoo") + function TestComponentFoo:render() end + function TestComponentFoo:willUnmount() + fooUnmounted = true + end + + local TestComponentBar = Roact.Component:extend("TestComponentBar") + function TestComponentBar:render() end + + local function makeDescriptors(navProp) + return { + Foo = { + getComponent = function() return TestComponentFoo end, + navigation = navProp, + }, + Bar = { + getComponent = function() return TestComponentBar end, + navigation = navProp, + }, + } + end + + local testNavigation1 = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + { routeName = "Bar", key = "Bar" }, + }, + index = 1, + }, + } + + local element = Roact.createElement(RobloxSwitchView, { + screenProps = {}, + navigation = testNavigation1, + descriptors = makeDescriptors(testNavigation1), + navigationConfig = { + keepVisitedScreensMounted = false, + }, + }) + + local instance = Roact.mount(element) + expect(fooUnmounted).to.equal(false) + + local testNavigation2 = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + { routeName = "Bar", key = "Bar" }, + }, + index = 2, + }, + } + + instance = Roact.update(instance, Roact.createElement(RobloxSwitchView, { + screenProps = {}, + navigation = testNavigation2, + descriptors = makeDescriptors(testNavigation2), + navigationConfig = { + keepVisitedScreensMounted = false, + } + })) + + expect(fooUnmounted).to.equal(true) + Roact.unmount(instance) + end) + + it("should not unmount inactive pages when keepVisitedScreensMounted is true", function() + local fooUnmounted = false + local TestComponentFoo = Roact.Component:extend("TestComponentFoo") + function TestComponentFoo:render() end + function TestComponentFoo:willUnmount() + fooUnmounted = true + end + + local TestComponentBar = Roact.Component:extend("TestComponentBar") + function TestComponentBar:render() end + + local function makeDescriptors(navProp) + return { + Foo = { + getComponent = function() return TestComponentFoo end, + navigation = navProp, + }, + Bar = { + getComponent = function() return TestComponentBar end, + navigation = navProp, + }, + } + end + + local testNavigation1 = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + { routeName = "Bar", key = "Bar" }, + }, + index = 1, + }, + } + + local element = Roact.createElement(RobloxSwitchView, { + screenProps = {}, + navigation = testNavigation1, + descriptors = makeDescriptors(testNavigation1), + navigationConfig = { + keepVisitedScreensMounted = true, + }, + }) + + local instance = Roact.mount(element) + expect(fooUnmounted).to.equal(false) + + local testNavigation2 = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + { routeName = "Bar", key = "Bar" }, + }, + index = 2, + }, + } + + instance = Roact.update(instance, Roact.createElement(RobloxSwitchView, { + screenProps = {}, + navigation = testNavigation2, + descriptors = makeDescriptors(testNavigation2), + navigationConfig = { + keepVisitedScreensMounted = true, + } + })) + + expect(fooUnmounted).to.equal(false) + + Roact.unmount(instance) + expect(fooUnmounted).to.equal(true) + end) + + it("should unmount inactive pages when keepVisitedScreensMounted switches from true to false", function() + local fooUnmounted = false + local TestComponentFoo = Roact.Component:extend("TestComponentFoo") + function TestComponentFoo:render() end + function TestComponentFoo:willUnmount() + fooUnmounted = true + end + + local TestComponentBar = Roact.Component:extend("TestComponentBar") + function TestComponentBar:render() end + + local function makeDescriptors(navProp) + return { + Foo = { + getComponent = function() return TestComponentFoo end, + navigation = navProp, + }, + Bar = { + getComponent = function() return TestComponentBar end, + navigation = navProp, + }, + } + end + + local testNavigation1 = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + { routeName = "Bar", key = "Bar" }, + }, + index = 1, + }, + } + + local element = Roact.createElement(RobloxSwitchView, { + screenProps = {}, + navigation = testNavigation1, + descriptors = makeDescriptors(testNavigation1), + navigationConfig = { + keepVisitedScreensMounted = true, + }, + }) + + local instance = Roact.mount(element) + + local testNavigation2 = { + state = { + routes = { + { routeName = "Foo", key = "Foo" }, + { routeName = "Bar", key = "Bar" }, + }, + index = 2, + }, + } + + -- We must update tree to make sure active screens list gets updated first! + instance = Roact.update(instance, Roact.createElement(RobloxSwitchView, { + screenProps = {}, + navigation = testNavigation2, + descriptors = makeDescriptors(testNavigation2), + navigationConfig = { + keepVisitedScreensMounted = true, + } + })) + + expect(fooUnmounted).to.equal(false) + + instance = Roact.update(instance, Roact.createElement(RobloxSwitchView, { + screenProps = {}, + navigation = testNavigation2, + descriptors = makeDescriptors(testNavigation2), + navigationConfig = { + keepVisitedScreensMounted = false, + } + })) + + expect(fooUnmounted).to.equal(true) + + Roact.unmount(instance) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/SceneView.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/SceneView.spec.lua new file mode 100644 index 0000000..3c51cc4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/SceneView.spec.lua @@ -0,0 +1,30 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local SceneView = require(script.Parent.Parent.SceneView) + + it("should mount inner component and pass down required props+context.navigation", function() + local testComponentNavigationFromProp = nil + local testComponentScreenProps = nil + + local TestComponent = Roact.Component:extend("TestComponent") + function TestComponent:render() + testComponentNavigationFromProp = self.props.navigation + testComponentScreenProps = self.props.screenProps + return nil + end + + local testScreenProps = {} + local testNav = {} + local element = Roact.createElement(SceneView, { + screenProps = testScreenProps, + navigation = testNav, + component = TestComponent, + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + + expect(testComponentScreenProps).to.equal(testScreenProps) + expect(testComponentNavigationFromProp).to.equal(testNav) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/withNavigation.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/withNavigation.spec.lua new file mode 100644 index 0000000..59af2e3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/withNavigation.spec.lua @@ -0,0 +1,85 @@ +return function() + local Roact = require(script.Parent.Parent.Parent.Parent.Roact) + local withNavigation = require(script.Parent.Parent.withNavigation) + local NavigationContext = require(script.Parent.Parent.NavigationContext) + + it("throws if no component is provided", function() + expect(function() + withNavigation(nil) + end).to.throw("withNavigation must be called with a Roact component (stateful or functional)") + end) + + it("should extract navigation object from provider and pass it through", function() + local testNavigation = {} + local extractedNavigation = nil + + local function Foo(props) + extractedNavigation = props.navigation + return nil + end + + local FooWithNavigation = withNavigation(Foo) + + local rootElement = Roact.createElement(NavigationContext.Provider, { + value = testNavigation, + }, { + Child = Roact.createElement(FooWithNavigation) + }) + + local tree = Roact.mount(rootElement) + Roact.unmount(tree) + + expect(extractedNavigation).to.equal(testNavigation) + end) + + it("should update with new navigation when navigation is updated", function() + local testNavigation = {} + local testNavigation2 = {} + local extractedNavigation = nil + + local function Foo(props) + extractedNavigation = props.navigation + return nil + end + + local FooWithNavigation = withNavigation(Foo) + + local rootElement = Roact.createElement(NavigationContext.Provider, { + value = testNavigation, + }, { + Child = Roact.createElement(FooWithNavigation) + }) + + local tree = Roact.mount(rootElement) + + local rootElement2 = Roact.createElement(NavigationContext.Provider, { + value = testNavigation2, + }, { + Child = Roact.createElement(FooWithNavigation) + }) + + Roact.update(tree, rootElement2) + + Roact.unmount(tree) + + expect(extractedNavigation).to.equal(testNavigation2) + end) + + it("should throw when used outside of a navigation provider", function() + local function Foo(props) + return nil + end + + local FooWithNavigation = withNavigation(Foo) + + local element = Roact.createElement(FooWithNavigation) + + local errorMessage = "withNavigation and withNavigationFocus can only " .. + "be used on a view hierarchy of a navigator. The wrapped component is " .. + "unable to get access to navigation from props or context" + + expect(function() + Roact.mount(element) + end).to.throw(errorMessage) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/withNavigationFocus.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/withNavigationFocus.spec.lua new file mode 100644 index 0000000..7b4961b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/_tests_/withNavigationFocus.spec.lua @@ -0,0 +1,143 @@ +return function() + local root = script.Parent.Parent.Parent + local Packages = root.Parent + local Roact = require(Packages.Roact) + local NavigationContext = require(script.Parent.Parent.NavigationContext) + local Events = require(root.Events) + local withNavigationFocus = require(script.Parent.Parent.withNavigationFocus) + + it("should pass focused=true when initially focused", function() + local testFocused = nil + + local function Foo(props) + testFocused = props.isFocused + return nil + end + + local FooWithNavigationFocus = withNavigationFocus(Foo) + + local navigationProp = { + isFocused = function() + return true + end, + addListener = function() + return { + remove = function() end + } + end + } + + local rootElement = Roact.createElement(NavigationContext.Provider, { + value = navigationProp, + }, { + child = Roact.createElement(FooWithNavigationFocus), + }) + + local tree = Roact.mount(rootElement) + expect(testFocused).to.equal(true) + + Roact.unmount(tree) + end) + + it("should pass focused=false when initially unfocused", function() + local testFocused = nil + + local function Foo(props) + testFocused = props.isFocused + return nil + end + + local FooWithNavigationFocus = withNavigationFocus(Foo) + + local navigationProp = { + isFocused = function() + return false + end, + addListener = function() + return { + remove = function() end + } + end + } + + local rootElement = Roact.createElement(NavigationContext.Provider, { + value = navigationProp, + }, { + child = Roact.createElement(FooWithNavigationFocus) + }) + + local tree = Roact.mount(rootElement) + expect(testFocused).to.equal(false) + + Roact.unmount(tree) + end) + + it("should re-render and set focused status for events", function() + local testListeners = {} + local testFocused = false + + local function Foo(props) + testFocused = props.isFocused + return Roact.createElement("TextButton") + end + + local FooWithNavigationFocus = withNavigationFocus(Foo) + + local navigationProp = { + isFocused = function() + return false + end, + addListener = function(event, listener) + testListeners[event] = listener + return { + remove = function() + testListeners[event] = nil + end + } + end + } + + local rootElement = Roact.createElement(NavigationContext.Provider, { + value = navigationProp, + }, { + child = Roact.createElement(FooWithNavigationFocus), + }) + + local tree = Roact.mount(rootElement) + expect(testFocused).to.equal(false) + expect(type(testListeners[Events.WillFocus])).to.equal("function") + expect(type(testListeners[Events.WillBlur])).to.equal("function") + + testListeners[Events.WillFocus]() + expect(testFocused).to.equal(true) + + testListeners[Events.WillBlur]() + expect(testFocused).to.equal(false) + + Roact.unmount(tree) + expect(testListeners[Events.WillFocus]).to.equal(nil) + expect(testListeners[Events.WillBlur]).to.equal(nil) + end) + + it("throws if component is not provided", function() + expect(function() + withNavigationFocus(nil) + end).to.throw("withNavigationFocus must be called with a Roact component (stateful or functional)") + end) + + it("should throw when used outside of a navigation provider", function() + local function Foo() + return nil + end + + local FooWithNavigationFocus = withNavigationFocus(Foo) + + local errorMessage = "withNavigation and withNavigationFocus can only " .. + "be used on a view hierarchy of a navigator. The wrapped component is " .. + "unable to get access to navigation from props or context" + + expect(function() + Roact.mount(Roact.createElement(FooWithNavigationFocus)) + end).to.throw(errorMessage) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/withNavigation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/withNavigation.lua new file mode 100644 index 0000000..8b81f6d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/withNavigation.lua @@ -0,0 +1,56 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/20e2625f351f90fadadbf98890270e43e744225b/packages/core/src/views/withNavigation.js +local Packages = script.Parent.Parent.Parent +local Cryo = require(Packages.Cryo) +local Roact = require(Packages.Roact) +local NavigationContext = require(script.Parent.NavigationContext) +local validate = require(script.Parent.Parent.utils.validate) + +local function isComponent(component) + local valueType = typeof(component) + return valueType == "function" or valueType == "table" +end + +--[[ + withNavigation() is a convenience function that you can use in your component's + render function to access the navigation context object. For example: + + local MyComponent = Roact.Component:extend("MyComponent") + + function MyComponent:render() + local navigation = self.props.navigation + return Roact.createElement("TextButton", { + [Roact.Activated] = function() + navigation.navigate("DetailPage") + end + }) + end + + return withNavigation(MyComponent) +]] +return function(component, config) + assert( + isComponent(component), + "withNavigation must be called with a Roact component (stateful or functional)" + ) + config = config or {} + + if config.forwardRef == nil then + config.forwardRef = true + end + + return function(props) + local navigationProp = props.navigation + return Roact.createElement(NavigationContext.Consumer, { + render = function(navigationContext) + local navigation = navigationProp or navigationContext + validate(navigation, "withNavigation and withNavigationFocus can only " .. + "be used on a view hierarchy of a navigator. The wrapped component is " .. + "unable to get access to navigation from props or context.") + return Roact.createElement(component, Cryo.Dictionary.join(props, { + navigation = navigation, + [Roact.Ref] = config.forwardRef and props[Roact.Ref] or nil, + })) + end, + }) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/withNavigationFocus.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/withNavigationFocus.lua new file mode 100644 index 0000000..ac245f2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-navigation/roact-navigation/views/withNavigationFocus.lua @@ -0,0 +1,83 @@ +-- upstream https://github.com/react-navigation/react-navigation/blob/20e2625f351f90fadadbf98890270e43e744225b/packages/core/src/views/withNavigationFocus.js +--[[ + withNavigationFocus() is a convenience function that extends withNavigation(), + allowing your render function (and therefor your subgraph) to access the + navigation context object AND an additional boolean that indicates whether or + not the containing screen component is in focus. For example: + + function MyButtonComponent:render() + return withNavigationFocus(function(navigation, focused) + return Roact.createElement("TextButton", { + Visible = focused, + [Roact.Event.Activated] = focused and function() + navigation.navigate("DetailPage") + end, + }) + end) + end + + This is very useful when writing generic components that need to work with + the navigation system (e.g. preventing buttons from navigating when a screen + is not in focus so you don't cause double-navigation). + + Note that if you ONLY need the 'navigation' context object, it is recommended + that you use withNavigation() for performance reasons. +]] +local root = script.Parent.Parent +local Packages = root.Parent +local Roact = require(Packages.Roact) +local Cryo = require(Packages.Cryo) +local Events = require(root.Events) +local withNavigation = require(script.Parent.withNavigation) + +local function isComponent(component) + local valueType = typeof(component) + return valueType == "function" or valueType == "table" +end + +return function(component) + assert( + isComponent(component), + "withNavigationFocus must be called with a Roact component (stateful or functional)" + ) + local NavigationFocusComponent = Roact.Component:extend("NavigationFocusComponent") + + function NavigationFocusComponent:init() + self.state = { + isFocused = self.props.navigation.isFocused(), + } + end + + function NavigationFocusComponent:didMount() + local navigation = self.props.navigation + + self.subscriptions = { + navigation.addListener(Events.WillFocus, function() + -- no spawn because we expect this to be called directly from safe paths + self:setState({ + isFocused = true, + }) + end), + navigation.addListener(Events.WillBlur, function() + -- no spawn because we expect this to be called directly from safe paths + self:setState({ + isFocused = false, + }) + end), + } + end + + function NavigationFocusComponent:willUnmount() + for _, subscription in ipairs(self.subscriptions) do + subscription.remove() + end + end + + function NavigationFocusComponent:render() + return Roact.createElement(component, Cryo.Dictionary.join(self.props, { + isFocused = self.state.isFocused, + })) + end + + return withNavigation(NavigationFocusComponent, { forwardRef = false }) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/Roact.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/Roact.lua new file mode 100644 index 0000000..08b72c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/Roact.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_roact"]["roact"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/Rodux.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/Rodux.lua new file mode 100644 index 0000000..96b67df --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/Rodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_rodux"]["rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/lock.toml new file mode 100644 index 0000000..90456f7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/lock.toml @@ -0,0 +1,9 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/roact-rodux" +version = "0.2.2" +commit = "78e2f5ac8c4679ef7216fbb9eedbcae31122545a" +source = "url+https://github.com/roblox/roact-rodux" +dependencies = [ + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/StoreProvider.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/StoreProvider.lua new file mode 100644 index 0000000..a226935 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/StoreProvider.lua @@ -0,0 +1,21 @@ +local Roact = require(script.Parent.Parent.Roact) + +local storeKey = require(script.Parent.storeKey) + +local StoreProvider = Roact.Component:extend("StoreProvider") + +function StoreProvider:init(props) + local store = props.store + + if store == nil then + error("Error initializing StoreProvider. Expected a `store` prop to be a Rodux store.") + end + + self._context[storeKey] = store +end + +function StoreProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +return StoreProvider \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/StoreProvider.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/StoreProvider.spec.lua new file mode 100644 index 0000000..e7236f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/StoreProvider.spec.lua @@ -0,0 +1,30 @@ +return function() + local StoreProvider = require(script.Parent.StoreProvider) + + local Roact = require(script.Parent.Parent.Roact) + local Rodux = require(script.Parent.Parent.Rodux) + + it("should be instantiable as a component", function() + local store = Rodux.Store.new(function() + return 0 + end) + local element = Roact.createElement(StoreProvider, { + store = store + }) + + expect(element).to.be.ok() + + local handle = Roact.mount(element, nil, "StoreProvider-test") + + Roact.unmount(handle) + store:destruct() + end) + + it("should expect a 'store' prop", function() + local element = Roact.createElement(StoreProvider) + + expect(function() + Roact.mount(element) + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/Symbol.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/Symbol.lua new file mode 100644 index 0000000..8b6adaf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/Symbol.lua @@ -0,0 +1,43 @@ +--[[ + A 'Symbol' is an opaque marker type that can be used to signify unique + statuses. Symbols have the type 'userdata', but when printed to the console, + the name of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +--[[ + Create an unnamed Symbol. Usually, you should create a named Symbol using + Symbol.named(name) +]] +function Symbol.unnamed() + local self = newproxy(true) + + getmetatable(self).__tostring = function() + return "Unnamed Symbol" + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/Symbol.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/Symbol.spec.lua new file mode 100644 index 0000000..cde9be0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/Symbol.spec.lua @@ -0,0 +1,45 @@ +return function() + local Symbol = require(script.Parent.Symbol) + + describe("named", function() + it("should give an opaque object", function() + local symbol = Symbol.named("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = Symbol.named("foo") + + expect(tostring(symbol):find("foo")).to.be.ok() + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.named("abc") + local symbolB = Symbol.named("abc") + + expect(symbolA).never.to.equal(symbolB) + end) + end) + + describe("unnamed", function() + it("should give an opaque object", function() + local symbol = Symbol.unnamed() + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to some string", function() + local symbol = Symbol.unnamed() + + expect(tostring(symbol)).to.be.a("string") + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.unnamed() + local symbolB = Symbol.unnamed() + + expect(symbolA).never.to.equal(symbolB) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/TempConfig.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/TempConfig.lua new file mode 100644 index 0000000..a752d7d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/TempConfig.lua @@ -0,0 +1,3 @@ +return { + newConnectionOrder = true, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/connect.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/connect.lua new file mode 100644 index 0000000..7e18ca1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/connect.lua @@ -0,0 +1,201 @@ +local Roact = require(script.Parent.Parent.Roact) +local getStore = require(script.Parent.getStore) +local shallowEqual = require(script.Parent.shallowEqual) +local join = require(script.Parent.join) + +local TempConfig = require(script.Parent.TempConfig) + +--[[ + Formats a multi-line message with printf-style placeholders. +]] +local function formatMessage(lines, parameters) + return table.concat(lines, "\n"):format(unpack(parameters or {})) +end + +local function noop() + return nil +end + +--[[ + The stateUpdater accepts props when they update and computes the + complete set of props that should be passed to the wrapped component. + + Each connected component will have a stateUpdater created for it. + + stateUpdater is put into the component's state in order for + getDerivedStateFromProps to be able to access it. It is not mutated. +]] +local function makeStateUpdater(store) + return function(nextProps, prevState, mappedStoreState) + -- The caller can optionally provide mappedStoreState if it needed that + -- value beforehand. Doing so is purely an optimization. + if mappedStoreState == nil then + mappedStoreState = prevState.mapStateToProps(store:getState(), nextProps) + end + + local propsForChild = join(nextProps, mappedStoreState, prevState.mappedStoreDispatch) + + return { + mappedStoreState = mappedStoreState, + propsForChild = propsForChild, + } + end +end + +--[[ + mapStateToProps: + (storeState, props) -> partialProps + OR + () -> (storeState, props) -> partialProps + mapDispatchToProps: (dispatch) -> partialProps +]] +local function connect(mapStateToPropsOrThunk, mapDispatchToProps) + local connectTrace = debug.traceback() + + if mapStateToPropsOrThunk ~= nil then + assert(typeof(mapStateToPropsOrThunk) == "function", "mapStateToProps must be a function or nil!") + else + mapStateToPropsOrThunk = noop + end + + if mapDispatchToProps ~= nil then + assert(typeof(mapDispatchToProps) == "function", "mapDispatchToProps must be a function or nil!") + else + mapDispatchToProps = noop + end + + return function(innerComponent) + if innerComponent == nil then + local message = formatMessage({ + "connect returns a function that must be passed a component.", + "Check the connection at:", + "%s", + }, { + connectTrace, + }) + + error(message, 2) + end + + local componentName = ("RoduxConnection(%s)"):format(tostring(innerComponent)) + + local Connection = Roact.Component:extend(componentName) + + function Connection.getDerivedStateFromProps(nextProps, prevState) + if prevState.stateUpdater ~= nil then + return prevState.stateUpdater(nextProps, prevState) + end + end + + function Connection:createStoreConnection() + self.storeChangedConnection = self.store.changed:connect(function(storeState) + self:setState(function(prevState, props) + local mappedStoreState = prevState.mapStateToProps(storeState, props) + + -- We run this check here so that we only check shallow + -- equality with the result of mapStateToProps, and not the + -- other props that could be passed through the connector. + if shallowEqual(mappedStoreState, prevState.mappedStoreState) then + return nil + end + + return prevState.stateUpdater(props, prevState, mappedStoreState) + end) + end) + end + + function Connection:init() + self.store = getStore(self) + + if self.store == nil then + local message = formatMessage({ + "Cannot initialize Roact-Rodux connection without being a descendent of StoreProvider!", + "Tried to wrap component %q", + "Make sure there is a StoreProvider above this component in the tree.", + }, { + tostring(innerComponent), + }) + + error(message) + end + + local storeState = self.store:getState() + + local mapStateToProps = mapStateToPropsOrThunk + local mappedStoreState = mapStateToProps(storeState, self.props) + + -- mapStateToPropsOrThunk can return a function instead of a state + -- value. In this variant, we keep that value as mapStateToProps + -- instead of the original mapStateToProps. This matches react-redux + -- and enables connectors to keep instance-level state. + if typeof(mappedStoreState) == "function" then + mapStateToProps = mappedStoreState + mappedStoreState = mapStateToProps(storeState, self.props) + end + + if mappedStoreState ~= nil and typeof(mappedStoreState) ~= "table" then + local message = formatMessage({ + "mapStateToProps must either return a table, or return another function that returns a table.", + "Instead, it returned %q, which is of type %s.", + }, { + tostring(mappedStoreState), + typeof(mappedStoreState), + }) + + error(message) + end + + local mappedStoreDispatch = mapDispatchToProps(function(...) + return self.store:dispatch(...) + end) + + local stateUpdater = makeStateUpdater(self.store) + + self.state = { + -- Combines props, mappedStoreDispatch, and the result of + -- mapStateToProps into propsForChild. Stored in state so that + -- getDerivedStateFromProps can access it. + stateUpdater = stateUpdater, + + -- Used by the store changed connection and stateUpdater to + -- construct propsForChild. + mapStateToProps = mapStateToProps, + + -- Used by stateUpdater to construct propsForChild. + mappedStoreDispatch = mappedStoreDispatch, + + -- Passed directly into the component that Connection is + -- wrapping. + propsForChild = nil, + } + + local extraState = stateUpdater(self.props, self.state, mappedStoreState) + + for key, value in pairs(extraState) do + self.state[key] = value + end + + if TempConfig.newConnectionOrder then + self:createStoreConnection() + end + end + + function Connection:didMount() + if not TempConfig.newConnectionOrder then + self:createStoreConnection() + end + end + + function Connection:willUnmount() + self.storeChangedConnection:disconnect() + end + + function Connection:render() + return Roact.createElement(innerComponent, self.state.propsForChild) + end + + return Connection + end +end + +return connect \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/connect.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/connect.spec.lua new file mode 100644 index 0000000..453e3d5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/connect.spec.lua @@ -0,0 +1,353 @@ +return function() + local connect = require(script.Parent.connect) + + local StoreProvider = require(script.Parent.StoreProvider) + + local Roact = require(script.Parent.Parent.Roact) + local Rodux = require(script.Parent.Parent.Rodux) + + local TempConfig = require(script.Parent.TempConfig) + + local function noop() + return nil + end + + local function NoopComponent() + return nil + end + + local function countReducer(state, action) + state = state or 0 + + if action.type == "increment" then + return state + 1 + end + + return state + end + + local reducer = Rodux.combineReducers({ + count = countReducer, + }) + + describe("Argument validation", function() + it("should accept no arguments", function() + connect() + end) + + it("should accept one function", function() + connect(noop) + end) + + it("should accept two functions", function() + connect(noop, noop) + end) + + it("should accept only the second function", function() + connect(nil, function() end) + end) + + it("should throw if not passed a component", function() + local selector = function(store) + return {} + end + + expect(function() + connect(selector)(nil) + end).to.throw() + end) + end) + + it("should throw if not mounted under a StoreProvider", function() + local ConnectedSomeComponent = connect()(NoopComponent) + + expect(function() + Roact.mount(Roact.createElement(ConnectedSomeComponent)) + end).to.throw() + end) + + it("should accept a higher-order function mapStateToProps", function() + local function mapStateToProps() + return function(state) + return { + count = state.count, + } + end + end + + local ConnectedSomeComponent = connect(mapStateToProps)(NoopComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent), + }) + + local handle = Roact.mount(tree) + + Roact.unmount(handle) + end) + + it("should not accept a higher-order mapStateToProps that returns a non-table value", function() + local function mapStateToProps() + return function(state) + return "nope" + end + end + + local ConnectedSomeComponent = connect(mapStateToProps)(NoopComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent), + }) + + expect(function() + Roact.mount(tree) + end).to.throw() + end) + + it("should not accept a mapStateToProps that returns a non-table value", function() + local function mapStateToProps() + return "nah" + end + + local ConnectedSomeComponent = connect(mapStateToProps)(NoopComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent), + }) + + expect(function() + Roact.mount(tree) + end).to.throw() + end) + + it("should abort renders when mapStateToProps returns the same data", function() + local function mapStateToProps(state) + return { + count = state.count, + } + end + + local renderCount = 0 + local function SomeComponent(props) + renderCount = renderCount + 1 + end + + local ConnectedSomeComponent = connect(mapStateToProps)(SomeComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent), + }) + + local handle = Roact.mount(tree) + + expect(renderCount).to.equal(1) + + store:dispatch({ type = "an unknown action" }) + store:flush() + + expect(renderCount).to.equal(1) + + store:dispatch({ type = "increment" }) + store:flush() + + expect(renderCount).to.equal(2) + + Roact.unmount(handle) + end) + + it("should only call mapDispatchToProps once and never re-render if no mapStateToProps was passed", function() + local dispatchCount = 0 + local mapDispatchToProps = function(dispatch) + dispatchCount = dispatchCount + 1 + + return { + increment = function() + return dispatch({ type = "increment" }) + end, + } + end + + local renderCount = 0 + local function SomeComponent(props) + renderCount = renderCount + 1 + end + + local ConnectedSomeComponent = connect(nil, mapDispatchToProps)(SomeComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent), + }) + + local handle = Roact.mount(tree) + + expect(dispatchCount).to.equal(1) + expect(renderCount).to.equal(1) + + store:dispatch({ type = "an unknown action" }) + store:flush() + + expect(dispatchCount).to.equal(1) + expect(renderCount).to.equal(1) + + store:dispatch({ type = "increment" }) + store:flush() + + expect(dispatchCount).to.equal(1) + expect(renderCount).to.equal(1) + + Roact.unmount(handle) + end) + + it("should return result values from the dispatch passed to mapDispatchToProps", function() + local function reducer() + return 0 + end + + local function fiveThunk() + return 5 + end + + local dispatch + local function SomeComponent(props) + dispatch = props.dispatch + end + + local function mapDispatchToProps(dispatch) + return { + dispatch = dispatch + } + end + + local ConnectedSomeComponent = connect(nil, mapDispatchToProps)(SomeComponent) + + -- We'll use the thunk middleware, as it should always return its result + local store = Rodux.Store.new(reducer, nil, { Rodux.thunkMiddleware }) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + someComponent = Roact.createElement(ConnectedSomeComponent) + }) + + local handle = Roact.mount(tree) + + expect(dispatch).to.be.a("function") + expect(dispatch(fiveThunk)).to.equal(5) + + Roact.unmount(handle) + end) + + it("should render parent elements before children", function() + local oldNewConnectionOrder = TempConfig.newConnectionOrder + TempConfig.newConnectionOrder = true + + local function mapStateToProps(state) + return { + count = state.count, + } + end + + local childWasRenderedFirst = false + + local function ChildComponent(props) + if props.count > props.parentCount then + childWasRenderedFirst = true + end + end + + local ConnectedChildComponent = connect(mapStateToProps)(ChildComponent) + + local function ParentComponent(props) + return Roact.createElement(ConnectedChildComponent, { + parentCount = props.count, + }) + end + + local ConnectedParentComponent = connect(mapStateToProps)(ParentComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + parent = Roact.createElement(ConnectedParentComponent), + }) + + local handle = Roact.mount(tree) + + store:dispatch({ type = "increment" }) + store:flush() + + store:dispatch({ type = "increment" }) + store:flush() + + Roact.unmount(handle) + + expect(childWasRenderedFirst).to.equal(false) + + TempConfig.newConnectionOrder = oldNewConnectionOrder + end) + + it("should render child elements before children when TempConfig.newConnectionOrder is false", function() + local oldNewConnectionOrder = TempConfig.newConnectionOrder + TempConfig.newConnectionOrder = false + + local function mapStateToProps(state) + return { + count = state.count, + } + end + + local childWasRenderedFirst = false + + local function ChildComponent(props) + if props.count > props.parentCount then + childWasRenderedFirst = true + end + end + + local ConnectedChildComponent = connect(mapStateToProps)(ChildComponent) + + local function ParentComponent(props) + return Roact.createElement(ConnectedChildComponent, { + parentCount = props.count, + }) + end + + local ConnectedParentComponent = connect(mapStateToProps)(ParentComponent) + + local store = Rodux.Store.new(reducer) + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + parent = Roact.createElement(ConnectedParentComponent), + }) + + local handle = Roact.mount(tree) + + store:dispatch({ type = "increment" }) + store:flush() + + store:dispatch({ type = "increment" }) + store:flush() + + Roact.unmount(handle) + + expect(childWasRenderedFirst).to.equal(true) + + TempConfig.newConnectionOrder = oldNewConnectionOrder + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/getStore.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/getStore.lua new file mode 100644 index 0000000..61fbd71 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/getStore.lua @@ -0,0 +1,7 @@ +local storeKey = require(script.Parent.storeKey) + +local function getStore(componentInstance) + return componentInstance._context[storeKey] +end + +return getStore \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/getStore.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/getStore.spec.lua new file mode 100644 index 0000000..c79966a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/getStore.spec.lua @@ -0,0 +1,62 @@ +return function() + local Roact = require(script.Parent.Parent.Roact) + local Rodux = require(script.Parent.Parent.Rodux) + + local StoreProvider = require(script.Parent.StoreProvider) + + local getStore = require(script.Parent.getStore) + + it("should return the store when present", function() + local function reducer() + return 0 + end + + local store = Rodux.Store.new(reducer) + local consumedStore = nil + + local StoreConsumer = Roact.Component:extend("StoreConsumer") + + function StoreConsumer:init() + consumedStore = getStore(self) + end + + function StoreConsumer:render() + return nil + end + + local tree = Roact.createElement(StoreProvider, { + store = store, + }, { + Consumer = Roact.createElement(StoreConsumer), + }) + + local handle = Roact.mount(tree) + + expect(consumedStore).to.equal(store) + + Roact.unmount(handle) + store:destruct() + end) + + it("should return nil when the store is not present", function() + -- Use a non-nil value to know for sure if StoreConsumer:init was called + local consumedStore = 6 + + local StoreConsumer = Roact.Component:extend("StoreConsumer") + + function StoreConsumer:init() + consumedStore = getStore(self) + end + + function StoreConsumer:render() + return nil + end + + local tree = Roact.createElement(StoreConsumer) + local handle = Roact.mount(tree) + + expect(consumedStore).to.equal(nil) + + Roact.unmount(handle) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/init.lua new file mode 100644 index 0000000..d7db6e6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/init.lua @@ -0,0 +1,13 @@ +local StoreProvider = require(script.StoreProvider) +local connect = require(script.connect) +local getStore = require(script.getStore) +local TempConfig = require(script.TempConfig) + +return { + StoreProvider = StoreProvider, + connect = connect, + UNSTABLE_getStore = getStore, + UNSTABLE_connect2 = connect, + + TEMP_CONFIG = TempConfig, +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/join.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/join.lua new file mode 100644 index 0000000..0e6b195 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/join.lua @@ -0,0 +1,17 @@ +local function join(...) + local result = {} + + for i = 1, select("#", ...) do + local source = select(i, ...) + + if source ~= nil then + for key, value in pairs(source) do + result[key] = value + end + end + end + + return result +end + +return join \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/shallowEqual.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/shallowEqual.lua new file mode 100644 index 0000000..8e9b68a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/shallowEqual.lua @@ -0,0 +1,23 @@ +local function shallowEqual(a, b) + if a == nil then + return b == nil + elseif b == nil then + return a == nil + end + + for key, value in pairs(a) do + if value ~= b[key] then + return false + end + end + + for key, value in pairs(b) do + if value ~= a[key] then + return false + end + end + + return true +end + +return shallowEqual \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/shallowEqual.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/shallowEqual.spec.lua new file mode 100644 index 0000000..fd0f9a2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/shallowEqual.spec.lua @@ -0,0 +1,45 @@ +return function() + local shallowEqual = require(script.Parent.shallowEqual) + + it("should compare dictionaries", function() + local a = { + a = "a", + b = {}, + c = 6, + } + + local b = { + b = a.b, + c = a.c, + a = a.a, + } + + local c = { + b = {}, + a = a.a, + c = a.c, + } + + local d = { + a = a.a, + b = a.b, + c = a.c, + d = "hello", + } + + expect(shallowEqual(a, a)).to.equal(true) + expect(shallowEqual(a, b)).to.equal(true) + expect(shallowEqual(a, c)).to.equal(false) + expect(shallowEqual(b, c)).to.equal(false) + expect(shallowEqual(a, d)).to.equal(false) + expect(shallowEqual(b, d)).to.equal(false) + end) + + it("should handle nil for either argument", function() + local a = {} + + expect(shallowEqual(nil, nil)).to.equal(true) + expect(shallowEqual(a, nil)).to.equal(false) + expect(shallowEqual(nil, a)).to.equal(false) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/storeKey.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/storeKey.lua new file mode 100644 index 0000000..bb77a74 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact-rodux/roact-rodux/storeKey.lua @@ -0,0 +1,3 @@ +local Symbol = require(script.Parent.Symbol) + +return Symbol.named("RoduxStore") \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/lock.toml new file mode 100644 index 0000000..90053c0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/roact" +version = "1.3.1" +commit = "380c3d652f41d896d2f5ebcc128aeed99d176184" +source = "url+https://github.com/roblox/roact" diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Binding.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Binding.lua new file mode 100644 index 0000000..b50acf6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Binding.lua @@ -0,0 +1,157 @@ +local createSignal = require(script.Parent.createSignal) +local Symbol = require(script.Parent.Symbol) +local Type = require(script.Parent.Type) + +local config = require(script.Parent.GlobalConfig).get() + +local BindingImpl = Symbol.named("BindingImpl") + +local BindingInternalApi = {} + +local bindingPrototype = {} + +function bindingPrototype:getValue() + return BindingInternalApi.getValue(self) +end + +function bindingPrototype:map(predicate) + return BindingInternalApi.map(self, predicate) +end + +local BindingPublicMeta = { + __index = bindingPrototype, + __tostring = function(self) + return string.format("RoactBinding(%s)", tostring(self:getValue())) + end, +} + +function BindingInternalApi.update(binding, newValue) + return binding[BindingImpl].update(newValue) +end + +function BindingInternalApi.subscribe(binding, callback) + return binding[BindingImpl].subscribe(callback) +end + +function BindingInternalApi.getValue(binding) + return binding[BindingImpl].getValue() +end + +function BindingInternalApi.create(initialValue) + local impl = { + value = initialValue, + changeSignal = createSignal(), + } + + function impl.subscribe(callback) + return impl.changeSignal:subscribe(callback) + end + + function impl.update(newValue) + impl.value = newValue + impl.changeSignal:fire(newValue) + end + + function impl.getValue() + return impl.value + end + + return setmetatable({ + [Type] = Type.Binding, + [BindingImpl] = impl, + }, BindingPublicMeta), impl.update +end + +function BindingInternalApi.map(upstreamBinding, predicate) + if config.typeChecks then + assert(Type.of(upstreamBinding) == Type.Binding, "Expected arg #1 to be a binding") + assert(typeof(predicate) == "function", "Expected arg #1 to be a function") + end + + local impl = {} + + function impl.subscribe(callback) + return BindingInternalApi.subscribe(upstreamBinding, function(newValue) + callback(predicate(newValue)) + end) + end + + function impl.update(newValue) + error("Bindings created by Binding:map(fn) cannot be updated directly", 2) + end + + function impl.getValue() + return predicate(upstreamBinding:getValue()) + end + + return setmetatable({ + [Type] = Type.Binding, + [BindingImpl] = impl, + }, BindingPublicMeta) +end + +function BindingInternalApi.join(upstreamBindings) + if config.typeChecks then + assert(typeof(upstreamBindings) == "table", "Expected arg #1 to be of type table") + + for key, value in pairs(upstreamBindings) do + if Type.of(value) ~= Type.Binding then + local message = ( + "Expected arg #1 to contain only bindings, but key %q had a non-binding value" + ):format( + tostring(key) + ) + error(message, 2) + end + end + end + + local impl = {} + + local function getValue() + local value = {} + + for key, upstream in pairs(upstreamBindings) do + value[key] = upstream:getValue() + end + + return value + end + + function impl.subscribe(callback) + local disconnects = {} + + for key, upstream in pairs(upstreamBindings) do + disconnects[key] = BindingInternalApi.subscribe(upstream, function(newValue) + callback(getValue()) + end) + end + + return function() + if disconnects == nil then + return + end + + for _, disconnect in pairs(disconnects) do + disconnect() + end + + disconnects = nil + end + end + + function impl.update(newValue) + error("Bindings created by joinBindings(...) cannot be updated directly", 2) + end + + function impl.getValue() + return getValue() + end + + return setmetatable({ + [Type] = Type.Binding, + [BindingImpl] = impl, + }, BindingPublicMeta) +end + +return BindingInternalApi \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Binding.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Binding.spec.lua new file mode 100644 index 0000000..f4fd03e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Binding.spec.lua @@ -0,0 +1,269 @@ +return function() + local createSpy = require(script.Parent.createSpy) + local Type = require(script.Parent.Type) + local GlobalConfig = require(script.Parent.GlobalConfig) + + local Binding = require(script.Parent.Binding) + + describe("Binding.create", function() + it("should return a Binding object and an update function", function() + local binding, update = Binding.create(1) + + expect(Type.of(binding)).to.equal(Type.Binding) + expect(typeof(update)).to.equal("function") + end) + + it("should support tostring on bindings", function() + local binding, update = Binding.create(1) + expect(tostring(binding)).to.equal("RoactBinding(1)") + + update("foo") + expect(tostring(binding)).to.equal("RoactBinding(foo)") + end) + end) + + describe("Binding object", function() + it("should provide a getter and setter", function() + local binding, update = Binding.create(1) + + expect(binding:getValue()).to.equal(1) + + update(3) + + expect(binding:getValue()).to.equal(3) + end) + + it("should let users subscribe and unsubscribe to its updates", function() + local binding, update = Binding.create(1) + + local spy = createSpy() + local disconnect = Binding.subscribe(binding, spy.value) + + expect(spy.callCount).to.equal(0) + + update(2) + + expect(spy.callCount).to.equal(1) + spy:assertCalledWith(2) + + disconnect() + update(3) + + expect(spy.callCount).to.equal(1) + end) + end) + + describe("Mapped bindings", function() + it("should be composable", function() + local word, updateWord = Binding.create("hi") + + local wordLength = word:map(string.len) + local isEvenLength = wordLength:map(function(value) + return value % 2 == 0 + end) + + expect(word:getValue()).to.equal("hi") + expect(wordLength:getValue()).to.equal(2) + expect(isEvenLength:getValue()).to.equal(true) + + updateWord("sup") + + expect(word:getValue()).to.equal("sup") + expect(wordLength:getValue()).to.equal(3) + expect(isEvenLength:getValue()).to.equal(false) + end) + + it("should cascade updates when subscribed", function() + -- base binding + local word, updateWord = Binding.create("hi") + + local wordSpy = createSpy() + local disconnectWord = Binding.subscribe(word, wordSpy.value) + + -- binding -> base binding + local length = word:map(string.len) + + local lengthSpy = createSpy() + local disconnectLength = Binding.subscribe(length, lengthSpy.value) + + -- binding -> binding -> base binding + local isEvenLength = length:map(function(value) + return value % 2 == 0 + end) + + local isEvenLengthSpy = createSpy() + local disconnectIsEvenLength = Binding.subscribe(isEvenLength, isEvenLengthSpy.value) + + expect(wordSpy.callCount).to.equal(0) + expect(lengthSpy.callCount).to.equal(0) + expect(isEvenLengthSpy.callCount).to.equal(0) + + updateWord("nice") + + expect(wordSpy.callCount).to.equal(1) + wordSpy:assertCalledWith("nice") + + expect(lengthSpy.callCount).to.equal(1) + lengthSpy:assertCalledWith(4) + + expect(isEvenLengthSpy.callCount).to.equal(1) + isEvenLengthSpy:assertCalledWith(true) + + disconnectWord() + disconnectLength() + disconnectIsEvenLength() + + updateWord("goodbye") + + expect(wordSpy.callCount).to.equal(1) + expect(isEvenLengthSpy.callCount).to.equal(1) + expect(lengthSpy.callCount).to.equal(1) + end) + + it("should throw when updated directly", function() + local source = Binding.create(1) + local mapped = source:map(function(v) + return v + end) + + expect(function() + Binding.update(mapped, 5) + end).to.throw() + end) + end) + + describe("Binding.join", function() + it("should have getValue", function() + local binding1 = Binding.create(1) + local binding2 = Binding.create(2) + local binding3 = Binding.create(3) + + local joinedBinding = Binding.join({ + binding1, + binding2, + foo = binding3, + }) + + local bindingValue = joinedBinding:getValue() + expect(bindingValue).to.be.a("table") + expect(bindingValue[1]).to.equal(1) + expect(bindingValue[2]).to.equal(2) + expect(bindingValue.foo).to.equal(3) + end) + + it("should update when any one of the subscribed bindings updates", function() + local binding1, update1 = Binding.create(1) + local binding2, update2 = Binding.create(2) + local binding3, update3 = Binding.create(3) + + local joinedBinding = Binding.join({ + binding1, + binding2, + foo = binding3, + }) + + local spy = createSpy() + Binding.subscribe(joinedBinding, spy.value) + + expect(spy.callCount).to.equal(0) + + update1(3) + expect(spy.callCount).to.equal(1) + + local args = spy:captureValues("value") + expect(args.value).to.be.a("table") + expect(args.value[1]).to.equal(3) + expect(args.value[2]).to.equal(2) + expect(args.value["foo"]).to.equal(3) + + update2(4) + expect(spy.callCount).to.equal(2) + + args = spy:captureValues("value") + expect(args.value).to.be.a("table") + expect(args.value[1]).to.equal(3) + expect(args.value[2]).to.equal(4) + expect(args.value["foo"]).to.equal(3) + + update3(8) + expect(spy.callCount).to.equal(3) + + args = spy:captureValues("value") + expect(args.value).to.be.a("table") + expect(args.value[1]).to.equal(3) + expect(args.value[2]).to.equal(4) + expect(args.value["foo"]).to.equal(8) + end) + + it("should disconnect from all upstream bindings", function() + local binding1, update1 = Binding.create(1) + local binding2, update2 = Binding.create(2) + + local joined = Binding.join({binding1, binding2}) + + local spy = createSpy() + local disconnect = Binding.subscribe(joined, spy.value) + + expect(spy.callCount).to.equal(0) + + update1(3) + expect(spy.callCount).to.equal(1) + + update2(3) + expect(spy.callCount).to.equal(2) + + disconnect() + update1(4) + expect(spy.callCount).to.equal(2) + + update2(2) + expect(spy.callCount).to.equal(2) + + local value = joined:getValue() + expect(value[1]).to.equal(4) + expect(value[2]).to.equal(2) + end) + + it("should be okay with calling disconnect multiple times", function() + local joined = Binding.join({}) + + local disconnect = Binding.subscribe(joined, function() end) + + disconnect() + disconnect() + end) + + it("should throw if updated directly", function() + local joined = Binding.join({}) + + expect(function() + Binding.update(joined, 0) + end) + end) + + it("should throw when a non-table value is passed", function() + GlobalConfig.scoped({ + typeChecks = true, + }, function() + expect(function() + Binding.join("hi") + end).to.throw() + end) + end) + + it("should throw when a non-binding value is passed via table", function() + GlobalConfig.scoped({ + typeChecks = true, + }, function() + expect(function() + local binding = Binding.create(123) + + Binding.join({ + binding, + "abcde", + }) + end).to.throw() + end) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.lua new file mode 100644 index 0000000..1283374 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.lua @@ -0,0 +1,508 @@ +local assign = require(script.Parent.assign) +local ComponentLifecyclePhase = require(script.Parent.ComponentLifecyclePhase) +local Type = require(script.Parent.Type) +local Symbol = require(script.Parent.Symbol) +local invalidSetStateMessages = require(script.Parent.invalidSetStateMessages) +local internalAssert = require(script.Parent.internalAssert) + +local config = require(script.Parent.GlobalConfig).get() + +--[[ + Calling setState during certain lifecycle allowed methods has the potential + to create an infinitely updating component. Rather than time out, we exit + with an error if an unreasonable number of self-triggering updates occur +]] +local MAX_PENDING_UPDATES = 100 + +local InternalData = Symbol.named("InternalData") + +local componentMissingRenderMessage = [[ +The component %q is missing the `render` method. +`render` must be defined when creating a Roact component!]] + +local tooManyUpdatesMessage = [[ +The component %q has reached the setState update recursion limit. +When using `setState` in `didUpdate`, make sure that it won't repeat infinitely!]] + +local componentClassMetatable = {} + +function componentClassMetatable:__tostring() + return self.__componentName +end + +local Component = {} +setmetatable(Component, componentClassMetatable) + +Component[Type] = Type.StatefulComponentClass +Component.__index = Component +Component.__componentName = "Component" + +--[[ + A method called by consumers of Roact to create a new component class. + Components can not be extended beyond this point, with the exception of + PureComponent. +]] +function Component:extend(name) + if config.typeChecks then + assert(Type.of(self) == Type.StatefulComponentClass, "Invalid `self` argument to `extend`.") + assert(typeof(name) == "string", "Component class name must be a string") + end + + local class = {} + + for key, value in pairs(self) do + -- Roact opts to make consumers use composition over inheritance, which + -- lines up with React. + -- https://reactjs.org/docs/composition-vs-inheritance.html + if key ~= "extend" then + class[key] = value + end + end + + class[Type] = Type.StatefulComponentClass + class.__index = class + class.__componentName = name + + setmetatable(class, componentClassMetatable) + + return class +end + +function Component:__getDerivedState(incomingProps, incomingState) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__getDerivedState`") + end + + local internalData = self[InternalData] + local componentClass = internalData.componentClass + + if componentClass.getDerivedStateFromProps ~= nil then + local derivedState = componentClass.getDerivedStateFromProps(incomingProps, incomingState) + + if derivedState ~= nil then + if config.typeChecks then + assert(typeof(derivedState) == "table", "getDerivedStateFromProps must return a table!") + end + + return derivedState + end + end + + return nil +end + +function Component:setState(mapState) + if config.typeChecks then + assert(Type.of(self) == Type.StatefulComponentInstance, "Invalid `self` argument to `extend`.") + end + + local internalData = self[InternalData] + local lifecyclePhase = internalData.lifecyclePhase + + --[[ + When preparing to update, rendering, or unmounting, it is not safe + to call `setState` as it will interfere with in-flight updates. It's + also disallowed during unmounting + ]] + if lifecyclePhase == ComponentLifecyclePhase.ShouldUpdate or + lifecyclePhase == ComponentLifecyclePhase.WillUpdate or + lifecyclePhase == ComponentLifecyclePhase.Render or + lifecyclePhase == ComponentLifecyclePhase.WillUnmount + then + local messageTemplate = invalidSetStateMessages[internalData.lifecyclePhase] + + local message = messageTemplate:format(tostring(internalData.componentClass)) + + error(message, 2) + end + + local pendingState = internalData.pendingState + + local partialState + if typeof(mapState) == "function" then + partialState = mapState(pendingState or self.state, self.props) + + -- Abort the state update if the given state updater function returns nil + if partialState == nil then + return + end + elseif typeof(mapState) == "table" then + partialState = mapState + else + error("Invalid argument to setState, expected function or table", 2) + end + + local newState + if pendingState ~= nil then + newState = assign(pendingState, partialState) + else + newState = assign({}, self.state, partialState) + end + + if lifecyclePhase == ComponentLifecyclePhase.Init then + -- If `setState` is called in `init`, we can skip triggering an update! + local derivedState = self:__getDerivedState(self.props, newState) + self.state = assign(newState, derivedState) + + elseif lifecyclePhase == ComponentLifecyclePhase.DidMount or + lifecyclePhase == ComponentLifecyclePhase.DidUpdate or + lifecyclePhase == ComponentLifecyclePhase.ReconcileChildren + then + --[[ + During certain phases of the component lifecycle, it's acceptable to + allow `setState` but defer the update until we're done with ones in flight. + We do this by collapsing it into any pending updates we have. + ]] + local derivedState = self:__getDerivedState(self.props, newState) + internalData.pendingState = assign(newState, derivedState) + + elseif lifecyclePhase == ComponentLifecyclePhase.Idle then + -- Outside of our lifecycle, the state update is safe to make immediately + self:__update(nil, newState) + + else + local messageTemplate = invalidSetStateMessages.default + + local message = messageTemplate:format(tostring(internalData.componentClass)) + + error(message, 2) + end +end + +--[[ + Returns the stack trace of where the element was created that this component + instance's properties are based on. + + Intended to be used primarily by diagnostic tools. +]] +function Component:getElementTraceback() + return self[InternalData].virtualNode.currentElement.source +end + +--[[ + Returns a snapshot of this component given the current props and state. Must + be overridden by consumers of Roact and should be a pure function with + regards to props and state. + + TODO (#199): Accept props and state as arguments. +]] +function Component:render() + local internalData = self[InternalData] + + local message = componentMissingRenderMessage:format( + tostring(internalData.componentClass) + ) + + error(message, 0) +end + +--[[ + Retrieves the context value corresponding to the given key. Can return nil + if a requested context key is not present +]] +function Component:__getContext(key) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__getContext`") + internalAssert(key ~= nil, "Context key cannot be nil") + end + + local virtualNode = self[InternalData].virtualNode + local context = virtualNode.context + + return context[key] +end + +--[[ + Adds a new context entry to this component's context table (which will be + passed down to child components). +]] +function Component:__addContext(key, value) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__addContext`") + end + local virtualNode = self[InternalData].virtualNode + + -- Make sure we store a reference to the component's original, unmodified + -- context the virtual node. In the reconciler, we'll restore the original + -- context if we need to replace the node (this happens when a node gets + -- re-rendered as a different component) + if virtualNode.originalContext == nil then + virtualNode.originalContext = virtualNode.context + end + + -- Build a new context table on top of the existing one, then apply it to + -- our virtualNode + local existing = virtualNode.context + virtualNode.context = assign({}, existing, { [key] = value }) +end + +--[[ + Performs property validation if the static method validateProps is declared. + validateProps should follow assert's expected arguments: + (false, message: string) | true. The function may return a message in the + true case; it will be ignored. If this fails, the function will throw the + error. +]] +function Component:__validateProps(props) + if not config.propValidation then + return + end + + local validator = self[InternalData].componentClass.validateProps + + if validator == nil then + return + end + + if typeof(validator) ~= "function" then + error(("validateProps must be a function, but it is a %s.\nCheck the definition of the component %q."):format( + typeof(validator), + self.__componentName + )) + end + + local success, failureReason = validator(props) + + if not success then + failureReason = failureReason or "" + error(("Property validation failed in %s: %s\n\n%s"):format( + self.__componentName, + tostring(failureReason), + self:getElementTraceback() or ""), + 0) + end +end + +--[[ + An internal method used by the reconciler to construct a new component + instance and attach it to the given virtualNode. +]] +function Component:__mount(reconciler, virtualNode) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentClass, "Invalid use of `__mount`") + internalAssert(Type.of(virtualNode) == Type.VirtualNode, "Expected arg #2 to be of type VirtualNode") + end + + local currentElement = virtualNode.currentElement + local hostParent = virtualNode.hostParent + + -- Contains all the information that we want to keep from consumers of + -- Roact, or even other parts of the codebase like the reconciler. + local internalData = { + reconciler = reconciler, + virtualNode = virtualNode, + componentClass = self, + lifecyclePhase = ComponentLifecyclePhase.Init, + } + + local instance = { + [Type] = Type.StatefulComponentInstance, + [InternalData] = internalData, + } + + setmetatable(instance, self) + + virtualNode.instance = instance + + local props = currentElement.props + + if self.defaultProps ~= nil then + props = assign({}, self.defaultProps, props) + end + + instance:__validateProps(props) + + instance.props = props + + local newContext = assign({}, virtualNode.legacyContext) + instance._context = newContext + + instance.state = assign({}, instance:__getDerivedState(instance.props, {})) + + if instance.init ~= nil then + instance:init(instance.props) + assign(instance.state, instance:__getDerivedState(instance.props, instance.state)) + end + + -- It's possible for init() to redefine _context! + virtualNode.legacyContext = instance._context + + internalData.lifecyclePhase = ComponentLifecyclePhase.Render + local renderResult = instance:render() + + internalData.lifecyclePhase = ComponentLifecyclePhase.ReconcileChildren + reconciler.updateVirtualNodeWithRenderResult(virtualNode, hostParent, renderResult) + + if instance.didMount ~= nil then + internalData.lifecyclePhase = ComponentLifecyclePhase.DidMount + instance:didMount() + end + + if internalData.pendingState ~= nil then + -- __update will handle pendingState, so we don't pass any new element or state + instance:__update(nil, nil) + end + + internalData.lifecyclePhase = ComponentLifecyclePhase.Idle +end + +--[[ + Internal method used by the reconciler to clean up any resources held by + this component instance. +]] +function Component:__unmount() + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__unmount`") + end + + local internalData = self[InternalData] + local virtualNode = internalData.virtualNode + local reconciler = internalData.reconciler + + if self.willUnmount ~= nil then + internalData.lifecyclePhase = ComponentLifecyclePhase.WillUnmount + self:willUnmount() + end + + for _, childNode in pairs(virtualNode.children) do + reconciler.unmountVirtualNode(childNode) + end +end + +--[[ + Internal method used by setState (to trigger updates based on state) and by + the reconciler (to trigger updates based on props) + + Returns true if the update was completed, false if it was cancelled by shouldUpdate +]] +function Component:__update(updatedElement, updatedState) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__update`") + internalAssert( + Type.of(updatedElement) == Type.Element or updatedElement == nil, + "Expected arg #1 to be of type Element or nil" + ) + internalAssert( + typeof(updatedState) == "table" or updatedState == nil, + "Expected arg #2 to be of type table or nil" + ) + end + + local internalData = self[InternalData] + local componentClass = internalData.componentClass + + local newProps = self.props + if updatedElement ~= nil then + newProps = updatedElement.props + + if componentClass.defaultProps ~= nil then + newProps = assign({}, componentClass.defaultProps, newProps) + end + + self:__validateProps(newProps) + end + + local updateCount = 0 + repeat + local finalState + local pendingState = nil + + -- Consume any pending state we might have + if internalData.pendingState ~= nil then + pendingState = internalData.pendingState + internalData.pendingState = nil + end + + -- Consume a standard update to state or props + if updatedState ~= nil or newProps ~= self.props then + if pendingState == nil then + finalState = updatedState or self.state + else + finalState = assign(pendingState, updatedState) + end + + local derivedState = self:__getDerivedState(newProps, finalState) + + if derivedState ~= nil then + finalState = assign({}, finalState, derivedState) + end + + updatedState = nil + else + finalState = pendingState + end + + if not self:__resolveUpdate(newProps, finalState) then + -- If the update was short-circuited, bubble the result up to the caller + return false + end + + updateCount = updateCount + 1 + + if updateCount > MAX_PENDING_UPDATES then + error(tooManyUpdatesMessage:format(tostring(internalData.componentClass)), 3) + end + until internalData.pendingState == nil + + return true +end + +--[[ + Internal method used by __update to apply new props and state + + Returns true if the update was completed, false if it was cancelled by shouldUpdate +]] +function Component:__resolveUpdate(incomingProps, incomingState) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__resolveUpdate`") + end + + local internalData = self[InternalData] + local virtualNode = internalData.virtualNode + local reconciler = internalData.reconciler + + local oldProps = self.props + local oldState = self.state + + if incomingProps == nil then + incomingProps = oldProps + end + if incomingState == nil then + incomingState = oldState + end + + if self.shouldUpdate ~= nil then + internalData.lifecyclePhase = ComponentLifecyclePhase.ShouldUpdate + local continueWithUpdate = self:shouldUpdate(incomingProps, incomingState) + + if not continueWithUpdate then + internalData.lifecyclePhase = ComponentLifecyclePhase.Idle + return false + end + end + + if self.willUpdate ~= nil then + internalData.lifecyclePhase = ComponentLifecyclePhase.WillUpdate + self:willUpdate(incomingProps, incomingState) + end + + internalData.lifecyclePhase = ComponentLifecyclePhase.Render + + self.props = incomingProps + self.state = incomingState + + local renderResult = virtualNode.instance:render() + + internalData.lifecyclePhase = ComponentLifecyclePhase.ReconcileChildren + reconciler.updateVirtualNodeWithRenderResult(virtualNode, virtualNode.hostParent, renderResult) + + if self.didUpdate ~= nil then + internalData.lifecyclePhase = ComponentLifecyclePhase.DidUpdate + self:didUpdate(oldProps, oldState) + end + + internalData.lifecyclePhase = ComponentLifecyclePhase.Idle + return true +end + +return Component \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/context.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/context.spec.lua new file mode 100644 index 0000000..1346a33 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/context.spec.lua @@ -0,0 +1,297 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local oneChild = require(script.Parent.Parent.oneChild) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be provided as an internal api on Component", function() + local Provider = Component:extend("Provider") + + function Provider:init() + self:__addContext("foo", "bar") + end + + function Provider:render() + end + + local element = createElement(Provider) + local hostParent = nil + local hostKey = "Provider" + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContext = { + foo = "bar", + } + + assertDeepEqual(node.context, expectedContext) + end) + + it("should be inherited from parent stateful nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + hello = self:__getContext("hello"), + value = self:__getContext("value"), + } + end + + function Consumer:render() + end + + local Parent = Component:extend("Parent") + + function Parent:render() + return createElement(Consumer) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local context = { + hello = "world", + value = 6, + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, context) + + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.context) + assertDeepEqual(node.context, context) + assertDeepEqual(capturedContext, context) + end) + + it("should be inherited from parent function nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + hello = self:__getContext("hello"), + value = self:__getContext("value"), + } + end + + function Consumer:render() + end + + local function Parent() + return createElement(Consumer) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local context = { + hello = "world", + value = 6, + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, context) + + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.context) + assertDeepEqual(node.context, context) + assertDeepEqual(capturedContext, context) + end) + + it("should not copy the context table if it doesn't need to", function() + local Parent = Component:extend("Parent") + + function Parent:init() + self:__addContext("parent", "I'm here!") + end + + function Parent:render() + -- Create some child element + return createElement(function() end) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local parentNode = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContext = { + parent = "I'm here!", + } + + assertDeepEqual(parentNode.context, expectedContext) + + local childNode = oneChild(parentNode.children) + + -- Parent and child should have the same context table + expect(parentNode.context).to.equal(childNode.context) + end) + + it("should not allow context to move up the tree", function() + local ChildProvider = Component:extend("ChildProvider") + + function ChildProvider:init() + self:__addContext("child", "I'm here too!") + end + + function ChildProvider:render() + end + + local ParentProvider = Component:extend("ParentProvider") + + function ParentProvider:init() + self:__addContext("parent", "I'm here!") + end + + function ParentProvider:render() + return createElement(ChildProvider) + end + + local element = createElement(ParentProvider) + local hostParent = nil + local hostKey = "Parent" + + local parentNode = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + local childNode = oneChild(parentNode.children) + + local expectedParentContext = { + parent = "I'm here!", + -- Context does not travel back up + } + + local expectedChildContext = { + parent = "I'm here!", + child = "I'm here too!" + } + + assertDeepEqual(parentNode.context, expectedParentContext) + assertDeepEqual(childNode.context, expectedChildContext) + end) + + it("should contain values put into the tree by parent nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + dont = self:__getContext("dont"), + frob = self:__getContext("frob"), + } + end + + function Consumer:render() + end + + local Provider = Component:extend("Provider") + + function Provider:init() + self:__addContext("frob", "ulator") + end + + function Provider:render() + return createElement(Consumer) + end + + local element = createElement(Provider) + local hostParent = nil + local hostKey = "Consumer" + local context = { + dont = "try it", + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, context) + + local initialContext = { + dont = "try it", + } + + local expectedContext = { + dont = "try it", + frob = "ulator", + } + + -- Because components mutate context, we're careful with equality + expect(node.context).never.to.equal(context) + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.context) + + assertDeepEqual(context, initialContext) + assertDeepEqual(node.context, expectedContext) + assertDeepEqual(capturedContext, expectedContext) + end) + + it("should transfer context to children that are replaced", function() + local ConsumerA = Component:extend("ConsumerA") + + local function captureAllContext(component) + return { + A = component:__getContext("A"), + B = component:__getContext("B"), + frob = component:__getContext("frob"), + } + end + + local capturedContextA + function ConsumerA:init() + self:__addContext("A", "hello") + + capturedContextA = captureAllContext(self) + end + + function ConsumerA:render() + end + + local ConsumerB = Component:extend("ConsumerB") + + local capturedContextB + function ConsumerB:init() + self:__addContext("B", "hello") + + capturedContextB = captureAllContext(self) + end + + function ConsumerB:render() + end + + local Provider = Component:extend("Provider") + + function Provider:init() + self:__addContext("frob", "ulator") + end + + function Provider:render() + local useConsumerB = self.props.useConsumerB + + if useConsumerB then + return createElement(ConsumerB) + else + return createElement(ConsumerA) + end + end + + local hostParent = nil + local hostKey = "Consumer" + + local element = createElement(Provider) + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContextA = { + frob = "ulator", + A = "hello", + } + + assertDeepEqual(capturedContextA, expectedContextA) + + local expectedContextB = { + frob = "ulator", + B = "hello", + } + + local replacedElement = createElement(Provider, { + useConsumerB = true, + }) + noopReconciler.updateVirtualNode(node, replacedElement) + + assertDeepEqual(capturedContextB, expectedContextB) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/defaultProps.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/defaultProps.spec.lua new file mode 100644 index 0000000..40edca0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/defaultProps.spec.lua @@ -0,0 +1,126 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local None = require(script.Parent.Parent.None) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should fill in when mounting before init", function() + local defaultProps = { + a = 3, + b = 2, + } + + local Foo = Component:extend("Foo") + + Foo.defaultProps = defaultProps + + local capturedProps + function Foo:init() + capturedProps = self.props + end + + function Foo:render() + end + + local initialProps = { + b = 4, + c = 6, + } + + local element = createElement(Foo, initialProps) + local hostParent = nil + local key = "Some Foo" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + local expectedProps = { + a = defaultProps.a, + b = initialProps.b, + c = initialProps.c, + } + + assertDeepEqual(capturedProps, expectedProps) + end) + + it("should fill in when updating via props", function() + local defaultProps = { + a = 3, + b = 2, + } + + local Foo = Component:extend("Foo") + + Foo.defaultProps = defaultProps + + local capturedProps + function Foo:render() + capturedProps = self.props + end + + local initialProps = { + b = 4, + c = 6, + } + + local element = createElement(Foo, initialProps) + local hostParent = nil + local key = "Some Foo" + + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + + local updatedProps = { + c = 5, + } + local updatedElement = createElement(Foo, updatedProps) + + noopReconciler.updateVirtualNode(node, updatedElement) + + local expectedProps = { + a = defaultProps.a, + b = defaultProps.b, + c = updatedProps.c, + } + + assertDeepEqual(capturedProps, expectedProps) + end) + + it("should respect None to override a default prop with nil", function() + local defaultProps = { + a = 3, + b = 2, + } + + local Foo = Component:extend("Foo") + + Foo.defaultProps = defaultProps + + local capturedProps + function Foo:render() + capturedProps = self.props + end + + local initialProps = { + b = None, + c = 4, + } + + local element = createElement(Foo, initialProps) + local hostParent = nil + local key = "Some Foo" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + local expectedProps = { + a = defaultProps.a, + b = nil, + c = initialProps.c, + } + + assertDeepEqual(capturedProps, expectedProps) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/didMount.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/didMount.spec.lua new file mode 100644 index 0000000..b728629 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/didMount.spec.lua @@ -0,0 +1,35 @@ +return function() + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked when mounted", function() + local MyComponent = Component:extend("MyComponent") + + local didMountSpy = createSpy() + + MyComponent.didMount = didMountSpy.value + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(didMountSpy.callCount).to.equal(1) + + local values = didMountSpy:captureValues("self") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/didUpdate.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/didUpdate.spec.lua new file mode 100644 index 0000000..269c6f4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/didUpdate.spec.lua @@ -0,0 +1,92 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked when updated via updateVirtualNode", function() + local MyComponent = Component:extend("MyComponent") + + local didUpdateSpy = createSpy() + MyComponent.didUpdate = didUpdateSpy.value + + function MyComponent:render() + return nil + end + + local initialProps = { + a = 5, + } + local initialElement = createElement(MyComponent, initialProps) + local hostParent = nil + local key = "Test" + + local virtualNode = noopReconciler.mountVirtualNode(initialElement, hostParent, key) + + expect(didUpdateSpy.callCount).to.equal(0) + + local newProps = { + a = 6, + b = 2, + } + local newElement = createElement(MyComponent, newProps) + noopReconciler.updateVirtualNode(virtualNode, newElement) + + expect(didUpdateSpy.callCount).to.equal(1) + + local values = didUpdateSpy:captureValues("self", "oldProps", "oldState") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + assertDeepEqual(values.oldProps, initialProps) + assertDeepEqual(values.oldState, {}) + end) + + it("should be invoked when updated via setState", function() + local MyComponent = Component:extend("MyComponent") + + local didUpdateSpy = createSpy() + MyComponent.didUpdate = didUpdateSpy.value + + local initialState = { + a = 4, + } + + local setState + function MyComponent:init() + setState = function(...) + return self:setState(...) + end + + self:setState(initialState) + end + + function MyComponent:render() + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(didUpdateSpy.callCount).to.equal(0) + + setState({ + a = 5, + }) + + expect(didUpdateSpy.callCount).to.equal(1) + + local values = didUpdateSpy:captureValues("self", "oldProps", "oldState") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + assertDeepEqual(values.oldProps, {}) + assertDeepEqual(values.oldState, initialState) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/extend.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/extend.spec.lua new file mode 100644 index 0000000..04a433a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/extend.spec.lua @@ -0,0 +1,29 @@ +return function() + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + it("should be extendable", function() + local MyComponent = Component:extend("The Senate") + + expect(MyComponent).to.be.ok() + expect(Type.of(MyComponent)).to.equal(Type.StatefulComponentClass) + end) + + it("should prevent extending a user component", function() + local MyComponent = Component:extend("Sheev") + + expect(function() + MyComponent:extend("Frank") + end).to.throw() + end) + + it("should use a given name", function() + local MyComponent = Component:extend("FooBar") + + local name = tostring(MyComponent) + + expect(name).to.be.a("string") + expect(name:find("FooBar")).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/getDerivedStateFromProps.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/getDerivedStateFromProps.spec.lua new file mode 100644 index 0000000..1f04cc8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/getDerivedStateFromProps.spec.lua @@ -0,0 +1,279 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createSpy = require(script.Parent.Parent.createSpy) + local createElement = require(script.Parent.Parent.createElement) + local createFragment = require(script.Parent.Parent.createFragment) + local createReconciler = require(script.Parent.Parent.createReconciler) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked on initial mount", function() + local getDerivedSpy = createSpy() + local WithDerivedState = Component:extend("WithDerivedState") + + WithDerivedState.getDerivedStateFromProps = getDerivedSpy.value + + function WithDerivedState:render() + return nil + end + + local element = createElement(WithDerivedState, { + someProp = 1, + }) + local hostParent = nil + local hostKey = "WithDerivedState" + + noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + expect(getDerivedSpy.callCount).to.equal(1) + + local values = getDerivedSpy:captureValues("props", "state") + + assertDeepEqual(values.props, { someProp = 1 }) + assertDeepEqual(values.state, {}) + end) + + it("should be invoked when updated via props", function() + local getDerivedSpy = createSpy() + local WithDerivedState = Component:extend("WithDerivedState") + + WithDerivedState.getDerivedStateFromProps = getDerivedSpy.value + + function WithDerivedState:render() + return nil + end + + local hostParent = nil + local hostKey = "WithDerivedState" + + local node = noopReconciler.mountVirtualNode(createElement(WithDerivedState, { + someProp = 1, + }), hostParent, hostKey) + + noopReconciler.updateVirtualNode(node, createElement(WithDerivedState, { + someProp = 2, + })) + + expect(getDerivedSpy.callCount).to.equal(2) + + local values = getDerivedSpy:captureValues("props", "state") + + assertDeepEqual(values.props, { someProp = 2 }) + assertDeepEqual(values.state, {}) + end) + + it("should be invoked when updated via state", function() + local getDerivedSpy = createSpy() + local WithDerivedState = Component:extend("WithDerivedState") + + WithDerivedState.getDerivedStateFromProps = getDerivedSpy.value + + function WithDerivedState:init() + self:setState({ + someState = 1, + }) + end + + function WithDerivedState:render() + return nil + end + + local element = createElement(WithDerivedState) + local hostParent = nil + local hostKey = "WithDerivedState" + + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + noopReconciler.updateVirtualNode(node, element, { + someState = 2, + }) + + -- getDerivedStateFromProps will be called: + -- * Once on empty props + -- * Once during the self:setState in init + -- * Once more, defensively, on the resulting state AFTER init + -- * On updating with new state via updateVirtualNode + expect(getDerivedSpy.callCount).to.equal(4) + + local values = getDerivedSpy:captureValues("props", "state") + + assertDeepEqual(values.props, {}) + assertDeepEqual(values.state, { someState = 2 }) + end) + + it("should be invoked when updating via state in init (which skips reconciliation)", function() + local getDerivedSpy = createSpy() + local WithDerivedState = Component:extend("WithDerivedState") + + WithDerivedState.getDerivedStateFromProps = getDerivedSpy.value + + function WithDerivedState:init() + self:setState({ + stateFromInit = 1, + }) + end + + function WithDerivedState:render() + return nil + end + + local element = createElement(WithDerivedState, { + someProp = 1, + }) + local hostParent = nil + local hostKey = "WithDerivedState" + + noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + -- getDerivedStateFromProps will be called: + -- * Once on empty props + -- * Once during the self:setState in init + -- * Once more, defensively, on the resulting state AFTER init + expect(getDerivedSpy.callCount).to.equal(3) + + local values = getDerivedSpy:captureValues("props", "state") + + assertDeepEqual(values.props, { + someProp = 1, + }) + assertDeepEqual(values.state, { + stateFromInit = 1, + }) + end) + + it("should receive defaultProps", function() + local getDerivedSpy = createSpy() + local WithDerivedState = Component:extend("WithDerivedState") + + WithDerivedState.defaultProps = { + someDefaultProp = "foo", + } + + WithDerivedState.getDerivedStateFromProps = getDerivedSpy.value + + function WithDerivedState:render() + return nil + end + + local element = createElement(WithDerivedState, { + someProp = 1, + }) + local hostParent = nil + local hostKey = "WithDerivedState" + + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + expect(getDerivedSpy.callCount).to.equal(1) + + local values = getDerivedSpy:captureValues("props", "state") + + assertDeepEqual(values.props, { + someDefaultProp = "foo", + someProp = 1, + }) + + -- Update via props, confirm that defaultProp is still present + element = createElement(WithDerivedState, { + someProp = 2, + }) + + noopReconciler.updateVirtualNode(node, element) + + expect(getDerivedSpy.callCount).to.equal(2) + + values = getDerivedSpy:captureValues("props", "state") + + assertDeepEqual(values.props, { + someDefaultProp = "foo", + someProp = 2, + }) + end) + + it("should derive state for all setState updates, even when deferred", function() + local Child = Component:extend("Child") + local stateUpdaterSpy = createSpy(function() + return {} + end) + local stateDerivedSpy = createSpy() + + function Child:render() + return nil + end + + function Child:didMount() + self.props.callback() + end + + local Parent = Component:extend("Parent") + + Parent.getDerivedStateFromProps = stateDerivedSpy.value + + function Parent:render() + local callback = function() + self:setState(stateUpdaterSpy.value) + end + + return createFragment({ + ChildA = createElement(Child, { + callback = callback, + }), + ChildB = createElement(Child, { + callback = callback, + }), + }) + end + + local element = createElement(Parent) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(stateUpdaterSpy.callCount).to.equal(2) + + -- getDerivedStateFromProps is always called on initial state + expect(stateDerivedSpy.callCount).to.equal(3) + end) + + it("should have derived state after assigning to state in init", function() + local getStateCallback + local getDerivedSpy = createSpy(function() + return { + derived = true, + } + end) + local WithDerivedState = Component:extend("WithDerivedState") + + WithDerivedState.getDerivedStateFromProps = getDerivedSpy.value + + function WithDerivedState:init() + self.state = { + init = true, + } + + getStateCallback = function() + return self.state + end + end + + function WithDerivedState:render() + return nil + end + + local hostParent = nil + local hostKey = "WithDerivedState" + local element = createElement(WithDerivedState) + + noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + expect(getDerivedSpy.callCount).to.equal(2) + + assertDeepEqual(getStateCallback(), { + init = true, + derived = true, + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/getElementTraceback.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/getElementTraceback.spec.lua new file mode 100644 index 0000000..1d3e0ac --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/getElementTraceback.spec.lua @@ -0,0 +1,67 @@ +return function() + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local GlobalConfig = require(script.Parent.Parent.GlobalConfig) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should return stack traces in initial renders", function() + local TestComponent = Component:extend("TestComponent") + + local stackTrace + function TestComponent:init() + stackTrace = self:getElementTraceback() + end + + function TestComponent:render() + return nil + end + + local config = { + elementTracing = true, + } + + GlobalConfig.scoped(config, function() + local element = createElement(TestComponent) + local hostParent = nil + local key = "Some key" + + noopReconciler.mountVirtualNode(element, hostParent, key) + end) + + expect(stackTrace).to.be.a("string") + end) + + itSKIP("it should return an updated stack trace after an update", function() end) + + it("should return nil when elementTracing is off", function() + local stackTrace = nil + + local config = { + elementTracing = false, + } + + local TestComponent = Component:extend("TestComponent") + + function TestComponent:init() + stackTrace = self:getElementTraceback() + end + + function TestComponent:render() + return nil + end + + GlobalConfig.scoped(config, function() + local element = createElement(TestComponent) + local hostParent = nil + local key = "Some key" + + noopReconciler.mountVirtualNode(element, hostParent, key) + end) + + expect(stackTrace).to.equal(nil) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/init.spec.lua new file mode 100644 index 0000000..af50997 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/init.spec.lua @@ -0,0 +1,41 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked with props when mounted", function() + local MyComponent = Component:extend("MyComponent") + + local initSpy = createSpy() + + MyComponent.init = initSpy.value + + function MyComponent:render() + return nil + end + + local props = { + a = 5, + } + local element = createElement(MyComponent, props) + local hostParent = nil + local key = "Some Component Key" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(initSpy.callCount).to.equal(1) + + local values = initSpy:captureValues("self", "props") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + expect(typeof(values.props)).to.equal("table") + assertDeepEqual(values.props, props) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/legacyContext.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/legacyContext.spec.lua new file mode 100644 index 0000000..e1014f2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/legacyContext.spec.lua @@ -0,0 +1,209 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be provided as a mutable self._context in Component:init", function() + local Provider = Component:extend("Provider") + + function Provider:init() + self._context.foo = "bar" + end + + function Provider:render() + end + + local element = createElement(Provider) + local hostParent = nil + local hostKey = "Provider" + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContext = { + foo = "bar", + } + + assertDeepEqual(node.legacyContext, expectedContext) + end) + + it("should be inherited from parent stateful nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = self._context + end + + function Consumer:render() + end + + local Parent = Component:extend("Parent") + + function Parent:render() + return createElement(Consumer) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local context = { + hello = "world", + value = 6, + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) + + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.legacyContext) + assertDeepEqual(node.legacyContext, context) + assertDeepEqual(capturedContext, context) + end) + + it("should be inherited from parent function nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = self._context + end + + function Consumer:render() + end + + local function Parent() + return createElement(Consumer) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local context = { + hello = "world", + value = 6, + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) + + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.legacyContext) + assertDeepEqual(node.legacyContext, context) + assertDeepEqual(capturedContext, context) + end) + + it("should contain values put into the tree by parent nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = self._context + end + + function Consumer:render() + end + + local Provider = Component:extend("Provider") + + function Provider:init() + self._context.frob = "ulator" + end + + function Provider:render() + return createElement(Consumer) + end + + local element = createElement(Provider) + local hostParent = nil + local hostKey = "Consumer" + local context = { + dont = "try it", + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) + + local initialContext = { + dont = "try it", + } + + local expectedContext = { + dont = "try it", + frob = "ulator", + } + + -- Because components mutate context, we're careful with equality + expect(node.legacyContext).never.to.equal(context) + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.legacyContext) + + assertDeepEqual(context, initialContext) + assertDeepEqual(node.legacyContext, expectedContext) + assertDeepEqual(capturedContext, expectedContext) + end) + + it("should transfer context to children that are replaced", function() + local ConsumerA = Component:extend("ConsumerA") + + local capturedContextA + function ConsumerA:init() + self._context.A = "hello" + + capturedContextA = self._context + end + + function ConsumerA:render() + end + + local ConsumerB = Component:extend("ConsumerB") + + local capturedContextB + function ConsumerB:init() + self._context.B = "hello" + + capturedContextB = self._context + end + + function ConsumerB:render() + end + + local Provider = Component:extend("Provider") + + function Provider:init() + self._context.frob = "ulator" + end + + function Provider:render() + local useConsumerB = self.props.useConsumerB + + if useConsumerB then + return createElement(ConsumerB) + else + return createElement(ConsumerA) + end + end + + local hostParent = nil + local hostKey = "Consumer" + + local element = createElement(Provider) + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContextA = { + frob = "ulator", + A = "hello", + } + + assertDeepEqual(capturedContextA, expectedContextA) + + local expectedContextB = { + frob = "ulator", + B = "hello", + } + + local replacedElement = createElement(Provider, { + useConsumerB = true, + }) + noopReconciler.updateVirtualNode(node, replacedElement) + + assertDeepEqual(capturedContextB, expectedContextB) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/render.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/render.spec.lua new file mode 100644 index 0000000..8dac00a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/render.spec.lua @@ -0,0 +1,150 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should throw on mount if not overridden", function() + local MyComponent = Component:extend("MyComponent") + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + local success, result = pcall(function() + noopReconciler.mountVirtualNode(element, hostParent, key) + end) + + expect(success).to.equal(false) + expect(result:match("MyComponent")).to.be.ok() + expect(result:match("render")).to.be.ok() + end) + + it("should be invoked when a component is mounted", function() + local Foo = Component:extend("Foo") + + local capturedProps + local capturedState + local renderSpy = createSpy(function(self) + capturedProps = self.props + capturedState = self.state + end) + Foo.render = renderSpy.value + + local element = createElement(Foo) + local hostParent = nil + local key = "Foo Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(renderSpy.callCount).to.equal(1) + + local renderArguments = renderSpy:captureValues("self") + + expect(Type.of(renderArguments.self)).to.equal(Type.StatefulComponentInstance) + assertDeepEqual(capturedProps, {}) + assertDeepEqual(capturedState, {}) + end) + + it("should be invoked when a component is updated via props", function() + local Foo = Component:extend("Foo") + + local capturedProps + local capturedState + local renderSpy = createSpy(function(self) + capturedProps = self.props + capturedState = self.state + end) + Foo.render = renderSpy.value + + local initialProps = { + a = 2, + } + local element = createElement(Foo, initialProps) + local hostParent = nil + local key = "Foo Test" + + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(renderSpy.callCount).to.equal(1) + + local firstRenderArguments = renderSpy:captureValues("self") + local firstProps = capturedProps + local firstState = capturedState + + expect(Type.of(firstRenderArguments.self)).to.equal(Type.StatefulComponentInstance) + assertDeepEqual(firstProps, initialProps) + assertDeepEqual(firstState, {}) + + local updatedProps = { + a = 3, + } + local newElement = createElement(Foo, updatedProps) + + noopReconciler.updateVirtualNode(node, newElement) + + expect(renderSpy.callCount).to.equal(2) + + local secondRenderArguments = renderSpy:captureValues("self") + local secondProps = capturedProps + local secondState = capturedState + + expect(Type.of(secondRenderArguments.self)).to.equal(Type.StatefulComponentInstance) + expect(secondProps).never.to.equal(firstProps) + assertDeepEqual(secondProps, updatedProps) + expect(secondState).to.equal(firstState) + end) + + it("should be invoked when a component is updated via state", function() + local Foo = Component:extend("Foo") + + local setState + function Foo:init() + setState = function(...) + return self:setState(...) + end + end + + local capturedProps + local capturedState + local renderSpy = createSpy(function(self) + capturedProps = self.props + capturedState = self.state + end) + Foo.render = renderSpy.value + + local element = createElement(Foo) + local hostParent = nil + local key = "Foo Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(renderSpy.callCount).to.equal(1) + + local firstRenderArguments = renderSpy:captureValues("self") + local firstProps = capturedProps + local firstState = capturedState + + expect(Type.of(firstRenderArguments.self)).to.equal(Type.StatefulComponentInstance) + + setState({}) + + expect(renderSpy.callCount).to.equal(2) + + local renderArguments = renderSpy:captureValues("self") + + expect(Type.of(renderArguments.self)).to.equal(Type.StatefulComponentInstance) + expect(capturedProps).to.equal(firstProps) + expect(capturedState).never.to.equal(firstState) + end) + + itSKIP("Test defaultProps on initial render", function() end) + itSKIP("Test defaultProps on prop update", function() end) + itSKIP("Test defaultProps on state update", function() end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/setState.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/setState.spec.lua new file mode 100644 index 0000000..88ae9f5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/setState.spec.lua @@ -0,0 +1,602 @@ +return function() + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local None = require(script.Parent.Parent.None) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + describe("setState", function() + it("should not trigger an extra update when called in init", function() + local renderCount = 0 + local updateCount = 0 + local capturedState + + local InitComponent = Component:extend("InitComponent") + + function InitComponent:init() + self:setState({ + a = 1 + }) + end + + function InitComponent:willUpdate() + updateCount = updateCount + 1 + end + + function InitComponent:render() + renderCount = renderCount + 1 + capturedState = self.state + return nil + end + + local initElement = createElement(InitComponent) + + noopReconciler.mountVirtualTree(initElement) + + expect(renderCount).to.equal(1) + expect(updateCount).to.equal(0) + expect(capturedState.a).to.equal(1) + end) + + it("should throw when called in render", function() + local TestComponent = Component:extend("TestComponent") + + function TestComponent:render() + self:setState({ + a = 1 + }) + end + + local renderElement = createElement(TestComponent) + + local success, result = pcall(noopReconciler.mountVirtualTree, renderElement) + + expect(success).to.equal(false) + expect(result:match("render")).to.be.ok() + expect(result:match("TestComponent")).to.be.ok() + end) + + it("should throw when called in shouldUpdate", function() + local TestComponent = Component:extend("TestComponent") + + function TestComponent:render() + return nil + end + + function TestComponent:shouldUpdate() + self:setState({ + a = 1 + }) + end + + local initialElement = createElement(TestComponent) + local updatedElement = createElement(TestComponent) + + local tree = noopReconciler.mountVirtualTree(initialElement) + + local success, result = pcall(noopReconciler.updateVirtualTree, tree, updatedElement) + + expect(success).to.equal(false) + expect(result:match("shouldUpdate")).to.be.ok() + expect(result:match("TestComponent")).to.be.ok() + end) + + it("should throw when called in willUpdate", function() + local TestComponent = Component:extend("TestComponent") + + function TestComponent:render() + return nil + end + + function TestComponent:willUpdate() + self:setState({ + a = 1 + }) + end + + local initialElement = createElement(TestComponent) + local updatedElement = createElement(TestComponent) + local tree = noopReconciler.mountVirtualTree(initialElement) + + local success, result = pcall(noopReconciler.updateVirtualTree, tree, updatedElement) + + expect(success).to.equal(false) + expect(result:match("willUpdate")).to.be.ok() + expect(result:match("TestComponent")).to.be.ok() + end) + + it("should throw when called in willUnmount", function() + local TestComponent = Component:extend("TestComponent") + + function TestComponent:render() + return nil + end + + function TestComponent:willUnmount() + self:setState({ + a = 1 + }) + end + + local element = createElement(TestComponent) + local tree = noopReconciler.mountVirtualTree(element) + + local success, result = pcall(noopReconciler.unmountVirtualTree, tree) + + expect(success).to.equal(false) + expect(result:match("willUnmount")).to.be.ok() + expect(result:match("TestComponent")).to.be.ok() + end) + + it("should remove values from state when the value is None", function() + local TestComponent = Component:extend("TestComponent") + local setStateCallback, getStateCallback + + function TestComponent:init() + setStateCallback = function(newState) + self:setState(newState) + end + + getStateCallback = function() + return self.state + end + + self:setState({ + value = 0 + }) + end + + function TestComponent:render() + return nil + end + + local element = createElement(TestComponent) + local instance = noopReconciler.mountVirtualNode(element, nil, "Test") + + expect(getStateCallback().value).to.equal(0) + + setStateCallback({ + value = None + }) + + expect(getStateCallback().value).to.equal(nil) + + noopReconciler.unmountVirtualNode(instance) + end) + + it("should invoke functions to compute a partial state", function() + local TestComponent = Component:extend("TestComponent") + local setStateCallback, getStateCallback, getPropsCallback + + function TestComponent:init() + setStateCallback = function(newState) + self:setState(newState) + end + + getStateCallback = function() + return self.state + end + + getPropsCallback = function() + return self.props + end + + self:setState({ + value = 0 + }) + end + + function TestComponent:render() + return nil + end + + local element = createElement(TestComponent) + local instance = noopReconciler.mountVirtualNode(element, nil, "Test") + + expect(getStateCallback().value).to.equal(0) + + setStateCallback(function(state, props) + expect(state).to.equal(getStateCallback()) + expect(props).to.equal(getPropsCallback()) + + return { + value = state.value + 1 + } + end) + + expect(getStateCallback().value).to.equal(1) + + noopReconciler.unmountVirtualNode(instance) + end) + + it("should cancel rendering if the function returns nil", function() + local TestComponent = Component:extend("TestComponent") + local setStateCallback + local renderCount = 0 + + function TestComponent:init() + setStateCallback = function(newState) + self:setState(newState) + end + + self:setState({ + value = 0 + }) + end + + function TestComponent:render() + renderCount = renderCount + 1 + return nil + end + + local element = createElement(TestComponent) + local instance = noopReconciler.mountVirtualNode(element, nil, "Test") + expect(renderCount).to.equal(1) + + setStateCallback(function(state, props) + return nil + end) + + expect(renderCount).to.equal(1) + + noopReconciler.unmountVirtualNode(instance) + end) + end) + + describe("setState suspension", function() + it("should defer setState triggered while reconciling", function() + local Child = Component:extend("Child") + local getParentStateCallback + + function Child:render() + return nil + end + + function Child:didMount() + self.props.callback() + end + + local Parent = Component:extend("Parent") + + function Parent:init() + getParentStateCallback = function() + return self.state + end + end + + function Parent:render() + return createElement(Child, { + callback = function() + self:setState({ + foo = "bar" + }) + end, + }) + end + + local element = createElement(Parent) + local hostParent = nil + local key = "Test" + + local result = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(result).to.be.ok() + expect(getParentStateCallback().foo).to.equal("bar") + end) + + it("should defer setState triggered while reconciling during an update", function() + local Child = Component:extend("Child") + local getParentStateCallback + + function Child:render() + return nil + end + + function Child:didUpdate() + self.props.callback() + end + + local Parent = Component:extend("Parent") + + function Parent:init() + getParentStateCallback = function() + return self.state + end + end + + function Parent:render() + return createElement(Child, { + callback = function() + -- This guards against a stack overflow that would be OUR fault + if not self.state.foo then + self:setState({ + foo = "bar" + }) + end + end, + }) + end + + local element = createElement(Parent) + local hostParent = nil + local key = "Test" + + local result = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(result).to.be.ok() + expect(getParentStateCallback().foo).to.equal(nil) + + result = noopReconciler.updateVirtualNode(result, createElement(Parent)) + + expect(result).to.be.ok() + expect(getParentStateCallback().foo).to.equal("bar") + + noopReconciler.unmountVirtualNode(result) + end) + + it("should combine pending state changes properly", function() + local Child = Component:extend("Child") + local getParentStateCallback + + function Child:render() + return nil + end + + function Child:didMount() + self.props.callback("foo", 1) + self.props.callback("bar", 3) + end + + local Parent = Component:extend("Parent") + + function Parent:init() + getParentStateCallback = function() + return self.state + end + end + + function Parent:render() + return createElement(Child, { + callback = function(key, value) + self:setState({ + [key] = value, + }) + end, + }) + end + + local element = createElement(Parent) + local hostParent = nil + local key = "Test" + + local result = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(result).to.be.ok() + expect(getParentStateCallback().foo).to.equal(1) + expect(getParentStateCallback().bar).to.equal(3) + + noopReconciler.unmountVirtualNode(result) + end) + + it("should abort properly when functional setState returns nil while deferred", function() + local Child = Component:extend("Child") + + function Child:render() + return nil + end + + function Child:didMount() + self.props.callback() + end + + local Parent = Component:extend("Parent") + + local renderSpy = createSpy(function(self) + return createElement(Child, { + callback = function() + self:setState(function() + -- abort the setState + return nil + end) + end, + }) + end) + + Parent.render = renderSpy.value + + local element = createElement(Parent) + local hostParent = nil + local key = "Test" + + local result = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(result).to.be.ok() + expect(renderSpy.callCount).to.equal(1) + + noopReconciler.unmountVirtualNode(result) + end) + + it("should still apply pending state if a subsequent state update was aborted", function() + local Child = Component:extend("Child") + local getParentStateCallback + + function Child:render() + return nil + end + + function Child:didMount() + self.props.callback(function() + return { + foo = 1, + } + end) + self.props.callback(function() + return nil + end) + end + + local Parent = Component:extend("Parent") + + function Parent:init() + getParentStateCallback = function() + return self.state + end + end + + function Parent:render() + return createElement(Child, { + callback = function(stateUpdater) + self:setState(stateUpdater) + end, + }) + end + + local element = createElement(Parent) + local hostParent = nil + local key = "Test" + + local result = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(result).to.be.ok() + expect(getParentStateCallback().foo).to.equal(1) + + noopReconciler.unmountVirtualNode(result) + end) + + it("should not re-process new state when pending state is present after update", function() + local setComponentState + local getComponentState + + local MyComponent = Component:extend("MyComponent") + + function MyComponent:init() + self:setState({ + hasUpdatedOnce = false, + counter = 0, + }) + + setComponentState = function(mapState) + self:setState(mapState) + end + + getComponentState = function() + return self.state + end + end + + function MyComponent:render() + return nil + end + + function MyComponent:didUpdate() + if self.state.hasUpdatedOnce == false then + self:setState({ + hasUpdatedOnce = true, + }) + end + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(getComponentState().hasUpdatedOnce).to.equal(false) + expect(getComponentState().counter).to.equal(0) + + setComponentState(function(state) + return { + counter = state.counter + 1 + } + end) + + expect(getComponentState().hasUpdatedOnce).to.equal(true) + expect(getComponentState().counter).to.equal(1) + end) + + it("should throw when an infinite update is triggered", function() + local InfiniteUpdater = Component:extend("InfiniteUpdater") + + function InfiniteUpdater:render() + return nil + end + + function InfiniteUpdater:didMount() + self:setState({}) + end + + function InfiniteUpdater:didUpdate() + self:setState({}) + end + + local element = createElement(InfiniteUpdater) + local hostParent = nil + local key = "Test" + + local success, result = pcall(noopReconciler.mountVirtualNode, element, hostParent, key) + + expect(success).to.equal(false) + expect(result:find("InfiniteUpdater")).to.be.ok() + expect(result:find("reached the setState update recursion limit")).to.be.ok() + end) + + itSKIP("should process single updates with both new and pending state", function() + --[[ + This situation shouldn't be possible currently, but the implementation + should support it for future update de-duplication + ]] + end) + + it("should call trigger update after didMount when setting state in didMount", function() + --[[ + Before setState suspension, it was possible to call setState in didMount but it would + not actually finish resolving didMount until after the entire update. + + This is theoretically problematic, as it means that lifecycle methods like didUpdate + could be called before didMount is finished. setState suspension resolves this by + suspending state updates made in didMount and didUpdate as well as reconciliation + ]] + local MyComponent = Component:extend("MyComponent") + + function MyComponent:init() + self:setState({ + status = "initial mount" + }) + + self.isMounted = false + end + + function MyComponent:render() + return nil + end + + function MyComponent:didMount() + self:setState({ + status = "mounted" + }) + + self.isMounted = true + end + + function MyComponent:didUpdate(oldProps, oldState) + expect(oldState.status).to.equal("initial mount") + expect(self.state.status).to.equal("mounted") + + expect(self.isMounted).to.equal(true) + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + local result = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(result).to.be.ok() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/shouldUpdate.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/shouldUpdate.spec.lua new file mode 100644 index 0000000..9d53b98 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/shouldUpdate.spec.lua @@ -0,0 +1,175 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked when props update", function() + local MyComponent = Component:extend("MyComponent") + + local capturedProps + local capturedState + local shouldUpdateSpy = createSpy(function(self) + capturedProps = self.props + capturedState = self.state + + return true + end) + + MyComponent.shouldUpdate = shouldUpdateSpy.value + + function MyComponent:render() + return nil + end + + local initialProps = { + a = 5, + } + local initialElement = createElement(MyComponent, initialProps) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(initialElement, hostParent, key) + + expect(shouldUpdateSpy.callCount).to.equal(0) + + local newProps = { + a = 6, + b = 2, + } + local newElement = createElement(MyComponent, newProps) + noopReconciler.updateVirtualNode(node, newElement) + + expect(shouldUpdateSpy.callCount).to.equal(1) + + local values = shouldUpdateSpy:captureValues("self", "newProps", "newState") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + + assertDeepEqual(values.newProps, newProps) + + assertDeepEqual(capturedProps, initialProps) + + expect(values.newState).to.equal(capturedState) + assertDeepEqual(capturedState, {}) + end) + + it("should be invoked when state is updated", function() + local MyComponent = Component:extend("MyComponent") + + local initialState = { + a = 1, + } + + local setState + local initState + function MyComponent:init() + setState = function(...) + return self:setState(...) + end + + self:setState(initialState) + + initState = self.state + end + + local capturedProps + local capturedState + local shouldUpdateSpy = createSpy(function(self) + capturedProps = self.props + capturedState = self.state + + return true + end) + + MyComponent.shouldUpdate = shouldUpdateSpy.value + + function MyComponent:render() + return nil + end + + local initialElement = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(initialElement, hostParent, key) + + expect(shouldUpdateSpy.callCount).to.equal(0) + + local newState = { + a = 2, + b = 3, + } + + setState(newState) + + expect(shouldUpdateSpy.callCount).to.equal(1) + + local values = shouldUpdateSpy:captureValues("self", "newProps", "newState") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + + expect(values.newProps).to.equal(capturedProps) + assertDeepEqual(capturedProps, {}) + + assertDeepEqual(capturedState, initialState) + expect(capturedState).to.equal(initState) + assertDeepEqual(values.newState, newState) + end) + + it("should not abort an update when returning true", function() + local MyComponent = Component:extend("MyComponent") + + function MyComponent:shouldUpdate() + return true + end + + local renderSpy = createSpy() + + MyComponent.render = renderSpy.value + + local initialElement = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(initialElement, hostParent, key) + + expect(renderSpy.callCount).to.equal(1) + + local newElement = createElement(MyComponent) + noopReconciler.updateVirtualNode(node, newElement) + + expect(renderSpy.callCount).to.equal(2) + end) + + it("should abort an update when retuning false", function() + local MyComponent = Component:extend("MyComponent") + + function MyComponent:shouldUpdate() + return false + end + + local renderSpy = createSpy() + + MyComponent.render = renderSpy.value + + local initialElement = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(initialElement, hostParent, key) + + expect(renderSpy.callCount).to.equal(1) + + local newElement = createElement(MyComponent) + noopReconciler.updateVirtualNode(node, newElement) + + expect(renderSpy.callCount).to.equal(1) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/validateProps.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/validateProps.spec.lua new file mode 100644 index 0000000..c7278e0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/validateProps.spec.lua @@ -0,0 +1,269 @@ +return function() + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local GlobalConfig = require(script.Parent.Parent.GlobalConfig) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked when mounted", function() + local config = { + propValidation = true, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + + local validatePropsSpy = createSpy(function() + return true + end) + + MyComponent.validateProps = validatePropsSpy.value + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + expect(validatePropsSpy.callCount).to.equal(1) + end) + end) + + it("should be invoked when props change", function() + local config = { + propValidation = true, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + + local validatePropsSpy = createSpy(function() + return true + end) + + MyComponent.validateProps = validatePropsSpy.value + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent, { a = 1 }) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + expect(validatePropsSpy.callCount).to.equal(1) + validatePropsSpy:assertCalledWithDeepEqual({ + a = 1, + }) + + local newElement = createElement(MyComponent, { a = 2 }) + noopReconciler.updateVirtualNode(node, newElement) + expect(validatePropsSpy.callCount).to.equal(2) + validatePropsSpy:assertCalledWithDeepEqual({ + a = 2, + }) + end) + end) + + it("should not be invoked when state changes", function() + local config = { + propValidation = true, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + + local setStateCallback = nil + local validatePropsSpy = createSpy(function() + return true + end) + + MyComponent.validateProps = validatePropsSpy.value + + function MyComponent:init() + setStateCallback = function(newState) + self:setState(newState) + end + end + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent, { a = 1 }) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(element, hostParent, key) + expect(validatePropsSpy.callCount).to.equal(1) + validatePropsSpy:assertCalledWithDeepEqual({ + a = 1 + }) + + setStateCallback({ + b = 1 + }) + + expect(validatePropsSpy.callCount).to.equal(1) + end) + end) + + it("should throw if validateProps is not a function", function() + local config = { + propValidation = true, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + MyComponent.validateProps = 1 + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + expect(function() + noopReconciler.mountVirtualNode(element, hostParent, key) + end).to.throw() + end) + end) + + it("should throw if validateProps returns false", function() + local config = { + propValidation = true, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + MyComponent.validateProps = function() + return false + end + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + expect(function() + noopReconciler.mountVirtualNode(element, hostParent, key) + end).to.throw() + end) + end) + + it("should include the component name in the error message", function() + local config = { + propValidation = true, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + MyComponent.validateProps = function() + return false + end + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + local success, error = pcall(function() + noopReconciler.mountVirtualNode(element, hostParent, key) + end) + + expect(success).to.equal(false) + local startIndex = error:find("MyComponent") + expect(startIndex).to.be.ok() + end) + end) + + it("should be invoked after defaultProps are applied", function() + local config = { + propValidation = true, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + + local validatePropsSpy = createSpy(function() + return true + end) + + MyComponent.validateProps = validatePropsSpy.value + + function MyComponent:render() + return nil + end + + MyComponent.defaultProps = { + b = 2, + } + + local element = createElement(MyComponent, { a = 1 }) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + expect(validatePropsSpy.callCount).to.equal(1) + validatePropsSpy:assertCalledWithDeepEqual({ + a = 1, + b = 2, + }) + + local newElement = createElement(MyComponent, { a = 2 }) + noopReconciler.updateVirtualNode(node, newElement) + expect(validatePropsSpy.callCount).to.equal(2) + validatePropsSpy:assertCalledWithDeepEqual({ + a = 2, + b = 2, + }) + end) + end) + + it("should not be invoked if the flag is off", function() + local config = { + propValidation = false, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + + local validatePropsSpy = createSpy(function() + return true + end) + + MyComponent.validateProps = validatePropsSpy.value + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent, { a = 1 }) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + expect(validatePropsSpy.callCount).to.equal(0) + + local newElement = createElement(MyComponent, { a = 2 }) + noopReconciler.updateVirtualNode(node, newElement) + expect(validatePropsSpy.callCount).to.equal(0) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/willUnmount.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/willUnmount.spec.lua new file mode 100644 index 0000000..590b61d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/willUnmount.spec.lua @@ -0,0 +1,36 @@ +return function() + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked when unmounted", function() + local MyComponent = Component:extend("MyComponent") + + local willUnmountSpy = createSpy() + + MyComponent.willUnmount = willUnmountSpy.value + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + noopReconciler.unmountVirtualNode(node) + + expect(willUnmountSpy.callCount).to.equal(1) + + local values = willUnmountSpy:captureValues("self") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/willUpdate.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/willUpdate.spec.lua new file mode 100644 index 0000000..b83937f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Component.spec/willUpdate.spec.lua @@ -0,0 +1,93 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local createSpy = require(script.Parent.Parent.createSpy) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local Type = require(script.Parent.Parent.Type) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be invoked when updated via updateVirtualNode", function() + local MyComponent = Component:extend("MyComponent") + + local willUpdateSpy = createSpy() + + MyComponent.willUpdate = willUpdateSpy.value + + function MyComponent:render() + return nil + end + + local initialProps = { + a = 5, + } + local initialElement = createElement(MyComponent, initialProps) + local hostParent = nil + local key = "Test" + + local node = noopReconciler.mountVirtualNode(initialElement, hostParent, key) + + local newProps = { + a = 6, + b = 2, + } + local newElement = createElement(MyComponent, newProps) + noopReconciler.updateVirtualNode(node, newElement) + + expect(willUpdateSpy.callCount).to.equal(1) + + local values = willUpdateSpy:captureValues("self", "newProps", "newState") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + assertDeepEqual(values.newProps, newProps) + assertDeepEqual(values.newState, {}) + end) + + it("it should be invoked when updated via setState", function() + local MyComponent = Component:extend("MyComponent") + local setComponentState + + local willUpdateSpy = createSpy() + + MyComponent.willUpdate = willUpdateSpy.value + + function MyComponent:init() + setComponentState = function(state) + self:setState(state) + end + + self:setState({ + foo = 1 + }) + end + + function MyComponent:render() + return nil + end + + local initialElement = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + noopReconciler.mountVirtualNode(initialElement, hostParent, key) + + expect(willUpdateSpy.callCount).to.equal(0) + + setComponentState({ + foo = 2 + }) + + expect(willUpdateSpy.callCount).to.equal(1) + + local values = willUpdateSpy:captureValues("self", "newProps", "newState") + + expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) + assertDeepEqual(values.newProps, {}) + assertDeepEqual(values.newState, { + foo = 2 + }) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ComponentLifecyclePhase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ComponentLifecyclePhase.lua new file mode 100644 index 0000000..dd23963 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ComponentLifecyclePhase.lua @@ -0,0 +1,19 @@ +local Symbol = require(script.Parent.Symbol) +local strict = require(script.Parent.strict) + +local ComponentLifecyclePhase = strict({ + -- Component methods + Init = Symbol.named("init"), + Render = Symbol.named("render"), + ShouldUpdate = Symbol.named("shouldUpdate"), + WillUpdate = Symbol.named("willUpdate"), + DidMount = Symbol.named("didMount"), + DidUpdate = Symbol.named("didUpdate"), + WillUnmount = Symbol.named("willUnmount"), + + -- Phases describing reconciliation status + ReconcileChildren = Symbol.named("reconcileChildren"), + Idle = Symbol.named("idle"), +}, "ComponentLifecyclePhase") + +return ComponentLifecyclePhase \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Config.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Config.lua new file mode 100644 index 0000000..6884bc4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Config.lua @@ -0,0 +1,123 @@ +--[[ + Exposes an interface to set global configuration values for Roact. + + Configuration can only occur once, and should only be done by an application + using Roact, not a library. + + Any keys that aren't recognized will cause errors. Configuration is only + intended for configuring Roact itself, not extensions or libraries. + + Configuration is expected to be set immediately after loading Roact. Setting + configuration values after an application starts may produce unpredictable + behavior. +]] + +-- Every valid configuration value should be non-nil in this table. +local defaultConfig = { + -- Enables asserts for internal Roact APIs. Useful for debugging Roact itself. + ["internalTypeChecks"] = false, + -- Enables stricter type asserts for Roact's public API. + ["typeChecks"] = false, + -- Enables storage of `debug.traceback()` values on elements for debugging. + ["elementTracing"] = false, + -- Enables validation of component props in stateful components. + ["propValidation"] = false, +} + +-- Build a list of valid configuration values up for debug messages. +local defaultConfigKeys = {} +for key in pairs(defaultConfig) do + table.insert(defaultConfigKeys, key) +end + +local Config = {} + +function Config.new() + local self = {} + + self._currentConfig = setmetatable({}, { + __index = function(_, key) + local message = ( + "Invalid global configuration key %q. Valid configuration keys are: %s" + ):format( + tostring(key), + table.concat(defaultConfigKeys, ", ") + ) + + error(message, 3) + end + }) + + -- We manually bind these methods here so that the Config's methods can be + -- used without passing in self, since they eventually get exposed on the + -- root Roact object. + self.set = function(...) + return Config.set(self, ...) + end + + self.get = function(...) + return Config.get(self, ...) + end + + self.scoped = function(...) + return Config.scoped(self, ...) + end + + self.set(defaultConfig) + + return self +end + +function Config:set(configValues) + -- Validate values without changing any configuration. + -- We only want to apply this configuration if it's valid! + for key, value in pairs(configValues) do + if defaultConfig[key] == nil then + local message = ( + "Invalid global configuration key %q (type %s). Valid configuration keys are: %s" + ):format( + tostring(key), + typeof(key), + table.concat(defaultConfigKeys, ", ") + ) + + error(message, 3) + end + + -- Right now, all configuration values must be boolean. + if typeof(value) ~= "boolean" then + local message = ( + "Invalid value %q (type %s) for global configuration key %q. Valid values are: true, false" + ):format( + tostring(value), + typeof(value), + tostring(key) + ) + + error(message, 3) + end + + self._currentConfig[key] = value + end +end + +function Config:get() + return self._currentConfig +end + +function Config:scoped(configValues, callback) + local previousValues = {} + for key, value in pairs(self._currentConfig) do + previousValues[key] = value + end + + self.set(configValues) + + local success, result = pcall(callback) + + self.set(previousValues) + + assert(success, result) +end + +return Config \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Config.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Config.spec.lua new file mode 100644 index 0000000..08a884f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Config.spec.lua @@ -0,0 +1,52 @@ +return function() + local Config = require(script.Parent.Config) + + it("should accept valid configuration", function() + local config = Config.new() + local values = config.get() + + expect(values.elementTracing).to.equal(false) + + config.set({ + elementTracing = true, + }) + + expect(values.elementTracing).to.equal(true) + end) + + it("should reject invalid configuration keys", function() + local config = Config.new() + + local badKey = "garblegoop" + + local ok, err = pcall(function() + config.set({ + [badKey] = true, + }) + end) + + expect(ok).to.equal(false) + + -- The error should mention our bad key somewhere. + expect(err:find(badKey)).to.be.ok() + end) + + it("should reject invalid configuration values", function() + local config = Config.new() + + local goodKey = "elementTracing" + local badValue = "Hello there!" + + local ok, err = pcall(function() + config.set({ + [goodKey] = badValue, + }) + end) + + expect(ok).to.equal(false) + + -- The error should mention both our key and value + expect(err:find(goodKey)).to.be.ok() + expect(err:find(badValue)).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementKind.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementKind.lua new file mode 100644 index 0000000..22e1e53 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementKind.lua @@ -0,0 +1,51 @@ +--[[ + Contains markers for annotating the type of an element. + + Use `ElementKind` as a key, and values from it as the value. + + local element = { + [ElementKind] = ElementKind.Host, + } +]] + +local Symbol = require(script.Parent.Symbol) +local strict = require(script.Parent.strict) +local Portal = require(script.Parent.Portal) + +local ElementKind = newproxy(true) + +local ElementKindInternal = { + Portal = Symbol.named("Portal"), + Host = Symbol.named("Host"), + Function = Symbol.named("Function"), + Stateful = Symbol.named("Stateful"), + Fragment = Symbol.named("Fragment"), +} + +function ElementKindInternal.of(value) + if typeof(value) ~= "table" then + return nil + end + + return value[ElementKind] +end + +local componentTypesToKinds = { + ["string"] = ElementKindInternal.Host, + ["function"] = ElementKindInternal.Function, + ["table"] = ElementKindInternal.Stateful, +} + +function ElementKindInternal.fromComponent(component) + if component == Portal then + return ElementKind.Portal + else + return componentTypesToKinds[typeof(component)] + end +end + +getmetatable(ElementKind).__index = ElementKindInternal + +strict(ElementKindInternal, "ElementKind") + +return ElementKind \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementKind.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementKind.spec.lua new file mode 100644 index 0000000..80f8c4e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementKind.spec.lua @@ -0,0 +1,54 @@ +return function() + local Portal = require(script.Parent.Portal) + local Component = require(script.Parent.Component) + + local ElementKind = require(script.Parent.ElementKind) + + describe("of", function() + it("should return nil for non-table values", function() + expect(ElementKind.of(nil)).to.equal(nil) + expect(ElementKind.of(5)).to.equal(nil) + expect(ElementKind.of(newproxy(true))).to.equal(nil) + end) + + it("should return nil for table values without an ElementKind key", function() + expect(ElementKind.of({})).to.equal(nil) + end) + + it("should return the ElementKind from a table", function() + local value = { + [ElementKind] = ElementKind.Stateful, + } + + expect(ElementKind.of(value)).to.equal(ElementKind.Stateful) + end) + end) + + describe("fromComponent", function() + it("should handle host components", function() + expect(ElementKind.fromComponent("foo")).to.equal(ElementKind.Host) + end) + + it("should handle function components", function() + local function foo() + end + + expect(ElementKind.fromComponent(foo)).to.equal(ElementKind.Function) + end) + + it("should handle stateful components", function() + local Foo = Component:extend("Foo") + + expect(ElementKind.fromComponent(Foo)).to.equal(ElementKind.Stateful) + end) + + it("should handle portals", function() + expect(ElementKind.fromComponent(Portal)).to.equal(ElementKind.Portal) + end) + + it("should return nil for invalid inputs", function() + expect(ElementKind.fromComponent(5)).to.equal(nil) + expect(ElementKind.fromComponent(newproxy(true))).to.equal(nil) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementUtils.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementUtils.lua new file mode 100644 index 0000000..971b6b1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementUtils.lua @@ -0,0 +1,99 @@ +local Type = require(script.Parent.Type) +local Symbol = require(script.Parent.Symbol) + +local function noop() + return nil +end + +local ElementUtils = {} + +--[[ + A signal value indicating that a child should use its parent's key, because + it has no key of its own. + + This occurs when you return only one element from a function component or + stateful render function. +]] +ElementUtils.UseParentKey = Symbol.named("UseParentKey") + +--[[ + Returns an iterator over the children of an element. + `elementOrElements` may be one of: + * a boolean + * nil + * a single element + * a fragment + * a table of elements + + If `elementOrElements` is a boolean or nil, this will return an iterator with + zero elements. + + If `elementOrElements` is a single element, this will return an iterator with + one element: a tuple where the first value is ElementUtils.UseParentKey, and + the second is the value of `elementOrElements`. + + If `elementOrElements` is a fragment or a table, this will return an iterator + over all the elements of the array. + + If `elementOrElements` is none of the above, this function will throw. +]] +function ElementUtils.iterateElements(elementOrElements) + local richType = Type.of(elementOrElements) + + -- Single child + if richType == Type.Element then + local called = false + + return function() + if called then + return nil + else + called = true + return ElementUtils.UseParentKey, elementOrElements + end + end + end + + local regularType = typeof(elementOrElements) + + if elementOrElements == nil or regularType == "boolean" then + return noop + end + + if regularType == "table" then + return pairs(elementOrElements) + end + + error("Invalid elements") +end + +--[[ + Gets the child corresponding to a given key, respecting Roact's rules for + children. Specifically: + * If `elements` is nil or a boolean, this will return `nil`, regardless of + the key given. + * If `elements` is a single element, this will return `nil`, unless the key + is ElementUtils.UseParentKey. + * If `elements` is a table of elements, this will return `elements[key]`. +]] +function ElementUtils.getElementByKey(elements, hostKey) + if elements == nil or typeof(elements) == "boolean" then + return nil + end + + if Type.of(elements) == Type.Element then + if hostKey == ElementUtils.UseParentKey then + return elements + end + + return nil + end + + if typeof(elements) == "table" then + return elements[hostKey] + end + + error("Invalid elements") +end + +return ElementUtils \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementUtils.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementUtils.spec.lua new file mode 100644 index 0000000..3457abb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/ElementUtils.spec.lua @@ -0,0 +1,95 @@ +return function() + local ElementUtils = require(script.Parent.ElementUtils) + local createElement = require(script.Parent.createElement) + local createFragment = require(script.Parent.createFragment) + local Type = require(script.Parent.Type) + + describe("iterateElements", function() + it("should iterate once for a single child", function() + local child = createElement("TextLabel") + local iterator = ElementUtils.iterateElements(child) + local iteratedKey, iteratedChild = iterator() + -- For single elements, the key should be UseParentKey + expect(iteratedKey).to.equal(ElementUtils.UseParentKey) + expect(iteratedChild).to.equal(child) + + iteratedKey = iterator() + expect(iteratedKey).to.equal(nil) + end) + + it("should iterate over tables", function() + local children = { + a = createElement("TextLabel"), + b = createElement("TextLabel"), + } + + local seenChildren = {} + local count = 0 + + for key, child in ElementUtils.iterateElements(children) do + expect(typeof(key)).to.equal("string") + expect(Type.of(child)).to.equal(Type.Element) + seenChildren[child] = key + count = count + 1 + end + + expect(count).to.equal(2) + expect(seenChildren[children.a]).to.equal("a") + expect(seenChildren[children.b]).to.equal("b") + end) + + it("should return a zero-element iterator for booleans", function() + local booleanIterator = ElementUtils.iterateElements(false) + expect(booleanIterator()).to.equal(nil) + end) + + it("should return a zero-element iterator for nil", function() + local nilIterator = ElementUtils.iterateElements(nil) + expect(nilIterator()).to.equal(nil) + end) + + it("should throw if given an illegal value", function() + expect(function() + ElementUtils.iterateElements(1) + end).to.throw() + end) + end) + + describe("getElementByKey", function() + it("should return nil for booleans", function() + expect(ElementUtils.getElementByKey(true, "test")).to.equal(nil) + end) + + it("should return nil for nil", function() + expect(ElementUtils.getElementByKey(nil, "test")).to.equal(nil) + end) + + describe("single elements", function() + local element = createElement("TextLabel") + + it("should return the element if the key is UseParentKey", function() + expect(ElementUtils.getElementByKey(element, ElementUtils.UseParentKey)).to.equal(element) + end) + + it("should return nil if the key is not UseParentKey", function() + expect(ElementUtils.getElementByKey(element, "test")).to.equal(nil) + end) + end) + + it("should return the corresponding element from a table", function() + local children = { + a = createElement("TextLabel"), + b = createElement("TextLabel"), + } + + expect(ElementUtils.getElementByKey(children, "a")).to.equal(children.a) + expect(ElementUtils.getElementByKey(children, "b")).to.equal(children.b) + end) + + it("should return nil if the key does not exist", function() + local children = createFragment({}) + + expect(ElementUtils.getElementByKey(children, "a")).to.equal(nil) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/GlobalConfig.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/GlobalConfig.lua new file mode 100644 index 0000000..3219835 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/GlobalConfig.lua @@ -0,0 +1,7 @@ +--[[ + Exposes a single instance of a configuration as Roact's GlobalConfig. +]] + +local Config = require(script.Parent.Config) + +return Config.new() \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/GlobalConfig.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/GlobalConfig.spec.lua new file mode 100644 index 0000000..760a2a3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/GlobalConfig.spec.lua @@ -0,0 +1,9 @@ +return function() + local GlobalConfig = require(script.Parent.GlobalConfig) + + it("should have the correct methods", function() + expect(GlobalConfig).to.be.ok() + expect(GlobalConfig.set).to.be.ok() + expect(GlobalConfig.get).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Logging.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Logging.lua new file mode 100644 index 0000000..17a9d6d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Logging.lua @@ -0,0 +1,159 @@ +--[[ + Centralized place to handle logging. Lets us: + - Unit test log output via `Logging.capture` + - Disable verbose log messages when not debugging Roact + + This should be broken out into a separate library with the addition of + scoping and logging configuration. +]] + +-- Determines whether log messages will go to stdout/stderr +local outputEnabled = true + +-- A set of LogInfo objects that should have messages inserted into them. +-- This is a set so that nested calls to Logging.capture will behave. +local collectors = {} + +-- A set of all stack traces that have called warnOnce. +local onceUsedLocations = {} + +--[[ + Indent a potentially multi-line string with the given number of tabs, in + addition to any indentation the string already has. +]] +local function indent(source, indentLevel) + local indentString = ("\t"):rep(indentLevel) + + return indentString .. source:gsub("\n", "\n" .. indentString) +end + +--[[ + Indents a list of strings and then concatenates them together with newlines + into a single string. +]] +local function indentLines(lines, indentLevel) + local outputBuffer = {} + + for _, line in ipairs(lines) do + table.insert(outputBuffer, indent(line, indentLevel)) + end + + return table.concat(outputBuffer, "\n") +end + +local logInfoMetatable = {} + +--[[ + Automatic coercion to strings for LogInfo objects to enable debugging them + more easily. +]] +function logInfoMetatable:__tostring() + local outputBuffer = {"LogInfo {"} + + local errorCount = #self.errors + local warningCount = #self.warnings + local infosCount = #self.infos + + if errorCount + warningCount + infosCount == 0 then + table.insert(outputBuffer, "\t(no messages)") + end + + if errorCount > 0 then + table.insert(outputBuffer, ("\tErrors (%d) {"):format(errorCount)) + table.insert(outputBuffer, indentLines(self.errors, 2)) + table.insert(outputBuffer, "\t}") + end + + if warningCount > 0 then + table.insert(outputBuffer, ("\tWarnings (%d) {"):format(warningCount)) + table.insert(outputBuffer, indentLines(self.warnings, 2)) + table.insert(outputBuffer, "\t}") + end + + if infosCount > 0 then + table.insert(outputBuffer, ("\tInfos (%d) {"):format(infosCount)) + table.insert(outputBuffer, indentLines(self.infos, 2)) + table.insert(outputBuffer, "\t}") + end + + table.insert(outputBuffer, "}") + + return table.concat(outputBuffer, "\n") +end + +local function createLogInfo() + local logInfo = { + errors = {}, + warnings = {}, + infos = {}, + } + + setmetatable(logInfo, logInfoMetatable) + + return logInfo +end + +local Logging = {} + +--[[ + Invokes `callback`, capturing all output that happens during its execution. + + Output will not go to stdout or stderr and will instead be put into a + LogInfo object that is returned. If `callback` throws, the error will be + bubbled up to the caller of `Logging.capture`. +]] +function Logging.capture(callback) + local collector = createLogInfo() + + local wasOutputEnabled = outputEnabled + outputEnabled = false + collectors[collector] = true + + local success, result = pcall(callback) + + collectors[collector] = nil + outputEnabled = wasOutputEnabled + + assert(success, result) + + return collector +end + +--[[ + Issues a warning with an automatically attached stack trace. +]] +function Logging.warn(messageTemplate, ...) + local message = messageTemplate:format(...) + + for collector in pairs(collectors) do + table.insert(collector.warnings, message) + end + + -- debug.traceback inserts a leading newline, so we trim it here + local trace = debug.traceback("", 2):sub(2) + local fullMessage = ("%s\n%s"):format(message, indent(trace, 1)) + + if outputEnabled then + warn(fullMessage) + end +end + +--[[ + Issues a warning like `Logging.warn`, but only outputs once per call site. + + This is useful for marking deprecated functions that might be called a lot; + using `warnOnce` instead of `warn` will reduce output noise while still + correctly marking all call sites. +]] +function Logging.warnOnce(messageTemplate, ...) + local trace = debug.traceback() + + if onceUsedLocations[trace] then + return + end + + onceUsedLocations[trace] = true + Logging.warn(messageTemplate, ...) +end + +return Logging \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/None.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/None.lua new file mode 100644 index 0000000..9f25d3a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/None.lua @@ -0,0 +1,7 @@ +local Symbol = require(script.Parent.Symbol) + +-- Marker used to specify that the value is nothing, because nil cannot be +-- stored in tables. +local None = Symbol.named("None") + +return None \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/NoopRenderer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/NoopRenderer.lua new file mode 100644 index 0000000..8d19157 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/NoopRenderer.lua @@ -0,0 +1,24 @@ +--[[ + Reference renderer intended for use in tests as well as for documenting the + minimum required interface for a Roact renderer. +]] + +local NoopRenderer = {} + +function NoopRenderer.isHostObject(target) + -- Attempting to use NoopRenderer to target a Roblox instance is almost + -- certainly a mistake. + return target == nil +end + +function NoopRenderer.mountHostNode(reconciler, node) +end + +function NoopRenderer.unmountHostNode(reconciler, node) +end + +function NoopRenderer.updateHostNode(reconciler, node, newElement) + return node +end + +return NoopRenderer \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Portal.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Portal.lua new file mode 100644 index 0000000..4db0a37 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Portal.lua @@ -0,0 +1,5 @@ +local Symbol = require(script.Parent.Symbol) + +local Portal = Symbol.named("Portal") + +return Portal \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Change.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Change.lua new file mode 100644 index 0000000..2a20adb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Change.lua @@ -0,0 +1,38 @@ +--[[ + Change is used to generate special prop keys that can be used to connect to + GetPropertyChangedSignal. + + Generally, Change is indexed by a Roblox property name: + + Roact.createElement("TextBox", { + [Roact.Change.Text] = function(rbx) + print("The TextBox", rbx, "changed text to", rbx.Text) + end, + }) +]] + +local Type = require(script.Parent.Parent.Type) + +local Change = {} + +local changeMetatable = { + __tostring = function(self) + return ("RoactHostChangeEvent(%s)"):format(self.name) + end, +} + +setmetatable(Change, { + __index = function(self, propertyName) + local changeListener = { + [Type] = Type.HostChangeEvent, + name = propertyName, + } + + setmetatable(changeListener, changeMetatable) + Change[propertyName] = changeListener + + return changeListener + end, +}) + +return Change diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Change.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Change.spec.lua new file mode 100644 index 0000000..903099d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Change.spec.lua @@ -0,0 +1,19 @@ +return function() + local Type = require(script.Parent.Parent.Type) + + local Change = require(script.Parent.Change) + + it("should yield change listener objects when indexed", function() + expect(Type.of(Change.Text)).to.equal(Type.HostChangeEvent) + expect(Type.of(Change.Selected)).to.equal(Type.HostChangeEvent) + end) + + it("should yield the same object when indexed again", function() + local a = Change.Text + local b = Change.Text + local c = Change.Selected + + expect(a).to.equal(b) + expect(a).never.to.equal(c) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Children.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Children.lua new file mode 100644 index 0000000..8c320dd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Children.lua @@ -0,0 +1,5 @@ +local Symbol = require(script.Parent.Parent.Symbol) + +local Children = Symbol.named("Children") + +return Children \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Event.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Event.lua new file mode 100644 index 0000000..f9aba02 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Event.lua @@ -0,0 +1,41 @@ +--[[ + Index into `Event` to get a prop key for attaching to an event on a Roblox + Instance. + + Example: + + Roact.createElement("TextButton", { + Text = "Hello, world!", + + [Roact.Event.MouseButton1Click] = function(rbx) + print("Clicked", rbx) + end + }) +]] + +local Type = require(script.Parent.Parent.Type) + +local Event = {} + +local eventMetatable = { + __tostring = function(self) + return ("RoactHostEvent(%s)"):format(self.name) + end, +} + +setmetatable(Event, { + __index = function(self, eventName) + local event = { + [Type] = Type.HostEvent, + name = eventName, + } + + setmetatable(event, eventMetatable) + + Event[eventName] = event + + return event + end, +}) + +return Event diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Event.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Event.spec.lua new file mode 100644 index 0000000..fc34e91 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Event.spec.lua @@ -0,0 +1,19 @@ +return function() + local Type = require(script.Parent.Parent.Type) + + local Event = require(script.Parent.Event) + + it("should yield event objects when indexed", function() + expect(Type.of(Event.MouseButton1Click)).to.equal(Type.HostEvent) + expect(Type.of(Event.Touched)).to.equal(Type.HostEvent) + end) + + it("should yield the same object when indexed again", function() + local a = Event.MouseButton1Click + local b = Event.MouseButton1Click + local c = Event.Touched + + expect(a).to.equal(b) + expect(a).never.to.equal(c) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Ref.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Ref.lua new file mode 100644 index 0000000..a86e4c2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PropMarkers/Ref.lua @@ -0,0 +1,5 @@ +local Symbol = require(script.Parent.Parent.Symbol) + +local Ref = Symbol.named("Ref") + +return Ref \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PureComponent.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PureComponent.lua new file mode 100644 index 0000000..0283298 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PureComponent.lua @@ -0,0 +1,41 @@ +--[[ + A version of Component with a `shouldUpdate` method that forces the + resulting component to be pure. +]] + +local Component = require(script.Parent.Component) + +local PureComponent = Component:extend("PureComponent") + +-- When extend()ing a component, you don't get an extend method. +-- This is to promote composition over inheritance. +-- PureComponent is an exception to this rule. +PureComponent.extend = Component.extend + +function PureComponent:shouldUpdate(newProps, newState) + -- In a vast majority of cases, if state updated, something has updated. + -- We don't bother checking in this case. + if newState ~= self.state then + return true + end + + if newProps == self.props then + return false + end + + for key, value in pairs(newProps) do + if self.props[key] ~= value then + return true + end + end + + for key, value in pairs(self.props) do + if newProps[key] ~= value then + return true + end + end + + return false +end + +return PureComponent \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PureComponent.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PureComponent.spec.lua new file mode 100644 index 0000000..b164437 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/PureComponent.spec.lua @@ -0,0 +1,75 @@ +return function() + local createElement = require(script.Parent.createElement) + local NoopRenderer = require(script.Parent.NoopRenderer) + local createReconciler = require(script.Parent.createReconciler) + + local PureComponent = require(script.Parent.PureComponent) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be extendable", function() + local MyComponent = PureComponent:extend("MyComponent") + + expect(MyComponent).to.be.ok() + end) + + it("should skip updates for shallow-equal props", function() + local updateCount = 0 + local setValue + + local PureChild = PureComponent:extend("PureChild") + + function PureChild:willUpdate() + updateCount = updateCount + 1 + end + + function PureChild:render() + return nil + end + + local PureContainer = PureComponent:extend("PureContainer") + + function PureContainer:init() + self.state = { + value = 0, + } + end + + function PureContainer:didMount() + setValue = function(value) + self:setState({ + value = value, + }) + end + end + + function PureContainer:render() + return createElement(PureChild, { + value = self.state.value, + }) + end + + local element = createElement(PureContainer) + local tree = noopReconciler.mountVirtualTree(element, nil, "PureComponent Tree") + + expect(updateCount).to.equal(0) + + setValue(1) + + expect(updateCount).to.equal(1) + + setValue(1) + + expect(updateCount).to.equal(1) + + setValue(2) + + expect(updateCount).to.equal(2) + + setValue(1) + + expect(updateCount).to.equal(3) + + noopReconciler.unmountVirtualTree(tree) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/RobloxRenderer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/RobloxRenderer.lua new file mode 100644 index 0000000..4f528ad --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/RobloxRenderer.lua @@ -0,0 +1,283 @@ +--[[ + Renderer that deals in terms of Roblox Instances. This is the most + well-supported renderer after NoopRenderer and is currently the only + renderer that does anything. +]] + +local Binding = require(script.Parent.Binding) +local Children = require(script.Parent.PropMarkers.Children) +local ElementKind = require(script.Parent.ElementKind) +local SingleEventManager = require(script.Parent.SingleEventManager) +local getDefaultInstanceProperty = require(script.Parent.getDefaultInstanceProperty) +local Ref = require(script.Parent.PropMarkers.Ref) +local Type = require(script.Parent.Type) +local internalAssert = require(script.Parent.internalAssert) + +local config = require(script.Parent.GlobalConfig).get() + +local applyPropsError = [[ +Error applying props: + %s +In element: +%s +]] + +local updatePropsError = [[ +Error updating props: + %s +In element: +%s +]] + +local function identity(...) + return ... +end + +local function applyRef(ref, newHostObject) + if ref == nil then + return + end + + if typeof(ref) == "function" then + ref(newHostObject) + elseif Type.of(ref) == Type.Binding then + Binding.update(ref, newHostObject) + else + -- TODO (#197): Better error message + error(("Invalid ref: Expected type Binding but got %s"):format( + typeof(ref) + )) + end +end + +local function setRobloxInstanceProperty(hostObject, key, newValue) + if newValue == nil then + local hostClass = hostObject.ClassName + local _, defaultValue = getDefaultInstanceProperty(hostClass, key) + newValue = defaultValue + end + + -- Assign the new value to the object + hostObject[key] = newValue + + return +end + +local function removeBinding(virtualNode, key) + local disconnect = virtualNode.bindings[key] + disconnect() + virtualNode.bindings[key] = nil +end + +local function attachBinding(virtualNode, key, newBinding) + local function updateBoundProperty(newValue) + local success, errorMessage = xpcall(function() + setRobloxInstanceProperty(virtualNode.hostObject, key, newValue) + end, identity) + + if not success then + local source = virtualNode.currentElement.source + + if source == nil then + source = "" + end + + local fullMessage = updatePropsError:format(errorMessage, source) + error(fullMessage, 0) + end + end + + if virtualNode.bindings == nil then + virtualNode.bindings = {} + end + + virtualNode.bindings[key] = Binding.subscribe(newBinding, updateBoundProperty) + + updateBoundProperty(newBinding:getValue()) +end + +local function detachAllBindings(virtualNode) + if virtualNode.bindings ~= nil then + for _, disconnect in pairs(virtualNode.bindings) do + disconnect() + end + end +end + +local function applyProp(virtualNode, key, newValue, oldValue) + if newValue == oldValue then + return + end + + if key == Ref or key == Children then + -- Refs and children are handled in a separate pass + return + end + + local internalKeyType = Type.of(key) + + if internalKeyType == Type.HostEvent or internalKeyType == Type.HostChangeEvent then + if virtualNode.eventManager == nil then + virtualNode.eventManager = SingleEventManager.new(virtualNode.hostObject) + end + + local eventName = key.name + + if internalKeyType == Type.HostChangeEvent then + virtualNode.eventManager:connectPropertyChange(eventName, newValue) + else + virtualNode.eventManager:connectEvent(eventName, newValue) + end + + return + end + + local newIsBinding = Type.of(newValue) == Type.Binding + local oldIsBinding = Type.of(oldValue) == Type.Binding + + if oldIsBinding then + removeBinding(virtualNode, key) + end + + if newIsBinding then + attachBinding(virtualNode, key, newValue) + else + setRobloxInstanceProperty(virtualNode.hostObject, key, newValue) + end +end + +local function applyProps(virtualNode, props) + for propKey, value in pairs(props) do + applyProp(virtualNode, propKey, value, nil) + end +end + +local function updateProps(virtualNode, oldProps, newProps) + -- Apply props that were added or updated + for propKey, newValue in pairs(newProps) do + local oldValue = oldProps[propKey] + + applyProp(virtualNode, propKey, newValue, oldValue) + end + + -- Clean up props that were removed + for propKey, oldValue in pairs(oldProps) do + local newValue = newProps[propKey] + + if newValue == nil then + applyProp(virtualNode, propKey, nil, oldValue) + end + end +end + +local RobloxRenderer = {} + +function RobloxRenderer.isHostObject(target) + return typeof(target) == "Instance" +end + +function RobloxRenderer.mountHostNode(reconciler, virtualNode) + local element = virtualNode.currentElement + local hostParent = virtualNode.hostParent + local hostKey = virtualNode.hostKey + + if config.internalTypeChecks then + internalAssert(ElementKind.of(element) == ElementKind.Host, "Element at given node is not a host Element") + end + if config.typeChecks then + assert(element.props.Name == nil, "Name can not be specified as a prop to a host component in Roact.") + assert(element.props.Parent == nil, "Parent can not be specified as a prop to a host component in Roact.") + end + + local instance = Instance.new(element.component) + virtualNode.hostObject = instance + + local success, errorMessage = xpcall(function() + applyProps(virtualNode, element.props) + end, identity) + + if not success then + local source = element.source + + if source == nil then + source = "" + end + + local fullMessage = applyPropsError:format(errorMessage, source) + error(fullMessage, 0) + end + + instance.Name = tostring(hostKey) + + local children = element.props[Children] + + if children ~= nil then + reconciler.updateVirtualNodeWithChildren(virtualNode, virtualNode.hostObject, children) + end + + instance.Parent = hostParent + virtualNode.hostObject = instance + + applyRef(element.props[Ref], instance) + + if virtualNode.eventManager ~= nil then + virtualNode.eventManager:resume() + end +end + +function RobloxRenderer.unmountHostNode(reconciler, virtualNode) + local element = virtualNode.currentElement + + applyRef(element.props[Ref], nil) + + for _, childNode in pairs(virtualNode.children) do + reconciler.unmountVirtualNode(childNode) + end + + detachAllBindings(virtualNode) + + virtualNode.hostObject:Destroy() +end + +function RobloxRenderer.updateHostNode(reconciler, virtualNode, newElement) + local oldProps = virtualNode.currentElement.props + local newProps = newElement.props + + if virtualNode.eventManager ~= nil then + virtualNode.eventManager:suspend() + end + + -- If refs changed, detach the old ref and attach the new one + if oldProps[Ref] ~= newProps[Ref] then + applyRef(oldProps[Ref], nil) + applyRef(newProps[Ref], virtualNode.hostObject) + end + + local success, errorMessage = xpcall(function() + updateProps(virtualNode, oldProps, newProps) + end, identity) + + if not success then + local source = newElement.source + + if source == nil then + source = "" + end + + local fullMessage = updatePropsError:format(errorMessage, source) + error(fullMessage, 0) + end + + local children = newElement.props[Children] + if children ~= nil or oldProps[Children] ~= nil then + reconciler.updateVirtualNodeWithChildren(virtualNode, virtualNode.hostObject, children) + end + + if virtualNode.eventManager ~= nil then + virtualNode.eventManager:resume() + end + + return virtualNode +end + +return RobloxRenderer diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/RobloxRenderer.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/RobloxRenderer.spec.lua new file mode 100644 index 0000000..1ea04cb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/RobloxRenderer.spec.lua @@ -0,0 +1,949 @@ +return function() + local assertDeepEqual = require(script.Parent.assertDeepEqual) + local Binding = require(script.Parent.Binding) + local Children = require(script.Parent.PropMarkers.Children) + local Component = require(script.Parent.Component) + local createElement = require(script.Parent.createElement) + local createFragment = require(script.Parent.createFragment) + local createReconciler = require(script.Parent.createReconciler) + local createRef = require(script.Parent.createRef) + local createSpy = require(script.Parent.createSpy) + local GlobalConfig = require(script.Parent.GlobalConfig) + local Portal = require(script.Parent.Portal) + local Ref = require(script.Parent.PropMarkers.Ref) + + local RobloxRenderer = require(script.Parent.RobloxRenderer) + + local reconciler = createReconciler(RobloxRenderer) + + describe("mountHostNode", function() + it("should create instances with correct props", function() + local parent = Instance.new("Folder") + local value = "Hello!" + local key = "Some Key" + + local element = createElement("StringValue", { + Value = value, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local root = parent:GetChildren()[1] + + expect(root.ClassName).to.equal("StringValue") + expect(root.Value).to.equal(value) + expect(root.Name).to.equal(key) + end) + + it("should create children with correct names and props", function() + local parent = Instance.new("Folder") + local rootValue = "Hey there!" + local childValue = 173 + local key = "Some Key" + + local element = createElement("StringValue", { + Value = rootValue, + }, { + ChildA = createElement("IntValue", { + Value = childValue, + }), + + ChildB = createElement("Folder"), + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local root = parent:GetChildren()[1] + + expect(root.ClassName).to.equal("StringValue") + expect(root.Value).to.equal(rootValue) + expect(root.Name).to.equal(key) + + expect(#root:GetChildren()).to.equal(2) + + local childA = root.ChildA + local childB = root.ChildB + + expect(childA).to.be.ok() + expect(childB).to.be.ok() + + expect(childA.ClassName).to.equal("IntValue") + expect(childA.Value).to.equal(childValue) + + expect(childB.ClassName).to.equal("Folder") + end) + + it("should attach Bindings to Roblox properties", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local binding, update = Binding.create(10) + local element = createElement("IntValue", { + Value = binding, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local instance = parent:GetChildren()[1] + + expect(instance.ClassName).to.equal("IntValue") + expect(instance.Value).to.equal(10) + + update(20) + + expect(instance.Value).to.equal(20) + + RobloxRenderer.unmountHostNode(reconciler, node) + end) + + it("should connect Binding refs", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local ref = createRef() + local element = createElement("Frame", { + [Ref] = ref, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local instance = parent:GetChildren()[1] + + expect(ref.current).to.be.ok() + expect(ref.current).to.equal(instance) + + RobloxRenderer.unmountHostNode(reconciler, node) + end) + + it("should call function refs", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local spyRef = createSpy() + local element = createElement("Frame", { + [Ref] = spyRef.value, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local instance = parent:GetChildren()[1] + + expect(spyRef.callCount).to.equal(1) + spyRef:assertCalledWith(instance) + + RobloxRenderer.unmountHostNode(reconciler, node) + end) + + it("should throw if setting invalid instance properties", function() + local configValues = { + elementTracing = true, + } + + GlobalConfig.scoped(configValues, function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local element = createElement("Frame", { + Frob = 6, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + local success, message = pcall(RobloxRenderer.mountHostNode, reconciler, node) + assert(not success, "Expected call to fail") + + expect(message:find("Frob")).to.be.ok() + expect(message:find("Frame")).to.be.ok() + expect(message:find("RobloxRenderer%.spec")).to.be.ok() + end) + end) + end) + + describe("updateHostNode", function() + it("should update node props and children", function() + -- TODO: Break up test + + local parent = Instance.new("Folder") + local key = "updateHostNodeTest" + local firstValue = "foo" + local newValue = "bar" + + local defaultStringValue = Instance.new("StringValue").Value + + local element = createElement("StringValue", { + Value = firstValue + }, { + ChildA = createElement("IntValue", { + Value = 1 + }), + ChildB = createElement("BoolValue", { + Value = true, + }), + ChildC = createElement("StringValue", { + Value = "test", + }), + ChildD = createElement("StringValue", { + Value = "test", + }) + }) + + local node = reconciler.createVirtualNode(element, parent, key) + RobloxRenderer.mountHostNode(reconciler, node) + + -- Not testing mountHostNode's work here, only testing that the + -- node is properly updated. + + local newElement = createElement("StringValue", { + Value = newValue, + }, { + -- ChildA changes element type. + ChildA = createElement("StringValue", { + Value = "test" + }), + -- ChildB changes child properties. + ChildB = createElement("BoolValue", { + Value = false, + }), + -- ChildC should reset its Value property back to the default. + ChildC = createElement("StringValue", {}), + -- ChildD is deleted. + -- ChildE is added. + ChildE = createElement("Folder", {}), + }) + + RobloxRenderer.updateHostNode(reconciler, node, newElement) + + local root = parent[key] + expect(root.ClassName).to.equal("StringValue") + expect(root.Value).to.equal(newValue) + expect(#root:GetChildren()).to.equal(4) + + local childA = root.ChildA + expect(childA.ClassName).to.equal("StringValue") + expect(childA.Value).to.equal("test") + + local childB = root.ChildB + expect(childB.ClassName).to.equal("BoolValue") + expect(childB.Value).to.equal(false) + + local childC = root.ChildC + expect(childC.ClassName).to.equal("StringValue") + expect(childC.Value).to.equal(defaultStringValue) + + local childE = root.ChildE + expect(childE.ClassName).to.equal("Folder") + end) + + it("should update Bindings", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local bindingA, updateA = Binding.create(10) + local element = createElement("IntValue", { + Value = bindingA, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + local instance = parent:GetChildren()[1] + + expect(instance.Value).to.equal(10) + + local bindingB, updateB = Binding.create(99) + local newElement = createElement("IntValue", { + Value = bindingB, + }) + + RobloxRenderer.updateHostNode(reconciler, node, newElement) + + expect(instance.Value).to.equal(99) + + updateA(123) + + expect(instance.Value).to.equal(99) + + updateB(123) + + expect(instance.Value).to.equal(123) + + RobloxRenderer.unmountHostNode(reconciler, node) + end) + + it("should update Binding refs", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local refA = createRef() + local refB = createRef() + + local element = createElement("Frame", { + [Ref] = refA, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local instance = parent:GetChildren()[1] + + expect(refA.current).to.equal(instance) + expect(refB.current).never.to.be.ok() + + local newElement = createElement("Frame", { + [Ref] = refB, + }) + + RobloxRenderer.updateHostNode(reconciler, node, newElement) + + expect(refA.current).never.to.be.ok() + expect(refB.current).to.equal(instance) + + RobloxRenderer.unmountHostNode(reconciler, node) + end) + + it("should call old function refs with nil and new function refs with a valid rbx", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local spyRefA = createSpy() + local spyRefB = createSpy() + + local element = createElement("Frame", { + [Ref] = spyRefA.value, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local instance = parent:GetChildren()[1] + + expect(spyRefA.callCount).to.equal(1) + spyRefA:assertCalledWith(instance) + expect(spyRefB.callCount).to.equal(0) + + local newElement = createElement("Frame", { + [Ref] = spyRefB.value, + }) + + RobloxRenderer.updateHostNode(reconciler, node, newElement) + + expect(spyRefA.callCount).to.equal(2) + spyRefA:assertCalledWith(nil) + expect(spyRefB.callCount).to.equal(1) + spyRefB:assertCalledWith(instance) + + RobloxRenderer.unmountHostNode(reconciler, node) + end) + + it("should not call function refs again if they didn't change", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local spyRef = createSpy() + + local element = createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + [Ref] = spyRef.value, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local instance = parent:GetChildren()[1] + + expect(spyRef.callCount).to.equal(1) + spyRef:assertCalledWith(instance) + + local newElement = createElement("Frame", { + Size = UDim2.new(0.5, 0, 0.5, 0), + [Ref] = spyRef.value, + }) + + RobloxRenderer.updateHostNode(reconciler, node, newElement) + + -- Not called again + expect(spyRef.callCount).to.equal(1) + end) + + it("should throw if setting invalid instance properties", function() + local configValues = { + elementTracing = true, + } + + GlobalConfig.scoped(configValues, function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local firstElement = createElement("Frame") + local secondElement = createElement("Frame", { + Frob = 6, + }) + + local node = reconciler.createVirtualNode(firstElement, parent, key) + RobloxRenderer.mountHostNode(reconciler, node) + + local success, message = pcall(RobloxRenderer.updateHostNode, reconciler, node, secondElement) + assert(not success, "Expected call to fail") + + expect(message:find("Frob")).to.be.ok() + expect(message:find("Frame")).to.be.ok() + expect(message:find("RobloxRenderer%.spec")).to.be.ok() + end) + end) + + it("should delete instances when reconciling to nil children", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local element = createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + child = createElement("Frame"), + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local instance = parent:GetChildren()[1] + expect(#instance:GetChildren()).to.equal(1) + + local newElement = createElement("Frame", { + Size = UDim2.new(0.5, 0, 0.5, 0), + }) + + RobloxRenderer.updateHostNode(reconciler, node, newElement) + expect(#instance:GetChildren()).to.equal(0) + end) + end) + + describe("unmountHostNode", function() + it("should delete instances from the inside-out", function() + local parent = Instance.new("Folder") + local key = "Root" + local element = createElement("Folder", nil, { + Child = createElement("Folder", nil, { + Grandchild = createElement("Folder"), + }), + }) + + local node = reconciler.mountVirtualNode(element, parent, key) + + expect(#parent:GetChildren()).to.equal(1) + + local root = parent:GetChildren()[1] + expect(#root:GetChildren()).to.equal(1) + + local child = root:GetChildren()[1] + expect(#child:GetChildren()).to.equal(1) + + local grandchild = child:GetChildren()[1] + + RobloxRenderer.unmountHostNode(reconciler, node) + + expect(grandchild.Parent).to.equal(nil) + expect(child.Parent).to.equal(nil) + expect(root.Parent).to.equal(nil) + end) + + it("should unsubscribe from any Bindings", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local binding, update = Binding.create(10) + local element = createElement("IntValue", { + Value = binding, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + local instance = parent:GetChildren()[1] + + expect(instance.Value).to.equal(10) + + RobloxRenderer.unmountHostNode(reconciler, node) + update(56) + + expect(instance.Value).to.equal(10) + end) + + it("should clear Binding refs", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local ref = createRef() + local element = createElement("Frame", { + [Ref] = ref, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(ref.current).to.be.ok() + + RobloxRenderer.unmountHostNode(reconciler, node) + + expect(ref.current).never.to.be.ok() + end) + + it("should call function refs with nil", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local spyRef = createSpy() + local element = createElement("Frame", { + [Ref] = spyRef.value, + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(spyRef.callCount).to.equal(1) + + RobloxRenderer.unmountHostNode(reconciler, node) + + expect(spyRef.callCount).to.equal(2) + spyRef:assertCalledWith(nil) + end) + end) + + describe("Portals", function() + it("should create and destroy instances as children of `target`", function() + local target = Instance.new("Folder") + + local function FunctionComponent(props) + return createElement("IntValue", { + Value = props.value, + }) + end + + local element = createElement(Portal, { + target = target, + }, { + folderOne = createElement("Folder"), + folderTwo = createElement("Folder"), + intValueOne = createElement(FunctionComponent, { + value = 42, + }), + }) + local hostParent = nil + local hostKey = "Some Key" + local node = reconciler.mountVirtualNode(element, hostParent, hostKey) + + expect(#target:GetChildren()).to.equal(3) + + expect(target:FindFirstChild("folderOne")).to.be.ok() + expect(target:FindFirstChild("folderTwo")).to.be.ok() + expect(target:FindFirstChild("intValueOne")).to.be.ok() + expect(target:FindFirstChild("intValueOne").Value).to.equal(42) + + reconciler.unmountVirtualNode(node) + + expect(#target:GetChildren()).to.equal(0) + end) + + it("should pass prop updates through to children", function() + local target = Instance.new("Folder") + + local firstElement = createElement(Portal, { + target = target, + }, { + ChildValue = createElement("IntValue", { + Value = 1, + }), + }) + + local secondElement = createElement(Portal, { + target = target, + }, { + ChildValue = createElement("IntValue", { + Value = 2, + }), + }) + + local hostParent = nil + local hostKey = "A Host Key" + local node = reconciler.mountVirtualNode(firstElement, hostParent, hostKey) + + expect(#target:GetChildren()).to.equal(1) + + local firstValue = target.ChildValue + expect(firstValue.Value).to.equal(1) + + node = reconciler.updateVirtualNode(node, secondElement) + + expect(#target:GetChildren()).to.equal(1) + + local secondValue = target.ChildValue + expect(firstValue).to.equal(secondValue) + expect(secondValue.Value).to.equal(2) + + reconciler.unmountVirtualNode(node) + + expect(#target:GetChildren()).to.equal(0) + end) + + it("should throw if `target` is nil", function() + -- TODO: Relax this restriction? + local element = createElement(Portal) + local hostParent = nil + local hostKey = "Keys for Everyone" + + expect(function() + reconciler.mountVirtualNode(element, hostParent, hostKey) + end).to.throw() + end) + + it("should throw if `target` is not a Roblox instance", function() + local element = createElement(Portal, { + target = {}, + }) + local hostParent = nil + local hostKey = "Unleash the keys!" + + expect(function() + reconciler.mountVirtualNode(element, hostParent, hostKey) + end).to.throw() + end) + + it("should recreate instances if `target` changes in an update", function() + local firstTarget = Instance.new("Folder") + local secondTarget = Instance.new("Folder") + + local firstElement = createElement(Portal, { + target = firstTarget, + }, { + ChildValue = createElement("IntValue", { + Value = 1, + }), + }) + + local secondElement = createElement(Portal, { + target = secondTarget, + }, { + ChildValue = createElement("IntValue", { + Value = 2, + }), + }) + + local hostParent = nil + local hostKey = "Some Key" + local node = reconciler.mountVirtualNode(firstElement, hostParent, hostKey) + + expect(#firstTarget:GetChildren()).to.equal(1) + expect(#secondTarget:GetChildren()).to.equal(0) + + local firstChild = firstTarget.ChildValue + expect(firstChild.Value).to.equal(1) + + node = reconciler.updateVirtualNode(node, secondElement) + + expect(#firstTarget:GetChildren()).to.equal(0) + expect(#secondTarget:GetChildren()).to.equal(1) + + local secondChild = secondTarget.ChildValue + expect(secondChild.Value).to.equal(2) + + reconciler.unmountVirtualNode(node) + + expect(#firstTarget:GetChildren()).to.equal(0) + expect(#secondTarget:GetChildren()).to.equal(0) + end) + end) + + describe("Fragments", function() + it("should parent the fragment's elements into the fragment's parent", function() + local hostParent = Instance.new("Folder") + + local fragment = createFragment({ + key = createElement("IntValue", { + Value = 1, + }), + key2 = createElement("IntValue", { + Value = 2, + }), + }) + + local node = reconciler.mountVirtualNode(fragment, hostParent, "test") + + expect(hostParent:FindFirstChild("key")).to.be.ok() + expect(hostParent.key.ClassName).to.equal("IntValue") + expect(hostParent.key.Value).to.equal(1) + + expect(hostParent:FindFirstChild("key2")).to.be.ok() + expect(hostParent.key2.ClassName).to.equal("IntValue") + expect(hostParent.key2.Value).to.equal(2) + + reconciler.unmountVirtualNode(node) + + expect(#hostParent:GetChildren()).to.equal(0) + end) + + it("should allow sibling fragment to have common keys", function() + local hostParent = Instance.new("Folder") + local hostKey = "Test" + + local function parent(props) + return createElement("IntValue", {}, { + fragmentA = createFragment({ + key = createElement("StringValue", { + Value = "A", + }), + key2 = createElement("StringValue", { + Value = "B", + }), + }), + fragmentB = createFragment({ + key = createElement("StringValue", { + Value = "C", + }), + key2 = createElement("StringValue", { + Value = "D", + }), + }), + }) + end + + local node = reconciler.mountVirtualNode(createElement(parent), hostParent, hostKey) + local parentChildren = hostParent[hostKey]:GetChildren() + + expect(#parentChildren).to.equal(4) + + local childValues = {} + + for _, child in pairs(parentChildren) do + expect(child.ClassName).to.equal("StringValue") + childValues[child.Value] = 1 + (childValues[child.Value] or 0) + end + + -- check if the StringValues have not collided + expect(childValues.A).to.equal(1) + expect(childValues.B).to.equal(1) + expect(childValues.C).to.equal(1) + expect(childValues.D).to.equal(1) + + reconciler.unmountVirtualNode(node) + + expect(#hostParent:GetChildren()).to.equal(0) + end) + + it("should render nested fragments", function() + local hostParent = Instance.new("Folder") + + local fragment = createFragment({ + key = createFragment({ + TheValue = createElement("IntValue", { + Value = 1, + }), + TheOtherValue = createElement("IntValue", { + Value = 2, + }) + }) + }) + + local node = reconciler.mountVirtualNode(fragment, hostParent, "Test") + + expect(hostParent:FindFirstChild("TheValue")).to.be.ok() + expect(hostParent.TheValue.ClassName).to.equal("IntValue") + expect(hostParent.TheValue.Value).to.equal(1) + + expect(hostParent:FindFirstChild("TheOtherValue")).to.be.ok() + expect(hostParent.TheOtherValue.ClassName).to.equal("IntValue") + expect(hostParent.TheOtherValue.Value).to.equal(2) + + reconciler.unmountVirtualNode(node) + + expect(#hostParent:GetChildren()).to.equal(0) + end) + + it("should not add any instances if the fragment is empty", function() + local hostParent = Instance.new("Folder") + + local node = reconciler.mountVirtualNode(createFragment({}), hostParent, "test") + + expect(#hostParent:GetChildren()).to.equal(0) + + reconciler.unmountVirtualNode(node) + + expect(#hostParent:GetChildren()).to.equal(0) + end) + end) + + describe("Context", function() + it("should pass context values through Roblox host nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + hello = self:__getContext("hello") + } + end + + function Consumer:render() + end + + local element = createElement("Folder", nil, { + Consumer = createElement(Consumer) + }) + local hostParent = nil + local hostKey = "Context Test" + local context = { + hello = "world", + } + local node = reconciler.mountVirtualNode(element, hostParent, hostKey, context) + + expect(capturedContext).never.to.equal(context) + assertDeepEqual(capturedContext, context) + + reconciler.unmountVirtualNode(node) + end) + + it("should pass context values through portal nodes", function() + local target = Instance.new("Folder") + + local Provider = Component:extend("Provider") + + function Provider:init() + self:__addContext("foo", "bar") + end + + function Provider:render() + return createElement("Folder", nil, self.props[Children]) + end + + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + foo = self:__getContext("foo"), + } + end + + function Consumer:render() + return nil + end + + local element = createElement(Provider, nil, { + Portal = createElement(Portal, { + target = target, + }, { + Consumer = createElement(Consumer), + }) + }) + local hostParent = nil + local hostKey = "Some Key" + reconciler.mountVirtualNode(element, hostParent, hostKey) + + assertDeepEqual(capturedContext, { + foo = "bar" + }) + end) + end) + + describe("Legacy context", function() + it("should pass context values through Roblox host nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = self._context + end + + function Consumer:render() + end + + local element = createElement("Folder", nil, { + Consumer = createElement(Consumer) + }) + local hostParent = nil + local hostKey = "Context Test" + local context = { + hello = "world", + } + local node = reconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) + + expect(capturedContext).never.to.equal(context) + assertDeepEqual(capturedContext, context) + + reconciler.unmountVirtualNode(node) + end) + + it("should pass context values through portal nodes", function() + local target = Instance.new("Folder") + + local Provider = Component:extend("Provider") + + function Provider:init() + self._context.foo = "bar" + end + + function Provider:render() + return createElement("Folder", nil, self.props[Children]) + end + + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = self._context + end + + function Consumer:render() + return nil + end + + local element = createElement(Provider, nil, { + Portal = createElement(Portal, { + target = target, + }, { + Consumer = createElement(Consumer), + }) + }) + local hostParent = nil + local hostKey = "Some Key" + reconciler.mountVirtualNode(element, hostParent, hostKey) + + assertDeepEqual(capturedContext, { + foo = "bar" + }) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/SingleEventManager.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/SingleEventManager.lua new file mode 100644 index 0000000..bb579c7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/SingleEventManager.lua @@ -0,0 +1,147 @@ +--[[ + A manager for a single host virtual node's connected events. +]] + +local Logging = require(script.Parent.Logging) + +local CHANGE_PREFIX = "Change." + +local EventStatus = { + -- No events are processed at all; they're silently discarded + Disabled = "Disabled", + + -- Events are stored in a queue; listeners are invoked when the manager is resumed + Suspended = "Suspended", + + -- Event listeners are invoked as the events fire + Enabled = "Enabled", +} + +local SingleEventManager = {} +SingleEventManager.__index = SingleEventManager + +function SingleEventManager.new(instance) + local self = setmetatable({ + -- The queue of suspended events + _suspendedEventQueue = {}, + + -- All the event connections being managed + -- Events are indexed by a string key + _connections = {}, + + -- All the listeners being managed + -- These are stored distinctly from the connections + -- Connections can have their listeners replaced at runtime + _listeners = {}, + + -- The suspension status of the manager + -- Managers start disabled and are "resumed" after the initial render + _status = EventStatus.Disabled, + + -- If true, the manager is processing queued events right now. + _isResuming = false, + + -- The Roblox instance the manager is managing + _instance = instance, + }, SingleEventManager) + + return self +end + +function SingleEventManager:connectEvent(key, listener) + self:_connect(key, self._instance[key], listener) +end + +function SingleEventManager:connectPropertyChange(key, listener) + local success, event = pcall(function() + return self._instance:GetPropertyChangedSignal(key) + end) + + if not success then + error(("Cannot get changed signal on property %q: %s"):format( + tostring(key), + event + ), 0) + end + + self:_connect(CHANGE_PREFIX .. key, event, listener) +end + +function SingleEventManager:_connect(eventKey, event, listener) + -- If the listener doesn't exist we can just disconnect the existing connection + if listener == nil then + if self._connections[eventKey] ~= nil then + self._connections[eventKey]:Disconnect() + self._connections[eventKey] = nil + end + + self._listeners[eventKey] = nil + else + if self._connections[eventKey] == nil then + self._connections[eventKey] = event:Connect(function(...) + if self._status == EventStatus.Enabled then + self._listeners[eventKey](self._instance, ...) + elseif self._status == EventStatus.Suspended then + -- Store this event invocation to be fired when resume is + -- called. + + local argumentCount = select("#", ...) + table.insert(self._suspendedEventQueue, { eventKey, argumentCount, ... }) + end + end) + end + + self._listeners[eventKey] = listener + end +end + +function SingleEventManager:suspend() + self._status = EventStatus.Suspended +end + +function SingleEventManager:resume() + -- If we're already resuming events for this instance, trying to resume + -- again would cause a disaster. + if self._isResuming then + return + end + + self._isResuming = true + + local index = 1 + + -- More events might be added to the queue when evaluating events, so we + -- need to be careful in order to preserve correct evaluation order. + while index <= #self._suspendedEventQueue do + local eventInvocation = self._suspendedEventQueue[index] + local listener = self._listeners[eventInvocation[1]] + local argumentCount = eventInvocation[2] + + -- The event might have been disconnected since suspension started; in + -- this case, we drop the event. + if listener ~= nil then + -- Wrap the listener in a coroutine to catch errors and handle + -- yielding correctly. + local listenerCo = coroutine.create(listener) + local success, result = coroutine.resume( + listenerCo, + self._instance, + unpack(eventInvocation, 3, 2 + argumentCount)) + + -- If the listener threw an error, we log it as a warning, since + -- there's no way to write error text in Roblox Lua without killing + -- our thread! + if not success then + Logging.warn("%s", result) + end + end + + index = index + 1 + end + + self._isResuming = false + self._status = EventStatus.Enabled + self._suspendedEventQueue = {} +end + +return SingleEventManager \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/SingleEventManager.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/SingleEventManager.spec.lua new file mode 100644 index 0000000..9d87e27 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/SingleEventManager.spec.lua @@ -0,0 +1,239 @@ +return function() + local assertDeepEqual = require(script.Parent.assertDeepEqual) + local createSpy = require(script.Parent.createSpy) + local Logging = require(script.Parent.Logging) + + local SingleEventManager = require(script.Parent.SingleEventManager) + + describe("new", function() + it("should create a SingleEventManager", function() + local manager = SingleEventManager.new() + + expect(manager).to.be.ok() + end) + end) + + describe("connectEvent", function() + it("should connect to events", function() + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + local eventSpy = createSpy() + + manager:connectEvent("Event", eventSpy.value) + manager:resume() + + instance:Fire("foo") + expect(eventSpy.callCount).to.equal(1) + eventSpy:assertCalledWith(instance, "foo") + + instance:Fire("bar") + expect(eventSpy.callCount).to.equal(2) + eventSpy:assertCalledWith(instance, "bar") + + manager:connectEvent("Event", nil) + + instance:Fire("baz") + expect(eventSpy.callCount).to.equal(2) + end) + + it("should drop events until resumed initially", function() + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + local eventSpy = createSpy() + + manager:connectEvent("Event", eventSpy.value) + + instance:Fire("foo") + expect(eventSpy.callCount).to.equal(0) + + manager:resume() + + instance:Fire("bar") + expect(eventSpy.callCount).to.equal(1) + eventSpy:assertCalledWith(instance, "bar") + end) + + it("should invoke suspended events when resumed", function() + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + local eventSpy = createSpy() + + manager:connectEvent("Event", eventSpy.value) + manager:resume() + + instance:Fire("foo") + expect(eventSpy.callCount).to.equal(1) + eventSpy:assertCalledWith(instance, "foo") + + manager:suspend() + + instance:Fire("bar") + expect(eventSpy.callCount).to.equal(1) + + manager:resume() + expect(eventSpy.callCount).to.equal(2) + eventSpy:assertCalledWith(instance, "bar") + end) + + it("should invoke events triggered during resumption in the correct order", function() + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + + local recordedValues = {} + local eventSpy = createSpy(function(_, value) + table.insert(recordedValues, value) + + if value == 2 then + instance:Fire(3) + elseif value == 3 then + instance:Fire(4) + end + end) + + manager:connectEvent("Event", eventSpy.value) + manager:suspend() + + instance:Fire(1) + instance:Fire(2) + + manager:resume() + expect(eventSpy.callCount).to.equal(4) + assertDeepEqual(recordedValues, {1, 2, 3, 4}) + end) + + it("should not invoke events fired during suspension but disconnected before resumption", function() + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + local eventSpy = createSpy() + + manager:connectEvent("Event", eventSpy.value) + manager:suspend() + + instance:Fire(1) + + manager:connectEvent("Event", nil) + + manager:resume() + expect(eventSpy.callCount).to.equal(0) + end) + + it("should not yield events through the SingleEventManager when resuming", function() + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + + manager:connectEvent("Event", function() + coroutine.yield() + end) + + manager:resume() + + local co = coroutine.create(function() + instance:Fire(5) + end) + + assert(coroutine.resume(co)) + expect(coroutine.status(co)).to.equal("dead") + + manager:suspend() + instance:Fire(5) + + co = coroutine.create(function() + manager:resume() + end) + + assert(coroutine.resume(co)) + expect(coroutine.status(co)).to.equal("dead") + end) + + it("should not throw errors through SingleEventManager when resuming", function() + local errorText = "Error from SingleEventManager test" + + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + + manager:connectEvent("Event", function() + error(errorText) + end) + + manager:resume() + + -- If we call instance:Fire() here, the error message will leak to + -- the console since the thread's resumption will be handled by + -- Roblox's scheduler. + + manager:suspend() + instance:Fire(5) + + local logInfo = Logging.capture(function() + manager:resume() + end) + + expect(#logInfo.errors).to.equal(0) + expect(#logInfo.warnings).to.equal(1) + expect(#logInfo.infos).to.equal(0) + + expect(logInfo.warnings[1]:find(errorText)).to.be.ok() + end) + + it("should not overflow with events if manager:resume() is invoked when resuming a suspended event", function() + local instance = Instance.new("BindableEvent") + local manager = SingleEventManager.new(instance) + + -- This connection emulates what happens if reconciliation is + -- triggered again in response to reconciliation. Without + -- appropriate guards, the inner resume() call will process the + -- Fire(1) event again, causing a nasty stack overflow. + local eventSpy = createSpy(function(_, value) + if value == 1 then + manager:suspend() + instance:Fire(2) + manager:resume() + end + end) + + manager:connectEvent("Event", eventSpy.value) + + manager:suspend() + instance:Fire(1) + manager:resume() + + expect(eventSpy.callCount).to.equal(2) + end) + end) + + describe("connectPropertyChange", function() + -- Since property changes utilize the same mechanisms as other events, + -- the tests here are slimmed down to reduce redundancy. + + it("should connect to property changes", function() + local instance = Instance.new("Folder") + local manager = SingleEventManager.new(instance) + local eventSpy = createSpy() + + manager:connectPropertyChange("Name", eventSpy.value) + manager:resume() + + instance.Name = "foo" + expect(eventSpy.callCount).to.equal(1) + eventSpy:assertCalledWith(instance) + + instance.Name = "bar" + expect(eventSpy.callCount).to.equal(2) + eventSpy:assertCalledWith(instance) + + manager:connectPropertyChange("Name") + + instance.Name = "baz" + expect(eventSpy.callCount).to.equal(2) + end) + + it("should throw an error if the property is invalid", function() + local instance = Instance.new("Folder") + local manager = SingleEventManager.new(instance) + + expect(function() + manager:connectPropertyChange("foo", function() end) + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Symbol.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Symbol.lua new file mode 100644 index 0000000..305d66a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Symbol.lua @@ -0,0 +1,30 @@ +--[[ + A 'Symbol' is an opaque marker type. + + Symbols have the type 'userdata', but when printed to the console, the name + of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Symbol.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Symbol.spec.lua new file mode 100644 index 0000000..e05061d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Symbol.spec.lua @@ -0,0 +1,24 @@ +return function() + local Symbol = require(script.Parent.Symbol) + + describe("named", function() + it("should give an opaque object", function() + local symbol = Symbol.named("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = Symbol.named("foo") + + expect(tostring(symbol):find("foo")).to.be.ok() + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.named("abc") + local symbolB = Symbol.named("abc") + + expect(symbolA).never.to.equal(symbolB) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Type.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Type.lua new file mode 100644 index 0000000..156ee0e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Type.lua @@ -0,0 +1,48 @@ +--[[ + Contains markers for annotating objects with types. + + To set the type of an object, use `Type` as a key and the actual marker as + the value: + + local foo = { + [Type] = Type.Foo, + } +]] + +local Symbol = require(script.Parent.Symbol) +local strict = require(script.Parent.strict) + +local Type = newproxy(true) + +local TypeInternal = {} + +local function addType(name) + TypeInternal[name] = Symbol.named("Roact" .. name) +end + +addType("Binding") +addType("Element") +addType("HostChangeEvent") +addType("HostEvent") +addType("StatefulComponentClass") +addType("StatefulComponentInstance") +addType("VirtualNode") +addType("VirtualTree") + +function TypeInternal.of(value) + if typeof(value) ~= "table" then + return nil + end + + return value[Type] +end + +getmetatable(Type).__index = TypeInternal + +getmetatable(Type).__tostring = function() + return "RoactType" +end + +strict(TypeInternal, "Type") + +return Type \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Type.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Type.spec.lua new file mode 100644 index 0000000..f247709 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/Type.spec.lua @@ -0,0 +1,24 @@ +return function() + local Type = require(script.Parent.Type) + + describe("of", function() + it("should return nil if the value is not a table", function() + expect(Type.of(1)).to.equal(nil) + expect(Type.of(true)).to.equal(nil) + expect(Type.of("test")).to.equal(nil) + expect(Type.of(print)).to.equal(nil) + end) + + it("should return nil if the table has no type", function() + expect(Type.of({})).to.equal(nil) + end) + + it("should return the assigned type", function() + local test = { + [Type] = Type.Element + } + + expect(Type.of(test)).to.equal(Type.Element) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assertDeepEqual.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assertDeepEqual.lua new file mode 100644 index 0000000..3f422d8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assertDeepEqual.lua @@ -0,0 +1,73 @@ +--[[ + A utility used to assert that two objects are value-equal recursively. It + outputs fairly nicely formatted messages to help diagnose why two objects + would be different. + + This should only be used in tests. +]] + +local function deepEqual(a, b) + if typeof(a) ~= typeof(b) then + local message = ("{1} is of type %s, but {2} is of type %s"):format( + typeof(a), + typeof(b) + ) + return false, message + end + + if typeof(a) == "table" then + local visitedKeys = {} + + for key, value in pairs(a) do + visitedKeys[key] = true + + local success, innerMessage = deepEqual(value, b[key]) + if not success then + local message = innerMessage + :gsub("{1}", ("{1}[%s]"):format(tostring(key))) + :gsub("{2}", ("{2}[%s]"):format(tostring(key))) + + return false, message + end + end + + for key, value in pairs(b) do + if not visitedKeys[key] then + local success, innerMessage = deepEqual(value, a[key]) + + if not success then + local message = innerMessage + :gsub("{1}", ("{1}[%s]"):format(tostring(key))) + :gsub("{2}", ("{2}[%s]"):format(tostring(key))) + + return false, message + end + end + end + + return true + end + + if a == b then + return true + end + + local message = "{1} ~= {2}" + return false, message +end + +local function assertDeepEqual(a, b) + local success, innerMessageTemplate = deepEqual(a, b) + + if not success then + local innerMessage = innerMessageTemplate + :gsub("{1}", "first") + :gsub("{2}", "second") + + local message = ("Values were not deep-equal.\n%s"):format(innerMessage) + + error(message, 2) + end +end + +return assertDeepEqual \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assertDeepEqual.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assertDeepEqual.spec.lua new file mode 100644 index 0000000..bece8d7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assertDeepEqual.spec.lua @@ -0,0 +1,99 @@ +return function() + local assertDeepEqual = require(script.Parent.assertDeepEqual) + + it("should fail with a message when args are not equal", function() + local success, message = pcall(assertDeepEqual, 1, 2) + + expect(success).to.equal(false) + expect(message:find("first ~= second")).to.be.ok() + + success, message = pcall(assertDeepEqual, { + foo = 1, + }, { + foo = 2, + }) + + expect(success).to.equal(false) + expect(message:find("first%[foo%] ~= second%[foo%]")).to.be.ok() + end) + + it("should compare non-table values using standard '==' equality", function() + assertDeepEqual(1, 1) + assertDeepEqual("hello", "hello") + assertDeepEqual(nil, nil) + + local someFunction = function() end + local theSameFunction = someFunction + + assertDeepEqual(someFunction, theSameFunction) + + local A = { + foo = someFunction + } + local B = { + foo = theSameFunction + } + + assertDeepEqual(A, B) + end) + + it("should fail when types differ", function() + local success, message = pcall(assertDeepEqual, 1, "1") + + expect(success).to.equal(false) + expect(message:find("first is of type number, but second is of type string")).to.be.ok() + end) + + it("should compare (and report about) nested tables", function() + local A = { + foo = "bar", + nested = { + foo = 1, + bar = 2, + } + } + local B = { + foo = "bar", + nested = { + foo = 1, + bar = 2, + } + } + + assertDeepEqual(A, B) + + local C = { + foo = "bar", + nested = { + foo = 1, + bar = 3, + } + } + + local success, message = pcall(assertDeepEqual, A, C) + + expect(success).to.equal(false) + expect(message:find("first%[nested%]%[bar%] ~= second%[nested%]%[bar%]")).to.be.ok() + end) + + it("should be commutative", function() + local equalArgsA = { + foo = "bar", + hello = "world", + } + local equalArgsB = { + foo = "bar", + hello = "world", + } + + assertDeepEqual(equalArgsA, equalArgsB) + assertDeepEqual(equalArgsB, equalArgsA) + + local nonEqualArgs = { + foo = "bar", + } + + expect(function() assertDeepEqual(equalArgsA, nonEqualArgs) end).to.throw() + expect(function() assertDeepEqual(nonEqualArgs, equalArgsA) end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assign.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assign.lua new file mode 100644 index 0000000..704c165 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assign.lua @@ -0,0 +1,27 @@ +local None = require(script.Parent.None) + +--[[ + Merges values from zero or more tables onto a target table. If a value is + set to None, it will instead be removed from the table. + + This function is identical in functionality to JavaScript's Object.assign. +]] +local function assign(target, ...) + for index = 1, select("#", ...) do + local source = select(index, ...) + + if source ~= nil then + for key, value in pairs(source) do + if value == None then + target[key] = nil + else + target[key] = value + end + end + end + end + + return target +end + +return assign \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assign.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assign.spec.lua new file mode 100644 index 0000000..24784a1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/assign.spec.lua @@ -0,0 +1,68 @@ +return function() + local None = require(script.Parent.None) + + local assign = require(script.Parent.assign) + + it("should accept zero additional tables", function() + local input = {} + local result = assign(input) + + expect(input).to.equal(result) + end) + + it("should merge multiple tables onto the given target table", function() + local target = { + a = 5, + b = 6, + } + + local source1 = { + b = 7, + c = 8, + } + + local source2 = { + b = 8, + } + + assign(target, source1, source2) + + expect(target.a).to.equal(5) + expect(target.b).to.equal(source2.b) + expect(target.c).to.equal(source1.c) + end) + + it("should remove keys if specified as None", function() + local target = { + foo = 2, + bar = 3, + } + + local source = { + foo = None, + } + + assign(target, source) + + expect(target.foo).to.equal(nil) + expect(target.bar).to.equal(3) + end) + + it("should re-add keys if specified after None", function() + local target = { + foo = 2, + } + + local source1 = { + foo = None, + } + + local source2 = { + foo = 3, + } + + assign(target, source1, source2) + + expect(target.foo).to.equal(source2.foo) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createContext.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createContext.lua new file mode 100644 index 0000000..b21635e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createContext.lua @@ -0,0 +1,151 @@ +local Symbol = require(script.Parent.Symbol) +local createFragment = require(script.Parent.createFragment) +local createSignal = require(script.Parent.createSignal) +local Children = require(script.Parent.PropMarkers.Children) +local Component = require(script.Parent.Component) + +--[[ + Construct the value that is assigned to Roact's context storage. +]] +local function createContextEntry(currentValue) + return { + value = currentValue, + onUpdate = createSignal(), + } +end + +local function createProvider(context) + local Provider = Component:extend("Provider") + + function Provider:init(props) + self.contextEntry = createContextEntry(props.value) + self:__addContext(context.key, self.contextEntry) + end + + function Provider:willUpdate(nextProps) + -- If the provided value changed, immediately update the context entry. + -- + -- During this update, any components that are reachable will receive + -- this updated value at the same time as any props and state updates + -- that are being applied. + if nextProps.value ~= self.props.value then + self.contextEntry.value = nextProps.value + end + end + + function Provider:didUpdate(prevProps) + -- If the provided value changed, after we've updated every reachable + -- component, fire a signal to update the rest. + -- + -- This signal will notify all context consumers. It's expected that + -- they will compare the last context value they updated with and only + -- trigger an update on themselves if this value is different. + -- + -- This codepath will generally only update consumer components that has + -- a component implementing shouldUpdate between them and the provider. + if prevProps.value ~= self.props.value then + self.contextEntry.onUpdate:fire(self.props.value) + end + end + + function Provider:render() + return createFragment(self.props[Children]) + end + + return Provider +end + +local function createConsumer(context) + local Consumer = Component:extend("Consumer") + + function Consumer.validateProps(props) + if type(props.render) ~= "function" then + return false, "Consumer expects a `render` function" + else + return true + end + end + + function Consumer:init(props) + -- This value may be nil, which indicates that our consumer is not a + -- descendant of a provider for this context item. + self.contextEntry = self:__getContext(context.key) + end + + function Consumer:render() + -- Render using the latest available for this context item. + -- + -- We don't store this value in state in order to have more fine-grained + -- control over our update behavior. + local value + if self.contextEntry ~= nil then + value = self.contextEntry.value + else + value = context.defaultValue + end + + return self.props.render(value) + end + + function Consumer:didUpdate() + -- Store the value that we most recently updated with. + -- + -- This value is compared in the contextEntry onUpdate hook below. + if self.contextEntry ~= nil then + self.lastValue = self.contextEntry.value + end + end + + function Consumer:didMount() + if self.contextEntry ~= nil then + -- When onUpdate is fired, a new value has been made available in + -- this context entry, but we may have already updated in the same + -- update cycle. + -- + -- To avoid sending a redundant update, we compare the new value + -- with the last value that we updated with (set in didUpdate) and + -- only update if they differ. This may happen when an update from a + -- provider was blocked by an intermediate component that returned + -- false from shouldUpdate. + self.disconnect = self.contextEntry.onUpdate:subscribe(function(newValue) + if newValue ~= self.lastValue then + -- Trigger a dummy state update. + self:setState({}) + end + end) + end + end + + function Consumer:willUnmount() + if self.disconnect ~= nil then + self.disconnect() + end + end + + return Consumer +end + +local Context = {} +Context.__index = Context + +function Context.new(defaultValue) + return setmetatable({ + defaultValue = defaultValue, + key = Symbol.named("ContextKey"), + }, Context) +end + +function Context:__tostring() + return "RoactContext" +end + +local function createContext(defaultValue) + local context = Context.new(defaultValue) + + return { + Provider = createProvider(context), + Consumer = createConsumer(context), + } +end + +return createContext diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createContext.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createContext.spec.lua new file mode 100644 index 0000000..432d39d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createContext.spec.lua @@ -0,0 +1,304 @@ +return function() + local Component = require(script.Parent.Component) + local NoopRenderer = require(script.Parent.NoopRenderer) + local Children = require(script.Parent.PropMarkers.Children) + local createContext = require(script.Parent.createContext) + local createElement = require(script.Parent.createElement) + local createFragment = require(script.Parent.createFragment) + local createReconciler = require(script.Parent.createReconciler) + local createSpy = require(script.Parent.createSpy) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should return a table", function() + local context = createContext("Test") + expect(context).to.be.ok() + expect(type(context)).to.equal("table") + end) + + it("should contain a Provider and a Consumer", function() + local context = createContext("Test") + expect(context.Provider).to.be.ok() + expect(context.Consumer).to.be.ok() + end) + + describe("Provider", function() + it("should render its children", function() + local context = createContext("Test") + + local Listener = createSpy(function() + return nil + end) + + local element = createElement(context.Provider, { + value = "Test", + }, { + Listener = createElement(Listener.value), + }) + + local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + noopReconciler.unmountVirtualTree(tree) + + expect(Listener.callCount).to.equal(1) + end) + end) + + describe("Consumer", function() + it("should expect a render function", function() + local context = createContext("Test") + local element = createElement(context.Consumer) + + expect(function() + noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + end).to.throw() + end) + + it("should return the default value if there is no Provider", function() + local valueSpy = createSpy() + local context = createContext("Test") + + local element = createElement(context.Consumer, { + render = valueSpy.value, + }) + + local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + noopReconciler.unmountVirtualTree(tree) + + valueSpy:assertCalledWith("Test") + end) + + it("should pass the value to the render function", function() + local valueSpy = createSpy() + local context = createContext("Test") + + local function Listener() + return createElement(context.Consumer, { + render = valueSpy.value, + }) + end + + local element = createElement(context.Provider, { + value = "NewTest", + }, { + Listener = createElement(Listener), + }) + + local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + noopReconciler.unmountVirtualTree(tree) + + valueSpy:assertCalledWith("NewTest") + end) + + it("should update when the value updates", function() + local valueSpy = createSpy() + local context = createContext("Test") + + local function Listener() + return createElement(context.Consumer, { + render = valueSpy.value, + }) + end + + local element = createElement(context.Provider, { + value = "NewTest", + }, { + Listener = createElement(Listener), + }) + + local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + + expect(valueSpy.callCount).to.equal(1) + valueSpy:assertCalledWith("NewTest") + + noopReconciler.updateVirtualTree(tree, createElement(context.Provider, { + value = "ThirdTest", + }, { + Listener = createElement(Listener), + })) + + expect(valueSpy.callCount).to.equal(2) + valueSpy:assertCalledWith("ThirdTest") + + noopReconciler.unmountVirtualTree(tree) + end) + + --[[ + This test is the same as the one above, but with a component that + always blocks updates in the middle. We expect behavior to be the + same. + ]] + it("should update when the value updates through an update blocking component", function() + local valueSpy = createSpy() + local context = createContext("Test") + + local UpdateBlocker = Component:extend("UpdateBlocker") + + function UpdateBlocker:render() + return createFragment(self.props[Children]) + end + + function UpdateBlocker:shouldUpdate() + return false + end + + local function Listener() + return createElement(context.Consumer, { + render = valueSpy.value, + }) + end + + local element = createElement(context.Provider, { + value = "NewTest", + }, { + Blocker = createElement(UpdateBlocker, nil, { + Listener = createElement(Listener), + }), + }) + + local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + + expect(valueSpy.callCount).to.equal(1) + valueSpy:assertCalledWith("NewTest") + + noopReconciler.updateVirtualTree(tree, createElement(context.Provider, { + value = "ThirdTest", + }, { + Blocker = createElement(UpdateBlocker, nil, { + Listener = createElement(Listener), + }), + })) + + expect(valueSpy.callCount).to.equal(2) + valueSpy:assertCalledWith("ThirdTest") + + noopReconciler.unmountVirtualTree(tree) + end) + + it("should behave correctly when the default value is nil", function() + local context = createContext(nil) + + local valueSpy = createSpy() + local function Listener() + return createElement(context.Consumer, { + render = valueSpy.value, + }) + end + + local tree = noopReconciler.mountVirtualTree(createElement(Listener), nil, "Provide Tree") + expect(valueSpy.callCount).to.equal(1) + valueSpy:assertCalledWith(nil) + + tree = noopReconciler.updateVirtualTree(tree, createElement(Listener)) + noopReconciler.unmountVirtualTree(tree) + + expect(valueSpy.callCount).to.equal(2) + valueSpy:assertCalledWith(nil) + end) + end) + + describe("Update order", function() + --[[ + This test ensures that there is no scenario where we can observe + 'update tearing' when props and context are updated at the same + time. + + Update tearing is scenario where a single update is partially + applied in multiple steps instead of atomically. This is observable + by components and can lead to strange bugs or errors. + + This instance of update tearing happens when updating a prop and a + context value in the same update. Image we represent our tree's + state as the current prop and context versions. Our initial state + is: + + (prop_1, context_1) + + The next state we would like to update to is: + + (prop_2, context_2) + + Under the bug reported in issue 259, Roact reaches three different + states in sequence: + + 1: (prop_1, context_1) - the initial state + 2: (prop_2, context_1) - woops! + 3: (prop_2, context_2) - correct end state + + In state 2, a user component was added that tried to access the + current context value, which was not set at the time. This raised an + error, because this state is not valid! + + The first proposed solution was to move the context update to happen + before the props update. It is easy to show that this will still + result in update tearing: + + 1: (prop_1, context_1) + 2: (prop_1, context_2) + 3: (prop_2, context_2) + + Although the initial concern about newly added components observing + old context values is fixed, there is still a state + desynchronization between props and state. + + We would instead like the following update sequence: + + 1: (prop_1, context_1) + 2: (prop_2, context_2) + + This test tries to ensure that is the case. + + The initial bug report is here: + https://github.com/Roblox/roact/issues/259 + ]] + it("should update context at the same time as props", function() + -- These values are used to make sure we reach both the first and + -- second state combinations we want to visit. + local observedA = false + local observedB = false + local updateCount = 0 + + local context = createContext("default") + + local function Listener(props) + return createElement(context.Consumer, { + render = function(value) + updateCount = updateCount + 1 + + if value == "context_1" then + expect(props.someProp).to.equal("prop_1") + observedA = true + elseif value == "context_2" then + expect(props.someProp).to.equal("prop_2") + observedB = true + else + error("Unexpected context value") + end + end, + }) + end + + local element1 = createElement(context.Provider, { + value = "context_1", + }, { + Child = createElement(Listener, { + someProp = "prop_1", + }), + }) + + local element2 = createElement(context.Provider, { + value = "context_2", + }, { + Child = createElement(Listener, { + someProp = "prop_2", + }), + }) + + local tree = noopReconciler.mountVirtualTree(element1, nil, "UpdateObservationIsFun") + noopReconciler.updateVirtualTree(tree, element2) + + expect(updateCount).to.equal(2) + expect(observedA).to.equal(true) + expect(observedB).to.equal(true) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createElement.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createElement.lua new file mode 100644 index 0000000..b902219 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createElement.lua @@ -0,0 +1,74 @@ +local Children = require(script.Parent.PropMarkers.Children) +local ElementKind = require(script.Parent.ElementKind) +local Logging = require(script.Parent.Logging) +local Type = require(script.Parent.Type) + +local config = require(script.Parent.GlobalConfig).get() + +local multipleChildrenMessage = [[ +The prop `Roact.Children` was defined but was overriden by the third parameter to createElement! +This can happen when a component passes props through to a child element but also uses the `children` argument: + + Roact.createElement("Frame", passedProps, { + child = ... + }) + +Instead, consider using a utility function to merge tables of children together: + + local children = mergeTables(passedProps[Roact.Children], { + child = ... + }) + + local fullProps = mergeTables(passedProps, { + [Roact.Children] = children + }) + + Roact.createElement("Frame", fullProps)]] + +--[[ + Creates a new element representing the given component. + + Elements are lightweight representations of what a component instance should + look like. + + Children is a shorthand for specifying `Roact.Children` as a key inside + props. If specified, the passed `props` table is mutated! +]] +local function createElement(component, props, children) + if config.typeChecks then + assert(component ~= nil, "`component` is required") + assert(typeof(props) == "table" or props == nil, "`props` must be a table or nil") + assert(typeof(children) == "table" or children == nil, "`children` must be a table or nil") + end + + if props == nil then + props = {} + end + + if children ~= nil then + if props[Children] ~= nil then + Logging.warnOnce(multipleChildrenMessage) + end + + props[Children] = children + end + + local elementKind = ElementKind.fromComponent(component) + + local element = { + [Type] = Type.Element, + [ElementKind] = elementKind, + component = component, + props = props, + } + + if config.elementTracing then + -- We trim out the leading newline since there's no way to specify the + -- trace level without also specifying a message. + element.source = debug.traceback("", 2):sub(2) + end + + return element +end + +return createElement \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createElement.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createElement.spec.lua new file mode 100644 index 0000000..6e05709 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createElement.spec.lua @@ -0,0 +1,110 @@ +return function() + local Component = require(script.Parent.Component) + local ElementKind = require(script.Parent.ElementKind) + local GlobalConfig = require(script.Parent.GlobalConfig) + local Logging = require(script.Parent.Logging) + local Type = require(script.Parent.Type) + local Portal = require(script.Parent.Portal) + local Children = require(script.Parent.PropMarkers.Children) + + local createElement = require(script.Parent.createElement) + + it("should create new primitive elements", function() + local element = createElement("Frame") + + expect(element).to.be.ok() + expect(Type.of(element)).to.equal(Type.Element) + expect(ElementKind.of(element)).to.equal(ElementKind.Host) + end) + + it("should create new functional elements", function() + local element = createElement(function() + end) + + expect(element).to.be.ok() + expect(Type.of(element)).to.equal(Type.Element) + expect(ElementKind.of(element)).to.equal(ElementKind.Function) + end) + + it("should create new stateful components", function() + local Foo = Component:extend("Foo") + + local element = createElement(Foo) + + expect(element).to.be.ok() + expect(Type.of(element)).to.equal(Type.Element) + expect(ElementKind.of(element)).to.equal(ElementKind.Stateful) + end) + + it("should create new portal elements", function() + local element = createElement(Portal) + + expect(element).to.be.ok() + expect(Type.of(element)).to.equal(Type.Element) + expect(ElementKind.of(element)).to.equal(ElementKind.Portal) + end) + + it("should accept props", function() + local element = createElement("StringValue", { + Value = "Foo", + }) + + expect(element).to.be.ok() + expect(element.props.Value).to.equal("Foo") + end) + + it("should accept props and children", function() + local child = createElement("IntValue") + + local element = createElement("StringValue", { + Value = "Foo", + }, { + Child = child, + }) + + expect(element).to.be.ok() + expect(element.props.Value).to.equal("Foo") + expect(element.props[Children]).to.be.ok() + expect(element.props[Children].Child).to.equal(child) + end) + + it("should accept children with without props", function() + local child = createElement("IntValue") + + local element = createElement("StringValue", nil, { + Child = child, + }) + + expect(element).to.be.ok() + expect(element.props[Children]).to.be.ok() + expect(element.props[Children].Child).to.equal(child) + end) + + it("should warn once if children is specified in two different ways", function() + local logInfo = Logging.capture(function() + -- Using a loop here to ensure that multiple occurences of the same + -- warning only cause output once. + for _ = 1, 2 do + createElement("Frame", { + [Children] = {}, + }, {}) + end + end) + + expect(#logInfo.warnings).to.equal(1) + expect(logInfo.warnings[1]:find("createElement")).to.be.ok() + expect(logInfo.warnings[1]:find("Children")).to.be.ok() + end) + + it("should have a `source` member if elementTracing is set", function() + local config = { + elementTracing = true, + } + + GlobalConfig.scoped(config, function() + local element = createElement("StringValue") + + expect(element.source).to.be.a("string") + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createFragment.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createFragment.lua new file mode 100644 index 0000000..91554f3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createFragment.lua @@ -0,0 +1,12 @@ +local ElementKind = require(script.Parent.ElementKind) +local Type = require(script.Parent.Type) + +local function createFragment(elements) + return { + [Type] = Type.Element, + [ElementKind] = ElementKind.Fragment, + elements = elements, + } +end + +return createFragment \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createFragment.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createFragment.spec.lua new file mode 100644 index 0000000..45de6c7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createFragment.spec.lua @@ -0,0 +1,21 @@ +return function() + local ElementKind = require(script.Parent.ElementKind) + local Type = require(script.Parent.Type) + + local createFragment = require(script.Parent.createFragment) + + it("should create new primitive elements", function() + local fragment = createFragment({}) + + expect(fragment).to.be.ok() + expect(Type.of(fragment)).to.equal(Type.Element) + expect(ElementKind.of(fragment)).to.equal(ElementKind.Fragment) + end) + + it("should accept children", function() + local subFragment = createFragment({}) + local fragment = createFragment({key = subFragment}) + + expect(fragment.elements.key).to.equal(subFragment) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconciler.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconciler.lua new file mode 100644 index 0000000..e4e43c6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconciler.lua @@ -0,0 +1,456 @@ +local Type = require(script.Parent.Type) +local ElementKind = require(script.Parent.ElementKind) +local ElementUtils = require(script.Parent.ElementUtils) +local Children = require(script.Parent.PropMarkers.Children) +local Symbol = require(script.Parent.Symbol) +local internalAssert = require(script.Parent.internalAssert) + +local config = require(script.Parent.GlobalConfig).get() + +local InternalData = Symbol.named("InternalData") + +--[[ + The reconciler is the mechanism in Roact that constructs the virtual tree + that later gets turned into concrete objects by the renderer. + + Roact's reconciler is constructed with the renderer as an argument, which + enables switching to different renderers for different platforms or + scenarios. + + When testing the reconciler itself, it's common to use `NoopRenderer` with + spies replacing some methods. The default (and only) reconciler interface + exposed by Roact right now uses `RobloxRenderer`. +]] +local function createReconciler(renderer) + local reconciler + local mountVirtualNode + local updateVirtualNode + local unmountVirtualNode + + --[[ + Unmount the given virtualNode, replacing it with a new node described by + the given element. + + Preserves host properties, depth, and legacyContext from parent. + ]] + local function replaceVirtualNode(virtualNode, newElement) + local hostParent = virtualNode.hostParent + local hostKey = virtualNode.hostKey + local depth = virtualNode.depth + + -- If the node that is being replaced has modified context, we need to + -- use the original *unmodified* context for the new node + -- The `originalContext` field will be nil if the context was unchanged + local context = virtualNode.originalContext or virtualNode.context + local parentLegacyContext = virtualNode.parentLegacyContext + + unmountVirtualNode(virtualNode) + local newNode = mountVirtualNode(newElement, hostParent, hostKey, context, parentLegacyContext) + + -- mountVirtualNode can return nil if the element is a boolean + if newNode ~= nil then + newNode.depth = depth + end + + return newNode + end + + --[[ + Utility to update the children of a virtual node based on zero or more + updated children given as elements. + ]] + local function updateChildren(virtualNode, hostParent, newChildElements) + if config.internalTypeChecks then + internalAssert(Type.of(virtualNode) == Type.VirtualNode, "Expected arg #1 to be of type VirtualNode") + end + + local removeKeys = {} + + -- Changed or removed children + for childKey, childNode in pairs(virtualNode.children) do + local newElement = ElementUtils.getElementByKey(newChildElements, childKey) + local newNode = updateVirtualNode(childNode, newElement) + + if newNode ~= nil then + virtualNode.children[childKey] = newNode + else + removeKeys[childKey] = true + end + end + + for childKey in pairs(removeKeys) do + virtualNode.children[childKey] = nil + end + + -- Added children + for childKey, newElement in ElementUtils.iterateElements(newChildElements) do + local concreteKey = childKey + if childKey == ElementUtils.UseParentKey then + concreteKey = virtualNode.hostKey + end + + if virtualNode.children[childKey] == nil then + local childNode = mountVirtualNode( + newElement, + hostParent, + concreteKey, + virtualNode.context, + virtualNode.legacyContext + ) + + -- mountVirtualNode can return nil if the element is a boolean + if childNode ~= nil then + childNode.depth = virtualNode.depth + 1 + virtualNode.children[childKey] = childNode + end + end + end + end + + local function updateVirtualNodeWithChildren(virtualNode, hostParent, newChildElements) + updateChildren(virtualNode, hostParent, newChildElements) + end + + local function updateVirtualNodeWithRenderResult(virtualNode, hostParent, renderResult) + if Type.of(renderResult) == Type.Element + or renderResult == nil + or typeof(renderResult) == "boolean" + then + updateChildren(virtualNode, hostParent, renderResult) + else + error(("%s\n%s"):format( + "Component returned invalid children:", + virtualNode.currentElement.source or "" + ), 0) + end + end + + --[[ + Unmounts the given virtual node and releases any held resources. + ]] + function unmountVirtualNode(virtualNode) + if config.internalTypeChecks then + internalAssert(Type.of(virtualNode) == Type.VirtualNode, "Expected arg #1 to be of type VirtualNode") + end + + local kind = ElementKind.of(virtualNode.currentElement) + + if kind == ElementKind.Host then + renderer.unmountHostNode(reconciler, virtualNode) + elseif kind == ElementKind.Function then + for _, childNode in pairs(virtualNode.children) do + unmountVirtualNode(childNode) + end + elseif kind == ElementKind.Stateful then + virtualNode.instance:__unmount() + elseif kind == ElementKind.Portal then + for _, childNode in pairs(virtualNode.children) do + unmountVirtualNode(childNode) + end + elseif kind == ElementKind.Fragment then + for _, childNode in pairs(virtualNode.children) do + unmountVirtualNode(childNode) + end + else + error(("Unknown ElementKind %q"):format(tostring(kind), 2)) + end + end + + local function updateFunctionVirtualNode(virtualNode, newElement) + local children = newElement.component(newElement.props) + + updateVirtualNodeWithRenderResult(virtualNode, virtualNode.hostParent, children) + + return virtualNode + end + + local function updatePortalVirtualNode(virtualNode, newElement) + local oldElement = virtualNode.currentElement + local oldTargetHostParent = oldElement.props.target + + local targetHostParent = newElement.props.target + + assert(renderer.isHostObject(targetHostParent), "Expected target to be host object") + + if targetHostParent ~= oldTargetHostParent then + return replaceVirtualNode(virtualNode, newElement) + end + + local children = newElement.props[Children] + + updateVirtualNodeWithChildren(virtualNode, targetHostParent, children) + + return virtualNode + end + + local function updateFragmentVirtualNode(virtualNode, newElement) + updateVirtualNodeWithChildren(virtualNode, virtualNode.hostParent, newElement.elements) + + return virtualNode + end + + --[[ + Update the given virtual node using a new element describing what it + should transform into. + + `updateVirtualNode` will return a new virtual node that should replace + the passed in virtual node. This is because a virtual node can be + updated with an element referencing a different component! + + In that case, `updateVirtualNode` will unmount the input virtual node, + mount a new virtual node, and return it in this case, while also issuing + a warning to the user. + ]] + function updateVirtualNode(virtualNode, newElement, newState) + if config.internalTypeChecks then + internalAssert(Type.of(virtualNode) == Type.VirtualNode, "Expected arg #1 to be of type VirtualNode") + end + if config.typeChecks then + assert( + Type.of(newElement) == Type.Element or typeof(newElement) == "boolean" or newElement == nil, + "Expected arg #2 to be of type Element, boolean, or nil" + ) + end + + -- If nothing changed, we can skip this update + if virtualNode.currentElement == newElement and newState == nil then + return virtualNode + end + + if typeof(newElement) == "boolean" or newElement == nil then + unmountVirtualNode(virtualNode) + return nil + end + + if virtualNode.currentElement.component ~= newElement.component then + return replaceVirtualNode(virtualNode, newElement) + end + + local kind = ElementKind.of(newElement) + + local shouldContinueUpdate = true + + if kind == ElementKind.Host then + virtualNode = renderer.updateHostNode(reconciler, virtualNode, newElement) + elseif kind == ElementKind.Function then + virtualNode = updateFunctionVirtualNode(virtualNode, newElement) + elseif kind == ElementKind.Stateful then + shouldContinueUpdate = virtualNode.instance:__update(newElement, newState) + elseif kind == ElementKind.Portal then + virtualNode = updatePortalVirtualNode(virtualNode, newElement) + elseif kind == ElementKind.Fragment then + virtualNode = updateFragmentVirtualNode(virtualNode, newElement) + else + error(("Unknown ElementKind %q"):format(tostring(kind), 2)) + end + + -- Stateful components can abort updates via shouldUpdate. If that + -- happens, we should stop doing stuff at this point. + if not shouldContinueUpdate then + return virtualNode + end + + virtualNode.currentElement = newElement + + return virtualNode + end + + --[[ + Constructs a new virtual node but not does mount it. + ]] + local function createVirtualNode(element, hostParent, hostKey, context, legacyContext) + if config.internalTypeChecks then + internalAssert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") + internalAssert(typeof(context) == "table" or context == nil, "Expected arg #4 to be of type table or nil") + internalAssert( + typeof(legacyContext) == "table" or legacyContext == nil, + "Expected arg #5 to be of type table or nil" + ) + end + if config.typeChecks then + assert(hostKey ~= nil, "Expected arg #3 to be non-nil") + assert( + Type.of(element) == Type.Element or typeof(element) == "boolean", + "Expected arg #1 to be of type Element or boolean" + ) + end + + return { + [Type] = Type.VirtualNode, + currentElement = element, + depth = 1, + children = {}, + hostParent = hostParent, + hostKey = hostKey, + + -- Legacy Context API + -- A table of context values inherited from the parent node + legacyContext = legacyContext, + + -- A saved copy of the parent context, used when replacing a node + parentLegacyContext = legacyContext, + + -- Context API + -- A table of context values inherited from the parent node + context = context or {}, + + -- A saved copy of the unmodified context; this will be updated when + -- a component adds new context and used when a node is replaced + originalContext = nil, + } + end + + local function mountFunctionVirtualNode(virtualNode) + local element = virtualNode.currentElement + + local children = element.component(element.props) + + updateVirtualNodeWithRenderResult(virtualNode, virtualNode.hostParent, children) + end + + local function mountPortalVirtualNode(virtualNode) + local element = virtualNode.currentElement + + local targetHostParent = element.props.target + local children = element.props[Children] + + assert(renderer.isHostObject(targetHostParent), "Expected target to be host object") + + updateVirtualNodeWithChildren(virtualNode, targetHostParent, children) + end + + local function mountFragmentVirtualNode(virtualNode) + local element = virtualNode.currentElement + local children = element.elements + + updateVirtualNodeWithChildren(virtualNode, virtualNode.hostParent, children) + end + + --[[ + Constructs a new virtual node and mounts it, but does not place it into + the tree. + ]] + function mountVirtualNode(element, hostParent, hostKey, context, legacyContext) + if config.internalTypeChecks then + internalAssert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") + internalAssert( + typeof(legacyContext) == "table" or legacyContext == nil, + "Expected arg #5 to be of type table or nil" + ) + end + if config.typeChecks then + assert(hostKey ~= nil, "Expected arg #3 to be non-nil") + assert( + Type.of(element) == Type.Element or typeof(element) == "boolean", + "Expected arg #1 to be of type Element or boolean" + ) + end + + -- Boolean values render as nil to enable terse conditional rendering. + if typeof(element) == "boolean" then + return nil + end + + local kind = ElementKind.of(element) + + local virtualNode = createVirtualNode(element, hostParent, hostKey, context, legacyContext) + + if kind == ElementKind.Host then + renderer.mountHostNode(reconciler, virtualNode) + elseif kind == ElementKind.Function then + mountFunctionVirtualNode(virtualNode) + elseif kind == ElementKind.Stateful then + element.component:__mount(reconciler, virtualNode) + elseif kind == ElementKind.Portal then + mountPortalVirtualNode(virtualNode) + elseif kind == ElementKind.Fragment then + mountFragmentVirtualNode(virtualNode) + else + error(("Unknown ElementKind %q"):format(tostring(kind), 2)) + end + + return virtualNode + end + + --[[ + Constructs a new Roact virtual tree, constructs a root node for + it, and mounts it. + ]] + local function mountVirtualTree(element, hostParent, hostKey) + if config.typeChecks then + assert(Type.of(element) == Type.Element, "Expected arg #1 to be of type Element") + assert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") + end + + if hostKey == nil then + hostKey = "RoactTree" + end + + local tree = { + [Type] = Type.VirtualTree, + [InternalData] = { + -- The root node of the tree, which starts into the hierarchy of + -- Roact component instances. + rootNode = nil, + mounted = true, + }, + } + + tree[InternalData].rootNode = mountVirtualNode(element, hostParent, hostKey) + + return tree + end + + --[[ + Unmounts the virtual tree, freeing all of its resources. + + No further operations should be done on the tree after it's been + unmounted, as indicated by its the `mounted` field. + ]] + local function unmountVirtualTree(tree) + local internalData = tree[InternalData] + if config.typeChecks then + assert(Type.of(tree) == Type.VirtualTree, "Expected arg #1 to be a Roact handle") + assert(internalData.mounted, "Cannot unmounted a Roact tree that has already been unmounted") + end + + internalData.mounted = false + + if internalData.rootNode ~= nil then + unmountVirtualNode(internalData.rootNode) + end + end + + --[[ + Utility method for updating the root node of a virtual tree given a new + element. + ]] + local function updateVirtualTree(tree, newElement) + local internalData = tree[InternalData] + if config.typeChecks then + assert(Type.of(tree) == Type.VirtualTree, "Expected arg #1 to be a Roact handle") + assert(Type.of(newElement) == Type.Element, "Expected arg #2 to be a Roact Element") + end + + internalData.rootNode = updateVirtualNode(internalData.rootNode, newElement) + + return tree + end + + reconciler = { + mountVirtualTree = mountVirtualTree, + unmountVirtualTree = unmountVirtualTree, + updateVirtualTree = updateVirtualTree, + + createVirtualNode = createVirtualNode, + mountVirtualNode = mountVirtualNode, + unmountVirtualNode = unmountVirtualNode, + updateVirtualNode = updateVirtualNode, + updateVirtualNodeWithChildren = updateVirtualNodeWithChildren, + updateVirtualNodeWithRenderResult = updateVirtualNodeWithRenderResult, + } + + return reconciler +end + +return createReconciler diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconciler.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconciler.spec.lua new file mode 100644 index 0000000..193dd25 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconciler.spec.lua @@ -0,0 +1,326 @@ +return function() + local assign = require(script.Parent.assign) + local createElement = require(script.Parent.createElement) + local createFragment = require(script.Parent.createFragment) + local createSpy = require(script.Parent.createSpy) + local NoopRenderer = require(script.Parent.NoopRenderer) + local Type = require(script.Parent.Type) + local ElementKind = require(script.Parent.ElementKind) + + local createReconciler = require(script.Parent.createReconciler) + + local noopReconciler = createReconciler(NoopRenderer) + + describe("tree operations", function() + it("should mount and unmount", function() + local tree = noopReconciler.mountVirtualTree(createElement("StringValue")) + + expect(tree).to.be.ok() + + noopReconciler.unmountVirtualTree(tree) + end) + + it("should mount, update, and unmount", function() + local tree = noopReconciler.mountVirtualTree(createElement("StringValue")) + + expect(tree).to.be.ok() + + noopReconciler.updateVirtualTree(tree, createElement("StringValue")) + + noopReconciler.unmountVirtualTree(tree) + end) + end) + + describe("booleans", function() + it("should mount booleans as nil", function() + local node = noopReconciler.mountVirtualNode(false, nil, "test") + expect(node).to.equal(nil) + end) + + it("should unmount nodes if they are updated to a boolean value", function() + local node = noopReconciler.mountVirtualNode(createElement("StringValue"), nil, "test") + + expect(node).to.be.ok() + + node = noopReconciler.updateVirtualNode(node, true) + + expect(node).to.equal(nil) + end) + end) + + describe("invalid elements", function() + it("should throw errors when attempting to mount invalid elements", function() + -- These function components return values with incorrect types + local returnsString = function() + return "Hello" + end + local returnsNumber = function() + return 1 + end + local returnsFunction = function() + return function() end + end + local returnsTable = function() + return {} + end + + local hostParent = nil + local key = "Some Key" + + expect(function() + noopReconciler.mountVirtualNode(createElement(returnsString), hostParent, key) + end).to.throw() + + expect(function() + noopReconciler.mountVirtualNode(createElement(returnsNumber), hostParent, key) + end).to.throw() + + expect(function() + noopReconciler.mountVirtualNode(createElement(returnsFunction), hostParent, key) + end).to.throw() + + expect(function() + noopReconciler.mountVirtualNode(createElement(returnsTable), hostParent, key) + end).to.throw() + end) + end) + + describe("Host components", function() + it("should invoke the renderer to mount host nodes", function() + local mountHostNode = createSpy(NoopRenderer.mountHostNode) + + local renderer = assign({}, NoopRenderer, { + mountHostNode = mountHostNode.value, + }) + + local reconciler = createReconciler(renderer) + + local element = createElement("StringValue") + local hostParent = nil + local key = "Some Key" + local node = reconciler.mountVirtualNode(element, hostParent, key) + + expect(Type.of(node)).to.equal(Type.VirtualNode) + + expect(mountHostNode.callCount).to.equal(1) + + local values = mountHostNode:captureValues("reconciler", "node") + + expect(values.reconciler).to.equal(reconciler) + expect(values.node).to.equal(node) + end) + + it("should invoke the renderer to update host nodes", function() + local updateHostNode = createSpy(NoopRenderer.updateHostNode) + + local renderer = assign({}, NoopRenderer, { + mountHostNode = NoopRenderer.mountHostNode, + updateHostNode = updateHostNode.value, + }) + + local reconciler = createReconciler(renderer) + + local element = createElement("StringValue") + local hostParent = nil + local key = "Key" + local node = reconciler.mountVirtualNode(element, hostParent, key) + + expect(Type.of(node)).to.equal(Type.VirtualNode) + + local newElement = createElement("StringValue") + local newNode = reconciler.updateVirtualNode(node, newElement) + + expect(newNode).to.equal(node) + + expect(updateHostNode.callCount).to.equal(1) + + local values = updateHostNode:captureValues("reconciler", "node", "newElement") + + expect(values.reconciler).to.equal(reconciler) + expect(values.node).to.equal(node) + expect(values.newElement).to.equal(newElement) + end) + + it("should invoke the renderer to unmount host nodes", function() + local unmountHostNode = createSpy(NoopRenderer.unmountHostNode) + + local renderer = assign({}, NoopRenderer, { + mountHostNode = NoopRenderer.mountHostNode, + unmountHostNode = unmountHostNode.value, + }) + + local reconciler = createReconciler(renderer) + + local element = createElement("StringValue") + local hostParent = nil + local key = "Key" + local node = reconciler.mountVirtualNode(element, hostParent, key) + + expect(Type.of(node)).to.equal(Type.VirtualNode) + + reconciler.unmountVirtualNode(node) + + expect(unmountHostNode.callCount).to.equal(1) + + local values = unmountHostNode:captureValues("reconciler", "node") + + expect(values.reconciler).to.equal(reconciler) + expect(values.node).to.equal(node) + end) + end) + + describe("Function components", function() + it("should mount and unmount function components", function() + local componentSpy = createSpy(function(props) + return nil + end) + + local element = createElement(componentSpy.value, { + someValue = 5, + }) + local hostParent = nil + local key = "A Key" + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(Type.of(node)).to.equal(Type.VirtualNode) + + expect(componentSpy.callCount).to.equal(1) + + local calledWith = componentSpy:captureValues("props") + + expect(calledWith.props).to.be.a("table") + expect(calledWith.props.someValue).to.equal(5) + + noopReconciler.unmountVirtualNode(node) + + expect(componentSpy.callCount).to.equal(1) + end) + + it("should mount single children of function components", function() + local childComponentSpy = createSpy(function(props) + return nil + end) + + local parentComponentSpy = createSpy(function(props) + return createElement(childComponentSpy.value, { + value = props.value + 1, + }) + end) + + local element = createElement(parentComponentSpy.value, { + value = 13, + }) + local hostParent = nil + local key = "A Key" + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(Type.of(node)).to.equal(Type.VirtualNode) + + expect(parentComponentSpy.callCount).to.equal(1) + expect(childComponentSpy.callCount).to.equal(1) + + local parentCalledWith = parentComponentSpy:captureValues("props") + local childCalledWith = childComponentSpy:captureValues("props") + + expect(parentCalledWith.props).to.be.a("table") + expect(parentCalledWith.props.value).to.equal(13) + + expect(childCalledWith.props).to.be.a("table") + expect(childCalledWith.props.value).to.equal(14) + + noopReconciler.unmountVirtualNode(node) + + expect(parentComponentSpy.callCount).to.equal(1) + expect(childComponentSpy.callCount).to.equal(1) + end) + + it("should mount fragments returned by function components", function() + local childAComponentSpy = createSpy(function(props) + return nil + end) + + local childBComponentSpy = createSpy(function(props) + return nil + end) + + local parentComponentSpy = createSpy(function(props) + return createFragment({ + A = createElement(childAComponentSpy.value, { + value = props.value + 1, + }), + B = createElement(childBComponentSpy.value, { + value = props.value + 5, + }), + }) + end) + + local element = createElement(parentComponentSpy.value, { + value = 17, + }) + local hostParent = nil + local key = "A Key" + local node = noopReconciler.mountVirtualNode(element, hostParent, key) + + expect(Type.of(node)).to.equal(Type.VirtualNode) + + expect(parentComponentSpy.callCount).to.equal(1) + expect(childAComponentSpy.callCount).to.equal(1) + expect(childBComponentSpy.callCount).to.equal(1) + + local parentCalledWith = parentComponentSpy:captureValues("props") + local childACalledWith = childAComponentSpy:captureValues("props") + local childBCalledWith = childBComponentSpy:captureValues("props") + + expect(parentCalledWith.props).to.be.a("table") + expect(parentCalledWith.props.value).to.equal(17) + + expect(childACalledWith.props).to.be.a("table") + expect(childACalledWith.props.value).to.equal(18) + + expect(childBCalledWith.props).to.be.a("table") + expect(childBCalledWith.props.value).to.equal(22) + + noopReconciler.unmountVirtualNode(node) + + expect(parentComponentSpy.callCount).to.equal(1) + expect(childAComponentSpy.callCount).to.equal(1) + expect(childBComponentSpy.callCount).to.equal(1) + end) + end) + + describe("Fragments", function() + it("should mount fragments", function() + local fragment = createFragment({}) + local node = noopReconciler.mountVirtualNode(fragment, nil, "test") + + expect(node).to.be.ok() + expect(ElementKind.of(node.currentElement)).to.equal(ElementKind.Fragment) + end) + + it("should mount an empty fragment", function() + local emptyFragment = createFragment({}) + local node = noopReconciler.mountVirtualNode(emptyFragment, nil, "test") + + expect(node).to.be.ok() + expect(next(node.children)).to.never.be.ok() + end) + + it("should mount all fragment's children", function() + local childComponentSpy = createSpy(function(props) + return nil + end) + local elements = {} + local totalElements = 5 + + for i=1, totalElements do + elements["key"..tostring(i)] = createElement(childComponentSpy.value, {}) + end + + local fragments = createFragment(elements) + local node = noopReconciler.mountVirtualNode(fragments, nil, "test") + + expect(node).to.be.ok() + expect(childComponentSpy.callCount).to.equal(totalElements) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconcilerCompat.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconcilerCompat.lua new file mode 100644 index 0000000..e79cf5a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconcilerCompat.lua @@ -0,0 +1,47 @@ +--[[ + Contains deprecated methods from Reconciler. Broken out so that removing + this shim is easy -- just delete this file and remove it from init. +]] + +local Logging = require(script.Parent.Logging) + +local reifyMessage = [[ +Roact.reify has been renamed to Roact.mount and will be removed in a future release. +Check the call to Roact.reify at: +]] + +local teardownMessage = [[ +Roact.teardown has been renamed to Roact.unmount and will be removed in a future release. +Check the call to Roact.teardown at: +]] + +local reconcileMessage = [[ +Roact.reconcile has been renamed to Roact.update and will be removed in a future release. +Check the call to Roact.reconcile at: +]] + +local function createReconcilerCompat(reconciler) + local compat = {} + + function compat.reify(...) + Logging.warnOnce(reifyMessage) + + return reconciler.mountVirtualTree(...) + end + + function compat.teardown(...) + Logging.warnOnce(teardownMessage) + + return reconciler.unmountVirtualTree(...) + end + + function compat.reconcile(...) + Logging.warnOnce(reconcileMessage) + + return reconciler.updateVirtualTree(...) + end + + return compat +end + +return createReconcilerCompat \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconcilerCompat.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconcilerCompat.spec.lua new file mode 100644 index 0000000..ea4d078 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createReconcilerCompat.spec.lua @@ -0,0 +1,82 @@ +return function() + local createElement = require(script.Parent.createElement) + local createReconciler = require(script.Parent.createReconciler) + local Logging = require(script.Parent.Logging) + local NoopRenderer = require(script.Parent.NoopRenderer) + + local createReconcilerCompat = require(script.Parent.createReconcilerCompat) + + local noopReconciler = createReconciler(NoopRenderer) + local compatReconciler = createReconcilerCompat(noopReconciler) + + it("reify should only warn once per call site", function() + local logInfo = Logging.capture(function() + -- We're using a loop so that we get the same stack trace and only one + -- warning hopefully. + for _ = 1, 2 do + local handle = compatReconciler.reify(createElement("StringValue")) + noopReconciler.unmountVirtualTree(handle) + end + end) + + expect(#logInfo.warnings).to.equal(1) + expect(logInfo.warnings[1]:find("reify")).to.be.ok() + + logInfo = Logging.capture(function() + -- This is a different call site, which should trigger another warning. + local handle = compatReconciler.reify(createElement("StringValue")) + noopReconciler.unmountVirtualTree(handle) + end) + + expect(#logInfo.warnings).to.equal(1) + expect(logInfo.warnings[1]:find("reify")).to.be.ok() + end) + + it("teardown should only warn once per call site", function() + local logInfo = Logging.capture(function() + -- We're using a loop so that we get the same stack trace and only one + -- warning hopefully. + for _ = 1, 2 do + local handle = noopReconciler.mountVirtualTree(createElement("StringValue")) + compatReconciler.teardown(handle) + end + end) + + expect(#logInfo.warnings).to.equal(1) + expect(logInfo.warnings[1]:find("teardown")).to.be.ok() + + logInfo = Logging.capture(function() + -- This is a different call site, which should trigger another warning. + local handle = noopReconciler.mountVirtualTree(createElement("StringValue")) + compatReconciler.teardown(handle) + end) + + expect(#logInfo.warnings).to.equal(1) + expect(logInfo.warnings[1]:find("teardown")).to.be.ok() + end) + + it("update should only warn once per call site", function() + local logInfo = Logging.capture(function() + -- We're using a loop so that we get the same stack trace and only one + -- warning hopefully. + for _ = 1, 2 do + local handle = noopReconciler.mountVirtualTree(createElement("StringValue")) + compatReconciler.reconcile(handle, createElement("StringValue")) + noopReconciler.unmountVirtualTree(handle) + end + end) + + expect(#logInfo.warnings).to.equal(1) + expect(logInfo.warnings[1]:find("reconcile")).to.be.ok() + + logInfo = Logging.capture(function() + -- This is a different call site, which should trigger another warning. + local handle = noopReconciler.mountVirtualTree(createElement("StringValue")) + compatReconciler.reconcile(handle, createElement("StringValue")) + noopReconciler.unmountVirtualTree(handle) + end) + + expect(#logInfo.warnings).to.equal(1) + expect(logInfo.warnings[1]:find("reconcile")).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createRef.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createRef.lua new file mode 100644 index 0000000..c13e1b5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createRef.lua @@ -0,0 +1,38 @@ +--[[ + A ref is nothing more than a binding with a special field 'current' + that maps to the getValue method of the binding +]] +local Binding = require(script.Parent.Binding) + +local function createRef() + local binding, _ = Binding.create(nil) + + local ref = {} + + --[[ + A ref is just redirected to a binding via its metatable + ]] + setmetatable(ref, { + __index = function(self, key) + if key == "current" then + return binding:getValue() + else + return binding[key] + end + end, + __newindex = function(self, key, value) + if key == "current" then + error("Cannot assign to the 'current' property of refs", 2) + end + + binding[key] = value + end, + __tostring = function(self) + return ("RoactRef(%s)"):format(tostring(binding:getValue())) + end, + }) + + return ref +end + +return createRef \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createRef.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createRef.spec.lua new file mode 100644 index 0000000..553e79d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createRef.spec.lua @@ -0,0 +1,55 @@ +return function() + local Binding = require(script.Parent.Binding) + local Type = require(script.Parent.Type) + + local createRef = require(script.Parent.createRef) + + it("should create refs, which are specialized bindings", function() + local ref = createRef() + + expect(Type.of(ref)).to.equal(Type.Binding) + expect(ref.current).to.equal(nil) + end) + + it("should have a 'current' field that is the same as the internal binding's value", function() + local ref = createRef() + + expect(ref.current).to.equal(nil) + + Binding.update(ref, 10) + expect(ref.current).to.equal(10) + end) + + it("should support tostring on refs", function() + local ref = createRef() + + expect(ref.current).to.equal(nil) + expect(tostring(ref)).to.equal("RoactRef(nil)") + + Binding.update(ref, 10) + expect(tostring(ref)).to.equal("RoactRef(10)") + end) + + it("should not allow assignments to the 'current' field", function() + local ref = createRef() + + expect(ref.current).to.equal(nil) + + Binding.update(ref, 99) + expect(ref.current).to.equal(99) + + expect(function() + ref.current = 77 + end).to.throw() + + expect(ref.current).to.equal(99) + end) + + it("should return the same thing from getValue as its current field", function() + local ref = createRef() + Binding.update(ref, 10) + + expect(ref:getValue()).to.equal(10) + expect(ref:getValue()).to.equal(ref.current) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSignal.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSignal.lua new file mode 100644 index 0000000..3db6354 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSignal.lua @@ -0,0 +1,75 @@ +--[[ + This is a simple signal implementation that has a dead-simple API. + + local signal = createSignal() + + local disconnect = signal:subscribe(function(foo) + print("Cool foo:", foo) + end) + + signal:fire("something") + + disconnect() +]] + +local function addToMap(map, addKey, addValue) + local new = {} + + for key, value in pairs(map) do + new[key] = value + end + + new[addKey] = addValue + + return new +end + +local function removeFromMap(map, removeKey) + local new = {} + + for key, value in pairs(map) do + if key ~= removeKey then + new[key] = value + end + end + + return new +end + +local function createSignal() + local connections = {} + + local function subscribe(self, callback) + assert(typeof(callback) == "function", "Can only subscribe to signals with a function.") + + local connection = { + callback = callback, + } + + connections = addToMap(connections, callback, connection) + + local function disconnect() + assert(not connection.disconnected, "Listeners can only be disconnected once.") + + connection.disconnected = true + connections = removeFromMap(connections, callback) + end + + return disconnect + end + + local function fire(self, ...) + for callback, connection in pairs(connections) do + if not connection.disconnected then + callback(...) + end + end + end + + return { + subscribe = subscribe, + fire = fire, + } +end + +return createSignal \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSignal.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSignal.spec.lua new file mode 100644 index 0000000..ed9cf97 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSignal.spec.lua @@ -0,0 +1,89 @@ +return function() + local createSignal = require(script.Parent.createSignal) + + local createSpy = require(script.Parent.createSpy) + + it("should fire subscribers and disconnect them", function() + local signal = createSignal() + + local spy = createSpy() + local disconnect = signal:subscribe(spy.value) + + expect(spy.callCount).to.equal(0) + + local a = 1 + local b = {} + local c = "hello" + signal:fire(a, b, c) + + expect(spy.callCount).to.equal(1) + spy:assertCalledWith(a, b, c) + + disconnect() + + signal:fire() + + expect(spy.callCount).to.equal(1) + end) + + it("should handle multiple subscribers", function() + local signal = createSignal() + + local spyA = createSpy() + local spyB = createSpy() + + local disconnectA = signal:subscribe(spyA.value) + local disconnectB = signal:subscribe(spyB.value) + + expect(spyA.callCount).to.equal(0) + expect(spyB.callCount).to.equal(0) + + local a = {} + local b = 67 + signal:fire(a, b) + + expect(spyA.callCount).to.equal(1) + spyA:assertCalledWith(a, b) + + expect(spyB.callCount).to.equal(1) + spyB:assertCalledWith(a, b) + + disconnectA() + + signal:fire(b, a) + + expect(spyA.callCount).to.equal(1) + + expect(spyB.callCount).to.equal(2) + spyB:assertCalledWith(b, a) + + disconnectB() + end) + + it("should stop firing a connection if disconnected mid-fire", function() + local signal = createSignal() + + -- In this test, we'll connect two listeners that each try to disconnect + -- the other. Because the order of listeners firing isn't defined, we + -- have to be careful to handle either case. + + local disconnectA + local disconnectB + + local spyA = createSpy(function() + disconnectB() + end) + + local spyB = createSpy(function() + disconnectA() + end) + + disconnectA = signal:subscribe(spyA.value) + disconnectB = signal:subscribe(spyB.value) + + signal:fire() + + -- Exactly once listener should have been called. + expect(spyA.callCount + spyB.callCount).to.equal(1) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSpy.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSpy.lua new file mode 100644 index 0000000..baeba1c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSpy.lua @@ -0,0 +1,85 @@ +--[[ + A utility used to create a function spy that can be used to robustly test + that functions are invoked the correct number of times and with the correct + number of arguments. + + This should only be used in tests. +]] + +local assertDeepEqual = require(script.Parent.assertDeepEqual) + +local function createSpy(inner) + local self = { + callCount = 0, + values = {}, + valuesLength = 0, + } + + self.value = function(...) + self.callCount = self.callCount + 1 + self.values = {...} + self.valuesLength = select("#", ...) + + if inner ~= nil then + return inner(...) + end + end + + self.assertCalledWith = function(_, ...) + local len = select("#", ...) + + if self.valuesLength ~= len then + error(("Expected %d arguments, but was called with %d arguments"):format( + self.valuesLength, + len + ), 2) + end + + for i = 1, len do + local expected = select(i, ...) + + assert(self.values[i] == expected, "value differs") + end + end + + self.assertCalledWithDeepEqual = function(_, ...) + local len = select("#", ...) + + if self.valuesLength ~= len then + error(("Expected %d arguments, but was called with %d arguments"):format( + self.valuesLength, + len + ), 2) + end + + for i = 1, len do + local expected = select(i, ...) + + assertDeepEqual(self.values[i], expected) + end + end + + self.captureValues = function(_, ...) + local len = select("#", ...) + local result = {} + + assert(self.valuesLength == len, "length of expected values differs from stored values") + + for i = 1, len do + local key = select(i, ...) + result[key] = self.values[i] + end + + return result + end + + setmetatable(self, { + __index = function(_, key) + error(("%q is not a valid member of spy"):format(key)) + end, + }) + + return self +end + +return createSpy \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSpy.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSpy.spec.lua new file mode 100644 index 0000000..8693693 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/createSpy.spec.lua @@ -0,0 +1,90 @@ +return function() + local createSpy = require(script.Parent.createSpy) + + describe("createSpy", function() + it("should create spies", function() + local spy = createSpy(function() end) + + expect(spy).to.be.ok() + end) + + it("should throw if spies are indexed by an invalid key", function() + local spy = createSpy(function() end) + + expect(function() + return spy.test + end).to.throw() + end) + end) + + describe("value", function() + it("should increment callCount when called", function() + local spy = createSpy(function() end) + spy.value() + + expect(spy.callCount).to.equal(1) + end) + + it("should store all values passed", function() + local spy = createSpy(function() end) + spy.value(1, true, "3") + + expect(spy.valuesLength).to.equal(3) + expect(spy.values[1]).to.equal(1) + expect(spy.values[2]).to.equal(true) + expect(spy.values[3]).to.equal("3") + end) + + it("should return the value of the inner function", function() + local spy = createSpy(function() + return true + end) + + expect(spy.value()).to.equal(true) + end) + end) + + describe("assertCalledWith", function() + it("should throw if the number of values differs", function() + local spy = createSpy(function() end) + spy.value(1, 2) + + expect(function() + spy:assertCalledWith(1) + end).to.throw() + end) + + it("should throw if any value differs", function() + local spy = createSpy(function() end) + spy.value(1, 2) + + expect(function() + spy:assertCalledWith(1, 3) + end).to.throw() + + expect(function() + spy:assertCalledWith(2, 3) + end).to.throw() + end) + end) + + describe("captureValues", function() + it("should throw if the number of values differs", function() + local spy = createSpy(function() end) + spy.value(1, 2) + + expect(function() + spy:captureValues("a") + end).to.throw() + end) + + it("should capture all values in a table", function() + local spy = createSpy(function() end) + spy.value(1, 2) + + local captured = spy:captureValues("a", "b") + expect(captured.a).to.equal(1) + expect(captured.b).to.equal(2) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/getDefaultInstanceProperty.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/getDefaultInstanceProperty.lua new file mode 100644 index 0000000..9a6a095 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/getDefaultInstanceProperty.lua @@ -0,0 +1,54 @@ +--[[ + Attempts to get the default value of a given property on a Roblox instance. + + This is used by the reconciler in cases where a prop was previously set on a + primitive component, but is no longer present in a component's new props. + + Eventually, Roblox might provide a nicer API to query the default property + of an object without constructing an instance of it. +]] + +local Symbol = require(script.Parent.Symbol) + +local Nil = Symbol.named("Nil") +local _cachedPropertyValues = {} + +local function getDefaultInstanceProperty(className, propertyName) + local classCache = _cachedPropertyValues[className] + + if classCache then + local propValue = classCache[propertyName] + + -- We have to use a marker here, because Lua doesn't distinguish + -- between 'nil' and 'not in a table' + if propValue == Nil then + return true, nil + end + + if propValue ~= nil then + return true, propValue + end + else + classCache = {} + _cachedPropertyValues[className] = classCache + end + + local created = Instance.new(className) + local ok, defaultValue = pcall(function() + return created[propertyName] + end) + + created:Destroy() + + if ok then + if defaultValue == nil then + classCache[propertyName] = Nil + else + classCache[propertyName] = defaultValue + end + end + + return ok, defaultValue +end + +return getDefaultInstanceProperty \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/getDefaultInstanceProperty.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/getDefaultInstanceProperty.spec.lua new file mode 100644 index 0000000..a126820 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/getDefaultInstanceProperty.spec.lua @@ -0,0 +1,33 @@ +return function() + local getDefaultInstanceProperty = require(script.Parent.getDefaultInstanceProperty) + + it("should get default name string values", function() + local _, defaultName = getDefaultInstanceProperty("StringValue", "Name") + + expect(defaultName).to.equal("Value") + end) + + it("should get default empty string values", function() + local _, defaultValue = getDefaultInstanceProperty("StringValue", "Value") + + expect(defaultValue).to.equal("") + end) + + it("should get default number values", function() + local _, defaultValue = getDefaultInstanceProperty("IntValue", "Value") + + expect(defaultValue).to.equal(0) + end) + + it("should get nil default values", function() + local _, defaultValue = getDefaultInstanceProperty("ObjectValue", "Value") + + expect(defaultValue).to.equal(nil) + end) + + it("should get bool default values", function() + local _, defaultValue = getDefaultInstanceProperty("BoolValue", "Value") + + expect(defaultValue).to.equal(false) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/init.lua new file mode 100644 index 0000000..0bac301 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/init.lua @@ -0,0 +1,48 @@ +--[[ + Packages up the internals of Roact and exposes a public API for it. +]] + +local GlobalConfig = require(script.GlobalConfig) +local createReconciler = require(script.createReconciler) +local createReconcilerCompat = require(script.createReconcilerCompat) +local RobloxRenderer = require(script.RobloxRenderer) +local strict = require(script.strict) +local Binding = require(script.Binding) + +local robloxReconciler = createReconciler(RobloxRenderer) +local reconcilerCompat = createReconcilerCompat(robloxReconciler) + +local Roact = strict { + Component = require(script.Component), + createElement = require(script.createElement), + createFragment = require(script.createFragment), + oneChild = require(script.oneChild), + PureComponent = require(script.PureComponent), + None = require(script.None), + Portal = require(script.Portal), + createRef = require(script.createRef), + createBinding = Binding.create, + joinBindings = Binding.join, + createContext = require(script.createContext), + + Change = require(script.PropMarkers.Change), + Children = require(script.PropMarkers.Children), + Event = require(script.PropMarkers.Event), + Ref = require(script.PropMarkers.Ref), + + mount = robloxReconciler.mountVirtualTree, + unmount = robloxReconciler.unmountVirtualTree, + update = robloxReconciler.updateVirtualTree, + + reify = reconcilerCompat.reify, + teardown = reconcilerCompat.teardown, + reconcile = reconcilerCompat.reconcile, + + setGlobalConfig = GlobalConfig.set, + + -- APIs that may change in the future without warning + UNSTABLE = { + }, +} + +return Roact \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/init.spec.lua new file mode 100644 index 0000000..652ee19 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/init.spec.lua @@ -0,0 +1,64 @@ +return function() + local Roact = require(script.Parent) + + it("should load with all public APIs", function() + local publicApi = { + createElement = "function", + createFragment = "function", + createRef = "function", + createBinding = "function", + joinBindings = "function", + mount = "function", + unmount = "function", + update = "function", + oneChild = "function", + setGlobalConfig = "function", + createContext = "function", + + -- These functions are deprecated and throw warnings! + reify = "function", + teardown = "function", + reconcile = "function", + + Component = true, + PureComponent = true, + Portal = true, + Children = true, + Event = true, + Change = true, + Ref = true, + None = true, + UNSTABLE = true, + } + + expect(Roact).to.be.ok() + + for key, valueType in pairs(publicApi) do + local success + if typeof(valueType) == "string" then + success = typeof(Roact[key]) == valueType + else + success = Roact[key] ~= nil + end + + if not success then + local existence = typeof(valueType) == "boolean" and "present" or "of type " .. valueType + local message = ( + "Expected public API member %q to be %s, but instead it was of type %s" + ):format(tostring(key), existence, typeof(Roact[key])) + + error(message) + end + end + + for key in pairs(Roact) do + if publicApi[key] == nil then + local message = ( + "Found unknown public API key %q!" + ):format(tostring(key)) + + error(message) + end + end + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/internalAssert.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/internalAssert.lua new file mode 100644 index 0000000..87c5dfc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/internalAssert.lua @@ -0,0 +1,7 @@ +local function internalAssert(condition, message) + if not condition then + error(message .. " (This is probably a bug in Roact!)", 3) + end +end + +return internalAssert \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/invalidSetStateMessages.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/invalidSetStateMessages.lua new file mode 100644 index 0000000..34571ce --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/invalidSetStateMessages.lua @@ -0,0 +1,44 @@ +--[[ + These messages are used by Component to help users diagnose when they're + calling setState in inappropriate places. + + The indentation may seem odd, but it's necessary to avoid introducing extra + whitespace into the error messages themselves. +]] +local ComponentLifecyclePhase = require(script.Parent.ComponentLifecyclePhase) + +local invalidSetStateMessages = {} + +invalidSetStateMessages[ComponentLifecyclePhase.WillUpdate] = [[ +setState cannot be used in the willUpdate lifecycle method. +Consider using the didUpdate method instead, or using getDerivedStateFromProps. + +Check the definition of willUpdate in the component %q.]] + +invalidSetStateMessages[ComponentLifecyclePhase.WillUnmount] = [[ +setState cannot be used in the willUnmount lifecycle method. +A component that is being unmounted cannot be updated! + +Check the definition of willUnmount in the component %q.]] + +invalidSetStateMessages[ComponentLifecyclePhase.ShouldUpdate] = [[ +setState cannot be used in the shouldUpdate lifecycle method. +shouldUpdate must be a pure function that only depends on props and state. + +Check the definition of shouldUpdate in the component %q.]] + +invalidSetStateMessages[ComponentLifecyclePhase.Render] = [[ +setState cannot be used in the render method. +render must be a pure function that only depends on props and state. + +Check the definition of render in the component %q.]] + +invalidSetStateMessages["default"] = [[ +setState can not be used in the current situation, because Roact doesn't know +which part of the lifecycle this component is in. + +This is a bug in Roact. +It was triggered by the component %q. +]] + +return invalidSetStateMessages \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/oneChild.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/oneChild.lua new file mode 100644 index 0000000..285d519 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/oneChild.lua @@ -0,0 +1,28 @@ +--[[ + Retrieves at most one child from the children passed to a component. + + If passed nil or an empty table, will return nil. + + Throws an error if passed more than one child. +]] +local function oneChild(children) + if not children then + return nil + end + + local key, child = next(children) + + if not child then + return nil + end + + local after = next(children, key) + + if after then + error("Expected at most child, had more than one child.", 2) + end + + return child +end + +return oneChild \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/oneChild.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/oneChild.spec.lua new file mode 100644 index 0000000..6540ce2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/oneChild.spec.lua @@ -0,0 +1,35 @@ +return function() + local createElement = require(script.Parent.createElement) + + local oneChild = require(script.Parent.oneChild) + + it("should get zero children from a table", function() + local children = {} + + expect(oneChild(children)).to.equal(nil) + end) + + it("should get exactly one child", function() + local child = createElement("Frame") + local children = { + foo = child, + } + + expect(oneChild(children)).to.equal(child) + end) + + it("should error with more than one child", function() + local children = { + a = createElement("Frame"), + b = createElement("Frame"), + } + + expect(function() + oneChild(children) + end).to.throw() + end) + + it("should handle being passed nil", function() + expect(oneChild(nil)).to.equal(nil) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/strict.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/strict.lua new file mode 100644 index 0000000..c1d21a5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/strict.lua @@ -0,0 +1,27 @@ +local function strict(t, name) + name = name or tostring(t) + + return setmetatable(t, { + __index = function(self, key) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + + __newindex = function(self, key, value) + local message = ("%q (%s) is not a valid member of %s"):format( + tostring(key), + typeof(key), + name + ) + + error(message, 2) + end, + }) +end + +return strict \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/strict.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/strict.spec.lua new file mode 100644 index 0000000..fc44bff --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_roact/roact/strict.spec.lua @@ -0,0 +1,25 @@ +return function() + local strict = require(script.Parent.strict) + + it("should error when getting a nonexistent key", function() + local t = strict({ + a = 1, + b = 2, + }) + + expect(function() + return t.c + end).to.throw() + end) + + it("should error when setting a nonexistent key", function() + local t = strict({ + a = 1, + b = 2, + }) + + expect(function() + t.c = 3 + end).to.throw() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/lock.toml new file mode 100644 index 0000000..ac51832 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/rodux" +version = "1.0.0" +commit = "416e8042bd6eac7a385cb0955079501fd44f11e1" +source = "url+https://github.com/roblox/rodux" diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/NoYield.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/NoYield.lua new file mode 100644 index 0000000..f9519f1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/NoYield.lua @@ -0,0 +1,29 @@ +--[[ + Calls a function and throws an error if it attempts to yield. + + Pass any number of arguments to the function after the callback. + + This function supports multiple return; all results returned from the + given function will be returned. +]] + +local function resultHandler(co, ok, ...) + if not ok then + local message = (...) + error(debug.traceback(co, message), 2) + end + + if coroutine.status(co) ~= "dead" then + error(debug.traceback(co, "Attempted to yield inside changed event!"), 2) + end + + return ... +end + +local function NoYield(callback, ...) + local co = coroutine.create(callback) + + return resultHandler(co, coroutine.resume(co, ...)) +end + +return NoYield \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/NoYield.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/NoYield.spec.lua new file mode 100644 index 0000000..f1e7cf0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/NoYield.spec.lua @@ -0,0 +1,56 @@ +return function() + local NoYield = require(script.Parent.NoYield) + + it("should call functions normally", function() + local callCount = 0 + + local function test(a, b) + expect(a).to.equal(5) + expect(b).to.equal(6) + + callCount = callCount + 1 + + return 11, "hello" + end + + local a, b = NoYield(test, 5, 6) + + expect(a).to.equal(11) + expect(b).to.equal("hello") + end) + + it("should throw on yield", function() + local preCount = 0 + local postCount = 0 + + local function testMethod() + preCount = preCount + 1 + wait() + postCount = postCount + 1 + end + + local ok, err = pcall(NoYield, testMethod) + + expect(preCount).to.equal(1) + expect(postCount).to.equal(0) + + expect(ok).to.equal(false) + expect(err:find("Attempted to yield inside changed event!")).to.be.ok() + expect(err:find("NoYield.spec")).to.be.ok() + end) + + it("should propagate error messages", function() + local count = 0 + + local function test() + count = count + 1 + error("foo") + end + + local ok, err = pcall(NoYield, test) + + expect(ok).to.equal(false) + expect(err:find("foo")).to.be.ok() + expect(err:find("NoYield.spec")).to.be.ok() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Signal.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Signal.lua new file mode 100644 index 0000000..dc4d041 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Signal.lua @@ -0,0 +1,75 @@ +--[[ + A limited, simple implementation of a Signal. + + Handlers are fired in order, and (dis)connections are properly handled when + executing an event. +]] + +local function immutableAppend(list, ...) + local new = {} + local len = #list + + for key = 1, len do + new[key] = list[key] + end + + for i = 1, select("#", ...) do + new[len + i] = select(i, ...) + end + + return new +end + +local function immutableRemoveValue(list, removeValue) + local new = {} + + for i = 1, #list do + if list[i] ~= removeValue then + table.insert(new, list[i]) + end + end + + return new +end + +local Signal = {} + +Signal.__index = Signal + +function Signal.new() + local self = { + _listeners = {} + } + + setmetatable(self, Signal) + + return self +end + +function Signal:connect(callback) + local listener = { + callback = callback, + disconnected = false, + } + + self._listeners = immutableAppend(self._listeners, listener) + + local function disconnect() + listener.disconnected = true + self._listeners = immutableRemoveValue(self._listeners, listener) + end + + return { + disconnect = disconnect + } +end + +function Signal:fire(...) + for _, listener in ipairs(self._listeners) do + if not listener.disconnected then + listener.callback(...) + end + end +end + +return Signal \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Signal.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Signal.spec.lua new file mode 100644 index 0000000..f00f947 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Signal.spec.lua @@ -0,0 +1,114 @@ +return function() + local Signal = require(script.Parent.Signal) + + it("should construct from nothing", function() + local signal = Signal.new() + + expect(signal).to.be.ok() + end) + + it("should fire connected callbacks", function() + local callCount = 0 + local value1 = "Hello World" + local value2 = 7 + + local callback = function(arg1, arg2) + expect(arg1).to.equal(value1) + expect(arg2).to.equal(value2) + callCount = callCount + 1 + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + + connection:disconnect() + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + end) + + it("should disconnect handlers", function() + local callback = function() + error("Callback was called after disconnect!") + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + connection:disconnect() + + signal:fire() + end) + + it("should fire handlers in order", function() + local signal = Signal.new() + local x = 0 + local y = 0 + + local callback1 = function() + expect(x).to.equal(0) + expect(y).to.equal(0) + x = x + 1 + end + + local callback2 = function() + expect(x).to.equal(1) + expect(y).to.equal(0) + y = y + 1 + end + + signal:connect(callback1) + signal:connect(callback2) + signal:fire() + + expect(x).to.equal(1) + expect(y).to.equal(1) + end) + + it("should continue firing despite mid-event disconnection", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionA + connectionA = signal:connect(function() + connectionA:disconnect() + countA = countA + 1 + end) + + signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(1) + end) + + it("should skip listeners that were disconnected during event evaluation", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionB + + signal:connect(function() + countA = countA + 1 + connectionB:disconnect() + end) + + connectionB = signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(0) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Store.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Store.lua new file mode 100644 index 0000000..90aa02f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Store.lua @@ -0,0 +1,131 @@ +local RunService = game:GetService("RunService") + +local Signal = require(script.Parent.Signal) +local NoYield = require(script.Parent.NoYield) + +local Store = {} + +-- This value is exposed as a private value so that the test code can stay in +-- sync with what event we listen to for dispatching the Changed event. +-- It may not be Heartbeat in the future. +Store._flushEvent = RunService.Heartbeat + +Store.__index = Store + +--[[ + Create a new Store whose state is transformed by the given reducer function. + + Each time an action is dispatched to the store, the new state of the store + is given by: + + state = reducer(state, action) + + Reducers do not mutate the state object, so the original state is still + valid. +]] +function Store.new(reducer, initialState, middlewares) + assert(typeof(reducer) == "function", "Bad argument #1 to Store.new, expected function.") + assert(middlewares == nil or typeof(middlewares) == "table", "Bad argument #3 to Store.new, expected nil or table.") + + local self = {} + + self._reducer = reducer + self._state = reducer(initialState, { + type = "@@INIT", + }) + self._lastState = self._state + + self._mutatedSinceFlush = false + self._connections = {} + + self.changed = Signal.new() + + setmetatable(self, Store) + + local connection = self._flushEvent:Connect(function() + self:flush() + end) + table.insert(self._connections, connection) + + if middlewares then + local unboundDispatch = self.dispatch + local dispatch = function(...) + return unboundDispatch(self, ...) + end + + for i = #middlewares, 1, -1 do + local middleware = middlewares[i] + dispatch = middleware(dispatch, self) + end + + self.dispatch = function(self, ...) + return dispatch(...) + end + end + + return self +end + +--[[ + Get the current state of the Store. Do not mutate this! +]] +function Store:getState() + return self._state +end + +--[[ + Dispatch an action to the store. This allows the store's reducer to mutate + the state of the application by creating a new copy of the state. + + Listeners on the changed event of the store are notified when the state + changes, but not necessarily on every Dispatch. +]] +function Store:dispatch(action) + if typeof(action) == "table" then + if action.type == nil then + error("action does not have a type field", 2) + end + + self._state = self._reducer(self._state, action) + self._mutatedSinceFlush = true + else + error(("actions of type %q are not permitted"):format(typeof(action)), 2) + end +end + +--[[ + Marks the store as deleted, disconnecting any outstanding connections. +]] +function Store:destruct() + for _, connection in ipairs(self._connections) do + connection:Disconnect() + end + + self._connections = nil +end + +--[[ + Flush all pending actions since the last change event was dispatched. +]] +function Store:flush() + if not self._mutatedSinceFlush then + return + end + + self._mutatedSinceFlush = false + + -- On self.changed:fire(), further actions may be immediately dispatched, in + -- which case self._lastState will be set to the most recent self._state, + -- unless we cache this value first + local state = self._state + + -- If a changed listener yields, *very* surprising bugs can ensue. + -- Because of that, changed listeners cannot yield. + NoYield(function() + self.changed:fire(state, self._lastState) + end) + + self._lastState = state +end + +return Store diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Store.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Store.spec.lua new file mode 100644 index 0000000..5e7a5fd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/Store.spec.lua @@ -0,0 +1,342 @@ +return function() + local Store = require(script.Parent.Store) + + describe("new", function() + it("should instantiate with a reducer", function() + local store = Store.new(function(state, action) + return "hello, world" + end) + + expect(store).to.be.ok() + expect(store:getState()).to.equal("hello, world") + + store:destruct() + end) + + it("should instantiate with a reducer and an initial state", function() + local store = Store.new(function(state, action) + return state + end, "initial state") + + expect(store).to.be.ok() + expect(store:getState()).to.equal("initial state") + + store:destruct() + end) + + it("should instantiate with a reducer, initial state, and middlewares", function() + local store = Store.new(function(state, action) + return state + end, "initial state", {}) + + expect(store).to.be.ok() + expect(store:getState()).to.equal("initial state") + + store:destruct() + end) + + it("should modify the dispatch method when middlewares are passed", function() + local middlewareInstantiateCount = 0 + local middlewareInvokeCount = 0 + local passedDispatch + local passedStore + local passedAction + + local function reducer(state, action) + if action.type == "test" then + return "test state" + end + + return state + end + + local function testMiddleware(nextDispatch, store) + middlewareInstantiateCount = middlewareInstantiateCount + 1 + passedDispatch = nextDispatch + passedStore = store + + return function(action) + middlewareInvokeCount = middlewareInvokeCount + 1 + passedAction = action + + nextDispatch(action) + end + end + + local store = Store.new(reducer, "initial state", { testMiddleware }) + + expect(middlewareInstantiateCount).to.equal(1) + expect(middlewareInvokeCount).to.equal(0) + expect(passedDispatch).to.be.a("function") + expect(passedStore).to.equal(store) + + store:dispatch({ + type = "test", + }) + + expect(middlewareInstantiateCount).to.equal(1) + expect(middlewareInvokeCount).to.equal(1) + expect(passedAction.type).to.equal("test") + + store:flush() + + expect(store:getState()).to.equal("test state") + + store:destruct() + end) + + it("should execute middleware left-to-right", function() + local events = {} + + local function reducer(state) + return state + end + + local function middlewareA(nextDispatch, store) + table.insert(events, "instantiate a") + return function(action) + table.insert(events, "execute a") + return nextDispatch(action) + end + end + + local function middlewareB(nextDispatch, store) + table.insert(events, "instantiate b") + return function(action) + table.insert(events, "execute b") + return nextDispatch(action) + end + end + + local store = Store.new(reducer, 5, { middlewareA, middlewareB }) + + expect(#events).to.equal(2) + expect(events[1]).to.equal("instantiate b") + expect(events[2]).to.equal("instantiate a") + + store:dispatch({ + type = "test", + }) + + expect(#events).to.equal(4) + expect(events[3]).to.equal("execute a") + expect(events[4]).to.equal("execute b") + end) + + it("should send an initial action with a 'type' field", function() + local lastAction + local callCount = 0 + + local store = Store.new(function(state, action) + lastAction = action + callCount = callCount + 1 + + return state + end) + + expect(callCount).to.equal(1) + expect(lastAction).to.be.a("table") + expect(lastAction.type).to.be.ok() + + store:destruct() + end) + end) + + describe("getState", function() + it("should get the current state", function() + local store = Store.new(function(state, action) + return "foo" + end) + + local state = store:getState() + + expect(state).to.equal("foo") + + store:destruct() + end) + end) + + describe("dispatch", function() + it("should be sent through the reducer", function() + local store = Store.new(function(state, action) + state = state or "foo" + + if action.type == "act" then + return "bar" + end + + return state + end) + + expect(store).to.be.ok() + expect(store:getState()).to.equal("foo") + + store:dispatch({ + type = "act", + }) + + store:flush() + + expect(store:getState()).to.equal("bar") + + store:destruct() + end) + + it("should trigger the changed event after a flush", function() + local store = Store.new(function(state, action) + state = state or 0 + + if action.type == "increment" then + return state + 1 + end + + return state + end) + + local callCount = 0 + + store.changed:connect(function(state, oldState) + expect(oldState).to.equal(0) + expect(state).to.equal(1) + + callCount = callCount + 1 + end) + + store:dispatch({ + type = "increment", + }) + + store:flush() + + expect(callCount).to.equal(1) + + store:destruct() + end) + + it("should handle actions dispatched within the changed event", function() + local store = Store.new(function(state, action) + state = state or { + value = 0, + } + + if action.type == "increment" then + return { + value = state.value + 1, + } + elseif action.type == "decrement" then + return { + value = state.value - 1, + } + end + + return state + end) + + local changeCount = 0 + + store.changed:connect(function(state, oldState) + expect(state).never.to.equal(oldState) + + if state.value > 0 then + store:dispatch({ + type = "decrement", + }) + end + + changeCount = changeCount + 1 + end) + + store:dispatch({ + type = "increment", + }) + store:flush() + store:flush() + + expect(changeCount).to.equal(2) + + store:destruct() + end) + + it("should prevent yielding from changed handler", function() + local preCount = 0 + local postCount = 0 + + local store = Store.new(function(state, action) + state = state or 0 + return state + 1 + end) + + store.changed:connect(function(state, oldState) + preCount = preCount + 1 + wait() + postCount = postCount + 1 + end) + + store:dispatch({ + type = "increment", + }) + + expect(function() + store:flush() + end).to.throw() + + expect(preCount).to.equal(1) + expect(postCount).to.equal(0) + + store:destruct() + end) + + it("should throw if an action is dispatched without a type field", function() + local store = Store.new(function(state, action) + return state + end) + + expect(function() + store:dispatch({}) + end).to.throw() + + store:destruct() + end) + + it("should throw if the action is not a function or table", function() + local store = Store.new(function(state, action) + return state + end) + + expect(function() + store:dispatch(1) + end).to.throw() + + store:destruct() + end) + end) + + describe("flush", function() + it("should not fire a changed event if there were no dispatches", function() + local store = Store.new(function() + end) + + local count = 0 + store.changed:connect(function() + count = count + 1 + end) + + store:flush() + + expect(count).to.equal(0) + + store:dispatch({ + type = "increment", + }) + store:flush() + + expect(count).to.equal(1) + + store:flush() + + expect(count).to.equal(1) + + store:destruct() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/combineReducers.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/combineReducers.lua new file mode 100644 index 0000000..f3023c4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/combineReducers.lua @@ -0,0 +1,22 @@ +--[[ + Create a composite reducer from a map of keys and sub-reducers. +]] +local function combineReducers(map) + return function(state, action) + -- If state is nil, substitute it with a blank table. + if state == nil then + state = {} + end + + local newState = {} + + for key, reducer in pairs(map) do + -- Each reducer gets its own state, not the entire state table + newState[key] = reducer(state[key], action) + end + + return newState + end +end + +return combineReducers diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/combineReducers.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/combineReducers.spec.lua new file mode 100644 index 0000000..a3a85af --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/combineReducers.spec.lua @@ -0,0 +1,52 @@ +return function() + local combineReducers = require(script.Parent.combineReducers) + + it("should invoke each sub-reducer for every action", function() + local aCount = 0 + local bCount = 0 + + local reducer = combineReducers({ + a = function(state, action) + aCount = aCount + 1 + end, + b = function(state, action) + bCount = bCount + 1 + end, + }) + + -- Mock reducer invocation + reducer({}, {}) + expect(aCount).to.equal(1) + expect(bCount).to.equal(1) + end) + + it("should assign each sub-reducer's value to the new state", function() + local reducer = combineReducers({ + a = function(state, action) + return (state or 0) + 1 + end, + b = function(state, action) + return (state or 0) + 3 + end, + }) + + local newState = reducer({}, {}) + expect(newState.a).to.equal(1) + expect(newState.b).to.equal(3) + end) + + it("should not throw when state is nil", function() + local reducer = combineReducers({ + a = function(state, action) + return (state or 0) + 1 + end, + b = function(state, action) + return (state or 0) + 3 + end, + }) + + expect(function() + reducer(nil, {}) + end).to.never.throw() + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/createReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/createReducer.lua new file mode 100644 index 0000000..5560b59 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/createReducer.lua @@ -0,0 +1,15 @@ +return function(initialState, handlers) + return function(state, action) + if state == nil then + state = initialState + end + + local handler = handlers[action.type] + + if handler then + return handler(state, action) + end + + return state + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/createReducer.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/createReducer.spec.lua new file mode 100644 index 0000000..a704944 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/createReducer.spec.lua @@ -0,0 +1,106 @@ +return function() + local createReducer = require(script.Parent.createReducer) + + it("should handle actions", function() + local reducer = createReducer({ + a = 0, + b = 0, + }, { + a = function(state, action) + return { + a = state.a + 1, + b = state.b, + } + end, + b = function(state, action) + return { + a = state.a, + b = state.b + 2, + } + end, + }) + + local newState = reducer({ + a = 0, + b = 0, + }, { + type = "a", + }) + + expect(newState.a).to.equal(1) + + newState = reducer(newState, { + type = "b", + }) + + expect(newState.b).to.equal(2) + end) + + it("should return the initial state if the state is nil", function() + local reducer = createReducer({ + a = 0, + b = 0, + -- We don't care about the actions here + }, {}) + + local newState = reducer(nil, {}) + expect(newState).to.be.ok() + expect(newState.a).to.equal(0) + expect(newState.b).to.equal(0) + end) + + it("should still run action handlers if the state is nil", function() + local callCount = 0 + + local reducer = createReducer(0, { + foo = function(state, action) + callCount = callCount + 1 + return nil + end + }) + + expect(callCount).to.equal(0) + + local newState = reducer(nil, { + type = "foo", + }) + + expect(callCount).to.equal(1) + expect(newState).to.equal(nil) + + newState = reducer(newState, { + type = "foo", + }) + + expect(callCount).to.equal(2) + expect(newState).to.equal(nil) + end) + + it("should return the same state if the action is not handled", function() + local initialState = { + a = 0, + b = 0, + } + + local reducer = createReducer(initialState, { + a = function(state, action) + return { + a = state.a + 1, + b = state.b, + } + end, + b = function(state, action) + return { + a = state.a, + b = state.b + 2, + } + end, + }) + + local newState = reducer(initialState, { + type = "c", + }) + + expect(newState).to.equal(initialState) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/init.lua new file mode 100644 index 0000000..acef1df --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/init.lua @@ -0,0 +1,13 @@ +local Store = require(script.Store) +local createReducer = require(script.createReducer) +local combineReducers = require(script.combineReducers) +local loggerMiddleware = require(script.loggerMiddleware) +local thunkMiddleware = require(script.thunkMiddleware) + +return { + Store = Store, + createReducer = createReducer, + combineReducers = combineReducers, + loggerMiddleware = loggerMiddleware.middleware, + thunkMiddleware = thunkMiddleware, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/init.spec.lua new file mode 100644 index 0000000..14a24ef --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/init.spec.lua @@ -0,0 +1,9 @@ +return function() + describe("Rodux", function() + it("should load", function() + local Rodux = require(script.Parent) + + expect(Rodux.Store).to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/loggerMiddleware.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/loggerMiddleware.lua new file mode 100644 index 0000000..9bc922b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/loggerMiddleware.lua @@ -0,0 +1,55 @@ +local indent = " " + +local function prettyPrint(value, indentLevel) + indentLevel = indentLevel or 0 + local output = {} + + if typeof(value) == "table" then + table.insert(output, "{\n") + + for key, value in pairs(value) do + table.insert(output, indent:rep(indentLevel + 1)) + table.insert(output, tostring(key)) + table.insert(output, " = ") + + table.insert(output, prettyPrint(value, indentLevel + 1)) + table.insert(output, "\n") + end + + table.insert(output, indent:rep(indentLevel)) + table.insert(output, "}") + elseif typeof(value) == "string" then + table.insert(output, string.format("%q", value)) + table.insert(output, " (string)") + else + table.insert(output, tostring(value)) + table.insert(output, " (") + table.insert(output, typeof(value)) + table.insert(output, ")") + end + + return table.concat(output, "") +end + +-- We want to be able to override outputFunction in tests, so the shape of this +-- module is kind of unconventional. +-- +-- We fix it this weird shape in init.lua. +local loggerMiddleware = { + outputFunction = print, +} + +function loggerMiddleware.middleware(nextDispatch, store) + return function(action) + local result = nextDispatch(action) + + loggerMiddleware.outputFunction(("Action dispatched: %s\nState changed to: %s"):format( + prettyPrint(action), + prettyPrint(store:getState()) + )) + + return result + end +end + +return loggerMiddleware diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/loggerMiddleware.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/loggerMiddleware.spec.lua new file mode 100644 index 0000000..2ec3ea3 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/loggerMiddleware.spec.lua @@ -0,0 +1,39 @@ +return function() + local Store = require(script.Parent.Store) + local loggerMiddleware = require(script.Parent.loggerMiddleware) + + it("should print whenever an action is dispatched", function() + local outputCount = 0 + local outputMessage + + local function reducer(state, action) + return state + end + + local store = Store.new(reducer, { + fooValue = 12345, + barValue = { + bazValue = "hiBaz", + }, + }, { loggerMiddleware.middleware }) + + loggerMiddleware.outputFunction = function(message) + outputCount = outputCount + 1 + outputMessage = message + end + + store:dispatch({ + type = "testActionType", + }) + + expect(outputCount).to.equal(1) + expect(outputMessage:find("testActionType")).to.be.ok() + expect(outputMessage:find("fooValue")).to.be.ok() + expect(outputMessage:find("12345")).to.be.ok() + expect(outputMessage:find("barValue")).to.be.ok() + expect(outputMessage:find("bazValue")).to.be.ok() + expect(outputMessage:find("hiBaz")).to.be.ok() + + loggerMiddleware.outputFunction = print + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/thunkMiddleware.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/thunkMiddleware.lua new file mode 100644 index 0000000..08c676b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/thunkMiddleware.lua @@ -0,0 +1,17 @@ +--[[ + A middleware that allows for functions to be dispatched. + Functions will receive a single argument, the store itself. + This middleware consumes the function; middleware further down the chain + will not receive it. +]] +local function thunkMiddleware(nextDispatch, store) + return function(action) + if typeof(action) == "function" then + return action(store) + else + return nextDispatch(action) + end + end +end + +return thunkMiddleware diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/thunkMiddleware.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/thunkMiddleware.spec.lua new file mode 100644 index 0000000..8f717e4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_rodux/rodux/thunkMiddleware.spec.lua @@ -0,0 +1,58 @@ +return function() + local Store = require(script.Parent.Store) + local thunkMiddleware = require(script.Parent.thunkMiddleware) + + it("should dispatch thunks", function() + local function reducer(state, action) + return state + end + + local store = Store.new(reducer, {}, { thunkMiddleware }) + local thunkCount = 0 + + local function thunk(store) + thunkCount = thunkCount + 1 + end + + store:dispatch(thunk) + + expect(thunkCount).to.equal(1) + end) + + it("should allow normal actions to pass through", function() + local reducerCount = 0 + + local function reducer(state, action) + reducerCount = reducerCount + 1 + return state + end + + local store = Store.new(reducer, {}, { thunkMiddleware }) + + store:dispatch({ + type = "test", + }) + + -- Reducer will be invoked twice: + -- Once when creating the store (@@INIT action) + -- Once when the test action is dispatched + expect(reducerCount).to.equal(2) + end) + + it("should return the value from the thunk", function() + local function reducer(state, action) + return state + end + + local store = Store.new(reducer, {}, { thunkMiddleware }) + local thunkValue = "test" + + local function thunk(store) + return thunkValue + end + + local result = store:dispatch(thunk) + + expect(result).to.equal(thunkValue) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/lock.toml new file mode 100644 index 0000000..080eca2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/string-utilities" +version = "1.0.0" +commit = "7cbef7885ca023d71dd93f02ffa32cbda65faa9c" +source = "url+https://github.com/roblox/string-utilities" diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/ParseQuery.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/ParseQuery.lua new file mode 100644 index 0000000..e3e0d8b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/ParseQuery.lua @@ -0,0 +1,84 @@ +--!nocheck +local RunService = game:GetService("RunService") +local StringSplit = require(script.Parent.StringSplit) + +function assertIsType(value, expectedType, name) + if RunService:IsStudio() or _G.__TESTEZ_RUNNING_TEST__ then + assert( + typeof(value) == expectedType, + string.format("expects %s to be a %s! it was: %s", name, expectedType, typeof(value)) + ) + end +end + +local function urlDecode(input) + assertIsType(input, "string", "input") + local decoded = string.gsub(input, '%%(%x%x)', function(charCode) + return string.char(tonumber(charCode, 16)) + end) + decoded = string.gsub(decoded, "+", " ") + return decoded; +end + +--[[ + Parses a query string into a lua table. + * supports both "&"" and ";" as separator (and "=" separates names/values) + * empty names or values are returned as "" (eg "k1=&=v2&k3") + * completely empty pairs are ignored (eg "k1=v1&&k3=v3") + * url decodes all names and values + * supports multiple values + * by default, all values are returned in tables + * if listKeyMapper (name => listKey) is provided: + * only the last value for a name is returned as that key (ie name) + * the list of values is returned at the key provided by listKeyMapper + * listKeyMapper can return the same input key (ie name), or nil (same effect) +]] +local function ParseQuery(input, listKeyMapper) + if input ~= nil then + assertIsType(input, "string", "input") + end + if listKeyMapper ~= nil then + assertIsType(listKeyMapper, "function", "listKeyMapper") + end + + local useListKeys = type(listKeyMapper) == "function" + local parsed = {} + if input and #input > 0 then + local items = StringSplit(input, "[&;]") + for _, item in ipairs(items) do + if item and #item > 0 then + local key, value = unpack(string.split(item, "=")) + if value == nil then + value = "" + end + key = urlDecode(key) + value = urlDecode(value) + if useListKeys then + if parsed[key] ~= nil then + local listKey = listKeyMapper(key) + if listKey == nil then + listKey = key + end + if type(parsed[listKey]) ~= "table" then + parsed[listKey] = { parsed[key] } + end + table.insert(parsed[listKey], value) + if key ~= listKey then + parsed[key] = value + end + else + parsed[key] = value + end + else + if parsed[key] == nil then + parsed[key] = {} + end + table.insert(parsed[key], value) + end + end + end + end + return parsed +end + +return ParseQuery diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/ParseQuery.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/ParseQuery.spec.lua new file mode 100644 index 0000000..79014d0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/ParseQuery.spec.lua @@ -0,0 +1,74 @@ +return function() + local ParseQuery = require(script.Parent.ParseQuery) + + describe("ParseQuery normal usage", function() + it("should parse a normal query string", function() + local parsed = ParseQuery("name1=value1&name2=value2") + + expect(parsed.name1[1]).to.equal("value1") + expect(parsed.name2[1]).to.equal("value2") + end) + + it("should parse multiple values for the same name", function() + local parsed = ParseQuery("name1=value1&name1=value2") + + expect(#parsed.name1).to.equal(2) + expect(parsed.name1[1]).to.equal("value1") + expect(parsed.name1[2]).to.equal("value2") + end) + + it("should allow custom keys for multiple values", function() + local parsed = ParseQuery("name1=value1&name2=value2&name2=value3", function(key) + return "list_of_" .. key + end) + + expect(parsed.name1).to.equal("value1") + expect(parsed.name2).to.equal("value3") + + expect(#parsed.list_of_name2).to.equal(2) + expect(parsed.list_of_name2[1]).to.equal("value2") + expect(parsed.list_of_name2[2]).to.equal("value3") + end) + + it("should url decode names and values", function() + local parsed = ParseQuery("name1=value+1&%5Fname2=value2") + + expect(parsed.name1[1]).to.equal("value 1") + expect(parsed._name2[1]).to.equal("value2") + end) + end) + + describe("ParseQuery edge cases", function() + it("should return an empty table on empty or nil input", function() + local parsed1 = ParseQuery("") + local parsed2 = ParseQuery(nil) + + expect(#parsed1).to.equal(0) + expect(#parsed2).to.equal(0) + end) + + it("should support listKeyMapper returning key or nil", function() + local parsed = ParseQuery("name1=value1&name2=value2&name2=value3", function()end) + + expect(parsed.name1).to.equal("value1") + expect(#parsed.name2).to.equal(2) + expect(parsed.name2[1]).to.equal("value2") + expect(parsed.name2[2]).to.equal("value3") + end) + + it("should use empty strings for unavailable values", function() + local parsed = ParseQuery("name1=&=value2&name3", function()end) + + expect(parsed.name1).to.equal("") + expect(parsed[""]).to.equal("value2") + expect(parsed.name3).to.equal("") + end) + + it("should support `;` as separator", function() + local parsed = ParseQuery("name1=value1;name2=value2", function()end) + + expect(parsed.name1).to.equal("value1") + expect(parsed.name2).to.equal("value2") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringReplaceAll.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringReplaceAll.lua new file mode 100644 index 0000000..1f6560c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringReplaceAll.lua @@ -0,0 +1,26 @@ +-- Performs multiple replacements on the given string. +-- replacements is a table where the keys are patterns to be replaced, and the +-- values are the replacements to be made. For example: +-- StringReplaceAll( +-- "key1 key2 key3", +-- {["%w+1"] = "value1", ["%w+2"] = "value2"} +-- ) +-- becomes "value1 value2 key3". +return function(str, replacements) + if type(str) ~= "string" then + return "" + end + + if type(replacements) ~= "table" then + return str + end + + local result = str + for piiStr, replaceStr in pairs(replacements) do + if type(piiStr) == "string" and type(replaceStr) == "string" then + result = string.gsub(result, piiStr, replaceStr) + end + end + + return result +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringReplaceAll.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringReplaceAll.spec.lua new file mode 100644 index 0000000..0a8cf74 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringReplaceAll.spec.lua @@ -0,0 +1,78 @@ +return function() + local StringReplaceAll = require(script.Parent.StringReplaceAll) + + it("should return empty string if str is not a string", function() + local result = StringReplaceAll(1, { "a" }) + expect(result).to.equal("") + + result = StringReplaceAll({ 1, 2, 3 }, { "a" }) + expect(result).to.equal("") + + result = StringReplaceAll(nil, { "a" }) + expect(result).to.equal("") + end) + + it("should return original string if replacements is not a table", function() + local result = StringReplaceAll("abc", "a") + expect(result).to.equal("abc") + + result = StringReplaceAll("abc", 1) + expect(result).to.equal("abc") + + result = StringReplaceAll("abc", nil) + expect(result).to.equal("abc") + end) + + it("should replace a PII string if provided", function() + local result = StringReplaceAll("abc", { a = "" }) + expect(result).to.equal("bc") + + result = StringReplaceAll("abc", { b = "" }) + expect(result).to.equal("ac") + + result = StringReplaceAll("abc-123-a2c", { ["2"] = "" }) + expect(result).to.equal("abc-13-ac") + + result = StringReplaceAll("abc-123-a2c", { a = "A" }) + expect(result).to.equal("Abc-123-A2c") + + -- special character + result = StringReplaceAll("hello, said Noob_123", { ["Noob_123"] = "Username" }) + expect(result).to.equal("hello, said Username") + end) + + it("should replace all PII strings provided", function() + local result = StringReplaceAll("abc", { a = "A", c = "d"}) + expect(result).to.equal("Abd") + + result = StringReplaceAll("abc", { a = "A", bc = "" }) + expect(result).to.equal("A") + + result = StringReplaceAll("abc-123-a23", { abc = "user", ["123"] = "id" }) + expect(result).to.equal("user-id-a23") + end) + + it("should return original string if replacements is an empty table or no matches are found", function() + local result = StringReplaceAll("abc", {}) + expect(result).to.equal("abc") + + result = StringReplaceAll("abc", { d = "e" }) + expect(result).to.equal("abc") + + result = StringReplaceAll("abc", { d = "e", e = "f" }) + expect(result).to.equal("abc") + end) + + it("should ignore PIIs if they are not strings", function() + local result = StringReplaceAll("abc", { a = 1 }) + expect(result).to.equal("abc") + + result = StringReplaceAll("abc", { a = 1, bc = "" }) + expect(result).to.equal("a") + end) + + it("should match the example given in the doc string", function() + local result = StringReplaceAll("key1 key2 key3", {["%w+1"] = "value1", ["%w+2"] = "value2"}) + expect(result).to.equal("value1 value2 key3") + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringSplit.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringSplit.lua new file mode 100644 index 0000000..12d8a1c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringSplit.lua @@ -0,0 +1,45 @@ +-- Split the input string according to the separator pattern into at most limit +-- pieces. If the separator is nil, it defaults to consecutive whitespace (%s+). +-- If the limit isn't defined, it will split the string into as many pieces as +-- it finds. +-- Note: There is now a string.split function built into Luau though it isn't an +-- exact replacement for this. +local function StringSplit(input, separator, limit) + if #input == 0 then + if string.find(input, separator) then + return {} + else + return {""} + end + end + if not limit then + limit = -1 + end + if limit == 1 then + return {input} + end + if not separator then + separator = "%s+" + end + local start, stop = string.find(input, separator) + if not start then + return {input} + end + -- special case, delimiter resolved to "" + if stop < 1 then + start, stop = string.find(string.sub(input, 2), separator) + start = start + 1 + stop = stop + 1 + end + local first = string.sub(input, 1, start - 1) + local rest = string.sub(input, stop + 1) + -- special case, non empty pattern found at the end + if #rest == 0 and stop >= start then + return {first, rest} + end + local items = StringSplit(rest, separator, limit - 1) + table.insert(items, 1, first) + return items +end + +return StringSplit diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringSplit.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringSplit.spec.lua new file mode 100644 index 0000000..48d0bcc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringSplit.spec.lua @@ -0,0 +1,108 @@ +return function() + local StringSplit = require(script.Parent.StringSplit) + + describe("Normal usage", function() + + it("should split a string by a one character separator", function() + local str = "Roblox Powering Imagination" + local words = StringSplit(str, " ") + + expect(#words).to.equal(3) + expect(words[1]).to.equal("Roblox") + expect(words[2]).to.equal("Powering") + expect(words[3]).to.equal("Imagination") + end) + + it("should split a string by a complex regex", function() + local str = "https://corp.roblox.com/technology" + local words = StringSplit(str, "[^a-z]+") + + expect(#words).to.equal(5) + expect(words[1]).to.equal("https") + expect(words[2]).to.equal("corp") + expect(words[3]).to.equal("roblox") + expect(words[4]).to.equal("com") + expect(words[5]).to.equal("technology") + end) + + it("should split on blank spaces by default", function() + local str = "together through\n\tplay" + local words = StringSplit(str) + + expect(#words).to.equal(3) + expect(words[1]).to.equal("together") + expect(words[2]).to.equal("through") + expect(words[3]).to.equal("play") + end) + + it("should not exceed the provided limit", function() + local str = "Modules/Common/StringUtilities/StringSplit" + local words = StringSplit(str, "/", 3) + + expect(#words).to.equal(3) + expect(words[1]).to.equal("Modules") + expect(words[2]).to.equal("Common") + expect(words[3]).to.equal("StringUtilities/StringSplit") + end) + + it("should give en empty string on leading, repeated and trailing separators", function() + local str = "/var//www/" + local words = StringSplit(str, "/") + + expect(#words).to.equal(5) + expect(words[1]).to.equal("") + expect(words[2]).to.equal("var") + expect(words[3]).to.equal("") + expect(words[4]).to.equal("www") + expect(words[5]).to.equal("") + end) + + end) + + describe("Edge case:", function() + + it("an empty string results in one empty result", function() + local str = "" + local words = StringSplit(str, "/") + + expect(#words).to.equal(1) + expect(words[1]).to.equal("") + end) + + it("an empty separator splits to characters, no leading/trailing empty strings", function() + local str = "lua" + local words = StringSplit(str, "") + + expect(#words).to.equal(3) + expect(words[1]).to.equal("l") + expect(words[2]).to.equal("u") + expect(words[3]).to.equal("a") + end) + + it("empty string AND separator should get an empty array", function() + local str = "" + local words = StringSplit(str, "") + + expect(#words).to.equal(0) + end) + + it("regex separators can resolve to empty and non empty in same operation", function() + local str = "//r#blox" + local words = StringSplit(str, "[^a-z]*") + + expect(#words).to.equal(6) + -- pattern resolved to "//", creating a leading empty string + expect(words[1]).to.equal("") + -- pattern resolved to "#" + expect(words[2]).to.equal("r") + -- pattern resolved to "", splitting by character + expect(words[3]).to.equal("b") + expect(words[4]).to.equal("l") + expect(words[5]).to.equal("o") + expect(words[6]).to.equal("x") + -- pattern resolved to "", NO trailing empty string + end) + + end) + +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringTrim.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringTrim.lua new file mode 100644 index 0000000..c460b47 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringTrim.lua @@ -0,0 +1,34 @@ +-- Trim the given set of characters from both ends of the input string. `sides` +-- can be set to {left = true} or {right = true} to trim from just one side. +local function StringTrim(input, chars, sides) + if not chars then + chars = "%s" + end + if #chars == 0 then + return input + end + if not sides then + sides = { + left = true, + right = true, + } + end + local trimmed = input + if sides.left then + local start = string.find(trimmed, "[^" .. chars .. "]") + if not start then + return "" + end + trimmed = string.sub(trimmed, start) + end + if sides.right then + local stop = string.find(trimmed, "[" .. chars .. "]+$") + if not stop then + return trimmed + end + trimmed = string.sub(trimmed, 1, stop-1) + end + return trimmed +end + +return StringTrim diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringTrim.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringTrim.spec.lua new file mode 100644 index 0000000..e3f85aa --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/StringTrim.spec.lua @@ -0,0 +1,54 @@ +return function() + local StringTrim = require(script.Parent.StringTrim) + + describe("Normal usage", function() + + it("should trim the specified character", function() + local str = "/Modules/Common/StringUtilities/" + local trimmed = StringTrim(str, "/") + + expect(trimmed).to.equal("Modules/Common/StringUtilities") + end) + + it("should accept multiple charcters", function() + local str = "(Roblox Powering Imagination){}" + local trimmed = StringTrim(str, "(){}") + + expect(trimmed).to.equal("Roblox Powering Imagination") + end) + + it("should be able to trim only on the right side", function() + local str = "(Roblox Powering Imagination){}" + local trimmed = StringTrim(str, "(){}", {right = true}) + + expect(trimmed).to.equal("(Roblox Powering Imagination") + end) + + it("should be able to trim only on the left side", function() + local str = "(Roblox Powering Imagination){}" + local trimmed = StringTrim(str, "(){}", {left = true}) + + expect(trimmed).to.equal("Roblox Powering Imagination){}") + end) + + it("should default to trimming blanks on both sides", function() + local str = "\tRoblox Powering Imagination \n" + local trimmed = StringTrim(str) + + expect(trimmed).to.equal("Roblox Powering Imagination") + end) + + end) + + describe("Edge case:", function() + + it("an empty character list is a no-op", function() + local str = " Roblox Powering Imagination " + local trimmed = StringTrim(str, "") + + expect(trimmed).to.equal(str) + end) + + end) + +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/init.lua new file mode 100644 index 0000000..7a39fc7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/init.lua @@ -0,0 +1,6 @@ +return { + ParseQuery = require(script.ParseQuery), + StringReplaceAll = require(script.StringReplaceAll), + StringSplit = require(script.StringSplit), + StringTrim = require(script.StringTrim), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/init.spec.lua new file mode 100644 index 0000000..a0faf51 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_string-utilities/string-utilities/init.spec.lua @@ -0,0 +1,5 @@ +return function() + it("should import without error", function() + require(script.Parent) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_t/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_t/lock.toml new file mode 100644 index 0000000..5663f1a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_t/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/t" +version = "1.2.5" +commit = "5d6ee3e23a658b12bc433b0837184215618e233e" +source = "url+https://github.com/roblox/t" diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/init.lua new file mode 100644 index 0000000..fb7248e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/init.lua @@ -0,0 +1,1109 @@ +-- t: a runtime typechecker for Roblox + +-- regular lua compatibility +local typeof = typeof or type + +local function primitive(typeName) + return function(value) + local valueType = typeof(value) + if valueType == typeName then + return true + else + return false, string.format("%s expected, got %s", typeName, valueType) + end + end +end + +local t = {} + +--[[** + matches any type except nil + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.any(value) + if value ~= nil then + return true + else + return false, "any expected, got nil" + end +end + +--Lua primitives + +--[[** + ensures Lua primitive boolean type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.boolean = primitive("boolean") + +--[[** + ensures Lua primitive thread type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.thread = primitive("thread") + +--[[** + ensures Lua primitive callback type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.callback = primitive("function") + +--[[** + ensures Lua primitive none type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.none = primitive("nil") + +--[[** + ensures Lua primitive string type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.string = primitive("string") + +--[[** + ensures Lua primitive table type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.table = primitive("table") + +--[[** + ensures Lua primitive userdata type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.userdata = primitive("userdata") + +--[[** + ensures value is a number and non-NaN + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.number(value) + local valueType = typeof(value) + if valueType == "number" then + if value == value then + return true + else + return false, "unexpected NaN value" + end + else + return false, string.format("number expected, got %s", valueType) + end +end + +--[[** + ensures value is NaN + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.nan(value) + if value ~= value then + return true + else + return false, "unexpected non-NaN value" + end +end + +-- roblox types + +--[[** + ensures Roblox Axes type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Axes = primitive("Axes") + +--[[** + ensures Roblox BrickColor type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.BrickColor = primitive("BrickColor") + +--[[** + ensures Roblox CFrame type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.CFrame = primitive("CFrame") + +--[[** + ensures Roblox Color3 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Color3 = primitive("Color3") + +--[[** + ensures Roblox ColorSequence type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.ColorSequence = primitive("ColorSequence") + +--[[** + ensures Roblox ColorSequenceKeypoint type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.ColorSequenceKeypoint = primitive("ColorSequenceKeypoint") + +--[[** + ensures Roblox DockWidgetPluginGuiInfo type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.DockWidgetPluginGuiInfo = primitive("DockWidgetPluginGuiInfo") + +--[[** + ensures Roblox Faces type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Faces = primitive("Faces") + +--[[** + ensures Roblox Instance type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Instance = primitive("Instance") + +--[[** + ensures Roblox NumberRange type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.NumberRange = primitive("NumberRange") + +--[[** + ensures Roblox NumberSequence type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.NumberSequence = primitive("NumberSequence") + +--[[** + ensures Roblox NumberSequenceKeypoint type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.NumberSequenceKeypoint = primitive("NumberSequenceKeypoint") + +--[[** + ensures Roblox PathWaypoint type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.PathWaypoint = primitive("PathWaypoint") + +--[[** + ensures Roblox PhysicalProperties type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.PhysicalProperties = primitive("PhysicalProperties") + +--[[** + ensures Roblox Random type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Random = primitive("Random") + +--[[** + ensures Roblox Ray type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Ray = primitive("Ray") + +--[[** + ensures Roblox Rect type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Rect = primitive("Rect") + +--[[** + ensures Roblox Region3 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Region3 = primitive("Region3") + +--[[** + ensures Roblox Region3int16 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Region3int16 = primitive("Region3int16") + +--[[** + ensures Roblox TweenInfo type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.TweenInfo = primitive("TweenInfo") + +--[[** + ensures Roblox UDim type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.UDim = primitive("UDim") + +--[[** + ensures Roblox UDim2 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.UDim2 = primitive("UDim2") + +--[[** + ensures Roblox Vector2 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Vector2 = primitive("Vector2") + +--[[** + ensures Roblox Vector3 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Vector3 = primitive("Vector3") + +--[[** + ensures Roblox Vector3int16 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Vector3int16 = primitive("Vector3int16") + +-- roblox enum types + +--[[** + ensures Roblox Enum type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Enum = primitive("Enum") + +--[[** + ensures Roblox EnumItem type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.EnumItem = primitive("EnumItem") + +--[[** + ensures Roblox RBXScriptSignal type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.RBXScriptSignal = primitive("RBXScriptSignal") + +--[[** + ensures Roblox RBXScriptConnection type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.RBXScriptConnection = primitive("RBXScriptConnection") + +--[[** + ensures value is a given literal value + + @param literal The literal to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.literal(...) + local size = select("#", ...) + if size == 1 then + local literal = ... + return function(value) + if value ~= literal then + return false, string.format("expected %s, got %s", tostring(literal), tostring(value)) + end + return true + end + else + local literals = {} + for i = 1, size do + local value = select(i, ...) + literals[i] = t.literal(value) + end + return t.union(unpack(literals)) + end +end + +--[[** + DEPRECATED + Please use t.literal +**--]] +t.exactly = t.literal + +--[[** + Returns a t.union of each key in the table as a t.literal + + @param keyTable The table to get keys from + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.keyOf(keyTable) + local keys = {} + for key in pairs(keyTable) do + keys[#keys + 1] = key + end + return t.literal(unpack(keys)) +end + +--[[** + Returns a t.union of each value in the table as a t.literal + + @param valueTable The table to get values from + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.valueOf(valueTable) + local values = {} + for _, value in pairs(valueTable) do + values[#values + 1] = value + end + return t.literal(unpack(values)) +end + +--[[** + ensures value is an integer + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.integer(value) + local success, errMsg = t.number(value) + if not success then + return false, errMsg or "" + end + if value%1 == 0 then + return true + else + return false, string.format("integer expected, got %s", value) + end +end + +--[[** + ensures value is a number where min <= value + + @param min The minimum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberMin(min) + return function(value) + local success, errMsg = t.number(value) + if not success then + return false, errMsg or "" + end + if value >= min then + return true + else + return false, string.format("number >= %s expected, got %s", min, value) + end + end +end + +--[[** + ensures value is a number where value <= max + + @param max The maximum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberMax(max) + return function(value) + local success, errMsg = t.number(value) + if not success then + return false, errMsg + end + if value <= max then + return true + else + return false, string.format("number <= %s expected, got %s", max, value) + end + end +end + +--[[** + ensures value is a number where min < value + + @param min The minimum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberMinExclusive(min) + return function(value) + local success, errMsg = t.number(value) + if not success then + return false, errMsg or "" + end + if min < value then + return true + else + return false, string.format("number > %s expected, got %s", min, value) + end + end +end + +--[[** + ensures value is a number where value < max + + @param max The maximum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberMaxExclusive(max) + return function(value) + local success, errMsg = t.number(value) + if not success then + return false, errMsg or "" + end + if value < max then + return true + else + return false, string.format("number < %s expected, got %s", max, value) + end + end +end + +--[[** + ensures value is a number where value > 0 + + @returns A function that will return true iff the condition is passed +**--]] +t.numberPositive = t.numberMinExclusive(0) + +--[[** + ensures value is a number where value < 0 + + @returns A function that will return true iff the condition is passed +**--]] +t.numberNegative = t.numberMaxExclusive(0) + +--[[** + ensures value is a number where min <= value <= max + + @param min The minimum to use + @param max The maximum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberConstrained(min, max) + assert(t.number(min) and t.number(max)) + local minCheck = t.numberMin(min) + local maxCheck = t.numberMax(max) + return function(value) + local minSuccess, minErrMsg = minCheck(value) + if not minSuccess then + return false, minErrMsg or "" + end + + local maxSuccess, maxErrMsg = maxCheck(value) + if not maxSuccess then + return false, maxErrMsg or "" + end + + return true + end +end + +--[[** + ensures value is a number where min < value < max + + @param min The minimum to use + @param max The maximum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberConstrainedExclusive(min, max) + assert(t.number(min) and t.number(max)) + local minCheck = t.numberMinExclusive(min) + local maxCheck = t.numberMaxExclusive(max) + return function(value) + local minSuccess, minErrMsg = minCheck(value) + if not minSuccess then + return false, minErrMsg or "" + end + + local maxSuccess, maxErrMsg = maxCheck(value) + if not maxSuccess then + return false, maxErrMsg or "" + end + + return true + end +end + +--[[** + ensures value matches string pattern + + @param string pattern to check against + + @returns A function that will return true iff the condition is passed +**--]] +function t.match(pattern) + assert(t.string(pattern)) + return function(value) + local stringSuccess, stringErrMsg = t.string(value) + if not stringSuccess then + return false, stringErrMsg + end + + if string.match(value, pattern) == nil then + return false, string.format("\"%s\" failed to match pattern \"%s\"", value, pattern) + end + + return true + end +end + +--[[** + ensures value is either nil or passes check + + @param check The check to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.optional(check) + assert(t.callback(check)) + return function(value) + if value == nil then + return true + end + local success, errMsg = check(value) + if success then + return true + else + return false, string.format("(optional) %s", errMsg or "") + end + end +end + +--[[** + matches given tuple against tuple type definition + + @param ... The type definition for the tuples + + @returns A function that will return true iff the condition is passed +**--]] +function t.tuple(...) + local checks = {...} + return function(...) + local args = {...} + for i = 1, #checks do + local success, errMsg = checks[i](args[i]) + if success == false then + return false, string.format("Bad tuple index #%s:\n\t%s", i, errMsg or "") + end + end + return true + end +end + +--[[** + ensures all keys in given table pass check + + @param check The function to use to check the keys + + @returns A function that will return true iff the condition is passed +**--]] +function t.keys(check) + assert(t.callback(check)) + return function(value) + local tableSuccess, tableErrMsg = t.table(value) + if tableSuccess == false then + return false, tableErrMsg or "" + end + + for key in pairs(value) do + local success, errMsg = check(key) + if success == false then + return false, string.format("bad key %s:\n\t%s", tostring(key), errMsg or "") + end + end + + return true + end +end + +--[[** + ensures all values in given table pass check + + @param check The function to use to check the values + + @returns A function that will return true iff the condition is passed +**--]] +function t.values(check) + assert(t.callback(check)) + return function(value) + local tableSuccess, tableErrMsg = t.table(value) + if tableSuccess == false then + return false, tableErrMsg or "" + end + + for key, val in pairs(value) do + local success, errMsg = check(val) + if success == false then + return false, string.format("bad value for key %s:\n\t%s", tostring(key), errMsg or "") + end + end + + return true + end +end + +--[[** + ensures value is a table and all keys pass keyCheck and all values pass valueCheck + + @param keyCheck The function to use to check the keys + @param valueCheck The function to use to check the values + + @returns A function that will return true iff the condition is passed +**--]] +function t.map(keyCheck, valueCheck) + assert(t.callback(keyCheck), t.callback(valueCheck)) + local keyChecker = t.keys(keyCheck) + local valueChecker = t.values(valueCheck) + return function(value) + local keySuccess, keyErr = keyChecker(value) + if not keySuccess then + return false, keyErr or "" + end + + local valueSuccess, valueErr = valueChecker(value) + if not valueSuccess then + return false, valueErr or "" + end + + return true + end +end + +do + local arrayKeysCheck = t.keys(t.integer) + --[[** + ensures value is an array and all values of the array match check + + @param check The check to compare all values with + + @returns A function that will return true iff the condition is passed + **--]] + function t.array(check) + assert(t.callback(check)) + local valuesCheck = t.values(check) + return function(value) + local keySuccess, keyErrMsg = arrayKeysCheck(value) + if keySuccess == false then + return false, string.format("[array] %s", keyErrMsg or "") + end + + -- # is unreliable for sparse arrays + -- Count upwards using ipairs to avoid false positives from the behavior of # + local arraySize = 0 + + for _, _ in ipairs(value) do + arraySize = arraySize + 1 + end + + for key in pairs(value) do + if key < 1 or key > arraySize then + return false, string.format("[array] key %s must be sequential", tostring(key)) + end + end + + local valueSuccess, valueErrMsg = valuesCheck(value) + if not valueSuccess then + return false, string.format("[array] %s", valueErrMsg or "") + end + + return true + end + end +end + +do + local callbackArray = t.array(t.callback) + --[[** + creates a union type + + @param ... The checks to union + + @returns A function that will return true iff the condition is passed + **--]] + function t.union(...) + local checks = {...} + assert(callbackArray(checks)) + return function(value) + for _, check in pairs(checks) do + if check(value) then + return true + end + end + return false, "bad type for union" + end + end + + --[[** + Alias for t.union + **--]] + t.some = t.union + + --[[** + creates an intersection type + + @param ... The checks to intersect + + @returns A function that will return true iff the condition is passed + **--]] + function t.intersection(...) + local checks = {...} + assert(callbackArray(checks)) + return function(value) + for _, check in pairs(checks) do + local success, errMsg = check(value) + if not success then + return false, errMsg or "" + end + end + return true + end + end + + --[[** + Alias for t.intersection + **--]] + t.every = t.intersection +end + +do + local checkInterface = t.map(t.any, t.callback) + --[[** + ensures value matches given interface definition + + @param checkTable The interface definition + + @returns A function that will return true iff the condition is passed + **--]] + function t.interface(checkTable) + assert(checkInterface(checkTable)) + return function(value) + local tableSuccess, tableErrMsg = t.table(value) + if tableSuccess == false then + return false, tableErrMsg or "" + end + + for key, check in pairs(checkTable) do + local success, errMsg = check(value[key]) + if success == false then + return false, string.format("[interface] bad value for %s:\n\t%s", tostring(key), errMsg or "") + end + end + return true + end + end + + --[[** + ensures value matches given interface definition strictly + + @param checkTable The interface definition + + @returns A function that will return true iff the condition is passed + **--]] + function t.strictInterface(checkTable) + assert(checkInterface(checkTable)) + return function(value) + local tableSuccess, tableErrMsg = t.table(value) + if tableSuccess == false then + return false, tableErrMsg or "" + end + + for key, check in pairs(checkTable) do + local success, errMsg = check(value[key]) + if success == false then + return false, string.format("[interface] bad value for %s:\n\t%s", tostring(key), errMsg or "") + end + end + + for key in pairs(value) do + if not checkTable[key] then + return false, string.format("[interface] unexpected field '%s'", tostring(key)) + end + end + + return true + end + end +end + +--[[** + ensure value is an Instance and it's ClassName matches the given ClassName + + @param className The class name to check for + + @returns A function that will return true iff the condition is passed +**--]] +function t.instanceOf(className, childTable) + assert(t.string(className)) + + local childrenCheck + if childTable ~= nil then + childrenCheck = t.children(childTable) + end + + return function(value) + local instanceSuccess, instanceErrMsg = t.Instance(value) + if not instanceSuccess then + return false, instanceErrMsg or "" + end + + if value.ClassName ~= className then + return false, string.format("%s expected, got %s", className, value.ClassName) + end + + if childrenCheck then + local childrenSuccess, childrenErrMsg = childrenCheck(value) + if not childrenSuccess then + return false, childrenErrMsg + end + end + + return true + end +end +t.instance = t.instanceOf + +--[[** + ensure value is an Instance and it's ClassName matches the given ClassName by an IsA comparison + + @param className The class name to check for + + @returns A function that will return true iff the condition is passed +**--]] +function t.instanceIsA(className, childTable) + assert(t.string(className)) + + local childrenCheck + if childTable ~= nil then + childrenCheck = t.children(childTable) + end + + return function(value) + local instanceSuccess, instanceErrMsg = t.Instance(value) + if not instanceSuccess then + return false, instanceErrMsg or "" + end + + if not value:IsA(className) then + return false, string.format("%s expected, got %s", className, value.ClassName) + end + + if childrenCheck then + local childrenSuccess, childrenErrMsg = childrenCheck(value) + if not childrenSuccess then + return false, childrenErrMsg + end + end + + return true + end +end + +--[[** + ensures value is an enum of the correct type + + @param enum The enum to check + + @returns A function that will return true iff the condition is passed +**--]] +function t.enum(enum) + assert(t.Enum(enum)) + return function(value) + local enumItemSuccess, enumItemErrMsg = t.EnumItem(value) + if not enumItemSuccess then + return false, enumItemErrMsg + end + + if value.EnumType == enum then + return true + else + return false, string.format("enum of %s expected, got enum of %s", tostring(enum), tostring(value.EnumType)) + end + end +end + +do + local checkWrap = t.tuple(t.callback, t.callback) + + --[[** + wraps a callback in an assert with checkArgs + + @param callback The function to wrap + @param checkArgs The functon to use to check arguments in the assert + + @returns A function that first asserts using checkArgs and then calls callback + **--]] + function t.wrap(callback, checkArgs) + assert(checkWrap(callback, checkArgs)) + return function(...) + assert(checkArgs(...)) + return callback(...) + end + end +end + +--[[** + asserts a given check + + @param check The function to wrap with an assert + + @returns A function that simply wraps the given check in an assert +**--]] +function t.strict(check) + return function(...) + assert(check(...)) + end +end + +do + local checkChildren = t.map(t.string, t.callback) + + --[[** + Takes a table where keys are child names and values are functions to check the children against. + Pass an instance tree into the function. + If at least one child passes each check, the overall check passes. + + Warning! If you pass in a tree with more than one child of the same name, this function will always return false + + @param checkTable The table to check against + + @returns A function that checks an instance tree + **--]] + function t.children(checkTable) + assert(checkChildren(checkTable)) + + return function(value) + local instanceSuccess, instanceErrMsg = t.Instance(value) + if not instanceSuccess then + return false, instanceErrMsg or "" + end + + local childrenByName = {} + for _, child in pairs(value:GetChildren()) do + local name = child.Name + if checkTable[name] then + if childrenByName[name] then + return false, string.format("Cannot process multiple children with the same name \"%s\"", name) + end + childrenByName[name] = child + end + end + + for name, check in pairs(checkTable) do + local success, errMsg = check(childrenByName[name]) + if not success then + return false, string.format("[%s.%s] %s", value:GetFullName(), name, errMsg or "") + end + end + + return true + end + end +end + +return t \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/init.spec.lua new file mode 100644 index 0000000..19d6ff5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/init.spec.lua @@ -0,0 +1,560 @@ +return function() + local t = require(script.Parent) + + it("should support basic types", function() + assert(t.any("")) + assert(t.boolean(true)) + assert(t.none(nil)) + assert(t.number(1)) + assert(t.string("foo")) + assert(t.table({})) + + assert(not (t.any(nil))) + assert(not (t.boolean("true"))) + assert(not (t.none(1))) + assert(not (t.number(true))) + assert(not (t.string(true))) + assert(not (t.table(82))) + end) + + it("should support special number types", function() + local maxTen = t.numberMax(10) + local minTwo = t.numberMin(2) + local maxTenEx = t.numberMaxExclusive(10) + local minTwoEx = t.numberMinExclusive(2) + local constrainedEightToEleven = t.numberConstrained(8, 11) + local constrainedEightToElevenEx = t.numberConstrainedExclusive(8, 11) + + assert(maxTen(5)) + assert(maxTen(10)) + assert(not (maxTen(11))) + assert(not (maxTen())) + + assert(minTwo(5)) + assert(minTwo(2)) + assert(not (minTwo(1))) + assert(not (minTwo())) + + assert(maxTenEx(5)) + assert(maxTenEx(9)) + assert(not (maxTenEx(10))) + assert(not (maxTenEx())) + + assert(minTwoEx(5)) + assert(minTwoEx(3)) + assert(not (minTwoEx(2))) + assert(not (minTwoEx())) + + assert(not (constrainedEightToEleven(7))) + assert(constrainedEightToEleven(8)) + assert(constrainedEightToEleven(9)) + assert(constrainedEightToEleven(11)) + assert(not (constrainedEightToEleven(12))) + assert(not (constrainedEightToEleven())) + + assert(not (constrainedEightToElevenEx(7))) + assert(not (constrainedEightToElevenEx(8))) + assert(constrainedEightToElevenEx(9)) + assert(not (constrainedEightToElevenEx(11))) + assert(not (constrainedEightToElevenEx(12))) + assert(not (constrainedEightToElevenEx())) + end) + + it("should support optional types", function() + local check = t.optional(t.string) + assert(check("")) + assert(check()) + assert(not (check(1))) + end) + + it("should support tuple types", function() + local myTupleCheck = t.tuple(t.number, t.string, t.optional(t.number)) + assert(myTupleCheck(1, "2", 3)) + assert(myTupleCheck(1, "2")) + assert(not (myTupleCheck(1, "2", "3"))) + end) + + it("should support union types", function() + local numberOrString = t.union(t.number, t.string) + assert(numberOrString(1)) + assert(numberOrString("1")) + assert(not (numberOrString(nil))) + end) + + it("should support literal types", function() + local checkSingle = t.literal("foo") + local checkUnion = t.union(t.literal("foo"), t.literal("bar"), t.literal("oof")) + + assert(checkSingle("foo")) + assert(checkUnion("foo")) + assert(checkUnion("bar")) + assert(checkUnion("oof")) + + assert(not (checkSingle("FOO"))) + assert(not (checkUnion("FOO"))) + assert(not (checkUnion("BAR"))) + assert(not (checkUnion("OOF"))) + end) + + it("should support multiple literal types", function() + local checkSingle = t.literal("foo") + local checkUnion = t.literal("foo", "bar", "oof") + + assert(checkSingle("foo")) + assert(checkUnion("foo")) + assert(checkUnion("bar")) + assert(checkUnion("oof")) + + assert(not (checkSingle("FOO"))) + assert(not (checkUnion("FOO"))) + assert(not (checkUnion("BAR"))) + assert(not (checkUnion("OOF"))) + end) + + it("should support intersection types", function() + local integerMax5000 = t.intersection(t.integer, t.numberMax(5000)) + assert(integerMax5000(1)) + assert(not (integerMax5000(5001))) + assert(not (integerMax5000(1.1))) + assert(not (integerMax5000("1"))) + end) + + describe("array", function() + it("should support array types", function() + local stringArray = t.array(t.string) + local anyArray = t.array(t.any) + local stringValues = t.values(t.string) + assert(not (anyArray("foo"))) + assert(anyArray({1, "2", 3})) + assert(not (stringArray({1, "2", 3}))) + assert(not (stringArray())) + assert(not (stringValues())) + assert(anyArray({"1", "2", "3"}, t.string)) + assert(not (anyArray({ + foo = "bar" + }))) + assert(not (anyArray({ + [1] = "non", + [5] = "sequential" + }))) + end) + + it("should not be fooled by sparse arrays", function() + local anyArray = t.array(t.any) + + assert(not (anyArray({ + [1] = 1, + [2] = 2, + [4] = 4, + }))) + end) + end) + + it("should support map types", function() + local stringNumberMap = t.map(t.string, t.number) + assert(stringNumberMap({})) + assert(stringNumberMap({a = 1})) + assert(not (stringNumberMap({[1] = "a"}))) + assert(not (stringNumberMap({a = "a"}))) + assert(not (stringNumberMap())) + end) + + it("should support interface types", function() + local IVector3 = t.interface({ + x = t.number, + y = t.number, + z = t.number, + }) + + assert(IVector3({ + w = 0, + x = 1, + y = 2, + z = 3, + })) + + assert(not (IVector3({ + w = 0, + x = 1, + y = 2, + }))) + end) + + it("should support strict interface types", function() + local IVector3 = t.strictInterface({ + x = t.number, + y = t.number, + z = t.number, + }) + + assert(not (IVector3(0))) + + assert(not (IVector3({ + w = 0, + x = 1, + y = 2, + z = 3, + }))) + + assert(not (IVector3({ + w = 0, + x = 1, + y = 2, + }))) + + assert(IVector3({ + x = 1, + y = 2, + z = 3, + })) + end) + + it("should support deep interface types", function() + local IPlayer = t.interface({ + name = t.string, + inventory = t.interface({ + size = t.number + }) + }) + + assert(IPlayer({ + name = "TestPlayer", + inventory = { + size = 1 + } + })) + + assert(not (IPlayer({ + inventory = { + size = 1 + } + }))) + + assert(not (IPlayer({ + name = "TestPlayer", + inventory = { + } + }))) + + assert(not (IPlayer({ + name = "TestPlayer", + }))) + end) + + it("should support deep optional interface types", function() + local IPlayer = t.interface({ + name = t.string, + inventory = t.optional(t.interface({ + size = t.number + })) + }) + + assert(IPlayer({ + name = "TestPlayer" + })) + + assert(not (IPlayer({ + name = "TestPlayer", + inventory = { + } + }))) + + assert(IPlayer({ + name = "TestPlayer", + inventory = { + size = 1 + } + })) + end) + + it("should support Roblox Instance types", function() + local stringValueCheck = t.instanceOf("StringValue") + local stringValue = Instance.new("StringValue") + local boolValue = Instance.new("BoolValue") + + assert(stringValueCheck(stringValue)) + assert(not (stringValueCheck(boolValue))) + assert(not (stringValueCheck())) + end) + + it("should support Roblox Instance types inheritance", function() + local guiObjectCheck = t.instanceIsA("GuiObject") + local frame = Instance.new("Frame") + local textLabel = Instance.new("TextLabel") + local stringValue = Instance.new("StringValue") + + assert(guiObjectCheck(frame)) + assert(guiObjectCheck(textLabel)) + assert(not (guiObjectCheck(stringValue))) + assert(not (guiObjectCheck())) + end) + + it("should support Roblox Enum types", function() + local sortOrderEnumCheck = t.enum(Enum.SortOrder) + assert(t.Enum(Enum.SortOrder)) + assert(not (t.Enum("Enum.SortOrder"))) + + assert(t.EnumItem(Enum.SortOrder.Name)) + assert(not (t.EnumItem("Enum.SortOrder.Name"))) + + assert(sortOrderEnumCheck(Enum.SortOrder.Name)) + assert(sortOrderEnumCheck(Enum.SortOrder.Custom)) + assert(not (sortOrderEnumCheck(Enum.EasingStyle.Linear))) + assert(not (sortOrderEnumCheck())) + end) + + it("should support Roblox RBXScriptSignal", function() + assert(t.RBXScriptSignal(game.ChildAdded)) + assert(not (t.RBXScriptSignal(nil))) + assert(not (t.RBXScriptSignal(Vector3.new()))) + end) + + -- TODO: Add this back when Lemur supports it + -- it("should support Roblox RBXScriptConnection", function() + -- local conn = game.ChildAdded:Connect(function() end) + -- assert(t.RBXScriptConnection(conn)) + -- assert(not (t.RBXScriptConnection(nil))) + -- assert(not (t.RBXScriptConnection(Vector3.new()))) + -- end) + + it("should support wrapping function types", function() + local checkFoo = t.tuple(t.string, t.number, t.optional(t.string)) + local foo = t.wrap(function(a, b, c) + local result = string.format("%s %d", a, b) + if c then + result = result .. " " .. c + end + return result + end, checkFoo) + + assert(not (pcall(foo))) + assert(not (pcall(foo, "a"))) + assert(not (pcall(foo, 2))) + assert(pcall(foo, "a", 1)) + assert(pcall(foo, "a", 1, "b")) + end) + + it("should support strict types", function() + local myType = t.strict(t.tuple(t.string, t.number)) + assert(not (pcall(function() + myType("a", "b") + end))) + assert(pcall(function() + myType("a", 1) + end)) + end) + + it("should support common OOP types", function() + local MyClass = {} + MyClass.__index = MyClass + + function MyClass.new() + local self = setmetatable({}, MyClass) + return self + end + + local function instanceOfClass(class) + return function(value) + local tableSuccess, tableErrMsg = t.table(value) + if not tableSuccess then + return false, tableErrMsg or "" + end + + local mt = getmetatable(value) + if not mt or mt.__index ~= class then + return false, "bad member of class" + end + + return true + end + end + + local instanceOfMyClass = instanceOfClass(MyClass) + + local myObject = MyClass.new() + assert(instanceOfMyClass(myObject)) + assert(not (instanceOfMyClass({}))) + assert(not (instanceOfMyClass())) + end) + + it("should not treat NaN as numbers", function() + assert(t.number(1)) + assert(not (t.number(0/0))) + assert(not (t.number("1"))) + end) + + it("should not treat numbers as NaN", function() + assert(not (t.nan(1))) + assert(t.nan(0/0)) + assert(not (t.nan("1"))) + end) + + it("should allow union of number and NaN", function() + local numberOrNaN = t.union(t.number, t.nan) + assert(numberOrNaN(1)) + assert(numberOrNaN(0/0)) + assert(not (numberOrNaN("1"))) + end) + + it("should support non-string keys for interfaces", function() + local key = {} + local myInterface = t.interface({ [key] = t.number }) + assert(myInterface({ [key] = 1 })) + assert(not (myInterface({ [key] = "1" }))) + end) + + it("should support failing on non-string keys for strict interfaces", function() + local myInterface = t.strictInterface({ a = t.number }) + assert(not (myInterface({ a = 1, [{}] = 2 }))) + end) + + it("should support children", function() + local myInterface = t.interface({ + buttonInFrame = t.intersection(t.instanceOf("Frame"), t.children({ + MyButton = t.instanceOf("ImageButton") + })) + }) + + assert(not (t.children({})(5))) + assert(not (myInterface({ buttonInFrame = Instance.new("Frame") }))) + + do + local frame = Instance.new("Frame") + local button = Instance.new("ImageButton", frame) + button.Name = "MyButton" + assert(myInterface({ buttonInFrame = frame })) + end + + do + local frame = Instance.new("Frame") + local button = Instance.new("ImageButton", frame) + button.Name = "NotMyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + + do + local frame = Instance.new("Frame") + local button = Instance.new("TextButton", frame) + button.Name = "MyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + + do + local frame = Instance.new("Frame") + local button1 = Instance.new("ImageButton", frame) + button1.Name = "MyButton" + local button2 = Instance.new("ImageButton", frame) + button2.Name = "MyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + end) + + it("should support t.instanceOf shorthand", function() + local myInterface = t.interface({ + buttonInFrame = t.instanceOf("Frame", { + MyButton = t.instanceOf("ImageButton") + }) + }) + + assert(not (t.children({})(5))) + assert(not (myInterface({ buttonInFrame = Instance.new("Frame") }))) + + do + local frame = Instance.new("Frame") + local button = Instance.new("ImageButton", frame) + button.Name = "MyButton" + assert(myInterface({ buttonInFrame = frame })) + end + + do + local frame = Instance.new("Frame") + local button = Instance.new("ImageButton", frame) + button.Name = "NotMyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + + do + local frame = Instance.new("Frame") + local button = Instance.new("TextButton", frame) + button.Name = "MyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + + do + local frame = Instance.new("Frame") + local button1 = Instance.new("ImageButton", frame) + button1.Name = "MyButton" + local button2 = Instance.new("ImageButton", frame) + button2.Name = "MyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + end) + + it("should support t.instanceIsA shorthand", function() + local myInterface = t.interface({ + buttonInFrame = t.instanceIsA("Frame", { + MyButton = t.instanceIsA("ImageButton") + }) + }) + + assert(not (t.children({})(5))) + assert(not (myInterface({ buttonInFrame = Instance.new("Frame") }))) + + do + local frame = Instance.new("Frame") + local button = Instance.new("ImageButton", frame) + button.Name = "MyButton" + assert(myInterface({ buttonInFrame = frame })) + end + + do + local frame = Instance.new("Frame") + local button = Instance.new("ImageButton", frame) + button.Name = "NotMyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + + do + local frame = Instance.new("Frame") + local button = Instance.new("TextButton", frame) + button.Name = "MyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + + do + local frame = Instance.new("Frame") + local button1 = Instance.new("ImageButton", frame) + button1.Name = "MyButton" + local button2 = Instance.new("ImageButton", frame) + button2.Name = "MyButton" + assert(not (myInterface({ buttonInFrame = frame }))) + end + end) + + it("should support t.match", function() + local check = t.match("%d+") + assert(check("123")) + assert(not (check("abc"))) + assert(not (check())) + end) + + it("should support t.keyOf", function() + local myNewEnum = { + OptionA = {}, + OptionB = {}, + } + local check = t.keyOf(myNewEnum) + assert(check("OptionA")) + assert(not (check("OptionC"))) + end) + + it("should support t.valueOf", function() + local myNewEnum = { + OptionA = {}, + OptionB = {}, + } + local check = t.valueOf(myNewEnum) + assert(check(myNewEnum.OptionA)) + assert(not (check(1010))) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/t.d.ts b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/t.d.ts new file mode 100644 index 0000000..b0b7d2c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/t.d.ts @@ -0,0 +1,214 @@ +/** checks to see if `value` is a T */ +type check = (value: unknown) => value is T; + +interface t { + // lua types + /** checks to see if `value` is an any */ + any: check; + /** checks to see if `value` is a boolean */ + boolean: check; + /** checks to see if `value` is a thread */ + thread: check; + /** checks to see if `value` is a Function */ + callback: check; + /** checks to see if `value` is undefined */ + none: check; + /** checks to see if `value` is a number, will _not_ match NaN */ + number: check; + /** checks to see if `value` is NaN */ + nan: check; + /** checks to see if `value` is a string */ + string: check; + /** checks to see if `value` is an object */ + table: check; + /** checks to see if `value` is a userdata */ + userdata: check; + + // roblox types + /** checks to see if `value` is an Axes */ + Axes: check; + /** checks to see if `value` is a BrickColor */ + BrickColor: check; + /** checks to see if `value` is a CFrame */ + CFrame: check; + /** checks to see if `value` is a Color3 */ + Color3: check; + /** checks to see if `value` is a ColorSequence */ + ColorSequence: check; + /** checks to see if `value` is a ColorSequenceKeypoint */ + ColorSequenceKeypoint: check; + /** checks to see if `value` is a DockWidgetPluginGuiInfo */ + DockWidgetPluginGuiInfo: check; + /** checks to see if `value` is a Faces */ + Faces: check; + /** checks to see if `value` is an Instance */ + Instance: check; + /** checks to see if `value` is a NumberRange */ + NumberRange: check; + /** checks to see if `value` is a NumberSequence */ + NumberSequence: check; + /** checks to see if `value` is a NumberSequenceKeypoint */ + NumberSequenceKeypoint: check; + /** checks to see if `value` is a PathWaypoint */ + PathWaypoint: check; + /** checks to see if `value` is a PhysicalProperties */ + PhysicalProperties: check; + /** checks to see if `value` is a Random */ + Random: check; + /** checks to see if `value` is a Ray */ + Ray: check; + /** checks to see if `value` is a Rect */ + Rect: check; + /** checks to see if `value` is a Region3 */ + Region3: check; + /** checks to see if `value` is a Region3int16 */ + Region3int16: check; + /** checks to see if `value` is a TweenInfo */ + TweenInfo: check; + /** checks to see if `value` is a UDim */ + UDim: check; + /** checks to see if `value` is a UDim2 */ + UDim2: check; + /** checks to see if `value` is a Vector2 */ + Vector2: check; + /** checks to see if `value` is a Vector3 */ + Vector3: check; + /** checks to see if `value` is a Vector3int16 */ + Vector3int16: check; + /** checks to see if `value` is a RBXScriptSignal */ + RBXScriptSignal: check; + /** checks to see if `value` is a RBXScriptConnection */ + RBXScriptConnection: check; + + /** + * checks to see if `value == literalValue` + */ + literal(this: void, literalValue: T): check; + literal>( + this: void, + ...args: T + ): T extends [infer A] + ? (value: unknown) => value is A + : T extends [infer A, infer B] + ? check + : T extends [infer A, infer B, infer C] + ? check + : T extends [infer A, infer B, infer C, infer D] + ? check + : T extends [infer A, infer B, infer C, infer D, infer E] + ? check + : T extends [infer A, infer B, infer C, infer D, infer E, infer F] + ? check + : never; + literal(this: void, literalValue: T): (value: unknown) => value is T; + + /** Returns a t.union of each key in the table as a t.literal */ + keyOf: (valueTable: T) => check; + + /** Returns a t.union of each value in the table as a t.literal */ + valueOf: (valueTable: T) => T extends { [P in keyof T]: infer U } ? check : never; + + /** checks to see if `value` is an integer */ + integer: (value: unknown) => value is number; + /** checks to see if `value` is a number and is more than or equal to `min` */ + numberMin: (min: number) => (value: unknown) => value is number; + /** checks to see if `value` is a number and is less than or equal to `max` */ + numberMax: (max: number) => (value: unknown) => value is number; + /** checks to see if `value` is a number and is more than `min` */ + numberMinExclusive: (min: number) => (value: unknown) => value is number; + /** checks to see if `value` is a number and is less than `max` */ + numberMaxExclusive: (max: number) => (value: unknown) => value is number; + /** checks to see if `value` is a number and is more than 0 */ + numberPositive: (value: unknown) => value is number; + /** checks to see if `value` is a number and is less than 0 */ + numberNegative: (value: unknown) => value is number; + /** checks to see if `value` is a number and `min <= value <= max` */ + numberConstrained: (min: number, max: number) => (value: unknown) => value is number; + /** checks to see if `value` is a number and `min < value < max` */ + numberConstrainedExclusive: (min: number, max: number) => (value: unknown) => value is number; + /** checks `t.string` and determines if value matches the pattern via `string.match(value, pattern)` */ + match: (pattern: string) => check; + /** checks to see if `value` is either nil or passes `check` */ + optional: (check: (value: unknown) => value is T) => check; + /** checks to see if `value` is a table and if its keys match against `check */ + keys: (check: (value: unknown) => value is T) => check>; + /** checks to see if `value` is a table and if its values match against `check` */ + values: (check: (value: unknown) => value is T) => check>; + /** checks to see if `value` is a table and all of its keys match against `keyCheck` and all of its values match against `valueCheck` */ + map: ( + keyCheck: (value: unknown) => value is K, + valueCheck: (value: unknown) => value is V + ) => check>; + /** checks to see if `value` is an array and all of its keys are sequential integers and all of its values match `check` */ + array: (check: (value: unknown) => value is T) => check>; + + /** checks to see if `value` matches any given check */ + union: >( + ...args: T + ) => T extends [check] + ? (value: unknown) => value is A + : T extends [check, check] + ? check + : T extends [check, check, check] + ? check + : T extends [check, check, check, check] + ? check + : T extends [check, check, check, check, check] + ? check + : T extends [check, check, check, check, check, check] + ? check + : never; + + /** checks to see if `value` matches all given checks */ + intersection: >( + ...args: T + ) => T extends [check] + ? (value: unknown) => value is A + : T extends [check, check] + ? check + : T extends [check, check, check] + ? check + : T extends [check, check, check, check] + ? check + : T extends [check, check, check, check, check] + ? check + : T extends [check, check, check, check, check, check] + ? check + : never; + + /** checks to see if `value` matches a given interface definition */ + interface: value is any }>( + checkTable: T + ) => check<{ [P in keyof T]: t.static }>; + + /** checks to see if `value` matches a given interface definition with no extra members */ + strictInterface: value is any }>( + checkTable: T + ) => check<{ [P in keyof T]: t.static }>; + + instanceOf(this: void, className: S): check; + instanceOf value is any }>( + this: void, + className: S, + checkTable: T + ): check }>; + + instanceIsA(this: void, className: S): check; + instanceIsA value is any }>( + this: void, + className: S, + checkTable: T + ): check }>; + + children: value is any }>( + checkTable: T + ) => check }>; +} + +declare namespace t { + /** creates a static type from a t-defined type */ + export type static = T extends check ? U : never; +} + +declare const t: t; +export = t; diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/ts.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/ts.lua new file mode 100644 index 0000000..7e7df9b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_t/t/ts.lua @@ -0,0 +1,554 @@ +-- t: a runtime typechecker for Roblox + +-- regular lua compatibility +local typeof = typeof or type + +local function primitive(typeName) + return function(value) + local valueType = typeof(value) + if valueType == typeName then + return true + else + return false + end + end +end + +local t = {} + +function t.any(value) + if value ~= nil then + return true + else + return false + end +end + +--Lua primitives + +t.boolean = primitive("boolean") +t.thread = primitive("thread") +t.callback = primitive("function") +t.none = primitive("nil") +t.string = primitive("string") +t.table = primitive("table") +t.userdata = primitive("userdata") + +function t.number(value) + local valueType = typeof(value) + if valueType == "number" then + if value == value then + return true + else + return false + end + else + return false + end +end + +function t.nan(value) + if value ~= value then + return true + else + return false + end +end + +-- roblox types + +t.Axes = primitive("Axes") +t.BrickColor = primitive("BrickColor") +t.CFrame = primitive("CFrame") +t.Color3 = primitive("Color3") +t.ColorSequence = primitive("ColorSequence") +t.ColorSequenceKeypoint = primitive("ColorSequenceKeypoint") +t.DockWidgetPluginGuiInfo = primitive("DockWidgetPluginGuiInfo") +t.Faces = primitive("Faces") +t.Instance = primitive("Instance") +t.NumberRange = primitive("NumberRange") +t.NumberSequence = primitive("NumberSequence") +t.NumberSequenceKeypoint = primitive("NumberSequenceKeypoint") +t.PathWaypoint = primitive("PathWaypoint") +t.PhysicalProperties = primitive("PhysicalProperties") +t.Random = primitive("Random") +t.Ray = primitive("Ray") +t.Rect = primitive("Rect") +t.Region3 = primitive("Region3") +t.Region3int16 = primitive("Region3int16") +t.TweenInfo = primitive("TweenInfo") +t.UDim = primitive("UDim") +t.UDim2 = primitive("UDim2") +t.Vector2 = primitive("Vector2") +t.Vector3 = primitive("Vector3") +t.Vector3int16 = primitive("Vector3int16") +t.Enum = primitive("Enum") +t.EnumItem = primitive("EnumItem") +t.RBXScriptSignal = primitive("RBXScriptSignal") +t.RBXScriptConnection = primitive("RBXScriptConnection") + +function t.literal(...) + local size = select("#", ...) + if size == 1 then + local literal = ... + return function(value) + if value ~= literal then + return false + end + return true + end + else + local literals = {} + for i = 1, size do + local value = select(i, ...) + literals[i] = t.literal(value) + end + return t.union(unpack(literals)) + end +end + +t.exactly = t.literal + +function t.keyOf(keyTable) + local keys = {} + for key in pairs(keyTable) do + keys[#keys + 1] = key + end + return t.literal(unpack(keys)) +end + +function t.valueOf(valueTable) + local values = {} + for _, value in pairs(valueTable) do + values[#values + 1] = value + end + return t.literal(unpack(values)) +end + +function t.integer(value) + local success = t.number(value) + if not success then + return false + end + if value%1 == 0 then + return true + else + return false + end +end + +function t.numberMin(min) + return function(value) + local success = t.number(value) + if not success then + return false + end + if value >= min then + return true + else + return false + end + end +end + +function t.numberMax(max) + return function(value) + local success = t.number(value) + if not success then + return false + end + if value <= max then + return true + else + return false + end + end +end + +function t.numberMinExclusive(min) + return function(value) + local success = t.number(value) + if not success then + return false + end + if min < value then + return true + else + return false + end + end +end + +function t.numberMaxExclusive(max) + return function(value) + local success = t.number(value) + if not success then + return false + end + if value < max then + return true + else + return false + end + end +end + +t.numberPositive = t.numberMinExclusive(0) +t.numberNegative = t.numberMaxExclusive(0) + +function t.numberConstrained(min, max) + assert(t.number(min) and t.number(max)) + local minCheck = t.numberMin(min) + local maxCheck = t.numberMax(max) + return function(value) + local minSuccess = minCheck(value) + if not minSuccess then + return false + end + + local maxSuccess = maxCheck(value) + if not maxSuccess then + return false + end + + return true + end +end + +function t.numberConstrainedExclusive(min, max) + assert(t.number(min) and t.number(max)) + local minCheck = t.numberMinExclusive(min) + local maxCheck = t.numberMaxExclusive(max) + return function(value) + local minSuccess = minCheck(value) + if not minSuccess then + return false + end + + local maxSuccess = maxCheck(value) + if not maxSuccess then + return false + end + + return true + end +end + +function t.match(pattern) + assert(t.string(pattern)) + return function(value) + local stringSuccess = t.string(value) + if not stringSuccess then + return false + end + + if string.match(value, pattern) == nil then + return false + end + + return true + end +end + +function t.optional(check) + assert(t.callback(check)) + return function(value) + if value == nil then + return true + end + local success = check(value) + if success then + return true + else + return false + end + end +end + +function t.tuple(...) + local checks = {...} + return function(...) + local args = {...} + for i = 1, #checks do + local success = checks[i](args[i]) + if success == false then + return false + end + end + return true + end +end + +function t.keys(check) + assert(t.callback(check)) + return function(value) + local tableSuccess = t.table(value) + if tableSuccess == false then + return false + end + + for key in pairs(value) do + local success = check(key) + if success == false then + return false + end + end + + return true + end +end + +function t.values(check) + assert(t.callback(check)) + return function(value) + local tableSuccess = t.table(value) + if tableSuccess == false then + return false + end + + for _, val in pairs(value) do + local success = check(val) + if success == false then + return false + end + end + + return true + end +end + +function t.map(keyCheck, valueCheck) + assert(t.callback(keyCheck), t.callback(valueCheck)) + local keyChecker = t.keys(keyCheck) + local valueChecker = t.values(valueCheck) + return function(value) + local keySuccess = keyChecker(value) + if not keySuccess then + return false + end + + local valueSuccess = valueChecker(value) + if not valueSuccess then + return false + end + + return true + end +end + +do + local arrayKeysCheck = t.keys(t.integer) + + function t.array(check) + assert(t.callback(check)) + local valuesCheck = t.values(check) + return function(value) + local keySuccess = arrayKeysCheck(value) + if keySuccess == false then + return false + end + + -- # is unreliable for sparse arrays + -- Count upwards using ipairs to avoid false positives from the behavior of # + local arraySize = 0 + + for _, _ in ipairs(value) do + arraySize = arraySize + 1 + end + + for key in pairs(value) do + if key < 1 or key > arraySize then + return false + end + end + + local valueSuccess = valuesCheck(value) + if not valueSuccess then + return false + end + + return true + end + end +end + +do + local callbackArray = t.array(t.callback) + + function t.union(...) + local checks = {...} + assert(callbackArray(checks)) + return function(value) + for _, check in pairs(checks) do + if check(value) then + return true + end + end + return false + end + end + + function t.intersection(...) + local checks = {...} + assert(callbackArray(checks)) + return function(value) + for _, check in pairs(checks) do + local success = check(value) + if not success then + return false + end + end + return true + end + end +end + +do + local checkInterface = t.map(t.any, t.callback) + + function t.interface(checkTable) + assert(checkInterface(checkTable)) + return function(value) + local tableSuccess = t.table(value) + if tableSuccess == false then + return false + end + + for key, check in pairs(checkTable) do + local success = check(value[key]) + if success == false then + return false + end + end + return true + end + end + + function t.strictInterface(checkTable) + assert(checkInterface(checkTable)) + return function(value) + local tableSuccess = t.table(value) + if tableSuccess == false then + return false + end + + for key, check in pairs(checkTable) do + local success = check(value[key]) + if success == false then + return false + end + end + + for key in pairs(value) do + if not checkTable[key] then + return false + end + end + + return true + end + end +end + +function t.instanceOf(className) + assert(t.string(className)) + return function(value) + local instanceSuccess = t.Instance(value) + if not instanceSuccess then + return false + end + + if value.ClassName ~= className then + return false + end + + return true + end +end +t.instance = t.instanceOf + +function t.instanceIsA(className) + assert(t.string(className)) + return function(value) + local instanceSuccess = t.Instance(value) + if not instanceSuccess then + return false + end + + if not value:IsA(className) then + return false + end + + return true + end +end + +function t.enum(enum) + assert(t.Enum(enum)) + return function(value) + local enumItemSuccess = t.EnumItem(value) + if not enumItemSuccess then + return false + end + + if value.EnumType == enum then + return true + else + return false + end + end +end + +do + local checkWrap = t.tuple(t.callback, t.callback) + + function t.wrap(callback, checkArgs) + assert(checkWrap(callback, checkArgs)) + return function(...) + assert(checkArgs(...)) + return callback(...) + end + end +end + +function t.strict(check) + return function(...) + assert(check(...)) + end +end + +do + local checkChildren = t.map(t.string, t.callback) + function t.children(checkTable) + assert(checkChildren(checkTable)) + + return function(value) + local instanceSuccess = t.Instance(value) + if not instanceSuccess then + return false + end + + local childrenByName = {} + for _, child in pairs(value:GetChildren()) do + local name = child.Name + if checkTable[name] then + if childrenByName[name] then + return false + end + childrenByName[name] = child + end + end + + for name, check in pairs(checkTable) do + local success = check(childrenByName[name]) + if not success then + return false + end + end + + return true + end + end +end + +return t \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/lock.toml new file mode 100644 index 0000000..e4ef557 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/testez" +version = "0.4.0" +commit = "6c169cd80edbacbe27aa1c15345a60c4cb5177c7" +source = "url+https://github.com/roblox/testez" diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Context.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Context.lua new file mode 100644 index 0000000..efd4993 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Context.lua @@ -0,0 +1,26 @@ +--[[ + The Context object implements a write-once key-value store. It also allows + for a new Context object to inherit the entries from an existing one. +]] +local Context = {} + +function Context.new(parent) + local meta = {} + local index = {} + meta.__index = index + + if parent then + for key, value in pairs(getmetatable(parent).__index) do + index[key] = value + end + end + + function meta.__newindex(_obj, key, value) + assert(index[key] == nil, string.format("Cannot reassign %s in context", tostring(key))) + index[key] = value + end + + return setmetatable({}, meta) +end + +return Context diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Expectation.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Expectation.lua new file mode 100644 index 0000000..96dc2c7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Expectation.lua @@ -0,0 +1,311 @@ +--[[ + Allows creation of expectation statements designed for behavior-driven + testing (BDD). See Chai (JS) or RSpec (Ruby) for examples of other BDD + frameworks. + + The Expectation class is exposed to tests as a function called `expect`: + + expect(5).to.equal(5) + expect(foo()).to.be.ok() + + Expectations can be negated using .never: + + expect(true).never.to.equal(false) + + Expectations throw errors when their conditions are not met. +]] + +local Expectation = {} + +--[[ + These keys don't do anything except make expectations read more cleanly +]] +local SELF_KEYS = { + to = true, + be = true, + been = true, + have = true, + was = true, + at = true, +} + +--[[ + These keys invert the condition expressed by the Expectation. +]] +local NEGATION_KEYS = { + never = true, +} + +--[[ + Extension of Lua's 'assert' that lets you specify an error level. +]] +local function assertLevel(condition, message, level) + message = message or "Assertion failed!" + level = level or 1 + + if not condition then + error(message, level + 1) + end +end + +--[[ + Returns a version of the given method that can be called with either . or : +]] +local function bindSelf(self, method) + return function(firstArg, ...) + if firstArg == self then + return method(self, ...) + else + return method(self, firstArg, ...) + end + end +end + +local function formatMessage(result, trueMessage, falseMessage) + if result then + return trueMessage + else + return falseMessage + end +end + +--[[ + Create a new expectation +]] +function Expectation.new(value) + local self = { + value = value, + successCondition = true, + condition = false, + matchers = {}, + _boundMatchers = {}, + } + + setmetatable(self, Expectation) + + self.a = bindSelf(self, self.a) + self.an = self.a + self.ok = bindSelf(self, self.ok) + self.equal = bindSelf(self, self.equal) + self.throw = bindSelf(self, self.throw) + self.near = bindSelf(self, self.near) + + return self +end + +function Expectation.checkMatcherNameCollisions(name) + if SELF_KEYS[name] or NEGATION_KEYS[name] or Expectation[name] then + return false + end + + return true +end + +function Expectation:extend(matchers) + self.matchers = matchers or {} + + for name, implementation in pairs(self.matchers) do + self._boundMatchers[name] = bindSelf(self, function(_self, ...) + local result = implementation(self.value, ...) + local pass = result.pass == self.successCondition + + assertLevel(pass, result.message, 3) + self:_resetModifiers() + return self + end) + end + + return self +end + +function Expectation.__index(self, key) + -- Keys that don't do anything except improve readability + if SELF_KEYS[key] then + return self + end + + -- Invert your assertion + if NEGATION_KEYS[key] then + local newExpectation = Expectation.new(self.value):extend(self.matchers) + newExpectation.successCondition = not self.successCondition + + return newExpectation + end + + if self._boundMatchers[key] then + return self._boundMatchers[key] + end + + -- Fall back to methods provided by Expectation + return Expectation[key] +end + +--[[ + Called by expectation terminators to reset modifiers in a statement. + + This makes chains like: + + expect(5) + .never.to.equal(6) + .to.equal(5) + + Work as expected. +]] +function Expectation:_resetModifiers() + self.successCondition = true +end + +--[[ + Assert that the expectation value is the given type. + + expect(5).to.be.a("number") +]] +function Expectation:a(typeName) + local result = (type(self.value) == typeName) == self.successCondition + + local message = formatMessage(self.successCondition, + ("Expected value of type %q, got value %q of type %s"):format( + typeName, + tostring(self.value), + type(self.value) + ), + ("Expected value not of type %q, got value %q of type %s"):format( + typeName, + tostring(self.value), + type(self.value) + ) + ) + + assertLevel(result, message, 3) + self:_resetModifiers() + + return self +end + +-- Make alias public on class +Expectation.an = Expectation.a + +--[[ + Assert that our expectation value is truthy +]] +function Expectation:ok() + local result = (self.value ~= nil) == self.successCondition + + local message = formatMessage(self.successCondition, + ("Expected value %q to be non-nil"):format( + tostring(self.value) + ), + ("Expected value %q to be nil"):format( + tostring(self.value) + ) + ) + + assertLevel(result, message, 3) + self:_resetModifiers() + + return self +end + +--[[ + Assert that our expectation value is equal to another value +]] +function Expectation:equal(otherValue) + local result = (self.value == otherValue) == self.successCondition + + local message = formatMessage(self.successCondition, + ("Expected value %q (%s), got %q (%s) instead"):format( + tostring(otherValue), + type(otherValue), + tostring(self.value), + type(self.value) + ), + ("Expected anything but value %q (%s)"):format( + tostring(otherValue), + type(otherValue) + ) + ) + + assertLevel(result, message, 3) + self:_resetModifiers() + + return self +end + +--[[ + Assert that our expectation value is equal to another value within some + inclusive limit. +]] +function Expectation:near(otherValue, limit) + assert(type(self.value) == "number", "Expectation value must be a number to use 'near'") + assert(type(otherValue) == "number", "otherValue must be a number") + assert(type(limit) == "number" or limit == nil, "limit must be a number or nil") + + limit = limit or 1e-7 + + local result = (math.abs(self.value - otherValue) <= limit) == self.successCondition + + local message = formatMessage(self.successCondition, + ("Expected value to be near %f (within %f) but got %f instead"):format( + otherValue, + limit, + self.value + ), + ("Expected value to not be near %f (within %f) but got %f instead"):format( + otherValue, + limit, + self.value + ) + ) + + assertLevel(result, message, 3) + self:_resetModifiers() + + return self +end + +--[[ + Assert that our functoid expectation value throws an error when called. + An optional error message can be passed to assert that the error message + contains the given value. +]] +function Expectation:throw(messageSubstring) + local ok, err = pcall(self.value) + local result = ok ~= self.successCondition + + if messageSubstring and not ok then + if self.successCondition then + result = err:find(messageSubstring, 1, true) ~= nil + else + result = err:find(messageSubstring, 1, true) == nil + end + end + + local message + + if messageSubstring then + message = formatMessage(self.successCondition, + ("Expected function to throw an error containing %q, but it %s"):format( + messageSubstring, + err and ("threw: %s"):format(err) or "did not throw." + ), + ("Expected function to never throw an error containing %q, but it threw: %s"):format( + messageSubstring, + tostring(err) + ) + ) + else + message = formatMessage(self.successCondition, + "Expected function to throw an error, but it did not throw.", + ("Expected function to succeed, but it threw an error: %s"):format( + tostring(err) + ) + ) + end + + assertLevel(result, message, 3) + self:_resetModifiers() + + return self +end + +return Expectation diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/ExpectationContext.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/ExpectationContext.lua new file mode 100644 index 0000000..b55f53c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/ExpectationContext.lua @@ -0,0 +1,38 @@ +local Expectation = require(script.Parent.Expectation) +local checkMatcherNameCollisions = Expectation.checkMatcherNameCollisions + +local function copy(t) + local result = {} + + for key, value in pairs(t) do + result[key] = value + end + + return result +end + +local ExpectationContext = {} +ExpectationContext.__index = ExpectationContext + +function ExpectationContext.new(parent) + local self = { + _extensions = parent and copy(parent._extensions) or {}, + } + + return setmetatable(self, ExpectationContext) +end + +function ExpectationContext:startExpectationChain(...) + return Expectation.new(...):extend(self._extensions) +end + +function ExpectationContext:extend(config) + for key, value in pairs(config) do + assert(self._extensions[key] == nil, string.format("Cannot reassign %q in expect.extend", key)) + assert(checkMatcherNameCollisions(key), string.format("Cannot overwrite matcher %q; it already exists", key)) + + self._extensions[key] = value + end +end + +return ExpectationContext diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/LifecycleHooks.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/LifecycleHooks.lua new file mode 100644 index 0000000..c60b497 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/LifecycleHooks.lua @@ -0,0 +1,89 @@ +local TestEnum = require(script.Parent.TestEnum) + +local LifecycleHooks = {} +LifecycleHooks.__index = LifecycleHooks + +function LifecycleHooks.new() + local self = { + _stack = {}, + } + return setmetatable(self, LifecycleHooks) +end + +--[[ + Returns an array of `beforeEach` hooks in FIFO order +]] +function LifecycleHooks:getBeforeEachHooks() + local key = TestEnum.NodeType.BeforeEach + local hooks = {} + + for _, level in ipairs(self._stack) do + for _, hook in ipairs(level[key]) do + table.insert(hooks, hook) + end + end + + return hooks +end + +--[[ + Returns an array of `afterEach` hooks in FILO order +]] +function LifecycleHooks:getAfterEachHooks() + local key = TestEnum.NodeType.AfterEach + local hooks = {} + + for _, level in ipairs(self._stack) do + for _, hook in ipairs(level[key]) do + table.insert(hooks, 1, hook) + end + end + + return hooks +end + +--[[ + Pushes uncalled beforeAll and afterAll hooks back up the stack +]] +function LifecycleHooks:popHooks() + table.remove(self._stack, #self._stack) +end + +function LifecycleHooks:pushHooksFrom(planNode) + assert(planNode ~= nil) + + table.insert(self._stack, { + [TestEnum.NodeType.BeforeAll] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.BeforeAll), + [TestEnum.NodeType.AfterAll] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.AfterAll), + [TestEnum.NodeType.BeforeEach] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.BeforeEach), + [TestEnum.NodeType.AfterEach] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.AfterEach), + }) +end + +--[[ + Get the beforeAll hooks from the current level. +]] +function LifecycleHooks:getBeforeAllHooks() + return self._stack[#self._stack][TestEnum.NodeType.BeforeAll] +end + +--[[ + Get the afterAll hooks from the current level. +]] +function LifecycleHooks:getAfterAllHooks() + return self._stack[#self._stack][TestEnum.NodeType.AfterAll] +end + +function LifecycleHooks:_getHooksOfType(nodes, key) + local hooks = {} + + for _, node in ipairs(nodes) do + if node.type == key then + table.insert(hooks, node.callback) + end + end + + return hooks +end + +return LifecycleHooks diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TeamCityReporter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TeamCityReporter.lua new file mode 100644 index 0000000..bab37e5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TeamCityReporter.lua @@ -0,0 +1,102 @@ +local TestService = game:GetService("TestService") + +local TestEnum = require(script.Parent.Parent.TestEnum) + +local TeamCityReporter = {} + +local function teamCityEscape(str) + str = string.gsub(str, "([]|'[])","|%1") + str = string.gsub(str, "\r", "|r") + str = string.gsub(str, "\n", "|n") + return str +end + +local function teamCityEnterSuite(suiteName) + return string.format("##teamcity[testSuiteStarted name='%s']", teamCityEscape(suiteName)) +end + +local function teamCityLeaveSuite(suiteName) + return string.format("##teamcity[testSuiteFinished name='%s']", teamCityEscape(suiteName)) +end + +local function teamCityEnterCase(caseName) + return string.format("##teamcity[testStarted name='%s']", teamCityEscape(caseName)) +end + +local function teamCityLeaveCase(caseName) + return string.format("##teamcity[testFinished name='%s']", teamCityEscape(caseName)) +end + +local function teamCityFailCase(caseName, errorMessage) + return string.format("##teamcity[testFailed name='%s' message='%s']", + teamCityEscape(caseName), teamCityEscape(errorMessage)) +end + +local function reportNode(node, buffer, level) + buffer = buffer or {} + level = level or 0 + if node.status == TestEnum.TestStatus.Skipped then + return buffer + end + if node.planNode.type == TestEnum.NodeType.Describe then + table.insert(buffer, teamCityEnterSuite(node.planNode.phrase)) + for _, child in ipairs(node.children) do + reportNode(child, buffer, level + 1) + end + table.insert(buffer, teamCityLeaveSuite(node.planNode.phrase)) + else + table.insert(buffer, teamCityEnterCase(node.planNode.phrase)) + if node.status == TestEnum.TestStatus.Failure then + table.insert(buffer, teamCityFailCase(node.planNode.phrase, table.concat(node.errors,"\n"))) + end + table.insert(buffer, teamCityLeaveCase(node.planNode.phrase)) + end +end + +local function reportRoot(node) + local buffer = {} + + for _, child in ipairs(node.children) do + reportNode(child, buffer, 0) + end + + return buffer +end + +local function report(root) + local buffer = reportRoot(root) + + return table.concat(buffer, "\n") +end + +function TeamCityReporter.report(results) + local resultBuffer = { + "Test results:", + report(results), + ("%d passed, %d failed, %d skipped"):format( + results.successCount, + results.failureCount, + results.skippedCount + ) + } + + print(table.concat(resultBuffer, "\n")) + + if results.failureCount > 0 then + print(("%d test nodes reported failures."):format(results.failureCount)) + end + + if #results.errors > 0 then + print("Errors reported by tests:") + print("") + + for _, message in ipairs(results.errors) do + TestService:Error(message) + + -- Insert a blank line after each error + print("") + end + end +end + +return TeamCityReporter \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TextReporter.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TextReporter.lua new file mode 100644 index 0000000..e40d858 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TextReporter.lua @@ -0,0 +1,106 @@ +--[[ + The TextReporter uses the results from a completed test to output text to + standard output and TestService. +]] + +local TestService = game:GetService("TestService") + +local TestEnum = require(script.Parent.Parent.TestEnum) + +local INDENT = (" "):rep(3) +local STATUS_SYMBOLS = { + [TestEnum.TestStatus.Success] = "+", + [TestEnum.TestStatus.Failure] = "-", + [TestEnum.TestStatus.Skipped] = "~" +} +local UNKNOWN_STATUS_SYMBOL = "?" + +local TextReporter = {} + +local function compareNodes(a, b) + return a.planNode.phrase:lower() < b.planNode.phrase:lower() +end + +local function reportNode(node, buffer, level) + buffer = buffer or {} + level = level or 0 + + if node.status == TestEnum.TestStatus.Skipped then + return buffer + end + + local line + + if node.status then + local symbol = STATUS_SYMBOLS[node.status] or UNKNOWN_STATUS_SYMBOL + + line = ("%s[%s] %s"):format( + INDENT:rep(level), + symbol, + node.planNode.phrase + ) + else + line = ("%s%s"):format( + INDENT:rep(level), + node.planNode.phrase + ) + end + + table.insert(buffer, line) + table.sort(node.children, compareNodes) + + for _, child in ipairs(node.children) do + reportNode(child, buffer, level + 1) + end + + return buffer +end + +local function reportRoot(node) + local buffer = {} + table.sort(node.children, compareNodes) + + for _, child in ipairs(node.children) do + reportNode(child, buffer, 0) + end + + return buffer +end + +local function report(root) + local buffer = reportRoot(root) + + return table.concat(buffer, "\n") +end + +function TextReporter.report(results) + local resultBuffer = { + "Test results:", + report(results), + ("%d passed, %d failed, %d skipped"):format( + results.successCount, + results.failureCount, + results.skippedCount + ) + } + + print(table.concat(resultBuffer, "\n")) + + if results.failureCount > 0 then + print(("%d test nodes reported failures."):format(results.failureCount)) + end + + if #results.errors > 0 then + print("Errors reported by tests:") + print("") + + for _, message in ipairs(results.errors) do + TestService:Error(message) + + -- Insert a blank line after each error + print("") + end + end +end + +return TextReporter \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TextReporterQuiet.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TextReporterQuiet.lua new file mode 100644 index 0000000..cbbb1b4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/Reporters/TextReporterQuiet.lua @@ -0,0 +1,97 @@ +--[[ + Copy of TextReporter that doesn't output successful tests. + + This should be temporary, it's just a workaround to make CI environments + happy in the short-term. +]] + +local TestService = game:GetService("TestService") + +local TestEnum = require(script.Parent.Parent.TestEnum) + +local INDENT = (" "):rep(3) +local STATUS_SYMBOLS = { + [TestEnum.TestStatus.Success] = "+", + [TestEnum.TestStatus.Failure] = "-", + [TestEnum.TestStatus.Skipped] = "~" +} +local UNKNOWN_STATUS_SYMBOL = "?" + +local TextReporterQuiet = {} + +local function reportNode(node, buffer, level) + buffer = buffer or {} + level = level or 0 + + if node.status == TestEnum.TestStatus.Skipped then + return buffer + end + + local line + + if node.status ~= TestEnum.TestStatus.Success then + local symbol = STATUS_SYMBOLS[node.status] or UNKNOWN_STATUS_SYMBOL + + line = ("%s[%s] %s"):format( + INDENT:rep(level), + symbol, + node.planNode.phrase + ) + end + + table.insert(buffer, line) + + for _, child in ipairs(node.children) do + reportNode(child, buffer, level + 1) + end + + return buffer +end + +local function reportRoot(node) + local buffer = {} + + for _, child in ipairs(node.children) do + reportNode(child, buffer, 0) + end + + return buffer +end + +local function report(root) + local buffer = reportRoot(root) + + return table.concat(buffer, "\n") +end + +function TextReporterQuiet.report(results) + local resultBuffer = { + "Test results:", + report(results), + ("%d passed, %d failed, %d skipped"):format( + results.successCount, + results.failureCount, + results.skippedCount + ) + } + + print(table.concat(resultBuffer, "\n")) + + if results.failureCount > 0 then + print(("%d test nodes reported failures."):format(results.failureCount)) + end + + if #results.errors > 0 then + print("Errors reported by tests:") + print("") + + for _, message in ipairs(results.errors) do + TestService:Error(message) + + -- Insert a blank line after each error + print("") + end + end +end + +return TextReporterQuiet \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestBootstrap.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestBootstrap.lua new file mode 100644 index 0000000..e3641a5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestBootstrap.lua @@ -0,0 +1,147 @@ +--[[ + Provides an interface to quickly run and report tests from a given object. +]] + +local TestPlanner = require(script.Parent.TestPlanner) +local TestRunner = require(script.Parent.TestRunner) +local TextReporter = require(script.Parent.Reporters.TextReporter) + +local TestBootstrap = {} + +local function stripSpecSuffix(name) + return (name:gsub("%.spec$", "")) +end +local function isSpecScript(aScript) + return aScript:IsA("ModuleScript") and aScript.Name:match("%.spec$") +end + +local function getPath(module, root) + root = root or game + + local path = {} + local last = module + + if last.Name == "init.spec" then + -- Use the directory's node for init.spec files. + last = last.Parent + end + + while last ~= nil and last ~= root do + table.insert(path, stripSpecSuffix(last.Name)) + last = last.Parent + end + table.insert(path, stripSpecSuffix(root.Name)) + + return path +end + +local function toStringPath(tablePath) + local stringPath = "" + local first = true + for _, element in ipairs(tablePath) do + if first then + stringPath = element + first = false + else + stringPath = element .. " " .. stringPath + end + end + return stringPath +end + +function TestBootstrap:getModulesImpl(root, modules, current) + modules = modules or {} + current = current or root + + if isSpecScript(current) then + local method = require(current) + local path = getPath(current, root) + local pathString = toStringPath(path) + + table.insert(modules, { + method = method, + path = path, + pathStringForSorting = pathString:lower() + }) + end +end + +--[[ + Find all the ModuleScripts in this tree that are tests. +]] +function TestBootstrap:getModules(root) + local modules = {} + + self:getModulesImpl(root, modules) + + for _, child in ipairs(root:GetDescendants()) do + self:getModulesImpl(root, modules, child) + end + + return modules +end + +--[[ + Runs all test and reports the results using the given test reporter. + + If no reporter is specified, a reasonable default is provided. + + This function demonstrates the expected workflow with this testing system: + 1. Locate test modules + 2. Generate test plan + 3. Run test plan + 4. Report test results + + This means we could hypothetically present a GUI to the developer that shows + the test plan before we execute it, allowing them to toggle specific tests + before they're run, but after they've been identified! +]] +function TestBootstrap:run(roots, reporter, otherOptions) + reporter = reporter or TextReporter + + otherOptions = otherOptions or {} + local showTimingInfo = otherOptions["showTimingInfo"] or false + local testNamePattern = otherOptions["testNamePattern"] + local extraEnvironment = otherOptions["extraEnvironment"] or {} + + if type(roots) ~= "table" then + error(("Bad argument #1 to TestBootstrap:run. Expected table, got %s"):format(typeof(roots)), 2) + end + + local startTime = tick() + + local modules = {} + for _, subRoot in ipairs(roots) do + local newModules = self:getModules(subRoot) + + for _, newModule in ipairs(newModules) do + table.insert(modules, newModule) + end + end + + local afterModules = tick() + + local plan = TestPlanner.createPlan(modules, testNamePattern, extraEnvironment) + local afterPlan = tick() + + local results = TestRunner.runPlan(plan) + local afterRun = tick() + + reporter.report(results) + local afterReport = tick() + + if showTimingInfo then + local timing = { + ("Took %f seconds to locate test modules"):format(afterModules - startTime), + ("Took %f seconds to create test plan"):format(afterPlan - afterModules), + ("Took %f seconds to run tests"):format(afterRun - afterPlan), + ("Took %f seconds to report tests"):format(afterReport - afterRun), + } + + print(table.concat(timing, "\n")) + end + + return results +end + +return TestBootstrap \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestEnum.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestEnum.lua new file mode 100644 index 0000000..d8d31b7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestEnum.lua @@ -0,0 +1,28 @@ +--[[ + Constants used throughout the testing framework. +]] + +local TestEnum = {} + +TestEnum.TestStatus = { + Success = "Success", + Failure = "Failure", + Skipped = "Skipped" +} + +TestEnum.NodeType = { + Describe = "Describe", + It = "It", + BeforeAll = "BeforeAll", + AfterAll = "AfterAll", + BeforeEach = "BeforeEach", + AfterEach = "AfterEach" +} + +TestEnum.NodeModifier = { + None = "None", + Skip = "Skip", + Focus = "Focus" +} + +return TestEnum \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestPlan.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestPlan.lua new file mode 100644 index 0000000..5537f56 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestPlan.lua @@ -0,0 +1,304 @@ +--[[ + Represents a tree of tests that have been loaded but not necessarily + executed yet. + + TestPlan objects are produced by TestPlanner. +]] + +local TestEnum = require(script.Parent.TestEnum) +local Expectation = require(script.Parent.Expectation) + +local function newEnvironment(currentNode, extraEnvironment) + local env = {} + + if extraEnvironment then + if type(extraEnvironment) ~= "table" then + error(("Bad argument #2 to newEnvironment. Expected table, got %s"):format( + typeof(extraEnvironment)), 2) + end + + for key, value in pairs(extraEnvironment) do + env[key] = value + end + end + + local function addChild(phrase, callback, nodeType, nodeModifier) + local node = currentNode:addChild(phrase, nodeType, nodeModifier) + node.callback = callback + if nodeType == TestEnum.NodeType.Describe then + node:expand() + end + return node + end + + function env.describeFOCUS(phrase, callback) + addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.Focus) + end + + function env.describeSKIP(phrase, callback) + addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.Skip) + end + + function env.describe(phrase, callback, nodeModifier) + addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.None) + end + + function env.itFOCUS(phrase, callback) + addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Focus) + end + + function env.itSKIP(phrase, callback) + addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip) + end + + function env.itFIXME(phrase, callback) + local node = addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip) + warn("FIXME: broken test", node:getFullName()) + end + + function env.it(phrase, callback, nodeModifier) + addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.None) + end + + -- Incrementing counter used to ensure that beforeAll, afterAll, beforeEach, afterEach have unique phrases + local lifecyclePhaseId = 0 + + local lifecycleHooks = { + [TestEnum.NodeType.BeforeAll] = "beforeAll", + [TestEnum.NodeType.AfterAll] = "afterAll", + [TestEnum.NodeType.BeforeEach] = "beforeEach", + [TestEnum.NodeType.AfterEach] = "afterEach" + } + + for nodeType, name in pairs(lifecycleHooks) do + env[name] = function(callback) + addChild(name .. "_" .. tostring(lifecyclePhaseId), callback, nodeType, TestEnum.NodeModifier.None) + lifecyclePhaseId = lifecyclePhaseId + 1 + end + end + + function env.FIXME(optionalMessage) + warn("FIXME: broken test", currentNode:getFullName(), optionalMessage or "") + + currentNode.modifier = TestEnum.NodeModifier.Skip + end + + function env.FOCUS() + currentNode.modifier = TestEnum.NodeModifier.Focus + end + + function env.SKIP() + currentNode.modifier = TestEnum.NodeModifier.Skip + end + + --[[ + This function is deprecated. Calling it is a no-op beyond generating a + warning. + ]] + function env.HACK_NO_XPCALL() + warn("HACK_NO_XPCALL is deprecated. It is now safe to yield in an " .. + "xpcall, so this is no longer necessary. It can be safely deleted.") + end + + env.fit = env.itFOCUS + env.xit = env.itSKIP + env.fdescribe = env.describeFOCUS + env.xdescribe = env.describeSKIP + + env.expect = setmetatable({ + extend = function(...) + error("Cannot call \"expect.extend\" from within a \"describe\" node.") + end, + }, { + __call = function(_self, ...) + return Expectation.new(...) + end, + }) + + return env +end + +local TestNode = {} +TestNode.__index = TestNode + +--[[ + Create a new test node. A pointer to the test plan, a phrase to describe it + and the type of node it is are required. The modifier is optional and will + be None if left blank. +]] +function TestNode.new(plan, phrase, nodeType, nodeModifier) + nodeModifier = nodeModifier or TestEnum.NodeModifier.None + + local node = { + plan = plan, + phrase = phrase, + type = nodeType, + modifier = nodeModifier, + children = {}, + callback = nil, + parent = nil, + } + + node.environment = newEnvironment(node, plan.extraEnvironment) + return setmetatable(node, TestNode) +end + +local function getModifier(name, pattern, modifier) + if pattern and (modifier == nil or modifier == TestEnum.NodeModifier.None) then + if name:match(pattern) then + return TestEnum.NodeModifier.Focus + else + return TestEnum.NodeModifier.Skip + end + end + return modifier +end + +function TestNode:addChild(phrase, nodeType, nodeModifier) + if nodeType == TestEnum.NodeType.It then + for _, child in pairs(self.children) do + if child.phrase == phrase then + error("Duplicate it block found: " .. child:getFullName()) + end + end + end + + local childName = self:getFullName() .. " " .. phrase + nodeModifier = getModifier(childName, self.plan.testNamePattern, nodeModifier) + local child = TestNode.new(self.plan, phrase, nodeType, nodeModifier) + child.parent = self + table.insert(self.children, child) + return child +end + +--[[ + Join the names of all the nodes back to the parent. +]] +function TestNode:getFullName() + if self.parent then + local parentPhrase = self.parent:getFullName() + if parentPhrase then + return parentPhrase .. " " .. self.phrase + end + end + return self.phrase +end + +--[[ + Expand a node by setting its callback environment and then calling it. Any + further it and describe calls within the callback will be added to the tree. +]] +function TestNode:expand() + local originalEnv = getfenv(self.callback) + local callbackEnv = setmetatable({}, { __index = originalEnv }) + for key, value in pairs(self.environment) do + callbackEnv[key] = value + end + -- Copy 'script' directly to new env to make Studio debugger happy. + -- Studio debugger does not look into __index, because of security reasons + callbackEnv.script = originalEnv.script + setfenv(self.callback, callbackEnv) + + local success, result = xpcall(self.callback, function(message) + return debug.traceback(tostring(message), 2) + end) + + if not success then + self.loadError = result + end +end + +local TestPlan = {} +TestPlan.__index = TestPlan + +--[[ + Create a new, empty TestPlan. +]] +function TestPlan.new(testNamePattern, extraEnvironment) + local plan = { + children = {}, + testNamePattern = testNamePattern, + extraEnvironment = extraEnvironment, + } + + return setmetatable(plan, TestPlan) +end + +--[[ + Add a new child under the test plan's root node. +]] +function TestPlan:addChild(phrase, nodeType, nodeModifier) + nodeModifier = getModifier(phrase, self.testNamePattern, nodeModifier) + local child = TestNode.new(self, phrase, nodeType, nodeModifier) + table.insert(self.children, child) + return child +end + +--[[ + Add a new describe node with the given method as a callback. Generates or + reuses all the describe nodes along the path. +]] +function TestPlan:addRoot(path, method) + local curNode = self + for i = #path, 1, -1 do + local nextNode = nil + + for _, child in ipairs(curNode.children) do + if child.phrase == path[i] then + nextNode = child + break + end + end + + if nextNode == nil then + nextNode = curNode:addChild(path[i], TestEnum.NodeType.Describe) + end + + curNode = nextNode + end + + curNode.callback = method + curNode:expand() +end + +--[[ + Calls the given callback on all nodes in the tree, traversed depth-first. +]] +function TestPlan:visitAllNodes(callback, root, level) + root = root or self + level = level or 0 + + for _, child in ipairs(root.children) do + callback(child, level) + + self:visitAllNodes(callback, child, level + 1) + end +end + +--[[ + Visualizes the test plan in a simple format, suitable for debugging the test + plan's structure. +]] +function TestPlan:visualize() + local buffer = {} + self:visitAllNodes(function(node, level) + table.insert(buffer, (" "):rep(3 * level) .. node.phrase) + end) + return table.concat(buffer, "\n") +end + +--[[ + Gets a list of all nodes in the tree for which the given callback returns + true. +]] +function TestPlan:findNodes(callback) + local results = {} + self:visitAllNodes(function(node) + if callback(node) then + table.insert(results, node) + end + end) + return results +end + +return TestPlan diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestPlanner.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestPlanner.lua new file mode 100644 index 0000000..6612ff5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestPlanner.lua @@ -0,0 +1,40 @@ +--[[ + Turns a series of specification functions into a test plan. + + Uses a TestPlanBuilder to keep track of the state of the tree being built. +]] +local TestPlan = require(script.Parent.TestPlan) + +local TestPlanner = {} + +--[[ + Create a new TestPlan from a list of specification functions. + + These functions should call a combination of `describe` and `it` (and their + variants), which will be turned into a test plan to be executed. + + Parameters: + - modulesList - list of tables describing test modules { + method, -- specification function described above + path, -- array of parent entires, first element is the leaf that owns `method` + pathStringForSorting -- a string representation of `path`, used for sorting of the test plan + } + - testNamePattern - Only tests matching this Lua pattern string will run. Pass empty or nil to run all tests + - extraEnvironment - Lua table holding additional functions and variables to be injected into the specification + function during execution +]] +function TestPlanner.createPlan(modulesList, testNamePattern, extraEnvironment) + local plan = TestPlan.new(testNamePattern, extraEnvironment) + + table.sort(modulesList, function(a, b) + return a.pathStringForSorting < b.pathStringForSorting + end) + + for _, module in ipairs(modulesList) do + plan:addRoot(module.path, module.method) + end + + return plan +end + +return TestPlanner \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestResults.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestResults.lua new file mode 100644 index 0000000..c39c829 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestResults.lua @@ -0,0 +1,112 @@ +--[[ + Represents a tree of test results. + + Each node in the tree corresponds directly to a node in a corresponding + TestPlan, accessible via the 'planNode' field. + + TestResults objects are produced by TestRunner using TestSession as state. +]] + +local TestEnum = require(script.Parent.TestEnum) + +local STATUS_SYMBOLS = { + [TestEnum.TestStatus.Success] = "+", + [TestEnum.TestStatus.Failure] = "-", + [TestEnum.TestStatus.Skipped] = "~" +} + +local TestResults = {} + +TestResults.__index = TestResults + +--[[ + Create a new TestResults tree that's linked to the given TestPlan. +]] +function TestResults.new(plan) + local self = { + successCount = 0, + failureCount = 0, + skippedCount = 0, + planNode = plan, + children = {}, + errors = {} + } + + setmetatable(self, TestResults) + + return self +end + +--[[ + Create a new result node that can be inserted into a TestResult tree. +]] +function TestResults.createNode(planNode) + local node = { + planNode = planNode, + children = {}, + errors = {}, + status = nil + } + + return node +end + +--[[ + Visit all test result nodes, depth-first. +]] +function TestResults:visitAllNodes(callback, root) + root = root or self + + for _, child in ipairs(root.children) do + callback(child) + + self:visitAllNodes(callback, child) + end +end + +--[[ + Creates a debug visualization of the test results. +]] +function TestResults:visualize(root, level) + root = root or self + level = level or 0 + + local buffer = {} + + for _, child in ipairs(root.children) do + if child.planNode.type == TestEnum.NodeType.It then + local symbol = STATUS_SYMBOLS[child.status] or "?" + local str = ("%s[%s] %s"):format( + (" "):rep(3 * level), + symbol, + child.planNode.phrase + ) + + if child.messages and #child.messages > 0 then + str = str .. "\n " .. (" "):rep(3 * level) .. table.concat(child.messages, "\n " .. (" "):rep(3 * level)) + end + + table.insert(buffer, str) + else + local str = ("%s%s"):format( + (" "):rep(3 * level), + child.planNode.phrase or "" + ) + + if child.status then + str = str .. (" (%s)"):format(child.status) + end + + table.insert(buffer, str) + + if #child.children > 0 then + local text = self:visualize(child, level + 1) + table.insert(buffer, text) + end + end + end + + return table.concat(buffer, "\n") +end + +return TestResults \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestRunner.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestRunner.lua new file mode 100644 index 0000000..9bf7d35 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestRunner.lua @@ -0,0 +1,186 @@ +--[[ + Contains the logic to run a test plan and gather test results from it. + + TestRunner accepts a TestPlan object, executes the planned tests, and + produces a TestResults object. While the tests are running, the system's + state is contained inside a TestSession object. +]] + +local TestEnum = require(script.Parent.TestEnum) +local TestSession = require(script.Parent.TestSession) +local LifecycleHooks = require(script.Parent.LifecycleHooks) + +local RUNNING_GLOBAL = "__TESTEZ_RUNNING_TEST__" + +local TestRunner = { + environment = {} +} + +local function wrapExpectContextWithPublicApi(expectationContext) + return setmetatable({ + extend = function(...) + expectationContext:extend(...) + end, + }, { + __call = function(_self, ...) + return expectationContext:startExpectationChain(...) + end, + }) +end + +--[[ + Runs the given TestPlan and returns a TestResults object representing the + results of the run. +]] +function TestRunner.runPlan(plan) + local session = TestSession.new(plan) + local lifecycleHooks = LifecycleHooks.new() + + local exclusiveNodes = plan:findNodes(function(node) + return node.modifier == TestEnum.NodeModifier.Focus + end) + + session.hasFocusNodes = #exclusiveNodes > 0 + + TestRunner.runPlanNode(session, plan, lifecycleHooks) + + return session:finalize() +end + +--[[ + Run the given test plan node and its descendants, using the given test + session to store all of the results. +]] +function TestRunner.runPlanNode(session, planNode, lifecycleHooks) + local function runCallback(callback, messagePrefix) + local success = true + local errorMessage + -- Any code can check RUNNING_GLOBAL to fork behavior based on + -- whether a test is running. We use this to avoid accessing + -- protected APIs; it's a workaround that will go away someday. + _G[RUNNING_GLOBAL] = true + + messagePrefix = messagePrefix or "" + + local testEnvironment = getfenv(callback) + + for key, value in pairs(TestRunner.environment) do + testEnvironment[key] = value + end + + testEnvironment.fail = function(message) + if message == nil then + message = "fail() was called." + end + + success = false + errorMessage = messagePrefix .. debug.traceback(tostring(message), 2) + end + + testEnvironment.expect = wrapExpectContextWithPublicApi(session:getExpectationContext()) + + local context = session:getContext() + + local nodeSuccess, nodeResult = xpcall( + function() + callback(context) + end, + function(message) + return messagePrefix .. debug.traceback(tostring(message), 2) + end + ) + + -- If a node threw an error, we prefer to use that message over + -- one created by fail() if it was set. + if not nodeSuccess then + success = false + errorMessage = nodeResult + end + + _G[RUNNING_GLOBAL] = nil + + return success, errorMessage + end + + local function runNode(childPlanNode) + -- Errors can be set either via `error` propagating upwards or + -- by a test calling fail([message]). + + for _, hook in ipairs(lifecycleHooks:getBeforeEachHooks()) do + local success, errorMessage = runCallback(hook, "beforeEach hook: ") + if not success then + return false, errorMessage + end + end + + do + local success, errorMessage = runCallback(childPlanNode.callback) + if not success then + return false, errorMessage + end + end + + for _, hook in ipairs(lifecycleHooks:getAfterEachHooks()) do + local success, errorMessage = runCallback(hook, "afterEach hook: ") + if not success then + return false, errorMessage + end + end + + return true, nil + end + + lifecycleHooks:pushHooksFrom(planNode) + + local halt = false + for _, hook in ipairs(lifecycleHooks:getBeforeAllHooks()) do + local success, errorMessage = runCallback(hook, "beforeAll hook: ") + if not success then + session:addDummyError("beforeAll", errorMessage) + halt = true + end + end + + if not halt then + for _, childPlanNode in ipairs(planNode.children) do + if childPlanNode.type == TestEnum.NodeType.It then + session:pushNode(childPlanNode) + if session:shouldSkip() then + session:setSkipped() + else + local success, errorMessage = runNode(childPlanNode) + + if success then + session:setSuccess() + else + session:setError(errorMessage) + end + end + session:popNode() + elseif childPlanNode.type == TestEnum.NodeType.Describe then + session:pushNode(childPlanNode) + TestRunner.runPlanNode(session, childPlanNode, lifecycleHooks) + + -- Did we have an error trying build a test plan? + if childPlanNode.loadError then + local message = "Error during planning: " .. childPlanNode.loadError + session:setError(message) + else + session:setStatusFromChildren() + end + session:popNode() + end + end + end + + for _, hook in ipairs(lifecycleHooks:getAfterAllHooks()) do + local success, errorMessage = runCallback(hook, "afterAll hook: ") + if not success then + session:addDummyError("afterAll", errorMessage) + end + end + + lifecycleHooks:popHooks() +end + +return TestRunner diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestSession.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestSession.lua new file mode 100644 index 0000000..285e11c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/TestSession.lua @@ -0,0 +1,243 @@ +--[[ + Represents the state relevant while executing a test plan. + + Used by TestRunner to produce a TestResults object. + + Uses the same tree building structure as TestPlanBuilder; TestSession keeps + track of a stack of nodes that represent the current path through the tree. +]] + +local TestEnum = require(script.Parent.TestEnum) +local TestResults = require(script.Parent.TestResults) +local Context = require(script.Parent.Context) +local ExpectationContext = require(script.Parent.ExpectationContext) + +local TestSession = {} + +TestSession.__index = TestSession + +--[[ + Create a TestSession related to the given TestPlan. + + The resulting TestResults object will be linked to this TestPlan. +]] +function TestSession.new(plan) + local self = { + results = TestResults.new(plan), + nodeStack = {}, + contextStack = {}, + expectationContextStack = {}, + hasFocusNodes = false + } + + setmetatable(self, TestSession) + + return self +end + +--[[ + Calculate success, failure, and skipped test counts in the tree at the + current point in the execution. +]] +function TestSession:calculateTotals() + local results = self.results + + results.successCount = 0 + results.failureCount = 0 + results.skippedCount = 0 + + results:visitAllNodes(function(node) + local status = node.status + local nodeType = node.planNode.type + + if nodeType == TestEnum.NodeType.It then + if status == TestEnum.TestStatus.Success then + results.successCount = results.successCount + 1 + elseif status == TestEnum.TestStatus.Failure then + results.failureCount = results.failureCount + 1 + elseif status == TestEnum.TestStatus.Skipped then + results.skippedCount = results.skippedCount + 1 + end + end + end) +end + +--[[ + Gathers all of the errors reported by tests and puts them at the top level + of the TestResults object. +]] +function TestSession:gatherErrors() + local results = self.results + + results.errors = {} + + results:visitAllNodes(function(node) + if #node.errors > 0 then + for _, message in ipairs(node.errors) do + table.insert(results.errors, message) + end + end + end) +end + +--[[ + Calculates test totals, verifies the tree is valid, and returns results. +]] +function TestSession:finalize() + if #self.nodeStack ~= 0 then + error("Cannot finalize TestResults with nodes still on the stack!", 2) + end + + self:calculateTotals() + self:gatherErrors() + + return self.results +end + +--[[ + Create a new test result node and push it onto the navigation stack. +]] +function TestSession:pushNode(planNode) + local node = TestResults.createNode(planNode) + local lastNode = self.nodeStack[#self.nodeStack] or self.results + table.insert(lastNode.children, node) + table.insert(self.nodeStack, node) + + local lastContext = self.contextStack[#self.contextStack] + local context = Context.new(lastContext) + table.insert(self.contextStack, context) + + local lastExpectationContext = self.expectationContextStack[#self.expectationContextStack] + local expectationContext = ExpectationContext.new(lastExpectationContext) + table.insert(self.expectationContextStack, expectationContext) +end + +--[[ + Pops a node off of the navigation stack. +]] +function TestSession:popNode() + assert(#self.nodeStack > 0, "Tried to pop from an empty node stack!") + table.remove(self.nodeStack, #self.nodeStack) + table.remove(self.contextStack, #self.contextStack) + table.remove(self.expectationContextStack, #self.expectationContextStack) +end + +--[[ + Gets the Context object for the current node. +]] +function TestSession:getContext() + assert(#self.contextStack > 0, "Tried to get context from an empty stack!") + return self.contextStack[#self.contextStack] +end + + +function TestSession:getExpectationContext() + assert(#self.expectationContextStack > 0, "Tried to get expectationContext from an empty stack!") + return self.expectationContextStack[#self.expectationContextStack] +end + +--[[ + Tells whether the current test we're in should be skipped. +]] +function TestSession:shouldSkip() + -- If our test tree had any exclusive tests, then normal tests are skipped! + if self.hasFocusNodes then + for i = #self.nodeStack, 1, -1 do + local node = self.nodeStack[i] + + -- Skipped tests are still skipped + if node.planNode.modifier == TestEnum.NodeModifier.Skip then + return true + end + + -- Focused tests are the only ones that aren't skipped + if node.planNode.modifier == TestEnum.NodeModifier.Focus then + return false + end + end + + return true + else + for i = #self.nodeStack, 1, -1 do + local node = self.nodeStack[i] + + if node.planNode.modifier == TestEnum.NodeModifier.Skip then + return true + end + end + end + + return false +end + +--[[ + Set the current node's status to Success. +]] +function TestSession:setSuccess() + assert(#self.nodeStack > 0, "Attempting to set success status on empty stack") + self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Success +end + +--[[ + Set the current node's status to Skipped. +]] +function TestSession:setSkipped() + assert(#self.nodeStack > 0, "Attempting to set skipped status on empty stack") + self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Skipped +end + +--[[ + Set the current node's status to Failure and adds a message to its list of + errors. +]] +function TestSession:setError(message) + assert(#self.nodeStack > 0, "Attempting to set error status on empty stack") + local last = self.nodeStack[#self.nodeStack] + last.status = TestEnum.TestStatus.Failure + table.insert(last.errors, message) +end + +--[[ + Add a dummy child node to the current node to hold the given error. This + allows an otherwise empty describe node to report an error in a more natural + way. +]] +function TestSession:addDummyError(phrase, message) + self:pushNode({type = TestEnum.NodeType.It, phrase = phrase}) + self:setError(message) + self:popNode() + self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Failure +end + +--[[ + Set the current node's status based on that of its children. If all children + are skipped, mark it as skipped. If any are fails, mark it as failed. + Otherwise, mark it as success. +]] +function TestSession:setStatusFromChildren() + assert(#self.nodeStack > 0, "Attempting to set status from children on empty stack") + + local last = self.nodeStack[#self.nodeStack] + local status = TestEnum.TestStatus.Success + local skipped = true + + -- If all children were skipped, then we were skipped + -- If any child failed, then we failed! + for _, child in ipairs(last.children) do + if child.status ~= TestEnum.TestStatus.Skipped then + skipped = false + + if child.status == TestEnum.TestStatus.Failure then + status = TestEnum.TestStatus.Failure + end + end + end + + if skipped then + status = TestEnum.TestStatus.Skipped + end + + last.status = status +end + +return TestSession diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/init.lua new file mode 100644 index 0000000..9c702a1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_testez/testez/init.lua @@ -0,0 +1,40 @@ +local Expectation = require(script.Expectation) +local TestBootstrap = require(script.TestBootstrap) +local TestEnum = require(script.TestEnum) +local TestPlan = require(script.TestPlan) +local TestPlanner = require(script.TestPlanner) +local TestResults = require(script.TestResults) +local TestRunner = require(script.TestRunner) +local TestSession = require(script.TestSession) +local TextReporter = require(script.Reporters.TextReporter) +local TextReporterQuiet = require(script.Reporters.TextReporterQuiet) +local TeamCityReporter = require(script.Reporters.TeamCityReporter) + +local function run(testRoot, callback) + local modules = TestBootstrap:getModules(testRoot) + local plan = TestPlanner.createPlan(modules) + local results = TestRunner.runPlan(plan) + + callback(results) +end + +local TestEZ = { + run = run, + + Expectation = Expectation, + TestBootstrap = TestBootstrap, + TestEnum = TestEnum, + TestPlan = TestPlan, + TestPlanner = TestPlanner, + TestResults = TestResults, + TestRunner = TestRunner, + TestSession = TestSession, + + Reporters = { + TextReporter = TextReporter, + TextReporterQuiet = TextReporterQuiet, + TeamCityReporter = TeamCityReporter, + }, +} + +return TestEZ \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/StringUtilities.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/StringUtilities.lua new file mode 100644 index 0000000..d7885b1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/StringUtilities.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_string-utilities"]["string-utilities"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/lock.toml new file mode 100644 index 0000000..7863203 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/lock.toml @@ -0,0 +1,9 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "roblox/url-builder" +version = "1.0.4" +commit = "cc593441e3efee555f0d6789c5df45138947909f" +source = "url+https://github.com/roblox/url-builder" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "StringUtilities roblox/string-utilities 1.0.0 url+https://github.com/roblox/string-utilities", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBase.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBase.lua new file mode 100644 index 0000000..7f3df53 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBase.lua @@ -0,0 +1,93 @@ + +local ContentProvider = game:GetService("ContentProvider") + +--- base configuration ----------------------------- +local baseUrl = ContentProvider.BaseUrl +baseUrl = string.gsub(baseUrl, ".*://", "") +baseUrl = string.gsub(baseUrl, "/.*", "") +baseUrl = string.gsub(baseUrl, "^www%.", "") + +local UrlBase = {} + +--[[ + builds the base URL for an API + + name (string): base name of the API, eg: "games" + params (optional table): a configuration table with any of the following: + * proto (optional string): the URL protocol, eg "http", "ftp", defaults to "https" + * version (optional int, string): an optional version, eg "1" will append "/v1" to the URL + * path (optional string): a subpath to append to the URL, no leading slash + * secure (optional boolean): equivalent to proto = "https" + all strings can be empty (including name), output will adapt accordingly + + alternatively, version can be provided as second argument eg ("games", 1) +]] +function UrlBase.new(name, params) + assert(type(name) == "string", "UrlBase.new: `name` should be a string") + if params == nil then + params = {} + end + if type(params) == "number" or type(params) == "string" then + params = {version = params} + end + assert(type(params) == "table", "UrlBase.new: `params` should be a table") + local proto = params.proto + local version = params.version + local path = params.path + if proto == nil then + if params.secure == false then + proto = "http" + else + proto = "https" + end + end + local urlbase = proto + if #proto > 0 then + urlbase = urlbase .. "://" + end + urlbase = urlbase .. name + if #name > 0 then + urlbase = urlbase .. "." + end + urlbase = urlbase .. baseUrl + if version ~= nil and #tostring(version) > 0 then + urlbase = urlbase .. "/v" .. tostring(version) + end + if path ~= nil and #path > 0 then + urlbase = urlbase .. "/" .. path + end + return urlbase +end + +local isQQ = string.sub(baseUrl, -6) == "qq.com" + +-- from Url.lua +UrlBase.API = UrlBase.new("api") +UrlBase.APIS = UrlBase.new("apis") +UrlBase.AUTH = UrlBase.new("auth") +UrlBase.CHAT = UrlBase.new("chat") +UrlBase.FRIENDS = UrlBase.new("friends", 1) +UrlBase.ASSETGAME = UrlBase.new("assetgame") +UrlBase.GAMES = UrlBase.new("games", 1) +UrlBase.NOTIFICATION = UrlBase.new("notification", 2) +UrlBase.PRESENCE = UrlBase.new("presence", 1) +UrlBase.REALTIME = UrlBase.new("realtime") +UrlBase.WEB = UrlBase.new("web") +UrlBase.WWW = UrlBase.new("www") +UrlBase.ASD = UrlBase.new("ads", 1) +UrlBase.FOLLOWINGS = UrlBase.new("followings", 1) +UrlBase.PREMIUM = UrlBase.new("premiumfeatures", 1) +UrlBase.BLOG = "https://blog.roblox.com" +UrlBase.CORP = isQQ and "https://roblox.qq.com" or "https://corp.roblox.com" +-- from Http.lua +UrlBase.ACCOUNTSETTINGS = UrlBase.new("accountsettings") +UrlBase.BADGES = UrlBase.new("badges", 1) +UrlBase.INVENTORY = UrlBase.new("inventory", 1) +UrlBase.CATALOG = UrlBase.new("catalog", 1) +-- from AEWebApi.lua +UrlBase.AVATAR = UrlBase.new("avatar", 1) + +UrlBase.MOBILENAV = "robloxmobile://navigation" +UrlBase.APPSFLYER = "https://ro.blox.com" + +return UrlBase diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBuilder.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBuilder.lua new file mode 100644 index 0000000..126d363 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBuilder.lua @@ -0,0 +1,390 @@ +local Packages = script.Parent.Parent + +local Cryo = require(Packages.Cryo) +local StringUtilities = require(Packages.StringUtilities) +local StringTrim = StringUtilities.StringTrim +local StringSplit = StringUtilities.StringSplit +local encodeURIComponent = require(script.Parent.encodeURIComponent) +local UrlBase = require(script.Parent.UrlBase) + +local GameUrlPatterns = require(script.Parent.UrlPatterns.GameUrlPatterns) +local UserUrlPatterns = require(script.Parent.UrlPatterns.UserUrlPatterns) +local StaticUrlPatterns = require(script.Parent.UrlPatterns.StaticUrlPatterns) +local CatalogUrlPatterns = require(script.Parent.UrlPatterns.CatalogUrlPatterns) + +local UrlBuilder = {} + +--[[ + UTILITY FUNCTIONS +]] + +-- splits by separator, trims whitspaces from each part and filters out empty ones +local function splitAndTrim(input, separator, limit) + local list = StringSplit(input, separator, limit) + list = Cryo.List.map(list, function(item) + return StringTrim(item) + end) + list = Cryo.List.filter(list, function(item) + return #item > 0 + end) + return list +end + +--[[ + PATTERN VALIDATION +]] + +-- a value should be a string, number, or a table of strings and numbers only +local function validateValueType(value) + local valueType = type(value) + if valueType == "string" then + return true + elseif valueType == "number" then + return true + elseif valueType == "table" then + for _, item in ipairs(value) do + local itemType = type(item) + if itemType ~= "string" and itemType ~= "number" then + return false + end + end + return true + else + return false + end +end + +-- validate that item is either a literal (no check needed), or a valid placeholder +local function assertPlaceholder(element) + if string.sub(element, 1, 1) == "{" then + assert(string.sub(element, -1, -1) == "}", "invalid pattern: placeholder items should end with `}`") + end +end + +-- validate a pattern's path or query element +local function assertElementIsValid(element, inQuery) + assert(type(element) == "table", "invalid pattern: elements should all be tables") + if inQuery then + assert(type(element.name) == "string", "invalid pattern: element name should be a string") + assert(#element.name > 0, "invalid pattern: element name should not be empty") + end + assert( + type(element.value) == "string" or type(element.value) == "number", + "invalid pattern: element value should be a string or number" + ) + if type(element.value) == "string" then + assertPlaceholder(element.value) + end + if element.optional ~= nil then + assert(type(element.optional) == "boolean", "invalid pattern: element optional should be a boolean") + end + if element.default ~= nil then + assert( + validateValueType(element.default), + "invalid pattern: element default should be a string, number, or a table of strings and numbers only" + ) + end + if element.collect ~= nil then + assert( + element.collect == "multi" or element.collect == "csv", + "invalid pattern: element optional should be one of `multi`, `csv`" + ) + end +end + +-- validate a full pattern object, could use a validator library in the future +local function assertPatternIsValid(pattern) + assert(type(pattern.base) == "string", "invalid pattern: base should be a string") + assert(#pattern.base > 0, "invalid pattern: base should not be empty") + assert(type(pattern.path) == "table", "invalid pattern: path should be a table") + for _, element in ipairs(pattern.path) do + assertElementIsValid(element, false) + end + if pattern.query ~= nil then + assert(type(pattern.query) == "table", "invalid pattern: query should be a table") + end + if type(pattern.query) == "table" then + for _, element in ipairs(pattern.query) do + assertElementIsValid(element, true) + end + end + if pattern.hash ~= nil then + assert(type(pattern.hash) == "string", "invalid pattern: hash should be a string") + end +end + +--[[ + STRING PATTERNS +]] + +-- converts a "/" or "&" delimited string into a list or path/query elements +local function buildElementsFromString(elements, inQuery) + local separator = inQuery and "&" or "/" + local elements = splitAndTrim(elements, separator) + elements = Cryo.List.map(elements, function(element) + local elementName = nil + local elementValue = StringTrim(element) + local elementOptional = nil + local elementDefault = nil + local elementCollect = nil + if inQuery then + local queryitems = StringSplit(elementValue, "=", 2) + elementName = StringTrim(queryitems[1]) + elementValue = queryitems[2] + if string.sub(elementName, 1, 1) == "{" then + elementName = StringTrim(elementName, "{}") + elementName = StringSplit(elementName, "|", 2)[1] + if elementValue == nil then + elementValue = queryitems[1] + end + elementName = StringTrim(elementName) + end + elementCollect = "multi" + end + if elementValue ~= nil and string.find(elementValue, "^{.*}$") then + elementValue = StringTrim(elementValue, "{}") + local valueitems = StringSplit(elementValue, "|", 2) + elementValue = StringTrim(valueitems[1]) + if #valueitems > 1 then + elementOptional = true + if #(valueitems[2]) > 1 then + elementDefault = valueitems[2] + end + end + elementValue = "{" .. elementValue .. "}" + end + return { + name = elementName, + value = elementValue, + optional = elementOptional, + default = elementDefault, + collect = elementCollect, + } + end) + return elements +end + +local function buildQueryStringFromTable(elements) + local stringpattern = {} + for name, value in pairs(elements) do + table.insert(stringpattern, name .. "=" .. value) + end + return table.concat(stringpattern, "&") +end + +-- expands a pattern, replacing string parts with element tables +local function simplifyPattern(pattern) + local patternbase = pattern.base + local patternpath = pattern.path + local patternquery = pattern.query + local patternhash = pattern.hash + if patternbase ~= nil and UrlBase[string.upper(patternbase)] ~= nil then + patternbase = UrlBase[string.upper(patternbase)] + end + if type(patternpath) == "string" then + patternpath = buildElementsFromString(patternpath, false) + end + if type(patternquery) == "table" and patternquery[1] == nil then + patternquery = buildQueryStringFromTable(patternquery) + end + if type(patternquery) == "string" then + patternquery = buildElementsFromString(patternquery, true) + end + return { + base = patternbase, + path = patternpath, + query = patternquery, + hash = patternhash, + } +end + +--[[ + PATTERN RESOLUTION +]] + +local function concatValues(elementValues, inQuery) + -- suppress all empty values, avoids double slashes and empty query params + elementValues = Cryo.List.filter(elementValues, function(value) + if inQuery then + return #splitAndTrim(value, "=") > 1 + end + return #value > 0 + end) + local separator = inQuery and "&" or "/" + return table.concat(elementValues, separator) +end + +-- resolves the element to a string, from the given input table +local function resolveElement(element, input, inQuery) + if input == nil then + input = {} + end + local elementValues + if type(element.value) == "string" and string.sub(element.value, 1, 1) == "{" then + elementValues = input[string.sub(element.value, 2, -2)] + if elementValues == nil then + elementValues = element.default + end + if not element.optional then + assert(elementValues ~= nil, "UrlBuilder: missing parameter: `" .. element.value .. "`") + end + if elementValues == nil then + -- at this point optional == true, so we remove the element from output + elementValues = {} + end + assert( + validateValueType(elementValues), + "UrlBuilder: invalid parameter: `" .. element.value .. "`, " .. + "should be a string, number, or a table of strings and numbers only" + ) + else + elementValues = element.value + end + if type(elementValues) ~= "table" then + elementValues = {elementValues} + end + elementValues = Cryo.List.map(elementValues, function(value) + return encodeURIComponent(tostring(value)) + end) + if inQuery then + -- we are resolving a query parameter + if element.collect == "csv" then + elementValues = {table.concat(elementValues, "%2C")} -- "," + end + elementValues = Cryo.List.map(elementValues, function(value) + return encodeURIComponent(element.name) .. "=" .. value + end) + end + return concatValues(elementValues, inQuery) +end + +local function resolveElementList(elementList, input, inQuery) + local elementValues = Cryo.List.map(elementList or {}, function(element) + return resolveElement(element, input, inQuery) + end) + return concatValues(elementValues, inQuery) +end + +--[[ + PATTERN CONSTRUCTION +]] + +--[[ + creates a new URL builder function for the given pattern + the function can then be called with an input (table) to generate a URL + + pattern (table): pattern specification with the following: + * base (string): the domain and base path for the URL, eg "http://static.roblox.com" + * the UrlBase module exposes a list of available APIs, or can be used to properly build a new base + * path (string or table): list of path elements, one of + * (string): "/" delimited string, each element can be a literal or a "{}" enclosed placeholder + * (table): list of full path elements, see details below + * query (optional table): list of querystring elements, one of + * (string) : query string, with optional placeholders, eg "imageId={images}&size=140" + * (table): dictionary of {name = value, ...} pairs, eg {imageId = "{images}", size = 140} + * (table): list of full query elements, see details below + * hash (optional string): will be appended AS IS (no placeholders or UrlEncode), separated by "#" + + path and query elements are tables with the following (some only apply to query elements): + * name (string): *query only* the name of the query parameter + * value (string or number): value of the element, can be a literal or a "{}" enclosed placeholder + * optional (optional boolean): marks the element as optional, if placeholder value can't be found + * default (optional string): value if placeholder can't be found, implies "optional = true" + * collect (optional string): *query only* how to resolve table values, one of + * "multi": param and value will be repeated, eg "p=v1&p=v2&p=v3", this is the default + * "csv": one param, values will be concatenated, eg "p=v1,v2,v3" +]] +function UrlBuilder.new(pattern) + pattern = simplifyPattern(pattern) + assertPatternIsValid(pattern) + return function(input, expected) + local url = StringTrim(pattern.base, "/", {right = true}) + local path = resolveElementList(pattern.path, input, false) + if #path > 0 then + url = url .. "/" .. path + end + -- append slash if URL only consists of "proto://domain" + if string.match(url, "[^/]/[^/]") == nil then + url = url .. "/" + end + local query = resolveElementList(pattern.query, input, true) + if #query > 0 then + url = url .. "?" .. query + end + if pattern.hash and #pattern.hash > 0 then + url = url .. "#" .. pattern.hash + end + -- testing, for development use only + if expected then + if url ~= expected then + warn("UrlBuilder: unexpected output for pattern:") + warn("UrlBuilder: expected `" .. expected .. "`") + warn("UrlBuilder: actual `" .. url .. "`") + end + return expected + end + return url + end +end + +--[[ + creates a new URL builder function from a string pattern + + pattern format: "base:path/to/{endpoint}?param1={value1}&{param2}" +]] +function UrlBuilder.fromString(pattern) + local patternitems = StringSplit(pattern, ":", 2) + if #patternitems < 2 then + patternitems = {"", patternitems[1]} + end + local patternbase = patternitems[1] + patternitems = StringSplit(patternitems[2], "%#", 2) + local patternhash = patternitems[2] or "" + patternitems = StringSplit(patternitems[1], "%?", 2) + local patternpath = patternitems[1] or "" + local patternquery = patternitems[2] or "" + -- in case ":" was the protocol delimiter of a full url (eg http://domain/...) + if string.sub(patternpath, 1, 2) == "//" then + patternitems = StringSplit(string.sub(patternpath, 3), "/", 2) + patternbase = patternbase .. "://" .. patternitems[1] + patternpath = patternitems[2] or "/" + end + return UrlBuilder.new({ + base = StringTrim(patternbase), + path = StringTrim(patternpath), + query = StringTrim(patternquery), + hash = patternhash, + }) +end + +--[[ + CONVENIENCE SHORTHANDS +]] + +function UrlBuilder.addQueryString(url, query) + local pattern = StringTrim(url) + local queryindex = string.find(pattern, "%?") + if queryindex == nil then + pattern = pattern .. "?" + elseif queryindex < #pattern then + pattern = pattern .. "&" + end + local queryitems = Cryo.Dictionary.keys(query) + queryitems = Cryo.List.map(queryitems, function(param) + return "{" .. param .. "}" + end) + queryitems = table.concat(queryitems, "&") + pattern = pattern .. queryitems + return UrlBuilder.fromString(pattern)(query) +end + +--[[ + PATTERN REGISTRATION +]] + +UrlBuilder.game = GameUrlPatterns(UrlBuilder) +UrlBuilder.user = UserUrlPatterns(UrlBuilder) +UrlBuilder.catalog = CatalogUrlPatterns(UrlBuilder) +UrlBuilder.static = StaticUrlPatterns(UrlBuilder) + +return UrlBuilder diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBuilder.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBuilder.spec.lua new file mode 100644 index 0000000..b2e39f5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlBuilder.spec.lua @@ -0,0 +1,85 @@ + +return function() + local UrlBuilder = require(script.Parent.UrlBuilder) + + describe("simple", function() + local pattern = UrlBuilder.fromString("api:game/{universeId}/thumbnail?size={pxWidth|150}") + + it("should generate proper url", function() + local url = pattern({ + universeId = "1356984689", + pxWidth = 720, + }) + expect(url).to.equal("https://api.roblox.com/game/1356984689/thumbnail?size=720") + end) + + it("table values duplicate url parts", function() + local url = pattern({ + universeId = {"1356984689", "8745654337"}, + pxWidth = {720, 320}, + }) + expect(url).to.equal("https://api.roblox.com/game/1356984689/8745654337/thumbnail?size=720&size=320") + end) + + it("empty table should remove url part", function() + local url = pattern({ + universeId = "1356984689", + pxWidth = {}, + }) + expect(url).to.equal("https://api.roblox.com/game/1356984689/thumbnail") + end) + + it("missing values should throw", function() + local function getUrl() + pattern({pxWidth = 720}) + end + expect(getUrl).to.throw() + end) + + it("missing optional values should not throw", function() + local url = pattern({universeId = "1356984689"}) + expect(url).to.equal("https://api.roblox.com/game/1356984689/thumbnail?size=150") + end) + + end) + + describe("complex", function() + local pattern = UrlBuilder.fromString(" https://roblox.com/ test/{special}/path / // {empty}/{multiple}/ x ? {num}¶m1=static&dupl={multiple}&") + + it("complex pattern/values with nultiple edge cases", function() + local url = pattern({ + special = "$pec!al", + num = 568, + multiple = {"ab/", "c d"}, + empty = "", + }) + expect(url).to.equal("https://roblox.com/test/%24pec!al/path/ab%2F/c%20d/x?num=568¶m1=static&dupl=ab%2F&dupl=c%20d") + end) + end) + + describe("invalid patterns", function() + + it("should throw on malformed placeholder", function() + local function invalidPattern() + UrlBuilder.fromString("api:game/{universeI}d/thumbnail?size={pxWidth|150}") + end + expect(invalidPattern).to.throw() + end) + + it("should throw on missing base", function() + local function invalidPattern() + UrlBuilder.fromString("/game/{universeId}/thumbnail?size={pxWidth|150}") + end + expect(invalidPattern).to.throw() + end) + + it("should throw on missing parameter name", function() + local function invalidPattern() + UrlBuilder.fromString("api:game/{universeId}/thumbnail?={pxWidth|150}") + end + expect(invalidPattern).to.throw() + end) + + end) + +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/.robloxrc b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/.robloxrc new file mode 100644 index 0000000..e7aa2a4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/.robloxrc @@ -0,0 +1,7 @@ +{ + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/CatalogUrlPatterns.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/CatalogUrlPatterns.lua new file mode 100644 index 0000000..5e116bf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/CatalogUrlPatterns.lua @@ -0,0 +1,29 @@ +--!nocheck + +return function(UrlBuilder) + + local CatalogUrlPatterns = {} + + CatalogUrlPatterns.info = { + webbundle = UrlBuilder.fromString("www:bundles/{assetId}"), + webasset = UrlBuilder.fromString("www:catalog/{assetId}"), + webpage = function(params) + if params.assetType == "Bundle" then + return CatalogUrlPatterns.info.webbundle(params) + elseif params.assetType == "Asset" then + return CatalogUrlPatterns.info.webasset(params) + else + warn(string.format("%s - unknown assetType of %s", tostring(script.name), tostring(params.assetType))) + return nil + end + end, + appsflyer = function(params) + return UrlBuilder.fromString("appsflyer:Ebh5?pid=share&is_retargeting=true&af_dp={mobileUrl}&af_web_dp={webUrl}")({ + mobileUrl = UrlBuilder.fromString("mobilenav:item_details?itemId={assetId}&itemType={assetType}")(params), + webUrl = CatalogUrlPatterns.info.webpage(params), + }) + end, + } + + return CatalogUrlPatterns +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/CatalogUrlPatterns.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/CatalogUrlPatterns.spec.lua new file mode 100644 index 0000000..adb1d17 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/CatalogUrlPatterns.spec.lua @@ -0,0 +1,25 @@ +return function() + local UrlBuilder = require(script.Parent.Parent.UrlBuilder) + + describe("appsflyer link", function() + it("should generate proper bundle url", function() + local url = UrlBuilder.catalog.info.appsflyer({ + assetId = "1356984689", + assetType = "Bundle", + }) + expect(url).to.equal("https://ro.blox.com/Ebh5?pid=share&is_retargeting=true" .. + "&af_dp=robloxmobile%3A%2F%2Fnavigation%2Fitem_details%3FitemId%3D1356984689%26itemType%3DBundle" .. + "&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fbundles%2F1356984689") + end) + + it("should generate proper asset url", function() + local url = UrlBuilder.catalog.info.appsflyer({ + assetId = "1356984689", + assetType = "Asset", + }) + expect(url).to.equal("https://ro.blox.com/Ebh5?pid=share&is_retargeting=true" .. + "&af_dp=robloxmobile%3A%2F%2Fnavigation%2Fitem_details%3FitemId%3D1356984689%26itemType%3DAsset" .. + "&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fcatalog%2F1356984689") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/GameUrlPatterns.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/GameUrlPatterns.lua new file mode 100644 index 0000000..f0b46f8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/GameUrlPatterns.lua @@ -0,0 +1,59 @@ +--!nocheck + +return function(UrlBuilder) + + local GameUrlPatterns = {} + + --- from GameInfoList.lua + GameUrlPatterns.info = { + webpage = UrlBuilder.fromString("www:games/{placeId}"), + store = UrlBuilder.fromString("www:games/store-section/{universeId}"), + badges = UrlBuilder.fromString("www:games/badges-section/{universeId}"), + servers = UrlBuilder.fromString("www:games/servers-section/{universeId}"), + serversPreopenCreateVip = UrlBuilder.fromString("www:games/servers-section-preopen-create-vip/{universeId}"), + group = UrlBuilder.fromString("www:groups/{creatorId}"), + user = UrlBuilder.fromString("www:users/{creatorId}/profile"), + -- {creatorType=Group|User, creatorId} + creator = function(params) + if params.creatorType == "Group" then + return GameUrlPatterns.info.group(params) + elseif params.creatorType == "User" then + return GameUrlPatterns.info.user(params) + end + warn(string.format("%s - unknown creatorType of %s", tostring(script.name), tostring(params.creatorType))) + return nil + end, + appsflyer = function(params) + return UrlBuilder.fromString("appsflyer:Ebh5?pid=share&is_retargeting=true&af_dp={mobileUrl}&af_web_dp={webUrl}")({ + mobileUrl = UrlBuilder.fromString("mobilenav:game_details?gameId={universeId}")(params), + webUrl = GameUrlPatterns.info.webpage(params), + }) + end, + } + + --- from Http/Requests/* + GameUrlPatterns.details = UrlBuilder.fromString("games:games?{universeIds}") + GameUrlPatterns.playability = UrlBuilder.fromString("games:games/multiget-playability-status?{universeIds}") + GameUrlPatterns.media = UrlBuilder.fromString("games:games/{universeId}/media") + GameUrlPatterns.favorite = UrlBuilder.fromString("games:games/{universeId}/favorites") + GameUrlPatterns.social = UrlBuilder.fromString("games:games/{universeId}/social-links/list") + GameUrlPatterns.recommended = UrlBuilder.fromString("games:games/recommendations/game/{universeId}?{paginationKey|}&{maxRows|6}") + GameUrlPatterns.thumbnail = UrlBuilder.fromString("games:games/game-thumbnails?{height|150}&{width|150}&{imageTokens}") + GameUrlPatterns.vote = { + -- votes for all users + all = UrlBuilder.fromString("games:games/{universeId}/votes"), + -- current user vote status + get = UrlBuilder.fromString("games:games/{universeId}/votes/user"), + set = UrlBuilder.fromString("games:games/{universeId}/user-votes"), + } + GameUrlPatterns.follow = { + get = UrlBuilder.fromString("followings:users/{userId}/universes/{universeId}/status"), + set = UrlBuilder.fromString("followings:users/{userId}/universes/{universeId}"), + } + GameUrlPatterns.report = UrlBuilder.fromString("www:abusereport/asset?id={placeId}") + + GameUrlPatterns.place = UrlBuilder.fromString("games:games/multiget-place-details?{placeIds}") + + return GameUrlPatterns + +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/GameUrlPatterns.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/GameUrlPatterns.spec.lua new file mode 100644 index 0000000..9f69078 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/GameUrlPatterns.spec.lua @@ -0,0 +1,15 @@ +return function() + local UrlBuilder = require(script.Parent.Parent.UrlBuilder) + + describe("appsflyer link", function() + it("should generate proper url", function() + local url = UrlBuilder.game.info.appsflyer({ + universeId = "1356984689", + placeId = "123456789", + }) + expect(url).to.equal("https://ro.blox.com/Ebh5?pid=share&is_retargeting=true" .. + "&af_dp=robloxmobile%3A%2F%2Fnavigation%2Fgame_details%3FgameId%3D1356984689" .. + "&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fgames%2F123456789") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/StaticUrlPatterns.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/StaticUrlPatterns.lua new file mode 100644 index 0000000..bfd968c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/StaticUrlPatterns.lua @@ -0,0 +1,55 @@ +return function(UrlBuilder) + local function isQQ() + return string.find(UrlBuilder.fromString("corp:")(), "qq.com") + end + + return { + catalog = UrlBuilder.fromString("www:catalog"), + buildersClub = UrlBuilder.fromString("www:mobile-app-upgrades/native-ios/bc"), + trades = UrlBuilder.fromString("www:trades"), + profile = UrlBuilder.fromString("www:users/profile"), + friends = UrlBuilder.fromString("www:users/friends"), + groups = UrlBuilder.fromString("www:my/groups"), + inventory = UrlBuilder.fromString("www:users/inventory"), + messages = UrlBuilder.fromString("www:my/messages"), + feed = UrlBuilder.fromString("www:feeds/inapp"), + develop = UrlBuilder.fromString("www:develop/landing"), + blog = UrlBuilder.fromString("blog:"), + help = UrlBuilder.fromString(isQQ() and "corp:faq" or "www:help"), + email = { + getSetEmail = UrlBuilder.fromString("accountSettings:v1/email"), + sendVerificationEmail = UrlBuilder.fromString("accountSettings:v1/email/verify") + }, + about = { + us = UrlBuilder.fromString("corp:"), + careers = UrlBuilder.fromString(isQQ() and "corp:careers.html" or "corp:careers"), + parents = UrlBuilder.fromString("corp:parents"), + terms = function(params) + if isQQ() and params.useGameQQUrls then + return UrlBuilder.fromString("https://game.qq.com/contract.shtml")() + else + return UrlBuilder.fromString("www:info/terms")() + end + end, + privacy = function(params) + if isQQ() and params.useGameQQUrls then + return UrlBuilder.fromString("https://game.qq.com/privacy_guide.shtml")() + else + return UrlBuilder.fromString("www:info/privacy")() + end + end, + }, + settings = { + account = UrlBuilder.fromString("www:my/account#!/info"), + security = UrlBuilder.fromString("www:my/account#!/security"), + privacy = UrlBuilder.fromString("www:my/account#!/privacy"), + billing = UrlBuilder.fromString("www:my/account#!/billing"), + notifications = UrlBuilder.fromString("www:my/account#!/notifications"), + }, + tencent = { + childrenPrivacyGuide = UrlBuilder.fromString("https://game.qq.com/privacy_guide_children.shtml"), + luobuRiderTerms = UrlBuilder.fromString("https://roblox.qq.com/web201904/newsdetail.html?newsid=12429812"), + reputationInfo = UrlBuilder.fromString("https://gamecredit.qq.com/static/games/index.htm"), + }, + } +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/UserUrlPatterns.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/UserUrlPatterns.lua new file mode 100644 index 0000000..a841f16 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/UrlPatterns/UserUrlPatterns.lua @@ -0,0 +1,22 @@ + +return function(UrlBuilder) + return { + profile = UrlBuilder.fromString("www:users/{userId}/profile"), + group = UrlBuilder.fromString("www:groups/{groupId}/{groupName|}#!/about"), + friends = UrlBuilder.fromString("www:users/{userId}/friends"), + inventory = UrlBuilder.fromString("www:users/{userId}/inventory"), + search = UrlBuilder.fromString("www:search/users?{keyword}"), + + report = function(params) + -- Web is fixing a bug that requires actionName and redirectUrl for this page to work + -- once fixed, this pattern function can be replaced with + -- UrlBuilder.fromString("www:abusereport/embedded/chat?id={userId}&{conversationId}"), + return UrlBuilder.fromString("www:abusereport/embedded/chat?id={userId}&{actionName}&{conversationId}&{redirecturl}")({ + userId = params.userId, + conversationId = params.conversationId, + actionName = "chat", + redirecturl = UrlBuilder.fromString("www:home")(), + }) + end + } +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/encodeURIComponent.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/encodeURIComponent.lua new file mode 100644 index 0000000..ec2079e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/encodeURIComponent.lua @@ -0,0 +1,7 @@ +local function formatCharacter(character) + return string.format("%%%02X", character:byte(1,1)) +end + +return function(stringToEncode) + return stringToEncode:gsub("[^%w_%-%!%.%~%*%'%(%)]", formatCharacter) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/encodeURIComponent.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/encodeURIComponent.spec.lua new file mode 100644 index 0000000..b37bbce --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/encodeURIComponent.spec.lua @@ -0,0 +1,18 @@ +return function() + local encodeURIComponent = require(script.Parent.encodeURIComponent) + + it('should not filter alphanumerics', function() + local str = 'abcXYZ1230' + expect(encodeURIComponent(str)).to.equal(str) + end) + + it('should not filter allowed special characters', function() + local str = 'abcABC123-_.!~*\'()' + expect(encodeURIComponent(str)).to.equal(str) + end) + + it('should filter other non-alphanumeric characters', function() + local str = 'hello world&result=true' + expect(encodeURIComponent(str)).to.equal('hello%20world%26result%3Dtrue') + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/init.lua new file mode 100644 index 0000000..21abeb9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/roblox_url-builder/url-builder/init.lua @@ -0,0 +1,5 @@ +return { + encodeURIComponent = require(script.encodeURIComponent), + UrlBase = require(script.UrlBase), + UrlBuilder = require(script.UrlBuilder), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Cryo.lua new file mode 100644 index 0000000..dbd1e28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Cryo.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_cryo"]["cryo"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Freeze.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Freeze.lua new file mode 100644 index 0000000..bdd302b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Freeze.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["freeze"]["freeze"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Promise.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Promise.lua new file mode 100644 index 0000000..5dea2b4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Promise.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["lua-promise"]["lua-promise"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Rodux.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Rodux.lua new file mode 100644 index 0000000..96b67df --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/Rodux.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["roblox_rodux"]["rodux"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/lock.toml new file mode 100644 index 0000000..bda8b0c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/lock.toml @@ -0,0 +1,12 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "rodux-networking" +version = "1.0.0" +commit = "8902a6aaf52d5cad67e8e972f8fb6969d1b81157" +source = "git+https://github.com/roblox/rodux-networking#v1.0.2" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Freeze freeze ef89f9d0 git+https://github.com/roblox/freeze#master", + "Promise lua-promise 1bb842c3 git+https://github.com/roblox/lua-promise#master", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "tutils tutils 937da4f7 git+https://github.com/roblox/tutils#master", +] diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Action.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Action.lua new file mode 100644 index 0000000..2760513 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Action.lua @@ -0,0 +1,68 @@ +--[[ + A helper function to define a Rodux action creator with an associated name. + + Normally when creating a Rodux action, you can just create a function: + + return function(value) + return { + type = "MyAction", + value = value, + } + end + + And then when you check for it in your reducer, you either use a constant, + or type out the string name: + + if action.type == "MyAction" then + -- change some state + end + + Typos here are a remarkably common bug. We also have the issue that there's + no link between reducers and the actions that they respond to! + + `Action` (this helper) provides a utility that makes this a bit cleaner. + + Instead, define your Rodux action like this: + + return Action("MyAction", function(value) + return { + value = value, + } + end) + + We no longer need to add the `type` field manually. + + Additionally, the returned action creator now has a 'name' property that can + be checked by your reducer: + + local MyAction = require(Reducers.MyAction) + + ... + + if action.type == MyAction.name then + -- change some state! + end + + Now we have a clear link between our reducers and the actions they use, and + if we ever typo a name, we'll get a warning in LuaCheck as well as an error + at runtime! +]] + +return function(name, fn) + assert(type(name) == "string", "A name must be provided to create an Action") + assert(type(fn) == "function", "A function must be provided to create an Action") + + return setmetatable({ + name = name, + }, { + __call = function(self, ...) + local result = fn(...) + + assert(type(result) == "table", "An action must return a table") + + result.type = name + + return result + end + }) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Cryo.lua new file mode 100644 index 0000000..ddd769d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Cryo.lua @@ -0,0 +1,4 @@ +local root = script.Parent +local Packages = root.Parent + +return require(Packages.Cryo) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Freeze.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Freeze.lua new file mode 100644 index 0000000..e4c6423 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Freeze.lua @@ -0,0 +1,4 @@ +local root = script.Parent +local Packages = root.Parent + +return require(Packages.Freeze) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/GET.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/GET.lua new file mode 100644 index 0000000..1f76609 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/GET.lua @@ -0,0 +1,6 @@ +local root = script.Parent +local makeRequestApi = require(root.makeRequestApi) + +return function(options) + return makeRequestApi(options, "GET") +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/GET.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/GET.spec.lua new file mode 100644 index 0000000..179af61 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/GET.spec.lua @@ -0,0 +1,24 @@ +return function() + local root = script.Parent + local mockNetworkImpl = function() + return nil + end + + local GET = require(root.GET)({ + keyPath = "hello.world", + networkImpl = mockNetworkImpl, + }) + + describe("WHEN invoked", function() + local endpoint = GET(script, function(requestBuilder) + return requestBuilder("example.com") + end) + + it("SHOULD return an object have all expected fields", function() + expect(endpoint.API).to.be.ok() + expect(endpoint.getStatus).to.be.ok() + expect(endpoint.Succeeded).to.be.ok() + expect(endpoint.Failed).to.be.ok() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/EnumNetworkStatus.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/EnumNetworkStatus.lua new file mode 100644 index 0000000..d8db68e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/EnumNetworkStatus.lua @@ -0,0 +1,20 @@ +local EnumNetworkStatus = {} + +local EnumValues = { + NotStarted = "NotStarted", + Fetching = "Fetching", + Done = "Done", + Failed = "Failed", +} + +setmetatable(EnumNetworkStatus, + { + __newindex = function(t, key, index) + end, + __index = function(t, index) + assert(EnumValues[index] ~= nil, ("EnumNetworkStatus has no value: " .. tostring(index))) + return EnumValues[index] + end + }) + +return EnumNetworkStatus diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Freeze.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Freeze.lua new file mode 100644 index 0000000..98dc4b4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Freeze.lua @@ -0,0 +1,4 @@ +local root = script.Parent.Parent +local Packages = root.Parent + +return require(Packages.Freeze) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Promise.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Promise.lua new file mode 100644 index 0000000..a7444c1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Promise.lua @@ -0,0 +1,4 @@ +local root = script.Parent.Parent +local Packages = root.Parent + +return require(Packages.Promise) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Rodux.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Rodux.lua new file mode 100644 index 0000000..b448060 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/Rodux.lua @@ -0,0 +1,4 @@ +local root = script.Parent.Parent +local Packages = root.Parent + +return require(Packages.Rodux) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/buildActionName.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/buildActionName.lua new file mode 100644 index 0000000..6190b72 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/buildActionName.lua @@ -0,0 +1,3 @@ +return function(options) + return "networkStatus:" .. tostring(options.keyPath) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/buildActionName.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/buildActionName.spec.lua new file mode 100644 index 0000000..feb7a5a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/buildActionName.spec.lua @@ -0,0 +1,17 @@ +return function() + local buildActionName = require(script.Parent.buildActionName) + + it("SHOULD concat networkStatus: to keyPath", function() + local result = buildActionName({ + keyPath = "Hello", + }) + expect(result).to.equal("networkStatus:Hello") + end) + + it("SHOULD concat networkStatus: to keyPath with depth", function() + local result = buildActionName({ + keyPath = "Hello.World", + }) + expect(result).to.equal("networkStatus:Hello.World") + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getDeepValue.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getDeepValue.lua new file mode 100644 index 0000000..dd449b6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getDeepValue.lua @@ -0,0 +1,12 @@ +return function(tab, keyPath) + local currentNode = tab + for _, key in ipairs(keyPath:split(".")) do + if not currentNode[key] then + return nil + end + + currentNode = currentNode[key] + end + + return currentNode +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getDeepValue.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getDeepValue.spec.lua new file mode 100644 index 0000000..0d463a2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getDeepValue.spec.lua @@ -0,0 +1,54 @@ +return function() + local getDeepValue = require(script.Parent.getDeepValue) + + describe("GIVEN an empty array", function() + local tab = {} + it("SHOULD return nil", function() + expect(getDeepValue(tab, "")).to.equal(nil) + expect(getDeepValue(tab, "hello.world")).to.equal(nil) + end) + end) + + describe("GIVEN a dictionary with hello.world", function() + local tab = { + hello = { + world = 100, + }, + } + describe("GIVEN an empty string as the second argument", function() + it("SHOULD return nil", function() + expect(getDeepValue(tab, "")).to.equal(nil) + end) + end) + + describe("GIVEN `goodbye.world` as the second argument", function() + it("SHOULD return nil", function() + expect(getDeepValue(tab, "goodbye.world")).to.equal(nil) + end) + end) + + describe("GIVEN `hello.there` as the second argument", function() + it("SHOULD return nil", function() + expect(getDeepValue(tab, "hello.there")).to.equal(nil) + end) + end) + + describe("GIVEN `hello.there` as the second argument", function() + it("SHOULD return nil", function() + expect(getDeepValue(tab, "hello.there")).to.equal(nil) + end) + end) + + describe("GIVEN `hello` as the second argument", function() + it("SHOULD return the hello table", function() + expect(getDeepValue(tab, "hello")).to.equal(tab.hello) + end) + end) + + describe("GIVEN `hello.world` as the second argument", function() + it("SHOULD return the 100 (the value mapped to hello.world)", function() + expect(getDeepValue(tab, "hello.world")).to.equal(100) + end) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getStatus.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getStatus.lua new file mode 100644 index 0000000..2f8fd73 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getStatus.lua @@ -0,0 +1,20 @@ +local root = script.Parent +local getDeepValue = require(root.getDeepValue) +local EnumNetworkStatus = require(root.EnumNetworkStatus) + +return function(options) + local keyPath = options.keyPath + + return function(state, fetchStatusKey) + assert(typeof(state) == "table") + assert(typeof(fetchStatusKey) == "string") + assert(#fetchStatusKey > 0) + local reducerValue = getDeepValue(state, keyPath) + assert(reducerValue, string.format( + "Reducer not found for keyPath: %s. Did you forget to call `installReducer`?", + keyPath + )) + + return reducerValue:get(fetchStatusKey) or EnumNetworkStatus.NotStarted + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getStatus.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getStatus.spec.lua new file mode 100644 index 0000000..da1d223 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/getStatus.spec.lua @@ -0,0 +1,35 @@ +local Freeze = require(script.Parent.Freeze) + +return function() + local getStatus = require(script.Parent.getStatus)({ + keyPath = "testingKeyPathStatus", + }) + local EnumNetworkStatus = require(script.Parent.EnumNetworkStatus) + local TEST_KEY_1 = "item_key" + + it("should return NotStarted for missing key", function() + local state = { testingKeyPathStatus = Freeze.UnorderedMap.new({}) } + local status = getStatus(state, TEST_KEY_1) + + expect(status).to.equal(EnumNetworkStatus.NotStarted) + end) + + it("should return matching status for state in store", function() + local statusesToTest = { + EnumNetworkStatus.NotStarted, + EnumNetworkStatus.Fetching, + EnumNetworkStatus.Done, + EnumNetworkStatus.Failed + } + + for _, testStatus in ipairs(statusesToTest) do + local state = { + testingKeyPathStatus = Freeze.UnorderedMap.new({ + [TEST_KEY_1] = testStatus + }) + } + + expect(getStatus(state, TEST_KEY_1)).to.equal(testStatus) + end + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/init.lua new file mode 100644 index 0000000..f9937bc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/init.lua @@ -0,0 +1,11 @@ +return function(options) + return { + getStatus = require(script.getStatus)(options), + setStatus = require(script.setStatus)(options), + installReducer = require(script.installReducer)(options), + + Enum = { + Status = require(script.EnumNetworkStatus), + }, + } +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/installReducer.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/installReducer.lua new file mode 100644 index 0000000..7478980 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/installReducer.lua @@ -0,0 +1,23 @@ +local Rodux = require(script.Parent.Rodux) +local Freeze = require(script.Parent.Freeze) +local buildActionName = require(script.Parent.buildActionName) + +return function(options) + return function() + return Rodux.createReducer(Freeze.UnorderedMap.new({}), { + [buildActionName(options)] = function(state, action) + local updatedStatus = {} + if #action.ids == 0 then + updatedStatus[action.keymapper()] = action.status + else + for _, id in ipairs(action.ids) do + local mappedId = action.keymapper(id) + updatedStatus[mappedId] = action.status + end + end + + return state:batchSet(updatedStatus) + end, + }) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/installReducer.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/installReducer.spec.lua new file mode 100644 index 0000000..b3c6a1e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/installReducer.spec.lua @@ -0,0 +1,107 @@ +return function() + local options = { + keyPath = "networkStatus" + } + local installReducer = require(script.Parent.installReducer)(options) + local buildActionName = require(script.Parent.buildActionName) + local getStatus = require(script.Parent.getStatus)(options) + local reducer = installReducer() + + describe("GIVEN an action with no id", function() + local initialAction = { + type = buildActionName(options), + ids = {}, + keymapper = function() return "key" end, + status = "test", + } + + it("SHOULD return a new UnorderedMap with the key mapped properly", function() + local result = reducer(nil, initialAction) + + expect(result).to.be.ok() + expect(result:get("key")).to.equal("test") + end) + + describe("GIVEN another action that rewrites the previous key", function() + local overwriteAction = { + type = buildActionName(options), + ids = {}, + keymapper = function() return "key" end, + status = "next-best-thing", + } + + it("SHOULD update the key accordingly", function() + local result = reducer(reducer(nil, initialAction), overwriteAction) + + expect(result).to.be.ok() + expect(result:get("key")).to.equal("next-best-thing") + end) + end) + + it("SHOULD be retrievable with getStatus", function() + local state = { + networkStatus = reducer(nil, initialAction), + } + local result = getStatus(state, "key") + expect(result).to.equal("test") + end) + end) + + describe("GIVEN an action with one id", function() + local initialAction = { + type = buildActionName(options), + ids = { "123" }, + keymapper = function(id) return "key:" .. id end, + status = "test", + } + + it("SHOULD return a new UnorderedMap with the key mapped properly", function() + local result = reducer(nil, initialAction) + + expect(result).to.be.ok() + expect(result:get("key:123")).to.equal("test") + end) + + describe("GIVEN another action that rewrites the previous key", function() + local overwriteAction = { + type = buildActionName(options), + ids = { "123" }, + keymapper = function(id) return "key:" .. id end, + status = "next-best-thing", + } + + it("SHOULD update the key accordingly", function() + local result = reducer(reducer(nil, initialAction), overwriteAction) + + expect(result).to.be.ok() + expect(result:get("key:123")).to.equal("next-best-thing") + end) + end) + + it("SHOULD be retrievable with getStatus", function() + local state = { + networkStatus = reducer(nil, initialAction), + } + local result = getStatus(state, "key:123") + expect(result).to.equal("test") + end) + end) + + describe("GIVEN an action with multiple ids", function() + local batchAction = { + type = buildActionName(options), + ids = { "123", "456", "789" }, + keymapper = function(id) return "key:" .. id end, + status = "the-same-status", + } + + it("SHOULD update all keys to the same status", function() + local result = reducer(nil, batchAction) + + expect(result).to.be.ok() + expect(result:get("key:123")).to.equal("the-same-status") + expect(result:get("key:456")).to.equal("the-same-status") + expect(result:get("key:789")).to.equal("the-same-status") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/setStatus.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/setStatus.lua new file mode 100644 index 0000000..979a7da --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/setStatus.lua @@ -0,0 +1,41 @@ +local EnumNetworkStatus = require(script.Parent.EnumNetworkStatus) +local buildActionName = require(script.Parent.buildActionName) + +return function(options) + local getStatus = require(script.Parent.getStatus)(options) + + local function actionCreator(ids, keymapper, status) + return { + ids = ids, + keymapper = keymapper, + status = status, + type = buildActionName(options), + } + end + + local function filter(state, ids, keymapper) + local filteredIds = {} + for _, id in ipairs(ids) do + local status = getStatus(state, keymapper(id)) + if status ~= EnumNetworkStatus.Fetching then + table.insert(filteredIds, id) + end + end + return filteredIds + end + + return function(store, ids, keymapper, promiseFunction) + local filteredIds = filter(store:getState(), ids, keymapper) + + store:dispatch(actionCreator(filteredIds, keymapper, EnumNetworkStatus.Fetching)) + + return promiseFunction(store, filteredIds):andThen(function(result) + store:dispatch(actionCreator(filteredIds, keymapper, EnumNetworkStatus.Done)) + return result + end, + function(errorString) + store:dispatch(actionCreator(filteredIds, keymapper, EnumNetworkStatus.Failed)) + error(errorString) + end) + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/setStatus.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/setStatus.spec.lua new file mode 100644 index 0000000..727e0c2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/NetworkStatus/setStatus.spec.lua @@ -0,0 +1,85 @@ +return function() + local Freeze = require(script.Parent.Freeze) + local EnumNetworkStatus = require(script.Parent.EnumNetworkStatus) + local Promise = require(script.Parent.Promise) + local setStatus = require(script.Parent.setStatus)({ + keyPath = "testingKeyPath", + }) + local mockStore = require(script.Parent.Parent.mockStore) + + describe("GIVEN a store", function() + local actionHistory = {} + local roduxStore = mockStore.config({ + dispatch = function(self, action) + if type(action) == "function" then + action(self) + else + table.insert(actionHistory, action) + end + end, + }) + + describe("GIVEN multiple ids with a valid keymapper and promise function", function() + local ids = { "123", "456", "789" } + local keymapper = function(id) + return "key" .. tostring(id) + end + + describe("WHEN there are no ongoing requests", function() + it("SHOULD not filter any ids", function() + roduxStore:setState({ + testingKeyPath = Freeze.UnorderedMap.new({}), + }) + + local promiseFunction = function(store, filteredIds) + expect(store).to.be.ok() + expect(#filteredIds).to.equal(#ids) + return Promise.resolve(filteredIds) + end + + setStatus(roduxStore, ids, keymapper, promiseFunction) + end) + + it("SHOULD only dispatch relevant actions", function() + expect(#actionHistory).to.equal(2) + + local secondToLastAction = actionHistory[#actionHistory - 1] + expect(secondToLastAction).to.be.ok() + expect(type(secondToLastAction)).to.equal("table") + expect(secondToLastAction.type).to.equal("networkStatus:testingKeyPath") + expect(secondToLastAction.status).to.equal(EnumNetworkStatus.Fetching) + + local lastAction = actionHistory[#actionHistory] + expect(lastAction).to.be.ok() + expect(type(lastAction)).to.equal("table") + expect(lastAction.type).to.equal("networkStatus:testingKeyPath") + expect(lastAction.status).to.equal(EnumNetworkStatus.Done) + end) + end) + + describe("WHEN there is already an ongoing request", function() + it("SHOULD filter the ids that are currently ongoing", function() + actionHistory = {} + roduxStore:setState({ + testingKeyPath = Freeze.UnorderedMap.new({ + [keymapper("123")] = EnumNetworkStatus.Fetching, + [keymapper("456")] = EnumNetworkStatus.Fetching, + [keymapper("789")] = EnumNetworkStatus.Fetching, + }), + }) + local promiseFunction = function(store, filteredIds) + expect(store).to.be.ok() + expect(#filteredIds).to.equal(0) + return Promise.resolve(filteredIds) + end + + setStatus(roduxStore, ids, keymapper, promiseFunction) + end) + + it("SHOULD still fire any actions if there are no ids", function() + expect(#actionHistory).to.equal(2) + end) + end) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/POST.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/POST.lua new file mode 100644 index 0000000..d5d74e5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/POST.lua @@ -0,0 +1,6 @@ +local root = script.Parent +local makeRequestApi = require(root.makeRequestApi) + +return function(options) + return makeRequestApi(options, "POST") +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/POST.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/POST.spec.lua new file mode 100644 index 0000000..a09d6d2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/POST.spec.lua @@ -0,0 +1,24 @@ +return function() + local root = script.Parent + local mockNetworkImpl = function() + return nil + end + + local POST = require(root.POST)({ + keyPath = "hello.world", + networkImpl = mockNetworkImpl, + }) + + describe("WHEN invoked", function() + local endpoint = POST(script, function(requestBuilder) + return requestBuilder("example.com") + end) + + it("SHOULD return an object have all expected fields", function() + expect(endpoint.API).to.be.ok() + expect(endpoint.getStatus).to.be.ok() + expect(endpoint.Succeeded).to.be.ok() + expect(endpoint.Failed).to.be.ok() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Promise.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Promise.lua new file mode 100644 index 0000000..cbb818f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/Promise.lua @@ -0,0 +1,4 @@ +local root = script.Parent +local Packages = root.Parent + +return require(Packages.Promise) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/Cryo.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/Cryo.lua new file mode 100644 index 0000000..834f875 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/Cryo.lua @@ -0,0 +1,4 @@ +local root = script.Parent.Parent +local Packages = root.Parent + +return require(Packages.Cryo) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/RequestBuilder.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/RequestBuilder.lua new file mode 100644 index 0000000..ac0d91f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/RequestBuilder.lua @@ -0,0 +1,139 @@ +local root = script.Parent +local Cryo = require(root.Cryo) + +local UriIds = {} +UriIds.__index = UriIds + +function UriIds:new(ids, delimiter) + return setmetatable({ + ids = UriIds.makeArray(ids), + delimiter = delimiter + }, self) +end + +function UriIds:setIds(ids) + self.ids = UriIds.makeArray(ids) +end + +function UriIds.makeArray(ids) + if type(ids) == "table" then + return ids + end + return {ids} +end + +function UriIds:__tostring() + return table.concat(self.ids, self.delimiter) +end + +local RequestBuilder = {} +RequestBuilder.__index = RequestBuilder + +function RequestBuilder:new(baseUrl) + return setmetatable({ + baseUrl = baseUrl, + keyMapper = nil, + args = {}, + pathElements = {}, + configurableIds = nil, + namedIds = {}, + idsDelimiter = ";", + options = {}, + }, self) +end + +function RequestBuilder:path(path) + table.insert(self.pathElements, path) + return self +end + +function RequestBuilder:id(ids, key) + if not key and #self.pathElements < 1 then + warn("Cannot name id or ids because there is no leading path segment and no name is provided") + end + local name = key or self.pathElements[#self.pathElements] + self.namedIds[name] = ids + + self.configurableIds = UriIds:new(ids, self.idsDelimiter) + table.insert(self.pathElements, self.configurableIds) + return self +end + +function RequestBuilder:queryArgWithIds(argName, ids) + self.namedIds[argName] = ids + self.configurableIds = UriIds:new(ids, self.idsDelimiter) + self:queryArgs({ + [argName] = self.configurableIds + }) + return self +end + +function RequestBuilder:queryArgs(args) + self.args = Cryo.Dictionary.join(self.args, args) + return self +end + +function RequestBuilder:body(dictionary) + self.options.postBody = dictionary + return self +end + +function RequestBuilder:makeKeyMapper() + return function(someId) + return self:makeUrl(someId) + end +end + +function RequestBuilder:makeUri(ids) + local fullPath = "" + for _, element in ipairs(self.pathElements) do + fullPath = fullPath .. "/" .. tostring(element) + end + return fullPath +end + +function RequestBuilder:makeQueryArgs(ids) + self:_plugInConfigurableIds(ids) + local argsString = "" + for k,v in pairs(self.args) do + local arg = tostring(k) .. "=" .. tostring(v) + if argsString:len() > 1 then + argsString = argsString .. "&" .. arg + else + argsString = arg + end + end + if argsString:len() > 1 then + return "?" .. argsString + end + return "" +end + +function RequestBuilder:makeUrl(ids) + self:_plugInConfigurableIds(ids) + local fullUrl = self.baseUrl .. self:makeUri(ids) .. self:makeQueryArgs(ids) + return fullUrl +end + +function RequestBuilder:makeOptions() + return self.options +end + +function RequestBuilder:_plugInConfigurableIds(ids) + if ids ~= nil and self.configurableIds then + self.configurableIds:setIds(ids) + end +end + +function RequestBuilder:getIds() + if self.configurableIds and self.configurableIds.ids then + return self.configurableIds.ids + end + return {} +end + +function RequestBuilder:getNamedIds() + return self.namedIds +end + +return RequestBuilder diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/RequestBuilder.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/RequestBuilder.spec.lua new file mode 100644 index 0000000..0d87d15 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/RequestBuilder.spec.lua @@ -0,0 +1,177 @@ +return function() + local root = script.Parent + local RequestBuilder = require(root.RequestBuilder) + local tutils = require(root.tutils) + + local baseUrl = "https://example.com" + describe("RequestBuilder basics", function() + it("builder functions should return self", function() + local builder = RequestBuilder:new() + + expect(builder).to.equal(builder:path("test")) + expect(builder).to.equal(builder:id("test")) + expect(builder).to.equal(builder:queryArgs({ "test" })) + expect(builder).to.equal(builder:body({ "test" })) + end) + + it("should be constructible from base URL", function() + + local builder = RequestBuilder:new(baseUrl) + expect(builder).to.be.ok() + expect(builder:makeUrl()).to.equal(baseUrl) + end) + + it("should be constructible from successive path calls", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("some"):path("element") + expect(builder:makeUrl()).to.equal(baseUrl .. "/some/element") + expect(tutils.shallowEqual(builder:getIds(), {})).to.equal(true) + end) + + it("should allow building query args", function() + local builder = RequestBuilder:new(baseUrl) + builder:queryArgs({ + arg = "value" + }) + expect(builder:makeUrl()).to.equal(baseUrl .. "?arg=value") + expect(tutils.shallowEqual(builder:getIds(), {})).to.equal(true) + end) + + it("should allow building multiple query args", function() + local builder = RequestBuilder:new(baseUrl) + builder:queryArgs({ + arg = "value" + }) + builder:queryArgs({ + arg2 = "value2" + }) + expect(builder:makeUrl()).to.equal(baseUrl .. "?arg2=value2&arg=value") + expect(tutils.shallowEqual(builder:getIds(), {})).to.equal(true) + end) + end) + + describe("RequestBuilder path ids", function() + it("should be constructible from path and single id", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("some/element"):id(123) + + local expectedUrl = baseUrl .. "/some/element/123" + expect(builder:makeUrl()).to.equal(expectedUrl) + + expect(builder:makeKeyMapper()).to.be.ok() + expect(builder:makeKeyMapper()()).to.equal(expectedUrl) + + expect(tutils.shallowEqual(builder:getIds(), {123})).to.equal(true) + end) + + it("should be constructible from path and ids array", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("some/element"):id({123, 321}) + + local staticUrlPart = baseUrl .. "/some/element/" + expect(builder:makeUrl()).to.equal(staticUrlPart .. "123;321") + + expect(builder:makeKeyMapper()(123)).to.equal(staticUrlPart .. "123") + expect(builder:makeKeyMapper()(321)).to.equal(staticUrlPart .. "321") + end) + + it("should allow swapping ids in makeUrl call", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("some/element"):id({}) + expect(builder:makeUrl({567, 789})).to.equal(baseUrl .. "/some/element/567;789") + end) + + it("should allow swapping ids in makeUrl call, but only for last ids group", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("some/element"):id(123):path("other"):id({}) + + local staticUrlPart = baseUrl .. "/some/element/123/other" + expect(builder:makeUrl(567)).to.equal(staticUrlPart .. "/567") + expect(builder:makeUrl({567})).to.equal(staticUrlPart .. "/567") + expect(builder:makeUrl({567, 789})).to.equal(staticUrlPart .. "/567;789") + + expect(builder:makeKeyMapper()(567)).to.equal(staticUrlPart .. "/567") + expect(builder:makeKeyMapper()(789)).to.equal(staticUrlPart .. "/789") + end) + + it("should map previous path segment to id", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("pathexample"):id(444) + local namedIds = builder:getNamedIds() + expect(namedIds["pathexample"]).to.be.ok() + expect(namedIds["pathexample"]).to.equal(444) + end) + + it("should map previous path segment to id with multiple path segments and ids", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("firstpathexample"):id(444):path("anotherpathexample"):path("yetanotherpathexample"):id(555) + local namedIds = builder:getNamedIds() + expect(namedIds["firstpathexample"]).to.be.ok() + expect(namedIds["anotherpathexample"]).to.never.be.ok() + expect(namedIds["yetanotherpathexample"]).to.be.ok() + + expect(namedIds["firstpathexample"]).to.equal(444) + expect(namedIds["yetanotherpathexample"]).to.equal(555) + end) + + it("should map previous path segment to id with multiple path segments and ids, with multiple ids given to function", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("firstpathexample"):id({ 333, 444 }):path("anotherpathexample"):path("yetanotherpathexample"):id(555) + local namedIds = builder:getNamedIds() + expect(namedIds["firstpathexample"]).to.be.ok() + expect(namedIds["anotherpathexample"]).to.never.be.ok() + expect(namedIds["yetanotherpathexample"]).to.be.ok() + + expect(tutils.deepEqual(namedIds["firstpathexample"], { 333, 444 })).to.equal(true) + expect(namedIds["yetanotherpathexample"]).to.equal(555) + end) + + it("should allow one to override the default key", function() + local builder = RequestBuilder:new(baseUrl) + local KEY1 = "KEY1" + builder:path("firstpathexample"):id({ 333, 444 }, KEY1):path("anotherpathexample"):path("yetanotherpathexample"):id(555) + local namedIds = builder:getNamedIds() + expect(namedIds["firstpathexample"]).to.never.be.ok() + expect(namedIds["anotherpathexample"]).to.never.be.ok() + expect(namedIds["yetanotherpathexample"]).to.be.ok() + + expect(tutils.deepEqual(namedIds[KEY1], { 333, 444 })).to.equal(true) + expect(namedIds["yetanotherpathexample"]).to.equal(555) + end) + end) + + describe("RequestBuilder query args and ids", function() + it("should allow swapping query argument ids in makeUrl call", function() + local builder = RequestBuilder:new(baseUrl) + builder:queryArgWithIds("arg", {123}) + expect(builder:makeUrl()).to.equal(baseUrl .. "?arg=123") + end) + + it("should allow swapping multiple query argument ids in makeUrl call", function() + local builder = RequestBuilder:new(baseUrl) + builder:queryArgWithIds("arg", {}) + expect(builder:makeUrl(123)).to.equal(baseUrl .. "?arg=123") + expect(builder:makeUrl({345, 456})).to.equal(baseUrl .. "?arg=345;456") + + expect(builder:makeKeyMapper()(567)).to.equal(baseUrl .. "?arg=567") + expect(builder:makeKeyMapper()({567, 321})).to.equal(baseUrl .. "?arg=567;321") + end) + + it("should allow swapping query argument ids in makeUrl call and not affect path ids", function() + local builder = RequestBuilder:new(baseUrl) + builder:path("some/element"):id(999):queryArgWithIds("arg", {}) + expect(builder:makeUrl(123)).to.equal(baseUrl .. "/some/element/999?arg=123") + expect(builder:makeUrl({345, 456})).to.equal(baseUrl .. "/some/element/999?arg=345;456") + end) + end) + + describe("RequestBuilder body", function() + it("should replace postBody", function() + local myBody = { hi = "there" } + local builder = RequestBuilder:new(baseUrl):body(myBody) + + expect(builder:makeOptions()).to.be.ok() + expect(builder:makeOptions().postBody.hi).to.equal("there") + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/init.lua new file mode 100644 index 0000000..7a3f212 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/init.lua @@ -0,0 +1 @@ +return require(script.RequestBuilder) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/tutils.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/tutils.lua new file mode 100644 index 0000000..8203b43 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RequestBuilder/tutils.lua @@ -0,0 +1,4 @@ +local root = script.Parent.Parent +local Packages = root.Parent + +return require(Packages.tutils) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RoduxNetworking.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RoduxNetworking.lua new file mode 100644 index 0000000..b4cf27d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RoduxNetworking.lua @@ -0,0 +1,37 @@ +local GET = require(script.Parent.GET) +local POST = require(script.Parent.POST) + +local RoduxNetworking = {} +RoduxNetworking.__index = RoduxNetworking + +function RoduxNetworking.new(options) + assert(options, "Expected options to be passed into RoduxNetworking") + assert(options.keyPath, "Expected options.keyPath to be passed into RoduxNetworking") + assert(options.networkImpl, "Expected options.networkImpl to be passed into RoduxNetworking") + + local self = { + options = options, + } + + return setmetatable(self, RoduxNetworking) +end + +function RoduxNetworking:GET(moduleScript, constructBuilderFunction) + assert(moduleScript, "RoduxNetworking:GET expects moduleScript argument") + assert(moduleScript, "RoduxNetworking:GET expects constructBuilderFunction argument") + return GET(self.options)(moduleScript, constructBuilderFunction) +end + +function RoduxNetworking:POST(...) + return POST(self.options)(...) +end + +function RoduxNetworking:getNetworkImpl() + return self.options.networkImpl +end + +function RoduxNetworking:setNetworkImpl(networkImpl) + self.options.networkImpl = networkImpl +end + +return RoduxNetworking \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RoduxNetworking.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RoduxNetworking.spec.lua new file mode 100644 index 0000000..8e98b28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/RoduxNetworking.spec.lua @@ -0,0 +1,50 @@ +return function() + local RoduxNetworking = require(script.Parent.RoduxNetworking) + local Promise = require(script.Parent.Promise) + local noOpt = function() + return Promise.resolve() + end + local moduleScript = { + Name = "moduleScript", + } + local builderConstructorFunction = function() + end + + describe("GIVEN an options configuration", function() + local options = { + keyPath = "", + networkImpl = noOpt + } + it("SHOULD return a unique object when .new is called", function() + local instance1 = RoduxNetworking.new(options) + local instance2 = RoduxNetworking.new(options) + + expect(instance1).to.be.ok() + expect(instance2).to.be.ok() + expect(instance1).to.never.equal(instance2) + end) + + describe("API", function() + local instance = RoduxNetworking.new(options) + it("SHOULD return an object when GET is called", function() + local result = instance:GET(moduleScript, builderConstructorFunction) + expect(result).to.be.ok() + end) + + it("SHOULD return an object when POST is called", function() + local result = instance:POST(moduleScript, builderConstructorFunction) + expect(result).to.be.ok() + end) + + describe("WHEN setNetworkImpl is called", function() + local newNetworkImpl = function() + return Promise.reject() + end + instance:setNetworkImpl(newNetworkImpl) + it("SHOULD return the given value when getNetworkImpl is called", function() + expect(instance:getNetworkImpl()).to.equal(newNetworkImpl) + end) + end) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/UrlBuilder.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/UrlBuilder.lua new file mode 100644 index 0000000..a732b95 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/UrlBuilder.lua @@ -0,0 +1,4 @@ +local root = script.Parent +local Packages = root.Parent + +return require(Packages.UrlBuilder) diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/init.lua new file mode 100644 index 0000000..70d797b --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/init.lua @@ -0,0 +1,29 @@ +local RoduxNetworking = require(script.RoduxNetworking) +local NetworkStatus = require(script.NetworkStatus) + +return { + config = function(options) + local roduxNetworkingInstance = RoduxNetworking.new(options) + local networkStatusInstance = NetworkStatus(options) + + return { + GET = function(...) + return roduxNetworkingInstance:GET(...) + end, + POST = function(...) + return roduxNetworkingInstance:POST(...) + end, + getNetworkImpl = function() + return roduxNetworkingInstance:getNetworkImpl() + end, + setNetworkImpl = function(...) + roduxNetworkingInstance:setNetworkImpl(...) + end, + + installReducer = networkStatusInstance.installReducer, + Enum = { + NetworkStatus = networkStatusInstance.Enum.Status, + }, + } + end, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/init.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/init.spec.lua new file mode 100644 index 0000000..7618438 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/init.spec.lua @@ -0,0 +1,21 @@ +return function() + local init = require(script.Parent) + local noOpt = function() + end + + describe("GIVEN an options configuration", function() + local instance = init.config({ + keyPath = "hello.world", + networkImpl = noOpt, + }) + + it("SHOULD have all expected fields", function() + expect(instance.GET).to.be.ok() + expect(instance.POST).to.be.ok() + expect(instance.Enum).to.be.ok() + expect(instance.installReducer).to.be.ok() + expect(instance.getNetworkImpl).to.be.ok() + expect(instance.setNetworkImpl).to.be.ok() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeActionCreator.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeActionCreator.lua new file mode 100644 index 0000000..8121ecc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeActionCreator.lua @@ -0,0 +1,21 @@ +local root = script.Parent +local Action = require(root.Action) + +return function(networkRequestScript) + return { + Succeeded = Action(networkRequestScript.Name .. "_Succeeded", function(ids, responseBody, namedIds) + return { + ids = ids, + responseBody = responseBody, + namedIds = namedIds, + } + end), + Failed = Action(networkRequestScript.Name .. "_Failed", function(ids, error, namedIds) + return { + ids = ids, + error = error, + namedIds = namedIds, + } + end), + } +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeActionCreator.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeActionCreator.spec.lua new file mode 100644 index 0000000..9afe984 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeActionCreator.spec.lua @@ -0,0 +1,13 @@ +return function() + local makeActionCreator = require(script.Parent.makeActionCreator) + + describe("GIVEN a script", function() + local myScript = Instance.new("ModuleScript") + local result = makeActionCreator(myScript) + + it("SHOULD return an object with a success field and failed field", function() + expect(result.Succeeded).to.be.ok() + expect(result.Failed).to.be.ok() + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApi.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApi.lua new file mode 100644 index 0000000..9ecf0a4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApi.lua @@ -0,0 +1,51 @@ +local root = script.Parent +local makeActionCreator = require(root.makeActionCreator) +local RequestBuilder = require(root.RequestBuilder) +local NetworkStatus = require(root.NetworkStatus) + +return function(options, methodType) + local keyPath = options.keyPath + + local myNetworkStatus = NetworkStatus({ + keyPath = keyPath, + }) + + return function(moduleScript, constructBuilderFunction) + local self = makeActionCreator(moduleScript) + self.API = function(...) + local userRequestBuilder = constructBuilderFunction(function(...) + return RequestBuilder:new(...) + end, ...) + + return function(store) + return myNetworkStatus.setStatus(store, userRequestBuilder:getIds(), userRequestBuilder:makeKeyMapper(), function(store, filteredIds) + local networkImpl = options.networkImpl + return networkImpl(userRequestBuilder:makeUrl(filteredIds), methodType, userRequestBuilder:makeOptions()):andThen( + function(payload) + store:dispatch(self.Succeeded(filteredIds, payload.responseBody, userRequestBuilder:getNamedIds())) + return payload + end, + function(errorString) + store:dispatch(self.Failed(filteredIds, error, userRequestBuilder:getNamedIds())) + -- Throw again so we can catch it outside of library + error(errorString) + end + ) + end) + end + end + + self.getStatus = (methodType == "GET") and function(state, key) + local userRequestBuilder = constructBuilderFunction(function(...) + return RequestBuilder:new(...) + end, key) + + local keymapper = userRequestBuilder:makeKeyMapper() + local mappedKey = keymapper(key) + + return myNetworkStatus.getStatus(state, mappedKey) + end + + return self + end +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApi.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApi.spec.lua new file mode 100644 index 0000000..9efc56f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApi.spec.lua @@ -0,0 +1,121 @@ +return function() + local root = script.Parent + local Freeze = require(root.Freeze) + local EnumNetworkStatus = require(root.NetworkStatus.EnumNetworkStatus) + local Promise = require(root.Promise) + local mockNetworkImpl = function(url, method, options) + return Promise.resolve({ + responseBody = "benj", + }) + end + local mockStore = require(root.mockStore) + + local GET = require(root.makeRequestApi)({ + keyPath = "hello.world", + networkImpl = mockNetworkImpl, + }, "GET") + + describe("GIVEN a store", function() + local actionHistory = {} + local roduxStore = mockStore.config({ + dispatch = function(self, action) + if type(action) == "function" then + action(self) + else + table.insert(actionHistory, action) + end + end, + + state = { + hello = { + world = Freeze.UnorderedMap.new({}), + } + } + }) + + describe("GIVEN a module script and builderFunction w/out parameters", function() + local hasBuilderFunctionRun = false + local receivedChannelArgument = nil + local mockGETChannels = GET(script, function(requestBuilder, channel) + hasBuilderFunctionRun = true + receivedChannelArgument = channel + + return requestBuilder("example.com"):path("v1"):path("channels"):id(channel):path("messages") + end) + + describe("GIVEN no parameters for a url", function() + it("SHOULD return a valid thunk", function() + local thunk = mockGETChannels.API("weather-channel") + expect(type(thunk)).to.equal("function") + end) + + describe("WHEN thunk is dispatched", function() + + it("SHOULD invoke builder function given to GET constructor", function() + roduxStore:dispatch(mockGETChannels.API("announcements")) + expect(hasBuilderFunctionRun).to.equal(true) + expect(receivedChannelArgument).to.equal("announcements") + end) + + it("SHOULD dispatch network status actions and payload action", function() + actionHistory = {} + roduxStore:dispatch(mockGETChannels.API("announcements")) + local action1 = actionHistory[1] + expect(action1).to.be.ok() + expect(action1.type).to.equal("networkStatus:hello.world") + expect(action1.status).to.equal(EnumNetworkStatus.Fetching) + end) + end) + end) + + describe("Action Creators", function() + it("SHOULD have a Succeeded action creator", function() + expect(mockGETChannels.Succeeded).to.be.ok() + local action = mockGETChannels.Succeeded({}, {}, {}) + expect(action).to.be.ok() + expect(action.ids).to.be.ok() + expect(action.responseBody).to.be.ok() + expect(action.namedIds).to.be.ok() + end) + + it("SHOULD have a Failed action creator", function() + expect(mockGETChannels.Failed).to.be.ok() + local action = mockGETChannels.Failed({}, {}, {}) + expect(action).to.be.ok() + expect(action.ids).to.be.ok() + expect(action.error).to.be.ok() + expect(action.namedIds).to.be.ok() + end) + end) + + describe("getStatus", function() + it("SHOULD return Done for successful network responses", function() + local store = mockStore.config({ + state = { + hello = { + world = Freeze.UnorderedMap.new({ + -- key mapper would usually generate this + ["example.com/v1/channels/faq/messages"] = "testingStatus", + }), + } + } + }) + + local status = mockGETChannels.getStatus(store:getState(), "faq") + expect(status).to.equal("testingStatus") + end) + + it("SHOULD throw for non-GET request types", function() + local POST = require(root.makeRequestApi)({ + keyPath = "hello.world", + networkImpl = mockNetworkImpl, + }, "POST") + + expect(function() + POST.getStatus({}, "testing") + end).to.throw() + end) + end) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApiThrows.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApiThrows.spec.lua new file mode 100644 index 0000000..a9eba5e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/makeRequestApiThrows.spec.lua @@ -0,0 +1,84 @@ +return function() + local root = script.Parent + local Freeze = require(root.Freeze) + local EnumNetworkStatus = require(root.NetworkStatus.EnumNetworkStatus) + local Promise = require(root.Promise) + local mockNetworkImpl = function(url, method, options) + return Promise.reject("networkError") + end + local mockStore = require(root.mockStore) + + local GET = require(root.makeRequestApi)({ + methodType = "GET", + keyPath = "hello.world", + networkImpl = mockNetworkImpl, + }) + + local function noOpt() + end + + describe("GIVEN a store", function() + local actionHistory = {} + local roduxStore = mockStore.config({ + dispatch = function(self, action) + if type(action) == "function" then + return action(self) + else + table.insert(actionHistory, action) + end + end, + + state = { + hello = { + world = Freeze.UnorderedMap.new({}), + } + } + }) + + describe("GIVEN a module script and builderFunction w/out parameters", function() + local hasBuilderFunctionRun = false + local receivedChannelArgument = nil + local mockGETChannels = GET(script, function(requestBuilder, channel) + hasBuilderFunctionRun = true + receivedChannelArgument = channel + + return requestBuilder("example.com"):path("v1"):path("channels"):id(channel):path("messages") + end) + + describe("WHEN thunk is dispatched", function() + it("SHOULD invoke builder function given to GET constructor", function() + roduxStore:dispatch(mockGETChannels.API("announcements")):catch(noOpt) + expect(hasBuilderFunctionRun).to.equal(true) + expect(receivedChannelArgument).to.equal("announcements") + end) + + it("SHOULD dispatch network status actions and payload action", function() + actionHistory = {} + roduxStore:dispatch(mockGETChannels.API("announcements")):catch(noOpt) + local action1 = actionHistory[1] + expect(action1).to.be.ok() + expect(action1.type).to.equal("networkStatus:hello.world") + expect(action1.status).to.equal(EnumNetworkStatus.Fetching) + end) + + it("SHOULD dispatch a failed network status action", function() + actionHistory = {} + roduxStore:dispatch(mockGETChannels.API("announcements")):catch(noOpt) + local action2 = actionHistory[2] + expect(action2).to.be.ok() + local isFound = string.find(action2.type, "Failed") + expect(isFound).to.be.ok() + end) + + it("SHOULD allow the promise to be caught", function() + local itWasCaught = false + roduxStore:dispatch(mockGETChannels.API("announcements")):catch(function() + itWasCaught = true + end) + + assert(itWasCaught, "Promise was not able to be caught when network fails") + end) + end) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/mockStore.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/mockStore.lua new file mode 100644 index 0000000..d67a0a8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/rodux-networking/mockStore.lua @@ -0,0 +1,23 @@ +return { + config = function(options) + options = options or {} + + local state = options.state or {} + return { + dispatch = options.dispatch or function(self, thunk) + if type(thunk) == "function" then + thunk(self) + end + end, + + getState = function() + return state + end, + + setState = function(_, newState) + state = newState + end, + } + + end, +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/tutils.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/tutils.lua new file mode 100644 index 0000000..cb6c720 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/rodux-networking/tutils.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent.Parent + +local package = PackageIndex["tutils"]["tutils"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/lock.toml b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/lock.toml new file mode 100644 index 0000000..7961fd2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/lock.toml @@ -0,0 +1,5 @@ +# Generated by Rotriever. Format subject to change in future releases. +name = "tutils" +version = "0.1.0" +commit = "937da4f7f354ebe6cf62348a80cc82fda91be2f0" +source = "git+https://github.com/roblox/tutils#master" diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/checkListConsistency.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/checkListConsistency.lua new file mode 100644 index 0000000..0f71828 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/checkListConsistency.lua @@ -0,0 +1,37 @@ +--[[ + Returns false if the given table has any of the following: + - a key that is neither a number or a string + - a mix of number and string keys + - number keys which are not exactly 1..#t +]] +return function(t) + local containsNumberKey = false + local containsStringKey = false + local numberConsistency = true + + local index = 1 + for x, _ in pairs(t) do + if type(x) == 'string' then + containsStringKey = true + elseif type(x) == 'number' then + if index ~= x then + numberConsistency = false + end + containsNumberKey = true + else + return false + end + + if containsStringKey and containsNumberKey then + return false + end + + index = index + 1 + end + + if containsNumberKey then + return numberConsistency + end + + return true +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/checkListConsistency.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/checkListConsistency.spec.lua new file mode 100644 index 0000000..a31f753 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/checkListConsistency.spec.lua @@ -0,0 +1,50 @@ +return function() + local checkListConsistency = require(script.Parent.checkListConsistency) + + describe("WHEN given a valid table", function() + it("SHOULD return true for lists", function() + expect(checkListConsistency({ 1, 2, 3 })).to.equal(true) + end) + + it("SHOULD return false for lists with holes", function() + local list = { + [1] = true, + [2] = nil, + [3] = true, + } + expect(checkListConsistency(list)).to.equal(false) + end) + + it("SHOULD return true for dictionary with string keys", function() + local dictionary = { + foo = "bar", + hello = "world", + } + expect(checkListConsistency(dictionary)).to.equal(true) + end) + + it("SHOULD return false for dictionary with mixed keys", function() + local dictionary = { + foo = "bar", + [100] = "hundred", + } + expect(checkListConsistency(dictionary)).to.equal(false) + end) + + it("SHOULD return false for dictionary with keys that are not a string or number", function() + local dictionary = { + [{}] = true, + } + expect(checkListConsistency(dictionary)).to.equal(false) + end) + + it("SHOULD return false for dictionary with number keys which are not exactly 1..#t", function() + local dictionary = { + [1] = "bar", + [2] = "foo", + [5] = "woof" + } + expect(checkListConsistency(dictionary)).to.equal(false) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepCopy.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepCopy.lua new file mode 100644 index 0000000..9145b3e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepCopy.lua @@ -0,0 +1,27 @@ +--[[ + Returns a deep copy of the given table. + Both the keys and the values of the table are deep copied. + Metatable of the table is copied. + If table is used as key in the input table, + the deep copy will not be deepEqual to the original. +]] + +local function deepCopy(A, seen) + if type(A) ~= 'table' then + return A + end + + if seen and seen[A] then + return seen[A] + end + + local alreadySeen = seen or {} + local newTable = setmetatable({}, getmetatable(A)) + alreadySeen[A] = newTable + for key, value in pairs(A) do + newTable[deepCopy(key, alreadySeen)] = deepCopy(value, alreadySeen) + end + return newTable +end + +return deepCopy diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepCopy.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepCopy.spec.lua new file mode 100644 index 0000000..2b276ac --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepCopy.spec.lua @@ -0,0 +1,66 @@ +return function() + local deepCopy = require(script.Parent.deepCopy) + local deepEqual = require(script.Parent.deepEqual) + + local function deepCopyAndCompare(value) + local aDeepCopyOfValue = deepCopy(value) + expect(deepEqual(value, aDeepCopyOfValue)).to.equal(true) + end + + it("SHOULD work for primitive data types", function() + deepCopyAndCompare(true) + deepCopyAndCompare(false) + deepCopyAndCompare(nil) + deepCopyAndCompare(100) + deepCopyAndCompare("Deep Copy") + end) + + local table1 = { + num = 1, + innerTable = { + innerString = "str" + } + } + local table2 = { + num = 1, + innerTable = { + innerString = "str", + innerInnerTable = table1, + }, + } + it("SHOULD correctly copy table without table as key ", function() + deepCopyAndCompare(table2) + + local deepCopyOfTable2 = deepCopy(table2) + + expect(table2.innerTable).to.never.equal(deepCopyOfTable2.innerTable) + expect(deepEqual(table2.innerTable, deepCopyOfTable2.innerTable)).to.equal(true) + + expect(table2.innerTable.innerInnerTable).to.never.equal(deepCopyOfTable2.innerTable.innerInnerTable) + expect(deepEqual(table2.innerTable.innerInnerTable, deepCopyOfTable2.innerTable.innerInnerTable)).to.equal(true) + end) + + local table3 = { + [table1] = table2, + } + it("SHOULD correctly copy table with table as key", function() + local deepCopyOfTable3 = deepCopy(table3) + expect(deepEqual(table3, deepCopyOfTable3)).to.equal(false) + + local table3Key, table3Value = next(table3) + local deepCopyOfTable3Key, deepCopyOfTable3Value = next(deepCopyOfTable3) + expect(table3Key).to.never.equal(deepCopyOfTable3Key) + expect(deepEqual(table3Key, deepCopyOfTable3Key)).to.equal(true) + expect(table3Value).to.never.equal(deepCopyOfTable3Value) + expect(deepEqual(table3Value, deepCopyOfTable3Value)).to.equal(true) + end) + + it("SHOULD create only one copy of a table in the table", function() + local deepCopyOfTable3 = deepCopy(table3) + local key, value = next(deepCopyOfTable3) + + expect(key).to.never.be.equal(table1) + expect(key).to.be.equal(value.innerTable.innerInnerTable) + expect(deepEqual(key, value.innerTable.innerInnerTable)).to.equal(true) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepEqual.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepEqual.lua new file mode 100644 index 0000000..4f11317 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepEqual.lua @@ -0,0 +1,46 @@ +--[[ + Takes two tables A and B, returns if they are deeply equal. ignoreMetatables specifies if metatables should be ignored + in the deep compare + Assumes tables do not have self-references +]] + +local function deepEqual(A, B, ignoreMetatables) + if A == B then + return true + end + local AType = type(A) + local BType = type(B) + if AType ~= BType then + return false + end + if AType ~= "table" then + return false + end + + if not ignoreMetatables then + local mt1 = getmetatable(A) + if mt1 and mt1.__eq then + --compare using built in method + return A == B + end + end + + local keySet = {} + + for key1, value1 in pairs(A) do + local value2 = B[key1] + if value2 == nil or not deepEqual(value1, value2, ignoreMetatables) then + return false + end + keySet[key1] = true + end + + for key2, _ in pairs(B) do + if not keySet[key2] then + return false + end + end + return true +end + +return deepEqual diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepEqual.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepEqual.spec.lua new file mode 100644 index 0000000..c692a59 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/deepEqual.spec.lua @@ -0,0 +1,57 @@ +return function() + local deepEqual = require(script.Parent.deepEqual) + + it("SHOULD work for primitive data types", function() + expect(deepEqual(1, 1)).to.equal(true) + expect(deepEqual("str1", "str1")).to.equal(true) + expect(deepEqual(1, 2)).to.equal(false) + expect(deepEqual("str1", "str2")).to.equal(false) + expect(deepEqual(nil, nil)).to.equal(true) + expect(deepEqual(nil, false)).to.equal(false) + end) + + it("SHOULD correctly identifies deeply-equal tables", function() + local table1 = { + num = 1, + innerTable = { + innerString = "str" + } + } + local table2 = { + num = 1, + innerTable = { + innerString = "str" + } + } + expect(deepEqual(table1, table2)).to.equal(true) + end) + + it("SHOULD correctly rejects non-deeply-equal tables", function() + local table1 = { + num = 1, + innerTable = { + innerString = "str" + } + } + local table2 = { + num = 1, + innerTable = { + innerString = "differentStr" + } + } + expect(deepEqual(table1, table2)).to.equal(false) + local table3 = { + num = 1, + innerTable = { + innerString = "str" + } + } + local table4 = { + num = 1, + innerTableWithDifferentKey = { + innerString = "str" + } + } + expect(deepEqual(table3, table4)).to.equal(false) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/equalKey.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/equalKey.lua new file mode 100644 index 0000000..0c9c702 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/equalKey.lua @@ -0,0 +1,10 @@ +--[[ + Takes two tables A, B and a key, returns if two tables have the same value at key +]] + +return function(A, B, key) + if A and B and key and key ~= "" and A[key] and B[key] and A[key] == B[key] then + return true + end + return false +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/equalKey.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/equalKey.spec.lua new file mode 100644 index 0000000..89a887a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/equalKey.spec.lua @@ -0,0 +1,78 @@ +return function() + local equalKey = require(script.Parent.equalKey) + + describe("WHEN given nil values", function() + it("SHOULD return false", function() + local testCase = function(tableA, tableB) + expect(equalKey(tableA, tableB)).to.equal(false) + expect(equalKey(tableA, tableB, "")).to.equal(false) + expect(equalKey(tableA, tableB, "key1")).to.equal(false) + end + + testCase(nil, nil) + testCase(nil, {}) + testCase({}, nil) + end) + end) + + describe("WHEN given table values", function() + it("SHOULD return false if key does not exist in either table (empty tables)", function() + local tableA, tableB = {}, {} + expect(equalKey(tableA, tableB)).to.equal(false) + expect(equalKey(tableA, tableB, "")).to.equal(false) + expect(equalKey(tableA, tableB, "key1")).to.equal(false) + end) + + it("SHOULD return false if key does not exist in either table (single keys)", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key2 = "value1", + } + expect(equalKey(tableA, tableB)).to.equal(false) + expect(equalKey(tableA, tableB, "")).to.equal(false) + expect(equalKey(tableA, tableB, "key1")).to.equal(false) + end) + + describe("WHEN key exists in both tables", function() + it("SHOULD return true if value of key is the same", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key1 = "value1", + } + expect(equalKey(tableA, tableB)).to.equal(false) + expect(equalKey(tableA, tableB, "")).to.equal(false) + expect(equalKey(tableA, tableB, "key1")).to.equal(true) + end) + + it("SHOULD return false if value of key is not the same", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key1 = "value2", + } + expect(equalKey(tableA, tableB)).to.equal(false) + expect(equalKey(tableA, tableB, "")).to.equal(false) + expect(equalKey(tableA, tableB, "key1")).to.equal(false) + end) + end) + + it("should return whether tables are equal to each other at key", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key1 = "value1", + key2 = "value2", + } + expect(equalKey(tableA, tableB)).to.equal(false) + expect(equalKey(tableA, tableB, "")).to.equal(false) + expect(equalKey(tableA, tableB, "key1")).to.equal(true) + expect(equalKey(tableA, tableB, "key2")).to.equal(false) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/fieldCount.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/fieldCount.lua new file mode 100644 index 0000000..c8ace7a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/fieldCount.lua @@ -0,0 +1,10 @@ +--[[ + Takes a table and returns the field count +]] +return function(t) + local fieldCount = 0 + for _ in pairs(t) do + fieldCount = fieldCount + 1 + end + return fieldCount +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/fieldCount.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/fieldCount.spec.lua new file mode 100644 index 0000000..57f7e00 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/fieldCount.spec.lua @@ -0,0 +1,29 @@ +return function() + local fieldCount = require(script.Parent.fieldCount) + + describe("WHEN given empty tables", function() + it("SHOULD return zero", function() + expect(fieldCount({})).to.equal(0) + end) + end) + + describe("WHEN given a valid dictionary", function() + it("should return table's field count", function() + local table1 = { + key1 = "value1", + } + expect(fieldCount(table1)).to.equal(1) + + local table2 = { + key1 = "value1", + key2 = "value2", + } + expect(fieldCount(table2)).to.equal(2) + end) + + it("should return list's count", function() + local list1 = {1, 2, 3} + expect(fieldCount(list1)).to.equal(3) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/init.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/init.lua new file mode 100644 index 0000000..de9e651 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/init.lua @@ -0,0 +1,16 @@ +--[[ + Provides functions for comparing and printing lua tables. +]] + +return { + checkListConsistency = require(script.checkListConsistency), + deepEqual = require(script.deepEqual), + deepCopy = require(script.deepCopy), + equalKey = require(script.equalKey), + fieldCount = require(script.fieldCount), + listDifferences = require(script.listDifferences), + print = require(script.print)(print), + shallowEqual = require(script.shallowEqual), + tableDifference = require(script.tableDifference), + toString = require(script.toString), +} diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/listDifferences.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/listDifferences.lua new file mode 100644 index 0000000..4ae41b7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/listDifferences.lua @@ -0,0 +1,35 @@ +local tableDifference = require(script.Parent.tableDifference) + +--[[ + Takes a list and returns a table whose + keys are elements of the list and whose + values are all true +]] +local function membershipTable(list) + local result = {} + for i = 1, #list do + result[list[i]] = true + end + return result +end + + +--[[ + Takes a table and returns a list of keys in that table +]] +local function listOfKeys(t) + local result = {} + for key,_ in pairs(t) do + table.insert(result, key) + end + return result +end + + +--[[ + Takes two lists A and B, returns a new list of elements of A + which are not in B +]] +return function(A, B) + return listOfKeys(tableDifference(membershipTable(A), membershipTable(B))) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/listDifferences.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/listDifferences.spec.lua new file mode 100644 index 0000000..f39ac18 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/listDifferences.spec.lua @@ -0,0 +1,46 @@ +return function() + local listDifferences = require(script.Parent.listDifferences) + local expectTable = require(script.Parent.unitTests.expectTable) + + describe("GIVEN two tables", function() + describe("WHEN the tables are lists", function() + it("SHOULD return an empty table if the lists share the same values", function() + local listA = { 1, 2, 3 } + local listB = { 1, 2, 3 } + expectTable(listDifferences(listA, listB)).toEqual({}) + end) + + it("SHOULD return a list of delta values if first parameter has extra values", function() + local listA = { 1, 2, 3, 4 } + local listB = { 1, 2, 3 } + expectTable(listDifferences(listA, listB)).toEqual({ 4 }) + end) + + it("SHOULD return a list of delta values if the second parameter has extra values", function() + local listA = { 1, 2, 3 } + local listB = { 1, 2, 3, 4 } + expectTable(listDifferences(listA, listB)).toEqual({ 4 }) + end) + end) + + describe("WHEN the tables are dictionaries", function() + it("SHOULD always return an empty table (same dictionaries)", function() + local dictionaryA = { foo = "bar" } + local dictionaryB = { foo = "bar" } + expectTable(listDifferences(dictionaryA, dictionaryB)).toEqual({}) + end) + + it("SHOULD always return an empty table (second has extra keys)", function() + local dictionaryA = { foo = "bar" } + local dictionaryB = { foo = "bar", hello = "world" } + expectTable(listDifferences(dictionaryA, dictionaryB)).toEqual({}) + end) + + it("SHOULD always return an empty table (first has extra keys)", function() + local dictionaryA = { foo = "bar" } + local dictionaryB = { foo = "bar", hello = "world" } + expectTable(listDifferences(dictionaryA, dictionaryB)).toEqual({}) + end) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/print.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/print.lua new file mode 100644 index 0000000..43b4cee --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/print.lua @@ -0,0 +1,73 @@ +return function(print) + local function makeKeyString(key) + if type(key) == "string" then + return string.format("%s", key) + else + return string.format("[%s]", tostring(key)) + end + end + + local function makeValueString(value) + local valueType = type(value) + if valueType == "string" then + return string.format("%q", value) + elseif valueType == "function" or valueType == "table" then + return string.format("<%s>", tostring(value)) + else + return string.format("%s", tostring(value)) + end + end + + local function printKeypair(key, value, indentStr, comment) + local keyString = makeKeyString(key) + local valueString = makeValueString(value) + + local commentStr = comment and string.format(" -- %s", comment) or "" + print(string.format("%s%s = %s,%s", indentStr, keyString, valueString, commentStr)) + end + + --[[ + For debugging. Prints the table on multiple lines to overcome log-line length + limitations which are otherwise necessary for performance. Use sparingly. + ]] + return function(t, indent) + indent = indent or ' ' + + if type(t) ~= "table" then + error("tutils.Print must be passed a table", 2) + end + + -- For cycle detection + local printedTables = {} + + local function recurse(subTable, tableKey, level) + -- Prevent cycles by keeping track of what tables we have printed + printedTables[subTable] = true + + local indentStr = string.rep(indent, level) + local valueIndentStr = string.rep(indent, level + 1) + + if tableKey then + print(string.format("%s%s = %s {", indentStr, makeKeyString(tableKey), makeValueString(subTable))) + else + print(string.format("%s%s {", indentStr, makeValueString(subTable))) + end + + for key, value in pairs(subTable) do + if type(value) == "table" then + if printedTables[value] then + printKeypair(key, value, valueIndentStr, "Possible cycle") + else + recurse(value, key, level + 1) + end + else + printKeypair(key, value, valueIndentStr) + end + end + + print(string.format("%s}%s", indentStr, (level > 0 and "," or ""))) + end + + recurse(t, nil, 0) + end +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/print.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/print.spec.lua new file mode 100644 index 0000000..53e984c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/print.spec.lua @@ -0,0 +1,92 @@ +return function() + local history = {} + local mockPrint = function(...) + local str = table.concat({ ... }, " " ) + table.insert(history, str) + end + local clearHistory = function() + history = {} + end + + local print = require(script.Parent.print)(mockPrint) + + describe("GIVEN a table", function() + it("SHOULD handle an empty table appropriately", function() + print({}) + + local firstLine = history[1] + expect(firstLine:sub(-1)).to.equal("{") + local secondLine = history[2] + expect(secondLine).to.equal("}") + + clearHistory() + end) + + it("SHOULD handle a simple list appropriately", function() + print({ 1, 2, 3 }) + + local firstLine = history[1] + expect(firstLine:sub(-1)).to.equal("{") + local lastLine = history[#history] + expect(lastLine).to.equal("}") + + local firstElement = history[2] + expect(firstElement).to.equal(" [1] = 1,") + local secondElement = history[3] + expect(secondElement).to.equal(" [2] = 2,") + local thirdElement = history[4] + expect(thirdElement).to.equal(" [3] = 3,") + + clearHistory() + end) + + it("SHOULD handle a simple dictionary appropriately", function() + print({ foo = "bar", hello = "world" }) + + local firstLine = history[1] + expect(firstLine:sub(-1)).to.equal("{") + local lastLine = history[#history] + expect(lastLine).to.equal("}") + + local firstElement = history[2] + expect(firstElement).to.equal(" hello = \"world\",") + local secondElement = history[3] + expect(secondElement).to.equal(" foo = \"bar\",") + + clearHistory() + end) + + it("SHOULD handle a cyclic dictionary by printing a warning", function() + local tab = {} + tab.element1 = tab + print(tab) + + local firstLine = history[1] + expect(firstLine:sub(-1)).to.equal("{") + local lastLine = history[#history] + expect(lastLine).to.equal("}") + + local firstElement = history[2] + local isFound = firstElement:find("Possible cycle$") + expect(isFound).to.be.ok() + + clearHistory() + end) + end) + + describe("GIVEN anything else", function() + it("SHOULD throw", function() + expect(function() + print(1) + end).to.throw() + + expect(function() + print(true) + end).to.throw() + + expect(function() + print("hello world") + end).to.throw() + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/shallowEqual.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/shallowEqual.lua new file mode 100644 index 0000000..f20eb41 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/shallowEqual.lua @@ -0,0 +1,27 @@ +--[[ + Takes two tables A and B, returns if they have the same key-value pairs + Except ignored keys +]] +return function(A, B, ignore) + if not A or not B then + return false + elseif A == B then + return true + end + if not ignore then + ignore = {} + end + + for key, value in pairs(A) do + if B[key] ~= value and not ignore[key] then + return false + end + end + for key, value in pairs(B) do + if A[key] ~= value and not ignore[key] then + return false + end + end + + return true +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/shallowEqual.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/shallowEqual.spec.lua new file mode 100644 index 0000000..0fb0f53 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/shallowEqual.spec.lua @@ -0,0 +1,108 @@ +return function() + local shallowEqual = require(script.Parent.shallowEqual) + + describe("WHEN given nil values", function() + it("SHOULD return false if both values are nil", function() + expect(shallowEqual(nil, nil)).to.equal(false) + end) + + it("SHOULD return false if either value is nil", function() + expect(shallowEqual(nil, {})).to.equal(false) + expect(shallowEqual({}, nil)).to.equal(false) + end) + end) + + describe("WHEN given similar table values", function() + it("SHOULD return true for two empty tables", function() + expect(shallowEqual({}, {})).to.equal(true) + end) + + it("SHOULD return true for one key dictionaries", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key1 = "value1", + } + expect(shallowEqual(tableA, tableB)).to.equal(true) + end) + end) + + describe("WHEN given dissimilar table values", function() + it("SHOULD return false for same key, different values", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key1 = "value2", + } + expect(shallowEqual(tableA, tableB)).to.equal(false) + end) + + it("SHOULD return false for different keys, same values", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key2 = "value1", + } + expect(shallowEqual(tableA, tableB)).to.equal(false) + end) + + it("SHOULD return false for different keys, different values", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key2 = "value2", + } + expect(shallowEqual(tableA, tableB)).to.equal(false) + end) + + it("SHOULD return false for extra keys", function() + local tableA = { + key1 = "value1", + } + local tableB = { + key1 = "value1", + key2 = "value2", + } + expect(shallowEqual(tableA, tableB)).to.equal(false) + end) + + it("SHOULD return false if the different table value exist in inner level of nesting tables", function() + local tableA = { + value1 = "value1", + value2 = { + innerValue1 = "value2", + innerValue2 = "value3", + } + } + local tableB = { + value1 = "value1", + value2 = { + innerValue1 = "value2", + innerValue2 = "value4", + } + } + expect(shallowEqual(tableA, tableB)).to.equal(false) + end) + end) + + describe("WHEN given ignore table", function() + it("SHOULD return true if the different table value exist in ignore table", function() + local tableA = { + value1 = "value1", + value2 = "value2", + } + local tableB = { + value1 = "value1", + value2 = "value1", + } + local ignore = { + value2 = "value1", + } + expect(shallowEqual(tableA, tableB, ignore)).to.equal(true) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/tableDifference.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/tableDifference.lua new file mode 100644 index 0000000..d805c30 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/tableDifference.lua @@ -0,0 +1,21 @@ +--[[ + Takes two tables A and B, returns a new table with elements of A + which are either not keys in B or have a different value in B +]] +return function(A, B) + local new = {} + + for keyA, valueA in pairs(A) do + if B[keyA] ~= A[keyA] then + new[keyA] = valueA + end + end + + for keyB, valueB in pairs(B) do + if B[keyB] ~= A[keyB] then + new[keyB] = valueB + end + end + + return new +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/tableDifference.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/tableDifference.spec.lua new file mode 100644 index 0000000..641bb2a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/tableDifference.spec.lua @@ -0,0 +1,48 @@ +return function() + local tableDifference = require(script.Parent.tableDifference) + local expectTable = require(script.Parent.unitTests.expectTable) + + describe("WHEN given two tables", function() + it("SHOULD return an empty table if lists are the same", function() + local listA = {1, 2, 3} + local listB = {1, 2, 3} + + expectTable(tableDifference(listA, listB)).toEqual({}) + end) + + it("SHOULD return a dictionary of the key value if the first list has an extra value", function() + local listA = {1, 2, 3, 4} + local listB = {1, 2, 3} + + expectTable(tableDifference(listA, listB)).toEqual({[4] = 4}) + end) + + it("SHOULD return a dictionary of the key value if the second list has an extra value", function() + local listA = {1, 2, 3} + local listB = {1, 2, 3, 4} + + expectTable(tableDifference(listA, listB)).toEqual({ [4] = 4 }) + end) + + it("SHOULD return a dictionary of the key value if a dictionary has an extra value", function() + local dictionaryA = { foo = "bar", hello = "world" } + local dictionaryB = { foo = "bar" } + + expectTable(tableDifference(dictionaryA, dictionaryB)).toEqual({ hello = "world" }) + end) + + it("SHOULD return a dictionary of the second table's key value if the key has different value in two tables", function() + local dictionaryA = { foo = "foo" } + local dictionaryB = { foo = "bar" } + + expectTable(tableDifference(dictionaryA, dictionaryB)).toEqual({ foo = "bar" }) + end) + + it("SHOULD return both keys and their value if there are 2 keys with same value in 2 tables", function() + local dictionaryA = { foo = "bar", hello = "world" } + local dictionaryB = { bar = "bar", hello = "world" } + + expectTable(tableDifference(dictionaryA, dictionaryB)).toEqual({ foo = "bar", bar = "bar" }) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/toString.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/toString.lua new file mode 100644 index 0000000..f6a5a24 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/toString.lua @@ -0,0 +1,36 @@ +local checkListConsistency = require(script.Parent.checkListConsistency) + +--[[ + Serializes the given table to a reasonable string that might even interpret as lua. +]] +local function recursiveToString(t, indent) + indent = indent or '' + + if type(t) == 'table' then + local result = "" + if not checkListConsistency(t) then + result = result .. "-- WARNING: this table fails the list consistency test\n" + end + result = result .. "{\n" + for k,v in pairs(t) do + if type(k) == 'string' then + result = result + .. " " + .. indent + .. tostring(k) + .. " = " + .. recursiveToString(v, " "..indent) + ..";\n" + end + if type(k) == 'number' then + result = result .. " " .. indent .. recursiveToString(v, " "..indent)..",\n" + end + end + result = result .. indent .. "}" + return result + else + return tostring(t) + end +end + +return recursiveToString \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/toString.spec.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/toString.spec.lua new file mode 100644 index 0000000..e52104a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/toString.spec.lua @@ -0,0 +1,40 @@ +return function() + local toString = require(script.Parent.toString) + + describe("WHEN given a table", function() + it("SHOULD handle simple lists", function() + local indent = "." + local result = toString({ 1, 2, 3 }, indent) + + expect(result).to.equal("{\n .1,\n .2,\n .3,\n.}") + end) + + it("SHOULD handle simple dictionaries", function() + local indent = "." + local result = toString({ hello = "world" }, indent) + + expect(result).to.equal("{\n .hello = world;\n.}") + end) + + it("SHOULD handle tables within tables", function() + local indent = "." + local result = toString({ {} }, indent) + + expect(result).to.equal("{\n .{\n .},\n.}") + end) + + it("SHOULD show a warning for mixed tables", function() + local result = toString({ 1, 2, hello = "world" }) + local findResult = result:find("WARNING: this table fails the list consistency test") + expect(findResult).to.be.ok() + end) + end) + + describe("WHEN given anything else", function() + it("SHOULD return the tostring equivalent", function() + expect(toString(1)).to.equal(tostring(1)) + expect(toString(true)).to.equal(tostring(true)) + expect(toString("hello")).to.equal(tostring("hello")) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/unitTests/expectTable.lua b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/unitTests/expectTable.lua new file mode 100644 index 0000000..a25b85a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/_Index/tutils/tutils/unitTests/expectTable.lua @@ -0,0 +1,15 @@ +local shallowEqual = require(script.Parent.Parent.shallowEqual) +local toString = require(script.Parent.Parent.toString) + +local function expectTable(tab) + return { + toEqual = function(value) + assert( + shallowEqual(tab, value), + string.format("expected: %s\ninstead got: %s", toString(value), toString(tab)) + ) + end, + } +end + +return expectTable \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/enumerate.lua b/Client2021/ExtraContent/LuaPackages/Packages/enumerate.lua new file mode 100644 index 0000000..73eb2b2 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/enumerate.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_enumerate"]["enumerate"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/t.lua b/Client2021/ExtraContent/LuaPackages/Packages/t.lua new file mode 100644 index 0000000..ef185ec --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/t.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["roblox_t"]["t"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Packages/tutils.lua b/Client2021/ExtraContent/LuaPackages/Packages/tutils.lua new file mode 100644 index 0000000..60f9805 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Packages/tutils.lua @@ -0,0 +1,12 @@ +--[[ + Package link auto-generated by Rotriever +]] +local PackageIndex = script.Parent._Index + +local package = PackageIndex["tutils"]["tutils"] + +if package.ClassName == "ModuleScript" then + return require(package) +end + +return package \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/PolicyProvider.lua b/Client2021/ExtraContent/LuaPackages/PolicyProvider.lua new file mode 100644 index 0000000..78cc600 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/PolicyProvider.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.PolicyProvider) diff --git a/Client2021/ExtraContent/LuaPackages/PremiumUpsellDeps.lua b/Client2021/ExtraContent/LuaPackages/PremiumUpsellDeps.lua new file mode 100644 index 0000000..40d4418 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/PremiumUpsellDeps.lua @@ -0,0 +1,12 @@ + +--[[ + Proxy package for dependencies for PremiumUpsellDeps. +]] + +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.PremiumUpsellDeps) diff --git a/Client2021/ExtraContent/LuaPackages/Promise.lua b/Client2021/ExtraContent/LuaPackages/Promise.lua new file mode 100644 index 0000000..be7c277 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Promise.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Promise) diff --git a/Client2021/ExtraContent/LuaPackages/PurchasePrompt.lua b/Client2021/ExtraContent/LuaPackages/PurchasePrompt.lua new file mode 100644 index 0000000..e90a85d --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/PurchasePrompt.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.PurchasePrompt) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/PurchasePromptDeps.lua b/Client2021/ExtraContent/LuaPackages/PurchasePromptDeps.lua new file mode 100644 index 0000000..5896cf7 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/PurchasePromptDeps.lua @@ -0,0 +1,12 @@ + +--[[ + Proxy package for dependencies for SocialLibraries. +]] + +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.PurchasePromptDeps) diff --git a/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/Constants.lua b/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/Constants.lua new file mode 100644 index 0000000..88ce971 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/Constants.lua @@ -0,0 +1,8 @@ +local Constants = { + SIGNALR_NAMESPACE = "TimedEntertainmentAllowanceNotifications", + SIGNALR_TYPE_NEW_INSTRUCTION = "NewInstruction", + HEARTBEAT_NOTIFICATIONS_NAMESPACE = "ScreenTimeClientNotifications", + HEARTBEAT_NOTIFICATION_TYPE_NEW_INSTRUCTION = "ScreentimeInstructionCheck", +} + +return Constants diff --git a/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/GetFFlagScreenTime.lua b/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/GetFFlagScreenTime.lua new file mode 100644 index 0000000..23bdccd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/GetFFlagScreenTime.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("LuaEnableScreenTime", false) + +return function() + return game:GetFastFlag("LuaEnableScreenTime") +end diff --git a/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/GetFFlagScreenTimeSignalR.lua b/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/GetFFlagScreenTimeSignalR.lua new file mode 100644 index 0000000..0f00a05 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/GetFFlagScreenTimeSignalR.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("LuaEnableScreenTimeSignalR", false) + +return function() + return game:GetFastFlag("LuaEnableScreenTimeSignalR") +end diff --git a/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/HttpRequests.lua b/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/HttpRequests.lua new file mode 100644 index 0000000..158afe4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/HttpRequests.lua @@ -0,0 +1,183 @@ +--[[ + Provides HTTP request methods for ScreenTime feature. +]] + +local CorePackages = game:GetService("CorePackages") +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) +local UrlBuilder = require(CorePackages.Packages.UrlBuilder).UrlBuilder +local Logging = require(CorePackages.Logging) +local ArgCheck = require(CorePackages.ArgCheck) + +local TEA_END_POINTS = { + GET_INSTRUCTIONS = "timed-entertainment-allowance/v1/instructions", + REPORT_EXECUTION = "timed-entertainment-allowance/v1/reportExecute", +} + +local RESPONSE_FORMATS = { + GET_INSTRUCTIONS = { + errorCode = "number", + instructions = "table" + }, + INSTRUCTION = { + type = "number", + instructionName = "string", + serialId = "string", + title = "string", + message = "string", + url = "string", + modalType = "number", + data = "string" + }, + REPORT_EXECUTION = { + errorCode = "number", + }, +} + +local TAG = "HttpRequests" + +local getInstructionsUrl = UrlBuilder.new({ + base = Url.APIS_URL, + path = TEA_END_POINTS.GET_INSTRUCTIONS +})() + +local reportExecutionUrl = UrlBuilder.new({ + base = Url.APIS_URL, + path = TEA_END_POINTS.REPORT_EXECUTION +})() + +--[[ + A helper function to check object table have the required fields and fields' + type specified in format table. + It will throw exceptions, so must be encapsulated by pcall. +]] +local function checkFormat(format, object) + for key, typeString in pairs(format) do + assert(object[key] ~= nil, "Missing key") + assert(type(object[key]) == typeString, "Wrong type") + end +end + +local HttpRequests = { + httpService = nil, +} + +--[[ + Create a new HttpRequests object. + + @param httpService: Pass in HttpService from game:GetService("HttpService") +]] +function HttpRequests:new(httpService) + ArgCheck.isNotNil(httpService, "httpService") + local obj = { + httpService = httpService, + } + setmetatable(obj, self) + self.__index = self + return obj +end + +--[[ + Query TEA endpoint to get the instructions to execute. + + @param callback: it should be non-nil with signature: + callback(success, unauthorized, instructions) + success (boolean): indicate whether the query is successful. + unauthorized (boolean): if success is false, indicate whether the + failure is due to authorization issue. + instructions (table): if success is true, this is the result + (associative array) of tables following + RESPONSE_FORMATS.INSTRUCTION format. +]] +function HttpRequests:getInstructions(callback) + ArgCheck.isNotNil(self.httpService, "httpService") + ArgCheck.isNotNil(callback, "callback") + local httpRequest = self.httpService:RequestInternal({ + Url = getInstructionsUrl, + Method = "GET", + }) + httpRequest:Start(function(reqSuccess, reqResponse) + local success + local err + local unauthorized = false + local instructions = {} + if not reqSuccess then + success = false + err = "Connection error" + elseif reqResponse.StatusCode == 401 then + success = false + unauthorized = true + err = "Unauthorized" + elseif reqResponse.StatusCode < 200 or reqResponse.StatusCode >= 400 then + success = false + err = "Status code: " .. reqResponse.StatusCode + else + -- reqSuccess == true and StatusCode >= 200 and StatusCode < 400 + success, err = pcall(function() + local json = self.httpService:JSONDecode(reqResponse.Body) + checkFormat(RESPONSE_FORMATS.GET_INSTRUCTIONS, json) + assert(json.errorCode == 0, "Error code is not 0") + for i, instruction in ipairs(json.instructions) do + checkFormat(RESPONSE_FORMATS.INSTRUCTION, instruction) + end + instructions = json.instructions + end) + end + if not success then + Logging.warn(TAG .. " getInstructions failed: " .. getInstructionsUrl .. ", ".. err) + end + callback(success, unauthorized, instructions) + end) +end + +--[[ + Tell TEA endpoint that an instruction has been executed. + + @param instructionName: from RESPONSE_FORMATS.INSTRUCTION + @param serialId: from RESPONSE_FORMATS.INSTRUCTION + @param callback: can be nil, signature: callback(success) + success (boolean): indicate whether the http request is successful. +]] +function HttpRequests:reportExecution(instructionName, serialId, callback) + ArgCheck.isNotNil(self.httpService, "httpService") + -- ISO 8601, Example: 2020-06-04T04:44:09Z + local formattedTime = os.date("%Y-%m-%dT%H:%M:%SZ") + local payload = self.httpService:JSONEncode({ + instructionName = instructionName, + serialId = serialId, + execTime = formattedTime, + }) + local httpRequest = self.httpService:RequestInternal({ + Url = reportExecutionUrl, + Method = "POST", + Headers = { + ["Content-Type"] = "application/json", + }, + Body = payload, + }) + httpRequest:Start(function(reqSuccess, reqResponse) + local success + local err + if not reqSuccess then + success = false + err = "Connection error" + elseif reqResponse.StatusCode < 200 and reqResponse.StatusCode >= 400 then + success = false + err = "Status code: " .. reqResponse.StatusCode + else + -- reqSuccess == true and StatusCode >= 200 and StatusCode < 400 + success, err = pcall(function() + local json = self.httpService:JSONDecode(reqResponse.Body) + checkFormat(RESPONSE_FORMATS.REPORT_EXECUTION, json) + assert(json.errorCode == 0, "Error code is not 0") + end) + end + if not success then + Logging.warn(TAG .. " reportExecution failed: " .. reportExecutionUrl .. ", ".. err) + end + if callback ~= nil then + callback(success) + end + end) +end + +return HttpRequests diff --git a/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/HttpRequests.spec.lua b/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/HttpRequests.spec.lua new file mode 100644 index 0000000..be0a6ea --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/HttpRequests.spec.lua @@ -0,0 +1,250 @@ +local HttpRequests = require(script.Parent.HttpRequests) + +function createMockHttpService(success, statusCode, errorCode, instructions) + local testBody = "test-body" + return { + RequestInternal = function(self, params) + return { + Start = function(self, callback) + local response = { + Body = testBody, + StatusCode = statusCode, + } + callback(success, response) + end + } + end, + JSONDecode = function(self, body) + assert(body == testBody) + return { + errorCode = errorCode, + instructions = instructions, + } + end, + JSONEncode = function(self, param) + return testBody + end, + } +end + +return function() + local testInstructions = {{ + type = 3, + instructionName = "name", + serialId = "id", + title = "title", + message = "message", + url = "url", + modalType = 0, + data = "", + }} + + describe("getInstructions()", function() + it("should correctly callback when succeeded", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 200, 0, testInstructions)) + local called = false + function callback(success, unauthorized, instructions) + expect(success).to.equal(true) + expect(unauthorized).to.equal(false) + expect(instructions).to.equal(testInstructions) + called = true; + end + httpRequests:getInstructions(callback) + expect(called).to.equal(true) + end) + + it("should throw when callback is nil", function() + local httpRequests = HttpRequests:new(createMockHttpService(false)) + success, err = pcall(function() + httpRequests:getInstructions() + end) + expect(success).to.equal(false) + end) + + it("should throw when not get from new", function() + called = false + function callback(success, unauthorized, instructions) + called = true; + end + success, err = pcall(function() + HttpRequests:getInstructions(callback) + end) + expect(success).to.equal(false) + expect(called).to.equal(false) + end) + + it("should correctly callback when connection error", function() + local httpRequests = HttpRequests:new(createMockHttpService(false)) + local called = false + function callback(success, unauthorized, instructions) + expect(success).to.equal(false) + called = true; + end + httpRequests:getInstructions(callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when 401", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 401)) + local called = false + function callback(success, unauthorized, instructions) + expect(success).to.equal(false) + expect(unauthorized).to.equal(true) + called = true; + end + httpRequests:getInstructions(callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when 412", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 412)) + local called = false + function callback(success, unauthorized, instructions) + expect(success).to.equal(false) + expect(unauthorized).to.equal(false) + called = true; + end + httpRequests:getInstructions(callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when errorCode", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 200, 1)) + local called = false + function callback(success, unauthorized, instructions) + expect(success).to.equal(false) + expect(unauthorized).to.equal(false) + called = true; + end + httpRequests:getInstructions(callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when decoding failed", function() + local httpService = createMockHttpService(true, 200, 0) + httpService.JSONDecode = function(self, body) + assert(false) + end + local httpRequests = HttpRequests:new(httpService) + local called = false + function callback(success, unauthorized, instructions) + expect(success).to.equal(false) + expect(unauthorized).to.equal(false) + called = true; + end + httpRequests:getInstructions(callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when wrong response json format", function() + local httpService = createMockHttpService(true, 200, 0) + httpService.JSONDecode = function(self, body) + return { + errorCode = 0, + } + end + local httpRequests = HttpRequests:new(httpService) + local called = false + function callback(success, unauthorized, instructions) + expect(success).to.equal(false) + expect(unauthorized).to.equal(false) + called = true; + end + httpRequests:getInstructions(callback) + expect(called).to.equal(true) + end) + end) + + describe("reportExecution()", function() + it("should correctly callback when succeeded", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 200, 0)) + local called = false + function callback(success) + expect(success).to.equal(true) + called = true; + end + httpRequests:reportExecution("a", "b", callback) + expect(called).to.equal(true) + end) + + it("should throw when not get from new", function() + called = false + function callback(success) + called = true; + end + success, err = pcall(function() + HttpRequests:reportExecution("a", "b", callback) + end) + expect(success).to.equal(false) + expect(called).to.equal(false) + end) + + it("should be ok with nil callback when succeeded", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 200, 0)) + httpRequests:reportExecution("a", "b", nil) + end) + + it("should correctly callback when connection error", function() + local httpRequests = HttpRequests:new(createMockHttpService(false)) + local called = false + function callback(success) + expect(success).to.equal(false) + called = true; + end + httpRequests:reportExecution("a", "b", callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when 401", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 401)) + local called = false + function callback(success) + expect(success).to.equal(false) + called = true; + end + httpRequests:reportExecution("a", "b", callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when errorCode", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 200, 1)) + local called = false + function callback(success) + expect(success).to.equal(false) + called = true; + end + httpRequests:reportExecution("a", "b", callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when decoding failed", function() + local httpService = createMockHttpService(true, 200, 0) + httpService.JSONDecode = function(self, body) + assert(false) + end + local httpRequests = HttpRequests:new(httpService) + local called = false + function callback(success) + expect(success).to.equal(false) + called = true; + end + httpRequests:reportExecution("a", "b", callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when wrong response json format", function() + local httpService = createMockHttpService(true, 200, 0) + httpService.JSONDecode = function(self, body) + return { } + end + local httpRequests = HttpRequests:new(httpService) + local called = false + function callback(success) + expect(success).to.equal(false) + called = true; + end + httpRequests:reportExecution("a", "b", callback) + expect(called).to.equal(true) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/Utils.lua b/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/Utils.lua new file mode 100644 index 0000000..16591d6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/Utils.lua @@ -0,0 +1,59 @@ +--[[ + Provides utility methods for ScreenTime feature. +]] + +local CorePackages = game:GetService("CorePackages") +local ArgCheck = require(CorePackages.ArgCheck) +local Logging = require(CorePackages.Logging) + +local TAG = "Utils" +-- This global key will be accessed by native code +local GLOBAL_KEY_LOCKED_OUT = "ScreenTime.lockedOut" +-- This string will be checked against in native code (Java, etc.) +local TRUE_VALUE = "true" + +local Utils = { + globalGetter = nil, + globalSetter = nil, +} + +--[[ + Create a new Utils object. + + @param dependencies injected dependencies to get and set global key values. + The value should be accessible from native code. + members & interfaces: + globalGetter(string key) -> string + globalSetter(string key, string value) +]] +function Utils:new(dependencies) + ArgCheck.isType(dependencies.globalGetter, "function", "globalGetter") + ArgCheck.isType(dependencies.globalSetter, "function", "globalSetter") + local obj = { + globalGetter = dependencies.globalGetter, + globalSetter = dependencies.globalSetter, + } + setmetatable(obj, self) + self.__index = self + return obj +end + +--[[ + Get the flag whether current user is locked out. +]] +function Utils:isLockedOut() + ArgCheck.isType(self.globalGetter, "function", "globalGetter") + return (self.globalGetter(GLOBAL_KEY_LOCKED_OUT) == TRUE_VALUE) +end + +--[[ + Set the flag that current user is locked out. + It will be reset after successful MSDK login. +]] +function Utils:setLockedOut() + Logging.warn(TAG .. " setLockedOut") + ArgCheck.isType(self.globalSetter, "function", "globalSetter") + self.globalSetter(GLOBAL_KEY_LOCKED_OUT, TRUE_VALUE) +end + +return Utils diff --git a/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/Utils.spec.lua b/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/Utils.spec.lua new file mode 100644 index 0000000..653b156 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Regulations/ScreenTime/Utils.spec.lua @@ -0,0 +1,52 @@ +local Utils = require(script.Parent.Utils) + +function createMockDependencies() + local dict = {} + return { + globalGetter = function(key) + return dict[key] + end, + globalSetter = function(key, val) + dict[key] = val + end, + } +end + +return function() + describe("new()", function() + it("should throw when getter is nil", function() + local mock = createMockDependencies() + mock.globalGetter = nil + success, err = pcall(function() + local utils = Utils:new(mock) + end) + expect(success).to.equal(false) + end) + + it("should throw when setter is nil", function() + local mock = createMockDependencies() + mock.globalSetter = nil + success, err = pcall(function() + local utils = Utils:new(mock) + end) + expect(success).to.equal(false) + end) + end) + + describe("isLockedOut()", function() + it("should return false before set", function() + local mock = createMockDependencies() + local utils = Utils:new(mock) + local flag = utils:isLockedOut() + expect(flag).to.equal(false) + end) + + it("should return true after set", function() + local mock = createMockDependencies() + local utils = Utils:new(mock) + utils:setLockedOut() + local flag = utils:isLockedOut() + expect(flag).to.equal(true) + end) + end) +end diff --git a/Client2021/ExtraContent/LuaPackages/Result.lua b/Client2021/ExtraContent/LuaPackages/Result.lua new file mode 100644 index 0000000..bcdcb3c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Result.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Result) diff --git a/Client2021/ExtraContent/LuaPackages/Rhodium.lua b/Client2021/ExtraContent/LuaPackages/Rhodium.lua new file mode 100644 index 0000000..7e41283 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Rhodium.lua @@ -0,0 +1,7 @@ +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Dev.Rhodium) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Roact.lua b/Client2021/ExtraContent/LuaPackages/Roact.lua new file mode 100644 index 0000000..abc1a07 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Roact.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Roact) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/RoactNavigation.lua b/Client2021/ExtraContent/LuaPackages/RoactNavigation.lua new file mode 100644 index 0000000..4df4421 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/RoactNavigation.lua @@ -0,0 +1,6 @@ +local CorePackages = game:GetService("CorePackages") +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.RoactNavigation) diff --git a/Client2021/ExtraContent/LuaPackages/RoactNavigationOld.lua b/Client2021/ExtraContent/LuaPackages/RoactNavigationOld.lua new file mode 100644 index 0000000..2f41b2f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/RoactNavigationOld.lua @@ -0,0 +1,6 @@ +local CorePackages = game:GetService("CorePackages") +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.RoactNavigationOld) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/RoactRodux.lua b/Client2021/ExtraContent/LuaPackages/RoactRodux.lua new file mode 100644 index 0000000..117fa09 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/RoactRodux.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.RoactRodux) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/RoactUtilities/.robloxrc b/Client2021/ExtraContent/LuaPackages/RoactUtilities/.robloxrc new file mode 100644 index 0000000..321fd28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/RoactUtilities/.robloxrc @@ -0,0 +1,12 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "ImplicitReturn": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/RoactUtilities/ExternalEventConnection.lua b/Client2021/ExtraContent/LuaPackages/RoactUtilities/ExternalEventConnection.lua new file mode 100644 index 0000000..0c1bc58 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/RoactUtilities/ExternalEventConnection.lua @@ -0,0 +1,51 @@ +--[[ + A component that establishes a connection to a Roblox event when it is rendered. +]] + +local CorePackages = game:GetService("CorePackages") +local Roact = require(CorePackages.Roact) +local ExternalEventConnection = Roact.Component:extend("ExternalEventConnection") + +function ExternalEventConnection:init() + self.connection = nil +end + +--[[ + Render the child component so that ExternalEventConnections can be nested like so: + + Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputBegan, + callback = inputBeganCallback, + }, { + Roact.createElement(ExternalEventConnection, { + event = UserInputService.InputEnded, + callback = inputChangedCallback, + }) + }) +]] +function ExternalEventConnection:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +function ExternalEventConnection:didMount() + local event = self.props.event + local callback = self.props.callback + + self.connection = event:Connect(callback) +end + +function ExternalEventConnection:didUpdate(oldProps) + if self.props.event ~= oldProps.event or self.props.callback ~= oldProps.callback then + self.connection:Disconnect() + + self.connection = self.props.event:Connect(self.props.callback) + end +end + +function ExternalEventConnection:willUnmount() + self.connection:Disconnect() + + self.connection = nil +end + +return ExternalEventConnection \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/RoactUtilities/ExternalEventConnection.spec.lua b/Client2021/ExtraContent/LuaPackages/RoactUtilities/ExternalEventConnection.spec.lua new file mode 100644 index 0000000..7a80b7f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/RoactUtilities/ExternalEventConnection.spec.lua @@ -0,0 +1,91 @@ +return function () + local CorePackages = game:GetService("CorePackages") + local Roact = require(CorePackages.Roact) + local ExternalEventConnection = require(script.Parent.ExternalEventConnection) + + it("if mounted, should call the callback when the event is triggered", function() + local event = Instance.new("BindableEvent") + local count = 0 + + local element = Roact.createElement(ExternalEventConnection, { + event = event.Event, + callback = function() + count = count + 1 + end, + }) + + local RoactInstance = Roact.mount(element) + event:Fire() + + expect(count).to.equal(1) + + Roact.unmount(RoactInstance) + event:Fire() + + expect(count).to.equal(1) + + event:Destroy() + end) + + it("should handle updating the callback or event", function() + local firstEvent = Instance.new("BindableEvent") + local secondEvent = Instance.new("BindableEvent") + local count = 0 + local changeState + + local EventContainer = Roact.Component:extend("EventContainer") + + function EventContainer:init() + self.state = { + event = firstEvent.Event, + callback = function() + count = count + 1 + end, + } + end + + function EventContainer:render() + return Roact.createElement(ExternalEventConnection, { + event = self.state.event, + callback = self.state.callback, + }) + end + + function EventContainer:didMount() + changeState = function(newState) + self:setState(newState) + end + end + + function EventContainer:willUnmount() + changeState = nil + end + + Roact.mount(Roact.createElement(EventContainer)) + firstEvent:Fire() + + expect(count).to.equal(1) + + changeState({ + event = secondEvent.Event, + }) + firstEvent:Fire() + + expect(count).to.equal(1) + + secondEvent:Fire() + + expect(count).to.equal(2) + + changeState({ + callback = function() + -- this is intentionally blank + end, + }) + secondEvent:Fire() + + expect(count).to.equal(2) + firstEvent:Destroy() + secondEvent:Destroy() + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Rodux.lua b/Client2021/ExtraContent/LuaPackages/Rodux.lua new file mode 100644 index 0000000..c300b31 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Rodux.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Rodux) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/Symbol.lua b/Client2021/ExtraContent/LuaPackages/Symbol.lua new file mode 100644 index 0000000..16d4a1a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/Symbol.lua @@ -0,0 +1,7 @@ +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.SymbolImpl) + +return require(CorePackages.SymbolImpl) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/SymbolImpl/.robloxrc b/Client2021/ExtraContent/LuaPackages/SymbolImpl/.robloxrc new file mode 100644 index 0000000..321fd28 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/SymbolImpl/.robloxrc @@ -0,0 +1,12 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "ImplicitReturn": "fatal", + "DeprecatedGlobal": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/SymbolImpl/Symbol.lua b/Client2021/ExtraContent/LuaPackages/SymbolImpl/Symbol.lua new file mode 100644 index 0000000..8b6adaf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/SymbolImpl/Symbol.lua @@ -0,0 +1,43 @@ +--[[ + A 'Symbol' is an opaque marker type that can be used to signify unique + statuses. Symbols have the type 'userdata', but when printed to the console, + the name of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +--[[ + Create an unnamed Symbol. Usually, you should create a named Symbol using + Symbol.named(name) +]] +function Symbol.unnamed() + local self = newproxy(true) + + getmetatable(self).__tostring = function() + return "Unnamed Symbol" + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/SymbolImpl/Symbol.spec.lua b/Client2021/ExtraContent/LuaPackages/SymbolImpl/Symbol.spec.lua new file mode 100644 index 0000000..03440f6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/SymbolImpl/Symbol.spec.lua @@ -0,0 +1,45 @@ +return function() + local Symbol = require(script.Parent.Symbol) + + describe("named", function() + it("should give an opaque object", function() + local symbol = Symbol.named("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = Symbol.named("foo") + local location = tostring(symbol):find("foo") + expect(location).to.be.ok() + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.named("abc") + local symbolB = Symbol.named("abc") + + expect(symbolA).never.to.equal(symbolB) + end) + end) + + describe("unnamed", function() + it("should give an opaque object", function() + local symbol = Symbol.unnamed() + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to some string", function() + local symbol = Symbol.unnamed() + + expect(tostring(symbol)).to.be.a("string") + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.unnamed() + local symbolB = Symbol.unnamed() + + expect(symbolA).never.to.equal(symbolB) + end) + end) +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/SymbolImpl/init.lua b/Client2021/ExtraContent/LuaPackages/SymbolImpl/init.lua new file mode 100644 index 0000000..6aaa8ef --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/SymbolImpl/init.lua @@ -0,0 +1 @@ +return require(script.Symbol) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/TestEZ.lua b/Client2021/ExtraContent/LuaPackages/TestEZ.lua new file mode 100644 index 0000000..d49f69c --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/TestEZ.lua @@ -0,0 +1,7 @@ +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.Packages) + +return require(CorePackages.Packages.Dev.TestEZ) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidation.lua b/Client2021/ExtraContent/LuaPackages/UGCValidation.lua new file mode 100644 index 0000000..c2295dd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidation.lua @@ -0,0 +1,9 @@ +-- Shared code used by Toolbox and RCC for validating UGC catalog uploads + +local CorePackages = game:GetService("CorePackages") + +local initify = require(CorePackages.initify) + +initify(CorePackages.UGCValidationImpl) + +return require(CorePackages.UGCValidationImpl) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/.robloxrc b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/.robloxrc new file mode 100644 index 0000000..81a5da6 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/.robloxrc @@ -0,0 +1,12 @@ +{ + "language": { + "mode": "nonstrict" + }, + "lint": { + "LocalShadow": "fatal", + "LocalUnused": "fatal", + "ImportUnused": "fatal", + "DeprecatedGlobal": "fatal", + "ImplicitReturn": "fatal" + } +} \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/Constants.lua b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/Constants.lua new file mode 100644 index 0000000..685d103 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/Constants.lua @@ -0,0 +1,247 @@ +local CorePackages = game:GetService("CorePackages") + +local Cryo = require(CorePackages.Cryo) + +-- switch this to Cryo.List.toSet when available +local function convertArrayToTable(array) + local result = {} + for _, v in pairs(array) do + result[v] = true + end + return result +end + +local Constants = {} + +Constants.MAX_HAT_TRIANGLES = 4000 + +Constants.MAX_TEXTURE_SIZE = 256 + +Constants.MATERIAL_WHITELIST = convertArrayToTable({ + Enum.Material.Plastic, +}) + +Constants.BANNED_CLASS_NAMES = { + "Script", + "LocalScript", + "ModuleScript", + "ParticleEmitter", + "Fire", + "Smoke", + "Sparkles", +} + +Constants.R6_BODY_PARTS = { + "Torso", + "Left Leg", + "Right Leg", + "Left Arm", + "Right Arm", +} + +Constants.R15_BODY_PARTS = { + "UpperTorso", + "LowerTorso", + + "LeftUpperLeg", + "LeftLowerLeg", + "LeftFoot", + + "RightUpperLeg", + "RightLowerLeg", + "RightFoot", + + "LeftUpperArm", + "LeftLowerArm", + "LeftHand", + + "RightUpperArm", + "RightLowerArm", + "RightHand", +} + +Constants.EXTRA_BANNED_NAMES = { + "Head", + "HumanoidRootPart", + "Humanoid", +} + +if game:GetFastFlag("UGCExtraBannedNames") then + local extraBannedNames = { + "Body Colors", + "Shirt Graphic", + "Shirt", + "Pants", + "Health", + "Animate", + } + for _, name in ipairs(extraBannedNames) do + table.insert(Constants.EXTRA_BANNED_NAMES, name) + end +end + +Constants.BANNED_NAMES = convertArrayToTable(Cryo.Dictionary.join( + Constants.R6_BODY_PARTS, + Constants.R15_BODY_PARTS, + Constants.EXTRA_BANNED_NAMES +)) + +Constants.ASSET_STATUS = { + UNKNOWN = "Unknown", + REVIEW_PENDING = "ReviewPending", + MODERATED = "Moderated", +} + +-- https://confluence.rbx.com/display/AVATAR/UGC+Accessory+Max+Sizes +-- Measurements are doubled to account full size +-- boundsOffset is used when measurements are non-symmetrical +-- i.e. WaistAccessory is 3 behind, 2.5 front +Constants.ASSET_TYPE_INFO = {} + +Constants.ASSET_TYPE_INFO[Enum.AssetType.Hat] = { + attachmentNames = { "HatAttachment" }, + bounds = { + HatAttachment = { + size = Vector3.new(3, 4, 3), + }, + }, +} + +Constants.ASSET_TYPE_INFO[Enum.AssetType.HairAccessory] = { + attachmentNames = { "HairAttachment" }, + bounds = { + HairAttachment = { + size = Vector3.new(3, 5, 3.5), + offset = Vector3.new(0, -0.5, 0.25), + }, + }, +} + +local FACE_BOUNDS = { size = Vector3.new(3, 2, 2) } +Constants.ASSET_TYPE_INFO[Enum.AssetType.FaceAccessory] = { + attachmentNames = { "FaceFrontAttachment", "FaceCenterAttachment" }, + bounds = { + FaceFrontAttachment = FACE_BOUNDS, + FaceCenterAttachment = FACE_BOUNDS, + }, +} + +Constants.ASSET_TYPE_INFO[Enum.AssetType.NeckAccessory] = { + attachmentNames = { "NeckAttachment" }, + bounds = { + NeckAttachment = { size = Vector3.new(3, 3, 2) }, + }, +} + +local SHOULDER_BOUNDS = { size = Vector3.new(3, 3, 3) } +Constants.ASSET_TYPE_INFO[Enum.AssetType.ShoulderAccessory] = { + attachmentNames = { + "NeckAttachment", + "LeftCollarAttachment", + "RightCollarAttachment", + "LeftShoulderAttachment", + "RightShoulderAttachment", + }, + bounds = { + NeckAttachment = { size = Vector3.new(7, 3, 3) }, + LeftCollarAttachment = SHOULDER_BOUNDS, + RightCollarAttachment = SHOULDER_BOUNDS, + LeftShoulderAttachment = SHOULDER_BOUNDS, + RightShoulderAttachment = SHOULDER_BOUNDS, + }, +} + +Constants.ASSET_TYPE_INFO[Enum.AssetType.FrontAccessory] = { + attachmentNames = { "BodyFrontAttachment" }, + bounds = { + BodyFrontAttachment = { size = Vector3.new(3, 3, 3) }, + }, +} + +Constants.ASSET_TYPE_INFO[Enum.AssetType.BackAccessory] = { + attachmentNames = { "BodyBackAttachment" }, + bounds = { + BodyBackAttachment = { + size = Vector3.new(10, 7, 4.5), + offset = Vector3.new(0, 0, 0.75), + }, + }, +} + +local WAIST_BOUNDS = { + size = Vector3.new(4, 3.5, 7), + offset = Vector3.new(0, -0.25, 0), +} +Constants.ASSET_TYPE_INFO[Enum.AssetType.WaistAccessory] = { + attachmentNames = { + "WaistBackAttachment", + "WaistFrontAttachment", + "WaistCenterAttachment", + }, + bounds = { + WaistBackAttachment = WAIST_BOUNDS, + WaistFrontAttachment = WAIST_BOUNDS, + WaistCenterAttachment = WAIST_BOUNDS, + } +} + +Constants.PROPERTIES = { + Instance = { + Archivable = true, + }, + Attachment = { + Visible = false, + }, + SpecialMesh = { + MeshType = Enum.MeshType.FileMesh, + Offset = Vector3.new(0, 0, 0), + VertexColor = Vector3.new(1, 1, 1), + }, + BasePart = { + Anchored = false, + Color = BrickColor.new("Medium stone grey").Color, -- luacheck: ignore BrickColor + CollisionGroupId = 0, -- collision groups can change by place + CustomPhysicalProperties = Cryo.None, -- ensure CustomPhysicalProperties is _not_ defined + Elasticity = 0.5, + Friction = 0.3, + LocalTransparencyModifier = 0, + Massless = false, -- this is already done by accessories internally + Reflectance = 0, + RootPriority = 0, + RotVelocity = Vector3.new(0, 0, 0), + Transparency = 0, + Velocity = Vector3.new(0, 0, 0), + + -- surface properties + BackParamA = -0.5, + BackParamB = 0.5, + BackSurfaceInput = Enum.InputType.NoInput, + BottomParamA = -0.5, + BottomParamB = 0.5, + BottomSurfaceInput = Enum.InputType.NoInput, + FrontParamA = -0.5, + FrontParamB = 0.5, + FrontSurfaceInput = Enum.InputType.NoInput, + LeftParamA = -0.5, + LeftParamB = 0.5, + LeftSurfaceInput = Enum.InputType.NoInput, + RightParamA = -0.5, + RightParamB = 0.5, + RightSurfaceInput = Enum.InputType.NoInput, + TopParamA = -0.5, + TopParamB = 0.5, + TopSurfaceInput = Enum.InputType.NoInput, + + BackSurface = Enum.SurfaceType.Smooth, + BottomSurface = Enum.SurfaceType.Smooth, + FrontSurface = Enum.SurfaceType.Smooth, + LeftSurface = Enum.SurfaceType.Smooth, + RightSurface = Enum.SurfaceType.Smooth, + TopSurface = Enum.SurfaceType.Smooth, + }, + Part = { + Shape = Enum.PartType.Block, + }, +} + +return Constants diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/init.lua b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/init.lua new file mode 100644 index 0000000..9421ccc --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/init.lua @@ -0,0 +1,96 @@ +game:DefineFastFlag("UGCValidateMeshBounds", false) +game:DefineFastFlag("UGCValidateHandleSize", false) +game:DefineFastFlag("UGCExtraBannedNames", false) + +local root = script + +local validateInstanceTree = require(root.validation.validateInstanceTree) +local validateMeshTriangles = require(root.validation.validateMeshTriangles) +local validateModeration = require(root.validation.validateModeration) +local validateMaterials = require(root.validation.validateMaterials) +local validateTags = require(root.validation.validateTags) +local validateMeshBounds = require(root.validation.validateMeshBounds) +local validateTextureSize = require(root.validation.validateTextureSize) +local validateHandleSize = require(root.validation.validateHandleSize) +local validateProperties = require(root.validation.validateProperties) + +local function validateInternal(isAsync, instances, assetTypeEnum, noModeration) + -- validate that only one instance was selected + if #instances == 0 then + return false, { "No instances selected" } + elseif #instances > 1 then + return false, { "More than one instance selected" } + end + + local instance = instances[1] + + local success, reasons + + success, reasons = validateInstanceTree(instance, assetTypeEnum) + if not success then + return false, reasons + end + + success, reasons = validateMaterials(instance) + if not success then + return false, reasons + end + + success, reasons = validateProperties(instance) + if not success then + return false, reasons + end + + success, reasons = validateTags(instance) + if not success then + return false, reasons + end + + if game:GetFastFlag("UGCValidateMeshBounds") then + success, reasons = validateMeshBounds(isAsync, instance, assetTypeEnum) + if not success then + return false, reasons + end + end + + success, reasons = validateTextureSize(isAsync, instance) + if not success then + return false, reasons + end + + if game:GetFastFlag("UGCValidateHandleSize") then + success, reasons = validateHandleSize(isAsync, instance) + if not success then + return false, reasons + end + end + + success, reasons = validateMeshTriangles(isAsync, instance) + if not success then + return false, reasons + end + + if not noModeration then + success, reasons = validateModeration(isAsync, instance) + if not success then + return false, reasons + end + end + + return true +end + +local UGCValidation = {} + +function UGCValidation.validate(instances, assetTypeEnum, noModeration) + local success, reasons = validateInternal(--[[ isAsync = ]] false, instances, assetTypeEnum, noModeration) + return success, reasons +end + +function UGCValidation.validateAsync(instances, assetTypeEnum, callback, noModeration) + coroutine.wrap(function() + callback(validateInternal(--[[ isAsync = ]] true, instances, assetTypeEnum, noModeration)) + end)() +end + +return UGCValidation diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/util/createAccessorySchema.lua b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/util/createAccessorySchema.lua new file mode 100644 index 0000000..ef05835 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/util/createAccessorySchema.lua @@ -0,0 +1,47 @@ +local function createAccessorySchema(attachmentName) + assert(attachmentName, "attachmentName cannot be nil") + return { + ClassName = "Accessory", + _children = { + { + Name = "ThumbnailConfiguration", + ClassName = "Configuration", + _optional = true, + _children = { + { + Name = "ThumbnailCameraTarget", + ClassName = "ObjectValue", + }, + { + Name = "ThumbnailCameraValue", + ClassName = "CFrameValue", + }, + }, + }, + { + Name = "Handle", + ClassName = "Part", + _children = { + { + Name = attachmentName, + ClassName = "Attachment", + }, + { + ClassName = "SpecialMesh", + }, + { + ClassName = "StringValue", + Name = "AvatarPartScaleType", + _optional = true, + }, + { + ClassName = "TouchTransmitter", + _optional = true, + }, + } + }, + }, + } +end + +return createAccessorySchema diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/util/getAssetCreationDetails.lua b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/util/getAssetCreationDetails.lua new file mode 100644 index 0000000..4fe76da --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/util/getAssetCreationDetails.lua @@ -0,0 +1,56 @@ +local HttpService = game:GetService("HttpService") +local HttpRbxApiService = game:GetService("HttpRbxApiService") +local ContentProvider = game:GetService("ContentProvider") + +local function getBaseDomain() + local baseUrl = ContentProvider.BaseUrl + if string.sub(baseUrl, #baseUrl) ~= "/" then + baseUrl = baseUrl .. "/" + end + local _, schemeEnd = string.find(baseUrl, "://") + local _, prefixEnd = string.find(baseUrl, "%.", schemeEnd + 1) + return string.sub(baseUrl, prefixEnd + 1) +end + +local MAX_RETRIES = 5 + +local function requestAndRetry(apiUrl, data, attempt) + if attempt == nil then + attempt = 0 + end + + local success, response = pcall(function() + return HttpRbxApiService:PostAsyncFullUrl(apiUrl, data) + end) + + if success then + return true, response + elseif attempt >= MAX_RETRIES then + return false, response + else + local timeToWait = 2^(attempt - 1) + wait(timeToWait) + return requestAndRetry(apiUrl, data, attempt + 1) + end +end + +local BASE_DOMAIN = getBaseDomain() +local ITEM_CONFIGURATION_URL = string.format("https://itemconfiguration.%s", BASE_DOMAIN) +local GET_ASSET_CREATION_DETAILS_URL = ITEM_CONFIGURATION_URL .. "v1/creations/get-asset-details" + +local function getAssetCreationDetails(isAsync, assetIds) + local success, response = requestAndRetry( + GET_ASSET_CREATION_DETAILS_URL, + HttpService:JSONEncode({ assetIds = assetIds }) + ) + + -- TODO: isAsync + + if success then + return true, HttpService:JSONDecode(response) + else + return false, response + end +end + +return getAssetCreationDetails \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/util/validateWithSchema.lua b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/util/validateWithSchema.lua new file mode 100644 index 0000000..0f8b7bf --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/util/validateWithSchema.lua @@ -0,0 +1,100 @@ +local function checkName(nameList, instanceName) + if type(nameList) == "table" then + for _, name in pairs(nameList) do + if name == instanceName then + return true + end + end + elseif type(nameList) == "string" then + return nameList == instanceName + end + return false +end + +local function getReadableName(nameList) + if type(nameList) == "table" then + return table.concat(nameList, " or ") + elseif type(nameList) == "string" then + return nameList + end + return "*" +end + +local function validateWithSchemaHelper(schema, instance, authorizedSet) + -- validate + if instance.ClassName ~= schema.ClassName or (schema.Name ~= nil and not checkName(schema.Name, instance.Name)) then + return { success = false } + end + + -- validate children + if schema._children then + for _, childSchema in pairs(schema._children) do + local found = false + local mostRecentFailure + for _, child in pairs(instance:GetChildren()) do + local result = validateWithSchemaHelper(childSchema, child, authorizedSet) + if result.success then + found = true + break + elseif result.message then + mostRecentFailure = result + end + end + if not found and not childSchema._optional then + if mostRecentFailure then + return mostRecentFailure + else + return { + success = false, + message = "Could not find a " + .. childSchema.ClassName + .. " called " + .. getReadableName(childSchema.Name) + .. " inside " + .. instance.Name + } + end + end + end + end + + authorizedSet[instance] = true + + return { success = true } +end + +local function validateWithSchema(schema, instance) + + if instance.ClassName ~= schema.ClassName or (schema.Name ~= nil and schema.Name ~= instance.Name) then + return { + success = false, + message = "Expected top-level instance to be a " .. schema.ClassName, + } + end + + local authorizedSet = {} + local result = validateWithSchemaHelper(schema, instance, authorizedSet) + + if not result.success then + return result + end + + -- check for extra descendants + local unauthorizedDescendantPaths = {} + for _, descendant in pairs(instance:GetDescendants()) do + if authorizedSet[descendant] == nil then + unauthorizedDescendantPaths[#unauthorizedDescendantPaths + 1] = descendant:GetFullName() + end + end + + if #unauthorizedDescendantPaths > 0 then + return { + success = false, + message = "Unexpected Descendants:\n" .. table.concat(unauthorizedDescendantPaths, "\n") + } + end + + return { success = true } +end + +return validateWithSchema \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/util/valueToString.lua b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/util/valueToString.lua new file mode 100644 index 0000000..29aed14 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/util/valueToString.lua @@ -0,0 +1,33 @@ +local CorePackages = game:GetService("CorePackages") + +local Cryo = require(CorePackages.Cryo) + +local function round(num, numDecimalPlaces) + local mult = 10^(numDecimalPlaces or 0) + return math.floor(num * mult + 0.5) / mult +end + +local function valueToString(propValue) + local valueType = typeof(propValue) + if propValue == Cryo.None then + return "not defined" + elseif valueType == "Vector3" then + return string.format( + "%d, %d, %d", + round(propValue.X, 2), + round(propValue.Y, 2), + round(propValue.Z, 2) + ) + elseif valueType == "Color3" then + return string.format( + "%d, %d, %d", + math.floor(propValue.r * 255), + math.floor(propValue.g * 255), + math.floor(propValue.b * 255) + ) + else + return tostring(propValue) + end +end + +return valueToString diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateHandleSize.lua b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateHandleSize.lua new file mode 100644 index 0000000..a2ec741 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateHandleSize.lua @@ -0,0 +1,59 @@ +local UGCValidationService = game:GetService("UGCValidationService") + +local root = script.Parent.Parent +local valueToString = require(root.util.valueToString) + +local MARGIN_OF_ERROR = 0.1 + +local function validateHandleSize(isAsync, instance) + -- these are guaranteed to exist thanks to validateInstanceTree being called beforehand + local handle = instance.Handle + local mesh = handle:FindFirstChildOfClass("SpecialMesh") + + local success, verts = pcall(function() + if isAsync then + return UGCValidationService:GetMeshVerts(mesh.MeshId) + else + return UGCValidationService:GetMeshVertsSync(mesh.MeshId) + end + end) + + if not success then + return false, { "Failed to read mesh" } + end + + local minX, maxX = math.huge, 0 + local minY, maxY = math.huge, 0 + local minZ, maxZ = math.huge, 0 + + for i = 1, #verts do + local vert = verts[i] * mesh.Scale + minX = math.min(minX, vert.X) + minY = math.min(minY, vert.Y) + minZ = math.min(minZ, vert.Z) + maxX = math.max(maxX, vert.X) + maxY = math.max(maxY, vert.Y) + maxZ = math.max(maxZ, vert.Z) + end + + local meshSize = Vector3.new( + maxX - minX, + maxY - minY, + maxZ - minZ + ) + + -- allow handle.Size to be within MARGIN_OF_ERROR of meshSize or larger + -- this is necessary since we're comparing floats + -- the size only needs to be a rough equivalent for thumbnailing + if handle.Size.X + MARGIN_OF_ERROR < meshSize.X + or handle.Size.Y + MARGIN_OF_ERROR < meshSize.Y + or handle.Size.Z + MARGIN_OF_ERROR < meshSize.Z then + return false, { + string.format("Accessory Handle size should be at least the size of the mesh ( %s )", valueToString(meshSize)) + } + end + + return true +end + +return validateHandleSize \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateInstanceTree.lua b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateInstanceTree.lua new file mode 100644 index 0000000..82b3ca8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateInstanceTree.lua @@ -0,0 +1,53 @@ +local root = script.Parent.Parent + +local Constants = require(root.Constants) +local createAccessorySchema = require(root.util.createAccessorySchema) +local validateWithSchema = require(root.util.validateWithSchema) + +-- validates a given instance based on a schema +local function validateInstanceTree(instance, assetTypeEnum) + local assetInfo = Constants.ASSET_TYPE_INFO[assetTypeEnum] + if not assetInfo then + return false, { "Could not validate" } + end + + local schema = createAccessorySchema(assetInfo.attachmentNames) + + -- validate using hat schema + local validationResult = validateWithSchema(schema, instance) + if validationResult.success == false then + return false, { validationResult.message } + end + + -- fallback case for if validateWithSchema breaks + local invalidDescendantsReasons = {} + if Constants.BANNED_NAMES[instance.Name] then + local reason = string.format("%s has an invalid name", instance:GetFullName()) + invalidDescendantsReasons[#invalidDescendantsReasons + 1] = reason + end + + for _, descendant in pairs(instance:GetDescendants()) do + for _, className in pairs(Constants.BANNED_CLASS_NAMES) do + if descendant:IsA(className) then + local reason = string.format( + "%s is of type %s which is not allowed", + descendant:GetFullName(), + className + ) + invalidDescendantsReasons[#invalidDescendantsReasons + 1] = reason + end + end + if Constants.BANNED_NAMES[descendant.Name] then + local reason = string.format("%s has an invalid name", descendant:GetFullName()) + invalidDescendantsReasons[#invalidDescendantsReasons + 1] = reason + end + end + + if #invalidDescendantsReasons > 0 then + return false, invalidDescendantsReasons + end + + return true +end + +return validateInstanceTree \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMaterials.lua b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMaterials.lua new file mode 100644 index 0000000..81408d4 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMaterials.lua @@ -0,0 +1,30 @@ +local root = script.Parent.Parent + +local Constants = require(root.Constants) + +-- ensures no descendant of instance has a material that does not exist in Constants.MATERIAL_WHITELIST +local function validateMaterials(instance) + local materialFailures = {} + for _, descendant in pairs(instance:GetDescendants()) do + if descendant:IsA("BasePart") and not Constants.MATERIAL_WHITELIST[descendant.Material] then + materialFailures[#materialFailures + 1] = descendant:GetFullName() + end + end + if #materialFailures > 0 then + local reasons = {} + local acceptedMaterialNames = {} + for material in pairs(Constants.MATERIAL_WHITELIST) do + acceptedMaterialNames[#acceptedMaterialNames + 1] = material.Name + end + reasons[#reasons + 1] = "Invalid materials for" + for _, name in pairs(materialFailures) do + reasons[#reasons + 1] = name + end + reasons[#reasons + 1] = "Accepted materials are " .. table.concat(acceptedMaterialNames, ", ") + return false, reasons + end + + return true +end + +return validateMaterials \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMeshBounds.lua b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMeshBounds.lua new file mode 100644 index 0000000..4d7f5f8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMeshBounds.lua @@ -0,0 +1,72 @@ +local UGCValidationService = game:GetService("UGCValidationService") + +local root = script.Parent.Parent + +local Constants = require(root.Constants) + +local DEFAULT_OFFSET = Vector3.new(0, 0, 0) + +local function pointInBounds(worldPos, boundsCF, boundsSize) + local objectPos = boundsCF:pointToObjectSpace(worldPos) + return objectPos.X >= -boundsSize.X/2 + and objectPos.X <= boundsSize.X/2 + and objectPos.Y >= -boundsSize.Y/2 + and objectPos.Y <= boundsSize.Y/2 + and objectPos.Z >= -boundsSize.Z/2 + and objectPos.Z <= boundsSize.Z/2 +end + +local function getAttachment(parent, names) + for _, name in pairs(names) do + local result = parent:FindFirstChild(name) + if result then + return result + end + end + return nil +end + +local function validateMeshBounds(isAsync, instance, assetTypeEnum) + local assetInfo = Constants.ASSET_TYPE_INFO[assetTypeEnum] + + -- these are guaranteed to exist thanks to validateInstanceTree being called beforehand + local handle = instance.Handle + local mesh = handle:FindFirstChildOfClass("SpecialMesh") + local attachment = getAttachment(handle, assetInfo.attachmentNames) + + if mesh.MeshId == "" then + return false, { "Mesh must contain valid MeshId" } + end + + local success, verts = pcall(function() + if isAsync then + return UGCValidationService:GetMeshVerts(mesh.MeshId) + else + return UGCValidationService:GetMeshVertsSync(mesh.MeshId) + end + end) + + if not success then + return false, { "Failed to read mesh" } + end + + local boundsInfo = assert(assetInfo.bounds[attachment.Name], "Could not find bounds for " .. attachment.Name) + local boundsSize = boundsInfo.size + local boundsOffset = boundsInfo.offset or DEFAULT_OFFSET + local boundsCF = handle.CFrame * attachment.CFrame * CFrame.new(boundsOffset) + + for _, vertPos in pairs(verts) do + local worldPos = handle.CFrame:pointToWorldSpace(vertPos * mesh.Scale) + if not pointInBounds(worldPos, boundsCF, boundsSize) then + return false, { + "Mesh is too large!", + string.format("Max size for type %s is ( %s )", assetTypeEnum.Name, tostring(boundsSize)), + "Use SpecialMesh.Scale if needed" + } + end + end + + return true +end + +return validateMeshBounds diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMeshTriangles.lua b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMeshTriangles.lua new file mode 100644 index 0000000..eb2a47f --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateMeshTriangles.lua @@ -0,0 +1,40 @@ +local UGCValidationService = game:GetService("UGCValidationService") + +local root = script.Parent.Parent + +local Constants = require(root.Constants) + +-- ensures accessory mesh does not have more triangles than Constants.MAX_HAT_TRIANGLES +local function validateMeshTriangles(isAsync, instance) + -- check mesh triangles + -- this is guaranteed to exist thanks to validateInstanceTree being called beforehand + local mesh = instance.Handle:FindFirstChildOfClass("SpecialMesh") + + if mesh.MeshId == "" then + return false, { "Mesh must contain valid MeshId" } + end + + local success, triangles = pcall(function() + if isAsync then + return UGCValidationService:GetMeshTriCount(mesh.MeshId) + else + return UGCValidationService:GetMeshTriCountSync(mesh.MeshId) + end + end) + + if not success then + return false, { "Failed to load mesh data" } + elseif triangles > Constants.MAX_HAT_TRIANGLES then + return false, { + string.format( + "Mesh has %d triangles, but the limit is %d", + triangles, + Constants.MAX_HAT_TRIANGLES + ) + } + end + + return true +end + +return validateMeshTriangles \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateModeration.lua b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateModeration.lua new file mode 100644 index 0000000..aa3bea8 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateModeration.lua @@ -0,0 +1,97 @@ +local root = script.Parent.Parent + +local Constants = require(root.Constants) +local getAssetCreationDetails = require(root.util.getAssetCreationDetails) + +local function parseContentId(contentIds, contentIdMap, object, fieldName) + local contentId = object[fieldName] + + -- map to ending digits + -- rbxassetid://1234 -> 1234 + -- http://www.roblox.com/asset/?id=1234 -> 1234 + local id = tonumber(string.match(contentId, "%d+$")) + if id == nil then + return false, { + "Could not parse ContentId", + contentId, + } + end + contentIdMap[id] = { + fieldName = fieldName, + instance = object, + } + table.insert(contentIds, id) + + return true +end + +local function parseDescendantContentIds(contentIds, contentIdMap, object) + for _, descendant in pairs(object:GetDescendants()) do + if descendant:IsA("SpecialMesh") then + local success, reasons + success, reasons = parseContentId(contentIds, contentIdMap, descendant, "MeshId") + if not success then + return false, reasons + end + success, reasons = parseContentId(contentIds, contentIdMap, descendant, "TextureId") + if not success then + return false, reasons + end + end + end + + return true +end + +-- ensures accessory content ids have all passed moderation review +local function validateModeration(isAsync, instance) + local contentIdMap = {} + local contentIds = {} + + local parseSuccess, parseReasons = parseDescendantContentIds(contentIds, contentIdMap, instance) + if not parseSuccess then + return false, parseReasons + end + + local moderatedIds = {} + + local success, response = getAssetCreationDetails(isAsync, contentIds) + + if not success or #response ~= #contentIds then + return false, { "Could not fetch details for assets" } + end + + for _, details in pairs(response) do + if details.status == Constants.ASSET_STATUS.UNKNOWN + or details.status == Constants.ASSET_STATUS.REVIEW_PENDING + or details.status == Constants.ASSET_STATUS.MODERATED + then + table.insert(moderatedIds, details.assetId) + end + end + + if #moderatedIds > 0 then + local moderationMessages = {} + for idx, id in pairs(moderatedIds) do + local mapped = contentIdMap[id] + if mapped then + moderationMessages[idx] = string.format( + "%s.%s ( %s )", + mapped.instance:GetFullName(), + mapped.fieldName, + id + ) + else + moderationMessages[idx] = id + end + end + return false, { + "The following asset IDs have not passed moderation:", + unpack(moderationMessages), + } + end + + return true +end + +return validateModeration diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateProperties.lua b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateProperties.lua new file mode 100644 index 0000000..cd87950 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateProperties.lua @@ -0,0 +1,71 @@ +local CorePackages = game:GetService("CorePackages") + +local Cryo = require(CorePackages.Cryo) + +local root = script.Parent.Parent + +local Constants = require(root.Constants) +local valueToString = require(root.util.valueToString) + +local EPSILON = 1e-5 + +local function floatEq(a, b) + return math.abs(a - b) <= EPSILON +end + +local function v3FloatEq(a, b) + return floatEq(a.X, b.X) and floatEq(a.Y, b.Y) and floatEq(a.Z, b.Z) +end + +local function c3FloatEq(a, b) + return floatEq(a.r, b.r) and floatEq(a.g, b.g) and floatEq(a.b, b.b) +end + +local function propEq(propValue, expectedValue) + local valueType = typeof(expectedValue) + if expectedValue == Cryo.None then + return propValue == nil + elseif valueType == "number" then + return floatEq(propValue, expectedValue) + elseif valueType == "Vector3" then + return v3FloatEq(propValue, expectedValue) + elseif valueType == "Color3" then + return c3FloatEq(propValue, expectedValue) + else + return propValue == expectedValue + end +end + +local function validateProperties(instance) + + -- full tree of instance + descendants + local objects = instance:GetDescendants() + table.insert(objects, instance) + + for _, object in pairs(objects) do + for className, properties in pairs(Constants.PROPERTIES) do + if object:IsA(className) then + for propName, expectedValue in pairs(properties) do + -- ensure property exists first + local propExists, propValue = pcall(function() return object[propName] end) + + if not propExists then + return false, { + string.format("Property %s does not exist on type %s", propName, object.ClassName) + } + end + + if not propEq(propValue, expectedValue) then + return false, { + string.format("Expected %s.%s to be %s", object:GetFullName(), propName, valueToString(expectedValue)) + } + end + end + end + end + end + + return true +end + +return validateProperties \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateTags.lua b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateTags.lua new file mode 100644 index 0000000..50c8c00 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateTags.lua @@ -0,0 +1,25 @@ +local CollectionService = game:GetService("CollectionService") + +local function validateTags(instance) + local objects = instance:GetDescendants() + table.insert(objects, instance) + + local hasTags = {} + for _, obj in pairs(objects) do + if #CollectionService:GetTags(obj) > 0 then + table.insert(hasTags, obj) + end + end + + if #hasTags > 0 then + local reasons = { "The following objects contain CollectionService tags:" } + for _, obj in pairs(hasTags) do + table.insert(reasons, obj:GetFullName()) + end + return false, reasons + end + + return true +end + +return validateTags \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateTextureSize.lua b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateTextureSize.lua new file mode 100644 index 0000000..aa50c53 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UGCValidationImpl/validation/validateTextureSize.lua @@ -0,0 +1,40 @@ +local UGCValidationService = game:GetService("UGCValidationService") + +local root = script.Parent.Parent + +local Constants = require(root.Constants) + +local function validateTextureSize(isAsync, instance) + -- this is guaranteed to exist thanks to validateInstanceTree being called beforehand + local mesh = instance.Handle:FindFirstChildOfClass("SpecialMesh") + + if mesh.TextureId == "" then + return false, { "Mesh must contain valid TextureId" } + end + + local success, imageSize = pcall(function() + if isAsync then + return UGCValidationService:GetTextureSize(mesh.TextureId) + else + return UGCValidationService:GetTextureSizeSync(mesh.TextureId) + end + end) + + if not success then + return false, { "Failed to load texture data", imageSize } + elseif imageSize.X > Constants.MAX_TEXTURE_SIZE or imageSize.Y > Constants.MAX_TEXTURE_SIZE then + return false, { + string.format( + "Texture size is %dx%d px, but the limit is %dx%d px", + imageSize.X, + imageSize.Y, + Constants.MAX_TEXTURE_SIZE, + Constants.MAX_TEXTURE_SIZE + ) + } + end + + return true +end + +return validateTextureSize \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UIBlox.lua b/Client2021/ExtraContent/LuaPackages/UIBlox.lua new file mode 100644 index 0000000..adc8236 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UIBlox.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +local UIBlox = require(CorePackages.Packages.UIBlox) + +return UIBlox \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UIBloxFlags/FFlagUIBloxSlidersFilterOldTouchInputs.lua b/Client2021/ExtraContent/LuaPackages/UIBloxFlags/FFlagUIBloxSlidersFilterOldTouchInputs.lua new file mode 100644 index 0000000..48f979e --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UIBloxFlags/FFlagUIBloxSlidersFilterOldTouchInputs.lua @@ -0,0 +1 @@ +return game:DefineFastFlag("UIBloxSlidersFilterOldTouchInputs", false) \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaAppUseNewUIBloxRoundedCorners.lua b/Client2021/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaAppUseNewUIBloxRoundedCorners.lua new file mode 100644 index 0000000..aeb0aa0 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaAppUseNewUIBloxRoundedCorners.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("LuaAppUseNewUIBloxRoundedCorners", false) + +return function() + return game:GetFastFlag("LuaAppUseNewUIBloxRoundedCorners") +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaFixItemTilePremiumIcon.lua b/Client2021/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaFixItemTilePremiumIcon.lua new file mode 100644 index 0000000..654c1f5 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaFixItemTilePremiumIcon.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("LuaPremiumCatalogTileFix", false) + +return function() + return game:GetFastFlag("LuaPremiumCatalogTileFix") +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaUIBloxGamepadSupport.lua b/Client2021/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaUIBloxGamepadSupport.lua new file mode 100644 index 0000000..93cbddd --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UIBloxFlags/GetFFlagLuaUIBloxGamepadSupport.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("LuaUIBloxGamepadSupport2", false) + +return function() + return game:GetFastFlag("LuaUIBloxGamepadSupport2") +end \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/UIBloxUniversalAppConfig.lua b/Client2021/ExtraContent/LuaPackages/UIBloxUniversalAppConfig.lua new file mode 100644 index 0000000..e9ebbb9 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UIBloxUniversalAppConfig.lua @@ -0,0 +1,15 @@ +-- See https://confluence.rbx.com/display/MOBAPP/UIBlox+Flagging +-- for more info on how to add values here +local CorePackages = game:GetService("CorePackages") + +local GetFFlagLuaAppUseNewUIBloxRoundedCorners = require(CorePackages.UIBloxFlags.GetFFlagLuaAppUseNewUIBloxRoundedCorners) +local GetFFlagLuaFixItemTilePremiumIcon = require(CorePackages.UIBloxFlags.GetFFlagLuaFixItemTilePremiumIcon) +local GetFFlagLuaUIBloxGamepadSupport = require(CorePackages.UIBloxFlags.GetFFlagLuaUIBloxGamepadSupport) +local FFlagUIBloxSlidersFilterOldTouchInputs = require(CorePackages.UIBloxFlags.FFlagUIBloxSlidersFilterOldTouchInputs) + +return { + useNewUICornerRoundedCorners = GetFFlagLuaAppUseNewUIBloxRoundedCorners(), + fixItemTilePremiumIcon = GetFFlagLuaFixItemTilePremiumIcon(), + enableExperimentalGamepadSupport = GetFFlagLuaUIBloxGamepadSupport(), + genericSliderFilterOldTouchInputs = FFlagUIBloxSlidersFilterOldTouchInputs, +} diff --git a/Client2021/ExtraContent/LuaPackages/UniversalAppPolicy.lua b/Client2021/ExtraContent/LuaPackages/UniversalAppPolicy.lua new file mode 100644 index 0000000..60bf2ce --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/UniversalAppPolicy.lua @@ -0,0 +1,11 @@ +local Modules = game:GetService("CoreGui").RobloxGui.Modules +local CorePackages = game:GetService("CorePackages") +local PolicyProvider = require(CorePackages.PolicyProvider) + +local Logger = require(Modules.LuaApp.Logger) +PolicyProvider.Logger:setParent(Logger) + +local readMemStorageForAppLaunch = PolicyProvider.GetPolicyImplementations.MemStorageService("app-policy") +local UniversalAppPolicyProvider = PolicyProvider.withGetPolicyImplementation(readMemStorageForAppLaunch) + +return UniversalAppPolicyProvider diff --git a/Client2021/ExtraContent/LuaPackages/enumerate.lua b/Client2021/ExtraContent/LuaPackages/enumerate.lua new file mode 100644 index 0000000..0468de1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/enumerate.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.enumerate) diff --git a/Client2021/ExtraContent/LuaPackages/initify.lua b/Client2021/ExtraContent/LuaPackages/initify.lua new file mode 100644 index 0000000..f6334a1 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/initify.lua @@ -0,0 +1,40 @@ +--[[ + Restructures a tree of ModuleScript objects to emulate the behavior of stock + Lua and Rojo's `init.lua` mechanism, which essentially lets you load folders + as modules. + + A file structure like this: + + foo (directory) + `-- bar (directory) + `-- init.lua (file) + + Is turned into: + + foo (Folder) + `-- bar (ModuleScript) +]] + +local function initify(rbx) + local init = rbx:FindFirstChild("init") + + if init then + init.Name = rbx.Name + init.Parent = rbx.Parent + + for _, child in ipairs(rbx:GetChildren()) do + child.Parent = init + end + + rbx:Destroy() + rbx = init + end + + for _, child in ipairs(rbx:GetChildren()) do + initify(child) + end + + return rbx +end + +return initify \ No newline at end of file diff --git a/Client2021/ExtraContent/LuaPackages/rotriever.lock b/Client2021/ExtraContent/LuaPackages/rotriever.lock new file mode 100644 index 0000000..8f73bdb --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/rotriever.lock @@ -0,0 +1,439 @@ +# This file is automatically @generated by rotriever. +# It is not intended for manual editing. +lockfile_format_version = 4 +proxy = "https://github.com/roblox/rotriever-proxy-index" + +[[package]] +name = "AvatarExperienceDeps" +version = "0.0.1" +commit = "5a31b41867d2c5ebf0b5670d9be27f3edd403545" +source = "git+https://github.com/roblox/avatar-experience-deps#master" +dependencies = ["RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components"] + +[[package]] +name = "CorePackages" +version = "0.1.0" +dependencies = [ + "AvatarExperienceDeps AvatarExperienceDeps 5a31b418 git+https://github.com/roblox/avatar-experience-deps#master", + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "DeveloperTools roblox/developer-tools 0.1.4 url+https://github.com/roblox/developer-tools", + "InGameMenuDependencies InGameMenuDependencies aecc56c1 git+https://github.com/roblox/in-game-menu-dependencies#master", + "LuaChatDeps LuaChatDeps f16c0046 git+https://github.com/roblox/lua-chat-deps#master", + "LuaDiscussionsDeps LuaDiscussionsDeps 149f4695 git+https://github.com/roblox/lua-discussions-deps#master", + "LuaSocialLibrariesDeps LuaSocialLibrariesDeps b04c5805 git+https://github.com/roblox/lua-social-libraries-deps#master", + "Lumberyak roblox/lumberyak 76fee23f git+https://github.com/roblox/lumberyak#master", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "PolicyProvider roblox/lua-roact-policy-provider 5f782af8 git+https://github.com/roblox/lua-roact-policy-provider#master", + "PremiumUpsellDeps PremiumUpsellDeps c51030b7 git+https://github.com/roblox/premium-upsell-deps#master", + "Promise lua-promise 1bb842c3 git+https://github.com/roblox/lua-promise#master", + "PurchasePrompt roblox/purchase-prompt 81ce762f git+https://github.com/roblox/purchasepromptscript-roact#master", + "PurchasePromptDeps roblox/purchase-prompt-deps c337b7da git+https://github.com/roblox/purchase-prompt-deps#master", + "Result roblox/lua-result c6817c84 git+https://github.com/roblox/lua-result#master", + "Rhodium roblox/rhodium 0.2.5 url+https://github.com/roblox/rhodium", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "RoactGamepad roblox/roact-gamepad 0.4.6 url+https://github.com/roblox/roact-gamepad", + "RoactNavigation roblox/roact-navigation 0.4.1 url+https://github.com/roblox/roact-navigation", + "RoactRodux roblox/roact-rodux 0.2.2 url+https://github.com/roblox/roact-rodux", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "StringUtilities roblox/string-utilities 1.0.0 url+https://github.com/roblox/string-utilities", + "TestEZ roblox/testez 0.4.0 url+https://github.com/roblox/testez", + "UIBlox UIBlox 3a82d9ed git+https://github.com/roblox/uiblox#master", + "UrlBuilder roblox/url-builder 1.0.4 url+https://github.com/roblox/url-builder", + "enumerate roblox/enumerate 1.0.0 url+https://github.com/roblox/enumerate", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", + "tutils tutils 937da4f7 git+https://github.com/roblox/tutils#master", +] + +[[package]] +name = "InGameMenuDependencies" +version = "0.1.0" +commit = "aecc56c1fa6348886a1073785dbb196b655a0a90" +source = "git+https://github.com/roblox/in-game-menu-dependencies#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "PolicyProvider roblox/lua-roact-policy-provider 5f782af8 git+https://github.com/roblox/lua-roact-policy-provider#master", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "RoactRodux roblox/roact-rodux 0.2.2 url+https://github.com/roblox/roact-rodux", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "UIBlox UIBlox 3a82d9ed git+https://github.com/roblox/uiblox#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "LuaChatDeps" +version = "0.1.3" +commit = "f16c004618db2eacb01bf85c73946d0711bfe922" +source = "git+https://github.com/roblox/lua-chat-deps#master" +dependencies = [ + "AssetCard roblox/asset-card 103b2a57 git+https://github.com/roblox/asset-card#v1.0.3", + "InfiniteScroller roblox/infinite-scroller 0.7.3 url+https://github.com/roblox/infinite-scroller", + "InfiniteScroller71 roblox/infinite-scroller 7286a922 git+https://github.com/roblox/infinite-scroller#v0.7.1", + "RoduxNetworking rodux-networking 8902a6aa git+https://github.com/roblox/rodux-networking#v1.0.2", + "UIBlox UIBlox 3a82d9ed git+https://github.com/roblox/uiblox#master", +] + +[[package]] +name = "LuaDiscussionsDeps" +version = "0.1.2" +commit = "149f4695b874c2e43760131eda24779669bb3bec" +source = "git+https://github.com/roblox/lua-discussions-deps#master" +dependencies = [ + "InfiniteScroll roblox/infinite-scroller 0.3.4 url+https://github.com/roblox/infinite-scroller", + "RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "RoactNavigation roact-navigation 0.1.1-linter-fix url+https://github.com/roblox/roact-navigation", + "RoduxNetworking rodux-networking 8902a6aa git+https://github.com/roblox/rodux-networking#v1.0.2", +] + +[[package]] +name = "LuaSocialLibrariesDeps" +version = "0.1.1" +commit = "b04c58051c6067aaceb41d062c8eb1ddffeaba0a" +source = "git+https://github.com/roblox/lua-social-libraries-deps#master" +dependencies = [ + "GenericPagination roblox/genericpagination 885168d0 git+https://github.com/roblox/genericpagination#master", + "Mock jtaylor/mock d2c4005c git+https://github.com/roblox/mock#master", + "RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", +] + +[[package]] +name = "PremiumUpsellDeps" +version = "0.0.0" +commit = "c51030b70236f2e9d69bb34348b7e045c2c437f6" +source = "git+https://github.com/roblox/premium-upsell-deps#master" +dependencies = ["RoactFitComponents roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components"] + +[[package]] +name = "UIBlox" +version = "0.1.1" +commit = "3a82d9edf6c4d9edf5bf34325ed4802c80d759dc" +source = "git+https://github.com/roblox/uiblox#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "InfiniteScroller roblox/infinite-scroller 0.5.6 url+https://github.com/roblox/infinite-scroller", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "RoactGamepad roblox/roact-gamepad 0.4.6 url+https://github.com/roblox/roact-gamepad", + "enumerate roblox/enumerate 1.0.0 url+https://github.com/roblox/enumerate", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "freeze" +version = "0.1.0" +commit = "ef89f9d0444a2a7f63d5fd913a38824f3edba69f" +source = "git+https://github.com/roblox/freeze#master" +dependencies = ["Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo"] + +[[package]] +name = "jtaylor/mock" +version = "0.1.0" +commit = "d2c4005c863fd2f9fa74b7391ada64906d242847" +source = "git+https://github.com/roblox/mock#master" + +[[package]] +name = "lua-promise" +version = "0.1.0" +commit = "1bb842c39b74105e3cd4c775e300d5139b19d039" +source = "git+https://github.com/roblox/lua-promise#master" +dependencies = ["tutils tutils 937da4f7 git+https://github.com/roblox/tutils#master"] + +[[package]] +name = "roact-navigation" +version = "0.1.1-linter-fix" +commit = "943c1c661c396a39ef3ca8f45927d4242732581d" +source = "url+https://github.com/roblox/roact-navigation" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", +] + +[[package]] +name = "roblox/asset-card" +version = "1.0.3" +commit = "103b2a571ef09611ac91d38f15c3777682f69bfe" +source = "git+https://github.com/roblox/asset-card#v1.0.3" +dependencies = [ + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "UIBlox UIBlox 3a82d9ed git+https://github.com/roblox/uiblox#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "roblox/cryo" +version = "1.0.0" +commit = "272caa8f3f3b3b29296b462f80b65cc9b1c92f1e" +source = "url+https://github.com/roblox/cryo" + +[[package]] +name = "roblox/dash" +version = "0.1.7" +commit = "03aaa4ab36e34e5d4c683c3ec337af80cf70a546" +source = "url+https://github.com/roblox/dash" + +[[package]] +name = "roblox/developer-tools" +version = "0.1.4" +commit = "90266d909da3b9264eb893a3761d80d97d2d3f4f" +source = "url+https://github.com/roblox/developer-tools" +dependencies = ["Dash roblox/dash 0.1.7 url+https://github.com/roblox/dash"] + +[[package]] +name = "roblox/enumerate" +version = "1.0.0" +commit = "48daaf0df47eaf36c154d691a8320239e4a1312e" +source = "url+https://github.com/roblox/enumerate" + +[[package]] +name = "roblox/genericpagination" +version = "0.1.0" +commit = "885168d01a0de5e0753653746ff8b392dc1d8acc" +source = "git+https://github.com/roblox/genericpagination#master" +dependencies = [ + "Promise lua-promise 1bb842c3 git+https://github.com/roblox/lua-promise#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "roblox/infinite-scroller" +version = "0.3.4" +commit = "43d4eedf0d7a0a97d2dd55e7f066ac617e7fde9f" +source = "url+https://github.com/roblox/infinite-scroller" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "roblox/infinite-scroller" +version = "0.5.6" +commit = "d622d74bec4a599c5f8ef642194fa9eaf973b5c0" +source = "url+https://github.com/roblox/infinite-scroller" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "roblox/infinite-scroller" +version = "0.7.1" +commit = "7286a92274106024c813d7a9364e84e1597f9034" +source = "git+https://github.com/roblox/infinite-scroller#v0.7.1" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Lumberyak roblox/lumberyak 0.1.1 url+https://github.com/roblox/lumberyak", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "roblox/infinite-scroller" +version = "0.7.3" +commit = "8dbd1980cdd54930abdd9d251263f364c9aa6f0b" +source = "url+https://github.com/roblox/infinite-scroller" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Lumberyak roblox/lumberyak 0.1.1 url+https://github.com/roblox/lumberyak", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "roblox/lua-result" +version = "0.1.0" +commit = "c6817c8455aa1f5922d83dd44c8bedbc7726e51d" +source = "git+https://github.com/roblox/lua-result#master" + +[[package]] +name = "roblox/lua-roact-policy-provider" +version = "0.1.0" +commit = "5f782af8bb981d6e9d2b3fa92f69b02bd4f4cf37" +source = "git+https://github.com/roblox/lua-roact-policy-provider#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Lumberyak roblox/lumberyak 76fee23f git+https://github.com/roblox/lumberyak#master", + "Mock jtaylor/mock d2c4005c git+https://github.com/roblox/mock#master", + "Promise lua-promise 1bb842c3 git+https://github.com/roblox/lua-promise#master", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "Symbol roblox/lua-symbol 139fdfe6 git+https://github.com/roblox/lua-symbol#master", + "tutils tutils 937da4f7 git+https://github.com/roblox/tutils#master", +] + +[[package]] +name = "roblox/lua-symbol" +version = "0.1.0" +commit = "139fdfe6e4d4eca690887d3beb80e1514af501bf" +source = "git+https://github.com/roblox/lua-symbol#master" + +[[package]] +name = "roblox/lumberyak" +version = "0.1.1" +commit = "76fee23fcefb988bfe97b4825dad792eb57c0e04" +source = "git+https://github.com/roblox/lumberyak#master" +dependencies = ["Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo"] + +[[package]] +name = "roblox/lumberyak" +version = "0.1.1" +commit = "80926beddd32fe7686e036da2c2e6e08d42c74cf" +source = "url+https://github.com/roblox/lumberyak" +dependencies = ["Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo"] + +[[package]] +name = "roblox/otter" +version = "0.1.3" +commit = "968bf9466a01286b74c14ef93a28d6ea8b23b561" +source = "url+https://github.com/roblox/otter" + +[[package]] +name = "roblox/purchase-prompt" +version = "0.1.6" +commit = "81ce762fbffc86c3402e4a8139c2eb6c6894eb5d" +source = "git+https://github.com/roblox/purchasepromptscript-roact#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "RoactRodux roblox/roact-rodux 0.2.2 url+https://github.com/roblox/roact-rodux", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "UIBlox UIBlox 3a82d9ed git+https://github.com/roblox/uiblox#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "roblox/purchase-prompt-deps" +version = "0.0.1" +commit = "c337b7da6ec46375e18cd3f14d6b5cb255fe0c0a" +source = "git+https://github.com/roblox/purchase-prompt-deps#master" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "RoactRodux roblox/roact-rodux 0.2.2 url+https://github.com/roblox/roact-rodux", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "UIBlox UIBlox 3a82d9ed git+https://github.com/roblox/uiblox#master", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "roblox/rhodium" +version = "0.2.5" +commit = "7e623fc1d95af129f8fe9ca959160969e469e2ef" +source = "url+https://github.com/roblox/rhodium" + +[[package]] +name = "roblox/roact" +version = "1.3.1" +commit = "380c3d652f41d896d2f5ebcc128aeed99d176184" +source = "url+https://github.com/roblox/roact" + +[[package]] +name = "roblox/roact-fit-components" +version = "1.2.5" +commit = "b784928fdef64215d38e2febae28412a3b2a520b" +source = "url+https://github.com/roblox/roact-fit-components" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", +] + +[[package]] +name = "roblox/roact-gamepad" +version = "0.4.6" +commit = "fc0b421162ed09a155ceeaf01142e58d8e686e74" +source = "url+https://github.com/roblox/roact-gamepad" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "enumerate roblox/enumerate 1.0.0 url+https://github.com/roblox/enumerate", + "t roblox/t 1.2.5 url+https://github.com/roblox/t", +] + +[[package]] +name = "roblox/roact-navigation" +version = "0.4.1" +commit = "20f4bf21637414c86d4a61965f8a01ae51a81f54" +source = "url+https://github.com/roblox/roact-navigation" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Otter roblox/otter 0.1.3 url+https://github.com/roblox/otter", + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", +] + +[[package]] +name = "roblox/roact-rodux" +version = "0.2.2" +commit = "78e2f5ac8c4679ef7216fbb9eedbcae31122545a" +source = "url+https://github.com/roblox/roact-rodux" +dependencies = [ + "Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", +] + +[[package]] +name = "roblox/rodux" +version = "1.0.0" +commit = "416e8042bd6eac7a385cb0955079501fd44f11e1" +source = "url+https://github.com/roblox/rodux" + +[[package]] +name = "roblox/string-utilities" +version = "1.0.0" +commit = "7cbef7885ca023d71dd93f02ffa32cbda65faa9c" +source = "url+https://github.com/roblox/string-utilities" + +[[package]] +name = "roblox/t" +version = "1.2.5" +commit = "5d6ee3e23a658b12bc433b0837184215618e233e" +source = "url+https://github.com/roblox/t" + +[[package]] +name = "roblox/testez" +version = "0.4.0" +commit = "6c169cd80edbacbe27aa1c15345a60c4cb5177c7" +source = "url+https://github.com/roblox/testez" + +[[package]] +name = "roblox/url-builder" +version = "1.0.4" +commit = "cc593441e3efee555f0d6789c5df45138947909f" +source = "url+https://github.com/roblox/url-builder" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "StringUtilities roblox/string-utilities 1.0.0 url+https://github.com/roblox/string-utilities", +] + +[[package]] +name = "rodux-networking" +version = "1.0.0" +commit = "8902a6aaf52d5cad67e8e972f8fb6969d1b81157" +source = "git+https://github.com/roblox/rodux-networking#v1.0.2" +dependencies = [ + "Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo", + "Freeze freeze ef89f9d0 git+https://github.com/roblox/freeze#master", + "Promise lua-promise 1bb842c3 git+https://github.com/roblox/lua-promise#master", + "Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux", + "tutils tutils 937da4f7 git+https://github.com/roblox/tutils#master", +] + +[[package]] +name = "tutils" +version = "0.1.0" +commit = "937da4f7f354ebe6cf62348a80cc82fda91be2f0" +source = "git+https://github.com/roblox/tutils#master" diff --git a/Client2021/ExtraContent/LuaPackages/rotriever.toml b/Client2021/ExtraContent/LuaPackages/rotriever.toml new file mode 100644 index 0000000..7ceb824 --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/rotriever.toml @@ -0,0 +1,47 @@ +[package] +name = "CorePackages" +author = "Roblox" +license = "" +version = "0.1.0" +proxy = "https://github.com/roblox/rotriever-proxy-index" + +[dependencies] +Roact = "github.com/roblox/roact@1.3.1" +Rodux = "github.com/roblox/rodux@1.0" +RoactRodux = "github.com/roblox/roact-rodux@0.2" +RoactNavigation = "github.com/roblox/roact-navigation@0.4.1" +Cryo = "github.com/roblox/cryo@1.0" +LuaChatDeps = { git = "https://github.com/Roblox/lua-chat-deps" } +LuaDiscussionsDeps = { git = "https://github.com/Roblox/lua-discussions-deps" } +LuaSocialLibrariesDeps = { git = "https://github.com/Roblox/lua-social-libraries-deps" } +PremiumUpsellDeps = { git = "https://github.com/Roblox/premium-upsell-deps" } +AvatarExperienceDeps = { git = "https://github.com/Roblox/avatar-experience-deps" } +PurchasePromptDeps = { git = "https://github.com/Roblox/purchase-prompt-deps" } +UIBlox = { git = "https://github.com/Roblox/uiblox", rev = "master" } +Otter = "github.com/roblox/otter@0.1.3" +t = "github.com/roblox/t@1.0" +enumerate = "github.com/roblox/enumerate@1.0.0" +PolicyProvider = { git = "https://github.com/Roblox/lua-roact-policy-provider", rev = "master" } +Lumberyak = { git = "https://github.com/Roblox/lumberyak" } +StringUtilities = "github.com/Roblox/string-utilities@1.0.0" +UrlBuilder = "github.com/Roblox/url-builder@1.0.4" + +# The following packages were ported from AppTempCommon +Promise = { git = "https://github.com/Roblox/lua-promise" } +Result = { git = "https://github.com/Roblox/lua-result" } +tutils = { git = "https://github.com/Roblox/tutils" } + +PurchasePrompt = { git = "https://github.com/Roblox/PurchasePromptScript-Roact", rev = "master" } + +InGameMenuDependencies = { git = "https://github.com/Roblox/in-game-menu-dependencies", rev = "master" } + +RoactGamepad = "github.com/roblox/roact-gamepad@0.4.6" + +[dev_dependencies] +TestEZ = "github.com/roblox/testez@0.4.0" +Rhodium = "github.com/roblox/rhodium@0.2.5" +DeveloperTools = "github.com/Roblox/developer-tools@0.1" + +[patch."https://github.rbx.com/roblox/uiblox"] +git = "https://github.com/roblox/uiblox" +rev = "master" diff --git a/Client2021/ExtraContent/LuaPackages/tutils.lua b/Client2021/ExtraContent/LuaPackages/tutils.lua new file mode 100644 index 0000000..1e69d3a --- /dev/null +++ b/Client2021/ExtraContent/LuaPackages/tutils.lua @@ -0,0 +1,8 @@ +local CorePackages = game:GetService("CorePackages") + +-- This covers all of the Packages folder, which is fairly defensive, but should +-- be okay even if it runs multiple times +local initify = require(CorePackages.initify) +initify(CorePackages.Packages) + +return require(CorePackages.Packages.tutils) diff --git a/Client2021/ExtraContent/models/AvatarContextMenu/AvatarContextArrow.rbxm b/Client2021/ExtraContent/models/AvatarContextMenu/AvatarContextArrow.rbxm new file mode 100644 index 0000000..f8cc772 Binary files /dev/null and b/Client2021/ExtraContent/models/AvatarContextMenu/AvatarContextArrow.rbxm differ diff --git a/Client2021/ExtraContent/models/DataModelPatch/DataModelPatch.rbxm b/Client2021/ExtraContent/models/DataModelPatch/DataModelPatch.rbxm new file mode 100644 index 0000000..8241818 Binary files /dev/null and b/Client2021/ExtraContent/models/DataModelPatch/DataModelPatch.rbxm differ diff --git a/Client2021/ExtraContent/places/InGameMenu.rbxl b/Client2021/ExtraContent/places/InGameMenu.rbxl new file mode 100644 index 0000000..fdd9a07 Binary files /dev/null and b/Client2021/ExtraContent/places/InGameMenu.rbxl differ diff --git a/Client2021/ExtraContent/places/Mobile.rbxl b/Client2021/ExtraContent/places/Mobile.rbxl new file mode 100644 index 0000000..6f79a66 Binary files /dev/null and b/Client2021/ExtraContent/places/Mobile.rbxl differ diff --git a/Client2021/ExtraContent/places/MobileChatPlace.rbxl b/Client2021/ExtraContent/places/MobileChatPlace.rbxl new file mode 100644 index 0000000..f087511 Binary files /dev/null and b/Client2021/ExtraContent/places/MobileChatPlace.rbxl differ diff --git a/Client2021/ExtraContent/places/RhodiumUnitTest.rbxl b/Client2021/ExtraContent/places/RhodiumUnitTest.rbxl new file mode 100644 index 0000000..5a813eb Binary files /dev/null and b/Client2021/ExtraContent/places/RhodiumUnitTest.rbxl differ diff --git a/Client2021/ExtraContent/textures/sky/white.png b/Client2021/ExtraContent/textures/sky/white.png new file mode 100644 index 0000000..3b46e3d Binary files /dev/null and b/Client2021/ExtraContent/textures/sky/white.png differ diff --git a/Client2021/ExtraContent/textures/ui/AvatarExperience/AvatarExperienceSkyboxDarkTheme.png b/Client2021/ExtraContent/textures/ui/AvatarExperience/AvatarExperienceSkyboxDarkTheme.png new file mode 100644 index 0000000..19607f5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/AvatarExperience/AvatarExperienceSkyboxDarkTheme.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_1x_1.png b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_1x_1.png new file mode 100644 index 0000000..598b460 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_1x_1.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_1x_2.png b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_1x_2.png new file mode 100644 index 0000000..05d488f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_1x_2.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_1.png b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_1.png new file mode 100644 index 0000000..e4909e5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_1.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_2.png b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_2.png new file mode 100644 index 0000000..9b4d115 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_2.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_3.png b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_3.png new file mode 100644 index 0000000..418b1f4 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_3.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_4.png b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_4.png new file mode 100644 index 0000000..92a6ddd Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_4.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_5.png b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_5.png new file mode 100644 index 0000000..5071046 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_2x_5.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_1.png b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_1.png new file mode 100644 index 0000000..fa03dbb Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_1.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_2.png b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_2.png new file mode 100644 index 0000000..3b42e87 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_2.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_3.png b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_3.png new file mode 100644 index 0000000..32d8877 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/AE/img_set_3x_3.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_1x_1.png b/Client2021/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_1x_1.png new file mode 100644 index 0000000..5bea3a6 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_1x_1.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_2x_1.png b/Client2021/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_2x_1.png new file mode 100644 index 0000000..2cee21b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_2x_1.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_3x_1.png b/Client2021/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_3x_1.png new file mode 100644 index 0000000..bb96425 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/InGameMenu/img_set_3x_1.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_1x_1.png b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_1x_1.png new file mode 100644 index 0000000..365c0b3 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_1x_1.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_1x_2.png b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_1x_2.png new file mode 100644 index 0000000..ceb4a3a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_1x_2.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_1.png b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_1.png new file mode 100644 index 0000000..c674921 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_1.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_2.png b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_2.png new file mode 100644 index 0000000..59204ec Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_2.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_3.png b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_3.png new file mode 100644 index 0000000..31a6c27 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_3.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_4.png b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_4.png new file mode 100644 index 0000000..f11cc68 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_4.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_5.png b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_5.png new file mode 100644 index 0000000..07f9eb0 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_2x_5.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_1.png b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_1.png new file mode 100644 index 0000000..bba3047 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_1.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_2.png b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_2.png new file mode 100644 index 0000000..8d21eff Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_2.png differ diff --git a/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_3.png b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_3.png new file mode 100644 index 0000000..13b5040 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/ImageSet/LuaApp/img_set_3x_3.png differ diff --git a/Client2021/ExtraContent/textures/ui/InGameChat/Caret.png b/Client2021/ExtraContent/textures/ui/InGameChat/Caret.png new file mode 100644 index 0000000..b16bad7 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/InGameChat/Caret.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px.png b/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px.png new file mode 100644 index 0000000..9219b54 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@2x.png new file mode 100644 index 0000000..74ee0db Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@3x.png new file mode 100644 index 0000000..63a2428 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-btn-blue-3px@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator.png b/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator.png new file mode 100644 index 0000000..3f2bed0 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator@2x.png new file mode 100644 index 0000000..92be6e5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator@3x.png new file mode 100644 index 0000000..cc157ad Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/9-slice/gr-loading-indicator@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-featured.png b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-featured.png new file mode 100644 index 0000000..716f358 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-featured.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-featured@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-featured@2x.png new file mode 100644 index 0000000..a03cced Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-featured@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-featured@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-featured@3x.png new file mode 100644 index 0000000..712b018 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-featured@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-popular.png b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-popular.png new file mode 100644 index 0000000..74becef Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-popular.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-popular@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-popular@2x.png new file mode 100644 index 0000000..bcf1fa1 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-popular@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-popular@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-popular@3x.png new file mode 100644 index 0000000..cd02edf Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-popular@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-top rated.png b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-top rated.png new file mode 100644 index 0000000..1b6f08a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-top rated.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-top rated@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-top rated@2x.png new file mode 100644 index 0000000..373be35 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-top rated@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-top rated@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-top rated@3x.png new file mode 100644 index 0000000..6a7dde2 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/category/ic-top rated@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up.png b/Client2021/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up.png new file mode 100644 index 0000000..73f833a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up@2x.png new file mode 100644 index 0000000..62314dd Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up@3x.png new file mode 100644 index 0000000..a9c91b2 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/dropdown/gr-tip-up@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/CharacterShadow.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/CharacterShadow.png new file mode 100644 index 0000000..381c17c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/CharacterShadow.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/DatePickerDivider.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/DatePickerDivider.png new file mode 100644 index 0000000..3d2702a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/DatePickerDivider.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/GridBackground.jpg b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/GridBackground.jpg new file mode 100644 index 0000000..e3ace0e Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/GridBackground.jpg differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/Vignette.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/Vignette.png new file mode 100644 index 0000000..1fdf8d9 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/Vignette.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/builderman.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/builderman.png new file mode 100644 index 0000000..00dfb9d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/builderman.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/gradient_bg.jpg b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/gradient_bg.jpg new file mode 100644 index 0000000..6585db8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/gradient_bg.jpg differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/logo_white_1x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/logo_white_1x.png new file mode 100644 index 0000000..e4cb43a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/logo_white_1x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/reversevignette.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/reversevignette.png new file mode 100644 index 0000000..10ec0a9 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/Auth/reversevignette.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/CityBackground.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/CityBackground.png new file mode 100644 index 0000000..820d79a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/CityBackground.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/CompactView_purplelayer.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/CompactView_purplelayer.png new file mode 100644 index 0000000..5b3f9a8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/CompactView_purplelayer.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/EducationalBackground.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/EducationalBackground.png new file mode 100644 index 0000000..2f20b00 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/EducationalBackground.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/GameDetailsBackground/abkg_general.jpg b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/GameDetailsBackground/abkg_general.jpg new file mode 100644 index 0000000..d143f4c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/GameDetailsBackground/abkg_general.jpg differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/GameDetailsBackground/loadingBkg_base.jpg b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/GameDetailsBackground/loadingBkg_base.jpg new file mode 100644 index 0000000..534edd2 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/GameDetailsBackground/loadingBkg_base.jpg differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/TopBottomBorder.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/TopBottomBorder.png new file mode 100644 index 0000000..02506d4 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/TopBottomBorder.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/WideView_purpleLayer.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/WideView_purpleLayer.png new file mode 100644 index 0000000..4414da7 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/WideView_purpleLayer.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-add.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-add.png new file mode 100644 index 0000000..c863bce Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-add.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-add@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-add@2x.png new file mode 100644 index 0000000..7f8368f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-add@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-add@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-add@3x.png new file mode 100644 index 0000000..b8a5b30 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-add@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84.png new file mode 100644 index 0000000..1ac156d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@2x.png new file mode 100644 index 0000000..97f6bda Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@3x.png new file mode 100644 index 0000000..a167ba0 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-84x84@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90.png new file mode 100644 index 0000000..ac423bb Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@2x.png new file mode 100644 index 0000000..2e44709 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@3x.png new file mode 100644 index 0000000..1cf1cfa Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar mask-90x90@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36.png new file mode 100644 index 0000000..3879628 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@2x.png new file mode 100644 index 0000000..952a375 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@3x.png new file mode 100644 index 0000000..baa0910 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-avatar-frame-36x36@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle.png new file mode 100644 index 0000000..5a41676 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle@2x.png new file mode 100644 index 0000000..e4347f1 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle@3x.png new file mode 100644 index 0000000..437fd5c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-bloom-circle@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px.png new file mode 100644 index 0000000..3608a54 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px@2x.png new file mode 100644 index 0000000..3ca1bc2 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px@3x.png new file mode 100644 index 0000000..cadf11a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gr-profile-150x150px@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100.png new file mode 100644 index 0000000..19be24b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100@2x.png new file mode 100644 index 0000000..8f56820 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100@3x.png new file mode 100644 index 0000000..6cbb155 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/gradient_0_100@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/itemcardbkg_dark.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/itemcardbkg_dark.png new file mode 100644 index 0000000..e0c2211 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/itemcardbkg_dark.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection.png new file mode 100644 index 0000000..9c270b2 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection@2x.png new file mode 100644 index 0000000..03c37c8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection@3x.png new file mode 100644 index 0000000..9379cf3 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noNetworkConnection@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noconnection.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noconnection.png new file mode 100644 index 0000000..4f5485f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noconnection.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noconnection@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noconnection@2x.png new file mode 100644 index 0000000..46004dc Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noconnection@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noconnection@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noconnection@3x.png new file mode 100644 index 0000000..4f9bb99 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/noconnection@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait.png new file mode 100644 index 0000000..d3091db Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait@2x.png new file mode 100644 index 0000000..a537692 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait@3x.png new file mode 100644 index 0000000..6c3298f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/ph-avatar-portrait@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/playBtnBackground.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/playBtnBackground.png new file mode 100644 index 0000000..698bee4 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/playBtnBackground.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask.png new file mode 100644 index 0000000..ae751a4 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask@2x.png new file mode 100644 index 0000000..0cd07f6 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask@3x.png new file mode 100644 index 0000000..7145a4d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36.png new file mode 100644 index 0000000..3879628 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36@2x.png new file mode 100644 index 0000000..952a375 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36@3x.png new file mode 100644 index 0000000..baa0910 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/profilemask_36@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer.png new file mode 100644 index 0000000..10baae3 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer@2x.png new file mode 100644 index 0000000..76bacac Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer_darkTheme.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer_darkTheme.png new file mode 100644 index 0000000..10baae3 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer_darkTheme.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer_darkTheme@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer_darkTheme@2x.png new file mode 100644 index 0000000..76bacac Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer_darkTheme@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer_lightTheme.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer_lightTheme.png new file mode 100644 index 0000000..10baae3 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer_lightTheme.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer_lightTheme@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer_lightTheme@2x.png new file mode 100644 index 0000000..76bacac Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/graphic/shimmer_lightTheme@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Amazon_large.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Amazon_large.png new file mode 100644 index 0000000..ac2e524 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Amazon_large.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Amazon_large@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Amazon_large@2x.png new file mode 100644 index 0000000..7f9928d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Amazon_large@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Amazon_large@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Amazon_large@3x.png new file mode 100644 index 0000000..e949612 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Amazon_large@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large.png new file mode 100644 index 0000000..55bd21a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large@2x.png new file mode 100644 index 0000000..a93c593 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large@3x.png new file mode 100644 index 0000000..a53b28c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/GameDetails/social/Discord_large@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX.png new file mode 100644 index 0000000..7d690e8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX@2x.png new file mode 100644 index 0000000..6d88c27 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX@3x.png new file mode 100644 index 0000000..16efb3c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-ROBUX@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add-down.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add-down.png new file mode 100644 index 0000000..0a3bf2b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add-down.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add-down@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add-down@2x.png new file mode 100644 index 0000000..5dcd81b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add-down@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add-down@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add-down@3x.png new file mode 100644 index 0000000..80ca4f4 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add-down@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add.png new file mode 100644 index 0000000..649ae8f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add@2x.png new file mode 100644 index 0000000..77e1f58 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add@3x.png new file mode 100644 index 0000000..2b12c24 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-add@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right.png new file mode 100644 index 0000000..c490b34 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right@2x.png new file mode 100644 index 0000000..5ceb151 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right@3x.png new file mode 100644 index 0000000..256287d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-arrow-right@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot.png new file mode 100644 index 0000000..2b1bbec Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot@2x.png new file mode 100644 index 0000000..f7987d6 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot@3x.png new file mode 100644 index 0000000..ffe172d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-blue-dot@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20.png new file mode 100644 index 0000000..577160d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20@2x.png new file mode 100644 index 0000000..47429b9 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20@3x.png new file mode 100644 index 0000000..c5643d2 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-chat20x20@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-favorite-filled.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-favorite-filled.png new file mode 100644 index 0000000..8fa0b77 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-favorite-filled.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-favorite-filled@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-favorite-filled@2x.png new file mode 100644 index 0000000..9419d89 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-favorite-filled@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-favorite.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-favorite.png new file mode 100644 index 0000000..fd51ec1 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-favorite.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-favorite@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-favorite@2x.png new file mode 100644 index 0000000..ef6f41b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-favorite@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-game.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-game.png new file mode 100644 index 0000000..b1b1b9a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-game.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-game@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-game@2x.png new file mode 100644 index 0000000..253963b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-game@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-game@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-game@3x.png new file mode 100644 index 0000000..e488025 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-game@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-games.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-games.png new file mode 100644 index 0000000..8d60528 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-games.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-games@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-games@2x.png new file mode 100644 index 0000000..5efd3a2 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-games@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-games@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-games@3x.png new file mode 100644 index 0000000..e27091b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-games@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-about.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-about.png new file mode 100644 index 0000000..9f15260 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-about.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-about@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-about@2x.png new file mode 100644 index 0000000..27a89a7 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-about@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-about@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-about@3x.png new file mode 100644 index 0000000..ba4742d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-about@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog.png new file mode 100644 index 0000000..cabd391 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog@2x.png new file mode 100644 index 0000000..84d3181 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog@3x.png new file mode 100644 index 0000000..9a71c4f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-blog@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club.png new file mode 100644 index 0000000..a729363 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club@2x.png new file mode 100644 index 0000000..15ab42e Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club@3x.png new file mode 100644 index 0000000..ecbfe67 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-builders-club@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog.png new file mode 100644 index 0000000..d6d740c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog@2x.png new file mode 100644 index 0000000..10f0c09 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog@3x.png new file mode 100644 index 0000000..a2b13c6 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-catalog@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-create.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-create.png new file mode 100644 index 0000000..7db7b0a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-create.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-create@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-create@2x.png new file mode 100644 index 0000000..ad53a89 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-create@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-create@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-create@3x.png new file mode 100644 index 0000000..05498c8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-create@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-events.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-events.png new file mode 100644 index 0000000..a586238 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-events.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-events@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-events@2x.png new file mode 100644 index 0000000..0940fc0 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-events@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-events@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-events@3x.png new file mode 100644 index 0000000..a76e24f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-events@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends.png new file mode 100644 index 0000000..75366b7 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends@2x.png new file mode 100644 index 0000000..3629480 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends@3x.png new file mode 100644 index 0000000..91e86f5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-friends@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups.png new file mode 100644 index 0000000..b07c7b9 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups@2x.png new file mode 100644 index 0000000..1e111ac Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups@3x.png new file mode 100644 index 0000000..412bf34 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-groups@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-help.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-help.png new file mode 100644 index 0000000..bbe33d6 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-help.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-help@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-help@2x.png new file mode 100644 index 0000000..8529801 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-help@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-help@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-help@3x.png new file mode 100644 index 0000000..6f569db Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-help@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory.png new file mode 100644 index 0000000..6a7a91d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory@2x.png new file mode 100644 index 0000000..df440ed Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory@3x.png new file mode 100644 index 0000000..b622838 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-inventory@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-message.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-message.png new file mode 100644 index 0000000..deaef34 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-message.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-message@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-message@2x.png new file mode 100644 index 0000000..676fa23 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-message@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-message@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-message@3x.png new file mode 100644 index 0000000..0b3e1cb Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-message@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed.png new file mode 100644 index 0000000..d3260bc Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed@2x.png new file mode 100644 index 0000000..1c99d76 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed@3x.png new file mode 100644 index 0000000..dc67f5d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-my-feed@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile.png new file mode 100644 index 0000000..86450b1 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile@2x.png new file mode 100644 index 0000000..d204bff Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile@3x.png new file mode 100644 index 0000000..c570d88 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-profile@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings.png new file mode 100644 index 0000000..7a0f9d1 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings@2x.png new file mode 100644 index 0000000..03a254b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings@3x.png new file mode 100644 index 0000000..375b1e5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more-settings@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more.png new file mode 100644 index 0000000..bad7231 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more@2x.png new file mode 100644 index 0000000..2f90207 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more@3x.png new file mode 100644 index 0000000..2bc8325 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-more@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20.png new file mode 100644 index 0000000..b8ec87e Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20@2x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20@2x.png new file mode 100644 index 0000000..b5fa7e8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20@3x.png b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20@3x.png new file mode 100644 index 0000000..f5f12f5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaApp/icons/ic-view-details20x20@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/btn-control-sm.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/btn-control-sm.png new file mode 100644 index 0000000..0623973 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/btn-control-sm.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right.png new file mode 100644 index 0000000..b0a79f4 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right@2x.png new file mode 100644 index 0000000..409dbfb Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right@3x.png new file mode 100644 index 0000000..22ed017 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-right@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip.png new file mode 100644 index 0000000..229f43c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@2x.png new file mode 100644 index 0000000..7d4a790 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@3x.png new file mode 100644 index 0000000..883c51b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self-tip@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self.png new file mode 100644 index 0000000..78ee347 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2.png new file mode 100644 index 0000000..f5e244b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2@2x.png new file mode 100644 index 0000000..0fc0b5a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2@3x.png new file mode 100644 index 0000000..bc0139c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self2@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self@2x.png new file mode 100644 index 0000000..9f44c8c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self@3x.png new file mode 100644 index 0000000..f74e963 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-self@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right.png new file mode 100644 index 0000000..517b79a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@2x.png new file mode 100644 index 0000000..49fed32 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@3x.png new file mode 100644 index 0000000..96e8a1e Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip-right@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip.png new file mode 100644 index 0000000..4120ce7 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip@2x.png new file mode 100644 index 0000000..b48e89a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip@3x.png new file mode 100644 index 0000000..11f56d3 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble-tip@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble.png new file mode 100644 index 0000000..de1cc1e Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2.png new file mode 100644 index 0000000..7512448 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2@2x.png new file mode 100644 index 0000000..8b84aff Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2@3x.png new file mode 100644 index 0000000..c76882b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble2@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble@2x.png new file mode 100644 index 0000000..c5a94b5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble@3x.png new file mode 100644 index 0000000..0ab7de8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/chat-bubble@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/error-toast.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/error-toast.png new file mode 100644 index 0000000..3fcdffd Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/error-toast.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/error-toast@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/error-toast@2x.png new file mode 100644 index 0000000..b514105 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/error-toast@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/error-toast@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/error-toast@3x.png new file mode 100644 index 0000000..0cf667d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/error-toast@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon.png new file mode 100644 index 0000000..8e2ae96 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon@2x.png new file mode 100644 index 0000000..c7020c7 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon@3x.png new file mode 100644 index 0000000..0025b77 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/gr-mask-game-icon@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/hello-button.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/hello-button.png new file mode 100644 index 0000000..22acfff Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/hello-button.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/hello-button@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/hello-button@2x.png new file mode 100644 index 0000000..415d1f5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/hello-button@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/hello-button@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/hello-button@3x.png new file mode 100644 index 0000000..5935ab4 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/hello-button@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-default.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-default.png new file mode 100644 index 0000000..a1e3247 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-default.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-default@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-default@2x.png new file mode 100644 index 0000000..bad7c52 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-default@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-default@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-default@3x.png new file mode 100644 index 0000000..e92e175 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-default@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message.png new file mode 100644 index 0000000..0384815 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message@2x.png new file mode 100644 index 0000000..ea0a875 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message@3x.png new file mode 100644 index 0000000..19d5308 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/input-send-message@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/modal.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/modal.png new file mode 100644 index 0000000..b127799 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/modal.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/modal@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/modal@2x.png new file mode 100644 index 0000000..1ffc6a9 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/modal@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/modal@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/modal@3x.png new file mode 100644 index 0000000..12d0f90 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/modal@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator.png new file mode 100644 index 0000000..38dd53a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator@2x.png new file mode 100644 index 0000000..3aa02fd Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator@3x.png new file mode 100644 index 0000000..1287b87 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/new-message-indicator@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar.png new file mode 100644 index 0000000..c47871b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar@2x.png new file mode 100644 index 0000000..bb7c513 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar@3x.png new file mode 100644 index 0000000..3e15a0d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/scroll-bar@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/search.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/search.png new file mode 100644 index 0000000..6fdddea Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/search.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/search@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/search@2x.png new file mode 100644 index 0000000..857edd9 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/search@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/search@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/search@3x.png new file mode 100644 index 0000000..eb8a142 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/search@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/system-message.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/system-message.png new file mode 100644 index 0000000..d26555b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/system-message.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/system-message@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/system-message@2x.png new file mode 100644 index 0000000..3d85f6f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/system-message@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/system-message@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/system-message@3x.png new file mode 100644 index 0000000..3e8c804 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/system-message@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble.png new file mode 100644 index 0000000..75f482a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble@2x.png new file mode 100644 index 0000000..c1a412f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble@3x.png new file mode 100644 index 0000000..7ba23b8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/9-slice/tag-bubble@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/friendmask.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/friendmask.png new file mode 100644 index 0000000..a42205f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/friendmask.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24.png new file mode 100644 index 0000000..35db759 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24@2x.png new file mode 100644 index 0000000..4886ac6 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24@3x.png new file mode 100644 index 0000000..b444c76 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-24x24@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60.png new file mode 100644 index 0000000..0deee67 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60@2x.png new file mode 100644 index 0000000..34a58a7 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60@3x.png new file mode 100644 index 0000000..8daff78 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-game-border-60x60@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52.png new file mode 100644 index 0000000..d3725fe Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52@2x.png new file mode 100644 index 0000000..c775cab Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10.png new file mode 100644 index 0000000..8d7fab3 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@2x.png new file mode 100644 index 0000000..07f3a24 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@3x.png new file mode 100644 index 0000000..a5de74f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-10x10@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12.png new file mode 100644 index 0000000..8b59d6a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@2x.png new file mode 100644 index 0000000..80c2bd4 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@3x.png new file mode 100644 index 0000000..e9eb973 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-12x12@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14.png new file mode 100644 index 0000000..1200f62 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@2x.png new file mode 100644 index 0000000..6f214d9 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@3x.png new file mode 100644 index 0000000..8afb3a6 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-14x14@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6.png new file mode 100644 index 0000000..b9d4a49 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@2x.png new file mode 100644 index 0000000..8b59d6a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@3x.png new file mode 100644 index 0000000..1f90cfd Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-6x6@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8.png new file mode 100644 index 0000000..be8d8c5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@2x.png new file mode 100644 index 0000000..c9632ff Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@3x.png new file mode 100644 index 0000000..0d194b0 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame-8x8@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame.png new file mode 100644 index 0000000..f622033 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame@2x.png new file mode 100644 index 0000000..24ff925 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame@3x.png new file mode 100644 index 0000000..6866f33 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-ingame@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10.png new file mode 100644 index 0000000..70bef5b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@2x.png new file mode 100644 index 0000000..b7d0d3f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@3x.png new file mode 100644 index 0000000..1daa6c7 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-10x10@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12.png new file mode 100644 index 0000000..32b5368 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12@3x.png new file mode 100644 index 0000000..7c38197 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x12@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x2@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x2@2x.png new file mode 100644 index 0000000..6a0afba Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-12x2@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14.png new file mode 100644 index 0000000..bb74b97 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@2x.png new file mode 100644 index 0000000..1015a19 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@3x.png new file mode 100644 index 0000000..eeb8cee Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-14x14@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8.png new file mode 100644 index 0000000..3d1a10a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@2x.png new file mode 100644 index 0000000..f2d5ec5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@3x.png new file mode 100644 index 0000000..fb9489f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio-8x8@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio.png new file mode 100644 index 0000000..94c64f4 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio@2x.png new file mode 100644 index 0000000..0c6d285 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio@3x.png new file mode 100644 index 0000000..db89806 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6.png new file mode 100644 index 0000000..afd86c0 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@2x.png new file mode 100644 index 0000000..32b5368 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@3x.png new file mode 100644 index 0000000..970a691 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-instudio_6x6@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10.png new file mode 100644 index 0000000..6dd796d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@2x.png new file mode 100644 index 0000000..6632f9b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@3x.png new file mode 100644 index 0000000..2c39a06 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-10x10@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12.png new file mode 100644 index 0000000..e8b271d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@2x.png new file mode 100644 index 0000000..6c3645b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@3x.png new file mode 100644 index 0000000..3b76e80 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-12x12@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14.png new file mode 100644 index 0000000..4ee79a3 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@2x.png new file mode 100644 index 0000000..2673de7 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@3x.png new file mode 100644 index 0000000..26b4071 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-14x14@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6.png new file mode 100644 index 0000000..d9f9181 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@2x.png new file mode 100644 index 0000000..e8b271d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@3x.png new file mode 100644 index 0000000..e3e9c60 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-6x6@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8.png new file mode 100644 index 0000000..d26c309 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@2x.png new file mode 100644 index 0000000..b3428ea Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@3x.png new file mode 100644 index 0000000..6a14d5c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online-8x8@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online.png new file mode 100644 index 0000000..a8efbec Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online@2x.png new file mode 100644 index 0000000..1133089 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online@3x.png new file mode 100644 index 0000000..5c61715 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-indicator-online@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48.png new file mode 100644 index 0000000..47bd340 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48@2x.png new file mode 100644 index 0000000..578a10a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers.png new file mode 100644 index 0000000..f59ead5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers@2x.png new file mode 100644 index 0000000..b4b9fbe Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers@3x.png new file mode 100644 index 0000000..4f3279f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-numbers@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow.png new file mode 100644 index 0000000..4c1fa77 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow@2x.png new file mode 100644 index 0000000..42e5a34 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow@3x.png new file mode 100644 index 0000000..64615ab Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-overlay-shadow@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36.png new file mode 100644 index 0000000..ecab811 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36@2x.png new file mode 100644 index 0000000..03b94cc Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36@3x.png new file mode 100644 index 0000000..b0d22fe Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-36x36@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted.png new file mode 100644 index 0000000..5917d8d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@2x.png new file mode 100644 index 0000000..911d0f7 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@3x.png new file mode 100644 index 0000000..c27bf49 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48.png new file mode 100644 index 0000000..e2f28f8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48@2x.png new file mode 100644 index 0000000..b1cd13b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48@3x.png new file mode 100644 index 0000000..9a0a00a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-profile-border-48x48@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on.png new file mode 100644 index 0000000..59255c1 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on@2x.png new file mode 100644 index 0000000..407fa57 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on@3x.png new file mode 100644 index 0000000..ed4ad48 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send-on@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send.png new file mode 100644 index 0000000..b5f28b2 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send@2x.png new file mode 100644 index 0000000..d647c1b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send@3x.png new file mode 100644 index 0000000..aced5ec Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/gr-send@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on.png new file mode 100644 index 0000000..0390cb4 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on@2x.png new file mode 100644 index 0000000..9ee440a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on@3x.png new file mode 100644 index 0000000..b897b38 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox-on@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox.png new file mode 100644 index 0000000..cfd7260 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox@2x.png new file mode 100644 index 0000000..7a04603 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox@3x.png new file mode 100644 index 0000000..09fd802 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/ic-checkbox@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/indicator-background.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/indicator-background.png new file mode 100644 index 0000000..c36fb92 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/indicator-background.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/send-white.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/send-white.png new file mode 100644 index 0000000..e9df684 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/send-white.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/send-white@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/send-white@2x.png new file mode 100644 index 0000000..030eeee Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/send-white@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/graphic/send-white@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/send-white@3x.png new file mode 100644 index 0000000..af160d0 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/graphic/send-white@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends.png new file mode 100644 index 0000000..a68367b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends@2x.png new file mode 100644 index 0000000..6e1b632 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends@3x.png new file mode 100644 index 0000000..b2a1be0 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-add-friends@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-alert.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-alert.png new file mode 100644 index 0000000..85d6d39 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-alert.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-alert@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-alert@2x.png new file mode 100644 index 0000000..7fb786a Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-alert@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-alert@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-alert@3x.png new file mode 100644 index 0000000..ab44ae0 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-alert@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-back-android.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-back-android.png new file mode 100644 index 0000000..2fb40d1 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-back-android.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-back-android@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-back-android@2x.png new file mode 100644 index 0000000..af6490d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-back-android@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-back.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-back.png new file mode 100644 index 0000000..42a2433 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-back.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-back@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-back@2x.png new file mode 100644 index 0000000..e2b4462 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-back@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-bc.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-bc.png new file mode 100644 index 0000000..acfbdb5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-bc.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-bc@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-bc@2x.png new file mode 100644 index 0000000..be1c042 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-bc@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-bc@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-bc@3x.png new file mode 100644 index 0000000..85d2fbd Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-bc@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large.png new file mode 100644 index 0000000..5a7141d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large@2x.png new file mode 100644 index 0000000..5673f80 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large@3x.png new file mode 100644 index 0000000..f07c89e Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-chat-large@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-check.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-check.png new file mode 100644 index 0000000..26d3557 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-check.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-check@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-check@2x.png new file mode 100644 index 0000000..7791da1 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-check@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-check@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-check@3x.png new file mode 100644 index 0000000..38cc530 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-check@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy.png new file mode 100644 index 0000000..511e290 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy@2x.png new file mode 100644 index 0000000..d2aa085 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy@3x.png new file mode 100644 index 0000000..fcd44c7 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-checkbox-on copy@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray.png new file mode 100644 index 0000000..f39bdfc Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray@2x.png new file mode 100644 index 0000000..18e07bd Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray@3x.png new file mode 100644 index 0000000..c746437 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-gray@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid.png new file mode 100644 index 0000000..5839bd5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid@2x.png new file mode 100644 index 0000000..48b7bbf Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid@3x.png new file mode 100644 index 0000000..5ec68b8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-clear-solid@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2.png new file mode 100644 index 0000000..1dd1e05 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2@2x.png new file mode 100644 index 0000000..3a845ff Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2@3x.png new file mode 100644 index 0000000..3326324 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-close-gray2@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-close-white.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-close-white.png new file mode 100644 index 0000000..af8f7b8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-close-white.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-create-group.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-create-group.png new file mode 100644 index 0000000..72c902c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-create-group.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-create-group@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-create-group@2x.png new file mode 100644 index 0000000..6041c11 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-create-group@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-create-group@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-create-group@3x.png new file mode 100644 index 0000000..569e60d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-create-group@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24.png new file mode 100644 index 0000000..ad3c8f9 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24@2x.png new file mode 100644 index 0000000..2dfef75 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24@3x.png new file mode 100644 index 0000000..73ee9b8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-createchat1-24x24@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-friends.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-friends.png new file mode 100644 index 0000000..c2dddec Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-friends.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-friends@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-friends@2x.png new file mode 100644 index 0000000..aafb10e Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-friends@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-friends@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-friends@3x.png new file mode 100644 index 0000000..c2610a8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-friends@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24.png new file mode 100644 index 0000000..a04a67c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24@2x.png new file mode 100644 index 0000000..a8ee17d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24@3x.png new file mode 100644 index 0000000..9276f55 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game-pressed-24x24@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game.png new file mode 100644 index 0000000..aeb09dc Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game@2x.png new file mode 100644 index 0000000..233dbe2 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game@3x.png new file mode 100644 index 0000000..4334107 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-game@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16.png new file mode 100644 index 0000000..4be38ac Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16@2x.png new file mode 100644 index 0000000..2d02bf0 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16@3x.png new file mode 100644 index 0000000..a3204eb Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group-16x16@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group.png new file mode 100644 index 0000000..db3b1b7 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group@2x.png new file mode 100644 index 0000000..48ec989 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group@3x.png new file mode 100644 index 0000000..38a5549 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-group@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-info.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-info.png new file mode 100644 index 0000000..0a9e434 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-info.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-info@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-info@2x.png new file mode 100644 index 0000000..2801509 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-info@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-info@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-info@3x.png new file mode 100644 index 0000000..2fb4765 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-info@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-leave.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-leave.png new file mode 100644 index 0000000..136f273 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-leave.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-leave@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-leave@2x.png new file mode 100644 index 0000000..53f544f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-leave@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-leave@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-leave@3x.png new file mode 100644 index 0000000..57966c6 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-leave@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-more.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-more.png new file mode 100644 index 0000000..718010b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-more.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-more@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-more@2x.png new file mode 100644 index 0000000..d107f2f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-more@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-more@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-more@3x.png new file mode 100644 index 0000000..7d871a6 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-more@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-nametag.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-nametag.png new file mode 100644 index 0000000..eee203e Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-nametag.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-nametag@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-nametag@2x.png new file mode 100644 index 0000000..1dc795d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-nametag@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-nametag@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-nametag@3x.png new file mode 100644 index 0000000..ad7710f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-nametag@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-notification.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-notification.png new file mode 100644 index 0000000..7cc3f5d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-notification.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-notification@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-notification@2x.png new file mode 100644 index 0000000..7db1065 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-notification@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-notification@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-notification@3x.png new file mode 100644 index 0000000..c49aa2b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-notification@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pin.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pin.png new file mode 100644 index 0000000..f7a8d92 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pin.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pin@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pin@2x.png new file mode 100644 index 0000000..9befe4b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pin@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pin@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pin@3x.png new file mode 100644 index 0000000..7387c33 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pin@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed.png new file mode 100644 index 0000000..b893f0b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed@2x.png new file mode 100644 index 0000000..1103fa5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed@3x.png new file mode 100644 index 0000000..37bfbc5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-pinpressed@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-profile.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-profile.png new file mode 100644 index 0000000..fdf6f87 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-profile.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-profile@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-profile@2x.png new file mode 100644 index 0000000..8649c34 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-profile@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-profile@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-profile@3x.png new file mode 100644 index 0000000..bd3fe06 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-profile@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-remove.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-remove.png new file mode 100644 index 0000000..089dba9 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-remove.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-remove@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-remove@2x.png new file mode 100644 index 0000000..a43d237 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-remove@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-remove@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-remove@3x.png new file mode 100644 index 0000000..0185b15 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-remove@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-resend.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-resend.png new file mode 100644 index 0000000..464bc87 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-resend.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-resend@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-resend@2x.png new file mode 100644 index 0000000..065d260 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-resend@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-resend@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-resend@3x.png new file mode 100644 index 0000000..c93f1d8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-resend@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-robux.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-robux.png new file mode 100644 index 0000000..30e32cf Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-robux.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-robux@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-robux@2x.png new file mode 100644 index 0000000..b181a12 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-robux@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-robux@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-robux@3x.png new file mode 100644 index 0000000..67f39f5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-robux@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray.png new file mode 100644 index 0000000..f2e0f74 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray@2x.png new file mode 100644 index 0000000..52661b5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray@3x.png new file mode 100644 index 0000000..365865f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search-gray@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search.png new file mode 100644 index 0000000..fefae0c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search@2x.png new file mode 100644 index 0000000..8585a75 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search@3x.png new file mode 100644 index 0000000..ed9ff2f Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-search@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-send.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-send.png new file mode 100644 index 0000000..c6951c1 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-send.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-send@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-send@2x.png new file mode 100644 index 0000000..d0afa8b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-send@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-send@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-send@3x.png new file mode 100644 index 0000000..41a0658 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-send@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20.png new file mode 100644 index 0000000..61f6d2e Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20@2x.png new file mode 100644 index 0000000..6661058 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20@3x.png new file mode 100644 index 0000000..be0e5a8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-unpin-20x20@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20.png new file mode 100644 index 0000000..b8ec87e Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20@2x.png new file mode 100644 index 0000000..b5fa7e8 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20@3x.png new file mode 100644 index 0000000..f5f12f5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/ic-viewdetails-20x20@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24.png new file mode 100644 index 0000000..119547d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24@2x.png new file mode 100644 index 0000000..47ecc6b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24@3x.png new file mode 100644 index 0000000..0bc59ee Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-24x24@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24.png new file mode 100644 index 0000000..e72edaa Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@2x.png new file mode 100644 index 0000000..3691a33 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@3x.png new file mode 100644 index 0000000..bf9e31b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/icon-share-game-pressed-24x24@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack.png new file mode 100644 index 0000000..b567c45 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack@2x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack@2x.png new file mode 100644 index 0000000..890f7f9 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack@3x.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack@3x.png new file mode 100644 index 0000000..a0b42ad Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/navigation_pushBack@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChat/icons/share-game-thumbnail.png b/Client2021/ExtraContent/textures/ui/LuaChat/icons/share-game-thumbnail.png new file mode 100644 index 0000000..1c4b30e Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChat/icons/share-game-thumbnail.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_checkbox.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_checkbox.png new file mode 100644 index 0000000..8bf4c14 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_checkbox.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose.png new file mode 100644 index 0000000..b87f16d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose@2x.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose@2x.png new file mode 100644 index 0000000..690d057 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose@3x.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose@3x.png new file mode 100644 index 0000000..634ee57 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_editing_compose@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff.png new file mode 100644 index 0000000..6e2cd7b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff@2x.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff@2x.png new file mode 100644 index 0000000..8f7339b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff@3x.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff@3x.png new file mode 100644 index 0000000..07422ba Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOff@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn.png new file mode 100644 index 0000000..7420fc4 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn@2x.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn@2x.png new file mode 100644 index 0000000..6e710f5 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn@3x.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn@3x.png new file mode 100644 index 0000000..ac4501c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/actions_notificationOn@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/common_search.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/common_search.png new file mode 100644 index 0000000..cd381bf Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/common_search.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/common_search@2x.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/common_search@2x.png new file mode 100644 index 0000000..3eb8a4d Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/common_search@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/common_search@3x.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/common_search@3x.png new file mode 100644 index 0000000..2638ca1 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/common_search@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/ic-add-friends.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/ic-add-friends.png new file mode 100644 index 0000000..199de94 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/ic-add-friends.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/ic-friend-empty-border.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/ic-friend-empty-border.png new file mode 100644 index 0000000..2bcf309 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/ic-friend-empty-border.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack.png new file mode 100644 index 0000000..b567c45 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack@2x.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack@2x.png new file mode 100644 index 0000000..890f7f9 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack@3x.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack@3x.png new file mode 100644 index 0000000..a0b42ad Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushBack@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight.png new file mode 100644 index 0000000..4f88566 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight@2x.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight@2x.png new file mode 100644 index 0000000..fce85d6 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight@3x.png b/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight@3x.png new file mode 100644 index 0000000..e4e78de Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaChatV2/navigation_pushRight@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonFill.png b/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonFill.png new file mode 100644 index 0000000..afae8bb Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonFill.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonFill@2x.png b/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonFill@2x.png new file mode 100644 index 0000000..0f8157c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonFill@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonFill@3x.png b/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonFill@3x.png new file mode 100644 index 0000000..72a5371 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonFill@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonStroke.png b/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonStroke.png new file mode 100644 index 0000000..70b102b Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonStroke.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonStroke@2x.png b/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonStroke@2x.png new file mode 100644 index 0000000..cee0a86 Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonStroke@2x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonStroke@3x.png b/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonStroke@3x.png new file mode 100644 index 0000000..0ac6f5c Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaDiscussions/buttonStroke@3x.png differ diff --git a/Client2021/ExtraContent/textures/ui/LuaDiscussions/search.png b/Client2021/ExtraContent/textures/ui/LuaDiscussions/search.png new file mode 100644 index 0000000..cd381bf Binary files /dev/null and b/Client2021/ExtraContent/textures/ui/LuaDiscussions/search.png differ diff --git a/Client2021/ExtraContent/translations/CoreScriptLocalization.csv b/Client2021/ExtraContent/translations/CoreScriptLocalization.csv new file mode 100644 index 0000000..8eb9a54 --- /dev/null +++ b/Client2021/ExtraContent/translations/CoreScriptLocalization.csv @@ -0,0 +1,751 @@ +Key,Context,Example,Source,de,en-us,es,es-es,fr,it,ja,ko,pt,pt-br,ru,zh-cn,zh-tw,zh-cjv +Authentication.Login.WeChat.AntiAddictionText,,,"Boycott bad games, refuse pirated games. Be aware of self-defense and being deceived. Playing games is good for your brain, but too much game play can harm your health. Manage your time well and enjoy a healthy lifestyle.","Boykottiere schlechte Spiele und lehne Raubkopien ab. Sei dir über Selbstverteidigung und Täuschungsversuche im Klaren. Spielen ist gut für dein Gehirn, aber zu viel des Guten kann deine Gesundheit beeinträchtigen. Teile dir deine Zeit gut ein und führe einen gesunden Lebensstil.","Boycott bad games, refuse pirated games. Be aware of self-defense and being deceived. Playing games is good for your brain, but too much game play can harm your health. Manage your time well and enjoy a healthy lifestyle.","Boicotea juegos con contenido inapropiado y di no a la piratería. Protégete y no dejes que te engañen. Jugar a videojuegos es bueno, pero jugar demasiado puede perjudicar tu salud. Gestiona bien tu tiempo para poder vivir una vida saludable.","Boicotea juegos con contenido inapropiado y di no a la piratería. Protégete y no dejes que te engañen. Jugar a videojuegos es bueno, pero jugar demasiado puede perjudicar tu salud. Gestiona bien tu tiempo para poder vivir una vida saludable.","Boycottez les mauvais jeux, évitez les jeux piratés. Faites attention à votre sécurité et ne vous faites pas berner. Jouer a des effets positifs sur le cerveau, mais en abuser risque de nuire à votre santé. Gérez votre temps convenablement et menez une vie saine.","Boicotta i giochi scadenti, rifiuta i giochi pirata. Difenditi e stai attento a non farti ingannare. I videogiochi sono un ottimo esercizio per la mente, ma esagerando diventano dannosi per la salute. Gestisci bene il tuo tempo e goditi uno stile di vita sano.",不適切なゲームには参加しないように心掛け、海賊版は拒否しましょう。自衛意識を高め、騙されないようにしましょう。ゲームをプレイすることは脳の働きを高めてくれますが、プレイしすぎると健康を害する恐れがあります。時間管理をきちんと行い、健康的なライフスタイルをお楽しみください。,"건전하지 않은 게임과 저작권 문제가 있는 게임은 플레이하지 마시고, 사기 행위에 연루되지 않도록 스스로를 보호하세요. 게임은 두뇌 발달에 도움이 되지만, 지나친 게임 플레이는 건강에 좋지 않습니다. 플레이 시간을 잘 조절해서 건강하게 게임을 즐기세요.","Boicote jogos ruins, recuse jogos piratas. Informe-se sobre autodefesa e técnicas de enganação. Jogar faz bem para o seu cérebro, mas jogar demais pode ser prejudicial à saúde. Administre seu tempo corretamente e tenha um estilo de vida saudável.","Boicote jogos ruins, recuse jogos piratas. Informe-se sobre autodefesa e técnicas de enganação. Jogar faz bem para o seu cérebro, mas jogar demais pode ser prejudicial à saúde. Administre seu tempo corretamente e tenha um estilo de vida saudável.","Избегайте плохих и пиратских игр. Будьте осторожны, и вас не обманут. Игры помогают развивать мозг, однако не надо с этим злоупотреблять – это может нанести вред здоровью. Разумно распределяйте свое время и наслаждайтесь правильным образом жизни!",抵制不良游戏,拒绝盗版游戏。注意自我保护,谨防受骗上当。适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。,抵制劣質與抄襲遊戲!玩遊戲有益身心,但過度沉迷會對身體造成影響。控制遊戲時間,享受健康人生!,抵制不良游戏,拒绝盗版游戏。注意自我保护,谨防受骗上当。适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。 +Authentication.Login.Label.WeChatAntiAddictionText,,,"Boycott bad games, refuse pirated games. Be aware of self-defense and being deceived. Playing games is good for your brain, but too much game play can harm your health. Manage your time well and enjoy a healthy lifestyle.","Boykottiere schlechte Spiele und lehne Raubkopien ab. Sei dir über Selbstverteidigung und Täuschungsversuche im Klaren. Spielen ist gut für dein Gehirn, aber zu viel des Guten kann deine Gesundheit beeinträchtigen. Teile dir deine Zeit gut ein und führe einen gesunden Lebensstil.","Boycott bad games, refuse pirated games. Be aware of self-defense and being deceived. Playing games is good for your brain, but too much game play can harm your health. Manage your time well and enjoy a healthy lifestyle.","Boicotea juegos con contenido inapropiado y di no a la piratería. No permitas que te engañen. Jugar a videojuegos es bueno, pero jugar demasiado puede perjudicar tu salud. Gestiona bien tu tiempo para vivir una vida balanceada y saludable.","Boicotea juegos con contenido inapropiado y di no a la piratería. No permitas que te engañen. Jugar a videojuegos es bueno, pero jugar demasiado puede perjudicar tu salud. Gestiona bien tu tiempo para vivir una vida balanceada y saludable.","Boycottez les mauvais jeux, évitez les jeux piratés. Faites attention à votre sécurité et ne vous faites pas berner. Jouer a des effets positifs sur le cerveau, mais en abuser risque de nuire à votre santé. Gérez votre temps convenablement et menez une vie saine.","Boicotta i giochi scadenti, rifiuta i giochi pirata. Difenditi e stai attento a non farti ingannare. I videogiochi sono un ottimo esercizio per la mente, ma esagerando diventano dannosi per la salute. Gestisci bene il tuo tempo e goditi uno stile di vita sano.",不適切なゲームには参加しないように心掛け、海賊版は拒否しましょう。自衛意識を高め、騙されないようにしましょう。ゲームをプレイすることは脳の働きを高めてくれますが、プレイしすぎると健康を害する恐れがあります。時間管理をきちんと行い、健康的なライフスタイルをお楽しみください。,"건전하지 않은 게임과 저작권 문제가 있는 게임은 플레이하지 마시고, 사기 행위에 연루되지 않도록 스스로를 보호하세요. 게임은 두뇌 발달에 도움이 되지만, 지나친 게임 플레이는 건강에 좋지 않습니다. 플레이 시간을 잘 조절해서 건강하게 게임을 즐기세요.","Boicote jogos ruins, recuse jogos piratas. Informe-se sobre autodefesa e técnicas de enganação. Jogar faz bem para o seu cérebro, mas jogar demais pode ser prejudicial à saúde. Administre seu tempo corretamente e tenha um estilo de vida saudável.","Boicote jogos ruins, recuse jogos piratas. Informe-se sobre autodefesa e técnicas de enganação. Jogar faz bem para o seu cérebro, mas jogar demais pode ser prejudicial à saúde. Administre seu tempo corretamente e tenha um estilo de vida saudável.","Избегайте плохих и пиратских игр. Будьте осторожны, и вас не обманут. Игры помогают развивать мозг, однако не надо ими злоупотреблять – это может нанести вред здоровью. Разумно распределяйте свое время и наслаждайтесь правильным образом жизни!",抵制不良游戏,拒绝盗版游戏。注意自我保护,谨防受骗上当。适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。,抵制劣質與抄襲遊戲!玩遊戲有益身心,但過度沉迷會對身體造成影響。控制遊戲時間,享受健康人生!,抵制不良游戏,拒绝盗版游戏。注意自我保护,谨防受骗上当。适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。 +Corescripts.AvatarContextMenu.FriendRequestPending,,,Friend Request Pending,Freundesanfrage ausstehend,Friend Request Pending,Solicitud de amistad pendiente,Solicitud de amistad pendiente,Demande d'ami en attente,Richiesta di amicizia in sospeso,友達リクエスト中,친구 요청 대기 중,Pedido de amizade pendente,Pedido de amizade pendente,Запрос о дружбе на рассмотрении,好友邀请待处理,好友邀請待處理,好友邀请待处理 +Corescripts.AvatarContextMenu.AddFriend,,,Add Friend,Freund hinzufügen,Add Friend,Añadir amigo,Añadir amigo,Ajouter un ami,Aggiungi amico,友達を追加,친구 추가,Adicionar amigo,Adicionar amigo,Добавить друга,添加好友,新增好友,添加好友 +Corescripts.AvatarContextMenu.Friends,,,Friends,Freunde,Friends,Amigos,Amigos,Amis,Amici,友達,친구,Amigos,Amigos,Друзья,好友,好友,好友 +Corescripts.AvatarContextMenu.AcceptFriendRequest,,,Accept Friend Request,Freundesanfrage annehmen,Accept Friend Request,Aceptar solicitud de amistad,Aceptar solicitud de amistad,Accepter l'invitation d'un ami,Accetta richiesta di amicizia,友達リクエストを承認する,친구 요청 수락,Aceitar pedido de amizade,Aceitar pedido de amizade,Принять запрос дружбы,接受好友邀请,接受好友邀請,接受好友邀请 +Corescripts.AvatarContextMenu.PlayerBlocked,,,Player Blocked,Spieler wurde gesperrt,Player Blocked,Jugador bloqueado,Jugador bloqueado,Joueur bloqué,Giocatore bloccato,プレイヤーをブロックしました,플레이어 차단됨,Jogador bloqueado,Jogador bloqueado,Заблокированные игроки,已屏蔽此玩家,已封鎖此玩家,已屏蔽此玩家 +Corescripts.AvatarContextMenu.Chat,,,Chat,Chat,Chat,Chat,Chat,Chat,Chat,チャット,채팅,Chat,Chat,Чат,聊天,聊天,聊天 +Corescripts.AvatarContextMenu.ChatDisabled,,,Chat Disabled,Chat deaktiviert,Chat Disabled,Chat desactivado,Chat desactivado,Chat désactivé,Chat disattivata,チャットを無効にしました,채팅 비활성화,Chat desabilitado,Chat desabilitado,Чат отключен,聊天已停用,聊天停用中,聊天已停用 +Corescripts.AvatarContextMenu.Wave,,,Wave,Winken,Wave,Saludar,Saludar,Saluer,Onda,手を振って挨拶,손 흔들기,Acenar,Acenar,Волна,招手,揮手,招手 +CoreScripts.PremiumModal.Title.PremiumRequired,,,Premium Required,Premium erforderlich,Premium Required,Se requiere Premium,Se requiere Premium,Niveau Premium requis,Premium necessario,Premiumが必要です,Premium 필요,Requer Premium,Requer Premium,Требуется премиум-подписка,需要 Premium 会员,需要 Premium,需要 Premium 会员 +CoreScripts.PremiumModal.Body.Description,,,Roblox Premium will get you:,Durch Roblox Premium erhältst du:,Roblox Premium will get you:,Con Roblox Premium también obtienes:,Con Roblox Premium también obtienes:,"Avec Roblox Premium, vous avez droit à :",Con Roblox Premium otterrai:,Roblox Premiumには以下が含まれています:,Roblox Premium 회원을 위한 다양한 혜택:,Roblox Premium dá direito a:,Roblox Premium dá direito a:,Преимущества премиум-подписки Roblox:,Roblox Premium 将给你:,Roblox Premium 將會給您:,Roblox Premium 将给你: +CoreScripts.PremiumModal.Body.RobuxMonthly,,,450 Robux per month,450 Robux pro Monat,450 Robux per month,450 Robux al mes,450 Robux al mes,450 Robux par mois,450 Robux al mese,1ヶ月に450Robux,매달 450 Robux 획득,450 Robux por mês,450 Robux por mês,450 Robux в месяц;,每月 450 Robux,每個月 450 Robux,每月 450 Robux +CoreScripts.PremiumModal.Body.PremiumOnlyAreas,,,Access to Premium only benefits,Zugriff auf exklusive Premium-Vorteile,Access to Premium only benefits,Acceso a beneficios exclusivos para suscriptores de Premium,Acceso a beneficios exclusivos para suscriptores de Premium,Accédez à des avantages réservés aux membres Premium,Accesso a vantaggi esclusivi Premium,Premium限定特典にアクセス,Premium 전용 혜택 이용,Acesso a benefícios exclusivos,Acesso a benefícios exclusivos,Доступ к возможностям только для премиум-подписчиков,Premium 会员限定福利,獲得 Premium 限定福利,Premium 会员限定福利 +CoreScripts.PremiumModal.Body.RobuxDiscount,,,10% bonus when buying Robux,10% Bonus beim Kauf von Robux,10% bonus when buying Robux,10% de Robux extra en la compra de nuestra moneda virtual,10% de Robux extra en la compra de nuestra moneda virtual,10 % de bonus lors de l'achat de Robux,Bonus del 10% quando acquisti Robux,Robux購入時に10%のボーナス,Robux 구입 시 10% 추가 보너스 증정,Bônus de 10% ao comprar Robux,Bônus de 10% ao comprar Robux,бонус в 10% при покупке Robux.,购买 Robux 时额外增送 10%,購買 Robux 時獲得 10% 額外 Robux,购买 Robux 时额外增送 10% +CoreScripts.PremiumModal.Action.PricePerMonth,,,{price}/month,{price}/Monat,{price}/month,{price} al mes,{price} al mes,{price} par mois,{price}/mese,{price}/月額,{price}/월,{price}/mês,{price}/mês,{price}/месяц,{price} / 月,{price} / 月,{price} / 月 +CoreScripts.PremiumModal.Error.PlatformUnavailable,,,Purchasing Roblox Premium is not supported on your platform. Please use your desktop to purchase Premium.,"Der Kauf von Roblox Premium wird auf deiner Plattform nicht unterstützt. Bitte verwende deinen Desktop-Computer, um Premium zu kaufen.",Purchasing Roblox Premium is not supported on your platform. Please use your desktop to purchase Premium.,La compra de Roblox Premium no es compatible con tu plataforma. Usa un equipo de escritorio para suscribirte.,La compra de Roblox Premium no es compatible con tu plataforma. Usa un equipo de escritorio para suscribirte.,L'achat de Roblox Premium n'est pas pris en charge sur votre plateforme. Veuillez utiliser un ordinateur pour l'acheter.,Non è possibile acquistare Roblox Premium sulla tua piattaforma. Acquista la versione Premium da un computer desktop.,お使いのプラットフォームはRoblox Premiumの購入に対応していません。Premiumを購入するにはデスクトップをお使いください。,Roblox Premium 구매가 지원되지 않는 플랫폼입니다. Premium을 구매하려면 데스크톱을 사용하세요.,A compra da assinatura Roblox Premium não está disponível na sua plataforma. Use o computador para comprar a assinatura Premium.,A compra da assinatura Roblox Premium não está disponível na sua plataforma. Use o computador para comprar a assinatura Premium.,Покупка премиум-подписки Roblox не поддерживается на вашей платформе. Используйте ПК для покупки.,你的平台不支持购买 Roblox Premium。请使用电脑购买 Premium 会员。,您的平台不支援購買 Roblox Premium,請使用電腦購買 Premium。,你的平台不支持购买 Roblox Premium。请使用电脑购买 Premium 会员。 +CoreScripts.PremiumModal.Error.AlreadyPremium,,,"Looks like the developer is trying to prompt you to purchase Roblox Premium, but you are already a Premium member!","Der Entwickler versucht, dich dazu aufzufordern, Roblox Premium zu kaufen, aber du bist bereits ein Premium-Mitglied!","Looks like the developer is trying to prompt you to purchase Roblox Premium, but you are already a Premium member!","Parece que el desarrollador te quiere motivar a que te suscribas a Premium, pero tú ya tienes una suscripción a Premium.","Parece que el desarrollador te quiere motivar a que te suscribas a Premium, pero tú ya tienes una suscripción a Premium.",Il semble que le dévelopeur essaie de vous inviter à acheter Roblox Premium mais vous êtes déjà une membre Premium !,"Sembra che lo sviluppatore stia cercando di farti acquistare Roblox Premium, ma sei già un membro Premium!",開発者がRoblox Premiumを購入するように促しているようですが、すでにPremiumメンバーです!,개발자가 Roblox Premium 구매를 도와드리려 한 것 같네요. 그런데 이미 Premium 회원이세요!,"Parece que o desenvolvedor quer convidar você para comprar uma assinatura Roblox Premium, mas você já é assinante!","Parece que o desenvolvedor quer convidar você para comprar uma assinatura Roblox Premium, mas você já é assinante!","Похоже, разработчик пытается убедить вас купить премиум-подписку Roblox, а она у вас уже есть!",开发者似乎想给你发送购买 Roblox Premium 的提示,但你已经是 Premium 会员了!,開發人員似乎想給您購買 Roblox Premium 的提示,但您已經是 Premium 會員了!,开发者似乎想给你发送购买 Roblox Premium 的提示,但你已经是 Premium 会员了! +CoreScripts.PremiumModal.Error.Unavailable,,,Premium is unavailable at the moment. Please again try later!,Premium ist derzeit nicht verfügbar. Bitte versuche es später wieder!,Premium is unavailable at the moment. Please again try later!,Premium no está disponible en este momento. Inténtalo más tarde.,Premium no está disponible en este momento. Inténtalo más tarde.,Le niveau Premium n'est pas disponible pour le moment. Réessayez plus tard !,Premium non è al momento disponibile. Riprova più tardi!,現在、Premiumが利用できません。あとでお試しください!,Premium이 일시적으로 이용 불가능합니다. 나중에 다시 시도하세요!,Roblox Premium não está disponível no momento. Tente mais tarde!,Roblox Premium não está disponível no momento. Tente mais tarde!,В настоящий момент премиум-подписка недоступна. Повторите попытку позже!,当前无法购买 Premium,请稍后重试。,目前無法購買 Premium,請稍後再試!,当前无法购买 Premium,请稍后重试。 +CoreScripts.PremiumModal.Body.RobuxMonthlyV2,,,{robux} Robux per month,{robux} Robux im Monat,{robux} Robux per month,{robux} Robux al mes,{robux} Robux al mes,{robux} Robux par mois,{robux} Robux al mese,1ヶ月につき{robux} Robux,매월 {robux} Robux 지급,{robux} Robux por mês,{robux} Robux por mês,{robux} Robux в месяц;,每月 {robux} Robux,每月 {robux} Robux,每月 {robux} Robux +CoreScripts.PremiumModal.Error.FailedNativePurchase,,,"Purchase was not complete, please try again.","Kauf wurde nicht abgeschlossen, bitte versuche es erneut.","Purchase was not complete, please try again.",No se ha finalizado la compra. Inténtalo de nuevo.,No se ha finalizado la compra. Inténtalo de nuevo.,"L'achat n'est pas finalisé, réessaie s'il te plaît.","L'acquisto non è stato completato, riprova.",購入は完了していません。もう一度お試しください。,구매를 완료하지 못했습니다. 다시 시도하세요.,A compra não foi concluída. Tente novamente.,A compra não foi concluída. Tente novamente.,"Покупка не удалась, попробуйте еще раз.",购买未完成,请重试。,購買未完成,請重新嘗試。,购买未完成,请重试。 +CoreScripts.PremiumModal.Title.Error,,,Error,Fehler,Error,Error,Error,Erreur,Errore,エラー,오류,Erro,Erro,Ошибка,错误,錯誤,错误 +CoreScripts.TopBar.Leaderboard,,,Leaderboard,Bestenliste,Leaderboard,Clasificación,Clasificación,Tableau des scores,Classifica,リーダーボード,리더보드,Classificação,Classificação,Список лидеров,排行榜,排行榜,排行榜 +CoreScripts.TopBar.Emotes,,,Emotes,Emotes,Emotes,Emotes,Emotes,Emotes,Emoticon,エモート,감정 표현,Emotes,Emotes,Эмоции,动作,動作,动作 +CoreScripts.TopBar.Inventory,,,Inventory,Inventar,Inventory,Inventario,Inventario,Inventaire,Inventario,インベントリ,인벤토리,Inventário,Inventário,Инвентарь,道具库,道具欄,道具库 +CoreScripts.TopBar.Leave,,,Leave,Verlassen,Leave,Salir,Salir,Quitter,Esci,終了,게임 종료,Sair,Sair,Выйти,离开,離開,离开 +CoreScripts.TopBar.Chat,,,Chat,Chat,Chat,Chat,Chat,Chat,Chat,チャット,채팅,Chat,Chat,Чат,聊天,聊天,聊天 +CoreScripts.TopBar.QuickMenu,,,Quick Menu,Schnellmenü,Quick Menu,Menú rápido,Menú rápido,Menu rapide,Menu rapido,クイックメニュー,퀵 메뉴,Menu rápido,Menu rápido,Быстрое меню,快捷菜单,快捷選單,快捷菜单 +CoreScripts.TopBar.Menu,,,Menu,Menü,Menu,Menú,Menú,Menu,Menu,メニュー,메뉴,Menu,Menu,Меню,菜单,選單,菜单 +CoreScripts.TopBar.Respawn,,,Respawn Character,Charakter respawnen,Respawn Character,Regenerar personaje,Regenerar personaje,Reproduction du personnage,Rigenera il personaggio,キャラクターをリスポーンする,캐릭터 리스폰,Regenerar personagem,Regenerar personagem,Возродить персонажа,重生角色,重生角色,重生角色 +CoreScripts.TopBar.Back,,,Back,Zurück,Back,Volver,Volver,Retour,Indietro,戻る,뒤로,Voltar,Voltar,Назад,返回,返回,返回 +CoreScripts.TopBar.GameNamePlaceHolder,,,Game,Spiel,Game,Juego,Juego,Jeu,Gioco,ゲーム,게임,Jogo,Jogo,Игра,游戏,遊戲,游戏 +Feature.SettingsHub.Label.LoadingFriendsListFailed,,,"An error occurred, please try again later.",Ein Fehler ist aufgetreten. Bitte versuche es später erneut.,"An error occurred, please try again later.",Se ha producido un error. Inténtalo de nuevo más tarde.,Se ha producido un error. Inténtalo de nuevo más tarde.,"Une erreur s'est produite, veuillez reéssayer plus tard.",Si è verificato un errore. Riprova più tardi.,エラーが発生しました。後でもう一度お試しください。,오류가 발생했어요. 나중에 다시 시도하세요.,"Ocorreu um erro, tente novamente mais tarde.","Ocorreu um erro, tente novamente mais tarde.",Произошла ошибка. Повторите попытку позже.,发生错误,请稍候重试。,發生錯誤,請稍後再試。,发生错误,请稍候重试。 +Feature.SettingsHub.Label.LeaveGame,,,Are you sure you want to leave the game?,Möchtest du das Spiel wirklich verlassen?,Are you sure you want to leave the game?,¿Seguro que deseas salir del juego?,¿Seguro que deseas salir del juego?,Voulez-vous vraiment quitter le jeu ?,Vuoi davvero uscire dal gioco?,ゲームを終了しますか?,게임을 종료하시겠습니까?,Quer mesmo sair do jogo?,Quer mesmo sair do jogo?,Выйти из игры?,是否确定要离开游戏?,確定離開遊戲?,是否确定要离开挑战? +Feature.SettingsHub.Action.CancelSearch,,,Cancel,Abbrechen,Cancel,Cancelar,Cancelar,Annuler,Annulla,キャンセル,취소,Cancelar,Cancelar,Отмена,取消,取消,取消 +Feature.SettingsHub.Label.DontLeaveButton,,,Don't Leave,Nicht verlassen,Don't Leave,No salir,No salir,Ne pas quitter,Non uscire,終了しない,계속하기,Não sair,Não sair,Не выходить,不要离开,不要離開,不要离开 +Feature.SettingsHub.Label.GameInviteError,,,Game invite could not be sent. Please try again later.,Spieleinladung konnte nicht gesendet werden. Bitte versuche es später erneut.,Game invite could not be sent. Please try again later.,No se ha podido enviar la invitación al juego. Inténtalo de nuevo más tarde.,No se ha podido enviar la invitación al juego. Inténtalo de nuevo más tarde.,L'invitation au jeu n'a pas pu être envoyée. Veuillez réessayer plus tard.,Non è stato possibile inviare l'invito di gioco. Riprova più tardi.,ゲームへの招待を送信できませんでした。しばらくしてからやり直してください。,게임 초대를 보내지 못했습니다. 나중에 다시 시도하세요.,O convite de jogo não pôde ser enviado. Tente novamente mais tarde.,O convite de jogo não pôde ser enviado. Tente novamente mais tarde.,"Приглашение от игры не было послано. Пожалуйста, повторите попытку.",无法发送游戏邀请。请稍后重试。,無法傳送遊戲邀請,請稍後再試。,无法发送游戏邀请。请稍后重试。 +Feature.SettingsHub.Label.ModeratedInviteError,,,Game invite was moderated and not sent.,Spieleinladung wurde von einem Moderator angepasst und nicht gesendet.,Game invite was moderated and not sent.,La invitación al juego ha sido moderada y no se ha enviado.,La invitación al juego ha sido moderada y no se ha enviado.,L'invitation au jeu a été modérée et n'a pu être envoyée.,L'invito di gioco è stato moderato e non inviato.,ゲームへの招待は、規制により送信されませんでした。,게임 초대가 검토 요청을 받아 전송되지 않았어요.,O convite de jogo foi moderado e não foi enviado.,O convite de jogo foi moderado e não foi enviado.,Приглашение в игру было проверено и не было отправлено.,游戏邀请已被过滤,未能发送。,遊戲邀請遭到過濾,無法傳送。,游戏邀请已被过滤,未能发送。 +Feature.SettingsHub.Action.InviteFriend,,,Invite,Einladen,Invite,Invitar,Invitar,Inviter,Invita,招待,초대,Convidar,Convidar,Пригласить,邀请,邀請,邀请 +Feature.SettingsHub.Heading.InviteFriends,,,Invite Friends,Freunde einladen,Invite Friends,Invitar amigos,Invitar amigos,Inviter des amis,Invita amici,友達を招待,친구 초대,Convidar amigos,Convidar amigos,Пригласить друзей,邀请好友,邀請好友,邀请好友 +Feature.SettingsHub.Action.InviteFriendsToPlay,,,Invite friends to play,Lade Freunde zum Spielen ein,Invite friends to play,Invitar amigos al juego,Invitar amigos al juego,Inviter des amis à jouer,Invita gli amici a giocare,友達を招待してプレイ,함께 플레이할 친구 초대,Convidar amigos para o jogo,Convidar amigos para o jogo,Пригласить друзей в игру,邀请好友加入游戏,邀請好友,邀请好友加入挑战 +Feature.SettingsHub.Label.Invited,,,Invited,Eingeladen,Invited,Invitado,Invitado,Invité,Invitato,招待済み,초대 완료,Convidado,Convidado,Приглашено,已邀请,已邀請,已邀请 +Feature.SettingsHub.Label.LeaveButton,,,Leave,Verlassen,Leave,Salir,Salir,Quitter,Esci,終了,게임 종료,Sair,Sair,Выйти,离开,離開,离开 +Feature.SettingsHub.Label.NoFriendsScreen,,,Make friends so you can invite them to play with you!,"Finde Freunde und lade sie ein, mit dir zu spielen!",Make friends so you can invite them to play with you!,¡Haz amigos para que los invites a jugar contigo!,¡Haz amigos para que los invites a jugar contigo!,Faites des amis pour que vous puissiez les inviter à jouer!,"Trova nuovi amici, così potrai invitarli a giocare con te!",ゲームで一緒にプレイするために友達を招待しましょう!,친구를 사귄 후 초대하여 함께 플레이하세요!,Faça amigos e os convide para jogar,Faça amigos e os convide para jogar,"Найдите друзей, чтобы играть вместе с ними!",认识更多朋友,邀请他们和你一起玩游戏!,結交好友,開始同樂!,认识更多朋友,邀请他们和你一起加入挑战! +Feature.SettingsHub.Label.Moderated,,,Moderated,Von einem Moderator angepasst,Moderated,Moderado,Moderado,Modéré,Moderato,規制対象,검토 요청됨,Moderado,Moderado,Проверено,已过滤,已遭過濾,已过滤 +Feature.SettingsHub.Label.InviteSearchNoResults,,,No results found,Keine Suchergebnisse,No results found,Sin resultados,Sin resultados,Aucun résultat trouvé,Nessun risultato trovato,結果が見つかりませんでした,검색 결과가 없습니다,Nenhum resultado,Nenhum resultado,Нет результатов,未找到搜索结果,找不到結果,未找到搜索结果 +Feature.SettingsHub.Label.RecentPlayed,,,Recently Played,Kürzlich gespielt,Recently Played,Recently Played,Recently Played,Recently Played,Recently Played,最近プレイしたもの,최근 플레이,Jogado recentemente,Jogado recentemente,Recently Played,Recently Played,最近玩過,Recently Played +Feature.SettingsHub.Label.SearchForFriendsPlaceholder,,,Search for friends,Suche nach Freunden,Search for friends,Buscar amigos,Buscar amigos,Chercher des amis,Cerca amici,友達を検索,친구 검색,Procurar amigos,Procurar amigos,Поиск друзей,搜索好友,搜尋好友,搜索好友 +Feature.SettingsHub.Label.Sending,,,Sending,Wird gesendet,Sending,Enviando,Enviando,En cours d'envoi,Invio...,送信しています,전송 중,Enviando,Enviando,Отправка,正在发送,正在傳送,正在发送 +Feature.SettingsHub.Action.InviteFriendsBack,,,Back,Zurück,Back,Volver,Volver,Arrière,Indietro,戻る,뒤로,Voltar,Voltar,Назад,返回,返回,返回 +Feature.SettingsHub.Message.InviteToGameTitle,,,Come join me in {PLACENAME},Komm mit mir zu {PLACENAME},Come join me in {PLACENAME},Únete a mí en {PLACENAME},Únete a mí en {PLACENAME},Rejoins-moi ici : {PLACENAME},Raggiungimi qui: {PLACENAME},{PLACENAME} で一緒にゲームしましょう。,{PLACENAME}에서 함께 즐겨요,Junte-se a mim em {PLACENAME},Junte-se a mim em {PLACENAME},Присоединяйтесь ко мне в: {PLACENAME},来和我一起加入“{PLACENAME} ”吧!,來 {PLACENAME} 和我一起同樂吧!,来和我一起加入“{PLACENAME} ”吧! +Feature.SettingsHub.TouchMovementMode.DPad,,,DPad,Steuerkreuz,DPad,Cruceta,Cruceta,DPad,Croce direzionale,DPad,D패드,Direcional,Direcional,Крестовина,十字键,方向鍵,十字键 +Feature.SettingsHub.Default.DynamicThumbstick,,,Default (Dynamic Thumbstick),Standard (Dynamischer Thumbstick),Default (Dynamic Thumbstick),Predeterminado (stick dinámico),Predeterminado (stick dinámico),Par défaut (Joystick),Predefinita (Levetta dinamica),デフォルト (サムスティック),기본값(다이내믹 엄지스틱),Padrão (direcional analógico dinâmico),Padrão (direcional analógico dinâmico),По умолчанию (динамический аналоговый стик),默认(动态摇杆),預設(動態類比搖桿),默认(动态摇杆) +Feature.SettingsHub.DefaultKeyboard,,,Default (Keyboard),Standard (Tastatur),Default (Keyboard),Predeterminado (teclado),Predeterminado (teclado),Par défaut (Clavier),Predefinita (Tastiera),デフォルト (キーボード),기본값(키보드),Padrão (teclado),Padrão (teclado),По умолчанию (клавиатура),默认(键盘),預設(鍵盤),默认(键盘) +InGame.CommonUI.Title.Warning,,,Warning,Warnung,Warning,Advertencia,Advertencia,Attention,Attenzione,警告,경고,Aviso,Aviso,Предупреждение,警告,警告,警告 +InGame.CommonUI.Title.Error,,,Error,Fehler,Error,Error,Error,Erreur,Errore,エラー,오류,Erro,Erro,Ошибка,错误,錯誤,错误 +InGame.CommonUI.Button.Retry,,,Retry,Erneut versuchen,Retry,Reintentar,Reintentar,Réessaye,Riprova,再試行,다시 시도,Tentar de novo,Tentar de novo,Повторить,重试,重試,重试 +InGame.CommonUI.Button.Ok,,,Ok,Okay,Ok,Aceptar,Aceptar,Ok,Ok,OK,확인,Ok,Ok,OK,确定,確定,确定 +InGame.CommonUI.Title.Alert,,,Alert,Warnung,Alert,Alerta,Alerta,Alerte,Avviso,注意,경고,Alerta,Alerta,Тревога,警告,注意,警告 +InGame.CommonUI.Label.Default,,,Default,Standard,Default,Predeterminado,Predeterminado,Par défaut,Predefinito,デフォルト,기본,Padrão,Padrão,По умолчанию,默认,預設,默认 +InGame.CommonUI.Label.Custom,,,Custom,Individualisiert,Custom,Personalizado,Personalizado,Personnalisé,Personalizzato,カスタム,사용자 정의,Personalizado,Personalizado,Особенное,自定义,自訂,自定义 +InGame.CommonUI.Label.PossiblyCustom,,,Possibly Custom,Möglicherweise benutzerdefiniert,Possibly Custom,Posiblemente una versión personalizada,Posiblemente una versión personalizada,Peut être personnalisé,Forse personalizzato,カスタムの可能性あり,사용자 정의일 수 있음,Possivelmente personalizado,Possivelmente personalizado,Возможно особенное,可能自定义,可能自訂,可能自定义 +InGame.CommonUI.Label.CustomOld,,,Custom Old,Benutzerdefiniert alt,Custom Old,Posiblemente un versión antigua personalizada,Posiblemente un versión antigua personalizada,Peut être une ancienne version personnalisée,Vecchia personalizzazione,旧カスタム,이전 버전의 사용자 정의,Personalizado antigo,Personalizado antigo,Старое особенное,自定义旧版,舊版自訂,自定义旧版 +InGame.ConnectionError.DisconnectCloudEditKick,,,Lost connection to Team Create. Please reconnect. ,Verbindung zu Team Create verloren. Bitte wieder verbinden. ,Lost connection to Team Create. Please reconnect. ,Se ha perdido la conexión a Creación en equipo. Vuelve a conectarte. ,Se ha perdido la conexión a Creación en equipo. Vuelve a conectarte. ,Connexion perdue lors de la création d'équipe. Veuillez vous reconnecter. ,Connessione persa con la creazione della squadra. È necessario riconnettersi. ,チームクリエイトへの接続が切断されました。再接続してください。 ,팀 만들기 중 접속이 끊어졌습니다. 다시 접속하세요.,Conexão perdida com Team Create. Reconecte-se. ,Conexão perdida com Team Create. Reconecte-se. ,"Потеряно соединение с командой создателей. Пожалуйста, установите соединение заново. ",与团队创作(Team Create)建立的连接被中断。请重新连接。,中斷與集體創作的連線,請重新連線。,与团队创作(Team Create)建立的连接被中断。请重新连接。 +InGame.ConnectionError.DisconnectWrongVersion,,,Your version of Roblox may be out of date. Please update Roblox and try again.,Deine Version von Roblox ist eventuell veraltet. Bitte aktualisiere Roblox und versuche es erneut.,Your version of Roblox may be out of date. Please update Roblox and try again.,Tu versión de Roblox puede que esté obsoleta. Actualízala e inténtalo de nuevo.,Tu versión de Roblox puede que esté obsoleta. Actualízala e inténtalo de nuevo.,Votre version de Roblox n'est plus à jour. Veuillez mettre Roblox à jour et réessayer.,La tua versione di Roblox potrebbe essere obsoleta. Aggiorna Roblox e prova di nuovo.,Robloxのバージョンが最新ではありません。Robloxをアップデートして、もう一度お試しください。,회원님이 사용 중인 Roblox 버전이 오래되었습니다. 업데이트 후 다시 시도하세요.,Sua versão do Roblox pode estar desatualizada. Atualize o Roblox e tente novamente.,Sua versão do Roblox pode estar desatualizada. Atualize o Roblox e tente novamente.,"Ваша версия Roblox, возможно, устарела. Пожалуйста, установите обновление для Roblox и попробуйте снова.",你的 Roblox 可能仍是老版本。请更新 Roblox 并重试。,您的 Roblox 版本可能過時,請更新 Roblox 後重新嘗試。,你的罗布乐思版本可能过时,请更新罗布乐思后重试。 +InGame.ConnectionError.TeleportFailure,,,Teleport failed due to an unexpected error.,Das Teleportieren ist aufgrund eines unerwarteten Fehlers fehlgeschlagen.,Teleport failed due to an unexpected error.,Error en la teletransportación debido a un problema inesperado.,Error en la teletransportación debido a un problema inesperado.,Échec de la téléportation en raison d'une erreur inattendue.,Teletrasporto non riuscito a causa di un errore imprevisto.,予期せぬエラーによりテレポートできませんでした。,예기치 못한 오류로 텔레포트 실패.,O teleporte falhou devido a um erro inesperado.,O teleporte falhou devido a um erro inesperado.,Произошла непредвиденная ошибка телепорта.,由于未知错误,传送失败。,發生錯誤,無法傳送。,由于未知错误,传送失败。 +InGame.ConnectionError.DisconnectDuplicatePlayer,,,Same account launched game from different device. Reconnect if you prefer to use this device.,"Das Spiel wurde mit demselben Konto auf einem anderen Gerät gestartet. Verbinde dich wieder, wenn du dieses Gerät benutzen möchtest.",Same account launched game from different device. Reconnect if you prefer to use this device.,Se ha utilizado la misma cuenta para lanzar el juego en un dispositivo diferente. Vuelve a conectarte si prefieres utilizar este dispositivo.,Se ha utilizado la misma cuenta para lanzar el juego en un dispositivo diferente. Vuelve a conectarte si prefieres utilizar este dispositivo.,Ce compte a lancé le jeu depuis un autre appareil. Veuillez vous reconnecter si vous préférez utiliser celui-ci.,Lo stesso account ha avviato il gioco da un altro dispositivo. Riconnettiti se preferisci usare questo dispositivo.,同じアカウントで別のデバイスからゲームを起動しました。このデバイスを使う場合は再接続してください。,다른 기기에서 같은 계정으로 게임에 접속했습니다. 본 기기를 사용하기를 원하시면 다시 접속하세요.,A mesma conta iniciou um jogo de um dispositivo diferente. Reconecte-se se você prefere usar este dispositivo.,A mesma conta iniciou um jogo de um dispositivo diferente. Reconecte-se se você prefere usar este dispositivo.,"Игра была запущена с той же учетной записи, но с другого устройства. Выберите нужное для вас устройство.",同一账号已在另一设备上运行游戏。如果你偏向于使用此设备,请重新连接。,此帳號已從其它裝置開啟此遊戲。若您想使用此裝置,請重新連線。,同一账号已在另一设备上运行游戏。如果你偏向于使用此设备,请重新连接。 +InGame.ConnectionError.DisconnectNewSecurityKeyMismatch,,,Lost connection due to an error.,Die Verbindung ist aufgrund eines Fehlers abgebrochen.,Lost connection due to an error.,Se ha perdido la conexión debido a un error.,Se ha perdido la conexión debido a un error.,Connexion perdue suite à une erreur.,Connessione persa a causa di un errore.,エラーにより接続が切断されました。,오류로 인해 연결 끊김,Conexão perdida devido a um erro.,Conexão perdida devido a um erro.,Из-за ошибки пропало соединение.,由于某项错误,连接被中断。,發生錯誤,連線中斷。,由于某项错误,连接被中断。 +InGame.ConnectionError.PlacelaunchUserLeft,,,The user you attempted to join has left the game.,"Der Benutzer, dem du beitreten wolltest, hat das Spiel verlassen.",The user you attempted to join has left the game.,El usuario al que querías unirte ha salido del juego.,El usuario al que querías unirte ha salido del juego.,L'utilisateur que vous avez essayé de rejoindre a quitté le jeu.,L'utente che hai provato a raggiungere ha abbandonato il gioco.,参加しようとしていたユーザーがゲームから退出しました。,함께 플레이하려던 사용자가 게임을 나갔습니다.,O usuário ao qual você estava tentando se juntar saiu do jogo.,O usuário ao qual você estava tentando se juntar saiu do jogo.,Соединяющийся с вами игрок вышел из игры,你尝试加入的用户已离开游戏。,您嘗試跟隨的使用者已離開遊戲。,你尝试加入的用户已离开游戏。 +InGame.ConnectionError.TeleportGameFull,,,"Teleport failed, server is full.","Teleportieren fehlgeschlagen, der Server ist voll.","Teleport failed, server is full.",Error en la teletransportación. El servidor está lleno.,Error en la teletransportación. El servidor está lleno.,Échec de la téléportation ; le serveur est complet.,"Teletrasporto non riuscito, il server è al completo.",テレポートできませんでした。サーバーが満員です。,텔레포트 실패. 서버가 만원입니다.,Falha no teleporte. Servidor lotado.,Falha no teleporte. Servidor lotado.,"Сервер переполнен, телепорт не удался.",服务器已满,传送失败。,伺服器額滿,無法傳送。,服务器已满,传送失败。 +InGame.ConnectionError.PlacelaunchHashExpired,,,This game is currently unavailable. Please try again later.,Dieses Spiel ist derzeit nicht verfügbar. Bitte versuche es später erneut.,This game is currently unavailable. Please try again later.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,"Ce jeu est indisponible pour le moment, veuillez réessayer plus tard.",Questo gioco non è al momento disponibile. Riprova più tardi.,このゲームは現在利用できません。後でもう一度お試しください。,이 게임은 현재 플레이할 수 없습니다. 나중에 다시 시도하세요.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Эта игра сейчас недоступна. Повторите попытку позже.,此游戏目前不可用,请稍后重试。,此遊戲目前不開放,請稍後再試。,此游戏目前不可用,请稍后重试。 +InGame.ConnectionError.DisconnectPlayerless,,,Server was shutting down as you tried to connect. Please try again.,Der Server wurde während deines Verbindungsversuchs heruntergefahren. Bitte versuche es erneut.,Server was shutting down as you tried to connect. Please try again.,El servidor se estaba cerrando mientras tratabas de conectarte. Inténtalo de nuevo.,El servidor se estaba cerrando mientras tratabas de conectarte. Inténtalo de nuevo.,Le serveur a fermé lors de votre tentative de connexion. Veuillez réessayer.,Il server è stato chiuso mentre provavi a connetterti. Riprova più tardi.,接続中にサーバーがシャットダウンしました。もう一度お試しください。,회원님이 연결하려고 하는 서버가 종료되었습니다. 다시 시도하세요. ,O servidor estava sendo desligado quando você tentou se conectar. Tente de novo mais tarde.,O servidor estava sendo desligado quando você tentou se conectar. Tente de novo mais tarde.,"В попытке соединения с сервером отказано. Пожалуйста, повторите ее позже.",服务器在你尝试连接时被关闭。请重试。,伺服器在連線過程關閉,請重新嘗試。,服务器在你尝试连接时被关闭。请重试。 +InGame.ConnectionError.TeleportGameNotFound,,,Attempted to teleport to a place that does not exist.,"Du hast versucht, dich an einen nicht existierenden Ort zu teleportieren.",Attempted to teleport to a place that does not exist.,Se ha intentado teletransportarse a un lugar que no existe.,Se ha intentado teletransportarse a un lugar que no existe.,Vous avez tenté de vous téléporter dans un emplacement qui n'existe pas.,Tentativo di teletrasporto verso una località che non esiste.,存在しないプレースへのテレポートを試みました。,존재하지 않는 장소로 텔레포트를 시도했습니다.,Tentativa de se teleportar para um local que não existe.,Tentativa de se teleportar para um local que não existe.,Попытка телепорта в несуществующее место.,尝试传送至一个不存在的场景。,您嘗試傳送的空間不存在。,尝试传送至一个不存在的场景。 +InGame.ConnectionError.DisconnectErrors,,,Lost connection due to an unknown error. Please reconnect.,Die Verbindung ist aufgrund eines unbekannten Fehlers abgebrochen. Bitte wieder verbinden.,Lost connection due to an unknown error. Please reconnect.,Se ha perdido la conexión debido a un error desconocido. Vuelve a conectarte.,Se ha perdido la conexión debido a un error desconocido. Vuelve a conectarte.,Connexion perdue suite à une erreur inconnue. Veuillez vous reconnecter.,Connessione persa a causa di un errore sconosciuto. Riconnettiti.,予期せぬエラーにより接続が切断されました。再接続してください。,알 수 없는 오류로 연결 끊김. 다시 접속하세요.,Conexão perdida devido a um erro desconhecido. Reconecte-se.,Conexão perdida devido a um erro desconhecido. Reconecte-se.,"Пропало соединение из-за неизвестной ошибки. Пожалуйста, установите соединение снова.",由于未知错误,连接被中断。请重新连接。,發生錯誤,連線中斷。請重新連線。,由于未知错误,连接被中断。请重新连接。 +InGame.ConnectionError.PlacelaunchUnauthorized,,,You do not have permission to join this game. ,"Du hast keine Berechtigung, diesem Spiel beizutreten. ",You do not have permission to join this game. ,No tienes permiso para unirte a este juego. ,No tienes permiso para unirte a este juego. ,Vous n'avez pas la permission de rejoindre ce jeu. ,Non hai i permessi necessari per partecipare a questa partita. ,このゲームに参加する権限がありません。 ,본 게임에 참가할 권한이 없습니다.,Você não tem permissão para entrar neste jogo. ,Você não tem permissão para entrar neste jogo. ,Не разрешено присоединяться к этой игре. ,你没有加入此游戏的权限。,您沒有加入此遊戲的權限。,你没有加入此游戏的权限。 +InGame.ConnectionError.DisconnectDuplicateTicket,,,Lost connection due to an error. Multiple connections detected from same account.,Die Verbindung ist aufgrund eines Fehlers abgebrochen. Mehrere Verbindungen desselben Kontos erkannt.,Lost connection due to an error. Multiple connections detected from same account.,Se ha perdido la conexión debido a un error. Se han detectado múltiples conexiones provenientes de la misma cuenta.,Se ha perdido la conexión debido a un error. Se han detectado múltiples conexiones provenientes de la misma cuenta.,Connexion perdue suite à une erreur ; plusieurs connexions détectées depuis le même compte.,Connessione persa a causa di un errore. Sono state rilevate diverse connessioni dallo stesso account.,エラーにより接続が切断されました。同じアカウントからの複数の接続を検出しました。,오류로 인해 연결이 끊어졌습니다. 동일 계정으로 다수의 접속이 발견되었습니다.,Conexão perdida devido a um erro. Várias conexões detectadas da mesma conta.,Conexão perdida devido a um erro. Várias conexões detectadas da mesma conta.,Из-за ошибки пропало соединение. С этой учетной записи следуют многочисленные попытки входа.,由于某项错误,连接被中断。检测到来自同一帐户的多个连接。,發生錯誤,連線中斷。偵測到多條來自同一帳號的連線。,由于某项错误,连接被中断。检测到来自同一帐户的多个连接。 +InGame.ConnectionError.DisconnectBadhash,,,Lost connection due to an error.,Die Verbindung ist aufgrund eines Fehlers abgebrochen.,Lost connection due to an error.,Se ha perdido la conexión debido a un error.,Se ha perdido la conexión debido a un error.,Connexion perdue suite à une erreur.,Connessione persa a causa di un errore.,エラーにより接続が切断されました。,오류로 인해 접속이 끊김,Conexão perdida devido a um erro.,Conexão perdida devido a um erro.,Из-за ошибки пропало соединение.,由于某项错误,连接被中断。,發生錯誤,連線中斷。,由于某项错误,连接被中断。 +InGame.ConnectionError.DisconnectSendPacketError,,,"There was a problem sending data, please reconnect.","Beim Senden der Daten ist ein Problem aufgetreten, bitte wieder verbinden.","There was a problem sending data, please reconnect.",Ha habido un problema al enviar los datos. Vuelve a conectarte.,Ha habido un problema al enviar los datos. Vuelve a conectarte.,"Une erreur s'est produite lors de l'envoi des données, veuillez vous reconnecter.","C'è stato un problema durante l'invio dei dati, è necessario riconnettersi.",データを送信中に問題が発生しました。再接続してください,데이터 전송 중 오류가 발생했어요. 다시 접속하세요.,Ocorreu um erro ao enviar os dados. Reconecte-se.,Ocorreu um erro ao enviar os dados. Reconecte-se.,"Проблемы с отправкой данных, пожалуйста, установите соединение повторно.",发送数据时出现问题,请重新连接。,傳送資料時發生錯誤,請重新連線。,发送数据时出现问题,请重新连接。 +InGame.ConnectionError.DisconnectOnRemoteSysStats,,,You have been kicked due to unexpected client behavior.,Du wurdest aufgrund unerwartetem Verhalten des Clients gekickt.,You have been kicked due to unexpected client behavior.,Has sido expulsado debido a un comportamiento inesperado del cliente.,Has sido expulsado debido a un comportamiento inesperado del cliente.,Vous avez été expulsé(e) à cause d'un comportement inattendu du client.,Sei stato espulso a causa di un comportamento imprevisto del client.,クライアントの予期せぬ動作により、ゲームから外されました。,예기치 못한 고객 행동으로 강제 퇴장당하셨습니다.,Você foi expulso devido a um comportamento inesperado do cliente.,Você foi expulso devido a um comportamento inesperado do cliente.,Вас исключили из игры за неподобающее поведение.,由于未知客户端行为,你已被踢出。,用戶端異常,您已被踢出。,由于未知客户端行为,你已被踢出。 +InGame.ConnectionError.DisconnectTimeout,,,Your connection timed out. Check your internet connection and try again.,Zeitüberschreitung der Verbindung. Bitte überprüfe deine Internetverbindung und versuche es erneut.,Your connection timed out. Check your internet connection and try again.,Se ha agotado el tiempo de la conexión. Comprueba tu conexión a internet e inténtalo de nuevo.,Se ha agotado el tiempo de la conexión. Comprueba tu conexión a internet e inténtalo de nuevo.,Votre connexion a été perdue. Vérifiez que vous disposez d'un accès à Internet et réessayez.,La tua connessione è scaduta. Controlla la tua connessione a Internet e riprova.,接続がタイムアウトしました。インターネット接続をチェックしてもう一度お試しください。,연결 대기 시간 초과. 인터넷 연결을 확인하고 다시 시도하세요.,Sua conexão expirou. Verifique sua conexão de internet e tente novamente.,Sua conexão expirou. Verifique sua conexão de internet e tente novamente.,Время для подключения истекло. Проверьте свое подключение к сети Интернет и повторите попытку.,你的连接已超时。请检查你的网络连接并重试。,連線逾時,請檢查您的網路連線後重新嘗試。,你的连接已超时。请检查你的网络连接并重试。 +InGame.ConnectionError.PlacelaunchHttpError,,,We are experiencing technical difficulties. Please try again later.,Wir haben technische Schwierigkeiten. Bitte versuche es später erneut.,We are experiencing technical difficulties. Please try again later.,Tenemos dificultades técnicas. Inténtalo de nuevo más tarde.,Tenemos dificultades técnicas. Inténtalo de nuevo más tarde.,Nous rencontrons des problèmes techniques. Veuillez réessayer plus tard.,Stiamo riscontrando delle difficoltà tecniche. Riprova più tardi.,技術的な問題が発生しています。後でもう一度お試しください。,기술적인 문제가 발생했습니다. 나중에 다시 시도하세요.,Estamos tendo dificuldades técnicas. Tente de novo mais tarde.,Estamos tendo dificuldades técnicas. Tente de novo mais tarde.,Сейчас технические проблемы. Повторите попытку позже.,我们遇到技术困难 。请稍后重试。,我們遇到技術困難,請稍後再試。,我们遇到技术困难 。请稍后重试。 +InGame.ConnectionError.DisconnectSecurityKeyMismatch,,,Lost connection due to an error.,Die Verbindung ist aufgrund eines Fehlers abgebrochen.,Lost connection due to an error.,Se ha perdido la conexión debido a un error.,Se ha perdido la conexión debido a un error.,Connexion perdue suite à une erreur.,Connessione persa a causa di un errore.,エラーにより接続が切断されました。,오류로 인해 접속이 끊겼습니다.,Conexão perdida devido a um erro.,Conexão perdida devido a um erro.,Из-за ошибки пропало соединение.,由于某项错误,连接被中断。,發生錯誤,連線中斷。,由于某项错误,连接被中断。 +InGame.ConnectionError.TeleportErrors,,,Teleport failed due to an unknown error.,Das Teleportieren ist aufgrund eines unbekannten Fehlers fehlgeschlagen.,Teleport failed due to an unknown error.,Error en la teletransportación debido a un problema inesperado.,Error en la teletransportación debido a un problema inesperado.,Échec de la téléportation en raison d'une erreur inconnue.,Teletrasporto non riuscito a causa di un errore sconosciuto.,不明なエラーによりテレポートできませんでした。,알 수 없는 오류로 인해 텔레포트 실패.,O teleporte falhou devido a um erro desconhecido.,O teleporte falhou devido a um erro desconhecido.,Произошла неопознанная ошибка телепорта.,由于未知错误,传送失败。,發生錯誤,無法傳送。,由于未知错误,传送失败。 +InGame.ConnectionError.PlacelaunchDisabled,,,This game is currently unavailable. Please try again later.,Dieses Spiel ist derzeit nicht verfügbar. Bitte versuche es später erneut.,This game is currently unavailable. Please try again later.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,"Ce jeu est indisponible pour le moment, veuillez réessayer plus tard.",Questo gioco non è al momento disponibile. Riprova più tardi.,このゲームは現在利用できません。後でもう一度お試しください。,이 게임은 현재 플레이할 수 없습니다. 나중에 다시 시도하세요.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Эта игра сейчас недоступна. Повторите попытку позже.,此游戏目前不可用,请稍后重试。,此遊戲目前不開放,請稍後再試。,此游戏目前不可用,请稍后重试。 +InGame.ConnectionError.TeleportGameEnded,,,"Teleport failed, server is no longer available.","Teleportieren fehlgeschlagen, der Server ist nicht mehr verfügbar.","Teleport failed, server is no longer available.",Error en la teletransportación. El servidor ya no está disponible.,Error en la teletransportación. El servidor ya no está disponible.,Échec de la téléportation ; le serveur n'est plus disponible.,"Teletrasporto non riuscito, il server non è più disponibile.",テレポートできませんでした。サーバーが利用できません。,텔레포트 실패. 서버를 더 이상 이용할 수 없습니다.,Falha no teleporte. O servidor não está mais disponível.,Falha no teleporte. O servidor não está mais disponível.,"Телепорт не удался, сервер недоступен.",服务器已不可用,传送失败。,伺服器已關閉,無法傳送。,服务器已不可用,传送失败。 +InGame.ConnectionError.DisconnectRejoin,,,Same account launched game from different device. Reconnect if you prefer to use this device.,"Das Spiel wurde mit demselben Konto auf einem anderen Gerät gestartet. Verbinde dich wieder, wenn du dieses Gerät benutzen möchtest.",Same account launched game from different device. Reconnect if you prefer to use this device.,Se ha utilizado la misma cuenta para lanzar el juego en un dispositivo diferente. Vuelve a conectarte si prefieres utilizar este dispositivo.,Se ha utilizado la misma cuenta para lanzar el juego en un dispositivo diferente. Vuelve a conectarte si prefieres utilizar este dispositivo.,Ce compte a lancé le jeu depuis un autre appareil. Veuillez vous reconnecter si vous préférez utiliser celui-ci.,Lo stesso account ha avviato il gioco da un altro dispositivo. Riconnettiti se preferisci usare questo dispositivo.,同じアカウントで別のデバイスからゲームを起動しました。このデバイスを使う場合は再接続してください。,다른 기기에서 같은 계정으로 게임에 접속했습니다. 본 기기를 이용하기를 원하시면 다시 접속하세요. ,A mesma conta iniciou um jogo de um dispositivo diferente. Reconecte-se se você prefere usar este dispositivo.,A mesma conta iniciou um jogo de um dispositivo diferente. Reconecte-se se você prefere usar este dispositivo.,"Игра была запущена с той же учетной записи, но с другого устройства. Выберите нужное для вас устройство.",同一账号已在另一设备上运行游戏。如果你偏向于使用此设备,请重新连接。,此帳號已從其它裝置開啟此遊戲。若您想使用此裝置,請重新連線。,同一账号已在另一设备上运行游戏。如果你偏向于使用此设备,请重新连接。 +InGame.ConnectionError.PlacelaunchFlooded,,,The server is currently busy. Please try again.,Der Server ist momentan ausgelastet. Bitte versuche es erneut.,The server is currently busy. Please try again.,El servidor está ocupado en este momento. Inténtelo de nuevo.,El servidor está ocupado en este momento. Inténtelo de nuevo.,Le serveur est occupé. Veuillez réessayer plus tard.,Il server è al momento occupato. Riprova.,現在サーバーが混雑しています。もう一度お試しください。,현재 서버 사용량이 많습니다. 다시 시도하세요.,O servidor está ocupado no momento. Tente de novo.,O servidor está ocupado no momento. Tente de novo.,Сейчас сервер занят. Повторите попытку.,服务器目前繁忙,请稍后重试。,伺服器忙碌中,請稍後再試。,服务器目前繁忙,请稍后重试。 +InGame.ConnectionError.DisconnectProtocolMismatch,,,Your version of Roblox may be out of date. Please update Roblox and try again.,Deine Version von Roblox ist eventuell veraltet. Bitte aktualisiere Roblox und versuche es erneut.,Your version of Roblox may be out of date. Please update Roblox and try again.,Tu versión de Roblox puede que esté obsoleta. Actualízala e inténtalo de nuevo.,Tu versión de Roblox puede que esté obsoleta. Actualízala e inténtalo de nuevo.,Votre version de Roblox n'est plus à jour. Veuillez mettre Roblox à jour et réessayer.,La tua versione di Roblox potrebbe essere obsoleta. Aggiorna Roblox e prova di nuovo.,Robloxのバージョンが最新ではありません。Robloxをアップデートして、もう一度お試しください。,회원님이 사용 중인 Roblox 버전이 오래되었습니다. 업데이트 후 다시 시도하세요.,Sua versão do Roblox pode estar desatualizada. Atualize o Roblox e tente novamente.,Sua versão do Roblox pode estar desatualizada. Atualize o Roblox e tente novamente.,"Ваша версия Roblox, возможно, устарела. Пожалуйста, установите обновление для Roblox и попробуйте снова.",你的 Roblox 可能仍是老版本。请更新 Roblox 并重试。,您的 Roblox 版本可能過時,請更新 Roblox 後重新嘗試。,你的罗布乐思版本可能过时,请更新罗布乐思后重试。 +InGame.ConnectionError.DisconnectRobloxMaintenance,,,Roblox has shut down the server for maintenance. Please try again.,Roblox hat den Server zur Wartung heruntergefahren. Bitte versuche es erneut.,Roblox has shut down the server for maintenance. Please try again.,Roblox ha cerrado el servidor por mantenimiento. Inténtalo de nuevo.,Roblox ha cerrado el servidor por mantenimiento. Inténtalo de nuevo.,Roblox a fermé ce serveur de jeu pour en effectuer la maintenance. Veuillez réessayer.,Roblox ha chiuso questo server per manutenzione. Riprova più tardi.,メンテナンスのため、Robloxによってサーバーがシャットダウンされています。もう一度お試しください。,Roblox가 유지보수를 위해 서버를 강제 종료하였습니다. 다시 시도하세요.,Roblox desligou o servidor para realizar uma manutenção. Tente novamente.,Roblox desligou o servidor para realizar uma manutenção. Tente novamente.,Roblox отключил игровой сервер для техобслуживания. Повторите попытку.,由于系统维护,Roblox 已关闭服务器。请重试。,即將進行維修,伺服器已關閉。請重新嘗試。,由于系统维护,罗布乐思已关闭服务器。请重试。 +InGame.ConnectionError.DisconnectHashTimeout,,,Your connection timed out. Check your internet connection and try again.,Zeitüberschreitung der Verbindung. Bitte überprüfe deine Internetverbindung und versuche es erneut.,Your connection timed out. Check your internet connection and try again.,Se ha agotado el tiempo de la conexión. Comprueba tu conexión a internet e inténtalo de nuevo.,Se ha agotado el tiempo de la conexión. Comprueba tu conexión a internet e inténtalo de nuevo.,Votre connexion a été perdue. Vérifiez que vous disposez d'un accès à Internet et réessayez.,La tua connessione è scaduta. Controlla la tua connessione a Internet e riprova.,接続がタイムアウトしました。インターネット接続をチェックしてもう一度お試しください。,연결 대기 시간 초과. 인터넷 연결 상태를 확인 후 다시 시도하세요.,Sua conexão expirou. Verifique sua conexão de internet e tente novamente.,Sua conexão expirou. Verifique sua conexão de internet e tente novamente.,Время для подключения истекло. Проверьте свое подключение к сети Интернет и повторите попытку.,你的连接已超时。请检查你的网络连接并重试。,連線逾時,請檢查您的網路連線後重新嘗試。,你的连接已超时。请检查你的网络连接并重试。 +InGame.ConnectionError.DisconnectDevMaintenance,,,The game's developer has temporarily shut down the game server. Please try again.,Der Entwickler des Spiels hat den Spielserver vorübergehend heruntergefahren. Bitte versuche es erneut.,The game's developer has temporarily shut down the game server. Please try again.,El desarrollador ha cerrado temporalmente el servidor del juego. Inténtalo de nuevo.,El desarrollador ha cerrado temporalmente el servidor del juego. Inténtalo de nuevo.,Le développeur a fermé le serveur du jeu temporairement. Veuillez réessayer plus tard.,Lo sviluppatore ha temporaneamente chiuso il server di gioco. Riprova più tardi.,ゲームの開発者が一時的にゲームサーバーをシャットダウンしました。もう一度お試しください。,본 게임의 개발자들이 일시적으로 게임 서버를 강제 종료하였습니다. 다시 시도하세요.,O desenvolvedor do jogo desligou o servidor do jogo temporariamente. Tente novamente.,O desenvolvedor do jogo desligou o servidor do jogo temporariamente. Tente novamente.,"Разработчик временно отключил игровой сервер. Пожалуйста, повторите попытку.",游戏开发者已暂时关闭游戏服务器。请重试。,遊戲開發人員已暫時關閉遊戲伺服器,請重新嘗試。,游戏开发者已暂时关闭游戏服务器。请重试。 +InGame.ConnectionError.DisconnectReceivePacketError,,,"There was a problem receiving data, please reconnect.","Beim Empfangen der Daten ist ein Problem aufgetreten, bitte wieder verbinden.","There was a problem receiving data, please reconnect.",Ha habido un problema al recibir los datos. Vuelve a conectarte.,Ha habido un problema al recibir los datos. Vuelve a conectarte.,"Une erreur s'est produite lors de la réception des données, veuillez vous reconnecter.","C'è stato un problema durante la ricezione dei dati, è necessario riconnettersi.",データを受信中に問題が発生しました。再接続してください,데이터 수신 오류가 발생했어요. 다시 접속하세요.,Ocorreu um erro ao receber os dados. Reconecte-se.,Ocorreu um erro ao receber os dados. Reconecte-se.,"Проблемы с получением данных, пожалуйста, установите соединение повторно.",接收数据时出现问题,请重新连接。,接收資料時發生錯誤,請重新連線。,接收数据时出现问题,请重新连接。 +InGame.ConnectionError.PlacelaunchOtherError,,,We are experiencing technical difficulties. Please try again later.,Wir haben technische Schwierigkeiten. Bitte versuche es später erneut.,We are experiencing technical difficulties. Please try again later.,Tenemos dificultades técnicas. Inténtalo de nuevo más tarde.,Tenemos dificultades técnicas. Inténtalo de nuevo más tarde.,Nous rencontrons des problèmes techniques. Veuillez réessayer plus tard.,Stiamo riscontrando delle difficoltà tecniche. Riprova più tardi.,技術的な問題が発生しています。後でもう一度お試しください。,기술적인 문제가 발생했습니다. 나중에 다시 시도하세요.,Estamos tendo dificuldades técnicas. Tente de novo mais tarde.,Estamos tendo dificuldades técnicas. Tente de novo mais tarde.,Сейчас технические проблемы. Повторите попытку позже.,我们遇到技术困难 。请稍后重试。,我們遇到技術困難,請稍後再試。,我们遇到技术困难 。请稍后重试。 +InGame.ConnectionError.PlacelaunchGameFull,,,The server you are attempting to join is currently full. Please try again.,"Der Server, dem du beitreten möchtest, ist derzeit voll. Bitte versuche es erneut.",The server you are attempting to join is currently full. Please try again.,El servidor al que estás tratando de unirte está lleno en este momento. Inténtalo de nuevo.,El servidor al que estás tratando de unirte está lleno en este momento. Inténtalo de nuevo.,Le serveur que vous tentez de rejoindre est actuellement complet. Veuillez réessayer.,Il server a cui stai cercando di unirti è attualmente al completo. Riprova più tardi.,参加しようとしているサーバーは現在満員です。もう一度試してください。,참가하려는 서버가 현재 만원입니다. 나중에 시도하세요.,O servidor no qual você está tentando entrar está cheio. Tente novamente.,O servidor no qual você está tentando entrar está cheio. Tente novamente.,"Выбранный вами сервер переполнен и соединение невозможно. Пожалуйста, повторите попытку.",你尝试加入的服务器目前已满。请重试。,您嘗試加入的伺服器已額滿,請重新嘗試。,你尝试加入的服务器目前已满。请重试。 +InGame.ConnectionError.TeleportFlooded,,,Too many teleport requests received.,Zu viele Anfragen zum Teleportieren erhalten.,Too many teleport requests received.,Se han recibido demasiadas solicitudes de teletransportación.,Se han recibido demasiadas solicitudes de teletransportación.,Trop de demandes de téléportation reçues.,Troppe richieste di teletrasporto ricevute.,テレポートのリクエスト受信が多すぎます。,텔레포트 요청 횟수 초과.,Excesso de solicitações de teleporte recebidas.,Excesso de solicitações de teleporte recebidas.,Получено слишком много запросов на телепорт.,已接收过多传送请求。,傳送請求過多。,已接收过多传送请求。 +InGame.ConnectionError.DisconnectIllegalTeleport,,,Lost connection due to an invalid teleport. ,Die Verbindung ist aufgrund einer ungültigen Teleportation abgebrochen. ,Lost connection due to an invalid teleport. ,Se ha perdido la conexión debido a una teletransportación no válida. ,Se ha perdido la conexión debido a una teletransportación no válida. ,Connexion perdue en raison d'une téléportation invalide. ,Connessione persa a causa di un teletrasporto non valido. ,無効なテレポートにより接続が切断されました。 ,유효하지 못한 텔레포트로 인해 연결 끊김,Conexão perdida devido a um teleporte inválido. ,Conexão perdida devido a um teleporte inválido. ,Пропало соединение из-за неправильного телепорта. ,由于无效传送,连接被中断。,傳送無效,連線中斷。,由于无效传送,连接被中断。 +InGame.ConnectionError.DisconnectEvicted,,,Same account launched game from different device. Reconnect if you prefer to use this device.,"Das Spiel wurde mit demselben Konto auf einem anderen Gerät gestartet. Verbinde dich wieder, wenn du dieses Gerät benutzen möchtest.",Same account launched game from different device. Reconnect if you prefer to use this device.,Se ha utilizado la misma cuenta para lanzar el juego en un dispositivo diferente. Vuelve a conectarte si prefieres utilizar este dispositivo.,Se ha utilizado la misma cuenta para lanzar el juego en un dispositivo diferente. Vuelve a conectarte si prefieres utilizar este dispositivo.,Ce compte a lancé le jeu depuis un autre appareil. Veuillez vous reconnecter si vous préférez utiliser celui-ci.,Lo stesso account ha avviato il gioco da un altro dispositivo. Riconnettiti se preferisci usare questo dispositivo.,同じアカウントで別のデバイスからゲームを起動しました。このデバイスを使う場合は再接続してください。,다른 기기에서 같은 계정으로 게임에 접속했습니다. 본 기기를 이용하기를 원하시면 다시 접속하세요. ,A mesma conta iniciou um jogo de um dispositivo diferente. Reconecte-se se você prefere usar este dispositivo.,A mesma conta iniciou um jogo de um dispositivo diferente. Reconecte-se se você prefere usar este dispositivo.,"Игра была запущена с той же учетной записи, но с другого устройства. Выберите нужное для вас устройство.",同一账号已在另一设备上运行游戏。如果你偏向于使用此设备,请重新连接。,此帳號已從其它裝置開啟此遊戲。若您想使用此裝置,請重新連線。,同一账号已在另一设备上运行游戏。如果你偏向于使用此设备,请重新连接。 +InGame.ConnectionError.PlacelaunchPartyCannotFit,,,Your party is too large to join this game. Try joining a different game.,"Dein Team ist zu groß, um diesem Spiel beizutreten. Versuche, einem anderen Spiel beizutreten.",Your party is too large to join this game. Try joining a different game.,Tu equipo es demasiado grande para unirse a este juego. Prueba un juego diferente.,Tu equipo es demasiado grande para unirse a este juego. Prueba un juego diferente.,Votre groupe comporte trop de membres pour rejoindre ce jeu. Essayez d'en rejoindre un autre.,Il tuo gruppo è troppo grande per partecipare a questa partita. Prova con un'altra partita.,人数が多すぎて、このゲームに参加できません。別のゲームに参加してください。,파티 인원이 너무 많아 게임에 참가할 수 없습니다. 다른 게임에 참가해 보세요.,Seu grupo é grande demais para entrar neste jogo. Tente entrar em outro jogo.,Seu grupo é grande demais para entrar neste jogo. Tente entrar em outro jogo.,Ваша группа слишком велика для присоединения к игре. Попробуйте присоединиться к другой.,你的队伍人数过多,无法加入此游戏。请尝试加入其他游戏。,您的隊伍人數過多,無法加入此遊戲。請嘗試加入其它遊戲。,你的队伍人数过多,无法加入此游戏。请尝试加入其他游戏。 +InGame.ConnectionError.PlacelaunchRestricted,,,The status of the game has changed and you no longer have access. Please try again later.,Der Status des Spiels hat sich geändert und du hast keinen Zugang mehr. Bitte versuche es später erneut.,The status of the game has changed and you no longer have access. Please try again later.,El estado de este juego ha cambiado y ya no tienes acceso. Inténtalo de nuevo más tarde.,El estado de este juego ha cambiado y ya no tienes acceso. Inténtalo de nuevo más tarde.,Le statut du jeu a changé et vous n'y avez plus accès. Veuillez réessayer plus tard.,Lo stato del gioco è cambiato e non hai più l'accesso. Riprova più tardi.,ゲームのステータスが変更されたため、アクセスできなくなりました。後でもう一度お試しください。,게임 상황이 변경되어 더 이상 접속하실 수 없습니다. 나중에 다시 시도하세요.,O status deste jogo mudou e você não tem mais acesso. Tente de novo mais tarde.,O status deste jogo mudou e você não tem mais acesso. Tente de novo mais tarde.,"Статус этой игры был изменен, и вы больше не имеете к ней доступа. Пожалуйста, повторите попытку.",游戏状态已更改,你已经失去访问权限。请稍后重试。,遊戲狀態已變更,您已失去此遊戲的通行權。請稍後再試。,游戏状态已更改,你已经失去访问权限。请稍后重试。 +InGame.ConnectionError.PlacelaunchGameEnded,,,This game is currently unavailable. Please try again later.,Dieses Spiel ist derzeit nicht verfügbar. Bitte versuche es später erneut.,This game is currently unavailable. Please try again later.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,"Ce jeu est indisponible pour le moment, veuillez réessayer plus tard.",Questo gioco non è al momento disponibile. Riprova più tardi.,このゲームは現在利用できません。後でもう一度お試しください。,이 게임은 현재 플레이할 수 없습니다. 나중에 다시 시도하세요.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Эта игра сейчас недоступна. Повторите попытку позже.,此游戏目前不可用,请稍后重试。,此遊戲目前不開放,請稍後再試。,此游戏目前不可用,请稍后重试。 +InGame.ConnectionError.PlacelaunchErrors,,,An error occured while joining the game. Please try again.,Beim Beitritt zum Spiel ist ein Fehler aufgetreten. Bitte versuche es erneut.,An error occured while joining the game. Please try again.,Se ha producido un error al unirse al juego. Inténtalo de nuevo.,Se ha producido un error al unirse al juego. Inténtalo de nuevo.,Une erreur est survenue lors de la connexion au jeu. Veuillez réessayer plus tard.,Si è verificato un errore mentre cercavi di unirti al gioco. Riprova più tardi.,ゲームの参加中にエラーが発生しました。もう一度お試しください。,게임 참가 중 오류가 발생했어요. 다시 시도하세요.,Ocorreu um erro ao tentar entrar no jogo. Tente novamente.,Ocorreu um erro ao tentar entrar no jogo. Tente novamente.,Произошла ошибка при входе в игру. Повторите попытку позже.,加入游戏时发生错误。请稍后重试。,加入遊戲時發生錯誤,請重新嘗試。,加入游戏时发生错误。请稍后重试。 +InGame.ConnectionError.DisconnectLuaKick,,,You have been kicked from the game. ,Du wurdest aus dem Spiel gekickt. ,You have been kicked from the game. ,Has sido expulsado del juego. ,Has sido expulsado del juego. ,Vous avez été expulsé(e) du jeu. ,Sei stato espulso dal gioco. ,ゲームから外されました。 ,게임에서 강제 퇴장당하셨어요.,Você foi removido do jogo. ,Você foi removido do jogo. ,Вас исключили из игры. ,你已被踢出游戏。,您已被踢出此遊戲。,你已被踢出游戏。 +InGame.ConnectionError.PlacelaunchHashException,,,This game is currently unavailable. Please try again later.,Dieses Spiel ist derzeit nicht verfügbar. Bitte versuche es später erneut.,This game is currently unavailable. Please try again later.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,Este juego no está disponible en este momento. Inténtalo de nuevo más tarde.,"Ce jeu est indisponible pour le moment, veuillez réessayer plus tard.",Questo gioco non è al momento disponibile. Riprova più tardi.,このゲームは現在利用できません。後でもう一度お試しください。,이 게임은 현재 플레이할 수 없습니다. 나중에 다시 시도하세요.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Este jogo não está disponível no momento. Tente de novo mais tarde.,Эта игра сейчас недоступна. Повторите попытку позже.,此游戏目前不可用,请稍后重试。,此遊戲目前不開放,請稍後再試。,此游戏目前不可用,请稍后重试。 +InGame.ConnectionError.DisconnectReceivePacketStreamError,,,"There was a problem streaming data, please reconnect.","Beim Streamen der Daten ist ein Problem aufgetreten, bitte wieder verbinden.","There was a problem streaming data, please reconnect.",Ha habido un problema al transmitir los datos. Vuelve a conectarte.,Ha habido un problema al transmitir los datos. Vuelve a conectarte.,"Une erreur s'est produite lors de la diffusion des données, veuillez vous reconnecter.","C'è stato un problema durante lo streaming dei dati, è necessario riconnettersi.",データをストリーミング中にエラーが発生しました。再接続してください。,데이터 스트리밍 중 오류가 발생했어요. 다시 연결하세요.,Ocorreu um erro ao transmitir os dados. Reconecte-se.,Ocorreu um erro ao transmitir os dados. Reconecte-se.,"Проблемы с транслированием данных, пожалуйста, установите соединение повторно.",流式处理数据时出现问题,请重新连接。,資料串流發生錯誤,請重新連線。,流式处理数据时出现问题,请重新连接。 +InGame.ConnectionError.PlacelaunchError,,,Unable to find the server you are attempting to join. Please try again later.,"Der Server, dem du beitreten möchtest, kann nicht gefunden werden. Bitte versuche es später erneut.",Unable to find the server you are attempting to join. Please try again later.,No se ha podido encontrar el servidor al que estás tratando de unirte. Inténtalo de nuevo más tarde.,No se ha podido encontrar el servidor al que estás tratando de unirte. Inténtalo de nuevo más tarde.,Impossible de trouver le serveur que vous tentez de rejoindre. Veuillez réessayer plus tard.,Impossibile trovare il server a cui stai cercando di unirti. Riprova più tardi.,参加しようとしているサーバーが見つかりません。後でもう一度試してください。,참가하려는 서버를 찾을 수 없습니다. 나중에 다시 시도하세요.,Impossível encontrar o servidor no qual você está tentando entrar. Tente novamente.,Impossível encontrar o servidor no qual você está tentando entrar. Tente novamente.,"Невозможно найти выбранный вами сервер. Пожалуйста, повторите попытку.",无法找到你尝试加入的服务器。请稍后重试。,找不到您嘗試加入的伺服器,請稍後再試。,无法找到你尝试加入的服务器。请稍后重试。 +InGame.ConnectionError.TeleportUnauthorized,,,Attempted to teleport to a place that is restricted.,"Du hast versucht, dich an einen zugangsbeschränkten Ort zu teleportieren.",Attempted to teleport to a place that is restricted.,Se ha intentado teletransportarse a un lugar que está restringido.,Se ha intentado teletransportarse a un lugar que está restringido.,Vous avez tenté de vous téléporter dans un emplacement à accès restreint.,Tentativo di teletrasporto verso una località riservata.,制限されたプレースへのテレポートを試みました。,금지된 장소로 텔레포트를 시도하였습니다.,Tentativa de se teleportar para um local restrito.,Tentativa de se teleportar para um local restrito.,Попытка телепорта в запрещенное место.,尝试传送至受限场景。,您嘗試傳送的空間受到限制。,尝试传送至受限场景。 +InGame.ConnectionError.Title.JoinError,,,Join Error,Verbindungsfehler,Join Error,Error al unirse,Error al unirse,Erreur de connexion,Errore di accesso,参加でエラーが発生,참가 오류 발생,Erro ao entrar,Erro ao entrar,Ошибка присоединения,加入错误,加入時發生錯誤,加入错误 +InGame.ConnectionError.Title.Disconnected,,,Disconnected,Verbindung getrennt,Disconnected,Desconectado,Desconectado,Déconnecté,Disconnesso,切断されました,연결 끊김,Desconectado,Desconectado,Отключено,连接已断开,連線中斷,连接已断开 +InGame.ConnectionError.Title.TeleportFailed,,,Teleport Failed,Teleportation gescheitert,Teleport Failed,Error en la teletransportación,Error en la teletransportación,Erreur de téléportation,Teletrasporto non riuscito,テレポートできませんでした,텔레포트 실패,Falha no teleporte,Falha no teleporte,Телепорт неудачен,传送失败,傳送失敗,传送失败 +InGame.ConnectionError.Message.ErrorCode,,,Error Code: {ERROR_CODE},Fehlercode: {ERROR_CODE},Error Code: {ERROR_CODE},Código del error: {ERROR_CODE},Código del error: {ERROR_CODE},Code Erreur : {ERROR_CODE},Codice di errore: {ERROR_CODE},エラーコード: {ERROR_CODE},오류 코드: {ERROR_CODE},Código de erro: {ERROR_CODE},Código de erro: {ERROR_CODE},Код ошибки: {ERROR_CODE},错误代码: {ERROR_CODE},錯誤代碼:{ERROR_CODE},错误代码: {ERROR_CODE} +InGame.ConnectionError.Button.Reconnect,,,Reconnect,Erneut verbinden,Reconnect,Reconectar,Reconectar,Reconnexion,Riconnetti,再接続,재연결,Reconectar,Reconectar,Повторное соединение,重新连接,重新連線,重新连接 +InGame.ConnectionError.DisconnectConnectionLost,,,Please check your internet connection and try again.,Bitte überprüfe deine Internetverbindung und versuche es erneut.,Please check your internet connection and try again.,Comprueba tu conexión a internet e inténtelo de nuevo.,Comprueba tu conexión a internet e inténtelo de nuevo.,Vérifie ta connexion internet et réessaye.,Controlla la tua connessione a Internet e riprova.,インターネット接続をチェックしてやり直してください。,인터넷 연결을 확인하고 다시 시도하세요.,Verifique sua conexão de internet e tente novamente.,Verifique sua conexão de internet e tente novamente.,Проверьте свое подключение к сети Интернет и повторите попытку.,请检查你的网络连接并重试。,請檢查網路連線再重新嘗試。,请检查你的网络连接并重试。 +InGame.ConnectionError.DisconnectIdle,,,You were disconnected for being idle {RBX_NUM} minutes,"Deine Verbindung wurde getrennt, da du {RBX_NUM} Minuten lang untätig warst",You were disconnected for being idle {RBX_NUM} minutes,Se te ha desconectado por no mostrar actividad durante {RBX_NUM} minutos.,Se te ha desconectado por no mostrar actividad durante {RBX_NUM} minutos.,Tu as été déconnecté.e car tu étais en mode inactif pendant {RBX_NUM} minutes,Sei stato disconnesso perché inattivo per {RBX_NUM} minuti,{RBX_NUM} 分間、アイドル状態だったので切断されました。,{RBX_NUM}분 동안 기본 상태로 있어 연결이 끊겼어요,Você perdeu a conexão por ficar inativo(a) por {RBX_NUM} minutos,Você perdeu a conexão por ficar inativo(a) por {RBX_NUM} minutos,"Вы отключены, так как не были активны в течение {RBX_NUM} мин.",已闲置 {RBX_NUM} 分钟,你的连接已断开。,您已閒置 {RBX_NUM} 分鐘,連線中斷,已闲置 {RBX_NUM} 分钟,你的连接已断开。 +InGame.ConnectionError.UnknownError,,,Unknown error.,Unbekannter Fehler.,Unknown error.,Error desconocido.,Error desconocido.,Erreur inconnue.,Errore imprevisto.,不明なエラーです。,알 수 없는 오류.,Erro desconhecido.,Erro desconhecido.,Неизвестная ошибка.,未知错误。,未知錯誤。,未知错误。 +InGame.ConnectionError.ReconnectFailed,,,Reconnect was unsuccessful. Please try again.,Wiederverbindung war nicht erfolgreich. Bitte versuche es erneut.,Reconnect was unsuccessful. Please try again.,Error en la reconexión. Inténtalo de nuevo.,Error en la reconexión. Inténtalo de nuevo.,Ta tentative de connexion a échoué. Réessaye.,Riconnessione non riuscita. Riprova.,再接続できませんでした。もう一度やり直してください。,재연결에 실패했어요. 다시 시도하세요,Falha ao reconectar. Tente novamente.,Falha ao reconectar. Tente novamente.,Повторное соединение неудачно. Повторите попытку.,重新连接未成功。请重试。,重新連線失敗,請再試一次。,重新连接未成功。请重试。 +InGame.EmotesMenu.SelectAnEmote,,,Select an Emote,Emote auswählen,Select an Emote,Seleccionar un emoticono,Seleccionar un emoticono,Choisis une emote,Seleziona un'emoticon,エモートを選ぶ,감정 표현 선택,Selecione um emote,Selecione um emote,Выбрать эмоцию,选择动作,選擇動作,选择动作 +InGame.EmotesMenu.NoEmotesEquipped,,,No Emotes equipped,Keine Emotes ausgerüstet,No Emotes equipped,No hay emoticonos equipados,No hay emoticonos equipados,Aucune Emote équipée,Nessuna emoticon equipaggiata,エモートをつけていません,장착된 감정 표현 없음,Nenhum emote equipado,Nenhum emote equipado,Эмоция не выбрана,未装备动作,未裝備動作,未装备动作 +InGame.EmotesMenu.ErrorMessageNotSupported,,,You can't use Emotes here.,Du kannst hier keine Emotes verwenden.,You can't use Emotes here.,No puedes usar emoticonos aquí.,No puedes usar emoticonos aquí.,Tu ne peux pas utiliser les Emotes ici.,Qui non puoi usare le emoticon.,ここではエモートは使えません。,여기서는 감정 표현을 사용할 수 없어요.,Você não pode usar emotes aqui.,Você não pode usar emotes aqui.,Здесь нельзя использовать эмоции.,无法在此游戏中使用动作。,無法在此遊戲使用動作。,无法在此游戏中使用动作。 +InGame.EmotesMenu.ErrorMessageR15Only,,,Only R15 avatars can use Emotes.,Nur R15 Avatars können Emotes verwenden.,Only R15 avatars can use Emotes.,Solo los avatares R15 pueden usar emoticonos.,Solo los avatares R15 pueden usar emoticonos.,Seuls les avatars R15 peuvent utiliser les Emotes.,Solo gli avatar R15 possono usare le emoticon.,R15指定のアバターのみエモートを使えます。,R15 아바타만 감정 표현을 사용할 수 있어요.,Apenas avatares R15 podem usar emotes.,Apenas avatares R15 podem usar emotes.,Только аватары R15 могут использовать эмоции.,只有 R15 虚拟形象可以使用动作。,只有 R15 虛擬人偶可以使用動作。,只有 R15 虚拟形象可以使用动作。 +InGame.EmotesMenu.ErrorMessageNoMatchingEmote,,,You can't use that Emote.,Du kannst dieses Emote nicht benutzen.,You can't use that Emote.,No puedes usar ese emoticono.,No puedes usar ese emoticono.,Tu ne peux pas utiliser cette Emote.,Non puoi usare quell'emoticon.,そのエモートは使えません。,이 감정 표현은 사용할 수 없어요.,Você não pode usar esse emote.,Você não pode usar esse emote.,Вы не можете использовать эту эмоцию.,无法使用该动作。,無法使用該動作。,无法使用该动作。 +InGame.EmotesMenu.ErrorMessageTemporarilyUnavailable,,,You can't use Emotes right now.,Du kannst Emotes derzeit nicht verwenden.,You can't use Emotes right now.,No puedes usar emoticonos en este momento.,No puedes usar emoticonos en este momento.,Tu ne peux pas utiliser d'Emote maintenant.,Adesso non puoi usare le emoticon.,今はエモートを使えません。,지금은 감정 표현을 사용할 수 없어요.,Você não pode usar emotes no momento.,Você não pode usar emotes no momento.,Вы не можете использовать эмоции прямо сейчас.,当前无法使用动作。,現在無法使用動作。,当前无法使用动作。 +InGame.EmotesMenu.EmotesDisabled,,,Emotes are disabled in this game.,Emotes sind in diesem Spiel deaktiviert.,Emotes are disabled in this game.,Los emoticonos no están activados en este juego.,Los emoticonos no están activados en este juego.,Les emotes sont désactivés dans ce jeu.,In questo gioco sono state disattivate le emoticon.,このゲームではエモートが無効化されています。,이 게임에서는 감정 표현을 이용할 수 없어요.,O emotes estão desabilitados neste jogo.,O emotes estão desabilitados neste jogo.,Выражение эмоций отключено в этой игре.,此游戏已停用动作。,此遊戲已停用動作。,此游戏已停用动作。 +FriendPlayerPrompt.promptCompletedCallback.UnknownError,,,An error occurred while sending {RBX_NAME} a friend request. Please try again later.,Beim Senden der Freundesanfrage an {RBX_NAME} ist ein Fehler aufgetreten. Bitte versuche es später erneut.,An error occurred while sending {RBX_NAME} a friend request. Please try again later.,Se ha producido un error al enviar una solicitud de amistad a {RBX_NAME}. Inténtalo de nuevo más tarde.,Se ha producido un error al enviar una solicitud de amistad a {RBX_NAME}. Inténtalo de nuevo más tarde.,Une erreur est survenue lors de l'envoi de la demande d'ami à {RBX_NAME}. Veuillez réessayer plus tard.,Si è verificato un errore nell'invio della richiesta di amicizia a {RBX_NAME}. Riprova più tardi.,{RBX_NAME} さんへの友達リクエスト中にエラーが発生しました。しばらくしてからやり直してください。,{RBX_NAME}님에게 친구 요청을 보내는 중 오류가 발생했어요. 나중에 다시 시도하세요.,Um erro ocorreu ao enviar um pedido de amizade para {RBX_NAME}. Tente de novo mais tarde.,Um erro ocorreu ao enviar um pedido de amizade para {RBX_NAME}. Tente de novo mais tarde.,При отправлении запроса дружбы пользователю {RBX_NAME} произошла ошибка. Повторите попытку позже.,向“{RBX_NAME}”发送好友邀请时发生错误。请稍候重试。,向 {RBX_NAME} 傳送好友邀請時發生錯誤,請稍後再試。,向“{RBX_NAME}”发送好友邀请时发生错误。请稍候重试。 +FriendPlayerPrompt.DoPromptUnfriendPlayer,,,Would you like to remove {RBX_NAME} from your friends list?,Möchtest du {RBX_NAME} von deiner Freundesliste entfernen?,Would you like to remove {RBX_NAME} from your friends list?,¿Quieres eliminar a {RBX_NAME} de tu lista de amigos?,¿Quieres eliminar a {RBX_NAME} de tu lista de amigos?,Souhaitez-vous retirer {RBX_NAME} de votre liste d'amis ?,Vuoi rimuovere {RBX_NAME} dalla tua lista amici?,{RBX_NAME} さんを友達リストから削除しますか?,{RBX_NAME}님을 친구 목록에서 삭제할까요?,Gostaria de remover {RBX_NAME} da sua lista de amigos?,Gostaria de remover {RBX_NAME} da sua lista de amigos?,Удалить пользователя {RBX_NAME} из списка друзей?,你想要将“{RBX_NAME}”从你的好友名单中移除吗?,您要將 {RBX_NAME} 從好友名單移除嗎?,你想要将“{RBX_NAME}”从你的好友名单中移除吗? +FriendPlayerPrompt.DoPromptRequestFriendPlayer,,,Would you like to send {RBX_NAME} a Friend Request?,Möchtest du {RBX_NAME} eine Freundesanfrage senden?,Would you like to send {RBX_NAME} a Friend Request?,¿Quieres enviar a {RBX_NAME} una solicitud de amistad?,¿Quieres enviar a {RBX_NAME} una solicitud de amistad?,Souhaitez-vous envoyer une demande d'ami à {RBX_NAME} ?,Vuoi inviare una richiesta di amicizia a {RBX_NAME}?,{RBX_NAME} さんに友達リクエストを送りますか?,{RBX_NAME}님에게 친구 요청을 보낼까요?,Gostaria de enviar um pedido de amizade para {RBX_NAME}?,Gostaria de enviar um pedido de amizade para {RBX_NAME}?,Отправить пользователю {RBX_NAME} запрос дружбы?,你想要向“{RBX_NAME}”发送好友邀请吗?,您要向 {RBX_NAME} 傳送好友邀請嗎?,你想要向“{RBX_NAME}”发送好友邀请吗? +FriendPlayerPrompt.promptCompletedCallback.AtFriendLimit,,,You can not send a friend request to {RBX_NAME} because they are at the max friend limit.,"Du kannst {RBX_NAME} keine Freundesanfrage senden, da dieser Spieler die max. Anzahl an Freunden erreicht hat.",You can not send a friend request to {RBX_NAME} because they are at the max friend limit.,No puedes enviar una solicitud de amistad a {RBX_NAME} porque ha alcanzado el límite máximo de amigos.,No puedes enviar una solicitud de amistad a {RBX_NAME} porque ha alcanzado el límite máximo de amigos.,"Vous ne pouvez pas envoyer une demande d'ami à {RBX_NAME}, car il a atteint sa limite maximale d'amis.",Non puoi inviare una richiesta di amicizia a {RBX_NAME} perché ha raggiunto il limite massimo di amici.,{RBX_NAME} さんの友達が上限に達しているため友達リクエストが送れませんでした。,{RBX_NAME}의 친구 수가 너무 많아 더 이상 친구 요청을 보낼 수 없습니다.,Você não pode enviar um pedido de amizade para {RBX_NAME} pois ele(a) alcançou o limite máximo de amigos.,Você não pode enviar um pedido de amizade para {RBX_NAME} pois ele(a) alcançou o limite máximo de amigos.,Нельзя отправить запрос дружбы пользователю {RBX_NAME}: количество его друзей достигло максимума.,由于对方已达到好友数量上限,你无法向“{RBX_NAME}”发送好友邀请。,無法傳送好友邀請,{RBX_NAME} 的好友人數已達上限。,由于对方已达到好友数量上限,你无法向“{RBX_NAME}”发送好友邀请。 +FriendPlayerPrompt.Title.SendFriendRequest,,,Send Friend Request?,Freundesanfrage senden?,Send Friend Request?,¿Enviar una solicitud de amistad?,¿Enviar una solicitud de amistad?,Envoyer demande d'ami?,Invia richiesta di amicizia?,友達リスエストを送信しますか。,친구 요청 전송?,Enviar pedido de amizade?,Enviar pedido de amizade?,Отправить запрос дружбы?,发送好友邀请?,傳送好友邀請?,发送好友邀请? +FriendPlayerPrompt.Title.UnfriendPlayer,,,Unfriend Player?,Spieler von der Freundesliste entfernen?,Unfriend Player?,¿Cancelar amistad con jugador?,¿Cancelar amistad con jugador?,Ne plus être ami du joueur?,Togli amicizia a giocatore?,プレイヤーを友達解除しますか。,친구를 끊을까요?,Cancelar amizade com jogador?,Cancelar amizade com jogador?,Недружественный игрок?,与玩家解除好友关系?,刪除此好友?,与玩家解除好友关系? +FriendPlayerPrompt.Label.Unfriend,,,Unfriend,Von der Freundesliste entfernen,Unfriend,Cancelar amistad,Cancelar amistad,Ne plus être ami,Togli amicizia,友達解除,친구 끊기,Remover amigo,Remover amigo,Недруг,解除好友关系,刪除好友,解除好友关系 +InGame.GameplayPaused.Title,,,Gameplay Paused,Spiel pausiert,Gameplay Paused,Juego pausado,Juego pausado,Jeu en pause,Gioco in pausa,ゲームプレイが一時停止しました,플레이가 일시 중지됨,Jogo pausado,Jogo pausado,Игра приостановлена,游戏已暂停,遊戲已暫停,游戏已暂停 +InGame.GameplayPaused.Body,,,Gameplay has been paused: please wait while the game content loads.,"Spiel pausiert: Bitte warte, solange der Spieleinhalt lädt.",Gameplay has been paused: please wait while the game content loads.,Se ha pausado el juego. Espera mientras se carga el contenido del juego.,Se ha pausado el juego. Espera mientras se carga el contenido del juego.,Le jeu est en pause : attends quelques instants pendant que le contenu du jeu se charge.,Il gioco è stato messo in pausa: attendi durante il caricamento del contenuto di gioco.,ゲームプレイが一時停止しました: ゲームコンテンツを読み込んでいる間、お待ちください。,플레이가 일시 중지되었습니다. 게임 콘텐츠를 불러오고 있으니 잠시만 기다리세요.,O jogo foi pausado: espere o carregamento do conteúdo.,O jogo foi pausado: espere o carregamento do conteúdo.,Игра приостановлена: дождитесь загрузки игровых данных,游戏已暂停,请耐心等待游戏内容加载完成。,遊戲已暫停,請等候遊戲內容完成載入。,游戏已暂停,请耐心等待游戏内容加载完成。 +InGame.HelpMenu.UpArrow,,,Up Arrow,Aufwärtspfeil,Up Arrow,Flecha hacia arriba,Flecha hacia arriba,Flèche haut,Freccia SU,上カーソル,위쪽 화살표,Seta para cima,Seta para cima,Стрелка вверх,上箭头,↑,上箭头 +InGame.HelpMenu.DownArrow,,,Down Arrow,Abwärtspfeil,Down Arrow,Flecha hacia abajo,Flecha hacia abajo,Flèche bas,Freccia GIÙ,下カーソル,아래쪽 화살표,Seta para baixo,Seta para baixo,Стрелка вниз,下箭头,↓,下箭头 +InGame.HelpMenu.LeftArrow,,,Left Arrow,Linkspfeil,Left Arrow,Flecha hacia la izquierda,Flecha hacia la izquierda,Flèche gauche,Freccia SINISTRA,左カーソル,왼쪽 화살표,Seta esquerda,Seta esquerda,Стрелка влево,左箭头,←,左箭头 +InGame.HelpMenu.RightArrow,,,Right Arrow,Rechtspfeil,Right Arrow,Flecha hacia la derecha,Flecha hacia la derecha,Flèche droite,Freccia DESTRA,右カーソル,오른쪽 화살표,Seta direita,Seta direita,Стрелка вправо,右箭头,→,右箭头 +InGame.HelpMenu.Label.ServerVersion,,,Server Version:,Serverversion:,Server Version:,Versión del servidor:,Versión del servidor:,Version du serveur :,Versione del server:,サーバーバージョン:,서버 버전:,Versão de servidor:,Versão de servidor:,Версия сервера:,服务器版本:,伺服器版本:,服务器版本: +InGame.HelpMenu.Label.ClientVersion,,,Client Version:,Client-Version:,Client Version:,Versión del cliente:,Versión del cliente:,Version du client :,Versione del client:,クライアントバージョン:,클라이언트 버전:,Versão de cliente:,Versão de cliente:,Версия клиента:,客户端版本:,客戶端版本:,客户端版本: +InGame.HelpMenu.Label.PlayerScripts,,,PlayerScripts:,SpielerScripts:,PlayerScripts:,PlayerScripts:,PlayerScripts:,PlayerScripts :,PlayerScripts:,PlayerScripts:,PlayerScripts:,Scripts de jogador:,Scripts de jogador:,СкриптИгрока:,玩家脚本:,PlayerScripts:,玩家脚本: +InGame.HelpMenu.Label.PlaceVersion,,,Place Version:,Orts-Version:,Place Version:,Versión del lugar:,Versión del lugar:,Version de la location :,Versione della località:,プレースバージョン:,장소 버전:,Versão do local:,Versão do local:,Версия расположения:,场景版本:,空間版本:,场景版本: +InGame.HelpMenu.Label.ClientCoreScriptVersion,,,Client CoreScript Version:,Client-CoreScript-Version:,Client CoreScript Version:,Versión CoreScript del cliente:,Versión CoreScript del cliente:,Version CoreScript du client :,Versione CoreScript del client:,クライアントCoreScriptバージョン:,클라이언트 CoreScript 버전:,Versão de cliente CoreScript:,Versão de cliente CoreScript:,Клиентская версия CoreScript:,客户端 CoreScript 版本:,客戶端 CoreScript 版本:,客户端 CoreScript 版本: +InGame.HelpMenu.Message.ClientVersion,,,Client Version: {RBX_NUM},Client-Version: {RBX_NUM},Client Version: {RBX_NUM},Versión del cliente: {RBX_NUM},Versión del cliente: {RBX_NUM},Version du client : {RBX_NUM},Versione del client: {RBX_NUM},クライアントバージョン: {RBX_NUM},클라이언트 버전: {RBX_NUM},Versão de cliente: {RBX_NUM},Versão de cliente: {RBX_NUM},Версия клиента: {RBX_NUM},客户端版本: {RBX_NUM},客戶端版本:{RBX_NUM},客户端版本: {RBX_NUM} +InGame.HelpMenu.Message.ServerVersion,,,Server Version: {RBX_NUM},Serverversion: {RBX_NUM},Server Version: {RBX_NUM},Versión del servidor: {RBX_NUM},Versión del servidor: {RBX_NUM},Version du serveur : {RBX_NUM},Versione del server: {RBX_NUM},サーバーバージョン: {RBX_NUM},서버 버전: {RBX_NUM},Versão de servidor: {RBX_NUM},Versão de servidor: {RBX_NUM},Версия сервера: {RBX_NUM},服务器版本: {RBX_NUM},伺服器版本:{RBX_NUM},服务器版本: {RBX_NUM} +InGame.HelpMenu.Message.PlayerScripts,,,PlayerScripts:{RBX_STR},SpielerScripts:{RBX_STR},PlayerScripts:{RBX_STR},PlayerScripts:{RBX_STR},PlayerScripts:{RBX_STR},PlayerScripts : {RBX_STR},PlayerScripts:{RBX_STR},PlayerScripts:{RBX_STR},PlayerScripts: {RBX_STR},Scripts de jogador: {RBX_STR},Scripts de jogador: {RBX_STR},СкриптИгрока: {RBX_STR},玩家脚本:{RBX_STR},PlayerScripts:{RBX_STR},玩家脚本:{RBX_STR} +InGame.HelpMenu.Message.PlaceVersion,,,Place Version: {RBX_NUM},Orts-Version: {RBX_NUM},Place Version: {RBX_NUM},Versión del lugar: {RBX_NUM},Versión del lugar: {RBX_NUM},Version de la location : {RBX_NUM},Versione della località: {RBX_NUM},プレースバージョン: {RBX_NUM},장소 버전: {RBX_NUM},Versão do local: {RBX_NUM},Versão do local: {RBX_NUM},Версия расположения: {RBX_NUM},场景版本:{RBX_NUM},空間版本:{RBX_NUM},场景版本:{RBX_NUM} +InGame.HelpMenu.Leave,,,Leave,Verlassen,Leave,Salir,Salir,Quitter,Esci,終了,나가기,Sair,Sair,Выйти,离开,離開,离开 +InGame.HelpMenu.Resume,,,Resume,Fortsetzen,Resume,Reanudar,Reanudar,Reprendre,Riprendi,再開,다시 시작,Continuar,Continuar,Вернуться,继续,繼續,继续 +InGame.HelpMenu.ConfirmLeaveGame,,,Are you sure you want to leave?,Möchtest du das Spiel wirklich verlassen?,Are you sure you want to leave?,¿Seguro que quieres salir del juego?,¿Seguro que quieres salir del juego?,Voulez-vous vraiment quitter ?,Vuoi davvero uscire?,終了してよろしいですか?,정말로 나갈까요?,Quer mesmo sair?,Quer mesmo sair?,Выйти из игры?,是否确定要离开?,確定離開?,是否确定要离开挑战? +InGame.InspectMenu.Description.NoInventoryNotice,,,This user isn't wearing any items you can inspect.,"Dieser Benutzer trägt keine Items, die du beschauen kannst.",This user isn't wearing any items you can inspect.,No puedes inspeccionar la ropa de este usuario porque no lleva nada puesto.,No puedes inspeccionar la ropa de este usuario porque no lleva nada puesto.,Cette utilisateur ne porte aucun objet possible d'être inspecté.,Questo utente non indossa alcun oggetto che puoi esaminare.,このユーザーはあなたが確認できるアイテムをつけていません。,이 사용자는 내가 확인할 수 있는 아이템을 갖추고 있지 않습니다.,Este usuário não está usando nenhum item que você possa inspecionar.,Este usuário não está usando nenhum item que você possa inspecionar.,Этот пользователь не носит интересующих вас предметов.,此用户未穿戴你可以检视的物品。,此使用者未穿戴可檢視的道具。,此用户未穿戴你可以检视的物品。 +InGame.InspectMenu.Description.MultipleBundlesNotice,,,This item is part of multiple bundles.,Dieses Item ist Teil mehrerer Pakete.,This item is part of multiple bundles.,Este objeto forma parte de varios paquetes.,Este objeto forma parte de varios paquetes.,Cet article fait partie de plusieurs paquets.,Questo oggetto fa parte di bundle multipli.,このアイテムは複数のバンドルの一部です。,이 아이템은 여러 번들의 일부입니다.,Este item faz parte de vários pacotes.,Este item faz parte de vários pacotes.,Этот предмет является частью нескольких наборов.,此物品为多个套装的一部分。,此道具為多個組合的一部分。,此物品为多个套装的一部分。 +InGame.InspectMenu.Description.SingleBundleNotice,,,This item is part of a bundle.,Dieses Item ist Teil eines Pakets.,This item is part of a bundle.,Este objeto forma parte de un paquete.,Este objeto forma parte de un paquete.,Cet article fait partie d'un paquet.,Questo oggetto fa parte di un bundle.,このアイテムは、バンドルの一部です。,이 아이템은 번들의 일부입니다.,Este item faz parte de um pacote.,Este item faz parte de um pacote.,Этот предмет является частью набора.,此物品为套装的一部分。,此道具為組合的一部分。,此物品为套装的一部分。 +InGame.InspectMenu.Action.TakeOff,,,Take Off,Abnehmen,Take Off,Quitar,Quitar,Enlever,Togli,外す,삭제,Remover,Remover,Снять,脱下,脫下,脱下 +InGame.InspectMenu.Action.TryOn,,,Try On,Anprobieren,Try On,Probar,Probar,Essaie,Prova,つける,해 보기,Experimentar,Experimentar,Примерить,试穿,試穿,试穿 +InGame.InspectMenu.Action.Buy,,,Buy,Kaufen,Buy,Comprar,Comprar,Acheter,Compra,買う,구매,Comprar,Comprar,Купить,购买,購買,购买 +InGame.InspectMenu.Action.Inspect,,,Inspect,Beschauen,Inspect,Inspeccionar,Inspeccionar,Inspecter,Esamina,確認,확인,Inspecionar,Inspecionar,Осмотреть,检视,檢視,检视 +InGame.InspectMenu.Action.View,,,View,Ansehen,View,Ver,Ver,Voir,Osserva,表示,보기,Ver,Ver,Осмотр,查看,查看,查看 +InGame.InspectMenu.Label.Owned,,,Owned,Im Besitz,Owned,En posesión,En posesión,Possédé,Posseduto,所有済み,소유함,Possuído,Possuído,В наличии,已拥有,已擁有,已拥有 +InGame.InspectMenu.Label.Offsale,,,Offsale,Offsale,Offsale,Fuera de venta,Fuera de venta,Pas en vente,Non in vendita,非売品,오프세일,Indisponível,Indisponível,Распродано,下架,下架,下架 +InGame.InspectMenu.Label.Limited,,,Limited,In limitierter Auflage,Limited,Limitado,Limitado,Limité-e,Limitato,限定,한정품,Limitado,Limitado,Ограничено,限量,限定,限量 +InGame.InspectMenu.Label.Inventory,,,{PLAYER_NAME}'s Inventory,{PLAYER_NAME}s Inventar,{PLAYER_NAME}'s Inventory,Inventario de {PLAYER_NAME},Inventario de {PLAYER_NAME},Inventaire de {PLAYER_NAME},Inventario di {PLAYER_NAME},{PLAYER_NAME} さんのインベントリ,{PLAYER_NAME}님의 인벤토리,Inventário de {PLAYER_NAME},Inventário de {PLAYER_NAME},Инвентарь игрока {PLAYER_NAME},“{PLAYER_NAME}”的道具库,{PLAYER_NAME} 的道具欄,“{PLAYER_NAME}”的道具库 +InGame.InspectMenu.Label.CurrentlyWearing,,,Currently Wearing,Derzeit getragen,Currently Wearing,Ropa actual,Ropa actual,Porté-e actuellement,Indossato attualmente,今つけているもの,현재 장착 중,Vestindo atualmente,Vestindo atualmente,Сейчас одето,目前穿戴,目前穿戴,目前穿戴 +InGame.InspectMenu.Label.By,,,By {CREATOR},Von {CREATOR},By {CREATOR},De {CREATOR},De {CREATOR},Par {CREATOR},Da {CREATOR},{CREATOR} 作,{CREATOR} 제작,De {CREATOR},De {CREATOR},Создатель: {CREATOR},创作者:{CREATOR},創作者:{CREATOR},创作者:{CREATOR} +InGame.InspectMenu.Description.LimitedNotice,,,This item can only be purchased from resellers in the Catalog.,Dieses Item kann nur von Fachhändlern im Katalog gekauft werden.,This item can only be purchased from resellers in the Catalog.,Este objeto solo puede comprarse de revendedores en el catálogo.,Este objeto solo puede comprarse de revendedores en el catálogo.,Objet disponible à l'achat uniquement à des revendeurs dans le Catalogue. ,Questo oggetto può essere acquistato solo dai rivenditori presenti nel catalogo.,このアイテムはカタログ内の再販者からのみ購入できます。,이 아이템은 카탈로그의 리셀러에게서만 구입할 수 있습니다.,Este item só pode ser comprado de revendedores no Catálogo.,Este item só pode ser comprado de revendedores no Catálogo.,Этот предмет можно приобрести только у продавцов из каталога.,此项目只能在商店中从转售者处购买。,此道具只能在型錄裡透過轉賣者購買。,此项目只能在商店中从转售者处购买。 +InGame.InspectMenu.Label.Costume,,,{PLAYER_NAME}'s Avatar,{PLAYER_NAME}s Avatar,{PLAYER_NAME}'s Avatar,Avatar de {PLAYER_NAME},Avatar de {PLAYER_NAME},L'avatar de {PLAYER_NAME},Avatar di {PLAYER_NAME},{PLAYER_NAME} さんのアバター,{PLAYER_NAME}님의 아바타,Avatar de {PLAYER_NAME},Avatar de {PLAYER_NAME},Аватар игрока {PLAYER_NAME},{PLAYER_NAME}的虚拟形象,{PLAYER_NAME} 的虛擬人偶,{PLAYER_NAME}的虚拟形象 +InGame.InspectMenu.Label.Avatar,,,{PLAYER_NAME}'s Avatar,{PLAYER_NAME}s Avatar,{PLAYER_NAME}'s Avatar,Avatar de {PLAYER_NAME},Avatar de {PLAYER_NAME},L'avatar de {PLAYER_NAME},Avatar di {PLAYER_NAME},{PLAYER_NAME} さんのアバター,{PLAYER_NAME}님의 아바타,Avatar de {PLAYER_NAME},Avatar de {PLAYER_NAME},Аватар игрока {PLAYER_NAME},{PLAYER_NAME}的虚拟形象,{PLAYER_NAME} 的虛擬人偶,{PLAYER_NAME}的虚拟形象 +InGame.InspectMenu.Label.NoResellers,,,No Resellers,Keine Fachhändler,No Resellers,No hay revendedores,No hay revendedores,Aucun revendeur,Nessun rivenditore,再販者がいません,재판매자가 없어요,Nenhum revendedor,Nenhum revendedor,Нет посредников,无人转售,沒有轉賣者,无人转售 +InGame.InspectMenu.Label.PremiumOnly,,,Premium Only,Nur Premium,Premium Only,Solo Premium,Solo Premium,Premium uniquement,Solo Premium,Premium限定,Premium 전용,Apenas Premium,Apenas Premium,Только премиум-подписка,仅限 Premium,Premium 限定,仅限 Premium +NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Decreased,,,Decreased to {RBX_NUMBER},Verringert auf {RBX_NUMBER},Decreased to {RBX_NUMBER},Reducción a {RBX_NUMBER},Reducción a {RBX_NUMBER},Réduit jusqu'à {RBX_NUMBER},Sceso a {RBX_NUMBER},{RBX_NUMBER}に減少,{RBX_NUMBER}(으)로 감소,Reduzido para {RBX_NUMBER},Reduzido para {RBX_NUMBER},Уменьшено до {RBX_NUMBER},减至 {RBX_NUMBER},減少到 {RBX_NUMBER},减至 {RBX_NUMBER} +NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Increased,,,Increased to {RBX_NUMBER},Erhöht auf {RBX_NUMBER},Increased to {RBX_NUMBER},Aumento hasta {RBX_NUMBER},Aumento hasta {RBX_NUMBER},Augmenté jusqu'à {RBX_NUMBER},Salito a {RBX_NUMBER},{RBX_NUMBER}に増加,{RBX_NUMBER}(으)로 증가,Aumentado para {RBX_NUMBER},Aumentado para {RBX_NUMBER},Увеличено до {RBX_NUMBER},增至 {RBX_NUMBER},增加到 {RBX_NUMBER},增至 {RBX_NUMBER} +NotificationScript2.NewFollower,,,New Follower {RBX_NAME} is now following you!,Neuer Follower {RBX_NAME} folgt dir jetzt!,New Follower {RBX_NAME} is now following you!,Seguidor nuevo: ¡{RBX_NAME} te está siguiendo!,Seguidor nuevo: ¡{RBX_NAME} te está siguiendo!,"Désormais, le nouvel abonné, {RBX_NAME}, vous suit !",Il nuovo follower {RBX_NAME} ha cominciato a seguirti!,新規フォロワー {RBX_NAME} さんがあなたをフォローしました!,새 팔로워 {RBX_NAME}님이 회원님을 팔로우합니다!,"O novo seguidor, {RBX_NAME}, está seguindo você!","O novo seguidor, {RBX_NAME}, está seguindo você!",На вас подписался пользователь {RBX_NAME}!,新粉丝“{RBX_NAME}”现在关注了你!,{RBX_NAME} 正在追蹤您!,新粉丝“{RBX_NAME}”现在关注了你! +NotificationScript2.FriendRequestEvent.Accept,,,You are now friends with {RBX_NAME}!,Du bist jetzt Freunde mit {RBX_NAME}!,You are now friends with {RBX_NAME}!,Ahora eres amigo de {RBX_NAME}.,Ahora eres amigo de {RBX_NAME}.,Vous êtes maintenant ami avec {RBX_NAME}!,Sei ora amici di {RBX_NAME}!,{RBX_NAME} さんと友達になりました!,이제 {RBX_NAME}님과 친구예요!,Agora você é amigo de {RBX_NAME}!,Agora você é amigo de {RBX_NAME}!,Вы теперь друзья с {RBX_NAME}!,你现在与 {RBX_NAME} 成为好友了!,您已與 {RBX_NAME} 成為好友!,你现在与 {RBX_NAME} 成为好友了! +NotificationScript2.onPointsRewarded.negative,,,You lost {RBX_NUMBER} points!,Du hast {RBX_NUMBER} Punkte verloren!,You lost {RBX_NUMBER} points!,¡Has perdido {RBX_NUMBER} puntos!,¡Has perdido {RBX_NUMBER} puntos!,Vous avez perdu {RBX_NUMBER} points !,Hai perso {RBX_NUMBER} punti!,{RBX_NUMBER} ポイント失いました!,{RBX_NUMBER} 포인트를 잃었어요!,Você perdeu {RBX_NUMBER} pontos!,Você perdeu {RBX_NUMBER} pontos!,Вы потеряли {RBX_NUMBER} очк.!,你丢失了 {RBX_NUMBER} 点!,您失去 {RBX_NUMBER} 點!,你丢失了 {RBX_NUMBER} 点! +NotificationScript2.onPointsAwarded.single,,,You received {RBX_NUMBER} point!,Du hast {RBX_NUMBER} Punkt erhalten!,You received {RBX_NUMBER} point!,¡Has recibido {RBX_NUMBER} punto!,¡Has recibido {RBX_NUMBER} punto!,Vous avez reçu {RBX_NUMBER} point !,Hai ricevuto {RBX_NUMBER} punto!,{RBX_NUMBER} ポイントをゲットしました!,{RBX_NUMBER} 포인트를 받았어요!,Você recebeu {RBX_NUMBER} ponto!,Você recebeu {RBX_NUMBER} ponto!,Вы получили {RBX_NUMBER} очк.!,你得到了 {RBX_NUMBER} 点!,您得到 {RBX_NUMBER} 點!,你得到了 {RBX_NUMBER} 点! +NotificationScript2.onPointsAwarded.multiple,,,You received {RBX_NUMBER} points!,Du hast {RBX_NUMBER} Punkte erhalten!,You received {RBX_NUMBER} points!,¡Has recibido {RBX_NUMBER} puntos!,¡Has recibido {RBX_NUMBER} puntos!,Vous avez reçu {RBX_NUMBER} points !,Hai ricevuto {RBX_NUMBER} punti!,{RBX_NUMBER} ポイントをゲットしました!,{RBX_NUMBER} 포인트를 받았어요!,Você recebeu {RBX_NUMBER} pontos!,Você recebeu {RBX_NUMBER} pontos!,Вы получили {RBX_NUMBER} очк.!,你得到了 {RBX_NUMBER} 点!,您得到 {RBX_NUMBER} 點!,你得到了 {RBX_NUMBER} 点! +NotificationScript2.onBadgeAwardedTitle,,,Badge Awarded,Abzeichen verliehen,Badge Awarded,Emblema concedido,Emblema concedido,Badge accordé,Contrassegno conferito,授与されたバッジ,배지 획득,Emblema concedido,Emblema concedido,Получен значок,已获得徽章,已獲得徽章,已获得徽章 +NotificationScript2.onBadgeAwardedDetail,,,"{RBX_NAME} won {CREATOR_NAME}'s ""{BADGE_NAME}"" award!",{RBX_NAME} hat das „{BADGE_NAME}“ von {CREATOR_NAME} gewonnen!,"{RBX_NAME} won {CREATOR_NAME}'s ""{BADGE_NAME}"" award!","¡{RBX_NAME} ha ganado el premio ""{BADGE_NAME}"" de {CREATOR_NAME}!","¡{RBX_NAME} ha ganado el premio ""{BADGE_NAME}"" de {CREATOR_NAME}!",{RBX_NAME} a gagné la récompense « {BADGE_NAME} » de {CREATOR_NAME} !,"{RBX_NAME} ha vinto il premio ""{BADGE_NAME}"" di {CREATOR_NAME}!",{RBX_NAME} が {CREATOR_NAME} の「{BADGE_NAME}」を受賞!,{RBX_NAME}님이 보상으로 {CREATOR_NAME}의 '{BADGE_NAME}'을(를) 획득했어요!,"{RBX_NAME} ganhou o prêmio ""{BADGE_NAME}"" de {CREATOR_NAME}!","{RBX_NAME} ganhou o prêmio ""{BADGE_NAME}"" de {CREATOR_NAME}!",{RBX_NAME} получает награду {CREATOR_NAME} «{BADGE_NAME}»!,{RBX_NAME}贏得了{CREATOR_NAME}的“{BADGE_NAME}”徽章!,{RBX_NAME} 贏得了 {CREATOR_NAME} 的「{BADGE_NAME}」徽章!,{RBX_NAME}贏得了{CREATOR_NAME}的“{BADGE_NAME}”徽章! +NotificationScript2.Screenshot.Title,,,Screenshot Taken,Screenshot erstellt,Screenshot Taken,Captura tomada,Captura tomada,Capture d'écran enregistrée,Screenshot catturato,撮影したスクリーンショット,스크린숏 찍기 완료,Captura de tela salva,Captura de tela salva,Снимок экрана сделан,已截取屏幕快照,已截圖,已截取屏幕快照 +NotificationScript2.Screenshot.Description,,,Check out your screenshots folder to see it.,"Schau dir deinen Screenshots-Ordner an, um ihn zu sehen.",Check out your screenshots folder to see it.,Puedes verla en la carpeta de capturas de pantalla.,Puedes verla en la carpeta de capturas de pantalla.,Ouvrez le dossier des captures d'écran pour la visualiser.,Vai nella cartella screenshot per vederlo.,見るにはスクリーンショットフォルダをチェック。,스크린숏 폴더에서 확인하세요.,Confira sua pasta de capturas de tela para vê-la.,Confira sua pasta de capturas de tela para vê-la.,"Откройте папку со снимками экрана, чтобы его увидеть.",请检查你的屏幕快照文件夹以查看。,前往您的截圖資料夾查看。,请检查你的屏幕快照文件夹以查看。 +NotificationScript2.Screenshot.ButtonText,,,Open Folder,Ordner öffnen,Open Folder,Abrir carpeta,Abrir carpeta,Ouvrir le dossier,Apri cartella,フォルダを開く,폴더 열기,Abrir pasta,Abrir pasta,Открыть папку,打开文件夹,開啟資料夾,打开文件夹 +NotificationScript2.Video.Title,,,Video Recorded,Video aufgenommen,Video Recorded,Vídeo grabado,Vídeo grabado,Vidéo enregistrée,Video registrato,録画したビデオ,녹화 비디오 완료,Vídeo gravado,Vídeo gravado,Видео записано,视频已录制,已錄製影片,视频已录制 +NotificationScript2.Video.Description,,,Check out your videos folder to see it.,"Schau dir deinen Video-Ordner an, um es zu sehen.",Check out your videos folder to see it.,Puedes verlo en la carpeta de vídeos.,Puedes verlo en la carpeta de vídeos.,Ouvrez le dossier des vidéos pour la visualiser.,Vai nella cartella video per vederlo.,見るにはビデオフォルダをチェック。,비디오 폴더에서 확인하세요.,Confira sua pasta de vídeos para vê-lo.,Confira sua pasta de vídeos para vê-lo.,"Откройте папку с видеозаписями, чтобы его увидеть.",检查你的视频文件夹以查看。,前往您的影片資料夾查看。,检查你的视频文件夹以查看。 +NotificationScript2.Video.ButtonText,,,Open Folder,Ordner öffnen,Open Folder,Abrir carpeta,Abrir carpeta,Ouvrir le dossier,Apri cartella,フォルダを開く,폴더 열기,Abrir pasta,Abrir pasta,Открыть папку,打开文件夹,開啟資料夾,打开文件夹 +PlayerDropDown.onUnfollowButtonPress.success,,,No longer following {RBX_NAME}.,Folgst nicht mehr {RBX_NAME},No longer following {RBX_NAME}.,Ya no estás siguiendo a {RBX_NAME}.,Ya no estás siguiendo a {RBX_NAME}.,Vous ne suivez plus {RBX_NAME}.,Non stai seguendo ancora {RBX_NAME}.,{RBX_NAME} さんをフォローしていません。,{RBX_NAME}님 팔로우 취소.,Você não está mais seguindo {RBX_NAME}.,Você não está mais seguindo {RBX_NAME}.,Вы перестали следовать {RBX_NAME}.,已取消关注“{RBX_NAME}”,已取消追蹤 {RBX_NAME}。,已取消关注“{RBX_NAME}” +PlayerDropDown.onFollowButtonPress.success,,,now following {RBX_NAME},folgst jetzt {RBX_NAME},now following {RBX_NAME},Estás siguiendo a {RBX_NAME},Estás siguiendo a {RBX_NAME},Vous suivez désormais {RBX_NAME}.,stai seguendo {RBX_NAME},{RBX_NAME} さんをフォロー中,{RBX_NAME} 팔로우 중,Seguindo {RBX_NAME},Seguindo {RBX_NAME},подписались на {RBX_NAME},现正关注“{RBX_NAME}”,你正在追蹤 {RBX_NAME},现正关注“{RBX_NAME}” +PlayerDropDown.CannotSendFriendRequest,,,Cannot send friend request,Freundesanfrage konnte nicht gesendet werden,Cannot send friend request,No se puede enviar una solicitud de amistad,No se puede enviar una solicitud de amistad,Tu ne peux pas envoyer de demande d’ami,Impossibile inviare richiesta di amicizia,友達リクエストを送信できません,친구 요청 전송 불가,Impossível enviar pedido de amizade,Impossível enviar pedido de amizade,Нельзя отправить запрос на приглашение в друзья,无法发送好友邀请,無法傳送好友邀請,无法发送好友邀请 +PlayerDropDown.FriendLimit,,,You are at the max friends limit.,Du hast die max. Anzahl an Freunden erreicht.,You are at the max friends limit.,Has llegado al número máximo de amigos permitido.,Has llegado al número máximo de amigos permitido.,Tu as atteint la limite d'amis.,Hai raggiunto il limite massimo di amici.,友達の数が上限に達しました。,친구 수가 최대 한도에 도달했어요.,Você alcançou o limite máximo de amigos.,Você alcançou o limite máximo de amigos.,Количество ваших друзей достигло максимума.,你的好友人数已达上限。,您已達好友人數上限。,你的好友人数已达上限。 +PlayerDropDown.OtherPlayerFriendLimit,,,{RBX_NAME} is at the max friends limit.,{RBX_NAME} hat die max. Anzahl an Freunden erreicht.,{RBX_NAME} is at the max friends limit.,{RBX_NAME} ha llegado al número máximo de amigos permitido.,{RBX_NAME} ha llegado al número máximo de amigos permitido.,{RBX_NAME} a atteint la limite d'amis.,{RBX_NAME} ha raggiunto il limite massimo di amici.,{RBX_NAME} さんで友達の数が上限に達しました。,{RBX_NAME}님의 친구 수가 최대 한도에 도달했어요.,{RBX_NAME} alcançou o limite máximo de amigos.,{RBX_NAME} alcançou o limite máximo de amigos.,Максимальное количество ваших друзей: {RBX_NAME},{RBX_NAME} 的好友人数已达上限。,{RBX_NAME} 已達好友人數上限。,{RBX_NAME} 的好友人数已达上限。 +PlayerDropDown.UnFollow,,,Unfollow,Nicht mehr folgen,Unfollow,Dejar de seguir,Dejar de seguir,Ne plus suivre,Non seguire più,フォローをやめる,팔로우 취소,Deixar de seguir,Deixar de seguir,Отписаться,取消关注,取消追蹤,取消关注 +PlayerDropDown.Follow,,,Follow,Folgen,Follow,Seguir,Seguir,Suivre,Segui,フォロー,팔로우,Seguir,Seguir,Подписаться,关注,追蹤,关注 +PlayerDropDown.UnBlock,,,Unblock Player,Spieler nicht mehr sperren,Unblock Player,Desbloquear jugador,Desbloquear jugador,Débloquer joueur,Sblocca giocatore,プレイヤーのブロックを解除,플레이어 차단 해제,Desbloquear jogador,Desbloquear jogador,Разблокировать игрока,取消屏蔽玩家,解除封鎖玩家,取消屏蔽玩家 +PlayerDropDown.Block,,,Block Player,Spieler sperren,Block Player,Bloquear jugador,Bloquear jugador,Bloquer joueur,Blocca giocatore,プレイヤーをブロック,플레이어 차단,Bloquear jogador,Bloquear jogador,Заблокировать игрока,屏蔽玩家,封鎖玩家,屏蔽玩家 +PlayerDropDown.Report,,,Report Abuse,Verstoß melden,Report Abuse,Denunciar abuso,Denunciar abuso,Signaler une infraction,Segnala un abuso,規約違反を報告,신고하기,Denunciar abuso,Denunciar abuso,Сообщить о нарушении,举报滥用,檢舉濫用,举报滥用 +PlayerDropDown.Examine,,,Examine Avatar,Avatar inspizieren,Examine Avatar,Inspeccionar avatar,Inspeccionar avatar,Examiner l'avatar,Esamina avatar,アバターをチェック,아바타 검토,Examinar avatar,Examinar avatar,Посмотреть аватар,查看虚拟形象,檢視人偶,查看虚拟形象 +PlayerDropDown.Unfriend,,,Unfriend,Von der Freundesliste entfernen,Unfriend,Cancelar amistad,Cancelar amistad,Retirer des amis,Togli amicizia,友達解除,친구 끊기,Remover amigo,Remover amigo,Удалить из друзей,解除好友关系,刪除好友,解除好友关系 +PlayerDropDown.FriendRequest,,,Friend Request,Anfrage senden,Friend Request,Enviar solicitud de amistad,Enviar solicitud de amistad,Demande d'amitié,Richiesta di amicizia,友達リクエスト,친구 요청,Solicitar amizade,Solicitar amizade,Запрос дружбы,好友请求,好友邀請,好友请求 +PlayerDropDown.CancelRequest,,,Cancel Request,Anfrage abbrechen,Cancel Request,Cancelar solicitud,Cancelar solicitud,Annuler la demande,Annulla richiesta,リクエストをキャンセル,요청 취소,Cancelar pedido,Cancelar pedido,Отменить запрос,取消请求,取消邀請,取消请求 +PlayerDropDown.Accept,,,Accept,Annehmen,Accept,Aceptar,Aceptar,Accepter,Accetta,承認する,수락,Aceitar,Aceitar,Принять,接受,接受,接受 +InGame.PlayerList.Players,,,Players,Spieler,Players,Jugadores,Jugadores,Joueurs,Giocatori,プレイヤー,플레이어,Jogadores,Jogadores,Игроки,玩家,玩家,玩家 +InGame.Presence.Label.InGame,,,In Game,Im Spiel,In Game,En el juego,En el juego,En Jeu,Nel gioco,ゲーム内,게임 이용 중,No Jogo,No Jogo,В игре,正在游戏中,遊戲中,正在游戏中 +InGame.Presence.Label.InStudio,,,In Studio,In Studio,In Studio,En Studio,En Studio,En Studio,In Studio,Studio内,Studio 사용 중,No Studio,No Studio,В студии,正在 Studio 中,在 Studio 中,正在 Studio 中 +InGame.Presence.Label.Offline,,,Offline,Offline,Offline,Sin conexión,Sin conexión,Hors ligne,Offline,オフライン,오프라인,Desconectado,Desconectado,Не в сети,离线,離線,离线 +InGame.Presence.Label.Online,,,Online,Online,Online,En línea,En línea,En ligne,Online,オンライン,온라인,Conectado,Conectado,В сети,在线,在線,在线 +PurchasePromptScript.PURCHASE_FAILED.CANNOT_GET_BALANCE,,,Cannot retrieve your balance at this time. Your account has not been charged. Please try again later.,Dein Guthaben kann derzeit nicht abgerufen werden. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.,Cannot retrieve your balance at this time. Your account has not been charged. Please try again later.,En este momento no es posible acceder a tu saldo. No se ha llevado a cabo ningún cobro en tu cuenta. Inténtalo de nuevo más tarde.,En este momento no es posible acceder a tu saldo. No se ha llevado a cabo ningún cobro en tu cuenta. Inténtalo de nuevo más tarde.,Impossible d'obtenir votre solde pour l'instant. Votre compte n'a pas été débité. Veuillez réessayer plus tard.,Impossibile recuperare i tuoi fondi in questo momento. Il tuo account non ha subito addebiti. Riprova più tardi.,現在は残額を参照できません。アカウントがチャージされませんでした。しばらくしてからやり直してください。,현재 잔액을 불러올 수 없어요. 계정에 비용이 청구되지 않습니다. 나중에 다시 시도하세요.,Impossível obter seu saldo no momento. Nada foi cobrado da sua conta. Tente novamente mais tarde.,Impossível obter seu saldo no momento. Nada foi cobrado da sua conta. Tente novamente mais tarde.,Не удалось получить доступ к вашему балансу. Средства с вашего счета не списаны. Повторите попытку позже.,当前无法读取你的余额。你的帐户未被扣款。请稍候重试。,目前無法取得您的餘額。您的帳號餘額維持不變,請稍後再試。,当前无法读取你的余额。你的帐户未被扣款。请稍候重试。 +PurchasePromptScript.ERROR_MSG.PURCHASE_DISABLED,,,In-game purchases are temporarily disabled,Käufe im Spiel derzeit deaktiviert sind,In-game purchases are temporarily disabled,Las compras dentro de la aplicación están desactivadas temporalmente,Las compras dentro de la aplicación están desactivadas temporalmente,Les achats intégrés sont temporairement désactivés,Gli acquisti all'interno del gioco sono temporaneamente disattivati,ゲーム内購入は一時的に無効になっています。,일시적으로 게임 내 구매를 이용할 수 없어요,As compras no jogo estão temporariamente desabilitadas,As compras no jogo estão temporariamente desabilitadas,Внутриигровые покупки временно недоступны,游戏内购买功能暂时停用,遊戲中購買暫時停用,游戏内购买功能暂时停用 +PurchasePromptScript.ERROR_MSG.MAINTENANCE,,,ROBLOX is performing maintenance,ROBLOX Wartungsarbeiten durchführt,ROBLOX is performing maintenance,ROBLOX está efectuando tareas de mantenimiento,ROBLOX está efectuando tareas de mantenimiento,ROBLOX est en cours de maintenance,ROBLOX sta effettuando la manutenzione,ROBLOXはメンテナンス中です。,ROBLOX 유지보수 중입니다,ROBLOX está em manutenção,ROBLOX está em manutenção,Проводится техническое обслуживание ROBLOX,Roblox 正在维护中,ROBLOX 正在進行維修,Roblox 正在维护中 +PurchasePromptScript.setBuyMoreRobuxDialog.PostBalanceText,,,The remaining {RBX_NUMBER} ROBUX will be credited to your balance.,Die verbleibenden {RBX_NUMBER} ROBUX werden deinem Guthaben gutgeschrieben.,The remaining {RBX_NUMBER} ROBUX will be credited to your balance.,Los {RBX_NUMBER} ROBUX restantes se cargarán a tu saldo.,Los {RBX_NUMBER} ROBUX restantes se cargarán a tu saldo.,Les {RBX_NUMBER} ROBUX restants seront portés à votre solde.,I rimanenti {RBX_NUMBER} ROBUX verranno accreditati ai tuoi fondi.,残りの {RBX_NUMBER} ROBUX があなたのアカウントの残高に戻ります。,남은 {RBX_NUMBER} ROBUX는 회원님의 계정에 적립됩니다.,Os {RBX_NUMBER} ROBUX restantes serão somados ao seu saldo.,Os {RBX_NUMBER} ROBUX restantes serão somados ao seu saldo.,Оставшиеся {RBX_NUMBER} ROBUX будут перечислены на ваш счет.,剩余的 {RBX_NUMBER} ROBUX 将计入你的余额。,剩餘的 {RBX_NUMBER} ROBUX 將會加入您的餘額。,剩余的 {RBX_NUMBER} ROBUX 将计入你的余额。 +PurchasePromptScript.PURCHASE_FAILED.THIRD_PARTY_DISABLED,,,Third-party item sales have been disabled for this place. Your account has not been charged.,Items von Drittanbietern können an diesem Ort nicht verkauft werden. Dein Konto wurde nicht belastet.,Third-party item sales have been disabled for this place. Your account has not been charged.,Las ventas de objetos de terceros están desactivadas para este lugar. No se ha llevado a cabo ningún cobro en tu cuenta.,Las ventas de objetos de terceros están desactivadas para este lugar. No se ha llevado a cabo ningún cobro en tu cuenta.,Les ventes d'objets par des tiers ont été désactivées pour cet emplacement. Votre compte n'a pas été débité.,Le vendite di oggetti di terzi sono state disattivate per questa località. Il tuo account non ha subito addebiti.,サードパーティ製のアイテムの販売はここでは禁止されています。アカウントがチャージされませんでした。,본 장소에 대한 제삼자 아이템 판매가 비활성화되었습니다. 계정에 비용이 청구되지 않아요.,Vendas de itens de terceiros foram desabilitadas para este local. Nada foi cobrado da sua conta.,Vendas de itens de terceiros foram desabilitadas para este local. Nada foi cobrado da sua conta.,Продажа сторонних товаров в этом месте запрещена. Средства с вашего счета не списаны.,此地点的第三方物品拍卖已停用。你的帐户未被扣款。,此空間的第三方買賣目前停用,您的帳號餘額維持不變。,此地点的第三方物品拍卖已停用。你的帐户未被扣款。 +PurchasePromptScript.PURCHASE_FAILED.NOT_ENOUGH_TIX,,,This item cost more tickets than you currently have. Try trading currency on www.roblox.com to get more tickets.,"Dieses Item kostet mehr Tickets, als du derzeit besitzt. Auf www.roblox.com kannst du Währungen tauschen, um mehr Tickets zu erhalten.",This item cost more tickets than you currently have. Try trading currency on www.roblox.com to get more tickets.,Este objeto cuesta más tiques de los que tienes en este momento. Intenta convertir tipos de moneda en www.roblox.com para obtener más tiques.,Este objeto cuesta más tiques de los que tienes en este momento. Intenta convertir tipos de moneda en www.roblox.com para obtener más tiques.,Cet objet coûte plus de tickets que vous n'en avez. Essayez d'échanger des devises sur www.roblox.com pour obtenir plus de tickets.,Questo oggetto costa più ticket di quanti tu ne abbia. Prova a scambiare valuta sul sito www.roblox.com per ottenere più ticket.,このアイテムの入手には現在お持ちのチケットでは足りません。www.roblox.com でお金のトレーディングをしてチケットを手に入れよう。,본 아이템은 현재 소지한 티켓보다 비싸요. 더 많은 티켓을 받으려면 www.roblox.com에서 통화를 거래하세요.,Este item custa mais tickets do que você tem no momento. Experimente trocar moeda do jogo em www.roblox.com para obter mais.,Este item custa mais tickets do que você tem no momento. Experimente trocar moeda do jogo em www.roblox.com para obter mais.,"Цена товара превышает имеющееся у вас количество купонов. Обменяйте валюту на странице www.roblox.com, чтобы получить больше купонов.",此物品的价格超过你当前拥有的票单。在 www.roblox.com 上尝试交易货币以获取更多票单。,您的票券不足,無法購買此道具。請在 www.roblox.com 交易貨幣,取得更多票券。,此物品的价格超过你当前拥有的票单。在 www.roblox.com 上尝试交易货币以获取更多票单。 +PurchasePromptScript.PURCHASE_FAILED.NOT_FOR_SALE,,,This item is not currently for sale. Your account has not been charged.,Dieses Item steht derzeit nicht zum Verkauf. Dein Konto wurde nicht belastet.,This item is not currently for sale. Your account has not been charged.,Este objeto no está a la venta en este momento. No se ha llevado a cabo ningún cobro en tu cuenta.,Este objeto no está a la venta en este momento. No se ha llevado a cabo ningún cobro en tu cuenta.,Cet objet n'est pas en vente pour l'instant. Votre compte n'a pas été débité.,Questo oggetto non è attualmente in vendita. Il tuo account non ha subito addebiti.,このアイテムは現在売られていません。アカウントがチャージされませんでした。,판매 중인 아이템이 아닙니다. 계정에 비용이 청구되지 않아요.,Este item não está disponível para compra no momento. Nada foi cobrado da sua conta.,Este item não está disponível para compra no momento. Nada foi cobrado da sua conta.,Этот товар в настоящее время не продается. Средства с вашего счета не списаны.,此物品当前为非卖品。你的帐户未被扣款。,此道具目前為非賣品。您的帳號餘額維持不變。,此物品当前为非卖品。你的帐户未被扣款。 +PurchasePromptScript.setPurchaseDataInGui.invalidBC,,,This item requires {RBX_NAME1}. Click 'Upgrade' to upgrade your Builders Club!,"Dieses Item erfordert {RBX_NAME1}. Klicke auf „Aufwerten“, um deinen Builders-Club-Status zu verbessern!",This item requires {RBX_NAME1}. Click 'Upgrade' to upgrade your Builders Club!,"Este objeto requiere {RBX_NAME1}. Dale a ""Mejorar"" para mejorar el Builders Club.","Este objeto requiere {RBX_NAME1}. Dale a ""Mejorar"" para mejorar el Builders Club.",Cet objet nécessite {RBX_NAME1}. Cliquez sur « Améliorer » pour améliorer votre Builders Club !,"Questo oggetto richiede: {RBX_NAME1}. Clicca su ""Migliora"" per potenziare il tuo Builders Club!",このアイテムには{RBX_NAME1} が必要です。「アップグレード」をクリックしてBuilders Clubをアップグレード!,본 아이템은 {RBX_NAME1}이(가) 필요합니다. '업그레이드' 버튼을 클릭해 Builders Club을 업그레이드하세요!,Este item requer {RBX_NAME1}. Clique em 'Melhorar' para melhorar seu Builders Club!,Este item requer {RBX_NAME1}. Clique em 'Melhorar' para melhorar seu Builders Club!,"Для этого предмета требуется: {RBX_NAME1}. Нажмите кнопку «Улучшение», чтобы улучшить клуб создателей!",此物品需要 {RBX_NAME1}。点按“升级”以升级你的 Builders Club !,此道具需要 {RBX_NAME1}。按下「升級」來升級您的 Builders Club!,此物品需要 {RBX_NAME1}。点按“升级”以升级你的 Builders Club ! +PurchasePromptScript.PURCHASE_FAILED.LIMITED,,,This limited item has no more copies. Try buying from another user on www.roblox.com. Your account has not been charged.,Von diesem limitierten Item gibt es keine weiteren Exemplare. Such auf www.roblox.com nach einem anderen Verkäufer. Dein Konto wurde nicht belastet.,This limited item has no more copies. Try buying from another user on www.roblox.com. Your account has not been charged.,Este objeto limitado no tiene más copias. Intenta comprárselo a otro usuario en www.roblox.com. No se ha llevado a cabo ningún cobro en tu cuenta.,Este objeto limitado no tiene más copias. Intenta comprárselo a otro usuario en www.roblox.com. No se ha llevado a cabo ningún cobro en tu cuenta.,Il n'y a plus d'exemplaires de cet objet en série limitée. Essayez de l'acheter à un autre utilisateur sur www.roblox.com. Votre compte n'a pas été débité.,Questo oggetto limitato non è più disponibile. Prova ad acquistarlo da un altro utente sul sito www.roblox.com. Il tuo account non ha subito addebiti.,この限定アイテムはもう残っていません。www.roblox.com で他のユーザから購入してみてください。アカウントがチャージされませんでした。,본 한정 아이템은 더 이상 재고가 없어요. www.roblox.com에서 다른 사용자에게 구매해보세요. 계정에 비용이 청구되지 않아요.,Esgotaram-se as cópias deste item limitado. Tente comprar de outro usuário em www.roblox.com. Nada foi cobrado da sua conta.,Esgotaram-se as cópias deste item limitado. Tente comprar de outro usuário em www.roblox.com. Nada foi cobrado da sua conta.,Этого ограниченного товара больше нет в продаже. Попробуйте купить его у другого пользователя на сайте www.roblox.com. Средства с вашего счета не списаны.,此限量物品已售完。请尝试在 www.roblox.com 上从别的用户手中购买。你的帐户未被扣款。,此限量道具已售完,請前往 www.roblox.com 向其他使用者購買。您的帳號餘額維持不變。,此限量物品已售完。请尝试在 www.roblox.com 上从别的用户手中购买。你的帐户未被扣款。 +PurchasePromptScript.PURCHASE_MSG.PURCHASE,,,Want to buy the {RBX_NAME1} {RBX_NAME2} for,Möchtest du {RBX_NAME1} {RBX_NAME2} kaufen für:,Want to buy the {RBX_NAME1} {RBX_NAME2} for,¿Quieres comprar {RBX_NAME1} {RBX_NAME2} por,¿Quieres comprar {RBX_NAME1} {RBX_NAME2} por,Vous voulez acheter le {RBX_NAME1} {RBX_NAME2} pour,Vuoi comprare {RBX_NAME1} {RBX_NAME2} per,{RBX_NAME1} {RBX_NAME2} を以下の価格で購入希望:,다음 금액으로 {RBX_NAME1} {RBX_NAME2}을(를) 구매할까요,Deseja comprar {RBX_NAME1} {RBX_NAME2} por,Deseja comprar {RBX_NAME1} {RBX_NAME2} por,Купить {RBX_NAME1} {RBX_NAME2} за,你想要以如下价格购买“{RBX_NAME1} - {RBX_NAME2}”:,購買{RBX_NAME1} {RBX_NAME2}?價格為,你想要以如下价格购买“{RBX_NAME1} - {RBX_NAME2}”: +PurchasePromptScript.PURCHASE_FAILED.CANNOT_GET_ITEM_PRICE,,,We couldn't retrieve the price of the item at this time. Your account has not been charged. Please try again later.,Der Preis des Items kann derzeit nicht abgerufen werden. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.,We couldn't retrieve the price of the item at this time. Your account has not been charged. Please try again later.,En este momento no podemos acceder al precio de este objeto. No se ha llevado a cabo ningún cobro en tu cuenta. Inténtalo de nuevo más tarde.,En este momento no podemos acceder al precio de este objeto. No se ha llevado a cabo ningún cobro en tu cuenta. Inténtalo de nuevo más tarde.,Nous ne pouvons pas récupérer le prix de cet objet pour l'instant. Votre compte n'a pas été débité. Veuillez réessayer plus tard.,Impossibile recuperare il prezzo dell'oggetto in questo momento. Il tuo account non ha subito addebiti. Riprova più tardi.,現在はアイテムの価格が参照できません。アカウントがチャージされませんでした。しばらくしてからやり直してください。,일시적으로 아이템 가격을 불러올 수 없습니다. 계정에 비용이 청구되지 않아요. 나중에 다시 시도하세요.,Não conseguimos obter o preço do item no momento. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Não conseguimos obter o preço do item no momento. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Не удалось получить данные о цене товара. Средства с вашего счета не списаны. Повторите попытку позже.,当前无法读取物品的价格。你的帐户未被扣款。请稍候重试。,目前無法取得道具價格。您的帳號餘額維持不變,請稍後再試。,当前无法读取物品的价格。你的帐户未被扣款。请稍候重试。 +PurchasePromptScript.PURCHASE_MSG.FREE,,,Would you like to take {RBX_NAME2} for FREE?,Möchtest du „{RBX_NAME2}“ gerne GRATIS nehmen?,Would you like to take {RBX_NAME2} for FREE?,¿Quieres obtener {RBX_NAME2} GRATIS?,¿Quieres obtener {RBX_NAME2} GRATIS?,Souhaitez-vous prendre l'objet {RBX_NAME2} GRATUITEMENT ?,Vuoi prendere l'oggetto {RBX_NAME2} GRATIS?,アイテム名{RBX_NAME2} を無料で入手したいですか?,무료로 {RBX_NAME2}을(를) 받을까요?,Gostaria de obter {RBX_NAME2} GRÁTIS?,Gostaria de obter {RBX_NAME2} GRÁTIS?,Хотите получить товар {RBX_NAME2} БЕСПЛАТНО?,你想要免费拿到“{RBX_NAME2}”吗?,您要免費領取 {RBX_NAME2} 嗎?,你想要免费拿到“{RBX_NAME2}”吗? +PurchasePromptScript.PURCHASE_FAILED.PROMPT_PURCHASE_ON_GUEST,,,"You need to create a ROBLOX account to buy items, visit www.roblox.com for more info.","Du musst ein ROBLOX-Konto erstellen, um Items zu kaufen. Auf www.roblox.com findest du weitere Infos.","You need to create a ROBLOX account to buy items, visit www.roblox.com for more info.",Tienes que crear una cuenta de ROBLOX para comprar objetos. Visita www.roblox.com para obtener más información.,Tienes que crear una cuenta de ROBLOX para comprar objetos. Visita www.roblox.com para obtener más información.,"Vous devez créer un compte ROBLOX pour acheter des objets, rendez-vous sur www.roblox.com pour plus d'informations.","Per poter comprare oggetti, devi creare un account ROBLOX. Visita il sito www.roblox.com per maggiori informazioni.",アイテムを購入するにはROBLOX アカウントを作る必要があります。詳しくは www.roblox.com で。,아이템을 구매하려면 ROBLOX 계정을 만들어야 해요. 자세한 정보는 www.roblox.com을 방문하세요.,Você precisa criar uma conta ROBLOX para comprar itens. Visite www.roblox.com para mais informações.,Você precisa criar uma conta ROBLOX para comprar itens. Visite www.roblox.com para mais informações.,"Создайте учетную запись ROBLOX, чтобы покупать предметы. Посетите сайт www.roblox.com.",你需要创建 ROBLOX 帐户以购买物品,访问 www.roblox.com 以获取更多信息。,若要購買道具,請前往 www.roblox.com 建立 Roblox 帳號。,你需要创建 ROBLOX 帐户以购买物品,访问 www.roblox.com 以获取更多信息。 +PurchasePromptScript.setBuyMoreRobuxDialog.descriptionText,,,You need {RBX_NUMBER} more ROBUX to buy the {RBX_NAME1} {RBX_NAME2}. Would you like to buy more ROBUX?,"Du brauchst noch {RBX_NUMBER} ROBUX, um {RBX_NAME1} {RBX_NAME2} zu kaufen. Möchtest du mehr ROBUX kaufen?",You need {RBX_NUMBER} more ROBUX to buy the {RBX_NAME1} {RBX_NAME2}. Would you like to buy more ROBUX?,Necesitas {RBX_NUMBER} ROBUX más para comprar {RBX_NAME1} {RBX_NAME2}. ¿Quieres comprar más ROBUX?,Necesitas {RBX_NUMBER} ROBUX más para comprar {RBX_NAME1} {RBX_NAME2}. ¿Quieres comprar más ROBUX?,Vous avez besoin de {RBX_NUMBER} ROBUX de plus pour acheter le {RBX_NAME1} {RBX_NAME2}. Souhaitez-vous acheter plus de ROBUX ?,Hai bisogno di {RBX_NUMBER} ROBUX in più per comprare: {RBX_NAME1} {RBX_NAME2}. Vuoi acquistare altri ROBUX?,{RBX_NAME1} {RBX_NAME2}.を買うにはあと{RBX_NUMBER}のROBUXが必要です。もっとROBUXを買いますか?,{RBX_NUMBER} ROBUX가 부족하여 {RBX_NAME1} {RBX_NAME2}을(를) 구매할 수 없습니다. ROBUX를 구매하시겠습니까?,Você precisa de mais {RBX_NUMBER} ROBUX para comprar o(a) {RBX_NAME2} {RBX_NAME1}. Gostaria de comprar mais ROBUX?,Você precisa de mais {RBX_NUMBER} ROBUX para comprar o(a) {RBX_NAME2} {RBX_NAME1}. Gostaria de comprar mais ROBUX?,Для покупки товара {RBX_NAME1} {RBX_NAME2} требуется еще {RBX_NUMBER} ROBUX. Приобрести больше ROBUX?,你还需要 {RBX_NUMBER} ROBUX 才能购买 {RBX_NAME1} {RBX_NAME2}。你想要购买更多 ROBUX 吗?,您還需要 {RBX_NUMBER} Robux 才能購買{RBX_NAME1} {RBX_NAME2}。您要加購 Robux 嗎?,你还需要 {RBX_NUMBER} ROBUX 才能购买 {RBX_NAME1} {RBX_NAME2}。你想要购买更多 ROBUX 吗? +PurchasePromptScript.PURCHASE_FAILED.UNDER_13,,,Your account is under 13. Purchase of this item is not allowed. Your account has not been charged.,Dein Konto ist für Spieler unter 13 Jahren. Der Kauf dieses Items ist nicht gestattet. Dein Konto wurde nicht belastet.,Your account is under 13. Purchase of this item is not allowed. Your account has not been charged.,Tu cuenta es para menores de 13 años. La compra de este objeto no está permitida. No se ha llevado a cabo ningún cobro en tu cuenta.,Tu cuenta es para menores de 13 años. La compra de este objeto no está permitida. No se ha llevado a cabo ningún cobro en tu cuenta.,Votre compte est Moins de 13 ans. L'achat de cet objet n'est pas permis. Votre compte n'a pas été débité.,Il tuo account è per giocatori con meno di 13 anni. L'acquisto di questo oggetto non è permesso. Il tuo account non ha subito addebiti.,あなたのアカウントは13才以下です。このアイテムの購入は認められていません。アカウントがチャージされませんでした。,만 13세 미만의 계정으로 본 아이템을 구매할 수 없어요. 계정에 비용이 청구되지 않아요.,Sua conta é para menor de 13 anos. A compra deste item não é permitida. Nada foi cobrado da sua conta.,Sua conta é para menor de 13 anos. A compra deste item não é permitida. Nada foi cobrado da sua conta.,Возраст владельца учетной записи не превышает 13 лет. Вы не можете купить этот товар. Средства с вашего счета не списаны.,此帐户的用户未满 13 岁,不允许购买此物品。你的帐户未被扣款。,您的帳號為 13 歲以下,不允許購買此道具。您的帳號餘額維持不變。,此帐户的用户未满 13 岁,不允许购买此物品。你的帐户未被扣款。 +PurchasePromptScript.PURCHASE_MSG.BALANCE_FUTURE,,,Your balance after this transaction will be {RBX_NUMBER}.,Nach dieser Transaktion wird dein Guthaben {RBX_NUMBER} betragen.,Your balance after this transaction will be {RBX_NUMBER}.,Tu saldo después de esta transacción será de {RBX_NUMBER}.,Tu saldo después de esta transacción será de {RBX_NUMBER}.,Votre solde après cette transaction sera de {RBX_NUMBER}.,"Dopo la transazione, i tuoi fondi saranno {RBX_NUMBER}.",取引後の残高は{RBX_NUMBER}になります。,본 거래 후의 예상 잔액은 {RBX_NUMBER}입니다.,Seu saldo depois desta transação será de {RBX_NUMBER}.,Seu saldo depois desta transação será de {RBX_NUMBER}.,После этой операции ваш баланс составит {RBX_NUMBER},此次交易后,你的余额将为 {RBX_NUMBER}。,您在此交易後的餘額將為 {RBX_NUMBER}。,此次交易后,你的余额将为 {RBX_NUMBER}。 +PurchasePromptScript.PURCHASE_MSG.BALANCE_NOW,,,Your balance is now {RBX_NUMBER}.,Dein Guthaben beträgt nun {RBX_NUMBER}.,Your balance is now {RBX_NUMBER}.,Tu saldo es ahora de {RBX_NUMBER}.,Tu saldo es ahora de {RBX_NUMBER}.,Votre solde est désormais de {RBX_NUMBER}.,Ora i tuoi fondi sono {RBX_NUMBER}.,残高は現在 {RBX_NUMBER} です。,현재 잔액은 {RBX_NUMBER}입니다.,Seu saldo agora é {RBX_NUMBER}.,Seu saldo agora é {RBX_NUMBER}.,Сейчас ваш баланс составляет {RBX_NUMBER}.,你的当前余额为 {RBX_NUMBER}。,您目前的餘額為 {RBX_NUMBER}。,你的当前余额为 {RBX_NUMBER}。 +PurchasePromptScript.ERROR_MSG.FAILED_NO_ITEM_NAME,,,Your purchase failed because {ERROR_REASON}. Your account has not been charged. Please try again later.,Dein Kauf ist fehlgeschlagen. Grund: {ERROR_REASON}. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.,Your purchase failed because {ERROR_REASON}. Your account has not been charged. Please try again later.,Se ha producido un error con tu compra debido a {ERROR_REASON}. No se te ha cobrado. Inténtalo de nuevo más tarde. ,Se ha producido un error con tu compra debido a {ERROR_REASON}. No se te ha cobrado. Inténtalo de nuevo más tarde. ,Échec de la transaction. Motif : {ERROR_REASON}. Votre compte n'a pas été débité. Veuillez réessayer plus tard.,L'acquisto non è andato a buon fine a causa del seguente errore: {ERROR_REASON}. Il tuo account non ha subito addebiti. Riprova più tardi.,{ERROR_REASON}のため購入を完了できませんでした。アカウントへの請求は行われていません。後でもう一度お試し下さい。,{ERROR_REASON} 때문에 구매에 실패했어요. 계정에 비용이 청구되지 않았어요. 나중에 다시 시도하세요.,Sua compra falhou. Motivo: {ERROR_REASON}. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Sua compra falhou. Motivo: {ERROR_REASON}. Nada foi cobrado da sua conta. Tente de novo mais tarde.,Произвести покупку не удалось: {ERROR_REASON}. Средства с вашей учетной записи не списаны. Повторите попытку позже.,由于{ERROR_REASON},你未能成功购买。你的帐户未被扣款。请稍候重试。,{ERROR_REASON},無法購買。您的帳號餘額維持不變,請稍後再試。,由于{ERROR_REASON},你未能成功购买。你的帐户未被扣款。请稍候重试。 +PurchasePromptScript.PURCHASE_MSG.FAILED,,,Your purchase of {RBX_NAME1} failed because {RBX_NAME2}. Your account has not been charged. Please try again later.,"Du konntest „{RBX_NAME1}“ nicht kaufen, da {RBX_NAME2}. Dein Konto wurde nicht belastet. Bitte versuche es später erneut.",Your purchase of {RBX_NAME1} failed because {RBX_NAME2}. Your account has not been charged. Please try again later.,Tu compra de {RBX_NAME1} no ha funcionado porque {RBX_NAME2}. No se ha realizado ningún cobro. Inténtalo de nuevo más tarde.,Tu compra de {RBX_NAME1} no ha funcionado porque {RBX_NAME2}. No se ha realizado ningún cobro. Inténtalo de nuevo más tarde.,Votre achat de {RBX_NAME1} a échoué à cause de {RBX_NAME2}. Votre compte n'a pas été débité. Veuillez réessayer plus tard.,Il tuo acquisto di {RBX_NAME1} non è riuscito perché: {RBX_NAME2}. Il tuo account non ha subito addebiti. Riprova più tardi.,{RBX_NAME1} のため、 {RBX_NAME2} の購入に失敗しました。アカウントがチャージされませんでした。しばらくしてからやり直してください。,{RBX_NAME2} 때문에 {RBX_NAME1}에 실패했어요. 계정에 비용이 청구되지 않았어요. 나중에 다시 시도하세요.,Sua compra de {RBX_NAME1} fracassou. Motivo: {RBX_NAME2}. Nada foi cobrado da sua conta. Tente novamente mais tarde.,Sua compra de {RBX_NAME1} fracassou. Motivo: {RBX_NAME2}. Nada foi cobrado da sua conta. Tente novamente mais tarde.,Не удалось купить товар {RBX_NAME1}: {RBX_NAME2}. Средства с вашего счета не списаны. Повторите попытку позже.,由于“{RBX_NAME2}”,未能成功购买“{RBX_NAME1}”。你的帐户未被扣款。请稍候重试。,{RBX_NAME2},無法購買 {RBX_NAME1}。您的帳號餘額維持不變,請稍後再試。,由于“{RBX_NAME2}”,未能成功购买“{RBX_NAME1}”。你的帐户未被扣款。请稍候重试。 +PurchasePromptScript.PURCHASE_MSG.SUCCEEDED,,,Your purchase of {RBX_NAME1} succeeded!,Du hast „{RBX_NAME1}“ gekauft!,Your purchase of {RBX_NAME1} succeeded!,¡Tu compra de {RBX_NAME1} ha funcionado!,¡Tu compra de {RBX_NAME1} ha funcionado!,Votre achat de {RBX_NAME1} a réussi !,Il tuo acquisto di {RBX_NAME1} è andato a buon fine!,{RBX_NAME1} の購入に成功しました!,{RBX_NAME1} 구매를 완료했어요!,Sua compra de {RBX_NAME1} foi bem-sucedida!,Sua compra de {RBX_NAME1} foi bem-sucedida!,Вы купили товар {RBX_NAME1}!,购买“{RBX_NAME1}”成功!,成功購買 {RBX_NAME1}!,购买“{RBX_NAME1}”成功! +PurchasePromptScript.ERROR_MSG.UNKNOWN,,,something went wrong,ein Problem aufgetreten ist,something went wrong,algo ha ido mal,algo ha ido mal,quelque chose s'est mal passé,qualcosa è andato storto,エラーが起きました,오류가 발생했어요,algo deu errado,algo deu errado,возникли проблемы,有地方出错,發生錯誤,有地方出错 +PurchasePromptScript.ERROR_MSG.INVALID_FUNDS,,,your account does not have enough ROBUX,dein Konto nicht über genügend ROBUX verfügt,your account does not have enough ROBUX,tu cuenta no tiene suficientes ROBUX,tu cuenta no tiene suficientes ROBUX,votre compte ne possède pas assez de ROBUX,il tuo account non ha abbastanza ROBUX,アカウントのROBUXが足りません。,계정에 ROBUX가 부족합니다,sua conta não tem ROBUX suficientes,sua conta não tem ROBUX suficientes,у вас недостаточно ROBUX,你的帐户没有足够的 Robux,您的 Robux 不足,你的帐户没有足够的 Robux +StatsUtil.KBps,,,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} Ko/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} kB/s,{RBX_NUMBER} kB/s,{RBX_NUMBER} кБ/с,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s,{RBX_NUMBER} KB/s +StatsUtil.MB,,,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} Mo,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} МБ,{RBX_NUMBER} MB,{RBX_NUMBER} MB,{RBX_NUMBER} MB +StatsUtil.ms,,,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} мс,{RBX_NUMBER} ms,{RBX_NUMBER} ms,{RBX_NUMBER} ms +,,,/join or /j : join channel.,/join oder /j : Kanal beitreten,/join or /j : join channel.,/join o /j : unirse al canal.,/join o /j : unirse al canal.,/join ou /j  : rejoindre le canal.,/join o /j : accedi al canale.,/join <チャンネル> または /j <チャンネル> : で、チャンネルに参加。,/join <채널> 또는 /j <채널> : 채널 가입.,/join ou /j : juntar-se a um canal.,/join ou /j : juntar-se a um canal.,/join <канал> или /j <канал> : подключиться к каналу.,/join <频道名称> or /j <频道名称> : 加入频道。,/join <頻道名稱> or /j <頻道名稱>:加入頻道。,/join <频道名称> or /j <频道名称> : 加入频道。 +,,,/leave or /l : leave channel. (leaves current if none specified),/leave oder /l : Kanal verlassen. (Ohne Angabe des Kanals wird der aktuelle Kanal verlassen.),/leave or /l : leave channel. (leaves current if none specified),/leave o /l : salir del canal (sale del canal actual si no se especifica ninguno).,/leave o /l : salir del canal (sale del canal actual si no se especifica ninguno).,/leave ou /l  : quitter canal. (reste sur le même si aucun n'est spécifié),/leave o /l : lascia il canale (quello attuale se non specificato).,/leave <チャンネル> または /l <チャンネル> : でチャンネルを終了。 (指定しなければ現在のチャネルを終了),/leave <채널> 또는 /l <채널> : 채널 나가기. (미지정 시 현재 채널에서 나가기),"/leave ou /l : sair do canal. (sai do atual, se não especificado)","/leave ou /l : sair do canal. (sai do atual, se não especificado)","/leave <канал> или /l <канал> : покинуть канал. (Вы покинете текущий канал, если не укажете другой.)",/leave <频道名称> 或 /l <频道名称> : 离开频道。(若未指定频道名称,你将离开当前频道),/leave <頻道名稱> 或 /l <頻道名稱>:離開頻道。(若未指定頻道名稱,您將離開目前頻道),/leave <频道名称> 或 /l <频道名称> : 离开频道。(若未指定频道名称,你将离开当前频道) +,,,"1,2,3...","1,2,3...","1,2,3...","1,2,3...","1,2,3...","1, 2, 3...","1, 2, 3...",1、2、3...,"1,2,3...","1,2,3...","1,2,3...","1,2,3...","1,2,3...",1、2、3…,"1,2,3..." +,,,A Http error has occured. Please close the client and try again.,Ein HTTP-Fehler ist aufgetreten. Schließe den Client und versuche es erneut.,A Http error has occured. Please close the client and try again.,Error en el http. Cierra el cliente e inténtalo de nuevo.,Error en el http. Cierra el cliente e inténtalo de nuevo.,"Une erreur HTTP s'est produite. Veuillez fermer le client, puis réessayez.",Si è verificato un errore http. Chiudi il client e riprova.,Httpエラーが発生しました。クライアントを閉じて、もう一度試してください。,Http 오류가 발생했어요. 클라이언트를 닫고 다시 시도하세요.,Erro no http. Feche o cliente e tente novamente.,Erro no http. Feche o cliente e tente novamente.,"Произошла ошибка Http. Пожалуйста, закройте клиент и попробуйте снова.",发生 Http 错误。请关闭客户端并重试。,發生 HTTP 錯誤,請關閉客戶端並重新嘗試。,发生 Http 错误。请关闭客户端并重试。 +,,,A Http error has occured. Please close the client and try again.. ({COUNT:int}),Ein HTTP-Fehler ist aufgetreten. Schließe den Client und versuche es erneut.. ({COUNT:int}),A Http error has occured. Please close the client and try again.. ({COUNT:int}),Error en el http. Cierra el cliente e inténtalo de nuevo.. ({COUNT:int}),Error en el http. Cierra el cliente e inténtalo de nuevo.. ({COUNT:int}),"Une erreur HTTP s'est produite. Veuillez fermer le client, puis réessayez.. ({COUNT:int})",Si è verificato un errore http. Chiudi il client e riprova.. ({COUNT:int}),HTTPエラーが発生しました。クライアントを閉じて、もう一度試してください。 ({COUNT:int}),Http 오류가 발생했어요. 클라이언트를 닫고 다시 시도하세요. ({COUNT:int}),Erro no http. Feche o cliente e tente novamente. ({COUNT:int}),Erro no http. Feche o cliente e tente novamente. ({COUNT:int}),"Произошла ошибка Http. Пожалуйста, закройте клиент и попробуйте снова. ({COUNT:int})",发生 Http 错误。请关闭客户端并重试。 ({COUNT:int}),發生 HTTP 錯誤,請關閉客戶端並重新嘗試。({COUNT:int}),发生 Http 错误。请关闭客户端并重试。 ({COUNT:int}) +,,,A/Left Arrow,A/Linkspfeil,A/Left Arrow,A/Cursor izquierdo,A/Cursor izquierdo,A/Flèche gauche,A/Freccia SINISTRA,A/左カーソル,A/왼쪽 화살표,A/seta esquerda,A/seta esquerda,A/стрелка влево,A/左箭头,A、←,A/左箭头 +,,,Abuse Description,Beschreibung des Verstoßes,Abuse Description,Descripción del abuso,Descripción del abuso,Description de l'infraction,Descrizione abuso,規約違反の詳細,욕설 내용,Descrição do abuso,Descrição do abuso,Описание нарушения,滥用描述,濫用說明,滥用描述 +,,,Accept,Annehmen,Accept,Aceptar,Aceptar,Accepter,Accetta,同意する,수락,Aceitar,Aceitar,Принять,接受,接受,接受 +,,,Accept Friend Request,Freundschaftsanfrage akzeptieren,Accept Friend Request,Aceptar solicitud de amistad,Aceptar solicitud de amistad,Accepter l'invitation d'un ami,Accetta la richiesta di amicizia,友達リクエストを承認する,친구 요청 수락,Aceitar solicitação de amizade,Aceitar solicitação de amizade,Принять запрос друга,接受好友邀请,接受好友邀請,接受好友邀请 +,,,Accessories,Accessoires,Accessories,Accesorios,Accesorios,Accessoires,Accessori,アクセサリ,장신구,Acessórios,Acessórios,Аксессуары,配饰,飾品,配饰 +,,,Account: 13+,Konto: 13+,Account: 13+,Cuenta: 13+,Cuenta: 13+,Compte : 13+,Account: più di 13 anni,アカウント:13+,계정: 만 13세 이상,Conta: 13+,Conta: 13+,Учетная запись: 13+,帐户:13+,帳號:13+,帐户:13+ +,,,Account: <13,Konto: <13,Account: <13,Cuenta: <13,Cuenta: <13,Compte : <13,Account: <13,アカウント: <13,계정: 만 13세 미만,Conta: <13,Conta: <13,Учетная запись: <13,帐户:<13,帳號:<13,帐户:<13 +,,,Account: Over 13 yrs,Konto: Über 13 J.,Account: Over 13 yrs,Cuenta: Mayor de 13 años,Cuenta: Mayor de 13 años,Compte : Plus de 13 ans,Account: più di 13 anni,アカウント:13才以上,계정: 만 13세 이상,Conta: Mais de 13 anos,Conta: Mais de 13 anos,Учетная запись: cтарше 13 лет,帐户:超过 13 岁,帳號:13 歲以上,帐户:超过 13 岁 +,,,Account: Under 13 yrs,Konto: Unter 13 J.,Account: Under 13 yrs,Cuenta: Menor de 13 años,Cuenta: Menor de 13 años,Compte : Moins de 13 ans,Account: meno di 13 anni,アカウント:13才以下,계정: 만 13세 미만,Conta: Menor de 13 anos,Conta: Menor de 13 anos,Учетная запись: не старше 13 лет,帐户:13 岁以下,帳號:13 歲以下,帐户:13 岁以下 +,,,Add Friend,Freund hinzufügen,Add Friend,Añadir amigo,Añadir amigo,Ajouter ami,Aggiungi amico,友達を追加,친구 추가,Adicionar amigo,Adicionar amigo,Добавить друга,添加好友,新增好友,添加好友 +,,,Adjust,Anpassen,Adjust,Ajustar,Ajustar,Ajuster,Modifica,調節,조정,Ajustar,Ajustar,Настроить,调整,調整,调整 +,,,An error occurred while unblocking {RBX_NAME}. Please try again later.,Bei der Aufhebung der Sperre von {RBX_NAME} ist ein Fehler aufgetreten. Bitte versuche es später erneut.,An error occurred while unblocking {RBX_NAME}. Please try again later.,Se ha producido un error al desbloquear a {RBX_NAME}. Inténtalo de nuevo más tarde.,Se ha producido un error al desbloquear a {RBX_NAME}. Inténtalo de nuevo más tarde.,Une erreur est survenue lors de la levée du blocage de {RBX_NAME}. Veuillez réessayer plus tard.,Si è verificato un errore nello sblocco di {RBX_NAME}. Riprova più tardi.,{RBX_NAME} さんのブロック解除でエラーが発生しました。しばらくしてからやり直してください。,{RBX_NAME}님의 차단을 해제하는 중 오류가 발생했어요. 나중에 다시 시도하세요.,Um erro ocorreu ao desbloquear {RBX_NAME}. Tente novamente mais tarde.,Um erro ocorreu ao desbloquear {RBX_NAME}. Tente novamente mais tarde.,При разблокировке пользователя {RBX_NAME} произошла ошибка. Повторите попытку позже.,取消屏蔽“{RBX_NAME}”时出错。请稍候重试。,解除封鎖 {RBX_NAME} 時發生錯誤,請稍後再試。,取消屏蔽“{RBX_NAME}”时出错。请稍候重试。 +,,,An error occurred while unfriending {RBX_NAME}. Please try again later.,Bei der Entfernung von {RBX_NAME} als Freund ist ein Fehler aufgetreten. Bitte versuche es später erneut.,An error occurred while unfriending {RBX_NAME}. Please try again later.,Se ha producido un error al cancelar la amistad con {RBX_NAME}. Inténtalo de nuevo más tarde.,Se ha producido un error al cancelar la amistad con {RBX_NAME}. Inténtalo de nuevo más tarde.,Une erreur est survenue lors de la suppression de l'ami {RBX_NAME}. Veuillez réessayer plus tard.,Si è verificato un errore nel togliere l'amicizia a {RBX_NAME}. Riprova più tardi.,{RBX_NAME} さんの友達解除でエラーが発生しました。しばらくしてからやり直してください。,{RBX_NAME}님을 친구 취소하는 중 오류가 발생했어요. 나중에 다시 시도하세요.,Um erro ocorreu ao cancelar amizade com {RBX_NAME}. Tente novamente mais tarde.,Um erro ocorreu ao cancelar amizade com {RBX_NAME}. Tente novamente mais tarde.,При удалении пользователя {RBX_NAME} из друзей произошла ошибка. Повторите попытку позже.,与“{RBX_NAME}”解除好友关系时出错。请稍候重试。,與 {RBX_NAME} 解除好友關係時發生錯誤,請稍後再試。,与“{RBX_NAME}”解除好友关系时出错。请稍候重试。 +,,,Animation,Animation,Animation,Animación,Animación,Animation,Animazione,アニメーション,애니메이션,Animação,Animação,Анимация,动画,動畫,动画 +,,,Are you sure you want to reset your character?,Möchtest du deinen Charakter wirklich zurücksetzen?,Are you sure you want to reset your character?,¿Seguro que deseas reiniciar tu personaje?,¿Seguro que deseas reiniciar tu personaje?,Voulez-vous vraiment réinitialiser le personnage ?,Vuoi davvero azzerare il tuo personaggio?,キャラクターをリセットしてよろしいですか?,캐릭터를 재설정하시겠습니까?,Quer mesmo reiniciar o personagem?,Quer mesmo reiniciar o personagem?,Сбросить персонажа?,是否确定要重置人物?,確定重置人偶?,是否确定要重置人物? +,,,Arms,Arme,Arms,Brazos,Brazos,Bras,Braccia,腕,팔,Braços,Braços,hua ,手臂,手臂,手臂 +,,,Audio,Audio,Audio,Audio,Audio,Audio,Audio,オーディオ,오디오,Áudio,Áudio,Звук,音频,音訊,音频 +,,,Automatic,Automatisch,Automatic,Automático,Automático,Automatique,Automatico,自動,자동,Automático,Automático,Автоматический,自动,自動,自动 +,,,Avatar,Avatar,Avatar,Avatar,Avatar,Avatar,Avatar,アバター,아바타,Avatar,Avatar,Аватар,虚拟形象,虛擬人偶,虚拟形象 +,,,Back Accessory,Rückseiten-Accessoire,Back Accessory,Accesorio trasero,Accesorio trasero,Accessoire arrière,Accessorio posteriore,背面アクセサリ,등 장신구,Acessório das costas,Acessório das costas,Задний аксессуар,背面配饰,背面飾品,背面配饰 +,,,Backpack,Rucksack,Backpack,Mochila,Mochila,Sac à dos,Zaino,バックパック,배낭,Mochila,Mochila,Рюкзак,背包,背包,背包 +,,,Backspace,Rücktaste,Backspace,Retroceso,Retroceso,Retour,BACKSPACE,バックスペース,백스페이스,Backspace,Backspace,Backspace,回退,退格鍵,回退 +,,,Badge,Abzeichen,Badge,Emblema,Emblema,Badge,Contrassegno,バッジ,배지,Emblema,Emblema,Значок,徽章,徽章,徽章 +,,,Badge Awarded,Abzeichen verliehen,Badge Awarded,Emblema concedido,Emblema concedido,Badge accordé,Contrassegno conferito,授与されたバッジ,배지 획득,Emblema concedido,Emblema concedido,Получен значок,已获得徽章,獲得徽章,已获得徽章 +,,,Block Player,Spieler sperren,Block Player,Bloquear jugador,Bloquear jugador,Bloquer joueur,Blocca giocatore,プレイヤーをブロック,플레이어 차단,Bloquear jogador,Bloquear jogador,Заблокировать игрока,取消屏蔽玩家,封鎖玩家,取消屏蔽玩家 +,,,Builders Club,Builders Club,Builders Club,Builders Club,Builders Club,Builders Club,Builders Club,Builders Club,Builders Club,Builders Club,Builders Club,Клуб создателей,Builders Club,Builders Club,Builders Club +,,,Bullying,Mobbing,Bullying,Abusos,Abusos,Harcèlement,Bullismo,いじめ,괴롭힘,Bullying,Bullying,Агрессивное поведение,欺凌,霸凌,欺凌 +,,,Button,Schaltfläche,Button,Botón,Botón,Bouton,Pulsante,ボタン,버튼,Botão,Botão,Кнопка,按钮,按鈕,按钮 +,,,Buy,Kaufen,Buy,Comprar,Comprar,Acheter,Compra,買う,구매,Comprar,Comprar,Купить,买,購買,买 +,,,Buy Now,Jetzt kaufen,Buy Now,Comprar ahora,Comprar ahora,Acheter maintenant,Compra ora,今すぐ買う,지금 구매하기,Comprar agora,Comprar agora,Купить,立即购买,現在購買,立即购买 +,,,Buy R$,R$ kaufen,Buy R$,Comprar R$,Comprar R$,Acheter des R$,Compra R$,R$ を買う,R$ 구매,Comprar R$,Comprar R$,Купить R$,购买 R$,購買 R$,购买 R$ +,,,"By clicking the 'Record Video' button, the menu will close and start recording your screen.","Wenn du auf die Schaltfläche „Video aufnehmen“ klickst, schließt sich das Menü und dein Bildschirm wird aufgezeichnet.","By clicking the 'Record Video' button, the menu will close and start recording your screen.","Al hacer clic en el botón ""Grabar vídeo"", el menú se cerrará y se empezará a grabar tu pantalla.","Al hacer clic en el botón ""Grabar vídeo"", el menú se cerrará y se empezará a grabar tu pantalla.","En cliquant sur le bouton Enregistrer vidéo, le menu se fermera et l'enregistrement de votre écran débutera.","Clicca sul pulsante ""Registra video"" per chiudere il menu e iniziare a registrare quanto avviene sullo schermo.",「ビデオの録画」ボタンをクリックするとメニューが閉じて画面の録画が始まります。,"""비디오 녹화"" 버튼을 클릭하면 본 화면이 종료되고 녹화가 시작됩니다.","Ao clicar no botão ‘Gravar vídeo’, o menu será fechado e a tela começará a ser gravada.","Ao clicar no botão ‘Gravar vídeo’, o menu será fechado e a tela começará a ser gravada.","После нажатия на кнопку «Запись видео» это меню закроется, и начнется запись видео с экрана.",点按“录制视频”按钮,目录会关闭,并开始录制你的屏幕。,按下「錄影」按鈕後,選單會關閉並開始錄下您的畫面。,点按“录制视频”按钮,目录会关闭,并开始录制你的屏幕。 +,,,"By clicking the 'Take Screenshot' button, the menu will close and take a screenshot and save it to your computer.","Wenn du auf die Schaltfläche „Screenshot machen“ klickst, schließt sich das Menü und ein Screenshot wird auf deinem Computer gespeichert.","By clicking the 'Take Screenshot' button, the menu will close and take a screenshot and save it to your computer.","Al hacer clic en el botón ""Hacer captura de pantalla"", el menú se cerrará y se hará una captura de pantalla que se guardará en tu ordenador.","Al hacer clic en el botón ""Hacer captura de pantalla"", el menú se cerrará y se hará una captura de pantalla que se guardará en tu ordenador.","En cliquant sur le bouton Capture d'écran, le menu se fermera et une capture d'écran sera sauvegardée dans votre ordinateur.","Clicca sul pulsante ""Cattura screenshot"" per chiudere il menu, catturare uno screenshot e salvarlo sul computer.",「スクリーンショットを撮る」ボタンをクリックするとメニューが閉じてスクリーンショットがパソコンに保存されます。,'스크린숏 찍기' 버튼을 클릭하면 본 화면 종료 후 스크린숏을 찍어 컴퓨터에 저장합니다.,"Ao clicar no botão ‘Captura de tela’, o menu será fechado e a tela será capturada e salva no seu computador.","Ao clicar no botão ‘Captura de tela’, o menu será fechado e a tela será capturada e salva no seu computador.","После нажатия на кнопку «Сделать снимок экрана» это меню закроется, и снимок сохранится на вашем компьютере.",点按“截取屏幕快照”按钮,目录会关闭,截取屏幕快照并保存至你的电脑。,按下「截圖」按鈕,選單會關閉並截圖儲存到您的電腦。,点按“截取屏幕快照”按钮,目录会关闭,截取屏幕快照并保存至你的电脑。 +,,,CPU,CPU,CPU,CPU,CPU,UCT,CPU,CPU,CPU,CPU,CPU,Процессор,CPU,中央處理器,CPU +,,,Camera Inverted,Kamera invertiert,Camera Inverted,Cámara invertida,Cámara invertida,Caméra inversée,Macchina fotografica invertita,カメラの反転,카메라 반전,Câmera Invertida,Câmera Invertida,Инвертированная камера,镜头反转,相機反轉,镜头反转 +,,,Camera Mode,Kameramodus,Camera Mode,Modo de cámara,Modo de cámara,Mode caméra,Modalità visuale,カメラのモード,카메라 모드,Modo da câmera,Modo da câmera,Режим камеры,镜头模式,相機模式,镜头模式 +,,,Camera Movement,Kamerabewegung,Camera Movement,Movimiento de la cámara,Movimiento de la cámara,Déplacement caméra,Movimento visuale,カメラの動き,카메라 이동,Movimento da câmera,Movimento da câmera,Движение камеры,镜头移动模式,相機移動,镜头移动模式 +,,,Camera Sensitivity,Kameraempfindlichkeit,Camera Sensitivity,Sensibilidad de la cámara,Sensibilidad de la cámara,Sensibilité de la caméra,Precisione telecamera,カメラ感度,카메라 민감도,Sensitividade da câmera,Sensitividade da câmera,Чувствительность камеры,镜头敏感度,相機靈敏度,镜头敏感度 +,,,Camera Zoom,Kamerazoom,Camera Zoom,Distancia de la cámara,Distancia de la cámara,Zoom caméra,Zoom visuale,カメラズーム,카메라 줌,Zoom da câmera,Zoom da câmera,Масштаб камеры,镜头缩放,相機縮放,镜头缩放 +,,,Can't follow user: {ERROR_REASON:translate},Folgen von Benutzer nicht möglich: {ERROR_REASON:translate},Can't follow user: {ERROR_REASON:translate},No es posible seguir al usuario: {ERROR_REASON:translate},No es posible seguir al usuario: {ERROR_REASON:translate},Impossible de suivre l'utilisateur : {ERROR_REASON:translate},Impossibile seguire l'utente: {ERROR_REASON:translate},ユーザーをフォローできません:{ERROR_REASON:translate},사용자를 팔로우할 수 없음: {ERROR_REASON:translate},Não é possível seguir o usuário: {ERROR_REASON:translate},Não é possível seguir o usuário: {ERROR_REASON:translate},Нельзя следить за пользователем: {ERROR_REASON:translate},由于{ERROR_REASON:translate},无法关注用户:,無法追蹤使用者:{ERROR_REASON:translate},由于{ERROR_REASON:translate},无法关注用户: +,,,Can't follow user: {ERROR_REASON:translate}. ({COUNT:int}),Folgen von Benutzer nicht möglich: {ERROR_REASON:translate}. ({COUNT:int}),Can't follow user: {ERROR_REASON:translate}. ({COUNT:int}),No es posible seguir al usuario: {ERROR_REASON:translate}. ({COUNT:int}),No es posible seguir al usuario: {ERROR_REASON:translate}. ({COUNT:int}),Impossible de suivre l'utilisateur : {ERROR_REASON:translate}. ({COUNT:int}),Impossibile seguire l'utente: {ERROR_REASON:translate}. ({COUNT:int}),ユーザーをフォローできません:{ERROR_REASON:translate}. ({COUNT:int}),사용자를 팔로우할 수 없음: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível seguir o usuário: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível seguir o usuário: {ERROR_REASON:translate}. ({COUNT:int}),Нельзя следить за пользователем: {ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},无法关注用户:. ({COUNT:int}),無法追蹤使用者:{ERROR_REASON:translate}。({COUNT:int}),由于{ERROR_REASON:translate},无法关注用户:. ({COUNT:int}) +,,,Can't follow user: {ERROR_REASON},Folgen von Benutzer nicht möglich: {ERROR_REASON},Can't follow user: {ERROR_REASON},No es posible seguir al usuario: {ERROR_REASON},No es posible seguir al usuario: {ERROR_REASON},Impossible de suivre l'utilisateur : {ERROR_REASON},Impossibile seguire l'utente: {ERROR_REASON},ユーザーをフォローできません:{ERROR_REASON},사용자를 팔로우할 수 없음: {ERROR_REASON},Não é possível seguir o usuário: {ERROR_REASON},Não é possível seguir o usuário: {ERROR_REASON},Нельзя следить за пользователем: {ERROR_REASON},由于{ERROR_REASON},无法关注用户:,無法追蹤使用者:{ERROR_REASON},由于{ERROR_REASON},无法关注用户: +,,,Can't join place {PLACEID:int}: {ERROR_REASON:translate},Beitritt zu {PLACEID:int} nicht möglich: {ERROR_REASON:translate},Can't join place {PLACEID:int}: {ERROR_REASON:translate},No es posible unirse al lugar {PLACEID:int}: {ERROR_REASON:translate},No es posible unirse al lugar {PLACEID:int}: {ERROR_REASON:translate},Impossible de rejoindre l'emplacement {PLACEID:int} : {ERROR_REASON:translate},Impossibile raggiungere la località {PLACEID:int}: {ERROR_REASON:translate},{PLACEID:int}のプレースに参加できません:{ERROR_REASON:translate},장소 {PLACEID:int}에 참여할 수 없음: {ERROR_REASON:translate},Não é possível entrar em {PLACEID:int}: {ERROR_REASON:translate},Não é possível entrar em {PLACEID:int}: {ERROR_REASON:translate},Нельзя присоединиться ({PLACEID:int}): {ERROR_REASON:translate},由于{ERROR_REASON:translate},你无法加入{PLACEID:int}:,無法加入空間 {PLACEID:int}:{ERROR_REASON:translate},由于{ERROR_REASON:translate},你无法加入{PLACEID:int}: +,,,Can't join place {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),Beitritt zu {PLACEID:int} nicht möglich: {ERROR_REASON:translate}. ({COUNT:int}),Can't join place {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),No es posible unirse al lugar {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),No es posible unirse al lugar {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),Impossible de rejoindre l'emplacement {PLACEID:int} : {ERROR_REASON:translate}. ({COUNT:int}),Impossibile raggiungere la località {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),{PLACEID:int}のプレースに参加できません:{ERROR_REASON:translate}. ({COUNT:int}),장소 {PLACEID:int}에 참여할 수 없음: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível entrar em {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível entrar em {PLACEID:int}: {ERROR_REASON:translate}. ({COUNT:int}),Нельзя присоединиться ({PLACEID:int}): {ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},你无法加入{PLACEID:int}:. ({COUNT:int}),無法加入空間 {PLACEID:int}:{ERROR_REASON:translate}。({COUNT:int}),由于{ERROR_REASON:translate},你无法加入{PLACEID:int}:. ({COUNT:int}) +,,,Can't join place {PLACEID}: {ERROR_REASON},Beitritt zu {PLACEID} nicht möglich: {ERROR_REASON},Can't join place {PLACEID}: {ERROR_REASON},No es posible unirse al lugar {PLACEID}: {ERROR_REASON},No es posible unirse al lugar {PLACEID}: {ERROR_REASON},Impossible de rejoindre l'emplacement {PLACEID} : {ERROR_REASON},Impossibile raggiungere la località {PLACEID}: {ERROR_REASON},{PLACEID} のプレースに参加できません:{ERROR_REASON},장소 {PLACEID}에 참여할 수 없음: {ERROR_REASON},Não é possível entrar em {PLACEID}: {ERROR_REASON},Não é possível entrar em {PLACEID}: {ERROR_REASON},Нельзя присоединиться ({PLACEID}): {ERROR_REASON},由于{ERROR_REASON},你无法加入{PLACEID}:,無法加入空間 {PLACEID}:{ERROR_REASON},由于{ERROR_REASON},你无法加入{PLACEID}: +,,,Canceling...,Abbrechen ...,Canceling...,Cancelando...,Cancelando...,Annulation...,Annullamento...,キャンセル中...,취소 중...,Cancelando...,Cancelando...,Отмена...,正在取消...,正在取消…,正在取消... +,,,Cannot find game server,Spielserver kann nicht gefunden werden,Cannot find game server,No se ha encontrado el servidor del juego,No se ha encontrado el servidor del juego,Serveur de jeu introuvable.,Impossibile trovare il server di gioco,ゲームサーバーが見つかりません,게임 서버를 찾을 수 없습니다,Não foi possível encontrar o servidor do jogo,Não foi possível encontrar o servidor do jogo,Не могу найти игровой сервер,无法找到游戏服务器,找不到遊戲伺服器,无法找到游戏服务器 +,,,Cannot find game server. ({COUNT:int}),Spielserver kann nicht gefunden werden. ({COUNT:int}),Cannot find game server. ({COUNT:int}),No se ha encontrado el servidor del juego. ({COUNT:int}),No se ha encontrado el servidor del juego. ({COUNT:int}),Serveur de jeu introuvable.. ({COUNT:int}),Impossibile trovare il server di gioco. ({COUNT:int}),ゲームサーバーが見つかりません ({COUNT:int}),게임 서버를 찾을 수 없습니다 ({COUNT:int}),Não foi possível encontrar o servidor do jogo ({COUNT:int}),Não foi possível encontrar o servidor do jogo ({COUNT:int}),Не могу найти игровой сервер ({COUNT:int}),无法找到游戏服务器 ({COUNT:int}),找不到遊戲伺服器。({COUNT:int}),无法找到游戏服务器 ({COUNT:int}) +,,,Cannot find game server. {RET:translate}...({COUNT:int}),Spielserver kann nicht gefunden werden. {RET:translate}...({COUNT:int}),Cannot find game server. {RET:translate}...({COUNT:int}),No se ha encontrado el servidor del juego. {RET:translate}...({COUNT:int}),No se ha encontrado el servidor del juego. {RET:translate}...({COUNT:int}),Serveur de jeu introuvable.. {RET:translate}...({COUNT:int}),Impossibile trovare il server di gioco. {RET:translate}...({COUNT:int}),ゲームサーバーが見つかりません。 {RET:translate}...({COUNT:int}),게임 서버를 찾을 수 없습니다. {RET:translate}...({COUNT:int}),Não foi possível encontrar o servidor do jogo. {RET:translate}...({COUNT:int}),Não foi possível encontrar o servidor do jogo. {RET:translate}...({COUNT:int}),Не могу найти игровой сервер. {RET:translate}...({COUNT:int}),无法找到游戏服务器. {RET:translate}...({COUNT:int}),找不到遊戲伺服器。{RET:translate}…({COUNT:int}),无法找到游戏服务器. {RET:translate}...({COUNT:int}) +,,,Cannot join game instance: {ERROR_REASON:translate},Beitritt zu Spielinstanz nicht möglich: {ERROR_REASON:translate},Cannot join game instance: {ERROR_REASON:translate},No es posible unirse a una instancia del juego: {ERROR_REASON:translate},No es posible unirse a una instancia del juego: {ERROR_REASON:translate},Impossible de rejoindre l'instance de jeu : {ERROR_REASON:translate},Impossibile accedere all'area di gioco: {ERROR_REASON:translate},ゲームインスタンスに参加できません:{ERROR_REASON:translate},게임 인스턴스에 참여할 수 없음: {ERROR_REASON:translate},Não é possível juntar-se ao jogo: {ERROR_REASON:translate},Não é possível juntar-se ao jogo: {ERROR_REASON:translate},Нельзя войти в локацию игры: {ERROR_REASON:translate},由于{ERROR_REASON:translate},无法加入游戏:,無法加入遊戲: {ERROR_REASON:translate},由于{ERROR_REASON:translate},无法加入游戏: +,,,Cannot join game instance: {ERROR_REASON:translate}. ({COUNT:int}),Beitritt zu Spielinstanz nicht möglich: {ERROR_REASON:translate}. ({COUNT:int}),Cannot join game instance: {ERROR_REASON:translate}. ({COUNT:int}),No es posible unirse a una instancia del juego: {ERROR_REASON:translate}. ({COUNT:int}),No es posible unirse a una instancia del juego: {ERROR_REASON:translate}. ({COUNT:int}),Impossible de rejoindre l'instance de jeu : {ERROR_REASON:translate}. ({COUNT:int}),Impossibile accedere all'area di gioco: {ERROR_REASON:translate}. ({COUNT:int}),ゲームインスタンスに参加できません:{ERROR_REASON:translate}. ({COUNT:int}),게임 인스턴스에 참여할 수 없음: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível juntar-se ao jogo: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível juntar-se ao jogo: {ERROR_REASON:translate}. ({COUNT:int}),Нельзя войти в локацию игры: {ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},无法加入游戏:. ({COUNT:int}),無法加入遊戲: {ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},无法加入游戏:. ({COUNT:int}) +,,,Cannot join game instance: {ERROR_REASON},Beitritt zu Spielinstanz nicht möglich: {ERROR_REASON},Cannot join game instance: {ERROR_REASON},No es posible unirse a una instancia del juego: {ERROR_REASON},No es posible unirse a una instancia del juego: {ERROR_REASON},Impossible de rejoindre l'instance de jeu : {ERROR_REASON},Impossibile accedere all'area di gioco: {ERROR_REASON},ゲームインスタンスに参加できません:{ERROR_REASON},게임 인스턴스에 참여할 수 없음: {ERROR_REASON},Não é possível juntar-se ao jogo: {ERROR_REASON},Não é possível juntar-se ao jogo: {ERROR_REASON},Нельзя войти в локацию игры: {ERROR_REASON},由于{ERROR_REASON},无法加入游戏:,無法加入遊戲: {ERROR_REASON},由于{ERROR_REASON},无法加入游戏: +,,,Cannot join private server: {ERROR_REASON:translate},Beitritt zu privatem Server nicht möglich: {ERROR_REASON:translate},Cannot join private server: {ERROR_REASON:translate},No es posible unirse al servidor privado: {ERROR_REASON:translate},No es posible unirse al servidor privado: {ERROR_REASON:translate},Impossible de rejoindre le serveur privé : {ERROR_REASON:translate},Impossibile accedere al server privato: {ERROR_REASON:translate},プライベートサーバーに参加できません:{ERROR_REASON:translate},비공개 서버에 참여할 수 없습니다: {ERROR_REASON:translate},Não é possível juntar-se ao servidor privado: {ERROR_REASON:translate},Não é possível juntar-se ao servidor privado: {ERROR_REASON:translate},Нельзя присоединиться к частному серверу: {ERROR_REASON:translate},由于{ERROR_REASON:translate},无法加入私人服务器:,無法加入私人伺服器:{ERROR_REASON:translate},由于{ERROR_REASON:translate},无法加入私人服务器: +,,,Cannot join private server: {ERROR_REASON:translate}. ({COUNT:int}),Beitritt zu privatem Server nicht möglich: {ERROR_REASON:translate}. ({COUNT:int}),Cannot join private server: {ERROR_REASON:translate}. ({COUNT:int}),No es posible unirse al servidor privado: {ERROR_REASON:translate}. ({COUNT:int}),No es posible unirse al servidor privado: {ERROR_REASON:translate}. ({COUNT:int}),Impossible de rejoindre le serveur privé : {ERROR_REASON:translate}. ({COUNT:int}),Impossibile accedere al server privato: {ERROR_REASON:translate}. ({COUNT:int}),プライベートサーバーに参加できません:{ERROR_REASON:translate}. ({COUNT:int}),비공개 서버에 참여할 수 없습니다: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível juntar-se ao servidor privado: {ERROR_REASON:translate}. ({COUNT:int}),Não é possível juntar-se ao servidor privado: {ERROR_REASON:translate}. ({COUNT:int}),Нельзя присоединиться к частному серверу: {ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},无法加入私人服务器:. ({COUNT:int}),無法加入私人伺服器:{ERROR_REASON:translate}. ({COUNT:int}),由于{ERROR_REASON:translate},无法加入私人服务器:. ({COUNT:int}) +,,,Cannot join private server: {ERROR_REASON},Beitritt zu privatem Server nicht möglich: {ERROR_REASON},Cannot join private server: {ERROR_REASON},No es posible unirse al servidor privado: {ERROR_REASON},No es posible unirse al servidor privado: {ERROR_REASON},Impossible de rejoindre le serveur privé : {ERROR_REASON},Impossibile accedere al server privato: {ERROR_REASON},プライベートサーバーに参加できません:{ERROR_REASON},비공개 서버에 참여할 수 없습니다: {ERROR_REASON},Não é possível juntar-se ao servidor privado: {ERROR_REASON},Não é possível juntar-se ao servidor privado: {ERROR_REASON},Нельзя присоединиться к частному серверу: {ERROR_REASON},由于{ERROR_REASON},无法加入私人服务器:,無法加入私人伺服器:{ERROR_REASON},由于{ERROR_REASON},无法加入私人服务器: +,,,Character Movement,Charakterbewegung,Character Movement,Movimiento del personaje,Movimiento del personaje,Déplacement personnage,Movimento personaggio,キャラクターの動き,캐릭터 이동,Movimento de personagem,Movimento de personagem,Движение персонажа,人物移动模式,人偶移動,人物移动模式 +,,,Chat,Chat,Chat,Chat,Chat,Chat,Chat,チャット,채팅,Chat,Chat,Чат,聊天,聊天,聊天 +,,,Chat ended because you didn't reply,"Chat wurde beendet, da du nicht geantwortet hast.",Chat ended because you didn't reply,El chat ha finalizado porque no has contestado.,El chat ha finalizado porque no has contestado.,Le chat a pris fin car vous n'avez pas répondu,"La chat terminata, non hai risposto",返事をしなかったのでチャットが終了しました,회원님이 대답을 하지 않아 채팅이 종료되었어요,O chat terminou porque você não respondeu,O chat terminou porque você não respondeu,Чат завершен: вы не ответили.,你没有回答,聊天结束,您沒有回覆,聊天已結束,你没有回答,聊天结束 +,,,Chat ended because you walked away,"Chat wurde beendet, da du dich entfernt hast.",Chat ended because you walked away,El chat ha finalizado porque te has alejado.,El chat ha finalizado porque te has alejado.,Le chat a pris fin car vous êtes parti,"La chat terminata, ti sei allontanato",立ち去ったのでチャットが終了しました,회원님이 채팅방을 나가 채팅이 종료되었어요,O chat terminou porque você se afastou,O chat terminou porque você se afastou,Чат завершен: вы ушли.,你已走开,聊天结束,您在範圍外,聊天已結束,你已走开,聊天结束 +,,,Cheating/Exploiting,Schummeln/Ausnutzen von Spielfehlern,Cheating/Exploiting,Trampas/abuso de errores,Trampas/abuso de errores,Triche/Exploitation,Inganno/Sfruttamento,チート/悪用,사기/악용,Trapaça/abuso,Trapaça/abuso,Жульничество,作弊/开挂,作弊 / 外掛,作弊/开挂 +,,,Check out your screenshots folder to see it.,Sieh ihn dir im Ordner mit den Screenshots an.,Check out your screenshots folder to see it.,Puedes verla en la carpeta de capturas de pantalla.,Puedes verla en la carpeta de capturas de pantalla.,Ouvrez le dossier des captures d'écran pour la visualiser.,Vai nella cartella screenshot per vederlo.,見るにはスクリーンショットフォルダをチェック。,스크린숏 폴더에서 확인하세요.,Confira sua pasta de capturas de tela para vê-la.,Confira sua pasta de capturas de tela para vê-la.,"Откройте папку со снимками экрана, чтобы его увидеть.",请检查你的屏幕快照文件夹以查看。,前往您的截圖資料夾查看。,请检查你的屏幕快照文件夹以查看。 +,,,Check out your videos folder to see it.,Sieh es dir im Ordner mit den Videos an.,Check out your videos folder to see it.,Puedes verlo en la carpeta de vídeos.,Puedes verlo en la carpeta de vídeos.,Ouvrez le dossier des vidéos pour la visualiser.,Vai nella cartella video per vederlo.,見るにはビデオフォルダをチェック。,비디오 폴더에서 확인하세요.,Confira sua pasta de vídeos para vê-lo.,Confira sua pasta de vídeos para vê-lo.,"Откройте папку с видеозаписями, чтобы его увидеть.",检查你的视频文件夹以查看。,前往您的影片資料夾查看。,检查你的视频文件夹以查看。 +,,,Choose One,Wähle eine der folgenden Optionen,Choose One,Elige uno,Elige uno,En choisir un(e),Scegli,ひとつ選ぶ,한 가지 선택,Escolha um,Escolha um,Выберите один вариант,选择一项,選擇,选择一项 +,,,Classic,Klassisch,Classic,Clásico,Clásico,Classique,Classica,クラシック,Classic,Clássico,Clássico,Классика,经典,經典,经典 +,,,Click to Move,Zum Bewegen klicken,Click to Move,Clic para moverse,Clic para moverse,Cliquer pour se déplacer,Clicca per muovere,クリックして移動,마우스 클릭으로 이동,Clique para mover,Clique para mover,Движение по нажатию,点按以移动,點擊移動,点按以移动 +,,,Climb Animation,Kletteranimation,Climb Animation,Animación de escalada,Animación de escalada,Animation d'escalade,Animazione scalata,登りアニメーション,오르기 애니메이션,Animação de escalada,Animação de escalada,Анимация подъема,攀爬动画,攀爬動畫,攀爬动画 +,,,Close Backpack,Rucksack schließen,Close Backpack,Cerrar mochila,Cerrar mochila,Fermer le sac à dos,Chiudi zaino,バックパックを閉じる,배낭 닫기,Fechar mochila,Fechar mochila,Закрыть рюкзак,关闭背包,關閉背包,关闭背包 +,,,Confirm Block,Sperre bestätigen,Confirm Block,Confirmar bloqueo,Confirmar bloqueo,Confirmer le blocage,Conferma blocco,ブロックを確認,차단 확인,Confirmar bloqueio,Confirmar bloqueio,Подтверждение блокировки,确认屏蔽,確認封鎖,确认屏蔽 +,,,Confirm Unblock,Aufhebung der Sperre bestätigen,Confirm Unblock,Confirmar desbloqueo,Confirmar desbloqueo,Confirmer le déblocage,Conferma sblocco,ブロック解除を確認,차단 해제 확인,Confirmar desbloqueio,Confirmar desbloqueio,Подтверждение разблокировки,确认取消屏蔽,確認解除封鎖,确认取消屏蔽 +,,,Connection attempt failed.,Verbindungsaufbau fehlgeschlagen.,Connection attempt failed.,Ha fallado el intento de conexión.,Ha fallado el intento de conexión.,Échec de la tentative de connexion.,Tentativo di connessione non riuscito.,接続に失敗しました。,연결 시도 실패.,Tentativa de conexão fracassada.,Tentativa de conexão fracassada.,Не удалось подключиться.,尝试连接失败。,連線失敗。,尝试连接失败。 +,,,Could not connect to game because game failed to start,"Verbindung zum Spiel konnte nicht hergestellt werden, da das Spiel nicht gestartet werden konnte.",Could not connect to game because game failed to start,No se ha podido conectar al juego porque no se ha podido iniciar.,No se ha podido conectar al juego porque no se ha podido iniciar.,Impossible de se connecter au jeu car il n'a pas réussi à être lancé,Impossibile connettersi al gioco perché l'avvio non è riuscito,ゲームの起動に失敗したため接続できませんでした。,게임 시작에 실패하여 접속할 수 없어요,Impossível conectar pois o jogo não conseguiu iniciar,Impossível conectar pois o jogo não conseguiu iniciar,"Не удалось подключиться к игре, так как она не запустилась.",开始此游戏失败,无法连接。,遊戲無法啟動,無法連線到遊戲,开始此游戏失败,无法连接。 +,,,Could not connect to game because game has ended,"Verbindung zum Spiel konnte nicht hergestellt werden, da das Spiel beendet wurde.",Could not connect to game because game has ended,No se ha podido conectar al juego porque ha terminado.,No se ha podido conectar al juego porque ha terminado.,Impossible de se connecter au jeu car il est terminé,Impossibile connettersi al gioco perché è terminato,ゲームが終了したため接続できませんでした。,게임이 종료되어 접속할 수 없어요,Impossível conectar pois o jogo já acabou,Impossível conectar pois o jogo já acabou,"Не удалось подключиться к игре, так как она завершилась.",此游戏已结束,无法连接。,遊戲已結束,無法連線到遊戲,此游戏已结束,无法连接。 +,,,Could not connect to game because game is disabled,"Verbindung zum Spiel konnte nicht hergestellt werden, da das Spiel deaktiviert ist.",Could not connect to game because game is disabled,No se ha podido conectar al juego porque está desactivado.,No se ha podido conectar al juego porque está desactivado.,Impossible de se connecter au jeu car il a été désactivé,Impossibile connettersi al gioco perché è disattivato,ゲームが無効なため接続できませんでした。,게임이 비활성화되어 접속할 수 없어요,Impossível conectar pois o jogo está desabilitado,Impossível conectar pois o jogo está desabilitado,"Не удалось подключиться к игре, так как она недоступна.",此游戏已停用,无法连接。,遊戲已停用,無法連線到遊戲,此游戏已停用,无法连接。 +,,,Could not connect to game because game is full,"Verbindung zum Spiel konnte nicht hergestellt werden, da das Spiel voll ist.",Could not connect to game because game is full,No se ha podido conectar al juego porque está lleno.,No se ha podido conectar al juego porque está lleno.,Impossible de se connecter au jeu car il est complet,Impossibile connettersi al gioco perché è al completo,ゲームが満員なため接続できませんでした。,게임이 가득 차서 접속할 수 없어요,Impossível conectar pois o jogo está cheio,Impossível conectar pois o jogo está cheio,"Не удалось подключиться к игре, так как она переполнена.",此游戏已满员,无法连接。,此遊戲已額滿,無法連線到遊戲,此游戏已满员,无法连接。 +,,,Could not connect to game because it is not available for your platform,"Verbindung zum Spiel konnte nicht hergestellt werden, da es für deine Plattform nicht verfügbar ist.",Could not connect to game because it is not available for your platform,No se ha podido conectar al juego porque no está disponible para tu plataforma.,No se ha podido conectar al juego porque no está disponible para tu plataforma.,Impossible de se connecter au jeu car il n'est pas disponible sur votre plateforme,Impossibile connettersi al gioco perché non è disponibile per la tua piattaforma,プラットフォームに対応していないためゲームに接続できませんでした。,플랫폼에서 이용할 수 없는 게임이므로 접속할 수 없어요,Impossível conectar pois o jogo não está disponível para a sua plataforma,Impossível conectar pois o jogo não está disponível para a sua plataforma,"Не удалось подключиться к игре, так как она недоступна на вашей платформе.",此游戏在你的平台上不可用,因此无法连接。,您的平台不支援此遊戲,無法連線到遊戲,此游戏在你的平台上不可用,因此无法连接。 +,,,Could not connect to game because user you were following has left the game,"Verbindung zum Spiel konnte nicht hergestellt werden, da der Benutzer, dem du gefolgt bist, das Spiel verlassen hat.",Could not connect to game because user you were following has left the game,No se ha podido conectar al juego porque el jugador al que seguías ha salido del mismo.,No se ha podido conectar al juego porque el jugador al que seguías ha salido del mismo.,Impossible de se connecter au jeu car l'utilisateur que vous suiviez l'a quitté,Impossibile connettersi al gioco perché l'utente che stavi seguendo è uscito,フォロー中のユーザーがゲームから退出したため接続できませんでした。,팔로우 중인 사용자가 게임에서 나가서 게임에 접속할 수 없어요,Impossível conectar pois o usuário que você estava seguindo saiu do jogo,Impossível conectar pois o usuário que você estava seguindo saiu do jogo,"Не удалось подключиться к игре, так как игрок, за которым вы последовали, ее покинул.",你关注的用户已离开游戏,因此无法连接。,您跟隨的使用者已離開遊戲,無法連線到遊戲,你关注的用户已离开游戏,因此无法连接。 +,,,Could not connect to game due to join script failure,"Verbindung zum Spiel konnte nicht hergestellt werden, da ein Problem mit dem Beitrittsskript aufgetreten ist.",Could not connect to game due to join script failure,No se ha podido conectar al juego a causa de un error en el script de unión.,No se ha podido conectar al juego a causa de un error en el script de unión.,Impossible de se connecter au jeu du fait d'un échec du script pour le rejoindre,Impossibile connettersi al gioco per un errore dello script di accesso,参加スクリプトの失敗により接続できませんでした。,참가 스크립트를 가져오지 못해 게임에 접속할 수 없어요,Impossível conectar devido a uma falha no script de entrada,Impossível conectar devido a uma falha no script de entrada,Не удалось подключиться к игре из-за ошибки в сценарии подключения.,由于加入脚本失败,无法连接至游戏,加入腳本發生錯誤,無法連線到遊戲,由于加入脚本失败,无法连接至游戏 +,,,"Could not connect to game, please try again later.",Verbindung zum Spiel konnte nicht hergestellt werden. Bitte versuche es später erneut.,"Could not connect to game, please try again later.",No se ha podido conectar al juego. Inténtalo de nuevo más tarde.,No se ha podido conectar al juego. Inténtalo de nuevo más tarde.,"Impossible de se connecter au jeu, veuillez réessayer plus tard.",Impossibile connettersi al gioco. Riprova più tardi.,ゲームに接続できませんでした。しばらくしてからやり直してください。,게임에 접속할 수 없어요. 나중에 다시 시도하세요.,Impossível conectar ao jogo. Tente de novo mais tarde.,Impossível conectar ao jogo. Tente de novo mais tarde.,Не удалось подключиться к игре. Повторите попытку позже.,无法连接至游戏,请稍候重试。,無法連線到遊戲,請稍後再試。,无法连接至游戏,请稍候重试。 +,,,Ctrl-Shift-C again to restore.,Zum Wiederherstellen erneut Strg-Umschalt-C drücken.,Ctrl-Shift-C again to restore.,Vuelve a pulsar Ctrl-Mayús-C para restablecerla.,Vuelve a pulsar Ctrl-Mayús-C para restablecerla.,Ctrl-Maj-C pour la restaurer.,Ancora CTRL-MAIUSC-C per ripristinare.,Ctrl-Shift-Cをもう一度押して復元。,복구하려면 Ctrl-Shift-C를 클릭하세요.,Ctrl-Shift-C de novo para restaurar.,Ctrl-Shift-C de novo para restaurar.,"Снова нажмите Ctrl-Shift-C, чтобы восстановить.",重按 Ctrl-Shift-C 以还原。,重按 Ctrl-Shift-C 復原。,重按 Ctrl-Shift-C 以还原。 +,,,Ctrl-Shift-G again to restore.,Zum Wiederherstellen erneut Strg-Umschalt-G drücken.,Ctrl-Shift-G again to restore.,Vuelve a pulsar Ctrl-Mayús-G para restablecerla.,Vuelve a pulsar Ctrl-Mayús-G para restablecerla.,Ctrl-Maj-G pour la restaurer.,Ancora CTRL-MAIUSC-G per ripristinare.,Ctrl-Shift-G をもう一度押して復元。,복원하려면 Ctrl-Shift-G를 클릭하세요.,Ctrl-Shift-G de novo para restaurar.,Ctrl-Shift-G de novo para restaurar.,"Снова нажмите Ctrl-Shift-G, чтобы восстановить.",重按 Ctrl-Shift-G 以还原。,重按 Ctrl-Shift-G 復原。,重按 Ctrl-Shift-G 以还原。 +,,,D/Right Arrow,D/Rechtspfeil,D/Right Arrow,D/Cursor derecho,D/Cursor derecho,D/Flèche droite,D/Freccia DESTRA,D/右カーソル,D/오른쪽 화살표,D/seta direita,D/seta direita,D/стрелка вправо,D/右箭头,D、→,D/右箭头 +,,,DPad,Steuerkreuz,DPad,Cruceta,Cruceta,Croix directionnelle,Croce direzionale,DPad,DPad,Direcional,Direcional,Крестовина,十字键,十字鍵,十字键 +,,,Dating,Dating,Dating,Solicitud de cita,Solicitud de cita,Drague,Appuntamento,デートの誘い,이성 교제,Namoro,Namoro,Флирт,约会,約會,约会 +,,,Decal,Decal,Decal,Adhesivo,Adhesivo,Décalcomanie,Decalcomania,デカール,데칼,Decalque,Decalque,Наклейка,贴纸,貼花,贴纸 +,,,Decline,Ablehnen,Decline,Rechazar,Rechazar,Décliner,Declina,拒否する,거절,Recusar,Recusar,Отклонить,拒绝,拒絕,拒绝 +,,,Decline Friend Request,Freundschaftsanfrage ablehnen,Decline Friend Request,Rechazar solicitud de amistad,Rechazar solicitud de amistad,Refuser la demande d'ami,Declinare la richiesta di un amico,友達リクエストを拒否する,친구 요청 거절,Rejeitar pedido de amizade,Rejeitar pedido de amizade,Отклонить запрос друга,拒绝好友邀请,拒絕好友邀請,拒绝好友邀请 +,,,Default (Classic),Standard (Klassisch),Default (Classic),Predeterminado (clásico),Predeterminado (clásico),Par défaut (Classique),Predefinita (Classica),デフォルト (クラシック),기본값 (Classic),Padrão (clássico),Padrão (clássico),По умолчанию (классика),默认(经典),預設(經典),默认(经典) +,,,Default (Follow),Standard (Folgen),Default (Follow),Predeterminado (seguir),Predeterminado (seguir),Par défaut (Suivre),Predefinita (Segui),デフォルト (フォロー),기본값 (팔로우),Padrão (seguir),Padrão (seguir),По умолчанию (следование),默认(追随),預設(追蹤),默认(追随) +,,,Default (Keyboard),Standard (Tastatur),Default (Keyboard),Predeterminado (teclado),Predeterminado (teclado),Par défaut (Clavier),Predefinita (Tastiera),デフォルト (キーボード),기본값 (키보드),Padrão (teclado),Padrão (teclado),По умолчанию (клавиатура),默认(键盘),預設(鍵盤),默认(键盘) +,,,Default (Thumbstick),Standard (Thumbstick),Default (Thumbstick),Predeterminado (stick),Predeterminado (stick),Par défaut (Joystick),Predefinita (Levetta),デフォルト (サムスティック),기본값 (엄지스틱),Padrão (direcional analógico),Padrão (direcional analógico),По умолчанию (аналоговый стик),默认(摇杆),預設(類比搖桿),默认(摇杆) +,,,Dev Console,Entwicklerkonsole,Dev Console,Consola de desarrollo,Consola de desarrollo,Console de dév.,Console svilup.,Dev コンソール,개발자 콘솔,Console de dev.,Console de dev.,Консоль разработчика,开发控制台,開發人員控制台,开发控制台 +,,,Developer Console,Entwicklerkonsole,Developer Console,Consola de desarrollo,Consola de desarrollo,Console de développement,Console sviluppatore,デベロッパーコンソール,개발자 콘솔,Console de desenvolvimento,Console de desenvolvimento,Консоль разработчика,开发人员控制台,開發人員控制台,开发人员控制台 +,,,"Developer has shut down all game servers or game server has shut down for other reasons, please reconnect",Der Entwickler hat alle Spielserver abgeschaltet oder die Verbindung zum Spielserver wurde aus anderen Gründen unterbrochen. Bitte erneut verbinden.,"Developer has shut down all game servers or game server has shut down for other reasons, please reconnect",El desarrollador ha cerrado todos los servidores del juego o el servidor del juego se ha cerrado por otras razones. Conéctate de nuevo.,El desarrollador ha cerrado todos los servidores del juego o el servidor del juego se ha cerrado por otras razones. Conéctate de nuevo.,"Le développeur a arrêté tous les serveurs du jeu, ou le serveur s'est arrêté pour d'autres raisons. Veuillez vous reconnecter.",Gli sviluppatori hanno chiuso tutti i server di gioco o il server si è chiuso per altri motivi. È necessario riconnettersi,開発者がすべてのゲームサーバーをシャットダウンしたか、他の理由でをシャットダウンしたようです。再接続してください,개발자가 모든 게임 서버를 종료했거나 혹은 기타 이유로 인해 게임 서버가 종료되었습니다. 다시 연결하세요.,O desenvolvedor encerrou todos os servidores do jogo ou o servidor do jogo foi encerrado por outros motivos. Conecte-se novamente.,O desenvolvedor encerrou todos os servidores do jogo ou o servidor do jogo foi encerrado por outros motivos. Conecte-se novamente.,"Разработчик отключил все сервера игры или они не работают по какой-либо другой причине. Пожалуйста, установите соединение повторно.",开发人员已经关闭所有游戏服务器,或由于其他原因服务器已关闭,请重新连接,開發人員控制台已關閉所有伺服器,或伺服器因其它原因關閉。請重新連線。,开发人员已经关闭所有游戏服务器,或由于其他原因服务器已关闭,请重新连接 +,,,Developer has shut down this game server for maintenance,Der Entwickler hat diesen Spielserver aufgrund von Wartungsarbeiten abgeschaltet,Developer has shut down this game server for maintenance,El desarrollador ha cerrado el servidor de este juego por mantenimiento,El desarrollador ha cerrado el servidor de este juego por mantenimiento,Le développeur a arrêté ce serveur de jeu pour effectuer la maintenance.,Lo sviluppatore ha chiuso questo server di gioco per manutenzione,メンテナンスのため、ディベロッパーによってゲームサーバーがシャットダウンされています。,유지보수를 위해 개발자가 본 게임 서버를 종료했습니다,O servidor deste jogo foi encerrado pelo desenvolvedor para manutenção,O servidor deste jogo foi encerrado pelo desenvolvedor para manutenção,Разработчик отключил игровой сервер для техобслуживания.,维护期间,开发人员已关闭此游戏服务器,開發人員已關閉此遊戲伺服器,维护期间,开发人员已关闭此游戏服务器 +,,,Disconnected due to Security Key Mismatch,Verbindung aufgrund einer Sicherheitsschlüssel-Diskrepanz unterbrochen,Disconnected due to Security Key Mismatch,Desconexión por incompatibilidad de la clave de seguridad ,Desconexión por incompatibilidad de la clave de seguridad ,"Vous avez été déconnecté(e), car la clé de sécurité ne correspond pas.",Disconnessione a causa di una mancata corrispondenza della chiave di sicurezza,セキュリティキーの不一致により接続が切断されました,보안키가 일치하지 않아 연결이 끊어졌습니다,Desconexão devido à incompatibilidade da Chave de Segurança,Desconexão devido à incompatibilidade da Chave de Segurança,Соединение прервано из-за несовпадения ключей безопасности.,由于安全密钥不匹配,连接中断,安全金鑰不符,連線中斷,由于安全密钥不匹配,连接中断 +,,,Disconnected due to a bad hash,Verbindung aufgrund eines fehlerhaften Hashs unterbrochen,Disconnected due to a bad hash,Desconexión por un error de hash,Desconexión por un error de hash,Vous avez été déconnecté(e) en raison d'un mauvais hachage.,Disconnessione a causa di un cattivo funzionamento dell'hash,ハッシュが正しくないため接続が切断されました,잘못된 해시로 인해 연결이 끊어졌습니다,Desconexão devido a um erro de hash,Desconexão devido a um erro de hash,Соединение прервано из-за ошибок хеширования.,由于错误的哈希码,连接中断,特徵碼錯誤,連線中斷,由于错误的哈希码,连接中断 +,,,"Disconnected due to timeout, please reconnect",Aufgrund einer Zeitüberschreitung unterbrochen. Bitte erneut verbinden.,"Disconnected due to timeout, please reconnect",Desconexión por agotamiento del tiempo de espera. Conéctate de nuevo.,Desconexión por agotamiento del tiempo de espera. Conéctate de nuevo.,"Vous avez été déconnecté(e), car votre session a expiré. Veuillez vous reconnecter.",Disconnessione a causa del time-out; è necessario riconnettersi,タイムアウトにより接続が失われました。再接続してください,시간 초과로 인해 연결이 끊어졌습니다. 다시 연결하세요.,Desconexão devido ao limite de tempo de espera ter sido atingido. Conecte-se novamente.,Desconexão devido ao limite de tempo de espera ter sido atingido. Conecte-se novamente.,"Разрыв соединения из-за превышения времени ожидания, пожалуйста, установите соединение повторно.",由于超时,连接已断开。请重新连接,連線逾時,請重新連線,由于超时,连接已断开。请重新连接 +,,,"Disconnected from game, please reconnect",Verbindung zum Spiel unterbrochen. Bitte erneut verbinden.,"Disconnected from game, please reconnect",Desconectado del juego. Conéctate de nuevo.,Desconectado del juego. Conéctate de nuevo.,Vous avez été déconnecté(e) du jeu. Veuillez vous reconnecter.,Disconnesso dal gioco; è necessario riconnettersi,ゲームへの接続が切断されました。再接続してください,게임 연결이 끊어졌습니다. 다시 연결하세요.,Você foi desconectado do jogo. Conecte-se novamente.,Você foi desconectado do jogo. Conecte-se novamente.,"Соединение и игрой прервано, пожалуйста, установите соединение повторно.",与游戏连接已断开,请重新连接,連線中斷,請重新連線,与游戏连接已断开,请重新连接 +,,,"Disconnected from game, please reconnect. {RET:translate}...({COUNT:int})",Verbindung zum Spiel unterbrochen. Bitte erneut verbinden.. {RET:translate}...({COUNT:int}),"Disconnected from game, please reconnect. {RET:translate}...({COUNT:int})",Desconectado del juego. Conéctate de nuevo.. {RET:translate}...({COUNT:int}),Desconectado del juego. Conéctate de nuevo.. {RET:translate}...({COUNT:int}),Vous avez été déconnecté(e) du jeu. Veuillez vous reconnecter.. {RET:translate}...({COUNT:int}),Disconnesso dal gioco; è necessario riconnettersi. {RET:translate}...({COUNT:int}),ゲームへの接続が切断されました。再接続してください. {RET:translate}...({COUNT:int}),게임 연결이 끊어졌습니다. 다시 연결하세요.. {RET:translate}...({COUNT:int}),Você foi desconectado do jogo. Conecte-se novamente.. {RET:translate}...({COUNT:int}),Você foi desconectado do jogo. Conecte-se novamente.. {RET:translate}...({COUNT:int}),"Соединение и игрой прервано, пожалуйста, установите соединение повторно.. {RET:translate}...({COUNT:int})",与游戏连接已断开,请重新连接. {RET:translate}...({COUNT:int}),連線中斷,請重新連線。{RET:translate}…({COUNT:int}),与游戏连接已断开,请重新连接. {RET:translate}...({COUNT:int}) +,,,"Disconnected from game, possibly due to game joined from another device",Verbindung zum Spiel wurde unterbochen. Möglicher Grund: Spielbeitritt von einem anderen Gerät.,"Disconnected from game, possibly due to game joined from another device","Desconectado del juego, posiblemente por conectarse al juego desde otro dispositivo.","Desconectado del juego, posiblemente por conectarse al juego desde otro dispositivo.",Vous avez été déconnecté(e) du jeu ; il se peut que vous ayez rejoint le jeu depuis un autre appareil.,Sei stato disconnesso dal gioco forse perché hai giocato una partita con un altro dispositivo,他のデバイスから参加したゲームのために、ゲームへの接続から切断された可能性があります,게임 연결이 끊어졌습니다. 다른 장치에서 게임에 참여한 것 같아요.,"Você foi desconectado do jogo, provavelmente porque entrou em um jogo através de outro dispositivo","Você foi desconectado do jogo, provavelmente porque entrou em um jogo através de outro dispositivo","Соединение с игрой прервано, возможно, из-за подключения к игре с другого устройства.",已断开与游戏的连接,可能是由于该游戏同时从另一个设备加入,連線中斷,您可能從其它裝置加入遊戲,已断开与游戏的连接,可能是由于该游戏同时从另一个设备加入 +,,,Dismiss,Verwerfen,Dismiss,Descartar,Descartar,Rejeter,Ignora,却下,취소,Dispensar,Dispensar,Отклонить,关闭,關閉,关闭 +,,,Do you allow game to create new place in your inventory?,"Erlaubst du dem Spiel, mehr Platz in deinem Inventar zu schaffen?",Do you allow game to create new place in your inventory?,¿Quieres permitir que el juego cree un espacio nuevo en tu inventario?,¿Quieres permitir que el juego cree un espacio nuevo en tu inventario?,Autorisez-vous le jeu à créer un nouvel emplacement dans votre inventaire ?,Vuoi permettere al gioco di creare nuovo spazio nel tuo inventario?,あなたのインベントリに新しい場所を作成しても良いですか?,게임 인벤토리에 새로운 장소를 만들까요?,Você permite que o jogo crie um novo espaço em seu inventário?,Você permite que o jogo crie um novo espaço em seu inventário?,Разрешить игре создать новое место в вашем инвентаре?,你是否允许游戏在你的道具中创建新的地点?,您要允許遊戲在您的道具欄創作新空間嗎?,你是否允许游戏在你的道具中创建新的地点? +,,,Don't Reset,Nicht zurücksetzen,Don't Reset,No reiniciar,No reiniciar,Ne pas réinitialiser,Non azzerare,リセットしない,재설정 취소,Não reiniciar,Não reiniciar,Не сбрасывать,不要重置,不要重置,不要重置 +,,,Drop Tool,Werkzeug ablegen,Drop Tool,Soltar herramienta,Soltar herramienta,Lâcher outil,Lascia strumento,ツールを手放す,도구 드롭,Largar,Largar,Выбросить,丢弃工具,捨棄工具,丢弃工具 +,,,ESC,ESC,ESC,ESC,ESC,ÉCHAP,ESC,ESC,ESC,ESC,ESC,ESC,ESC,ESC,ESC +,,,English text,English text,English text,English text,English text,Texte en français,Italian text,英文テキスト,한국어 텍스트,Portuguese,Portuguese,Текст на русском,English text,English text,English text +,,,Equip Tools,Werkzeuge ausrüsten,Equip Tools,Equipar herramientas,Equipar herramientas,Prendre outils,Equipaggia strum.,ツールをつける,도구 장착,Equipar,Equipar,Назначить,配备工具,裝上工具,配备工具 +,,,Equip/Unequip Tools,Werkzeug-Ausrüstung hinzufügen/abnehmen,Equip/Unequip Tools,Equipar/desequipar herramientas,Equipar/desequipar herramientas,Prendre/Lâcher outils,Equipaggia/Rimuovi,ツールをつける/外す,도구 장착/해제,Trocar ferramentas,Trocar ferramentas,Назначить/убрать предметы,配备/取消配备工具,裝上 / 卸下工具,配备/取消配备工具 +,,,Error Blocking Player,Fehler beim Sperren des Spielers,Error Blocking Player,Error al bloquear al jugador,Error al bloquear al jugador,Erreur en bloquant le joueur,Errore di blocco giocatore,プレイヤーのブロックエラー,플레이어 차단 오류,Erro ao bloquear jogador,Erro ao bloquear jogador,Ошибка блокировки игрока,屏蔽玩家时出错,封鎖玩家時發生錯誤,屏蔽玩家时出错 +,,,Error Unblocking Player,Fehler beim Aufheben der Sperre des Spielers,Error Unblocking Player,Error al desbloquear al jugador,Error al desbloquear al jugador,Erreur en levant le blocage du joueur,Errore di sblocco giocatore,プレーヤーブロック解除エラー,플레이어 차단 해제 오류,Erro ao desbloquear jogador,Erro ao desbloquear jogador,Ошибка разблокировки игрока,取消屏蔽玩家时出错,解除封鎖玩家時發生錯誤,取消屏蔽玩家时出错 +,,,"Error processing ticket, please reconnect",Fehler bei der Ticketbearbeitung. Bitte erneut verbinden.,"Error processing ticket, please reconnect",Error al procesar un tique. Conéctate de nuevo.,Error al procesar un tique. Conéctate de nuevo.,"Erreur lors du traitement du ticket, veuillez vous reconnecter.",Errore di elaborazione della richiesta; è necessario riconnettersi,チケットの処理中にエラーが発生しました。再接続してください,티켓 처리 중 오류가 발생했어요. 다시 연결하세요.,Erro ao processar o ticket. Conecte-se novamente,Erro ao processar o ticket. Conecte-se novamente,"Сообщение об ошибке процесса, пожалуйста, установите соединение повторно.",处理票单时出错,请重新连接,處理票券時發生錯誤,請重新連線。,处理票单时出错,请重新连接 +,,,"Error while receiving data, please reconnect",Fehler beim Datenempfang. Bitte erneut verbinden,"Error while receiving data, please reconnect",Error al recibir los datos. Conéctate de nuevo.,Error al recibir los datos. Conéctate de nuevo.,"Erreur lors de la réception des données, veuillez vous reconnecter.",Errore di ricezione dei dati; è necessario riconnettersi,データを受信中にエラーが発生しました。再接続してください,데이터 수신 중 오류가 발생했어요. 다시 연결하세요.,Erro ao receber os dados. Conecte-se novamente.,Erro ao receber os dados. Conecte-se novamente.,"Ошибка при получении данных, пожалуйста, установите соединение повторно.",接收数据时出错,请重新连接,接收資料時發生錯誤,請重新連線。,接收数据时出错,请重新连接 +,,,"Error while sending data, please reconnect",Fehler beim Senden der Daten. Bitte erneut verbinden.,"Error while sending data, please reconnect",Error al enviar los datos. Conéctate de nuevo.,Error al enviar los datos. Conéctate de nuevo.,"Erreur lors de l'envoi des données, veuillez vous reconnecter.",Errore durante l'invio dei dati; è necessario riconnettersi,データを送信中にエラーが発生しました。再接続してください,데이터 전송 중 오류가 발생했어요. 다시 연결하세요.,Erro ao enviar os dados. Conecte-se novamente.,Erro ao enviar os dados. Conecte-se novamente.,"Ошибка при отправке данных, пожалуйста, установите соединение повторно.",发送数据时出错,请重新连接,傳送資料時發生錯誤,請重新連線。,发送数据时出错,请重新连接 +,,,"Error while streaming data, please reconnect",Fehler bei der Datenübertragung. Bitte erneut verbinden.,"Error while streaming data, please reconnect",Error al transmitir los datos. Conéctate de nuevo.,Error al transmitir los datos. Conéctate de nuevo.,"Erreur lors de la diffusion des données, veuillez vous reconnecter.",Errore durante lo streaming dei dati; è necessario riconnettersi,データをストリーミング中にエラーが発生しました。再接続してください,데이터 스트리밍 중 오류가 발생했어요. 다시 연결하세요.,Erro ao transmitir os dados. Conecte-se novamente.,Erro ao transmitir os dados. Conecte-se novamente.,"Ошибка при передаче данных, пожалуйста, установите соединение повторно.",流式处理数据时出错,请重新连接,資料串流發生錯誤,請重新連線。,流式处理数据时出错,请重新连接 +,,,Face,Gesicht,Face,Cara,Cara,Visage,Faccia,顔,얼굴,Rosto,Rosto,Лицо,脸部,臉部,脸部 +,,,Face Accessory,Gesichts-Accessoire,Face Accessory,Accesorio para la cara,Accesorio para la cara,Accessoire de visage,Accessorio faccia,顔アクセサリ,얼굴 장신구,Acessório de rosto,Acessório de rosto,Аксессуар для лица,脸部配饰,臉部飾品,脸部配饰 +,,,Failed to connect to the Game. (ID = {RBX_NUMBER}: {RBX_NAME}),Verbindung zum Spiel fehlgeschlagen. (ID = {RBX_NUMBER}: {RBX_NAME}),Failed to connect to the Game. (ID = {RBX_NUMBER}: {RBX_NAME}),No se ha podido conectar con el juego. (ID = {RBX_NUMBER}: {RBX_NAME}),No se ha podido conectar con el juego. (ID = {RBX_NUMBER}: {RBX_NAME}),Échec de la connexion au jeu. (ID = {RBX_NUMBER} : {RBX_NAME}),Connessione al gioco non riuscita. (ID = {RBX_NUMBER}: {RBX_NAME}),ゲームに接続できませんでした。(ID = {RBX_NUMBER}: {RBX_NAME}),게임 연결에 실패했어요. (ID = {RBX_NUMBER}: {RBX_NAME}),Falha ao conectar no jogo. (ID = {RBX_NUMBER}: {RBX_NAME}),Falha ao conectar no jogo. (ID = {RBX_NUMBER}: {RBX_NAME}),Не удалось подключиться к игре. (ID = {RBX_NUMBER}: {RBX_NAME}),无法连接至游戏。(ID = {RBX_NUMBER}: {RBX_NAME}),無法連線到遊戲。(ID = {RBX_NUMBER}: {RBX_NAME}),无法连接至游戏。(ID = {RBX_NUMBER}: {RBX_NAME}) +,,,Fall Animation,Fallanimation,Fall Animation,Animación de caída,Animación de caída,Animation de chute,Animazione caduta,落下アニメーション,낙하 애니메이션,Animação de queda,Animação de queda,Анимация падения,下降动画,跌落動畫,下降动画 +,,,Follow,Folgen,Follow,Seguir,Seguir,Suivre,Segui,フォロー,팔로우,Seguir,Seguir,Слежение,跟随,追蹤,跟随 +,,,Follow Player,Spieler folgen,Follow Player,Seguir a jugador,Seguir a jugador,Suivre joueur,Segui giocatore,プレイヤーをフォロー,플레이어 팔로우,Seguir jogador,Seguir jogador,Подписаться на игрока,关注玩家,追蹤玩家,关注玩家 +,,,Followed user has left the game,"Der Benutzer, dem du folgst, hat das Spiel verlassen",Followed user has left the game,El usuario al que sigues ha abandonado el juego,El usuario al que sigues ha abandonado el juego,L'utilisateur que vous suiviez a quitté la partie.,L'utente seguito ha abbandonato il gioco,フォローしたユーザーがゲームを終了しました,팔로우 중인 사용자가 게임에서 나갔어요.,O usuário que você segue saiu do jogo,O usuário que você segue saiu do jogo,Отслеживаемый игрок вышел из игры,关注用户已离开游戏,您追蹤的使用者已離開遊戲,关注用户已离开游戏 +,,,Followed user has left the game. ({COUNT:int}),"Der Benutzer, dem du folgst, hat das Spiel verlassen. ({COUNT:int})",Followed user has left the game. ({COUNT:int}),El usuario al que sigues ha abandonado el juego. ({COUNT:int}),El usuario al que sigues ha abandonado el juego. ({COUNT:int}),L'utilisateur que vous suiviez a quitté la partie.. ({COUNT:int}),L'utente seguito ha abbandonato il gioco. ({COUNT:int}),フォローしたユーザーがゲームを終了しました。 ({COUNT:int}),팔로우 중인 사용자가 게임에서 나갔어요. ({COUNT:int}),O usuário que você segue saiu do jogo ({COUNT:int}),O usuário que você segue saiu do jogo ({COUNT:int}),Отслеживаемый игрок вышел из игры ({COUNT:int}),关注用户已离开游戏 ({COUNT:int}),您追蹤的使用者已離開遊戲。({COUNT:int}),关注用户已离开游戏 ({COUNT:int}) +,,,Followed user has left the game. {RET:translate}...({COUNT:int}),"Der Benutzer, dem du folgst, hat das Spiel verlassen. {RET:translate}...({COUNT:int})",Followed user has left the game. {RET:translate}...({COUNT:int}),El usuario al que sigues ha abandonado el juego. {RET:translate}...({COUNT:int}),El usuario al que sigues ha abandonado el juego. {RET:translate}...({COUNT:int}),L'utilisateur que vous suiviez a quitté la partie.. {RET:translate}...({COUNT:int}),L'utente seguito ha abbandonato il gioco. {RET:translate}...({COUNT:int}),フォローしたユーザーがゲームを終了しました。 {RET:translate}...({COUNT:int}),팔로우 중인 사용자가 게임에서 나갔어요.. {RET:translate}...({COUNT:int}),O usuário que você segue saiu do jogo. {RET:translate}...({COUNT:int}),O usuário que você segue saiu do jogo. {RET:translate}...({COUNT:int}),Отслеживаемый игрок вышел из игры. {RET:translate}...({COUNT:int}),关注用户已离开游戏. {RET:translate}...({COUNT:int}),您追蹤的使用者已離開遊戲。{RET:translate}…({COUNT:int}),关注用户已离开游戏. {RET:translate}...({COUNT:int}) +,,,Friend,Freund,Friend,Amigo,Amigo,Ami,Amico,友達,친구,Amigo,Amigo,Друг,好友,好友,好友 +,,,Friend Limit Reached,Max. Anzahl an Freunden erreicht,Friend Limit Reached,Has alcanzado el límite de amigos,Has alcanzado el límite de amigos,Limite d'amis atteinte,Limiti di amici raggiunto,友達の数が上限に達しました,최대 친구 수 도달,Limite de amigos alcançado,Limite de amigos alcançado,Достигнуто максимальное количество друзей,已达好友数量上限,已達好友上限,已达好友数量上限 +,,,From {RBX_NAME},Von {RBX_NAME},From {RBX_NAME},De {RBX_NAME},De {RBX_NAME},De {RBX_NAME},Da {RBX_NAME},送信者: {RBX_NAME},발신: {RBX_NAME},De {RBX_NAME},De {RBX_NAME},Отправитель: {RBX_NAME},自“{RBX_NAME}”,自 {RBX_NAME},自“{RBX_NAME}” +,,,Front Accessory,Vorderseiten-Accessoire,Front Accessory,Accesorio frontal,Accesorio frontal,Accessoire avant,Accessorio anteriore,前面アクセサリ,가슴 장신구,Acessório da frente,Acessório da frente,Передний аксессуар,正面配饰,正面飾品,正面配饰 +,,,Fullscreen,Vollbild,Fullscreen,Pantalla completa,Pantalla completa,Plein écran,Schermo intero,フルスクリーン,전체 화면,Tela cheia,Tela cheia,Полный экран,全屏,全螢幕,全屏 +,,,GPU,GPU,GPU,GPU,GPU,Processeur Graphique,GPU,GPU,GPU,GPU,GPU,Графический процессор,GPU,圖形處理器,GPU +,,,Game,Spiel,Game,Juego,Juego,Jeu,Gioco,ゲーム,게임,Jogo,Jogo,Игра,游戏,遊戲,游戏 +,,,Game Menu Toggle,Spielmenü umschalten,Game Menu Toggle,Alternar menú del juego,Alternar menú del juego,Menu du jeu (act./désact.),Attiva/Disattiva menu di gioco,ゲームメニュー切り換え,게임 메뉴 전환,Ativar menu do jogo,Ativar menu do jogo,Вызов игрового меню,游戏菜单切换,遊戲選單切換,游戏菜单切换 +,,,Game Pass,Spielpass,Game Pass,Pase de juego,Pase de juego,Passe de jeu,Pass di gioco,ゲームパス,게임패스,Passe de jogo,Passe de jogo,Игровой пропуск,游戏通行证,遊戲證,游戏通行证 +,,,"Game join request expired or invalid, please try again.",Anfrage zum Spielbeitritt abgelaufen oder ungültig. Bitte versuche es erneut.,"Game join request expired or invalid, please try again.",La solicitud para unirse al juego ha caducado o no es válida. Inténtalo de nuevo.,La solicitud para unirse al juego ha caducado o no es válida. Inténtalo de nuevo.,"La demande pour rejoindre le jeu a expirée ou est invalide, veuillez réessayer.",Richiesta di accesso al gioco scaduta o non valida. Riprova.,ゲーム参加リクエストが期限切れか無効です。やり直してください。,게임 가입 요청이 만료되었거나 유효하지 않습니다. 다시 시도하세요.,Pedido de entrada no jogo expirado ou inválido. Tente novamente.,Pedido de entrada no jogo expirado ou inválido. Tente novamente.,"Срок приглашения в игру истек, или оно является недействительным. Повторите попытку.",加入游戏的请求已过期或无效,请重试。,遊戲加入請求過期或無效,請重新嘗試。,加入游戏的请求已过期或无效,请重试。 +,,,Game or Player?,Spiel oder Spieler?,Game or Player?,¿Juego o jugador?,¿Juego o jugador?,Jeu ou joueur ?,Gioco o giocatore?,ゲームですか、プレイヤーですか?,게임 혹은 플레이어?,Jogo ou jogador?,Jogo ou jogador?,Игра или игрок?,游戏或玩家?,遊戲或玩家?,游戏或玩家? +,,,Gear,Ausrüstung,Gear,Equipamiento,Equipamiento,Équipement,Attrezzatura,ギア,기어,Equipamento,Equipamento,Снаряжение,装备,裝備,装备 +,,,Goodbye!,Tschüss!,Goodbye!,¡Hasta luego!,¡Hasta luego!,Au revoir !,Addio!,さようなら!,안녕히 가세요!,Tchau!,Tchau!,До свидания!,再见!,再見!,再见! +,,,Graphics Level,Grafikstufe,Graphics Level,Nivel de gráficos,Nivel de gráficos,Niveau graphismes,Livello grafica,グラフィックレベル,그래픽 수준,Nível dos gráficos,Nível dos gráficos,Уровень графики,图形级别,圖形層級,图形级别 +,,,Graphics Mode,Grafikmodus,Graphics Mode,Modo de gráficos,Modo de gráficos,Mode graphismes,Modalità grafica,グラフィックモード,그래픽 모드,Modo de gráficos,Modo de gráficos,Режим графики,图形模式,圖形模式,图形模式 +,,,Graphics Quality,Grafikqualität,Graphics Quality,Calidad gráfica,Calidad gráfica,Qualité graphismes,Qualità grafica,グラフィック品質,그래픽 품질,Qualidade dos gráficos,Qualidade dos gráficos,Качество графики,图形画质,圖形畫質,图形画质 +,,,Group Emblem,Gruppenemblem,Group Emblem,Emblema de grupo,Emblema de grupo,Emblème de groupe,Emblema gruppo,グループエンブレム,그룹 엠블렘,Emblema de grupo,Emblema de grupo,Эмблема группы,群组徽章,群組圖像,群组徽章 +,,,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML,HTML +,,,Hair Accessory,Haar-Accessoire,Hair Accessory,Accesorio para el pelo,Accesorio para el pelo,Accessoire de cheveux,Accessorio capelli,ヘアアクセサリ,헤어 장신구,Acessório de cabelo,Acessório de cabelo,Аксессуар для волос,头发配饰,髮型飾品,头发配饰 +,,,Hash Exception,Hash-Ausnahme,Hash Exception,Excepción del hash,Excepción del hash,Exception de hachage.,Eccezione hash,ハッシュが例外です,해시 예외,Exceção de hash,Exceção de hash,Исключение хеширования,哈希码例外,特徵碼例外,哈希码例外 +,,,Hash Exception. ({COUNT:int}),Hash-Ausnahme. ({COUNT:int}),Hash Exception. ({COUNT:int}),Excepción del hash. ({COUNT:int}),Excepción del hash. ({COUNT:int}),Exception de hachage.. ({COUNT:int}),Eccezione hash. ({COUNT:int}),ハッシュが例外です。 ({COUNT:int}),해시 예외 ({COUNT:int}),Exceção de hash ({COUNT:int}),Exceção de hash ({COUNT:int}),Исключение хеширования ({COUNT:int}),哈希码例外 ({COUNT:int}),特徵碼例外。({COUNT:int}),哈希码例外 ({COUNT:int}) +,,,Hash Expired,Hash abgelaufen,Hash Expired,El hash ha caducado,El hash ha caducado,Expiration du hachage.,Hash scaduto,ハッシュが期限切れです,해시 만료됨,Hash expirado,Hash expirado,Хеширование истекло,哈希码失效,特徵碼過期。,哈希码失效 +,,,Hash Expired. ({COUNT:int}),Hash abgelaufen. ({COUNT:int}),Hash Expired. ({COUNT:int}),El hash ha caducado. ({COUNT:int}),El hash ha caducado. ({COUNT:int}),Expiration du hachage.. ({COUNT:int}),Hash scaduto. ({COUNT:int}),ハッシュが期限切れです。 ({COUNT:int}),해시 만료됨 ({COUNT:int}),Hash expirado ({COUNT:int}),Hash expirado ({COUNT:int}),Хеширование истекло ({COUNT:int}),哈希码失效 ({COUNT:int}),特徵碼過期。({COUNT:int}),哈希码失效 ({COUNT:int}) +,,,Hash Expired. {RET:translate}...({COUNT:int}),Hash abgelaufen. {RET:translate}...({COUNT:int}),Hash Expired. {RET:translate}...({COUNT:int}),El hash ha caducado. {RET:translate}...({COUNT:int}),El hash ha caducado. {RET:translate}...({COUNT:int}),Expiration du hachage.. {RET:translate}...({COUNT:int}),Hash scaduto. {RET:translate}...({COUNT:int}),ハッシュが期限切れです。 {RET:translate}...({COUNT:int}),해시 만료됨. {RET:translate}...({COUNT:int}),Hash expirado. {RET:translate}...({COUNT:int}),Hash expirado. {RET:translate}...({COUNT:int}),Хеширование истекло. {RET:translate}...({COUNT:int}),哈希码失效. {RET:translate}...({COUNT:int}),特徵碼過期。{RET:translate}…({COUNT:int}),哈希码失效. {RET:translate}...({COUNT:int}) +,,,Hat,Hut,Hat,Sombrero,Sombrero,Chapeau,Cappello,帽子,모자,Chapéu,Chapéu,Головной убор,帽子,帽子,帽子 +,,,Head,Kopf,Head,Cabeza,Cabeza,Tête,Testa,頭,머리,Cabeça,Cabeça,Голова,头部,頭部,头部 +,,,Help,Hilfe,Help,Ayuda,Ayuda,Aide,Guida,ヘルプ,도움말,Ajuda,Ajuda,Справка,帮助,協助,帮助 +,,,Hiding Core GUI,Grafische Standard-Benutzeroberfläche verbergen,Hiding Core GUI,Ocultando la interfaz básica,Ocultando la interfaz básica,Cacher l'IGU principale,Nascondere interfaccia base,コア GUI を隠す。,코어 GUI 숨기기,Escondendo interface básica,Escondendo interface básica,Скрытие элементов основного интерфейса,隐藏核心 GUI,隱藏標準介面,隐藏核心 GUI +,,,Hiding Custom GUI,Individuelle grafische Benutzeroberfläche verbergen,Hiding Custom GUI,Ocultando la interfaz básica personalizada,Ocultando la interfaz básica personalizada,Cacher l'IGU personnalisée,Nascondere interfaccia personalizzata,カスタムGUIを隠す。,사용자 정의 GUI 숨기기,Escondendo interface personalizada,Escondendo interface personalizada,Скрытие элементов пользовательского интерфейса,隐藏自定义 GUI,隱藏自訂介面,隐藏自定义 GUI +,,,Idle Animation,Untätigkeitsanimation,Idle Animation,Animación de inactividad,Animación de inactividad,Animation oisif,Animazione inattività,待機アニメーション,대기 애니메이션,Animação de inatividade,Animação de inatividade,Анимация ожидания,闲置动画,閒置動畫,闲置动画 +,,,Image,Bild,Image,Imagen,Imagen,Image,Immagine,イメージ,이미지,Imagem,Imagem,Изображение,图像,圖像,图像 +,,,Inappropriate Content,Unangemessener Inhalt,Inappropriate Content,Contenido inadecuado,Contenido inadecuado,Contenu inapproprié,Contenuto non appropriato,不適切なコンテンツ,부적절한 콘텐츠,Conteúdo inapropriado,Conteúdo inapropriado,Неприличное содержимое,内容不当,內容不當,内容不当 +,,,Inappropriate Username,Unangemessener Benutzername,Inappropriate Username,Nombre de usuario inadecuado,Nombre de usuario inadecuado,Nom d'utilisateur inapproprié,Nome utente non appropriato,不適切なユーザーネーム,부적절한 사용자 이름,Nome de usuário inapropriado,Nome de usuário inapropriado,Неприличное имя пользователя,用户名不当,使用者名稱不當,用户名不当 +,,,Invalid JSON response received,Ungültige JSON-Antwort erhalten,Invalid JSON response received,Se ha recibido una respuesta JSON no válida,Se ha recibido una respuesta JSON no válida,Réponse JSON invalide reçue.,Ricevuta risposta JSON non valida,無効なJSON応答を受信しました,유효하지 않은 JSON 응답 수신됨,Resposta do JSON inválida,Resposta do JSON inválida,Получен некорректный ответ JSON,接收到无效的 JSON ,接收到無效的 JSON 回應,接收到无效的 JSON +,,,Invalid teleport destination,Ungültiger Teleport-Bestimmungsort,Invalid teleport destination,Destino del teleport no válido,Destino del teleport no válido,Destination de téléportation non valide.,Destinazione del teletrasporto non valida,テレポートの移動先が無効です,유효하지 않은 텔레포트 목적지,Destino de teleport inválido,Destino de teleport inválido,Некорректное назначение телепортирования.,传送目的地无效,傳送目的地無效,传送目的地无效 +,,,Joining server. ({COUNT:int}),Verbindung zum Server wird hergestellt. ({COUNT:int}),Joining server. ({COUNT:int}),Uniéndose a un servidor. ({COUNT:int}),Uniéndose a un servidor. ({COUNT:int}),Connexion au serveur.... ({COUNT:int}),Connessione al server. ({COUNT:int}),サーバーに接続中。 ({COUNT:int}),서버 입장 중. ({COUNT:int}),Entrando no servidor ({COUNT:int}),Entrando no servidor ({COUNT:int}),Подключение к серверу ({COUNT:int}),正在加入服务器 ({COUNT:int}),正在加入伺服器。({COUNT:int}),正在加入服务器 ({COUNT:int}) +,,,Joining server. {RET:translate}...({COUNT:int}),Verbindung zum Server wird hergestellt. {RET:translate}...({COUNT:int}),Joining server. {RET:translate}...({COUNT:int}),Uniéndose a un servidor. {RET:translate}...({COUNT:int}),Uniéndose a un servidor. {RET:translate}...({COUNT:int}),Connexion au serveur.... {RET:translate}...({COUNT:int}),Connessione al server. {RET:translate}...({COUNT:int}),サーバーに接続中。 {RET:translate}...({COUNT:int}),서버 입장 중. {RET:translate}...({COUNT:int}),Entrando no servidor. {RET:translate}...({COUNT:int}),Entrando no servidor. {RET:translate}...({COUNT:int}),Подключение к серверу. {RET:translate}...({COUNT:int}),正在加入服务器. {RET:translate}...({COUNT:int}),正在加入伺服器。{RET:translate}…({COUNT:int}),正在加入服务器. {RET:translate}...({COUNT:int}) +,,,Jump,Springen,Jump,Saltar,Saltar,Sauter,Salta,ジャンプ,점프,Pular,Pular,Прыжок,跳跃,跳躍,跳跃 +,,,Jump Animation,Springanimation,Jump Animation,Animación de salto,Animación de salto,Animation de saut,Animazione salto,ジャンプアニメーション,점프 애니메이션,Animação de salto,Animação de salto,Анимация прыжка,跳跃动画,跳躍動畫,跳跃动画 +,,,KOs,K.o.,KOs,N.º de KO,N.º de KO,Ko,KO,KO回数,KOs,Nocautes,Nocautes,Нокауты,击倒数,擊倒數,击倒数 +,,,Keyboard + Mouse,Tastatur + Maus,Keyboard + Mouse,Teclado + ratón,Teclado + ratón,Clavier + souris,Tastiera e mouse,キーボード + マウス,키보드 + 마우스,Teclado + mouse,Teclado + mouse,Клавиатура + мышь,键盘+鼠标,鍵盤+滑鼠,键盘+鼠标 +,,,Kicked by server. Please close and rejoin another game,Vom Sever gekickt. Bitte schließen und einem anderen Spiel beitreten.,Kicked by server. Please close and rejoin another game,Expulsado del servidor. Cierra el juego y únete a otro.,Expulsado del servidor. Cierra el juego y únete a otro.,Le serveur vous a expulsé(e). Veuillez quitter le jeu et rejoindre une autre partie.,Espulso dal server. Chiudi e ricomincia una nuova partita,ゲームから外されました。閉じてから別のゲームに参加してください,서버에서 강퇴 퇴장되었습니다. 게임 종료 후 다른 게임에 참가하세요.,Você foi expulso pelo servidor. Feche a tela e entre em outro jogo.,Você foi expulso pelo servidor. Feche a tela e entre em outro jogo.,"Вы были исключены сервером. Пожалуйста, закройте игру и зайдите снова.",已被服务器踢出。请关闭并重新加入其他游戏,您被伺服器踢出,請關閉程式並加入新的遊戲。,已被服务器踢出。请关闭并重新加入其他游戏 +,,,Label,Label,Label,Etiqueta,Etiqueta,Étiquette,Etichetta,ラベル,라벨,Rótulo,Rótulo,Метка,标签,標籤,标签 +,,,Leave Game,Spiel verlassen,Leave Game,Salir del juego,Salir del juego,Quitter le jeu,Esci dal gioco,ゲームを終了,게임 종료,Sair do jogo,Sair do jogo,Выйти из игры,离开游戏,離開遊戲,离开游戏 +,,,Left Arm,Linker Arm,Left Arm,Brazo izquierdo,Brazo izquierdo,Bras gauche,Braccio sinistro,左腕,왼팔,Braço esquerdo,Braço esquerdo,Левая рука,左臂,左臂,左臂 +,,,Left Leg,Linkes Bein,Left Leg,Pierna izquierda,Pierna izquierda,Jambe gauche,Gamba sinistra,左脚,왼다리,Perna esquerda,Perna esquerda,Левая нога,左腿,左腿,左腿 +,,,Left Mouse Button,Linke Maustaste,Left Mouse Button,Botón izquierdo del ratón,Botón izquierdo del ratón,Bouton gauche de la souris,Sinistro/Mouse,左マウスボタン,마우스 왼쪽 버튼,Mouse (esquerdo),Mouse (esquerdo),ЛКМ,鼠标左键,左滑鼠鍵,鼠标左键 +,,,Legs,Beine,Legs,Piernas,Piernas,Jambes,Gambe,脚,다리,Pernas,Pernas,Ноги,腿部,腿部,腿部 +,,,Loading,Laden,Loading,Cargando,Cargando,En cours,Caricamento,読み込み中,불러오는 중,Carregando,Carregando,Загрузка,正在载入,正在載入,正在载入 +,,,Loading.,Laden.,Loading.,Cargando.,Cargando.,En cours.,Caricamento.,読み込み中。,불러오는 중.,Carregando.,Carregando.,Загрузка,正在载入。,正在載入.,正在载入。 +,,,Loading..,Laden ...,Loading..,Cargando..,Cargando..,En cours..,Caricamento..,読み込み中..,불러오는 중..,Carregando..,Carregando..,Загрузка,正在载入..,正在載入..,正在载入.. +,,,Loading...,Laden ...,Loading...,Cargando...,Cargando...,En cours...,Caricam...,読み込み中...,불러오는 중...,Carregando...,Carregando...,Загрузка,正在载入...,正在載入...,正在载入... +,,,Lost connection to server due to timeout,Die Verbindung zum Server wurde aufgrund einer Zeitüberschreitung unterbrochen.,Lost connection to server due to timeout,Se ha perdido la conexión con el servidor por agotamiento del tiempo de espera,Se ha perdido la conexión con el servidor por agotamiento del tiempo de espera,Connexion au serveur perdue ; la session a expiré.,Connessione persa con il server per un errore di time-out,タイムアウトでサーバーへの接続が失われました,시간 초과로 인해 연결 해제됨,A conexão com o servidor foi perdida pois o limite de tempo de espera foi atingido,A conexão com o servidor foi perdida pois o limite de tempo de espera foi atingido,Разрыв соединения с сервером из-за превышения времени ожидания.,由于超时,与服务器的连接已断开,連線逾時,與伺服器連線中斷,由于超时,与服务器的连接已断开 +,,,Lua,Lua,Lua,Lua,Lua,Lua,Lua,Lua,Lua,Lua,Lua,Lua,Lua ,Lua,Lua +,,,Manual,Manuell,Manual,Manual,Manual,Manuel,Manuale,マニュアル,수동,Manual,Manual,Ручной,手动,手動,手动 +,,,Mem,Speicher,Mem,Mem.,Mem.,Mém,Mem,メモリー,메모리,Mem.,Mem.,Память,内存,記憶體,内存 +,,,Memory,Speicher,Memory,Memoria,Memoria,Mémoire,Memoria,メモリー,메모리,Memória,Memória,Память,内存,記憶體,内存 +,,,Menu Items,Menüobjekte,Menu Items,Objetos del menú,Objetos del menú,Objets du menu,Oggetti menu,メニューアイテム,메뉴 아이템,Itens de menu,Itens de menu,Пункты меню,菜单项目,選單項目,菜单项目 +,,,Menu Navigation,Menünavigation,Menu Navigation,Navegación del menú,Navegación del menú,Navigation du menu,Menu di navigazione,メニューナビゲーション,메뉴 내비게이션,Navegação do menu,Navegação do menu,Навигация в меню,菜单导航,選單導覽,菜单导航 +,,,Mesh,Mesh,Mesh,Mesh,Mesh,Maillage,Mesh,メッシュ,메시,Malha,Malha,Сетка,网格,模組,网格 +,,,MeshPart,MeshPart,MeshPart,MeshPart,MeshPart,MeshPart,Parte mesh,MeshPart,MeshPart,MeshPart,MeshPart,Полигональная часть,MeshPart,MeshPart,MeshPart +,,,Misc,Verschiedenes,Misc,Misc.,Misc.,Divers,Vari,その他,기타,Diversos,Diversos,Разное,杂项,其它,杂项 +,,,Model,Modell,Model,Modelo,Modelo,Modèle,Modello,モデル,모델,Modelo,Modelo,Модель,模型,模型,模型 +,,,Mouse Sensitivity,Mausempfindlichkeit,Mouse Sensitivity,Sensibilidad del ratón,Sensibilidad del ratón,Sensibilité de la souris,Precisione mouse,マウス感度,마우스 감도,Sensibilidade do mouse,Sensibilidade do mouse,Чувствительность мыши,鼠标灵敏度,滑鼠靈敏度,鼠标灵敏度 +,,,Mouse Wheel,Mausrad,Mouse Wheel,Rueda del ratón,Rueda del ratón,Molette de la souris,Rotellina mouse,マウスホイール,마우스 휠,Roda do mouse,Roda do mouse,Колесо мыши,鼠标滚轮,滑鼠滾輪,鼠标滚轮 +,,,Mouselock,Maussperre,Mouselock,Bloqueo del ratón,Bloqueo del ratón,Verrouillage souris,Blocco mouse,マウスロック,마우스 잠금,Travar mouse,Travar mouse,Фиксация мыши,鼠标锁定,滑鼠鎖定,鼠标锁定 +,,,Move,Bewegen,Move,Mover,Mover,Se déplacer,Muovi,移動,이동,Mover,Mover,Передвижение,移动,移動,移动 +,,,Move Backward,Rückwärts bewegen,Move Backward,Moverse hacia atrás,Moverse hacia atrás,Reculer,Muovi indietro,後ろに移動,뒤로 이동,Mover (trás),Mover (trás),Назад,后退,向後,后退 +,,,Move Forward,Vorwärts bewegen,Move Forward,Moverse hacia delante,Moverse hacia delante,Avancer,Muovi in avanti,前に移動,앞으로 이동,Mover (frente),Mover (frente),Вперед,向前,向前,向前 +,,,Move Left,Nach links bewegen,Move Left,Moverse a la izquierda,Moverse a la izquierda,Aller à gauche,Muovi a sinistra,左に移動,왼쪽으로 이동,Mover (esquerda),Mover (esquerda),Влево,左移,向左,左移 +,,,Move Right,Nach rechts bewegen,Move Right,Moverse a la derecha,Moverse a la derecha,Aller à droite,Muovi a destra,右に移動,오른쪽으로 이동,Mover (direita),Mover (direita),Вправо,右移,向右,右移 +,,,Movement Mode,Bewegungsmodus,Movement Mode,Modo de movimiento,Modo de movimiento,Mode déplacement,Modalità movimento,動作モード,이동 모드,Modo de movimento,Modo de movimento,Режим передвижения,移动模式,移動模式,移动模式 +,,,Neck Accessory,Hals-Accessoire,Neck Accessory,Accesorio para el cuello,Accesorio para el cuello,Accessoire de cou,Accessorio collo,首アクセサリ,목 장신구,Acessório de pescoço,Acessório de pescoço,Аксессуар для шеи,颈部配饰,頸部飾品,颈部配饰 +,,,Network error {RBX_NUMBER},Netzwerkfehler {RBX_NUMBER},Network error {RBX_NUMBER},Error de red {RBX_NUMBER},Error de red {RBX_NUMBER},Erreur réseau {RBX_NUMBER},Errore di rete {RBX_NUMBER},ネットワークエラー {RBX_NUMBER},네트워크 오류 {RBX_NUMBER},Erro de rede {RBX_NUMBER},Erro de rede {RBX_NUMBER},Ошибка сети {RBX_NUMBER},网络错误 {RBX_NUMBER},網路錯誤 {RBX_NUMBER},网络错误 {RBX_NUMBER} +,,,New Follower,Neuer Follower,New Follower,Seguidor nuevo,Seguidor nuevo,Nouvel abonné,Nuovo follower,新規フォロワー,새 팔로워,Novo seguidor,Novo seguidor,Новый подписчик,新粉丝,新追蹤者,新粉丝 +,,,New Friend,Neuer Freund,New Friend,Amigo nuevo,Amigo nuevo,Nouvel ami,Nuovo amico,新しい友達,새 친구,Novo amigo,Novo amigo,Новый друг,新好友,新好友,新好友 +,,,Not authorized to join this game,"Du bist nicht befugt, diesem Spiel beizutreten",Not authorized to join this game,No tienes autorización para unirte a este juego,No tienes autorización para unirte a este juego,Vous n'avez pas l'autorisation de rejoindre ce jeu.,Non hai l'autorizzazione per partecipare a questa partita,このゲームに参加する権限がありません,게임 참여 권한 없음,Você não tem autorização para entrar neste jogo,Você não tem autorização para entrar neste jogo,Не разрешено присоединяться к этой игре,没有加入此游戏的权限,權限不足,無法加入此遊戲。,没有加入此游戏的权限 +,,,Not authorized to join this game. ({COUNT:int}),"Du bist nicht befugt, diesem Spiel beizutreten. ({COUNT:int})",Not authorized to join this game. ({COUNT:int}),No tienes autorización para unirte a este juego. ({COUNT:int}),No tienes autorización para unirte a este juego. ({COUNT:int}),Vous n'avez pas l'autorisation de rejoindre ce jeu.. ({COUNT:int}),Non hai l'autorizzazione per partecipare a questa partita. ({COUNT:int}),このゲームに参加する権限がありません。 ({COUNT:int}),게임 참여 권한 없음 ({COUNT:int}),Você não tem autorização para entrar neste jogo ({COUNT:int}),Você não tem autorização para entrar neste jogo ({COUNT:int}),Не разрешено присоединяться к этой игре ({COUNT:int}),没有加入此游戏的权限 ({COUNT:int}),權限不足,無法加入此遊戲。({COUNT:int}),没有加入此游戏的权限 ({COUNT:int}) +,,,Not authorized to join this game. {RET:translate}...({COUNT:int}),"Du bist nicht befugt, diesem Spiel beizutreten. {RET:translate}...({COUNT:int})",Not authorized to join this game. {RET:translate}...({COUNT:int}),No tienes autorización para unirte a este juego. {RET:translate}...({COUNT:int}),No tienes autorización para unirte a este juego. {RET:translate}...({COUNT:int}),Vous n'avez pas l'autorisation de rejoindre ce jeu.. {RET:translate}...({COUNT:int}),Non hai l'autorizzazione per partecipare a questa partita. {RET:translate}...({COUNT:int}),このゲームに参加する権限がありません。 {RET:translate}...({COUNT:int}),게임 참여 권한 없음. {RET:translate}...({COUNT:int}),Você não tem autorização para entrar neste jogo. {RET:translate}...({COUNT:int}),Você não tem autorização para entrar neste jogo. {RET:translate}...({COUNT:int}),Не разрешено присоединяться к этой игре. {RET:translate}...({COUNT:int}),没有加入此游戏的权限. {RET:translate}...({COUNT:int}),權限不足,無法加入此遊戲。{RET:translate}…({COUNT:int}),没有加入此游戏的权限. {RET:translate}...({COUNT:int}) +,,,Notifications,Benachrichtigungen,Notifications,Notificaciones,Notificaciones,Notifications,Notifiche,お知らせ,알림,Notificações,Notificações,Уведомления,通知,通知,通知 +,,,OK,Okay,OK,Aceptar,Aceptar,Ok,OK,OK,확인,OK,OK,OK,好,確定,好 +,,,Off,Aus,Off,Desactivado,Desactivado,Arrêt,Disattivato,オフ,끄기,Desligado,Desligado,Выкл.,关闭,關閉,关闭 +,,,Offsite Links,Externe Links,Offsite Links,Enlaces externos,Enlaces externos,Liens hors site,Collegamenti ad altri siti,外部リンク,외부 링크,Links externos,Links externos,Внешние ссылки,离线链接,站外連結,离线链接 +,,,Okay,Okay,Okay,Aceptar,Aceptar,D'accord,OK,OK,확인,Ok,Ok,ОК,好,好,好 +,,,On,An,On,Activado,Activado,Marche,Attivato,オン,켜기,Ligado,Ligado,Вкл.,开启,開啟,开启 +,,,Open,Öffnen,Open,Abrir,Abrir,Ouvert(e),Apri,開く,열기,Abrir,Abrir,Открыть,打开,開啟,打开 +,,,Open Folder,Ordner öffnen,Open Folder,Abrir carpeta,Abrir carpeta,Ouvrir le dossier,Apri cartella,フォルダを開く,폴더 열기,Abrir pasta,Abrir pasta,Открыть папку,打开文件夹,開啟資料夾,打开文件夹 +,,,Outrageous Builders Club,Outrageous Builders Club,Outrageous Builders Club,Outrageous Builders Club,Outrageous Builders Club,Outrageous Builders Club,Turbo Builders Club,Outrageous Builders Club,Outrageous Builders Club,Outrageous Builders Club,Outrageous Builders Club,Невероятный клуб создателей,Outrageous Builders Club,Outrageous Builders Club,Outrageous Builders Club +,,,Package,Paket,Package,Paquete,Paquete,Pack,Pacchetto,パッケージ,패키지,Pacote,Pacote,Набор,套装,套裝,套装 +,,,Pants,Hose,Pants,Pantalones,Pantalones,Pantalon,Pantaloni,  ズボン,바지,Calças,Calças,Штаны,裤子,褲子,裤子 +,,,Perf. Stats,Leistungsw.,Perf. Stats,Est. de rend.,Est. de rend.,Stats perf.,Stat. prest.,パフォーマンス解析,성능 통계,Estat. Des.,Estat. Des.,Показатели произ.,表现统计,效能數據,表现统计 +,,,Performance Stats,Leistungswerte,Performance Stats,Estad. de rendimiento,Estad. de rendimiento,Stats de performance,Statistiche prestazioni,パフォーマンス解析,성능 통계,Estat. de desempenho,Estat. de desempenho,Показатели производительности,表现统计,效能數據,表现统计 +,,,Personal Question,Persönliche Frage,Personal Question,Pregunta personal,Pregunta personal,Question personnelle,Domanda personale,プライベートな質問,개인 질문,Pergunta pessoal,Pergunta pessoal,Личный вопрос,私人问题,私人問題,私人问题 +,,,Phys,Phys.,Phys,Fís.,Fís.,Phys.,Fisico,物理,물리,Fís.,Fís.,Физ.,物理,物理,物理 +,,,Physics,Physik,Physics,Física,Física,Physique,Fisica,物理,물리,Física,Física,Физика,物理,物理,物理 +,,,Place,Ort,Place,Lugar,Lugar,Emplacement,Località,プレース,장소,Local,Local,Место,地点,空間,地点 +,,,Player,Spieler,Player,Jugador,Jugador,Joueur,Giocatore,プレイヤー,플레이어,Jogador,Jogador,Игрок,玩家,玩家,玩家 +,,,Playerlist,Spielerliste,Playerlist,Lista de jugadores,Lista de jugadores,Liste de joueurs,Lista giocatori,プレイヤーリスト,플레이어 목록,Jogadores,Jogadores,Список игроков,玩家名单,玩家名單,玩家名单 +,,,Players,Spieler,Players,Jugadores,Jugadores,Joueurs,Giocatori,プレイヤー,플레이어,Jogadores,Jogadores,Игроки,玩家,玩家,玩家 +,,,Plugin,Plug-in,Plugin,Plugin,Plugin,Plugin,Plug-in,プラグイン,플러그인,Plugin,Plugin,Расширение,插件,外掛程式,插件 +,,,Point Awarded,Punkt verliehen,Point Awarded,Punto concedido,Punto concedido,Point accordé,Punto attribuito,ポイント授与,획득한 포인트,Ponto concedido,Ponto concedido,Получено очко,已获得点数,已獲點數,已获得点数 +,,,Points Awarded,Punkte verliehen,Points Awarded,Puntos concedidos,Puntos concedidos,Points accordés,Punti attribuiti,ポイント授与,획득한 포인트,Pontos concedidos,Pontos concedidos,Получены очки,已获得点数,已獲點數,已获得点数 +,,,Print Screen,Drucktaste,Print Screen,Imprimir pantalla,Imprimir pantalla,Impression écran,STAMP,画面プリント,화면 캡쳐,Captura de Tela,Captura de Tela,Print Screen,屏幕截图,截圖,屏幕截图 +,,,"Protocol mismatch, please reconnect",Protokoll-Diskrepanz. Bitte erneut verbinden.,"Protocol mismatch, please reconnect",Incompatibilidad del protocolo. Conéctate de nuevo.,Incompatibilidad del protocolo. Conéctate de nuevo.,"Le protocole ne correspond pas, veuillez vous reconnecter.",Mancata corrispondenza del protocollo; è necessario riconnettersi,プロトコルの不一致です。再接続してください,프로토콜이 일치하지 않아요. 다시 연결하세요.,Incompatibilidade do Protocolo. Conecte-se novamente.,Incompatibilidade do Protocolo. Conecte-se novamente.,"Несовпадение протокола, пожалуйста, установите соединение повторно.",协议不匹配,请重新连接,通訊協定不符,請重新連線,协议不匹配,请重新连接 +,,,Purchasing,Wird gekauft ...,Purchasing,Compras,Compras,Achat,Acquisto,購入中,구매 중,Comprando,Comprando,Покупка,正在购买,正在購買,正在购买 +,,,ROBLOX version is out of date. Please uninstall and try again.,Die ROBLOX-Version ist nicht aktuell. Bitte deinstallieren und erneut versuchen.,ROBLOX version is out of date. Please uninstall and try again.,"Esta versión de ROBLOX es obsoleta. Por favor, desinstálala y vuelve a intentarlo.","Esta versión de ROBLOX es obsoleta. Por favor, desinstálala y vuelve a intentarlo.",Cette version de ROBLOX est obsolète. Veuillez désinstaller et réessayer.,Versione di ROBLOX obsoleta. Disinstalla e riprova.,古いバージョンのROBLOXです。アンインストールしてリトライしてください。,ROBLOX 버전이 오래되었습니다. 프로그램 제거 후 다시 시도하세요.,Sua versão do ROBLOX não está atualizada. Desinstale e tente novamente.,Sua versão do ROBLOX não está atualizada. Desinstale e tente novamente.,Эта версия ROBLOX устарела. Удалите приложение и повторите попытку.,Roblox 版本已过期。请解除安装并重试。,ROBLOX 版本過時,請重新安裝 ROBLOX。,Roblox 版本已过期。请解除安装并重试。 +,,,Record,Aufnehmen,Record,Grabar,Grabar,Enregistrer,Registra,録画,녹화,Gravar,Gravar,Запись,录制,錄影,录制 +,,,Record Video,Video aufnehmen,Record Video,Grabar vídeo,Grabar vídeo,Enregistrer vidéo,Registra video,ビデオ録画,비디오 녹화,Gravar vídeo,Gravar vídeo,Запись видео,录制视频,錄影,录制视频 +,,,Recv,Empfangen,Recv,Recib.,Recib.,Reçu,Ricev.,受信,받음,Receb.,Receb.,Получ.,收到,收到,收到 +,,,Remove From Hotbar,Schnellzugriff entfernen,Remove From Hotbar,Eliminar de la barra de acceso rápido,Eliminar de la barra de acceso rápido,Retirer de la barre de raccourcis,Togli da barra scelta rap.,ホットバーから削除,핫바 창에서 제거,Remover da barra,Remover da barra,Удалить из панели быстрого доступа,从快捷栏删除,從快捷列移除,从快捷栏删除 +,,,Report,Melden,Report,Denunciar,Denunciar,Signaler,Segnala,報告する,신고,Denunciar,Denunciar,Сообщить,举报,檢舉,举报 +,,,Report Abuse,Verstoß melden,Report Abuse,Denunciar abuso,Denunciar abuso,Signaler infraction,Segnala abuso,規約違反を報告,신고하기,Denunciar abuso,Denunciar abuso,Сообщить о нарушении,举报滥用,檢舉濫用,举报滥用 +,,,Request Sent,Anfrage gesendet,Request Sent,Solicitud enviada,Solicitud enviada,Demande envoyée,Richiesta inviata,リクエストを送信しました,요청 보냄,Pedido enviado,Pedido enviado,Запрос отправлен,请求已发送,請求已送出,请求已发送 +,,,Requested game is full,Das angefragte Spiel ist voll,Requested game is full,El juego solicitado está lleno,El juego solicitado está lleno,La partie demandée est complète.,Il gioco richiesto è al completo,リクエストしたゲームは満員です。,요청한 게임이 만원입니다.,O jogo solicitado está cheio,O jogo solicitado está cheio,Запрашиваемая игра переполнена,请求的游戏已满员,請求的遊戲已額滿,请求的游戏已满员 +,,,"Requested game is full, retrying...",Gewünschtes Spiel ist voll. Neuer Versuch ...,"Requested game is full, retrying...",El juego solicitado está lleno. Intentándolo de nuevo...,El juego solicitado está lleno. Intentándolo de nuevo...,"Le jeu demandé est complet, nouvelle tentative...",Il gioco richiesto è al completo. Nuovo tentativo...,リクエストしたゲームは満員です。リトライ中...,요청한 게임이 만원입니다. 재시도 중...,O jogo solicitado está cheio. Tentando de novo...,O jogo solicitado está cheio. Tentando de novo...,Запрашиваемая игра переполнена. Новая попытка...,已请求的游戏已满员。正在重试...,請求的遊戲已額滿,正在重試…,已请求的游戏已满员。正在重试... +,,,Requested game is full. ({COUNT:int}),Das angefragte Spiel ist voll. ({COUNT:int}),Requested game is full. ({COUNT:int}),El juego solicitado está lleno. ({COUNT:int}),El juego solicitado está lleno. ({COUNT:int}),La partie demandée est complète.. ({COUNT:int}),Il gioco richiesto è al completo. ({COUNT:int}),リクエストしたゲームは満員です。 ({COUNT:int}),요청한 게임이 만원입니다. ({COUNT:int}),O jogo solicitado está cheio ({COUNT:int}),O jogo solicitado está cheio ({COUNT:int}),Запрашиваемая игра переполнена ({COUNT:int}),请求的游戏已满员 ({COUNT:int}),請求的遊戲已額滿。({COUNT:int}),请求的游戏已满员 ({COUNT:int}) +,,,Requested game is full. {RET:translate}...({COUNT:int}),Das angefragte Spiel ist voll. {RET:translate}...({COUNT:int}),Requested game is full. {RET:translate}...({COUNT:int}),El juego solicitado está lleno. {RET:translate}...({COUNT:int}),El juego solicitado está lleno. {RET:translate}...({COUNT:int}),La partie demandée est complète.. {RET:translate}...({COUNT:int}),Il gioco richiesto è al completo. {RET:translate}...({COUNT:int}),リクエストしたゲームは満員です。 {RET:translate}...({COUNT:int}),요청한 게임이 만원입니다. {RET:translate}...({COUNT:int}),O jogo solicitado está cheio. {RET:translate}...({COUNT:int}),O jogo solicitado está cheio. {RET:translate}...({COUNT:int}),Запрашиваемая игра переполнена. {RET:translate}...({COUNT:int}),请求的游戏已满员. {RET:translate}...({COUNT:int}),請求的遊戲已額滿。{RET:translate}…({COUNT:int}),请求的游戏已满员. {RET:translate}...({COUNT:int}) +,,,Requesting Server...,Serveranfrage ...,Requesting Server...,Solicitando servidor...,Solicitando servidor...,Demande serveur...,Richiesta server...,サーバーにリクエスト中...,서버 요청 중...,Solicitando servidor...,Solicitando servidor...,Запрос сервера...,正在向服务器发送请求...,正在請求伺服器…,正在向服务器发送请求... +,,,Reset,Zurücksetzen,Reset,Reiniciar,Reiniciar,Réinitialiser,Azzera,リセット,재설정,Reiniciar,Reiniciar,Сброс,重置,重置,重置 +,,,Resume Game,Spiel fortsetzen,Resume Game,Seguir jugando,Seguir jugando,Reprendre le jeu,Riprendi gioco,ゲームを再開,게임 계속하기,Continuar jogo,Continuar jogo,Вернуться в игру,继续游戏,繼續遊戲,继续游戏 +,,,Retrying,Neuer Versuch,Retrying,Intentándolo de nuevo,Intentándolo de nuevo,Nouvelle tentative,Nuovo tentativo,リトライ中,재시도 중,Tentando novamente,Tentando novamente,Новая попытка,正在重试,正在重試,正在重试 +,,,Revoke Friend Request,Widerruf der Freundschaftsanfrage,Revoke Friend Request,Revocar solicitud de amigo,Revocar solicitud de amigo,Révoquer une demande d'ami,Revoca richiesta amico,友達リクエストを取り消す,친구 요청 취소,Revocar pedido de amigo,Revocar pedido de amigo,Отменить запрос друга,撤销好友邀请,取消好友邀請,撤销好友邀请 +,,,Right Arm,Rechter Arm,Right Arm,Brazo derecho,Brazo derecho,Bras droit,Braccio destro,右腕,오른팔,Braço direito,Braço direito,Правая рука,右臂,右臂,右臂 +,,,Right Leg,Rechtes Bein,Right Leg,Pierna derecha,Pierna derecha,Jambe droite,Gamba destra,右脚,오른다리,Perna direita,Perna direita,Правая нога,右腿,右腿,右腿 +,,,Right Mouse Button,Rechte Maustaste,Right Mouse Button,Botón derecho del ratón,Botón derecho del ratón,Bouton droit de la souris,Destro/Mouse,右マウスボタン,마우스 오른쪽 버튼,Mouse (direito),Mouse (direito),ПКМ,鼠标右键,右滑鼠鍵,鼠标右键 +,,,Roblox Menu,Roblox-Menü,Roblox Menu,Menú de Roblox,Menú de Roblox,Menu Roblox,Menu Roblox,Roblox メニュー,Roblox 메뉴,Menu Roblox,Menu Roblox,Меню Roblox,系统菜单,Roblox 選單,系统菜单 +,,,Roblox has shut down this game server for maintenance,Roblox hat diesen Spielserver aufgrund von Wartungsarbeiten abgeschaltet,Roblox has shut down this game server for maintenance,Roblox ha cerrado el servidor de este juego por mantenimiento,Roblox ha cerrado el servidor de este juego por mantenimiento,Roblox a arrêté ce serveur de jeu pour effectuer la maintenance.,Roblox ha chiuso questo server di gioco per manutenzione,メンテナンスのため、Roblox によってゲームサーバーがシャットダウンされています。,Roblox가 유지보수를 위해 본 게임 서버를 종료했습니다.,O servidor deste jogo foi encerrado pela Roblox para manutenção,O servidor deste jogo foi encerrado pela Roblox para manutenção,Roblox отключил игровой сервер для техобслуживания.,维护期间,Roblox 已关闭此游戏服务器,即將進行伺服器維護,Roblox 已關閉此遊戲伺服器,维护期间,Roblox 已关闭此游戏服务器 +,,,Roblox has shut down this game server for maintenance. {RET:translate}...({COUNT:int}),Roblox hat diesen Spielserver aufgrund von Wartungsarbeiten abgeschaltet. {RET:translate}...({COUNT:int}),Roblox has shut down this game server for maintenance. {RET:translate}...({COUNT:int}),Roblox ha cerrado el servidor de este juego por mantenimiento. {RET:translate}...({COUNT:int}),Roblox ha cerrado el servidor de este juego por mantenimiento. {RET:translate}...({COUNT:int}),Roblox a arrêté ce serveur de jeu pour effectuer la maintenance.. {RET:translate}...({COUNT:int}),Roblox ha chiuso questo server di gioco per manutenzione. {RET:translate}...({COUNT:int}),メンテナンスのため、Roblox によってゲームサーバーがシャットダウンされています。. {RET:translate}...({COUNT:int}),Roblox가 유지보수를 위해 본 게임 서버를 종료했습니다. {RET:translate}...({COUNT:int}),O servidor deste jogo foi encerrado pela Roblox para manutenção. {RET:translate}...({COUNT:int}),O servidor deste jogo foi encerrado pela Roblox para manutenção. {RET:translate}...({COUNT:int}),Roblox отключил игровой сервер для техобслуживания.. {RET:translate}...({COUNT:int}),维护期间,Roblox 已关闭此游戏服务器. {RET:translate}...({COUNT:int}),即將進行伺服器維護,Roblox 已關閉此遊戲伺服器。{RET:translate}…({COUNT:int}),维护期间,Roblox 已关闭此游戏服务器. {RET:translate}...({COUNT:int}) +,,,Rotate,Drehen,Rotate,Rotar,Rotar,Tourner,Ruota,回転,회전,Girar,Girar,Вращение,旋转,旋轉,旋转 +,,,Rotate Camera,Kamera drehen,Rotate Camera,Rotar cámara,Rotar cámara,Rotation de caméra,Ruota visuale,カメラを回転,카메라 회전,Girar câmera,Girar câmera,Вращение камеры,旋转镜头,旋轉相機,旋转镜头 +,,,Run Animation,Laufanimation,Run Animation,Animación de carrera,Animación de carrera,Animation de course,Animazione corsa,走るアニメーション,달리기 애니메이션,Animação de corrida,Animação de corrida,Анимация бега,跑步动画,奔跑動畫,跑步动画 +,,,S/Down Arrow,S/Abwärtspfeil,S/Down Arrow,S/Cursor abajo,S/Cursor abajo,S/Flèche bas,S/Freccia GIÙ,S/下カーソル,S/아래쪽 화살표,S/seta para baixo,S/seta para baixo,S/стрелка вниз,S/下箭头,S、↓,S/下箭头 +,,,Safe Zone,Sichere Zone,Safe Zone,Zona segura,Zona segura,Zone sécurisée,Zona sicura,セーフゾーン,안전 구역,Zona de segurança,Zona de segurança,Безопасная зона,安全区,安全區,安全区 +,,,Save To Disk,Auf Festplatte speichern,Save To Disk,Guardar en disco,Guardar en disco,Sauvegarder sur le disque,Salva su disco,ディスクに保存,디스크에 저장,Salvar em disco,Salvar em disco,Сохранить на диск,保存至磁盘,儲存到硬碟,保存至磁盘 +,,,Scamming,Scamming,Scamming,Estafa,Estafa,Arnaque,Truffa,詐欺,신용 범죄,Fraude,Fraude,Мошенничество,诈骗,詐騙,诈骗 +,,,Screenshot,Screenshot,Screenshot,Captura de pantalla,Captura de pantalla,Capture d'écran,Screenshot,スクリーンショット,스크린숏,Captura de tela,Captura de tela,Снимок экрана,屏幕截图,截圖,屏幕截图 +,,,Screenshot Taken,Screenshot gespeichert,Screenshot Taken,Captura tomada,Captura tomada,Capture d'écran prise,Screenshot catturato,撮影したスクリーンショット,스크린숏 찍기 완료,Captura de tela salva,Captura de tela salva,Снимок экрана сделан,已截取屏幕快照,已截圖,已截取屏幕快照 +,,,Select/Swap,Auswählen/Austauschen,Select/Swap,Seleccionar/alternar,Seleccionar/alternar,Sélection/Échange,Selez./Scambia,選択/交換,선택/교체,Selecionar/trocar,Selecionar/trocar,Выбрать/сменить,选取/切换,選擇 / 切換,选取/切换 +,,,Send Friend Request,Freundesanfrage senden,Send Friend Request,Enviar solicitud de amistad,Enviar solicitud de amistad,Envoyer demande d'ami,Invia richiesta di amicizia,友達リクエストを送信,친구 요청 보내기,Enviar pedido de amizade,Enviar pedido de amizade,Отправить запрос дружбы,发送好友邀请,傳送好友邀請,发送好友邀请 +,,,Send Game Invites,Spieleinladungen senden,Send Game Invites,Enviar invitaciones al juego,Enviar invitaciones al juego,Envoyer des invitations,Manda inviti di gioco,ゲームへの招待を送る,게임 초대 전송,Enviar convites,Enviar convites,Отправить приглашения,发送游戏邀请,傳送遊戲邀請,发送游戏邀请 +,,,Send Request,Anfrage senden,Send Request,Enviar solicitud,Enviar solicitud,Envoyer demande,Invia richiesta,リクエスト送信,요청 전송,Enviar pedido,Enviar pedido,Отправить запрос,发送请求,傳送請求,发送请求 +,,,Sent you a friend request!,hat dir eine Freundesanfrage gesendet!,Sent you a friend request!,te ha enviado una solicitud de amistad,te ha enviado una solicitud de amistad,vous a envoyé une demande d'ami !,ti ha inviato una richiesta di amicizia!,君に友達リクエストを送ったよ!,친구 요청을 보냈어요!,enviou um pedido de amizade para você!,enviou um pedido de amizade para você!,отправил вам запрос дружбы!,向你发送了好友邀请!,已向您傳送好友邀請!,向你发送了好友邀请! +,,,"Server found, loading.... ({COUNT:int})","Server gefunden, wird geladen .... ({COUNT:int})","Server found, loading.... ({COUNT:int})",Servidor encontrado. Cargando.... ({COUNT:int}),Servidor encontrado. Cargando.... ({COUNT:int}),"Serveur trouvé, chargement en cours.... ({COUNT:int})","Server trovato, caricamento.... ({COUNT:int})",サーバが見つかりました。読み込み中です... ({COUNT:int}),"서버를 찾았습니다, 불러오는 중... ({COUNT:int})","Servidor encontrado, carregando... ({COUNT:int})","Servidor encontrado, carregando... ({COUNT:int})",Сервер найден. Загрузка... ({COUNT:int}),找到服务器,正在加载... ({COUNT:int}),找到伺服器,正在載入…({COUNT:int}),找到服务器,正在加载... ({COUNT:int}) +,,,"Server found, loading.... {RET:translate}...({COUNT:int})","Server gefunden, wird geladen .... {RET:translate}...({COUNT:int})","Server found, loading.... {RET:translate}...({COUNT:int})",Servidor encontrado. Cargando.... {RET:translate}...({COUNT:int}),Servidor encontrado. Cargando.... {RET:translate}...({COUNT:int}),"Serveur trouvé, chargement en cours.... {RET:translate}...({COUNT:int})","Server trovato, caricamento.... {RET:translate}...({COUNT:int})",サーバが見つかりました。読み込み中です.... {RET:translate}...({COUNT:int}),"서버를 찾았습니다, 불러오는 중.... {RET:translate}...({COUNT:int})","Servidor encontrado, carregando.... {RET:translate}...({COUNT:int})","Servidor encontrado, carregando.... {RET:translate}...({COUNT:int})",Сервер найден. Загрузка.... {RET:translate}...({COUNT:int}),找到服务器,正在加载.... {RET:translate}...({COUNT:int}),找到伺服器,正在載入…{RET:translate}…({COUNT:int}),找到服务器,正在加载.... {RET:translate}...({COUNT:int}) +,,,Server is busy,Server ist ausgelastet,Server is busy,El servidor está ocupado,El servidor está ocupado,Le serveur est occupé.,Il server è occupato,サーバーがビジー状態です,서버 사용량이 많아요.,Servidor ocupado,Servidor ocupado,Сервер занят,服务器忙,伺服器忙碌中,服务器忙 +,,,Server is busy. ({COUNT:int}),Server ist ausgelastet. ({COUNT:int}),Server is busy. ({COUNT:int}),El servidor está ocupado. ({COUNT:int}),El servidor está ocupado. ({COUNT:int}),Le serveur est occupé.. ({COUNT:int}),Il server è occupato. ({COUNT:int}),サーバーがビジー状態です。 ({COUNT:int}),서버 사용량이 많아요. ({COUNT:int}),Servidor ocupado ({COUNT:int}),Servidor ocupado ({COUNT:int}),Сервер занят ({COUNT:int}),服务器忙 ({COUNT:int}),伺服器忙碌中。({COUNT:int}),服务器忙 ({COUNT:int}) +,,,Server is busy. {RET:translate}...({COUNT:int}),Server ist ausgelastet. {RET:translate}...({COUNT:int}),Server is busy. {RET:translate}...({COUNT:int}),El servidor está ocupado. {RET:translate}...({COUNT:int}),El servidor está ocupado. {RET:translate}...({COUNT:int}),Le serveur est occupé.. {RET:translate}...({COUNT:int}),Il server è occupato. {RET:translate}...({COUNT:int}),サーバーがビジー状態です。 {RET:translate}...({COUNT:int}),서버 사용량이 많아요.. {RET:translate}...({COUNT:int}),Servidor ocupado. {RET:translate}...({COUNT:int}),Servidor ocupado. {RET:translate}...({COUNT:int}),Сервер занят. {RET:translate}...({COUNT:int}),服务器忙. {RET:translate}...({COUNT:int}),伺服器忙碌中。{RET:translate}…({COUNT:int}),服务器忙. {RET:translate}...({COUNT:int}) +,,,Server was shutdown due to no active players,"Der Server wurde abgeschaltet, weil keine aktiven Spieler vorhanden waren",Server was shutdown due to no active players,Se ha cerrado el servidor por inactividad de los jugadores,Se ha cerrado el servidor por inactividad de los jugadores,"Le serveur a été fermé, car aucun joueur n'était actif.",Il server è stato chiuso per mancanza di giocatori attivi,アクティブなプレイヤーがいないため、サーバーはシャットダウンされました,플레이 중인 플레이어가 없어 서버가 종료되었습니다,O servidor foi encerrado pois não há jogadores ativos,O servidor foi encerrado pois não há jogadores ativos,Сервер был отключен из-за отсутствия игроков.,由于没有活跃玩家,服务器关闭,沒有活躍玩家,伺服器已關閉,由于没有活跃玩家,服务器关闭 +,,,Set by Developer,Vom Entwickler festgelegt,Set by Developer,Configurado por el desarrollador,Configurado por el desarrollador,Défini par développeur,Impostato da sviluppatore,開発者による設定,개발자 설정,Definido pelo desenvolvedor,Definido pelo desenvolvedor,Установлено разработчиком,由开发人员设置,由開發人員設定,由开发人员设置 +,,,Settings,Einstellungen,Settings,Config.,Config.,Paramètres,Impostazioni,設定,설정,Config.,Config.,Настройки,设置,設定,设置 +,,,Shift,Umschalttaste,Shift,Mayúsculas,Mayúsculas,Maj,MAIUSC,シフト,Shift,Shift,Shift,Shift,Shift,Shift,Shift +,,,Shift Lock Switch,Shift-Lock-Schalter,Shift Lock Switch,Bloqueo de mayúsculas,Bloqueo de mayúsculas,Touche Verr. Vue,BLOC MAIUSC,シフトロックスイッチ,Shift Lock 전환,Trava do shift,Trava do shift,Переключение клавишей Shift,镜头切换开关,Shift Lock 開關,镜头切换开关 +,,,Shirt,Hemd,Shirt,Camisa,Camisa,Chemise,Camicia,シャツ,셔츠,Camisa,Camisa,Рубашка,衬衫,襯衫,衬衫 +,,,Shoulder Accessory,Schulter-Accessoire,Shoulder Accessory,Accesorio para el hombro,Accesorio para el hombro,Accessoire d'épaule,Accessorio spalla,肩アクセサリ,어깨 장신구,Acessório de ombro,Acessório de ombro,Аксессуар для плеч,肩部配饰,肩膀飾品,肩部配饰 +,,,SolidModel,SolidModel,SolidModel,SolidModel,SolidModel,SolidModel,Modello solido,SolidModel,SolidModel,SolidModel,SolidModel,Твердая модель,SolidModel,SolidModel,SolidModel +,,,Space,Leertaste,Space,Espacio,Espacio,Espace,BARRA SPAZIATRICE,スペース,스페이스,Espaço,Espaço,Пробел,空格,空格,空格 +,,,Speed,Tempo,Speed,Velocidad,Velocidad,Vitesse,Velocità,速度,속도,Velocidade,Velocidade,Скорость,速度,速度,速度 +,,,Stop Recording,Aufnahme beenden,Stop Recording,Detener grabación,Detener grabación,Arrêter l'enregistrement,Interrompi registrazione,録画を停止する,녹화 중지,Parar gravação,Parar gravação,Остановить запись,停止录音,停止錄影,停止录音 +,,,Submit,Abschicken,Submit,Enviar,Enviar,Soumettre,Invia,送信,확인,Enviar,Enviar,Отправить,提交,提交,提交 +,,,Swearing,Fluchen,Swearing,Palabras malsonantes,Palabras malsonantes,Insultes,Turpiloquio,ののしり,욕설,Palavrões,Palavrões,Ненормативная лексика,脏话谩骂,髒話謾罵,脏话谩骂 +,,,Swim Animation,Schwimmanimation,Swim Animation,Animación de natación,Animación de natación,Animation de nage,Animazione nuotata,泳ぐアニメーション,수영 애니메이션,Animação de nado,Animação de nado,Анимация плавания,游泳动画,游泳動畫,游泳动画 +,,,Switch Tool,Werkzeug wechseln,Switch Tool,Alternar herramienta,Alternar herramienta,Outil d'échange,Cambia strumento,ツールを交換,도구 전환,Trocar ferramenta,Trocar ferramenta,Сменить предмет,切换工具,切換工具,切换工具 +,,,T-Shirt,T-Shirt,T-Shirt,Camiseta,Camiseta,Tee-shirt,Maglietta,Tシャツ,티셔츠,Camiseta,Camiseta,Футболка,T 恤,T 恤,T 恤 +,,,TAB,Tabulator,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB,TAB +,,,Take Free,Gratis nehmen,Take Free,Obtener gratis,Obtener gratis,Prendre gratuitement,Prendi gratis,無料配布,무료 획득,Obter grátis,Obter grátis,Получить бесплатно,免费获取,免費領取,免费获取 +,,,Take Screenshot,Screenshot machen,Take Screenshot,Hacer captura de pantalla,Hacer captura de pantalla,Capture d'écran,Cattura screenshot,スクリーンショットを撮る,스크린숏 찍기,Captura de tela,Captura de tela,Сделать снимок экрана,截取屏幕快照,截圖,截取屏幕快照 +,,,Tap to Move,Zum Bewegen tippen,Tap to Move,Tocar para mover,Tocar para mover,Toucher pour se déplacer,Tocca per muovere,タップして移動,눌러서 이동,Toque para mover,Toque para mover,Передвижение по касанию,轻点以移动,輕觸移動,轻点以移动 +,,,Text,Text,Text,Texto,Texto,Texte,Testo,テキスト,텍스트,Texto,Texto,Текст,文字,文字,文字 +,,,Thanks for your report! Our moderators will evaluate the username.,Danke für deine Meldung! Unsere Moderatoren werden den Benutzernamen überprüfen.,Thanks for your report! Our moderators will evaluate the username.,Gracias por denunciarlo. Nuestros moderadores analizarán el nombre de usuario.,Gracias por denunciarlo. Nuestros moderadores analizarán el nombre de usuario.,Merci pour ce signalement ! Nos modérateurs vont examiner le nom de cet utilisateur.,Grazie della segnalazione! I moderatori esamineranno il nome utente.,ご報告ありがとうございます! モデレータがユーザーネームを確認します。,신고해주셔서 감사합니다! 검열팀이 사용자 이름을 검토할 거예요.,Obrigado por sua denúncia! Nossos moderadores avaliarão o nome de usuário.,Obrigado por sua denúncia! Nossos moderadores avaliarão o nome de usuário.,Спасибо за сообщение! Наши модераторы рассмотрят имя этого пользователя.,感谢你的举报!我们的审查员将对用户名进行评估。,感謝您的檢舉,我們的管理員將會審查該使用者名稱。,感谢你的举报!我们的审查员将对用户名进行评估。 +,,,Thanks for your report! Our moderators will review the chat logs and evaluate what happened.,"Danke für deine Meldung! Unsere Moderatoren werden sich die Chatprotokolle ansehen und überprüfen, was vorgefallen ist.",Thanks for your report! Our moderators will review the chat logs and evaluate what happened.,Gracias por denunciarlo. Nuestros moderadores comprobarán el registro del chat para analizar lo sucedido.,Gracias por denunciarlo. Nuestros moderadores comprobarán el registro del chat para analizar lo sucedido.,Merci pour ce signalement ! Nos modérateurs vont examiner le journal de chat et évaluer la situation.,Grazie della segnalazione! I moderatori controlleranno i registri della chat ed esamineranno l'accaduto.,ご報告ありがとうございます! モデレータがチャットログから状況を確認します。,신고해주셔서 감사합니다! 검열팀이 채팅 기록을 검토한 후 사건을 조사할 거예요.,Obrigado por sua denúncia! Nossos moderadores revisarão o histórico de chat e avaliarão o ocorrido.,Obrigado por sua denúncia! Nossos moderadores revisarão o histórico de chat e avaliarão o ocorrido.,"Спасибо за сообщение! Наши модераторы просмотрят журнал чата, чтобы оценить ситуацию.",感谢你的举报!我们的审查员将审阅聊天记录并进行评估。,感謝您的檢舉,我們的管理員將會審查聊天紀錄。,感谢你的举报!我们的审查员将审阅聊天记录并进行评估。 +,,,Thanks for your report! Our moderators will review the place and make a determination.,Danke für deine Meldung! Unsere Moderatoren werden sich den Ort ansehen und eine Entscheidung treffen.,Thanks for your report! Our moderators will review the place and make a determination.,Gracias por denunciarlo. Nuestros moderadores analizarán el juego y tomarán una decisión.,Gracias por denunciarlo. Nuestros moderadores analizarán el juego y tomarán una decisión.,Merci pour ce signalement ! Nos modérateurs vont examiner le jeu avant de prendre une décision.,Grazie della segnalazione! I moderatori esamineranno il gioco e prenderanno una decisione.,ご報告ありがとうございます! モデレータがプレースを確認のうえ判断いたします。,신고해주셔서 감사합니다! 검열팀이 장소를 검토하고 결정을 내릴 거예요.,Obrigado por sua denúncia! Nossos moderadores verificarão o jogo e irão tomar uma decisão.,Obrigado por sua denúncia! Nossos moderadores verificarão o jogo e irão tomar uma decisão.,Спасибо за сообщение! Наши модераторы изучат это место и примут решение.,感谢你的举报!我们的审查员将查看该地点并进行评估。,感謝您的檢舉,我們的管理員將會審查該地點。,感谢你的举报!我们的审查员将查看该地点并进行评估。 +,,,Thanks for your report! We've recorded your report for evaluation.,Danke für deine Meldung! Sie wurde zur Auswertung gespeichert.,Thanks for your report! We've recorded your report for evaluation.,Gracias por denunciarlo. Hemos guardado la denuncia para analizar lo sucedido.,Gracias por denunciarlo. Hemos guardado la denuncia para analizar lo sucedido.,Merci pour ce signalement ! Nous l'avons bien pris en compte et allons l'examiner.,Grazie della segnalazione! La tua segnalazione è stata registrata per essere esaminata.,ご報告ありがとうございます! レポートは評価のため記録されました。,신고해주셔서 감사합니다! 검토를 위해 회원님의 신고를 저장했어요.,Obrigado por sua denúncia! Registramos a sua denúncia para que seja avaliada.,Obrigado por sua denúncia! Registramos a sua denúncia para que seja avaliada.,Спасибо за сообщение! Оно будет рассмотрено.,感谢你的举报!我们已记录你的举报信息以进行评估。,感謝您的檢舉,我們的管理員將會盡速審查。,感谢你的举报!我们已记录你的举报信息以进行评估。 +,,,This game has been disconnected because you have joined a game from another device,"Dieses Spiel wurde unterbrochen, weil du einem Spiel von einem anderen Gerät beigetreten bist",This game has been disconnected because you have joined a game from another device,Se ha desconectado este juego porque te has unido a un juego desde otro dispositivo,Se ha desconectado este juego porque te has unido a un juego desde otro dispositivo,"Vous avez été déconnecté(e) de ce jeu, car vous en avez rejoint un autre depuis un appareil différent.",Questo gioco è stato disconnesso perché hai giocato una partita con un altro dispositivo,別のデバイスからゲームに参加したため、このゲームへの接続が切断されました,다른 기기에서 게임에 참여하여 본 게임의 연결이 끊어졌어요.,Este jogo foi desconectado pois você entrou em um jogo através de outro dispositivo,Este jogo foi desconectado pois você entrou em um jogo através de outro dispositivo,"Соединение с этой игрой было прервано, так как вы стали играть с другого устройства.",由于你从另一设备加入游戏,此游戏已断开连接,連線中斷,您已從其它狀置加入遊戲,由于你从另一设备加入游戏,此游戏已断开连接 +,,,This game has been disconnected because you have joined a game from another device. {RET:translate}...({COUNT:int}),"Dieses Spiel wurde unterbrochen, weil du einem Spiel von einem anderen Gerät beigetreten bist. {RET:translate}...({COUNT:int})",This game has been disconnected because you have joined a game from another device. {RET:translate}...({COUNT:int}),Se ha desconectado este juego porque te has unido a un juego desde otro dispositivo. {RET:translate}...({COUNT:int}),Se ha desconectado este juego porque te has unido a un juego desde otro dispositivo. {RET:translate}...({COUNT:int}),"Vous avez été déconnecté(e) de ce jeu, car vous en avez rejoint un autre depuis un appareil différent.. {RET:translate}...({COUNT:int})",Questo gioco è stato disconnesso perché hai giocato una partita con un altro dispositivo. {RET:translate}...({COUNT:int}),別のデバイスからゲームに参加したため、このゲームへの接続が切断されました. {RET:translate}...({COUNT:int}),다른 기기에서 게임에 참여하여 본 게임의 연결이 끊어졌어요.. {RET:translate}...({COUNT:int}),Este jogo foi desconectado pois você entrou em um jogo através de outro dispositivo. {RET:translate}...({COUNT:int}),Este jogo foi desconectado pois você entrou em um jogo através de outro dispositivo. {RET:translate}...({COUNT:int}),"Соединение с этой игрой было прервано, так как вы стали играть с другого устройства.. {RET:translate}...({COUNT:int})",由于你从另一设备加入游戏,此游戏已断开连接. {RET:translate}...({COUNT:int}),連線中斷,您已從其它狀置加入遊戲。{RET:translate}…({COUNT:int}),由于你从另一设备加入游戏,此游戏已断开连接. {RET:translate}...({COUNT:int}) +,,,This game has ended,Dieses Spiel ist beendet,This game has ended,Este juego ha finalizado,Este juego ha finalizado,Cette partie est terminée.,Questo gioco è stato chiuso,このゲームは終了しました,이 게임은 종료되었어요.,Este jogo acabou,Este jogo acabou,Эта игра закончена,此游戏已结束,此遊戲已結束,此游戏已结束 +,,,This game has ended. ({COUNT:int}),Dieses Spiel ist beendet. ({COUNT:int}),This game has ended. ({COUNT:int}),Este juego ha finalizado. ({COUNT:int}),Este juego ha finalizado. ({COUNT:int}),Cette partie est terminée.. ({COUNT:int}),Questo gioco è stato chiuso. ({COUNT:int}),このゲームは終了しました。 ({COUNT:int}),이 게임은 종료되었어요. ({COUNT:int}),Este jogo acabou ({COUNT:int}),Este jogo acabou ({COUNT:int}),Эта игра закончена ({COUNT:int}),此游戏已结束 ({COUNT:int}),此遊戲已結束 ({COUNT:int}),此游戏已结束 ({COUNT:int}) +,,,This game has ended. {RET:translate}...({COUNT:int}),Dieses Spiel ist beendet. {RET:translate}...({COUNT:int}),This game has ended. {RET:translate}...({COUNT:int}),Este juego ha finalizado. {RET:translate}...({COUNT:int}),Este juego ha finalizado. {RET:translate}...({COUNT:int}),Cette partie est terminée.. {RET:translate}...({COUNT:int}),Questo gioco è stato chiuso. {RET:translate}...({COUNT:int}),このゲームは終了しました。 {RET:translate}...({COUNT:int}),이 게임은 종료되었어요.. {RET:translate}...({COUNT:int}),Este jogo acabou. {RET:translate}...({COUNT:int}),Este jogo acabou. {RET:translate}...({COUNT:int}),Эта игра закончена. {RET:translate}...({COUNT:int}),此游戏已结束. {RET:translate}...({COUNT:int}),此遊戲已結束。{RET:translate}…({COUNT:int}),此游戏已结束. {RET:translate}...({COUNT:int}) +,,,This game has shut down,Dieses Spiel wurde abgebrochen.,This game has shut down,Este juego ha sido cerrado.,Este juego ha sido cerrado.,Ce jeu a dû fermer.,Questo gioco è stato chiuso,このゲームはシャットダウン中です,게임이 종료되었습니다.,Este jogo foi encerrado,Este jogo foi encerrado,Эта игра больше не доступна.,此游戏已关闭,此遊戲已關閉,此游戏已关闭 +,,,This game is disabled,Dieses Spiel wurde deaktiviert,This game is disabled,Este juego está desactivado,Este juego está desactivado,Ce jeu est désactivé.,Questo gioco è stato disattivato,このゲームは無効です,비활성화된 게임입니다.,Este jogo está desabilitado,Este jogo está desabilitado,Эта игра отключена,游戏已禁用,此遊戲已停用,游戏已禁用 +,,,This game is disabled. ({COUNT:int}),Dieses Spiel wurde deaktiviert. ({COUNT:int}),This game is disabled. ({COUNT:int}),Este juego está desactivado. ({COUNT:int}),Este juego está desactivado. ({COUNT:int}),Ce jeu est désactivé.. ({COUNT:int}),Questo gioco è stato disattivato. ({COUNT:int}),このゲームは無効です。 ({COUNT:int}),비활성화된 게임입니다. ({COUNT:int}),Este jogo está desabilitado ({COUNT:int}),Este jogo está desabilitado ({COUNT:int}),Эта игра отключена ({COUNT:int}),游戏已禁用 ({COUNT:int}),此遊戲已停用。({COUNT:int}),游戏已禁用 ({COUNT:int}) +,,,This game is disabled. {RET:translate}...({COUNT:int}),Dieses Spiel wurde deaktiviert. {RET:translate}...({COUNT:int}),This game is disabled. {RET:translate}...({COUNT:int}),Este juego está desactivado. {RET:translate}...({COUNT:int}),Este juego está desactivado. {RET:translate}...({COUNT:int}),Ce jeu est désactivé.. {RET:translate}...({COUNT:int}),Questo gioco è stato disattivato. {RET:translate}...({COUNT:int}),このゲームは無効です。 {RET:translate}...({COUNT:int}),비활성화된 게임입니다. {RET:translate}...({COUNT:int}),Este jogo está desabilitado. {RET:translate}...({COUNT:int}),Este jogo está desabilitado. {RET:translate}...({COUNT:int}),Эта игра отключена. {RET:translate}...({COUNT:int}),游戏已禁用. {RET:translate}...({COUNT:int}),此遊戲已停用。{RET:translate}…({COUNT:int}),游戏已禁用. {RET:translate}...({COUNT:int}) +,,,This game is not available. Please try another,Dieses Spiel ist nicht verfügbar. Bitte probiere ein anderes.,This game is not available. Please try another,Este juego no está disponible. Inténtalo con otro.,Este juego no está disponible. Inténtalo con otro.,Ce jeu n'est pas disponible. Veuillez en essayer un autre,Questo gioco non è disponibile. Provane un altro,このゲームは利用できません。他を試してください。,이용할 수 없는 게임입니다. 다른 게임을 시도하세요,Este jogo não está disponível. Tente outro,Este jogo não está disponível. Tente outro,Эта игра недоступна. Попробуйте другую,此游戏不可用。请试试另一个,此遊戲無法遊玩,請嘗試其它遊戲,此游戏不可用。请试试另一个 +,,,This game is restricted,Dieses Spiel ist begrenzt,This game is restricted,Este juego está restringido,Este juego está restringido,Ce jeu est en accès limité.,Questa partita è riservata,このゲームは制限されています,제한된 게임입니다.,Este jogo é restrito,Este jogo é restrito,Эта игра запрещена,此游戏有限制,此遊戲已受限,此游戏有限制 +,,,This game is restricted. ({COUNT:int}),Dieses Spiel ist begrenzt. ({COUNT:int}),This game is restricted. ({COUNT:int}),Este juego está restringido. ({COUNT:int}),Este juego está restringido. ({COUNT:int}),Ce jeu est en accès limité.. ({COUNT:int}),Questa partita è riservata. ({COUNT:int}),このゲームは制限されています。 ({COUNT:int}),제한된 게임입니다. ({COUNT:int}),Este jogo é restrito ({COUNT:int}),Este jogo é restrito ({COUNT:int}),Эта игра запрещена ({COUNT:int}),此游戏有限制 ({COUNT:int}),此遊戲已受限。({COUNT:int}),此游戏有限制 ({COUNT:int}) +,,,This game is restricted. {RET:translate}...({COUNT:int}),Dieses Spiel ist begrenzt. {RET:translate}...({COUNT:int}),This game is restricted. {RET:translate}...({COUNT:int}),Este juego está restringido. {RET:translate}...({COUNT:int}),Este juego está restringido. {RET:translate}...({COUNT:int}),Ce jeu est en accès limité.. {RET:translate}...({COUNT:int}),Questa partita è riservata. {RET:translate}...({COUNT:int}),このゲームは制限されています。 {RET:translate}...({COUNT:int}),제한된 게임입니다. {RET:translate}...({COUNT:int}),Este jogo é restrito. {RET:translate}...({COUNT:int}),Este jogo é restrito. {RET:translate}...({COUNT:int}),Эта игра запрещена. {RET:translate}...({COUNT:int}),此游戏有限制. {RET:translate}...({COUNT:int}),此遊戲已受限。{RET:translate}…({COUNT:int}),此游戏有限制. {RET:translate}...({COUNT:int}) +,,,This is a popup,Dies ist ein Pop-up.,This is a popup,Esta es una ventana emergente.,Esta es una ventana emergente.,Ceci est une fenêtre popup,Questo è un popup,これはポップアップです,팝업 메뉴입니다,Este é um popup,Este é um popup,Всплывающее сообщение,这是弹出式窗口,此為彈出視窗,这是弹出式窗口 +,,,This is a test purchase; your account will not be charged.,Dies ist ein Testkauf. Dein Konto wird dadurch nicht belastet.,This is a test purchase; your account will not be charged.,Esta compra es una prueba; no se llevará a cabo ningún cobro en tu cuenta.,Esta compra es una prueba; no se llevará a cabo ningún cobro en tu cuenta.,Ceci est un achat test ; votre compte ne sera pas débité.,Questo è un acquisto di prova; il tuo account non subirà addebiti.,これはテスト購入です。アカウントはチャージされません。,테스트 구매입니다. 계정에 비용이 청구되지 않아요.,Esta é uma compra de teste. Nada será cobrado da sua conta.,Esta é uma compra de teste. Nada será cobrado da sua conta.,Это тестовая покупка.Средства с вашего счета списаны не будут.,此次购买为测试性质;你的帐户将不会被扣款。,此為測試性購買,您的帳號餘額將維持不變。,此次购买为测试性质;你的帐户将不会被扣款。 +,,,This item cost more ROBUX than you can purchase. Please visit www.roblox.com to purchase more ROBUX.,"Dieses Item kostet mehr ROBUX, als du kaufen kannst. Bitte besuche www.roblox.com, um mehr ROBUX zu kaufen.",This item cost more ROBUX than you can purchase. Please visit www.roblox.com to purchase more ROBUX.,Este objeto cuesta más ROBUX de los que puedes comprar. Visita www.roblox.com para comprar más ROBUX.,Este objeto cuesta más ROBUX de los que puedes comprar. Visita www.roblox.com para comprar más ROBUX.,Cet objet coûte trop de ROBUX pour que vous l'achetiez. Veuillez vous rendre sur www.roblox.com pour acheter plus de ROBUX.,Questo oggetto costa più ROBUX di quanti tu possa acquistarne. Visita il sito www.roblox.com per acquistare altri ROBUX.,このアイテムを買うにはお持ちのROBUXでは足りません。www.roblox.com にアクセスしてROBUXを追加購入してください。,ROBUX가 부족해 본 아이템을 구매할 수 없습니다. ROBUX를 구매하려면 www.roblox.com을 방문하세요.,Este item custa mais ROBUX do que você pode comprar. Visite www.roblox.com para comprar mais ROBUX.,Este item custa mais ROBUX do que você pode comprar. Visite www.roblox.com para comprar mais ROBUX.,"Цена товара превышает количество ROBUX, которое вы можете купить. Перейдите на страницу www.roblox.com, чтобы приобрести больше ROBUX.",此物品的价格超过了你拥有的 ROBUX。请前往 www.roblox.com 购买更多 ROBUX。,您的 Robux 不足,無法購買此道具。請前往 www.roblox.com 加購 Robux。,此物品的价格超过了你拥有的 ROBUX。请前往 www.roblox.com 购买更多 ROBUX。 +,,,This item cost more ROBUX than you have available. Please leave this game and go to the ROBUX screen to purchase more.,"Dieses Item kostet mehr ROBUX, als dir zur Verfügung stehen. Bitte verlasse dieses Spiel, gehe zum ROBUX-Menü und kaufe dort mehr.",This item cost more ROBUX than you have available. Please leave this game and go to the ROBUX screen to purchase more.,Este objeto cuesta más ROBUX de los que tienes disponibles. Sal de este juego y ve a la pantalla de ROBUX para comprar más.,Este objeto cuesta más ROBUX de los que tienes disponibles. Sal de este juego y ve a la pantalla de ROBUX para comprar más.,Cet objet coûte plus de ROBUX que vous n'en avez. Veuillez quitter le jeu et aller à l'écran ROBUX pour en acheter plus.,Questo oggetto costa più ROBUX di quanti tu ne abbia. Esci dal gioco e vai nella schermata dei ROBUX per acquistarne altri.,このアイテムを買うにはお持ちのROBUXでは足りません。もっと購入するにはゲームを終了してROBUX画面に行ってください。,ROBUX가 부족해 본 아이템을 구매할 수 없습니다. ROBUX를 구매하려면 게임을 종료 후 ROBUX 화면으로 이동하세요.,Este item custa mais ROBUX do que você possui disponível. Saia do jogo e vá para a tela de ROBUX para comprar mais.,Este item custa mais ROBUX do que você possui disponível. Saia do jogo e vá para a tela de ROBUX para comprar mais.,"Цена товара превышает имеющееся у вас количество ROBUX. Выйдите из этой игры и перейдите в раздел ROBUX, чтобы купить больше.",此物品的价格超过了你拥有的 ROBUX。请离开此游戏,前往 ROBUX 屏幕以购买更多。,您的 Robux 不足,無法購買此道具。請離開此遊戲,前往 Robux 畫面加購 Robux。,此物品的价格超过了你拥有的 ROBUX。请离开此游戏,前往 ROBUX 屏幕以购买更多。 +,,,This was a test purchase.,Dies war ein Testkauf.,This was a test purchase.,Esta compra ha sido una prueba.,Esta compra ha sido una prueba.,C'était un achat test.,Questo era un acquisto di prova.,テスト購入でした。,테스트 구매입니다.,Esta foi uma compra de teste.,Esta foi uma compra de teste.,Это была тестовая покупка.,此次购买为测试性质。,此為測試性購買。,此次购买为测试性质。 +,,,Thumbpad,Daumenpad,Thumbpad,Cruceta,Cruceta,Pavé directionnel,Croce direzionale,サムパッド,엄지패드,Direcional,Direcional,Кнопочная панель,拇指垫,虛擬搖桿,拇指垫 +,,,Thumbstick,Thumbstick,Thumbstick,Stick,Stick,Joystick,Levetta,サムスティック,엄지스틱,Direcional analógico,Direcional analógico,Аналоговый стик,摇杆,類比搖桿,摇杆 +,,,To {RBX_NAME},An {RBX_NAME},To {RBX_NAME},Para {RBX_NAME},Para {RBX_NAME},À {RBX_NAME},A {RBX_NAME},宛先: {RBX_NAME},수신: {RBX_NAME},Para {RBX_NAME},Para {RBX_NAME},Получатель: {RBX_NAME},至“{RBX_NAME}”,給 {RBX_NAME},至“{RBX_NAME}” +,,,Torso,Oberkörper,Torso,Torso,Torso,Torse,Busto,胴体,몸통,Tronco,Tronco,Корпус,身体主干,軀幹,身体主干 +,,,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club,Потрясающий клуб создателей,Turbo Builders Club,Turbo Builders Club,Turbo Builders Club +,,,Type Of Abuse,Art des Verstoßes,Type Of Abuse,Tipo de abuso,Tipo de abuso,Type d'abus,Tipo di abuso,規約違反の種類,욕설 유형,Tipo de abuso,Tipo de abuso,Тип нарушения,滥用类型,濫用類型,滥用类型 +,,,Unblock Player,Spieler nicht mehr sperren,Unblock Player,Desbloquear jugador,Desbloquear jugador,Débloquer joueur,Sblocca giocatore,プレイヤーのブロックを解除,플레이어 차단 해제,Desbloq. jogador,Desbloq. jogador,Разблокировать игрока,取消屏蔽玩家,解除封鎖玩家,取消屏蔽玩家 +,,,Unequip Tools,Werkzeug-Ausrüstung abnehmen,Unequip Tools,Desequipar herramientas,Desequipar herramientas,Outils non équipés,Rimuovi strum.,ツールを外す,도구 장착 해제,Desequipar,Desequipar,Убрать,取消配备工具,卸下工具,取消配备工具 +,,,Unfollow Player,Spieler nicht mehr folgen,Unfollow Player,Dejar de seguir a jugador,Dejar de seguir a jugador,Ne plus suivre joueur,Non seguire giocatore,プレイヤーをフォローしない,플레이어 팔로우 취소,Parar de seguir jogador,Parar de seguir jogador,Отписаться от игрока,取消关注玩家,取消追蹤玩家,取消关注玩家 +,,,Unfriend Player,Spieler von der Freundesliste entfernen,Unfriend Player,Cancelar amistad con jugador,Cancelar amistad con jugador,Ne plus être ami du joueur,Togli amicizia a giocatore,プレイヤーを友達解除,플레이어 친구 끊기,Cancelar amizade,Cancelar amizade,Удалить из друзей,与玩家解除好友关系,刪除好友,与玩家解除好友关系 +,,,Upgrade,Aufwerten,Upgrade,Mejorar,Mejorar,Améliorer,Migliora,アップグレード,업그레이드,Melhorar,Melhorar,Улучшение,升级,升級,升级 +,,,Upload to YouTube,Auf YouTube hochladen,Upload to YouTube,Cargar a YouTube,Cargar a YouTube,Télécharger sur YouTube,Carica su YouTube,YouTubeにアップロード,YouTube에 업로드,Enviar para o YouTube,Enviar para o YouTube,Загрузить на YouTube,上传至 Youtube,上傳到 YouTube,上传至 Youtube +,,,Use Tool,Werkzeug verwenden,Use Tool,Utilizar herramienta,Utilizar herramienta,Utiliser outil,Usa strumento,ツールを使用,도구 사용,Usar,Usar,Использовать предмет,使用工具,使用工具,使用工具 +,,,Version not compatible with server. Please uninstall and try again.,Version nicht mit Server kompatibel. Bitte deinstallieren und erneut versuchen.,Version not compatible with server. Please uninstall and try again.,"Esta versión no es compatible con el servidor. Por favor, desinstálala y vuelve a intentarlo.","Esta versión no es compatible con el servidor. Por favor, desinstálala y vuelve a intentarlo.",Version incompatible avec le serveur. Veuillez désinstaller et réessayer.,Versione non compatibile con il server. Disinstalla e riprova.,サーバと互換性のないバージョンです。アンインストールしてリトライしてください。,서버와 호환되지 않는 버전입니다. 프로그램 제거 후 다시 시도하세요.,Versão incompatível com servidor. Desinstale e tente novamente.,Versão incompatível com servidor. Desinstale e tente novamente.,Версия приложения несовместима с сервером. Удалите приложение и повторите попытку.,版本与服务器不匹配。请解除安装并重试。,版本與伺服器不相容,請解除安裝後重新嘗試。,版本与服务器不匹配。请解除安装并重试。 +,,,Video,Video,Video,Vídeo,Vídeo,Vidéo,Video,ビデオ,비디오,Vídeo,Vídeo,Видео,视频,影片,视频 +,,,Video Recorded,Video aufgezeichnet,Video Recorded,Vídeo grabado,Vídeo grabado,Vidéo enregistrée,Video registrato,録画したビデオ,녹화 비디오 완료,Vídeo gravado,Vídeo gravado,Видео записано,视频已录制,已錄製影片,视频已录制 +,,,Video Settings,Video-Einstellungen,Video Settings,Configuración de vídeo,Configuración de vídeo,Paramètres vidéo,Impostazioni video,ビデオ設定,비디오 설정,Configurações de vídeo,Configurações de vídeo,Настройки видео,视频设置,影片設定,视频设置 +,,,Volume,Lautstärke,Volume,Volumen,Volumen,Volume,Volume,ボリューム,볼륨,Volume,Volume,Громкость,音量,音量,音量 +,,,W/Up Arrow,W/Aufwärtspfeil,W/Up Arrow,W/Cursor arriba,W/Cursor arriba,W/Flèche haut,W/Freccia SU,W/上カーソル,W/위쪽 화살표,W/seta para cima,W/seta para cima,W/стрелка вверх,W/上箭头,W、↑,W/上箭头 +,,,Waist Accessory,Taillen-Accessoire,Waist Accessory,Accesorio para la cintura,Accesorio para la cintura,Accessoire de taille,Accessorio vita,ウエストアクセサリ,허리 장신구,Acessório de cintura,Acessório de cintura,Аксессуар для талии,腰部配饰,腰部飾品,腰部配饰 +,,,Waiting for an available server. ({COUNT:int}),Warten auf verfügbaren Server. ({COUNT:int}),Waiting for an available server. ({COUNT:int}),Esperando un servidor disponible. ({COUNT:int}),Esperando un servidor disponible. ({COUNT:int}),En attente d'un serveur disponible. ({COUNT:int}),In attesa di un server disponibile. ({COUNT:int}),使用可能なサーバを待機中 ({COUNT:int}),이용 가능한 서버를 기다리는 중 ({COUNT:int}),Aguardando um servidor disponível ({COUNT:int}),Aguardando um servidor disponível ({COUNT:int}),Ожидание доступного сервера ({COUNT:int}),正在等待可用服务器 ({COUNT:int}),正在等待可用伺服器。({COUNT:int}),正在等待可用服务器 ({COUNT:int}) +,,,Waiting for an available server. {RET:translate}...({COUNT:int}),Warten auf verfügbaren Server. {RET:translate}...({COUNT:int}),Waiting for an available server. {RET:translate}...({COUNT:int}),Esperando un servidor disponible. {RET:translate}...({COUNT:int}),Esperando un servidor disponible. {RET:translate}...({COUNT:int}),En attente d'un serveur disponible. {RET:translate}...({COUNT:int}),In attesa di un server disponibile. {RET:translate}...({COUNT:int}),使用可能なサーバを待機中. {RET:translate}...({COUNT:int}),이용 가능한 서버를 기다리는 중. {RET:translate}...({COUNT:int}),Aguardando um servidor disponível. {RET:translate}...({COUNT:int}),Aguardando um servidor disponível. {RET:translate}...({COUNT:int}),Ожидание доступного сервера. {RET:translate}...({COUNT:int}),正在等待可用服务器. {RET:translate}...({COUNT:int}),正在等待可用伺服器。{RET:translate}…({COUNT:int}),正在等待可用服务器. {RET:translate}...({COUNT:int}) +,,,Walk Animation,Gehanimation,Walk Animation,Animación de marcha,Animación de marcha,Animation de marche,Animazione camminata,歩くアニメーション,걷기 애니메이션,Animação de caminhada,Animação de caminhada,Анимация ходьбы,行走动画,步行動畫,行走动画 +,,,Which Player?,Welcher Spieler?,Which Player?,¿A qué jugador?,¿A qué jugador?,Quel joueur ?,Quale giocatore?,どのプレイヤーですか?,플레이어 선택,Qual jogador?,Qual jogador?,Какой игрок?,哪位玩家?,哪位玩家?,哪位玩家? +,,,Wipeouts,Auslöschungen,Wipeouts,Destrucciones,Destrucciones,Éliminations,Annientamenti,死亡回数,사망,Aniquilações,Aniquilações,Уничтожения,死亡数,死亡數,死亡数 +,,,Would you like to unblock {RBX_NAME}?,Möchtest du die Sperre von {RBX_NAME} aufheben?,Would you like to unblock {RBX_NAME}?,¿Quieres desbloquear a {RBX_NAME}?,¿Quieres desbloquear a {RBX_NAME}?,Souhaitez-vous lever le blocage de {RBX_NAME} ?,Vuoi sbloccare {RBX_NAME}?,{RBX_NAME} さんをブロック解除しますか?,{RBX_NAME}님을 차단 해제할까요?,Gostaria de desbloquear {RBX_NAME}?,Gostaria de desbloquear {RBX_NAME}?,Разблокировать пользователя {RBX_NAME}?,你想要解除对“{RBX_NAME}”的屏蔽吗?,確定解除封鎖 {RBX_NAME}?,你想要解除对“{RBX_NAME}”的屏蔽吗? +,,,You already own this item. Your account has not been charged.,Dieses Item besitzt du bereits. Dein Konto wurde nicht belastet.,You already own this item. Your account has not been charged.,Ya tienes este objeto. No se ha llevado a cabo ningún cobro en tu cuenta.,Ya tienes este objeto. No se ha llevado a cabo ningún cobro en tu cuenta.,Vous possédez déjà cet objet. Votre compte n'a pas été débité.,Possiedi già questo oggetto. Il tuo account non ha subito addebiti.,すでにこのアイテムを持っています。アカウントはチャージされていません。,이미 보유한 아이템입니다. 계정에 비용이 청구되지 않아요.,Você já possui este item. Nada foi cobrado da sua conta.,Você já possui este item. Nada foi cobrado da sua conta.,У вас уже есть этот предмет. Средства с вашего счета не списаны.,你已拥有这件物品。你的帐户未被扣款。,您已擁有此道具,您的帳號餘額維持不變。,你已拥有这件物品。你的帐户未被扣款。 +,,,You are,Du,You are,Estás,Estás,Vous,Sei,あなたは,현재,Você está,Você está,Вы,你是,您在,你是 +,,,You are already playing a game. Please shut down the other game and try again,Du spielst bereits ein Spiel. Bitte brich das andere Spiel ab und versuche es erneut.,You are already playing a game. Please shut down the other game and try again,Ya estás jugando a un juego. Cierra el otro juego e inténtalo de nuevo.,Ya estás jugando a un juego. Cierra el otro juego e inténtalo de nuevo.,"Vous êtes déjà dans un jeu. Veuillez le quitter, puis réessayez.",Stai già giocando a un gioco. Chiudi l'altro gioco e riprova,すでにゲームをプレイ中です。ゲームを終了してもう一度試してください,게임을 플레이 중에요. 다른 게임을 닫고 다시 시도하세요.,Você já está jogando um jogo. Saia do jogo atual e tente novamente.,Você já está jogando um jogo. Saia do jogo atual e tente novamente.,"У вас уже запущена игра. Пожалуйста, закройте ее и попробуйте еще раз.",你已经在游戏中。请关闭其他游戏并重试,您正在玩其它遊戲,請關閉該遊戲並重新嘗試,你已经在游戏中。请关闭其他游戏并重试 +,,,You are too far away to chat!,"Du bist zu weit entfernt, um zu chatten!",You are too far away to chat!,¡Estás demasiado lejos para chatear!,¡Estás demasiado lejos para chatear!,Vous êtes trop loin pour discuter !,Sei troppo lontano per chattare!,遠すぎてチャット出来ません!,너무 멀리 있어서 채팅할 수 없어요!,Você está longe demais para participar do chat!,Você está longe demais para participar do chat!,Вы слишком далеко и не можете общаться в чате!,距离太远,无法聊天!,距離太遠,無法聊天。,距离太远,无法聊天! +,,,You can not send a friend request because you are at the max friend limit.,"Du kannst keine Freundesanfrage senden, da du die max. Anzahl an Freunden erreicht hast.",You can not send a friend request because you are at the max friend limit.,No puedes enviar una solicitud de amistad porque has alcanzado el límite máximo de amigos.,No puedes enviar una solicitud de amistad porque has alcanzado el límite máximo de amigos.,Vous ne pouvez pas envoyer une demande d'ami car vous avez atteint votre limite maximale d'amis.,Non puoi inviare una richiesta di amicizia perché hai raggiunto il limite massimo di amici.,友達の数が上限に達したため友達リクエストが送れません。,최대 친구 목록이 초과되어 친구 요청을 보낼 수 없어요.,Você não pode enviar um pedido de amizade pois você alcançou o limite máximo de amigos.,Você não pode enviar um pedido de amizade pois você alcançou o limite máximo de amigos.,Вы не можете отправить запрос дружбы: количество ваших друзей достигло максимума.,由于你已达到好友数量上限,你无法发送好友邀请。,您的好友人數已達上限,無法傳送好友邀請。,由于你已达到好友数量上限,你无法发送好友邀请。 +,,,"You have been disconnection from Team Create, please reconnect",Verbindung zu Team Create wurde unterbrochen. Bitte erneut verbinden.,"You have been disconnection from Team Create, please reconnect",Te has desconectado de Creación en equipo. Conéctate de nuevo.,Te has desconectado de Creación en equipo. Conéctate de nuevo.,Vous avez été déconnecté(e) de la création d'équipe. Veuillez vous reconnecter.,Sei stato disconnesso dalla creazione della squadra; è necessario riconnettersi,チームクリエイトとの接続が切断されました。再接続してください,팀 만들기 연결이 끊어졌어요. 다시 연결하세요.,Você foi desconectado do Team Create. Conecte-se novamente.,Você foi desconectado do Team Create. Conecte-se novamente.,"Соединение с командой создателей было прервано, пожалуйста, установите соединение повторно.",你已断开与团队开发的连接,请重新连接,與集體創作中斷連線,請重新連線,你已断开与团队开发的连接,请重新连接 +,,,You have been kicked from the game,Du wurdest aus dem Spiel gekickt,You have been kicked from the game,Se te ha expulsado del juego,Se te ha expulsado del juego,Vous avez été expulsé(e) du jeu.,Sei stato espulso dal gioco,ゲームから外されました,게임에서 강제 퇴장되었습니다.,Você foi expulso do jogo,Você foi expulso do jogo,Вас исключили из игры.,你已被游戏踢出,您已被遊戲踢出,你已被游戏踢出 +,,,You have lost the connection to the game,Die Verbindung zum Spiel wurde unterbrochen.,You have lost the connection to the game,Has perdido la conexión con el juego.,Has perdido la conexión con el juego.,Votre connexion au jeu a été perdue.,Sei stato disconnesso dal gioco,ゲームへの接続が失われました。,게임 연결이 끊어졌어요,Você perdeu a conexão com o jogo,Você perdeu a conexão com o jogo,Прервано подключение к игре.,你已中断与游戏的连接,與遊戲的連線中斷,你已中断与游戏的连接 +,,,You have no notifications,Du hast keine Benachrichtigungen.,You have no notifications,No tienes notificaciones,No tienes notificaciones,Vous n'avez aucune notification,Non hai notifiche,お知らせはありません,알림이 없어요,Você não possui notificações,Você não possui notificações,Уведомлений нет,你没有通知,您沒有通知,你没有通知 +,,,You must wait {RBX_NUMBER} second before sending another message!,"Du musst {RBX_NUMBER} Sekunde lang warten, bevor du eine weitere Nachricht senden kannst!",You must wait {RBX_NUMBER} second before sending another message!,¡Debes esperar {RBX_NUMBER} segundo antes de enviar otro mensaje!,¡Debes esperar {RBX_NUMBER} segundo antes de enviar otro mensaje!,Vous devez attendre {RBX_NUMBER} seconde avant d'envoyer un autre message !,Devi aspettare {RBX_NUMBER} secondo prima di inviare un altro messaggio!,{RBX_NUMBER} 秒待ってから次のメッセージを送ってください!,추가 메시지를 보내기 전에 {RBX_NUMBER}초 동안 기다려야 해요!,Você precisa esperar {RBX_NUMBER} segundo antes de enviar outra mensagem!,Você precisa esperar {RBX_NUMBER} segundo antes de enviar outra mensagem!,Вы сможете отправить новое сообщение только через {RBX_NUMBER} сек.!,发送另一条消息前你必须等待 {RBX_NUMBER} 秒!,請在 {RBX_NUMBER} 秒後再傳送新的訊息。,发送另一条消息前你必须等待 {RBX_NUMBER} 秒! +,,,You need {RBX_NUMBER} more {RBX_NAME} to buy this item.,"Du benötigst noch {RBX_NUMBER} weitere {RBX_NAME}, um dieses Item zu kaufen.",You need {RBX_NUMBER} more {RBX_NAME} to buy this item.,Necesitas {RBX_NUMBER} {RBX_NAME} más para comprar este objeto.,Necesitas {RBX_NUMBER} {RBX_NAME} más para comprar este objeto.,Vous avez besoin de {RBX_NUMBER} {RBX_NAME} de plus pour acheter cet objet.,Hai bisogno di {RBX_NUMBER} {RBX_NAME} in più per comprare questo oggetto.,このアイテムを買うにはあと {RBX_NUMBER} の {RBX_NAME} が必要です。,본 아이템을 구매하려면 {RBX_NAME}이(가) {RBX_NUMBER}개 더 필요해요.,Você precisa de mais {RBX_NUMBER} {RBX_NAME} para comprar este item.,Você precisa de mais {RBX_NUMBER} {RBX_NAME} para comprar este item.,Для покупки этого товара требуется на {RBX_NUMBER} больше {RBX_NAME}.,你还需要 {RBX_NUMBER} 个 “{RBX_NAME}” 才能购买此物品。,您還需要 {RBX_NUMBER} {RBX_NAME} 才能購買此道具。,你还需要 {RBX_NUMBER} 个 “{RBX_NAME}” 才能购买此物品。 +,,,You were kicked from '{RBX_NAME}',Du wurdest aus „{RBX_NAME}“ gekickt.,You were kicked from '{RBX_NAME}',"Se te ha expulsado de ""{RBX_NAME}"".","Se te ha expulsado de ""{RBX_NAME}"".",Vous avez été expulsé de {RBX_NAME},"Sei stato rimosso da ""{RBX_NAME}""",あなたは 「{RBX_NAME}」から外されました。,'{RBX_NAME}'에서 강제 퇴장되었습니다.,Você foi expulso(a) de '{RBX_NAME}',Você foi expulso(a) de '{RBX_NAME}',Вы исключены из «{RBX_NAME}».,“{RBX_NAME}”已将你踢出,您已被踢出「{RBX_NAME}」,“{RBX_NAME}”已将你踢出 +,,,You were kicked from '{RBX_NAME}' for the following reason(s): {RBX_NAME},Du wurdest aus folgenden Gründen aus „{RBX_NAME}“ gekickt: {RBX_NAME},You were kicked from '{RBX_NAME}' for the following reason(s): {RBX_NAME},"Se te ha expulsado de ""{RBX_NAME}"" por el/los motivo/s siguiente/s: {RBX_NAME}","Se te ha expulsado de ""{RBX_NAME}"" por el/los motivo/s siguiente/s: {RBX_NAME}",Vous avez été expulsé de {RBX_NAME} pour la/les raison(s) suivante(s) : {RBX_NAME},"Sei stato rimosso da ""{RBX_NAME}"" per le seguenti motivazioni: {RBX_NAME}",あなたは 「{RBX_NAME}」から次の理由により外されました: {RBX_NAME},'{RBX_NAME}'에서 강제 퇴장되었습니다. 퇴장 이유: {RBX_NAME},Você foi expulso(a) de '{RBX_NAME}‘. Motivo(s): {RBX_NAME},Você foi expulso(a) de '{RBX_NAME}‘. Motivo(s): {RBX_NAME},Вы исключены из «{RBX_NAME}» по следующим причинам: {RBX_NAME},“{RBX_NAME}”已将你踢出,原因为:{RBX_NAME},您因下列原因被踢出「{RBX_NAME}」:{RBX_NAME},“{RBX_NAME}”已将你踢出,原因为:{RBX_NAME} +,,,YouTube Video,YouTube-Video,YouTube Video,Vídeo de YouTube,Vídeo de YouTube,Vidéo YouTube,Video su YouTube,YouTube ビデオ,YouTube 비디오,Vídeo do YouTube,Vídeo do YouTube,Видео YouTube,Youtube 视频,YouTube 影片,Youtube 视频 +,,,Your account balance will not be affected by this transaction.,Das Guthaben deines Kontos wird durch diese Transaktion nicht beeinflusst.,Your account balance will not be affected by this transaction.,Esta transacción no modificará el saldo de tu cuenta.,Esta transacción no modificará el saldo de tu cuenta.,Le solde de votre compte ne sera pas affecté par cette transaction.,I fondi del tuo account non verranno intaccati da questa transazione.,この取引であなたのアカウントは変化しません。,본 거래는 계정 잔액에 영향을 주지 않아요.,O saldo da sua conta não será afetado por esta transação.,O saldo da sua conta não será afetado por esta transação.,После этой операции средства с вашего счета списаны не будут.,你的帐户余额将不会收到此次交易的影响。,您的帳號餘額不受此交易影響。,你的帐户余额将不会收到此次交易的影响。 +,,,Your balance after this transaction will be R${RBX_NUMBER},Nach dieser Transaktion wird dein Guthaben {RBX_NUMBER} R$ betragen.,Your balance after this transaction will be R${RBX_NUMBER},Tu saldo después de esta transacción será de {RBX_NUMBER} R$.,Tu saldo después de esta transacción será de {RBX_NUMBER} R$.,Votre solde après cette transaction sera de {RBX_NUMBER} R$,"Dopo la transazione, avrai R${RBX_NUMBER}",この取引の後の残高は R${RBX_NUMBER}です,본 거래 후 예상 잔액은 R${RBX_NUMBER}입니다.,Seu saldo depois desta transação será de R$ {RBX_NUMBER},Seu saldo depois desta transação será de R$ {RBX_NUMBER},После этой операции ваш баланс составит R${RBX_NUMBER},此次交易后,你的余额将为 R${RBX_NUMBER}。,您在此交易後的餘額將為 R${RBX_NUMBER}。,此次交易后,你的余额将为 R${RBX_NUMBER}。 +,,,Your party is too large to fit,Deine Party ist zu groß,Your party is too large to fit,Tu equipo es demasiado grande,Tu equipo es demasiado grande,Votre groupe comporte trop de membres.,Il tuo gruppo è troppo grande,パーティーが大きすぎてフィットしません,파티원이 너무 많아 참가할 수 없어요,Sua equipe é muito grande,Sua equipe é muito grande,Ваша группа слишком велика,你的团队人数过多,无法加入,您的隊伍人數過多,無法加入,你的团队人数过多,无法加入 +,,,Your party is too large to fit. ({COUNT:int}),Deine Party ist zu groß. ({COUNT:int}),Your party is too large to fit. ({COUNT:int}),Tu equipo es demasiado grande. ({COUNT:int}),Tu equipo es demasiado grande. ({COUNT:int}),Votre groupe comporte trop de membres.. ({COUNT:int}),Il tuo gruppo è troppo grande. ({COUNT:int}),パーティーが大きすぎてフィットしません。 ({COUNT:int}),파티원이 너무 많아 참가할 수 없어요. ({COUNT:int}),Sua equipe é muito grande ({COUNT:int}),Sua equipe é muito grande ({COUNT:int}),Ваша группа слишком велика ({COUNT:int}),你的团队人数过多,无法加入 ({COUNT:int}),您的隊伍人數過多,無法加入。({COUNT:int}),你的团队人数过多,无法加入 ({COUNT:int}) +,,,Zoom In,Einzoomen,Zoom In,Acercar,Acercar,Zoom avant,Ingrandisci,ズームイン,확대,Aproximar zoom,Aproximar zoom,Приблизить,放大,拉近,放大 +,,,Zoom In/Out,Ein-/Auszoomen,Zoom In/Out,Acercar/alejar,Acercar/alejar,Zoom avant/arrière,Aumenta/Riduci,ズームイン/ズームアウト,확대/축소,Aprox./afastar,Aprox./afastar,Приблизить/отдалить,放大/缩小,拉近 / 拉遠,放大/缩小 +,,,Zoom Out,Auszoomen,Zoom Out,Alejar,Alejar,Zoom arrière,Riduci,ズームアウト,축소,Afastar zoom,Afastar zoom,Отдалить,缩小,拉遠,缩小 +,,,update me,aktualisiere mich,update me,actualízame,actualízame,tenez-moi au courant,aggiornami,自分をアップデート,업데이트 요청,atualize-me,atualize-me,обновите меня,向我发送更新信息,通知我,向我发送更新信息 +,,,"{RBX_NAME1} won {RBX_NAME2}'s ""{RBX_NAME3}"" award!",{RBX_NAME1} wurde von {RBX_NAME2} die Auszeichnung „{RBX_NAME3}“ verliehen!,"{RBX_NAME1} won {RBX_NAME2}'s ""{RBX_NAME3}"" award!","¡{RBX_NAME1} ha ganado el galardón ""{RBX_NAME3}"" de {RBX_NAME2}!","¡{RBX_NAME1} ha ganado el galardón ""{RBX_NAME3}"" de {RBX_NAME2}!",{RBX_NAME1} a gagné la récompense « {RBX_NAME3} » de {RBX_NAME2} !,"{RBX_NAME1} ha vinto il premio ""{RBX_NAME3}"" di {RBX_NAME2}!",{RBX_NAME1} {RBX_NAME2} の「{RBX_NAME3}」を受賞!,"{RBX_NAME1}이(가) {RBX_NAME3}의 ""{RBX_NAME2}"" 상을 획득했어요!","{RBX_NAME1} ganhou o prêmio ""{RBX_NAME3}“ de {RBX_NAME2}!","{RBX_NAME1} ganhou o prêmio ""{RBX_NAME3}“ de {RBX_NAME2}!",{RBX_NAME1} получает награду {RBX_NAME2} «{RBX_NAME3}»!,{RBX_NAME1}赢得了{RBX_NAME2}的“{RBX_NAME3}”奖!,{RBX_NAME1} 贏得 {RBX_NAME2} 的「{RBX_NAME3}」獎!,{RBX_NAME1}赢得了{RBX_NAME2}的“{RBX_NAME3}”奖! +,,,{RBX_NAME} was kicked,{RBX_NAME} wurde gekickt,{RBX_NAME} was kicked,Se ha expulsado a {RBX_NAME}.,Se ha expulsado a {RBX_NAME}.,{RBX_NAME} a été expulsé,{RBX_NAME} è stato rimosso,{RBX_NAME} は外されました。,{RBX_NAME}님이 강제 퇴장되었습니다.,{RBX_NAME} foi expulso(a),{RBX_NAME} foi expulso(a),Пользователь {RBX_NAME} исключен,“{RBX_NAME}”被踢出,{RBX_NAME} 已被踢出,“{RBX_NAME}”被踢出 +,,,{RBX_NAME} was kicked for the following reason(s): {RBX_NAME},{RBX_NAME} wurde aus folgenden Gründen gekickt: {RBX_NAME},{RBX_NAME} was kicked for the following reason(s): {RBX_NAME},Se ha expulsado a {RBX_NAME} por el/los motivo/s siguiente/s: {RBX_NAME},Se ha expulsado a {RBX_NAME} por el/los motivo/s siguiente/s: {RBX_NAME},{RBX_NAME} a été expulsé pour la/les raison(s) suivante(s) : {RBX_NAME},{RBX_NAME} è stato rimosso per le seguenti motivazioni: {RBX_NAME},{RBX_NAME} は次の理由により外されました: {RBX_NAME},{RBX_NAME}님이 강제 퇴장되었습니다. 퇴장 이유: {RBX_NAME},{RBX_NAME} foi expulso(a). Motivo(s): {RBX_NAME},{RBX_NAME} foi expulso(a). Motivo(s): {RBX_NAME},Пользователь {RBX_NAME} исключен по следующим причинам: {RBX_NAME},已将“{RBX_NAME}”踢出,原因为:{RBX_NAME},{RBX_NAME} 因下列原因被踢出:{RBX_NAME},已将“{RBX_NAME}”踢出,原因为:{RBX_NAME} +,,,{RBX_NAME} was muted for the following reason(s): {RBX_NAME},{RBX_NAME} wurde aus folgenden Gründen stummgeschaltet: {RBX_NAME},{RBX_NAME} was muted for the following reason(s): {RBX_NAME},Se ha silenciado a {RBX_NAME} por el/los motivo/s siguiente/s: {RBX_NAME},Se ha silenciado a {RBX_NAME} por el/los motivo/s siguiente/s: {RBX_NAME},{RBX_NAME} a été bâillonné pour la/les raison(s) suivante(s) : {RBX_NAME},{RBX_NAME} non ha più la parola per le seguenti motivazioni: {RBX_NAME},{RBX_NAME} は次の理由によりミュートされました: {RBX_NAME},{RBX_NAME}님이 음소거 되었어요. 음소거 이유: {RBX_NAME},{RBX_NAME} foi silenciado(a). Motivo(s): {RBX_NAME},{RBX_NAME} foi silenciado(a). Motivo(s): {RBX_NAME},Пользователь {RBX_NAME} добавлен в список игнорируемых по следующим причинам: {RBX_NAME},“{RBX_NAME}”被禁言的原因为:{RBX_NAME},{RBX_NAME} 因下列原因被靜音:{RBX_NAME},“{RBX_NAME}”被禁言的原因为:{RBX_NAME} +,,,Waiting for an available server,Warte auf verfügbaren Server,Waiting for an available server,Esperando un servidor disponible,Esperando un servidor disponible,En attente d'un serveur disponible,In attesa di un server disponibile,使用可能なサーバを待機中,이용 가능한 서버를 기다리는 중,Aguardando um servidor disponível,Aguardando um servidor disponível,Ожидание доступного сервера,正在等待可用服务器,等待可用伺服器,正在等待可用服务器 +,,,"Server found, loading...","Server gefunden, wird geladen ...","Server found, loading...",Servidor encontrado. Cargando...,Servidor encontrado. Cargando...,"Serveur trouvé, chargement en cours...","Server trovato, caricamento...",サーバが見つかりました。読み込み中です...,"서버를 찾았습니다, 불러오는 중...","Servidor encontrado, carregando...","Servidor encontrado, carregando...",Сервер найден. Загрузка...,找到服务器,正在加载...,找到伺服器,正在載入…,找到服务器,正在加载... +,,,Reset Character,Charakter zurücksetzen,Reset Character,Reiniciar personaje,Reiniciar personaje,Réinitialiser le personnage,Azzera personaggio,キャラクターをリセット,캐릭터 재설정,Reiniciar,Reiniciar,Сброс персонажа,重置人物,重置人偶,重置人物 +,,,Dynamic Thumbstick,Dynamischer Thumbstick,Dynamic Thumbstick,Pulsador dinámico,Pulsador dinámico,Joystick dynamique,Pattina dinamica,ダイナミックサムスティック,다이나믹 엄지스틱,Direcional analógico dinâmico,Direcional analógico dinâmico,Динамический джойстик,动态摇杆,動態類比搖桿,动态摇杆 +,,,Joining server,Verbindung zum Server wird hergestellt,Joining server,Uniéndose a un servidor,Uniéndose a un servidor,Connexion au serveur...,Connessione al server,サーバーに接続中,서버 입장 중,Entrando no servidor,Entrando no servidor,Подключение к серверу,正在加入服务器,加入伺服器,正在加入服务器 +,,,Sent,Gesendet,Sent,Enviado,Enviado,Envoyé,Inviato,送付済み,보냄,Enviado,Enviado,Отправленные,已发送,已傳送,已发送 +Network,,,(Network),(Netzwerk),(Network),(Red),(Red),(Réseau),(Rete),(ネットワーク),(네트워크),(Rede),(Rede),(Сеть),(网络),(網路),(网络) +GameChat_ChatCommandsTeller_SwitchChannelCommand,,,/c : switch channel menu tabs.,/c : Zum Wechseln zwischen Kanalmenüreitern.,/c : switch channel menu tabs.,/c : alternar pestañas del menú del chat.,/c : alternar pestañas del menú del chat.,/c  : échanger les onglets du menu Canal.,/c : cambia scheda nel menu dei canali.,/c <チャンネル> : チャンネルメニュータブを切り替える。,/c <채널> : 채널 메뉴 탭 전환.,/c : trocar abas de menu de canal.,/c : trocar abas de menu de canal.,/c <канал> : переключить вкладку в меню каналов.,/c <频道名称> : 切换频道菜单标签。,/c <頻道名稱>:切換頻道選單標籤。,/c <频道名称> : 切换频道菜单标签。 +GameChat_ChatCommandsTeller_MeCommand,,,/me : roleplaying command for doing actions.,/me : Rollenspielbefehl für Aktionen.,/me : roleplaying command for doing actions.,/me : comando de rol para realizar acciones.,/me : comando de rol para realizar acciones.,/me  : commande jeu de rôle pour accomplir des actions.,/me : comando per descrivere azioni e giocare di ruolo.,/me <テキスト> : アクションのためのロールプレイングコマンド。,/me <텍스트> : 작업 수행을 위한 역할 놀이 명령어.,/me : comando de RPG para realizar ações.,/me : comando de RPG para realizar ações.,/me <текст> : сообщить о выполнении какого-либо действия.,/me : 做动作的角色扮演指令。,/me <文字>:角色扮演動作的指令。,/me : 做动作的角色扮演指令。 +GameChat_ChatCommandsTeller_MuteCommand,,,/mute : mute a speaker.,/mute : Schaltet einen Sprecher stumm.,/mute : mute a speaker.,/mute : silenciar a un usuario.,/mute : silenciar a un usuario.,/mute  : bâillonne un interlocuteur.,/mute : togli la parola a un giocatore.,/mute <スピーカー> : 相手をミュート。,/mute <스피커> : 스피커 음소거.,/mute : silenciar uma pessoa.,/mute : silenciar uma pessoa.,/mute <пользователь> : игнорировать пользователя.,/mute <用户名称>:将用户禁言。,/mute <使用者名稱> : 將此使用者靜音。,/mute <用户名称>:将用户禁言。 +GameChat_ChatCommandsTeller_TeamCommand,,,/team or /t : send a team chat to players on your team.,/team oder /t : Sendet eine Teamnachricht an Spieler deines Teams.,/team or /t : send a team chat to players on your team.,/team o /t : enviar un mensaje de chat de equipo a los jugadores de tu equipo.,/team o /t : enviar un mensaje de chat de equipo a los jugadores de tu equipo.,/team ou /t  : envoyer un message aux joueurs de votre équipe.,/team o /t : invia un messaggio a tutti i giocatori della tua squadra.,/team <メッセージ> または /t <メッセージ> : 自分のチームメンバーにチームチャットを送る。,/team <메시지> 또는 /t <메시지> : 팀 내 플레이어에게 팀 채팅 전송.,/team ou /t : enviar um chat de equipe aos jogadores da sua equipe.,/team ou /t : enviar um chat de equipe aos jogadores da sua equipe.,/team <сообщение> или /t <сообщение> : отправить сообщение игрокам из вашей команды.,/team 或 /t : 向你团队的玩家发送团队聊天。,/team <訊息> 或 /t <訊息> : 傳送訊息給隊伍中的玩家。,/team 或 /t : 向你团队的玩家发送团队聊天。 +GameChat_ChatCommandsTeller_UnMuteCommand,,,/unmute : unmute a speaker.,/unmute : Hebt Stummschaltung eines Sprechers auf.,/unmute : unmute a speaker.,/unmute : cancelar silencio de un usuario.,/unmute : cancelar silencio de un usuario.,/unmute  : retire le bâillon d'un interlocuteur.,/unmute : restituisci la parola a un giocatore.,/unmute <スピーカー> : 相手のミュートを解除.,/unmute <스피커> : 스피커 음소거 해제.,/unmute : remover silêncio de uma pessoa.,/unmute : remover silêncio de uma pessoa.,/unmute <пользователь> : перестать игнорировать пользователя.,/unmute <用户名称> : 取消该用户禁言。,/unmute <使用者名稱> : 將此使用者解除靜音。,/unmute <用户名称> : 取消该用户禁言。 +GameChat_ChatCommandsTeller_WhisperCommand,,,/whisper or /w : open private message channel with speaker.,/whisper oder /w : Öffnet privaten Nachrichtenkanal mit Sprecher.,/whisper or /w : open private message channel with speaker.,/whisper o /w : abrir canal de mensajes privados con un usuario.,/whisper o /w : abrir canal de mensajes privados con un usuario.,/whisper ou /w  : ouvre un canal de discussion privé avec l'interlocuteur.,/whisper o /w : apri un canale privato con un giocatore.,/whisper <スピーカー> または /w <スピーカー> : プライベートメッセージチャンネルを開く,/whisper <스피커> 또는 /w <스피커> : 스피커 채널에서 비공개 메시지 열기.,/whisper ou /w : abrir um canal de mensagem privada com uma pessoa.,/whisper ou /w : abrir um canal de mensagem privada com uma pessoa.,/whisper <пользователь> или /w <пользователь> : открыть канал для личной переписки с пользователем.,/whisper <用户名称> 或 /w <用户名称> : 与此用户开启私人消息频道。,/whisper <使用者名稱> 或 /w <使用者名稱> : 與此使用者開啟私人訊息頻道。,/whisper <用户名称> 或 /w <用户名称> : 与此用户开启私人消息频道。 +Average,,,Average,Durchschnitt,Average,Promedio,Promedio,Moyen,Media,平均,평균,Média,Média,Среднее,平均,平均,平均 +GameChat_ChatServiceRunner_ChannelDoesNotExist,,,Channel {RBX_NAME} does not exist.,Kanal „{RBX_NAME}“ existiert nicht.,Channel {RBX_NAME} does not exist.,El canal {RBX_NAME} no existe.,El canal {RBX_NAME} no existe.,Le canal {RBX_NAME} n'existe pas.,Il canale {RBX_NAME} non esiste.,チャンネル {RBX_NAME} は存在しません。,{RBX_NAME} 채널이 없어요.,O canal {RBX_NAME} não existe.,O canal {RBX_NAME} não existe.,Канала «{RBX_NAME}» не существует.,频道“{RBX_NAME}”不存在。,頻道「{RBX_NAME}」不存在。,频道“{RBX_NAME}”不存在。 +GameChat_ChatCommandsTeller_AllChannelWelcomeMessage,,,Chat '/?' or '/help' for a list of chat commands.,"Gib „/?“ oder „/help“ im Chat ein, um eine Liste der Chatbefehle zu erhalten.",Chat '/?' or '/help' for a list of chat commands.,"Envía ""/?"" o ""/help"" para obtener la lista de comandos del chat.","Envía ""/?"" o ""/help"" para obtener la lista de comandos del chat.","Dans le chat, « /? » ou « /help » pour la liste des commandes du chat.","Scrivi ""/?"" o ""/help"" per avere l'elenco dei comandi della chat.",チャットで 「/?」 または 「/help」を入力するとチャットコマンドの一覧を表示します。,채팅창에 '/?' 또는 '/도움말'을 입력하면 채팅 명령어 목록을 볼 수 있어요.,Digite '/?' ou '/help' no chat para ver uma lista de comandos.,Digite '/?' ou '/help' no chat para ver uma lista de comandos.,"Введите «/?» или «/help», чтобы увидеть список команд чата.",Chat “/?”或 “/help” 可获取聊天指令清单。,輸入「/?」或「/help」取得聊天指令清單。,Chat “/?”或 “/help” 可获取聊天指令清单。 +GameChat_SwallowGuestChat_Message,,,Create a free account to get access to chat permissions!,"Erstelle ein kostenloses Konto, um Zugriff auf Chatberechtigungen zu erhalten!",Create a free account to get access to chat permissions!,¡Crea una cuenta gratuita para obtener los permisos de acceso al chat!,¡Crea una cuenta gratuita para obtener los permisos de acceso al chat!,Créez un compte gratuit pour accéder aux permissions de chat !,Crea un account gratuito per avere accesso ai permessi della chat!,フリーアカウントを作ってチャット権限にアクセス!,무료 계정을 생성해 채팅 권한을 이용하세요!,Crie uma conta grátis para ter acesso a permissões de chat!,Crie uma conta grátis para ter acesso a permissões de chat!,"Создайте бесплатную учетную запись, чтобы настроить права доступа в чате!",创建免费帐户以获取聊天权限!,若要使用聊天功能,請建立免費帳號。,创建免费帐户以获取聊天权限! +Current,,,Current,Aktuell,Current,Actual,Actual,Actuel,Attuale,現在,현재,Atual,Atual,Текущее,当前,目前,当前 +DisableVoiceKey,,,Disable Voice Chat,Sprach-Chat deaktivieren,Disable Voice Chat,Desactivar chat de voz,Desactivar chat de voz,Désactiver le chat vocal,Disattiva chat vocale,ボイスチャットを無効にする,음성 채팅 비활성화,Desativar conversa por chat,Desativar conversa por chat,Отключить голосовой чат,停用语音聊天,停用語音聊天,停用语音聊天 +EnableVoiceKey,,,Enable Voice Chat,Sprach-Chat aktivieren,Enable Voice Chat,Activar chat de voz,Activar chat de voz,Activer le chat vocal,Attiva chat vocale,ボイスチャットを有効にする,음성 채팅 활성화,Ativar conversa por chat,Ativar conversa por chat,Включить голосовой чат,启用语音聊天,啟用語音聊天,启用语音聊天 +key_142,,,New Friend {RBX_NAME},Neuer Freund {RBX_NAME},New Friend {RBX_NAME},Amigo nuevo {RBX_NAME},Amigo nuevo {RBX_NAME},Nouvel ami {RBX_NAME},Nuovo amico {RBX_NAME},新しい友達 {RBX_NAME} さん,새 친구 {RBX_NAME},Novo amigo {RBX_NAME},Novo amigo {RBX_NAME},Новый друг {RBX_NAME},新好友{RBX_NAME},新好友 {RBX_NAME},新好友{RBX_NAME} +KEY_DESCRIPTION_OPTIONAL,,,Optional,Optional,Optional,Opcional,Opcional,Optionnel,Opzionale,オプション,선택 항목,Opcional,Opcional,По усмотрению,可选,可選填,可选 +key_145,,,Points Awarded You received {RBX_NUMBER} points!,Punkte verliehen Du hast {RBX_NUMBER} Punkte erhalten!,Points Awarded You received {RBX_NUMBER} points!,Puntos concedidos ¡Has recibido {RBX_NUMBER} puntos!,Puntos concedidos ¡Has recibido {RBX_NUMBER} puntos!,Points accordés Vous avez reçu {RBX_NUMBER} points !,Punti attribuiti Hai ricevuto {RBX_NUMBER} punti!,ポイント授与 ポイントを {RBX_NUMBER}ゲットしました !,획득한 포인트 {RBX_NUMBER}포인트를 받았어요!,Pontos concedidos Você recebeu {RBX_NUMBER} pontos!,Pontos concedidos Você recebeu {RBX_NUMBER} pontos!,Получены очки Вы получили {RBX_NUMBER} очк.!,已获得点数 你得到了 {RBX_NUMBER} 点!,獲得點數:您得到 {RBX_NUMBER} 點!,已获得点数 你得到了 {RBX_NUMBER} 点! +Received,,,Received,Empfangen,Received,Recibido,Recibido,Reçus,Ricevuti,受信済み,받음,Recebido,Recebido,Принято,已收到,收到,已收到 +BACKPACK_SEARCH,,,Search,Suchen,Search,Buscar,Buscar,Rechercher,Cerca,検索,검색,Pesquisar,Pesquisar,Поиск,搜索,搜尋,搜索 +KEY_DESCRIPTION_SHORT_DECRIPTION_OPTIONAL,,,Short Description (Optional),Kurzbeschreibung (optional),Short Description (Optional),Descripción breve (opcional),Descripción breve (opcional),Description brève (facultatif),Descrizione breve (opzionale),簡単な説明(オプション),신고 내용 입력 (선택),Breve descrição (opcional),Breve descrição (opcional),Короткое описание (необязательно),短描述(可选),簡介(可選填),短描述(可选) +GameChat_MuteSpeaker_SpeakerDoesNotExist,,,Speaker '{RBX_NAME}' does not exist.,Sprecher „{RBX_NAME}“ existiert nicht.,Speaker '{RBX_NAME}' does not exist.,"El usuario ""{RBX_NAME}"" no existe.","El usuario ""{RBX_NAME}"" no existe.",L'interlocuteur {RBX_NAME} n'existe pas.,"Il giocatore ""{RBX_NAME}"" non esiste.",「{RBX_NAME}」は存在しません。,스피커 '{RBX_NAME}'이(가) 없어요.,'{RBX_NAME}' não existe.,'{RBX_NAME}' não existe.,Пользователя {RBX_NAME} не существует.,发言者“{RBX_NAME}”不存在。,使用者「{RBX_NAME}」不存在。,发言者“{RBX_NAME}”不存在。 +GameChat_ChatMain_SpeakerHasBeenBlocked,,,Speaker '{RBX_NAME}' has been blocked.,Sprecher „{RBX_NAME}“ wurde gesperrt.,Speaker '{RBX_NAME}' has been blocked.,"Se ha bloqueado al usuario ""{RBX_NAME}"".","Se ha bloqueado al usuario ""{RBX_NAME}"".",L'interlocuteur {RBX_NAME} a été bloqué.,"Hai bloccato il giocatore ""{RBX_NAME}"".",「{RBX_NAME}」はブロック中です。,스피커 '{RBX_NAME}'님을 차단했어요.,'{RBX_NAME}' foi bloqueado.,'{RBX_NAME}' foi bloqueado.,Пользователь {RBX_NAME} заблокирован.,发言者“{RBX_NAME}”已被屏蔽。,已封鎖使用者「{RBX_NAME}」。,发言者“{RBX_NAME}”已被屏蔽。 +GameChat_ChatMain_SpeakerHasBeenMuted,,,Speaker '{RBX_NAME}' has been muted.,Sprecher „{RBX_NAME}“ wurde stummgeschaltet.,Speaker '{RBX_NAME}' has been muted.,"Se ha silenciado al usuario ""{RBX_NAME}"".","Se ha silenciado al usuario ""{RBX_NAME}"".",L'interlocuteur {RBX_NAME} a été bâillonné.,"Hai tolto la parola al giocatore ""{RBX_NAME}"".",「{RBX_NAME}」をミュートしました。,스피커 '{RBX_NAME}'이(가) 음소거되었어요.,'{RBX_NAME}' foi silenciado(a).,'{RBX_NAME}' foi silenciado(a).,Пользователь {RBX_NAME} добавлен в список игнорируемых.,发言者“{RBX_NAME}”已被禁言。,已將使用者「{RBX_NAME}」靜音。,发言者“{RBX_NAME}”已被禁言。 +GameChat_ChatMain_SpeakerHasBeenUnBlocked,,,Speaker '{RBX_NAME}' has been unblocked.,Sperrung von Sprecher „{RBX_NAME}“ wurde aufgehoben.,Speaker '{RBX_NAME}' has been unblocked.,"Se ha desbloqueado al usuario ""{RBX_NAME}"".","Se ha desbloqueado al usuario ""{RBX_NAME}"".",L'interlocuteur {RBX_NAME} n'est plus bloqué.,"Hai sbloccato il giocatore ""{RBX_NAME}"".",「{RBX_NAME}」のブロックが解除されました。,스피커 '{RBX_NAME}'님 차단을 해제했어요.,'{RBX_NAME}' foi desbloqueado.,'{RBX_NAME}' foi desbloqueado.,Пользователь {RBX_NAME} разблокирован.,发言者“{RBX_NAME}”已被取消屏蔽。,已解除封鎖使用者「{RBX_NAME}」。,发言者“{RBX_NAME}”已被取消屏蔽。 +GameChat_ChatMain_SpeakerHasBeenUnMuted,,,Speaker '{RBX_NAME}' has been unmuted.,Stummschaltung von Sprecher „{RBX_NAME}“ wurde aufgehoben.,Speaker '{RBX_NAME}' has been unmuted.,"Se ha cancelado el silencio del usuario ""{RBX_NAME}"".","Se ha cancelado el silencio del usuario ""{RBX_NAME}"".",L'interlocuteur {RBX_NAME} n'est plus bâillonné.,"Hai restituito la parola al giocatore ""{RBX_NAME}"".",「{RBX_NAME}」のミュートを解除しました。,스피커 '{RBX_NAME}'의 음소거가 해제되었어요.,O silêncio de '{RBX_NAME}' foi removido.,O silêncio de '{RBX_NAME}' foi removido.,Пользователь {RBX_NAME} удален из списка игнорируемых.,发言者“{RBX_NAME}”已被取消禁言。,已將使用者「{RBX_NAME}」解除靜音。,发言者“{RBX_NAME}”已被取消禁言。 +GameChat_ChatMain_ChatBarTextTouch,,,Tap here to chat,Tippe zum Chatten hier.,Tap here to chat,Toca aquí para chatear,Toca aquí para chatear,Touchez ici pour discuter,Tocca qui per chattare,ここをタップしてチャットする,여기를 클릭한 후 내용을 입력하세요,Toque aqui para escrever,Toque aqui para escrever,"Коснитесь здесь, чтобы общаться в чате",轻点此处以聊天,按下此處聊天,轻点此处以聊天 +Target,,,Target,Ziel,Target,Objetivo,Objetivo,Cible,Scopo,目標,대상,Alvo,Alvo,Цель,目标,目標,目标 +GameChat_ChatService_ChatFilterIssues,,,The chat filter is currently experiencing issues and messages may be slow to appear.,Es gibt derzeit Probleme mit dem Chatfilter. Nachrichten können deshalb mit Verzögerung angezeigt werden.,The chat filter is currently experiencing issues and messages may be slow to appear.,El filtro del chat sufre problemas en este momento y es posible que los mensajes tarden un poco en aparecer.,El filtro del chat sufre problemas en este momento y es posible que los mensajes tarden un poco en aparecer.,Le filtre de chat connaît actuellement des problèmes et les messages pourraient mettre du temps à apparaître.,Il filtro della chat sta riscontrando dei problemi e i messaggi potrebbero apparire in ritardo.,現在、チャットフィルターに問題があるためメッセージの表示が遅れています。,현재 채팅 필터에 문제가 있어 메시지 표시가 느릴 수 있어요.,O filtro de chat está com problemas no momento e as mensagens podem demorar para aparecer.,O filtro de chat está com problemas no momento e as mensagens podem demorar para aparecer.,Могут возникать задержки в передаче сообщений из-за проблем с фильтром чата.,聊天过滤器当前遇到问题,消息显示可能出现延迟。,文字過濾系統發生問題,訊息可能會延遲顯示。,聊天过滤器当前遇到问题,消息显示可能出现延迟。 +GameChat_ChatCommandsTeller_Desc,,,These are the basic chat commands.,Das sind die grundlegenden Chatbefehle.,These are the basic chat commands.,Estos son los comandos básicos del chat.,Estos son los comandos básicos del chat.,Voici les commandes de chat basiques.,Questi sono i comandi base della chat.,これらは基本的なチャットコマンドです。,기본 채팅 명령어에요.,Esses são os comandos de chat básicos.,Esses são os comandos de chat básicos.,Это основные команды чата.,这些是基本聊天指令。,以下是基本聊天室指令。,这些是基本聊天指令。 +GameChat_ChatServiceRunner_SystemChannelWelcomeMessage,,,This channel is for system and game notifications.,Dieser Kanal ist für System- und Spielbenachrichtigungen.,This channel is for system and game notifications.,Este canal es para notificaciones del sistema y del juego.,Este canal es para notificaciones del sistema y del juego.,Ce canal est réservé aux notifications système et de jeu.,Questo canale è per le notifiche di gioco e del sistema.,このチャンネルはシステムとゲーム通知のためのものです。,이 채널은 시스템 및 게임 알림용이에요.,Este canal é destinado a notificações do sistema e jogo.,Este canal é destinado a notificações do sistema e jogo.,Этот канал предназначен для системных и игровых уведомлений.,此频道用于发送系统及游戏通知。,此頻道為系統及遊戲通知專用。,此频道用于发送系统及游戏通知。 +GameChat_GetVersion_Message,,,This game is running chat version [{RBX_NUMBER}.{RBX_NUMBER}].,Die Chatversion dieses Spiels ist [{RBX_NUMBER}.{RBX_NUMBER}].,This game is running chat version [{RBX_NUMBER}.{RBX_NUMBER}].,Este juego utiliza la versión del chat [{RBX_NUMBER} {RBX_NUMBER}].,Este juego utiliza la versión del chat [{RBX_NUMBER} {RBX_NUMBER}].,Le jeu utilise la version de chat [{RBX_NUMBER} {RBX_NUMBER}].,Questo gioco usa la versione [{RBX_NUMBER}.{RBX_NUMBER}] della chat.,このゲームはチャットバージョン [{RBX_NUMBER}.{RBX_NUMBER}]を実行しています。,이 게임은 채팅 버전 [{RBX_NUMBER}. {RBX_NUMBER}]을(를) 실행합니다.,Este jogo está rodando a versão de chat [{RBX_NUMBER}.{RBX_NUMBER}].,Este jogo está rodando a versão de chat [{RBX_NUMBER}.{RBX_NUMBER}].,Эта игра поддерживает чат версии [{RBX_NUMBER}.{RBX_NUMBER}].,此游戏正在运行聊天版本 [{RBX_NUMBER}.{RBX_NUMBER}]。,此遊戲正在使用聊天室版本 [{RBX_NUMBER}.{RBX_NUMBER}]。,此游戏正在运行聊天版本 [{RBX_NUMBER}.{RBX_NUMBER}]。 +GameChat_TeamChat_WelcomeMessage,,,This is a private channel between you and your team members.,Dies ist ein privater Kanal für dich und deine Teammitglieder.,This is a private channel between you and your team members.,Este es un canal privado entre tú y los miembros de tu equipo.,Este es un canal privado entre tú y los miembros de tu equipo.,Ceci est un canal privé entre les membres de votre équipe et vous.,Questo è un canale privato tra te e i membri della tua squadra.,これはあなたとあなたのチームメンバーとのプライベートチャンネルです。,회원님과 팀원 간의 비공개 채널이에요.,Este é um canal privado entre você e os membros da sua equipe.,Este é um canal privado entre você e os membros da sua equipe.,Это канал для личного общения участников вашей команды.,这是你与团队成员之间的私人频道。,這是您與隊伍成員的私人頻道。,这是你与团队成员之间的私人频道。 +GameChat_ChatMain_ChatBarText,,,"To chat click here or press ""/"" key",Klicke zum Chatten hier oder drücke die „/“-Taste.,"To chat click here or press ""/"" key","Para chatear, haz clic aquí o pulsa la tecla ""/"".","Para chatear, haz clic aquí o pulsa la tecla ""/"".","Pour discuter, cliquez ici ou sur la touche « / »","Per chattare, clicca qui o premi il tasto ""/""",チャットするにはここをクリックするか 「 / 」 キーを押します。,"여기를 클릭하거나 ""/"" 키를 누른 후 채팅을 시작하세요","Para escrever clique aqui ou aperte a tecla ""/""","Para escrever clique aqui ou aperte a tecla ""/""","Чтобы общаться в чате, нажмите здесь или на клавишу «/»",若要聊天,请点按此处或按下“/”键,若要聊天,請按下此處或「/」鍵,若要聊天,请点按此处或按下“/”键 +GameChat_ChatChannel_MutedInChannel,,,You are muted and cannot talk in this channel,Du wurdest stummgeschaltet und kannst in diesem Kanal nicht kommunizieren.,You are muted and cannot talk in this channel,Se te ha silenciado y no puedes hablar en este canal.,Se te ha silenciado y no puedes hablar en este canal.,Vous êtes bâillonné et ne pouvez pas parler sur ce canal,Non hai più la parola e non puoi chattare in questo canale,あなたはミュートされこのチャンネルで話すことは出来ません。,이 채널에서 음소거되어 이야기할 수 없어요,Você está silenciado(a) e não pode falar neste canal,Você está silenciado(a) e não pode falar neste canal,Вы добавлены в список игнорируемых и не можете общаться на этом канале,你已被禁言,无法在此频道聊天,您遭到靜音,無法在此頻道聊天,你已被禁言,无法在此频道聊天 +GameChat_PrivateMessaging_CannotChat,,,You are not able to chat with this player.,Du kannst mit diesem Spieler nicht chatten.,You are not able to chat with this player.,No puedes chatear con este jugador.,No puedes chatear con este jugador.,Vous ne pouvez pas discuter avec ce joueur.,Non puoi chattare con questo giocatore.,このプレイヤーとチャットすることは出来ません。,이 플레이어와 채팅할 수 없어요.,Você não pode participar de chat com este jogador.,Você não pode participar de chat com este jogador.,Вы не можете общаться в чате с этим игроком.,你无法与此玩家聊天。,您無法與此玩家聊天。,你无法与此玩家聊天。 +GameChat_ChatServiceRunner_YouAreNotInChannel,,,You are not in channel {RBX_NAME},Du befindest dich nicht in Kanal „{RBX_NAME}“.,You are not in channel {RBX_NAME},No estás en el canal {RBX_NAME}.,No estás en el canal {RBX_NAME}.,Vous n'êtes pas sur le canal {RBX_NAME},Non ti trovi nel canale {RBX_NAME},あなたはチャンネル {RBX_NAME} にいません。,{RBX_NAME} 채널에 있지 않아요,Você não está no canal {RBX_NAME},Você não está no canal {RBX_NAME},Вы не на канале «{RBX_NAME}»,你不在频道“{RBX_NAME}”,您不在 {RBX_NAME} 頻道,你不在频道“{RBX_NAME}” +GameChat_SwitchChannel_NotInChannel,,,You are not in channel: '{RBX_NAME}',Du befindest dich nicht in Kanal „{RBX_NAME}“.,You are not in channel: '{RBX_NAME}',"No estás en el canal ""{RBX_NAME}"".","No estás en el canal ""{RBX_NAME}"".",Vous n'êtes pas sur le canal : {RBX_NAME},"Non ti trovi nel canale: ""{RBX_NAME}""",あなたはチャネル: 「{RBX_NAME}」にいません。,{RBX_NAME}' 채널에 있지 않아요,Você não está no canal: '{RBX_NAME}',Você não está no canal: '{RBX_NAME}',Вы не на канале «{RBX_NAME}»,你不在频道:“{RBX_NAME}”,您不在「{RBX_NAME}」頻道,你不在频道:“{RBX_NAME}” +GameChat_TeamChat_NowInTeam,,,You are now on the '{RBX_NAME}' team.,Du bist nun Mitglied im Team „{RBX_NAME}“.,You are now on the '{RBX_NAME}' team.,"Ahora formas parte del equipo ""{RBX_NAME}"".","Ahora formas parte del equipo ""{RBX_NAME}"".",Vous êtes désormais dans l'équipe {RBX_NAME}.,"Ora sei nella squadra ""{RBX_NAME}"".",あなたは現在「{RBX_NAME}」チームに所属しています。,현재 '{RBX_NAME}'팀에 속해 있어요.,Você agora está na equipe '{RBX_NAME}'.,Você agora está na equipe '{RBX_NAME}'.,Вы вступили в команду {RBX_NAME}.,你正在团队“{RBX_NAME}”中。,您在「{RBX_NAME}」隊伍。,你正在团队“{RBX_NAME}”中。 +GameChat_PrivateMessaging_NowChattingWith,,,You are now privately chatting with {RBX_NAME},Du unterhältst dich nun privat mit {RBX_NAME}.,You are now privately chatting with {RBX_NAME},Estás chateando en privado con {RBX_NAME}.,Estás chateando en privado con {RBX_NAME}.,"Maintenant, vous discutez en privé avec {RBX_NAME}",Ora stai chattando in privato con {RBX_NAME},あなたは現在、{RBX_NAME} さんとプライベートチャット中です。,현재 {RBX_NAME}님과 비공개 채팅 중이에요,Você agora está em um chat privado com {RBX_NAME},Você agora está em um chat privado com {RBX_NAME},Открыт канал для личного общения с пользователем {RBX_NAME},你正在与“{RBX_NAME}”私聊,您正在與{RBX_NAME}私聊,你正在与“{RBX_NAME}”私聊 +GameChat_ChatServiceRunner_YouCannotJoinChannel,,,You cannot join channel {RBX_NAME},Du kannst Kanal „{RBX_NAME}“ nicht beitreten.,You cannot join channel {RBX_NAME},No puedes unirte al canal {RBX_NAME}.,No puedes unirte al canal {RBX_NAME}.,Vous ne pouvez pas rejoindre le canal {RBX_NAME},Non puoi accedere al canale {RBX_NAME},チャンネル {RBX_NAME} に参加することは出来ません。,{RBX_NAME} 채널에 가입할 수 없어요,Você não pode entrar no canal {RBX_NAME},Você não pode entrar no canal {RBX_NAME},Вы не можете подключиться к каналу «{RBX_NAME}»,你无法加入频道“{RBX_NAME}”,您無法加入{RBX_NAME} 頻道,你无法加入频道“{RBX_NAME}” +GameChat_ChatServiceRunner_YouCannotLeaveChannel,,,You cannot leave channel {RBX_NAME},Du kannst Kanal „{RBX_NAME}“ nicht verlassen.,You cannot leave channel {RBX_NAME},No puedes salir del canal {RBX_NAME}.,No puedes salir del canal {RBX_NAME}.,Vous ne pouvez pas quitter le canal {RBX_NAME},Non puoi lasciare il canale {RBX_NAME},チャンネル {RBX_NAME} を終了できません。,{RBX_NAME} 채널에서 나갈 수 없어요,Você não pode sair do canal {RBX_NAME},Você não pode sair do canal {RBX_NAME},Вы не можете покинуть канал «{RBX_NAME}».,你无法离开频道“{RBX_NAME}”,您無法離開 {RBX_NAME} 頻道,你无法离开频道“{RBX_NAME}” +GameChat_ChatService_CannotLeaveChannel,,,You cannot leave this channel.,Du kannst diesen Kanal nicht verlassen.,You cannot leave this channel.,No puedes salir de este canal.,No puedes salir de este canal.,Vous ne pouvez pas quitter ce canal.,Non puoi lasciare questo canale.,このチャンネルを終了できません。,채널에서 나갈 수 없어요.,Você não pode sair deste canal.,Você não pode sair deste canal.,Вы не можете покинуть этот канал.,你无法离开此频道。,您無法離開此頻道。,你无法离开此频道。 +GameChat_DoMuteCommand_CannotMuteSelf,,,You cannot mute yourself.,Du kannst dich nicht selbst stummschalten.,You cannot mute yourself.,No puedes silenciarte a ti mismo.,No puedes silenciarte a ti mismo.,Vous ne pouvez pas vous bâillonner.,Non puoi togliere la parola a te stesso.,自分をミュートすることは出来ません。,자신을 음소거할 수 없어요.,Você não pode silenciar a si mesmo.,Você não pode silenciar a si mesmo.,Невозможно добавить себя в список игнорируемых.,你无法将自己禁言。,您無法將自己靜音。,你无法将自己禁言。 +GameChat_TeamChat_CannotTeamChatIfNotInTeam,,,You cannot team chat if you are not on a team!,"Teamchat ist nur verfügbar, wenn du Mitglied eines Teams bist!",You cannot team chat if you are not on a team!,¡No puedes chatear con tu equipo si no formas parte de un equipo!,¡No puedes chatear con tu equipo si no formas parte de un equipo!,Vous ne pouvez pas avoir de discussion d'équipe si vous n'appartenez pas à une équipe !,Non puoi chattare con la squadra se non sei in una squadra!,チームに所属していなければチームチャットは出来ません。,팀에 속하지 않으면 팀 채팅을 이용할 수 없어요!,Você não pode participar de chat de equipe se não estiver em uma!,Você não pode participar de chat de equipe se não estiver em uma!,"Вы не можете общаться в командном чате, если не состоите в команде!",如果你不在该团队,则无法进行团队聊天。,若您不在隊伍中,您無法使用隊伍頻道。,如果你不在该团队,则无法进行团队聊天。 +GameChat_PrivateMessaging_CannotWhisperToSelf,,,You cannot whisper to yourself.,Du kannst dir nicht selbst etwas zuflüstern.,You cannot whisper to yourself.,No puedes enviarte mensajes privados a ti mismo.,No puedes enviarte mensajes privados a ti mismo.,Vous ne pouvez pas murmurer à votre propre oreille.,Non puoi aprire un canale privato con te stesso.,自分自身に話しかけることは出来ません。,자신에게 귓속말할 수 없어요.,Você não pode sussurrar para si mesmo.,Você não pode sussurrar para si mesmo.,Нельзя отправлять себе личные сообщения.,你无法与自己开启私人频道。,您無法與自己開啟私人頻道。,你无法与自己开启私人频道。 +GameChat_ChatService_YouHaveLeftChannel,,,You have left channel '{RBX_NAME}',Du hast Kanal „{RBX_NAME}“ verlassen.,You have left channel '{RBX_NAME}',"Has salido del canal ""{RBX_NAME}"".","Has salido del canal ""{RBX_NAME}"".",Vous avez quitté le canal {RBX_NAME},"Hai lasciato il canale ""{RBX_NAME}""",チャンネル 「{RBX_NAME}」を退出しました。,{RBX_NAME}' 채널에서 나왔어요,Você saiu do canal '{RBX_NAME}',Você saiu do canal '{RBX_NAME}',Вы покинули канал «{RBX_NAME}»,你已离开频道“{RBX_NAME}”,您已離開「{RBX_NAME}」頻道,你已离开频道“{RBX_NAME}” +GameChat_ChatFloodDetector_Message,,,You must wait before sending another message!,"Du musst warten, bevor du eine weitere Nachricht senden kannst!",You must wait before sending another message!,¡Debes esperar antes de enviar otro mensaje!,¡Debes esperar antes de enviar otro mensaje!,Vous devez attendre avant d'envoyer un autre message !,Devi aspettare prima di inviare un altro messaggio!,少し待ってから次のメッセージを送ってください!,추가 메시지를 보내기 전에 기다려야 해요!,Você precisa esperar antes de enviar outra mensagem!,Você precisa esperar antes de enviar outra mensagem!,"Необходимо подождать, прежде чем отправлять новое сообщение!",发送另一条消息前你必须等待!,請稍後再傳送訊息。,发送另一条消息前你必须等待! +GameChat_ChatFloodDetector_MessageDisplaySeconds,,,You must wait {RBX_NUMBER} seconds before sending another message!,"Du musst {RBX_NUMBER} Sekunden lang warten, bevor du eine weitere Nachricht senden kannst!",You must wait {RBX_NUMBER} seconds before sending another message!,¡Debes esperar {RBX_NUMBER} segundos antes de enviar otro mensaje!,¡Debes esperar {RBX_NUMBER} segundos antes de enviar otro mensaje!,Vous devez attendre {RBX_NUMBER} secondes avant d'envoyer un autre message !,Devi aspettare {RBX_NUMBER} secondi prima di inviare un altro messaggio!,{RBX_NUMBER} 秒待ってから次のメッセージを送ってください!,추가 메시지를 보내기 전에 {RBX_NUMBER}초 동안 기다려야 해요!,Você precisa esperar {RBX_NUMBER} segundos antes de enviar outra mensagem!,Você precisa esperar {RBX_NUMBER} segundos antes de enviar outra mensagem!,Вы сможете отправить новое сообщение только через {RBX_NUMBER} сек.!,发送另一条消息前你必须等待 {RBX_NUMBER} 秒!,請 {RBX_NUMBER} 秒後再傳送訊息。,发送另一条消息前你必须等待 {RBX_NUMBER} 秒! +KEY_PLAYER_IDLE_DISCONNECT,,,You were disconnected for being idle {RBX_NUMBER} minutes,"Deine Verbindung wurde getrennt, da du {RBX_NUMBER} Minuten lang untätig warst.",You were disconnected for being idle {RBX_NUMBER} minutes,Se te ha desconectado por no mostrar actividad durante {RBX_NUMBER} minutos.,Se te ha desconectado por no mostrar actividad durante {RBX_NUMBER} minutos.,Vous avez été déconnecté car vous êtes resté inactif durant {RBX_NUMBER} minutes.,Sei stato disconnesso perché inattivo per {RBX_NUMBER} minuti,{RBX_NUMBER} 分間アイドル状態だったので切断されました。,{RBX_NUMBER}분 동안 기본 상태로 있어 연결이 끊겼어요,Você perdeu a conexão por ficar inativo(a) por {RBX_NUMBER} minutos,Você perdeu a conexão por ficar inativo(a) por {RBX_NUMBER} minutos,"Вы отключены, так как не были активны в течение {RBX_NUMBER} мин.",由于闲置超过 {RBX_NUMBER} 分钟,你已被断开,您已閒置 {RBX_NUMBER} 分鐘,連線中斷,由于闲置超过 {RBX_NUMBER} 分钟,你已被断开 +GameChat_ChatMessageValidator_SettingsError,,,Your chat settings prevent you from sending messages.,Aufgrund deiner Chateinstellungen kannst du keine Nachrichten senden.,Your chat settings prevent you from sending messages.,Tu configuración de chat te impide enviar mensajes.,Tu configuración de chat te impide enviar mensajes.,Vos paramètres de chat vous empêchent d'envoyer des messages.,Non puoi inviare messaggi per le impostazioni della tua chat.,メッセージが送れないチャット設定です。,채팅 설정 때문에 메시지를 보낼 수 없어요.,Suas configurações de chat impedem que você envie mensagens.,Suas configurações de chat impedem que você envie mensagens.,В ваших настройках чата заблокирована возможность отправлять сообщения.,你的聊天设置禁止你发送消息。,您的聊天設定禁止您傳送訊息。,你的聊天设置禁止你发送消息。 +GameChat_FriendChatNotifier_JoinMessage,,,Your friend {RBX_NAME} has joined the game.,Dein Freund {RBX_NAME} ist dem Spiel beigetreten.,Your friend {RBX_NAME} has joined the game.,Tu amigo {RBX_NAME} se ha unido al juego.,Tu amigo {RBX_NAME} se ha unido al juego.,Votre ami {RBX_NAME} a rejoint le jeu.,Il tuo amico {RBX_NAME} è entrato nel gioco.,あなたの友人、{RBX_NAME} さんがゲームに参加しました。,친구 {RBX_NAME}님이 게임에 참가했어요.,Seu amigo {RBX_NAME} juntou-se ao jogo.,Seu amigo {RBX_NAME} juntou-se ao jogo.,Ваш друг {RBX_NAME} присоединился к игре.,你的朋友“{RBX_NAME}”已加入游戏。,您的好友 {RBX_NAME} 已加入遊戲。,你的朋友“{RBX_NAME}”已加入挑战。 +GameChat_ChatMessageValidator_WhitespaceError,,,Your message contains whitespace that is not allowed.,Deine Nachricht enthält unzulässige Leerräume.,Your message contains whitespace that is not allowed.,Tu mensaje contiene espacios vacíos que no se permiten.,Tu mensaje contiene espacios vacíos que no se permiten.,Votre message contient des espaces blancs qui sont interdits.,Il tuo messaggio contiene spazi vuoti non consentiti.,メッセージに許可されていないスペースが含まれています。,메시지에 허용되지 않는 여백이 있어요.,"Sua mensagem contém um espaço em branco, que não é permitido.","Sua mensagem contém um espaço em branco, que não é permitido.",Ваше сообщение содержит недопустимый пробел.,你的消息包含不被允许的空格。,訊息禁止使用空白字元。,你的消息包含不被允许的空格。 +GameChat_ChatMessageValidator_MaxLengthError,,,Your message exceeds the maximum message length.,Deine Nachricht überschreitet die zulässige Nachrichtenlänge.,Your message exceeds the maximum message length.,Tu mensaje supera la longitud máxima permitida.,Tu mensaje supera la longitud máxima permitida.,Votre message dépasse la longueur maximale.,Il tuo messaggio supera la lunghezza massima consentita.,メッセージが最大文字数を超えています。,메시지 길이 한도를 초과했어요.,Sua mensagem ultrapassa o tamanho máximo de mensagem.,Sua mensagem ultrapassa o tamanho máximo de mensagem.,Превышено максимально допустимое количество символов в сообщении.,你的消息已超过最大长度限制。,您的訊息超過長度限制。,你的消息已超过最大长度限制。 +GameChat_SwitchChannel_NowInChannel,,,You are now chatting in channel: '{RBX_NAME}',Du chattest jetzt in Kanal „{RBX_NAME}“.,You are now chatting in channel: '{RBX_NAME}',"Estás chateando en el canal ""{RBX_NAME}"".","Estás chateando en el canal ""{RBX_NAME}"".","Maintenant, vous discutez sur le canal : {RBX_NAME}","Ora stai parlando nel canale: ""{RBX_NAME}""",あなたの現在のチャットチャンネルは: 「{RBX_NAME}」です。,'{RBX_NAME}' 채널에서 채팅 중이에요,Você agora está no canal de chat: '{RBX_NAME}',Você agora está no canal de chat: '{RBX_NAME}',Вы общаетесь на канале «{RBX_NAME}»,你当前的聊天频道为:“{RBX_NAME}”,您目前在「{RBX_NAME}」頻道聊天,你当前的聊天频道为:“{RBX_NAME}” +Sent,,,Sent,Gesendet,Sent,Enviados,Enviados,Envoyé,Inviati,送信しました,보냄,Enviado,Enviado,Отправлено,已发送,已傳送,已发送 +LocalizationTools.Main.RibbonBarButton,,,Tools,,Tools,Herramientas,Herramientas,Outils,,ツール,도구,,,Инструменты,工具,工具,工具 +LocalizationTools.Main.WindowTitle,,,Localization Tools,,Localization Tools,Herramientas de localización,Herramientas de localización,Outils de localisation,,多言語化ツール,로컬리제이션 도구,,,Инструменты локализации,本地化工具,本地化工具,本地化工具 +LocalizationTools.Main.ToolTipMessage,,,Hide/show the Localization Tools View,,Hide/show the Localization Tools View,Ocultar o mostrar la visualización de las herramientas de localización,Ocultar o mostrar la visualización de las herramientas de localización,Cacher/montrer la visualisation des outils de localization,,多言語化ツールの非表示/表示,로컬리제이션 도구 보기 숨기기/표시,,,Показать/скрыть обзор инструментов локализации,隐藏/显示本地化工具视图,隱藏 / 顯示本地化工具檢視模式,隐藏/显示本地化工具视图 +LocalizationTools.EmbeddedTableSection.TextCaptureStartText,,,Start untranslated text capture,,Start untranslated text capture,Empezar la captura del texto no traducido,Empezar la captura del texto no traducido,Commencer la capture du texte non traduit,,未翻訳のテキスト抽出を開始,번역되지 않은 텍스트 캡처 시작,,,Начать захват непереведенного текста,开始抓取未翻译的文本,開始擷取未翻譯文字,开始抓取未翻译的文本 +LocalizationTools.EmbeddedTableSection.TextCaptureStopText,,,Stop untranslated text capture,,Stop untranslated text capture,Detener la captura del texto no traducido,Detener la captura del texto no traducido,Arrêter la capture du text non traduit,,未翻訳のテキスト抽出を停止,번역되지 않은 텍스트 캡처 중지,,,Остановить захват непереведенного текста,停止抓取未翻译的文本,停止擷取未翻譯文字,停止抓取未翻译的文本 +LocalizationTools.EmbeddedTableSection.SectionLabel,,,Embedded Localization Table,,Embedded Localization Table,Tabla de localización insertada,Tabla de localización insertada,Tableau de localization intégré,,組み込み済み多言語化テーブル,임베디드 로컬리제이션 테이블,,,Встроенная локализационная таблица,嵌入本地化表格,內嵌本地化表格,嵌入本地化表格 +LocalizationTools.EmbeddedTableSection.TextCaptureButton,,,Text Capture,,Text Capture,Captura del texto,Captura del texto,Capture du texte,,テキスト抽出,텍스트 캡처,,,Захват текста,文本抓取,文字擷取,文本抓取 +LocalizationTools.EmbeddedTableSection.ExportButton,,,Export,,Export,Exportar,Exportar,Exporter,,書き出す,내보내기,,,Экспорт,导出,匯出,导出 +LocalizationTools.EmbeddedTableSection.ImportButton,,,Import,,Import,Importar,Importar,Importer,,インポート,가져오기,,,Импорт,导入,匯入,导入 +LocalizationTools.EmbeddedTableSection.ExportTextLabel,,,Export LocalizationTables under LocalizationService to CSV files,,Export LocalizationTables under LocalizationService to CSV files,Exportar LocalizationTables bajo LocalizationService a archivos CSV,Exportar LocalizationTables bajo LocalizationService a archivos CSV,Exporter Tableaux de Localization sous Service Localisation vers des fichiers CSV,,LocalizationService の下の LocalizationTables をCSVファイルをエクスポート,LocalizationService 아래의 LocalizationTables를 CSV 파일로 내보내기,,,Экспортировать LocalizationTables через LocalizationService в CSV-файлы,在 LocalizationService 下导出 LocalizationTables 至 CSV 文件,將 LocalizationService 底下的 LocalizationTable 匯出為 CSV 檔案,在 LocalizationService 下导出 LocalizationTables 至 CSV 文件 +LocalizationTools.EmbeddedTableSection.ImportTextLabel,,,Import CSV files to LocalizationTables under LocalizationService,,Import CSV files to LocalizationTables under LocalizationService,Importar archivos CSV a LocalizationTables bajo LocalizationService,Importar archivos CSV a LocalizationTables bajo LocalizationService,Importer des fichiers CSV vers Tableaux de Localization sous Service Localization,,LocalizationService の中の LocalizationTables にCSVファイルをインポート,CSV 파일을 LocalizationService 아래의 LocalizationTables로 가져오기,,,Импортировать CSV-файлы в LocalizationTables через LocalizationService,在 LocalizationService 下导入 LocalizationTables 至 CSV 文件,將 CSV 檔案匯入為 LocalizationService 底下的 LocalizationTable,在 LocalizationService 下导入 LocalizationTables 至 CSV 文件 +LocalizationTools.GameTableSection.CloudTablePageLinkText,,,Click here to configure your cloud localization table,,Click here to configure your cloud localization table,Haz clic aquí para configurar la tabla de localización en la nube,Haz clic aquí para configurar la tabla de localización en la nube,Cliquer ici pour configurer votre tableau de localization en cloud,,ここをクリックして、クラウドローカライゼーションテーブルを環境設定,클라우드 로컬리제이션 테이블을 구성하려면 여기를 클릭하세요.,,,"Щелкните здесь, чтобы настроить облачную локализационную таблицу",点击这里来配置你的云本地化表格,按下此處設定雲端本地化表格,点击这里来配置你的云本地化表格 +LocalizationTools.GameTableSection.DownloadButton,,,Download,,Download,Descargar,Descargar,Télécharger,,ダウンロード,다운로드,,,Загрузить,下载,下載,下载 +LocalizationTools.GameTableSection.DownloadTableLabel,,,Download table as CSV,,Download table as CSV,Descargar la tabla como CSV,Descargar la tabla como CSV,Télécharger le tableau en format CSV,,CSVとしてテーブルをダウンロード,테이블을 CSV로 다운로드,,,Загрузить таблицу как CSV,下载表格并保存至 CSV 格式,將表格下載為 CSV 檔案,下载表格并保存至 CSV 格式 +LocalizationTools.GameTableSection.UpdateButton,,,Update,,Update,Actualizar,Actualizar,Mise à jour,,アップデート,업데이트,,,Обновить,更新,更新,更新 +LocalizationTools.GameTableSection.UpdateTableLabel,,,Update with new content from CSV,,Update with new content from CSV,Actualizar con nuevo contenido de CSV,Actualizar con nuevo contenido de CSV,Mise à jour nouveau contenu du CSV,,CSVからの新しいコンテンツを入れてアップデート,CSV에서 새 콘텐츠로 업데이트,,,Обновить с использованием нового контента из CSV,更新 CSV 中的新内容,使用 CSV 檔案的新內容更新,更新 CSV 中的新内容 +LocalizationTools.GameTableSection.AdvancedButton,,,Advanced,,Advanced,Avanzado,Avanzado,Avancé,,詳細設定,고급,,,Расширенные настройки,高级,進階,高级 +LocalizationTools.GameTableSection.ReplaceButton,,,Replace,,Replace,Reemplazar,Reemplazar,Remplacer,,置き換え,바꾸기,,,Заменить,替换,取代,替换 +LocalizationTools.GameTableSection.ReplaceTableLabel,,,Replace entire cloud table with CSV,,Replace entire cloud table with CSV,Reemplazar toda la tabla en la nube con CSV,Reemplazar toda la tabla en la nube con CSV,Remplacer l'ensemble du tableau cloud par le CSV,,クラウドテーブル全体をCSVで置き換える,전체 클라우드 테이블을 CSV로 바꾸기,,,Заменить всю облачную таблицу CSV-файлом,将整个云表格替换为 CSV,將整個雲端表格取代成 CSV 檔案,将整个云表格替换为 CSV +LocalizationTools.GameTableSection.PublishPlaceMessage,,,Publish this place to upload a table,,Publish this place to upload a table,Publicar este lugar para cargar una tabla,Publicar este lugar para cargar una tabla,Publier cet espace pour télécharger un tableau,,テーブルをアップロードするには、このプレースを公開,이 플레이스를 게시하여 테이블 업로드,,,"Опубликовать место, чтобы загрузить таблицу",发布此场景来上传表格,發布地點後即可上傳表格,发布此场景来上传表格 +LocalizationTools.GameTableSection.SectionLabel,,,Cloud Localization Table,,Cloud Localization Table,Tabla de localización en la nube,Tabla de localización en la nube,Tableau de localisation cloud,,クラウド多言語化テーブル,클라우드 로컬리제이션 테이블,,,Облачная локализационная таблица,云本地化表格,雲端本地化表格,云本地化表格 +LocalizationTools.UploadDialogContent.PatchEmptyMessage,,,Patch empty. Upload anyway?,,Patch empty. Upload anyway?,Revisión vacía. ¿Quieres seguir cargando?,Revisión vacía. ¿Quieres seguir cargando?,Patch vide. Télécharger quand même ?,,パッチが空です。それでもアップロードしますか?,패치가 비어 있습니다. 업로드할까요?,,,Патч пустой. Все равно загрузить?,空补丁,仍要上传?,此更新檔空白,繼續上傳?,空补丁,仍要上传? +LocalizationTools.UploadDialogContent.UploadPatchMessage,,,Upload patch?,,Upload patch?,¿Cargar revisión?,¿Cargar revisión?,Télécharger un patch ?,,パッチをアップロードしますか?,패치를 업로드할까요?,,,Загрузить патч?,上传补丁?,上傳更新檔?,上传补丁? +LocalizationTools.UploadDialogContent.AddEntriesPreText,,,Add entries: ,,Add entries: ,Añadir entradas: ,Añadir entradas: ,Ajouter des entrées : ,,入力内容を追加: ,항목 추가:,,,Добавить записи: ,添加条目:,新增條目:,添加条目: +LocalizationTools.UploadDialogContent.AddTranslationsPretext,,,Add translations: ,,Add translations: ,Añadir traducciones: ,Añadir traducciones: ,Ajouter des traductions : ,,翻訳を追加: ,번역 추가:,,,Добавить переводы: ,添加翻译:,新增翻譯:,添加翻译: +LocalizationTools.UploadDialogContent.ChangeTranslationsPretext,,,Change translations: ,,Change translations: ,Cambiar traducciones: ,Cambiar traducciones: ,Changer des traductions : ,,翻訳を変更: ,번역 변경:,,,Изменить переводы: ,更改翻译:,變更翻譯:,更改翻译: +LocalizationTools.UploadDialogContent.DeleteEntriesPretext,,,Delete entries: ,,Delete entries: ,Eliminar entradas: ,Eliminar entradas: ,Supprimer des entrées : ,,入力内容を削除: ,항목 삭제:,,,Удалить записи: ,删除条目:,刪除條目:,删除条目: +LocalizationTools.UploadDialogContent.DeleteTranslationsPretext,,,Delete translations: ,,Delete translations: ,Eliminar traducciones: ,Eliminar traducciones: ,Supprimer des traductions : ,,翻訳を削除: ,번역 삭제:,,,Удалить переводы: ,删除翻译:,刪除翻譯:,删除翻译: +LocalizationTools.UploadDialogContent.AddLanguagesPretext,,,Add languages: ,,Add languages: ,Añadir idiomas: ,Añadir idiomas: ,Ajouter des langues : ,,言語を追加: ,언어 추가:,,,Добавить языки: ,添加语言:,新增語言:,添加语言: +LocalizationTools.UploadDialogContent.PatchContainsLabel,,,This patch contains: ,,This patch contains: ,Esta revisión contiene: ,Esta revisión contiene: ,Ce patch contient : ,,このパッチに含まれるもの: ,이 패치는 다음을 포함합니다.,,,Этот патч содержит: ,此补丁包括:,此更新檔包含:,此补丁包括: +LocalizationTools.UploadDialogContent.PatchTotalRowsLabel,,,Total rows: ,,Total rows: ,Filas totales: ,Filas totales: ,Total lignes : ,,行の合計: ,전체 행: ,,,Всего строк: ,总条数:,總列數:,总条数: +LocalizationTools.UploadDialogContent.PatchTotalTranslationsLabel,,,Total translations: ,,Total translations: ,Traducciones totales: ,Traducciones totales: ,Total traductions : ,,翻訳の合計: ,전체 번역: ,,,Всего переводов: ,总翻译数:,總翻譯數:,总翻译数: +LocalizationTools.UploadDialogContent.PatchLanguagesLabel,,,Languages: ,,Languages: ,Idiomas: ,Idiomas: ,Langues : ,,言語: ,언어:,,,Языки: ,语言:,語言:,语言: +LocalizationTools.UploadDialogContent.PatchInvalidLanguagesLabel,,,Invalid columns: ,,Invalid columns: ,Columnas no válidas: ,Columnas no válidas: ,Colonnes non valides : ,,無効な列: ,유효하지 않은 열: ,,,Столбцы с ошибками: ,无效的列:,無效行:,无效的列: +LocalizationTools.UploadDialogContent.PatchWillLabel,,,This patch will: ,,This patch will: ,Esta revisión hará lo siguiente: ,Esta revisión hará lo siguiente: ,Ce patcher sera : ,,このパッチは: ,이 패치는 다음 항목을 확인합니다.,,,Цель этого патча: ,此补丁将:,此更新將會:,此补丁将: +LocalizationTools.UploadDialogContent.CancelButton,,,Cancel,,Cancel,Cancelar,Cancelar,Annuler,,キャンセル,취소,,,Отмена,取消,取消,取消 +LocalizationTools.UploadDialogContent.ConfirmButton,,,Confirm,,Confirm,Confirmar,Confirmar,Confirmer,,確定,확인,,,Подтвердить,确认,確認,确认 +LocalizationTools.AddWebEntriesToRbxEntries.WrongFommatedWebTableMessage,,,Wrongly formatted web table.,,Wrongly formatted web table.,Tabla web formateada incorrectamente.,Tabla web formateada incorrectamente.,Tableau web mal formaté.,,ウェブテーブルの形式が間違っています。,웹 테이블 형식이 잘못되었습니다.,,,Неправильно отформатированная веб-таблица.,本地化表格格式有误。,網路表格格式錯誤。,本地化表格格式有误。 +LocalizationTools.AddWebEntriesToRbxEntries.ExpectedTableTypeMessage,,,Expected table type.,,Expected table type.,Tipo de tabla esperado.,Tipo de tabla esperado.,Type de tableau attendu.,,予測されるテーブルの種類。,테이블 유형이 필요합니다.,,,Ожидаемый тип таблицы.,表格类型缺失。,預定表格類型。,表格类型缺失。 +LocalizationTools.AddWebEntriesToRbxEntries.NoLocaleMessage,,,Web table contained translation with no locale,,Web table contained translation with no locale,La tabla web contiene una traducción sin idioma,La tabla web contiene una traducción sin idioma,Le tableau web contient une traduction sans locale,,ウェブテーブルに地域指定なしの翻訳があります,로케일이 없는 번역이 포함된 웹 테이블,,,Веб-таблица содержит перевод без языковых настроек,本地化表格包含未标明语言的翻译,網路表格包含未註明地區的翻譯,本地化表格包含未标明语言的翻译 +LocalizationTools.PageDownloader.DecodeFailedMessage,,,Downloaded table failed to decode,,Downloaded table failed to decode,Error de decodificación de la tabla descargada,Error de decodificación de la tabla descargada,Le tableau téléchargé n'a pas pu être décodé,,ダウンロードテーブルがデコードできませんでした,다운로드한 테이블을 디코딩할 수 없음,,,Ошибка декодирования загруженной таблицы,下载的表格解码失败,無法解碼下載的表格,下载的表格解码失败 +LocalizationTools.PageDownloader.FailedWithStatusCodeMessage,,,"Uploading table failed with status code: {1}, and response: {2}",,"Uploading table failed with status code: {1}, and response: {2}",Error al cargar la tabla con código de estado: {1} y respuesta: {2},Error al cargar la tabla con código de estado: {1} y respuesta: {2},Le chargement du tableau a échoué en raison du code avec statut: {1} et réponse : {2},,テーブルのアップロードができませんでした。ステータスコードは: {1}、レスポンス: {2},상태 코드: {1} 및 응답: {2} 때문에 테이블을 업로드할 수 없음,,,"Ошибка загрузки таблицы, код статуса: {1}, отклик: {2}",上传表格失败,状态码:{1},响应码:{2},表格上傳失敗,狀態碼:{1},回應:{2},上传表格失败,状态码:{1},响应码:{2} +LocalizationTools.PageDownloader.DownloadFailedMessage,,,Download failed,,Download failed,Error al descargar,Error al descargar,Le téléchargement a échoué,,ダウンロードできませんでした,다운로드 실패,,,Ошибка загрузки,下载失败,下載失敗,下载失败 +LocalizationTools.UploadDownloadFlow.UnexpectedErrorMessage,,,Unexpected error,,Unexpected error,Error inesperado,Error inesperado,Une erreur s'est produite,,予期せぬエラーが起きました,예기치 않은 오류,,,Непредвиденная ошибка,未知错误,意外錯誤,未知错误 +LocalizationTools.UploadDownloadFlow.CSVReadFailedMessage,,,CSV read failed,,CSV read failed,Error en la lectura de CSV,Error en la lectura de CSV,La lecture CSV a échoué,,CSVの読み取りができませんでした,CSV 읽기 실패,,,Ошибка чтения CSV,CSV 读取失败,CSV 讀取失敗,CSV 读取失败 +LocalizationTools.UploadDownloadFlow.OpenCSVCanceledMessage,,,Open CSV canceled,,Open CSV canceled,Apertura de CSV cancelada,Apertura de CSV cancelada,Ouverture CSV annulée,,CSVを開けるがキャンセルされました,CSV 열기가 취소됨,,,Открытие CSV отменено,打开 CSV 失败,已取消開啟 CSV,打开 CSV 失败 +LocalizationTools.UploadDownloadFlow.CSVWriteFailedMessage,,,CSV write failed,,CSV write failed,Error en la escritura de CSV,Error en la escritura de CSV,L'écriture CSV a échoué,,CSVの書き込みができませんでした,CSV 쓰기 실패,,,Ошибка записи CSV,CSV 写入失败,CSV 寫入失敗,CSV 写入失败 +LocalizationTools.UploadDownloadFlow.SaveCSVCanceledMessage,,,Save CSV canceled,,Save CSV canceled,Guardado de CSV cancelado,Guardado de CSV cancelado,La sauvegarde CSV a échoué,,CSV保存がキャンセルされました,CSV 저장이 취소됨,,,Сохранение CSV отменено,取消保存 CSV ,已取消儲存 CSV,取消保存 CSV +LocalizationTools.UploadDownloadFlow.BusyMessage,,,busy,,busy,ocupado,ocupado,occupé,,ビジー状態,사용 중,,,выполняется,系统繁忙,忙碌,系统繁忙 +LocalizationTools.UploadDownloadFlow.OpenCSVFileMessage,,,Open CSV file...,,Open CSV file...,Abrir archivo CSV...,Abrir archivo CSV...,Ouvrir fichier CSV...,,CSVファイルを開く...,CSV 파일 열기...,,,Открыть CSV-файл...,打开 CSV 文件...,開啟 CSV 檔案…,打开 CSV 文件... +LocalizationTools.UploadDownloadFlow.ComputingPatchMessage,,,Computing patch...,,Computing patch...,Calculando revisión...,Calculando revisión...,Patch informatique...,,パッチを計算中...,패치를 컴퓨팅하는 중...,,,Вычисление патча...,正在计算补丁...,正在運算更新檔…,正在计算补丁... +LocalizationTools.UploadDownloadFlow.ConfirmUploadMessage,,,Confirm upload...,,Confirm upload...,Confirmar carga...,Confirmar carga...,Confirmer le téléchargement...,,アップロードを確定...,업로드 확인...,,,Подтверждение загрузки...,确定上传...,確認上傳…,确定上传... +LocalizationTools.UploadDownloadFlow.ConfirmUploadDialogTitle,,,Comfirm Upload,,Comfirm Upload,Confirmar carga,Confirmar carga,Confirmer le téléchargement,,アップロードを確定,업로드 확인,,,Подтвердить загрузку,确认上传,確認上傳,确认上传 +LocalizationTools.UploadDownloadFlow.UploadingPatchMessage,,,Uploading patch...,,Uploading patch...,Cargando revisión...,Cargando revisión...,Téléchargement patch...,,パッチをアップロード中...,패치를 업로드하는 중...,,,Загрузка патча...,正在上传补丁...,正在上傳更新檔…,正在上传补丁... +LocalizationTools.UploadDownloadFlow.UploadCompleteMessage,,,Upload complete,,Upload complete,Carga completada,Carga completada,Téléchargement complet,,アップロード完了,업로드 완료,,,Загрузка завершена,上传完成,上傳完成,上传完成 +LocalizationTools.UploadDownloadFlow.UploadFailedMessage,,,Upload failed,,Upload failed,Error al cargar,Error al cargar,Échec du téléchargement,,アップロードできませんでした,업로드 실패,,,Ошибка загрузки,上传失败,上傳失敗,上传失败 +LocalizationTools.UploadDownloadFlow.UploadCanceledMessage,,,Upload canceled,,Upload canceled,Carga cancelada,Carga cancelada,Téléchargement annulé,,アップロードがキャンセルされました,업로드 취소됨,,,Загрузка отменена,上传已取消,上傳已取消,上传已取消 +LocalizationTools.UploadDownloadFlow.ComputePatchFailedMessage,,,Compute patch failed,,Compute patch failed,Error al calcular la revisión,Error al calcular la revisión,Échec du patch de calcul,,パッチ計算ができませんでした,패치 컴퓨팅 실패,,,Ошибка вычисления патча,计算补丁失败,更新檔運算失敗,计算补丁失败 +LocalizationTools.UploadDownloadFlow.DownloadingTableMessage,,,Downloading table...,,Downloading table...,Descargando tabla...,Descargando tabla...,Téléchargement tableau...,,テーブルをダウンロード中...,테이블을 다운로드하는 중...,,,Загружается таблица...,正在下载表格...,正在下載表格…,正在下载表格... +LocalizationTools.UploadDownloadFlow.SelectCSVFileMessage,,,Select CSV file...,,Select CSV file...,Seleccionar archivo CSV...,Seleccionar archivo CSV...,Sélectionner fichier CSV...,,CSVファイルを選択...,CSV 파일 선택...,,,Выбрать CSV-файл...,选择 CSV 文件...,選擇 CSV 檔案…,选择 CSV 文件... +LocalizationTools.UploadDownloadFlow.TableWrittenToFileMessage,,,Table written to file,,Table written to file,Tabla escrita en el archivo,Tabla escrita en el archivo,Tableau écrit au dossier,,ファイルに書かれたテーブル,파일에 기록된 테이블,,,Таблица записана в файл,表格已写入文件,表格已寫入檔案,表格已写入文件 +LocalizationTools.Main.ToolbarLabel,,,Localization,,Localization,Localización,Localización,Localisation,,多言語化,로컬리제이션,,,Локализация,本地化,本地化,本地化 +LocalizationTools.AddWebEntriesToRbxEntries.WrongFormatWebTableMessage,,,Wrongly formatted web table.,,Wrongly formatted web table.,Tabla web formateada incorrectamente.,Tabla web formateada incorrectamente.,Tableau web mal formaté.,,ウェブテーブルの形式が間違っています。,웹 테이블 형식이 잘못되었습니다.,,,Неправильно отформатирована веб-таблица.,本地化表格格式有误。,網路表格格式錯誤。,本地化表格格式有误。 +CoreScripts.AvatarEditorPrompts.GameNamePlaceHolder,,,Unknown Game Name,Unbekannter Spielname,Unknown Game Name,Nombre del juego desconocido,Nombre del juego desconocido,Nom du jeu inconnu,Nome gioco sconosciuto,ゲーム名が不明です,알 수 없는 게임 이름,Nome do jogo desconhecido,Nome do jogo desconhecido,Неизвестное название игры,游戏名称未知,未知遊戲名稱,游戏名称未知 +CoreScripts.AvatarEditorPrompts.InventoryReadAccessPromptTitle,,,Access Items?,Auf Items zugreifen?,Access Items?,¿Acceder a los objetos?,¿Acceder a los objetos?,Accès aux articles ?,Accedere agli oggetti?,アイテムにアクセスしますか?,아이템을 이용할까요?,Acessar itens?,Acessar itens?,Разрешить доступ к предметам?,访问道具?,存取道具?,访问道具? +CoreScripts.AvatarEditorPrompts.InventoryReadAccessPromptText,,,{RBX_NAME} would like to access your avatar’s items,{RBX_NAME} möchte auf die Items deines Avatars zugreifen,{RBX_NAME} would like to access your avatar’s items,{RBX_NAME} quiere acceder a los objetos de tu avatar,{RBX_NAME} quiere acceder a los objetos de tu avatar,{RBX_NAME} aimerait accéder aux articles de ton avatar,{RBX_NAME} vorrebbe accedere agli oggetti del tuo avatar,{RBX_NAME} さんがあなたのアバターアイテムへのアクセスを希望しています,{RBX_NAME}에서 아바타 아이템을 이용하려고 해요,{RBX_NAME} quer acessar os itens de seu avatar,{RBX_NAME} quer acessar os itens de seu avatar,{RBX_NAME} запрашивает доступ к предметам вашего аватара,{RBX_NAME} 想访问你的虚拟形象道具,{RBX_NAME} 想存取您的虛擬人偶的道具,{RBX_NAME} 想访问你的虚拟形象道具 +CoreScripts.AvatarEditorPrompts.CreateOutfitPromptTitle,,,Save Avatar Character,Avatarcharakter speichern,Save Avatar Character,Guardar el avatar,Guardar el avatar,Enregistrer le personnage de l'avatar,Salva personaggio avatar,アバターキャラクターを保存,아바타 캐릭터 저장,Salvar avatar,Salvar avatar,Сохранение аватара персонажа,保存虚拟形象角色,儲存虛擬人偶角色,保存虚拟形象角色 +CoreScripts.AvatarEditorPrompts.CreateOutfitPromptText,,,Do you want to save this character to Roblox? ,Möchtest du diesen Charakter auf Roblox speichern? ,Do you want to save this character to Roblox? ,¿Quieres guardar este avatar en Roblox? ,¿Quieres guardar este avatar en Roblox? ,Veux-tu enregistrer ce personnage sur Roblox ? ,Vuoi salvare questo personaggio su Roblox? ,Robloxにこのキャラクターを保存しますか? ,이 캐릭터를 Roblox에 저장할까요?,Gostaria de salvar este personagem no Roblox? ,Gostaria de salvar este personagem no Roblox? ,Вы хотите сохранить этого персонажа в Roblox? ,将此角色保存至 Roblox?,將此角色儲存到 Roblox?,将此角色保存至 Roblox? +CoreScripts.AvatarEditorPrompts.CreateOutfitPromptNo,,,No,Nein,No,No,No,Non,No,いいえ,아니요,Não,Não,Нет,否,否,否 +CoreScripts.AvatarEditorPrompts.CreateOutfitPromptYes,,,Yes,Ja,Yes,Sí,Sí,Oui,Sì,はい,예,Sim,Sim,Да,是,是,是 +CoreScripts.AvatarEditorPrompts.InventoryReadAccessPromptNo,,,Cancel,Abbrechen,Cancel,Cancelar,Cancelar,Annuler,Annulla,キャンセル,취소,Cancelar,Cancelar,Отмена,取消,取消,取消 +CoreScripts.AvatarEditorPrompts.InventoryReadAccessPromptYes,,,Ok,Okay,Ok,Aceptar,Aceptar,Ok,OK,OK,확인,Ok,Ok,ОК,确定,確定,确定 +CoreScripts.AvatarEditorPrompts.SaveAvatarPromptTitle,,,Equip Avatar with Items,Statte deinen Avatar mit Items aus,Equip Avatar with Items,Equipar avatar con los objetos,Equipar avatar con los objetos,Équiper Avatar avec des articles,Equipaggia avatar con oggetti,アバターにアイテムを装備,아바타에 아이템 장착,Equipar avatar com itens,Equipar avatar com itens,Снаряжение аватара предметами,为虚拟形象装备道具,為虛擬人偶裝備道具,为虚拟形象装备道具 +CoreScripts.AvatarEditorPrompts.SaveAvatarPromptYes,,,Ok,Okay,Ok,Aceptar,Aceptar,Ok,OK,OK,확인,Ok,Ok,ОК,确定,確定,确定 +CoreScripts.AvatarEditorPrompts.SaveAvatarPromptNo,,,Cancel,Abbrechen,Cancel,Cancelar,Cancelar,Annuler,Annulla,キャンセル,취소,Cancelar,Cancelar,Отмена,取消,取消,取消 +CoreScripts.AvatarEditorPrompts.FavouriteItemPromptTitle,,,Favorite Item,Item in Favoriten,Favorite Item,Añadir objeto a Favoritos,Añadir objeto a Favoritos,Article préféré,Oggetto preferito,アイテムをお気に入り登録,아이템을 즐겨찾기에 추가,Adicionar item aos favoritos,Adicionar item aos favoritos,Добавление в избранное,最爱道具,最愛道具,最爱道具 +CoreScripts.AvatarEditorPrompts.FavouriteItemPromptText,,,Do you want to add {RBX_NAME} to favorites?,Möchtest du {RBX_NAME} zu deinen Favoriten hinzufügen?,Do you want to add {RBX_NAME} to favorites?,¿Quieres añadir {RBX_NAME} a tus favoritos?,¿Quieres añadir {RBX_NAME} a tus favoritos?,Veux-tu ajouter {RBX_NAME} à tes préférés ?,Vuoi aggiungere {RBX_NAME} ai preferiti?,{RBX_NAME} をお気に入りに追加しますか?,{RBX_NAME}을(를) 즐겨찾기에 추가할까요?,Você quer adicionar {RBX_NAME} aos seus favoritos?,Você quer adicionar {RBX_NAME} aos seus favoritos?,Хотите добавить в избранное предмет {RBX_NAME}?,将 {RBX_NAME} 添加至最爱?,確定將 {RBX_NAME} 加到最愛?,将 {RBX_NAME} 添加至最爱? +CoreScripts.AvatarEditorPrompts.UnfavouriteItemPromptTitle,,,Unfavorite Item,Item von Favoriten entfernen,Unfavorite Item,Quitar objeto de Favoritos,Quitar objeto de Favoritos,Retirer l'article des préférés,Oggetto non preferito,アイテムのお気に入り登録を解除,아이템을 즐겨찾기에서 제거,Remover item dos favoritos,Remover item dos favoritos,Удаление из избранного,移出最爱的道具,解除最愛道具,移出最爱的道具 +CoreScripts.AvatarEditorPrompts.UnfavouriteItemPromptText,,,Do you want to remove {RBX_NAME} from favorites?,Möchtest du {RBX_NAME} von deinen Favoriten entfernen?,Do you want to remove {RBX_NAME} from favorites?,¿Quieres quitar {RBX_NAME} de tus favoritos?,¿Quieres quitar {RBX_NAME} de tus favoritos?,Veux-tu retirer {RBX_NAME} des préférés ?,Vuoi rimuovere {RBX_NAME} dai preferiti?,{RBX_NAME} をお気に入りから削除しますか?,{RBX_NAME}을(를) 즐겨찾기에서 제거할까요?,Você quer remover {RBX_NAME} de seus favoritos?,Você quer remover {RBX_NAME} de seus favoritos?,Хотите удалить из избранного предмет {RBX_NAME}?,将 {RBX_NAME} 从“最爱”中移除?,確定將 {RBX_NAME} 解除最愛?,将 {RBX_NAME} 从“最爱”中移除? +CoreScripts.AvatarEditorPrompts.FavouriteItemPromptNo,,,No,Nein,No,No,No,Non,No,いいえ,아니요,Não,Não,Нет,否,否,否 +CoreScripts.AvatarEditorPrompts.FavouriteItemPromptYes,,,Yes,Ja,Yes,Sí,Sí,Oui,Sì,はい,예,Sim,Sim,Да,是,是,是 +CoreScripts.AvatarEditorPrompts.OutfitNamePlaceholder,,,Enter Character Name,Charakternamen eingeben,Enter Character Name,Ingresar el nombre del personaje,Ingresar el nombre del personaje,Saisir le nom du personnage,Inserisci il nome del personaggio,キャラクターの名前を入力,캐릭터 이름 입력,Insira o nome do personagem,Insira o nome do personagem,Введите имя персонажа,输入角色名称,輸入角色名稱,输入角色名称 +CoreScripts.AvatarEditorPrompts.EnterOutfitNamePromptTitle,,,Character Name,Charaktername,Character Name,Nombre del personaje,Nombre del personaje,Nom du personnage,Nome Personaggio,キャラクターの名前,캐릭터 이름,Nome do personagem,Nome do personagem,,角色名称,角色名稱,角色名称 +CoreScripts.AvatarEditorPrompts.EnterOutfitNamePromptYes,,,Confirm,Bestätigen,Confirm,Confirmar,Confirmar,Confirmer,Conferma,確認,확인,Confirmar,Confirmar,,确认,確認,确认 +CoreScripts.AvatarEditorPrompts.ItemsListLoadingFailed,,,Loading Failed,Laden fehlgeschlagen,Loading Failed,Error al cargar,Error al cargar,Échec du téléchargement,Caricamento non riuscito,読み込みできませんでした,불러오기 실패,Falha no carregamento,Falha no carregamento,,加载失败,載入失敗,加载失败 +CoreScripts.AvatarEditorPrompts.SaveAvatarPromptText,,,{RBX_NAME} would like to make the following changes to your avatar:,{RBX_NAME} möchte folgende Änderungen an deinem Avatar vornehmen:,{RBX_NAME} would like to make the following changes to your avatar:,{RBX_NAME} quiere hacer los siguientes cambios a tu avatar:,{RBX_NAME} quiere hacer los siguientes cambios a tu avatar:,{RBX_NAME} souhaiterait faire les changements suivants sur ton avatar :,{RBX_NAME} vuoi fare le seguenti modifiche al tuo avatar:,{RBX_NAME} さんがあなたのアバターに以下の変更を希望しています:,{RBX_NAME}님이 아바타를 다음과 같이 변경하려고 합니다.,{RBX_NAME} gostaria de fazer as seguintes mudanças em seu avatar:,{RBX_NAME} gostaria de fazer as seguintes mudanças em seu avatar:,{RBX_NAME} хочет снарядить вашего аватара следующими предметами: ,{RBX_NAME} 想对您的虚拟形象作出以下更改:,{RBX_NAME} 想要為您的虛擬人偶進行以下變更:,{RBX_NAME} 想对您的虚拟形象作出以下更改: +CoreScripts.AvatarEditorPrompts.Adding,,,Adding:,Fügt hinzu:,Adding:,Añadir:,Añadir:,Ajouter :,Aggiungi:,追加中:,추가 중:,Adicionando:,Adicionando:,,添加:,新增:,添加: +CoreScripts.AvatarEditorPrompts.Removing,,,Removing:,Entfernt:,Removing:,Eliminar:,Eliminar:,Enlever :,Rimuovi:,削除中:,제거 중:,Removendo:,Removendo:,,移除:,移除:,移除: +CoreScripts.AvatarEditorPrompts.NoChangedAssets,,,No item changes,Keine Item-Änderungen,No item changes,No hay cambios de objetos,No hay cambios de objetos,Aucun changements d'article,Nessuna modifica,アイテム変更なし,아이템 변경 없음,Sem mudanças nos itens,Sem mudanças nos itens,,道具没有更改,道具沒有變更,道具没有更改 +CoreScripts.AvatarEditorPrompts.EnterOutfitNamePromptNo,,,Cancel,Abbrechen,Cancel,Cancelar,Cancelar,Annuler,Annulla,キャンセル,취소,Cancelar,Cancelar,,取消,取消,取消 +InGame.EmotesMenu.VisitShopToGetEmotes,,,You don't have emotes. Visit Shop to get emotes!,"Du hast keine Emotes. Besuche den Shop, um Emotes zu besorgen.",You don't have emotes. Visit Shop to get emotes!,No tienes emotes. Visita la Tienda de avatares para conseguir algunos.,No tienes emotes. Visita la Tienda de avatares para conseguir algunos.,Tu n'as d'émotes. Rends-toi dans la section Acheter pour en trouver !,Non hai nessun emote. Visita il negozio per acquistare le emotes!,エモートがありません。ショップに行ってエモートをゲットしてください!,보유 중인 감정 표현이 없습니다. 상점을 방문해 감정 표현을 획득하세요!,Você não tem nenhum emote. Visite a loja para adquirir emotes!,Você não tem nenhum emote. Visite a loja para adquirir emotes!,,您没有动作,前往商店获取动作吧!,您沒有動作,前往商店購買動作吧!,您没有动作,前往商店获取动作吧! +InGame.EmotesMenu.ErrorMessageSwitchToR15,,,Switch to your R15 avatar to play Emote.,"Wechsle zu deinem R15-Avatar, um das Emote zu spielen.",Switch to your R15 avatar to play Emote.,Cambia tu avatar a R15 para usar los emotes.,Cambia tu avatar a R15 para usar los emotes.,Passe à ton avatar R15 pour jouer à Emote.,Passa al tuo avatar R15 per giocare a Emote.,エモートを表示するにはR15アバターに切り替えてください。,감정 표현을 적용하려면 R15 아바타로 전환하세요.,Mude seu avatar para o modelo R15 para usar os emotes.,Mude seu avatar para o modelo R15 para usar os emotes.,,切换成R15虚拟形象以使用此动作。,切換成 R15 虛擬人偶即可使用此動作。,切换成R15虚拟形象以使用此动作。 +InGame.EmotesMenu.ErrorMessageAnimationPlaying,,,You cannot play Emotes during this action.,Du kannst das Emote während dieser Aktion nicht spielen.,You cannot play Emotes during this action.,No puedes usar los emotes durante esta acción.,No puedes usar los emotes durante esta acción.,Tu ne peux pas jouer à Emotes pendant cette action.,Non puoi giocare Emote durante questa azione.,この動作中はエモートを表示できません。,이 동작 중에는 감정 표현을 적용할 수 없습니다.,Você não pode usar emotes durante esta ação.,Você não pode usar emotes durante esta ação.,,无法在此动作进行时使用其他动作。,無法在此動作執行時使用其他動作。,无法在此动作进行时使用其他动作。 diff --git a/Client2021/PlatformContent/pc/fonts/NotoSansCJKjp-Regular.otf b/Client2021/PlatformContent/pc/fonts/NotoSansCJKjp-Regular.otf new file mode 100644 index 0000000..296fbeb Binary files /dev/null and b/Client2021/PlatformContent/pc/fonts/NotoSansCJKjp-Regular.otf differ diff --git a/Client2021/PlatformContent/pc/terrain/diffuse.dds b/Client2021/PlatformContent/pc/terrain/diffuse.dds new file mode 100644 index 0000000..669a312 Binary files /dev/null and b/Client2021/PlatformContent/pc/terrain/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/terrain/diffusearray.dds b/Client2021/PlatformContent/pc/terrain/diffusearray.dds new file mode 100644 index 0000000..8ac4f78 Binary files /dev/null and b/Client2021/PlatformContent/pc/terrain/diffusearray.dds differ diff --git a/Client2021/PlatformContent/pc/terrain/materials.json b/Client2021/PlatformContent/pc/terrain/materials.json new file mode 100644 index 0000000..3d61834 --- /dev/null +++ b/Client2021/PlatformContent/pc/terrain/materials.json @@ -0,0 +1,610 @@ +{ + "platform": "pc", + "atlas": + { + "pc": { + "width": 2048, + "height": 2048, + "tileSize": 256, + "tileCount": 6, + + "sliceSize": 512, + + "borderSize": 42 + }, + "ios": { + "width": 2048, + "height": 2048, + "tileSize": 256, + "tileCount": 6, + + "borderSize": 42 + }, + "android": { + "width": 2048, + "height": 2048, + "tileSize": 256, + "tileCount": 6, + + "borderSize": 42 + }, + "durango": { + "width": 32, + "height": 32, + "tileSize": 4, + "tileCount": 6, + + "sliceSize": 512, + + "borderSize": 42 + } + }, + "materials":[ + { + "name":"Air"} + , + { + "name":"Water", + "water":0.5} + , + { + "name":"Grass", + "base_color":[ + 106, + 127, + 63] + , + "texture_top":{ + "tiling":0.25, + "detiling_legacy":0.25, + "detiling":4, + "rotation":1, + "blend":{ + "min":0.0829, + "max":1, + "scale":0.435} + } + , + "texture_side":{ + "tiling":0.2, + "detiling_legacy":0.2, + "detiling":5, + "blend":{ + "min":0.067, + "max":0.933, + "scale":0.1998} + } + , + "texture_bottom":{ + "tiling":0.45, + "detiling_legacy":0.45, + "detiling":2.2219, + "blend":{ + "min":0.05, + "max":1.3329, + "scale":0.5399} + } + } + , + { + "name":"Slate", + "base_color":[ + 63, + 127, + 107] + , + "shift":0.1, + "texture":{ + "tiling":0.55, + "detiling_legacy":0.55, + "detiling":1.818, + "blend":{ + "min":0.25, + "max":1, + "scale":0.12} + } + , + "blend":{ + "min":0, + "max":1, + "scale":0.1, + "blend":{ + "min":0, + "max":1, + "scale":0.1} + } + } + , + { + "name":"Concrete", + "base_color":[ + 127, + 102, + 63] + , + "quantize":0.5, + "type":"hard", + "texture_top":{ + "tiling":0.5, + "detiling_legacy":0.8, + "detiling":1.25, + "blend":{ + "min":-0.05, + "max":1, + "scale":0.0552} + } + , + "texture_side":{ + "tiling":0.4, + "detiling_legacy":0.4, + "detiling":2.5, + "blend":{ + "min":-0.05, + "max":0.9498, + "scale":0.1602} + } + } + , + { + "name":"Brick", + "base_color":[ + 138, + 86, + 62] + , + "cubify":1, + "type":"hard", + "mapping":"cube", + "texture":{ + "tiling":0.8, + "detiling_legacy":0.2, + "detiling":5, + "blend":{ + "min":0.1668, + "max":0.767, + "scale":0.1397} + } + } + , + { + "name":"Sand", + "base_color":[ + 143, + 126, + 95] + , + "texture_top":{ + "tiling":0.25, + "detiling_legacy":0.25, + "detiling":4, + "blend":{ + "min":0.017, + "max":1.0499, + "scale":0.3552} + } + , + "texture_side":{ + "tiling":0.25, + "detiling_legacy":0.55, + "detiling":1.818, + "blend":{ + "min":0.1668, + "max":0.9829, + "scale":0.3947} + } + } + , + { + "name":"WoodPlanks", + "base_color":[ + 139, + 109, + 79] + , + "cubify":1, + "type":"hard", + "mapping":"cube", + "texture":{ + "tiling":0.8, + "detiling_legacy":0.5, + "detiling":2, + "blend":{ + "min":0.15, + "max":0.883, + "scale":9.9999e-05} + } + } + , + { + "name":"Rock", + "base_color":[ + 102, + 108, + 111] + , + "shift":0.2998, + "type":"hardsoft", + "texture":{ + "tiling":0.55, + "detiling_legacy":0.55, + "detiling":1.818, + "rotation":0.5, + "blend":{ + "min":0.05, + "max":0.4, + "scale":0.0798} + } + } + , + { + "name":"Glacier", + "base_color":[ + 101, + 176, + 234] + , + "texture_top":{ + "tiling":0.23, + "detiling_legacy":0.55, + "detiling":1.818, + "blend":{ + "min":0.2669, + "max":0.967, + "scale":0.3552} + } + , + "texture_side":{ + "tiling":0.2, + "detiling_legacy":0.9, + "detiling":1.1108, + "blend":{ + "min":0.1168, + "max":0.833, + "scale":0.165} + } + , + "texture_bottom":{ + "tiling":0.23, + "detiling_legacy":0.5997, + "detiling":1.6653, + "blend":{ + "min":0.4499, + "max":1.1, + "scale":0.3197} + } + } + , + { + "name":"Snow", + "base_color":[ + 195, + 199, + 218] + , + "texture":{ + "tiling":0.2998, + "detiling_legacy":0.2998, + "detiling":3.33555703802, + "blend":{ + "min":-0.067, + "max":0.8999, + "scale":0.4649} + } + } + , + { + "name":"Sandstone", + "base_color":[ + 137, + 90, + 71] + , + "texture_top":{ + "tiling":0.5, + "detiling_legacy":0.6995, + "detiling":1.429592, + "rotation":0.5, + "blend":{ + "min":0.067, + "max":1.2, + "scale":0.2849} + } + , + "texture_side":{ + "tiling":0.2998, + "detiling_legacy":0.5, + "detiling":2, + "blend":{ + "min":0.05, + "max":0.6169, + "scale":0.225} + } + , + "texture_bottom":{ + "tiling":0.45, + "detiling_legacy":0.45, + "detiling":3.333, + "blend":{ + "min":-0.0829, + "max":1.1, + "scale":0.4199} + } + } + , + { + "name":"Mud", + "base_color":[ + 58, + 46, + 36] + , + "texture":{ + "tiling":0.2998, + "detiling_legacy":0.1, + "detiling":10, + "blend":{ + "min":0.1668, + "max":0.933, + "scale":0.3149} + } + } + , + { + "name":"Basalt", + "base_color":[ + 30, + 30, + 37] + , + "shift":0.2, + "type":"hardsoft", + "texture":{ + "tiling":0.55, + "detiling_legacy":0.55, + "detiling":1.818, + "blend":{ + "min":0.1, + "max":1.0326, + "scale":0.0402} + } + } + , + { + "name":"Ground", + "base_color":[ + 102, + 92, + 59] + , + "texture":{ + "tiling":0.2998, + "detiling_legacy":0.2998, + "detiling":3.333, + "rotation":0.5, + "blend":{ + "min":0.2669, + "max":0.933, + "scale":0.195} + } + } + , + { + "name":"CrackedLava", + "base_color":[ + 232, + 156, + 74] + , + "shift":0.1, + "texture":{ + "tiling":0.28, + "detiling_legacy":0.2998, + "detiling":3.333, + "rotation":0.5, + "blend":{ + "min":-0.067, + "max":0.85, + "scale":0.2148} + } + } + , + { + "name":"Asphalt", + "base_color":[ + 115, + 123, + 107] + , + "texture_top":{ + "tiling":0.25, + "detiling_legacy":0.23, + "detiling":4.3478, + "blend":{ + "min":0.3, + "max":1.0326, + "scale":0.24} + } + , + "texture_side":{ + "tiling":0.2, + "detiling_legacy":0.9, + "detiling":1.1108, + "blend":{ + "min":-0.017, + "max":0.967, + "scale":0.1096} + } + } + , + { + "name":"Cobblestone", + "base_color":[ + 132, + 123, + 90] + , + "quantize":0.1499, + "type":"hardsoft", + "texture_top":{ + "tiling":0.23, + "detiling_legacy":0.25, + "detiling":4, + "blend":{ + "min":0.133, + "max":1.167, + "scale":0.2597} + } + , + "texture_side":{ + "tiling":0.2, + "detiling_legacy":0.9, + "detiling":1.1108, + "blend":{ + "min":-0.1, + "max":0.5669, + "scale":0.1998} + } + } + , + { + "name":"Ice", + "base_color":[ + 129, + 194, + 224] + , + "shift":0.1499, + "texture_top":{ + "tiling":0.25, + "detiling_legacy":0.25, + "detiling":4, + "rotation":0.1} + , + "texture_side":{ + "tiling":0.25, + "detiling_legacy":0.25, + "detiling":4} + , + "blend":{ + "min":0, + "max":1, + "scale":0.1} + } + , + { + "name":"LeafyGrass", + "base_color":[ + 115, + 132, + 74] + , + "texture_top":{ + "tiling":0.25, + "detiling_legacy":0.33, + "detiling":3.0299, + "rotation":1, + "blend":{ + "min":0.017, + "max":0.767, + "scale":0.2898} + } + , + "texture_side":{ + "tiling":0.25, + "detiling_legacy":0.25, + "detiling":4, + "blend":{ + "min":0.133, + "max":0.75, + "scale":0.1902} + } + } + , + { + "name":"Salt", + "base_color":[ + 198, + 189, + 181] + , + "texture_top":{ + "tiling":0.23, + "detiling_legacy":0.55, + "detiling":1.818, + "blend":{ + "min":0.1668, + "max":1, + "scale":0.3251} + } + , + "texture_side":{ + "tiling":0.2, + "detiling_legacy":0.9, + "detiling":1.1108, + "blend":{ + "min":-0.067, + "max":0.9168, + "scale":0.2597} + } + } + , + { + "name":"Limestone", + "base_color":[ + 206, + 173, + 148] + , + "texture_top":{ + "tiling":0.25, + "detiling_legacy":0.55, + "detiling":1.818, + "rotation":0.1, + "blend":{ + "min":0.1668, + "max":0.9168, + "scale":0.3498} + } + , + "texture_side":{ + "tiling":0.25, + "detiling_legacy":0.5, + "detiling":2, + "blend":{ + "min":0, + "max":0.967, + "scale":0.135} + } + } + , + { + "name":"Pavement", + "base_color":[ + 148, + 148, + 140] + , + "quantize":0.25, + "type":"hardsoft", + "texture_top":{ + "tiling":0.46, + "detiling_legacy":0.25, + "detiling":4, + "blend":{ + "min":0.2169, + "max":0.9829, + "scale":0.12} + } + , + "texture_side":{ + "tiling":0.46, + "detiling_legacy":0.25, + "detiling":4, + "blend":{ + "min":-0.133, + "max":1.067, + "scale":0.1152} + } + } + ] + } diff --git a/Client2021/PlatformContent/pc/terrain/normal.dds b/Client2021/PlatformContent/pc/terrain/normal.dds new file mode 100644 index 0000000..1b42d82 Binary files /dev/null and b/Client2021/PlatformContent/pc/terrain/normal.dds differ diff --git a/Client2021/PlatformContent/pc/terrain/normalarray.dds b/Client2021/PlatformContent/pc/terrain/normalarray.dds new file mode 100644 index 0000000..c4e48a5 Binary files /dev/null and b/Client2021/PlatformContent/pc/terrain/normalarray.dds differ diff --git a/Client2021/PlatformContent/pc/terrain/reflection.dds b/Client2021/PlatformContent/pc/terrain/reflection.dds new file mode 100644 index 0000000..436cf6b Binary files /dev/null and b/Client2021/PlatformContent/pc/terrain/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/terrain/reflectionarray.dds b/Client2021/PlatformContent/pc/terrain/reflectionarray.dds new file mode 100644 index 0000000..1ba222b Binary files /dev/null and b/Client2021/PlatformContent/pc/terrain/reflectionarray.dds differ diff --git a/Client2021/PlatformContent/pc/textures/aluminum/diffuse.dds b/Client2021/PlatformContent/pc/textures/aluminum/diffuse.dds new file mode 100644 index 0000000..fee64f0 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/aluminum/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/aluminum/normal.dds b/Client2021/PlatformContent/pc/textures/aluminum/normal.dds new file mode 100644 index 0000000..89eb58b Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/aluminum/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/aluminum/normaldetail.dds b/Client2021/PlatformContent/pc/textures/aluminum/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/aluminum/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/aluminum/reflection.dds b/Client2021/PlatformContent/pc/textures/aluminum/reflection.dds new file mode 100644 index 0000000..dc4dc71 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/aluminum/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/brdfLUT.dds b/Client2021/PlatformContent/pc/textures/brdfLUT.dds new file mode 100644 index 0000000..00e8911 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/brdfLUT.dds differ diff --git a/Client2021/PlatformContent/pc/textures/brick/diffuse.dds b/Client2021/PlatformContent/pc/textures/brick/diffuse.dds new file mode 100644 index 0000000..740279f Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/brick/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/brick/normal.dds b/Client2021/PlatformContent/pc/textures/brick/normal.dds new file mode 100644 index 0000000..a3ead64 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/brick/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/brick/normaldetail.dds b/Client2021/PlatformContent/pc/textures/brick/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/brick/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/brick/reflection.dds b/Client2021/PlatformContent/pc/textures/brick/reflection.dds new file mode 100644 index 0000000..3bbc369 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/brick/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/cobblestone/diffuse.dds b/Client2021/PlatformContent/pc/textures/cobblestone/diffuse.dds new file mode 100644 index 0000000..099820b Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/cobblestone/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/cobblestone/normal.dds b/Client2021/PlatformContent/pc/textures/cobblestone/normal.dds new file mode 100644 index 0000000..feb08d2 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/cobblestone/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/cobblestone/normaldetail.dds b/Client2021/PlatformContent/pc/textures/cobblestone/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/cobblestone/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/cobblestone/reflection.dds b/Client2021/PlatformContent/pc/textures/cobblestone/reflection.dds new file mode 100644 index 0000000..0221722 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/cobblestone/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/concrete/diffuse.dds b/Client2021/PlatformContent/pc/textures/concrete/diffuse.dds new file mode 100644 index 0000000..6a96c3f Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/concrete/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/concrete/normal.dds b/Client2021/PlatformContent/pc/textures/concrete/normal.dds new file mode 100644 index 0000000..dc45805 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/concrete/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/concrete/normaldetail.dds b/Client2021/PlatformContent/pc/textures/concrete/normaldetail.dds new file mode 100644 index 0000000..3e8e359 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/concrete/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/concrete/reflection.dds b/Client2021/PlatformContent/pc/textures/concrete/reflection.dds new file mode 100644 index 0000000..98c43e5 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/concrete/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/diamondplate/diffuse.dds b/Client2021/PlatformContent/pc/textures/diamondplate/diffuse.dds new file mode 100644 index 0000000..99cb032 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/diamondplate/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/diamondplate/normal.dds b/Client2021/PlatformContent/pc/textures/diamondplate/normal.dds new file mode 100644 index 0000000..fe0024e Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/diamondplate/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/diamondplate/normaldetail.dds b/Client2021/PlatformContent/pc/textures/diamondplate/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/diamondplate/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/diamondplate/reflection.dds b/Client2021/PlatformContent/pc/textures/diamondplate/reflection.dds new file mode 100644 index 0000000..f779f95 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/diamondplate/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/fabric/diffuse.dds b/Client2021/PlatformContent/pc/textures/fabric/diffuse.dds new file mode 100644 index 0000000..289594c Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/fabric/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/fabric/normal.dds b/Client2021/PlatformContent/pc/textures/fabric/normal.dds new file mode 100644 index 0000000..ce4b735 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/fabric/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/fabric/normaldetail.dds b/Client2021/PlatformContent/pc/textures/fabric/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/fabric/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/fabric/reflection.dds b/Client2021/PlatformContent/pc/textures/fabric/reflection.dds new file mode 100644 index 0000000..ad09c84 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/fabric/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/glass/diffuse.dds b/Client2021/PlatformContent/pc/textures/glass/diffuse.dds new file mode 100644 index 0000000..e5e919b Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/glass/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/glass/normal.dds b/Client2021/PlatformContent/pc/textures/glass/normal.dds new file mode 100644 index 0000000..dfb65ba Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/glass/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/glass/normaldetail.dds b/Client2021/PlatformContent/pc/textures/glass/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/glass/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/glass/reflection.dds b/Client2021/PlatformContent/pc/textures/glass/reflection.dds new file mode 100644 index 0000000..6475721 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/glass/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/granite/diffuse.dds b/Client2021/PlatformContent/pc/textures/granite/diffuse.dds new file mode 100644 index 0000000..0b9a1a8 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/granite/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/granite/normal.dds b/Client2021/PlatformContent/pc/textures/granite/normal.dds new file mode 100644 index 0000000..5a4349f Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/granite/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/granite/normaldetail.dds b/Client2021/PlatformContent/pc/textures/granite/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/granite/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/granite/reflection.dds b/Client2021/PlatformContent/pc/textures/granite/reflection.dds new file mode 100644 index 0000000..3c8724d Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/granite/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/grass/diffuse.dds b/Client2021/PlatformContent/pc/textures/grass/diffuse.dds new file mode 100644 index 0000000..4627b89 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/grass/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/grass/normal.dds b/Client2021/PlatformContent/pc/textures/grass/normal.dds new file mode 100644 index 0000000..b596d9c Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/grass/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/grass/normaldetail.dds b/Client2021/PlatformContent/pc/textures/grass/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/grass/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/grass/reflection.dds b/Client2021/PlatformContent/pc/textures/grass/reflection.dds new file mode 100644 index 0000000..ac89c47 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/grass/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/ice/diffuse.dds b/Client2021/PlatformContent/pc/textures/ice/diffuse.dds new file mode 100644 index 0000000..fee64f0 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/ice/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/ice/normal.dds b/Client2021/PlatformContent/pc/textures/ice/normal.dds new file mode 100644 index 0000000..bde9eb0 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/ice/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/ice/normaldetail.dds b/Client2021/PlatformContent/pc/textures/ice/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/ice/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/ice/reflection.dds b/Client2021/PlatformContent/pc/textures/ice/reflection.dds new file mode 100644 index 0000000..6aff272 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/ice/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/marble/diffuse.dds b/Client2021/PlatformContent/pc/textures/marble/diffuse.dds new file mode 100644 index 0000000..e1804f0 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/marble/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/marble/normal.dds b/Client2021/PlatformContent/pc/textures/marble/normal.dds new file mode 100644 index 0000000..6e0449d Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/marble/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/marble/normaldetail.dds b/Client2021/PlatformContent/pc/textures/marble/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/marble/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/marble/reflection.dds b/Client2021/PlatformContent/pc/textures/marble/reflection.dds new file mode 100644 index 0000000..6c2b9f7 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/marble/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/metal/diffuse.dds b/Client2021/PlatformContent/pc/textures/metal/diffuse.dds new file mode 100644 index 0000000..5cd9550 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/metal/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/metal/normal.dds b/Client2021/PlatformContent/pc/textures/metal/normal.dds new file mode 100644 index 0000000..8502091 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/metal/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/metal/normaldetail.dds b/Client2021/PlatformContent/pc/textures/metal/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/metal/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/metal/reflection.dds b/Client2021/PlatformContent/pc/textures/metal/reflection.dds new file mode 100644 index 0000000..f0cb534 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/metal/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/pebble/diffuse.dds b/Client2021/PlatformContent/pc/textures/pebble/diffuse.dds new file mode 100644 index 0000000..6b385e0 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/pebble/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/pebble/normal.dds b/Client2021/PlatformContent/pc/textures/pebble/normal.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/pebble/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/pebble/normaldetail.dds b/Client2021/PlatformContent/pc/textures/pebble/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/pebble/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/pebble/reflection.dds b/Client2021/PlatformContent/pc/textures/pebble/reflection.dds new file mode 100644 index 0000000..ebf97fd Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/pebble/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/plastic/diffuse.dds b/Client2021/PlatformContent/pc/textures/plastic/diffuse.dds new file mode 100644 index 0000000..d4850dd Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/plastic/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/plastic/normal.dds b/Client2021/PlatformContent/pc/textures/plastic/normal.dds new file mode 100644 index 0000000..db1025b Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/plastic/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/plastic/normaldetail.dds b/Client2021/PlatformContent/pc/textures/plastic/normaldetail.dds new file mode 100644 index 0000000..594f51b Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/plastic/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/rust/diffuse.dds b/Client2021/PlatformContent/pc/textures/rust/diffuse.dds new file mode 100644 index 0000000..ab44658 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/rust/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/rust/normal.dds b/Client2021/PlatformContent/pc/textures/rust/normal.dds new file mode 100644 index 0000000..f98bf87 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/rust/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/rust/normaldetail.dds b/Client2021/PlatformContent/pc/textures/rust/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/rust/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/rust/reflection.dds b/Client2021/PlatformContent/pc/textures/rust/reflection.dds new file mode 100644 index 0000000..8f74ee4 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/rust/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/sand/diffuse.dds b/Client2021/PlatformContent/pc/textures/sand/diffuse.dds new file mode 100644 index 0000000..d37a7c4 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sand/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/sand/normal.dds b/Client2021/PlatformContent/pc/textures/sand/normal.dds new file mode 100644 index 0000000..2d468c1 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sand/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/sand/normaldetail.dds b/Client2021/PlatformContent/pc/textures/sand/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sand/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/sand/reflection.dds b/Client2021/PlatformContent/pc/textures/sand/reflection.dds new file mode 100644 index 0000000..756ae3d Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sand/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/sky/indoor512_bk.tex b/Client2021/PlatformContent/pc/textures/sky/indoor512_bk.tex new file mode 100644 index 0000000..49e3285 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sky/indoor512_bk.tex differ diff --git a/Client2021/PlatformContent/pc/textures/sky/indoor512_dn.tex b/Client2021/PlatformContent/pc/textures/sky/indoor512_dn.tex new file mode 100644 index 0000000..698bae2 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sky/indoor512_dn.tex differ diff --git a/Client2021/PlatformContent/pc/textures/sky/indoor512_ft.tex b/Client2021/PlatformContent/pc/textures/sky/indoor512_ft.tex new file mode 100644 index 0000000..ed84018 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sky/indoor512_ft.tex differ diff --git a/Client2021/PlatformContent/pc/textures/sky/indoor512_lf.tex b/Client2021/PlatformContent/pc/textures/sky/indoor512_lf.tex new file mode 100644 index 0000000..2fdf770 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sky/indoor512_lf.tex differ diff --git a/Client2021/PlatformContent/pc/textures/sky/indoor512_rt.tex b/Client2021/PlatformContent/pc/textures/sky/indoor512_rt.tex new file mode 100644 index 0000000..41ce094 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sky/indoor512_rt.tex differ diff --git a/Client2021/PlatformContent/pc/textures/sky/indoor512_up.tex b/Client2021/PlatformContent/pc/textures/sky/indoor512_up.tex new file mode 100644 index 0000000..f3a695f Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sky/indoor512_up.tex differ diff --git a/Client2021/PlatformContent/pc/textures/sky/sky512_bk.tex b/Client2021/PlatformContent/pc/textures/sky/sky512_bk.tex new file mode 100644 index 0000000..3f55061 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sky/sky512_bk.tex differ diff --git a/Client2021/PlatformContent/pc/textures/sky/sky512_dn.tex b/Client2021/PlatformContent/pc/textures/sky/sky512_dn.tex new file mode 100644 index 0000000..96fa75c Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sky/sky512_dn.tex differ diff --git a/Client2021/PlatformContent/pc/textures/sky/sky512_ft.tex b/Client2021/PlatformContent/pc/textures/sky/sky512_ft.tex new file mode 100644 index 0000000..bf370f1 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sky/sky512_ft.tex differ diff --git a/Client2021/PlatformContent/pc/textures/sky/sky512_lf.tex b/Client2021/PlatformContent/pc/textures/sky/sky512_lf.tex new file mode 100644 index 0000000..af7f1cd Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sky/sky512_lf.tex differ diff --git a/Client2021/PlatformContent/pc/textures/sky/sky512_rt.tex b/Client2021/PlatformContent/pc/textures/sky/sky512_rt.tex new file mode 100644 index 0000000..a78b967 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sky/sky512_rt.tex differ diff --git a/Client2021/PlatformContent/pc/textures/sky/sky512_up.tex b/Client2021/PlatformContent/pc/textures/sky/sky512_up.tex new file mode 100644 index 0000000..9c2aae7 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/sky/sky512_up.tex differ diff --git a/Client2021/PlatformContent/pc/textures/slate/diffuse.dds b/Client2021/PlatformContent/pc/textures/slate/diffuse.dds new file mode 100644 index 0000000..56137ae Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/slate/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/slate/normal.dds b/Client2021/PlatformContent/pc/textures/slate/normal.dds new file mode 100644 index 0000000..2c828ce Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/slate/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/slate/normaldetail.dds b/Client2021/PlatformContent/pc/textures/slate/normaldetail.dds new file mode 100644 index 0000000..d7fa2b2 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/slate/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/slate/reflection.dds b/Client2021/PlatformContent/pc/textures/slate/reflection.dds new file mode 100644 index 0000000..6531376 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/slate/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/studs.dds b/Client2021/PlatformContent/pc/textures/studs.dds new file mode 100644 index 0000000..6ff77fb Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/studs.dds differ diff --git a/Client2021/PlatformContent/pc/textures/wangIndex.dds b/Client2021/PlatformContent/pc/textures/wangIndex.dds new file mode 100644 index 0000000..e613ff1 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/wangIndex.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_01.dds b/Client2021/PlatformContent/pc/textures/water/normal_01.dds new file mode 100644 index 0000000..6af8a34 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_01.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_02.dds b/Client2021/PlatformContent/pc/textures/water/normal_02.dds new file mode 100644 index 0000000..e959d4c Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_02.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_03.dds b/Client2021/PlatformContent/pc/textures/water/normal_03.dds new file mode 100644 index 0000000..ae92c2a Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_03.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_04.dds b/Client2021/PlatformContent/pc/textures/water/normal_04.dds new file mode 100644 index 0000000..69438e7 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_04.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_05.dds b/Client2021/PlatformContent/pc/textures/water/normal_05.dds new file mode 100644 index 0000000..4c83614 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_05.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_06.dds b/Client2021/PlatformContent/pc/textures/water/normal_06.dds new file mode 100644 index 0000000..6fd9c61 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_06.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_07.dds b/Client2021/PlatformContent/pc/textures/water/normal_07.dds new file mode 100644 index 0000000..3a7ccad Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_07.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_08.dds b/Client2021/PlatformContent/pc/textures/water/normal_08.dds new file mode 100644 index 0000000..751be07 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_08.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_09.dds b/Client2021/PlatformContent/pc/textures/water/normal_09.dds new file mode 100644 index 0000000..43efa56 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_09.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_10.dds b/Client2021/PlatformContent/pc/textures/water/normal_10.dds new file mode 100644 index 0000000..a0cdc95 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_10.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_11.dds b/Client2021/PlatformContent/pc/textures/water/normal_11.dds new file mode 100644 index 0000000..c563269 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_11.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_12.dds b/Client2021/PlatformContent/pc/textures/water/normal_12.dds new file mode 100644 index 0000000..4051cfe Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_12.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_13.dds b/Client2021/PlatformContent/pc/textures/water/normal_13.dds new file mode 100644 index 0000000..ac7659b Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_13.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_14.dds b/Client2021/PlatformContent/pc/textures/water/normal_14.dds new file mode 100644 index 0000000..8bf2344 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_14.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_15.dds b/Client2021/PlatformContent/pc/textures/water/normal_15.dds new file mode 100644 index 0000000..9a0dc3d Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_15.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_16.dds b/Client2021/PlatformContent/pc/textures/water/normal_16.dds new file mode 100644 index 0000000..e42fa1d Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_16.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_17.dds b/Client2021/PlatformContent/pc/textures/water/normal_17.dds new file mode 100644 index 0000000..ea57939 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_17.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_18.dds b/Client2021/PlatformContent/pc/textures/water/normal_18.dds new file mode 100644 index 0000000..3f0921d Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_18.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_19.dds b/Client2021/PlatformContent/pc/textures/water/normal_19.dds new file mode 100644 index 0000000..f620846 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_19.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_20.dds b/Client2021/PlatformContent/pc/textures/water/normal_20.dds new file mode 100644 index 0000000..f369b89 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_20.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_21.dds b/Client2021/PlatformContent/pc/textures/water/normal_21.dds new file mode 100644 index 0000000..250ebcd Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_21.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_22.dds b/Client2021/PlatformContent/pc/textures/water/normal_22.dds new file mode 100644 index 0000000..d059da7 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_22.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_23.dds b/Client2021/PlatformContent/pc/textures/water/normal_23.dds new file mode 100644 index 0000000..3c1fde7 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_23.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_24.dds b/Client2021/PlatformContent/pc/textures/water/normal_24.dds new file mode 100644 index 0000000..758b2c1 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_24.dds differ diff --git a/Client2021/PlatformContent/pc/textures/water/normal_25.dds b/Client2021/PlatformContent/pc/textures/water/normal_25.dds new file mode 100644 index 0000000..767118e Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/water/normal_25.dds differ diff --git a/Client2021/PlatformContent/pc/textures/wood/diffuse.dds b/Client2021/PlatformContent/pc/textures/wood/diffuse.dds new file mode 100644 index 0000000..68a7671 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/wood/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/wood/normal.dds b/Client2021/PlatformContent/pc/textures/wood/normal.dds new file mode 100644 index 0000000..71c2e30 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/wood/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/wood/normaldetail.dds b/Client2021/PlatformContent/pc/textures/wood/normaldetail.dds new file mode 100644 index 0000000..9d8a998 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/wood/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/wood/reflection.dds b/Client2021/PlatformContent/pc/textures/wood/reflection.dds new file mode 100644 index 0000000..f7aaedf Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/wood/reflection.dds differ diff --git a/Client2021/PlatformContent/pc/textures/woodplanks/diffuse.dds b/Client2021/PlatformContent/pc/textures/woodplanks/diffuse.dds new file mode 100644 index 0000000..c7be12f Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/woodplanks/diffuse.dds differ diff --git a/Client2021/PlatformContent/pc/textures/woodplanks/normal.dds b/Client2021/PlatformContent/pc/textures/woodplanks/normal.dds new file mode 100644 index 0000000..de4d73f Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/woodplanks/normal.dds differ diff --git a/Client2021/PlatformContent/pc/textures/woodplanks/normaldetail.dds b/Client2021/PlatformContent/pc/textures/woodplanks/normaldetail.dds new file mode 100644 index 0000000..b878e92 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/woodplanks/normaldetail.dds differ diff --git a/Client2021/PlatformContent/pc/textures/woodplanks/reflection.dds b/Client2021/PlatformContent/pc/textures/woodplanks/reflection.dds new file mode 100644 index 0000000..eed6151 Binary files /dev/null and b/Client2021/PlatformContent/pc/textures/woodplanks/reflection.dds differ diff --git a/Client2021/ReflectionMetadata.xml b/Client2021/ReflectionMetadata.xml new file mode 100644 index 0000000..9fb0877 --- /dev/null +++ b/Client2021/ReflectionMetadata.xml @@ -0,0 +1,7723 @@ + + + + + + BindableFunction + Scripting + Allow functions defined in one script to be called by another script + 40 + 66 + + + + + + Invoke + Causes the function assigned to OnInvoke to be called. Arguments passed to this function get passed to OnInvoke function. + + + + + + + OnInvoke + Should be defined as a function. This function is called when Invoke() is called. Number of arguments is variable. + + + + + + + BindableEvent + Scripting + Allow events defined in one script to be subscribed to by another script + + 50 + 67 + + + + + Fire + Used to make the custom event fire (see Event for more info). Arguments can be variable length. + + + + + + + Event + This event fires when the Fire() method is used. Receives the variable length arguments from Fire(). + + + + + + + TouchTransmitter + Used by networking and replication code to transmit touch events - no other purpose + + false + 30 + 37 + + + + + ForceField + Avatar + Prevents joint breakage from explosions, and stops Humanoids from taking damage + 30 + 37 + Model + + + + + PluginManager + + + + + + PluginManagerInterface + + + + + TeleportService + Allows players to seamlessly leave a game and join another + + + + CustomizedTeleportUI + true + Deprecated + + + + + + + Plugin + 30 + 86 + + + + + + PluginMouse + + + + + + Glue + true + BasePart + + + + + CollectionService + A service which provides collections of instances based on tags assigned to them. + + + + + ItemAdded + true + Deprecated. Use GetInstanceAddedSignal instead. + + + + + ItemRemoved + true + Deprecated. Use GetInstancedRemovedSignal instead. + + + + + + + GetCollection + true + Deprecated. Use GetTagged instead. + + + + + GetTagged + Returns an array of all of the instances in the data model which have the given tag. + + + + + AddTag + Adds a tag to an instance. + + + + + RemoveTag + Removes a tag to an instance. + + + + + GetTags + Returns a list of all the collections that an instance belongs to. + + + + + HasTag + Returns whether the given instance has the given tag. + + + + + GetInstanceAddedSignal + Returns a signal that fires when the given tag either has a new instance with that tag added to the data model or that tag is assigned to an instance within the data model. + + + + + GetInstanceRemovedSignal + Returns a signal that fires when the given tag either has an instance with that tag removed from the data model or that tag is removed from an instance within the data model. + + + + + + + JointsService + true + + + + + RunService + + + + + BadgeService + + + + + LogService + + + + + AssetService + A service used to set and get information about assets stored on the Roblox website. + + + + + RevertAsset + Reverts a given place id to the version number provided. Returns true if successful on reverting, false otherwise. + + + + + SetPlacePermissions + Sets the permissions for a placeID to the place accessType. An optional table (inviteList) can be included that will set the accessType for only the player names provided. The table should be set up as an array of usernames (strings). + + + + + GetPlacePermissions + Given a placeID, this function will return a table with the permissions of the place. Useful for determining what kind of permissions a particular user may have for a place. + + + + + GetAssetVersions + Given a placeID, this function will return a table with the version info of the place. An optional arg of page number can be used to page through all revisions (a single page may hold about 50 revisions). + + + + + GetCreatorAssetID + Given a creationID, this function will return the asset that created the creationID. If no other asset created the given creationID, 0 is returned. + + + + + + + HttpService + + + + + HttpEnabled + true + Enabling http requests from scripts + + + + + + + GetAsync + Server + + + + + PostAsync + Server + + + + + + + AnalyticsService + + + + + ApiKey + true + Set ApiKey + + + + + + + InsertService + A service used to insert objects stored on the website into the game. + + + + + AllowClientInsertModels + true + Can be set in non-filtering-enabled places to allow LoadAsset to be used in LocalScripts. + + + + + AllowInsertFreeModels + false + true + -1 + Allows free models to be inserted into place. + + + + + + + GetCollection + Returns a table for the assets stored in the category. A category is an setId from www.roblox.com that links to a set. <a href='http://wiki.roblox.com/index.php?title=API:Class/InsertService/GetCollection' target='_blank'>More info on table format</a>. <a href='http://wiki.roblox.com/index.php/Sets' target='_blank'>More info on sets</a> + + + + + Insert + Inserts the Instance into the workspace. It is recommended to use Instance.Parent = game.Workspace instead, as this can cause issues currently. + + + + + ApproveAssetId + true + Deprecated + + + + + ApproveAssetVersionId + true + Deprecated + + + + + + + + GetBaseSets + Returns a table containing a list of the various setIds that are ROBLOX approved. <a href='http://wiki.roblox.com/index.php/Sets' target='_blank'>More info on sets</a> + + + + + GetUserSets + Returns a table containing a list of the various setIds that correspond to argument 'userId'. <a href='http://wiki.roblox.com/index.php/Sets' target='_blank'>More info on sets</a> + + + + + GetBaseCategories + true + Deprecated. Use GetBaseSets() instead. + + + + + GetUserCategories + true + Deprecated. Use GetUserSets() instead. + + + + + LoadAsset + Returns a Model containing the Instance that resides at AssetId on the web. This call will also yield the script until the model is returned. Script execution can still continue, however, if you use a <a href='http://wiki.roblox.com/index.php?title=Coroutine' target='_blank'>coroutine</a>. + + + + + LoadAssetVersion + Similar to LoadAsset, but instead an AssetVersionId is passed in, which refers to a particular version of the asset which is not neccessarily the latest version. + + + + + + + Hat + Avatar + 30 + 45 + true + + + + + Accessory + Avatar + 30 + 32 + Model + + + + + LocalBackpack + + + + + LocalBackpackItem + + + + + MotorFeature + true + + + + + Attachment + Constraints + 30 + 81 + PVInstance + + + + + + Rotation + + + + + WorldRotation + true + Deprecated. Use WorldOrientation instead + + + + + Orientation + Euler angles applied in YXZ order + + + + + WorldOrientation + Euler angles applied in YXZ order + + + + + Axis + Primary axis. Corresponds to the LookVector, or the first column in the part-local Attachment CFrame rotation matrix + + + + + SecondaryAxis + Secondary axis. Corresponds to the UpVector, or the second column in the part-local Attachment CFrame rotation matrix + + + + + WorldAxis + Primary axis in world space. Corresponds to the LookVector, or the first column in the world space Attachment CFrame rotation matrix. + + + + + SecondaryWorldAxis + Secondary axis in world space. Corresponds to the UpVector, or the second column in the world space Attachment CFrame rotation matrix. + + + + + + + + Bone + Animations + 30 + 114 + PVInstance + PVInstance + Bone + + + + + + Constraint + Physics + 30 + 86 + BasePart + + + + + Enabled + Toggles whether or not this constraint is enabled. Disabled constraints will not render in game. + + + + + Color + The color of the in-game visual. + + + + + Visible + Toggles the in-game visual associated with this constraint. + + + + + Active + Read-only boolean, true if the Constraint is active in world. + + + + + + + + BallSocketConstraint + Constraints + 30 + 86 + BasePart + + + + + LimitsEnabled + Enables the angular limit between the axis of Attachment0 and the axis of Attachment1. + + + + + UpperAngle + Maximum angle between the two main axes. Value in [0, 180]. + + + + + Restitution + Restitution of the limit, or how elastic it is. Value in [0, 1]. + + + + + TwistLimitsEnabled + Enables the angular limits around the main axis of Attachment1. + + + + + TwistUpperAngle + Upper angular limit around the axis of Attachment1. Value in [-180, 180]. + + + + + TwistLowerAngle + Lower angular limit around the axis of Attachment1. Value in [-180, 180]. + + + + + Radius + Radius of the in-game visual. Value in [0, inf). + + + + + + + + RopeConstraint + Constraints + 30 + 89 + BasePart + + + + + Length + The length of the rope or the maximum distance between the two attachments. Value in [0, inf). + + + + + Restitution + Restitution of the rope, or how elastic it is. Value in [0, 1]. + + + + + CurrentDistance + Current distance between the two attachments. Value in [0, inf). + + + + + Thickness + The thickness of the in-game visual (diameter). Value in [0, inf). + + + + + + + + RodConstraint + Constraints + 30 + 90 + BasePart + + + + + Length + The length of the rod or the distance to be maintained between the two attachments. Value in [0, inf). + + + + + CurrentDistance + Current distance between the two attachments. Value in [0, inf). + + + + + Thickness + The thickness of the in-game visual (diameter). Value in [0, inf). + + + + + + + + SpringConstraint + Constraints + 30 + 91 + BasePart + + + + + LimitsEnabled + Enables limits on the length of the spring. + + + + + Stiffness + The stiffness parameter of the spring. Force is scaled based on distance from the free length. The units of this property are force / distance. Value in [0, inf). + + + + + Damping + The damping parameter of the spring. The force is scaled with respect to relative velocity. The units of this property are force / velocity. Value in [0, inf). + + + + + FreeLength + The distance (in studs) between the two attachments at which the spring exerts no stiffness force. Value in [0, inf). + + + + + MaxForce + The maximum force that the spring can apply. Useful to prevent instabilities. The units are mass * studs / seconds^2. Value in [0, inf). + + + + + MaxLength + Maximum spring length, or the maxium distance between the two attachments. Value in [0, inf). + + + + + MinLength + Minimum spring length, or the minimum distance between the two attachments. Value in [0, inf). + + + + + Radius + The radius of the in-game spring coil visual. Value in [0, inf). + + + + + Thickness + The thickness of the spring wire (diameter) in the in-game visual. Value in [0, inf). + + + + + Coils + The number of coils in the in-game visual. Value in [0, 8]. + + + + + CurrentLength + Current distance between the two attachments. Value in [0, inf). + + + + + + + + WeldConstraint + Constraints + 30 + 94 + PVInstance + + + + + Active + Read-only boolean, true if the joint is active in world. Rigid joints may be inactive if they are redundant or form cycles. + + + + + + + + NoCollisionConstraint + Constraints + 30 + 105 + PVInstance + + + + + Enabled + If true Part0 and Part1 will not collide, if false the parts will collide. + + + + + + + + HingeConstraint + Constraints + 30 + 87 + BasePart + + + + + ActuatorType + Type of the rotational actuator: None, Motor, or Servo. + + + + + LimitsEnabled + Enables the angular limits on rotations around the main axis of Attachment0. + + + + + UpperAngle + Upper limit for the angle from the SecondaryAxis of Attachment0 to the SecondaryAxis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + LowerAngle + Lower limit for the angle from the SecondaryAxis of Attachment0 to the SecondaryAxis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + AngularRestitution + Restitution of the two limits, or how elastic they are. Value in [0,1]. + + + + + AngularVelocity + The target angular velocity of the motor in radians per second around the rotation axis. Value in [0, inf). + + + + + MotorMaxTorque + The maximum torque the motor can apply to achieve the target angular velocity. Value in [0, inf). + + + + + MotorMaxAcceleration + The maximum angular acceleration of the motor in radians per second square. Value in [0, inf). + + + + + AngularSpeed + Target angular speed. This value is unsigned as the servo will always move toward its target. Value in [0, inf). + + + + + ServoMaxTorque + Maximum torque the servo motor can apply. Value in [0, inf). + + + + + TargetAngle + Target angle for the SecondaryAxis of Attachment1 from the SecondaryAxis of Attachment0 around the rotation axis. Value in [-180, 180]. + + + + + CurrentAngle + Signed angle between the SecondaryAxis of Attchement0 and the SecondaryAxis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + Radius + Radius of the in-game visual. Value in [0, inf). + + + + + + + + SlidingBallConstraint + Constraints + 30 + 88 + BasePart + + + + + ActuatorType + Type of linear actuator (along the axis of the slider): None, Motor, or Servo. + + + + + LimitsEnabled + Enables the limits on the linear motion along the axis of the slider. + + + + + LowerLimit + Lower limit for the position of Attachment1 with respect to Attachment0 along the slider axis. Value in (-inf, inf). + + + + + UpperLimit + Upper limit for the position of Attachment1 with respect to Attachment0 along the slider axis. Value in (-inf, inf). + + + + + Restitution + Restitution of the two limits, or how elastic they are. Value in [0, 1]. + + + + + Velocity + The target linear velocity of the motor in studs per second along the slider axis. Value in (-inf, inf). + + + + + MotorMaxForce + The maximum force the motor can apply to achieve the target velocity. Units are mass * studs / seconds^2. Value in [0, inf). + + + + + MotorMaxAcceleration + The maximum acceleration of the motor in studs per second squared. Value in [0, inf). + + + + + Speed + Target speed in studs per second. This value is unsigned as the servo will always move toward its target. Value in [0, inf). + + + + + ServoMaxForce + Maximum force the servo motor can apply. Units are mass * studs / seconds^2. Value in [0, inf). + + + + + TargetPosition + Target position of Attachment1 with respect to Attachment0 along the slider axis. Value in (-inf, inf). + + + + + CurrentPosition + Current position of Attachment1 with respect to Attachment0 along the slider axis. Value in (-inf, inf). + + + + + Size + Size of the in-game visual associated with this constraint. Value in [0, inf). + + + + + + + + PrismaticConstraint + Constraints + 30 + 88 + BasePart + + + + + + CylindricalConstraint + Constraints + 30 + 95 + BasePart + + + + + InclinationAngle + Direction of the rotation axis as an angle from the x-axis in the xy-plane of Attachment0. Value in [-180, 180]. + + + + + AngularActuatorType + Type of angular actuator: None, Motor, or Servo. + + + + + AngularLimitsEnabled + Enables the angular limits around the rotation axis. + + + + + UpperAngle + Upper limit for the angle (in degrees) between the reference axis and the SecondaryAxis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + LowerAngle + Lower limit for the angle (in degrees) between the reference axis and the SecondaryAxis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + AngularRestitution + Restitution of the two limits, or how elastic they are. Value in [0, 1]. + + + + + AngularVelocity + The target angular velocity of the motor in radians per second around the rotation axis. Value in [0, inf). + + + + + MotorMaxTorque + The maximum torque the motor can apply to achieve the target angular velocity. The units are mass * studs^2 / second^2. Value in [0, inf). + + + + + MotorMaxAngularAcceleration + The maximum angular acceleration of the motor in radians per second squared. Value in [0, inf). + + + + + AngularSpeed + Target angular speed. This value is unsigned as the servo will always move toward its target. In radians per second. Value in [0, inf). + + + + + ServoMaxTorque + Maximum torque the servo motor can apply. The units are mass * studs^2 / second^2. Value in [0, inf). + + + + + TargetAngle + Target angle (in degrees) between the reference axis and the secondary axis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + CurrentAngle + Signed angle (in degrees) between the reference axis and the secondary axis of Attachment1 around the rotation axis. Value in [-180, 180]. + + + + + WorldRotationAxis + The unit vector direction of the rotation axis in world coordinates. + + + + + RotationAxisVisible + Enable the visibility of the rotation axis. + + + + + + + + AlignOrientation + Constraints + 30 + 100 + BasePart + + + + + AlignPosition + Constraints + 30 + 99 + BasePart + + + + + VectorForce + Constraints + 30 + 102 + Model + + + + + LineForce + Constraints + 30 + 101 + BasePart + + + + + Torque + Constraints + 30 + 103 + BasePart + + + + + AngularVelocity + Constraints + 30 + 103 + BasePart + + + + + Mouse + Used to receive input from the user. Actually tracks mouse events and keyboard events. + + + + + Hit + The CoordinateFrame of where the Mouse ray is currently hitting a 3D object in the Workspace. If the mouse is not over any 3D objects in the Workspace, this property is nil. + + + + + Icon + The current Texture of the Mouse Icon. Stored as a string, for more information on how to format the string <a href='http://wiki.roblox.com/index.php/Content' target='_blank'>go here</a> + + + + + Origin + The CoordinateFrame of where the Mouse is when the mouse is not clicking. + + + + + Origin + The CoordinateFrame of where the Mouse is when the mouse is not clicking. This CoordinateFrame will be very close to the Camera.CoordinateFrame. + + + + + Target + The Part the mouse is currently over. If the mouse is not currently over any object (on the skybox, for example) this property is nil. + + + + + TargetFilter + A Part or Model that the Mouse will ignore when trying to find the Target, TargetSurface and Hit. + + + + + TargetSurface + The NormalId (Top, Left, Down, etc.) of the face of the part the Mouse is currently over. + + + + + UnitRay + The Unit Ray from where the mouse is (Origin) to the current Mouse.Target. + + + + + ViewSizeX + The viewport's (game window) width in pixels. + + + + + ViewSizeY + The viewport's (game window) height in pixels. + + + + + X + The absolute pixel position of the Mouse along the x-axis of the viewport (game window). Values start at 0 on the left hand side of the screen and increase to the right. + + + + + Y + The absolute pixel position of the Mouse along the y-axis of the viewport (game window). Values start at 0 on the top of the screen and increase to the bottom. + + + + + + + Button1Down + Fired when the first button (usually the left, but could be another) on the mouse is depressed. + + + + + Button1Up + Fired when the first button (usually the left, but could be another) on the mouse is release. + + + + + Button2Down + This event is currently non-operational. + + + + + Button2Up + This event is currently non-operational. + + + + + Idle + Fired constantly when the mouse is not firing any other event (i.e. the mouse isn't moving, nor any buttons being pressed or depressed). + + + + + KeyDown + Fired when a user presses a key on the keyboard. Argument is a string representation of the key. If the key has no string representation (such as space), the string passed in is the keycode for that character. Keycodes are currently in ASCII. + + + + + KeyUp + Fired when a user releases a key on the keyboard. Argument is a string representation of the key. If the key has no string representation (such as space), the string passed in is the keycode for that character. Keycodes are currently in ASCII. + + + + + Move + Fired when the mouse X or Y member changes. + + + + + WheelBackward + This event is currently non-operational. + + + + + WheelForward + This event is currently non-operational. + + + + + + + ProfilingItem + + + + + ChangeHistoryService + + + + + RotateP + BasePart + + + + + RotateV + BasePart + + + + + ScriptContext + + + + + Selection + + + + + VelocityMotor + BasePart + + + + + Weld + 200 + 34 + BasePart + + + + + TaskScheduler + false + + + + + SetThreadShare + true + Deprecated + + + + + + + StatsItem + + + + + Snap + 200 + 34 + BasePart + + + + + FileMesh + BasePart + + + + + ClickDetector + 3D Interfaces + Raises mouse events for parent object + 30 + 41 + BasePart + + + + + MaxActivationDistance + The maximum distance a Player's character can be from the ClickDetector's parent Part that will allow the Player's mouse to fire events on this object. + + + + + + + MouseClick + Fired when a player clicks on the parent Part of ClickDetector. The argument provided is always of type Player. + + + + + MouseHoverEnter + Fired when a player's mouse enters on the parent Part of ClickDetector. The argument provided is always of type Player. + + + + + MouseHoverLeave + Fired when a player's mouse leaves the parent Part of ClickDetector. The argument provided is always of type Player. + + + + + + + + Clothing + 20 + + + + + + Smoke + Effects + Makes the parent part or model object emit smoke + 30 + 59 + BasePart + + + + + + Trail + Effects + Makes two attachments emit trail when moving + 30 + 93 + Model + + + + + LightEmission + 0 + 1 + + + + + + LightInfluence + 0 + 1 + + + + + + ZOffset + -1 + 1 + + + + + + Lifetime + 0 + 20 + + + + + + TextureLength + 0 + 5 + 40 + + + + + + MinLength + 0 + 1 + + + + + + + + + Beam + Effects + Makes beam between two attachments + 30 + 96 + BasePart + + + + + LightEmission + 0 + 1 + + + + + + LightInfluence + 0 + 1 + + + + + + TextureSpeed + -1 + 1 + + + + + + TextureLength + 0 + 5 + 40 + + + + + + CurveSize0 + -10 + 10 + + + + + + CurveSize1 + -10 + 10 + + + + + + ZOffset + -1 + 1 + + + + + + + + SurfaceAppearance + 3D Interfaces + Overrides the visual appearance of its parent MeshPart + 40 + 10 + + + + + AlphaMode + Determines how the ColorMap's alpha channel behaves. Not scriptable at runtime. + + + + + ColorMap + Image ID used to color the surface. If left empty, surface uses Part Color. Not scriptable at runtime. + + + + + MetalnessMap + Grayscale Image ID that determines the metalness of the surface. Not scriptable at runtime. + + + + + NormalMap + Image ID of the normal map used to control surface bumps and curvature. Not scriptable at runtime. + + + + + RoughnessMap + Grayscale Image ID that determines the roughness of the surface. Not scriptable at runtime. + + + + + TexturePack + Internal asset link + + + + + + + ParticleEmitter + Effects + A generic particle system. + 30 + 80 + BasePart + + + + + LightEmission + 0 + 1 + + + + + LightInfluence + Specifies the amount of influence lighting has on the particle emmitter. A value of 0 is unlit, 1 is fully lit. Fractional values blend from unlit to lit. + 0 + 1 + + + + + + Drag + 0 + 5 + + + + + VelocityInheritance + 0 + 1 + + + + + Rate + 0 + 100 + 100 + + + + + + Rotation + -180 + 180 + 72 + + + + + RotSpeed + -360 + 360 + 72 + + + + + Speed + 0 + 100 + 100 + + + + + Lifetime + 0 + 5 + + + + + + + + Sparkles + Effects + Makes the parent part or model object fantastic + 30 + 42 + BasePart + + + + + Explosion + Effects + 30 + 36 + Creates an Explosion! This can be used as a purely graphical effect, or can be made to damage objects. + BasePart + + + + + BlastPressure + How much force this Explosion exerts on objects within it's BlastRadius. Setting this to 0 creates a purely graphical effect. A larger number will cause Parts to fly away at higher velocities. + + + + + BlastRadius + How big the Explosion is. This is a circle starting from the center of the Explosion's Position, the larger this property the larger the circle of destruction. + + + + + Position + Where the Explosion occurs in absolute world coordinates. + + + + + ExplosionType + Defines the behavior of the Explosion. <a href='http://wiki.roblox.com/index.php/ExplosionType' target='_blank'>More info</a> + + + + + + + Fire + Effects + Makes the parent part or model object emit fire + 30 + 61 + BasePart + + + + + Color + The color of the base of the fire. See SecondaryColor for more. + + + + + Heat + How hot the fire appears to be. The flame moves quicker the higher this value is set. + + + + + SecondaryColor + The color the fire interpolates to from Color. The longer a particle exists in the fire, the close to this color it becomes. + + + + + Size + How large the fire appears to be. + + + + + + + Seat + Interaction + 30 + 35 + + + + + Platform + + Equivalent to a seat, except that the character stands up rather than sits down. + 30 + 35 + + + + + SkateboardPlatform + true + 30 + 35 + + + + + VehicleSeat + Interaction + Automatically finds and powers hinge joints in an assembly. Ignores motors. + 30 + 35 + Model + + + + + Tool + Interaction + 30 + 17 + StarterPack + + + + + Flag + true + 30 + 38 + + + + + CanBeDropped + If someone is carrying this flag, this bool determines whether or not they can drop it and run. + + + + + TeamColor + The Team this flag is for. Corresponds with the TeamColors in the Teams service. + + + + + + + FlagStand + true + 30 + 39 + + + + + BackpackItem + 20 + + + + + Decal + 3D Interfaces + 40 + 7 + Describes a texture that is placed on one of the sides of the Part it is parented to. + BasePart + + + + + Face + Describes the face of the Part the decal will be applied to. <a href='http://wiki.roblox.com/index.php/NormalId' target='_blank'>More info</a> + + + + + Shiny + How much light will appear to reflect off of the decal. + + + + + Specular + How light will react to the surface of the decal. + + + + + Transparency + How visible the decal is. 1 is completely invisible, while 0 is completely opaque + 0 + 1 + + + + + + + JointInstance + 200 + 34 + + + + + Active + Read-only boolean, true if the joint is active in world. Rigid joints may be inactive if they are redundant or form cycles. + + + + + + + Message + 110 + 33 + true + StarterGui + + + + + Hint + true + 110 + 33 + + + + + IntValue + Values + 30 + 4 + Stores a int value in it's Value member. Useful to share int information across multiple scripts. + + + + + RayValue + Values + 30 + 4 + Stores a Ray value in it's Value member. Useful to share Ray information across multiple scripts. + + + + + IntConstrainedValue + true + Values + 30 + 4 + Stores an int value in it's Value member. Value is clamped to be in range of Min and MaxValue. Useful to share int information across multiple scripts. + + + + MaxValue + The maximum we allow this Value to be set. If Value is set higher than this, it automatically gets adjusted to MaxValue + + + + + MinValue + The minimum we allow this Value to be set. If Value is set lower than this, it automatically gets adjusted to MinValue + + + + + + DoubleConstrainedValue + true + Values + 30 + 4 + Stores a double value in it's Value member. Value is clamped to be in range of Min and MaxValue. Useful to share double information across multiple scripts. + + + + + MaxValue + The maximum we allow this Value to be set. If Value is set higher than this, it automatically gets adjusted to MaxValue + + + + + MinValue + The minimum we allow this Value to be set. If Value is set lower than this, it automatically gets adjusted to MinValue + + + + + + + BoolValue + Values + 30 + 4 + Stores a boolean value in it's Value member. Useful to share boolean information across multiple scripts. + + + + + CustomEvent + 30 + true + 4 + + + + + CustomEventReceiver + 30 + true + 4 + + + + + FloorWire + true + 30 + 4 + Renders a thin cylinder than can be adorned with textures that 'flow' from one object to the next. Has basic pathing abilities and attempts to to not intersect anything. <a href='http://wiki.roblox.com/index.php/FloorWire_Guide' target='_blank'>More info</a> + + + + + CycleOffset + Controls how the decals are positioned along the wire. <a href='http://wiki.roblox.com/index.php/CycleOffset' target='_blank'>More info</a> + + + + + From + The object the FloorWire 'emits' from + + + + + StudsBetweenTextures + The space between two textures on the wire. Note: studs are relative depending on how far the camera is from the FloorWire. + + + + + Texture + The image we use to render the textures that flow from beginning to end of the FloorWire. + + + + + TextureSize + The size in studs of the Texture we use to flow from one object to the next. + + + + + To + The object the FloorWire 'emits' to + + + + + Velocity + The rate of travel that the textures flow along the wire. + + + + + WireRadius + How thick the wire is. + + + + + + + NumberValue + Values + 30 + 4 + + + + + StringValue + Values + 30 + 4 + + + + + Vector3Value + Values + 30 + 4 + + + + + CFrameValue + Values + 30 + 4 + Stores a CFrame value in it's Value member. Useful to share CFrame information across multiple scripts. + + + + + Color3Value + Values + 30 + 4 + Stores a Color3 value in it's Value member. Useful to share Color3 information across multiple scripts. + + + + + BrickColorValue + Values + 30 + 4 + Stores a BrickColor value in it's Value member. Useful to share BrickColor information across multiple scripts. + + + + + ValueBase + Values + 30 + 4 + The base class to all Value Objects. + + + + + ObjectValue + Values + 30 + 4 + + + + + SpecialMesh + Meshes + 30 + 8 + BasePart + + + + + BlockMesh + Meshes + 30 + 8 + BasePart + + + + + CylinderMesh + Meshes + 30 + 8 + BasePart + true + + + + + BevelMesh + Meshes + false + true + + + + + DataModelMesh + false + + + + + + Texture + 3D Interfaces + 40 + 10 + BasePart + + + + + Sound + Sounds + 10 + 11 + + + + + play + true + Deprecated. Use Play() instead + + + + + + + PlayOnRemove + The sound will play when it is removed from the Workspace. Looped sounds don't play + + + + + + + + EchoSoundEffect + An echo audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + Delay + 0.1 + 5 + 100 + + + + + Feedback + 0 + 1 + 100 + + + + + DryLevel + -80 + 10 + 100 + + + + + WetLevel + -80 + 100 + 100 + + + + + + + + FlangeSoundEffect + A Flanging audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + Mix + 0 + 1 + 100 + + + + + Depth + 0.01 + 1 + 100 + + + + + Rate + 0 + 20 + 100 + + + + + + + + DistortionSoundEffect + A Distortion audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + Level + 0 + 1 + 100 + + + + + + + + PitchShiftSoundEffect + A Pitch Shifting audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + Octave + 0.5 + 2 + 100 + + + + + + + + ChorusSoundEffect + A Chorus audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + Mix + 0 + 1 + 100 + + + + + Rate + 0 + 20 + 100 + + + + + Depth + 0 + 1 + 100 + + + + + + + + TremoloSoundEffect + A Tremolo audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + Frequency + 0.1 + 20 + 100 + + + + + Depth + 0 + 1 + 100 + + + + + Duty + 0 + 1 + 100 + + + + + + + + ReverbSoundEffect + A Reverb audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + DecayTime + 0.1 + 20 + 100 + + + + + Diffusion + 0 + 1 + 100 + + + + + Density + 0 + 1 + 100 + + + + + DryLevel + -80 + 20 + 100 + + + + + WetLevel + -80 + 20 + 100 + + + + + + + + EqualizerSoundEffect + An Three-band Equalizer audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + LowGain + -80 + 10 + 100 + + + + + MidGain + -80 + 10 + 100 + + + + + HighGain + -80 + 10 + 100 + + + + + + + + CompressorSoundEffect + A Compressor audio effect that can be applied to a Sound or SoundGroup. + Sounds + 20 + 84 + Sound + + + + + Threshold + -80 + 0 + 100 + + + + + Attack + 0.001 + 1 + 100 + + + + + Release + 0.001 + 5 + 100 + + + + + Ratio + 1 + 50 + 100 + + + + + GainMakeup + 0 + 30 + 100 + + + + + + + + SoundGroup + Sounds + 20 + 85 + SoundService + + + + + + + + + StockSound + false + -1 + + + + + SoundService + 500 + 31 + + + + + + AmbientReverb + + The ambient sound environment. May not work when using hardware sound + + + + + DopplerScale + + The doppler scale is a general scaling factor for how much the pitch varies due to doppler shifting in 3D sound. Doppler is the pitch bending effect when a sound comes towards the listener or moves away from it, much like the effect you hear when a train goes past you with its horn sounding. With dopplerscale you can exaggerate or diminish the effect. + + + + + DistanceFactor + + the relative distance factor, compared to 1.0 meters. + + + + + RolloffScale + + Setting this value makes the sound drop off faster or slower. The higher the value, the faster volume will attenuate, and conversely the lower the value, the slower it will attenuate. For example a rolloff factor of 1 will simulate the real world, where as a value of 2 will make sounds attenuate 2 times quicker. + + + + + + + Backpack + 30 + 20 + false + + + + + StarterPack + 30 + 20 + + + + + StarterPlayer + 30 + 79 + + + + + StarterGear + 30 + 20 + false + + + + + + CoreGui + 30 + 46 + + + + + + CorePackages + 30 + 20 + + + + + + RobloxPluginGuiService + 30 + 46 + + + + + + PluginGuiService + 30 + 46 + + + + + + PluginDebugService + 30 + 46 + + + + + + Studio + + + + + Show Plugin GUI Service in Explorer + + + + + + + + UIGridStyleLayout + GUI + false + GuiBase2d + + + + + + SetCustomSortFunction + When SortOrder is set to Custom, this lua function is used to determine the ordering of elements. Function should take two arguments (each will be an Instance child to compare), and return true if a comes before b, otherwise return false. In other words, use this function the same way you would use a table.sort function. The sorting should be deterministic, otherwise sort will fail and fall back to name order. + true + + + + + ApplyLayout + Forces a relayout of all elements. Useful when sort is set to Custom. + + + + + + + + SortOrder + Determines how we decide which element to place next. Can be Name or Custom. If using Custom, make sure SetCustomSortFunction was called with an appropriate sort function. + + + + + FillDirection + Determines which direction to fill the grid. Can be Horizontal or Vertical. + + + + + HorizontalAlignment + Determines how grid is placed within it's parent's container in the x direction. Can be Left, Center, or Right. + + + + + VerticalAlignment + Determines how grid is placed within it's parent's container in the y direction. Can be Top, Center, or Bottom. + + + + + + + + UIListLayout + 30 + 26 + GUI + Sets the position of UI elements in a list. You can use a UIListLayout by parenting it to a GuiObject. The UIListLayout will then apply itself to all of its GuiObject siblings. + GuiBase2d + + + + + + Padding + Determines the amount of free space between each element. Can be set either using scale (Percentage of parent's size in the current direction) or offset (a static spacing value, similar to pixel size). + + + + + + + + UIGridLayout + 30 + 26 + GUI + Sets the position of UI elements in a 2D grid (this can be modified to 1D grid for list layout). This will also set the elements to a particular size, although this can be overridden with particular constraints on elements. You can use a UIGridLayout by parenting it to a GuiObject. The UIGridLayout will then apply itself to all of its GuiObject siblings. + GuiBase2d + + + + + + CellSize + Denotes what size each element should be. Can be overridden by elements using constraints on individual elements. + + + + + CellPadding + How much space between elements there should be. + + + + + FillDirectionMaxCells + Determines how many cells over in the FillDirection we go before starting a new row or column. Set to 0 for max cell count. Will be clamped if this is set higher than the parent container allows room for. + + + + + AbsoluteSize + Returns the current size of the grid. If more elements are added, this can increase. If elements are removed this can decrease. + + + + + StartCorner + Which corner we start laying the elements out from. Can be TopLeft, TopRight, BottomLeft, BottomRight. + + + + + + + + + UIPageLayout + 30 + 26 + GUI + Creates a paged viewing window, like the home screen of a mobile device. You can use a UIPageLayout by parenting it to a GuiObject. The UIPageLayout will then apply itself to all of its GuiObject siblings. + GuiBase2d + + + + + + CurrentPage + The page that is either currently being displayed or is the target of the current animation. + + + + + + Circular + Whether or not the page layout wraps around at the ends. + + + + + + Padding + Determines the amount that pages are separated from each other by. Can be set either using scale (Percentage of parent's size in the current direction) or offset (a static spacing value, similar to pixel size). + + + + + + Animated + Whether or not to animate transitions between pages. + + + + + + EasingStyle + The easing style to use when performing an animation. + + + + + + EasingDirection + The easing direction to use when performing an animation. + + + + + + TweenTime + The length of the animation. + + + + + + + + Next + Sets CurrentPage to the page after the current page and animates to it, or does nothing if there isn't a next page. + + + + + Previous + Sets CurrentPage to the page after the current page and animates to it, or does nothing if there isn't a next page. + + + + + JumpTo + If the instance is in the layout, then it sets CurrentPage to it and animtes to it. If circular layout is set, it will take the shortest path. + + + + + JumpToIndex + If the index is >= 0 and less than the size of the layout, acts like JumpTo. If it's out of bounds and circular is set, it will animate the full distance between the in-bounds index of CurrentPage and the new index. + + + + + + + + PageEnter + Fires when a page comes into view, and is going to be rendered. + + + + + PageLeave + Fires when a page leaves view, and will not be rendered. + + + + + Stopped + Fires when an animation to CurrentPage is completed without being cancelled, and the view stops scrolling. + + + + + + + + UITableLayout + 30 + 26 + GUI + Provides a layout of rows and columns that are sized based on the cells in them. + GuiBase2d + + + + + + Padding + The amount of padding to insert in between the cells of the table. + + + + + + FillEmptySpaceRows + Whether the table should expand to fill the available space of its container, row-wise. + + + + + + FillEmptySpaceColumns + Whether the table should expand to fill the available space of its container, column-wise. + + + + + + MajorAxis + Whether the direct siblings are considered the rows or the columns. The children of the direct siblings are the columns or rows, respectively. + + + + + + + + UISizeConstraint + 30 + 26 + GUI + Ensures a GuiObject does not become smaller or larger than the min and max size. If an element with a constraint is under the control of a layout, the constraint takes precedence in determining the element’s size, but not position. You can use a Constraint by parenting it to the element you wish to constrain. + GuiBase2d + + + + + + MinSize + The smallest size the GuiObject is allowed to be. + + + + + MaxSize + The biggest size the GuiObject is allowed to be. + + + + + + + + UITextSizeConstraint + 30 + 26 + GUI + Ensures a GuiObject with text does not allow the font size to become larger or smaller than min and max text sizes. If an element with a constraint is under the control of a layout, the constraint takes precedence in determining the element’s size, but not position. You can use a Constraint by parenting it to the element you wish to constrain. + GuiBase2d + + + + + + MinTextSize + The smallest size the font is allowed to be. + + + + + MaxTextSize + The biggest size the font is allowed to be. + + + + + + + + UIAspectRatioConstraint + 30 + 26 + GUI + Ensures a GuiObject will always have a particular aspect ratio. If an element with a constraint is under the control of a layout, the constraint takes precedence in determining the element’s size, but not position. You can use a Constraint by parenting it to the element you wish to constrain. + + + + + + AspectRatio + The aspect ratio to maintain. This is the width/height. Only positive numbers allowed. + + + + + AspectType + Describes how the aspect ratio will determine its size. Options are FitWithinMaxSize, ScaleWithParentSize. FitWithinMaxSize will make the element the maximum size it can be within the current possible AbsoluteSize of the element while maintaining the AspectRatio. ScaleWithParentSize will make the element the closest to the parent element’s maximum size while maintaining aspect ratio. + + + + + DominantAxis + Describes which axis to use when determining the new size of the element, while keeping respect to the aspect ratio. + + + + + + + + UIScale + 30 + 26 + GUI + Uniformly scales a GUI object and all its children. + GuiBase2d + + + + + + Scale + The scale factor to apply. + + + + + + + + UIPadding + 30 + 26 + GUI + Insets the children of the GuiObject this is parented to, by the specified padding. + GuiBase2d + + + + + + PaddingLeft + The padding to apply on the left side relative to the parent's normal size. + + + + + PaddingRight + The padding to apply on the right side relative to the parent's normal size. + + + + + PaddingTop + The padding to apply on the top side relative to the parent's normal size. + + + + + PaddingBottom + The padding to apply on the bottom side relative to the parent's normal size. + + + + + + + + UIGradient + 30 + 26 + GUI + Apply a gradient to the parent GuiObject. + GuiBase2d + + + + + + Color + The (sequence of) color3 of the gradient. + + + + + + Transparency + The (sequence of) transparency of the gradient. + + + + + + Rotation + Clockwise rotation in degrees. + + + + + + Offset + Offset of gradient center in scale. + + + + + + + + UICorner + 30 + 26 + GUI + Modify corner properties of parent GuiObject. + GuiBase2d + + + + + CornerRadius + Round corner with specified radius. + + + + + + + + TweenBase + false + + + + + + PlaybackState + The current state of how the tween is animating. Possible values are Begin, Playing, Paused, Completed and Cancelled. This property is modified by using functions such as Tween:Play(), Tween:Pause(), and Tween:Cancel(). Read-only. + + + + + + + + Play + Starts or resumes (if Tween.PlaybackState is Paused) the tween animation. If current PlaybackState is Cancelled, this property will reset the tween to the beginning properties and play the animations from the beginning. + + + + + Pause + Temporarily stops the tween animation. Animation can be resumed by calling Play(). + + + + + Cancel + Stops the tween animation. Animation can be restarted by calling Play(). Animation will start from the beginning values. + + + + + + + + Completed + Fires when the tween either reaches PlaybackState Completed or Cancelled. PlaybackState of one of these types is passed as the first arg to the function listening to this event. + + + + + + + + + Tween + An object linked to an instance that animates properties on the instance over a specified period of time. Useful for easily moving UI objects around, rotating objects, etc. without having to write a lot of code. To create a new tween, please use TweenService:Create. + + + + + + Instance + The object this tween is operating on. Read-only. + + + + + + TweenInfo + Specifies how the tween animates. Read-only. + + + + + + + + + TweenService + Service responsible for creating tweens on instances. + + + + + + + Create + Creates a Tween object bound to a particular Instance. The first arg is the Instance to tween. The second arg is a TweenInfo struct, which specifies how a tween should behave. The third arg is a table, which should specify the properties to tween as keys, with the end value specified as values to the keys. + + + + + GetValue + Transforms a linear alpha to a given EasingStyle and EasingDirection. + + + + + + + + + + StarterGui + 30 + 46 + + + + + + SetCoreGuiEnabled + Will stop/begin certain core gui elements being rendered. See CoreGuiType for core guis that can be modified. + + + + + GetCoreGuiEnabled + Returns a boolean describing whether a CoreGuiType is currently being rendered. + + + + + + + + GuiService + The GuiService is a special service, which currently allows developers to control what GuiObject is currently being selected by the Gamepad Gui navigator, and allows clients to check if Roblox's main menu is currently open. This service has a lot of hidden members, which are mainly used internally by Roblox's CoreScripts. + + + + + + GetGuiInset + Returns a Tuple containing two Vector2 values representing the offset of user GUIs in pixels from the top right corner of the screen and the bottom right corner of the screen respectively. + + + + + + + + ContextActionService + A service used to bind input to various lua functions. + + + + + + BindAction + Binds 'functionToBind' to fire when any 'inputTypes' happen. InputTypes can be variable in number and type. Types can be Enum.KeyCode, single character strings corresponding to keys, or Enum.UserInputType. 'actionName' is a key used by many other ContextActionService functions to query state. 'createTouchButton' if true will create a button on screen on touch devices. This button will fire 'functionToBind' with three arguments: first argument is the actionName, second argument is the UserInputState of the input, and the third is the InputObject that fired this function. If 'functionToBind' yields or returns nil or Enum.ContextActionResult.Sink, the input will be sunk. If it returns Enum.ContextActionResult.Pass, the next bound action in the stack will be invoked. + + + + + SetTitle + If 'actionName' key contains a bound action, then 'title' is set as the title of the touch button. Does nothing if a touch button was not created. No guarantees are made whether title will be set when button is manipulated. + + + + + SetDescription + If 'actionName' key contains a bound action, then 'description' is set as the description of the bound action. This description will appear for users in a listing of current actions availables. + + + + + SetImage + If 'actionName' key contains a bound action, then 'image' is set as the image of the touch button. Does nothing if a touch button was not created. No guarantees are made whether image will be set when button is manipulated. + + + + + SetPosition + If 'actionName' key contains a bound action, then 'position' is set as the position of the touch button. Does nothing if a touch button was not created. No guarantees are made whether position will be set when button is manipulated. + + + + + UnbindAction + If 'actionName' key contains a bound action, removes function from being called by all input that it was bound by (if function was also bound by a different action name as well, those bound input are still active). Will also remove any touch button created (if button was manipulated manually there is no guarantee it will be cleaned up). + + + + + UnbindAllActions + Removes all functions bound. No actionNames will remain. All touch buttons will be removed. If button was manipulated manually there is no guarantee it will be cleaned up. + + + + + GetBoundActionInfo + Returns a table with info regarding the function bound with 'actionName'. Table has the keys 'title' (current title that was set with SetTitle) 'image' (image set with SetImage) 'description' (description set with SetDescription) 'inputTypes' (tuple containing all input bound for this 'actionName') 'createTouchButton' (whether or not we created a touch button for this 'actionName'). + + + + + GetAllBoundActionInfo + Returns a table with all bound action info. Each entry is a key with 'actionName' and value being the same table you would get from ContextActionService:GetBoundActionInfo('actionName'). + + + + + + + + GetButton + If 'actionName' key contains a bound action, then this will return the touch button (if was created). Returns nil if a touch button was not created. No guarantees are made whether button will be retrievable when button is manipulated. + + + + + + + + PointsService + A service used to query and award points for Roblox users using the universal point system. + true + + + + + + PointsAwarded + Fired when points are successfully awarded 'userId'. Also returns the updated balance of points for usedId in universe via 'userBalanceInUniverse', total points via 'userTotalBalance', and the amount points that were awarded via 'pointsAwarded'. This event fires on the server and also all clients in the game that awarded the points. + + + + + + + + AwardPoints + Will attempt to award the 'amount' points to 'userId', returns 'userId' awarded to, the number of points awarded, the new point total the user has in the game, and the total number of points the user now has. Will also fire PointsService.PointsAwarded. Works with server scripts ONLY. + + + + + GetPointBalance + Returns the overall balance of points that player with userId has (the sum of all points across all games). Works with server scripts ONLY. + + + + + GetGamePointBalance + Returns the balance of points that player with userId has in the current game (all placeID points combined within the game). Works with server scripts ONLY. + + + + + GetAwardablePoints + Returns the number of points the current universe can award to players. Works with server scripts ONLY. + + + + + + + + Chat + 510 + 33 + + + + + + + + + + ChatService + 510 + 33 + + + + + + + + LocalizationTable + 30 + 97 + Localization + A database of strings used in the game and their translations. + + + + + + LocalizationService + 530 + 92 + + + + PreferredLanguage + Gets the system's preferred language (A Language enum). + + + + + GetLocaleId + Gets the system's LocaleId (Ex: "en-US"). + + + + + + + MarketplaceService + 46 + + + + + + PromptPurchase + Will prompt 'player' to purchase the item associated with 'assetId'. 'equipIfPurchased' is an optional argument that will give the item to the player immediately if they buy it (only applies to gear). 'currencyType' is also optional and will attempt to prompt the user with a specified currency if the product can be purchased with this currency, otherwise we use the default currency of the product. + + + + + + + + GetProductInfo + Takes one argument "assetId" which should be a number of an asset on www.roblox.com. Returns a table containing the product information (if this process fails, returns an empty table). + + + + + PlayerOwnsAsset + Checks to see if 'Player' owns the product associated with 'assetId'. Returns true if the player owns it, false otherwise. This call will produce a warning if called on a guest player. + + + + + + + ProcessReceipt + Callback that is executed for pending Developer Product receipts. + <p>If this function does not return Enum.ProductPurchaseDecision.PurchaseGranted, then you will not be granted the money for the purchase!</p> + <p>The callback will be invoked with a table, containing the following informational fields:</p> + <ul> + <li>PlayerId: int64 - the id of the player making the purchase.</li> + <li>PlaceIdWherePurchased: int64: - the specific place where the purchase was made.</li> + <li>PurchaseId: string - a unique identifier for the purchase, should be used to prevent granting an item multiple times for one purchase.</li> + <li>ProductId: int64 - the id of the purchased product.</li> + <li>CurrencyType: CurrencyType - the type of currency used (Tix, Robux).</li> + <li>CurrencySpent: int - the amount of currency spent on the product for this purchase.</li> + </ul> + + + + + + + + PromptPurchaseFinished + Fired when a 'player' dismisses a purchase dialog for 'assetId'. If the player purchased the item 'isPurchased' will be true, otherwise it will be false. This call will produce a warning if called on a guest player. + + + + + + + + UserInputService + + + + + TouchEnabled + Returns true if the local device accepts touch input, false otherwise. + + + + + KeyboardEnabled + Returns true if the local device accepts keyboard input, false otherwise. + + + + + MouseEnabled + Returns true if the local device accepts mouse input, false otherwise. + + + + + AccelerometerEnabled + Returns true if the local device has an accelerometer, false otherwise. + + + + + GyroscopeEnabled + Returns true if the local device has an gyroscope, false otherwise. + + + + + + + + TouchTap + Fired when a user taps their finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the tap gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchPinch + Fired when a user pinches their fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the pinch gesture. 'scale' is a float that indicates the difference from the beginning of the pinch gesture. 'velocity' is a float indicating how quickly the pinch gesture is happening. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchSwipe + Fired when a user swipes their fingers on a TouchEnabled device. 'swipeDirection' is an Enum.SwipeDirection, indicating the direction the user swiped. 'numberOfTouches' is an int that indicates how many touches were involved with the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchLongPress + Fired when a user holds at least one finger for a short amount of time on the same screen position on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchRotate + Fired when a user rotates two fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'rotation' is a float indicating how much the rotation has gone from the start of the gesture. 'velocity' is a float that indicates how quickly the gesture is being performed. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchPan + Fired when a user drags at least one finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'totalTranslation' is a Vector2, indicating how far the pan gesture has gone from its starting point. 'velocity' is a Vector2 that indicates how quickly the gesture is being performed in each dimension. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + + TouchStarted + Fired when a user places their finger on a TouchEnabled device. 'touch' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchMoved + Fired when a user moves their finger on a TouchEnabled device. 'touch' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchEnded + Fired when a user moves their finger on a TouchEnabled device. 'touch' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + + InputBegan + Fired when a user begins interacting via a Human-Computer Interface device (Mouse button down, touch begin, keyboard button down, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + InputChanged + Fired when a user changes interacting via a Human-Computer Interface device (Mouse move, touch move, mouse wheel, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + InputEnded + Fired when a user stops interacting via a Human-Computer Interface device (Mouse button up, touch end, keyboard button up, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. This event will always fire regardless of game state. + + + + + + TextBoxFocused + Fired when a user clicks/taps on a textbox to begin text entry. Argument is the textbox that was put in focus. This also fires if a textbox forces focus on the user. This event only fires locally. + + + + + TextBoxFocusReleased + Fired when a user stops text entry into a textbox (usually by pressing return or clicking/tapping somewhere else on the screen). Argument is the textbox that was taken out of focus. This event only fires locally. + + + + + DeviceAccelerationChanged + Fired when a user moves a device that has an accelerometer. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. This event only fires locally. + + + + + DeviceGravityChanged + Fired when the force of gravity changes on a device that has an accelerometer. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. This event only fires locally. + + + + + DeviceRotationChanged + Fired when a user rotates a device that has an gyroscope. This is fired with an InputObject, which has type Enum.InputType.Gyroscope, and position that shows total rotation in each local device axis. The delta property describes the amount of rotation that last happened. A second argument of Vector4 is the device's current quaternion rotation in reference to it's default reference frame. This event only fires locally. + + + + + + + + GetDeviceAcceleration + Returns an InputObject that describes the device's current acceleration. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. The delta property describes the amount of rotation that last happened. This event only fires locally. + + + + + GetDeviceGravity + Returns an InputObject that describes the device's current gravity vector. This is fired with an InputObject, which has type Enum.InputType.Accelerometer, and position that shows the g force in each local device axis. The delta property describes the amount of rotation that last happened. This event only fires locally. + + + + + GetDeviceRotation + Returns an InputObject and a Vector4 that describes the device's current rotation vector. This is fired with an InputObject, which has type Enum.InputType.Gyroscope, and position that shows total rotation in each local device axis. The delta property describes the amount of rotation that last happened. The Vector4 is the device's current quaternion rotation in reference to it's default reference frame. This event only fires locally. + + + + + + + + Atmosphere + Environment + 5 + 28 + Lighting + + + + + Density + 0 + 1 + 1000 + A value that controls the air particulates density. The proportion of airborne particles per unit of view depth between the camera and the sky. + + + + + Offset + 0 + 1 + 1000 + A value that offsets the quantity of light scattering events among particles in the atmosphere between the camera and the sky. + + + + + Height + 0 + 3 + 1000 + A value that controls the height of fog in the atmosphere above the horizon (not volumetric...yet...). + + + + + Color + A Color3 value that changes the hue of the atmosphere, mixed with the sky color. + + + + + Decay + A Color3 value for the the hue the atmosphere takes on away from the sun towards the horizon, mixed with the sky color (aka extinction). + + + + + Haze + 0 + 10 + 1000 + A value that controls the haziness of the atmosphere between the camera and the sky (aka turbidity). + + + + + Glare + 0 + 10 + 1000 + A value that increases the glow of the atmosphere around light the sun. + + + + + + + + Sky + Environment + 5 + 28 + Lighting + + + + + ColorCorrectionEffect + Post Processing Effects + 20 + 83 + + + + + Brightness + -1 + 1 + + + + + Contrast + -1 + 1 + + + + + Saturation + -1 + 1 + + + + + + + BloomEffect + Post Processing Effects + 20 + 83 + + + + + Intensity + 0 + 1 + + + + + Threshold + 0.8 + 4 + 1000 + + + + + Size + 0 + 56 + 56 + + + + + + + + BlurEffect + Post Processing Effects + 20 + 83 + + + + + Size + 0 + 56 + 56 + + + + + + + + DepthOfFieldEffect + Post Processing Effects + 20 + 83 + Depth based blur (Depth of Field) effect. + + + + + FocusDistance + 0 + 200.0 + 10000 + Controls the distance in stud units away from the camera where are in focus. + + + + + InFocusRadius + 0 + 50.0 + 10000 + Controls the distance in stud units away from the FocusDistance where both in front and behind objects can begin to blur. + + + + + NearIntensity + 0 + 1 + 10000 + Maximum intensity of the near blur -the higher the Intensity, the stronger the blur is allowed to reach. + + + + + FarIntensity + 0 + 1 + 10000 + Maximum intensity of the far blur -the higher the Intensity, the stronger the blur is allowed to reach. + + + + + + + + SunRaysEffect + Post Processing Effects + 20 + 83 + + + + + Intensity + 0 + 1 + 1000 + + + + + Spread + 0 + 1 + 1000 + + + + + + + + Motor + 20 + false + BasePart + + + + + Humanoid + Avatar + 30 + 9 + Model + + + + + + MoveTo + Attempts to move the Humanoid and it's associated character to 'part'. 'location' is used as an offset from part's origin. + + + + + Jump + + + + + Sit + + + + + TakeDamage + Decreases health by the amount. Use this instead of changing health directly to make sure weapons are filtered for things such as ForceField(s). + + + + + UnequipTools + + Takes any active gear/tools that the Humanoid is using and puts them into the backpack. This function only works on Humanoids with a corresponding Player. + + + + + EquipTool + + Takes a specified tool and equips it to the Humanoid's Character. Tool argument should be of type 'Tool'. + + + + + ReplaceBodyPartR15 + Replaces the desired bodypart on the Humanoid's Character using a specified Enum.BodyPartR15 and BasePart. Returns a success boolean. + + + + + GetBodyPartR15 + Returns a Enum.BodyPartR15 given a body part in the Humanoid's Character. + + + + + + + NameOcclusion + + Sets how to display other humanoid names to this humanoid's player. <a href='http://wiki.roblox.com/index.php/NameOcclusion' target='_blank'>More info</a> + + + Health + How many hit points the Humanoid has. When this number reaches 0 or goes below 0, the Humanoid's character falls apart and will respawn. + + + MaxHealth + The maximum number of hit points a Humanoid's health can reach. If the Humanoid's health is set over this amount, the health gets set to this value. + + + TargetPoint + The location that the Humanoid is trying to walk to. + + + Torso + Humanoid.RootPart will be the preferred way of getting a character's humanoid root part. + true + + + LeftLeg + In R6 this property get the player's left leg. In R15 this gets nothing. + true + + + RightLeg + In R6 this property get the player's right leg. In R15 this gets nothing. + true + + + CollisionType + An emum that selects the collision type for R15 and Rthro characters. InnerBox is classic style collisions for all characters, OuterBox is dynamically sized collisions based on Mesh size. + + + + + + + BodyColors + Avatar + 20 + Model + + + + + Shirt + Avatar + 20 + 43 + Model + + + + + Pants + Avatar + 20 + 44 + Model + + + + + ShirtGraphic + Avatar + 20 + 40 + Model + + + + + Skin + true + 20 + + + + + DebugSettings + false + 20 + + + + + FaceInstance + false + + + + + GameSettings + false + 20 + + + + + GlobalSettings + false + 20 + + + + + Item + false + 20 + + + + + NetworkPeer + false + + + + + NetworkSettings + false + 20 + + + + + PVInstance + false + + + + + CoordinateFrame + true + Deprecated. Use CFrame instead + + + + + + + PackageLink + 1 + 98 + false + + + + + Status + Current status of the Package + true + + + + + + + RenderSettings + false + 20 + + + + + RootInstance + false + + + + + ServiceProvider + false + + + + + service + true + Use GetService() instead + + + + + GetService + Instance:isService:0 + + + + + FindService + Instance:isService:0 + + + + + + + ProfilingItem + false + + + + + NetworkMarker + false + + + + + + Hopper + true + Use StarterPack instead + 20 + + + + + + Instance + false + + + + + + Archivable + Determines whether or not an Instance can be saved when the game closes/attempts to save the game. Note: this only applies to games that use Data Persistence, or SavePlaceAsync. + + + + + ClassName + The string name of this Instance's most derived class. + + + + + Parent + The Instance that is directly above this Instance in the tree. + + + + + + + + + GetDebugId + false + This function is for internal testing. Don't use in production code + + + + + Clone + Returns a copy of this Object and all its children. The copy's Parent is nil + + + + + clone + true + Use Clone() instead + + + + + isA + true + Use IsA() instead + + + + + IsA + Returns a boolean if this Instance is of type 'className' or a is a subclass of type 'className'. If 'className' is not a valid class type in ROBLOX, this function will always return false. <a href='http://wiki.roblox.com/index.php/IsA' target='_blank'>More info</a> + Instance:Any:0 + + + + + FindFirstChild + Returns the first child of this Instance that matches the first argument 'name'. The second argument 'recursive' is an optional boolean (defaults to false) that will force the call to traverse down thru all of this Instance's descendants until it finds an object with a name that matches the 'name' argument. The function will return nil if no Instance is found. + + + + + FindFirstChildOfClass + Returns the first child of this Instance that with a ClassName equal to 'className'. The function will return nil if no Instance is found. + Instance:isScriptCreatable:0 + + + + + FindFirstChildWhichIsA + Returns the first child of this Instance that :IsA(className). The second argument 'recursive' is an optional boolean (defaults to false) that will force the call to traverse down thru all of this Instance's descendants until it finds an object with a name that matches the 'className' argument. The function will return nil if no Instance is found. + Instance:Any:0 + + + + + FindFirstAncestor + Returns the first ancestor of this Instance that matches the first argument 'name'. The function will return nil if no Instance is found. + + + + + FindFirstAncestorOfClass + Returns the first ancestor of this Instance with a ClassName equal to 'className'. The function will return nil if no Instance is found. + Instance:isScriptCreatable:0 + + + + + FindFirstAncestorWhichIsA + Returns the first ancestor of this Instance that :IsA(className). The function will return nil if no Instance is found. + Instance:Any:0 + + + + + GetFullName + Returns a string that shows the path from the root node (DataModel) to this Instance. This string does not include the root node (DataModel). + + + + + children + true + Use GetChildren() instead + + + + + getChildren + true + Use GetChildren() instead + + + + + GetChildren + Returns a read-only table of this Object's children + + + + + GetDescendants + Returns an array containing all of the descendants of the instance. Returns in preorder traversal, or in other words, where the parents come before their children, depth first. + + + + + Remove + Deprecated. Use ClearAllChildren() to get rid of all child objects, or Destroy() to invalidate this object and its descendants + true + + + + + remove + true + Use Remove() instead + + + + + ClearAllChildren + Removes all children (but not this object) from the workspace. + + + + + Destroy + Removes object and all of its children from the workspace. Disconnects object and all children from open connections. Object and children may not be usable after calling Destroy. + + + + + findFirstChild + true + Use FindFirstChild() instead + + + + + + + + AncestryChanged + Fired when any of this object's ancestors change. First argument 'child' is the object whose parent changed. Second argument 'parent' is the first argument's new parent. + + + + + DescendantAdded + Fired after an Instance is parented to this object, or any of this object's descendants. The 'descendant' argument is the Instance that is being added. + + + + + DescendantRemoving + Fired after an Instance is unparented from this object, or any of this object's descendants. The 'descendant' argument is the Instance that is being added. + + + + + Changed + Fired after a property changes value. The property argument is the name of the property + + + + + + + + BodyGyro + Legacy Body Movers + Attempts to maintain a fixed orientation of its parent Part + 140 + 14 + BasePart + + + + + + MaxTorque + The maximum torque that will be exerted on the Part + + + + + maxTorque + true + Use MaxTorque instead + + + + + D + The dampening factor applied to this force + + + + + P + The power continually applied to this force + + + + + CFrame + The cframe that this force is trying to orient its parent Part to. Note: this force only uses the rotation of the cframe, not the position. + + + + + cframe + true + Use CFrame instead + + + + + + + BodyPosition + Legacy Body Movers + 140 + 14 + BasePart + + + + + + MaxForce + The maximum force that will be exerted on the Part + + + + + maxForce + true + Use MaxForce instead + + + + + D + The dampening factor applied to this force + + + + + P + The power factor continually applied to this force + + + + + Position + The Vector3 that this force is trying to position its parent Part to. + + + + + position + true + Use position instead + + + + + + + RocketPropulsion + Legacy Body Movers + 140 + 14 + A propulsion system that mimics a rocket + BasePart + + + + + BodyVelocity + Legacy Body Movers + 140 + 14 + BasePart + + + + + MaxForce + The maximum force that will be exerted on the Part in each axis + + + + + maxForce + true + Use MaxForce instead + + + + + P + The amount of power we add to the system. The higher the power, the quicker the force will achieve its goal. + + + + + Velocity + The velocity this system tries to achieve. How quickly the system reaches this velocity (if ever) is defined by P. + + + + + velocity + true + Use Velocity instead + + + + + + + BodyAngularVelocity + Legacy Body Movers + 140 + 14 + BasePart + + + + MaxTorque + The maximum torque that will be exerted on the Part in each axis + + + maxTorque + true + Use MaxTorque instead + + + P + The amount of power we add to the system. The higher the power, the quicker the force will achieve its goal. + + + AngularVelocity + The rotational velocity this system tries to achieve. How quickly the system reaches this velocity is defined by P. + + + angularVelocity + true + Use AngularVelocity instead + + + + + + BodyForce + Legacy Body Movers + 140 + 14 + When parented to a physical part, BodyForce will continually exert a force upon its parent object. + BasePart + + + + Force + The continual force exerted on an object, defined in each axis. + + + force + true + Use Force instead + + + + + + BodyThrust + Legacy Body Movers + 140 + 14 + BasePart + + + + + + Force + The power continually applied to this force + + + + + force + true + Use Force instead + + + + + Location + The Vector3 location of where to apply the force to. + + + + + location + true + Use Location instead + + + + + + + Hole + true + 20 + + + + + Feature + 20 + + + + + + Teams + This Service-level object is the container for all Team objects in a level. A map that supports team games must have a Teams service. <a href='http://wiki.roblox.com/index.php/Team' target='_blank'>More info</a> + 140 + 23 + Teams + + + + + GetPlayers + Returns a read-only table of players which are on this team. + + + + + + + Team + Interaction + The Team class is used to represent a faction in a team game. The only valid location for a Team object is under the Teams service. <a href='http://wiki.roblox.com/index.php/Team' target='_blank'>More info</a> + 10 + 24 + Teams + + + + + SpawnLocation + Interaction + 30 + 25 + + + + + NetworkClient + false + 30 + 16 + + + + + NetworkServer + false + 30 + 15 + + + + + LuaSourceContainer + false + + + + + CurrentEditor + The name of the player who is currently editing the script in Team Create. + true + + + + + + + Script + Scripting + 30 + 6 + + + + + + LinkedScript + + This property is under development. Do not use + + + + + + + + LocalScript + Scripting + 40 + 18 + A script that runs on clients, NOT servers. LocalScripts can only run when parented under one of the following: + 1) A player's Backpack. + 2) A player's Character model. + 3) A player's PlayerGui. + 4) A player's PlayerScripts. + 5) The ReplicatedFirst service. + + + + + + + RenderingTest + false + Scripting + 40 + dummy summary + 5 + + + + + + NetworkReplicator + 30 + 29 + + + + + + Model + 100 + 2 + A construct used to group Parts and other objects together, also allows manipulation of multiple objects. + PVInstance + + + + + BreakJoints + Breaks all surface joints contained within + + + + + GetModelCFrame + Returns a CFrame that has position of the centroid of all Parts in the Model. The rotation matrix is either the rotation matrix of the user-defined PrimaryPart, or if not specified then a part in the Model chosen by the engine. + + + + + GetModelSize + Returns a Vector3 that is union of the extents of all Parts in the model. + + + + + MakeJoints + Creates the appropriate SurfaceJoints between all touching Parts contrained within the model. Technically, this function calls MakeJoints() on all Parts inside the model. + + + + + MoveTo + Moves the centroid of the Model to the specified location, respecting all relative distances between parts in the model. + + + + + ResetOrientationToIdentity + Rotates all parts in the model to the orientation that was set using SetIdentityOrientation(). If this function has never been called, rotation is reset to GetModelCFrame()'s rotation. + + + + + SetIdentityOrientation + Takes the current rotation matrix of the model and stores it as the model's identity matrix. The rotation is applied when ResetOrientationToIdentity() is called. + + + + + TranslateBy + Similar to MoveTo(), except instead of moving to an explicit location, we use the model's current CFrame location and offset it. + + + + + GetPrimaryPartCFrame + Returns the cframe of the Model.PrimaryPart. If PrimaryPart is nil, then this function will throw an error. + + + + + SetPrimaryPartCFrame + Sets the cframe of the Model.PrimaryPart. If PrimaryPart is nil, then this function will throw an error. This also sets the cframe of all descendant Parts relative to the cframe change to PrimaryPart. + + + + + makeJoints + Use MakeJoints() instead + true + + + + + move + true + Use MoveTo() instead + + + + + + + PrimaryPart + A Part that serves as a reference for the Model's CFrame. Used in conjunction with GetModelPrimaryPartCFrame and SetModelPrimaryPartCFrame. Use this to rotate/translate all Parts relative to the PrimaryPart. + + + + + LevelOfDetail + Automatically generate impostor meshes to be rendered outside of streaming radius. + + + + + + + + Status + true + 100 + 2 + + + + + move + true + Use MoveTo() instead + + + + + + + + DataModel + The root of ROBLOX's parent-child hierarchy (commonly known as game after the global variable used to access it) + + + + + + OnClose + true + Deprecated. Use DataModel.BindToClose + + + + + + + + + + + PrivateServerId + true + + + + + PrivateServerOwnerId + true + + + + + + VIPServerId + true + + + + + VIPServerOwnerId + true + + + + + + Workspace + + + + + workspace + true + Deprecated. Use Workspace + + + + + ShowMouse + true + Deprecated. Use Workspace.IsMouseCursorVisible + + + + + IsLoaded + Returns true if the game has finished loading, false otherwise. Check this before listening to the Loaded signal to ensure a script knows when a game finishes loading. + + + + + + + + Loaded + Fires when the game finishes loading. Use this to know when to remove your custom loading gui. It is best to check IsLoaded() before connecting to this event, as the game may load before the event is connected to. + + + + + + + + SetPlaceID + true + Use SetPlaceId() instead + + + + + SetCreatorID + true + Use SetCreatorId() instead + + + + + + + + DataStoreService + Responsible for storing data across multiple user created places + + + + + + GetDataStore + Returns a data store with the given name and scope + + + + + GetGlobalDataStore + Returns the default data store + + + + + GetOrderedDataStore + Returns an ordered data store with the given name and scope + + + + + + + + GlobalDataStore + Exposes functions for saving and loading data for the DataStoreService + -1 + + + + + + OnUpdate + Sets callback as a function to be executed any time the value associated with key is changed. It is important to disconnect the connection when the subscription to the key is no longer needed. + + + + + + + + GetAsync + Returns the value of the entry in the DataStore with the given key + + + + + IncrementAsync + Increments the value of a particular key amd returns the incremented value + + + + + SetAsync + Sets the value of the key. This overwrites any existing data stored in the key + + + + + UpdateAsync + Retrieves the value of the key from the website, and updates it with a new value. The callback until the value fetched matches the value on the web. Returning nil means it will not save. + + + + + + + + OrderedDataStore + A type of DataStore where values must be positive integers. This makes OrderedDataStore suitable for leaderboard related scripting where you are required to order large amounts of data efficiently. + -1 + + + + + + GetSortedAsync + Returns a DataStorePages object. The length of each page is determined by pageSize, and the order is determined by isAscending. minValue and maxValue are optional parameters which will filter the result. + + + + + + + + HopperBin + true + 240 + 22 + + + + + + Camera + 5 + 5 + Model + + + + + CameraSubject + Where the Camera's focus is. Any rotation of the camera will be about this subject. + + + + + CameraType + Defines how the camera will behave. <a href='http://wiki.roblox.com/index.php/CameraType' target='_blank'>More info</a> + + + + + CoordinateFrame + true + The current position and rotation of the Camera. For most CameraTypes, the rotation is set such that the CoordinateFrame lookVector is pointing at the Focus. + + + + + CFrame + The current position and rotation of the Camera. For most CameraTypes, the rotation is set such that the CoordinateFrame lookVector is pointing at the Focus. + + + + + FieldOfViewMode + Determines how the field of view responds to changes of screen size and aspect ratio. + + + + + FieldOfView + Describes the view angle along the vertical viewport axis. + + + + + DiagonalFieldOfView + Describes the view angle along the diagonal viewport axis. + + + + + MaxAxisFieldOfView + Describes the view angle along the maximum-length viewport axis. + + + + + Focus + The current CoordinateFrame that the camera is looking at. Note: it is not always guaranteed that the camera is always looking here. + + + + + ViewportSize + Holds the x,y screen resolution of the viewport the camera is presenting (note: this can differ from the AbsoluteSize property of a full screen gui). + + + + + NearPlaneZ + The negative z-offset of the view frustum's near clipping plane. + + + + + + + GetRoll + Returns the camera's current roll. Roll is defined in radians, and is stored as the delta from the camera's y axis default normal vector. + + + + + WorldToScreenPoint + Takes a 3D position in the world and projects it onto x,y coordinates of screen space. Returns two values, first is a Vector3 that has x,y position and z position which is distance from camera (negative if behind camera, positive if in front). Second return value is a boolean indicating if the first argument is an on-screen coordinate. + + + + + ScreenPointToRay + Takes a 2D screen position and produces a Ray object to be used for 3D raycasting. Input is x,y screen coordinates, and a (optional, defaults to 0) z position which sets how far in the camera look vector to start the ray origin. + + + + + ViewportPointToRay + Same as ScreenPointToRay, except no GUI offsets are taken into account. Useful for things like casting a ray from the middle of the Camera.ViewportSize + + + + + WorldToViewportPoint + Same as WorldToScreenPoint, except no GUI offsets are taken into account. + + + + + SetRoll + Sets the camera's current roll. Roll is defined in radians, and is stored as the delta from the camera's y axis default normal vector. + + + + + + + + Players + 20 + 21 + + + + + CharacterAutoLoads + true + Set to true, when a player joins a game, they get a character automatically, as well as when they die. When set to false, characters do not auto load and will only load in using Player:LoadCharacter(). + + + + + + + players + true + Use GetPlayers() instead + + + + + + + + ReplicatedStorage + 30 + 70 + A container whose contents are replicated to all clients and the server. + + + + + + RobloxReplicatedStorage + false + + + + + + ReplicatedFirst + 30 + 70 + A container whose contents are replicated to all clients (but not back to the server) first before anything else. Useful for creating loading guis, tutorials, etc. + + + + + RemoveRobloxLoadingScreen + Removes the default Roblox loading screen from view. Call this when you are ready to either show your own loading gui, or when the game is ready to play. + + + + + + + + ServerStorage + 30 + 69 + A container whose contents are only on the server. + + + + + + ServerScriptService + 30 + 71 + A container whose contents should be scripts. Scripts that are added to the container are run on the server. + + + + + + ReplicatedScriptService + 30 + 70 + A container whose contents should be scripts. Scripts that are added to the container are run on the server and the client. + + + + + + StudioService + A service for interfacing with the current studio state from Lua. + + + + + + Lighting + 30 + 13 + Responsible for all lighting aspects of the world (affects how things are rendered). + + + + + GetMinutesAfterMidnight + The number of minutes that the current time is past midnight. If currently at midnight, returns 0. Will return decimal values if not at an exact minute. + + + + + GetMoonDirection + Returns the lookVector (Vector3) of the moon. If this lookVector was used in a CFrame, the Part would face the moon. + + + + + GetMoonPhase + Currently always returns 0.75. MoonPhase cannot be edited. + + + + + GetSunDirection + Returns the lookVector (Vector3) of the sun. If this lookVector was used in a CFrame, the Part would face the sun. + + + + + SetMinutesAfterMidnight + Sets the time to be a certain number of minutes after midnight. This works with integer and decimal values. + + + + + + + Ambient + The hue of the global lighting. Changing this changes the color tint of all objects in the Workspace. + + + + + Brightness + How much global light each Part in the Workspace receives. Standard range is 0 to 2 (0 being little light), but can be increased all the way to 10 (colors start to be appear very different at this value). + 0 + 10 + 1000 + + + + + EnvironmentDiffuseScale + Sets scale [0-1] of Diffuse Environment Lighting to add to Ambient. + 0 + 1 + 1000 + + + + + EnvironmentSpecularScale + Sets scale [0-1] of Specular Environment Lighting to add to Ambient. + 0 + 1 + 1000 + + + + + ExposureCompensation + Exposure compensation amount. Applies a bias to the exposure level prior to the tonemap step. +1 indicates twice as much exposure and -1 means half as much exposure. + -3 + 3 + 600 + + + + + ShadowSoftness + This property controls how blurry the shadows are. + 0 + 1 + 100 + + + + + ColorShift_Bottom + The hue of global lighting on the bottom surfaces of an object. + + + + + ColorShift_Top + The hue of global lighting on the top surfaces of an object. + + + + + GeographicLatitude + The latitude position the level is placed at. This affects sun position. <a href='http://wiki.roblox.com/index.php/GeographicLatitude' target='_blank'>More info</a> + 0 + 360 + 360 + + + + + GlobalShadows + Flag enabling shadows from sun and moon in the place + + + + + OutdoorAmbient + Effective ambient value for outdoors, effectively shadow color outdoors (requires GlobalShadows enabled) + + + + + Outlines + Flag enabling or disabling outlines on parts and terrain + + + + + ShadowColor + Color the shadows appear as. Shadows are drawn mostly for characters, but depending on the lighting will also show for Parts in the Workspace. Rendering settings can also affect if shadows are drawn. + + + + + TimeOfDay + A string that represent the current time of day. Time is in 24-hour clock format "XX::YY:ZZ", where X is hour, Y is minute, and Z is seconds. + + + + + ClockTime + 0 + 24 + 240 + + + + + FogColor + A Color3 value that changes the hue of the atmosphere, mixed with the sky color. + + + + + FogEnd + The distance at which fog completely blocks your vision. This distance is relative to the camera position. Units are in studs + + + + + FogStart + The distance at which the fog gradient begins. This distance is relative to the camera position. Units are in studs. + + + + + + + LightingChanged + Fired whenever a property of Lighting is changed, or a skybox or atmosphere is added or removed. Skyboxes are of type 'Sky' these and 'Atmosphere' should be parented directly to lighting. + + + + + + + + TestService + 1000 + 68 + + + + + + DebuggerManager + + + + + + + + ScriptDebugger + + + + + + + + DebuggerBreakpoint + + + + + + + + DebuggerWatch + + + + + + + + Debris + 30 + A service that provides utility in cleaning up objects + + + + + addItem + true + Use AddItem() instead + + + + + AddItem + Adds an Instance into the debris service that will later be destroyed. Second argument 'lifetime' is optional and specifies how long (in seconds) to wait before destroying the item. If no time is specified then the item added will automatically be destroyed in 10 seconds. + + + + + + + MaxItems + true + Deprecated. No replacement + + + + + + + + Accoutrement + 20 + 32 + false + + + + + + Player + false + 10 + 12 + + + + + + CharacterAppearance + false + Model + + + + + CameraMode + An enum that describes how a Player's camera is allowed to behave. <a href='http://wiki.roblox.com/index.php/CameraMode' target='_blank'>More info</a>. + + + + + DataReady + true + Read-only. If true, this Player's persistent data can be loaded, false otherwise. <a href='http://wiki.roblox.com/index.php/ROBLOX_Scripting_How_To:_Data_Persistence' target='_blank'>Info on Data Persistence</a>. + + + + + DataComplexity + true + + + + + + + + LoadCharacter + true + Loads in a new character for this player. This will replace the player's current character, if they have one. This should be used in conjunction with Players.CharacterAutoLoads to control spawning of characters. This function only works from a server-side script (NOT a LocalScript). + + + + + LoadData + true + + + + + SaveData + true + + + + + SaveBoolean + true + + + + + SaveInstance + true + + + + + SaveString + true + + + + + LoadBoolean + true + + + + + LoadNumber + true + + + + + LoadString + true + + + + + LoadInstance + true + + + + + SaveNumber + true + + + + + playerFromCharacter + true + Use GetPlayerFromCharacter() instead + + + + + SetUnder13 + true + + + + + + + + WaitForDataReady + true + true + Yields until the persistent data for this Player is ready to be loaded. <a href='http://wiki.roblox.com/index.php/ROBLOX_Scripting_How_To:_Data_Persistence' target='_blank'>Info on Data Persistence</a>. + + + + + + + + + Idled + Fired periodically after the user has been AFK for a while. Currently this event is only fired for the *local* Player. "time" is the time in seconds that the user has been idle. + + + + + + + + Workspace + 5 + 19 + + + + + FindPartsInRegion3 + Returns parts in the area defined by the Region3, up to specified maxCount or 100, whichever is less + + + + + FindPartsInRegion3WithIgnoreList + Returns parts in the area defined by the Region3, up to specified maxCount or 100, whichever is less + + + + + FindPartOnRay + Deprecated. Use WorldRoot:Raycast() instead + true + + + + + FindPartOnRayWithIgnoreList + Deprecated. Use WorldRoot:Raycast() instead + true + + + + + + + PGSPhysicsSolverEnabled + Boolean used to enable the new physics solver + + + + + FallenPartsDestroyHeight + Sets the height at which falling characters and parts are destroyed. This property is not scriptable and can only be set in Studio + + + + + + + + BasePart + A structural class, not creatable + 3 + false + + + + + + Color + Color3 of the part. + + + + + CFrame + Contains information regarding the Part's position and a matrix that defines the Part's rotation. Can read/write. <a href='http://wiki.roblox.com/index.php/Cframe' target='_blank'>More info</a> + + + + + CanCollide + Determines whether physical interactions with other Parts are respected. If true, will collide and react with physics to other Parts. If false, other parts will pass thru instead of colliding + + + + + Anchored + Determines whether or not physics acts upon the Part. If true, part stays 'Anchored' in space, not moving regardless of any collision/forces acting upon it. If false, physics works normally on the part. + + + + + Massless + If true the part will be massless when welded to another part that is not massless. The part will still have mass like a normal part if it is an assembly root part according to GetRootPart(). + + + + + RootPriority + An integer from -127 to 127. Compares before other all other part properties besides massless for deciding which part is the assembly root part according to GetRootPart(). + + + + + Elasticity + A float value ranging from 0.0f to 1.0f. Sets how much the Part will rebound against another. a value of 1 is like a superball, and 0 is like a lead block. + 0 + 1 + + + + + Friction + A float value ranging from 0.0f to 1.0f. Sets how much the Part will be able to slide. a value of 1 is no sliding, and 0 is no friction, so infinite sliding. + 0 + 2 + + + + + Locked + Determines whether building tools (in-game and studio) can manipulate this Part. If true, no editing allowed. If false, editing is allowed. + + + + + CastShadow + Determines whether this Part casts a shadow. + + + + + Material + Specifies the look and feel the Part should have. Note: this does not define the color the Part is, see BrickColor for that. <a href='http://wiki.roblox.com/index.php/Material' target='_blank'>More info</a> + + + + + Reflectance + Specifies how shiny the Part is. A value of 1 is completely reflective (chrome), while a value of 0 is no reflectance (concrete wall) + 0 + 1 + + + + + ResizeIncrement + Sets the value for the smallest change in size allowable by the Resize(NormalId, int) function. + + + + + ResizeableFaces + Sets the value for the faces allowed to be resized by the Resize(NormalId, int) function. + + + + + Transparency + Sets how visible an object is. A value of 1 makes the object invisible, while a value of 0 makes the object opaque. + 0 + 1 + + + + + Velocity + How fast the Part is traveling in studs/second. This property is NOT recommended to be modified directly, unless there is good reason. Otherwise, try using a BodyForce to move a Part. + + + + + PositionLocal + Position relative to parent part, or global space if there is no parent. + + + + + OrientationLocal + Orientation relative to parent part, or global space if there is no parent. + + + + + Orientation + Rotation around X, Y, and Z axis. Rotations applied in YXZ order. + + + + + Rotation + + + + + CenterOfMass + + + + + + + + makeJoints + Use MakeJoints() instead + true + + + + + MakeJoints + Creates the appropriate SurfaceJoints with all parts that are touching this Instance (including internal joints in the Instance, as in a Model). This uses the SurfaceTypes defined on the surfaces of parts to create the appropriate welds. <a href='http://wiki.roblox.com/index.php/MakeJoints' target='_blank'>More info</a> + + + + + BreakJoints + Destroys SurfaceJoints with all parts that are touching this Instance (including internal joints in the Instance, as in a Model). + + + + + GetMass + Returns a number that is the mass of this Instance. Mass of a Part is immutable, and is changed only by the size of the Part. + + + + + Resize + Resizes a Part in the direction of the face defined by 'NormalId', by the amount specified by 'deltaAmount'. If the operation will expand the part to intersect another Instance, the part will not resize at all. Return true if the call is successful, false otherwise. + + + + + getMass + Use GetMass() instead + true + + + + + + + OutfitChanged + true + + + + + LocalSimulationTouched + true + Deprecated. Use Touched instead + + + + + StoppedTouching + + Deprecated. Use TouchEnded instead + + + + + TouchEnded + Fired when the part stops touching another part + + + + + + + Part + Parts + A plastic building block - the fundamental component of ROBLOX + 110 + 1 + Workspace + + + + + TrussPart + Parts + An extendable building truss + 120 + 1 + Model + + + + + WedgePart + Parts + A Wedge Part + 120 + 1 + Model + + + + + PrismPart + A Prism Part + false + true + 120 + 1 + + + + + PyramidPart + A Pyramid Part + false + true + 120 + 1 + + + + + ParallelRampPart + A ParallelRamp Part + false + true + 120 + 1 + + + + + RightAngleRampPart + A RightAngleRamp Part + false + true + 120 + 1 + + + + + CornerWedgePart + Parts + A CornerWedge Part + 120 + 1 + Workspace + + + + + PlayerGui + A container instance that syncs data between a single player and the server. ScreenGui objects that are placed in this container will be shown to the Player parent only + 130 + 46 + + + + + SelectionImageObject + Overrides the default selection adornment (used for gamepads). For best results, this should point to a GuiObject. + + + + + + + PlayerScripts + A container instance that contains LocalScripts. LocalScript objects that are placed in this container will be execute only when a Player is the parent. + 130 + 78 + + + + + StandalonePluginScripts + A container instance that contains Scripts. Useful only for Plugins. When Studio starts, we load plugins into the 'UserPlugin' data model and execute Scripts contained in StandalonePluginScripts container. If a plugin doesn't have such a container then the plugin isn't loaded into UserPlugin data model. When a data model for a place is created (e.g. the Edit data model), we load plugins into said data model and execute only those Scripts which are not contained in StandalonePluginScripts container. + 130 + 78 + true + + + + + StarterPlayerScripts + A container instance that contains LocalScripts. LocalScript objects that are placed in this container will be copied to new Players on startup. + 130 + 78 + false + + + + + StarterCharacterScripts + A container instance that contains LocalScripts. LocalScript objects that are placed in this container will be copied to new characters on startup. + 130 + 78 + false + + + + + + GuiMain + Deprecated, please use ScreenGui + true + 140 + 47 + + + + + + LayerCollector + The base class of ScreenGui, BillboardGui, and SurfaceGui. + false + + + + Enabled + Whether or not this should be displayed. + + + ZIndexBehavior + Controls the behavior of the ZIndex property for descendants of this object. It can be set to Global (Default) or Sibling. + + + + + + + ScreenGui + GUI + The core GUI object on which tools are built. Add Frames/Labels/Buttons to this object to have them rendered as a 2D overlay + 140 + 47 + BasePlayerGui + + + + + FunctionalTest + Deprecated. Use TestService instead + true + 10 + + + + + BillboardGui + GUI + A GUI that adorns an object in the 3D world. Add Frames/Labels/Buttons to this object to have them rendered while attached to a 3D object + 140 + 64 + GuiBase2d + + + + + + Adornee + The Object the billboard gui uses as its base to render from. Currently, the only way to set this property is thru a script, and must exist in the workspace. This will only render if the object assigned derives from BasePart. + + + + + AbsolutePosition + A read-only Vector2 value that is the GuiObject's current position (x,y) in pixel space, from the top left corner of the GuiObject. + + + + + AbsoluteSize + A read-only Vector2 value that is the GuiObject's current size (width, height) in pixel space. + + + + + Active + If true, this GuiObject can fire mouse events and will pass them to any GuiObjects layered underneath, while false will do neither. + + + + + AlwaysOnTop + If true, billboard gui does not get occluded by 3D objects, but always renders on the screen. + + + + + Enabled + If true, billboard gui will render, otherwise rendering will be skipped. + + + + + ExtentsOffset + A Vector3 (x,y,z) defined in studs that will offset the gui from the extents of the 3d object it is rendering from. + + + + + PlayerToHideFrom + Specifies a Player that the BillboardGui will not render to. + + + + + StudsOffset + A Vector3 (x,y,z) defined in studs that will offset the gui from the centroid of the 3d object it is rendering from + + + + + SizeOffset + A Vector2 (x,y) defined in studs that will offset the gui size from it's current size. + + + + + Size + A UDim2 value describing the size of the BillboardGui. More information on UDim2 is available <a href='http://wiki.roblox.com/index.php/UDim2' target='_blank'>here</a>. Relative values are defined as one-to-one with studs. + + + + + LightInfluence + Specifies the amount of influence lighting has on the billboard gui. A value of 0 is unlit, 1 is fully lit. Fractional values blend from unlit to lit. + 0 + 1 + + + + + + + + SurfaceGui + GUI + Renders its contained GuiObjects flat against the face of a part. + 140 + 64 + GuiBase2d + + + + + + Adornee + The Object the surface gui uses as its base to render from. Currently, the only way to set this property is thru a script, and must exist in the workspace. This will only render if the object assigned derives from BasePart. + + + + + Active + If true, this GuiObject can fire mouse events and will pass them to any GuiObjects layered underneath, while false will do neither. + + + + + Enabled + If true, surface gui will render, otherwise rendering will be skipped. + + + + + LightInfluence + Specifies the amount of influence lighting has on the surface gui. A value of 0 is unlit, 1 is fully lit. Fractional values blend from unlit to lit. + 0 + 1 + + + + + + + + + + GuiBase2d + false + + + + + + AbsolutePosition + A read-only Vector2 value that is the GuiObject's current position (x,y) in pixel space, from the top left corner of the GuiObject. + + + + + AbsoluteSize + A read-only Vector2 value that is the GuiObject's current size (width, height) in pixel space. + + + + + + + + InputObject + An object that describes a particular user input, such as mouse movement, touches, keyboard, and more. + + + + + UserInputType + An enum that describes what kind of input this object is describing (mousebutton, touch, etc.). See Enum.UserInputType for more info. + + + + + UserInputState + An enum that describes what state of a particular input (touch began, touch moved, touch ended, etc.). See Enum.UserInputState for more info. + + + + + Position + A Vector3 value that describes a positional value of this input. For mouse and touch input, this is the screen position of the mouse/touch, described in the x and y components. For mouse wheel input, the z component describes whether the wheel was moved forward or backward. + + + + + KeyCode + An enum that describes what kind of input is being pressed. For types of input like Keyboard, this describes what key was pressed. For input like mousebutton, this provides no additional information. + + + + + + + + GuiObject + false + + + + + + TweenPosition + Smoothly moves a GuiObject from its current position to 'endPosition'. The only required argument is 'endPosition'. <a href='http://wiki.roblox.com/index.php/TweenPosition' target='_blank'>More info</a> + + + + + TweenSize + Smoothly translates a GuiObject's current size to 'endSize'. The only required argument is 'endSize'. <a href='http://wiki.roblox.com/index.php/TweenSize' target='_blank'>More info</a> + + + + + TweenSizeAndPosition + Smoothly translates a GuiObject's current size to 'endSize', and also smoothly translates the GuiObject's current position to 'endPosition'. The only required arguments are 'endSize' and 'endPosition'. <a href='http://wiki.roblox.com/index.php/TweenSizeAndPosition' target='_blank'>More info</a> + + + + + + + + Active + If true, this GuiObject can fire mouse events and will pass them to any GuiObjects layered underneath, while false will do neither. + + + + + BackgroundColor3 + A Color3 value that specifies the background color for the GuiObject. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + BackgroundTransparency + A number value that specifies how transparent the background of the GuiObject is. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + 0 + 1 + + + + + BorderColor3 + A Color3 value that specifies the color of the outline of the GuiObject. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + BorderSizePixel + A number value that specifies the thickness (in pixels) of the outline of the GuiObject. Currently this value can only be set to either 0 or 1, any other number has no effect. This value is ignored if the Style property (not found on all GuiObjects) is set to something besides custom. + + + + + ClipsDescendants + If set to true, any descendants of this GuiObject will only render if contained within it's borders. If set to false, all descendants will render regardless of position. + + + + + Draggable + true + If true, allows a GuiObject to be dragged by the user's mouse. The events 'DragBegin' and 'DragStopped' are fired when the appropriate action happens, and only will fire on Draggable=true GuiObjects. + + + + + Size + A UDim2 value describing the size of the GuiObject on screen in both absolute and relative coordinates. More information on UDim2 is available <a href='http://wiki.roblox.com/index.php/UDim2' target='_blank'>here</a>. + + + + + Position + A UDim2 value describing the position of the top-left corner of the GuiObject on screen. More information on UDim2 is available <a href='http://wiki.roblox.com/index.php/UDim2' target='_blank'>here</a>. + + + + + SizeConstraint + The direction(s) that an object can be resized in. <a href='http://wiki.roblox.com/index.php/SizeConstraint' target='_blank'>More info</a>. + + + + + ZIndex + Describes the ordering in which overlapping GuiObjects will be drawn. A value of 1 is drawn first, while higher values are drawn in ascending order (each value draws over the last). + + + + + BackgroundColor + true + Deprecated. Use BackgroundColor3 instead + + + + + BorderColor + true + Deprecated. Use BorderColor3 instead + + + + + SelectionImageObject + Overrides the default selection adornment (used for gamepads). For best results, this should point to a GuiObject. + + + + + + + + DragBegin + true + Fired when a GuiObject with Draggable set to true starts to be dragged. 'InitialPosition' is a UDim2 value of the position of the GuiObject before any drag operation began. + + + + + DragStopped + true + Always fired after a DragBegin event, DragStopped is fired when the user releases the mouse button causing a drag operation on the GuiObject. Arguments 'x', and 'y' specify the top-left absolute position of the GuiObject when the event is fired. + + + + + MouseEnter + Fired when the mouse enters a GuiObject, as long as the GuiObject is active (see active property for more detail). Arguments 'x', and 'y' specify the absolute pixel position of the mouse. + + + + + MouseLeave + Fired when the mouse leaves a GuiObject, as long as the GuiObject is active (see active property for more detail). Arguments 'x', and 'y' specify the absolute pixel position of the mouse. + + + + + MouseMoved + Fired when the mouse is inside a GuiObject and moves, as long as the GuiObject is active (see active property for more detail). Arguments 'x', and 'y' specify the absolute pixel position of the mouse. + + + + + + TouchTap + Fired when a user taps their finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the tap gesture. This event only fires locally. This event will always fire regardless of game state. + + + + + TouchPinch + Fired when a user pinches their fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the pinch gesture. 'scale' is a float that indicates the difference from the beginning of the pinch gesture. 'velocity' is a float indicating how quickly the pinch gesture is happening. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. + + + + + TouchSwipe + Fired when a user swipes their fingers on a TouchEnabled device. 'swipeDirection' is an Enum.SwipeDirection, indicating the direction the user swiped. 'numberOfTouches' is an int that indicates how many touches were involved with the gesture. This event only fires locally. + + + + + TouchLongPress + Fired when a user holds at least one finger for a short amount of time on the same screen position on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. + + + + + TouchRotate + Fired when a user rotates two fingers on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'rotation' is a float indicating how much the rotation has gone from the start of the gesture. 'velocity' is a float that indicates how quickly the gesture is being performed. 'state' indicates the Enum.UserInputState of the gesture. This event only fires locally. + + + + + TouchPan + Fired when a user drags at least one finger on a TouchEnabled device. 'touchPositions' is a Lua array of Vector2, each indicating the position of all the fingers involved in the gesture. 'totalTranslation' is a Vector2, indicating how far the pan gesture has gone from its starting point. 'velocity' is a Vector2 that indicates how quickly the gesture is being performed in each dimension. 'state' indicates the Enum.UserInputState of the gesture. + + + + + + InputBegan + Fired when a user begins interacting via a Human-Computer Interface device (Mouse button down, touch begin, keyboard button down, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. + + + + + InputChanged + Fired when a user changes interacting via a Human-Computer Interface device (Mouse move, touch move, mouse wheel, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. + + + + + InputEnded + Fired when a user stops interacting via a Human-Computer Interface device (Mouse button up, touch end, keyboard button up, etc.). 'inputObject' is an InputObject, which contains useful data for querying user input. This event only fires locally. + + + + + + + + + Frame + GUI + A container object used to layout other GUI objects + 150 + 48 + GuiBase2d + + + + + Style + Determines how a frame will look. Uses Enum.FrameStyle. <a href='http://wiki.roblox.com/index.php?title=API:Enum/FrameStyle' target='_blank'>More info</a> + + + + + + + ScrollingFrame + GUI + Studio.App.RobloxRibbonMainWindow.ScrollingFrameTooltip + 150 + 48 + GuiBase2d + + + + + ScrollingEnabled + Determines whether or not scrolling is allowed on this frame. If turned off, no scroll bars will be rendered. + + + + + CanvasSize + Determines the size of the area that is scrollable. The UDim2 is calculated using the parent gui's size, similar to the regular Size property on gui objects. + + + + + CanvasPosition + The absolute position the scroll frame is in respect to the canvas size. The minimum this can be set to is (0,0), while the max is the absolute canvas size - AbsoluteWindowSize. + + + + + AbsoluteWindowSize + The size in pixels of the frame, without the scrollbars. + + + + + ScrollBarThickness + How thick the scroll bar appears. This applies to both the horizontal and vertical scroll bars. Can be set to 0 for no bars render. + + + + + TopImage + The "Up" image on the vertical scrollbar. Size of this is always ScrollBarThickness by ScrollBarThickness. This is also used as the "left" image on the horizontal scroll bar. + + + + + MidImage + The "Middle" image on the vertical scrollbar. Size of this can vary in the y direction, but is always set at ScrollBarThickness in x direction. This is also used as the "mid" image on the horizontal scroll bar. + + + + + BottomImage + The "Down" image on the vertical scrollbar. Size of this is always ScrollBarThickness by ScrollBarThickness. This is also used as the "right" image on the horizontal scroll bar. + + + + + + + ImageLabel + GUI + A GUI object containing an Image + 180 + 49 + GuiBase2d + + + + + Image + Specifies the id of the texture to display. <a href='http://wiki.roblox.com/index.php?title=API:Class/ImageLabel/Image' target='_blank'>More info</a> + + + + + ScaleType + Specifies how an image should be displayed. See ScaleType for more info. + + + + + SliceCenter + If ScaleType is set to Slice, this Rect is used to specify the central part of the image. Everything outside of this is considered to be the border. + + + + + TileSize + If ScaleType is set to Tile, this sets the size of the tile. + + + + + + + + TextLabel + GUI + A GUI object containing text + 190 + 50 + GuiBase2d + + + + + TextColor + true + Deprecated. Use TextColor3 instead + + + + + + + TextButton + GUI + A GUI button containing text + 170 + 51 + GuiBase2d + + + + + TextColor + true + Deprecated. Use TextColor3 instead + + + + + + + TextBox + GUI + A text entry box + 170 + 51 + GuiBase2d + + + + + TextColor + true + Deprecated. Use TextColor3 instead + + + + + + + GuiButton + GUI + A GUI button containing an Image + false + 160 + 52 + + + + + AutoButtonColor + Determines whether a button changes color automatically when reacting to mouse events. + + + + + Modal + Allows the mouse to be free in first person mode. If a button with this property set to true is visible, the mouse is 'free' in first person mode. + + + + + Style + Determines how a button will look, including mouse event states. Uses Enum.ButtonStyle. <a href='http://wiki.roblox.com/index.php?title=API:Class/GuiButton/Style' target='_blank'>More info</a> + + + + + + + MouseButton1Click + Fired when the mouse is over the button, and the mouse down and up events fire without the mouse leaving the button. + + + + + MouseButton1Down + Fired when the mouse button is pushed down on a button. + + + + + MouseButton1Up + Fired when the mouse button is released on a button. + + + + + MouseButton2Click + This function currently does not work :( + + + + + MouseButton2Down + This function currently does not work :( + + + + + MouseButton2Up + This function currently does not work :( + + + + + + + ViewportFrame + GUI + A GUI that can show 3D objects + 30 + 52 + GuiBase2d + + + + + CurrentCamera + Current Camera of children objects + + + + + ImageTransparency + A number value that specifies how transparent the rendered image of the ViewportFrame is + 0 + 1 + + + + + ImageColor3 + The rendered image of the ViewportFrame will be multiplied by this color + + + + + Ambient + Changing this changes the color tint of all objects in the ViewportFrame. + + + + + LightColor + Directional light color for objects in the ViewportFrame. + + + + + LightDirection + Light direction. Value will be normalized. All values valid except (0,0,0). + + + + + + + ImageButton + GUI + A GUI button containing an Image + 160 + 52 + GuiBase2d + + + + + Image + Specifies the asset id of the texture to display. <a href='http://wiki.roblox.com/index.php?title=API:Class/ImageButton/Image' target='_blank'>More info</a> + + + + + ScaleType + Specifies how an image should be displayed. See ScaleType for more info. + + + + + SliceCenter + If ScaleType is set to Slice, this Rect is used to specify the central part of the image. Everything outside of this is considered to be the border. + + + + + TileSize + If ScaleType is set to Tile, this sets the size of the tile. + + + + + + + Handles + Adornments + A 3D GUI object to represent draggable handles + + 190 + 53 + + + + + ArcHandles + Adornments + A 3D GUI object to represent draggable arc handles + + 200 + 56 + + + + + SelectionBox + Adornments + A 3D GUI object to represent the visible selection around an object + 210 + 54 + + + + + SelectionSphere + Adornments + A 3D GUI object to represent the visible selection around an object + 210 + 54 + + + + + SurfaceSelection + Adornments + A 3D GUI object to represent the visible selection around a face of an object + 210 + 55 + + + + + Configuration + An object that can be placed under parts to hold Value objects that represent that part's configuration + 220 + 58 + + + + + HumanoidDescription + An object that specifies the appearance of Humanoid characters + 22 + 104 + + + + + Folder + An object that can be created to hold and organize objects + 10 + 77 + + + + + WorldModel + true + World + An object that contains a World. Supports rigid joints. Unlike Workspace, WorldModels do not support dynamics simulation. + 22 + 19 + + + + + Motor6D + Animations + The Motor6D object is used to make movable joints between two Parts. + 200 + 106 + + + + + BoxHandleAdornment + Adornments + The BoxHandleAdornment is a rectangular prism that can be adorned to a BasePart. + 205 + 111 + + + + + ConeHandleAdornment + Adornments + A ConeHandleAdornment is a cone that can be adorned to a BasePart. + 205 + 110 + + + + + CylinderHandleAdornment + Adornments + The CylinderHandleAdornment is a cylinder that can be adorned to a BasePart. + 205 + 109 + + + + + SphereHandleAdornment + Adornments + The SphereHandleAdornment is a sphere that can be adorned to a BasePart. + 205 + 112 + + + + + LineHandleAdornment + Adornments + The LineHandleAdornment is a line that can be adorned to a BasePart. + 205 + 107 + + + + + ImageHandleAdornment + Adornments + The ImageHandleAdornment is an image that can be adorned to a BasePart. + 205 + 108 + + + + + SelectionPartLasso + true + A visual line drawn representation between two part objects + 220 + 57 + + + + + SelectionPointLasso + true + A visual line drawn representation between two positions + 220 + 57 + + + + + PartPairLasso + A visual line drawn representation between two parts. + 220 + 57 + + + + + Pose + The pose of a joint relative to it's parent part in a keyframe + 220 + 60 + false + + + + + KeyframeMarker + Represents when an event should be fired in an animation + 220 + 60 + false + + + + + Keyframe + One keyframe of an animation + 220 + 60 + false + + + + + Animation + Animations + Represents a linked animation object, containing keyframes and poses. + 220 + 60 + + + + + AnimationTrack + Returned by a call to LoadAnimation. Controls the playback of an animation on a Humanoid. + 220 + 60 + + + + + AnimationController + Animations + Allows animations to be played on joints of the parent object. + 220 + 60 + + + + + CharacterMesh + Meshes + Modifies the appearance of a body part. + 220 + 60 + Model + + + + + Dialog + 3D Interfaces + An object used to make dialog trees to converse with players + 220 + 62 + + + + + ConversationDistance + The maximum distance that the player's character can be from the dialog's parent in order to use the dialog. + + + + + GoodbyeChoiceActive + Indicates whether or not an extra choice is available for the player to exit the dialog tree at this node. + + + + + GoodbyeDialog + The prompt text for an extra choice that allows the player to exit the dialog tree at this node. + + + + + InUse + Indicates whether or not the dialog is currently being used by one or more players. + + + + + InitialPrompt + The chat message that is displayed to the player when they first activate the dialog. + + + + + Purpose + Describes the purpose of the dialog, which is used to display a relevant icon on the dialog's activation button. + + + + + Tone + Describes the tone of the dialog, which is used to display a relevant color in the dialog interface. + + + + + BehaviorType + Indicates how the dialog may be used by players. Use Enum.DialogBehaviorType.SinglePlayer if only one player should interact with the dialog at a time, otherwise use Enum.DialogBehaviorType.MultiplePlayers. + + + + + + + GetCurrentPlayers + Returns an array of the players currently conversing with this dialog. + + + + + + + DialogChoice + 3D Interfaces + An object used to make dialog trees to converse with players + 220 + 63 + + + + + UnionOperation + A UnionOperation is a union of multiple parts + true + false + 105 + 73 + + + + + UsePartColor + Override the colors of the mesh with the part color. + + + + + + + NegateOperation + A NegateOperation can be used to create holes in other parts + true + false + 104 + 72 + + + + + UsePartColor + Override the colors of the mesh with the part color. + + + + + + + MeshPart + Parts + A MeshPart is a physically simulatable mesh + true + true + 105 + 73 + Model + + + + + Terrain + Object representing a high performance bounded grid of static 4x4 parts + true + false + 5 + 65 + + + + + WaterTransparency + 0 + 1 + + + + + WaterWaveSize + 0 + 1 + + + + + WaterWaveSpeed + 0 + 100 + + + + + WaterReflectance + 0 + 1 + + + + + Decoration + Enables terrain materials decoration + + + + + + + GetCell + Returns CellMaterial, CellBlock, CellOrientation + + + + + GetWaterCell + + Returns hasAnyWater, WaterForce, WaterDirection + + + + + SetWaterCell + + + + + + + + Light + Lights + Parent of all light objects + 30 + 13 + PVInstance + + + + + Brightness + 0 + 40 + 2000 + + + + + + + PointLight + Lights + Makes the parent part emit light in a spherical shape + 30 + 13 + PVInstance + + + + + Range + 0 + 60 + + + + + + + SpotLight + Lights + Makes the parent part emit light in a conical shape + 30 + 13 + PVInstance + + + + + Range + 0 + 60 + + + + + Angle + 0 + 180 + + + + + + + SurfaceLight + Lights + Makes the parent part emit light in a frustum shape from rectangle defined by part + 30 + 13 + PVInstance + + + + + Range + 0 + 60 + + + + + Angle + 0 + 180 + + + + + + + RemoteFunction + Scripting + Allow functions defined in one script to be called by another script across client/server boundary + 40 + 74 + + + + + InvokeClient + Server + + + + + InvokeServer + Client + + + + + + + OnClientInvoke + Client + + + + + OnServerInvoke + Server + + + + + + + RemoteEvent + Scripting + Allow events defined in one script to be subscribed to by another script across client/server boundary + 50 + 75 + + + + + FireAllClients + Server + + + + + FireClient + Server + + + + + FireServer + Client + + + + + + + OnClientEvent + Client + + + + + OnServerEvent + Server + + + + + + + TerrainRegion + Object representing a snapshot of the region of terrain + true + 20 + 65 + false + + + + + ModuleScript + Scripting + A script fragment. Only runs when another script uses require() on it. + 50 + 76 + + + + + + + + ContextActionResult + + + + Sink + If 'functionToBind' from ContextActionService:BindAction() returns Enum.ContextActionResult.Sink, the input event will stop at that function and no other bound actions under it will be invoked. This is the default behavior if 'functionToBind' does not return anything or yields in any way. + + + + + Pass + If 'functionToBind' from ContextActionService:BindAction() returns Enum.ContextActionResult.Pass, the input event is considered to have not been handled by 'functionToBind' and will continue being passed to actions bound to the same input type. + + + + + + Material + + + + Air + false + + + + + Water + false + + + + + Rock + false + + + + + Glacier + false + + + + + Snow + false + + + + + Sandstone + false + + + + + Mud + false + + + + + Basalt + false + + + + + Ground + false + + + + + CrackedLava + false + + + + + Asphalt + false + + + + + LeafyGrass + false + + + + + Salt + false + + + + + Limestone + false + + + + + Pavement + false + + + + + + Status + true + + + + Poison + true + + + + + Confusion + true + + + + + + SaveFilter + true + + + + + PrivilegeType + true + + + + + Genre + true + + + + + GearGenreSetting + true + + + + + GearType + true + + + + + SortOrder + The ordering to use for sorting an array of GuiObjects. + + + + Name + Sort by alphabetical ordering of the Name property. + + + + + LayoutOrder + Sort using the less than operator on the LayoutOrder property of GuiObject. + + + + + Custom + true + + + + + + ZIndexBehavior + Controls the behavior of the ZIndex property. + + + + Global + The ZIndex property will override the default value computed from the depth in the hierarchy. + + + + + Sibling + The ZIndex property will control the order that the GuiObject will be rendered relative to its siblings. + + + + + + ScaleType + Controls how an image is displayed. + + + + Stretch + Force the image to fill the available space. + + + + + Slice + Use the SliceCenter property to stretch the middle of the image but maintain crisp borders. + + + + + Tile + Tile the image using the TileSize property. + + + + + Fit + Size the image to the largest size that will fit in the available space while maintaining aspect ratio. + + + + + Crop + Fill the available space, maintaining aspect ratio by cropping the edges if necessary. + + + + + diff --git a/Client2021/SyntaxPlayerBeta.exe b/Client2021/SyntaxPlayerBeta.exe new file mode 100644 index 0000000..1b5f2a8 Binary files /dev/null and b/Client2021/SyntaxPlayerBeta.exe differ diff --git a/Client2021/content/avatar/character.rbxm b/Client2021/content/avatar/character.rbxm new file mode 100644 index 0000000..248da30 --- /dev/null +++ b/Client2021/content/avatar/character.rbxm @@ -0,0 +1,952 @@ + + null + nil + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + erik.cassel + RBX1 + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 194 + + 0 + 19.5 + 22.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 1 + 1 + + + + + 2 + 2 + + 0 + Mesh + + 0 + 0 + 0 + + + 1.25 + 1.25 + 1.25 + + + + 1 + 1 + 1 + + + + + + 5 + face + rbxasset://textures/face.png + 0 + + + + + + 0 + 0.600000024 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + HairAttachment + + + + + + 0 + 0.600000024 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + HatAttachment + + + + + + 0 + 0 + -0.600000024 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + FaceFrontAttachment + + + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + FaceCenterAttachment + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 194 + + 0 + 18 + 22.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 2 + 0 + true + 256 + Torso + 0 + 0 + 0 + 2 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 2 + 1 + + + + + 5 + roblox + + 0 + + + + + + 0 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckAttachment + + + + + + 0 + 0 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyFrontAttachment + + + + + + 0 + 0 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyBackAttachment + + + + + + -1 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftCollarAttachment + + + + + + 1 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightCollarAttachment + + + + + + 0 + -1 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistFrontAttachment + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistCenterAttachment + + + + + + 0 + -1 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistBackAttachment + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 194 + + 1.5 + 18 + 22.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Left Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 1.0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderAttachment + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftGripAttachment + + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 194 + + -1.5 + 18 + 22.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Right Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 1.0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderAttachment + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightGripAttachment + + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 194 + + 0.5 + 16 + 22.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Left Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftFootAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 194 + + -0.5 + 16 + 22.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Right Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightFootAttachment + false + + + + + + 0 + 100 + 100 + 0 + 50 + 100 + 89 + Humanoid + 100 + 2 + 16 + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 194 + + 0 + 18 + 22.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 0 + 0 + true + 256 + HumanoidRootPart + 0 + 0 + 0 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 1 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 2 + 1 + + + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootAttachment + false + + + + + \ No newline at end of file diff --git a/Client2021/content/avatar/characterR15.rbxm b/Client2021/content/avatar/characterR15.rbxm new file mode 100644 index 0000000..0dc757a --- /dev/null +++ b/Client2021/content/avatar/characterR15.rbxm @@ -0,0 +1,2332 @@ + + null + nil + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + Player + RBX9909D4D409004F18956C91B88A6A32A3 + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 311 + + -8.03512478 + 2.3499999 + -12.6754742 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + HumanoidRootPart + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 1 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + 1 + 1 + + 2 + 2 + 1 + + + + + + -0 + -0 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootRigAttachment + false + + + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -9.2211628 + 2.14999962 + -11.7571526 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 0.299999982 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532219986 + LeftHand + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999762 + 0.299999982 + 0.999999881 + + + + + + 0.000478863716 + 0.149999991 + 5.96046448e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftWristRigAttachment + false + + + + + + -1.1920929e-007 + -0.149999633 + -1.46306121e-007 + 1 + 0 + -0 + 0 + 6.12323426e-017 + 1 + 0 + -1 + 6.12323426e-017 + + LeftGripAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -9.2211628 + 2.84999967 + -11.7571526 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 1.20000029 + 1 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532219991 + LeftLowerArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999762 + 1.20000029 + 1 + + + + + + 0.000478506088 + 0.25000003 + 7.64462551e-020 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftElbowRigAttachment + false + + + + + + 0.000478506088 + -0.549999952 + 7.64462551e-020 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftWristRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -9.22116375 + 3.29999995 + -11.7571526 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 1.40000033 + 0.99999994 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532219996 + LeftUpperArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999762 + 1.40000033 + 0.99999994 + + + + + + 0.250109196 + 0.449999809 + 8.94069672e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderRigAttachment + false + + + + + + 0.000479102135 + -0.200000167 + 8.94069672e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftElbowRigAttachment + false + + + + + + 2.38418579e-007 + 0.700000286 + -2.70968314e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -6.84908628 + 2.14999962 + -13.5937948 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999881 + 0.299999982 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532219997 + RightHand + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999881 + 0.299999982 + 0.999999881 + + + + + + 3.57627869e-007 + 0.149999991 + 5.96046448e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightWristRigAttachment + false + + + + + + 0 + -0.149999633 + -1.46306121e-007 + 1 + 0 + -0 + 0 + 6.12323426e-017 + 1 + 0 + -1 + 6.12323426e-017 + + RightGripAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -6.84908628 + 2.84999967 + -13.5937948 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 1.20000029 + 1 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532219999 + RightLowerArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999762 + 1.20000029 + 1 + + + + + + 1.1920929e-007 + 0.25000003 + 7.64462551e-020 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightElbowRigAttachment + false + + + + + + 1.1920929e-007 + -0.549999952 + -6.86244753e-018 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightWristRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -6.84908581 + 3.29999995 + -13.5937958 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.999999642 + 1.40000033 + 0.99999994 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220004 + RightUpperArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.999999642 + 1.40000033 + 0.99999994 + + + + + + -0.250020266 + 0.449999809 + 8.94069672e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderRigAttachment + false + + + + + + -5.96046448e-007 + -0.200000167 + 8.94069672e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightElbowRigAttachment + false + + + + + + -9.53674316e-007 + 0.700000286 + -2.70968314e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.03512478 + 3.19999981 + -12.6754742 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + true + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 2 + 1.60000014 + 1.00000036 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220007 + UpperTorso + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 2 + 1.60000014 + 1.00000036 + + + + + + -5.96046448e-008 + -0.450000018 + 1.1920929e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistRigAttachment + false + + + + + + -5.96046448e-008 + 0.799999952 + 1.1920929e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckRigAttachment + false + + + + + + -1.24989128 + 0.549999952 + 1.1920929e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderRigAttachment + false + + + + + + 1.24998045 + 0.549999952 + 1.1920929e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderRigAttachment + false + + + + + + -5.96046448e-008 + -0.200000048 + -0.499999881 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyFrontAttachment + false + + + + + + -5.96046448e-008 + -0.200000048 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyBackAttachment + false + + + + + + -0.999999881 + 0.800000191 + -7.27397378e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftCollarAttachment + false + + + + + + 0.99999994 + 0.799999952 + 4.61295997e-008 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightCollarAttachment + false + + + + + + 0.0 + 0.8 + 0.0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.43047047 + 0.150000095 + -12.3693666 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 1 + 0.300000191 + 1 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220012 + LeftFoot + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 1 + 0.300000191 + 1 + + + + + + 0 + 0.05 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftAnkleRigAttachment + false + + + + + + 0 + -0.15 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftFootAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.43047047 + 0.950000286 + -12.3693666 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.99999994 + 1.50000036 + 1.00000012 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220017 + LeftLowerLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.99999994 + 1.50000036 + 1.00000012 + + + + + + -0 + 0.249999642 + -1.78813934e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftKneeRigAttachment + false + + + + + + -1.78813934e-007 + -0.749997616 + 6.29340548e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftAnkleRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.43047047 + 1.49999988 + -12.3693666 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 1.00000036 + 1.49999976 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220018 + LeftUpperLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 1.00000036 + 1.49999976 + 0.999999881 + + + + + + 5.96046448e-008 + 0.5 + -1.63912773e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftHipRigAttachment + false + + + + + + 5.96046448e-008 + -0.299999952 + -1.63912773e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftKneeRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -7.63977861 + 0.150000095 + -12.9815807 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.99999994 + 0.300000191 + 1 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220020 + RightFoot + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.99999994 + 0.300000191 + 1 + + + + + + 0 + 0.05 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightAnkleRigAttachment + false + + + + + + 0 + -0.15 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightFootAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -7.63977861 + 0.950000286 + -12.9815807 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 0.99999994 + 1.50000036 + 1.00000012 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220027 + RightLowerLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 0.99999994 + 1.50000036 + 1.00000012 + + + + + + -0 + 0.249999642 + 4.35260044e-005 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightKneeRigAttachment + false + + + + + + -0 + -0.750000477 + 9.82746205e-005 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightAnkleRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -7.63977861 + 1.49999988 + -12.9815807 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + false + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 1.00000048 + 1.49999976 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220031 + RightUpperLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 1.00000048 + 1.49999976 + 0.999999881 + + + + + + -0 + 0.5 + -1.04308128e-007 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightHipRigAttachment + false + + + + + + -0 + -0.299999952 + 4.36005103e-005 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightKneeRigAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.03512478 + 2.19999981 + -12.6754742 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + true + 2 + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + + 1.99999976 + 0.399999976 + 1.00000012 + + -0.5 + 0.5 + 0 + 0 + true + 256 + http://www.roblox.com/asset/?id=532220036 + LowerTorso + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + + 1.99999976 + 0.399999976 + 1.00000012 + + + + + + -1.1920929e-007 + 0.150000036 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootRigAttachment + false + + + + + + -1.1920929e-007 + 0.550000072 + 7.64462551e-020 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistRigAttachment + false + + + + + + -0.500000119 + -0.199999958 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftHipRigAttachment + false + + + + + + 0.499999881 + -0.199999958 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightHipRigAttachment + false + + + + + + 0.0 + -0.2 + 0.0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistCenterAttachment + false + + + + + + 0.0 + -0.2 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistFrontAttachment + false + + + + + + 0.0 + -0.2 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistBackAttachment + false + + + + + + 0 + 100 + 100 + 1.35000002 + 50 + 100 + 89 + Humanoid + 100 + 2 + 1 + 16 + + + + Animator + + + + + BodyWidthScale + 1 + + + + + BodyHeightScale + 1 + + + + + BodyDepthScale + 1 + + + + + HeadScale + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 311 + + -8.03495789 + 4.5 + -12.6752586 + 0.790692151 + 1.11743961e-035 + 0.612213969 + -5.6419155e-036 + 1 + -1.0965738e-035 + -0.612213969 + 5.21646317e-036 + 0.790692151 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 0 + + -1.40129846e-045 + 0 + -1.40129846e-045 + + 1 + 1 + + 2 + 1 + 1 + + + + + 2 + 2 + + 0 + Mesh + + 0 + 0 + 0 + + + 1.25 + 1.25 + 1.25 + + + + 1 + 1 + 1 + + + + + + + 3.93568822e-009 + 0 + -0.000272244215 + 1 + 7.87137555e-009 + 3.02998127e-015 + -7.87137555e-009 + 1 + -4.1444258e-016 + -3.02998127e-015 + 4.14442554e-016 + 1 + + FaceCenterAttachment + false + + + + + + 3.93568866e-009 + 0 + -0.600272298 + 1 + 7.87137555e-009 + 3.02998127e-015 + -7.87137555e-009 + 1 + -4.1444258e-016 + -3.02998127e-015 + 4.14442554e-016 + 1 + + FaceFrontAttachment + false + + + + + + 8.65851391e-009 + 0.599999905 + -0.000272244215 + 1 + 7.87137555e-009 + 3.02998127e-015 + -7.87137555e-009 + 1 + -4.1444258e-016 + -3.02998127e-015 + 4.14442554e-016 + 1 + + HairAttachment + false + + + + + + 8.65851391e-009 + 0.599999905 + -0.000272244215 + 1 + 7.87137555e-009 + 3.02998127e-015 + -7.87137555e-009 + 1 + -4.1444258e-016 + -3.02998127e-015 + 4.14442554e-016 + 1 + + HatAttachment + false + + + + + + -0 + -0.500000119 + -0.000272244215 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckRigAttachment + false + + + + + 5 + face + rbxasset://textures/face.png + 0 + + + + + \ No newline at end of file diff --git a/Client2021/content/avatar/characterR15V2.rbxm b/Client2021/content/avatar/characterR15V2.rbxm new file mode 100644 index 0000000..b02f520 Binary files /dev/null and b/Client2021/content/avatar/characterR15V2.rbxm differ diff --git a/Client2021/content/avatar/characterR15V4.rbxm b/Client2021/content/avatar/characterR15V4.rbxm new file mode 100644 index 0000000..fa1004d Binary files /dev/null and b/Client2021/content/avatar/characterR15V4.rbxm differ diff --git a/Client2021/content/avatar/compositing/CompositExtraSlot0.mesh b/Client2021/content/avatar/compositing/CompositExtraSlot0.mesh new file mode 100644 index 0000000..87b9d85 Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositExtraSlot0.mesh differ diff --git a/Client2021/content/avatar/compositing/CompositExtraSlot1.mesh b/Client2021/content/avatar/compositing/CompositExtraSlot1.mesh new file mode 100644 index 0000000..6874c5e Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositExtraSlot1.mesh differ diff --git a/Client2021/content/avatar/compositing/CompositExtraSlot2.mesh b/Client2021/content/avatar/compositing/CompositExtraSlot2.mesh new file mode 100644 index 0000000..423f198 Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositExtraSlot2.mesh differ diff --git a/Client2021/content/avatar/compositing/CompositExtraSlot3.mesh b/Client2021/content/avatar/compositing/CompositExtraSlot3.mesh new file mode 100644 index 0000000..de05ced Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositExtraSlot3.mesh differ diff --git a/Client2021/content/avatar/compositing/CompositExtraSlot4.mesh b/Client2021/content/avatar/compositing/CompositExtraSlot4.mesh new file mode 100644 index 0000000..ff9cb62 Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositExtraSlot4.mesh differ diff --git a/Client2021/content/avatar/compositing/CompositFullAtlasBaseTexture.mesh b/Client2021/content/avatar/compositing/CompositFullAtlasBaseTexture.mesh new file mode 100644 index 0000000..b7e6595 Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositFullAtlasBaseTexture.mesh differ diff --git a/Client2021/content/avatar/compositing/CompositFullAtlasOverlayTexture.mesh b/Client2021/content/avatar/compositing/CompositFullAtlasOverlayTexture.mesh new file mode 100644 index 0000000..eda1938 Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositFullAtlasOverlayTexture.mesh differ diff --git a/Client2021/content/avatar/compositing/CompositLeftArmBase.mesh b/Client2021/content/avatar/compositing/CompositLeftArmBase.mesh new file mode 100644 index 0000000..5bcc4ae Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositLeftArmBase.mesh differ diff --git a/Client2021/content/avatar/compositing/CompositLeftLegBase.mesh b/Client2021/content/avatar/compositing/CompositLeftLegBase.mesh new file mode 100644 index 0000000..f4712ce Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositLeftLegBase.mesh differ diff --git a/Client2021/content/avatar/compositing/CompositPantsTemplate.mesh b/Client2021/content/avatar/compositing/CompositPantsTemplate.mesh new file mode 100644 index 0000000..756ee03 Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositPantsTemplate.mesh differ diff --git a/Client2021/content/avatar/compositing/CompositQuad.mesh b/Client2021/content/avatar/compositing/CompositQuad.mesh new file mode 100644 index 0000000..192abf2 Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositQuad.mesh differ diff --git a/Client2021/content/avatar/compositing/CompositRightArmBase.mesh b/Client2021/content/avatar/compositing/CompositRightArmBase.mesh new file mode 100644 index 0000000..02f2721 Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositRightArmBase.mesh differ diff --git a/Client2021/content/avatar/compositing/CompositRightLegBase.mesh b/Client2021/content/avatar/compositing/CompositRightLegBase.mesh new file mode 100644 index 0000000..b287939 Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositRightLegBase.mesh differ diff --git a/Client2021/content/avatar/compositing/CompositShirtTemplate.mesh b/Client2021/content/avatar/compositing/CompositShirtTemplate.mesh new file mode 100644 index 0000000..75487e1 Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositShirtTemplate.mesh differ diff --git a/Client2021/content/avatar/compositing/CompositTShirt.mesh b/Client2021/content/avatar/compositing/CompositTShirt.mesh new file mode 100644 index 0000000..b39b8ac Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositTShirt.mesh differ diff --git a/Client2021/content/avatar/compositing/CompositTorsoBase.mesh b/Client2021/content/avatar/compositing/CompositTorsoBase.mesh new file mode 100644 index 0000000..0388bde Binary files /dev/null and b/Client2021/content/avatar/compositing/CompositTorsoBase.mesh differ diff --git a/Client2021/content/avatar/compositing/R15CompositLeftArmBase.mesh b/Client2021/content/avatar/compositing/R15CompositLeftArmBase.mesh new file mode 100644 index 0000000..c262d08 Binary files /dev/null and b/Client2021/content/avatar/compositing/R15CompositLeftArmBase.mesh differ diff --git a/Client2021/content/avatar/compositing/R15CompositRightArmBase.mesh b/Client2021/content/avatar/compositing/R15CompositRightArmBase.mesh new file mode 100644 index 0000000..c745b94 Binary files /dev/null and b/Client2021/content/avatar/compositing/R15CompositRightArmBase.mesh differ diff --git a/Client2021/content/avatar/compositing/R15CompositTorsoBase.mesh b/Client2021/content/avatar/compositing/R15CompositTorsoBase.mesh new file mode 100644 index 0000000..341d8b6 Binary files /dev/null and b/Client2021/content/avatar/compositing/R15CompositTorsoBase.mesh differ diff --git a/Client2021/content/avatar/defaultPants.rbxm b/Client2021/content/avatar/defaultPants.rbxm new file mode 100644 index 0000000..eb935f8 Binary files /dev/null and b/Client2021/content/avatar/defaultPants.rbxm differ diff --git a/Client2021/content/avatar/defaultShirt.rbxm b/Client2021/content/avatar/defaultShirt.rbxm new file mode 100644 index 0000000..9160556 Binary files /dev/null and b/Client2021/content/avatar/defaultShirt.rbxm differ diff --git a/Client2021/content/avatar/heads/head.mesh b/Client2021/content/avatar/heads/head.mesh new file mode 100644 index 0000000..26db950 Binary files /dev/null and b/Client2021/content/avatar/heads/head.mesh differ diff --git a/Client2021/content/avatar/heads/headA.mesh b/Client2021/content/avatar/heads/headA.mesh new file mode 100644 index 0000000..76b2e72 Binary files /dev/null and b/Client2021/content/avatar/heads/headA.mesh differ diff --git a/Client2021/content/avatar/heads/headB.mesh b/Client2021/content/avatar/heads/headB.mesh new file mode 100644 index 0000000..7705a05 Binary files /dev/null and b/Client2021/content/avatar/heads/headB.mesh differ diff --git a/Client2021/content/avatar/heads/headC.mesh b/Client2021/content/avatar/heads/headC.mesh new file mode 100644 index 0000000..4672c59 Binary files /dev/null and b/Client2021/content/avatar/heads/headC.mesh differ diff --git a/Client2021/content/avatar/heads/headD.mesh b/Client2021/content/avatar/heads/headD.mesh new file mode 100644 index 0000000..ff6cd6c Binary files /dev/null and b/Client2021/content/avatar/heads/headD.mesh differ diff --git a/Client2021/content/avatar/heads/headE.mesh b/Client2021/content/avatar/heads/headE.mesh new file mode 100644 index 0000000..d722d29 Binary files /dev/null and b/Client2021/content/avatar/heads/headE.mesh differ diff --git a/Client2021/content/avatar/heads/headF.mesh b/Client2021/content/avatar/heads/headF.mesh new file mode 100644 index 0000000..7c0b311 Binary files /dev/null and b/Client2021/content/avatar/heads/headF.mesh differ diff --git a/Client2021/content/avatar/heads/headG.mesh b/Client2021/content/avatar/heads/headG.mesh new file mode 100644 index 0000000..38db235 Binary files /dev/null and b/Client2021/content/avatar/heads/headG.mesh differ diff --git a/Client2021/content/avatar/heads/headH.mesh b/Client2021/content/avatar/heads/headH.mesh new file mode 100644 index 0000000..fe7365b Binary files /dev/null and b/Client2021/content/avatar/heads/headH.mesh differ diff --git a/Client2021/content/avatar/heads/headI.mesh b/Client2021/content/avatar/heads/headI.mesh new file mode 100644 index 0000000..8ff7ba1 Binary files /dev/null and b/Client2021/content/avatar/heads/headI.mesh differ diff --git a/Client2021/content/avatar/heads/headJ.mesh b/Client2021/content/avatar/heads/headJ.mesh new file mode 100644 index 0000000..ff76d44 Binary files /dev/null and b/Client2021/content/avatar/heads/headJ.mesh differ diff --git a/Client2021/content/avatar/heads/headK.mesh b/Client2021/content/avatar/heads/headK.mesh new file mode 100644 index 0000000..b08bffc Binary files /dev/null and b/Client2021/content/avatar/heads/headK.mesh differ diff --git a/Client2021/content/avatar/heads/headL.mesh b/Client2021/content/avatar/heads/headL.mesh new file mode 100644 index 0000000..645531b Binary files /dev/null and b/Client2021/content/avatar/heads/headL.mesh differ diff --git a/Client2021/content/avatar/heads/headM.mesh b/Client2021/content/avatar/heads/headM.mesh new file mode 100644 index 0000000..ba144e8 Binary files /dev/null and b/Client2021/content/avatar/heads/headM.mesh differ diff --git a/Client2021/content/avatar/heads/headN.mesh b/Client2021/content/avatar/heads/headN.mesh new file mode 100644 index 0000000..6fc4620 Binary files /dev/null and b/Client2021/content/avatar/heads/headN.mesh differ diff --git a/Client2021/content/avatar/heads/headO.mesh b/Client2021/content/avatar/heads/headO.mesh new file mode 100644 index 0000000..a41f85f Binary files /dev/null and b/Client2021/content/avatar/heads/headO.mesh differ diff --git a/Client2021/content/avatar/heads/headP.mesh b/Client2021/content/avatar/heads/headP.mesh new file mode 100644 index 0000000..cdb1647 Binary files /dev/null and b/Client2021/content/avatar/heads/headP.mesh differ diff --git a/Client2021/content/avatar/meshes/leftarm.mesh b/Client2021/content/avatar/meshes/leftarm.mesh new file mode 100644 index 0000000..6e8bb63 Binary files /dev/null and b/Client2021/content/avatar/meshes/leftarm.mesh differ diff --git a/Client2021/content/avatar/meshes/leftleg.mesh b/Client2021/content/avatar/meshes/leftleg.mesh new file mode 100644 index 0000000..aba3a29 Binary files /dev/null and b/Client2021/content/avatar/meshes/leftleg.mesh differ diff --git a/Client2021/content/avatar/meshes/rightarm.mesh b/Client2021/content/avatar/meshes/rightarm.mesh new file mode 100644 index 0000000..14a52a4 Binary files /dev/null and b/Client2021/content/avatar/meshes/rightarm.mesh differ diff --git a/Client2021/content/avatar/meshes/rightleg.mesh b/Client2021/content/avatar/meshes/rightleg.mesh new file mode 100644 index 0000000..dab065d Binary files /dev/null and b/Client2021/content/avatar/meshes/rightleg.mesh differ diff --git a/Client2021/content/avatar/meshes/torso.mesh b/Client2021/content/avatar/meshes/torso.mesh new file mode 100644 index 0000000..43d58d1 Binary files /dev/null and b/Client2021/content/avatar/meshes/torso.mesh differ diff --git a/Client2021/content/avatar/morpherEditorR15.rbxmx b/Client2021/content/avatar/morpherEditorR15.rbxmx new file mode 100644 index 0000000..7af6e5d --- /dev/null +++ b/Client2021/content/avatar/morpherEditorR15.rbxmx @@ -0,0 +1,4041 @@ + + false + null + nil + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + R15 + RBX081D55DA3E944921A37ACF5DF5DC888A + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 7.66999817 + 2.30299997 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + false + 256 + HumanoidRootPart + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 3 + 0 + 1 + + 0 + 0 + 0 + + 1 + 1 + + 2 + 1.96000004 + 1 + + + + + + 0 + -0.342999995 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootRigAttachment + + false + + + + OriginalPosition + + + 0 + -0.349999994 + 0 + + + + + + + OriginalSize + + + 2 + 2 + 1 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 6.16999769 + 2.10695553 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 0.300030679 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715537 + http://www.roblox.com/asset/?id=1699715537 + LeftHand + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.999999762 + 0.29403007 + 0.999999881 + + + + + + 0.000478982925 + 0.122544497 + 5.96046448e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftWristRigAttachment + + false + + + + OriginalPosition + + + 0.000478982925 + 0.125045404 + 5.96046448e-08 + + + + + + + + 0 + -0.146955431 + -1.46306121e-07 + 1 + 0 + -0 + 0 + 6.12323426e-17 + 1 + 0 + -1 + 6.12323426e-17 + + LeftGripAttachment + + false + + + + OriginalPosition + + + 0 + -0.149954513 + -1.46306121e-07 + + + + + + + + 0.000478625298 + -0.490910143 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + 0.000478982925 + 0.122544497 + 5.96046448e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + LeftWrist + RBX3F40768EF729487C8794F9B4240FB7C4 + RBX22C13C16C5FB47AABB4365E4AA62349F + + + + + + OriginalSize + + + 0.999999762 + 0.300030679 + 0.999999881 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 6.16999817 + 2.72041011 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.999999642 + 1.05191803 + 1 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715541 + http://www.roblox.com/asset/?id=1699715541 + LeftLowerArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.999999642 + 1.03087974 + 1 + + + + + + 0.000478625298 + 0.253514439 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftElbowRigAttachment + + false + + + + OriginalPosition + + + 0.000478625298 + 0.258688211 + 7.64462551e-20 + + + + + + + + 0.000478625298 + -0.490910143 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftWristRigAttachment + + false + + + + OriginalPosition + + + 0.000478625298 + -0.5009287 + 7.64462551e-20 + + + + + + + + 0.000479221344 + -0.327375263 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + 0.000478625298 + 0.253514439 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + LeftElbow + RBX65B95BED76AB4402A5C58B280CC87F73 + RBX3F40768EF729487C8794F9B4240FB7C4 + + + + + + OriginalSize + + + 0.999999642 + 1.05191803 + 1 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 6.16999769 + 3.30129981 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 1.16867065 + 0.99999994 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715550 + http://www.roblox.com/asset/?id=1699715550 + LeftUpperArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.999999762 + 1.14529729 + 0.99999994 + + + + + + 0.500000358 + 0.386440158 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderRigAttachment + + false + + + + OriginalPosition + + + 0.500000358 + 0.394326687 + 8.94069672e-08 + + + + + + + + 0.000479221344 + -0.327375263 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftElbowRigAttachment + + false + + + + OriginalPosition + + + 0.000479221344 + -0.334056377 + 8.94069672e-08 + + + + + + + + 2.38418579e-07 + 0.572640121 + -2.70968314e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderAttachment + + false + + + + OriginalPosition + + + 2.38418579e-07 + 0.584326625 + -2.70968314e-08 + + + + + + + + -1 + 0.551756799 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + 0.500000358 + 0.386440158 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + LeftShoulder + RBXA9943A145CA34B9199D5810AE7AC1AD5 + RBX65B95BED76AB4402A5C58B280CC87F73 + + + + + + OriginalSize + + + 0.999999762 + 1.16867065 + 0.99999994 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 9.16999817 + 2.10695553 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.999999881 + 0.300030679 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715557 + http://www.roblox.com/asset/?id=1699715557 + RightHand + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.999999881 + 0.29403007 + 0.999999881 + + + + + + 3.57627869e-07 + 0.122544497 + 5.96046448e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightWristRigAttachment + + false + + + + OriginalPosition + + + 3.57627869e-07 + 0.125045404 + 5.96046448e-08 + + + + + + + + 0 + -0.146955431 + -1.46306121e-07 + 1 + 0 + -0 + 0 + 6.12323426e-17 + 1 + 0 + -1 + 6.12323426e-17 + + RightGripAttachment + + false + + + + OriginalPosition + + + 0 + -0.149954513 + -1.46306121e-07 + + + + + + + + 1.1920929e-07 + -0.490910143 + -6.86244753e-18 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + 3.57627869e-07 + 0.122544497 + 5.96046448e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + RightWrist + RBX36AF3E43DBBF47F29C81B6C3A652EF28 + RBX138B2F31E7B64B03B104DB392895E6A2 + + + + + + OriginalSize + + + 0.999999881 + 0.300030679 + 0.999999881 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 9.16999817 + 2.72041011 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.999999762 + 1.05191803 + 1 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715562 + http://www.roblox.com/asset/?id=1699715562 + RightLowerArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.999999762 + 1.03087974 + 1 + + + + + + 1.1920929e-07 + 0.253407896 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightElbowRigAttachment + + false + + + + OriginalPosition + + + 1.1920929e-07 + 0.258579493 + 7.64462551e-20 + + + + + + + + 1.1920929e-07 + -0.490910143 + -6.86244753e-18 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightWristRigAttachment + + false + + + + OriginalPosition + + + 1.1920929e-07 + -0.5009287 + -6.86244753e-18 + + + + + + + + -5.96046448e-07 + -0.327481806 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + 1.1920929e-07 + 0.253407896 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + RightElbow + RBXE08AA1E71CF7447581F14072A70CB0ED + RBX36AF3E43DBBF47F29C81B6C3A652EF28 + + + + + + OriginalSize + + + 0.999999762 + 1.05191803 + 1 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 9.16999912 + 3.30129981 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.999999642 + 1.16867065 + 0.99999994 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715576 + http://www.roblox.com/asset/?id=1699715576 + RightUpperArm + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.999999642 + 1.14529729 + 0.99999994 + + + + + + -0.500000715 + 0.386440158 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderRigAttachment + + false + + + + OriginalPosition + + + -0.500000715 + 0.394326687 + 8.94069672e-08 + + + + + + + + -5.96046448e-07 + -0.327481806 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightElbowRigAttachment + + false + + + + OriginalPosition + + + -5.96046448e-07 + -0.334165096 + 8.94069672e-08 + + + + + + + + -8.34465027e-07 + 0.572640121 + -2.70968314e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderAttachment + + false + + + + OriginalPosition + + + -8.34465027e-07 + 0.584326625 + -2.70968314e-08 + + + + + + + + 0.99999994 + 0.551756799 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -0.500000715 + 0.386440158 + 8.94069672e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + RightShoulder + RBXA9943A145CA34B9199D5810AE7AC1AD5 + RBXE08AA1E71CF7447581F14072A70CB0ED + + + + + + OriginalSize + + + 0.999999642 + 1.16867065 + 0.99999994 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 7.66999817 + 3.13598323 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 2 + 1.60003424 + 1.00000036 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715593 + http://www.roblox.com/asset/?id=1699715593 + UpperTorso + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 2 + 1.56803358 + 1.00000036 + + + + + + -5.96046448e-08 + -0.783986032 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistRigAttachment + + false + + + + OriginalPosition + + + -5.96046448e-08 + -0.799985707 + 1.1920929e-07 + + + + + + + + -5.96046448e-08 + 0.784016788 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckRigAttachment + + false + + + + OriginalPosition + + + -5.96046448e-08 + 0.800017118 + 1.1920929e-07 + + + + + + + + -1 + 0.551756799 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderRigAttachment + + false + + + + OriginalPosition + + + -1 + 0.56301713 + 1.1920929e-07 + + + + + + + + 0.99999994 + 0.551756799 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderRigAttachment + + false + + + + OriginalPosition + + + 0.99999994 + 0.56301713 + 1.1920929e-07 + + + + + + + + -5.96046448e-08 + -0.195985973 + -0.499999881 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyFrontAttachment + + false + + + + OriginalPosition + + + -5.96046448e-08 + -0.199985683 + -0.499999881 + + + + + + + + -5.96046448e-08 + -0.195985973 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyBackAttachment + + false + + + + OriginalPosition + + + -5.96046448e-08 + -0.199985683 + 0.5 + + + + + + + + -0.999999881 + 0.784016669 + -7.27397378e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftCollarAttachment + + false + + + + OriginalPosition + + + -0.999999881 + 0.800016999 + -7.27397378e-08 + + + + + + + + 0.99999994 + 0.78401643 + 4.61295997e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightCollarAttachment + + false + + + + OriginalPosition + + + 0.99999994 + 0.800016761 + 4.61295997e-08 + + + + + + + + -5.05859497e-08 + 0.784016788 + 7.11172419e-08 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckAttachment + + false + + + + OriginalPosition + + + -5.05859497e-08 + 0.800017118 + 7.11172419e-08 + + + + + + + + -1.1920929e-07 + 0.196024418 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -5.96046448e-08 + -0.783986032 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + Waist + RBXB8FFBCE768F34D8D8D966F48788528E0 + RBXA9943A145CA34B9199D5810AE7AC1AD5 + + + + + + OriginalSize + + + 2 + 1.60003424 + 1.00000036 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 7.16999817 + 0.147000074 + -6.00000095 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 1 + 0.300000191 + 1 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715602 + http://www.roblox.com/asset/?id=1699715602 + LeftFoot + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 1 + 0.294000179 + 1 + + + + + + -1.78813934e-07 + 0.0999008864 + -1.7222776e-06 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftAnkleRigAttachment + + false + + + + OriginalPosition + + + -1.78813934e-07 + 0.101939678 + -1.7222776e-06 + + + + + + + + -1.1920929e-07 + -0.536214054 + -2.21401592e-06 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -1.78813934e-07 + 0.0999008864 + -1.7222776e-06 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + LeftAnkle + RBX893C6CF7B2374480B69BBBE805825872 + RBX0FB02BF4485A49C89C7ABEF4D19DB3BE + + + + + + OriginalSize + + + 1 + 0.300000191 + 1 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 7.16999817 + 0.783115029 + -6.00000048 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.99999994 + 1.1930927 + 0.999999523 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715610 + http://www.roblox.com/asset/?id=1699715610 + LeftLowerLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.99999994 + 1.16923082 + 0.999999523 + + + + + + 2.98023224e-08 + 0.371438056 + -1.60860594e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftKneeRigAttachment + + false + + + + OriginalPosition + + + 2.98023224e-08 + 0.379018426 + -1.60860594e-07 + + + + + + + + -1.1920929e-07 + -0.536214054 + -2.21401592e-06 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftAnkleRigAttachment + + false + + + + OriginalPosition + + + -1.1920929e-07 + -0.547157168 + -2.21401592e-06 + + + + + + + + 8.94069672e-08 + -0.393080324 + -4.29081496e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + 2.98023224e-08 + 0.371438056 + -1.60860594e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + LeftKnee + RBXAF9FA9B09EA04DFEA9D919064EDE7D6F + RBX893C6CF7B2374480B69BBBE805825872 + + + + + + OriginalSize + + + 0.99999994 + 1.1930927 + 0.999999523 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 7.16999817 + 1.54763341 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 1.00000036 + 1.21656859 + 0.999999881 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715616 + http://www.roblox.com/asset/?id=1699715616 + LeftUpperLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 1.00000036 + 1.19223726 + 0.999999881 + + + + + + 5.96046448e-08 + 0.412366509 + -1.63912773e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftHipRigAttachment + + false + + + + OriginalPosition + + + 5.96046448e-08 + 0.420782149 + -1.63912773e-07 + + + + + + + + 8.94069672e-08 + -0.393080324 + -4.29081496e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftKneeRigAttachment + + false + + + + OriginalPosition + + + 8.94069672e-08 + -0.401102364 + -4.29081496e-07 + + + + + + + + -0.500000119 + -0.195972815 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + 5.96046448e-08 + 0.412366509 + -1.63912773e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + LeftHip + RBXB8FFBCE768F34D8D8D966F48788528E0 + RBXAF9FA9B09EA04DFEA9D919064EDE7D6F + + + + + + OriginalSize + + + 1.00000036 + 1.21656859 + 0.999999881 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 8.16999817 + 0.147000074 + -5.99999952 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.99999994 + 0.300000191 + 1 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715627 + http://www.roblox.com/asset/?id=1699715627 + RightFoot + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.99999994 + 0.294000179 + 1 + + + + + + -0 + 0.0999007672 + 7.64477954e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightAnkleRigAttachment + + false + + + + OriginalPosition + + + -0 + 0.101939559 + 7.64477954e-05 + + + + + + + + -0 + -0.536214054 + 7.62689815e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -0 + 0.0999007672 + 7.64477954e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + RightAnkle + RBX934B19001E8E48CD955C16029E83167A + RBXC1076273B95B402B92B9B47AC621CE81 + + + + + + OriginalSize + + + 0.99999994 + 0.300000191 + 1 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 8.16999817 + 0.78311491 + -5.99999952 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 0.99999994 + 1.19309282 + 1.00000012 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715632 + http://www.roblox.com/asset/?id=1699715632 + RightLowerLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 0.99999994 + 1.16923094 + 1.00000012 + + + + + + -0 + 0.371590823 + 2.5553607e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightKneeRigAttachment + + false + + + + OriginalPosition + + + -0 + 0.379174292 + 2.5553607e-05 + + + + + + + + -0 + -0.536214054 + 7.62689815e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightAnkleRigAttachment + + false + + + + OriginalPosition + + + -0 + -0.547157168 + 7.62689815e-05 + + + + + + + + -0 + -0.392927587 + -2.18767891e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -0 + 0.371590823 + 2.5553607e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + RightKnee + RBX1174DFBAABA34412840A7F27FF9DDCE9 + RBX934B19001E8E48CD955C16029E83167A + + + + + + OriginalSize + + + 0.99999994 + 1.19309282 + 1.00000012 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 8.16999817 + 1.54763329 + -5.99995232 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 1.00000048 + 1.21656859 + 0.99996686 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715641 + http://www.roblox.com/asset/?id=1699715641 + RightUpperLeg + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 1.00000048 + 1.19223726 + 0.99996686 + + + + + + -0 + 0.412366629 + -6.67300628e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightHipRigAttachment + + false + + + + OriginalPosition + + + -0 + 0.420782268 + -6.67300628e-05 + + + + + + + + -0 + -0.392927587 + -2.18767891e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightKneeRigAttachment + + false + + + + OriginalPosition + + + -0 + -0.400946498 + -2.18767891e-05 + + + + + + + + 0.499999881 + -0.195972815 + -1.91208565e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -0 + 0.412366629 + -6.67300628e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + RightHip + RBXB8FFBCE768F34D8D8D966F48788528E0 + RBX1174DFBAABA34412840A7F27FF9DDCE9 + + + + + + OriginalSize + + + 1.00000048 + 1.21656859 + 0.99996686 + + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 7.66999817 + 2.15597272 + -6 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + 2 + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + + 1.99999976 + 0.400055438 + 1.00000012 + + -0.5 + 0.5 + 0 + 0 + false + 256 + http://www.roblox.com/asset/?id=1699715652 + http://www.roblox.com/asset/?id=1699715652 + LowerTorso + + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 1.99999976 + 0.392054349 + 1.00000012 + + + + + + -1.1920929e-07 + -0.195972815 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RootRigAttachment + + false + + + + OriginalPosition + + + -1.1920929e-07 + -0.199972257 + -0 + + + + + + + + -1.1920929e-07 + 0.196024418 + 7.64462551e-20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistRigAttachment + + false + + + + OriginalPosition + + + -1.1920929e-07 + 0.200024918 + 7.64462551e-20 + + + + + + + + -0.500000119 + -0.195972815 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftHipRigAttachment + + false + + + + OriginalPosition + + + -0.500000119 + -0.199972257 + -0 + + + + + + + + 0.499999881 + -0.195972815 + -1.91208565e-05 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightHipRigAttachment + + false + + + + OriginalPosition + + + 0.499999881 + -0.199972257 + -1.91208565e-05 + + + + + + + + -4.2200088e-07 + -0.195972815 + -1.65436123e-24 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistCenterAttachment + + false + + + + OriginalPosition + + + -4.2200088e-07 + -0.199972257 + -1.65436123e-24 + + + + + + + + -1.32219867e-07 + -0.195972815 + -0.50000006 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistFrontAttachment + + false + + + + OriginalPosition + + + -1.32219867e-07 + -0.199972257 + -0.50000006 + + + + + + + + -1.46413214e-07 + -0.195972815 + 0.50000006 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistBackAttachment + + false + + + + OriginalPosition + + + -1.46413214e-07 + -0.199972257 + 0.50000006 + + + + + + + + 0 + -0.342999995 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -1.1920929e-07 + -0.195972815 + -0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + Root + RBX081D55DA3E944921A37ACF5DF5DC888A + RBXB8FFBCE768F34D8D8D966F48788528E0 + + + + + + OriginalSize + + + 1.99999976 + 0.400055438 + 1.00000012 + + + + + + + true + true + true + 2 + 0 + 0 + 100 + 1.32300007 + + 1 + 0.980000019 + 1 + + 1 + 50 + 100 + 89 + Humanoid + 100 + 2 + 1 + + 16 + + + + BodyDepthScale + + 1 + + + + + BodyHeightScale + + 0.98000001907348632813 + + + + + BodyProportionScale + + 0 + + + + + BodyTypeScale + + 0 + + + + + BodyWidthScale + + 1 + + + + + HeadScale + + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 7.66999817 + 4.42000008 + -5.99972773 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + 0 + 4293256415 + + false + + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + false + 256 + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + 1 + 1 + + 2 + 1 + 1 + + + + + 2 + 2 + + 0 + Mesh + + 0 + 0 + 0 + + + 1.25 + 1.25 + 1.25 + + + + + 1 + 1 + 1 + + + + + OriginalSize + + + 1.25 + 1.25 + 1.25 + + + + + + + + 3.93568822e-09 + 0 + -0.000272244215 + 1 + 7.87137555e-09 + 3.02998127e-15 + -7.87137555e-09 + 1 + -4.1444258e-16 + -3.02998127e-15 + 4.14442554e-16 + 1 + + FaceCenterAttachment + + false + + + + OriginalPosition + + + 3.93568822e-09 + 0 + -0.000272244215 + + + + + + + + 3.93568866e-09 + 0 + -0.600272298 + 1 + 7.87137555e-09 + 3.02998127e-15 + -7.87137555e-09 + 1 + -4.1444258e-16 + -3.02998127e-15 + 4.14442554e-16 + 1 + + FaceFrontAttachment + + false + + + + OriginalPosition + + + 3.93568866e-09 + 0 + -0.600272298 + + + + + + + + 8.65851391e-09 + 0.599999905 + -0.000272244215 + 1 + 7.87137555e-09 + 3.02998127e-15 + -7.87137555e-09 + 1 + -4.1444258e-16 + -3.02998127e-15 + 4.14442554e-16 + 1 + + HairAttachment + + false + + + + OriginalPosition + + + 8.65851391e-09 + 0.599999905 + -0.000272244215 + + + + + + + + 8.65851391e-09 + 0.599999905 + -0.000272244215 + 1 + 7.87137555e-09 + 3.02998127e-15 + -7.87137555e-09 + 1 + -4.1444258e-16 + -3.02998127e-15 + 4.14442554e-16 + 1 + + HatAttachment + + false + + + + OriginalPosition + + + 8.65851391e-09 + 0.599999905 + -0.000272244215 + + + + + + + + -0 + -0.500000119 + -0.000272244215 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckRigAttachment + + false + + + + OriginalPosition + + + -0 + -0.500000119 + -0.000272244215 + + + + + + + 4294967295 + 5 + face + + rbxasset://textures/face.png + 0 + + + + + + -5.96046448e-08 + 0.784016788 + 1.1920929e-07 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + -0 + -0.500000119 + -0.000272244215 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + false + 0 + Neck + RBXA9943A145CA34B9199D5810AE7AC1AD5 + RBX3A01263AEC0C4FB289F443C3B3EEF144 + + + + + + OriginalSize + + + 2 + 1 + 1 + + + + + + + false + + Animate + {023B546E-F0DD-4EFB-A64F-BC4BC43CBC7B} + + + + + + cheer + + + + + + http://www.roblox.com/asset/?id=507770677 + CheerAnim + + + + + + + climb + + + + + + http://www.roblox.com/asset/?id=507765644 + ClimbAnim + + + + + + + dance + + + + + + http://www.roblox.com/asset/?id=507771019 + Animation1 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507771955 + Animation2 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507772104 + Animation3 + + + + + Weight + + 10 + + + + + + + dance2 + + + + + + http://www.roblox.com/asset/?id=507776043 + Animation1 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507776720 + Animation2 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507776879 + Animation3 + + + + + Weight + + 10 + + + + + + + dance3 + + + + + + http://www.roblox.com/asset/?id=507777268 + Animation1 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507777451 + Animation2 + + + + + Weight + + 10 + + + + + + http://www.roblox.com/asset/?id=507777623 + Animation3 + + + + + Weight + + 10 + + + + + + + fall + + + + + + http://www.roblox.com/asset/?id=507767968 + FallAnim + + + + + + + idle + + + + + + http://www.roblox.com/asset/?id=507766388 + Animation1 + + + + + Weight + + 9 + + + + + + http://www.roblox.com/asset/?id=507766666 + Animation2 + + + + + Weight + + 1 + + + + + + + jump + + + + + + http://www.roblox.com/asset/?id=507765000 + JumpAnim + + + + + + + laugh + + + + + + http://www.roblox.com/asset/?id=507770818 + LaughAnim + + + + + + + point + + + + + + http://www.roblox.com/asset/?id=507770453 + PointAnim + + + + + + + run + + + + + + http://www.roblox.com/asset/?id=507767714 + RunAnim + + + + + + + sit + + + + + + http://www.roblox.com/asset/?id=507768133 + SitAnim + + + + + + + swim + + + + + + http://www.roblox.com/asset/?id=507784897 + Swim + + + + + + + swimidle + + + + + + http://www.roblox.com/asset/?id=481825862 + SwimIdle + + + + + + + toollunge + + + + + + http://www.roblox.com/asset/?id=522638767 + ToolLungeAnim + + + + + + + toolnone + + + + + + http://www.roblox.com/asset/?id=507768375 + ToolNoneAnim + + + + + + + toolslash + + + + + + http://www.roblox.com/asset/?id=522635514 + ToolSlashAnim + + + + + + + walk + + + + + + http://www.roblox.com/asset/?id=540798782 + WalkAnim + + + + + + + wave + + + + + + http://www.roblox.com/asset/?id=507770239 + WaveAnim + + + + + + + \ No newline at end of file diff --git a/Client2021/content/avatar/morpherEditorR6.rbxmx b/Client2021/content/avatar/morpherEditorR6.rbxmx new file mode 100644 index 0000000..ca7f595 --- /dev/null +++ b/Client2021/content/avatar/morpherEditorR6.rbxmx @@ -0,0 +1,1121 @@ + + false + null + nil + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + R6 + RBX268D3553F8164BE0944302D499E3B918 + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 10.7362061 + 5.16300297 + -2.11117506 + 0.766051888 + 0 + 0.642794013 + 0 + 1 + 0 + -0.642794013 + 0 + 0.766051888 + + true + 0 + 4288914085 + + false + + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 1 + 1 + + + + + 4294967295 + 5 + face + + rbxasset://textures/face.png + 0 + + + + + 2 + 2 + + 0 + Mesh + + 0 + 0 + 0 + + + 1.25 + 1.25 + 1.25 + + + + + 1 + 1 + 1 + + + + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + FaceCenterAttachment + + false + + + + + + 0 + 0 + -0.600000024 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + FaceFrontAttachment + + false + + + + + + 0 + 0.600000024 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + HairAttachment + + false + + + + + + 0 + 0.600000024 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + HatAttachment + + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 10.7362061 + 3.66300297 + -2.11117506 + 0.766051888 + 0 + 0.642794013 + 0 + 1 + 0 + -0.642794013 + 0 + 0.766051888 + + true + 0 + 4288914085 + + false + + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 2 + 0 + true + 256 + Torso + 0 + 0 + 0 + 2 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 2 + 1 + + + + + + 1 + 0.5 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + -1 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + -1 + 0 + 0 + + 0 + false + 0.100000001 + Right Shoulder + RBX335EB02A1E034C03BDB8817CC0227D43 + RBXF656CD68CA394FF3B913BD5A491CF6F1 + + + + + + + -1 + 0.5 + 0 + 0 + 0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + + 0.5 + 0.5 + 0 + 0 + 0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + 0 + false + 0.100000001 + Left Shoulder + RBX335EB02A1E034C03BDB8817CC0227D43 + RBX6746C39D16F440CDAF23D54B9D99B38A + + + + + + + 1 + -1 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + -1 + 0 + 0 + + + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + -1 + 0 + 0 + + 0 + false + 0.100000001 + Right Hip + RBX335EB02A1E034C03BDB8817CC0227D43 + RBXF436A38E2161481E82066DF8D79AB417 + + + + + + + -1 + -1 + 0 + 0 + 0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + + -0.5 + 1 + 0 + 0 + 0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + 0 + false + 0.100000001 + Left Hip + RBX335EB02A1E034C03BDB8817CC0227D43 + RBX1515CFDCBD8E4B02B9C4053FDCB3F8B7 + + + + + + + 0 + 1 + 0 + -1 + 0 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + + + 0 + -0.5 + 0 + -1 + 0 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + + 0 + false + 0.100000001 + Neck + RBX335EB02A1E034C03BDB8817CC0227D43 + RBX268D3553F8164BE0944302D499E3B918 + + + + + + + 0 + 0 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyBackAttachment + + false + + + + + + 0 + 0 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyFrontAttachment + + false + + + + + + -1 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftCollarAttachment + + false + + + + + + 0 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckAttachment + + false + + + + + + 1 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightCollarAttachment + + false + + + + + + 0 + -1 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistBackAttachment + + false + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistCenterAttachment + + false + + + + + + 0 + -1 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistFrontAttachment + + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 9.58712769 + 3.66300297 + -1.1469841 + 0.766051888 + 0 + 0.642794013 + 0 + 1 + 0 + -0.642794013 + 0 + 0.766051888 + + true + 0 + 4288914085 + + false + + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Left Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 0.5 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderAttachment + + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 11.8852844 + 3.66300297 + -3.07536602 + 0.766051888 + 0 + 0.642794013 + 0 + 1 + 0 + -0.642794013 + 0 + 0.766051888 + + true + 0 + 4294939796 + + false + + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Right Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 0.5 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderAttachment + + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 10.3531799 + 1.66300297 + -1.78977799 + 0.766051888 + 0 + 0.642794013 + 0 + 1 + 0 + -0.642794013 + 0 + 0.766051888 + + true + 0 + 4294939796 + + false + + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Left Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 11.1192322 + 1.66300297 + -2.43257213 + 0.766051888 + 0 + 0.642794013 + 0 + 1 + 0 + -0.642794013 + 0 + 0.766051888 + + true + 0 + 4288914085 + + false + + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Right Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + true + true + true + 2 + 100 + 0 + 0 + 0 + + 1 + 1 + 1 + + 1 + 50 + 0 + 89 + Humanoid + 100 + 2 + 0 + + 16 + + + + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 10.7362061 + 3.66300297 + -2.11117506 + 0.766051888 + 2.24691483e-39 + 0.642794013 + 2.93314189e-39 + 1 + 0 + -0.642794013 + -1.88538683e-39 + 0.766051888 + + true + 0 + 4279069100 + + false + + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 0 + 0 + true + 256 + HumanoidRootPart + 0 + 0 + 0 + 0 + 0 + + 0 + 0 + 0 + + + -0.5 + 0.5 + 0 + 0 + 1 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 2 + 1 + + + + + + 0 + 0 + 0 + -1 + 0 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + + + 0 + 0 + 0 + -1 + 0 + 0 + 0 + 0 + 1 + 0 + 1 + -0 + + 0 + false + 0.100000001 + RootJoint + RBXC27DD24388A54334A651B4AE62D07FEC + RBX335EB02A1E034C03BDB8817CC0227D43 + + + + + + \ No newline at end of file diff --git a/Client2021/content/avatar/scripts/humanoidAnimateLocalKeyframe.rbxm b/Client2021/content/avatar/scripts/humanoidAnimateLocalKeyframe.rbxm new file mode 100644 index 0000000..ad71482 --- /dev/null +++ b/Client2021/content/avatar/scripts/humanoidAnimateLocalKeyframe.rbxm @@ -0,0 +1,664 @@ + + null + nil + + + false + + Animate + animTable[animName][idx].weight) do + roll = roll - animTable[animName][idx].weight + idx = idx + 1 + end +-- print(animName .. " " .. idx .. " [" .. origRoll .. "]") + local anim = animTable[animName][idx].anim + + -- switch animation + if (anim ~= currentAnimInstance) then + + if (currentAnimTrack ~= nil) then + currentAnimTrack:Stop(transitionTime) + currentAnimTrack:Destroy() + end + + currentAnimSpeed = 1.0 + + -- load it to the humanoid; get AnimationTrack + currentAnimTrack = humanoid:LoadAnimation(anim) + currentAnimTrack.Priority = Enum.AnimationPriority.Core + + -- play the animation + currentAnimTrack:Play(transitionTime) + currentAnim = animName + currentAnimInstance = anim + + -- set up keyframe name triggers + if (currentAnimKeyframeHandler ~= nil) then + currentAnimKeyframeHandler:disconnect() + end + currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:connect(keyFrameReachedFunc) + + end + +end + +------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------- + +local toolAnimName = "" +local toolAnimTrack = nil +local toolAnimInstance = nil +local currentToolAnimKeyframeHandler = nil + +function toolKeyFrameReachedFunc(frameName) + if (frameName == "End") then +-- print("Keyframe : ".. frameName) + playToolAnimation(toolAnimName, 0.0, Humanoid) + end +end + + +function playToolAnimation(animName, transitionTime, humanoid, priority) + + local roll = math.random(1, animTable[animName].totalWeight) + local origRoll = roll + local idx = 1 + while (roll > animTable[animName][idx].weight) do + roll = roll - animTable[animName][idx].weight + idx = idx + 1 + end +-- print(animName .. " * " .. idx .. " [" .. origRoll .. "]") + local anim = animTable[animName][idx].anim + + if (toolAnimInstance ~= anim) then + + if (toolAnimTrack ~= nil) then + toolAnimTrack:Stop() + toolAnimTrack:Destroy() + transitionTime = 0 + end + + -- load it to the humanoid; get AnimationTrack + toolAnimTrack = humanoid:LoadAnimation(anim) + if priority then + toolAnimTrack.Priority = priority + end + + -- play the animation + toolAnimTrack:Play(transitionTime) + toolAnimName = animName + toolAnimInstance = anim + + currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:connect(toolKeyFrameReachedFunc) + end +end + +function stopToolAnimations() + local oldAnim = toolAnimName + + if (currentToolAnimKeyframeHandler ~= nil) then + currentToolAnimKeyframeHandler:disconnect() + end + + toolAnimName = "" + toolAnimInstance = nil + if (toolAnimTrack ~= nil) then + toolAnimTrack:Stop() + toolAnimTrack:Destroy() + toolAnimTrack = nil + end + + + return oldAnim +end + +------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------- + + +function onRunning(speed) + if speed > 0.01 then + playAnimation("walk", 0.1, Humanoid) + if currentAnimInstance and currentAnimInstance.AnimationId == "http://www.roblox.com/asset/?id=180426354" then + setAnimationSpeed(speed / 14.5) + end + pose = "Running" + else + if emoteNames[currentAnim] == nil then + playAnimation("idle", 0.1, Humanoid) + pose = "Standing" + end + end +end + +function onDied() + pose = "Dead" +end + +function onJumping() + playAnimation("jump", 0.1, Humanoid) + jumpAnimTime = jumpAnimDuration + pose = "Jumping" +end + +function onClimbing(speed) + playAnimation("climb", 0.1, Humanoid) + setAnimationSpeed(speed / 12.0) + pose = "Climbing" +end + +function onGettingUp() + pose = "GettingUp" +end + +function onFreeFall() + if (jumpAnimTime <= 0) then + playAnimation("fall", fallTransitionTime, Humanoid) + end + pose = "FreeFall" +end + +function onFallingDown() + pose = "FallingDown" +end + +function onSeated() + pose = "Seated" +end + +function onPlatformStanding() + pose = "PlatformStanding" +end + +function onSwimming(speed) + if speed > 0 then + pose = "Running" + else + pose = "Standing" + end +end + +function getTool() + for _, kid in ipairs(Figure:GetChildren()) do + if kid.className == "Tool" then return kid end + end + return nil +end + +function getToolAnim(tool) + for _, c in ipairs(tool:GetChildren()) do + if c.Name == "toolanim" and c.className == "StringValue" then + return c + end + end + return nil +end + +function animateTool() + + if (toolAnim == "None") then + playToolAnimation("toolnone", toolTransitionTime, Humanoid, Enum.AnimationPriority.Idle) + return + end + + if (toolAnim == "Slash") then + playToolAnimation("toolslash", 0, Humanoid, Enum.AnimationPriority.Action) + return + end + + if (toolAnim == "Lunge") then + playToolAnimation("toollunge", 0, Humanoid, Enum.AnimationPriority.Action) + return + end +end + +function moveSit() + RightShoulder.MaxVelocity = 0.15 + LeftShoulder.MaxVelocity = 0.15 + RightShoulder:SetDesiredAngle(3.14 /2) + LeftShoulder:SetDesiredAngle(-3.14 /2) + RightHip:SetDesiredAngle(3.14 /2) + LeftHip:SetDesiredAngle(-3.14 /2) +end + +local lastTick = 0 + +function move(time) + local amplitude = 1 + local frequency = 1 + local deltaTime = time - lastTick + lastTick = time + + local climbFudge = 0 + local setAngles = false + + if (jumpAnimTime > 0) then + jumpAnimTime = jumpAnimTime - deltaTime + end + + if (pose == "FreeFall" and jumpAnimTime <= 0) then + playAnimation("fall", fallTransitionTime, Humanoid) + elseif (pose == "Seated") then + playAnimation("sit", 0.5, Humanoid) + return + elseif (pose == "Running") then + playAnimation("walk", 0.1, Humanoid) + elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then +-- print("Wha " .. pose) + stopAllAnimations() + amplitude = 0.1 + frequency = 1 + setAngles = true + end + + if (setAngles) then + local desiredAngle = amplitude * math.sin(time * frequency) + + RightShoulder:SetDesiredAngle(desiredAngle + climbFudge) + LeftShoulder:SetDesiredAngle(desiredAngle - climbFudge) + RightHip:SetDesiredAngle(-desiredAngle) + LeftHip:SetDesiredAngle(-desiredAngle) + end + + -- Tool Animation handling + local tool = getTool() + if tool and tool:FindFirstChild("Handle") then + + local animStringValueObject = getToolAnim(tool) + + if animStringValueObject then + toolAnim = animStringValueObject.Value + -- message recieved, delete StringValue + animStringValueObject.Parent = nil + toolAnimTime = time + .3 + end + + if time > toolAnimTime then + toolAnimTime = 0 + toolAnim = "None" + end + + animateTool() + else + stopToolAnimations() + toolAnim = "None" + toolAnimInstance = nil + toolAnimTime = 0 + end +end + +-- connect events +Humanoid.Died:connect(onDied) +Humanoid.Running:connect(onRunning) +Humanoid.Jumping:connect(onJumping) +Humanoid.Climbing:connect(onClimbing) +Humanoid.GettingUp:connect(onGettingUp) +Humanoid.FreeFalling:connect(onFreeFall) +Humanoid.FallingDown:connect(onFallingDown) +Humanoid.Seated:connect(onSeated) +Humanoid.PlatformStanding:connect(onPlatformStanding) +Humanoid.Swimming:connect(onSwimming) + +-- setup emote chat hook +game:GetService("Players").LocalPlayer.Chatted:connect(function(msg) + local emote = "" + if msg == "/e dance" then + emote = dances[math.random(1, #dances)] + elseif (string.sub(msg, 1, 3) == "/e ") then + emote = string.sub(msg, 4) + elseif (string.sub(msg, 1, 7) == "/emote ") then + emote = string.sub(msg, 8) + end + + if (pose == "Standing" and emoteNames[emote] ~= nil) then + playAnimation(emote, 0.1, Humanoid) + end + +end) + + +-- main program + +-- initialize to idle +playAnimation("idle", 0.1, Humanoid) +pose = "Standing" + +while Figure.Parent ~= nil do + local _, time = wait(0.1) + move(time) +end + + +]]> + + + + idle + + + + + http://www.roblox.com/asset/?id=180435571 + Animation1 + + + + Weight + 9 + + + + + + http://www.roblox.com/asset/?id=180435792 + Animation2 + + + + Weight + 1 + + + + + + + walk + + + + + http://www.roblox.com/asset/?id=180426354 + WalkAnim + + + + + + run + + + + + http://www.roblox.com/asset/?id=180426354 + RunAnim + + + + + + jump + + + + + http://www.roblox.com/asset/?id=125750702 + JumpAnim + + + + + + climb + + + + + http://www.roblox.com/asset/?id=180436334 + ClimbAnim + + + + + + toolnone + + + + + http://www.roblox.com/asset/?id=182393478 + ToolNoneAnim + + + + + + fall + + + + + http://www.roblox.com/asset/?id=180436148 + FallAnim + + + + + + sit + + + + + http://www.roblox.com/asset/?id=178130996 + SitAnim + + + + + \ No newline at end of file diff --git a/Client2021/content/avatar/scripts/humanoidAnimateR15.rbxm b/Client2021/content/avatar/scripts/humanoidAnimateR15.rbxm new file mode 100644 index 0000000..b3d27ce Binary files /dev/null and b/Client2021/content/avatar/scripts/humanoidAnimateR15.rbxm differ diff --git a/Client2021/content/avatar/scripts/humanoidAnimateR15ScaledV3.rbxm b/Client2021/content/avatar/scripts/humanoidAnimateR15ScaledV3.rbxm new file mode 100644 index 0000000..a0d58e7 Binary files /dev/null and b/Client2021/content/avatar/scripts/humanoidAnimateR15ScaledV3.rbxm differ diff --git a/Client2021/content/avatar/scripts/humanoidAnimateR15ScaledV4.rbxm b/Client2021/content/avatar/scripts/humanoidAnimateR15ScaledV4.rbxm new file mode 100644 index 0000000..6d3a5cb Binary files /dev/null and b/Client2021/content/avatar/scripts/humanoidAnimateR15ScaledV4.rbxm differ diff --git a/Client2021/content/avatar/scripts/humanoidHealthRegenScript.rbxmx b/Client2021/content/avatar/scripts/humanoidHealthRegenScript.rbxmx new file mode 100644 index 0000000..ece49f1 --- /dev/null +++ b/Client2021/content/avatar/scripts/humanoidHealthRegenScript.rbxmx @@ -0,0 +1,32 @@ + + null + nil + + + false + + Health + {EC3A881D-5F49-4644-A69D-FB60F2E59FF2} + + + + \ No newline at end of file diff --git a/Client2021/content/configs/DataModelPatchConfig/DataModelPatchConfig.json b/Client2021/content/configs/DataModelPatchConfig/DataModelPatchConfig.json new file mode 100644 index 0000000..33aea42 --- /dev/null +++ b/Client2021/content/configs/DataModelPatchConfig/DataModelPatchConfig.json @@ -0,0 +1 @@ +{"AppStorageResetId": "0", "AssetId": "5345954812", "AssetVersion": "871", "IsForcedUpdate": false} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/de-de.json b/Client2021/content/configs/DateTimeLocaleConfigs/de-de.json new file mode 100644 index 0000000..919d45f --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/de-de.json @@ -0,0 +1,85 @@ +{ + "months": [ + "Januar", + "Februar", + "März", + "April", + "Mai", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "Dezember" + ], + "monthsShort": [ + "Jan.", + "Feb.", + "März", + "Apr.", + "Mai", + "Juni", + "Juli", + "Aug.", + "Sep.", + "Okt.", + "Nov.", + "Dez." + ], + "weekdays": [ + "Sonntag", + "Montag", + "Dienstag", + "Mittwoch", + "Donnerstag", + "Freitag", + "Samstag" + ], + "weekdaysShort": [ + "So.", + "Mo.", + "Di.", + "Mi.", + "Do.", + "Fr.", + "Sa." + ], + "weekdaysMin": [ + "So", + "Mo", + "Di", + "Mi", + "Do", + "Fr", + "Sa" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "DD.MM.YYYY", + "LL": "D. MMMM YYYY", + "LLL": "D. MMMM YYYY HH:mm", + "LLLL": "dddd, D. MMMM YYYY HH:mm", + "l": "D.M.YYYY", + "ll": "D. MMM YYYY", + "lll": "D. MMM YYYY HH:mm", + "llll": "ddd, D. MMM YYYY HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/en-au.json b/Client2021/content/configs/DateTimeLocaleConfigs/en-au.json new file mode 100644 index 0000000..2d690c4 --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/en-au.json @@ -0,0 +1,85 @@ +{ + "months": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + "monthsShort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "weekdays": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "weekdaysShort": [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + ], + "weekdaysMin": [ + "Su", + "Mo", + "Tu", + "We", + "Th", + "Fr", + "Sa" + ], + "longDateFormat": { + "LT": "h:mm A", + "LTS": "h:mm:ss A", + "L": "DD/MM/YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY h:mm A", + "LLLL": "dddd, D MMMM YYYY h:mm A", + "l": "D/M/YYYY", + "ll": "D MMM YYYY", + "lll": "D MMM YYYY h:mm A", + "llll": "ddd, D MMM YYYY h:mm A" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/en-ca.json b/Client2021/content/configs/DateTimeLocaleConfigs/en-ca.json new file mode 100644 index 0000000..3cf0fde --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/en-ca.json @@ -0,0 +1,81 @@ +{ + "months": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + "monthsShort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "weekdays": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "weekdaysShort": [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + ], + "weekdaysMin": [ + "Su", + "Mo", + "Tu", + "We", + "Th", + "Fr", + "Sa" + ], + "longDateFormat": { + "LT": "h:mm A", + "LTS": "h:mm:ss A", + "L": "YYYY-MM-DD", + "LL": "MMMM D, YYYY", + "LLL": "MMMM D, YYYY h:mm A", + "LLLL": "dddd, MMMM D, YYYY h:mm A", + "l": "YYYY-M-D", + "ll": "MMM D, YYYY", + "lll": "MMM D, YYYY h:mm A", + "llll": "ddd, MMM D, YYYY h:mm A" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ] +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/en-gb.json b/Client2021/content/configs/DateTimeLocaleConfigs/en-gb.json new file mode 100644 index 0000000..e5c6378 --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/en-gb.json @@ -0,0 +1,85 @@ +{ + "months": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + "monthsShort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "weekdays": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "weekdaysShort": [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + ], + "weekdaysMin": [ + "Su", + "Mo", + "Tu", + "We", + "Th", + "Fr", + "Sa" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "DD/MM/YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY HH:mm", + "LLLL": "dddd, D MMMM YYYY HH:mm", + "l": "D/M/YYYY", + "ll": "D MMM YYYY", + "lll": "D MMM YYYY HH:mm", + "llll": "ddd, D MMM YYYY HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/en-nz.json b/Client2021/content/configs/DateTimeLocaleConfigs/en-nz.json new file mode 100644 index 0000000..2d690c4 --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/en-nz.json @@ -0,0 +1,85 @@ +{ + "months": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + "monthsShort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "weekdays": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "weekdaysShort": [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + ], + "weekdaysMin": [ + "Su", + "Mo", + "Tu", + "We", + "Th", + "Fr", + "Sa" + ], + "longDateFormat": { + "LT": "h:mm A", + "LTS": "h:mm:ss A", + "L": "DD/MM/YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY h:mm A", + "LLLL": "dddd, D MMMM YYYY h:mm A", + "l": "D/M/YYYY", + "ll": "D MMM YYYY", + "lll": "D MMM YYYY h:mm A", + "llll": "ddd, D MMM YYYY h:mm A" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/en-us.json b/Client2021/content/configs/DateTimeLocaleConfigs/en-us.json new file mode 100644 index 0000000..c866091 --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/en-us.json @@ -0,0 +1,85 @@ +{ + "months": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + "monthsShort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "weekdays": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "weekdaysShort": [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + ], + "weekdaysMin": [ + "Su", + "Mo", + "Tu", + "We", + "Th", + "Fr", + "Sa" + ], + "longDateFormat": { + "LT": "h:mm A", + "LTS": "h:mm:ss A", + "L": "MM/DD/YYYY", + "LL": "MMMM D, YYYY", + "LLL": "MMMM D, YYYY h:mm A", + "LLLL": "dddd, MMMM D, YYYY h:mm A", + "l": "M/D/YYYY", + "ll": "MMM D, YYYY", + "lll": "MMM D, YYYY h:mm A", + "llll": "ddd, MMM D, YYYY h:mm A" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/es-es.json b/Client2021/content/configs/DateTimeLocaleConfigs/es-es.json new file mode 100644 index 0000000..c627149 --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/es-es.json @@ -0,0 +1,85 @@ +{ + "months": [ + "enero", + "febrero", + "marzo", + "abril", + "mayo", + "junio", + "julio", + "agosto", + "septiembre", + "octubre", + "noviembre", + "diciembre" + ], + "monthsShort": [ + "ene.", + "feb.", + "mar.", + "abr.", + "may.", + "jun.", + "jul.", + "ago.", + "sep.", + "oct.", + "nov.", + "dic." + ], + "weekdays": [ + "domingo", + "lunes", + "martes", + "miércoles", + "jueves", + "viernes", + "sábado" + ], + "weekdaysShort": [ + "dom.", + "lun.", + "mar.", + "mié.", + "jue.", + "vie.", + "sáb." + ], + "weekdaysMin": [ + "do", + "lu", + "ma", + "mi", + "ju", + "vi", + "sá" + ], + "longDateFormat": { + "LT": "H:mm", + "LTS": "H:mm:ss", + "L": "DD/MM/YYYY", + "LL": "D [de] MMMM [de] YYYY", + "LLL": "D [de] MMMM [de] YYYY H:mm", + "LLLL": "dddd, D [de] MMMM [de] YYYY H:mm", + "l": "D/M/YYYY", + "ll": "D [de] MMM [de] YYYY", + "lll": "D [de] MMM [de] YYYY H:mm", + "llll": "ddd, D [de] MMM [de] YYYY H:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/es-mx.json b/Client2021/content/configs/DateTimeLocaleConfigs/es-mx.json new file mode 100644 index 0000000..52b9a37 --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/es-mx.json @@ -0,0 +1,85 @@ +{ + "months": [ + "enero", + "febrero", + "marzo", + "abril", + "mayo", + "junio", + "julio", + "agosto", + "septiembre", + "octubre", + "noviembre", + "diciembre" + ], + "monthsShort": [ + "ene.", + "feb.", + "mar.", + "abr.", + "may.", + "jun.", + "jul.", + "ago.", + "sep.", + "oct.", + "nov.", + "dic." + ], + "weekdays": [ + "domingo", + "lunes", + "martes", + "miércoles", + "jueves", + "viernes", + "sábado" + ], + "weekdaysShort": [ + "dom.", + "lun.", + "mar.", + "mié.", + "jue.", + "vie.", + "sáb." + ], + "weekdaysMin": [ + "do", + "lu", + "ma", + "mi", + "ju", + "vi", + "sá" + ], + "longDateFormat": { + "LT": "h:mm A", + "LTS": "h:mm:ss A", + "L": "DD/MM/YYYY", + "LL": "D [de] MMMM [de] YYYY", + "LLL": "D [de] MMMM [de] YYYY h:mm A", + "LLLL": "dddd, D [de] MMMM [de] YYYY h:mm A", + "l": "D/M/YYYY", + "ll": "D [de] MMM [de] YYYY", + "lll": "D [de] MMM [de] YYYY h:mm A", + "llll": "ddd, D [de] MMM [de] YYYY h:mm A" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "a.m.", + "upperCase": "a.m." + }, + { + "startFrom": 1200, + "lowerCase": "p.m.", + "upperCase": "p.m." + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/fr-ca.json b/Client2021/content/configs/DateTimeLocaleConfigs/fr-ca.json new file mode 100644 index 0000000..975d55c --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/fr-ca.json @@ -0,0 +1,81 @@ +{ + "months": [ + "janvier", + "février", + "mars", + "avril", + "mai", + "juin", + "juillet", + "août", + "septembre", + "octobre", + "novembre", + "décembre" + ], + "monthsShort": [ + "janv.", + "févr.", + "mars", + "avr.", + "mai", + "juin", + "juil.", + "août", + "sept.", + "oct.", + "nov.", + "déc." + ], + "weekdays": [ + "dimanche", + "lundi", + "mardi", + "mercredi", + "jeudi", + "vendredi", + "samedi" + ], + "weekdaysShort": [ + "dim.", + "lun.", + "mar.", + "mer.", + "jeu.", + "ven.", + "sam." + ], + "weekdaysMin": [ + "di", + "lu", + "ma", + "me", + "je", + "ve", + "sa" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY-MM-DD", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY HH:mm", + "LLLL": "dddd D MMMM YYYY HH:mm", + "l": "YYYY-M-D", + "ll": "D MMM YYYY", + "lll": "D MMM YYYY HH:mm", + "llll": "ddd D MMM YYYY HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ] +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/fr-fr.json b/Client2021/content/configs/DateTimeLocaleConfigs/fr-fr.json new file mode 100644 index 0000000..67fedaa --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/fr-fr.json @@ -0,0 +1,85 @@ +{ + "months": [ + "janvier", + "février", + "mars", + "avril", + "mai", + "juin", + "juillet", + "août", + "septembre", + "octobre", + "novembre", + "décembre" + ], + "monthsShort": [ + "janv.", + "févr.", + "mars", + "avr.", + "mai", + "juin", + "juil.", + "août", + "sept.", + "oct.", + "nov.", + "déc." + ], + "weekdays": [ + "dimanche", + "lundi", + "mardi", + "mercredi", + "jeudi", + "vendredi", + "samedi" + ], + "weekdaysShort": [ + "dim.", + "lun.", + "mar.", + "mer.", + "jeu.", + "ven.", + "sam." + ], + "weekdaysMin": [ + "di", + "lu", + "ma", + "me", + "je", + "ve", + "sa" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "DD/MM/YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY HH:mm", + "LLLL": "dddd D MMMM YYYY HH:mm", + "l": "D/M/YYYY", + "ll": "D MMM YYYY", + "lll": "D MMM YYYY HH:mm", + "llll": "ddd D MMM YYYY HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/it-it.json b/Client2021/content/configs/DateTimeLocaleConfigs/it-it.json new file mode 100644 index 0000000..99e998c --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/it-it.json @@ -0,0 +1,85 @@ +{ + "months": [ + "gennaio", + "febbraio", + "marzo", + "aprile", + "maggio", + "giugno", + "luglio", + "agosto", + "settembre", + "ottobre", + "novembre", + "dicembre" + ], + "monthsShort": [ + "gen", + "feb", + "mar", + "apr", + "mag", + "giu", + "lug", + "ago", + "set", + "ott", + "nov", + "dic" + ], + "weekdays": [ + "domenica", + "lunedì", + "martedì", + "mercoledì", + "giovedì", + "venerdì", + "sabato" + ], + "weekdaysShort": [ + "dom", + "lun", + "mar", + "mer", + "gio", + "ven", + "sab" + ], + "weekdaysMin": [ + "do", + "lu", + "ma", + "me", + "gi", + "ve", + "sa" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "DD/MM/YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY HH:mm", + "LLLL": "dddd D MMMM YYYY HH:mm", + "l": "D/M/YYYY", + "ll": "D MMM YYYY", + "lll": "D MMM YYYY HH:mm", + "llll": "ddd D MMM YYYY HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/ja-jp.json b/Client2021/content/configs/DateTimeLocaleConfigs/ja-jp.json new file mode 100644 index 0000000..cdc6475 --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/ja-jp.json @@ -0,0 +1,81 @@ +{ + "months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "monthsShort": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "weekdays": [ + "日曜日", + "月曜日", + "火曜日", + "水曜日", + "木曜日", + "金曜日", + "土曜日" + ], + "weekdaysShort": [ + "日", + "月", + "火", + "水", + "木", + "金", + "土" + ], + "weekdaysMin": [ + "日", + "月", + "火", + "水", + "木", + "金", + "土" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY/MM/DD", + "LL": "YYYY年M月D日", + "LLL": "YYYY年M月D日 HH:mm", + "LLLL": "YYYY年M月D日 dddd HH:mm", + "l": "YYYY/MM/DD", + "ll": "YYYY年M月D日", + "lll": "YYYY年M月D日 HH:mm", + "llll": "YYYY年M月D日(ddd) HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "午前", + "upperCase": "午前" + }, + { + "startFrom": 1200, + "lowerCase": "午後", + "upperCase": "午後" + } + ] +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/ko-kr.json b/Client2021/content/configs/DateTimeLocaleConfigs/ko-kr.json new file mode 100644 index 0000000..314ed1b --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/ko-kr.json @@ -0,0 +1,81 @@ +{ + "months": [ + "1월", + "2월", + "3월", + "4월", + "5월", + "6월", + "7월", + "8월", + "9월", + "10월", + "11월", + "12월" + ], + "monthsShort": [ + "1월", + "2월", + "3월", + "4월", + "5월", + "6월", + "7월", + "8월", + "9월", + "10월", + "11월", + "12월" + ], + "weekdays": [ + "일요일", + "월요일", + "화요일", + "수요일", + "목요일", + "금요일", + "토요일" + ], + "weekdaysShort": [ + "일", + "월", + "화", + "수", + "목", + "금", + "토" + ], + "weekdaysMin": [ + "일", + "월", + "화", + "수", + "목", + "금", + "토" + ], + "longDateFormat": { + "LT": "A h:mm", + "LTS": "A h:mm:ss", + "L": "YYYY.MM.DD.", + "LL": "YYYY년 MMMM D일", + "LLL": "YYYY년 MMMM D일 A h:mm", + "LLLL": "YYYY년 MMMM D일 dddd A h:mm", + "l": "YYYY.MM.DD.", + "ll": "YYYY년 MMMM D일", + "lll": "YYYY년 MMMM D일 A h:mm", + "llll": "YYYY년 MMMM D일 dddd A h:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "오전", + "upperCase": "오전" + }, + { + "startFrom": 1200, + "lowerCase": "오후", + "upperCase": "오후" + } + ] +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/pt-br.json b/Client2021/content/configs/DateTimeLocaleConfigs/pt-br.json new file mode 100644 index 0000000..1d4bc09 --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/pt-br.json @@ -0,0 +1,81 @@ +{ + "months": [ + "Janeiro", + "Fevereiro", + "Março", + "Abril", + "Maio", + "Junho", + "Julho", + "Agosto", + "Setembro", + "Outubro", + "Novembro", + "Dezembro" + ], + "monthsShort": [ + "Jan", + "Fev", + "Mar", + "Abr", + "Mai", + "Jun", + "Jul", + "Ago", + "Set", + "Out", + "Nov", + "Dez" + ], + "weekdays": [ + "Domingo", + "Segunda-feira", + "Terça-feira", + "Quarta-feira", + "Quinta-feira", + "Sexta-feira", + "Sábado" + ], + "weekdaysShort": [ + "Dom", + "Seg", + "Ter", + "Qua", + "Qui", + "Sex", + "Sáb" + ], + "weekdaysMin": [ + "Do", + "2ª", + "3ª", + "4ª", + "5ª", + "6ª", + "Sá" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "DD/MM/YYYY", + "LL": "D [de] MMMM [de] YYYY", + "LLL": "D [de] MMMM [de] YYYY [às] HH:mm", + "LLLL": "dddd, D [de] MMMM [de] YYYY [às] HH:mm", + "l": "D/M/YYYY", + "ll": "D [de] MMM [de] YYYY", + "lll": "D [de] MMM [de] YYYY [às] HH:mm", + "llll": "ddd, D [de] MMM [de] YYYY [às] HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ] +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/pt-pt.json b/Client2021/content/configs/DateTimeLocaleConfigs/pt-pt.json new file mode 100644 index 0000000..8b41382 --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/pt-pt.json @@ -0,0 +1,85 @@ +{ + "months": [ + "Janeiro", + "Fevereiro", + "Março", + "Abril", + "Maio", + "Junho", + "Julho", + "Agosto", + "Setembro", + "Outubro", + "Novembro", + "Dezembro" + ], + "monthsShort": [ + "Jan", + "Fev", + "Mar", + "Abr", + "Mai", + "Jun", + "Jul", + "Ago", + "Set", + "Out", + "Nov", + "Dez" + ], + "weekdays": [ + "Domingo", + "Segunda-feira", + "Terça-feira", + "Quarta-feira", + "Quinta-feira", + "Sexta-feira", + "Sábado" + ], + "weekdaysShort": [ + "Dom", + "Seg", + "Ter", + "Qua", + "Qui", + "Sex", + "Sáb" + ], + "weekdaysMin": [ + "Do", + "2ª", + "3ª", + "4ª", + "5ª", + "6ª", + "Sá" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "DD/MM/YYYY", + "LL": "D [de] MMMM [de] YYYY", + "LLL": "D [de] MMMM [de] YYYY HH:mm", + "LLLL": "dddd, D [de] MMMM [de] YYYY HH:mm", + "l": "D/M/YYYY", + "ll": "D [de] MMM [de] YYYY", + "lll": "D [de] MMM [de] YYYY HH:mm", + "llll": "ddd, D [de] MMM [de] YYYY HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "am", + "upperCase": "AM" + }, + { + "startFrom": 1200, + "lowerCase": "pm", + "upperCase": "PM" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/ru-ru.json b/Client2021/content/configs/DateTimeLocaleConfigs/ru-ru.json new file mode 100644 index 0000000..3293a5f --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/ru-ru.json @@ -0,0 +1,95 @@ +{ + "months": [ + "январь", + "февраль", + "март", + "апрель", + "май", + "июнь", + "июль", + "август", + "сентябрь", + "октябрь", + "ноябрь", + "декабрь" + ], + "monthsShort": [ + "янв.", + "февр.", + "март", + "апр.", + "май", + "июнь", + "июль", + "авг.", + "сент.", + "окт.", + "нояб.", + "дек." + ], + "weekdays": [ + "воскресенье", + "понедельник", + "вторник", + "среда", + "четверг", + "пятница", + "суббота" + ], + "weekdaysShort": [ + "вс", + "пн", + "вт", + "ср", + "чт", + "пт", + "сб" + ], + "weekdaysMin": [ + "вс", + "пн", + "вт", + "ср", + "чт", + "пт", + "сб" + ], + "longDateFormat": { + "LT": "H:mm", + "LTS": "H:mm:ss", + "L": "DD.MM.YYYY", + "LL": "D MMMM YYYY г.", + "LLL": "D MMMM YYYY г., H:mm", + "LLLL": "dddd, D MMMM YYYY г., H:mm", + "l": "D.M.YYYY", + "ll": "D MMM YYYY г.", + "lll": "D MMM YYYY г., H:mm", + "llll": "ddd, D MMM YYYY г., H:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "ночи", + "upperCase": "ночи" + }, + { + "startFrom": 400, + "lowerCase": "утра", + "upperCase": "утра" + }, + { + "startFrom": 1200, + "lowerCase": "дня", + "upperCase": "дня" + }, + { + "startFrom": 1700, + "lowerCase": "вечера", + "upperCase": "вечера" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/zh-cjv.json b/Client2021/content/configs/DateTimeLocaleConfigs/zh-cjv.json new file mode 100644 index 0000000..612593e --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/zh-cjv.json @@ -0,0 +1,105 @@ +{ + "months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "monthsShort": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "weekdays": [ + "星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六" + ], + "weekdaysShort": [ + "周日", + "周一", + "周二", + "周三", + "周四", + "周五", + "周六" + ], + "weekdaysMin": [ + "日", + "一", + "二", + "三", + "四", + "五", + "六" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY/MM/DD", + "LL": "YYYY年M月D日", + "LLL": "YYYY年M月D日Ah点mm分", + "LLLL": "YYYY年M月D日ddddAh点mm分", + "l": "YYYY/M/D", + "ll": "YYYY年M月D日", + "lll": "YYYY年M月D日 HH:mm", + "llll": "YYYY年M月D日dddd HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "凌晨", + "upperCase": "凌晨" + }, + { + "startFrom": 600, + "lowerCase": "早上", + "upperCase": "早上" + }, + { + "startFrom": 900, + "lowerCase": "上午", + "upperCase": "上午" + }, + { + "startFrom": 1130, + "lowerCase": "中午", + "upperCase": "中午" + }, + { + "startFrom": 1230, + "lowerCase": "下午", + "upperCase": "下午" + }, + { + "startFrom": 1800, + "lowerCase": "晚上", + "upperCase": "晚上" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/zh-cn.json b/Client2021/content/configs/DateTimeLocaleConfigs/zh-cn.json new file mode 100644 index 0000000..612593e --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/zh-cn.json @@ -0,0 +1,105 @@ +{ + "months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "monthsShort": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "weekdays": [ + "星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六" + ], + "weekdaysShort": [ + "周日", + "周一", + "周二", + "周三", + "周四", + "周五", + "周六" + ], + "weekdaysMin": [ + "日", + "一", + "二", + "三", + "四", + "五", + "六" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY/MM/DD", + "LL": "YYYY年M月D日", + "LLL": "YYYY年M月D日Ah点mm分", + "LLLL": "YYYY年M月D日ddddAh点mm分", + "l": "YYYY/M/D", + "ll": "YYYY年M月D日", + "lll": "YYYY年M月D日 HH:mm", + "llll": "YYYY年M月D日dddd HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "凌晨", + "upperCase": "凌晨" + }, + { + "startFrom": 600, + "lowerCase": "早上", + "upperCase": "早上" + }, + { + "startFrom": 900, + "lowerCase": "上午", + "upperCase": "上午" + }, + { + "startFrom": 1130, + "lowerCase": "中午", + "upperCase": "中午" + }, + { + "startFrom": 1230, + "lowerCase": "下午", + "upperCase": "下午" + }, + { + "startFrom": 1800, + "lowerCase": "晚上", + "upperCase": "晚上" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/zh-hans.json b/Client2021/content/configs/DateTimeLocaleConfigs/zh-hans.json new file mode 100644 index 0000000..612593e --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/zh-hans.json @@ -0,0 +1,105 @@ +{ + "months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "monthsShort": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "weekdays": [ + "星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六" + ], + "weekdaysShort": [ + "周日", + "周一", + "周二", + "周三", + "周四", + "周五", + "周六" + ], + "weekdaysMin": [ + "日", + "一", + "二", + "三", + "四", + "五", + "六" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY/MM/DD", + "LL": "YYYY年M月D日", + "LLL": "YYYY年M月D日Ah点mm分", + "LLLL": "YYYY年M月D日ddddAh点mm分", + "l": "YYYY/M/D", + "ll": "YYYY年M月D日", + "lll": "YYYY年M月D日 HH:mm", + "llll": "YYYY年M月D日dddd HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "凌晨", + "upperCase": "凌晨" + }, + { + "startFrom": 600, + "lowerCase": "早上", + "upperCase": "早上" + }, + { + "startFrom": 900, + "lowerCase": "上午", + "upperCase": "上午" + }, + { + "startFrom": 1130, + "lowerCase": "中午", + "upperCase": "中午" + }, + { + "startFrom": 1230, + "lowerCase": "下午", + "upperCase": "下午" + }, + { + "startFrom": 1800, + "lowerCase": "晚上", + "upperCase": "晚上" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/zh-hant.json b/Client2021/content/configs/DateTimeLocaleConfigs/zh-hant.json new file mode 100644 index 0000000..1955433 --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/zh-hant.json @@ -0,0 +1,105 @@ +{ + "months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "monthsShort": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "weekdays": [ + "星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六" + ], + "weekdaysShort": [ + "週日", + "週一", + "週二", + "週三", + "週四", + "週五", + "週六" + ], + "weekdaysMin": [ + "日", + "一", + "二", + "三", + "四", + "五", + "六" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY/MM/DD", + "LL": "YYYY年M月D日", + "LLL": "YYYY年M月D日 HH:mm", + "LLLL": "YYYY年M月D日dddd HH:mm", + "l": "YYYY/M/D", + "ll": "YYYY年M月D日", + "lll": "YYYY年M月D日 HH:mm", + "llll": "YYYY年M月D日dddd HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "凌晨", + "upperCase": "凌晨" + }, + { + "startFrom": 600, + "lowerCase": "早上", + "upperCase": "早上" + }, + { + "startFrom": 900, + "lowerCase": "上午", + "upperCase": "上午" + }, + { + "startFrom": 1130, + "lowerCase": "中午", + "upperCase": "中午" + }, + { + "startFrom": 1230, + "lowerCase": "下午", + "upperCase": "下午" + }, + { + "startFrom": 1800, + "lowerCase": "晚上", + "upperCase": "晚上" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/zh-hk.json b/Client2021/content/configs/DateTimeLocaleConfigs/zh-hk.json new file mode 100644 index 0000000..1955433 --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/zh-hk.json @@ -0,0 +1,105 @@ +{ + "months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "monthsShort": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "weekdays": [ + "星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六" + ], + "weekdaysShort": [ + "週日", + "週一", + "週二", + "週三", + "週四", + "週五", + "週六" + ], + "weekdaysMin": [ + "日", + "一", + "二", + "三", + "四", + "五", + "六" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY/MM/DD", + "LL": "YYYY年M月D日", + "LLL": "YYYY年M月D日 HH:mm", + "LLLL": "YYYY年M月D日dddd HH:mm", + "l": "YYYY/M/D", + "ll": "YYYY年M月D日", + "lll": "YYYY年M月D日 HH:mm", + "llll": "YYYY年M月D日dddd HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "凌晨", + "upperCase": "凌晨" + }, + { + "startFrom": 600, + "lowerCase": "早上", + "upperCase": "早上" + }, + { + "startFrom": 900, + "lowerCase": "上午", + "upperCase": "上午" + }, + { + "startFrom": 1130, + "lowerCase": "中午", + "upperCase": "中午" + }, + { + "startFrom": 1230, + "lowerCase": "下午", + "upperCase": "下午" + }, + { + "startFrom": 1800, + "lowerCase": "晚上", + "upperCase": "晚上" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/DateTimeLocaleConfigs/zh-tw.json b/Client2021/content/configs/DateTimeLocaleConfigs/zh-tw.json new file mode 100644 index 0000000..1955433 --- /dev/null +++ b/Client2021/content/configs/DateTimeLocaleConfigs/zh-tw.json @@ -0,0 +1,105 @@ +{ + "months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "monthsShort": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "weekdays": [ + "星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六" + ], + "weekdaysShort": [ + "週日", + "週一", + "週二", + "週三", + "週四", + "週五", + "週六" + ], + "weekdaysMin": [ + "日", + "一", + "二", + "三", + "四", + "五", + "六" + ], + "longDateFormat": { + "LT": "HH:mm", + "LTS": "HH:mm:ss", + "L": "YYYY/MM/DD", + "LL": "YYYY年M月D日", + "LLL": "YYYY年M月D日 HH:mm", + "LLLL": "YYYY年M月D日dddd HH:mm", + "l": "YYYY/M/D", + "ll": "YYYY年M月D日", + "lll": "YYYY年M月D日 HH:mm", + "llll": "YYYY年M月D日dddd HH:mm" + }, + "meridiem": [ + { + "startFrom": 0, + "lowerCase": "凌晨", + "upperCase": "凌晨" + }, + { + "startFrom": 600, + "lowerCase": "早上", + "upperCase": "早上" + }, + { + "startFrom": 900, + "lowerCase": "上午", + "upperCase": "上午" + }, + { + "startFrom": 1130, + "lowerCase": "中午", + "upperCase": "中午" + }, + { + "startFrom": 1230, + "lowerCase": "下午", + "upperCase": "下午" + }, + { + "startFrom": 1800, + "lowerCase": "晚上", + "upperCase": "晚上" + } + ], + "week": { + "dow": 1, + "doy": 4 + } +} \ No newline at end of file diff --git a/Client2021/content/configs/ReflectionLoggerConfig/EphemeralCounterWhitelist.json b/Client2021/content/configs/ReflectionLoggerConfig/EphemeralCounterWhitelist.json new file mode 100644 index 0000000..9048ff7 --- /dev/null +++ b/Client2021/content/configs/ReflectionLoggerConfig/EphemeralCounterWhitelist.json @@ -0,0 +1,15 @@ +{ + "LocalizationService": [ + "SystemLocaleId", + "RobloxLocaleId", + "GetCorescriptLocalizations" + ], + "LocalizationTable": [ + "GetString", + "DevelopmentLanguage", + "GetContents", + "SetContents", + "SetEntry", + "RemoveKey" + ] +} diff --git a/Client2021/content/configs/ReflectionLoggerConfig/EphemeralCounterWhitelistMock.json b/Client2021/content/configs/ReflectionLoggerConfig/EphemeralCounterWhitelistMock.json new file mode 100644 index 0000000..9048ff7 --- /dev/null +++ b/Client2021/content/configs/ReflectionLoggerConfig/EphemeralCounterWhitelistMock.json @@ -0,0 +1,15 @@ +{ + "LocalizationService": [ + "SystemLocaleId", + "RobloxLocaleId", + "GetCorescriptLocalizations" + ], + "LocalizationTable": [ + "GetString", + "DevelopmentLanguage", + "GetContents", + "SetContents", + "SetEntry", + "RemoveKey" + ] +} diff --git a/Client2021/content/fonts/AccanthisADFStd-Regular.otf b/Client2021/content/fonts/AccanthisADFStd-Regular.otf new file mode 100644 index 0000000..b3d6f03 Binary files /dev/null and b/Client2021/content/fonts/AccanthisADFStd-Regular.otf differ diff --git a/Client2021/content/fonts/AmaticSC-Bold.ttf b/Client2021/content/fonts/AmaticSC-Bold.ttf new file mode 100644 index 0000000..0f24d11 Binary files /dev/null and b/Client2021/content/fonts/AmaticSC-Bold.ttf differ diff --git a/Client2021/content/fonts/AmaticSC-Regular.ttf b/Client2021/content/fonts/AmaticSC-Regular.ttf new file mode 100644 index 0000000..5f16090 Binary files /dev/null and b/Client2021/content/fonts/AmaticSC-Regular.ttf differ diff --git a/Client2021/content/fonts/Balthazar-Regular.ttf b/Client2021/content/fonts/Balthazar-Regular.ttf new file mode 100644 index 0000000..fff2ff7 Binary files /dev/null and b/Client2021/content/fonts/Balthazar-Regular.ttf differ diff --git a/Client2021/content/fonts/Bangers-Regular.ttf b/Client2021/content/fonts/Bangers-Regular.ttf new file mode 100644 index 0000000..027af19 Binary files /dev/null and b/Client2021/content/fonts/Bangers-Regular.ttf differ diff --git a/Client2021/content/fonts/ComicNeue-Angular-Bold.ttf b/Client2021/content/fonts/ComicNeue-Angular-Bold.ttf new file mode 100644 index 0000000..d70a258 Binary files /dev/null and b/Client2021/content/fonts/ComicNeue-Angular-Bold.ttf differ diff --git a/Client2021/content/fonts/Creepster-Regular.ttf b/Client2021/content/fonts/Creepster-Regular.ttf new file mode 100644 index 0000000..e09c147 Binary files /dev/null and b/Client2021/content/fonts/Creepster-Regular.ttf differ diff --git a/Client2021/content/fonts/DenkOne-Regular.ttf b/Client2021/content/fonts/DenkOne-Regular.ttf new file mode 100644 index 0000000..e20703d Binary files /dev/null and b/Client2021/content/fonts/DenkOne-Regular.ttf differ diff --git a/Client2021/content/fonts/Fondamento-Italic.ttf b/Client2021/content/fonts/Fondamento-Italic.ttf new file mode 100644 index 0000000..80f50c4 Binary files /dev/null and b/Client2021/content/fonts/Fondamento-Italic.ttf differ diff --git a/Client2021/content/fonts/Fondamento-Regular.ttf b/Client2021/content/fonts/Fondamento-Regular.ttf new file mode 100644 index 0000000..a2bc80c Binary files /dev/null and b/Client2021/content/fonts/Fondamento-Regular.ttf differ diff --git a/Client2021/content/fonts/FredokaOne-Regular.ttf b/Client2021/content/fonts/FredokaOne-Regular.ttf new file mode 100644 index 0000000..9b384aa Binary files /dev/null and b/Client2021/content/fonts/FredokaOne-Regular.ttf differ diff --git a/Client2021/content/fonts/GothamSSm-Black.otf b/Client2021/content/fonts/GothamSSm-Black.otf new file mode 100644 index 0000000..8faae7e Binary files /dev/null and b/Client2021/content/fonts/GothamSSm-Black.otf differ diff --git a/Client2021/content/fonts/GothamSSm-Bold.otf b/Client2021/content/fonts/GothamSSm-Bold.otf new file mode 100644 index 0000000..cc64c8b Binary files /dev/null and b/Client2021/content/fonts/GothamSSm-Bold.otf differ diff --git a/Client2021/content/fonts/GothamSSm-Book.otf b/Client2021/content/fonts/GothamSSm-Book.otf new file mode 100644 index 0000000..5cdc825 Binary files /dev/null and b/Client2021/content/fonts/GothamSSm-Book.otf differ diff --git a/Client2021/content/fonts/GothamSSm-Medium.otf b/Client2021/content/fonts/GothamSSm-Medium.otf new file mode 100644 index 0000000..27f90c0 Binary files /dev/null and b/Client2021/content/fonts/GothamSSm-Medium.otf differ diff --git a/Client2021/content/fonts/GrenzeGotisch-Bold.ttf b/Client2021/content/fonts/GrenzeGotisch-Bold.ttf new file mode 100644 index 0000000..4ca6313 Binary files /dev/null and b/Client2021/content/fonts/GrenzeGotisch-Bold.ttf differ diff --git a/Client2021/content/fonts/GrenzeGotisch-Regular.ttf b/Client2021/content/fonts/GrenzeGotisch-Regular.ttf new file mode 100644 index 0000000..3552e82 Binary files /dev/null and b/Client2021/content/fonts/GrenzeGotisch-Regular.ttf differ diff --git a/Client2021/content/fonts/Guru-Regular.otf b/Client2021/content/fonts/Guru-Regular.otf new file mode 100644 index 0000000..20d7398 Binary files /dev/null and b/Client2021/content/fonts/Guru-Regular.otf differ diff --git a/Client2021/content/fonts/HWYGOTH.ttf b/Client2021/content/fonts/HWYGOTH.ttf new file mode 100644 index 0000000..20ac3e2 Binary files /dev/null and b/Client2021/content/fonts/HWYGOTH.ttf differ diff --git a/Client2021/content/fonts/Inconsolata-Regular.ttf b/Client2021/content/fonts/Inconsolata-Regular.ttf new file mode 100644 index 0000000..bbc9647 Binary files /dev/null and b/Client2021/content/fonts/Inconsolata-Regular.ttf differ diff --git a/Client2021/content/fonts/IndieFlower-Regular.ttf b/Client2021/content/fonts/IndieFlower-Regular.ttf new file mode 100644 index 0000000..1070aac Binary files /dev/null and b/Client2021/content/fonts/IndieFlower-Regular.ttf differ diff --git a/Client2021/content/fonts/JosefinSans-Regular.ttf b/Client2021/content/fonts/JosefinSans-Regular.ttf new file mode 100644 index 0000000..89d36f8 Binary files /dev/null and b/Client2021/content/fonts/JosefinSans-Regular.ttf differ diff --git a/Client2021/content/fonts/Jura-Regular.ttf b/Client2021/content/fonts/Jura-Regular.ttf new file mode 100644 index 0000000..64ae1a3 Binary files /dev/null and b/Client2021/content/fonts/Jura-Regular.ttf differ diff --git a/Client2021/content/fonts/Kalam-Regular.ttf b/Client2021/content/fonts/Kalam-Regular.ttf new file mode 100644 index 0000000..16f1586 Binary files /dev/null and b/Client2021/content/fonts/Kalam-Regular.ttf differ diff --git a/Client2021/content/fonts/LuckiestGuy-Regular.ttf b/Client2021/content/fonts/LuckiestGuy-Regular.ttf new file mode 100644 index 0000000..8c79c87 Binary files /dev/null and b/Client2021/content/fonts/LuckiestGuy-Regular.ttf differ diff --git a/Client2021/content/fonts/Merriweather-Italic.ttf b/Client2021/content/fonts/Merriweather-Italic.ttf new file mode 100644 index 0000000..179acf3 Binary files /dev/null and b/Client2021/content/fonts/Merriweather-Italic.ttf differ diff --git a/Client2021/content/fonts/Merriweather-Regular.ttf b/Client2021/content/fonts/Merriweather-Regular.ttf new file mode 100644 index 0000000..18da9e5 Binary files /dev/null and b/Client2021/content/fonts/Merriweather-Regular.ttf differ diff --git a/Client2021/content/fonts/Michroma-Regular.ttf b/Client2021/content/fonts/Michroma-Regular.ttf new file mode 100644 index 0000000..0c35394 Binary files /dev/null and b/Client2021/content/fonts/Michroma-Regular.ttf differ diff --git a/Client2021/content/fonts/NotoSansBengaliUI-Regular.ttf b/Client2021/content/fonts/NotoSansBengaliUI-Regular.ttf new file mode 100644 index 0000000..f53a89c Binary files /dev/null and b/Client2021/content/fonts/NotoSansBengaliUI-Regular.ttf differ diff --git a/Client2021/content/fonts/NotoSansDevanagariUI-Regular.ttf b/Client2021/content/fonts/NotoSansDevanagariUI-Regular.ttf new file mode 100644 index 0000000..1f9fb2e Binary files /dev/null and b/Client2021/content/fonts/NotoSansDevanagariUI-Regular.ttf differ diff --git a/Client2021/content/fonts/NotoSansGeorgian-Regular.ttf b/Client2021/content/fonts/NotoSansGeorgian-Regular.ttf new file mode 100644 index 0000000..8a96e5e Binary files /dev/null and b/Client2021/content/fonts/NotoSansGeorgian-Regular.ttf differ diff --git a/Client2021/content/fonts/NotoSansKhmerUI-Regular.ttf b/Client2021/content/fonts/NotoSansKhmerUI-Regular.ttf new file mode 100644 index 0000000..a965f2b Binary files /dev/null and b/Client2021/content/fonts/NotoSansKhmerUI-Regular.ttf differ diff --git a/Client2021/content/fonts/NotoSansMyanmarUI-Regular.ttf b/Client2021/content/fonts/NotoSansMyanmarUI-Regular.ttf new file mode 100644 index 0000000..b015550 Binary files /dev/null and b/Client2021/content/fonts/NotoSansMyanmarUI-Regular.ttf differ diff --git a/Client2021/content/fonts/NotoSansSinhalaUI-Regular.ttf b/Client2021/content/fonts/NotoSansSinhalaUI-Regular.ttf new file mode 100644 index 0000000..4a49b3c Binary files /dev/null and b/Client2021/content/fonts/NotoSansSinhalaUI-Regular.ttf differ diff --git a/Client2021/content/fonts/NotoSansThaiUI-Regular.ttf b/Client2021/content/fonts/NotoSansThaiUI-Regular.ttf new file mode 100644 index 0000000..89bc809 Binary files /dev/null and b/Client2021/content/fonts/NotoSansThaiUI-Regular.ttf differ diff --git a/Client2021/content/fonts/Nunito-Regular.ttf b/Client2021/content/fonts/Nunito-Regular.ttf new file mode 100644 index 0000000..fdeb018 Binary files /dev/null and b/Client2021/content/fonts/Nunito-Regular.ttf differ diff --git a/Client2021/content/fonts/Oswald-Bold.ttf b/Client2021/content/fonts/Oswald-Bold.ttf new file mode 100644 index 0000000..6d99a38 Binary files /dev/null and b/Client2021/content/fonts/Oswald-Bold.ttf differ diff --git a/Client2021/content/fonts/Oswald-Regular.ttf b/Client2021/content/fonts/Oswald-Regular.ttf new file mode 100644 index 0000000..2492c44 Binary files /dev/null and b/Client2021/content/fonts/Oswald-Regular.ttf differ diff --git a/Client2021/content/fonts/PatrickHand-Regular.ttf b/Client2021/content/fonts/PatrickHand-Regular.ttf new file mode 100644 index 0000000..1e871cf Binary files /dev/null and b/Client2021/content/fonts/PatrickHand-Regular.ttf differ diff --git a/Client2021/content/fonts/PermanentMarker-Regular.ttf b/Client2021/content/fonts/PermanentMarker-Regular.ttf new file mode 100644 index 0000000..6541e9d Binary files /dev/null and b/Client2021/content/fonts/PermanentMarker-Regular.ttf differ diff --git a/Client2021/content/fonts/PressStart2P-Regular.ttf b/Client2021/content/fonts/PressStart2P-Regular.ttf new file mode 100644 index 0000000..e659e95 Binary files /dev/null and b/Client2021/content/fonts/PressStart2P-Regular.ttf differ diff --git a/Client2021/content/fonts/Roboto-Bold.ttf b/Client2021/content/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000..d998cf5 Binary files /dev/null and b/Client2021/content/fonts/Roboto-Bold.ttf differ diff --git a/Client2021/content/fonts/Roboto-Italic.ttf b/Client2021/content/fonts/Roboto-Italic.ttf new file mode 100644 index 0000000..5b390ff Binary files /dev/null and b/Client2021/content/fonts/Roboto-Italic.ttf differ diff --git a/Client2021/content/fonts/Roboto-Regular.ttf b/Client2021/content/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..2b6392f Binary files /dev/null and b/Client2021/content/fonts/Roboto-Regular.ttf differ diff --git a/Client2021/content/fonts/RobotoCondensed-Regular.ttf b/Client2021/content/fonts/RobotoCondensed-Regular.ttf new file mode 100644 index 0000000..62dd61e Binary files /dev/null and b/Client2021/content/fonts/RobotoCondensed-Regular.ttf differ diff --git a/Client2021/content/fonts/RobotoMono-Regular.ttf b/Client2021/content/fonts/RobotoMono-Regular.ttf new file mode 100644 index 0000000..7c4ce36 Binary files /dev/null and b/Client2021/content/fonts/RobotoMono-Regular.ttf differ diff --git a/Client2021/content/fonts/RomanAntique.otf b/Client2021/content/fonts/RomanAntique.otf new file mode 100644 index 0000000..dac6f3d Binary files /dev/null and b/Client2021/content/fonts/RomanAntique.otf differ diff --git a/Client2021/content/fonts/Sarpanch-Bold.ttf b/Client2021/content/fonts/Sarpanch-Bold.ttf new file mode 100644 index 0000000..f7603a2 Binary files /dev/null and b/Client2021/content/fonts/Sarpanch-Bold.ttf differ diff --git a/Client2021/content/fonts/Sarpanch-Regular.ttf b/Client2021/content/fonts/Sarpanch-Regular.ttf new file mode 100644 index 0000000..65ae057 Binary files /dev/null and b/Client2021/content/fonts/Sarpanch-Regular.ttf differ diff --git a/Client2021/content/fonts/SourceSansPro-Bold.ttf b/Client2021/content/fonts/SourceSansPro-Bold.ttf new file mode 100644 index 0000000..1f430e2 Binary files /dev/null and b/Client2021/content/fonts/SourceSansPro-Bold.ttf differ diff --git a/Client2021/content/fonts/SourceSansPro-It.ttf b/Client2021/content/fonts/SourceSansPro-It.ttf new file mode 100644 index 0000000..e7b9182 Binary files /dev/null and b/Client2021/content/fonts/SourceSansPro-It.ttf differ diff --git a/Client2021/content/fonts/SourceSansPro-Light.ttf b/Client2021/content/fonts/SourceSansPro-Light.ttf new file mode 100644 index 0000000..348871a Binary files /dev/null and b/Client2021/content/fonts/SourceSansPro-Light.ttf differ diff --git a/Client2021/content/fonts/SourceSansPro-Regular.ttf b/Client2021/content/fonts/SourceSansPro-Regular.ttf new file mode 100644 index 0000000..b422bf4 Binary files /dev/null and b/Client2021/content/fonts/SourceSansPro-Regular.ttf differ diff --git a/Client2021/content/fonts/SourceSansPro-Semibold.ttf b/Client2021/content/fonts/SourceSansPro-Semibold.ttf new file mode 100644 index 0000000..2908e0d Binary files /dev/null and b/Client2021/content/fonts/SourceSansPro-Semibold.ttf differ diff --git a/Client2021/content/fonts/SpecialElite-Regular.ttf b/Client2021/content/fonts/SpecialElite-Regular.ttf new file mode 100644 index 0000000..a645a5e Binary files /dev/null and b/Client2021/content/fonts/SpecialElite-Regular.ttf differ diff --git a/Client2021/content/fonts/TitilliumWeb-Bold.ttf b/Client2021/content/fonts/TitilliumWeb-Bold.ttf new file mode 100644 index 0000000..b51a4d6 Binary files /dev/null and b/Client2021/content/fonts/TitilliumWeb-Bold.ttf differ diff --git a/Client2021/content/fonts/TitilliumWeb-Regular.ttf b/Client2021/content/fonts/TitilliumWeb-Regular.ttf new file mode 100644 index 0000000..a54ad4b Binary files /dev/null and b/Client2021/content/fonts/TitilliumWeb-Regular.ttf differ diff --git a/Client2021/content/fonts/TwemojiMozilla.ttf b/Client2021/content/fonts/TwemojiMozilla.ttf new file mode 100644 index 0000000..6091c67 Binary files /dev/null and b/Client2021/content/fonts/TwemojiMozilla.ttf differ diff --git a/Client2021/content/fonts/Ubuntu-Italic.ttf b/Client2021/content/fonts/Ubuntu-Italic.ttf new file mode 100644 index 0000000..b022726 Binary files /dev/null and b/Client2021/content/fonts/Ubuntu-Italic.ttf differ diff --git a/Client2021/content/fonts/Ubuntu-Regular.ttf b/Client2021/content/fonts/Ubuntu-Regular.ttf new file mode 100644 index 0000000..dbb834a Binary files /dev/null and b/Client2021/content/fonts/Ubuntu-Regular.ttf differ diff --git a/Client2021/content/fonts/arial.ttf b/Client2021/content/fonts/arial.ttf new file mode 100644 index 0000000..729da61 Binary files /dev/null and b/Client2021/content/fonts/arial.ttf differ diff --git a/Client2021/content/fonts/arialbd.ttf b/Client2021/content/fonts/arialbd.ttf new file mode 100644 index 0000000..aac564c Binary files /dev/null and b/Client2021/content/fonts/arialbd.ttf differ diff --git a/Client2021/content/fonts/gamecontrollerdb.txt b/Client2021/content/fonts/gamecontrollerdb.txt new file mode 100644 index 0000000..dd49836 --- /dev/null +++ b/Client2021/content/fonts/gamecontrollerdb.txt @@ -0,0 +1,89 @@ +# Windows - DINPUT +8f0e1200000000000000504944564944,Acme,platform:Windows,x:b2,a:b0,b:b1,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +341a3608000000000000504944564944,Afterglow PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, +ffff0000000000000000504944564944,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, +6d0416c2000000000000504944564944,Generic DirectInput Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, +6d0419c2000000000000504944564944,Logitech F710 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, +88880803000000000000504944564944,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b9,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b0,y:b3,platform:Windows, +4c056802000000000000504944564944,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Windows, +25090500000000000000504944564944,PS3 DualShock,a:b2,b:b1,back:b9,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b0,y:b3,platform:Windows, +4c05c405000000000000504944564944,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, +6d0418c2000000000000504944564944,Logitech RumblePad 2 USB,platform:Windows,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, +36280100000000000000504944564944,OUYA Controller,platform:Windows,a:b0,b:b3,y:b2,x:b1,start:b14,guide:b15,leftstick:b6,rightstick:b7,leftshoulder:b4,rightshoulder:b5,dpup:b8,dpleft:b10,dpdown:b9,dpright:b11,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:b12,righttrigger:b13, +4f0400b3000000000000504944564944,Thrustmaster Firestorm Dual Power,a:b0,b:b2,y:b3,x:b1,start:b10,guide:b8,back:b9,leftstick:b11,rightstick:b12,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Windows, +00f00300000000000000504944564944,RetroUSB.com RetroPad,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Windows, +00f0f100000000000000504944564944,RetroUSB.com Super RetroPort,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Windows, +28040140000000000000504944564944,GamePad Pro USB,platform:Windows,a:b1,b:b2,x:b0,y:b3,back:b8,start:b9,leftshoulder:b4,rightshoulder:b5,leftx:a0,lefty:a1,lefttrigger:b6,righttrigger:b7, +ff113133000000000000504944564944,SVEN X-PAD,platform:Windows,a:b2,b:b3,y:b1,x:b0,start:b5,back:b4,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a4,lefttrigger:b8,righttrigger:b9, +8f0e0300000000000000504944564944,Piranha xtreme,platform:Windows,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +8f0e0d31000000000000504944564944,Multilaser JS071 USB,platform:Windows,a:b1,b:b2,y:b3,x:b0,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7, +10080300000000000000504944564944,PS2 USB,platform:Windows,a:b2,b:b1,y:b0,x:b3,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a4,righty:a2,lefttrigger:b4,righttrigger:b5, +79000600000000000000504944564944,G-Shark GS-GP702,a:b2,b:b1,x:b3,y:b0,back:b8,start:b9,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a4,lefttrigger:b6,righttrigger:b7,platform:Windows, +4b12014d000000000000504944564944,NYKO AIRFLO,a:b0,b:b1,x:b2,y:b3,back:b8,guide:b10,start:b9,leftstick:a0,rightstick:a2,leftshoulder:a3,rightshoulder:b5,dpup:h0.1,dpdown:h0.0,dpleft:h0.8,dpright:h0.2,leftx:h0.6,lefty:h0.12,rightx:h0.9,righty:h0.4,lefttrigger:b6,righttrigger:b7,platform:Windows, +d6206dca000000000000504944564944,PowerA Pro Ex,a:b1,b:b2,x:b0,y:b3,back:b8,guide:b12,start:b9,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.0,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7,platform:Windows, +a3060cff000000000000504944564944,Saitek P2500,a:b2,b:b3,y:b1,x:b0,start:b4,guide:b10,back:b5,leftstick:b8,rightstick:b9,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,platform:Windows, +8f0e0300000000000000504944564944,Trust GTX 28,a:b2,b:b1,y:b0,x:b3,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7,platform:Windows, +4f0415b3000000000000504944564944,Thrustmaster Dual Analog 3.2,platform:Windows,x:b1,a:b0,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, + +# OS X +0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X, +6d0400000000000016c2000000000000,Logitech F310 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, +6d0400000000000018c2000000000000,Logitech F510 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, +6d040000000000001fc2000000000000,Logitech F710 Gamepad (XInput),a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, +6d0400000000000019c2000000000000,Logitech Wireless Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, +4c050000000000006802000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Mac OS X, +4c05000000000000c405000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,Platform:Mac OS X, +5e040000000000008e02000000000000,X360 Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, +891600000000000000fd000000000000,Razer Onza Tournament,a:b0,b:b1,y:b3,x:b2,start:b8,guide:b10,back:b9,leftstick:b6,rightstick:b7,leftshoulder:b4,rightshoulder:b5,dpup:b11,dpleft:b13,dpdown:b12,dpright:b14,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Mac OS X, +4f0400000000000000b3000000000000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,y:b3,x:b1,start:b10,guide:b8,back:b9,leftstick:b11,rightstick:,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Mac OS X, +8f0e0000000000000300000000000000,Piranha xtreme,platform:Mac OS X,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +0d0f0000000000004d00000000000000,HORI Gem Pad 3,platform:Mac OS X,a:b1,b:b2,y:b3,x:b0,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7, +79000000000000000600000000000000,G-Shark GP-702,a:b2,b:b1,x:b3,y:b0,back:b8,start:b9,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:b6,righttrigger:b7,platform:Mac OS X, +4f0400000000000015b3000000000000,Thrustmaster Dual Analog 3.2,platform:Mac OS X,x:b1,a:b0,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, + +# Linux +0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, +03000000ba2200002010000001010000,Jess Technology USB Game Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux, +030000006d04000019c2000010010000,Logitech Cordless RumblePad 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, +030000006d0400001dc2000014400000,Logitech F310 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000006d0400001ec2000020200000,Logitech F510 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000006d04000019c2000011010000,Logitech F710 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, +030000006d0400001fc2000005030000,Logitech F710 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000004c0500006802000011010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux, +030000004c050000c405000011010000,Sony DualShock 4,a:b1,b:b2,y:b3,x:b0,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a5,lefttrigger:b6,righttrigger:b7,platform:Linux, +03000000de280000ff11000001000000,Valve Streaming Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e0400008e02000014010000,X360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e0400008e02000010010000,X360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e0400001907000000010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +03000000100800000100000010010000,Twin USB PS2 Adapter,a:b2,b:b1,y:b0,x:b3,start:b9,guide:,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a2,lefttrigger:b4,righttrigger:b5,platform:Linux, +03000000a306000023f6000011010000,Saitek Cyborg V.1 Game Pad,a:b1,b:b2,y:b3,x:b0,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a4,lefttrigger:b6,righttrigger:b7,platform:Linux, +030000004f04000020b3000010010000,Thrustmaster 2 in 1 DT,a:b0,b:b2,y:b3,x:b1,start:b9,guide:,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Linux, +030000004f04000023b3000000010000,Thrustmaster Dual Trigger 3-in-1,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a5, +030000008f0e00000300000010010000,GreenAsia Inc. USB Joystick ,platform:Linux,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +030000008f0e00001200000010010000,GreenAsia Inc. USB Joystick ,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a2, +030000005e0400009102000007010000,X360 Wireless Controller,a:b0,b:b1,y:b3,x:b2,start:b7,guide:b8,back:b6,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:b13,dpleft:b11,dpdown:b14,dpright:b12,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux, +030000006d04000016c2000010010000,Logitech Logitech Dual Action,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, +03000000260900008888000000010000,GameCube {WiseGroup USB box},a:b0,b:b2,y:b3,x:b1,start:b7,leftshoulder:,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,rightstick:,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,platform:Linux, +030000006d04000011c2000010010000,Logitech WingMan Cordless RumblePad,a:b0,b:b1,y:b4,x:b3,start:b8,guide:b5,back:b2,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:b9,righttrigger:b10,platform:Linux, +030000006d04000018c2000010010000,Logitech Logitech RumblePad 2 USB,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3, +05000000d6200000ad0d000001000000,Moga Pro,platform:Linux,a:b0,b:b1,y:b3,x:b2,start:b6,leftstick:b7,rightstick:b8,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a5,righttrigger:a4, +030000004f04000009d0000000010000,Thrustmaster Run N Drive Wireless PS3,platform:Linux,a:b1,b:b2,x:b0,y:b3,start:b9,guide:b12,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7, +030000004f04000008d0000000010000,Thrustmaster Run N Drive Wireless,platform:Linux,a:b1,b:b2,x:b0,y:b3,start:b9,back:b8,leftstick:b10,rightstick:b11,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a5,lefttrigger:b6,righttrigger:b7, +0300000000f000000300000000010000,RetroUSB.com RetroPad,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Linux, +0300000000f00000f100000000010000,RetroUSB.com Super RetroPort,a:b1,b:b5,x:b0,y:b4,back:b2,start:b3,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,platform:Linux, +030000006f0e00001f01000000010000,Generic X-Box pad,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b6,guide:b8,start:b7,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,leftstick:b9,rightstick:b10,leftx:a0,lefty:a1,rightx:a3,righty:a4, +03000000280400000140000000010000,Gravis GamePad Pro USB ,platform:Linux,x:b0,a:b1,b:b2,y:b3,back:b8,start:b9,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftx:a0,lefty:a1, +030000005e0400008902000021010000,Microsoft X-Box pad v2 (US),platform:Linux,x:b3,a:b0,b:b1,y:b4,back:b6,start:b7,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b5,lefttrigger:a2,rightshoulder:b2,righttrigger:a5,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a3,righty:a4, +030000006f0e00001e01000011010000,Rock Candy Gamepad for PS3,platform:Linux,a:b1,b:b2,x:b0,y:b3,back:b8,start:b9,guide:b12,leftshoulder:b4,rightshoulder:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2, +03000000250900000500000000010000,Sony PS2 pad with SmartJoy adapter,platform:Linux,a:b2,b:b1,y:b0,x:b3,start:b8,back:b9,leftstick:b10,rightstick:b11,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b4,righttrigger:b5, +030000008916000000fd000024010000,Razer Onza Tournament,a:b0,b:b1,y:b3,x:b2,start:b7,guide:b8,back:b6,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:b13,dpleft:b11,dpdown:b14,dpright:b12,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux, +030000004f04000000b3000010010000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,y:b3,x:b1,start:b10,guide:b8,back:b9,leftstick:b11,rightstick:b12,leftshoulder:b4,rightshoulder:b6,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b5,righttrigger:b7,platform:Linux, +03000000ad1b000001f5000033050000,Hori Pad EX Turbo 2,a:b0,b:b1,y:b3,x:b2,start:b7,guide:b8,back:b6,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux, +050000004c050000c405000000010000,PS4 Controller (Bluetooth),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, +060000004c0500006802000000010000,PS3 Controller (Bluetooth),a:b14,b:b13,y:b12,x:b15,start:b3,guide:b16,back:b0,leftstick:b1,rightstick:b2,leftshoulder:b10,rightshoulder:b11,dpup:b4,dpleft:b7,dpdown:b6,dpright:b5,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b8,righttrigger:b9,platform:Linux, +03000000790000000600000010010000,DragonRise Inc. Generic USB Joystick ,platform:Linux,x:b3,a:b2,b:b1,y:b0,back:b8,start:b9,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a3,righty:a4, +03000000666600000488000000010000,Super Joy Box 5 Pro,platform:Linux,a:b2,b:b1,x:b3,y:b0,back:b9,start:b8,leftshoulder:b6,rightshoulder:b7,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b4,righttrigger:b5,dpup:b12,dpleft:b15,dpdown:b14,dpright:b13, +05000000362800000100000002010000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,platform:Linux,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2, +05000000362800000100000003010000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,platform:Linux,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2, +030000008916000001fd000024010000,Razer Onza Classic Edition,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b6,guide:b8,start:b7,dpleft:b11,dpdown:b14,dpright:b12,dpup:b13,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,leftstick:b9,rightstick:b10,leftx:a0,lefty:a1,rightx:a3,righty:a4, +030000005e040000d102000001010000,Microsoft X-Box One pad,platform:Linux,x:b2,a:b0,b:b1,y:b3,back:b6,guide:b8,start:b7,dpleft:h0.8,dpdown:h0.0,dpdown:h0.4,dpright:h0.0,dpright:h0.2,dpup:h0.0,dpup:h0.1,leftshoulder:h0.0,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,leftstick:b9,rightstick:b10,leftx:a0,lefty:a1,rightx:a3,righty:a4, \ No newline at end of file diff --git a/Client2021/content/fonts/zekton_rg.ttf b/Client2021/content/fonts/zekton_rg.ttf new file mode 100644 index 0000000..8ad2dd1 Binary files /dev/null and b/Client2021/content/fonts/zekton_rg.ttf differ diff --git a/Client2021/content/models/AnimationEditor/AnimationEditorGUI.rbxm b/Client2021/content/models/AnimationEditor/AnimationEditorGUI.rbxm new file mode 100644 index 0000000..52bbda4 Binary files /dev/null and b/Client2021/content/models/AnimationEditor/AnimationEditorGUI.rbxm differ diff --git a/Client2021/content/models/RigBuilder/AnthroRigs.rbxm b/Client2021/content/models/RigBuilder/AnthroRigs.rbxm new file mode 100644 index 0000000..b10542a Binary files /dev/null and b/Client2021/content/models/RigBuilder/AnthroRigs.rbxm differ diff --git a/Client2021/content/models/RigBuilder/RigBuilderGUI.rbxm b/Client2021/content/models/RigBuilder/RigBuilderGUI.rbxm new file mode 100644 index 0000000..df189cb Binary files /dev/null and b/Client2021/content/models/RigBuilder/RigBuilderGUI.rbxm differ diff --git a/Client2021/content/models/Thumbnails/Mannequins/R15.rbxm b/Client2021/content/models/Thumbnails/Mannequins/R15.rbxm new file mode 100644 index 0000000..db99688 Binary files /dev/null and b/Client2021/content/models/Thumbnails/Mannequins/R15.rbxm differ diff --git a/Client2021/content/models/Thumbnails/Mannequins/R6.rbxmx b/Client2021/content/models/Thumbnails/Mannequins/R6.rbxmx new file mode 100644 index 0000000..757716b --- /dev/null +++ b/Client2021/content/models/Thumbnails/Mannequins/R6.rbxmx @@ -0,0 +1,970 @@ + + null + nil + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + MrGrey + RBX1 + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 208 + + 0 + 4.5 + -0.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Head + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 1 + 1 + + + + + 2 + 2 + + 0 + Mesh + + 0 + 0 + 0 + + + 1.25 + 1.25 + 1.25 + + + + 1 + 1 + 1 + + + + + + 5 + face + rbxasset://textures/face.png + 0 + + + + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + FaceCenterAttachment + false + + + + + + 0 + 0 + -0.600000024 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + FaceFrontAttachment + false + + + + + + 0 + 0.600000024 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + HairAttachment + false + + + + + + 0 + 0.600000024 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + HatAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 208 + + 0 + 3 + -0.5 + -1 + 0 + -0 + -0 + 1 + -0 + -0 + 0 + -1 + + true + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + 0 + 0 + 2 + 0 + true + 256 + Torso + 0 + 0 + 0 + 2 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 2 + 2 + 1 + + + + + + 1 + 0.5 + 0 + 0 + 0 + 1 + 0 + 1 + 0 + -1 + -0 + -0 + + + -0.5 + 0.5 + 0 + 0 + 0 + 1 + 0 + 1 + 0 + -1 + -0 + -0 + + 0 + 0.100000001 + Right Shoulder + RBX4 + RBX6 + + + + + + -1 + 0.5 + 0 + -0 + -0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + + 0.5 + 0.5 + 0 + -0 + -0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + 0 + 0.100000001 + Left Shoulder + RBX4 + RBX8 + + + + + + 1 + -1 + 0 + 0 + 0 + 1 + 0 + 1 + 0 + -1 + -0 + -0 + + + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 1 + 0 + -1 + -0 + -0 + + 0 + 0.100000001 + Right Hip + RBX4 + RBX10 + + + + + + -1 + -1 + 0 + -0 + -0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + + -0.5 + 1 + 0 + -0 + -0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + 0 + 0.100000001 + Left Hip + RBX4 + RBX12 + + + + + + 0 + 1 + 0 + -1 + -0 + -0 + 0 + 0 + 1 + 0 + 1 + 0 + + + 0 + -0.5 + 0 + -1 + -0 + -0 + 0 + 0 + 1 + 0 + 1 + 0 + + 0 + 0.100000001 + Neck + RBX4 + RBX1 + + + + + + 0 + 0 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyBackAttachment + false + + + + + + 0 + 0 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + BodyFrontAttachment + false + + + + + + -1 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftCollarAttachment + false + + + + + + 0 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + NeckAttachment + false + + + + + + 1 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightCollarAttachment + false + + + + + + 0 + -1 + 0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistBackAttachment + false + + + + + + 0 + -1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistCenterAttachment + false + + + + + + 0 + -1 + -0.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + WaistFrontAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 208 + + 1.5 + 3 + -0.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Left Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + LeftShoulderAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + 208 + + -1.5 + 3 + -0.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Right Arm + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + RightShoulderAttachment + false + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 208 + + 0.5 + 1 + -0.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Left Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + false + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + 208 + + -0.5 + 1 + -0.5 + -1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + -1 + + false + + false + + 0.5 + 0.300000012 + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 256 + Right Leg + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + 0 + 1 + + 1 + 2 + 1 + + + + + + 0 + 100 + 100 + 0 + 50 + 100 + 89 + Humanoid + 100 + 2 + 0 + 16 + + + + \ No newline at end of file diff --git a/Client2021/content/models/Thumbnails/Mannequins/Rthro.rbxm b/Client2021/content/models/Thumbnails/Mannequins/Rthro.rbxm new file mode 100644 index 0000000..b1603cf Binary files /dev/null and b/Client2021/content/models/Thumbnails/Mannequins/Rthro.rbxm differ diff --git a/Client2021/content/models/ViewSelector/Axis.mesh b/Client2021/content/models/ViewSelector/Axis.mesh new file mode 100644 index 0000000..83cd155 Binary files /dev/null and b/Client2021/content/models/ViewSelector/Axis.mesh differ diff --git a/Client2021/content/models/ViewSelector/Basic.mesh b/Client2021/content/models/ViewSelector/Basic.mesh new file mode 100644 index 0000000..077ecb8 Binary files /dev/null and b/Client2021/content/models/ViewSelector/Basic.mesh differ diff --git a/Client2021/content/models/ViewSelector/Corner.mesh b/Client2021/content/models/ViewSelector/Corner.mesh new file mode 100644 index 0000000..291e930 Binary files /dev/null and b/Client2021/content/models/ViewSelector/Corner.mesh differ diff --git a/Client2021/content/models/ViewSelector/ViewSelector.rbxm b/Client2021/content/models/ViewSelector/ViewSelector.rbxm new file mode 100644 index 0000000..7c3b9e1 Binary files /dev/null and b/Client2021/content/models/ViewSelector/ViewSelector.rbxm differ diff --git a/Client2021/content/sky/clouds.dds b/Client2021/content/sky/clouds.dds new file mode 100644 index 0000000..cb5f05a Binary files /dev/null and b/Client2021/content/sky/clouds.dds differ diff --git a/Client2021/content/sky/cloudsfb.dds b/Client2021/content/sky/cloudsfb.dds new file mode 100644 index 0000000..0e146d2 Binary files /dev/null and b/Client2021/content/sky/cloudsfb.dds differ diff --git a/Client2021/content/sky/moon.jpg b/Client2021/content/sky/moon.jpg new file mode 100644 index 0000000..247b6cd Binary files /dev/null and b/Client2021/content/sky/moon.jpg differ diff --git a/Client2021/content/sky/noise.dds b/Client2021/content/sky/noise.dds new file mode 100644 index 0000000..91023d4 Binary files /dev/null and b/Client2021/content/sky/noise.dds differ diff --git a/Client2021/content/sky/noisefb.dds b/Client2021/content/sky/noisefb.dds new file mode 100644 index 0000000..0e3707b Binary files /dev/null and b/Client2021/content/sky/noisefb.dds differ diff --git a/Client2021/content/sky/sun.jpg b/Client2021/content/sky/sun.jpg new file mode 100644 index 0000000..41057ac Binary files /dev/null and b/Client2021/content/sky/sun.jpg differ diff --git a/Client2021/content/sounds/action_falling.mp3 b/Client2021/content/sounds/action_falling.mp3 new file mode 100644 index 0000000..6408717 Binary files /dev/null and b/Client2021/content/sounds/action_falling.mp3 differ diff --git a/Client2021/content/sounds/action_footsteps_plastic.mp3 b/Client2021/content/sounds/action_footsteps_plastic.mp3 new file mode 100644 index 0000000..6804420 Binary files /dev/null and b/Client2021/content/sounds/action_footsteps_plastic.mp3 differ diff --git a/Client2021/content/sounds/action_get_up.mp3 b/Client2021/content/sounds/action_get_up.mp3 new file mode 100644 index 0000000..ababd77 Binary files /dev/null and b/Client2021/content/sounds/action_get_up.mp3 differ diff --git a/Client2021/content/sounds/action_jump.mp3 b/Client2021/content/sounds/action_jump.mp3 new file mode 100644 index 0000000..7159074 Binary files /dev/null and b/Client2021/content/sounds/action_jump.mp3 differ diff --git a/Client2021/content/sounds/action_jump_land.mp3 b/Client2021/content/sounds/action_jump_land.mp3 new file mode 100644 index 0000000..3d68ce1 Binary files /dev/null and b/Client2021/content/sounds/action_jump_land.mp3 differ diff --git a/Client2021/content/sounds/action_swim.mp3 b/Client2021/content/sounds/action_swim.mp3 new file mode 100644 index 0000000..95dd2d5 Binary files /dev/null and b/Client2021/content/sounds/action_swim.mp3 differ diff --git a/Client2021/content/sounds/impact_explosion_03.mp3 b/Client2021/content/sounds/impact_explosion_03.mp3 new file mode 100644 index 0000000..4501c83 Binary files /dev/null and b/Client2021/content/sounds/impact_explosion_03.mp3 differ diff --git a/Client2021/content/sounds/impact_water.mp3 b/Client2021/content/sounds/impact_water.mp3 new file mode 100644 index 0000000..741862b Binary files /dev/null and b/Client2021/content/sounds/impact_water.mp3 differ diff --git a/Client2021/content/sounds/uuhhh.mp3 b/Client2021/content/sounds/uuhhh.mp3 new file mode 100644 index 0000000..81b3565 Binary files /dev/null and b/Client2021/content/sounds/uuhhh.mp3 differ diff --git a/Client2021/content/textures/AlignTool/AlignTool.png b/Client2021/content/textures/AlignTool/AlignTool.png new file mode 100644 index 0000000..278b590 Binary files /dev/null and b/Client2021/content/textures/AlignTool/AlignTool.png differ diff --git a/Client2021/content/textures/AlignTool/Center.png b/Client2021/content/textures/AlignTool/Center.png new file mode 100644 index 0000000..0f52463 Binary files /dev/null and b/Client2021/content/textures/AlignTool/Center.png differ diff --git a/Client2021/content/textures/AlignTool/Help.png b/Client2021/content/textures/AlignTool/Help.png new file mode 100644 index 0000000..c575bb0 Binary files /dev/null and b/Client2021/content/textures/AlignTool/Help.png differ diff --git a/Client2021/content/textures/AlignTool/Help_Dark.png b/Client2021/content/textures/AlignTool/Help_Dark.png new file mode 100644 index 0000000..49e0851 Binary files /dev/null and b/Client2021/content/textures/AlignTool/Help_Dark.png differ diff --git a/Client2021/content/textures/AlignTool/Help_Light.png b/Client2021/content/textures/AlignTool/Help_Light.png new file mode 100644 index 0000000..604e6f0 Binary files /dev/null and b/Client2021/content/textures/AlignTool/Help_Light.png differ diff --git a/Client2021/content/textures/AlignTool/Max.png b/Client2021/content/textures/AlignTool/Max.png new file mode 100644 index 0000000..e197ea8 Binary files /dev/null and b/Client2021/content/textures/AlignTool/Max.png differ diff --git a/Client2021/content/textures/AlignTool/Min.png b/Client2021/content/textures/AlignTool/Min.png new file mode 100644 index 0000000..e7a5390 Binary files /dev/null and b/Client2021/content/textures/AlignTool/Min.png differ diff --git a/Client2021/content/textures/AlignTool/button_center_24.png b/Client2021/content/textures/AlignTool/button_center_24.png new file mode 100644 index 0000000..014aad4 Binary files /dev/null and b/Client2021/content/textures/AlignTool/button_center_24.png differ diff --git a/Client2021/content/textures/AlignTool/button_max_24.png b/Client2021/content/textures/AlignTool/button_max_24.png new file mode 100644 index 0000000..a222ca6 Binary files /dev/null and b/Client2021/content/textures/AlignTool/button_max_24.png differ diff --git a/Client2021/content/textures/AlignTool/button_min_24.png b/Client2021/content/textures/AlignTool/button_min_24.png new file mode 100644 index 0000000..b4dcf22 Binary files /dev/null and b/Client2021/content/textures/AlignTool/button_min_24.png differ diff --git a/Client2021/content/textures/AnchorCursor.png b/Client2021/content/textures/AnchorCursor.png new file mode 100644 index 0000000..cb00a6a Binary files /dev/null and b/Client2021/content/textures/AnchorCursor.png differ diff --git a/Client2021/content/textures/AnimationEditor/Checkmark.png b/Client2021/content/textures/AnimationEditor/Checkmark.png new file mode 100644 index 0000000..2d7a684 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/Checkmark.png differ diff --git a/Client2021/content/textures/AnimationEditor/Circle.png b/Client2021/content/textures/AnimationEditor/Circle.png new file mode 100644 index 0000000..eea192e Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/Circle.png differ diff --git a/Client2021/content/textures/AnimationEditor/Close.png b/Client2021/content/textures/AnimationEditor/Close.png new file mode 100644 index 0000000..2281cae Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/Close.png differ diff --git a/Client2021/content/textures/AnimationEditor/Pin.png b/Client2021/content/textures/AnimationEditor/Pin.png new file mode 100644 index 0000000..3b13b68 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/Pin.png differ diff --git a/Client2021/content/textures/AnimationEditor/RoundedBackground.png b/Client2021/content/textures/AnimationEditor/RoundedBackground.png new file mode 100644 index 0000000..602e90f Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/RoundedBackground.png differ diff --git a/Client2021/content/textures/AnimationEditor/RoundedBorder.png b/Client2021/content/textures/AnimationEditor/RoundedBorder.png new file mode 100644 index 0000000..8335459 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/RoundedBorder.png differ diff --git a/Client2021/content/textures/AnimationEditor/ScrollbarBottom.png b/Client2021/content/textures/AnimationEditor/ScrollbarBottom.png new file mode 100644 index 0000000..f84d082 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/ScrollbarBottom.png differ diff --git a/Client2021/content/textures/AnimationEditor/ScrollbarMiddle.png b/Client2021/content/textures/AnimationEditor/ScrollbarMiddle.png new file mode 100644 index 0000000..0e90165 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/ScrollbarMiddle.png differ diff --git a/Client2021/content/textures/AnimationEditor/ScrollbarTop.png b/Client2021/content/textures/AnimationEditor/ScrollbarTop.png new file mode 100644 index 0000000..beb8d14 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/ScrollbarTop.png differ diff --git a/Client2021/content/textures/AnimationEditor/addEvent_border.png b/Client2021/content/textures/AnimationEditor/addEvent_border.png new file mode 100644 index 0000000..cdae253 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/addEvent_border.png differ diff --git a/Client2021/content/textures/AnimationEditor/addEvent_inner.png b/Client2021/content/textures/AnimationEditor/addEvent_inner.png new file mode 100644 index 0000000..bcbfe9d Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/addEvent_inner.png differ diff --git a/Client2021/content/textures/AnimationEditor/animation_editor_32x32.png b/Client2021/content/textures/AnimationEditor/animation_editor_32x32.png new file mode 100644 index 0000000..2342f5b Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/animation_editor_32x32.png differ diff --git a/Client2021/content/textures/AnimationEditor/animation_editor_blue.png b/Client2021/content/textures/AnimationEditor/animation_editor_blue.png new file mode 100644 index 0000000..7d20402 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/animation_editor_blue.png differ diff --git a/Client2021/content/textures/AnimationEditor/btn_addEvent_border.png b/Client2021/content/textures/AnimationEditor/btn_addEvent_border.png new file mode 100644 index 0000000..cee4d3c Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/btn_addEvent_border.png differ diff --git a/Client2021/content/textures/AnimationEditor/btn_addEvent_inner.png b/Client2021/content/textures/AnimationEditor/btn_addEvent_inner.png new file mode 100644 index 0000000..ac37d56 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/btn_addEvent_inner.png differ diff --git a/Client2021/content/textures/AnimationEditor/btn_clearText.png b/Client2021/content/textures/AnimationEditor/btn_clearText.png new file mode 100644 index 0000000..a910d33 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/btn_clearText.png differ diff --git a/Client2021/content/textures/AnimationEditor/btn_collapse.png b/Client2021/content/textures/AnimationEditor/btn_collapse.png new file mode 100644 index 0000000..5bdcbeb Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/btn_collapse.png differ diff --git a/Client2021/content/textures/AnimationEditor/btn_delete.png b/Client2021/content/textures/AnimationEditor/btn_delete.png new file mode 100644 index 0000000..6a4c117 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/btn_delete.png differ diff --git a/Client2021/content/textures/AnimationEditor/btn_edit.png b/Client2021/content/textures/AnimationEditor/btn_edit.png new file mode 100644 index 0000000..49f301d Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/btn_edit.png differ diff --git a/Client2021/content/textures/AnimationEditor/btn_expand.png b/Client2021/content/textures/AnimationEditor/btn_expand.png new file mode 100644 index 0000000..35922dc Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/btn_expand.png differ diff --git a/Client2021/content/textures/AnimationEditor/btn_manage.png b/Client2021/content/textures/AnimationEditor/btn_manage.png new file mode 100644 index 0000000..7545e88 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/btn_manage.png differ diff --git a/Client2021/content/textures/AnimationEditor/btn_removeEvent.png b/Client2021/content/textures/AnimationEditor/btn_removeEvent.png new file mode 100644 index 0000000..6bd3c1b Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/btn_removeEvent.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_collapse.png b/Client2021/content/textures/AnimationEditor/button_collapse.png new file mode 100644 index 0000000..503a6f5 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_collapse.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_control_end.png b/Client2021/content/textures/AnimationEditor/button_control_end.png new file mode 100644 index 0000000..9493b62 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_control_end.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_control_next.png b/Client2021/content/textures/AnimationEditor/button_control_next.png new file mode 100644 index 0000000..e498efc Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_control_next.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_control_play.png b/Client2021/content/textures/AnimationEditor/button_control_play.png new file mode 100644 index 0000000..7e13020 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_control_play.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_control_previous.png b/Client2021/content/textures/AnimationEditor/button_control_previous.png new file mode 100644 index 0000000..95ad3cf Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_control_previous.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_control_start.png b/Client2021/content/textures/AnimationEditor/button_control_start.png new file mode 100644 index 0000000..2a13172 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_control_start.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_expand.png b/Client2021/content/textures/AnimationEditor/button_expand.png new file mode 100644 index 0000000..53d348d Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_expand.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_hierarchy_closed.png b/Client2021/content/textures/AnimationEditor/button_hierarchy_closed.png new file mode 100644 index 0000000..2a70e31 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_hierarchy_closed.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_hierarchy_opened.png b/Client2021/content/textures/AnimationEditor/button_hierarchy_opened.png new file mode 100644 index 0000000..c2d1e6d Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_hierarchy_opened.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_lock.png b/Client2021/content/textures/AnimationEditor/button_lock.png new file mode 100644 index 0000000..cf9ca95 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_lock.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_loop.png b/Client2021/content/textures/AnimationEditor/button_loop.png new file mode 100644 index 0000000..e585d2f Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_loop.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_pause_white@2x.png b/Client2021/content/textures/AnimationEditor/button_pause_white@2x.png new file mode 100644 index 0000000..7215f9b Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_pause_white@2x.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_popup_close.png b/Client2021/content/textures/AnimationEditor/button_popup_close.png new file mode 100644 index 0000000..3511f7c Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_popup_close.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_radio_background.png b/Client2021/content/textures/AnimationEditor/button_radio_background.png new file mode 100644 index 0000000..1f15a29 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_radio_background.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_radio_default.png b/Client2021/content/textures/AnimationEditor/button_radio_default.png new file mode 100644 index 0000000..5c54fd1 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_radio_default.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_radio_innercircle.png b/Client2021/content/textures/AnimationEditor/button_radio_innercircle.png new file mode 100644 index 0000000..b6a9efe Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_radio_innercircle.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_search.png b/Client2021/content/textures/AnimationEditor/button_search.png new file mode 100644 index 0000000..a08992b Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_search.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_zoom.png b/Client2021/content/textures/AnimationEditor/button_zoom.png new file mode 100644 index 0000000..966d410 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_zoom.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_zoom_default_left.png b/Client2021/content/textures/AnimationEditor/button_zoom_default_left.png new file mode 100644 index 0000000..e371819 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_zoom_default_left.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_zoom_default_left@2x.png b/Client2021/content/textures/AnimationEditor/button_zoom_default_left@2x.png new file mode 100644 index 0000000..45c7cc7 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_zoom_default_left@2x.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_zoom_default_right.png b/Client2021/content/textures/AnimationEditor/button_zoom_default_right.png new file mode 100644 index 0000000..e371819 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_zoom_default_right.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_zoom_default_right@2x.png b/Client2021/content/textures/AnimationEditor/button_zoom_default_right@2x.png new file mode 100644 index 0000000..45c7cc7 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_zoom_default_right@2x.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_zoom_hoverpressed_left.png b/Client2021/content/textures/AnimationEditor/button_zoom_hoverpressed_left.png new file mode 100644 index 0000000..4d19d86 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_zoom_hoverpressed_left.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_zoom_hoverpressed_left@2x.png b/Client2021/content/textures/AnimationEditor/button_zoom_hoverpressed_left@2x.png new file mode 100644 index 0000000..3b2f615 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_zoom_hoverpressed_left@2x.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_zoom_hoverpressed_right.png b/Client2021/content/textures/AnimationEditor/button_zoom_hoverpressed_right.png new file mode 100644 index 0000000..4d19d86 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_zoom_hoverpressed_right.png differ diff --git a/Client2021/content/textures/AnimationEditor/button_zoom_hoverpressed_right@2x.png b/Client2021/content/textures/AnimationEditor/button_zoom_hoverpressed_right@2x.png new file mode 100644 index 0000000..3b2f615 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/button_zoom_hoverpressed_right@2x.png differ diff --git a/Client2021/content/textures/AnimationEditor/eventMarker_border.png b/Client2021/content/textures/AnimationEditor/eventMarker_border.png new file mode 100644 index 0000000..6e05d59 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/eventMarker_border.png differ diff --git a/Client2021/content/textures/AnimationEditor/eventMarker_border_selected.png b/Client2021/content/textures/AnimationEditor/eventMarker_border_selected.png new file mode 100644 index 0000000..4771ef5 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/eventMarker_border_selected.png differ diff --git a/Client2021/content/textures/AnimationEditor/eventMarker_inner.png b/Client2021/content/textures/AnimationEditor/eventMarker_inner.png new file mode 100644 index 0000000..e387ed0 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/eventMarker_inner.png differ diff --git a/Client2021/content/textures/AnimationEditor/fbximportlogo.png b/Client2021/content/textures/AnimationEditor/fbximportlogo.png new file mode 100644 index 0000000..6acee80 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/fbximportlogo.png differ diff --git a/Client2021/content/textures/AnimationEditor/ic-checkbox-active.png b/Client2021/content/textures/AnimationEditor/ic-checkbox-active.png new file mode 100644 index 0000000..b897b38 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/ic-checkbox-active.png differ diff --git a/Client2021/content/textures/AnimationEditor/ic-checkbox-off.png b/Client2021/content/textures/AnimationEditor/ic-checkbox-off.png new file mode 100644 index 0000000..09fd802 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/ic-checkbox-off.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_add.png b/Client2021/content/textures/AnimationEditor/icon_add.png new file mode 100644 index 0000000..887ece1 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_add.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_checkmark.png b/Client2021/content/textures/AnimationEditor/icon_checkmark.png new file mode 100644 index 0000000..a61f1f2 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_checkmark.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_close.png b/Client2021/content/textures/AnimationEditor/icon_close.png new file mode 100644 index 0000000..04e03cf Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_close.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_dark_warning.png b/Client2021/content/textures/AnimationEditor/icon_dark_warning.png new file mode 100644 index 0000000..c8385d3 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_dark_warning.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_delete.png b/Client2021/content/textures/AnimationEditor/icon_delete.png new file mode 100644 index 0000000..f643c9b Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_delete.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_delete_disabled.png b/Client2021/content/textures/AnimationEditor/icon_delete_disabled.png new file mode 100644 index 0000000..8eac738 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_delete_disabled.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_error.png b/Client2021/content/textures/AnimationEditor/icon_error.png new file mode 100644 index 0000000..fe8e0de Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_error.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_hierarchy_end_white.png b/Client2021/content/textures/AnimationEditor/icon_hierarchy_end_white.png new file mode 100644 index 0000000..4b9fc99 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_hierarchy_end_white.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_keyIndicator.png b/Client2021/content/textures/AnimationEditor/icon_keyIndicator.png new file mode 100644 index 0000000..9d92ac4 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_keyIndicator.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_keyIndicator_selected.png b/Client2021/content/textures/AnimationEditor/icon_keyIndicator_selected.png new file mode 100644 index 0000000..ed16d3c Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_keyIndicator_selected.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_pin.png b/Client2021/content/textures/AnimationEditor/icon_pin.png new file mode 100644 index 0000000..523ed1e Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_pin.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_showmore.png b/Client2021/content/textures/AnimationEditor/icon_showmore.png new file mode 100644 index 0000000..0ecc4ad Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_showmore.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_warning.png b/Client2021/content/textures/AnimationEditor/icon_warning.png new file mode 100644 index 0000000..a563e29 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_warning.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_warning_ik.png b/Client2021/content/textures/AnimationEditor/icon_warning_ik.png new file mode 100644 index 0000000..67c98b2 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_warning_ik.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_whitetriangle_down.png b/Client2021/content/textures/AnimationEditor/icon_whitetriangle_down.png new file mode 100644 index 0000000..09c5db8 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_whitetriangle_down.png differ diff --git a/Client2021/content/textures/AnimationEditor/icon_whitetriangle_up.png b/Client2021/content/textures/AnimationEditor/icon_whitetriangle_up.png new file mode 100644 index 0000000..5bdc664 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/icon_whitetriangle_up.png differ diff --git a/Client2021/content/textures/AnimationEditor/image_keyframe_bounce_selected.png b/Client2021/content/textures/AnimationEditor/image_keyframe_bounce_selected.png new file mode 100644 index 0000000..6e8863f Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/image_keyframe_bounce_selected.png differ diff --git a/Client2021/content/textures/AnimationEditor/image_keyframe_bounce_unselected.png b/Client2021/content/textures/AnimationEditor/image_keyframe_bounce_unselected.png new file mode 100644 index 0000000..391c175 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/image_keyframe_bounce_unselected.png differ diff --git a/Client2021/content/textures/AnimationEditor/image_keyframe_constant_selected.png b/Client2021/content/textures/AnimationEditor/image_keyframe_constant_selected.png new file mode 100644 index 0000000..5d68420 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/image_keyframe_constant_selected.png differ diff --git a/Client2021/content/textures/AnimationEditor/image_keyframe_constant_unselected.png b/Client2021/content/textures/AnimationEditor/image_keyframe_constant_unselected.png new file mode 100644 index 0000000..1c976e6 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/image_keyframe_constant_unselected.png differ diff --git a/Client2021/content/textures/AnimationEditor/image_keyframe_cubic_selected.png b/Client2021/content/textures/AnimationEditor/image_keyframe_cubic_selected.png new file mode 100644 index 0000000..67a63a0 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/image_keyframe_cubic_selected.png differ diff --git a/Client2021/content/textures/AnimationEditor/image_keyframe_cubic_unselected.png b/Client2021/content/textures/AnimationEditor/image_keyframe_cubic_unselected.png new file mode 100644 index 0000000..2a9d7db Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/image_keyframe_cubic_unselected.png differ diff --git a/Client2021/content/textures/AnimationEditor/image_keyframe_elastic_selected.png b/Client2021/content/textures/AnimationEditor/image_keyframe_elastic_selected.png new file mode 100644 index 0000000..7216182 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/image_keyframe_elastic_selected.png differ diff --git a/Client2021/content/textures/AnimationEditor/image_keyframe_elastic_unselected.png b/Client2021/content/textures/AnimationEditor/image_keyframe_elastic_unselected.png new file mode 100644 index 0000000..e56ad88 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/image_keyframe_elastic_unselected.png differ diff --git a/Client2021/content/textures/AnimationEditor/image_keyframe_linear_selected.png b/Client2021/content/textures/AnimationEditor/image_keyframe_linear_selected.png new file mode 100644 index 0000000..3d4d969 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/image_keyframe_linear_selected.png differ diff --git a/Client2021/content/textures/AnimationEditor/image_keyframe_linear_unselected.png b/Client2021/content/textures/AnimationEditor/image_keyframe_linear_unselected.png new file mode 100644 index 0000000..3f3709c Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/image_keyframe_linear_unselected.png differ diff --git a/Client2021/content/textures/AnimationEditor/image_scrollbar_vertical_bot.png b/Client2021/content/textures/AnimationEditor/image_scrollbar_vertical_bot.png new file mode 100644 index 0000000..b419435 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/image_scrollbar_vertical_bot.png differ diff --git a/Client2021/content/textures/AnimationEditor/image_scrollbar_vertical_mid.png b/Client2021/content/textures/AnimationEditor/image_scrollbar_vertical_mid.png new file mode 100644 index 0000000..56558a3 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/image_scrollbar_vertical_mid.png differ diff --git a/Client2021/content/textures/AnimationEditor/image_scrollbar_vertical_top.png b/Client2021/content/textures/AnimationEditor/image_scrollbar_vertical_top.png new file mode 100644 index 0000000..e0437c5 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/image_scrollbar_vertical_top.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_dark_scalebar_arrows.png b/Client2021/content/textures/AnimationEditor/img_dark_scalebar_arrows.png new file mode 100644 index 0000000..a5b5527 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_dark_scalebar_arrows.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_dark_scalebar_bar.png b/Client2021/content/textures/AnimationEditor/img_dark_scalebar_bar.png new file mode 100644 index 0000000..b5627c7 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_dark_scalebar_bar.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_dark_scrubberhead.png b/Client2021/content/textures/AnimationEditor/img_dark_scrubberhead.png new file mode 100644 index 0000000..1d4abcb Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_dark_scrubberhead.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_dark_timetag_bg.png b/Client2021/content/textures/AnimationEditor/img_dark_timetag_bg.png new file mode 100644 index 0000000..f4b0ecb Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_dark_timetag_bg.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_eventGroupMarker_border.png b/Client2021/content/textures/AnimationEditor/img_eventGroupMarker_border.png new file mode 100644 index 0000000..c782465 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_eventGroupMarker_border.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_eventGroupMarker_border_selected.png b/Client2021/content/textures/AnimationEditor/img_eventGroupMarker_border_selected.png new file mode 100644 index 0000000..1bbc18b Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_eventGroupMarker_border_selected.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_eventGroupMarker_inner.png b/Client2021/content/textures/AnimationEditor/img_eventGroupMarker_inner.png new file mode 100644 index 0000000..c600b0c Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_eventGroupMarker_inner.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_eventMarker_border.png b/Client2021/content/textures/AnimationEditor/img_eventMarker_border.png new file mode 100644 index 0000000..af75451 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_eventMarker_border.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_eventMarker_border_selected.png b/Client2021/content/textures/AnimationEditor/img_eventMarker_border_selected.png new file mode 100644 index 0000000..ed30a94 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_eventMarker_border_selected.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_eventMarker_inner.png b/Client2021/content/textures/AnimationEditor/img_eventMarker_inner.png new file mode 100644 index 0000000..b20aa90 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_eventMarker_inner.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_eventMarker_min.png b/Client2021/content/textures/AnimationEditor/img_eventMarker_min.png new file mode 100644 index 0000000..da504be Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_eventMarker_min.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_forwardslash.png b/Client2021/content/textures/AnimationEditor/img_forwardslash.png new file mode 100644 index 0000000..ebe9092 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_forwardslash.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_key_border.png b/Client2021/content/textures/AnimationEditor/img_key_border.png new file mode 100644 index 0000000..bac1fe3 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_key_border.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_key_indicator_border.png b/Client2021/content/textures/AnimationEditor/img_key_indicator_border.png new file mode 100644 index 0000000..bb35719 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_key_indicator_border.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_key_indicator_inner.png b/Client2021/content/textures/AnimationEditor/img_key_indicator_inner.png new file mode 100644 index 0000000..10b29f5 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_key_indicator_inner.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_key_indicator_selected_border.png b/Client2021/content/textures/AnimationEditor/img_key_indicator_selected_border.png new file mode 100644 index 0000000..309841e Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_key_indicator_selected_border.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_key_indicator_selected_inner.png b/Client2021/content/textures/AnimationEditor/img_key_indicator_selected_inner.png new file mode 100644 index 0000000..10b29f5 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_key_indicator_selected_inner.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_key_inner.png b/Client2021/content/textures/AnimationEditor/img_key_inner.png new file mode 100644 index 0000000..c281f85 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_key_inner.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_key_selected_border.png b/Client2021/content/textures/AnimationEditor/img_key_selected_border.png new file mode 100644 index 0000000..84868ec Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_key_selected_border.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_key_selected_inner.png b/Client2021/content/textures/AnimationEditor/img_key_selected_inner.png new file mode 100644 index 0000000..03fd465 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_key_selected_inner.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_scalebar_arrows.png b/Client2021/content/textures/AnimationEditor/img_scalebar_arrows.png new file mode 100644 index 0000000..92c86ae Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_scalebar_arrows.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_scalebar_arrows_border.png b/Client2021/content/textures/AnimationEditor/img_scalebar_arrows_border.png new file mode 100644 index 0000000..33af34a Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_scalebar_arrows_border.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_scrubberhead.png b/Client2021/content/textures/AnimationEditor/img_scrubberhead.png new file mode 100644 index 0000000..8bed057 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_scrubberhead.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_timetag.png b/Client2021/content/textures/AnimationEditor/img_timetag.png new file mode 100644 index 0000000..99a0999 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_timetag.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_timetag_border.png b/Client2021/content/textures/AnimationEditor/img_timetag_border.png new file mode 100644 index 0000000..49a59e7 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_timetag_border.png differ diff --git a/Client2021/content/textures/AnimationEditor/img_triangle.png b/Client2021/content/textures/AnimationEditor/img_triangle.png new file mode 100644 index 0000000..1516c70 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/img_triangle.png differ diff --git a/Client2021/content/textures/AnimationEditor/menu_shadow_bottom.png b/Client2021/content/textures/AnimationEditor/menu_shadow_bottom.png new file mode 100644 index 0000000..64c5ca8 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/menu_shadow_bottom.png differ diff --git a/Client2021/content/textures/AnimationEditor/menu_shadow_side_left.png b/Client2021/content/textures/AnimationEditor/menu_shadow_side_left.png new file mode 100644 index 0000000..3e1e14b Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/menu_shadow_side_left.png differ diff --git a/Client2021/content/textures/AnimationEditor/menu_shadow_side_right.png b/Client2021/content/textures/AnimationEditor/menu_shadow_side_right.png new file mode 100644 index 0000000..dc3a41f Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/menu_shadow_side_right.png differ diff --git a/Client2021/content/textures/AnimationEditor/menu_shadow_top.png b/Client2021/content/textures/AnimationEditor/menu_shadow_top.png new file mode 100644 index 0000000..47003ca Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/menu_shadow_top.png differ diff --git a/Client2021/content/textures/AnimationEditor/rig_builder_32x32.png b/Client2021/content/textures/AnimationEditor/rig_builder_32x32.png new file mode 100644 index 0000000..336e4d4 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/rig_builder_32x32.png differ diff --git a/Client2021/content/textures/AnimationEditor/rigbuilder_blue.png b/Client2021/content/textures/AnimationEditor/rigbuilder_blue.png new file mode 100644 index 0000000..a0d7101 Binary files /dev/null and b/Client2021/content/textures/AnimationEditor/rigbuilder_blue.png differ diff --git a/Client2021/content/textures/ArrowCursor.png b/Client2021/content/textures/ArrowCursor.png new file mode 100644 index 0000000..694c26a Binary files /dev/null and b/Client2021/content/textures/ArrowCursor.png differ diff --git a/Client2021/content/textures/ArrowCursorDecalDrag.png b/Client2021/content/textures/ArrowCursorDecalDrag.png new file mode 100644 index 0000000..694c26a Binary files /dev/null and b/Client2021/content/textures/ArrowCursorDecalDrag.png differ diff --git a/Client2021/content/textures/ArrowFarCursor.png b/Client2021/content/textures/ArrowFarCursor.png new file mode 100644 index 0000000..daf5471 Binary files /dev/null and b/Client2021/content/textures/ArrowFarCursor.png differ diff --git a/Client2021/content/textures/AssetManager/explorer.png b/Client2021/content/textures/AssetManager/explorer.png new file mode 100644 index 0000000..6ecc0dc Binary files /dev/null and b/Client2021/content/textures/AssetManager/explorer.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/AvatarEditor.png b/Client2021/content/textures/AvatarEditorImages/AvatarEditor.png new file mode 100644 index 0000000..373a4bb Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/AvatarEditor.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/AvatarEditor_LightTheme.png b/Client2021/content/textures/AvatarEditorImages/AvatarEditor_LightTheme.png new file mode 100644 index 0000000..5982dce Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/AvatarEditor_LightTheme.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Catalog.png b/Client2021/content/textures/AvatarEditorImages/Catalog.png new file mode 100644 index 0000000..8b75001 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Catalog.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Catalog_LightTheme.png b/Client2021/content/textures/AvatarEditorImages/Catalog_LightTheme.png new file mode 100644 index 0000000..0405fe7 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Catalog_LightTheme.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/DarkPixel.png b/Client2021/content/textures/AvatarEditorImages/DarkPixel.png new file mode 100644 index 0000000..e562476 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/DarkPixel.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/LightPixel.png b/Client2021/content/textures/AvatarEditorImages/LightPixel.png new file mode 100644 index 0000000..e9a7e58 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/LightPixel.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Sheet.png b/Client2021/content/textures/AvatarEditorImages/Sheet.png new file mode 100644 index 0000000..efb94f4 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Sheet.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Sliders/body-type-slider-background.png b/Client2021/content/textures/AvatarEditorImages/Sliders/body-type-slider-background.png new file mode 100644 index 0000000..a9801aa Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Sliders/body-type-slider-background.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Sliders/body-type-slider-background@2x.png b/Client2021/content/textures/AvatarEditorImages/Sliders/body-type-slider-background@2x.png new file mode 100644 index 0000000..bc1e9d4 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Sliders/body-type-slider-background@2x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Sliders/body-type-slider-background@3x.png b/Client2021/content/textures/AvatarEditorImages/Sliders/body-type-slider-background@3x.png new file mode 100644 index 0000000..fd27267 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Sliders/body-type-slider-background@3x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty.png b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty.png new file mode 100644 index 0000000..cc1a2a5 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@2x.png b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@2x.png new file mode 100644 index 0000000..b9fd04b Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@2x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@3x.png b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@3x.png new file mode 100644 index 0000000..7bfe852 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-empty@3x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill.png b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill.png new file mode 100644 index 0000000..bc50d10 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@2x.png b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@2x.png new file mode 100644 index 0000000..30c1c22 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@2x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@3x.png b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@3x.png new file mode 100644 index 0000000..5f4ab0f Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slide-bar-fill@3x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slider.png b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slider.png new file mode 100644 index 0000000..12022f4 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slider.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slider@2x.png b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slider@2x.png new file mode 100644 index 0000000..1ae093c Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slider@2x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slider@3x.png b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slider@3x.png new file mode 100644 index 0000000..6d6ef28 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Sliders/gr-slider@3x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Stretch/bar-empty-mid.png b/Client2021/content/textures/AvatarEditorImages/Stretch/bar-empty-mid.png new file mode 100644 index 0000000..c2c751e Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Stretch/bar-empty-mid.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@2x.png b/Client2021/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@2x.png new file mode 100644 index 0000000..32d55f8 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@2x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@3x.png b/Client2021/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@3x.png new file mode 100644 index 0000000..de31bc7 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Stretch/bar-empty-mid@3x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Stretch/bar-full-mid.png b/Client2021/content/textures/AvatarEditorImages/Stretch/bar-full-mid.png new file mode 100644 index 0000000..5990100 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Stretch/bar-full-mid.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Stretch/bar-full-mid@2x.png b/Client2021/content/textures/AvatarEditorImages/Stretch/bar-full-mid@2x.png new file mode 100644 index 0000000..a4f9403 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Stretch/bar-full-mid@2x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Stretch/bar-full-mid@3x.png b/Client2021/content/textures/AvatarEditorImages/Stretch/bar-full-mid@3x.png new file mode 100644 index 0000000..eeb9e7e Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Stretch/bar-full-mid@3x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Stretch/gr-tail.png b/Client2021/content/textures/AvatarEditorImages/Stretch/gr-tail.png new file mode 100644 index 0000000..deb2988 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Stretch/gr-tail.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/Stretch/gr-tail@2x.png b/Client2021/content/textures/AvatarEditorImages/Stretch/gr-tail@2x.png new file mode 100644 index 0000000..1a88af0 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/Stretch/gr-tail@2x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/circle_blue.png b/Client2021/content/textures/AvatarEditorImages/circle_blue.png new file mode 100644 index 0000000..63ced88 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/circle_blue.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/circle_blue@2x.png b/Client2021/content/textures/AvatarEditorImages/circle_blue@2x.png new file mode 100644 index 0000000..3fb53a6 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/circle_blue@2x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/circle_blue@3x.png b/Client2021/content/textures/AvatarEditorImages/circle_blue@3x.png new file mode 100644 index 0000000..5cc69fa Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/circle_blue@3x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/circle_gray4.png b/Client2021/content/textures/AvatarEditorImages/circle_gray4.png new file mode 100644 index 0000000..5043d1d Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/circle_gray4.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/circle_gray4@2x.png b/Client2021/content/textures/AvatarEditorImages/circle_gray4@2x.png new file mode 100644 index 0000000..fba16ba Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/circle_gray4@2x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/circle_gray4@3x.png b/Client2021/content/textures/AvatarEditorImages/circle_gray4@3x.png new file mode 100644 index 0000000..29e91f3 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/circle_gray4@3x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/gr-selection-border.png b/Client2021/content/textures/AvatarEditorImages/gr-selection-border.png new file mode 100644 index 0000000..95d8647 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/gr-selection-border.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/gr-selection-border@2x.png b/Client2021/content/textures/AvatarEditorImages/gr-selection-border@2x.png new file mode 100644 index 0000000..1541ea9 Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/gr-selection-border@2x.png differ diff --git a/Client2021/content/textures/AvatarEditorImages/gr-selection-border@3x.png b/Client2021/content/textures/AvatarEditorImages/gr-selection-border@3x.png new file mode 100644 index 0000000..a7d560e Binary files /dev/null and b/Client2021/content/textures/AvatarEditorImages/gr-selection-border@3x.png differ diff --git a/Client2021/content/textures/AvatarImporter/button_avatarType.png b/Client2021/content/textures/AvatarImporter/button_avatarType.png new file mode 100644 index 0000000..88581b2 Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/button_avatarType.png differ diff --git a/Client2021/content/textures/AvatarImporter/button_avatarType_border.png b/Client2021/content/textures/AvatarImporter/button_avatarType_border.png new file mode 100644 index 0000000..6341f4a Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/button_avatarType_border.png differ diff --git a/Client2021/content/textures/AvatarImporter/button_close.png b/Client2021/content/textures/AvatarImporter/button_close.png new file mode 100644 index 0000000..bc0da39 Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/button_close.png differ diff --git a/Client2021/content/textures/AvatarImporter/fbximportlogo.png b/Client2021/content/textures/AvatarImporter/fbximportlogo.png new file mode 100644 index 0000000..6acee80 Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/fbximportlogo.png differ diff --git a/Client2021/content/textures/AvatarImporter/icon_AvatarImporter.png b/Client2021/content/textures/AvatarImporter/icon_AvatarImporter.png new file mode 100644 index 0000000..924f3c9 Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/icon_AvatarImporter.png differ diff --git a/Client2021/content/textures/AvatarImporter/icon_error.png b/Client2021/content/textures/AvatarImporter/icon_error.png new file mode 100644 index 0000000..db2872f Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/icon_error.png differ diff --git a/Client2021/content/textures/AvatarImporter/img_dark_R15.png b/Client2021/content/textures/AvatarImporter/img_dark_R15.png new file mode 100644 index 0000000..9939615 Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/img_dark_R15.png differ diff --git a/Client2021/content/textures/AvatarImporter/img_dark_Rthro.png b/Client2021/content/textures/AvatarImporter/img_dark_Rthro.png new file mode 100644 index 0000000..227c53d Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/img_dark_Rthro.png differ diff --git a/Client2021/content/textures/AvatarImporter/img_dark_RthroNarrow.png b/Client2021/content/textures/AvatarImporter/img_dark_RthroNarrow.png new file mode 100644 index 0000000..f8af124 Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/img_dark_RthroNarrow.png differ diff --git a/Client2021/content/textures/AvatarImporter/img_dark_custom.png b/Client2021/content/textures/AvatarImporter/img_dark_custom.png new file mode 100644 index 0000000..558eccb Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/img_dark_custom.png differ diff --git a/Client2021/content/textures/AvatarImporter/img_light_R15.png b/Client2021/content/textures/AvatarImporter/img_light_R15.png new file mode 100644 index 0000000..b6b0657 Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/img_light_R15.png differ diff --git a/Client2021/content/textures/AvatarImporter/img_light_Rthro.png b/Client2021/content/textures/AvatarImporter/img_light_Rthro.png new file mode 100644 index 0000000..5ca69d2 Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/img_light_Rthro.png differ diff --git a/Client2021/content/textures/AvatarImporter/img_light_RthroNarrow.png b/Client2021/content/textures/AvatarImporter/img_light_RthroNarrow.png new file mode 100644 index 0000000..3aa1643 Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/img_light_RthroNarrow.png differ diff --git a/Client2021/content/textures/AvatarImporter/img_light_custom.png b/Client2021/content/textures/AvatarImporter/img_light_custom.png new file mode 100644 index 0000000..d43765e Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/img_light_custom.png differ diff --git a/Client2021/content/textures/AvatarImporter/img_window_BG.png b/Client2021/content/textures/AvatarImporter/img_window_BG.png new file mode 100644 index 0000000..afae8bb Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/img_window_BG.png differ diff --git a/Client2021/content/textures/AvatarImporter/img_window_header.png b/Client2021/content/textures/AvatarImporter/img_window_header.png new file mode 100644 index 0000000..eda1219 Binary files /dev/null and b/Client2021/content/textures/AvatarImporter/img_window_header.png differ diff --git a/Client2021/content/textures/Blank.png b/Client2021/content/textures/Blank.png new file mode 100644 index 0000000..3add73f Binary files /dev/null and b/Client2021/content/textures/Blank.png differ diff --git a/Client2021/content/textures/ClassImages.PNG b/Client2021/content/textures/ClassImages.PNG new file mode 100644 index 0000000..3ed3274 Binary files /dev/null and b/Client2021/content/textures/ClassImages.PNG differ diff --git a/Client2021/content/textures/CollisionGroupsEditor/ToolbarIcon.png b/Client2021/content/textures/CollisionGroupsEditor/ToolbarIcon.png new file mode 100644 index 0000000..08542c9 Binary files /dev/null and b/Client2021/content/textures/CollisionGroupsEditor/ToolbarIcon.png differ diff --git a/Client2021/content/textures/CollisionGroupsEditor/assign-hover.png b/Client2021/content/textures/CollisionGroupsEditor/assign-hover.png new file mode 100644 index 0000000..91f7db7 Binary files /dev/null and b/Client2021/content/textures/CollisionGroupsEditor/assign-hover.png differ diff --git a/Client2021/content/textures/CollisionGroupsEditor/assign.png b/Client2021/content/textures/CollisionGroupsEditor/assign.png new file mode 100644 index 0000000..fc915f6 Binary files /dev/null and b/Client2021/content/textures/CollisionGroupsEditor/assign.png differ diff --git a/Client2021/content/textures/CollisionGroupsEditor/checked-bluebg.png b/Client2021/content/textures/CollisionGroupsEditor/checked-bluebg.png new file mode 100644 index 0000000..3dddcfb Binary files /dev/null and b/Client2021/content/textures/CollisionGroupsEditor/checked-bluebg.png differ diff --git a/Client2021/content/textures/CollisionGroupsEditor/checked-whitebg.png b/Client2021/content/textures/CollisionGroupsEditor/checked-whitebg.png new file mode 100644 index 0000000..78bdb73 Binary files /dev/null and b/Client2021/content/textures/CollisionGroupsEditor/checked-whitebg.png differ diff --git a/Client2021/content/textures/CollisionGroupsEditor/delete-hover.png b/Client2021/content/textures/CollisionGroupsEditor/delete-hover.png new file mode 100644 index 0000000..a0b6401 Binary files /dev/null and b/Client2021/content/textures/CollisionGroupsEditor/delete-hover.png differ diff --git a/Client2021/content/textures/CollisionGroupsEditor/delete.png b/Client2021/content/textures/CollisionGroupsEditor/delete.png new file mode 100644 index 0000000..9437121 Binary files /dev/null and b/Client2021/content/textures/CollisionGroupsEditor/delete.png differ diff --git a/Client2021/content/textures/CollisionGroupsEditor/manage-hover.png b/Client2021/content/textures/CollisionGroupsEditor/manage-hover.png new file mode 100644 index 0000000..273f99e Binary files /dev/null and b/Client2021/content/textures/CollisionGroupsEditor/manage-hover.png differ diff --git a/Client2021/content/textures/CollisionGroupsEditor/manage.png b/Client2021/content/textures/CollisionGroupsEditor/manage.png new file mode 100644 index 0000000..81c6517 Binary files /dev/null and b/Client2021/content/textures/CollisionGroupsEditor/manage.png differ diff --git a/Client2021/content/textures/CollisionGroupsEditor/rename-hover.png b/Client2021/content/textures/CollisionGroupsEditor/rename-hover.png new file mode 100644 index 0000000..df47a4b Binary files /dev/null and b/Client2021/content/textures/CollisionGroupsEditor/rename-hover.png differ diff --git a/Client2021/content/textures/CollisionGroupsEditor/rename.png b/Client2021/content/textures/CollisionGroupsEditor/rename.png new file mode 100644 index 0000000..bd26275 Binary files /dev/null and b/Client2021/content/textures/CollisionGroupsEditor/rename.png differ diff --git a/Client2021/content/textures/CollisionGroupsEditor/unchecked.png b/Client2021/content/textures/CollisionGroupsEditor/unchecked.png new file mode 100644 index 0000000..dd8aa02 Binary files /dev/null and b/Client2021/content/textures/CollisionGroupsEditor/unchecked.png differ diff --git a/Client2021/content/textures/ConstraintCursor.png b/Client2021/content/textures/ConstraintCursor.png new file mode 100644 index 0000000..a89d0ad Binary files /dev/null and b/Client2021/content/textures/ConstraintCursor.png differ diff --git a/Client2021/content/textures/Cursors/CrossMouseIcon.png b/Client2021/content/textures/Cursors/CrossMouseIcon.png new file mode 100644 index 0000000..a18529c Binary files /dev/null and b/Client2021/content/textures/Cursors/CrossMouseIcon.png differ diff --git a/Client2021/content/textures/Cursors/Gamepad/Pointer.png b/Client2021/content/textures/Cursors/Gamepad/Pointer.png new file mode 100644 index 0000000..5c406dc Binary files /dev/null and b/Client2021/content/textures/Cursors/Gamepad/Pointer.png differ diff --git a/Client2021/content/textures/Cursors/Gamepad/Pointer@2x.png b/Client2021/content/textures/Cursors/Gamepad/Pointer@2x.png new file mode 100644 index 0000000..e896fbb Binary files /dev/null and b/Client2021/content/textures/Cursors/Gamepad/Pointer@2x.png differ diff --git a/Client2021/content/textures/Cursors/Gamepad/PointerOver.png b/Client2021/content/textures/Cursors/Gamepad/PointerOver.png new file mode 100644 index 0000000..7663ea1 Binary files /dev/null and b/Client2021/content/textures/Cursors/Gamepad/PointerOver.png differ diff --git a/Client2021/content/textures/Cursors/Gamepad/PointerOver@2x.png b/Client2021/content/textures/Cursors/Gamepad/PointerOver@2x.png new file mode 100644 index 0000000..e0dbd85 Binary files /dev/null and b/Client2021/content/textures/Cursors/Gamepad/PointerOver@2x.png differ diff --git a/Client2021/content/textures/Cursors/mouseIconCameraTrack.png b/Client2021/content/textures/Cursors/mouseIconCameraTrack.png new file mode 100644 index 0000000..7056866 Binary files /dev/null and b/Client2021/content/textures/Cursors/mouseIconCameraTrack.png differ diff --git a/Client2021/content/textures/DarkThemeLoadingCircle.png b/Client2021/content/textures/DarkThemeLoadingCircle.png new file mode 100644 index 0000000..00b086b Binary files /dev/null and b/Client2021/content/textures/DarkThemeLoadingCircle.png differ diff --git a/Client2021/content/textures/DevConsole/Arrow.png b/Client2021/content/textures/DevConsole/Arrow.png new file mode 100644 index 0000000..f55bbff Binary files /dev/null and b/Client2021/content/textures/DevConsole/Arrow.png differ diff --git a/Client2021/content/textures/DevConsole/Clear.png b/Client2021/content/textures/DevConsole/Clear.png new file mode 100644 index 0000000..9f1bf3a Binary files /dev/null and b/Client2021/content/textures/DevConsole/Clear.png differ diff --git a/Client2021/content/textures/DevConsole/Close.png b/Client2021/content/textures/DevConsole/Close.png new file mode 100644 index 0000000..31d348a Binary files /dev/null and b/Client2021/content/textures/DevConsole/Close.png differ diff --git a/Client2021/content/textures/DevConsole/Error.png b/Client2021/content/textures/DevConsole/Error.png new file mode 100644 index 0000000..470b6e5 Binary files /dev/null and b/Client2021/content/textures/DevConsole/Error.png differ diff --git a/Client2021/content/textures/DevConsole/Filter-filled.png b/Client2021/content/textures/DevConsole/Filter-filled.png new file mode 100644 index 0000000..1f43780 Binary files /dev/null and b/Client2021/content/textures/DevConsole/Filter-filled.png differ diff --git a/Client2021/content/textures/DevConsole/Filter-stroke.png b/Client2021/content/textures/DevConsole/Filter-stroke.png new file mode 100644 index 0000000..6c00c0e Binary files /dev/null and b/Client2021/content/textures/DevConsole/Filter-stroke.png differ diff --git a/Client2021/content/textures/DevConsole/Info.png b/Client2021/content/textures/DevConsole/Info.png new file mode 100644 index 0000000..d6a13bb Binary files /dev/null and b/Client2021/content/textures/DevConsole/Info.png differ diff --git a/Client2021/content/textures/DevConsole/Maximize.png b/Client2021/content/textures/DevConsole/Maximize.png new file mode 100644 index 0000000..8016be6 Binary files /dev/null and b/Client2021/content/textures/DevConsole/Maximize.png differ diff --git a/Client2021/content/textures/DevConsole/Minimize.png b/Client2021/content/textures/DevConsole/Minimize.png new file mode 100644 index 0000000..0065af8 Binary files /dev/null and b/Client2021/content/textures/DevConsole/Minimize.png differ diff --git a/Client2021/content/textures/DevConsole/Search.png b/Client2021/content/textures/DevConsole/Search.png new file mode 100644 index 0000000..b9c95cb Binary files /dev/null and b/Client2021/content/textures/DevConsole/Search.png differ diff --git a/Client2021/content/textures/DevConsole/Sort.png b/Client2021/content/textures/DevConsole/Sort.png new file mode 100644 index 0000000..152394d Binary files /dev/null and b/Client2021/content/textures/DevConsole/Sort.png differ diff --git a/Client2021/content/textures/DevConsole/Warning.png b/Client2021/content/textures/DevConsole/Warning.png new file mode 100644 index 0000000..d614a50 Binary files /dev/null and b/Client2021/content/textures/DevConsole/Warning.png differ diff --git a/Client2021/content/textures/DeveloperFramework/AssetPreview/close_button.png b/Client2021/content/textures/DeveloperFramework/AssetPreview/close_button.png new file mode 100644 index 0000000..05116cd Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/AssetPreview/close_button.png differ diff --git a/Client2021/content/textures/DeveloperFramework/AssetPreview/more.png b/Client2021/content/textures/DeveloperFramework/AssetPreview/more.png new file mode 100644 index 0000000..65abae2 Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/AssetPreview/more.png differ diff --git a/Client2021/content/textures/DeveloperFramework/AssetRender/hierarchy.png b/Client2021/content/textures/DeveloperFramework/AssetRender/hierarchy.png new file mode 100644 index 0000000..853f4e5 Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/AssetRender/hierarchy.png differ diff --git a/Client2021/content/textures/DeveloperFramework/AudioPlayer/audioPlay_BG.png b/Client2021/content/textures/DeveloperFramework/AudioPlayer/audioPlay_BG.png new file mode 100644 index 0000000..779664b Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/AudioPlayer/audioPlay_BG.png differ diff --git a/Client2021/content/textures/DeveloperFramework/Favorites/star_filled.png b/Client2021/content/textures/DeveloperFramework/Favorites/star_filled.png new file mode 100644 index 0000000..fbf2a35 Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/Favorites/star_filled.png differ diff --git a/Client2021/content/textures/DeveloperFramework/Favorites/star_stroke.png b/Client2021/content/textures/DeveloperFramework/Favorites/star_stroke.png new file mode 100644 index 0000000..366df74 Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/Favorites/star_stroke.png differ diff --git a/Client2021/content/textures/DeveloperFramework/MediaPlayerControls/pause_button.png b/Client2021/content/textures/DeveloperFramework/MediaPlayerControls/pause_button.png new file mode 100644 index 0000000..d6ba58e Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/MediaPlayerControls/pause_button.png differ diff --git a/Client2021/content/textures/DeveloperFramework/MediaPlayerControls/play_button.png b/Client2021/content/textures/DeveloperFramework/MediaPlayerControls/play_button.png new file mode 100644 index 0000000..a97bde8 Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/MediaPlayerControls/play_button.png differ diff --git a/Client2021/content/textures/DeveloperFramework/Votes/rating_small.png b/Client2021/content/textures/DeveloperFramework/Votes/rating_small.png new file mode 100644 index 0000000..4a3100d Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/Votes/rating_small.png differ diff --git a/Client2021/content/textures/DeveloperFramework/checkbox_checked_dark.png b/Client2021/content/textures/DeveloperFramework/checkbox_checked_dark.png new file mode 100644 index 0000000..68fae09 Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/checkbox_checked_dark.png differ diff --git a/Client2021/content/textures/DeveloperFramework/checkbox_checked_light.png b/Client2021/content/textures/DeveloperFramework/checkbox_checked_light.png new file mode 100644 index 0000000..d261816 Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/checkbox_checked_light.png differ diff --git a/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_dark.png b/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_dark.png new file mode 100644 index 0000000..9b12b6c Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_dark.png differ diff --git a/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_disabled_dark.png b/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_disabled_dark.png new file mode 100644 index 0000000..e11cdd1 Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_disabled_dark.png differ diff --git a/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_disabled_light.png b/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_disabled_light.png new file mode 100644 index 0000000..db7b9dd Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_disabled_light.png differ diff --git a/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_hover_dark.png b/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_hover_dark.png new file mode 100644 index 0000000..13b77c5 Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_hover_dark.png differ diff --git a/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_hover_light.png b/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_hover_light.png new file mode 100644 index 0000000..871dc3e Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_hover_light.png differ diff --git a/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_light.png b/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_light.png new file mode 100644 index 0000000..f131a1b Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/checkbox_unchecked_light.png differ diff --git a/Client2021/content/textures/DeveloperFramework/slider_bg.png b/Client2021/content/textures/DeveloperFramework/slider_bg.png new file mode 100644 index 0000000..ff6ff8e Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/slider_bg.png differ diff --git a/Client2021/content/textures/DeveloperFramework/slider_knob.png b/Client2021/content/textures/DeveloperFramework/slider_knob.png new file mode 100644 index 0000000..1098b7e Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/slider_knob.png differ diff --git a/Client2021/content/textures/DeveloperFramework/slider_knob_light.png b/Client2021/content/textures/DeveloperFramework/slider_knob_light.png new file mode 100644 index 0000000..94db6a2 Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/slider_knob_light.png differ diff --git a/Client2021/content/textures/DeveloperFramework/slider_knob_ouline.png b/Client2021/content/textures/DeveloperFramework/slider_knob_ouline.png new file mode 100644 index 0000000..9cced45 Binary files /dev/null and b/Client2021/content/textures/DeveloperFramework/slider_knob_ouline.png differ diff --git a/Client2021/content/textures/DeveloperInspector/ToolbarIcon.png b/Client2021/content/textures/DeveloperInspector/ToolbarIcon.png new file mode 100644 index 0000000..88633f5 Binary files /dev/null and b/Client2021/content/textures/DeveloperInspector/ToolbarIcon.png differ diff --git a/Client2021/content/textures/DraftsWidget/deletedSource.png b/Client2021/content/textures/DraftsWidget/deletedSource.png new file mode 100644 index 0000000..55e3e8b Binary files /dev/null and b/Client2021/content/textures/DraftsWidget/deletedSource.png differ diff --git a/Client2021/content/textures/DraftsWidget/newSource.png b/Client2021/content/textures/DraftsWidget/newSource.png new file mode 100644 index 0000000..a754f40 Binary files /dev/null and b/Client2021/content/textures/DraftsWidget/newSource.png differ diff --git a/Client2021/content/textures/FillCursor.png b/Client2021/content/textures/FillCursor.png new file mode 100644 index 0000000..752cebc Binary files /dev/null and b/Client2021/content/textures/FillCursor.png differ diff --git a/Client2021/content/textures/FlatCursor.png b/Client2021/content/textures/FlatCursor.png new file mode 100644 index 0000000..7d6e845 Binary files /dev/null and b/Client2021/content/textures/FlatCursor.png differ diff --git a/Client2021/content/textures/GameSettings/Arrow.png b/Client2021/content/textures/GameSettings/Arrow.png new file mode 100644 index 0000000..152394d Binary files /dev/null and b/Client2021/content/textures/GameSettings/Arrow.png differ diff --git a/Client2021/content/textures/GameSettings/ArrowLeft.png b/Client2021/content/textures/GameSettings/ArrowLeft.png new file mode 100644 index 0000000..dc1ffab Binary files /dev/null and b/Client2021/content/textures/GameSettings/ArrowLeft.png differ diff --git a/Client2021/content/textures/GameSettings/CenterPlus.png b/Client2021/content/textures/GameSettings/CenterPlus.png new file mode 100644 index 0000000..05af26f Binary files /dev/null and b/Client2021/content/textures/GameSettings/CenterPlus.png differ diff --git a/Client2021/content/textures/GameSettings/CheckedBoxDark.png b/Client2021/content/textures/GameSettings/CheckedBoxDark.png new file mode 100644 index 0000000..96f4a42 Binary files /dev/null and b/Client2021/content/textures/GameSettings/CheckedBoxDark.png differ diff --git a/Client2021/content/textures/GameSettings/CheckedBoxLight.png b/Client2021/content/textures/GameSettings/CheckedBoxLight.png new file mode 100644 index 0000000..ad084c4 Binary files /dev/null and b/Client2021/content/textures/GameSettings/CheckedBoxLight.png differ diff --git a/Client2021/content/textures/GameSettings/DottedBorder.png b/Client2021/content/textures/GameSettings/DottedBorder.png new file mode 100644 index 0000000..18ee0e3 Binary files /dev/null and b/Client2021/content/textures/GameSettings/DottedBorder.png differ diff --git a/Client2021/content/textures/GameSettings/DottedBorder_Square.png b/Client2021/content/textures/GameSettings/DottedBorder_Square.png new file mode 100644 index 0000000..97f5507 Binary files /dev/null and b/Client2021/content/textures/GameSettings/DottedBorder_Square.png differ diff --git a/Client2021/content/textures/GameSettings/Error.png b/Client2021/content/textures/GameSettings/Error.png new file mode 100644 index 0000000..701f9a7 Binary files /dev/null and b/Client2021/content/textures/GameSettings/Error.png differ diff --git a/Client2021/content/textures/GameSettings/ErrorIcon.png b/Client2021/content/textures/GameSettings/ErrorIcon.png new file mode 100644 index 0000000..e15c23e Binary files /dev/null and b/Client2021/content/textures/GameSettings/ErrorIcon.png differ diff --git a/Client2021/content/textures/GameSettings/Gradient-Border.png b/Client2021/content/textures/GameSettings/Gradient-Border.png new file mode 100644 index 0000000..7a5cca4 Binary files /dev/null and b/Client2021/content/textures/GameSettings/Gradient-Border.png differ diff --git a/Client2021/content/textures/GameSettings/ModeratedAsset.jpg b/Client2021/content/textures/GameSettings/ModeratedAsset.jpg new file mode 100644 index 0000000..40d646e Binary files /dev/null and b/Client2021/content/textures/GameSettings/ModeratedAsset.jpg differ diff --git a/Client2021/content/textures/GameSettings/RadioButton.png b/Client2021/content/textures/GameSettings/RadioButton.png new file mode 100644 index 0000000..0e6557d Binary files /dev/null and b/Client2021/content/textures/GameSettings/RadioButton.png differ diff --git a/Client2021/content/textures/GameSettings/RoundArrowButton.png b/Client2021/content/textures/GameSettings/RoundArrowButton.png new file mode 100644 index 0000000..fed6fa1 Binary files /dev/null and b/Client2021/content/textures/GameSettings/RoundArrowButton.png differ diff --git a/Client2021/content/textures/GameSettings/ScrollBarBottom.png b/Client2021/content/textures/GameSettings/ScrollBarBottom.png new file mode 100644 index 0000000..852c120 Binary files /dev/null and b/Client2021/content/textures/GameSettings/ScrollBarBottom.png differ diff --git a/Client2021/content/textures/GameSettings/ScrollBarBottom_Wide.png b/Client2021/content/textures/GameSettings/ScrollBarBottom_Wide.png new file mode 100644 index 0000000..dc04cd1 Binary files /dev/null and b/Client2021/content/textures/GameSettings/ScrollBarBottom_Wide.png differ diff --git a/Client2021/content/textures/GameSettings/ScrollBarMiddle.png b/Client2021/content/textures/GameSettings/ScrollBarMiddle.png new file mode 100644 index 0000000..c244b9e Binary files /dev/null and b/Client2021/content/textures/GameSettings/ScrollBarMiddle.png differ diff --git a/Client2021/content/textures/GameSettings/ScrollBarMiddle_Wide.png b/Client2021/content/textures/GameSettings/ScrollBarMiddle_Wide.png new file mode 100644 index 0000000..12b12e3 Binary files /dev/null and b/Client2021/content/textures/GameSettings/ScrollBarMiddle_Wide.png differ diff --git a/Client2021/content/textures/GameSettings/ScrollBarTop.png b/Client2021/content/textures/GameSettings/ScrollBarTop.png new file mode 100644 index 0000000..c0dadb9 Binary files /dev/null and b/Client2021/content/textures/GameSettings/ScrollBarTop.png differ diff --git a/Client2021/content/textures/GameSettings/ScrollBarTop_Wide.png b/Client2021/content/textures/GameSettings/ScrollBarTop_Wide.png new file mode 100644 index 0000000..210ed76 Binary files /dev/null and b/Client2021/content/textures/GameSettings/ScrollBarTop_Wide.png differ diff --git a/Client2021/content/textures/GameSettings/ToolbarIcon.png b/Client2021/content/textures/GameSettings/ToolbarIcon.png new file mode 100644 index 0000000..be1bedf Binary files /dev/null and b/Client2021/content/textures/GameSettings/ToolbarIcon.png differ diff --git a/Client2021/content/textures/GameSettings/UncheckedBox.png b/Client2021/content/textures/GameSettings/UncheckedBox.png new file mode 100644 index 0000000..60fab3d Binary files /dev/null and b/Client2021/content/textures/GameSettings/UncheckedBox.png differ diff --git a/Client2021/content/textures/GameSettings/Warning.png b/Client2021/content/textures/GameSettings/Warning.png new file mode 100644 index 0000000..6e5f9e6 Binary files /dev/null and b/Client2021/content/textures/GameSettings/Warning.png differ diff --git a/Client2021/content/textures/GameSettings/add.png b/Client2021/content/textures/GameSettings/add.png new file mode 100644 index 0000000..4e5eab4 Binary files /dev/null and b/Client2021/content/textures/GameSettings/add.png differ diff --git a/Client2021/content/textures/GameSettings/copy.png b/Client2021/content/textures/GameSettings/copy.png new file mode 100644 index 0000000..10cbc18 Binary files /dev/null and b/Client2021/content/textures/GameSettings/copy.png differ diff --git a/Client2021/content/textures/GameSettings/delete.PNG b/Client2021/content/textures/GameSettings/delete.PNG new file mode 100644 index 0000000..a2cde20 Binary files /dev/null and b/Client2021/content/textures/GameSettings/delete.PNG differ diff --git a/Client2021/content/textures/GameSettings/edit.png b/Client2021/content/textures/GameSettings/edit.png new file mode 100644 index 0000000..2a6fafd Binary files /dev/null and b/Client2021/content/textures/GameSettings/edit.png differ diff --git a/Client2021/content/textures/GameSettings/friendsIcon.png b/Client2021/content/textures/GameSettings/friendsIcon.png new file mode 100644 index 0000000..537c3a7 Binary files /dev/null and b/Client2021/content/textures/GameSettings/friendsIcon.png differ diff --git a/Client2021/content/textures/GameSettings/placeholder.png b/Client2021/content/textures/GameSettings/placeholder.png new file mode 100644 index 0000000..a2c2aaa Binary files /dev/null and b/Client2021/content/textures/GameSettings/placeholder.png differ diff --git a/Client2021/content/textures/GameSettings/search.png b/Client2021/content/textures/GameSettings/search.png new file mode 100644 index 0000000..4c606f7 Binary files /dev/null and b/Client2021/content/textures/GameSettings/search.png differ diff --git a/Client2021/content/textures/GameSettings/zoom.PNG b/Client2021/content/textures/GameSettings/zoom.PNG new file mode 100644 index 0000000..999a1b4 Binary files /dev/null and b/Client2021/content/textures/GameSettings/zoom.PNG differ diff --git a/Client2021/content/textures/GlueCursor.png b/Client2021/content/textures/GlueCursor.png new file mode 100644 index 0000000..fdba03c Binary files /dev/null and b/Client2021/content/textures/GlueCursor.png differ diff --git a/Client2021/content/textures/HingeCursor.png b/Client2021/content/textures/HingeCursor.png new file mode 100644 index 0000000..99700b1 Binary files /dev/null and b/Client2021/content/textures/HingeCursor.png differ diff --git a/Client2021/content/textures/Icon_Stream_Off.png b/Client2021/content/textures/Icon_Stream_Off.png new file mode 100644 index 0000000..bbda9be Binary files /dev/null and b/Client2021/content/textures/Icon_Stream_Off.png differ diff --git a/Client2021/content/textures/Icon_Stream_Off@2x.png b/Client2021/content/textures/Icon_Stream_Off@2x.png new file mode 100644 index 0000000..979ef9c Binary files /dev/null and b/Client2021/content/textures/Icon_Stream_Off@2x.png differ diff --git a/Client2021/content/textures/Icon_Stream_Off@3x.png b/Client2021/content/textures/Icon_Stream_Off@3x.png new file mode 100644 index 0000000..e365e43 Binary files /dev/null and b/Client2021/content/textures/Icon_Stream_Off@3x.png differ diff --git a/Client2021/content/textures/LightThemeLoadingCircle.png b/Client2021/content/textures/LightThemeLoadingCircle.png new file mode 100644 index 0000000..5ddc825 Binary files /dev/null and b/Client2021/content/textures/LightThemeLoadingCircle.png differ diff --git a/Client2021/content/textures/LockCursor.png b/Client2021/content/textures/LockCursor.png new file mode 100644 index 0000000..4520327 Binary files /dev/null and b/Client2021/content/textures/LockCursor.png differ diff --git a/Client2021/content/textures/MaterialCursor.png b/Client2021/content/textures/MaterialCursor.png new file mode 100644 index 0000000..f43fa99 Binary files /dev/null and b/Client2021/content/textures/MaterialCursor.png differ diff --git a/Client2021/content/textures/MorpherEditor/mainButtonIcon.png b/Client2021/content/textures/MorpherEditor/mainButtonIcon.png new file mode 100644 index 0000000..3b2f615 Binary files /dev/null and b/Client2021/content/textures/MorpherEditor/mainButtonIcon.png differ diff --git a/Client2021/content/textures/MotorCursor.png b/Client2021/content/textures/MotorCursor.png new file mode 100644 index 0000000..64efe3e Binary files /dev/null and b/Client2021/content/textures/MotorCursor.png differ diff --git a/Client2021/content/textures/MouseLockedCursor.png b/Client2021/content/textures/MouseLockedCursor.png new file mode 100644 index 0000000..2ab5527 Binary files /dev/null and b/Client2021/content/textures/MouseLockedCursor.png differ diff --git a/Client2021/content/textures/PluginManagement/allowed.png b/Client2021/content/textures/PluginManagement/allowed.png new file mode 100644 index 0000000..d59721f Binary files /dev/null and b/Client2021/content/textures/PluginManagement/allowed.png differ diff --git a/Client2021/content/textures/PluginManagement/back.png b/Client2021/content/textures/PluginManagement/back.png new file mode 100644 index 0000000..ef27a4d Binary files /dev/null and b/Client2021/content/textures/PluginManagement/back.png differ diff --git a/Client2021/content/textures/PluginManagement/checked_dark.png b/Client2021/content/textures/PluginManagement/checked_dark.png new file mode 100644 index 0000000..c91d830 Binary files /dev/null and b/Client2021/content/textures/PluginManagement/checked_dark.png differ diff --git a/Client2021/content/textures/PluginManagement/checked_light.png b/Client2021/content/textures/PluginManagement/checked_light.png new file mode 100644 index 0000000..92f6467 Binary files /dev/null and b/Client2021/content/textures/PluginManagement/checked_light.png differ diff --git a/Client2021/content/textures/PluginManagement/declined.png b/Client2021/content/textures/PluginManagement/declined.png new file mode 100644 index 0000000..2515f6e Binary files /dev/null and b/Client2021/content/textures/PluginManagement/declined.png differ diff --git a/Client2021/content/textures/PluginManagement/edit.png b/Client2021/content/textures/PluginManagement/edit.png new file mode 100644 index 0000000..afe2243 Binary files /dev/null and b/Client2021/content/textures/PluginManagement/edit.png differ diff --git a/Client2021/content/textures/PluginManagement/unchecked.png b/Client2021/content/textures/PluginManagement/unchecked.png new file mode 100644 index 0000000..0514209 Binary files /dev/null and b/Client2021/content/textures/PluginManagement/unchecked.png differ diff --git a/Client2021/content/textures/PublishPlaceAs/TransparentWhiteImagePlaceholder.png b/Client2021/content/textures/PublishPlaceAs/TransparentWhiteImagePlaceholder.png new file mode 100644 index 0000000..f22c728 Binary files /dev/null and b/Client2021/content/textures/PublishPlaceAs/TransparentWhiteImagePlaceholder.png differ diff --git a/Client2021/content/textures/PublishPlaceAs/WhiteNew.png b/Client2021/content/textures/PublishPlaceAs/WhiteNew.png new file mode 100644 index 0000000..1ac32e9 Binary files /dev/null and b/Client2021/content/textures/PublishPlaceAs/WhiteNew.png differ diff --git a/Client2021/content/textures/PublishPlaceAs/common_checkmarkCircle.png b/Client2021/content/textures/PublishPlaceAs/common_checkmarkCircle.png new file mode 100644 index 0000000..dbcf169 Binary files /dev/null and b/Client2021/content/textures/PublishPlaceAs/common_checkmarkCircle.png differ diff --git a/Client2021/content/textures/PublishPlaceAs/navigation_pushBack.png b/Client2021/content/textures/PublishPlaceAs/navigation_pushBack.png new file mode 100644 index 0000000..45a50d2 Binary files /dev/null and b/Client2021/content/textures/PublishPlaceAs/navigation_pushBack.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/button_checkbox_square.png b/Client2021/content/textures/RoactStudioWidgets/button_checkbox_square.png new file mode 100644 index 0000000..af883a9 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/button_checkbox_square.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/button_default.png b/Client2021/content/textures/RoactStudioWidgets/button_default.png new file mode 100644 index 0000000..28eb7a7 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/button_default.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/button_hover.png b/Client2021/content/textures/RoactStudioWidgets/button_hover.png new file mode 100644 index 0000000..5184f6d Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/button_hover.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/button_pressed.png b/Client2021/content/textures/RoactStudioWidgets/button_pressed.png new file mode 100644 index 0000000..5484114 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/button_pressed.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/button_radiobutton_chosen.png b/Client2021/content/textures/RoactStudioWidgets/button_radiobutton_chosen.png new file mode 100644 index 0000000..2b1bbec Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/button_radiobutton_chosen.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/button_radiobutton_default.png b/Client2021/content/textures/RoactStudioWidgets/button_radiobutton_default.png new file mode 100644 index 0000000..fbb0cff Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/button_radiobutton_default.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/checkbox_square.png b/Client2021/content/textures/RoactStudioWidgets/checkbox_square.png new file mode 100644 index 0000000..af883a9 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/checkbox_square.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/icon_tick.png b/Client2021/content/textures/RoactStudioWidgets/icon_tick.png new file mode 100644 index 0000000..997f788 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/icon_tick.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/slider_bar_background_dark.png b/Client2021/content/textures/RoactStudioWidgets/slider_bar_background_dark.png new file mode 100644 index 0000000..d5d0335 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/slider_bar_background_dark.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/slider_bar_background_light.png b/Client2021/content/textures/RoactStudioWidgets/slider_bar_background_light.png new file mode 100644 index 0000000..bf10bdb Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/slider_bar_background_light.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/slider_bar_dark.png b/Client2021/content/textures/RoactStudioWidgets/slider_bar_dark.png new file mode 100644 index 0000000..3e1c713 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/slider_bar_dark.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/slider_bar_light.png b/Client2021/content/textures/RoactStudioWidgets/slider_bar_light.png new file mode 100644 index 0000000..bc50d10 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/slider_bar_light.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/slider_caret.png b/Client2021/content/textures/RoactStudioWidgets/slider_caret.png new file mode 100644 index 0000000..5f5e4b1 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/slider_caret.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/slider_caret_disabled.png b/Client2021/content/textures/RoactStudioWidgets/slider_caret_disabled.png new file mode 100644 index 0000000..5f5e4b1 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/slider_caret_disabled.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/slider_handle_dark.png b/Client2021/content/textures/RoactStudioWidgets/slider_handle_dark.png new file mode 100644 index 0000000..00dba99 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/slider_handle_dark.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/slider_handle_light.png b/Client2021/content/textures/RoactStudioWidgets/slider_handle_light.png new file mode 100644 index 0000000..49a1913 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/slider_handle_light.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/toggle_disable_dark.png b/Client2021/content/textures/RoactStudioWidgets/toggle_disable_dark.png new file mode 100644 index 0000000..ba3f314 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/toggle_disable_dark.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/toggle_disable_light.png b/Client2021/content/textures/RoactStudioWidgets/toggle_disable_light.png new file mode 100644 index 0000000..8b68fa7 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/toggle_disable_light.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/toggle_off_dark.png b/Client2021/content/textures/RoactStudioWidgets/toggle_off_dark.png new file mode 100644 index 0000000..88d1e15 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/toggle_off_dark.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/toggle_off_light.png b/Client2021/content/textures/RoactStudioWidgets/toggle_off_light.png new file mode 100644 index 0000000..a359e38 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/toggle_off_light.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/toggle_on_dark.png b/Client2021/content/textures/RoactStudioWidgets/toggle_on_dark.png new file mode 100644 index 0000000..9ac71b1 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/toggle_on_dark.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/toggle_on_disable_dark.png b/Client2021/content/textures/RoactStudioWidgets/toggle_on_disable_dark.png new file mode 100644 index 0000000..03d4e80 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/toggle_on_disable_dark.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/toggle_on_disable_light.png b/Client2021/content/textures/RoactStudioWidgets/toggle_on_disable_light.png new file mode 100644 index 0000000..e214d74 Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/toggle_on_disable_light.png differ diff --git a/Client2021/content/textures/RoactStudioWidgets/toggle_on_light.png b/Client2021/content/textures/RoactStudioWidgets/toggle_on_light.png new file mode 100644 index 0000000..1a1087e Binary files /dev/null and b/Client2021/content/textures/RoactStudioWidgets/toggle_on_light.png differ diff --git a/Client2021/content/textures/StudioConvertToPackagePlugin/placeholder.png b/Client2021/content/textures/StudioConvertToPackagePlugin/placeholder.png new file mode 100644 index 0000000..ec8f91e Binary files /dev/null and b/Client2021/content/textures/StudioConvertToPackagePlugin/placeholder.png differ diff --git a/Client2021/content/textures/StudioPlayerEmulator/player_emulator_32.png b/Client2021/content/textures/StudioPlayerEmulator/player_emulator_32.png new file mode 100644 index 0000000..fb386ef Binary files /dev/null and b/Client2021/content/textures/StudioPlayerEmulator/player_emulator_32.png differ diff --git a/Client2021/content/textures/StudioSharedUI/alert_error_2x.png b/Client2021/content/textures/StudioSharedUI/alert_error_2x.png new file mode 100644 index 0000000..92e55f0 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/alert_error_2x.png differ diff --git a/Client2021/content/textures/StudioSharedUI/alert_info_2x.png b/Client2021/content/textures/StudioSharedUI/alert_info_2x.png new file mode 100644 index 0000000..6e1f1c6 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/alert_info_2x.png differ diff --git a/Client2021/content/textures/StudioSharedUI/alert_warning_2x.png b/Client2021/content/textures/StudioSharedUI/alert_warning_2x.png new file mode 100644 index 0000000..d7f6a08 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/alert_warning_2x.png differ diff --git a/Client2021/content/textures/StudioSharedUI/arrowSpritesheet.png b/Client2021/content/textures/StudioSharedUI/arrowSpritesheet.png new file mode 100644 index 0000000..0750c8c Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/arrowSpritesheet.png differ diff --git a/Client2021/content/textures/StudioSharedUI/avatarMask.png b/Client2021/content/textures/StudioSharedUI/avatarMask.png new file mode 100644 index 0000000..1ac156d Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/avatarMask.png differ diff --git a/Client2021/content/textures/StudioSharedUI/clear-hover.png b/Client2021/content/textures/StudioSharedUI/clear-hover.png new file mode 100644 index 0000000..fee06ad Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/clear-hover.png differ diff --git a/Client2021/content/textures/StudioSharedUI/clear.png b/Client2021/content/textures/StudioSharedUI/clear.png new file mode 100644 index 0000000..0d00fe3 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/clear.png differ diff --git a/Client2021/content/textures/StudioSharedUI/close.png b/Client2021/content/textures/StudioSharedUI/close.png new file mode 100644 index 0000000..bd22a81 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/close.png differ diff --git a/Client2021/content/textures/StudioSharedUI/default_group.png b/Client2021/content/textures/StudioSharedUI/default_group.png new file mode 100644 index 0000000..cd4ca17 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/default_group.png differ diff --git a/Client2021/content/textures/StudioSharedUI/default_user.png b/Client2021/content/textures/StudioSharedUI/default_user.png new file mode 100644 index 0000000..79e92a8 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/default_user.png differ diff --git a/Client2021/content/textures/StudioSharedUI/dot.png b/Client2021/content/textures/StudioSharedUI/dot.png new file mode 100644 index 0000000..e890040 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/dot.png differ diff --git a/Client2021/content/textures/StudioSharedUI/dropShadow.png b/Client2021/content/textures/StudioSharedUI/dropShadow.png new file mode 100644 index 0000000..d0841b4 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/dropShadow.png differ diff --git a/Client2021/content/textures/StudioSharedUI/folder.png b/Client2021/content/textures/StudioSharedUI/folder.png new file mode 100644 index 0000000..55259e3 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/folder.png differ diff --git a/Client2021/content/textures/StudioSharedUI/grid.png b/Client2021/content/textures/StudioSharedUI/grid.png new file mode 100644 index 0000000..8654a0b Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/grid.png differ diff --git a/Client2021/content/textures/StudioSharedUI/grid_2x.png b/Client2021/content/textures/StudioSharedUI/grid_2x.png new file mode 100644 index 0000000..0d9e3fd Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/grid_2x.png differ diff --git a/Client2021/content/textures/StudioSharedUI/import.png b/Client2021/content/textures/StudioSharedUI/import.png new file mode 100644 index 0000000..5cf509a Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/import.png differ diff --git a/Client2021/content/textures/StudioSharedUI/import_2x.png b/Client2021/content/textures/StudioSharedUI/import_2x.png new file mode 100644 index 0000000..17d719a Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/import_2x.png differ diff --git a/Client2021/content/textures/StudioSharedUI/list.png b/Client2021/content/textures/StudioSharedUI/list.png new file mode 100644 index 0000000..9c2025b Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/list.png differ diff --git a/Client2021/content/textures/StudioSharedUI/list_2x.png b/Client2021/content/textures/StudioSharedUI/list_2x.png new file mode 100644 index 0000000..b128108 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/list_2x.png differ diff --git a/Client2021/content/textures/StudioSharedUI/menu.png b/Client2021/content/textures/StudioSharedUI/menu.png new file mode 100644 index 0000000..fc70cb1 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/menu.png differ diff --git a/Client2021/content/textures/StudioSharedUI/preview_clear.png b/Client2021/content/textures/StudioSharedUI/preview_clear.png new file mode 100644 index 0000000..d0e8d18 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/preview_clear.png differ diff --git a/Client2021/content/textures/StudioSharedUI/preview_expand.png b/Client2021/content/textures/StudioSharedUI/preview_expand.png new file mode 100644 index 0000000..7de6fef Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/preview_expand.png differ diff --git a/Client2021/content/textures/StudioSharedUI/search.png b/Client2021/content/textures/StudioSharedUI/search.png new file mode 100644 index 0000000..2db1080 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/search.png differ diff --git a/Client2021/content/textures/StudioSharedUI/spawn_withbg_24.png b/Client2021/content/textures/StudioSharedUI/spawn_withbg_24.png new file mode 100644 index 0000000..827984e Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/spawn_withbg_24.png differ diff --git a/Client2021/content/textures/StudioSharedUI/spawn_withbg_32.png b/Client2021/content/textures/StudioSharedUI/spawn_withbg_32.png new file mode 100644 index 0000000..16d53c2 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/spawn_withbg_32.png differ diff --git a/Client2021/content/textures/StudioSharedUI/spawn_withoutbg_24.png b/Client2021/content/textures/StudioSharedUI/spawn_withoutbg_24.png new file mode 100644 index 0000000..101cb49 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/spawn_withoutbg_24.png differ diff --git a/Client2021/content/textures/StudioSharedUI/spawn_withoutbg_32.png b/Client2021/content/textures/StudioSharedUI/spawn_withoutbg_32.png new file mode 100644 index 0000000..72dae33 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/spawn_withoutbg_32.png differ diff --git a/Client2021/content/textures/StudioSharedUI/statusSuccess.png b/Client2021/content/textures/StudioSharedUI/statusSuccess.png new file mode 100644 index 0000000..35c806c Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/statusSuccess.png differ diff --git a/Client2021/content/textures/StudioSharedUI/statusWarning.png b/Client2021/content/textures/StudioSharedUI/statusWarning.png new file mode 100644 index 0000000..6e5f9e6 Binary files /dev/null and b/Client2021/content/textures/StudioSharedUI/statusWarning.png differ diff --git a/Client2021/content/textures/StudioToolbox/Animation.png b/Client2021/content/textures/StudioToolbox/Animation.png new file mode 100644 index 0000000..5002fd5 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/Animation.png differ diff --git a/Client2021/content/textures/StudioToolbox/ArrowCollapsed.png b/Client2021/content/textures/StudioToolbox/ArrowCollapsed.png new file mode 100644 index 0000000..7a18bdb Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/ArrowCollapsed.png differ diff --git a/Client2021/content/textures/StudioToolbox/ArrowDownIconWhite.png b/Client2021/content/textures/StudioToolbox/ArrowDownIconWhite.png new file mode 100644 index 0000000..c5f9c2a Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/ArrowDownIconWhite.png differ diff --git a/Client2021/content/textures/StudioToolbox/ArrowExpanded.png b/Client2021/content/textures/StudioToolbox/ArrowExpanded.png new file mode 100644 index 0000000..c682205 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/ArrowExpanded.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/CenterPlus.png b/Client2021/content/textures/StudioToolbox/AssetConfig/CenterPlus.png new file mode 100644 index 0000000..05af26f Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/CenterPlus.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/copy_2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/copy_2x.png new file mode 100644 index 0000000..f46a53b Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/copy_2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/creations.png b/Client2021/content/textures/StudioToolbox/AssetConfig/creations.png new file mode 100644 index 0000000..0f1663a Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/creations.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/creations@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/creations@2x.png new file mode 100644 index 0000000..25534ed Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/creations@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/creations@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/creations@3x.png new file mode 100644 index 0000000..4725d58 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/creations@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/editlisting.png b/Client2021/content/textures/StudioToolbox/AssetConfig/editlisting.png new file mode 100644 index 0000000..c6e4874 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/editlisting.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/editlisting@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/editlisting@2x.png new file mode 100644 index 0000000..3abe57a Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/editlisting@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/editlisting@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/editlisting@3x.png new file mode 100644 index 0000000..1b69544 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/editlisting@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/gridview.png b/Client2021/content/textures/StudioToolbox/AssetConfig/gridview.png new file mode 100644 index 0000000..36b3c5c Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/gridview.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/gridview@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/gridview@2x.png new file mode 100644 index 0000000..f0d2fe1 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/gridview@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/gridview@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/gridview@3x.png new file mode 100644 index 0000000..2da3f0b Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/gridview@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/inventory.png b/Client2021/content/textures/StudioToolbox/AssetConfig/inventory.png new file mode 100644 index 0000000..10599b5 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/inventory.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/inventory@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/inventory@2x.png new file mode 100644 index 0000000..ea92214 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/inventory@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/inventory@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/inventory@3x.png new file mode 100644 index 0000000..861c888 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/inventory@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/listview.png b/Client2021/content/textures/StudioToolbox/AssetConfig/listview.png new file mode 100644 index 0000000..66cf09f Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/listview.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/listview@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/listview@2x.png new file mode 100644 index 0000000..7516e7f Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/listview@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/listview@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/listview@3x.png new file mode 100644 index 0000000..fd35d0d Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/listview@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/marketplace.png b/Client2021/content/textures/StudioToolbox/AssetConfig/marketplace.png new file mode 100644 index 0000000..114cf9c Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/marketplace.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/marketplace@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/marketplace@2x.png new file mode 100644 index 0000000..1ee0bd6 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/marketplace@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/marketplace@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/marketplace@3x.png new file mode 100644 index 0000000..b99e341 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/marketplace@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/menu_friends.png b/Client2021/content/textures/StudioToolbox/AssetConfig/menu_friends.png new file mode 100644 index 0000000..401847e Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/menu_friends.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/menu_friends@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/menu_friends@2x.png new file mode 100644 index 0000000..3a91c59 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/menu_friends@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/menu_friends@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/menu_friends@3x.png new file mode 100644 index 0000000..2fa4d99 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/menu_friends@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/offsale.png b/Client2021/content/textures/StudioToolbox/AssetConfig/offsale.png new file mode 100644 index 0000000..9191737 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/offsale.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/offsale@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/offsale@2x.png new file mode 100644 index 0000000..463a57c Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/offsale@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/offsale@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/offsale@3x.png new file mode 100644 index 0000000..97ae618 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/offsale@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/onsale.png b/Client2021/content/textures/StudioToolbox/AssetConfig/onsale.png new file mode 100644 index 0000000..d8aef73 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/onsale.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/onsale@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/onsale@2x.png new file mode 100644 index 0000000..5ccc0d3 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/onsale@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/onsale@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/onsale@3x.png new file mode 100644 index 0000000..41f34d7 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/onsale@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/pending.png b/Client2021/content/textures/StudioToolbox/AssetConfig/pending.png new file mode 100644 index 0000000..3bae1ed Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/pending.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/pending@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/pending@2x.png new file mode 100644 index 0000000..9ff8939 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/pending@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/pending@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/pending@3x.png new file mode 100644 index 0000000..597e6c6 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/pending@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/plugin_temp.png b/Client2021/content/textures/StudioToolbox/AssetConfig/plugin_temp.png new file mode 100644 index 0000000..3fd6953 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/plugin_temp.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/private.png b/Client2021/content/textures/StudioToolbox/AssetConfig/private.png new file mode 100644 index 0000000..6b64f56 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/private.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/private@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/private@2x.png new file mode 100644 index 0000000..826a07a Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/private@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/private@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/private@3x.png new file mode 100644 index 0000000..4d7b2ff Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/private@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/public.png b/Client2021/content/textures/StudioToolbox/AssetConfig/public.png new file mode 100644 index 0000000..5d5d51b Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/public.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/public@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/public@2x.png new file mode 100644 index 0000000..bcd3aac Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/public@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/public@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/public@3x.png new file mode 100644 index 0000000..88e2800 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/public@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/readyforsale.png b/Client2021/content/textures/StudioToolbox/AssetConfig/readyforsale.png new file mode 100644 index 0000000..79ff880 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/readyforsale.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/readyforsale@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/readyforsale@2x.png new file mode 100644 index 0000000..7fc5bad Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/readyforsale@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/readyforsale@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/readyforsale@3x.png new file mode 100644 index 0000000..26bd8d5 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/readyforsale@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/recent.png b/Client2021/content/textures/StudioToolbox/AssetConfig/recent.png new file mode 100644 index 0000000..873cc6d Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/recent.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/recent@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/recent@2x.png new file mode 100644 index 0000000..ca550c1 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/recent@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/recent@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/recent@3x.png new file mode 100644 index 0000000..199f1e0 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/recent@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/rejected.png b/Client2021/content/textures/StudioToolbox/AssetConfig/rejected.png new file mode 100644 index 0000000..4738d37 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/rejected.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/rejected@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/rejected@2x.png new file mode 100644 index 0000000..483a83f Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/rejected@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/rejected@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/rejected@3x.png new file mode 100644 index 0000000..64d5173 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/rejected@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/restore.png b/Client2021/content/textures/StudioToolbox/AssetConfig/restore.png new file mode 100644 index 0000000..0b414f7 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/restore.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/restore@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/restore@2x.png new file mode 100644 index 0000000..2c5dfa4 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/restore@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/restore@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/restore@3x.png new file mode 100644 index 0000000..2305a15 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/restore@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/sales.png b/Client2021/content/textures/StudioToolbox/AssetConfig/sales.png new file mode 100644 index 0000000..00d8dcc Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/sales.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/sales@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/sales@2x.png new file mode 100644 index 0000000..938b9a3 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/sales@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/sales@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/sales@3x.png new file mode 100644 index 0000000..3cc2836 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/sales@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/selected.png b/Client2021/content/textures/StudioToolbox/AssetConfig/selected.png new file mode 100644 index 0000000..ea5fd9b Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/selected.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/selected@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/selected@2x.png new file mode 100644 index 0000000..26bd8d5 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/selected@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/selected@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/selected@3x.png new file mode 100644 index 0000000..0335111 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/selected@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/version.png b/Client2021/content/textures/StudioToolbox/AssetConfig/version.png new file mode 100644 index 0000000..b212d7d Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/version.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/version@2x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/version@2x.png new file mode 100644 index 0000000..7ea5f32 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/version@2x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetConfig/version@3x.png b/Client2021/content/textures/StudioToolbox/AssetConfig/version@3x.png new file mode 100644 index 0000000..3499bc2 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetConfig/version@3x.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/Likes_Grey.png b/Client2021/content/textures/StudioToolbox/AssetPreview/Likes_Grey.png new file mode 100644 index 0000000..9209f80 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/Likes_Grey.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/Link_Arrow.png b/Client2021/content/textures/StudioToolbox/AssetPreview/Link_Arrow.png new file mode 100644 index 0000000..b480c24 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/Link_Arrow.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/OffSale.png b/Client2021/content/textures/StudioToolbox/AssetPreview/OffSale.png new file mode 100644 index 0000000..a08e3ed Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/OffSale.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/OnSale.png b/Client2021/content/textures/StudioToolbox/AssetPreview/OnSale.png new file mode 100644 index 0000000..0095136 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/OnSale.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/Pending.png b/Client2021/content/textures/StudioToolbox/AssetPreview/Pending.png new file mode 100644 index 0000000..abe6b5e Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/Pending.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/ReadyforSale.png b/Client2021/content/textures/StudioToolbox/AssetPreview/ReadyforSale.png new file mode 100644 index 0000000..0be6696 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/ReadyforSale.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/Rejected.png b/Client2021/content/textures/StudioToolbox/AssetPreview/Rejected.png new file mode 100644 index 0000000..5f5bdb0 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/Rejected.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/audioPlay_BG.png b/Client2021/content/textures/StudioToolbox/AssetPreview/audioPlay_BG.png new file mode 100644 index 0000000..779664b Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/audioPlay_BG.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/close.png b/Client2021/content/textures/StudioToolbox/AssetPreview/close.png new file mode 100644 index 0000000..1ac968b Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/close.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/fullscreen.png b/Client2021/content/textures/StudioToolbox/AssetPreview/fullscreen.png new file mode 100644 index 0000000..3014364 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/fullscreen.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/fullscreen_exit.png b/Client2021/content/textures/StudioToolbox/AssetPreview/fullscreen_exit.png new file mode 100644 index 0000000..f0ceb71 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/fullscreen_exit.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/hierarchy.png b/Client2021/content/textures/StudioToolbox/AssetPreview/hierarchy.png new file mode 100644 index 0000000..853f4e5 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/hierarchy.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/magnifier_ph.png b/Client2021/content/textures/StudioToolbox/AssetPreview/magnifier_ph.png new file mode 100644 index 0000000..6ad4ef5 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/magnifier_ph.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/more.png b/Client2021/content/textures/StudioToolbox/AssetPreview/more.png new file mode 100644 index 0000000..65abae2 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/more.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/pause_button.png b/Client2021/content/textures/StudioToolbox/AssetPreview/pause_button.png new file mode 100644 index 0000000..d6ba58e Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/pause_button.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/play_button.png b/Client2021/content/textures/StudioToolbox/AssetPreview/play_button.png new file mode 100644 index 0000000..a97bde8 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/play_button.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/preview.png b/Client2021/content/textures/StudioToolbox/AssetPreview/preview.png new file mode 100644 index 0000000..6d4e824 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/preview.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/rating_large.png b/Client2021/content/textures/StudioToolbox/AssetPreview/rating_large.png new file mode 100644 index 0000000..b816657 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/rating_large.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/rating_small.png b/Client2021/content/textures/StudioToolbox/AssetPreview/rating_small.png new file mode 100644 index 0000000..4a3100d Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/rating_small.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/star_filled.png b/Client2021/content/textures/StudioToolbox/AssetPreview/star_filled.png new file mode 100644 index 0000000..fbf2a35 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/star_filled.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/star_stroke.png b/Client2021/content/textures/StudioToolbox/AssetPreview/star_stroke.png new file mode 100644 index 0000000..366df74 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/star_stroke.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/vote_down.png b/Client2021/content/textures/StudioToolbox/AssetPreview/vote_down.png new file mode 100644 index 0000000..4bbe19e Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/vote_down.png differ diff --git a/Client2021/content/textures/StudioToolbox/AssetPreview/vote_up.png b/Client2021/content/textures/StudioToolbox/AssetPreview/vote_up.png new file mode 100644 index 0000000..35bc285 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AssetPreview/vote_up.png differ diff --git a/Client2021/content/textures/StudioToolbox/AudioPreview/pause.png b/Client2021/content/textures/StudioToolbox/AudioPreview/pause.png new file mode 100644 index 0000000..86a1d64 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AudioPreview/pause.png differ diff --git a/Client2021/content/textures/StudioToolbox/AudioPreview/pause_hover.png b/Client2021/content/textures/StudioToolbox/AudioPreview/pause_hover.png new file mode 100644 index 0000000..004e098 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AudioPreview/pause_hover.png differ diff --git a/Client2021/content/textures/StudioToolbox/AudioPreview/play.png b/Client2021/content/textures/StudioToolbox/AudioPreview/play.png new file mode 100644 index 0000000..20d4aac Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AudioPreview/play.png differ diff --git a/Client2021/content/textures/StudioToolbox/AudioPreview/play_hover.png b/Client2021/content/textures/StudioToolbox/AudioPreview/play_hover.png new file mode 100644 index 0000000..9a5b37a Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/AudioPreview/play_hover.png differ diff --git a/Client2021/content/textures/StudioToolbox/Clear.png b/Client2021/content/textures/StudioToolbox/Clear.png new file mode 100644 index 0000000..0d00fe3 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/Clear.png differ diff --git a/Client2021/content/textures/StudioToolbox/ClearHover.png b/Client2021/content/textures/StudioToolbox/ClearHover.png new file mode 100644 index 0000000..fee06ad Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/ClearHover.png differ diff --git a/Client2021/content/textures/StudioToolbox/DeleteButton.png b/Client2021/content/textures/StudioToolbox/DeleteButton.png new file mode 100644 index 0000000..05116cd Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/DeleteButton.png differ diff --git a/Client2021/content/textures/StudioToolbox/EndorsedBadge.png b/Client2021/content/textures/StudioToolbox/EndorsedBadge.png new file mode 100644 index 0000000..b3f5fb4 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/EndorsedBadge.png differ diff --git a/Client2021/content/textures/StudioToolbox/NoBackgroundIcon.png b/Client2021/content/textures/StudioToolbox/NoBackgroundIcon.png new file mode 100644 index 0000000..1c1e179 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/NoBackgroundIcon.png differ diff --git a/Client2021/content/textures/StudioToolbox/ProductOwned.png b/Client2021/content/textures/StudioToolbox/ProductOwned.png new file mode 100644 index 0000000..1e29177 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/ProductOwned.png differ diff --git a/Client2021/content/textures/StudioToolbox/RoundedBackground.png b/Client2021/content/textures/StudioToolbox/RoundedBackground.png new file mode 100644 index 0000000..602e90f Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/RoundedBackground.png differ diff --git a/Client2021/content/textures/StudioToolbox/RoundedBorder.png b/Client2021/content/textures/StudioToolbox/RoundedBorder.png new file mode 100644 index 0000000..8335459 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/RoundedBorder.png differ diff --git a/Client2021/content/textures/StudioToolbox/ScrollBarBottom.png b/Client2021/content/textures/StudioToolbox/ScrollBarBottom.png new file mode 100644 index 0000000..235055a Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/ScrollBarBottom.png differ diff --git a/Client2021/content/textures/StudioToolbox/ScrollBarMiddle.png b/Client2021/content/textures/StudioToolbox/ScrollBarMiddle.png new file mode 100644 index 0000000..ed7ca4a Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/ScrollBarMiddle.png differ diff --git a/Client2021/content/textures/StudioToolbox/ScrollBarTop.png b/Client2021/content/textures/StudioToolbox/ScrollBarTop.png new file mode 100644 index 0000000..d01c82f Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/ScrollBarTop.png differ diff --git a/Client2021/content/textures/StudioToolbox/Search.png b/Client2021/content/textures/StudioToolbox/Search.png new file mode 100644 index 0000000..f4934f7 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/Search.png differ diff --git a/Client2021/content/textures/StudioToolbox/SearchOptions.png b/Client2021/content/textures/StudioToolbox/SearchOptions.png new file mode 100644 index 0000000..6c894e1 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/SearchOptions.png differ diff --git a/Client2021/content/textures/StudioToolbox/Tabs/Inventory.png b/Client2021/content/textures/StudioToolbox/Tabs/Inventory.png new file mode 100644 index 0000000..2c21b17 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/Tabs/Inventory.png differ diff --git a/Client2021/content/textures/StudioToolbox/Tabs/MyCreations.png b/Client2021/content/textures/StudioToolbox/Tabs/MyCreations.png new file mode 100644 index 0000000..df19608 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/Tabs/MyCreations.png differ diff --git a/Client2021/content/textures/StudioToolbox/Tabs/Recent.png b/Client2021/content/textures/StudioToolbox/Tabs/Recent.png new file mode 100644 index 0000000..538e331 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/Tabs/Recent.png differ diff --git a/Client2021/content/textures/StudioToolbox/Tabs/Shop.png b/Client2021/content/textures/StudioToolbox/Tabs/Shop.png new file mode 100644 index 0000000..f1c20f7 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/Tabs/Shop.png differ diff --git a/Client2021/content/textures/StudioToolbox/ToolboxIcon.png b/Client2021/content/textures/StudioToolbox/ToolboxIcon.png new file mode 100644 index 0000000..2e99ec3 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/ToolboxIcon.png differ diff --git a/Client2021/content/textures/StudioToolbox/Voting/Thumb.png b/Client2021/content/textures/StudioToolbox/Voting/Thumb.png new file mode 100644 index 0000000..21a239b Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/Voting/Thumb.png differ diff --git a/Client2021/content/textures/StudioToolbox/Voting/thumb-down.png b/Client2021/content/textures/StudioToolbox/Voting/thumb-down.png new file mode 100644 index 0000000..d36de77 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/Voting/thumb-down.png differ diff --git a/Client2021/content/textures/StudioToolbox/Voting/thumbs-down-filled.png b/Client2021/content/textures/StudioToolbox/Voting/thumbs-down-filled.png new file mode 100644 index 0000000..1976341 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/Voting/thumbs-down-filled.png differ diff --git a/Client2021/content/textures/StudioToolbox/Voting/thumbs-up-filled.png b/Client2021/content/textures/StudioToolbox/Voting/thumbs-up-filled.png new file mode 100644 index 0000000..f97cbdd Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/Voting/thumbs-up-filled.png differ diff --git a/Client2021/content/textures/StudioToolbox/Voting/thumbup.png b/Client2021/content/textures/StudioToolbox/Voting/thumbup.png new file mode 100644 index 0000000..babd290 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/Voting/thumbup.png differ diff --git a/Client2021/content/textures/StudioToolbox/placeholder_video.png b/Client2021/content/textures/StudioToolbox/placeholder_video.png new file mode 100644 index 0000000..be98f33 Binary files /dev/null and b/Client2021/content/textures/StudioToolbox/placeholder_video.png differ diff --git a/Client2021/content/textures/StudioUIEditor/icon_resize1.png b/Client2021/content/textures/StudioUIEditor/icon_resize1.png new file mode 100644 index 0000000..23aef57 Binary files /dev/null and b/Client2021/content/textures/StudioUIEditor/icon_resize1.png differ diff --git a/Client2021/content/textures/StudioUIEditor/icon_resize2.png b/Client2021/content/textures/StudioUIEditor/icon_resize2.png new file mode 100644 index 0000000..b50268f Binary files /dev/null and b/Client2021/content/textures/StudioUIEditor/icon_resize2.png differ diff --git a/Client2021/content/textures/StudioUIEditor/icon_resize3.png b/Client2021/content/textures/StudioUIEditor/icon_resize3.png new file mode 100644 index 0000000..3258512 Binary files /dev/null and b/Client2021/content/textures/StudioUIEditor/icon_resize3.png differ diff --git a/Client2021/content/textures/StudioUIEditor/icon_resize4.png b/Client2021/content/textures/StudioUIEditor/icon_resize4.png new file mode 100644 index 0000000..2ac469f Binary files /dev/null and b/Client2021/content/textures/StudioUIEditor/icon_resize4.png differ diff --git a/Client2021/content/textures/StudioUIEditor/icon_rotate1.png b/Client2021/content/textures/StudioUIEditor/icon_rotate1.png new file mode 100644 index 0000000..849be92 Binary files /dev/null and b/Client2021/content/textures/StudioUIEditor/icon_rotate1.png differ diff --git a/Client2021/content/textures/StudioUIEditor/icon_rotate2.png b/Client2021/content/textures/StudioUIEditor/icon_rotate2.png new file mode 100644 index 0000000..ee35889 Binary files /dev/null and b/Client2021/content/textures/StudioUIEditor/icon_rotate2.png differ diff --git a/Client2021/content/textures/StudioUIEditor/icon_rotate3.png b/Client2021/content/textures/StudioUIEditor/icon_rotate3.png new file mode 100644 index 0000000..a87e5bc Binary files /dev/null and b/Client2021/content/textures/StudioUIEditor/icon_rotate3.png differ diff --git a/Client2021/content/textures/StudioUIEditor/icon_rotate4.png b/Client2021/content/textures/StudioUIEditor/icon_rotate4.png new file mode 100644 index 0000000..e6934f7 Binary files /dev/null and b/Client2021/content/textures/StudioUIEditor/icon_rotate4.png differ diff --git a/Client2021/content/textures/StudioUIEditor/icon_rotate5.png b/Client2021/content/textures/StudioUIEditor/icon_rotate5.png new file mode 100644 index 0000000..2b69bc6 Binary files /dev/null and b/Client2021/content/textures/StudioUIEditor/icon_rotate5.png differ diff --git a/Client2021/content/textures/StudioUIEditor/icon_rotate6.png b/Client2021/content/textures/StudioUIEditor/icon_rotate6.png new file mode 100644 index 0000000..70dedbd Binary files /dev/null and b/Client2021/content/textures/StudioUIEditor/icon_rotate6.png differ diff --git a/Client2021/content/textures/StudioUIEditor/icon_rotate7.png b/Client2021/content/textures/StudioUIEditor/icon_rotate7.png new file mode 100644 index 0000000..048264c Binary files /dev/null and b/Client2021/content/textures/StudioUIEditor/icon_rotate7.png differ diff --git a/Client2021/content/textures/StudioUIEditor/icon_rotate8.png b/Client2021/content/textures/StudioUIEditor/icon_rotate8.png new file mode 100644 index 0000000..33afb5d Binary files /dev/null and b/Client2021/content/textures/StudioUIEditor/icon_rotate8.png differ diff --git a/Client2021/content/textures/StudioUIEditor/resizeHandleDropShadow.png b/Client2021/content/textures/StudioUIEditor/resizeHandleDropShadow.png new file mode 100644 index 0000000..b0aaf29 Binary files /dev/null and b/Client2021/content/textures/StudioUIEditor/resizeHandleDropShadow.png differ diff --git a/Client2021/content/textures/StudioUIEditor/valueBoxRoundedRectangle.png b/Client2021/content/textures/StudioUIEditor/valueBoxRoundedRectangle.png new file mode 100644 index 0000000..602e90f Binary files /dev/null and b/Client2021/content/textures/StudioUIEditor/valueBoxRoundedRectangle.png differ diff --git a/Client2021/content/textures/SurfacesDefault.png b/Client2021/content/textures/SurfacesDefault.png new file mode 100644 index 0000000..be149ad Binary files /dev/null and b/Client2021/content/textures/SurfacesDefault.png differ diff --git a/Client2021/content/textures/TerrainTools/DownArrowButtonOpen17.png b/Client2021/content/textures/TerrainTools/DownArrowButtonOpen17.png new file mode 100644 index 0000000..a4a8e42 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/DownArrowButtonOpen17.png differ diff --git a/Client2021/content/textures/TerrainTools/EdgesSquare17x1.png b/Client2021/content/textures/TerrainTools/EdgesSquare17x1.png new file mode 100644 index 0000000..5866d7f Binary files /dev/null and b/Client2021/content/textures/TerrainTools/EdgesSquare17x1.png differ diff --git a/Client2021/content/textures/TerrainTools/UpArrowButtonOpen17.png b/Client2021/content/textures/TerrainTools/UpArrowButtonOpen17.png new file mode 100644 index 0000000..75d8bc5 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/UpArrowButtonOpen17.png differ diff --git a/Client2021/content/textures/TerrainTools/button_arrow.png b/Client2021/content/textures/TerrainTools/button_arrow.png new file mode 100644 index 0000000..31b851c Binary files /dev/null and b/Client2021/content/textures/TerrainTools/button_arrow.png differ diff --git a/Client2021/content/textures/TerrainTools/button_arrow_down.png b/Client2021/content/textures/TerrainTools/button_arrow_down.png new file mode 100644 index 0000000..f3c6616 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/button_arrow_down.png differ diff --git a/Client2021/content/textures/TerrainTools/button_default.png b/Client2021/content/textures/TerrainTools/button_default.png new file mode 100644 index 0000000..28eb7a7 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/button_default.png differ diff --git a/Client2021/content/textures/TerrainTools/button_hover.png b/Client2021/content/textures/TerrainTools/button_hover.png new file mode 100644 index 0000000..5184f6d Binary files /dev/null and b/Client2021/content/textures/TerrainTools/button_hover.png differ diff --git a/Client2021/content/textures/TerrainTools/button_pressed.png b/Client2021/content/textures/TerrainTools/button_pressed.png new file mode 100644 index 0000000..5484114 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/button_pressed.png differ diff --git a/Client2021/content/textures/TerrainTools/checkbox_square.png b/Client2021/content/textures/TerrainTools/checkbox_square.png new file mode 100644 index 0000000..af883a9 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/checkbox_square.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_flatten_both.png b/Client2021/content/textures/TerrainTools/icon_flatten_both.png new file mode 100644 index 0000000..eda6621 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_flatten_both.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_flatten_erode.png b/Client2021/content/textures/TerrainTools/icon_flatten_erode.png new file mode 100644 index 0000000..08cf742 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_flatten_erode.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_flatten_grow.png b/Client2021/content/textures/TerrainTools/icon_flatten_grow.png new file mode 100644 index 0000000..c5a1b14 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_flatten_grow.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_picker_disable.png b/Client2021/content/textures/TerrainTools/icon_picker_disable.png new file mode 100644 index 0000000..7886a65 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_picker_disable.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_picker_disable_dark.png b/Client2021/content/textures/TerrainTools/icon_picker_disable_dark.png new file mode 100644 index 0000000..88fac4f Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_picker_disable_dark.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_picker_enable.png b/Client2021/content/textures/TerrainTools/icon_picker_enable.png new file mode 100644 index 0000000..dea3f2e Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_picker_enable.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_regions_copy.png b/Client2021/content/textures/TerrainTools/icon_regions_copy.png new file mode 100644 index 0000000..d95bfa9 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_regions_copy.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_regions_delete.png b/Client2021/content/textures/TerrainTools/icon_regions_delete.png new file mode 100644 index 0000000..cb5bbc3 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_regions_delete.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_regions_fill.png b/Client2021/content/textures/TerrainTools/icon_regions_fill.png new file mode 100644 index 0000000..a5ad504 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_regions_fill.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_regions_move.png b/Client2021/content/textures/TerrainTools/icon_regions_move.png new file mode 100644 index 0000000..f1dd66f Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_regions_move.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_regions_paste.png b/Client2021/content/textures/TerrainTools/icon_regions_paste.png new file mode 100644 index 0000000..a602011 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_regions_paste.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_regions_resize.png b/Client2021/content/textures/TerrainTools/icon_regions_resize.png new file mode 100644 index 0000000..1306cc0 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_regions_resize.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_regions_rotate.png b/Client2021/content/textures/TerrainTools/icon_regions_rotate.png new file mode 100644 index 0000000..feeb0dd Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_regions_rotate.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_regions_select.png b/Client2021/content/textures/TerrainTools/icon_regions_select.png new file mode 100644 index 0000000..bb6d6e1 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_regions_select.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_shape_cube.png b/Client2021/content/textures/TerrainTools/icon_shape_cube.png new file mode 100644 index 0000000..30b4f5a Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_shape_cube.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_shape_cylinder.png b/Client2021/content/textures/TerrainTools/icon_shape_cylinder.png new file mode 100644 index 0000000..6054bf2 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_shape_cylinder.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_shape_sphere.png b/Client2021/content/textures/TerrainTools/icon_shape_sphere.png new file mode 100644 index 0000000..41458ef Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_shape_sphere.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_terrain_big.png b/Client2021/content/textures/TerrainTools/icon_terrain_big.png new file mode 100644 index 0000000..6c27ece Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_terrain_big.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_tick.png b/Client2021/content/textures/TerrainTools/icon_tick.png new file mode 100644 index 0000000..997f788 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_tick.png differ diff --git a/Client2021/content/textures/TerrainTools/icon_tick_grey.png b/Client2021/content/textures/TerrainTools/icon_tick_grey.png new file mode 100644 index 0000000..d451338 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/icon_tick_grey.png differ diff --git a/Client2021/content/textures/TerrainTools/import_delete.png b/Client2021/content/textures/TerrainTools/import_delete.png new file mode 100644 index 0000000..a4d34bf Binary files /dev/null and b/Client2021/content/textures/TerrainTools/import_delete.png differ diff --git a/Client2021/content/textures/TerrainTools/import_edit.png b/Client2021/content/textures/TerrainTools/import_edit.png new file mode 100644 index 0000000..25d52ae Binary files /dev/null and b/Client2021/content/textures/TerrainTools/import_edit.png differ diff --git a/Client2021/content/textures/TerrainTools/import_selectImg_dark.png b/Client2021/content/textures/TerrainTools/import_selectImg_dark.png new file mode 100644 index 0000000..a6c34a8 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/import_selectImg_dark.png differ diff --git a/Client2021/content/textures/TerrainTools/import_select_image.png b/Client2021/content/textures/TerrainTools/import_select_image.png new file mode 100644 index 0000000..2014a44 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/import_select_image.png differ diff --git a/Client2021/content/textures/TerrainTools/import_toggleOff.png b/Client2021/content/textures/TerrainTools/import_toggleOff.png new file mode 100644 index 0000000..407ebec Binary files /dev/null and b/Client2021/content/textures/TerrainTools/import_toggleOff.png differ diff --git a/Client2021/content/textures/TerrainTools/import_toggleOff_dark.png b/Client2021/content/textures/TerrainTools/import_toggleOff_dark.png new file mode 100644 index 0000000..43b4783 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/import_toggleOff_dark.png differ diff --git a/Client2021/content/textures/TerrainTools/import_toggleOn.png b/Client2021/content/textures/TerrainTools/import_toggleOn.png new file mode 100644 index 0000000..494336e Binary files /dev/null and b/Client2021/content/textures/TerrainTools/import_toggleOn.png differ diff --git a/Client2021/content/textures/TerrainTools/import_toggleOn_dark.png b/Client2021/content/textures/TerrainTools/import_toggleOn_dark.png new file mode 100644 index 0000000..2f83bf4 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/import_toggleOn_dark.png differ diff --git a/Client2021/content/textures/TerrainTools/locked.png b/Client2021/content/textures/TerrainTools/locked.png new file mode 100644 index 0000000..69794d5 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/locked.png differ diff --git a/Client2021/content/textures/TerrainTools/mt_add.png b/Client2021/content/textures/TerrainTools/mt_add.png new file mode 100644 index 0000000..f9e0c40 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mt_add.png differ diff --git a/Client2021/content/textures/TerrainTools/mt_convert_part.png b/Client2021/content/textures/TerrainTools/mt_convert_part.png new file mode 100644 index 0000000..941f1ac Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mt_convert_part.png differ diff --git a/Client2021/content/textures/TerrainTools/mt_erode.png b/Client2021/content/textures/TerrainTools/mt_erode.png new file mode 100644 index 0000000..5ace872 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mt_erode.png differ diff --git a/Client2021/content/textures/TerrainTools/mt_flatten.png b/Client2021/content/textures/TerrainTools/mt_flatten.png new file mode 100644 index 0000000..1ae9a75 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mt_flatten.png differ diff --git a/Client2021/content/textures/TerrainTools/mt_generate.png b/Client2021/content/textures/TerrainTools/mt_generate.png new file mode 100644 index 0000000..dffcf7d Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mt_generate.png differ diff --git a/Client2021/content/textures/TerrainTools/mt_grow.png b/Client2021/content/textures/TerrainTools/mt_grow.png new file mode 100644 index 0000000..76e9333 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mt_grow.png differ diff --git a/Client2021/content/textures/TerrainTools/mt_paint.png b/Client2021/content/textures/TerrainTools/mt_paint.png new file mode 100644 index 0000000..03096f5 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mt_paint.png differ diff --git a/Client2021/content/textures/TerrainTools/mt_regions.png b/Client2021/content/textures/TerrainTools/mt_regions.png new file mode 100644 index 0000000..f9aaae8 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mt_regions.png differ diff --git a/Client2021/content/textures/TerrainTools/mt_replace.png b/Client2021/content/textures/TerrainTools/mt_replace.png new file mode 100644 index 0000000..14fc070 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mt_replace.png differ diff --git a/Client2021/content/textures/TerrainTools/mt_sea_level.png b/Client2021/content/textures/TerrainTools/mt_sea_level.png new file mode 100644 index 0000000..a552a52 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mt_sea_level.png differ diff --git a/Client2021/content/textures/TerrainTools/mt_smooth.png b/Client2021/content/textures/TerrainTools/mt_smooth.png new file mode 100644 index 0000000..76aadbc Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mt_smooth.png differ diff --git a/Client2021/content/textures/TerrainTools/mt_subtract.png b/Client2021/content/textures/TerrainTools/mt_subtract.png new file mode 100644 index 0000000..a9b2a6a Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mt_subtract.png differ diff --git a/Client2021/content/textures/TerrainTools/mt_terrain_clear.png b/Client2021/content/textures/TerrainTools/mt_terrain_clear.png new file mode 100644 index 0000000..3ef4be7 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mt_terrain_clear.png differ diff --git a/Client2021/content/textures/TerrainTools/mt_terrain_import.png b/Client2021/content/textures/TerrainTools/mt_terrain_import.png new file mode 100644 index 0000000..5b8085c Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mt_terrain_import.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_air.png b/Client2021/content/textures/TerrainTools/mtrl_air.png new file mode 100644 index 0000000..f9eb777 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_air.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_asphalt.png b/Client2021/content/textures/TerrainTools/mtrl_asphalt.png new file mode 100644 index 0000000..7b35820 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_asphalt.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_basalt.png b/Client2021/content/textures/TerrainTools/mtrl_basalt.png new file mode 100644 index 0000000..aaccf88 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_basalt.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_brick.png b/Client2021/content/textures/TerrainTools/mtrl_brick.png new file mode 100644 index 0000000..e0feaac Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_brick.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_cobblestone.png b/Client2021/content/textures/TerrainTools/mtrl_cobblestone.png new file mode 100644 index 0000000..f36e40b Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_cobblestone.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_concrete.png b/Client2021/content/textures/TerrainTools/mtrl_concrete.png new file mode 100644 index 0000000..8786719 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_concrete.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_crackedlava.png b/Client2021/content/textures/TerrainTools/mtrl_crackedlava.png new file mode 100644 index 0000000..03dcd6c Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_crackedlava.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_glacier.png b/Client2021/content/textures/TerrainTools/mtrl_glacier.png new file mode 100644 index 0000000..bd795db Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_glacier.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_grass.png b/Client2021/content/textures/TerrainTools/mtrl_grass.png new file mode 100644 index 0000000..bb58cb5 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_grass.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_ground.png b/Client2021/content/textures/TerrainTools/mtrl_ground.png new file mode 100644 index 0000000..b605720 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_ground.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_ice.png b/Client2021/content/textures/TerrainTools/mtrl_ice.png new file mode 100644 index 0000000..535b8b2 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_ice.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_leafygrass.png b/Client2021/content/textures/TerrainTools/mtrl_leafygrass.png new file mode 100644 index 0000000..fd4a10f Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_leafygrass.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_limestone.png b/Client2021/content/textures/TerrainTools/mtrl_limestone.png new file mode 100644 index 0000000..a0c1419 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_limestone.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_mud.png b/Client2021/content/textures/TerrainTools/mtrl_mud.png new file mode 100644 index 0000000..44ccbbc Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_mud.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_pavement.png b/Client2021/content/textures/TerrainTools/mtrl_pavement.png new file mode 100644 index 0000000..4306661 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_pavement.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_rock.png b/Client2021/content/textures/TerrainTools/mtrl_rock.png new file mode 100644 index 0000000..a66785e Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_rock.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_salt.png b/Client2021/content/textures/TerrainTools/mtrl_salt.png new file mode 100644 index 0000000..00d8c17 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_salt.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_sand.png b/Client2021/content/textures/TerrainTools/mtrl_sand.png new file mode 100644 index 0000000..df3431b Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_sand.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_sandstone.png b/Client2021/content/textures/TerrainTools/mtrl_sandstone.png new file mode 100644 index 0000000..9a31639 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_sandstone.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_slate.png b/Client2021/content/textures/TerrainTools/mtrl_slate.png new file mode 100644 index 0000000..e2b4a65 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_slate.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_snow.png b/Client2021/content/textures/TerrainTools/mtrl_snow.png new file mode 100644 index 0000000..9698dda Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_snow.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_water.png b/Client2021/content/textures/TerrainTools/mtrl_water.png new file mode 100644 index 0000000..0b9595a Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_water.png differ diff --git a/Client2021/content/textures/TerrainTools/mtrl_woodplanks.png b/Client2021/content/textures/TerrainTools/mtrl_woodplanks.png new file mode 100644 index 0000000..d7a3cee Binary files /dev/null and b/Client2021/content/textures/TerrainTools/mtrl_woodplanks.png differ diff --git a/Client2021/content/textures/TerrainTools/progress_bar.png b/Client2021/content/textures/TerrainTools/progress_bar.png new file mode 100644 index 0000000..4c8be1c Binary files /dev/null and b/Client2021/content/textures/TerrainTools/progress_bar.png differ diff --git a/Client2021/content/textures/TerrainTools/radio_button_bullet.png b/Client2021/content/textures/TerrainTools/radio_button_bullet.png new file mode 100644 index 0000000..0f96b39 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/radio_button_bullet.png differ diff --git a/Client2021/content/textures/TerrainTools/radio_button_bullet_dark.png b/Client2021/content/textures/TerrainTools/radio_button_bullet_dark.png new file mode 100644 index 0000000..c846d72 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/radio_button_bullet_dark.png differ diff --git a/Client2021/content/textures/TerrainTools/radio_button_frame.png b/Client2021/content/textures/TerrainTools/radio_button_frame.png new file mode 100644 index 0000000..fbb0cff Binary files /dev/null and b/Client2021/content/textures/TerrainTools/radio_button_frame.png differ diff --git a/Client2021/content/textures/TerrainTools/radio_button_frame_dark.png b/Client2021/content/textures/TerrainTools/radio_button_frame_dark.png new file mode 100644 index 0000000..b3d9af0 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/radio_button_frame_dark.png differ diff --git a/Client2021/content/textures/TerrainTools/sliderbar_blue.png b/Client2021/content/textures/TerrainTools/sliderbar_blue.png new file mode 100644 index 0000000..0fbbf6b Binary files /dev/null and b/Client2021/content/textures/TerrainTools/sliderbar_blue.png differ diff --git a/Client2021/content/textures/TerrainTools/sliderbar_button.png b/Client2021/content/textures/TerrainTools/sliderbar_button.png new file mode 100644 index 0000000..0f8e1c7 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/sliderbar_button.png differ diff --git a/Client2021/content/textures/TerrainTools/sliderbar_grey.png b/Client2021/content/textures/TerrainTools/sliderbar_grey.png new file mode 100644 index 0000000..dd1cdaa Binary files /dev/null and b/Client2021/content/textures/TerrainTools/sliderbar_grey.png differ diff --git a/Client2021/content/textures/TerrainTools/unlocked.png b/Client2021/content/textures/TerrainTools/unlocked.png new file mode 100644 index 0000000..ee12272 Binary files /dev/null and b/Client2021/content/textures/TerrainTools/unlocked.png differ diff --git a/Client2021/content/textures/UnAnchorCursor.png b/Client2021/content/textures/UnAnchorCursor.png new file mode 100644 index 0000000..4ca5381 Binary files /dev/null and b/Client2021/content/textures/UnAnchorCursor.png differ diff --git a/Client2021/content/textures/UnlockCursor.png b/Client2021/content/textures/UnlockCursor.png new file mode 100644 index 0000000..b765c16 Binary files /dev/null and b/Client2021/content/textures/UnlockCursor.png differ diff --git a/Client2021/content/textures/ViewSelector/back.png b/Client2021/content/textures/ViewSelector/back.png new file mode 100644 index 0000000..9efb52b Binary files /dev/null and b/Client2021/content/textures/ViewSelector/back.png differ diff --git a/Client2021/content/textures/ViewSelector/back_hover.png b/Client2021/content/textures/ViewSelector/back_hover.png new file mode 100644 index 0000000..0b75db4 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/back_hover.png differ diff --git a/Client2021/content/textures/ViewSelector/back_hover_zh_cn.png b/Client2021/content/textures/ViewSelector/back_hover_zh_cn.png new file mode 100644 index 0000000..4c1cd59 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/back_hover_zh_cn.png differ diff --git a/Client2021/content/textures/ViewSelector/back_zh_cn.png b/Client2021/content/textures/ViewSelector/back_zh_cn.png new file mode 100644 index 0000000..78e0d29 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/back_zh_cn.png differ diff --git a/Client2021/content/textures/ViewSelector/background.png b/Client2021/content/textures/ViewSelector/background.png new file mode 100644 index 0000000..eda1219 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/background.png differ diff --git a/Client2021/content/textures/ViewSelector/bottom.png b/Client2021/content/textures/ViewSelector/bottom.png new file mode 100644 index 0000000..2d6ca53 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/bottom.png differ diff --git a/Client2021/content/textures/ViewSelector/bottom_hover.png b/Client2021/content/textures/ViewSelector/bottom_hover.png new file mode 100644 index 0000000..36f744c Binary files /dev/null and b/Client2021/content/textures/ViewSelector/bottom_hover.png differ diff --git a/Client2021/content/textures/ViewSelector/bottom_hover_zh_cn.png b/Client2021/content/textures/ViewSelector/bottom_hover_zh_cn.png new file mode 100644 index 0000000..1f4f17b Binary files /dev/null and b/Client2021/content/textures/ViewSelector/bottom_hover_zh_cn.png differ diff --git a/Client2021/content/textures/ViewSelector/bottom_zh_cn.png b/Client2021/content/textures/ViewSelector/bottom_zh_cn.png new file mode 100644 index 0000000..1342fef Binary files /dev/null and b/Client2021/content/textures/ViewSelector/bottom_zh_cn.png differ diff --git a/Client2021/content/textures/ViewSelector/face_arrow.png b/Client2021/content/textures/ViewSelector/face_arrow.png new file mode 100644 index 0000000..f29eb16 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/face_arrow.png differ diff --git a/Client2021/content/textures/ViewSelector/front.png b/Client2021/content/textures/ViewSelector/front.png new file mode 100644 index 0000000..a1543bf Binary files /dev/null and b/Client2021/content/textures/ViewSelector/front.png differ diff --git a/Client2021/content/textures/ViewSelector/front_hover.png b/Client2021/content/textures/ViewSelector/front_hover.png new file mode 100644 index 0000000..762fcae Binary files /dev/null and b/Client2021/content/textures/ViewSelector/front_hover.png differ diff --git a/Client2021/content/textures/ViewSelector/front_hover_zh_cn.png b/Client2021/content/textures/ViewSelector/front_hover_zh_cn.png new file mode 100644 index 0000000..ea81e16 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/front_hover_zh_cn.png differ diff --git a/Client2021/content/textures/ViewSelector/front_zh_cn.png b/Client2021/content/textures/ViewSelector/front_zh_cn.png new file mode 100644 index 0000000..9663a7e Binary files /dev/null and b/Client2021/content/textures/ViewSelector/front_zh_cn.png differ diff --git a/Client2021/content/textures/ViewSelector/left.png b/Client2021/content/textures/ViewSelector/left.png new file mode 100644 index 0000000..afe2401 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/left.png differ diff --git a/Client2021/content/textures/ViewSelector/left_hover.png b/Client2021/content/textures/ViewSelector/left_hover.png new file mode 100644 index 0000000..532ec74 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/left_hover.png differ diff --git a/Client2021/content/textures/ViewSelector/left_hover_zh_cn.png b/Client2021/content/textures/ViewSelector/left_hover_zh_cn.png new file mode 100644 index 0000000..20ab1d5 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/left_hover_zh_cn.png differ diff --git a/Client2021/content/textures/ViewSelector/left_zh_cn.png b/Client2021/content/textures/ViewSelector/left_zh_cn.png new file mode 100644 index 0000000..3402de2 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/left_zh_cn.png differ diff --git a/Client2021/content/textures/ViewSelector/right.png b/Client2021/content/textures/ViewSelector/right.png new file mode 100644 index 0000000..8965244 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/right.png differ diff --git a/Client2021/content/textures/ViewSelector/right_hover.png b/Client2021/content/textures/ViewSelector/right_hover.png new file mode 100644 index 0000000..c7b72ca Binary files /dev/null and b/Client2021/content/textures/ViewSelector/right_hover.png differ diff --git a/Client2021/content/textures/ViewSelector/right_hover_zh_cn.png b/Client2021/content/textures/ViewSelector/right_hover_zh_cn.png new file mode 100644 index 0000000..282f743 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/right_hover_zh_cn.png differ diff --git a/Client2021/content/textures/ViewSelector/right_zh_cn.png b/Client2021/content/textures/ViewSelector/right_zh_cn.png new file mode 100644 index 0000000..8b09d6a Binary files /dev/null and b/Client2021/content/textures/ViewSelector/right_zh_cn.png differ diff --git a/Client2021/content/textures/ViewSelector/top.png b/Client2021/content/textures/ViewSelector/top.png new file mode 100644 index 0000000..64a529e Binary files /dev/null and b/Client2021/content/textures/ViewSelector/top.png differ diff --git a/Client2021/content/textures/ViewSelector/top_hover.png b/Client2021/content/textures/ViewSelector/top_hover.png new file mode 100644 index 0000000..5b35738 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/top_hover.png differ diff --git a/Client2021/content/textures/ViewSelector/top_hover_zh_cn.png b/Client2021/content/textures/ViewSelector/top_hover_zh_cn.png new file mode 100644 index 0000000..083cd90 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/top_hover_zh_cn.png differ diff --git a/Client2021/content/textures/ViewSelector/top_zh_cn.png b/Client2021/content/textures/ViewSelector/top_zh_cn.png new file mode 100644 index 0000000..b582f11 Binary files /dev/null and b/Client2021/content/textures/ViewSelector/top_zh_cn.png differ diff --git a/Client2021/content/textures/WeldCursor.png b/Client2021/content/textures/WeldCursor.png new file mode 100644 index 0000000..a686be3 Binary files /dev/null and b/Client2021/content/textures/WeldCursor.png differ diff --git a/Client2021/content/textures/advClosed-hand-anchored.png b/Client2021/content/textures/advClosed-hand-anchored.png new file mode 100644 index 0000000..1d71af1 Binary files /dev/null and b/Client2021/content/textures/advClosed-hand-anchored.png differ diff --git a/Client2021/content/textures/advClosed-hand-no-weld.png b/Client2021/content/textures/advClosed-hand-no-weld.png new file mode 100644 index 0000000..d914399 Binary files /dev/null and b/Client2021/content/textures/advClosed-hand-no-weld.png differ diff --git a/Client2021/content/textures/advClosed-hand-weld.png b/Client2021/content/textures/advClosed-hand-weld.png new file mode 100644 index 0000000..2a8e734 Binary files /dev/null and b/Client2021/content/textures/advClosed-hand-weld.png differ diff --git a/Client2021/content/textures/advClosed-hand.png b/Client2021/content/textures/advClosed-hand.png new file mode 100644 index 0000000..1adb82f Binary files /dev/null and b/Client2021/content/textures/advClosed-hand.png differ diff --git a/Client2021/content/textures/advCursor-default.png b/Client2021/content/textures/advCursor-default.png new file mode 100644 index 0000000..289c415 Binary files /dev/null and b/Client2021/content/textures/advCursor-default.png differ diff --git a/Client2021/content/textures/advCursor-openedHand.png b/Client2021/content/textures/advCursor-openedHand.png new file mode 100644 index 0000000..1aea61b Binary files /dev/null and b/Client2021/content/textures/advCursor-openedHand.png differ diff --git a/Client2021/content/textures/advCursor-white.png b/Client2021/content/textures/advCursor-white.png new file mode 100644 index 0000000..0ec7adc Binary files /dev/null and b/Client2021/content/textures/advCursor-white.png differ diff --git a/Client2021/content/textures/advancedMove.png b/Client2021/content/textures/advancedMove.png new file mode 100644 index 0000000..8621f0f Binary files /dev/null and b/Client2021/content/textures/advancedMove.png differ diff --git a/Client2021/content/textures/advancedMoveResize.png b/Client2021/content/textures/advancedMoveResize.png new file mode 100644 index 0000000..a7421be Binary files /dev/null and b/Client2021/content/textures/advancedMoveResize.png differ diff --git a/Client2021/content/textures/advancedMove_joint.png b/Client2021/content/textures/advancedMove_joint.png new file mode 100644 index 0000000..76f7f0a Binary files /dev/null and b/Client2021/content/textures/advancedMove_joint.png differ diff --git a/Client2021/content/textures/advancedMove_keysOnly.png b/Client2021/content/textures/advancedMove_keysOnly.png new file mode 100644 index 0000000..1b55927 Binary files /dev/null and b/Client2021/content/textures/advancedMove_keysOnly.png differ diff --git a/Client2021/content/textures/advancedMove_noJoint.png b/Client2021/content/textures/advancedMove_noJoint.png new file mode 100644 index 0000000..6066608 Binary files /dev/null and b/Client2021/content/textures/advancedMove_noJoint.png differ diff --git a/Client2021/content/textures/blackBkg_round.png b/Client2021/content/textures/blackBkg_round.png new file mode 100644 index 0000000..2796afa Binary files /dev/null and b/Client2021/content/textures/blackBkg_round.png differ diff --git a/Client2021/content/textures/blackBkg_square.png b/Client2021/content/textures/blackBkg_square.png new file mode 100644 index 0000000..0382ce3 Binary files /dev/null and b/Client2021/content/textures/blackBkg_square.png differ diff --git a/Client2021/content/textures/blockUpperLeft.png b/Client2021/content/textures/blockUpperLeft.png new file mode 100644 index 0000000..93a8190 Binary files /dev/null and b/Client2021/content/textures/blockUpperLeft.png differ diff --git a/Client2021/content/textures/chatBubble_bot_notifyGray_dotDotDot.png b/Client2021/content/textures/chatBubble_bot_notifyGray_dotDotDot.png new file mode 100644 index 0000000..cd76f6f Binary files /dev/null and b/Client2021/content/textures/chatBubble_bot_notifyGray_dotDotDot.png differ diff --git a/Client2021/content/textures/collapsibleArrowDown.png b/Client2021/content/textures/collapsibleArrowDown.png new file mode 100644 index 0000000..9224923 Binary files /dev/null and b/Client2021/content/textures/collapsibleArrowDown.png differ diff --git a/Client2021/content/textures/collapsibleArrowRight.png b/Client2021/content/textures/collapsibleArrowRight.png new file mode 100644 index 0000000..759dcd4 Binary files /dev/null and b/Client2021/content/textures/collapsibleArrowRight.png differ diff --git a/Client2021/content/textures/explosion.png b/Client2021/content/textures/explosion.png new file mode 100644 index 0000000..5c5787d Binary files /dev/null and b/Client2021/content/textures/explosion.png differ diff --git a/Client2021/content/textures/face.png b/Client2021/content/textures/face.png new file mode 100644 index 0000000..08254c0 Binary files /dev/null and b/Client2021/content/textures/face.png differ diff --git a/Client2021/content/textures/glow.png b/Client2021/content/textures/glow.png new file mode 100644 index 0000000..886de92 Binary files /dev/null and b/Client2021/content/textures/glow.png differ diff --git a/Client2021/content/textures/gradient.png b/Client2021/content/textures/gradient.png new file mode 100644 index 0000000..540d93a Binary files /dev/null and b/Client2021/content/textures/gradient.png differ diff --git a/Client2021/content/textures/grid16.png b/Client2021/content/textures/grid16.png new file mode 100644 index 0000000..4c17cb0 Binary files /dev/null and b/Client2021/content/textures/grid16.png differ diff --git a/Client2021/content/textures/grid2.png b/Client2021/content/textures/grid2.png new file mode 100644 index 0000000..c9379c8 Binary files /dev/null and b/Client2021/content/textures/grid2.png differ diff --git a/Client2021/content/textures/grid4.png b/Client2021/content/textures/grid4.png new file mode 100644 index 0000000..55d88e3 Binary files /dev/null and b/Client2021/content/textures/grid4.png differ diff --git a/Client2021/content/textures/icon_ROBUX.png b/Client2021/content/textures/icon_ROBUX.png new file mode 100644 index 0000000..9e5c0f8 Binary files /dev/null and b/Client2021/content/textures/icon_ROBUX.png differ diff --git a/Client2021/content/textures/icon_ROBUX@2x.png b/Client2021/content/textures/icon_ROBUX@2x.png new file mode 100644 index 0000000..0b2a791 Binary files /dev/null and b/Client2021/content/textures/icon_ROBUX@2x.png differ diff --git a/Client2021/content/textures/loading/cancelButton.png b/Client2021/content/textures/loading/cancelButton.png new file mode 100644 index 0000000..77e6a39 Binary files /dev/null and b/Client2021/content/textures/loading/cancelButton.png differ diff --git a/Client2021/content/textures/loading/darkLoadingTexture.png b/Client2021/content/textures/loading/darkLoadingTexture.png new file mode 100644 index 0000000..90a0910 Binary files /dev/null and b/Client2021/content/textures/loading/darkLoadingTexture.png differ diff --git a/Client2021/content/textures/loading/loadingCircle.png b/Client2021/content/textures/loading/loadingCircle.png new file mode 100644 index 0000000..6788c78 Binary files /dev/null and b/Client2021/content/textures/loading/loadingCircle.png differ diff --git a/Client2021/content/textures/loading/loadingTexture.png b/Client2021/content/textures/loading/loadingTexture.png new file mode 100644 index 0000000..b58f420 Binary files /dev/null and b/Client2021/content/textures/loading/loadingTexture.png differ diff --git a/Client2021/content/textures/loading/loadingvignette.png b/Client2021/content/textures/loading/loadingvignette.png new file mode 100644 index 0000000..5952539 Binary files /dev/null and b/Client2021/content/textures/loading/loadingvignette.png differ diff --git a/Client2021/content/textures/loading/robloxTilt.png b/Client2021/content/textures/loading/robloxTilt.png new file mode 100644 index 0000000..068fe31 Binary files /dev/null and b/Client2021/content/textures/loading/robloxTilt.png differ diff --git a/Client2021/content/textures/loading/robloxTiltRed.png b/Client2021/content/textures/loading/robloxTiltRed.png new file mode 100644 index 0000000..8cfc292 Binary files /dev/null and b/Client2021/content/textures/loading/robloxTiltRed.png differ diff --git a/Client2021/content/textures/loading/robloxlogo.png b/Client2021/content/textures/loading/robloxlogo.png new file mode 100644 index 0000000..2d05445 Binary files /dev/null and b/Client2021/content/textures/loading/robloxlogo.png differ diff --git a/Client2021/content/textures/localizationExport.png b/Client2021/content/textures/localizationExport.png new file mode 100644 index 0000000..591c2c9 Binary files /dev/null and b/Client2021/content/textures/localizationExport.png differ diff --git a/Client2021/content/textures/localizationImport.png b/Client2021/content/textures/localizationImport.png new file mode 100644 index 0000000..10dda0e Binary files /dev/null and b/Client2021/content/textures/localizationImport.png differ diff --git a/Client2021/content/textures/localizationTargetEnglish.png b/Client2021/content/textures/localizationTargetEnglish.png new file mode 100644 index 0000000..5d528f1 Binary files /dev/null and b/Client2021/content/textures/localizationTargetEnglish.png differ diff --git a/Client2021/content/textures/localizationTargetSpanish.png b/Client2021/content/textures/localizationTargetSpanish.png new file mode 100644 index 0000000..34e1db9 Binary files /dev/null and b/Client2021/content/textures/localizationTargetSpanish.png differ diff --git a/Client2021/content/textures/localizationTestingIcon.png b/Client2021/content/textures/localizationTestingIcon.png new file mode 100644 index 0000000..dad16bd Binary files /dev/null and b/Client2021/content/textures/localizationTestingIcon.png differ diff --git a/Client2021/content/textures/localizationUIScrapingOff.png b/Client2021/content/textures/localizationUIScrapingOff.png new file mode 100644 index 0000000..ca3fc50 Binary files /dev/null and b/Client2021/content/textures/localizationUIScrapingOff.png differ diff --git a/Client2021/content/textures/localizationUIScrapingOn.png b/Client2021/content/textures/localizationUIScrapingOn.png new file mode 100644 index 0000000..e034998 Binary files /dev/null and b/Client2021/content/textures/localizationUIScrapingOn.png differ diff --git a/Client2021/content/textures/menuDownArrow.png b/Client2021/content/textures/menuDownArrow.png new file mode 100644 index 0000000..286ecd8 Binary files /dev/null and b/Client2021/content/textures/menuDownArrow.png differ diff --git a/Client2021/content/textures/meshPartFallback.png b/Client2021/content/textures/meshPartFallback.png new file mode 100644 index 0000000..1c1e179 Binary files /dev/null and b/Client2021/content/textures/meshPartFallback.png differ diff --git a/Client2021/content/textures/particles/SquareParticle.png b/Client2021/content/textures/particles/SquareParticle.png new file mode 100644 index 0000000..6c9aca8 Binary files /dev/null and b/Client2021/content/textures/particles/SquareParticle.png differ diff --git a/Client2021/content/textures/particles/common_alpha.dds b/Client2021/content/textures/particles/common_alpha.dds new file mode 100644 index 0000000..2cc9c9c Binary files /dev/null and b/Client2021/content/textures/particles/common_alpha.dds differ diff --git a/Client2021/content/textures/particles/explosion01_core_alpha.png b/Client2021/content/textures/particles/explosion01_core_alpha.png new file mode 100644 index 0000000..47ae484 Binary files /dev/null and b/Client2021/content/textures/particles/explosion01_core_alpha.png differ diff --git a/Client2021/content/textures/particles/explosion01_core_main.dds b/Client2021/content/textures/particles/explosion01_core_main.dds new file mode 100644 index 0000000..44fd908 Binary files /dev/null and b/Client2021/content/textures/particles/explosion01_core_main.dds differ diff --git a/Client2021/content/textures/particles/explosion01_implosion_color.png b/Client2021/content/textures/particles/explosion01_implosion_color.png new file mode 100644 index 0000000..2f1e9ed Binary files /dev/null and b/Client2021/content/textures/particles/explosion01_implosion_color.png differ diff --git a/Client2021/content/textures/particles/explosion01_implosion_main.dds b/Client2021/content/textures/particles/explosion01_implosion_main.dds new file mode 100644 index 0000000..a5bf1b5 Binary files /dev/null and b/Client2021/content/textures/particles/explosion01_implosion_main.dds differ diff --git a/Client2021/content/textures/particles/explosion01_shockwave_main.dds b/Client2021/content/textures/particles/explosion01_shockwave_main.dds new file mode 100644 index 0000000..7a30ca6 Binary files /dev/null and b/Client2021/content/textures/particles/explosion01_shockwave_main.dds differ diff --git a/Client2021/content/textures/particles/explosion01_smoke_alpha.dds b/Client2021/content/textures/particles/explosion01_smoke_alpha.dds new file mode 100644 index 0000000..99807c7 Binary files /dev/null and b/Client2021/content/textures/particles/explosion01_smoke_alpha.dds differ diff --git a/Client2021/content/textures/particles/explosion01_smoke_color_new.dds b/Client2021/content/textures/particles/explosion01_smoke_color_new.dds new file mode 100644 index 0000000..fd4df8f Binary files /dev/null and b/Client2021/content/textures/particles/explosion01_smoke_color_new.dds differ diff --git a/Client2021/content/textures/particles/explosion01_smoke_main.dds b/Client2021/content/textures/particles/explosion01_smoke_main.dds new file mode 100644 index 0000000..c37f99a Binary files /dev/null and b/Client2021/content/textures/particles/explosion01_smoke_main.dds differ diff --git a/Client2021/content/textures/particles/explosion_alpha.dds b/Client2021/content/textures/particles/explosion_alpha.dds new file mode 100644 index 0000000..add095f Binary files /dev/null and b/Client2021/content/textures/particles/explosion_alpha.dds differ diff --git a/Client2021/content/textures/particles/explosion_color.dds b/Client2021/content/textures/particles/explosion_color.dds new file mode 100644 index 0000000..f69e2cc Binary files /dev/null and b/Client2021/content/textures/particles/explosion_color.dds differ diff --git a/Client2021/content/textures/particles/fire_alpha.dds b/Client2021/content/textures/particles/fire_alpha.dds new file mode 100644 index 0000000..812a506 Binary files /dev/null and b/Client2021/content/textures/particles/fire_alpha.dds differ diff --git a/Client2021/content/textures/particles/fire_color.dds b/Client2021/content/textures/particles/fire_color.dds new file mode 100644 index 0000000..5d569ad Binary files /dev/null and b/Client2021/content/textures/particles/fire_color.dds differ diff --git a/Client2021/content/textures/particles/fire_main.dds b/Client2021/content/textures/particles/fire_main.dds new file mode 100644 index 0000000..22a5049 Binary files /dev/null and b/Client2021/content/textures/particles/fire_main.dds differ diff --git a/Client2021/content/textures/particles/fire_sparks_color.dds b/Client2021/content/textures/particles/fire_sparks_color.dds new file mode 100644 index 0000000..8876db1 Binary files /dev/null and b/Client2021/content/textures/particles/fire_sparks_color.dds differ diff --git a/Client2021/content/textures/particles/fire_sparks_main.dds b/Client2021/content/textures/particles/fire_sparks_main.dds new file mode 100644 index 0000000..4468e5a Binary files /dev/null and b/Client2021/content/textures/particles/fire_sparks_main.dds differ diff --git a/Client2021/content/textures/particles/forcefield_alpha.dds b/Client2021/content/textures/particles/forcefield_alpha.dds new file mode 100644 index 0000000..03fdac0 Binary files /dev/null and b/Client2021/content/textures/particles/forcefield_alpha.dds differ diff --git a/Client2021/content/textures/particles/forcefield_glow_alpha.dds b/Client2021/content/textures/particles/forcefield_glow_alpha.dds new file mode 100644 index 0000000..326310f Binary files /dev/null and b/Client2021/content/textures/particles/forcefield_glow_alpha.dds differ diff --git a/Client2021/content/textures/particles/forcefield_glow_color.dds b/Client2021/content/textures/particles/forcefield_glow_color.dds new file mode 100644 index 0000000..39dfb8e Binary files /dev/null and b/Client2021/content/textures/particles/forcefield_glow_color.dds differ diff --git a/Client2021/content/textures/particles/forcefield_glow_main.dds b/Client2021/content/textures/particles/forcefield_glow_main.dds new file mode 100644 index 0000000..d1a6472 Binary files /dev/null and b/Client2021/content/textures/particles/forcefield_glow_main.dds differ diff --git a/Client2021/content/textures/particles/forcefield_vortex_color.dds b/Client2021/content/textures/particles/forcefield_vortex_color.dds new file mode 100644 index 0000000..fe33e22 Binary files /dev/null and b/Client2021/content/textures/particles/forcefield_vortex_color.dds differ diff --git a/Client2021/content/textures/particles/forcefield_vortex_main.dds b/Client2021/content/textures/particles/forcefield_vortex_main.dds new file mode 100644 index 0000000..d67cf49 Binary files /dev/null and b/Client2021/content/textures/particles/forcefield_vortex_main.dds differ diff --git a/Client2021/content/textures/particles/legacy_fire_alpha_color.dds b/Client2021/content/textures/particles/legacy_fire_alpha_color.dds new file mode 100644 index 0000000..da0fb05 Binary files /dev/null and b/Client2021/content/textures/particles/legacy_fire_alpha_color.dds differ diff --git a/Client2021/content/textures/particles/smoke_color.dds b/Client2021/content/textures/particles/smoke_color.dds new file mode 100644 index 0000000..a4d0a4f Binary files /dev/null and b/Client2021/content/textures/particles/smoke_color.dds differ diff --git a/Client2021/content/textures/particles/smoke_main.dds b/Client2021/content/textures/particles/smoke_main.dds new file mode 100644 index 0000000..1aaef8d Binary files /dev/null and b/Client2021/content/textures/particles/smoke_main.dds differ diff --git a/Client2021/content/textures/particles/sparkles_color.dds b/Client2021/content/textures/particles/sparkles_color.dds new file mode 100644 index 0000000..4fd1f85 Binary files /dev/null and b/Client2021/content/textures/particles/sparkles_color.dds differ diff --git a/Client2021/content/textures/particles/sparkles_main.dds b/Client2021/content/textures/particles/sparkles_main.dds new file mode 100644 index 0000000..d1875fd Binary files /dev/null and b/Client2021/content/textures/particles/sparkles_main.dds differ diff --git a/Client2021/content/textures/rotationArrow.png b/Client2021/content/textures/rotationArrow.png new file mode 100644 index 0000000..5bd9eb5 Binary files /dev/null and b/Client2021/content/textures/rotationArrow.png differ diff --git a/Client2021/content/textures/shadowblurmask.png b/Client2021/content/textures/shadowblurmask.png new file mode 100644 index 0000000..ae33768 Binary files /dev/null and b/Client2021/content/textures/shadowblurmask.png differ diff --git a/Client2021/content/textures/sparkle.png b/Client2021/content/textures/sparkle.png new file mode 100644 index 0000000..510b061 Binary files /dev/null and b/Client2021/content/textures/sparkle.png differ diff --git a/Client2021/content/textures/transformFiveDegrees.png b/Client2021/content/textures/transformFiveDegrees.png new file mode 100644 index 0000000..1201cea Binary files /dev/null and b/Client2021/content/textures/transformFiveDegrees.png differ diff --git a/Client2021/content/textures/transformNinetyDegrees.png b/Client2021/content/textures/transformNinetyDegrees.png new file mode 100644 index 0000000..253ad36 Binary files /dev/null and b/Client2021/content/textures/transformNinetyDegrees.png differ diff --git a/Client2021/content/textures/transformOneDegree.png b/Client2021/content/textures/transformOneDegree.png new file mode 100644 index 0000000..79cb61f Binary files /dev/null and b/Client2021/content/textures/transformOneDegree.png differ diff --git a/Client2021/content/textures/transformTwentyTwoDegrees.png b/Client2021/content/textures/transformTwentyTwoDegrees.png new file mode 100644 index 0000000..556b024 Binary files /dev/null and b/Client2021/content/textures/transformTwentyTwoDegrees.png differ diff --git a/Client2021/content/textures/ui/AvatarContextMenu_Arrow.png b/Client2021/content/textures/ui/AvatarContextMenu_Arrow.png new file mode 100644 index 0000000..d41d3c7 Binary files /dev/null and b/Client2021/content/textures/ui/AvatarContextMenu_Arrow.png differ diff --git a/Client2021/content/textures/ui/Backpack/Backpack.png b/Client2021/content/textures/ui/Backpack/Backpack.png new file mode 100644 index 0000000..8d9048a Binary files /dev/null and b/Client2021/content/textures/ui/Backpack/Backpack.png differ diff --git a/Client2021/content/textures/ui/Backpack/Backpack@2x.png b/Client2021/content/textures/ui/Backpack/Backpack@2x.png new file mode 100644 index 0000000..9814297 Binary files /dev/null and b/Client2021/content/textures/ui/Backpack/Backpack@2x.png differ diff --git a/Client2021/content/textures/ui/Backpack/Backpack_Down.png b/Client2021/content/textures/ui/Backpack/Backpack_Down.png new file mode 100644 index 0000000..b687a19 Binary files /dev/null and b/Client2021/content/textures/ui/Backpack/Backpack_Down.png differ diff --git a/Client2021/content/textures/ui/Backpack/Backpack_Down@2x.png b/Client2021/content/textures/ui/Backpack/Backpack_Down@2x.png new file mode 100644 index 0000000..5c46d4a Binary files /dev/null and b/Client2021/content/textures/ui/Backpack/Backpack_Down@2x.png differ diff --git a/Client2021/content/textures/ui/Backpack/ScrollDownArrow.png b/Client2021/content/textures/ui/Backpack/ScrollDownArrow.png new file mode 100644 index 0000000..68e14aa Binary files /dev/null and b/Client2021/content/textures/ui/Backpack/ScrollDownArrow.png differ diff --git a/Client2021/content/textures/ui/Backpack/ScrollUpArrow.png b/Client2021/content/textures/ui/Backpack/ScrollUpArrow.png new file mode 100644 index 0000000..3824e05 Binary files /dev/null and b/Client2021/content/textures/ui/Backpack/ScrollUpArrow.png differ diff --git a/Client2021/content/textures/ui/Backpack_Close.png b/Client2021/content/textures/ui/Backpack_Close.png new file mode 100644 index 0000000..2b3679b Binary files /dev/null and b/Client2021/content/textures/ui/Backpack_Close.png differ diff --git a/Client2021/content/textures/ui/Backpack_Close@2x.png b/Client2021/content/textures/ui/Backpack_Close@2x.png new file mode 100644 index 0000000..1716f55 Binary files /dev/null and b/Client2021/content/textures/ui/Backpack_Close@2x.png differ diff --git a/Client2021/content/textures/ui/Backpack_Open.png b/Client2021/content/textures/ui/Backpack_Open.png new file mode 100644 index 0000000..24fba15 Binary files /dev/null and b/Client2021/content/textures/ui/Backpack_Open.png differ diff --git a/Client2021/content/textures/ui/Backpack_Open@2x.png b/Client2021/content/textures/ui/Backpack_Open@2x.png new file mode 100644 index 0000000..a56634c Binary files /dev/null and b/Client2021/content/textures/ui/Backpack_Open@2x.png differ diff --git a/Client2021/content/textures/ui/BottomRoundedRect8px.png b/Client2021/content/textures/ui/BottomRoundedRect8px.png new file mode 100644 index 0000000..6031e36 Binary files /dev/null and b/Client2021/content/textures/ui/BottomRoundedRect8px.png differ diff --git a/Client2021/content/textures/ui/ButtonLeft.png b/Client2021/content/textures/ui/ButtonLeft.png new file mode 100644 index 0000000..f2654bd Binary files /dev/null and b/Client2021/content/textures/ui/ButtonLeft.png differ diff --git a/Client2021/content/textures/ui/ButtonLeftDown.png b/Client2021/content/textures/ui/ButtonLeftDown.png new file mode 100644 index 0000000..67cdc7f Binary files /dev/null and b/Client2021/content/textures/ui/ButtonLeftDown.png differ diff --git a/Client2021/content/textures/ui/ButtonRight.png b/Client2021/content/textures/ui/ButtonRight.png new file mode 100644 index 0000000..8defe2e Binary files /dev/null and b/Client2021/content/textures/ui/ButtonRight.png differ diff --git a/Client2021/content/textures/ui/ButtonRightDown.png b/Client2021/content/textures/ui/ButtonRightDown.png new file mode 100644 index 0000000..218f2d2 Binary files /dev/null and b/Client2021/content/textures/ui/ButtonRightDown.png differ diff --git a/Client2021/content/textures/ui/Camera/CameraToast9Slice.png b/Client2021/content/textures/ui/Camera/CameraToast9Slice.png new file mode 100644 index 0000000..a24ad33 Binary files /dev/null and b/Client2021/content/textures/ui/Camera/CameraToast9Slice.png differ diff --git a/Client2021/content/textures/ui/Camera/CameraToastIcon.png b/Client2021/content/textures/ui/Camera/CameraToastIcon.png new file mode 100644 index 0000000..4920cbc Binary files /dev/null and b/Client2021/content/textures/ui/Camera/CameraToastIcon.png differ diff --git a/Client2021/content/textures/ui/Chat/Chat.png b/Client2021/content/textures/ui/Chat/Chat.png new file mode 100644 index 0000000..d42e4ea Binary files /dev/null and b/Client2021/content/textures/ui/Chat/Chat.png differ diff --git a/Client2021/content/textures/ui/Chat/Chat@2x.png b/Client2021/content/textures/ui/Chat/Chat@2x.png new file mode 100644 index 0000000..57ea151 Binary files /dev/null and b/Client2021/content/textures/ui/Chat/Chat@2x.png differ diff --git a/Client2021/content/textures/ui/Chat/ChatDown.png b/Client2021/content/textures/ui/Chat/ChatDown.png new file mode 100644 index 0000000..002119b Binary files /dev/null and b/Client2021/content/textures/ui/Chat/ChatDown.png differ diff --git a/Client2021/content/textures/ui/Chat/ChatDown@2x.png b/Client2021/content/textures/ui/Chat/ChatDown@2x.png new file mode 100644 index 0000000..f25f80d Binary files /dev/null and b/Client2021/content/textures/ui/Chat/ChatDown@2x.png differ diff --git a/Client2021/content/textures/ui/Chat/ChatDownFlip.png b/Client2021/content/textures/ui/Chat/ChatDownFlip.png new file mode 100644 index 0000000..9523d39 Binary files /dev/null and b/Client2021/content/textures/ui/Chat/ChatDownFlip.png differ diff --git a/Client2021/content/textures/ui/Chat/ChatDownFlip@2x.png b/Client2021/content/textures/ui/Chat/ChatDownFlip@2x.png new file mode 100644 index 0000000..dd6b15e Binary files /dev/null and b/Client2021/content/textures/ui/Chat/ChatDownFlip@2x.png differ diff --git a/Client2021/content/textures/ui/Chat/ChatFlip.png b/Client2021/content/textures/ui/Chat/ChatFlip.png new file mode 100644 index 0000000..f0b508a Binary files /dev/null and b/Client2021/content/textures/ui/Chat/ChatFlip.png differ diff --git a/Client2021/content/textures/ui/Chat/ChatFlip@2x.png b/Client2021/content/textures/ui/Chat/ChatFlip@2x.png new file mode 100644 index 0000000..1e8d6d1 Binary files /dev/null and b/Client2021/content/textures/ui/Chat/ChatFlip@2x.png differ diff --git a/Client2021/content/textures/ui/Chat/MessageCounter.png b/Client2021/content/textures/ui/Chat/MessageCounter.png new file mode 100644 index 0000000..4ba3cbd Binary files /dev/null and b/Client2021/content/textures/ui/Chat/MessageCounter.png differ diff --git a/Client2021/content/textures/ui/Chat/MessageCounter@2x.png b/Client2021/content/textures/ui/Chat/MessageCounter@2x.png new file mode 100644 index 0000000..de2cc47 Binary files /dev/null and b/Client2021/content/textures/ui/Chat/MessageCounter@2x.png differ diff --git a/Client2021/content/textures/ui/Chat/ToggleChat.png b/Client2021/content/textures/ui/Chat/ToggleChat.png new file mode 100644 index 0000000..1bc7450 Binary files /dev/null and b/Client2021/content/textures/ui/Chat/ToggleChat.png differ diff --git a/Client2021/content/textures/ui/Chat/ToggleChat@2x.png b/Client2021/content/textures/ui/Chat/ToggleChat@2x.png new file mode 100644 index 0000000..5ab133e Binary files /dev/null and b/Client2021/content/textures/ui/Chat/ToggleChat@2x.png differ diff --git a/Client2021/content/textures/ui/Chat/ToggleChatDown.png b/Client2021/content/textures/ui/Chat/ToggleChatDown.png new file mode 100644 index 0000000..2956a50 Binary files /dev/null and b/Client2021/content/textures/ui/Chat/ToggleChatDown.png differ diff --git a/Client2021/content/textures/ui/Chat/ToggleChatDown@2x.png b/Client2021/content/textures/ui/Chat/ToggleChatDown@2x.png new file mode 100644 index 0000000..065be38 Binary files /dev/null and b/Client2021/content/textures/ui/Chat/ToggleChatDown@2x.png differ diff --git a/Client2021/content/textures/ui/Chat/ToggleChatDownFlip.png b/Client2021/content/textures/ui/Chat/ToggleChatDownFlip.png new file mode 100644 index 0000000..34d7376 Binary files /dev/null and b/Client2021/content/textures/ui/Chat/ToggleChatDownFlip.png differ diff --git a/Client2021/content/textures/ui/Chat/ToggleChatDownFlip@2x.png b/Client2021/content/textures/ui/Chat/ToggleChatDownFlip@2x.png new file mode 100644 index 0000000..c9a6dff Binary files /dev/null and b/Client2021/content/textures/ui/Chat/ToggleChatDownFlip@2x.png differ diff --git a/Client2021/content/textures/ui/Chat/ToggleChatFlip.png b/Client2021/content/textures/ui/Chat/ToggleChatFlip.png new file mode 100644 index 0000000..e929cfa Binary files /dev/null and b/Client2021/content/textures/ui/Chat/ToggleChatFlip.png differ diff --git a/Client2021/content/textures/ui/Chat/ToggleChatFlip@2x.png b/Client2021/content/textures/ui/Chat/ToggleChatFlip@2x.png new file mode 100644 index 0000000..1d02265 Binary files /dev/null and b/Client2021/content/textures/ui/Chat/ToggleChatFlip@2x.png differ diff --git a/Client2021/content/textures/ui/Chat/VRChatBackground.png b/Client2021/content/textures/ui/Chat/VRChatBackground.png new file mode 100644 index 0000000..694a50f Binary files /dev/null and b/Client2021/content/textures/ui/Chat/VRChatBackground.png differ diff --git a/Client2021/content/textures/ui/CloseButton.png b/Client2021/content/textures/ui/CloseButton.png new file mode 100644 index 0000000..6ba8d7b Binary files /dev/null and b/Client2021/content/textures/ui/CloseButton.png differ diff --git a/Client2021/content/textures/ui/CloseButton_dn.png b/Client2021/content/textures/ui/CloseButton_dn.png new file mode 100644 index 0000000..ee4b9ff Binary files /dev/null and b/Client2021/content/textures/ui/CloseButton_dn.png differ diff --git a/Client2021/content/textures/ui/Controls/RadialFill.png b/Client2021/content/textures/ui/Controls/RadialFill.png new file mode 100644 index 0000000..c2ded74 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/RadialFill.png differ diff --git a/Client2021/content/textures/ui/Controls/RadialFill@2x.png b/Client2021/content/textures/ui/Controls/RadialFill@2x.png new file mode 100644 index 0000000..c131cc1 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/RadialFill@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/RadialFill@3x.png b/Client2021/content/textures/ui/Controls/RadialFill@3x.png new file mode 100644 index 0000000..3747a35 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/RadialFill@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/TouchTapIcon.png b/Client2021/content/textures/ui/Controls/TouchTapIcon.png new file mode 100644 index 0000000..5d6c71d Binary files /dev/null and b/Client2021/content/textures/ui/Controls/TouchTapIcon.png differ diff --git a/Client2021/content/textures/ui/Controls/TouchTapIcon@2x.png b/Client2021/content/textures/ui/Controls/TouchTapIcon@2x.png new file mode 100644 index 0000000..60da2a0 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/TouchTapIcon@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/TouchTapIcon@3x.png b/Client2021/content/textures/ui/Controls/TouchTapIcon@3x.png new file mode 100644 index 0000000..b74821a Binary files /dev/null and b/Client2021/content/textures/ui/Controls/TouchTapIcon@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/apostrophe.png b/Client2021/content/textures/ui/Controls/apostrophe.png new file mode 100644 index 0000000..1cbcec5 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/apostrophe.png differ diff --git a/Client2021/content/textures/ui/Controls/apostrophe@2x.png b/Client2021/content/textures/ui/Controls/apostrophe@2x.png new file mode 100644 index 0000000..aa9885c Binary files /dev/null and b/Client2021/content/textures/ui/Controls/apostrophe@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/apostrophe@3x.png b/Client2021/content/textures/ui/Controls/apostrophe@3x.png new file mode 100644 index 0000000..0c36385 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/apostrophe@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/backspace.png b/Client2021/content/textures/ui/Controls/backspace.png new file mode 100644 index 0000000..89a9dba Binary files /dev/null and b/Client2021/content/textures/ui/Controls/backspace.png differ diff --git a/Client2021/content/textures/ui/Controls/backspace@2x.png b/Client2021/content/textures/ui/Controls/backspace@2x.png new file mode 100644 index 0000000..e2da463 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/backspace@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/backspace@3x.png b/Client2021/content/textures/ui/Controls/backspace@3x.png new file mode 100644 index 0000000..eff780c Binary files /dev/null and b/Client2021/content/textures/ui/Controls/backspace@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/comma.png b/Client2021/content/textures/ui/Controls/comma.png new file mode 100644 index 0000000..cb40735 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/comma.png differ diff --git a/Client2021/content/textures/ui/Controls/comma@2x.png b/Client2021/content/textures/ui/Controls/comma@2x.png new file mode 100644 index 0000000..8b519e1 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/comma@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/comma@3x.png b/Client2021/content/textures/ui/Controls/comma@3x.png new file mode 100644 index 0000000..48115b1 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/comma@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/command.png b/Client2021/content/textures/ui/Controls/command.png new file mode 100644 index 0000000..1023e65 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/command.png differ diff --git a/Client2021/content/textures/ui/Controls/command@2x.png b/Client2021/content/textures/ui/Controls/command@2x.png new file mode 100644 index 0000000..6b443fa Binary files /dev/null and b/Client2021/content/textures/ui/Controls/command@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/command@3x.png b/Client2021/content/textures/ui/Controls/command@3x.png new file mode 100644 index 0000000..8de5d78 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/command@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/dpadDown.png b/Client2021/content/textures/ui/Controls/dpadDown.png new file mode 100644 index 0000000..57d188d Binary files /dev/null and b/Client2021/content/textures/ui/Controls/dpadDown.png differ diff --git a/Client2021/content/textures/ui/Controls/dpadDown@2x.png b/Client2021/content/textures/ui/Controls/dpadDown@2x.png new file mode 100644 index 0000000..171579d Binary files /dev/null and b/Client2021/content/textures/ui/Controls/dpadDown@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/dpadDown@3x.png b/Client2021/content/textures/ui/Controls/dpadDown@3x.png new file mode 100644 index 0000000..d19e0f6 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/dpadDown@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/dpadLeft.png b/Client2021/content/textures/ui/Controls/dpadLeft.png new file mode 100644 index 0000000..2b6e0f4 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/dpadLeft.png differ diff --git a/Client2021/content/textures/ui/Controls/dpadLeft@2x.png b/Client2021/content/textures/ui/Controls/dpadLeft@2x.png new file mode 100644 index 0000000..98cc113 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/dpadLeft@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/dpadLeft@3x.png b/Client2021/content/textures/ui/Controls/dpadLeft@3x.png new file mode 100644 index 0000000..83e5cad Binary files /dev/null and b/Client2021/content/textures/ui/Controls/dpadLeft@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/dpadRight.png b/Client2021/content/textures/ui/Controls/dpadRight.png new file mode 100644 index 0000000..31ac318 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/dpadRight.png differ diff --git a/Client2021/content/textures/ui/Controls/dpadRight@2x.png b/Client2021/content/textures/ui/Controls/dpadRight@2x.png new file mode 100644 index 0000000..a5e65b8 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/dpadRight@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/dpadRight@3x.png b/Client2021/content/textures/ui/Controls/dpadRight@3x.png new file mode 100644 index 0000000..1d3420c Binary files /dev/null and b/Client2021/content/textures/ui/Controls/dpadRight@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/dpadUp.png b/Client2021/content/textures/ui/Controls/dpadUp.png new file mode 100644 index 0000000..5c88e8a Binary files /dev/null and b/Client2021/content/textures/ui/Controls/dpadUp.png differ diff --git a/Client2021/content/textures/ui/Controls/dpadUp@2x.png b/Client2021/content/textures/ui/Controls/dpadUp@2x.png new file mode 100644 index 0000000..1f9aad1 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/dpadUp@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/dpadUp@3x.png b/Client2021/content/textures/ui/Controls/dpadUp@3x.png new file mode 100644 index 0000000..69ff4b8 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/dpadUp@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/graveaccent.png b/Client2021/content/textures/ui/Controls/graveaccent.png new file mode 100644 index 0000000..854ac6a Binary files /dev/null and b/Client2021/content/textures/ui/Controls/graveaccent.png differ diff --git a/Client2021/content/textures/ui/Controls/graveaccent@2x.png b/Client2021/content/textures/ui/Controls/graveaccent@2x.png new file mode 100644 index 0000000..83b9b76 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/graveaccent@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/graveaccent@3x.png b/Client2021/content/textures/ui/Controls/graveaccent@3x.png new file mode 100644 index 0000000..d32bb06 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/graveaccent@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/key_single.png b/Client2021/content/textures/ui/Controls/key_single.png new file mode 100644 index 0000000..edff63d Binary files /dev/null and b/Client2021/content/textures/ui/Controls/key_single.png differ diff --git a/Client2021/content/textures/ui/Controls/key_single@2x.png b/Client2021/content/textures/ui/Controls/key_single@2x.png new file mode 100644 index 0000000..f487e5f Binary files /dev/null and b/Client2021/content/textures/ui/Controls/key_single@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/key_single@3x.png b/Client2021/content/textures/ui/Controls/key_single@3x.png new file mode 100644 index 0000000..da72448 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/key_single@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/option.png b/Client2021/content/textures/ui/Controls/option.png new file mode 100644 index 0000000..65eb580 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/option.png differ diff --git a/Client2021/content/textures/ui/Controls/option@2x.png b/Client2021/content/textures/ui/Controls/option@2x.png new file mode 100644 index 0000000..be2f81d Binary files /dev/null and b/Client2021/content/textures/ui/Controls/option@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/option@3x.png b/Client2021/content/textures/ui/Controls/option@3x.png new file mode 100644 index 0000000..f8c0f63 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/option@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/period.png b/Client2021/content/textures/ui/Controls/period.png new file mode 100644 index 0000000..35d3cf1 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/period.png differ diff --git a/Client2021/content/textures/ui/Controls/period@2x.png b/Client2021/content/textures/ui/Controls/period@2x.png new file mode 100644 index 0000000..c5dfa5a Binary files /dev/null and b/Client2021/content/textures/ui/Controls/period@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/period@3x.png b/Client2021/content/textures/ui/Controls/period@3x.png new file mode 100644 index 0000000..18af97d Binary files /dev/null and b/Client2021/content/textures/ui/Controls/period@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/return.png b/Client2021/content/textures/ui/Controls/return.png new file mode 100644 index 0000000..858f905 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/return.png differ diff --git a/Client2021/content/textures/ui/Controls/return@2x.png b/Client2021/content/textures/ui/Controls/return@2x.png new file mode 100644 index 0000000..2f30873 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/return@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/return@3x.png b/Client2021/content/textures/ui/Controls/return@3x.png new file mode 100644 index 0000000..92e8022 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/return@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/shift.png b/Client2021/content/textures/ui/Controls/shift.png new file mode 100644 index 0000000..45b6973 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/shift.png differ diff --git a/Client2021/content/textures/ui/Controls/shift@2x.png b/Client2021/content/textures/ui/Controls/shift@2x.png new file mode 100644 index 0000000..812e648 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/shift@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/shift@3x.png b/Client2021/content/textures/ui/Controls/shift@3x.png new file mode 100644 index 0000000..7213354 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/shift@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/spacebar.png b/Client2021/content/textures/ui/Controls/spacebar.png new file mode 100644 index 0000000..c0c1dd3 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/spacebar.png differ diff --git a/Client2021/content/textures/ui/Controls/spacebar@2x.png b/Client2021/content/textures/ui/Controls/spacebar@2x.png new file mode 100644 index 0000000..23f4252 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/spacebar@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/spacebar@3x.png b/Client2021/content/textures/ui/Controls/spacebar@3x.png new file mode 100644 index 0000000..14564da Binary files /dev/null and b/Client2021/content/textures/ui/Controls/spacebar@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/tab.png b/Client2021/content/textures/ui/Controls/tab.png new file mode 100644 index 0000000..f6e4864 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/tab.png differ diff --git a/Client2021/content/textures/ui/Controls/tab@2x.png b/Client2021/content/textures/ui/Controls/tab@2x.png new file mode 100644 index 0000000..2ee5ecb Binary files /dev/null and b/Client2021/content/textures/ui/Controls/tab@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/tab@3x.png b/Client2021/content/textures/ui/Controls/tab@3x.png new file mode 100644 index 0000000..ecfba52 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/tab@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxA.png b/Client2021/content/textures/ui/Controls/xboxA.png new file mode 100644 index 0000000..6a90671 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxA.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxA@2x.png b/Client2021/content/textures/ui/Controls/xboxA@2x.png new file mode 100644 index 0000000..f624796 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxA@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxA@3x.png b/Client2021/content/textures/ui/Controls/xboxA@3x.png new file mode 100644 index 0000000..46db725 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxA@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxB.png b/Client2021/content/textures/ui/Controls/xboxB.png new file mode 100644 index 0000000..f5e5139 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxB.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxB@2x.png b/Client2021/content/textures/ui/Controls/xboxB@2x.png new file mode 100644 index 0000000..7523a98 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxB@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxB@3x.png b/Client2021/content/textures/ui/Controls/xboxB@3x.png new file mode 100644 index 0000000..7fe3327 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxB@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxLS.png b/Client2021/content/textures/ui/Controls/xboxLS.png new file mode 100644 index 0000000..dd6759f Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxLS.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxLS@2x.png b/Client2021/content/textures/ui/Controls/xboxLS@2x.png new file mode 100644 index 0000000..1b1ce97 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxLS@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxLS@3x.png b/Client2021/content/textures/ui/Controls/xboxLS@3x.png new file mode 100644 index 0000000..4997d96 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxLS@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxRS.png b/Client2021/content/textures/ui/Controls/xboxRS.png new file mode 100644 index 0000000..c7cbf0c Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxRS.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxRS@2x.png b/Client2021/content/textures/ui/Controls/xboxRS@2x.png new file mode 100644 index 0000000..7df063f Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxRS@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxRS@3x.png b/Client2021/content/textures/ui/Controls/xboxRS@3x.png new file mode 100644 index 0000000..5d0d2de Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxRS@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxX.png b/Client2021/content/textures/ui/Controls/xboxX.png new file mode 100644 index 0000000..7a1d412 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxX.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxX@2x.png b/Client2021/content/textures/ui/Controls/xboxX@2x.png new file mode 100644 index 0000000..02de28b Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxX@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxX@3x.png b/Client2021/content/textures/ui/Controls/xboxX@3x.png new file mode 100644 index 0000000..22ac4df Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxX@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxY.png b/Client2021/content/textures/ui/Controls/xboxY.png new file mode 100644 index 0000000..b905456 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxY.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxY@2x.png b/Client2021/content/textures/ui/Controls/xboxY@2x.png new file mode 100644 index 0000000..b221996 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxY@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxY@3x.png b/Client2021/content/textures/ui/Controls/xboxY@3x.png new file mode 100644 index 0000000..15bc891 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxY@3x.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxmenu.png b/Client2021/content/textures/ui/Controls/xboxmenu.png new file mode 100644 index 0000000..eb0b0c7 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxmenu.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxmenu@2x.png b/Client2021/content/textures/ui/Controls/xboxmenu@2x.png new file mode 100644 index 0000000..841ecfe Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxmenu@2x.png differ diff --git a/Client2021/content/textures/ui/Controls/xboxmenu@3x.png b/Client2021/content/textures/ui/Controls/xboxmenu@3x.png new file mode 100644 index 0000000..c9d5240 Binary files /dev/null and b/Client2021/content/textures/ui/Controls/xboxmenu@3x.png differ diff --git a/Client2021/content/textures/ui/DPadSheet.png b/Client2021/content/textures/ui/DPadSheet.png new file mode 100644 index 0000000..046e2e7 Binary files /dev/null and b/Client2021/content/textures/ui/DPadSheet.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/Large/OrangeHighlight.png b/Client2021/content/textures/ui/Emotes/Editor/Large/OrangeHighlight.png new file mode 100644 index 0000000..21d9db4 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/Large/OrangeHighlight.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/Large/OrangeHighlight@2x.png b/Client2021/content/textures/ui/Emotes/Editor/Large/OrangeHighlight@2x.png new file mode 100644 index 0000000..0270edc Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/Large/OrangeHighlight@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/Large/OrangeHighlight@3x.png b/Client2021/content/textures/ui/Emotes/Editor/Large/OrangeHighlight@3x.png new file mode 100644 index 0000000..fb3ad04 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/Large/OrangeHighlight@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/Large/Wheel.png b/Client2021/content/textures/ui/Emotes/Editor/Large/Wheel.png new file mode 100644 index 0000000..401a3e5 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/Large/Wheel.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/Large/Wheel@2x.png b/Client2021/content/textures/ui/Emotes/Editor/Large/Wheel@2x.png new file mode 100644 index 0000000..f5bde94 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/Large/Wheel@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/Large/Wheel@3x.png b/Client2021/content/textures/ui/Emotes/Editor/Large/Wheel@3x.png new file mode 100644 index 0000000..49b2b9a Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/Large/Wheel@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/Small/OrangeHighlight.png b/Client2021/content/textures/ui/Emotes/Editor/Small/OrangeHighlight.png new file mode 100644 index 0000000..b48d3e6 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/Small/OrangeHighlight.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/Small/OrangeHighlight@2x.png b/Client2021/content/textures/ui/Emotes/Editor/Small/OrangeHighlight@2x.png new file mode 100644 index 0000000..655b95f Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/Small/OrangeHighlight@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/Small/OrangeHighlight@3x.png b/Client2021/content/textures/ui/Emotes/Editor/Small/OrangeHighlight@3x.png new file mode 100644 index 0000000..27aa8da Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/Small/OrangeHighlight@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/Small/Wheel.png b/Client2021/content/textures/ui/Emotes/Editor/Small/Wheel.png new file mode 100644 index 0000000..83ff5fb Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/Small/Wheel.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/Small/Wheel@2x.png b/Client2021/content/textures/ui/Emotes/Editor/Small/Wheel@2x.png new file mode 100644 index 0000000..f03d5eb Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/Small/Wheel@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/Small/Wheel@3x.png b/Client2021/content/textures/ui/Emotes/Editor/Small/Wheel@3x.png new file mode 100644 index 0000000..928a2b4 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/Small/Wheel@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight.png b/Client2021/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight.png new file mode 100644 index 0000000..0ae5c24 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight@2x.png b/Client2021/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight@2x.png new file mode 100644 index 0000000..5b010b7 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight@3x.png b/Client2021/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight@3x.png new file mode 100644 index 0000000..d08d6a4 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/TenFoot/OrangeHighlight@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/TenFoot/Wheel.png b/Client2021/content/textures/ui/Emotes/Editor/TenFoot/Wheel.png new file mode 100644 index 0000000..73341a7 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/TenFoot/Wheel.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/TenFoot/Wheel@2x.png b/Client2021/content/textures/ui/Emotes/Editor/TenFoot/Wheel@2x.png new file mode 100644 index 0000000..23902e6 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/TenFoot/Wheel@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Editor/TenFoot/Wheel@3x.png b/Client2021/content/textures/ui/Emotes/Editor/TenFoot/Wheel@3x.png new file mode 100644 index 0000000..f998964 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Editor/TenFoot/Wheel@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/EmotesIcon.png b/Client2021/content/textures/ui/Emotes/EmotesIcon.png new file mode 100644 index 0000000..f052e6a Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/EmotesIcon.png differ diff --git a/Client2021/content/textures/ui/Emotes/EmotesIcon@2x.png b/Client2021/content/textures/ui/Emotes/EmotesIcon@2x.png new file mode 100644 index 0000000..a3ba2b8 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/EmotesIcon@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/EmotesIcon@3x.png b/Client2021/content/textures/ui/Emotes/EmotesIcon@3x.png new file mode 100644 index 0000000..4d673ad Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/EmotesIcon@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/EmotesRadialIcon.png b/Client2021/content/textures/ui/Emotes/EmotesRadialIcon.png new file mode 100644 index 0000000..b385307 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/EmotesRadialIcon.png differ diff --git a/Client2021/content/textures/ui/Emotes/EmotesRadialIcon@2x.png b/Client2021/content/textures/ui/Emotes/EmotesRadialIcon@2x.png new file mode 100644 index 0000000..930a201 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/EmotesRadialIcon@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/EmotesRadialIcon@3x.png b/Client2021/content/textures/ui/Emotes/EmotesRadialIcon@3x.png new file mode 100644 index 0000000..056cde1 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/EmotesRadialIcon@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/ErrorIcon.png b/Client2021/content/textures/ui/Emotes/ErrorIcon.png new file mode 100644 index 0000000..32ede95 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/ErrorIcon.png differ diff --git a/Client2021/content/textures/ui/Emotes/ErrorIcon@2x.png b/Client2021/content/textures/ui/Emotes/ErrorIcon@2x.png new file mode 100644 index 0000000..72b37d6 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/ErrorIcon@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/ErrorIcon@3x.png b/Client2021/content/textures/ui/Emotes/ErrorIcon@3x.png new file mode 100644 index 0000000..6107a79 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/ErrorIcon@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Large/CircleBackground.png b/Client2021/content/textures/ui/Emotes/Large/CircleBackground.png new file mode 100644 index 0000000..7cccc02 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Large/CircleBackground.png differ diff --git a/Client2021/content/textures/ui/Emotes/Large/CircleBackground@2x.png b/Client2021/content/textures/ui/Emotes/Large/CircleBackground@2x.png new file mode 100644 index 0000000..54756a6 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Large/CircleBackground@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Large/CircleBackground@3x.png b/Client2021/content/textures/ui/Emotes/Large/CircleBackground@3x.png new file mode 100644 index 0000000..2a5d729 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Large/CircleBackground@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Large/SegmentedCircle.png b/Client2021/content/textures/ui/Emotes/Large/SegmentedCircle.png new file mode 100644 index 0000000..c7352ed Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Large/SegmentedCircle.png differ diff --git a/Client2021/content/textures/ui/Emotes/Large/SegmentedCircle@2x.png b/Client2021/content/textures/ui/Emotes/Large/SegmentedCircle@2x.png new file mode 100644 index 0000000..c721a9d Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Large/SegmentedCircle@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Large/SegmentedCircle@3x.png b/Client2021/content/textures/ui/Emotes/Large/SegmentedCircle@3x.png new file mode 100644 index 0000000..78f76f4 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Large/SegmentedCircle@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Large/SelectedGradient.png b/Client2021/content/textures/ui/Emotes/Large/SelectedGradient.png new file mode 100644 index 0000000..d2b6838 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Large/SelectedGradient.png differ diff --git a/Client2021/content/textures/ui/Emotes/Large/SelectedGradient@2x.png b/Client2021/content/textures/ui/Emotes/Large/SelectedGradient@2x.png new file mode 100644 index 0000000..2af23af Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Large/SelectedGradient@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Large/SelectedGradient@3x.png b/Client2021/content/textures/ui/Emotes/Large/SelectedGradient@3x.png new file mode 100644 index 0000000..e254af6 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Large/SelectedGradient@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Large/SelectedLine.png b/Client2021/content/textures/ui/Emotes/Large/SelectedLine.png new file mode 100644 index 0000000..74fbd9f Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Large/SelectedLine.png differ diff --git a/Client2021/content/textures/ui/Emotes/Large/SelectedLine@2x.png b/Client2021/content/textures/ui/Emotes/Large/SelectedLine@2x.png new file mode 100644 index 0000000..cc073ae Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Large/SelectedLine@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Large/SelectedLine@3x.png b/Client2021/content/textures/ui/Emotes/Large/SelectedLine@3x.png new file mode 100644 index 0000000..0e7190f Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Large/SelectedLine@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Small/CircleBackground.png b/Client2021/content/textures/ui/Emotes/Small/CircleBackground.png new file mode 100644 index 0000000..141945f Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Small/CircleBackground.png differ diff --git a/Client2021/content/textures/ui/Emotes/Small/CircleBackground@2x.png b/Client2021/content/textures/ui/Emotes/Small/CircleBackground@2x.png new file mode 100644 index 0000000..c8fb422 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Small/CircleBackground@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Small/CircleBackground@3x.png b/Client2021/content/textures/ui/Emotes/Small/CircleBackground@3x.png new file mode 100644 index 0000000..c4d5dab Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Small/CircleBackground@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Small/SegmentedCircle.png b/Client2021/content/textures/ui/Emotes/Small/SegmentedCircle.png new file mode 100644 index 0000000..3f450c8 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Small/SegmentedCircle.png differ diff --git a/Client2021/content/textures/ui/Emotes/Small/SegmentedCircle@2x.png b/Client2021/content/textures/ui/Emotes/Small/SegmentedCircle@2x.png new file mode 100644 index 0000000..fc63af4 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Small/SegmentedCircle@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Small/SegmentedCircle@3x.png b/Client2021/content/textures/ui/Emotes/Small/SegmentedCircle@3x.png new file mode 100644 index 0000000..ec66f0d Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Small/SegmentedCircle@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Small/SelectedGradient.png b/Client2021/content/textures/ui/Emotes/Small/SelectedGradient.png new file mode 100644 index 0000000..04b0a60 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Small/SelectedGradient.png differ diff --git a/Client2021/content/textures/ui/Emotes/Small/SelectedGradient@2x.png b/Client2021/content/textures/ui/Emotes/Small/SelectedGradient@2x.png new file mode 100644 index 0000000..d5bcaad Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Small/SelectedGradient@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Small/SelectedGradient@3x.png b/Client2021/content/textures/ui/Emotes/Small/SelectedGradient@3x.png new file mode 100644 index 0000000..cc830c8 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Small/SelectedGradient@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Small/SelectedLine.png b/Client2021/content/textures/ui/Emotes/Small/SelectedLine.png new file mode 100644 index 0000000..022db41 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Small/SelectedLine.png differ diff --git a/Client2021/content/textures/ui/Emotes/Small/SelectedLine@2x.png b/Client2021/content/textures/ui/Emotes/Small/SelectedLine@2x.png new file mode 100644 index 0000000..07e9f98 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Small/SelectedLine@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/Small/SelectedLine@3x.png b/Client2021/content/textures/ui/Emotes/Small/SelectedLine@3x.png new file mode 100644 index 0000000..5a5aeda Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/Small/SelectedLine@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/TenFoot/CircleBackground.png b/Client2021/content/textures/ui/Emotes/TenFoot/CircleBackground.png new file mode 100644 index 0000000..c66a2a1 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/TenFoot/CircleBackground.png differ diff --git a/Client2021/content/textures/ui/Emotes/TenFoot/CircleBackground@2x.png b/Client2021/content/textures/ui/Emotes/TenFoot/CircleBackground@2x.png new file mode 100644 index 0000000..f53c980 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/TenFoot/CircleBackground@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/TenFoot/CircleBackground@3x.png b/Client2021/content/textures/ui/Emotes/TenFoot/CircleBackground@3x.png new file mode 100644 index 0000000..0732b7c Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/TenFoot/CircleBackground@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/TenFoot/SegmentedCircle.png b/Client2021/content/textures/ui/Emotes/TenFoot/SegmentedCircle.png new file mode 100644 index 0000000..c9cbcf6 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/TenFoot/SegmentedCircle.png differ diff --git a/Client2021/content/textures/ui/Emotes/TenFoot/SegmentedCircle@2x.png b/Client2021/content/textures/ui/Emotes/TenFoot/SegmentedCircle@2x.png new file mode 100644 index 0000000..bfd4caf Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/TenFoot/SegmentedCircle@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/TenFoot/SegmentedCircle@3x.png b/Client2021/content/textures/ui/Emotes/TenFoot/SegmentedCircle@3x.png new file mode 100644 index 0000000..e43514d Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/TenFoot/SegmentedCircle@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/TenFoot/SelectedGradient.png b/Client2021/content/textures/ui/Emotes/TenFoot/SelectedGradient.png new file mode 100644 index 0000000..db43475 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/TenFoot/SelectedGradient.png differ diff --git a/Client2021/content/textures/ui/Emotes/TenFoot/SelectedGradient@2x.png b/Client2021/content/textures/ui/Emotes/TenFoot/SelectedGradient@2x.png new file mode 100644 index 0000000..02c8a6c Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/TenFoot/SelectedGradient@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/TenFoot/SelectedGradient@3x.png b/Client2021/content/textures/ui/Emotes/TenFoot/SelectedGradient@3x.png new file mode 100644 index 0000000..2a214f5 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/TenFoot/SelectedGradient@3x.png differ diff --git a/Client2021/content/textures/ui/Emotes/TenFoot/SelectedLine.png b/Client2021/content/textures/ui/Emotes/TenFoot/SelectedLine.png new file mode 100644 index 0000000..96a6f70 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/TenFoot/SelectedLine.png differ diff --git a/Client2021/content/textures/ui/Emotes/TenFoot/SelectedLine@2x.png b/Client2021/content/textures/ui/Emotes/TenFoot/SelectedLine@2x.png new file mode 100644 index 0000000..e7da9af Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/TenFoot/SelectedLine@2x.png differ diff --git a/Client2021/content/textures/ui/Emotes/TenFoot/SelectedLine@3x.png b/Client2021/content/textures/ui/Emotes/TenFoot/SelectedLine@3x.png new file mode 100644 index 0000000..32c5be8 Binary files /dev/null and b/Client2021/content/textures/ui/Emotes/TenFoot/SelectedLine@3x.png differ diff --git a/Client2021/content/textures/ui/ErrorIcon.png b/Client2021/content/textures/ui/ErrorIcon.png new file mode 100644 index 0000000..420b21b Binary files /dev/null and b/Client2021/content/textures/ui/ErrorIcon.png differ diff --git a/Client2021/content/textures/ui/ErrorIconSmall.png b/Client2021/content/textures/ui/ErrorIconSmall.png new file mode 100644 index 0000000..213a1ff Binary files /dev/null and b/Client2021/content/textures/ui/ErrorIconSmall.png differ diff --git a/Client2021/content/textures/ui/ErrorPrompt/PrimaryButton.png b/Client2021/content/textures/ui/ErrorPrompt/PrimaryButton.png new file mode 100644 index 0000000..afae8bb Binary files /dev/null and b/Client2021/content/textures/ui/ErrorPrompt/PrimaryButton.png differ diff --git a/Client2021/content/textures/ui/ErrorPrompt/PrimaryButton@2x.png b/Client2021/content/textures/ui/ErrorPrompt/PrimaryButton@2x.png new file mode 100644 index 0000000..0f8157c Binary files /dev/null and b/Client2021/content/textures/ui/ErrorPrompt/PrimaryButton@2x.png differ diff --git a/Client2021/content/textures/ui/ErrorPrompt/PrimaryButton@3x.png b/Client2021/content/textures/ui/ErrorPrompt/PrimaryButton@3x.png new file mode 100644 index 0000000..72a5371 Binary files /dev/null and b/Client2021/content/textures/ui/ErrorPrompt/PrimaryButton@3x.png differ diff --git a/Client2021/content/textures/ui/ErrorPrompt/SecondaryButton.png b/Client2021/content/textures/ui/ErrorPrompt/SecondaryButton.png new file mode 100644 index 0000000..70b102b Binary files /dev/null and b/Client2021/content/textures/ui/ErrorPrompt/SecondaryButton.png differ diff --git a/Client2021/content/textures/ui/ErrorPrompt/SecondaryButton@2x.png b/Client2021/content/textures/ui/ErrorPrompt/SecondaryButton@2x.png new file mode 100644 index 0000000..cee0a86 Binary files /dev/null and b/Client2021/content/textures/ui/ErrorPrompt/SecondaryButton@2x.png differ diff --git a/Client2021/content/textures/ui/ErrorPrompt/SecondaryButton@3x.png b/Client2021/content/textures/ui/ErrorPrompt/SecondaryButton@3x.png new file mode 100644 index 0000000..0ac6f5c Binary files /dev/null and b/Client2021/content/textures/ui/ErrorPrompt/SecondaryButton@3x.png differ diff --git a/Client2021/content/textures/ui/ErrorPrompt/ShimmerOverlay.png b/Client2021/content/textures/ui/ErrorPrompt/ShimmerOverlay.png new file mode 100644 index 0000000..a38dcc8 Binary files /dev/null and b/Client2021/content/textures/ui/ErrorPrompt/ShimmerOverlay.png differ diff --git a/Client2021/content/textures/ui/ErrorPrompt/ShimmerOverlay@2x.png b/Client2021/content/textures/ui/ErrorPrompt/ShimmerOverlay@2x.png new file mode 100644 index 0000000..4747f05 Binary files /dev/null and b/Client2021/content/textures/ui/ErrorPrompt/ShimmerOverlay@2x.png differ diff --git a/Client2021/content/textures/ui/ErrorPrompt/ShimmerOverlay@3x.png b/Client2021/content/textures/ui/ErrorPrompt/ShimmerOverlay@3x.png new file mode 100644 index 0000000..923705d Binary files /dev/null and b/Client2021/content/textures/ui/ErrorPrompt/ShimmerOverlay@3x.png differ diff --git a/Client2021/content/textures/ui/ExpandArrowSheet.png b/Client2021/content/textures/ui/ExpandArrowSheet.png new file mode 100644 index 0000000..d52edc1 Binary files /dev/null and b/Client2021/content/textures/ui/ExpandArrowSheet.png differ diff --git a/Client2021/content/textures/ui/Gear.png b/Client2021/content/textures/ui/Gear.png new file mode 100644 index 0000000..56a46db Binary files /dev/null and b/Client2021/content/textures/ui/Gear.png differ diff --git a/Client2021/content/textures/ui/Gear_dn.png b/Client2021/content/textures/ui/Gear_dn.png new file mode 100644 index 0000000..38df9a3 Binary files /dev/null and b/Client2021/content/textures/ui/Gear_dn.png differ diff --git a/Client2021/content/textures/ui/GuiImagePlaceholder.png b/Client2021/content/textures/ui/GuiImagePlaceholder.png new file mode 100644 index 0000000..5a6d524 Binary files /dev/null and b/Client2021/content/textures/ui/GuiImagePlaceholder.png differ diff --git a/Client2021/content/textures/ui/Health-BKG-Center.png b/Client2021/content/textures/ui/Health-BKG-Center.png new file mode 100644 index 0000000..dea1b3b Binary files /dev/null and b/Client2021/content/textures/ui/Health-BKG-Center.png differ diff --git a/Client2021/content/textures/ui/Health-BKG-Center@2x.png b/Client2021/content/textures/ui/Health-BKG-Center@2x.png new file mode 100644 index 0000000..3fe7101 Binary files /dev/null and b/Client2021/content/textures/ui/Health-BKG-Center@2x.png differ diff --git a/Client2021/content/textures/ui/Health-BKG-Left-Cap.png b/Client2021/content/textures/ui/Health-BKG-Left-Cap.png new file mode 100644 index 0000000..9baf764 Binary files /dev/null and b/Client2021/content/textures/ui/Health-BKG-Left-Cap.png differ diff --git a/Client2021/content/textures/ui/Health-BKG-Left-Cap@2x.png b/Client2021/content/textures/ui/Health-BKG-Left-Cap@2x.png new file mode 100644 index 0000000..6a7825c Binary files /dev/null and b/Client2021/content/textures/ui/Health-BKG-Left-Cap@2x.png differ diff --git a/Client2021/content/textures/ui/Health-BKG-Right-Cap.png b/Client2021/content/textures/ui/Health-BKG-Right-Cap.png new file mode 100644 index 0000000..66af73a Binary files /dev/null and b/Client2021/content/textures/ui/Health-BKG-Right-Cap.png differ diff --git a/Client2021/content/textures/ui/Health-BKG-Right-Cap@2x.png b/Client2021/content/textures/ui/Health-BKG-Right-Cap@2x.png new file mode 100644 index 0000000..7c3f951 Binary files /dev/null and b/Client2021/content/textures/ui/Health-BKG-Right-Cap@2x.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/BackgroundGlow.png b/Client2021/content/textures/ui/InGameMenu/BackgroundGlow.png new file mode 100644 index 0000000..f437d62 Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/BackgroundGlow.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/BackgroundGlow@2x.png b/Client2021/content/textures/ui/InGameMenu/BackgroundGlow@2x.png new file mode 100644 index 0000000..f7751c7 Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/BackgroundGlow@2x.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/BackgroundGlow@3x.png b/Client2021/content/textures/ui/InGameMenu/BackgroundGlow@3x.png new file mode 100644 index 0000000..e47d7bb Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/BackgroundGlow@3x.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/CircleCutout.png b/Client2021/content/textures/ui/InGameMenu/CircleCutout.png new file mode 100644 index 0000000..621336f Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/CircleCutout.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/GenericController.png b/Client2021/content/textures/ui/InGameMenu/GenericController.png new file mode 100644 index 0000000..6951483 Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/GenericController.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/GenericController@2x.png b/Client2021/content/textures/ui/InGameMenu/GenericController@2x.png new file mode 100644 index 0000000..fe4257c Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/GenericController@2x.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/QuarterCircle.png b/Client2021/content/textures/ui/InGameMenu/QuarterCircle.png new file mode 100644 index 0000000..c7c51a8 Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/QuarterCircle.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/ScrollBottom.png b/Client2021/content/textures/ui/InGameMenu/ScrollBottom.png new file mode 100644 index 0000000..dca5b38 Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/ScrollBottom.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/ScrollBottom@2x.png b/Client2021/content/textures/ui/InGameMenu/ScrollBottom@2x.png new file mode 100644 index 0000000..6a2d21d Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/ScrollBottom@2x.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/ScrollBottom@3x.png b/Client2021/content/textures/ui/InGameMenu/ScrollBottom@3x.png new file mode 100644 index 0000000..2c242d5 Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/ScrollBottom@3x.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/ScrollMiddle.png b/Client2021/content/textures/ui/InGameMenu/ScrollMiddle.png new file mode 100644 index 0000000..330507d Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/ScrollMiddle.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/ScrollMiddle@2x.png b/Client2021/content/textures/ui/InGameMenu/ScrollMiddle@2x.png new file mode 100644 index 0000000..8f89578 Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/ScrollMiddle@2x.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/ScrollMiddle@3x.png b/Client2021/content/textures/ui/InGameMenu/ScrollMiddle@3x.png new file mode 100644 index 0000000..46db69f Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/ScrollMiddle@3x.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/ScrollTop.png b/Client2021/content/textures/ui/InGameMenu/ScrollTop.png new file mode 100644 index 0000000..2fb159a Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/ScrollTop.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/ScrollTop@2x.png b/Client2021/content/textures/ui/InGameMenu/ScrollTop@2x.png new file mode 100644 index 0000000..d2a3a61 Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/ScrollTop@2x.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/ScrollTop@3x.png b/Client2021/content/textures/ui/InGameMenu/ScrollTop@3x.png new file mode 100644 index 0000000..d3fe75c Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/ScrollTop@3x.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/WhiteSquare.png b/Client2021/content/textures/ui/InGameMenu/WhiteSquare.png new file mode 100644 index 0000000..dc4e635 Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/WhiteSquare.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/XboxController.png b/Client2021/content/textures/ui/InGameMenu/XboxController.png new file mode 100644 index 0000000..d27ffb7 Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/XboxController.png differ diff --git a/Client2021/content/textures/ui/InGameMenu/XboxController@2x.png b/Client2021/content/textures/ui/InGameMenu/XboxController@2x.png new file mode 100644 index 0000000..e86c676 Binary files /dev/null and b/Client2021/content/textures/ui/InGameMenu/XboxController@2x.png differ diff --git a/Client2021/content/textures/ui/Input/DashedLine.png b/Client2021/content/textures/ui/Input/DashedLine.png new file mode 100644 index 0000000..f9e45e3 Binary files /dev/null and b/Client2021/content/textures/ui/Input/DashedLine.png differ diff --git a/Client2021/content/textures/ui/Input/DashedLine90.png b/Client2021/content/textures/ui/Input/DashedLine90.png new file mode 100644 index 0000000..59ae6c7 Binary files /dev/null and b/Client2021/content/textures/ui/Input/DashedLine90.png differ diff --git a/Client2021/content/textures/ui/Input/Disk_padded.png b/Client2021/content/textures/ui/Input/Disk_padded.png new file mode 100644 index 0000000..ccbda80 Binary files /dev/null and b/Client2021/content/textures/ui/Input/Disk_padded.png differ diff --git a/Client2021/content/textures/ui/Input/IntroCamera.png b/Client2021/content/textures/ui/Input/IntroCamera.png new file mode 100644 index 0000000..bcf3f9c Binary files /dev/null and b/Client2021/content/textures/ui/Input/IntroCamera.png differ diff --git a/Client2021/content/textures/ui/Input/IntroCameraPinch.png b/Client2021/content/textures/ui/Input/IntroCameraPinch.png new file mode 100644 index 0000000..9521250 Binary files /dev/null and b/Client2021/content/textures/ui/Input/IntroCameraPinch.png differ diff --git a/Client2021/content/textures/ui/Input/IntroMove.png b/Client2021/content/textures/ui/Input/IntroMove.png new file mode 100644 index 0000000..a435304 Binary files /dev/null and b/Client2021/content/textures/ui/Input/IntroMove.png differ diff --git a/Client2021/content/textures/ui/Input/IntroMove@2x.png b/Client2021/content/textures/ui/Input/IntroMove@2x.png new file mode 100644 index 0000000..1ce8682 Binary files /dev/null and b/Client2021/content/textures/ui/Input/IntroMove@2x.png differ diff --git a/Client2021/content/textures/ui/Input/Ring_padded.png b/Client2021/content/textures/ui/Input/Ring_padded.png new file mode 100644 index 0000000..54cd6eb Binary files /dev/null and b/Client2021/content/textures/ui/Input/Ring_padded.png differ diff --git a/Client2021/content/textures/ui/Input/TouchControlsSheetV2.png b/Client2021/content/textures/ui/Input/TouchControlsSheetV2.png new file mode 100644 index 0000000..1999108 Binary files /dev/null and b/Client2021/content/textures/ui/Input/TouchControlsSheetV2.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/Button_outline.png b/Client2021/content/textures/ui/InspectMenu/Button_outline.png new file mode 100644 index 0000000..e80c078 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/Button_outline.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/Button_outline@2x.png b/Client2021/content/textures/ui/InspectMenu/Button_outline@2x.png new file mode 100644 index 0000000..f597ed5 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/Button_outline@2x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/Button_outline@3x.png b/Client2021/content/textures/ui/InspectMenu/Button_outline@3x.png new file mode 100644 index 0000000..e591c6c Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/Button_outline@3x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/Button_white.png b/Client2021/content/textures/ui/InspectMenu/Button_white.png new file mode 100644 index 0000000..a9d584a Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/Button_white.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/Button_white@2x.png b/Client2021/content/textures/ui/InspectMenu/Button_white@2x.png new file mode 100644 index 0000000..d95687a Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/Button_white@2x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/Button_white@3x.png b/Client2021/content/textures/ui/InspectMenu/Button_white@3x.png new file mode 100644 index 0000000..c5b0167 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/Button_white@3x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/caret-tail-left@2x.png b/Client2021/content/textures/ui/InspectMenu/caret-tail-left@2x.png new file mode 100644 index 0000000..5eb7bba Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/caret-tail-left@2x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/caret_tail_left.png b/Client2021/content/textures/ui/InspectMenu/caret_tail_left.png new file mode 100644 index 0000000..bda2423 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/caret_tail_left.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/caret_tail_left@3x.png b/Client2021/content/textures/ui/InspectMenu/caret_tail_left@3x.png new file mode 100644 index 0000000..adbf680 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/caret_tail_left@3x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/gr-item-selector-triangle.png b/Client2021/content/textures/ui/InspectMenu/gr-item-selector-triangle.png new file mode 100644 index 0000000..44cdf7e Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/gr-item-selector-triangle.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/gr-item-selector-triangle@2x.png b/Client2021/content/textures/ui/InspectMenu/gr-item-selector-triangle@2x.png new file mode 100644 index 0000000..43e60f7 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/gr-item-selector-triangle@2x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/gr-item-selector-triangle@3x.png b/Client2021/content/textures/ui/InspectMenu/gr-item-selector-triangle@3x.png new file mode 100644 index 0000000..612624d Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/gr-item-selector-triangle@3x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/gr-item-selector.png b/Client2021/content/textures/ui/InspectMenu/gr-item-selector.png new file mode 100644 index 0000000..cf8650d Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/gr-item-selector.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/gr-item-selector@2x.png b/Client2021/content/textures/ui/InspectMenu/gr-item-selector@2x.png new file mode 100644 index 0000000..0b2bdb5 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/gr-item-selector@2x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/gr-item-selector@3x.png b/Client2021/content/textures/ui/InspectMenu/gr-item-selector@3x.png new file mode 100644 index 0000000..a632c89 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/gr-item-selector@3x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_alert_tilt.png b/Client2021/content/textures/ui/InspectMenu/ico_alert_tilt.png new file mode 100644 index 0000000..7bb27f4 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_alert_tilt.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_alert_tilt@2x.png b/Client2021/content/textures/ui/InspectMenu/ico_alert_tilt@2x.png new file mode 100644 index 0000000..5d9dd77 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_alert_tilt@2x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_alert_tilt@3x.png b/Client2021/content/textures/ui/InspectMenu/ico_alert_tilt@3x.png new file mode 100644 index 0000000..263b4c0 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_alert_tilt@3x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_favorite.png b/Client2021/content/textures/ui/InspectMenu/ico_favorite.png new file mode 100644 index 0000000..fd24702 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_favorite.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_favorite@2x.png b/Client2021/content/textures/ui/InspectMenu/ico_favorite@2x.png new file mode 100644 index 0000000..e1bfd0a Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_favorite@2x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_favorite@3x.png b/Client2021/content/textures/ui/InspectMenu/ico_favorite@3x.png new file mode 100644 index 0000000..ec7b258 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_favorite@3x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_favorite_off.png b/Client2021/content/textures/ui/InspectMenu/ico_favorite_off.png new file mode 100644 index 0000000..fca7e0f Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_favorite_off.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_favorite_off@2x.png b/Client2021/content/textures/ui/InspectMenu/ico_favorite_off@2x.png new file mode 100644 index 0000000..102aac2 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_favorite_off@2x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_favorite_off@3x.png b/Client2021/content/textures/ui/InspectMenu/ico_favorite_off@3x.png new file mode 100644 index 0000000..37b23bb Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_favorite_off@3x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_inspect.png b/Client2021/content/textures/ui/InspectMenu/ico_inspect.png new file mode 100644 index 0000000..00f16d7 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_inspect.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_inspect@2x.png b/Client2021/content/textures/ui/InspectMenu/ico_inspect@2x.png new file mode 100644 index 0000000..2fcd3c1 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_inspect@2x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_inspect@3x.png b/Client2021/content/textures/ui/InspectMenu/ico_inspect@3x.png new file mode 100644 index 0000000..b7c4045 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_inspect@3x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_isnt-wearing.png b/Client2021/content/textures/ui/InspectMenu/ico_isnt-wearing.png new file mode 100644 index 0000000..28c37f0 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_isnt-wearing.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_isnt-wearing@2x.png b/Client2021/content/textures/ui/InspectMenu/ico_isnt-wearing@2x.png new file mode 100644 index 0000000..f569ddf Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_isnt-wearing@2x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_isnt-wearing@3x.png b/Client2021/content/textures/ui/InspectMenu/ico_isnt-wearing@3x.png new file mode 100644 index 0000000..27716aa Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_isnt-wearing@3x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_robux.png b/Client2021/content/textures/ui/InspectMenu/ico_robux.png new file mode 100644 index 0000000..3a32134 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_robux.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_robux@2x.png b/Client2021/content/textures/ui/InspectMenu/ico_robux@2x.png new file mode 100644 index 0000000..3a58a23 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_robux@2x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/ico_robux@3x.png b/Client2021/content/textures/ui/InspectMenu/ico_robux@3x.png new file mode 100644 index 0000000..37202cf Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/ico_robux@3x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/scroll_bar.png b/Client2021/content/textures/ui/InspectMenu/scroll_bar.png new file mode 100644 index 0000000..d8f14c4 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/scroll_bar.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/scroll_bar@2x.png b/Client2021/content/textures/ui/InspectMenu/scroll_bar@2x.png new file mode 100644 index 0000000..306cec6 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/scroll_bar@2x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/scroll_bar@3x.png b/Client2021/content/textures/ui/InspectMenu/scroll_bar@3x.png new file mode 100644 index 0000000..7714b43 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/scroll_bar@3x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/selection_regular.png b/Client2021/content/textures/ui/InspectMenu/selection_regular.png new file mode 100644 index 0000000..725b9f1 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/selection_regular.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/selection_regular@2x.png b/Client2021/content/textures/ui/InspectMenu/selection_regular@2x.png new file mode 100644 index 0000000..efd5440 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/selection_regular@2x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/selection_regular@3x.png b/Client2021/content/textures/ui/InspectMenu/selection_regular@3x.png new file mode 100644 index 0000000..5d54e43 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/selection_regular@3x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/selection_rounded.png b/Client2021/content/textures/ui/InspectMenu/selection_rounded.png new file mode 100644 index 0000000..7bb8105 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/selection_rounded.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/selection_rounded@2x.png b/Client2021/content/textures/ui/InspectMenu/selection_rounded@2x.png new file mode 100644 index 0000000..17f7a9e Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/selection_rounded@2x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/selection_rounded@3x.png b/Client2021/content/textures/ui/InspectMenu/selection_rounded@3x.png new file mode 100644 index 0000000..8009388 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/selection_rounded@3x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/x.png b/Client2021/content/textures/ui/InspectMenu/x.png new file mode 100644 index 0000000..bb8fd52 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/x@2x.png b/Client2021/content/textures/ui/InspectMenu/x@2x.png new file mode 100644 index 0000000..dc164a4 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/x@2x.png differ diff --git a/Client2021/content/textures/ui/InspectMenu/x@3x.png b/Client2021/content/textures/ui/InspectMenu/x@3x.png new file mode 100644 index 0000000..5c03866 Binary files /dev/null and b/Client2021/content/textures/ui/InspectMenu/x@3x.png differ diff --git a/Client2021/content/textures/ui/Keyboard/close_button_background.png b/Client2021/content/textures/ui/Keyboard/close_button_background.png new file mode 100644 index 0000000..ceec973 Binary files /dev/null and b/Client2021/content/textures/ui/Keyboard/close_button_background.png differ diff --git a/Client2021/content/textures/ui/Keyboard/close_button_icon.png b/Client2021/content/textures/ui/Keyboard/close_button_icon.png new file mode 100644 index 0000000..bb306ad Binary files /dev/null and b/Client2021/content/textures/ui/Keyboard/close_button_icon.png differ diff --git a/Client2021/content/textures/ui/Keyboard/close_button_selection.png b/Client2021/content/textures/ui/Keyboard/close_button_selection.png new file mode 100644 index 0000000..87bbfed Binary files /dev/null and b/Client2021/content/textures/ui/Keyboard/close_button_selection.png differ diff --git a/Client2021/content/textures/ui/Keyboard/key_selection_9slice.png b/Client2021/content/textures/ui/Keyboard/key_selection_9slice.png new file mode 100644 index 0000000..9c6fc50 Binary files /dev/null and b/Client2021/content/textures/ui/Keyboard/key_selection_9slice.png differ diff --git a/Client2021/content/textures/ui/Keyboard/mic_icon.png b/Client2021/content/textures/ui/Keyboard/mic_icon.png new file mode 100644 index 0000000..7e09484 Binary files /dev/null and b/Client2021/content/textures/ui/Keyboard/mic_icon.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/Aluminium.png b/Client2021/content/textures/ui/LegacyRbxGui/Aluminium.png new file mode 100644 index 0000000..c97ee68 Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/Aluminium.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/Asphalt.png b/Client2021/content/textures/ui/LegacyRbxGui/Asphalt.png new file mode 100644 index 0000000..a80093b Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/Asphalt.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/Cement.png b/Client2021/content/textures/ui/LegacyRbxGui/Cement.png new file mode 100644 index 0000000..a706a60 Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/Cement.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/Cinder block.png b/Client2021/content/textures/ui/LegacyRbxGui/Cinder block.png new file mode 100644 index 0000000..0d2dae7 Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/Cinder block.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/CloseButton.png b/Client2021/content/textures/ui/LegacyRbxGui/CloseButton.png new file mode 100644 index 0000000..6e01c6c Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/CloseButton.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/ComboBoxArrow.png b/Client2021/content/textures/ui/LegacyRbxGui/ComboBoxArrow.png new file mode 100644 index 0000000..529082e Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/ComboBoxArrow.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/Gold.png b/Client2021/content/textures/ui/LegacyRbxGui/Gold.png new file mode 100644 index 0000000..6597c05 Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/Gold.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/Granite .png b/Client2021/content/textures/ui/LegacyRbxGui/Granite .png new file mode 100644 index 0000000..7c69a36 Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/Granite .png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/GravelSide.png b/Client2021/content/textures/ui/LegacyRbxGui/GravelSide.png new file mode 100644 index 0000000..2abe94d Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/GravelSide.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/IronSide.png b/Client2021/content/textures/ui/LegacyRbxGui/IronSide.png new file mode 100644 index 0000000..096ae0a Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/IronSide.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/LogSide.png b/Client2021/content/textures/ui/LegacyRbxGui/LogSide.png new file mode 100644 index 0000000..732684e Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/LogSide.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/M1Side.png b/Client2021/content/textures/ui/LegacyRbxGui/M1Side.png new file mode 100644 index 0000000..93d0e5b Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/M1Side.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/PlankSide.png b/Client2021/content/textures/ui/LegacyRbxGui/PlankSide.png new file mode 100644 index 0000000..a300a3f Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/PlankSide.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/PlasticBlueTop.png b/Client2021/content/textures/ui/LegacyRbxGui/PlasticBlueTop.png new file mode 100644 index 0000000..93d9a6d Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/PlasticBlueTop.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/PlasticRedTop.png b/Client2021/content/textures/ui/LegacyRbxGui/PlasticRedTop.png new file mode 100644 index 0000000..d8b9ea3 Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/PlasticRedTop.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/StoneBlockSide.png b/Client2021/content/textures/ui/LegacyRbxGui/StoneBlockSide.png new file mode 100644 index 0000000..5d05da3 Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/StoneBlockSide.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/_preview water 03.png b/Client2021/content/textures/ui/LegacyRbxGui/_preview water 03.png new file mode 100644 index 0000000..5e44e05 Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/_preview water 03.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/brickSide.png b/Client2021/content/textures/ui/LegacyRbxGui/brickSide.png new file mode 100644 index 0000000..86a92a9 Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/brickSide.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/configIcon.png b/Client2021/content/textures/ui/LegacyRbxGui/configIcon.png new file mode 100644 index 0000000..e3687b9 Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/configIcon.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/health_greenBar.png b/Client2021/content/textures/ui/LegacyRbxGui/health_greenBar.png new file mode 100644 index 0000000..4a7c908 Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/health_greenBar.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/popup_greenCheckCircle.png b/Client2021/content/textures/ui/LegacyRbxGui/popup_greenCheckCircle.png new file mode 100644 index 0000000..1bcdabe Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/popup_greenCheckCircle.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/popup_redx.png b/Client2021/content/textures/ui/LegacyRbxGui/popup_redx.png new file mode 100644 index 0000000..e4471a6 Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/popup_redx.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/popup_warnTriangle.png b/Client2021/content/textures/ui/LegacyRbxGui/popup_warnTriangle.png new file mode 100644 index 0000000..cc98476 Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/popup_warnTriangle.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/sandside.png b/Client2021/content/textures/ui/LegacyRbxGui/sandside.png new file mode 100644 index 0000000..32a24aa Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/sandside.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/scroll.png b/Client2021/content/textures/ui/LegacyRbxGui/scroll.png new file mode 100644 index 0000000..50551ad Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/scroll.png differ diff --git a/Client2021/content/textures/ui/LegacyRbxGui/x.png b/Client2021/content/textures/ui/LegacyRbxGui/x.png new file mode 100644 index 0000000..bce3b85 Binary files /dev/null and b/Client2021/content/textures/ui/LegacyRbxGui/x.png differ diff --git a/Client2021/content/textures/ui/LoadingBKG.png b/Client2021/content/textures/ui/LoadingBKG.png new file mode 100644 index 0000000..2fc377d Binary files /dev/null and b/Client2021/content/textures/ui/LoadingBKG.png differ diff --git a/Client2021/content/textures/ui/LoadingScreen/BackgroundDark.png b/Client2021/content/textures/ui/LoadingScreen/BackgroundDark.png new file mode 100644 index 0000000..6df6d97 Binary files /dev/null and b/Client2021/content/textures/ui/LoadingScreen/BackgroundDark.png differ diff --git a/Client2021/content/textures/ui/LoadingScreen/BackgroundLight.png b/Client2021/content/textures/ui/LoadingScreen/BackgroundLight.png new file mode 100644 index 0000000..d1b2701 Binary files /dev/null and b/Client2021/content/textures/ui/LoadingScreen/BackgroundLight.png differ diff --git a/Client2021/content/textures/ui/LoadingScreen/LoadingSpinner.png b/Client2021/content/textures/ui/LoadingScreen/LoadingSpinner.png new file mode 100644 index 0000000..b577d8e Binary files /dev/null and b/Client2021/content/textures/ui/LoadingScreen/LoadingSpinner.png differ diff --git a/Client2021/content/textures/ui/Lobby/Buttons/glow_nine_slice.png b/Client2021/content/textures/ui/Lobby/Buttons/glow_nine_slice.png new file mode 100644 index 0000000..f67905e Binary files /dev/null and b/Client2021/content/textures/ui/Lobby/Buttons/glow_nine_slice.png differ diff --git a/Client2021/content/textures/ui/Lobby/Buttons/more_nine_slice_button.png b/Client2021/content/textures/ui/Lobby/Buttons/more_nine_slice_button.png new file mode 100644 index 0000000..7e287ed Binary files /dev/null and b/Client2021/content/textures/ui/Lobby/Buttons/more_nine_slice_button.png differ diff --git a/Client2021/content/textures/ui/Lobby/Buttons/nine_slice_button.png b/Client2021/content/textures/ui/Lobby/Buttons/nine_slice_button.png new file mode 100644 index 0000000..adf36ae Binary files /dev/null and b/Client2021/content/textures/ui/Lobby/Buttons/nine_slice_button.png differ diff --git a/Client2021/content/textures/ui/Lobby/Buttons/scroll_button.png b/Client2021/content/textures/ui/Lobby/Buttons/scroll_button.png new file mode 100644 index 0000000..e40ed8c Binary files /dev/null and b/Client2021/content/textures/ui/Lobby/Buttons/scroll_button.png differ diff --git a/Client2021/content/textures/ui/Lobby/Buttons/scroll_down.png b/Client2021/content/textures/ui/Lobby/Buttons/scroll_down.png new file mode 100644 index 0000000..b7ca49b Binary files /dev/null and b/Client2021/content/textures/ui/Lobby/Buttons/scroll_down.png differ diff --git a/Client2021/content/textures/ui/Lobby/Buttons/scroll_left.png b/Client2021/content/textures/ui/Lobby/Buttons/scroll_left.png new file mode 100644 index 0000000..1ad66bb Binary files /dev/null and b/Client2021/content/textures/ui/Lobby/Buttons/scroll_left.png differ diff --git a/Client2021/content/textures/ui/Lobby/Buttons/scroll_right.png b/Client2021/content/textures/ui/Lobby/Buttons/scroll_right.png new file mode 100644 index 0000000..7c2fa78 Binary files /dev/null and b/Client2021/content/textures/ui/Lobby/Buttons/scroll_right.png differ diff --git a/Client2021/content/textures/ui/Lobby/Buttons/scroll_up.png b/Client2021/content/textures/ui/Lobby/Buttons/scroll_up.png new file mode 100644 index 0000000..c411535 Binary files /dev/null and b/Client2021/content/textures/ui/Lobby/Buttons/scroll_up.png differ diff --git a/Client2021/content/textures/ui/Lobby/Icons/back_icon.png b/Client2021/content/textures/ui/Lobby/Icons/back_icon.png new file mode 100644 index 0000000..501a503 Binary files /dev/null and b/Client2021/content/textures/ui/Lobby/Icons/back_icon.png differ diff --git a/Client2021/content/textures/ui/Menu/Hamburger.png b/Client2021/content/textures/ui/Menu/Hamburger.png new file mode 100644 index 0000000..6e0c2e4 Binary files /dev/null and b/Client2021/content/textures/ui/Menu/Hamburger.png differ diff --git a/Client2021/content/textures/ui/Menu/Hamburger@2x.png b/Client2021/content/textures/ui/Menu/Hamburger@2x.png new file mode 100644 index 0000000..a5ce478 Binary files /dev/null and b/Client2021/content/textures/ui/Menu/Hamburger@2x.png differ diff --git a/Client2021/content/textures/ui/Menu/HamburgerDown.png b/Client2021/content/textures/ui/Menu/HamburgerDown.png new file mode 100644 index 0000000..aaf1032 Binary files /dev/null and b/Client2021/content/textures/ui/Menu/HamburgerDown.png differ diff --git a/Client2021/content/textures/ui/Menu/HamburgerDown@2x.png b/Client2021/content/textures/ui/Menu/HamburgerDown@2x.png new file mode 100644 index 0000000..43ab55c Binary files /dev/null and b/Client2021/content/textures/ui/Menu/HamburgerDown@2x.png differ diff --git a/Client2021/content/textures/ui/Menu/buttonActive.png b/Client2021/content/textures/ui/Menu/buttonActive.png new file mode 100644 index 0000000..f685760 Binary files /dev/null and b/Client2021/content/textures/ui/Menu/buttonActive.png differ diff --git a/Client2021/content/textures/ui/Menu/buttonBackground.png b/Client2021/content/textures/ui/Menu/buttonBackground.png new file mode 100644 index 0000000..379241a Binary files /dev/null and b/Client2021/content/textures/ui/Menu/buttonBackground.png differ diff --git a/Client2021/content/textures/ui/Menu/buttonHover.png b/Client2021/content/textures/ui/Menu/buttonHover.png new file mode 100644 index 0000000..916ddff Binary files /dev/null and b/Client2021/content/textures/ui/Menu/buttonHover.png differ diff --git a/Client2021/content/textures/ui/Menu/hamburger3D.png b/Client2021/content/textures/ui/Menu/hamburger3D.png new file mode 100644 index 0000000..a9d70c5 Binary files /dev/null and b/Client2021/content/textures/ui/Menu/hamburger3D.png differ diff --git a/Client2021/content/textures/ui/Menu/hoverPopupLeft.png b/Client2021/content/textures/ui/Menu/hoverPopupLeft.png new file mode 100644 index 0000000..484b0d2 Binary files /dev/null and b/Client2021/content/textures/ui/Menu/hoverPopupLeft.png differ diff --git a/Client2021/content/textures/ui/Menu/hoverPopupMid.png b/Client2021/content/textures/ui/Menu/hoverPopupMid.png new file mode 100644 index 0000000..0f55008 Binary files /dev/null and b/Client2021/content/textures/ui/Menu/hoverPopupMid.png differ diff --git a/Client2021/content/textures/ui/Menu/hoverPopupRight.png b/Client2021/content/textures/ui/Menu/hoverPopupRight.png new file mode 100644 index 0000000..ea0b32a Binary files /dev/null and b/Client2021/content/textures/ui/Menu/hoverPopupRight.png differ diff --git a/Client2021/content/textures/ui/Menu/rectBackground.png b/Client2021/content/textures/ui/Menu/rectBackground.png new file mode 100644 index 0000000..758edfe Binary files /dev/null and b/Client2021/content/textures/ui/Menu/rectBackground.png differ diff --git a/Client2021/content/textures/ui/Menu/rectBackgroundWhite.png b/Client2021/content/textures/ui/Menu/rectBackgroundWhite.png new file mode 100644 index 0000000..6bbb4b6 Binary files /dev/null and b/Client2021/content/textures/ui/Menu/rectBackgroundWhite.png differ diff --git a/Client2021/content/textures/ui/Modal.png b/Client2021/content/textures/ui/Modal.png new file mode 100644 index 0000000..ec08e5b Binary files /dev/null and b/Client2021/content/textures/ui/Modal.png differ diff --git a/Client2021/content/textures/ui/Motor.png b/Client2021/content/textures/ui/Motor.png new file mode 100644 index 0000000..140c5cc Binary files /dev/null and b/Client2021/content/textures/ui/Motor.png differ diff --git a/Client2021/content/textures/ui/NetworkPause/no connection.png b/Client2021/content/textures/ui/NetworkPause/no connection.png new file mode 100644 index 0000000..88b13c5 Binary files /dev/null and b/Client2021/content/textures/ui/NetworkPause/no connection.png differ diff --git a/Client2021/content/textures/ui/NetworkPause/no connection@2x.png b/Client2021/content/textures/ui/NetworkPause/no connection@2x.png new file mode 100644 index 0000000..6eecbbd Binary files /dev/null and b/Client2021/content/textures/ui/NetworkPause/no connection@2x.png differ diff --git a/Client2021/content/textures/ui/NetworkPause/no connection@3x.png b/Client2021/content/textures/ui/NetworkPause/no connection@3x.png new file mode 100644 index 0000000..f8e1393 Binary files /dev/null and b/Client2021/content/textures/ui/NetworkPause/no connection@3x.png differ diff --git a/Client2021/content/textures/ui/PerformanceStats/BackgroundRounded.png b/Client2021/content/textures/ui/PerformanceStats/BackgroundRounded.png new file mode 100644 index 0000000..763d78e Binary files /dev/null and b/Client2021/content/textures/ui/PerformanceStats/BackgroundRounded.png differ diff --git a/Client2021/content/textures/ui/PerformanceStats/OvalKey.png b/Client2021/content/textures/ui/PerformanceStats/OvalKey.png new file mode 100644 index 0000000..90678c2 Binary files /dev/null and b/Client2021/content/textures/ui/PerformanceStats/OvalKey.png differ diff --git a/Client2021/content/textures/ui/PerformanceStats/TargetFiller.png b/Client2021/content/textures/ui/PerformanceStats/TargetFiller.png new file mode 100644 index 0000000..839cc28 Binary files /dev/null and b/Client2021/content/textures/ui/PerformanceStats/TargetFiller.png differ diff --git a/Client2021/content/textures/ui/PerformanceStats/TargetKey.png b/Client2021/content/textures/ui/PerformanceStats/TargetKey.png new file mode 100644 index 0000000..9008605 Binary files /dev/null and b/Client2021/content/textures/ui/PerformanceStats/TargetKey.png differ diff --git a/Client2021/content/textures/ui/PerformanceStats/TargetLine.png b/Client2021/content/textures/ui/PerformanceStats/TargetLine.png new file mode 100644 index 0000000..c1b434a Binary files /dev/null and b/Client2021/content/textures/ui/PerformanceStats/TargetLine.png differ diff --git a/Client2021/content/textures/ui/Plastic.png b/Client2021/content/textures/ui/Plastic.png new file mode 100644 index 0000000..0dd2054 Binary files /dev/null and b/Client2021/content/textures/ui/Plastic.png differ diff --git a/Client2021/content/textures/ui/PlayerList/Accept.png b/Client2021/content/textures/ui/PlayerList/Accept.png new file mode 100644 index 0000000..17c4258 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/Accept.png differ diff --git a/Client2021/content/textures/ui/PlayerList/Accept@2x.png b/Client2021/content/textures/ui/PlayerList/Accept@2x.png new file mode 100644 index 0000000..83a4af7 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/Accept@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/Accept@3x.png b/Client2021/content/textures/ui/PlayerList/Accept@3x.png new file mode 100644 index 0000000..97af405 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/Accept@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/AcceptButton.png b/Client2021/content/textures/ui/PlayerList/AcceptButton.png new file mode 100644 index 0000000..ae223d2 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/AcceptButton.png differ diff --git a/Client2021/content/textures/ui/PlayerList/AddFriend.png b/Client2021/content/textures/ui/PlayerList/AddFriend.png new file mode 100644 index 0000000..734d61a Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/AddFriend.png differ diff --git a/Client2021/content/textures/ui/PlayerList/AddFriend@2x.png b/Client2021/content/textures/ui/PlayerList/AddFriend@2x.png new file mode 100644 index 0000000..91ad14d Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/AddFriend@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/AddFriend@3x.png b/Client2021/content/textures/ui/PlayerList/AddFriend@3x.png new file mode 100644 index 0000000..eec1ed5 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/AddFriend@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/AdminIcon.png b/Client2021/content/textures/ui/PlayerList/AdminIcon.png new file mode 100644 index 0000000..9fe796a Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/AdminIcon.png differ diff --git a/Client2021/content/textures/ui/PlayerList/AdminIcon@2x.png b/Client2021/content/textures/ui/PlayerList/AdminIcon@2x.png new file mode 100644 index 0000000..ec9ad96 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/AdminIcon@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/AdminIcon@3x.png b/Client2021/content/textures/ui/PlayerList/AdminIcon@3x.png new file mode 100644 index 0000000..4da946e Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/AdminIcon@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/AvatarBackground.png b/Client2021/content/textures/ui/PlayerList/AvatarBackground.png new file mode 100644 index 0000000..1ca8143 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/AvatarBackground.png differ diff --git a/Client2021/content/textures/ui/PlayerList/AvatarBackground@2x.png b/Client2021/content/textures/ui/PlayerList/AvatarBackground@2x.png new file mode 100644 index 0000000..907b997 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/AvatarBackground@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/AvatarBackground@3x.png b/Client2021/content/textures/ui/PlayerList/AvatarBackground@3x.png new file mode 100644 index 0000000..b7e5df2 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/AvatarBackground@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/Block.png b/Client2021/content/textures/ui/PlayerList/Block.png new file mode 100644 index 0000000..733f3db Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/Block.png differ diff --git a/Client2021/content/textures/ui/PlayerList/Block@2x.png b/Client2021/content/textures/ui/PlayerList/Block@2x.png new file mode 100644 index 0000000..5d8788a Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/Block@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/Block@3x.png b/Client2021/content/textures/ui/PlayerList/Block@3x.png new file mode 100644 index 0000000..194eba1 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/Block@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/BlockedIcon.png b/Client2021/content/textures/ui/PlayerList/BlockedIcon.png new file mode 100644 index 0000000..1ddb436 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/BlockedIcon.png differ diff --git a/Client2021/content/textures/ui/PlayerList/CharacterImageBackground.png b/Client2021/content/textures/ui/PlayerList/CharacterImageBackground.png new file mode 100644 index 0000000..2620ac5 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/CharacterImageBackground.png differ diff --git a/Client2021/content/textures/ui/PlayerList/Clear.png b/Client2021/content/textures/ui/PlayerList/Clear.png new file mode 100644 index 0000000..7ddf2a3 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/Clear.png differ diff --git a/Client2021/content/textures/ui/PlayerList/Clear@2x.png b/Client2021/content/textures/ui/PlayerList/Clear@2x.png new file mode 100644 index 0000000..4ae0fde Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/Clear@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/Clear@3x.png b/Client2021/content/textures/ui/PlayerList/Clear@3x.png new file mode 100644 index 0000000..8deff72 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/Clear@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/FollowingIcon.png b/Client2021/content/textures/ui/PlayerList/FollowingIcon.png new file mode 100644 index 0000000..31a8b11 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/FollowingIcon.png differ diff --git a/Client2021/content/textures/ui/PlayerList/FollowingIcon@2x.png b/Client2021/content/textures/ui/PlayerList/FollowingIcon@2x.png new file mode 100644 index 0000000..d8bd4fa Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/FollowingIcon@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/FollowingIcon@3x.png b/Client2021/content/textures/ui/PlayerList/FollowingIcon@3x.png new file mode 100644 index 0000000..def46a5 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/FollowingIcon@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/FriendIcon.png b/Client2021/content/textures/ui/PlayerList/FriendIcon.png new file mode 100644 index 0000000..07db059 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/FriendIcon.png differ diff --git a/Client2021/content/textures/ui/PlayerList/FriendIcon@2x.png b/Client2021/content/textures/ui/PlayerList/FriendIcon@2x.png new file mode 100644 index 0000000..acc0cfe Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/FriendIcon@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/FriendIcon@3x.png b/Client2021/content/textures/ui/PlayerList/FriendIcon@3x.png new file mode 100644 index 0000000..9db9fce Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/FriendIcon@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/NewAvatarBackground.png b/Client2021/content/textures/ui/PlayerList/NewAvatarBackground.png new file mode 100644 index 0000000..3a69036 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/NewAvatarBackground.png differ diff --git a/Client2021/content/textures/ui/PlayerList/NewAvatarBackground@2x.png b/Client2021/content/textures/ui/PlayerList/NewAvatarBackground@2x.png new file mode 100644 index 0000000..c63ed56 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/NewAvatarBackground@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/NewAvatarBackground@3x.png b/Client2021/content/textures/ui/PlayerList/NewAvatarBackground@3x.png new file mode 100644 index 0000000..af99a97 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/NewAvatarBackground@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/NewFollowing.png b/Client2021/content/textures/ui/PlayerList/NewFollowing.png new file mode 100644 index 0000000..d98c3eb Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/NewFollowing.png differ diff --git a/Client2021/content/textures/ui/PlayerList/NewFollowing@2x.png b/Client2021/content/textures/ui/PlayerList/NewFollowing@2x.png new file mode 100644 index 0000000..672b7fa Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/NewFollowing@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/NewFollowing@3x.png b/Client2021/content/textures/ui/PlayerList/NewFollowing@3x.png new file mode 100644 index 0000000..b12f374 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/NewFollowing@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/NotificationOff.png b/Client2021/content/textures/ui/PlayerList/NotificationOff.png new file mode 100644 index 0000000..6e2cd7b Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/NotificationOff.png differ diff --git a/Client2021/content/textures/ui/PlayerList/NotificationOff@2x.png b/Client2021/content/textures/ui/PlayerList/NotificationOff@2x.png new file mode 100644 index 0000000..8f7339b Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/NotificationOff@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/NotificationOff@3x.png b/Client2021/content/textures/ui/PlayerList/NotificationOff@3x.png new file mode 100644 index 0000000..07422ba Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/NotificationOff@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/NotificationOn.png b/Client2021/content/textures/ui/PlayerList/NotificationOn.png new file mode 100644 index 0000000..7420fc4 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/NotificationOn.png differ diff --git a/Client2021/content/textures/ui/PlayerList/NotificationOn@2x.png b/Client2021/content/textures/ui/PlayerList/NotificationOn@2x.png new file mode 100644 index 0000000..6e710f5 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/NotificationOn@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/NotificationOn@3x.png b/Client2021/content/textures/ui/PlayerList/NotificationOn@3x.png new file mode 100644 index 0000000..ac4501c Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/NotificationOn@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/OwnerIcon.png b/Client2021/content/textures/ui/PlayerList/OwnerIcon.png new file mode 100644 index 0000000..eb7f86b Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/OwnerIcon.png differ diff --git a/Client2021/content/textures/ui/PlayerList/OwnerIcon@2x.png b/Client2021/content/textures/ui/PlayerList/OwnerIcon@2x.png new file mode 100644 index 0000000..e419233 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/OwnerIcon@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/OwnerIcon@3x.png b/Client2021/content/textures/ui/PlayerList/OwnerIcon@3x.png new file mode 100644 index 0000000..ad6160a Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/OwnerIcon@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/PremiumIcon.png b/Client2021/content/textures/ui/PlayerList/PremiumIcon.png new file mode 100644 index 0000000..5ff348e Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/PremiumIcon.png differ diff --git a/Client2021/content/textures/ui/PlayerList/PremiumIcon@2x.png b/Client2021/content/textures/ui/PlayerList/PremiumIcon@2x.png new file mode 100644 index 0000000..3b7033d Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/PremiumIcon@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/PremiumIcon@3x.png b/Client2021/content/textures/ui/PlayerList/PremiumIcon@3x.png new file mode 100644 index 0000000..e47ec8b Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/PremiumIcon@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/Report.png b/Client2021/content/textures/ui/PlayerList/Report.png new file mode 100644 index 0000000..856168e Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/Report.png differ diff --git a/Client2021/content/textures/ui/PlayerList/Report@2x.png b/Client2021/content/textures/ui/PlayerList/Report@2x.png new file mode 100644 index 0000000..72fe895 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/Report@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/Report@3x.png b/Client2021/content/textures/ui/PlayerList/Report@3x.png new file mode 100644 index 0000000..538a17a Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/Report@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/SelectOn.png b/Client2021/content/textures/ui/PlayerList/SelectOn.png new file mode 100644 index 0000000..14bae6a Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/SelectOn.png differ diff --git a/Client2021/content/textures/ui/PlayerList/SelectOn@2x.png b/Client2021/content/textures/ui/PlayerList/SelectOn@2x.png new file mode 100644 index 0000000..57455fd Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/SelectOn@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/SelectOn@3x.png b/Client2021/content/textures/ui/PlayerList/SelectOn@3x.png new file mode 100644 index 0000000..2812bbc Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/SelectOn@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/StarIcon.png b/Client2021/content/textures/ui/PlayerList/StarIcon.png new file mode 100644 index 0000000..3b34cfd Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/StarIcon.png differ diff --git a/Client2021/content/textures/ui/PlayerList/StarIcon@2x.png b/Client2021/content/textures/ui/PlayerList/StarIcon@2x.png new file mode 100644 index 0000000..08dbc3f Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/StarIcon@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/StarIcon@3x.png b/Client2021/content/textures/ui/PlayerList/StarIcon@3x.png new file mode 100644 index 0000000..dea4e24 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/StarIcon@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/TileShadowMissingTop.png b/Client2021/content/textures/ui/PlayerList/TileShadowMissingTop.png new file mode 100644 index 0000000..d09f5fb Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/TileShadowMissingTop.png differ diff --git a/Client2021/content/textures/ui/PlayerList/UnFriend.png b/Client2021/content/textures/ui/PlayerList/UnFriend.png new file mode 100644 index 0000000..257796c Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/UnFriend.png differ diff --git a/Client2021/content/textures/ui/PlayerList/UnFriend@2x.png b/Client2021/content/textures/ui/PlayerList/UnFriend@2x.png new file mode 100644 index 0000000..5f7da30 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/UnFriend@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/UnFriend@3x.png b/Client2021/content/textures/ui/PlayerList/UnFriend@3x.png new file mode 100644 index 0000000..bfb72fc Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/UnFriend@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/ViewAvatar.png b/Client2021/content/textures/ui/PlayerList/ViewAvatar.png new file mode 100644 index 0000000..6f75c2e Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/ViewAvatar.png differ diff --git a/Client2021/content/textures/ui/PlayerList/ViewAvatar@2x.png b/Client2021/content/textures/ui/PlayerList/ViewAvatar@2x.png new file mode 100644 index 0000000..0b741cd Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/ViewAvatar@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/ViewAvatar@3x.png b/Client2021/content/textures/ui/PlayerList/ViewAvatar@3x.png new file mode 100644 index 0000000..5dee4ff Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/ViewAvatar@3x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/developer.png b/Client2021/content/textures/ui/PlayerList/developer.png new file mode 100644 index 0000000..50aadab Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/developer.png differ diff --git a/Client2021/content/textures/ui/PlayerList/developer@2x.png b/Client2021/content/textures/ui/PlayerList/developer@2x.png new file mode 100644 index 0000000..6b04624 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/developer@2x.png differ diff --git a/Client2021/content/textures/ui/PlayerList/developer@3x.png b/Client2021/content/textures/ui/PlayerList/developer@3x.png new file mode 100644 index 0000000..c1415a7 Binary files /dev/null and b/Client2021/content/textures/ui/PlayerList/developer@3x.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/LeftButton.png b/Client2021/content/textures/ui/PurchasePrompt/LeftButton.png new file mode 100644 index 0000000..bc8b4a1 Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/LeftButton.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/LeftButton@2x.png b/Client2021/content/textures/ui/PurchasePrompt/LeftButton@2x.png new file mode 100644 index 0000000..0ff72eb Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/LeftButton@2x.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/LeftButtonDown.png b/Client2021/content/textures/ui/PurchasePrompt/LeftButtonDown.png new file mode 100644 index 0000000..d97bd19 Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/LeftButtonDown.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/LeftButtonDown@2x.png b/Client2021/content/textures/ui/PurchasePrompt/LeftButtonDown@2x.png new file mode 100644 index 0000000..3796731 Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/LeftButtonDown@2x.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/LoadingBG.png b/Client2021/content/textures/ui/PurchasePrompt/LoadingBG.png new file mode 100644 index 0000000..59d572b Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/LoadingBG.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/LoadingBG@2x.png b/Client2021/content/textures/ui/PurchasePrompt/LoadingBG@2x.png new file mode 100644 index 0000000..f21b03c Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/LoadingBG@2x.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/Premium.png b/Client2021/content/textures/ui/PurchasePrompt/Premium.png new file mode 100644 index 0000000..f95800d Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/Premium.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/PurchasePromptBG.png b/Client2021/content/textures/ui/PurchasePrompt/PurchasePromptBG.png new file mode 100644 index 0000000..2feb249 Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/PurchasePromptBG.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/PurchasePromptBG@2x.png b/Client2021/content/textures/ui/PurchasePrompt/PurchasePromptBG@2x.png new file mode 100644 index 0000000..13654f8 Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/PurchasePromptBG@2x.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/RightButton.png b/Client2021/content/textures/ui/PurchasePrompt/RightButton.png new file mode 100644 index 0000000..55089ea Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/RightButton.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/RightButton@2x.png b/Client2021/content/textures/ui/PurchasePrompt/RightButton@2x.png new file mode 100644 index 0000000..ae62615 Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/RightButton@2x.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/RightButtonDown.png b/Client2021/content/textures/ui/PurchasePrompt/RightButtonDown.png new file mode 100644 index 0000000..592ff90 Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/RightButtonDown.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/RightButtonDown@2x.png b/Client2021/content/textures/ui/PurchasePrompt/RightButtonDown@2x.png new file mode 100644 index 0000000..828fe27 Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/RightButtonDown@2x.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/SingleButton.png b/Client2021/content/textures/ui/PurchasePrompt/SingleButton.png new file mode 100644 index 0000000..706a95a Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/SingleButton.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/SingleButton@2x.png b/Client2021/content/textures/ui/PurchasePrompt/SingleButton@2x.png new file mode 100644 index 0000000..0d01ba4 Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/SingleButton@2x.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/SingleButtonDown.png b/Client2021/content/textures/ui/PurchasePrompt/SingleButtonDown.png new file mode 100644 index 0000000..885b1bf Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/SingleButtonDown.png differ diff --git a/Client2021/content/textures/ui/PurchasePrompt/SingleButtonDown@2x.png b/Client2021/content/textures/ui/PurchasePrompt/SingleButtonDown@2x.png new file mode 100644 index 0000000..fe010c6 Binary files /dev/null and b/Client2021/content/textures/ui/PurchasePrompt/SingleButtonDown@2x.png differ diff --git a/Client2021/content/textures/ui/RecordDown.png b/Client2021/content/textures/ui/RecordDown.png new file mode 100644 index 0000000..9708d84 Binary files /dev/null and b/Client2021/content/textures/ui/RecordDown.png differ diff --git a/Client2021/content/textures/ui/ResetIcon.png b/Client2021/content/textures/ui/ResetIcon.png new file mode 100644 index 0000000..b16996e Binary files /dev/null and b/Client2021/content/textures/ui/ResetIcon.png differ diff --git a/Client2021/content/textures/ui/RobloxNameIcon.png b/Client2021/content/textures/ui/RobloxNameIcon.png new file mode 100644 index 0000000..360d9c2 Binary files /dev/null and b/Client2021/content/textures/ui/RobloxNameIcon.png differ diff --git a/Client2021/content/textures/ui/RobuxIcon.png b/Client2021/content/textures/ui/RobuxIcon.png new file mode 100644 index 0000000..d14fe16 Binary files /dev/null and b/Client2021/content/textures/ui/RobuxIcon.png differ diff --git a/Client2021/content/textures/ui/RoundedRect8px.png b/Client2021/content/textures/ui/RoundedRect8px.png new file mode 100644 index 0000000..245a445 Binary files /dev/null and b/Client2021/content/textures/ui/RoundedRect8px.png differ diff --git a/Client2021/content/textures/ui/Scroll/scroll-bottom.png b/Client2021/content/textures/ui/Scroll/scroll-bottom.png new file mode 100644 index 0000000..9c5695c Binary files /dev/null and b/Client2021/content/textures/ui/Scroll/scroll-bottom.png differ diff --git a/Client2021/content/textures/ui/Scroll/scroll-bottom@2x.png b/Client2021/content/textures/ui/Scroll/scroll-bottom@2x.png new file mode 100644 index 0000000..85b70e6 Binary files /dev/null and b/Client2021/content/textures/ui/Scroll/scroll-bottom@2x.png differ diff --git a/Client2021/content/textures/ui/Scroll/scroll-middle.png b/Client2021/content/textures/ui/Scroll/scroll-middle.png new file mode 100644 index 0000000..f4489d7 Binary files /dev/null and b/Client2021/content/textures/ui/Scroll/scroll-middle.png differ diff --git a/Client2021/content/textures/ui/Scroll/scroll-middle@2x.png b/Client2021/content/textures/ui/Scroll/scroll-middle@2x.png new file mode 100644 index 0000000..22d0f11 Binary files /dev/null and b/Client2021/content/textures/ui/Scroll/scroll-middle@2x.png differ diff --git a/Client2021/content/textures/ui/Scroll/scroll-top.png b/Client2021/content/textures/ui/Scroll/scroll-top.png new file mode 100644 index 0000000..bc6dcce Binary files /dev/null and b/Client2021/content/textures/ui/Scroll/scroll-top.png differ diff --git a/Client2021/content/textures/ui/Scroll/scroll-top@2x.png b/Client2021/content/textures/ui/Scroll/scroll-top@2x.png new file mode 100644 index 0000000..0ead0e8 Binary files /dev/null and b/Client2021/content/textures/ui/Scroll/scroll-top@2x.png differ diff --git a/Client2021/content/textures/ui/SearchIcon.png b/Client2021/content/textures/ui/SearchIcon.png new file mode 100644 index 0000000..58772d0 Binary files /dev/null and b/Client2021/content/textures/ui/SearchIcon.png differ diff --git a/Client2021/content/textures/ui/SelectionBox.png b/Client2021/content/textures/ui/SelectionBox.png new file mode 100644 index 0000000..290b647 Binary files /dev/null and b/Client2021/content/textures/ui/SelectionBox.png differ diff --git a/Client2021/content/textures/ui/SelectionBox@2x.png b/Client2021/content/textures/ui/SelectionBox@2x.png new file mode 100644 index 0000000..65d1711 Binary files /dev/null and b/Client2021/content/textures/ui/SelectionBox@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/DropDown/DropDown.png b/Client2021/content/textures/ui/Settings/DropDown/DropDown.png new file mode 100644 index 0000000..39e6dc8 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/DropDown/DropDown.png differ diff --git a/Client2021/content/textures/ui/Settings/DropDown/DropDown@2x.png b/Client2021/content/textures/ui/Settings/DropDown/DropDown@2x.png new file mode 100644 index 0000000..ccdd651 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/DropDown/DropDown@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/AButtonDark.png b/Client2021/content/textures/ui/Settings/Help/AButtonDark.png new file mode 100644 index 0000000..da4ec7a Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/AButtonDark.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/AButtonDark@2x.png b/Client2021/content/textures/ui/Settings/Help/AButtonDark@2x.png new file mode 100644 index 0000000..b2e9cba Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/AButtonDark@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/AButtonLight.png b/Client2021/content/textures/ui/Settings/Help/AButtonLight.png new file mode 100644 index 0000000..431e4a7 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/AButtonLight.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/AButtonLight@2x.png b/Client2021/content/textures/ui/Settings/Help/AButtonLight@2x.png new file mode 100644 index 0000000..d140dba Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/AButtonLight@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/AButtonLightSmall.png b/Client2021/content/textures/ui/Settings/Help/AButtonLightSmall.png new file mode 100644 index 0000000..50f8035 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/AButtonLightSmall.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/BButtonDark.png b/Client2021/content/textures/ui/Settings/Help/BButtonDark.png new file mode 100644 index 0000000..9b607a9 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/BButtonDark.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/BButtonDark@2x.png b/Client2021/content/textures/ui/Settings/Help/BButtonDark@2x.png new file mode 100644 index 0000000..df1a1a5 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/BButtonDark@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/BButtonLight.png b/Client2021/content/textures/ui/Settings/Help/BButtonLight.png new file mode 100644 index 0000000..598b7b9 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/BButtonLight.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/BButtonLight@2x.png b/Client2021/content/textures/ui/Settings/Help/BButtonLight@2x.png new file mode 100644 index 0000000..e2e67db Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/BButtonLight@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/EscapeIcon.png b/Client2021/content/textures/ui/Settings/Help/EscapeIcon.png new file mode 100644 index 0000000..7c53d55 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/EscapeIcon.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/GenericController.png b/Client2021/content/textures/ui/Settings/Help/GenericController.png new file mode 100644 index 0000000..db52b1f Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/GenericController.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/GenericController@2x.png b/Client2021/content/textures/ui/Settings/Help/GenericController@2x.png new file mode 100644 index 0000000..3735735 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/GenericController@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/LeaveIcon.png b/Client2021/content/textures/ui/Settings/Help/LeaveIcon.png new file mode 100644 index 0000000..6d780ae Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/LeaveIcon.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/ResetIcon.png b/Client2021/content/textures/ui/Settings/Help/ResetIcon.png new file mode 100644 index 0000000..391ac31 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/ResetIcon.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/RotateCameraGesture.png b/Client2021/content/textures/ui/Settings/Help/RotateCameraGesture.png new file mode 100644 index 0000000..c0147d6 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/RotateCameraGesture.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/UseToolGesture.png b/Client2021/content/textures/ui/Settings/Help/UseToolGesture.png new file mode 100644 index 0000000..fe3c364 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/UseToolGesture.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/XButtonDark.png b/Client2021/content/textures/ui/Settings/Help/XButtonDark.png new file mode 100644 index 0000000..cc71edc Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/XButtonDark.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/XButtonDark@2x.png b/Client2021/content/textures/ui/Settings/Help/XButtonDark@2x.png new file mode 100644 index 0000000..0d53c7a Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/XButtonDark@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/XButtonLight.png b/Client2021/content/textures/ui/Settings/Help/XButtonLight.png new file mode 100644 index 0000000..62c13ec Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/XButtonLight.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/XButtonLight@2x.png b/Client2021/content/textures/ui/Settings/Help/XButtonLight@2x.png new file mode 100644 index 0000000..1d5bc77 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/XButtonLight@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/XboxController.png b/Client2021/content/textures/ui/Settings/Help/XboxController.png new file mode 100644 index 0000000..882124a Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/XboxController.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/XboxController@2x.png b/Client2021/content/textures/ui/Settings/Help/XboxController@2x.png new file mode 100644 index 0000000..9ea335a Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/XboxController@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/YButtonDark.png b/Client2021/content/textures/ui/Settings/Help/YButtonDark.png new file mode 100644 index 0000000..131c8ca Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/YButtonDark.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/YButtonDark@2x.png b/Client2021/content/textures/ui/Settings/Help/YButtonDark@2x.png new file mode 100644 index 0000000..3b5e223 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/YButtonDark@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/YButtonLight.png b/Client2021/content/textures/ui/Settings/Help/YButtonLight.png new file mode 100644 index 0000000..56ce742 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/YButtonLight.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/YButtonLight@2x.png b/Client2021/content/textures/ui/Settings/Help/YButtonLight@2x.png new file mode 100644 index 0000000..1a3ee30 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/YButtonLight@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Help/ZoomGesture.png b/Client2021/content/textures/ui/Settings/Help/ZoomGesture.png new file mode 100644 index 0000000..68ee910 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Help/ZoomGesture.png differ diff --git a/Client2021/content/textures/ui/Settings/LeaveGame/Button_1080.png b/Client2021/content/textures/ui/Settings/LeaveGame/Button_1080.png new file mode 100644 index 0000000..54ddf21 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/LeaveGame/Button_1080.png differ diff --git a/Client2021/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow.png b/Client2021/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow.png new file mode 100644 index 0000000..5949f9a Binary files /dev/null and b/Client2021/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow.png differ diff --git a/Client2021/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow@2x.png b/Client2021/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow@2x.png new file mode 100644 index 0000000..0ff5623 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow@3x.png b/Client2021/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow@3x.png new file mode 100644 index 0000000..a157bf9 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/LeaveGame/artAssets_DownArrow@3x.png differ diff --git a/Client2021/content/textures/ui/Settings/LeaveGame/gr-item selector-8px corner.png b/Client2021/content/textures/ui/Settings/LeaveGame/gr-item selector-8px corner.png new file mode 100644 index 0000000..9cb9fb2 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/LeaveGame/gr-item selector-8px corner.png differ diff --git a/Client2021/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle.png b/Client2021/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle.png new file mode 100644 index 0000000..f142181 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle.png differ diff --git a/Client2021/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle@2x.png b/Client2021/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle@2x.png new file mode 100644 index 0000000..432dece Binary files /dev/null and b/Client2021/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle@3x.png b/Client2021/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle@3x.png new file mode 100644 index 0000000..f94d265 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/LeaveGame/playernumber_strokeStyle@3x.png differ diff --git a/Client2021/content/textures/ui/Settings/LeaveGame/selectorWithIcon.png b/Client2021/content/textures/ui/Settings/LeaveGame/selectorWithIcon.png new file mode 100644 index 0000000..99764d2 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/LeaveGame/selectorWithIcon.png differ diff --git a/Client2021/content/textures/ui/Settings/LeaveGame/selectorWithIcon@2x.png b/Client2021/content/textures/ui/Settings/LeaveGame/selectorWithIcon@2x.png new file mode 100644 index 0000000..7dfdbcf Binary files /dev/null and b/Client2021/content/textures/ui/Settings/LeaveGame/selectorWithIcon@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/LeaveGame/selectorWithIcon@3x.png b/Client2021/content/textures/ui/Settings/LeaveGame/selectorWithIcon@3x.png new file mode 100644 index 0000000..f201559 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/LeaveGame/selectorWithIcon@3x.png differ diff --git a/Client2021/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle.png b/Client2021/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle.png new file mode 100644 index 0000000..423d326 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle.png differ diff --git a/Client2021/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle@2x.png b/Client2021/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle@2x.png new file mode 100644 index 0000000..c373e16 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle@3x.png b/Client2021/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle@3x.png new file mode 100644 index 0000000..9bcf274 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/LeaveGame/thumb_strokeStyle@3x.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuBackground.png b/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuBackground.png new file mode 100644 index 0000000..4614da6 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuBackground.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuButton.png b/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuButton.png new file mode 100644 index 0000000..632c7d2 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuButton.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuButton@2x.png b/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuButton@2x.png new file mode 100644 index 0000000..a7f7a27 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuButton@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png b/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png new file mode 100644 index 0000000..4a4267e Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected@2x.png b/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected@2x.png new file mode 100644 index 0000000..3c33658 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuButtonSelected@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuSelection.png b/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuSelection.png new file mode 100644 index 0000000..c0f8936 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuSelection.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuSelection@2x.png b/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuSelection@2x.png new file mode 100644 index 0000000..b3eb828 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarAssets/MenuSelection@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab.png b/Client2021/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab.png new file mode 100644 index 0000000..5b0c02c Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab@2x.png b/Client2021/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab@2x.png new file mode 100644 index 0000000..61a2380 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarIcons/GameSettingsTab@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarIcons/HelpTab.png b/Client2021/content/textures/ui/Settings/MenuBarIcons/HelpTab.png new file mode 100644 index 0000000..a3a18c1 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarIcons/HelpTab.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarIcons/HelpTab@2x.png b/Client2021/content/textures/ui/Settings/MenuBarIcons/HelpTab@2x.png new file mode 100644 index 0000000..c55cfad Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarIcons/HelpTab@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarIcons/HomeTab.png b/Client2021/content/textures/ui/Settings/MenuBarIcons/HomeTab.png new file mode 100644 index 0000000..e1a6468 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarIcons/HomeTab.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarIcons/HomeTab@2x.png b/Client2021/content/textures/ui/Settings/MenuBarIcons/HomeTab@2x.png new file mode 100644 index 0000000..edec627 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarIcons/HomeTab@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon.png b/Client2021/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon.png new file mode 100644 index 0000000..fc253e4 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon@2x.png b/Client2021/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon@2x.png new file mode 100644 index 0000000..8ab99f7 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarIcons/PlayersTabIcon@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarIcons/RecordTab.png b/Client2021/content/textures/ui/Settings/MenuBarIcons/RecordTab.png new file mode 100644 index 0000000..37616b6 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarIcons/RecordTab.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarIcons/RecordTab@2x.png b/Client2021/content/textures/ui/Settings/MenuBarIcons/RecordTab@2x.png new file mode 100644 index 0000000..ee7db49 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarIcons/RecordTab@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab.png b/Client2021/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab.png new file mode 100644 index 0000000..69eb788 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab.png differ diff --git a/Client2021/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab@2x.png b/Client2021/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab@2x.png new file mode 100644 index 0000000..8efb061 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/MenuBarIcons/ReportAbuseTab@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Players/AddFriendIcon.png b/Client2021/content/textures/ui/Settings/Players/AddFriendIcon.png new file mode 100644 index 0000000..d63e933 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Players/AddFriendIcon.png differ diff --git a/Client2021/content/textures/ui/Settings/Players/AddFriendIcon@2x.png b/Client2021/content/textures/ui/Settings/Players/AddFriendIcon@2x.png new file mode 100644 index 0000000..ddb5658 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Players/AddFriendIcon@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Players/FriendIcon.png b/Client2021/content/textures/ui/Settings/Players/FriendIcon.png new file mode 100644 index 0000000..ce78c14 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Players/FriendIcon.png differ diff --git a/Client2021/content/textures/ui/Settings/Players/FriendIcon@2x.png b/Client2021/content/textures/ui/Settings/Players/FriendIcon@2x.png new file mode 100644 index 0000000..420f532 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Players/FriendIcon@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Players/ReportFlagIcon.png b/Client2021/content/textures/ui/Settings/Players/ReportFlagIcon.png new file mode 100644 index 0000000..667ce8b Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Players/ReportFlagIcon.png differ diff --git a/Client2021/content/textures/ui/Settings/Players/ReportFlagIcon@2x.png b/Client2021/content/textures/ui/Settings/Players/ReportFlagIcon@2x.png new file mode 100644 index 0000000..e5346b0 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Players/ReportFlagIcon@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/Alert.png b/Client2021/content/textures/ui/Settings/Radial/Alert.png new file mode 100644 index 0000000..314d122 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/Alert.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/Alert@2x.png b/Client2021/content/textures/ui/Settings/Radial/Alert@2x.png new file mode 100644 index 0000000..6c3c966 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/Alert@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/Backpack.png b/Client2021/content/textures/ui/Settings/Radial/Backpack.png new file mode 100644 index 0000000..4658ae8 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/Backpack.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/Backpack@2x.png b/Client2021/content/textures/ui/Settings/Radial/Backpack@2x.png new file mode 100644 index 0000000..d222525 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/Backpack@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/Bottom.png b/Client2021/content/textures/ui/Settings/Radial/Bottom.png new file mode 100644 index 0000000..edb40d9 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/Bottom.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/BottomLeft.png b/Client2021/content/textures/ui/Settings/Radial/BottomLeft.png new file mode 100644 index 0000000..4560e63 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/BottomLeft.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/BottomLeftSelected.png b/Client2021/content/textures/ui/Settings/Radial/BottomLeftSelected.png new file mode 100644 index 0000000..f678b7d Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/BottomLeftSelected.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/BottomRight.png b/Client2021/content/textures/ui/Settings/Radial/BottomRight.png new file mode 100644 index 0000000..34ea9d2 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/BottomRight.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/BottomRightSelected.png b/Client2021/content/textures/ui/Settings/Radial/BottomRightSelected.png new file mode 100644 index 0000000..c044854 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/BottomRightSelected.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/BottomSelected.png b/Client2021/content/textures/ui/Settings/Radial/BottomSelected.png new file mode 100644 index 0000000..eed548c Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/BottomSelected.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/Chat.png b/Client2021/content/textures/ui/Settings/Radial/Chat.png new file mode 100644 index 0000000..b0bc68c Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/Chat.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/Chat@2x.png b/Client2021/content/textures/ui/Settings/Radial/Chat@2x.png new file mode 100644 index 0000000..1b9e5bc Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/Chat@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/EmptyBottom.png b/Client2021/content/textures/ui/Settings/Radial/EmptyBottom.png new file mode 100644 index 0000000..964257a Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/EmptyBottom.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/EmptyBottomLeft.png b/Client2021/content/textures/ui/Settings/Radial/EmptyBottomLeft.png new file mode 100644 index 0000000..a7b3a80 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/EmptyBottomLeft.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/EmptyBottomRight.png b/Client2021/content/textures/ui/Settings/Radial/EmptyBottomRight.png new file mode 100644 index 0000000..87e8935 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/EmptyBottomRight.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/EmptyTop.png b/Client2021/content/textures/ui/Settings/Radial/EmptyTop.png new file mode 100644 index 0000000..90da292 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/EmptyTop.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/EmptyTopLeft.png b/Client2021/content/textures/ui/Settings/Radial/EmptyTopLeft.png new file mode 100644 index 0000000..84fd9a4 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/EmptyTopLeft.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/EmptyTopRight.png b/Client2021/content/textures/ui/Settings/Radial/EmptyTopRight.png new file mode 100644 index 0000000..53be061 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/EmptyTopRight.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/Leave.png b/Client2021/content/textures/ui/Settings/Radial/Leave.png new file mode 100644 index 0000000..5f69290 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/Leave.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/Leave@2x.png b/Client2021/content/textures/ui/Settings/Radial/Leave@2x.png new file mode 100644 index 0000000..d4f66b5 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/Leave@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/Menu.png b/Client2021/content/textures/ui/Settings/Radial/Menu.png new file mode 100644 index 0000000..bff7118 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/Menu.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/Menu@2x.png b/Client2021/content/textures/ui/Settings/Radial/Menu@2x.png new file mode 100644 index 0000000..b56e6b6 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/Menu@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/PlayerList.png b/Client2021/content/textures/ui/Settings/Radial/PlayerList.png new file mode 100644 index 0000000..fae417d Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/PlayerList.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/PlayerList@2x.png b/Client2021/content/textures/ui/Settings/Radial/PlayerList@2x.png new file mode 100644 index 0000000..9a91e3e Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/PlayerList@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/RadialLabel.png b/Client2021/content/textures/ui/Settings/Radial/RadialLabel.png new file mode 100644 index 0000000..c7065c9 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/RadialLabel.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/RadialLabel@2x.png b/Client2021/content/textures/ui/Settings/Radial/RadialLabel@2x.png new file mode 100644 index 0000000..349aed3 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/RadialLabel@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/Top.png b/Client2021/content/textures/ui/Settings/Radial/Top.png new file mode 100644 index 0000000..a87d6eb Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/Top.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/TopLeft.png b/Client2021/content/textures/ui/Settings/Radial/TopLeft.png new file mode 100644 index 0000000..64b0ec1 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/TopLeft.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/TopLeftSelected.png b/Client2021/content/textures/ui/Settings/Radial/TopLeftSelected.png new file mode 100644 index 0000000..0069778 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/TopLeftSelected.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/TopRight.png b/Client2021/content/textures/ui/Settings/Radial/TopRight.png new file mode 100644 index 0000000..73831c9 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/TopRight.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/TopRightSelected.png b/Client2021/content/textures/ui/Settings/Radial/TopRightSelected.png new file mode 100644 index 0000000..9ead602 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/TopRightSelected.png differ diff --git a/Client2021/content/textures/ui/Settings/Radial/TopSelected.png b/Client2021/content/textures/ui/Settings/Radial/TopSelected.png new file mode 100644 index 0000000..6c73255 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Radial/TopSelected.png differ diff --git a/Client2021/content/textures/ui/Settings/ShareGame/icons.png b/Client2021/content/textures/ui/Settings/ShareGame/icons.png new file mode 100644 index 0000000..b99f2dc Binary files /dev/null and b/Client2021/content/textures/ui/Settings/ShareGame/icons.png differ diff --git a/Client2021/content/textures/ui/Settings/ShareGame/icons@2x.png b/Client2021/content/textures/ui/Settings/ShareGame/icons@2x.png new file mode 100644 index 0000000..202d232 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/ShareGame/icons@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/ShareGame/icons@3x.png b/Client2021/content/textures/ui/Settings/ShareGame/icons@3x.png new file mode 100644 index 0000000..a13edee Binary files /dev/null and b/Client2021/content/textures/ui/Settings/ShareGame/icons@3x.png differ diff --git a/Client2021/content/textures/ui/Settings/Slider/BarLeft.png b/Client2021/content/textures/ui/Settings/Slider/BarLeft.png new file mode 100644 index 0000000..1787ff0 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Slider/BarLeft.png differ diff --git a/Client2021/content/textures/ui/Settings/Slider/BarLeft@2x.png b/Client2021/content/textures/ui/Settings/Slider/BarLeft@2x.png new file mode 100644 index 0000000..b6bb751 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Slider/BarLeft@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Slider/BarRight.png b/Client2021/content/textures/ui/Settings/Slider/BarRight.png new file mode 100644 index 0000000..04bca90 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Slider/BarRight.png differ diff --git a/Client2021/content/textures/ui/Settings/Slider/BarRight@2x.png b/Client2021/content/textures/ui/Settings/Slider/BarRight@2x.png new file mode 100644 index 0000000..26e8a6f Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Slider/BarRight@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Slider/Left.png b/Client2021/content/textures/ui/Settings/Slider/Left.png new file mode 100644 index 0000000..c078d2c Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Slider/Left.png differ diff --git a/Client2021/content/textures/ui/Settings/Slider/Left@2x.png b/Client2021/content/textures/ui/Settings/Slider/Left@2x.png new file mode 100644 index 0000000..3ec089d Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Slider/Left@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Slider/Less.png b/Client2021/content/textures/ui/Settings/Slider/Less.png new file mode 100644 index 0000000..afbb0a6 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Slider/Less.png differ diff --git a/Client2021/content/textures/ui/Settings/Slider/More.png b/Client2021/content/textures/ui/Settings/Slider/More.png new file mode 100644 index 0000000..05edb54 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Slider/More.png differ diff --git a/Client2021/content/textures/ui/Settings/Slider/Right.png b/Client2021/content/textures/ui/Settings/Slider/Right.png new file mode 100644 index 0000000..a607724 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Slider/Right.png differ diff --git a/Client2021/content/textures/ui/Settings/Slider/Right@2x.png b/Client2021/content/textures/ui/Settings/Slider/Right@2x.png new file mode 100644 index 0000000..53ef3f8 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Slider/Right@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Slider/SelectedBarLeft.png b/Client2021/content/textures/ui/Settings/Slider/SelectedBarLeft.png new file mode 100644 index 0000000..6c98b40 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Slider/SelectedBarLeft.png differ diff --git a/Client2021/content/textures/ui/Settings/Slider/SelectedBarLeft@2x.png b/Client2021/content/textures/ui/Settings/Slider/SelectedBarLeft@2x.png new file mode 100644 index 0000000..6476f9e Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Slider/SelectedBarLeft@2x.png differ diff --git a/Client2021/content/textures/ui/Settings/Slider/SelectedBarRight.png b/Client2021/content/textures/ui/Settings/Slider/SelectedBarRight.png new file mode 100644 index 0000000..a0e0162 Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Slider/SelectedBarRight.png differ diff --git a/Client2021/content/textures/ui/Settings/Slider/SelectedBarRight@2x.png b/Client2021/content/textures/ui/Settings/Slider/SelectedBarRight@2x.png new file mode 100644 index 0000000..d5229bc Binary files /dev/null and b/Client2021/content/textures/ui/Settings/Slider/SelectedBarRight@2x.png differ diff --git a/Client2021/content/textures/ui/SingleButton.png b/Client2021/content/textures/ui/SingleButton.png new file mode 100644 index 0000000..27d2c0d Binary files /dev/null and b/Client2021/content/textures/ui/SingleButton.png differ diff --git a/Client2021/content/textures/ui/SingleButtonDown.png b/Client2021/content/textures/ui/SingleButtonDown.png new file mode 100644 index 0000000..4e50523 Binary files /dev/null and b/Client2021/content/textures/ui/SingleButtonDown.png differ diff --git a/Client2021/content/textures/ui/Slider-BKG-Center.png b/Client2021/content/textures/ui/Slider-BKG-Center.png new file mode 100644 index 0000000..7563293 Binary files /dev/null and b/Client2021/content/textures/ui/Slider-BKG-Center.png differ diff --git a/Client2021/content/textures/ui/Slider-BKG-Center@2x.png b/Client2021/content/textures/ui/Slider-BKG-Center@2x.png new file mode 100644 index 0000000..284dae0 Binary files /dev/null and b/Client2021/content/textures/ui/Slider-BKG-Center@2x.png differ diff --git a/Client2021/content/textures/ui/Slider-BKG-Left-Cap.png b/Client2021/content/textures/ui/Slider-BKG-Left-Cap.png new file mode 100644 index 0000000..ab76642 Binary files /dev/null and b/Client2021/content/textures/ui/Slider-BKG-Left-Cap.png differ diff --git a/Client2021/content/textures/ui/Slider-BKG-Left-Cap@2x.png b/Client2021/content/textures/ui/Slider-BKG-Left-Cap@2x.png new file mode 100644 index 0000000..defda45 Binary files /dev/null and b/Client2021/content/textures/ui/Slider-BKG-Left-Cap@2x.png differ diff --git a/Client2021/content/textures/ui/Slider-BKG-Right-Cap.png b/Client2021/content/textures/ui/Slider-BKG-Right-Cap.png new file mode 100644 index 0000000..82d19f3 Binary files /dev/null and b/Client2021/content/textures/ui/Slider-BKG-Right-Cap.png differ diff --git a/Client2021/content/textures/ui/Slider-BKG-Right-Cap@2x.png b/Client2021/content/textures/ui/Slider-BKG-Right-Cap@2x.png new file mode 100644 index 0000000..0dcd8be Binary files /dev/null and b/Client2021/content/textures/ui/Slider-BKG-Right-Cap@2x.png differ diff --git a/Client2021/content/textures/ui/Slider-Fill-Center.png b/Client2021/content/textures/ui/Slider-Fill-Center.png new file mode 100644 index 0000000..a9db65e Binary files /dev/null and b/Client2021/content/textures/ui/Slider-Fill-Center.png differ diff --git a/Client2021/content/textures/ui/Slider-Fill-Center@2x.png b/Client2021/content/textures/ui/Slider-Fill-Center@2x.png new file mode 100644 index 0000000..c3355cc Binary files /dev/null and b/Client2021/content/textures/ui/Slider-Fill-Center@2x.png differ diff --git a/Client2021/content/textures/ui/Slider-Fill-Left-Cap.png b/Client2021/content/textures/ui/Slider-Fill-Left-Cap.png new file mode 100644 index 0000000..5d796da Binary files /dev/null and b/Client2021/content/textures/ui/Slider-Fill-Left-Cap.png differ diff --git a/Client2021/content/textures/ui/Slider-Fill-Left-Cap@2x.png b/Client2021/content/textures/ui/Slider-Fill-Left-Cap@2x.png new file mode 100644 index 0000000..2ff6d0b Binary files /dev/null and b/Client2021/content/textures/ui/Slider-Fill-Left-Cap@2x.png differ diff --git a/Client2021/content/textures/ui/Slider-Fill-Right-Cap.png b/Client2021/content/textures/ui/Slider-Fill-Right-Cap.png new file mode 100644 index 0000000..188106b Binary files /dev/null and b/Client2021/content/textures/ui/Slider-Fill-Right-Cap.png differ diff --git a/Client2021/content/textures/ui/Slider-Fill-Right-Cap@2x.png b/Client2021/content/textures/ui/Slider-Fill-Right-Cap@2x.png new file mode 100644 index 0000000..84ae4c8 Binary files /dev/null and b/Client2021/content/textures/ui/Slider-Fill-Right-Cap@2x.png differ diff --git a/Client2021/content/textures/ui/Slider.png b/Client2021/content/textures/ui/Slider.png new file mode 100644 index 0000000..b11c5a3 Binary files /dev/null and b/Client2021/content/textures/ui/Slider.png differ diff --git a/Client2021/content/textures/ui/Slider_dn.png b/Client2021/content/textures/ui/Slider_dn.png new file mode 100644 index 0000000..2aa542e Binary files /dev/null and b/Client2021/content/textures/ui/Slider_dn.png differ diff --git a/Client2021/content/textures/ui/Slider_sel.png b/Client2021/content/textures/ui/Slider_sel.png new file mode 100644 index 0000000..2aa542e Binary files /dev/null and b/Client2021/content/textures/ui/Slider_sel.png differ diff --git a/Client2021/content/textures/ui/TixIcon.png b/Client2021/content/textures/ui/TixIcon.png new file mode 100644 index 0000000..8b08e62 Binary files /dev/null and b/Client2021/content/textures/ui/TixIcon.png differ diff --git a/Client2021/content/textures/ui/TopBar/HealthBar.png b/Client2021/content/textures/ui/TopBar/HealthBar.png new file mode 100644 index 0000000..f9d4c2f Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/HealthBar.png differ diff --git a/Client2021/content/textures/ui/TopBar/HealthBarBase.png b/Client2021/content/textures/ui/TopBar/HealthBarBase.png new file mode 100644 index 0000000..0439bc3 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/HealthBarBase.png differ diff --git a/Client2021/content/textures/ui/TopBar/HealthBarBaseTV.png b/Client2021/content/textures/ui/TopBar/HealthBarBaseTV.png new file mode 100644 index 0000000..115a95b Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/HealthBarBaseTV.png differ diff --git a/Client2021/content/textures/ui/TopBar/HealthBarTV.png b/Client2021/content/textures/ui/TopBar/HealthBarTV.png new file mode 100644 index 0000000..8476cf1 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/HealthBarTV.png differ diff --git a/Client2021/content/textures/ui/TopBar/Round.png b/Client2021/content/textures/ui/TopBar/Round.png new file mode 100644 index 0000000..a596802 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/Round.png differ diff --git a/Client2021/content/textures/ui/TopBar/WhiteOverlayAsset.png b/Client2021/content/textures/ui/TopBar/WhiteOverlayAsset.png new file mode 100644 index 0000000..87a654a Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/WhiteOverlayAsset.png differ diff --git a/Client2021/content/textures/ui/TopBar/chatOff.png b/Client2021/content/textures/ui/TopBar/chatOff.png new file mode 100644 index 0000000..d402937 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/chatOff.png differ diff --git a/Client2021/content/textures/ui/TopBar/chatOff@2x.png b/Client2021/content/textures/ui/TopBar/chatOff@2x.png new file mode 100644 index 0000000..373df97 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/chatOff@2x.png differ diff --git a/Client2021/content/textures/ui/TopBar/chatOff@3x.png b/Client2021/content/textures/ui/TopBar/chatOff@3x.png new file mode 100644 index 0000000..b30d557 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/chatOff@3x.png differ diff --git a/Client2021/content/textures/ui/TopBar/chatOn.png b/Client2021/content/textures/ui/TopBar/chatOn.png new file mode 100644 index 0000000..ff63fba Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/chatOn.png differ diff --git a/Client2021/content/textures/ui/TopBar/chatOn@2x.png b/Client2021/content/textures/ui/TopBar/chatOn@2x.png new file mode 100644 index 0000000..873c4dc Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/chatOn@2x.png differ diff --git a/Client2021/content/textures/ui/TopBar/chatOn@3x.png b/Client2021/content/textures/ui/TopBar/chatOn@3x.png new file mode 100644 index 0000000..1ad2470 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/chatOn@3x.png differ diff --git a/Client2021/content/textures/ui/TopBar/close.png b/Client2021/content/textures/ui/TopBar/close.png new file mode 100644 index 0000000..9257b02 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/close.png differ diff --git a/Client2021/content/textures/ui/TopBar/close@2x.png b/Client2021/content/textures/ui/TopBar/close@2x.png new file mode 100644 index 0000000..970d2a8 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/close@2x.png differ diff --git a/Client2021/content/textures/ui/TopBar/close@3x.png b/Client2021/content/textures/ui/TopBar/close@3x.png new file mode 100644 index 0000000..dfebebe Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/close@3x.png differ diff --git a/Client2021/content/textures/ui/TopBar/coloredlogo.png b/Client2021/content/textures/ui/TopBar/coloredlogo.png new file mode 100644 index 0000000..0dbef90 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/coloredlogo.png differ diff --git a/Client2021/content/textures/ui/TopBar/coloredlogo@2x.png b/Client2021/content/textures/ui/TopBar/coloredlogo@2x.png new file mode 100644 index 0000000..bad82f2 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/coloredlogo@2x.png differ diff --git a/Client2021/content/textures/ui/TopBar/coloredlogo@3x.png b/Client2021/content/textures/ui/TopBar/coloredlogo@3x.png new file mode 100644 index 0000000..66cc5d6 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/coloredlogo@3x.png differ diff --git a/Client2021/content/textures/ui/TopBar/dropshadow.png b/Client2021/content/textures/ui/TopBar/dropshadow.png new file mode 100644 index 0000000..0e9aa0b Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/dropshadow.png differ diff --git a/Client2021/content/textures/ui/TopBar/dropshadow@2x.png b/Client2021/content/textures/ui/TopBar/dropshadow@2x.png new file mode 100644 index 0000000..9b14c0f Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/dropshadow@2x.png differ diff --git a/Client2021/content/textures/ui/TopBar/emotesOff.png b/Client2021/content/textures/ui/TopBar/emotesOff.png new file mode 100644 index 0000000..264fccf Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/emotesOff.png differ diff --git a/Client2021/content/textures/ui/TopBar/emotesOff@2x.png b/Client2021/content/textures/ui/TopBar/emotesOff@2x.png new file mode 100644 index 0000000..b7a4964 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/emotesOff@2x.png differ diff --git a/Client2021/content/textures/ui/TopBar/emotesOff@3x.png b/Client2021/content/textures/ui/TopBar/emotesOff@3x.png new file mode 100644 index 0000000..18c8667 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/emotesOff@3x.png differ diff --git a/Client2021/content/textures/ui/TopBar/emotesOn.png b/Client2021/content/textures/ui/TopBar/emotesOn.png new file mode 100644 index 0000000..cb94c1a Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/emotesOn.png differ diff --git a/Client2021/content/textures/ui/TopBar/emotesOn@2x.png b/Client2021/content/textures/ui/TopBar/emotesOn@2x.png new file mode 100644 index 0000000..8c856ce Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/emotesOn@2x.png differ diff --git a/Client2021/content/textures/ui/TopBar/emotesOn@3x.png b/Client2021/content/textures/ui/TopBar/emotesOn@3x.png new file mode 100644 index 0000000..7e189ab Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/emotesOn@3x.png differ diff --git a/Client2021/content/textures/ui/TopBar/iconBase.png b/Client2021/content/textures/ui/TopBar/iconBase.png new file mode 100644 index 0000000..c3e6531 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/iconBase.png differ diff --git a/Client2021/content/textures/ui/TopBar/iconBase@2x.png b/Client2021/content/textures/ui/TopBar/iconBase@2x.png new file mode 100644 index 0000000..ef9cb30 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/iconBase@2x.png differ diff --git a/Client2021/content/textures/ui/TopBar/iconBase@3x.png b/Client2021/content/textures/ui/TopBar/iconBase@3x.png new file mode 100644 index 0000000..abe4210 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/iconBase@3x.png differ diff --git a/Client2021/content/textures/ui/TopBar/inventoryOff.png b/Client2021/content/textures/ui/TopBar/inventoryOff.png new file mode 100644 index 0000000..4bbd0a3 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/inventoryOff.png differ diff --git a/Client2021/content/textures/ui/TopBar/inventoryOff@2x.png b/Client2021/content/textures/ui/TopBar/inventoryOff@2x.png new file mode 100644 index 0000000..9359065 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/inventoryOff@2x.png differ diff --git a/Client2021/content/textures/ui/TopBar/inventoryOff@3x.png b/Client2021/content/textures/ui/TopBar/inventoryOff@3x.png new file mode 100644 index 0000000..001f48a Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/inventoryOff@3x.png differ diff --git a/Client2021/content/textures/ui/TopBar/inventoryOn.png b/Client2021/content/textures/ui/TopBar/inventoryOn.png new file mode 100644 index 0000000..c0eaaa9 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/inventoryOn.png differ diff --git a/Client2021/content/textures/ui/TopBar/inventoryOn@2x.png b/Client2021/content/textures/ui/TopBar/inventoryOn@2x.png new file mode 100644 index 0000000..944c3c9 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/inventoryOn@2x.png differ diff --git a/Client2021/content/textures/ui/TopBar/inventoryOn@3x.png b/Client2021/content/textures/ui/TopBar/inventoryOn@3x.png new file mode 100644 index 0000000..bab32ca Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/inventoryOn@3x.png differ diff --git a/Client2021/content/textures/ui/TopBar/leaderboardOff.png b/Client2021/content/textures/ui/TopBar/leaderboardOff.png new file mode 100644 index 0000000..b3cfeb6 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/leaderboardOff.png differ diff --git a/Client2021/content/textures/ui/TopBar/leaderboardOff@2x.png b/Client2021/content/textures/ui/TopBar/leaderboardOff@2x.png new file mode 100644 index 0000000..fd63518 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/leaderboardOff@2x.png differ diff --git a/Client2021/content/textures/ui/TopBar/leaderboardOff@3x.png b/Client2021/content/textures/ui/TopBar/leaderboardOff@3x.png new file mode 100644 index 0000000..3521087 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/leaderboardOff@3x.png differ diff --git a/Client2021/content/textures/ui/TopBar/leaderboardOn.png b/Client2021/content/textures/ui/TopBar/leaderboardOn.png new file mode 100644 index 0000000..8184527 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/leaderboardOn.png differ diff --git a/Client2021/content/textures/ui/TopBar/leaderboardOn@2x.png b/Client2021/content/textures/ui/TopBar/leaderboardOn@2x.png new file mode 100644 index 0000000..d5c2899 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/leaderboardOn@2x.png differ diff --git a/Client2021/content/textures/ui/TopBar/leaderboardOn@3x.png b/Client2021/content/textures/ui/TopBar/leaderboardOn@3x.png new file mode 100644 index 0000000..da38dea Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/leaderboardOn@3x.png differ diff --git a/Client2021/content/textures/ui/TopBar/moreOff.png b/Client2021/content/textures/ui/TopBar/moreOff.png new file mode 100644 index 0000000..b59dac4 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/moreOff.png differ diff --git a/Client2021/content/textures/ui/TopBar/moreOff@2x.png b/Client2021/content/textures/ui/TopBar/moreOff@2x.png new file mode 100644 index 0000000..488f7ed Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/moreOff@2x.png differ diff --git a/Client2021/content/textures/ui/TopBar/moreOff@3x.png b/Client2021/content/textures/ui/TopBar/moreOff@3x.png new file mode 100644 index 0000000..0342030 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/moreOff@3x.png differ diff --git a/Client2021/content/textures/ui/TopBar/moreOn.png b/Client2021/content/textures/ui/TopBar/moreOn.png new file mode 100644 index 0000000..a0440f2 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/moreOn.png differ diff --git a/Client2021/content/textures/ui/TopBar/moreOn@2x.png b/Client2021/content/textures/ui/TopBar/moreOn@2x.png new file mode 100644 index 0000000..64813d8 Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/moreOn@2x.png differ diff --git a/Client2021/content/textures/ui/TopBar/moreOn@3x.png b/Client2021/content/textures/ui/TopBar/moreOn@3x.png new file mode 100644 index 0000000..480dc3c Binary files /dev/null and b/Client2021/content/textures/ui/TopBar/moreOn@3x.png differ diff --git a/Client2021/content/textures/ui/TopRoundedRect8px.png b/Client2021/content/textures/ui/TopRoundedRect8px.png new file mode 100644 index 0000000..237d6b4 Binary files /dev/null and b/Client2021/content/textures/ui/TopRoundedRect8px.png differ diff --git a/Client2021/content/textures/ui/TouchControlsSheet.png b/Client2021/content/textures/ui/TouchControlsSheet.png new file mode 100644 index 0000000..da0293f Binary files /dev/null and b/Client2021/content/textures/ui/TouchControlsSheet.png differ diff --git a/Client2021/content/textures/ui/VR/Radial/Icons/2DUI.png b/Client2021/content/textures/ui/VR/Radial/Icons/2DUI.png new file mode 100644 index 0000000..23ce445 Binary files /dev/null and b/Client2021/content/textures/ui/VR/Radial/Icons/2DUI.png differ diff --git a/Client2021/content/textures/ui/VR/Radial/Icons/Backpack.png b/Client2021/content/textures/ui/VR/Radial/Icons/Backpack.png new file mode 100644 index 0000000..d8a6237 Binary files /dev/null and b/Client2021/content/textures/ui/VR/Radial/Icons/Backpack.png differ diff --git a/Client2021/content/textures/ui/VR/Radial/Icons/Recenter.png b/Client2021/content/textures/ui/VR/Radial/Icons/Recenter.png new file mode 100644 index 0000000..b21a586 Binary files /dev/null and b/Client2021/content/textures/ui/VR/Radial/Icons/Recenter.png differ diff --git a/Client2021/content/textures/ui/VR/Radial/SliceActive.png b/Client2021/content/textures/ui/VR/Radial/SliceActive.png new file mode 100644 index 0000000..8776b51 Binary files /dev/null and b/Client2021/content/textures/ui/VR/Radial/SliceActive.png differ diff --git a/Client2021/content/textures/ui/VR/Radial/SliceBackground.png b/Client2021/content/textures/ui/VR/Radial/SliceBackground.png new file mode 100644 index 0000000..fd9c037 Binary files /dev/null and b/Client2021/content/textures/ui/VR/Radial/SliceBackground.png differ diff --git a/Client2021/content/textures/ui/VR/Radial/SliceDisabled.png b/Client2021/content/textures/ui/VR/Radial/SliceDisabled.png new file mode 100644 index 0000000..ad926ad Binary files /dev/null and b/Client2021/content/textures/ui/VR/Radial/SliceDisabled.png differ diff --git a/Client2021/content/textures/ui/VR/VRPointerDiscBlue.png b/Client2021/content/textures/ui/VR/VRPointerDiscBlue.png new file mode 100644 index 0000000..ceab1b0 Binary files /dev/null and b/Client2021/content/textures/ui/VR/VRPointerDiscBlue.png differ diff --git a/Client2021/content/textures/ui/VR/VRPointerDiscRed.png b/Client2021/content/textures/ui/VR/VRPointerDiscRed.png new file mode 100644 index 0000000..ba88732 Binary files /dev/null and b/Client2021/content/textures/ui/VR/VRPointerDiscRed.png differ diff --git a/Client2021/content/textures/ui/VR/button.png b/Client2021/content/textures/ui/VR/button.png new file mode 100644 index 0000000..403dfde Binary files /dev/null and b/Client2021/content/textures/ui/VR/button.png differ diff --git a/Client2021/content/textures/ui/VR/buttonActive.png b/Client2021/content/textures/ui/VR/buttonActive.png new file mode 100644 index 0000000..f685760 Binary files /dev/null and b/Client2021/content/textures/ui/VR/buttonActive.png differ diff --git a/Client2021/content/textures/ui/VR/buttonBackground.png b/Client2021/content/textures/ui/VR/buttonBackground.png new file mode 100644 index 0000000..379241a Binary files /dev/null and b/Client2021/content/textures/ui/VR/buttonBackground.png differ diff --git a/Client2021/content/textures/ui/VR/buttonHover.png b/Client2021/content/textures/ui/VR/buttonHover.png new file mode 100644 index 0000000..916ddff Binary files /dev/null and b/Client2021/content/textures/ui/VR/buttonHover.png differ diff --git a/Client2021/content/textures/ui/VR/buttonSelected.png b/Client2021/content/textures/ui/VR/buttonSelected.png new file mode 100644 index 0000000..6759aea Binary files /dev/null and b/Client2021/content/textures/ui/VR/buttonSelected.png differ diff --git a/Client2021/content/textures/ui/VR/chat.png b/Client2021/content/textures/ui/VR/chat.png new file mode 100644 index 0000000..5017f20 Binary files /dev/null and b/Client2021/content/textures/ui/VR/chat.png differ diff --git a/Client2021/content/textures/ui/VR/circleWhite.png b/Client2021/content/textures/ui/VR/circleWhite.png new file mode 100644 index 0000000..728fe7b Binary files /dev/null and b/Client2021/content/textures/ui/VR/circleWhite.png differ diff --git a/Client2021/content/textures/ui/VR/closeButtonPadded.png b/Client2021/content/textures/ui/VR/closeButtonPadded.png new file mode 100644 index 0000000..0db5f8d Binary files /dev/null and b/Client2021/content/textures/ui/VR/closeButtonPadded.png differ diff --git a/Client2021/content/textures/ui/VR/hamburger.png b/Client2021/content/textures/ui/VR/hamburger.png new file mode 100644 index 0000000..a9d70c5 Binary files /dev/null and b/Client2021/content/textures/ui/VR/hamburger.png differ diff --git a/Client2021/content/textures/ui/VR/hoverPopupLeft.png b/Client2021/content/textures/ui/VR/hoverPopupLeft.png new file mode 100644 index 0000000..484b0d2 Binary files /dev/null and b/Client2021/content/textures/ui/VR/hoverPopupLeft.png differ diff --git a/Client2021/content/textures/ui/VR/hoverPopupMid.png b/Client2021/content/textures/ui/VR/hoverPopupMid.png new file mode 100644 index 0000000..0f55008 Binary files /dev/null and b/Client2021/content/textures/ui/VR/hoverPopupMid.png differ diff --git a/Client2021/content/textures/ui/VR/hoverPopupRight.png b/Client2021/content/textures/ui/VR/hoverPopupRight.png new file mode 100644 index 0000000..ea0b32a Binary files /dev/null and b/Client2021/content/textures/ui/VR/hoverPopupRight.png differ diff --git a/Client2021/content/textures/ui/VR/notifications.png b/Client2021/content/textures/ui/VR/notifications.png new file mode 100644 index 0000000..ce18573 Binary files /dev/null and b/Client2021/content/textures/ui/VR/notifications.png differ diff --git a/Client2021/content/textures/ui/VR/notifier_glow.png b/Client2021/content/textures/ui/VR/notifier_glow.png new file mode 100644 index 0000000..9483328 Binary files /dev/null and b/Client2021/content/textures/ui/VR/notifier_glow.png differ diff --git a/Client2021/content/textures/ui/VR/recenter.png b/Client2021/content/textures/ui/VR/recenter.png new file mode 100644 index 0000000..a287342 Binary files /dev/null and b/Client2021/content/textures/ui/VR/recenter.png differ diff --git a/Client2021/content/textures/ui/VR/recenterFrame.png b/Client2021/content/textures/ui/VR/recenterFrame.png new file mode 100644 index 0000000..1f02bdf Binary files /dev/null and b/Client2021/content/textures/ui/VR/recenterFrame.png differ diff --git a/Client2021/content/textures/ui/VR/rectBackground.png b/Client2021/content/textures/ui/VR/rectBackground.png new file mode 100644 index 0000000..758edfe Binary files /dev/null and b/Client2021/content/textures/ui/VR/rectBackground.png differ diff --git a/Client2021/content/textures/ui/VR/rectBackgroundWhite.png b/Client2021/content/textures/ui/VR/rectBackgroundWhite.png new file mode 100644 index 0000000..0446670 Binary files /dev/null and b/Client2021/content/textures/ui/VR/rectBackgroundWhite.png differ diff --git a/Client2021/content/textures/ui/VR/toggle2D.png b/Client2021/content/textures/ui/VR/toggle2D.png new file mode 100644 index 0000000..78def18 Binary files /dev/null and b/Client2021/content/textures/ui/VR/toggle2D.png differ diff --git a/Client2021/content/textures/ui/Vehicle/SpeedBar.png b/Client2021/content/textures/ui/Vehicle/SpeedBar.png new file mode 100644 index 0000000..1913f2b Binary files /dev/null and b/Client2021/content/textures/ui/Vehicle/SpeedBar.png differ diff --git a/Client2021/content/textures/ui/Vehicle/SpeedBar@2x.png b/Client2021/content/textures/ui/Vehicle/SpeedBar@2x.png new file mode 100644 index 0000000..3935f79 Binary files /dev/null and b/Client2021/content/textures/ui/Vehicle/SpeedBar@2x.png differ diff --git a/Client2021/content/textures/ui/Vehicle/SpeedBarBKG.png b/Client2021/content/textures/ui/Vehicle/SpeedBarBKG.png new file mode 100644 index 0000000..af55200 Binary files /dev/null and b/Client2021/content/textures/ui/Vehicle/SpeedBarBKG.png differ diff --git a/Client2021/content/textures/ui/Vehicle/SpeedBarBKG@2x.png b/Client2021/content/textures/ui/Vehicle/SpeedBarBKG@2x.png new file mode 100644 index 0000000..b85a6e5 Binary files /dev/null and b/Client2021/content/textures/ui/Vehicle/SpeedBarBKG@2x.png differ diff --git a/Client2021/content/textures/ui/Vehicle/SpeedBarEmpty.png b/Client2021/content/textures/ui/Vehicle/SpeedBarEmpty.png new file mode 100644 index 0000000..7889a14 Binary files /dev/null and b/Client2021/content/textures/ui/Vehicle/SpeedBarEmpty.png differ diff --git a/Client2021/content/textures/ui/Vehicle/SpeedBarEmpty@2x.png b/Client2021/content/textures/ui/Vehicle/SpeedBarEmpty@2x.png new file mode 100644 index 0000000..880b096 Binary files /dev/null and b/Client2021/content/textures/ui/Vehicle/SpeedBarEmpty@2x.png differ diff --git a/Client2021/content/textures/ui/VirtualCursor/cursorDefault.png b/Client2021/content/textures/ui/VirtualCursor/cursorDefault.png new file mode 100644 index 0000000..05f7f56 Binary files /dev/null and b/Client2021/content/textures/ui/VirtualCursor/cursorDefault.png differ diff --git a/Client2021/content/textures/ui/VirtualCursor/cursorDefault@2x.png b/Client2021/content/textures/ui/VirtualCursor/cursorDefault@2x.png new file mode 100644 index 0000000..afef5e8 Binary files /dev/null and b/Client2021/content/textures/ui/VirtualCursor/cursorDefault@2x.png differ diff --git a/Client2021/content/textures/ui/VirtualCursor/cursorDefault@3x.png b/Client2021/content/textures/ui/VirtualCursor/cursorDefault@3x.png new file mode 100644 index 0000000..fb6d317 Binary files /dev/null and b/Client2021/content/textures/ui/VirtualCursor/cursorDefault@3x.png differ diff --git a/Client2021/content/textures/ui/VirtualCursor/cursorHover.png b/Client2021/content/textures/ui/VirtualCursor/cursorHover.png new file mode 100644 index 0000000..8355372 Binary files /dev/null and b/Client2021/content/textures/ui/VirtualCursor/cursorHover.png differ diff --git a/Client2021/content/textures/ui/VirtualCursor/cursorHover@2x.png b/Client2021/content/textures/ui/VirtualCursor/cursorHover@2x.png new file mode 100644 index 0000000..ca3741c Binary files /dev/null and b/Client2021/content/textures/ui/VirtualCursor/cursorHover@2x.png differ diff --git a/Client2021/content/textures/ui/VirtualCursor/cursorHover@3x.png b/Client2021/content/textures/ui/VirtualCursor/cursorHover@3x.png new file mode 100644 index 0000000..d644e48 Binary files /dev/null and b/Client2021/content/textures/ui/VirtualCursor/cursorHover@3x.png differ diff --git a/Client2021/content/textures/ui/VirtualCursor/cursorPressed.png b/Client2021/content/textures/ui/VirtualCursor/cursorPressed.png new file mode 100644 index 0000000..41db5df Binary files /dev/null and b/Client2021/content/textures/ui/VirtualCursor/cursorPressed.png differ diff --git a/Client2021/content/textures/ui/VirtualCursor/cursorPressed@2x.png b/Client2021/content/textures/ui/VirtualCursor/cursorPressed@2x.png new file mode 100644 index 0000000..3748ba5 Binary files /dev/null and b/Client2021/content/textures/ui/VirtualCursor/cursorPressed@2x.png differ diff --git a/Client2021/content/textures/ui/VirtualCursor/cursorPressed@3x.png b/Client2021/content/textures/ui/VirtualCursor/cursorPressed@3x.png new file mode 100644 index 0000000..8ac74d3 Binary files /dev/null and b/Client2021/content/textures/ui/VirtualCursor/cursorPressed@3x.png differ diff --git a/Client2021/content/textures/ui/account_over13.png b/Client2021/content/textures/ui/account_over13.png new file mode 100644 index 0000000..0a91b6c Binary files /dev/null and b/Client2021/content/textures/ui/account_over13.png differ diff --git a/Client2021/content/textures/ui/account_under13.png b/Client2021/content/textures/ui/account_under13.png new file mode 100644 index 0000000..8ecf24c Binary files /dev/null and b/Client2021/content/textures/ui/account_under13.png differ diff --git a/Client2021/content/textures/ui/btn_grey.png b/Client2021/content/textures/ui/btn_grey.png new file mode 100644 index 0000000..3ae3fff Binary files /dev/null and b/Client2021/content/textures/ui/btn_grey.png differ diff --git a/Client2021/content/textures/ui/btn_greyTransp.png b/Client2021/content/textures/ui/btn_greyTransp.png new file mode 100644 index 0000000..0a39a1e Binary files /dev/null and b/Client2021/content/textures/ui/btn_greyTransp.png differ diff --git a/Client2021/content/textures/ui/btn_newBlue.png b/Client2021/content/textures/ui/btn_newBlue.png new file mode 100644 index 0000000..d7b482e Binary files /dev/null and b/Client2021/content/textures/ui/btn_newBlue.png differ diff --git a/Client2021/content/textures/ui/btn_newBlue@2x.png b/Client2021/content/textures/ui/btn_newBlue@2x.png new file mode 100644 index 0000000..ca2f2e9 Binary files /dev/null and b/Client2021/content/textures/ui/btn_newBlue@2x.png differ diff --git a/Client2021/content/textures/ui/btn_newBlueGlow.png b/Client2021/content/textures/ui/btn_newBlueGlow.png new file mode 100644 index 0000000..1b49a6c Binary files /dev/null and b/Client2021/content/textures/ui/btn_newBlueGlow.png differ diff --git a/Client2021/content/textures/ui/btn_newBlueGlow@2x.png b/Client2021/content/textures/ui/btn_newBlueGlow@2x.png new file mode 100644 index 0000000..379ec28 Binary files /dev/null and b/Client2021/content/textures/ui/btn_newBlueGlow@2x.png differ diff --git a/Client2021/content/textures/ui/btn_newGrey.png b/Client2021/content/textures/ui/btn_newGrey.png new file mode 100644 index 0000000..fb6cb06 Binary files /dev/null and b/Client2021/content/textures/ui/btn_newGrey.png differ diff --git a/Client2021/content/textures/ui/btn_newGrey@2x.png b/Client2021/content/textures/ui/btn_newGrey@2x.png new file mode 100644 index 0000000..b877522 Binary files /dev/null and b/Client2021/content/textures/ui/btn_newGrey@2x.png differ diff --git a/Client2021/content/textures/ui/btn_newGreyGlow.png b/Client2021/content/textures/ui/btn_newGreyGlow.png new file mode 100644 index 0000000..fc3e25a Binary files /dev/null and b/Client2021/content/textures/ui/btn_newGreyGlow.png differ diff --git a/Client2021/content/textures/ui/btn_newGreyGlow@2x.png b/Client2021/content/textures/ui/btn_newGreyGlow@2x.png new file mode 100644 index 0000000..017b746 Binary files /dev/null and b/Client2021/content/textures/ui/btn_newGreyGlow@2x.png differ diff --git a/Client2021/content/textures/ui/btn_newWhite.png b/Client2021/content/textures/ui/btn_newWhite.png new file mode 100644 index 0000000..4e415ee Binary files /dev/null and b/Client2021/content/textures/ui/btn_newWhite.png differ diff --git a/Client2021/content/textures/ui/btn_newWhite@2x.png b/Client2021/content/textures/ui/btn_newWhite@2x.png new file mode 100644 index 0000000..82c0825 Binary files /dev/null and b/Client2021/content/textures/ui/btn_newWhite@2x.png differ diff --git a/Client2021/content/textures/ui/btn_newWhiteGlow.png b/Client2021/content/textures/ui/btn_newWhiteGlow.png new file mode 100644 index 0000000..0373243 Binary files /dev/null and b/Client2021/content/textures/ui/btn_newWhiteGlow.png differ diff --git a/Client2021/content/textures/ui/btn_newWhiteGlow@2x.png b/Client2021/content/textures/ui/btn_newWhiteGlow@2x.png new file mode 100644 index 0000000..e03f4dc Binary files /dev/null and b/Client2021/content/textures/ui/btn_newWhiteGlow@2x.png differ diff --git a/Client2021/content/textures/ui/btn_red.png b/Client2021/content/textures/ui/btn_red.png new file mode 100644 index 0000000..4ab7dec Binary files /dev/null and b/Client2021/content/textures/ui/btn_red.png differ diff --git a/Client2021/content/textures/ui/btn_redGlow.png b/Client2021/content/textures/ui/btn_redGlow.png new file mode 100644 index 0000000..5d6249e Binary files /dev/null and b/Client2021/content/textures/ui/btn_redGlow.png differ diff --git a/Client2021/content/textures/ui/btn_white.png b/Client2021/content/textures/ui/btn_white.png new file mode 100644 index 0000000..4c6d4c8 Binary files /dev/null and b/Client2021/content/textures/ui/btn_white.png differ diff --git a/Client2021/content/textures/ui/chatBubble_blue_notify_bkg.png b/Client2021/content/textures/ui/chatBubble_blue_notify_bkg.png new file mode 100644 index 0000000..a7925dc Binary files /dev/null and b/Client2021/content/textures/ui/chatBubble_blue_notify_bkg.png differ diff --git a/Client2021/content/textures/ui/chatBubble_green_notify_bkg.png b/Client2021/content/textures/ui/chatBubble_green_notify_bkg.png new file mode 100644 index 0000000..1f38d6a Binary files /dev/null and b/Client2021/content/textures/ui/chatBubble_green_notify_bkg.png differ diff --git a/Client2021/content/textures/ui/chatBubble_red_notify_bkg.png b/Client2021/content/textures/ui/chatBubble_red_notify_bkg.png new file mode 100644 index 0000000..7503309 Binary files /dev/null and b/Client2021/content/textures/ui/chatBubble_red_notify_bkg.png differ diff --git a/Client2021/content/textures/ui/chatBubble_white_notify_bkg.png b/Client2021/content/textures/ui/chatBubble_white_notify_bkg.png new file mode 100644 index 0000000..59be44c Binary files /dev/null and b/Client2021/content/textures/ui/chatBubble_white_notify_bkg.png differ diff --git a/Client2021/content/textures/ui/chat_teamButton.png b/Client2021/content/textures/ui/chat_teamButton.png new file mode 100644 index 0000000..ecceb50 Binary files /dev/null and b/Client2021/content/textures/ui/chat_teamButton.png differ diff --git a/Client2021/content/textures/ui/chat_teamButton@2x.png b/Client2021/content/textures/ui/chat_teamButton@2x.png new file mode 100644 index 0000000..d36c05e Binary files /dev/null and b/Client2021/content/textures/ui/chat_teamButton@2x.png differ diff --git a/Client2021/content/textures/ui/clb_robux_20.png b/Client2021/content/textures/ui/clb_robux_20.png new file mode 100644 index 0000000..33dba39 Binary files /dev/null and b/Client2021/content/textures/ui/clb_robux_20.png differ diff --git a/Client2021/content/textures/ui/clb_robux_20@2x.png b/Client2021/content/textures/ui/clb_robux_20@2x.png new file mode 100644 index 0000000..01caf4c Binary files /dev/null and b/Client2021/content/textures/ui/clb_robux_20@2x.png differ diff --git a/Client2021/content/textures/ui/clb_robux_20@3x.png b/Client2021/content/textures/ui/clb_robux_20@3x.png new file mode 100644 index 0000000..28f1c90 Binary files /dev/null and b/Client2021/content/textures/ui/clb_robux_20@3x.png differ diff --git a/Client2021/content/textures/ui/common/robux.png b/Client2021/content/textures/ui/common/robux.png new file mode 100644 index 0000000..ec76c30 Binary files /dev/null and b/Client2021/content/textures/ui/common/robux.png differ diff --git a/Client2021/content/textures/ui/common/robux@2x.png b/Client2021/content/textures/ui/common/robux@2x.png new file mode 100644 index 0000000..3666e3b Binary files /dev/null and b/Client2021/content/textures/ui/common/robux@2x.png differ diff --git a/Client2021/content/textures/ui/common/robux@3x.png b/Client2021/content/textures/ui/common/robux@3x.png new file mode 100644 index 0000000..303a061 Binary files /dev/null and b/Client2021/content/textures/ui/common/robux@3x.png differ diff --git a/Client2021/content/textures/ui/common/robux_color.png b/Client2021/content/textures/ui/common/robux_color.png new file mode 100644 index 0000000..1db38ba Binary files /dev/null and b/Client2021/content/textures/ui/common/robux_color.png differ diff --git a/Client2021/content/textures/ui/common/robux_color@2x.png b/Client2021/content/textures/ui/common/robux_color@2x.png new file mode 100644 index 0000000..8774a12 Binary files /dev/null and b/Client2021/content/textures/ui/common/robux_color@2x.png differ diff --git a/Client2021/content/textures/ui/common/robux_color@3x.png b/Client2021/content/textures/ui/common/robux_color@3x.png new file mode 100644 index 0000000..cec394c Binary files /dev/null and b/Client2021/content/textures/ui/common/robux_color@3x.png differ diff --git a/Client2021/content/textures/ui/common/robux_small.png b/Client2021/content/textures/ui/common/robux_small.png new file mode 100644 index 0000000..545e5cc Binary files /dev/null and b/Client2021/content/textures/ui/common/robux_small.png differ diff --git a/Client2021/content/textures/ui/common/robux_small@2x.png b/Client2021/content/textures/ui/common/robux_small@2x.png new file mode 100644 index 0000000..6619fb6 Binary files /dev/null and b/Client2021/content/textures/ui/common/robux_small@2x.png differ diff --git a/Client2021/content/textures/ui/common/robux_small@3x.png b/Client2021/content/textures/ui/common/robux_small@3x.png new file mode 100644 index 0000000..3dd1fff Binary files /dev/null and b/Client2021/content/textures/ui/common/robux_small@3x.png differ diff --git a/Client2021/content/textures/ui/dialog_blue.png b/Client2021/content/textures/ui/dialog_blue.png new file mode 100644 index 0000000..96b9adb Binary files /dev/null and b/Client2021/content/textures/ui/dialog_blue.png differ diff --git a/Client2021/content/textures/ui/dialog_blue@2x.png b/Client2021/content/textures/ui/dialog_blue@2x.png new file mode 100644 index 0000000..c40fc31 Binary files /dev/null and b/Client2021/content/textures/ui/dialog_blue@2x.png differ diff --git a/Client2021/content/textures/ui/dialog_green.png b/Client2021/content/textures/ui/dialog_green.png new file mode 100644 index 0000000..64d012c Binary files /dev/null and b/Client2021/content/textures/ui/dialog_green.png differ diff --git a/Client2021/content/textures/ui/dialog_green@2x.png b/Client2021/content/textures/ui/dialog_green@2x.png new file mode 100644 index 0000000..775b4f9 Binary files /dev/null and b/Client2021/content/textures/ui/dialog_green@2x.png differ diff --git a/Client2021/content/textures/ui/dialog_purpose_help.png b/Client2021/content/textures/ui/dialog_purpose_help.png new file mode 100644 index 0000000..311d441 Binary files /dev/null and b/Client2021/content/textures/ui/dialog_purpose_help.png differ diff --git a/Client2021/content/textures/ui/dialog_purpose_quest.png b/Client2021/content/textures/ui/dialog_purpose_quest.png new file mode 100644 index 0000000..14efd26 Binary files /dev/null and b/Client2021/content/textures/ui/dialog_purpose_quest.png differ diff --git a/Client2021/content/textures/ui/dialog_purpose_shop.png b/Client2021/content/textures/ui/dialog_purpose_shop.png new file mode 100644 index 0000000..df2bb7f Binary files /dev/null and b/Client2021/content/textures/ui/dialog_purpose_shop.png differ diff --git a/Client2021/content/textures/ui/dialog_red.png b/Client2021/content/textures/ui/dialog_red.png new file mode 100644 index 0000000..6b3c644 Binary files /dev/null and b/Client2021/content/textures/ui/dialog_red.png differ diff --git a/Client2021/content/textures/ui/dialog_red@2x.png b/Client2021/content/textures/ui/dialog_red@2x.png new file mode 100644 index 0000000..c62b873 Binary files /dev/null and b/Client2021/content/textures/ui/dialog_red@2x.png differ diff --git a/Client2021/content/textures/ui/dialog_tail.png b/Client2021/content/textures/ui/dialog_tail.png new file mode 100644 index 0000000..b9686bc Binary files /dev/null and b/Client2021/content/textures/ui/dialog_tail.png differ diff --git a/Client2021/content/textures/ui/dialog_tail@2x.png b/Client2021/content/textures/ui/dialog_tail@2x.png new file mode 100644 index 0000000..ff18256 Binary files /dev/null and b/Client2021/content/textures/ui/dialog_tail@2x.png differ diff --git a/Client2021/content/textures/ui/dialog_white.png b/Client2021/content/textures/ui/dialog_white.png new file mode 100644 index 0000000..9911129 Binary files /dev/null and b/Client2021/content/textures/ui/dialog_white.png differ diff --git a/Client2021/content/textures/ui/dialog_white@2x.png b/Client2021/content/textures/ui/dialog_white@2x.png new file mode 100644 index 0000000..f80f843 Binary files /dev/null and b/Client2021/content/textures/ui/dialog_white@2x.png differ diff --git a/Client2021/content/textures/ui/dropdown_arrow.png b/Client2021/content/textures/ui/dropdown_arrow.png new file mode 100644 index 0000000..eddc0be Binary files /dev/null and b/Client2021/content/textures/ui/dropdown_arrow.png differ diff --git a/Client2021/content/textures/ui/dropdown_arrow@2x.png b/Client2021/content/textures/ui/dropdown_arrow@2x.png new file mode 100644 index 0000000..0db921e Binary files /dev/null and b/Client2021/content/textures/ui/dropdown_arrow@2x.png differ diff --git a/Client2021/content/textures/ui/homeButton.png b/Client2021/content/textures/ui/homeButton.png new file mode 100644 index 0000000..b9ad006 Binary files /dev/null and b/Client2021/content/textures/ui/homeButton.png differ diff --git a/Client2021/content/textures/ui/homeButton@2x.png b/Client2021/content/textures/ui/homeButton@2x.png new file mode 100644 index 0000000..2c2cdc0 Binary files /dev/null and b/Client2021/content/textures/ui/homeButton@2x.png differ diff --git a/Client2021/content/textures/ui/icon_BC-16.png b/Client2021/content/textures/ui/icon_BC-16.png new file mode 100644 index 0000000..dedd8d4 Binary files /dev/null and b/Client2021/content/textures/ui/icon_BC-16.png differ diff --git a/Client2021/content/textures/ui/icon_OBC-16.png b/Client2021/content/textures/ui/icon_OBC-16.png new file mode 100644 index 0000000..e539d5d Binary files /dev/null and b/Client2021/content/textures/ui/icon_OBC-16.png differ diff --git a/Client2021/content/textures/ui/icon_TBC-16.png b/Client2021/content/textures/ui/icon_TBC-16.png new file mode 100644 index 0000000..208dde3 Binary files /dev/null and b/Client2021/content/textures/ui/icon_TBC-16.png differ diff --git a/Client2021/content/textures/ui/icon_admin-16.png b/Client2021/content/textures/ui/icon_admin-16.png new file mode 100644 index 0000000..ae240d5 Binary files /dev/null and b/Client2021/content/textures/ui/icon_admin-16.png differ diff --git a/Client2021/content/textures/ui/icon_follower-16.png b/Client2021/content/textures/ui/icon_follower-16.png new file mode 100644 index 0000000..20dd9f1 Binary files /dev/null and b/Client2021/content/textures/ui/icon_follower-16.png differ diff --git a/Client2021/content/textures/ui/icon_following-16.png b/Client2021/content/textures/ui/icon_following-16.png new file mode 100644 index 0000000..a75dd9f Binary files /dev/null and b/Client2021/content/textures/ui/icon_following-16.png differ diff --git a/Client2021/content/textures/ui/icon_friendrequestrecieved-16.png b/Client2021/content/textures/ui/icon_friendrequestrecieved-16.png new file mode 100644 index 0000000..f7972f1 Binary files /dev/null and b/Client2021/content/textures/ui/icon_friendrequestrecieved-16.png differ diff --git a/Client2021/content/textures/ui/icon_friendrequestsent_16.png b/Client2021/content/textures/ui/icon_friendrequestsent_16.png new file mode 100644 index 0000000..947a5cf Binary files /dev/null and b/Client2021/content/textures/ui/icon_friendrequestsent_16.png differ diff --git a/Client2021/content/textures/ui/icon_friends_16.png b/Client2021/content/textures/ui/icon_friends_16.png new file mode 100644 index 0000000..17c5369 Binary files /dev/null and b/Client2021/content/textures/ui/icon_friends_16.png differ diff --git a/Client2021/content/textures/ui/icon_intern-16.png b/Client2021/content/textures/ui/icon_intern-16.png new file mode 100644 index 0000000..b4afb27 Binary files /dev/null and b/Client2021/content/textures/ui/icon_intern-16.png differ diff --git a/Client2021/content/textures/ui/icon_localization-16.png b/Client2021/content/textures/ui/icon_localization-16.png new file mode 100644 index 0000000..627b160 Binary files /dev/null and b/Client2021/content/textures/ui/icon_localization-16.png differ diff --git a/Client2021/content/textures/ui/icon_mutualfollowing-16.png b/Client2021/content/textures/ui/icon_mutualfollowing-16.png new file mode 100644 index 0000000..40adece Binary files /dev/null and b/Client2021/content/textures/ui/icon_mutualfollowing-16.png differ diff --git a/Client2021/content/textures/ui/icon_placeowner.png b/Client2021/content/textures/ui/icon_placeowner.png new file mode 100644 index 0000000..30f66ff Binary files /dev/null and b/Client2021/content/textures/ui/icon_placeowner.png differ diff --git a/Client2021/content/textures/ui/icon_premium-16.png b/Client2021/content/textures/ui/icon_premium-16.png new file mode 100644 index 0000000..912d9d2 Binary files /dev/null and b/Client2021/content/textures/ui/icon_premium-16.png differ diff --git a/Client2021/content/textures/ui/icon_star-16.png b/Client2021/content/textures/ui/icon_star-16.png new file mode 100644 index 0000000..189b9ca Binary files /dev/null and b/Client2021/content/textures/ui/icon_star-16.png differ diff --git a/Client2021/content/textures/ui/mouseLock_off.png b/Client2021/content/textures/ui/mouseLock_off.png new file mode 100644 index 0000000..c0b562a Binary files /dev/null and b/Client2021/content/textures/ui/mouseLock_off.png differ diff --git a/Client2021/content/textures/ui/mouseLock_off@2x.png b/Client2021/content/textures/ui/mouseLock_off@2x.png new file mode 100644 index 0000000..3773877 Binary files /dev/null and b/Client2021/content/textures/ui/mouseLock_off@2x.png differ diff --git a/Client2021/content/textures/ui/mouseLock_on.png b/Client2021/content/textures/ui/mouseLock_on.png new file mode 100644 index 0000000..9728774 Binary files /dev/null and b/Client2021/content/textures/ui/mouseLock_on.png differ diff --git a/Client2021/content/textures/ui/mouseLock_on@2x.png b/Client2021/content/textures/ui/mouseLock_on@2x.png new file mode 100644 index 0000000..c789d46 Binary files /dev/null and b/Client2021/content/textures/ui/mouseLock_on@2x.png differ diff --git a/Client2021/content/textures/ui/move.png b/Client2021/content/textures/ui/move.png new file mode 100644 index 0000000..d081458 Binary files /dev/null and b/Client2021/content/textures/ui/move.png differ diff --git a/Client2021/content/textures/ui/newBkg_square.png b/Client2021/content/textures/ui/newBkg_square.png new file mode 100644 index 0000000..901ffc1 Binary files /dev/null and b/Client2021/content/textures/ui/newBkg_square.png differ diff --git a/Client2021/content/textures/ui/newBkg_square@2x.png b/Client2021/content/textures/ui/newBkg_square@2x.png new file mode 100644 index 0000000..6956f48 Binary files /dev/null and b/Client2021/content/textures/ui/newBkg_square@2x.png differ diff --git a/Client2021/content/textures/ui/scroll-bottom.png b/Client2021/content/textures/ui/scroll-bottom.png new file mode 100644 index 0000000..f186156 Binary files /dev/null and b/Client2021/content/textures/ui/scroll-bottom.png differ diff --git a/Client2021/content/textures/ui/scroll-bottom@2x.png b/Client2021/content/textures/ui/scroll-bottom@2x.png new file mode 100644 index 0000000..3fb978f Binary files /dev/null and b/Client2021/content/textures/ui/scroll-bottom@2x.png differ diff --git a/Client2021/content/textures/ui/scroll-middle.png b/Client2021/content/textures/ui/scroll-middle.png new file mode 100644 index 0000000..1a7cfa7 Binary files /dev/null and b/Client2021/content/textures/ui/scroll-middle.png differ diff --git a/Client2021/content/textures/ui/scroll-middle@2x.png b/Client2021/content/textures/ui/scroll-middle@2x.png new file mode 100644 index 0000000..a101d48 Binary files /dev/null and b/Client2021/content/textures/ui/scroll-middle@2x.png differ diff --git a/Client2021/content/textures/ui/scroll-top.png b/Client2021/content/textures/ui/scroll-top.png new file mode 100644 index 0000000..1c85194 Binary files /dev/null and b/Client2021/content/textures/ui/scroll-top.png differ diff --git a/Client2021/content/textures/ui/scroll-top@2x.png b/Client2021/content/textures/ui/scroll-top@2x.png new file mode 100644 index 0000000..0720737 Binary files /dev/null and b/Client2021/content/textures/ui/scroll-top@2x.png differ diff --git a/Client2021/content/textures/ui/scrollbar.png b/Client2021/content/textures/ui/scrollbar.png new file mode 100644 index 0000000..1a3f361 Binary files /dev/null and b/Client2021/content/textures/ui/scrollbar.png differ diff --git a/Client2021/content/textures/ui/scrollbuttonDown.png b/Client2021/content/textures/ui/scrollbuttonDown.png new file mode 100644 index 0000000..fc6be92 Binary files /dev/null and b/Client2021/content/textures/ui/scrollbuttonDown.png differ diff --git a/Client2021/content/textures/ui/scrollbuttonDown_dn.png b/Client2021/content/textures/ui/scrollbuttonDown_dn.png new file mode 100644 index 0000000..38f1d5f Binary files /dev/null and b/Client2021/content/textures/ui/scrollbuttonDown_dn.png differ diff --git a/Client2021/content/textures/ui/scrollbuttonDown_ds.png b/Client2021/content/textures/ui/scrollbuttonDown_ds.png new file mode 100644 index 0000000..fb7f2f3 Binary files /dev/null and b/Client2021/content/textures/ui/scrollbuttonDown_ds.png differ diff --git a/Client2021/content/textures/ui/scrollbuttonDown_ovr.png b/Client2021/content/textures/ui/scrollbuttonDown_ovr.png new file mode 100644 index 0000000..38f1d5f Binary files /dev/null and b/Client2021/content/textures/ui/scrollbuttonDown_ovr.png differ diff --git a/Client2021/content/textures/ui/scrollbuttonUp.png b/Client2021/content/textures/ui/scrollbuttonUp.png new file mode 100644 index 0000000..311b520 Binary files /dev/null and b/Client2021/content/textures/ui/scrollbuttonUp.png differ diff --git a/Client2021/content/textures/ui/scrollbuttonUp_dn.png b/Client2021/content/textures/ui/scrollbuttonUp_dn.png new file mode 100644 index 0000000..aef3474 Binary files /dev/null and b/Client2021/content/textures/ui/scrollbuttonUp_dn.png differ diff --git a/Client2021/content/textures/ui/scrollbuttonUp_ds.png b/Client2021/content/textures/ui/scrollbuttonUp_ds.png new file mode 100644 index 0000000..551dbe7 Binary files /dev/null and b/Client2021/content/textures/ui/scrollbuttonUp_ds.png differ diff --git a/Client2021/content/textures/ui/scrollbuttonUp_ovr.png b/Client2021/content/textures/ui/scrollbuttonUp_ovr.png new file mode 100644 index 0000000..aef3474 Binary files /dev/null and b/Client2021/content/textures/ui/scrollbuttonUp_ovr.png differ diff --git a/Client2021/content/textures/ui/slider_new_tab.png b/Client2021/content/textures/ui/slider_new_tab.png new file mode 100644 index 0000000..ccc9ee1 Binary files /dev/null and b/Client2021/content/textures/ui/slider_new_tab.png differ diff --git a/Client2021/content/textures/ui/slider_new_tab@2x.png b/Client2021/content/textures/ui/slider_new_tab@2x.png new file mode 100644 index 0000000..07231fc Binary files /dev/null and b/Client2021/content/textures/ui/slider_new_tab@2x.png differ diff --git a/Client2021/content/textures/ui/traildot.png b/Client2021/content/textures/ui/traildot.png new file mode 100644 index 0000000..de2c6f1 Binary files /dev/null and b/Client2021/content/textures/ui/traildot.png differ diff --git a/Client2021/content/textures/ui/vr_active.png b/Client2021/content/textures/ui/vr_active.png new file mode 100644 index 0000000..bda5347 Binary files /dev/null and b/Client2021/content/textures/ui/vr_active.png differ diff --git a/Client2021/content/textures/ui/vr_idle.png b/Client2021/content/textures/ui/vr_idle.png new file mode 100644 index 0000000..62b95c8 Binary files /dev/null and b/Client2021/content/textures/ui/vr_idle.png differ diff --git a/Client2021/content/textures/ui/waypoint.png b/Client2021/content/textures/ui/waypoint.png new file mode 100644 index 0000000..17d23aa Binary files /dev/null and b/Client2021/content/textures/ui/waypoint.png differ diff --git a/Client2021/content/textures/whiteCircle.png b/Client2021/content/textures/whiteCircle.png new file mode 100644 index 0000000..e1e5e4c Binary files /dev/null and b/Client2021/content/textures/whiteCircle.png differ diff --git a/Client2021/shaders/keepme b/Client2021/shaders/keepme new file mode 100644 index 0000000..e69de29 diff --git a/Client2021/shaders/shaders_d3d10.pack b/Client2021/shaders/shaders_d3d10.pack new file mode 100644 index 0000000..a49cc1f Binary files /dev/null and b/Client2021/shaders/shaders_d3d10.pack differ diff --git a/Client2021/shaders/shaders_d3d10_1.pack b/Client2021/shaders/shaders_d3d10_1.pack new file mode 100644 index 0000000..451988f Binary files /dev/null and b/Client2021/shaders/shaders_d3d10_1.pack differ diff --git a/Client2021/shaders/shaders_d3d11.pack b/Client2021/shaders/shaders_d3d11.pack new file mode 100644 index 0000000..fffeca3 Binary files /dev/null and b/Client2021/shaders/shaders_d3d11.pack differ diff --git a/Client2021/shaders/shaders_d3d9.pack b/Client2021/shaders/shaders_d3d9.pack new file mode 100644 index 0000000..e2237ab Binary files /dev/null and b/Client2021/shaders/shaders_d3d9.pack differ diff --git a/Client2021/shaders/shaders_glsl.pack b/Client2021/shaders/shaders_glsl.pack new file mode 100644 index 0000000..653529f Binary files /dev/null and b/Client2021/shaders/shaders_glsl.pack differ diff --git a/Client2021/shaders/shaders_glsl3.pack b/Client2021/shaders/shaders_glsl3.pack new file mode 100644 index 0000000..76e9eaa Binary files /dev/null and b/Client2021/shaders/shaders_glsl3.pack differ diff --git a/Client2021/shaders/shaders_vulkan_desktop.pack b/Client2021/shaders/shaders_vulkan_desktop.pack new file mode 100644 index 0000000..8dc143e Binary files /dev/null and b/Client2021/shaders/shaders_vulkan_desktop.pack differ diff --git a/Client2021/ssl/cacert.pem b/Client2021/ssl/cacert.pem new file mode 100644 index 0000000..3a08fd8 --- /dev/null +++ b/Client2021/ssl/cacert.pem @@ -0,0 +1,3435 @@ +## +## Bundle of CA Root Certificates +## +## Certificate data from Mozilla as of: Tue Dec 8 04:12:05 2020 GMT +## +## This is a bundle of X.509 certificates of public Certificate Authorities +## (CA). These were automatically extracted from Mozilla's root certificates +## file (certdata.txt). This file can be found in the mozilla source tree: +## https://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt +## +## It contains the certificates in PEM format and therefore +## can be directly used with curl / libcurl / php_curl, or with +## an Apache+mod_ssl webserver for SSL client authentication. +## Just configure this file as the SSLCACertificateFile. +## +## Conversion done with mk-ca-bundle.pl version 1.28. +## SHA256: d820b8696d8ffe42064a1384a56a8981cdc7e7e198036bbb5fa04a6c282dd9a2 +## + + +GlobalSign Root CA +================== +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx +GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds +b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV +BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD +VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa +DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc +THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb +Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP +c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX +gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF +AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj +Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG +j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH +hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC +X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +GlobalSign Root CA - R2 +======================= +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UECxMXR2xv +YmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh +bFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT +aWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6 +ErPLv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8eoLrvozp +s6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjN +S7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CL +TfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pazq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6C +ygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUm+IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5nbG9i +YWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjAN +BgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp +9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu +01yiPqFbQfXf5WRDLenVOavSot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG7 +9G+dwfCMNYxdAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +Entrust.net Premium 2048 Secure Server CA +========================================= +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u +ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp +bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV +BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx +NzUwNTFaFw0yOTA3MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3 +d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl +MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u +ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL +Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr +hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW +nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi +VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJ +KoZIhvcNAQEFBQADggEBADubj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPy +T/4xmf3IDExoU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5bu/8j72gZyxKT +J1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+bYQLCIt+jerXmCHG8+c8eS9e +nNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/ErfF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- + +Baltimore CyberTrust Root +========================= +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJRTESMBAGA1UE +ChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3li +ZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoXDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMC +SUUxEjAQBgNVBAoTCUJhbHRpbW9yZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFs +dGltb3JlIEN5YmVyVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKME +uyKrmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjrIZ3AQSsB +UnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeKmpYcqWe4PwzV9/lSEy/C +G9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSuXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9 +XbIGevOF6uvUA65ehD5f/xXtabz5OTZydc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjpr +l3RjM71oGDHweI12v/yejl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoI +VDaGezq1BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEB +BQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT929hkTI7gQCvlYpNRh +cL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3WgxjkzSswF07r51XgdIGn9w/xZchMB5 +hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsa +Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H +RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +Entrust Root Certification Authority +==================================== +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0Lm5ldC9DUFMgaXMgaW5jb3Jw +b3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMWKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsG +A1UEAxMkRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0 +MloXDTI2MTEyNzIwNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMu +MTkwNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSByZWZlcmVu +Y2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNVBAMTJEVudHJ1c3QgUm9v +dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ALaVtkNC+sZtKm9I35RMOVcF7sN5EUFoNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYsz +A9u3g3s+IIRe7bJWKKf44LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOww +Cj0Yzfv9KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGIrb68 +j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi94DkZfs0Nw4pgHBN +rziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOBsDCBrTAOBgNVHQ8BAf8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAigA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1 +MzQyWjAfBgNVHSMEGDAWgBRokORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DH +hmak8fdLQ/uEvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9tO1KzKtvn1ISM +Y/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6ZuaAGAT/3B+XxFNSRuzFVJ7yVTa +v52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTS +W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0 +tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +GeoTrust Global CA +================== +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9iYWwgQ0EwHhcNMDIwNTIxMDQw +MDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5j +LjEbMBkGA1UEAxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjo +BbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDviS2Aelet +8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU1XupGc1V3sjs0l44U+Vc +T4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagU +vTLrGAMoUgRx5aszPeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVk +DBF9qn1luMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKInZ57Q +zxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfStQWVYrmm3ok9Nns4 +d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcFPseKUgzbFbS9bZvlxrFUaKnjaZC2 +mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Unhw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6p +XE0zX5IJL4hmXXeXxx12E6nV5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvm +Mw== +-----END CERTIFICATE----- + +GeoTrust Universal CA +===================== +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN +R2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVyc2FsIENBMB4XDTA0MDMwNDA1 +MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IElu +Yy4xHjAcBgNVBAMTFUdlb1RydXN0IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAKYVVaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9t +JPi8cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTTQjOgNB0e +RXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFhF7em6fgemdtzbvQKoiFs +7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2vc7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d +8Lsrlh/eezJS/R27tQahsiFepdaVaH/wmZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7V +qnJNk22CDtucvc+081xdVHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3Cga +Rr0BHdCXteGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZf9hB +Z3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfReBi9Fi1jUIxaS5BZu +KGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+nhutxx9z3SxPGWX9f5NAEC7S8O08 +ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0 +XG0D08DYj3rWMB8GA1UdIwQYMBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fXIwjhmF7DWgh2 +qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzynANXH/KttgCJwpQzgXQQpAvvL +oJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0zuzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsK +xr2EoyNB3tZ3b4XUhRxQ4K5RirqNPnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxF +KyDuSN/n3QmOGKjaQI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2 +DFKWkoRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9ER/frslK +xfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQtDF4JbAiXfKM9fJP/P6EU +p8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/SfuvmbJxPgWp6ZKy7PtXny3YuxadIwVyQD8vI +P/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- + +GeoTrust Universal CA 2 +======================= +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN +R2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwHhcNMDQwMzA0 +MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3Qg +SW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0 +DE81WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUGFF+3Qs17 +j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdqXbboW0W63MOhBW9Wjo8Q +JqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxLse4YuU6W3Nx2/zu+z18DwPw76L5GG//a +QMJS9/7jOvdqdzXQ2o3rXhhqMcceujwbKNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2 +WP0+GfPtDCapkzj4T8FdIgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP +20gaXT73y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRthAAn +ZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgocQIgfksILAAX/8sgC +SqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4Lt1ZrtmhN79UNdxzMk+MBB4zsslG +8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2 ++/CfXGJx7Tz0RzgQKzAfBgNVHSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8E +BAMCAYYwDQYJKoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z +dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQL1EuxBRa3ugZ +4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgrFg5fNuH8KrUwJM/gYwx7WBr+ +mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSoag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpq +A1Ihn0CoZ1Dy81of398j9tx4TuaYT1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpg +Y+RdM4kX2TGq2tbzGDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiP +pm8m1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJVOCiNUW7d +FGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH6aLcr34YEoP9VhdBLtUp +gn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwXQMAJKOSLakhT2+zNVVXxxvjpoixMptEm +X36vWkzaH6byHCx+rgIW0lbQL1dTR+iS +-----END CERTIFICATE----- + +Comodo AAA Services root +======================== +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS +R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg +TGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAw +MFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hl +c3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV +BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQuaBtDFcCLNSS1UY8y2bmhG +C1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe3M/vg4aijJRPn2jymJBGhCfHdr/jzDUs +i14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszW +Y19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjH +Ypy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEK +Iz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0f +BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNl +cy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2Vz +LmNybDANBgkqhkiG9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm +7l3sAg9g1o1QGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z +8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C +12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +QuoVadis Root CA +================ +-----BEGIN CERTIFICATE----- +MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJCTTEZMBcGA1UE +ChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAz +MTkxODMzMzNaFw0yMTAzMTcxODMzMzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRp +cyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQD +EyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Ypli4kVEAkOPcahdxYTMuk +J0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2DrOpm2RgbaIr1VxqYuvXtdj182d6UajtL +F8HVj71lODqV0D1VNk7feVcxKh7YWWVJWCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeL +YzcS19Dsw3sgQUSj7cugF+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWen +AScOospUxbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCCAk4w +PQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVvdmFkaXNvZmZzaG9y +ZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREwggENMIIBCQYJKwYBBAG+WAABMIH7 +MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNlIG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmlj +YXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJs +ZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh +Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYIKwYBBQUHAgEW +Fmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3TKbkGGew5Oanwl4Rqy+/fMIGu +BgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rqy+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkw +FwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MS4wLAYDVQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6 +tlCLMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSkfnIYj9lo +fFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf87C9TqnN7Az10buYWnuul +LsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1RcHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2x +gI4JVrmcGmD+XcHXetwReNDWXcG31a0ymQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi +5upZIof4l/UO/erMkqQWxFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi +5nrQNiOKSnQ2+Q== +-----END CERTIFICATE----- + +QuoVadis Root CA 2 +================== +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT +EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMjAeFw0wNjExMjQx +ODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCaGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6 +XJxgFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55JWpzmM+Yk +lvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bBrrcCaoF6qUWD4gXmuVbB +lDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp+ARz8un+XJiM9XOva7R+zdRcAitMOeGy +lZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt +66/3FsvbzSUr5R/7mp/iUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1Jdxn +wQ5hYIizPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og/zOh +D7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UHoycR7hYQe7xFSkyy +BNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuIyV77zGHcizN300QyNQliBJIWENie +J0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1Ud +DgQWBBQahGK8SEwzJQTU7tD2A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGU +a6FJpEcwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2fBluornFdLwUv +Z+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzng/iN/Ae42l9NLmeyhP3ZRPx3 +UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2BlfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodm +VjB3pjd4M1IQWK4/YY7yarHvGH5KWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK ++JDSV6IZUaUtl0HaB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrW +IozchLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPRTUIZ3Ph1 +WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWDmbA4CD/pXvk1B+TJYm5X +f6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0ZohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II +4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8 +VCLAAVBpQ570su9t+Oza8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +QuoVadis Root CA 3 +================== +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT +EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0wNjExMjQx +OTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNgg +DhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUrH556VOij +KTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd8lyyBTNvijbO0BNO/79K +DDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9CabwvvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbv +BNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwp +p5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8 +nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEX +MJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyM +Gf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclz +uD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHT +BgkrBgEEAb5YAAMwgcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmlj +YXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYB +BQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD +VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4 +ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UE +AxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZV +qyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSemd1o417+s +hvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd+LJ2w/w4E6oM3kJpK27z +POuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2 +Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp +8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBC +bjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXu +g/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91p +vGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbr +qZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +Security Communication Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP +U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw +HhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP +U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw +8yl89f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJDKaVv0uM +DPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9Ms+k2Y7CI9eNqPPYJayX +5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/NQV3Is00qVUarH9oe4kA92819uZKAnDfd +DJZkndwi92SL32HeFZRSFaB9UslLqCHJxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2 +JChzAgMBAAGjPzA9MB0GA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vGkl3g +0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfrUj94nK9NrvjVT8+a +mCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5Bw+SUEmK3TGXX8npN6o7WWWXlDLJ +s58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJUJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ +6rBK+1YWc26sTfcioU+tHXotRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAi +FL39vmwLAw== +-----END CERTIFICATE----- + +Sonera Class 2 Root CA +====================== +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEPMA0GA1UEChMG +U29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAxMDQwNjA3Mjk0MFoXDTIxMDQw +NjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNVBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJh +IENsYXNzMiBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3 +/Ei9vX+ALTU74W+oZ6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybT +dXnt5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s3TmVToMG +f+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2EjvOr7nQKV0ba5cTppCD8P +tOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu8nYybieDwnPz3BjotJPqdURrBGAgcVeH +nfO+oJAjPYok4doh28MCAwEAAaMzMDEwDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITT +XjwwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt +0jSv9zilzqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/3DEI +cbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvDFNr450kkkdAdavph +Oe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6Tk6ezAyNlNzZRZxe7EJQY670XcSx +EtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLH +llpwrN9M +-----END CERTIFICATE----- + +XRamp Global CA Root +==================== +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UE +BhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2Vj +dXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDQxMTAxMTcxNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMx +HjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkg +U2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS638eMpSe2OAtp87ZOqCwu +IR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCPKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMx +foArtYzAQDsRhtDLooY2YKTVMIJt2W7QDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FE +zG+gSqmUsE3a56k0enI4qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqs +AxcZZPRaJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNViPvry +xS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASsjVy16bYbMDYGA1UdHwQvMC0wK6Ap +oCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMC +AQEwDQYJKoZIhvcNAQEFBQADggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc +/Kh4ZzXxHfARvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLaIR9NmXmd4c8n +nxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSyi6mx5O+aGtA9aZnuqCij4Tyz +8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQO+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +Go Daddy Class 2 CA +=================== +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMY +VGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkG +A1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g +RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQAD +ggENADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv +2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+qN1j3hybX2C32 +qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6j +YGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmY +vLEHZ6IVDd2gWMZEewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0O +BBYEFNLEsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h/t2o +atTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMu +MTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wim +PQoZ+YeAEW5p5JYXMP80kWNyOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKt +I3lpjbi2Tc7PTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mERdEr/VxqHD3VI +Ls9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5CufReYNnyicsbkqWletNw+vHX/b +vZ8= +-----END CERTIFICATE----- + +Starfield Class 2 CA +==================== +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzElMCMGA1UEChMc +U3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZpZWxkIENsYXNzIDIg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQwNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBo +MQswCQYDVQQGEwJVUzElMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAG +A1UECxMpU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqG +SIb3DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf8MOh2tTY +bitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN+lq2cwQlZut3f+dZxkqZ +JRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVm +epsZGD3/cVE8MC5fvj13c7JdBmzDI1aaK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSN +F4Azbl5KXZnJHoe0nRrA1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HF +MIHCMB0GA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fRzt0f +hvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNo +bm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24g +QXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGs +afPzWdqbAYcaT1epoXkJKtv3L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLM +PUxA2IGvd56Deruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynpVSJYACPq4xJD +KVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEYWQPJIrSPnNVeKtelttQKbfi3 +QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +DigiCert Assured ID Root CA +=========================== +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzEx +MTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0Ew +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7cJpSIqvTO +9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYPmDI2dsze3Tyoou9q+yHy +UmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW +/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpy +oeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf +GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF +66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkq +hkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2Bc +EkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38Fn +SbNd67IJKusm7Xi+fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i +8b5QZ7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +DigiCert Global Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw +MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn +TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5 +BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H +4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y +7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB +o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm +8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF +BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr +EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt +tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886 +UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +DigiCert High Assurance EV Root CA +================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSsw +KQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAw +MFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ +MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFu +Y2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm+9S75S0t +Mqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTWPNt0OKRKzE0lgvdKpVMS +OO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEMxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3 +MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQ +NAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUe +h10aUAsgEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY +JhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQ +V8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFp +myPInngiK3BD41VHMWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkK +mNEVX58Svnw2Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K +-----END CERTIFICATE----- + +DST Root CA X3 +============== +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQK +ExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4X +DTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1 +cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmT +rE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9 +UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRy +xXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40d +utolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQ +MA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikug +dB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjE +GB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bw +RLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubS +fZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- + +SwissSign Gold CA - G2 +====================== +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNIMRUw +EwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0gRzIwHhcN +MDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dp +c3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUq +t2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+bbqBHH5C +jCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c6bM8K8vzARO/Ws/BtQpg +vd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqEemA8atufK+ze3gE/bk3lUIbLtK/tREDF +ylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvR +AiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuend +jIj3o02yMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkO +peUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR +7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGi +GqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64 +OfPAeGZe6Drn8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm +5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr +44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOf +Mke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6m +Gu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxp +mo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCChdiDyyJk +vC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid392qgQmwLOM7XdVAyksLf +KzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG2mqeSz53OiATIgHQv2ieY2Br +NU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6Lqj +viOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +SwissSign Silver CA - G2 +======================== +-----BEGIN CERTIFICATE----- +MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ0gxFTAT +BgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMB4X +DTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0NlowRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3 +aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644 +N0MvFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7brYT7QbNHm ++/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieFnbAVlDLaYQ1HTWBCrpJH +6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH6ATK72oxh9TAtvmUcXtnZLi2kUpCe2Uu +MGoM9ZDulebyzYLs2aFK7PayS+VFheZteJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5h +qAaEuSh6XzjZG6k4sIN/c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5 +FZGkECwJMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRHHTBs +ROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTfjNFusB3hB48IHpmc +celM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb65i/4z3GcRm25xBWNOHkDRUjvxF3X +CO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRB +tjpbO8tFnb0cwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 +cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBAHPGgeAn0i0P +4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShpWJHckRE1qTodvBqlYJ7YH39F +kWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L +3XWgwF15kIwb4FDm3jH+mHtwX6WQ2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx +/uNncqCxv1yL5PqZIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFa +DGi8aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2Xem1ZqSqP +e97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQRdAtq/gsD/KNVV4n+Ssuu +WxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJ +DIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ub +DgEj8Z+7fNzcbBGXJbLytGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u +-----END CERTIFICATE----- + +GeoTrust Primary Certification Authority +======================================== +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMoR2VvVHJ1c3QgUHJpbWFyeSBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgx +CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQ +cmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9AWbK7hWN +b6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjAZIVcFU2Ix7e64HXprQU9 +nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE07e9GceBrAqg1cmuXm2bgyxx5X9gaBGge +RwLmnWDiNpcB3841kt++Z8dtd1k7j53WkBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGt +tm/81w7a4DSwDRp35+MImO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJKoZI +hvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ16CePbJC/kRYkRj5K +Ts4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl4b7UVXGYNTq+k+qurUKykG/g/CFN +NWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6KoKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHa +Floxt/m0cYASSJlyc1pZU8FjUjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG +1riR/aYNKxoUAT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- + +thawte Primary Root CA +====================== +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCBqTELMAkGA1UE +BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2 +aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3 +MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwg +SW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMv +KGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMT +FnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCs +oPD7gFnUnMekz52hWXMJEEUMDSxuaPFsW0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ +1CRfBsDMRJSUjQJib+ta3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGc +q/gcfomk6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6Sk/K +aAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94JNqR32HuHUETVPm4p +afs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XPr87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUF +AAOCAQEAeRHAS7ORtvzw6WfUDW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeE +uzLlQRHAd9mzYJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2/qxAeeWsEG89 +jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/LHbTY5xZ3Y+m4Q6gLkH3LpVH +z7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7jVaMaA== +-----END CERTIFICATE----- + +VeriSign Class 3 Public Primary Certification Authority - G5 +============================================================ +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCByjELMAkGA1UE +BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO +ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk +IHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2ln +biBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBh +dXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCvJAgIKXo1nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKz +j/i5Vbext0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIzSdhD +Y2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQGBO+QueQA5N06tRn/ +Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+rCpSx4/VBEnkjWNHiDxpg8v+R70r +fk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/ +BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2Uv +Z2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKvMzEzMA0GCSqG +SIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzEp6B4Eq1iDkVwZMXnl2YtmAl+ +X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKE +KQsTb47bDN0lAtukixlE0kF6BWlKWE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiC +Km0oHw0LxOXnGiYZ4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vE +ZV8NhnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +SecureTrust CA +============== +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNlY3VyZVRy +dXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAe +BgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQX +OZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO0gMdA+9t +DWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIaowW8xQmxSPmjL8xk037uH +GFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b +01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmH +ursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYj +aHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ +KoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSu +SceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHf +mbx8IVQr5Fiiu1cprp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZ +nMUFdAvnZyPSCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +Secure Global CA +================ +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBH +bG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEg +MB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwg +Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jx +YDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa/FHtaMbQ +bqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJjnIFHovdRIWCQtBJwB1g +8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnIHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYV +HDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi +0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAn +oCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEA +MA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+ +OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cn +CDpOGR86p1hcF895P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/5 +3CYNv6ZHdAbYiNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +COMODO Certification Authority +============================== +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEb +MBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD +T01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH ++7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTww +xHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV +4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA +1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVI +rLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9k +b2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOC +AQEAPpiem/Yb6dc5t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CP +OGEIqB6BCsAvIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmc +IGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN ++8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ== +-----END CERTIFICATE----- + +Network Solutions Certificate Authority +======================================= +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQG +EwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydOZXR3b3Jr +IFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMx +MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwzc7MEL7xx +jOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPPOCwGJgl6cvf6UDL4wpPT +aaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rlmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXT +crA/vGp97Eh/jcOrqnErU2lBUzS1sLnFBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc +/Qzpf14Dl847ABSHJ3A4qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMB +AAGjgZcwgZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwubmV0c29sc3NsLmNv +bS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3JpdHkuY3JsMA0GCSqGSIb3DQEBBQUA +A4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc86fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q +4LqILPxFzBiwmZVRDuwduIj/h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/ +GGUsyfJj4akH/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHNpGxlaKFJdlxD +ydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- + +COMODO ECC Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix +GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X +4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni +wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG +FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA +U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +Certigna +======== +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZSMRIw +EAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMwNVoXDTI3 +MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwI +Q2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7q +XOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyH +GxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbwzBfsV1/p +ogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q130yGLMLLGq/jj8UEYkg +DncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKf +Irjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQ +tCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJ +BgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/J +SP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEA +hQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+ +ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1klu +PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY +1gkIl2PlwS6wt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +Cybertrust Global Root +====================== +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYGA1UEChMPQ3li +ZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBSb290MB4XDTA2MTIxNTA4 +MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQD +ExZDeWJlcnRydXN0IEdsb2JhbCBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA ++Mi8vRRQZhP/8NN57CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW +0ozSJ8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2yHLtgwEZL +AfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iPt3sMpTjr3kfb1V05/Iin +89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNzFtApD0mpSPCzqrdsxacwOUBdrsTiXSZT +8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAYXSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2 +MDSgMqAwhi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3JsMB8G +A1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUAA4IBAQBW7wojoFRO +lZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMjWqd8BfP9IjsO0QbE2zZMcwSO5bAi +5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUxXOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2 +hO0j9n0Hq0V+09+zv+mKts2oomcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+T +X3EJIrduPuocA06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +ePKI Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQG +EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsMIWVQS0kg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMx +MjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEq +MCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAHSyZbCUNs +IZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAhijHyl3SJCRImHJ7K2RKi +lTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3XDZoTM1PRYfl61dd4s5oz9wCGzh1NlDiv +qOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX +12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0O +WQqraffAsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+ +ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnao +lQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/ +vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXi +Zo1jDiVN1Rmy5nk3pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/Qkqi +MAwGA1UdEwQFMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B0 +1GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq +KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdV +xrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEP +NXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+r +GNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUBo2M3IUxE +xJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS/jQ6fbjpKdx2qcgw+BRx +gMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C6pSe3VkQw63d4k3jMdXH7Ojy +sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD +BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +certSIGN ROOT CA +================ +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREwDwYD +VQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQxNzIwMDRa +Fw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UE +CxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7I +JUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHH +rfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5dRdY4zTW2 +ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQOA7+j0xbm0bqQfWwCHTD +0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwvJoIQ4uNllAoEwF73XVv4EOLQunpL+943 +AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B +Af8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IB +AQA+0hyJLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8 +SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0 +x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIlt +vBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7Nz +TogVZ96edhBiIL5VaZVDADlN9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +GeoTrust Primary Certification Authority - G3 +============================================= +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA4IEdlb1RydXN0 +IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFy +eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIz +NTk1OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAo +YykgMjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMT +LUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz+uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5j +K/BGvESyiaHAKAxJcCGVn2TAppMSAmUmhsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdE +c5IiaacDiGydY8hS2pgn5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3C +IShwiP/WJmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exALDmKu +dlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZChuOl1UcCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMR5yo6hTgMdHNxr +2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IBAQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9 +cr5HqQ6XErhK8WTTOd8lNNTBzU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbE +Ap7aDHdlDkQNkv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD +AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUHSJsMC8tJP33s +t/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2Gspki4cErx5z481+oghLrGREt +-----END CERTIFICATE----- + +thawte Primary Root CA - G2 +=========================== +-----BEGIN CERTIFICATE----- +MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDELMAkGA1UEBhMC +VVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMpIDIwMDcgdGhhd3RlLCBJbmMu +IC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3Qg +Q0EgLSBHMjAeFw0wNzExMDUwMDAwMDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEV +MBMGA1UEChMMdGhhd3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBG +b3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAt +IEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/BebfowJPDQfGAFG6DAJS +LSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6papu+7qzcMBniKI11KOasf2twu8x+qi5 +8/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU +mtgAMADna3+FGO6Lts6KDPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUN +G4k8VIZ3KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41oxXZ3K +rr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== +-----END CERTIFICATE----- + +thawte Primary Root CA - G3 +=========================== +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCBrjELMAkGA1UE +BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2 +aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0w +ODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh +d3RlLCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9uMTgwNgYD +VQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIG +A1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAsr8nLPvb2FvdeHsbnndmgcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2At +P0LMqmsywCPLLEHd5N/8YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC ++BsUa0Lfb1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS99irY +7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2SzhkGcuYMXDhpxwTW +vGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUkOQIDAQABo0IwQDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJ +KoZIhvcNAQELBQADggEBABpA2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweK +A3rD6z8KLFIWoCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu +t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7cKUGRIjxpp7sC +8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fMm7v/OeZWYdMKp8RcTGB7BXcm +er/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZuMdRAGmI0Nj81Aa6sY6A= +-----END CERTIFICATE----- + +GeoTrust Primary Certification Authority - G2 +============================================= +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA3IEdlb1RydXN0IElu +Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1 +OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg +MjAwNyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMTLUdl +b1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjB2MBAGByqGSM49AgEG +BSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcLSo17VDs6bl8VAsBQps8lL33KSLjHUGMc +KiEIfJo22Av+0SbFWDEwKCXzXV2juLaltJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+ +EVXVMAoGCCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGTqQ7m +ndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBuczrD6ogRLQy7rQkgu2 +npaqBA+K +-----END CERTIFICATE----- + +VeriSign Universal Root Certification Authority +=============================================== +-----BEGIN CERTIFICATE----- +MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCBvTELMAkGA1UE +BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO +ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk +IHVzZSBvbmx5MTgwNgYDVQQDEy9WZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTAeFw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJV +UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv +cmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNhbCBSb290IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj +1mCOkdeQmIN65lgZOIzF9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGP +MiJhgsWHH26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+HLL72 +9fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN/BMReYTtXlT2NJ8I +AfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPTrJ9VAMf2CGqUuV/c4DPxhGD5WycR +tPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0G +CCsGAQUFBwEMBGEwX6FdoFswWTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2O +a8PPgGrUSBgsexkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud +DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4sAPmLGd75JR3 +Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+seQxIcaBlVZaDrHC1LGmWazx +Y8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTx +P/jgdFcrGJ2BtMQo2pSXpXDrrB2+BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+P +wGZsY6rp2aQW9IHRlRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4 +mJO37M2CYfE45k+XmCpajQ== +-----END CERTIFICATE----- + +VeriSign Class 3 Public Primary Certification Authority - G4 +============================================================ +-----BEGIN CERTIFICATE----- +MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjELMAkGA1UEBhMC +VVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3 +b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVz +ZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBU +cnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRo +b3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5 +IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8 +Utpkmw4tXNherJI9/gHmGUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGz +rl0Bp3vefLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEw +HzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVyaXNpZ24u +Y29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMWkf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMD +A2gAMGUCMGYhDBgmYFo4e1ZC4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIx +AJw9SDkjOVgaFRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== +-----END CERTIFICATE----- + +NetLock Arany (Class Gold) Főtanúsítvány +======================================== +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G +A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610 +dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB +cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx +MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO +ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6 +c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu +0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw +/HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk +H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw +fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1 +neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW +qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta +YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna +NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu +dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +Hongkong Post Root CA 1 +======================= +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoT +DUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMB4XDTAzMDUx +NTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25n +IFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1 +ApzQjVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEnPzlTCeqr +auh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjhZY4bXSNmO7ilMlHIhqqh +qZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9nnV0ttgCXjqQesBCNnLsak3c78QA3xMY +V18meMjWCnl3v/evt3a5pQuEF10Q6m/hq5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNV +HRMBAf8ECDAGAQH/AgEDMA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7i +h9legYsCmEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI37pio +l7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clBoiMBdDhViw+5Lmei +IAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJsEhTkYY2sEJCehFC78JZvRZ+K88ps +T/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpOfMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilT +c4afU9hDDl3WY4JxHYB0yvbiAmvZWg== +-----END CERTIFICATE----- + +SecureSign RootCA11 +=================== +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDErMCkGA1UEChMi +SmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoGA1UEAxMTU2VjdXJlU2lnbiBS +b290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSsw +KQYDVQQKEyJKYXBhbiBDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1 +cmVTaWduIFJvb3RDQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvL +TJszi1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8h9uuywGO +wvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOVMdrAG/LuYpmGYz+/3ZMq +g6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rP +O7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitA +bpSACW22s293bzUIUPsCh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZX +t94wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKCh +OBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xmKbabfSVSSUOrTC4r +bnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQX5Ucv+2rIrVls4W6ng+4reV6G4pQ +Oh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWrQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01 +y8hSyn+B/tlr0/cR7SXf+Of5pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061 +lgeLKBObjBmNQSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- + +Microsec e-Szigno Root CA 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER +MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv +c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE +BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt +U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA +fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG +0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA +pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm +1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC +AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf +QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE +FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o +lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX +I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02 +yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi +LXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +GlobalSign Root CA - R3 +======================= +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv +YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh +bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT +aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt +iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ +0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3 +rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl +OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2 +xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7 +lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8 +EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E +bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18 +YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r +kpeDMdmztcpHWD9f +-----END CERTIFICATE----- + +Autoridad de Certificacion Firmaprofesional CIF A62634068 +========================================================= +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UEBhMCRVMxQjBA +BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 +MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEyMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIw +QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB +NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD +Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P +B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY +7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH +ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI +plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX +MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX +LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK +bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU +vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1Ud +EwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNH +DhpkLzCBpgYDVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBvACAAZABlACAA +bABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBlAGwAbwBuAGEAIAAwADgAMAAx +ADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx +51tkljYyGOylMnfX40S2wBEqgLk9am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qk +R71kMrv2JYSiJ0L1ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaP +T481PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS3a/DTg4f +Jl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5kSeTy36LssUzAKh3ntLFl +osS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF3dvd6qJ2gHN99ZwExEWN57kci57q13XR +crHedUTnQn3iV2t93Jm8PYMo6oCTjcVMZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoR +saS8I8nkvof/uZS2+F0gStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTD +KCOM/iczQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQBjLMi +6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- + +Izenpe.com +========== +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYDVQQG +EwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcNMDcxMjEz +MTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMu +QS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ +03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAK +ClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6HLmYRY2xU ++zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFXuaOKmMPsOzTFlUFpfnXC +PCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQDyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxT +OTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbK +F7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK +0GqfvEyNBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+ +0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbB +leStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwID +AQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+ +SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBG +NjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l +Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbga +kEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8q +hT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Cs +g1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCTVyvehQP5 +aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGkLhObNA5me0mrZJfQRsN5 +nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqtujWTI6cfSN01RpiyEGjkpTHC +ClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZo +Q0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1Z +WrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +Chambers of Commerce Root - 2008 +================================ +-----BEGIN CERTIFICATE----- +MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYDVQQGEwJFVTFD +MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv +bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu +QS4xKTAnBgNVBAMTIENoYW1iZXJzIG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEy +Mjk1MFoXDTM4MDczMTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNl +ZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQF +EwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJl +cnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW928sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKA +XuFixrYp4YFs8r/lfTJqVKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorj +h40G072QDuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR5gN/ +ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfLZEFHcpOrUMPrCXZk +NNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05aSd+pZgvMPMZ4fKecHePOjlO+Bd5g +D2vlGts/4+EhySnB8esHnFIbAURRPHsl18TlUlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331 +lubKgdaX8ZSD6e2wsWsSaR6s+12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ +0wlf2eOKNcx5Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj +ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAxhduub+84Mxh2 +EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNVHQ4EFgQU+SSsD7K1+HnA+mCI +G8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1+HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJ +BgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNh +bWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENh +bWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDiC +CQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUH +AgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAJASryI1 +wqM58C7e6bXpeHxIvj99RZJe6dqxGfwWPJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH +3qLPaYRgM+gQDROpI9CF5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbU +RWpGqOt1glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaHFoI6 +M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2pSB7+R5KBWIBpih1 +YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MDxvbxrN8y8NmBGuScvfaAFPDRLLmF +9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QGtjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcK +zBIKinmwPQN/aUv0NCB9szTqjktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvG +nrDQWzilm1DefhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg +OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZd0jQ +-----END CERTIFICATE----- + +Global Chambersign Root - 2008 +============================== +-----BEGIN CERTIFICATE----- +MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYDVQQGEwJFVTFD +MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv +bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu +QS4xJzAlBgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMx +NDBaFw0zODA3MzExMjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUg +Y3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJ +QTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD +aGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDf +VtPkOpt2RbQT2//BthmLN0EYlVJH6xedKYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXf +XjaOcNFccUMd2drvXNL7G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0 +ZJJ0YPP2zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4ddPB +/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyGHoiMvvKRhI9lNNgA +TH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2Id3UwD2ln58fQ1DJu7xsepeY7s2M +H/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3VyJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfe +Ox2YItaswTXbo6Al/3K1dh3ebeksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSF +HTynyQbehP9r6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh +wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsogzCtLkykPAgMB +AAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQWBBS5CcqcHtvTbDprru1U8VuT +BjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDprru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UE +BhMCRVUxQzBBBgNVBAcTOk1hZHJpZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJm +aXJtYS5jb20vYWRkcmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJm +aXJtYSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiCCQDJzdPp +1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0 +dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAICIf3DekijZBZRG +/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZUohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6 +ReAJ3spED8IXDneRRXozX1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/s +dZ7LoR/xfxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVza2Mg +9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yydYhz2rXzdpjEetrHH +foUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMdSqlapskD7+3056huirRXhOukP9Du +qqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9OAP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETr +P3iZ8ntxPjzxmKfFGBI/5rsoM0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVq +c5iJWzouE4gev8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z +09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B +-----END CERTIFICATE----- + +Go Daddy Root Certificate Authority - G2 +======================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu +MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G +A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq +9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD ++qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd +fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl +NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9 +BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac +vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r +5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV +N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1 +-----END CERTIFICATE----- + +Starfield Root Certificate Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0 +eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw +DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg +VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB +dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv +W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs +bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk +N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf +ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU +JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol +TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx +4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw +F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ +c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +Starfield Services Root Certificate Authority - G2 +================================================== +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl +IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT +dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2 +h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa +hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP +LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB +rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG +SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP +E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy +xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza +YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6 +-----END CERTIFICATE----- + +AffirmTrust Commercial +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMB4XDTEw +MDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6Eqdb +DuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yrba0F8PrV +C8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPALMeIrJmqbTFeurCA+ukV6 +BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1yHp52UKqK39c/s4mT6NmgTWvRLpUHhww +MmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNV +HQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPG +hi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDi +qX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv +0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0kh +sUlHRUe072o0EclNmsxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +AffirmTrust Networking +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMB4XDTEw +MDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SE +Hi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbuakCNrmreI +dIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRLQESxG9fhwoXA3hA/Pe24 +/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gb +h+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNV +HQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIu +UFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF6 +12S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23 +WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9 +/ZFvgrG+CJPbFEfxojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +AffirmTrust Premium +=================== +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4XDTEwMDEy +OTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRy +dXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtn +BKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ+jjeRFcV +5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrSs8PhaJyJ+HoAVt70VZVs ++7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmd +GPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5R +p9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NI +S+LI+H+SqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u04 +6uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5 +/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo ++Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByv +MiPIs0laUZx2KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC +6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S +L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK ++4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmV +BtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFg +IxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8GKa1qF60 +g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaORtGdFNrHF+QFlozEJLUb +zxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAloGRwYQw== +-----END CERTIFICATE----- + +AffirmTrust Premium ECC +======================= +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDASBgNV +BAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAeFw0xMDAx +MjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1U +cnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQ +N8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0GA1UdDgQW +BBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAK +BggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/VsaobgxCd05DhT1wV/GzTjxi+zygk8N53X +57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKM +eQ== +-----END CERTIFICATE----- + +Certum Trusted Network CA +========================= +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK +ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy +MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU +ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC +l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J +J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4 +fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0 +cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw +DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj +jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1 +mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj +Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +TWCA Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ +VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG +EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB +IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx +QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC +oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP +4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r +y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG +9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC +mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW +QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY +T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny +Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +Security Communication RootCA2 +============================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc +U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh +dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC +SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy +aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++ ++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R +3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV +spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K +EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8 +QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB +CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj +u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk +3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q +tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29 +mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +EC-ACC +====== +-----BEGIN CERTIFICATE----- +MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB8zELMAkGA1UE +BhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2VydGlmaWNhY2lvIChOSUYgUS0w +ODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYD +VQQLEyxWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UE +CxMsSmVyYXJxdWlhIEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMT +BkVDLUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQGEwJFUzE7 +MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8gKE5JRiBRLTA4MDExNzYt +SSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBDZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZl +Z2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQubmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJh +cnF1aWEgRW50aXRhdHMgZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUND +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R85iK +w5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm4CgPukLjbo73FCeT +ae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaVHMf5NLWUhdWZXqBIoH7nF2W4onW4 +HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNdQlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0a +E9jD2z3Il3rucO2n5nzbcc8tlGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw +0JDnJwIDAQABo4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4opvpXY0wfwYD +VR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBodHRwczovL3d3dy5jYXRjZXJ0 +Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5l +dC92ZXJhcnJlbCAwDQYJKoZIhvcNAQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJ +lF7W2u++AVtd0x7Y/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNa +Al6kSBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhyRp/7SNVe +l+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOSAgu+TGbrIP65y7WZf+a2 +E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xlnJ2lYJU6Un/10asIbvPuW/mIPX64b24D +5EI= +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions RootCA 2011 +======================================================= +-----BEGIN CERTIFICATE----- +MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1IxRDBCBgNVBAoT +O0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9y +aXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z +IFJvb3RDQSAyMDExMB4XDTExMTIwNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYT +AkdSMUQwQgYDVQQKEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z +IENlcnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNo +IEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPzdYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI +1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa +71HFK9+WXesyHgLacEnsbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u +8yBRQlqD75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSPFEDH +3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNVHRMBAf8EBTADAQH/ +MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp5dgTBCPuQSUwRwYDVR0eBEAwPqA8 +MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQub3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQu +b3JnMA0GCSqGSIb3DQEBBQUAA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVt +XdMiKahsog2p6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 +TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7dIsXRSZMFpGD +/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8AcysNnq/onN694/BtZqhFLKPM58N +7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXIl7WdmplNsDz4SgCbZN2fOUvRJ9e4 +-----END CERTIFICATE----- + +Actalis Authentication Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVQxDjAM +BgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UE +AwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDky +MjExMjIwMlowazELMAkGA1UEBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlz +IFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNvUTufClrJ +wkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX4ay8IMKx4INRimlNAJZa +by/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9KK3giq0itFZljoZUj5NDKd45RnijMCO6 +zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1f +YVEiVRvjRuPjPdA1YprbrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2 +oxgkg4YQ51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2Fbe8l +EfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxeKF+w6D9Fz8+vm2/7 +hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4Fv6MGn8i1zeQf1xcGDXqVdFUNaBr8 +EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbnfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5 +jF66CyCU3nuDuP/jVo23Eek7jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLY +iDrIn3hm7YnzezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQALe3KHwGCmSUyI +WOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70jsNjLiNmsGe+b7bAEzlgqqI0 +JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDzWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKx +K3JCaKygvU5a2hi/a5iB0P2avl4VSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+ +Xlff1ANATIGk0k9jpwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC +4yyXX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+OkfcvHlXHo +2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7RK4X9p2jIugErsWx0Hbhz +lefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btUZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXem +OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9 +vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +Trustis FPS Root CA +=================== +-----BEGIN CERTIFICATE----- +MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQG +EwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQLExNUcnVzdGlzIEZQUyBSb290 +IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTExMzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNV +BAoTD1RydXN0aXMgTGltaXRlZDEcMBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQ +RUN+AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihHiTHcDnlk +H5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjjvSkCqPoc4Vu5g6hBSLwa +cY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zt +o3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlBOrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEA +AaNTMFEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAd +BgNVHQ4EFgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01GX2c +GE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmWzaD+vkAMXBJV+JOC +yinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP41BIy+Q7DsdwyhEQsb8tGD+pmQQ9P +8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZEf1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHV +l/9D7S3B2l0pKoU/rGXuhg8FjZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYl +iB6XzCGcKQENZetX2fNXlrtIzYE= +-----END CERTIFICATE----- + +Buypass Class 2 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMiBSb290IENBMB4X +DTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1owTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1 +g1Lr6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPVL4O2fuPn +9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC911K2GScuVr1QGbNgGE41b +/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHxMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqU +CqTqc/sLUegTBxj6DvEr0VQVfTzh97QZQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeff +awrbD02TTqigzXsu8lkBarcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgI +zRFo1clrUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLiFRhn +Bkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRSP/TizPJhk9H9Z2vX +Uq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN9SG9dKpN6nIDSdvHXx1iY8f93ZHs +M+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxPAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFMmAd+BikoL1RpzzuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAU18h9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3tOluwlN5E40EI +osHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo+fsicdl9sz1Gv7SEr5AcD48S +aq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYd +DnkM/crqJIByw5c/8nerQyIKx+u2DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWD +LfJ6v9r9jv6ly0UsH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0 +oyLQI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK75t98biGC +wWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h3PFaTWwyI0PurKju7koS +CTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPzY11aWOIv4x3kqdbQCtCev9eBCfHJxyYN +rJgWVqA= +-----END CERTIFICATE----- + +Buypass Class 3 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMyBSb290IENBMB4X +DTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFowTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRH +sJ8YZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3EN3coTRiR +5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9tznDDgFHmV0ST9tD+leh +7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX0DJq1l1sDPGzbjniazEuOQAnFN44wOwZ +ZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH +2xc519woe2v1n/MuwU8XKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV +/afmiSTYzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvSO1UQ +RwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D34xFMFbG02SrZvPA +Xpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgPK9Dx2hzLabjKSWJtyNBjYt1gD1iq +j6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFEe4zf/lb+74suwvTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAACAjQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXSIGrs/CIBKM+G +uIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2HJLw5QY33KbmkJs4j1xrG0aG +Q0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsaO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8 +ZORK15FTAaggiG6cX0S5y2CBNOxv033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2 +KSb12tjE8nVhz36udmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz +6MkEkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg413OEMXbug +UZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvDu79leNKGef9JOxqDDPDe +eOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq4/g7u9xN12TyUb7mqqta6THuBrxzvxNi +Cp/HuZc= +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 3 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgx +MDAxMTAyOTU2WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN8ELg63iIVl6bmlQdTQyK +9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/RLyTPWGrTs0NvvAgJ1gORH8EGoel15YU +NpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZF +iP0Zf3WHHx+xGwpzJFu5ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W +0eDrXltMEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1A/d2O2GCahKqGFPr +AyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOyWL6ukK2YJ5f+AbGwUgC4TeQbIXQb +fsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzT +ucpH9sry9uetuUg/vBa3wW306gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7h +P0HHRwA11fXT91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4pTpPDpFQUWw== +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTAe +Fw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NThaME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxE +LVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOAD +ER03UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42tSHKXzlA +BF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9RySPocq60vFYJfxLLHLGv +KZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsMlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7z +p+hnUquVH+BGPtikw8paxTGA6Eian5Rp/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUC +AwEAAaOCARowggEWMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ +4PGEMA4GA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVjdG9y +eS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUyMENBJTIwMiUyMDIw +MDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRlcmV2b2NhdGlvbmxpc3QwQ6BBoD+G +PWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAw +OS5jcmwwDQYJKoZIhvcNAQELBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm +2H6NMLVwMeniacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4KzCUqNQT4YJEV +dT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8PIWmawomDeCTmGCufsYkl4ph +X5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3YJohw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 EV 2009 +================================= +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUwNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfS +egpnljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM03TP1YtHh +zRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6ZqQTMFexgaDbtCHu39b+T +7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lRp75mpoo6Kr3HGrHhFPC+Oh25z1uxav60 +sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure35 +11H3a6UCAwEAAaOCASQwggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyv +cop9NteaHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFwOi8v +ZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xhc3MlMjAzJTIwQ0El +MjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1ERT9jZXJ0aWZpY2F0ZXJldm9jYXRp +b25saXN0MEagRKBChkBodHRwOi8vd3d3LmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xh +c3NfM19jYV8yX2V2XzIwMDkuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+ +PPoeUSbrh/Yp3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNFCSuGdXzfX2lX +ANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7naxpeG0ILD5EJt/rDiZE4OJudA +NCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqXKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVv +w9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +CA Disig Root R2 +================ +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNVBAYTAlNLMRMw +EQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMuMRkwFwYDVQQDExBDQSBEaXNp +ZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQyMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sx +EzARBgNVBAcTCkJyYXRpc2xhdmExEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERp +c2lnIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbC +w3OeNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNHPWSb6Wia +xswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3Ix2ymrdMxp7zo5eFm1tL7 +A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbeQTg06ov80egEFGEtQX6sx3dOy1FU+16S +GBsEWmjGycT6txOgmLcRK7fWV8x8nhfRyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqV +g8NTEQxzHQuyRpDRQjrOQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa +5Beny912H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJQfYE +koopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUDi/ZnWejBBhG93c+A +Ak9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORsnLMOPReisjQS1n6yqEm70XooQL6i +Fh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5u +Qu0wDQYJKoZIhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqfGopTpti72TVV +sRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkblvdhuDvEK7Z4bLQjb/D907Je +dR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W8 +1k/BfDxujRNt+3vrMNDcTa/F1balTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjx +mHHEt38OFdAlab0inSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01 +utI3gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18DrG5gPcFw0 +sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3OszMOl6W8KjptlwlCFtaOg +UxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8xL4ysEr3vQCj8KWefshNPZiTEUxnpHikV +7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +ACCVRAIZ1 +========= +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UEAwwJQUNDVlJB +SVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQswCQYDVQQGEwJFUzAeFw0xMTA1 +MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwH +UEtJQUNDVjENMAsGA1UECgwEQUNDVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCbqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gM +jmoYHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWoG2ioPej0 +RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpAlHPrzg5XPAOBOp0KoVdD +aaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhrIA8wKFSVf+DuzgpmndFALW4ir50awQUZ +0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDG +WuzndN9wrqODJerWx5eHk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs7 +8yM2x/474KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMOm3WR +5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpacXpkatcnYGMN285J +9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPluUsXQA+xtrn13k/c4LOsOxFwYIRK +Q26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYIKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRw +Oi8vd3d3LmFjY3YuZXMvZmlsZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEu +Y3J0MB8GCCsGAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeTVfZW6oHlNsyM +Hj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIGCCsGAQUFBwICMIIBFB6CARAA +QQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUAcgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBh +AO0AegAgAGQAZQAgAGwAYQAgAEEAQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUA +YwBuAG8AbABvAGcA7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBj +AHQAcgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAAQwBQAFMA +IABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUAczAwBggrBgEFBQcCARYk +aHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2MuaHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0 +dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRtaW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2 +MV9kZXIuY3JsMA4GA1UdDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZI +hvcNAQEFBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdpD70E +R9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gUJyCpZET/LtZ1qmxN +YEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+mAM/EKXMRNt6GGT6d7hmKG9Ww7Y49 +nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepDvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJ +TS+xJlsndQAJxGJ3KQhfnlmstn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3 +sCPdK6jT2iWH7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szAh1xA2syVP1Xg +Nce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xFd3+YJ5oyXSrjhO7FmGYvliAd +3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2HpPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3p +EfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +TWCA Global Root CA +=================== +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcxEjAQBgNVBAoT +CVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMTVFdDQSBHbG9iYWwgUm9vdCBD +QTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQK +EwlUQUlXQU4tQ0ExEDAOBgNVBAsTB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2C +nJfF10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz0ALfUPZV +r2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfChMBwqoJimFb3u/Rk28OKR +Q4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbHzIh1HrtsBv+baz4X7GGqcXzGHaL3SekV +tTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1W +KKD+u4ZqyPpcC1jcxkt2yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99 +sy2sbZCilaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYPoA/p +yJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQABDzfuBSO6N+pjWxn +kjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcEqYSjMq+u7msXi7Kx/mzhkIyIqJdI +zshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6g +cFGn90xHNcgL1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WFH6vPNOw/KP4M +8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNoRI2T9GRwoD2dKAXDOXC4Ynsg +/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlg +lPx4mI88k1HtQJAH32RjJMtOcQWh15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryP +A9gK8kxkRr05YuWW6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3m +i4TWnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5jwa19hAM8 +EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWzaGHQRiapIVJpLesux+t3 +zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmyKwbQBM0= +-----END CERTIFICATE----- + +TeliaSonera Root CA v1 +====================== +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAwNzEUMBIGA1UE +CgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJvb3QgQ0EgdjEwHhcNMDcxMDE4 +MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYDVQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwW +VGVsaWFTb25lcmEgUm9vdCBDQSB2MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+ +6yfwIaPzaSZVfp3FVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA +3GV17CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+XZ75Ljo1k +B1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+/jXh7VB7qTCNGdMJjmhn +Xb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxH +oLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkmdtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3 +F0fUTPHSiXk+TT2YqGHeOh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJ +oWjiUIMusDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4pgd7 +gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fsslESl1MpWtTwEhDc +TwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQarMCpgKIv7NHfirZ1fpoeDVNAgMB +AAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qW +DNXr+nuqF+gTEjANBgkqhkiG9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNm +zqjMDfz1mgbldxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1TjTQpgcmLNkQfW +pb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBedY2gea+zDTYa4EzAvXUYNR0PV +G6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpc +c41teyWRyu5FrgZLAMzTsVlQ2jqIOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOT +JsjrDNYmiLbAJM+7vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2 +qReWt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcnHL/EVlP6 +Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVxSK236thZiNSQvxaz2ems +WWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +E-Tugra Certification Authority +=============================== +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNVBAYTAlRSMQ8w +DQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamls +ZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN +ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMw +NTEyMDk0OFoXDTIzMDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmEx +QDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxl +cmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQD +DB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEA4vU/kwVRHoViVF56C/UYB4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vd +hQd2h8y/L5VMzH2nPbxHD5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5K +CKpbknSFQ9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEoq1+g +ElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3Dk14opz8n8Y4e0ypQ +BaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcHfC425lAcP9tDJMW/hkd5s3kc91r0 +E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsutdEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gz +rt48Ue7LE3wBf4QOXVGUnhMMti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAq +jqFGOjGY5RH8zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn +rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUXU8u3Zg5mTPj5 +dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6Jyr+zE7S6E5UMA8GA1UdEwEB +/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEG +MA0GCSqGSIb3DQEBCwUAA4ICAQAFNzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAK +kEh47U6YA5n+KGCRHTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jO +XKqYGwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c77NCR807 +VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3+GbHeJAAFS6LrVE1Uweo +a2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WKvJUawSg5TB9D0pH0clmKuVb8P7Sd2nCc +dlqMQ1DujjByTd//SffGqWfZbawCEeI6FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEV +KV0jq9BgoRJP3vQXzTLlyb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gT +Dx4JnW2PAJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpDy4Q0 +8ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8dNL/+I5c30jn6PQ0G +C7TbO6Orb1wdtn7os4I07QZcJA== +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 2 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgx +MDAxMTA0MDE0WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUdAqSzm1nzHoqvNK38DcLZ +SBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiCFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/F +vudocP05l03Sx5iRUKrERLMjfTlH6VJi1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx970 +2cu+fjOlbpSD8DT6IavqjnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGV +WOHAD3bZwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/WSA2AHmgoCJrjNXy +YdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhyNsZt+U2e+iKo4YFWz827n+qrkRk4 +r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPACuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNf +vNoBYimipidx5joifsFvHZVwIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR +3p1m0IvVVGb6g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlPBSeOE6Fuwg== +-----END CERTIFICATE----- + +Atos TrustedRoot 2011 +===================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UEAwwVQXRvcyBU +cnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0xMTA3MDcxNDU4 +MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMMFUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsG +A1UECgwEQXRvczELMAkGA1UEBhMCREUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV +hTuXbyo7LjvPpvMpNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr +54rMVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+SZFhyBH+ +DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ4J7sVaE3IqKHBAUsR320 +HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0Lcp2AMBYHlT8oDv3FdU9T1nSatCQujgKR +z3bFmx5VdJx4IbHwLfELn8LVlhgf8FQieowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7R +l+lwrrw7GWzbITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZ +bNshMBgGA1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +CwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8jvZfza1zv7v1Apt+h +k6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kPDpFrdRbhIfzYJsdHt6bPWHJxfrrh +TZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pcmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a9 +61qn8FYiqTxlVMYVqL2Gns2Dlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G +3mB/ufNPRJLvKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +QuoVadis Root CA 1 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakE +PBtVwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWerNrwU8lm +PNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF34168Xfuw6cwI2H44g4hWf6 +Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh4Pw5qlPafX7PGglTvF0FBM+hSo+LdoIN +ofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXpUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/l +g6AnhF4EwfWQvTA9xO+oabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV +7qJZjqlc3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/GKubX +9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSthfbZxbGL0eUQMk1f +iyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KOTk0k+17kBL5yG6YnLUlamXrXXAkg +t3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOtzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZI +hvcNAQELBQADggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2cDMT/uFPpiN3 +GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUNqXsCHKnQO18LwIE6PWThv6ct +Tr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP ++V04ikkwj+3x6xn0dxoxGE1nVGwvb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh +3jRJjehZrJ3ydlo28hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fa +wx/kNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNjZgKAvQU6 +O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhpq1467HxpvMc7hU6eFbm0 +FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFtnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOV +hMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +QuoVadis Root CA 2 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFh +ZiFfqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMWn4rjyduY +NM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ymc5GQYaYDFCDy54ejiK2t +oIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+o +MiwMzAkd056OXbxMmO7FGmh77FOm6RQ1o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+l +V0POKa2Mq1W/xPtbAd0jIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZo +L1NesNKqIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz8eQQ +sSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43ehvNURG3YBZwjgQQvD +6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l7ZizlWNof/k19N+IxWA1ksB8aRxh +lRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALGcC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZI +hvcNAQELBQADggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RCroijQ1h5fq7K +pVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0GaW/ZZGYjeVYg3UQt4XAoeo0L9 +x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgz +dWqTHBLmYF5vHX/JHyPLhGGfHoJE+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6X +U/IyAgkwo1jwDQHVcsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+Nw +mNtddbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNgKCLjsZWD +zYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeMHVOyToV7BjjHLPj4sHKN +JeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4WSr2Rz0ZiC3oheGe7IUIarFsNMkd7Egr +O3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +QuoVadis Root CA 3 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286 +IxSR/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNuFoM7pmRL +Mon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXRU7Ox7sWTaYI+FrUoRqHe +6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+cra1AdHkrAj80//ogaX3T7mH1urPnMNA3 +I4ZyYUUpSFlob3emLoG+B01vr87ERRORFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3U +VDmrJqMz6nWB2i3ND0/kA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f7 +5li59wzweyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634RylsSqi +Md5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBpVzgeAVuNVejH38DM +dyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0QA4XN8f+MFrXBsj6IbGB/kE+V9/Yt +rQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZI +hvcNAQELBQADggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnIFUBhynLWcKzS +t/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5WvvoxXqA/4Ti2Tk08HS6IT7SdEQ +TXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFgu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9Du +DcpmvJRPpq3t/O5jrFc/ZSXPsoaP0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGib +Ih6BJpsQBJFxwAYf3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmD +hPbl8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+DhcI00iX +0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HNPlopNLk9hM6xZdRZkZFW +dSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ywaZWWDYWGWVjUTR939+J399roD1B0y2 +PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +DigiCert Assured ID Root G2 +=========================== +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgw +MTE1MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSAn61UQbVH +35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4HteccbiJVMWWXvdMX0h5i89vq +bFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9HpEgjAALAcKxHad3A2m67OeYfcgnDmCXRw +VWmvo2ifv922ebPynXApVfSr/5Vh88lAbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OP +YLfykqGxvYmJHzDNw6YuYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+Rn +lTGNAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTO +w0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPIQW5pJ6d1Ee88hjZv +0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I0jJmwYrA8y8678Dj1JGG0VDjA9tz +d29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4GnilmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAW +hsI6yLETcDbYz+70CjTVW0z9B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0M +jomZmWzwPDCvON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +DigiCert Assured ID Root G3 +=========================== +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYD +VQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJfZn4f5dwb +RXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17QRSAPWXYQ1qAk8C3eNvJs +KTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgF +UaFNN6KDec6NHSrkhDAKBggqhkjOPQQDAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5Fy +YZ5eEJJZVrmDxxDnOOlYJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy +1vUhZscv6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +DigiCert Global Root G2 +======================= +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUx +MjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJ +kTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO +3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauV +BJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyM +UNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu +5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9PMWLSn/pvtsr +F9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0U +WTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBH +QRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/ +iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +DigiCert Global Root G3 +======================= +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYD +VQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAw +MDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k +aWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FGfp4tn+6O +YwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPOZ9wj/wMco+I+o0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNp +Yim8S8YwCgYIKoZIzj0EAwMDaAAwZQIxAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y +3maTD/HMsQmP3Wyr+mt/oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34 +VOKa5Vt8sycX +-----END CERTIFICATE----- + +DigiCert Trusted Root G4 +======================== +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEw +HwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEp +pz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9o +k3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7Fsa +vOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGY +QJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6 +MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtm +mnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7 +f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFH +dL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8 +oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2SV1EY+CtnJYY +ZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd+SeuMIW59mdNOj6PWTkiU0Tr +yF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWcfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy +7zBZLq7gcfJW5GqXb5JQbZaNaHqasjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iah +ixTXTBmyUEFxPT9NcCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN +5r5N0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie4u1Ki7wb +/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mIr/OSmbaz5mEP0oUA51Aa +5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tK +G48BtieVU+i2iW1bvGjUI+iLUaJW+fCmgKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP +82Z+ +-----END CERTIFICATE----- + +COMODO RSA Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCBhTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMTE5MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR6FSS0gpWsawNJN3Fz0Rn +dJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8Xpz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZ +FGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+ +5eNu/Nio5JIk2kNrYrhV/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pG +x8cgoLEfZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z+pUX +2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7wqP/0uK3pN/u6uPQL +OvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZahSL0896+1DSJMwBGB7FY79tOi4lu3 +sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVICu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+C +GCe01a60y1Dma/RMhnEw6abfFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5 +WdYgGq/yapiqcrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvlwFTPoCWOAvn9sKIN9SCYPBMt +rFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+ +nq6PK7o9mfjYcwlYRm6mnPTXJ9OV2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSg +tZx8jb8uk2IntznaFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwW +sRqZCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiKboHGhfKp +pC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmckejkk9u+UJueBPSZI9FoJA +zMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yLS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHq +ZJx64SIDqZxubw5lT2yHh17zbqD5daWbQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk52 +7RH89elWsn2/x20Kk4yl0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7I +LaZRfyHBNVOFBkpdn627G190 +-----END CERTIFICATE----- + +USERTrust RSA Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCAEmUXNg7D2wiz +0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2j +Y0K2dvKpOyuR+OJv0OwWIJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFn +RghRy4YUVD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O ++T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT79uq +/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6c0Plfg6lZrEpfDKE +Y1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJM +lXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8 +yexDJtC/QV9AqURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+ +eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPFUp/L+M+ZBn8b2kMVn54CVVeW +FPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KOVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ +7l8wXEskEVX/JJpuXior7gtNn3/3ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQ +Eg9zKC7F4iRO/Fjs8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM +8WcRiQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYzeSf7dNXGi +FSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZXHlKYC6SQK5MNyosycdi +yA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9c +J2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRBVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGw +sAvgnEzDHNb842m1R0aBL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gx +Q+6IHdfGjjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +USERTrust ECC Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqfloI+d61SRvU8Za2EurxtW2 +0eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinngo4N+LZfQYcTxmdwlkWOrfzCjtHDix6Ez +nPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNV +HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBB +HU6+4WMBzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbWRNZu +9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R4 +=========================== +-----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprl +OQcJFspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAwDgYDVR0P +AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61FuOJAf/sKbvu+M8k8o4TV +MAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGXkPoUVy0D7O48027KqGx2vKLeuwIgJ6iF +JzWbVsaj8kfSt24bAgAXqmemFZHe+pTsewv4n4Q= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R5 +=========================== +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6 +SFkc8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8kehOvRnkmS +h5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYIKoZIzj0EAwMDaAAwZQIxAOVpEslu28Yx +uglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7 +yFz9SO8NdCKoCOJuxUnOxwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +Staat der Nederlanden Root CA - G3 +================================== +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJOTDEeMBwGA1UE +CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFhdCBkZXIgTmVkZXJsYW5kZW4g +Um9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloXDTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMC +TkwxHjAcBgNVBAoMFVN0YWF0IGRlciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5l +ZGVybGFuZGVuIFJvb3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4y +olQPcPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WWIkYFsO2t +x1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqXxz8ecAgwoNzFs21v0IJy +EavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFyKJLZWyNtZrVtB0LrpjPOktvA9mxjeM3K +Tj215VKb8b475lRgsGYeCasH/lSJEULR9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUur +mkVLoR9BvUhTFXFkC4az5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU5 +1nus6+N86U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7Ngzp +07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHPbMk7ccHViLVlvMDo +FxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXtBznaqB16nzaeErAMZRKQFWDZJkBE +41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTtXUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleu +yjWcLhL75LpdINyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD +U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwpLiniyMMB8jPq +KqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8Ipf3YF3qKS9Ysr1YvY2WTxB1 +v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixpgZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA +8KCWAg8zxXHzniN9lLf9OtMJgwYh/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b +8KKaa8MFSu1BYBQw0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0r +mj1AfsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq4BZ+Extq +1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR1VmiiXTTn74eS9fGbbeI +JG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/QFH1T/U67cjF68IeHRaVesd+QnGTbksV +tzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM94B7IWcnMFk= +-----END CERTIFICATE----- + +Staat der Nederlanden EV Root CA +================================ +-----BEGIN CERTIFICATE----- +MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJOTDEeMBwGA1UE +CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFhdCBkZXIgTmVkZXJsYW5kZW4g +RVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0yMjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5M +MR4wHAYDVQQKDBVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRl +cmxhbmRlbiBFViBSb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkk +SzrSM4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nCUiY4iKTW +O0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3dZ//BYY1jTw+bbRcwJu+r +0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46prfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8 +Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13lpJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gV +XJrm0w912fxBmJc+qiXbj5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr +08C+eKxCKFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS/ZbV +0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0XcgOPvZuM5l5Tnrmd +74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH1vI4gnPah1vlPNOePqc7nvQDs/nx +fRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrPpx9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwa +ivsnuL8wbqg7MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI +eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u2dfOWBfoqSmu +c0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHSv4ilf0X8rLiltTMMgsT7B/Zq +5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTCwPTxGfARKbalGAKb12NMcIxHowNDXLldRqAN +b/9Zjr7dn3LDWyvfjFvO5QxGbJKyCqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tN +f1zuacpzEPuKqf2evTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi +5Dp6Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIaGl6I6lD4 +WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeLeG9QgkRQP2YGiqtDhFZK +DyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGy +eUN51q1veieQA6TqJIc/2b3Z6fJfUEkc7uzXLg== +-----END CERTIFICATE----- + +IdenTrust Commercial Root CA 1 +============================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBS +b290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQwMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzES +MBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENB +IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ld +hNlT3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU+ehcCuz/ +mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gpS0l4PJNgiCL8mdo2yMKi +1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1bVoE/c40yiTcdCMbXTMTEl3EASX2MN0C +XZ/g1Ue9tOsbobtJSdifWwLziuQkkORiT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl +3ZBWzvurpWCdxJ35UrCLvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzy +NeVJSQjKVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZKdHzV +WYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHTc+XvvqDtMwt0viAg +xGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hvl7yTmvmcEpB4eoCHFddydJxVdHix +uuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5NiGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZI +hvcNAQELBQADggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwtLRvM7Kqas6pg +ghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93nAbowacYXVKV7cndJZ5t+qnt +ozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmV +YjzlVYA211QC//G5Xc7UI2/YRYRKW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUX +feu+h1sXIFRRk0pTAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/ro +kTLql1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG4iZZRHUe +2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZmUlO+KWA2yUPHGNiiskz +Z2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7R +cGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +IdenTrust Public Sector Root CA 1 +================================= +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3Rv +ciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcNMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJV +UzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBS +b290IENBIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTy +P4o7ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGyRBb06tD6 +Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlSbdsHyo+1W/CD80/HLaXI +rcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF/YTLNiCBWS2ab21ISGHKTN9T0a9SvESf +qy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoS +mJxZZoY+rfGwyj4GD3vwEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFn +ol57plzy9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9VGxyh +LrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ2fjXctscvG29ZV/v +iDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsVWaFHVCkugyhfHMKiq3IXAAaOReyL +4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gDW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8B +Af8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMw +DQYJKoZIhvcNAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHVDRDtfULAj+7A +mgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9TaDKQGXSc3z1i9kKlT/YPyNt +GtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8GlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFt +m6/n6J91eEyrRjuazr8FGF1NFTwWmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMx +NRF4eKLg6TCMf4DfWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4 +Mhn5+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJtshquDDI +ajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhAGaQdp/lLQzfcaFpPz+vC +ZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ +3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +Entrust Root Certification Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVy +bXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ug +b25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIw +HhcNMDkwNzA3MTcyNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoT +DUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMx +OTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP +/vaCeb9zYQYKpSfYs1/TRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXz +HHfV1IWNcCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hWwcKU +s/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1U1+cPvQXLOZprE4y +TGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0jaWvYkxN4FisZDQSA/i2jZRjJKRx +AgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ6 +0B7vfec7aVHUbI2fkBJmqzANBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5Z +iXMRrEPR9RP/jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v1fN2D807iDgi +nWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4RnAuknZoh8/CbCzB428Hch0P+ +vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmHVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xO +e4pIb4tF9g== +-----END CERTIFICATE----- + +Entrust Root Certification Authority - EC1 +========================================== +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkGA1UEBhMCVVMx +FjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVn +YWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEzMDEGA1UEAxMqRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRUMxMB4XDTEyMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYw +FAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2Fs +LXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQg +dXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt +IEVDMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHy +AsWfoPZb1YsGGYZPUxBtByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef +9eNi1KlHBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVCR98crlOZF7ZvHH3h +vxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nXhTcGtXsI/esni0qU+eH6p44mCOh8 +kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +CFCA EV ROOT +============ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4GA1UE +CgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNB +IEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkxMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEw +MC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQD +DAxDRkNBIEVWIFJPT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnV +BU03sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpLTIpTUnrD +7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5/ZOkVIBMUtRSqy5J35DN +uF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp7hZZLDRJGqgG16iI0gNyejLi6mhNbiyW +ZXvKWfry4t3uMCz7zEasxGPrb382KzRzEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7 +xzbh72fROdOXW3NiGUgthxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9f +py25IGvPa931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqotaK8K +gWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNgTnYGmE69g60dWIol +hdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfVPKPtl8MeNPo4+QgO48BdK4PRVmrJ +tqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hvcWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAf +BgNVHSMEGDAWgBTj/i39KNALtbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObTej/tUxPQ4i9q +ecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdLjOztUmCypAbqTuv0axn96/Ua +4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBSESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sG +E5uPhnEFtC+NiWYzKXZUmhH4J/qyP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfX +BDrDMlI1Dlb4pd19xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjn +aH9dCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN5mydLIhy +PDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe/v5WOaHIz16eGWRGENoX +kbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+ZAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3C +ekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GB CA +=============================== +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQG +EwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAw +MzJaFw0zOTEyMDExNTEwMzFaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEds +b2JhbCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3HEokKtaX +scriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGxWuR51jIjK+FTzJlFXHtP +rby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk +9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNku7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4o +Qnc/nSMbsrY9gBQHTC5P99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvg +GUpuuy9rM2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZI +hvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrghcViXfa43FK8+5/ea4n32cZiZBKpD +dHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0 +VQreUGdNZtGn//3ZwLWoo4rOZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEui +HZeeevJuQHHfaPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +SZAFIR ROOT CA2 +=============== +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQELBQAwUTELMAkG +A1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6ZW5pb3dhIFMuQS4xGDAWBgNV +BAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkwNzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJ +BgNVBAYTAlBMMSgwJgYDVQQKDB9LcmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYD +VQQDDA9TWkFGSVIgUk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5Q +qEvNQLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT3PSQ1hNK +DJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw3gAeqDRHu5rr/gsUvTaE +2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr63fE9biCloBK0TXC5ztdyO4mTp4CEHCdJ +ckm1/zuVnsHMyAHs6A6KCpbns6aH5db5BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwi +ieDhZNRnvDF5YTy7ykHNXGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsFAAOC +AQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw8PRBEew/R40/cof5 +O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOGnXkZ7/e7DDWQw4rtTw/1zBLZpD67 +oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCPoky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul +4+vJhaAlIDf7js4MNIThPIGyd05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6 ++/NNIxuZMzSgLvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +Certum Trusted Network CA 2 +=========================== +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCBgDELMAkGA1UE +BhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1 +bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29y +ayBDQSAyMCIYDzIwMTExMDA2MDgzOTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQ +TDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENB +IDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWADGSdhhuWZGc/IjoedQF9 +7/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+o +CgCXhVqqndwpyeI1B+twTUrWwbNWuKFBOJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40b +Rr5HMNUuctHFY9rnY3lEfktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2p +uTRZCr+ESv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1mo130 +GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02isx7QBlrd9pPPV3WZ +9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOWOZV7bIBaTxNyxtd9KXpEulKkKtVB +Rgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgezTv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pye +hizKV/Ma5ciSixqClnrDvFASadgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vM +BhBgu4M1t15n3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZI +hvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQF/xlhMcQSZDe28cmk4gmb3DW +Al45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTfCVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuA +L55MYIR4PSFk1vtBHxgP58l1cb29XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMo +clm2q8KMZiYcdywmdjWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tM +pkT/WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jbAoJnwTnb +w3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksqP/ujmv5zMnHCnsZy4Ypo +J/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Kob7a6bINDd82Kkhehnlt4Fj1F4jNy3eFm +ypnTycUm/Q1oBEauttmbjL4ZvrHG8hnjXALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLX +is7VmFxWlgPF7ncGNf/P5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7 +zAYspsbiDrW5viSP +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions RootCA 2015 +======================================================= +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcT +BkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0 +aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAx +MTIxWjCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMg +QWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNV +BAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIw +MTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDC+Kk/G4n8PDwEXT2QNrCROnk8Zlrv +bTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+eh +iGsxr/CL0BgzuNtFajT0AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+ +6PAQZe104S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06CojXd +FPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV9Cz82XBST3i4vTwr +i5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrDgfgXy5I2XdGj2HUb4Ysn6npIQf1F +GQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2 +fu/Z8VFRfS0myGlZYeCsargqNhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9mu +iNX6hME6wGkoLfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVdctA4GGqd83EkVAswDQYJKoZI +hvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0IXtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+ +D1hYc2Ryx+hFjtyp8iY/xnmMsVMIM4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrM +d/K4kPFox/la/vot9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+y +d+2VZ5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/eaj8GsGsVn +82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnhX9izjFk0WaSrT2y7Hxjb +davYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQl033DlZdwJVqwjbDG2jJ9SrcR5q+ss7F +Jej6A7na+RZukYT1HCjI/CbM1xyQVqdfbzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVt +J94Cj8rDtSvK6evIIVM4pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGa +JI7ZjnHKe7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0vm9q +p/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions ECC RootCA 2015 +=========================================================== +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0 +aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u +cyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgRUNDIFJvb3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEw +MzcxMlowgaoxCzAJBgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmlj +IEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUQwQgYD +VQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIEVDQyBSb290 +Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKgQehLgoRc4vgxEZmGZE4JJS+dQS8KrjVP +dJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJajq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoK +Vlp8aQuqgAkkbH7BRqNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFLQiC4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaeplSTA +GiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7SofTUwJCA3sS61kFyjn +dc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +ISRG Root X1 +============ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UE +BhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQD +EwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQG +EwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMT +DElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54r +Vygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj1 +3Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8K +b4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCN +Aymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ +4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf +1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFu +hjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQH +usEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/r +OPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4G +A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY +9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV +0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwt +hDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJw +TdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nx +e5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZA +JzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahD +YVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9n +JEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJ +m+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +AC RAIZ FNMT-RCM +================ +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsxCzAJBgNVBAYT +AkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTAeFw0wODEw +MjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJD +TTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBALpxgHpMhm5/yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcf +qQgfBBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAzWHFctPVr +btQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxFtBDXaEAUwED653cXeuYL +j2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z374jNUUeAlz+taibmSXaXvMiwzn15Cou +08YfxGyqxRxqAQVKL9LFwag0Jl1mpdICIfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mw +WsXmo8RZZUc1g16p6DULmbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnT +tOmlcYF7wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peSMKGJ +47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2ZSysV4999AeU14EC +ll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMetUqIJ5G+GR4of6ygnXYMgrwTJbFaa +i0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FPd9xf3E6Jobd2Sn9R2gzL+HYJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1o +dHRwOi8vd3d3LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1RXxlDPiyN8+s +D8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYMLVN0V2Ue1bLdI4E7pWYjJ2cJ +j+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrT +Qfv6MooqtyuGC2mDOL7Nii4LcK2NJpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW ++YJF1DngoABd15jmfZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7 +Ixjp6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp1txyM/1d +8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B9kiABdcPUXmsEKvU7ANm +5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wokRqEIr9baRRmW1FMdW4R58MD3R++Lj8UG +rp1MYp3/RgT408m2ECVAdf4WqslKYIYvuu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +Amazon Root CA 1 +================ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAxMB4XDTE1 +MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALJ4gHHKeNXjca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgH +FzZM9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qwIFAGbHrQ +gLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6VOujw5H5SNz/0egwLX0t +dHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L93FcXmn/6pUCyziKrlA4b9v7LWIbxcce +VOF34GfID5yHI9Y/QCB/IIDEgEw+OyQmjgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3 +DQEBCwUAA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDIU5PM +CCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUsN+gDS63pYaACbvXy +8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vvo/ufQJVtMVT8QtPHRh8jrdkPSHCa +2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2 +xJNDd2ZhwLnoQdeXeGADbkpyrqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +Amazon Root CA 2 +================ +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAyMB4XDTE1 +MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBAK2Wny2cSkxKgXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4 +kHbZW0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg1dKmSYXp +N+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K8nu+NQWpEjTj82R0Yiw9 +AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvd +fLC6HM783k81ds8P+HgfajZRRidhW+mez/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAEx +kv8LV/SasrlX6avvDXbR8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSS +btqDT6ZjmUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz7Mt0 +Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6+XUyo05f7O0oYtlN +c/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI0u1ufm8/0i2BWSlmy5A5lREedCf+ +3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSw +DPBMMPQFWAJI/TPlUq9LhONmUjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oA +A7CXDpO8Wqj2LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kSk5Nrp+gvU5LE +YFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl7uxMMne0nxrpS10gxdr9HIcW +xkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygmbtmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQ +gj9sAq+uEjonljYE1x2igGOpm/HlurR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbW +aQbLU8uz/mtBzUF+fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoV +Yh63n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE76KlXIx3 +KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H9jVlpNMKVv/1F2Rs76gi +JUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT4PsJYGw= +-----END CERTIFICATE----- + +Amazon Root CA 3 +================ +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAzMB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZB +f8ANm+gBG1bG8lKlui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjr +Zt6jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSrttvXBp43 +rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkrBqWTrBqYaGFy+uGh0Psc +eGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteMYyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +Amazon Root CA 4 +================ +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSA0MB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN +/sGKe0uoe0ZLY7Bi9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri +83BkM6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WBMAoGCCqGSM49BAMDA2gA +MGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlwCkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1 +AE47xDqUEpHJWEadIRNyp4iciuRMStuW1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 +============================================= +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIxGDAWBgNVBAcT +D0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxpbXNlbCB2ZSBUZWtub2xvamlr +IEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0wKwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24g +TWVya2V6aSAtIEthbXUgU00xNjA0BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRp +ZmlrYXNpIC0gU3VydW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYD +VQQGEwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXllIEJpbGlt +c2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklUQUsxLTArBgNVBAsTJEth +bXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBTTTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11 +IFNNIFNTTCBLb2sgU2VydGlmaWthc2kgLSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAr3UwM6q7a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y8 +6Ij5iySrLqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INrN3wc +wv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2XYacQuFWQfw4tJzh0 +3+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/iSIzL+aFCr2lqBs23tPcLG07xxO9 +WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4fAJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQU +ZT/HiobGPN08VFw1+DrtUgxHV8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJ +KoZIhvcNAQELBQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPfIPP54+M638yc +lNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4lzwDGrpDxpa5RXI4s6ehlj2R +e37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0j +q5Rm+K37DwhuJi1/FwcJsoz7UMCflo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +GDCA TrustAUTH R5 ROOT +====================== +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCQ04xMjAw +BgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8wHQYDVQQD +DBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVow +YjELMAkGA1UEBhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJjDp6L3TQs +AlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBjTnnEt1u9ol2x8kECK62p +OqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+uKU49tm7srsHwJ5uu4/Ts765/94Y9cnrr +pftZTqfrlYwiOXnhLQiPzLyRuEH3FMEjqcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ +9Cy5WmYqsBebnh52nUpmMUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQ +xXABZG12ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloPzgsM +R6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3GkL30SgLdTMEZeS1SZ +D2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeCjGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4 +oR24qoAATILnsn8JuLwwoC8N9VKejveSswoAHQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx +9hoh49pwBiFYFIeFd3mqgnkCAwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlR +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZmDRd9FBUb1Ov9 +H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5COmSdI31R9KrO9b7eGZONn35 +6ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ryL3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd ++PwyvzeG5LuOmCd+uh8W4XAR8gPfJWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQ +HtZa37dG/OaG+svgIHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBD +F8Io2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV09tL7ECQ +8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQXR4EzzffHqhmsYzmIGrv +/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrqT8p+ck0LcIymSLumoRT2+1hEmRSuqguT +aaApJUqlyyvdimYHFngVV3Eb7PVHhPOeMTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +TrustCor RootCert CA-1 +====================== +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYDVQQGEwJQQTEP +MA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3Ig +U3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkxHzAdBgNVBAMMFlRydXN0Q29yIFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkx +MjMxMTcyMzE2WjCBpDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFu +YW1hIENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUGA1UECwwe +VHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZUcnVzdENvciBSb290Q2Vy +dCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv463leLCJhJrMxnHQFgKq1mq +jQCj/IDHUHuO1CAmujIS2CNUSSUQIpidRtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4 +pQa81QBeCQryJ3pS/C3Vseq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0 +JEsq1pme9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CVEY4h +gLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorWhnAbJN7+KIor0Gqw +/Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/DeOxCbeKyKsZn3MzUOcwHwYDVR0j +BBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwDQYJKoZIhvcNAQELBQADggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5 +mDo4Nvu7Zp5I/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf +ke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZyonnMlo2HD6C +qFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djtsL1Ac59v2Z3kf9YKVmgenFK+P +3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdNzl/HHk484IkzlQsPpTLWPFp5LBk= +-----END CERTIFICATE----- + +TrustCor RootCert CA-2 +====================== +-----BEGIN CERTIFICATE----- +MIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNVBAYTAlBBMQ8w +DQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQwIgYDVQQKDBtUcnVzdENvciBT +eXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRydXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0 +eTEfMB0GA1UEAwwWVHJ1c3RDb3IgUm9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEy +MzExNzI2MzlaMIGkMQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5h +bWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U +cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29yIFJvb3RDZXJ0 +IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnIG7CKqJiJJWQdsg4foDSq8Gb +ZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+QVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9Nk +RvRUqdw6VC0xK5mC8tkq1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1 +oYxOdqHp2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nKDOOb +XUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hapeaz6LMvYHL1cEksr1 +/p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF3wP+TfSvPd9cW436cOGlfifHhi5q +jxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88oWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQP +eSghYA2FFn3XVDjxklb9tTNMg9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+Ctg +rKAmrhQhJ8Z3mjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh +8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAdBgNVHQ4EFgQU +2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6UnrybPZx9mCAZ5YwwYrIwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/h +Osh80QA9z+LqBrWyOrsGS2h60COXdKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnp +kpfbsEZC89NiqpX+MWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv +2wnL/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RXCI/hOWB3 +S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYaZH9bDTMJBzN7Bj8RpFxw +PIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dv +DDqPys/cA8GiCcjl/YBeyGBCARsaU1q7N6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYU +RpFHmygk71dSTlxCnKr3Sewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANE +xdqtvArBAs8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp5KeX +RKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu1uwJ +-----END CERTIFICATE----- + +TrustCor ECA-1 +============== +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYDVQQGEwJQQTEP +MA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3Ig +U3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkxFzAVBgNVBAMMDlRydXN0Q29yIEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3Mjgw +N1owgZwxCzAJBgNVBAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5 +MSQwIgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRydXN0Q29y +IENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3IgRUNBLTEwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb3w9U73NjKYKtR8aja+3+XzP4Q1HpGjOR +MRegdMTUpwHmspI+ap3tDvl0mEDTPwOABoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23 +xFUfJ3zSCNV2HykVh0A53ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmc +p0yJF4OuowReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/wZ0+ +fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZFZtS6mFjBAgMBAAGj +YzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAfBgNVHSMEGDAWgBREnkj1zG1I1KBL +f/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF +AAOCAQEABT41XBVwm8nHc2FvcivUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u +/ukZMjgDfxT2AHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F +hcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50soIipX1TH0Xs +J5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BIWJZpTdwHjFGTot+fDz2LYLSC +jaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1WitJ/X5g== +-----END CERTIFICATE----- + +SSL.com Root Certification Authority RSA +======================================== +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxDjAM +BgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24x +MTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYw +MjEyMTczOTM5WhcNNDEwMjEyMTczOTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx +EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NM +LmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2RxFdHaxh3a3by/ZPkPQ/C +Fp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aXqhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8 +P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcCC52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/ge +oeOy3ZExqysdBP+lSgQ36YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkp +k8zruFvh/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrFYD3Z +fBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93EJNyAKoFBbZQ+yODJ +gUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVcUS4cK38acijnALXRdMbX5J+tB5O2 +UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi8 +1xtZPCvM8hnIk2snYxnP/Okm+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4s +bE6x/c+cCbqiM+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGVcpNxJK1ok1iOMq8bs3AD/CUr +dIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBcHadm47GUBwwyOabqG7B52B2ccETjit3E+ZUf +ijhDPwGFpUenPUayvOUiaPd7nNgsPgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAsl +u1OJD7OAUN5F7kR/q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjq +erQ0cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jra6x+3uxj +MxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90IH37hVZkLId6Tngr75qNJ +vTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/YK9f1JmzJBjSWFupwWRoyeXkLtoh/D1JI +Pb9s2KJELtFOt3JY04kTlf5Eq/jXixtunLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406y +wKBjYZC6VWg3dGq2ktufoYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NI +WuuA8ShYIc2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +SSL.com Root Certification Authority ECC +======================================== +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xMTAv +BgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEy +MTgxNDAzWhcNNDEwMjEyMTgxNDAzWjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO +BgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuBBAAiA2IA +BEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI7Z4INcgn64mMU1jrYor+ +8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPgCemB+vNH06NjMGEwHQYDVR0OBBYEFILR +hXMw5zUE044CkvvlpNHEIejNMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTT +jgKS++Wk0cQh6M0wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCW +e+0F+S8Tkdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+gA0z +5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +SSL.com EV Root Certification Authority RSA R2 +============================================== +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAlVTMQ4w +DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9u +MTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MB4XDTE3MDUzMTE4MTQzN1oXDTQyMDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI +DAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYD +VQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvqM0fNTPl9fb69LT3w23jh +hqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssufOePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7w +cXHswxzpY6IXFJ3vG2fThVUCAtZJycxa4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTO +Zw+oz12WGQvE43LrrdF9HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+ +B6KjBSYRaZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcAb9Zh +CBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQGp8hLH94t2S42Oim +9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQVPWKchjgGAGYS5Fl2WlPAApiiECto +RHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMOpgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+Slm +JuwgUHfbSguPvuUCYHBBXtSuUDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48 ++qvWBkofZ6aYMBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa49QaAJadz20Zp +qJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBWs47LCp1Jjr+kxJG7ZhcFUZh1 +++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nx +Y/hoLVUE0fKNsKTPvDxeH3jnpaAgcLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2G +guDKBAdRUNf/ktUM79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDz +OFSz/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXtll9ldDz7 +CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEmKf7GUmG6sXP/wwyc5Wxq +lD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKKQbNmC1r7fSOl8hqw/96bg5Qu0T/fkreR +rwU7ZcegbLHNYhLDkBvjJc40vG93drEQw/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1 +hlMYegouCRw2n5H9gooiS9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX +9hwJ1C07mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +SSL.com EV Root Certification Authority ECC +=========================================== +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xNDAy +BgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYw +MjEyMTgxNTIzWhcNNDEwMjEyMTgxNTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx +EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NM +LmNvbSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMAVIbc/R/fALhBYlzccBYy +3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1KthkuWnBaBu2+8KGwytAJKaNjMGEwHQYDVR0O +BBYEFFvKXuXe0oGqzagtZFG22XKbl+ZPMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe +5d7SgarNqC1kUbbZcpuX5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJ +N+vp1RPZytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZgh5Mm +m7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +GlobalSign Root CA - R6 +======================= +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEgMB4GA1UECxMX +R2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds +b2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQxMjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9i +YWxTaWduIFJvb3QgQ0EgLSBSNjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFs +U2lnbjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQss +grRIxutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1kZguSgMpE +3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxDaNc9PIrFsmbVkJq3MQbF +vuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJwLnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqM +PKq0pPbzlUoSB239jLKJz9CgYXfIWHSw1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+ +azayOeSsJDa38O+2HBNXk7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05O +WgtH8wY2SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/hbguy +CLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4nWUx2OVvq+aWh2IMP +0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpYrZxCRXluDocZXFSxZba/jJvcE+kN +b7gu3GduyYsRtYQUigAZcIN5kZeR1BonvzceMgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQE +AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNV +HSMEGDAWgBSubAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGtIxg93eFyRJa0 +lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr6155wsTLxDKZmOMNOsIeDjHfrY +BzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLjvUYAGm0CuiVdjaExUd1URhxN25mW7xocBFym +Fe944Hn+Xds+qkxV/ZoVqW/hpvvfcDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr +3TsTjxKM4kEaSHpzoHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB1 +0jZpnOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfspA9MRf/T +uTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+vJJUEeKgDu+6B5dpffItK +oZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+t +JDfLRVpOoERIyNiwmcUVhAn21klJwGW45hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GC CA +=============================== +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQswCQYDVQQGEwJD +SDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEo +MCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRa +Fw00MjA1MDkwOTU4MzNaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQL +ExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4nieUqjFqdr +VCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4Wp2OQ0jnUsYd4XxiWD1Ab +NTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7TrYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0E +AwMDaAAwZQIwJsdpW9zV57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtk +AjEA2zQgMgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +GTS Root R1 +=========== +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQG +EwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJv +b3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAG +A1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx +9vaMf/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7wCl7r +aKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjwTcLCeoiKu7rPWRnW +r4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0PfyblqAj+lug8aJRT7oM6iCsVlgmy4HqM +LnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly +4cpk9+aCEI3oncKKiPo4Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr +06zqkUspzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70paDPvOmbsB4om +3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrNVjzRlwW5y0vtOUucxD/SVRNu +JLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEM +BQADggIBADiWCu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1 +d5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6ZXPYfcX3v73sv +fuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZRgyFmxhE+885H7pwoHyXa/6xm +ld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3d8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9b +gsiG1eGZbYwE8na6SfZu6W0eX6DvJ4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq +4BjFbkerQUIpm/ZgDdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWEr +tXvM+SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyyF62ARPBo +pY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9SQ98POyDGCBDTtWTurQ0 +sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdwsE3PYJ/HQcu51OyLemGhmW/HGY0dVHLql +CFF1pkgl +-----END CERTIFICATE----- + +GTS Root R2 +=========== +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQbkepxlqz5yDFMJo/aFLybzANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQG +EwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJv +b3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAG +A1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTuk +k3LvCvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY6Dlo +7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAuMC6C/Pq8tBcKSOWI +m8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7kRXuJVfeKH2JShBKzwkCX44ofR5Gm +dFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbu +ak7MkogwTZq9TwtImoS1mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscsz +cTJGr61K8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW +Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKaG73Vululycsl +aVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCqgc7dGtxRcw1PcOnlthYhGXmy +5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEM +BQADggIBALZp8KZ3/p7uC4Gt4cCpx/k1HUCCq+YEtN/L9x0Pg/B+E02NjO7jMyLDOfxA325BS0JT +vhaI8dI4XsRomRyYUpOM52jtG2pzegVATX9lO9ZY8c6DR2Dj/5epnGB3GFW1fgiTz9D2PGcDFWEJ ++YF59exTpJ/JjwGLc8R3dtyDovUMSRqodt6Sm2T4syzFJ9MHwAiApJiS4wGWAqoC7o87xdFtCjMw +c3i5T1QWvwsHoaRc5svJXISPD+AVdyx+Jn7axEvbpxZ3B7DNdehyQtaVhJ2Gg/LkkM0JR9SLA3Da +WsYDQvTtN6LwG1BUSw7YhN4ZKJmBR64JGz9I0cNv4rBgF/XuIwKl2gBbbZCr7qLpGzvpx0QnRY5r +n/WkhLx3+WuXrD5RRaIRpsyF7gpo8j5QOHokYh4XIDdtak23CZvJ/KRY9bb7nE4Yu5UC56Gtmwfu +Nmsk0jmGwZODUNKBRqhfYlcsu2xkiAhu7xNUX90txGdj08+JN7+dIPT7eoOboB6BAFDC5AwiWVIQ +7UNWhwD4FFKnHYuTjKJNRn8nxnGbJN7k2oaLDX5rIMHAnuFl2GqjpuiFizoHCBy69Y9Vmhh1fuXs +gWbRIXOhNUQLgD1bnF5vKheW0YMjiGZt5obicDIvUiLnyOd/xCxgXS/Dr55FBcOEArf9LAhST4Ld +o/DUhgkC +-----END CERTIFICATE----- + +GTS Root R3 +=========== +-----BEGIN CERTIFICATE----- +MIICDDCCAZGgAwIBAgIQbkepx2ypcyRAiQ8DVd2NHTAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJV +UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg +UjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE +ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUU +Rout736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL24Cej +QjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTB8Sa6oC2uhYHP +0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEAgFukfCPAlaUs3L6JbyO5o91lAFJekazInXJ0 +glMLfalAvWhgxeG4VDvBNhcl2MG9AjEAnjWSdIUlUfUk7GRSJFClH9voy8l27OyCbvWFGFPouOOa +KaqW04MjyaR7YbPMAuhd +-----END CERTIFICATE----- + +GTS Root R4 +=========== +-----BEGIN CERTIFICATE----- +MIICCjCCAZGgAwIBAgIQbkepyIuUtui7OyrYorLBmTAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJV +UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg +UjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE +ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa +6zzuhXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvRHYqj +QjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSATNbrdP9JNqPV +2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBkAjBqUFJ0CMRw3J5QdCHojXohw0+WbhXRIjVhLfoI +N+4Zba3bssx9BzT1YBkstTTZbyACMANxsbqjYAuG7ZoIapVon+Kz4ZNkfF6Tpt95LY2F45TPI11x +zPKwTdb+mciUqXWi4w== +-----END CERTIFICATE----- + +UCA Global G2 Root +================== +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQG +EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBHbG9iYWwgRzIgUm9vdDAeFw0x +NjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0xCzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlU +cnVzdDEbMBkGA1UEAwwSVUNBIEdsb2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxeYrb3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmT +oni9kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzmVHqUwCoV +8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/RVogvGjqNO7uCEeBHANBS +h6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDcC/Vkw85DvG1xudLeJ1uK6NjGruFZfc8o +LTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIjtm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/ +R+zvWr9LesGtOxdQXGLYD0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBe +KW4bHAyvj5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6DlNaBa +4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6iIis7nCs+dwp4wwc +OxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznPO6Q0ibd5Ei9Hxeepl2n8pndntd97 +8XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFIHEjMz15DD/pQwIX4wVZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo +5sOASD0Ee/ojL3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl1qnN3e92mI0A +Ds0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oUb3n09tDh05S60FdRvScFDcH9 +yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LVPtateJLbXDzz2K36uGt/xDYotgIVilQsnLAX +c47QN6MUPJiVAAwpBVueSUmxX8fjy88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHo +jhJi6IjMtX9Gl8CbEGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZk +bxqgDMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI+Vg7RE+x +ygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGyYiGqhkCyLmTTX8jjfhFn +RR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bXUB+K+wb1whnw0A== +-----END CERTIFICATE----- + +UCA Extended Validation Root +============================ +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQG +EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9u +IFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMxMDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8G +A1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrs +iWogD4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvSsPGP2KxF +Rv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aopO2z6+I9tTcg1367r3CTu +eUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dksHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR +59mzLC52LqGj3n5qiAno8geK+LLNEOfic0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH +0mK1lTnj8/FtDw5lhIpjVMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KR +el7sFsLzKuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/TuDv +B0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41Gsx2VYVdWf6/wFlth +WG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs1+lvK9JKBZP8nm9rZ/+I8U6laUpS +NwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQDfwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS +3H5aBZ8eNJr34RQwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQEL +BQADggIBADaNl8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQVBcZEhrxH9cM +aVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5c6sq1WnIeJEmMX3ixzDx/BR4 +dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb ++7lsq+KePRXBOy5nAliRn+/4Qh8st2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOW +F3sGPjLtx7dCvHaj2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwi +GpWOvpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2CxR9GUeOc +GMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmxcmtpzyKEC2IPrNkZAJSi +djzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbMfjKaiJUINlK73nZfdklJrX+9ZSCyycEr +dhh2n1ax +-----END CERTIFICATE----- + +Certigna Root CA +================ +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAwWjELMAkGA1UE +BhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAwMiA0ODE0NjMwODEwMDAzNjEZ +MBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0xMzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjda +MFoxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYz +MDgxMDAwMzYxGTAXBgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sOty3tRQgX +stmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9MCiBtnyN6tMbaLOQdLNyz +KNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPuI9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8 +JXrJhFwLrN1CTivngqIkicuQstDuI7pmTLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16 +XdG+RCYyKfHx9WzMfgIhC59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq +4NYKpkDfePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3YzIoej +wpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWTCo/1VTp2lc5ZmIoJ +lXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1kJWumIWmbat10TWuXekG9qxf5kBdI +jzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp/ +/TBt2dzhauH8XwIDAQABo4IBGjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczovL3d3d3cuY2Vy +dGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilodHRwOi8vY3JsLmNlcnRpZ25h +LmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYraHR0cDovL2NybC5kaGlteW90aXMuY29tL2Nl +cnRpZ25hcm9vdGNhLmNybDANBgkqhkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOIt +OoldaDgvUSILSo3L6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxP +TGRGHVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH60BGM+RFq +7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncBlA2c5uk5jR+mUYyZDDl3 +4bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdio2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd +8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS +6Cvu5zHbugRqh5jnxV/vfaci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaY +tlu3zM63Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayhjWZS +aX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw3kAP+HwV96LOPNde +E4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +emSign Root CA - G1 +=================== +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJJTjET +MBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl +ZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBHMTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgx +ODMwMDBaMGcxCzAJBgNVBAYTAklOMRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVk +aHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQzf2N4aLTN +LnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO8oG0x5ZOrRkVUkr+PHB1 +cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aqd7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHW +DV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhMtTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ +6DqS0hdW5TUaQBw+jSztOd9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrH +hQIDAQABo0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQDAgEG +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31xPaOfG1vR2vjTnGs2 +vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjMwiI/aTvFthUvozXGaCocV685743Q +NcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6dGNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q ++Mri/Tm3R7nrft8EI6/6nAYH6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeih +U80Bv2noWgbyRQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +emSign ECC Root CA - G3 +======================= +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQGEwJJTjETMBEG +A1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg +MB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4 +MTgzMDAwWjBrMQswCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11 +ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0WXTsuwYc +58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xySfvalY8L1X44uT6EYGQIr +MgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuBzhccLikenEhjQjAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+D +CBeQyh+KTOgNG3qxrdWBCUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7 +jHvrZQnD+JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +emSign Root CA - C1 +=================== +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkGA1UEBhMCVVMx +EzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNp +Z24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UE +BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQD +ExNlbVNpZ24gUm9vdCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+up +ufGZBczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZHdPIWoU/ +Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH3DspVpNqs8FqOp099cGX +OFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvHGPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4V +I5b2P/AgNBbeCsbEBEV5f6f9vtKppa+cxSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleooms +lMuoaJuvimUnzYnu3Yy1aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+ +XJGFehiqTbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD +ggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87/kOXSTKZEhVb3xEp +/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4kqNPEjE2NuLe/gDEo2APJ62gsIq1 +NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrGYQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9 +wC68AivTxEDkigcxHpvOJpkT+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQ +BmIMMMAVSKeoWXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +emSign ECC Root CA - C3 +======================= +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQGEwJVUzETMBEG +A1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMxIDAeBgNVBAMTF2VtU2lnbiBF +Q0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UE +BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQD +ExdlbVNpZ24gRUNDIFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd +6bciMK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4OjavtisIGJAnB9 +SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0OBBYEFPtaSNCAIEDyqOkA +B2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gA +MGUCMQC02C8Cif22TGK6Q04ThHK1rt0c3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwU +ZOR8loMRnLDRWmFLpg9J0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +Hongkong Post Root CA 3 +======================= +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQELBQAwbzELMAkG +A1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJSG9uZyBLb25nMRYwFAYDVQQK +Ew1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25na29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2 +MDMwMjI5NDZaFw00MjA2MDMwMjI5NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtv +bmcxEjAQBgNVBAcTCUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMX +SG9uZ2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz +iNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFOdem1p+/l6TWZ5Mwc50tf +jTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mIVoBc+L0sPOFMV4i707mV78vH9toxdCim +5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOe +sL4jpNrcyCse2m5FHomY2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj +0mRiikKYvLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+TtbNe/ +JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZbx39ri1UbSsUgYT2u +y1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+l2oBlKN8W4UdKjk60FSh0Tlxnf0h ++bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YKTE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsG +xVd7GYYKecsAyVKvQv83j+GjHno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwID +AQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEwDQYJKoZIhvcN +AQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG7BJ8dNVI0lkUmcDrudHr9Egw +W62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCkMpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWld +y8joRTnU+kLBEUx3XZL7av9YROXrgZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov ++BS5gLNdTaqX4fnkGMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDc +eqFS3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJmOzj/2ZQw +9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+l6mc1X5VTMbeRRAc6uk7 +nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6cJfTzPV4e0hz5sy229zdcxsshTrD3mUcY +hcErulWuBurQB7Lcq9CClnXO0lD+mefPL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB +60PZ2Pierc+xYw5F9KBaLJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fq +dBb9HxEGmpv0 +-----END CERTIFICATE----- + +Entrust Root Certification Authority - G4 +========================================= +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAwgb4xCzAJBgNV +BAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3Qu +bmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1 +dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eSAtIEc0MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYT +AlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eSAtIEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3D +umSXbcr3DbVZwbPLqGgZ2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV +3imz/f3ET+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j5pds +8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAMC1rlLAHGVK/XqsEQ +e9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73TDtTUXm6Hnmo9RR3RXRv06QqsYJn7 +ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNXwbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5X +xNMhIWNlUpEbsZmOeX7m640A2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV +7rtNOzK+mndmnqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8 +dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwlN4y6mACXi0mW +Hv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNjc0kCAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9n +MA0GCSqGSIb3DQEBCwUAA4ICAQAS5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4Q +jbRaZIxowLByQzTSGwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht +7LGrhFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/B7NTeLUK +YvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uIAeV8KEsD+UmDfLJ/fOPt +jqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbwH5Lk6rWS02FREAutp9lfx1/cH6NcjKF+ +m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKW +RGhXxNUzzxkvFMSUHHuk2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjA +JOgc47OlIQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk5F6G ++TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuYn/PIjhs4ViFqUZPT +kcpG2om3PVODLAgfi49T3f+sHw== +-----END CERTIFICATE----- + +Microsoft ECC Root Certificate Authority 2017 +============================================= +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQgRUND +IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4 +MjMxNjA0WjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZRogPZnZH6 +thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYbhGBKia/teQ87zvH2RPUB +eMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIy5lycFIM ++Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlf +Xu5gKcs68tvWMoQZP3zVL8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaR +eNtUjGUBiudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +Microsoft RSA Root Certificate Authority 2017 +============================================= +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQg +UlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIw +NzE4MjMwMDIzWjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u +MTYwNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZNt9GkMml +7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0ZdDMbRnMlfl7rEqUrQ7e +S0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw7 +1VdyvD/IybLeS2v4I2wDwAW9lcfNcztmgGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+ +dkC0zVJhUXAoP8XFWvLJjEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49F +yGcohJUcaDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaGYaRS +MLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6W6IYZVcSn2i51BVr +lMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4KUGsTuqwPN1q3ErWQgR5WrlcihtnJ +0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJ +ClTUFLkqqNfs+avNJVgyeY+QW5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZCLgLNFgVZJ8og +6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OCgMNPOsduET/m4xaRhPtthH80 +dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk ++ONVFT24bcMKpBLBaYVu32TxU5nhSnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex +/2kskZGT4d9Mozd2TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDy +AmH3pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGRxpl/j8nW +ZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiAppGWSZI1b7rCoucL5mxAyE +7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKT +c0QWbej09+CVgI+WXTik9KveCjCHk9hNAHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D +5KbvtwEwXlGjefVwaaZBRA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +e-Szigno Root CA 2017 +===================== +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNVBAYTAkhVMREw +DwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUt +MjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJvb3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZa +Fw00MjA4MjIxMjA3MDZaMHExCzAJBgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UE +CgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3pp +Z25vIFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtvxie+RJCx +s1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+HWyx7xf58etqjYzBhMA8G +A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSHERUI0arBeAyxr87GyZDv +vzAEwDAfBgNVHSMEGDAWgBSHERUI0arBeAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEA +tVfd14pVCzbhhkT61NlojbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxO +svxyqltZ+efcMQ== +-----END CERTIFICATE----- + +certSIGN Root CA G2 +=================== +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNVBAYTAlJPMRQw +EgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjAeFw0xNzAy +MDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJBgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lH +TiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAMDFdRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05 +N0IwvlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZuIt4Imfk +abBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhpn+Sc8CnTXPnGFiWeI8Mg +wT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKscpc/I1mbySKEwQdPzH/iV8oScLumZfNp +dWO9lfsbl83kqK/20U6o2YpxJM02PbyWxPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91Qqh +ngLjYl/rNUssuHLoPj1PrCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732 +jcZZroiFDsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fxDTvf +95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgyLcsUDFDYg2WD7rlc +z8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6CeWRgKRM+o/1Pcmqr4tTluCRVLERL +iohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud +DgQWBBSCIS1mxteg4BXrzkwJd8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOB +ywaK8SJJ6ejqkX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQlqiCA2ClV9+BB +/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0OJD7uNGzcgbJceaBxXntC6Z5 +8hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+cNywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5 +BiKDUyUM/FHE5r7iOZULJK2v0ZXkltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklW +atKcsWMy5WHgUyIOpwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tU +Sxfj03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZkPuXaTH4M +NMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE1LlSVHJ7liXMvGnjSG4N +0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MXQRBdJ3NghVdJIgc= +-----END CERTIFICATE----- + +Trustwave Global Certification Authority +======================================== +-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJV +UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2 +ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTAeFw0xNzA4MjMxOTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJV +UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2 +ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALldUShLPDeS0YLOvR29 +zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0XznswuvCAAJWX/NKSqIk4cXGIDtiLK0thAf +LdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4Bq +stTnoApTAbqOl5F2brz81Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9o +WN0EACyW80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotPJqX+ +OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1lRtzuzWniTY+HKE40 +Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfwhI0Vcnyh78zyiGG69Gm7DIwLdVcE +uE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm ++9jaJXLE9gCxInm943xZYkqcBW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqj +ifLJS3tBEW1ntwiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1UdDwEB/wQEAwIB +BjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W0OhUKDtkLSGm+J1WE2pIPU/H +PinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfeuyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0H +ZJDmHvUqoai7PF35owgLEQzxPy0QlG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla +4gt5kNdXElE1GYhBaCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5R +vbbEsLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPTMaCm/zjd +zyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qequ5AvzSxnI9O4fKSTx+O +856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxhVicGaeVyQYHTtgGJoC86cnn+OjC/QezH +Yj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu +3R3y4G5OBVixwJAWKqQ9EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP +29FpHOTKyeC2nOnOcXHebD8WpHk= +-----END CERTIFICATE----- + +Trustwave Global ECC P256 Certification Authority +================================================= +-----BEGIN CERTIFICATE----- +MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYDVQQGEwJVUzER +MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy +dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1 +NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABH77bOYj +43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoNFWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqm +P62jQzBBMA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt +0UrrdaVKEJmzsaGLSvcwCgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjz +RM4q3wghDDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 +-----END CERTIFICATE----- + +Trustwave Global ECC P384 Certification Authority +================================================= +-----BEGIN CERTIFICATE----- +MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJVUzER +MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy +dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4 +NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuBBAAiA2IABGvaDXU1CDFH +Ba5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJj9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr +/TklZvFe/oyujUF5nQlgziip04pt89ZF1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNV +HQ8BAf8EBQMDBwYAMB0GA1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNn +ADBkAjA3AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsCMGcl +CrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVuSw== +-----END CERTIFICATE-----